[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@2.3.1/schema.json\",\n  \"changelog\": [\"@changesets/changelog-github\", { \"repo\": \"bluesky-social/atproto\" }],\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": []\n}\n"
  },
  {
    "path": ".changeset/giant-goats-repeat.md",
    "content": "---\n'@atproto/bsky': patch\n---\n\nRefactor `FeatureGatesClient` to add `scope()` method and docs.\n"
  },
  {
    "path": ".changeset/nine-eyes-switch.md",
    "content": "---\n'@atproto/bsky': patch\n---\n\nRemove feature gate for new user onboarding\n"
  },
  {
    "path": ".dockerignore",
    "content": "node_modules\n**/dist\n.DS_Store\n.git\nDockerfile\n"
  },
  {
    "path": ".eslintignore",
    "content": "dist\nnode_modules\n\n# buf\npackages/bsky/src/proto\npackages/bsync/src/proto\n\n# codegen\npackages/api/src/client\npackages/bsky/src/lexicon\npackages/pds/src/lexicon\npackages/ozone/src/lexicon\n\n# @atproto/lex\npackages/lexicon-resolver/src/lexicons\npackages/lex/*/src/lexicons\npackages/lex/*/tests/lexicons\npackages/oauth/oauth-client-browser-example/src/lexicons\n\n# others\npackages/oauth/*/src/locales/*/messages.ts\npackages/oauth/oauth-provider-frontend/src/routeTree.gen.ts\npackages/oauth/oauth-client-expo/android/build\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"root\": true,\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/base\",\n    \"plugin:@typescript-eslint/eslint-recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:prettier/recommended\",\n    \"plugin:import/recommended\",\n    \"plugin:import/typescript\"\n  ],\n  \"plugins\": [\"n\"],\n  \"ignorePatterns\": [\"dist\", \"node_modules\"],\n  \"rules\": {\n    \"no-var\": \"error\",\n    \"prefer-const\": \"warn\",\n    \"no-misleading-character-class\": \"warn\",\n    \"eqeqeq\": [\"error\", \"always\", { \"null\": \"ignore\" }],\n    \"n/global-require\": \"error\",\n    \"n/no-extraneous-import\": \"error\",\n    \"n/prefer-node-protocol\": \"error\",\n    \"import/extensions\": [\"off\", \"ignorePackages\"],\n    \"import/export\": \"off\",\n    \"import/namespace\": \"off\",\n    \"import/no-deprecated\": \"off\",\n    \"import/no-absolute-path\": \"error\",\n    \"import/no-dynamic-require\": \"error\",\n    \"import/no-self-import\": \"error\",\n    \"import/order\": [\n      \"error\",\n      {\n        \"named\": true,\n        \"distinctGroup\": true,\n        \"alphabetize\": { \"order\": \"asc\" },\n        \"newlines-between\": \"never\",\n        \"groups\": [\n          \"builtin\",\n          \"external\",\n          \"internal\",\n          \"parent\",\n          [\"index\", \"sibling\"],\n          \"object\"\n        ]\n      }\n    ],\n    \"@typescript-eslint/no-unused-vars\": [\n      \"error\",\n      {\n        \"argsIgnorePattern\": \"^_\",\n        \"varsIgnorePattern\": \"^_\",\n        \"ignoreRestSiblings\": true\n      }\n    ],\n    \"@typescript-eslint/ban-ts-comment\": \"off\",\n    \"@typescript-eslint/no-empty-interface\": \"off\",\n    \"@typescript-eslint/explicit-module-boundary-types\": \"off\",\n    \"@typescript-eslint/no-empty-function\": \"off\",\n    \"@typescript-eslint/no-explicit-any\": \"off\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"jest.config.js\"],\n      \"env\": { \"commonjs\": true }\n    },\n    {\n      \"files\": [\"vite.config.js\", \"vite.config.cjs\", \"vite.config.mjs\"],\n      \"env\": { \"node\": true }\n    },\n    {\n      \"files\": [\"jest.setup.js\"],\n      \"env\": { \"jest\": true }\n    },\n    {\n      \"files\": [\"*.js\", \"*.cjs\"],\n      \"rules\": {\n        \"@typescript-eslint/no-var-requires\": \"off\"\n      }\n    },\n    {\n      \"files\": [\"**/*.test.ts\", \"**/tests/**/*.ts\"],\n      \"rules\": {\n        \"n/no-extraneous-import\": [\n          \"error\",\n          { \"allowModules\": [\"@atproto/dev-env\"] }\n        ]\n      }\n    }\n  ],\n  \"settings\": {\n    \"node\": { \"version\": \">=18.7.0\" },\n    \"import/internal-regex\": \"^@atproto(?:-labs)?/\",\n    \"import/parsers\": { \"@typescript-eslint/parser\": [\".ts\", \".tsx\"] },\n    \"import/resolver\": {\n      \"typescript\": {\n        \"project\": [\n          \"tsconfig.json\",\n          \"packages/lex/*/tsconfig.json\",\n          \"packages/oauth/*/tsconfig.json\",\n          \"packages/oauth/*/tsconfig.src.json\",\n          \"packages/internal/*/tsconfig.json\",\n          \"packages/*/tsconfig.json\"\n        ]\n      },\n      \"node\": {\n        \"extensions\": [\".js\", \".jsx\", \".json\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# buf\npackages/bsky/src/proto/** linguist-generated=true\npackages/bsync/src/proto/** linguist-generated=true\n\n# codegen\npackages/api/src/client/** linguist-generated=true\npackages/bsky/src/lexicon/** linguist-generated=true\npackages/pds/src/lexicon/** linguist-generated=true\npackages/ozone/src/lexicon/** linguist-generated=true\n\n# @atproto/lex\npackages/lexicon-resolver/src/lexicons/** linguist-generated=true\n\n# i18n\npackages/oauth/oauth-provider-ui/src/locales/**/messages.po linguist-generated=true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n---\n\n**Describe the bug**\n\n<!-- A clear and concise description of what the bug is. -->\n\n**To Reproduce**\n\nSteps to reproduce the behavior:\n\n1.\n\n**Expected behavior**\n\n<!-- A clear and concise description of what you expected to happen. -->\n\n**Details**\n\n- Operating system:\n- Node version:\n\n**Additional context**\n\n<!-- Add any other context about the problem here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature-request\nassignees: ''\n---\n\n**Is your feature request related to a problem? Please describe.**\n\n<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\n\n**Describe the solution you'd like**\n\n<!-- A clear and concise description of what you want to happen. -->\n\n**Describe alternatives you've considered**\n\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n**Additional context**\n\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/workflows/build-and-push-bsky-aws.yaml",
    "content": "name: build-and-push-bsky-aws\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}\n  USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}\n  PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}\n  IMAGE_NAME: bsky-app-view\n\njobs:\n  bsky-container-aws:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME}}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/bsky/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-bsky-ghcr.yaml",
    "content": "name: build-and-push-bsky-ghcr\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ghcr.io\n  USERNAME: ${{ github.actor }}\n  PASSWORD: ${{ secrets.GITHUB_TOKEN }}\n\n  # github.repository as <account>/<repo>\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  bsky-container-ghcr:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME }}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=bsky:,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/bsky/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-bsync-aws.yaml",
    "content": "name: build-and-push-bsync-aws\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}\n  USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}\n  PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}\n  IMAGE_NAME: bsync\n\njobs:\n  bsync-container-aws:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME}}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/bsync/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-bsync-ghcr.yaml",
    "content": "name: build-and-push-bsync-ghcr\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ghcr.io\n  USERNAME: ${{ github.actor }}\n  PASSWORD: ${{ secrets.GITHUB_TOKEN }}\n\n  # github.repository as <account>/<repo>\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  bsync-container-ghcr:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME }}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=bsync:,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/bsync/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-ozone-aws.yaml",
    "content": "name: build-and-push-ozone-aws\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}\n  USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}\n  PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}\n  IMAGE_NAME: ozone\n\njobs:\n  ozone-container-aws:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME}}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/ozone/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-ozone-ghcr.yaml",
    "content": "name: build-and-push-ozone-ghcr\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ghcr.io\n  USERNAME: ${{ github.actor }}\n  PASSWORD: ${{ secrets.GITHUB_TOKEN }}\n\n  # github.repository as <account>/<repo>\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  ozone-container-ghcr:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME }}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=ozone:,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/ozone/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-pds-aws.yaml",
    "content": "name: build-and-push-pds-aws\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}\n  USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}\n  PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}\n  IMAGE_NAME: pds\n\njobs:\n  pds-container-aws:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME}}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/pds/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/build-and-push-pds-ghcr.yaml",
    "content": "name: build-and-push-pds-ghcr\non:\n  push:\n    branches:\n      - main\n\nenv:\n  REGISTRY: ghcr.io\n  USERNAME: ${{ github.actor }}\n  PASSWORD: ${{ secrets.GITHUB_TOKEN }}\n\n  # github.repository as <account>/<repo>\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  pds-container-ghcr:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Docker buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v2\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ env.USERNAME }}\n          password: ${{ env.PASSWORD }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: |\n            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=sha,enable=true,priority=100,prefix=pds:,suffix=,format=long\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: ${{ github.event_name != 'pull_request' }}\n          file: ./services/pds/Dockerfile\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/claude.yaml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.\n          # prompt: 'Update the pull request description to include a summary of changes.'\n\n          # Optional: Add claude_args to customize behavior and configuration\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://code.claude.com/docs/en/cli-reference for available options\n          # claude_args: '--allowed-tools Bash(gh pr:*)'\n\n          # NOTE(sfn): we can add a custom system prompt here\n\n          claude_args: |\n            --model claude-opus-4-5-20251101\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "name: Publish\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  id-token: write\n  contents: write\n  pull-requests: write\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\njobs:\n  build:\n    name: Publish\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v6\n        with:\n          cache: pnpm\n          node-version-file: '.nvmrc'\n          registry-url: 'https://registry.npmjs.org'\n      - run: npm install --global npm@latest\n      - run: pnpm install --frozen-lockfile\n        env:\n          PUPPETEER_SKIP_DOWNLOAD: true\n      - name: Publish\n        uses: changesets/action@v1\n        id: changesets\n        with:\n          publish: pnpm release\n          version: pnpm run version-packages\n          commit: 'Version packages'\n          title: 'Version packages'\n"
  },
  {
    "path": ".github/workflows/repo.yaml",
    "content": "name: Repository CI\n\non:\n  pull_request:\n    branches:\n      - '*'\n\nconcurrency:\n  group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v6\n        with:\n          cache: pnpm\n          node-version-file: '.nvmrc'\n      - run: pnpm install --frozen-lockfile\n        env:\n          PUPPETEER_SKIP_DOWNLOAD: true\n      - run: pnpm build\n      - uses: actions/upload-artifact@v4\n        with:\n          name: dist\n          retention-days: 2\n          path: |\n            packages/*/dist\n            packages/*/*/dist\n            packages/lex/*/src/lexicons\n            packages/lex/*/tests/lexicons\n            packages/oauth/oauth-client-browser-example/src/lexicons\n            packages/oauth/*/src/locales/*/messages.ts\n\n  changeset:\n    name: Changeset\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0 # needed for git diff against base branch\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v6\n        with:\n          cache: pnpm\n          node-version-file: '.nvmrc'\n      - run: pnpm install --frozen-lockfile\n        env:\n          PUPPETEER_SKIP_DOWNLOAD: true\n      - run: pnpm changeset status --since=origin/${{ github.base_ref }}\n\n  test:\n    name: Test\n    needs: build\n    runs-on: ubuntu-22.04\n    # Puppeteer does not work in recent Ubuntu versions without a workaround due\n    # to sandboxing issues. Using \"ubuntu-latest\" results in the following\n    # error:\n    #\n    # No usable sandbox! If you are running on Ubuntu 23.10+ or another Linux\n    # distro that has disabled unprivileged user namespaces with AppArmor, see\n    # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md.\n    # Otherwise see\n    # https://chromium.googlesource.com/chromium/src/+/main/docs/linux/suid_sandbox_development.md\n    # for more information on developing with the (older) SUID sandbox. If you\n    # want to live dangerously and need an immediate workaround, you can try\n    # using --no-sandbox.\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8]\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v6\n        with:\n          cache: pnpm\n          node-version-file: '.nvmrc'\n      - run: echo \"CURRENT_MONTH=$(date +'%Y-%m')\" >> $GITHUB_ENV\n      - uses: actions/cache@v4\n        name: Cache Puppeteer browser binaries\n        with:\n          path: ~/.cache/puppeteer\n          key: ${{ env.CURRENT_MONTH }}-${{ runner.os }}-${{ runner.arch }}\n      - run: pnpm install --frozen-lockfile\n      - uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: packages\n      - run: pnpm test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests\n\n  verify:\n    name: Verify\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v6\n        with:\n          cache: pnpm\n          node-version-file: '.nvmrc'\n      - run: pnpm install --frozen-lockfile\n        env:\n          PUPPETEER_SKIP_DOWNLOAD: true\n      - uses: actions/download-artifact@v4\n        with:\n          name: dist\n          path: packages\n      - run: pnpm verify\n"
  },
  {
    "path": ".github/workflows/sync-internal.yaml",
    "content": "name: Sync to internal repo\n\non:\n  push:\n    branches: [main]\n\njobs:\n  sync:\n    if: github.repository == 'bluesky-social/atproto'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout public repo\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Generate GitHub App Token\n        id: app-token\n        uses: actions/create-github-app-token@v1\n        with:\n          app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}\n          private-key: ${{ secrets.SYNC_INTERNAL_PK }}\n          repositories: atproto-internal\n      - name: Push to internal repo\n        env:\n          TOKEN: ${{ steps.app-token.outputs.token }}\n        run: |\n          git config user.name \"github-actions\"\n          git config user.email \"test@users.noreply.github.com\"\n          git config --unset-all http.https://github.com/.extraheader\n          git remote add internal https://x-access-token:${TOKEN}@github.com/bluesky-social/atproto-internal.git\n          git push internal main --force\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nlerna-debug.log\nnpm-debug.log\nyarn-error.log\npackages/**/dist\n.idea\npackages/*/coverage\ntest.sqlite\n.DS_Store\n*.log\n*.tsbuildinfo\n.*.env\n.env.*\n.env\n\\#*\\#\n*~\n*.swp\n.claude/\ncoverage\n"
  },
  {
    "path": ".npmrc",
    "content": "enable-pre-post-scripts = true\ninclude-workspace-root = true\n"
  },
  {
    "path": ".nvmrc",
    "content": "22\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\ninterop-test-files\n__snapshots__\ndist\npnpm-lock.yaml\n.pnpm*\n.changeset\nCHANGELOG.md\n\n# buf\npackages/bsky/src/proto\npackages/bsync/src/proto\n\n# codegen\npackages/api/src/client\npackages/bsky/src/lexicon\npackages/pds/src/lexicon\npackages/ozone/src/lexicon\n\n# @atproto/lex\npackages/lexicon-resolver/src/lexicons\npackages/lex/*/src/lexicons\npackages/lex/*/tests/lexicons\npackages/oauth/oauth-client-browser-example/src/lexicons\n\n# others\npackages/oauth/*/src/locales/*/messages.ts\npackages/oauth/oauth-provider-frontend/src/routeTree.gen.ts\npackages/oauth/oauth-client-expo/android/build\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"all\",\n  \"tabWidth\": 2,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"plugins\": [\"prettier-plugin-tailwindcss\"],\n  \"overrides\": [\n    {\n      \"files\": \"*.hbs\",\n      \"options\": {\n        \"singleQuote\": false\n      }\n    },\n    {\n      \"files\": [\".eslintrc\"],\n      \"options\": {\n        \"parser\": \"json\",\n        \"trailingComma\": \"none\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"wengerk.highlight-bad-chars\",\n    \"esbenp.prettier-vscode\",\n    \"streetsidesoftware.code-spell-checker\",\n    \"vitest.explorer\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"cSpell.language\": \"en,en-US\",\n  \"cSpell.words\": [\n    \"algs\",\n    \"appview\",\n    \"atproto\",\n    \"blockstore\",\n    \"bluesky\",\n    \"bsky\",\n    \"bsync\",\n    \"cbor\",\n    \"clsx\",\n    \"consolas\",\n    \"dpop\",\n    \"googleusercontent\",\n    \"hcaptcha\",\n    \"hexeditor\",\n    \"ingester\",\n    \"insertable\",\n    \"ipld\",\n    \"jwks\",\n    \"keypair\",\n    \"kysely\",\n    \"merkle\",\n    \"msid\",\n    \"multibase\",\n    \"multiformats\",\n    \"nameserver\",\n    \"oidc\",\n    \"pkce\",\n    \"ponyfill\",\n    \"proxied\",\n    \"ssrf\",\n    \"undici\",\n    \"webcrypto\",\n    \"whatwg\",\n    \"xrpc\"\n  ],\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\",\n    \"source.sortImports\": \"never\"\n  },\n  \"files.associations\": {\n    \"**/tsconfig/*.json\": \"jsonc\"\n  },\n  \"files.defaultLanguage\": \"ts\",\n  \"files.insertFinalNewline\": true,\n  \"files.trimTrailingWhitespace\": true,\n  \"prettier.semi\": false,\n  \"prettier.singleQuote\": true,\n  \"prettier.trailingComma\": \"es5\",\n  \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
  },
  {
    "path": "CONTRIBUTORS.md",
    "content": "# Contributors\n\nATProto receives so many contributions that we could never list everyone who deserves it. However, in this document we want to give special thanks to contributors who solve particularly difficult tasks, send security disclosures, or in some way deserve recognition.\n\n### The AT Protocol maintainers give their thanks to:\n\n#### [rmcan](https://github.com/rmcan), Security disclosure, April 2023\n\n#### [ianklatzco](https://github.com/ianklatzco), Security disclosure, April 2023\n\n#### lily, Security disclosure, May 2023\n\n#### [april](https://github.com/april), Security disclosure, May 2023\n\n#### [TowhidKashem](https://github.com/TowhidKashem), Security disclosure, May 2023\n\n#### [DavidBuchanan314](https://github.com/DavidBuchanan314), Security disclosure, May 2023\n\n#### [goeo\\_](https://bsky.app/profile/did:web:genco.me), Security disclosure, May 2024\n\n#### [DavidBuchanan314](https://github.com/DavidBuchanan314), Security disclosure, November 2024\n\n#### [daniel](https://hackerone.com/daniel), Security disclosure, November 2024\n\n#### [imax](https://github.com/imax9000), Security disclosure, January 2025\n\n#### [avivkeller](https://github.com/avivkeller), Security disclosure, December 2025\n"
  },
  {
    "path": "LICENSE-APACHE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "LICENSE-MIT.txt",
    "content": "MIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Dual MIT/Apache-2.0 License\n\nCopyright (c) 2022-2026 Bluesky Social PBC, and Contributors\n\nExcept 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>).\n\nDownstream 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.\n"
  },
  {
    "path": "Makefile",
    "content": "\nSHELL = /bin/bash\n.SHELLFLAGS = -o pipefail -c\n\n.PHONY: help\nhelp: ## Print info about all commands\n\t@echo \"Helper Commands:\"\n\t@echo\n\t@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = \":.*?## \"}; {printf \"    \\033[01;32m%-20s\\033[0m %s\\n\", $$1, $$2}'\n\t@echo\n\t@echo \"NOTE: dependencies between commands are not automatic. Eg, you must run 'deps' and 'build' first, and after any changes\"\n\n.PHONY: build\nbuild: ## Compile all modules\n\tpnpm build\n\n.PHONY: test\ntest: ## Run all tests\n\tpnpm test\n\n.PHONY: run-dev-env\nrun-dev-env: ## Run a \"development environment\" shell\n\tcd packages/dev-env; NODE_ENV=development pnpm run start\n\n.PHONY: run-dev-env-logged\nrun-dev-env-logged: ## Run a \"development environment\" shell (with logging)\n\tcd packages/dev-env; LOG_ENABLED=true NODE_ENV=development pnpm run start | pnpm exec pino-pretty\n\n.PHONY: codegen\ncodegen: ## Re-generate packages from lexicon/ files\n\tpnpm codegen\n\n.PHONY: lint\nlint: ## Run style checks and verify syntax\n\tpnpm verify\n\n.PHONY: fmt\nfmt: ## Run syntax re-formatting\n\tpnpm format\n\n.PHONY: fmt-lexicons\nfmt-lexicons: ## Run syntax re-formatting, just on .json files\n\tpnpm exec eslint ./lexicons/ --ext .json --fix\n\n.PHONY: deps\ndeps: ## Installs dependent libs using 'pnpm install'\n\tpnpm install --frozen-lockfile\n\n.PHONY: clean\nclean: ## Deletes all 'dist' and 'node_package' directories (including nested)\n\trm -rf **/dist **/node_packages\n\n.PHONY: nvm-setup\nnvm-setup: ## Use NVM to install and activate node+pnpm\n\tnvm install 18\n\tnvm use 18\n\tcorepack enable\n"
  },
  {
    "path": "README.md",
    "content": "# AT Protocol Reference Implementation (TypeScript)\n\nWelcome friends!\n\nThis repository contains Bluesky's reference implementation of AT Protocol, and of the `app.bsky` microblogging application service backend.\n\n## What is in here?\n\n**TypeScript Packages:**\n\n| Package                                                                       | Docs                                       | NPM                                                                                                             |\n| ----------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |\n| `@atproto/api`: client library                                                | [README](./packages/api/README.md)         | [![NPM](https://img.shields.io/npm/v/@atproto/api)](https://www.npmjs.com/package/@atproto/api)                 |\n| `@atproto/common-web`: shared code and helpers which can run in web browsers  | [README](./packages/common-web/README.md)  | [![NPM](https://img.shields.io/npm/v/@atproto/common-web)](https://www.npmjs.com/package/@atproto/common-web)   |\n| `@atproto/common`: shared code and helpers which doesn't work in web browsers | [README](./packages/common/README.md)      | [![NPM](https://img.shields.io/npm/v/@atproto/common)](https://www.npmjs.com/package/@atproto/common)           |\n| `@atproto/crypto`: cryptographic signing and key serialization                | [README](./packages/crypto/README.md)      | [![NPM](https://img.shields.io/npm/v/@atproto/crypto)](https://www.npmjs.com/package/@atproto/crypto)           |\n| `@atproto/identity`: DID and handle resolution                                | [README](./packages/identity/README.md)    | [![NPM](https://img.shields.io/npm/v/@atproto/identity)](https://www.npmjs.com/package/@atproto/identity)       |\n| `@atproto/lexicon`: schema definition language                                | [README](./packages/lexicon/README.md)     | [![NPM](https://img.shields.io/npm/v/@atproto/lexicon)](https://www.npmjs.com/package/@atproto/lexicon)         |\n| `@atproto/repo`: data storage structure, including MST                        | [README](./packages/repo/README.md)        | [![NPM](https://img.shields.io/npm/v/@atproto/repo)](https://www.npmjs.com/package/@atproto/repo)               |\n| `@atproto/syntax`: string parsers for identifiers                             | [README](./packages/syntax/README.md)      | [![NPM](https://img.shields.io/npm/v/@atproto/syntax)](https://www.npmjs.com/package/@atproto/syntax)           |\n| `@atproto/xrpc`: client-side HTTP API helpers                                 | [README](./packages/xrpc/README.md)        | [![NPM](https://img.shields.io/npm/v/@atproto/xrpc)](https://www.npmjs.com/package/@atproto/xrpc)               |\n| `@atproto/xrpc-server`: server-side HTTP API helpers                          | [README](./packages/xrpc-server/README.md) | [![NPM](https://img.shields.io/npm/v/@atproto/xrpc-server)](https://www.npmjs.com/package/@atproto/xrpc-server) |\n\n**TypeScript Services:**\n\n- `pds`: \"Personal Data Server\", hosting repo content for atproto accounts. Most implementation code in `packages/pds`, with runtime wrapper in `services/pds`. See [bluesky-social/pds](https://github.com/bluesky-social/pds) for directions on self-hosting.\n- `bsky`: AppView implementation of the `app.bsky.*` API endpoints. Running on main network at `api.bsky.app`. Most implementation code in `packages/bsky`, with runtime wrapper in `services/bsky`.\n\n**Lexicons:** for both the `com.atproto.*` and `app.bsky.*` are canonically versioned in this repo, for now, under `./lexicons/`. These are JSON files in the [Lexicon schema definition language](https://atproto.com/specs/lexicon), similar to JSON Schema or OpenAPI.\n\n**Interoperability Test Data:** the language-neutral test files in `./interop-test-files/` may be useful for other protocol implementations to ensure that they follow the specification correctly\n\nThe source code for the Bluesky Social client app (for web and mobile) can be found at [bluesky-social/social-app](https://github.com/bluesky-social/social-app).\n\nGo programming language source code is in [bluesky-social/indigo](https://github.com/bluesky-social/indigo), including the BGS implementation.\n\n## Developer Quickstart\n\nWe recommend [`nvm`](https://github.com/nvm-sh/nvm) for managing Node.js installs. This project requires Node.js version 18. `pnpm` is used to manage the workspace of multiple packages. You can install it with `npm install --global pnpm`.\n\nThere is a Makefile which can help with basic development tasks:\n\n```shell\n# use existing nvm to install node 18 and pnpm\nmake nvm-setup\n\n# pull dependencies and build all local packages\nmake deps\nmake build\n\n# run the tests, using Docker services as needed\nmake test\n\n# run a local PDS and AppView with fake test accounts and data\n# (this requires a global installation of `jq` and `docker`)\nmake run-dev-env\n\n# show all other commands\nmake help\n```\n\n## About AT Protocol\n\nThe Authenticated Transfer Protocol (\"ATP\" or \"atproto\") is a decentralized social media protocol, developed by [Bluesky Social PBC](https://bsky.social). Learn more at:\n\n- [Overview and Guides](https://atproto.com/guides/overview) 👈 Best starting point\n- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions\n- [Protocol Specifications](https://atproto.com/specs/atp)\n- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol)\n\nThe Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these \"Lexicons\" is `app.bsky.*`.\n\n## Contributions\n\n> While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.\n\n**Rules:**\n\n- We may not respond to your issue or PR.\n- We may close an issue or PR without much feedback.\n- We may lock discussions or contributions if our attention is getting DDOSed.\n- We do not provide support for build issues.\n\n**Guidelines:**\n\n- Check for existing issues before filing a new one, please.\n- Open an issue and give some time for discussion before submitting a PR.\n- If submitting a PR that includes a lexicon change, please get sign off on the lexicon change _before_ doing the implementation.\n- Issues are for bugs & feature requests related to the TypeScript implementation of atproto and related services.\n  - For high-level discussions, please use the [Discussion Forum](https://github.com/bluesky-social/atproto/discussions).\n  - For client issues, please use the relevant [social-app](https://github.com/bluesky-social/social-app) repo.\n- Stay away from PRs that:\n  - Refactor large parts of the codebase\n  - Add entirely new features without prior discussion\n  - Change the tooling or frameworks used without prior discussion\n  - Introduce new unnecessary dependencies\n\nRemember, we serve a wide community of users. Our day-to-day involves us constantly asking \"which top priority is our top priority.\" If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth.\n\n## Are you a developer interested in building on atproto?\n\nBluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more.\n\n## Security disclosures\n\nIf you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team, and we'll respond promptly. See [SECURITY.md](https://github.com/bluesky-social/atproto/blob/main/SECURITY.md) for more info.\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n\nBluesky 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).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nPlease do NOT report possible security vulnerabilities in public channels such as GitHub Issues. If you believe you have found a security vulnerability, please email us at `security@bsky.app` with a description of the issue.\n\nWe will acknowledge the vulnerability as soon as possible - within 3 business days - and follow up when a fix lands. Please avoid discussing the vulnerability until we do so.\n\nWith your consent, we will add you to the repository [CONTRIBUTORS](https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md) file.\n"
  },
  {
    "path": "interop-test-files/README.md",
    "content": "\natproto Interop Test Files\n==========================\n\nThis directory contains reusable files for testing interoperability and specification compliance for atproto (AT Protocol).\n\nThe protocol itself is documented at <https://atproto.com/specs/atp>. If there are conflicts or ambiguity between these test files and the specs, the specs are the authority, and these test files should usually be corrected.\n\nThese files are intended to be simple (JSON, text files, etc) and mostly self-documenting.\n"
  },
  {
    "path": "interop-test-files/crypto/signature-fixtures.json",
    "content": "[\n  {\n    \"comment\": \"valid P-256 key and signature, with low-S signature\",\n    \"messageBase64\": \"oWVoZWxsb2V3b3JsZA\",\n    \"algorithm\": \"ES256\",\n    \"didDocSuite\": \"EcdsaSecp256r1VerificationKey2019\",\n    \"publicKeyDid\": \"did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo\",\n    \"publicKeyMultibase\": \"zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh\",\n    \"signatureBase64\": \"2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg\",\n    \"validSignature\": true,\n    \"tags\": []\n  },\n  {\n    \"comment\": \"valid K-256 key and signature, with low-S signature\",\n    \"messageBase64\": \"oWVoZWxsb2V3b3JsZA\",\n    \"algorithm\": \"ES256K\",\n    \"didDocSuite\": \"EcdsaSecp256k1VerificationKey2019\",\n    \"publicKeyDid\": \"did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc\",\n    \"publicKeyMultibase\": \"z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t\",\n    \"signatureBase64\": \"5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA\",\n    \"validSignature\": true,\n    \"tags\": []\n  },\n  {\n    \"comment\": \"P-256 key and signature, with non-low-S signature which is invalid in atproto\",\n    \"messageBase64\": \"oWVoZWxsb2V3b3JsZA\",\n    \"algorithm\": \"ES256\",\n    \"didDocSuite\": \"EcdsaSecp256r1VerificationKey2019\",\n    \"publicKeyDid\": \"did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo\",\n    \"publicKeyMultibase\": \"zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh\",\n    \"signatureBase64\": \"2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw\",\n    \"validSignature\": false,\n    \"tags\": [\"high-s\"]\n  },\n  {\n    \"comment\": \"K-256 key and signature, with non-low-S signature which is invalid in atproto\",\n    \"messageBase64\": \"oWVoZWxsb2V3b3JsZA\",\n    \"algorithm\": \"ES256K\",\n    \"didDocSuite\": \"EcdsaSecp256k1VerificationKey2019\",\n    \"publicKeyDid\": \"did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc\",\n    \"publicKeyMultibase\": \"z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t\",\n    \"signatureBase64\": \"5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ\",\n    \"validSignature\": false,\n    \"tags\": [\"high-s\"]\n  },\n  {\n    \"comment\": \"P-256 key and signature, with DER-encoded signature which is invalid in atproto\",\n    \"messageBase64\": \"oWVoZWxsb2V3b3JsZA\",\n    \"algorithm\": \"ES256\",\n    \"didDocSuite\": \"EcdsaSecp256r1VerificationKey2019\",\n    \"publicKeyDid\": \"did:key:zDnaeT6hL2RnTdUhAPLij1QBkhYZnmuKyM7puQLW1tkF4Zkt8\",\n    \"publicKeyMultibase\": \"ze8N2PPxnu19hmBQ58t5P3E9Yj6CqakJmTVCaKvf9Byq2\",\n    \"signatureBase64\": \"MEQCIFxYelWJ9lNcAVt+jK0y/T+DC/X4ohFZ+m8f9SEItkY1AiACX7eXz5sgtaRrz/SdPR8kprnbHMQVde0T2R8yOTBweA\",\n    \"validSignature\": false,\n    \"tags\": [\"der-encoded\"]\n  },\n  {\n    \"comment\": \"K-256 key and signature, with DER-encoded signature which is invalid in atproto\",\n    \"messageBase64\": \"oWVoZWxsb2V3b3JsZA\",\n    \"algorithm\": \"ES256K\",\n    \"didDocSuite\": \"EcdsaSecp256k1VerificationKey2019\",\n    \"publicKeyDid\": \"did:key:zQ3shnriYMXc8wvkbJqfNWh5GXn2bVAeqTC92YuNbek4npqGF\",\n    \"publicKeyMultibase\": \"z22uZXWP8fdHXi4jyx8cCDiBf9qQTsAe6VcycoMQPfcMQX\",\n    \"signatureBase64\": \"MEUCIQCWumUqJqOCqInXF7AzhIRg2MhwRz2rWZcOEsOjPmNItgIgXJH7RnqfYY6M0eg33wU0sFYDlprwdOcpRn78Sz5ePgk\",\n    \"validSignature\": false,\n    \"tags\": [\"der-encoded\"]\n  }\n]\n"
  },
  {
    "path": "interop-test-files/crypto/w3c_didkey_K256.json",
    "content": "[\n  {\n    \"privateKeyBytesHex\": \"9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c\",\n    \"publicDidKey\": \"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme\"\n  },\n  {\n    \"privateKeyBytesHex\": \"f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed\",\n    \"publicDidKey\": \"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2\"\n  },\n  {\n    \"privateKeyBytesHex\": \"6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02\",\n    \"publicDidKey\": \"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N\"\n  },\n  {\n    \"privateKeyBytesHex\": \"c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15\",\n    \"publicDidKey\": \"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy\"\n  },\n  {\n    \"privateKeyBytesHex\": \"175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133\",\n    \"publicDidKey\": \"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj\"\n  }\n]\n"
  },
  {
    "path": "interop-test-files/crypto/w3c_didkey_P256.json",
    "content": "[\n  {\n    \"privateKeyBytesBase58\": \"9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp\",\n    \"publicDidKey\": \"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb\"\n  }\n]\n"
  },
  {
    "path": "interop-test-files/syntax/atidentifier_syntax_invalid.txt",
    "content": "\n# invalid handles\ndid:thing.test\ndid:thing\njohn-.test\njohn.0\njohn.-\nxn--bcher-.tld\njohn..test\njo_hn.test\n\n# invalid DIDs\ndid\ndidmethodval\nmethod:did:val\ndid:method:\ndidmethod:val\ndid:methodval)\n:did:method:val\ndid:method:val:\ndid:method:val%\nDID:method:val\n\n# other invalid stuff\nemail@example.com\n@handle@example.com\n@handle\nblah\n"
  },
  {
    "path": "interop-test-files/syntax/atidentifier_syntax_valid.txt",
    "content": "\n# allows valid handles\nXX.LCS.MIT.EDU\njohn.test\njan.test\na234567890123456789.test\njohn2.test\njohn-john.test\n\n# allows valid DIDs\ndid:method:val\ndid:method:VAL\ndid:method:val123\ndid:method:123\ndid:method:val-two\n"
  },
  {
    "path": "interop-test-files/syntax/aturi_syntax_invalid.txt",
    "content": "\n# enforces spec basics\na://did:plc:asdf123\nat//did:plc:asdf123\nat:/a/did:plc:asdf123\nat:/did:plc:asdf123\nAT://did:plc:asdf123\nhttp://did:plc:asdf123\n://did:plc:asdf123\nat:did:plc:asdf123\nat:/did:plc:asdf123\nat:///did:plc:asdf123\nat://:/did:plc:asdf123\nat:/ /did:plc:asdf123\nat://did:plc:asdf123 \nat://did:plc:asdf123/ \n at://did:plc:asdf123\nat://did:plc:asdf123/com.atproto.feed.post \nat://did:plc:asdf123/com.atproto.feed.post# \nat://did:plc:asdf123/com.atproto.feed.post#/ \nat://did:plc:asdf123/com.atproto.feed.post#/frag \nat://did:plc:asdf123/com.atproto.feed.post#fr ag\n//did:plc:asdf123\nat://name\nat://name.0\nat://diD:plc:asdf123\nat://did:plc:asdf123/com.atproto.feed.p@st\nat://did:plc:asdf123/com.atproto.feed.p$st\nat://did:plc:asdf123/com.atproto.feed.p%st\nat://did:plc:asdf123/com.atproto.feed.p&st\nat://did:plc:asdf123/com.atproto.feed.p()t\nat://did:plc:asdf123/com.atproto.feed_post\nat://did:plc:asdf123/-com.atproto.feed.post\nat://did:plc:asdf@123/com.atproto.feed.post\nat://DID:plc:asdf123\nat://user.bsky.123\nat://bsky\nat://did:plc:\nat://did:plc:\nat://frag\n\n# too long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200)\nat://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\n\n# has specified behavior on edge cases\nat://user.bsky.social//\nat://user.bsky.social//com.atproto.feed.post\nat://user.bsky.social/com.atproto.feed.post//\nat://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more',\nat://did:plc:asdf123/short/stuff\nat://did:plc:asdf123/12345\n\n# enforces no trailing slashes\nat://did:plc:asdf123/\nat://user.bsky.social/\nat://did:plc:asdf123/com.atproto.feed.post/\nat://did:plc:asdf123/com.atproto.feed.post/record/\nat://did:plc:asdf123/com.atproto.feed.post/record/#/frag\n\n# enforces strict paths\nat://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf\n\n# is very permissive about fragments\nat://did:plc:asdf123#\nat://did:plc:asdf123##\n#at://did:plc:asdf123\nat://did:plc:asdf123#/asdf#/asdf\n\n# new less permissive about record keys for Lexicon use (with recordkey more specified)\nat://did:plc:asdf123/com.atproto.feed.post/%23\nat://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123\nat://did:plc:asdf123/com.atproto.feed.post/~'sdf123\")\nat://did:plc:asdf123/com.atproto.feed.post/$\nat://did:plc:asdf123/com.atproto.feed.post/@\nat://did:plc:asdf123/com.atproto.feed.post/!\nat://did:plc:asdf123/com.atproto.feed.post/*\nat://did:plc:asdf123/com.atproto.feed.post/(\nat://did:plc:asdf123/com.atproto.feed.post/,\nat://did:plc:asdf123/com.atproto.feed.post/;\nat://did:plc:asdf123/com.atproto.feed.post/abc%30123\nat://did:plc:asdf123/com.atproto.feed.post/%30\nat://did:plc:asdf123/com.atproto.feed.post/%3\nat://did:plc:asdf123/com.atproto.feed.post/%\nat://did:plc:asdf123/com.atproto.feed.post/%zz\nat://did:plc:asdf123/com.atproto.feed.post/%%%\n\n# disallow dot / double-dot\nat://did:plc:asdf123/com.atproto.feed.post/.\nat://did:plc:asdf123/com.atproto.feed.post/..\n"
  },
  {
    "path": "interop-test-files/syntax/aturi_syntax_valid.txt",
    "content": "\n# enforces spec basics\nat://did:plc:asdf123\nat://user.bsky.social\nat://did:plc:asdf123/com.atproto.feed.post\nat://did:plc:asdf123/com.atproto.feed.post/record\n\n# very long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(512)\nat://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\n\n# enforces no trailing slashes\nat://did:plc:asdf123\nat://user.bsky.social\nat://did:plc:asdf123/com.atproto.feed.post\nat://did:plc:asdf123/com.atproto.feed.post/record\n\n# enforces strict paths\nat://did:plc:asdf123/com.atproto.feed.post/asdf123\n\n# is very permissive about record keys\nat://did:plc:asdf123/com.atproto.feed.post/asdf123\nat://did:plc:asdf123/com.atproto.feed.post/a\n\nat://did:plc:asdf123/com.atproto.feed.post/asdf-123\nat://did:abc:123\nat://did:abc:123/io.nsid.someFunc/record-key\n\nat://did:abc:123/io.nsid.someFunc/self.\nat://did:abc:123/io.nsid.someFunc/lang:\nat://did:abc:123/io.nsid.someFunc/:\nat://did:abc:123/io.nsid.someFunc/-\nat://did:abc:123/io.nsid.someFunc/_\nat://did:abc:123/io.nsid.someFunc/~\nat://did:abc:123/io.nsid.someFunc/...\nat://did:plc:asdf123/com.atproto.feed.postV2\n"
  },
  {
    "path": "interop-test-files/syntax/datetime_parse_invalid.txt",
    "content": "# superficial syntax parses ok, but are not valid datetimes for semantic reasons (eg, \"month zero\")\n1985-00-12T23:20:50.123Z\n1985-04-00T23:20:50.123Z\n1985-13-12T23:20:50.123Z\n1985-04-12T25:20:50.123Z\n1985-04-12T23:99:50.123Z\n1985-04-12T23:20:61.123Z\n"
  },
  {
    "path": "interop-test-files/syntax/datetime_syntax_invalid.txt",
    "content": "\n# subtle changes to: 1985-04-12T23:20:50.123Z\n1985-04-12T23:20:50.123z\n01985-04-12T23:20:50.123Z\n985-04-12T23:20:50.123Z\n1985-04-12T23:20:50.Z\n1985-04-32T23;20:50.123Z\n1985-04-32T23;20:50.123Z\n\n# en-dash and em-dash\n1985—04-32T23;20:50.123Z\n1985–04-32T23;20:50.123Z\n\n# whitespace\n 1985-04-12T23:20:50.123Z\n1985-04-12T23:20:50.123Z \n1985-04-12T 23:20:50.123Z\n\n# not enough zero padding\n1985-4-12T23:20:50.123Z\n1985-04-2T23:20:50.123Z\n1985-04-12T3:20:50.123Z\n1985-04-12T23:0:50.123Z\n1985-04-12T23:20:5.123Z\n\n# too much zero padding\n01985-04-12T23:20:50.123Z\n1985-004-12T23:20:50.123Z\n1985-04-012T23:20:50.123Z\n1985-04-12T023:20:50.123Z\n1985-04-12T23:020:50.123Z\n1985-04-12T23:20:050.123Z\n\n# strict capitalization (ISO-8601)\n1985-04-12t23:20:50.123Z\n1985-04-12T23:20:50.123z\n\n# RFC-3339, but not ISO-8601\n1985-04-12T23:20:50.123-00:00\n1985-04-12_23:20:50.123Z\n1985-04-12 23:20:50.123Z\n\n# ISO-8601, but weird\n1985-04-274T23:20:50.123Z\n\n# timezone is required\n1985-04-12T23:20:50.123\n1985-04-12T23:20:50\n\n1985-04-12\n1985-04-12T23:20Z\n1985-04-12T23:20:5Z\n1985-04-12T23:20:50.123\n+001985-04-12T23:20:50.123Z\n23:20:50.123Z\n\n1985-04-12T23:20:50.123+00\n1985-04-12T23:20:50.123+00:0\n1985-04-12T23:20:50.123+0:00\n1985-04-12T23:20:50.123\n1985-04-12T23:20:50.123+0000\n1985-04-12T23:20:50.123+00\n1985-04-12T23:20:50.123+\n1985-04-12T23:20:50.123-\n\n# ISO-8601, but normalizes to a negative time\n0000-01-01T00:00:00+01:00\n-000001-12-31T23:00:00.000Z\n"
  },
  {
    "path": "interop-test-files/syntax/datetime_syntax_valid.txt",
    "content": "# \"preferred\"\n1985-04-12T23:20:50.123Z\n1985-04-12T23:20:50.000Z\n2000-01-01T00:00:00.000Z\n1985-04-12T23:20:50.123456Z\n1985-04-12T23:20:50.120Z\n1985-04-12T23:20:50.120000Z\n\n# \"supported\"\n1985-04-12T23:20:50.1235678912345Z\n1985-04-12T23:20:50.100Z\n1985-04-12T23:20:50Z\n1985-04-12T23:20:50.0Z\n1985-04-12T23:20:50.123+00:00\n1985-04-12T23:20:50.123-07:00\n1985-04-12T23:20:50.123+07:00\n1985-04-12T23:20:50.123+01:45\n0985-04-12T23:20:50.123-07:00\n1985-04-12T23:20:50.123-07:00\n0123-01-01T00:00:00.000Z\n\n# various precisions, up through at least 12 digits\n1985-04-12T23:20:50.1Z\n1985-04-12T23:20:50.12Z\n1985-04-12T23:20:50.123Z\n1985-04-12T23:20:50.1234Z\n1985-04-12T23:20:50.12345Z\n1985-04-12T23:20:50.123456Z\n1985-04-12T23:20:50.1234567Z\n1985-04-12T23:20:50.12345678Z\n1985-04-12T23:20:50.123456789Z\n1985-04-12T23:20:50.1234567890Z\n1985-04-12T23:20:50.12345678901Z\n1985-04-12T23:20:50.123456789012Z\n\n# extreme but currently allowed\n0010-12-31T23:00:00.000Z\n1000-12-31T23:00:00.000Z\n1900-12-31T23:00:00.000Z\n3001-12-31T23:00:00.000Z\n"
  },
  {
    "path": "interop-test-files/syntax/did_syntax_invalid.txt",
    "content": "did\ndidmethodval\nmethod:did:val\ndid:method:\ndidmethod:val\ndid:methodval)\n:did:method:val\ndid.method.val\ndid:method:val:\ndid:method:val%\nDID:method:val\ndid:METHOD:val\ndid:m123:val\ndid:method:val/two\ndid:method:val?two\ndid:method:val#two\ndid:method:val%\ndid:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n\n"
  },
  {
    "path": "interop-test-files/syntax/did_syntax_valid.txt",
    "content": "did:method:val\ndid:method:VAL\ndid:method:val123\ndid:method:123\ndid:method:val-two\ndid:method:val_two\ndid:method:val.two\ndid:method:val:two\ndid:method:val%BB\ndid:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\ndid:m:v\ndid:method::::val\ndid:method:-\ndid:method:-:_:.:%ab\ndid:method:.\ndid:method:_\ndid:method::.\n\n# allows some real DID values\ndid:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid\ndid:example:123456789abcdefghi\ndid:plc:7iza6de2dwap2sbkpav7c6c6\ndid:web:example.com\ndid:web:localhost%3A1234\ndid:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N\ndid:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a\n"
  },
  {
    "path": "interop-test-files/syntax/handle_syntax_invalid.txt",
    "content": "# throws on invalid handles\ndid:thing.test\ndid:thing\njohn-.test\njohn.0\njohn.-\nxn--bcher-.tld\njohn..test\njo_hn.test\n-john.test\n.john.test\njo!hn.test\njo%hn.test\njo&hn.test\njo@hn.test\njo*hn.test\njo|hn.test\njo:hn.test\njo/hn.test\njohn💩.test\nbücher.test\njohn .test\njohn.test.\njohn\njohn.\n.john\njohn.test.\n.john.test\n john.test\njohn.test \njoh-.test\njohn.-est\njohn.tes-\n\n# max over all handle: 'shoooort' + '.loooooooooooooooooooooooooong'.repeat(9) + '.test'\nshoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test\n\n# max segment: 'short.' + 'o'.repeat(64) + '.test'\nshort.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test\n\n# throws on \"dotless\" TLD handles\norg\nai\ngg\nio\n\n# correctly validates corner cases (modern vs. old RFCs)\ncn.8\nthing.0aa\nthing.0aa\n\n# does not allow IP addresses as handles\n127.0.0.1\n192.168.0.142\nfe80::7325:8a97:c100:94b\n2600:3c03::f03c:9100:feb0:af1f\n\n# examples from stackoverflow   \n-notvalid.at-all\n-thing.com\nwww.masełkowski.pl.com\n"
  },
  {
    "path": "interop-test-files/syntax/handle_syntax_valid.txt",
    "content": "# allows valid handles\nA.ISI.EDU\nXX.LCS.MIT.EDU\nSRI-NIC.ARPA\njohn.test\njan.test\na234567890123456789.test\njohn2.test\njohn-john.test\njohn.bsky.app\njo.hn\na.co\na.org\njoh.n\nj0.h0\njaymome-johnber123456.test\njay.mome-johnber123456.test\njohn.test.bsky.app\n\n# max over all handle: 'shoooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test'\nshoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test\n\n# max segment: 'short.' + 'o'.repeat(63) + '.test'\nshort.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test\n\n# NOTE: this probably isn't ever going to be a real domain, but my read of the RFC is that it would be possible\njohn.t\n\n# allows .local and .arpa handles (proto-level)\nlaptop.local\nlaptop.arpa\n\n# allows punycode handles\n# 💩.test\nxn--ls8h.test\n# bücher.tld\nxn--bcher-kva.tld\nxn--3jk.com\nxn--w3d.com\nxn--vqb.com\nxn--ppd.com\nxn--cs9a.com\nxn--8r9a.com\nxn--cfd.com\nxn--5jk.com\nxn--2lb.com\n\n# allows onion (Tor) handles\nexpyuzz4wqqyqhjn.onion\nfriend.expyuzz4wqqyqhjn.onion\ng2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion\nfriend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion\nfriend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion\n2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion\nfriend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion\n\n# correctly validates corner cases (modern vs. old RFCs)\n12345.test\n8.cn\n4chan.org\n4chan.o-g\nblah.4chan.org\nthing.a01\n120.0.0.1.com\n0john.test\n9sta--ck.com\n99stack.com\n0ohn.test\njohn.t--t\nthing.0aa.thing\n\n# examples from stackoverflow   \nstack.com\nsta-ck.com\nsta---ck.com\nsta--ck9.com\nstack99.com\nsta99ck.com\ngoogle.com.uk\ngoogle.co.in\ngoogle.com\nmaselkowski.pl\nm.maselkowski.pl\nxn--masekowski-d0b.pl\nxn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s\nxn--stackoverflow.com\nstackoverflow.xn--com\nstackoverflow.co.uk\nxn--masekowski-d0b.pl\nxn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s\n"
  },
  {
    "path": "interop-test-files/syntax/nsid_syntax_invalid.txt",
    "content": "# length checks\ncom.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo\ncom.example.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\ncom.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.foo\n\n# invalid examples\ncom.example.foo.*\ncom.example.foo.blah*\ncom.example.foo.*blah\ncom.exa💩ple.thing\na-0.b-1.c-3\na-0.b-1.c-o\n1.0.0.127.record\n0two.example.foo\nexample.com\ncom.example\na.\n.one.two.three\none.two.three \none.two..three\none .two.three\n one.two.three\ncom.exa💩ple.thing\ncom.atproto.feed.p@st\ncom.atproto.feed.p_st\ncom.atproto.feed.p*st\ncom.atproto.feed.po#t\ncom.atproto.feed.p!ot\ncom.example-.foo\ncom.example.fooBar.2\n"
  },
  {
    "path": "interop-test-files/syntax/nsid_syntax_valid.txt",
    "content": "# length checks\ncom.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo\ncom.example.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\ncom.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.foo\n\n# valid examples\ncom.example.fooBar\ncom.example.fooBarV2\nnet.users.bob.ping\na.b.c\nm.xn--masekowski-d0b.pl\none.two.three\none.two.three.four-and.FiVe\none.2.three\na-0.b-1.c\na0.b1.cc\ncn.8.lex.stuff\ntest.12345.record\na01.thing.record\na.0.c\nxn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two\na0.b1.c3\ncom.example.f00\n\n# allows onion (Tor) NSIDs\nonion.expyuzz4wqqyqhjn.spec.getThing\nonion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing\n\n# allows starting-with-numeric segments (same as domains)\norg.4chan.lex.getThing\ncn.8.lex.stuff\nonion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing\n"
  },
  {
    "path": "interop-test-files/syntax/recordkey_syntax_invalid.txt",
    "content": "# specs\nalpha/beta\n.\n..\n#extra\n@handle\nany space\nany+space\nnumber[3]\nnumber(3)\n\"quote\"\ndHJ1ZQ==\n\n# too long: 'o'.repeat(513)\nooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\n"
  },
  {
    "path": "interop-test-files/syntax/recordkey_syntax_valid.txt",
    "content": "# specs\nself\nexample.com\n~1.2-3_\ndHJ1ZQ\n_\nliteral:self\npre:fix\n\n# more corner-cases\n:\n-\n_\n~\n...\nself.\nlang:\n:lang\n\n# very long: 'o'.repeat(512)\noooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\n"
  },
  {
    "path": "interop-test-files/syntax/tid_syntax_invalid.txt",
    "content": "\n# not base32\n3jzfcijpj2z21\n0000000000000\n\n# too long/short\n3jzfcijpj2z2aa\n3jzfcijpj2z2\n\n# old dashes syntax not actually supported (TTTT-TTT-TTTT-CC)\n3jzf-cij-pj2z-2a\n\n# high bit can't be high\nzzzzzzzzzzzzz\nkjzfcijpj2z2a\n"
  },
  {
    "path": "interop-test-files/syntax/tid_syntax_valid.txt",
    "content": "# 13 digits\n# 234567abcdefghijklmnopqrstuvwxyz\n\n3jzfcijpj2z2a\n7777777777777\n3zzzzzzzzzzzz\n"
  },
  {
    "path": "jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  projects: ['<rootDir>/packages/*/jest.config.js'],\n}\n"
  },
  {
    "path": "jest.setup.ts",
    "content": "import dotenv from 'dotenv'\n\ndotenv.config({ path: './test.env' })\n"
  },
  {
    "path": "lexicons/app/bsky/actor/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.defs\",\n  \"defs\": {\n    \"profileViewBasic\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"handle\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"displayName\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 64,\n          \"maxLength\": 640\n        },\n        \"pronouns\": { \"type\": \"string\" },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"associated\": {\n          \"type\": \"ref\",\n          \"ref\": \"#profileAssociated\"\n        },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#viewerState\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"verification\": {\n          \"type\": \"ref\",\n          \"ref\": \"#verificationState\"\n        },\n        \"status\": {\n          \"type\": \"ref\",\n          \"ref\": \"#statusView\"\n        },\n        \"debug\": {\n          \"type\": \"unknown\",\n          \"description\": \"Debug information for internal development\"\n        }\n      }\n    },\n    \"profileView\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"handle\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"displayName\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 64,\n          \"maxLength\": 640\n        },\n        \"pronouns\": { \"type\": \"string\" },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 256,\n          \"maxLength\": 2560\n        },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"associated\": {\n          \"type\": \"ref\",\n          \"ref\": \"#profileAssociated\"\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#viewerState\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"verification\": {\n          \"type\": \"ref\",\n          \"ref\": \"#verificationState\"\n        },\n        \"status\": {\n          \"type\": \"ref\",\n          \"ref\": \"#statusView\"\n        },\n        \"debug\": {\n          \"type\": \"unknown\",\n          \"description\": \"Debug information for internal development\"\n        }\n      }\n    },\n    \"profileViewDetailed\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"handle\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"displayName\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 64,\n          \"maxLength\": 640\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 256,\n          \"maxLength\": 2560\n        },\n        \"pronouns\": { \"type\": \"string\" },\n        \"website\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"banner\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"followersCount\": { \"type\": \"integer\" },\n        \"followsCount\": { \"type\": \"integer\" },\n        \"postsCount\": { \"type\": \"integer\" },\n        \"associated\": {\n          \"type\": \"ref\",\n          \"ref\": \"#profileAssociated\"\n        },\n        \"joinedViaStarterPack\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#starterPackViewBasic\"\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#viewerState\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"pinnedPost\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.repo.strongRef\"\n        },\n        \"verification\": {\n          \"type\": \"ref\",\n          \"ref\": \"#verificationState\"\n        },\n        \"status\": {\n          \"type\": \"ref\",\n          \"ref\": \"#statusView\"\n        },\n        \"debug\": {\n          \"type\": \"unknown\",\n          \"description\": \"Debug information for internal development\"\n        }\n      }\n    },\n    \"profileAssociated\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"lists\": { \"type\": \"integer\" },\n        \"feedgens\": { \"type\": \"integer\" },\n        \"starterPacks\": { \"type\": \"integer\" },\n        \"labeler\": { \"type\": \"boolean\" },\n        \"chat\": { \"type\": \"ref\", \"ref\": \"#profileAssociatedChat\" },\n        \"activitySubscription\": {\n          \"type\": \"ref\",\n          \"ref\": \"#profileAssociatedActivitySubscription\"\n        },\n        \"germ\": { \"type\": \"ref\", \"ref\": \"#profileAssociatedGerm\" }\n      }\n    },\n    \"profileAssociatedChat\": {\n      \"type\": \"object\",\n      \"required\": [\"allowIncoming\"],\n      \"properties\": {\n        \"allowIncoming\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"all\", \"none\", \"following\"]\n        }\n      }\n    },\n    \"profileAssociatedGerm\": {\n      \"type\": \"object\",\n      \"required\": [\"showButtonTo\", \"messageMeUrl\"],\n      \"properties\": {\n        \"messageMeUrl\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"showButtonTo\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"usersIFollow\", \"everyone\"]\n        }\n      }\n    },\n    \"profileAssociatedActivitySubscription\": {\n      \"type\": \"object\",\n      \"required\": [\"allowSubscriptions\"],\n      \"properties\": {\n        \"allowSubscriptions\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"followers\", \"mutuals\", \"none\"]\n        }\n      }\n    },\n    \"viewerState\": {\n      \"type\": \"object\",\n      \"description\": \"Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.\",\n      \"properties\": {\n        \"muted\": { \"type\": \"boolean\" },\n        \"mutedByList\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#listViewBasic\"\n        },\n        \"blockedBy\": { \"type\": \"boolean\" },\n        \"blocking\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"blockingByList\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#listViewBasic\"\n        },\n        \"following\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"followedBy\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"knownFollowers\": {\n          \"description\": \"This property is present only in selected cases, as an optimization.\",\n          \"type\": \"ref\",\n          \"ref\": \"#knownFollowers\"\n        },\n        \"activitySubscription\": {\n          \"description\": \"This property is present only in selected cases, as an optimization.\",\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.notification.defs#activitySubscription\"\n        }\n      }\n    },\n    \"knownFollowers\": {\n      \"type\": \"object\",\n      \"description\": \"The subject's followers whom you also follow\",\n      \"required\": [\"count\", \"followers\"],\n      \"properties\": {\n        \"count\": { \"type\": \"integer\" },\n        \"followers\": {\n          \"type\": \"array\",\n          \"minLength\": 0,\n          \"maxLength\": 5,\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#profileViewBasic\"\n          }\n        }\n      }\n    },\n    \"verificationState\": {\n      \"type\": \"object\",\n      \"description\": \"Represents the verification information about the user this object is attached to.\",\n      \"required\": [\"verifications\", \"verifiedStatus\", \"trustedVerifierStatus\"],\n      \"properties\": {\n        \"verifications\": {\n          \"type\": \"array\",\n          \"description\": \"All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#verificationView\" }\n        },\n        \"verifiedStatus\": {\n          \"type\": \"string\",\n          \"description\": \"The user's status as a verified account.\",\n          \"knownValues\": [\"valid\", \"invalid\", \"none\"]\n        },\n        \"trustedVerifierStatus\": {\n          \"type\": \"string\",\n          \"description\": \"The user's status as a trusted verifier.\",\n          \"knownValues\": [\"valid\", \"invalid\", \"none\"]\n        }\n      }\n    },\n    \"verificationView\": {\n      \"type\": \"object\",\n      \"description\": \"An individual verification for an associated subject.\",\n      \"required\": [\"issuer\", \"uri\", \"isValid\", \"createdAt\"],\n      \"properties\": {\n        \"issuer\": {\n          \"type\": \"string\",\n          \"description\": \"The user who issued this verification.\",\n          \"format\": \"did\"\n        },\n        \"uri\": {\n          \"type\": \"string\",\n          \"description\": \"The AT-URI of the verification record.\",\n          \"format\": \"at-uri\"\n        },\n        \"isValid\": {\n          \"type\": \"boolean\",\n          \"description\": \"True if the verification passes validation, otherwise false.\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"description\": \"Timestamp when the verification was created.\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n    \"preferences\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"union\",\n        \"refs\": [\n          \"#adultContentPref\",\n          \"#contentLabelPref\",\n          \"#savedFeedsPref\",\n          \"#savedFeedsPrefV2\",\n          \"#personalDetailsPref\",\n          \"#declaredAgePref\",\n          \"#feedViewPref\",\n          \"#threadViewPref\",\n          \"#interestsPref\",\n          \"#mutedWordsPref\",\n          \"#hiddenPostsPref\",\n          \"#bskyAppStatePref\",\n          \"#labelersPref\",\n          \"#postInteractionSettingsPref\",\n          \"#verificationPrefs\",\n          \"#liveEventPreferences\"\n        ]\n      }\n    },\n    \"adultContentPref\": {\n      \"type\": \"object\",\n      \"required\": [\"enabled\"],\n      \"properties\": {\n        \"enabled\": { \"type\": \"boolean\", \"default\": false }\n      }\n    },\n    \"contentLabelPref\": {\n      \"type\": \"object\",\n      \"required\": [\"label\", \"visibility\"],\n      \"properties\": {\n        \"labelerDid\": {\n          \"type\": \"string\",\n          \"description\": \"Which labeler does this preference apply to? If undefined, applies globally.\",\n          \"format\": \"did\"\n        },\n        \"label\": { \"type\": \"string\" },\n        \"visibility\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"ignore\", \"show\", \"warn\", \"hide\"]\n        }\n      }\n    },\n    \"savedFeed\": {\n      \"type\": \"object\",\n      \"required\": [\"id\", \"type\", \"value\", \"pinned\"],\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"feed\", \"list\", \"timeline\"]\n        },\n        \"value\": {\n          \"type\": \"string\"\n        },\n        \"pinned\": {\n          \"type\": \"boolean\"\n        }\n      }\n    },\n    \"savedFeedsPrefV2\": {\n      \"type\": \"object\",\n      \"required\": [\"items\"],\n      \"properties\": {\n        \"items\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.actor.defs#savedFeed\"\n          }\n        }\n      }\n    },\n    \"savedFeedsPref\": {\n      \"type\": \"object\",\n      \"required\": [\"pinned\", \"saved\"],\n      \"properties\": {\n        \"pinned\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\"\n          }\n        },\n        \"saved\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\"\n          }\n        },\n        \"timelineIndex\": {\n          \"type\": \"integer\"\n        }\n      }\n    },\n    \"personalDetailsPref\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"birthDate\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The birth date of account owner.\"\n        }\n      }\n    },\n    \"declaredAgePref\": {\n      \"type\": \"object\",\n      \"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.\",\n      \"properties\": {\n        \"isOverAge13\": {\n          \"type\": \"boolean\",\n          \"description\": \"Indicates if the user has declared that they are over 13 years of age.\"\n        },\n        \"isOverAge16\": {\n          \"type\": \"boolean\",\n          \"description\": \"Indicates if the user has declared that they are over 16 years of age.\"\n        },\n        \"isOverAge18\": {\n          \"type\": \"boolean\",\n          \"description\": \"Indicates if the user has declared that they are over 18 years of age.\"\n        }\n      }\n    },\n    \"feedViewPref\": {\n      \"type\": \"object\",\n      \"required\": [\"feed\"],\n      \"properties\": {\n        \"feed\": {\n          \"type\": \"string\",\n          \"description\": \"The URI of the feed, or an identifier which describes the feed.\"\n        },\n        \"hideReplies\": {\n          \"type\": \"boolean\",\n          \"description\": \"Hide replies in the feed.\"\n        },\n        \"hideRepliesByUnfollowed\": {\n          \"type\": \"boolean\",\n          \"description\": \"Hide replies in the feed if they are not by followed users.\",\n          \"default\": true\n        },\n        \"hideRepliesByLikeCount\": {\n          \"type\": \"integer\",\n          \"description\": \"Hide replies in the feed if they do not have this number of likes.\"\n        },\n        \"hideReposts\": {\n          \"type\": \"boolean\",\n          \"description\": \"Hide reposts in the feed.\"\n        },\n        \"hideQuotePosts\": {\n          \"type\": \"boolean\",\n          \"description\": \"Hide quote posts in the feed.\"\n        }\n      }\n    },\n    \"threadViewPref\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"sort\": {\n          \"type\": \"string\",\n          \"description\": \"Sorting mode for threads.\",\n          \"knownValues\": [\"oldest\", \"newest\", \"most-likes\", \"random\", \"hotness\"]\n        }\n      }\n    },\n    \"interestsPref\": {\n      \"type\": \"object\",\n      \"required\": [\"tags\"],\n      \"properties\": {\n        \"tags\": {\n          \"type\": \"array\",\n          \"maxLength\": 100,\n          \"items\": { \"type\": \"string\", \"maxLength\": 640, \"maxGraphemes\": 64 },\n          \"description\": \"A list of tags which describe the account owner's interests gathered during onboarding.\"\n        }\n      }\n    },\n    \"mutedWordTarget\": {\n      \"type\": \"string\",\n      \"knownValues\": [\"content\", \"tag\"],\n      \"maxLength\": 640,\n      \"maxGraphemes\": 64\n    },\n    \"mutedWord\": {\n      \"type\": \"object\",\n      \"description\": \"A word that the account owner has muted.\",\n      \"required\": [\"value\", \"targets\"],\n      \"properties\": {\n        \"id\": { \"type\": \"string\" },\n        \"value\": {\n          \"type\": \"string\",\n          \"description\": \"The muted word itself.\",\n          \"maxLength\": 10000,\n          \"maxGraphemes\": 1000\n        },\n        \"targets\": {\n          \"type\": \"array\",\n          \"description\": \"The intended targets of the muted word.\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.actor.defs#mutedWordTarget\"\n          }\n        },\n        \"actorTarget\": {\n          \"type\": \"string\",\n          \"description\": \"Groups of users to apply the muted word to. If undefined, applies to all users.\",\n          \"knownValues\": [\"all\", \"exclude-following\"],\n          \"default\": \"all\"\n        },\n        \"expiresAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date and time at which the muted word will expire and no longer be applied.\"\n        }\n      }\n    },\n    \"mutedWordsPref\": {\n      \"type\": \"object\",\n      \"required\": [\"items\"],\n      \"properties\": {\n        \"items\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.actor.defs#mutedWord\"\n          },\n          \"description\": \"A list of words the account owner has muted.\"\n        }\n      }\n    },\n    \"hiddenPostsPref\": {\n      \"type\": \"object\",\n      \"required\": [\"items\"],\n      \"properties\": {\n        \"items\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\", \"format\": \"at-uri\" },\n          \"description\": \"A list of URIs of posts the account owner has hidden.\"\n        }\n      }\n    },\n    \"labelersPref\": {\n      \"type\": \"object\",\n      \"required\": [\"labelers\"],\n      \"properties\": {\n        \"labelers\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#labelerPrefItem\"\n          }\n        }\n      }\n    },\n    \"labelerPrefItem\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        }\n      }\n    },\n    \"bskyAppStatePref\": {\n      \"description\": \"A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"activeProgressGuide\": {\n          \"type\": \"ref\",\n          \"ref\": \"#bskyAppProgressGuide\"\n        },\n        \"queuedNudges\": {\n          \"description\": \"An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.\",\n          \"type\": \"array\",\n          \"maxLength\": 1000,\n          \"items\": { \"type\": \"string\", \"maxLength\": 100 }\n        },\n        \"nuxs\": {\n          \"description\": \"Storage for NUXs the user has encountered.\",\n          \"type\": \"array\",\n          \"maxLength\": 100,\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.actor.defs#nux\"\n          }\n        }\n      }\n    },\n    \"bskyAppProgressGuide\": {\n      \"description\": \"If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.\",\n      \"type\": \"object\",\n      \"required\": [\"guide\"],\n      \"properties\": {\n        \"guide\": { \"type\": \"string\", \"maxLength\": 100 }\n      }\n    },\n    \"nux\": {\n      \"type\": \"object\",\n      \"description\": \"A new user experiences (NUX) storage object\",\n      \"required\": [\"id\", \"completed\"],\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\",\n          \"maxLength\": 100\n        },\n        \"completed\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"data\": {\n          \"description\": \"Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.\",\n          \"type\": \"string\",\n          \"maxLength\": 3000,\n          \"maxGraphemes\": 300\n        },\n        \"expiresAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date and time at which the NUX will expire and should be considered completed.\"\n        }\n      }\n    },\n    \"verificationPrefs\": {\n      \"type\": \"object\",\n      \"description\": \"Preferences for how verified accounts appear in the app.\",\n      \"required\": [],\n      \"properties\": {\n        \"hideBadges\": {\n          \"description\": \"Hide the blue check badges for verified accounts and trusted verifiers.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      }\n    },\n    \"liveEventPreferences\": {\n      \"type\": \"object\",\n      \"description\": \"Preferences for live events.\",\n      \"properties\": {\n        \"hiddenFeedIds\": {\n          \"description\": \"A list of feed IDs that the user has hidden from live events.\",\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" }\n        },\n        \"hideAllFeeds\": {\n          \"description\": \"Whether to hide all feeds from live events.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      }\n    },\n    \"postInteractionSettingsPref\": {\n      \"type\": \"object\",\n      \"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.\",\n      \"required\": [],\n      \"properties\": {\n        \"threadgateAllowRules\": {\n          \"description\": \"Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.\",\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\n              \"app.bsky.feed.threadgate#mentionRule\",\n              \"app.bsky.feed.threadgate#followerRule\",\n              \"app.bsky.feed.threadgate#followingRule\",\n              \"app.bsky.feed.threadgate#listRule\"\n            ]\n          }\n        },\n        \"postgateEmbeddingRules\": {\n          \"description\": \"Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.\",\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\"app.bsky.feed.postgate#disableRule\"]\n          }\n        }\n      }\n    },\n    \"statusView\": {\n      \"type\": \"object\",\n      \"required\": [\"status\", \"record\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"The status for the account.\",\n          \"knownValues\": [\"app.bsky.actor.status#live\"]\n        },\n        \"record\": { \"type\": \"unknown\" },\n        \"embed\": {\n          \"type\": \"union\",\n          \"description\": \"An optional embed associated with the status.\",\n          \"refs\": [\"app.bsky.embed.external#view\"]\n        },\n        \"expiresAt\": {\n          \"type\": \"string\",\n          \"description\": \"The date when this status will expire. The application might choose to no longer return the status after expiration.\",\n          \"format\": \"datetime\"\n        },\n        \"isActive\": {\n          \"type\": \"boolean\",\n          \"description\": \"True if the status is not expired, false if it is expired. Only present if expiration was set.\"\n        },\n        \"isDisabled\": {\n          \"type\": \"boolean\",\n          \"description\": \"True if the user's go-live access has been disabled by a moderator, false otherwise.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/getPreferences.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.getPreferences\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {}\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"preferences\"],\n          \"properties\": {\n            \"preferences\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.actor.defs#preferences\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/getProfile.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.getProfile\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Handle or DID of account to fetch profile of.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewDetailed\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/getProfiles.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.getProfiles\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get detailed profile views of multiple actors.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actors\"],\n        \"properties\": {\n          \"actors\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n            \"maxLength\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"profiles\"],\n          \"properties\": {\n            \"profiles\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileViewDetailed\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/getSuggestions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.getSuggestions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            },\n            \"recId\": {\n              \"type\": \"integer\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/profile.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.profile\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A declaration of a Bluesky account profile.\",\n      \"key\": \"literal:self\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"maxGraphemes\": 64,\n            \"maxLength\": 640\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"Free-form profile description text.\",\n            \"maxGraphemes\": 256,\n            \"maxLength\": 2560\n          },\n          \"pronouns\": {\n            \"type\": \"string\",\n            \"description\": \"Free-form pronouns text.\",\n            \"maxGraphemes\": 20,\n            \"maxLength\": 200\n          },\n          \"website\": { \"type\": \"string\", \"format\": \"uri\" },\n          \"avatar\": {\n            \"type\": \"blob\",\n            \"description\": \"Small image to be displayed next to posts from account. AKA, 'profile picture'\",\n            \"accept\": [\"image/png\", \"image/jpeg\"],\n            \"maxSize\": 1000000\n          },\n          \"banner\": {\n            \"type\": \"blob\",\n            \"description\": \"Larger horizontal image to display behind profile view.\",\n            \"accept\": [\"image/png\", \"image/jpeg\"],\n            \"maxSize\": 1000000\n          },\n          \"labels\": {\n            \"type\": \"union\",\n            \"description\": \"Self-label values, specific to the Bluesky application, on the overall account.\",\n            \"refs\": [\"com.atproto.label.defs#selfLabels\"]\n          },\n          \"joinedViaStarterPack\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.repo.strongRef\"\n          },\n          \"pinnedPost\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.repo.strongRef\"\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/putPreferences.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.putPreferences\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Set the private preferences attached to the account.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"preferences\"],\n          \"properties\": {\n            \"preferences\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.actor.defs#preferences\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/searchActors.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.searchActors\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find actors (profiles) matching search criteria. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"term\": {\n            \"type\": \"string\",\n            \"description\": \"DEPRECATED: use 'q' instead.\"\n          },\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 25\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/searchActorsTypeahead.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.searchActorsTypeahead\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"term\": {\n            \"type\": \"string\",\n            \"description\": \"DEPRECATED: use 'q' instead.\"\n          },\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query prefix; not a full query string.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileViewBasic\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/actor/status.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.actor.status\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A declaration of a Bluesky account status.\",\n      \"key\": \"literal:self\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"status\", \"createdAt\"],\n        \"properties\": {\n          \"status\": {\n            \"type\": \"string\",\n            \"description\": \"The status for the account.\",\n            \"knownValues\": [\"app.bsky.actor.status#live\"]\n          },\n          \"embed\": {\n            \"type\": \"union\",\n            \"description\": \"An optional embed associated with the status.\",\n            \"refs\": [\"app.bsky.embed.external\"]\n          },\n          \"durationMinutes\": {\n            \"type\": \"integer\",\n            \"description\": \"The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.\",\n            \"minimum\": 1\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    },\n    \"live\": {\n      \"type\": \"token\",\n      \"description\": \"Advertises an account as currently offering live content.\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/ageassurance/begin.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.ageassurance.begin\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Initiate Age Assurance for an account.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"email\", \"language\", \"countryCode\"],\n          \"properties\": {\n            \"email\": {\n              \"type\": \"string\",\n              \"description\": \"The user's email address to receive Age Assurance instructions.\"\n            },\n            \"language\": {\n              \"type\": \"string\",\n              \"description\": \"The user's preferred language for communication during the Age Assurance process.\"\n            },\n            \"countryCode\": {\n              \"type\": \"string\",\n              \"description\": \"An ISO 3166-1 alpha-2 code of the user's location.\"\n            },\n            \"regionCode\": {\n              \"type\": \"string\",\n              \"description\": \"An optional ISO 3166-2 code of the user's region or state within the country.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#state\"\n        }\n      },\n      \"errors\": [\n        { \"name\": \"InvalidEmail\" },\n        { \"name\": \"DidTooLong\" },\n        { \"name\": \"InvalidInitiation\" },\n        { \"name\": \"RegionNotSupported\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/ageassurance/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.ageassurance.defs\",\n  \"defs\": {\n    \"access\": {\n      \"description\": \"The access level granted based on Age Assurance data we've processed.\",\n      \"type\": \"string\",\n      \"knownValues\": [\"unknown\", \"none\", \"safe\", \"full\"]\n    },\n    \"status\": {\n      \"type\": \"string\",\n      \"description\": \"The status of the Age Assurance process.\",\n      \"knownValues\": [\"unknown\", \"pending\", \"assured\", \"blocked\"]\n    },\n    \"state\": {\n      \"type\": \"object\",\n      \"description\": \"The user's computed Age Assurance state.\",\n      \"required\": [\"status\", \"access\"],\n      \"properties\": {\n        \"lastInitiatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The timestamp when this state was last updated.\"\n        },\n        \"status\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#status\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"stateMetadata\": {\n      \"type\": \"object\",\n      \"description\": \"Additional metadata needed to compute Age Assurance state client-side.\",\n      \"required\": [],\n      \"properties\": {\n        \"accountCreatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The account creation timestamp.\"\n        }\n      }\n    },\n    \"config\": {\n      \"type\": \"object\",\n      \"description\": \"\",\n      \"required\": [\"regions\"],\n      \"properties\": {\n        \"regions\": {\n          \"type\": \"array\",\n          \"description\": \"The per-region Age Assurance configuration.\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.ageassurance.defs#configRegion\"\n          }\n        }\n      }\n    },\n    \"configRegion\": {\n      \"type\": \"object\",\n      \"description\": \"The Age Assurance configuration for a specific region.\",\n      \"required\": [\"countryCode\", \"minAccessAge\", \"rules\"],\n      \"properties\": {\n        \"countryCode\": {\n          \"type\": \"string\",\n          \"description\": \"The ISO 3166-1 alpha-2 country code this configuration applies to.\"\n        },\n        \"regionCode\": {\n          \"type\": \"string\",\n          \"description\": \"The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.\"\n        },\n        \"minAccessAge\": {\n          \"type\": \"integer\",\n          \"description\": \"The minimum age (as a whole integer) required to use Bluesky in this region.\"\n        },\n        \"rules\": {\n          \"type\": \"array\",\n          \"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.\",\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\n              \"#configRegionRuleDefault\",\n              \"#configRegionRuleIfDeclaredOverAge\",\n              \"#configRegionRuleIfDeclaredUnderAge\",\n              \"#configRegionRuleIfAssuredOverAge\",\n              \"#configRegionRuleIfAssuredUnderAge\",\n              \"#configRegionRuleIfAccountNewerThan\",\n              \"#configRegionRuleIfAccountOlderThan\"\n            ]\n          }\n        }\n      }\n    },\n    \"configRegionRuleDefault\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies by default.\",\n      \"required\": [\"access\"],\n      \"properties\": {\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"configRegionRuleIfDeclaredOverAge\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.\",\n      \"required\": [\"age\", \"access\"],\n      \"properties\": {\n        \"age\": {\n          \"type\": \"integer\",\n          \"description\": \"The age threshold as a whole integer.\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"configRegionRuleIfDeclaredUnderAge\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies if the user has declared themselves under a certain age.\",\n      \"required\": [\"age\", \"access\"],\n      \"properties\": {\n        \"age\": {\n          \"type\": \"integer\",\n          \"description\": \"The age threshold as a whole integer.\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"configRegionRuleIfAssuredOverAge\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.\",\n      \"required\": [\"age\", \"access\"],\n      \"properties\": {\n        \"age\": {\n          \"type\": \"integer\",\n          \"description\": \"The age threshold as a whole integer.\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"configRegionRuleIfAssuredUnderAge\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies if the user has been assured to be under a certain age.\",\n      \"required\": [\"age\", \"access\"],\n      \"properties\": {\n        \"age\": {\n          \"type\": \"integer\",\n          \"description\": \"The age threshold as a whole integer.\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"configRegionRuleIfAccountNewerThan\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies if the account is equal-to or newer than a certain date.\",\n      \"required\": [\"date\", \"access\"],\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date threshold as a datetime string.\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"configRegionRuleIfAccountOlderThan\": {\n      \"type\": \"object\",\n      \"description\": \"Age Assurance rule that applies if the account is older than a certain date.\",\n      \"required\": [\"date\", \"access\"],\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date threshold as a datetime string.\"\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        }\n      }\n    },\n    \"event\": {\n      \"type\": \"object\",\n      \"description\": \"Object used to store Age Assurance data in stash.\",\n      \"required\": [\"createdAt\", \"status\", \"access\", \"attemptId\", \"countryCode\"],\n      \"properties\": {\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date and time of this write operation.\"\n        },\n        \"attemptId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for this instance of the Age Assurance flow, in UUID format.\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"The status of the Age Assurance process.\",\n          \"knownValues\": [\"unknown\", \"pending\", \"assured\", \"blocked\"]\n        },\n        \"access\": {\n          \"description\": \"The access level granted based on Age Assurance data we've processed.\",\n          \"type\": \"string\",\n          \"knownValues\": [\"unknown\", \"none\", \"safe\", \"full\"]\n        },\n        \"countryCode\": {\n          \"type\": \"string\",\n          \"description\": \"The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.\"\n        },\n        \"regionCode\": {\n          \"type\": \"string\",\n          \"description\": \"The ISO 3166-2 region code provided when beginning the Age Assurance flow.\"\n        },\n        \"email\": {\n          \"type\": \"string\",\n          \"description\": \"The email used for Age Assurance.\"\n        },\n        \"initIp\": {\n          \"type\": \"string\",\n          \"description\": \"The IP address used when initiating the Age Assurance flow.\"\n        },\n        \"initUa\": {\n          \"type\": \"string\",\n          \"description\": \"The user agent used when initiating the Age Assurance flow.\"\n        },\n        \"completeIp\": {\n          \"type\": \"string\",\n          \"description\": \"The IP address used when completing the Age Assurance flow.\"\n        },\n        \"completeUa\": {\n          \"type\": \"string\",\n          \"description\": \"The user agent used when completing the Age Assurance flow.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/ageassurance/getConfig.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.ageassurance.getConfig\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns Age Assurance configuration for use on the client.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#config\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/ageassurance/getState.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.ageassurance.getState\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"countryCode\"],\n        \"properties\": {\n          \"countryCode\": { \"type\": \"string\" },\n          \"regionCode\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"state\", \"metadata\"],\n          \"properties\": {\n            \"state\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.ageassurance.defs#state\"\n            },\n            \"metadata\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.ageassurance.defs#stateMetadata\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authCreatePosts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authCreatePosts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Create Bluesky Posts\",\n      \"title:lang\": {},\n      \"detail\": \"Can not update or delete posts.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"rpc\",\n          \"inheritAud\": true,\n          \"lxm\": [\n            \"app.bsky.video.uploadVideo\",\n            \"app.bsky.video.getJobStatus\",\n            \"app.bsky.video.getUploadLimits\"\n          ]\n        },\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\"],\n          \"collection\": [\n            \"app.bsky.feed.post\",\n            \"app.bsky.feed.postgate\",\n            \"app.bsky.feed.threadgate\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authDeleteContent.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authDeleteContent\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Delete Bluesky Content\",\n      \"title:lang\": {},\n      \"detail\": \"Clean up public account history: posts, reposts, and likes.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"delete\"],\n          \"collection\": [\n            \"app.bsky.feed.like\",\n            \"app.bsky.feed.post\",\n            \"app.bsky.feed.postgate\",\n            \"app.bsky.feed.repost\",\n            \"app.bsky.feed.threadgate\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authFullApp.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authFullApp\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Full Bluesky Social App Permissions\",\n      \"title:lang\": {},\n      \"detail\": \"Manage all public content and interactions, private preferences and subscriptions, and other Bluesky-specific app features and data.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"rpc\",\n          \"inheritAud\": true,\n          \"lxm\": [\n            \"app.bsky.actor.getPreferences\",\n            \"app.bsky.actor.getProfile\",\n            \"app.bsky.actor.getProfiles\",\n            \"app.bsky.actor.getSuggestions\",\n            \"app.bsky.actor.putPreferences\",\n            \"app.bsky.actor.searchActors\",\n            \"app.bsky.actor.searchActorsTypeahead\",\n            \"app.bsky.bookmark.createBookmark\",\n            \"app.bsky.bookmark.deleteBookmark\",\n            \"app.bsky.bookmark.getBookmarks\",\n            \"app.bsky.contact.dismissMatch\",\n            \"app.bsky.contact.getMatches\",\n            \"app.bsky.contact.getSyncStatus\",\n            \"app.bsky.contact.importContacts\",\n            \"app.bsky.contact.removeData\",\n            \"app.bsky.contact.startPhoneVerification\",\n            \"app.bsky.contact.verifyPhone\",\n            \"app.bsky.feed.describeFeedGenerator\",\n            \"app.bsky.feed.getActorFeeds\",\n            \"app.bsky.feed.getActorLikes\",\n            \"app.bsky.feed.getAuthorFeed\",\n            \"app.bsky.feed.getFeed\",\n            \"app.bsky.feed.getFeedGenerator\",\n            \"app.bsky.feed.getFeedGenerators\",\n            \"app.bsky.feed.getFeedSkeleton\",\n            \"app.bsky.feed.getLikes\",\n            \"app.bsky.feed.getListFeed\",\n            \"app.bsky.feed.getPostThread\",\n            \"app.bsky.feed.getPosts\",\n            \"app.bsky.feed.getQuotes\",\n            \"app.bsky.feed.getRepostedBy\",\n            \"app.bsky.feed.getSuggestedFeeds\",\n            \"app.bsky.feed.getTimeline\",\n            \"app.bsky.feed.searchPosts\",\n            \"app.bsky.feed.sendInteractions\",\n            \"app.bsky.graph.getActorStarterPacks\",\n            \"app.bsky.graph.getBlocks\",\n            \"app.bsky.graph.getFollowers\",\n            \"app.bsky.graph.getFollows\",\n            \"app.bsky.graph.getKnownFollowers\",\n            \"app.bsky.graph.getList\",\n            \"app.bsky.graph.getListBlocks\",\n            \"app.bsky.graph.getListMutes\",\n            \"app.bsky.graph.getLists\",\n            \"app.bsky.graph.getListsWithMembership\",\n            \"app.bsky.graph.getMutes\",\n            \"app.bsky.graph.getRelationships\",\n            \"app.bsky.graph.getStarterPack\",\n            \"app.bsky.graph.getStarterPacks\",\n            \"app.bsky.graph.getStarterPacksWithMembership\",\n            \"app.bsky.graph.getSuggestedFollowsByActor\",\n            \"app.bsky.graph.muteActor\",\n            \"app.bsky.graph.muteActorList\",\n            \"app.bsky.graph.muteThread\",\n            \"app.bsky.graph.searchStarterPacks\",\n            \"app.bsky.graph.unmuteActor\",\n            \"app.bsky.graph.unmuteActorList\",\n            \"app.bsky.graph.unmuteThread\",\n            \"app.bsky.labeler.getServices\",\n            \"app.bsky.notification.getPreferences\",\n            \"app.bsky.notification.getUnreadCount\",\n            \"app.bsky.notification.listActivitySubscriptions\",\n            \"app.bsky.notification.listNotifications\",\n            \"app.bsky.notification.putActivitySubscription\",\n            \"app.bsky.notification.putPreferences\",\n            \"app.bsky.notification.putPreferencesV2\",\n            \"app.bsky.notification.registerPush\",\n            \"app.bsky.notification.unregisterPush\",\n            \"app.bsky.notification.updateSeen\",\n            \"app.bsky.unspecced.getAgeAssuranceState\",\n            \"app.bsky.unspecced.getConfig\",\n            \"app.bsky.unspecced.getOnboardingSuggestedStarterPacks\",\n            \"app.bsky.unspecced.getPopularFeedGenerators\",\n            \"app.bsky.unspecced.getPostThreadOtherV2\",\n            \"app.bsky.unspecced.getPostThreadV2\",\n            \"app.bsky.unspecced.getSuggestedFeeds\",\n            \"app.bsky.unspecced.getSuggestedFeedsSkeleton\",\n            \"app.bsky.unspecced.getSuggestedStarterPacks\",\n            \"app.bsky.unspecced.getSuggestedStarterPacksSkeleton\",\n            \"app.bsky.unspecced.getSuggestedUsers\",\n            \"app.bsky.unspecced.getSuggestedUsersSkeleton\",\n            \"app.bsky.unspecced.getSuggestionsSkeleton\",\n            \"app.bsky.unspecced.getTaggedSuggestions\",\n            \"app.bsky.unspecced.getTrendingTopics\",\n            \"app.bsky.unspecced.getTrends\",\n            \"app.bsky.unspecced.getTrendsSkeleton\",\n            \"app.bsky.unspecced.initAgeAssurance\",\n            \"app.bsky.unspecced.searchActorsSkeleton\",\n            \"app.bsky.unspecced.searchPostsSkeleton\",\n            \"app.bsky.unspecced.searchStarterPacksSkeleton\",\n            \"app.bsky.video.getJobStatus\",\n            \"app.bsky.video.getUploadLimits\",\n            \"app.bsky.video.uploadVideo\"\n          ]\n        },\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\", \"update\", \"delete\"],\n          \"collection\": [\n            \"app.bsky.actor.profile\",\n            \"app.bsky.actor.status\",\n            \"app.bsky.feed.like\",\n            \"app.bsky.feed.post\",\n            \"app.bsky.feed.postgate\",\n            \"app.bsky.feed.repost\",\n            \"app.bsky.feed.threadgate\",\n            \"app.bsky.graph.block\",\n            \"app.bsky.graph.follow\",\n            \"app.bsky.graph.list\",\n            \"app.bsky.graph.listblock\",\n            \"app.bsky.graph.listitem\",\n            \"app.bsky.graph.starterpack\",\n            \"app.bsky.notification.declaration\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authManageFeedDeclarations.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authManageFeedDeclarations\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Manage Hosted Feeds\",\n      \"title:lang\": {},\n      \"detail\": \"Configure feed generator declaration records.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\", \"update\", \"delete\"],\n          \"collection\": [\"app.bsky.feed.generator\"]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authManageLabelerService.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authManageLabelerService\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Manage Hosted Labeling Service\",\n      \"title:lang\": {},\n      \"detail\": \"Configure labeler declaration records.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\", \"update\", \"delete\"],\n          \"collection\": [\"app.bsky.labeler.service\"]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authManageModeration.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authManageModeration\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Manage Personal Moderation\",\n      \"title:lang\": {},\n      \"detail\": \"Control over blocks, mutes, mod lists, mod services, and preferences.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"rpc\",\n          \"inheritAud\": true,\n          \"lxm\": [\n            \"app.bsky.actor.getPreferences\",\n            \"app.bsky.actor.putPreferences\",\n            \"app.bsky.graph.muteActor\",\n            \"app.bsky.graph.muteActorList\",\n            \"app.bsky.graph.muteThread\",\n            \"app.bsky.graph.unmuteActor\",\n            \"app.bsky.graph.unmuteActorList\",\n            \"app.bsky.graph.unmuteThread\"\n          ]\n        },\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\", \"update\", \"delete\"],\n          \"collection\": [\"app.bsky.graph.block\", \"app.bsky.graph.listblock\"]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authManageNotifications.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authManageNotifications\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Manage Bluesky Notifications\",\n      \"title:lang\": {},\n      \"detail\": \"View and configure notifications for the Bluesky app.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"rpc\",\n          \"inheritAud\": true,\n          \"lxm\": [\n            \"app.bsky.notification.getPreferences\",\n            \"app.bsky.notification.getUnreadCount\",\n            \"app.bsky.notification.listActivitySubscriptions\",\n            \"app.bsky.notification.listNotifications\",\n            \"app.bsky.notification.putActivitySubscription\",\n            \"app.bsky.notification.putPreferences\",\n            \"app.bsky.notification.putPreferencesV2\",\n            \"app.bsky.notification.registerPush\",\n            \"app.bsky.notification.unregisterPush\",\n            \"app.bsky.notification.updateSeen\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authManageProfile.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authManageProfile\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Manage Bluesky Profile\",\n      \"title:lang\": {},\n      \"detail\": \"Update profile data, as well as status and public chat visibility.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\", \"update\", \"delete\"],\n          \"collection\": [\n            \"app.bsky.actor.profile\",\n            \"app.bsky.actor.status\",\n            \"app.bsky.notification.declaration\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/authViewAll.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.authViewAll\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Read-only access to all content\",\n      \"title:lang\": {},\n      \"detail\": \"View Bluesky network content from account perspective, and read all notifications and preferences.\",\n      \"detail:lang\": {},\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"rpc\",\n          \"inheritAud\": true,\n          \"lxm\": [\n            \"app.bsky.actor.getPreferences\",\n            \"app.bsky.actor.getProfile\",\n            \"app.bsky.actor.getProfiles\",\n            \"app.bsky.actor.getSuggestions\",\n            \"app.bsky.actor.searchActors\",\n            \"app.bsky.actor.searchActorsTypeahead\",\n            \"app.bsky.bookmark.getBookmarks\",\n            \"app.bsky.feed.describeFeedGenerator\",\n            \"app.bsky.feed.getActorFeeds\",\n            \"app.bsky.feed.getActorLikes\",\n            \"app.bsky.feed.getAuthorFeed\",\n            \"app.bsky.feed.getFeed\",\n            \"app.bsky.feed.getFeedGenerator\",\n            \"app.bsky.feed.getFeedGenerators\",\n            \"app.bsky.feed.getFeedSkeleton\",\n            \"app.bsky.feed.getLikes\",\n            \"app.bsky.feed.getListFeed\",\n            \"app.bsky.feed.getPostThread\",\n            \"app.bsky.feed.getPosts\",\n            \"app.bsky.feed.getQuotes\",\n            \"app.bsky.feed.getRepostedBy\",\n            \"app.bsky.feed.getSuggestedFeeds\",\n            \"app.bsky.feed.getTimeline\",\n            \"app.bsky.feed.searchPosts\",\n            \"app.bsky.graph.getActorStarterPacks\",\n            \"app.bsky.graph.getBlocks\",\n            \"app.bsky.graph.getFollowers\",\n            \"app.bsky.graph.getFollows\",\n            \"app.bsky.graph.getKnownFollowers\",\n            \"app.bsky.graph.getListBlocks\",\n            \"app.bsky.graph.getListMutes\",\n            \"app.bsky.graph.getLists\",\n            \"app.bsky.graph.getListsWithMembership\",\n            \"app.bsky.graph.getMutes\",\n            \"app.bsky.graph.getRelationships\",\n            \"app.bsky.graph.getStarterPack\",\n            \"app.bsky.graph.getStarterPacks\",\n            \"app.bsky.graph.getStarterPacksWithMembership\",\n            \"app.bsky.graph.getSuggestedFollowsByActor\",\n            \"app.bsky.graph.searchStarterPacks\",\n            \"app.bsky.labeler.getServices\",\n            \"app.bsky.notification.getPreferences\",\n            \"app.bsky.notification.getUnreadCount\",\n            \"app.bsky.notification.listActivitySubscriptions\",\n            \"app.bsky.notification.listNotifications\",\n            \"app.bsky.notification.updateSeen\",\n            \"app.bsky.unspecced.getAgeAssuranceState\",\n            \"app.bsky.unspecced.getConfig\",\n            \"app.bsky.unspecced.getOnboardingSuggestedStarterPacks\",\n            \"app.bsky.unspecced.getPopularFeedGenerators\",\n            \"app.bsky.unspecced.getPostThreadOtherV2\",\n            \"app.bsky.unspecced.getPostThreadV2\",\n            \"app.bsky.unspecced.getSuggestedFeeds\",\n            \"app.bsky.unspecced.getSuggestedFeedsSkeleton\",\n            \"app.bsky.unspecced.getSuggestedStarterPacks\",\n            \"app.bsky.unspecced.getSuggestedStarterPacksSkeleton\",\n            \"app.bsky.unspecced.getSuggestedUsers\",\n            \"app.bsky.unspecced.getSuggestedUsersSkeleton\",\n            \"app.bsky.unspecced.getSuggestionsSkeleton\",\n            \"app.bsky.unspecced.getTaggedSuggestions\",\n            \"app.bsky.unspecced.getTrendingTopics\",\n            \"app.bsky.unspecced.getTrends\",\n            \"app.bsky.unspecced.getTrendsSkeleton\",\n            \"app.bsky.unspecced.searchActorsSkeleton\",\n            \"app.bsky.unspecced.searchPostsSkeleton\",\n            \"app.bsky.unspecced.searchStarterPacksSkeleton\",\n            \"app.bsky.video.getUploadLimits\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/bookmark/createBookmark.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.bookmark.createBookmark\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"cid\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"UnsupportedCollection\",\n          \"description\": \"The URI to be bookmarked is for an unsupported collection.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/bookmark/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.bookmark.defs\",\n  \"defs\": {\n    \"bookmark\": {\n      \"description\": \"Object used to store bookmark data in stash.\",\n      \"type\": \"object\",\n      \"required\": [\"subject\"],\n      \"properties\": {\n        \"subject\": {\n          \"description\": \"A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.\",\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.repo.strongRef\"\n        }\n      }\n    },\n    \"bookmarkView\": {\n      \"type\": \"object\",\n      \"required\": [\"subject\", \"item\"],\n      \"properties\": {\n        \"subject\": {\n          \"description\": \"A strong ref to the bookmarked record.\",\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.repo.strongRef\"\n        },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"item\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"app.bsky.feed.defs#blockedPost\",\n            \"app.bsky.feed.defs#notFoundPost\",\n            \"app.bsky.feed.defs#postView\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/bookmark/deleteBookmark.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.bookmark.deleteBookmark\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"UnsupportedCollection\",\n          \"description\": \"The URI to be bookmarked is for an unsupported collection.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/bookmark/getBookmarks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.bookmark.getBookmarks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Gets views of records bookmarked by the authenticated user. Requires authentication.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"bookmarks\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"bookmarks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.bookmark.defs#bookmarkView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.defs\",\n  \"defs\": {\n    \"matchAndContactIndex\": {\n      \"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.\",\n      \"type\": \"object\",\n      \"required\": [\"match\", \"contactIndex\"],\n      \"properties\": {\n        \"match\": {\n          \"description\": \"Profile of the matched user.\",\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileView\"\n        },\n        \"contactIndex\": {\n          \"description\": \"The index of this match in the import contact input.\",\n          \"type\": \"integer\",\n          \"minimum\": 0,\n          \"maximum\": 999\n        }\n      }\n    },\n    \"syncStatus\": {\n      \"type\": \"object\",\n      \"required\": [\"syncedAt\", \"matchesCount\"],\n      \"properties\": {\n        \"syncedAt\": {\n          \"description\": \"Last date when contacts where imported.\",\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"matchesCount\": {\n          \"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.\",\n          \"type\": \"integer\",\n          \"minimum\": 0\n        }\n      }\n    },\n    \"notification\": {\n      \"description\": \"A stash object to be sent via bsync representing a notification to be created.\",\n      \"type\": \"object\",\n      \"required\": [\"from\", \"to\"],\n      \"properties\": {\n        \"from\": {\n          \"description\": \"The DID of who this notification comes from.\",\n          \"type\": \"string\",\n          \"format\": \"did\"\n        },\n        \"to\": {\n          \"description\": \"The DID of who this notification should go to.\",\n          \"type\": \"string\",\n          \"format\": \"did\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/dismissMatch.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.dismissMatch\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\"],\n          \"properties\": {\n            \"subject\": {\n              \"description\": \"The subject's DID to dismiss the match with.\",\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/getMatches.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.getMatches\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"matches\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"matches\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InvalidLimit\"\n        },\n        {\n          \"name\": \"InvalidCursor\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/getSyncStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.getSyncStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Gets the user's current contact import status. Requires authentication.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {}\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"syncStatus\": {\n              \"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.\",\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.contact.defs#syncStatus\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/importContacts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.importContacts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"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.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"token\", \"contacts\"],\n          \"properties\": {\n            \"token\": {\n              \"description\": \"JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.\",\n              \"type\": \"string\"\n            },\n            \"contacts\": {\n              \"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`.\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"minLength\": 1,\n              \"maxLength\": 1000\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"matchesAndContactIndexes\"],\n          \"properties\": {\n            \"matchesAndContactIndexes\": {\n              \"description\": \"The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.contact.defs#matchAndContactIndex\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InvalidContacts\"\n        },\n        {\n          \"name\": \"TooManyContacts\"\n        },\n        {\n          \"name\": \"InvalidToken\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/removeData.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.removeData\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/sendNotification.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.sendNotification\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"System endpoint to send notifications related to contact imports. Requires role authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"from\", \"to\"],\n          \"properties\": {\n            \"from\": {\n              \"description\": \"The DID of who this notification comes from.\",\n              \"type\": \"string\",\n              \"format\": \"did\"\n            },\n            \"to\": {\n              \"description\": \"The DID of who this notification should go to.\",\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/startPhoneVerification.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.startPhoneVerification\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"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.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"phone\"],\n          \"properties\": {\n            \"phone\": {\n              \"description\": \"The phone number to receive the code via SMS.\",\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"RateLimitExceeded\"\n        },\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InvalidPhone\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/contact/verifyPhone.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.contact.verifyPhone\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"phone\", \"code\"],\n          \"properties\": {\n            \"phone\": {\n              \"description\": \"The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.\",\n              \"type\": \"string\"\n            },\n            \"code\": {\n              \"description\": \"The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.\",\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"token\"],\n          \"properties\": {\n            \"token\": {\n              \"description\": \"JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.\",\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"RateLimitExceeded\"\n        },\n        {\n          \"name\": \"InvalidDid\"\n        },\n        {\n          \"name\": \"InvalidPhone\"\n        },\n        {\n          \"name\": \"InvalidCode\"\n        },\n        {\n          \"name\": \"InternalError\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/draft/createDraft.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.draft.createDraft\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"draft\"],\n          \"properties\": {\n            \"draft\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.draft.defs#draft\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"id\"],\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\",\n              \"description\": \"The ID of the created draft.\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"DraftLimitReached\",\n          \"description\": \"Trying to insert a new draft when the limit was already reached.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/draft/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.draft.defs\",\n  \"defs\": {\n    \"draftWithId\": {\n      \"description\": \"A draft with an identifier, used to store drafts in private storage (stash).\",\n      \"type\": \"object\",\n      \"required\": [\"id\", \"draft\"],\n      \"properties\": {\n        \"id\": {\n          \"description\": \"A TID to be used as a draft identifier.\",\n          \"type\": \"string\",\n          \"format\": \"tid\"\n        },\n        \"draft\": {\n          \"type\": \"ref\",\n          \"ref\": \"#draft\"\n        }\n      }\n    },\n    \"draft\": {\n      \"description\": \"A draft containing an array of draft posts.\",\n      \"type\": \"object\",\n      \"required\": [\"posts\"],\n      \"properties\": {\n        \"deviceId\": {\n          \"type\": \"string\",\n          \"description\": \"UUIDv4 identifier of the device that created this draft.\",\n          \"maxLength\": 100\n        },\n        \"deviceName\": {\n          \"type\": \"string\",\n          \"description\": \"The device and/or platform on which the draft was created.\",\n          \"maxLength\": 100\n        },\n        \"posts\": {\n          \"description\": \"Array of draft posts that compose this draft.\",\n          \"type\": \"array\",\n          \"minLength\": 1,\n          \"maxLength\": 100,\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#draftPost\"\n          }\n        },\n        \"langs\": {\n          \"type\": \"array\",\n          \"description\": \"Indicates human language of posts primary text content.\",\n          \"maxLength\": 3,\n          \"items\": { \"type\": \"string\", \"format\": \"language\" }\n        },\n        \"postgateEmbeddingRules\": {\n          \"description\": \"Embedding rules for the postgates to be created when this draft is published.\",\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\"app.bsky.feed.postgate#disableRule\"]\n          }\n        },\n        \"threadgateAllow\": {\n          \"description\": \"Allow-rules for the threadgate to be created when this draft is published.\",\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\n              \"app.bsky.feed.threadgate#mentionRule\",\n              \"app.bsky.feed.threadgate#followerRule\",\n              \"app.bsky.feed.threadgate#followingRule\",\n              \"app.bsky.feed.threadgate#listRule\"\n            ]\n          }\n        }\n      }\n    },\n    \"draftPost\": {\n      \"description\": \"One of the posts that compose a draft.\",\n      \"type\": \"object\",\n      \"required\": [\"text\"],\n      \"properties\": {\n        \"text\": {\n          \"type\": \"string\",\n          \"maxLength\": 10000,\n          \"maxGraphemes\": 1000,\n          \"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.\"\n        },\n        \"labels\": {\n          \"type\": \"union\",\n          \"description\": \"Self-label values for this post. Effectively content warnings.\",\n          \"refs\": [\"com.atproto.label.defs#selfLabels\"]\n        },\n        \"embedImages\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#draftEmbedImage\" },\n          \"maxLength\": 4\n        },\n        \"embedVideos\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#draftEmbedVideo\" },\n          \"maxLength\": 1\n        },\n        \"embedExternals\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#draftEmbedExternal\" },\n          \"maxLength\": 1\n        },\n        \"embedRecords\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#draftEmbedRecord\" },\n          \"maxLength\": 1\n        }\n      }\n    },\n\n    \"draftView\": {\n      \"description\": \"View to present drafts data to users.\",\n      \"type\": \"object\",\n      \"required\": [\"id\", \"draft\", \"createdAt\", \"updatedAt\"],\n      \"properties\": {\n        \"id\": {\n          \"description\": \"A TID to be used as a draft identifier.\",\n          \"type\": \"string\",\n          \"format\": \"tid\"\n        },\n        \"draft\": {\n          \"type\": \"ref\",\n          \"ref\": \"#draft\"\n        },\n        \"createdAt\": {\n          \"description\": \"The time the draft was created.\",\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"updatedAt\": {\n          \"description\": \"The time the draft was last updated.\",\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n\n    \"draftEmbedLocalRef\": {\n      \"type\": \"object\",\n      \"required\": [\"path\"],\n      \"properties\": {\n        \"path\": {\n          \"type\": \"string\",\n          \"description\": \"Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts.\",\n          \"minLength\": 1,\n          \"maxLength\": 1024\n        }\n      }\n    },\n    \"draftEmbedCaption\": {\n      \"type\": \"object\",\n      \"required\": [\"lang\", \"content\"],\n      \"properties\": {\n        \"lang\": {\n          \"type\": \"string\",\n          \"format\": \"language\"\n        },\n        \"content\": {\n          \"type\": \"string\",\n          \"maxLength\": 10000\n        }\n      }\n    },\n\n    \"draftEmbedImage\": {\n      \"type\": \"object\",\n      \"required\": [\"localRef\"],\n      \"properties\": {\n        \"localRef\": {\n          \"type\": \"ref\",\n          \"ref\": \"#draftEmbedLocalRef\"\n        },\n        \"alt\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 2000\n        }\n      }\n    },\n    \"draftEmbedVideo\": {\n      \"type\": \"object\",\n      \"required\": [\"localRef\"],\n      \"properties\": {\n        \"localRef\": {\n          \"type\": \"ref\",\n          \"ref\": \"#draftEmbedLocalRef\"\n        },\n        \"alt\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 2000\n        },\n        \"captions\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#draftEmbedCaption\"\n          },\n          \"maxLength\": 20\n        }\n      }\n    },\n    \"draftEmbedExternal\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"uri\" }\n      }\n    },\n    \"draftEmbedRecord\": {\n      \"type\": \"object\",\n      \"required\": [\"record\"],\n      \"properties\": {\n        \"record\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/draft/deleteDraft.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.draft.deleteDraft\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Deletes a draft by ID. Requires authentication.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"id\"],\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\",\n              \"format\": \"tid\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/draft/getDrafts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.draft.getDrafts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Gets views of user drafts. Requires authentication.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"drafts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"drafts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.draft.defs#draftView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/draft/updateDraft.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.draft.updateDraft\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"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.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"draft\"],\n          \"properties\": {\n            \"draft\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.draft.defs#draftWithId\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/embed/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.embed.defs\",\n  \"defs\": {\n    \"aspectRatio\": {\n      \"type\": \"object\",\n      \"description\": \"width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.\",\n      \"required\": [\"width\", \"height\"],\n      \"properties\": {\n        \"width\": { \"type\": \"integer\", \"minimum\": 1 },\n        \"height\": { \"type\": \"integer\", \"minimum\": 1 }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/embed/external.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.embed.external\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"description\": \"A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).\",\n      \"required\": [\"external\"],\n      \"properties\": {\n        \"external\": {\n          \"type\": \"ref\",\n          \"ref\": \"#external\"\n        }\n      }\n    },\n    \"external\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"title\", \"description\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"title\": { \"type\": \"string\" },\n        \"description\": { \"type\": \"string\" },\n        \"thumb\": {\n          \"type\": \"blob\",\n          \"accept\": [\"image/*\"],\n          \"maxSize\": 1000000\n        }\n      }\n    },\n    \"view\": {\n      \"type\": \"object\",\n      \"required\": [\"external\"],\n      \"properties\": {\n        \"external\": {\n          \"type\": \"ref\",\n          \"ref\": \"#viewExternal\"\n        }\n      }\n    },\n    \"viewExternal\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"title\", \"description\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"title\": { \"type\": \"string\" },\n        \"description\": { \"type\": \"string\" },\n        \"thumb\": { \"type\": \"string\", \"format\": \"uri\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/embed/images.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.embed.images\",\n  \"description\": \"A set of images embedded in a Bluesky record (eg, a post).\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"required\": [\"images\"],\n      \"properties\": {\n        \"images\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#image\" },\n          \"maxLength\": 4\n        }\n      }\n    },\n    \"image\": {\n      \"type\": \"object\",\n      \"required\": [\"image\", \"alt\"],\n      \"properties\": {\n        \"image\": {\n          \"type\": \"blob\",\n          \"accept\": [\"image/*\"],\n          \"maxSize\": 1000000\n        },\n        \"alt\": {\n          \"type\": \"string\",\n          \"description\": \"Alt text description of the image, for accessibility.\"\n        },\n        \"aspectRatio\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.embed.defs#aspectRatio\"\n        }\n      }\n    },\n    \"view\": {\n      \"type\": \"object\",\n      \"required\": [\"images\"],\n      \"properties\": {\n        \"images\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#viewImage\" },\n          \"maxLength\": 4\n        }\n      }\n    },\n    \"viewImage\": {\n      \"type\": \"object\",\n      \"required\": [\"thumb\", \"fullsize\", \"alt\"],\n      \"properties\": {\n        \"thumb\": {\n          \"type\": \"string\",\n          \"format\": \"uri\",\n          \"description\": \"Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.\"\n        },\n        \"fullsize\": {\n          \"type\": \"string\",\n          \"format\": \"uri\",\n          \"description\": \"Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.\"\n        },\n        \"alt\": {\n          \"type\": \"string\",\n          \"description\": \"Alt text description of the image, for accessibility.\"\n        },\n        \"aspectRatio\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.embed.defs#aspectRatio\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/embed/record.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.embed.record\",\n  \"description\": \"A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"required\": [\"record\"],\n      \"properties\": {\n        \"record\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" }\n      }\n    },\n    \"view\": {\n      \"type\": \"object\",\n      \"required\": [\"record\"],\n      \"properties\": {\n        \"record\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"#viewRecord\",\n            \"#viewNotFound\",\n            \"#viewBlocked\",\n            \"#viewDetached\",\n            \"app.bsky.feed.defs#generatorView\",\n            \"app.bsky.graph.defs#listView\",\n            \"app.bsky.labeler.defs#labelerView\",\n            \"app.bsky.graph.defs#starterPackViewBasic\"\n          ]\n        }\n      }\n    },\n    \"viewRecord\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"author\", \"value\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"author\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewBasic\"\n        },\n        \"value\": {\n          \"type\": \"unknown\",\n          \"description\": \"The record data itself.\"\n        },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"replyCount\": { \"type\": \"integer\" },\n        \"repostCount\": { \"type\": \"integer\" },\n        \"likeCount\": { \"type\": \"integer\" },\n        \"quoteCount\": { \"type\": \"integer\" },\n        \"embeds\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\n              \"app.bsky.embed.images#view\",\n              \"app.bsky.embed.video#view\",\n              \"app.bsky.embed.external#view\",\n              \"app.bsky.embed.record#view\",\n              \"app.bsky.embed.recordWithMedia#view\"\n            ]\n          }\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"viewNotFound\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"notFound\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"notFound\": { \"type\": \"boolean\", \"const\": true }\n      }\n    },\n    \"viewBlocked\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"blocked\", \"author\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"blocked\": { \"type\": \"boolean\", \"const\": true },\n        \"author\": { \"type\": \"ref\", \"ref\": \"app.bsky.feed.defs#blockedAuthor\" }\n      }\n    },\n    \"viewDetached\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"detached\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"detached\": { \"type\": \"boolean\", \"const\": true }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/embed/recordWithMedia.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.embed.recordWithMedia\",\n  \"description\": \"A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"required\": [\"record\", \"media\"],\n      \"properties\": {\n        \"record\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.embed.record\"\n        },\n        \"media\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"app.bsky.embed.images\",\n            \"app.bsky.embed.video\",\n            \"app.bsky.embed.external\"\n          ]\n        }\n      }\n    },\n    \"view\": {\n      \"type\": \"object\",\n      \"required\": [\"record\", \"media\"],\n      \"properties\": {\n        \"record\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.embed.record#view\"\n        },\n        \"media\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"app.bsky.embed.images#view\",\n            \"app.bsky.embed.video#view\",\n            \"app.bsky.embed.external#view\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/embed/video.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.embed.video\",\n  \"description\": \"A video embedded in a Bluesky record (eg, a post).\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"required\": [\"video\"],\n      \"properties\": {\n        \"video\": {\n          \"type\": \"blob\",\n          \"description\": \"The mp4 video file. May be up to 100mb, formerly limited to 50mb.\",\n          \"accept\": [\"video/mp4\"],\n          \"maxSize\": 100000000\n        },\n        \"captions\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#caption\" },\n          \"maxLength\": 20\n        },\n        \"alt\": {\n          \"type\": \"string\",\n          \"description\": \"Alt text description of the video, for accessibility.\",\n          \"maxGraphemes\": 1000,\n          \"maxLength\": 10000\n        },\n        \"aspectRatio\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.embed.defs#aspectRatio\"\n        },\n        \"presentation\": {\n          \"type\": \"string\",\n          \"description\": \"A hint to the client about how to present the video.\",\n          \"knownValues\": [\"default\", \"gif\"]\n        }\n      }\n    },\n    \"caption\": {\n      \"type\": \"object\",\n      \"required\": [\"lang\", \"file\"],\n      \"properties\": {\n        \"lang\": {\n          \"type\": \"string\",\n          \"format\": \"language\"\n        },\n        \"file\": {\n          \"type\": \"blob\",\n          \"accept\": [\"text/vtt\"],\n          \"maxSize\": 20000\n        }\n      }\n    },\n    \"view\": {\n      \"type\": \"object\",\n      \"required\": [\"cid\", \"playlist\"],\n      \"properties\": {\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"playlist\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"thumbnail\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"alt\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 1000,\n          \"maxLength\": 10000\n        },\n        \"aspectRatio\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.embed.defs#aspectRatio\"\n        },\n        \"presentation\": {\n          \"type\": \"string\",\n          \"description\": \"A hint to the client about how to present the video.\",\n          \"knownValues\": [\"default\", \"gif\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.defs\",\n  \"defs\": {\n    \"postView\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"author\", \"record\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"author\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewBasic\"\n        },\n        \"record\": { \"type\": \"unknown\" },\n        \"embed\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"app.bsky.embed.images#view\",\n            \"app.bsky.embed.video#view\",\n            \"app.bsky.embed.external#view\",\n            \"app.bsky.embed.record#view\",\n            \"app.bsky.embed.recordWithMedia#view\"\n          ]\n        },\n        \"bookmarkCount\": { \"type\": \"integer\" },\n        \"replyCount\": { \"type\": \"integer\" },\n        \"repostCount\": { \"type\": \"integer\" },\n        \"likeCount\": { \"type\": \"integer\" },\n        \"quoteCount\": { \"type\": \"integer\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#viewerState\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"threadgate\": { \"type\": \"ref\", \"ref\": \"#threadgateView\" },\n        \"debug\": {\n          \"type\": \"unknown\",\n          \"description\": \"Debug information for internal development\"\n        }\n      }\n    },\n    \"viewerState\": {\n      \"type\": \"object\",\n      \"description\": \"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.\",\n      \"properties\": {\n        \"repost\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"like\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"bookmarked\": { \"type\": \"boolean\" },\n        \"threadMuted\": { \"type\": \"boolean\" },\n        \"replyDisabled\": { \"type\": \"boolean\" },\n        \"embeddingDisabled\": { \"type\": \"boolean\" },\n        \"pinned\": { \"type\": \"boolean\" }\n      }\n    },\n    \"threadContext\": {\n      \"type\": \"object\",\n      \"description\": \"Metadata about this post within the context of the thread it is in.\",\n      \"properties\": {\n        \"rootAuthorLike\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"feedViewPost\": {\n      \"type\": \"object\",\n      \"required\": [\"post\"],\n      \"properties\": {\n        \"post\": { \"type\": \"ref\", \"ref\": \"#postView\" },\n        \"reply\": { \"type\": \"ref\", \"ref\": \"#replyRef\" },\n        \"reason\": { \"type\": \"union\", \"refs\": [\"#reasonRepost\", \"#reasonPin\"] },\n        \"feedContext\": {\n          \"type\": \"string\",\n          \"description\": \"Context provided by feed generator that may be passed back alongside interactions.\",\n          \"maxLength\": 2000\n        },\n        \"reqId\": {\n          \"type\": \"string\",\n          \"description\": \"Unique identifier per request that may be passed back alongside interactions.\",\n          \"maxLength\": 100\n        }\n      }\n    },\n    \"replyRef\": {\n      \"type\": \"object\",\n      \"required\": [\"root\", \"parent\"],\n      \"properties\": {\n        \"root\": {\n          \"type\": \"union\",\n          \"refs\": [\"#postView\", \"#notFoundPost\", \"#blockedPost\"]\n        },\n        \"parent\": {\n          \"type\": \"union\",\n          \"refs\": [\"#postView\", \"#notFoundPost\", \"#blockedPost\"]\n        },\n        \"grandparentAuthor\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewBasic\",\n          \"description\": \"When parent is a reply to another post, this is the author of that post.\"\n        }\n      }\n    },\n    \"reasonRepost\": {\n      \"type\": \"object\",\n      \"required\": [\"by\", \"indexedAt\"],\n      \"properties\": {\n        \"by\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileViewBasic\" },\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"reasonPin\": {\n      \"type\": \"object\",\n      \"properties\": {}\n    },\n    \"threadViewPost\": {\n      \"type\": \"object\",\n      \"required\": [\"post\"],\n      \"properties\": {\n        \"post\": { \"type\": \"ref\", \"ref\": \"#postView\" },\n        \"parent\": {\n          \"type\": \"union\",\n          \"refs\": [\"#threadViewPost\", \"#notFoundPost\", \"#blockedPost\"]\n        },\n        \"replies\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"union\",\n            \"refs\": [\"#threadViewPost\", \"#notFoundPost\", \"#blockedPost\"]\n          }\n        },\n        \"threadContext\": { \"type\": \"ref\", \"ref\": \"#threadContext\" }\n      }\n    },\n    \"notFoundPost\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"notFound\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"notFound\": { \"type\": \"boolean\", \"const\": true }\n      }\n    },\n    \"blockedPost\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"blocked\", \"author\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"blocked\": { \"type\": \"boolean\", \"const\": true },\n        \"author\": { \"type\": \"ref\", \"ref\": \"#blockedAuthor\" }\n      }\n    },\n    \"blockedAuthor\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#viewerState\" }\n      }\n    },\n    \"generatorView\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"did\", \"creator\", \"displayName\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"creator\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" },\n        \"displayName\": { \"type\": \"string\" },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 300,\n          \"maxLength\": 3000\n        },\n        \"descriptionFacets\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n        },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"likeCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"acceptsInteractions\": { \"type\": \"boolean\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#generatorViewerState\" },\n        \"contentMode\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"app.bsky.feed.defs#contentModeUnspecified\",\n            \"app.bsky.feed.defs#contentModeVideo\"\n          ]\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"generatorViewerState\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"like\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"skeletonFeedPost\": {\n      \"type\": \"object\",\n      \"required\": [\"post\"],\n      \"properties\": {\n        \"post\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"reason\": {\n          \"type\": \"union\",\n          \"refs\": [\"#skeletonReasonRepost\", \"#skeletonReasonPin\"]\n        },\n        \"feedContext\": {\n          \"type\": \"string\",\n          \"description\": \"Context that will be passed through to client and may be passed to feed generator back alongside interactions.\",\n          \"maxLength\": 2000\n        }\n      }\n    },\n    \"skeletonReasonRepost\": {\n      \"type\": \"object\",\n      \"required\": [\"repost\"],\n      \"properties\": {\n        \"repost\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"skeletonReasonPin\": {\n      \"type\": \"object\",\n      \"properties\": {}\n    },\n    \"threadgateView\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"record\": { \"type\": \"unknown\" },\n        \"lists\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.graph.defs#listViewBasic\" }\n        }\n      }\n    },\n    \"interaction\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"item\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"event\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"app.bsky.feed.defs#requestLess\",\n            \"app.bsky.feed.defs#requestMore\",\n            \"app.bsky.feed.defs#clickthroughItem\",\n            \"app.bsky.feed.defs#clickthroughAuthor\",\n            \"app.bsky.feed.defs#clickthroughReposter\",\n            \"app.bsky.feed.defs#clickthroughEmbed\",\n            \"app.bsky.feed.defs#interactionSeen\",\n            \"app.bsky.feed.defs#interactionLike\",\n            \"app.bsky.feed.defs#interactionRepost\",\n            \"app.bsky.feed.defs#interactionReply\",\n            \"app.bsky.feed.defs#interactionQuote\",\n            \"app.bsky.feed.defs#interactionShare\"\n          ]\n        },\n        \"feedContext\": {\n          \"type\": \"string\",\n          \"description\": \"Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.\",\n          \"maxLength\": 2000\n        },\n        \"reqId\": {\n          \"type\": \"string\",\n          \"description\": \"Unique identifier per request that may be passed back alongside interactions.\",\n          \"maxLength\": 100\n        }\n      }\n    },\n    \"requestLess\": {\n      \"type\": \"token\",\n      \"description\": \"Request that less content like the given feed item be shown in the feed\"\n    },\n    \"requestMore\": {\n      \"type\": \"token\",\n      \"description\": \"Request that more content like the given feed item be shown in the feed\"\n    },\n    \"clickthroughItem\": {\n      \"type\": \"token\",\n      \"description\": \"User clicked through to the feed item\"\n    },\n    \"clickthroughAuthor\": {\n      \"type\": \"token\",\n      \"description\": \"User clicked through to the author of the feed item\"\n    },\n    \"clickthroughReposter\": {\n      \"type\": \"token\",\n      \"description\": \"User clicked through to the reposter of the feed item\"\n    },\n    \"clickthroughEmbed\": {\n      \"type\": \"token\",\n      \"description\": \"User clicked through to the embedded content of the feed item\"\n    },\n    \"contentModeUnspecified\": {\n      \"type\": \"token\",\n      \"description\": \"Declares the feed generator returns any types of posts.\"\n    },\n    \"contentModeVideo\": {\n      \"type\": \"token\",\n      \"description\": \"Declares the feed generator returns posts containing app.bsky.embed.video embeds.\"\n    },\n    \"interactionSeen\": {\n      \"type\": \"token\",\n      \"description\": \"Feed item was seen by user\"\n    },\n    \"interactionLike\": {\n      \"type\": \"token\",\n      \"description\": \"User liked the feed item\"\n    },\n    \"interactionRepost\": {\n      \"type\": \"token\",\n      \"description\": \"User reposted the feed item\"\n    },\n    \"interactionReply\": {\n      \"type\": \"token\",\n      \"description\": \"User replied to the feed item\"\n    },\n    \"interactionQuote\": {\n      \"type\": \"token\",\n      \"description\": \"User quoted the feed item\"\n    },\n    \"interactionShare\": {\n      \"type\": \"token\",\n      \"description\": \"User shared the feed item\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/describeFeedGenerator.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.describeFeedGenerator\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"feeds\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#feed\" }\n            },\n            \"links\": { \"type\": \"ref\", \"ref\": \"#links\" }\n          }\n        }\n      }\n    },\n    \"feed\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"links\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"privacyPolicy\": { \"type\": \"string\" },\n        \"termsOfService\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/generator.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.generator\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"did\", \"displayName\", \"createdAt\"],\n        \"properties\": {\n          \"did\": { \"type\": \"string\", \"format\": \"did\" },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"maxGraphemes\": 24,\n            \"maxLength\": 240\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"maxGraphemes\": 300,\n            \"maxLength\": 3000\n          },\n          \"descriptionFacets\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n          },\n          \"avatar\": {\n            \"type\": \"blob\",\n            \"accept\": [\"image/png\", \"image/jpeg\"],\n            \"maxSize\": 1000000\n          },\n          \"acceptsInteractions\": {\n            \"type\": \"boolean\",\n            \"description\": \"Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions\"\n          },\n          \"labels\": {\n            \"type\": \"union\",\n            \"description\": \"Self-label values\",\n            \"refs\": [\"com.atproto.label.defs#selfLabels\"]\n          },\n          \"contentMode\": {\n            \"type\": \"string\",\n            \"knownValues\": [\n              \"app.bsky.feed.defs#contentModeUnspecified\",\n              \"app.bsky.feed.defs#contentModeVideo\"\n            ]\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getActorFeeds.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getActorFeeds\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of feeds (feed generator records) created by the actor (in the actor's repo).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feeds\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#generatorView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getActorLikes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getActorLikes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feed\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feed\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#feedViewPost\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"BlockedActor\" }, { \"name\": \"BlockedByActor\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getAuthorFeed.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getAuthorFeed\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"filter\": {\n            \"type\": \"string\",\n            \"description\": \"Combinations of post/repost types to include in response.\",\n            \"knownValues\": [\n              \"posts_with_replies\",\n              \"posts_no_replies\",\n              \"posts_with_media\",\n              \"posts_and_author_threads\",\n              \"posts_with_video\"\n            ],\n            \"default\": \"posts_with_replies\"\n          },\n          \"includePins\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feed\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feed\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#feedViewPost\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"BlockedActor\" }, { \"name\": \"BlockedByActor\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getFeed.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getFeed\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a hydrated feed from an actor's selected feed generator. Implemented by App View.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"feed\"],\n        \"properties\": {\n          \"feed\": { \"type\": \"string\", \"format\": \"at-uri\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feed\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feed\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#feedViewPost\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"UnknownFeed\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getFeedGenerator.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getFeedGenerator\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get information about a feed generator. Implemented by AppView.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"feed\"],\n        \"properties\": {\n          \"feed\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"AT-URI of the feed generator record.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"view\", \"isOnline\", \"isValid\"],\n          \"properties\": {\n            \"view\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.feed.defs#generatorView\"\n            },\n            \"isOnline\": {\n              \"type\": \"boolean\",\n              \"description\": \"Indicates whether the feed generator service has been online recently, or else seems to be inactive.\"\n            },\n            \"isValid\": {\n              \"type\": \"boolean\",\n              \"description\": \"Indicates whether the feed generator service is compatible with the record declaration.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getFeedGenerators.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getFeedGenerators\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get information about a list of feed generators.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"feeds\"],\n        \"properties\": {\n          \"feeds\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"at-uri\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feeds\"],\n          \"properties\": {\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#generatorView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getFeedSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getFeedSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"feed\"],\n        \"properties\": {\n          \"feed\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference to feed generator record describing the specific feed being requested.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feed\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feed\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#skeletonFeedPost\"\n              }\n            },\n            \"reqId\": {\n              \"type\": \"string\",\n              \"description\": \"Unique identifier per request that may be passed back alongside interactions.\",\n              \"maxLength\": 100\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"UnknownFeed\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getLikes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getLikes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get like records which reference a subject (by AT-URI and CID).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uri\"],\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"AT-URI of the subject (eg, a post record).\"\n          },\n          \"cid\": {\n            \"type\": \"string\",\n            \"format\": \"cid\",\n            \"description\": \"CID of the subject record (aka, specific version of record), to filter likes.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"likes\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"cursor\": { \"type\": \"string\" },\n            \"likes\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#like\" }\n            }\n          }\n        }\n      }\n    },\n    \"like\": {\n      \"type\": \"object\",\n      \"required\": [\"indexedAt\", \"createdAt\", \"actor\"],\n      \"properties\": {\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"actor\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getListFeed.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getListFeed\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"list\"],\n        \"properties\": {\n          \"list\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to the list record.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feed\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feed\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#feedViewPost\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"UnknownList\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getPostThread.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getPostThread\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uri\"],\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to post record.\"\n          },\n          \"depth\": {\n            \"type\": \"integer\",\n            \"description\": \"How many levels of reply depth should be included in response.\",\n            \"default\": 6,\n            \"minimum\": 0,\n            \"maximum\": 1000\n          },\n          \"parentHeight\": {\n            \"type\": \"integer\",\n            \"description\": \"How many levels of parent (and grandparent, etc) post to include.\",\n            \"default\": 80,\n            \"minimum\": 0,\n            \"maximum\": 1000\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"thread\"],\n          \"properties\": {\n            \"thread\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"app.bsky.feed.defs#threadViewPost\",\n                \"app.bsky.feed.defs#notFoundPost\",\n                \"app.bsky.feed.defs#blockedPost\"\n              ]\n            },\n            \"threadgate\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.feed.defs#threadgateView\"\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"NotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getPosts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getPosts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uris\"],\n        \"properties\": {\n          \"uris\": {\n            \"type\": \"array\",\n            \"description\": \"List of post AT-URIs to return hydrated views for.\",\n            \"items\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"maxLength\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"posts\"],\n          \"properties\": {\n            \"posts\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.feed.defs#postView\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getQuotes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getQuotes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of quotes for a given post.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uri\"],\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) of post record\"\n          },\n          \"cid\": {\n            \"type\": \"string\",\n            \"format\": \"cid\",\n            \"description\": \"If supplied, filters to quotes of specific version (by CID) of the post record.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"posts\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"cursor\": { \"type\": \"string\" },\n            \"posts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#postView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getRepostedBy.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getRepostedBy\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of reposts for a given post.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uri\"],\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) of post record\"\n          },\n          \"cid\": {\n            \"type\": \"string\",\n            \"format\": \"cid\",\n            \"description\": \"If supplied, filters to reposts of specific version (by CID) of the post record.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"repostedBy\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"cursor\": { \"type\": \"string\" },\n            \"repostedBy\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getSuggestedFeeds.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getSuggestedFeeds\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested feeds (feed generators) for the requesting account.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feeds\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#generatorView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/getTimeline.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.getTimeline\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"algorithm\": {\n            \"type\": \"string\",\n            \"description\": \"Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feed\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feed\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#feedViewPost\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/like.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.like\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record declaring a 'like' of a piece of subject content.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n          \"via\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/post.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.post\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record containing a Bluesky post.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"text\", \"createdAt\"],\n        \"properties\": {\n          \"text\": {\n            \"type\": \"string\",\n            \"maxLength\": 3000,\n            \"maxGraphemes\": 300,\n            \"description\": \"The primary post content. May be an empty string, if there are embeds.\"\n          },\n          \"entities\": {\n            \"type\": \"array\",\n            \"description\": \"DEPRECATED: replaced by app.bsky.richtext.facet.\",\n            \"items\": { \"type\": \"ref\", \"ref\": \"#entity\" }\n          },\n          \"facets\": {\n            \"type\": \"array\",\n            \"description\": \"Annotations of text (mentions, URLs, hashtags, etc)\",\n            \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n          },\n          \"reply\": { \"type\": \"ref\", \"ref\": \"#replyRef\" },\n          \"embed\": {\n            \"type\": \"union\",\n            \"refs\": [\n              \"app.bsky.embed.images\",\n              \"app.bsky.embed.video\",\n              \"app.bsky.embed.external\",\n              \"app.bsky.embed.record\",\n              \"app.bsky.embed.recordWithMedia\"\n            ]\n          },\n          \"langs\": {\n            \"type\": \"array\",\n            \"description\": \"Indicates human language of post primary text content.\",\n            \"maxLength\": 3,\n            \"items\": { \"type\": \"string\", \"format\": \"language\" }\n          },\n          \"labels\": {\n            \"type\": \"union\",\n            \"description\": \"Self-label values for this post. Effectively content warnings.\",\n            \"refs\": [\"com.atproto.label.defs#selfLabels\"]\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"description\": \"Additional hashtags, in addition to any included in post text and facets.\",\n            \"maxLength\": 8,\n            \"items\": { \"type\": \"string\", \"maxLength\": 640, \"maxGraphemes\": 64 }\n          },\n          \"createdAt\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Client-declared timestamp when this post was originally created.\"\n          }\n        }\n      }\n    },\n    \"replyRef\": {\n      \"type\": \"object\",\n      \"required\": [\"root\", \"parent\"],\n      \"properties\": {\n        \"root\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" },\n        \"parent\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" }\n      }\n    },\n    \"entity\": {\n      \"type\": \"object\",\n      \"description\": \"Deprecated: use facets instead.\",\n      \"required\": [\"index\", \"type\", \"value\"],\n      \"properties\": {\n        \"index\": { \"type\": \"ref\", \"ref\": \"#textSlice\" },\n        \"type\": {\n          \"type\": \"string\",\n          \"description\": \"Expected values are 'mention' and 'link'.\"\n        },\n        \"value\": { \"type\": \"string\" }\n      }\n    },\n    \"textSlice\": {\n      \"type\": \"object\",\n      \"description\": \"Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.\",\n      \"required\": [\"start\", \"end\"],\n      \"properties\": {\n        \"start\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"end\": { \"type\": \"integer\", \"minimum\": 0 }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/postgate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.postgate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"tid\",\n      \"description\": \"Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"post\", \"createdAt\"],\n        \"properties\": {\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n          \"post\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to the post record.\"\n          },\n          \"detachedEmbeddingUris\": {\n            \"type\": \"array\",\n            \"maxLength\": 50,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"at-uri\"\n            },\n            \"description\": \"List of AT-URIs embedding this post that the author has detached from.\"\n          },\n          \"embeddingRules\": {\n            \"description\": \"List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.\",\n            \"type\": \"array\",\n            \"maxLength\": 5,\n            \"items\": {\n              \"type\": \"union\",\n              \"refs\": [\"#disableRule\"]\n            }\n          }\n        }\n      }\n    },\n    \"disableRule\": {\n      \"type\": \"object\",\n      \"description\": \"Disables embedding of this post.\",\n      \"properties\": {}\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/repost.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.repost\",\n  \"defs\": {\n    \"main\": {\n      \"description\": \"Record representing a 'repost' of an existing Bluesky post.\",\n      \"type\": \"record\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n          \"via\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/searchPosts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.searchPosts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"q\"],\n        \"properties\": {\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.\"\n          },\n          \"sort\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"top\", \"latest\"],\n            \"default\": \"latest\",\n            \"description\": \"Specifies the ranking order of results.\"\n          },\n          \"since\": {\n            \"type\": \"string\",\n            \"description\": \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\"\n          },\n          \"until\": {\n            \"type\": \"string\",\n            \"description\": \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\"\n          },\n          \"mentions\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.\"\n          },\n          \"author\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Filter to posts by the given account. Handles are resolved to DID before query-time.\"\n          },\n          \"lang\": {\n            \"type\": \"string\",\n            \"format\": \"language\",\n            \"description\": \"Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.\"\n          },\n          \"domain\": {\n            \"type\": \"string\",\n            \"description\": \"Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"uri\",\n            \"description\": \"Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.\"\n          },\n          \"tag\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"maxLength\": 640,\n              \"maxGraphemes\": 64\n            },\n            \"description\": \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 25\n          },\n          \"cursor\": {\n            \"type\": \"string\",\n            \"description\": \"Optional pagination mechanism; may not necessarily allow scrolling through entire result set.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"posts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"hitsTotal\": {\n              \"type\": \"integer\",\n              \"description\": \"Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.\"\n            },\n            \"posts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#postView\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"BadQueryString\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/sendInteractions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.sendInteractions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Send information about interactions with feed items back to the feed generator that served them.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"interactions\"],\n          \"properties\": {\n            \"feed\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"interactions\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#interaction\"\n              }\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/feed/threadgate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.feed.threadgate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"tid\",\n      \"description\": \"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"post\", \"createdAt\"],\n        \"properties\": {\n          \"post\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to the post record.\"\n          },\n          \"allow\": {\n            \"description\": \"List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.\",\n            \"type\": \"array\",\n            \"maxLength\": 5,\n            \"items\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"#mentionRule\",\n                \"#followerRule\",\n                \"#followingRule\",\n                \"#listRule\"\n              ]\n            }\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n          \"hiddenReplies\": {\n            \"type\": \"array\",\n            \"maxLength\": 300,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"at-uri\"\n            },\n            \"description\": \"List of hidden reply URIs.\"\n          }\n        }\n      }\n    },\n    \"mentionRule\": {\n      \"type\": \"object\",\n      \"description\": \"Allow replies from actors mentioned in your post.\",\n      \"properties\": {}\n    },\n    \"followerRule\": {\n      \"type\": \"object\",\n      \"description\": \"Allow replies from actors who follow you.\",\n      \"properties\": {}\n    },\n    \"followingRule\": {\n      \"type\": \"object\",\n      \"description\": \"Allow replies from actors you follow.\",\n      \"properties\": {}\n    },\n    \"listRule\": {\n      \"type\": \"object\",\n      \"description\": \"Allow replies from actors on a list.\",\n      \"required\": [\"list\"],\n      \"properties\": {\n        \"list\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/block.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.block\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account to be blocked.\"\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.defs\",\n  \"defs\": {\n    \"listViewBasic\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"name\", \"purpose\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"name\": { \"type\": \"string\", \"maxLength\": 64, \"minLength\": 1 },\n        \"purpose\": { \"type\": \"ref\", \"ref\": \"#listPurpose\" },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"listItemCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#listViewerState\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"listView\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"creator\", \"name\", \"purpose\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"creator\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" },\n        \"name\": { \"type\": \"string\", \"maxLength\": 64, \"minLength\": 1 },\n        \"purpose\": { \"type\": \"ref\", \"ref\": \"#listPurpose\" },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 300,\n          \"maxLength\": 3000\n        },\n        \"descriptionFacets\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n        },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"listItemCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#listViewerState\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"listItemView\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"subject\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"subject\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" }\n      }\n    },\n    \"starterPackView\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"record\", \"creator\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"record\": { \"type\": \"unknown\" },\n        \"creator\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewBasic\"\n        },\n        \"list\": { \"type\": \"ref\", \"ref\": \"#listViewBasic\" },\n        \"listItemsSample\": {\n          \"type\": \"array\",\n          \"maxLength\": 12,\n          \"items\": { \"type\": \"ref\", \"ref\": \"#listItemView\" }\n        },\n        \"feeds\": {\n          \"type\": \"array\",\n          \"maxLength\": 3,\n          \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.feed.defs#generatorView\" }\n        },\n        \"joinedWeekCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"joinedAllTimeCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"starterPackViewBasic\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"record\", \"creator\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"record\": { \"type\": \"unknown\" },\n        \"creator\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewBasic\"\n        },\n        \"listItemCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"joinedWeekCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"joinedAllTimeCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"listPurpose\": {\n      \"type\": \"string\",\n      \"knownValues\": [\n        \"app.bsky.graph.defs#modlist\",\n        \"app.bsky.graph.defs#curatelist\",\n        \"app.bsky.graph.defs#referencelist\"\n      ]\n    },\n    \"modlist\": {\n      \"type\": \"token\",\n      \"description\": \"A list of actors to apply an aggregate moderation action (mute/block) on.\"\n    },\n    \"curatelist\": {\n      \"type\": \"token\",\n      \"description\": \"A list of actors used for curation purposes such as list feeds or interaction gating.\"\n    },\n    \"referencelist\": {\n      \"type\": \"token\",\n      \"description\": \"A list of actors used for only for reference purposes such as within a starter pack.\"\n    },\n    \"listViewerState\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"muted\": { \"type\": \"boolean\" },\n        \"blocked\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"notFoundActor\": {\n      \"type\": \"object\",\n      \"description\": \"indicates that a handle or DID could not be resolved\",\n      \"required\": [\"actor\", \"notFound\"],\n      \"properties\": {\n        \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n        \"notFound\": { \"type\": \"boolean\", \"const\": true }\n      }\n    },\n    \"relationship\": {\n      \"type\": \"object\",\n      \"description\": \"lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"following\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\",\n          \"description\": \"if the actor follows this DID, this is the AT-URI of the follow record\"\n        },\n        \"followedBy\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\",\n          \"description\": \"if the actor is followed by this DID, contains the AT-URI of the follow record\"\n        },\n        \"blocking\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\",\n          \"description\": \"if the actor blocks this DID, this is the AT-URI of the block record\"\n        },\n        \"blockedBy\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\",\n          \"description\": \"if the actor is blocked by this DID, contains the AT-URI of the block record\"\n        },\n        \"blockingByList\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\",\n          \"description\": \"if the actor blocks this DID via a block list, this is the AT-URI of the listblock record\"\n        },\n        \"blockedByList\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\",\n          \"description\": \"if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/follow.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.follow\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": { \"type\": \"string\", \"format\": \"did\" },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n          \"via\": { \"type\": \"ref\", \"ref\": \"com.atproto.repo.strongRef\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getActorStarterPacks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getActorStarterPacks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of starter packs created by the actor.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.graph.defs#starterPackViewBasic\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getBlocks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getBlocks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates which accounts the requesting account is currently blocking. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"blocks\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"blocks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getFollowers.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getFollowers\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates accounts which follow a specified account (actor).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\", \"followers\"],\n          \"properties\": {\n            \"subject\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.actor.defs#profileView\"\n            },\n            \"cursor\": { \"type\": \"string\" },\n            \"followers\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getFollows.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getFollows\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates accounts which a specified account (actor) follows.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\", \"follows\"],\n          \"properties\": {\n            \"subject\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.actor.defs#profileView\"\n            },\n            \"cursor\": { \"type\": \"string\" },\n            \"follows\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getKnownFollowers.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getKnownFollowers\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates accounts which follow a specified account (actor) and are followed by the viewer.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\", \"followers\"],\n          \"properties\": {\n            \"subject\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.actor.defs#profileView\"\n            },\n            \"cursor\": { \"type\": \"string\" },\n            \"followers\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getList.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getList\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Gets a 'view' (with additional context) of a specified list.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"list\"],\n        \"properties\": {\n          \"list\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) of the list record to hydrate.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"list\", \"items\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"list\": { \"type\": \"ref\", \"ref\": \"app.bsky.graph.defs#listView\" },\n            \"items\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.graph.defs#listItemView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getListBlocks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getListBlocks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get mod lists that the requesting account (actor) is blocking. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"lists\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"lists\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.graph.defs#listView\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getListMutes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getListMutes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"lists\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"lists\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.graph.defs#listView\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getLists.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getLists\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates the lists created by a specified account (actor).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"The account (actor) to enumerate lists from.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"purposes\": {\n            \"type\": \"array\",\n            \"description\": \"Optional filter by list purpose. If not specified, all supported types are returned.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"modlist\", \"curatelist\"]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"lists\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"lists\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.graph.defs#listView\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getListsWithMembership.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getListsWithMembership\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"The account (actor) to check for membership.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"purposes\": {\n            \"type\": \"array\",\n            \"description\": \"Optional filter by list purpose. If not specified, all supported types are returned.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"modlist\", \"curatelist\"]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"listsWithMembership\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"listsWithMembership\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#listWithMembership\" }\n            }\n          }\n        }\n      }\n    },\n    \"listWithMembership\": {\n      \"description\": \"A list and an optional list item indicating membership of a target user to that list.\",\n      \"type\": \"object\",\n      \"required\": [\"list\"],\n      \"properties\": {\n        \"list\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#listView\"\n        },\n        \"listItem\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#listItemView\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getMutes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getMutes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"mutes\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"mutes\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getRelationships.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getRelationships\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates public relationships between one account, and a list of other accounts. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Primary account requesting relationships for.\"\n          },\n          \"others\": {\n            \"type\": \"array\",\n            \"description\": \"List of 'other' accounts to be related back to the primary.\",\n            \"maxLength\": 30,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"relationships\"],\n          \"properties\": {\n            \"actor\": { \"type\": \"string\", \"format\": \"did\" },\n            \"relationships\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"app.bsky.graph.defs#relationship\",\n                  \"app.bsky.graph.defs#notFoundActor\"\n                ]\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"ActorNotFound\",\n          \"description\": \"the primary actor at-identifier could not be resolved\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getStarterPack.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getStarterPack\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Gets a view of a starter pack.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"starterPack\"],\n        \"properties\": {\n          \"starterPack\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) of the starter pack record.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPack\"],\n          \"properties\": {\n            \"starterPack\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.graph.defs#starterPackView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getStarterPacks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getStarterPacks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get views for a list of starter packs.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uris\"],\n        \"properties\": {\n          \"uris\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"maxLength\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.graph.defs#starterPackViewBasic\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getStarterPacksWithMembership.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getStarterPacksWithMembership\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"The account (actor) to check for membership.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacksWithMembership\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"starterPacksWithMembership\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#starterPackWithMembership\" }\n            }\n          }\n        }\n      }\n    },\n    \"starterPackWithMembership\": {\n      \"description\": \"A starter pack and an optional list item indicating membership of a target user to that starter pack.\",\n      \"type\": \"object\",\n      \"required\": [\"starterPack\"],\n      \"properties\": {\n        \"starterPack\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#starterPackView\"\n        },\n        \"listItem\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.graph.defs#listItemView\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/getSuggestedFollowsByActor.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.getSuggestedFollowsByActor\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"suggestions\"],\n          \"properties\": {\n            \"suggestions\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            },\n            \"isFallback\": {\n              \"type\": \"boolean\",\n              \"description\": \"DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid\",\n              \"default\": false\n            },\n            \"recId\": {\n              \"type\": \"integer\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/list.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.list\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"name\", \"purpose\", \"createdAt\"],\n        \"properties\": {\n          \"purpose\": {\n            \"type\": \"ref\",\n            \"description\": \"Defines the purpose of the list (aka, moderation-oriented or curration-oriented)\",\n            \"ref\": \"app.bsky.graph.defs#listPurpose\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"maxLength\": 64,\n            \"minLength\": 1,\n            \"description\": \"Display name for list; can not be empty.\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"maxGraphemes\": 300,\n            \"maxLength\": 3000\n          },\n          \"descriptionFacets\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n          },\n          \"avatar\": {\n            \"type\": \"blob\",\n            \"accept\": [\"image/png\", \"image/jpeg\"],\n            \"maxSize\": 1000000\n          },\n          \"labels\": {\n            \"type\": \"union\",\n            \"refs\": [\"com.atproto.label.defs#selfLabels\"]\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/listblock.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.listblock\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record representing a block relationship against an entire an entire list of accounts (actors).\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to the mod list record.\"\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/listitem.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.listitem\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"list\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The account which is included on the list.\"\n          },\n          \"list\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to the list record (app.bsky.graph.list).\"\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/muteActor.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.muteActor\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actor\"],\n          \"properties\": {\n            \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/muteActorList.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.muteActorList\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"list\"],\n          \"properties\": {\n            \"list\": { \"type\": \"string\", \"format\": \"at-uri\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/muteThread.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.muteThread\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"root\"],\n          \"properties\": {\n            \"root\": { \"type\": \"string\", \"format\": \"at-uri\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/searchStarterPacks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.searchStarterPacks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find starter packs matching search criteria. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"q\"],\n        \"properties\": {\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 25\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.graph.defs#starterPackViewBasic\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/starterpack.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.starterpack\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record defining a starter pack of actors and feeds for new users.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"name\", \"list\", \"createdAt\"],\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"maxGraphemes\": 50,\n            \"maxLength\": 500,\n            \"minLength\": 1,\n            \"description\": \"Display name for starter pack; can not be empty.\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"maxGraphemes\": 300,\n            \"maxLength\": 3000\n          },\n          \"descriptionFacets\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n          },\n          \"list\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to the list record.\"\n          },\n          \"feeds\": {\n            \"type\": \"array\",\n            \"maxLength\": 3,\n            \"items\": { \"type\": \"ref\", \"ref\": \"#feedItem\" }\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    },\n    \"feedItem\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/unmuteActor.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.unmuteActor\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Unmutes the specified account. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actor\"],\n          \"properties\": {\n            \"actor\": { \"type\": \"string\", \"format\": \"at-identifier\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/unmuteActorList.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.unmuteActorList\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Unmutes the specified list of accounts. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"list\"],\n          \"properties\": {\n            \"list\": { \"type\": \"string\", \"format\": \"at-uri\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/unmuteThread.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.unmuteThread\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Unmutes the specified thread. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"root\"],\n          \"properties\": {\n            \"root\": { \"type\": \"string\", \"format\": \"at-uri\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/graph/verification.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.graph.verification\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"subject\", \"handle\", \"displayName\", \"createdAt\"],\n        \"properties\": {\n          \"subject\": {\n            \"description\": \"DID of the subject the verification applies to.\",\n            \"type\": \"string\",\n            \"format\": \"did\"\n          },\n          \"handle\": {\n            \"description\": \"Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.\",\n            \"type\": \"string\",\n            \"format\": \"handle\"\n          },\n          \"displayName\": {\n            \"description\": \"Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.\",\n            \"type\": \"string\"\n          },\n          \"createdAt\": {\n            \"description\": \"Date of when the verification was created.\",\n            \"type\": \"string\",\n            \"format\": \"datetime\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/labeler/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.labeler.defs\",\n  \"defs\": {\n    \"labelerView\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"creator\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"creator\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" },\n        \"likeCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#labelerViewerState\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        }\n      }\n    },\n    \"labelerViewDetailed\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"creator\", \"policies\", \"indexedAt\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"creator\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" },\n        \"policies\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.labeler.defs#labelerPolicies\"\n        },\n        \"likeCount\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"#labelerViewerState\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"reasonTypes\": {\n          \"description\": \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.moderation.defs#reasonType\"\n          }\n        },\n        \"subjectTypes\": {\n          \"description\": \"The set of subject types (account, record, etc) this service accepts reports on.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.moderation.defs#subjectType\"\n          }\n        },\n        \"subjectCollections\": {\n          \"type\": \"array\",\n          \"description\": \"Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.\",\n          \"items\": { \"type\": \"string\", \"format\": \"nsid\" }\n        }\n      }\n    },\n    \"labelerViewerState\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"like\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"labelerPolicies\": {\n      \"type\": \"object\",\n      \"required\": [\"labelValues\"],\n      \"properties\": {\n        \"labelValues\": {\n          \"type\": \"array\",\n          \"description\": \"The label values which this labeler publishes. May include global or custom labels.\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.label.defs#labelValue\"\n          }\n        },\n        \"labelValueDefinitions\": {\n          \"type\": \"array\",\n          \"description\": \"Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.label.defs#labelValueDefinition\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/labeler/getServices.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.labeler.getServices\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get information about a list of labeler services.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"dids\"],\n        \"properties\": {\n          \"dids\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"did\" }\n          },\n          \"detailed\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"views\"],\n          \"properties\": {\n            \"views\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"app.bsky.labeler.defs#labelerView\",\n                  \"app.bsky.labeler.defs#labelerViewDetailed\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/labeler/service.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.labeler.service\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A declaration of the existence of labeler service.\",\n      \"key\": \"literal:self\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"policies\", \"createdAt\"],\n        \"properties\": {\n          \"policies\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.labeler.defs#labelerPolicies\"\n          },\n          \"labels\": {\n            \"type\": \"union\",\n            \"refs\": [\"com.atproto.label.defs#selfLabels\"]\n          },\n          \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n          \"reasonTypes\": {\n            \"description\": \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.moderation.defs#reasonType\"\n            }\n          },\n          \"subjectTypes\": {\n            \"description\": \"The set of subject types (account, record, etc) this service accepts reports on.\",\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.moderation.defs#subjectType\"\n            }\n          },\n          \"subjectCollections\": {\n            \"type\": \"array\",\n            \"description\": \"Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.\",\n            \"items\": { \"type\": \"string\", \"format\": \"nsid\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/declaration.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.declaration\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A declaration of the user's choices related to notifications that can be produced by them.\",\n      \"key\": \"literal:self\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"allowSubscriptions\"],\n        \"properties\": {\n          \"allowSubscriptions\": {\n            \"type\": \"string\",\n            \"description\": \"A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.\",\n            \"knownValues\": [\"followers\", \"mutuals\", \"none\"]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.defs\",\n  \"defs\": {\n    \"recordDeleted\": {\n      \"type\": \"object\",\n      \"properties\": {}\n    },\n    \"chatPreference\": {\n      \"type\": \"object\",\n      \"required\": [\"include\", \"push\"],\n      \"properties\": {\n        \"include\": { \"type\": \"string\", \"knownValues\": [\"all\", \"accepted\"] },\n        \"push\": { \"type\": \"boolean\" }\n      }\n    },\n    \"filterablePreference\": {\n      \"type\": \"object\",\n      \"required\": [\"include\", \"list\", \"push\"],\n      \"properties\": {\n        \"include\": { \"type\": \"string\", \"knownValues\": [\"all\", \"follows\"] },\n        \"list\": { \"type\": \"boolean\" },\n        \"push\": { \"type\": \"boolean\" }\n      }\n    },\n    \"preference\": {\n      \"type\": \"object\",\n      \"required\": [\"list\", \"push\"],\n      \"properties\": {\n        \"list\": { \"type\": \"boolean\" },\n        \"push\": { \"type\": \"boolean\" }\n      }\n    },\n    \"preferences\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"chat\",\n        \"follow\",\n        \"like\",\n        \"likeViaRepost\",\n        \"mention\",\n        \"quote\",\n        \"reply\",\n        \"repost\",\n        \"repostViaRepost\",\n        \"starterpackJoined\",\n        \"subscribedPost\",\n        \"unverified\",\n        \"verified\"\n      ],\n      \"properties\": {\n        \"chat\": { \"type\": \"ref\", \"ref\": \"#chatPreference\" },\n        \"follow\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"like\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"likeViaRepost\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"mention\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"quote\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"reply\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"repost\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"repostViaRepost\": { \"type\": \"ref\", \"ref\": \"#filterablePreference\" },\n        \"starterpackJoined\": { \"type\": \"ref\", \"ref\": \"#preference\" },\n        \"subscribedPost\": { \"type\": \"ref\", \"ref\": \"#preference\" },\n        \"unverified\": { \"type\": \"ref\", \"ref\": \"#preference\" },\n        \"verified\": { \"type\": \"ref\", \"ref\": \"#preference\" }\n      }\n    },\n    \"activitySubscription\": {\n      \"type\": \"object\",\n      \"required\": [\"post\", \"reply\"],\n      \"properties\": {\n        \"post\": { \"type\": \"boolean\" },\n        \"reply\": { \"type\": \"boolean\" }\n      }\n    },\n    \"subjectActivitySubscription\": {\n      \"description\": \"Object used to store activity subscription data in stash.\",\n      \"type\": \"object\",\n      \"required\": [\"subject\", \"activitySubscription\"],\n      \"properties\": {\n        \"subject\": { \"type\": \"string\", \"format\": \"did\" },\n        \"activitySubscription\": {\n          \"type\": \"ref\",\n          \"ref\": \"#activitySubscription\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/getPreferences.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.getPreferences\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get notification-related preferences for an account. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {}\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"preferences\"],\n          \"properties\": {\n            \"preferences\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#preferences\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/getUnreadCount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.getUnreadCount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Count the number of unread notifications for the requesting account. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"priority\": { \"type\": \"boolean\" },\n          \"seenAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"count\"],\n          \"properties\": {\n            \"count\": { \"type\": \"integer\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/listActivitySubscriptions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.listActivitySubscriptions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subscriptions\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"subscriptions\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/listNotifications.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.listNotifications\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerate notifications for the requesting account. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"reasons\": {\n            \"description\": \"Notification reasons to include in response.\",\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"A reason that matches the reason property of #notification.\"\n            }\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"priority\": { \"type\": \"boolean\" },\n          \"cursor\": { \"type\": \"string\" },\n          \"seenAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"notifications\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"notifications\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#notification\" }\n            },\n            \"priority\": { \"type\": \"boolean\" },\n            \"seenAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n          }\n        }\n      }\n    },\n    \"notification\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"uri\",\n        \"cid\",\n        \"author\",\n        \"reason\",\n        \"record\",\n        \"isRead\",\n        \"indexedAt\"\n      ],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"author\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#profileView\" },\n        \"reason\": {\n          \"type\": \"string\",\n          \"description\": \"The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.\",\n          \"knownValues\": [\n            \"like\",\n            \"repost\",\n            \"follow\",\n            \"mention\",\n            \"reply\",\n            \"quote\",\n            \"starterpack-joined\",\n            \"verified\",\n            \"unverified\",\n            \"like-via-repost\",\n            \"repost-via-repost\",\n            \"subscribed-post\",\n            \"contact-match\"\n          ]\n        },\n        \"reasonSubject\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"record\": { \"type\": \"unknown\" },\n        \"isRead\": { \"type\": \"boolean\" },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/putActivitySubscription.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.putActivitySubscription\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\", \"activitySubscription\"],\n          \"properties\": {\n            \"subject\": { \"type\": \"string\", \"format\": \"did\" },\n            \"activitySubscription\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#activitySubscription\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\"],\n          \"properties\": {\n            \"subject\": { \"type\": \"string\", \"format\": \"did\" },\n            \"activitySubscription\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#activitySubscription\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/putPreferences.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.putPreferences\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Set notification-related preferences for an account. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"priority\"],\n          \"properties\": {\n            \"priority\": { \"type\": \"boolean\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/putPreferencesV2.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.putPreferencesV2\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Set notification-related preferences for an account. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"chat\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#chatPreference\"\n            },\n            \"follow\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"like\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"likeViaRepost\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"mention\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"quote\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"reply\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"repost\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"repostViaRepost\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#filterablePreference\"\n            },\n            \"starterpackJoined\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#preference\"\n            },\n            \"subscribedPost\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#preference\"\n            },\n            \"unverified\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#preference\"\n            },\n            \"verified\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#preference\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"preferences\"],\n          \"properties\": {\n            \"preferences\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.notification.defs#preferences\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/registerPush.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.registerPush\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Register to receive push notifications, via a specified service, for the requesting account. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"serviceDid\", \"token\", \"platform\", \"appId\"],\n          \"properties\": {\n            \"serviceDid\": { \"type\": \"string\", \"format\": \"did\" },\n            \"token\": { \"type\": \"string\" },\n            \"platform\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"ios\", \"android\", \"web\"]\n            },\n            \"appId\": { \"type\": \"string\" },\n            \"ageRestricted\": {\n              \"type\": \"boolean\",\n              \"description\": \"Set to true when the actor is age restricted\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/unregisterPush.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.unregisterPush\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"serviceDid\", \"token\", \"platform\", \"appId\"],\n          \"properties\": {\n            \"serviceDid\": { \"type\": \"string\", \"format\": \"did\" },\n            \"token\": { \"type\": \"string\" },\n            \"platform\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"ios\", \"android\", \"web\"]\n            },\n            \"appId\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/notification/updateSeen.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.notification.updateSeen\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Notify server that the requesting account has seen notifications. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"seenAt\"],\n          \"properties\": {\n            \"seenAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/richtext/facet.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.richtext.facet\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"description\": \"Annotation of a sub-string within rich text.\",\n      \"required\": [\"index\", \"features\"],\n      \"properties\": {\n        \"index\": { \"type\": \"ref\", \"ref\": \"#byteSlice\" },\n        \"features\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"union\", \"refs\": [\"#mention\", \"#link\", \"#tag\"] }\n        }\n      }\n    },\n    \"mention\": {\n      \"type\": \"object\",\n      \"description\": \"Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    },\n    \"link\": {\n      \"type\": \"object\",\n      \"description\": \"Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"uri\" }\n      }\n    },\n    \"tag\": {\n      \"type\": \"object\",\n      \"description\": \"Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').\",\n      \"required\": [\"tag\"],\n      \"properties\": {\n        \"tag\": { \"type\": \"string\", \"maxLength\": 640, \"maxGraphemes\": 64 }\n      }\n    },\n    \"byteSlice\": {\n      \"type\": \"object\",\n      \"description\": \"Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.\",\n      \"required\": [\"byteStart\", \"byteEnd\"],\n      \"properties\": {\n        \"byteStart\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"byteEnd\": { \"type\": \"integer\", \"minimum\": 0 }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.defs\",\n  \"defs\": {\n    \"skeletonSearchPost\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"skeletonSearchActor\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    },\n    \"skeletonSearchStarterPack\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"trendingTopic\": {\n      \"type\": \"object\",\n      \"required\": [\"topic\", \"link\"],\n      \"properties\": {\n        \"topic\": { \"type\": \"string\" },\n        \"displayName\": { \"type\": \"string\" },\n        \"description\": { \"type\": \"string\" },\n        \"link\": { \"type\": \"string\" }\n      }\n    },\n    \"skeletonTrend\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"topic\",\n        \"displayName\",\n        \"link\",\n        \"startedAt\",\n        \"postCount\",\n        \"dids\"\n      ],\n      \"properties\": {\n        \"topic\": { \"type\": \"string\" },\n        \"displayName\": { \"type\": \"string\" },\n        \"link\": { \"type\": \"string\" },\n        \"startedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"postCount\": { \"type\": \"integer\" },\n        \"status\": { \"type\": \"string\", \"knownValues\": [\"hot\"] },\n        \"category\": { \"type\": \"string\" },\n        \"dids\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\",\n            \"format\": \"did\"\n          }\n        }\n      }\n    },\n    \"trendView\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"topic\",\n        \"displayName\",\n        \"link\",\n        \"startedAt\",\n        \"postCount\",\n        \"actors\"\n      ],\n      \"properties\": {\n        \"topic\": { \"type\": \"string\" },\n        \"displayName\": { \"type\": \"string\" },\n        \"link\": { \"type\": \"string\" },\n        \"startedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"postCount\": { \"type\": \"integer\" },\n        \"status\": { \"type\": \"string\", \"knownValues\": [\"hot\"] },\n        \"category\": { \"type\": \"string\" },\n        \"actors\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"app.bsky.actor.defs#profileViewBasic\"\n          }\n        }\n      }\n    },\n    \"threadItemPost\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"post\",\n        \"moreParents\",\n        \"moreReplies\",\n        \"opThread\",\n        \"hiddenByThreadgate\",\n        \"mutedByViewer\"\n      ],\n      \"properties\": {\n        \"post\": { \"type\": \"ref\", \"ref\": \"app.bsky.feed.defs#postView\" },\n        \"moreParents\": {\n          \"type\": \"boolean\",\n          \"description\": \"This post has more parents that were not present in the response. This is just a boolean, without the number of parents.\"\n        },\n        \"moreReplies\": {\n          \"type\": \"integer\",\n          \"description\": \"This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate.\"\n        },\n        \"opThread\": {\n          \"type\": \"boolean\",\n          \"description\": \"This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread.\"\n        },\n        \"hiddenByThreadgate\": {\n          \"type\": \"boolean\",\n          \"description\": \"The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.\"\n        },\n        \"mutedByViewer\": {\n          \"type\": \"boolean\",\n          \"description\": \"This is by an account muted by the viewer requesting it.\"\n        }\n      }\n    },\n    \"threadItemNoUnauthenticated\": {\n      \"type\": \"object\",\n      \"properties\": {}\n    },\n    \"threadItemNotFound\": {\n      \"type\": \"object\",\n      \"properties\": {}\n    },\n    \"threadItemBlocked\": {\n      \"type\": \"object\",\n      \"required\": [\"author\"],\n      \"properties\": {\n        \"author\": { \"type\": \"ref\", \"ref\": \"app.bsky.feed.defs#blockedAuthor\" }\n      }\n    },\n    \"ageAssuranceState\": {\n      \"type\": \"object\",\n      \"description\": \"The computed state of the age assurance process, returned to the user in question on certain authenticated requests.\",\n      \"required\": [\"status\"],\n      \"properties\": {\n        \"lastInitiatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The timestamp when this state was last updated.\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"The status of the age assurance process.\",\n          \"knownValues\": [\"unknown\", \"pending\", \"assured\", \"blocked\"]\n        }\n      }\n    },\n    \"ageAssuranceEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Object used to store age assurance data in stash.\",\n      \"required\": [\"createdAt\", \"status\", \"attemptId\"],\n      \"properties\": {\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date and time of this write operation.\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"The status of the age assurance process.\",\n          \"knownValues\": [\"unknown\", \"pending\", \"assured\"]\n        },\n        \"attemptId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for this instance of the age assurance flow, in UUID format.\"\n        },\n        \"email\": {\n          \"type\": \"string\",\n          \"description\": \"The email used for AA.\"\n        },\n        \"initIp\": {\n          \"type\": \"string\",\n          \"description\": \"The IP address used when initiating the AA flow.\"\n        },\n        \"initUa\": {\n          \"type\": \"string\",\n          \"description\": \"The user agent used when initiating the AA flow.\"\n        },\n        \"completeIp\": {\n          \"type\": \"string\",\n          \"description\": \"The IP address used when completing the AA flow.\"\n        },\n        \"completeUa\": {\n          \"type\": \"string\",\n          \"description\": \"The user agent used when completing the AA flow.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getAgeAssuranceState.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getAgeAssuranceState\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.unspecced.defs#ageAssuranceState\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getConfig.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getConfig\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get miscellaneous runtime configuration.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [],\n          \"properties\": {\n            \"checkEmailConfirmed\": { \"type\": \"boolean\" },\n            \"liveNow\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#liveNowConfig\" }\n            }\n          }\n        }\n      }\n    },\n    \"liveNowConfig\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"domains\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"domains\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getOnboardingSuggestedStarterPacks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested starterpacks for onboarding\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.graph.defs#starterPackView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"at-uri\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of suggested users for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedOnboardingUsers\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"category\": {\n            \"type\": \"string\",\n            \"description\": \"Category of users to get suggestions for.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 50,\n            \"default\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"dids\"],\n          \"properties\": {\n            \"dids\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"did\"\n              }\n            },\n            \"recId\": {\n              \"type\": \"string\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getPopularFeedGenerators.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getPopularFeedGenerators\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"An unspecced view of globally popular feed generators.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"query\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feeds\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#generatorView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getPostThreadOtherV2.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getPostThreadOtherV2\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"anchor\"],\n        \"properties\": {\n          \"anchor\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to post record. This is the anchor post.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"thread\"],\n          \"properties\": {\n            \"thread\": {\n              \"type\": \"array\",\n              \"description\": \"A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#threadItem\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"threadItem\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"depth\", \"value\"],\n      \"properties\": {\n        \"uri\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\"\n        },\n        \"depth\": {\n          \"type\": \"integer\",\n          \"description\": \"The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.\"\n        },\n        \"value\": {\n          \"type\": \"union\",\n          \"refs\": [\"app.bsky.unspecced.defs#threadItemPost\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getPostThreadV2.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getPostThreadV2\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"anchor\"],\n        \"properties\": {\n          \"anchor\": {\n            \"type\": \"string\",\n            \"format\": \"at-uri\",\n            \"description\": \"Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post.\"\n          },\n          \"above\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to include parents above the anchor.\",\n            \"default\": true\n          },\n          \"below\": {\n            \"type\": \"integer\",\n            \"description\": \"How many levels of replies to include below the anchor.\",\n            \"default\": 6,\n            \"minimum\": 0,\n            \"maximum\": 20\n          },\n          \"branchingFactor\": {\n            \"type\": \"integer\",\n            \"description\": \"Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).\",\n            \"default\": 10,\n            \"minimum\": 0,\n            \"maximum\": 100\n          },\n          \"sort\": {\n            \"type\": \"string\",\n            \"description\": \"Sorting for the thread replies.\",\n            \"knownValues\": [\"newest\", \"oldest\", \"top\"],\n            \"default\": \"oldest\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"thread\", \"hasOtherReplies\"],\n          \"properties\": {\n            \"thread\": {\n              \"type\": \"array\",\n              \"description\": \"A flat list of thread items. The depth of each item is indicated by the depth property inside the item.\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#threadItem\"\n              }\n            },\n            \"threadgate\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.feed.defs#threadgateView\"\n            },\n            \"hasOtherReplies\": {\n              \"type\": \"boolean\",\n              \"description\": \"Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them.\"\n            }\n          }\n        }\n      }\n    },\n    \"threadItem\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"depth\", \"value\"],\n      \"properties\": {\n        \"uri\": {\n          \"type\": \"string\",\n          \"format\": \"at-uri\"\n        },\n        \"depth\": {\n          \"type\": \"integer\",\n          \"description\": \"The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.\"\n        },\n        \"value\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"app.bsky.unspecced.defs#threadItemPost\",\n            \"app.bsky.unspecced.defs#threadItemNoUnauthenticated\",\n            \"app.bsky.unspecced.defs#threadItemNotFound\",\n            \"app.bsky.unspecced.defs#threadItemBlocked\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedFeeds.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedFeeds\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested feeds\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feeds\"],\n          \"properties\": {\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.feed.defs#generatorView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedFeedsSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"feeds\"],\n          \"properties\": {\n            \"feeds\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"at-uri\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedOnboardingUsers.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedOnboardingUsers\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested users for onboarding\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"string\",\n            \"description\": \"Category of users to get suggestions for.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 50,\n            \"default\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            },\n            \"recId\": {\n              \"type\": \"string\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedStarterPacks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested starterpacks\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.graph.defs#starterPackView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedStarterPacksSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"at-uri\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedUsers.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedUsers\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggested users\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"string\",\n            \"description\": \"Category of users to get suggestions for.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 50,\n            \"default\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.actor.defs#profileView\"\n              }\n            },\n            \"recId\": {\n              \"type\": \"string\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestedUsersSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"category\": {\n            \"type\": \"string\",\n            \"description\": \"Category of users to get suggestions for.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 50,\n            \"default\": 25\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"dids\"],\n          \"properties\": {\n            \"dids\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"did\"\n              }\n            },\n            \"recId\": {\n              \"type\": \"string\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getSuggestionsSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"relativeToDid\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#skeletonSearchActor\"\n              }\n            },\n            \"relativeToDid\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.\"\n            },\n            \"recId\": {\n              \"type\": \"integer\",\n              \"description\": \"DEPRECATED: use recIdStr instead.\"\n            },\n            \"recIdStr\": {\n              \"type\": \"string\",\n              \"description\": \"Snowflake for this recommendation, use when submitting recommendation events.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getTaggedSuggestions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getTaggedSuggestions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of suggestions (feeds and users) tagged with categories\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {}\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"suggestions\"],\n          \"properties\": {\n            \"suggestions\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#suggestion\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"suggestion\": {\n      \"type\": \"object\",\n      \"required\": [\"tag\", \"subjectType\", \"subject\"],\n      \"properties\": {\n        \"tag\": { \"type\": \"string\" },\n        \"subjectType\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"actor\", \"feed\"]\n        },\n        \"subject\": { \"type\": \"string\", \"format\": \"uri\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getTrendingTopics.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getTrendingTopics\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a list of trending topics\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"topics\", \"suggested\"],\n          \"properties\": {\n            \"topics\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#trendingTopic\"\n              }\n            },\n            \"suggested\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#trendingTopic\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getTrends.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getTrends\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get the current trends on the network\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"trends\"],\n          \"properties\": {\n            \"trends\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#trendView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/getTrendsSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.getTrendsSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 25,\n            \"default\": 10\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"trends\"],\n          \"properties\": {\n            \"trends\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#skeletonTrend\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/initAgeAssurance.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.initAgeAssurance\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"email\", \"language\", \"countryCode\"],\n          \"properties\": {\n            \"email\": {\n              \"type\": \"string\",\n              \"description\": \"The user's email address to receive assurance instructions.\"\n            },\n            \"language\": {\n              \"type\": \"string\",\n              \"description\": \"The user's preferred language for communication during the assurance process.\"\n            },\n            \"countryCode\": {\n              \"type\": \"string\",\n              \"description\": \"An ISO 3166-1 alpha-2 code of the user's location.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.unspecced.defs#ageAssuranceState\"\n        }\n      },\n      \"errors\": [\n        { \"name\": \"InvalidEmail\" },\n        { \"name\": \"DidTooLong\" },\n        { \"name\": \"InvalidInitiation\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/searchActorsSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.searchActorsSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Backend Actors (profile) search, returns only skeleton.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"q\"],\n        \"properties\": {\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax.\"\n          },\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.\"\n          },\n          \"typeahead\": {\n            \"type\": \"boolean\",\n            \"description\": \"If true, acts as fast/simple 'typeahead' query.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 25\n          },\n          \"cursor\": {\n            \"type\": \"string\",\n            \"description\": \"Optional pagination mechanism; may not necessarily allow scrolling through entire result set.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actors\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"hitsTotal\": {\n              \"type\": \"integer\",\n              \"description\": \"Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.\"\n            },\n            \"actors\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#skeletonSearchActor\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"BadQueryString\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/searchPostsSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.searchPostsSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Backend Posts search, returns only skeleton\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"q\"],\n        \"properties\": {\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.\"\n          },\n          \"sort\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"top\", \"latest\"],\n            \"default\": \"latest\",\n            \"description\": \"Specifies the ranking order of results.\"\n          },\n          \"since\": {\n            \"type\": \"string\",\n            \"description\": \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\"\n          },\n          \"until\": {\n            \"type\": \"string\",\n            \"description\": \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\"\n          },\n          \"mentions\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.\"\n          },\n          \"author\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Filter to posts by the given account. Handles are resolved to DID before query-time.\"\n          },\n          \"lang\": {\n            \"type\": \"string\",\n            \"format\": \"language\",\n            \"description\": \"Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.\"\n          },\n          \"domain\": {\n            \"type\": \"string\",\n            \"description\": \"Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"uri\",\n            \"description\": \"Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.\"\n          },\n          \"tag\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"maxLength\": 640,\n              \"maxGraphemes\": 64\n            },\n            \"description\": \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\"\n          },\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 25\n          },\n          \"cursor\": {\n            \"type\": \"string\",\n            \"description\": \"Optional pagination mechanism; may not necessarily allow scrolling through entire result set.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"posts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"hitsTotal\": {\n              \"type\": \"integer\",\n              \"description\": \"Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.\"\n            },\n            \"posts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#skeletonSearchPost\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"BadQueryString\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.unspecced.searchStarterPacksSkeleton\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Backend Starter Pack search, returns only skeleton.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"q\"],\n        \"properties\": {\n          \"q\": {\n            \"type\": \"string\",\n            \"description\": \"Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.\"\n          },\n          \"viewer\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID of the account making the request (not included for public/unauthenticated queries).\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 25\n          },\n          \"cursor\": {\n            \"type\": \"string\",\n            \"description\": \"Optional pagination mechanism; may not necessarily allow scrolling through entire result set.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"starterPacks\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"hitsTotal\": {\n              \"type\": \"integer\",\n              \"description\": \"Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.\"\n            },\n            \"starterPacks\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"app.bsky.unspecced.defs#skeletonSearchStarterPack\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"BadQueryString\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/video/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.video.defs\",\n  \"defs\": {\n    \"jobStatus\": {\n      \"type\": \"object\",\n      \"required\": [\"jobId\", \"did\", \"state\"],\n      \"properties\": {\n        \"jobId\": { \"type\": \"string\" },\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"state\": {\n          \"type\": \"string\",\n          \"description\": \"The state of the video processing job. All values not listed as a known value indicate that the job is in process.\",\n          \"knownValues\": [\"JOB_STATE_COMPLETED\", \"JOB_STATE_FAILED\"]\n        },\n        \"progress\": {\n          \"type\": \"integer\",\n          \"minimum\": 0,\n          \"maximum\": 100,\n          \"description\": \"Progress within the current processing state.\"\n        },\n        \"blob\": { \"type\": \"blob\" },\n        \"error\": { \"type\": \"string\" },\n        \"message\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/video/getJobStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.video.getJobStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get status details for a video processing job.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"jobId\"],\n        \"properties\": {\n          \"jobId\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"jobStatus\"],\n          \"properties\": {\n            \"jobStatus\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.video.defs#jobStatus\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/video/getUploadLimits.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.video.getUploadLimits\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get video upload limits for the authenticated user.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"canUpload\"],\n          \"properties\": {\n            \"canUpload\": { \"type\": \"boolean\" },\n            \"remainingDailyVideos\": { \"type\": \"integer\" },\n            \"remainingDailyBytes\": { \"type\": \"integer\" },\n            \"message\": { \"type\": \"string\" },\n            \"error\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/app/bsky/video/uploadVideo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"app.bsky.video.uploadVideo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Upload a video to be processed then stored on the PDS.\",\n      \"input\": {\n        \"encoding\": \"video/mp4\"\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"jobStatus\"],\n          \"properties\": {\n            \"jobStatus\": {\n              \"type\": \"ref\",\n              \"ref\": \"app.bsky.video.defs#jobStatus\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/actor/declaration.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.actor.declaration\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A declaration of a Bluesky chat account.\",\n      \"key\": \"literal:self\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"allowIncoming\"],\n        \"properties\": {\n          \"allowIncoming\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"all\", \"none\", \"following\"]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/actor/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.actor.defs\",\n  \"defs\": {\n    \"profileViewBasic\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"handle\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"displayName\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 64,\n          \"maxLength\": 640\n        },\n        \"avatar\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"associated\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileAssociated\"\n        },\n        \"viewer\": { \"type\": \"ref\", \"ref\": \"app.bsky.actor.defs#viewerState\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"chatDisabled\": {\n          \"type\": \"boolean\",\n          \"description\": \"Set to true when the actor cannot actively participate in conversations\"\n        },\n        \"verification\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#verificationState\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/actor/deleteAccount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.actor.deleteAccount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/actor/exportAccountData.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.actor.exportAccountData\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"output\": {\n        \"encoding\": \"application/jsonl\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/authFullChatClient.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.authFullChatClient\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"permission-set\",\n      \"title\": \"Full Chat Client (All Conversations)\",\n      \"title:lang\": {},\n      \"detail\": \"Control of all chat conversations and configuration management.\",\n      \"detail:lang\": {\n        \"en\": \"All Chat Conversations\"\n      },\n      \"permissions\": [\n        {\n          \"type\": \"permission\",\n          \"resource\": \"rpc\",\n          \"inheritAud\": true,\n          \"lxm\": [\n            \"chat.bsky.actor.deleteAccount\",\n            \"chat.bsky.actor.exportAccountData\",\n            \"chat.bsky.convo.acceptConvo\",\n            \"chat.bsky.convo.addReaction\",\n            \"chat.bsky.convo.deleteMessageForSelf\",\n            \"chat.bsky.convo.getConvo\",\n            \"chat.bsky.convo.getConvoAvailability\",\n            \"chat.bsky.convo.getConvoForMembers\",\n            \"chat.bsky.convo.getLog\",\n            \"chat.bsky.convo.getMessages\",\n            \"chat.bsky.convo.leaveConvo\",\n            \"chat.bsky.convo.listConvos\",\n            \"chat.bsky.convo.muteConvo\",\n            \"chat.bsky.convo.removeReaction\",\n            \"chat.bsky.convo.sendMessage\",\n            \"chat.bsky.convo.sendMessageBatch\",\n            \"chat.bsky.convo.unmuteConvo\",\n            \"chat.bsky.convo.updateAllRead\",\n            \"chat.bsky.convo.updateRead\"\n          ]\n        },\n        {\n          \"type\": \"permission\",\n          \"resource\": \"repo\",\n          \"action\": [\"create\", \"update\", \"delete\"],\n          \"collection\": [\"chat.bsky.actor.declaration\"]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/acceptConvo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.acceptConvo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"rev\": {\n              \"description\": \"Rev when the convo was accepted. If not present, the convo was already accepted.\",\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/addReaction.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.addReaction\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\", \"messageId\", \"value\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" },\n            \"messageId\": { \"type\": \"string\" },\n            \"value\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"maxLength\": 64,\n              \"minGraphemes\": 1,\n              \"maxGraphemes\": 1\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"message\"],\n          \"properties\": {\n            \"message\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#messageView\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"ReactionMessageDeleted\",\n          \"description\": \"Indicates that the message has been deleted and reactions can no longer be added/removed.\"\n        },\n        {\n          \"name\": \"ReactionLimitReached\",\n          \"description\": \"Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent.\"\n        },\n        {\n          \"name\": \"ReactionInvalidValue\",\n          \"description\": \"Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.defs\",\n  \"defs\": {\n    \"messageRef\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"messageId\", \"convoId\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"convoId\": { \"type\": \"string\" },\n        \"messageId\": { \"type\": \"string\" }\n      }\n    },\n    \"messageInput\": {\n      \"type\": \"object\",\n      \"required\": [\"text\"],\n      \"properties\": {\n        \"text\": {\n          \"type\": \"string\",\n          \"maxLength\": 10000,\n          \"maxGraphemes\": 1000\n        },\n        \"facets\": {\n          \"type\": \"array\",\n          \"description\": \"Annotations of text (mentions, URLs, hashtags, etc)\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n        },\n        \"embed\": {\n          \"type\": \"union\",\n          \"refs\": [\"app.bsky.embed.record\"]\n        }\n      }\n    },\n    \"messageView\": {\n      \"type\": \"object\",\n      \"required\": [\"id\", \"rev\", \"text\", \"sender\", \"sentAt\"],\n      \"properties\": {\n        \"id\": { \"type\": \"string\" },\n        \"rev\": { \"type\": \"string\" },\n        \"text\": {\n          \"type\": \"string\",\n          \"maxLength\": 10000,\n          \"maxGraphemes\": 1000\n        },\n        \"facets\": {\n          \"type\": \"array\",\n          \"description\": \"Annotations of text (mentions, URLs, hashtags, etc)\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"app.bsky.richtext.facet\" }\n        },\n        \"embed\": {\n          \"type\": \"union\",\n          \"refs\": [\"app.bsky.embed.record#view\"]\n        },\n        \"reactions\": {\n          \"type\": \"array\",\n          \"description\": \"Reactions to this message, in ascending order of creation time.\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#reactionView\" }\n        },\n        \"sender\": { \"type\": \"ref\", \"ref\": \"#messageViewSender\" },\n        \"sentAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"deletedMessageView\": {\n      \"type\": \"object\",\n      \"required\": [\"id\", \"rev\", \"sender\", \"sentAt\"],\n      \"properties\": {\n        \"id\": { \"type\": \"string\" },\n        \"rev\": { \"type\": \"string\" },\n        \"sender\": { \"type\": \"ref\", \"ref\": \"#messageViewSender\" },\n        \"sentAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"messageViewSender\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    },\n    \"reactionView\": {\n      \"type\": \"object\",\n      \"required\": [\"value\", \"sender\", \"createdAt\"],\n      \"properties\": {\n        \"value\": { \"type\": \"string\" },\n        \"sender\": { \"type\": \"ref\", \"ref\": \"#reactionViewSender\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"reactionViewSender\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    },\n    \"messageAndReactionView\": {\n      \"type\": \"object\",\n      \"required\": [\"message\", \"reaction\"],\n      \"properties\": {\n        \"message\": { \"type\": \"ref\", \"ref\": \"#messageView\" },\n        \"reaction\": { \"type\": \"ref\", \"ref\": \"#reactionView\" }\n      }\n    },\n    \"convoView\": {\n      \"type\": \"object\",\n      \"required\": [\"id\", \"rev\", \"members\", \"muted\", \"unreadCount\"],\n      \"properties\": {\n        \"id\": { \"type\": \"string\" },\n        \"rev\": { \"type\": \"string\" },\n        \"members\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"chat.bsky.actor.defs#profileViewBasic\"\n          }\n        },\n        \"lastMessage\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageView\", \"#deletedMessageView\"]\n        },\n        \"lastReaction\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageAndReactionView\"]\n        },\n        \"muted\": { \"type\": \"boolean\" },\n        \"status\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"request\", \"accepted\"]\n        },\n        \"unreadCount\": { \"type\": \"integer\" }\n      }\n    },\n    \"logBeginConvo\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" }\n      }\n    },\n    \"logAcceptConvo\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" }\n      }\n    },\n    \"logLeaveConvo\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" }\n      }\n    },\n    \"logMuteConvo\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" }\n      }\n    },\n    \"logUnmuteConvo\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" }\n      }\n    },\n    \"logCreateMessage\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\", \"message\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" },\n        \"message\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageView\", \"#deletedMessageView\"]\n        }\n      }\n    },\n    \"logDeleteMessage\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\", \"message\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" },\n        \"message\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageView\", \"#deletedMessageView\"]\n        }\n      }\n    },\n    \"logReadMessage\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\", \"message\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" },\n        \"message\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageView\", \"#deletedMessageView\"]\n        }\n      }\n    },\n    \"logAddReaction\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\", \"message\", \"reaction\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" },\n        \"message\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageView\", \"#deletedMessageView\"]\n        },\n        \"reaction\": { \"type\": \"ref\", \"ref\": \"#reactionView\" }\n      }\n    },\n    \"logRemoveReaction\": {\n      \"type\": \"object\",\n      \"required\": [\"rev\", \"convoId\", \"message\", \"reaction\"],\n      \"properties\": {\n        \"rev\": { \"type\": \"string\" },\n        \"convoId\": { \"type\": \"string\" },\n        \"message\": {\n          \"type\": \"union\",\n          \"refs\": [\"#messageView\", \"#deletedMessageView\"]\n        },\n        \"reaction\": { \"type\": \"ref\", \"ref\": \"#reactionView\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/deleteMessageForSelf.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.deleteMessageForSelf\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\", \"messageId\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" },\n            \"messageId\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"chat.bsky.convo.defs#deletedMessageView\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/getConvo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.getConvo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"convoId\"],\n        \"properties\": {\n          \"convoId\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convo\"],\n          \"properties\": {\n            \"convo\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#convoView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/getConvoAvailability.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.getConvoAvailability\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"members\"],\n        \"properties\": {\n          \"members\": {\n            \"type\": \"array\",\n            \"minLength\": 1,\n            \"maxLength\": 10,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"canChat\"],\n          \"properties\": {\n            \"canChat\": {\n              \"type\": \"boolean\"\n            },\n            \"convo\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#convoView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/getConvoForMembers.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.getConvoForMembers\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"members\"],\n        \"properties\": {\n          \"members\": {\n            \"type\": \"array\",\n            \"minLength\": 1,\n            \"maxLength\": 10,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convo\"],\n          \"properties\": {\n            \"convo\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#convoView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/getLog.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.getLog\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [],\n        \"properties\": {\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"logs\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"logs\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"chat.bsky.convo.defs#logBeginConvo\",\n                  \"chat.bsky.convo.defs#logAcceptConvo\",\n                  \"chat.bsky.convo.defs#logLeaveConvo\",\n                  \"chat.bsky.convo.defs#logMuteConvo\",\n                  \"chat.bsky.convo.defs#logUnmuteConvo\",\n                  \"chat.bsky.convo.defs#logCreateMessage\",\n                  \"chat.bsky.convo.defs#logDeleteMessage\",\n                  \"chat.bsky.convo.defs#logReadMessage\",\n                  \"chat.bsky.convo.defs#logAddReaction\",\n                  \"chat.bsky.convo.defs#logRemoveReaction\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/getMessages.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.getMessages\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"convoId\"],\n        \"properties\": {\n          \"convoId\": { \"type\": \"string\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"messages\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"messages\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"chat.bsky.convo.defs#messageView\",\n                  \"chat.bsky.convo.defs#deletedMessageView\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/leaveConvo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.leaveConvo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\", \"rev\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" },\n            \"rev\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/listConvos.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.listConvos\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"readState\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"unread\"]\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"request\", \"accepted\"]\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convos\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"convos\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"chat.bsky.convo.defs#convoView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/muteConvo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.muteConvo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convo\"],\n          \"properties\": {\n            \"convo\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#convoView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/removeReaction.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.removeReaction\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\", \"messageId\", \"value\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" },\n            \"messageId\": { \"type\": \"string\" },\n            \"value\": {\n              \"type\": \"string\",\n              \"minLength\": 1,\n              \"maxLength\": 64,\n              \"minGraphemes\": 1,\n              \"maxGraphemes\": 1\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"message\"],\n          \"properties\": {\n            \"message\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#messageView\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"ReactionMessageDeleted\",\n          \"description\": \"Indicates that the message has been deleted and reactions can no longer be added/removed.\"\n        },\n        {\n          \"name\": \"ReactionInvalidValue\",\n          \"description\": \"Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/sendMessage.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.sendMessage\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\", \"message\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" },\n            \"message\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#messageInput\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"chat.bsky.convo.defs#messageView\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/sendMessageBatch.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.sendMessageBatch\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"items\"],\n          \"properties\": {\n            \"items\": {\n              \"type\": \"array\",\n              \"maxLength\": 100,\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#batchItem\"\n              }\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"items\"],\n          \"properties\": {\n            \"items\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"chat.bsky.convo.defs#messageView\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"batchItem\": {\n      \"type\": \"object\",\n      \"required\": [\"convoId\", \"message\"],\n      \"properties\": {\n        \"convoId\": { \"type\": \"string\" },\n        \"message\": {\n          \"type\": \"ref\",\n          \"ref\": \"chat.bsky.convo.defs#messageInput\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/unmuteConvo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.unmuteConvo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convo\"],\n          \"properties\": {\n            \"convo\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#convoView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/updateAllRead.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.updateAllRead\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"status\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"request\", \"accepted\"]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"updatedCount\"],\n          \"properties\": {\n            \"updatedCount\": {\n              \"description\": \"The count of updated convos.\",\n              \"type\": \"integer\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/convo/updateRead.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.convo.updateRead\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convoId\"],\n          \"properties\": {\n            \"convoId\": { \"type\": \"string\" },\n            \"messageId\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"convo\"],\n          \"properties\": {\n            \"convo\": {\n              \"type\": \"ref\",\n              \"ref\": \"chat.bsky.convo.defs#convoView\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/moderation/getActorMetadata.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.moderation.getActorMetadata\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"actor\"],\n        \"properties\": {\n          \"actor\": { \"type\": \"string\", \"format\": \"did\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"day\", \"month\", \"all\"],\n          \"properties\": {\n            \"day\": { \"type\": \"ref\", \"ref\": \"#metadata\" },\n            \"month\": { \"type\": \"ref\", \"ref\": \"#metadata\" },\n            \"all\": { \"type\": \"ref\", \"ref\": \"#metadata\" }\n          }\n        }\n      }\n    },\n    \"metadata\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"messagesSent\",\n        \"messagesReceived\",\n        \"convos\",\n        \"convosStarted\"\n      ],\n      \"properties\": {\n        \"messagesSent\": { \"type\": \"integer\" },\n        \"messagesReceived\": { \"type\": \"integer\" },\n        \"convos\": { \"type\": \"integer\" },\n        \"convosStarted\": { \"type\": \"integer\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/moderation/getMessageContext.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.moderation.getMessageContext\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"messageId\"],\n        \"properties\": {\n          \"convoId\": {\n            \"type\": \"string\",\n            \"description\": \"Conversation that the message is from. NOTE: this field will eventually be required.\"\n          },\n          \"messageId\": { \"type\": \"string\" },\n          \"before\": { \"type\": \"integer\", \"default\": 5 },\n          \"after\": { \"type\": \"integer\", \"default\": 5 }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"messages\"],\n          \"properties\": {\n            \"messages\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"chat.bsky.convo.defs#messageView\",\n                  \"chat.bsky.convo.defs#deletedMessageView\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/chat/bsky/moderation/updateActorAccess.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"chat.bsky.moderation.updateActorAccess\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actor\", \"allowAccess\"],\n          \"properties\": {\n            \"actor\": { \"type\": \"string\", \"format\": \"did\" },\n            \"allowAccess\": { \"type\": \"boolean\" },\n            \"ref\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.defs\",\n  \"defs\": {\n    \"statusAttr\": {\n      \"type\": \"object\",\n      \"required\": [\"applied\"],\n      \"properties\": {\n        \"applied\": { \"type\": \"boolean\" },\n        \"ref\": { \"type\": \"string\" }\n      }\n    },\n    \"accountView\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"handle\", \"indexedAt\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"email\": { \"type\": \"string\" },\n        \"relatedRecords\": { \"type\": \"array\", \"items\": { \"type\": \"unknown\" } },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"invitedBy\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.server.defs#inviteCode\"\n        },\n        \"invites\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.server.defs#inviteCode\"\n          }\n        },\n        \"invitesDisabled\": { \"type\": \"boolean\" },\n        \"emailConfirmedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"inviteNote\": { \"type\": \"string\" },\n        \"deactivatedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"threatSignatures\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#threatSignature\"\n          }\n        }\n      }\n    },\n    \"repoRef\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    },\n    \"repoBlobRef\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"cid\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"recordUri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"threatSignature\": {\n      \"type\": \"object\",\n      \"required\": [\"property\", \"value\"],\n      \"properties\": {\n        \"property\": { \"type\": \"string\" },\n        \"value\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/deleteAccount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.deleteAccount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete a user account as an administrator.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/disableAccountInvites.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.disableAccountInvites\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Disable an account from receiving new invite codes, but does not invalidate existing codes.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"account\"],\n          \"properties\": {\n            \"account\": { \"type\": \"string\", \"format\": \"did\" },\n            \"note\": {\n              \"type\": \"string\",\n              \"description\": \"Optional reason for disabled invites.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/disableInviteCodes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.disableInviteCodes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Disable some set of codes and/or all codes associated with a set of users.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"codes\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            },\n            \"accounts\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/enableAccountInvites.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.enableAccountInvites\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Re-enable an account's ability to receive invite codes.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"account\"],\n          \"properties\": {\n            \"account\": { \"type\": \"string\", \"format\": \"did\" },\n            \"note\": {\n              \"type\": \"string\",\n              \"description\": \"Optional reason for enabled invites.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/getAccountInfo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.getAccountInfo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about an account.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": { \"type\": \"string\", \"format\": \"did\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.admin.defs#accountView\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/getAccountInfos.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.getAccountInfos\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about some accounts.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"dids\"],\n        \"properties\": {\n          \"dids\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"did\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"infos\"],\n          \"properties\": {\n            \"infos\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"com.atproto.admin.defs#accountView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/getInviteCodes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.getInviteCodes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get an admin view of invite codes.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"sort\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"recent\", \"usage\"],\n            \"default\": \"recent\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 500,\n            \"default\": 100\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"codes\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"codes\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"com.atproto.server.defs#inviteCode\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/getSubjectStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.getSubjectStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get the service-specific admin status of a subject (account, record, or blob).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"did\": { \"type\": \"string\", \"format\": \"did\" },\n          \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n          \"blob\": { \"type\": \"string\", \"format\": \"cid\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\"],\n          \"properties\": {\n            \"subject\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"com.atproto.admin.defs#repoRef\",\n                \"com.atproto.repo.strongRef\",\n                \"com.atproto.admin.defs#repoBlobRef\"\n              ]\n            },\n            \"takedown\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.admin.defs#statusAttr\"\n            },\n            \"deactivated\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.admin.defs#statusAttr\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/searchAccounts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.searchAccounts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get list of accounts that matches your search query.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"email\": { \"type\": \"string\" },\n          \"cursor\": { \"type\": \"string\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"accounts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"accounts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"com.atproto.admin.defs#accountView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/sendEmail.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.sendEmail\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Send email to a user's account email address.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"recipientDid\", \"content\", \"senderDid\"],\n          \"properties\": {\n            \"recipientDid\": { \"type\": \"string\", \"format\": \"did\" },\n            \"content\": { \"type\": \"string\" },\n            \"subject\": { \"type\": \"string\" },\n            \"senderDid\": { \"type\": \"string\", \"format\": \"did\" },\n            \"comment\": {\n              \"type\": \"string\",\n              \"description\": \"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"sent\"],\n          \"properties\": {\n            \"sent\": { \"type\": \"boolean\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/updateAccountEmail.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.updateAccountEmail\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Administrative action to update an account's email.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"account\", \"email\"],\n          \"properties\": {\n            \"account\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\",\n              \"description\": \"The handle or DID of the repo.\"\n            },\n            \"email\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/updateAccountHandle.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.updateAccountHandle\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Administrative action to update an account's handle.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"handle\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"handle\": { \"type\": \"string\", \"format\": \"handle\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/updateAccountPassword.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.updateAccountPassword\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Update the password for a user account as an administrator.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"password\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"password\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/updateAccountSigningKey.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.updateAccountSigningKey\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Administrative action to update an account's signing key in their Did document.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"signingKey\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"signingKey\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"Did-key formatted public key\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/admin/updateSubjectStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.admin.updateSubjectStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Update the service-specific admin status of a subject (account, record, or blob).\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\"],\n          \"properties\": {\n            \"subject\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"com.atproto.admin.defs#repoRef\",\n                \"com.atproto.repo.strongRef\",\n                \"com.atproto.admin.defs#repoBlobRef\"\n              ]\n            },\n            \"takedown\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.admin.defs#statusAttr\"\n            },\n            \"deactivated\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.admin.defs#statusAttr\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\"],\n          \"properties\": {\n            \"subject\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"com.atproto.admin.defs#repoRef\",\n                \"com.atproto.repo.strongRef\",\n                \"com.atproto.admin.defs#repoBlobRef\"\n              ]\n            },\n            \"takedown\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.admin.defs#statusAttr\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.defs\",\n  \"defs\": {\n    \"identityInfo\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"handle\", \"didDoc\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": {\n          \"type\": \"string\",\n          \"format\": \"handle\",\n          \"description\": \"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.\"\n        },\n        \"didDoc\": {\n          \"type\": \"unknown\",\n          \"description\": \"The complete DID document for the identity.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/getRecommendedDidCredentials.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.getRecommendedDidCredentials\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Describe the credentials that should be included in the DID doc of an account that is migrating to this service.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"rotationKeys\": {\n              \"description\": \"Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.\",\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            },\n            \"alsoKnownAs\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            },\n            \"verificationMethods\": { \"type\": \"unknown\" },\n            \"services\": { \"type\": \"unknown\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/refreshIdentity.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.refreshIdentity\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"identifier\"],\n          \"properties\": {\n            \"identifier\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.identity.defs#identityInfo\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"HandleNotFound\",\n          \"description\": \"The resolution process confirmed that the handle does not resolve to any DID.\"\n        },\n        {\n          \"name\": \"DidNotFound\",\n          \"description\": \"The DID resolution process confirmed that there is no current DID.\"\n        },\n        {\n          \"name\": \"DidDeactivated\",\n          \"description\": \"The DID previously existed, but has been deactivated.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/requestPlcOperationSignature.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.requestPlcOperationSignature\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Request an email with a code to in order to request a signed PLC operation. Requires Auth.\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/resolveDid.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.resolveDid\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Resolves DID to DID document. Does not bi-directionally verify handle.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"DID to resolve.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"didDoc\"],\n          \"properties\": {\n            \"didDoc\": {\n              \"type\": \"unknown\",\n              \"description\": \"The complete DID document for the identity.\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"DidNotFound\",\n          \"description\": \"The DID resolution process confirmed that there is no current DID.\"\n        },\n        {\n          \"name\": \"DidDeactivated\",\n          \"description\": \"The DID previously existed, but has been deactivated.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/resolveHandle.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.resolveHandle\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"handle\"],\n        \"properties\": {\n          \"handle\": {\n            \"type\": \"string\",\n            \"format\": \"handle\",\n            \"description\": \"The handle to resolve.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"HandleNotFound\",\n          \"description\": \"The resolution process confirmed that the handle does not resolve to any DID.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/resolveIdentity.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.resolveIdentity\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"identifier\"],\n        \"properties\": {\n          \"identifier\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"Handle or DID to resolve.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.identity.defs#identityInfo\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"HandleNotFound\",\n          \"description\": \"The resolution process confirmed that the handle does not resolve to any DID.\"\n        },\n        {\n          \"name\": \"DidNotFound\",\n          \"description\": \"The DID resolution process confirmed that there is no current DID.\"\n        },\n        {\n          \"name\": \"DidDeactivated\",\n          \"description\": \"The DID previously existed, but has been deactivated.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/signPlcOperation.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.signPlcOperation\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Signs a PLC operation to update some value(s) in the requesting DID's document.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"token\": {\n              \"description\": \"A token received through com.atproto.identity.requestPlcOperationSignature\",\n              \"type\": \"string\"\n            },\n            \"rotationKeys\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            },\n            \"alsoKnownAs\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            },\n            \"verificationMethods\": { \"type\": \"unknown\" },\n            \"services\": { \"type\": \"unknown\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"operation\"],\n          \"properties\": {\n            \"operation\": {\n              \"type\": \"unknown\",\n              \"description\": \"A signed DID PLC operation.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/submitPlcOperation.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.submitPlcOperation\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"operation\"],\n          \"properties\": {\n            \"operation\": { \"type\": \"unknown\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/identity/updateHandle.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.identity.updateHandle\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"handle\"],\n          \"properties\": {\n            \"handle\": {\n              \"type\": \"string\",\n              \"format\": \"handle\",\n              \"description\": \"The new handle.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/label/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.label.defs\",\n  \"defs\": {\n    \"label\": {\n      \"type\": \"object\",\n      \"description\": \"Metadata tag on an atproto resource (eg, repo or record).\",\n      \"required\": [\"src\", \"uri\", \"val\", \"cts\"],\n      \"properties\": {\n        \"ver\": {\n          \"type\": \"integer\",\n          \"description\": \"The AT Protocol version of the label object.\"\n        },\n        \"src\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"DID of the actor who created this label.\"\n        },\n        \"uri\": {\n          \"type\": \"string\",\n          \"format\": \"uri\",\n          \"description\": \"AT URI of the record, repository (account), or other resource that this label applies to.\"\n        },\n        \"cid\": {\n          \"type\": \"string\",\n          \"format\": \"cid\",\n          \"description\": \"Optionally, CID specifying the specific version of 'uri' resource this label applies to.\"\n        },\n        \"val\": {\n          \"type\": \"string\",\n          \"maxLength\": 128,\n          \"description\": \"The short string name of the value or type of this label.\"\n        },\n        \"neg\": {\n          \"type\": \"boolean\",\n          \"description\": \"If true, this is a negation label, overwriting a previous label.\"\n        },\n        \"cts\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp when this label was created.\"\n        },\n        \"exp\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp at which this label expires (no longer applies).\"\n        },\n        \"sig\": {\n          \"type\": \"bytes\",\n          \"description\": \"Signature of dag-cbor encoded label.\"\n        }\n      }\n    },\n    \"selfLabels\": {\n      \"type\": \"object\",\n      \"description\": \"Metadata tags on an atproto record, published by the author within the record.\",\n      \"required\": [\"values\"],\n      \"properties\": {\n        \"values\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#selfLabel\" },\n          \"maxLength\": 10\n        }\n      }\n    },\n    \"selfLabel\": {\n      \"type\": \"object\",\n      \"description\": \"Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.\",\n      \"required\": [\"val\"],\n      \"properties\": {\n        \"val\": {\n          \"type\": \"string\",\n          \"maxLength\": 128,\n          \"description\": \"The short string name of the value or type of this label.\"\n        }\n      }\n    },\n    \"labelValueDefinition\": {\n      \"type\": \"object\",\n      \"description\": \"Declares a label value and its expected interpretations and behaviors.\",\n      \"required\": [\"identifier\", \"severity\", \"blurs\", \"locales\"],\n      \"properties\": {\n        \"identifier\": {\n          \"type\": \"string\",\n          \"description\": \"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).\",\n          \"maxLength\": 100,\n          \"maxGraphemes\": 100\n        },\n        \"severity\": {\n          \"type\": \"string\",\n          \"description\": \"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.\",\n          \"knownValues\": [\"inform\", \"alert\", \"none\"]\n        },\n        \"blurs\": {\n          \"type\": \"string\",\n          \"description\": \"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.\",\n          \"knownValues\": [\"content\", \"media\", \"none\"]\n        },\n        \"defaultSetting\": {\n          \"type\": \"string\",\n          \"description\": \"The default setting for this label.\",\n          \"knownValues\": [\"ignore\", \"warn\", \"hide\"],\n          \"default\": \"warn\"\n        },\n        \"adultOnly\": {\n          \"type\": \"boolean\",\n          \"description\": \"Does the user need to have adult content enabled in order to configure this label?\"\n        },\n        \"locales\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#labelValueDefinitionStrings\" }\n        }\n      }\n    },\n    \"labelValueDefinitionStrings\": {\n      \"type\": \"object\",\n      \"description\": \"Strings which describe the label in the UI, localized into a specific language.\",\n      \"required\": [\"lang\", \"name\", \"description\"],\n      \"properties\": {\n        \"lang\": {\n          \"type\": \"string\",\n          \"description\": \"The code of the language these strings are written in.\",\n          \"format\": \"language\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"A short human-readable name for the label.\",\n          \"maxGraphemes\": 64,\n          \"maxLength\": 640\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A longer description of what the label means and why it might be applied.\",\n          \"maxGraphemes\": 10000,\n          \"maxLength\": 100000\n        }\n      }\n    },\n    \"labelValue\": {\n      \"type\": \"string\",\n      \"knownValues\": [\n        \"!hide\",\n        \"!warn\",\n        \"!no-unauthenticated\",\n        \"porn\",\n        \"sexual\",\n        \"nudity\",\n        \"graphic-media\",\n        \"bot\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/label/queryLabels.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.label.queryLabels\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uriPatterns\"],\n        \"properties\": {\n          \"uriPatterns\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\" },\n            \"description\": \"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.\"\n          },\n          \"sources\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"did\" },\n            \"description\": \"Optional list of label sources (DIDs) to filter on.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 250,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"labels\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"labels\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/label/subscribeLabels.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.label.subscribeLabels\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"subscription\",\n      \"description\": \"Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"cursor\": {\n            \"type\": \"integer\",\n            \"description\": \"The last known event seq number to backfill from.\"\n          }\n        }\n      },\n      \"message\": {\n        \"schema\": {\n          \"type\": \"union\",\n          \"refs\": [\"#labels\", \"#info\"]\n        }\n      },\n      \"errors\": [{ \"name\": \"FutureCursor\" }]\n    },\n    \"labels\": {\n      \"type\": \"object\",\n      \"required\": [\"seq\", \"labels\"],\n      \"properties\": {\n        \"seq\": { \"type\": \"integer\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        }\n      }\n    },\n    \"info\": {\n      \"type\": \"object\",\n      \"required\": [\"name\"],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"OutdatedCursor\"]\n        },\n        \"message\": {\n          \"type\": \"string\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/lexicon/resolveLexicon.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.lexicon.resolveLexicon\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Resolves an atproto lexicon (NSID) to a schema.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"nsid\": {\n            \"format\": \"nsid\",\n            \"type\": \"string\",\n            \"description\": \"The lexicon NSID to resolve.\"\n          }\n        },\n        \"required\": [\"nsid\"]\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"cid\": {\n              \"type\": \"string\",\n              \"format\": \"cid\",\n              \"description\": \"The CID of the lexicon schema record.\"\n            },\n            \"schema\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.lexicon.schema#main\",\n              \"description\": \"The resolved lexicon schema record.\"\n            },\n            \"uri\": {\n              \"type\": \"string\",\n              \"format\": \"at-uri\",\n              \"description\": \"The AT-URI of the lexicon schema record.\"\n            }\n          },\n          \"required\": [\"uri\", \"cid\", \"schema\"]\n        }\n      },\n      \"errors\": [\n        {\n          \"description\": \"No lexicon was resolved for the NSID.\",\n          \"name\": \"LexiconNotFound\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/lexicon/schema.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.lexicon.schema\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).\",\n      \"key\": \"nsid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"lexicon\"],\n        \"properties\": {\n          \"lexicon\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/moderation/createReport.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.moderation.createReport\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"reasonType\", \"subject\"],\n          \"properties\": {\n            \"reasonType\": {\n              \"type\": \"ref\",\n              \"description\": \"Indicates the broad category of violation the report is for.\",\n              \"ref\": \"com.atproto.moderation.defs#reasonType\"\n            },\n            \"reason\": {\n              \"type\": \"string\",\n              \"maxGraphemes\": 2000,\n              \"maxLength\": 20000,\n              \"description\": \"Additional context about the content and violation.\"\n            },\n            \"subject\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"com.atproto.admin.defs#repoRef\",\n                \"com.atproto.repo.strongRef\"\n              ]\n            },\n            \"modTool\": {\n              \"type\": \"ref\",\n              \"ref\": \"#modTool\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\n            \"id\",\n            \"reasonType\",\n            \"subject\",\n            \"reportedBy\",\n            \"createdAt\"\n          ],\n          \"properties\": {\n            \"id\": { \"type\": \"integer\" },\n            \"reasonType\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.moderation.defs#reasonType\"\n            },\n            \"reason\": {\n              \"type\": \"string\",\n              \"maxGraphemes\": 2000,\n              \"maxLength\": 20000\n            },\n            \"subject\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"com.atproto.admin.defs#repoRef\",\n                \"com.atproto.repo.strongRef\"\n              ]\n            },\n            \"reportedBy\": { \"type\": \"string\", \"format\": \"did\" },\n            \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n          }\n        }\n      }\n    },\n    \"modTool\": {\n      \"type\": \"object\",\n      \"description\": \"Moderation tool information for tracing the source of the action\",\n      \"required\": [\"name\"],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')\"\n        },\n        \"meta\": {\n          \"type\": \"unknown\",\n          \"description\": \"Additional arbitrary metadata about the source\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/moderation/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.moderation.defs\",\n  \"defs\": {\n    \"reasonType\": {\n      \"type\": \"string\",\n      \"knownValues\": [\n        \"com.atproto.moderation.defs#reasonSpam\",\n        \"com.atproto.moderation.defs#reasonViolation\",\n        \"com.atproto.moderation.defs#reasonMisleading\",\n        \"com.atproto.moderation.defs#reasonSexual\",\n        \"com.atproto.moderation.defs#reasonRude\",\n        \"com.atproto.moderation.defs#reasonOther\",\n        \"com.atproto.moderation.defs#reasonAppeal\",\n\n        \"tools.ozone.report.defs#reasonAppeal\",\n        \"tools.ozone.report.defs#reasonOther\",\n\n        \"tools.ozone.report.defs#reasonViolenceAnimal\",\n        \"tools.ozone.report.defs#reasonViolenceThreats\",\n        \"tools.ozone.report.defs#reasonViolenceGraphicContent\",\n        \"tools.ozone.report.defs#reasonViolenceGlorification\",\n        \"tools.ozone.report.defs#reasonViolenceExtremistContent\",\n        \"tools.ozone.report.defs#reasonViolenceTrafficking\",\n        \"tools.ozone.report.defs#reasonViolenceOther\",\n\n        \"tools.ozone.report.defs#reasonSexualAbuseContent\",\n        \"tools.ozone.report.defs#reasonSexualNCII\",\n        \"tools.ozone.report.defs#reasonSexualDeepfake\",\n        \"tools.ozone.report.defs#reasonSexualAnimal\",\n        \"tools.ozone.report.defs#reasonSexualUnlabeled\",\n        \"tools.ozone.report.defs#reasonSexualOther\",\n\n        \"tools.ozone.report.defs#reasonChildSafetyCSAM\",\n        \"tools.ozone.report.defs#reasonChildSafetyGroom\",\n        \"tools.ozone.report.defs#reasonChildSafetyPrivacy\",\n        \"tools.ozone.report.defs#reasonChildSafetyHarassment\",\n        \"tools.ozone.report.defs#reasonChildSafetyOther\",\n\n        \"tools.ozone.report.defs#reasonHarassmentTroll\",\n        \"tools.ozone.report.defs#reasonHarassmentTargeted\",\n        \"tools.ozone.report.defs#reasonHarassmentHateSpeech\",\n        \"tools.ozone.report.defs#reasonHarassmentDoxxing\",\n        \"tools.ozone.report.defs#reasonHarassmentOther\",\n\n        \"tools.ozone.report.defs#reasonMisleadingBot\",\n        \"tools.ozone.report.defs#reasonMisleadingImpersonation\",\n        \"tools.ozone.report.defs#reasonMisleadingSpam\",\n        \"tools.ozone.report.defs#reasonMisleadingScam\",\n        \"tools.ozone.report.defs#reasonMisleadingElections\",\n        \"tools.ozone.report.defs#reasonMisleadingOther\",\n\n        \"tools.ozone.report.defs#reasonRuleSiteSecurity\",\n        \"tools.ozone.report.defs#reasonRuleProhibitedSales\",\n        \"tools.ozone.report.defs#reasonRuleBanEvasion\",\n        \"tools.ozone.report.defs#reasonRuleOther\",\n\n        \"tools.ozone.report.defs#reasonSelfHarmContent\",\n        \"tools.ozone.report.defs#reasonSelfHarmED\",\n        \"tools.ozone.report.defs#reasonSelfHarmStunts\",\n        \"tools.ozone.report.defs#reasonSelfHarmSubstances\",\n        \"tools.ozone.report.defs#reasonSelfHarmOther\"\n      ]\n    },\n    \"reasonSpam\": {\n      \"type\": \"token\",\n      \"description\": \"Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.\"\n    },\n    \"reasonViolation\": {\n      \"type\": \"token\",\n      \"description\": \"Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.\"\n    },\n    \"reasonMisleading\": {\n      \"type\": \"token\",\n      \"description\": \"Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.\"\n    },\n    \"reasonSexual\": {\n      \"type\": \"token\",\n      \"description\": \"Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.\"\n    },\n    \"reasonRude\": {\n      \"type\": \"token\",\n      \"description\": \"Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.\"\n    },\n    \"reasonOther\": {\n      \"type\": \"token\",\n      \"description\": \"Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.\"\n    },\n    \"reasonAppeal\": {\n      \"type\": \"token\",\n      \"description\": \"Appeal a previously taken moderation action\"\n    },\n    \"subjectType\": {\n      \"type\": \"string\",\n      \"description\": \"Tag describing a type of subject that might be reported.\",\n      \"knownValues\": [\"account\", \"record\", \"chat\"]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/applyWrites.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.applyWrites\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repo\", \"writes\"],\n          \"properties\": {\n            \"repo\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\",\n              \"description\": \"The handle or DID of the repo (aka, current account).\"\n            },\n            \"validate\": {\n              \"type\": \"boolean\",\n              \"description\": \"Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.\"\n            },\n            \"writes\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\"#create\", \"#update\", \"#delete\"],\n                \"closed\": true\n              }\n            },\n            \"swapCommit\": {\n              \"type\": \"string\",\n              \"description\": \"If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.\",\n              \"format\": \"cid\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [],\n          \"properties\": {\n            \"commit\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.repo.defs#commitMeta\"\n            },\n            \"results\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\"#createResult\", \"#updateResult\", \"#deleteResult\"],\n                \"closed\": true\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidSwap\",\n          \"description\": \"Indicates that the 'swapCommit' parameter did not match current commit.\"\n        }\n      ]\n    },\n    \"create\": {\n      \"type\": \"object\",\n      \"description\": \"Operation which creates a new record.\",\n      \"required\": [\"collection\", \"value\"],\n      \"properties\": {\n        \"collection\": { \"type\": \"string\", \"format\": \"nsid\" },\n        \"rkey\": {\n          \"type\": \"string\",\n          \"maxLength\": 512,\n          \"format\": \"record-key\",\n          \"description\": \"NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.\"\n        },\n        \"value\": { \"type\": \"unknown\" }\n      }\n    },\n    \"update\": {\n      \"type\": \"object\",\n      \"description\": \"Operation which updates an existing record.\",\n      \"required\": [\"collection\", \"rkey\", \"value\"],\n      \"properties\": {\n        \"collection\": { \"type\": \"string\", \"format\": \"nsid\" },\n        \"rkey\": { \"type\": \"string\", \"format\": \"record-key\" },\n        \"value\": { \"type\": \"unknown\" }\n      }\n    },\n    \"delete\": {\n      \"type\": \"object\",\n      \"description\": \"Operation which deletes an existing record.\",\n      \"required\": [\"collection\", \"rkey\"],\n      \"properties\": {\n        \"collection\": { \"type\": \"string\", \"format\": \"nsid\" },\n        \"rkey\": { \"type\": \"string\", \"format\": \"record-key\" }\n      }\n    },\n    \"createResult\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"validationStatus\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"valid\", \"unknown\"]\n        }\n      }\n    },\n    \"updateResult\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"validationStatus\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"valid\", \"unknown\"]\n        }\n      }\n    },\n    \"deleteResult\": {\n      \"type\": \"object\",\n      \"required\": [],\n      \"properties\": {}\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/createRecord.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.createRecord\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create a single new repository record. Requires auth, implemented by PDS.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repo\", \"collection\", \"record\"],\n          \"properties\": {\n            \"repo\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\",\n              \"description\": \"The handle or DID of the repo (aka, current account).\"\n            },\n            \"collection\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\",\n              \"description\": \"The NSID of the record collection.\"\n            },\n            \"rkey\": {\n              \"type\": \"string\",\n              \"format\": \"record-key\",\n              \"description\": \"The Record Key.\",\n              \"maxLength\": 512\n            },\n            \"validate\": {\n              \"type\": \"boolean\",\n              \"description\": \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\"\n            },\n            \"record\": {\n              \"type\": \"unknown\",\n              \"description\": \"The record itself. Must contain a $type field.\"\n            },\n            \"swapCommit\": {\n              \"type\": \"string\",\n              \"format\": \"cid\",\n              \"description\": \"Compare and swap with the previous commit by CID.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"cid\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"commit\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.repo.defs#commitMeta\"\n            },\n            \"validationStatus\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"valid\", \"unknown\"]\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidSwap\",\n          \"description\": \"Indicates that 'swapCommit' didn't match current repo commit.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.defs\",\n  \"defs\": {\n    \"commitMeta\": {\n      \"type\": \"object\",\n      \"required\": [\"cid\", \"rev\"],\n      \"properties\": {\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"rev\": { \"type\": \"string\", \"format\": \"tid\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/deleteRecord.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.deleteRecord\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repo\", \"collection\", \"rkey\"],\n          \"properties\": {\n            \"repo\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\",\n              \"description\": \"The handle or DID of the repo (aka, current account).\"\n            },\n            \"collection\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\",\n              \"description\": \"The NSID of the record collection.\"\n            },\n            \"rkey\": {\n              \"type\": \"string\",\n              \"format\": \"record-key\",\n              \"description\": \"The Record Key.\"\n            },\n            \"swapRecord\": {\n              \"type\": \"string\",\n              \"format\": \"cid\",\n              \"description\": \"Compare and swap with the previous record by CID.\"\n            },\n            \"swapCommit\": {\n              \"type\": \"string\",\n              \"format\": \"cid\",\n              \"description\": \"Compare and swap with the previous commit by CID.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"commit\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.repo.defs#commitMeta\"\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"InvalidSwap\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/describeRepo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.describeRepo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get information about an account and repository, including the list of collections. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"repo\"],\n        \"properties\": {\n          \"repo\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"The handle or DID of the repo.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\n            \"handle\",\n            \"did\",\n            \"didDoc\",\n            \"collections\",\n            \"handleIsCorrect\"\n          ],\n          \"properties\": {\n            \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"didDoc\": {\n              \"type\": \"unknown\",\n              \"description\": \"The complete DID document for this account.\"\n            },\n            \"collections\": {\n              \"type\": \"array\",\n              \"description\": \"List of all the collections (NSIDs) for which this repo contains at least one record.\",\n              \"items\": { \"type\": \"string\", \"format\": \"nsid\" }\n            },\n            \"handleIsCorrect\": {\n              \"type\": \"boolean\",\n              \"description\": \"Indicates if handle is currently valid (resolves bi-directionally)\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/getRecord.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.getRecord\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a single record from a repository. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"repo\", \"collection\", \"rkey\"],\n        \"properties\": {\n          \"repo\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"The handle or DID of the repo.\"\n          },\n          \"collection\": {\n            \"type\": \"string\",\n            \"format\": \"nsid\",\n            \"description\": \"The NSID of the record collection.\"\n          },\n          \"rkey\": {\n            \"type\": \"string\",\n            \"description\": \"The Record Key.\",\n            \"format\": \"record-key\"\n          },\n          \"cid\": {\n            \"type\": \"string\",\n            \"format\": \"cid\",\n            \"description\": \"The CID of the version of the record. If not specified, then return the most recent version.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"value\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"value\": { \"type\": \"unknown\" }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"RecordNotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/importRepo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.importRepo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.\",\n      \"input\": {\n        \"encoding\": \"application/vnd.ipld.car\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/listMissingBlobs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.listMissingBlobs\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 1000,\n            \"default\": 500\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"blobs\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"blobs\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#recordBlob\" }\n            }\n          }\n        }\n      }\n    },\n    \"recordBlob\": {\n      \"type\": \"object\",\n      \"required\": [\"cid\", \"recordUri\"],\n      \"properties\": {\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"recordUri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/listRecords.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.listRecords\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List a range of records in a repository, matching a specific collection. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"repo\", \"collection\"],\n        \"properties\": {\n          \"repo\": {\n            \"type\": \"string\",\n            \"format\": \"at-identifier\",\n            \"description\": \"The handle or DID of the repo.\"\n          },\n          \"collection\": {\n            \"type\": \"string\",\n            \"format\": \"nsid\",\n            \"description\": \"The NSID of the record type.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50,\n            \"description\": \"The number of records to return.\"\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"reverse\": {\n            \"type\": \"boolean\",\n            \"description\": \"Flag to reverse the order of the returned records.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"records\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"records\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#record\" }\n            }\n          }\n        }\n      }\n    },\n    \"record\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\", \"value\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"value\": { \"type\": \"unknown\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/putRecord.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.putRecord\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repo\", \"collection\", \"rkey\", \"record\"],\n          \"nullable\": [\"swapRecord\"],\n          \"properties\": {\n            \"repo\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\",\n              \"description\": \"The handle or DID of the repo (aka, current account).\"\n            },\n            \"collection\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\",\n              \"description\": \"The NSID of the record collection.\"\n            },\n            \"rkey\": {\n              \"type\": \"string\",\n              \"format\": \"record-key\",\n              \"description\": \"The Record Key.\",\n              \"maxLength\": 512\n            },\n            \"validate\": {\n              \"type\": \"boolean\",\n              \"description\": \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\"\n            },\n            \"record\": {\n              \"type\": \"unknown\",\n              \"description\": \"The record to write.\"\n            },\n            \"swapRecord\": {\n              \"type\": \"string\",\n              \"format\": \"cid\",\n              \"description\": \"Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation\"\n            },\n            \"swapCommit\": {\n              \"type\": \"string\",\n              \"format\": \"cid\",\n              \"description\": \"Compare and swap with the previous commit by CID.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uri\", \"cid\"],\n          \"properties\": {\n            \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"commit\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.repo.defs#commitMeta\"\n            },\n            \"validationStatus\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"valid\", \"unknown\"]\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"InvalidSwap\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/strongRef.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.strongRef\",\n  \"description\": \"A URI with a content-hash fingerprint.\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\", \"cid\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/repo/uploadBlob.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.repo.uploadBlob\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.\",\n      \"input\": {\n        \"encoding\": \"*/*\"\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"blob\"],\n          \"properties\": {\n            \"blob\": { \"type\": \"blob\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/activateAccount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.activateAccount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/checkAccountStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.checkAccountStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\n            \"activated\",\n            \"validDid\",\n            \"repoCommit\",\n            \"repoRev\",\n            \"repoBlocks\",\n            \"indexedRecords\",\n            \"privateStateValues\",\n            \"expectedBlobs\",\n            \"importedBlobs\"\n          ],\n          \"properties\": {\n            \"activated\": { \"type\": \"boolean\" },\n            \"validDid\": { \"type\": \"boolean\" },\n            \"repoCommit\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"repoRev\": { \"type\": \"string\" },\n            \"repoBlocks\": { \"type\": \"integer\" },\n            \"indexedRecords\": { \"type\": \"integer\" },\n            \"privateStateValues\": { \"type\": \"integer\" },\n            \"expectedBlobs\": { \"type\": \"integer\" },\n            \"importedBlobs\": { \"type\": \"integer\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/confirmEmail.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.confirmEmail\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Confirm an email using a token from com.atproto.server.requestEmailConfirmation.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"email\", \"token\"],\n          \"properties\": {\n            \"email\": { \"type\": \"string\" },\n            \"token\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"AccountNotFound\" },\n        { \"name\": \"ExpiredToken\" },\n        { \"name\": \"InvalidToken\" },\n        { \"name\": \"InvalidEmail\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/createAccount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.createAccount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create an account. Implemented by PDS.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"handle\"],\n          \"properties\": {\n            \"email\": { \"type\": \"string\" },\n            \"handle\": {\n              \"type\": \"string\",\n              \"format\": \"handle\",\n              \"description\": \"Requested handle for the account.\"\n            },\n            \"did\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"Pre-existing atproto DID, being imported to a new account.\"\n            },\n            \"inviteCode\": { \"type\": \"string\" },\n            \"verificationCode\": { \"type\": \"string\" },\n            \"verificationPhone\": { \"type\": \"string\" },\n            \"password\": {\n              \"type\": \"string\",\n              \"description\": \"Initial account password. May need to meet instance-specific password strength requirements.\"\n            },\n            \"recoveryKey\": {\n              \"type\": \"string\",\n              \"description\": \"DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.\"\n            },\n            \"plcOp\": {\n              \"type\": \"unknown\",\n              \"description\": \"A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"description\": \"Account login session returned on successful account creation.\",\n          \"required\": [\"accessJwt\", \"refreshJwt\", \"handle\", \"did\"],\n          \"properties\": {\n            \"accessJwt\": { \"type\": \"string\" },\n            \"refreshJwt\": { \"type\": \"string\" },\n            \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n            \"did\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"The DID of the new account.\"\n            },\n            \"didDoc\": {\n              \"type\": \"unknown\",\n              \"description\": \"Complete DID document.\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"InvalidHandle\" },\n        { \"name\": \"InvalidPassword\" },\n        { \"name\": \"InvalidInviteCode\" },\n        { \"name\": \"HandleNotAvailable\" },\n        { \"name\": \"UnsupportedDomain\" },\n        { \"name\": \"UnresolvableDid\" },\n        { \"name\": \"IncompatibleDidDoc\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/createAppPassword.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.createAppPassword\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create an App Password.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"name\"],\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"A short name for the App Password, to help distinguish them.\"\n            },\n            \"privileged\": {\n              \"type\": \"boolean\",\n              \"description\": \"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"#appPassword\"\n        }\n      },\n      \"errors\": [{ \"name\": \"AccountTakedown\" }]\n    },\n    \"appPassword\": {\n      \"type\": \"object\",\n      \"required\": [\"name\", \"password\", \"createdAt\"],\n      \"properties\": {\n        \"name\": { \"type\": \"string\" },\n        \"password\": { \"type\": \"string\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"privileged\": { \"type\": \"boolean\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/createInviteCode.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.createInviteCode\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create an invite code.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"useCount\"],\n          \"properties\": {\n            \"useCount\": { \"type\": \"integer\" },\n            \"forAccount\": { \"type\": \"string\", \"format\": \"did\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"code\"],\n          \"properties\": {\n            \"code\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/createInviteCodes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.createInviteCodes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create invite codes.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"codeCount\", \"useCount\"],\n          \"properties\": {\n            \"codeCount\": { \"type\": \"integer\", \"default\": 1 },\n            \"useCount\": { \"type\": \"integer\" },\n            \"forAccounts\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\", \"format\": \"did\" }\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"codes\"],\n          \"properties\": {\n            \"codes\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#accountCodes\" }\n            }\n          }\n        }\n      }\n    },\n    \"accountCodes\": {\n      \"type\": \"object\",\n      \"required\": [\"account\", \"codes\"],\n      \"properties\": {\n        \"account\": { \"type\": \"string\" },\n        \"codes\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/createSession.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.createSession\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create an authentication session.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"identifier\", \"password\"],\n          \"properties\": {\n            \"identifier\": {\n              \"type\": \"string\",\n              \"description\": \"Handle or other identifier supported by the server for the authenticating user.\"\n            },\n            \"password\": { \"type\": \"string\" },\n            \"authFactorToken\": { \"type\": \"string\" },\n            \"allowTakendown\": {\n              \"type\": \"boolean\",\n              \"description\": \"When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"accessJwt\", \"refreshJwt\", \"handle\", \"did\"],\n          \"properties\": {\n            \"accessJwt\": { \"type\": \"string\" },\n            \"refreshJwt\": { \"type\": \"string\" },\n            \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"didDoc\": { \"type\": \"unknown\" },\n            \"email\": { \"type\": \"string\" },\n            \"emailConfirmed\": { \"type\": \"boolean\" },\n            \"emailAuthFactor\": { \"type\": \"boolean\" },\n            \"active\": { \"type\": \"boolean\" },\n            \"status\": {\n              \"type\": \"string\",\n              \"description\": \"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.\",\n              \"knownValues\": [\"takendown\", \"suspended\", \"deactivated\"]\n            }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"AccountTakedown\" },\n        { \"name\": \"AuthFactorTokenRequired\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/deactivateAccount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.deactivateAccount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"deleteAfter\": {\n              \"type\": \"string\",\n              \"format\": \"datetime\",\n              \"description\": \"A recommendation to server as to how long they should hold onto the deactivated account before deleting.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.defs\",\n  \"defs\": {\n    \"inviteCode\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"code\",\n        \"available\",\n        \"disabled\",\n        \"forAccount\",\n        \"createdBy\",\n        \"createdAt\",\n        \"uses\"\n      ],\n      \"properties\": {\n        \"code\": { \"type\": \"string\" },\n        \"available\": { \"type\": \"integer\" },\n        \"disabled\": { \"type\": \"boolean\" },\n        \"forAccount\": { \"type\": \"string\" },\n        \"createdBy\": { \"type\": \"string\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"uses\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#inviteCodeUse\" }\n        }\n      }\n    },\n    \"inviteCodeUse\": {\n      \"type\": \"object\",\n      \"required\": [\"usedBy\", \"usedAt\"],\n      \"properties\": {\n        \"usedBy\": { \"type\": \"string\", \"format\": \"did\" },\n        \"usedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/deleteAccount.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.deleteAccount\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"password\", \"token\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"password\": { \"type\": \"string\" },\n            \"token\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"ExpiredToken\" }, { \"name\": \"InvalidToken\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/deleteSession.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.deleteSession\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n      \"errors\": [{ \"name\": \"InvalidToken\" }, { \"name\": \"ExpiredToken\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/describeServer.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.describeServer\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Describes the server's account creation requirements and capabilities. Implemented by PDS.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"availableUserDomains\"],\n          \"properties\": {\n            \"inviteCodeRequired\": {\n              \"type\": \"boolean\",\n              \"description\": \"If true, an invite code must be supplied to create an account on this instance.\"\n            },\n            \"phoneVerificationRequired\": {\n              \"type\": \"boolean\",\n              \"description\": \"If true, a phone verification token must be supplied to create an account on this instance.\"\n            },\n            \"availableUserDomains\": {\n              \"type\": \"array\",\n              \"description\": \"List of domain suffixes that can be used in account handles.\",\n              \"items\": { \"type\": \"string\" }\n            },\n            \"links\": {\n              \"type\": \"ref\",\n              \"description\": \"URLs of service policy documents.\",\n              \"ref\": \"#links\"\n            },\n            \"contact\": {\n              \"type\": \"ref\",\n              \"description\": \"Contact information\",\n              \"ref\": \"#contact\"\n            },\n            \"did\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      }\n    },\n    \"links\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"privacyPolicy\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"termsOfService\": { \"type\": \"string\", \"format\": \"uri\" }\n      }\n    },\n    \"contact\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"email\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/getAccountInviteCodes.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.getAccountInviteCodes\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get all invite codes for the current account. Requires auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"includeUsed\": { \"type\": \"boolean\", \"default\": true },\n          \"createAvailable\": {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Controls whether any new 'earned' but not 'created' invites should be created.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"codes\"],\n          \"properties\": {\n            \"codes\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"com.atproto.server.defs#inviteCode\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"DuplicateCreate\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/getServiceAuth.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.getServiceAuth\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a signed token on behalf of the requesting DID for the requested service.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"aud\"],\n        \"properties\": {\n          \"aud\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the service that the token will be used to authenticate with\"\n          },\n          \"exp\": {\n            \"type\": \"integer\",\n            \"description\": \"The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.\"\n          },\n          \"lxm\": {\n            \"type\": \"string\",\n            \"format\": \"nsid\",\n            \"description\": \"Lexicon (XRPC) method to bind the requested token to\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"token\"],\n          \"properties\": {\n            \"token\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"BadExpiration\",\n          \"description\": \"Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/getSession.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.getSession\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get information about the current auth session. Requires auth.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"handle\", \"did\"],\n          \"properties\": {\n            \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"didDoc\": { \"type\": \"unknown\" },\n            \"email\": { \"type\": \"string\" },\n            \"emailConfirmed\": { \"type\": \"boolean\" },\n            \"emailAuthFactor\": { \"type\": \"boolean\" },\n            \"active\": { \"type\": \"boolean\" },\n            \"status\": {\n              \"type\": \"string\",\n              \"description\": \"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.\",\n              \"knownValues\": [\"takendown\", \"suspended\", \"deactivated\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/listAppPasswords.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.listAppPasswords\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List all App Passwords.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"passwords\"],\n          \"properties\": {\n            \"passwords\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#appPassword\" }\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"AccountTakedown\" }]\n    },\n    \"appPassword\": {\n      \"type\": \"object\",\n      \"required\": [\"name\", \"createdAt\"],\n      \"properties\": {\n        \"name\": { \"type\": \"string\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"privileged\": { \"type\": \"boolean\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/refreshSession.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.refreshSession\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"accessJwt\", \"refreshJwt\", \"handle\", \"did\"],\n          \"properties\": {\n            \"accessJwt\": { \"type\": \"string\" },\n            \"refreshJwt\": { \"type\": \"string\" },\n            \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"didDoc\": { \"type\": \"unknown\" },\n            \"email\": { \"type\": \"string\" },\n            \"emailConfirmed\": { \"type\": \"boolean\" },\n            \"emailAuthFactor\": { \"type\": \"boolean\" },\n            \"active\": { \"type\": \"boolean\" },\n            \"status\": {\n              \"type\": \"string\",\n              \"description\": \"Hosting status of the account. If not specified, then assume 'active'.\",\n              \"knownValues\": [\"takendown\", \"suspended\", \"deactivated\"]\n            }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"AccountTakedown\" },\n        { \"name\": \"InvalidToken\" },\n        { \"name\": \"ExpiredToken\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/requestAccountDelete.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.requestAccountDelete\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Initiate a user account deletion via email.\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/requestEmailConfirmation.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.requestEmailConfirmation\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Request an email with a code to confirm ownership of email.\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/requestEmailUpdate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.requestEmailUpdate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Request a token in order to update email.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"tokenRequired\"],\n          \"properties\": {\n            \"tokenRequired\": { \"type\": \"boolean\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/requestPasswordReset.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.requestPasswordReset\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Initiate a user account password reset via email.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"email\"],\n          \"properties\": {\n            \"email\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/reserveSigningKey.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.reserveSigningKey\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"did\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"The DID to reserve a key for.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"signingKey\"],\n          \"properties\": {\n            \"signingKey\": {\n              \"type\": \"string\",\n              \"description\": \"The public key for the reserved signing key, in did:key serialization.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/resetPassword.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.resetPassword\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Reset a user account password using a token.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"token\", \"password\"],\n          \"properties\": {\n            \"token\": { \"type\": \"string\" },\n            \"password\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"ExpiredToken\" }, { \"name\": \"InvalidToken\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/revokeAppPassword.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.revokeAppPassword\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Revoke an App Password by name.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"name\"],\n          \"properties\": {\n            \"name\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/server/updateEmail.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.server.updateEmail\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Update an account's email.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"email\"],\n          \"properties\": {\n            \"email\": { \"type\": \"string\" },\n            \"emailAuthFactor\": { \"type\": \"boolean\" },\n            \"token\": {\n              \"type\": \"string\",\n              \"description\": \"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"ExpiredToken\" },\n        { \"name\": \"InvalidToken\" },\n        { \"name\": \"TokenRequired\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.defs\",\n  \"defs\": {\n    \"hostStatus\": {\n      \"type\": \"string\",\n      \"knownValues\": [\"active\", \"idle\", \"offline\", \"throttled\", \"banned\"]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getBlob.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getBlob\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\", \"cid\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the account.\"\n          },\n          \"cid\": {\n            \"type\": \"string\",\n            \"format\": \"cid\",\n            \"description\": \"The CID of the blob to fetch\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"*/*\"\n      },\n      \"errors\": [\n        { \"name\": \"BlobNotFound\" },\n        { \"name\": \"RepoNotFound\" },\n        { \"name\": \"RepoTakendown\" },\n        { \"name\": \"RepoSuspended\" },\n        { \"name\": \"RepoDeactivated\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getBlocks.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getBlocks\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\", \"cids\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          },\n          \"cids\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\", \"format\": \"cid\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/vnd.ipld.car\"\n      },\n      \"errors\": [\n        { \"name\": \"BlockNotFound\" },\n        { \"name\": \"RepoNotFound\" },\n        { \"name\": \"RepoTakendown\" },\n        { \"name\": \"RepoSuspended\" },\n        { \"name\": \"RepoDeactivated\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getCheckout.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getCheckout\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"DEPRECATED - please use com.atproto.sync.getRepo instead\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/vnd.ipld.car\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getHead.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getHead\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"DEPRECATED - please use com.atproto.sync.getLatestCommit instead\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"root\"],\n          \"properties\": {\n            \"root\": { \"type\": \"string\", \"format\": \"cid\" }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"HeadNotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getHostStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getHostStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Returns information about a specified upstream host, as consumed by the server. Implemented by relays.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"hostname\"],\n        \"properties\": {\n          \"hostname\": {\n            \"type\": \"string\",\n            \"description\": \"Hostname of the host (eg, PDS or relay) being queried.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"hostname\"],\n          \"properties\": {\n            \"hostname\": { \"type\": \"string\" },\n            \"seq\": {\n              \"type\": \"integer\",\n              \"description\": \"Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).\"\n            },\n            \"accountCount\": {\n              \"type\": \"integer\",\n              \"description\": \"Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.\"\n            },\n            \"status\": {\n              \"type\": \"ref\",\n              \"ref\": \"com.atproto.sync.defs#hostStatus\"\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"HostNotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getLatestCommit.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getLatestCommit\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get the current commit CID & revision of the specified repo. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"cid\", \"rev\"],\n          \"properties\": {\n            \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n            \"rev\": { \"type\": \"string\", \"format\": \"tid\" }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"RepoNotFound\" },\n        { \"name\": \"RepoTakendown\" },\n        { \"name\": \"RepoSuspended\" },\n        { \"name\": \"RepoDeactivated\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getRecord.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getRecord\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\", \"collection\", \"rkey\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          },\n          \"collection\": { \"type\": \"string\", \"format\": \"nsid\" },\n          \"rkey\": {\n            \"type\": \"string\",\n            \"description\": \"Record Key\",\n            \"format\": \"record-key\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/vnd.ipld.car\"\n      },\n      \"errors\": [\n        { \"name\": \"RecordNotFound\" },\n        { \"name\": \"RepoNotFound\" },\n        { \"name\": \"RepoTakendown\" },\n        { \"name\": \"RepoSuspended\" },\n        { \"name\": \"RepoDeactivated\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getRepo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getRepo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          },\n          \"since\": {\n            \"type\": \"string\",\n            \"format\": \"tid\",\n            \"description\": \"The revision ('rev') of the repo to create a diff from.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/vnd.ipld.car\"\n      },\n      \"errors\": [\n        { \"name\": \"RepoNotFound\" },\n        { \"name\": \"RepoTakendown\" },\n        { \"name\": \"RepoSuspended\" },\n        { \"name\": \"RepoDeactivated\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/getRepoStatus.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.getRepoStatus\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"active\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"active\": { \"type\": \"boolean\" },\n            \"status\": {\n              \"type\": \"string\",\n              \"description\": \"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.\",\n              \"knownValues\": [\n                \"takendown\",\n                \"suspended\",\n                \"deleted\",\n                \"deactivated\",\n                \"desynchronized\",\n                \"throttled\"\n              ]\n            },\n            \"rev\": {\n              \"type\": \"string\",\n              \"format\": \"tid\",\n              \"description\": \"Optional field, the current rev of the repo, if active=true\"\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"RepoNotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/listBlobs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.listBlobs\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"The DID of the repo.\"\n          },\n          \"since\": {\n            \"type\": \"string\",\n            \"format\": \"tid\",\n            \"description\": \"Optional revision of the repo to list blobs since.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 1000,\n            \"default\": 500\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"cids\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"cids\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\", \"format\": \"cid\" }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        { \"name\": \"RepoNotFound\" },\n        { \"name\": \"RepoTakendown\" },\n        { \"name\": \"RepoSuspended\" },\n        { \"name\": \"RepoDeactivated\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/listHosts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.listHosts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 1000,\n            \"default\": 200\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"hosts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"hosts\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#host\" },\n              \"description\": \"Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.\"\n            }\n          }\n        }\n      }\n    },\n    \"host\": {\n      \"type\": \"object\",\n      \"required\": [\"hostname\"],\n      \"properties\": {\n        \"hostname\": {\n          \"type\": \"string\",\n          \"description\": \"hostname of server; not a URL (no scheme)\"\n        },\n        \"seq\": {\n          \"type\": \"integer\",\n          \"description\": \"Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).\"\n        },\n        \"accountCount\": { \"type\": \"integer\" },\n        \"status\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.sync.defs#hostStatus\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/listRepos.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.listRepos\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 1000,\n            \"default\": 500\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repos\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"repos\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#repo\" }\n            }\n          }\n        }\n      }\n    },\n    \"repo\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"head\", \"rev\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"head\": {\n          \"type\": \"string\",\n          \"format\": \"cid\",\n          \"description\": \"Current repo commit CID\"\n        },\n        \"rev\": { \"type\": \"string\", \"format\": \"tid\" },\n        \"active\": { \"type\": \"boolean\" },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.\",\n          \"knownValues\": [\n            \"takendown\",\n            \"suspended\",\n            \"deleted\",\n            \"deactivated\",\n            \"desynchronized\",\n            \"throttled\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/listReposByCollection.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.listReposByCollection\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Enumerates all the DIDs which have records with the given collection NSID.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"collection\"],\n        \"properties\": {\n          \"collection\": { \"type\": \"string\", \"format\": \"nsid\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"description\": \"Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.\",\n            \"minimum\": 1,\n            \"maximum\": 2000,\n            \"default\": 500\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repos\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"repos\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"#repo\" }\n            }\n          }\n        }\n      }\n    },\n    \"repo\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/notifyOfUpdate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.notifyOfUpdate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"hostname\"],\n          \"properties\": {\n            \"hostname\": {\n              \"type\": \"string\",\n              \"description\": \"Hostname of the current service (usually a PDS) that is notifying of update.\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/requestCrawl.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.requestCrawl\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"hostname\"],\n          \"properties\": {\n            \"hostname\": {\n              \"type\": \"string\",\n              \"description\": \"Hostname of the current service (eg, PDS) that is requesting to be crawled.\"\n            }\n          }\n        }\n      },\n      \"errors\": [{ \"name\": \"HostBanned\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/sync/subscribeRepos.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.sync.subscribeRepos\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"subscription\",\n      \"description\": \"Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"cursor\": {\n            \"type\": \"integer\",\n            \"description\": \"The last known event seq number to backfill from.\"\n          }\n        }\n      },\n      \"message\": {\n        \"schema\": {\n          \"type\": \"union\",\n          \"refs\": [\"#commit\", \"#sync\", \"#identity\", \"#account\", \"#info\"]\n        }\n      },\n      \"errors\": [\n        { \"name\": \"FutureCursor\" },\n        {\n          \"name\": \"ConsumerTooSlow\",\n          \"description\": \"If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.\"\n        }\n      ]\n    },\n    \"commit\": {\n      \"type\": \"object\",\n      \"description\": \"Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.\",\n      \"required\": [\n        \"seq\",\n        \"rebase\",\n        \"tooBig\",\n        \"repo\",\n        \"commit\",\n        \"rev\",\n        \"since\",\n        \"blocks\",\n        \"ops\",\n        \"blobs\",\n        \"time\"\n      ],\n      \"nullable\": [\"since\"],\n      \"properties\": {\n        \"seq\": {\n          \"type\": \"integer\",\n          \"description\": \"The stream sequence number of this message.\"\n        },\n        \"rebase\": { \"type\": \"boolean\", \"description\": \"DEPRECATED -- unused\" },\n        \"tooBig\": {\n          \"type\": \"boolean\",\n          \"description\": \"DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.\"\n        },\n        \"repo\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"The repo this event comes from. Note that all other message types name this field 'did'.\"\n        },\n        \"commit\": {\n          \"type\": \"cid-link\",\n          \"description\": \"Repo commit object CID.\"\n        },\n        \"rev\": {\n          \"type\": \"string\",\n          \"format\": \"tid\",\n          \"description\": \"The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.\"\n        },\n        \"since\": {\n          \"type\": \"string\",\n          \"format\": \"tid\",\n          \"description\": \"The rev of the last emitted commit from this repo (if any).\"\n        },\n        \"blocks\": {\n          \"type\": \"bytes\",\n          \"description\": \"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.\",\n          \"maxLength\": 2000000\n        },\n        \"ops\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#repoOp\",\n            \"description\": \"List of repo mutation operations in this commit (eg, records created, updated, or deleted).\"\n          },\n          \"maxLength\": 200\n        },\n        \"blobs\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"cid-link\",\n            \"description\": \"DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.\"\n          }\n        },\n        \"prevData\": {\n          \"type\": \"cid-link\",\n          \"description\": \"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.\"\n        },\n        \"time\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp of when this message was originally broadcast.\"\n        }\n      }\n    },\n    \"sync\": {\n      \"type\": \"object\",\n      \"description\": \"Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.\",\n      \"required\": [\"seq\", \"did\", \"blocks\", \"rev\", \"time\"],\n      \"properties\": {\n        \"seq\": {\n          \"type\": \"integer\",\n          \"description\": \"The stream sequence number of this message.\"\n        },\n        \"did\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"The account this repo event corresponds to. Must match that in the commit object.\"\n        },\n        \"blocks\": {\n          \"type\": \"bytes\",\n          \"description\": \"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.\",\n          \"maxLength\": 10000\n        },\n        \"rev\": {\n          \"type\": \"string\",\n          \"description\": \"The rev of the commit. This value must match that in the commit object.\"\n        },\n        \"time\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp of when this message was originally broadcast.\"\n        }\n      }\n    },\n    \"identity\": {\n      \"type\": \"object\",\n      \"description\": \"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.\",\n      \"required\": [\"seq\", \"did\", \"time\"],\n      \"properties\": {\n        \"seq\": { \"type\": \"integer\" },\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"time\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"handle\": {\n          \"type\": \"string\",\n          \"format\": \"handle\",\n          \"description\": \"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.\"\n        }\n      }\n    },\n    \"account\": {\n      \"type\": \"object\",\n      \"description\": \"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.\",\n      \"required\": [\"seq\", \"did\", \"time\", \"active\"],\n      \"properties\": {\n        \"seq\": { \"type\": \"integer\" },\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"time\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"active\": {\n          \"type\": \"boolean\",\n          \"description\": \"Indicates that the account has a repository which can be fetched from the host that emitted this event.\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"If active=false, this optional field indicates a reason for why the account is not active.\",\n          \"knownValues\": [\n            \"takendown\",\n            \"suspended\",\n            \"deleted\",\n            \"deactivated\",\n            \"desynchronized\",\n            \"throttled\"\n          ]\n        }\n      }\n    },\n    \"info\": {\n      \"type\": \"object\",\n      \"required\": [\"name\"],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"OutdatedCursor\"]\n        },\n        \"message\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"repoOp\": {\n      \"type\": \"object\",\n      \"description\": \"A repo operation, ie a mutation of a single record.\",\n      \"required\": [\"action\", \"path\", \"cid\"],\n      \"nullable\": [\"cid\"],\n      \"properties\": {\n        \"action\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"create\", \"update\", \"delete\"]\n        },\n        \"path\": { \"type\": \"string\" },\n        \"cid\": {\n          \"type\": \"cid-link\",\n          \"description\": \"For creates and updates, the new record CID. For deletions, null.\"\n        },\n        \"prev\": {\n          \"type\": \"cid-link\",\n          \"description\": \"For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/addReservedHandle.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.addReservedHandle\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Add a handle to the set of reserved handles.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"handle\"],\n          \"properties\": {\n            \"handle\": { \"type\": \"string\" }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/checkHandleAvailability.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.checkHandleAvailability\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"handle\"],\n        \"properties\": {\n          \"handle\": {\n            \"type\": \"string\",\n            \"format\": \"handle\",\n            \"description\": \"Tentative handle. Will be checked for availability or used to build handle suggestions.\"\n          },\n          \"email\": {\n            \"type\": \"string\",\n            \"description\": \"User-provided email. Might be used to build handle suggestions.\"\n          },\n          \"birthDate\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"User-provided birth date. Might be used to build handle suggestions.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"handle\", \"result\"],\n          \"properties\": {\n            \"handle\": {\n              \"type\": \"string\",\n              \"format\": \"handle\",\n              \"description\": \"Echo of the input handle.\"\n            },\n            \"result\": {\n              \"type\": \"union\",\n              \"refs\": [\"#resultAvailable\", \"#resultUnavailable\"]\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidEmail\",\n          \"description\": \"An invalid email was provided.\"\n        }\n      ]\n    },\n    \"resultAvailable\": {\n      \"type\": \"object\",\n      \"description\": \"Indicates the provided handle is available.\",\n      \"properties\": {}\n    },\n    \"resultUnavailable\": {\n      \"type\": \"object\",\n      \"description\": \"Indicates the provided handle is unavailable and gives suggestions of available handles.\",\n      \"required\": [\"suggestions\"],\n      \"properties\": {\n        \"suggestions\": {\n          \"type\": \"array\",\n          \"description\": \"List of suggested handles based on the provided inputs.\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#suggestion\"\n          }\n        }\n      }\n    },\n    \"suggestion\": {\n      \"type\": \"object\",\n      \"required\": [\"handle\", \"method\"],\n      \"properties\": {\n        \"handle\": {\n          \"type\": \"string\",\n          \"format\": \"handle\"\n        },\n        \"method\": {\n          \"type\": \"string\",\n          \"description\": \"Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/checkSignupQueue.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.checkSignupQueue\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Check accounts location in signup queue.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"activated\"],\n          \"properties\": {\n            \"activated\": { \"type\": \"boolean\" },\n            \"placeInQueue\": { \"type\": \"integer\" },\n            \"estimatedTimeMs\": { \"type\": \"integer\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/dereferenceScope.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.dereferenceScope\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Allows finding the oauth permission scope from a reference\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"scope\"],\n        \"properties\": {\n          \"scope\": {\n            \"type\": \"string\",\n            \"description\": \"The scope reference (starts with 'ref:')\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"scope\"],\n          \"properties\": {\n            \"scope\": {\n              \"type\": \"string\",\n              \"description\": \"The full oauth permission scope\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidScopeReference\",\n          \"description\": \"An invalid scope reference was provided.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/fetchLabels.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.fetchLabels\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"since\": { \"type\": \"integer\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 250,\n            \"default\": 50\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"labels\"],\n          \"properties\": {\n            \"labels\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/requestPhoneVerification.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.requestPhoneVerification\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Request a verification code to be sent to the supplied phone number\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"phoneNumber\"],\n          \"properties\": {\n            \"phoneNumber\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/atproto/temp/revokeAccountCredentials.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.atproto.temp.revokeAccountCredentials\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"account\"],\n          \"properties\": {\n            \"account\": {\n              \"type\": \"string\",\n              \"format\": \"at-identifier\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/com/germnetwork/declaration.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.germnetwork.declaration\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A declaration of a Germ Network account\",\n      \"key\": \"literal:self\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"version\", \"currentKey\"],\n        \"properties\": {\n          \"version\": {\n            \"type\": \"string\",\n            \"description\": \"Semver version number, without pre-release or build information, for the format of opaque content\",\n            \"minLength\": 5,\n            \"maxLength\": 14\n          },\n          \"currentKey\": {\n            \"type\": \"bytes\",\n            \"description\": \"Opaque value, an ed25519 public key prefixed with a byte enum\"\n          },\n          \"messageMe\": {\n            \"type\": \"ref\",\n            \"description\": \"Controls who can message this account\",\n            \"ref\": \"#messageMe\"\n          },\n          \"keyPackage\": {\n            \"type\": \"bytes\",\n            \"description\": \"Opaque value, contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey\"\n          },\n          \"continuityProofs\": {\n            \"type\": \"array\",\n            \"description\": \"Array of opaque values to allow for key rolling\",\n            \"items\": {\n              \"type\": \"bytes\"\n            },\n            \"maxLength\": 1000\n          }\n        }\n      }\n    },\n    \"messageMe\": {\n      \"type\": \"object\",\n      \"required\": [\"showButtonTo\", \"messageMeUrl\"],\n      \"properties\": {\n        \"messageMeUrl\": {\n          \"type\": \"string\",\n          \"description\": \"A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who wish to message each other\",\n          \"format\": \"uri\",\n          \"minLength\": 1,\n          \"maxLength\": 2047\n        },\n        \"showButtonTo\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"none\", \"usersIFollow\", \"everyone\"],\n          \"description\": \"The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer.\",\n          \"minLength\": 1,\n          \"maxLength\": 100\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/communication/createTemplate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.communication.createTemplate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Administrative action to create a new, re-usable communication (email for now) template.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subject\", \"contentMarkdown\", \"name\"],\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"Name of the template.\"\n            },\n            \"contentMarkdown\": {\n              \"type\": \"string\",\n              \"description\": \"Content of the template, markdown supported, can contain variable placeholders.\"\n            },\n            \"subject\": {\n              \"type\": \"string\",\n              \"description\": \"Subject of the message, used in emails.\"\n            },\n            \"lang\": {\n              \"type\": \"string\",\n              \"format\": \"language\",\n              \"description\": \"Message language.\"\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"DID of the user who is creating the template.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.communication.defs#templateView\"\n        }\n      },\n      \"errors\": [{ \"name\": \"DuplicateTemplateName\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/communication/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.communication.defs\",\n  \"defs\": {\n    \"templateView\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"name\",\n        \"contentMarkdown\",\n        \"disabled\",\n        \"lastUpdatedBy\",\n        \"createdAt\",\n        \"updatedAt\"\n      ],\n      \"properties\": {\n        \"id\": { \"type\": \"string\" },\n        \"name\": { \"type\": \"string\", \"description\": \"Name of the template.\" },\n        \"subject\": {\n          \"type\": \"string\",\n          \"description\": \"Content of the template, can contain markdown and variable placeholders.\"\n        },\n        \"contentMarkdown\": {\n          \"type\": \"string\",\n          \"description\": \"Subject of the message, used in emails.\"\n        },\n        \"disabled\": { \"type\": \"boolean\" },\n        \"lang\": {\n          \"type\": \"string\",\n          \"format\": \"language\",\n          \"description\": \"Message language.\"\n        },\n        \"lastUpdatedBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"DID of the user who last updated the template.\"\n        },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"updatedAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/communication/deleteTemplate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.communication.deleteTemplate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete a communication template.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"id\"],\n          \"properties\": {\n            \"id\": { \"type\": \"string\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/communication/listTemplates.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.communication.listTemplates\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get list of all communication templates.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"communicationTemplates\"],\n          \"properties\": {\n            \"communicationTemplates\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.communication.defs#templateView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/communication/updateTemplate.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.communication.updateTemplate\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Administrative action to update an existing communication template. Allows passing partial fields to patch specific fields only.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"id\"],\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\",\n              \"description\": \"ID of the template to be updated.\"\n            },\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"Name of the template.\"\n            },\n            \"lang\": {\n              \"type\": \"string\",\n              \"format\": \"language\",\n              \"description\": \"Message language.\"\n            },\n            \"contentMarkdown\": {\n              \"type\": \"string\",\n              \"description\": \"Content of the template, markdown supported, can contain variable placeholders.\"\n            },\n            \"subject\": {\n              \"type\": \"string\",\n              \"description\": \"Subject of the message, used in emails.\"\n            },\n            \"updatedBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"DID of the user who is updating the template.\"\n            },\n            \"disabled\": {\n              \"type\": \"boolean\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.communication.defs#templateView\"\n        }\n      },\n      \"errors\": [{ \"name\": \"DuplicateTemplateName\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/hosting/getAccountHistory.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.hosting.getAccountHistory\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get account history, e.g. log of updated email addresses or other identity information.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": { \"type\": \"string\", \"format\": \"did\" },\n          \"events\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"knownValues\": [\n                \"accountCreated\",\n                \"emailUpdated\",\n                \"emailConfirmed\",\n                \"passwordUpdated\",\n                \"handleUpdated\"\n              ]\n            }\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"events\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"events\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#event\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"event\": {\n      \"type\": \"object\",\n      \"required\": [\"details\", \"createdBy\", \"createdAt\"],\n      \"properties\": {\n        \"details\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"#accountCreated\",\n            \"#emailUpdated\",\n            \"#emailConfirmed\",\n            \"#passwordUpdated\",\n            \"#handleUpdated\"\n          ]\n        },\n        \"createdBy\": { \"type\": \"string\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"accountCreated\": {\n      \"type\": \"object\",\n      \"required\": [],\n      \"properties\": {\n        \"email\": { \"type\": \"string\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" }\n      }\n    },\n    \"emailUpdated\": {\n      \"type\": \"object\",\n      \"required\": [\"email\"],\n      \"properties\": {\n        \"email\": { \"type\": \"string\" }\n      }\n    },\n    \"emailConfirmed\": {\n      \"type\": \"object\",\n      \"required\": [\"email\"],\n      \"properties\": {\n        \"email\": { \"type\": \"string\" }\n      }\n    },\n    \"passwordUpdated\": {\n      \"type\": \"object\",\n      \"required\": [],\n      \"properties\": {}\n    },\n    \"handleUpdated\": {\n      \"type\": \"object\",\n      \"required\": [\"handle\"],\n      \"properties\": {\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/cancelScheduledActions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.cancelScheduledActions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Cancel all pending scheduled moderation actions for specified subjects\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subjects\"],\n          \"properties\": {\n            \"subjects\": {\n              \"type\": \"array\",\n              \"maxLength\": 100,\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"did\"\n              },\n              \"description\": \"Array of DID subjects to cancel scheduled actions for\"\n            },\n            \"comment\": {\n              \"type\": \"string\",\n              \"description\": \"Optional comment describing the reason for cancellation\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"#cancellationResults\"\n        }\n      }\n    },\n    \"cancellationResults\": {\n      \"type\": \"object\",\n      \"required\": [\"succeeded\", \"failed\"],\n      \"properties\": {\n        \"succeeded\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\", \"format\": \"did\" },\n          \"description\": \"DIDs for which all pending scheduled actions were successfully cancelled\"\n        },\n        \"failed\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#failedCancellation\" },\n          \"description\": \"DIDs for which cancellation failed with error details\"\n        }\n      }\n    },\n    \"failedCancellation\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"error\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"error\": { \"type\": \"string\" },\n        \"errorCode\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.defs\",\n  \"defs\": {\n    \"modEventView\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"event\",\n        \"subject\",\n        \"subjectBlobCids\",\n        \"createdBy\",\n        \"createdAt\"\n      ],\n      \"properties\": {\n        \"id\": { \"type\": \"integer\" },\n        \"event\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"#modEventTakedown\",\n            \"#modEventReverseTakedown\",\n            \"#modEventComment\",\n            \"#modEventReport\",\n            \"#modEventLabel\",\n            \"#modEventAcknowledge\",\n            \"#modEventEscalate\",\n            \"#modEventMute\",\n            \"#modEventUnmute\",\n            \"#modEventMuteReporter\",\n            \"#modEventUnmuteReporter\",\n            \"#modEventEmail\",\n            \"#modEventResolveAppeal\",\n            \"#modEventDivert\",\n            \"#modEventTag\",\n            \"#accountEvent\",\n            \"#identityEvent\",\n            \"#recordEvent\",\n            \"#modEventPriorityScore\",\n            \"#ageAssuranceEvent\",\n            \"#ageAssuranceOverrideEvent\",\n            \"#ageAssurancePurgeEvent\",\n            \"#revokeAccountCredentialsEvent\",\n            \"#scheduleTakedownEvent\",\n            \"#cancelScheduledTakedownEvent\"\n          ]\n        },\n        \"subject\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"com.atproto.admin.defs#repoRef\",\n            \"com.atproto.repo.strongRef\",\n            \"chat.bsky.convo.defs#messageRef\"\n          ]\n        },\n        \"subjectBlobCids\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"createdBy\": { \"type\": \"string\", \"format\": \"did\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"creatorHandle\": { \"type\": \"string\" },\n        \"subjectHandle\": { \"type\": \"string\" },\n        \"modTool\": { \"type\": \"ref\", \"ref\": \"#modTool\" }\n      }\n    },\n    \"modEventViewDetail\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"event\",\n        \"subject\",\n        \"subjectBlobs\",\n        \"createdBy\",\n        \"createdAt\"\n      ],\n      \"properties\": {\n        \"id\": { \"type\": \"integer\" },\n        \"event\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"#modEventTakedown\",\n            \"#modEventReverseTakedown\",\n            \"#modEventComment\",\n            \"#modEventReport\",\n            \"#modEventLabel\",\n            \"#modEventAcknowledge\",\n            \"#modEventEscalate\",\n            \"#modEventMute\",\n            \"#modEventUnmute\",\n            \"#modEventMuteReporter\",\n            \"#modEventUnmuteReporter\",\n            \"#modEventEmail\",\n            \"#modEventResolveAppeal\",\n            \"#modEventDivert\",\n            \"#modEventTag\",\n            \"#accountEvent\",\n            \"#identityEvent\",\n            \"#recordEvent\",\n            \"#modEventPriorityScore\",\n            \"#ageAssuranceEvent\",\n            \"#ageAssuranceOverrideEvent\",\n            \"#ageAssurancePurgeEvent\",\n            \"#revokeAccountCredentialsEvent\",\n            \"#scheduleTakedownEvent\",\n            \"#cancelScheduledTakedownEvent\"\n          ]\n        },\n        \"subject\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"#repoView\",\n            \"#repoViewNotFound\",\n            \"#recordView\",\n            \"#recordViewNotFound\"\n          ]\n        },\n        \"subjectBlobs\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#blobView\" }\n        },\n        \"createdBy\": { \"type\": \"string\", \"format\": \"did\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"modTool\": { \"type\": \"ref\", \"ref\": \"#modTool\" }\n      }\n    },\n    \"subjectStatusView\": {\n      \"type\": \"object\",\n      \"required\": [\"id\", \"subject\", \"createdAt\", \"updatedAt\", \"reviewState\"],\n      \"properties\": {\n        \"id\": { \"type\": \"integer\" },\n        \"subject\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"com.atproto.admin.defs#repoRef\",\n            \"com.atproto.repo.strongRef\",\n            \"chat.bsky.convo.defs#messageRef\"\n          ]\n        },\n        \"hosting\": {\n          \"type\": \"union\",\n          \"refs\": [\"#accountHosting\", \"#recordHosting\"]\n        },\n        \"subjectBlobCids\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\", \"format\": \"cid\" }\n        },\n        \"subjectRepoHandle\": { \"type\": \"string\" },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp referencing when the last update was made to the moderation status of the subject\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp referencing the first moderation status impacting event was emitted on the subject\"\n        },\n        \"reviewState\": {\n          \"type\": \"ref\",\n          \"ref\": \"#subjectReviewState\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Sticky comment on the subject.\"\n        },\n        \"priorityScore\": {\n          \"type\": \"integer\",\n          \"description\": \"Numeric value representing the level of priority. Higher score means higher priority.\",\n          \"minimum\": 0,\n          \"maximum\": 100\n        },\n        \"muteUntil\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"muteReportingUntil\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"lastReviewedBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        },\n        \"lastReviewedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"lastReportedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"lastAppealedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp referencing when the author of the subject appealed a moderation action\"\n        },\n        \"takendown\": {\n          \"type\": \"boolean\"\n        },\n        \"appealed\": {\n          \"type\": \"boolean\",\n          \"description\": \"True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.\"\n        },\n        \"suspendUntil\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"tags\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" }\n        },\n        \"accountStats\": {\n          \"description\": \"Statistics related to the account subject\",\n          \"type\": \"ref\",\n          \"ref\": \"#accountStats\"\n        },\n        \"recordsStats\": {\n          \"description\": \"Statistics related to the record subjects authored by the subject's account\",\n          \"type\": \"ref\",\n          \"ref\": \"#recordsStats\"\n        },\n        \"accountStrike\": {\n          \"description\": \"Strike information for the account (account-level only)\",\n          \"type\": \"ref\",\n          \"ref\": \"#accountStrike\"\n        },\n        \"ageAssuranceState\": {\n          \"type\": \"string\",\n          \"description\": \"Current age assurance state of the subject.\",\n          \"knownValues\": [\"pending\", \"assured\", \"unknown\", \"reset\", \"blocked\"]\n        },\n        \"ageAssuranceUpdatedBy\": {\n          \"type\": \"string\",\n          \"description\": \"Whether or not the last successful update to age assurance was made by the user or admin.\",\n          \"knownValues\": [\"admin\", \"user\"]\n        }\n      }\n    },\n    \"subjectView\": {\n      \"description\": \"Detailed view of a subject. For record subjects, the author's repo and profile will be returned.\",\n      \"type\": \"object\",\n      \"required\": [\"type\", \"subject\"],\n      \"properties\": {\n        \"type\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.moderation.defs#subjectType\"\n        },\n        \"subject\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"ref\",\n          \"ref\": \"#subjectStatusView\"\n        },\n        \"repo\": {\n          \"type\": \"ref\",\n          \"ref\": \"#repoViewDetail\"\n        },\n        \"profile\": {\n          \"type\": \"union\",\n          \"refs\": []\n        },\n        \"record\": {\n          \"type\": \"ref\",\n          \"ref\": \"#recordViewDetail\"\n        }\n      }\n    },\n    \"accountStats\": {\n      \"description\": \"Statistics about a particular account subject\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"reportCount\": {\n          \"description\": \"Total number of reports on the account\",\n          \"type\": \"integer\"\n        },\n        \"appealCount\": {\n          \"description\": \"Total number of appeals against a moderation action on the account\",\n          \"type\": \"integer\"\n        },\n        \"suspendCount\": {\n          \"description\": \"Number of times the account was suspended\",\n          \"type\": \"integer\"\n        },\n        \"escalateCount\": {\n          \"description\": \"Number of times the account was escalated\",\n          \"type\": \"integer\"\n        },\n        \"takedownCount\": {\n          \"description\": \"Number of times the account was taken down\",\n          \"type\": \"integer\"\n        }\n      }\n    },\n    \"recordsStats\": {\n      \"description\": \"Statistics about a set of record subject items\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"totalReports\": {\n          \"description\": \"Cumulative sum of the number of reports on the items in the set\",\n          \"type\": \"integer\"\n        },\n        \"reportedCount\": {\n          \"description\": \"Number of items that were reported at least once\",\n          \"type\": \"integer\"\n        },\n        \"escalatedCount\": {\n          \"description\": \"Number of items that were escalated at least once\",\n          \"type\": \"integer\"\n        },\n        \"appealedCount\": {\n          \"description\": \"Number of items that were appealed at least once\",\n          \"type\": \"integer\"\n        },\n        \"subjectCount\": {\n          \"description\": \"Total number of item in the set\",\n          \"type\": \"integer\"\n        },\n        \"pendingCount\": {\n          \"description\": \"Number of item currently in \\\"reviewOpen\\\" or \\\"reviewEscalated\\\" state\",\n          \"type\": \"integer\"\n        },\n        \"processedCount\": {\n          \"description\": \"Number of item currently in \\\"reviewNone\\\" or \\\"reviewClosed\\\" state\",\n          \"type\": \"integer\"\n        },\n        \"takendownCount\": {\n          \"description\": \"Number of item currently taken down\",\n          \"type\": \"integer\"\n        }\n      }\n    },\n    \"accountStrike\": {\n      \"description\": \"Strike information for an account\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"activeStrikeCount\": {\n          \"description\": \"Current number of active strikes (excluding expired strikes)\",\n          \"type\": \"integer\"\n        },\n        \"totalStrikeCount\": {\n          \"description\": \"Total number of strikes ever received (including expired strikes)\",\n          \"type\": \"integer\"\n        },\n        \"firstStrikeAt\": {\n          \"description\": \"Timestamp of the first strike received\",\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"lastStrikeAt\": {\n          \"description\": \"Timestamp of the most recent strike received\",\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n    \"subjectReviewState\": {\n      \"type\": \"string\",\n      \"knownValues\": [\n        \"tools.ozone.moderation.defs#reviewOpen\",\n        \"tools.ozone.moderation.defs#reviewEscalated\",\n        \"tools.ozone.moderation.defs#reviewClosed\",\n        \"tools.ozone.moderation.defs#reviewNone\"\n      ]\n    },\n    \"reviewOpen\": {\n      \"type\": \"token\",\n      \"description\": \"Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator\"\n    },\n    \"reviewEscalated\": {\n      \"type\": \"token\",\n      \"description\": \"Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator\"\n    },\n    \"reviewClosed\": {\n      \"type\": \"token\",\n      \"description\": \"Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator\"\n    },\n    \"reviewNone\": {\n      \"type\": \"token\",\n      \"description\": \"Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it\"\n    },\n    \"modEventTakedown\": {\n      \"type\": \"object\",\n      \"description\": \"Take down a subject permanently or temporarily\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"durationInHours\": {\n          \"type\": \"integer\",\n          \"description\": \"Indicates how long the takedown should be in effect before automatically expiring.\"\n        },\n        \"acknowledgeAccountSubjects\": {\n          \"type\": \"boolean\",\n          \"description\": \"If true, all other reports on content authored by this account will be resolved (acknowledged).\"\n        },\n        \"policies\": {\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Names/Keywords of the policies that drove the decision.\"\n        },\n        \"severityLevel\": {\n          \"type\": \"string\",\n          \"description\": \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\"\n        },\n        \"targetServices\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\", \"knownValues\": [\"appview\", \"pds\"] },\n          \"description\": \"List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.\"\n        },\n        \"strikeCount\": {\n          \"type\": \"integer\",\n          \"description\": \"Number of strikes to assign to the user for this violation.\"\n        },\n        \"strikeExpiresAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"When the strike should expire. If not provided, the strike never expires.\"\n        }\n      }\n    },\n    \"modEventReverseTakedown\": {\n      \"type\": \"object\",\n      \"description\": \"Revert take down action on a subject\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Describe reasoning behind the reversal.\"\n        },\n        \"policies\": {\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Names/Keywords of the policy infraction for which takedown is being reversed.\"\n        },\n        \"severityLevel\": {\n          \"type\": \"string\",\n          \"description\": \"Severity level of the violation. Usually set from the last policy infraction's severity.\"\n        },\n        \"strikeCount\": {\n          \"type\": \"integer\",\n          \"description\": \"Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity.\"\n        }\n      }\n    },\n    \"modEventResolveAppeal\": {\n      \"type\": \"object\",\n      \"description\": \"Resolve appeal on a subject\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Describe resolution.\"\n        }\n      }\n    },\n    \"modEventComment\": {\n      \"type\": \"object\",\n      \"description\": \"Add a comment to a subject. An empty comment will clear any previously set sticky comment.\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"sticky\": {\n          \"type\": \"boolean\",\n          \"description\": \"Make the comment persistent on the subject\"\n        }\n      }\n    },\n    \"modEventReport\": {\n      \"type\": \"object\",\n      \"description\": \"Report a subject\",\n      \"required\": [\"reportType\"],\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"isReporterMuted\": {\n          \"type\": \"boolean\",\n          \"description\": \"Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject.\"\n        },\n        \"reportType\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.moderation.defs#reasonType\"\n        }\n      }\n    },\n    \"modEventLabel\": {\n      \"type\": \"object\",\n      \"description\": \"Apply/Negate labels on a subject\",\n      \"required\": [\"createLabelVals\", \"negateLabelVals\"],\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"createLabelVals\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" }\n        },\n        \"negateLabelVals\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" }\n        },\n        \"durationInHours\": {\n          \"type\": \"integer\",\n          \"description\": \"Indicates how long the label will remain on the subject. Only applies on labels that are being added.\"\n        }\n      }\n    },\n    \"modEventPriorityScore\": {\n      \"type\": \"object\",\n      \"description\": \"Set priority score of the subject. Higher score means higher priority.\",\n      \"required\": [\"score\"],\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"score\": {\n          \"type\": \"integer\",\n          \"minimum\": 0,\n          \"maximum\": 100\n        }\n      }\n    },\n    \"ageAssuranceEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Age assurance info coming directly from users. Only works on DID subjects.\",\n      \"required\": [\"createdAt\", \"status\", \"attemptId\"],\n      \"properties\": {\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"The date and time of this write operation.\"\n        },\n        \"attemptId\": {\n          \"type\": \"string\",\n          \"description\": \"The unique identifier for this instance of the age assurance flow, in UUID format.\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"The status of the Age Assurance process.\",\n          \"knownValues\": [\"unknown\", \"pending\", \"assured\"]\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        },\n        \"countryCode\": {\n          \"type\": \"string\",\n          \"description\": \"The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.\"\n        },\n        \"regionCode\": {\n          \"type\": \"string\",\n          \"description\": \"The ISO 3166-2 region code provided when beginning the Age Assurance flow.\"\n        },\n        \"initIp\": {\n          \"type\": \"string\",\n          \"description\": \"The IP address used when initiating the AA flow.\"\n        },\n        \"initUa\": {\n          \"type\": \"string\",\n          \"description\": \"The user agent used when initiating the AA flow.\"\n        },\n        \"completeIp\": {\n          \"type\": \"string\",\n          \"description\": \"The IP address used when completing the AA flow.\"\n        },\n        \"completeUa\": {\n          \"type\": \"string\",\n          \"description\": \"The user agent used when completing the AA flow.\"\n        }\n      }\n    },\n    \"ageAssuranceOverrideEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Age assurance status override by moderators. Only works on DID subjects.\",\n      \"required\": [\"comment\", \"status\"],\n      \"properties\": {\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.\",\n          \"knownValues\": [\"assured\", \"reset\", \"blocked\"]\n        },\n        \"access\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.ageassurance.defs#access\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"minLength\": 1,\n          \"description\": \"Comment describing the reason for the override.\"\n        }\n      }\n    },\n    \"ageAssurancePurgeEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only.\",\n      \"required\": [\"comment\"],\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\",\n          \"minLength\": 1,\n          \"description\": \"Comment describing the reason for the purge.\"\n        }\n      }\n    },\n    \"revokeAccountCredentialsEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Account credentials revocation by moderators. Only works on DID subjects.\",\n      \"required\": [\"comment\"],\n      \"properties\": {\n        \"comment\": {\n          \"minLength\": 1,\n          \"type\": \"string\",\n          \"description\": \"Comment describing the reason for the revocation.\"\n        }\n      }\n    },\n    \"modEventAcknowledge\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"acknowledgeAccountSubjects\": {\n          \"type\": \"boolean\",\n          \"description\": \"If true, all other reports on content authored by this account will be resolved (acknowledged).\"\n        }\n      }\n    },\n    \"modEventEscalate\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" }\n      }\n    },\n    \"modEventMute\": {\n      \"type\": \"object\",\n      \"description\": \"Mute incoming reports on a subject\",\n      \"required\": [\"durationInHours\"],\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"durationInHours\": {\n          \"type\": \"integer\",\n          \"description\": \"Indicates how long the subject should remain muted.\"\n        }\n      }\n    },\n    \"modEventUnmute\": {\n      \"type\": \"object\",\n      \"description\": \"Unmute action on a subject\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Describe reasoning behind the reversal.\"\n        }\n      }\n    },\n    \"modEventMuteReporter\": {\n      \"type\": \"object\",\n      \"description\": \"Mute incoming reports from an account\",\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"durationInHours\": {\n          \"type\": \"integer\",\n          \"description\": \"Indicates how long the account should remain muted. Falsy value here means a permanent mute.\"\n        }\n      }\n    },\n    \"modEventUnmuteReporter\": {\n      \"type\": \"object\",\n      \"description\": \"Unmute incoming reports from an account\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Describe reasoning behind the reversal.\"\n        }\n      }\n    },\n    \"modEventEmail\": {\n      \"type\": \"object\",\n      \"description\": \"Keep a log of outgoing email to a user\",\n      \"required\": [\"subjectLine\"],\n      \"properties\": {\n        \"subjectLine\": {\n          \"type\": \"string\",\n          \"description\": \"The subject line of the email sent to the user.\"\n        },\n        \"content\": {\n          \"type\": \"string\",\n          \"description\": \"The content of the email sent to the user.\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Additional comment about the outgoing comm.\"\n        },\n        \"policies\": {\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Names/Keywords of the policies that necessitated the email.\"\n        },\n        \"severityLevel\": {\n          \"type\": \"string\",\n          \"description\": \"Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense\"\n        },\n        \"strikeCount\": {\n          \"type\": \"integer\",\n          \"description\": \"Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense.\"\n        },\n        \"strikeExpiresAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"When the strike should expire. If not provided, the strike never expires.\"\n        },\n        \"isDelivered\": {\n          \"type\": \"boolean\",\n          \"description\": \"Indicates whether the email was successfully delivered to the user's inbox.\"\n        }\n      }\n    },\n    \"modEventDivert\": {\n      \"type\": \"object\",\n      \"description\": \"Divert a record's blobs to a 3rd party service for further scanning/tagging\",\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" }\n      }\n    },\n    \"modEventTag\": {\n      \"type\": \"object\",\n      \"description\": \"Add/Remove a tag on a subject\",\n      \"required\": [\"add\", \"remove\"],\n      \"properties\": {\n        \"add\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Tags to be added to the subject. If already exists, won't be duplicated.\"\n        },\n        \"remove\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated.\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Additional comment about added/removed tags.\"\n        }\n      }\n    },\n    \"accountEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.\",\n      \"required\": [\"timestamp\", \"active\"],\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"active\": {\n          \"type\": \"boolean\",\n          \"description\": \"Indicates that the account has a repository which can be fetched from the host that emitted this event.\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"unknown\",\n            \"deactivated\",\n            \"deleted\",\n            \"takendown\",\n            \"suspended\",\n            \"tombstoned\"\n          ]\n        },\n        \"timestamp\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n    \"identityEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.\",\n      \"required\": [\"timestamp\"],\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"pdsHost\": { \"type\": \"string\", \"format\": \"uri\" },\n        \"tombstone\": { \"type\": \"boolean\" },\n        \"timestamp\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n    \"recordEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.\",\n      \"required\": [\"timestamp\", \"op\"],\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"op\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"create\", \"update\", \"delete\"]\n        },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"timestamp\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"scheduleTakedownEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Logs a scheduled takedown action for an account.\",\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" },\n        \"executeAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"executeAfter\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"executeUntil\": { \"type\": \"string\", \"format\": \"datetime\" }\n      }\n    },\n    \"cancelScheduledTakedownEvent\": {\n      \"type\": \"object\",\n      \"description\": \"Logs cancellation of a scheduled takedown action for an account.\",\n      \"properties\": {\n        \"comment\": { \"type\": \"string\" }\n      }\n    },\n    \"repoView\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"did\",\n        \"handle\",\n        \"relatedRecords\",\n        \"indexedAt\",\n        \"moderation\"\n      ],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"email\": { \"type\": \"string\" },\n        \"relatedRecords\": { \"type\": \"array\", \"items\": { \"type\": \"unknown\" } },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"moderation\": { \"type\": \"ref\", \"ref\": \"#moderation\" },\n        \"invitedBy\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.server.defs#inviteCode\"\n        },\n        \"invitesDisabled\": { \"type\": \"boolean\" },\n        \"inviteNote\": { \"type\": \"string\" },\n        \"deactivatedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"threatSignatures\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.admin.defs#threatSignature\"\n          }\n        }\n      }\n    },\n    \"repoViewDetail\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"did\",\n        \"handle\",\n        \"relatedRecords\",\n        \"indexedAt\",\n        \"moderation\"\n      ],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"handle\": { \"type\": \"string\", \"format\": \"handle\" },\n        \"email\": { \"type\": \"string\" },\n        \"relatedRecords\": { \"type\": \"array\", \"items\": { \"type\": \"unknown\" } },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"moderation\": { \"type\": \"ref\", \"ref\": \"#moderationDetail\" },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"invitedBy\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.server.defs#inviteCode\"\n        },\n        \"invites\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.server.defs#inviteCode\"\n          }\n        },\n        \"invitesDisabled\": { \"type\": \"boolean\" },\n        \"inviteNote\": { \"type\": \"string\" },\n        \"emailConfirmedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"deactivatedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"threatSignatures\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.atproto.admin.defs#threatSignature\"\n          }\n        }\n      }\n    },\n    \"repoViewNotFound\": {\n      \"type\": \"object\",\n      \"required\": [\"did\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" }\n      }\n    },\n    \"recordView\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"uri\",\n        \"cid\",\n        \"value\",\n        \"blobCids\",\n        \"indexedAt\",\n        \"moderation\",\n        \"repo\"\n      ],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"value\": { \"type\": \"unknown\" },\n        \"blobCids\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\", \"format\": \"cid\" }\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"moderation\": { \"type\": \"ref\", \"ref\": \"#moderation\" },\n        \"repo\": { \"type\": \"ref\", \"ref\": \"#repoView\" }\n      }\n    },\n    \"recordViewDetail\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"uri\",\n        \"cid\",\n        \"value\",\n        \"blobs\",\n        \"indexedAt\",\n        \"moderation\",\n        \"repo\"\n      ],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"value\": { \"type\": \"unknown\" },\n        \"blobs\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"#blobView\" }\n        },\n        \"labels\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"ref\", \"ref\": \"com.atproto.label.defs#label\" }\n        },\n        \"indexedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"moderation\": { \"type\": \"ref\", \"ref\": \"#moderationDetail\" },\n        \"repo\": { \"type\": \"ref\", \"ref\": \"#repoView\" }\n      }\n    },\n    \"recordViewNotFound\": {\n      \"type\": \"object\",\n      \"required\": [\"uri\"],\n      \"properties\": {\n        \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n      }\n    },\n    \"moderation\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"subjectStatus\": { \"type\": \"ref\", \"ref\": \"#subjectStatusView\" }\n      }\n    },\n    \"moderationDetail\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"subjectStatus\": {\n          \"type\": \"ref\",\n          \"ref\": \"#subjectStatusView\"\n        }\n      }\n    },\n    \"blobView\": {\n      \"type\": \"object\",\n      \"required\": [\"cid\", \"mimeType\", \"size\", \"createdAt\"],\n      \"properties\": {\n        \"cid\": { \"type\": \"string\", \"format\": \"cid\" },\n        \"mimeType\": { \"type\": \"string\" },\n        \"size\": { \"type\": \"integer\" },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"details\": {\n          \"type\": \"union\",\n          \"refs\": [\"#imageDetails\", \"#videoDetails\"]\n        },\n        \"moderation\": { \"type\": \"ref\", \"ref\": \"#moderation\" }\n      }\n    },\n    \"imageDetails\": {\n      \"type\": \"object\",\n      \"required\": [\"width\", \"height\"],\n      \"properties\": {\n        \"width\": { \"type\": \"integer\" },\n        \"height\": { \"type\": \"integer\" }\n      }\n    },\n    \"videoDetails\": {\n      \"type\": \"object\",\n      \"required\": [\"width\", \"height\", \"length\"],\n      \"properties\": {\n        \"width\": { \"type\": \"integer\" },\n        \"height\": { \"type\": \"integer\" },\n        \"length\": { \"type\": \"integer\" }\n      }\n    },\n    \"accountHosting\": {\n      \"type\": \"object\",\n      \"required\": [\"status\"],\n      \"properties\": {\n        \"status\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"takendown\",\n            \"suspended\",\n            \"deleted\",\n            \"deactivated\",\n            \"unknown\"\n          ]\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"deletedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"deactivatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"reactivatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n    \"recordHosting\": {\n      \"type\": \"object\",\n      \"required\": [\"status\"],\n      \"properties\": {\n        \"status\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"deleted\", \"unknown\"]\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"deletedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    },\n    \"reporterStats\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"did\",\n        \"accountReportCount\",\n        \"recordReportCount\",\n        \"reportedAccountCount\",\n        \"reportedRecordCount\",\n        \"takendownAccountCount\",\n        \"takendownRecordCount\",\n        \"labeledAccountCount\",\n        \"labeledRecordCount\"\n      ],\n      \"properties\": {\n        \"did\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        },\n        \"accountReportCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of reports made by the user on accounts.\"\n        },\n        \"recordReportCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of reports made by the user on records.\"\n        },\n        \"reportedAccountCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of accounts reported by the user.\"\n        },\n        \"reportedRecordCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of records reported by the user.\"\n        },\n        \"takendownAccountCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of accounts taken down as a result of the user's reports.\"\n        },\n        \"takendownRecordCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of records taken down as a result of the user's reports.\"\n        },\n        \"labeledAccountCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of accounts labeled as a result of the user's reports.\"\n        },\n        \"labeledRecordCount\": {\n          \"type\": \"integer\",\n          \"description\": \"The total number of records labeled as a result of the user's reports.\"\n        }\n      }\n    },\n    \"modTool\": {\n      \"type\": \"object\",\n      \"description\": \"Moderation tool information for tracing the source of the action\",\n      \"required\": [\"name\"],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name/identifier of the source (e.g., 'automod', 'ozone/workspace')\"\n        },\n        \"meta\": {\n          \"type\": \"unknown\",\n          \"description\": \"Additional arbitrary metadata about the source\"\n        }\n      }\n    },\n    \"timelineEventPlcCreate\": {\n      \"type\": \"token\",\n      \"description\": \"Moderation event timeline event for a PLC create operation\"\n    },\n    \"timelineEventPlcOperation\": {\n      \"type\": \"token\",\n      \"description\": \"Moderation event timeline event for generic PLC operation\"\n    },\n    \"timelineEventPlcTombstone\": {\n      \"type\": \"token\",\n      \"description\": \"Moderation event timeline event for a PLC tombstone operation\"\n    },\n    \"scheduledActionView\": {\n      \"type\": \"object\",\n      \"description\": \"View of a scheduled moderation action\",\n      \"required\": [\"id\", \"action\", \"did\", \"createdBy\", \"createdAt\", \"status\"],\n      \"properties\": {\n        \"id\": {\n          \"type\": \"integer\",\n          \"description\": \"Auto-incrementing row ID\"\n        },\n        \"action\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"takedown\"],\n          \"description\": \"Type of action to be executed\"\n        },\n        \"eventData\": {\n          \"type\": \"unknown\",\n          \"description\": \"Serialized event object that will be propagated to the event when performed\"\n        },\n        \"did\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"Subject DID for the action\"\n        },\n        \"executeAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Exact time to execute the action\"\n        },\n        \"executeAfter\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Earliest time to execute the action (for randomized scheduling)\"\n        },\n        \"executeUntil\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Latest time to execute the action (for randomized scheduling)\"\n        },\n        \"randomizeExecution\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether execution time should be randomized within the specified range\"\n        },\n        \"createdBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"DID of the user who created this scheduled action\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"When the scheduled action was created\"\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"When the scheduled action was last updated\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"pending\", \"executed\", \"cancelled\", \"failed\"],\n          \"description\": \"Current status of the scheduled action\"\n        },\n        \"lastExecutedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"When the action was last attempted to be executed\"\n        },\n        \"lastFailureReason\": {\n          \"type\": \"string\",\n          \"description\": \"Reason for the last execution failure\"\n        },\n        \"executionEventId\": {\n          \"type\": \"integer\",\n          \"description\": \"ID of the moderation event created when action was successfully executed\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/emitEvent.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.emitEvent\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Take a moderation action on an actor.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"event\", \"subject\", \"createdBy\"],\n          \"properties\": {\n            \"event\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"tools.ozone.moderation.defs#modEventTakedown\",\n                \"tools.ozone.moderation.defs#modEventAcknowledge\",\n                \"tools.ozone.moderation.defs#modEventEscalate\",\n                \"tools.ozone.moderation.defs#modEventComment\",\n                \"tools.ozone.moderation.defs#modEventLabel\",\n                \"tools.ozone.moderation.defs#modEventReport\",\n                \"tools.ozone.moderation.defs#modEventMute\",\n                \"tools.ozone.moderation.defs#modEventUnmute\",\n                \"tools.ozone.moderation.defs#modEventMuteReporter\",\n                \"tools.ozone.moderation.defs#modEventUnmuteReporter\",\n                \"tools.ozone.moderation.defs#modEventReverseTakedown\",\n                \"tools.ozone.moderation.defs#modEventResolveAppeal\",\n                \"tools.ozone.moderation.defs#modEventEmail\",\n                \"tools.ozone.moderation.defs#modEventDivert\",\n                \"tools.ozone.moderation.defs#modEventTag\",\n                \"tools.ozone.moderation.defs#accountEvent\",\n                \"tools.ozone.moderation.defs#identityEvent\",\n                \"tools.ozone.moderation.defs#recordEvent\",\n                \"tools.ozone.moderation.defs#modEventPriorityScore\",\n                \"tools.ozone.moderation.defs#ageAssuranceEvent\",\n                \"tools.ozone.moderation.defs#ageAssuranceOverrideEvent\",\n                \"tools.ozone.moderation.defs#ageAssurancePurgeEvent\",\n                \"tools.ozone.moderation.defs#revokeAccountCredentialsEvent\",\n                \"tools.ozone.moderation.defs#scheduleTakedownEvent\",\n                \"tools.ozone.moderation.defs#cancelScheduledTakedownEvent\"\n              ]\n            },\n            \"subject\": {\n              \"type\": \"union\",\n              \"refs\": [\n                \"com.atproto.admin.defs#repoRef\",\n                \"com.atproto.repo.strongRef\"\n              ]\n            },\n            \"subjectBlobCids\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"cid\"\n              }\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            },\n            \"modTool\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.moderation.defs#modTool\"\n            },\n            \"externalId\": {\n              \"type\": \"string\",\n              \"description\": \"An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.moderation.defs#modEventView\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"SubjectHasAction\"\n        },\n        {\n          \"name\": \"DuplicateExternalId\",\n          \"description\": \"An event with the same external ID already exists for the subject.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getAccountTimeline.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getAccountTimeline\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get timeline of all available events of an account. This includes moderation events, account history and did history.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"timeline\"],\n          \"properties\": {\n            \"timeline\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#timelineItem\"\n              }\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"RepoNotFound\"\n        }\n      ]\n    },\n    \"timelineItem\": {\n      \"type\": \"object\",\n      \"required\": [\"day\", \"summary\"],\n      \"properties\": {\n        \"day\": {\n          \"type\": \"string\"\n        },\n        \"summary\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#timelineItemSummary\"\n          }\n        }\n      }\n    },\n    \"timelineItemSummary\": {\n      \"type\": \"object\",\n      \"required\": [\"eventSubjectType\", \"eventType\", \"count\"],\n      \"properties\": {\n        \"eventSubjectType\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"account\", \"record\", \"chat\"]\n        },\n        \"eventType\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"tools.ozone.moderation.defs#modEventTakedown\",\n            \"tools.ozone.moderation.defs#modEventReverseTakedown\",\n            \"tools.ozone.moderation.defs#modEventComment\",\n            \"tools.ozone.moderation.defs#modEventReport\",\n            \"tools.ozone.moderation.defs#modEventLabel\",\n            \"tools.ozone.moderation.defs#modEventAcknowledge\",\n            \"tools.ozone.moderation.defs#modEventEscalate\",\n            \"tools.ozone.moderation.defs#modEventMute\",\n            \"tools.ozone.moderation.defs#modEventUnmute\",\n            \"tools.ozone.moderation.defs#modEventMuteReporter\",\n            \"tools.ozone.moderation.defs#modEventUnmuteReporter\",\n            \"tools.ozone.moderation.defs#modEventEmail\",\n            \"tools.ozone.moderation.defs#modEventResolveAppeal\",\n            \"tools.ozone.moderation.defs#modEventDivert\",\n            \"tools.ozone.moderation.defs#modEventTag\",\n            \"tools.ozone.moderation.defs#accountEvent\",\n            \"tools.ozone.moderation.defs#identityEvent\",\n            \"tools.ozone.moderation.defs#recordEvent\",\n            \"tools.ozone.moderation.defs#modEventPriorityScore\",\n            \"tools.ozone.moderation.defs#revokeAccountCredentialsEvent\",\n            \"tools.ozone.moderation.defs#ageAssuranceEvent\",\n            \"tools.ozone.moderation.defs#ageAssuranceOverrideEvent\",\n            \"tools.ozone.moderation.defs#timelineEventPlcCreate\",\n            \"tools.ozone.moderation.defs#timelineEventPlcOperation\",\n            \"tools.ozone.moderation.defs#timelineEventPlcTombstone\",\n            \"tools.ozone.hosting.getAccountHistory#accountCreated\",\n            \"tools.ozone.hosting.getAccountHistory#emailConfirmed\",\n            \"tools.ozone.hosting.getAccountHistory#passwordUpdated\",\n            \"tools.ozone.hosting.getAccountHistory#handleUpdated\",\n            \"tools.ozone.moderation.defs#scheduleTakedownEvent\",\n            \"tools.ozone.moderation.defs#cancelScheduledTakedownEvent\"\n          ]\n        },\n        \"count\": {\n          \"type\": \"integer\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getEvent.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getEvent\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about a moderation event.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"id\"],\n        \"properties\": {\n          \"id\": { \"type\": \"integer\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.moderation.defs#modEventViewDetail\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getRecord.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getRecord\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about a record.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uri\"],\n        \"properties\": {\n          \"uri\": { \"type\": \"string\", \"format\": \"at-uri\" },\n          \"cid\": { \"type\": \"string\", \"format\": \"cid\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.moderation.defs#recordViewDetail\"\n        }\n      },\n      \"errors\": [{ \"name\": \"RecordNotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getRecords.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getRecords\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about some records.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"uris\"],\n        \"properties\": {\n          \"uris\": {\n            \"type\": \"array\",\n            \"maxLength\": 100,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"at-uri\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"records\"],\n          \"properties\": {\n            \"records\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"tools.ozone.moderation.defs#recordViewDetail\",\n                  \"tools.ozone.moderation.defs#recordViewNotFound\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getRepo.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getRepo\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about a repository.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": { \"type\": \"string\", \"format\": \"did\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.moderation.defs#repoViewDetail\"\n        }\n      },\n      \"errors\": [{ \"name\": \"RepoNotFound\" }]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getReporterStats.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getReporterStats\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get reporter stats for a list of users.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"dids\"],\n        \"properties\": {\n          \"dids\": {\n            \"type\": \"array\",\n            \"maxLength\": 100,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"stats\"],\n          \"properties\": {\n            \"stats\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.moderation.defs#reporterStats\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getRepos.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getRepos\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about some repositories.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"dids\"],\n        \"properties\": {\n          \"dids\": {\n            \"type\": \"array\",\n            \"maxLength\": 100,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repos\"],\n          \"properties\": {\n            \"repos\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"union\",\n                \"refs\": [\n                  \"tools.ozone.moderation.defs#repoViewDetail\",\n                  \"tools.ozone.moderation.defs#repoViewNotFound\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/getSubjects.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.getSubjects\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about subjects.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"subjects\"],\n        \"properties\": {\n          \"subjects\": {\n            \"type\": \"array\",\n            \"maxLength\": 100,\n            \"minLength\": 1,\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subjects\"],\n          \"properties\": {\n            \"subjects\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.moderation.defs#subjectView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/listScheduledActions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.listScheduledActions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"List scheduled moderation actions with optional filtering\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"statuses\"],\n          \"properties\": {\n            \"startsAfter\": {\n              \"type\": \"string\",\n              \"format\": \"datetime\",\n              \"description\": \"Filter actions scheduled to execute after this time\"\n            },\n            \"endsBefore\": {\n              \"type\": \"string\",\n              \"format\": \"datetime\",\n              \"description\": \"Filter actions scheduled to execute before this time\"\n            },\n            \"subjects\": {\n              \"type\": \"array\",\n              \"maxLength\": 100,\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"did\"\n              },\n              \"description\": \"Filter actions for specific DID subjects\"\n            },\n            \"statuses\": {\n              \"type\": \"array\",\n              \"minLength\": 1,\n              \"items\": {\n                \"type\": \"string\",\n                \"knownValues\": [\"pending\", \"executed\", \"cancelled\", \"failed\"]\n              },\n              \"description\": \"Filter actions by status\"\n            },\n            \"limit\": {\n              \"type\": \"integer\",\n              \"minimum\": 1,\n              \"maximum\": 100,\n              \"default\": 50,\n              \"description\": \"Maximum number of results to return\"\n            },\n            \"cursor\": {\n              \"type\": \"string\",\n              \"description\": \"Cursor for pagination\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"actions\"],\n          \"properties\": {\n            \"actions\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.moderation.defs#scheduledActionView\"\n              }\n            },\n            \"cursor\": {\n              \"type\": \"string\",\n              \"description\": \"Cursor for next page of results\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/queryEvents.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.queryEvents\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List moderation events related to a subject.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"types\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned.\"\n          },\n          \"createdBy\": {\n            \"type\": \"string\",\n            \"format\": \"did\"\n          },\n          \"sortDirection\": {\n            \"type\": \"string\",\n            \"default\": \"desc\",\n            \"enum\": [\"asc\", \"desc\"],\n            \"description\": \"Sort direction for the events. Defaults to descending order of created at timestamp.\"\n          },\n          \"createdAfter\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Retrieve events created after a given timestamp\"\n          },\n          \"createdBefore\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Retrieve events created before a given timestamp\"\n          },\n          \"subject\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          },\n          \"collections\": {\n            \"type\": \"array\",\n            \"maxLength\": 20,\n            \"description\": \"If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\"\n            }\n          },\n          \"subjectType\": {\n            \"type\": \"string\",\n            \"description\": \"If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n            \"knownValues\": [\"account\", \"record\"]\n          },\n          \"includeAllUserRecords\": {\n            \"type\": \"boolean\",\n            \"default\": false,\n            \"description\": \"If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned.\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"hasComment\": {\n            \"type\": \"boolean\",\n            \"description\": \"If true, only events with comments are returned\"\n          },\n          \"comment\": {\n            \"type\": \"string\",\n            \"description\": \"If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition.\"\n          },\n          \"addedLabels\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"If specified, only events where all of these labels were added are returned\"\n          },\n          \"removedLabels\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"If specified, only events where all of these labels were removed are returned\"\n          },\n          \"addedTags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"If specified, only events where all of these tags were added are returned\"\n          },\n          \"removedTags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"If specified, only events where all of these tags were removed are returned\"\n          },\n          \"reportTypes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"policies\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"If specified, only events where the action policies match any of the given policies are returned\"\n            }\n          },\n          \"modTool\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"If specified, only events where the modTool name matches any of the given values are returned\"\n          },\n          \"batchId\": {\n            \"type\": \"string\",\n            \"description\": \"If specified, only events where the batchId matches the given value are returned\"\n          },\n          \"ageAssuranceState\": {\n            \"type\": \"string\",\n            \"description\": \"If specified, only events where the age assurance state matches the given value are returned\",\n            \"knownValues\": [\"pending\", \"assured\", \"unknown\", \"reset\", \"blocked\"]\n          },\n          \"withStrike\": {\n            \"type\": \"boolean\",\n            \"description\": \"If specified, only events where strikeCount value is set are returned.\"\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"events\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"events\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.moderation.defs#modEventView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/queryStatuses.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.queryStatuses\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"View moderation statuses of subjects (record or repo).\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"queueCount\": {\n            \"type\": \"integer\",\n            \"description\": \"Number of queues being used by moderators. Subjects will be split among all queues.\"\n          },\n          \"queueIndex\": {\n            \"type\": \"integer\",\n            \"description\": \"Index of the queue to fetch subjects from. Works only when queueCount value is specified.\"\n          },\n          \"queueSeed\": {\n            \"type\": \"string\",\n            \"description\": \"A seeder to shuffle/balance the queue items.\"\n          },\n          \"includeAllUserRecords\": {\n            \"type\": \"boolean\",\n            \"description\": \"All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned.\"\n          },\n          \"subject\": {\n            \"type\": \"string\",\n            \"format\": \"uri\",\n            \"description\": \"The subject to get the status for.\"\n          },\n          \"comment\": {\n            \"type\": \"string\",\n            \"description\": \"Search subjects by keyword from comments\"\n          },\n          \"reportedAfter\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects reported after a given timestamp\"\n          },\n          \"reportedBefore\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects reported before a given timestamp\"\n          },\n          \"reviewedAfter\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects reviewed after a given timestamp\"\n          },\n          \"hostingDeletedAfter\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects where the associated record/account was deleted after a given timestamp\"\n          },\n          \"hostingDeletedBefore\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects where the associated record/account was deleted before a given timestamp\"\n          },\n          \"hostingUpdatedAfter\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects where the associated record/account was updated after a given timestamp\"\n          },\n          \"hostingUpdatedBefore\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects where the associated record/account was updated before a given timestamp\"\n          },\n          \"hostingStatuses\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Search subjects by the status of the associated record/account\"\n          },\n          \"reviewedBefore\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Search subjects reviewed before a given timestamp\"\n          },\n          \"includeMuted\": {\n            \"type\": \"boolean\",\n            \"description\": \"By default, we don't include muted subjects in the results. Set this to true to include them.\"\n          },\n          \"onlyMuted\": {\n            \"type\": \"boolean\",\n            \"description\": \"When set to true, only muted subjects and reporters will be returned.\"\n          },\n          \"reviewState\": {\n            \"type\": \"string\",\n            \"description\": \"Specify when fetching subjects in a certain state\",\n            \"knownValues\": [\n              \"tools.ozone.moderation.defs#reviewOpen\",\n              \"tools.ozone.moderation.defs#reviewClosed\",\n              \"tools.ozone.moderation.defs#reviewEscalated\",\n              \"tools.ozone.moderation.defs#reviewNone\"\n            ]\n          },\n          \"ignoreSubjects\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"uri\"\n            }\n          },\n          \"lastReviewedBy\": {\n            \"type\": \"string\",\n            \"format\": \"did\",\n            \"description\": \"Get all subject statuses that were reviewed by a specific moderator\"\n          },\n          \"sortField\": {\n            \"type\": \"string\",\n            \"default\": \"lastReportedAt\",\n            \"enum\": [\n              \"lastReviewedAt\",\n              \"lastReportedAt\",\n              \"reportedRecordsCount\",\n              \"takendownRecordsCount\",\n              \"priorityScore\"\n            ]\n          },\n          \"sortDirection\": {\n            \"type\": \"string\",\n            \"default\": \"desc\",\n            \"enum\": [\"asc\", \"desc\"]\n          },\n          \"takendown\": {\n            \"type\": \"boolean\",\n            \"description\": \"Get subjects that were taken down\"\n          },\n          \"appealed\": {\n            \"type\": \"boolean\",\n            \"description\": \"Get subjects in unresolved appealed status\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"maxLength\": 25,\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters\"\n            }\n          },\n          \"excludeTags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          },\n          \"collections\": {\n            \"type\": \"array\",\n            \"maxLength\": 20,\n            \"description\": \"If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\"\n            }\n          },\n          \"subjectType\": {\n            \"type\": \"string\",\n            \"description\": \"If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n            \"knownValues\": [\"account\", \"record\"]\n          },\n          \"minAccountSuspendCount\": {\n            \"type\": \"integer\",\n            \"description\": \"If specified, only subjects that belong to an account that has at least this many suspensions will be returned.\"\n          },\n          \"minReportedRecordsCount\": {\n            \"type\": \"integer\",\n            \"description\": \"If specified, only subjects that belong to an account that has at least this many reported records will be returned.\"\n          },\n          \"minTakendownRecordsCount\": {\n            \"type\": \"integer\",\n            \"description\": \"If specified, only subjects that belong to an account that has at least this many taken down records will be returned.\"\n          },\n          \"minPriorityScore\": {\n            \"minimum\": 0,\n            \"maximum\": 100,\n            \"type\": \"integer\",\n            \"description\": \"If specified, only subjects that have priority score value above the given value will be returned.\"\n          },\n          \"minStrikeCount\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"description\": \"If specified, only subjects that belong to an account that has at least this many active strikes will be returned.\"\n          },\n          \"ageAssuranceState\": {\n            \"type\": \"string\",\n            \"description\": \"If specified, only subjects with the given age assurance state will be returned.\",\n            \"knownValues\": [\"pending\", \"assured\", \"unknown\", \"reset\", \"blocked\"]\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"subjectStatuses\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"subjectStatuses\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.moderation.defs#subjectStatusView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/scheduleAction.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.scheduleAction\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Schedule a moderation action to be executed at a future time\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"action\", \"subjects\", \"createdBy\", \"scheduling\"],\n          \"properties\": {\n            \"action\": {\n              \"type\": \"union\",\n              \"refs\": [\"#takedown\"]\n            },\n            \"subjects\": {\n              \"type\": \"array\",\n              \"maxLength\": 100,\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"did\"\n              },\n              \"description\": \"Array of DID subjects to schedule the action for\"\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            },\n            \"scheduling\": {\n              \"type\": \"ref\",\n              \"ref\": \"#schedulingConfig\"\n            },\n            \"modTool\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.moderation.defs#modTool\",\n              \"description\": \"This will be propagated to the moderation event when it is applied\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"#scheduledActionResults\"\n        }\n      }\n    },\n    \"takedown\": {\n      \"type\": \"object\",\n      \"description\": \"Schedule a takedown action\",\n      \"properties\": {\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"durationInHours\": {\n          \"type\": \"integer\",\n          \"description\": \"Indicates how long the takedown should be in effect before automatically expiring.\"\n        },\n        \"acknowledgeAccountSubjects\": {\n          \"type\": \"boolean\",\n          \"description\": \"If true, all other reports on content authored by this account will be resolved (acknowledged).\"\n        },\n        \"policies\": {\n          \"type\": \"array\",\n          \"maxLength\": 5,\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"description\": \"Names/Keywords of the policies that drove the decision.\"\n        },\n        \"severityLevel\": {\n          \"type\": \"string\",\n          \"description\": \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\"\n        },\n        \"strikeCount\": {\n          \"type\": \"integer\",\n          \"description\": \"Number of strikes to assign to the user when takedown is applied.\"\n        },\n        \"strikeExpiresAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"When the strike should expire. If not provided, the strike never expires.\"\n        },\n        \"emailContent\": {\n          \"type\": \"string\",\n          \"description\": \"Email content to be sent to the user upon takedown.\"\n        },\n        \"emailSubject\": {\n          \"type\": \"string\",\n          \"description\": \"Subject of the email to be sent to the user upon takedown.\"\n        }\n      }\n    },\n    \"schedulingConfig\": {\n      \"type\": \"object\",\n      \"description\": \"Configuration for when the action should be executed\",\n      \"properties\": {\n        \"executeAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Exact time to execute the action\"\n        },\n        \"executeAfter\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Earliest time to execute the action (for randomized scheduling)\"\n        },\n        \"executeUntil\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Latest time to execute the action (for randomized scheduling)\"\n        }\n      }\n    },\n    \"scheduledActionResults\": {\n      \"type\": \"object\",\n      \"required\": [\"succeeded\", \"failed\"],\n      \"properties\": {\n        \"succeeded\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\",\n            \"format\": \"did\"\n          }\n        },\n        \"failed\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"#failedScheduling\"\n          }\n        }\n      }\n    },\n    \"failedScheduling\": {\n      \"type\": \"object\",\n      \"required\": [\"subject\", \"error\"],\n      \"properties\": {\n        \"subject\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        },\n        \"error\": {\n          \"type\": \"string\"\n        },\n        \"errorCode\": {\n          \"type\": \"string\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/moderation/searchRepos.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.moderation.searchRepos\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find repositories based on a search term.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"term\": {\n            \"type\": \"string\",\n            \"description\": \"DEPRECATED: use 'q' instead\"\n          },\n          \"q\": { \"type\": \"string\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": { \"type\": \"string\" }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"repos\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"repos\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.moderation.defs#repoView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/report/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.report.defs\",\n  \"defs\": {\n    \"reasonType\": {\n      \"type\": \"string\",\n      \"knownValues\": [\n        \"tools.ozone.report.defs#reasonAppeal\",\n        \"tools.ozone.report.defs#reasonOther\",\n\n        \"tools.ozone.report.defs#reasonViolenceAnimal\",\n        \"tools.ozone.report.defs#reasonViolenceThreats\",\n        \"tools.ozone.report.defs#reasonViolenceGraphicContent\",\n        \"tools.ozone.report.defs#reasonViolenceGlorification\",\n        \"tools.ozone.report.defs#reasonViolenceExtremistContent\",\n        \"tools.ozone.report.defs#reasonViolenceTrafficking\",\n        \"tools.ozone.report.defs#reasonViolenceOther\",\n\n        \"tools.ozone.report.defs#reasonSexualAbuseContent\",\n        \"tools.ozone.report.defs#reasonSexualNCII\",\n        \"tools.ozone.report.defs#reasonSexualDeepfake\",\n        \"tools.ozone.report.defs#reasonSexualAnimal\",\n        \"tools.ozone.report.defs#reasonSexualUnlabeled\",\n        \"tools.ozone.report.defs#reasonSexualOther\",\n\n        \"tools.ozone.report.defs#reasonChildSafetyCSAM\",\n        \"tools.ozone.report.defs#reasonChildSafetyGroom\",\n        \"tools.ozone.report.defs#reasonChildSafetyPrivacy\",\n        \"tools.ozone.report.defs#reasonChildSafetyHarassment\",\n        \"tools.ozone.report.defs#reasonChildSafetyOther\",\n\n        \"tools.ozone.report.defs#reasonHarassmentTroll\",\n        \"tools.ozone.report.defs#reasonHarassmentTargeted\",\n        \"tools.ozone.report.defs#reasonHarassmentHateSpeech\",\n        \"tools.ozone.report.defs#reasonHarassmentDoxxing\",\n        \"tools.ozone.report.defs#reasonHarassmentOther\",\n\n        \"tools.ozone.report.defs#reasonMisleadingBot\",\n        \"tools.ozone.report.defs#reasonMisleadingImpersonation\",\n        \"tools.ozone.report.defs#reasonMisleadingSpam\",\n        \"tools.ozone.report.defs#reasonMisleadingScam\",\n        \"tools.ozone.report.defs#reasonMisleadingElections\",\n        \"tools.ozone.report.defs#reasonMisleadingOther\",\n\n        \"tools.ozone.report.defs#reasonRuleSiteSecurity\",\n        \"tools.ozone.report.defs#reasonRuleProhibitedSales\",\n        \"tools.ozone.report.defs#reasonRuleBanEvasion\",\n        \"tools.ozone.report.defs#reasonRuleOther\",\n\n        \"tools.ozone.report.defs#reasonSelfHarmContent\",\n        \"tools.ozone.report.defs#reasonSelfHarmED\",\n        \"tools.ozone.report.defs#reasonSelfHarmStunts\",\n        \"tools.ozone.report.defs#reasonSelfHarmSubstances\",\n        \"tools.ozone.report.defs#reasonSelfHarmOther\"\n      ]\n    },\n    \"reasonAppeal\": {\n      \"type\": \"token\",\n      \"description\": \"Appeal a previously taken moderation action\"\n    },\n    \"reasonOther\": {\n      \"type\": \"token\",\n      \"description\": \"An issue not included in these options\"\n    },\n    \"reasonViolenceAnimal\": {\n      \"type\": \"token\",\n      \"description\": \"Animal welfare violations\"\n    },\n    \"reasonViolenceThreats\": {\n      \"type\": \"token\",\n      \"description\": \"Threats or incitement\"\n    },\n    \"reasonViolenceGraphicContent\": {\n      \"type\": \"token\",\n      \"description\": \"Graphic violent content\"\n    },\n    \"reasonViolenceGlorification\": {\n      \"type\": \"token\",\n      \"description\": \"Glorification of violence\"\n    },\n    \"reasonViolenceExtremistContent\": {\n      \"type\": \"token\",\n      \"description\": \"Extremist content. These reports will be sent only be sent to the application's Moderation Authority.\"\n    },\n    \"reasonViolenceTrafficking\": {\n      \"type\": \"token\",\n      \"description\": \"Human trafficking\"\n    },\n    \"reasonViolenceOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other violent content\"\n    },\n\n    \"reasonSexualAbuseContent\": {\n      \"type\": \"token\",\n      \"description\": \"Adult sexual abuse content\"\n    },\n    \"reasonSexualNCII\": {\n      \"type\": \"token\",\n      \"description\": \"Non-consensual intimate imagery\"\n    },\n    \"reasonSexualDeepfake\": {\n      \"type\": \"token\",\n      \"description\": \"Deepfake adult content\"\n    },\n    \"reasonSexualAnimal\": {\n      \"type\": \"token\",\n      \"description\": \"Animal sexual abuse\"\n    },\n    \"reasonSexualUnlabeled\": {\n      \"type\": \"token\",\n      \"description\": \"Unlabelled adult content\"\n    },\n    \"reasonSexualOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other sexual violence content\"\n    },\n\n    \"reasonChildSafetyCSAM\": {\n      \"type\": \"token\",\n      \"description\": \"Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority.\"\n    },\n    \"reasonChildSafetyGroom\": {\n      \"type\": \"token\",\n      \"description\": \"Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority.\"\n    },\n    \"reasonChildSafetyPrivacy\": {\n      \"type\": \"token\",\n      \"description\": \"Privacy violation involving a minor\"\n    },\n    \"reasonChildSafetyHarassment\": {\n      \"type\": \"token\",\n      \"description\": \"Harassment or bullying of minors\"\n    },\n    \"reasonChildSafetyOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other child safety. These reports will be sent only be sent to the application's Moderation Authority.\"\n    },\n\n    \"reasonHarassmentTroll\": {\n      \"type\": \"token\",\n      \"description\": \"Trolling\"\n    },\n    \"reasonHarassmentTargeted\": {\n      \"type\": \"token\",\n      \"description\": \"Targeted harassment\"\n    },\n    \"reasonHarassmentHateSpeech\": {\n      \"type\": \"token\",\n      \"description\": \"Hate speech\"\n    },\n    \"reasonHarassmentDoxxing\": {\n      \"type\": \"token\",\n      \"description\": \"Doxxing\"\n    },\n    \"reasonHarassmentOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other harassing or hateful content\"\n    },\n\n    \"reasonMisleadingBot\": {\n      \"type\": \"token\",\n      \"description\": \"Fake account or bot\"\n    },\n    \"reasonMisleadingImpersonation\": {\n      \"type\": \"token\",\n      \"description\": \"Impersonation\"\n    },\n    \"reasonMisleadingSpam\": {\n      \"type\": \"token\",\n      \"description\": \"Spam\"\n    },\n    \"reasonMisleadingScam\": {\n      \"type\": \"token\",\n      \"description\": \"Scam\"\n    },\n    \"reasonMisleadingElections\": {\n      \"type\": \"token\",\n      \"description\": \"False information about elections\"\n    },\n    \"reasonMisleadingOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other misleading content\"\n    },\n\n    \"reasonRuleSiteSecurity\": {\n      \"type\": \"token\",\n      \"description\": \"Hacking or system attacks\"\n    },\n    \"reasonRuleProhibitedSales\": {\n      \"type\": \"token\",\n      \"description\": \"Promoting or selling prohibited items or services\"\n    },\n    \"reasonRuleBanEvasion\": {\n      \"type\": \"token\",\n      \"description\": \"Banned user returning\"\n    },\n    \"reasonRuleOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other\"\n    },\n    \"reasonSelfHarmContent\": {\n      \"type\": \"token\",\n      \"description\": \"Content promoting or depicting self-harm\"\n    },\n    \"reasonSelfHarmED\": {\n      \"type\": \"token\",\n      \"description\": \"Eating disorders\"\n    },\n    \"reasonSelfHarmStunts\": {\n      \"type\": \"token\",\n      \"description\": \"Dangerous challenges or activities\"\n    },\n    \"reasonSelfHarmSubstances\": {\n      \"type\": \"token\",\n      \"description\": \"Dangerous substances or drug abuse\"\n    },\n    \"reasonSelfHarmOther\": {\n      \"type\": \"token\",\n      \"description\": \"Other dangerous content\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/safelink/addRule.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.safelink.addRule\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Add a new URL safety rule\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"url\", \"pattern\", \"action\", \"reason\"],\n          \"properties\": {\n            \"url\": {\n              \"type\": \"string\",\n              \"description\": \"The URL or domain to apply the rule to\"\n            },\n            \"pattern\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#patternType\"\n            },\n            \"action\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#actionType\"\n            },\n            \"reason\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#reasonType\"\n            },\n            \"comment\": {\n              \"type\": \"string\",\n              \"description\": \"Optional comment about the decision\"\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"Author DID. Only respected when using admin auth\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.safelink.defs#event\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"InvalidUrl\",\n          \"description\": \"The provided URL is invalid\"\n        },\n        {\n          \"name\": \"RuleAlreadyExists\",\n          \"description\": \"A rule for this URL/domain already exists\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/safelink/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.safelink.defs\",\n  \"defs\": {\n    \"event\": {\n      \"type\": \"object\",\n      \"description\": \"An event for URL safety decisions\",\n      \"required\": [\n        \"id\",\n        \"eventType\",\n        \"url\",\n        \"pattern\",\n        \"action\",\n        \"reason\",\n        \"createdBy\",\n        \"createdAt\"\n      ],\n      \"properties\": {\n        \"id\": {\n          \"type\": \"integer\",\n          \"description\": \"Auto-incrementing row ID\"\n        },\n        \"eventType\": {\n          \"type\": \"ref\",\n          \"ref\": \"#eventType\"\n        },\n        \"url\": {\n          \"type\": \"string\",\n          \"description\": \"The URL that this rule applies to\"\n        },\n        \"pattern\": {\n          \"type\": \"ref\",\n          \"ref\": \"#patternType\"\n        },\n        \"action\": {\n          \"type\": \"ref\",\n          \"ref\": \"#actionType\"\n        },\n        \"reason\": {\n          \"type\": \"ref\",\n          \"ref\": \"#reasonType\"\n        },\n        \"createdBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"DID of the user who created this rule\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Optional comment about the decision\"\n        }\n      }\n    },\n    \"eventType\": {\n      \"type\": \"string\",\n      \"knownValues\": [\"addRule\", \"updateRule\", \"removeRule\"]\n    },\n    \"patternType\": {\n      \"type\": \"string\",\n      \"knownValues\": [\"domain\", \"url\"]\n    },\n    \"actionType\": {\n      \"type\": \"string\",\n      \"knownValues\": [\"block\", \"warn\", \"whitelist\"]\n    },\n    \"reasonType\": {\n      \"type\": \"string\",\n      \"knownValues\": [\"csam\", \"spam\", \"phishing\", \"none\"]\n    },\n    \"urlRule\": {\n      \"type\": \"object\",\n      \"description\": \"Input for creating a URL safety rule\",\n      \"required\": [\n        \"url\",\n        \"pattern\",\n        \"action\",\n        \"reason\",\n        \"createdBy\",\n        \"createdAt\",\n        \"updatedAt\"\n      ],\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\",\n          \"description\": \"The URL or domain to apply the rule to\"\n        },\n        \"pattern\": {\n          \"type\": \"ref\",\n          \"ref\": \"#patternType\"\n        },\n        \"action\": {\n          \"type\": \"ref\",\n          \"ref\": \"#actionType\"\n        },\n        \"reason\": {\n          \"type\": \"ref\",\n          \"ref\": \"#reasonType\"\n        },\n        \"comment\": {\n          \"type\": \"string\",\n          \"description\": \"Optional comment about the decision\"\n        },\n        \"createdBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"DID of the user added the rule.\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp when the rule was created\"\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp when the rule was last updated\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/safelink/queryEvents.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.safelink.queryEvents\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Query URL safety audit events\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\",\n              \"description\": \"Cursor for pagination\"\n            },\n            \"limit\": {\n              \"type\": \"integer\",\n              \"minimum\": 1,\n              \"maximum\": 100,\n              \"default\": 50,\n              \"description\": \"Maximum number of results to return\"\n            },\n            \"urls\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"description\": \"Filter by specific URLs or domains\"\n            },\n            \"patternType\": {\n              \"type\": \"string\",\n              \"description\": \"Filter by pattern type\"\n            },\n            \"sortDirection\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"asc\", \"desc\"],\n              \"default\": \"desc\",\n              \"description\": \"Sort direction\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"events\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\",\n              \"description\": \"Next cursor for pagination. Only present if there are more results.\"\n            },\n            \"events\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.safelink.defs#event\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/safelink/queryRules.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.safelink.queryRules\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Query URL safety rules\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\",\n              \"description\": \"Cursor for pagination\"\n            },\n            \"limit\": {\n              \"type\": \"integer\",\n              \"minimum\": 1,\n              \"maximum\": 100,\n              \"default\": 50,\n              \"description\": \"Maximum number of results to return\"\n            },\n            \"urls\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"description\": \"Filter by specific URLs or domains\"\n            },\n            \"patternType\": {\n              \"type\": \"string\",\n              \"description\": \"Filter by pattern type\"\n            },\n            \"actions\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"description\": \"Filter by action types\"\n            },\n            \"reason\": {\n              \"type\": \"string\",\n              \"description\": \"Filter by reason type\"\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"Filter by rule creator\"\n            },\n            \"sortDirection\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"asc\", \"desc\"],\n              \"default\": \"desc\",\n              \"description\": \"Sort direction\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"rules\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\",\n              \"description\": \"Next cursor for pagination. Only present if there are more results.\"\n            },\n            \"rules\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.safelink.defs#urlRule\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/safelink/removeRule.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.safelink.removeRule\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Remove an existing URL safety rule\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"url\", \"pattern\"],\n          \"properties\": {\n            \"url\": {\n              \"type\": \"string\",\n              \"description\": \"The URL or domain to remove the rule for\"\n            },\n            \"pattern\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#patternType\"\n            },\n            \"comment\": {\n              \"type\": \"string\",\n              \"description\": \"Optional comment about why the rule is being removed\"\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"Optional DID of the user. Only respected when using admin auth.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.safelink.defs#event\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"RuleNotFound\",\n          \"description\": \"No active rule found for this URL/domain\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/safelink/updateRule.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.safelink.updateRule\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Update an existing URL safety rule\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"url\", \"pattern\", \"action\", \"reason\"],\n          \"properties\": {\n            \"url\": {\n              \"type\": \"string\",\n              \"description\": \"The URL or domain to update the rule for\"\n            },\n            \"pattern\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#patternType\"\n            },\n            \"action\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#actionType\"\n            },\n            \"reason\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.safelink.defs#reasonType\"\n            },\n            \"comment\": {\n              \"type\": \"string\",\n              \"description\": \"Optional comment about the update\"\n            },\n            \"createdBy\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"Optional DID to credit as the creator. Only respected for admin_token authentication.\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.safelink.defs#event\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"RuleNotFound\",\n          \"description\": \"No active rule found for this URL/domain\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/server/getConfig.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.server.getConfig\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get details about ozone's server configuration.\",\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"appview\": {\n              \"type\": \"ref\",\n              \"ref\": \"#serviceConfig\"\n            },\n            \"pds\": {\n              \"type\": \"ref\",\n              \"ref\": \"#serviceConfig\"\n            },\n            \"blobDivert\": {\n              \"type\": \"ref\",\n              \"ref\": \"#serviceConfig\"\n            },\n            \"chat\": {\n              \"type\": \"ref\",\n              \"ref\": \"#serviceConfig\"\n            },\n            \"viewer\": {\n              \"type\": \"ref\",\n              \"ref\": \"#viewerConfig\"\n            },\n            \"verifierDid\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"description\": \"The did of the verifier used for verification.\"\n            }\n          }\n        }\n      }\n    },\n    \"serviceConfig\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        }\n      }\n    },\n    \"viewerConfig\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"role\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"tools.ozone.team.defs#roleAdmin\",\n            \"tools.ozone.team.defs#roleModerator\",\n            \"tools.ozone.team.defs#roleTriage\",\n            \"tools.ozone.team.defs#roleVerifier\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/addValues.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.addValues\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"name\", \"values\"],\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"Name of the set to add values to\"\n            },\n            \"values\": {\n              \"type\": \"array\",\n              \"minLength\": 1,\n              \"maxLength\": 1000,\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"description\": \"Array of string values to add to the set\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.defs\",\n  \"defs\": {\n    \"set\": {\n      \"type\": \"object\",\n      \"required\": [\"name\"],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"minLength\": 3,\n          \"maxLength\": 128\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 1024,\n          \"maxLength\": 10240\n        }\n      }\n    },\n    \"setView\": {\n      \"type\": \"object\",\n      \"required\": [\"name\", \"setSize\", \"createdAt\", \"updatedAt\"],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"minLength\": 3,\n          \"maxLength\": 128\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 1024,\n          \"maxLength\": 10240\n        },\n        \"setSize\": {\n          \"type\": \"integer\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/deleteSet.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.deleteSet\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete an entire set. Attempting to delete a set that does not exist will result in an error.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"name\"],\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"Name of the set to delete\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"SetNotFound\",\n          \"description\": \"set with the given name does not exist\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/deleteValues.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.deleteValues\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"name\", \"values\"],\n          \"properties\": {\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"Name of the set to delete values from\"\n            },\n            \"values\": {\n              \"type\": \"array\",\n              \"minLength\": 1,\n              \"items\": {\n                \"type\": \"string\"\n              },\n              \"description\": \"Array of string values to delete from the set\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"SetNotFound\",\n          \"description\": \"set with the given name does not exist\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/getValues.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.getValues\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get a specific set and its values\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"name\"],\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 1000,\n            \"default\": 100\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"set\", \"values\"],\n          \"properties\": {\n            \"set\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.set.defs#setView\"\n            },\n            \"values\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            },\n            \"cursor\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"SetNotFound\",\n          \"description\": \"set with the given name does not exist\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/querySets.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.querySets\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Query available sets\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          },\n          \"namePrefix\": {\n            \"type\": \"string\"\n          },\n          \"sortBy\": {\n            \"type\": \"string\",\n            \"enum\": [\"name\", \"createdAt\", \"updatedAt\"],\n            \"default\": \"name\"\n          },\n          \"sortDirection\": {\n            \"type\": \"string\",\n            \"default\": \"asc\",\n            \"enum\": [\"asc\", \"desc\"],\n            \"description\": \"Defaults to ascending order of name field.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"sets\"],\n          \"properties\": {\n            \"sets\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.set.defs#setView\"\n              }\n            },\n            \"cursor\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/set/upsertSet.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.set.upsertSet\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create or update set metadata\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.set.defs#set\"\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.set.defs#setView\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/setting/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.setting.defs\",\n  \"defs\": {\n    \"option\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"key\",\n        \"value\",\n        \"did\",\n        \"scope\",\n        \"createdBy\",\n        \"lastUpdatedBy\"\n      ],\n      \"properties\": {\n        \"key\": {\n          \"type\": \"string\",\n          \"format\": \"nsid\"\n        },\n        \"did\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        },\n        \"value\": {\n          \"type\": \"unknown\"\n        },\n        \"description\": {\n          \"type\": \"string\",\n          \"maxGraphemes\": 1024,\n          \"maxLength\": 10240\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\"\n        },\n        \"managerRole\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"tools.ozone.team.defs#roleModerator\",\n            \"tools.ozone.team.defs#roleTriage\",\n            \"tools.ozone.team.defs#roleAdmin\",\n            \"tools.ozone.team.defs#roleVerifier\"\n          ]\n        },\n        \"scope\": {\n          \"type\": \"string\",\n          \"knownValues\": [\"instance\", \"personal\"]\n        },\n        \"createdBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        },\n        \"lastUpdatedBy\": {\n          \"type\": \"string\",\n          \"format\": \"did\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/setting/listOptions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.setting.listOptions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List settings with optional filtering\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          },\n          \"scope\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"instance\", \"personal\"],\n            \"default\": \"instance\"\n          },\n          \"prefix\": {\n            \"type\": \"string\",\n            \"description\": \"Filter keys by prefix\"\n          },\n          \"keys\": {\n            \"type\": \"array\",\n            \"maxLength\": 100,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\"\n            },\n            \"description\": \"Filter for only the specified keys. Ignored if prefix is provided\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"options\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"options\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.setting.defs#option\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/setting/removeOptions.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.setting.removeOptions\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete settings by key\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"keys\", \"scope\"],\n          \"properties\": {\n            \"keys\": {\n              \"type\": \"array\",\n              \"minLength\": 1,\n              \"maxLength\": 200,\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"nsid\"\n              }\n            },\n            \"scope\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"instance\", \"personal\"]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"properties\": {}\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/setting/upsertOption.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.setting.upsertOption\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Create or update setting option\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"key\", \"scope\", \"value\"],\n          \"properties\": {\n            \"key\": {\n              \"type\": \"string\",\n              \"format\": \"nsid\"\n            },\n            \"scope\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"instance\", \"personal\"]\n            },\n            \"value\": {\n              \"type\": \"unknown\"\n            },\n            \"description\": {\n              \"type\": \"string\",\n              \"maxLength\": 2000\n            },\n            \"managerRole\": {\n              \"type\": \"string\",\n              \"knownValues\": [\n                \"tools.ozone.team.defs#roleModerator\",\n                \"tools.ozone.team.defs#roleTriage\",\n                \"tools.ozone.team.defs#roleVerifier\",\n                \"tools.ozone.team.defs#roleAdmin\"\n              ]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"option\"],\n          \"properties\": {\n            \"option\": {\n              \"type\": \"ref\",\n              \"ref\": \"tools.ozone.setting.defs#option\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/signature/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.signature.defs\",\n  \"defs\": {\n    \"sigDetail\": {\n      \"type\": \"object\",\n      \"required\": [\"property\", \"value\"],\n      \"properties\": {\n        \"property\": { \"type\": \"string\" },\n        \"value\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/signature/findCorrelation.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.signature.findCorrelation\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Find all correlated threat signatures between 2 or more accounts.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"dids\"],\n        \"properties\": {\n          \"dids\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"details\"],\n          \"properties\": {\n            \"details\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.signature.defs#sigDetail\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/signature/findRelatedAccounts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.signature.findRelatedAccounts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Get accounts that share some matching threat signatures with the root account.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"did\"],\n        \"properties\": {\n          \"did\": {\n            \"type\": \"string\",\n            \"format\": \"did\"\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"accounts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"accounts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#relatedAccount\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"relatedAccount\": {\n      \"type\": \"object\",\n      \"required\": [\"account\"],\n      \"properties\": {\n        \"account\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.admin.defs#accountView\"\n        },\n        \"similarities\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"ref\",\n            \"ref\": \"tools.ozone.signature.defs#sigDetail\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/signature/searchAccounts.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.signature.searchAccounts\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"Search for accounts that match one or more threat signature values.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"values\"],\n        \"properties\": {\n          \"values\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cursor\": { \"type\": \"string\" },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"accounts\"],\n          \"properties\": {\n            \"cursor\": { \"type\": \"string\" },\n            \"accounts\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"com.atproto.admin.defs#accountView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/team/addMember.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.team.addMember\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Add a member to the ozone team. Requires admin role.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\", \"role\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"role\": {\n              \"type\": \"string\",\n              \"knownValues\": [\n                \"tools.ozone.team.defs#roleAdmin\",\n                \"tools.ozone.team.defs#roleModerator\",\n                \"tools.ozone.team.defs#roleVerifier\",\n                \"tools.ozone.team.defs#roleTriage\"\n              ]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.team.defs#member\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"MemberAlreadyExists\",\n          \"description\": \"Member already exists in the team.\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/team/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.team.defs\",\n  \"defs\": {\n    \"member\": {\n      \"type\": \"object\",\n      \"required\": [\"did\", \"role\"],\n      \"properties\": {\n        \"did\": { \"type\": \"string\", \"format\": \"did\" },\n        \"disabled\": { \"type\": \"boolean\" },\n        \"profile\": {\n          \"type\": \"ref\",\n          \"ref\": \"app.bsky.actor.defs#profileViewDetailed\"\n        },\n        \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"updatedAt\": { \"type\": \"string\", \"format\": \"datetime\" },\n        \"lastUpdatedBy\": { \"type\": \"string\" },\n        \"role\": {\n          \"type\": \"string\",\n          \"knownValues\": [\n            \"tools.ozone.team.defs#roleAdmin\",\n            \"tools.ozone.team.defs#roleModerator\",\n            \"tools.ozone.team.defs#roleTriage\",\n            \"tools.ozone.team.defs#roleVerifier\"\n          ]\n        }\n      }\n    },\n    \"roleAdmin\": {\n      \"type\": \"token\",\n      \"description\": \"Admin role. Highest level of access, can perform all actions.\"\n    },\n    \"roleModerator\": {\n      \"type\": \"token\",\n      \"description\": \"Moderator role. Can perform most actions.\"\n    },\n    \"roleTriage\": {\n      \"type\": \"token\",\n      \"description\": \"Triage role. Mostly intended for monitoring and escalating issues.\"\n    },\n    \"roleVerifier\": {\n      \"type\": \"token\",\n      \"description\": \"Verifier role. Only allowed to issue verifications.\"\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/team/deleteMember.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.team.deleteMember\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Delete a member from ozone team. Requires admin role.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" }\n          }\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"MemberNotFound\",\n          \"description\": \"The member being deleted does not exist\"\n        },\n        {\n          \"name\": \"CannotDeleteSelf\",\n          \"description\": \"You can not delete yourself from the team\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/team/listMembers.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.team.listMembers\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List all members with access to the ozone service.\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"q\": {\n            \"type\": \"string\"\n          },\n          \"disabled\": {\n            \"type\": \"boolean\"\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"cursor\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"members\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"members\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.team.defs#member\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/team/updateMember.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.team.updateMember\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Update a member in the ozone service. Requires admin role.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"did\"],\n          \"properties\": {\n            \"did\": { \"type\": \"string\", \"format\": \"did\" },\n            \"disabled\": { \"type\": \"boolean\" },\n            \"role\": {\n              \"type\": \"string\",\n              \"knownValues\": [\n                \"tools.ozone.team.defs#roleAdmin\",\n                \"tools.ozone.team.defs#roleModerator\",\n                \"tools.ozone.team.defs#roleVerifier\",\n                \"tools.ozone.team.defs#roleTriage\"\n              ]\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"ref\",\n          \"ref\": \"tools.ozone.team.defs#member\"\n        }\n      },\n      \"errors\": [\n        {\n          \"name\": \"MemberNotFound\",\n          \"description\": \"The member being updated does not exist in the team\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/verification/defs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.verification.defs\",\n  \"defs\": {\n    \"verificationView\": {\n      \"type\": \"object\",\n      \"description\": \"Verification data for the associated subject.\",\n      \"required\": [\n        \"issuer\",\n        \"uri\",\n        \"subject\",\n        \"handle\",\n        \"displayName\",\n        \"createdAt\"\n      ],\n      \"properties\": {\n        \"issuer\": {\n          \"type\": \"string\",\n          \"description\": \"The user who issued this verification.\",\n          \"format\": \"did\"\n        },\n        \"uri\": {\n          \"type\": \"string\",\n          \"description\": \"The AT-URI of the verification record.\",\n          \"format\": \"at-uri\"\n        },\n        \"subject\": {\n          \"type\": \"string\",\n          \"format\": \"did\",\n          \"description\": \"The subject of the verification.\"\n        },\n        \"handle\": {\n          \"type\": \"string\",\n          \"description\": \"Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.\",\n          \"format\": \"handle\"\n        },\n        \"displayName\": {\n          \"type\": \"string\",\n          \"description\": \"Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"description\": \"Timestamp when the verification was created.\",\n          \"format\": \"datetime\"\n        },\n        \"revokeReason\": {\n          \"type\": \"string\",\n          \"description\": \"Describes the reason for revocation, also indicating that the verification is no longer valid.\"\n        },\n        \"revokedAt\": {\n          \"type\": \"string\",\n          \"description\": \"Timestamp when the verification was revoked.\",\n          \"format\": \"datetime\"\n        },\n        \"revokedBy\": {\n          \"type\": \"string\",\n          \"description\": \"The user who revoked this verification.\",\n          \"format\": \"did\"\n        },\n        \"subjectProfile\": {\n          \"type\": \"union\",\n          \"refs\": []\n        },\n        \"issuerProfile\": {\n          \"type\": \"union\",\n          \"refs\": []\n        },\n        \"subjectRepo\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"tools.ozone.moderation.defs#repoViewDetail\",\n            \"tools.ozone.moderation.defs#repoViewNotFound\"\n          ]\n        },\n        \"issuerRepo\": {\n          \"type\": \"union\",\n          \"refs\": [\n            \"tools.ozone.moderation.defs#repoViewDetail\",\n            \"tools.ozone.moderation.defs#repoViewNotFound\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/verification/grantVerifications.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.verification.grantVerifications\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Grant verifications to multiple subjects. Allows batch processing of up to 100 verifications at once.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"verifications\"],\n          \"properties\": {\n            \"verifications\": {\n              \"type\": \"array\",\n              \"description\": \"Array of verification requests to process\",\n              \"maxLength\": 100,\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#verificationInput\"\n              }\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"verifications\", \"failedVerifications\"],\n          \"properties\": {\n            \"verifications\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.verification.defs#verificationView\"\n              }\n            },\n            \"failedVerifications\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#grantError\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"verificationInput\": {\n      \"type\": \"object\",\n      \"required\": [\"subject\", \"handle\", \"displayName\"],\n      \"properties\": {\n        \"subject\": {\n          \"type\": \"string\",\n          \"description\": \"The did of the subject being verified\",\n          \"format\": \"did\"\n        },\n        \"handle\": {\n          \"type\": \"string\",\n          \"description\": \"Handle of the subject the verification applies to at the moment of verifying.\",\n          \"format\": \"handle\"\n        },\n        \"displayName\": {\n          \"type\": \"string\",\n          \"description\": \"Display name of the subject the verification applies to at the moment of verifying.\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"datetime\",\n          \"description\": \"Timestamp for verification record. Defaults to current time when not specified.\"\n        }\n      }\n    },\n    \"grantError\": {\n      \"type\": \"object\",\n      \"description\": \"Error object for failed verifications.\",\n      \"required\": [\"error\", \"subject\"],\n      \"properties\": {\n        \"error\": {\n          \"type\": \"string\",\n          \"description\": \"Error message describing the reason for failure.\"\n        },\n        \"subject\": {\n          \"type\": \"string\",\n          \"description\": \"The did of the subject being verified\",\n          \"format\": \"did\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/verification/listVerifications.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.verification.listVerifications\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"List verifications\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"cursor\": {\n            \"type\": \"string\",\n            \"description\": \"Pagination cursor\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"description\": \"Maximum number of results to return\",\n            \"minimum\": 1,\n            \"maximum\": 100,\n            \"default\": 50\n          },\n          \"createdAfter\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Filter to verifications created after this timestamp\"\n          },\n          \"createdBefore\": {\n            \"type\": \"string\",\n            \"format\": \"datetime\",\n            \"description\": \"Filter to verifications created before this timestamp\"\n          },\n          \"issuers\": {\n            \"type\": \"array\",\n            \"maxLength\": 100,\n            \"description\": \"Filter to verifications from specific issuers\",\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          },\n          \"subjects\": {\n            \"type\": \"array\",\n            \"description\": \"Filter to specific verified DIDs\",\n            \"maxLength\": 100,\n            \"items\": {\n              \"type\": \"string\",\n              \"format\": \"did\"\n            }\n          },\n          \"sortDirection\": {\n            \"type\": \"string\",\n            \"description\": \"Sort direction for creation date\",\n            \"enum\": [\"asc\", \"desc\"],\n            \"default\": \"desc\"\n          },\n          \"isRevoked\": {\n            \"type\": \"boolean\",\n            \"description\": \"Filter to verifications that are revoked or not. By default, includes both.\"\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"verifications\"],\n          \"properties\": {\n            \"cursor\": {\n              \"type\": \"string\"\n            },\n            \"verifications\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"tools.ozone.verification.defs#verificationView\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lexicons/tools/ozone/verification/revokeVerifications.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"tools.ozone.verification.revokeVerifications\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"Revoke previously granted verifications in batches of up to 100.\",\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"uris\"],\n          \"properties\": {\n            \"uris\": {\n              \"type\": \"array\",\n              \"description\": \"Array of verification record uris to revoke\",\n              \"maxLength\": 100,\n              \"items\": {\n                \"type\": \"string\",\n                \"description\": \"The AT-URI of the verification record to revoke.\",\n                \"format\": \"at-uri\"\n              }\n            },\n            \"revokeReason\": {\n              \"type\": \"string\",\n              \"description\": \"Reason for revoking the verification. This is optional and can be omitted if not needed.\",\n              \"maxLength\": 1000\n            }\n          }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"revokedVerifications\", \"failedRevocations\"],\n          \"properties\": {\n            \"revokedVerifications\": {\n              \"type\": \"array\",\n              \"description\": \"List of verification uris successfully revoked\",\n              \"items\": {\n                \"type\": \"string\",\n                \"format\": \"at-uri\"\n              }\n            },\n            \"failedRevocations\": {\n              \"type\": \"array\",\n              \"description\": \"List of verification uris that couldn't be revoked, including failure reasons\",\n              \"items\": {\n                \"type\": \"ref\",\n                \"ref\": \"#revokeError\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"revokeError\": {\n      \"type\": \"object\",\n      \"description\": \"Error object for failed revocations\",\n      \"required\": [\"uri\", \"error\"],\n      \"properties\": {\n        \"uri\": {\n          \"type\": \"string\",\n          \"description\": \"The AT-URI of the verification record that failed to revoke.\",\n          \"format\": \"at-uri\"\n        },\n        \"error\": {\n          \"type\": \"string\",\n          \"description\": \"Description of the error that occurred during revocation.\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"atp\",\n  \"version\": \"0.0.1\",\n  \"repository\": \"git@github.com:bluesky-social/atproto.git\",\n  \"author\": \"Bluesky Social PBC <hello@blueskyweb.xyz>\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"packageManager\": \"pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81\",\n  \"scripts\": {\n    \"lint:fix\": \"pnpm lint --fix\",\n    \"lint\": \"NODE_OPTIONS=--max_old_space_size=4096 eslint . --ext .ts,.js,.tsx,.jsx\",\n    \"style:fix\": \"prettier --write .\",\n    \"style\": \"prettier --check .\",\n    \"verify\": \"pnpm --stream '/^verify:.+$/'\",\n    \"verify:style\": \"pnpm run style\",\n    \"verify:lint\": \"pnpm lint\",\n    \"verify:types\": \"tsc --build tsconfig.json\",\n    \"format\": \"pnpm lint:fix && pnpm style:fix\",\n    \"precodegen\": \"pnpm run --recursive --stream --filter '@atproto/lex-cli...' --filter '@atproto/lex...' build --force\",\n    \"codegen\": \"pnpm run --sort --recursive --stream --parallel codegen\",\n    \"build\": \"pnpm run --sort --recursive --stream build\",\n    \"i18n\": \"pnpm run --parallel i18n:extract\",\n    \"dev\": \"NODE_ENV=development pnpm run --recursive --parallel --stream '/^(dev|dev:.+)$/'\",\n    \"dev:tsc\": \"tsc --build tsconfig.json --preserveWatchOutput --watch\",\n    \"test\": \"LOG_ENABLED=false ./packages/dev-infra/with-test-redis-and-db.sh pnpm test --stream --recursive\",\n    \"test:withFlags\": \"pnpm run test --\",\n    \"changeset\": \"changeset\",\n    \"release\": \"pnpm build && changeset publish\",\n    \"version-packages\": \"changeset version && git add .\"\n  },\n  \"devDependencies\": {\n    \"@atproto/dev-env\": \"workspace:^\",\n    \"@changesets/changelog-github\": \"^0.5.1\",\n    \"@changesets/cli\": \"^2.29.7\",\n    \"@swc/core\": \"^1.3.42\",\n    \"@swc/jest\": \"^0.2.24\",\n    \"@types/jest\": \"^28.1.4\",\n    \"@types/node\": \"^18.19.67\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.4.0\",\n    \"@typescript-eslint/parser\": \"^7.4.0\",\n    \"@vitest/coverage-v8\": \"4.0.16\",\n    \"dotenv\": \"^16.0.3\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-import-resolver-typescript\": \"^3.7.0\",\n    \"eslint-plugin-import\": \"^2.31.0\",\n    \"eslint-plugin-n\": \"^17.15.0\",\n    \"eslint-plugin-prettier\": \"^5.1.3\",\n    \"jest\": \"^28.1.2\",\n    \"node-gyp\": \"^9.3.1\",\n    \"pino-pretty\": \"^9.1.0\",\n    \"prettier\": \"^3.2.5\",\n    \"prettier-config-standard\": \"^7.0.0\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.11\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"cookie\": \"^0.7.2\"\n    }\n  },\n  \"workspace\": [\n    \"packages/*\",\n    \"packages/lex/*\",\n    \"packages/oauth/*\",\n    \"packages/internal/*\"\n  ],\n  \"workspaces\": {\n    \"packages\": [\n      \"packages/*\",\n      \"packages/lex/*\",\n      \"packages/oauth/*\",\n      \"packages/internal/*\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/README.md",
    "content": "# Packages\n\n## Applications\n\n- [PDS](./pds): The Personal Data Server (PDS). This is atproto's main server-side implementation.\n- [Dev Env](./dev-env): A command-line application for developers to construct and manage development environments.\n- [Lexicon CLI](./lex-cli/): A command-line application for generating code and documentation from Lexicon schemas.\n\n## Libraries\n\n- [API](./api): A library for communicating with atproto servers.\n- [Common](./common): A library containing code which is shared between atproto packages.\n- [Crypto](./crypto): Atproto's common cryptographic operations.\n- [Syntax](./syntax): A library for identifier syntax: NSID, AT URI, handles, etc.\n- [Lexicon](./lexicon): A library for validating data using atproto's schema system.\n- [OAuth Provider](./oauth/oauth-provider): A library for supporting ATPROTO's OAuth.\n- [Repo](./repo): The \"atproto repository\" core implementation (a Merkle Search Tree).\n- [WebSocket Client](./ws-client): A library for working with long-lived WebSocket client connections.\n- [XRPC](./xrpc): An XRPC client implementation.\n- [XRPC Server](./xrpc-server): An XRPC server implementation.\n\n## Benchmarking and profiling\n\nOnly applicable to packages which contain benchmarks(`jest.bench.config.js`).\n\nYou can run benchmarks with `pnpm bench`.\n\n### Attaching a profiler\n\nRunning `pnpm bench:profile` will launch `bench` with `--inspect-brk` flag.\nExecution will be paused until a debugger is attached, you can read more\nabout node debuggers [here](https://nodejs.org/en/docs/guides/debugging-getting-started#inspector-clients)\n\nAn easy way to profile is:\n\n1. open `about://inspect` in chrome\n2. select which process to connect to(there will probably only be one)\n3. go to performance tab\n4. press record, this will unpause execution\n5. wait for the benches to run\n6. finish recording\n"
  },
  {
    "path": "packages/api/CHANGELOG.md",
    "content": "# @atproto/api\n\n## 0.19.4\n\n### Patch Changes\n\n- [#4709](https://github.com/bluesky-social/atproto/pull/4709) [`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa) Thanks [@foysalit](https://github.com/foysalit)! - Introduce a purge event to remove ozone's data on age assurance\n\n- Updated dependencies [[`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f)]:\n  - @atproto/syntax@0.5.1\n\n## 0.19.3\n\n### Patch Changes\n\n- [#4717](https://github.com/bluesky-social/atproto/pull/4717) [`76ab6ea`](https://github.com/bluesky-social/atproto/commit/76ab6eaa7bfa49fc218299d09446bb339c700bb5) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Trust `status` returned from `refreshSession` and do not fall back to a potentially stale value from `this.session`.\n\n## 0.19.2\n\n### Patch Changes\n\n- [#4683](https://github.com/bluesky-social/atproto/pull/4683) [`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Introduce recIdStr field\n\n- [#4713](https://github.com/bluesky-social/atproto/pull/4713) [`0e5df95`](https://github.com/bluesky-social/atproto/commit/0e5df95e3a8d81931524848d301cd43d1f12fb78) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make sure to always trigger a `refreshSession` when `resumeSession()` is called\n\n## 0.19.1\n\n### Patch Changes\n\n- [#4704](https://github.com/bluesky-social/atproto/pull/4704) [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Add feed to sendInteractions input\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common-web@0.4.18\n  - @atproto/lexicon@0.6.2\n\n## 0.19.0\n\n### Minor Changes\n\n- [#4631](https://github.com/bluesky-social/atproto/pull/4631) [`450f085`](https://github.com/bluesky-social/atproto/commit/450f0856630fa08c20dc60fef8b5d2a07b9a2552) Thanks [@LotharieSlayer](https://github.com/LotharieSlayer)! - Updating atproto app password based session example in README\n\n## 0.18.21\n\n### Patch Changes\n\n- [#4633](https://github.com/bluesky-social/atproto/pull/4633) [`60f84eb`](https://github.com/bluesky-social/atproto/commit/60f84ebe47016828add07b143c403e331c58ee78) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Increase draft char limit from 300 to 1000\n\n- [#4632](https://github.com/bluesky-social/atproto/pull/4632) [`50dfbec`](https://github.com/bluesky-social/atproto/commit/50dfbec512682d35e8108b952e8f0533da71beef) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add app.bsky.unspecced.getSuggestedOnboardingUser and app.bsky.unspecced.getSuggestedOnboardingUsersSkeleton\n\n- [#4594](https://github.com/bluesky-social/atproto/pull/4594) [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317) Thanks [@bnewbold](https://github.com/bnewbold)! - update germ networks lexicon\n\n- [#4594](https://github.com/bluesky-social/atproto/pull/4594) [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317) Thanks [@bnewbold](https://github.com/bnewbold)! - add `none` to germ declaration record\n\n## 0.18.20\n\n### Patch Changes\n\n- [#4591](https://github.com/bluesky-social/atproto/pull/4591) [`4f5c400`](https://github.com/bluesky-social/atproto/commit/4f5c4001271bbf38b30506efd30ebdabb969878f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Rename `platform` to `deviceName` on `draft` view, add maxLength.\n\n## 0.18.19\n\n### Patch Changes\n\n- [#4590](https://github.com/bluesky-social/atproto/pull/4590) [`25cea46`](https://github.com/bluesky-social/atproto/commit/25cea46aaa3d84521d1e977b67d3ac3581304ba1) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `deviceId` and `platform` to drafts as optional props\n\n- Updated dependencies []:\n  - @atproto/common-web@0.4.15\n\n## 0.18.18\n\n### Patch Changes\n\n- [#4581](https://github.com/bluesky-social/atproto/pull/4581) [`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7) Thanks [@mozzius](https://github.com/mozzius)! - Add `presentation` to video embed as a hint to the client about how to display the video\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/common-web@0.4.14\n\n## 0.18.17\n\n### Patch Changes\n\n- [#4565](https://github.com/bluesky-social/atproto/pull/4565) [`cbd5837`](https://github.com/bluesky-social/atproto/commit/cbd5837f015e6b5e098a60098faea82e7f9419f3) Thanks [@cuducos](https://github.com/cuducos)! - Re-add `recId` to suggested users (now, as string)\n\n- [#4547](https://github.com/bluesky-social/atproto/pull/4547) [`d8e5363`](https://github.com/bluesky-social/atproto/commit/d8e53636c84da6dd3dd69e1d260f4fa617f3883c) Thanks [@cuducos](https://github.com/cuducos)! - Removes `recId` from suggested users — we need it as a string, so we're gonna re-add it as string (instead of integer) later.\n\n- [#4415](https://github.com/bluesky-social/atproto/pull/4415) [`9bdd358`](https://github.com/bluesky-social/atproto/commit/9bdd35881aa7efce6595ef708ba13d99c473d114) Thanks [@bnewbold](https://github.com/bnewbold)! - support for Germ Networks chat declaration records\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/syntax@0.4.3\n  - @atproto/common-web@0.4.13\n  - @atproto/lexicon@0.6.1\n\n## 0.18.16\n\n### Patch Changes\n\n- [#4552](https://github.com/bluesky-social/atproto/pull/4552) [`ccd8964`](https://github.com/bluesky-social/atproto/commit/ccd89643313799f47c2f009c5c9dca48540275f1) Thanks [@mozzius](https://github.com/mozzius)! - Add `draft` lexicons\n\n## 0.18.15\n\n### Patch Changes\n\n- [#4543](https://github.com/bluesky-social/atproto/pull/4543) [`f58029b`](https://github.com/bluesky-social/atproto/commit/f58029ba54305bed361c834a42bd96022b5b3c59) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `liveEventPreferences` to user preferences, add `updateLiveEventPreferences` to API SDK\n\n## 0.18.14\n\n### Patch Changes\n\n- [#4539](https://github.com/bluesky-social/atproto/pull/4539) [`3ffebd0`](https://github.com/bluesky-social/atproto/commit/3ffebd0bf25776308e06e4b083dc2d0e156d9ac0) Thanks [@mozzius](https://github.com/mozzius)! - Add $cashtag support to the Rich Text facet detection\n\n- Updated dependencies []:\n  - @atproto/common-web@0.4.12\n\n## 0.18.13\n\n### Patch Changes\n\n- [#4520](https://github.com/bluesky-social/atproto/pull/4520) [`d2ed731`](https://github.com/bluesky-social/atproto/commit/d2ed7311a20b8c990003628c932e3e5aa6569086) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `isDisabled` to `#statusView`\n\n## 0.18.12\n\n### Patch Changes\n\n- [#4516](https://github.com/bluesky-social/atproto/pull/4516) [`7750b91`](https://github.com/bluesky-social/atproto/commit/7750b91500eef6965a17bc8ec0b3ddfd6327485a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `uri` and `cid` to `#statusView`\n\n## 0.18.11\n\n### Patch Changes\n\n- [#4513](https://github.com/bluesky-social/atproto/pull/4513) [`7ef8935`](https://github.com/bluesky-social/atproto/commit/7ef893563b25252ecf246e0d75e17855a7284e53) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `minAccessAge` to Age Assurance regional configs.\n\n## 0.18.10\n\n### Patch Changes\n\n- [#4440](https://github.com/bluesky-social/atproto/pull/4440) [`63f97ae`](https://github.com/bluesky-social/atproto/commit/63f97ae9c1f57def2d489ab8ce7f83a84a7d1ba1) Thanks [@iwsmith](https://github.com/iwsmith)! - Add `recID` field to `getSuggestedUsers` and `getSuggestedUsersSkeleton`\n\n## 0.18.9\n\n### Patch Changes\n\n- [#4470](https://github.com/bluesky-social/atproto/pull/4470) [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use 401 status code as signal that the credentials are invalid and should no longer be used.\n\n- [#4470](https://github.com/bluesky-social/atproto/pull/4470) [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The `CredentialSession.resumeSession()` method now leverages full session data to restore user sessions in a single HTTP call (instead of up to three before). Servers that do not return `email` and `emailConfirmed` session fields will still be supported, but will cause an additional request to `com.atproto.server.getSession` to fetch the missing data.\n\n- Updated dependencies []:\n  - @atproto/common-web@0.4.8\n\n## 0.18.8\n\n### Patch Changes\n\n- [#4452](https://github.com/bluesky-social/atproto/pull/4452) [`2e5a24c`](https://github.com/bluesky-social/atproto/commit/2e5a24cb875650120365e3f5c23a041e61a5f9c4) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Remove WARNING from contact lexicons\n\n- [#4445](https://github.com/bluesky-social/atproto/pull/4445) [`5622bcf`](https://github.com/bluesky-social/atproto/commit/5622bcf02315f9f24940a32aa3a6d9341c646c59) Thanks [@mozzius](https://github.com/mozzius)! - Add XRPC errors for `contact` APIs\n\n## 0.18.7\n\n### Patch Changes\n\n- [#4436](https://github.com/bluesky-social/atproto/pull/4436) [`e266405`](https://github.com/bluesky-social/atproto/commit/e266405a89cd081ff96d36a784a31dd917c60a15) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add \"contact-match\" to listNotification reasons\n\n## 0.18.6\n\n### Patch Changes\n\n- [#4432](https://github.com/bluesky-social/atproto/pull/4432) [`39fa570`](https://github.com/bluesky-social/atproto/commit/39fa57080fa04aa547b093cfeaaced3e2e62fc41) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add new read-only `#declaredAgePref` with computed age flags e.g. `isOverAge18`.\n\n- [#4430](https://github.com/bluesky-social/atproto/pull/4430) [`f4cef84`](https://github.com/bluesky-social/atproto/commit/f4cef84494114ca927c66428920ca3dc24ad2b1e) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add app.bsky.contact.sendNotification endpoint\n\n## 0.18.5\n\n### Patch Changes\n\n- [#4393](https://github.com/bluesky-social/atproto/pull/4393) [`380aa3b`](https://github.com/bluesky-social/atproto/commit/380aa3bfe73b5c4e59961c27ae988786b69c129d) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add app.bsky.contact.\\* lexicons, still without error handling. This is unstable and should not be used at this state.\n\n- [#4418](https://github.com/bluesky-social/atproto/pull/4418) [`308f432`](https://github.com/bluesky-social/atproto/commit/308f432f7aef196b4df0a6dc7c5367ab5a8b8964) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Expand return type of relationships in app.bsky.graph.getRelationships\n\n- [#4423](https://github.com/bluesky-social/atproto/pull/4423) [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763) Thanks [@foysalit](https://github.com/foysalit)! - Add min length for required comment fields in ozone events\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/common-web@0.4.7\n  - @atproto/xrpc@0.7.7\n\n## 0.18.4\n\n### Patch Changes\n\n- [#4407](https://github.com/bluesky-social/atproto/pull/4407) [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds ageassurance namespace, methods, and utils for Age Assurance V2\n\n- Updated dependencies [[`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69)]:\n  - @atproto/syntax@0.4.2\n  - @atproto/common-web@0.4.6\n\n## 0.18.3\n\n### Patch Changes\n\n- [#4347](https://github.com/bluesky-social/atproto/pull/4347) [`69f53d6`](https://github.com/bluesky-social/atproto/commit/69f53d632d84f255cafa8b10698184048a71b97b) Thanks [@bnewbold](https://github.com/bnewbold)! - lexicon updates to have fully-qualified token refs in knownValue lists\n\n- Updated dependencies []:\n  - @atproto/common-web@0.4.5\n\n## 0.18.2\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n  - @atproto/lexicon@0.5.2\n  - @atproto/xrpc@0.7.6\n\n## 0.18.1\n\n### Patch Changes\n\n- [#4340](https://github.com/bluesky-social/atproto/pull/4340) [`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e) Thanks [@foysalit](https://github.com/foysalit)! - Add optional email data to scheduled action api in ozone\n\n- [#4344](https://github.com/bluesky-social/atproto/pull/4344) [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc) Thanks [@foysalit](https://github.com/foysalit)! - Add targetServices param to takedown events allowing mods to specify which service to apply takedown on\n\n## 0.18.0\n\n### Minor Changes\n\n- [#4227](https://github.com/bluesky-social/atproto/pull/4227) [`94ddc8219`](https://github.com/bluesky-social/atproto/commit/94ddc8219c144475df622137ab88895255136eda) Thanks [@bnewbold](https://github.com/bnewbold)! - Introduce `com.atproto.lexicon.resolveLexicon` lexicon method\n\n### Patch Changes\n\n- [#4269](https://github.com/bluesky-social/atproto/pull/4269) [`39b5c08e0`](https://github.com/bluesky-social/atproto/commit/39b5c08e0799468eba0c3bf50f4f5a8104c35f34) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Deprecate and remove `prioritizeFollowedUsers` setting from preferences response types and `getPostThreadV2` query params.\n\n## 0.17.7\n\n### Patch Changes\n\n- [#4317](https://github.com/bluesky-social/atproto/pull/4317) [`15fe80c39`](https://github.com/bluesky-social/atproto/commit/15fe80c39ff428652dfaa6b30c0bdb59a145aac6) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `via` to `follow` record to mirror `like` records and provide a way to reference the starter pack that the follow originated from.\n\n## 0.17.6\n\n### Patch Changes\n\n- [#4314](https://github.com/bluesky-social/atproto/pull/4314) [`7c1429fe3`](https://github.com/bluesky-social/atproto/commit/7c1429fe36226d0d57e57c037ba4221d2fbd57ee) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add debug field to `PostView` and `ProfileView*` (see cdb6b27fc6be1e858476d8c55fd0c37561b972b4)\n\n## 0.17.5\n\n### Patch Changes\n\n- [#4279](https://github.com/bluesky-social/atproto/pull/4279) [`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031) Thanks [@foysalit](https://github.com/foysalit)! - Add strike system to ozone\n\n## 0.17.4\n\n### Patch Changes\n\n- [#4299](https://github.com/bluesky-social/atproto/pull/4299) [`a8e307ef4`](https://github.com/bluesky-social/atproto/commit/a8e307ef4851b164ee38bb5149343631e329f143) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Record types are now exported as both `.Record` (as they used to) and `.Main` (for consistency)\n\n## 0.17.3\n\n### Patch Changes\n\n- [#4268](https://github.com/bluesky-social/atproto/pull/4268) [`386f583cf`](https://github.com/bluesky-social/atproto/commit/386f583cffa2c596a12be4e98dde498f3b8670f6) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Bump threadgate `hiddenReplies` field `maxLength` to 300.\n\n## 0.17.2\n\n### Patch Changes\n\n- [#4262](https://github.com/bluesky-social/atproto/pull/4262) [`1cb5b9b80`](https://github.com/bluesky-social/atproto/commit/1cb5b9b80c20a054f7fbacd89d0d440dc2241d81) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Finalize report reason lexicons, update migration map in Ozone\n\n## 0.17.1\n\n### Patch Changes\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Create a dedicated type for the `proxy` property\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve validation when setting an `Agent`'s `proxy` property\n\n- [#4241](https://github.com/bluesky-social/atproto/pull/4241) [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd) Thanks [@foysalit](https://github.com/foysalit)! - Add scheduled action api to ozone\n\n## 0.17.0\n\n### Minor Changes\n\n- [#4238](https://github.com/bluesky-social/atproto/pull/4238) [`dba2d30e2`](https://github.com/bluesky-social/atproto/commit/dba2d30e2c4ce0eb624f2139b485719d14474940) Thanks [@gaearon](https://github.com/gaearon)! - Don't clear session in resumeSession on transient network errors\n\n### Patch Changes\n\n- [#4232](https://github.com/bluesky-social/atproto/pull/4232) [`7f38ee03c`](https://github.com/bluesky-social/atproto/commit/7f38ee03c01357686a4ce54cdf8eed4e37074a58) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add `pronouns` to `profileView` and `profileViewBasic`\n\n## 0.16.11\n\n### Patch Changes\n\n- [#4228](https://github.com/bluesky-social/atproto/pull/4228) [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add app.bsky.unspecced.getOnboardingSuggestedStarterPacks\n\n- [#4228](https://github.com/bluesky-social/atproto/pull/4228) [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton\n\n## 0.16.10\n\n### Patch Changes\n\n- [#4224](https://github.com/bluesky-social/atproto/pull/4224) [`8dc4caf55`](https://github.com/bluesky-social/atproto/commit/8dc4caf55840578c835b4c851d4a599c15627a78) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Add `pronouns` and `website` to `app.bsky.actor.profile`\n\n## 0.16.9\n\n### Patch Changes\n\n- [#4189](https://github.com/bluesky-social/atproto/pull/4189) [`ff30786af`](https://github.com/bluesky-social/atproto/commit/ff30786af6f72ad6506939bfca01a3f55a096c1c) Thanks [@foysalit](https://github.com/foysalit)! - Add revoke credentials moderation event type to lexicons\n\n## 0.16.8\n\n### Patch Changes\n\n- [#3881](https://github.com/bluesky-social/atproto/pull/3881) [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add expanded moderation report reasons as outlined in\n  [RFC-0009](https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity)\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/common-web@0.4.3\n  - @atproto/lexicon@0.5.1\n  - @atproto/xrpc@0.7.5\n\n## 0.16.7\n\n### Patch Changes\n\n- [#4164](https://github.com/bluesky-social/atproto/pull/4164) [`09717f29a`](https://github.com/bluesky-social/atproto/commit/09717f29ac7ca742c9c3310980dbe4d112b7597f) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add bookmarks lexicons\n\n## 0.16.6\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/xrpc@0.7.4\n\n## 0.16.5\n\n### Patch Changes\n\n- [#4142](https://github.com/bluesky-social/atproto/pull/4142) [`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - add com.atproto.temp.revokeAccountCredentials lexicon schema\n\n## 0.16.4\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/xrpc@0.7.3\n\n## 0.16.3\n\n### Patch Changes\n\n- [#4109](https://github.com/bluesky-social/atproto/pull/4109) [`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e) Thanks [@foysalit](https://github.com/foysalit)! - Add batchId filter to tools.ozone.moderation.queryEvents endpoint\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/lexicon@0.4.13\n  - @atproto/xrpc@0.7.2\n\n## 0.16.2\n\n### Patch Changes\n\n- [#4081](https://github.com/bluesky-social/atproto/pull/4081) [`c370d933b`](https://github.com/bluesky-social/atproto/commit/c370d933b76b4e15b83a82b40d1b6a32bd54add6) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Adds `purpose` filtering to `app.bsky.graph.getLists`.\n  Adds `app.bsky.graph.getListsWithMembership`.\n  Adds `app.bsky.graph.getStarterPacksWithMembership`.\n\n## 0.16.1\n\n### Patch Changes\n\n- [#3927](https://github.com/bluesky-social/atproto/pull/3927) [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef) Thanks [@foysalit](https://github.com/foysalit)! - Introduces ozone event timeline lexicons\n\n## 0.16.0\n\n### Minor Changes\n\n- [#4072](https://github.com/bluesky-social/atproto/pull/4072) [`9751eebd7`](https://github.com/bluesky-social/atproto/commit/9751eebd718066984a91046b63e410caecd64022) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Remove app.bsky.unspecced.checkHandleAvailability, add com.atproto.temp.checkHandleAvailability\n\n## 0.15.27\n\n### Patch Changes\n\n- [#4058](https://github.com/bluesky-social/atproto/pull/4058) [`8787fd9de`](https://github.com/bluesky-social/atproto/commit/8787fd9dea769716412c9883e355cd496664bc6e) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Only allow initiating age assurance flow from certain states, return `InvalidInitiation` error if violated.\n\n- [#4049](https://github.com/bluesky-social/atproto/pull/4049) [`dc84906c8`](https://github.com/bluesky-social/atproto/commit/dc84906c865e8a97939a909dd3f75decde538363) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - app.bsky.unspecced.checkHandleAvailability lexicon\n\n## 0.15.26\n\n### Patch Changes\n\n- [#4041](https://github.com/bluesky-social/atproto/pull/4041) [`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add `unregisterPush` API\n\n- [#4048](https://github.com/bluesky-social/atproto/pull/4048) [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e) Thanks [@foysalit](https://github.com/foysalit)! - Add externalId to ozone events for deduping events per subject and event type\n\n## 0.15.25\n\n### Patch Changes\n\n- [#4028](https://github.com/bluesky-social/atproto/pull/4028) [`88c136427`](https://github.com/bluesky-social/atproto/commit/88c136427451a20d21812a1aa88a70cf21904138) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Age assurance compliance\n\n## 0.15.24\n\n### Patch Changes\n\n- [#4034](https://github.com/bluesky-social/atproto/pull/4034) [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1) Thanks [@foysalit](https://github.com/foysalit)! - Add age assurance event types to ozone lexicons\n\n- Updated dependencies [[`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/lexicon@0.4.12\n  - @atproto/xrpc@0.7.1\n\n## 0.15.23\n\n### Patch Changes\n\n- [#3991](https://github.com/bluesky-social/atproto/pull/3991) [`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8) Thanks [@foysalit](https://github.com/foysalit)! - Add modTool parameter to ozone events\n\n## 0.15.22\n\n### Patch Changes\n\n- [#3945](https://github.com/bluesky-social/atproto/pull/3945) [`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b) Thanks [@foysalit](https://github.com/foysalit)! - Add safelink module in ozone\n\n## 0.15.21\n\n### Patch Changes\n\n- [#4010](https://github.com/bluesky-social/atproto/pull/4010) [`d344723a1`](https://github.com/bluesky-social/atproto/commit/d344723a1018b2436b5453526397936bd587a2e2) Thanks [@mozzius](https://github.com/mozzius)! - Loosen constraints for saved feed preferences\n\n## 0.15.20\n\n### Patch Changes\n\n- [#4005](https://github.com/bluesky-social/atproto/pull/4005) [`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7) Thanks [@mozzius](https://github.com/mozzius)! - add `subscribed-post` notification reason\n\n## 0.15.19\n\n### Patch Changes\n\n- [#3997](https://github.com/bluesky-social/atproto/pull/3997) [`376778a92`](https://github.com/bluesky-social/atproto/commit/376778a92f08fb6709c4cde736bfaca7393a72e1) Thanks [@mozzius](https://github.com/mozzius)! - Add put method for AppBskyNotificationDeclarationRecord\n\n## 0.15.18\n\n### Patch Changes\n\n- [#3995](https://github.com/bluesky-social/atproto/pull/3995) [`e3e31b2b9`](https://github.com/bluesky-social/atproto/commit/e3e31b2b9bf8c4de6b2d7fa992c3b3795686ea72) Thanks [@mozzius](https://github.com/mozzius)! - Add put method to record utility classes\n\n## 0.15.17\n\n### Patch Changes\n\n- [#3990](https://github.com/bluesky-social/atproto/pull/3990) [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add activity subscription lexicons\n\n## 0.15.16\n\n### Patch Changes\n\n- [#3966](https://github.com/bluesky-social/atproto/pull/3966) [`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef) Thanks [@mozzius](https://github.com/mozzius)! - Rename notification preference lexicon \"filter\" key to \"include\"\n\n## 0.15.15\n\n### Patch Changes\n\n- [#2934](https://github.com/bluesky-social/atproto/pull/2934) [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix bug where fuzzy matching mute words was over-zealous e.g. `Andor` matching `and/or`.\n\n- [#2934](https://github.com/bluesky-social/atproto/pull/2934) [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Updates mute word matching to include a `matches: MuteWordMatch[]` property on the `muted-word` `cause` type returned as part of a `ModerationDecision`.\n\n## 0.15.14\n\n### Patch Changes\n\n- [#3901](https://github.com/bluesky-social/atproto/pull/3901) [`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1) Thanks [@mozzius](https://github.com/mozzius)! - Add notification preferences V2 lexicons\n\n## 0.15.13\n\n### Patch Changes\n\n- [#3929](https://github.com/bluesky-social/atproto/pull/3929) [`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Rename `getPostThreadHiddenV2` to `getPostThreadOtherV2` to better reflect the intent of the API.\n\n## 0.15.12\n\n### Patch Changes\n\n- [#3912](https://github.com/bluesky-social/atproto/pull/3912) [`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Unify `getPostThreadV2` and `getPostThreadHiddenV2` responses under `app.bsky.unspecced.defs` namespace and a single interface via `threadItemPost`.\n\n## 0.15.11\n\n### Patch Changes\n\n- [#3910](https://github.com/bluesky-social/atproto/pull/3910) [`a978681fd`](https://github.com/bluesky-social/atproto/commit/a978681fde1c138a5298bae77e5dc36ce155f955) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Updates to app.bsky.unspecced.getPostThreadHiddenV2 done in f6d5a467e71fb54996754cce7747b1e98a34442b (https://github.com/bluesky-social/atproto/pull/3909)\n\n## 0.15.10\n\n### Patch Changes\n\n- [#3825](https://github.com/bluesky-social/atproto/pull/3825) [`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add app.bsky.unspecced.getPostThreadV2\n\n- [#3825](https://github.com/bluesky-social/atproto/pull/3825) [`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add app.bsky.unspecced.getPostThreadHiddenV2\n\n## 0.15.9\n\n### Patch Changes\n\n- [#3882](https://github.com/bluesky-social/atproto/pull/3882) [`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159) Thanks [@mozzius](https://github.com/mozzius)! - add a \"via\" field to reposts and likes allowing a reference a repost, and then give a notification when a repost is liked or reposted.\n\n## 0.15.8\n\n### Patch Changes\n\n- [#3869](https://github.com/bluesky-social/atproto/pull/3869) [`80f402f36`](https://github.com/bluesky-social/atproto/commit/80f402f3663af08fd048300738d04c67aa2b9cb8) Thanks [@haileyok](https://github.com/haileyok)! - add `reqId` to feed interactions\n\n## 0.15.7\n\n### Patch Changes\n\n- [#3860](https://github.com/bluesky-social/atproto/pull/3860) [`86b315388`](https://github.com/bluesky-social/atproto/commit/86b3153884099ceeb0cfdb9d2bfdd447c39fb35a) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add liveNow to app.bsky.unspecced.getConfig\n\n## 0.15.6\n\n### Patch Changes\n\n- [#3824](https://github.com/bluesky-social/atproto/pull/3824) [`3a65b68f7`](https://github.com/bluesky-social/atproto/commit/3a65b68f7dc63c8bfbea0ae615f8ae984272f2e4) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add app.bsky.actor.status lexicon\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/common-web@0.4.2\n  - @atproto/xrpc@0.7.0\n  - @atproto/lexicon@0.4.11\n\n## 0.15.5\n\n### Patch Changes\n\n- [#3765](https://github.com/bluesky-social/atproto/pull/3765) [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9) Thanks [@foysalit](https://github.com/foysalit)! - Add verification lexicons to ozone\n\n## 0.15.4\n\n### Patch Changes\n\n- [#3768](https://github.com/bluesky-social/atproto/pull/3768) [`7af77f3ed`](https://github.com/bluesky-social/atproto/commit/7af77f3edfe52f77729f61de4188e8375f03b4ef) Thanks [@devinivy](https://github.com/devinivy)! - Support tools.ozone.hosting.getAccountHistory\n\n## 0.15.3\n\n### Patch Changes\n\n- [#3773](https://github.com/bluesky-social/atproto/pull/3773) [`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add verification notifications\n\n## 0.15.2\n\n### Patch Changes\n\n- [#3770](https://github.com/bluesky-social/atproto/pull/3770) [`553c988f1`](https://github.com/bluesky-social/atproto/commit/553c988f1d226b3d2fbe94c117b088f5c82db794) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `verificationPrefs` and `hideBadges` setting to user prefs.\n\n## 0.15.1\n\n### Patch Changes\n\n- [#3761](https://github.com/bluesky-social/atproto/pull/3761) [`688268b6a`](https://github.com/bluesky-social/atproto/commit/688268b6a5ee30f0922ee152ffbd26583d164ae4) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add verification state to profile view lexicons\n\n- [#3766](https://github.com/bluesky-social/atproto/pull/3766) [`8d99915ce`](https://github.com/bluesky-social/atproto/commit/8d99915ce02c73b9b37bf121ccd2703fa14a906a) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update chat verification lexicon\n\n## 0.15.0\n\n### Minor Changes\n\n- [#3715](https://github.com/bluesky-social/atproto/pull/3715) [`23462184d`](https://github.com/bluesky-social/atproto/commit/23462184dc941ba2fc3b4d054985a53715585020) Thanks [@knotbin](https://github.com/knotbin)! - run codegen for changes in lex-cli\n\n## 0.14.22\n\n### Patch Changes\n\n- [#3714](https://github.com/bluesky-social/atproto/pull/3714) [`fc61662d7`](https://github.com/bluesky-social/atproto/commit/fc61662d7b88597f78383e37ee54264a8bb4b670) Thanks [@bnewbold](https://github.com/bnewbold)! - new lexicons: listHosts and getHostStatus endpoints under com.atproto.sync\n\n- [#3741](https://github.com/bluesky-social/atproto/pull/3741) [`ca07871c4`](https://github.com/bluesky-social/atproto/commit/ca07871c487abc99fe7b7f8671aa8d98eb5dc4bb) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update reaction limit on chat lexicon\n\n## 0.14.21\n\n### Patch Changes\n\n- [#3724](https://github.com/bluesky-social/atproto/pull/3724) [`8b7bf7e8f`](https://github.com/bluesky-social/atproto/commit/8b7bf7e8f0e5447c68633a87a2a3cff99f9e7e1c) Thanks [@haileyok](https://github.com/haileyok)! - Return `ProfileView` from `getSuggestedUsers` unspecced endpoint\n\n## 0.14.20\n\n### Patch Changes\n\n- [#3713](https://github.com/bluesky-social/atproto/pull/3713) [`0e681d303`](https://github.com/bluesky-social/atproto/commit/0e681d3036fd0b35c6d2198638392051b2ce4c81) Thanks [@haileyok](https://github.com/haileyok)! - add `getSuggestedUsers` and `getSuggestedUsersSkeleton`\n\n## 0.14.19\n\n### Patch Changes\n\n- [#3680](https://github.com/bluesky-social/atproto/pull/3680) [`efb302db1`](https://github.com/bluesky-social/atproto/commit/efb302db1a615b68795c725a22489dbd0400e011) Thanks [@haileyok](https://github.com/haileyok)! - Add unspecced `getSuggestedFeeds` and associated types\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common-web@0.4.1\n  - @atproto/lexicon@0.4.10\n  - @atproto/xrpc@0.6.12\n\n## 0.14.18\n\n### Patch Changes\n\n- [#3706](https://github.com/bluesky-social/atproto/pull/3706) [`04b6230cd`](https://github.com/bluesky-social/atproto/commit/04b6230cd2fbfe4a06cb00ab8ccb8e6c87c6c546) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Return `StarterPackView` instead of `StarterPackViewBasic` from `getSuggestedStarterPacks`\n\n## 0.14.17\n\n### Patch Changes\n\n- [#2519](https://github.com/bluesky-social/atproto/pull/2519) [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f) Thanks [@dholms](https://github.com/dholms)! - Add com.atproto.admin.updateAccountSigningKey\n\n- [#3673](https://github.com/bluesky-social/atproto/pull/3673) [`2b7efb6cb`](https://github.com/bluesky-social/atproto/commit/2b7efb6cb1c93a108570efdafe9d9ec3f1018dfa) Thanks [@haileyok](https://github.com/haileyok)! - Add `getTrends`, `getTrendsSkeleton`, and associated types\n\n- [#3705](https://github.com/bluesky-social/atproto/pull/3705) [`b0a0f1484`](https://github.com/bluesky-social/atproto/commit/b0a0f1484378adeb5e2aa20b9b6ff2c2eca0f740) Thanks [@dholms](https://github.com/dholms)! - Fix codegent for com.atproto.admin.updateAccountSigningKey\n\n- [#3677](https://github.com/bluesky-social/atproto/pull/3677) [`0eea698be`](https://github.com/bluesky-social/atproto/commit/0eea698bef76520ae4cc0e1f2efbb588a0459556) Thanks [@haileyok](https://github.com/haileyok)! - Add `getSuggestedStarterPacks`, `getSuggestedStarterPacksSkeleton`, and associated types\n\n## 0.14.16\n\n### Patch Changes\n\n- [#3695](https://github.com/bluesky-social/atproto/pull/3695) [`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Fix last reaction lexicon\n\n## 0.14.15\n\n### Patch Changes\n\n- [#3692](https://github.com/bluesky-social/atproto/pull/3692) [`b4ab5011b`](https://github.com/bluesky-social/atproto/commit/b4ab5011bcc64f9f05122a8773806af8e0c13146) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Support more ways to instantiate an `Agent`\n\n## 0.14.14\n\n### Patch Changes\n\n- [#3685](https://github.com/bluesky-social/atproto/pull/3685) [`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update chat reaction lexicon\n\n## 0.14.13\n\n### Patch Changes\n\n- [#3651](https://github.com/bluesky-social/atproto/pull/3651) [`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd) Thanks [@foysalit](https://github.com/foysalit)! - Add getSubjects endpoint to ozone for fetching detailed view of multiple subjects\n\n## 0.14.12\n\n### Patch Changes\n\n- [#3674](https://github.com/bluesky-social/atproto/pull/3674) [`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0) Thanks [@mozzius](https://github.com/mozzius)! - run codegen for changes in chat lexicon\n\n## 0.14.11\n\n### Patch Changes\n\n- [#3670](https://github.com/bluesky-social/atproto/pull/3670) [`d87ffc7bf`](https://github.com/bluesky-social/atproto/commit/d87ffc7bfe3c1e792dc84a320544eb2e053d61ce) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add DM reactions lexicons\n\n## 0.14.10\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n  - @atproto/lexicon@0.4.9\n  - @atproto/xrpc@0.6.11\n\n## 0.14.9\n\n### Patch Changes\n\n- [#3587](https://github.com/bluesky-social/atproto/pull/3587) [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952) Thanks [@foysalit](https://github.com/foysalit)! - Add searchable handle and displayName to ozone team members\n\n## 0.14.8\n\n### Patch Changes\n\n- [#3585](https://github.com/bluesky-social/atproto/pull/3585) [`38320191e`](https://github.com/bluesky-social/atproto/commit/38320191e559f8b928c6e951a9b4a6207240bfc1) Thanks [@dholms](https://github.com/dholms)! - Wrap sync v1.1 semantics. Add #sync event to subscribeRepos and deprecate #handle and #tombstone events\n\n- [#3602](https://github.com/bluesky-social/atproto/pull/3602) [`6bcbb6d8c`](https://github.com/bluesky-social/atproto/commit/6bcbb6d8cd3696280935ff7892d8e191fd21fa49) Thanks [@devinivy](https://github.com/devinivy)! - Permit 100mb video embeds.\n\n- [#2264](https://github.com/bluesky-social/atproto/pull/2264) [`dc6e4ecb0`](https://github.com/bluesky-social/atproto/commit/dc6e4ecb0e09bbf4bc7a79c6ac43fb6da4166200) Thanks [@bnewbold](https://github.com/bnewbold)! - new com.atproto.identity endpoints: resolveDid, resolveIdentity, refreshIdentity\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/syntax@0.3.4\n  - @atproto/lexicon@0.4.8\n  - @atproto/xrpc@0.6.10\n\n## 0.14.7\n\n### Patch Changes\n\n- [#3521](https://github.com/bluesky-social/atproto/pull/3521) [`99e2809ca`](https://github.com/bluesky-social/atproto/commit/99e2809ca2ebf70acaa10254f140a8dd0fad4305) Thanks [@bnewbold](https://github.com/bnewbold)! - Add `reasonTypes`, `subjectTypes`, and `subjectCollections` properties to labeler service records.\n\n- [#2506](https://github.com/bluesky-social/atproto/pull/2506) [`27b0a7be1`](https://github.com/bluesky-social/atproto/commit/27b0a7be1ed1b6e098114791d84ec9dc844db552) Thanks [@bnewbold](https://github.com/bnewbold)! - remove some deprecated fields from com.atproto Lexicons\n\n- [#3579](https://github.com/bluesky-social/atproto/pull/3579) [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Mirror new labeler service record properties on `labelerViewDetailed`.\n\n## 0.14.6\n\n### Patch Changes\n\n- [#3576](https://github.com/bluesky-social/atproto/pull/3576) [`44f81f2eb`](https://github.com/bluesky-social/atproto/commit/44f81f2eb9229e21aec4472b3a05e855396dbec5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add chat.bsky.convo.updateAllRead lex\n\n## 0.14.5\n\n### Patch Changes\n\n- [#3449](https://github.com/bluesky-social/atproto/pull/3449) [`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f) Thanks [@dholms](https://github.com/dholms)! - Updated subscribeRepo to include prev CIDs for operations and covering proofs for all ops.\n\n- [#3572](https://github.com/bluesky-social/atproto/pull/3572) [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a) Thanks [@foysalit](https://github.com/foysalit)! - Make comment property optional on ozone comment event\n\n- [#3391](https://github.com/bluesky-social/atproto/pull/3391) [`6e382f67a`](https://github.com/bluesky-social/atproto/commit/6e382f67aa73532efadfea80ff96a27b526cb178) Thanks [@bnewbold](https://github.com/bnewbold)! - update sync lexicons for induction firehose\n\n## 0.14.4\n\n### Patch Changes\n\n- [#3561](https://github.com/bluesky-social/atproto/pull/3561) [`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c) Thanks [@foysalit](https://github.com/foysalit)! - Add Divert event to the list of allowed ozone events\n\n## 0.14.3\n\n### Patch Changes\n\n- [#3564](https://github.com/bluesky-social/atproto/pull/3564) [`22af31a89`](https://github.com/bluesky-social/atproto/commit/22af31a898476c5e317aea263af366bddda120d6) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update chat lexicons\n\n- [#2378](https://github.com/bluesky-social/atproto/pull/2378) [`01874c4be`](https://github.com/bluesky-social/atproto/commit/01874c4be73a41ffb8fe28378f674949aa2c938f) Thanks [@bnewbold](https://github.com/bnewbold)! - set 'tid' and 'record-key' formats on com.atproto.sync and com.atproto.repo lexicons\n\n## 0.14.2\n\n### Patch Changes\n\n- [#3524](https://github.com/bluesky-social/atproto/pull/3524) [`010f10c6f`](https://github.com/bluesky-social/atproto/commit/010f10c6f212f699ad42c0349a58bbcf2172e3cc) Thanks [@bnewbold](https://github.com/bnewbold)! - add com.atproto.sync.listReposByCollection Lexicon\n\n- [#3546](https://github.com/bluesky-social/atproto/pull/3546) [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9) Thanks [@foysalit](https://github.com/foysalit)! - Add reporter stats endpoint on ozone service\n\n## 0.14.1\n\n### Patch Changes\n\n- [#3535](https://github.com/bluesky-social/atproto/pull/3535) [`ba5bb6e66`](https://github.com/bluesky-social/atproto/commit/ba5bb6e667fb58bbefd332844957de575e102ca3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix bug preventing \"logout()\" calls from working.\n\n## 0.14.0\n\n### Minor Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update Lexicon derived code to better reflect data typings. In particular, Lexicon derived interfaces will now explicitly include the `$type` property that can be present in the data.\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Helper functions (e.g. `NS.isRecord`) no longer casts the output value. Use `asPredicate(NS.validateRecord)` to create a predicate function that will ensure that an unknown value is indeed an `NS.Record`. The `isX` helper function's purpose is to discriminate between `$type`d values from unions.\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fixes a bug that would clear interests prefs when updating hidden posts\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n  - @atproto/xrpc@0.6.9\n\n## 0.13.35\n\n### Patch Changes\n\n- [#3495](https://github.com/bluesky-social/atproto/pull/3495) [`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153) Thanks [@foysalit](https://github.com/foysalit)! - Add a priority score to ozone subjects\n\n## 0.13.34\n\n### Patch Changes\n\n- [#3496](https://github.com/bluesky-social/atproto/pull/3496) [`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add followerRule threadgate\n\n- [#3501](https://github.com/bluesky-social/atproto/pull/3501) [`636951e47`](https://github.com/bluesky-social/atproto/commit/636951e4728cd52c2e5355eb93b47d7e869b67e9) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Include `followerRule` as valid setting in `postInteractionSettings` pref\n\n## 0.13.33\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3494](https://github.com/bluesky-social/atproto/pull/3494) [`87ed907a6`](https://github.com/bluesky-social/atproto/commit/87ed907a6b96b408c02c9af819cec8380a453254) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `setPostInteractionSettings` for configuring default interaction settings for creation of posts\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39)]:\n  - @atproto/common-web@0.4.0\n  - @atproto/lexicon@0.4.6\n  - @atproto/syntax@0.3.2\n  - @atproto/xrpc@0.6.8\n\n## 0.13.32\n\n### Patch Changes\n\n- [#3352](https://github.com/bluesky-social/atproto/pull/3352) [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905) Thanks [@foysalit](https://github.com/foysalit)! - Auto resolve appeals when taking down\n\n- Updated dependencies [[`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9)]:\n  - @atproto/xrpc@0.6.7\n\n## 0.13.31\n\n### Patch Changes\n\n- [#3441](https://github.com/bluesky-social/atproto/pull/3441) [`8c6c7813a`](https://github.com/bluesky-social/atproto/commit/8c6c7813a9c2110c8fe21acdca8f09554a1983ce) Thanks [@mozzius](https://github.com/mozzius)! - Allow passing `allowTakendown` to createSession\n\n## 0.13.30\n\n### Patch Changes\n\n- [#3429](https://github.com/bluesky-social/atproto/pull/3429) [`e6e6aea38`](https://github.com/bluesky-social/atproto/commit/e6e6aea3814e3d0bb42a537f80d77947e85fa73f) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - add feedViewPost.threadContext defs\n\n- [#3390](https://github.com/bluesky-social/atproto/pull/3390) [`c0a75d310`](https://github.com/bluesky-social/atproto/commit/c0a75d310aa92c067799a97d1acc5bd0543114c5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - posts_with_video filter in getAuthorFeed\n\n## 0.13.29\n\n### Patch Changes\n\n- [#3416](https://github.com/bluesky-social/atproto/pull/3416) [`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `tools.ozone.moderation.queryStatuses` lexicon\n\n## 0.13.28\n\n### Patch Changes\n\n- [#3389](https://github.com/bluesky-social/atproto/pull/3389) [`cbf17066f`](https://github.com/bluesky-social/atproto/commit/cbf17066f314fbc7f2e943127ee4a9f589f8bec2) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - add feedgen content mode lexicon spec\n\n## 0.13.27\n\n### Patch Changes\n\n- [#3364](https://github.com/bluesky-social/atproto/pull/3364) [`e277158f7`](https://github.com/bluesky-social/atproto/commit/e277158f70a831b04fde3ec84b3c1eaa6ce82e9d) Thanks [@iwsmith](https://github.com/iwsmith)! - add recId to getSuggestions\n\n## 0.13.26\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/common-web@0.3.2\n  - @atproto/lexicon@0.4.5\n  - @atproto/xrpc@0.6.6\n\n## 0.13.25\n\n### Patch Changes\n\n- [#3271](https://github.com/bluesky-social/atproto/pull/3271) [`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a) Thanks [@foysalit](https://github.com/foysalit)! - Allow setting policy names with takedown actions and when querying events\n\n## 0.13.24\n\n### Patch Changes\n\n- [#3294](https://github.com/bluesky-social/atproto/pull/3294) [`d90d999de`](https://github.com/bluesky-social/atproto/commit/d90d999defda01a9b04dbce129e254990062c283) Thanks [@foysalit](https://github.com/foysalit)! - Limit tags filter to 25 max and remove 25 char limit for tag item\n\n## 0.13.23\n\n### Patch Changes\n\n- [#3251](https://github.com/bluesky-social/atproto/pull/3251) [`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7) Thanks [@foysalit](https://github.com/foysalit)! - Allow createSession to request takendown account scope\n\n- [#3280](https://github.com/bluesky-social/atproto/pull/3280) [`9ea2cce9a`](https://github.com/bluesky-social/atproto/commit/9ea2cce9a4c0a08994a8cb5abc81dc4bc2221d0c) Thanks [@foysalit](https://github.com/foysalit)! - Apply ozone queue splitting at the database query level\n\n## 0.13.22\n\n### Patch Changes\n\n- [#3270](https://github.com/bluesky-social/atproto/pull/3270) [`f22383cee`](https://github.com/bluesky-social/atproto/commit/f22383cee8feb8b9f761c801ab6e07ad8dc019ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add support for label def aliases, deprecation notices. This provides support for the deprecated `gore` label until a full cleanup effort can be completed.\n\n## 0.13.21\n\n### Patch Changes\n\n- [#3250](https://github.com/bluesky-social/atproto/pull/3250) [`dced566de`](https://github.com/bluesky-social/atproto/commit/dced566de5079ef4208801db476a7e7416f5e5aa) Thanks [@haileyok](https://github.com/haileyok)! - add trending topics\n\n## 0.13.20\n\n### Patch Changes\n\n- [#3222](https://github.com/bluesky-social/atproto/pull/3222) [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34) Thanks [@gaearon](https://github.com/gaearon)! - Add optional reasons param to listNotifications\n\n- Updated dependencies [[`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95)]:\n  - @atproto/lexicon@0.4.4\n  - @atproto/xrpc@0.6.5\n\n## 0.13.19\n\n### Patch Changes\n\n- [#3171](https://github.com/bluesky-social/atproto/pull/3171) [`ed2236220`](https://github.com/bluesky-social/atproto/commit/ed2236220900ab9a6132c525289cfdd959733a42) Thanks [@foysalit](https://github.com/foysalit)! - Allow moderators to optionally acknowledge all open subjects of an account when acknowledging account level reports\n\n## 0.13.18\n\n### Patch Changes\n\n- [#3082](https://github.com/bluesky-social/atproto/pull/3082) [`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc) Thanks [@gaearon](https://github.com/gaearon)! - Add hotness as a thread sorting option\n\n## 0.13.17\n\n### Patch Changes\n\n- [#2978](https://github.com/bluesky-social/atproto/pull/2978) [`a4b528e5f`](https://github.com/bluesky-social/atproto/commit/a4b528e5f51c8bfca56b293b0059b88d138ec421) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add searchStarterPacks and searchStarterPacksSkeleton\n\n- [#3056](https://github.com/bluesky-social/atproto/pull/3056) [`2e7aa211d`](https://github.com/bluesky-social/atproto/commit/2e7aa211d2cbc629899c7f87f1713b13b932750b) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add com.atproto.temp.addReservedHandle lexicon\n\n## 0.13.16\n\n### Patch Changes\n\n- [#2988](https://github.com/bluesky-social/atproto/pull/2988) [`48d08a469`](https://github.com/bluesky-social/atproto/commit/48d08a469f75837e3b7e879d286d12780440b8b8) Thanks [@foysalit](https://github.com/foysalit)! - Make durationInHours optional for mute reporter event\n\n- [#2911](https://github.com/bluesky-social/atproto/pull/2911) [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export the generated lexicons `schemas` definitions\n\n- [#2953](https://github.com/bluesky-social/atproto/pull/2953) [`561431fe4`](https://github.com/bluesky-social/atproto/commit/561431fe4897e81767dc768e9a31020d09bf86ff) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add convoView.opened to lexicon definition\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/syntax@0.3.1\n  - @atproto/lexicon@0.4.3\n  - @atproto/xrpc@0.6.4\n\n## 0.13.15\n\n### Patch Changes\n\n- [#2661](https://github.com/bluesky-social/atproto/pull/2661) [`d6f33b474`](https://github.com/bluesky-social/atproto/commit/d6f33b4742e0b94722a993efc7d18833d9416bb6) Thanks [@foysalit](https://github.com/foysalit)! - Add mod events and status filter for account and record hosting status\n\n- [#2957](https://github.com/bluesky-social/atproto/pull/2957) [`b6eeb81c6`](https://github.com/bluesky-social/atproto/commit/b6eeb81c6d454b5ae91b05a21fc1820274c1b429) Thanks [@gaearon](https://github.com/gaearon)! - Detect facets in parallel\n\n- [#2917](https://github.com/bluesky-social/atproto/pull/2917) [`839202a3d`](https://github.com/bluesky-social/atproto/commit/839202a3d2b01de25de900cec7540019545798c6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow instantiating an API Agent with a string or URL\n\n- [#2933](https://github.com/bluesky-social/atproto/pull/2933) [`e680d55ca`](https://github.com/bluesky-social/atproto/commit/e680d55ca2d7f6b213e2a8693eba6be39163ba41) Thanks [@mozzius](https://github.com/mozzius)! - Fix handling of invalid facets in RichText\n\n- [#2905](https://github.com/bluesky-social/atproto/pull/2905) [`c4b5e5395`](https://github.com/bluesky-social/atproto/commit/c4b5e53957463c37dd16fdd1b897d4ab02ab8e84) Thanks [@foysalit](https://github.com/foysalit)! - Add user specific and instance-wide settings api for ozone\n\n## 0.13.14\n\n### Patch Changes\n\n- [#2918](https://github.com/bluesky-social/atproto/pull/2918) [`209238769`](https://github.com/bluesky-social/atproto/commit/209238769c0bf38bf04f7fa9621eeb176b5c0ed8) Thanks [@devinivy](https://github.com/devinivy)! - add app.bsky.unspecced.getConfig endpoint\n\n- [#2931](https://github.com/bluesky-social/atproto/pull/2931) [`73f40e63a`](https://github.com/bluesky-social/atproto/commit/73f40e63abe3283efc0a27eef781c00b497caad1) Thanks [@dholms](https://github.com/dholms)! - Add threatSignatures to ozone repo views\n\n## 0.13.13\n\n### Patch Changes\n\n- [#2914](https://github.com/bluesky-social/atproto/pull/2914) [`19e36afb2`](https://github.com/bluesky-social/atproto/commit/19e36afb2c13dbc7b1033eb3cab5e7fc6f496fdc) Thanks [@foysalit](https://github.com/foysalit)! - Add collections and subjectType filters to ozone's queryEvents and queryStatuses endpoints\n\n## 0.13.12\n\n### Patch Changes\n\n- [#2636](https://github.com/bluesky-social/atproto/pull/2636) [`22d039a22`](https://github.com/bluesky-social/atproto/commit/22d039a229e3ef08a793e1c98b473b1b8e18ac5e) Thanks [@foysalit](https://github.com/foysalit)! - Sets api to manage lists of strings on ozone, mostly aimed for automod configuration\n\n## 0.13.11\n\n### Patch Changes\n\n- [#2857](https://github.com/bluesky-social/atproto/pull/2857) [`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds support for muting words within link cards attached to `RecordWithMedia` embeds.\n\n## 0.13.10\n\n### Patch Changes\n\n- [#2855](https://github.com/bluesky-social/atproto/pull/2855) [`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5) Thanks [@dholms](https://github.com/dholms)! - Add tools.ozone.signature lexicons\n\n## 0.13.9\n\n### Patch Changes\n\n- [#2836](https://github.com/bluesky-social/atproto/pull/2836) [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c) Thanks [@foysalit](https://github.com/foysalit)! - Add getRepos and getRecords endpoints for bulk fetching\n\n## 0.13.8\n\n### Patch Changes\n\n- [#2771](https://github.com/bluesky-social/atproto/pull/2771) [`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93) Thanks [@mozzius](https://github.com/mozzius)! - Add pinned posts to profile record and getAuthorFeed\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94)]:\n  - @atproto/xrpc@0.6.3\n  - @atproto/common-web@0.3.1\n  - @atproto/lexicon@0.4.2\n\n## 0.13.7\n\n### Patch Changes\n\n- [#2807](https://github.com/bluesky-social/atproto/pull/2807) [`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e) Thanks [@foysalit](https://github.com/foysalit)! - Introduce a acknowledgeAccountSubjects flag on takedown event to ack all subjects from the author that need review\n\n- [#2810](https://github.com/bluesky-social/atproto/pull/2810) [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add NUX API\n\n- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/xrpc@0.6.2\n\n## 0.13.6\n\n### Patch Changes\n\n- [#2780](https://github.com/bluesky-social/atproto/pull/2780) [`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d) Thanks [@foysalit](https://github.com/foysalit)! - Add language property to communication templates\n\n## 0.13.5\n\n### Patch Changes\n\n- [#2751](https://github.com/bluesky-social/atproto/pull/2751) [`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14) Thanks [@devinivy](https://github.com/devinivy)! - Lexicons and support for video embeds within bsky posts.\n\n## 0.13.4\n\n### Patch Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Drop use of `AtpBaseClient` class\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose the `CredentialSession` class that can be used to instantiate both `Agent` and `XrpcClient`, while internally managing credential based (username/password) sessions.\n\n- [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate Agent.accountDid in favor of Agent.assertDid\n\n- [#2737](https://github.com/bluesky-social/atproto/pull/2737) [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `threadgate: ThreadgateView` to response from `getPostThread`\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `Agent` is no longer an abstract class. Instead it can be instantiated using object implementing a new `SessionManager` interface. If your project extends `Agent` and overrides the constructor or any method implementations, consider that you may want to call them from `super`.\n\n- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/xrpc@0.6.1\n\n## 0.13.3\n\n### Patch Changes\n\n- [#2735](https://github.com/bluesky-social/atproto/pull/2735) [`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7) Thanks [@haileyok](https://github.com/haileyok)! - add `quoteCount` to embed view\n\n## 0.13.2\n\n### Patch Changes\n\n- [#2658](https://github.com/bluesky-social/atproto/pull/2658) [`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb) Thanks [@haileyok](https://github.com/haileyok)! - Adds `app.bsky.feed.getQuotes` lexicon and handlers\n\n- [#2675](https://github.com/bluesky-social/atproto/pull/2675) [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `postgate` records to power quote gating and detached quote posts, plus `hiddenReplies` to the `threadgate` record.\n\n## 0.13.1\n\n### Patch Changes\n\n- [#2708](https://github.com/bluesky-social/atproto/pull/2708) [`22af354a5`](https://github.com/bluesky-social/atproto/commit/22af354a5db595d7cbc0e65f02601de3565337e1) Thanks [@devinivy](https://github.com/devinivy)! - Export AtpAgentOptions type to better support extending AtpAgent.\n\n## 0.13.0\n\n### Minor Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)!\n\n  #### Motivation\n\n  The motivation for these changes is the need to make the `@atproto/api` package\n  compatible with OAuth session management. We don't have OAuth client support\n  \"launched\" and documented quite yet, so you can keep using the current app\n  password authentication system. When we do \"launch\" OAuth support and begin\n  encouraging its usage in the near future (see the [OAuth\n  Roadmap](https://github.com/bluesky-social/atproto/discussions/2656)), these\n  changes will make it easier to migrate.\n\n  In addition, the redesigned session management system fixes a bug that could\n  cause the session data to become invalid when Agent clones are created (e.g.\n  using `agent.withProxy()`).\n\n  #### New Features\n\n  We've restructured the `XrpcClient` HTTP fetch handler to be specified during\n  the instantiation of the XRPC client, through the constructor, instead of using\n  a default implementation (which was statically defined).\n\n  With this refactor, the XRPC client is now more modular and reusable. Session\n  management, retries, cryptographic signing, and other request-specific logic can\n  be implemented in the fetch handler itself rather than by the calling code.\n\n  A new abstract class named `Agent`, has been added to `@atproto/api`. This class\n  will be the base class for all Bluesky agents classes in the `@atproto`\n  ecosystem. It is meant to be extended by implementations that provide session\n  management and fetch handling.\n\n  As you adapt your code to these changes, make sure to use the `Agent` type\n  wherever you expect to receive an agent, and use the `AtpAgent` type (class)\n  only to instantiate your client. The reason for this is to be forward compatible\n  with the OAuth agent implementation that will also extend `Agent`, and not\n  `AtpAgent`.\n\n  ```ts\n  import { Agent, AtpAgent } from '@atproto/api'\n\n  async function setupAgent(\n    service: string,\n    username: string,\n    password: string,\n  ): Promise<Agent> {\n    const agent = new AtpAgent({\n      service,\n      persistSession: (evt, session) => {\n        // handle session update\n      },\n    })\n\n    await agent.login(username, password)\n\n    return agent\n  }\n  ```\n\n  ```ts\n  import { Agent } from '@atproto/api'\n\n  async function doStuffWithAgent(agent: Agent, arg: string) {\n    return agent.resolveHandle(arg)\n  }\n  ```\n\n  ```ts\n  import { Agent, AtpAgent } from '@atproto/api'\n\n  class MyClass {\n    agent: Agent\n\n    constructor() {\n      this.agent = new AtpAgent()\n    }\n  }\n  ```\n\n  #### Breaking changes\n\n  Most of the changes introduced in this version are backward-compatible. However,\n  there are a couple of breaking changes you should be aware of:\n\n  - Customizing `fetch`: The ability to customize the `fetch: FetchHandler`\n    property of `@atproto/xrpc`'s `Client` and `@atproto/api`'s `AtpAgent` classes\n    has been removed. Previously, the `fetch` property could be set to a function\n    that would be used as the fetch handler for that instance, and was initialized\n    to a default fetch handler. That property is still accessible in a read-only\n    fashion through the `fetchHandler` property and can only be set during the\n    instance creation. Attempting to set/get the `fetch` property will now result\n    in an error.\n  - The `fetch()` method, as well as WhatWG compliant `Request` and `Headers`\n    constructors, must be globally available in your environment. Use a polyfill\n    if necessary.\n  - The `AtpBaseClient` has been removed. The `AtpServiceClient` has been renamed\n    `AtpBaseClient`. Any code using either of these classes will need to be\n    updated.\n  - Instead of _wrapping_ an `XrpcClient` in its `xrpc` property, the\n    `AtpBaseClient` (formerly `AtpServiceClient`) class - created through\n    `lex-cli` - now _extends_ the `XrpcClient` class. This means that a client\n    instance now passes the `instanceof XrpcClient` check. The `xrpc` property now\n    returns the instance itself and has been deprecated.\n  - `setSessionPersistHandler` is no longer available on the `AtpAgent` or\n    `BskyAgent` classes. The session handler can only be set though the\n    `persistSession` options of the `AtpAgent` constructor.\n  - The new class hierarchy is as follows:\n    - `BskyAgent` extends `AtpAgent`: but add no functionality (hence its\n      deprecation).\n    - `AtpAgent` extends `Agent`: adds password based session management.\n    - `Agent` extends `AtpBaseClient`: this abstract class that adds syntactic sugar\n      methods `app.bsky` lexicons. It also adds abstract session management\n      methods and adds atproto specific utilities\n      (`labelers` & `proxy` headers, cloning capability)\n    - `AtpBaseClient` extends `XrpcClient`: automatically code that adds fully\n      typed lexicon defined namespaces (`instance.app.bsky.feed.getPosts()`) to\n      the `XrpcClient`.\n    - `XrpcClient` is the base class.\n\n  #### Non-breaking changes\n\n  - The `com.*` and `app.*` namespaces have been made directly available to every\n    `Agent` instances.\n\n  #### Deprecations\n\n  - The default export of the `@atproto/xrpc` package has been deprecated. Use\n    named exports instead.\n  - The `Client` and `ServiceClient` classes are now deprecated. They are replaced by a single `XrpcClient` class.\n  - The default export of the `@atproto/api` package has been deprecated. Use\n    named exports instead.\n  - The `BskyAgent` has been deprecated. Use the `AtpAgent` class instead.\n  - The `xrpc` property of the `AtpClient` instances has been deprecated. The\n    instance itself should be used as the XRPC client.\n  - The `api` property of the `AtpAgent` and `BskyAgent` instances has been\n    deprecated. Use the instance itself instead.\n\n  #### Migration\n\n  ##### The `@atproto/api` package\n\n  If you were relying on the `AtpBaseClient` solely to perform validation, use\n  this:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { AtpBaseClient, ComAtprotoSyncSubscribeRepos } from '@atproto/api'\n\n  const baseClient = new AtpBaseClient()\n\n  baseClient.xrpc.lex.assertValidXrpcMessage('io.example.doStuff', {\n    // ...\n  })\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { lexicons } from '@atproto/api'\n\n  lexicons.assertValidXrpcMessage('io.example.doStuff', {\n    // ...\n  })\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If you are extending the `BskyAgent` to perform custom `session` manipulation, define your own `Agent` subclass instead:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { BskyAgent } from '@atproto/api'\n\n  class MyAgent extends BskyAgent {\n    private accessToken?: string\n\n    async createOrRefreshSession(identifier: string, password: string) {\n      // custom logic here\n\n      this.accessToken = 'my-access-jwt'\n    }\n\n    async doStuff() {\n      return this.call('io.example.doStuff', {\n        headers: {\n          Authorization: this.accessToken && `Bearer ${this.accessToken}`,\n        },\n      })\n    }\n  }\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { Agent } from '@atproto/api'\n\n  class MyAgent extends Agent {\n    private accessToken?: string\n    public did?: string\n\n    constructor(private readonly service: string | URL) {\n      super({\n        service,\n        headers: {\n          Authorization: () =>\n            this.accessToken ? `Bearer ${this.accessToken}` : null,\n        },\n      })\n    }\n\n    clone(): MyAgent {\n      const agent = new MyAgent(this.service)\n      agent.accessToken = this.accessToken\n      agent.did = this.did\n      return this.copyInto(agent)\n    }\n\n    async createOrRefreshSession(identifier: string, password: string) {\n      // custom logic here\n\n      this.did = 'did:example:123'\n      this.accessToken = 'my-access-jwt'\n    }\n  }\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If you are monkey patching the `xrpc` service client to perform client-side rate limiting, you can now do this in the `FetchHandler` function:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { BskyAgent } from '@atproto/api'\n  import { RateLimitThreshold } from 'rate-limit-threshold'\n\n  const agent = new BskyAgent()\n  const limiter = new RateLimitThreshold(3000, 300_000)\n\n  const origCall = agent.api.xrpc.call\n  agent.api.xrpc.call = async function (...args) {\n    await limiter.wait()\n    return origCall.call(this, ...args)\n  }\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { AtpAgent } from '@atproto/api'\n  import { RateLimitThreshold } from 'rate-limit-threshold'\n\n  class LimitedAtpAgent extends AtpAgent {\n    constructor(options: AtpAgentOptions) {\n      const fetch: typeof globalThis.fetch = options.fetch ?? globalThis.fetch\n      const limiter = new RateLimitThreshold(3000, 300_000)\n\n      super({\n        ...options,\n        fetch: async (...args) => {\n          await limiter.wait()\n          return fetch(...args)\n        },\n      })\n    }\n  }\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If you configure a static `fetch` handler on the `BskyAgent` class - for example\n  to modify the headers of every request - you can now do this by providing your\n  own `fetch` function:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { BskyAgent, defaultFetchHandler } from '@atproto/api'\n\n  BskyAgent.configure({\n    fetch: async (httpUri, httpMethod, httpHeaders, httpReqBody) => {\n      const ua = httpHeaders['User-Agent']\n\n      httpHeaders['User-Agent'] = ua ? `${ua} ${userAgent}` : userAgent\n\n      return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody)\n    },\n  })\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { AtpAgent } from '@atproto/api'\n\n  class MyAtpAgent extends AtpAgent {\n    constructor(options: AtpAgentOptions) {\n      const fetch = options.fetch ?? globalThis.fetch\n\n      super({\n        ...options,\n        fetch: async (url, init) => {\n          const headers = new Headers(init.headers)\n\n          const ua = headersList.get('User-Agent')\n          headersList.set('User-Agent', ua ? `${ua} ${userAgent}` : userAgent)\n\n          return fetch(url, { ...init, headers })\n        },\n      })\n    }\n  }\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  <!-- <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  // before\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  // after\n  ```\n\n  </td>\n  </tr>\n  </table> -->\n\n  ##### The `@atproto/xrpc` package\n\n  The `Client` and `ServiceClient` classes are now **deprecated**. If you need a\n  lexicon based client, you should update the code to use the `XrpcClient` class\n  instead.\n\n  The deprecated `ServiceClient` class now extends the new `XrpcClient` class.\n  Because of this, the `fetch` `FetchHandler` can no longer be configured on the\n  `Client` instances (including the default export of the package). If you are not\n  relying on the `fetch` `FetchHandler`, the new changes should have no impact on\n  your code. Beware that the deprecated classes will eventually be removed in a\n  future version.\n\n  Since its use has completely changed, the `FetchHandler` type has also\n  completely changed. The new `FetchHandler` type is now a function that receives\n  a `url` pathname and a `RequestInit` object and returns a `Promise<Response>`.\n  This function is responsible for making the actual request to the server.\n\n  ```ts\n  export type FetchHandler = (\n    this: void,\n    /**\n     * The URL (pathname + query parameters) to make the request to, without the\n     * origin. The origin (protocol, hostname, and port) must be added by this\n     * {@link FetchHandler}, typically based on authentication or other factors.\n     */\n    url: string,\n    init: RequestInit,\n  ) => Promise<Response>\n  ```\n\n  A noticeable change that has been introduced is that the `uri` field of the\n  `ServiceClient` class has _not_ been ported to the new `XrpcClient` class. It is\n  now the responsibility of the `FetchHandler` to determine the full URL to make\n  the request to. The same goes for the `headers`, which should now be set through\n  the `FetchHandler` function.\n\n  If you _do_ rely on the legacy `Client.fetch` property to perform custom logic\n  upon request, you will need to migrate your code to use the new `XrpcClient`\n  class. The `XrpcClient` class has a similar API to the old `ServiceClient`\n  class, but with a few differences:\n\n  - The `Client` + `ServiceClient` duality was removed in favor of a single\n    `XrpcClient` class. This means that:\n\n    - There no longer exists a centralized lexicon registry. If you need a global\n      lexicon registry, you can maintain one yourself using a `new Lexicons` (from\n      `@atproto/lexicon`).\n    - The `FetchHandler` is no longer a statically defined property of the\n      `Client` class. Instead, it is passed as an argument to the `XrpcClient`\n      constructor.\n\n  - The `XrpcClient` constructor now requires a `FetchHandler` function as the\n    first argument, and an optional `Lexicon` instance as the second argument.\n  - The `setHeader` and `unsetHeader` methods were not ported to the new\n    `XrpcClient` class. If you need to set or unset headers, you should do so in\n    the `FetchHandler` function provided in the constructor arg.\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import client, { defaultFetchHandler } from '@atproto/xrpc'\n\n  client.fetch = function (\n    httpUri: string,\n    httpMethod: string,\n    httpHeaders: Headers,\n    httpReqBody: unknown,\n  ) {\n    // Custom logic here\n    return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody)\n  }\n\n  client.addLexicon({\n    lexicon: 1,\n    id: 'io.example.doStuff',\n    defs: {},\n  })\n\n  const instance = client.service('http://my-service.com')\n\n  instance.setHeader('my-header', 'my-value')\n\n  await instance.call('io.example.doStuff')\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient(\n    async (url, init) => {\n      const headers = new Headers(init.headers)\n\n      headers.set('my-header', 'my-value')\n\n      // Custom logic here\n\n      const fullUrl = new URL(url, 'http://my-service.com')\n\n      return fetch(fullUrl, { ...init, headers })\n    },\n    [\n      {\n        lexicon: 1,\n        id: 'io.example.doStuff',\n        defs: {},\n      },\n    ],\n  )\n\n  await instance.call('io.example.doStuff')\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If your fetch handler does not require any \"custom logic\", and all you need is\n  an `XrpcClient` that makes its HTTP requests towards a static service URL, the\n  previous example can be simplified to:\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient('http://my-service.com', [\n    {\n      lexicon: 1,\n      id: 'io.example.doStuff',\n      defs: {},\n    },\n  ])\n  ```\n\n  If you need to add static headers to all requests, you can instead instantiate\n  the `XrpcClient` as follows:\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient(\n    {\n      service: 'http://my-service.com',\n      headers: {\n        'my-header': 'my-value',\n      },\n    },\n    [\n      {\n        lexicon: 1,\n        id: 'io.example.doStuff',\n        defs: {},\n      },\n    ],\n  )\n  ```\n\n  If you need the headers or service url to be dynamic, you can define them using\n  functions:\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient(\n    {\n      service: () => 'http://my-service.com',\n      headers: {\n        'my-header': () => 'my-value',\n        'my-ignored-header': () => null, // ignored\n      },\n    },\n    [\n      {\n        lexicon: 1,\n        id: 'io.example.doStuff',\n        defs: {},\n      },\n    ],\n  )\n  ```\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add the ability to use `fetch()` compatible `BodyInit` body when making XRPC calls.\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/lexicon@0.4.1\n  - @atproto/xrpc@0.6.0\n\n## 0.12.29\n\n### Patch Changes\n\n- [#2668](https://github.com/bluesky-social/atproto/pull/2668) [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c) Thanks [@dholms](https://github.com/dholms)! - Add lxm and exp parameters to com.atproto.server.getServiceAuth\n\n## 0.12.28\n\n### Patch Changes\n\n- [#2676](https://github.com/bluesky-social/atproto/pull/2676) [`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove `app.bsky.feed.detach` record, to be replaced by `app.bsky.feed.postgate` record in a future release.\n\n## 0.12.27\n\n### Patch Changes\n\n- [#2664](https://github.com/bluesky-social/atproto/pull/2664) [`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `app.bsky.feed.detach` record lexicons.\n\n## 0.12.26\n\n### Patch Changes\n\n- [#2276](https://github.com/bluesky-social/atproto/pull/2276) [`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Updates muted words lexicons to include new attributes `id`, `actorTarget`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words.\n\n## 0.12.25\n\n### Patch Changes\n\n- [#2570](https://github.com/bluesky-social/atproto/pull/2570) [`12dcdb668`](https://github.com/bluesky-social/atproto/commit/12dcdb668c8ec0f8a89689c326ab3e9dbc6d2f3c) Thanks [@sugyan](https://github.com/sugyan)! - Fix `hasMutedWord` for facets with multiple features\n\n- [#2648](https://github.com/bluesky-social/atproto/pull/2648) [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b) Thanks [@dholms](https://github.com/dholms)! - Support for priority notifications\n\n## 0.12.24\n\n### Patch Changes\n\n- [#2613](https://github.com/bluesky-social/atproto/pull/2613) [`ed5810179`](https://github.com/bluesky-social/atproto/commit/ed5810179006f254f2035fe1f0e3c4798080cfe0) Thanks [@haileyok](https://github.com/haileyok)! - Support for starter packs in record embed views.\n\n- [#2554](https://github.com/bluesky-social/atproto/pull/2554) [`0529bec99`](https://github.com/bluesky-social/atproto/commit/0529bec99183439829a3553f45ac7203763144c3) Thanks [@sugyan](https://github.com/sugyan)! - Add missing `getPreferences` union return types\n\n## 0.12.23\n\n### Patch Changes\n\n- [#2492](https://github.com/bluesky-social/atproto/pull/2492) [`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863) Thanks [@pfrazee](https://github.com/pfrazee)! - Added bsky app state preference and improved protections against race conditions in preferences sdk\n\n## 0.12.22\n\n### Patch Changes\n\n- [#2553](https://github.com/bluesky-social/atproto/pull/2553) [`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02) Thanks [@devinivy](https://github.com/devinivy)! - Support for starter packs (app.bsky.graph.starterpack)\n\n## 0.12.21\n\n### Patch Changes\n\n- [#2460](https://github.com/bluesky-social/atproto/pull/2460) [`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24) Thanks [@foysalit](https://github.com/foysalit)! - Add DB backed team member management for ozone\n\n## 0.12.20\n\n### Patch Changes\n\n- [#2582](https://github.com/bluesky-social/atproto/pull/2582) [`ea0f10b5d`](https://github.com/bluesky-social/atproto/commit/ea0f10b5d0d334eb587032c54d5ace9ea811cf26) Thanks [@pfrazee](https://github.com/pfrazee)! - Remove client-side enforcement of labeler limits\n\n## 0.12.19\n\n### Patch Changes\n\n- [#2558](https://github.com/bluesky-social/atproto/pull/2558) [`7c1973841`](https://github.com/bluesky-social/atproto/commit/7c1973841dab416ae19435d37853aeea1f579d39) Thanks [@dholms](https://github.com/dholms)! - Add thread mute routes and viewer state\n\n## 0.12.18\n\n### Patch Changes\n\n- [#2557](https://github.com/bluesky-social/atproto/pull/2557) [`58abcbd8b`](https://github.com/bluesky-social/atproto/commit/58abcbd8b6e42a1f66bda6acc3ee6a2c0894e546) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds \"social proof\": `knowFollowers` to `ViewerState` for `ProfileViewDetailed`\n  views and `app.bsky.graph.getKnownFollowers` method for listing known followers\n  of a given user.\n\n## 0.12.17\n\n### Patch Changes\n\n- [#2426](https://github.com/bluesky-social/atproto/pull/2426) [`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.admin.searchAccounts lexicon to allow searching for accounts using email address\n\n## 0.12.16\n\n### Patch Changes\n\n- [#2539](https://github.com/bluesky-social/atproto/pull/2539) [`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46) Thanks [@dholms](https://github.com/dholms)! - Allow updating deactivation state through admin.updateSubjectStatus\n\n## 0.12.15\n\n### Patch Changes\n\n- [#2531](https://github.com/bluesky-social/atproto/pull/2531) [`255d5ea1f`](https://github.com/bluesky-social/atproto/commit/255d5ea1f06726547cdbe59c83bd18f2d4746912) Thanks [@dholms](https://github.com/dholms)! - Account deactivation. Current hosting status returned on session routes.\n\n## 0.12.14\n\n### Patch Changes\n\n- [#2533](https://github.com/bluesky-social/atproto/pull/2533) [`c4af6a409`](https://github.com/bluesky-social/atproto/commit/c4af6a409ea2171c3cf1d0e7c8ed496794a3f049) Thanks [@devinivy](https://github.com/devinivy)! - Support for post embeds in chat lexicons\n\n## 0.12.13\n\n### Patch Changes\n\n- [#2517](https://github.com/bluesky-social/atproto/pull/2517) [`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9) Thanks [@dholms](https://github.com/dholms)! - Add privileged flag to app password routes\n\n## 0.12.12\n\n### Patch Changes\n\n- [#2442](https://github.com/bluesky-social/atproto/pull/2442) [`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.label.queryLabels endpoint on appview and allow viewing external labels through ozone\n\n## 0.12.11\n\n### Patch Changes\n\n- [#2499](https://github.com/bluesky-social/atproto/pull/2499) [`06d2328ee`](https://github.com/bluesky-social/atproto/commit/06d2328eeb8d706018dbdf7cc7b9862dd65b96cb) Thanks [@devinivy](https://github.com/devinivy)! - Misc tweaks and fixes to chat lexicons\n\n## 0.12.10\n\n### Patch Changes\n\n- [#2485](https://github.com/bluesky-social/atproto/pull/2485) [`d32f7215f`](https://github.com/bluesky-social/atproto/commit/d32f7215f69bc87f50890d9cfdb09840c2fbaa41) Thanks [@devinivy](https://github.com/devinivy)! - Add lexicons for chat.bsky namespace\n\n## 0.12.9\n\n### Patch Changes\n\n- [#2467](https://github.com/bluesky-social/atproto/pull/2467) [`f83b4c8ca`](https://github.com/bluesky-social/atproto/commit/f83b4c8cad01cebc1b67caa6c7ebe45f07b2f318) Thanks [@haileyok](https://github.com/haileyok)! - Modify label-handling on user's own content to still apply blurring\n\n## 0.12.8\n\n### Patch Changes\n\n- [`58f719cc1`](https://github.com/bluesky-social/atproto/commit/58f719cc1c8d0ebd5ad7cf11221372b671cd7857) Thanks [@devinivy](https://github.com/devinivy)! - Add grandparent author to feed item reply ref\n\n## 0.12.7\n\n### Patch Changes\n\n- [#2390](https://github.com/bluesky-social/atproto/pull/2390) [`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933) Thanks [@foysalit](https://github.com/foysalit)! - Allow muting reports from accounts via `#modEventMuteReporter` event\n\n## 0.12.6\n\n### Patch Changes\n\n- [#2427](https://github.com/bluesky-social/atproto/pull/2427) [`b9b7c5821`](https://github.com/bluesky-social/atproto/commit/b9b7c582199d57d2fe0af8af5c8c411ed34f5b9d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduces V2 of saved feeds preferences. V2 and v1 prefs are incompatible. v1\n  methods and preference objects are retained for backwards compatability, but are\n  considered deprecated. Developers should immediately migrate to v2 interfaces.\n\n## 0.12.5\n\n### Patch Changes\n\n- [#2419](https://github.com/bluesky-social/atproto/pull/2419) [`3424a1770`](https://github.com/bluesky-social/atproto/commit/3424a17703891f5678ec76ef97e696afb3288b22) Thanks [@pfrazee](https://github.com/pfrazee)! - Add authFactorToken to session objects\n\n## 0.12.4\n\n### Patch Changes\n\n- [#2416](https://github.com/bluesky-social/atproto/pull/2416) [`93a4a4df9`](https://github.com/bluesky-social/atproto/commit/93a4a4df9ce38f89a5d05e300d247b85fb007e05) Thanks [@devinivy](https://github.com/devinivy)! - Support for email auth factor lexicons\n\n## 0.12.3\n\n### Patch Changes\n\n- [#2383](https://github.com/bluesky-social/atproto/pull/2383) [`0edef0ec0`](https://github.com/bluesky-social/atproto/commit/0edef0ec01403fd6097a4d2875b68313f2f1261f) Thanks [@dholms](https://github.com/dholms)! - Added feed generator interaction lexicons\n\n- [#2409](https://github.com/bluesky-social/atproto/pull/2409) [`c6d758b8b`](https://github.com/bluesky-social/atproto/commit/c6d758b8b63f4ef50b2ab9afc62164e92a53e7f0) Thanks [@devinivy](https://github.com/devinivy)! - Support for upcoming post search params\n\n## 0.12.2\n\n### Patch Changes\n\n- [#2344](https://github.com/bluesky-social/atproto/pull/2344) [`abc6f82da`](https://github.com/bluesky-social/atproto/commit/abc6f82da38abef2b1bbe8d9e41a0534a5418c9e) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Support muting words that contain apostrophes and other punctuation\n\n## 0.12.1\n\n### Patch Changes\n\n- [#2342](https://github.com/bluesky-social/atproto/pull/2342) [`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds the `associated` property to `profile` and `profile-basic` views, bringing them in line with `profile-detailed` views.\n\n## 0.12.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- [#2338](https://github.com/bluesky-social/atproto/pull/2338) [`36f2e966c`](https://github.com/bluesky-social/atproto/commit/36f2e966cba6cc90ba4320520da5c7381cfb8086) Thanks [@pfrazee](https://github.com/pfrazee)! - Fix: correctly detected blocked quote-posts when moderating posts\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common-web@0.3.0\n  - @atproto/lexicon@0.4.0\n  - @atproto/syntax@0.3.0\n  - @atproto/xrpc@0.5.0\n\n## 0.11.2\n\n### Patch Changes\n\n- [#2328](https://github.com/bluesky-social/atproto/pull/2328) [`7dd9941b7`](https://github.com/bluesky-social/atproto/commit/7dd9941b73dbbd82601740e021cc87d765af60ca) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove unecessary escapes from regex, which was causing a minification error when bundled in React Native.\n\n## 0.11.1\n\n### Patch Changes\n\n- [#2312](https://github.com/bluesky-social/atproto/pull/2312) [`219480764`](https://github.com/bluesky-social/atproto/commit/2194807644cbdb0021e867437693300c1b0e55f5) Thanks [@pfrazee](https://github.com/pfrazee)! - Fixed an issue that would cause agent clones to drop the PDS URI config.\n\n## 0.11.0\n\n### Minor Changes\n\n- [#2302](https://github.com/bluesky-social/atproto/pull/2302) [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0) Thanks [@dholms](https://github.com/dholms)! - - Breaking changes\n  - Redesigned the `moderate*` APIs which now output a `ModerationUI` object.\n  - `agent.getPreferences()` output object `BskyPreferences` has been modified.\n  - Moved Ozone routes from `com.atproto.admin` to `tools.ozone` namespace.\n  - Additions\n    - Added support for labeler configuration in `Agent.configure()` and `agent.configureLabelerHeader()`.\n    - Added `agent.addLabeler()` and `agent.removeLabeler()` preference methods.\n    - Muted words and hidden posts are now handled in the `moderate*` APIs.\n    - Added `agent.getLabelers()` and `agent.getLabelDefinitions()`.\n    - Added `agent.configureProxyHeader()` and `withProxy()` methods to support remote service proxying behaviors.\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n  - @atproto/lexicon@0.3.3\n  - @atproto/syntax@0.2.1\n  - @atproto/xrpc@0.4.3\n\n## 0.10.5\n\n### Patch Changes\n\n- [#2279](https://github.com/bluesky-social/atproto/pull/2279) [`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655) Thanks [@gaearon](https://github.com/gaearon)! - Change Following feed prefs to only show replies from people you follow by default\n\n## 0.10.4\n\n### Patch Changes\n\n- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Export regex from rich text detection\n\n- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Disallow rare unicode whitespace characters from tags\n\n- [#2260](https://github.com/bluesky-social/atproto/pull/2260) [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Allow tags to lead with numbers\n\n## 0.10.3\n\n### Patch Changes\n\n- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix double sanitization bug when editing muted words.\n\n- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - More sanitization of muted words, including newlines and leading/trailing whitespace\n\n- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `sanitizeMutedWordValue` util\n\n- [#2247](https://github.com/bluesky-social/atproto/pull/2247) [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Handle hash emoji in mute words\n\n## 0.10.2\n\n### Patch Changes\n\n- [#2245](https://github.com/bluesky-social/atproto/pull/2245) [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410) Thanks [@mary-ext](https://github.com/mary-ext)! - Prevent hashtag emoji from being parsed as a tag\n\n- [#2218](https://github.com/bluesky-social/atproto/pull/2218) [`43531905c`](https://github.com/bluesky-social/atproto/commit/43531905ce1aec6d36d9be5943782811ecca6e6d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix mute word upsert logic by ensuring we're comparing sanitized word values\n\n- [#2245](https://github.com/bluesky-social/atproto/pull/2245) [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410) Thanks [@mary-ext](https://github.com/mary-ext)! - Properly calculate length of tag\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/lexicon@0.3.2\n  - @atproto/xrpc@0.4.2\n\n## 0.10.1\n\n### Patch Changes\n\n- [#2215](https://github.com/bluesky-social/atproto/pull/2215) [`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add missing `getPreferences` union return types\n\n## 0.10.0\n\n### Minor Changes\n\n- [#2170](https://github.com/bluesky-social/atproto/pull/2170) [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a) Thanks [@dholms](https://github.com/dholms)! - Add lexicons and methods for account migration\n\n### Patch Changes\n\n- [#2195](https://github.com/bluesky-social/atproto/pull/2195) [`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add muted words/tags and hidden posts prefs and methods\"\n\n## 0.9.8\n\n### Patch Changes\n\n- [#2192](https://github.com/bluesky-social/atproto/pull/2192) [`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e) Thanks [@foysalit](https://github.com/foysalit)! - Tag event on moderation subjects and allow filtering events and subjects by tags\n\n## 0.9.7\n\n### Patch Changes\n\n- [#2188](https://github.com/bluesky-social/atproto/pull/2188) [`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b) Thanks [@dholms](https://github.com/dholms)! - Added timelineIndex to savedFeedsPref\n\n## 0.9.6\n\n### Patch Changes\n\n- [#2124](https://github.com/bluesky-social/atproto/pull/2124) [`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0) Thanks [@foysalit](https://github.com/foysalit)! - Allow filtering for comment, label, report type and date range on queryModerationEvents endpoint.\n\n## 0.9.5\n\n### Patch Changes\n\n- [#2090](https://github.com/bluesky-social/atproto/pull/2090) [`8994d363`](https://github.com/bluesky-social/atproto/commit/8994d3633adad1c02569d6d44ae896e18195e8e2) Thanks [@dholms](https://github.com/dholms)! - add checkSignupQueue method and expose refreshSession on agent\n\n## 0.9.4\n\n### Patch Changes\n\n- [#2086](https://github.com/bluesky-social/atproto/pull/2086) [`4171c04a`](https://github.com/bluesky-social/atproto/commit/4171c04ad81c5734a4558bc41fa1c4f3a1aba18c) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `setInterestsPref` method to BskyAgent, and `interests` prop to\n  `getPreferences` response.\n\n## 0.9.3\n\n### Patch Changes\n\n- [#2081](https://github.com/bluesky-social/atproto/pull/2081) [`5368245a`](https://github.com/bluesky-social/atproto/commit/5368245a6ef7095c86ad166fb04ff9bef27c3c3e) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add unspecced route for new onboarding `app.bsky.unspecced.getTaggedSuggestions`\n\n## 0.9.2\n\n### Patch Changes\n\n- [#2045](https://github.com/bluesky-social/atproto/pull/2045) [`15f38560`](https://github.com/bluesky-social/atproto/commit/15f38560b9e2dc3af8cf860826e7477234fe6a2d) Thanks [@foysalit](https://github.com/foysalit)! - support new lexicons for admin communication templates\n\n## 0.9.1\n\n### Patch Changes\n\n- [#2062](https://github.com/bluesky-social/atproto/pull/2062) [`c6fc73ae`](https://github.com/bluesky-social/atproto/commit/c6fc73aee6c245d12f876abd11889b8dbd0ce2ed) Thanks [@dholms](https://github.com/dholms)! - Directly pass create account params in api agent\n\n## 0.9.0\n\n### Minor Changes\n\n- [#2039](https://github.com/bluesky-social/atproto/pull/2039) [`bf8d718c`](https://github.com/bluesky-social/atproto/commit/bf8d718cf918ac8d8a2cb1f57fde80535284642d) Thanks [@dholms](https://github.com/dholms)! - Namespace lexicon codegen\n\n### Patch Changes\n\n- [#2056](https://github.com/bluesky-social/atproto/pull/2056) [`e43396af`](https://github.com/bluesky-social/atproto/commit/e43396af0973748dd2d034e88d35cf7ae8b4df2c) Thanks [@dholms](https://github.com/dholms)! - Added phone verification methods/schemas to agent.\n\n- [#1988](https://github.com/bluesky-social/atproto/pull/1988) [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6) Thanks [@bnewbold](https://github.com/bnewbold)! - remove deprecated app.bsky.unspecced.getPopular endpoint\n\n## 0.8.0\n\n### Minor Changes\n\n- [#2010](https://github.com/bluesky-social/atproto/pull/2010) [`14067733`](https://github.com/bluesky-social/atproto/commit/140677335f76b99129c1f593d9e11d64624386c6) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Improve `resumeSession` event emission. It will no longer double emit when some\n  requests fail, and the `create-failed` event has been replaced by `expired`\n  where appropriate, and with a new event `network-error` where appropriate or an\n  unknown error occurs.\n\n## 0.7.4\n\n### Patch Changes\n\n- [#1966](https://github.com/bluesky-social/atproto/pull/1966) [`8f3f43cb`](https://github.com/bluesky-social/atproto/commit/8f3f43cb40f79ff7c52f81290daec55cfb000093) Thanks [@pfrazee](https://github.com/pfrazee)! - Fix to the application of the no-unauthenticated label\n\n## 0.7.3\n\n### Patch Changes\n\n- [#1962](https://github.com/bluesky-social/atproto/pull/1962) [`7dec9df3`](https://github.com/bluesky-social/atproto/commit/7dec9df3b583ee8c06c0c6a7e32c259820dc84a5) Thanks [@pfrazee](https://github.com/pfrazee)! - Add seenAt time to listNotifications output\n\n## 0.7.2\n\n### Patch Changes\n\n- [#1776](https://github.com/bluesky-social/atproto/pull/1776) [`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `posts_and_author_threads` filter to `getAuthorFeed`\n\n## 0.7.1\n\n### Patch Changes\n\n- [#1944](https://github.com/bluesky-social/atproto/pull/1944) [`60deea17`](https://github.com/bluesky-social/atproto/commit/60deea17622f7c574c18432a55ced4e1cdc1b3a1) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Strip trailing colon from URLs in rich-text facet detection.\n\n## 0.7.0\n\n### Minor Changes\n\n- [#1937](https://github.com/bluesky-social/atproto/pull/1937) [`45352f9b`](https://github.com/bluesky-social/atproto/commit/45352f9b6d02aa405be94e9102424d983912ca5d) Thanks [@pfrazee](https://github.com/pfrazee)! - Add the !no-unauthenticated label to the moderation SDK\n\n## 0.6.24\n\n### Patch Changes\n\n- [#1912](https://github.com/bluesky-social/atproto/pull/1912) [`378fc613`](https://github.com/bluesky-social/atproto/commit/378fc6132f621ca517897c9467ed5bba134b3776) Thanks [@devinivy](https://github.com/devinivy)! - Contains breaking lexicon changes: removing legacy com.atproto admin endpoints, making uri field required on app.bsky list views.\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60)]:\n  - @atproto/syntax@0.1.5\n  - @atproto/lexicon@0.3.1\n  - @atproto/xrpc@0.4.1\n\n## 0.6.23\n\n### Patch Changes\n\n- [#1806](https://github.com/bluesky-social/atproto/pull/1806) [`772736a0`](https://github.com/bluesky-social/atproto/commit/772736a01081f39504e1b19a1b3687783bb78f07) Thanks [@devinivy](https://github.com/devinivy)! - respect pds endpoint during session resumption\n\n## 0.6.22\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/xrpc@0.4.0\n  - @atproto/common-web@0.2.3\n  - @atproto/syntax@0.1.4\n\n## 0.6.21\n\n### Patch Changes\n\n- [#1779](https://github.com/bluesky-social/atproto/pull/1779) [`9c98a5ba`](https://github.com/bluesky-social/atproto/commit/9c98a5baaf503b02238a6afe4f6e2b79c5181693) Thanks [@pfrazee](https://github.com/pfrazee)! - modlist helpers added to bsky-agent, add blockingByList to viewer state lexicon\n\n- [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7) Thanks [@devinivy](https://github.com/devinivy)! - Allow pds to serve did doc with credentials, API client to respect PDS listed in the did doc.\n\n- Updated dependencies [[`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/common-web@0.2.2\n  - @atproto/lexicon@0.2.3\n  - @atproto/syntax@0.1.3\n  - @atproto/xrpc@0.3.3\n\n## 0.6.20\n\n### Patch Changes\n\n- [#1568](https://github.com/bluesky-social/atproto/pull/1568) [`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b) Thanks [@dholms](https://github.com/dholms)! - Added email verification and update flows\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/common-web@0.2.1\n  - @atproto/lexicon@0.2.2\n  - @atproto/syntax@0.1.2\n  - @atproto/xrpc@0.3.2\n\n## 0.6.19\n\n### Patch Changes\n\n- [#1674](https://github.com/bluesky-social/atproto/pull/1674) [`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Strip leading `#` from from detected tag facets\n\n## 0.6.18\n\n### Patch Changes\n\n- [#1651](https://github.com/bluesky-social/atproto/pull/1651) [`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds support for hashtags in the `RichText.detectFacets` method.\n\n## 0.6.17\n\n### Patch Changes\n\n- [#1637](https://github.com/bluesky-social/atproto/pull/1637) [`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduce general support for tags on posts\n\n## 0.6.16\n\n### Patch Changes\n\n- [#1653](https://github.com/bluesky-social/atproto/pull/1653) [`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242) Thanks [@pfrazee](https://github.com/pfrazee)! - Improve the types of the thread and feed preferences APIs\n\n## 0.6.15\n\n### Patch Changes\n\n- [#1639](https://github.com/bluesky-social/atproto/pull/1639) [`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f) Thanks [@pfrazee](https://github.com/pfrazee)! - Added new preferences for feed and thread view behaviors.\n\n## 0.6.14\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n  - @atproto/lexicon@0.2.1\n  - @atproto/xrpc@0.3.1\n\n## 0.6.13\n\n### Patch Changes\n\n- [#1553](https://github.com/bluesky-social/atproto/pull/1553) [`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds a new method `app.bsky.graph.getSuggestedFollowsByActor`. This method\n  returns suggested follows for a given actor based on their likes and follows.\n"
  },
  {
    "path": "packages/api/OAUTH.md",
    "content": "# OAuth Client Quickstart\n\nThis document describes how to implement OAuth based authentication in a\nbrowser-based Single Page App (SPA), to communicate with\n[atproto](https://atproto.com) API services.\n\n## Prerequisites\n\n- You need a web server - or at the very least a static file server - to host your SPA.\n\n> [!TIP]\n>\n> During development, you can use a local server to host your client metadata.\n> You will need to use a tunneling service like [ngrok](https://ngrok.com/) to\n> make your local server accessible from the internet.\n\n> [!TIP]\n>\n> You can use a service like [GitHub Pages](https://pages.github.com/) to host\n> your client metadata and SPA for free.\n\n- You must be able to build and deploy a SPA to your server.\n\n## Step 1: Create your client metadata\n\nBased on your hosting server endpoint, you will first need to choose a\n`client_id`. That `client_id` will be used to identify your client to\nAuthorization Servers. A `client_id` must be a URL pointing to a JSON file\nwhich contains your client metadata. The client metadata **must** contain a\n`client_id` that is the URL used to access the metadata.\n\nHere is an example client metadata.\n\n```json\n{\n  \"client_id\": \"https://example.com/client-metadata.json\",\n  \"client_name\": \"Example atproto Browser App\",\n  \"client_uri\": \"https://example.com\",\n  \"logo_uri\": \"https://example.com/logo.png\",\n  \"tos_uri\": \"https://example.com/tos\",\n  \"policy_uri\": \"https://example.com/policy\",\n  \"redirect_uris\": [\"https://example.com/callback\"],\n  \"scope\": \"atproto\",\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"response_types\": [\"code\"],\n  \"token_endpoint_auth_method\": \"none\",\n  \"application_type\": \"web\",\n  \"dpop_bound_access_tokens\": true\n}\n```\n\n- `redirect_uris`: An array of URLs that will be used as the redirect URIs for\n  the OAuth flow. This should typically contain a single URL that points to a\n  page on your SPA that will handle the OAuth response. This URL must be HTTPS.\n\n- `client_id`: The URL where the client metadata is hosted. This field must be\n  the exact same as the URL used to access the metadata.\n\n- `client_name`: The name of your client. Will be displayed to the user during\n  the authentication process.\n\n- `client_uri`: The URL of your client. Whether or not this value is actually\n  displayed / used is up to the Authorization Server.\n\n- `logo_uri`: The URL of your client's logo. Should be displayed to the user\n  during the authentication process. Whether your logo is actually displayed\n  during the authentication process or not is up to the Authorization Server.\n\n- `tos_uri`: The URL of your client's terms of service. Will be displayed to\n  the user during the authentication process.\n\n- `policy_uri`: The URL of your client's privacy policy. Will be displayed to\n  the user during the authentication process.\n\n- If you don't want or need the user to stay authenticated for long periods\n  (better for security), you can remove `refresh_token` from the `grant_types`.\n\n> [!NOTE]\n>\n> To mitigate phishing attacks, the Authentication Server will typically _not_\n> display the `client_uri` or `logo_uri` to the user. If you don't see your logo\n> or client name during the authentication process, don't worry. This is normal.\n> The `client_name` _is_ generally displayed for all clients.\n\nUpload this JSON file so that it is accessible at the URL you chose for your\n`client_id`.\n\n## Step 2: Setup your SPA\n\nStart by setting up your SPA. You can use any framework you like, or none at\nall. In this example, we will use TypeScript and Parcel, with plain JavaScript.\n\n```bash\nnpm init -y\nnpm install --save-dev @atproto/oauth-client-browser\nnpm install --save-dev @atproto/api\nnpm install --save-dev parcel\nnpm install --save-dev parcel-reporter-static-files-copy\nmkdir -p src\nmkdir -p static\n```\n\nCreate a `.parcelrc` file with the following (exact) content:\n\n```json\n{\n  \"extends\": [\"@parcel/config-default\"],\n  \"reporters\": [\"...\", \"parcel-reporter-static-files-copy\"]\n}\n```\n\nCreate an `src/index.html` file with the following content:\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>My First OAuth App</title>\n    <script type=\"module\" src=\"app.ts\"></script>\n  </head>\n  <body>\n    Loading...\n  </body>\n</html>\n```\n\nAnd an `src/app.ts` file, with the following content:\n\n```typescript\nconsole.log('Hello from atproto OAuth example app!')\n```\n\nStart the app in development mode:\n\n```bash\nnpx parcel src/index.html\n```\n\nIn another terminal, open a tunnel to your local server:\n\n```bash\nngrok http 1234\n```\n\nCreate a `static/client-metadata.json` file with the client metadata you created\nin [Step 1](#step-1-create-your-client-metadata). Use the hostname provided by\nngrok as the `client_id`:\n\n```json\n{\n  \"client_id\": \"https://<RANDOM_VALUE>.ngrok.app/client-metadata.json\",\n  \"client_name\": \"My First atproto OAuth App\",\n  \"client_uri\": \"https://<RANDOM_VALUE>.ngrok.app\",\n  \"redirect_uris\": [\"https://<RANDOM_VALUE>.ngrok.app/\"],\n  \"grant_types\": [\"authorization_code\"],\n  \"response_types\": [\"code\"],\n  \"token_endpoint_auth_method\": \"none\",\n  \"application_type\": \"web\",\n  \"dpop_bound_access_tokens\": true\n}\n```\n\n## Step 3: Implement the OAuth flow\n\nReplace the content of the `src/app.ts` file, with the following content:\n\n```typescript\nimport { Agent } from '@atproto/api'\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nasync function main() {\n  const oauthClient = await BrowserOAuthClient.load({\n    clientId: '<YOUR_CLIENT_ID>',\n    handleResolver: 'https://bsky.social/',\n  })\n\n  // TO BE CONTINUED\n}\n\ndocument.addEventListener('DOMContentLoaded', main)\n```\n\n> [!CAUTION]\n>\n> Using Bluesky-hosted services for handle resolution (eg, the `bsky.social`\n> endpoint) will leak both user IP addresses and handle identifier to Bluesky,\n> a third party. While Bluesky has a declared privacy policy, both developers\n> and users of applications need to be informed of and aware of the privacy\n> implications of this arrangement. Application developers are encouraged to\n> improve user privacy by operating their own handle resolution service when\n> possible. If you are a PDS self-hoster, you can use your PDS's URL for\n> `handleResolver`.\n\nThe `oauthClient` is now configured to communicate with the user's\nAuthorization Service. You can now initialize it in order to detect if the user\nis already authenticated. Replace the `// TO BE CONTINUED` comment with the\nfollowing code:\n\n```typescript\nconst result = await oauthClient.init()\n\nif (result) {\n  if ('state' in result) {\n    console.log('The user was just redirected back from the authorization page')\n  }\n\n  console.log(`The user is currently signed in as ${result.session.did}`)\n}\n\nconst session = result?.session\n\n// TO BE CONTINUED\n```\n\nAt this point you can detect if the user is already authenticated or not (by\nchecking if `session` is `undefined`).\n\nLet's initiate an authentication flow if the user is not authenticated. Replace\nthe `// TO BE CONTINUED` comment with the following code:\n\n```typescript\nif (!session) {\n  const handle = prompt('Enter your atproto handle to authenticate')\n  if (!handle) throw new Error('Authentication process canceled by the user')\n\n  const url = await oauthClient.authorize(handle)\n\n  // Redirect the user to the authorization page\n  window.open(url, '_self', 'noopener')\n\n  // Protect against browser's back-forward cache\n  await new Promise<never>((resolve, reject) => {\n    setTimeout(\n      reject,\n      10_000,\n      new Error('User navigated back from the authorization page'),\n    )\n  })\n}\n\n// TO BE CONTINUED\n```\n\nAt this point in the script, the user **will** be authenticated. Authenticated\nAPI calls can be made using the `session`. The `session` can be used to instantiate the\n`Agent` class from `@atproto/api`. Let's make a simple call to the API to\nretrieve the user's profile. Replace the `// TO BE CONTINUED` comment with the\nfollowing code:\n\n```typescript\nif (session) {\n  const agent = new Agent(session)\n\n  const fetchProfile = async () => {\n    const profile = await agent.getProfile({ actor: agent.did })\n    return profile.data\n  }\n\n  // Update the user interface\n\n  document.body.textContent = `Authenticated as ${agent.did}`\n\n  const profileBtn = document.createElement('button')\n  document.body.appendChild(profileBtn)\n  profileBtn.textContent = 'Fetch Profile'\n  profileBtn.onclick = async () => {\n    const profile = await fetchProfile()\n    outputPre.textContent = JSON.stringify(profile, null, 2)\n  }\n\n  const logoutBtn = document.createElement('button')\n  document.body.appendChild(logoutBtn)\n  logoutBtn.textContent = 'Logout'\n  logoutBtn.onclick = async () => {\n    await session.signOut()\n    window.location.reload()\n  }\n\n  const outputPre = document.createElement('pre')\n  document.body.appendChild(outputPre)\n}\n```\n\n[API]: ./README.md\n"
  },
  {
    "path": "packages/api/README.md",
    "content": "# ATP API\n\nThis API is a client for ATProtocol servers. It communicates using HTTP. It includes:\n\n- ✔️ APIs for ATProto and Bluesky.\n- ✔️ Validation and complete typescript types.\n- ✔️ Session management.\n- ✔️ A RichText library.\n\n## Getting started\n\nFirst install the package:\n\n```sh\nyarn add @atproto/api\n```\n\nThen in your application:\n\n```typescript\nimport { Agent, CredentialSession } from '@atproto/api'\n\nconst session = new CredentialSession(new URL('https://bsky.social'))\nawait session.login(account)\nconst agent = new Agent(session)\n```\n\n## Usage\n\n### Session management\n\nYou'll need an authenticated session for most API calls. There are two ways to\nmanage sessions:\n\n1. [App password based session management](#app-password-based-session-management)\n2. [OAuth based session management](#oauth-based-session-management)\n\n#### App password based session management\n\nUsername / password based authentication can be performed using the `Agent`\nclass.\n\n> [!CAUTION]\n>\n> This method is deprecated in favor of OAuth based session management. It is\n> recommended to use OAuth based session management (through the\n> `@atproto/oauth-client-*` packages).\n\n```typescript\nimport { Agent, CredentialSession, type AtpAgentLoginOpts } from '@atproto/api'\n\n// Configure connection to the server with authentification\nconst account: AtpAgentLoginOpts = {\n  identifier: 'your.bsky.social',\n  password: 'xxxx-xxxx-xxxx-xxxx',\n}\n\nasync function authenticate(account: AtpAgentLoginOpts): Promise<Agent> {\n  const session = new CredentialSession(new URL('https://example.com'))\n  await session.login(account)\n  const agent = new Agent(session)\n  return agent\n}\n\n;(async () => {\n  console.log('Authenticating...')\n  const agent = await authenticate(account)\n  console.log(`Authenticated as from: ${agent.sessionManager.did}`)\n})()\n```\n\n#### OAuth based session management\n\nDepending on the environment used by your application, different OAuth clients\nare available:\n\n- [@atproto/oauth-client-browser](https://www.npmjs.com/package/@atproto/oauth-client-browser):\n  for the browser.\n- [@atproto/oauth-client-node](https://www.npmjs.com/package/@atproto/oauth-client-node): for\n  Node.js.\n- [@atproto/oauth-client](https://www.npmjs.com/package/@atproto/oauth-client):\n  Lower level; compatible with most JS engines.\n\nEvery `@atproto/oauth-client-*` implementation has a different way to obtain an\n`OAuthSession` instance that can be used to instantiate an `Agent` (from\n`@atproto/api`). Here is an example restoring a previously saved session:\n\n```typescript\nimport { Agent } from '@atproto/api'\nimport { OAuthClient } from '@atproto/oauth-client'\n\nconst oauthClient = new OAuthClient({\n  // ...\n})\n\nconst oauthSession = await oauthClient.restore('did:plc:123')\n\n// Instantiate the api Agent using an OAuthSession\nconst agent = new Agent(oauthSession)\n```\n\n### API calls\n\nThe agent includes methods for many common operations, including:\n\n```typescript\n// The DID of the user currently authenticated (or undefined)\nagent.did\nagent.accountDid // Throws if the user is not authenticated\n\n// Feeds and content\nawait agent.getTimeline(params, opts)\nawait agent.getAuthorFeed(params, opts)\nawait agent.getPostThread(params, opts)\nawait agent.getPost(params)\nawait agent.getPosts(params, opts)\nawait agent.getLikes(params, opts)\nawait agent.getRepostedBy(params, opts)\nawait agent.post(record)\nawait agent.deletePost(postUri)\nawait agent.like(uri, cid)\nawait agent.deleteLike(likeUri)\nawait agent.repost(uri, cid)\nawait agent.deleteRepost(repostUri)\nawait agent.uploadBlob(data, opts)\n\n// Social graph\nawait agent.getFollows(params, opts)\nawait agent.getFollowers(params, opts)\nawait agent.follow(did)\nawait agent.deleteFollow(followUri)\n\n// Actors\nawait agent.getProfile(params, opts)\nawait agent.upsertProfile(updateFn)\nawait agent.getProfiles(params, opts)\nawait agent.getSuggestions(params, opts)\nawait agent.searchActors(params, opts)\nawait agent.searchActorsTypeahead(params, opts)\nawait agent.mute(did)\nawait agent.unmute(did)\nawait agent.muteModList(listUri)\nawait agent.unmuteModList(listUri)\nawait agent.blockModList(listUri)\nawait agent.unblockModList(listUri)\n\n// Notifications\nawait agent.listNotifications(params, opts)\nawait agent.countUnreadNotifications(params, opts)\nawait agent.updateSeenNotifications()\n\n// Identity\nawait agent.resolveHandle(params, opts)\nawait agent.updateHandle(params, opts)\n\n// Legacy: Session management should be performed through the SessionManager\n// rather than the Agent instance.\nif (agent instanceof AtpAgent) {\n  // AtpAgent instances support using different sessions during their lifetime\n  await agent.createAccount({ ... }) // session a\n  await agent.login({ ... }) // session b\n  await agent.resumeSession(savedSession) // session c\n}\n```\n\n### Validation and types\n\nThe package includes a complete types system which includes validation and type-guards. For example, to validate a post record:\n\n```typescript\nimport { AppBskyFeedPost } from '@atproto/api'\n\nconst post = {...}\nif (AppBskyFeedPost.isRecord(post)) {\n  // typescript now recognizes `post` as a AppBskyFeedPost.Record\n  // however -- we still need to validate it\n  const res = AppBskyFeedPost.validateRecord(post)\n  if (res.success) {\n    // a valid record\n  } else {\n    // something is wrong\n    console.log(res.error)\n  }\n}\n```\n\n### Rich text\n\nSome records (ie posts) use the `app.bsky.richtext` lexicon. At the moment richtext is only used for links and mentions, but it will be extended over time to include bold, italic, and so on.\n\nℹ️ It is **strongly** recommended to use this package's `RichText` library. Javascript encodes strings in utf16 while the protocol (and most other programming environments) use utf8. Converting between the two is challenging, but `RichText` handles that for you.\n\n```typescript\nimport { RichText } from '@atproto/api'\n\n// creating richtext\nconst rt = new RichText({\n  text: 'Hello @alice.com, check out this link: https://example.com',\n})\nawait rt.detectFacets(agent) // automatically detects mentions and links\nconst postRecord = {\n  $type: 'app.bsky.feed.post',\n  text: rt.text,\n  facets: rt.facets,\n  createdAt: new Date().toISOString(),\n}\n\n// rendering as markdown\nlet markdown = ''\nfor (const segment of rt.segments()) {\n  if (segment.isLink()) {\n    markdown += `[${segment.text}](${segment.link?.uri})`\n  } else if (segment.isMention()) {\n    markdown += `[${segment.text}](https://my-bsky-app.com/user/${segment.mention?.did})`\n  } else {\n    markdown += segment.text\n  }\n}\n\n// calculating string lengths\nconst rt2 = new RichText({ text: 'Hello' })\nconsole.log(rt2.length) // => 5\nconsole.log(rt2.graphemeLength) // => 5\nconst rt3 = new RichText({ text: '👨‍👩‍👧‍👧' })\nconsole.log(rt3.length) // => 25\nconsole.log(rt3.graphemeLength) // => 1\n```\n\n### Moderation\n\nApplying the moderation system is a challenging task, but we've done our best to simplify it for you. The Moderation API helps handle a wide range of tasks, including:\n\n- Moderator labeling\n- User muting (including mutelists)\n- User blocking\n- Mutewords\n- Hidden posts\n\nFor more information, see the [Moderation Documentation](./docs/moderation.md).\n\n```typescript\nimport { moderatePost } from '@atproto/api'\n\n// First get the user's moderation prefs and their label definitions\n// =\n\nconst prefs = await agent.getPreferences()\nconst labelDefs = await agent.getLabelDefinitions(prefs)\n\n// We call the appropriate moderation function for the content\n// =\n\nconst postMod = moderatePost(postView, {\n  userDid: agent.session.did,\n  moderationPrefs: prefs.moderationPrefs,\n  labelDefs,\n})\n\n// We then use the output to decide how to affect rendering\n// =\n\n// in feeds\nif (postMod.ui('contentList').filter) {\n  // don't include in feeds\n}\nif (postMod.ui('contentList').blur) {\n  // render the whole object behind a cover (use postMod.ui('contentList').blurs to explain)\n  if (postMod.ui('contentList').noOverride) {\n    // do not allow the cover the be removed\n  }\n}\nif (postMod.ui('contentList').alert || postMod.ui('contentList').inform) {\n  // render warnings on the post\n  // find the warnings in postMod.ui('contentList').alerts and postMod.ui('contentList').informs\n}\n\n// viewed directly\nif (postMod.ui('contentView').filter) {\n  // don't include in feeds\n}\nif (postMod.ui('contentView').blur) {\n  // render the whole object behind a cover (use postMod.ui('contentView').blurs to explain)\n  if (postMod.ui('contentView').noOverride) {\n    // do not allow the cover the be removed\n  }\n}\nif (postMod.ui('contentView').alert || postMod.ui('contentView').inform) {\n  // render warnings on the post\n  // find the warnings in postMod.ui('contentView').alerts and postMod.ui('contentView').informs\n}\n\n// post embeds in all contexts\nif (postMod.ui('contentMedia').blur) {\n  // render the whole object behind a cover (use postMod.ui('contentMedia').blurs to explain)\n  if (postMod.ui('contentMedia').noOverride) {\n    // do not allow the cover the be removed\n  }\n}\n```\n\n## Advanced\n\n### Advanced API calls\n\nThe methods above are convenience wrappers. It covers most but not all available methods.\n\nThe AT Protocol identifies methods and records with reverse-DNS names. You can use them on the agent as well:\n\n```typescript\nconst res1 = await agent.com.atproto.repo.createRecord({\n  did: alice.did,\n  collection: 'app.bsky.feed.post',\n  record: {\n    $type: 'app.bsky.feed.post',\n    text: 'Hello, world!',\n    createdAt: new Date().toISOString(),\n  },\n})\nconst res2 = await agent.com.atproto.repo.listRecords({\n  repo: alice.did,\n  collection: 'app.bsky.feed.post',\n})\n\nconst res3 = await agent.app.bsky.feed.post.create(\n  { repo: alice.did },\n  {\n    text: 'Hello, world!',\n    createdAt: new Date().toISOString(),\n  },\n)\nconst res4 = await agent.app.bsky.feed.post.list({ repo: alice.did })\n```\n\n### Non-browser configuration\n\nIf your environment doesn't have a built-in `fetch` implementation, you'll need\nto provide one. This will typically be done through a polyfill.\n\n### Bring your own fetch\n\nIf you want to provide your own `fetch` implementation, you can do so by\ninstantiating the sessionManager with a custom fetch implementation:\n\n```typescript\nimport { AtpAgent } from '@atproto/api'\n\nconst myFetch = (input: RequestInfo | URL, init?: RequestInit) => {\n  console.log('requesting', input)\n  const response = await globalThis.fetch(input, init)\n  console.log('got response', response)\n  return response\n}\n\nconst agent = new AtpAgent({\n  service: 'https://example.com',\n  fetch: myFetch,\n})\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/api/definitions/labels.json",
    "content": "[\n  {\n    \"identifier\": \"!hide\",\n    \"configurable\": false,\n    \"defaultSetting\": \"hide\",\n    \"flags\": [\"no-override\", \"no-self\"],\n    \"severity\": \"alert\",\n    \"blurs\": \"content\",\n    \"behaviors\": {\n      \"account\": {\n        \"profileList\": \"blur\",\n        \"profileView\": \"blur\",\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\",\n        \"displayName\": \"blur\",\n        \"contentList\": \"blur\",\n        \"contentView\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\",\n        \"displayName\": \"blur\"\n      },\n      \"content\": {\n        \"contentList\": \"blur\",\n        \"contentView\": \"blur\"\n      }\n    }\n  },\n  {\n    \"identifier\": \"!warn\",\n    \"configurable\": false,\n    \"defaultSetting\": \"warn\",\n    \"flags\": [\"no-self\"],\n    \"severity\": \"none\",\n    \"blurs\": \"content\",\n    \"behaviors\": {\n      \"account\": {\n        \"profileList\": \"blur\",\n        \"profileView\": \"blur\",\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\",\n        \"contentList\": \"blur\",\n        \"contentView\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\",\n        \"displayName\": \"blur\"\n      },\n      \"content\": {\n        \"contentList\": \"blur\",\n        \"contentView\": \"blur\"\n      }\n    }\n  },\n  {\n    \"identifier\": \"!no-unauthenticated\",\n    \"configurable\": false,\n    \"defaultSetting\": \"hide\",\n    \"flags\": [\"no-override\", \"unauthed\"],\n    \"severity\": \"none\",\n    \"blurs\": \"content\",\n    \"behaviors\": {\n      \"account\": {\n        \"profileList\": \"blur\",\n        \"profileView\": \"blur\",\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\",\n        \"displayName\": \"blur\",\n        \"contentList\": \"blur\",\n        \"contentView\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\",\n        \"displayName\": \"blur\"\n      },\n      \"content\": {\n        \"contentList\": \"blur\",\n        \"contentView\": \"blur\"\n      }\n    }\n  },\n  {\n    \"identifier\": \"porn\",\n    \"configurable\": true,\n    \"defaultSetting\": \"hide\",\n    \"flags\": [\"adult\"],\n    \"severity\": \"none\",\n    \"blurs\": \"media\",\n    \"behaviors\": {\n      \"account\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"content\": {\n        \"contentMedia\": \"blur\"\n      }\n    }\n  },\n  {\n    \"identifier\": \"sexual\",\n    \"configurable\": true,\n    \"defaultSetting\": \"warn\",\n    \"flags\": [\"adult\"],\n    \"severity\": \"none\",\n    \"blurs\": \"media\",\n    \"behaviors\": {\n      \"account\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"content\": {\n        \"contentMedia\": \"blur\"\n      }\n    }\n  },\n  {\n    \"identifier\": \"nudity\",\n    \"configurable\": true,\n    \"defaultSetting\": \"ignore\",\n    \"flags\": [],\n    \"severity\": \"none\",\n    \"blurs\": \"media\",\n    \"behaviors\": {\n      \"account\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"content\": {\n        \"contentMedia\": \"blur\"\n      }\n    }\n  },\n  {\n    \"identifier\": \"graphic-media\",\n    \"alias\": [\"gore\"],\n    \"flags\": [\"adult\"],\n    \"configurable\": true,\n    \"defaultSetting\": \"warn\",\n    \"severity\": \"none\",\n    \"blurs\": \"media\",\n    \"behaviors\": {\n      \"account\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"profile\": {\n        \"avatar\": \"blur\",\n        \"banner\": \"blur\"\n      },\n      \"content\": {\n        \"contentMedia\": \"blur\"\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "packages/api/docs/moderation.md",
    "content": "# Moderation API\n\nApplying the moderation system is a challenging task, but we've done our best to simplify it for you. The Moderation API helps handle a wide range of tasks, including:\n\n- Moderator labeling\n- User muting (including mutelists)\n- User blocking\n- Mutewords\n- Hidden posts\n\n## Configuration\n\nEvery moderation function takes a set of options which look like this:\n\n```typescript\n{\n  // the logged-in user's DID\n  userDid: 'did:plc:1234...',\n\n  moderationPrefs: {\n    // is adult content allowed?\n    adultContentEnabled: true,\n\n    // the global label settings (used on self-labels)\n    labels: {\n      porn: 'hide',\n      sexual: 'warn',\n      nudity: 'ignore',\n      // ...\n    },\n\n    // the subscribed labelers and their label settings\n    labelers: [\n      {\n        did: 'did:plc:1234...',\n        labels: {\n          porn: 'hide',\n          sexual: 'warn',\n          nudity: 'ignore',\n          // ...\n        }\n      }\n    ],\n\n    mutedWords: [/* ... */],\n    hiddenPosts: [/* ... */]\n  },\n\n  // custom label definitions\n  labelDefs: {\n    // labelerDid => defs[]\n    'did:plc:1234...': [\n      /* ... */\n    ]\n  }\n}\n```\n\nThis should match the following interfaces:\n\n```typescript\nexport interface ModerationPrefsLabeler {\n  did: string\n  labels: Record<string, LabelPreference>\n}\n\nexport interface ModerationPrefs {\n  adultContentEnabled: boolean\n  labels: Record<string, LabelPreference>\n  labelers: ModerationPrefsLabeler[]\n  mutedWords: AppBskyActorDefs.MutedWord[]\n  hiddenPosts: string[]\n}\n\nexport interface ModerationOpts {\n  userDid: string | undefined\n  prefs: ModerationPrefs\n  /**\n   * Map of labeler did -> custom definitions\n   */\n  labelDefs?: Record<string, InterpretedLabelValueDefinition[]>\n}\n```\n\nYou can quickly grab the `ModerationPrefs` using the `agent.getPreferences()` method:\n\n```typescript\nconst prefs = await agent.getPreferences()\nmoderatePost(post, {\n  userDid: /*...*/,\n  prefs: prefs.moderationPrefs,\n  labelDefs: /*...*/\n})\n```\n\nTo gather the label definitions (`labelDefs`) see the _Labelers_ section below.\n\n## Labelers\n\nLabelers are services that provide moderation labels. Your application will typically have 1+ top-level labelers set with the ability to do \"takedowns\" on content. This is controlled via this static function, though the default is to use Bluesky's moderation:\n\n```typescript\nBskyAgent.configure({\n  appLabelers: ['did:web:my-labeler.com'],\n})\n```\n\nUsers may also add their own labelers. The active labelers are controlled via an HTTP header which is automatically set by the agent when `getPreferences` is called, or when the labeler preferences are changed.\n\nLabelers publish a `app.bsky.labeler.service` record that looks like this:\n\n```js\n{\n  $type: 'app.bsky.labeler.service',\n  policies: {\n    // the list of label values the labeler will publish\n    labelValues: [\n      'rude',\n    ],\n    // any custom definitions the labeler will be using\n    labelValueDefinitions: [\n      {\n        identifier: 'rude',\n        blurs: 'content',\n        severity: 'alert',\n        defaultSetting: 'warn',\n        adultOnly: false,\n        locales: [\n          {\n            lang: 'en',\n            name: 'Rude',\n            description: 'Not keeping things civil.',\n          },\n        ],\n      },\n    ],\n  },\n  createdAt: '2024-03-12T17:17:17.215Z'\n}\n```\n\nThe label value definition are custom labels which only apply to that labeler. Your client needs to sync those definitions in order to correctly interpret them. To do that, call `app.bsky.labeler.getService()` (or the `getServices` batch variant) periodically to fetch their definitions. We recommend caching the response (at time our writing the official client uses a TTL of 6 hours).\n\nHere is how to do this:\n\n```typescript\nimport { AtpAgent } from '@atproto/api'\n\nconst agent = new AtpAgent({ service: 'https://example.com' })\n// assume `agent` is a signed in session\nconst prefs = await agent.getPreferences()\nconst labelDefs = await agent.getLabelDefinitions(prefs)\n\nmoderatePost(post, {\n  userDid: agent.session.did,\n  prefs: prefs.moderationPrefs,\n  labelDefs,\n})\n```\n\n## The `moderate*()` APIs\n\nThe SDK exports methods to moderate the different kinds of content on the network.\n\n```typescript\nimport {\n  moderateProfile,\n  moderatePost,\n  moderateNotification,\n  moderateFeedGen,\n  moderateUserList,\n  moderateLabeler,\n} from '@atproto/api'\n```\n\nEach of these follows the same API signature:\n\n```typescript\nconst res = moderatePost(post, moderationOptions)\n```\n\nThe response object provides an API for figuring out what your UI should do in different contexts.\n\n```typescript\nres.ui(context) /* =>\n\nModerationUI {\n  filter: boolean // should the content be removed from the interface?\n  blur: boolean // should the content be put behind a cover?\n  alert: boolean // should an alert be put on the content? (negative)\n  inform: boolean // should an informational notice be put on the content? (neutral)\n  noOverride: boolean // if blur=true, should the UI disable opening the cover?\n\n  // the reasons for each of the flags:\n  filters: ModerationCause[]\n  blurs: ModerationCause[]\n  alerts: ModerationCause[]\n  informs: ModerationCause[]\n}\n*/\n```\n\nThere are multiple UI contexts available:\n\n- `profileList` A profile being listed, eg in search or a follower list\n- `profileView` A profile being viewed directly\n- `avatar` The user's avatar in any context\n- `banner` The user's banner in any context\n- `displayName` The user's display name in any context\n- `contentList` Content being listed, eg posts in a feed, posts as replies, a user list list, a feed generator list, etc\n- `contentView` Content being viewed direct, eg an opened post, the user list page, the feedgen page, etc\n- `contentMedia ` Media inside the content, eg a picture embedded in a post\n\nHere's how a post in a feed would use these tools to make a decision:\n\n```typescript\nconst mod = moderatePost(post, moderationOptions)\n\nif (mod.ui('contentList').filter) {\n  // dont show the post\n}\nif (mod.ui('contentList').blur) {\n  // cover the post with the explanation from mod.ui('contentList').blurs[0]\n  if (mod.ui('contentList').noOverride) {\n    // dont allow the cover to be removed\n  }\n}\nif (mod.ui('contentMedia').blur) {\n  // cover the post's embedded images with the explanation from mod.ui('contentMedia').blurs[0]\n  if (mod.ui('contentMedia').noOverride) {\n    // dont allow the cover to be removed\n  }\n}\nif (mod.ui('avatar').blur) {\n  // cover the avatar with the explanation from mod.ui('avatar').blurs[0]\n  if (mod.ui('avatar').noOverride) {\n    // dont allow the cover to be removed\n  }\n}\nfor (const alert of mod.ui('contentList').alerts) {\n  // render this alert\n}\nfor (const inform of mod.ui('contentList').informs) {\n  // render this inform\n}\n```\n\n## Sending moderation reports\n\nAny Labeler is capable of receiving moderation reports. As a result, you need to specify which labeler should receive the report. You do this with the `Atproto-Proxy` header:\n\n```typescript\nagent\n  .withProxy('atproto_labeler', 'did:web:my-labeler.com')\n  .createModerationReport({\n    reasonType: 'com.atproto.moderation.defs#reasonViolation',\n    reason: 'They were being such a jerk to me!',\n    subject: { did: 'did:web:bob.com' },\n  })\n```\n"
  },
  {
    "path": "packages/api/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'API',\n  transform: { '^.+\\\\.ts$': '@swc/jest' },\n  testTimeout: 60000,\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/api/jest.d.ts",
    "content": "declare namespace jest {\n  // eslint-disable-next-line\n  interface Matchers<R, T = {}> {\n    toBeModerationResult(\n      expected: ModerationTestSuiteResultFlag[] | undefined,\n      context?: string,\n      stringifiedResult?: string,\n      ignoreCause?: boolean,\n    ): R\n  }\n\n  interface Expect {\n    toBeModerationResult(\n      expected: ModerationTestSuiteResultFlag[] | undefined,\n      context?: string,\n      stringifiedResult?: string,\n      ignoreCause?: boolean,\n    ): void\n  }\n}\n"
  },
  {
    "path": "packages/api/jest.setup.ts",
    "content": "import { ModerationUI } from './src'\nimport { ModerationTestSuiteResultFlag } from './tests/util/moderation-behavior'\n\nexpect.extend({\n  toBeModerationResult(\n    actual: ModerationUI,\n    expected: ModerationTestSuiteResultFlag[] | undefined,\n    context = '',\n    stringifiedResult: string | undefined = undefined,\n    _ignoreCause = false,\n  ) {\n    const fail = (msg: string) => ({\n      pass: false,\n      message: () =>\n        `${msg}.${\n          stringifiedResult ? ` Full result: ${stringifiedResult}` : ''\n        }`,\n    })\n    // let cause = actual.causes?.type as string\n    // if (actual.cause?.type === 'label') {\n    //   cause = `label:${actual.cause.labelDef.id}`\n    // } else if (actual.cause?.type === 'muted') {\n    //   if (actual.cause.source.type === 'list') {\n    //     cause = 'muted-by-list'\n    //   }\n    // } else if (actual.cause?.type === 'blocking') {\n    //   if (actual.cause.source.type === 'list') {\n    //     cause = 'blocking-by-list'\n    //   }\n    // }\n    if (!expected) {\n      // if (!ignoreCause && actual.cause) {\n      //   return fail(`${context} expected to be a no-op, got ${cause}`)\n      // }\n      if (actual.inform) {\n        return fail(`${context} expected to be a no-op, got inform=true`)\n      }\n      if (actual.alert) {\n        return fail(`${context} expected to be a no-op, got alert=true`)\n      }\n      if (actual.blur) {\n        return fail(`${context} expected to be a no-op, got blur=true`)\n      }\n      if (actual.filter) {\n        return fail(`${context} expected to be a no-op, got filter=true`)\n      }\n      if (actual.noOverride) {\n        return fail(`${context} expected to be a no-op, got noOverride=true`)\n      }\n    } else {\n      // if (!ignoreCause && cause !== expected.cause) {\n      //   return fail(`${context} expected to be ${expected.cause}, got ${cause}`)\n      // }\n      const expectedInform = expected.includes('inform')\n      if (!!actual.inform !== expectedInform) {\n        return fail(\n          `${context} expected to be inform=${expectedInform}, got ${\n            actual.inform || false\n          }`,\n        )\n      }\n      const expectedAlert = expected.includes('alert')\n      if (!!actual.alert !== expectedAlert) {\n        return fail(\n          `${context} expected to be alert=${expectedAlert}, got ${\n            actual.alert || false\n          }`,\n        )\n      }\n      const expectedBlur = expected.includes('blur')\n      if (!!actual.blur !== expectedBlur) {\n        return fail(\n          `${context} expected to be blur=${expectedBlur}, got ${\n            actual.blur || false\n          }`,\n        )\n      }\n      const expectedFilter = expected.includes('filter')\n      if (!!actual.filter !== expectedFilter) {\n        return fail(\n          `${context} expected to be filter=${expectedFilter}, got ${\n            actual.filter || false\n          }`,\n        )\n      }\n      const expectedNoOverride = expected.includes('noOverride')\n      if (!!actual.noOverride !== expectedNoOverride) {\n        return fail(\n          `${context} expected to be noOverride=${expectedNoOverride}, got ${\n            actual.noOverride || false\n          }`,\n        )\n      }\n    }\n    return { pass: true, message: () => '' }\n  },\n})\n"
  },
  {
    "path": "packages/api/package.json",
    "content": "{\n  \"name\": \"@atproto/api\",\n  \"version\": \"0.19.4\",\n  \"license\": \"MIT\",\n  \"description\": \"Client library for atproto and Bluesky\",\n  \"keywords\": [\n    \"atproto\",\n    \"bluesky\",\n    \"api\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/api\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"codegen\": \"node ./scripts/generate-code.mjs && lex gen-api --yes ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/* ../../lexicons/com/germnetwork/*\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"jest\"\n  },\n  \"dependencies\": {\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"await-lock\": \"^2.2.2\",\n    \"multiformats\": \"^9.9.0\",\n    \"tlds\": \"^1.234.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-cli\": \"workspace:^\",\n    \"@jest/globals\": \"^28.1.3\",\n    \"jest\": \"^28.1.2\",\n    \"prettier\": \"^3.2.5\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/api/scripts/code/labels.mjs",
    "content": "import * as url from 'url'\nimport { readFileSync, writeFileSync } from 'fs'\nimport { join } from 'path'\nimport * as prettier from 'prettier'\n\nconst __dirname = url.fileURLToPath(new URL('.', import.meta.url))\n\nconst labelsDef = JSON.parse(\n  readFileSync(\n    join(__dirname, '..', '..', 'definitions', 'labels.json'),\n    'utf8',\n  ),\n)\n\nwriteFileSync(\n  join(__dirname, '..', '..', 'src', 'moderation', 'const', 'labels.ts'),\n  await gen(),\n  'utf8',\n)\n\nasync function gen() {\n  const knownValues = new Set()\n  const flattenedLabelDefs = []\n\n  for (const { alias: aliases, ...label } of labelsDef) {\n    knownValues.add(label.identifier)\n    flattenedLabelDefs.push([label.identifier, { ...label, locales: [] }])\n\n    if (aliases) {\n      for (const alias of aliases) {\n        knownValues.add(alias)\n        flattenedLabelDefs.push([\n          alias,\n          {\n            ...label,\n            identifier: alias,\n            locales: [],\n            comment: `@deprecated alias for \\`${label.identifier}\\``,\n          },\n        ])\n      }\n    }\n  }\n\n  let labelDefsStr = `{`\n  for (const [key, { comment, ...value }] of flattenedLabelDefs) {\n    const commentStr = comment ? `\\n/** ${comment} */\\n` : ''\n    labelDefsStr += `${commentStr}'${key}': ${JSON.stringify(value, null, 2)},`\n  }\n  labelDefsStr += `}`\n\n  return prettier.format(\n    `/** this doc is generated by ./scripts/code/labels.mjs **/\n  import {InterpretedLabelValueDefinition, LabelPreference} from '../types'\n\n  export type KnownLabelValue = ${Array.from(knownValues)\n    .map((value) => `\"${value}\"`)\n    .join(' | ')}\n\n  export const DEFAULT_LABEL_SETTINGS: Record<string, LabelPreference> = ${JSON.stringify(\n    Object.fromEntries(\n      labelsDef\n        .filter((label) => label.configurable)\n        .map((label) => [label.identifier, label.defaultSetting]),\n    ),\n  )}\n\n  export const LABELS: Record<KnownLabelValue, InterpretedLabelValueDefinition> = ${labelDefsStr}\n  `,\n    { semi: false, parser: 'typescript', singleQuote: true },\n  )\n}\n\nexport {}\n"
  },
  {
    "path": "packages/api/scripts/generate-code.mjs",
    "content": "import './code/labels.mjs'\n\nexport {}\n"
  },
  {
    "path": "packages/api/src/age-assurance.test.ts",
    "content": "import { describe, expect, it } from '@jest/globals'\nimport {\n  ageAssuranceRuleIDs,\n  computeAgeAssuranceRegionAccess,\n  getAgeAssuranceRegionConfig,\n} from './age-assurance'\nimport { AppBskyAgeassuranceDefs } from './client'\n\ndescribe('age-assurance', () => {\n  describe('getAgeAssuranceRegionConfig', () => {\n    const config: AppBskyAgeassuranceDefs.Config = {\n      regions: [\n        {\n          countryCode: 'US',\n          regionCode: 'CA',\n          minAccessAge: 13,\n          rules: [],\n        },\n        {\n          countryCode: 'US',\n          minAccessAge: 13,\n          rules: [],\n        },\n      ],\n    }\n\n    it('should find region by country code only', () => {\n      const result = getAgeAssuranceRegionConfig(config, {\n        countryCode: 'US',\n      })\n\n      expect(result).toEqual({\n        countryCode: 'US',\n        minAccessAge: 13,\n        rules: [],\n      })\n    })\n\n    it('should find region by country code and region code', () => {\n      const result = getAgeAssuranceRegionConfig(config, {\n        countryCode: 'US',\n        regionCode: 'CA',\n      })\n\n      expect(result).toEqual({\n        countryCode: 'US',\n        regionCode: 'CA',\n        minAccessAge: 13,\n        rules: [],\n      })\n    })\n\n    it('should return undefined when no matching region found', () => {\n      const result = getAgeAssuranceRegionConfig(config, {\n        countryCode: 'GB',\n      })\n\n      expect(result).toBeUndefined()\n    })\n  })\n\n  describe('computeAgeAssuranceRegionAccess', () => {\n    const region: AppBskyAgeassuranceDefs.ConfigRegion = {\n      countryCode: 'US',\n      minAccessAge: 13,\n      rules: [\n        {\n          $type: ageAssuranceRuleIDs.IfAccountNewerThan,\n          date: '2025-12-10T00:00:00Z',\n          access: 'none',\n        },\n        {\n          $type: ageAssuranceRuleIDs.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ageAssuranceRuleIDs.IfAssuredOverAge,\n          age: 16,\n          access: 'safe',\n        },\n        {\n          $type: ageAssuranceRuleIDs.IfDeclaredOverAge,\n          age: 16,\n          access: 'safe',\n        },\n        {\n          $type: ageAssuranceRuleIDs.Default,\n          access: 'none',\n        },\n      ],\n    }\n\n    it('should apply default if no data provided', () => {\n      const result = computeAgeAssuranceRegionAccess(region, {})\n\n      expect(result).toEqual({\n        access: 'none',\n        reason: ageAssuranceRuleIDs.Default,\n      })\n    })\n\n    describe('IfAccountNewerThan', () => {\n      it('should block accounts created after threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          accountCreatedAt: new Date(2025, 11, 15).toISOString(),\n          declaredAge: 18,\n        })\n        expect(result).toEqual({\n          access: 'none',\n          reason: ageAssuranceRuleIDs.IfAccountNewerThan,\n        })\n      })\n\n      it('should allow accounts created before threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          accountCreatedAt: new Date(2025, 10, 1).toISOString(),\n          declaredAge: 18,\n        })\n        expect(result).toEqual({\n          access: 'safe',\n          reason: ageAssuranceRuleIDs.IfDeclaredOverAge,\n        })\n      })\n\n      it('should allow accounts created exactly at threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          accountCreatedAt: new Date(2025, 11, 1).toISOString(),\n          declaredAge: 18,\n        })\n        expect(result).toEqual({\n          access: 'safe',\n          reason: ageAssuranceRuleIDs.IfDeclaredOverAge,\n        })\n      })\n\n      it('should not apply rule when accountCreatedAt is not provided', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          declaredAge: 15,\n        })\n        expect(result).toEqual({\n          access: 'none',\n          reason: ageAssuranceRuleIDs.Default,\n        })\n      })\n\n      it('should not apply rule when assuredAge is present', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          accountCreatedAt: new Date(2025, 11, 15).toISOString(),\n          assuredAge: 20,\n        })\n        expect(result).toEqual({\n          access: 'full',\n          reason: ageAssuranceRuleIDs.IfAssuredOverAge,\n        })\n      })\n    })\n\n    describe('IfDeclaredOverAge rule', () => {\n      it('should allow users at or above age threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          declaredAge: 18,\n        })\n\n        expect(result).toEqual({\n          access: 'safe',\n          reason: ageAssuranceRuleIDs.IfDeclaredOverAge,\n        })\n      })\n\n      it('should allow users above age threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          declaredAge: 25,\n        })\n\n        expect(result).toEqual({\n          access: 'safe',\n          reason: ageAssuranceRuleIDs.IfDeclaredOverAge,\n        })\n      })\n\n      it('should not allow users below age threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          declaredAge: 17,\n        })\n\n        expect(result).toEqual({\n          access: 'safe',\n          reason: ageAssuranceRuleIDs.IfDeclaredOverAge,\n        })\n      })\n    })\n\n    describe('IfAssuredOverAge rule', () => {\n      it('should allow users at or above assured age threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          assuredAge: 18,\n        })\n\n        expect(result).toEqual({\n          access: 'full',\n          reason: ageAssuranceRuleIDs.IfAssuredOverAge,\n        })\n      })\n\n      it('should not allow users below assured age threshold', () => {\n        const result = computeAgeAssuranceRegionAccess(region, {\n          assuredAge: 17,\n        })\n\n        expect(result).toEqual({\n          access: 'safe',\n          reason: ageAssuranceRuleIDs.IfAssuredOverAge,\n        })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/api/src/age-assurance.ts",
    "content": "import { AppBskyAgeassuranceDefs } from './client'\nimport { ids } from './client/lexicons'\n\nexport type AgeAssuranceRuleID = Exclude<\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleDefault['$type']\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleIfDeclaredOverAge['$type']\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleIfDeclaredUnderAge['$type']\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleIfAssuredOverAge['$type']\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleIfAssuredUnderAge['$type']\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleIfAccountNewerThan['$type']\n  | AppBskyAgeassuranceDefs.ConfigRegionRuleIfAccountOlderThan['$type'],\n  undefined\n>\n\nexport const ageAssuranceRuleIDs: Record<string, AgeAssuranceRuleID> = {\n  Default: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleDefault`,\n  IfDeclaredOverAge: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleIfDeclaredOverAge`,\n  IfDeclaredUnderAge: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleIfDeclaredUnderAge`,\n  IfAssuredOverAge: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleIfAssuredOverAge`,\n  IfAssuredUnderAge: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleIfAssuredUnderAge`,\n  IfAccountNewerThan: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleIfAccountNewerThan`,\n  IfAccountOlderThan: `${ids.AppBskyAgeassuranceDefs}#configRegionRuleIfAccountOlderThan`,\n}\n\n/**\n * Returns the first matched region configuration based on the provided geolocation.\n */\nexport function getAgeAssuranceRegionConfig(\n  config: AppBskyAgeassuranceDefs.Config,\n  geolocation: {\n    countryCode: string\n    regionCode?: string\n  },\n): AppBskyAgeassuranceDefs.ConfigRegion | undefined {\n  const { regions } = config\n  return regions.find(({ countryCode, regionCode }) => {\n    if (countryCode === geolocation.countryCode) {\n      return !regionCode || regionCode === geolocation.regionCode\n    }\n  })\n}\n\nexport function computeAgeAssuranceRegionAccess(\n  region: AppBskyAgeassuranceDefs.ConfigRegion,\n  data:\n    | {\n        /**\n         * The account creation date in ISO 8601 format. Only checked if we\n         * don't have an assured age, such as on the client.\n         */\n        accountCreatedAt?: string\n        /**\n         * The user's declared age\n         */\n        declaredAge?: number\n        /**\n         * The user's minimum age as assured by a trusted third party.\n         */\n        assuredAge?: number\n      }\n    | undefined,\n):\n  | {\n      access: AppBskyAgeassuranceDefs.Access\n      reason: AgeAssuranceRuleID\n    }\n  | undefined {\n  // first match wins\n  for (const rule of region.rules) {\n    if (AppBskyAgeassuranceDefs.isConfigRegionRuleIfAccountNewerThan(rule)) {\n      if (data?.accountCreatedAt && !data?.assuredAge) {\n        const accountCreatedAt = new Date(data.accountCreatedAt)\n        const threshold = new Date(rule.date)\n        if (accountCreatedAt >= threshold) {\n          return {\n            access: rule.access,\n            reason: rule.$type,\n          }\n        }\n      }\n    } else if (\n      AppBskyAgeassuranceDefs.isConfigRegionRuleIfAccountOlderThan(rule)\n    ) {\n      if (data?.accountCreatedAt && !data?.assuredAge) {\n        const accountCreatedAt = new Date(data.accountCreatedAt)\n        const threshold = new Date(rule.date)\n        if (accountCreatedAt < threshold) {\n          return {\n            access: rule.access,\n            reason: rule.$type,\n          }\n        }\n      }\n    } else if (\n      AppBskyAgeassuranceDefs.isConfigRegionRuleIfDeclaredOverAge(rule)\n    ) {\n      if (data?.declaredAge !== undefined && data.declaredAge >= rule.age) {\n        return {\n          access: rule.access,\n          reason: rule.$type,\n        }\n      }\n    } else if (\n      AppBskyAgeassuranceDefs.isConfigRegionRuleIfDeclaredUnderAge(rule)\n    ) {\n      if (data?.declaredAge !== undefined && data.declaredAge < rule.age) {\n        return {\n          access: rule.access,\n          reason: rule.$type,\n        }\n      }\n    } else if (\n      AppBskyAgeassuranceDefs.isConfigRegionRuleIfAssuredOverAge(rule)\n    ) {\n      if (data?.assuredAge && data.assuredAge >= rule.age) {\n        return {\n          access: rule.access,\n          reason: rule.$type,\n        }\n      }\n    } else if (\n      AppBskyAgeassuranceDefs.isConfigRegionRuleIfAssuredUnderAge(rule)\n    ) {\n      if (data?.assuredAge && data.assuredAge < rule.age) {\n        return {\n          access: rule.access,\n          reason: rule.$type,\n        }\n      }\n    } else if (AppBskyAgeassuranceDefs.isConfigRegionRuleDefault(rule)) {\n      return {\n        access: rule.access,\n        reason: rule.$type,\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/api/src/agent.ts",
    "content": "import AwaitLock from 'await-lock'\nimport { TID, retry } from '@atproto/common-web'\nimport { AtUri, ensureValidDid } from '@atproto/syntax'\nimport {\n  FetchHandler,\n  FetchHandlerOptions,\n  XrpcClient,\n  buildFetchHandler,\n} from '@atproto/xrpc'\nimport {\n  AppBskyActorDefs,\n  AppBskyActorProfile,\n  AppBskyFeedPost,\n  AppBskyLabelerDefs,\n  AppNS,\n  ChatNS,\n  ComAtprotoRepoPutRecord,\n  ComNS,\n  ToolsNS,\n} from './client/index'\nimport { schemas } from './client/lexicons'\nimport { MutedWord, Nux } from './client/types/app/bsky/actor/defs'\nimport { $Typed, Un$Typed } from './client/util'\nimport { BSKY_LABELER_DID } from './const'\nimport { interpretLabelValueDefinitions } from './moderation'\nimport { DEFAULT_LABEL_SETTINGS } from './moderation/const/labels'\nimport {\n  InterpretedLabelValueDefinition,\n  LabelPreference,\n  ModerationPrefs,\n} from './moderation/types'\nimport * as predicate from './predicate'\nimport { SessionManager } from './session-manager'\nimport {\n  AtpAgentGlobalOpts,\n  AtprotoProxy,\n  AtprotoServiceType,\n  BskyFeedViewPreference,\n  BskyInterestsPreference,\n  BskyPreferences,\n  BskyThreadViewPreference,\n  asAtprotoProxy,\n  asDid,\n  isDid,\n} from './types'\nimport {\n  getSavedFeedType,\n  sanitizeMutedWordValue,\n  savedFeedsToUriArrays,\n  validateNux,\n  validateSavedFeed,\n} from './util'\n\nconst FEED_VIEW_PREF_DEFAULTS = {\n  hideReplies: false,\n  hideRepliesByUnfollowed: true,\n  hideRepliesByLikeCount: 0,\n  hideReposts: false,\n  hideQuotePosts: false,\n}\n\nconst THREAD_VIEW_PREF_DEFAULTS = {\n  sort: 'hotness',\n}\n\nexport type { FetchHandler }\n\n/**\n * An {@link Agent} is an {@link AtpBaseClient} with the following\n * additional features:\n * - AT Protocol labelers configuration utilities\n * - AT Protocol proxy configuration utilities\n * - Cloning utilities\n * - `app.bsky` syntactic sugar\n * - `com.atproto` syntactic sugar\n */\nexport class Agent extends XrpcClient {\n  //#region Static configuration\n\n  /**\n   * The labelers to be used across all requests with the takedown capability\n   */\n  static appLabelers: readonly string[] = [BSKY_LABELER_DID]\n\n  /**\n   * Configures the Agent (or its sub classes) globally.\n   */\n  static configure(opts: AtpAgentGlobalOpts) {\n    if (opts.appLabelers) {\n      this.appLabelers = opts.appLabelers.map(asDid) // Validate & copy\n    }\n  }\n\n  //#endregion\n\n  com = new ComNS(this)\n  app = new AppNS(this)\n  chat = new ChatNS(this)\n  tools = new ToolsNS(this)\n\n  /** @deprecated use `this` instead */\n  get xrpc(): XrpcClient {\n    return this\n  }\n\n  readonly sessionManager: SessionManager\n\n  constructor(options: SessionManager | FetchHandler | FetchHandlerOptions) {\n    const sessionManager: SessionManager =\n      typeof options === 'object' && 'fetchHandler' in options\n        ? options\n        : {\n            did: undefined,\n            fetchHandler: buildFetchHandler(options),\n          }\n\n    super((url, init) => {\n      const headers = new Headers(init?.headers)\n\n      if (this.proxy && !headers.has('atproto-proxy')) {\n        headers.set('atproto-proxy', this.proxy)\n      }\n\n      // Merge the labelers header of this particular request with the app &\n      // instance labelers.\n      headers.set(\n        'atproto-accept-labelers',\n        [\n          ...this.appLabelers.map((l) => `${l};redact`),\n          ...this.labelers,\n          headers.get('atproto-accept-labelers')?.trim(),\n        ]\n          .filter(Boolean)\n          .join(', '),\n      )\n\n      return this.sessionManager.fetchHandler(url, { ...init, headers })\n    }, schemas)\n\n    this.sessionManager = sessionManager\n  }\n\n  //#region Cloning utilities\n\n  clone(): Agent {\n    return this.copyInto(new Agent(this.sessionManager))\n  }\n\n  copyInto<T extends Agent>(inst: T): T {\n    inst.configureLabelers(this.labelers)\n    inst.configureProxy(this.proxy ?? null)\n    inst.clearHeaders()\n    for (const [key, value] of this.headers) inst.setHeader(key, value)\n    return inst\n  }\n\n  withProxy(serviceType: AtprotoServiceType, did: string) {\n    const inst = this.clone()\n    inst.configureProxy(`${asDid(did)}#${serviceType}`)\n    return inst as ReturnType<this['clone']>\n  }\n\n  //#endregion\n\n  //#region ATPROTO labelers configuration utilities\n\n  /**\n   * The labelers statically configured on the class of the current instance.\n   */\n  get appLabelers() {\n    return (this.constructor as typeof Agent).appLabelers\n  }\n\n  labelers: readonly string[] = []\n\n  configureLabelers(labelerDids: readonly string[]) {\n    this.labelers = labelerDids.map(asDid) // Validate & copy\n  }\n\n  /** @deprecated use {@link configureLabelers} instead */\n  configureLabelersHeader(labelerDids: readonly string[]) {\n    // Filtering non-did values for backwards compatibility\n    this.configureLabelers(labelerDids.filter(isDid))\n  }\n\n  //#endregion\n\n  //#region ATPROTO proxy configuration utilities\n\n  proxy?: AtprotoProxy\n\n  configureProxy(value: AtprotoProxy | null) {\n    if (value === null) this.proxy = undefined\n    else this.proxy = asAtprotoProxy(value)\n  }\n\n  /** @deprecated use {@link configureProxy} instead */\n  configureProxyHeader(serviceType: AtprotoServiceType, did: string) {\n    // Ignoring non-did values for backwards compatibility\n    if (isDid(did)) this.configureProxy(`${did}#${serviceType}`)\n  }\n\n  //#endregion\n\n  //#region Session management\n\n  /**\n   * Get the authenticated user's DID, if any.\n   */\n  get did() {\n    return this.sessionManager.did\n  }\n\n  /** @deprecated Use {@link Agent.assertDid} instead */\n  get accountDid() {\n    return this.assertDid\n  }\n\n  /**\n   * Get the authenticated user's DID, or throw an error if not authenticated.\n   */\n  get assertDid(): string {\n    this.assertAuthenticated()\n    return this.did\n  }\n\n  /**\n   * Assert that the user is authenticated.\n   */\n  public assertAuthenticated(): asserts this is { did: string } {\n    if (!this.did) throw new Error('Not logged in')\n  }\n\n  //#endregion\n\n  /** @deprecated use \"this\" instead */\n  get api() {\n    return this\n  }\n\n  //#region \"com.atproto\" lexicon short hand methods\n\n  /**\n   * Upload a binary blob to the server\n   */\n  uploadBlob: typeof this.com.atproto.repo.uploadBlob = (data, opts) =>\n    this.com.atproto.repo.uploadBlob(data, opts)\n\n  /**\n   * Resolve a handle to a DID\n   */\n  resolveHandle: typeof this.com.atproto.identity.resolveHandle = (\n    params,\n    opts,\n  ) => this.com.atproto.identity.resolveHandle(params, opts)\n\n  /**\n   * Change the user's handle\n   */\n  updateHandle: typeof this.com.atproto.identity.updateHandle = (data, opts) =>\n    this.com.atproto.identity.updateHandle(data, opts)\n\n  /**\n   * Create a moderation report\n   */\n  createModerationReport: typeof this.com.atproto.moderation.createReport = (\n    data,\n    opts,\n  ) => this.com.atproto.moderation.createReport(data, opts)\n\n  //#endregion\n\n  //#region \"app.bsky\" lexicon short hand methods\n\n  getTimeline: typeof this.app.bsky.feed.getTimeline = (params, opts) =>\n    this.app.bsky.feed.getTimeline(params, opts)\n\n  getAuthorFeed: typeof this.app.bsky.feed.getAuthorFeed = (params, opts) =>\n    this.app.bsky.feed.getAuthorFeed(params, opts)\n\n  getActorLikes: typeof this.app.bsky.feed.getActorLikes = (params, opts) =>\n    this.app.bsky.feed.getActorLikes(params, opts)\n\n  getPostThread: typeof this.app.bsky.feed.getPostThread = (params, opts) =>\n    this.app.bsky.feed.getPostThread(params, opts)\n\n  getPost: typeof this.app.bsky.feed.post.get = (params) =>\n    this.app.bsky.feed.post.get(params)\n\n  getPosts: typeof this.app.bsky.feed.getPosts = (params, opts) =>\n    this.app.bsky.feed.getPosts(params, opts)\n\n  getLikes: typeof this.app.bsky.feed.getLikes = (params, opts) =>\n    this.app.bsky.feed.getLikes(params, opts)\n\n  getRepostedBy: typeof this.app.bsky.feed.getRepostedBy = (params, opts) =>\n    this.app.bsky.feed.getRepostedBy(params, opts)\n\n  getFollows: typeof this.app.bsky.graph.getFollows = (params, opts) =>\n    this.app.bsky.graph.getFollows(params, opts)\n\n  getFollowers: typeof this.app.bsky.graph.getFollowers = (params, opts) =>\n    this.app.bsky.graph.getFollowers(params, opts)\n\n  getProfile: typeof this.app.bsky.actor.getProfile = (params, opts) =>\n    this.app.bsky.actor.getProfile(params, opts)\n\n  getProfiles: typeof this.app.bsky.actor.getProfiles = (params, opts) =>\n    this.app.bsky.actor.getProfiles(params, opts)\n\n  getSuggestions: typeof this.app.bsky.actor.getSuggestions = (params, opts) =>\n    this.app.bsky.actor.getSuggestions(params, opts)\n\n  searchActors: typeof this.app.bsky.actor.searchActors = (params, opts) =>\n    this.app.bsky.actor.searchActors(params, opts)\n\n  searchActorsTypeahead: typeof this.app.bsky.actor.searchActorsTypeahead = (\n    params,\n    opts,\n  ) => this.app.bsky.actor.searchActorsTypeahead(params, opts)\n\n  listNotifications: typeof this.app.bsky.notification.listNotifications = (\n    params,\n    opts,\n  ) => this.app.bsky.notification.listNotifications(params, opts)\n\n  countUnreadNotifications: typeof this.app.bsky.notification.getUnreadCount = (\n    params,\n    opts,\n  ) => this.app.bsky.notification.getUnreadCount(params, opts)\n\n  getLabelers: typeof this.app.bsky.labeler.getServices = (params, opts) =>\n    this.app.bsky.labeler.getServices(params, opts)\n\n  async getLabelDefinitions(\n    prefs: BskyPreferences | ModerationPrefs | string[],\n  ): Promise<Record<string, InterpretedLabelValueDefinition[]>> {\n    // collect the labeler dids\n    const dids: string[] = [...this.appLabelers]\n    if (isBskyPrefs(prefs)) {\n      dids.push(...prefs.moderationPrefs.labelers.map((l) => l.did))\n    } else if (isModPrefs(prefs)) {\n      dids.push(...prefs.labelers.map((l) => l.did))\n    } else {\n      dids.push(...prefs)\n    }\n\n    // fetch their definitions\n    const labelers = await this.getLabelers({\n      dids,\n      detailed: true,\n    })\n\n    // assemble a map of labeler dids to the interpreted label value definitions\n    const labelDefs = {}\n    if (labelers.data) {\n      for (const labeler of labelers.data\n        .views as AppBskyLabelerDefs.LabelerViewDetailed[]) {\n        labelDefs[labeler.creator.did] = interpretLabelValueDefinitions(labeler)\n      }\n    }\n\n    return labelDefs\n  }\n\n  async post(\n    record: Partial<AppBskyFeedPost.Record> &\n      Omit<AppBskyFeedPost.Record, 'createdAt'>,\n  ) {\n    record.createdAt ||= new Date().toISOString()\n    return this.app.bsky.feed.post.create(\n      { repo: this.accountDid },\n      record as AppBskyFeedPost.Record,\n    )\n  }\n\n  async deletePost(postUri: string) {\n    this.assertAuthenticated()\n\n    const postUrip = new AtUri(postUri)\n    return this.app.bsky.feed.post.delete({\n      repo: postUrip.hostname,\n      rkey: postUrip.rkey,\n    })\n  }\n\n  async like(uri: string, cid: string, via?: { uri: string; cid: string }) {\n    return this.app.bsky.feed.like.create(\n      { repo: this.accountDid },\n      {\n        subject: { uri, cid },\n        createdAt: new Date().toISOString(),\n        via,\n      },\n    )\n  }\n\n  async deleteLike(likeUri: string) {\n    this.assertAuthenticated()\n\n    const likeUrip = new AtUri(likeUri)\n    return this.app.bsky.feed.like.delete({\n      repo: likeUrip.hostname,\n      rkey: likeUrip.rkey,\n    })\n  }\n\n  async repost(uri: string, cid: string, via?: { uri: string; cid: string }) {\n    return this.app.bsky.feed.repost.create(\n      { repo: this.accountDid },\n      {\n        subject: { uri, cid },\n        createdAt: new Date().toISOString(),\n        via,\n      },\n    )\n  }\n\n  async deleteRepost(repostUri: string) {\n    this.assertAuthenticated()\n\n    const repostUrip = new AtUri(repostUri)\n    return this.app.bsky.feed.repost.delete({\n      repo: repostUrip.hostname,\n      rkey: repostUrip.rkey,\n    })\n  }\n\n  async follow(subjectDid: string, via?: { uri: string; cid: string }) {\n    return this.app.bsky.graph.follow.create(\n      { repo: this.accountDid },\n      {\n        subject: subjectDid,\n        createdAt: new Date().toISOString(),\n        via,\n      },\n    )\n  }\n\n  async deleteFollow(followUri: string) {\n    this.assertAuthenticated()\n\n    const followUrip = new AtUri(followUri)\n    return this.app.bsky.graph.follow.delete({\n      repo: followUrip.hostname,\n      rkey: followUrip.rkey,\n    })\n  }\n\n  /**\n   * @note: Using this method will reset the whole profile record if it\n   * previously contained invalid values (wrt to the profile lexicon).\n   */\n  async upsertProfile(\n    updateFn: (\n      existing: AppBskyActorProfile.Record | undefined,\n    ) =>\n      | Un$Typed<AppBskyActorProfile.Record>\n      | Promise<Un$Typed<AppBskyActorProfile.Record>>,\n  ): Promise<void> {\n    const upsert = async () => {\n      const repo = this.assertDid\n      const collection = 'app.bsky.actor.profile'\n\n      const existing = await this.com.atproto.repo\n        .getRecord({ repo, collection, rkey: 'self' })\n        .catch((_) => undefined)\n\n      const existingRecord: AppBskyActorProfile.Record | undefined =\n        existing && predicate.isValidProfile(existing.data.value)\n          ? existing.data.value\n          : undefined\n\n      // run the update\n      const updated = await updateFn(existingRecord)\n\n      // validate the value returned by the update function\n      const validation = AppBskyActorProfile.validateRecord({\n        $type: collection,\n        ...updated,\n      })\n\n      if (!validation.success) {\n        throw validation.error\n      }\n\n      await this.com.atproto.repo.putRecord({\n        repo,\n        collection,\n        rkey: 'self',\n        record: validation.value,\n        swapRecord: existing?.data.cid || null,\n      })\n    }\n\n    return retry(upsert, {\n      maxRetries: 5,\n      retryable: (e) => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError,\n    })\n  }\n\n  async mute(actor: string) {\n    return this.app.bsky.graph.muteActor({ actor })\n  }\n\n  async unmute(actor: string) {\n    return this.app.bsky.graph.unmuteActor({ actor })\n  }\n\n  async muteModList(uri: string) {\n    return this.app.bsky.graph.muteActorList({ list: uri })\n  }\n\n  async unmuteModList(uri: string) {\n    return this.app.bsky.graph.unmuteActorList({ list: uri })\n  }\n\n  async blockModList(uri: string) {\n    return this.app.bsky.graph.listblock.create(\n      { repo: this.accountDid },\n      {\n        subject: uri,\n        createdAt: new Date().toISOString(),\n      },\n    )\n  }\n\n  async unblockModList(uri: string) {\n    const repo = this.accountDid\n\n    const listInfo = await this.app.bsky.graph.getList({\n      list: uri,\n      limit: 1,\n    })\n\n    const blocked = listInfo.data.list.viewer?.blocked\n    if (blocked) {\n      const { rkey } = new AtUri(blocked)\n      return this.app.bsky.graph.listblock.delete({\n        repo,\n        rkey,\n      })\n    }\n  }\n\n  async updateSeenNotifications(seenAt = new Date().toISOString()) {\n    return this.app.bsky.notification.updateSeen({ seenAt })\n  }\n\n  async getPreferences(): Promise<BskyPreferences> {\n    const prefs: BskyPreferences = {\n      feeds: {\n        saved: undefined,\n        pinned: undefined,\n      },\n      // @ts-ignore populating below\n      savedFeeds: undefined,\n      feedViewPrefs: {\n        home: {\n          ...FEED_VIEW_PREF_DEFAULTS,\n        },\n      },\n      threadViewPrefs: { ...THREAD_VIEW_PREF_DEFAULTS },\n      moderationPrefs: {\n        adultContentEnabled: false,\n        labels: { ...DEFAULT_LABEL_SETTINGS },\n        labelers: this.appLabelers.map((did) => ({\n          did,\n          labels: {},\n        })),\n        mutedWords: [],\n        hiddenPosts: [],\n      },\n      birthDate: undefined,\n      interests: {\n        tags: [],\n      },\n      bskyAppState: {\n        queuedNudges: [],\n        activeProgressGuide: undefined,\n        nuxs: [],\n      },\n      postInteractionSettings: {\n        threadgateAllowRules: undefined,\n        postgateEmbeddingRules: undefined,\n      },\n      verificationPrefs: {\n        hideBadges: false,\n      },\n      liveEventPreferences: {\n        hiddenFeedIds: [],\n        hideAllFeeds: false,\n      },\n    }\n    const res = await this.app.bsky.actor.getPreferences({})\n    const labelPrefs: AppBskyActorDefs.ContentLabelPref[] = []\n    for (const pref of res.data.preferences) {\n      if (predicate.isValidAdultContentPref(pref)) {\n        // adult content preferences\n        prefs.moderationPrefs.adultContentEnabled = pref.enabled\n      } else if (predicate.isValidContentLabelPref(pref)) {\n        // content label preference\n        const adjustedPref = adjustLegacyContentLabelPref(pref)\n        labelPrefs.push(adjustedPref)\n      } else if (predicate.isValidLabelersPref(pref)) {\n        // labelers preferences\n        prefs.moderationPrefs.labelers = this.appLabelers\n          .map((did: string) => ({ did, labels: {} }))\n          .concat(\n            pref.labelers.map((labeler) => ({\n              ...labeler,\n              labels: {},\n            })),\n          )\n      } else if (predicate.isValidSavedFeedsPrefV2(pref)) {\n        prefs.savedFeeds = pref.items\n      } else if (predicate.isValidSavedFeedsPref(pref)) {\n        // saved and pinned feeds\n        prefs.feeds.saved = pref.saved\n        prefs.feeds.pinned = pref.pinned\n      } else if (predicate.isValidPersonalDetailsPref(pref)) {\n        // birth date (irl)\n        if (pref.birthDate) {\n          prefs.birthDate = new Date(pref.birthDate)\n        }\n      } else if (predicate.isValidDeclaredAgePref(pref)) {\n        const { $type: _, ...declaredAgePref } = pref\n        prefs.declaredAge = declaredAgePref\n      } else if (predicate.isValidFeedViewPref(pref)) {\n        // feed view preferences\n        const { $type: _, feed, ...v } = pref\n        prefs.feedViewPrefs[feed] = { ...FEED_VIEW_PREF_DEFAULTS, ...v }\n      } else if (predicate.isValidThreadViewPref(pref)) {\n        // thread view preferences\n        const { $type: _, ...v } = pref\n        prefs.threadViewPrefs = { ...prefs.threadViewPrefs, ...v }\n      } else if (predicate.isValidInterestsPref(pref)) {\n        const { $type: _, ...v } = pref\n        prefs.interests = { ...prefs.interests, ...v }\n      } else if (predicate.isValidMutedWordsPref(pref)) {\n        prefs.moderationPrefs.mutedWords = pref.items\n\n        if (prefs.moderationPrefs.mutedWords.length) {\n          prefs.moderationPrefs.mutedWords =\n            prefs.moderationPrefs.mutedWords.map((word) => {\n              word.actorTarget = word.actorTarget || 'all'\n              return word\n            })\n        }\n      } else if (predicate.isValidHiddenPostsPref(pref)) {\n        prefs.moderationPrefs.hiddenPosts = pref.items\n      } else if (predicate.isValidBskyAppStatePref(pref)) {\n        prefs.bskyAppState.queuedNudges = pref.queuedNudges || []\n        prefs.bskyAppState.activeProgressGuide = pref.activeProgressGuide\n        prefs.bskyAppState.nuxs = pref.nuxs || []\n      } else if (predicate.isValidPostInteractionSettingsPref(pref)) {\n        prefs.postInteractionSettings.threadgateAllowRules =\n          pref.threadgateAllowRules\n        prefs.postInteractionSettings.postgateEmbeddingRules =\n          pref.postgateEmbeddingRules\n      } else if (predicate.isValidVerificationPrefs(pref)) {\n        prefs.verificationPrefs = {\n          hideBadges: pref.hideBadges,\n        }\n      } else if (predicate.isValidLiveEventPreferences(pref)) {\n        prefs.liveEventPreferences = {\n          hiddenFeedIds: pref.hiddenFeedIds || [],\n          hideAllFeeds: pref.hideAllFeeds ?? false,\n        }\n      }\n    }\n\n    /*\n     * If `prefs.savedFeeds` is undefined, no `savedFeedsPrefV2` exists, which\n     * means we want to try to migrate if needed.\n     *\n     * If v1 prefs exist, they will be migrated to v2.\n     *\n     * If no v1 prefs exist, the user is either new, or could be old and has\n     * never edited their feeds.\n     */\n    if (prefs.savedFeeds == null) {\n      const { saved, pinned } = prefs.feeds\n\n      if (saved && pinned) {\n        const uniqueMigratedSavedFeeds: Map<\n          string,\n          AppBskyActorDefs.SavedFeed\n        > = new Map()\n\n        // insert Following feed first\n        uniqueMigratedSavedFeeds.set('timeline', {\n          id: TID.nextStr(),\n          type: 'timeline',\n          value: 'following',\n          pinned: true,\n        })\n\n        // use pinned as source of truth for feed order\n        for (const uri of pinned) {\n          const type = getSavedFeedType(uri)\n          // only want supported types\n          if (type === 'unknown') continue\n          uniqueMigratedSavedFeeds.set(uri, {\n            id: TID.nextStr(),\n            type,\n            value: uri,\n            pinned: true,\n          })\n        }\n\n        for (const uri of saved) {\n          if (!uniqueMigratedSavedFeeds.has(uri)) {\n            const type = getSavedFeedType(uri)\n            // only want supported types\n            if (type === 'unknown') continue\n            uniqueMigratedSavedFeeds.set(uri, {\n              id: TID.nextStr(),\n              type,\n              value: uri,\n              pinned: false,\n            })\n          }\n        }\n\n        prefs.savedFeeds = Array.from(uniqueMigratedSavedFeeds.values())\n      } else {\n        prefs.savedFeeds = [\n          {\n            id: TID.nextStr(),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ]\n      }\n\n      // save to user preferences so this migration doesn't re-occur\n      await this.overwriteSavedFeeds(prefs.savedFeeds)\n    }\n\n    // apply the label prefs\n    for (const pref of labelPrefs) {\n      if (pref.labelerDid) {\n        const labeler = prefs.moderationPrefs.labelers.find(\n          (labeler) => labeler.did === pref.labelerDid,\n        )\n        if (!labeler) continue\n        labeler.labels[pref.label] = pref.visibility as LabelPreference\n      } else {\n        prefs.moderationPrefs.labels[pref.label] =\n          pref.visibility as LabelPreference\n      }\n    }\n\n    prefs.moderationPrefs.labels = remapLegacyLabels(\n      prefs.moderationPrefs.labels,\n    )\n\n    // automatically configure the client\n    this.configureLabelers(prefsArrayToLabelerDids(res.data.preferences))\n\n    return prefs\n  }\n\n  async overwriteSavedFeeds(savedFeeds: AppBskyActorDefs.SavedFeed[]) {\n    savedFeeds.forEach(validateSavedFeed)\n    const uniqueSavedFeeds = new Map<string, AppBskyActorDefs.SavedFeed>()\n    savedFeeds.forEach((feed) => {\n      // remove and re-insert to preserve order\n      if (uniqueSavedFeeds.has(feed.id)) {\n        uniqueSavedFeeds.delete(feed.id)\n      }\n      uniqueSavedFeeds.set(feed.id, feed)\n    })\n    return this.updateSavedFeedsV2Preferences(() =>\n      Array.from(uniqueSavedFeeds.values()),\n    )\n  }\n\n  async updateSavedFeeds(savedFeedsToUpdate: AppBskyActorDefs.SavedFeed[]) {\n    savedFeedsToUpdate.map(validateSavedFeed)\n    return this.updateSavedFeedsV2Preferences((savedFeeds) => {\n      return savedFeeds.map((savedFeed) => {\n        const updatedVersion = savedFeedsToUpdate.find(\n          (updated) => savedFeed.id === updated.id,\n        )\n        if (updatedVersion) {\n          return {\n            ...savedFeed,\n            // only update pinned\n            pinned: updatedVersion.pinned,\n          }\n        }\n        return savedFeed\n      })\n    })\n  }\n\n  async addSavedFeeds(\n    savedFeeds: Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[],\n  ) {\n    const toSave: AppBskyActorDefs.SavedFeed[] = savedFeeds.map((f) => ({\n      ...f,\n      id: TID.nextStr(),\n    }))\n    toSave.forEach(validateSavedFeed)\n    return this.updateSavedFeedsV2Preferences((savedFeeds) => [\n      ...savedFeeds,\n      ...toSave,\n    ])\n  }\n\n  async removeSavedFeeds(ids: string[]) {\n    return this.updateSavedFeedsV2Preferences((savedFeeds) => [\n      ...savedFeeds.filter((feed) => !ids.find((id) => feed.id === id)),\n    ])\n  }\n\n  /**\n   * @deprecated use `overwriteSavedFeeds`\n   */\n  async setSavedFeeds(saved: string[], pinned: string[]) {\n    return this.updateFeedPreferences(() => ({\n      saved,\n      pinned,\n    }))\n  }\n\n  /**\n   * @deprecated use `addSavedFeeds`\n   */\n  async addSavedFeed(v: string) {\n    return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({\n      saved: [...saved.filter((uri) => uri !== v), v],\n      pinned,\n    }))\n  }\n\n  /**\n   * @deprecated use `removeSavedFeeds`\n   */\n  async removeSavedFeed(v: string) {\n    return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({\n      saved: saved.filter((uri) => uri !== v),\n      pinned: pinned.filter((uri) => uri !== v),\n    }))\n  }\n\n  /**\n   * @deprecated use `addSavedFeeds` or `updateSavedFeeds`\n   */\n  async addPinnedFeed(v: string) {\n    return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({\n      saved: [...saved.filter((uri) => uri !== v), v],\n      pinned: [...pinned.filter((uri) => uri !== v), v],\n    }))\n  }\n\n  /**\n   * @deprecated use `updateSavedFeeds` or `removeSavedFeeds`\n   */\n  async removePinnedFeed(v: string) {\n    return this.updateFeedPreferences((saved: string[], pinned: string[]) => ({\n      saved,\n      pinned: pinned.filter((uri) => uri !== v),\n    }))\n  }\n\n  async setAdultContentEnabled(v: boolean) {\n    await this.updatePreferences((prefs) => {\n      const adultContentPref = prefs.findLast(\n        predicate.isValidAdultContentPref,\n      ) || {\n        $type: 'app.bsky.actor.defs#adultContentPref',\n        enabled: v,\n      }\n\n      adultContentPref.enabled = v\n\n      return prefs\n        .filter((pref) => !AppBskyActorDefs.isAdultContentPref(pref))\n        .concat(adultContentPref)\n    })\n  }\n\n  async setContentLabelPref(\n    key: string,\n    value: LabelPreference,\n    labelerDid?: string,\n  ) {\n    if (labelerDid) {\n      ensureValidDid(labelerDid)\n    }\n    await this.updatePreferences((prefs) => {\n      const labelPref = prefs\n        .filter(predicate.isValidContentLabelPref)\n        .findLast(\n          (pref) => pref.label === key && pref.labelerDid === labelerDid,\n        ) || {\n        $type: 'app.bsky.actor.defs#contentLabelPref',\n        label: key,\n        labelerDid,\n        visibility: value,\n      }\n\n      labelPref.visibility = value\n\n      let legacyLabelPref: $Typed<AppBskyActorDefs.ContentLabelPref> | undefined\n      if (AppBskyActorDefs.isContentLabelPref(labelPref)) {\n        // is global\n        if (!labelPref.labelerDid) {\n          const legacyLabelValue = {\n            'graphic-media': 'gore',\n            porn: 'nsfw',\n            sexual: 'suggestive',\n            // Protect against using toString, hasOwnProperty, etc. as a label:\n            __proto__: null,\n          }[labelPref.label]\n\n          // if it's a legacy label, double-write the legacy label\n          if (legacyLabelValue) {\n            legacyLabelPref = prefs\n              .filter(predicate.isValidContentLabelPref)\n              .findLast(\n                (pref) =>\n                  pref.label === legacyLabelValue &&\n                  pref.labelerDid === undefined,\n              ) || {\n              $type: 'app.bsky.actor.defs#contentLabelPref',\n              label: legacyLabelValue,\n              labelerDid: undefined,\n              visibility: value,\n            }\n\n            legacyLabelPref!.visibility = value\n          }\n        }\n      }\n\n      return prefs\n        .filter(\n          (pref) =>\n            !AppBskyActorDefs.isContentLabelPref(pref) ||\n            !(pref.label === key && pref.labelerDid === labelerDid),\n        )\n        .concat(labelPref)\n        .filter((pref) => {\n          if (!legacyLabelPref) return true\n          return (\n            !AppBskyActorDefs.isContentLabelPref(pref) ||\n            !(\n              pref.label === legacyLabelPref.label &&\n              pref.labelerDid === undefined\n            )\n          )\n        })\n        .concat(legacyLabelPref ? [legacyLabelPref] : [])\n    })\n  }\n\n  async addLabeler(did: string) {\n    const prefs = await this.updatePreferences((prefs) => {\n      const labelersPref = prefs.findLast(predicate.isValidLabelersPref) || {\n        $type: 'app.bsky.actor.defs#labelersPref',\n        labelers: [],\n      }\n\n      if (!labelersPref.labelers.some((labeler) => labeler.did === did)) {\n        labelersPref.labelers.push({ did })\n      }\n\n      return prefs\n        .filter((pref) => !AppBskyActorDefs.isLabelersPref(pref))\n        .concat(labelersPref)\n    })\n    // automatically configure the client\n    this.configureLabelers(prefsArrayToLabelerDids(prefs))\n  }\n\n  async removeLabeler(did: string) {\n    const prefs = await this.updatePreferences((prefs) => {\n      const labelersPref = prefs.findLast(predicate.isValidLabelersPref) || {\n        $type: 'app.bsky.actor.defs#labelersPref',\n        labelers: [],\n      }\n\n      labelersPref.labelers = labelersPref.labelers.filter((l) => l.did !== did)\n\n      return prefs\n        .filter((pref) => !AppBskyActorDefs.isLabelersPref(pref))\n        .concat(labelersPref)\n    })\n    // automatically configure the client\n    this.configureLabelers(prefsArrayToLabelerDids(prefs))\n  }\n\n  async setPersonalDetails({\n    birthDate,\n  }: {\n    birthDate: string | Date | undefined\n  }) {\n    await this.updatePreferences((prefs) => {\n      const personalDetailsPref = prefs.findLast(\n        predicate.isValidPersonalDetailsPref,\n      ) || {\n        $type: 'app.bsky.actor.defs#personalDetailsPref',\n      }\n\n      personalDetailsPref.birthDate =\n        birthDate instanceof Date ? birthDate.toISOString() : birthDate\n\n      return prefs\n        .filter((pref) => !AppBskyActorDefs.isPersonalDetailsPref(pref))\n        .concat(personalDetailsPref)\n    })\n  }\n\n  async setFeedViewPrefs(feed: string, pref: Partial<BskyFeedViewPreference>) {\n    await this.updatePreferences((prefs) => {\n      const existing = prefs\n        .filter(predicate.isValidFeedViewPref)\n        .findLast((pref) => pref.feed === feed)\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isFeedViewPref(p) || p.feed !== feed)\n        .concat({\n          ...existing,\n          ...pref,\n          $type: 'app.bsky.actor.defs#feedViewPref',\n          feed,\n        })\n    })\n  }\n\n  async setThreadViewPrefs(pref: Partial<BskyThreadViewPreference>) {\n    await this.updatePreferences((prefs) => {\n      const existing = prefs.findLast(predicate.isValidThreadViewPref)\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isThreadViewPref(p))\n        .concat({\n          ...existing,\n          ...pref,\n          $type: 'app.bsky.actor.defs#threadViewPref',\n        })\n    })\n  }\n\n  async setInterestsPref(pref: Partial<BskyInterestsPreference>) {\n    await this.updatePreferences((prefs) => {\n      const existing = prefs.findLast(predicate.isValidInterestsPref)\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isInterestsPref(p))\n        .concat({\n          ...existing,\n          ...pref,\n          $type: 'app.bsky.actor.defs#interestsPref',\n        })\n    })\n  }\n\n  /**\n   * Add a muted word to user preferences.\n   */\n  async addMutedWord(\n    mutedWord: Pick<\n      MutedWord,\n      'value' | 'targets' | 'actorTarget' | 'expiresAt'\n    >,\n  ) {\n    const sanitizedValue = sanitizeMutedWordValue(mutedWord.value)\n\n    if (!sanitizedValue) return\n\n    await this.updatePreferences((prefs) => {\n      let mutedWordsPref = prefs.findLast(predicate.isValidMutedWordsPref)\n\n      const newMutedWord: AppBskyActorDefs.MutedWord = {\n        id: TID.nextStr(),\n        value: sanitizedValue,\n        targets: mutedWord.targets || [],\n        actorTarget: mutedWord.actorTarget || 'all',\n        expiresAt: mutedWord.expiresAt || undefined,\n      }\n\n      if (mutedWordsPref) {\n        mutedWordsPref.items.push(newMutedWord)\n\n        /**\n         * Migrate any old muted words that don't have an id\n         */\n        mutedWordsPref.items = migrateLegacyMutedWordsItems(\n          mutedWordsPref.items,\n        )\n      } else {\n        // if the pref doesn't exist, create it\n        mutedWordsPref = {\n          $type: 'app.bsky.actor.defs#mutedWordsPref',\n          items: [newMutedWord],\n        }\n      }\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))\n        .concat(mutedWordsPref)\n    })\n  }\n\n  /**\n   * Convenience method to add muted words to user preferences\n   */\n  async addMutedWords(newMutedWords: AppBskyActorDefs.MutedWord[]) {\n    await Promise.all(newMutedWords.map((word) => this.addMutedWord(word)))\n  }\n\n  /**\n   * @deprecated use `addMutedWords` or `addMutedWord` instead\n   */\n  async upsertMutedWords(\n    mutedWords: Pick<\n      MutedWord,\n      'value' | 'targets' | 'actorTarget' | 'expiresAt'\n    >[],\n  ) {\n    await this.addMutedWords(mutedWords)\n  }\n\n  /**\n   * Update a muted word in user preferences.\n   */\n  async updateMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {\n    await this.updatePreferences((prefs) => {\n      const mutedWordsPref = prefs.findLast(predicate.isValidMutedWordsPref)\n\n      if (mutedWordsPref) {\n        mutedWordsPref.items = mutedWordsPref.items.map((existingItem) => {\n          const match = matchMutedWord(existingItem, mutedWord)\n\n          if (match) {\n            const updated = {\n              ...existingItem,\n              ...mutedWord,\n            }\n            return {\n              id: existingItem.id || TID.nextStr(),\n              value:\n                sanitizeMutedWordValue(updated.value) || existingItem.value,\n              targets: updated.targets || [],\n              actorTarget: updated.actorTarget || 'all',\n              expiresAt: updated.expiresAt || undefined,\n            }\n          } else {\n            return existingItem\n          }\n        })\n\n        /**\n         * Migrate any old muted words that don't have an id\n         */\n        mutedWordsPref.items = migrateLegacyMutedWordsItems(\n          mutedWordsPref.items,\n        )\n\n        return prefs\n          .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))\n          .concat(mutedWordsPref)\n      }\n\n      return prefs\n    })\n  }\n\n  /**\n   * Remove a muted word from user preferences.\n   */\n  async removeMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {\n    await this.updatePreferences((prefs) => {\n      const mutedWordsPref = prefs.findLast(predicate.isValidMutedWordsPref)\n\n      if (mutedWordsPref) {\n        for (let i = 0; i < mutedWordsPref.items.length; i++) {\n          const match = matchMutedWord(mutedWordsPref.items[i], mutedWord)\n\n          if (match) {\n            mutedWordsPref.items.splice(i, 1)\n            break\n          }\n        }\n\n        /**\n         * Migrate any old muted words that don't have an id\n         */\n        mutedWordsPref.items = migrateLegacyMutedWordsItems(\n          mutedWordsPref.items,\n        )\n\n        return prefs\n          .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))\n          .concat(mutedWordsPref)\n      }\n\n      return prefs\n    })\n  }\n\n  /**\n   * Convenience method to remove muted words from user preferences\n   */\n  async removeMutedWords(mutedWords: AppBskyActorDefs.MutedWord[]) {\n    await Promise.all(mutedWords.map((word) => this.removeMutedWord(word)))\n  }\n\n  async hidePost(postUri: string) {\n    await this.updateHiddenPost(postUri, 'hide')\n  }\n\n  async unhidePost(postUri: string) {\n    await this.updateHiddenPost(postUri, 'unhide')\n  }\n\n  async bskyAppQueueNudges(nudges: string | string[]) {\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {\n        $type: 'app.bsky.actor.defs#bskyAppStatePref',\n      }\n\n      pref.queuedNudges = (pref.queuedNudges || []).concat(nudges)\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))\n        .concat(pref)\n    })\n  }\n\n  async bskyAppDismissNudges(nudges: string | string[]) {\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {\n        $type: 'app.bsky.actor.defs#bskyAppStatePref',\n      }\n\n      nudges = Array.isArray(nudges) ? nudges : [nudges]\n      pref.queuedNudges = (pref.queuedNudges || []).filter(\n        (nudge) => !nudges.includes(nudge),\n      )\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))\n        .concat(pref)\n    })\n  }\n\n  async bskyAppSetActiveProgressGuide(\n    guide: AppBskyActorDefs.BskyAppProgressGuide | undefined,\n  ) {\n    if (guide) {\n      const result = AppBskyActorDefs.validateBskyAppProgressGuide(guide)\n      if (!result.success) throw result.error\n    }\n\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {\n        $type: 'app.bsky.actor.defs#bskyAppStatePref',\n      }\n\n      pref.activeProgressGuide = guide\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))\n        .concat(pref)\n    })\n  }\n\n  /**\n   * Insert or update a NUX in user prefs\n   */\n  async bskyAppUpsertNux(nux: Nux) {\n    validateNux(nux)\n\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {\n        $type: 'app.bsky.actor.defs#bskyAppStatePref',\n      }\n\n      pref.nuxs = pref.nuxs || []\n\n      const existing = pref.nuxs?.find((n) => {\n        return n.id === nux.id\n      })\n\n      let next: AppBskyActorDefs.Nux\n\n      if (existing) {\n        next = {\n          id: existing.id,\n          completed: nux.completed,\n          data: nux.data,\n          expiresAt: nux.expiresAt,\n        }\n      } else {\n        next = nux\n      }\n\n      // remove duplicates and append\n      pref.nuxs = pref.nuxs.filter((n) => n.id !== nux.id).concat(next)\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))\n        .concat(pref)\n    })\n  }\n\n  /**\n   * Removes NUXs from user preferences.\n   */\n  async bskyAppRemoveNuxs(ids: string[]) {\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidBskyAppStatePref) || {\n        $type: 'app.bsky.actor.defs#bskyAppStatePref',\n      }\n\n      pref.nuxs = (pref.nuxs || []).filter((nux) => !ids.includes(nux.id))\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))\n        .concat(pref)\n    })\n  }\n\n  async setPostInteractionSettings(\n    settings: AppBskyActorDefs.PostInteractionSettingsPref,\n  ) {\n    const result =\n      AppBskyActorDefs.validatePostInteractionSettingsPref(settings)\n    // Fool-proofing (should not be needed because of type safety)\n    if (!result.success) throw result.error\n\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(\n        predicate.isValidPostInteractionSettingsPref,\n      ) || {\n        $type: 'app.bsky.actor.defs#postInteractionSettingsPref',\n      }\n\n      /**\n       * Matches handling of `threadgate.allow` where `undefined` means \"everyone\"\n       */\n      pref.threadgateAllowRules = settings.threadgateAllowRules\n      pref.postgateEmbeddingRules = settings.postgateEmbeddingRules\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isPostInteractionSettingsPref(p))\n        .concat(pref)\n    })\n  }\n\n  async setVerificationPrefs(settings: AppBskyActorDefs.VerificationPrefs) {\n    const result = AppBskyActorDefs.validateVerificationPrefs(settings)\n    // Fool-proofing (should not be needed because of type safety)\n    if (!result.success) throw result.error\n\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidVerificationPrefs) || {\n        $type: 'app.bsky.actor.defs#verificationPrefs',\n        hideBadges: false,\n      }\n\n      pref.hideBadges = settings.hideBadges\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isVerificationPrefs(p))\n        .concat(pref)\n    })\n  }\n\n  async updateLiveEventPreferences(\n    action:\n      | { type: 'hideFeed'; id: string }\n      | { type: 'unhideFeed'; id: string }\n      | { type: 'toggleHideAllFeeds' },\n  ) {\n    return this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidLiveEventPreferences) || {\n        $type: 'app.bsky.actor.defs#liveEventPreferences',\n        hiddenFeedIds: [],\n        hideAllFeeds: false,\n      }\n\n      const hiddenFeedIds = new Set(pref.hiddenFeedIds || [])\n\n      switch (action.type) {\n        case 'hideFeed':\n          hiddenFeedIds.add(action.id)\n          break\n        case 'unhideFeed':\n          hiddenFeedIds.delete(action.id)\n          break\n        case 'toggleHideAllFeeds':\n          pref.hideAllFeeds = !pref.hideAllFeeds\n          break\n      }\n\n      pref.hiddenFeedIds = [...hiddenFeedIds]\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isLiveEventPreferences(p))\n        .concat(pref)\n    })\n  }\n\n  //- Private methods\n\n  #prefsLock = new AwaitLock()\n\n  /**\n   * This function updates the preferences of a user and allows for a callback function to be executed\n   * before the update.\n   * @param cb - cb is a callback function that takes in a single parameter of type\n   * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to\n   * update the preferences of the user. The function is called with the current preferences as an\n   * argument and if the callback returns false, the preferences are not updated.\n   */\n  private async updatePreferences(\n    cb: (\n      prefs: AppBskyActorDefs.Preferences,\n    ) => AppBskyActorDefs.Preferences | false,\n  ) {\n    try {\n      await this.#prefsLock.acquireAsync()\n      const res = await this.app.bsky.actor.getPreferences({})\n      const newPrefs = cb(res.data.preferences)\n      if (newPrefs === false) {\n        return res.data.preferences\n      }\n      await this.app.bsky.actor.putPreferences({\n        preferences: newPrefs,\n      })\n      return newPrefs\n    } finally {\n      this.#prefsLock.release()\n    }\n  }\n\n  private async updateHiddenPost(postUri: string, action: 'hide' | 'unhide') {\n    await this.updatePreferences((prefs) => {\n      const pref = prefs.findLast(predicate.isValidHiddenPostsPref) || {\n        $type: 'app.bsky.actor.defs#hiddenPostsPref',\n        items: [],\n      }\n\n      const hiddenItems = new Set(pref.items)\n\n      if (action === 'hide') hiddenItems.add(postUri)\n      else hiddenItems.delete(postUri)\n\n      pref.items = [...hiddenItems]\n\n      return prefs\n        .filter((p) => !AppBskyActorDefs.isHiddenPostsPref(p))\n        .concat(pref)\n    })\n  }\n\n  /**\n   * A helper specifically for updating feed preferences\n   */\n  private async updateFeedPreferences(\n    cb: (\n      saved: string[],\n      pinned: string[],\n    ) => { saved: string[]; pinned: string[] },\n  ): Promise<{ saved: string[]; pinned: string[] }> {\n    let res\n    await this.updatePreferences((prefs) => {\n      const feedsPref = prefs.findLast(predicate.isValidSavedFeedsPref) || {\n        $type: 'app.bsky.actor.defs#savedFeedsPref',\n        saved: [],\n        pinned: [],\n      }\n\n      res = cb(feedsPref.saved, feedsPref.pinned)\n      feedsPref.saved = res.saved\n      feedsPref.pinned = res.pinned\n\n      return prefs\n        .filter((pref) => !AppBskyActorDefs.isSavedFeedsPref(pref))\n        .concat(feedsPref)\n    })\n    return res\n  }\n\n  private async updateSavedFeedsV2Preferences(\n    cb: (\n      savedFeedsPref: AppBskyActorDefs.SavedFeed[],\n    ) => AppBskyActorDefs.SavedFeed[],\n  ): Promise<AppBskyActorDefs.SavedFeed[]> {\n    let maybeMutatedSavedFeeds: AppBskyActorDefs.SavedFeed[] = []\n\n    await this.updatePreferences((prefs) => {\n      const existingV2Pref = prefs.findLast(\n        predicate.isValidSavedFeedsPrefV2,\n      ) || {\n        $type: 'app.bsky.actor.defs#savedFeedsPrefV2',\n        items: [],\n      }\n\n      const newSavedFeeds = cb(existingV2Pref.items)\n\n      // enforce ordering: pinned first, then saved\n      existingV2Pref.items = [...newSavedFeeds].sort((a, b) =>\n        // @NOTE: preserve order of items with the same pinned status\n        a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1,\n      )\n\n      // Store the return value\n      maybeMutatedSavedFeeds = newSavedFeeds\n\n      let updatedPrefs = prefs\n        .filter((pref) => !AppBskyActorDefs.isSavedFeedsPrefV2(pref))\n        .concat(existingV2Pref)\n\n      /*\n       * If there's a v2 pref present, it means this account was migrated from v1\n       * to v2. During the transition period, we double write v2 prefs back to\n       * v1, but NOT the other way around.\n       */\n      let existingV1Pref = prefs.findLast(predicate.isValidSavedFeedsPref)\n      if (existingV1Pref) {\n        const { saved, pinned } = existingV1Pref\n        const v2Compat = savedFeedsToUriArrays(\n          // v1 only supports feeds and lists\n          existingV2Pref.items.filter((i) => ['feed', 'list'].includes(i.type)),\n        )\n        existingV1Pref = {\n          ...existingV1Pref,\n          saved: Array.from(new Set([...saved, ...v2Compat.saved])),\n          pinned: Array.from(new Set([...pinned, ...v2Compat.pinned])),\n        }\n        updatedPrefs = updatedPrefs\n          .filter((pref) => !AppBskyActorDefs.isSavedFeedsPref(pref))\n          .concat(existingV1Pref)\n      }\n\n      return updatedPrefs\n    })\n\n    return maybeMutatedSavedFeeds\n  }\n\n  //#endregion\n}\n\n/**\n * Helper to transform the legacy content preferences.\n */\nfunction adjustLegacyContentLabelPref(\n  pref: AppBskyActorDefs.ContentLabelPref,\n): AppBskyActorDefs.ContentLabelPref {\n  let visibility = pref.visibility\n\n  // adjust legacy values\n  if (visibility === 'show') {\n    visibility = 'ignore'\n  }\n\n  return { ...pref, visibility }\n}\n\n/**\n * Re-maps legacy labels to new labels on READ. Does not save these changes to\n * the user's preferences.\n */\nfunction remapLegacyLabels(\n  labels: BskyPreferences['moderationPrefs']['labels'],\n) {\n  const _labels = { ...labels }\n  const legacyToNewMap: Record<string, string | undefined> = {\n    gore: 'graphic-media',\n    nsfw: 'porn',\n    suggestive: 'sexual',\n  }\n\n  for (const labelName in _labels) {\n    const newLabelName = legacyToNewMap[labelName]!\n    if (newLabelName) {\n      _labels[newLabelName] = _labels[labelName]\n    }\n  }\n\n  return _labels\n}\n\n/**\n * A helper to get the currently enabled labelers from the full preferences array\n */\nfunction prefsArrayToLabelerDids(\n  prefs: AppBskyActorDefs.Preferences,\n): string[] {\n  const labelersPref = prefs.findLast(predicate.isValidLabelersPref)\n  let dids: string[] = []\n  if (labelersPref) {\n    dids = (labelersPref as AppBskyActorDefs.LabelersPref).labelers.map(\n      (labeler) => labeler.did,\n    )\n  }\n  return dids\n}\n\nfunction isBskyPrefs(v: any): v is BskyPreferences {\n  return (\n    v &&\n    typeof v === 'object' &&\n    'moderationPrefs' in v &&\n    isModPrefs(v.moderationPrefs)\n  )\n}\n\nfunction isModPrefs(v: any): v is ModerationPrefs {\n  return v && typeof v === 'object' && 'labelers' in v\n}\n\nfunction migrateLegacyMutedWordsItems(items: AppBskyActorDefs.MutedWord[]) {\n  return items.map((item) => ({\n    ...item,\n    id: item.id || TID.nextStr(),\n  }))\n}\n\nfunction matchMutedWord(\n  existingWord: AppBskyActorDefs.MutedWord,\n  newWord: AppBskyActorDefs.MutedWord,\n): boolean {\n  // id is undefined in legacy implementation\n  const existingId = existingWord.id\n  // prefer matching based on id\n  const matchById = existingId && existingId === newWord.id\n  // handle legacy case where id is not set\n  const legacyMatchByValue = !existingId && existingWord.value === newWord.value\n\n  return matchById || legacyMatchByValue\n}\n"
  },
  {
    "path": "packages/api/src/atp-agent.ts",
    "content": "import { getPdsEndpoint, isValidDidDoc } from '@atproto/common-web'\nimport {\n  ErrorResponseBody,\n  Gettable,\n  ResponseType,\n  XRPCError,\n  XrpcClient,\n  errorResponseBody,\n} from '@atproto/xrpc'\nimport { Agent } from './agent'\nimport {\n  ComAtprotoServerCreateAccount,\n  ComAtprotoServerCreateSession,\n  ComAtprotoServerGetSession,\n  ComAtprotoServerNS,\n  ComAtprotoServerRefreshSession,\n} from './client'\nimport { schemas } from './client/lexicons'\nimport { SessionManager } from './session-manager'\nimport {\n  AtpAgentLoginOpts,\n  AtpPersistSessionHandler,\n  AtpSessionData,\n} from './types'\n\nconst ReadableStream = globalThis.ReadableStream as\n  | typeof globalThis.ReadableStream\n  | undefined\n\nexport type AtpAgentOptions = {\n  service: string | URL\n  persistSession?: AtpPersistSessionHandler\n  fetch?: typeof globalThis.fetch\n  headers?: Iterable<[string, Gettable<null | string>]>\n}\n\n/**\n * A wrapper around the {@link Agent} class that uses credential based session\n * management. This class also exposes most of the session management methods\n * directly.\n *\n * This class will be deprecated in the near future. Use {@link Agent} directly\n * with a {@link CredentialSession} instead:\n *\n *  ```ts\n *  const session = new CredentialSession({\n *    service: new URL('https://example.com'),\n *  })\n *\n *  const agent = new Agent(session)\n *  ```\n */\nexport class AtpAgent extends Agent {\n  readonly sessionManager: CredentialSession\n\n  constructor(options: AtpAgentOptions | CredentialSession) {\n    const sessionManager =\n      options instanceof CredentialSession\n        ? options\n        : new CredentialSession(\n            new URL(options.service),\n            options.fetch,\n            options.persistSession,\n          )\n\n    super(sessionManager)\n\n    // This assignment is already being done in the super constructor, but we\n    // need to do it here to make TypeScript happy.\n    this.sessionManager = sessionManager\n\n    if (!(options instanceof CredentialSession) && options.headers) {\n      for (const [key, value] of options.headers) {\n        this.setHeader(key, value)\n      }\n    }\n  }\n\n  clone(): AtpAgent {\n    return this.copyInto(new AtpAgent(this.sessionManager))\n  }\n\n  get session() {\n    return this.sessionManager.session\n  }\n\n  get hasSession() {\n    return this.sessionManager.hasSession\n  }\n\n  get did() {\n    return this.sessionManager.did\n  }\n\n  get serviceUrl() {\n    return this.sessionManager.serviceUrl\n  }\n\n  get pdsUrl() {\n    return this.sessionManager.pdsUrl\n  }\n\n  get dispatchUrl() {\n    return this.sessionManager.dispatchUrl\n  }\n\n  /** @deprecated use {@link serviceUrl} instead */\n  get service() {\n    return this.serviceUrl\n  }\n\n  get persistSession() {\n    throw new Error(\n      'Cannot set persistSession directly. \"persistSession\" is defined through the constructor and will be invoked automatically when session data changes.',\n    )\n  }\n\n  set persistSession(v: unknown) {\n    throw new Error(\n      'Cannot set persistSession directly. \"persistSession\" must be defined in the constructor and can no longer be changed.',\n    )\n  }\n\n  /** @deprecated use {@link AtpAgent.serviceUrl} instead */\n  getServiceUrl() {\n    return this.serviceUrl\n  }\n\n  async resumeSession(\n    session: AtpSessionData,\n  ): Promise<ComAtprotoServerGetSession.Response> {\n    return this.sessionManager.resumeSession(session)\n  }\n\n  async createAccount(\n    data: ComAtprotoServerCreateAccount.InputSchema,\n    opts?: ComAtprotoServerCreateAccount.CallOptions,\n  ): Promise<ComAtprotoServerCreateAccount.Response> {\n    return this.sessionManager.createAccount(data, opts)\n  }\n\n  async login(\n    opts: AtpAgentLoginOpts,\n  ): Promise<ComAtprotoServerCreateSession.Response> {\n    return this.sessionManager.login(opts)\n  }\n\n  async logout(): Promise<void> {\n    return this.sessionManager.logout()\n  }\n}\n\n/**\n * Credentials (username / password) based session manager. Instances of this\n * class will typically be used as the session manager for an {@link AtpAgent}.\n * They can also be used with an {@link XrpcClient}, if you want to use you\n * own Lexicons.\n */\nexport class CredentialSession implements SessionManager {\n  public pdsUrl?: URL // The PDS URL, driven by the did doc\n  public session?: AtpSessionData\n  public refreshSessionPromise?: Promise<ComAtprotoServerRefreshSession.Response>\n\n  /**\n   * Private {@link ComAtprotoServerNS} used to perform session management API\n   * calls on the service endpoint. Calls performed by this agent will not be\n   * authenticated using the user's session to allow proper manual configuration\n   * of the headers when performing session management operations.\n   */\n  protected server = new ComAtprotoServerNS(\n    // Note that the use of the codegen \"schemas\" (to instantiate `this.api`),\n    // as well as the use of `ComAtprotoServerNS` will cause this class to\n    // reference (way) more code than it actually needs. It is not possible,\n    // with the current state of the codegen, to generate a client that only\n    // includes the methods that are actually used by this class. This is a\n    // known limitation that should be addressed in a future version of the\n    // codegen.\n    new XrpcClient((url, init) => {\n      return (0, this.fetch)(new URL(url, this.serviceUrl), init)\n    }, schemas),\n  )\n\n  constructor(\n    public readonly serviceUrl: URL,\n    public fetch = globalThis.fetch,\n    protected readonly persistSession?: AtpPersistSessionHandler,\n  ) {}\n\n  get did() {\n    return this.session?.did\n  }\n\n  get dispatchUrl() {\n    return this.pdsUrl || this.serviceUrl\n  }\n\n  get hasSession() {\n    return !!this.session\n  }\n\n  /**\n   * Sets a WhatWG \"fetch()\" function to be used for making HTTP requests.\n   */\n  setFetch(fetch = globalThis.fetch) {\n    this.fetch = fetch\n  }\n\n  async fetchHandler(url: string, init?: RequestInit): Promise<Response> {\n    // wait for any active session-refreshes to finish\n    await this.refreshSessionPromise\n\n    const initialUri = new URL(url, this.dispatchUrl)\n    const initialReq = new Request(initialUri, init)\n\n    const initialToken = this.session?.accessJwt\n    if (!initialToken || initialReq.headers.has('authorization')) {\n      return (0, this.fetch)(initialReq)\n    }\n\n    initialReq.headers.set('authorization', `Bearer ${initialToken}`)\n    const initialRes = await (0, this.fetch)(initialReq)\n\n    if (!this.session?.refreshJwt) {\n      return initialRes\n    }\n    const isExpiredToken =\n      initialRes.status === 401 ||\n      (await isErrorResponse(initialRes, [400], ['ExpiredToken']))\n\n    if (!isExpiredToken) {\n      return initialRes\n    }\n\n    try {\n      await this.refreshSession()\n    } catch {\n      return initialRes\n    }\n\n    if (init?.signal?.aborted) {\n      return initialRes\n    }\n\n    // The stream was already consumed. We cannot retry the request. A solution\n    // would be to tee() the input stream but that would bufferize the entire\n    // stream in memory which can lead to memory starvation. Instead, we will\n    // return the original response and let the calling code handle retries.\n    if (ReadableStream && init?.body instanceof ReadableStream) {\n      return initialRes\n    }\n\n    // Return initial \"ExpiredToken\" response if the session was not refreshed.\n    const updatedToken = this.session?.accessJwt\n    if (!updatedToken || updatedToken === initialToken) {\n      return initialRes\n    }\n\n    // Make sure the initial request is cancelled to avoid leaking resources\n    // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection\n    await initialRes.body?.cancel()\n\n    // We need to re-compute the URI in case the PDS endpoint has changed\n    const updatedUri = new URL(url, this.dispatchUrl)\n    const updatedReq = new Request(updatedUri, init)\n\n    updatedReq.headers.set('authorization', `Bearer ${updatedToken}`)\n\n    return await (0, this.fetch)(updatedReq)\n  }\n\n  /**\n   * Create a new account and hydrate its session in this agent.\n   */\n  async createAccount(\n    data: ComAtprotoServerCreateAccount.InputSchema,\n    opts?: ComAtprotoServerCreateAccount.CallOptions,\n  ): Promise<ComAtprotoServerCreateAccount.Response> {\n    // Clear any existing session\n    this.session = undefined\n    this.refreshSessionPromise = undefined\n\n    try {\n      const res = await this.server.createAccount(data, opts)\n      this.session = {\n        accessJwt: res.data.accessJwt,\n        refreshJwt: res.data.refreshJwt,\n        handle: res.data.handle,\n        did: res.data.did,\n        email: data.email,\n        emailConfirmed: false,\n        emailAuthFactor: false,\n        active: true,\n      }\n      this.persistSession?.('create', this.session)\n      this._updateApiEndpoint(res.data.didDoc)\n      return res\n    } catch (e) {\n      this.session = undefined\n      this.persistSession?.('create-failed', undefined)\n      throw e\n    }\n  }\n\n  /**\n   * Start a new session with this agent.\n   */\n  async login(\n    opts: AtpAgentLoginOpts,\n  ): Promise<ComAtprotoServerCreateSession.Response> {\n    // Clear any existing session\n    this.session = undefined\n    this.refreshSessionPromise = undefined\n\n    try {\n      const res = await this.server.createSession({\n        identifier: opts.identifier,\n        password: opts.password,\n        authFactorToken: opts.authFactorToken,\n        allowTakendown: opts.allowTakendown,\n      })\n\n      if (this.session) {\n        throw new Error('Concurrent login detected')\n      }\n\n      this.session = {\n        accessJwt: res.data.accessJwt,\n        refreshJwt: res.data.refreshJwt,\n        handle: res.data.handle,\n        did: res.data.did,\n        email: res.data.email,\n        emailConfirmed: res.data.emailConfirmed,\n        emailAuthFactor: res.data.emailAuthFactor,\n        active: res.data.active ?? true,\n        status: res.data.status,\n      }\n      this._updateApiEndpoint(res.data.didDoc)\n      this.persistSession?.('create', this.session)\n      return res\n    } catch (e) {\n      this.session = undefined\n      this.persistSession?.('create-failed', undefined)\n      throw e\n    }\n  }\n\n  async logout(): Promise<void> {\n    if (this.session) {\n      try {\n        await this.server.deleteSession(undefined, {\n          headers: {\n            authorization: `Bearer ${this.session.refreshJwt}`,\n          },\n        })\n      } catch {\n        // Ignore errors\n      } finally {\n        this.session = undefined\n        this.persistSession?.('expired', undefined)\n      }\n    }\n  }\n\n  /**\n   * Resume a pre-existing session with this agent.\n   *\n   * @note that a rejected promise from this method indicates a failure to\n   * refresh the session after resuming it but does not indicate a failure to\n   * set the session itself. In case of rejection, check the presence of\n   * {@link CredentialSession.session} after calling this method to ensure the\n   * session was set.\n   */\n  async resumeSession(\n    session: AtpSessionData,\n  ): Promise<ComAtprotoServerRefreshSession.Response> {\n    // Protect against multiple calls to resumeSession that would trigger a\n    // refresh for the same session simultaneously.\n    // Ideally, this check would be based on a session identifier, but since\n    // we don't have one, we will just check the refresh token.\n    if (session.refreshJwt !== this.session?.refreshJwt) {\n      // Set the current session, and discard any pending refresh operation..\n      this.session = session\n      this.refreshSessionPromise = undefined\n    }\n\n    // Ensure that the session is still valid by forcing a refresh. This will\n    // also ensure that persistSession handler is called.\n    const result = await this.refreshSession()\n\n    // Fool-proofing: another concurrent operation may have replaced the session\n    // while we were waiting for the refresh to complete.\n    if (session.did !== this.session?.did) {\n      throw new Error('DID mismatch on resumeSession')\n    }\n\n    return result\n  }\n\n  /**\n   * Internal helper to refresh sessions\n   * - Wraps the actual implementation in a promise-guard to ensure only\n   *   one refresh is attempted at a time.\n   */\n  async refreshSession(): Promise<ComAtprotoServerRefreshSession.Response> {\n    if (!this.session) {\n      throw new Error('Unexpected state: no session to refresh')\n    }\n\n    // Do not refresh if we already have a refresh in progress\n    if (this.refreshSessionPromise) return this.refreshSessionPromise\n\n    const promise = this._refreshSessionInner().finally(() => {\n      if (this.refreshSessionPromise === promise) {\n        this.refreshSessionPromise = undefined\n      }\n    })\n\n    this.refreshSessionPromise = promise\n\n    return promise\n  }\n\n  /**\n   * Internal helper to refresh sessions (actual behavior)\n   */\n  private async _refreshSessionInner(): Promise<ComAtprotoServerRefreshSession.Response> {\n    const { session } = this\n\n    // Should never happen\n    if (!session) throw new Error('No session to refresh')\n\n    try {\n      const res = await this.server.refreshSession(undefined, {\n        headers: { authorization: `Bearer ${session.refreshJwt}` },\n      })\n\n      const { data } = res\n\n      // Something is very wrong if the DID changes during a refresh\n      if (data.did !== session.did) {\n        throw new XRPCError(\n          ResponseType.InvalidRequest,\n          'Invalid session',\n          'InvalidDID',\n        )\n      }\n\n      // Historically, refreshSession did not return all the fields from\n      // getSession. In particular, email, emailConfirmed and emailAuthFactor\n      // were missing. Similarly, some servers might not return the didDoc in\n      // refreshSession. We fetch them via getSession if missing, allowing to\n      // ensure that we are always talking with the right PDS.\n      if (data.emailConfirmed == null || data.didDoc == null) {\n        try {\n          const res = await this.server.getSession(undefined, {\n            headers: { authorization: `Bearer ${data.accessJwt}` },\n          })\n\n          // Fool proofing (should always match)\n          if (res.data.did === data.did) {\n            Object.assign(data, res.data)\n          }\n        } catch {\n          // Noop, we'll keep the current values we have\n        }\n      }\n\n      // protect against concurrent session updates\n      if (this.session !== session) {\n        return Promise.reject(new Error('Concurrent session update detected'))\n      }\n\n      // succeeded, update the session\n      this.session = {\n        did: data.did,\n        accessJwt: data.accessJwt,\n        refreshJwt: data.refreshJwt,\n        handle: data.handle ?? session.handle,\n        email: data.email ?? session.email,\n        emailConfirmed: data.emailConfirmed ?? session.emailConfirmed,\n        emailAuthFactor: data.emailAuthFactor ?? session.emailAuthFactor,\n        active: data.active ?? session.active ?? true,\n        status: data.status,\n      }\n\n      this._updateApiEndpoint(res.data.didDoc)\n      this.persistSession?.('update', this.session)\n\n      return res\n    } catch (err) {\n      // protect against concurrent session updates\n      if (this.session === session) {\n        if (\n          err instanceof XRPCError &&\n          (err.status === 401 ||\n            err.error === 'InvalidDID' ||\n            ['ExpiredToken', 'InvalidToken'].includes(err.error))\n        ) {\n          // failed due to a bad refresh token\n          this.session = undefined\n          this.persistSession?.('expired', undefined)\n        } else {\n          // Assume the problem is transient and the session can be reused later.\n          this.session = session\n          this.persistSession?.('network-error', session)\n        }\n      }\n\n      throw err\n    }\n  }\n\n  /**\n   * Helper to update the pds endpoint dynamically.\n   *\n   * The session methods (create, resume, refresh) may respond with the user's\n   * did document which contains the user's canonical PDS endpoint. That endpoint\n   * may differ from the endpoint used to contact the server. We capture that\n   * PDS endpoint and update the client to use that given endpoint for future\n   * requests. (This helps ensure smooth migrations between PDSes, especially\n   * when the PDSes are operated by a single org.)\n   */\n  private _updateApiEndpoint(didDoc: unknown) {\n    const endpoint = isValidDidDoc(didDoc) ? getPdsEndpoint(didDoc) : undefined\n    if (endpoint) {\n      this.pdsUrl = new URL(endpoint)\n    } else {\n      // If the did doc is invalid (or missing), we clear the pdsUrl (should\n      // never happen). This is fine if the auth server and PDS are the same\n      // service, or if the auth server will proxy requests to the right PDS\n      // (which is the case for Bluesky's \"entryway\").\n      this.pdsUrl = undefined\n    }\n  }\n}\n\nfunction isErrorObject(v: unknown): v is ErrorResponseBody {\n  return errorResponseBody.safeParse(v).success\n}\n\nasync function isErrorResponse(\n  response: Response,\n  status: number[],\n  errorNames: string[],\n): Promise<boolean> {\n  if (!status.includes(response.status)) return false\n  // Some engines (react-native 👀) don't expose a response.body property...\n  // if (!response.body) return false\n  try {\n    const json = await peekJson(response, 10 * 1024)\n    return isErrorObject(json) && (errorNames as any[]).includes(json.error)\n  } catch (err) {\n    return false\n  }\n}\n\nasync function peekJson(\n  response: Response,\n  maxSize = Infinity,\n): Promise<unknown> {\n  if (extractType(response) !== 'application/json') throw new Error('Not JSON')\n  if (extractLength(response) > maxSize) throw new Error('Response too large')\n  return response.clone().json()\n}\n\nfunction extractLength({ headers }: Response) {\n  return headers.get('Content-Length')\n    ? Number(headers.get('Content-Length'))\n    : NaN\n}\n\nfunction extractType({ headers }: Response) {\n  return headers.get('Content-Type')?.split(';')[0]?.trim()\n}\n"
  },
  {
    "path": "packages/api/src/bsky-agent.ts",
    "content": "import { AtpAgent } from './atp-agent'\n\n/** @deprecated use {@link AtpAgent} instead */\nexport class BskyAgent extends AtpAgent {\n  clone(): this {\n    if (this.constructor === BskyAgent) {\n      const agent = new BskyAgent(this.sessionManager)\n      return this.copyInto(agent as this)\n    }\n\n    // sub-classes should override this method\n    throw new TypeError('Cannot clone a subclass of BskyAgent')\n  }\n}\n"
  },
  {
    "path": "packages/api/src/client/index.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  XrpcClient,\n  type FetchHandler,\n  type FetchHandlerOptions,\n} from '@atproto/xrpc'\nimport { schemas } from './lexicons.js'\nimport { CID } from 'multiformats/cid'\nimport { type OmitKey, type Un$Typed } from './util.js'\nimport * as AppBskyActorDefs from './types/app/bsky/actor/defs.js'\nimport * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences.js'\nimport * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile.js'\nimport * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles.js'\nimport * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions.js'\nimport * as AppBskyActorProfile from './types/app/bsky/actor/profile.js'\nimport * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences.js'\nimport * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors.js'\nimport * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead.js'\nimport * as AppBskyActorStatus from './types/app/bsky/actor/status.js'\nimport * as AppBskyAgeassuranceBegin from './types/app/bsky/ageassurance/begin.js'\nimport * as AppBskyAgeassuranceDefs from './types/app/bsky/ageassurance/defs.js'\nimport * as AppBskyAgeassuranceGetConfig from './types/app/bsky/ageassurance/getConfig.js'\nimport * as AppBskyAgeassuranceGetState from './types/app/bsky/ageassurance/getState.js'\nimport * as AppBskyBookmarkCreateBookmark from './types/app/bsky/bookmark/createBookmark.js'\nimport * as AppBskyBookmarkDefs from './types/app/bsky/bookmark/defs.js'\nimport * as AppBskyBookmarkDeleteBookmark from './types/app/bsky/bookmark/deleteBookmark.js'\nimport * as AppBskyBookmarkGetBookmarks from './types/app/bsky/bookmark/getBookmarks.js'\nimport * as AppBskyContactDefs from './types/app/bsky/contact/defs.js'\nimport * as AppBskyContactDismissMatch from './types/app/bsky/contact/dismissMatch.js'\nimport * as AppBskyContactGetMatches from './types/app/bsky/contact/getMatches.js'\nimport * as AppBskyContactGetSyncStatus from './types/app/bsky/contact/getSyncStatus.js'\nimport * as AppBskyContactImportContacts from './types/app/bsky/contact/importContacts.js'\nimport * as AppBskyContactRemoveData from './types/app/bsky/contact/removeData.js'\nimport * as AppBskyContactSendNotification from './types/app/bsky/contact/sendNotification.js'\nimport * as AppBskyContactStartPhoneVerification from './types/app/bsky/contact/startPhoneVerification.js'\nimport * as AppBskyContactVerifyPhone from './types/app/bsky/contact/verifyPhone.js'\nimport * as AppBskyDraftCreateDraft from './types/app/bsky/draft/createDraft.js'\nimport * as AppBskyDraftDefs from './types/app/bsky/draft/defs.js'\nimport * as AppBskyDraftDeleteDraft from './types/app/bsky/draft/deleteDraft.js'\nimport * as AppBskyDraftGetDrafts from './types/app/bsky/draft/getDrafts.js'\nimport * as AppBskyDraftUpdateDraft from './types/app/bsky/draft/updateDraft.js'\nimport * as AppBskyEmbedDefs from './types/app/bsky/embed/defs.js'\nimport * as AppBskyEmbedExternal from './types/app/bsky/embed/external.js'\nimport * as AppBskyEmbedImages from './types/app/bsky/embed/images.js'\nimport * as AppBskyEmbedRecord from './types/app/bsky/embed/record.js'\nimport * as AppBskyEmbedRecordWithMedia from './types/app/bsky/embed/recordWithMedia.js'\nimport * as AppBskyEmbedVideo from './types/app/bsky/embed/video.js'\nimport * as AppBskyFeedDefs from './types/app/bsky/feed/defs.js'\nimport * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator.js'\nimport * as AppBskyFeedGenerator from './types/app/bsky/feed/generator.js'\nimport * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds.js'\nimport * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes.js'\nimport * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed.js'\nimport * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed.js'\nimport * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator.js'\nimport * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators.js'\nimport * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'\nimport * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'\nimport * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'\nimport * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'\nimport * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'\nimport * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'\nimport * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'\nimport * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'\nimport * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline.js'\nimport * as AppBskyFeedLike from './types/app/bsky/feed/like.js'\nimport * as AppBskyFeedPost from './types/app/bsky/feed/post.js'\nimport * as AppBskyFeedPostgate from './types/app/bsky/feed/postgate.js'\nimport * as AppBskyFeedRepost from './types/app/bsky/feed/repost.js'\nimport * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts.js'\nimport * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions.js'\nimport * as AppBskyFeedThreadgate from './types/app/bsky/feed/threadgate.js'\nimport * as AppBskyGraphBlock from './types/app/bsky/graph/block.js'\nimport * as AppBskyGraphDefs from './types/app/bsky/graph/defs.js'\nimport * as AppBskyGraphFollow from './types/app/bsky/graph/follow.js'\nimport * as AppBskyGraphGetActorStarterPacks from './types/app/bsky/graph/getActorStarterPacks.js'\nimport * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks.js'\nimport * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers.js'\nimport * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows.js'\nimport * as AppBskyGraphGetKnownFollowers from './types/app/bsky/graph/getKnownFollowers.js'\nimport * as AppBskyGraphGetList from './types/app/bsky/graph/getList.js'\nimport * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks.js'\nimport * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes.js'\nimport * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists.js'\nimport * as AppBskyGraphGetListsWithMembership from './types/app/bsky/graph/getListsWithMembership.js'\nimport * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes.js'\nimport * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelationships.js'\nimport * as AppBskyGraphGetStarterPack from './types/app/bsky/graph/getStarterPack.js'\nimport * as AppBskyGraphGetStarterPacks from './types/app/bsky/graph/getStarterPacks.js'\nimport * as AppBskyGraphGetStarterPacksWithMembership from './types/app/bsky/graph/getStarterPacksWithMembership.js'\nimport * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor.js'\nimport * as AppBskyGraphList from './types/app/bsky/graph/list.js'\nimport * as AppBskyGraphListblock from './types/app/bsky/graph/listblock.js'\nimport * as AppBskyGraphListitem from './types/app/bsky/graph/listitem.js'\nimport * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor.js'\nimport * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList.js'\nimport * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread.js'\nimport * as AppBskyGraphSearchStarterPacks from './types/app/bsky/graph/searchStarterPacks.js'\nimport * as AppBskyGraphStarterpack from './types/app/bsky/graph/starterpack.js'\nimport * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'\nimport * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'\nimport * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'\nimport * as AppBskyGraphVerification from './types/app/bsky/graph/verification.js'\nimport * as AppBskyLabelerDefs from './types/app/bsky/labeler/defs.js'\nimport * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'\nimport * as AppBskyLabelerService from './types/app/bsky/labeler/service.js'\nimport * as AppBskyNotificationDeclaration from './types/app/bsky/notification/declaration.js'\nimport * as AppBskyNotificationDefs from './types/app/bsky/notification/defs.js'\nimport * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'\nimport * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'\nimport * as AppBskyNotificationListActivitySubscriptions from './types/app/bsky/notification/listActivitySubscriptions.js'\nimport * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'\nimport * as AppBskyNotificationPutActivitySubscription from './types/app/bsky/notification/putActivitySubscription.js'\nimport * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'\nimport * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'\nimport * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'\nimport * as AppBskyNotificationUnregisterPush from './types/app/bsky/notification/unregisterPush.js'\nimport * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'\nimport * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet.js'\nimport * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs.js'\nimport * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'\nimport * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacks from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'\nimport * as AppBskyUnspeccedGetPostThreadOtherV2 from './types/app/bsky/unspecced/getPostThreadOtherV2.js'\nimport * as AppBskyUnspeccedGetPostThreadV2 from './types/app/bsky/unspecced/getPostThreadV2.js'\nimport * as AppBskyUnspeccedGetSuggestedFeeds from './types/app/bsky/unspecced/getSuggestedFeeds.js'\nimport * as AppBskyUnspeccedGetSuggestedFeedsSkeleton from './types/app/bsky/unspecced/getSuggestedFeedsSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedOnboardingUsers from './types/app/bsky/unspecced/getSuggestedOnboardingUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacks from './types/app/bsky/unspecced/getSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedUsers from './types/app/bsky/unspecced/getSuggestedUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedUsersSkeleton from './types/app/bsky/unspecced/getSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspecced/getSuggestionsSkeleton.js'\nimport * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions.js'\nimport * as AppBskyUnspeccedGetTrendingTopics from './types/app/bsky/unspecced/getTrendingTopics.js'\nimport * as AppBskyUnspeccedGetTrends from './types/app/bsky/unspecced/getTrends.js'\nimport * as AppBskyUnspeccedGetTrendsSkeleton from './types/app/bsky/unspecced/getTrendsSkeleton.js'\nimport * as AppBskyUnspeccedInitAgeAssurance from './types/app/bsky/unspecced/initAgeAssurance.js'\nimport * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton.js'\nimport * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton.js'\nimport * as AppBskyUnspeccedSearchStarterPacksSkeleton from './types/app/bsky/unspecced/searchStarterPacksSkeleton.js'\nimport * as AppBskyVideoDefs from './types/app/bsky/video/defs.js'\nimport * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus.js'\nimport * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits.js'\nimport * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo.js'\nimport * as ChatBskyActorDeclaration from './types/chat/bsky/actor/declaration.js'\nimport * as ChatBskyActorDefs from './types/chat/bsky/actor/defs.js'\nimport * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount.js'\nimport * as ChatBskyActorExportAccountData from './types/chat/bsky/actor/exportAccountData.js'\nimport * as ChatBskyConvoAcceptConvo from './types/chat/bsky/convo/acceptConvo.js'\nimport * as ChatBskyConvoAddReaction from './types/chat/bsky/convo/addReaction.js'\nimport * as ChatBskyConvoDefs from './types/chat/bsky/convo/defs.js'\nimport * as ChatBskyConvoDeleteMessageForSelf from './types/chat/bsky/convo/deleteMessageForSelf.js'\nimport * as ChatBskyConvoGetConvo from './types/chat/bsky/convo/getConvo.js'\nimport * as ChatBskyConvoGetConvoAvailability from './types/chat/bsky/convo/getConvoAvailability.js'\nimport * as ChatBskyConvoGetConvoForMembers from './types/chat/bsky/convo/getConvoForMembers.js'\nimport * as ChatBskyConvoGetLog from './types/chat/bsky/convo/getLog.js'\nimport * as ChatBskyConvoGetMessages from './types/chat/bsky/convo/getMessages.js'\nimport * as ChatBskyConvoLeaveConvo from './types/chat/bsky/convo/leaveConvo.js'\nimport * as ChatBskyConvoListConvos from './types/chat/bsky/convo/listConvos.js'\nimport * as ChatBskyConvoMuteConvo from './types/chat/bsky/convo/muteConvo.js'\nimport * as ChatBskyConvoRemoveReaction from './types/chat/bsky/convo/removeReaction.js'\nimport * as ChatBskyConvoSendMessage from './types/chat/bsky/convo/sendMessage.js'\nimport * as ChatBskyConvoSendMessageBatch from './types/chat/bsky/convo/sendMessageBatch.js'\nimport * as ChatBskyConvoUnmuteConvo from './types/chat/bsky/convo/unmuteConvo.js'\nimport * as ChatBskyConvoUpdateAllRead from './types/chat/bsky/convo/updateAllRead.js'\nimport * as ChatBskyConvoUpdateRead from './types/chat/bsky/convo/updateRead.js'\nimport * as ChatBskyModerationGetActorMetadata from './types/chat/bsky/moderation/getActorMetadata.js'\nimport * as ChatBskyModerationGetMessageContext from './types/chat/bsky/moderation/getMessageContext.js'\nimport * as ChatBskyModerationUpdateActorAccess from './types/chat/bsky/moderation/updateActorAccess.js'\nimport * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs.js'\nimport * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount.js'\nimport * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites.js'\nimport * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes.js'\nimport * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites.js'\nimport * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo.js'\nimport * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos.js'\nimport * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes.js'\nimport * as ComAtprotoAdminGetSubjectStatus from './types/com/atproto/admin/getSubjectStatus.js'\nimport * as ComAtprotoAdminSearchAccounts from './types/com/atproto/admin/searchAccounts.js'\nimport * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail.js'\nimport * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail.js'\nimport * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle.js'\nimport * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword.js'\nimport * as ComAtprotoAdminUpdateAccountSigningKey from './types/com/atproto/admin/updateAccountSigningKey.js'\nimport * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus.js'\nimport * as ComAtprotoIdentityDefs from './types/com/atproto/identity/defs.js'\nimport * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials.js'\nimport * as ComAtprotoIdentityRefreshIdentity from './types/com/atproto/identity/refreshIdentity.js'\nimport * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature.js'\nimport * as ComAtprotoIdentityResolveDid from './types/com/atproto/identity/resolveDid.js'\nimport * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle.js'\nimport * as ComAtprotoIdentityResolveIdentity from './types/com/atproto/identity/resolveIdentity.js'\nimport * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation.js'\nimport * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation.js'\nimport * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle.js'\nimport * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js'\nimport * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels.js'\nimport * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels.js'\nimport * as ComAtprotoLexiconResolveLexicon from './types/com/atproto/lexicon/resolveLexicon.js'\nimport * as ComAtprotoLexiconSchema from './types/com/atproto/lexicon/schema.js'\nimport * as ComAtprotoModerationCreateReport from './types/com/atproto/moderation/createReport.js'\nimport * as ComAtprotoModerationDefs from './types/com/atproto/moderation/defs.js'\nimport * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'\nimport * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js'\nimport * as ComAtprotoRepoDefs from './types/com/atproto/repo/defs.js'\nimport * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js'\nimport * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js'\nimport * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js'\nimport * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js'\nimport * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js'\nimport * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js'\nimport * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js'\nimport * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef.js'\nimport * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'\nimport * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount.js'\nimport * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus.js'\nimport * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail.js'\nimport * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount.js'\nimport * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword.js'\nimport * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode.js'\nimport * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes.js'\nimport * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession.js'\nimport * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount.js'\nimport * as ComAtprotoServerDefs from './types/com/atproto/server/defs.js'\nimport * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount.js'\nimport * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession.js'\nimport * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer.js'\nimport * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes.js'\nimport * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth.js'\nimport * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession.js'\nimport * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords.js'\nimport * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession.js'\nimport * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete.js'\nimport * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation.js'\nimport * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate.js'\nimport * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset.js'\nimport * as ComAtprotoServerReserveSigningKey from './types/com/atproto/server/reserveSigningKey.js'\nimport * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword.js'\nimport * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword.js'\nimport * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail.js'\nimport * as ComAtprotoSyncDefs from './types/com/atproto/sync/defs.js'\nimport * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob.js'\nimport * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks.js'\nimport * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout.js'\nimport * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead.js'\nimport * as ComAtprotoSyncGetHostStatus from './types/com/atproto/sync/getHostStatus.js'\nimport * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit.js'\nimport * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord.js'\nimport * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo.js'\nimport * as ComAtprotoSyncGetRepoStatus from './types/com/atproto/sync/getRepoStatus.js'\nimport * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs.js'\nimport * as ComAtprotoSyncListHosts from './types/com/atproto/sync/listHosts.js'\nimport * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos.js'\nimport * as ComAtprotoSyncListReposByCollection from './types/com/atproto/sync/listReposByCollection.js'\nimport * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate.js'\nimport * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl.js'\nimport * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos.js'\nimport * as ComAtprotoTempAddReservedHandle from './types/com/atproto/temp/addReservedHandle.js'\nimport * as ComAtprotoTempCheckHandleAvailability from './types/com/atproto/temp/checkHandleAvailability.js'\nimport * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue.js'\nimport * as ComAtprotoTempDereferenceScope from './types/com/atproto/temp/dereferenceScope.js'\nimport * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels.js'\nimport * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification.js'\nimport * as ComAtprotoTempRevokeAccountCredentials from './types/com/atproto/temp/revokeAccountCredentials.js'\nimport * as ComGermnetworkDeclaration from './types/com/germnetwork/declaration.js'\nimport * as ToolsOzoneCommunicationCreateTemplate from './types/tools/ozone/communication/createTemplate.js'\nimport * as ToolsOzoneCommunicationDefs from './types/tools/ozone/communication/defs.js'\nimport * as ToolsOzoneCommunicationDeleteTemplate from './types/tools/ozone/communication/deleteTemplate.js'\nimport * as ToolsOzoneCommunicationListTemplates from './types/tools/ozone/communication/listTemplates.js'\nimport * as ToolsOzoneCommunicationUpdateTemplate from './types/tools/ozone/communication/updateTemplate.js'\nimport * as ToolsOzoneHostingGetAccountHistory from './types/tools/ozone/hosting/getAccountHistory.js'\nimport * as ToolsOzoneModerationCancelScheduledActions from './types/tools/ozone/moderation/cancelScheduledActions.js'\nimport * as ToolsOzoneModerationDefs from './types/tools/ozone/moderation/defs.js'\nimport * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent.js'\nimport * as ToolsOzoneModerationGetAccountTimeline from './types/tools/ozone/moderation/getAccountTimeline.js'\nimport * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent.js'\nimport * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord.js'\nimport * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords.js'\nimport * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/getRepo.js'\nimport * as ToolsOzoneModerationGetReporterStats from './types/tools/ozone/moderation/getReporterStats.js'\nimport * as ToolsOzoneModerationGetRepos from './types/tools/ozone/moderation/getRepos.js'\nimport * as ToolsOzoneModerationGetSubjects from './types/tools/ozone/moderation/getSubjects.js'\nimport * as ToolsOzoneModerationListScheduledActions from './types/tools/ozone/moderation/listScheduledActions.js'\nimport * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents.js'\nimport * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses.js'\nimport * as ToolsOzoneModerationScheduleAction from './types/tools/ozone/moderation/scheduleAction.js'\nimport * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos.js'\nimport * as ToolsOzoneReportDefs from './types/tools/ozone/report/defs.js'\nimport * as ToolsOzoneSafelinkAddRule from './types/tools/ozone/safelink/addRule.js'\nimport * as ToolsOzoneSafelinkDefs from './types/tools/ozone/safelink/defs.js'\nimport * as ToolsOzoneSafelinkQueryEvents from './types/tools/ozone/safelink/queryEvents.js'\nimport * as ToolsOzoneSafelinkQueryRules from './types/tools/ozone/safelink/queryRules.js'\nimport * as ToolsOzoneSafelinkRemoveRule from './types/tools/ozone/safelink/removeRule.js'\nimport * as ToolsOzoneSafelinkUpdateRule from './types/tools/ozone/safelink/updateRule.js'\nimport * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig.js'\nimport * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues.js'\nimport * as ToolsOzoneSetDefs from './types/tools/ozone/set/defs.js'\nimport * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet.js'\nimport * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues.js'\nimport * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues.js'\nimport * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets.js'\nimport * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet.js'\nimport * as ToolsOzoneSettingDefs from './types/tools/ozone/setting/defs.js'\nimport * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions.js'\nimport * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions.js'\nimport * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption.js'\nimport * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs.js'\nimport * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation.js'\nimport * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts.js'\nimport * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts.js'\nimport * as ToolsOzoneTeamAddMember from './types/tools/ozone/team/addMember.js'\nimport * as ToolsOzoneTeamDefs from './types/tools/ozone/team/defs.js'\nimport * as ToolsOzoneTeamDeleteMember from './types/tools/ozone/team/deleteMember.js'\nimport * as ToolsOzoneTeamListMembers from './types/tools/ozone/team/listMembers.js'\nimport * as ToolsOzoneTeamUpdateMember from './types/tools/ozone/team/updateMember.js'\nimport * as ToolsOzoneVerificationDefs from './types/tools/ozone/verification/defs.js'\nimport * as ToolsOzoneVerificationGrantVerifications from './types/tools/ozone/verification/grantVerifications.js'\nimport * as ToolsOzoneVerificationListVerifications from './types/tools/ozone/verification/listVerifications.js'\nimport * as ToolsOzoneVerificationRevokeVerifications from './types/tools/ozone/verification/revokeVerifications.js'\n\nexport * as AppBskyActorDefs from './types/app/bsky/actor/defs.js'\nexport * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences.js'\nexport * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile.js'\nexport * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles.js'\nexport * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions.js'\nexport * as AppBskyActorProfile from './types/app/bsky/actor/profile.js'\nexport * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences.js'\nexport * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors.js'\nexport * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead.js'\nexport * as AppBskyActorStatus from './types/app/bsky/actor/status.js'\nexport * as AppBskyAgeassuranceBegin from './types/app/bsky/ageassurance/begin.js'\nexport * as AppBskyAgeassuranceDefs from './types/app/bsky/ageassurance/defs.js'\nexport * as AppBskyAgeassuranceGetConfig from './types/app/bsky/ageassurance/getConfig.js'\nexport * as AppBskyAgeassuranceGetState from './types/app/bsky/ageassurance/getState.js'\nexport * as AppBskyBookmarkCreateBookmark from './types/app/bsky/bookmark/createBookmark.js'\nexport * as AppBskyBookmarkDefs from './types/app/bsky/bookmark/defs.js'\nexport * as AppBskyBookmarkDeleteBookmark from './types/app/bsky/bookmark/deleteBookmark.js'\nexport * as AppBskyBookmarkGetBookmarks from './types/app/bsky/bookmark/getBookmarks.js'\nexport * as AppBskyContactDefs from './types/app/bsky/contact/defs.js'\nexport * as AppBskyContactDismissMatch from './types/app/bsky/contact/dismissMatch.js'\nexport * as AppBskyContactGetMatches from './types/app/bsky/contact/getMatches.js'\nexport * as AppBskyContactGetSyncStatus from './types/app/bsky/contact/getSyncStatus.js'\nexport * as AppBskyContactImportContacts from './types/app/bsky/contact/importContacts.js'\nexport * as AppBskyContactRemoveData from './types/app/bsky/contact/removeData.js'\nexport * as AppBskyContactSendNotification from './types/app/bsky/contact/sendNotification.js'\nexport * as AppBskyContactStartPhoneVerification from './types/app/bsky/contact/startPhoneVerification.js'\nexport * as AppBskyContactVerifyPhone from './types/app/bsky/contact/verifyPhone.js'\nexport * as AppBskyDraftCreateDraft from './types/app/bsky/draft/createDraft.js'\nexport * as AppBskyDraftDefs from './types/app/bsky/draft/defs.js'\nexport * as AppBskyDraftDeleteDraft from './types/app/bsky/draft/deleteDraft.js'\nexport * as AppBskyDraftGetDrafts from './types/app/bsky/draft/getDrafts.js'\nexport * as AppBskyDraftUpdateDraft from './types/app/bsky/draft/updateDraft.js'\nexport * as AppBskyEmbedDefs from './types/app/bsky/embed/defs.js'\nexport * as AppBskyEmbedExternal from './types/app/bsky/embed/external.js'\nexport * as AppBskyEmbedImages from './types/app/bsky/embed/images.js'\nexport * as AppBskyEmbedRecord from './types/app/bsky/embed/record.js'\nexport * as AppBskyEmbedRecordWithMedia from './types/app/bsky/embed/recordWithMedia.js'\nexport * as AppBskyEmbedVideo from './types/app/bsky/embed/video.js'\nexport * as AppBskyFeedDefs from './types/app/bsky/feed/defs.js'\nexport * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator.js'\nexport * as AppBskyFeedGenerator from './types/app/bsky/feed/generator.js'\nexport * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds.js'\nexport * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes.js'\nexport * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed.js'\nexport * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed.js'\nexport * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator.js'\nexport * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators.js'\nexport * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'\nexport * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'\nexport * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'\nexport * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'\nexport * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'\nexport * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'\nexport * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'\nexport * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'\nexport * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline.js'\nexport * as AppBskyFeedLike from './types/app/bsky/feed/like.js'\nexport * as AppBskyFeedPost from './types/app/bsky/feed/post.js'\nexport * as AppBskyFeedPostgate from './types/app/bsky/feed/postgate.js'\nexport * as AppBskyFeedRepost from './types/app/bsky/feed/repost.js'\nexport * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts.js'\nexport * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions.js'\nexport * as AppBskyFeedThreadgate from './types/app/bsky/feed/threadgate.js'\nexport * as AppBskyGraphBlock from './types/app/bsky/graph/block.js'\nexport * as AppBskyGraphDefs from './types/app/bsky/graph/defs.js'\nexport * as AppBskyGraphFollow from './types/app/bsky/graph/follow.js'\nexport * as AppBskyGraphGetActorStarterPacks from './types/app/bsky/graph/getActorStarterPacks.js'\nexport * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks.js'\nexport * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers.js'\nexport * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows.js'\nexport * as AppBskyGraphGetKnownFollowers from './types/app/bsky/graph/getKnownFollowers.js'\nexport * as AppBskyGraphGetList from './types/app/bsky/graph/getList.js'\nexport * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks.js'\nexport * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes.js'\nexport * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists.js'\nexport * as AppBskyGraphGetListsWithMembership from './types/app/bsky/graph/getListsWithMembership.js'\nexport * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes.js'\nexport * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelationships.js'\nexport * as AppBskyGraphGetStarterPack from './types/app/bsky/graph/getStarterPack.js'\nexport * as AppBskyGraphGetStarterPacks from './types/app/bsky/graph/getStarterPacks.js'\nexport * as AppBskyGraphGetStarterPacksWithMembership from './types/app/bsky/graph/getStarterPacksWithMembership.js'\nexport * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor.js'\nexport * as AppBskyGraphList from './types/app/bsky/graph/list.js'\nexport * as AppBskyGraphListblock from './types/app/bsky/graph/listblock.js'\nexport * as AppBskyGraphListitem from './types/app/bsky/graph/listitem.js'\nexport * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor.js'\nexport * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList.js'\nexport * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread.js'\nexport * as AppBskyGraphSearchStarterPacks from './types/app/bsky/graph/searchStarterPacks.js'\nexport * as AppBskyGraphStarterpack from './types/app/bsky/graph/starterpack.js'\nexport * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'\nexport * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'\nexport * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'\nexport * as AppBskyGraphVerification from './types/app/bsky/graph/verification.js'\nexport * as AppBskyLabelerDefs from './types/app/bsky/labeler/defs.js'\nexport * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'\nexport * as AppBskyLabelerService from './types/app/bsky/labeler/service.js'\nexport * as AppBskyNotificationDeclaration from './types/app/bsky/notification/declaration.js'\nexport * as AppBskyNotificationDefs from './types/app/bsky/notification/defs.js'\nexport * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'\nexport * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'\nexport * as AppBskyNotificationListActivitySubscriptions from './types/app/bsky/notification/listActivitySubscriptions.js'\nexport * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'\nexport * as AppBskyNotificationPutActivitySubscription from './types/app/bsky/notification/putActivitySubscription.js'\nexport * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'\nexport * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'\nexport * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'\nexport * as AppBskyNotificationUnregisterPush from './types/app/bsky/notification/unregisterPush.js'\nexport * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'\nexport * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet.js'\nexport * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs.js'\nexport * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'\nexport * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'\nexport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacks from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js'\nexport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.js'\nexport * as AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.js'\nexport * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'\nexport * as AppBskyUnspeccedGetPostThreadOtherV2 from './types/app/bsky/unspecced/getPostThreadOtherV2.js'\nexport * as AppBskyUnspeccedGetPostThreadV2 from './types/app/bsky/unspecced/getPostThreadV2.js'\nexport * as AppBskyUnspeccedGetSuggestedFeeds from './types/app/bsky/unspecced/getSuggestedFeeds.js'\nexport * as AppBskyUnspeccedGetSuggestedFeedsSkeleton from './types/app/bsky/unspecced/getSuggestedFeedsSkeleton.js'\nexport * as AppBskyUnspeccedGetSuggestedOnboardingUsers from './types/app/bsky/unspecced/getSuggestedOnboardingUsers.js'\nexport * as AppBskyUnspeccedGetSuggestedStarterPacks from './types/app/bsky/unspecced/getSuggestedStarterPacks.js'\nexport * as AppBskyUnspeccedGetSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js'\nexport * as AppBskyUnspeccedGetSuggestedUsers from './types/app/bsky/unspecced/getSuggestedUsers.js'\nexport * as AppBskyUnspeccedGetSuggestedUsersSkeleton from './types/app/bsky/unspecced/getSuggestedUsersSkeleton.js'\nexport * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspecced/getSuggestionsSkeleton.js'\nexport * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions.js'\nexport * as AppBskyUnspeccedGetTrendingTopics from './types/app/bsky/unspecced/getTrendingTopics.js'\nexport * as AppBskyUnspeccedGetTrends from './types/app/bsky/unspecced/getTrends.js'\nexport * as AppBskyUnspeccedGetTrendsSkeleton from './types/app/bsky/unspecced/getTrendsSkeleton.js'\nexport * as AppBskyUnspeccedInitAgeAssurance from './types/app/bsky/unspecced/initAgeAssurance.js'\nexport * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton.js'\nexport * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton.js'\nexport * as AppBskyUnspeccedSearchStarterPacksSkeleton from './types/app/bsky/unspecced/searchStarterPacksSkeleton.js'\nexport * as AppBskyVideoDefs from './types/app/bsky/video/defs.js'\nexport * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus.js'\nexport * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits.js'\nexport * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo.js'\nexport * as ChatBskyActorDeclaration from './types/chat/bsky/actor/declaration.js'\nexport * as ChatBskyActorDefs from './types/chat/bsky/actor/defs.js'\nexport * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount.js'\nexport * as ChatBskyActorExportAccountData from './types/chat/bsky/actor/exportAccountData.js'\nexport * as ChatBskyConvoAcceptConvo from './types/chat/bsky/convo/acceptConvo.js'\nexport * as ChatBskyConvoAddReaction from './types/chat/bsky/convo/addReaction.js'\nexport * as ChatBskyConvoDefs from './types/chat/bsky/convo/defs.js'\nexport * as ChatBskyConvoDeleteMessageForSelf from './types/chat/bsky/convo/deleteMessageForSelf.js'\nexport * as ChatBskyConvoGetConvo from './types/chat/bsky/convo/getConvo.js'\nexport * as ChatBskyConvoGetConvoAvailability from './types/chat/bsky/convo/getConvoAvailability.js'\nexport * as ChatBskyConvoGetConvoForMembers from './types/chat/bsky/convo/getConvoForMembers.js'\nexport * as ChatBskyConvoGetLog from './types/chat/bsky/convo/getLog.js'\nexport * as ChatBskyConvoGetMessages from './types/chat/bsky/convo/getMessages.js'\nexport * as ChatBskyConvoLeaveConvo from './types/chat/bsky/convo/leaveConvo.js'\nexport * as ChatBskyConvoListConvos from './types/chat/bsky/convo/listConvos.js'\nexport * as ChatBskyConvoMuteConvo from './types/chat/bsky/convo/muteConvo.js'\nexport * as ChatBskyConvoRemoveReaction from './types/chat/bsky/convo/removeReaction.js'\nexport * as ChatBskyConvoSendMessage from './types/chat/bsky/convo/sendMessage.js'\nexport * as ChatBskyConvoSendMessageBatch from './types/chat/bsky/convo/sendMessageBatch.js'\nexport * as ChatBskyConvoUnmuteConvo from './types/chat/bsky/convo/unmuteConvo.js'\nexport * as ChatBskyConvoUpdateAllRead from './types/chat/bsky/convo/updateAllRead.js'\nexport * as ChatBskyConvoUpdateRead from './types/chat/bsky/convo/updateRead.js'\nexport * as ChatBskyModerationGetActorMetadata from './types/chat/bsky/moderation/getActorMetadata.js'\nexport * as ChatBskyModerationGetMessageContext from './types/chat/bsky/moderation/getMessageContext.js'\nexport * as ChatBskyModerationUpdateActorAccess from './types/chat/bsky/moderation/updateActorAccess.js'\nexport * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs.js'\nexport * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount.js'\nexport * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites.js'\nexport * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes.js'\nexport * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites.js'\nexport * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo.js'\nexport * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos.js'\nexport * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes.js'\nexport * as ComAtprotoAdminGetSubjectStatus from './types/com/atproto/admin/getSubjectStatus.js'\nexport * as ComAtprotoAdminSearchAccounts from './types/com/atproto/admin/searchAccounts.js'\nexport * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail.js'\nexport * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail.js'\nexport * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle.js'\nexport * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword.js'\nexport * as ComAtprotoAdminUpdateAccountSigningKey from './types/com/atproto/admin/updateAccountSigningKey.js'\nexport * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus.js'\nexport * as ComAtprotoIdentityDefs from './types/com/atproto/identity/defs.js'\nexport * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials.js'\nexport * as ComAtprotoIdentityRefreshIdentity from './types/com/atproto/identity/refreshIdentity.js'\nexport * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature.js'\nexport * as ComAtprotoIdentityResolveDid from './types/com/atproto/identity/resolveDid.js'\nexport * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle.js'\nexport * as ComAtprotoIdentityResolveIdentity from './types/com/atproto/identity/resolveIdentity.js'\nexport * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation.js'\nexport * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation.js'\nexport * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle.js'\nexport * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js'\nexport * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels.js'\nexport * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels.js'\nexport * as ComAtprotoLexiconResolveLexicon from './types/com/atproto/lexicon/resolveLexicon.js'\nexport * as ComAtprotoLexiconSchema from './types/com/atproto/lexicon/schema.js'\nexport * as ComAtprotoModerationCreateReport from './types/com/atproto/moderation/createReport.js'\nexport * as ComAtprotoModerationDefs from './types/com/atproto/moderation/defs.js'\nexport * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'\nexport * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js'\nexport * as ComAtprotoRepoDefs from './types/com/atproto/repo/defs.js'\nexport * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js'\nexport * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js'\nexport * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js'\nexport * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js'\nexport * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js'\nexport * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js'\nexport * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js'\nexport * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef.js'\nexport * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'\nexport * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount.js'\nexport * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus.js'\nexport * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail.js'\nexport * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount.js'\nexport * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword.js'\nexport * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode.js'\nexport * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes.js'\nexport * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession.js'\nexport * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount.js'\nexport * as ComAtprotoServerDefs from './types/com/atproto/server/defs.js'\nexport * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount.js'\nexport * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession.js'\nexport * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer.js'\nexport * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes.js'\nexport * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth.js'\nexport * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession.js'\nexport * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords.js'\nexport * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession.js'\nexport * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete.js'\nexport * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation.js'\nexport * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate.js'\nexport * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset.js'\nexport * as ComAtprotoServerReserveSigningKey from './types/com/atproto/server/reserveSigningKey.js'\nexport * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword.js'\nexport * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword.js'\nexport * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail.js'\nexport * as ComAtprotoSyncDefs from './types/com/atproto/sync/defs.js'\nexport * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob.js'\nexport * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks.js'\nexport * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout.js'\nexport * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead.js'\nexport * as ComAtprotoSyncGetHostStatus from './types/com/atproto/sync/getHostStatus.js'\nexport * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit.js'\nexport * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord.js'\nexport * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo.js'\nexport * as ComAtprotoSyncGetRepoStatus from './types/com/atproto/sync/getRepoStatus.js'\nexport * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs.js'\nexport * as ComAtprotoSyncListHosts from './types/com/atproto/sync/listHosts.js'\nexport * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos.js'\nexport * as ComAtprotoSyncListReposByCollection from './types/com/atproto/sync/listReposByCollection.js'\nexport * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate.js'\nexport * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl.js'\nexport * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos.js'\nexport * as ComAtprotoTempAddReservedHandle from './types/com/atproto/temp/addReservedHandle.js'\nexport * as ComAtprotoTempCheckHandleAvailability from './types/com/atproto/temp/checkHandleAvailability.js'\nexport * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue.js'\nexport * as ComAtprotoTempDereferenceScope from './types/com/atproto/temp/dereferenceScope.js'\nexport * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels.js'\nexport * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification.js'\nexport * as ComAtprotoTempRevokeAccountCredentials from './types/com/atproto/temp/revokeAccountCredentials.js'\nexport * as ComGermnetworkDeclaration from './types/com/germnetwork/declaration.js'\nexport * as ToolsOzoneCommunicationCreateTemplate from './types/tools/ozone/communication/createTemplate.js'\nexport * as ToolsOzoneCommunicationDefs from './types/tools/ozone/communication/defs.js'\nexport * as ToolsOzoneCommunicationDeleteTemplate from './types/tools/ozone/communication/deleteTemplate.js'\nexport * as ToolsOzoneCommunicationListTemplates from './types/tools/ozone/communication/listTemplates.js'\nexport * as ToolsOzoneCommunicationUpdateTemplate from './types/tools/ozone/communication/updateTemplate.js'\nexport * as ToolsOzoneHostingGetAccountHistory from './types/tools/ozone/hosting/getAccountHistory.js'\nexport * as ToolsOzoneModerationCancelScheduledActions from './types/tools/ozone/moderation/cancelScheduledActions.js'\nexport * as ToolsOzoneModerationDefs from './types/tools/ozone/moderation/defs.js'\nexport * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent.js'\nexport * as ToolsOzoneModerationGetAccountTimeline from './types/tools/ozone/moderation/getAccountTimeline.js'\nexport * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent.js'\nexport * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord.js'\nexport * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords.js'\nexport * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/getRepo.js'\nexport * as ToolsOzoneModerationGetReporterStats from './types/tools/ozone/moderation/getReporterStats.js'\nexport * as ToolsOzoneModerationGetRepos from './types/tools/ozone/moderation/getRepos.js'\nexport * as ToolsOzoneModerationGetSubjects from './types/tools/ozone/moderation/getSubjects.js'\nexport * as ToolsOzoneModerationListScheduledActions from './types/tools/ozone/moderation/listScheduledActions.js'\nexport * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents.js'\nexport * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses.js'\nexport * as ToolsOzoneModerationScheduleAction from './types/tools/ozone/moderation/scheduleAction.js'\nexport * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos.js'\nexport * as ToolsOzoneReportDefs from './types/tools/ozone/report/defs.js'\nexport * as ToolsOzoneSafelinkAddRule from './types/tools/ozone/safelink/addRule.js'\nexport * as ToolsOzoneSafelinkDefs from './types/tools/ozone/safelink/defs.js'\nexport * as ToolsOzoneSafelinkQueryEvents from './types/tools/ozone/safelink/queryEvents.js'\nexport * as ToolsOzoneSafelinkQueryRules from './types/tools/ozone/safelink/queryRules.js'\nexport * as ToolsOzoneSafelinkRemoveRule from './types/tools/ozone/safelink/removeRule.js'\nexport * as ToolsOzoneSafelinkUpdateRule from './types/tools/ozone/safelink/updateRule.js'\nexport * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig.js'\nexport * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues.js'\nexport * as ToolsOzoneSetDefs from './types/tools/ozone/set/defs.js'\nexport * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet.js'\nexport * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues.js'\nexport * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues.js'\nexport * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets.js'\nexport * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet.js'\nexport * as ToolsOzoneSettingDefs from './types/tools/ozone/setting/defs.js'\nexport * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions.js'\nexport * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions.js'\nexport * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption.js'\nexport * as ToolsOzoneSignatureDefs from './types/tools/ozone/signature/defs.js'\nexport * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation.js'\nexport * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts.js'\nexport * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts.js'\nexport * as ToolsOzoneTeamAddMember from './types/tools/ozone/team/addMember.js'\nexport * as ToolsOzoneTeamDefs from './types/tools/ozone/team/defs.js'\nexport * as ToolsOzoneTeamDeleteMember from './types/tools/ozone/team/deleteMember.js'\nexport * as ToolsOzoneTeamListMembers from './types/tools/ozone/team/listMembers.js'\nexport * as ToolsOzoneTeamUpdateMember from './types/tools/ozone/team/updateMember.js'\nexport * as ToolsOzoneVerificationDefs from './types/tools/ozone/verification/defs.js'\nexport * as ToolsOzoneVerificationGrantVerifications from './types/tools/ozone/verification/grantVerifications.js'\nexport * as ToolsOzoneVerificationListVerifications from './types/tools/ozone/verification/listVerifications.js'\nexport * as ToolsOzoneVerificationRevokeVerifications from './types/tools/ozone/verification/revokeVerifications.js'\n\nexport const APP_BSKY_ACTOR = {\n  StatusLive: 'app.bsky.actor.status#live',\n}\nexport const APP_BSKY_FEED = {\n  DefsRequestLess: 'app.bsky.feed.defs#requestLess',\n  DefsRequestMore: 'app.bsky.feed.defs#requestMore',\n  DefsClickthroughItem: 'app.bsky.feed.defs#clickthroughItem',\n  DefsClickthroughAuthor: 'app.bsky.feed.defs#clickthroughAuthor',\n  DefsClickthroughReposter: 'app.bsky.feed.defs#clickthroughReposter',\n  DefsClickthroughEmbed: 'app.bsky.feed.defs#clickthroughEmbed',\n  DefsContentModeUnspecified: 'app.bsky.feed.defs#contentModeUnspecified',\n  DefsContentModeVideo: 'app.bsky.feed.defs#contentModeVideo',\n  DefsInteractionSeen: 'app.bsky.feed.defs#interactionSeen',\n  DefsInteractionLike: 'app.bsky.feed.defs#interactionLike',\n  DefsInteractionRepost: 'app.bsky.feed.defs#interactionRepost',\n  DefsInteractionReply: 'app.bsky.feed.defs#interactionReply',\n  DefsInteractionQuote: 'app.bsky.feed.defs#interactionQuote',\n  DefsInteractionShare: 'app.bsky.feed.defs#interactionShare',\n}\nexport const APP_BSKY_GRAPH = {\n  DefsModlist: 'app.bsky.graph.defs#modlist',\n  DefsCuratelist: 'app.bsky.graph.defs#curatelist',\n  DefsReferencelist: 'app.bsky.graph.defs#referencelist',\n}\nexport const COM_ATPROTO_MODERATION = {\n  DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam',\n  DefsReasonViolation: 'com.atproto.moderation.defs#reasonViolation',\n  DefsReasonMisleading: 'com.atproto.moderation.defs#reasonMisleading',\n  DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',\n  DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',\n  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',\n  DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',\n}\nexport const TOOLS_OZONE_MODERATION = {\n  DefsReviewOpen: 'tools.ozone.moderation.defs#reviewOpen',\n  DefsReviewEscalated: 'tools.ozone.moderation.defs#reviewEscalated',\n  DefsReviewClosed: 'tools.ozone.moderation.defs#reviewClosed',\n  DefsReviewNone: 'tools.ozone.moderation.defs#reviewNone',\n  DefsTimelineEventPlcCreate:\n    'tools.ozone.moderation.defs#timelineEventPlcCreate',\n  DefsTimelineEventPlcOperation:\n    'tools.ozone.moderation.defs#timelineEventPlcOperation',\n  DefsTimelineEventPlcTombstone:\n    'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n}\nexport const TOOLS_OZONE_REPORT = {\n  DefsReasonAppeal: 'tools.ozone.report.defs#reasonAppeal',\n  DefsReasonOther: 'tools.ozone.report.defs#reasonOther',\n  DefsReasonViolenceAnimal: 'tools.ozone.report.defs#reasonViolenceAnimal',\n  DefsReasonViolenceThreats: 'tools.ozone.report.defs#reasonViolenceThreats',\n  DefsReasonViolenceGraphicContent:\n    'tools.ozone.report.defs#reasonViolenceGraphicContent',\n  DefsReasonViolenceGlorification:\n    'tools.ozone.report.defs#reasonViolenceGlorification',\n  DefsReasonViolenceExtremistContent:\n    'tools.ozone.report.defs#reasonViolenceExtremistContent',\n  DefsReasonViolenceTrafficking:\n    'tools.ozone.report.defs#reasonViolenceTrafficking',\n  DefsReasonViolenceOther: 'tools.ozone.report.defs#reasonViolenceOther',\n  DefsReasonSexualAbuseContent:\n    'tools.ozone.report.defs#reasonSexualAbuseContent',\n  DefsReasonSexualNCII: 'tools.ozone.report.defs#reasonSexualNCII',\n  DefsReasonSexualDeepfake: 'tools.ozone.report.defs#reasonSexualDeepfake',\n  DefsReasonSexualAnimal: 'tools.ozone.report.defs#reasonSexualAnimal',\n  DefsReasonSexualUnlabeled: 'tools.ozone.report.defs#reasonSexualUnlabeled',\n  DefsReasonSexualOther: 'tools.ozone.report.defs#reasonSexualOther',\n  DefsReasonChildSafetyCSAM: 'tools.ozone.report.defs#reasonChildSafetyCSAM',\n  DefsReasonChildSafetyGroom: 'tools.ozone.report.defs#reasonChildSafetyGroom',\n  DefsReasonChildSafetyPrivacy:\n    'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n  DefsReasonChildSafetyHarassment:\n    'tools.ozone.report.defs#reasonChildSafetyHarassment',\n  DefsReasonChildSafetyOther: 'tools.ozone.report.defs#reasonChildSafetyOther',\n  DefsReasonHarassmentTroll: 'tools.ozone.report.defs#reasonHarassmentTroll',\n  DefsReasonHarassmentTargeted:\n    'tools.ozone.report.defs#reasonHarassmentTargeted',\n  DefsReasonHarassmentHateSpeech:\n    'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n  DefsReasonHarassmentDoxxing:\n    'tools.ozone.report.defs#reasonHarassmentDoxxing',\n  DefsReasonHarassmentOther: 'tools.ozone.report.defs#reasonHarassmentOther',\n  DefsReasonMisleadingBot: 'tools.ozone.report.defs#reasonMisleadingBot',\n  DefsReasonMisleadingImpersonation:\n    'tools.ozone.report.defs#reasonMisleadingImpersonation',\n  DefsReasonMisleadingSpam: 'tools.ozone.report.defs#reasonMisleadingSpam',\n  DefsReasonMisleadingScam: 'tools.ozone.report.defs#reasonMisleadingScam',\n  DefsReasonMisleadingElections:\n    'tools.ozone.report.defs#reasonMisleadingElections',\n  DefsReasonMisleadingOther: 'tools.ozone.report.defs#reasonMisleadingOther',\n  DefsReasonRuleSiteSecurity: 'tools.ozone.report.defs#reasonRuleSiteSecurity',\n  DefsReasonRuleProhibitedSales:\n    'tools.ozone.report.defs#reasonRuleProhibitedSales',\n  DefsReasonRuleBanEvasion: 'tools.ozone.report.defs#reasonRuleBanEvasion',\n  DefsReasonRuleOther: 'tools.ozone.report.defs#reasonRuleOther',\n  DefsReasonSelfHarmContent: 'tools.ozone.report.defs#reasonSelfHarmContent',\n  DefsReasonSelfHarmED: 'tools.ozone.report.defs#reasonSelfHarmED',\n  DefsReasonSelfHarmStunts: 'tools.ozone.report.defs#reasonSelfHarmStunts',\n  DefsReasonSelfHarmSubstances:\n    'tools.ozone.report.defs#reasonSelfHarmSubstances',\n  DefsReasonSelfHarmOther: 'tools.ozone.report.defs#reasonSelfHarmOther',\n}\nexport const TOOLS_OZONE_TEAM = {\n  DefsRoleAdmin: 'tools.ozone.team.defs#roleAdmin',\n  DefsRoleModerator: 'tools.ozone.team.defs#roleModerator',\n  DefsRoleTriage: 'tools.ozone.team.defs#roleTriage',\n  DefsRoleVerifier: 'tools.ozone.team.defs#roleVerifier',\n}\n\nexport class AtpBaseClient extends XrpcClient {\n  app: AppNS\n  chat: ChatNS\n  com: ComNS\n  tools: ToolsNS\n\n  constructor(options: FetchHandler | FetchHandlerOptions) {\n    super(options, schemas)\n    this.app = new AppNS(this)\n    this.chat = new ChatNS(this)\n    this.com = new ComNS(this)\n    this.tools = new ToolsNS(this)\n  }\n\n  /** @deprecated use `this` instead */\n  get xrpc(): XrpcClient {\n    return this\n  }\n}\n\nexport class AppNS {\n  _client: XrpcClient\n  bsky: AppBskyNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.bsky = new AppBskyNS(client)\n  }\n}\n\nexport class AppBskyNS {\n  _client: XrpcClient\n  actor: AppBskyActorNS\n  ageassurance: AppBskyAgeassuranceNS\n  bookmark: AppBskyBookmarkNS\n  contact: AppBskyContactNS\n  draft: AppBskyDraftNS\n  embed: AppBskyEmbedNS\n  feed: AppBskyFeedNS\n  graph: AppBskyGraphNS\n  labeler: AppBskyLabelerNS\n  notification: AppBskyNotificationNS\n  richtext: AppBskyRichtextNS\n  unspecced: AppBskyUnspeccedNS\n  video: AppBskyVideoNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.actor = new AppBskyActorNS(client)\n    this.ageassurance = new AppBskyAgeassuranceNS(client)\n    this.bookmark = new AppBskyBookmarkNS(client)\n    this.contact = new AppBskyContactNS(client)\n    this.draft = new AppBskyDraftNS(client)\n    this.embed = new AppBskyEmbedNS(client)\n    this.feed = new AppBskyFeedNS(client)\n    this.graph = new AppBskyGraphNS(client)\n    this.labeler = new AppBskyLabelerNS(client)\n    this.notification = new AppBskyNotificationNS(client)\n    this.richtext = new AppBskyRichtextNS(client)\n    this.unspecced = new AppBskyUnspeccedNS(client)\n    this.video = new AppBskyVideoNS(client)\n  }\n}\n\nexport class AppBskyActorNS {\n  _client: XrpcClient\n  profile: AppBskyActorProfileRecord\n  status: AppBskyActorStatusRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.profile = new AppBskyActorProfileRecord(client)\n    this.status = new AppBskyActorStatusRecord(client)\n  }\n\n  getPreferences(\n    params?: AppBskyActorGetPreferences.QueryParams,\n    opts?: AppBskyActorGetPreferences.CallOptions,\n  ): Promise<AppBskyActorGetPreferences.Response> {\n    return this._client.call(\n      'app.bsky.actor.getPreferences',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getProfile(\n    params?: AppBskyActorGetProfile.QueryParams,\n    opts?: AppBskyActorGetProfile.CallOptions,\n  ): Promise<AppBskyActorGetProfile.Response> {\n    return this._client.call(\n      'app.bsky.actor.getProfile',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getProfiles(\n    params?: AppBskyActorGetProfiles.QueryParams,\n    opts?: AppBskyActorGetProfiles.CallOptions,\n  ): Promise<AppBskyActorGetProfiles.Response> {\n    return this._client.call(\n      'app.bsky.actor.getProfiles',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestions(\n    params?: AppBskyActorGetSuggestions.QueryParams,\n    opts?: AppBskyActorGetSuggestions.CallOptions,\n  ): Promise<AppBskyActorGetSuggestions.Response> {\n    return this._client.call(\n      'app.bsky.actor.getSuggestions',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  putPreferences(\n    data?: AppBskyActorPutPreferences.InputSchema,\n    opts?: AppBskyActorPutPreferences.CallOptions,\n  ): Promise<AppBskyActorPutPreferences.Response> {\n    return this._client.call(\n      'app.bsky.actor.putPreferences',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  searchActors(\n    params?: AppBskyActorSearchActors.QueryParams,\n    opts?: AppBskyActorSearchActors.CallOptions,\n  ): Promise<AppBskyActorSearchActors.Response> {\n    return this._client.call(\n      'app.bsky.actor.searchActors',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  searchActorsTypeahead(\n    params?: AppBskyActorSearchActorsTypeahead.QueryParams,\n    opts?: AppBskyActorSearchActorsTypeahead.CallOptions,\n  ): Promise<AppBskyActorSearchActorsTypeahead.Response> {\n    return this._client.call(\n      'app.bsky.actor.searchActorsTypeahead',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyActorProfileRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyActorProfile.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.actor.profile',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyActorProfile.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.actor.profile',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyActorProfile.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.actor.profile'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      {\n        collection,\n        rkey: 'self',\n        ...params,\n        record: { ...record, $type: collection },\n      },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyActorProfile.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.actor.profile'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.actor.profile', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyActorStatusRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyActorStatus.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.actor.status',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyActorStatus.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.actor.status',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyActorStatus.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.actor.status'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      {\n        collection,\n        rkey: 'self',\n        ...params,\n        record: { ...record, $type: collection },\n      },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyActorStatus.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.actor.status'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.actor.status', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyAgeassuranceNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  begin(\n    data?: AppBskyAgeassuranceBegin.InputSchema,\n    opts?: AppBskyAgeassuranceBegin.CallOptions,\n  ): Promise<AppBskyAgeassuranceBegin.Response> {\n    return this._client\n      .call('app.bsky.ageassurance.begin', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyAgeassuranceBegin.toKnownErr(e)\n      })\n  }\n\n  getConfig(\n    params?: AppBskyAgeassuranceGetConfig.QueryParams,\n    opts?: AppBskyAgeassuranceGetConfig.CallOptions,\n  ): Promise<AppBskyAgeassuranceGetConfig.Response> {\n    return this._client.call(\n      'app.bsky.ageassurance.getConfig',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getState(\n    params?: AppBskyAgeassuranceGetState.QueryParams,\n    opts?: AppBskyAgeassuranceGetState.CallOptions,\n  ): Promise<AppBskyAgeassuranceGetState.Response> {\n    return this._client.call(\n      'app.bsky.ageassurance.getState',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyBookmarkNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  createBookmark(\n    data?: AppBskyBookmarkCreateBookmark.InputSchema,\n    opts?: AppBskyBookmarkCreateBookmark.CallOptions,\n  ): Promise<AppBskyBookmarkCreateBookmark.Response> {\n    return this._client\n      .call('app.bsky.bookmark.createBookmark', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyBookmarkCreateBookmark.toKnownErr(e)\n      })\n  }\n\n  deleteBookmark(\n    data?: AppBskyBookmarkDeleteBookmark.InputSchema,\n    opts?: AppBskyBookmarkDeleteBookmark.CallOptions,\n  ): Promise<AppBskyBookmarkDeleteBookmark.Response> {\n    return this._client\n      .call('app.bsky.bookmark.deleteBookmark', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyBookmarkDeleteBookmark.toKnownErr(e)\n      })\n  }\n\n  getBookmarks(\n    params?: AppBskyBookmarkGetBookmarks.QueryParams,\n    opts?: AppBskyBookmarkGetBookmarks.CallOptions,\n  ): Promise<AppBskyBookmarkGetBookmarks.Response> {\n    return this._client.call(\n      'app.bsky.bookmark.getBookmarks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyContactNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  dismissMatch(\n    data?: AppBskyContactDismissMatch.InputSchema,\n    opts?: AppBskyContactDismissMatch.CallOptions,\n  ): Promise<AppBskyContactDismissMatch.Response> {\n    return this._client\n      .call('app.bsky.contact.dismissMatch', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyContactDismissMatch.toKnownErr(e)\n      })\n  }\n\n  getMatches(\n    params?: AppBskyContactGetMatches.QueryParams,\n    opts?: AppBskyContactGetMatches.CallOptions,\n  ): Promise<AppBskyContactGetMatches.Response> {\n    return this._client\n      .call('app.bsky.contact.getMatches', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyContactGetMatches.toKnownErr(e)\n      })\n  }\n\n  getSyncStatus(\n    params?: AppBskyContactGetSyncStatus.QueryParams,\n    opts?: AppBskyContactGetSyncStatus.CallOptions,\n  ): Promise<AppBskyContactGetSyncStatus.Response> {\n    return this._client\n      .call('app.bsky.contact.getSyncStatus', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyContactGetSyncStatus.toKnownErr(e)\n      })\n  }\n\n  importContacts(\n    data?: AppBskyContactImportContacts.InputSchema,\n    opts?: AppBskyContactImportContacts.CallOptions,\n  ): Promise<AppBskyContactImportContacts.Response> {\n    return this._client\n      .call('app.bsky.contact.importContacts', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyContactImportContacts.toKnownErr(e)\n      })\n  }\n\n  removeData(\n    data?: AppBskyContactRemoveData.InputSchema,\n    opts?: AppBskyContactRemoveData.CallOptions,\n  ): Promise<AppBskyContactRemoveData.Response> {\n    return this._client\n      .call('app.bsky.contact.removeData', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyContactRemoveData.toKnownErr(e)\n      })\n  }\n\n  sendNotification(\n    data?: AppBskyContactSendNotification.InputSchema,\n    opts?: AppBskyContactSendNotification.CallOptions,\n  ): Promise<AppBskyContactSendNotification.Response> {\n    return this._client.call(\n      'app.bsky.contact.sendNotification',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  startPhoneVerification(\n    data?: AppBskyContactStartPhoneVerification.InputSchema,\n    opts?: AppBskyContactStartPhoneVerification.CallOptions,\n  ): Promise<AppBskyContactStartPhoneVerification.Response> {\n    return this._client\n      .call('app.bsky.contact.startPhoneVerification', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyContactStartPhoneVerification.toKnownErr(e)\n      })\n  }\n\n  verifyPhone(\n    data?: AppBskyContactVerifyPhone.InputSchema,\n    opts?: AppBskyContactVerifyPhone.CallOptions,\n  ): Promise<AppBskyContactVerifyPhone.Response> {\n    return this._client\n      .call('app.bsky.contact.verifyPhone', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyContactVerifyPhone.toKnownErr(e)\n      })\n  }\n}\n\nexport class AppBskyDraftNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  createDraft(\n    data?: AppBskyDraftCreateDraft.InputSchema,\n    opts?: AppBskyDraftCreateDraft.CallOptions,\n  ): Promise<AppBskyDraftCreateDraft.Response> {\n    return this._client\n      .call('app.bsky.draft.createDraft', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyDraftCreateDraft.toKnownErr(e)\n      })\n  }\n\n  deleteDraft(\n    data?: AppBskyDraftDeleteDraft.InputSchema,\n    opts?: AppBskyDraftDeleteDraft.CallOptions,\n  ): Promise<AppBskyDraftDeleteDraft.Response> {\n    return this._client.call('app.bsky.draft.deleteDraft', opts?.qp, data, opts)\n  }\n\n  getDrafts(\n    params?: AppBskyDraftGetDrafts.QueryParams,\n    opts?: AppBskyDraftGetDrafts.CallOptions,\n  ): Promise<AppBskyDraftGetDrafts.Response> {\n    return this._client.call(\n      'app.bsky.draft.getDrafts',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  updateDraft(\n    data?: AppBskyDraftUpdateDraft.InputSchema,\n    opts?: AppBskyDraftUpdateDraft.CallOptions,\n  ): Promise<AppBskyDraftUpdateDraft.Response> {\n    return this._client.call('app.bsky.draft.updateDraft', opts?.qp, data, opts)\n  }\n}\n\nexport class AppBskyEmbedNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n}\n\nexport class AppBskyFeedNS {\n  _client: XrpcClient\n  generator: AppBskyFeedGeneratorRecord\n  like: AppBskyFeedLikeRecord\n  post: AppBskyFeedPostRecord\n  postgate: AppBskyFeedPostgateRecord\n  repost: AppBskyFeedRepostRecord\n  threadgate: AppBskyFeedThreadgateRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.generator = new AppBskyFeedGeneratorRecord(client)\n    this.like = new AppBskyFeedLikeRecord(client)\n    this.post = new AppBskyFeedPostRecord(client)\n    this.postgate = new AppBskyFeedPostgateRecord(client)\n    this.repost = new AppBskyFeedRepostRecord(client)\n    this.threadgate = new AppBskyFeedThreadgateRecord(client)\n  }\n\n  describeFeedGenerator(\n    params?: AppBskyFeedDescribeFeedGenerator.QueryParams,\n    opts?: AppBskyFeedDescribeFeedGenerator.CallOptions,\n  ): Promise<AppBskyFeedDescribeFeedGenerator.Response> {\n    return this._client.call(\n      'app.bsky.feed.describeFeedGenerator',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getActorFeeds(\n    params?: AppBskyFeedGetActorFeeds.QueryParams,\n    opts?: AppBskyFeedGetActorFeeds.CallOptions,\n  ): Promise<AppBskyFeedGetActorFeeds.Response> {\n    return this._client.call(\n      'app.bsky.feed.getActorFeeds',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getActorLikes(\n    params?: AppBskyFeedGetActorLikes.QueryParams,\n    opts?: AppBskyFeedGetActorLikes.CallOptions,\n  ): Promise<AppBskyFeedGetActorLikes.Response> {\n    return this._client\n      .call('app.bsky.feed.getActorLikes', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedGetActorLikes.toKnownErr(e)\n      })\n  }\n\n  getAuthorFeed(\n    params?: AppBskyFeedGetAuthorFeed.QueryParams,\n    opts?: AppBskyFeedGetAuthorFeed.CallOptions,\n  ): Promise<AppBskyFeedGetAuthorFeed.Response> {\n    return this._client\n      .call('app.bsky.feed.getAuthorFeed', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedGetAuthorFeed.toKnownErr(e)\n      })\n  }\n\n  getFeed(\n    params?: AppBskyFeedGetFeed.QueryParams,\n    opts?: AppBskyFeedGetFeed.CallOptions,\n  ): Promise<AppBskyFeedGetFeed.Response> {\n    return this._client\n      .call('app.bsky.feed.getFeed', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedGetFeed.toKnownErr(e)\n      })\n  }\n\n  getFeedGenerator(\n    params?: AppBskyFeedGetFeedGenerator.QueryParams,\n    opts?: AppBskyFeedGetFeedGenerator.CallOptions,\n  ): Promise<AppBskyFeedGetFeedGenerator.Response> {\n    return this._client.call(\n      'app.bsky.feed.getFeedGenerator',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getFeedGenerators(\n    params?: AppBskyFeedGetFeedGenerators.QueryParams,\n    opts?: AppBskyFeedGetFeedGenerators.CallOptions,\n  ): Promise<AppBskyFeedGetFeedGenerators.Response> {\n    return this._client.call(\n      'app.bsky.feed.getFeedGenerators',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getFeedSkeleton(\n    params?: AppBskyFeedGetFeedSkeleton.QueryParams,\n    opts?: AppBskyFeedGetFeedSkeleton.CallOptions,\n  ): Promise<AppBskyFeedGetFeedSkeleton.Response> {\n    return this._client\n      .call('app.bsky.feed.getFeedSkeleton', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedGetFeedSkeleton.toKnownErr(e)\n      })\n  }\n\n  getLikes(\n    params?: AppBskyFeedGetLikes.QueryParams,\n    opts?: AppBskyFeedGetLikes.CallOptions,\n  ): Promise<AppBskyFeedGetLikes.Response> {\n    return this._client.call('app.bsky.feed.getLikes', params, undefined, opts)\n  }\n\n  getListFeed(\n    params?: AppBskyFeedGetListFeed.QueryParams,\n    opts?: AppBskyFeedGetListFeed.CallOptions,\n  ): Promise<AppBskyFeedGetListFeed.Response> {\n    return this._client\n      .call('app.bsky.feed.getListFeed', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedGetListFeed.toKnownErr(e)\n      })\n  }\n\n  getPostThread(\n    params?: AppBskyFeedGetPostThread.QueryParams,\n    opts?: AppBskyFeedGetPostThread.CallOptions,\n  ): Promise<AppBskyFeedGetPostThread.Response> {\n    return this._client\n      .call('app.bsky.feed.getPostThread', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedGetPostThread.toKnownErr(e)\n      })\n  }\n\n  getPosts(\n    params?: AppBskyFeedGetPosts.QueryParams,\n    opts?: AppBskyFeedGetPosts.CallOptions,\n  ): Promise<AppBskyFeedGetPosts.Response> {\n    return this._client.call('app.bsky.feed.getPosts', params, undefined, opts)\n  }\n\n  getQuotes(\n    params?: AppBskyFeedGetQuotes.QueryParams,\n    opts?: AppBskyFeedGetQuotes.CallOptions,\n  ): Promise<AppBskyFeedGetQuotes.Response> {\n    return this._client.call('app.bsky.feed.getQuotes', params, undefined, opts)\n  }\n\n  getRepostedBy(\n    params?: AppBskyFeedGetRepostedBy.QueryParams,\n    opts?: AppBskyFeedGetRepostedBy.CallOptions,\n  ): Promise<AppBskyFeedGetRepostedBy.Response> {\n    return this._client.call(\n      'app.bsky.feed.getRepostedBy',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedFeeds(\n    params?: AppBskyFeedGetSuggestedFeeds.QueryParams,\n    opts?: AppBskyFeedGetSuggestedFeeds.CallOptions,\n  ): Promise<AppBskyFeedGetSuggestedFeeds.Response> {\n    return this._client.call(\n      'app.bsky.feed.getSuggestedFeeds',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getTimeline(\n    params?: AppBskyFeedGetTimeline.QueryParams,\n    opts?: AppBskyFeedGetTimeline.CallOptions,\n  ): Promise<AppBskyFeedGetTimeline.Response> {\n    return this._client.call(\n      'app.bsky.feed.getTimeline',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  searchPosts(\n    params?: AppBskyFeedSearchPosts.QueryParams,\n    opts?: AppBskyFeedSearchPosts.CallOptions,\n  ): Promise<AppBskyFeedSearchPosts.Response> {\n    return this._client\n      .call('app.bsky.feed.searchPosts', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyFeedSearchPosts.toKnownErr(e)\n      })\n  }\n\n  sendInteractions(\n    data?: AppBskyFeedSendInteractions.InputSchema,\n    opts?: AppBskyFeedSendInteractions.CallOptions,\n  ): Promise<AppBskyFeedSendInteractions.Response> {\n    return this._client.call(\n      'app.bsky.feed.sendInteractions',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyFeedGeneratorRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyFeedGenerator.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.feed.generator',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyFeedGenerator.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.feed.generator',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedGenerator.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.generator'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedGenerator.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.generator'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.feed.generator', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyFeedLikeRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyFeedLike.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.feed.like',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyFeedLike.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.feed.like',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedLike.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.like'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedLike.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.like'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.feed.like', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyFeedPostRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyFeedPost.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.feed.post',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyFeedPost.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.feed.post',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedPost.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.post'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedPost.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.post'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.feed.post', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyFeedPostgateRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyFeedPostgate.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.feed.postgate',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyFeedPostgate.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.feed.postgate',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedPostgate.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.postgate'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedPostgate.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.postgate'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.feed.postgate', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyFeedRepostRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyFeedRepost.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.feed.repost',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyFeedRepost.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.feed.repost',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedRepost.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.repost'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedRepost.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.repost'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.feed.repost', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyFeedThreadgateRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyFeedThreadgate.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.feed.threadgate',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: AppBskyFeedThreadgate.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.feed.threadgate',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedThreadgate.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.threadgate'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyFeedThreadgate.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.feed.threadgate'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.feed.threadgate', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphNS {\n  _client: XrpcClient\n  block: AppBskyGraphBlockRecord\n  follow: AppBskyGraphFollowRecord\n  list: AppBskyGraphListRecord\n  listblock: AppBskyGraphListblockRecord\n  listitem: AppBskyGraphListitemRecord\n  starterpack: AppBskyGraphStarterpackRecord\n  verification: AppBskyGraphVerificationRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.block = new AppBskyGraphBlockRecord(client)\n    this.follow = new AppBskyGraphFollowRecord(client)\n    this.list = new AppBskyGraphListRecord(client)\n    this.listblock = new AppBskyGraphListblockRecord(client)\n    this.listitem = new AppBskyGraphListitemRecord(client)\n    this.starterpack = new AppBskyGraphStarterpackRecord(client)\n    this.verification = new AppBskyGraphVerificationRecord(client)\n  }\n\n  getActorStarterPacks(\n    params?: AppBskyGraphGetActorStarterPacks.QueryParams,\n    opts?: AppBskyGraphGetActorStarterPacks.CallOptions,\n  ): Promise<AppBskyGraphGetActorStarterPacks.Response> {\n    return this._client.call(\n      'app.bsky.graph.getActorStarterPacks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getBlocks(\n    params?: AppBskyGraphGetBlocks.QueryParams,\n    opts?: AppBskyGraphGetBlocks.CallOptions,\n  ): Promise<AppBskyGraphGetBlocks.Response> {\n    return this._client.call(\n      'app.bsky.graph.getBlocks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getFollowers(\n    params?: AppBskyGraphGetFollowers.QueryParams,\n    opts?: AppBskyGraphGetFollowers.CallOptions,\n  ): Promise<AppBskyGraphGetFollowers.Response> {\n    return this._client.call(\n      'app.bsky.graph.getFollowers',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getFollows(\n    params?: AppBskyGraphGetFollows.QueryParams,\n    opts?: AppBskyGraphGetFollows.CallOptions,\n  ): Promise<AppBskyGraphGetFollows.Response> {\n    return this._client.call(\n      'app.bsky.graph.getFollows',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getKnownFollowers(\n    params?: AppBskyGraphGetKnownFollowers.QueryParams,\n    opts?: AppBskyGraphGetKnownFollowers.CallOptions,\n  ): Promise<AppBskyGraphGetKnownFollowers.Response> {\n    return this._client.call(\n      'app.bsky.graph.getKnownFollowers',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getList(\n    params?: AppBskyGraphGetList.QueryParams,\n    opts?: AppBskyGraphGetList.CallOptions,\n  ): Promise<AppBskyGraphGetList.Response> {\n    return this._client.call('app.bsky.graph.getList', params, undefined, opts)\n  }\n\n  getListBlocks(\n    params?: AppBskyGraphGetListBlocks.QueryParams,\n    opts?: AppBskyGraphGetListBlocks.CallOptions,\n  ): Promise<AppBskyGraphGetListBlocks.Response> {\n    return this._client.call(\n      'app.bsky.graph.getListBlocks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getListMutes(\n    params?: AppBskyGraphGetListMutes.QueryParams,\n    opts?: AppBskyGraphGetListMutes.CallOptions,\n  ): Promise<AppBskyGraphGetListMutes.Response> {\n    return this._client.call(\n      'app.bsky.graph.getListMutes',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getLists(\n    params?: AppBskyGraphGetLists.QueryParams,\n    opts?: AppBskyGraphGetLists.CallOptions,\n  ): Promise<AppBskyGraphGetLists.Response> {\n    return this._client.call('app.bsky.graph.getLists', params, undefined, opts)\n  }\n\n  getListsWithMembership(\n    params?: AppBskyGraphGetListsWithMembership.QueryParams,\n    opts?: AppBskyGraphGetListsWithMembership.CallOptions,\n  ): Promise<AppBskyGraphGetListsWithMembership.Response> {\n    return this._client.call(\n      'app.bsky.graph.getListsWithMembership',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getMutes(\n    params?: AppBskyGraphGetMutes.QueryParams,\n    opts?: AppBskyGraphGetMutes.CallOptions,\n  ): Promise<AppBskyGraphGetMutes.Response> {\n    return this._client.call('app.bsky.graph.getMutes', params, undefined, opts)\n  }\n\n  getRelationships(\n    params?: AppBskyGraphGetRelationships.QueryParams,\n    opts?: AppBskyGraphGetRelationships.CallOptions,\n  ): Promise<AppBskyGraphGetRelationships.Response> {\n    return this._client\n      .call('app.bsky.graph.getRelationships', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyGraphGetRelationships.toKnownErr(e)\n      })\n  }\n\n  getStarterPack(\n    params?: AppBskyGraphGetStarterPack.QueryParams,\n    opts?: AppBskyGraphGetStarterPack.CallOptions,\n  ): Promise<AppBskyGraphGetStarterPack.Response> {\n    return this._client.call(\n      'app.bsky.graph.getStarterPack',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getStarterPacks(\n    params?: AppBskyGraphGetStarterPacks.QueryParams,\n    opts?: AppBskyGraphGetStarterPacks.CallOptions,\n  ): Promise<AppBskyGraphGetStarterPacks.Response> {\n    return this._client.call(\n      'app.bsky.graph.getStarterPacks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getStarterPacksWithMembership(\n    params?: AppBskyGraphGetStarterPacksWithMembership.QueryParams,\n    opts?: AppBskyGraphGetStarterPacksWithMembership.CallOptions,\n  ): Promise<AppBskyGraphGetStarterPacksWithMembership.Response> {\n    return this._client.call(\n      'app.bsky.graph.getStarterPacksWithMembership',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedFollowsByActor(\n    params?: AppBskyGraphGetSuggestedFollowsByActor.QueryParams,\n    opts?: AppBskyGraphGetSuggestedFollowsByActor.CallOptions,\n  ): Promise<AppBskyGraphGetSuggestedFollowsByActor.Response> {\n    return this._client.call(\n      'app.bsky.graph.getSuggestedFollowsByActor',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  muteActor(\n    data?: AppBskyGraphMuteActor.InputSchema,\n    opts?: AppBskyGraphMuteActor.CallOptions,\n  ): Promise<AppBskyGraphMuteActor.Response> {\n    return this._client.call('app.bsky.graph.muteActor', opts?.qp, data, opts)\n  }\n\n  muteActorList(\n    data?: AppBskyGraphMuteActorList.InputSchema,\n    opts?: AppBskyGraphMuteActorList.CallOptions,\n  ): Promise<AppBskyGraphMuteActorList.Response> {\n    return this._client.call(\n      'app.bsky.graph.muteActorList',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  muteThread(\n    data?: AppBskyGraphMuteThread.InputSchema,\n    opts?: AppBskyGraphMuteThread.CallOptions,\n  ): Promise<AppBskyGraphMuteThread.Response> {\n    return this._client.call('app.bsky.graph.muteThread', opts?.qp, data, opts)\n  }\n\n  searchStarterPacks(\n    params?: AppBskyGraphSearchStarterPacks.QueryParams,\n    opts?: AppBskyGraphSearchStarterPacks.CallOptions,\n  ): Promise<AppBskyGraphSearchStarterPacks.Response> {\n    return this._client.call(\n      'app.bsky.graph.searchStarterPacks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  unmuteActor(\n    data?: AppBskyGraphUnmuteActor.InputSchema,\n    opts?: AppBskyGraphUnmuteActor.CallOptions,\n  ): Promise<AppBskyGraphUnmuteActor.Response> {\n    return this._client.call('app.bsky.graph.unmuteActor', opts?.qp, data, opts)\n  }\n\n  unmuteActorList(\n    data?: AppBskyGraphUnmuteActorList.InputSchema,\n    opts?: AppBskyGraphUnmuteActorList.CallOptions,\n  ): Promise<AppBskyGraphUnmuteActorList.Response> {\n    return this._client.call(\n      'app.bsky.graph.unmuteActorList',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  unmuteThread(\n    data?: AppBskyGraphUnmuteThread.InputSchema,\n    opts?: AppBskyGraphUnmuteThread.CallOptions,\n  ): Promise<AppBskyGraphUnmuteThread.Response> {\n    return this._client.call(\n      'app.bsky.graph.unmuteThread',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyGraphBlockRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphBlock.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.block',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyGraphBlock.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.block',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphBlock.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.block'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphBlock.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.block'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.block', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphFollowRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphFollow.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.follow',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyGraphFollow.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.follow',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphFollow.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.follow'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphFollow.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.follow'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.follow', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphListRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphList.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.list',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyGraphList.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.list',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphList.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.list'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphList.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.list'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.list', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphListblockRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphListblock.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.listblock',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: AppBskyGraphListblock.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.listblock',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphListblock.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.listblock'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphListblock.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.listblock'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.listblock', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphListitemRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphListitem.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.listitem',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{ uri: string; cid: string; value: AppBskyGraphListitem.Record }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.listitem',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphListitem.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.listitem'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphListitem.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.listitem'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.listitem', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphStarterpackRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphStarterpack.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.starterpack',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: AppBskyGraphStarterpack.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.starterpack',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphStarterpack.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.starterpack'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphStarterpack.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.starterpack'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.starterpack', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyGraphVerificationRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyGraphVerification.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.graph.verification',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: AppBskyGraphVerification.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.graph.verification',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphVerification.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.verification'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyGraphVerification.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.graph.verification'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.graph.verification', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyLabelerNS {\n  _client: XrpcClient\n  service: AppBskyLabelerServiceRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.service = new AppBskyLabelerServiceRecord(client)\n  }\n\n  getServices(\n    params?: AppBskyLabelerGetServices.QueryParams,\n    opts?: AppBskyLabelerGetServices.CallOptions,\n  ): Promise<AppBskyLabelerGetServices.Response> {\n    return this._client.call(\n      'app.bsky.labeler.getServices',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyLabelerServiceRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyLabelerService.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.labeler.service',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: AppBskyLabelerService.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.labeler.service',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyLabelerService.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.labeler.service'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      {\n        collection,\n        rkey: 'self',\n        ...params,\n        record: { ...record, $type: collection },\n      },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyLabelerService.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.labeler.service'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.labeler.service', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyNotificationNS {\n  _client: XrpcClient\n  declaration: AppBskyNotificationDeclarationRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.declaration = new AppBskyNotificationDeclarationRecord(client)\n  }\n\n  getPreferences(\n    params?: AppBskyNotificationGetPreferences.QueryParams,\n    opts?: AppBskyNotificationGetPreferences.CallOptions,\n  ): Promise<AppBskyNotificationGetPreferences.Response> {\n    return this._client.call(\n      'app.bsky.notification.getPreferences',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getUnreadCount(\n    params?: AppBskyNotificationGetUnreadCount.QueryParams,\n    opts?: AppBskyNotificationGetUnreadCount.CallOptions,\n  ): Promise<AppBskyNotificationGetUnreadCount.Response> {\n    return this._client.call(\n      'app.bsky.notification.getUnreadCount',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listActivitySubscriptions(\n    params?: AppBskyNotificationListActivitySubscriptions.QueryParams,\n    opts?: AppBskyNotificationListActivitySubscriptions.CallOptions,\n  ): Promise<AppBskyNotificationListActivitySubscriptions.Response> {\n    return this._client.call(\n      'app.bsky.notification.listActivitySubscriptions',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listNotifications(\n    params?: AppBskyNotificationListNotifications.QueryParams,\n    opts?: AppBskyNotificationListNotifications.CallOptions,\n  ): Promise<AppBskyNotificationListNotifications.Response> {\n    return this._client.call(\n      'app.bsky.notification.listNotifications',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  putActivitySubscription(\n    data?: AppBskyNotificationPutActivitySubscription.InputSchema,\n    opts?: AppBskyNotificationPutActivitySubscription.CallOptions,\n  ): Promise<AppBskyNotificationPutActivitySubscription.Response> {\n    return this._client.call(\n      'app.bsky.notification.putActivitySubscription',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  putPreferences(\n    data?: AppBskyNotificationPutPreferences.InputSchema,\n    opts?: AppBskyNotificationPutPreferences.CallOptions,\n  ): Promise<AppBskyNotificationPutPreferences.Response> {\n    return this._client.call(\n      'app.bsky.notification.putPreferences',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  putPreferencesV2(\n    data?: AppBskyNotificationPutPreferencesV2.InputSchema,\n    opts?: AppBskyNotificationPutPreferencesV2.CallOptions,\n  ): Promise<AppBskyNotificationPutPreferencesV2.Response> {\n    return this._client.call(\n      'app.bsky.notification.putPreferencesV2',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  registerPush(\n    data?: AppBskyNotificationRegisterPush.InputSchema,\n    opts?: AppBskyNotificationRegisterPush.CallOptions,\n  ): Promise<AppBskyNotificationRegisterPush.Response> {\n    return this._client.call(\n      'app.bsky.notification.registerPush',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  unregisterPush(\n    data?: AppBskyNotificationUnregisterPush.InputSchema,\n    opts?: AppBskyNotificationUnregisterPush.CallOptions,\n  ): Promise<AppBskyNotificationUnregisterPush.Response> {\n    return this._client.call(\n      'app.bsky.notification.unregisterPush',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateSeen(\n    data?: AppBskyNotificationUpdateSeen.InputSchema,\n    opts?: AppBskyNotificationUpdateSeen.CallOptions,\n  ): Promise<AppBskyNotificationUpdateSeen.Response> {\n    return this._client.call(\n      'app.bsky.notification.updateSeen',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class AppBskyNotificationDeclarationRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: AppBskyNotificationDeclaration.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'app.bsky.notification.declaration',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: AppBskyNotificationDeclaration.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'app.bsky.notification.declaration',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyNotificationDeclaration.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.notification.declaration'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      {\n        collection,\n        rkey: 'self',\n        ...params,\n        record: { ...record, $type: collection },\n      },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<AppBskyNotificationDeclaration.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'app.bsky.notification.declaration'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'app.bsky.notification.declaration', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class AppBskyRichtextNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n}\n\nexport class AppBskyUnspeccedNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getAgeAssuranceState(\n    params?: AppBskyUnspeccedGetAgeAssuranceState.QueryParams,\n    opts?: AppBskyUnspeccedGetAgeAssuranceState.CallOptions,\n  ): Promise<AppBskyUnspeccedGetAgeAssuranceState.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getAgeAssuranceState',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getConfig(\n    params?: AppBskyUnspeccedGetConfig.QueryParams,\n    opts?: AppBskyUnspeccedGetConfig.CallOptions,\n  ): Promise<AppBskyUnspeccedGetConfig.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getConfig',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getOnboardingSuggestedStarterPacks(\n    params?: AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.QueryParams,\n    opts?: AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.CallOptions,\n  ): Promise<AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getOnboardingSuggestedStarterPacksSkeleton(\n    params?: AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getOnboardingSuggestedUsersSkeleton(\n    params?: AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getPopularFeedGenerators(\n    params?: AppBskyUnspeccedGetPopularFeedGenerators.QueryParams,\n    opts?: AppBskyUnspeccedGetPopularFeedGenerators.CallOptions,\n  ): Promise<AppBskyUnspeccedGetPopularFeedGenerators.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getPopularFeedGenerators',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getPostThreadOtherV2(\n    params?: AppBskyUnspeccedGetPostThreadOtherV2.QueryParams,\n    opts?: AppBskyUnspeccedGetPostThreadOtherV2.CallOptions,\n  ): Promise<AppBskyUnspeccedGetPostThreadOtherV2.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getPostThreadOtherV2',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getPostThreadV2(\n    params?: AppBskyUnspeccedGetPostThreadV2.QueryParams,\n    opts?: AppBskyUnspeccedGetPostThreadV2.CallOptions,\n  ): Promise<AppBskyUnspeccedGetPostThreadV2.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getPostThreadV2',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedFeeds(\n    params?: AppBskyUnspeccedGetSuggestedFeeds.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedFeeds.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedFeeds.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedFeeds',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedFeedsSkeleton(\n    params?: AppBskyUnspeccedGetSuggestedFeedsSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedFeedsSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedFeedsSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedOnboardingUsers(\n    params?: AppBskyUnspeccedGetSuggestedOnboardingUsers.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedOnboardingUsers.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedOnboardingUsers.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedOnboardingUsers',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedStarterPacks(\n    params?: AppBskyUnspeccedGetSuggestedStarterPacks.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedStarterPacks.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedStarterPacks.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedStarterPacks',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedStarterPacksSkeleton(\n    params?: AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedUsers(\n    params?: AppBskyUnspeccedGetSuggestedUsers.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedUsers.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedUsers.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedUsers',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestedUsersSkeleton(\n    params?: AppBskyUnspeccedGetSuggestedUsersSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestedUsersSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestedUsersSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestedUsersSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSuggestionsSkeleton(\n    params?: AppBskyUnspeccedGetSuggestionsSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetSuggestionsSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetSuggestionsSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getSuggestionsSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getTaggedSuggestions(\n    params?: AppBskyUnspeccedGetTaggedSuggestions.QueryParams,\n    opts?: AppBskyUnspeccedGetTaggedSuggestions.CallOptions,\n  ): Promise<AppBskyUnspeccedGetTaggedSuggestions.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getTaggedSuggestions',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getTrendingTopics(\n    params?: AppBskyUnspeccedGetTrendingTopics.QueryParams,\n    opts?: AppBskyUnspeccedGetTrendingTopics.CallOptions,\n  ): Promise<AppBskyUnspeccedGetTrendingTopics.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getTrendingTopics',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getTrends(\n    params?: AppBskyUnspeccedGetTrends.QueryParams,\n    opts?: AppBskyUnspeccedGetTrends.CallOptions,\n  ): Promise<AppBskyUnspeccedGetTrends.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getTrends',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getTrendsSkeleton(\n    params?: AppBskyUnspeccedGetTrendsSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedGetTrendsSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedGetTrendsSkeleton.Response> {\n    return this._client.call(\n      'app.bsky.unspecced.getTrendsSkeleton',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  initAgeAssurance(\n    data?: AppBskyUnspeccedInitAgeAssurance.InputSchema,\n    opts?: AppBskyUnspeccedInitAgeAssurance.CallOptions,\n  ): Promise<AppBskyUnspeccedInitAgeAssurance.Response> {\n    return this._client\n      .call('app.bsky.unspecced.initAgeAssurance', opts?.qp, data, opts)\n      .catch((e) => {\n        throw AppBskyUnspeccedInitAgeAssurance.toKnownErr(e)\n      })\n  }\n\n  searchActorsSkeleton(\n    params?: AppBskyUnspeccedSearchActorsSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedSearchActorsSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedSearchActorsSkeleton.Response> {\n    return this._client\n      .call('app.bsky.unspecced.searchActorsSkeleton', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyUnspeccedSearchActorsSkeleton.toKnownErr(e)\n      })\n  }\n\n  searchPostsSkeleton(\n    params?: AppBskyUnspeccedSearchPostsSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedSearchPostsSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedSearchPostsSkeleton.Response> {\n    return this._client\n      .call('app.bsky.unspecced.searchPostsSkeleton', params, undefined, opts)\n      .catch((e) => {\n        throw AppBskyUnspeccedSearchPostsSkeleton.toKnownErr(e)\n      })\n  }\n\n  searchStarterPacksSkeleton(\n    params?: AppBskyUnspeccedSearchStarterPacksSkeleton.QueryParams,\n    opts?: AppBskyUnspeccedSearchStarterPacksSkeleton.CallOptions,\n  ): Promise<AppBskyUnspeccedSearchStarterPacksSkeleton.Response> {\n    return this._client\n      .call(\n        'app.bsky.unspecced.searchStarterPacksSkeleton',\n        params,\n        undefined,\n        opts,\n      )\n      .catch((e) => {\n        throw AppBskyUnspeccedSearchStarterPacksSkeleton.toKnownErr(e)\n      })\n  }\n}\n\nexport class AppBskyVideoNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getJobStatus(\n    params?: AppBskyVideoGetJobStatus.QueryParams,\n    opts?: AppBskyVideoGetJobStatus.CallOptions,\n  ): Promise<AppBskyVideoGetJobStatus.Response> {\n    return this._client.call(\n      'app.bsky.video.getJobStatus',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getUploadLimits(\n    params?: AppBskyVideoGetUploadLimits.QueryParams,\n    opts?: AppBskyVideoGetUploadLimits.CallOptions,\n  ): Promise<AppBskyVideoGetUploadLimits.Response> {\n    return this._client.call(\n      'app.bsky.video.getUploadLimits',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  uploadVideo(\n    data?: AppBskyVideoUploadVideo.InputSchema,\n    opts?: AppBskyVideoUploadVideo.CallOptions,\n  ): Promise<AppBskyVideoUploadVideo.Response> {\n    return this._client.call('app.bsky.video.uploadVideo', opts?.qp, data, opts)\n  }\n}\n\nexport class ChatNS {\n  _client: XrpcClient\n  bsky: ChatBskyNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.bsky = new ChatBskyNS(client)\n  }\n}\n\nexport class ChatBskyNS {\n  _client: XrpcClient\n  actor: ChatBskyActorNS\n  convo: ChatBskyConvoNS\n  moderation: ChatBskyModerationNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.actor = new ChatBskyActorNS(client)\n    this.convo = new ChatBskyConvoNS(client)\n    this.moderation = new ChatBskyModerationNS(client)\n  }\n}\n\nexport class ChatBskyActorNS {\n  _client: XrpcClient\n  declaration: ChatBskyActorDeclarationRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.declaration = new ChatBskyActorDeclarationRecord(client)\n  }\n\n  deleteAccount(\n    data?: ChatBskyActorDeleteAccount.InputSchema,\n    opts?: ChatBskyActorDeleteAccount.CallOptions,\n  ): Promise<ChatBskyActorDeleteAccount.Response> {\n    return this._client.call(\n      'chat.bsky.actor.deleteAccount',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  exportAccountData(\n    params?: ChatBskyActorExportAccountData.QueryParams,\n    opts?: ChatBskyActorExportAccountData.CallOptions,\n  ): Promise<ChatBskyActorExportAccountData.Response> {\n    return this._client.call(\n      'chat.bsky.actor.exportAccountData',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class ChatBskyActorDeclarationRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: ChatBskyActorDeclaration.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'chat.bsky.actor.declaration',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: ChatBskyActorDeclaration.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'chat.bsky.actor.declaration',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<ChatBskyActorDeclaration.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'chat.bsky.actor.declaration'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      {\n        collection,\n        rkey: 'self',\n        ...params,\n        record: { ...record, $type: collection },\n      },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<ChatBskyActorDeclaration.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'chat.bsky.actor.declaration'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'chat.bsky.actor.declaration', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class ChatBskyConvoNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  acceptConvo(\n    data?: ChatBskyConvoAcceptConvo.InputSchema,\n    opts?: ChatBskyConvoAcceptConvo.CallOptions,\n  ): Promise<ChatBskyConvoAcceptConvo.Response> {\n    return this._client.call(\n      'chat.bsky.convo.acceptConvo',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  addReaction(\n    data?: ChatBskyConvoAddReaction.InputSchema,\n    opts?: ChatBskyConvoAddReaction.CallOptions,\n  ): Promise<ChatBskyConvoAddReaction.Response> {\n    return this._client\n      .call('chat.bsky.convo.addReaction', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ChatBskyConvoAddReaction.toKnownErr(e)\n      })\n  }\n\n  deleteMessageForSelf(\n    data?: ChatBskyConvoDeleteMessageForSelf.InputSchema,\n    opts?: ChatBskyConvoDeleteMessageForSelf.CallOptions,\n  ): Promise<ChatBskyConvoDeleteMessageForSelf.Response> {\n    return this._client.call(\n      'chat.bsky.convo.deleteMessageForSelf',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  getConvo(\n    params?: ChatBskyConvoGetConvo.QueryParams,\n    opts?: ChatBskyConvoGetConvo.CallOptions,\n  ): Promise<ChatBskyConvoGetConvo.Response> {\n    return this._client.call(\n      'chat.bsky.convo.getConvo',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getConvoAvailability(\n    params?: ChatBskyConvoGetConvoAvailability.QueryParams,\n    opts?: ChatBskyConvoGetConvoAvailability.CallOptions,\n  ): Promise<ChatBskyConvoGetConvoAvailability.Response> {\n    return this._client.call(\n      'chat.bsky.convo.getConvoAvailability',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getConvoForMembers(\n    params?: ChatBskyConvoGetConvoForMembers.QueryParams,\n    opts?: ChatBskyConvoGetConvoForMembers.CallOptions,\n  ): Promise<ChatBskyConvoGetConvoForMembers.Response> {\n    return this._client.call(\n      'chat.bsky.convo.getConvoForMembers',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getLog(\n    params?: ChatBskyConvoGetLog.QueryParams,\n    opts?: ChatBskyConvoGetLog.CallOptions,\n  ): Promise<ChatBskyConvoGetLog.Response> {\n    return this._client.call('chat.bsky.convo.getLog', params, undefined, opts)\n  }\n\n  getMessages(\n    params?: ChatBskyConvoGetMessages.QueryParams,\n    opts?: ChatBskyConvoGetMessages.CallOptions,\n  ): Promise<ChatBskyConvoGetMessages.Response> {\n    return this._client.call(\n      'chat.bsky.convo.getMessages',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  leaveConvo(\n    data?: ChatBskyConvoLeaveConvo.InputSchema,\n    opts?: ChatBskyConvoLeaveConvo.CallOptions,\n  ): Promise<ChatBskyConvoLeaveConvo.Response> {\n    return this._client.call('chat.bsky.convo.leaveConvo', opts?.qp, data, opts)\n  }\n\n  listConvos(\n    params?: ChatBskyConvoListConvos.QueryParams,\n    opts?: ChatBskyConvoListConvos.CallOptions,\n  ): Promise<ChatBskyConvoListConvos.Response> {\n    return this._client.call(\n      'chat.bsky.convo.listConvos',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  muteConvo(\n    data?: ChatBskyConvoMuteConvo.InputSchema,\n    opts?: ChatBskyConvoMuteConvo.CallOptions,\n  ): Promise<ChatBskyConvoMuteConvo.Response> {\n    return this._client.call('chat.bsky.convo.muteConvo', opts?.qp, data, opts)\n  }\n\n  removeReaction(\n    data?: ChatBskyConvoRemoveReaction.InputSchema,\n    opts?: ChatBskyConvoRemoveReaction.CallOptions,\n  ): Promise<ChatBskyConvoRemoveReaction.Response> {\n    return this._client\n      .call('chat.bsky.convo.removeReaction', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ChatBskyConvoRemoveReaction.toKnownErr(e)\n      })\n  }\n\n  sendMessage(\n    data?: ChatBskyConvoSendMessage.InputSchema,\n    opts?: ChatBskyConvoSendMessage.CallOptions,\n  ): Promise<ChatBskyConvoSendMessage.Response> {\n    return this._client.call(\n      'chat.bsky.convo.sendMessage',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  sendMessageBatch(\n    data?: ChatBskyConvoSendMessageBatch.InputSchema,\n    opts?: ChatBskyConvoSendMessageBatch.CallOptions,\n  ): Promise<ChatBskyConvoSendMessageBatch.Response> {\n    return this._client.call(\n      'chat.bsky.convo.sendMessageBatch',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  unmuteConvo(\n    data?: ChatBskyConvoUnmuteConvo.InputSchema,\n    opts?: ChatBskyConvoUnmuteConvo.CallOptions,\n  ): Promise<ChatBskyConvoUnmuteConvo.Response> {\n    return this._client.call(\n      'chat.bsky.convo.unmuteConvo',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateAllRead(\n    data?: ChatBskyConvoUpdateAllRead.InputSchema,\n    opts?: ChatBskyConvoUpdateAllRead.CallOptions,\n  ): Promise<ChatBskyConvoUpdateAllRead.Response> {\n    return this._client.call(\n      'chat.bsky.convo.updateAllRead',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateRead(\n    data?: ChatBskyConvoUpdateRead.InputSchema,\n    opts?: ChatBskyConvoUpdateRead.CallOptions,\n  ): Promise<ChatBskyConvoUpdateRead.Response> {\n    return this._client.call('chat.bsky.convo.updateRead', opts?.qp, data, opts)\n  }\n}\n\nexport class ChatBskyModerationNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getActorMetadata(\n    params?: ChatBskyModerationGetActorMetadata.QueryParams,\n    opts?: ChatBskyModerationGetActorMetadata.CallOptions,\n  ): Promise<ChatBskyModerationGetActorMetadata.Response> {\n    return this._client.call(\n      'chat.bsky.moderation.getActorMetadata',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getMessageContext(\n    params?: ChatBskyModerationGetMessageContext.QueryParams,\n    opts?: ChatBskyModerationGetMessageContext.CallOptions,\n  ): Promise<ChatBskyModerationGetMessageContext.Response> {\n    return this._client.call(\n      'chat.bsky.moderation.getMessageContext',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  updateActorAccess(\n    data?: ChatBskyModerationUpdateActorAccess.InputSchema,\n    opts?: ChatBskyModerationUpdateActorAccess.CallOptions,\n  ): Promise<ChatBskyModerationUpdateActorAccess.Response> {\n    return this._client.call(\n      'chat.bsky.moderation.updateActorAccess',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ComNS {\n  _client: XrpcClient\n  atproto: ComAtprotoNS\n  germnetwork: ComGermnetworkNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.atproto = new ComAtprotoNS(client)\n    this.germnetwork = new ComGermnetworkNS(client)\n  }\n}\n\nexport class ComAtprotoNS {\n  _client: XrpcClient\n  admin: ComAtprotoAdminNS\n  identity: ComAtprotoIdentityNS\n  label: ComAtprotoLabelNS\n  lexicon: ComAtprotoLexiconNS\n  moderation: ComAtprotoModerationNS\n  repo: ComAtprotoRepoNS\n  server: ComAtprotoServerNS\n  sync: ComAtprotoSyncNS\n  temp: ComAtprotoTempNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.admin = new ComAtprotoAdminNS(client)\n    this.identity = new ComAtprotoIdentityNS(client)\n    this.label = new ComAtprotoLabelNS(client)\n    this.lexicon = new ComAtprotoLexiconNS(client)\n    this.moderation = new ComAtprotoModerationNS(client)\n    this.repo = new ComAtprotoRepoNS(client)\n    this.server = new ComAtprotoServerNS(client)\n    this.sync = new ComAtprotoSyncNS(client)\n    this.temp = new ComAtprotoTempNS(client)\n  }\n}\n\nexport class ComAtprotoAdminNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  deleteAccount(\n    data?: ComAtprotoAdminDeleteAccount.InputSchema,\n    opts?: ComAtprotoAdminDeleteAccount.CallOptions,\n  ): Promise<ComAtprotoAdminDeleteAccount.Response> {\n    return this._client.call(\n      'com.atproto.admin.deleteAccount',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  disableAccountInvites(\n    data?: ComAtprotoAdminDisableAccountInvites.InputSchema,\n    opts?: ComAtprotoAdminDisableAccountInvites.CallOptions,\n  ): Promise<ComAtprotoAdminDisableAccountInvites.Response> {\n    return this._client.call(\n      'com.atproto.admin.disableAccountInvites',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  disableInviteCodes(\n    data?: ComAtprotoAdminDisableInviteCodes.InputSchema,\n    opts?: ComAtprotoAdminDisableInviteCodes.CallOptions,\n  ): Promise<ComAtprotoAdminDisableInviteCodes.Response> {\n    return this._client.call(\n      'com.atproto.admin.disableInviteCodes',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  enableAccountInvites(\n    data?: ComAtprotoAdminEnableAccountInvites.InputSchema,\n    opts?: ComAtprotoAdminEnableAccountInvites.CallOptions,\n  ): Promise<ComAtprotoAdminEnableAccountInvites.Response> {\n    return this._client.call(\n      'com.atproto.admin.enableAccountInvites',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  getAccountInfo(\n    params?: ComAtprotoAdminGetAccountInfo.QueryParams,\n    opts?: ComAtprotoAdminGetAccountInfo.CallOptions,\n  ): Promise<ComAtprotoAdminGetAccountInfo.Response> {\n    return this._client.call(\n      'com.atproto.admin.getAccountInfo',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getAccountInfos(\n    params?: ComAtprotoAdminGetAccountInfos.QueryParams,\n    opts?: ComAtprotoAdminGetAccountInfos.CallOptions,\n  ): Promise<ComAtprotoAdminGetAccountInfos.Response> {\n    return this._client.call(\n      'com.atproto.admin.getAccountInfos',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getInviteCodes(\n    params?: ComAtprotoAdminGetInviteCodes.QueryParams,\n    opts?: ComAtprotoAdminGetInviteCodes.CallOptions,\n  ): Promise<ComAtprotoAdminGetInviteCodes.Response> {\n    return this._client.call(\n      'com.atproto.admin.getInviteCodes',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSubjectStatus(\n    params?: ComAtprotoAdminGetSubjectStatus.QueryParams,\n    opts?: ComAtprotoAdminGetSubjectStatus.CallOptions,\n  ): Promise<ComAtprotoAdminGetSubjectStatus.Response> {\n    return this._client.call(\n      'com.atproto.admin.getSubjectStatus',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  searchAccounts(\n    params?: ComAtprotoAdminSearchAccounts.QueryParams,\n    opts?: ComAtprotoAdminSearchAccounts.CallOptions,\n  ): Promise<ComAtprotoAdminSearchAccounts.Response> {\n    return this._client.call(\n      'com.atproto.admin.searchAccounts',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  sendEmail(\n    data?: ComAtprotoAdminSendEmail.InputSchema,\n    opts?: ComAtprotoAdminSendEmail.CallOptions,\n  ): Promise<ComAtprotoAdminSendEmail.Response> {\n    return this._client.call(\n      'com.atproto.admin.sendEmail',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateAccountEmail(\n    data?: ComAtprotoAdminUpdateAccountEmail.InputSchema,\n    opts?: ComAtprotoAdminUpdateAccountEmail.CallOptions,\n  ): Promise<ComAtprotoAdminUpdateAccountEmail.Response> {\n    return this._client.call(\n      'com.atproto.admin.updateAccountEmail',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateAccountHandle(\n    data?: ComAtprotoAdminUpdateAccountHandle.InputSchema,\n    opts?: ComAtprotoAdminUpdateAccountHandle.CallOptions,\n  ): Promise<ComAtprotoAdminUpdateAccountHandle.Response> {\n    return this._client.call(\n      'com.atproto.admin.updateAccountHandle',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateAccountPassword(\n    data?: ComAtprotoAdminUpdateAccountPassword.InputSchema,\n    opts?: ComAtprotoAdminUpdateAccountPassword.CallOptions,\n  ): Promise<ComAtprotoAdminUpdateAccountPassword.Response> {\n    return this._client.call(\n      'com.atproto.admin.updateAccountPassword',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateAccountSigningKey(\n    data?: ComAtprotoAdminUpdateAccountSigningKey.InputSchema,\n    opts?: ComAtprotoAdminUpdateAccountSigningKey.CallOptions,\n  ): Promise<ComAtprotoAdminUpdateAccountSigningKey.Response> {\n    return this._client.call(\n      'com.atproto.admin.updateAccountSigningKey',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateSubjectStatus(\n    data?: ComAtprotoAdminUpdateSubjectStatus.InputSchema,\n    opts?: ComAtprotoAdminUpdateSubjectStatus.CallOptions,\n  ): Promise<ComAtprotoAdminUpdateSubjectStatus.Response> {\n    return this._client.call(\n      'com.atproto.admin.updateSubjectStatus',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ComAtprotoIdentityNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getRecommendedDidCredentials(\n    params?: ComAtprotoIdentityGetRecommendedDidCredentials.QueryParams,\n    opts?: ComAtprotoIdentityGetRecommendedDidCredentials.CallOptions,\n  ): Promise<ComAtprotoIdentityGetRecommendedDidCredentials.Response> {\n    return this._client.call(\n      'com.atproto.identity.getRecommendedDidCredentials',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  refreshIdentity(\n    data?: ComAtprotoIdentityRefreshIdentity.InputSchema,\n    opts?: ComAtprotoIdentityRefreshIdentity.CallOptions,\n  ): Promise<ComAtprotoIdentityRefreshIdentity.Response> {\n    return this._client\n      .call('com.atproto.identity.refreshIdentity', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoIdentityRefreshIdentity.toKnownErr(e)\n      })\n  }\n\n  requestPlcOperationSignature(\n    data?: ComAtprotoIdentityRequestPlcOperationSignature.InputSchema,\n    opts?: ComAtprotoIdentityRequestPlcOperationSignature.CallOptions,\n  ): Promise<ComAtprotoIdentityRequestPlcOperationSignature.Response> {\n    return this._client.call(\n      'com.atproto.identity.requestPlcOperationSignature',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  resolveDid(\n    params?: ComAtprotoIdentityResolveDid.QueryParams,\n    opts?: ComAtprotoIdentityResolveDid.CallOptions,\n  ): Promise<ComAtprotoIdentityResolveDid.Response> {\n    return this._client\n      .call('com.atproto.identity.resolveDid', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoIdentityResolveDid.toKnownErr(e)\n      })\n  }\n\n  resolveHandle(\n    params?: ComAtprotoIdentityResolveHandle.QueryParams,\n    opts?: ComAtprotoIdentityResolveHandle.CallOptions,\n  ): Promise<ComAtprotoIdentityResolveHandle.Response> {\n    return this._client\n      .call('com.atproto.identity.resolveHandle', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoIdentityResolveHandle.toKnownErr(e)\n      })\n  }\n\n  resolveIdentity(\n    params?: ComAtprotoIdentityResolveIdentity.QueryParams,\n    opts?: ComAtprotoIdentityResolveIdentity.CallOptions,\n  ): Promise<ComAtprotoIdentityResolveIdentity.Response> {\n    return this._client\n      .call('com.atproto.identity.resolveIdentity', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoIdentityResolveIdentity.toKnownErr(e)\n      })\n  }\n\n  signPlcOperation(\n    data?: ComAtprotoIdentitySignPlcOperation.InputSchema,\n    opts?: ComAtprotoIdentitySignPlcOperation.CallOptions,\n  ): Promise<ComAtprotoIdentitySignPlcOperation.Response> {\n    return this._client.call(\n      'com.atproto.identity.signPlcOperation',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  submitPlcOperation(\n    data?: ComAtprotoIdentitySubmitPlcOperation.InputSchema,\n    opts?: ComAtprotoIdentitySubmitPlcOperation.CallOptions,\n  ): Promise<ComAtprotoIdentitySubmitPlcOperation.Response> {\n    return this._client.call(\n      'com.atproto.identity.submitPlcOperation',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateHandle(\n    data?: ComAtprotoIdentityUpdateHandle.InputSchema,\n    opts?: ComAtprotoIdentityUpdateHandle.CallOptions,\n  ): Promise<ComAtprotoIdentityUpdateHandle.Response> {\n    return this._client.call(\n      'com.atproto.identity.updateHandle',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ComAtprotoLabelNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  queryLabels(\n    params?: ComAtprotoLabelQueryLabels.QueryParams,\n    opts?: ComAtprotoLabelQueryLabels.CallOptions,\n  ): Promise<ComAtprotoLabelQueryLabels.Response> {\n    return this._client.call(\n      'com.atproto.label.queryLabels',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class ComAtprotoLexiconNS {\n  _client: XrpcClient\n  schema: ComAtprotoLexiconSchemaRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.schema = new ComAtprotoLexiconSchemaRecord(client)\n  }\n\n  resolveLexicon(\n    params?: ComAtprotoLexiconResolveLexicon.QueryParams,\n    opts?: ComAtprotoLexiconResolveLexicon.CallOptions,\n  ): Promise<ComAtprotoLexiconResolveLexicon.Response> {\n    return this._client\n      .call('com.atproto.lexicon.resolveLexicon', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoLexiconResolveLexicon.toKnownErr(e)\n      })\n  }\n}\n\nexport class ComAtprotoLexiconSchemaRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: ComAtprotoLexiconSchema.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'com.atproto.lexicon.schema',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: ComAtprotoLexiconSchema.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'com.atproto.lexicon.schema',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<ComAtprotoLexiconSchema.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'com.atproto.lexicon.schema'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<ComAtprotoLexiconSchema.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'com.atproto.lexicon.schema'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'com.atproto.lexicon.schema', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class ComAtprotoModerationNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  createReport(\n    data?: ComAtprotoModerationCreateReport.InputSchema,\n    opts?: ComAtprotoModerationCreateReport.CallOptions,\n  ): Promise<ComAtprotoModerationCreateReport.Response> {\n    return this._client.call(\n      'com.atproto.moderation.createReport',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ComAtprotoRepoNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  applyWrites(\n    data?: ComAtprotoRepoApplyWrites.InputSchema,\n    opts?: ComAtprotoRepoApplyWrites.CallOptions,\n  ): Promise<ComAtprotoRepoApplyWrites.Response> {\n    return this._client\n      .call('com.atproto.repo.applyWrites', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoRepoApplyWrites.toKnownErr(e)\n      })\n  }\n\n  createRecord(\n    data?: ComAtprotoRepoCreateRecord.InputSchema,\n    opts?: ComAtprotoRepoCreateRecord.CallOptions,\n  ): Promise<ComAtprotoRepoCreateRecord.Response> {\n    return this._client\n      .call('com.atproto.repo.createRecord', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoRepoCreateRecord.toKnownErr(e)\n      })\n  }\n\n  deleteRecord(\n    data?: ComAtprotoRepoDeleteRecord.InputSchema,\n    opts?: ComAtprotoRepoDeleteRecord.CallOptions,\n  ): Promise<ComAtprotoRepoDeleteRecord.Response> {\n    return this._client\n      .call('com.atproto.repo.deleteRecord', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoRepoDeleteRecord.toKnownErr(e)\n      })\n  }\n\n  describeRepo(\n    params?: ComAtprotoRepoDescribeRepo.QueryParams,\n    opts?: ComAtprotoRepoDescribeRepo.CallOptions,\n  ): Promise<ComAtprotoRepoDescribeRepo.Response> {\n    return this._client.call(\n      'com.atproto.repo.describeRepo',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getRecord(\n    params?: ComAtprotoRepoGetRecord.QueryParams,\n    opts?: ComAtprotoRepoGetRecord.CallOptions,\n  ): Promise<ComAtprotoRepoGetRecord.Response> {\n    return this._client\n      .call('com.atproto.repo.getRecord', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoRepoGetRecord.toKnownErr(e)\n      })\n  }\n\n  importRepo(\n    data?: ComAtprotoRepoImportRepo.InputSchema,\n    opts?: ComAtprotoRepoImportRepo.CallOptions,\n  ): Promise<ComAtprotoRepoImportRepo.Response> {\n    return this._client.call(\n      'com.atproto.repo.importRepo',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  listMissingBlobs(\n    params?: ComAtprotoRepoListMissingBlobs.QueryParams,\n    opts?: ComAtprotoRepoListMissingBlobs.CallOptions,\n  ): Promise<ComAtprotoRepoListMissingBlobs.Response> {\n    return this._client.call(\n      'com.atproto.repo.listMissingBlobs',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listRecords(\n    params?: ComAtprotoRepoListRecords.QueryParams,\n    opts?: ComAtprotoRepoListRecords.CallOptions,\n  ): Promise<ComAtprotoRepoListRecords.Response> {\n    return this._client.call(\n      'com.atproto.repo.listRecords',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  putRecord(\n    data?: ComAtprotoRepoPutRecord.InputSchema,\n    opts?: ComAtprotoRepoPutRecord.CallOptions,\n  ): Promise<ComAtprotoRepoPutRecord.Response> {\n    return this._client\n      .call('com.atproto.repo.putRecord', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoRepoPutRecord.toKnownErr(e)\n      })\n  }\n\n  uploadBlob(\n    data?: ComAtprotoRepoUploadBlob.InputSchema,\n    opts?: ComAtprotoRepoUploadBlob.CallOptions,\n  ): Promise<ComAtprotoRepoUploadBlob.Response> {\n    return this._client.call(\n      'com.atproto.repo.uploadBlob',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ComAtprotoServerNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  activateAccount(\n    data?: ComAtprotoServerActivateAccount.InputSchema,\n    opts?: ComAtprotoServerActivateAccount.CallOptions,\n  ): Promise<ComAtprotoServerActivateAccount.Response> {\n    return this._client.call(\n      'com.atproto.server.activateAccount',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  checkAccountStatus(\n    params?: ComAtprotoServerCheckAccountStatus.QueryParams,\n    opts?: ComAtprotoServerCheckAccountStatus.CallOptions,\n  ): Promise<ComAtprotoServerCheckAccountStatus.Response> {\n    return this._client.call(\n      'com.atproto.server.checkAccountStatus',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  confirmEmail(\n    data?: ComAtprotoServerConfirmEmail.InputSchema,\n    opts?: ComAtprotoServerConfirmEmail.CallOptions,\n  ): Promise<ComAtprotoServerConfirmEmail.Response> {\n    return this._client\n      .call('com.atproto.server.confirmEmail', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerConfirmEmail.toKnownErr(e)\n      })\n  }\n\n  createAccount(\n    data?: ComAtprotoServerCreateAccount.InputSchema,\n    opts?: ComAtprotoServerCreateAccount.CallOptions,\n  ): Promise<ComAtprotoServerCreateAccount.Response> {\n    return this._client\n      .call('com.atproto.server.createAccount', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerCreateAccount.toKnownErr(e)\n      })\n  }\n\n  createAppPassword(\n    data?: ComAtprotoServerCreateAppPassword.InputSchema,\n    opts?: ComAtprotoServerCreateAppPassword.CallOptions,\n  ): Promise<ComAtprotoServerCreateAppPassword.Response> {\n    return this._client\n      .call('com.atproto.server.createAppPassword', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerCreateAppPassword.toKnownErr(e)\n      })\n  }\n\n  createInviteCode(\n    data?: ComAtprotoServerCreateInviteCode.InputSchema,\n    opts?: ComAtprotoServerCreateInviteCode.CallOptions,\n  ): Promise<ComAtprotoServerCreateInviteCode.Response> {\n    return this._client.call(\n      'com.atproto.server.createInviteCode',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  createInviteCodes(\n    data?: ComAtprotoServerCreateInviteCodes.InputSchema,\n    opts?: ComAtprotoServerCreateInviteCodes.CallOptions,\n  ): Promise<ComAtprotoServerCreateInviteCodes.Response> {\n    return this._client.call(\n      'com.atproto.server.createInviteCodes',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  createSession(\n    data?: ComAtprotoServerCreateSession.InputSchema,\n    opts?: ComAtprotoServerCreateSession.CallOptions,\n  ): Promise<ComAtprotoServerCreateSession.Response> {\n    return this._client\n      .call('com.atproto.server.createSession', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerCreateSession.toKnownErr(e)\n      })\n  }\n\n  deactivateAccount(\n    data?: ComAtprotoServerDeactivateAccount.InputSchema,\n    opts?: ComAtprotoServerDeactivateAccount.CallOptions,\n  ): Promise<ComAtprotoServerDeactivateAccount.Response> {\n    return this._client.call(\n      'com.atproto.server.deactivateAccount',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  deleteAccount(\n    data?: ComAtprotoServerDeleteAccount.InputSchema,\n    opts?: ComAtprotoServerDeleteAccount.CallOptions,\n  ): Promise<ComAtprotoServerDeleteAccount.Response> {\n    return this._client\n      .call('com.atproto.server.deleteAccount', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerDeleteAccount.toKnownErr(e)\n      })\n  }\n\n  deleteSession(\n    data?: ComAtprotoServerDeleteSession.InputSchema,\n    opts?: ComAtprotoServerDeleteSession.CallOptions,\n  ): Promise<ComAtprotoServerDeleteSession.Response> {\n    return this._client\n      .call('com.atproto.server.deleteSession', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerDeleteSession.toKnownErr(e)\n      })\n  }\n\n  describeServer(\n    params?: ComAtprotoServerDescribeServer.QueryParams,\n    opts?: ComAtprotoServerDescribeServer.CallOptions,\n  ): Promise<ComAtprotoServerDescribeServer.Response> {\n    return this._client.call(\n      'com.atproto.server.describeServer',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getAccountInviteCodes(\n    params?: ComAtprotoServerGetAccountInviteCodes.QueryParams,\n    opts?: ComAtprotoServerGetAccountInviteCodes.CallOptions,\n  ): Promise<ComAtprotoServerGetAccountInviteCodes.Response> {\n    return this._client\n      .call('com.atproto.server.getAccountInviteCodes', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoServerGetAccountInviteCodes.toKnownErr(e)\n      })\n  }\n\n  getServiceAuth(\n    params?: ComAtprotoServerGetServiceAuth.QueryParams,\n    opts?: ComAtprotoServerGetServiceAuth.CallOptions,\n  ): Promise<ComAtprotoServerGetServiceAuth.Response> {\n    return this._client\n      .call('com.atproto.server.getServiceAuth', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoServerGetServiceAuth.toKnownErr(e)\n      })\n  }\n\n  getSession(\n    params?: ComAtprotoServerGetSession.QueryParams,\n    opts?: ComAtprotoServerGetSession.CallOptions,\n  ): Promise<ComAtprotoServerGetSession.Response> {\n    return this._client.call(\n      'com.atproto.server.getSession',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listAppPasswords(\n    params?: ComAtprotoServerListAppPasswords.QueryParams,\n    opts?: ComAtprotoServerListAppPasswords.CallOptions,\n  ): Promise<ComAtprotoServerListAppPasswords.Response> {\n    return this._client\n      .call('com.atproto.server.listAppPasswords', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoServerListAppPasswords.toKnownErr(e)\n      })\n  }\n\n  refreshSession(\n    data?: ComAtprotoServerRefreshSession.InputSchema,\n    opts?: ComAtprotoServerRefreshSession.CallOptions,\n  ): Promise<ComAtprotoServerRefreshSession.Response> {\n    return this._client\n      .call('com.atproto.server.refreshSession', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerRefreshSession.toKnownErr(e)\n      })\n  }\n\n  requestAccountDelete(\n    data?: ComAtprotoServerRequestAccountDelete.InputSchema,\n    opts?: ComAtprotoServerRequestAccountDelete.CallOptions,\n  ): Promise<ComAtprotoServerRequestAccountDelete.Response> {\n    return this._client.call(\n      'com.atproto.server.requestAccountDelete',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  requestEmailConfirmation(\n    data?: ComAtprotoServerRequestEmailConfirmation.InputSchema,\n    opts?: ComAtprotoServerRequestEmailConfirmation.CallOptions,\n  ): Promise<ComAtprotoServerRequestEmailConfirmation.Response> {\n    return this._client.call(\n      'com.atproto.server.requestEmailConfirmation',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  requestEmailUpdate(\n    data?: ComAtprotoServerRequestEmailUpdate.InputSchema,\n    opts?: ComAtprotoServerRequestEmailUpdate.CallOptions,\n  ): Promise<ComAtprotoServerRequestEmailUpdate.Response> {\n    return this._client.call(\n      'com.atproto.server.requestEmailUpdate',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  requestPasswordReset(\n    data?: ComAtprotoServerRequestPasswordReset.InputSchema,\n    opts?: ComAtprotoServerRequestPasswordReset.CallOptions,\n  ): Promise<ComAtprotoServerRequestPasswordReset.Response> {\n    return this._client.call(\n      'com.atproto.server.requestPasswordReset',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  reserveSigningKey(\n    data?: ComAtprotoServerReserveSigningKey.InputSchema,\n    opts?: ComAtprotoServerReserveSigningKey.CallOptions,\n  ): Promise<ComAtprotoServerReserveSigningKey.Response> {\n    return this._client.call(\n      'com.atproto.server.reserveSigningKey',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  resetPassword(\n    data?: ComAtprotoServerResetPassword.InputSchema,\n    opts?: ComAtprotoServerResetPassword.CallOptions,\n  ): Promise<ComAtprotoServerResetPassword.Response> {\n    return this._client\n      .call('com.atproto.server.resetPassword', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerResetPassword.toKnownErr(e)\n      })\n  }\n\n  revokeAppPassword(\n    data?: ComAtprotoServerRevokeAppPassword.InputSchema,\n    opts?: ComAtprotoServerRevokeAppPassword.CallOptions,\n  ): Promise<ComAtprotoServerRevokeAppPassword.Response> {\n    return this._client.call(\n      'com.atproto.server.revokeAppPassword',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  updateEmail(\n    data?: ComAtprotoServerUpdateEmail.InputSchema,\n    opts?: ComAtprotoServerUpdateEmail.CallOptions,\n  ): Promise<ComAtprotoServerUpdateEmail.Response> {\n    return this._client\n      .call('com.atproto.server.updateEmail', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoServerUpdateEmail.toKnownErr(e)\n      })\n  }\n}\n\nexport class ComAtprotoSyncNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getBlob(\n    params?: ComAtprotoSyncGetBlob.QueryParams,\n    opts?: ComAtprotoSyncGetBlob.CallOptions,\n  ): Promise<ComAtprotoSyncGetBlob.Response> {\n    return this._client\n      .call('com.atproto.sync.getBlob', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetBlob.toKnownErr(e)\n      })\n  }\n\n  getBlocks(\n    params?: ComAtprotoSyncGetBlocks.QueryParams,\n    opts?: ComAtprotoSyncGetBlocks.CallOptions,\n  ): Promise<ComAtprotoSyncGetBlocks.Response> {\n    return this._client\n      .call('com.atproto.sync.getBlocks', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetBlocks.toKnownErr(e)\n      })\n  }\n\n  getCheckout(\n    params?: ComAtprotoSyncGetCheckout.QueryParams,\n    opts?: ComAtprotoSyncGetCheckout.CallOptions,\n  ): Promise<ComAtprotoSyncGetCheckout.Response> {\n    return this._client.call(\n      'com.atproto.sync.getCheckout',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getHead(\n    params?: ComAtprotoSyncGetHead.QueryParams,\n    opts?: ComAtprotoSyncGetHead.CallOptions,\n  ): Promise<ComAtprotoSyncGetHead.Response> {\n    return this._client\n      .call('com.atproto.sync.getHead', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetHead.toKnownErr(e)\n      })\n  }\n\n  getHostStatus(\n    params?: ComAtprotoSyncGetHostStatus.QueryParams,\n    opts?: ComAtprotoSyncGetHostStatus.CallOptions,\n  ): Promise<ComAtprotoSyncGetHostStatus.Response> {\n    return this._client\n      .call('com.atproto.sync.getHostStatus', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetHostStatus.toKnownErr(e)\n      })\n  }\n\n  getLatestCommit(\n    params?: ComAtprotoSyncGetLatestCommit.QueryParams,\n    opts?: ComAtprotoSyncGetLatestCommit.CallOptions,\n  ): Promise<ComAtprotoSyncGetLatestCommit.Response> {\n    return this._client\n      .call('com.atproto.sync.getLatestCommit', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetLatestCommit.toKnownErr(e)\n      })\n  }\n\n  getRecord(\n    params?: ComAtprotoSyncGetRecord.QueryParams,\n    opts?: ComAtprotoSyncGetRecord.CallOptions,\n  ): Promise<ComAtprotoSyncGetRecord.Response> {\n    return this._client\n      .call('com.atproto.sync.getRecord', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetRecord.toKnownErr(e)\n      })\n  }\n\n  getRepo(\n    params?: ComAtprotoSyncGetRepo.QueryParams,\n    opts?: ComAtprotoSyncGetRepo.CallOptions,\n  ): Promise<ComAtprotoSyncGetRepo.Response> {\n    return this._client\n      .call('com.atproto.sync.getRepo', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetRepo.toKnownErr(e)\n      })\n  }\n\n  getRepoStatus(\n    params?: ComAtprotoSyncGetRepoStatus.QueryParams,\n    opts?: ComAtprotoSyncGetRepoStatus.CallOptions,\n  ): Promise<ComAtprotoSyncGetRepoStatus.Response> {\n    return this._client\n      .call('com.atproto.sync.getRepoStatus', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncGetRepoStatus.toKnownErr(e)\n      })\n  }\n\n  listBlobs(\n    params?: ComAtprotoSyncListBlobs.QueryParams,\n    opts?: ComAtprotoSyncListBlobs.CallOptions,\n  ): Promise<ComAtprotoSyncListBlobs.Response> {\n    return this._client\n      .call('com.atproto.sync.listBlobs', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncListBlobs.toKnownErr(e)\n      })\n  }\n\n  listHosts(\n    params?: ComAtprotoSyncListHosts.QueryParams,\n    opts?: ComAtprotoSyncListHosts.CallOptions,\n  ): Promise<ComAtprotoSyncListHosts.Response> {\n    return this._client.call(\n      'com.atproto.sync.listHosts',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listRepos(\n    params?: ComAtprotoSyncListRepos.QueryParams,\n    opts?: ComAtprotoSyncListRepos.CallOptions,\n  ): Promise<ComAtprotoSyncListRepos.Response> {\n    return this._client.call(\n      'com.atproto.sync.listRepos',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listReposByCollection(\n    params?: ComAtprotoSyncListReposByCollection.QueryParams,\n    opts?: ComAtprotoSyncListReposByCollection.CallOptions,\n  ): Promise<ComAtprotoSyncListReposByCollection.Response> {\n    return this._client.call(\n      'com.atproto.sync.listReposByCollection',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  notifyOfUpdate(\n    data?: ComAtprotoSyncNotifyOfUpdate.InputSchema,\n    opts?: ComAtprotoSyncNotifyOfUpdate.CallOptions,\n  ): Promise<ComAtprotoSyncNotifyOfUpdate.Response> {\n    return this._client.call(\n      'com.atproto.sync.notifyOfUpdate',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  requestCrawl(\n    data?: ComAtprotoSyncRequestCrawl.InputSchema,\n    opts?: ComAtprotoSyncRequestCrawl.CallOptions,\n  ): Promise<ComAtprotoSyncRequestCrawl.Response> {\n    return this._client\n      .call('com.atproto.sync.requestCrawl', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ComAtprotoSyncRequestCrawl.toKnownErr(e)\n      })\n  }\n}\n\nexport class ComAtprotoTempNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  addReservedHandle(\n    data?: ComAtprotoTempAddReservedHandle.InputSchema,\n    opts?: ComAtprotoTempAddReservedHandle.CallOptions,\n  ): Promise<ComAtprotoTempAddReservedHandle.Response> {\n    return this._client.call(\n      'com.atproto.temp.addReservedHandle',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  checkHandleAvailability(\n    params?: ComAtprotoTempCheckHandleAvailability.QueryParams,\n    opts?: ComAtprotoTempCheckHandleAvailability.CallOptions,\n  ): Promise<ComAtprotoTempCheckHandleAvailability.Response> {\n    return this._client\n      .call('com.atproto.temp.checkHandleAvailability', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoTempCheckHandleAvailability.toKnownErr(e)\n      })\n  }\n\n  checkSignupQueue(\n    params?: ComAtprotoTempCheckSignupQueue.QueryParams,\n    opts?: ComAtprotoTempCheckSignupQueue.CallOptions,\n  ): Promise<ComAtprotoTempCheckSignupQueue.Response> {\n    return this._client.call(\n      'com.atproto.temp.checkSignupQueue',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  dereferenceScope(\n    params?: ComAtprotoTempDereferenceScope.QueryParams,\n    opts?: ComAtprotoTempDereferenceScope.CallOptions,\n  ): Promise<ComAtprotoTempDereferenceScope.Response> {\n    return this._client\n      .call('com.atproto.temp.dereferenceScope', params, undefined, opts)\n      .catch((e) => {\n        throw ComAtprotoTempDereferenceScope.toKnownErr(e)\n      })\n  }\n\n  fetchLabels(\n    params?: ComAtprotoTempFetchLabels.QueryParams,\n    opts?: ComAtprotoTempFetchLabels.CallOptions,\n  ): Promise<ComAtprotoTempFetchLabels.Response> {\n    return this._client.call(\n      'com.atproto.temp.fetchLabels',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  requestPhoneVerification(\n    data?: ComAtprotoTempRequestPhoneVerification.InputSchema,\n    opts?: ComAtprotoTempRequestPhoneVerification.CallOptions,\n  ): Promise<ComAtprotoTempRequestPhoneVerification.Response> {\n    return this._client.call(\n      'com.atproto.temp.requestPhoneVerification',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  revokeAccountCredentials(\n    data?: ComAtprotoTempRevokeAccountCredentials.InputSchema,\n    opts?: ComAtprotoTempRevokeAccountCredentials.CallOptions,\n  ): Promise<ComAtprotoTempRevokeAccountCredentials.Response> {\n    return this._client.call(\n      'com.atproto.temp.revokeAccountCredentials',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ComGermnetworkNS {\n  _client: XrpcClient\n  declaration: ComGermnetworkDeclarationRecord\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.declaration = new ComGermnetworkDeclarationRecord(client)\n  }\n}\n\nexport class ComGermnetworkDeclarationRecord {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  async list(\n    params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>,\n  ): Promise<{\n    cursor?: string\n    records: { uri: string; value: ComGermnetworkDeclaration.Record }[]\n  }> {\n    const res = await this._client.call('com.atproto.repo.listRecords', {\n      collection: 'com.germnetwork.declaration',\n      ...params,\n    })\n    return res.data\n  }\n\n  async get(\n    params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: ComGermnetworkDeclaration.Record\n  }> {\n    const res = await this._client.call('com.atproto.repo.getRecord', {\n      collection: 'com.germnetwork.declaration',\n      ...params,\n    })\n    return res.data\n  }\n\n  async create(\n    params: OmitKey<\n      ComAtprotoRepoCreateRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<ComGermnetworkDeclaration.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'com.germnetwork.declaration'\n    const res = await this._client.call(\n      'com.atproto.repo.createRecord',\n      undefined,\n      {\n        collection,\n        rkey: 'self',\n        ...params,\n        record: { ...record, $type: collection },\n      },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async put(\n    params: OmitKey<\n      ComAtprotoRepoPutRecord.InputSchema,\n      'collection' | 'record'\n    >,\n    record: Un$Typed<ComGermnetworkDeclaration.Record>,\n    headers?: Record<string, string>,\n  ): Promise<{ uri: string; cid: string }> {\n    const collection = 'com.germnetwork.declaration'\n    const res = await this._client.call(\n      'com.atproto.repo.putRecord',\n      undefined,\n      { collection, ...params, record: { ...record, $type: collection } },\n      { encoding: 'application/json', headers },\n    )\n    return res.data\n  }\n\n  async delete(\n    params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>,\n    headers?: Record<string, string>,\n  ): Promise<void> {\n    await this._client.call(\n      'com.atproto.repo.deleteRecord',\n      undefined,\n      { collection: 'com.germnetwork.declaration', ...params },\n      { headers },\n    )\n  }\n}\n\nexport class ToolsNS {\n  _client: XrpcClient\n  ozone: ToolsOzoneNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.ozone = new ToolsOzoneNS(client)\n  }\n}\n\nexport class ToolsOzoneNS {\n  _client: XrpcClient\n  communication: ToolsOzoneCommunicationNS\n  hosting: ToolsOzoneHostingNS\n  moderation: ToolsOzoneModerationNS\n  safelink: ToolsOzoneSafelinkNS\n  server: ToolsOzoneServerNS\n  set: ToolsOzoneSetNS\n  setting: ToolsOzoneSettingNS\n  signature: ToolsOzoneSignatureNS\n  team: ToolsOzoneTeamNS\n  verification: ToolsOzoneVerificationNS\n\n  constructor(client: XrpcClient) {\n    this._client = client\n    this.communication = new ToolsOzoneCommunicationNS(client)\n    this.hosting = new ToolsOzoneHostingNS(client)\n    this.moderation = new ToolsOzoneModerationNS(client)\n    this.safelink = new ToolsOzoneSafelinkNS(client)\n    this.server = new ToolsOzoneServerNS(client)\n    this.set = new ToolsOzoneSetNS(client)\n    this.setting = new ToolsOzoneSettingNS(client)\n    this.signature = new ToolsOzoneSignatureNS(client)\n    this.team = new ToolsOzoneTeamNS(client)\n    this.verification = new ToolsOzoneVerificationNS(client)\n  }\n}\n\nexport class ToolsOzoneCommunicationNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  createTemplate(\n    data?: ToolsOzoneCommunicationCreateTemplate.InputSchema,\n    opts?: ToolsOzoneCommunicationCreateTemplate.CallOptions,\n  ): Promise<ToolsOzoneCommunicationCreateTemplate.Response> {\n    return this._client\n      .call('tools.ozone.communication.createTemplate', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneCommunicationCreateTemplate.toKnownErr(e)\n      })\n  }\n\n  deleteTemplate(\n    data?: ToolsOzoneCommunicationDeleteTemplate.InputSchema,\n    opts?: ToolsOzoneCommunicationDeleteTemplate.CallOptions,\n  ): Promise<ToolsOzoneCommunicationDeleteTemplate.Response> {\n    return this._client.call(\n      'tools.ozone.communication.deleteTemplate',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  listTemplates(\n    params?: ToolsOzoneCommunicationListTemplates.QueryParams,\n    opts?: ToolsOzoneCommunicationListTemplates.CallOptions,\n  ): Promise<ToolsOzoneCommunicationListTemplates.Response> {\n    return this._client.call(\n      'tools.ozone.communication.listTemplates',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  updateTemplate(\n    data?: ToolsOzoneCommunicationUpdateTemplate.InputSchema,\n    opts?: ToolsOzoneCommunicationUpdateTemplate.CallOptions,\n  ): Promise<ToolsOzoneCommunicationUpdateTemplate.Response> {\n    return this._client\n      .call('tools.ozone.communication.updateTemplate', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneCommunicationUpdateTemplate.toKnownErr(e)\n      })\n  }\n}\n\nexport class ToolsOzoneHostingNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getAccountHistory(\n    params?: ToolsOzoneHostingGetAccountHistory.QueryParams,\n    opts?: ToolsOzoneHostingGetAccountHistory.CallOptions,\n  ): Promise<ToolsOzoneHostingGetAccountHistory.Response> {\n    return this._client.call(\n      'tools.ozone.hosting.getAccountHistory',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class ToolsOzoneModerationNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  cancelScheduledActions(\n    data?: ToolsOzoneModerationCancelScheduledActions.InputSchema,\n    opts?: ToolsOzoneModerationCancelScheduledActions.CallOptions,\n  ): Promise<ToolsOzoneModerationCancelScheduledActions.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.cancelScheduledActions',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  emitEvent(\n    data?: ToolsOzoneModerationEmitEvent.InputSchema,\n    opts?: ToolsOzoneModerationEmitEvent.CallOptions,\n  ): Promise<ToolsOzoneModerationEmitEvent.Response> {\n    return this._client\n      .call('tools.ozone.moderation.emitEvent', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneModerationEmitEvent.toKnownErr(e)\n      })\n  }\n\n  getAccountTimeline(\n    params?: ToolsOzoneModerationGetAccountTimeline.QueryParams,\n    opts?: ToolsOzoneModerationGetAccountTimeline.CallOptions,\n  ): Promise<ToolsOzoneModerationGetAccountTimeline.Response> {\n    return this._client\n      .call(\n        'tools.ozone.moderation.getAccountTimeline',\n        params,\n        undefined,\n        opts,\n      )\n      .catch((e) => {\n        throw ToolsOzoneModerationGetAccountTimeline.toKnownErr(e)\n      })\n  }\n\n  getEvent(\n    params?: ToolsOzoneModerationGetEvent.QueryParams,\n    opts?: ToolsOzoneModerationGetEvent.CallOptions,\n  ): Promise<ToolsOzoneModerationGetEvent.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.getEvent',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getRecord(\n    params?: ToolsOzoneModerationGetRecord.QueryParams,\n    opts?: ToolsOzoneModerationGetRecord.CallOptions,\n  ): Promise<ToolsOzoneModerationGetRecord.Response> {\n    return this._client\n      .call('tools.ozone.moderation.getRecord', params, undefined, opts)\n      .catch((e) => {\n        throw ToolsOzoneModerationGetRecord.toKnownErr(e)\n      })\n  }\n\n  getRecords(\n    params?: ToolsOzoneModerationGetRecords.QueryParams,\n    opts?: ToolsOzoneModerationGetRecords.CallOptions,\n  ): Promise<ToolsOzoneModerationGetRecords.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.getRecords',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getRepo(\n    params?: ToolsOzoneModerationGetRepo.QueryParams,\n    opts?: ToolsOzoneModerationGetRepo.CallOptions,\n  ): Promise<ToolsOzoneModerationGetRepo.Response> {\n    return this._client\n      .call('tools.ozone.moderation.getRepo', params, undefined, opts)\n      .catch((e) => {\n        throw ToolsOzoneModerationGetRepo.toKnownErr(e)\n      })\n  }\n\n  getReporterStats(\n    params?: ToolsOzoneModerationGetReporterStats.QueryParams,\n    opts?: ToolsOzoneModerationGetReporterStats.CallOptions,\n  ): Promise<ToolsOzoneModerationGetReporterStats.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.getReporterStats',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getRepos(\n    params?: ToolsOzoneModerationGetRepos.QueryParams,\n    opts?: ToolsOzoneModerationGetRepos.CallOptions,\n  ): Promise<ToolsOzoneModerationGetRepos.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.getRepos',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  getSubjects(\n    params?: ToolsOzoneModerationGetSubjects.QueryParams,\n    opts?: ToolsOzoneModerationGetSubjects.CallOptions,\n  ): Promise<ToolsOzoneModerationGetSubjects.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.getSubjects',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  listScheduledActions(\n    data?: ToolsOzoneModerationListScheduledActions.InputSchema,\n    opts?: ToolsOzoneModerationListScheduledActions.CallOptions,\n  ): Promise<ToolsOzoneModerationListScheduledActions.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.listScheduledActions',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  queryEvents(\n    params?: ToolsOzoneModerationQueryEvents.QueryParams,\n    opts?: ToolsOzoneModerationQueryEvents.CallOptions,\n  ): Promise<ToolsOzoneModerationQueryEvents.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.queryEvents',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  queryStatuses(\n    params?: ToolsOzoneModerationQueryStatuses.QueryParams,\n    opts?: ToolsOzoneModerationQueryStatuses.CallOptions,\n  ): Promise<ToolsOzoneModerationQueryStatuses.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.queryStatuses',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  scheduleAction(\n    data?: ToolsOzoneModerationScheduleAction.InputSchema,\n    opts?: ToolsOzoneModerationScheduleAction.CallOptions,\n  ): Promise<ToolsOzoneModerationScheduleAction.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.scheduleAction',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  searchRepos(\n    params?: ToolsOzoneModerationSearchRepos.QueryParams,\n    opts?: ToolsOzoneModerationSearchRepos.CallOptions,\n  ): Promise<ToolsOzoneModerationSearchRepos.Response> {\n    return this._client.call(\n      'tools.ozone.moderation.searchRepos',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class ToolsOzoneSafelinkNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  addRule(\n    data?: ToolsOzoneSafelinkAddRule.InputSchema,\n    opts?: ToolsOzoneSafelinkAddRule.CallOptions,\n  ): Promise<ToolsOzoneSafelinkAddRule.Response> {\n    return this._client\n      .call('tools.ozone.safelink.addRule', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneSafelinkAddRule.toKnownErr(e)\n      })\n  }\n\n  queryEvents(\n    data?: ToolsOzoneSafelinkQueryEvents.InputSchema,\n    opts?: ToolsOzoneSafelinkQueryEvents.CallOptions,\n  ): Promise<ToolsOzoneSafelinkQueryEvents.Response> {\n    return this._client.call(\n      'tools.ozone.safelink.queryEvents',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  queryRules(\n    data?: ToolsOzoneSafelinkQueryRules.InputSchema,\n    opts?: ToolsOzoneSafelinkQueryRules.CallOptions,\n  ): Promise<ToolsOzoneSafelinkQueryRules.Response> {\n    return this._client.call(\n      'tools.ozone.safelink.queryRules',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  removeRule(\n    data?: ToolsOzoneSafelinkRemoveRule.InputSchema,\n    opts?: ToolsOzoneSafelinkRemoveRule.CallOptions,\n  ): Promise<ToolsOzoneSafelinkRemoveRule.Response> {\n    return this._client\n      .call('tools.ozone.safelink.removeRule', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneSafelinkRemoveRule.toKnownErr(e)\n      })\n  }\n\n  updateRule(\n    data?: ToolsOzoneSafelinkUpdateRule.InputSchema,\n    opts?: ToolsOzoneSafelinkUpdateRule.CallOptions,\n  ): Promise<ToolsOzoneSafelinkUpdateRule.Response> {\n    return this._client\n      .call('tools.ozone.safelink.updateRule', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneSafelinkUpdateRule.toKnownErr(e)\n      })\n  }\n}\n\nexport class ToolsOzoneServerNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  getConfig(\n    params?: ToolsOzoneServerGetConfig.QueryParams,\n    opts?: ToolsOzoneServerGetConfig.CallOptions,\n  ): Promise<ToolsOzoneServerGetConfig.Response> {\n    return this._client.call(\n      'tools.ozone.server.getConfig',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class ToolsOzoneSetNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  addValues(\n    data?: ToolsOzoneSetAddValues.InputSchema,\n    opts?: ToolsOzoneSetAddValues.CallOptions,\n  ): Promise<ToolsOzoneSetAddValues.Response> {\n    return this._client.call('tools.ozone.set.addValues', opts?.qp, data, opts)\n  }\n\n  deleteSet(\n    data?: ToolsOzoneSetDeleteSet.InputSchema,\n    opts?: ToolsOzoneSetDeleteSet.CallOptions,\n  ): Promise<ToolsOzoneSetDeleteSet.Response> {\n    return this._client\n      .call('tools.ozone.set.deleteSet', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneSetDeleteSet.toKnownErr(e)\n      })\n  }\n\n  deleteValues(\n    data?: ToolsOzoneSetDeleteValues.InputSchema,\n    opts?: ToolsOzoneSetDeleteValues.CallOptions,\n  ): Promise<ToolsOzoneSetDeleteValues.Response> {\n    return this._client\n      .call('tools.ozone.set.deleteValues', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneSetDeleteValues.toKnownErr(e)\n      })\n  }\n\n  getValues(\n    params?: ToolsOzoneSetGetValues.QueryParams,\n    opts?: ToolsOzoneSetGetValues.CallOptions,\n  ): Promise<ToolsOzoneSetGetValues.Response> {\n    return this._client\n      .call('tools.ozone.set.getValues', params, undefined, opts)\n      .catch((e) => {\n        throw ToolsOzoneSetGetValues.toKnownErr(e)\n      })\n  }\n\n  querySets(\n    params?: ToolsOzoneSetQuerySets.QueryParams,\n    opts?: ToolsOzoneSetQuerySets.CallOptions,\n  ): Promise<ToolsOzoneSetQuerySets.Response> {\n    return this._client.call(\n      'tools.ozone.set.querySets',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  upsertSet(\n    data?: ToolsOzoneSetUpsertSet.InputSchema,\n    opts?: ToolsOzoneSetUpsertSet.CallOptions,\n  ): Promise<ToolsOzoneSetUpsertSet.Response> {\n    return this._client.call('tools.ozone.set.upsertSet', opts?.qp, data, opts)\n  }\n}\n\nexport class ToolsOzoneSettingNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  listOptions(\n    params?: ToolsOzoneSettingListOptions.QueryParams,\n    opts?: ToolsOzoneSettingListOptions.CallOptions,\n  ): Promise<ToolsOzoneSettingListOptions.Response> {\n    return this._client.call(\n      'tools.ozone.setting.listOptions',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  removeOptions(\n    data?: ToolsOzoneSettingRemoveOptions.InputSchema,\n    opts?: ToolsOzoneSettingRemoveOptions.CallOptions,\n  ): Promise<ToolsOzoneSettingRemoveOptions.Response> {\n    return this._client.call(\n      'tools.ozone.setting.removeOptions',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  upsertOption(\n    data?: ToolsOzoneSettingUpsertOption.InputSchema,\n    opts?: ToolsOzoneSettingUpsertOption.CallOptions,\n  ): Promise<ToolsOzoneSettingUpsertOption.Response> {\n    return this._client.call(\n      'tools.ozone.setting.upsertOption',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n\nexport class ToolsOzoneSignatureNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  findCorrelation(\n    params?: ToolsOzoneSignatureFindCorrelation.QueryParams,\n    opts?: ToolsOzoneSignatureFindCorrelation.CallOptions,\n  ): Promise<ToolsOzoneSignatureFindCorrelation.Response> {\n    return this._client.call(\n      'tools.ozone.signature.findCorrelation',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  findRelatedAccounts(\n    params?: ToolsOzoneSignatureFindRelatedAccounts.QueryParams,\n    opts?: ToolsOzoneSignatureFindRelatedAccounts.CallOptions,\n  ): Promise<ToolsOzoneSignatureFindRelatedAccounts.Response> {\n    return this._client.call(\n      'tools.ozone.signature.findRelatedAccounts',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  searchAccounts(\n    params?: ToolsOzoneSignatureSearchAccounts.QueryParams,\n    opts?: ToolsOzoneSignatureSearchAccounts.CallOptions,\n  ): Promise<ToolsOzoneSignatureSearchAccounts.Response> {\n    return this._client.call(\n      'tools.ozone.signature.searchAccounts',\n      params,\n      undefined,\n      opts,\n    )\n  }\n}\n\nexport class ToolsOzoneTeamNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  addMember(\n    data?: ToolsOzoneTeamAddMember.InputSchema,\n    opts?: ToolsOzoneTeamAddMember.CallOptions,\n  ): Promise<ToolsOzoneTeamAddMember.Response> {\n    return this._client\n      .call('tools.ozone.team.addMember', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneTeamAddMember.toKnownErr(e)\n      })\n  }\n\n  deleteMember(\n    data?: ToolsOzoneTeamDeleteMember.InputSchema,\n    opts?: ToolsOzoneTeamDeleteMember.CallOptions,\n  ): Promise<ToolsOzoneTeamDeleteMember.Response> {\n    return this._client\n      .call('tools.ozone.team.deleteMember', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneTeamDeleteMember.toKnownErr(e)\n      })\n  }\n\n  listMembers(\n    params?: ToolsOzoneTeamListMembers.QueryParams,\n    opts?: ToolsOzoneTeamListMembers.CallOptions,\n  ): Promise<ToolsOzoneTeamListMembers.Response> {\n    return this._client.call(\n      'tools.ozone.team.listMembers',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  updateMember(\n    data?: ToolsOzoneTeamUpdateMember.InputSchema,\n    opts?: ToolsOzoneTeamUpdateMember.CallOptions,\n  ): Promise<ToolsOzoneTeamUpdateMember.Response> {\n    return this._client\n      .call('tools.ozone.team.updateMember', opts?.qp, data, opts)\n      .catch((e) => {\n        throw ToolsOzoneTeamUpdateMember.toKnownErr(e)\n      })\n  }\n}\n\nexport class ToolsOzoneVerificationNS {\n  _client: XrpcClient\n\n  constructor(client: XrpcClient) {\n    this._client = client\n  }\n\n  grantVerifications(\n    data?: ToolsOzoneVerificationGrantVerifications.InputSchema,\n    opts?: ToolsOzoneVerificationGrantVerifications.CallOptions,\n  ): Promise<ToolsOzoneVerificationGrantVerifications.Response> {\n    return this._client.call(\n      'tools.ozone.verification.grantVerifications',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n\n  listVerifications(\n    params?: ToolsOzoneVerificationListVerifications.QueryParams,\n    opts?: ToolsOzoneVerificationListVerifications.CallOptions,\n  ): Promise<ToolsOzoneVerificationListVerifications.Response> {\n    return this._client.call(\n      'tools.ozone.verification.listVerifications',\n      params,\n      undefined,\n      opts,\n    )\n  }\n\n  revokeVerifications(\n    data?: ToolsOzoneVerificationRevokeVerifications.InputSchema,\n    opts?: ToolsOzoneVerificationRevokeVerifications.CallOptions,\n  ): Promise<ToolsOzoneVerificationRevokeVerifications.Response> {\n    return this._client.call(\n      'tools.ozone.verification.revokeVerifications',\n      opts?.qp,\n      data,\n      opts,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/api/src/client/lexicons.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type LexiconDoc,\n  Lexicons,\n  ValidationError,\n  type ValidationResult,\n} from '@atproto/lexicon'\nimport { type $Typed, is$typed, maybe$typed } from './util.js'\n\nexport const schemaDict = {\n  AppBskyActorDefs: {\n    lexicon: 1,\n    id: 'app.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileView: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileViewDetailed: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          website: {\n            type: 'string',\n            format: 'uri',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          banner: {\n            type: 'string',\n            format: 'uri',\n          },\n          followersCount: {\n            type: 'integer',\n          },\n          followsCount: {\n            type: 'integer',\n          },\n          postsCount: {\n            type: 'integer',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          joinedViaStarterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          pinnedPost: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileAssociated: {\n        type: 'object',\n        properties: {\n          lists: {\n            type: 'integer',\n          },\n          feedgens: {\n            type: 'integer',\n          },\n          starterPacks: {\n            type: 'integer',\n          },\n          labeler: {\n            type: 'boolean',\n          },\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedChat',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedActivitySubscription',\n          },\n          germ: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedGerm',\n          },\n        },\n      },\n      profileAssociatedChat: {\n        type: 'object',\n        required: ['allowIncoming'],\n        properties: {\n          allowIncoming: {\n            type: 'string',\n            knownValues: ['all', 'none', 'following'],\n          },\n        },\n      },\n      profileAssociatedGerm: {\n        type: 'object',\n        required: ['showButtonTo', 'messageMeUrl'],\n        properties: {\n          messageMeUrl: {\n            type: 'string',\n            format: 'uri',\n          },\n          showButtonTo: {\n            type: 'string',\n            knownValues: ['usersIFollow', 'everyone'],\n          },\n        },\n      },\n      profileAssociatedActivitySubscription: {\n        type: 'object',\n        required: ['allowSubscriptions'],\n        properties: {\n          allowSubscriptions: {\n            type: 'string',\n            knownValues: ['followers', 'mutuals', 'none'],\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.\",\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          mutedByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          blockedBy: {\n            type: 'boolean',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blockingByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          knownFollowers: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#knownFollowers',\n          },\n          activitySubscription: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n      knownFollowers: {\n        type: 'object',\n        description: \"The subject's followers whom you also follow\",\n        required: ['count', 'followers'],\n        properties: {\n          count: {\n            type: 'integer',\n          },\n          followers: {\n            type: 'array',\n            minLength: 0,\n            maxLength: 5,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      verificationState: {\n        type: 'object',\n        description:\n          'Represents the verification information about the user this object is attached to.',\n        required: ['verifications', 'verifiedStatus', 'trustedVerifierStatus'],\n        properties: {\n          verifications: {\n            type: 'array',\n            description:\n              'All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#verificationView',\n            },\n          },\n          verifiedStatus: {\n            type: 'string',\n            description: \"The user's status as a verified account.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n          trustedVerifierStatus: {\n            type: 'string',\n            description: \"The user's status as a trusted verifier.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n        },\n      },\n      verificationView: {\n        type: 'object',\n        description: 'An individual verification for an associated subject.',\n        required: ['issuer', 'uri', 'isValid', 'createdAt'],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          isValid: {\n            type: 'boolean',\n            description:\n              'True if the verification passes validation, otherwise false.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n        },\n      },\n      preferences: {\n        type: 'array',\n        items: {\n          type: 'union',\n          refs: [\n            'lex:app.bsky.actor.defs#adultContentPref',\n            'lex:app.bsky.actor.defs#contentLabelPref',\n            'lex:app.bsky.actor.defs#savedFeedsPref',\n            'lex:app.bsky.actor.defs#savedFeedsPrefV2',\n            'lex:app.bsky.actor.defs#personalDetailsPref',\n            'lex:app.bsky.actor.defs#declaredAgePref',\n            'lex:app.bsky.actor.defs#feedViewPref',\n            'lex:app.bsky.actor.defs#threadViewPref',\n            'lex:app.bsky.actor.defs#interestsPref',\n            'lex:app.bsky.actor.defs#mutedWordsPref',\n            'lex:app.bsky.actor.defs#hiddenPostsPref',\n            'lex:app.bsky.actor.defs#bskyAppStatePref',\n            'lex:app.bsky.actor.defs#labelersPref',\n            'lex:app.bsky.actor.defs#postInteractionSettingsPref',\n            'lex:app.bsky.actor.defs#verificationPrefs',\n            'lex:app.bsky.actor.defs#liveEventPreferences',\n          ],\n        },\n      },\n      adultContentPref: {\n        type: 'object',\n        required: ['enabled'],\n        properties: {\n          enabled: {\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      contentLabelPref: {\n        type: 'object',\n        required: ['label', 'visibility'],\n        properties: {\n          labelerDid: {\n            type: 'string',\n            description:\n              'Which labeler does this preference apply to? If undefined, applies globally.',\n            format: 'did',\n          },\n          label: {\n            type: 'string',\n          },\n          visibility: {\n            type: 'string',\n            knownValues: ['ignore', 'show', 'warn', 'hide'],\n          },\n        },\n      },\n      savedFeed: {\n        type: 'object',\n        required: ['id', 'type', 'value', 'pinned'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          type: {\n            type: 'string',\n            knownValues: ['feed', 'list', 'timeline'],\n          },\n          value: {\n            type: 'string',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      savedFeedsPrefV2: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#savedFeed',\n            },\n          },\n        },\n      },\n      savedFeedsPref: {\n        type: 'object',\n        required: ['pinned', 'saved'],\n        properties: {\n          pinned: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          saved: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          timelineIndex: {\n            type: 'integer',\n          },\n        },\n      },\n      personalDetailsPref: {\n        type: 'object',\n        properties: {\n          birthDate: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The birth date of account owner.',\n          },\n        },\n      },\n      declaredAgePref: {\n        type: 'object',\n        description:\n          \"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.\",\n        properties: {\n          isOverAge13: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 13 years of age.',\n          },\n          isOverAge16: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 16 years of age.',\n          },\n          isOverAge18: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 18 years of age.',\n          },\n        },\n      },\n      feedViewPref: {\n        type: 'object',\n        required: ['feed'],\n        properties: {\n          feed: {\n            type: 'string',\n            description:\n              'The URI of the feed, or an identifier which describes the feed.',\n          },\n          hideReplies: {\n            type: 'boolean',\n            description: 'Hide replies in the feed.',\n          },\n          hideRepliesByUnfollowed: {\n            type: 'boolean',\n            description:\n              'Hide replies in the feed if they are not by followed users.',\n            default: true,\n          },\n          hideRepliesByLikeCount: {\n            type: 'integer',\n            description:\n              'Hide replies in the feed if they do not have this number of likes.',\n          },\n          hideReposts: {\n            type: 'boolean',\n            description: 'Hide reposts in the feed.',\n          },\n          hideQuotePosts: {\n            type: 'boolean',\n            description: 'Hide quote posts in the feed.',\n          },\n        },\n      },\n      threadViewPref: {\n        type: 'object',\n        properties: {\n          sort: {\n            type: 'string',\n            description: 'Sorting mode for threads.',\n            knownValues: [\n              'oldest',\n              'newest',\n              'most-likes',\n              'random',\n              'hotness',\n            ],\n          },\n        },\n      },\n      interestsPref: {\n        type: 'object',\n        required: ['tags'],\n        properties: {\n          tags: {\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'string',\n              maxLength: 640,\n              maxGraphemes: 64,\n            },\n            description:\n              \"A list of tags which describe the account owner's interests gathered during onboarding.\",\n          },\n        },\n      },\n      mutedWordTarget: {\n        type: 'string',\n        knownValues: ['content', 'tag'],\n        maxLength: 640,\n        maxGraphemes: 64,\n      },\n      mutedWord: {\n        type: 'object',\n        description: 'A word that the account owner has muted.',\n        required: ['value', 'targets'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n            description: 'The muted word itself.',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          targets: {\n            type: 'array',\n            description: 'The intended targets of the muted word.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWordTarget',\n            },\n          },\n          actorTarget: {\n            type: 'string',\n            description:\n              'Groups of users to apply the muted word to. If undefined, applies to all users.',\n            knownValues: ['all', 'exclude-following'],\n            default: 'all',\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the muted word will expire and no longer be applied.',\n          },\n        },\n      },\n      mutedWordsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWord',\n            },\n            description: 'A list of words the account owner has muted.',\n          },\n        },\n      },\n      hiddenPostsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            description:\n              'A list of URIs of posts the account owner has hidden.',\n          },\n        },\n      },\n      labelersPref: {\n        type: 'object',\n        required: ['labelers'],\n        properties: {\n          labelers: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#labelerPrefItem',\n            },\n          },\n        },\n      },\n      labelerPrefItem: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      bskyAppStatePref: {\n        description:\n          \"A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.\",\n        type: 'object',\n        properties: {\n          activeProgressGuide: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#bskyAppProgressGuide',\n          },\n          queuedNudges: {\n            description:\n              'An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.',\n            type: 'array',\n            maxLength: 1000,\n            items: {\n              type: 'string',\n              maxLength: 100,\n            },\n          },\n          nuxs: {\n            description: 'Storage for NUXs the user has encountered.',\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#nux',\n            },\n          },\n        },\n      },\n      bskyAppProgressGuide: {\n        description:\n          'If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.',\n        type: 'object',\n        required: ['guide'],\n        properties: {\n          guide: {\n            type: 'string',\n            maxLength: 100,\n          },\n        },\n      },\n      nux: {\n        type: 'object',\n        description: 'A new user experiences (NUX) storage object',\n        required: ['id', 'completed'],\n        properties: {\n          id: {\n            type: 'string',\n            maxLength: 100,\n          },\n          completed: {\n            type: 'boolean',\n            default: false,\n          },\n          data: {\n            description:\n              'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.',\n            type: 'string',\n            maxLength: 3000,\n            maxGraphemes: 300,\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the NUX will expire and should be considered completed.',\n          },\n        },\n      },\n      verificationPrefs: {\n        type: 'object',\n        description: 'Preferences for how verified accounts appear in the app.',\n        required: [],\n        properties: {\n          hideBadges: {\n            description:\n              'Hide the blue check badges for verified accounts and trusted verifiers.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      liveEventPreferences: {\n        type: 'object',\n        description: 'Preferences for live events.',\n        properties: {\n          hiddenFeedIds: {\n            description:\n              'A list of feed IDs that the user has hidden from live events.',\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          hideAllFeeds: {\n            description: 'Whether to hide all feeds from live events.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      postInteractionSettingsPref: {\n        type: 'object',\n        description:\n          '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.',\n        required: [],\n        properties: {\n          threadgateAllowRules: {\n            description:\n              'Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n        },\n      },\n      statusView: {\n        type: 'object',\n        required: ['status', 'record'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          status: {\n            type: 'string',\n            description: 'The status for the account.',\n            knownValues: ['app.bsky.actor.status#live'],\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            description: 'An optional embed associated with the status.',\n            refs: ['lex:app.bsky.embed.external#view'],\n          },\n          expiresAt: {\n            type: 'string',\n            description:\n              'The date when this status will expire. The application might choose to no longer return the status after expiration.',\n            format: 'datetime',\n          },\n          isActive: {\n            type: 'boolean',\n            description:\n              'True if the status is not expired, false if it is expired. Only present if expiration was set.',\n          },\n          isDisabled: {\n            type: 'boolean',\n            description:\n              \"True if the user's go-live access has been disabled by a moderator, false otherwise.\",\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfile',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID of account to fetch profile of.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfiles: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfiles',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get detailed profile views of multiple actors.',\n        parameters: {\n          type: 'params',\n          required: ['actors'],\n          properties: {\n            actors: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['profiles'],\n            properties: {\n              profiles: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.profile',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account profile.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          properties: {\n            displayName: {\n              type: 'string',\n              maxGraphemes: 64,\n              maxLength: 640,\n            },\n            description: {\n              type: 'string',\n              description: 'Free-form profile description text.',\n              maxGraphemes: 256,\n              maxLength: 2560,\n            },\n            pronouns: {\n              type: 'string',\n              description: 'Free-form pronouns text.',\n              maxGraphemes: 20,\n              maxLength: 200,\n            },\n            website: {\n              type: 'string',\n              format: 'uri',\n            },\n            avatar: {\n              type: 'blob',\n              description:\n                \"Small image to be displayed next to posts from account. AKA, 'profile picture'\",\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            banner: {\n              type: 'blob',\n              description:\n                'Larger horizontal image to display behind profile view.',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values, specific to the Bluesky application, on the overall account.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            joinedViaStarterPack: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            pinnedPost: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Set the private preferences attached to the account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActors: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActors',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actors (profiles) matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActorsTypeahead: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActorsTypeahead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description: 'Search query prefix; not a full query string.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorStatus: {\n    lexicon: 1,\n    id: 'app.bsky.actor.status',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account status.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['status', 'createdAt'],\n          properties: {\n            status: {\n              type: 'string',\n              description: 'The status for the account.',\n              knownValues: ['app.bsky.actor.status#live'],\n            },\n            embed: {\n              type: 'union',\n              description: 'An optional embed associated with the status.',\n              refs: ['lex:app.bsky.embed.external'],\n            },\n            durationMinutes: {\n              type: 'integer',\n              description:\n                'The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.',\n              minimum: 1,\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      live: {\n        type: 'token',\n        description:\n          'Advertises an account as currently offering live content.',\n      },\n    },\n  },\n  AppBskyAgeassuranceBegin: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.begin',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate Age Assurance for an account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive Age Assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the Age Assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n              regionCode: {\n                type: 'string',\n                description:\n                  \"An optional ISO 3166-2 code of the user's region or state within the country.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#state',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n          {\n            name: 'RegionNotSupported',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyAgeassuranceDefs: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.defs',\n    defs: {\n      access: {\n        description:\n          \"The access level granted based on Age Assurance data we've processed.\",\n        type: 'string',\n        knownValues: ['unknown', 'none', 'safe', 'full'],\n      },\n      status: {\n        type: 'string',\n        description: 'The status of the Age Assurance process.',\n        knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n      },\n      state: {\n        type: 'object',\n        description: \"The user's computed Age Assurance state.\",\n        required: ['status', 'access'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#status',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      stateMetadata: {\n        type: 'object',\n        description:\n          'Additional metadata needed to compute Age Assurance state client-side.',\n        required: [],\n        properties: {\n          accountCreatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The account creation timestamp.',\n          },\n        },\n      },\n      config: {\n        type: 'object',\n        description: '',\n        required: ['regions'],\n        properties: {\n          regions: {\n            type: 'array',\n            description: 'The per-region Age Assurance configuration.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.ageassurance.defs#configRegion',\n            },\n          },\n        },\n      },\n      configRegion: {\n        type: 'object',\n        description: 'The Age Assurance configuration for a specific region.',\n        required: ['countryCode', 'minAccessAge', 'rules'],\n        properties: {\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code this configuration applies to.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.',\n          },\n          minAccessAge: {\n            type: 'integer',\n            description:\n              'The minimum age (as a whole integer) required to use Bluesky in this region.',\n          },\n          rules: {\n            type: 'array',\n            description:\n              '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.',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.ageassurance.defs#configRegionRuleDefault',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan',\n              ],\n            },\n          },\n        },\n      },\n      configRegionRuleDefault: {\n        type: 'object',\n        description: 'Age Assurance rule that applies by default.',\n        required: ['access'],\n        properties: {\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountNewerThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is equal-to or newer than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountOlderThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is older than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        description: 'Object used to store Age Assurance data in stash.',\n        required: ['createdAt', 'status', 'access', 'attemptId', 'countryCode'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the Age Assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n          access: {\n            description:\n              \"The access level granted based on Age Assurance data we've processed.\",\n            type: 'string',\n            knownValues: ['unknown', 'none', 'safe', 'full'],\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for Age Assurance.',\n          },\n          initIp: {\n            type: 'string',\n            description:\n              'The IP address used when initiating the Age Assurance flow.',\n          },\n          initUa: {\n            type: 'string',\n            description:\n              'The user agent used when initiating the Age Assurance flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description:\n              'The IP address used when completing the Age Assurance flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description:\n              'The user agent used when completing the Age Assurance flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns Age Assurance configuration for use on the client.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#config',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetState: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.',\n        parameters: {\n          type: 'params',\n          required: ['countryCode'],\n          properties: {\n            countryCode: {\n              type: 'string',\n            },\n            regionCode: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['state', 'metadata'],\n            properties: {\n              state: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#state',\n              },\n              metadata: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#stateMetadata',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkCreateBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.createBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkDefs: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.defs',\n    defs: {\n      bookmark: {\n        description: 'Object used to store bookmark data in stash.',\n        type: 'object',\n        required: ['subject'],\n        properties: {\n          subject: {\n            description:\n              'A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      bookmarkView: {\n        type: 'object',\n        required: ['subject', 'item'],\n        properties: {\n          subject: {\n            description: 'A strong ref to the bookmarked record.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          item: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#blockedPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#postView',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkDeleteBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.deleteBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkGetBookmarks: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.getBookmarks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Gets views of records bookmarked by the authenticated user. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['bookmarks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              bookmarks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.bookmark.defs#bookmarkView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDefs: {\n    lexicon: 1,\n    id: 'app.bsky.contact.defs',\n    defs: {\n      matchAndContactIndex: {\n        description:\n          '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.',\n        type: 'object',\n        required: ['match', 'contactIndex'],\n        properties: {\n          match: {\n            description: 'Profile of the matched user.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          contactIndex: {\n            description: 'The index of this match in the import contact input.',\n            type: 'integer',\n            minimum: 0,\n            maximum: 999,\n          },\n        },\n      },\n      syncStatus: {\n        type: 'object',\n        required: ['syncedAt', 'matchesCount'],\n        properties: {\n          syncedAt: {\n            description: 'Last date when contacts where imported.',\n            type: 'string',\n            format: 'datetime',\n          },\n          matchesCount: {\n            description:\n              '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.',\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n      notification: {\n        description:\n          'A stash object to be sent via bsync representing a notification to be created.',\n        type: 'object',\n        required: ['from', 'to'],\n        properties: {\n          from: {\n            description: 'The DID of who this notification comes from.',\n            type: 'string',\n            format: 'did',\n          },\n          to: {\n            description: 'The DID of who this notification should go to.',\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDismissMatch: {\n    lexicon: 1,\n    id: 'app.bsky.contact.dismissMatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                description: \"The subject's DID to dismiss the match with.\",\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetMatches: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getMatches',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matches'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              matches: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidLimit',\n          },\n          {\n            name: 'InvalidCursor',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetSyncStatus: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getSyncStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets the user's current contact import status. Requires authentication.\",\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              syncStatus: {\n                description:\n                  \"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.\",\n                type: 'ref',\n                ref: 'lex:app.bsky.contact.defs#syncStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactImportContacts: {\n    lexicon: 1,\n    id: 'app.bsky.contact.importContacts',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'contacts'],\n            properties: {\n              token: {\n                description:\n                  'JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.',\n                type: 'string',\n              },\n              contacts: {\n                description:\n                  \"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`.\",\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                minLength: 1,\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matchesAndContactIndexes'],\n            properties: {\n              matchesAndContactIndexes: {\n                description:\n                  'The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.',\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.contact.defs#matchAndContactIndex',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidContacts',\n          },\n          {\n            name: 'TooManyContacts',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactRemoveData: {\n    lexicon: 1,\n    id: 'app.bsky.contact.removeData',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactSendNotification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.sendNotification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'System endpoint to send notifications related to contact imports. Requires role authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['from', 'to'],\n            properties: {\n              from: {\n                description: 'The DID of who this notification comes from.',\n                type: 'string',\n                format: 'did',\n              },\n              to: {\n                description: 'The DID of who this notification should go to.',\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactStartPhoneVerification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.startPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          '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.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone'],\n            properties: {\n              phone: {\n                description: 'The phone number to receive the code via SMS.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactVerifyPhone: {\n    lexicon: 1,\n    id: 'app.bsky.contact.verifyPhone',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone', 'code'],\n            properties: {\n              phone: {\n                description:\n                  'The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n              code: {\n                description:\n                  'The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                description:\n                  'JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InvalidCode',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftCreateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.createDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draft',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'The ID of the created draft.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DraftLimitReached',\n            description:\n              'Trying to insert a new draft when the limit was already reached.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftDefs: {\n    lexicon: 1,\n    id: 'app.bsky.draft.defs',\n    defs: {\n      draftWithId: {\n        description:\n          'A draft with an identifier, used to store drafts in private storage (stash).',\n        type: 'object',\n        required: ['id', 'draft'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n        },\n      },\n      draft: {\n        description: 'A draft containing an array of draft posts.',\n        type: 'object',\n        required: ['posts'],\n        properties: {\n          deviceId: {\n            type: 'string',\n            description:\n              'UUIDv4 identifier of the device that created this draft.',\n            maxLength: 100,\n          },\n          deviceName: {\n            type: 'string',\n            description:\n              'The device and/or platform on which the draft was created.',\n            maxLength: 100,\n          },\n          posts: {\n            description: 'Array of draft posts that compose this draft.',\n            type: 'array',\n            minLength: 1,\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftPost',\n            },\n          },\n          langs: {\n            type: 'array',\n            description:\n              'Indicates human language of posts primary text content.',\n            maxLength: 3,\n            items: {\n              type: 'string',\n              format: 'language',\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Embedding rules for the postgates to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n          threadgateAllow: {\n            description:\n              'Allow-rules for the threadgate to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n        },\n      },\n      draftPost: {\n        description: 'One of the posts that compose a draft.',\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n            description:\n              '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.',\n          },\n          labels: {\n            type: 'union',\n            description:\n              'Self-label values for this post. Effectively content warnings.',\n            refs: ['lex:com.atproto.label.defs#selfLabels'],\n          },\n          embedImages: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedImage',\n            },\n            maxLength: 4,\n          },\n          embedVideos: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedVideo',\n            },\n            maxLength: 1,\n          },\n          embedExternals: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedExternal',\n            },\n            maxLength: 1,\n          },\n          embedRecords: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedRecord',\n            },\n            maxLength: 1,\n          },\n        },\n      },\n      draftView: {\n        description: 'View to present drafts data to users.',\n        type: 'object',\n        required: ['id', 'draft', 'createdAt', 'updatedAt'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n          createdAt: {\n            description: 'The time the draft was created.',\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            description: 'The time the draft was last updated.',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      draftEmbedLocalRef: {\n        type: 'object',\n        required: ['path'],\n        properties: {\n          path: {\n            type: 'string',\n            description:\n              'Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts.',\n            minLength: 1,\n            maxLength: 1024,\n          },\n        },\n      },\n      draftEmbedCaption: {\n        type: 'object',\n        required: ['lang', 'content'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          content: {\n            type: 'string',\n            maxLength: 10000,\n          },\n        },\n      },\n      draftEmbedImage: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n        },\n      },\n      draftEmbedVideo: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedCaption',\n            },\n            maxLength: 20,\n          },\n        },\n      },\n      draftEmbedExternal: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      draftEmbedRecord: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftDeleteDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.deleteDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Deletes a draft by ID. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftGetDrafts: {\n    lexicon: 1,\n    id: 'app.bsky.draft.getDrafts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets views of user drafts. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['drafts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              drafts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.draft.defs#draftView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftUpdateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.updateDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"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.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draftWithId',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.embed.defs',\n    defs: {\n      aspectRatio: {\n        type: 'object',\n        description:\n          'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n            minimum: 1,\n          },\n          height: {\n            type: 'integer',\n            minimum: 1,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedExternal: {\n    lexicon: 1,\n    id: 'app.bsky.embed.external',\n    defs: {\n      main: {\n        type: 'object',\n        description:\n          \"A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).\",\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#external',\n          },\n        },\n      },\n      external: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#viewExternal',\n          },\n        },\n      },\n      viewExternal: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedImages: {\n    lexicon: 1,\n    id: 'app.bsky.embed.images',\n    description: 'A set of images embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#image',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      image: {\n        type: 'object',\n        required: ['image', 'alt'],\n        properties: {\n          image: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#viewImage',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      viewImage: {\n        type: 'object',\n        required: ['thumb', 'fullsize', 'alt'],\n        properties: {\n          thumb: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.',\n          },\n          fullsize: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.',\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecord: {\n    lexicon: 1,\n    id: 'app.bsky.embed.record',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.record#viewRecord',\n              'lex:app.bsky.embed.record#viewNotFound',\n              'lex:app.bsky.embed.record#viewBlocked',\n              'lex:app.bsky.embed.record#viewDetached',\n              'lex:app.bsky.feed.defs#generatorView',\n              'lex:app.bsky.graph.defs#listView',\n              'lex:app.bsky.labeler.defs#labelerView',\n              'lex:app.bsky.graph.defs#starterPackViewBasic',\n            ],\n          },\n        },\n      },\n      viewRecord: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'value', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          value: {\n            type: 'unknown',\n            description: 'The record data itself.',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          embeds: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images#view',\n                'lex:app.bsky.embed.video#view',\n                'lex:app.bsky.embed.external#view',\n                'lex:app.bsky.embed.record#view',\n                'lex:app.bsky.embed.recordWithMedia#view',\n              ],\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      viewNotFound: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      viewBlocked: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      viewDetached: {\n        type: 'object',\n        required: ['uri', 'detached'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          detached: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecordWithMedia: {\n    lexicon: 1,\n    id: 'app.bsky.embed.recordWithMedia',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images',\n              'lex:app.bsky.embed.video',\n              'lex:app.bsky.embed.external',\n            ],\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record#view',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedVideo: {\n    lexicon: 1,\n    id: 'app.bsky.embed.video',\n    description: 'A video embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['video'],\n        properties: {\n          video: {\n            type: 'blob',\n            description:\n              'The mp4 video file. May be up to 100mb, formerly limited to 50mb.',\n            accept: ['video/mp4'],\n            maxSize: 100000000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.video#caption',\n            },\n            maxLength: 20,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the video, for accessibility.',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n      caption: {\n        type: 'object',\n        required: ['lang', 'file'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          file: {\n            type: 'blob',\n            accept: ['text/vtt'],\n            maxSize: 20000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['cid', 'playlist'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          playlist: {\n            type: 'string',\n            format: 'uri',\n          },\n          thumbnail: {\n            type: 'string',\n            format: 'uri',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.feed.defs',\n    defs: {\n      postView: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'record', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n              'lex:app.bsky.embed.record#view',\n              'lex:app.bsky.embed.recordWithMedia#view',\n            ],\n          },\n          bookmarkCount: {\n            type: 'integer',\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          threadgate: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadgateView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.\",\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          bookmarked: {\n            type: 'boolean',\n          },\n          threadMuted: {\n            type: 'boolean',\n          },\n          replyDisabled: {\n            type: 'boolean',\n          },\n          embeddingDisabled: {\n            type: 'boolean',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      threadContext: {\n        type: 'object',\n        description:\n          'Metadata about this post within the context of the thread it is in.',\n        properties: {\n          rootAuthorLike: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      feedViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#replyRef',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#reasonRepost',\n              'lex:app.bsky.feed.defs#reasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context provided by feed generator that may be passed back alongside interactions.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          grandparentAuthor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            description:\n              'When parent is a reply to another post, this is the author of that post.',\n          },\n        },\n      },\n      reasonRepost: {\n        type: 'object',\n        required: ['by', 'indexedAt'],\n        properties: {\n          by: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#threadViewPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          replies: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.defs#threadViewPost',\n                'lex:app.bsky.feed.defs#notFoundPost',\n                'lex:app.bsky.feed.defs#blockedPost',\n              ],\n            },\n          },\n          threadContext: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadContext',\n          },\n        },\n      },\n      notFoundPost: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      blockedPost: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      blockedAuthor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n        },\n      },\n      generatorView: {\n        type: 'object',\n        required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          acceptsInteractions: {\n            type: 'boolean',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#generatorViewerState',\n          },\n          contentMode: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#contentModeUnspecified',\n              'app.bsky.feed.defs#contentModeVideo',\n            ],\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      generatorViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonFeedPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#skeletonReasonRepost',\n              'lex:app.bsky.feed.defs#skeletonReasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context that will be passed through to client and may be passed to feed generator back alongside interactions.',\n            maxLength: 2000,\n          },\n        },\n      },\n      skeletonReasonRepost: {\n        type: 'object',\n        required: ['repost'],\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonReasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadgateView: {\n        type: 'object',\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          lists: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listViewBasic',\n            },\n          },\n        },\n      },\n      interaction: {\n        type: 'object',\n        properties: {\n          item: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          event: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#requestLess',\n              'app.bsky.feed.defs#requestMore',\n              'app.bsky.feed.defs#clickthroughItem',\n              'app.bsky.feed.defs#clickthroughAuthor',\n              'app.bsky.feed.defs#clickthroughReposter',\n              'app.bsky.feed.defs#clickthroughEmbed',\n              'app.bsky.feed.defs#interactionSeen',\n              'app.bsky.feed.defs#interactionLike',\n              'app.bsky.feed.defs#interactionRepost',\n              'app.bsky.feed.defs#interactionReply',\n              'app.bsky.feed.defs#interactionQuote',\n              'app.bsky.feed.defs#interactionShare',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      requestLess: {\n        type: 'token',\n        description:\n          'Request that less content like the given feed item be shown in the feed',\n      },\n      requestMore: {\n        type: 'token',\n        description:\n          'Request that more content like the given feed item be shown in the feed',\n      },\n      clickthroughItem: {\n        type: 'token',\n        description: 'User clicked through to the feed item',\n      },\n      clickthroughAuthor: {\n        type: 'token',\n        description: 'User clicked through to the author of the feed item',\n      },\n      clickthroughReposter: {\n        type: 'token',\n        description: 'User clicked through to the reposter of the feed item',\n      },\n      clickthroughEmbed: {\n        type: 'token',\n        description:\n          'User clicked through to the embedded content of the feed item',\n      },\n      contentModeUnspecified: {\n        type: 'token',\n        description: 'Declares the feed generator returns any types of posts.',\n      },\n      contentModeVideo: {\n        type: 'token',\n        description:\n          'Declares the feed generator returns posts containing app.bsky.embed.video embeds.',\n      },\n      interactionSeen: {\n        type: 'token',\n        description: 'Feed item was seen by user',\n      },\n      interactionLike: {\n        type: 'token',\n        description: 'User liked the feed item',\n      },\n      interactionRepost: {\n        type: 'token',\n        description: 'User reposted the feed item',\n      },\n      interactionReply: {\n        type: 'token',\n        description: 'User replied to the feed item',\n      },\n      interactionQuote: {\n        type: 'token',\n        description: 'User quoted the feed item',\n      },\n      interactionShare: {\n        type: 'token',\n        description: 'User shared the feed item',\n      },\n    },\n  },\n  AppBskyFeedDescribeFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.describeFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'feeds'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.describeFeedGenerator#feed',\n                },\n              },\n              links: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.describeFeedGenerator#links',\n              },\n            },\n          },\n        },\n      },\n      feed: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n          },\n          termsOfService: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.generator',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.',\n        key: 'any',\n        record: {\n          type: 'object',\n          required: ['did', 'displayName', 'createdAt'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            displayName: {\n              type: 'string',\n              maxGraphemes: 24,\n              maxLength: 240,\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            acceptsInteractions: {\n              type: 'boolean',\n              description:\n                'Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions',\n            },\n            labels: {\n              type: 'union',\n              description: 'Self-label values',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            contentMode: {\n              type: 'string',\n              knownValues: [\n                'app.bsky.feed.defs#contentModeUnspecified',\n                'app.bsky.feed.defs#contentModeVideo',\n              ],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a list of feeds (feed generator records) created by the actor (in the actor's repo).\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetAuthorFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getAuthorFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            filter: {\n              type: 'string',\n              description:\n                'Combinations of post/repost types to include in response.',\n              knownValues: [\n                'posts_with_replies',\n                'posts_no_replies',\n                'posts_with_media',\n                'posts_and_author_threads',\n                'posts_with_video',\n              ],\n              default: 'posts_with_replies',\n            },\n            includePins: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a hydrated feed from an actor's selected feed generator. Implemented by App View.\",\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator. Implemented by AppView.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the feed generator record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['view', 'isOnline', 'isValid'],\n            properties: {\n              view: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#generatorView',\n              },\n              isOnline: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service has been online recently, or else seems to be inactive.',\n              },\n              isValid: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service is compatible with the record declaration.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of feed generators.',\n        parameters: {\n          type: 'params',\n          required: ['feeds'],\n          properties: {\n            feeds: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference to feed generator record describing the specific feed being requested.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#skeletonFeedPost',\n                },\n              },\n              reqId: {\n                type: 'string',\n                description:\n                  'Unique identifier per request that may be passed back alongside interactions.',\n                maxLength: 100,\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get like records which reference a subject (by AT-URI and CID).',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the subject (eg, a post record).',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'CID of the subject record (aka, specific version of record), to filter likes.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'likes'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              likes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.getLikes#like',\n                },\n              },\n            },\n          },\n        },\n      },\n      like: {\n        type: 'object',\n        required: ['indexedAt', 'createdAt', 'actor'],\n        properties: {\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          actor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetListFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getListFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownList',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPostThread: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPostThread',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to post record.',\n            },\n            depth: {\n              type: 'integer',\n              description:\n                'How many levels of reply depth should be included in response.',\n              default: 6,\n              minimum: 0,\n              maximum: 1000,\n            },\n            parentHeight: {\n              type: 'integer',\n              description:\n                'How many levels of parent (and grandparent, etc) post to include.',\n              default: 80,\n              minimum: 0,\n              maximum: 1000,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.defs#threadViewPost',\n                  'lex:app.bsky.feed.defs#notFoundPost',\n                  'lex:app.bsky.feed.defs#blockedPost',\n                ],\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'NotFound',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.\",\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              description: 'List of post AT-URIs to return hydrated views for.',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetQuotes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getQuotes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of quotes for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to quotes of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'posts'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetRepostedBy: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getRepostedBy',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of reposts for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to reposts of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'repostedBy'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              repostedBy: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested feeds (feed generators) for the requesting account.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetTimeline: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.\",\n        parameters: {\n          type: 'params',\n          properties: {\n            algorithm: {\n              type: 'string',\n              description:\n                \"Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedLike: {\n    lexicon: 1,\n    id: 'app.bsky.feed.like',\n    defs: {\n      main: {\n        type: 'record',\n        description: \"Record declaring a 'like' of a piece of subject content.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.post',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'Record containing a Bluesky post.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['text', 'createdAt'],\n          properties: {\n            text: {\n              type: 'string',\n              maxLength: 3000,\n              maxGraphemes: 300,\n              description:\n                'The primary post content. May be an empty string, if there are embeds.',\n            },\n            entities: {\n              type: 'array',\n              description: 'DEPRECATED: replaced by app.bsky.richtext.facet.',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.post#entity',\n              },\n            },\n            facets: {\n              type: 'array',\n              description:\n                'Annotations of text (mentions, URLs, hashtags, etc)',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            reply: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.post#replyRef',\n            },\n            embed: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images',\n                'lex:app.bsky.embed.video',\n                'lex:app.bsky.embed.external',\n                'lex:app.bsky.embed.record',\n                'lex:app.bsky.embed.recordWithMedia',\n              ],\n            },\n            langs: {\n              type: 'array',\n              description:\n                'Indicates human language of post primary text content.',\n              maxLength: 3,\n              items: {\n                type: 'string',\n                format: 'language',\n              },\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values for this post. Effectively content warnings.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            tags: {\n              type: 'array',\n              description:\n                'Additional hashtags, in addition to any included in post text and facets.',\n              maxLength: 8,\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Client-declared timestamp when this post was originally created.',\n            },\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          parent: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      entity: {\n        type: 'object',\n        description: 'Deprecated: use facets instead.',\n        required: ['index', 'type', 'value'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.post#textSlice',\n          },\n          type: {\n            type: 'string',\n            description: \"Expected values are 'mention' and 'link'.\",\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n      textSlice: {\n        type: 'object',\n        description:\n          'Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.',\n        required: ['start', 'end'],\n        properties: {\n          start: {\n            type: 'integer',\n            minimum: 0,\n          },\n          end: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPostgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.postgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          'Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.',\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            detachedEmbeddingUris: {\n              type: 'array',\n              maxLength: 50,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description:\n                'List of AT-URIs embedding this post that the author has detached from.',\n            },\n            embeddingRules: {\n              description:\n                'List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: ['lex:app.bsky.feed.postgate#disableRule'],\n              },\n            },\n          },\n        },\n      },\n      disableRule: {\n        type: 'object',\n        description: 'Disables embedding of this post.',\n        properties: {},\n      },\n    },\n  },\n  AppBskyFeedRepost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.repost',\n    defs: {\n      main: {\n        description:\n          \"Record representing a 'repost' of an existing Bluesky post.\",\n        type: 'record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedSearchPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.searchPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedSendInteractions: {\n    lexicon: 1,\n    id: 'app.bsky.feed.sendInteractions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Send information about interactions with feed items back to the feed generator that served them.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['interactions'],\n            properties: {\n              feed: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              interactions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#interaction',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedThreadgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.threadgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          \"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.\",\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            allow: {\n              description:\n                'List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.threadgate#mentionRule',\n                  'lex:app.bsky.feed.threadgate#followerRule',\n                  'lex:app.bsky.feed.threadgate#followingRule',\n                  'lex:app.bsky.feed.threadgate#listRule',\n                ],\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            hiddenReplies: {\n              type: 'array',\n              maxLength: 300,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description: 'List of hidden reply URIs.',\n            },\n          },\n        },\n      },\n      mentionRule: {\n        type: 'object',\n        description: 'Allow replies from actors mentioned in your post.',\n        properties: {},\n      },\n      followerRule: {\n        type: 'object',\n        description: 'Allow replies from actors who follow you.',\n        properties: {},\n      },\n      followingRule: {\n        type: 'object',\n        description: 'Allow replies from actors you follow.',\n        properties: {},\n      },\n      listRule: {\n        type: 'object',\n        description: 'Allow replies from actors on a list.',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphBlock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.block',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'DID of the account to be blocked.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphDefs: {\n    lexicon: 1,\n    id: 'app.bsky.graph.defs',\n    defs: {\n      listViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'name', 'purpose'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'name', 'purpose', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listItemView: {\n        type: 'object',\n        required: ['uri', 'subject'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n      starterPackView: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          listItemsSample: {\n            type: 'array',\n            maxLength: 12,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listItemView',\n            },\n          },\n          feeds: {\n            type: 'array',\n            maxLength: 3,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.defs#generatorView',\n            },\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      starterPackViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listPurpose: {\n        type: 'string',\n        knownValues: [\n          'app.bsky.graph.defs#modlist',\n          'app.bsky.graph.defs#curatelist',\n          'app.bsky.graph.defs#referencelist',\n        ],\n      },\n      modlist: {\n        type: 'token',\n        description:\n          'A list of actors to apply an aggregate moderation action (mute/block) on.',\n      },\n      curatelist: {\n        type: 'token',\n        description:\n          'A list of actors used for curation purposes such as list feeds or interaction gating.',\n      },\n      referencelist: {\n        type: 'token',\n        description:\n          'A list of actors used for only for reference purposes such as within a starter pack.',\n      },\n      listViewerState: {\n        type: 'object',\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          blocked: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      notFoundActor: {\n        type: 'object',\n        description: 'indicates that a handle or DID could not be resolved',\n        required: ['actor', 'notFound'],\n        properties: {\n          actor: {\n            type: 'string',\n            format: 'at-identifier',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      relationship: {\n        type: 'object',\n        description:\n          'lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor follows this DID, this is the AT-URI of the follow record',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is followed by this DID, contains the AT-URI of the follow record',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID, this is the AT-URI of the block record',\n          },\n          blockedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID, contains the AT-URI of the block record',\n          },\n          blockingByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID via a block list, this is the AT-URI of the listblock record',\n          },\n          blockedByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphFollow: {\n    lexicon: 1,\n    id: 'app.bsky.graph.follow',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetActorStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getActorStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of starter packs created by the actor.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates which accounts the requesting account is currently blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blocks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blocks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollows: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollows',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which a specified account (actor) follows.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'follows'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              follows: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetKnownFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getKnownFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor) and are followed by the viewer.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getList',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets a 'view' (with additional context) of a specified list.\",\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the list record to hydrate.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list', 'items'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              list: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#listView',\n              },\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listItemView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get mod lists that the requesting account (actor) is blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetLists: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getLists',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to enumerate lists from.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListsWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListsWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['listsWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              listsWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getListsWithMembership#listWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      listWithMembership: {\n        description:\n          'A list and an optional list item indicating membership of a target user to that list.',\n        type: 'object',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['mutes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              mutes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetRelationships: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getRelationships',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates public relationships between one account, and a list of other accounts. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Primary account requesting relationships for.',\n            },\n            others: {\n              type: 'array',\n              description:\n                \"List of 'other' accounts to be related back to the primary.\",\n              maxLength: 30,\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['relationships'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              relationships: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.graph.defs#relationship',\n                    'lex:app.bsky.graph.defs#notFoundActor',\n                  ],\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ActorNotFound',\n            description:\n              'the primary actor at-identifier could not be resolved',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyGraphGetStarterPack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPack',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets a view of a starter pack.',\n        parameters: {\n          type: 'params',\n          required: ['starterPack'],\n          properties: {\n            starterPack: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the starter pack record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPack'],\n            properties: {\n              starterPack: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#starterPackView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get views for a list of starter packs.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacksWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacksWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacksWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacksWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      starterPackWithMembership: {\n        description:\n          'A starter pack and an optional list item indicating membership of a target user to that starter pack.',\n        type: 'object',\n        required: ['starterPack'],\n        properties: {\n          starterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetSuggestedFollowsByActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getSuggestedFollowsByActor',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n              isFallback: {\n                type: 'boolean',\n                description:\n                  'DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid',\n                default: false,\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.list',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'purpose', 'createdAt'],\n          properties: {\n            purpose: {\n              type: 'ref',\n              description:\n                'Defines the purpose of the list (aka, moderation-oriented or curration-oriented)',\n              ref: 'lex:app.bsky.graph.defs#listPurpose',\n            },\n            name: {\n              type: 'string',\n              maxLength: 64,\n              minLength: 1,\n              description: 'Display name for list; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListblock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listblock',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a block relationship against an entire an entire list of accounts (actors).',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the mod list record.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListitem: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listitem',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'list', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'The account which is included on the list.',\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to the list record (app.bsky.graph.list).',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphSearchStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.searchStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find starter packs matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphStarterpack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.starterpack',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record defining a starter pack of actors and feeds for new users.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'list', 'createdAt'],\n          properties: {\n            name: {\n              type: 'string',\n              maxGraphemes: 50,\n              maxLength: 500,\n              minLength: 1,\n              description: 'Display name for starter pack; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            feeds: {\n              type: 'array',\n              maxLength: 3,\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.starterpack#feedItem',\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      feedItem: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified list of accounts. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified thread. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphVerification: {\n    lexicon: 1,\n    id: 'app.bsky.graph.verification',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'handle', 'displayName', 'createdAt'],\n          properties: {\n            subject: {\n              description: 'DID of the subject the verification applies to.',\n              type: 'string',\n              format: 'did',\n            },\n            handle: {\n              description:\n                'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n              type: 'string',\n              format: 'handle',\n            },\n            displayName: {\n              description:\n                'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n              type: 'string',\n            },\n            createdAt: {\n              description: 'Date of when the verification was created.',\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerDefs: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.defs',\n    defs: {\n      labelerView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      labelerViewDetailed: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'policies', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          policies: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          reasonTypes: {\n            description:\n              \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#reasonType',\n            },\n          },\n          subjectTypes: {\n            description:\n              'The set of subject types (account, record, etc) this service accepts reports on.',\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#subjectType',\n            },\n          },\n          subjectCollections: {\n            type: 'array',\n            description:\n              'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n            items: {\n              type: 'string',\n              format: 'nsid',\n            },\n          },\n        },\n      },\n      labelerViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      labelerPolicies: {\n        type: 'object',\n        required: ['labelValues'],\n        properties: {\n          labelValues: {\n            type: 'array',\n            description:\n              'The label values which this labeler publishes. May include global or custom labels.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValue',\n            },\n          },\n          labelValueDefinitions: {\n            type: 'array',\n            description:\n              'Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinition',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerGetServices: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.getServices',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of labeler services.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            detailed: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['views'],\n            properties: {\n              views: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.labeler.defs#labelerView',\n                    'lex:app.bsky.labeler.defs#labelerViewDetailed',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerService: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.service',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of the existence of labeler service.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['policies', 'createdAt'],\n          properties: {\n            policies: {\n              type: 'ref',\n              ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            reasonTypes: {\n              description:\n                \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n            },\n            subjectTypes: {\n              description:\n                'The set of subject types (account, record, etc) this service accepts reports on.',\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#subjectType',\n              },\n            },\n            subjectCollections: {\n              type: 'array',\n              description:\n                'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDeclaration: {\n    lexicon: 1,\n    id: 'app.bsky.notification.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"A declaration of the user's choices related to notifications that can be produced by them.\",\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowSubscriptions'],\n          properties: {\n            allowSubscriptions: {\n              type: 'string',\n              description:\n                \"A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.\",\n              knownValues: ['followers', 'mutuals', 'none'],\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDefs: {\n    lexicon: 1,\n    id: 'app.bsky.notification.defs',\n    defs: {\n      recordDeleted: {\n        type: 'object',\n        properties: {},\n      },\n      chatPreference: {\n        type: 'object',\n        required: ['include', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'accepted'],\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      filterablePreference: {\n        type: 'object',\n        required: ['include', 'list', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'follows'],\n          },\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preference: {\n        type: 'object',\n        required: ['list', 'push'],\n        properties: {\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preferences: {\n        type: 'object',\n        required: [\n          'chat',\n          'follow',\n          'like',\n          'likeViaRepost',\n          'mention',\n          'quote',\n          'reply',\n          'repost',\n          'repostViaRepost',\n          'starterpackJoined',\n          'subscribedPost',\n          'unverified',\n          'verified',\n        ],\n        properties: {\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#chatPreference',\n          },\n          follow: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          like: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          likeViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          mention: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          quote: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repostViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          starterpackJoined: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          subscribedPost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          unverified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          verified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n        },\n      },\n      activitySubscription: {\n        type: 'object',\n        required: ['post', 'reply'],\n        properties: {\n          post: {\n            type: 'boolean',\n          },\n          reply: {\n            type: 'boolean',\n          },\n        },\n      },\n      subjectActivitySubscription: {\n        description:\n          'Object used to store activity subscription data in stash.',\n        type: 'object',\n        required: ['subject', 'activitySubscription'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get notification-related preferences for an account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetUnreadCount: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getUnreadCount',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Count the number of unread notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            priority: {\n              type: 'boolean',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['count'],\n            properties: {\n              count: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListActivitySubscriptions: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listActivitySubscriptions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subscriptions'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subscriptions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListNotifications: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listNotifications',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            reasons: {\n              description: 'Notification reasons to include in response.',\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'A reason that matches the reason property of #notification.',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            priority: {\n              type: 'boolean',\n            },\n            cursor: {\n              type: 'string',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['notifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              notifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.notification.listNotifications#notification',\n                },\n              },\n              priority: {\n                type: 'boolean',\n              },\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      notification: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'author',\n          'reason',\n          'record',\n          'isRead',\n          'indexedAt',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          reason: {\n            type: 'string',\n            description:\n              'The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.',\n            knownValues: [\n              'like',\n              'repost',\n              'follow',\n              'mention',\n              'reply',\n              'quote',\n              'starterpack-joined',\n              'verified',\n              'unverified',\n              'like-via-repost',\n              'repost-via-repost',\n              'subscribed-post',\n              'contact-match',\n            ],\n          },\n          reasonSubject: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          record: {\n            type: 'unknown',\n          },\n          isRead: {\n            type: 'boolean',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutActivitySubscription: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putActivitySubscription',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'activitySubscription'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['priority'],\n            properties: {\n              priority: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferencesV2: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferencesV2',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              chat: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#chatPreference',\n              },\n              follow: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              like: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              likeViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              mention: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              quote: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              reply: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repostViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              starterpackJoined: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              subscribedPost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              unverified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              verified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationRegisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.registerPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Register to receive push notifications, via a specified service, for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n              ageRestricted: {\n                type: 'boolean',\n                description: 'Set to true when the actor is age restricted',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUnregisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.unregisterPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUpdateSeen: {\n    lexicon: 1,\n    id: 'app.bsky.notification.updateSeen',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify server that the requesting account has seen notifications. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['seenAt'],\n            properties: {\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyRichtextFacet: {\n    lexicon: 1,\n    id: 'app.bsky.richtext.facet',\n    defs: {\n      main: {\n        type: 'object',\n        description: 'Annotation of a sub-string within rich text.',\n        required: ['index', 'features'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.richtext.facet#byteSlice',\n          },\n          features: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.richtext.facet#mention',\n                'lex:app.bsky.richtext.facet#link',\n                'lex:app.bsky.richtext.facet#tag',\n              ],\n            },\n          },\n        },\n      },\n      mention: {\n        type: 'object',\n        description:\n          \"Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.\",\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      link: {\n        type: 'object',\n        description:\n          'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      tag: {\n        type: 'object',\n        description:\n          \"Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').\",\n        required: ['tag'],\n        properties: {\n          tag: {\n            type: 'string',\n            maxLength: 640,\n            maxGraphemes: 64,\n          },\n        },\n      },\n      byteSlice: {\n        type: 'object',\n        description:\n          'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.',\n        required: ['byteStart', 'byteEnd'],\n        properties: {\n          byteStart: {\n            type: 'integer',\n            minimum: 0,\n          },\n          byteEnd: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.defs',\n    defs: {\n      skeletonSearchPost: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonSearchActor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      skeletonSearchStarterPack: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      trendingTopic: {\n        type: 'object',\n        required: ['topic', 'link'],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n        },\n      },\n      skeletonTrend: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'dids',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          dids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n      },\n      trendView: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'actors',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          actors: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      threadItemPost: {\n        type: 'object',\n        required: [\n          'post',\n          'moreParents',\n          'moreReplies',\n          'opThread',\n          'hiddenByThreadgate',\n          'mutedByViewer',\n        ],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          moreParents: {\n            type: 'boolean',\n            description:\n              'This post has more parents that were not present in the response. This is just a boolean, without the number of parents.',\n          },\n          moreReplies: {\n            type: 'integer',\n            description:\n              'This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate.',\n          },\n          opThread: {\n            type: 'boolean',\n            description:\n              'This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread.',\n          },\n          hiddenByThreadgate: {\n            type: 'boolean',\n            description:\n              'The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.',\n          },\n          mutedByViewer: {\n            type: 'boolean',\n            description:\n              'This is by an account muted by the viewer requesting it.',\n          },\n        },\n      },\n      threadItemNoUnauthenticated: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemNotFound: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemBlocked: {\n        type: 'object',\n        required: ['author'],\n        properties: {\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      ageAssuranceState: {\n        type: 'object',\n        description:\n          'The computed state of the age assurance process, returned to the user in question on certain authenticated requests.',\n        required: ['status'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description: 'Object used to store age assurance data in stash.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for AA.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetAgeAssuranceState: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getAgeAssuranceState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get miscellaneous runtime configuration.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              checkEmailConfirmed: {\n                type: 'boolean',\n              },\n              liveNow: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getConfig#liveNowConfig',\n                },\n              },\n            },\n          },\n        },\n      },\n      liveNowConfig: {\n        type: 'object',\n        required: ['did', 'domains'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          domains: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedOnboardingUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPopularFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPopularFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'An unspecced view of globally popular feed generators.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            query: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadOtherV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadOtherV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadOtherV2#threadItem',\n                },\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: ['lex:app.bsky.unspecced.defs#threadItemPost'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post.',\n            },\n            above: {\n              type: 'boolean',\n              description: 'Whether to include parents above the anchor.',\n              default: true,\n            },\n            below: {\n              type: 'integer',\n              description:\n                'How many levels of replies to include below the anchor.',\n              default: 6,\n              minimum: 0,\n              maximum: 20,\n            },\n            branchingFactor: {\n              type: 'integer',\n              description:\n                'Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).',\n              default: 10,\n              minimum: 0,\n              maximum: 100,\n            },\n            sort: {\n              type: 'string',\n              description: 'Sorting for the thread replies.',\n              knownValues: ['newest', 'oldest', 'top'],\n              default: 'oldest',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread', 'hasOtherReplies'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadV2#threadItem',\n                },\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n              hasOtherReplies: {\n                type: 'boolean',\n                description:\n                  'Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them.',\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.unspecced.defs#threadItemPost',\n              'lex:app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n              'lex:app.bsky.unspecced.defs#threadItemNotFound',\n              'lex:app.bsky.unspecced.defs#threadItemBlocked',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested feeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedOnboardingUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedOnboardingUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestionsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestionsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            relativeToDid: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n              relativeToDid: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.',\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTaggedSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTaggedSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggestions (feeds and users) tagged with categories',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',\n                },\n              },\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['tag', 'subjectType', 'subject'],\n        properties: {\n          tag: {\n            type: 'string',\n          },\n          subjectType: {\n            type: 'string',\n            knownValues: ['actor', 'feed'],\n          },\n          subject: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendingTopics: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendingTopics',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of trending topics',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['topics', 'suggested'],\n            properties: {\n              topics: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n              suggested: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrends: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrends',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get the current trends on the network',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonTrend',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedInitAgeAssurance: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.initAgeAssurance',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchActorsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchActorsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Actors (profile) search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            typeahead: {\n              type: 'boolean',\n              description: \"If true, acts as fast/simple 'typeahead' query.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchPostsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchPostsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Posts search, returns only skeleton',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                \"DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Starter Pack search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchStarterPack',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyVideoDefs: {\n    lexicon: 1,\n    id: 'app.bsky.video.defs',\n    defs: {\n      jobStatus: {\n        type: 'object',\n        required: ['jobId', 'did', 'state'],\n        properties: {\n          jobId: {\n            type: 'string',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          state: {\n            type: 'string',\n            description:\n              'The state of the video processing job. All values not listed as a known value indicate that the job is in process.',\n            knownValues: ['JOB_STATE_COMPLETED', 'JOB_STATE_FAILED'],\n          },\n          progress: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n            description: 'Progress within the current processing state.',\n          },\n          blob: {\n            type: 'blob',\n          },\n          error: {\n            type: 'string',\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetJobStatus: {\n    lexicon: 1,\n    id: 'app.bsky.video.getJobStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get status details for a video processing job.',\n        parameters: {\n          type: 'params',\n          required: ['jobId'],\n          properties: {\n            jobId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetUploadLimits: {\n    lexicon: 1,\n    id: 'app.bsky.video.getUploadLimits',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get video upload limits for the authenticated user.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canUpload'],\n            properties: {\n              canUpload: {\n                type: 'boolean',\n              },\n              remainingDailyVideos: {\n                type: 'integer',\n              },\n              remainingDailyBytes: {\n                type: 'integer',\n              },\n              message: {\n                type: 'string',\n              },\n              error: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoUploadVideo: {\n    lexicon: 1,\n    id: 'app.bsky.video.uploadVideo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Upload a video to be processed then stored on the PDS.',\n        input: {\n          encoding: 'video/mp4',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeclaration: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky chat account.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowIncoming'],\n          properties: {\n            allowIncoming: {\n              type: 'string',\n              knownValues: ['all', 'none', 'following'],\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          chatDisabled: {\n            type: 'boolean',\n            description:\n              'Set to true when the actor cannot actively participate in conversations',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeleteAccount: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorExportAccountData: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.exportAccountData',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/jsonl',\n        },\n      },\n    },\n  },\n  ChatBskyConvoAcceptConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.acceptConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rev: {\n                description:\n                  'Rev when the convo was accepted. If not present, the convo was already accepted.',\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoAddReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.addReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionLimitReached',\n            description:\n              \"Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent.\",\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.defs',\n    defs: {\n      messageRef: {\n        type: 'object',\n        required: ['did', 'messageId', 'convoId'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          convoId: {\n            type: 'string',\n          },\n          messageId: {\n            type: 'string',\n          },\n        },\n      },\n      messageInput: {\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record'],\n          },\n        },\n      },\n      messageView: {\n        type: 'object',\n        required: ['id', 'rev', 'text', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record#view'],\n          },\n          reactions: {\n            type: 'array',\n            description:\n              'Reactions to this message, in ascending order of creation time.',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.convo.defs#reactionView',\n            },\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      deletedMessageView: {\n        type: 'object',\n        required: ['id', 'rev', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      messageViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      reactionView: {\n        type: 'object',\n        required: ['value', 'sender', 'createdAt'],\n        properties: {\n          value: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionViewSender',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reactionViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      messageAndReactionView: {\n        type: 'object',\n        required: ['message', 'reaction'],\n        properties: {\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      convoView: {\n        type: 'object',\n        required: ['id', 'rev', 'members', 'muted', 'unreadCount'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          members: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.actor.defs#profileViewBasic',\n            },\n          },\n          lastMessage: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          lastReaction: {\n            type: 'union',\n            refs: ['lex:chat.bsky.convo.defs#messageAndReactionView'],\n          },\n          muted: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['request', 'accepted'],\n          },\n          unreadCount: {\n            type: 'integer',\n          },\n        },\n      },\n      logBeginConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logAcceptConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logLeaveConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logMuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logUnmuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logCreateMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logDeleteMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logReadMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logAddReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      logRemoveReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoDeleteMessageForSelf: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.deleteMessageForSelf',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#deletedMessageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvo',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoAvailability: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canChat'],\n            properties: {\n              canChat: {\n                type: 'boolean',\n              },\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoForMembers: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoForMembers',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetLog: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getLog',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: [],\n          properties: {\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['logs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              logs: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#logBeginConvo',\n                    'lex:chat.bsky.convo.defs#logAcceptConvo',\n                    'lex:chat.bsky.convo.defs#logLeaveConvo',\n                    'lex:chat.bsky.convo.defs#logMuteConvo',\n                    'lex:chat.bsky.convo.defs#logUnmuteConvo',\n                    'lex:chat.bsky.convo.defs#logCreateMessage',\n                    'lex:chat.bsky.convo.defs#logDeleteMessage',\n                    'lex:chat.bsky.convo.defs#logReadMessage',\n                    'lex:chat.bsky.convo.defs#logAddReaction',\n                    'lex:chat.bsky.convo.defs#logRemoveReaction',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetMessages: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getMessages',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoLeaveConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.leaveConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'rev'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              rev: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoListConvos: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.listConvos',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            readState: {\n              type: 'string',\n              knownValues: ['unread'],\n            },\n            status: {\n              type: 'string',\n              knownValues: ['request', 'accepted'],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              convos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#convoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoMuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.muteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoRemoveReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.removeReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoSendMessage: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessage',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'message'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageInput',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoSendMessageBatch: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessageBatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.sendMessageBatch#batchItem',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#messageView',\n                },\n              },\n            },\n          },\n        },\n      },\n      batchItem: {\n        type: 'object',\n        required: ['convoId', 'message'],\n        properties: {\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageInput',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUnmuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.unmuteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateAllRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateAllRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              status: {\n                type: 'string',\n                knownValues: ['request', 'accepted'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['updatedCount'],\n            properties: {\n              updatedCount: {\n                description: 'The count of updated convos.',\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetActorMetadata: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getActorMetadata',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['day', 'month', 'all'],\n            properties: {\n              day: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              month: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              all: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n            },\n          },\n        },\n      },\n      metadata: {\n        type: 'object',\n        required: [\n          'messagesSent',\n          'messagesReceived',\n          'convos',\n          'convosStarted',\n        ],\n        properties: {\n          messagesSent: {\n            type: 'integer',\n          },\n          messagesReceived: {\n            type: 'integer',\n          },\n          convos: {\n            type: 'integer',\n          },\n          convosStarted: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetMessageContext: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getMessageContext',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['messageId'],\n          properties: {\n            convoId: {\n              type: 'string',\n              description:\n                'Conversation that the message is from. NOTE: this field will eventually be required.',\n            },\n            messageId: {\n              type: 'string',\n            },\n            before: {\n              type: 'integer',\n              default: 5,\n            },\n            after: {\n              type: 'integer',\n              default: 5,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationUpdateActorAccess: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.updateActorAccess',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor', 'allowAccess'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              allowAccess: {\n                type: 'boolean',\n              },\n              ref: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDefs: {\n    lexicon: 1,\n    id: 'com.atproto.admin.defs',\n    defs: {\n      statusAttr: {\n        type: 'object',\n        required: ['applied'],\n        properties: {\n          applied: {\n            type: 'boolean',\n          },\n          ref: {\n            type: 'string',\n          },\n        },\n      },\n      accountView: {\n        type: 'object',\n        required: ['did', 'handle', 'indexedAt'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoRef: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      repoBlobRef: {\n        type: 'object',\n        required: ['did', 'cid'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      threatSignature: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.admin.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable an account from receiving new invite codes, but does not invalidate existing codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for disabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable some set of codes and/or all codes associated with a set of users.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminEnableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.enableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Re-enable an account's ability to receive invite codes.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for enabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfo: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about an account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfos: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['infos'],\n            properties: {\n              infos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get an admin view of invite codes.',\n        parameters: {\n          type: 'params',\n          properties: {\n            sort: {\n              type: 'string',\n              knownValues: ['recent', 'usage'],\n              default: 'recent',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 500,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getSubjectStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the service-specific admin status of a subject (account, record, or blob).',\n        parameters: {\n          type: 'params',\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            blob: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSearchAccounts: {\n    lexicon: 1,\n    id: 'com.atproto.admin.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of accounts that matches your search query.',\n        parameters: {\n          type: 'params',\n          properties: {\n            email: {\n              type: 'string',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSendEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.sendEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Send email to a user's account email address.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['recipientDid', 'content', 'senderDid'],\n            properties: {\n              recipientDid: {\n                type: 'string',\n                format: 'did',\n              },\n              content: {\n                type: 'string',\n              },\n              subject: {\n                type: 'string',\n              },\n              senderDid: {\n                type: 'string',\n                format: 'did',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  \"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sent'],\n            properties: {\n              sent: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account', 'email'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n                description: 'The handle or DID of the repo.',\n              },\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountHandle: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's handle.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'handle'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountPassword: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the password for a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Administrative action to update an account's signing key in their Did document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'signingKey'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              signingKey: {\n                type: 'string',\n                format: 'did',\n                description: 'Did-key formatted public key',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateSubjectStatus',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the service-specific admin status of a subject (account, record, or blob).',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityDefs: {\n    lexicon: 1,\n    id: 'com.atproto.identity.defs',\n    defs: {\n      identityInfo: {\n        type: 'object',\n        required: ['did', 'handle', 'didDoc'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.\",\n          },\n          didDoc: {\n            type: 'unknown',\n            description: 'The complete DID document for the identity.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityGetRecommendedDidCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.identity.getRecommendedDidCredentials',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Describe the credentials that should be included in the DID doc of an account that is migrating to this service.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rotationKeys: {\n                description:\n                  'Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.',\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityRefreshIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.refreshIdentity',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier'],\n            properties: {\n              identifier: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityRequestPlcOperationSignature: {\n    lexicon: 1,\n    id: 'com.atproto.identity.requestPlcOperationSignature',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to in order to request a signed PLC operation. Requires Auth.',\n      },\n    },\n  },\n  ComAtprotoIdentityResolveDid: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveDid',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves DID to DID document. Does not bi-directionally verify handle.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['didDoc'],\n            properties: {\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for the identity.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveHandle',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description: 'The handle to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveIdentity',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).',\n        parameters: {\n          type: 'params',\n          required: ['identifier'],\n          properties: {\n            identifier: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentitySignPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.signPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Signs a PLC operation to update some value(s) in the requesting DID's document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              token: {\n                description:\n                  'A token received through com.atproto.identity.requestPlcOperationSignature',\n                type: 'string',\n              },\n              rotationKeys: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n                description: 'A signed DID PLC operation.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentitySubmitPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.submitPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityUpdateHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.updateHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'The new handle.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelDefs: {\n    lexicon: 1,\n    id: 'com.atproto.label.defs',\n    defs: {\n      label: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto resource (eg, repo or record).',\n        required: ['src', 'uri', 'val', 'cts'],\n        properties: {\n          ver: {\n            type: 'integer',\n            description: 'The AT Protocol version of the label object.',\n          },\n          src: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the actor who created this label.',\n          },\n          uri: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'AT URI of the record, repository (account), or other resource that this label applies to.',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n            description:\n              \"Optionally, CID specifying the specific version of 'uri' resource this label applies to.\",\n          },\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n          neg: {\n            type: 'boolean',\n            description:\n              'If true, this is a negation label, overwriting a previous label.',\n          },\n          cts: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when this label was created.',\n          },\n          exp: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp at which this label expires (no longer applies).',\n          },\n          sig: {\n            type: 'bytes',\n            description: 'Signature of dag-cbor encoded label.',\n          },\n        },\n      },\n      selfLabels: {\n        type: 'object',\n        description:\n          'Metadata tags on an atproto record, published by the author within the record.',\n        required: ['values'],\n        properties: {\n          values: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#selfLabel',\n            },\n            maxLength: 10,\n          },\n        },\n      },\n      selfLabel: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',\n        required: ['val'],\n        properties: {\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n        },\n      },\n      labelValueDefinition: {\n        type: 'object',\n        description:\n          'Declares a label value and its expected interpretations and behaviors.',\n        required: ['identifier', 'severity', 'blurs', 'locales'],\n        properties: {\n          identifier: {\n            type: 'string',\n            description:\n              \"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).\",\n            maxLength: 100,\n            maxGraphemes: 100,\n          },\n          severity: {\n            type: 'string',\n            description:\n              \"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.\",\n            knownValues: ['inform', 'alert', 'none'],\n          },\n          blurs: {\n            type: 'string',\n            description:\n              \"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.\",\n            knownValues: ['content', 'media', 'none'],\n          },\n          defaultSetting: {\n            type: 'string',\n            description: 'The default setting for this label.',\n            knownValues: ['ignore', 'warn', 'hide'],\n            default: 'warn',\n          },\n          adultOnly: {\n            type: 'boolean',\n            description:\n              'Does the user need to have adult content enabled in order to configure this label?',\n          },\n          locales: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',\n            },\n          },\n        },\n      },\n      labelValueDefinitionStrings: {\n        type: 'object',\n        description:\n          'Strings which describe the label in the UI, localized into a specific language.',\n        required: ['lang', 'name', 'description'],\n        properties: {\n          lang: {\n            type: 'string',\n            description:\n              'The code of the language these strings are written in.',\n            format: 'language',\n          },\n          name: {\n            type: 'string',\n            description: 'A short human-readable name for the label.',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            description:\n              'A longer description of what the label means and why it might be applied.',\n            maxGraphemes: 10000,\n            maxLength: 100000,\n          },\n        },\n      },\n      labelValue: {\n        type: 'string',\n        knownValues: [\n          '!hide',\n          '!no-promote',\n          '!warn',\n          '!no-unauthenticated',\n          'dmca-violation',\n          'doxxing',\n          'porn',\n          'sexual',\n          'nudity',\n          'nsfl',\n          'gore',\n        ],\n      },\n    },\n  },\n  ComAtprotoLabelQueryLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.queryLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.',\n        parameters: {\n          type: 'params',\n          required: ['uriPatterns'],\n          properties: {\n            uriPatterns: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                \"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.\",\n            },\n            sources: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n              description:\n                'Optional list of label sources (DIDs) to filter on.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelSubscribeLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.subscribeLabels',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.label.subscribeLabels#labels',\n              'lex:com.atproto.label.subscribeLabels#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n        ],\n      },\n      labels: {\n        type: 'object',\n        required: ['seq', 'labels'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLexiconResolveLexicon: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.resolveLexicon',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Resolves an atproto lexicon (NSID) to a schema.',\n        parameters: {\n          type: 'params',\n          properties: {\n            nsid: {\n              format: 'nsid',\n              type: 'string',\n              description: 'The lexicon NSID to resolve.',\n            },\n          },\n          required: ['nsid'],\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n                description: 'The CID of the lexicon schema record.',\n              },\n              schema: {\n                type: 'ref',\n                ref: 'lex:com.atproto.lexicon.schema#main',\n                description: 'The resolved lexicon schema record.',\n              },\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n                description: 'The AT-URI of the lexicon schema record.',\n              },\n            },\n            required: ['uri', 'cid', 'schema'],\n          },\n        },\n        errors: [\n          {\n            description: 'No lexicon was resolved for the NSID.',\n            name: 'LexiconNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoLexiconSchema: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.schema',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).\",\n        key: 'nsid',\n        record: {\n          type: 'object',\n          required: ['lexicon'],\n          properties: {\n            lexicon: {\n              type: 'integer',\n              description:\n                \"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.\",\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationCreateReport: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.createReport',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['reasonType', 'subject'],\n            properties: {\n              reasonType: {\n                type: 'ref',\n                description:\n                  'Indicates the broad category of violation the report is for.',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n                description:\n                  'Additional context about the content and violation.',\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.createReport#modTool',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'id',\n              'reasonType',\n              'subject',\n              'reportedBy',\n              'createdAt',\n            ],\n            properties: {\n              id: {\n                type: 'integer',\n              },\n              reasonType: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              reportedBy: {\n                type: 'string',\n                format: 'did',\n              },\n              createdAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationDefs: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'com.atproto.moderation.defs#reasonSpam',\n          'com.atproto.moderation.defs#reasonViolation',\n          'com.atproto.moderation.defs#reasonMisleading',\n          'com.atproto.moderation.defs#reasonSexual',\n          'com.atproto.moderation.defs#reasonRude',\n          'com.atproto.moderation.defs#reasonOther',\n          'com.atproto.moderation.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonSpam: {\n        type: 'token',\n        description:\n          'Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.',\n      },\n      reasonViolation: {\n        type: 'token',\n        description:\n          'Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.',\n      },\n      reasonMisleading: {\n        type: 'token',\n        description:\n          'Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.',\n      },\n      reasonSexual: {\n        type: 'token',\n        description:\n          'Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.',\n      },\n      reasonRude: {\n        type: 'token',\n        description:\n          'Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.',\n      },\n      reasonOther: {\n        type: 'token',\n        description:\n          'Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.',\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      subjectType: {\n        type: 'string',\n        description: 'Tag describing a type of subject that might be reported.',\n        knownValues: ['account', 'record', 'chat'],\n      },\n    },\n  },\n  ComAtprotoRepoApplyWrites: {\n    lexicon: 1,\n    id: 'com.atproto.repo.applyWrites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'writes'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              writes: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#create',\n                    'lex:com.atproto.repo.applyWrites#update',\n                    'lex:com.atproto.repo.applyWrites#delete',\n                  ],\n                  closed: true,\n                },\n              },\n              swapCommit: {\n                type: 'string',\n                description:\n                  'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              results: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#createResult',\n                    'lex:com.atproto.repo.applyWrites#updateResult',\n                    'lex:com.atproto.repo.applyWrites#deleteResult',\n                  ],\n                  closed: true,\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that the 'swapCommit' parameter did not match current commit.\",\n          },\n        ],\n      },\n      create: {\n        type: 'object',\n        description: 'Operation which creates a new record.',\n        required: ['collection', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            maxLength: 512,\n            format: 'record-key',\n            description:\n              'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      update: {\n        type: 'object',\n        description: 'Operation which updates an existing record.',\n        required: ['collection', 'rkey', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      delete: {\n        type: 'object',\n        description: 'Operation which deletes an existing record.',\n        required: ['collection', 'rkey'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n        },\n      },\n      createResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      updateResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      deleteResult: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n    },\n  },\n  ComAtprotoRepoCreateRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.createRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Create a single new repository record. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'record'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record itself. Must contain a $type field.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that 'swapCommit' didn't match current repo commit.\",\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDefs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.defs',\n    defs: {\n      commitMeta: {\n        type: 'object',\n        required: ['cid', 'rev'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoDeleteRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.deleteRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDescribeRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.describeRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about an account and repository, including the list of collections. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'handle',\n              'did',\n              'didDoc',\n              'collections',\n              'handleIsCorrect',\n            ],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for this account.',\n              },\n              collections: {\n                type: 'array',\n                description:\n                  'List of all the collections (NSIDs) for which this repo contains at least one record.',\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              handleIsCorrect: {\n                type: 'boolean',\n                description:\n                  'Indicates if handle is currently valid (resolves bi-directionally)',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a single record from a repository. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection', 'rkey'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record collection.',\n            },\n            rkey: {\n              type: 'string',\n              description: 'The Record Key.',\n              format: 'record-key',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'The CID of the version of the record. If not specified, then return the most recent version.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'value'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              value: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoImportRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.importRepo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.',\n        input: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListMissingBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listMissingBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blobs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blobs: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob',\n                },\n              },\n            },\n          },\n        },\n      },\n      recordBlob: {\n        type: 'object',\n        required: ['cid', 'recordUri'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListRecords: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List a range of records in a repository, matching a specific collection. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record type.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n              description: 'The number of records to return.',\n            },\n            cursor: {\n              type: 'string',\n            },\n            reverse: {\n              type: 'boolean',\n              description: 'Flag to reverse the order of the returned records.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              records: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listRecords#record',\n                },\n              },\n            },\n          },\n        },\n      },\n      record: {\n        type: 'object',\n        required: ['uri', 'cid', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoPutRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.putRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey', 'record'],\n            nullable: ['swapRecord'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record to write.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoStrongRef: {\n    lexicon: 1,\n    id: 'com.atproto.repo.strongRef',\n    description: 'A URI with a content-hash fingerprint.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoUploadBlob: {\n    lexicon: 1,\n    id: 'com.atproto.repo.uploadBlob',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.',\n        input: {\n          encoding: '*/*',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blob'],\n            properties: {\n              blob: {\n                type: 'blob',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerActivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.activateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.\",\n      },\n    },\n  },\n  ComAtprotoServerCheckAccountStatus: {\n    lexicon: 1,\n    id: 'com.atproto.server.checkAccountStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'activated',\n              'validDid',\n              'repoCommit',\n              'repoRev',\n              'repoBlocks',\n              'indexedRecords',\n              'privateStateValues',\n              'expectedBlobs',\n              'importedBlobs',\n            ],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              validDid: {\n                type: 'boolean',\n              },\n              repoCommit: {\n                type: 'string',\n                format: 'cid',\n              },\n              repoRev: {\n                type: 'string',\n              },\n              repoBlocks: {\n                type: 'integer',\n              },\n              indexedRecords: {\n                type: 'integer',\n              },\n              privateStateValues: {\n                type: 'integer',\n              },\n              expectedBlobs: {\n                type: 'integer',\n              },\n              importedBlobs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerConfirmEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.confirmEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'token'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountNotFound',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InvalidEmail',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an account. Implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Requested handle for the account.',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Pre-existing atproto DID, being imported to a new account.',\n              },\n              inviteCode: {\n                type: 'string',\n              },\n              verificationCode: {\n                type: 'string',\n              },\n              verificationPhone: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n                description:\n                  'Initial account password. May need to meet instance-specific password strength requirements.',\n              },\n              recoveryKey: {\n                type: 'string',\n                description:\n                  'DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.',\n              },\n              plcOp: {\n                type: 'unknown',\n                description:\n                  'A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            description:\n              'Account login session returned on successful account creation.',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID of the new account.',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'Complete DID document.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidHandle',\n          },\n          {\n            name: 'InvalidPassword',\n          },\n          {\n            name: 'InvalidInviteCode',\n          },\n          {\n            name: 'HandleNotAvailable',\n          },\n          {\n            name: 'UnsupportedDomain',\n          },\n          {\n            name: 'UnresolvableDid',\n          },\n          {\n            name: 'IncompatibleDidDoc',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an App Password.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description:\n                  'A short name for the App Password, to help distinguish them.',\n              },\n              privileged: {\n                type: 'boolean',\n                description:\n                  \"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.createAppPassword#appPassword',\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'password', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          password: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCode: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCode',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an invite code.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['useCount'],\n            properties: {\n              useCount: {\n                type: 'integer',\n              },\n              forAccount: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['code'],\n            properties: {\n              code: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create invite codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codeCount', 'useCount'],\n            properties: {\n              codeCount: {\n                type: 'integer',\n                default: 1,\n              },\n              useCount: {\n                type: 'integer',\n              },\n              forAccounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.createInviteCodes#accountCodes',\n                },\n              },\n            },\n          },\n        },\n      },\n      accountCodes: {\n        type: 'object',\n        required: ['account', 'codes'],\n        properties: {\n          account: {\n            type: 'string',\n          },\n          codes: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.createSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an authentication session.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier', 'password'],\n            properties: {\n              identifier: {\n                type: 'string',\n                description:\n                  'Handle or other identifier supported by the server for the authenticating user.',\n              },\n              password: {\n                type: 'string',\n              },\n              authFactorToken: {\n                type: 'string',\n              },\n              allowTakendown: {\n                type: 'boolean',\n                description:\n                  'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'AuthFactorTokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeactivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deactivateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              deleteAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'A recommendation to server as to how long they should hold onto the deactivated account before deleting.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDefs: {\n    lexicon: 1,\n    id: 'com.atproto.server.defs',\n    defs: {\n      inviteCode: {\n        type: 'object',\n        required: [\n          'code',\n          'available',\n          'disabled',\n          'forAccount',\n          'createdBy',\n          'createdAt',\n          'uses',\n        ],\n        properties: {\n          code: {\n            type: 'string',\n          },\n          available: {\n            type: 'integer',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          forAccount: {\n            type: 'string',\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          uses: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCodeUse',\n            },\n          },\n        },\n      },\n      inviteCodeUse: {\n        type: 'object',\n        required: ['usedBy', 'usedAt'],\n        properties: {\n          usedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          usedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password', 'token'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeleteSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        errors: [\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDescribeServer: {\n    lexicon: 1,\n    id: 'com.atproto.server.describeServer',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Describes the server's account creation requirements and capabilities. Implemented by PDS.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'availableUserDomains'],\n            properties: {\n              inviteCodeRequired: {\n                type: 'boolean',\n                description:\n                  'If true, an invite code must be supplied to create an account on this instance.',\n              },\n              phoneVerificationRequired: {\n                type: 'boolean',\n                description:\n                  'If true, a phone verification token must be supplied to create an account on this instance.',\n              },\n              availableUserDomains: {\n                type: 'array',\n                description:\n                  'List of domain suffixes that can be used in account handles.',\n                items: {\n                  type: 'string',\n                },\n              },\n              links: {\n                type: 'ref',\n                description: 'URLs of service policy documents.',\n                ref: 'lex:com.atproto.server.describeServer#links',\n              },\n              contact: {\n                type: 'ref',\n                description: 'Contact information',\n                ref: 'lex:com.atproto.server.describeServer#contact',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n            format: 'uri',\n          },\n          termsOfService: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      contact: {\n        type: 'object',\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerGetAccountInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.getAccountInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get all invite codes for the current account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            includeUsed: {\n              type: 'boolean',\n              default: true,\n            },\n            createAvailable: {\n              type: 'boolean',\n              default: true,\n              description:\n                \"Controls whether any new 'earned' but not 'created' invites should be created.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateCreate',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetServiceAuth: {\n    lexicon: 1,\n    id: 'com.atproto.server.getServiceAuth',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a signed token on behalf of the requesting DID for the requested service.',\n        parameters: {\n          type: 'params',\n          required: ['aud'],\n          properties: {\n            aud: {\n              type: 'string',\n              format: 'did',\n              description:\n                'The DID of the service that the token will be used to authenticate with',\n            },\n            exp: {\n              type: 'integer',\n              description:\n                'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',\n            },\n            lxm: {\n              type: 'string',\n              format: 'nsid',\n              description:\n                'Lexicon (XRPC) method to bind the requested token to',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadExpiration',\n            description:\n              'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.getSession',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about the current auth session. Requires auth.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'did'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerListAppPasswords: {\n    lexicon: 1,\n    id: 'com.atproto.server.listAppPasswords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all App Passwords.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['passwords'],\n            properties: {\n              passwords: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.listAppPasswords#appPassword',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRefreshSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.refreshSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  \"Hosting status of the account. If not specified, then assume 'active'.\",\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRequestAccountDelete: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestAccountDelete',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account deletion via email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailConfirmation: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailConfirmation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to confirm ownership of email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Request a token in order to update email.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['tokenRequired'],\n            properties: {\n              tokenRequired: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRequestPasswordReset: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestPasswordReset',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account password reset via email.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerReserveSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.server.reserveSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID to reserve a key for.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['signingKey'],\n            properties: {\n              signingKey: {\n                type: 'string',\n                description:\n                  'The public key for the reserved signing key, in did:key serialization.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerResetPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.resetPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Reset a user account password using a token.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'password'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRevokeAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.revokeAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Revoke an App Password by name.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerUpdateEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.updateEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              token: {\n                type: 'string',\n                description:\n                  \"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.\",\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'TokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncDefs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.defs',\n    defs: {\n      hostStatus: {\n        type: 'string',\n        knownValues: ['active', 'idle', 'offline', 'throttled', 'banned'],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlob: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlob',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cid'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the account.',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description: 'The CID of the blob to fetch',\n            },\n          },\n        },\n        output: {\n          encoding: '*/*',\n        },\n        errors: [\n          {\n            name: 'BlobNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlocks: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cids'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            cids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'BlockNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetCheckout: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getCheckout',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'DEPRECATED - please use com.atproto.sync.getRepo instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoSyncGetHead: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED - please use com.atproto.sync.getLatestCommit instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HeadNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetHostStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHostStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns information about a specified upstream host, as consumed by the server. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          required: ['hostname'],\n          properties: {\n            hostname: {\n              type: 'string',\n              description:\n                'Hostname of the host (eg, PDS or relay) being queried.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n              },\n              seq: {\n                type: 'integer',\n                description:\n                  'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n              },\n              accountCount: {\n                type: 'integer',\n                description:\n                  'Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.',\n              },\n              status: {\n                type: 'ref',\n                ref: 'lex:com.atproto.sync.defs#hostStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetLatestCommit: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getLatestCommit',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the current commit CID & revision of the specified repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cid', 'rev'],\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'collection', 'rkey'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            rkey: {\n              type: 'string',\n              description: 'Record Key',\n              format: 'record-key',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepo: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.\",\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description:\n                \"The revision ('rev') of the repo to create a diff from.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepoStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepoStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'active'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: [\n                  'takendown',\n                  'suspended',\n                  'deleted',\n                  'deactivated',\n                  'desynchronized',\n                  'throttled',\n                ],\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n                description:\n                  'Optional field, the current rev of the repo, if active=true',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description: 'Optional revision of the repo to list blobs since.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cids'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              cids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListHosts: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listHosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 200,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hosts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hosts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listHosts#host',\n                },\n                description:\n                  'Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.',\n              },\n            },\n          },\n        },\n      },\n      host: {\n        type: 'object',\n        required: ['hostname'],\n        properties: {\n          hostname: {\n            type: 'string',\n            description: 'hostname of server; not a URL (no scheme)',\n          },\n          seq: {\n            type: 'integer',\n            description:\n              'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n          },\n          accountCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:com.atproto.sync.defs#hostStatus',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listRepos#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did', 'head', 'rev'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          head: {\n            type: 'string',\n            format: 'cid',\n            description: 'Current repo commit CID',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n          active: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListReposByCollection: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listReposByCollection',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DIDs which have records with the given collection NSID.',\n        parameters: {\n          type: 'params',\n          required: ['collection'],\n          properties: {\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            limit: {\n              type: 'integer',\n              description:\n                'Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.',\n              minimum: 1,\n              maximum: 2000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listReposByCollection#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncNotifyOfUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.sync.notifyOfUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (usually a PDS) that is notifying of update.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncRequestCrawl: {\n    lexicon: 1,\n    id: 'com.atproto.sync.requestCrawl',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (eg, PDS) that is requesting to be crawled.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostBanned',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncSubscribeRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.subscribeRepos',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.sync.subscribeRepos#commit',\n              'lex:com.atproto.sync.subscribeRepos#sync',\n              'lex:com.atproto.sync.subscribeRepos#identity',\n              'lex:com.atproto.sync.subscribeRepos#account',\n              'lex:com.atproto.sync.subscribeRepos#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n          {\n            name: 'ConsumerTooSlow',\n            description:\n              'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.',\n          },\n        ],\n      },\n      commit: {\n        type: 'object',\n        description:\n          'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.',\n        required: [\n          'seq',\n          'rebase',\n          'tooBig',\n          'repo',\n          'commit',\n          'rev',\n          'since',\n          'blocks',\n          'ops',\n          'blobs',\n          'time',\n        ],\n        nullable: ['since'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          rebase: {\n            type: 'boolean',\n            description: 'DEPRECATED -- unused',\n          },\n          tooBig: {\n            type: 'boolean',\n            description:\n              'DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.',\n          },\n          repo: {\n            type: 'string',\n            format: 'did',\n            description:\n              \"The repo this event comes from. Note that all other message types name this field 'did'.\",\n          },\n          commit: {\n            type: 'cid-link',\n            description: 'Repo commit object CID.',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.',\n          },\n          since: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the last emitted commit from this repo (if any).',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.\",\n            maxLength: 2000000,\n          },\n          ops: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.sync.subscribeRepos#repoOp',\n              description:\n                'List of repo mutation operations in this commit (eg, records created, updated, or deleted).',\n            },\n            maxLength: 200,\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'cid-link',\n              description:\n                'DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.',\n            },\n          },\n          prevData: {\n            type: 'cid-link',\n            description:\n              \"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.\",\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      sync: {\n        type: 'object',\n        description:\n          'Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.',\n        required: ['seq', 'did', 'blocks', 'rev', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description:\n              'The account this repo event corresponds to. Must match that in the commit object.',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.\",\n            maxLength: 10000,\n          },\n          rev: {\n            type: 'string',\n            description:\n              'The rev of the commit. This value must match that in the commit object.',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      identity: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.\",\n        required: ['seq', 'did', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.\",\n          },\n        },\n      },\n      account: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.\",\n        required: ['seq', 'did', 'time', 'active'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a reason for why the account is not active.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n      repoOp: {\n        type: 'object',\n        description: 'A repo operation, ie a mutation of a single record.',\n        required: ['action', 'path', 'cid'],\n        nullable: ['cid'],\n        properties: {\n          action: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          path: {\n            type: 'string',\n          },\n          cid: {\n            type: 'cid-link',\n            description:\n              'For creates and updates, the new record CID. For deletions, null.',\n          },\n          prev: {\n            type: 'cid-link',\n            description:\n              'For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempAddReservedHandle: {\n    lexicon: 1,\n    id: 'com.atproto.temp.addReservedHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a handle to the set of reserved handles.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckHandleAvailability: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkHandleAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description:\n                'Tentative handle. Will be checked for availability or used to build handle suggestions.',\n            },\n            email: {\n              type: 'string',\n              description:\n                'User-provided email. Might be used to build handle suggestions.',\n            },\n            birthDate: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'User-provided birth date. Might be used to build handle suggestions.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'result'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Echo of the input handle.',\n              },\n              result: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.temp.checkHandleAvailability#resultAvailable',\n                  'lex:com.atproto.temp.checkHandleAvailability#resultUnavailable',\n                ],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n            description: 'An invalid email was provided.',\n          },\n        ],\n      },\n      resultAvailable: {\n        type: 'object',\n        description: 'Indicates the provided handle is available.',\n        properties: {},\n      },\n      resultUnavailable: {\n        type: 'object',\n        description:\n          'Indicates the provided handle is unavailable and gives suggestions of available handles.',\n        required: ['suggestions'],\n        properties: {\n          suggestions: {\n            type: 'array',\n            description:\n              'List of suggested handles based on the provided inputs.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.temp.checkHandleAvailability#suggestion',\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['handle', 'method'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          method: {\n            type: 'string',\n            description:\n              'Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckSignupQueue: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkSignupQueue',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Check accounts location in signup queue.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['activated'],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              placeInQueue: {\n                type: 'integer',\n              },\n              estimatedTimeMs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempDereferenceScope: {\n    lexicon: 1,\n    id: 'com.atproto.temp.dereferenceScope',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Allows finding the oauth permission scope from a reference',\n        parameters: {\n          type: 'params',\n          required: ['scope'],\n          properties: {\n            scope: {\n              type: 'string',\n              description: \"The scope reference (starts with 'ref:')\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['scope'],\n            properties: {\n              scope: {\n                type: 'string',\n                description: 'The full oauth permission scope',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidScopeReference',\n            description: 'An invalid scope reference was provided.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoTempFetchLabels: {\n    lexicon: 1,\n    id: 'com.atproto.temp.fetchLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.',\n        parameters: {\n          type: 'params',\n          properties: {\n            since: {\n              type: 'integer',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRequestPhoneVerification: {\n    lexicon: 1,\n    id: 'com.atproto.temp.requestPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a verification code to be sent to the supplied phone number',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phoneNumber'],\n            properties: {\n              phoneNumber: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRevokeAccountCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.temp.revokeAccountCredentials',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComGermnetworkDeclaration: {\n    lexicon: 1,\n    id: 'com.germnetwork.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Germ Network account',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['version', 'currentKey'],\n          properties: {\n            version: {\n              type: 'string',\n              description:\n                'Semver version number, without pre-release or build information, for the format of opaque content',\n              minLength: 5,\n              maxLength: 14,\n            },\n            currentKey: {\n              type: 'bytes',\n              description:\n                'Opaque value, an ed25519 public key prefixed with a byte enum',\n            },\n            messageMe: {\n              type: 'ref',\n              description: 'Controls who can message this account',\n              ref: 'lex:com.germnetwork.declaration#messageMe',\n            },\n            keyPackage: {\n              type: 'bytes',\n              description:\n                'Opaque value, contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey',\n            },\n            continuityProofs: {\n              type: 'array',\n              description: 'Array of opaque values to allow for key rolling',\n              items: {\n                type: 'bytes',\n              },\n              maxLength: 1000,\n            },\n          },\n        },\n      },\n      messageMe: {\n        type: 'object',\n        required: ['showButtonTo', 'messageMeUrl'],\n        properties: {\n          messageMeUrl: {\n            type: 'string',\n            description:\n              'A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who wish to message each other',\n            format: 'uri',\n            minLength: 1,\n            maxLength: 2047,\n          },\n          showButtonTo: {\n            type: 'string',\n            knownValues: ['none', 'usersIFollow', 'everyone'],\n            description:\n              \"The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer.\",\n            minLength: 1,\n            maxLength: 100,\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationCreateTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.createTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Administrative action to create a new, re-usable communication (email for now) template.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'contentMarkdown', 'name'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the template.',\n              },\n              contentMarkdown: {\n                type: 'string',\n                description:\n                  'Content of the template, markdown supported, can contain variable placeholders.',\n              },\n              subject: {\n                type: 'string',\n                description: 'Subject of the message, used in emails.',\n              },\n              lang: {\n                type: 'string',\n                format: 'language',\n                description: 'Message language.',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'DID of the user who is creating the template.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.communication.defs#templateView',\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateTemplateName',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneCommunicationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.defs',\n    defs: {\n      templateView: {\n        type: 'object',\n        required: [\n          'id',\n          'name',\n          'contentMarkdown',\n          'disabled',\n          'lastUpdatedBy',\n          'createdAt',\n          'updatedAt',\n        ],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          name: {\n            type: 'string',\n            description: 'Name of the template.',\n          },\n          subject: {\n            type: 'string',\n            description:\n              'Content of the template, can contain markdown and variable placeholders.',\n          },\n          contentMarkdown: {\n            type: 'string',\n            description: 'Subject of the message, used in emails.',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          lang: {\n            type: 'string',\n            format: 'language',\n            description: 'Message language.',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who last updated the template.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationDeleteTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.deleteTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a communication template.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationListTemplates: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.listTemplates',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of all communication templates.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['communicationTemplates'],\n            properties: {\n              communicationTemplates: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.communication.defs#templateView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationUpdateTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.updateTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Administrative action to update an existing communication template. Allows passing partial fields to patch specific fields only.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'ID of the template to be updated.',\n              },\n              name: {\n                type: 'string',\n                description: 'Name of the template.',\n              },\n              lang: {\n                type: 'string',\n                format: 'language',\n                description: 'Message language.',\n              },\n              contentMarkdown: {\n                type: 'string',\n                description:\n                  'Content of the template, markdown supported, can contain variable placeholders.',\n              },\n              subject: {\n                type: 'string',\n                description: 'Subject of the message, used in emails.',\n              },\n              updatedBy: {\n                type: 'string',\n                format: 'did',\n                description: 'DID of the user who is updating the template.',\n              },\n              disabled: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.communication.defs#templateView',\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateTemplateName',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneHostingGetAccountHistory: {\n    lexicon: 1,\n    id: 'tools.ozone.hosting.getAccountHistory',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get account history, e.g. log of updated email addresses or other identity information.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            events: {\n              type: 'array',\n              items: {\n                type: 'string',\n                knownValues: [\n                  'accountCreated',\n                  'emailUpdated',\n                  'emailConfirmed',\n                  'passwordUpdated',\n                  'handleUpdated',\n                ],\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.hosting.getAccountHistory#event',\n                },\n              },\n            },\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        required: ['details', 'createdBy', 'createdAt'],\n        properties: {\n          details: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.hosting.getAccountHistory#accountCreated',\n              'lex:tools.ozone.hosting.getAccountHistory#emailUpdated',\n              'lex:tools.ozone.hosting.getAccountHistory#emailConfirmed',\n              'lex:tools.ozone.hosting.getAccountHistory#passwordUpdated',\n              'lex:tools.ozone.hosting.getAccountHistory#handleUpdated',\n            ],\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      accountCreated: {\n        type: 'object',\n        required: [],\n        properties: {\n          email: {\n            type: 'string',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n        },\n      },\n      emailUpdated: {\n        type: 'object',\n        required: ['email'],\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n      emailConfirmed: {\n        type: 'object',\n        required: ['email'],\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n      passwordUpdated: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n      handleUpdated: {\n        type: 'object',\n        required: ['handle'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationCancelScheduledActions: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.cancelScheduledActions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Cancel all pending scheduled moderation actions for specified subjects',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjects'],\n            properties: {\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description:\n                  'Array of DID subjects to cancel scheduled actions for',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  'Optional comment describing the reason for cancellation',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.cancelScheduledActions#cancellationResults',\n          },\n        },\n      },\n      cancellationResults: {\n        type: 'object',\n        required: ['succeeded', 'failed'],\n        properties: {\n          succeeded: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n            description:\n              'DIDs for which all pending scheduled actions were successfully cancelled',\n          },\n          failed: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.cancelScheduledActions#failedCancellation',\n            },\n            description:\n              'DIDs for which cancellation failed with error details',\n          },\n        },\n      },\n      failedCancellation: {\n        type: 'object',\n        required: ['did', 'error'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          error: {\n            type: 'string',\n          },\n          errorCode: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.defs',\n    defs: {\n      modEventView: {\n        type: 'object',\n        required: [\n          'id',\n          'event',\n          'subject',\n          'subjectBlobCids',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          event: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#modEventTakedown',\n              'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n              'lex:tools.ozone.moderation.defs#modEventComment',\n              'lex:tools.ozone.moderation.defs#modEventReport',\n              'lex:tools.ozone.moderation.defs#modEventLabel',\n              'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n              'lex:tools.ozone.moderation.defs#modEventEscalate',\n              'lex:tools.ozone.moderation.defs#modEventMute',\n              'lex:tools.ozone.moderation.defs#modEventUnmute',\n              'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventEmail',\n              'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n              'lex:tools.ozone.moderation.defs#modEventDivert',\n              'lex:tools.ozone.moderation.defs#modEventTag',\n              'lex:tools.ozone.moderation.defs#accountEvent',\n              'lex:tools.ozone.moderation.defs#identityEvent',\n              'lex:tools.ozone.moderation.defs#recordEvent',\n              'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n              'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n              'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n              'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.admin.defs#repoRef',\n              'lex:com.atproto.repo.strongRef',\n              'lex:chat.bsky.convo.defs#messageRef',\n            ],\n          },\n          subjectBlobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          creatorHandle: {\n            type: 'string',\n          },\n          subjectHandle: {\n            type: 'string',\n          },\n          modTool: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modTool',\n          },\n        },\n      },\n      modEventViewDetail: {\n        type: 'object',\n        required: [\n          'id',\n          'event',\n          'subject',\n          'subjectBlobs',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          event: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#modEventTakedown',\n              'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n              'lex:tools.ozone.moderation.defs#modEventComment',\n              'lex:tools.ozone.moderation.defs#modEventReport',\n              'lex:tools.ozone.moderation.defs#modEventLabel',\n              'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n              'lex:tools.ozone.moderation.defs#modEventEscalate',\n              'lex:tools.ozone.moderation.defs#modEventMute',\n              'lex:tools.ozone.moderation.defs#modEventUnmute',\n              'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventEmail',\n              'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n              'lex:tools.ozone.moderation.defs#modEventDivert',\n              'lex:tools.ozone.moderation.defs#modEventTag',\n              'lex:tools.ozone.moderation.defs#accountEvent',\n              'lex:tools.ozone.moderation.defs#identityEvent',\n              'lex:tools.ozone.moderation.defs#recordEvent',\n              'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n              'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n              'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n              'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoView',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n              'lex:tools.ozone.moderation.defs#recordView',\n              'lex:tools.ozone.moderation.defs#recordViewNotFound',\n            ],\n          },\n          subjectBlobs: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.defs#blobView',\n            },\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          modTool: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modTool',\n          },\n        },\n      },\n      subjectStatusView: {\n        type: 'object',\n        required: ['id', 'subject', 'createdAt', 'updatedAt', 'reviewState'],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.admin.defs#repoRef',\n              'lex:com.atproto.repo.strongRef',\n              'lex:chat.bsky.convo.defs#messageRef',\n            ],\n          },\n          hosting: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#accountHosting',\n              'lex:tools.ozone.moderation.defs#recordHosting',\n            ],\n          },\n          subjectBlobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n          subjectRepoHandle: {\n            type: 'string',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing when the last update was made to the moderation status of the subject',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing the first moderation status impacting event was emitted on the subject',\n          },\n          reviewState: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectReviewState',\n          },\n          comment: {\n            type: 'string',\n            description: 'Sticky comment on the subject.',\n          },\n          priorityScore: {\n            type: 'integer',\n            description:\n              'Numeric value representing the level of priority. Higher score means higher priority.',\n            minimum: 0,\n            maximum: 100,\n          },\n          muteUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          muteReportingUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastReviewedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          lastReviewedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastReportedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastAppealedAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing when the author of the subject appealed a moderation action',\n          },\n          takendown: {\n            type: 'boolean',\n          },\n          appealed: {\n            type: 'boolean',\n            description:\n              'True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.',\n          },\n          suspendUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          tags: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          accountStats: {\n            description: 'Statistics related to the account subject',\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#accountStats',\n          },\n          recordsStats: {\n            description:\n              \"Statistics related to the record subjects authored by the subject's account\",\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordsStats',\n          },\n          accountStrike: {\n            description:\n              'Strike information for the account (account-level only)',\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#accountStrike',\n          },\n          ageAssuranceState: {\n            type: 'string',\n            description: 'Current age assurance state of the subject.',\n            knownValues: ['pending', 'assured', 'unknown', 'reset', 'blocked'],\n          },\n          ageAssuranceUpdatedBy: {\n            type: 'string',\n            description:\n              'Whether or not the last successful update to age assurance was made by the user or admin.',\n            knownValues: ['admin', 'user'],\n          },\n        },\n      },\n      subjectView: {\n        description:\n          \"Detailed view of a subject. For record subjects, the author's repo and profile will be returned.\",\n        type: 'object',\n        required: ['type', 'subject'],\n        properties: {\n          type: {\n            type: 'ref',\n            ref: 'lex:com.atproto.moderation.defs#subjectType',\n          },\n          subject: {\n            type: 'string',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoViewDetail',\n          },\n          profile: {\n            type: 'union',\n            refs: [],\n          },\n          record: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordViewDetail',\n          },\n        },\n      },\n      accountStats: {\n        description: 'Statistics about a particular account subject',\n        type: 'object',\n        properties: {\n          reportCount: {\n            description: 'Total number of reports on the account',\n            type: 'integer',\n          },\n          appealCount: {\n            description:\n              'Total number of appeals against a moderation action on the account',\n            type: 'integer',\n          },\n          suspendCount: {\n            description: 'Number of times the account was suspended',\n            type: 'integer',\n          },\n          escalateCount: {\n            description: 'Number of times the account was escalated',\n            type: 'integer',\n          },\n          takedownCount: {\n            description: 'Number of times the account was taken down',\n            type: 'integer',\n          },\n        },\n      },\n      recordsStats: {\n        description: 'Statistics about a set of record subject items',\n        type: 'object',\n        properties: {\n          totalReports: {\n            description:\n              'Cumulative sum of the number of reports on the items in the set',\n            type: 'integer',\n          },\n          reportedCount: {\n            description: 'Number of items that were reported at least once',\n            type: 'integer',\n          },\n          escalatedCount: {\n            description: 'Number of items that were escalated at least once',\n            type: 'integer',\n          },\n          appealedCount: {\n            description: 'Number of items that were appealed at least once',\n            type: 'integer',\n          },\n          subjectCount: {\n            description: 'Total number of item in the set',\n            type: 'integer',\n          },\n          pendingCount: {\n            description:\n              'Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state',\n            type: 'integer',\n          },\n          processedCount: {\n            description:\n              'Number of item currently in \"reviewNone\" or \"reviewClosed\" state',\n            type: 'integer',\n          },\n          takendownCount: {\n            description: 'Number of item currently taken down',\n            type: 'integer',\n          },\n        },\n      },\n      accountStrike: {\n        description: 'Strike information for an account',\n        type: 'object',\n        properties: {\n          activeStrikeCount: {\n            description:\n              'Current number of active strikes (excluding expired strikes)',\n            type: 'integer',\n          },\n          totalStrikeCount: {\n            description:\n              'Total number of strikes ever received (including expired strikes)',\n            type: 'integer',\n          },\n          firstStrikeAt: {\n            description: 'Timestamp of the first strike received',\n            type: 'string',\n            format: 'datetime',\n          },\n          lastStrikeAt: {\n            description: 'Timestamp of the most recent strike received',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      subjectReviewState: {\n        type: 'string',\n        knownValues: [\n          'tools.ozone.moderation.defs#reviewOpen',\n          'tools.ozone.moderation.defs#reviewEscalated',\n          'tools.ozone.moderation.defs#reviewClosed',\n          'tools.ozone.moderation.defs#reviewNone',\n        ],\n      },\n      reviewOpen: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator',\n      },\n      reviewEscalated: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator',\n      },\n      reviewClosed: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator',\n      },\n      reviewNone: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it',\n      },\n      modEventTakedown: {\n        type: 'object',\n        description: 'Take down a subject permanently or temporarily',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the takedown should be in effect before automatically expiring.',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that drove the decision.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\",\n          },\n          targetServices: {\n            type: 'array',\n            items: {\n              type: 'string',\n              knownValues: ['appview', 'pds'],\n            },\n            description:\n              'List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.',\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user for this violation.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n        },\n      },\n      modEventReverseTakedown: {\n        type: 'object',\n        description: 'Revert take down action on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policy infraction for which takedown is being reversed.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation. Usually set from the last policy infraction's severity.\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              \"Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity.\",\n          },\n        },\n      },\n      modEventResolveAppeal: {\n        type: 'object',\n        description: 'Resolve appeal on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe resolution.',\n          },\n        },\n      },\n      modEventComment: {\n        type: 'object',\n        description:\n          'Add a comment to a subject. An empty comment will clear any previously set sticky comment.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          sticky: {\n            type: 'boolean',\n            description: 'Make the comment persistent on the subject',\n          },\n        },\n      },\n      modEventReport: {\n        type: 'object',\n        description: 'Report a subject',\n        required: ['reportType'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          isReporterMuted: {\n            type: 'boolean',\n            description:\n              \"Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject.\",\n          },\n          reportType: {\n            type: 'ref',\n            ref: 'lex:com.atproto.moderation.defs#reasonType',\n          },\n        },\n      },\n      modEventLabel: {\n        type: 'object',\n        description: 'Apply/Negate labels on a subject',\n        required: ['createLabelVals', 'negateLabelVals'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          createLabelVals: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          negateLabelVals: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the label will remain on the subject. Only applies on labels that are being added.',\n          },\n        },\n      },\n      modEventPriorityScore: {\n        type: 'object',\n        description:\n          'Set priority score of the subject. Higher score means higher priority.',\n        required: ['score'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          score: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description:\n          'Age assurance info coming directly from users. Only works on DID subjects.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n      ageAssuranceOverrideEvent: {\n        type: 'object',\n        description:\n          'Age assurance status override by moderators. Only works on DID subjects.',\n        required: ['comment', 'status'],\n        properties: {\n          status: {\n            type: 'string',\n            description:\n              'The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.',\n            knownValues: ['assured', 'reset', 'blocked'],\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n          comment: {\n            type: 'string',\n            minLength: 1,\n            description: 'Comment describing the reason for the override.',\n          },\n        },\n      },\n      ageAssurancePurgeEvent: {\n        type: 'object',\n        description:\n          'Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only.',\n        required: ['comment'],\n        properties: {\n          comment: {\n            type: 'string',\n            minLength: 1,\n            description: 'Comment describing the reason for the purge.',\n          },\n        },\n      },\n      revokeAccountCredentialsEvent: {\n        type: 'object',\n        description:\n          'Account credentials revocation by moderators. Only works on DID subjects.',\n        required: ['comment'],\n        properties: {\n          comment: {\n            minLength: 1,\n            type: 'string',\n            description: 'Comment describing the reason for the revocation.',\n          },\n        },\n      },\n      modEventAcknowledge: {\n        type: 'object',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n        },\n      },\n      modEventEscalate: {\n        type: 'object',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      modEventMute: {\n        type: 'object',\n        description: 'Mute incoming reports on a subject',\n        required: ['durationInHours'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description: 'Indicates how long the subject should remain muted.',\n          },\n        },\n      },\n      modEventUnmute: {\n        type: 'object',\n        description: 'Unmute action on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n        },\n      },\n      modEventMuteReporter: {\n        type: 'object',\n        description: 'Mute incoming reports from an account',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the account should remain muted. Falsy value here means a permanent mute.',\n          },\n        },\n      },\n      modEventUnmuteReporter: {\n        type: 'object',\n        description: 'Unmute incoming reports from an account',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n        },\n      },\n      modEventEmail: {\n        type: 'object',\n        description: 'Keep a log of outgoing email to a user',\n        required: ['subjectLine'],\n        properties: {\n          subjectLine: {\n            type: 'string',\n            description: 'The subject line of the email sent to the user.',\n          },\n          content: {\n            type: 'string',\n            description: 'The content of the email sent to the user.',\n          },\n          comment: {\n            type: 'string',\n            description: 'Additional comment about the outgoing comm.',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that necessitated the email.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n          isDelivered: {\n            type: 'boolean',\n            description:\n              \"Indicates whether the email was successfully delivered to the user's inbox.\",\n          },\n        },\n      },\n      modEventDivert: {\n        type: 'object',\n        description:\n          \"Divert a record's blobs to a 3rd party service for further scanning/tagging\",\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      modEventTag: {\n        type: 'object',\n        description: 'Add/Remove a tag on a subject',\n        required: ['add', 'remove'],\n        properties: {\n          add: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n            description:\n              \"Tags to be added to the subject. If already exists, won't be duplicated.\",\n          },\n          remove: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n            description:\n              \"Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated.\",\n          },\n          comment: {\n            type: 'string',\n            description: 'Additional comment about added/removed tags.',\n          },\n        },\n      },\n      accountEvent: {\n        type: 'object',\n        description:\n          'Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp', 'active'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            knownValues: [\n              'unknown',\n              'deactivated',\n              'deleted',\n              'takendown',\n              'suspended',\n              'tombstoned',\n            ],\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      identityEvent: {\n        type: 'object',\n        description:\n          'Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          pdsHost: {\n            type: 'string',\n            format: 'uri',\n          },\n          tombstone: {\n            type: 'boolean',\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      recordEvent: {\n        type: 'object',\n        description:\n          'Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp', 'op'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          op: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      scheduleTakedownEvent: {\n        type: 'object',\n        description: 'Logs a scheduled takedown action for an account.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      cancelScheduledTakedownEvent: {\n        type: 'object',\n        description:\n          'Logs cancellation of a scheduled takedown action for an account.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      repoView: {\n        type: 'object',\n        required: [\n          'did',\n          'handle',\n          'relatedRecords',\n          'indexedAt',\n          'moderation',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoViewDetail: {\n        type: 'object',\n        required: [\n          'did',\n          'handle',\n          'relatedRecords',\n          'indexedAt',\n          'moderation',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderationDetail',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoViewNotFound: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      recordView: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'value',\n          'blobCids',\n          'indexedAt',\n          'moderation',\n          'repo',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n          blobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoView',\n          },\n        },\n      },\n      recordViewDetail: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'value',\n          'blobs',\n          'indexedAt',\n          'moderation',\n          'repo',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.defs#blobView',\n            },\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderationDetail',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoView',\n          },\n        },\n      },\n      recordViewNotFound: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      moderation: {\n        type: 'object',\n        properties: {\n          subjectStatus: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n        },\n      },\n      moderationDetail: {\n        type: 'object',\n        properties: {\n          subjectStatus: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n        },\n      },\n      blobView: {\n        type: 'object',\n        required: ['cid', 'mimeType', 'size', 'createdAt'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          mimeType: {\n            type: 'string',\n          },\n          size: {\n            type: 'integer',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          details: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#imageDetails',\n              'lex:tools.ozone.moderation.defs#videoDetails',\n            ],\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n        },\n      },\n      imageDetails: {\n        type: 'object',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n          },\n          height: {\n            type: 'integer',\n          },\n        },\n      },\n      videoDetails: {\n        type: 'object',\n        required: ['width', 'height', 'length'],\n        properties: {\n          width: {\n            type: 'integer',\n          },\n          height: {\n            type: 'integer',\n          },\n          length: {\n            type: 'integer',\n          },\n        },\n      },\n      accountHosting: {\n        type: 'object',\n        required: ['status'],\n        properties: {\n          status: {\n            type: 'string',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'unknown',\n            ],\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deletedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          reactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      recordHosting: {\n        type: 'object',\n        required: ['status'],\n        properties: {\n          status: {\n            type: 'string',\n            knownValues: ['deleted', 'unknown'],\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deletedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reporterStats: {\n        type: 'object',\n        required: [\n          'did',\n          'accountReportCount',\n          'recordReportCount',\n          'reportedAccountCount',\n          'reportedRecordCount',\n          'takendownAccountCount',\n          'takendownRecordCount',\n          'labeledAccountCount',\n          'labeledRecordCount',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          accountReportCount: {\n            type: 'integer',\n            description:\n              'The total number of reports made by the user on accounts.',\n          },\n          recordReportCount: {\n            type: 'integer',\n            description:\n              'The total number of reports made by the user on records.',\n          },\n          reportedAccountCount: {\n            type: 'integer',\n            description: 'The total number of accounts reported by the user.',\n          },\n          reportedRecordCount: {\n            type: 'integer',\n            description: 'The total number of records reported by the user.',\n          },\n          takendownAccountCount: {\n            type: 'integer',\n            description:\n              \"The total number of accounts taken down as a result of the user's reports.\",\n          },\n          takendownRecordCount: {\n            type: 'integer',\n            description:\n              \"The total number of records taken down as a result of the user's reports.\",\n          },\n          labeledAccountCount: {\n            type: 'integer',\n            description:\n              \"The total number of accounts labeled as a result of the user's reports.\",\n          },\n          labeledRecordCount: {\n            type: 'integer',\n            description:\n              \"The total number of records labeled as a result of the user's reports.\",\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'automod', 'ozone/workspace')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n      timelineEventPlcCreate: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for a PLC create operation',\n      },\n      timelineEventPlcOperation: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for generic PLC operation',\n      },\n      timelineEventPlcTombstone: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for a PLC tombstone operation',\n      },\n      scheduledActionView: {\n        type: 'object',\n        description: 'View of a scheduled moderation action',\n        required: ['id', 'action', 'did', 'createdBy', 'createdAt', 'status'],\n        properties: {\n          id: {\n            type: 'integer',\n            description: 'Auto-incrementing row ID',\n          },\n          action: {\n            type: 'string',\n            knownValues: ['takedown'],\n            description: 'Type of action to be executed',\n          },\n          eventData: {\n            type: 'unknown',\n            description:\n              'Serialized event object that will be propagated to the event when performed',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description: 'Subject DID for the action',\n          },\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Exact time to execute the action',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Earliest time to execute the action (for randomized scheduling)',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Latest time to execute the action (for randomized scheduling)',\n          },\n          randomizeExecution: {\n            type: 'boolean',\n            description:\n              'Whether execution time should be randomized within the specified range',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who created this scheduled action',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the scheduled action was created',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the scheduled action was last updated',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['pending', 'executed', 'cancelled', 'failed'],\n            description: 'Current status of the scheduled action',\n          },\n          lastExecutedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the action was last attempted to be executed',\n          },\n          lastFailureReason: {\n            type: 'string',\n            description: 'Reason for the last execution failure',\n          },\n          executionEventId: {\n            type: 'integer',\n            description:\n              'ID of the moderation event created when action was successfully executed',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationEmitEvent: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.emitEvent',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Take a moderation action on an actor.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['event', 'subject', 'createdBy'],\n            properties: {\n              event: {\n                type: 'union',\n                refs: [\n                  'lex:tools.ozone.moderation.defs#modEventTakedown',\n                  'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n                  'lex:tools.ozone.moderation.defs#modEventEscalate',\n                  'lex:tools.ozone.moderation.defs#modEventComment',\n                  'lex:tools.ozone.moderation.defs#modEventLabel',\n                  'lex:tools.ozone.moderation.defs#modEventReport',\n                  'lex:tools.ozone.moderation.defs#modEventMute',\n                  'lex:tools.ozone.moderation.defs#modEventUnmute',\n                  'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n                  'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n                  'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n                  'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n                  'lex:tools.ozone.moderation.defs#modEventEmail',\n                  'lex:tools.ozone.moderation.defs#modEventDivert',\n                  'lex:tools.ozone.moderation.defs#modEventTag',\n                  'lex:tools.ozone.moderation.defs#accountEvent',\n                  'lex:tools.ozone.moderation.defs#identityEvent',\n                  'lex:tools.ozone.moderation.defs#recordEvent',\n                  'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n                  'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n                  'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n                  'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n                  'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n                  'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n                  'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n                ],\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              subjectBlobCids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.defs#modTool',\n              },\n              externalId: {\n                type: 'string',\n                description:\n                  'An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modEventView',\n          },\n        },\n        errors: [\n          {\n            name: 'SubjectHasAction',\n          },\n          {\n            name: 'DuplicateExternalId',\n            description:\n              'An event with the same external ID already exists for the subject.',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetAccountTimeline: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getAccountTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get timeline of all available events of an account. This includes moderation events, account history and did history.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['timeline'],\n            properties: {\n              timeline: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItem',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n      timelineItem: {\n        type: 'object',\n        required: ['day', 'summary'],\n        properties: {\n          day: {\n            type: 'string',\n          },\n          summary: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItemSummary',\n            },\n          },\n        },\n      },\n      timelineItemSummary: {\n        type: 'object',\n        required: ['eventSubjectType', 'eventType', 'count'],\n        properties: {\n          eventSubjectType: {\n            type: 'string',\n            knownValues: ['account', 'record', 'chat'],\n          },\n          eventType: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.moderation.defs#modEventTakedown',\n              'tools.ozone.moderation.defs#modEventReverseTakedown',\n              'tools.ozone.moderation.defs#modEventComment',\n              'tools.ozone.moderation.defs#modEventReport',\n              'tools.ozone.moderation.defs#modEventLabel',\n              'tools.ozone.moderation.defs#modEventAcknowledge',\n              'tools.ozone.moderation.defs#modEventEscalate',\n              'tools.ozone.moderation.defs#modEventMute',\n              'tools.ozone.moderation.defs#modEventUnmute',\n              'tools.ozone.moderation.defs#modEventMuteReporter',\n              'tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'tools.ozone.moderation.defs#modEventEmail',\n              'tools.ozone.moderation.defs#modEventResolveAppeal',\n              'tools.ozone.moderation.defs#modEventDivert',\n              'tools.ozone.moderation.defs#modEventTag',\n              'tools.ozone.moderation.defs#accountEvent',\n              'tools.ozone.moderation.defs#identityEvent',\n              'tools.ozone.moderation.defs#recordEvent',\n              'tools.ozone.moderation.defs#modEventPriorityScore',\n              'tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'tools.ozone.moderation.defs#ageAssuranceEvent',\n              'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'tools.ozone.moderation.defs#timelineEventPlcCreate',\n              'tools.ozone.moderation.defs#timelineEventPlcOperation',\n              'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n              'tools.ozone.hosting.getAccountHistory#accountCreated',\n              'tools.ozone.hosting.getAccountHistory#emailConfirmed',\n              'tools.ozone.hosting.getAccountHistory#passwordUpdated',\n              'tools.ozone.hosting.getAccountHistory#handleUpdated',\n              'tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          count: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetEvent: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getEvent',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a moderation event.',\n        parameters: {\n          type: 'params',\n          required: ['id'],\n          properties: {\n            id: {\n              type: 'integer',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modEventViewDetail',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRecord: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a record.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordViewDetail',\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetRecords: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some records.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              records: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:tools.ozone.moderation.defs#recordViewDetail',\n                    'lex:tools.ozone.moderation.defs#recordViewNotFound',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRepo: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a repository.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoViewDetail',\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetReporterStats: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getReporterStats',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get reporter stats for a list of users.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['stats'],\n            properties: {\n              stats: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#reporterStats',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRepos: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some repositories.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:tools.ozone.moderation.defs#repoViewDetail',\n                    'lex:tools.ozone.moderation.defs#repoViewNotFound',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetSubjects: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getSubjects',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about subjects.',\n        parameters: {\n          type: 'params',\n          required: ['subjects'],\n          properties: {\n            subjects: {\n              type: 'array',\n              maxLength: 100,\n              minLength: 1,\n              items: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjects'],\n            properties: {\n              subjects: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#subjectView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationListScheduledActions: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.listScheduledActions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'List scheduled moderation actions with optional filtering',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['statuses'],\n            properties: {\n              startsAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'Filter actions scheduled to execute after this time',\n              },\n              endsBefore: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'Filter actions scheduled to execute before this time',\n              },\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description: 'Filter actions for specific DID subjects',\n              },\n              statuses: {\n                type: 'array',\n                minLength: 1,\n                items: {\n                  type: 'string',\n                  knownValues: ['pending', 'executed', 'cancelled', 'failed'],\n                },\n                description: 'Filter actions by status',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actions'],\n            properties: {\n              actions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#scheduledActionView',\n                },\n              },\n              cursor: {\n                type: 'string',\n                description: 'Cursor for next page of results',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationQueryEvents: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.queryEvents',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List moderation events related to a subject.',\n        parameters: {\n          type: 'params',\n          properties: {\n            types: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned.',\n            },\n            createdBy: {\n              type: 'string',\n              format: 'did',\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'desc',\n              enum: ['asc', 'desc'],\n              description:\n                'Sort direction for the events. Defaults to descending order of created at timestamp.',\n            },\n            createdAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Retrieve events created after a given timestamp',\n            },\n            createdBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Retrieve events created before a given timestamp',\n            },\n            subject: {\n              type: 'string',\n              format: 'uri',\n            },\n            collections: {\n              type: 'array',\n              maxLength: 20,\n              description:\n                \"If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n            subjectType: {\n              type: 'string',\n              description:\n                \"If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n              knownValues: ['account', 'record'],\n            },\n            includeAllUserRecords: {\n              type: 'boolean',\n              default: false,\n              description:\n                \"If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            hasComment: {\n              type: 'boolean',\n              description: 'If true, only events with comments are returned',\n            },\n            comment: {\n              type: 'string',\n              description:\n                'If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition.',\n            },\n            addedLabels: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these labels were added are returned',\n            },\n            removedLabels: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these labels were removed are returned',\n            },\n            addedTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these tags were added are returned',\n            },\n            removedTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these tags were removed are returned',\n            },\n            reportTypes: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            policies: {\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'If specified, only events where the action policies match any of the given policies are returned',\n              },\n            },\n            modTool: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where the modTool name matches any of the given values are returned',\n            },\n            batchId: {\n              type: 'string',\n              description:\n                'If specified, only events where the batchId matches the given value are returned',\n            },\n            ageAssuranceState: {\n              type: 'string',\n              description:\n                'If specified, only events where the age assurance state matches the given value are returned',\n              knownValues: [\n                'pending',\n                'assured',\n                'unknown',\n                'reset',\n                'blocked',\n              ],\n            },\n            withStrike: {\n              type: 'boolean',\n              description:\n                'If specified, only events where strikeCount value is set are returned.',\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#modEventView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationQueryStatuses: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.queryStatuses',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'View moderation statuses of subjects (record or repo).',\n        parameters: {\n          type: 'params',\n          properties: {\n            queueCount: {\n              type: 'integer',\n              description:\n                'Number of queues being used by moderators. Subjects will be split among all queues.',\n            },\n            queueIndex: {\n              type: 'integer',\n              description:\n                'Index of the queue to fetch subjects from. Works only when queueCount value is specified.',\n            },\n            queueSeed: {\n              type: 'string',\n              description: 'A seeder to shuffle/balance the queue items.',\n            },\n            includeAllUserRecords: {\n              type: 'boolean',\n              description:\n                \"All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned.\",\n            },\n            subject: {\n              type: 'string',\n              format: 'uri',\n              description: 'The subject to get the status for.',\n            },\n            comment: {\n              type: 'string',\n              description: 'Search subjects by keyword from comments',\n            },\n            reportedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reported after a given timestamp',\n            },\n            reportedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reported before a given timestamp',\n            },\n            reviewedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reviewed after a given timestamp',\n            },\n            hostingDeletedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was deleted after a given timestamp',\n            },\n            hostingDeletedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was deleted before a given timestamp',\n            },\n            hostingUpdatedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was updated after a given timestamp',\n            },\n            hostingUpdatedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was updated before a given timestamp',\n            },\n            hostingStatuses: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'Search subjects by the status of the associated record/account',\n            },\n            reviewedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reviewed before a given timestamp',\n            },\n            includeMuted: {\n              type: 'boolean',\n              description:\n                \"By default, we don't include muted subjects in the results. Set this to true to include them.\",\n            },\n            onlyMuted: {\n              type: 'boolean',\n              description:\n                'When set to true, only muted subjects and reporters will be returned.',\n            },\n            reviewState: {\n              type: 'string',\n              description: 'Specify when fetching subjects in a certain state',\n              knownValues: [\n                'tools.ozone.moderation.defs#reviewOpen',\n                'tools.ozone.moderation.defs#reviewClosed',\n                'tools.ozone.moderation.defs#reviewEscalated',\n                'tools.ozone.moderation.defs#reviewNone',\n              ],\n            },\n            ignoreSubjects: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'uri',\n              },\n            },\n            lastReviewedBy: {\n              type: 'string',\n              format: 'did',\n              description:\n                'Get all subject statuses that were reviewed by a specific moderator',\n            },\n            sortField: {\n              type: 'string',\n              default: 'lastReportedAt',\n              enum: [\n                'lastReviewedAt',\n                'lastReportedAt',\n                'reportedRecordsCount',\n                'takendownRecordsCount',\n                'priorityScore',\n              ],\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'desc',\n              enum: ['asc', 'desc'],\n            },\n            takendown: {\n              type: 'boolean',\n              description: 'Get subjects that were taken down',\n            },\n            appealed: {\n              type: 'boolean',\n              description: 'Get subjects in unresolved appealed status',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            tags: {\n              type: 'array',\n              maxLength: 25,\n              items: {\n                type: 'string',\n                description:\n                  'Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters',\n              },\n            },\n            excludeTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            collections: {\n              type: 'array',\n              maxLength: 20,\n              description:\n                \"If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n            subjectType: {\n              type: 'string',\n              description:\n                \"If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n              knownValues: ['account', 'record'],\n            },\n            minAccountSuspendCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many suspensions will be returned.',\n            },\n            minReportedRecordsCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many reported records will be returned.',\n            },\n            minTakendownRecordsCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many taken down records will be returned.',\n            },\n            minPriorityScore: {\n              minimum: 0,\n              maximum: 100,\n              type: 'integer',\n              description:\n                'If specified, only subjects that have priority score value above the given value will be returned.',\n            },\n            minStrikeCount: {\n              type: 'integer',\n              minimum: 1,\n              description:\n                'If specified, only subjects that belong to an account that has at least this many active strikes will be returned.',\n            },\n            ageAssuranceState: {\n              type: 'string',\n              description:\n                'If specified, only subjects with the given age assurance state will be returned.',\n              knownValues: [\n                'pending',\n                'assured',\n                'unknown',\n                'reset',\n                'blocked',\n              ],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjectStatuses'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subjectStatuses: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationScheduleAction: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.scheduleAction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Schedule a moderation action to be executed at a future time',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['action', 'subjects', 'createdBy', 'scheduling'],\n            properties: {\n              action: {\n                type: 'union',\n                refs: ['lex:tools.ozone.moderation.scheduleAction#takedown'],\n              },\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description: 'Array of DID subjects to schedule the action for',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n              },\n              scheduling: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.scheduleAction#schedulingConfig',\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.defs#modTool',\n                description:\n                  'This will be propagated to the moderation event when it is applied',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.scheduleAction#scheduledActionResults',\n          },\n        },\n      },\n      takedown: {\n        type: 'object',\n        description: 'Schedule a takedown action',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the takedown should be in effect before automatically expiring.',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that drove the decision.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user when takedown is applied.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n          emailContent: {\n            type: 'string',\n            description: 'Email content to be sent to the user upon takedown.',\n          },\n          emailSubject: {\n            type: 'string',\n            description:\n              'Subject of the email to be sent to the user upon takedown.',\n          },\n        },\n      },\n      schedulingConfig: {\n        type: 'object',\n        description: 'Configuration for when the action should be executed',\n        properties: {\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Exact time to execute the action',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Earliest time to execute the action (for randomized scheduling)',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Latest time to execute the action (for randomized scheduling)',\n          },\n        },\n      },\n      scheduledActionResults: {\n        type: 'object',\n        required: ['succeeded', 'failed'],\n        properties: {\n          succeeded: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n          failed: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.scheduleAction#failedScheduling',\n            },\n          },\n        },\n      },\n      failedScheduling: {\n        type: 'object',\n        required: ['subject', 'error'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          error: {\n            type: 'string',\n          },\n          errorCode: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationSearchRepos: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.searchRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Find repositories based on a search term.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead\",\n            },\n            q: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#repoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneReportDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.report.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      reasonOther: {\n        type: 'token',\n        description: 'An issue not included in these options',\n      },\n      reasonViolenceAnimal: {\n        type: 'token',\n        description: 'Animal welfare violations',\n      },\n      reasonViolenceThreats: {\n        type: 'token',\n        description: 'Threats or incitement',\n      },\n      reasonViolenceGraphicContent: {\n        type: 'token',\n        description: 'Graphic violent content',\n      },\n      reasonViolenceGlorification: {\n        type: 'token',\n        description: 'Glorification of violence',\n      },\n      reasonViolenceExtremistContent: {\n        type: 'token',\n        description:\n          \"Extremist content. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonViolenceTrafficking: {\n        type: 'token',\n        description: 'Human trafficking',\n      },\n      reasonViolenceOther: {\n        type: 'token',\n        description: 'Other violent content',\n      },\n      reasonSexualAbuseContent: {\n        type: 'token',\n        description: 'Adult sexual abuse content',\n      },\n      reasonSexualNCII: {\n        type: 'token',\n        description: 'Non-consensual intimate imagery',\n      },\n      reasonSexualDeepfake: {\n        type: 'token',\n        description: 'Deepfake adult content',\n      },\n      reasonSexualAnimal: {\n        type: 'token',\n        description: 'Animal sexual abuse',\n      },\n      reasonSexualUnlabeled: {\n        type: 'token',\n        description: 'Unlabelled adult content',\n      },\n      reasonSexualOther: {\n        type: 'token',\n        description: 'Other sexual violence content',\n      },\n      reasonChildSafetyCSAM: {\n        type: 'token',\n        description:\n          \"Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonChildSafetyGroom: {\n        type: 'token',\n        description:\n          \"Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonChildSafetyPrivacy: {\n        type: 'token',\n        description: 'Privacy violation involving a minor',\n      },\n      reasonChildSafetyHarassment: {\n        type: 'token',\n        description: 'Harassment or bullying of minors',\n      },\n      reasonChildSafetyOther: {\n        type: 'token',\n        description:\n          \"Other child safety. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonHarassmentTroll: {\n        type: 'token',\n        description: 'Trolling',\n      },\n      reasonHarassmentTargeted: {\n        type: 'token',\n        description: 'Targeted harassment',\n      },\n      reasonHarassmentHateSpeech: {\n        type: 'token',\n        description: 'Hate speech',\n      },\n      reasonHarassmentDoxxing: {\n        type: 'token',\n        description: 'Doxxing',\n      },\n      reasonHarassmentOther: {\n        type: 'token',\n        description: 'Other harassing or hateful content',\n      },\n      reasonMisleadingBot: {\n        type: 'token',\n        description: 'Fake account or bot',\n      },\n      reasonMisleadingImpersonation: {\n        type: 'token',\n        description: 'Impersonation',\n      },\n      reasonMisleadingSpam: {\n        type: 'token',\n        description: 'Spam',\n      },\n      reasonMisleadingScam: {\n        type: 'token',\n        description: 'Scam',\n      },\n      reasonMisleadingElections: {\n        type: 'token',\n        description: 'False information about elections',\n      },\n      reasonMisleadingOther: {\n        type: 'token',\n        description: 'Other misleading content',\n      },\n      reasonRuleSiteSecurity: {\n        type: 'token',\n        description: 'Hacking or system attacks',\n      },\n      reasonRuleProhibitedSales: {\n        type: 'token',\n        description: 'Promoting or selling prohibited items or services',\n      },\n      reasonRuleBanEvasion: {\n        type: 'token',\n        description: 'Banned user returning',\n      },\n      reasonRuleOther: {\n        type: 'token',\n        description: 'Other',\n      },\n      reasonSelfHarmContent: {\n        type: 'token',\n        description: 'Content promoting or depicting self-harm',\n      },\n      reasonSelfHarmED: {\n        type: 'token',\n        description: 'Eating disorders',\n      },\n      reasonSelfHarmStunts: {\n        type: 'token',\n        description: 'Dangerous challenges or activities',\n      },\n      reasonSelfHarmSubstances: {\n        type: 'token',\n        description: 'Dangerous substances or drug abuse',\n      },\n      reasonSelfHarmOther: {\n        type: 'token',\n        description: 'Other dangerous content',\n      },\n    },\n  },\n  ToolsOzoneSafelinkAddRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.addRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a new URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern', 'action', 'reason'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to apply the rule to',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              action: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#actionType',\n              },\n              reason: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#reasonType',\n              },\n              comment: {\n                type: 'string',\n                description: 'Optional comment about the decision',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'Author DID. Only respected when using admin auth',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidUrl',\n            description: 'The provided URL is invalid',\n          },\n          {\n            name: 'RuleAlreadyExists',\n            description: 'A rule for this URL/domain already exists',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSafelinkDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.defs',\n    defs: {\n      event: {\n        type: 'object',\n        description: 'An event for URL safety decisions',\n        required: [\n          'id',\n          'eventType',\n          'url',\n          'pattern',\n          'action',\n          'reason',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n            description: 'Auto-incrementing row ID',\n          },\n          eventType: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#eventType',\n          },\n          url: {\n            type: 'string',\n            description: 'The URL that this rule applies to',\n          },\n          pattern: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#patternType',\n          },\n          action: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#actionType',\n          },\n          reason: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#reasonType',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who created this rule',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          comment: {\n            type: 'string',\n            description: 'Optional comment about the decision',\n          },\n        },\n      },\n      eventType: {\n        type: 'string',\n        knownValues: ['addRule', 'updateRule', 'removeRule'],\n      },\n      patternType: {\n        type: 'string',\n        knownValues: ['domain', 'url'],\n      },\n      actionType: {\n        type: 'string',\n        knownValues: ['block', 'warn', 'whitelist'],\n      },\n      reasonType: {\n        type: 'string',\n        knownValues: ['csam', 'spam', 'phishing', 'none'],\n      },\n      urlRule: {\n        type: 'object',\n        description: 'Input for creating a URL safety rule',\n        required: [\n          'url',\n          'pattern',\n          'action',\n          'reason',\n          'createdBy',\n          'createdAt',\n          'updatedAt',\n        ],\n        properties: {\n          url: {\n            type: 'string',\n            description: 'The URL or domain to apply the rule to',\n          },\n          pattern: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#patternType',\n          },\n          action: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#actionType',\n          },\n          reason: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#reasonType',\n          },\n          comment: {\n            type: 'string',\n            description: 'Optional comment about the decision',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user added the rule.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when the rule was created',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when the rule was last updated',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkQueryEvents: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.queryEvents',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Query URL safety audit events',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              urls: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by specific URLs or domains',\n              },\n              patternType: {\n                type: 'string',\n                description: 'Filter by pattern type',\n              },\n              sortDirection: {\n                type: 'string',\n                knownValues: ['asc', 'desc'],\n                default: 'desc',\n                description: 'Sort direction',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n                description:\n                  'Next cursor for pagination. Only present if there are more results.',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.safelink.defs#event',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkQueryRules: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.queryRules',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Query URL safety rules',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              urls: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by specific URLs or domains',\n              },\n              patternType: {\n                type: 'string',\n                description: 'Filter by pattern type',\n              },\n              actions: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by action types',\n              },\n              reason: {\n                type: 'string',\n                description: 'Filter by reason type',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'Filter by rule creator',\n              },\n              sortDirection: {\n                type: 'string',\n                knownValues: ['asc', 'desc'],\n                default: 'desc',\n                description: 'Sort direction',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['rules'],\n            properties: {\n              cursor: {\n                type: 'string',\n                description:\n                  'Next cursor for pagination. Only present if there are more results.',\n              },\n              rules: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.safelink.defs#urlRule',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkRemoveRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.removeRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Remove an existing URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to remove the rule for',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  'Optional comment about why the rule is being removed',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Optional DID of the user. Only respected when using admin auth.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'RuleNotFound',\n            description: 'No active rule found for this URL/domain',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSafelinkUpdateRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.updateRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Update an existing URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern', 'action', 'reason'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to update the rule for',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              action: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#actionType',\n              },\n              reason: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#reasonType',\n              },\n              comment: {\n                type: 'string',\n                description: 'Optional comment about the update',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Optional DID to credit as the creator. Only respected for admin_token authentication.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'RuleNotFound',\n            description: 'No active rule found for this URL/domain',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneServerGetConfig: {\n    lexicon: 1,\n    id: 'tools.ozone.server.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: \"Get details about ozone's server configuration.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              appview: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              pds: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              blobDivert: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              chat: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              viewer: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#viewerConfig',\n              },\n              verifierDid: {\n                type: 'string',\n                format: 'did',\n                description: 'The did of the verifier used for verification.',\n              },\n            },\n          },\n        },\n      },\n      serviceConfig: {\n        type: 'object',\n        properties: {\n          url: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      viewerConfig: {\n        type: 'object',\n        properties: {\n          role: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetAddValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.addValues',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name', 'values'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to add values to',\n              },\n              values: {\n                type: 'array',\n                minLength: 1,\n                maxLength: 1000,\n                items: {\n                  type: 'string',\n                },\n                description: 'Array of string values to add to the set',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.set.defs',\n    defs: {\n      set: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            minLength: 3,\n            maxLength: 128,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n        },\n      },\n      setView: {\n        type: 'object',\n        required: ['name', 'setSize', 'createdAt', 'updatedAt'],\n        properties: {\n          name: {\n            type: 'string',\n            minLength: 3,\n            maxLength: 128,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n          setSize: {\n            type: 'integer',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetDeleteSet: {\n    lexicon: 1,\n    id: 'tools.ozone.set.deleteSet',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Delete an entire set. Attempting to delete a set that does not exist will result in an error.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to delete',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetDeleteValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.deleteValues',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name', 'values'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to delete values from',\n              },\n              values: {\n                type: 'array',\n                minLength: 1,\n                items: {\n                  type: 'string',\n                },\n                description: 'Array of string values to delete from the set',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetGetValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.getValues',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a specific set and its values',\n        parameters: {\n          type: 'params',\n          required: ['name'],\n          properties: {\n            name: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['set', 'values'],\n            properties: {\n              set: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.set.defs#setView',\n              },\n              values: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              cursor: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetQuerySets: {\n    lexicon: 1,\n    id: 'tools.ozone.set.querySets',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Query available sets',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            namePrefix: {\n              type: 'string',\n            },\n            sortBy: {\n              type: 'string',\n              enum: ['name', 'createdAt', 'updatedAt'],\n              default: 'name',\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'asc',\n              enum: ['asc', 'desc'],\n              description: 'Defaults to ascending order of name field.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sets'],\n            properties: {\n              sets: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.set.defs#setView',\n                },\n              },\n              cursor: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetUpsertSet: {\n    lexicon: 1,\n    id: 'tools.ozone.set.upsertSet',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create or update set metadata',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.set.defs#set',\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.set.defs#setView',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.defs',\n    defs: {\n      option: {\n        type: 'object',\n        required: [\n          'key',\n          'value',\n          'did',\n          'scope',\n          'createdBy',\n          'lastUpdatedBy',\n        ],\n        properties: {\n          key: {\n            type: 'string',\n            format: 'nsid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          value: {\n            type: 'unknown',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          managerRole: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n          scope: {\n            type: 'string',\n            knownValues: ['instance', 'personal'],\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingListOptions: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.listOptions',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List settings with optional filtering',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            scope: {\n              type: 'string',\n              knownValues: ['instance', 'personal'],\n              default: 'instance',\n            },\n            prefix: {\n              type: 'string',\n              description: 'Filter keys by prefix',\n            },\n            keys: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n              description:\n                'Filter for only the specified keys. Ignored if prefix is provided',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['options'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              options: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.setting.defs#option',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingRemoveOptions: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.removeOptions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete settings by key',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['keys', 'scope'],\n            properties: {\n              keys: {\n                type: 'array',\n                minLength: 1,\n                maxLength: 200,\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              scope: {\n                type: 'string',\n                knownValues: ['instance', 'personal'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingUpsertOption: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.upsertOption',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create or update setting option',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['key', 'scope', 'value'],\n            properties: {\n              key: {\n                type: 'string',\n                format: 'nsid',\n              },\n              scope: {\n                type: 'string',\n                knownValues: ['instance', 'personal'],\n              },\n              value: {\n                type: 'unknown',\n              },\n              description: {\n                type: 'string',\n                maxLength: 2000,\n              },\n              managerRole: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleTriage',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleAdmin',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['option'],\n            properties: {\n              option: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.setting.defs#option',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.defs',\n    defs: {\n      sigDetail: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureFindCorrelation: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.findCorrelation',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find all correlated threat signatures between 2 or more accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['details'],\n            properties: {\n              details: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.signature.defs#sigDetail',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureFindRelatedAccounts: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.findRelatedAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get accounts that share some matching threat signatures with the root account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.signature.findRelatedAccounts#relatedAccount',\n                },\n              },\n            },\n          },\n        },\n      },\n      relatedAccount: {\n        type: 'object',\n        required: ['account'],\n        properties: {\n          account: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n          similarities: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.signature.defs#sigDetail',\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureSearchAccounts: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Search for accounts that match one or more threat signature values.',\n        parameters: {\n          type: 'params',\n          required: ['values'],\n          properties: {\n            values: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneTeamAddMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.addMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a member to the ozone team. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'role'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              role: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleAdmin',\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleTriage',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.team.defs#member',\n          },\n        },\n        errors: [\n          {\n            name: 'MemberAlreadyExists',\n            description: 'Member already exists in the team.',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneTeamDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.team.defs',\n    defs: {\n      member: {\n        type: 'object',\n        required: ['did', 'role'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          profile: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n          },\n          role: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n        },\n      },\n      roleAdmin: {\n        type: 'token',\n        description:\n          'Admin role. Highest level of access, can perform all actions.',\n      },\n      roleModerator: {\n        type: 'token',\n        description: 'Moderator role. Can perform most actions.',\n      },\n      roleTriage: {\n        type: 'token',\n        description:\n          'Triage role. Mostly intended for monitoring and escalating issues.',\n      },\n      roleVerifier: {\n        type: 'token',\n        description: 'Verifier role. Only allowed to issue verifications.',\n      },\n    },\n  },\n  ToolsOzoneTeamDeleteMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.deleteMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a member from ozone team. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'MemberNotFound',\n            description: 'The member being deleted does not exist',\n          },\n          {\n            name: 'CannotDeleteSelf',\n            description: 'You can not delete yourself from the team',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneTeamListMembers: {\n    lexicon: 1,\n    id: 'tools.ozone.team.listMembers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all members with access to the ozone service.',\n        parameters: {\n          type: 'params',\n          properties: {\n            q: {\n              type: 'string',\n            },\n            disabled: {\n              type: 'boolean',\n            },\n            roles: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['members'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              members: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.team.defs#member',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneTeamUpdateMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.updateMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update a member in the ozone service. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              disabled: {\n                type: 'boolean',\n              },\n              role: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleAdmin',\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleTriage',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.team.defs#member',\n          },\n        },\n        errors: [\n          {\n            name: 'MemberNotFound',\n            description: 'The member being updated does not exist in the team',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneVerificationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.defs',\n    defs: {\n      verificationView: {\n        type: 'object',\n        description: 'Verification data for the associated subject.',\n        required: [\n          'issuer',\n          'uri',\n          'subject',\n          'handle',\n          'displayName',\n          'createdAt',\n        ],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'string',\n            format: 'did',\n            description: 'The subject of the verification.',\n          },\n          handle: {\n            type: 'string',\n            description:\n              'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            description:\n              'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n          revokeReason: {\n            type: 'string',\n            description:\n              'Describes the reason for revocation, also indicating that the verification is no longer valid.',\n          },\n          revokedAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was revoked.',\n            format: 'datetime',\n          },\n          revokedBy: {\n            type: 'string',\n            description: 'The user who revoked this verification.',\n            format: 'did',\n          },\n          subjectProfile: {\n            type: 'union',\n            refs: [],\n          },\n          issuerProfile: {\n            type: 'union',\n            refs: [],\n          },\n          subjectRepo: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoViewDetail',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n            ],\n          },\n          issuerRepo: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoViewDetail',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationGrantVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.grantVerifications',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Grant verifications to multiple subjects. Allows batch processing of up to 100 verifications at once.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications'],\n            properties: {\n              verifications: {\n                type: 'array',\n                description: 'Array of verification requests to process',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.grantVerifications#verificationInput',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications', 'failedVerifications'],\n            properties: {\n              verifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.defs#verificationView',\n                },\n              },\n              failedVerifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.grantVerifications#grantError',\n                },\n              },\n            },\n          },\n        },\n      },\n      verificationInput: {\n        type: 'object',\n        required: ['subject', 'handle', 'displayName'],\n        properties: {\n          subject: {\n            type: 'string',\n            description: 'The did of the subject being verified',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            description:\n              'Handle of the subject the verification applies to at the moment of verifying.',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            description:\n              'Display name of the subject the verification applies to at the moment of verifying.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp for verification record. Defaults to current time when not specified.',\n          },\n        },\n      },\n      grantError: {\n        type: 'object',\n        description: 'Error object for failed verifications.',\n        required: ['error', 'subject'],\n        properties: {\n          error: {\n            type: 'string',\n            description: 'Error message describing the reason for failure.',\n          },\n          subject: {\n            type: 'string',\n            description: 'The did of the subject being verified',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationListVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.listVerifications',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List verifications',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'string',\n              description: 'Pagination cursor',\n            },\n            limit: {\n              type: 'integer',\n              description: 'Maximum number of results to return',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            createdAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Filter to verifications created after this timestamp',\n            },\n            createdBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Filter to verifications created before this timestamp',\n            },\n            issuers: {\n              type: 'array',\n              maxLength: 100,\n              description: 'Filter to verifications from specific issuers',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            subjects: {\n              type: 'array',\n              description: 'Filter to specific verified DIDs',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            sortDirection: {\n              type: 'string',\n              description: 'Sort direction for creation date',\n              enum: ['asc', 'desc'],\n              default: 'desc',\n            },\n            isRevoked: {\n              type: 'boolean',\n              description:\n                'Filter to verifications that are revoked or not. By default, includes both.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              verifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.defs#verificationView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationRevokeVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.revokeVerifications',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke previously granted verifications in batches of up to 100.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uris'],\n            properties: {\n              uris: {\n                type: 'array',\n                description: 'Array of verification record uris to revoke',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  description:\n                    'The AT-URI of the verification record to revoke.',\n                  format: 'at-uri',\n                },\n              },\n              revokeReason: {\n                type: 'string',\n                description:\n                  'Reason for revoking the verification. This is optional and can be omitted if not needed.',\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['revokedVerifications', 'failedRevocations'],\n            properties: {\n              revokedVerifications: {\n                type: 'array',\n                description: 'List of verification uris successfully revoked',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n              failedRevocations: {\n                type: 'array',\n                description:\n                  \"List of verification uris that couldn't be revoked, including failure reasons\",\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.revokeVerifications#revokeError',\n                },\n              },\n            },\n          },\n        },\n      },\n      revokeError: {\n        type: 'object',\n        description: 'Error object for failed revocations',\n        required: ['uri', 'error'],\n        properties: {\n          uri: {\n            type: 'string',\n            description:\n              'The AT-URI of the verification record that failed to revoke.',\n            format: 'at-uri',\n          },\n          error: {\n            type: 'string',\n            description:\n              'Description of the error that occurred during revocation.',\n          },\n        },\n      },\n    },\n  },\n} as const satisfies Record<string, LexiconDoc>\nexport const schemas = Object.values(schemaDict) satisfies LexiconDoc[]\nexport const lexicons: Lexicons = new Lexicons(schemas)\n\nexport function validate<T extends { $type: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType: true,\n): ValidationResult<T>\nexport function validate<T extends { $type?: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: false,\n): ValidationResult<T>\nexport function validate(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: boolean,\n): ValidationResult {\n  return (requiredType ? is$typed : maybe$typed)(v, id, hash)\n    ? lexicons.validate(`${id}#${hash}`, v)\n    : {\n        success: false,\n        error: new ValidationError(\n          `Must be an object with \"${hash === 'main' ? id : `${id}#${hash}`}\" $type property`,\n        ),\n      }\n}\n\nexport const ids = {\n  AppBskyActorDefs: 'app.bsky.actor.defs',\n  AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences',\n  AppBskyActorGetProfile: 'app.bsky.actor.getProfile',\n  AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles',\n  AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions',\n  AppBskyActorProfile: 'app.bsky.actor.profile',\n  AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',\n  AppBskyActorSearchActors: 'app.bsky.actor.searchActors',\n  AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',\n  AppBskyActorStatus: 'app.bsky.actor.status',\n  AppBskyAgeassuranceBegin: 'app.bsky.ageassurance.begin',\n  AppBskyAgeassuranceDefs: 'app.bsky.ageassurance.defs',\n  AppBskyAgeassuranceGetConfig: 'app.bsky.ageassurance.getConfig',\n  AppBskyAgeassuranceGetState: 'app.bsky.ageassurance.getState',\n  AppBskyBookmarkCreateBookmark: 'app.bsky.bookmark.createBookmark',\n  AppBskyBookmarkDefs: 'app.bsky.bookmark.defs',\n  AppBskyBookmarkDeleteBookmark: 'app.bsky.bookmark.deleteBookmark',\n  AppBskyBookmarkGetBookmarks: 'app.bsky.bookmark.getBookmarks',\n  AppBskyContactDefs: 'app.bsky.contact.defs',\n  AppBskyContactDismissMatch: 'app.bsky.contact.dismissMatch',\n  AppBskyContactGetMatches: 'app.bsky.contact.getMatches',\n  AppBskyContactGetSyncStatus: 'app.bsky.contact.getSyncStatus',\n  AppBskyContactImportContacts: 'app.bsky.contact.importContacts',\n  AppBskyContactRemoveData: 'app.bsky.contact.removeData',\n  AppBskyContactSendNotification: 'app.bsky.contact.sendNotification',\n  AppBskyContactStartPhoneVerification:\n    'app.bsky.contact.startPhoneVerification',\n  AppBskyContactVerifyPhone: 'app.bsky.contact.verifyPhone',\n  AppBskyDraftCreateDraft: 'app.bsky.draft.createDraft',\n  AppBskyDraftDefs: 'app.bsky.draft.defs',\n  AppBskyDraftDeleteDraft: 'app.bsky.draft.deleteDraft',\n  AppBskyDraftGetDrafts: 'app.bsky.draft.getDrafts',\n  AppBskyDraftUpdateDraft: 'app.bsky.draft.updateDraft',\n  AppBskyEmbedDefs: 'app.bsky.embed.defs',\n  AppBskyEmbedExternal: 'app.bsky.embed.external',\n  AppBskyEmbedImages: 'app.bsky.embed.images',\n  AppBskyEmbedRecord: 'app.bsky.embed.record',\n  AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',\n  AppBskyEmbedVideo: 'app.bsky.embed.video',\n  AppBskyFeedDefs: 'app.bsky.feed.defs',\n  AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator',\n  AppBskyFeedGenerator: 'app.bsky.feed.generator',\n  AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds',\n  AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes',\n  AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',\n  AppBskyFeedGetFeed: 'app.bsky.feed.getFeed',\n  AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator',\n  AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators',\n  AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton',\n  AppBskyFeedGetLikes: 'app.bsky.feed.getLikes',\n  AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed',\n  AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',\n  AppBskyFeedGetPosts: 'app.bsky.feed.getPosts',\n  AppBskyFeedGetQuotes: 'app.bsky.feed.getQuotes',\n  AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',\n  AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds',\n  AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline',\n  AppBskyFeedLike: 'app.bsky.feed.like',\n  AppBskyFeedPost: 'app.bsky.feed.post',\n  AppBskyFeedPostgate: 'app.bsky.feed.postgate',\n  AppBskyFeedRepost: 'app.bsky.feed.repost',\n  AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts',\n  AppBskyFeedSendInteractions: 'app.bsky.feed.sendInteractions',\n  AppBskyFeedThreadgate: 'app.bsky.feed.threadgate',\n  AppBskyGraphBlock: 'app.bsky.graph.block',\n  AppBskyGraphDefs: 'app.bsky.graph.defs',\n  AppBskyGraphFollow: 'app.bsky.graph.follow',\n  AppBskyGraphGetActorStarterPacks: 'app.bsky.graph.getActorStarterPacks',\n  AppBskyGraphGetBlocks: 'app.bsky.graph.getBlocks',\n  AppBskyGraphGetFollowers: 'app.bsky.graph.getFollowers',\n  AppBskyGraphGetFollows: 'app.bsky.graph.getFollows',\n  AppBskyGraphGetKnownFollowers: 'app.bsky.graph.getKnownFollowers',\n  AppBskyGraphGetList: 'app.bsky.graph.getList',\n  AppBskyGraphGetListBlocks: 'app.bsky.graph.getListBlocks',\n  AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes',\n  AppBskyGraphGetLists: 'app.bsky.graph.getLists',\n  AppBskyGraphGetListsWithMembership: 'app.bsky.graph.getListsWithMembership',\n  AppBskyGraphGetMutes: 'app.bsky.graph.getMutes',\n  AppBskyGraphGetRelationships: 'app.bsky.graph.getRelationships',\n  AppBskyGraphGetStarterPack: 'app.bsky.graph.getStarterPack',\n  AppBskyGraphGetStarterPacks: 'app.bsky.graph.getStarterPacks',\n  AppBskyGraphGetStarterPacksWithMembership:\n    'app.bsky.graph.getStarterPacksWithMembership',\n  AppBskyGraphGetSuggestedFollowsByActor:\n    'app.bsky.graph.getSuggestedFollowsByActor',\n  AppBskyGraphList: 'app.bsky.graph.list',\n  AppBskyGraphListblock: 'app.bsky.graph.listblock',\n  AppBskyGraphListitem: 'app.bsky.graph.listitem',\n  AppBskyGraphMuteActor: 'app.bsky.graph.muteActor',\n  AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList',\n  AppBskyGraphMuteThread: 'app.bsky.graph.muteThread',\n  AppBskyGraphSearchStarterPacks: 'app.bsky.graph.searchStarterPacks',\n  AppBskyGraphStarterpack: 'app.bsky.graph.starterpack',\n  AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor',\n  AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList',\n  AppBskyGraphUnmuteThread: 'app.bsky.graph.unmuteThread',\n  AppBskyGraphVerification: 'app.bsky.graph.verification',\n  AppBskyLabelerDefs: 'app.bsky.labeler.defs',\n  AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',\n  AppBskyLabelerService: 'app.bsky.labeler.service',\n  AppBskyNotificationDeclaration: 'app.bsky.notification.declaration',\n  AppBskyNotificationDefs: 'app.bsky.notification.defs',\n  AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',\n  AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',\n  AppBskyNotificationListActivitySubscriptions:\n    'app.bsky.notification.listActivitySubscriptions',\n  AppBskyNotificationListNotifications:\n    'app.bsky.notification.listNotifications',\n  AppBskyNotificationPutActivitySubscription:\n    'app.bsky.notification.putActivitySubscription',\n  AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',\n  AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',\n  AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',\n  AppBskyNotificationUnregisterPush: 'app.bsky.notification.unregisterPush',\n  AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',\n  AppBskyRichtextFacet: 'app.bsky.richtext.facet',\n  AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',\n  AppBskyUnspeccedGetAgeAssuranceState:\n    'app.bsky.unspecced.getAgeAssuranceState',\n  AppBskyUnspeccedGetConfig: 'app.bsky.unspecced.getConfig',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetPopularFeedGenerators:\n    'app.bsky.unspecced.getPopularFeedGenerators',\n  AppBskyUnspeccedGetPostThreadOtherV2:\n    'app.bsky.unspecced.getPostThreadOtherV2',\n  AppBskyUnspeccedGetPostThreadV2: 'app.bsky.unspecced.getPostThreadV2',\n  AppBskyUnspeccedGetSuggestedFeeds: 'app.bsky.unspecced.getSuggestedFeeds',\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton:\n    'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n  AppBskyUnspeccedGetSuggestedOnboardingUsers:\n    'app.bsky.unspecced.getSuggestedOnboardingUsers',\n  AppBskyUnspeccedGetSuggestedStarterPacks:\n    'app.bsky.unspecced.getSuggestedStarterPacks',\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetSuggestedUsers: 'app.bsky.unspecced.getSuggestedUsers',\n  AppBskyUnspeccedGetSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetSuggestionsSkeleton:\n    'app.bsky.unspecced.getSuggestionsSkeleton',\n  AppBskyUnspeccedGetTaggedSuggestions:\n    'app.bsky.unspecced.getTaggedSuggestions',\n  AppBskyUnspeccedGetTrendingTopics: 'app.bsky.unspecced.getTrendingTopics',\n  AppBskyUnspeccedGetTrends: 'app.bsky.unspecced.getTrends',\n  AppBskyUnspeccedGetTrendsSkeleton: 'app.bsky.unspecced.getTrendsSkeleton',\n  AppBskyUnspeccedInitAgeAssurance: 'app.bsky.unspecced.initAgeAssurance',\n  AppBskyUnspeccedSearchActorsSkeleton:\n    'app.bsky.unspecced.searchActorsSkeleton',\n  AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton',\n  AppBskyUnspeccedSearchStarterPacksSkeleton:\n    'app.bsky.unspecced.searchStarterPacksSkeleton',\n  AppBskyVideoDefs: 'app.bsky.video.defs',\n  AppBskyVideoGetJobStatus: 'app.bsky.video.getJobStatus',\n  AppBskyVideoGetUploadLimits: 'app.bsky.video.getUploadLimits',\n  AppBskyVideoUploadVideo: 'app.bsky.video.uploadVideo',\n  ChatBskyActorDeclaration: 'chat.bsky.actor.declaration',\n  ChatBskyActorDefs: 'chat.bsky.actor.defs',\n  ChatBskyActorDeleteAccount: 'chat.bsky.actor.deleteAccount',\n  ChatBskyActorExportAccountData: 'chat.bsky.actor.exportAccountData',\n  ChatBskyConvoAcceptConvo: 'chat.bsky.convo.acceptConvo',\n  ChatBskyConvoAddReaction: 'chat.bsky.convo.addReaction',\n  ChatBskyConvoDefs: 'chat.bsky.convo.defs',\n  ChatBskyConvoDeleteMessageForSelf: 'chat.bsky.convo.deleteMessageForSelf',\n  ChatBskyConvoGetConvo: 'chat.bsky.convo.getConvo',\n  ChatBskyConvoGetConvoAvailability: 'chat.bsky.convo.getConvoAvailability',\n  ChatBskyConvoGetConvoForMembers: 'chat.bsky.convo.getConvoForMembers',\n  ChatBskyConvoGetLog: 'chat.bsky.convo.getLog',\n  ChatBskyConvoGetMessages: 'chat.bsky.convo.getMessages',\n  ChatBskyConvoLeaveConvo: 'chat.bsky.convo.leaveConvo',\n  ChatBskyConvoListConvos: 'chat.bsky.convo.listConvos',\n  ChatBskyConvoMuteConvo: 'chat.bsky.convo.muteConvo',\n  ChatBskyConvoRemoveReaction: 'chat.bsky.convo.removeReaction',\n  ChatBskyConvoSendMessage: 'chat.bsky.convo.sendMessage',\n  ChatBskyConvoSendMessageBatch: 'chat.bsky.convo.sendMessageBatch',\n  ChatBskyConvoUnmuteConvo: 'chat.bsky.convo.unmuteConvo',\n  ChatBskyConvoUpdateAllRead: 'chat.bsky.convo.updateAllRead',\n  ChatBskyConvoUpdateRead: 'chat.bsky.convo.updateRead',\n  ChatBskyModerationGetActorMetadata: 'chat.bsky.moderation.getActorMetadata',\n  ChatBskyModerationGetMessageContext: 'chat.bsky.moderation.getMessageContext',\n  ChatBskyModerationUpdateActorAccess: 'chat.bsky.moderation.updateActorAccess',\n  ComAtprotoAdminDefs: 'com.atproto.admin.defs',\n  ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount',\n  ComAtprotoAdminDisableAccountInvites:\n    'com.atproto.admin.disableAccountInvites',\n  ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',\n  ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites',\n  ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo',\n  ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos',\n  ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes',\n  ComAtprotoAdminGetSubjectStatus: 'com.atproto.admin.getSubjectStatus',\n  ComAtprotoAdminSearchAccounts: 'com.atproto.admin.searchAccounts',\n  ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail',\n  ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail',\n  ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle',\n  ComAtprotoAdminUpdateAccountPassword:\n    'com.atproto.admin.updateAccountPassword',\n  ComAtprotoAdminUpdateAccountSigningKey:\n    'com.atproto.admin.updateAccountSigningKey',\n  ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus',\n  ComAtprotoIdentityDefs: 'com.atproto.identity.defs',\n  ComAtprotoIdentityGetRecommendedDidCredentials:\n    'com.atproto.identity.getRecommendedDidCredentials',\n  ComAtprotoIdentityRefreshIdentity: 'com.atproto.identity.refreshIdentity',\n  ComAtprotoIdentityRequestPlcOperationSignature:\n    'com.atproto.identity.requestPlcOperationSignature',\n  ComAtprotoIdentityResolveDid: 'com.atproto.identity.resolveDid',\n  ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle',\n  ComAtprotoIdentityResolveIdentity: 'com.atproto.identity.resolveIdentity',\n  ComAtprotoIdentitySignPlcOperation: 'com.atproto.identity.signPlcOperation',\n  ComAtprotoIdentitySubmitPlcOperation:\n    'com.atproto.identity.submitPlcOperation',\n  ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle',\n  ComAtprotoLabelDefs: 'com.atproto.label.defs',\n  ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels',\n  ComAtprotoLabelSubscribeLabels: 'com.atproto.label.subscribeLabels',\n  ComAtprotoLexiconResolveLexicon: 'com.atproto.lexicon.resolveLexicon',\n  ComAtprotoLexiconSchema: 'com.atproto.lexicon.schema',\n  ComAtprotoModerationCreateReport: 'com.atproto.moderation.createReport',\n  ComAtprotoModerationDefs: 'com.atproto.moderation.defs',\n  ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites',\n  ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord',\n  ComAtprotoRepoDefs: 'com.atproto.repo.defs',\n  ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord',\n  ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo',\n  ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',\n  ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo',\n  ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs',\n  ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',\n  ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',\n  ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',\n  ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',\n  ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount',\n  ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus',\n  ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',\n  ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',\n  ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',\n  ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',\n  ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes',\n  ComAtprotoServerCreateSession: 'com.atproto.server.createSession',\n  ComAtprotoServerDeactivateAccount: 'com.atproto.server.deactivateAccount',\n  ComAtprotoServerDefs: 'com.atproto.server.defs',\n  ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount',\n  ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession',\n  ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer',\n  ComAtprotoServerGetAccountInviteCodes:\n    'com.atproto.server.getAccountInviteCodes',\n  ComAtprotoServerGetServiceAuth: 'com.atproto.server.getServiceAuth',\n  ComAtprotoServerGetSession: 'com.atproto.server.getSession',\n  ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords',\n  ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',\n  ComAtprotoServerRequestAccountDelete:\n    'com.atproto.server.requestAccountDelete',\n  ComAtprotoServerRequestEmailConfirmation:\n    'com.atproto.server.requestEmailConfirmation',\n  ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',\n  ComAtprotoServerRequestPasswordReset:\n    'com.atproto.server.requestPasswordReset',\n  ComAtprotoServerReserveSigningKey: 'com.atproto.server.reserveSigningKey',\n  ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',\n  ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',\n  ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',\n  ComAtprotoSyncDefs: 'com.atproto.sync.defs',\n  ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',\n  ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',\n  ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',\n  ComAtprotoSyncGetHead: 'com.atproto.sync.getHead',\n  ComAtprotoSyncGetHostStatus: 'com.atproto.sync.getHostStatus',\n  ComAtprotoSyncGetLatestCommit: 'com.atproto.sync.getLatestCommit',\n  ComAtprotoSyncGetRecord: 'com.atproto.sync.getRecord',\n  ComAtprotoSyncGetRepo: 'com.atproto.sync.getRepo',\n  ComAtprotoSyncGetRepoStatus: 'com.atproto.sync.getRepoStatus',\n  ComAtprotoSyncListBlobs: 'com.atproto.sync.listBlobs',\n  ComAtprotoSyncListHosts: 'com.atproto.sync.listHosts',\n  ComAtprotoSyncListRepos: 'com.atproto.sync.listRepos',\n  ComAtprotoSyncListReposByCollection: 'com.atproto.sync.listReposByCollection',\n  ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate',\n  ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',\n  ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',\n  ComAtprotoTempAddReservedHandle: 'com.atproto.temp.addReservedHandle',\n  ComAtprotoTempCheckHandleAvailability:\n    'com.atproto.temp.checkHandleAvailability',\n  ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue',\n  ComAtprotoTempDereferenceScope: 'com.atproto.temp.dereferenceScope',\n  ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels',\n  ComAtprotoTempRequestPhoneVerification:\n    'com.atproto.temp.requestPhoneVerification',\n  ComAtprotoTempRevokeAccountCredentials:\n    'com.atproto.temp.revokeAccountCredentials',\n  ComGermnetworkDeclaration: 'com.germnetwork.declaration',\n  ToolsOzoneCommunicationCreateTemplate:\n    'tools.ozone.communication.createTemplate',\n  ToolsOzoneCommunicationDefs: 'tools.ozone.communication.defs',\n  ToolsOzoneCommunicationDeleteTemplate:\n    'tools.ozone.communication.deleteTemplate',\n  ToolsOzoneCommunicationListTemplates:\n    'tools.ozone.communication.listTemplates',\n  ToolsOzoneCommunicationUpdateTemplate:\n    'tools.ozone.communication.updateTemplate',\n  ToolsOzoneHostingGetAccountHistory: 'tools.ozone.hosting.getAccountHistory',\n  ToolsOzoneModerationCancelScheduledActions:\n    'tools.ozone.moderation.cancelScheduledActions',\n  ToolsOzoneModerationDefs: 'tools.ozone.moderation.defs',\n  ToolsOzoneModerationEmitEvent: 'tools.ozone.moderation.emitEvent',\n  ToolsOzoneModerationGetAccountTimeline:\n    'tools.ozone.moderation.getAccountTimeline',\n  ToolsOzoneModerationGetEvent: 'tools.ozone.moderation.getEvent',\n  ToolsOzoneModerationGetRecord: 'tools.ozone.moderation.getRecord',\n  ToolsOzoneModerationGetRecords: 'tools.ozone.moderation.getRecords',\n  ToolsOzoneModerationGetRepo: 'tools.ozone.moderation.getRepo',\n  ToolsOzoneModerationGetReporterStats:\n    'tools.ozone.moderation.getReporterStats',\n  ToolsOzoneModerationGetRepos: 'tools.ozone.moderation.getRepos',\n  ToolsOzoneModerationGetSubjects: 'tools.ozone.moderation.getSubjects',\n  ToolsOzoneModerationListScheduledActions:\n    'tools.ozone.moderation.listScheduledActions',\n  ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents',\n  ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses',\n  ToolsOzoneModerationScheduleAction: 'tools.ozone.moderation.scheduleAction',\n  ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos',\n  ToolsOzoneReportDefs: 'tools.ozone.report.defs',\n  ToolsOzoneSafelinkAddRule: 'tools.ozone.safelink.addRule',\n  ToolsOzoneSafelinkDefs: 'tools.ozone.safelink.defs',\n  ToolsOzoneSafelinkQueryEvents: 'tools.ozone.safelink.queryEvents',\n  ToolsOzoneSafelinkQueryRules: 'tools.ozone.safelink.queryRules',\n  ToolsOzoneSafelinkRemoveRule: 'tools.ozone.safelink.removeRule',\n  ToolsOzoneSafelinkUpdateRule: 'tools.ozone.safelink.updateRule',\n  ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig',\n  ToolsOzoneSetAddValues: 'tools.ozone.set.addValues',\n  ToolsOzoneSetDefs: 'tools.ozone.set.defs',\n  ToolsOzoneSetDeleteSet: 'tools.ozone.set.deleteSet',\n  ToolsOzoneSetDeleteValues: 'tools.ozone.set.deleteValues',\n  ToolsOzoneSetGetValues: 'tools.ozone.set.getValues',\n  ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets',\n  ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet',\n  ToolsOzoneSettingDefs: 'tools.ozone.setting.defs',\n  ToolsOzoneSettingListOptions: 'tools.ozone.setting.listOptions',\n  ToolsOzoneSettingRemoveOptions: 'tools.ozone.setting.removeOptions',\n  ToolsOzoneSettingUpsertOption: 'tools.ozone.setting.upsertOption',\n  ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs',\n  ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation',\n  ToolsOzoneSignatureFindRelatedAccounts:\n    'tools.ozone.signature.findRelatedAccounts',\n  ToolsOzoneSignatureSearchAccounts: 'tools.ozone.signature.searchAccounts',\n  ToolsOzoneTeamAddMember: 'tools.ozone.team.addMember',\n  ToolsOzoneTeamDefs: 'tools.ozone.team.defs',\n  ToolsOzoneTeamDeleteMember: 'tools.ozone.team.deleteMember',\n  ToolsOzoneTeamListMembers: 'tools.ozone.team.listMembers',\n  ToolsOzoneTeamUpdateMember: 'tools.ozone.team.updateMember',\n  ToolsOzoneVerificationDefs: 'tools.ozone.verification.defs',\n  ToolsOzoneVerificationGrantVerifications:\n    'tools.ozone.verification.grantVerifications',\n  ToolsOzoneVerificationListVerifications:\n    'tools.ozone.verification.listVerifications',\n  ToolsOzoneVerificationRevokeVerifications:\n    'tools.ozone.verification.revokeVerifications',\n} as const\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyNotificationDefs from '../notification/defs.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'app.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  createdAt?: string\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n\nexport interface ProfileView {\n  $type?: 'app.bsky.actor.defs#profileView'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  description?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileView = 'profileView'\n\nexport function isProfileView<V>(v: V) {\n  return is$typed(v, id, hashProfileView)\n}\n\nexport function validateProfileView<V>(v: V) {\n  return validate<ProfileView & V>(v, id, hashProfileView)\n}\n\nexport interface ProfileViewDetailed {\n  $type?: 'app.bsky.actor.defs#profileViewDetailed'\n  did: string\n  handle: string\n  displayName?: string\n  description?: string\n  pronouns?: string\n  website?: string\n  avatar?: string\n  banner?: string\n  followersCount?: number\n  followsCount?: number\n  postsCount?: number\n  associated?: ProfileAssociated\n  joinedViaStarterPack?: AppBskyGraphDefs.StarterPackViewBasic\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewDetailed = 'profileViewDetailed'\n\nexport function isProfileViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashProfileViewDetailed)\n}\n\nexport function validateProfileViewDetailed<V>(v: V) {\n  return validate<ProfileViewDetailed & V>(v, id, hashProfileViewDetailed)\n}\n\nexport interface ProfileAssociated {\n  $type?: 'app.bsky.actor.defs#profileAssociated'\n  lists?: number\n  feedgens?: number\n  starterPacks?: number\n  labeler?: boolean\n  chat?: ProfileAssociatedChat\n  activitySubscription?: ProfileAssociatedActivitySubscription\n  germ?: ProfileAssociatedGerm\n}\n\nconst hashProfileAssociated = 'profileAssociated'\n\nexport function isProfileAssociated<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociated)\n}\n\nexport function validateProfileAssociated<V>(v: V) {\n  return validate<ProfileAssociated & V>(v, id, hashProfileAssociated)\n}\n\nexport interface ProfileAssociatedChat {\n  $type?: 'app.bsky.actor.defs#profileAssociatedChat'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n}\n\nconst hashProfileAssociatedChat = 'profileAssociatedChat'\n\nexport function isProfileAssociatedChat<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedChat)\n}\n\nexport function validateProfileAssociatedChat<V>(v: V) {\n  return validate<ProfileAssociatedChat & V>(v, id, hashProfileAssociatedChat)\n}\n\nexport interface ProfileAssociatedGerm {\n  $type?: 'app.bsky.actor.defs#profileAssociatedGerm'\n  messageMeUrl: string\n  showButtonTo: 'usersIFollow' | 'everyone' | (string & {})\n}\n\nconst hashProfileAssociatedGerm = 'profileAssociatedGerm'\n\nexport function isProfileAssociatedGerm<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedGerm)\n}\n\nexport function validateProfileAssociatedGerm<V>(v: V) {\n  return validate<ProfileAssociatedGerm & V>(v, id, hashProfileAssociatedGerm)\n}\n\nexport interface ProfileAssociatedActivitySubscription {\n  $type?: 'app.bsky.actor.defs#profileAssociatedActivitySubscription'\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n}\n\nconst hashProfileAssociatedActivitySubscription =\n  'profileAssociatedActivitySubscription'\n\nexport function isProfileAssociatedActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedActivitySubscription)\n}\n\nexport function validateProfileAssociatedActivitySubscription<V>(v: V) {\n  return validate<ProfileAssociatedActivitySubscription & V>(\n    v,\n    id,\n    hashProfileAssociatedActivitySubscription,\n  )\n}\n\n/** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.actor.defs#viewerState'\n  muted?: boolean\n  mutedByList?: AppBskyGraphDefs.ListViewBasic\n  blockedBy?: boolean\n  blocking?: string\n  blockingByList?: AppBskyGraphDefs.ListViewBasic\n  following?: string\n  followedBy?: string\n  knownFollowers?: KnownFollowers\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** The subject's followers whom you also follow */\nexport interface KnownFollowers {\n  $type?: 'app.bsky.actor.defs#knownFollowers'\n  count: number\n  followers: ProfileViewBasic[]\n}\n\nconst hashKnownFollowers = 'knownFollowers'\n\nexport function isKnownFollowers<V>(v: V) {\n  return is$typed(v, id, hashKnownFollowers)\n}\n\nexport function validateKnownFollowers<V>(v: V) {\n  return validate<KnownFollowers & V>(v, id, hashKnownFollowers)\n}\n\n/** Represents the verification information about the user this object is attached to. */\nexport interface VerificationState {\n  $type?: 'app.bsky.actor.defs#verificationState'\n  /** All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included. */\n  verifications: VerificationView[]\n  /** The user's status as a verified account. */\n  verifiedStatus: 'valid' | 'invalid' | 'none' | (string & {})\n  /** The user's status as a trusted verifier. */\n  trustedVerifierStatus: 'valid' | 'invalid' | 'none' | (string & {})\n}\n\nconst hashVerificationState = 'verificationState'\n\nexport function isVerificationState<V>(v: V) {\n  return is$typed(v, id, hashVerificationState)\n}\n\nexport function validateVerificationState<V>(v: V) {\n  return validate<VerificationState & V>(v, id, hashVerificationState)\n}\n\n/** An individual verification for an associated subject. */\nexport interface VerificationView {\n  $type?: 'app.bsky.actor.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** True if the verification passes validation, otherwise false. */\n  isValid: boolean\n  /** Timestamp when the verification was created. */\n  createdAt: string\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n\nexport type Preferences = (\n  | $Typed<AdultContentPref>\n  | $Typed<ContentLabelPref>\n  | $Typed<SavedFeedsPref>\n  | $Typed<SavedFeedsPrefV2>\n  | $Typed<PersonalDetailsPref>\n  | $Typed<DeclaredAgePref>\n  | $Typed<FeedViewPref>\n  | $Typed<ThreadViewPref>\n  | $Typed<InterestsPref>\n  | $Typed<MutedWordsPref>\n  | $Typed<HiddenPostsPref>\n  | $Typed<BskyAppStatePref>\n  | $Typed<LabelersPref>\n  | $Typed<PostInteractionSettingsPref>\n  | $Typed<VerificationPrefs>\n  | $Typed<LiveEventPreferences>\n  | { $type: string }\n)[]\n\nexport interface AdultContentPref {\n  $type?: 'app.bsky.actor.defs#adultContentPref'\n  enabled: boolean\n}\n\nconst hashAdultContentPref = 'adultContentPref'\n\nexport function isAdultContentPref<V>(v: V) {\n  return is$typed(v, id, hashAdultContentPref)\n}\n\nexport function validateAdultContentPref<V>(v: V) {\n  return validate<AdultContentPref & V>(v, id, hashAdultContentPref)\n}\n\nexport interface ContentLabelPref {\n  $type?: 'app.bsky.actor.defs#contentLabelPref'\n  /** Which labeler does this preference apply to? If undefined, applies globally. */\n  labelerDid?: string\n  label: string\n  visibility: 'ignore' | 'show' | 'warn' | 'hide' | (string & {})\n}\n\nconst hashContentLabelPref = 'contentLabelPref'\n\nexport function isContentLabelPref<V>(v: V) {\n  return is$typed(v, id, hashContentLabelPref)\n}\n\nexport function validateContentLabelPref<V>(v: V) {\n  return validate<ContentLabelPref & V>(v, id, hashContentLabelPref)\n}\n\nexport interface SavedFeed {\n  $type?: 'app.bsky.actor.defs#savedFeed'\n  id: string\n  type: 'feed' | 'list' | 'timeline' | (string & {})\n  value: string\n  pinned: boolean\n}\n\nconst hashSavedFeed = 'savedFeed'\n\nexport function isSavedFeed<V>(v: V) {\n  return is$typed(v, id, hashSavedFeed)\n}\n\nexport function validateSavedFeed<V>(v: V) {\n  return validate<SavedFeed & V>(v, id, hashSavedFeed)\n}\n\nexport interface SavedFeedsPrefV2 {\n  $type?: 'app.bsky.actor.defs#savedFeedsPrefV2'\n  items: SavedFeed[]\n}\n\nconst hashSavedFeedsPrefV2 = 'savedFeedsPrefV2'\n\nexport function isSavedFeedsPrefV2<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPrefV2)\n}\n\nexport function validateSavedFeedsPrefV2<V>(v: V) {\n  return validate<SavedFeedsPrefV2 & V>(v, id, hashSavedFeedsPrefV2)\n}\n\nexport interface SavedFeedsPref {\n  $type?: 'app.bsky.actor.defs#savedFeedsPref'\n  pinned: string[]\n  saved: string[]\n  timelineIndex?: number\n}\n\nconst hashSavedFeedsPref = 'savedFeedsPref'\n\nexport function isSavedFeedsPref<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPref)\n}\n\nexport function validateSavedFeedsPref<V>(v: V) {\n  return validate<SavedFeedsPref & V>(v, id, hashSavedFeedsPref)\n}\n\nexport interface PersonalDetailsPref {\n  $type?: 'app.bsky.actor.defs#personalDetailsPref'\n  /** The birth date of account owner. */\n  birthDate?: string\n}\n\nconst hashPersonalDetailsPref = 'personalDetailsPref'\n\nexport function isPersonalDetailsPref<V>(v: V) {\n  return is$typed(v, id, hashPersonalDetailsPref)\n}\n\nexport function validatePersonalDetailsPref<V>(v: V) {\n  return validate<PersonalDetailsPref & V>(v, id, hashPersonalDetailsPref)\n}\n\n/** 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. */\nexport interface DeclaredAgePref {\n  $type?: 'app.bsky.actor.defs#declaredAgePref'\n  /** Indicates if the user has declared that they are over 13 years of age. */\n  isOverAge13?: boolean\n  /** Indicates if the user has declared that they are over 16 years of age. */\n  isOverAge16?: boolean\n  /** Indicates if the user has declared that they are over 18 years of age. */\n  isOverAge18?: boolean\n}\n\nconst hashDeclaredAgePref = 'declaredAgePref'\n\nexport function isDeclaredAgePref<V>(v: V) {\n  return is$typed(v, id, hashDeclaredAgePref)\n}\n\nexport function validateDeclaredAgePref<V>(v: V) {\n  return validate<DeclaredAgePref & V>(v, id, hashDeclaredAgePref)\n}\n\nexport interface FeedViewPref {\n  $type?: 'app.bsky.actor.defs#feedViewPref'\n  /** The URI of the feed, or an identifier which describes the feed. */\n  feed: string\n  /** Hide replies in the feed. */\n  hideReplies?: boolean\n  /** Hide replies in the feed if they are not by followed users. */\n  hideRepliesByUnfollowed: boolean\n  /** Hide replies in the feed if they do not have this number of likes. */\n  hideRepliesByLikeCount?: number\n  /** Hide reposts in the feed. */\n  hideReposts?: boolean\n  /** Hide quote posts in the feed. */\n  hideQuotePosts?: boolean\n}\n\nconst hashFeedViewPref = 'feedViewPref'\n\nexport function isFeedViewPref<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPref)\n}\n\nexport function validateFeedViewPref<V>(v: V) {\n  return validate<FeedViewPref & V>(v, id, hashFeedViewPref)\n}\n\nexport interface ThreadViewPref {\n  $type?: 'app.bsky.actor.defs#threadViewPref'\n  /** Sorting mode for threads. */\n  sort?:\n    | 'oldest'\n    | 'newest'\n    | 'most-likes'\n    | 'random'\n    | 'hotness'\n    | (string & {})\n}\n\nconst hashThreadViewPref = 'threadViewPref'\n\nexport function isThreadViewPref<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPref)\n}\n\nexport function validateThreadViewPref<V>(v: V) {\n  return validate<ThreadViewPref & V>(v, id, hashThreadViewPref)\n}\n\nexport interface InterestsPref {\n  $type?: 'app.bsky.actor.defs#interestsPref'\n  /** A list of tags which describe the account owner's interests gathered during onboarding. */\n  tags: string[]\n}\n\nconst hashInterestsPref = 'interestsPref'\n\nexport function isInterestsPref<V>(v: V) {\n  return is$typed(v, id, hashInterestsPref)\n}\n\nexport function validateInterestsPref<V>(v: V) {\n  return validate<InterestsPref & V>(v, id, hashInterestsPref)\n}\n\nexport type MutedWordTarget = 'content' | 'tag' | (string & {})\n\n/** A word that the account owner has muted. */\nexport interface MutedWord {\n  $type?: 'app.bsky.actor.defs#mutedWord'\n  id?: string\n  /** The muted word itself. */\n  value: string\n  /** The intended targets of the muted word. */\n  targets: MutedWordTarget[]\n  /** Groups of users to apply the muted word to. If undefined, applies to all users. */\n  actorTarget: 'all' | 'exclude-following' | (string & {})\n  /** The date and time at which the muted word will expire and no longer be applied. */\n  expiresAt?: string\n}\n\nconst hashMutedWord = 'mutedWord'\n\nexport function isMutedWord<V>(v: V) {\n  return is$typed(v, id, hashMutedWord)\n}\n\nexport function validateMutedWord<V>(v: V) {\n  return validate<MutedWord & V>(v, id, hashMutedWord)\n}\n\nexport interface MutedWordsPref {\n  $type?: 'app.bsky.actor.defs#mutedWordsPref'\n  /** A list of words the account owner has muted. */\n  items: MutedWord[]\n}\n\nconst hashMutedWordsPref = 'mutedWordsPref'\n\nexport function isMutedWordsPref<V>(v: V) {\n  return is$typed(v, id, hashMutedWordsPref)\n}\n\nexport function validateMutedWordsPref<V>(v: V) {\n  return validate<MutedWordsPref & V>(v, id, hashMutedWordsPref)\n}\n\nexport interface HiddenPostsPref {\n  $type?: 'app.bsky.actor.defs#hiddenPostsPref'\n  /** A list of URIs of posts the account owner has hidden. */\n  items: string[]\n}\n\nconst hashHiddenPostsPref = 'hiddenPostsPref'\n\nexport function isHiddenPostsPref<V>(v: V) {\n  return is$typed(v, id, hashHiddenPostsPref)\n}\n\nexport function validateHiddenPostsPref<V>(v: V) {\n  return validate<HiddenPostsPref & V>(v, id, hashHiddenPostsPref)\n}\n\nexport interface LabelersPref {\n  $type?: 'app.bsky.actor.defs#labelersPref'\n  labelers: LabelerPrefItem[]\n}\n\nconst hashLabelersPref = 'labelersPref'\n\nexport function isLabelersPref<V>(v: V) {\n  return is$typed(v, id, hashLabelersPref)\n}\n\nexport function validateLabelersPref<V>(v: V) {\n  return validate<LabelersPref & V>(v, id, hashLabelersPref)\n}\n\nexport interface LabelerPrefItem {\n  $type?: 'app.bsky.actor.defs#labelerPrefItem'\n  did: string\n}\n\nconst hashLabelerPrefItem = 'labelerPrefItem'\n\nexport function isLabelerPrefItem<V>(v: V) {\n  return is$typed(v, id, hashLabelerPrefItem)\n}\n\nexport function validateLabelerPrefItem<V>(v: V) {\n  return validate<LabelerPrefItem & V>(v, id, hashLabelerPrefItem)\n}\n\n/** A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this. */\nexport interface BskyAppStatePref {\n  $type?: 'app.bsky.actor.defs#bskyAppStatePref'\n  activeProgressGuide?: BskyAppProgressGuide\n  /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */\n  queuedNudges?: string[]\n  /** Storage for NUXs the user has encountered. */\n  nuxs?: Nux[]\n}\n\nconst hashBskyAppStatePref = 'bskyAppStatePref'\n\nexport function isBskyAppStatePref<V>(v: V) {\n  return is$typed(v, id, hashBskyAppStatePref)\n}\n\nexport function validateBskyAppStatePref<V>(v: V) {\n  return validate<BskyAppStatePref & V>(v, id, hashBskyAppStatePref)\n}\n\n/** If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress. */\nexport interface BskyAppProgressGuide {\n  $type?: 'app.bsky.actor.defs#bskyAppProgressGuide'\n  guide: string\n}\n\nconst hashBskyAppProgressGuide = 'bskyAppProgressGuide'\n\nexport function isBskyAppProgressGuide<V>(v: V) {\n  return is$typed(v, id, hashBskyAppProgressGuide)\n}\n\nexport function validateBskyAppProgressGuide<V>(v: V) {\n  return validate<BskyAppProgressGuide & V>(v, id, hashBskyAppProgressGuide)\n}\n\n/** A new user experiences (NUX) storage object */\nexport interface Nux {\n  $type?: 'app.bsky.actor.defs#nux'\n  id: string\n  completed: boolean\n  /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */\n  data?: string\n  /** The date and time at which the NUX will expire and should be considered completed. */\n  expiresAt?: string\n}\n\nconst hashNux = 'nux'\n\nexport function isNux<V>(v: V) {\n  return is$typed(v, id, hashNux)\n}\n\nexport function validateNux<V>(v: V) {\n  return validate<Nux & V>(v, id, hashNux)\n}\n\n/** Preferences for how verified accounts appear in the app. */\nexport interface VerificationPrefs {\n  $type?: 'app.bsky.actor.defs#verificationPrefs'\n  /** Hide the blue check badges for verified accounts and trusted verifiers. */\n  hideBadges: boolean\n}\n\nconst hashVerificationPrefs = 'verificationPrefs'\n\nexport function isVerificationPrefs<V>(v: V) {\n  return is$typed(v, id, hashVerificationPrefs)\n}\n\nexport function validateVerificationPrefs<V>(v: V) {\n  return validate<VerificationPrefs & V>(v, id, hashVerificationPrefs)\n}\n\n/** Preferences for live events. */\nexport interface LiveEventPreferences {\n  $type?: 'app.bsky.actor.defs#liveEventPreferences'\n  /** A list of feed IDs that the user has hidden from live events. */\n  hiddenFeedIds?: string[]\n  /** Whether to hide all feeds from live events. */\n  hideAllFeeds: boolean\n}\n\nconst hashLiveEventPreferences = 'liveEventPreferences'\n\nexport function isLiveEventPreferences<V>(v: V) {\n  return is$typed(v, id, hashLiveEventPreferences)\n}\n\nexport function validateLiveEventPreferences<V>(v: V) {\n  return validate<LiveEventPreferences & V>(v, id, hashLiveEventPreferences)\n}\n\n/** 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. */\nexport interface PostInteractionSettingsPref {\n  $type?: 'app.bsky.actor.defs#postInteractionSettingsPref'\n  /** Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  threadgateAllowRules?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n  /** Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashPostInteractionSettingsPref = 'postInteractionSettingsPref'\n\nexport function isPostInteractionSettingsPref<V>(v: V) {\n  return is$typed(v, id, hashPostInteractionSettingsPref)\n}\n\nexport function validatePostInteractionSettingsPref<V>(v: V) {\n  return validate<PostInteractionSettingsPref & V>(\n    v,\n    id,\n    hashPostInteractionSettingsPref,\n  )\n}\n\nexport interface StatusView {\n  $type?: 'app.bsky.actor.defs#statusView'\n  uri?: string\n  cid?: string\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  record: { [_ in string]: unknown }\n  embed?: $Typed<AppBskyEmbedExternal.View> | { $type: string }\n  /** The date when this status will expire. The application might choose to no longer return the status after expiration. */\n  expiresAt?: string\n  /** True if the status is not expired, false if it is expired. Only present if expiration was set. */\n  isActive?: boolean\n  /** True if the user's go-live access has been disabled by a moderator, false otherwise. */\n  isDisabled?: boolean\n}\n\nconst hashStatusView = 'statusView'\n\nexport function isStatusView<V>(v: V) {\n  return is$typed(v, id, hashStatusView)\n}\n\nexport function validateStatusView<V>(v: V) {\n  return validate<StatusView & V>(v, id, hashStatusView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/getProfile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfile'\n\nexport type QueryParams = {\n  /** Handle or DID of account to fetch profile of. */\n  actor: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyActorDefs.ProfileViewDetailed\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/getProfiles.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfiles'\n\nexport type QueryParams = {\n  actors: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  profiles: AppBskyActorDefs.ProfileViewDetailed[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/getSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getSuggestions'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/profile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.profile'\n\nexport interface Main {\n  $type: 'app.bsky.actor.profile'\n  displayName?: string\n  /** Free-form profile description text. */\n  description?: string\n  /** Free-form pronouns text. */\n  pronouns?: string\n  website?: string\n  /** Small image to be displayed next to posts from account. AKA, 'profile picture' */\n  avatar?: BlobRef\n  /** Larger horizontal image to display behind profile view. */\n  banner?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/searchActors.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActors'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/searchActorsTypeahead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActorsTypeahead'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query prefix; not a full query string. */\n  q?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/actor/status.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.status'\n\nexport interface Main {\n  $type: 'app.bsky.actor.status'\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  embed?: $Typed<AppBskyEmbedExternal.Main> | { $type: string }\n  /** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. */\n  durationMinutes?: number\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Advertises an account as currently offering live content. */\nexport const LIVE = `${id}#live`\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/ageassurance/begin.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.begin'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive Age Assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the Age Assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n  /** An optional ISO 3166-2 code of the user's region or state within the country. */\n  regionCode?: string\n}\n\nexport type OutputSchema = AppBskyAgeassuranceDefs.State\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidEmailError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidTooLongError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidInitiationError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RegionNotSupportedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidEmail') return new InvalidEmailError(e)\n    if (e.error === 'DidTooLong') return new DidTooLongError(e)\n    if (e.error === 'InvalidInitiation') return new InvalidInitiationError(e)\n    if (e.error === 'RegionNotSupported') return new RegionNotSupportedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/ageassurance/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.defs'\n\n/** The access level granted based on Age Assurance data we've processed. */\nexport type Access = 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n/** The status of the Age Assurance process. */\nexport type Status =\n  | 'unknown'\n  | 'pending'\n  | 'assured'\n  | 'blocked'\n  | (string & {})\n\n/** The user's computed Age Assurance state. */\nexport interface State {\n  $type?: 'app.bsky.ageassurance.defs#state'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  status: Status\n  access: Access\n}\n\nconst hashState = 'state'\n\nexport function isState<V>(v: V) {\n  return is$typed(v, id, hashState)\n}\n\nexport function validateState<V>(v: V) {\n  return validate<State & V>(v, id, hashState)\n}\n\n/** Additional metadata needed to compute Age Assurance state client-side. */\nexport interface StateMetadata {\n  $type?: 'app.bsky.ageassurance.defs#stateMetadata'\n  /** The account creation timestamp. */\n  accountCreatedAt?: string\n}\n\nconst hashStateMetadata = 'stateMetadata'\n\nexport function isStateMetadata<V>(v: V) {\n  return is$typed(v, id, hashStateMetadata)\n}\n\nexport function validateStateMetadata<V>(v: V) {\n  return validate<StateMetadata & V>(v, id, hashStateMetadata)\n}\n\nexport interface Config {\n  $type?: 'app.bsky.ageassurance.defs#config'\n  /** The per-region Age Assurance configuration. */\n  regions: ConfigRegion[]\n}\n\nconst hashConfig = 'config'\n\nexport function isConfig<V>(v: V) {\n  return is$typed(v, id, hashConfig)\n}\n\nexport function validateConfig<V>(v: V) {\n  return validate<Config & V>(v, id, hashConfig)\n}\n\n/** The Age Assurance configuration for a specific region. */\nexport interface ConfigRegion {\n  $type?: 'app.bsky.ageassurance.defs#configRegion'\n  /** The ISO 3166-1 alpha-2 country code this configuration applies to. */\n  countryCode: string\n  /** The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. */\n  regionCode?: string\n  /** The minimum age (as a whole integer) required to use Bluesky in this region. */\n  minAccessAge: number\n  /** 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. */\n  rules: (\n    | $Typed<ConfigRegionRuleDefault>\n    | $Typed<ConfigRegionRuleIfDeclaredOverAge>\n    | $Typed<ConfigRegionRuleIfDeclaredUnderAge>\n    | $Typed<ConfigRegionRuleIfAssuredOverAge>\n    | $Typed<ConfigRegionRuleIfAssuredUnderAge>\n    | $Typed<ConfigRegionRuleIfAccountNewerThan>\n    | $Typed<ConfigRegionRuleIfAccountOlderThan>\n    | { $type: string }\n  )[]\n}\n\nconst hashConfigRegion = 'configRegion'\n\nexport function isConfigRegion<V>(v: V) {\n  return is$typed(v, id, hashConfigRegion)\n}\n\nexport function validateConfigRegion<V>(v: V) {\n  return validate<ConfigRegion & V>(v, id, hashConfigRegion)\n}\n\n/** Age Assurance rule that applies by default. */\nexport interface ConfigRegionRuleDefault {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleDefault'\n  access: Access\n}\n\nconst hashConfigRegionRuleDefault = 'configRegionRuleDefault'\n\nexport function isConfigRegionRuleDefault<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleDefault)\n}\n\nexport function validateConfigRegionRuleDefault<V>(v: V) {\n  return validate<ConfigRegionRuleDefault & V>(\n    v,\n    id,\n    hashConfigRegionRuleDefault,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfDeclaredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredOverAge =\n  'configRegionRuleIfDeclaredOverAge'\n\nexport function isConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredOverAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves under a certain age. */\nexport interface ConfigRegionRuleIfDeclaredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredUnderAge =\n  'configRegionRuleIfDeclaredUnderAge'\n\nexport function isConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfAssuredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredOverAge = 'configRegionRuleIfAssuredOverAge'\n\nexport function isConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredOverAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be under a certain age. */\nexport interface ConfigRegionRuleIfAssuredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredUnderAge =\n  'configRegionRuleIfAssuredUnderAge'\n\nexport function isConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the account is equal-to or newer than a certain date. */\nexport interface ConfigRegionRuleIfAccountNewerThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountNewerThan =\n  'configRegionRuleIfAccountNewerThan'\n\nexport function isConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountNewerThan)\n}\n\nexport function validateConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountNewerThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountNewerThan,\n  )\n}\n\n/** Age Assurance rule that applies if the account is older than a certain date. */\nexport interface ConfigRegionRuleIfAccountOlderThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountOlderThan =\n  'configRegionRuleIfAccountOlderThan'\n\nexport function isConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountOlderThan)\n}\n\nexport function validateConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountOlderThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountOlderThan,\n  )\n}\n\n/** Object used to store Age Assurance data in stash. */\nexport interface Event {\n  $type?: 'app.bsky.ageassurance.defs#event'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the Age Assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n  /** The access level granted based on Age Assurance data we've processed. */\n  access: 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The email used for Age Assurance. */\n  email?: string\n  /** The IP address used when initiating the Age Assurance flow. */\n  initIp?: string\n  /** The user agent used when initiating the Age Assurance flow. */\n  initUa?: string\n  /** The IP address used when completing the Age Assurance flow. */\n  completeIp?: string\n  /** The user agent used when completing the Age Assurance flow. */\n  completeUa?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/ageassurance/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyAgeassuranceDefs.Config\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/ageassurance/getState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getState'\n\nexport type QueryParams = {\n  countryCode: string\n  regionCode?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  state: AppBskyAgeassuranceDefs.State\n  metadata: AppBskyAgeassuranceDefs.StateMetadata\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/bookmark/createBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.createBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n  cid: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class UnsupportedCollectionError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'UnsupportedCollection')\n      return new UnsupportedCollectionError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/bookmark/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.defs'\n\n/** Object used to store bookmark data in stash. */\nexport interface Bookmark {\n  $type?: 'app.bsky.bookmark.defs#bookmark'\n  subject: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashBookmark = 'bookmark'\n\nexport function isBookmark<V>(v: V) {\n  return is$typed(v, id, hashBookmark)\n}\n\nexport function validateBookmark<V>(v: V) {\n  return validate<Bookmark & V>(v, id, hashBookmark)\n}\n\nexport interface BookmarkView {\n  $type?: 'app.bsky.bookmark.defs#bookmarkView'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  item:\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.PostView>\n    | { $type: string }\n}\n\nconst hashBookmarkView = 'bookmarkView'\n\nexport function isBookmarkView<V>(v: V) {\n  return is$typed(v, id, hashBookmarkView)\n}\n\nexport function validateBookmarkView<V>(v: V) {\n  return validate<BookmarkView & V>(v, id, hashBookmarkView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/bookmark/deleteBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.deleteBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class UnsupportedCollectionError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'UnsupportedCollection')\n      return new UnsupportedCollectionError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/bookmark/getBookmarks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyBookmarkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.getBookmarks'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  bookmarks: AppBskyBookmarkDefs.BookmarkView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.defs'\n\n/** 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. */\nexport interface MatchAndContactIndex {\n  $type?: 'app.bsky.contact.defs#matchAndContactIndex'\n  match: AppBskyActorDefs.ProfileView\n  /** The index of this match in the import contact input. */\n  contactIndex: number\n}\n\nconst hashMatchAndContactIndex = 'matchAndContactIndex'\n\nexport function isMatchAndContactIndex<V>(v: V) {\n  return is$typed(v, id, hashMatchAndContactIndex)\n}\n\nexport function validateMatchAndContactIndex<V>(v: V) {\n  return validate<MatchAndContactIndex & V>(v, id, hashMatchAndContactIndex)\n}\n\nexport interface SyncStatus {\n  $type?: 'app.bsky.contact.defs#syncStatus'\n  /** Last date when contacts where imported. */\n  syncedAt: string\n  /** 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. */\n  matchesCount: number\n}\n\nconst hashSyncStatus = 'syncStatus'\n\nexport function isSyncStatus<V>(v: V) {\n  return is$typed(v, id, hashSyncStatus)\n}\n\nexport function validateSyncStatus<V>(v: V) {\n  return validate<SyncStatus & V>(v, id, hashSyncStatus)\n}\n\n/** A stash object to be sent via bsync representing a notification to be created. */\nexport interface Notification {\n  $type?: 'app.bsky.contact.defs#notification'\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/dismissMatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.dismissMatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The subject's DID to dismiss the match with. */\n  subject: string\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/getMatches.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getMatches'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  matches: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidLimitError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidCursorError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InvalidLimit') return new InvalidLimitError(e)\n    if (e.error === 'InvalidCursor') return new InvalidCursorError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/getSyncStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getSyncStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  syncStatus?: AppBskyContactDefs.SyncStatus\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/importContacts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.importContacts'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`. */\n  token: string\n  /** 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`. */\n  contacts: string[]\n}\n\nexport interface OutputSchema {\n  /** The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list. */\n  matchesAndContactIndexes: AppBskyContactDefs.MatchAndContactIndex[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidContactsError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class TooManyContactsError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InvalidContacts') return new InvalidContactsError(e)\n    if (e.error === 'TooManyContacts') return new TooManyContactsError(e)\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/removeData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.removeData'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/sendNotification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.sendNotification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/startPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.startPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to receive the code via SMS. */\n  phone: string\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RateLimitExceededError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidPhoneError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RateLimitExceeded') return new RateLimitExceededError(e)\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InvalidPhone') return new InvalidPhoneError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/contact/verifyPhone.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.verifyPhone'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`. */\n  phone: string\n  /** The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`. */\n  code: string\n}\n\nexport interface OutputSchema {\n  /** JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call. */\n  token: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RateLimitExceededError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidPhoneError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidCodeError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InternalError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RateLimitExceeded') return new RateLimitExceededError(e)\n    if (e.error === 'InvalidDid') return new InvalidDidError(e)\n    if (e.error === 'InvalidPhone') return new InvalidPhoneError(e)\n    if (e.error === 'InvalidCode') return new InvalidCodeError(e)\n    if (e.error === 'InternalError') return new InternalError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/draft/createDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.createDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.Draft\n}\n\nexport interface OutputSchema {\n  /** The ID of the created draft. */\n  id: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class DraftLimitReachedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'DraftLimitReached') return new DraftLimitReachedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/draft/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.defs'\n\n/** A draft with an identifier, used to store drafts in private storage (stash). */\nexport interface DraftWithId {\n  $type?: 'app.bsky.draft.defs#draftWithId'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n}\n\nconst hashDraftWithId = 'draftWithId'\n\nexport function isDraftWithId<V>(v: V) {\n  return is$typed(v, id, hashDraftWithId)\n}\n\nexport function validateDraftWithId<V>(v: V) {\n  return validate<DraftWithId & V>(v, id, hashDraftWithId)\n}\n\n/** A draft containing an array of draft posts. */\nexport interface Draft {\n  $type?: 'app.bsky.draft.defs#draft'\n  /** UUIDv4 identifier of the device that created this draft. */\n  deviceId?: string\n  /** The device and/or platform on which the draft was created. */\n  deviceName?: string\n  /** Array of draft posts that compose this draft. */\n  posts: DraftPost[]\n  /** Indicates human language of posts primary text content. */\n  langs?: string[]\n  /** Embedding rules for the postgates to be created when this draft is published. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n  /** Allow-rules for the threadgate to be created when this draft is published. */\n  threadgateAllow?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashDraft = 'draft'\n\nexport function isDraft<V>(v: V) {\n  return is$typed(v, id, hashDraft)\n}\n\nexport function validateDraft<V>(v: V) {\n  return validate<Draft & V>(v, id, hashDraft)\n}\n\n/** One of the posts that compose a draft. */\nexport interface DraftPost {\n  $type?: 'app.bsky.draft.defs#draftPost'\n  /** 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. */\n  text: string\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  embedImages?: DraftEmbedImage[]\n  embedVideos?: DraftEmbedVideo[]\n  embedExternals?: DraftEmbedExternal[]\n  embedRecords?: DraftEmbedRecord[]\n}\n\nconst hashDraftPost = 'draftPost'\n\nexport function isDraftPost<V>(v: V) {\n  return is$typed(v, id, hashDraftPost)\n}\n\nexport function validateDraftPost<V>(v: V) {\n  return validate<DraftPost & V>(v, id, hashDraftPost)\n}\n\n/** View to present drafts data to users. */\nexport interface DraftView {\n  $type?: 'app.bsky.draft.defs#draftView'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n  /** The time the draft was created. */\n  createdAt: string\n  /** The time the draft was last updated. */\n  updatedAt: string\n}\n\nconst hashDraftView = 'draftView'\n\nexport function isDraftView<V>(v: V) {\n  return is$typed(v, id, hashDraftView)\n}\n\nexport function validateDraftView<V>(v: V) {\n  return validate<DraftView & V>(v, id, hashDraftView)\n}\n\nexport interface DraftEmbedLocalRef {\n  $type?: 'app.bsky.draft.defs#draftEmbedLocalRef'\n  /** Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts. */\n  path: string\n}\n\nconst hashDraftEmbedLocalRef = 'draftEmbedLocalRef'\n\nexport function isDraftEmbedLocalRef<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedLocalRef)\n}\n\nexport function validateDraftEmbedLocalRef<V>(v: V) {\n  return validate<DraftEmbedLocalRef & V>(v, id, hashDraftEmbedLocalRef)\n}\n\nexport interface DraftEmbedCaption {\n  $type?: 'app.bsky.draft.defs#draftEmbedCaption'\n  lang: string\n  content: string\n}\n\nconst hashDraftEmbedCaption = 'draftEmbedCaption'\n\nexport function isDraftEmbedCaption<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedCaption)\n}\n\nexport function validateDraftEmbedCaption<V>(v: V) {\n  return validate<DraftEmbedCaption & V>(v, id, hashDraftEmbedCaption)\n}\n\nexport interface DraftEmbedImage {\n  $type?: 'app.bsky.draft.defs#draftEmbedImage'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n}\n\nconst hashDraftEmbedImage = 'draftEmbedImage'\n\nexport function isDraftEmbedImage<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedImage)\n}\n\nexport function validateDraftEmbedImage<V>(v: V) {\n  return validate<DraftEmbedImage & V>(v, id, hashDraftEmbedImage)\n}\n\nexport interface DraftEmbedVideo {\n  $type?: 'app.bsky.draft.defs#draftEmbedVideo'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n  captions?: DraftEmbedCaption[]\n}\n\nconst hashDraftEmbedVideo = 'draftEmbedVideo'\n\nexport function isDraftEmbedVideo<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedVideo)\n}\n\nexport function validateDraftEmbedVideo<V>(v: V) {\n  return validate<DraftEmbedVideo & V>(v, id, hashDraftEmbedVideo)\n}\n\nexport interface DraftEmbedExternal {\n  $type?: 'app.bsky.draft.defs#draftEmbedExternal'\n  uri: string\n}\n\nconst hashDraftEmbedExternal = 'draftEmbedExternal'\n\nexport function isDraftEmbedExternal<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedExternal)\n}\n\nexport function validateDraftEmbedExternal<V>(v: V) {\n  return validate<DraftEmbedExternal & V>(v, id, hashDraftEmbedExternal)\n}\n\nexport interface DraftEmbedRecord {\n  $type?: 'app.bsky.draft.defs#draftEmbedRecord'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashDraftEmbedRecord = 'draftEmbedRecord'\n\nexport function isDraftEmbedRecord<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedRecord)\n}\n\nexport function validateDraftEmbedRecord<V>(v: V) {\n  return validate<DraftEmbedRecord & V>(v, id, hashDraftEmbedRecord)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/draft/deleteDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.deleteDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/draft/getDrafts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.getDrafts'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  drafts: AppBskyDraftDefs.DraftView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/draft/updateDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.updateDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.DraftWithId\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/embed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.defs'\n\n/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */\nexport interface AspectRatio {\n  $type?: 'app.bsky.embed.defs#aspectRatio'\n  width: number\n  height: number\n}\n\nconst hashAspectRatio = 'aspectRatio'\n\nexport function isAspectRatio<V>(v: V) {\n  return is$typed(v, id, hashAspectRatio)\n}\n\nexport function validateAspectRatio<V>(v: V) {\n  return validate<AspectRatio & V>(v, id, hashAspectRatio)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/embed/external.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.external'\n\n/** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). */\nexport interface Main {\n  $type?: 'app.bsky.embed.external'\n  external: External\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface External {\n  $type?: 'app.bsky.embed.external#external'\n  uri: string\n  title: string\n  description: string\n  thumb?: BlobRef\n}\n\nconst hashExternal = 'external'\n\nexport function isExternal<V>(v: V) {\n  return is$typed(v, id, hashExternal)\n}\n\nexport function validateExternal<V>(v: V) {\n  return validate<External & V>(v, id, hashExternal)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.external#view'\n  external: ViewExternal\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewExternal {\n  $type?: 'app.bsky.embed.external#viewExternal'\n  uri: string\n  title: string\n  description: string\n  thumb?: string\n}\n\nconst hashViewExternal = 'viewExternal'\n\nexport function isViewExternal<V>(v: V) {\n  return is$typed(v, id, hashViewExternal)\n}\n\nexport function validateViewExternal<V>(v: V) {\n  return validate<ViewExternal & V>(v, id, hashViewExternal)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/embed/images.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.images'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.images'\n  images: Image[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Image {\n  $type?: 'app.bsky.embed.images#image'\n  image: BlobRef\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashImage = 'image'\n\nexport function isImage<V>(v: V) {\n  return is$typed(v, id, hashImage)\n}\n\nexport function validateImage<V>(v: V) {\n  return validate<Image & V>(v, id, hashImage)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.images#view'\n  images: ViewImage[]\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewImage {\n  $type?: 'app.bsky.embed.images#viewImage'\n  /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */\n  thumb: string\n  /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */\n  fullsize: string\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashViewImage = 'viewImage'\n\nexport function isViewImage<V>(v: V) {\n  return is$typed(v, id, hashViewImage)\n}\n\nexport function validateViewImage<V>(v: V) {\n  return validate<ViewImage & V>(v, id, hashViewImage)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/embed/record.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as AppBskyLabelerDefs from '../labeler/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\nimport type * as AppBskyEmbedRecordWithMedia from './recordWithMedia.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.record'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.record'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.record#view'\n  record:\n    | $Typed<ViewRecord>\n    | $Typed<ViewNotFound>\n    | $Typed<ViewBlocked>\n    | $Typed<ViewDetached>\n    | $Typed<AppBskyFeedDefs.GeneratorView>\n    | $Typed<AppBskyGraphDefs.ListView>\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyGraphDefs.StarterPackViewBasic>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewRecord {\n  $type?: 'app.bsky.embed.record#viewRecord'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  /** The record data itself. */\n  value: { [_ in string]: unknown }\n  labels?: ComAtprotoLabelDefs.Label[]\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  embeds?: (\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  )[]\n  indexedAt: string\n}\n\nconst hashViewRecord = 'viewRecord'\n\nexport function isViewRecord<V>(v: V) {\n  return is$typed(v, id, hashViewRecord)\n}\n\nexport function validateViewRecord<V>(v: V) {\n  return validate<ViewRecord & V>(v, id, hashViewRecord)\n}\n\nexport interface ViewNotFound {\n  $type?: 'app.bsky.embed.record#viewNotFound'\n  uri: string\n  notFound: true\n}\n\nconst hashViewNotFound = 'viewNotFound'\n\nexport function isViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashViewNotFound)\n}\n\nexport function validateViewNotFound<V>(v: V) {\n  return validate<ViewNotFound & V>(v, id, hashViewNotFound)\n}\n\nexport interface ViewBlocked {\n  $type?: 'app.bsky.embed.record#viewBlocked'\n  uri: string\n  blocked: true\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashViewBlocked = 'viewBlocked'\n\nexport function isViewBlocked<V>(v: V) {\n  return is$typed(v, id, hashViewBlocked)\n}\n\nexport function validateViewBlocked<V>(v: V) {\n  return validate<ViewBlocked & V>(v, id, hashViewBlocked)\n}\n\nexport interface ViewDetached {\n  $type?: 'app.bsky.embed.record#viewDetached'\n  uri: string\n  detached: true\n}\n\nconst hashViewDetached = 'viewDetached'\n\nexport function isViewDetached<V>(v: V) {\n  return is$typed(v, id, hashViewDetached)\n}\n\nexport function validateViewDetached<V>(v: V) {\n  return validate<ViewDetached & V>(v, id, hashViewDetached)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/embed/recordWithMedia.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedRecord from './record.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.recordWithMedia'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.recordWithMedia'\n  record: AppBskyEmbedRecord.Main\n  media:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | { $type: string }\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.recordWithMedia#view'\n  record: AppBskyEmbedRecord.View\n  media:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/embed/video.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.video'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.video'\n  /** The mp4 video file. May be up to 100mb, formerly limited to 50mb. */\n  video: BlobRef\n  captions?: Caption[]\n  /** Alt text description of the video, for accessibility. */\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Caption {\n  $type?: 'app.bsky.embed.video#caption'\n  lang: string\n  file: BlobRef\n}\n\nconst hashCaption = 'caption'\n\nexport function isCaption<V>(v: V) {\n  return is$typed(v, id, hashCaption)\n}\n\nexport function validateCaption<V>(v: V) {\n  return validate<Caption & V>(v, id, hashCaption)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.video#view'\n  cid: string\n  playlist: string\n  thumbnail?: string\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.defs'\n\nexport interface PostView {\n  $type?: 'app.bsky.feed.defs#postView'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  record: { [_ in string]: unknown }\n  embed?:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<AppBskyEmbedRecord.View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  bookmarkCount?: number\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  indexedAt: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  threadgate?: ThreadgateView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashPostView = 'postView'\n\nexport function isPostView<V>(v: V) {\n  return is$typed(v, id, hashPostView)\n}\n\nexport function validatePostView<V>(v: V) {\n  return validate<PostView & V>(v, id, hashPostView)\n}\n\n/** Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.feed.defs#viewerState'\n  repost?: string\n  like?: string\n  bookmarked?: boolean\n  threadMuted?: boolean\n  replyDisabled?: boolean\n  embeddingDisabled?: boolean\n  pinned?: boolean\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** Metadata about this post within the context of the thread it is in. */\nexport interface ThreadContext {\n  $type?: 'app.bsky.feed.defs#threadContext'\n  rootAuthorLike?: string\n}\n\nconst hashThreadContext = 'threadContext'\n\nexport function isThreadContext<V>(v: V) {\n  return is$typed(v, id, hashThreadContext)\n}\n\nexport function validateThreadContext<V>(v: V) {\n  return validate<ThreadContext & V>(v, id, hashThreadContext)\n}\n\nexport interface FeedViewPost {\n  $type?: 'app.bsky.feed.defs#feedViewPost'\n  post: PostView\n  reply?: ReplyRef\n  reason?: $Typed<ReasonRepost> | $Typed<ReasonPin> | { $type: string }\n  /** Context provided by feed generator that may be passed back alongside interactions. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashFeedViewPost = 'feedViewPost'\n\nexport function isFeedViewPost<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPost)\n}\n\nexport function validateFeedViewPost<V>(v: V) {\n  return validate<FeedViewPost & V>(v, id, hashFeedViewPost)\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.defs#replyRef'\n  root:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  parent:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  grandparentAuthor?: AppBskyActorDefs.ProfileViewBasic\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\nexport interface ReasonRepost {\n  $type?: 'app.bsky.feed.defs#reasonRepost'\n  by: AppBskyActorDefs.ProfileViewBasic\n  uri?: string\n  cid?: string\n  indexedAt: string\n}\n\nconst hashReasonRepost = 'reasonRepost'\n\nexport function isReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashReasonRepost)\n}\n\nexport function validateReasonRepost<V>(v: V) {\n  return validate<ReasonRepost & V>(v, id, hashReasonRepost)\n}\n\nexport interface ReasonPin {\n  $type?: 'app.bsky.feed.defs#reasonPin'\n}\n\nconst hashReasonPin = 'reasonPin'\n\nexport function isReasonPin<V>(v: V) {\n  return is$typed(v, id, hashReasonPin)\n}\n\nexport function validateReasonPin<V>(v: V) {\n  return validate<ReasonPin & V>(v, id, hashReasonPin)\n}\n\nexport interface ThreadViewPost {\n  $type?: 'app.bsky.feed.defs#threadViewPost'\n  post: PostView\n  parent?:\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  replies?: (\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  )[]\n  threadContext?: ThreadContext\n}\n\nconst hashThreadViewPost = 'threadViewPost'\n\nexport function isThreadViewPost<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPost)\n}\n\nexport function validateThreadViewPost<V>(v: V) {\n  return validate<ThreadViewPost & V>(v, id, hashThreadViewPost)\n}\n\nexport interface NotFoundPost {\n  $type?: 'app.bsky.feed.defs#notFoundPost'\n  uri: string\n  notFound: true\n}\n\nconst hashNotFoundPost = 'notFoundPost'\n\nexport function isNotFoundPost<V>(v: V) {\n  return is$typed(v, id, hashNotFoundPost)\n}\n\nexport function validateNotFoundPost<V>(v: V) {\n  return validate<NotFoundPost & V>(v, id, hashNotFoundPost)\n}\n\nexport interface BlockedPost {\n  $type?: 'app.bsky.feed.defs#blockedPost'\n  uri: string\n  blocked: true\n  author: BlockedAuthor\n}\n\nconst hashBlockedPost = 'blockedPost'\n\nexport function isBlockedPost<V>(v: V) {\n  return is$typed(v, id, hashBlockedPost)\n}\n\nexport function validateBlockedPost<V>(v: V) {\n  return validate<BlockedPost & V>(v, id, hashBlockedPost)\n}\n\nexport interface BlockedAuthor {\n  $type?: 'app.bsky.feed.defs#blockedAuthor'\n  did: string\n  viewer?: AppBskyActorDefs.ViewerState\n}\n\nconst hashBlockedAuthor = 'blockedAuthor'\n\nexport function isBlockedAuthor<V>(v: V) {\n  return is$typed(v, id, hashBlockedAuthor)\n}\n\nexport function validateBlockedAuthor<V>(v: V) {\n  return validate<BlockedAuthor & V>(v, id, hashBlockedAuthor)\n}\n\nexport interface GeneratorView {\n  $type?: 'app.bsky.feed.defs#generatorView'\n  uri: string\n  cid: string\n  did: string\n  creator: AppBskyActorDefs.ProfileView\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  likeCount?: number\n  acceptsInteractions?: boolean\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: GeneratorViewerState\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  indexedAt: string\n}\n\nconst hashGeneratorView = 'generatorView'\n\nexport function isGeneratorView<V>(v: V) {\n  return is$typed(v, id, hashGeneratorView)\n}\n\nexport function validateGeneratorView<V>(v: V) {\n  return validate<GeneratorView & V>(v, id, hashGeneratorView)\n}\n\nexport interface GeneratorViewerState {\n  $type?: 'app.bsky.feed.defs#generatorViewerState'\n  like?: string\n}\n\nconst hashGeneratorViewerState = 'generatorViewerState'\n\nexport function isGeneratorViewerState<V>(v: V) {\n  return is$typed(v, id, hashGeneratorViewerState)\n}\n\nexport function validateGeneratorViewerState<V>(v: V) {\n  return validate<GeneratorViewerState & V>(v, id, hashGeneratorViewerState)\n}\n\nexport interface SkeletonFeedPost {\n  $type?: 'app.bsky.feed.defs#skeletonFeedPost'\n  post: string\n  reason?:\n    | $Typed<SkeletonReasonRepost>\n    | $Typed<SkeletonReasonPin>\n    | { $type: string }\n  /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */\n  feedContext?: string\n}\n\nconst hashSkeletonFeedPost = 'skeletonFeedPost'\n\nexport function isSkeletonFeedPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonFeedPost)\n}\n\nexport function validateSkeletonFeedPost<V>(v: V) {\n  return validate<SkeletonFeedPost & V>(v, id, hashSkeletonFeedPost)\n}\n\nexport interface SkeletonReasonRepost {\n  $type?: 'app.bsky.feed.defs#skeletonReasonRepost'\n  repost: string\n}\n\nconst hashSkeletonReasonRepost = 'skeletonReasonRepost'\n\nexport function isSkeletonReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonRepost)\n}\n\nexport function validateSkeletonReasonRepost<V>(v: V) {\n  return validate<SkeletonReasonRepost & V>(v, id, hashSkeletonReasonRepost)\n}\n\nexport interface SkeletonReasonPin {\n  $type?: 'app.bsky.feed.defs#skeletonReasonPin'\n}\n\nconst hashSkeletonReasonPin = 'skeletonReasonPin'\n\nexport function isSkeletonReasonPin<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonPin)\n}\n\nexport function validateSkeletonReasonPin<V>(v: V) {\n  return validate<SkeletonReasonPin & V>(v, id, hashSkeletonReasonPin)\n}\n\nexport interface ThreadgateView {\n  $type?: 'app.bsky.feed.defs#threadgateView'\n  uri?: string\n  cid?: string\n  record?: { [_ in string]: unknown }\n  lists?: AppBskyGraphDefs.ListViewBasic[]\n}\n\nconst hashThreadgateView = 'threadgateView'\n\nexport function isThreadgateView<V>(v: V) {\n  return is$typed(v, id, hashThreadgateView)\n}\n\nexport function validateThreadgateView<V>(v: V) {\n  return validate<ThreadgateView & V>(v, id, hashThreadgateView)\n}\n\nexport interface Interaction {\n  $type?: 'app.bsky.feed.defs#interaction'\n  item?: string\n  event?:\n    | 'app.bsky.feed.defs#requestLess'\n    | 'app.bsky.feed.defs#requestMore'\n    | 'app.bsky.feed.defs#clickthroughItem'\n    | 'app.bsky.feed.defs#clickthroughAuthor'\n    | 'app.bsky.feed.defs#clickthroughReposter'\n    | 'app.bsky.feed.defs#clickthroughEmbed'\n    | 'app.bsky.feed.defs#interactionSeen'\n    | 'app.bsky.feed.defs#interactionLike'\n    | 'app.bsky.feed.defs#interactionRepost'\n    | 'app.bsky.feed.defs#interactionReply'\n    | 'app.bsky.feed.defs#interactionQuote'\n    | 'app.bsky.feed.defs#interactionShare'\n    | (string & {})\n  /** Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashInteraction = 'interaction'\n\nexport function isInteraction<V>(v: V) {\n  return is$typed(v, id, hashInteraction)\n}\n\nexport function validateInteraction<V>(v: V) {\n  return validate<Interaction & V>(v, id, hashInteraction)\n}\n\n/** Request that less content like the given feed item be shown in the feed */\nexport const REQUESTLESS = `${id}#requestLess`\n/** Request that more content like the given feed item be shown in the feed */\nexport const REQUESTMORE = `${id}#requestMore`\n/** User clicked through to the feed item */\nexport const CLICKTHROUGHITEM = `${id}#clickthroughItem`\n/** User clicked through to the author of the feed item */\nexport const CLICKTHROUGHAUTHOR = `${id}#clickthroughAuthor`\n/** User clicked through to the reposter of the feed item */\nexport const CLICKTHROUGHREPOSTER = `${id}#clickthroughReposter`\n/** User clicked through to the embedded content of the feed item */\nexport const CLICKTHROUGHEMBED = `${id}#clickthroughEmbed`\n/** Declares the feed generator returns any types of posts. */\nexport const CONTENTMODEUNSPECIFIED = `${id}#contentModeUnspecified`\n/** Declares the feed generator returns posts containing app.bsky.embed.video embeds. */\nexport const CONTENTMODEVIDEO = `${id}#contentModeVideo`\n/** Feed item was seen by user */\nexport const INTERACTIONSEEN = `${id}#interactionSeen`\n/** User liked the feed item */\nexport const INTERACTIONLIKE = `${id}#interactionLike`\n/** User reposted the feed item */\nexport const INTERACTIONREPOST = `${id}#interactionRepost`\n/** User replied to the feed item */\nexport const INTERACTIONREPLY = `${id}#interactionReply`\n/** User quoted the feed item */\nexport const INTERACTIONQUOTE = `${id}#interactionQuote`\n/** User shared the feed item */\nexport const INTERACTIONSHARE = `${id}#interactionShare`\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/describeFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.describeFeedGenerator'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  feeds: Feed[]\n  links?: Links\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Feed {\n  $type?: 'app.bsky.feed.describeFeedGenerator#feed'\n  uri: string\n}\n\nconst hashFeed = 'feed'\n\nexport function isFeed<V>(v: V) {\n  return is$typed(v, id, hashFeed)\n}\n\nexport function validateFeed<V>(v: V) {\n  return validate<Feed & V>(v, id, hashFeed)\n}\n\nexport interface Links {\n  $type?: 'app.bsky.feed.describeFeedGenerator#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/generator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.generator'\n\nexport interface Main {\n  $type: 'app.bsky.feed.generator'\n  did: string\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  /** Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions */\n  acceptsInteractions?: boolean\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getActorFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorFeeds'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getActorLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorLikes'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BlockedActorError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class BlockedByActorError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BlockedActor') return new BlockedActorError(e)\n    if (e.error === 'BlockedByActor') return new BlockedByActorError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getAuthorFeed'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n  /** Combinations of post/repost types to include in response. */\n  filter?:\n    | 'posts_with_replies'\n    | 'posts_no_replies'\n    | 'posts_with_media'\n    | 'posts_and_author_threads'\n    | 'posts_with_video'\n    | (string & {})\n  includePins?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BlockedActorError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class BlockedByActorError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BlockedActor') return new BlockedActorError(e)\n    if (e.error === 'BlockedByActor') return new BlockedByActorError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeed'\n\nexport type QueryParams = {\n  feed: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class UnknownFeedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'UnknownFeed') return new UnknownFeedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerator'\n\nexport type QueryParams = {\n  /** AT-URI of the feed generator record. */\n  feed: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  view: AppBskyFeedDefs.GeneratorView\n  /** Indicates whether the feed generator service has been online recently, or else seems to be inactive. */\n  isOnline: boolean\n  /** Indicates whether the feed generator service is compatible with the record declaration. */\n  isValid: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerators'\n\nexport type QueryParams = {\n  feeds: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedSkeleton'\n\nexport type QueryParams = {\n  /** Reference to feed generator record describing the specific feed being requested. */\n  feed: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.SkeletonFeedPost[]\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class UnknownFeedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'UnknownFeed') return new UnknownFeedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getLikes'\n\nexport type QueryParams = {\n  /** AT-URI of the subject (eg, a post record). */\n  uri: string\n  /** CID of the subject record (aka, specific version of record), to filter likes. */\n  cid?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  likes: Like[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Like {\n  $type?: 'app.bsky.feed.getLikes#like'\n  indexedAt: string\n  createdAt: string\n  actor: AppBskyActorDefs.ProfileView\n}\n\nconst hashLike = 'like'\n\nexport function isLike<V>(v: V) {\n  return is$typed(v, id, hashLike)\n}\n\nexport function validateLike<V>(v: V) {\n  return validate<Like & V>(v, id, hashLike)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getListFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getListFeed'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class UnknownListError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'UnknownList') return new UnknownListError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getPostThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPostThread'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. */\n  uri: string\n  /** How many levels of reply depth should be included in response. */\n  depth?: number\n  /** How many levels of parent (and grandparent, etc) post to include. */\n  parentHeight?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  thread:\n    | $Typed<AppBskyFeedDefs.ThreadViewPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | { $type: string }\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class NotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'NotFound') return new NotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPosts'\n\nexport type QueryParams = {\n  /** List of post AT-URIs to return hydrated views for. */\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getQuotes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getQuotes'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to quotes of specific version (by CID) of the post record. */\n  cid?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getRepostedBy.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getRepostedBy'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to reposts of specific version (by CID) of the post record. */\n  cid?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  repostedBy: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/getTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getTimeline'\n\nexport type QueryParams = {\n  /** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. */\n  algorithm?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/like.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.like'\n\nexport interface Main {\n  $type: 'app.bsky.feed.like'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/post.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.post'\n\nexport interface Main {\n  $type: 'app.bsky.feed.post'\n  /** The primary post content. May be an empty string, if there are embeds. */\n  text: string\n  /** DEPRECATED: replaced by app.bsky.richtext.facet. */\n  entities?: Entity[]\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  reply?: ReplyRef\n  embed?:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | $Typed<AppBskyEmbedRecord.Main>\n    | $Typed<AppBskyEmbedRecordWithMedia.Main>\n    | { $type: string }\n  /** Indicates human language of post primary text content. */\n  langs?: string[]\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  /** Additional hashtags, in addition to any included in post text and facets. */\n  tags?: string[]\n  /** Client-declared timestamp when this post was originally created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.post#replyRef'\n  root: ComAtprotoRepoStrongRef.Main\n  parent: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\n/** Deprecated: use facets instead. */\nexport interface Entity {\n  $type?: 'app.bsky.feed.post#entity'\n  index: TextSlice\n  /** Expected values are 'mention' and 'link'. */\n  type: string\n  value: string\n}\n\nconst hashEntity = 'entity'\n\nexport function isEntity<V>(v: V) {\n  return is$typed(v, id, hashEntity)\n}\n\nexport function validateEntity<V>(v: V) {\n  return validate<Entity & V>(v, id, hashEntity)\n}\n\n/** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. */\nexport interface TextSlice {\n  $type?: 'app.bsky.feed.post#textSlice'\n  start: number\n  end: number\n}\n\nconst hashTextSlice = 'textSlice'\n\nexport function isTextSlice<V>(v: V) {\n  return is$typed(v, id, hashTextSlice)\n}\n\nexport function validateTextSlice<V>(v: V) {\n  return validate<TextSlice & V>(v, id, hashTextSlice)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/postgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.postgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.postgate'\n  createdAt: string\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of AT-URIs embedding this post that the author has detached from. */\n  detachedEmbeddingUris?: string[]\n  /** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  embeddingRules?: ($Typed<DisableRule> | { $type: string })[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Disables embedding of this post. */\nexport interface DisableRule {\n  $type?: 'app.bsky.feed.postgate#disableRule'\n}\n\nconst hashDisableRule = 'disableRule'\n\nexport function isDisableRule<V>(v: V) {\n  return is$typed(v, id, hashDisableRule)\n}\n\nexport function validateDisableRule<V>(v: V) {\n  return validate<DisableRule & V>(v, id, hashDisableRule)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/repost.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.repost'\n\nexport interface Main {\n  $type: 'app.bsky.feed.repost'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/searchPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.searchPosts'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort?: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  limit?: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BadQueryStringError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BadQueryString') return new BadQueryStringError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/sendInteractions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.sendInteractions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  feed?: string\n  interactions: AppBskyFeedDefs.Interaction[]\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/feed/threadgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.threadgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.threadgate'\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  allow?: (\n    | $Typed<MentionRule>\n    | $Typed<FollowerRule>\n    | $Typed<FollowingRule>\n    | $Typed<ListRule>\n    | { $type: string }\n  )[]\n  createdAt: string\n  /** List of hidden reply URIs. */\n  hiddenReplies?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Allow replies from actors mentioned in your post. */\nexport interface MentionRule {\n  $type?: 'app.bsky.feed.threadgate#mentionRule'\n}\n\nconst hashMentionRule = 'mentionRule'\n\nexport function isMentionRule<V>(v: V) {\n  return is$typed(v, id, hashMentionRule)\n}\n\nexport function validateMentionRule<V>(v: V) {\n  return validate<MentionRule & V>(v, id, hashMentionRule)\n}\n\n/** Allow replies from actors who follow you. */\nexport interface FollowerRule {\n  $type?: 'app.bsky.feed.threadgate#followerRule'\n}\n\nconst hashFollowerRule = 'followerRule'\n\nexport function isFollowerRule<V>(v: V) {\n  return is$typed(v, id, hashFollowerRule)\n}\n\nexport function validateFollowerRule<V>(v: V) {\n  return validate<FollowerRule & V>(v, id, hashFollowerRule)\n}\n\n/** Allow replies from actors you follow. */\nexport interface FollowingRule {\n  $type?: 'app.bsky.feed.threadgate#followingRule'\n}\n\nconst hashFollowingRule = 'followingRule'\n\nexport function isFollowingRule<V>(v: V) {\n  return is$typed(v, id, hashFollowingRule)\n}\n\nexport function validateFollowingRule<V>(v: V) {\n  return validate<FollowingRule & V>(v, id, hashFollowingRule)\n}\n\n/** Allow replies from actors on a list. */\nexport interface ListRule {\n  $type?: 'app.bsky.feed.threadgate#listRule'\n  list: string\n}\n\nconst hashListRule = 'listRule'\n\nexport function isListRule<V>(v: V) {\n  return is$typed(v, id, hashListRule)\n}\n\nexport function validateListRule<V>(v: V) {\n  return validate<ListRule & V>(v, id, hashListRule)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/block.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.block'\n\nexport interface Main {\n  $type: 'app.bsky.graph.block'\n  /** DID of the account to be blocked. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.defs'\n\nexport interface ListViewBasic {\n  $type?: 'app.bsky.graph.defs#listViewBasic'\n  uri: string\n  cid: string\n  name: string\n  purpose: ListPurpose\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt?: string\n}\n\nconst hashListViewBasic = 'listViewBasic'\n\nexport function isListViewBasic<V>(v: V) {\n  return is$typed(v, id, hashListViewBasic)\n}\n\nexport function validateListViewBasic<V>(v: V) {\n  return validate<ListViewBasic & V>(v, id, hashListViewBasic)\n}\n\nexport interface ListView {\n  $type?: 'app.bsky.graph.defs#listView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  name: string\n  purpose: ListPurpose\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt: string\n}\n\nconst hashListView = 'listView'\n\nexport function isListView<V>(v: V) {\n  return is$typed(v, id, hashListView)\n}\n\nexport function validateListView<V>(v: V) {\n  return validate<ListView & V>(v, id, hashListView)\n}\n\nexport interface ListItemView {\n  $type?: 'app.bsky.graph.defs#listItemView'\n  uri: string\n  subject: AppBskyActorDefs.ProfileView\n}\n\nconst hashListItemView = 'listItemView'\n\nexport function isListItemView<V>(v: V) {\n  return is$typed(v, id, hashListItemView)\n}\n\nexport function validateListItemView<V>(v: V) {\n  return validate<ListItemView & V>(v, id, hashListItemView)\n}\n\nexport interface StarterPackView {\n  $type?: 'app.bsky.graph.defs#starterPackView'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  list?: ListViewBasic\n  listItemsSample?: ListItemView[]\n  feeds?: AppBskyFeedDefs.GeneratorView[]\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackView = 'starterPackView'\n\nexport function isStarterPackView<V>(v: V) {\n  return is$typed(v, id, hashStarterPackView)\n}\n\nexport function validateStarterPackView<V>(v: V) {\n  return validate<StarterPackView & V>(v, id, hashStarterPackView)\n}\n\nexport interface StarterPackViewBasic {\n  $type?: 'app.bsky.graph.defs#starterPackViewBasic'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  listItemCount?: number\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackViewBasic = 'starterPackViewBasic'\n\nexport function isStarterPackViewBasic<V>(v: V) {\n  return is$typed(v, id, hashStarterPackViewBasic)\n}\n\nexport function validateStarterPackViewBasic<V>(v: V) {\n  return validate<StarterPackViewBasic & V>(v, id, hashStarterPackViewBasic)\n}\n\nexport type ListPurpose =\n  | 'app.bsky.graph.defs#modlist'\n  | 'app.bsky.graph.defs#curatelist'\n  | 'app.bsky.graph.defs#referencelist'\n  | (string & {})\n\n/** A list of actors to apply an aggregate moderation action (mute/block) on. */\nexport const MODLIST = `${id}#modlist`\n/** A list of actors used for curation purposes such as list feeds or interaction gating. */\nexport const CURATELIST = `${id}#curatelist`\n/** A list of actors used for only for reference purposes such as within a starter pack. */\nexport const REFERENCELIST = `${id}#referencelist`\n\nexport interface ListViewerState {\n  $type?: 'app.bsky.graph.defs#listViewerState'\n  muted?: boolean\n  blocked?: string\n}\n\nconst hashListViewerState = 'listViewerState'\n\nexport function isListViewerState<V>(v: V) {\n  return is$typed(v, id, hashListViewerState)\n}\n\nexport function validateListViewerState<V>(v: V) {\n  return validate<ListViewerState & V>(v, id, hashListViewerState)\n}\n\n/** indicates that a handle or DID could not be resolved */\nexport interface NotFoundActor {\n  $type?: 'app.bsky.graph.defs#notFoundActor'\n  actor: string\n  notFound: true\n}\n\nconst hashNotFoundActor = 'notFoundActor'\n\nexport function isNotFoundActor<V>(v: V) {\n  return is$typed(v, id, hashNotFoundActor)\n}\n\nexport function validateNotFoundActor<V>(v: V) {\n  return validate<NotFoundActor & V>(v, id, hashNotFoundActor)\n}\n\n/** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) */\nexport interface Relationship {\n  $type?: 'app.bsky.graph.defs#relationship'\n  did: string\n  /** if the actor follows this DID, this is the AT-URI of the follow record */\n  following?: string\n  /** if the actor is followed by this DID, contains the AT-URI of the follow record */\n  followedBy?: string\n  /** if the actor blocks this DID, this is the AT-URI of the block record */\n  blocking?: string\n  /** if the actor is blocked by this DID, contains the AT-URI of the block record */\n  blockedBy?: string\n  /** if the actor blocks this DID via a block list, this is the AT-URI of the listblock record */\n  blockingByList?: string\n  /** if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record */\n  blockedByList?: string\n}\n\nconst hashRelationship = 'relationship'\n\nexport function isRelationship<V>(v: V) {\n  return is$typed(v, id, hashRelationship)\n}\n\nexport function validateRelationship<V>(v: V) {\n  return validate<Relationship & V>(v, id, hashRelationship)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/follow.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.follow'\n\nexport interface Main {\n  $type: 'app.bsky.graph.follow'\n  subject: string\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getActorStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getActorStarterPacks'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getBlocks'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blocks: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getFollows.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollows'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  follows: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getKnownFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getKnownFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getList'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the list record to hydrate. */\n  list: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  list: AppBskyGraphDefs.ListView\n  items: AppBskyGraphDefs.ListItemView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getListBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListBlocks'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getListMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListMutes'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getLists.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getLists'\n\nexport type QueryParams = {\n  /** The account (actor) to enumerate lists from. */\n  actor: string\n  limit?: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getListsWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListsWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit?: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  listsWithMembership: ListWithMembership[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\n/** A list and an optional list item indicating membership of a target user to that list. */\nexport interface ListWithMembership {\n  $type?: 'app.bsky.graph.getListsWithMembership#listWithMembership'\n  list: AppBskyGraphDefs.ListView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashListWithMembership = 'listWithMembership'\n\nexport function isListWithMembership<V>(v: V) {\n  return is$typed(v, id, hashListWithMembership)\n}\n\nexport function validateListWithMembership<V>(v: V) {\n  return validate<ListWithMembership & V>(v, id, hashListWithMembership)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getMutes'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  mutes: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getRelationships.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getRelationships'\n\nexport type QueryParams = {\n  /** Primary account requesting relationships for. */\n  actor: string\n  /** List of 'other' accounts to be related back to the primary. */\n  others?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actor?: string\n  relationships: (\n    | $Typed<AppBskyGraphDefs.Relationship>\n    | $Typed<AppBskyGraphDefs.NotFoundActor>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class ActorNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'ActorNotFound') return new ActorNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getStarterPack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPack'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the starter pack record. */\n  starterPack: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPack: AppBskyGraphDefs.StarterPackView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacks'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getStarterPacksWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacksWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacksWithMembership: StarterPackWithMembership[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\n/** A starter pack and an optional list item indicating membership of a target user to that starter pack. */\nexport interface StarterPackWithMembership {\n  $type?: 'app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership'\n  starterPack: AppBskyGraphDefs.StarterPackView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashStarterPackWithMembership = 'starterPackWithMembership'\n\nexport function isStarterPackWithMembership<V>(v: V) {\n  return is$typed(v, id, hashStarterPackWithMembership)\n}\n\nexport function validateStarterPackWithMembership<V>(v: V) {\n  return validate<StarterPackWithMembership & V>(\n    v,\n    id,\n    hashStarterPackWithMembership,\n  )\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getSuggestedFollowsByActor'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: AppBskyActorDefs.ProfileView[]\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n  /** DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid */\n  isFallback: boolean\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/list.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.list'\n\nexport interface Main {\n  $type: 'app.bsky.graph.list'\n  purpose: AppBskyGraphDefs.ListPurpose\n  /** Display name for list; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/listblock.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listblock'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listblock'\n  /** Reference (AT-URI) to the mod list record. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/listitem.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listitem'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listitem'\n  /** The account which is included on the list. */\n  subject: string\n  /** Reference (AT-URI) to the list record (app.bsky.graph.list). */\n  list: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/muteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/muteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/muteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/searchStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.searchStarterPacks'\n\nexport type QueryParams = {\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/starterpack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.starterpack'\n\nexport interface Main {\n  $type: 'app.bsky.graph.starterpack'\n  /** Display name for starter pack; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  feeds?: FeedItem[]\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface FeedItem {\n  $type?: 'app.bsky.graph.starterpack#feedItem'\n  uri: string\n}\n\nconst hashFeedItem = 'feedItem'\n\nexport function isFeedItem<V>(v: V) {\n  return is$typed(v, id, hashFeedItem)\n}\n\nexport function validateFeedItem<V>(v: V) {\n  return validate<FeedItem & V>(v, id, hashFeedItem)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/unmuteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/unmuteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/unmuteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/graph/verification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.verification'\n\nexport interface Main {\n  $type: 'app.bsky.graph.verification'\n  /** DID of the subject the verification applies to. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Date of when the verification was created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/labeler/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.defs'\n\nexport interface LabelerView {\n  $type?: 'app.bsky.labeler.defs#labelerView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabelerView = 'labelerView'\n\nexport function isLabelerView<V>(v: V) {\n  return is$typed(v, id, hashLabelerView)\n}\n\nexport function validateLabelerView<V>(v: V) {\n  return validate<LabelerView & V>(v, id, hashLabelerView)\n}\n\nexport interface LabelerViewDetailed {\n  $type?: 'app.bsky.labeler.defs#labelerViewDetailed'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  policies: LabelerPolicies\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n}\n\nconst hashLabelerViewDetailed = 'labelerViewDetailed'\n\nexport function isLabelerViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewDetailed)\n}\n\nexport function validateLabelerViewDetailed<V>(v: V) {\n  return validate<LabelerViewDetailed & V>(v, id, hashLabelerViewDetailed)\n}\n\nexport interface LabelerViewerState {\n  $type?: 'app.bsky.labeler.defs#labelerViewerState'\n  like?: string\n}\n\nconst hashLabelerViewerState = 'labelerViewerState'\n\nexport function isLabelerViewerState<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewerState)\n}\n\nexport function validateLabelerViewerState<V>(v: V) {\n  return validate<LabelerViewerState & V>(v, id, hashLabelerViewerState)\n}\n\nexport interface LabelerPolicies {\n  $type?: 'app.bsky.labeler.defs#labelerPolicies'\n  /** The label values which this labeler publishes. May include global or custom labels. */\n  labelValues: ComAtprotoLabelDefs.LabelValue[]\n  /** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */\n  labelValueDefinitions?: ComAtprotoLabelDefs.LabelValueDefinition[]\n}\n\nconst hashLabelerPolicies = 'labelerPolicies'\n\nexport function isLabelerPolicies<V>(v: V) {\n  return is$typed(v, id, hashLabelerPolicies)\n}\n\nexport function validateLabelerPolicies<V>(v: V) {\n  return validate<LabelerPolicies & V>(v, id, hashLabelerPolicies)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/labeler/getServices.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.getServices'\n\nexport type QueryParams = {\n  dids: string[]\n  detailed?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  views: (\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyLabelerDefs.LabelerViewDetailed>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/labeler/service.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.service'\n\nexport interface Main {\n  $type: 'app.bsky.labeler.service'\n  policies: AppBskyLabelerDefs.LabelerPolicies\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.declaration'\n\nexport interface Main {\n  $type: 'app.bsky.notification.declaration'\n  /** A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'. */\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.defs'\n\nexport interface RecordDeleted {\n  $type?: 'app.bsky.notification.defs#recordDeleted'\n}\n\nconst hashRecordDeleted = 'recordDeleted'\n\nexport function isRecordDeleted<V>(v: V) {\n  return is$typed(v, id, hashRecordDeleted)\n}\n\nexport function validateRecordDeleted<V>(v: V) {\n  return validate<RecordDeleted & V>(v, id, hashRecordDeleted)\n}\n\nexport interface ChatPreference {\n  $type?: 'app.bsky.notification.defs#chatPreference'\n  include: 'all' | 'accepted' | (string & {})\n  push: boolean\n}\n\nconst hashChatPreference = 'chatPreference'\n\nexport function isChatPreference<V>(v: V) {\n  return is$typed(v, id, hashChatPreference)\n}\n\nexport function validateChatPreference<V>(v: V) {\n  return validate<ChatPreference & V>(v, id, hashChatPreference)\n}\n\nexport interface FilterablePreference {\n  $type?: 'app.bsky.notification.defs#filterablePreference'\n  include: 'all' | 'follows' | (string & {})\n  list: boolean\n  push: boolean\n}\n\nconst hashFilterablePreference = 'filterablePreference'\n\nexport function isFilterablePreference<V>(v: V) {\n  return is$typed(v, id, hashFilterablePreference)\n}\n\nexport function validateFilterablePreference<V>(v: V) {\n  return validate<FilterablePreference & V>(v, id, hashFilterablePreference)\n}\n\nexport interface Preference {\n  $type?: 'app.bsky.notification.defs#preference'\n  list: boolean\n  push: boolean\n}\n\nconst hashPreference = 'preference'\n\nexport function isPreference<V>(v: V) {\n  return is$typed(v, id, hashPreference)\n}\n\nexport function validatePreference<V>(v: V) {\n  return validate<Preference & V>(v, id, hashPreference)\n}\n\nexport interface Preferences {\n  $type?: 'app.bsky.notification.defs#preferences'\n  chat: ChatPreference\n  follow: FilterablePreference\n  like: FilterablePreference\n  likeViaRepost: FilterablePreference\n  mention: FilterablePreference\n  quote: FilterablePreference\n  reply: FilterablePreference\n  repost: FilterablePreference\n  repostViaRepost: FilterablePreference\n  starterpackJoined: Preference\n  subscribedPost: Preference\n  unverified: Preference\n  verified: Preference\n}\n\nconst hashPreferences = 'preferences'\n\nexport function isPreferences<V>(v: V) {\n  return is$typed(v, id, hashPreferences)\n}\n\nexport function validatePreferences<V>(v: V) {\n  return validate<Preferences & V>(v, id, hashPreferences)\n}\n\nexport interface ActivitySubscription {\n  $type?: 'app.bsky.notification.defs#activitySubscription'\n  post: boolean\n  reply: boolean\n}\n\nconst hashActivitySubscription = 'activitySubscription'\n\nexport function isActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashActivitySubscription)\n}\n\nexport function validateActivitySubscription<V>(v: V) {\n  return validate<ActivitySubscription & V>(v, id, hashActivitySubscription)\n}\n\n/** Object used to store activity subscription data in stash. */\nexport interface SubjectActivitySubscription {\n  $type?: 'app.bsky.notification.defs#subjectActivitySubscription'\n  subject: string\n  activitySubscription: ActivitySubscription\n}\n\nconst hashSubjectActivitySubscription = 'subjectActivitySubscription'\n\nexport function isSubjectActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashSubjectActivitySubscription)\n}\n\nexport function validateSubjectActivitySubscription<V>(v: V) {\n  return validate<SubjectActivitySubscription & V>(\n    v,\n    id,\n    hashSubjectActivitySubscription,\n  )\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/getUnreadCount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getUnreadCount'\n\nexport type QueryParams = {\n  priority?: boolean\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  count: number\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/listActivitySubscriptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listActivitySubscriptions'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subscriptions: AppBskyActorDefs.ProfileView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/listNotifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listNotifications'\n\nexport type QueryParams = {\n  /** Notification reasons to include in response. */\n  reasons?: string[]\n  limit?: number\n  priority?: boolean\n  cursor?: string\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  notifications: Notification[]\n  priority?: boolean\n  seenAt?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Notification {\n  $type?: 'app.bsky.notification.listNotifications#notification'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileView\n  /** The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. */\n  reason:\n    | 'like'\n    | 'repost'\n    | 'follow'\n    | 'mention'\n    | 'reply'\n    | 'quote'\n    | 'starterpack-joined'\n    | 'verified'\n    | 'unverified'\n    | 'like-via-repost'\n    | 'repost-via-repost'\n    | 'subscribed-post'\n    | 'contact-match'\n    | (string & {})\n  reasonSubject?: string\n  record: { [_ in string]: unknown }\n  isRead: boolean\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/putActivitySubscription.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putActivitySubscription'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject: string\n  activitySubscription: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface OutputSchema {\n  subject: string\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  priority: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/putPreferencesV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferencesV2'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  chat?: AppBskyNotificationDefs.ChatPreference\n  follow?: AppBskyNotificationDefs.FilterablePreference\n  like?: AppBskyNotificationDefs.FilterablePreference\n  likeViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  mention?: AppBskyNotificationDefs.FilterablePreference\n  quote?: AppBskyNotificationDefs.FilterablePreference\n  reply?: AppBskyNotificationDefs.FilterablePreference\n  repost?: AppBskyNotificationDefs.FilterablePreference\n  repostViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  starterpackJoined?: AppBskyNotificationDefs.Preference\n  subscribedPost?: AppBskyNotificationDefs.Preference\n  unverified?: AppBskyNotificationDefs.Preference\n  verified?: AppBskyNotificationDefs.Preference\n}\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/registerPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.registerPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n  /** Set to true when the actor is age restricted */\n  ageRestricted?: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/unregisterPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.unregisterPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/notification/updateSeen.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.updateSeen'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  seenAt: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/richtext/facet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.richtext.facet'\n\n/** Annotation of a sub-string within rich text. */\nexport interface Main {\n  $type?: 'app.bsky.richtext.facet'\n  index: ByteSlice\n  features: ($Typed<Mention> | $Typed<Link> | $Typed<Tag> | { $type: string })[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\n/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */\nexport interface Mention {\n  $type?: 'app.bsky.richtext.facet#mention'\n  did: string\n}\n\nconst hashMention = 'mention'\n\nexport function isMention<V>(v: V) {\n  return is$typed(v, id, hashMention)\n}\n\nexport function validateMention<V>(v: V) {\n  return validate<Mention & V>(v, id, hashMention)\n}\n\n/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */\nexport interface Link {\n  $type?: 'app.bsky.richtext.facet#link'\n  uri: string\n}\n\nconst hashLink = 'link'\n\nexport function isLink<V>(v: V) {\n  return is$typed(v, id, hashLink)\n}\n\nexport function validateLink<V>(v: V) {\n  return validate<Link & V>(v, id, hashLink)\n}\n\n/** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */\nexport interface Tag {\n  $type?: 'app.bsky.richtext.facet#tag'\n  tag: string\n}\n\nconst hashTag = 'tag'\n\nexport function isTag<V>(v: V) {\n  return is$typed(v, id, hashTag)\n}\n\nexport function validateTag<V>(v: V) {\n  return validate<Tag & V>(v, id, hashTag)\n}\n\n/** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */\nexport interface ByteSlice {\n  $type?: 'app.bsky.richtext.facet#byteSlice'\n  byteStart: number\n  byteEnd: number\n}\n\nconst hashByteSlice = 'byteSlice'\n\nexport function isByteSlice<V>(v: V) {\n  return is$typed(v, id, hashByteSlice)\n}\n\nexport function validateByteSlice<V>(v: V) {\n  return validate<ByteSlice & V>(v, id, hashByteSlice)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.defs'\n\nexport interface SkeletonSearchPost {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchPost'\n  uri: string\n}\n\nconst hashSkeletonSearchPost = 'skeletonSearchPost'\n\nexport function isSkeletonSearchPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchPost)\n}\n\nexport function validateSkeletonSearchPost<V>(v: V) {\n  return validate<SkeletonSearchPost & V>(v, id, hashSkeletonSearchPost)\n}\n\nexport interface SkeletonSearchActor {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchActor'\n  did: string\n}\n\nconst hashSkeletonSearchActor = 'skeletonSearchActor'\n\nexport function isSkeletonSearchActor<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchActor)\n}\n\nexport function validateSkeletonSearchActor<V>(v: V) {\n  return validate<SkeletonSearchActor & V>(v, id, hashSkeletonSearchActor)\n}\n\nexport interface SkeletonSearchStarterPack {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchStarterPack'\n  uri: string\n}\n\nconst hashSkeletonSearchStarterPack = 'skeletonSearchStarterPack'\n\nexport function isSkeletonSearchStarterPack<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchStarterPack)\n}\n\nexport function validateSkeletonSearchStarterPack<V>(v: V) {\n  return validate<SkeletonSearchStarterPack & V>(\n    v,\n    id,\n    hashSkeletonSearchStarterPack,\n  )\n}\n\nexport interface TrendingTopic {\n  $type?: 'app.bsky.unspecced.defs#trendingTopic'\n  topic: string\n  displayName?: string\n  description?: string\n  link: string\n}\n\nconst hashTrendingTopic = 'trendingTopic'\n\nexport function isTrendingTopic<V>(v: V) {\n  return is$typed(v, id, hashTrendingTopic)\n}\n\nexport function validateTrendingTopic<V>(v: V) {\n  return validate<TrendingTopic & V>(v, id, hashTrendingTopic)\n}\n\nexport interface SkeletonTrend {\n  $type?: 'app.bsky.unspecced.defs#skeletonTrend'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  dids: string[]\n}\n\nconst hashSkeletonTrend = 'skeletonTrend'\n\nexport function isSkeletonTrend<V>(v: V) {\n  return is$typed(v, id, hashSkeletonTrend)\n}\n\nexport function validateSkeletonTrend<V>(v: V) {\n  return validate<SkeletonTrend & V>(v, id, hashSkeletonTrend)\n}\n\nexport interface TrendView {\n  $type?: 'app.bsky.unspecced.defs#trendView'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nconst hashTrendView = 'trendView'\n\nexport function isTrendView<V>(v: V) {\n  return is$typed(v, id, hashTrendView)\n}\n\nexport function validateTrendView<V>(v: V) {\n  return validate<TrendView & V>(v, id, hashTrendView)\n}\n\nexport interface ThreadItemPost {\n  $type?: 'app.bsky.unspecced.defs#threadItemPost'\n  post: AppBskyFeedDefs.PostView\n  /** This post has more parents that were not present in the response. This is just a boolean, without the number of parents. */\n  moreParents: boolean\n  /** This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. */\n  moreReplies: number\n  /** This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. */\n  opThread: boolean\n  /** The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. */\n  hiddenByThreadgate: boolean\n  /** This is by an account muted by the viewer requesting it. */\n  mutedByViewer: boolean\n}\n\nconst hashThreadItemPost = 'threadItemPost'\n\nexport function isThreadItemPost<V>(v: V) {\n  return is$typed(v, id, hashThreadItemPost)\n}\n\nexport function validateThreadItemPost<V>(v: V) {\n  return validate<ThreadItemPost & V>(v, id, hashThreadItemPost)\n}\n\nexport interface ThreadItemNoUnauthenticated {\n  $type?: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated'\n}\n\nconst hashThreadItemNoUnauthenticated = 'threadItemNoUnauthenticated'\n\nexport function isThreadItemNoUnauthenticated<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNoUnauthenticated)\n}\n\nexport function validateThreadItemNoUnauthenticated<V>(v: V) {\n  return validate<ThreadItemNoUnauthenticated & V>(\n    v,\n    id,\n    hashThreadItemNoUnauthenticated,\n  )\n}\n\nexport interface ThreadItemNotFound {\n  $type?: 'app.bsky.unspecced.defs#threadItemNotFound'\n}\n\nconst hashThreadItemNotFound = 'threadItemNotFound'\n\nexport function isThreadItemNotFound<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNotFound)\n}\n\nexport function validateThreadItemNotFound<V>(v: V) {\n  return validate<ThreadItemNotFound & V>(v, id, hashThreadItemNotFound)\n}\n\nexport interface ThreadItemBlocked {\n  $type?: 'app.bsky.unspecced.defs#threadItemBlocked'\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashThreadItemBlocked = 'threadItemBlocked'\n\nexport function isThreadItemBlocked<V>(v: V) {\n  return is$typed(v, id, hashThreadItemBlocked)\n}\n\nexport function validateThreadItemBlocked<V>(v: V) {\n  return validate<ThreadItemBlocked & V>(v, id, hashThreadItemBlocked)\n}\n\n/** The computed state of the age assurance process, returned to the user in question on certain authenticated requests. */\nexport interface AgeAssuranceState {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceState'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n}\n\nconst hashAgeAssuranceState = 'ageAssuranceState'\n\nexport function isAgeAssuranceState<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceState)\n}\n\nexport function validateAgeAssuranceState<V>(v: V) {\n  return validate<AgeAssuranceState & V>(v, id, hashAgeAssuranceState)\n}\n\n/** Object used to store age assurance data in stash. */\nexport interface AgeAssuranceEvent {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The email used for AA. */\n  email?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getAgeAssuranceState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getAgeAssuranceState'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  checkEmailConfirmed?: boolean\n  liveNow?: LiveNowConfig[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface LiveNowConfig {\n  $type?: 'app.bsky.unspecced.getConfig#liveNowConfig'\n  did: string\n  domains: string[]\n}\n\nconst hashLiveNowConfig = 'liveNowConfig'\n\nexport function isLiveNowConfig<V>(v: V) {\n  return is$typed(v, id, hashLiveNowConfig)\n}\n\nexport function validateLiveNowConfig<V>(v: V) {\n  return validate<LiveNowConfig & V>(v, id, hashLiveNowConfig)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPopularFeedGenerators'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n  query?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getPostThreadOtherV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadOtherV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post. */\n  anchor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of other thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadOtherV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getPostThreadV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post. */\n  anchor: string\n  /** Whether to include parents above the anchor. */\n  above?: boolean\n  /** How many levels of replies to include below the anchor. */\n  below?: number\n  /** Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated). */\n  branchingFactor?: number\n  /** Sorting for the thread replies. */\n  sort?: 'newest' | 'oldest' | 'top' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n  /** Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them. */\n  hasOtherReplies: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value:\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemPost>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNotFound>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemBlocked>\n    | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedFeedsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeedsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedOnboardingUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedOnboardingUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getSuggestionsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestionsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit?: number\n  cursor?: string\n  /** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. */\n  relativeToDid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n  /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */\n  relativeToDid?: string\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getTaggedSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTaggedSuggestions'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: Suggestion[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Suggestion {\n  $type?: 'app.bsky.unspecced.getTaggedSuggestions#suggestion'\n  tag: string\n  subjectType: 'actor' | 'feed' | (string & {})\n  subject: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getTrendingTopics.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendingTopics'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  topics: AppBskyUnspeccedDefs.TrendingTopic[]\n  suggested: AppBskyUnspeccedDefs.TrendingTopic[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getTrends.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrends'\n\nexport type QueryParams = {\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.TrendView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/getTrendsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.SkeletonTrend[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/initAgeAssurance.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.initAgeAssurance'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n}\n\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidEmailError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidTooLongError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidInitiationError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidEmail') return new InvalidEmailError(e)\n    if (e.error === 'DidTooLong') return new DidTooLongError(e)\n    if (e.error === 'InvalidInitiation') return new InvalidInitiationError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/searchActorsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchActorsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  /** If true, acts as fast/simple 'typeahead' query. */\n  typeahead?: boolean\n  limit?: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BadQueryStringError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BadQueryString') return new BadQueryStringError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/searchPostsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchPostsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort?: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries. */\n  viewer?: string\n  limit?: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyUnspeccedDefs.SkeletonSearchPost[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BadQueryStringError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BadQueryString') return new BadQueryStringError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/unspecced/searchStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit?: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  starterPacks: AppBskyUnspeccedDefs.SkeletonSearchStarterPack[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BadQueryStringError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BadQueryString') return new BadQueryStringError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/video/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.defs'\n\nexport interface JobStatus {\n  $type?: 'app.bsky.video.defs#jobStatus'\n  jobId: string\n  did: string\n  /** The state of the video processing job. All values not listed as a known value indicate that the job is in process. */\n  state: 'JOB_STATE_COMPLETED' | 'JOB_STATE_FAILED' | (string & {})\n  /** Progress within the current processing state. */\n  progress?: number\n  blob?: BlobRef\n  error?: string\n  message?: string\n}\n\nconst hashJobStatus = 'jobStatus'\n\nexport function isJobStatus<V>(v: V) {\n  return is$typed(v, id, hashJobStatus)\n}\n\nexport function validateJobStatus<V>(v: V) {\n  return validate<JobStatus & V>(v, id, hashJobStatus)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/video/getJobStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getJobStatus'\n\nexport type QueryParams = {\n  jobId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/video/getUploadLimits.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getUploadLimits'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canUpload: boolean\n  remainingDailyVideos?: number\n  remainingDailyBytes?: number\n  message?: string\n  error?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/app/bsky/video/uploadVideo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.uploadVideo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'video/mp4'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/actor/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.declaration'\n\nexport interface Main {\n  $type: 'chat.bsky.actor.declaration'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'chat.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  avatar?: string\n  associated?: AppBskyActorDefs.ProfileAssociated\n  viewer?: AppBskyActorDefs.ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** Set to true when the actor cannot actively participate in conversations */\n  chatDisabled?: boolean\n  verification?: AppBskyActorDefs.VerificationState\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/actor/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.deleteAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/actor/exportAccountData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.exportAccountData'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: Uint8Array\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/acceptConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.acceptConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  /** Rev when the convo was accepted. If not present, the convo was already accepted. */\n  rev?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/addReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.addReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class ReactionMessageDeletedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class ReactionLimitReachedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class ReactionInvalidValueError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'ReactionMessageDeleted')\n      return new ReactionMessageDeletedError(e)\n    if (e.error === 'ReactionLimitReached')\n      return new ReactionLimitReachedError(e)\n    if (e.error === 'ReactionInvalidValue')\n      return new ReactionInvalidValueError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../../../app/bsky/richtext/facet.js'\nimport type * as AppBskyEmbedRecord from '../../../app/bsky/embed/record.js'\nimport type * as ChatBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.defs'\n\nexport interface MessageRef {\n  $type?: 'chat.bsky.convo.defs#messageRef'\n  did: string\n  convoId: string\n  messageId: string\n}\n\nconst hashMessageRef = 'messageRef'\n\nexport function isMessageRef<V>(v: V) {\n  return is$typed(v, id, hashMessageRef)\n}\n\nexport function validateMessageRef<V>(v: V) {\n  return validate<MessageRef & V>(v, id, hashMessageRef)\n}\n\nexport interface MessageInput {\n  $type?: 'chat.bsky.convo.defs#messageInput'\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.Main> | { $type: string }\n}\n\nconst hashMessageInput = 'messageInput'\n\nexport function isMessageInput<V>(v: V) {\n  return is$typed(v, id, hashMessageInput)\n}\n\nexport function validateMessageInput<V>(v: V) {\n  return validate<MessageInput & V>(v, id, hashMessageInput)\n}\n\nexport interface MessageView {\n  $type?: 'chat.bsky.convo.defs#messageView'\n  id: string\n  rev: string\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.View> | { $type: string }\n  /** Reactions to this message, in ascending order of creation time. */\n  reactions?: ReactionView[]\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashMessageView = 'messageView'\n\nexport function isMessageView<V>(v: V) {\n  return is$typed(v, id, hashMessageView)\n}\n\nexport function validateMessageView<V>(v: V) {\n  return validate<MessageView & V>(v, id, hashMessageView)\n}\n\nexport interface DeletedMessageView {\n  $type?: 'chat.bsky.convo.defs#deletedMessageView'\n  id: string\n  rev: string\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashDeletedMessageView = 'deletedMessageView'\n\nexport function isDeletedMessageView<V>(v: V) {\n  return is$typed(v, id, hashDeletedMessageView)\n}\n\nexport function validateDeletedMessageView<V>(v: V) {\n  return validate<DeletedMessageView & V>(v, id, hashDeletedMessageView)\n}\n\nexport interface MessageViewSender {\n  $type?: 'chat.bsky.convo.defs#messageViewSender'\n  did: string\n}\n\nconst hashMessageViewSender = 'messageViewSender'\n\nexport function isMessageViewSender<V>(v: V) {\n  return is$typed(v, id, hashMessageViewSender)\n}\n\nexport function validateMessageViewSender<V>(v: V) {\n  return validate<MessageViewSender & V>(v, id, hashMessageViewSender)\n}\n\nexport interface ReactionView {\n  $type?: 'chat.bsky.convo.defs#reactionView'\n  value: string\n  sender: ReactionViewSender\n  createdAt: string\n}\n\nconst hashReactionView = 'reactionView'\n\nexport function isReactionView<V>(v: V) {\n  return is$typed(v, id, hashReactionView)\n}\n\nexport function validateReactionView<V>(v: V) {\n  return validate<ReactionView & V>(v, id, hashReactionView)\n}\n\nexport interface ReactionViewSender {\n  $type?: 'chat.bsky.convo.defs#reactionViewSender'\n  did: string\n}\n\nconst hashReactionViewSender = 'reactionViewSender'\n\nexport function isReactionViewSender<V>(v: V) {\n  return is$typed(v, id, hashReactionViewSender)\n}\n\nexport function validateReactionViewSender<V>(v: V) {\n  return validate<ReactionViewSender & V>(v, id, hashReactionViewSender)\n}\n\nexport interface MessageAndReactionView {\n  $type?: 'chat.bsky.convo.defs#messageAndReactionView'\n  message: MessageView\n  reaction: ReactionView\n}\n\nconst hashMessageAndReactionView = 'messageAndReactionView'\n\nexport function isMessageAndReactionView<V>(v: V) {\n  return is$typed(v, id, hashMessageAndReactionView)\n}\n\nexport function validateMessageAndReactionView<V>(v: V) {\n  return validate<MessageAndReactionView & V>(v, id, hashMessageAndReactionView)\n}\n\nexport interface ConvoView {\n  $type?: 'chat.bsky.convo.defs#convoView'\n  id: string\n  rev: string\n  members: ChatBskyActorDefs.ProfileViewBasic[]\n  lastMessage?:\n    | $Typed<MessageView>\n    | $Typed<DeletedMessageView>\n    | { $type: string }\n  lastReaction?: $Typed<MessageAndReactionView> | { $type: string }\n  muted: boolean\n  status?: 'request' | 'accepted' | (string & {})\n  unreadCount: number\n}\n\nconst hashConvoView = 'convoView'\n\nexport function isConvoView<V>(v: V) {\n  return is$typed(v, id, hashConvoView)\n}\n\nexport function validateConvoView<V>(v: V) {\n  return validate<ConvoView & V>(v, id, hashConvoView)\n}\n\nexport interface LogBeginConvo {\n  $type?: 'chat.bsky.convo.defs#logBeginConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogBeginConvo = 'logBeginConvo'\n\nexport function isLogBeginConvo<V>(v: V) {\n  return is$typed(v, id, hashLogBeginConvo)\n}\n\nexport function validateLogBeginConvo<V>(v: V) {\n  return validate<LogBeginConvo & V>(v, id, hashLogBeginConvo)\n}\n\nexport interface LogAcceptConvo {\n  $type?: 'chat.bsky.convo.defs#logAcceptConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogAcceptConvo = 'logAcceptConvo'\n\nexport function isLogAcceptConvo<V>(v: V) {\n  return is$typed(v, id, hashLogAcceptConvo)\n}\n\nexport function validateLogAcceptConvo<V>(v: V) {\n  return validate<LogAcceptConvo & V>(v, id, hashLogAcceptConvo)\n}\n\nexport interface LogLeaveConvo {\n  $type?: 'chat.bsky.convo.defs#logLeaveConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogLeaveConvo = 'logLeaveConvo'\n\nexport function isLogLeaveConvo<V>(v: V) {\n  return is$typed(v, id, hashLogLeaveConvo)\n}\n\nexport function validateLogLeaveConvo<V>(v: V) {\n  return validate<LogLeaveConvo & V>(v, id, hashLogLeaveConvo)\n}\n\nexport interface LogMuteConvo {\n  $type?: 'chat.bsky.convo.defs#logMuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogMuteConvo = 'logMuteConvo'\n\nexport function isLogMuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogMuteConvo)\n}\n\nexport function validateLogMuteConvo<V>(v: V) {\n  return validate<LogMuteConvo & V>(v, id, hashLogMuteConvo)\n}\n\nexport interface LogUnmuteConvo {\n  $type?: 'chat.bsky.convo.defs#logUnmuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogUnmuteConvo = 'logUnmuteConvo'\n\nexport function isLogUnmuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogUnmuteConvo)\n}\n\nexport function validateLogUnmuteConvo<V>(v: V) {\n  return validate<LogUnmuteConvo & V>(v, id, hashLogUnmuteConvo)\n}\n\nexport interface LogCreateMessage {\n  $type?: 'chat.bsky.convo.defs#logCreateMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogCreateMessage = 'logCreateMessage'\n\nexport function isLogCreateMessage<V>(v: V) {\n  return is$typed(v, id, hashLogCreateMessage)\n}\n\nexport function validateLogCreateMessage<V>(v: V) {\n  return validate<LogCreateMessage & V>(v, id, hashLogCreateMessage)\n}\n\nexport interface LogDeleteMessage {\n  $type?: 'chat.bsky.convo.defs#logDeleteMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogDeleteMessage = 'logDeleteMessage'\n\nexport function isLogDeleteMessage<V>(v: V) {\n  return is$typed(v, id, hashLogDeleteMessage)\n}\n\nexport function validateLogDeleteMessage<V>(v: V) {\n  return validate<LogDeleteMessage & V>(v, id, hashLogDeleteMessage)\n}\n\nexport interface LogReadMessage {\n  $type?: 'chat.bsky.convo.defs#logReadMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogReadMessage = 'logReadMessage'\n\nexport function isLogReadMessage<V>(v: V) {\n  return is$typed(v, id, hashLogReadMessage)\n}\n\nexport function validateLogReadMessage<V>(v: V) {\n  return validate<LogReadMessage & V>(v, id, hashLogReadMessage)\n}\n\nexport interface LogAddReaction {\n  $type?: 'chat.bsky.convo.defs#logAddReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogAddReaction = 'logAddReaction'\n\nexport function isLogAddReaction<V>(v: V) {\n  return is$typed(v, id, hashLogAddReaction)\n}\n\nexport function validateLogAddReaction<V>(v: V) {\n  return validate<LogAddReaction & V>(v, id, hashLogAddReaction)\n}\n\nexport interface LogRemoveReaction {\n  $type?: 'chat.bsky.convo.defs#logRemoveReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogRemoveReaction = 'logRemoveReaction'\n\nexport function isLogRemoveReaction<V>(v: V) {\n  return is$typed(v, id, hashLogRemoveReaction)\n}\n\nexport function validateLogRemoveReaction<V>(v: V) {\n  return validate<LogRemoveReaction & V>(v, id, hashLogRemoveReaction)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/deleteMessageForSelf.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.deleteMessageForSelf'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.DeletedMessageView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/getConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvo'\n\nexport type QueryParams = {\n  convoId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/getConvoAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoAvailability'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canChat: boolean\n  convo?: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/getConvoForMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoForMembers'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/getLog.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getLog'\n\nexport type QueryParams = {\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  logs: (\n    | $Typed<ChatBskyConvoDefs.LogBeginConvo>\n    | $Typed<ChatBskyConvoDefs.LogAcceptConvo>\n    | $Typed<ChatBskyConvoDefs.LogLeaveConvo>\n    | $Typed<ChatBskyConvoDefs.LogMuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogUnmuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogCreateMessage>\n    | $Typed<ChatBskyConvoDefs.LogDeleteMessage>\n    | $Typed<ChatBskyConvoDefs.LogReadMessage>\n    | $Typed<ChatBskyConvoDefs.LogAddReaction>\n    | $Typed<ChatBskyConvoDefs.LogRemoveReaction>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/getMessages.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getMessages'\n\nexport type QueryParams = {\n  convoId: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/leaveConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.leaveConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convoId: string\n  rev: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/listConvos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.listConvos'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n  readState?: 'unread' | (string & {})\n  status?: 'request' | 'accepted' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  convos: ChatBskyConvoDefs.ConvoView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/muteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.muteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/removeReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.removeReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class ReactionMessageDeletedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class ReactionInvalidValueError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'ReactionMessageDeleted')\n      return new ReactionMessageDeletedError(e)\n    if (e.error === 'ReactionInvalidValue')\n      return new ReactionInvalidValueError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/sendMessage.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessage'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.MessageView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/sendMessageBatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessageBatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  items: BatchItem[]\n}\n\nexport interface OutputSchema {\n  items: ChatBskyConvoDefs.MessageView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface BatchItem {\n  $type?: 'chat.bsky.convo.sendMessageBatch#batchItem'\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nconst hashBatchItem = 'batchItem'\n\nexport function isBatchItem<V>(v: V) {\n  return is$typed(v, id, hashBatchItem)\n}\n\nexport function validateBatchItem<V>(v: V) {\n  return validate<BatchItem & V>(v, id, hashBatchItem)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/unmuteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.unmuteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/updateAllRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateAllRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  status?: 'request' | 'accepted' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** The count of updated convos. */\n  updatedCount: number\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/convo/updateRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId?: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/moderation/getActorMetadata.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getActorMetadata'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  day: Metadata\n  month: Metadata\n  all: Metadata\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Metadata {\n  $type?: 'chat.bsky.moderation.getActorMetadata#metadata'\n  messagesSent: number\n  messagesReceived: number\n  convos: number\n  convosStarted: number\n}\n\nconst hashMetadata = 'metadata'\n\nexport function isMetadata<V>(v: V) {\n  return is$typed(v, id, hashMetadata)\n}\n\nexport function validateMetadata<V>(v: V) {\n  return validate<Metadata & V>(v, id, hashMetadata)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/moderation/getMessageContext.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from '../convo/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getMessageContext'\n\nexport type QueryParams = {\n  /** Conversation that the message is from. NOTE: this field will eventually be required. */\n  convoId?: string\n  messageId: string\n  before?: number\n  after?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/chat/bsky/moderation/updateActorAccess.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.updateActorAccess'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n  allowAccess: boolean\n  ref?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.defs'\n\nexport interface StatusAttr {\n  $type?: 'com.atproto.admin.defs#statusAttr'\n  applied: boolean\n  ref?: string\n}\n\nconst hashStatusAttr = 'statusAttr'\n\nexport function isStatusAttr<V>(v: V) {\n  return is$typed(v, id, hashStatusAttr)\n}\n\nexport function validateStatusAttr<V>(v: V) {\n  return validate<StatusAttr & V>(v, id, hashStatusAttr)\n}\n\nexport interface AccountView {\n  $type?: 'com.atproto.admin.defs#accountView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords?: { [_ in string]: unknown }[]\n  indexedAt: string\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  emailConfirmedAt?: string\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ThreatSignature[]\n}\n\nconst hashAccountView = 'accountView'\n\nexport function isAccountView<V>(v: V) {\n  return is$typed(v, id, hashAccountView)\n}\n\nexport function validateAccountView<V>(v: V) {\n  return validate<AccountView & V>(v, id, hashAccountView)\n}\n\nexport interface RepoRef {\n  $type?: 'com.atproto.admin.defs#repoRef'\n  did: string\n}\n\nconst hashRepoRef = 'repoRef'\n\nexport function isRepoRef<V>(v: V) {\n  return is$typed(v, id, hashRepoRef)\n}\n\nexport function validateRepoRef<V>(v: V) {\n  return validate<RepoRef & V>(v, id, hashRepoRef)\n}\n\nexport interface RepoBlobRef {\n  $type?: 'com.atproto.admin.defs#repoBlobRef'\n  did: string\n  cid: string\n  recordUri?: string\n}\n\nconst hashRepoBlobRef = 'repoBlobRef'\n\nexport function isRepoBlobRef<V>(v: V) {\n  return is$typed(v, id, hashRepoBlobRef)\n}\n\nexport function validateRepoBlobRef<V>(v: V) {\n  return validate<RepoBlobRef & V>(v, id, hashRepoBlobRef)\n}\n\nexport interface ThreatSignature {\n  $type?: 'com.atproto.admin.defs#threatSignature'\n  property: string\n  value: string\n}\n\nconst hashThreatSignature = 'threatSignature'\n\nexport function isThreatSignature<V>(v: V) {\n  return is$typed(v, id, hashThreatSignature)\n}\n\nexport function validateThreatSignature<V>(v: V) {\n  return validate<ThreatSignature & V>(v, id, hashThreatSignature)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/disableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for disabled invites. */\n  note?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/disableInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codes?: string[]\n  accounts?: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.enableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for enabled invites. */\n  note?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/getAccountInfo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoAdminDefs.AccountView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/getAccountInfos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  infos: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/getInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getInviteCodes'\n\nexport type QueryParams = {\n  sort?: 'recent' | 'usage' | (string & {})\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/getSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getSubjectStatus'\n\nexport type QueryParams = {\n  did?: string\n  uri?: string\n  blob?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.searchAccounts'\n\nexport type QueryParams = {\n  email?: string\n  cursor?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/sendEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.sendEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  recipientDid: string\n  content: string\n  subject?: string\n  senderDid: string\n  /** Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers */\n  comment?: string\n}\n\nexport interface OutputSchema {\n  sent: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/updateAccountEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo. */\n  account: string\n  email: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/updateAccountHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  handle: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/updateAccountPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/updateAccountSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  /** Did-key formatted public key */\n  signingKey: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/admin/updateSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateSubjectStatus'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.defs'\n\nexport interface IdentityInfo {\n  $type?: 'com.atproto.identity.defs#identityInfo'\n  did: string\n  /** The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document. */\n  handle: string\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nconst hashIdentityInfo = 'identityInfo'\n\nexport function isIdentityInfo<V>(v: V) {\n  return is$typed(v, id, hashIdentityInfo)\n}\n\nexport function validateIdentityInfo<V>(v: V) {\n  return validate<IdentityInfo & V>(v, id, hashIdentityInfo)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/getRecommendedDidCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.getRecommendedDidCredentials'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs. */\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/refreshIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.refreshIdentity'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  identifier: string\n}\n\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class HandleNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'HandleNotFound') return new HandleNotFoundError(e)\n    if (e.error === 'DidNotFound') return new DidNotFoundError(e)\n    if (e.error === 'DidDeactivated') return new DidDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/requestPlcOperationSignature.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.requestPlcOperationSignature'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/resolveDid.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveDid'\n\nexport type QueryParams = {\n  /** DID to resolve. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class DidNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'DidNotFound') return new DidNotFoundError(e)\n    if (e.error === 'DidDeactivated') return new DidDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/resolveHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveHandle'\n\nexport type QueryParams = {\n  /** The handle to resolve. */\n  handle: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class HandleNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'HandleNotFound') return new HandleNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/resolveIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveIdentity'\n\nexport type QueryParams = {\n  /** Handle or DID to resolve. */\n  identifier: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class HandleNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DidDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'HandleNotFound') return new HandleNotFoundError(e)\n    if (e.error === 'DidNotFound') return new DidNotFoundError(e)\n    if (e.error === 'DidDeactivated') return new DidDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/signPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.signPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A token received through com.atproto.identity.requestPlcOperationSignature */\n  token?: string\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport interface OutputSchema {\n  /** A signed DID PLC operation. */\n  operation: { [_ in string]: unknown }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/submitPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.submitPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  operation: { [_ in string]: unknown }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/identity/updateHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.updateHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The new handle. */\n  handle: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/label/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.defs'\n\n/** Metadata tag on an atproto resource (eg, repo or record). */\nexport interface Label {\n  $type?: 'com.atproto.label.defs#label'\n  /** The AT Protocol version of the label object. */\n  ver?: number\n  /** DID of the actor who created this label. */\n  src: string\n  /** AT URI of the record, repository (account), or other resource that this label applies to. */\n  uri: string\n  /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */\n  cid?: string\n  /** The short string name of the value or type of this label. */\n  val: string\n  /** If true, this is a negation label, overwriting a previous label. */\n  neg?: boolean\n  /** Timestamp when this label was created. */\n  cts: string\n  /** Timestamp at which this label expires (no longer applies). */\n  exp?: string\n  /** Signature of dag-cbor encoded label. */\n  sig?: Uint8Array\n}\n\nconst hashLabel = 'label'\n\nexport function isLabel<V>(v: V) {\n  return is$typed(v, id, hashLabel)\n}\n\nexport function validateLabel<V>(v: V) {\n  return validate<Label & V>(v, id, hashLabel)\n}\n\n/** Metadata tags on an atproto record, published by the author within the record. */\nexport interface SelfLabels {\n  $type?: 'com.atproto.label.defs#selfLabels'\n  values: SelfLabel[]\n}\n\nconst hashSelfLabels = 'selfLabels'\n\nexport function isSelfLabels<V>(v: V) {\n  return is$typed(v, id, hashSelfLabels)\n}\n\nexport function validateSelfLabels<V>(v: V) {\n  return validate<SelfLabels & V>(v, id, hashSelfLabels)\n}\n\n/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */\nexport interface SelfLabel {\n  $type?: 'com.atproto.label.defs#selfLabel'\n  /** The short string name of the value or type of this label. */\n  val: string\n}\n\nconst hashSelfLabel = 'selfLabel'\n\nexport function isSelfLabel<V>(v: V) {\n  return is$typed(v, id, hashSelfLabel)\n}\n\nexport function validateSelfLabel<V>(v: V) {\n  return validate<SelfLabel & V>(v, id, hashSelfLabel)\n}\n\n/** Declares a label value and its expected interpretations and behaviors. */\nexport interface LabelValueDefinition {\n  $type?: 'com.atproto.label.defs#labelValueDefinition'\n  /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */\n  identifier: string\n  /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */\n  severity: 'inform' | 'alert' | 'none' | (string & {})\n  /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */\n  blurs: 'content' | 'media' | 'none' | (string & {})\n  /** The default setting for this label. */\n  defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})\n  /** Does the user need to have adult content enabled in order to configure this label? */\n  adultOnly?: boolean\n  locales: LabelValueDefinitionStrings[]\n}\n\nconst hashLabelValueDefinition = 'labelValueDefinition'\n\nexport function isLabelValueDefinition<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinition)\n}\n\nexport function validateLabelValueDefinition<V>(v: V) {\n  return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)\n}\n\n/** Strings which describe the label in the UI, localized into a specific language. */\nexport interface LabelValueDefinitionStrings {\n  $type?: 'com.atproto.label.defs#labelValueDefinitionStrings'\n  /** The code of the language these strings are written in. */\n  lang: string\n  /** A short human-readable name for the label. */\n  name: string\n  /** A longer description of what the label means and why it might be applied. */\n  description: string\n}\n\nconst hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'\n\nexport function isLabelValueDefinitionStrings<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinitionStrings)\n}\n\nexport function validateLabelValueDefinitionStrings<V>(v: V) {\n  return validate<LabelValueDefinitionStrings & V>(\n    v,\n    id,\n    hashLabelValueDefinitionStrings,\n  )\n}\n\nexport type LabelValue =\n  | '!hide'\n  | '!no-promote'\n  | '!warn'\n  | '!no-unauthenticated'\n  | 'dmca-violation'\n  | 'doxxing'\n  | 'porn'\n  | 'sexual'\n  | 'nudity'\n  | 'nsfl'\n  | 'gore'\n  | (string & {})\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/label/queryLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.queryLabels'\n\nexport type QueryParams = {\n  /** List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI. */\n  uriPatterns: string[]\n  /** Optional list of label sources (DIDs) to filter on. */\n  sources?: string[]\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/label/subscribeLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.subscribeLabels'\n\nexport interface Labels {\n  $type?: 'com.atproto.label.subscribeLabels#labels'\n  seq: number\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabels = 'labels'\n\nexport function isLabels<V>(v: V) {\n  return is$typed(v, id, hashLabels)\n}\n\nexport function validateLabels<V>(v: V) {\n  return validate<Labels & V>(v, id, hashLabels)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.label.subscribeLabels#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/lexicon/resolveLexicon.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLexiconSchema from './schema.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.resolveLexicon'\n\nexport type QueryParams = {\n  /** The lexicon NSID to resolve. */\n  nsid: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The CID of the lexicon schema record. */\n  cid: string\n  schema: ComAtprotoLexiconSchema.Main\n  /** The AT-URI of the lexicon schema record. */\n  uri: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class LexiconNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'LexiconNotFound') return new LexiconNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/lexicon/schema.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.schema'\n\nexport interface Main {\n  $type: 'com.atproto.lexicon.schema'\n  /** Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system. */\n  lexicon: number\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/moderation/createReport.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.createReport'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  /** Additional context about the content and violation. */\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  modTool?: ModTool\n}\n\nexport interface OutputSchema {\n  id: number\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  reportedBy: string\n  createdAt: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'com.atproto.moderation.createReport#modTool'\n  /** Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.defs'\n\nexport type ReasonType =\n  | 'com.atproto.moderation.defs#reasonSpam'\n  | 'com.atproto.moderation.defs#reasonViolation'\n  | 'com.atproto.moderation.defs#reasonMisleading'\n  | 'com.atproto.moderation.defs#reasonSexual'\n  | 'com.atproto.moderation.defs#reasonRude'\n  | 'com.atproto.moderation.defs#reasonOther'\n  | 'com.atproto.moderation.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`. */\nexport const REASONSPAM = `${id}#reasonSpam`\n/** Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`. */\nexport const REASONVIOLATION = `${id}#reasonViolation`\n/** Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`. */\nexport const REASONMISLEADING = `${id}#reasonMisleading`\n/** Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`. */\nexport const REASONSEXUAL = `${id}#reasonSexual`\n/** Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`. */\nexport const REASONRUDE = `${id}#reasonRude`\n/** Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`. */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n\n/** Tag describing a type of subject that might be reported. */\nexport type SubjectType = 'account' | 'record' | 'chat' | (string & {})\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/applyWrites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.applyWrites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  writes: ($Typed<Create> | $Typed<Update> | $Typed<Delete>)[]\n  /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  results?: (\n    | $Typed<CreateResult>\n    | $Typed<UpdateResult>\n    | $Typed<DeleteResult>\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidSwapError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidSwap') return new InvalidSwapError(e)\n  }\n\n  return e\n}\n\n/** Operation which creates a new record. */\nexport interface Create {\n  $type?: 'com.atproto.repo.applyWrites#create'\n  collection: string\n  /** NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. */\n  rkey?: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashCreate = 'create'\n\nexport function isCreate<V>(v: V) {\n  return is$typed(v, id, hashCreate)\n}\n\nexport function validateCreate<V>(v: V) {\n  return validate<Create & V>(v, id, hashCreate)\n}\n\n/** Operation which updates an existing record. */\nexport interface Update {\n  $type?: 'com.atproto.repo.applyWrites#update'\n  collection: string\n  rkey: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashUpdate = 'update'\n\nexport function isUpdate<V>(v: V) {\n  return is$typed(v, id, hashUpdate)\n}\n\nexport function validateUpdate<V>(v: V) {\n  return validate<Update & V>(v, id, hashUpdate)\n}\n\n/** Operation which deletes an existing record. */\nexport interface Delete {\n  $type?: 'com.atproto.repo.applyWrites#delete'\n  collection: string\n  rkey: string\n}\n\nconst hashDelete = 'delete'\n\nexport function isDelete<V>(v: V) {\n  return is$typed(v, id, hashDelete)\n}\n\nexport function validateDelete<V>(v: V) {\n  return validate<Delete & V>(v, id, hashDelete)\n}\n\nexport interface CreateResult {\n  $type?: 'com.atproto.repo.applyWrites#createResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashCreateResult = 'createResult'\n\nexport function isCreateResult<V>(v: V) {\n  return is$typed(v, id, hashCreateResult)\n}\n\nexport function validateCreateResult<V>(v: V) {\n  return validate<CreateResult & V>(v, id, hashCreateResult)\n}\n\nexport interface UpdateResult {\n  $type?: 'com.atproto.repo.applyWrites#updateResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashUpdateResult = 'updateResult'\n\nexport function isUpdateResult<V>(v: V) {\n  return is$typed(v, id, hashUpdateResult)\n}\n\nexport function validateUpdateResult<V>(v: V) {\n  return validate<UpdateResult & V>(v, id, hashUpdateResult)\n}\n\nexport interface DeleteResult {\n  $type?: 'com.atproto.repo.applyWrites#deleteResult'\n}\n\nconst hashDeleteResult = 'deleteResult'\n\nexport function isDeleteResult<V>(v: V) {\n  return is$typed(v, id, hashDeleteResult)\n}\n\nexport function validateDeleteResult<V>(v: V) {\n  return validate<DeleteResult & V>(v, id, hashDeleteResult)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/createRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.createRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey?: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record itself. Must contain a $type field. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidSwapError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidSwap') return new InvalidSwapError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.defs'\n\nexport interface CommitMeta {\n  $type?: 'com.atproto.repo.defs#commitMeta'\n  cid: string\n  rev: string\n}\n\nconst hashCommitMeta = 'commitMeta'\n\nexport function isCommitMeta<V>(v: V) {\n  return is$typed(v, id, hashCommitMeta)\n}\n\nexport function validateCommitMeta<V>(v: V) {\n  return validate<CommitMeta & V>(v, id, hashCommitMeta)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/deleteRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.deleteRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Compare and swap with the previous record by CID. */\n  swapRecord?: string\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidSwapError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidSwap') return new InvalidSwapError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/describeRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.describeRepo'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  /** The complete DID document for this account. */\n  didDoc: { [_ in string]: unknown }\n  /** List of all the collections (NSIDs) for which this repo contains at least one record. */\n  collections: string[]\n  /** Indicates if handle is currently valid (resolves bi-directionally) */\n  handleIsCorrect: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.getRecord'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** The CID of the version of the record. If not specified, then return the most recent version. */\n  cid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  value: { [_ in string]: unknown }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RecordNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RecordNotFound') return new RecordNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/importRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.importRepo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/vnd.ipld.car'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/listMissingBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listMissingBlobs'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blobs: RecordBlob[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface RecordBlob {\n  $type?: 'com.atproto.repo.listMissingBlobs#recordBlob'\n  cid: string\n  recordUri: string\n}\n\nconst hashRecordBlob = 'recordBlob'\n\nexport function isRecordBlob<V>(v: V) {\n  return is$typed(v, id, hashRecordBlob)\n}\n\nexport function validateRecordBlob<V>(v: V) {\n  return validate<RecordBlob & V>(v, id, hashRecordBlob)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/listRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listRecords'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record type. */\n  collection: string\n  /** The number of records to return. */\n  limit?: number\n  cursor?: string\n  /** Flag to reverse the order of the returned records. */\n  reverse?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  records: Record[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Record {\n  $type?: 'com.atproto.repo.listRecords#record'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashRecord = 'record'\n\nexport function isRecord<V>(v: V) {\n  return is$typed(v, id, hashRecord)\n}\n\nexport function validateRecord<V>(v: V) {\n  return validate<Record & V>(v, id, hashRecord)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/putRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.putRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record to write. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */\n  swapRecord?: string | null\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidSwapError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidSwap') return new InvalidSwapError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/strongRef.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.strongRef'\n\nexport interface Main {\n  $type?: 'com.atproto.repo.strongRef'\n  uri: string\n  cid: string\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/repo/uploadBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.uploadBlob'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  blob: BlobRef\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: string\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/activateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.activateAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/checkAccountStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.checkAccountStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  validDid: boolean\n  repoCommit: string\n  repoRev: string\n  repoBlocks: number\n  indexedRecords: number\n  privateStateValues: number\n  expectedBlobs: number\n  importedBlobs: number\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/confirmEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.confirmEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  token: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class AccountNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class ExpiredTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidEmailError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'AccountNotFound') return new AccountNotFoundError(e)\n    if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n    if (e.error === 'InvalidEmail') return new InvalidEmailError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/createAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email?: string\n  /** Requested handle for the account. */\n  handle: string\n  /** Pre-existing atproto DID, being imported to a new account. */\n  did?: string\n  inviteCode?: string\n  verificationCode?: string\n  verificationPhone?: string\n  /** Initial account password. May need to meet instance-specific password strength requirements. */\n  password?: string\n  /** DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. */\n  recoveryKey?: string\n  /** A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. */\n  plcOp?: { [_ in string]: unknown }\n}\n\n/** Account login session returned on successful account creation. */\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  /** The DID of the new account. */\n  did: string\n  /** Complete DID document. */\n  didDoc?: { [_ in string]: unknown }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidHandleError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidPasswordError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidInviteCodeError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class HandleNotAvailableError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class UnsupportedDomainError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class UnresolvableDidError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class IncompatibleDidDocError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidHandle') return new InvalidHandleError(e)\n    if (e.error === 'InvalidPassword') return new InvalidPasswordError(e)\n    if (e.error === 'InvalidInviteCode') return new InvalidInviteCodeError(e)\n    if (e.error === 'HandleNotAvailable') return new HandleNotAvailableError(e)\n    if (e.error === 'UnsupportedDomain') return new UnsupportedDomainError(e)\n    if (e.error === 'UnresolvableDid') return new UnresolvableDidError(e)\n    if (e.error === 'IncompatibleDidDoc') return new IncompatibleDidDocError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/createAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A short name for the App Password, to help distinguish them. */\n  name: string\n  /** If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients. */\n  privileged?: boolean\n}\n\nexport type OutputSchema = AppPassword\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class AccountTakedownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'AccountTakedown') return new AccountTakedownError(e)\n  }\n\n  return e\n}\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.createAppPassword#appPassword'\n  name: string\n  password: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/createInviteCode.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCode'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  useCount: number\n  forAccount?: string\n}\n\nexport interface OutputSchema {\n  code: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/createInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codeCount: number\n  useCount: number\n  forAccounts?: string[]\n}\n\nexport interface OutputSchema {\n  codes: AccountCodes[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface AccountCodes {\n  $type?: 'com.atproto.server.createInviteCodes#accountCodes'\n  account: string\n  codes: string[]\n}\n\nconst hashAccountCodes = 'accountCodes'\n\nexport function isAccountCodes<V>(v: V) {\n  return is$typed(v, id, hashAccountCodes)\n}\n\nexport function validateAccountCodes<V>(v: V) {\n  return validate<AccountCodes & V>(v, id, hashAccountCodes)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/createSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createSession'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Handle or other identifier supported by the server for the authenticating user. */\n  identifier: string\n  password: string\n  authFactorToken?: string\n  /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */\n  allowTakendown?: boolean\n}\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class AccountTakedownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class AuthFactorTokenRequiredError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'AccountTakedown') return new AccountTakedownError(e)\n    if (e.error === 'AuthFactorTokenRequired')\n      return new AuthFactorTokenRequiredError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/deactivateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deactivateAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A recommendation to server as to how long they should hold onto the deactivated account before deleting. */\n  deleteAfter?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.defs'\n\nexport interface InviteCode {\n  $type?: 'com.atproto.server.defs#inviteCode'\n  code: string\n  available: number\n  disabled: boolean\n  forAccount: string\n  createdBy: string\n  createdAt: string\n  uses: InviteCodeUse[]\n}\n\nconst hashInviteCode = 'inviteCode'\n\nexport function isInviteCode<V>(v: V) {\n  return is$typed(v, id, hashInviteCode)\n}\n\nexport function validateInviteCode<V>(v: V) {\n  return validate<InviteCode & V>(v, id, hashInviteCode)\n}\n\nexport interface InviteCodeUse {\n  $type?: 'com.atproto.server.defs#inviteCodeUse'\n  usedBy: string\n  usedAt: string\n}\n\nconst hashInviteCodeUse = 'inviteCodeUse'\n\nexport function isInviteCodeUse<V>(v: V) {\n  return is$typed(v, id, hashInviteCodeUse)\n}\n\nexport function validateInviteCodeUse<V>(v: V) {\n  return validate<InviteCodeUse & V>(v, id, hashInviteCodeUse)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n  token: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class ExpiredTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/deleteSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class ExpiredTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n    if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/describeServer.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.describeServer'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** If true, an invite code must be supplied to create an account on this instance. */\n  inviteCodeRequired?: boolean\n  /** If true, a phone verification token must be supplied to create an account on this instance. */\n  phoneVerificationRequired?: boolean\n  /** List of domain suffixes that can be used in account handles. */\n  availableUserDomains: string[]\n  links?: Links\n  contact?: Contact\n  did: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Links {\n  $type?: 'com.atproto.server.describeServer#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n\nexport interface Contact {\n  $type?: 'com.atproto.server.describeServer#contact'\n  email?: string\n}\n\nconst hashContact = 'contact'\n\nexport function isContact<V>(v: V) {\n  return is$typed(v, id, hashContact)\n}\n\nexport function validateContact<V>(v: V) {\n  return validate<Contact & V>(v, id, hashContact)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getAccountInviteCodes'\n\nexport type QueryParams = {\n  includeUsed?: boolean\n  /** Controls whether any new 'earned' but not 'created' invites should be created. */\n  createAvailable?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class DuplicateCreateError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'DuplicateCreate') return new DuplicateCreateError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/getServiceAuth.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getServiceAuth'\n\nexport type QueryParams = {\n  /** The DID of the service that the token will be used to authenticate with */\n  aud: string\n  /** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */\n  exp?: number\n  /** Lexicon (XRPC) method to bind the requested token to */\n  lxm?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  token: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class BadExpirationError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BadExpiration') return new BadExpirationError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/getSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/listAppPasswords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.listAppPasswords'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  passwords: AppPassword[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class AccountTakedownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'AccountTakedown') return new AccountTakedownError(e)\n  }\n\n  return e\n}\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.listAppPasswords#appPassword'\n  name: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/refreshSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.refreshSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** Hosting status of the account. If not specified, then assume 'active'. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class AccountTakedownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class ExpiredTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'AccountTakedown') return new AccountTakedownError(e)\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n    if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/requestAccountDelete.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestAccountDelete'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailConfirmation'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailUpdate'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  tokenRequired: boolean\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/requestPasswordReset.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestPasswordReset'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/reserveSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.reserveSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID to reserve a key for. */\n  did?: string\n}\n\nexport interface OutputSchema {\n  /** The public key for the reserved signing key, in did:key serialization. */\n  signingKey: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/resetPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.resetPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  token: string\n  password: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class ExpiredTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/revokeAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.revokeAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  name: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/server/updateEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.updateEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  emailAuthFactor?: boolean\n  /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */\n  token?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class ExpiredTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class InvalidTokenError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class TokenRequiredError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)\n    if (e.error === 'InvalidToken') return new InvalidTokenError(e)\n    if (e.error === 'TokenRequired') return new TokenRequiredError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.defs'\n\nexport type HostStatus =\n  | 'active'\n  | 'idle'\n  | 'offline'\n  | 'throttled'\n  | 'banned'\n  | (string & {})\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlob'\n\nexport type QueryParams = {\n  /** The DID of the account. */\n  did: string\n  /** The CID of the blob to fetch */\n  cid: string\n}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: Uint8Array\n}\n\nexport class BlobNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoTakendownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoSuspendedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BlobNotFound') return new BlobNotFoundError(e)\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n    if (e.error === 'RepoTakendown') return new RepoTakendownError(e)\n    if (e.error === 'RepoSuspended') return new RepoSuspendedError(e)\n    if (e.error === 'RepoDeactivated') return new RepoDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlocks'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  cids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: Uint8Array\n}\n\nexport class BlockNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoTakendownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoSuspendedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'BlockNotFound') return new BlockNotFoundError(e)\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n    if (e.error === 'RepoTakendown') return new RepoTakendownError(e)\n    if (e.error === 'RepoSuspended') return new RepoSuspendedError(e)\n    if (e.error === 'RepoDeactivated') return new RepoDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getCheckout.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getCheckout'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: Uint8Array\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getHead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHead'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  root: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class HeadNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'HeadNotFound') return new HeadNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getHostStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHostStatus'\n\nexport type QueryParams = {\n  /** Hostname of the host (eg, PDS or relay) being queried. */\n  hostname: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  /** Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts. */\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class HostNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'HostNotFound') return new HostNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getLatestCommit.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getLatestCommit'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cid: string\n  rev: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoTakendownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoSuspendedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n    if (e.error === 'RepoTakendown') return new RepoTakendownError(e)\n    if (e.error === 'RepoSuspended') return new RepoSuspendedError(e)\n    if (e.error === 'RepoDeactivated') return new RepoDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRecord'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  collection: string\n  /** Record Key */\n  rkey: string\n}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: Uint8Array\n}\n\nexport class RecordNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoTakendownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoSuspendedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RecordNotFound') return new RecordNotFoundError(e)\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n    if (e.error === 'RepoTakendown') return new RepoTakendownError(e)\n    if (e.error === 'RepoSuspended') return new RepoSuspendedError(e)\n    if (e.error === 'RepoDeactivated') return new RepoDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepo'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** The revision ('rev') of the repo to create a diff from. */\n  since?: string\n}\nexport type InputSchema = undefined\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: Uint8Array\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoTakendownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoSuspendedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n    if (e.error === 'RepoTakendown') return new RepoTakendownError(e)\n    if (e.error === 'RepoSuspended') return new RepoSuspendedError(e)\n    if (e.error === 'RepoDeactivated') return new RepoDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/getRepoStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepoStatus'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  active: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n  /** Optional field, the current rev of the repo, if active=true */\n  rev?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/listBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listBlobs'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** Optional revision of the repo to list blobs since. */\n  since?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  cids: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoTakendownError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoSuspendedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RepoDeactivatedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n    if (e.error === 'RepoTakendown') return new RepoTakendownError(e)\n    if (e.error === 'RepoSuspended') return new RepoSuspendedError(e)\n    if (e.error === 'RepoDeactivated') return new RepoDeactivatedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/listHosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listHosts'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first. */\n  hosts: Host[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Host {\n  $type?: 'com.atproto.sync.listHosts#host'\n  /** hostname of server; not a URL (no scheme) */\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nconst hashHost = 'host'\n\nexport function isHost<V>(v: V) {\n  return is$typed(v, id, hashHost)\n}\n\nexport function validateHost<V>(v: V) {\n  return validate<Host & V>(v, id, hashHost)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/listRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listRepos'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listRepos#repo'\n  did: string\n  /** Current repo commit CID */\n  head: string\n  rev: string\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/listReposByCollection.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listReposByCollection'\n\nexport type QueryParams = {\n  collection: string\n  /** Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists. */\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listReposByCollection#repo'\n  did: string\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/notifyOfUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.notifyOfUpdate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (usually a PDS) that is notifying of update. */\n  hostname: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/requestCrawl.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.requestCrawl'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (eg, PDS) that is requesting to be crawled. */\n  hostname: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class HostBannedError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'HostBanned') return new HostBannedError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.subscribeRepos'\n\n/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */\nexport interface Commit {\n  $type?: 'com.atproto.sync.subscribeRepos#commit'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** DEPRECATED -- unused */\n  rebase: boolean\n  /** DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */\n  tooBig: boolean\n  /** The repo this event comes from. Note that all other message types name this field 'did'. */\n  repo: string\n  /** Repo commit object CID. */\n  commit: CID\n  /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */\n  rev: string\n  /** The rev of the last emitted commit from this repo (if any). */\n  since: string | null\n  /** CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list. */\n  blocks: Uint8Array\n  ops: RepoOp[]\n  blobs: CID[]\n  /** The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose. */\n  prevData?: CID\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashCommit = 'commit'\n\nexport function isCommit<V>(v: V) {\n  return is$typed(v, id, hashCommit)\n}\n\nexport function validateCommit<V>(v: V) {\n  return validate<Commit & V>(v, id, hashCommit)\n}\n\n/** Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository. */\nexport interface Sync {\n  $type?: 'com.atproto.sync.subscribeRepos#sync'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** The account this repo event corresponds to. Must match that in the commit object. */\n  did: string\n  /** CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'. */\n  blocks: Uint8Array\n  /** The rev of the commit. This value must match that in the commit object. */\n  rev: string\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashSync = 'sync'\n\nexport function isSync<V>(v: V) {\n  return is$typed(v, id, hashSync)\n}\n\nexport function validateSync<V>(v: V) {\n  return validate<Sync & V>(v, id, hashSync)\n}\n\n/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */\nexport interface Identity {\n  $type?: 'com.atproto.sync.subscribeRepos#identity'\n  seq: number\n  did: string\n  time: string\n  /** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */\n  handle?: string\n}\n\nconst hashIdentity = 'identity'\n\nexport function isIdentity<V>(v: V) {\n  return is$typed(v, id, hashIdentity)\n}\n\nexport function validateIdentity<V>(v: V) {\n  return validate<Identity & V>(v, id, hashIdentity)\n}\n\n/** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */\nexport interface Account {\n  $type?: 'com.atproto.sync.subscribeRepos#account'\n  seq: number\n  did: string\n  time: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  /** If active=false, this optional field indicates a reason for why the account is not active. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashAccount = 'account'\n\nexport function isAccount<V>(v: V) {\n  return is$typed(v, id, hashAccount)\n}\n\nexport function validateAccount<V>(v: V) {\n  return validate<Account & V>(v, id, hashAccount)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.sync.subscribeRepos#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n\n/** A repo operation, ie a mutation of a single record. */\nexport interface RepoOp {\n  $type?: 'com.atproto.sync.subscribeRepos#repoOp'\n  action: 'create' | 'update' | 'delete' | (string & {})\n  path: string\n  /** For creates and updates, the new record CID. For deletions, null. */\n  cid: CID | null\n  /** For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined. */\n  prev?: CID\n}\n\nconst hashRepoOp = 'repoOp'\n\nexport function isRepoOp<V>(v: V) {\n  return is$typed(v, id, hashRepoOp)\n}\n\nexport function validateRepoOp<V>(v: V) {\n  return validate<RepoOp & V>(v, id, hashRepoOp)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/addReservedHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.addReservedHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  handle: string\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/checkHandleAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkHandleAvailability'\n\nexport type QueryParams = {\n  /** Tentative handle. Will be checked for availability or used to build handle suggestions. */\n  handle: string\n  /** User-provided email. Might be used to build handle suggestions. */\n  email?: string\n  /** User-provided birth date. Might be used to build handle suggestions. */\n  birthDate?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Echo of the input handle. */\n  handle: string\n  result:\n    | $Typed<ResultAvailable>\n    | $Typed<ResultUnavailable>\n    | { $type: string }\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidEmailError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidEmail') return new InvalidEmailError(e)\n  }\n\n  return e\n}\n\n/** Indicates the provided handle is available. */\nexport interface ResultAvailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultAvailable'\n}\n\nconst hashResultAvailable = 'resultAvailable'\n\nexport function isResultAvailable<V>(v: V) {\n  return is$typed(v, id, hashResultAvailable)\n}\n\nexport function validateResultAvailable<V>(v: V) {\n  return validate<ResultAvailable & V>(v, id, hashResultAvailable)\n}\n\n/** Indicates the provided handle is unavailable and gives suggestions of available handles. */\nexport interface ResultUnavailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'\n  /** List of suggested handles based on the provided inputs. */\n  suggestions: Suggestion[]\n}\n\nconst hashResultUnavailable = 'resultUnavailable'\n\nexport function isResultUnavailable<V>(v: V) {\n  return is$typed(v, id, hashResultUnavailable)\n}\n\nexport function validateResultUnavailable<V>(v: V) {\n  return validate<ResultUnavailable & V>(v, id, hashResultUnavailable)\n}\n\nexport interface Suggestion {\n  $type?: 'com.atproto.temp.checkHandleAvailability#suggestion'\n  handle: string\n  /** Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics. */\n  method: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/checkSignupQueue.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkSignupQueue'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  placeInQueue?: number\n  estimatedTimeMs?: number\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/dereferenceScope.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.dereferenceScope'\n\nexport type QueryParams = {\n  /** The scope reference (starts with 'ref:') */\n  scope: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The full oauth permission scope */\n  scope: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidScopeReferenceError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidScopeReference')\n      return new InvalidScopeReferenceError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/fetchLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.fetchLabels'\n\nexport type QueryParams = {\n  since?: number\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.requestPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  phoneNumber: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/atproto/temp/revokeAccountCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.revokeAccountCredentials'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/com/germnetwork/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../lexicons'\nimport { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.germnetwork.declaration'\n\nexport interface Main {\n  $type: 'com.germnetwork.declaration'\n  /** Semver version number, without pre-release or build information, for the format of opaque content */\n  version: string\n  /** Opaque value, an ed25519 public key prefixed with a byte enum */\n  currentKey: Uint8Array\n  messageMe?: MessageMe\n  /** Opaque value, contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey */\n  keyPackage?: Uint8Array\n  /** Array of opaque values to allow for key rolling */\n  continuityProofs?: Uint8Array[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface MessageMe {\n  $type?: 'com.germnetwork.declaration#messageMe'\n  /** A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who wish to message each other */\n  messageMeUrl: string\n  /** The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer. */\n  showButtonTo: 'none' | 'usersIFollow' | 'everyone' | (string & {})\n}\n\nconst hashMessageMe = 'messageMe'\n\nexport function isMessageMe<V>(v: V) {\n  return is$typed(v, id, hashMessageMe)\n}\n\nexport function validateMessageMe<V>(v: V) {\n  return validate<MessageMe & V>(v, id, hashMessageMe)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/communication/createTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.createTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the template. */\n  name: string\n  /** Content of the template, markdown supported, can contain variable placeholders. */\n  contentMarkdown: string\n  /** Subject of the message, used in emails. */\n  subject: string\n  /** Message language. */\n  lang?: string\n  /** DID of the user who is creating the template. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneCommunicationDefs.TemplateView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class DuplicateTemplateNameError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'DuplicateTemplateName')\n      return new DuplicateTemplateNameError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/communication/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.defs'\n\nexport interface TemplateView {\n  $type?: 'tools.ozone.communication.defs#templateView'\n  id: string\n  /** Name of the template. */\n  name: string\n  /** Content of the template, can contain markdown and variable placeholders. */\n  subject?: string\n  /** Subject of the message, used in emails. */\n  contentMarkdown: string\n  disabled: boolean\n  /** Message language. */\n  lang?: string\n  /** DID of the user who last updated the template. */\n  lastUpdatedBy: string\n  createdAt: string\n  updatedAt: string\n}\n\nconst hashTemplateView = 'templateView'\n\nexport function isTemplateView<V>(v: V) {\n  return is$typed(v, id, hashTemplateView)\n}\n\nexport function validateTemplateView<V>(v: V) {\n  return validate<TemplateView & V>(v, id, hashTemplateView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/communication/deleteTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.deleteTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/communication/listTemplates.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.listTemplates'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  communicationTemplates: ToolsOzoneCommunicationDefs.TemplateView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/communication/updateTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.updateTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** ID of the template to be updated. */\n  id: string\n  /** Name of the template. */\n  name?: string\n  /** Message language. */\n  lang?: string\n  /** Content of the template, markdown supported, can contain variable placeholders. */\n  contentMarkdown?: string\n  /** Subject of the message, used in emails. */\n  subject?: string\n  /** DID of the user who is updating the template. */\n  updatedBy?: string\n  disabled?: boolean\n}\n\nexport type OutputSchema = ToolsOzoneCommunicationDefs.TemplateView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class DuplicateTemplateNameError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'DuplicateTemplateName')\n      return new DuplicateTemplateNameError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/hosting/getAccountHistory.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.hosting.getAccountHistory'\n\nexport type QueryParams = {\n  did: string\n  events?:\n    | 'accountCreated'\n    | 'emailUpdated'\n    | 'emailConfirmed'\n    | 'passwordUpdated'\n    | 'handleUpdated'\n    | (string & {})[]\n  cursor?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  events: Event[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface Event {\n  $type?: 'tools.ozone.hosting.getAccountHistory#event'\n  details:\n    | $Typed<AccountCreated>\n    | $Typed<EmailUpdated>\n    | $Typed<EmailConfirmed>\n    | $Typed<PasswordUpdated>\n    | $Typed<HandleUpdated>\n    | { $type: string }\n  createdBy: string\n  createdAt: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n\nexport interface AccountCreated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#accountCreated'\n  email?: string\n  handle?: string\n}\n\nconst hashAccountCreated = 'accountCreated'\n\nexport function isAccountCreated<V>(v: V) {\n  return is$typed(v, id, hashAccountCreated)\n}\n\nexport function validateAccountCreated<V>(v: V) {\n  return validate<AccountCreated & V>(v, id, hashAccountCreated)\n}\n\nexport interface EmailUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#emailUpdated'\n  email: string\n}\n\nconst hashEmailUpdated = 'emailUpdated'\n\nexport function isEmailUpdated<V>(v: V) {\n  return is$typed(v, id, hashEmailUpdated)\n}\n\nexport function validateEmailUpdated<V>(v: V) {\n  return validate<EmailUpdated & V>(v, id, hashEmailUpdated)\n}\n\nexport interface EmailConfirmed {\n  $type?: 'tools.ozone.hosting.getAccountHistory#emailConfirmed'\n  email: string\n}\n\nconst hashEmailConfirmed = 'emailConfirmed'\n\nexport function isEmailConfirmed<V>(v: V) {\n  return is$typed(v, id, hashEmailConfirmed)\n}\n\nexport function validateEmailConfirmed<V>(v: V) {\n  return validate<EmailConfirmed & V>(v, id, hashEmailConfirmed)\n}\n\nexport interface PasswordUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#passwordUpdated'\n}\n\nconst hashPasswordUpdated = 'passwordUpdated'\n\nexport function isPasswordUpdated<V>(v: V) {\n  return is$typed(v, id, hashPasswordUpdated)\n}\n\nexport function validatePasswordUpdated<V>(v: V) {\n  return validate<PasswordUpdated & V>(v, id, hashPasswordUpdated)\n}\n\nexport interface HandleUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#handleUpdated'\n  handle: string\n}\n\nconst hashHandleUpdated = 'handleUpdated'\n\nexport function isHandleUpdated<V>(v: V) {\n  return is$typed(v, id, hashHandleUpdated)\n}\n\nexport function validateHandleUpdated<V>(v: V) {\n  return validate<HandleUpdated & V>(v, id, hashHandleUpdated)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/cancelScheduledActions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.cancelScheduledActions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of DID subjects to cancel scheduled actions for */\n  subjects: string[]\n  /** Optional comment describing the reason for cancellation */\n  comment?: string\n}\n\nexport type OutputSchema = CancellationResults\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface CancellationResults {\n  $type?: 'tools.ozone.moderation.cancelScheduledActions#cancellationResults'\n  /** DIDs for which all pending scheduled actions were successfully cancelled */\n  succeeded: string[]\n  /** DIDs for which cancellation failed with error details */\n  failed: FailedCancellation[]\n}\n\nconst hashCancellationResults = 'cancellationResults'\n\nexport function isCancellationResults<V>(v: V) {\n  return is$typed(v, id, hashCancellationResults)\n}\n\nexport function validateCancellationResults<V>(v: V) {\n  return validate<CancellationResults & V>(v, id, hashCancellationResults)\n}\n\nexport interface FailedCancellation {\n  $type?: 'tools.ozone.moderation.cancelScheduledActions#failedCancellation'\n  did: string\n  error: string\n  errorCode?: string\n}\n\nconst hashFailedCancellation = 'failedCancellation'\n\nexport function isFailedCancellation<V>(v: V) {\n  return is$typed(v, id, hashFailedCancellation)\n}\n\nexport function validateFailedCancellation<V>(v: V) {\n  return validate<FailedCancellation & V>(v, id, hashFailedCancellation)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as ChatBskyConvoDefs from '../../../chat/bsky/convo/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\nimport type * as AppBskyAgeassuranceDefs from '../../../app/bsky/ageassurance/defs.js'\nimport type * as ComAtprotoServerDefs from '../../../com/atproto/server/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.defs'\n\nexport interface ModEventView {\n  $type?: 'tools.ozone.moderation.defs#modEventView'\n  id: number\n  event:\n    | $Typed<ModEventTakedown>\n    | $Typed<ModEventReverseTakedown>\n    | $Typed<ModEventComment>\n    | $Typed<ModEventReport>\n    | $Typed<ModEventLabel>\n    | $Typed<ModEventAcknowledge>\n    | $Typed<ModEventEscalate>\n    | $Typed<ModEventMute>\n    | $Typed<ModEventUnmute>\n    | $Typed<ModEventMuteReporter>\n    | $Typed<ModEventUnmuteReporter>\n    | $Typed<ModEventEmail>\n    | $Typed<ModEventResolveAppeal>\n    | $Typed<ModEventDivert>\n    | $Typed<ModEventTag>\n    | $Typed<AccountEvent>\n    | $Typed<IdentityEvent>\n    | $Typed<RecordEvent>\n    | $Typed<ModEventPriorityScore>\n    | $Typed<AgeAssuranceEvent>\n    | $Typed<AgeAssuranceOverrideEvent>\n    | $Typed<AgeAssurancePurgeEvent>\n    | $Typed<RevokeAccountCredentialsEvent>\n    | $Typed<ScheduleTakedownEvent>\n    | $Typed<CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ChatBskyConvoDefs.MessageRef>\n    | { $type: string }\n  subjectBlobCids: string[]\n  createdBy: string\n  createdAt: string\n  creatorHandle?: string\n  subjectHandle?: string\n  modTool?: ModTool\n}\n\nconst hashModEventView = 'modEventView'\n\nexport function isModEventView<V>(v: V) {\n  return is$typed(v, id, hashModEventView)\n}\n\nexport function validateModEventView<V>(v: V) {\n  return validate<ModEventView & V>(v, id, hashModEventView)\n}\n\nexport interface ModEventViewDetail {\n  $type?: 'tools.ozone.moderation.defs#modEventViewDetail'\n  id: number\n  event:\n    | $Typed<ModEventTakedown>\n    | $Typed<ModEventReverseTakedown>\n    | $Typed<ModEventComment>\n    | $Typed<ModEventReport>\n    | $Typed<ModEventLabel>\n    | $Typed<ModEventAcknowledge>\n    | $Typed<ModEventEscalate>\n    | $Typed<ModEventMute>\n    | $Typed<ModEventUnmute>\n    | $Typed<ModEventMuteReporter>\n    | $Typed<ModEventUnmuteReporter>\n    | $Typed<ModEventEmail>\n    | $Typed<ModEventResolveAppeal>\n    | $Typed<ModEventDivert>\n    | $Typed<ModEventTag>\n    | $Typed<AccountEvent>\n    | $Typed<IdentityEvent>\n    | $Typed<RecordEvent>\n    | $Typed<ModEventPriorityScore>\n    | $Typed<AgeAssuranceEvent>\n    | $Typed<AgeAssuranceOverrideEvent>\n    | $Typed<AgeAssurancePurgeEvent>\n    | $Typed<RevokeAccountCredentialsEvent>\n    | $Typed<ScheduleTakedownEvent>\n    | $Typed<CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<RepoView>\n    | $Typed<RepoViewNotFound>\n    | $Typed<RecordView>\n    | $Typed<RecordViewNotFound>\n    | { $type: string }\n  subjectBlobs: BlobView[]\n  createdBy: string\n  createdAt: string\n  modTool?: ModTool\n}\n\nconst hashModEventViewDetail = 'modEventViewDetail'\n\nexport function isModEventViewDetail<V>(v: V) {\n  return is$typed(v, id, hashModEventViewDetail)\n}\n\nexport function validateModEventViewDetail<V>(v: V) {\n  return validate<ModEventViewDetail & V>(v, id, hashModEventViewDetail)\n}\n\nexport interface SubjectStatusView {\n  $type?: 'tools.ozone.moderation.defs#subjectStatusView'\n  id: number\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ChatBskyConvoDefs.MessageRef>\n    | { $type: string }\n  hosting?: $Typed<AccountHosting> | $Typed<RecordHosting> | { $type: string }\n  subjectBlobCids?: string[]\n  subjectRepoHandle?: string\n  /** Timestamp referencing when the last update was made to the moderation status of the subject */\n  updatedAt: string\n  /** Timestamp referencing the first moderation status impacting event was emitted on the subject */\n  createdAt: string\n  reviewState: SubjectReviewState\n  /** Sticky comment on the subject. */\n  comment?: string\n  /** Numeric value representing the level of priority. Higher score means higher priority. */\n  priorityScore?: number\n  muteUntil?: string\n  muteReportingUntil?: string\n  lastReviewedBy?: string\n  lastReviewedAt?: string\n  lastReportedAt?: string\n  /** Timestamp referencing when the author of the subject appealed a moderation action */\n  lastAppealedAt?: string\n  takendown?: boolean\n  /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */\n  appealed?: boolean\n  suspendUntil?: string\n  tags?: string[]\n  accountStats?: AccountStats\n  recordsStats?: RecordsStats\n  accountStrike?: AccountStrike\n  /** Current age assurance state of the subject. */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n  /** Whether or not the last successful update to age assurance was made by the user or admin. */\n  ageAssuranceUpdatedBy?: 'admin' | 'user' | (string & {})\n}\n\nconst hashSubjectStatusView = 'subjectStatusView'\n\nexport function isSubjectStatusView<V>(v: V) {\n  return is$typed(v, id, hashSubjectStatusView)\n}\n\nexport function validateSubjectStatusView<V>(v: V) {\n  return validate<SubjectStatusView & V>(v, id, hashSubjectStatusView)\n}\n\n/** Detailed view of a subject. For record subjects, the author's repo and profile will be returned. */\nexport interface SubjectView {\n  $type?: 'tools.ozone.moderation.defs#subjectView'\n  type: ComAtprotoModerationDefs.SubjectType\n  subject: string\n  status?: SubjectStatusView\n  repo?: RepoViewDetail\n  profile?: { $type: string }\n  record?: RecordViewDetail\n}\n\nconst hashSubjectView = 'subjectView'\n\nexport function isSubjectView<V>(v: V) {\n  return is$typed(v, id, hashSubjectView)\n}\n\nexport function validateSubjectView<V>(v: V) {\n  return validate<SubjectView & V>(v, id, hashSubjectView)\n}\n\n/** Statistics about a particular account subject */\nexport interface AccountStats {\n  $type?: 'tools.ozone.moderation.defs#accountStats'\n  /** Total number of reports on the account */\n  reportCount?: number\n  /** Total number of appeals against a moderation action on the account */\n  appealCount?: number\n  /** Number of times the account was suspended */\n  suspendCount?: number\n  /** Number of times the account was escalated */\n  escalateCount?: number\n  /** Number of times the account was taken down */\n  takedownCount?: number\n}\n\nconst hashAccountStats = 'accountStats'\n\nexport function isAccountStats<V>(v: V) {\n  return is$typed(v, id, hashAccountStats)\n}\n\nexport function validateAccountStats<V>(v: V) {\n  return validate<AccountStats & V>(v, id, hashAccountStats)\n}\n\n/** Statistics about a set of record subject items */\nexport interface RecordsStats {\n  $type?: 'tools.ozone.moderation.defs#recordsStats'\n  /** Cumulative sum of the number of reports on the items in the set */\n  totalReports?: number\n  /** Number of items that were reported at least once */\n  reportedCount?: number\n  /** Number of items that were escalated at least once */\n  escalatedCount?: number\n  /** Number of items that were appealed at least once */\n  appealedCount?: number\n  /** Total number of item in the set */\n  subjectCount?: number\n  /** Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state */\n  pendingCount?: number\n  /** Number of item currently in \"reviewNone\" or \"reviewClosed\" state */\n  processedCount?: number\n  /** Number of item currently taken down */\n  takendownCount?: number\n}\n\nconst hashRecordsStats = 'recordsStats'\n\nexport function isRecordsStats<V>(v: V) {\n  return is$typed(v, id, hashRecordsStats)\n}\n\nexport function validateRecordsStats<V>(v: V) {\n  return validate<RecordsStats & V>(v, id, hashRecordsStats)\n}\n\n/** Strike information for an account */\nexport interface AccountStrike {\n  $type?: 'tools.ozone.moderation.defs#accountStrike'\n  /** Current number of active strikes (excluding expired strikes) */\n  activeStrikeCount?: number\n  /** Total number of strikes ever received (including expired strikes) */\n  totalStrikeCount?: number\n  /** Timestamp of the first strike received */\n  firstStrikeAt?: string\n  /** Timestamp of the most recent strike received */\n  lastStrikeAt?: string\n}\n\nconst hashAccountStrike = 'accountStrike'\n\nexport function isAccountStrike<V>(v: V) {\n  return is$typed(v, id, hashAccountStrike)\n}\n\nexport function validateAccountStrike<V>(v: V) {\n  return validate<AccountStrike & V>(v, id, hashAccountStrike)\n}\n\nexport type SubjectReviewState =\n  | 'tools.ozone.moderation.defs#reviewOpen'\n  | 'tools.ozone.moderation.defs#reviewEscalated'\n  | 'tools.ozone.moderation.defs#reviewClosed'\n  | 'tools.ozone.moderation.defs#reviewNone'\n  | (string & {})\n\n/** Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator */\nexport const REVIEWOPEN = `${id}#reviewOpen`\n/** Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator */\nexport const REVIEWESCALATED = `${id}#reviewEscalated`\n/** Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator */\nexport const REVIEWCLOSED = `${id}#reviewClosed`\n/** Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it */\nexport const REVIEWNONE = `${id}#reviewNone`\n\n/** Take down a subject permanently or temporarily */\nexport interface ModEventTakedown {\n  $type?: 'tools.ozone.moderation.defs#modEventTakedown'\n  comment?: string\n  /** Indicates how long the takedown should be in effect before automatically expiring. */\n  durationInHours?: number\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n  /** Names/Keywords of the policies that drove the decision. */\n  policies?: string[]\n  /** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */\n  severityLevel?: string\n  /** List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services. */\n  targetServices?: ('appview' | 'pds' | (string & {}))[]\n  /** Number of strikes to assign to the user for this violation. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n}\n\nconst hashModEventTakedown = 'modEventTakedown'\n\nexport function isModEventTakedown<V>(v: V) {\n  return is$typed(v, id, hashModEventTakedown)\n}\n\nexport function validateModEventTakedown<V>(v: V) {\n  return validate<ModEventTakedown & V>(v, id, hashModEventTakedown)\n}\n\n/** Revert take down action on a subject */\nexport interface ModEventReverseTakedown {\n  $type?: 'tools.ozone.moderation.defs#modEventReverseTakedown'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n  /** Names/Keywords of the policy infraction for which takedown is being reversed. */\n  policies?: string[]\n  /** Severity level of the violation. Usually set from the last policy infraction's severity. */\n  severityLevel?: string\n  /** Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity. */\n  strikeCount?: number\n}\n\nconst hashModEventReverseTakedown = 'modEventReverseTakedown'\n\nexport function isModEventReverseTakedown<V>(v: V) {\n  return is$typed(v, id, hashModEventReverseTakedown)\n}\n\nexport function validateModEventReverseTakedown<V>(v: V) {\n  return validate<ModEventReverseTakedown & V>(\n    v,\n    id,\n    hashModEventReverseTakedown,\n  )\n}\n\n/** Resolve appeal on a subject */\nexport interface ModEventResolveAppeal {\n  $type?: 'tools.ozone.moderation.defs#modEventResolveAppeal'\n  /** Describe resolution. */\n  comment?: string\n}\n\nconst hashModEventResolveAppeal = 'modEventResolveAppeal'\n\nexport function isModEventResolveAppeal<V>(v: V) {\n  return is$typed(v, id, hashModEventResolveAppeal)\n}\n\nexport function validateModEventResolveAppeal<V>(v: V) {\n  return validate<ModEventResolveAppeal & V>(v, id, hashModEventResolveAppeal)\n}\n\n/** Add a comment to a subject. An empty comment will clear any previously set sticky comment. */\nexport interface ModEventComment {\n  $type?: 'tools.ozone.moderation.defs#modEventComment'\n  comment?: string\n  /** Make the comment persistent on the subject */\n  sticky?: boolean\n}\n\nconst hashModEventComment = 'modEventComment'\n\nexport function isModEventComment<V>(v: V) {\n  return is$typed(v, id, hashModEventComment)\n}\n\nexport function validateModEventComment<V>(v: V) {\n  return validate<ModEventComment & V>(v, id, hashModEventComment)\n}\n\n/** Report a subject */\nexport interface ModEventReport {\n  $type?: 'tools.ozone.moderation.defs#modEventReport'\n  comment?: string\n  /** Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject. */\n  isReporterMuted?: boolean\n  reportType: ComAtprotoModerationDefs.ReasonType\n}\n\nconst hashModEventReport = 'modEventReport'\n\nexport function isModEventReport<V>(v: V) {\n  return is$typed(v, id, hashModEventReport)\n}\n\nexport function validateModEventReport<V>(v: V) {\n  return validate<ModEventReport & V>(v, id, hashModEventReport)\n}\n\n/** Apply/Negate labels on a subject */\nexport interface ModEventLabel {\n  $type?: 'tools.ozone.moderation.defs#modEventLabel'\n  comment?: string\n  createLabelVals: string[]\n  negateLabelVals: string[]\n  /** Indicates how long the label will remain on the subject. Only applies on labels that are being added. */\n  durationInHours?: number\n}\n\nconst hashModEventLabel = 'modEventLabel'\n\nexport function isModEventLabel<V>(v: V) {\n  return is$typed(v, id, hashModEventLabel)\n}\n\nexport function validateModEventLabel<V>(v: V) {\n  return validate<ModEventLabel & V>(v, id, hashModEventLabel)\n}\n\n/** Set priority score of the subject. Higher score means higher priority. */\nexport interface ModEventPriorityScore {\n  $type?: 'tools.ozone.moderation.defs#modEventPriorityScore'\n  comment?: string\n  score: number\n}\n\nconst hashModEventPriorityScore = 'modEventPriorityScore'\n\nexport function isModEventPriorityScore<V>(v: V) {\n  return is$typed(v, id, hashModEventPriorityScore)\n}\n\nexport function validateModEventPriorityScore<V>(v: V) {\n  return validate<ModEventPriorityScore & V>(v, id, hashModEventPriorityScore)\n}\n\n/** Age assurance info coming directly from users. Only works on DID subjects. */\nexport interface AgeAssuranceEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  access?: AppBskyAgeassuranceDefs.Access\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode?: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n\n/** Age assurance status override by moderators. Only works on DID subjects. */\nexport interface AgeAssuranceOverrideEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n  /** The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state. */\n  status: 'assured' | 'reset' | 'blocked' | (string & {})\n  access?: AppBskyAgeassuranceDefs.Access\n  /** Comment describing the reason for the override. */\n  comment: string\n}\n\nconst hashAgeAssuranceOverrideEvent = 'ageAssuranceOverrideEvent'\n\nexport function isAgeAssuranceOverrideEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceOverrideEvent)\n}\n\nexport function validateAgeAssuranceOverrideEvent<V>(v: V) {\n  return validate<AgeAssuranceOverrideEvent & V>(\n    v,\n    id,\n    hashAgeAssuranceOverrideEvent,\n  )\n}\n\n/** Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only. */\nexport interface AgeAssurancePurgeEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssurancePurgeEvent'\n  /** Comment describing the reason for the purge. */\n  comment: string\n}\n\nconst hashAgeAssurancePurgeEvent = 'ageAssurancePurgeEvent'\n\nexport function isAgeAssurancePurgeEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssurancePurgeEvent)\n}\n\nexport function validateAgeAssurancePurgeEvent<V>(v: V) {\n  return validate<AgeAssurancePurgeEvent & V>(v, id, hashAgeAssurancePurgeEvent)\n}\n\n/** Account credentials revocation by moderators. Only works on DID subjects. */\nexport interface RevokeAccountCredentialsEvent {\n  $type?: 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n  /** Comment describing the reason for the revocation. */\n  comment: string\n}\n\nconst hashRevokeAccountCredentialsEvent = 'revokeAccountCredentialsEvent'\n\nexport function isRevokeAccountCredentialsEvent<V>(v: V) {\n  return is$typed(v, id, hashRevokeAccountCredentialsEvent)\n}\n\nexport function validateRevokeAccountCredentialsEvent<V>(v: V) {\n  return validate<RevokeAccountCredentialsEvent & V>(\n    v,\n    id,\n    hashRevokeAccountCredentialsEvent,\n  )\n}\n\nexport interface ModEventAcknowledge {\n  $type?: 'tools.ozone.moderation.defs#modEventAcknowledge'\n  comment?: string\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n}\n\nconst hashModEventAcknowledge = 'modEventAcknowledge'\n\nexport function isModEventAcknowledge<V>(v: V) {\n  return is$typed(v, id, hashModEventAcknowledge)\n}\n\nexport function validateModEventAcknowledge<V>(v: V) {\n  return validate<ModEventAcknowledge & V>(v, id, hashModEventAcknowledge)\n}\n\nexport interface ModEventEscalate {\n  $type?: 'tools.ozone.moderation.defs#modEventEscalate'\n  comment?: string\n}\n\nconst hashModEventEscalate = 'modEventEscalate'\n\nexport function isModEventEscalate<V>(v: V) {\n  return is$typed(v, id, hashModEventEscalate)\n}\n\nexport function validateModEventEscalate<V>(v: V) {\n  return validate<ModEventEscalate & V>(v, id, hashModEventEscalate)\n}\n\n/** Mute incoming reports on a subject */\nexport interface ModEventMute {\n  $type?: 'tools.ozone.moderation.defs#modEventMute'\n  comment?: string\n  /** Indicates how long the subject should remain muted. */\n  durationInHours: number\n}\n\nconst hashModEventMute = 'modEventMute'\n\nexport function isModEventMute<V>(v: V) {\n  return is$typed(v, id, hashModEventMute)\n}\n\nexport function validateModEventMute<V>(v: V) {\n  return validate<ModEventMute & V>(v, id, hashModEventMute)\n}\n\n/** Unmute action on a subject */\nexport interface ModEventUnmute {\n  $type?: 'tools.ozone.moderation.defs#modEventUnmute'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n}\n\nconst hashModEventUnmute = 'modEventUnmute'\n\nexport function isModEventUnmute<V>(v: V) {\n  return is$typed(v, id, hashModEventUnmute)\n}\n\nexport function validateModEventUnmute<V>(v: V) {\n  return validate<ModEventUnmute & V>(v, id, hashModEventUnmute)\n}\n\n/** Mute incoming reports from an account */\nexport interface ModEventMuteReporter {\n  $type?: 'tools.ozone.moderation.defs#modEventMuteReporter'\n  comment?: string\n  /** Indicates how long the account should remain muted. Falsy value here means a permanent mute. */\n  durationInHours?: number\n}\n\nconst hashModEventMuteReporter = 'modEventMuteReporter'\n\nexport function isModEventMuteReporter<V>(v: V) {\n  return is$typed(v, id, hashModEventMuteReporter)\n}\n\nexport function validateModEventMuteReporter<V>(v: V) {\n  return validate<ModEventMuteReporter & V>(v, id, hashModEventMuteReporter)\n}\n\n/** Unmute incoming reports from an account */\nexport interface ModEventUnmuteReporter {\n  $type?: 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n}\n\nconst hashModEventUnmuteReporter = 'modEventUnmuteReporter'\n\nexport function isModEventUnmuteReporter<V>(v: V) {\n  return is$typed(v, id, hashModEventUnmuteReporter)\n}\n\nexport function validateModEventUnmuteReporter<V>(v: V) {\n  return validate<ModEventUnmuteReporter & V>(v, id, hashModEventUnmuteReporter)\n}\n\n/** Keep a log of outgoing email to a user */\nexport interface ModEventEmail {\n  $type?: 'tools.ozone.moderation.defs#modEventEmail'\n  /** The subject line of the email sent to the user. */\n  subjectLine: string\n  /** The content of the email sent to the user. */\n  content?: string\n  /** Additional comment about the outgoing comm. */\n  comment?: string\n  /** Names/Keywords of the policies that necessitated the email. */\n  policies?: string[]\n  /** Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense */\n  severityLevel?: string\n  /** Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n  /** Indicates whether the email was successfully delivered to the user's inbox. */\n  isDelivered?: boolean\n}\n\nconst hashModEventEmail = 'modEventEmail'\n\nexport function isModEventEmail<V>(v: V) {\n  return is$typed(v, id, hashModEventEmail)\n}\n\nexport function validateModEventEmail<V>(v: V) {\n  return validate<ModEventEmail & V>(v, id, hashModEventEmail)\n}\n\n/** Divert a record's blobs to a 3rd party service for further scanning/tagging */\nexport interface ModEventDivert {\n  $type?: 'tools.ozone.moderation.defs#modEventDivert'\n  comment?: string\n}\n\nconst hashModEventDivert = 'modEventDivert'\n\nexport function isModEventDivert<V>(v: V) {\n  return is$typed(v, id, hashModEventDivert)\n}\n\nexport function validateModEventDivert<V>(v: V) {\n  return validate<ModEventDivert & V>(v, id, hashModEventDivert)\n}\n\n/** Add/Remove a tag on a subject */\nexport interface ModEventTag {\n  $type?: 'tools.ozone.moderation.defs#modEventTag'\n  /** Tags to be added to the subject. If already exists, won't be duplicated. */\n  add: string[]\n  /** Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. */\n  remove: string[]\n  /** Additional comment about added/removed tags. */\n  comment?: string\n}\n\nconst hashModEventTag = 'modEventTag'\n\nexport function isModEventTag<V>(v: V) {\n  return is$typed(v, id, hashModEventTag)\n}\n\nexport function validateModEventTag<V>(v: V) {\n  return validate<ModEventTag & V>(v, id, hashModEventTag)\n}\n\n/** Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface AccountEvent {\n  $type?: 'tools.ozone.moderation.defs#accountEvent'\n  comment?: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  status?:\n    | 'unknown'\n    | 'deactivated'\n    | 'deleted'\n    | 'takendown'\n    | 'suspended'\n    | 'tombstoned'\n    | (string & {})\n  timestamp: string\n}\n\nconst hashAccountEvent = 'accountEvent'\n\nexport function isAccountEvent<V>(v: V) {\n  return is$typed(v, id, hashAccountEvent)\n}\n\nexport function validateAccountEvent<V>(v: V) {\n  return validate<AccountEvent & V>(v, id, hashAccountEvent)\n}\n\n/** Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface IdentityEvent {\n  $type?: 'tools.ozone.moderation.defs#identityEvent'\n  comment?: string\n  handle?: string\n  pdsHost?: string\n  tombstone?: boolean\n  timestamp: string\n}\n\nconst hashIdentityEvent = 'identityEvent'\n\nexport function isIdentityEvent<V>(v: V) {\n  return is$typed(v, id, hashIdentityEvent)\n}\n\nexport function validateIdentityEvent<V>(v: V) {\n  return validate<IdentityEvent & V>(v, id, hashIdentityEvent)\n}\n\n/** Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface RecordEvent {\n  $type?: 'tools.ozone.moderation.defs#recordEvent'\n  comment?: string\n  op: 'create' | 'update' | 'delete' | (string & {})\n  cid?: string\n  timestamp: string\n}\n\nconst hashRecordEvent = 'recordEvent'\n\nexport function isRecordEvent<V>(v: V) {\n  return is$typed(v, id, hashRecordEvent)\n}\n\nexport function validateRecordEvent<V>(v: V) {\n  return validate<RecordEvent & V>(v, id, hashRecordEvent)\n}\n\n/** Logs a scheduled takedown action for an account. */\nexport interface ScheduleTakedownEvent {\n  $type?: 'tools.ozone.moderation.defs#scheduleTakedownEvent'\n  comment?: string\n  executeAt?: string\n  executeAfter?: string\n  executeUntil?: string\n}\n\nconst hashScheduleTakedownEvent = 'scheduleTakedownEvent'\n\nexport function isScheduleTakedownEvent<V>(v: V) {\n  return is$typed(v, id, hashScheduleTakedownEvent)\n}\n\nexport function validateScheduleTakedownEvent<V>(v: V) {\n  return validate<ScheduleTakedownEvent & V>(v, id, hashScheduleTakedownEvent)\n}\n\n/** Logs cancellation of a scheduled takedown action for an account. */\nexport interface CancelScheduledTakedownEvent {\n  $type?: 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n  comment?: string\n}\n\nconst hashCancelScheduledTakedownEvent = 'cancelScheduledTakedownEvent'\n\nexport function isCancelScheduledTakedownEvent<V>(v: V) {\n  return is$typed(v, id, hashCancelScheduledTakedownEvent)\n}\n\nexport function validateCancelScheduledTakedownEvent<V>(v: V) {\n  return validate<CancelScheduledTakedownEvent & V>(\n    v,\n    id,\n    hashCancelScheduledTakedownEvent,\n  )\n}\n\nexport interface RepoView {\n  $type?: 'tools.ozone.moderation.defs#repoView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords: { [_ in string]: unknown }[]\n  indexedAt: string\n  moderation: Moderation\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invitesDisabled?: boolean\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ComAtprotoAdminDefs.ThreatSignature[]\n}\n\nconst hashRepoView = 'repoView'\n\nexport function isRepoView<V>(v: V) {\n  return is$typed(v, id, hashRepoView)\n}\n\nexport function validateRepoView<V>(v: V) {\n  return validate<RepoView & V>(v, id, hashRepoView)\n}\n\nexport interface RepoViewDetail {\n  $type?: 'tools.ozone.moderation.defs#repoViewDetail'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords: { [_ in string]: unknown }[]\n  indexedAt: string\n  moderation: ModerationDetail\n  labels?: ComAtprotoLabelDefs.Label[]\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  inviteNote?: string\n  emailConfirmedAt?: string\n  deactivatedAt?: string\n  threatSignatures?: ComAtprotoAdminDefs.ThreatSignature[]\n}\n\nconst hashRepoViewDetail = 'repoViewDetail'\n\nexport function isRepoViewDetail<V>(v: V) {\n  return is$typed(v, id, hashRepoViewDetail)\n}\n\nexport function validateRepoViewDetail<V>(v: V) {\n  return validate<RepoViewDetail & V>(v, id, hashRepoViewDetail)\n}\n\nexport interface RepoViewNotFound {\n  $type?: 'tools.ozone.moderation.defs#repoViewNotFound'\n  did: string\n}\n\nconst hashRepoViewNotFound = 'repoViewNotFound'\n\nexport function isRepoViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashRepoViewNotFound)\n}\n\nexport function validateRepoViewNotFound<V>(v: V) {\n  return validate<RepoViewNotFound & V>(v, id, hashRepoViewNotFound)\n}\n\nexport interface RecordView {\n  $type?: 'tools.ozone.moderation.defs#recordView'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n  blobCids: string[]\n  indexedAt: string\n  moderation: Moderation\n  repo: RepoView\n}\n\nconst hashRecordView = 'recordView'\n\nexport function isRecordView<V>(v: V) {\n  return is$typed(v, id, hashRecordView)\n}\n\nexport function validateRecordView<V>(v: V) {\n  return validate<RecordView & V>(v, id, hashRecordView)\n}\n\nexport interface RecordViewDetail {\n  $type?: 'tools.ozone.moderation.defs#recordViewDetail'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n  blobs: BlobView[]\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n  moderation: ModerationDetail\n  repo: RepoView\n}\n\nconst hashRecordViewDetail = 'recordViewDetail'\n\nexport function isRecordViewDetail<V>(v: V) {\n  return is$typed(v, id, hashRecordViewDetail)\n}\n\nexport function validateRecordViewDetail<V>(v: V) {\n  return validate<RecordViewDetail & V>(v, id, hashRecordViewDetail)\n}\n\nexport interface RecordViewNotFound {\n  $type?: 'tools.ozone.moderation.defs#recordViewNotFound'\n  uri: string\n}\n\nconst hashRecordViewNotFound = 'recordViewNotFound'\n\nexport function isRecordViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashRecordViewNotFound)\n}\n\nexport function validateRecordViewNotFound<V>(v: V) {\n  return validate<RecordViewNotFound & V>(v, id, hashRecordViewNotFound)\n}\n\nexport interface Moderation {\n  $type?: 'tools.ozone.moderation.defs#moderation'\n  subjectStatus?: SubjectStatusView\n}\n\nconst hashModeration = 'moderation'\n\nexport function isModeration<V>(v: V) {\n  return is$typed(v, id, hashModeration)\n}\n\nexport function validateModeration<V>(v: V) {\n  return validate<Moderation & V>(v, id, hashModeration)\n}\n\nexport interface ModerationDetail {\n  $type?: 'tools.ozone.moderation.defs#moderationDetail'\n  subjectStatus?: SubjectStatusView\n}\n\nconst hashModerationDetail = 'moderationDetail'\n\nexport function isModerationDetail<V>(v: V) {\n  return is$typed(v, id, hashModerationDetail)\n}\n\nexport function validateModerationDetail<V>(v: V) {\n  return validate<ModerationDetail & V>(v, id, hashModerationDetail)\n}\n\nexport interface BlobView {\n  $type?: 'tools.ozone.moderation.defs#blobView'\n  cid: string\n  mimeType: string\n  size: number\n  createdAt: string\n  details?: $Typed<ImageDetails> | $Typed<VideoDetails> | { $type: string }\n  moderation?: Moderation\n}\n\nconst hashBlobView = 'blobView'\n\nexport function isBlobView<V>(v: V) {\n  return is$typed(v, id, hashBlobView)\n}\n\nexport function validateBlobView<V>(v: V) {\n  return validate<BlobView & V>(v, id, hashBlobView)\n}\n\nexport interface ImageDetails {\n  $type?: 'tools.ozone.moderation.defs#imageDetails'\n  width: number\n  height: number\n}\n\nconst hashImageDetails = 'imageDetails'\n\nexport function isImageDetails<V>(v: V) {\n  return is$typed(v, id, hashImageDetails)\n}\n\nexport function validateImageDetails<V>(v: V) {\n  return validate<ImageDetails & V>(v, id, hashImageDetails)\n}\n\nexport interface VideoDetails {\n  $type?: 'tools.ozone.moderation.defs#videoDetails'\n  width: number\n  height: number\n  length: number\n}\n\nconst hashVideoDetails = 'videoDetails'\n\nexport function isVideoDetails<V>(v: V) {\n  return is$typed(v, id, hashVideoDetails)\n}\n\nexport function validateVideoDetails<V>(v: V) {\n  return validate<VideoDetails & V>(v, id, hashVideoDetails)\n}\n\nexport interface AccountHosting {\n  $type?: 'tools.ozone.moderation.defs#accountHosting'\n  status:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'unknown'\n    | (string & {})\n  updatedAt?: string\n  createdAt?: string\n  deletedAt?: string\n  deactivatedAt?: string\n  reactivatedAt?: string\n}\n\nconst hashAccountHosting = 'accountHosting'\n\nexport function isAccountHosting<V>(v: V) {\n  return is$typed(v, id, hashAccountHosting)\n}\n\nexport function validateAccountHosting<V>(v: V) {\n  return validate<AccountHosting & V>(v, id, hashAccountHosting)\n}\n\nexport interface RecordHosting {\n  $type?: 'tools.ozone.moderation.defs#recordHosting'\n  status: 'deleted' | 'unknown' | (string & {})\n  updatedAt?: string\n  createdAt?: string\n  deletedAt?: string\n}\n\nconst hashRecordHosting = 'recordHosting'\n\nexport function isRecordHosting<V>(v: V) {\n  return is$typed(v, id, hashRecordHosting)\n}\n\nexport function validateRecordHosting<V>(v: V) {\n  return validate<RecordHosting & V>(v, id, hashRecordHosting)\n}\n\nexport interface ReporterStats {\n  $type?: 'tools.ozone.moderation.defs#reporterStats'\n  did: string\n  /** The total number of reports made by the user on accounts. */\n  accountReportCount: number\n  /** The total number of reports made by the user on records. */\n  recordReportCount: number\n  /** The total number of accounts reported by the user. */\n  reportedAccountCount: number\n  /** The total number of records reported by the user. */\n  reportedRecordCount: number\n  /** The total number of accounts taken down as a result of the user's reports. */\n  takendownAccountCount: number\n  /** The total number of records taken down as a result of the user's reports. */\n  takendownRecordCount: number\n  /** The total number of accounts labeled as a result of the user's reports. */\n  labeledAccountCount: number\n  /** The total number of records labeled as a result of the user's reports. */\n  labeledRecordCount: number\n}\n\nconst hashReporterStats = 'reporterStats'\n\nexport function isReporterStats<V>(v: V) {\n  return is$typed(v, id, hashReporterStats)\n}\n\nexport function validateReporterStats<V>(v: V) {\n  return validate<ReporterStats & V>(v, id, hashReporterStats)\n}\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'tools.ozone.moderation.defs#modTool'\n  /** Name/identifier of the source (e.g., 'automod', 'ozone/workspace') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n\n/** Moderation event timeline event for a PLC create operation */\nexport const TIMELINEEVENTPLCCREATE = `${id}#timelineEventPlcCreate`\n/** Moderation event timeline event for generic PLC operation */\nexport const TIMELINEEVENTPLCOPERATION = `${id}#timelineEventPlcOperation`\n/** Moderation event timeline event for a PLC tombstone operation */\nexport const TIMELINEEVENTPLCTOMBSTONE = `${id}#timelineEventPlcTombstone`\n\n/** View of a scheduled moderation action */\nexport interface ScheduledActionView {\n  $type?: 'tools.ozone.moderation.defs#scheduledActionView'\n  /** Auto-incrementing row ID */\n  id: number\n  /** Type of action to be executed */\n  action: 'takedown' | (string & {})\n  /** Serialized event object that will be propagated to the event when performed */\n  eventData?: { [_ in string]: unknown }\n  /** Subject DID for the action */\n  did: string\n  /** Exact time to execute the action */\n  executeAt?: string\n  /** Earliest time to execute the action (for randomized scheduling) */\n  executeAfter?: string\n  /** Latest time to execute the action (for randomized scheduling) */\n  executeUntil?: string\n  /** Whether execution time should be randomized within the specified range */\n  randomizeExecution?: boolean\n  /** DID of the user who created this scheduled action */\n  createdBy: string\n  /** When the scheduled action was created */\n  createdAt: string\n  /** When the scheduled action was last updated */\n  updatedAt?: string\n  /** Current status of the scheduled action */\n  status: 'pending' | 'executed' | 'cancelled' | 'failed' | (string & {})\n  /** When the action was last attempted to be executed */\n  lastExecutedAt?: string\n  /** Reason for the last execution failure */\n  lastFailureReason?: string\n  /** ID of the moderation event created when action was successfully executed */\n  executionEventId?: number\n}\n\nconst hashScheduledActionView = 'scheduledActionView'\n\nexport function isScheduledActionView<V>(v: V) {\n  return is$typed(v, id, hashScheduledActionView)\n}\n\nexport function validateScheduledActionView<V>(v: V) {\n  return validate<ScheduledActionView & V>(v, id, hashScheduledActionView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.emitEvent'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  event:\n    | $Typed<ToolsOzoneModerationDefs.ModEventTakedown>\n    | $Typed<ToolsOzoneModerationDefs.ModEventAcknowledge>\n    | $Typed<ToolsOzoneModerationDefs.ModEventEscalate>\n    | $Typed<ToolsOzoneModerationDefs.ModEventComment>\n    | $Typed<ToolsOzoneModerationDefs.ModEventLabel>\n    | $Typed<ToolsOzoneModerationDefs.ModEventReport>\n    | $Typed<ToolsOzoneModerationDefs.ModEventMute>\n    | $Typed<ToolsOzoneModerationDefs.ModEventUnmute>\n    | $Typed<ToolsOzoneModerationDefs.ModEventMuteReporter>\n    | $Typed<ToolsOzoneModerationDefs.ModEventUnmuteReporter>\n    | $Typed<ToolsOzoneModerationDefs.ModEventReverseTakedown>\n    | $Typed<ToolsOzoneModerationDefs.ModEventResolveAppeal>\n    | $Typed<ToolsOzoneModerationDefs.ModEventEmail>\n    | $Typed<ToolsOzoneModerationDefs.ModEventDivert>\n    | $Typed<ToolsOzoneModerationDefs.ModEventTag>\n    | $Typed<ToolsOzoneModerationDefs.AccountEvent>\n    | $Typed<ToolsOzoneModerationDefs.IdentityEvent>\n    | $Typed<ToolsOzoneModerationDefs.RecordEvent>\n    | $Typed<ToolsOzoneModerationDefs.ModEventPriorityScore>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssuranceEvent>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssuranceOverrideEvent>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssurancePurgeEvent>\n    | $Typed<ToolsOzoneModerationDefs.RevokeAccountCredentialsEvent>\n    | $Typed<ToolsOzoneModerationDefs.ScheduleTakedownEvent>\n    | $Typed<ToolsOzoneModerationDefs.CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  subjectBlobCids?: string[]\n  createdBy: string\n  modTool?: ToolsOzoneModerationDefs.ModTool\n  /** An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject. */\n  externalId?: string\n}\n\nexport type OutputSchema = ToolsOzoneModerationDefs.ModEventView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class SubjectHasActionError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class DuplicateExternalIdError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'SubjectHasAction') return new SubjectHasActionError(e)\n    if (e.error === 'DuplicateExternalId')\n      return new DuplicateExternalIdError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getAccountTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getAccountTimeline'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  timeline: TimelineItem[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n  }\n\n  return e\n}\n\nexport interface TimelineItem {\n  $type?: 'tools.ozone.moderation.getAccountTimeline#timelineItem'\n  day: string\n  summary: TimelineItemSummary[]\n}\n\nconst hashTimelineItem = 'timelineItem'\n\nexport function isTimelineItem<V>(v: V) {\n  return is$typed(v, id, hashTimelineItem)\n}\n\nexport function validateTimelineItem<V>(v: V) {\n  return validate<TimelineItem & V>(v, id, hashTimelineItem)\n}\n\nexport interface TimelineItemSummary {\n  $type?: 'tools.ozone.moderation.getAccountTimeline#timelineItemSummary'\n  eventSubjectType: 'account' | 'record' | 'chat' | (string & {})\n  eventType:\n    | 'tools.ozone.moderation.defs#modEventTakedown'\n    | 'tools.ozone.moderation.defs#modEventReverseTakedown'\n    | 'tools.ozone.moderation.defs#modEventComment'\n    | 'tools.ozone.moderation.defs#modEventReport'\n    | 'tools.ozone.moderation.defs#modEventLabel'\n    | 'tools.ozone.moderation.defs#modEventAcknowledge'\n    | 'tools.ozone.moderation.defs#modEventEscalate'\n    | 'tools.ozone.moderation.defs#modEventMute'\n    | 'tools.ozone.moderation.defs#modEventUnmute'\n    | 'tools.ozone.moderation.defs#modEventMuteReporter'\n    | 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n    | 'tools.ozone.moderation.defs#modEventEmail'\n    | 'tools.ozone.moderation.defs#modEventResolveAppeal'\n    | 'tools.ozone.moderation.defs#modEventDivert'\n    | 'tools.ozone.moderation.defs#modEventTag'\n    | 'tools.ozone.moderation.defs#accountEvent'\n    | 'tools.ozone.moderation.defs#identityEvent'\n    | 'tools.ozone.moderation.defs#recordEvent'\n    | 'tools.ozone.moderation.defs#modEventPriorityScore'\n    | 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n    | 'tools.ozone.moderation.defs#timelineEventPlcCreate'\n    | 'tools.ozone.moderation.defs#timelineEventPlcOperation'\n    | 'tools.ozone.moderation.defs#timelineEventPlcTombstone'\n    | 'tools.ozone.hosting.getAccountHistory#accountCreated'\n    | 'tools.ozone.hosting.getAccountHistory#emailConfirmed'\n    | 'tools.ozone.hosting.getAccountHistory#passwordUpdated'\n    | 'tools.ozone.hosting.getAccountHistory#handleUpdated'\n    | 'tools.ozone.moderation.defs#scheduleTakedownEvent'\n    | 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n    | (string & {})\n  count: number\n}\n\nconst hashTimelineItemSummary = 'timelineItemSummary'\n\nexport function isTimelineItemSummary<V>(v: V) {\n  return is$typed(v, id, hashTimelineItemSummary)\n}\n\nexport function validateTimelineItemSummary<V>(v: V) {\n  return validate<TimelineItemSummary & V>(v, id, hashTimelineItemSummary)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getEvent.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getEvent'\n\nexport type QueryParams = {\n  id: number\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.ModEventViewDetail\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRecord'\n\nexport type QueryParams = {\n  uri: string\n  cid?: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.RecordViewDetail\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RecordNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RecordNotFound') return new RecordNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRecords'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  records: (\n    | $Typed<ToolsOzoneModerationDefs.RecordViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RecordViewNotFound>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRepo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.RepoViewDetail\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RepoNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RepoNotFound') return new RepoNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getReporterStats.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getReporterStats'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  stats: ToolsOzoneModerationDefs.ReporterStats[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRepos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  repos: (\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n  )[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/getSubjects.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getSubjects'\n\nexport type QueryParams = {\n  subjects: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subjects: ToolsOzoneModerationDefs.SubjectView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/listScheduledActions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.listScheduledActions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Filter actions scheduled to execute after this time */\n  startsAfter?: string\n  /** Filter actions scheduled to execute before this time */\n  endsBefore?: string\n  /** Filter actions for specific DID subjects */\n  subjects?: string[]\n  /** Filter actions by status */\n  statuses: ('pending' | 'executed' | 'cancelled' | 'failed' | (string & {}))[]\n  /** Maximum number of results to return */\n  limit?: number\n  /** Cursor for pagination */\n  cursor?: string\n}\n\nexport interface OutputSchema {\n  actions: ToolsOzoneModerationDefs.ScheduledActionView[]\n  /** Cursor for next page of results */\n  cursor?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/queryEvents.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.queryEvents'\n\nexport type QueryParams = {\n  /** The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned. */\n  types?: string[]\n  createdBy?: string\n  /** Sort direction for the events. Defaults to descending order of created at timestamp. */\n  sortDirection?: 'asc' | 'desc'\n  /** Retrieve events created after a given timestamp */\n  createdAfter?: string\n  /** Retrieve events created before a given timestamp */\n  createdBefore?: string\n  subject?: string\n  /** If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored. */\n  collections?: string[]\n  /** If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */\n  subjectType?: 'account' | 'record' | (string & {})\n  /** If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned. */\n  includeAllUserRecords?: boolean\n  limit?: number\n  /** If true, only events with comments are returned */\n  hasComment?: boolean\n  /** If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition. */\n  comment?: string\n  /** If specified, only events where all of these labels were added are returned */\n  addedLabels?: string[]\n  /** If specified, only events where all of these labels were removed are returned */\n  removedLabels?: string[]\n  /** If specified, only events where all of these tags were added are returned */\n  addedTags?: string[]\n  /** If specified, only events where all of these tags were removed are returned */\n  removedTags?: string[]\n  reportTypes?: string[]\n  policies?: string[]\n  /** If specified, only events where the modTool name matches any of the given values are returned */\n  modTool?: string[]\n  /** If specified, only events where the batchId matches the given value are returned */\n  batchId?: string\n  /** If specified, only events where the age assurance state matches the given value are returned */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n  /** If specified, only events where strikeCount value is set are returned. */\n  withStrike?: boolean\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  events: ToolsOzoneModerationDefs.ModEventView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.queryStatuses'\n\nexport type QueryParams = {\n  /** Number of queues being used by moderators. Subjects will be split among all queues. */\n  queueCount?: number\n  /** Index of the queue to fetch subjects from. Works only when queueCount value is specified. */\n  queueIndex?: number\n  /** A seeder to shuffle/balance the queue items. */\n  queueSeed?: string\n  /** All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned. */\n  includeAllUserRecords?: boolean\n  /** The subject to get the status for. */\n  subject?: string\n  /** Search subjects by keyword from comments */\n  comment?: string\n  /** Search subjects reported after a given timestamp */\n  reportedAfter?: string\n  /** Search subjects reported before a given timestamp */\n  reportedBefore?: string\n  /** Search subjects reviewed after a given timestamp */\n  reviewedAfter?: string\n  /** Search subjects where the associated record/account was deleted after a given timestamp */\n  hostingDeletedAfter?: string\n  /** Search subjects where the associated record/account was deleted before a given timestamp */\n  hostingDeletedBefore?: string\n  /** Search subjects where the associated record/account was updated after a given timestamp */\n  hostingUpdatedAfter?: string\n  /** Search subjects where the associated record/account was updated before a given timestamp */\n  hostingUpdatedBefore?: string\n  /** Search subjects by the status of the associated record/account */\n  hostingStatuses?: string[]\n  /** Search subjects reviewed before a given timestamp */\n  reviewedBefore?: string\n  /** By default, we don't include muted subjects in the results. Set this to true to include them. */\n  includeMuted?: boolean\n  /** When set to true, only muted subjects and reporters will be returned. */\n  onlyMuted?: boolean\n  /** Specify when fetching subjects in a certain state */\n  reviewState?:\n    | 'tools.ozone.moderation.defs#reviewOpen'\n    | 'tools.ozone.moderation.defs#reviewClosed'\n    | 'tools.ozone.moderation.defs#reviewEscalated'\n    | 'tools.ozone.moderation.defs#reviewNone'\n    | (string & {})\n  ignoreSubjects?: string[]\n  /** Get all subject statuses that were reviewed by a specific moderator */\n  lastReviewedBy?: string\n  sortField?:\n    | 'lastReviewedAt'\n    | 'lastReportedAt'\n    | 'reportedRecordsCount'\n    | 'takendownRecordsCount'\n    | 'priorityScore'\n  sortDirection?: 'asc' | 'desc'\n  /** Get subjects that were taken down */\n  takendown?: boolean\n  /** Get subjects in unresolved appealed status */\n  appealed?: boolean\n  limit?: number\n  tags?: string[]\n  excludeTags?: string[]\n  cursor?: string\n  /** If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored. */\n  collections?: string[]\n  /** If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */\n  subjectType?: 'account' | 'record' | (string & {})\n  /** If specified, only subjects that belong to an account that has at least this many suspensions will be returned. */\n  minAccountSuspendCount?: number\n  /** If specified, only subjects that belong to an account that has at least this many reported records will be returned. */\n  minReportedRecordsCount?: number\n  /** If specified, only subjects that belong to an account that has at least this many taken down records will be returned. */\n  minTakendownRecordsCount?: number\n  /** If specified, only subjects that have priority score value above the given value will be returned. */\n  minPriorityScore?: number\n  /** If specified, only subjects that belong to an account that has at least this many active strikes will be returned. */\n  minStrikeCount?: number\n  /** If specified, only subjects with the given age assurance state will be returned. */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subjectStatuses: ToolsOzoneModerationDefs.SubjectStatusView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/scheduleAction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.scheduleAction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  action: $Typed<Takedown> | { $type: string }\n  /** Array of DID subjects to schedule the action for */\n  subjects: string[]\n  createdBy: string\n  scheduling: SchedulingConfig\n  modTool?: ToolsOzoneModerationDefs.ModTool\n}\n\nexport type OutputSchema = ScheduledActionResults\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\n/** Schedule a takedown action */\nexport interface Takedown {\n  $type?: 'tools.ozone.moderation.scheduleAction#takedown'\n  comment?: string\n  /** Indicates how long the takedown should be in effect before automatically expiring. */\n  durationInHours?: number\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n  /** Names/Keywords of the policies that drove the decision. */\n  policies?: string[]\n  /** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */\n  severityLevel?: string\n  /** Number of strikes to assign to the user when takedown is applied. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n  /** Email content to be sent to the user upon takedown. */\n  emailContent?: string\n  /** Subject of the email to be sent to the user upon takedown. */\n  emailSubject?: string\n}\n\nconst hashTakedown = 'takedown'\n\nexport function isTakedown<V>(v: V) {\n  return is$typed(v, id, hashTakedown)\n}\n\nexport function validateTakedown<V>(v: V) {\n  return validate<Takedown & V>(v, id, hashTakedown)\n}\n\n/** Configuration for when the action should be executed */\nexport interface SchedulingConfig {\n  $type?: 'tools.ozone.moderation.scheduleAction#schedulingConfig'\n  /** Exact time to execute the action */\n  executeAt?: string\n  /** Earliest time to execute the action (for randomized scheduling) */\n  executeAfter?: string\n  /** Latest time to execute the action (for randomized scheduling) */\n  executeUntil?: string\n}\n\nconst hashSchedulingConfig = 'schedulingConfig'\n\nexport function isSchedulingConfig<V>(v: V) {\n  return is$typed(v, id, hashSchedulingConfig)\n}\n\nexport function validateSchedulingConfig<V>(v: V) {\n  return validate<SchedulingConfig & V>(v, id, hashSchedulingConfig)\n}\n\nexport interface ScheduledActionResults {\n  $type?: 'tools.ozone.moderation.scheduleAction#scheduledActionResults'\n  succeeded: string[]\n  failed: FailedScheduling[]\n}\n\nconst hashScheduledActionResults = 'scheduledActionResults'\n\nexport function isScheduledActionResults<V>(v: V) {\n  return is$typed(v, id, hashScheduledActionResults)\n}\n\nexport function validateScheduledActionResults<V>(v: V) {\n  return validate<ScheduledActionResults & V>(v, id, hashScheduledActionResults)\n}\n\nexport interface FailedScheduling {\n  $type?: 'tools.ozone.moderation.scheduleAction#failedScheduling'\n  subject: string\n  error: string\n  errorCode?: string\n}\n\nconst hashFailedScheduling = 'failedScheduling'\n\nexport function isFailedScheduling<V>(v: V) {\n  return is$typed(v, id, hashFailedScheduling)\n}\n\nexport function validateFailedScheduling<V>(v: V) {\n  return validate<FailedScheduling & V>(v, id, hashFailedScheduling)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/moderation/searchRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.searchRepos'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead */\n  term?: string\n  q?: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: ToolsOzoneModerationDefs.RepoView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/report/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.report.defs'\n\nexport type ReasonType =\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n/** An issue not included in these options */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Animal welfare violations */\nexport const REASONVIOLENCEANIMAL = `${id}#reasonViolenceAnimal`\n/** Threats or incitement */\nexport const REASONVIOLENCETHREATS = `${id}#reasonViolenceThreats`\n/** Graphic violent content */\nexport const REASONVIOLENCEGRAPHICCONTENT = `${id}#reasonViolenceGraphicContent`\n/** Glorification of violence */\nexport const REASONVIOLENCEGLORIFICATION = `${id}#reasonViolenceGlorification`\n/** Extremist content. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONVIOLENCEEXTREMISTCONTENT = `${id}#reasonViolenceExtremistContent`\n/** Human trafficking */\nexport const REASONVIOLENCETRAFFICKING = `${id}#reasonViolenceTrafficking`\n/** Other violent content */\nexport const REASONVIOLENCEOTHER = `${id}#reasonViolenceOther`\n/** Adult sexual abuse content */\nexport const REASONSEXUALABUSECONTENT = `${id}#reasonSexualAbuseContent`\n/** Non-consensual intimate imagery */\nexport const REASONSEXUALNCII = `${id}#reasonSexualNCII`\n/** Deepfake adult content */\nexport const REASONSEXUALDEEPFAKE = `${id}#reasonSexualDeepfake`\n/** Animal sexual abuse */\nexport const REASONSEXUALANIMAL = `${id}#reasonSexualAnimal`\n/** Unlabelled adult content */\nexport const REASONSEXUALUNLABELED = `${id}#reasonSexualUnlabeled`\n/** Other sexual violence content */\nexport const REASONSEXUALOTHER = `${id}#reasonSexualOther`\n/** Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYCSAM = `${id}#reasonChildSafetyCSAM`\n/** Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYGROOM = `${id}#reasonChildSafetyGroom`\n/** Privacy violation involving a minor */\nexport const REASONCHILDSAFETYPRIVACY = `${id}#reasonChildSafetyPrivacy`\n/** Harassment or bullying of minors */\nexport const REASONCHILDSAFETYHARASSMENT = `${id}#reasonChildSafetyHarassment`\n/** Other child safety. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYOTHER = `${id}#reasonChildSafetyOther`\n/** Trolling */\nexport const REASONHARASSMENTTROLL = `${id}#reasonHarassmentTroll`\n/** Targeted harassment */\nexport const REASONHARASSMENTTARGETED = `${id}#reasonHarassmentTargeted`\n/** Hate speech */\nexport const REASONHARASSMENTHATESPEECH = `${id}#reasonHarassmentHateSpeech`\n/** Doxxing */\nexport const REASONHARASSMENTDOXXING = `${id}#reasonHarassmentDoxxing`\n/** Other harassing or hateful content */\nexport const REASONHARASSMENTOTHER = `${id}#reasonHarassmentOther`\n/** Fake account or bot */\nexport const REASONMISLEADINGBOT = `${id}#reasonMisleadingBot`\n/** Impersonation */\nexport const REASONMISLEADINGIMPERSONATION = `${id}#reasonMisleadingImpersonation`\n/** Spam */\nexport const REASONMISLEADINGSPAM = `${id}#reasonMisleadingSpam`\n/** Scam */\nexport const REASONMISLEADINGSCAM = `${id}#reasonMisleadingScam`\n/** False information about elections */\nexport const REASONMISLEADINGELECTIONS = `${id}#reasonMisleadingElections`\n/** Other misleading content */\nexport const REASONMISLEADINGOTHER = `${id}#reasonMisleadingOther`\n/** Hacking or system attacks */\nexport const REASONRULESITESECURITY = `${id}#reasonRuleSiteSecurity`\n/** Promoting or selling prohibited items or services */\nexport const REASONRULEPROHIBITEDSALES = `${id}#reasonRuleProhibitedSales`\n/** Banned user returning */\nexport const REASONRULEBANEVASION = `${id}#reasonRuleBanEvasion`\n/** Other */\nexport const REASONRULEOTHER = `${id}#reasonRuleOther`\n/** Content promoting or depicting self-harm */\nexport const REASONSELFHARMCONTENT = `${id}#reasonSelfHarmContent`\n/** Eating disorders */\nexport const REASONSELFHARMED = `${id}#reasonSelfHarmED`\n/** Dangerous challenges or activities */\nexport const REASONSELFHARMSTUNTS = `${id}#reasonSelfHarmStunts`\n/** Dangerous substances or drug abuse */\nexport const REASONSELFHARMSUBSTANCES = `${id}#reasonSelfHarmSubstances`\n/** Other dangerous content */\nexport const REASONSELFHARMOTHER = `${id}#reasonSelfHarmOther`\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/safelink/addRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.addRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to apply the rule to */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  action: ToolsOzoneSafelinkDefs.ActionType\n  reason: ToolsOzoneSafelinkDefs.ReasonType\n  /** Optional comment about the decision */\n  comment?: string\n  /** Author DID. Only respected when using admin auth */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class InvalidUrlError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class RuleAlreadyExistsError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'InvalidUrl') return new InvalidUrlError(e)\n    if (e.error === 'RuleAlreadyExists') return new RuleAlreadyExistsError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/safelink/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.defs'\n\n/** An event for URL safety decisions */\nexport interface Event {\n  $type?: 'tools.ozone.safelink.defs#event'\n  /** Auto-incrementing row ID */\n  id: number\n  eventType: EventType\n  /** The URL that this rule applies to */\n  url: string\n  pattern: PatternType\n  action: ActionType\n  reason: ReasonType\n  /** DID of the user who created this rule */\n  createdBy: string\n  createdAt: string\n  /** Optional comment about the decision */\n  comment?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n\nexport type EventType = 'addRule' | 'updateRule' | 'removeRule' | (string & {})\nexport type PatternType = 'domain' | 'url' | (string & {})\nexport type ActionType = 'block' | 'warn' | 'whitelist' | (string & {})\nexport type ReasonType = 'csam' | 'spam' | 'phishing' | 'none' | (string & {})\n\n/** Input for creating a URL safety rule */\nexport interface UrlRule {\n  $type?: 'tools.ozone.safelink.defs#urlRule'\n  /** The URL or domain to apply the rule to */\n  url: string\n  pattern: PatternType\n  action: ActionType\n  reason: ReasonType\n  /** Optional comment about the decision */\n  comment?: string\n  /** DID of the user added the rule. */\n  createdBy: string\n  /** Timestamp when the rule was created */\n  createdAt: string\n  /** Timestamp when the rule was last updated */\n  updatedAt: string\n}\n\nconst hashUrlRule = 'urlRule'\n\nexport function isUrlRule<V>(v: V) {\n  return is$typed(v, id, hashUrlRule)\n}\n\nexport function validateUrlRule<V>(v: V) {\n  return validate<UrlRule & V>(v, id, hashUrlRule)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/safelink/queryEvents.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.queryEvents'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Cursor for pagination */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit?: number\n  /** Filter by specific URLs or domains */\n  urls?: string[]\n  /** Filter by pattern type */\n  patternType?: string\n  /** Sort direction */\n  sortDirection?: 'asc' | 'desc' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** Next cursor for pagination. Only present if there are more results. */\n  cursor?: string\n  events: ToolsOzoneSafelinkDefs.Event[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/safelink/queryRules.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.queryRules'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Cursor for pagination */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit?: number\n  /** Filter by specific URLs or domains */\n  urls?: string[]\n  /** Filter by pattern type */\n  patternType?: string\n  /** Filter by action types */\n  actions?: string[]\n  /** Filter by reason type */\n  reason?: string\n  /** Filter by rule creator */\n  createdBy?: string\n  /** Sort direction */\n  sortDirection?: 'asc' | 'desc' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** Next cursor for pagination. Only present if there are more results. */\n  cursor?: string\n  rules: ToolsOzoneSafelinkDefs.UrlRule[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/safelink/removeRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.removeRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to remove the rule for */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  /** Optional comment about why the rule is being removed */\n  comment?: string\n  /** Optional DID of the user. Only respected when using admin auth. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RuleNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RuleNotFound') return new RuleNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/safelink/updateRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.updateRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to update the rule for */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  action: ToolsOzoneSafelinkDefs.ActionType\n  reason: ToolsOzoneSafelinkDefs.ReasonType\n  /** Optional comment about the update */\n  comment?: string\n  /** Optional DID to credit as the creator. Only respected for admin_token authentication. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class RuleNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'RuleNotFound') return new RuleNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/server/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.server.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  appview?: ServiceConfig\n  pds?: ServiceConfig\n  blobDivert?: ServiceConfig\n  chat?: ServiceConfig\n  viewer?: ViewerConfig\n  /** The did of the verifier used for verification. */\n  verifierDid?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface ServiceConfig {\n  $type?: 'tools.ozone.server.getConfig#serviceConfig'\n  url?: string\n}\n\nconst hashServiceConfig = 'serviceConfig'\n\nexport function isServiceConfig<V>(v: V) {\n  return is$typed(v, id, hashServiceConfig)\n}\n\nexport function validateServiceConfig<V>(v: V) {\n  return validate<ServiceConfig & V>(v, id, hashServiceConfig)\n}\n\nexport interface ViewerConfig {\n  $type?: 'tools.ozone.server.getConfig#viewerConfig'\n  role?:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n}\n\nconst hashViewerConfig = 'viewerConfig'\n\nexport function isViewerConfig<V>(v: V) {\n  return is$typed(v, id, hashViewerConfig)\n}\n\nexport function validateViewerConfig<V>(v: V) {\n  return validate<ViewerConfig & V>(v, id, hashViewerConfig)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/addValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.addValues'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to add values to */\n  name: string\n  /** Array of string values to add to the set */\n  values: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.defs'\n\nexport interface Set {\n  $type?: 'tools.ozone.set.defs#set'\n  name: string\n  description?: string\n}\n\nconst hashSet = 'set'\n\nexport function isSet<V>(v: V) {\n  return is$typed(v, id, hashSet)\n}\n\nexport function validateSet<V>(v: V) {\n  return validate<Set & V>(v, id, hashSet)\n}\n\nexport interface SetView {\n  $type?: 'tools.ozone.set.defs#setView'\n  name: string\n  description?: string\n  setSize: number\n  createdAt: string\n  updatedAt: string\n}\n\nconst hashSetView = 'setView'\n\nexport function isSetView<V>(v: V) {\n  return is$typed(v, id, hashSetView)\n}\n\nexport function validateSetView<V>(v: V) {\n  return validate<SetView & V>(v, id, hashSetView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/deleteSet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.deleteSet'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to delete */\n  name: string\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class SetNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'SetNotFound') return new SetNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/deleteValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.deleteValues'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to delete values from */\n  name: string\n  /** Array of string values to delete from the set */\n  values: string[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class SetNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'SetNotFound') return new SetNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/getValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.getValues'\n\nexport type QueryParams = {\n  name: string\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  set: ToolsOzoneSetDefs.SetView\n  values: string[]\n  cursor?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class SetNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'SetNotFound') return new SetNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/querySets.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.querySets'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n  namePrefix?: string\n  sortBy?: 'name' | 'createdAt' | 'updatedAt'\n  /** Defaults to ascending order of name field. */\n  sortDirection?: 'asc' | 'desc'\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  sets: ToolsOzoneSetDefs.SetView[]\n  cursor?: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/set/upsertSet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.upsertSet'\n\nexport type QueryParams = {}\nexport type InputSchema = ToolsOzoneSetDefs.Set\nexport type OutputSchema = ToolsOzoneSetDefs.SetView\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/setting/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.defs'\n\nexport interface Option {\n  $type?: 'tools.ozone.setting.defs#option'\n  key: string\n  did: string\n  value: { [_ in string]: unknown }\n  description?: string\n  createdAt?: string\n  updatedAt?: string\n  managerRole?:\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n  scope: 'instance' | 'personal' | (string & {})\n  createdBy: string\n  lastUpdatedBy: string\n}\n\nconst hashOption = 'option'\n\nexport function isOption<V>(v: V) {\n  return is$typed(v, id, hashOption)\n}\n\nexport function validateOption<V>(v: V) {\n  return validate<Option & V>(v, id, hashOption)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/setting/listOptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSettingDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.listOptions'\n\nexport type QueryParams = {\n  limit?: number\n  cursor?: string\n  scope?: 'instance' | 'personal' | (string & {})\n  /** Filter keys by prefix */\n  prefix?: string\n  /** Filter for only the specified keys. Ignored if prefix is provided */\n  keys?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  options: ToolsOzoneSettingDefs.Option[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/setting/removeOptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.removeOptions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  keys: string[]\n  scope: 'instance' | 'personal' | (string & {})\n}\n\nexport interface OutputSchema {}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/setting/upsertOption.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSettingDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.upsertOption'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  key: string\n  scope: 'instance' | 'personal' | (string & {})\n  value: { [_ in string]: unknown }\n  description?: string\n  managerRole?:\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleAdmin'\n    | (string & {})\n}\n\nexport interface OutputSchema {\n  option: ToolsOzoneSettingDefs.Option\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/signature/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.defs'\n\nexport interface SigDetail {\n  $type?: 'tools.ozone.signature.defs#sigDetail'\n  property: string\n  value: string\n}\n\nconst hashSigDetail = 'sigDetail'\n\nexport function isSigDetail<V>(v: V) {\n  return is$typed(v, id, hashSigDetail)\n}\n\nexport function validateSigDetail<V>(v: V) {\n  return validate<SigDetail & V>(v, id, hashSigDetail)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/signature/findCorrelation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSignatureDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.findCorrelation'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  details: ToolsOzoneSignatureDefs.SigDetail[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/signature/findRelatedAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ToolsOzoneSignatureDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.findRelatedAccounts'\n\nexport type QueryParams = {\n  did: string\n  cursor?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: RelatedAccount[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface RelatedAccount {\n  $type?: 'tools.ozone.signature.findRelatedAccounts#relatedAccount'\n  account: ComAtprotoAdminDefs.AccountView\n  similarities?: ToolsOzoneSignatureDefs.SigDetail[]\n}\n\nconst hashRelatedAccount = 'relatedAccount'\n\nexport function isRelatedAccount<V>(v: V) {\n  return is$typed(v, id, hashRelatedAccount)\n}\n\nexport function validateRelatedAccount<V>(v: V) {\n  return validate<RelatedAccount & V>(v, id, hashRelatedAccount)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/signature/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.searchAccounts'\n\nexport type QueryParams = {\n  values: string[]\n  cursor?: string\n  limit?: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/team/addMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.addMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleTriage'\n    | (string & {})\n}\n\nexport type OutputSchema = ToolsOzoneTeamDefs.Member\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class MemberAlreadyExistsError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'MemberAlreadyExists')\n      return new MemberAlreadyExistsError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/team/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.defs'\n\nexport interface Member {\n  $type?: 'tools.ozone.team.defs#member'\n  did: string\n  disabled?: boolean\n  profile?: AppBskyActorDefs.ProfileViewDetailed\n  createdAt?: string\n  updatedAt?: string\n  lastUpdatedBy?: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n}\n\nconst hashMember = 'member'\n\nexport function isMember<V>(v: V) {\n  return is$typed(v, id, hashMember)\n}\n\nexport function validateMember<V>(v: V) {\n  return validate<Member & V>(v, id, hashMember)\n}\n\n/** Admin role. Highest level of access, can perform all actions. */\nexport const ROLEADMIN = `${id}#roleAdmin`\n/** Moderator role. Can perform most actions. */\nexport const ROLEMODERATOR = `${id}#roleModerator`\n/** Triage role. Mostly intended for monitoring and escalating issues. */\nexport const ROLETRIAGE = `${id}#roleTriage`\n/** Verifier role. Only allowed to issue verifications. */\nexport const ROLEVERIFIER = `${id}#roleVerifier`\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/team/deleteMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.deleteMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n}\n\nexport class MemberNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport class CannotDeleteSelfError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'MemberNotFound') return new MemberNotFoundError(e)\n    if (e.error === 'CannotDeleteSelf') return new CannotDeleteSelfError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/team/listMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.listMembers'\n\nexport type QueryParams = {\n  q?: string\n  disabled?: boolean\n  roles?: string[]\n  limit?: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  members: ToolsOzoneTeamDefs.Member[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/team/updateMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.updateMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  disabled?: boolean\n  role?:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleTriage'\n    | (string & {})\n}\n\nexport type OutputSchema = ToolsOzoneTeamDefs.Member\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport class MemberNotFoundError extends XRPCError {\n  constructor(src: XRPCError) {\n    super(src.status, src.error, src.message, src.headers, { cause: src })\n  }\n}\n\nexport function toKnownErr(e: any) {\n  if (e instanceof XRPCError) {\n    if (e.error === 'MemberNotFound') return new MemberNotFoundError(e)\n  }\n\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/verification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from '../moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.defs'\n\n/** Verification data for the associated subject. */\nexport interface VerificationView {\n  $type?: 'tools.ozone.verification.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** The subject of the verification. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Timestamp when the verification was created. */\n  createdAt: string\n  /** Describes the reason for revocation, also indicating that the verification is no longer valid. */\n  revokeReason?: string\n  /** Timestamp when the verification was revoked. */\n  revokedAt?: string\n  /** The user who revoked this verification. */\n  revokedBy?: string\n  subjectProfile?: { $type: string }\n  issuerProfile?: { $type: string }\n  subjectRepo?:\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n  issuerRepo?:\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/verification/grantVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneVerificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.grantVerifications'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of verification requests to process */\n  verifications: VerificationInput[]\n}\n\nexport interface OutputSchema {\n  verifications: ToolsOzoneVerificationDefs.VerificationView[]\n  failedVerifications: GrantError[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\nexport interface VerificationInput {\n  $type?: 'tools.ozone.verification.grantVerifications#verificationInput'\n  /** The did of the subject being verified */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying. */\n  displayName: string\n  /** Timestamp for verification record. Defaults to current time when not specified. */\n  createdAt?: string\n}\n\nconst hashVerificationInput = 'verificationInput'\n\nexport function isVerificationInput<V>(v: V) {\n  return is$typed(v, id, hashVerificationInput)\n}\n\nexport function validateVerificationInput<V>(v: V) {\n  return validate<VerificationInput & V>(v, id, hashVerificationInput)\n}\n\n/** Error object for failed verifications. */\nexport interface GrantError {\n  $type?: 'tools.ozone.verification.grantVerifications#grantError'\n  /** Error message describing the reason for failure. */\n  error: string\n  /** The did of the subject being verified */\n  subject: string\n}\n\nconst hashGrantError = 'grantError'\n\nexport function isGrantError<V>(v: V) {\n  return is$typed(v, id, hashGrantError)\n}\n\nexport function validateGrantError<V>(v: V) {\n  return validate<GrantError & V>(v, id, hashGrantError)\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/verification/listVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneVerificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.listVerifications'\n\nexport type QueryParams = {\n  /** Pagination cursor */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit?: number\n  /** Filter to verifications created after this timestamp */\n  createdAfter?: string\n  /** Filter to verifications created before this timestamp */\n  createdBefore?: string\n  /** Filter to verifications from specific issuers */\n  issuers?: string[]\n  /** Filter to specific verified DIDs */\n  subjects?: string[]\n  /** Sort direction for creation date */\n  sortDirection?: 'asc' | 'desc'\n  /** Filter to verifications that are revoked or not. By default, includes both. */\n  isRevoked?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  verifications: ToolsOzoneVerificationDefs.VerificationView[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n"
  },
  {
    "path": "packages/api/src/client/types/tools/ozone/verification/revokeVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { HeadersMap, XRPCError } from '@atproto/xrpc'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.revokeVerifications'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of verification record uris to revoke */\n  uris: string[]\n  /** Reason for revoking the verification. This is optional and can be omitted if not needed. */\n  revokeReason?: string\n}\n\nexport interface OutputSchema {\n  /** List of verification uris successfully revoked */\n  revokedVerifications: string[]\n  /** List of verification uris that couldn't be revoked, including failure reasons */\n  failedRevocations: RevokeError[]\n}\n\nexport interface CallOptions {\n  signal?: AbortSignal\n  headers?: HeadersMap\n  qp?: QueryParams\n  encoding?: 'application/json'\n}\n\nexport interface Response {\n  success: boolean\n  headers: HeadersMap\n  data: OutputSchema\n}\n\nexport function toKnownErr(e: any) {\n  return e\n}\n\n/** Error object for failed revocations */\nexport interface RevokeError {\n  $type?: 'tools.ozone.verification.revokeVerifications#revokeError'\n  /** The AT-URI of the verification record that failed to revoke. */\n  uri: string\n  /** Description of the error that occurred during revocation. */\n  error: string\n}\n\nconst hashRevokeError = 'revokeError'\n\nexport function isRevokeError<V>(v: V) {\n  return is$typed(v, id, hashRevokeError)\n}\n\nexport function validateRevokeError<V>(v: V) {\n  return validate<RevokeError & V>(v, id, hashRevokeError)\n}\n"
  },
  {
    "path": "packages/api/src/client/util.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\n\nimport { type ValidationResult } from '@atproto/lexicon'\n\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n\nexport type $Typed<V, T extends string = string> = V & { $type: T }\nexport type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>\n\nexport type $Type<Id extends string, Hash extends string> = Hash extends 'main'\n  ? Id\n  : `${Id}#${Hash}`\n\nfunction isObject<V>(v: V): v is V & object {\n  return v != null && typeof v === 'object'\n}\n\nfunction is$type<Id extends string, Hash extends string>(\n  $type: unknown,\n  id: Id,\n  hash: Hash,\n): $type is $Type<Id, Hash> {\n  return hash === 'main'\n    ? $type === id\n    : // $type === `${id}#${hash}`\n      typeof $type === 'string' &&\n        $type.length === id.length + 1 + hash.length &&\n        $type.charCodeAt(id.length) === 35 /* '#' */ &&\n        $type.startsWith(id) &&\n        $type.endsWith(hash)\n}\n\nexport type $TypedObject<\n  V,\n  Id extends string,\n  Hash extends string,\n> = V extends {\n  $type: $Type<Id, Hash>\n}\n  ? V\n  : V extends { $type?: string }\n    ? V extends { $type?: infer T extends $Type<Id, Hash> }\n      ? V & { $type: T }\n      : never\n    : V & { $type: $Type<Id, Hash> }\n\nexport function is$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is $TypedObject<V, Id, Hash> {\n  return isObject(v) && '$type' in v && is$type(v.$type, id, hash)\n}\n\nexport function maybe$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is V & object & { $type?: $Type<Id, Hash> } {\n  return (\n    isObject(v) &&\n    ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)\n  )\n}\n\nexport type Validator<R = unknown> = (v: unknown) => ValidationResult<R>\nexport type ValidatorParam<V extends Validator> =\n  V extends Validator<infer R> ? R : never\n\n/**\n * Utility function that allows to convert a \"validate*\" utility function into a\n * type predicate.\n */\nexport function asPredicate<V extends Validator>(validate: V) {\n  return function <T>(v: T): v is T & ValidatorParam<V> {\n    return validate(v).success\n  }\n}\n"
  },
  {
    "path": "packages/api/src/const.ts",
    "content": "export const BSKY_LABELER_DID = 'did:plc:ar7c4by46qjdydhdevvrndac'\n"
  },
  {
    "path": "packages/api/src/index.ts",
    "content": "import { Lexicons } from '@atproto/lexicon'\nimport { lexicons as internalLexicons } from './client/lexicons'\n\nexport { AtUri } from '@atproto/syntax'\nexport {\n  BlobRef,\n  jsonStringToLex,\n  jsonToLex,\n  lexToJson,\n  stringifyLex,\n} from '@atproto/lexicon'\nexport { parseLanguage } from '@atproto/common-web'\nexport * from './types'\nexport * from './const'\nexport * from './util'\nexport * from './client'\nexport { schemas } from './client/lexicons'\nexport type { $Typed, Un$Typed } from './client/util'\nexport { asPredicate } from './client/util'\nexport * from './rich-text/rich-text'\nexport * from './rich-text/sanitization'\nexport * from './rich-text/unicode'\nexport * from './rich-text/util'\nexport * from './moderation'\nexport * from './moderation/types'\nexport * from './mocker'\nexport * from './age-assurance'\nexport { DEFAULT_LABEL_SETTINGS, LABELS } from './moderation/const/labels'\nexport { Agent } from './agent'\n\nexport { AtpAgent, type AtpAgentOptions } from './atp-agent'\nexport { CredentialSession } from './atp-agent'\nexport { BskyAgent } from './bsky-agent'\n\nexport {\n  /** @deprecated */\n  AtpAgent as default,\n} from './atp-agent'\n\n// Expose a copy to prevent alteration of the internal Lexicon instance used by\n// the AtpBaseClient class.\nexport const lexicons = new Lexicons(internalLexicons)\n"
  },
  {
    "path": "packages/api/src/mocker.ts",
    "content": "import {\n  AppBskyActorDefs,\n  AppBskyEmbedRecord,\n  AppBskyFeedDefs,\n  AppBskyFeedPost,\n  AppBskyGraphDefs,\n  AppBskyNotificationListNotifications,\n  ComAtprotoLabelDefs,\n} from './client'\nimport { $Typed, Un$Typed } from './client/util'\n\nconst FAKE_CID = 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq'\n\nexport const mock = {\n  post({\n    text,\n    facets,\n    reply,\n    embed,\n  }: {\n    text: string\n    facets?: AppBskyFeedPost.Record['facets']\n    reply?: AppBskyFeedPost.ReplyRef\n    embed?: AppBskyFeedPost.Record['embed']\n  }): $Typed<AppBskyFeedPost.Record> {\n    return {\n      $type: 'app.bsky.feed.post',\n      text,\n      facets,\n      reply,\n      embed,\n      langs: ['en'],\n      createdAt: new Date().toISOString(),\n    }\n  },\n\n  postView({\n    record,\n    author,\n    embed,\n    replyCount,\n    repostCount,\n    likeCount,\n    viewer,\n    labels,\n  }: {\n    record: AppBskyFeedPost.Record\n    author: AppBskyActorDefs.ProfileViewBasic\n    embed?: AppBskyFeedDefs.PostView['embed']\n    replyCount?: number\n    repostCount?: number\n    likeCount?: number\n    viewer?: AppBskyFeedDefs.ViewerState\n    labels?: ComAtprotoLabelDefs.Label[]\n  }): $Typed<AppBskyFeedDefs.PostView> {\n    return {\n      $type: 'app.bsky.feed.defs#postView',\n      uri: `at://${author.did}/app.bsky.feed.post/fake`,\n      cid: FAKE_CID,\n      author,\n      record,\n      embed,\n      replyCount,\n      repostCount,\n      likeCount,\n      indexedAt: new Date().toISOString(),\n      viewer,\n      labels,\n    }\n  },\n\n  embedRecordView({\n    record,\n    author,\n    labels,\n  }: {\n    record: AppBskyFeedPost.Record\n    author: AppBskyActorDefs.ProfileViewBasic\n    labels?: ComAtprotoLabelDefs.Label[]\n  }): $Typed<AppBskyEmbedRecord.View> {\n    return {\n      $type: 'app.bsky.embed.record#view',\n      record: {\n        $type: 'app.bsky.embed.record#viewRecord',\n        uri: `at://${author.did}/app.bsky.feed.post/fake`,\n        cid: FAKE_CID,\n        author,\n        value: record,\n        labels,\n        indexedAt: new Date().toISOString(),\n      },\n    }\n  },\n\n  profileViewBasic({\n    handle,\n    displayName,\n    description,\n    viewer,\n    labels,\n  }: {\n    handle: string\n    displayName?: string\n    description?: string\n    viewer?: AppBskyActorDefs.ViewerState\n    labels?: ComAtprotoLabelDefs.Label[]\n  }): AppBskyActorDefs.ProfileViewBasic {\n    return {\n      did: `did:web:${handle}`,\n      handle,\n      displayName,\n      // @ts-expect-error technically not in ProfileViewBasic but useful in some cases\n      description,\n      viewer,\n      labels,\n    }\n  },\n\n  actorViewerState({\n    muted,\n    mutedByList,\n    blockedBy,\n    blocking,\n    blockingByList,\n    following,\n    followedBy,\n  }: {\n    muted?: boolean\n    mutedByList?: AppBskyGraphDefs.ListViewBasic\n    blockedBy?: boolean\n    blocking?: string\n    blockingByList?: AppBskyGraphDefs.ListViewBasic\n    following?: string\n    followedBy?: string\n  }): AppBskyActorDefs.ViewerState {\n    return {\n      muted,\n      mutedByList,\n      blockedBy,\n      blocking,\n      blockingByList,\n      following,\n      followedBy,\n    }\n  },\n\n  listViewBasic({ name }: { name: string }): AppBskyGraphDefs.ListViewBasic {\n    return {\n      uri: 'at://did:plc:fake/app.bsky.graph.list/fake',\n      cid: FAKE_CID,\n      name,\n      purpose: 'app.bsky.graph.defs#modlist',\n      indexedAt: new Date().toISOString(),\n    }\n  },\n\n  replyNotification({\n    author,\n    record,\n    labels,\n  }: {\n    record: AppBskyFeedPost.Record\n    author: Un$Typed<AppBskyActorDefs.ProfileViewBasic>\n    labels?: ComAtprotoLabelDefs.Label[]\n  }): AppBskyNotificationListNotifications.Notification {\n    return {\n      uri: `at://${author.did}/app.bsky.feed.post/fake`,\n      cid: FAKE_CID,\n      author,\n      reason: 'reply',\n      reasonSubject: `at://${author.did}/app.bsky.feed.post/fake-parent`,\n      record,\n      isRead: false,\n      indexedAt: new Date().toISOString(),\n      labels,\n    }\n  },\n\n  followNotification({\n    author,\n    subjectDid,\n    labels,\n  }: {\n    author: Un$Typed<AppBskyActorDefs.ProfileViewBasic>\n    subjectDid: string\n    labels?: ComAtprotoLabelDefs.Label[]\n  }): AppBskyNotificationListNotifications.Notification {\n    return {\n      uri: `at://${author.did}/app.bsky.graph.follow/fake`,\n      cid: FAKE_CID,\n      author,\n      reason: 'follow',\n      record: {\n        $type: 'app.bsky.graph.follow',\n        createdAt: new Date().toISOString(),\n        subject: subjectDid,\n      },\n      isRead: false,\n      indexedAt: new Date().toISOString(),\n      labels,\n    }\n  },\n\n  label({\n    val,\n    uri,\n    src,\n  }: {\n    val: string\n    uri: string\n    src?: string\n  }): ComAtprotoLabelDefs.Label {\n    return {\n      src: src || 'did:plc:fake-labeler',\n      uri,\n      val,\n      cts: new Date().toISOString(),\n    }\n  },\n}\n"
  },
  {
    "path": "packages/api/src/moderation/const/labels.ts",
    "content": "/** this doc is generated by ./scripts/code/labels.mjs **/\nimport { InterpretedLabelValueDefinition, LabelPreference } from '../types'\n\nexport type KnownLabelValue =\n  | '!hide'\n  | '!warn'\n  | '!no-unauthenticated'\n  | 'porn'\n  | 'sexual'\n  | 'nudity'\n  | 'graphic-media'\n  | 'gore'\n\nexport const DEFAULT_LABEL_SETTINGS: Record<string, LabelPreference> = {\n  porn: 'hide',\n  sexual: 'warn',\n  nudity: 'ignore',\n  'graphic-media': 'warn',\n}\n\nexport const LABELS: Record<KnownLabelValue, InterpretedLabelValueDefinition> =\n  {\n    '!hide': {\n      identifier: '!hide',\n      configurable: false,\n      defaultSetting: 'hide',\n      flags: ['no-override', 'no-self'],\n      severity: 'alert',\n      blurs: 'content',\n      behaviors: {\n        account: {\n          profileList: 'blur',\n          profileView: 'blur',\n          avatar: 'blur',\n          banner: 'blur',\n          displayName: 'blur',\n          contentList: 'blur',\n          contentView: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n          displayName: 'blur',\n        },\n        content: {\n          contentList: 'blur',\n          contentView: 'blur',\n        },\n      },\n      locales: [],\n    },\n    '!warn': {\n      identifier: '!warn',\n      configurable: false,\n      defaultSetting: 'warn',\n      flags: ['no-self'],\n      severity: 'none',\n      blurs: 'content',\n      behaviors: {\n        account: {\n          profileList: 'blur',\n          profileView: 'blur',\n          avatar: 'blur',\n          banner: 'blur',\n          contentList: 'blur',\n          contentView: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n          displayName: 'blur',\n        },\n        content: {\n          contentList: 'blur',\n          contentView: 'blur',\n        },\n      },\n      locales: [],\n    },\n    '!no-unauthenticated': {\n      identifier: '!no-unauthenticated',\n      configurable: false,\n      defaultSetting: 'hide',\n      flags: ['no-override', 'unauthed'],\n      severity: 'none',\n      blurs: 'content',\n      behaviors: {\n        account: {\n          profileList: 'blur',\n          profileView: 'blur',\n          avatar: 'blur',\n          banner: 'blur',\n          displayName: 'blur',\n          contentList: 'blur',\n          contentView: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n          displayName: 'blur',\n        },\n        content: {\n          contentList: 'blur',\n          contentView: 'blur',\n        },\n      },\n      locales: [],\n    },\n    porn: {\n      identifier: 'porn',\n      configurable: true,\n      defaultSetting: 'hide',\n      flags: ['adult'],\n      severity: 'none',\n      blurs: 'media',\n      behaviors: {\n        account: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        content: {\n          contentMedia: 'blur',\n        },\n      },\n      locales: [],\n    },\n    sexual: {\n      identifier: 'sexual',\n      configurable: true,\n      defaultSetting: 'warn',\n      flags: ['adult'],\n      severity: 'none',\n      blurs: 'media',\n      behaviors: {\n        account: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        content: {\n          contentMedia: 'blur',\n        },\n      },\n      locales: [],\n    },\n    nudity: {\n      identifier: 'nudity',\n      configurable: true,\n      defaultSetting: 'ignore',\n      flags: [],\n      severity: 'none',\n      blurs: 'media',\n      behaviors: {\n        account: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        content: {\n          contentMedia: 'blur',\n        },\n      },\n      locales: [],\n    },\n    'graphic-media': {\n      identifier: 'graphic-media',\n      flags: ['adult'],\n      configurable: true,\n      defaultSetting: 'warn',\n      severity: 'none',\n      blurs: 'media',\n      behaviors: {\n        account: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        content: {\n          contentMedia: 'blur',\n        },\n      },\n      locales: [],\n    },\n    /** @deprecated alias for `graphic-media` */\n    gore: {\n      identifier: 'gore',\n      flags: ['adult'],\n      configurable: true,\n      defaultSetting: 'warn',\n      severity: 'none',\n      blurs: 'media',\n      behaviors: {\n        account: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        profile: {\n          avatar: 'blur',\n          banner: 'blur',\n        },\n        content: {\n          contentMedia: 'blur',\n        },\n      },\n      locales: [],\n    },\n  }\n"
  },
  {
    "path": "packages/api/src/moderation/decision.ts",
    "content": "import { AppBskyGraphDefs } from '../client/index'\nimport { LABELS } from './const/labels'\nimport { MuteWordMatch } from './mutewords'\nimport {\n  BLOCK_BEHAVIOR,\n  CUSTOM_LABEL_VALUE_RE,\n  HIDE_BEHAVIOR,\n  Label,\n  LabelPreference,\n  LabelTarget,\n  MUTEWORD_BEHAVIOR,\n  MUTE_BEHAVIOR,\n  ModerationBehavior,\n  ModerationCause,\n  ModerationOpts,\n  NOOP_BEHAVIOR,\n} from './types'\nimport { ModerationUI } from './ui'\n\nenum ModerationBehaviorSeverity {\n  High,\n  Medium,\n  Low,\n}\n\nexport class ModerationDecision {\n  did = ''\n  isMe = false\n  causes: ModerationCause[] = []\n\n  constructor() {}\n\n  static merge(\n    ...decisions: (ModerationDecision | undefined)[]\n  ): ModerationDecision {\n    const decisionsFiltered = decisions.filter((v) => v != null)\n    const decision = new ModerationDecision()\n    if (decisionsFiltered[0]) {\n      decision.did = decisionsFiltered[0].did\n      decision.isMe = decisionsFiltered[0].isMe\n    }\n    decision.causes = decisionsFiltered.flatMap((d) => d.causes)\n    return decision\n  }\n\n  downgrade() {\n    for (const cause of this.causes) {\n      cause.downgraded = true\n    }\n    return this\n  }\n\n  get blocked() {\n    return !!this.blockCause\n  }\n\n  get muted() {\n    return !!this.muteCause\n  }\n\n  get blockCause() {\n    return this.causes.find(\n      (cause) =>\n        cause.type === 'blocking' ||\n        cause.type === 'blocked-by' ||\n        cause.type === 'block-other',\n    )\n  }\n\n  get muteCause() {\n    return this.causes.find((cause) => cause.type === 'muted')\n  }\n\n  get labelCauses() {\n    return this.causes.filter((cause) => cause.type === 'label')\n  }\n\n  ui(context: keyof ModerationBehavior): ModerationUI {\n    const ui = new ModerationUI()\n    for (const cause of this.causes) {\n      if (\n        cause.type === 'blocking' ||\n        cause.type === 'blocked-by' ||\n        cause.type === 'block-other'\n      ) {\n        if (this.isMe) {\n          continue\n        }\n        if (context === 'profileList' || context === 'contentList') {\n          ui.filters.push(cause)\n        }\n        if (!cause.downgraded) {\n          if (BLOCK_BEHAVIOR[context] === 'blur') {\n            ui.noOverride = true\n            ui.blurs.push(cause)\n          } else if (BLOCK_BEHAVIOR[context] === 'alert') {\n            ui.alerts.push(cause)\n          } else if (BLOCK_BEHAVIOR[context] === 'inform') {\n            ui.informs.push(cause)\n          }\n        }\n      } else if (cause.type === 'muted') {\n        if (this.isMe) {\n          continue\n        }\n        if (context === 'profileList' || context === 'contentList') {\n          ui.filters.push(cause)\n        }\n        if (!cause.downgraded) {\n          if (MUTE_BEHAVIOR[context] === 'blur') {\n            ui.blurs.push(cause)\n          } else if (MUTE_BEHAVIOR[context] === 'alert') {\n            ui.alerts.push(cause)\n          } else if (MUTE_BEHAVIOR[context] === 'inform') {\n            ui.informs.push(cause)\n          }\n        }\n      } else if (cause.type === 'mute-word') {\n        if (this.isMe) {\n          continue\n        }\n        if (context === 'contentList') {\n          ui.filters.push(cause)\n        }\n        if (!cause.downgraded) {\n          if (MUTEWORD_BEHAVIOR[context] === 'blur') {\n            ui.blurs.push(cause)\n          } else if (MUTEWORD_BEHAVIOR[context] === 'alert') {\n            ui.alerts.push(cause)\n          } else if (MUTEWORD_BEHAVIOR[context] === 'inform') {\n            ui.informs.push(cause)\n          }\n        }\n      } else if (cause.type === 'hidden') {\n        if (context === 'profileList' || context === 'contentList') {\n          ui.filters.push(cause)\n        }\n        if (!cause.downgraded) {\n          if (HIDE_BEHAVIOR[context] === 'blur') {\n            ui.blurs.push(cause)\n          } else if (HIDE_BEHAVIOR[context] === 'alert') {\n            ui.alerts.push(cause)\n          } else if (HIDE_BEHAVIOR[context] === 'inform') {\n            ui.informs.push(cause)\n          }\n        }\n      } else if (cause.type === 'label') {\n        if (context === 'profileList' && cause.target === 'account') {\n          if (cause.setting === 'hide' && !this.isMe) {\n            ui.filters.push(cause)\n          }\n        } else if (\n          context === 'contentList' &&\n          (cause.target === 'account' || cause.target === 'content')\n        ) {\n          if (cause.setting === 'hide' && !this.isMe) {\n            ui.filters.push(cause)\n          }\n        }\n        if (!cause.downgraded) {\n          if (cause.behavior[context] === 'blur') {\n            ui.blurs.push(cause)\n            if (cause.noOverride && !this.isMe) {\n              ui.noOverride = true\n            }\n          } else if (cause.behavior[context] === 'alert') {\n            ui.alerts.push(cause)\n          } else if (cause.behavior[context] === 'inform') {\n            ui.informs.push(cause)\n          }\n        }\n      }\n    }\n\n    ui.filters.sort(sortByPriority)\n    ui.blurs.sort(sortByPriority)\n\n    return ui\n  }\n\n  setDid(did: string) {\n    this.did = did\n  }\n\n  setIsMe(isMe: boolean) {\n    this.isMe = isMe\n  }\n\n  addHidden(hidden: boolean) {\n    if (hidden) {\n      this.causes.push({\n        type: 'hidden',\n        source: { type: 'user' },\n        priority: 6,\n      })\n    }\n  }\n\n  addMutedWord(matches: MuteWordMatch[] | undefined) {\n    if (matches?.length) {\n      this.causes.push({\n        type: 'mute-word',\n        source: { type: 'user' },\n        priority: 6,\n        matches,\n      })\n    }\n  }\n\n  addBlocking(blocking: string | undefined) {\n    if (blocking) {\n      this.causes.push({\n        type: 'blocking',\n        source: { type: 'user' },\n        priority: 3,\n      })\n    }\n  }\n\n  addBlockingByList(\n    blockingByList: AppBskyGraphDefs.ListViewBasic | undefined,\n  ) {\n    if (blockingByList) {\n      this.causes.push({\n        type: 'blocking',\n        source: { type: 'list', list: blockingByList },\n        priority: 3,\n      })\n    }\n  }\n\n  addBlockedBy(blockedBy: boolean | undefined) {\n    if (blockedBy) {\n      this.causes.push({\n        type: 'blocked-by',\n        source: { type: 'user' },\n        priority: 4,\n      })\n    }\n  }\n\n  addBlockOther(blockOther: boolean | undefined) {\n    if (blockOther) {\n      this.causes.push({\n        type: 'block-other',\n        source: { type: 'user' },\n        priority: 4,\n      })\n    }\n  }\n\n  addLabel(target: LabelTarget, label: Label, opts: ModerationOpts) {\n    // look up the label definition\n    const labelDef = CUSTOM_LABEL_VALUE_RE.test(label.val)\n      ? opts.labelDefs?.[label.src]?.find(\n          (def) => def.identifier === label.val,\n        ) || LABELS[label.val]\n      : LABELS[label.val]\n    if (!labelDef) {\n      // ignore labels we don't understand\n      return\n    }\n\n    // look up the label preference\n    const isSelf = label.src === this.did\n    const labeler = isSelf\n      ? undefined\n      : opts.prefs.labelers.find((s) => s.did === label.src)\n\n    if (!isSelf && !labeler) {\n      return // skip labelers not configured by the user\n    }\n    if (isSelf && labelDef.flags.includes('no-self')) {\n      return // skip self-labels that aren't supported\n    }\n\n    // establish the label preference for interpretation\n    let labelPref: LabelPreference = labelDef.defaultSetting || 'ignore'\n    if (!labelDef.configurable) {\n      labelPref = labelDef.defaultSetting || 'hide'\n    } else if (\n      labelDef.flags.includes('adult') &&\n      !opts.prefs.adultContentEnabled\n    ) {\n      labelPref = 'hide'\n    } else if (labeler?.labels[labelDef.identifier]) {\n      labelPref = labeler?.labels[labelDef.identifier]\n    } else if (opts.prefs.labels[labelDef.identifier]) {\n      labelPref = opts.prefs.labels[labelDef.identifier]\n    }\n\n    // ignore labels the user has asked to ignore\n    if (labelPref === 'ignore') {\n      return\n    }\n\n    // ignore 'unauthed' labels when the user is authed\n    if (labelDef.flags.includes('unauthed') && !!opts.userDid) {\n      return\n    }\n\n    // establish the priority of the label\n    let priority: 1 | 2 | 5 | 7 | 8\n    const severity = measureModerationBehaviorSeverity(\n      labelDef.behaviors[target],\n    )\n    if (\n      labelDef.flags.includes('no-override') ||\n      (labelDef.flags.includes('adult') && !opts.prefs.adultContentEnabled)\n    ) {\n      priority = 1\n    } else if (labelPref === 'hide') {\n      priority = 2\n    } else if (severity === ModerationBehaviorSeverity.High) {\n      // blurring profile view or content view\n      priority = 5\n    } else if (severity === ModerationBehaviorSeverity.Medium) {\n      // blurring content list or content media\n      priority = 7\n    } else {\n      // blurring avatar, adding alerts\n      priority = 8\n    }\n\n    let noOverride = false\n    if (labelDef.flags.includes('no-override')) {\n      noOverride = true\n    } else if (\n      labelDef.flags.includes('adult') &&\n      !opts.prefs.adultContentEnabled\n    ) {\n      noOverride = true\n    }\n\n    this.causes.push({\n      type: 'label',\n      source:\n        isSelf || !labeler\n          ? { type: 'user' }\n          : { type: 'labeler', did: labeler.did },\n      label,\n      labelDef,\n      target,\n      setting: labelPref,\n      behavior: labelDef.behaviors[target] || NOOP_BEHAVIOR,\n      noOverride,\n      priority,\n    })\n  }\n\n  addMuted(muted: boolean | undefined) {\n    if (muted) {\n      this.causes.push({\n        type: 'muted',\n        source: { type: 'user' },\n        priority: 6,\n      })\n    }\n  }\n\n  addMutedByList(mutedByList: AppBskyGraphDefs.ListViewBasic | undefined) {\n    if (mutedByList) {\n      this.causes.push({\n        type: 'muted',\n        source: { type: 'list', list: mutedByList },\n        priority: 6,\n      })\n    }\n  }\n}\n\nfunction measureModerationBehaviorSeverity(\n  beh: ModerationBehavior | undefined,\n): ModerationBehaviorSeverity {\n  if (!beh) {\n    return ModerationBehaviorSeverity.Low\n  }\n  if (beh.profileView === 'blur' || beh.contentView === 'blur') {\n    return ModerationBehaviorSeverity.High\n  }\n  if (beh.contentList === 'blur' || beh.contentMedia === 'blur') {\n    return ModerationBehaviorSeverity.Medium\n  }\n  return ModerationBehaviorSeverity.Low\n}\n\nfunction sortByPriority(a: ModerationCause, b: ModerationCause) {\n  return a.priority - b.priority\n}\n"
  },
  {
    "path": "packages/api/src/moderation/index.ts",
    "content": "import { ModerationDecision } from './decision'\nimport { decideAccount } from './subjects/account'\nimport { decideFeedGenerator } from './subjects/feed-generator'\nimport { decideNotification } from './subjects/notification'\nimport { decidePost } from './subjects/post'\nimport { decideProfile } from './subjects/profile'\nimport { decideUserList } from './subjects/user-list'\nimport {\n  ModerationOpts,\n  ModerationSubjectFeedGenerator,\n  ModerationSubjectNotification,\n  ModerationSubjectPost,\n  ModerationSubjectProfile,\n  ModerationSubjectUserList,\n} from './types'\n\nexport { ModerationUI } from './ui'\nexport { ModerationDecision } from './decision'\nexport { hasMutedWord, matchMuteWords } from './mutewords'\nexport {\n  interpretLabelValueDefinition,\n  interpretLabelValueDefinitions,\n} from './util'\n\nexport function moderateProfile(\n  subject: ModerationSubjectProfile,\n  opts: ModerationOpts,\n): ModerationDecision {\n  return ModerationDecision.merge(\n    decideAccount(subject, opts),\n    decideProfile(subject, opts),\n  )\n}\n\nexport function moderatePost(\n  subject: ModerationSubjectPost,\n  opts: ModerationOpts,\n): ModerationDecision {\n  return decidePost(subject, opts)\n}\n\nexport function moderateNotification(\n  subject: ModerationSubjectNotification,\n  opts: ModerationOpts,\n): ModerationDecision {\n  return decideNotification(subject, opts)\n}\n\nexport function moderateFeedGenerator(\n  subject: ModerationSubjectFeedGenerator,\n  opts: ModerationOpts,\n): ModerationDecision {\n  return decideFeedGenerator(subject, opts)\n}\n\nexport function moderateUserList(\n  subject: ModerationSubjectUserList,\n  opts: ModerationOpts,\n): ModerationDecision {\n  return decideUserList(subject, opts)\n}\n"
  },
  {
    "path": "packages/api/src/moderation/mutewords.ts",
    "content": "import { AppBskyActorDefs, AppBskyRichtextFacet } from '../client'\n\nconst REGEX = {\n  LEADING_TRAILING_PUNCTUATION: /(?:^\\p{P}+|\\p{P}+$)/gu,\n  ESCAPE: /[[\\]{}()*+?.\\\\^$|\\s]/g,\n  SEPARATORS: /[/\\-–—()[\\]_]+/g,\n  WORD_BOUNDARY: /[\\s\\n\\t\\r\\f\\v]+?/g,\n}\n\n/**\n * List of 2-letter lang codes for languages that either don't use spaces, or\n * don't use spaces in a way conducive to word-based filtering.\n *\n * For these, we use a simple `String.includes` to check for a match.\n */\nconst LANGUAGE_EXCEPTIONS = [\n  'ja', // Japanese\n  'zh', // Chinese\n  'ko', // Korean\n  'th', // Thai\n  'vi', // Vietnamese\n]\n\nexport type MuteWordMatch = {\n  /**\n   * The `AppBskyActorDefs.MutedWord` that matched.\n   */\n  word: AppBskyActorDefs.MutedWord\n  /**\n   * The string that matched the muted word.\n   */\n  predicate: string\n}\n\nexport type Params = {\n  mutedWords: AppBskyActorDefs.MutedWord[]\n  text: string\n  facets?: AppBskyRichtextFacet.Main[]\n  outlineTags?: string[]\n  languages?: string[]\n  actor?: AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewBasic\n}\n\n/**\n * Checks if the given text matches any of the muted words, returning an array\n * of matches. If no matches are found, returns `undefined`.\n */\nexport function matchMuteWords({\n  mutedWords,\n  text,\n  facets,\n  outlineTags,\n  languages,\n  actor,\n}: Params): MuteWordMatch[] | undefined {\n  const exception = LANGUAGE_EXCEPTIONS.includes(languages?.[0] || '')\n  const tags = ([] as string[])\n    .concat(outlineTags || [])\n    .concat(\n      (facets || []).flatMap((facet) =>\n        facet.features.filter(AppBskyRichtextFacet.isTag).map((tag) => tag.tag),\n      ),\n    )\n    .map((t) => t.toLowerCase())\n\n  const matches: MuteWordMatch[] = []\n\n  outer: for (const muteWord of mutedWords) {\n    const mutedWord = muteWord.value.toLowerCase()\n    const postText = text.toLowerCase()\n\n    // expired, ignore\n    if (muteWord.expiresAt && muteWord.expiresAt < new Date().toISOString())\n      continue\n\n    if (\n      muteWord.actorTarget === 'exclude-following' &&\n      Boolean(actor?.viewer?.following)\n    )\n      continue\n\n    // `content` applies to tags as well\n    if (tags.includes(mutedWord)) {\n      matches.push({ word: muteWord, predicate: muteWord.value })\n      continue\n    }\n    // rest of the checks are for `content` only\n    if (!muteWord.targets.includes('content')) continue\n    // single character or other exception, has to use includes\n    if ((mutedWord.length === 1 || exception) && postText.includes(mutedWord)) {\n      matches.push({ word: muteWord, predicate: muteWord.value })\n      continue\n    }\n    // too long\n    if (mutedWord.length > postText.length) continue\n    // exact match\n    if (mutedWord === postText) {\n      matches.push({ word: muteWord, predicate: muteWord.value })\n      continue\n    }\n    // any muted phrase with space or punctuation\n    if (/(?:\\s|\\p{P})+?/u.test(mutedWord) && postText.includes(mutedWord)) {\n      matches.push({ word: muteWord, predicate: muteWord.value })\n      continue\n    }\n\n    // check individual character groups\n    const words = postText.split(REGEX.WORD_BOUNDARY)\n    for (const word of words) {\n      if (word === mutedWord) {\n        matches.push({ word: muteWord, predicate: word })\n        continue outer\n      }\n\n      // compare word without leading/trailing punctuation, but allow internal\n      // punctuation (such as `s@ssy`)\n      const wordTrimmedPunctuation = word.replace(\n        REGEX.LEADING_TRAILING_PUNCTUATION,\n        '',\n      )\n\n      if (mutedWord === wordTrimmedPunctuation) {\n        matches.push({ word: muteWord, predicate: word })\n        continue outer\n      }\n\n      if (mutedWord.length > wordTrimmedPunctuation.length) continue\n\n      if (/\\p{P}+/u.test(wordTrimmedPunctuation)) {\n        /**\n         * Exit case for any punctuation within the predicate that we _do_\n         * allow e.g. `and/or` should not match `Andor`.\n         */\n        if (/[/]+/.test(wordTrimmedPunctuation)) {\n          continue outer\n        }\n\n        const spacedWord = wordTrimmedPunctuation.replace(/\\p{P}+/gu, ' ')\n        if (spacedWord === mutedWord) {\n          matches.push({ word: muteWord, predicate: word })\n          continue outer\n        }\n\n        const contiguousWord = spacedWord.replace(/\\s/gu, '')\n        if (contiguousWord === mutedWord) {\n          matches.push({ word: muteWord, predicate: word })\n          continue outer\n        }\n\n        const wordParts = wordTrimmedPunctuation.split(/\\p{P}+/u)\n        for (const wordPart of wordParts) {\n          if (wordPart === mutedWord) {\n            matches.push({ word: muteWord, predicate: word })\n            continue outer\n          }\n        }\n      }\n    }\n  }\n\n  return matches.length ? matches : undefined\n}\n\n/**\n * Checks if the given text matches any of the muted words, returning a boolean\n * if any matches are found.\n */\nexport function hasMutedWord(params: Params) {\n  return !!matchMuteWords(params)\n}\n"
  },
  {
    "path": "packages/api/src/moderation/subjects/account.ts",
    "content": "import { ModerationDecision } from '../decision'\nimport { Label, ModerationOpts, ModerationSubjectProfile } from '../types'\n\nexport function decideAccount(\n  subject: ModerationSubjectProfile,\n  opts: ModerationOpts,\n): ModerationDecision {\n  const acc = new ModerationDecision()\n\n  acc.setDid(subject.did)\n  acc.setIsMe(subject.did === opts.userDid)\n  if (subject.viewer?.muted) {\n    if (subject.viewer?.mutedByList) {\n      acc.addMutedByList(subject.viewer?.mutedByList)\n    } else {\n      acc.addMuted(subject.viewer?.muted)\n    }\n  }\n  if (subject.viewer?.blocking) {\n    if (subject.viewer?.blockingByList) {\n      acc.addBlockingByList(subject.viewer?.blockingByList)\n    } else {\n      acc.addBlocking(subject.viewer?.blocking)\n    }\n  }\n  acc.addBlockedBy(subject.viewer?.blockedBy)\n\n  for (const label of filterAccountLabels(subject.labels)) {\n    acc.addLabel('account', label, opts)\n  }\n\n  return acc\n}\n\nexport function filterAccountLabels(labels?: Label[]): Label[] {\n  if (!labels) {\n    return []\n  }\n  return labels.filter(\n    (label) =>\n      !label.uri.endsWith('/app.bsky.actor.profile/self') ||\n      label.val === '!no-unauthenticated',\n  )\n}\n"
  },
  {
    "path": "packages/api/src/moderation/subjects/feed-generator.ts",
    "content": "import { ModerationDecision } from '../decision'\nimport { ModerationOpts, ModerationSubjectFeedGenerator } from '../types'\nimport { decideAccount } from './account'\nimport { decideProfile } from './profile'\n\nexport function decideFeedGenerator(\n  subject: ModerationSubjectFeedGenerator,\n  opts: ModerationOpts,\n): ModerationDecision {\n  const acc = new ModerationDecision()\n\n  acc.setDid(subject.creator.did)\n  acc.setIsMe(subject.creator.did === opts.userDid)\n  if (subject.labels?.length) {\n    for (const label of subject.labels) {\n      acc.addLabel('content', label, opts)\n    }\n  }\n  return ModerationDecision.merge(\n    acc,\n    decideAccount(subject.creator, opts),\n    decideProfile(subject.creator, opts),\n  )\n}\n"
  },
  {
    "path": "packages/api/src/moderation/subjects/notification.ts",
    "content": "import { ModerationDecision } from '../decision'\nimport { ModerationOpts, ModerationSubjectNotification } from '../types'\nimport { decideAccount } from './account'\nimport { decideProfile } from './profile'\n\nexport function decideNotification(\n  subject: ModerationSubjectNotification,\n  opts: ModerationOpts,\n): ModerationDecision {\n  const acc = new ModerationDecision()\n\n  acc.setDid(subject.author.did)\n  acc.setIsMe(subject.author.did === opts.userDid)\n  if (subject.labels?.length) {\n    for (const label of subject.labels) {\n      acc.addLabel('content', label, opts)\n    }\n  }\n\n  return ModerationDecision.merge(\n    acc,\n    decideAccount(subject.author, opts),\n    decideProfile(subject.author, opts),\n  )\n}\n"
  },
  {
    "path": "packages/api/src/moderation/subjects/post.ts",
    "content": "import {\n  AppBskyActorDefs,\n  AppBskyEmbedExternal,\n  AppBskyEmbedImages,\n  AppBskyEmbedRecord,\n  AppBskyEmbedRecordWithMedia,\n  AppBskyFeedPost,\n} from '../../client'\nimport { $Typed } from '../../client/util'\nimport { ModerationDecision } from '../decision'\nimport { MuteWordMatch, matchMuteWords } from '../mutewords'\nimport { ModerationOpts, ModerationSubjectPost } from '../types'\nimport { decideAccount } from './account'\nimport { decideProfile } from './profile'\n\nexport function decidePost(\n  subject: ModerationSubjectPost,\n  opts: ModerationOpts,\n): ModerationDecision {\n  return ModerationDecision.merge(\n    decideSubject(subject, opts),\n    decideEmbed(subject.embed, opts)?.downgrade(),\n    decideAccount(subject.author, opts),\n    decideProfile(subject.author, opts),\n  )\n}\n\nfunction decideSubject(\n  subject: ModerationSubjectPost,\n  opts: ModerationOpts,\n): ModerationDecision {\n  const acc = new ModerationDecision()\n\n  acc.setDid(subject.author.did)\n  acc.setIsMe(subject.author.did === opts.userDid)\n  if (subject.labels?.length) {\n    for (const label of subject.labels) {\n      acc.addLabel('content', label, opts)\n    }\n  }\n  acc.addHidden(checkHiddenPost(subject, opts.prefs.hiddenPosts))\n  if (!acc.isMe) {\n    acc.addMutedWord(matchAllMuteWords(subject, opts.prefs.mutedWords))\n  }\n\n  return acc\n}\n\nfunction decideEmbed(\n  embed:\n    | undefined\n    | $Typed<AppBskyEmbedRecord.View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string },\n  opts: ModerationOpts,\n) {\n  if (embed) {\n    if (\n      (AppBskyEmbedRecord.isView(embed) ||\n        AppBskyEmbedRecordWithMedia.isView(embed)) &&\n      AppBskyEmbedRecord.isViewRecord(embed.record)\n    ) {\n      // quote post\n      return decideQuotedPost(embed.record, opts)\n    } else if (\n      AppBskyEmbedRecordWithMedia.isView(embed) &&\n      AppBskyEmbedRecord.isViewRecord(embed.record.record)\n    ) {\n      // quoted post with media\n      return decideQuotedPost(embed.record.record, opts)\n    } else if (\n      (AppBskyEmbedRecord.isView(embed) ||\n        AppBskyEmbedRecordWithMedia.isView(embed)) &&\n      AppBskyEmbedRecord.isViewBlocked(embed.record)\n    ) {\n      // blocked quote post\n      return decideBlockedQuotedPost(embed.record, opts)\n    } else if (\n      AppBskyEmbedRecordWithMedia.isView(embed) &&\n      AppBskyEmbedRecord.isViewBlocked(embed.record.record)\n    ) {\n      // blocked quoted post with media\n      return decideBlockedQuotedPost(embed.record.record, opts)\n    }\n  }\n\n  return undefined\n}\n\nfunction decideQuotedPost(\n  subject: AppBskyEmbedRecord.ViewRecord,\n  opts: ModerationOpts,\n) {\n  const acc = new ModerationDecision()\n  acc.setDid(subject.author.did)\n  acc.setIsMe(subject.author.did === opts.userDid)\n  if (subject.labels?.length) {\n    for (const label of subject.labels) {\n      acc.addLabel('content', label, opts)\n    }\n  }\n  return ModerationDecision.merge(\n    acc,\n    decideAccount(subject.author, opts),\n    decideProfile(subject.author, opts),\n  )\n}\n\nfunction decideBlockedQuotedPost(\n  subject: AppBskyEmbedRecord.ViewBlocked,\n  opts: ModerationOpts,\n) {\n  const acc = new ModerationDecision()\n  acc.setDid(subject.author.did)\n  acc.setIsMe(subject.author.did === opts.userDid)\n  if (subject.author.viewer?.muted) {\n    if (subject.author.viewer?.mutedByList) {\n      acc.addMutedByList(subject.author.viewer?.mutedByList)\n    } else {\n      acc.addMuted(subject.author.viewer?.muted)\n    }\n  }\n  if (subject.author.viewer?.blocking) {\n    if (subject.author.viewer?.blockingByList) {\n      acc.addBlockingByList(subject.author.viewer?.blockingByList)\n    } else {\n      acc.addBlocking(subject.author.viewer?.blocking)\n    }\n  }\n  acc.addBlockedBy(subject.author.viewer?.blockedBy)\n  return acc\n}\n\nfunction checkHiddenPost(\n  subject: ModerationSubjectPost,\n  hiddenPosts: string[] | undefined,\n) {\n  if (!hiddenPosts?.length) {\n    return false\n  }\n  if (hiddenPosts.includes(subject.uri)) {\n    return true\n  }\n  if (subject.embed) {\n    if (\n      AppBskyEmbedRecord.isView(subject.embed) &&\n      AppBskyEmbedRecord.isViewRecord(subject.embed.record) &&\n      hiddenPosts.includes(subject.embed.record.uri)\n    ) {\n      return true\n    }\n    if (\n      AppBskyEmbedRecordWithMedia.isView(subject.embed) &&\n      AppBskyEmbedRecord.isViewRecord(subject.embed.record.record) &&\n      hiddenPosts.includes(subject.embed.record.record.uri)\n    ) {\n      return true\n    }\n  }\n  return false\n}\n\nfunction matchAllMuteWords(\n  subject: ModerationSubjectPost,\n  mutedWords: AppBskyActorDefs.MutedWord[] | undefined,\n): MuteWordMatch[] | undefined {\n  if (!mutedWords?.length) {\n    return\n  }\n\n  const postAuthor = subject.author\n\n  if (AppBskyFeedPost.isRecord(subject.record)) {\n    const post = subject.record as AppBskyFeedPost.Record\n\n    const matches = matchMuteWords({\n      mutedWords,\n      text: post.text,\n      facets: post.facets,\n      outlineTags: post.tags,\n      languages: post.langs,\n      actor: postAuthor,\n    })\n    // post text\n    if (matches) {\n      return matches\n    }\n\n    if (post.embed && AppBskyEmbedImages.isMain(post.embed)) {\n      // post images\n      for (const image of post.embed.images) {\n        const matches = matchMuteWords({\n          mutedWords,\n          text: image.alt,\n          languages: post.langs,\n          actor: postAuthor,\n        })\n        if (matches) {\n          return matches\n        }\n      }\n    }\n  }\n\n  const { embed } = subject\n  if (embed) {\n    // quote post\n    if (\n      (AppBskyEmbedRecord.isView(embed) ||\n        AppBskyEmbedRecordWithMedia.isView(embed)) &&\n      AppBskyEmbedRecord.isViewRecord(embed.record)\n    ) {\n      if (AppBskyFeedPost.isRecord(embed.record.value)) {\n        const embeddedPost = embed.record.value as AppBskyFeedPost.Record\n        const embedAuthor = embed.record.author\n        const matches = matchMuteWords({\n          mutedWords,\n          text: embeddedPost.text,\n          facets: embeddedPost.facets,\n          outlineTags: embeddedPost.tags,\n          languages: embeddedPost.langs,\n          actor: embedAuthor,\n        })\n\n        // quoted post text\n        if (matches) {\n          return matches\n        }\n\n        // quoted post's images\n        if (AppBskyEmbedImages.isMain(embeddedPost.embed)) {\n          for (const image of embeddedPost.embed.images) {\n            const matches = matchMuteWords({\n              mutedWords,\n              text: image.alt,\n              languages: embeddedPost.langs,\n              actor: embedAuthor,\n            })\n            if (matches) {\n              return matches\n            }\n          }\n        }\n\n        // quoted post's link card\n        if (AppBskyEmbedExternal.isMain(embeddedPost.embed)) {\n          const { external } = embeddedPost.embed\n          const matches = matchMuteWords({\n            mutedWords,\n            text: external.title + ' ' + external.description,\n            languages: [],\n            actor: embedAuthor,\n          })\n          if (matches) {\n            return matches\n          }\n        }\n\n        if (AppBskyEmbedRecordWithMedia.isMain(embeddedPost.embed)) {\n          // quoted post's link card when it did a quote + media\n          if (AppBskyEmbedExternal.isMain(embeddedPost.embed.media)) {\n            const { external } = embeddedPost.embed.media\n            const matches = matchMuteWords({\n              mutedWords,\n              text: external.title + ' ' + external.description,\n              languages: [],\n              actor: embedAuthor,\n            })\n            if (matches) {\n              return matches\n            }\n          }\n\n          // quoted post's images when it did a quote + media\n          if (AppBskyEmbedImages.isMain(embeddedPost.embed.media)) {\n            for (const image of embeddedPost.embed.media.images) {\n              const matches = matchMuteWords({\n                mutedWords,\n                text: image.alt,\n                languages: AppBskyFeedPost.isRecord(embeddedPost.record)\n                  ? embeddedPost.langs\n                  : [],\n                actor: embedAuthor,\n              })\n              if (matches) {\n                return matches\n              }\n            }\n          }\n        }\n      }\n    }\n    // link card\n    else if (AppBskyEmbedExternal.isView(embed)) {\n      const { external } = embed\n      const matches = matchMuteWords({\n        mutedWords,\n        text: external.title + ' ' + external.description,\n        languages: [],\n        actor: postAuthor,\n      })\n      if (matches) {\n        return matches\n      }\n    }\n    // quote post with media\n    else if (\n      AppBskyEmbedRecordWithMedia.isView(embed) &&\n      AppBskyEmbedRecord.isViewRecord(embed.record.record)\n    ) {\n      const embedAuthor = embed.record.record.author\n\n      // quoted post text\n      if (AppBskyFeedPost.isRecord(embed.record.record.value)) {\n        const post = embed.record.record.value as AppBskyFeedPost.Record\n        const matches = matchMuteWords({\n          mutedWords,\n          text: post.text,\n          facets: post.facets,\n          outlineTags: post.tags,\n          languages: post.langs,\n          actor: embedAuthor,\n        })\n        if (matches) {\n          return matches\n        }\n      }\n\n      // quoted post images\n      if (AppBskyEmbedImages.isView(embed.media)) {\n        for (const image of embed.media.images) {\n          const matches = matchMuteWords({\n            mutedWords,\n            text: image.alt,\n            languages: AppBskyFeedPost.isRecord(subject.record)\n              ? (subject.record as AppBskyFeedPost.Record).langs\n              : [],\n            actor: embedAuthor,\n          })\n          if (matches) {\n            return matches\n          }\n        }\n      }\n\n      if (AppBskyEmbedExternal.isView(embed.media)) {\n        const { external } = embed.media\n        const matches = matchMuteWords({\n          mutedWords,\n          text: external.title + ' ' + external.description,\n          languages: [],\n          actor: embedAuthor,\n        })\n        if (matches) {\n          return matches\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/api/src/moderation/subjects/profile.ts",
    "content": "import { ModerationDecision } from '../decision'\nimport { Label, ModerationOpts, ModerationSubjectProfile } from '../types'\n\nexport function decideProfile(\n  subject: ModerationSubjectProfile,\n  opts: ModerationOpts,\n): ModerationDecision {\n  const acc = new ModerationDecision()\n\n  acc.setDid(subject.did)\n  acc.setIsMe(subject.did === opts.userDid)\n  for (const label of filterProfileLabels(subject.labels)) {\n    acc.addLabel('profile', label, opts)\n  }\n\n  return acc\n}\n\nexport function filterProfileLabels(labels?: Label[]): Label[] {\n  if (!labels) {\n    return []\n  }\n  return labels.filter((label) =>\n    label.uri.endsWith('/app.bsky.actor.profile/self'),\n  )\n}\n"
  },
  {
    "path": "packages/api/src/moderation/subjects/user-list.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { AppBskyActorDefs } from '../../client/index'\nimport { ModerationDecision } from '../decision'\nimport { ModerationOpts, ModerationSubjectUserList } from '../types'\nimport { decideAccount } from './account'\nimport { decideProfile } from './profile'\n\nexport function decideUserList(\n  subject: ModerationSubjectUserList,\n  opts: ModerationOpts,\n): ModerationDecision {\n  const acc = new ModerationDecision()\n\n  const creator =\n    // Note: ListViewBasic should not contain a creator field, but let's support it anyway\n    'creator' in subject && isProfile(subject.creator)\n      ? subject.creator\n      : undefined\n\n  if (creator) {\n    acc.setDid(creator.did)\n    acc.setIsMe(creator.did === opts.userDid)\n    if (subject.labels?.length) {\n      for (const label of subject.labels) {\n        acc.addLabel('content', label, opts)\n      }\n    }\n    return ModerationDecision.merge(\n      acc,\n      decideAccount(creator, opts),\n      decideProfile(creator, opts),\n    )\n  }\n\n  const creatorDid = new AtUri(subject.uri).hostname\n  acc.setDid(creatorDid)\n  acc.setIsMe(creatorDid === opts.userDid)\n  if (subject.labels?.length) {\n    for (const label of subject.labels) {\n      acc.addLabel('content', label, opts)\n    }\n  }\n  return acc\n}\n\nfunction isProfile(v: any): v is AppBskyActorDefs.ProfileViewBasic {\n  return v && typeof v === 'object' && 'did' in v\n}\n"
  },
  {
    "path": "packages/api/src/moderation/types.ts",
    "content": "import {\n  AppBskyActorDefs,\n  AppBskyFeedDefs,\n  AppBskyGraphDefs,\n  AppBskyNotificationListNotifications,\n  ChatBskyActorDefs,\n  ComAtprotoLabelDefs,\n} from '../client/index'\nimport { KnownLabelValue } from './const/labels'\nimport { MuteWordMatch } from './mutewords'\n\n// syntax\n// =\n\nexport const CUSTOM_LABEL_VALUE_RE = /^[a-z-]+$/\n\n// behaviors\n// =\n\nexport interface ModerationBehavior {\n  profileList?: 'blur' | 'alert' | 'inform'\n  profileView?: 'blur' | 'alert' | 'inform'\n  avatar?: 'blur' | 'alert'\n  banner?: 'blur'\n  displayName?: 'blur'\n  contentList?: 'blur' | 'alert' | 'inform'\n  contentView?: 'blur' | 'alert' | 'inform'\n  contentMedia?: 'blur'\n}\nexport const BLOCK_BEHAVIOR: ModerationBehavior = {\n  profileList: 'blur',\n  profileView: 'alert',\n  avatar: 'blur',\n  banner: 'blur',\n  contentList: 'blur',\n  contentView: 'blur',\n}\nexport const MUTE_BEHAVIOR: ModerationBehavior = {\n  profileList: 'inform',\n  profileView: 'alert',\n  contentList: 'blur',\n  contentView: 'inform',\n}\nexport const MUTEWORD_BEHAVIOR: ModerationBehavior = {\n  contentList: 'blur',\n  contentView: 'blur',\n}\nexport const HIDE_BEHAVIOR: ModerationBehavior = {\n  contentList: 'blur',\n  contentView: 'blur',\n}\nexport const NOOP_BEHAVIOR: ModerationBehavior = {}\n\n// labels\n// =\n\nexport type Label = ComAtprotoLabelDefs.Label\nexport type LabelTarget = 'account' | 'profile' | 'content'\nexport type LabelPreference = 'ignore' | 'warn' | 'hide'\n\nexport type LabelValueDefinitionFlag =\n  | 'no-override'\n  | 'adult'\n  | 'unauthed'\n  | 'no-self'\n\nexport interface InterpretedLabelValueDefinition\n  extends ComAtprotoLabelDefs.LabelValueDefinition {\n  definedBy?: string | undefined // did of labeler or undefined for global\n  configurable: boolean\n  defaultSetting: LabelPreference // type narrowing\n  flags: LabelValueDefinitionFlag[]\n  behaviors: {\n    account?: ModerationBehavior\n    profile?: ModerationBehavior\n    content?: ModerationBehavior\n  }\n}\n\nexport type LabelDefinitionMap = Record<\n  KnownLabelValue,\n  InterpretedLabelValueDefinition\n>\n\n// subjects\n// =\n\nexport type ModerationSubjectProfile =\n  | AppBskyActorDefs.ProfileViewBasic\n  | AppBskyActorDefs.ProfileView\n  | AppBskyActorDefs.ProfileViewDetailed\n  | ChatBskyActorDefs.ProfileViewBasic\n\nexport type ModerationSubjectPost = AppBskyFeedDefs.PostView\n\nexport type ModerationSubjectNotification =\n  AppBskyNotificationListNotifications.Notification\n\nexport type ModerationSubjectFeedGenerator = AppBskyFeedDefs.GeneratorView\n\nexport type ModerationSubjectUserList =\n  | AppBskyGraphDefs.ListViewBasic\n  | AppBskyGraphDefs.ListView\n\nexport type ModerationSubject =\n  | ModerationSubjectProfile\n  | ModerationSubjectPost\n  | ModerationSubjectNotification\n  | ModerationSubjectFeedGenerator\n  | ModerationSubjectUserList\n\n// behaviors\n// =\n\nexport type ModerationCauseSource =\n  | { type: 'user' }\n  | { type: 'list'; list: AppBskyGraphDefs.ListViewBasic }\n  | { type: 'labeler'; did: string }\n\nexport type ModerationCause =\n  | {\n      type: 'blocking'\n      source: ModerationCauseSource\n      priority: 3\n      downgraded?: boolean\n    }\n  | {\n      type: 'blocked-by'\n      source: ModerationCauseSource\n      priority: 4\n      downgraded?: boolean\n    }\n  | {\n      type: 'block-other'\n      source: ModerationCauseSource\n      priority: 4\n      downgraded?: boolean\n    }\n  | {\n      type: 'label'\n      source: ModerationCauseSource\n      label: Label\n      labelDef: InterpretedLabelValueDefinition\n      target: LabelTarget\n      setting: LabelPreference\n      behavior: ModerationBehavior\n      noOverride: boolean\n      priority: 1 | 2 | 5 | 7 | 8\n      downgraded?: boolean\n    }\n  | {\n      type: 'muted'\n      source: ModerationCauseSource\n      priority: 6\n      downgraded?: boolean\n    }\n  | {\n      type: 'mute-word'\n      source: ModerationCauseSource\n      priority: 6\n      downgraded?: boolean\n      matches: MuteWordMatch[]\n    }\n  | {\n      type: 'hidden'\n      source: ModerationCauseSource\n      priority: 6\n      downgraded?: boolean\n    }\n\nexport interface ModerationPrefsLabeler {\n  did: string\n  labels: Record<string, LabelPreference>\n}\n\nexport interface ModerationPrefs {\n  adultContentEnabled: boolean\n  labels: Record<string, LabelPreference>\n  labelers: ModerationPrefsLabeler[]\n  mutedWords: AppBskyActorDefs.MutedWord[]\n  hiddenPosts: string[]\n}\n\nexport interface ModerationOpts {\n  userDid: string | undefined\n  prefs: ModerationPrefs\n  /**\n   * Map of labeler did -> custom definitions\n   */\n  labelDefs?: Record<string, InterpretedLabelValueDefinition[]>\n}\n"
  },
  {
    "path": "packages/api/src/moderation/ui.ts",
    "content": "import { ModerationCause } from './types'\n\nexport class ModerationUI {\n  noOverride = false\n  filters: ModerationCause[] = []\n  blurs: ModerationCause[] = []\n  alerts: ModerationCause[] = []\n  informs: ModerationCause[] = []\n  get filter(): boolean {\n    return this.filters.length !== 0\n  }\n  get blur(): boolean {\n    return this.blurs.length !== 0\n  }\n  get alert(): boolean {\n    return this.alerts.length !== 0\n  }\n  get inform(): boolean {\n    return this.informs.length !== 0\n  }\n}\n"
  },
  {
    "path": "packages/api/src/moderation/util.ts",
    "content": "import {\n  AppBskyEmbedRecord,\n  AppBskyEmbedRecordWithMedia,\n  AppBskyLabelerDefs,\n  ComAtprotoLabelDefs,\n} from '../client'\nimport { asPredicate } from '../client/util'\nimport {\n  InterpretedLabelValueDefinition,\n  LabelPreference,\n  LabelValueDefinitionFlag,\n  ModerationBehavior,\n} from './types'\n\nexport function isQuotedPost(embed: unknown): embed is AppBskyEmbedRecord.View {\n  return Boolean(embed && AppBskyEmbedRecord.isView(embed))\n}\n\nexport function isQuotedPostWithMedia(\n  embed: unknown,\n): embed is AppBskyEmbedRecordWithMedia.View {\n  return Boolean(embed && AppBskyEmbedRecordWithMedia.isView(embed))\n}\n\nexport function interpretLabelValueDefinition(\n  def: ComAtprotoLabelDefs.LabelValueDefinition,\n  definedBy: string | undefined,\n): InterpretedLabelValueDefinition {\n  const behaviors: {\n    account: ModerationBehavior\n    profile: ModerationBehavior\n    content: ModerationBehavior\n  } = {\n    account: {},\n    profile: {},\n    content: {},\n  }\n  const alertOrInform: 'alert' | 'inform' | undefined =\n    def.severity === 'alert'\n      ? 'alert'\n      : def.severity === 'inform'\n        ? 'inform'\n        : undefined\n  if (def.blurs === 'content') {\n    // target=account, blurs=content\n    behaviors.account.profileList = alertOrInform\n    behaviors.account.profileView = alertOrInform\n    behaviors.account.contentList = 'blur'\n    behaviors.account.contentView = def.adultOnly ? 'blur' : alertOrInform\n    // target=profile, blurs=content\n    behaviors.profile.profileList = alertOrInform\n    behaviors.profile.profileView = alertOrInform\n    // target=content, blurs=content\n    behaviors.content.contentList = 'blur'\n    behaviors.content.contentView = def.adultOnly ? 'blur' : alertOrInform\n  } else if (def.blurs === 'media') {\n    // target=account, blurs=media\n    behaviors.account.profileList = alertOrInform\n    behaviors.account.profileView = alertOrInform\n    behaviors.account.avatar = 'blur'\n    behaviors.account.banner = 'blur'\n    // target=profile, blurs=media\n    behaviors.profile.profileList = alertOrInform\n    behaviors.profile.profileView = alertOrInform\n    behaviors.profile.avatar = 'blur'\n    behaviors.profile.banner = 'blur'\n    // target=content, blurs=media\n    behaviors.content.contentMedia = 'blur'\n  } else if (def.blurs === 'none') {\n    // target=account, blurs=none\n    behaviors.account.profileList = alertOrInform\n    behaviors.account.profileView = alertOrInform\n    behaviors.account.contentList = alertOrInform\n    behaviors.account.contentView = alertOrInform\n    // target=profile, blurs=none\n    behaviors.profile.profileList = alertOrInform\n    behaviors.profile.profileView = alertOrInform\n    // target=content, blurs=none\n    behaviors.content.contentList = alertOrInform\n    behaviors.content.contentView = alertOrInform\n  }\n\n  let defaultSetting: LabelPreference = 'warn'\n  if (def.defaultSetting === 'hide' || def.defaultSetting === 'ignore') {\n    defaultSetting = def.defaultSetting as LabelPreference\n  }\n\n  const flags: LabelValueDefinitionFlag[] = ['no-self']\n  if (def.adultOnly) {\n    flags.push('adult')\n  }\n\n  return {\n    ...def,\n    definedBy,\n    configurable: true,\n    defaultSetting,\n    flags,\n    behaviors,\n  }\n}\n\nexport function interpretLabelValueDefinitions(\n  labelerView: AppBskyLabelerDefs.LabelerViewDetailed,\n): InterpretedLabelValueDefinition[] {\n  return (labelerView.policies?.labelValueDefinitions || [])\n    .filter(asPredicate(ComAtprotoLabelDefs.validateLabelValueDefinition))\n    .map((labelValDef) =>\n      interpretLabelValueDefinition(labelValDef, labelerView.creator.did),\n    )\n}\n"
  },
  {
    "path": "packages/api/src/predicate.ts",
    "content": "import { AppBskyActorDefs, AppBskyActorProfile } from './client/index'\nimport { asPredicate } from './client/util'\n\nexport const isValidProfile = asPredicate(AppBskyActorProfile.validateRecord)\nexport const isValidAdultContentPref = asPredicate(\n  AppBskyActorDefs.validateAdultContentPref,\n)\nexport const isValidBskyAppStatePref = asPredicate(\n  AppBskyActorDefs.validateBskyAppStatePref,\n)\nexport const isValidContentLabelPref = asPredicate(\n  AppBskyActorDefs.validateContentLabelPref,\n)\nexport const isValidFeedViewPref = asPredicate(\n  AppBskyActorDefs.validateFeedViewPref,\n)\nexport const isValidHiddenPostsPref = asPredicate(\n  AppBskyActorDefs.validateHiddenPostsPref,\n)\nexport const isValidInterestsPref = asPredicate(\n  AppBskyActorDefs.validateInterestsPref,\n)\nexport const isValidLabelersPref = asPredicate(\n  AppBskyActorDefs.validateLabelersPref,\n)\nexport const isValidMutedWordsPref = asPredicate(\n  AppBskyActorDefs.validateMutedWordsPref,\n)\nexport const isValidPersonalDetailsPref = asPredicate(\n  AppBskyActorDefs.validatePersonalDetailsPref,\n)\nexport const isValidDeclaredAgePref = asPredicate(\n  AppBskyActorDefs.validateDeclaredAgePref,\n)\nexport const isValidPostInteractionSettingsPref = asPredicate(\n  AppBskyActorDefs.validatePostInteractionSettingsPref,\n)\nexport const isValidSavedFeedsPref = asPredicate(\n  AppBskyActorDefs.validateSavedFeedsPref,\n)\nexport const isValidSavedFeedsPrefV2 = asPredicate(\n  AppBskyActorDefs.validateSavedFeedsPrefV2,\n)\nexport const isValidThreadViewPref = asPredicate(\n  AppBskyActorDefs.validateThreadViewPref,\n)\nexport const isValidVerificationPrefs = asPredicate(\n  AppBskyActorDefs.validateVerificationPrefs,\n)\nexport const isValidLiveEventPreferences = asPredicate(\n  AppBskyActorDefs.validateLiveEventPreferences,\n)\n"
  },
  {
    "path": "packages/api/src/rich-text/detection.ts",
    "content": "import TLDs from 'tlds'\nimport { AppBskyRichtextFacet } from '../client'\nimport { UnicodeString } from './unicode'\nimport {\n  CASHTAG_REGEX,\n  MENTION_REGEX,\n  TAG_REGEX,\n  TRAILING_PUNCTUATION_REGEX,\n  URL_REGEX,\n} from './util'\n\nexport type Facet = AppBskyRichtextFacet.Main\n\nexport function detectFacets(text: UnicodeString): Facet[] | undefined {\n  let match\n  const facets: Facet[] = []\n  {\n    // mentions\n    const re = MENTION_REGEX\n    while ((match = re.exec(text.utf16))) {\n      if (!isValidDomain(match[3]) && !match[3].endsWith('.test')) {\n        continue // probably not a handle\n      }\n\n      const start = text.utf16.indexOf(match[3], match.index) - 1\n      facets.push({\n        $type: 'app.bsky.richtext.facet',\n        index: {\n          byteStart: text.utf16IndexToUtf8Index(start),\n          byteEnd: text.utf16IndexToUtf8Index(start + match[3].length + 1),\n        },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#mention',\n            did: match[3], // must be resolved afterwards\n          },\n        ],\n      })\n    }\n  }\n  {\n    // links\n    const re = URL_REGEX\n    while ((match = re.exec(text.utf16))) {\n      let uri = match[2]\n      if (!uri.startsWith('http')) {\n        const domain = match.groups?.domain\n        if (!domain || !isValidDomain(domain)) {\n          continue\n        }\n        uri = `https://${uri}`\n      }\n      const start = text.utf16.indexOf(match[2], match.index)\n      const index = { start, end: start + match[2].length }\n      // strip ending puncuation\n      if (/[.,;:!?]$/.test(uri)) {\n        uri = uri.slice(0, -1)\n        index.end--\n      }\n      if (/[)]$/.test(uri) && !uri.includes('(')) {\n        uri = uri.slice(0, -1)\n        index.end--\n      }\n      facets.push({\n        index: {\n          byteStart: text.utf16IndexToUtf8Index(index.start),\n          byteEnd: text.utf16IndexToUtf8Index(index.end),\n        },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#link',\n            uri,\n          },\n        ],\n      })\n    }\n  }\n  {\n    const re = TAG_REGEX\n    while ((match = re.exec(text.utf16))) {\n      const leading = match[1]\n      let tag = match[2]\n\n      if (!tag) continue\n\n      // strip ending punctuation and any spaces\n      tag = tag.trim().replace(TRAILING_PUNCTUATION_REGEX, '')\n\n      if (tag.length === 0 || tag.length > 64) continue\n\n      const index = match.index + leading.length\n\n      facets.push({\n        index: {\n          byteStart: text.utf16IndexToUtf8Index(index),\n          byteEnd: text.utf16IndexToUtf8Index(index + 1 + tag.length),\n        },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#tag',\n            tag: tag,\n          },\n        ],\n      })\n    }\n  }\n  {\n    // cashtags\n    const re = CASHTAG_REGEX\n    while ((match = re.exec(text.utf16))) {\n      const leading = match[1]\n      let ticker = match[2]\n\n      if (!ticker) continue\n\n      // Normalize to uppercase\n      ticker = ticker.toUpperCase()\n\n      const index = match.index + leading.length\n\n      facets.push({\n        index: {\n          byteStart: text.utf16IndexToUtf8Index(index),\n          byteEnd: text.utf16IndexToUtf8Index(index + 1 + ticker.length), // +1 for $\n        },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#tag',\n            tag: '$' + ticker, // Store with $ prefix\n          },\n        ],\n      })\n    }\n  }\n  return facets.length > 0 ? facets : undefined\n}\n\nfunction isValidDomain(str: string): boolean {\n  return !!TLDs.find((tld) => {\n    const i = str.lastIndexOf(tld)\n    if (i === -1) {\n      return false\n    }\n    return str.charAt(i - 1) === '.' && i === str.length - tld.length\n  })\n}\n"
  },
  {
    "path": "packages/api/src/rich-text/rich-text.ts",
    "content": "/*\n= Rich Text Manipulation\n\nWhen we sanitize rich text, we have to update the entity indices as the\ntext is modified. This can be modeled as inserts() and deletes() of the\nrich text string. The possible scenarios are outlined below, along with\ntheir expected behaviors.\n\nNOTE: Slices are start inclusive, end exclusive\n\n== richTextInsert()\n\nTarget string:\n\n   0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l l o   w o r l d   // string value\n       ^-------^           // target slice {start: 2, end: 7}\n\nScenarios:\n\nA: ^                       // insert \"test\" at 0\nB:        ^                // insert \"test\" at 4\nC:                 ^       // insert \"test\" at 8\n\nA = before           -> move both by num added\nB = inner            -> move end by num added\nC = after            -> noop\n\nResults:\n\nA: 0 1 2 3 4 5 6 7 8 910   // string indices\n   t e s t h e l l o   w   // string value\n               ^-------^   // target slice {start: 6, end: 11}\n\nB: 0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l l t e s t o   w   // string value\n       ^---------------^   // target slice {start: 2, end: 11}\n\nC: 0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l l o   w o t e s   // string value\n       ^-------^           // target slice {start: 2, end: 7}\n\n== richTextDelete()\n\nTarget string:\n\n   0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l l o   w o r l d   // string value\n       ^-------^           // target slice {start: 2, end: 7}\n\nScenarios:\n\nA: ^---------------^       // remove slice {start: 0, end: 9}\nB:               ^-----^   // remove slice {start: 7, end: 11}\nC:         ^-----------^   // remove slice {start: 4, end: 11}\nD:       ^-^               // remove slice {start: 3, end: 5}\nE:   ^-----^               // remove slice {start: 1, end: 5}\nF: ^-^                     // remove slice {start: 0, end: 2}\n\nA = entirely outer   -> delete slice\nB = entirely after   -> noop\nC = partially after  -> move end to remove-start\nD = entirely inner   -> move end by num removed\nE = partially before -> move start to remove-start index, move end by num removed\nF = entirely before  -> move both by num removed\n\nResults:\n\nA: 0 1 2 3 4 5 6 7 8 910   // string indices\n   l d                     // string value\n                           // target slice (deleted)\n\nB: 0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l l o   w           // string value\n       ^-------^           // target slice {start: 2, end: 7}\n\nC: 0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l l                 // string value\n       ^-^                 // target slice {start: 2, end: 4}\n\nD: 0 1 2 3 4 5 6 7 8 910   // string indices\n   h e l   w o r l d       // string value\n       ^---^               // target slice {start: 2, end: 5}\n\nE: 0 1 2 3 4 5 6 7 8 910   // string indices\n   h   w o r l d           // string value\n     ^-^                   // target slice {start: 1, end: 3}\n\nF: 0 1 2 3 4 5 6 7 8 910   // string indices\n   l l o   w o r l d       // string value\n   ^-------^               // target slice {start: 0, end: 5}\n */\n\nimport { AppBskyFeedPost, AppBskyRichtextFacet, AtpBaseClient } from '../client'\nimport { detectFacets } from './detection'\nimport { sanitizeRichText } from './sanitization'\nimport { UnicodeString } from './unicode'\n\nexport type Facet = AppBskyRichtextFacet.Main\nexport type FacetLink = AppBskyRichtextFacet.Link\nexport type FacetMention = AppBskyRichtextFacet.Mention\nexport type FacetTag = AppBskyRichtextFacet.Tag\nexport type Entity = AppBskyFeedPost.Entity\n\nexport interface RichTextProps {\n  text: string\n  facets?: Facet[]\n  /**\n   * @deprecated Use facets instead\n   */\n  entities?: Entity[]\n}\n\nexport interface RichTextOpts {\n  cleanNewlines?: boolean\n}\n\nexport class RichTextSegment {\n  constructor(\n    public text: string,\n    public facet?: Facet,\n  ) {}\n\n  get link(): FacetLink | undefined {\n    return this.facet?.features.find(AppBskyRichtextFacet.isLink)\n  }\n\n  isLink() {\n    return !!this.link\n  }\n\n  get mention(): FacetMention | undefined {\n    return this.facet?.features.find(AppBskyRichtextFacet.isMention)\n  }\n\n  isMention() {\n    return !!this.mention\n  }\n\n  get tag(): FacetTag | undefined {\n    return this.facet?.features.find(AppBskyRichtextFacet.isTag)\n  }\n\n  isTag() {\n    return !!this.tag\n  }\n}\n\nexport class RichText {\n  unicodeText: UnicodeString\n  facets?: Facet[]\n\n  constructor(props: RichTextProps, opts?: RichTextOpts) {\n    this.unicodeText = new UnicodeString(props.text)\n    this.facets = props.facets\n    if (!this.facets?.length && props.entities?.length) {\n      this.facets = entitiesToFacets(this.unicodeText, props.entities)\n    }\n    if (this.facets) {\n      this.facets = this.facets.filter(facetFilter).sort(facetSort)\n    }\n    if (opts?.cleanNewlines) {\n      sanitizeRichText(this, { cleanNewlines: true }).copyInto(this)\n    }\n  }\n\n  get text() {\n    return this.unicodeText.toString()\n  }\n\n  get length() {\n    return this.unicodeText.length\n  }\n\n  get graphemeLength() {\n    return this.unicodeText.graphemeLength\n  }\n\n  clone() {\n    return new RichText({\n      text: this.unicodeText.utf16,\n      facets: cloneDeep(this.facets),\n    })\n  }\n\n  copyInto(target: RichText) {\n    target.unicodeText = this.unicodeText\n    target.facets = cloneDeep(this.facets)\n  }\n\n  *segments(): Generator<RichTextSegment, void, void> {\n    const facets = this.facets || []\n    if (!facets.length) {\n      yield new RichTextSegment(this.unicodeText.utf16)\n      return\n    }\n\n    let textCursor = 0\n    let facetCursor = 0\n    do {\n      const currFacet = facets[facetCursor]\n      if (textCursor < currFacet.index.byteStart) {\n        yield new RichTextSegment(\n          this.unicodeText.slice(textCursor, currFacet.index.byteStart),\n        )\n      } else if (textCursor > currFacet.index.byteStart) {\n        facetCursor++\n        continue\n      }\n      if (currFacet.index.byteStart < currFacet.index.byteEnd) {\n        const subtext = this.unicodeText.slice(\n          currFacet.index.byteStart,\n          currFacet.index.byteEnd,\n        )\n        if (!subtext.trim()) {\n          // dont empty string entities\n          yield new RichTextSegment(subtext)\n        } else {\n          yield new RichTextSegment(subtext, currFacet)\n        }\n      }\n      textCursor = currFacet.index.byteEnd\n      facetCursor++\n    } while (facetCursor < facets.length)\n    if (textCursor < this.unicodeText.length) {\n      yield new RichTextSegment(\n        this.unicodeText.slice(textCursor, this.unicodeText.length),\n      )\n    }\n  }\n\n  insert(insertIndex: number, insertText: string) {\n    this.unicodeText = new UnicodeString(\n      this.unicodeText.slice(0, insertIndex) +\n        insertText +\n        this.unicodeText.slice(insertIndex),\n    )\n\n    if (!this.facets?.length) {\n      return this\n    }\n\n    const numCharsAdded = insertText.length\n    for (const ent of this.facets) {\n      // see comment at top of file for labels of each scenario\n      // scenario A (before)\n      if (insertIndex <= ent.index.byteStart) {\n        // move both by num added\n        ent.index.byteStart += numCharsAdded\n        ent.index.byteEnd += numCharsAdded\n      }\n      // scenario B (inner)\n      else if (\n        insertIndex >= ent.index.byteStart &&\n        insertIndex < ent.index.byteEnd\n      ) {\n        // move end by num added\n        ent.index.byteEnd += numCharsAdded\n      }\n      // scenario C (after)\n      // noop\n    }\n    return this\n  }\n\n  delete(removeStartIndex: number, removeEndIndex: number) {\n    this.unicodeText = new UnicodeString(\n      this.unicodeText.slice(0, removeStartIndex) +\n        this.unicodeText.slice(removeEndIndex),\n    )\n\n    if (!this.facets?.length) {\n      return this\n    }\n\n    const numCharsRemoved = removeEndIndex - removeStartIndex\n    for (const ent of this.facets) {\n      // see comment at top of file for labels of each scenario\n      // scenario A (entirely outer)\n      if (\n        removeStartIndex <= ent.index.byteStart &&\n        removeEndIndex >= ent.index.byteEnd\n      ) {\n        // delete slice (will get removed in final pass)\n        ent.index.byteStart = 0\n        ent.index.byteEnd = 0\n      }\n      // scenario B (entirely after)\n      else if (removeStartIndex > ent.index.byteEnd) {\n        // noop\n      }\n      // scenario C (partially after)\n      else if (\n        removeStartIndex > ent.index.byteStart &&\n        removeStartIndex <= ent.index.byteEnd &&\n        removeEndIndex > ent.index.byteEnd\n      ) {\n        // move end to remove start\n        ent.index.byteEnd = removeStartIndex\n      }\n      // scenario D (entirely inner)\n      else if (\n        removeStartIndex >= ent.index.byteStart &&\n        removeEndIndex <= ent.index.byteEnd\n      ) {\n        // move end by num removed\n        ent.index.byteEnd -= numCharsRemoved\n      }\n      // scenario E (partially before)\n      else if (\n        removeStartIndex < ent.index.byteStart &&\n        removeEndIndex >= ent.index.byteStart &&\n        removeEndIndex <= ent.index.byteEnd\n      ) {\n        // move start to remove-start index, move end by num removed\n        ent.index.byteStart = removeStartIndex\n        ent.index.byteEnd -= numCharsRemoved\n      }\n      // scenario F (entirely before)\n      else if (removeEndIndex < ent.index.byteStart) {\n        // move both by num removed\n        ent.index.byteStart -= numCharsRemoved\n        ent.index.byteEnd -= numCharsRemoved\n      }\n    }\n\n    // filter out any facets that were made irrelevant\n    this.facets = this.facets.filter(\n      (ent) => ent.index.byteStart < ent.index.byteEnd,\n    )\n    return this\n  }\n\n  /**\n   * Detects facets such as links and mentions\n   * Note: Overwrites the existing facets with auto-detected facets\n   */\n  async detectFacets(agent: AtpBaseClient) {\n    this.facets = detectFacets(this.unicodeText)\n    if (this.facets) {\n      const promises: Promise<void>[] = []\n      for (const facet of this.facets) {\n        for (const feature of facet.features) {\n          if (AppBskyRichtextFacet.isMention(feature)) {\n            promises.push(\n              agent.com.atproto.identity\n                .resolveHandle({ handle: feature.did })\n                .then((res) => res?.data.did)\n                .catch((_) => undefined)\n                .then((did) => {\n                  feature.did = did || ''\n                }),\n            )\n          }\n        }\n      }\n      await Promise.allSettled(promises)\n      this.facets.sort(facetSort)\n    }\n  }\n\n  /**\n   * Detects facets such as links and mentions but does not resolve them\n   * Will produce invalid facets! For instance, mentions will not have their DIDs set.\n   * Note: Overwrites the existing facets with auto-detected facets\n   */\n  detectFacetsWithoutResolution() {\n    this.facets = detectFacets(this.unicodeText)\n    if (this.facets) {\n      this.facets.sort(facetSort)\n    }\n  }\n}\n\nconst facetSort = (a: Facet, b: Facet) => a.index.byteStart - b.index.byteStart\n\nconst facetFilter = (facet: Facet) =>\n  // discard negative-length facets. zero-length facets are valid\n  facet.index.byteStart <= facet.index.byteEnd\n\nfunction entitiesToFacets(text: UnicodeString, entities: Entity[]): Facet[] {\n  const facets: Facet[] = []\n  for (const ent of entities) {\n    if (ent.type === 'link') {\n      facets.push({\n        $type: 'app.bsky.richtext.facet',\n        index: {\n          byteStart: text.utf16IndexToUtf8Index(ent.index.start),\n          byteEnd: text.utf16IndexToUtf8Index(ent.index.end),\n        },\n        features: [{ $type: 'app.bsky.richtext.facet#link', uri: ent.value }],\n      })\n    } else if (ent.type === 'mention') {\n      facets.push({\n        $type: 'app.bsky.richtext.facet',\n        index: {\n          byteStart: text.utf16IndexToUtf8Index(ent.index.start),\n          byteEnd: text.utf16IndexToUtf8Index(ent.index.end),\n        },\n        features: [\n          { $type: 'app.bsky.richtext.facet#mention', did: ent.value },\n        ],\n      })\n    }\n  }\n  return facets\n}\n\nfunction cloneDeep<T>(v: T): T {\n  if (typeof v === 'undefined') {\n    return v\n  }\n  return JSON.parse(JSON.stringify(v))\n}\n"
  },
  {
    "path": "packages/api/src/rich-text/sanitization.ts",
    "content": "import { RichText } from './rich-text'\nimport { UnicodeString } from './unicode'\n\n// this regex is intentionally matching on the zero-with-separator codepoint\n// eslint-disable-next-line no-misleading-character-class\nconst EXCESS_SPACE_RE = /[\\r\\n]([\\u00AD\\u2060\\u200D\\u200C\\u200B\\s]*[\\r\\n]){2,}/\nconst REPLACEMENT_STR = '\\n\\n'\n\nexport function sanitizeRichText(\n  richText: RichText,\n  opts: { cleanNewlines?: boolean },\n) {\n  if (opts.cleanNewlines) {\n    richText = clean(richText, EXCESS_SPACE_RE, REPLACEMENT_STR)\n  }\n  return richText\n}\n\nfunction clean(\n  richText: RichText,\n  targetRegexp: RegExp,\n  replacementString: string,\n): RichText {\n  richText = richText.clone()\n\n  let match = richText.unicodeText.utf16.match(targetRegexp)\n  while (match && typeof match.index !== 'undefined') {\n    const oldText = richText.unicodeText\n    const removeStartIndex = richText.unicodeText.utf16IndexToUtf8Index(\n      match.index,\n    )\n    const removeEndIndex = removeStartIndex + new UnicodeString(match[0]).length\n    richText.delete(removeStartIndex, removeEndIndex)\n    if (richText.unicodeText.utf16 === oldText.utf16) {\n      break // sanity check\n    }\n    richText.insert(removeStartIndex, replacementString)\n    match = richText.unicodeText.utf16.match(targetRegexp)\n  }\n\n  return richText\n}\n"
  },
  {
    "path": "packages/api/src/rich-text/unicode.ts",
    "content": "/**\n * Javascript uses utf16-encoded strings while most environments and specs\n * have standardized around utf8 (including JSON).\n *\n * After some lengthy debated we decided that richtext facets need to use\n * utf8 indices. This means we need tools to convert indices between utf8\n * and utf16, and that's precisely what this library handles.\n */\n\nimport { graphemeLen } from '@atproto/common-web'\n\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder()\n\nexport class UnicodeString {\n  utf16: string\n  utf8: Uint8Array\n  private _graphemeLen?: number | undefined\n\n  constructor(utf16: string) {\n    this.utf16 = utf16\n    this.utf8 = encoder.encode(utf16)\n  }\n\n  get length() {\n    return this.utf8.byteLength\n  }\n\n  get graphemeLength() {\n    if (!this._graphemeLen) {\n      this._graphemeLen = graphemeLen(this.utf16)\n    }\n    return this._graphemeLen\n  }\n\n  slice(start?: number, end?: number): string {\n    return decoder.decode(this.utf8.slice(start, end))\n  }\n\n  utf16IndexToUtf8Index(i: number) {\n    return encoder.encode(this.utf16.slice(0, i)).byteLength\n  }\n\n  toString() {\n    return this.utf16\n  }\n}\n"
  },
  {
    "path": "packages/api/src/rich-text/util.ts",
    "content": "export const MENTION_REGEX = /(^|\\s|\\()(@)([a-zA-Z0-9.-]+)(\\b)/g\nexport const URL_REGEX =\n  /(^|\\s|\\()((https?:\\/\\/[\\S]+)|((?<domain>[a-z][a-z0-9]*(\\.[a-z0-9]+)+)[\\S]*))/gim\nexport const TRAILING_PUNCTUATION_REGEX = /\\p{P}+$/gu\n\n/**\n * `\\ufe0f` emoji modifier\n * `\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2` zero-width spaces (likely incomplete)\n */\nexport const TAG_REGEX =\n  // eslint-disable-next-line no-misleading-character-class\n  /(^|\\s)[#＃]((?!\\ufe0f)[^\\s\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2]*[^\\d\\s\\p{P}\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2]+[^\\s\\u00AD\\u2060\\u200A\\u200B\\u200C\\u200D\\u20e2]*)?/gu\n\nexport const CASHTAG_REGEX =\n  /(^|\\s|\\()\\$([A-Za-z][A-Za-z0-9]{0,4})(?=\\s|$|[.,;:!?)\"'\\u2019])/gu\n"
  },
  {
    "path": "packages/api/src/session-manager.ts",
    "content": "import { FetchHandlerObject } from '@atproto/xrpc'\n\nexport interface SessionManager extends FetchHandlerObject {\n  readonly did?: string\n}\n"
  },
  {
    "path": "packages/api/src/types.ts",
    "content": "import { AppBskyActorDefs } from './client'\nimport { ModerationPrefs } from './moderation/types'\n\nexport type UnknownServiceType = string & NonNullable<unknown>\nexport type AtprotoServiceType = 'atproto_labeler' | UnknownServiceType\nexport function isAtprotoServiceType<T extends string>(\n  input: T,\n): input is T & AtprotoServiceType {\n  return !input.includes(' ') && !input.includes('#')\n}\n\n// @TODO use tools from @atproto/did\nexport type Did = `did:${string}:${string}`\nexport function isDid<T extends string>(input: T): input is T & Did {\n  if (!input.startsWith('did:')) return false\n  if (input.length < 8) return false\n  if (input.length > 2048) return false\n  const msidx = input.indexOf(':', 4)\n  return msidx > 4 && msidx < input.length - 1\n}\n\nexport function assertDid(input: string): asserts input is Did {\n  if (!isDid(input)) throw new TypeError(`Invalid DID: ${input}`)\n}\n\nexport function asDid<T extends string>(input: T) {\n  assertDid(input)\n  return input\n}\n\nexport type AtprotoProxy = `${Did}#${AtprotoServiceType}`\nexport function isAtprotoProxy(input: string): input is AtprotoProxy {\n  const { length, [0]: did, [1]: service } = input.split('#')\n  return length === 2 && isDid(did) && isAtprotoServiceType(service)\n}\n\nexport function assertAtprotoProxy(\n  input: string,\n): asserts input is AtprotoProxy {\n  if (!isAtprotoProxy(input)) {\n    throw new TypeError(\n      `Invalid DID reference: ${input} (must be of the form did:example:alice#service)`,\n    )\n  }\n}\n\nexport function asAtprotoProxy<T extends string>(input: T) {\n  assertAtprotoProxy(input)\n  return input\n}\n\n/**\n * Used by the PersistSessionHandler to indicate what change occurred\n */\nexport type AtpSessionEvent =\n  | 'create'\n  | 'create-failed'\n  | 'update'\n  | 'expired'\n  | 'network-error'\n\n/**\n * Used by AtpAgent to store active sessions\n */\nexport interface AtpSessionData {\n  refreshJwt: string\n  accessJwt: string\n  handle: string\n  did: string\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active: boolean\n  status?: string\n}\n\n/**\n * Handler signature passed to AtpAgent to store session data\n */\nexport type AtpPersistSessionHandler = (\n  evt: AtpSessionEvent,\n  session: AtpSessionData | undefined,\n) => void | Promise<void>\n\n/**\n * AtpAgent login() opts\n */\nexport interface AtpAgentLoginOpts {\n  identifier: string\n  password: string\n  authFactorToken?: string | undefined\n  allowTakendown?: boolean\n}\n\n/**\n * AtpAgent global config opts\n */\nexport interface AtpAgentGlobalOpts {\n  appLabelers?: string[]\n}\n\n/**\n * Bluesky feed view preferences\n */\n\nexport interface BskyFeedViewPreference {\n  hideReplies: boolean\n  hideRepliesByUnfollowed: boolean\n  hideRepliesByLikeCount: number\n  hideReposts: boolean\n  hideQuotePosts: boolean\n  [key: string]: any\n}\n\n/**\n * Bluesky thread view preferences\n */\nexport interface BskyThreadViewPreference {\n  sort: string\n  [key: string]: any\n}\n\n/**\n * Bluesky interests preferences\n */\nexport interface BskyInterestsPreference {\n  tags: string[]\n  [key: string]: any\n}\n\n/**\n * Bluesky preferences\n */\nexport interface BskyPreferences {\n  /**\n   * @deprecated use `savedFeeds`\n   */\n  feeds: {\n    saved?: string[]\n    pinned?: string[]\n  }\n  savedFeeds: AppBskyActorDefs.SavedFeed[]\n  feedViewPrefs: Record<string, BskyFeedViewPreference>\n  threadViewPrefs: BskyThreadViewPreference\n  moderationPrefs: ModerationPrefs\n  birthDate: Date | undefined\n  /**\n   * Read-only preference containing value(s) inferred from the user's declared\n   * birthdate. Absence of this preference object in the response indicates\n   * that the user has not made a declaration.\n   */\n  declaredAge?: AppBskyActorDefs.DeclaredAgePref\n  interests: BskyInterestsPreference\n  bskyAppState: {\n    queuedNudges: string[]\n    activeProgressGuide: AppBskyActorDefs.BskyAppProgressGuide | undefined\n    nuxs: AppBskyActorDefs.Nux[]\n  }\n  postInteractionSettings: AppBskyActorDefs.PostInteractionSettingsPref\n  verificationPrefs: AppBskyActorDefs.VerificationPrefs\n  liveEventPreferences: {\n    hiddenFeedIds: string[]\n    hideAllFeeds: boolean\n  }\n}\n"
  },
  {
    "path": "packages/api/src/util.ts",
    "content": "import { z } from 'zod'\nimport { AtUri } from '@atproto/syntax'\nimport { AppBskyActorDefs } from './client'\nimport { Nux } from './client/types/app/bsky/actor/defs'\n\nexport function sanitizeMutedWordValue(value: string) {\n  return (\n    value\n      .trim()\n      .replace(/^#(?!\\ufe0f)/, '')\n      // eslint-disable-next-line no-misleading-character-class\n      .replace(/[\\r\\n\\u00AD\\u2060\\u200D\\u200C\\u200B]+/, '')\n  )\n}\n\nexport function savedFeedsToUriArrays(\n  savedFeeds: AppBskyActorDefs.SavedFeed[],\n): {\n  pinned: string[]\n  saved: string[]\n} {\n  const pinned: string[] = []\n  const saved: string[] = []\n\n  for (const feed of savedFeeds) {\n    if (feed.pinned) {\n      pinned.push(feed.value)\n      // saved in v1 includes pinned\n      saved.push(feed.value)\n    } else {\n      saved.push(feed.value)\n    }\n  }\n\n  return {\n    pinned,\n    saved,\n  }\n}\n\n/**\n * Get the type of a saved feed, used by deprecated methods for backwards\n * compat. Should not be used moving forward. *Invalid URIs will throw.*\n *\n * @param uri - The AT URI of the saved feed\n */\nexport function getSavedFeedType(\n  uri: string,\n): AppBskyActorDefs.SavedFeed['type'] {\n  const urip = new AtUri(uri)\n\n  switch (urip.collection) {\n    case 'app.bsky.feed.generator':\n      return 'feed'\n    case 'app.bsky.graph.list':\n      return 'list'\n    default:\n      return 'unknown'\n  }\n}\n\nexport function validateSavedFeed(savedFeed: AppBskyActorDefs.SavedFeed) {\n  if (!savedFeed.id) {\n    throw new Error('Saved feed must have an `id` - use a TID')\n  }\n\n  if (['feed', 'list'].includes(savedFeed.type)) {\n    const uri = new AtUri(savedFeed.value)\n    const isFeed = uri.collection === 'app.bsky.feed.generator'\n    const isList = uri.collection === 'app.bsky.graph.list'\n\n    if (savedFeed.type === 'feed' && !isFeed) {\n      throw new Error(\n        `Saved feed of type 'feed' must be a feed, got ${uri.collection}`,\n      )\n    }\n    if (savedFeed.type === 'list' && !isList) {\n      throw new Error(\n        `Saved feed of type 'list' must be a list, got ${uri.collection}`,\n      )\n    }\n  }\n}\n\nexport const nuxSchema = z\n  .object({\n    id: z.string().max(64),\n    completed: z.boolean(),\n    data: z.string().max(300).optional(),\n    expiresAt: z.string().datetime().optional(),\n  })\n  .strict()\n\nexport function validateNux(nux: Nux) {\n  nuxSchema.parse(nux)\n}\n"
  },
  {
    "path": "packages/api/tests/atp-agent.test.ts",
    "content": "import { TID } from '@atproto/common-web'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport {\n  AppBskyActorDefs,\n  AppBskyActorProfile,\n  AtpAgent,\n  ComAtprotoRepoPutRecord,\n  DEFAULT_LABEL_SETTINGS,\n} from '../src'\nimport { asPredicate } from '../src/client/util'\nimport {\n  getSavedFeedType,\n  savedFeedsToUriArrays,\n  validateSavedFeed,\n} from '../src/util'\n\ndescribe('agent', () => {\n  let network: TestNetworkNoAppView\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'api_atp_agent',\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getProfileDisplayName = async (\n    agent: AtpAgent,\n  ): Promise<string | undefined> => {\n    try {\n      const res = await agent.app.bsky.actor.profile.get({\n        repo: agent.accountDid,\n        rkey: 'self',\n      })\n      return res.value.displayName ?? ''\n    } catch (err) {\n      return undefined\n    }\n  }\n\n  it('clones correctly', () => {\n    const agent = new AtpAgent({ service: network.pds.url })\n    const agent2 = agent.clone()\n    expect(agent2 instanceof AtpAgent).toBeTruthy()\n    expect(agent.service).toEqual(agent2.service)\n  })\n\n  it('upsertProfile correctly creates and updates profiles.', async () => {\n    const agent = new AtpAgent({ service: network.pds.url })\n\n    await agent.createAccount({\n      handle: 'user1.test',\n      email: 'user1@test.com',\n      password: 'password',\n    })\n    const displayName1 = await getProfileDisplayName(agent)\n    expect(displayName1).toBeFalsy()\n\n    await agent.upsertProfile((existing) => {\n      expect(existing).toBeFalsy()\n      return {\n        displayName: 'Bob',\n      }\n    })\n\n    const displayName2 = await getProfileDisplayName(agent)\n    expect(displayName2).toBe('Bob')\n\n    await agent.upsertProfile((existing) => {\n      expect(existing).toBeTruthy()\n      return {\n        displayName: existing?.displayName?.toUpperCase(),\n      }\n    })\n\n    const displayName3 = await getProfileDisplayName(agent)\n    expect(displayName3).toBe('BOB')\n  })\n\n  it('upsertProfile correctly handles CAS failures.', async () => {\n    const agent = new AtpAgent({ service: network.pds.url })\n    await agent.createAccount({\n      handle: 'user2.test',\n      email: 'user2@test.com',\n      password: 'password',\n    })\n\n    const displayName1 = await getProfileDisplayName(agent)\n    expect(displayName1).toBeFalsy()\n\n    let hasConflicted = false\n    let ranTwice = false\n    await agent.upsertProfile(async (_existing) => {\n      if (!hasConflicted) {\n        await agent.com.atproto.repo.putRecord({\n          repo: agent.accountDid,\n          collection: 'app.bsky.actor.profile',\n          rkey: 'self',\n          record: {\n            $type: 'app.bsky.actor.profile',\n            displayName: String(Math.random()),\n          },\n        })\n        hasConflicted = true\n      } else {\n        ranTwice = true\n      }\n      return {\n        displayName: 'Bob',\n      }\n    })\n    expect(ranTwice).toBe(true)\n\n    const displayName2 = await getProfileDisplayName(agent)\n    expect(displayName2).toBe('Bob')\n  })\n\n  it('upsertProfile wont endlessly retry CAS failures.', async () => {\n    const agent = new AtpAgent({ service: network.pds.url })\n    await agent.createAccount({\n      handle: 'user3.test',\n      email: 'user3@test.com',\n      password: 'password',\n    })\n\n    const displayName1 = await getProfileDisplayName(agent)\n    expect(displayName1).toBeFalsy()\n\n    const p = agent.upsertProfile(async (_existing) => {\n      await agent.com.atproto.repo.putRecord({\n        repo: agent.accountDid,\n        collection: 'app.bsky.actor.profile',\n        rkey: 'self',\n        record: {\n          $type: 'app.bsky.actor.profile',\n          displayName: String(Math.random()),\n        },\n      })\n      return {\n        displayName: 'Bob',\n      }\n    })\n    await expect(p).rejects.toThrow(ComAtprotoRepoPutRecord.InvalidSwapError)\n  })\n\n  it('upsertProfile validates the record.', async () => {\n    const agent = new AtpAgent({ service: network.pds.url })\n    await agent.createAccount({\n      handle: 'user4.test',\n      email: 'user4@test.com',\n      password: 'password',\n    })\n\n    const p = agent.upsertProfile((_existing) => {\n      return {\n        displayName: { string: 'Bob' },\n      } as unknown as AppBskyActorProfile.Record\n    })\n    await expect(p).rejects.toThrow('Record/displayName must be a string')\n  })\n\n  describe('app', () => {\n    it('should retrieve the api app', () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      expect(agent.api).toBe(agent)\n      expect(agent.app).toBeDefined()\n    })\n  })\n\n  describe('post', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.post({ text: 'foo' })).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('deletePost', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.deletePost('foo')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('like', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.like('foo', 'bar')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('deleteLike', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.deleteLike('foo')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('repost', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.repost('foo', 'bar')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('deleteRepost', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.deleteRepost('foo')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('follow', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.follow('foo')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('deleteFollow', () => {\n    it('should throw if no session', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await expect(agent.deleteFollow('foo')).rejects.toThrow('Not logged in')\n    })\n  })\n\n  describe('preferences methods', () => {\n    it('gets and sets preferences correctly', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n      await agent.createAccount({\n        handle: 'user5.test',\n        email: 'user5@test.com',\n        password: 'password',\n      })\n\n      const DEFAULT_LABELERS = AtpAgent.appLabelers.map((did) => ({\n        did,\n        labels: {},\n      }))\n\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: { pinned: undefined, saved: undefined },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: DEFAULT_LABEL_SETTINGS,\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setAdultContentEnabled(true)\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: { pinned: undefined, saved: undefined },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: true,\n          labels: DEFAULT_LABEL_SETTINGS,\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setAdultContentEnabled(false)\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: { pinned: undefined, saved: undefined },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: DEFAULT_LABEL_SETTINGS,\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setContentLabelPref('misinfo', 'hide')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: { pinned: undefined, saved: undefined },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: { ...DEFAULT_LABEL_SETTINGS, misinfo: 'hide' },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setContentLabelPref('spam', 'ignore')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: { pinned: undefined, saved: undefined },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: [],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: [\n            'at://bob.com/app.bsky.feed.generator/fake',\n            'at://bob.com/app.bsky.feed.generator/fake2',\n          ],\n          saved: [\n            'at://bob.com/app.bsky.feed.generator/fake',\n            'at://bob.com/app.bsky.feed.generator/fake2',\n          ],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: undefined,\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setFeedViewPrefs('home', { hideReplies: true })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setFeedViewPrefs('home', { hideReplies: false })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setFeedViewPrefs('other', { hideReplies: true })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n          other: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'hotness',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setThreadViewPrefs({ sort: 'random' })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n          other: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'random',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setThreadViewPrefs({ sort: 'oldest' })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n          other: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'oldest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setInterestsPref({ tags: ['foo', 'bar'] })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake2'],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            misinfo: 'hide',\n            spam: 'ignore',\n          },\n          labelers: DEFAULT_LABELERS,\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n          other: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'oldest',\n        },\n        interests: {\n          tags: ['foo', 'bar'],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: [],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n    })\n\n    it('resolves duplicates correctly', async () => {\n      const agent = new AtpAgent({ service: network.pds.url })\n\n      await agent.createAccount({\n        handle: 'user6.test',\n        email: 'user6@test.com',\n        password: 'password',\n      })\n\n      await agent.app.bsky.actor.putPreferences({\n        preferences: [\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'porn',\n            visibility: 'show',\n          },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'porn',\n            visibility: 'hide',\n          },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'porn',\n            visibility: 'show',\n          },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'porn',\n            visibility: 'warn',\n          },\n          {\n            $type: 'app.bsky.actor.defs#labelersPref',\n            labelers: [\n              {\n                did: 'did:plc:first-labeler',\n              },\n            ],\n          },\n          {\n            $type: 'app.bsky.actor.defs#labelersPref',\n            labelers: [\n              {\n                did: 'did:plc:first-labeler',\n              },\n              {\n                did: 'did:plc:other',\n              },\n            ],\n          },\n          {\n            $type: 'app.bsky.actor.defs#adultContentPref',\n            enabled: true,\n          },\n          {\n            $type: 'app.bsky.actor.defs#adultContentPref',\n            enabled: false,\n          },\n          {\n            $type: 'app.bsky.actor.defs#adultContentPref',\n            enabled: true,\n          },\n          {\n            $type: 'app.bsky.actor.defs#savedFeedsPref',\n            pinned: [\n              'at://bob.com/app.bsky.feed.generator/fake',\n              'at://bob.com/app.bsky.feed.generator/fake2',\n            ],\n            saved: [\n              'at://bob.com/app.bsky.feed.generator/fake',\n              'at://bob.com/app.bsky.feed.generator/fake2',\n            ],\n          },\n          {\n            $type: 'app.bsky.actor.defs#savedFeedsPref',\n            pinned: [],\n            saved: [],\n          },\n          {\n            $type: 'app.bsky.actor.defs#personalDetailsPref',\n            birthDate: '2023-09-11T18:05:42.556Z',\n          },\n          {\n            $type: 'app.bsky.actor.defs#personalDetailsPref',\n            birthDate: '2021-09-11T18:05:42.556Z',\n          },\n          {\n            $type: 'app.bsky.actor.defs#declaredAgePref',\n            isOverAge13: false,\n            isOverAge16: false,\n            isOverAge18: false,\n          },\n          {\n            $type: 'app.bsky.actor.defs#feedViewPref',\n            feed: 'home',\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n          {\n            $type: 'app.bsky.actor.defs#feedViewPref',\n            feed: 'home',\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n          {\n            $type: 'app.bsky.actor.defs#threadViewPref',\n            sort: 'oldest',\n          },\n          {\n            $type: 'app.bsky.actor.defs#threadViewPref',\n            sort: 'newest',\n          },\n          {\n            $type: 'app.bsky.actor.defs#bskyAppStatePref',\n            queuedNudges: ['one'],\n          },\n          {\n            $type: 'app.bsky.actor.defs#bskyAppStatePref',\n            activeProgressGuide: undefined,\n            queuedNudges: ['two'],\n          },\n        ],\n      })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: [],\n        },\n        moderationPrefs: {\n          adultContentEnabled: true,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            porn: 'warn',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n            {\n              did: 'did:plc:other',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2021-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'newest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setAdultContentEnabled(false)\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: [],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            porn: 'warn',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n            {\n              did: 'did:plc:other',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2021-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'newest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setContentLabelPref('porn', 'ignore')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: [],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            nsfw: 'ignore',\n            porn: 'ignore',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n            {\n              did: 'did:plc:other',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2021-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'newest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.removeLabeler('did:plc:other')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ],\n        feeds: {\n          pinned: [],\n          saved: [],\n        },\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            nsfw: 'ignore',\n            porn: 'ignore',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2021-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'newest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            nsfw: 'ignore',\n            porn: 'ignore',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2021-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'newest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            nsfw: 'ignore',\n            porn: 'ignore',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: true,\n            hideRepliesByUnfollowed: false,\n            hideRepliesByLikeCount: 10,\n            hideReposts: true,\n            hideQuotePosts: true,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'newest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      await agent.setFeedViewPrefs('home', {\n        hideReplies: false,\n        hideRepliesByUnfollowed: true,\n        hideRepliesByLikeCount: 0,\n        hideReposts: false,\n        hideQuotePosts: false,\n      })\n      await agent.setThreadViewPrefs({\n        sort: 'oldest',\n      })\n      await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })\n      await agent.bskyAppQueueNudges('three')\n      await expect(agent.getPreferences()).resolves.toStrictEqual({\n        feeds: {\n          pinned: ['at://bob.com/app.bsky.feed.generator/fake'],\n          saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n        },\n        savedFeeds: [\n          {\n            id: expect.any(String),\n            pinned: true,\n            type: 'timeline',\n            value: 'following',\n          },\n        ],\n        moderationPrefs: {\n          adultContentEnabled: false,\n          labels: {\n            ...DEFAULT_LABEL_SETTINGS,\n            nsfw: 'ignore',\n            porn: 'ignore',\n          },\n          labelers: [\n            ...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),\n            {\n              did: 'did:plc:first-labeler',\n              labels: {},\n            },\n          ],\n          mutedWords: [],\n          hiddenPosts: [],\n        },\n        birthDate: new Date('2023-09-11T18:05:42.556Z'),\n        declaredAge: {\n          isOverAge13: false,\n          isOverAge16: false,\n          isOverAge18: false,\n        },\n        feedViewPrefs: {\n          home: {\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n        },\n        threadViewPrefs: {\n          sort: 'oldest',\n        },\n        interests: {\n          tags: [],\n        },\n        bskyAppState: {\n          activeProgressGuide: undefined,\n          queuedNudges: ['two', 'three'],\n          nuxs: [],\n        },\n        postInteractionSettings: {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        },\n        verificationPrefs: {\n          hideBadges: false,\n        },\n        liveEventPreferences: {\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        },\n      })\n\n      const res = await agent.app.bsky.actor.getPreferences()\n      expect(res.data.preferences.sort(byType)).toStrictEqual(\n        [\n          {\n            $type: 'app.bsky.actor.defs#bskyAppStatePref',\n            queuedNudges: ['two', 'three'],\n          },\n          {\n            $type: 'app.bsky.actor.defs#adultContentPref',\n            enabled: false,\n          },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'porn',\n            visibility: 'ignore',\n          },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'nsfw',\n            visibility: 'ignore',\n          },\n          {\n            $type: 'app.bsky.actor.defs#labelersPref',\n            labelers: [\n              {\n                did: 'did:plc:first-labeler',\n              },\n            ],\n          },\n          {\n            $type: 'app.bsky.actor.defs#savedFeedsPref',\n            pinned: ['at://bob.com/app.bsky.feed.generator/fake'],\n            saved: ['at://bob.com/app.bsky.feed.generator/fake'],\n          },\n          {\n            $type: 'app.bsky.actor.defs#savedFeedsPrefV2',\n            items: [\n              {\n                id: expect.any(String),\n                pinned: true,\n                type: 'timeline',\n                value: 'following',\n              },\n            ],\n          },\n          {\n            $type: 'app.bsky.actor.defs#personalDetailsPref',\n            birthDate: '2023-09-11T18:05:42.556Z',\n          },\n          {\n            $type: 'app.bsky.actor.defs#declaredAgePref',\n            isOverAge13: false,\n            isOverAge16: false,\n            isOverAge18: false,\n          },\n          {\n            $type: 'app.bsky.actor.defs#feedViewPref',\n            feed: 'home',\n            hideReplies: false,\n            hideRepliesByUnfollowed: true,\n            hideRepliesByLikeCount: 0,\n            hideReposts: false,\n            hideQuotePosts: false,\n          },\n          {\n            $type: 'app.bsky.actor.defs#threadViewPref',\n            sort: 'oldest',\n          },\n        ].sort(byType),\n      )\n    })\n\n    describe('muted words', () => {\n      let agent: AtpAgent\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n        await agent.createAccount({\n          handle: 'user7.test',\n          email: 'user7@test.com',\n          password: 'password',\n        })\n      })\n\n      afterEach(async () => {\n        const { moderationPrefs } = await agent.getPreferences()\n        await agent.removeMutedWords(moderationPrefs.mutedWords)\n      })\n\n      describe('addMutedWord', () => {\n        it('inserts', async () => {\n          const expiresAt = new Date(Date.now() + 6e3).toISOString()\n          await agent.addMutedWord({\n            value: 'word',\n            targets: ['content'],\n            actorTarget: 'all',\n            expiresAt,\n          })\n\n          const { moderationPrefs } = await agent.getPreferences()\n          const word = moderationPrefs.mutedWords.find(\n            (m) => m.value === 'word',\n          )\n\n          expect(word!.id).toBeTruthy()\n          expect(word!.targets).toEqual(['content'])\n          expect(word!.actorTarget).toEqual('all')\n          expect(word!.expiresAt).toEqual(expiresAt)\n        })\n\n        it('single-hash #, no insert', async () => {\n          await agent.addMutedWord({\n            value: '#',\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n\n          // sanitized to empty string, not inserted\n          expect(moderationPrefs.mutedWords.length).toEqual(0)\n        })\n\n        it('multi-hash ##, inserts #', async () => {\n          await agent.addMutedWord({\n            value: '##',\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n          expect(\n            moderationPrefs.mutedWords.find((m) => m.value === '#'),\n          ).toBeTruthy()\n        })\n\n        it('multi-hash ##hashtag, inserts #hashtag', async () => {\n          await agent.addMutedWord({\n            value: '##hashtag',\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n          expect(\n            moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'),\n          ).toBeTruthy()\n        })\n\n        it('hash emoji #️⃣, inserts #️⃣', async () => {\n          await agent.addMutedWord({\n            value: '#️⃣',\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n          expect(\n            moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),\n          ).toBeTruthy()\n        })\n\n        it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => {\n          await agent.addMutedWord({\n            value: '##️⃣',\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n          expect(\n            moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),\n          ).toBeTruthy()\n        })\n\n        it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => {\n          await agent.addMutedWord({\n            value: '###️⃣',\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n          expect(\n            moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'),\n          ).toBeTruthy()\n        })\n\n        it(`includes apostrophes e.g. Bluesky's`, async () => {\n          await agent.addMutedWord({\n            value: `Bluesky's`,\n            targets: [],\n            actorTarget: 'all',\n          })\n          const { mutedWords } = (await agent.getPreferences()).moderationPrefs\n\n          expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy()\n        })\n\n        describe(`invalid characters`, () => {\n          it('#<zws>, no insert', async () => {\n            await agent.addMutedWord({\n              value: '#​',\n              targets: [],\n              actorTarget: 'all',\n            })\n            const { moderationPrefs } = await agent.getPreferences()\n            expect(moderationPrefs.mutedWords.length).toEqual(0)\n          })\n\n          it('#<zws>ab, inserts ab', async () => {\n            await agent.addMutedWord({\n              value: '#​ab',\n              targets: [],\n              actorTarget: 'all',\n            })\n            const { moderationPrefs } = await agent.getPreferences()\n            expect(moderationPrefs.mutedWords.length).toEqual(1)\n          })\n\n          it('phrase with newline, inserts phrase without newline', async () => {\n            await agent.addMutedWord({\n              value: 'test value\\n with newline',\n              targets: [],\n              actorTarget: 'all',\n            })\n            const { moderationPrefs } = await agent.getPreferences()\n            expect(\n              moderationPrefs.mutedWords.find(\n                (m) => m.value === 'test value with newline',\n              ),\n            ).toBeTruthy()\n          })\n\n          it('phrase with newlines, inserts phrase without newlines', async () => {\n            await agent.addMutedWord({\n              value: 'test value\\n\\r with newline',\n              targets: [],\n              actorTarget: 'all',\n            })\n            const { moderationPrefs } = await agent.getPreferences()\n            expect(\n              moderationPrefs.mutedWords.find(\n                (m) => m.value === 'test value with newline',\n              ),\n            ).toBeTruthy()\n          })\n\n          it('empty space, no insert', async () => {\n            await agent.addMutedWord({\n              value: ' ',\n              targets: [],\n              actorTarget: 'all',\n            })\n            const { moderationPrefs } = await agent.getPreferences()\n            expect(moderationPrefs.mutedWords.length).toEqual(0)\n          })\n\n          it(`' trim ', inserts 'trim'`, async () => {\n            await agent.addMutedWord({\n              value: ' trim ',\n              targets: [],\n              actorTarget: 'all',\n            })\n            const { moderationPrefs } = await agent.getPreferences()\n            expect(\n              moderationPrefs.mutedWords.find((m) => m.value === 'trim'),\n            ).toBeTruthy()\n          })\n        })\n      })\n\n      describe('addMutedWords', () => {\n        it('inserts happen sequentially, no clobbering', async () => {\n          await agent.addMutedWords([\n            { value: 'a', targets: ['content'], actorTarget: 'all' },\n            { value: 'b', targets: ['content'], actorTarget: 'all' },\n            { value: 'c', targets: ['content'], actorTarget: 'all' },\n          ])\n\n          const { moderationPrefs } = await agent.getPreferences()\n\n          expect(moderationPrefs.mutedWords.length).toEqual(3)\n        })\n      })\n\n      describe('upsertMutedWords (deprecated)', () => {\n        it('no longer upserts, calls addMutedWords', async () => {\n          await agent.upsertMutedWords([\n            { value: 'both', targets: ['content'], actorTarget: 'all' },\n          ])\n          await agent.upsertMutedWords([\n            { value: 'both', targets: ['tag'], actorTarget: 'all' },\n          ])\n\n          const { moderationPrefs } = await agent.getPreferences()\n\n          expect(moderationPrefs.mutedWords.length).toEqual(2)\n        })\n      })\n\n      describe('updateMutedWord', () => {\n        it(`word doesn't exist, no update or insert`, async () => {\n          await agent.updateMutedWord({\n            value: 'word',\n            targets: ['tag', 'content'],\n            actorTarget: 'all',\n          })\n          const { moderationPrefs } = await agent.getPreferences()\n          expect(moderationPrefs.mutedWords.length).toEqual(0)\n        })\n\n        it('updates and sanitizes new value', async () => {\n          await agent.addMutedWord({\n            value: 'value',\n            targets: ['content'],\n            actorTarget: 'all',\n          })\n\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'value',\n          )\n\n          await agent.updateMutedWord({\n            ...word!,\n            value: '#new value',\n          })\n\n          const b = await agent.getPreferences()\n          const updatedWord = b.moderationPrefs.mutedWords.find(\n            (m) => m.id === word!.id,\n          )\n\n          expect(updatedWord!.value).toEqual('new value')\n          expect(updatedWord).toHaveProperty('targets', ['content'])\n        })\n\n        it('updates targets', async () => {\n          await agent.addMutedWord({\n            value: 'word',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'word',\n          )\n\n          await agent.updateMutedWord({\n            ...word!,\n            targets: ['content'],\n          })\n\n          const b = await agent.getPreferences()\n\n          expect(\n            b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),\n          ).toHaveProperty('targets', ['content'])\n        })\n\n        it('updates actorTarget', async () => {\n          await agent.addMutedWord({\n            value: 'value',\n            targets: ['content'],\n            actorTarget: 'all',\n          })\n\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'value',\n          )\n\n          await agent.updateMutedWord({\n            ...word!,\n            actorTarget: 'exclude-following',\n          })\n\n          const b = await agent.getPreferences()\n\n          expect(\n            b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),\n          ).toHaveProperty('actorTarget', 'exclude-following')\n        })\n\n        it('updates expiresAt', async () => {\n          const expiresAt = new Date(Date.now() + 6e3).toISOString()\n          const expiresAt2 = new Date(Date.now() + 10e3).toISOString()\n          await agent.addMutedWord({\n            value: 'value',\n            targets: ['content'],\n            expiresAt,\n            actorTarget: 'all',\n          })\n\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'value',\n          )\n\n          await agent.updateMutedWord({\n            ...word!,\n            expiresAt: expiresAt2,\n          })\n\n          const b = await agent.getPreferences()\n\n          expect(\n            b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),\n          ).toHaveProperty('expiresAt', expiresAt2)\n        })\n\n        it(`doesn't update if value is sanitized to be falsy`, async () => {\n          await agent.addMutedWord({\n            value: 'rug',\n            targets: ['content'],\n            actorTarget: 'all',\n          })\n\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'rug',\n          )\n\n          await agent.updateMutedWord({\n            ...word!,\n            value: '',\n          })\n\n          const b = await agent.getPreferences()\n\n          expect(\n            b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),\n          ).toHaveProperty('value', 'rug')\n        })\n      })\n\n      describe('removeMutedWord', () => {\n        it('removes word', async () => {\n          await agent.addMutedWord({\n            value: 'word',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'word',\n          )\n\n          await agent.removeMutedWord(word!)\n\n          const b = await agent.getPreferences()\n\n          expect(\n            b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),\n          ).toBeFalsy()\n        })\n\n        it(`word doesn't exist, no action`, async () => {\n          await agent.addMutedWord({\n            value: 'word',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n          const a = await agent.getPreferences()\n          const word = a.moderationPrefs.mutedWords.find(\n            (m) => m.value === 'word',\n          )\n\n          await agent.removeMutedWord({\n            value: 'another',\n            targets: [],\n            actorTarget: 'all',\n          })\n\n          const b = await agent.getPreferences()\n\n          expect(\n            b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),\n          ).toBeTruthy()\n        })\n      })\n\n      describe('removeMutedWords', () => {\n        it(`removes sequentially, no clobbering`, async () => {\n          await agent.addMutedWords([\n            { value: 'a', targets: ['content'], actorTarget: 'all ' },\n            { value: 'b', targets: ['content'], actorTarget: 'all ' },\n            { value: 'c', targets: ['content'], actorTarget: 'all ' },\n          ])\n\n          const a = await agent.getPreferences()\n          await agent.removeMutedWords(a.moderationPrefs.mutedWords)\n          const b = await agent.getPreferences()\n\n          expect(b.moderationPrefs.mutedWords.length).toEqual(0)\n        })\n      })\n    })\n\n    describe('legacy muted words', () => {\n      let agent: AtpAgent\n\n      async function updatePreferences(\n        agent: AtpAgent,\n        cb: (\n          prefs: AppBskyActorDefs.Preferences,\n        ) => AppBskyActorDefs.Preferences | false,\n      ) {\n        const res = await agent.app.bsky.actor.getPreferences({})\n        const newPrefs = cb(res.data.preferences)\n        if (newPrefs === false) {\n          return\n        }\n        await agent.app.bsky.actor.putPreferences({\n          preferences: newPrefs,\n        })\n      }\n\n      async function addLegacyMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {\n        await updatePreferences(agent, (prefs) => {\n          const mutedWordsPref = prefs.findLast(\n            asPredicate(AppBskyActorDefs.validateMutedWordsPref),\n          ) || {\n            $type: 'app.bsky.actor.defs#mutedWordsPref',\n            items: [],\n          }\n\n          mutedWordsPref.items.push({\n            value: mutedWord.value,\n            targets: mutedWord.targets,\n            actorTarget: 'all',\n          })\n\n          return prefs\n            .filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))\n            .concat([mutedWordsPref])\n        })\n      }\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n        await agent.createAccount({\n          handle: 'user7-1.test',\n          email: 'user7-1@test.com',\n          password: 'password',\n        })\n      })\n\n      afterEach(async () => {\n        const { moderationPrefs } = await agent.getPreferences()\n        await agent.removeMutedWords(moderationPrefs.mutedWords)\n      })\n\n      describe(`upsertMutedWords (and addMutedWord)`, () => {\n        it(`adds new word, migrates old words`, async () => {\n          await addLegacyMutedWord({\n            value: 'word',\n            targets: ['content'],\n            actorTarget: 'all',\n          })\n\n          {\n            const { moderationPrefs } = await agent.getPreferences()\n            const word = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word',\n            )\n            expect(word).toBeTruthy()\n            expect(word!.id).toBeFalsy()\n          }\n\n          await agent.upsertMutedWords([\n            { value: 'word2', targets: ['tag'], actorTarget: 'all' },\n          ])\n\n          {\n            const { moderationPrefs } = await agent.getPreferences()\n            const word = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word',\n            )\n            const word2 = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word2',\n            )\n\n            expect(word!.id).toBeTruthy()\n            expect(word2!.id).toBeTruthy()\n          }\n        })\n      })\n\n      describe(`updateMutedWord`, () => {\n        it(`updates legacy word, migrates old words`, async () => {\n          await addLegacyMutedWord({\n            value: 'word',\n            targets: ['content'],\n            actorTarget: 'all',\n          })\n          await addLegacyMutedWord({\n            value: 'word2',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n\n          await agent.updateMutedWord({\n            value: 'word',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n\n          {\n            const { moderationPrefs } = await agent.getPreferences()\n            const word = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word',\n            )\n            const word2 = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word2',\n            )\n\n            expect(moderationPrefs.mutedWords.length).toEqual(2)\n            expect(word!.id).toBeTruthy()\n            expect(word!.targets).toEqual(['tag'])\n            expect(word2!.id).toBeTruthy()\n          }\n        })\n      })\n\n      describe(`removeMutedWord`, () => {\n        it(`removes legacy word, migrates old words`, async () => {\n          await addLegacyMutedWord({\n            value: 'word',\n            targets: ['content'],\n            actorTarget: 'all',\n          })\n          await addLegacyMutedWord({\n            value: 'word2',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n\n          await agent.removeMutedWord({\n            value: 'word',\n            targets: ['tag'],\n            actorTarget: 'all',\n          })\n\n          {\n            const { moderationPrefs } = await agent.getPreferences()\n            const word = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word',\n            )\n            const word2 = moderationPrefs.mutedWords.find(\n              (w) => w.value === 'word2',\n            )\n\n            expect(moderationPrefs.mutedWords.length).toEqual(1)\n            expect(word).toBeFalsy()\n            expect(word2!.id).toBeTruthy()\n          }\n        })\n      })\n    })\n\n    describe('hidden posts', () => {\n      let agent: AtpAgent\n      const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake'\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n        await agent.createAccount({\n          handle: 'user8.test',\n          email: 'user8@test.com',\n          password: 'password',\n        })\n      })\n\n      it('hidePost', async () => {\n        await agent.hidePost(postUri)\n        await agent.hidePost(postUri) // double, should dedupe\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'moderationPrefs.hiddenPosts',\n          [postUri],\n        )\n      })\n\n      it('unhidePost', async () => {\n        await agent.unhidePost(postUri)\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'moderationPrefs.hiddenPosts',\n          [],\n        )\n        // no issues calling a second time\n        await agent.unhidePost(postUri)\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'moderationPrefs.hiddenPosts',\n          [],\n        )\n      })\n    })\n\n    describe(`saved feeds v2`, () => {\n      let agent: AtpAgent\n      let i = 0\n      const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`\n      const listUri = () => `at://bob.com/app.bsky.graph.list/${i++}`\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n        await agent.createAccount({\n          handle: 'user9.test',\n          email: 'user9@test.com',\n          password: 'password',\n        })\n      })\n\n      beforeEach(async () => {\n        await agent.app.bsky.actor.putPreferences({\n          preferences: [],\n        })\n      })\n\n      describe(`addSavedFeeds`, () => {\n        it('works', async () => {\n          const feed = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          await agent.addSavedFeeds([feed])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([\n            {\n              ...feed,\n              id: expect.any(String),\n            },\n          ])\n        })\n\n        it('throws if feed is specified and list provided', async () => {\n          const list = listUri()\n          await expect(() =>\n            agent.addSavedFeeds([\n              {\n                type: 'feed',\n                value: list,\n                pinned: true,\n              },\n            ]),\n          ).rejects.toThrow()\n        })\n\n        it('throws if list is specified and feed provided', async () => {\n          const feed = feedUri()\n          await expect(() =>\n            agent.addSavedFeeds([\n              {\n                type: 'list',\n                value: feed,\n                pinned: true,\n              },\n            ]),\n          ).rejects.toThrow()\n        })\n\n        it(`timeline`, async () => {\n          const feeds = await agent.addSavedFeeds([\n            {\n              type: 'timeline',\n              value: 'following',\n              pinned: true,\n            },\n          ])\n          const prefs = await agent.getPreferences()\n          expect(\n            prefs.savedFeeds.filter((f) => f.type === 'timeline'),\n          ).toStrictEqual(feeds)\n        })\n\n        it(`allows duplicates`, async () => {\n          const feed = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          await agent.addSavedFeeds([feed])\n          await agent.addSavedFeeds([feed])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([\n            {\n              ...feed,\n              id: expect.any(String),\n            },\n            {\n              ...feed,\n              id: expect.any(String),\n            },\n          ])\n        })\n\n        it(`adds multiple`, async () => {\n          const a = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const b = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          await agent.addSavedFeeds([a, b])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([\n            {\n              ...a,\n              id: expect.any(String),\n            },\n            {\n              ...b,\n              id: expect.any(String),\n            },\n          ])\n        })\n\n        it(`appends multiple`, async () => {\n          const a = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const b = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          const c = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const d = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          await agent.addSavedFeeds([a, b])\n          await agent.addSavedFeeds([c, d])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([\n            {\n              ...a,\n              id: expect.any(String),\n            },\n            {\n              ...c,\n              id: expect.any(String),\n            },\n            {\n              ...b,\n              id: expect.any(String),\n            },\n            {\n              ...d,\n              id: expect.any(String),\n            },\n          ])\n        })\n      })\n\n      describe(`removeSavedFeeds`, () => {\n        it('works', async () => {\n          const feed = {\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const savedFeeds = await agent.addSavedFeeds([feed])\n          await agent.removeSavedFeeds([savedFeeds[0].id])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([])\n        })\n      })\n\n      describe(`overwriteSavedFeeds`, () => {\n        it(`dedupes by id, takes last, preserves order based on last found`, async () => {\n          const a = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const b = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          await agent.overwriteSavedFeeds([a, b, a])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([b, a])\n        })\n\n        it(`preserves order`, async () => {\n          const a = feedUri()\n          const b = feedUri()\n          const c = feedUri()\n          const d = feedUri()\n\n          await agent.overwriteSavedFeeds([\n            {\n              id: TID.nextStr(),\n              type: 'timeline',\n              value: a,\n              pinned: true,\n            },\n            {\n              id: TID.nextStr(),\n              type: 'feed',\n              value: b,\n              pinned: false,\n            },\n            {\n              id: TID.nextStr(),\n              type: 'feed',\n              value: c,\n              pinned: true,\n            },\n            {\n              id: TID.nextStr(),\n              type: 'feed',\n              value: d,\n              pinned: false,\n            },\n          ])\n\n          const { savedFeeds } = await agent.getPreferences()\n          expect(savedFeeds.filter((f) => f.pinned)).toStrictEqual([\n            {\n              id: expect.any(String),\n              type: 'timeline',\n              value: a,\n              pinned: true,\n            },\n            {\n              id: expect.any(String),\n              type: 'feed',\n              value: c,\n              pinned: true,\n            },\n          ])\n          expect(savedFeeds.filter((f) => !f.pinned)).toEqual([\n            {\n              id: expect.any(String),\n              type: 'feed',\n              value: b,\n              pinned: false,\n            },\n            {\n              id: expect.any(String),\n              type: 'feed',\n              value: d,\n              pinned: false,\n            },\n          ])\n        })\n      })\n\n      describe(`updateSavedFeeds`, () => {\n        it(`updates affect order, saved last, new pins last`, async () => {\n          const a = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const b = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          const c = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n\n          await agent.overwriteSavedFeeds([a, b, c])\n          await agent.updateSavedFeeds([\n            {\n              ...b,\n              pinned: false,\n            },\n          ])\n\n          const prefs1 = await agent.getPreferences()\n          expect(prefs1.savedFeeds).toStrictEqual([\n            a,\n            c,\n            {\n              ...b,\n              pinned: false,\n            },\n          ])\n\n          await agent.updateSavedFeeds([\n            {\n              ...b,\n              pinned: true,\n            },\n          ])\n\n          const prefs2 = await agent.getPreferences()\n          expect(prefs2.savedFeeds).toStrictEqual([a, c, b])\n        })\n\n        it(`cannot override original id`, async () => {\n          const a = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: true,\n          }\n          await agent.overwriteSavedFeeds([a])\n          await agent.updateSavedFeeds([\n            {\n              ...a,\n              pinned: false,\n              id: TID.nextStr(),\n            },\n          ])\n          const prefs = await agent.getPreferences()\n          expect(prefs.savedFeeds).toStrictEqual([a])\n        })\n\n        it(`updates multiple`, async () => {\n          const a = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          const b = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n          const c = {\n            id: TID.nextStr(),\n            type: 'feed',\n            value: feedUri(),\n            pinned: false,\n          }\n\n          await agent.overwriteSavedFeeds([a, b, c])\n          await agent.updateSavedFeeds([\n            {\n              ...b,\n              pinned: true,\n            },\n            {\n              ...c,\n              pinned: true,\n            },\n          ])\n\n          const prefs1 = await agent.getPreferences()\n          expect(prefs1.savedFeeds).toStrictEqual([\n            {\n              ...b,\n              pinned: true,\n            },\n            {\n              ...c,\n              pinned: true,\n            },\n            a,\n          ])\n        })\n      })\n\n      describe(`utils`, () => {\n        describe(`savedFeedsToUriArrays`, () => {\n          const { saved, pinned } = savedFeedsToUriArrays([\n            {\n              id: '',\n              type: 'feed',\n              value: 'a',\n              pinned: true,\n            },\n            {\n              id: '',\n              type: 'feed',\n              value: 'b',\n              pinned: false,\n            },\n            {\n              id: '',\n              type: 'feed',\n              value: 'c',\n              pinned: true,\n            },\n          ])\n          expect(saved).toStrictEqual(['a', 'b', 'c'])\n          expect(pinned).toStrictEqual(['a', 'c'])\n        })\n\n        describe(`getSavedFeedType`, () => {\n          it(`works`, () => {\n            expect(getSavedFeedType('at://foo.com')).toBe('unknown')\n            expect(getSavedFeedType(feedUri())).toBe('feed')\n            expect(getSavedFeedType(listUri())).toBe('list')\n            expect(\n              getSavedFeedType('at://did:plc:fake/app.bsky.graph.follow/fake'),\n            ).toBe('unknown')\n          })\n        })\n\n        describe(`validateSavedFeed`, () => {\n          it(`throws if missing id`, () => {\n            // really only checks length at time of writing\n            expect(() =>\n              validateSavedFeed({\n                id: '',\n                type: 'feed',\n                value: feedUri(),\n                pinned: false,\n              }),\n            ).toThrow()\n          })\n\n          it(`does not throw if a UUID is used as id`, () => {\n            // really only checks length at time of writing\n            expect(() =>\n              validateSavedFeed({\n                id: '497dcba3-ecbf-4587-a2dd-5eb0665e6880',\n                type: 'feed',\n                value: feedUri(),\n                pinned: false,\n              }),\n            ).not.toThrow()\n          })\n\n          it(`throws if mismatched types`, () => {\n            expect(() =>\n              validateSavedFeed({\n                id: TID.nextStr(),\n                type: 'list',\n                value: feedUri(),\n                pinned: false,\n              }),\n            ).toThrow()\n            expect(() =>\n              validateSavedFeed({\n                id: TID.nextStr(),\n                type: 'feed',\n                value: listUri(),\n                pinned: false,\n              }),\n            ).toThrow()\n          })\n\n          it(`ignores values it can't validate`, () => {\n            expect(() =>\n              validateSavedFeed({\n                id: TID.nextStr(),\n                type: 'timeline',\n                value: 'following',\n                pinned: false,\n              }),\n            ).not.toThrow()\n            expect(() =>\n              validateSavedFeed({\n                id: TID.nextStr(),\n                type: 'unknown',\n                value: 'could be @nyt4!ng',\n                pinned: false,\n              }),\n            ).not.toThrow()\n          })\n        })\n      })\n    })\n\n    describe(`saved feeds v2: migration scenarios`, () => {\n      let agent: AtpAgent\n      let i = 0\n      const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n        await agent.createAccount({\n          handle: 'user10.test',\n          email: 'user10@test.com',\n          password: 'password',\n        })\n      })\n\n      beforeEach(async () => {\n        await agent.app.bsky.actor.putPreferences({\n          preferences: [],\n        })\n      })\n\n      it('CRUD action before migration, no timeline inserted', async () => {\n        const feed = {\n          type: 'feed',\n          value: feedUri(),\n          pinned: false,\n        }\n        await agent.addSavedFeeds([feed])\n        const prefs = await agent.getPreferences()\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            ...feed,\n            id: expect.any(String),\n          },\n        ])\n      })\n\n      it('CRUD action AFTER migration, timeline was inserted', async () => {\n        await agent.getPreferences()\n        const feed = {\n          type: 'feed',\n          value: feedUri(),\n          pinned: false,\n        }\n        await agent.addSavedFeeds([feed])\n        const prefs = await agent.getPreferences()\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n          {\n            ...feed,\n            id: expect.any(String),\n          },\n        ])\n      })\n\n      // fresh account OR an old account with no v1 prefs to migrate from\n      it(`brand new user, v1 remains undefined`, async () => {\n        const prefs = await agent.getPreferences()\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ])\n        // no v1 prefs to populate from\n        expect(prefs.feeds).toStrictEqual({\n          saved: undefined,\n          pinned: undefined,\n        })\n      })\n\n      it(`brand new user, v2 does not write to v1`, async () => {\n        const a = feedUri()\n        // migration happens\n        await agent.getPreferences()\n        await agent.addSavedFeeds([\n          {\n            type: 'feed',\n            value: a,\n            pinned: false,\n          },\n        ])\n        const prefs = await agent.getPreferences()\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n          {\n            id: expect.any(String),\n            type: 'feed',\n            value: a,\n            pinned: false,\n          },\n        ])\n        // no v1 prefs to populate from\n        expect(prefs.feeds).toStrictEqual({\n          saved: undefined,\n          pinned: undefined,\n        })\n      })\n\n      it(`existing user with v1 prefs, migrates`, async () => {\n        const one = feedUri()\n        const two = feedUri()\n        await agent.app.bsky.actor.putPreferences({\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#savedFeedsPref',\n              pinned: [one],\n              saved: [one, two],\n            },\n          ],\n        })\n        const prefs = await agent.getPreferences()\n\n        // deprecated interface receives what it normally would\n        expect(prefs.feeds).toStrictEqual({\n          pinned: [one],\n          saved: [one, two],\n        })\n        // new interface gets new timeline + old pinned feed\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n          {\n            id: expect.any(String),\n            type: 'feed',\n            value: one,\n            pinned: true,\n          },\n          {\n            id: expect.any(String),\n            type: 'feed',\n            value: two,\n            pinned: false,\n          },\n        ])\n      })\n\n      it('squashes duplicates during migration', async () => {\n        const one = feedUri()\n        const two = feedUri()\n        await agent.app.bsky.actor.putPreferences({\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#savedFeedsPref',\n              pinned: [one, two],\n              saved: [one, two],\n            },\n            {\n              $type: 'app.bsky.actor.defs#savedFeedsPref',\n              pinned: [],\n              saved: [],\n            },\n          ],\n        })\n\n        // performs migration\n        const prefs = await agent.getPreferences()\n        expect(prefs.feeds).toStrictEqual({\n          pinned: [],\n          saved: [],\n        })\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ])\n\n        const res = await agent.app.bsky.actor.getPreferences()\n        expect(res.data.preferences).toStrictEqual([\n          {\n            $type: 'app.bsky.actor.defs#savedFeedsPrefV2',\n            items: [\n              {\n                id: expect.any(String),\n                type: 'timeline',\n                value: 'following',\n                pinned: true,\n              },\n            ],\n          },\n          {\n            $type: 'app.bsky.actor.defs#savedFeedsPref',\n            pinned: [],\n            saved: [],\n          },\n        ])\n      })\n\n      it('v2 writes persist to v1, not the inverse', async () => {\n        const a = feedUri()\n        const b = feedUri()\n        const c = feedUri()\n        const d = feedUri()\n        const e = feedUri()\n\n        await agent.app.bsky.actor.putPreferences({\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#savedFeedsPref',\n              pinned: [a, b],\n              saved: [a, b],\n            },\n          ],\n        })\n\n        // client updates, migrates to v2\n        // a and b are both pinned\n        await agent.getPreferences()\n\n        // new write to v2, c is saved\n        await agent.addSavedFeeds([\n          {\n            type: 'feed',\n            value: c,\n            pinned: false,\n          },\n        ])\n\n        // v2 write wrote to v1 also\n        const res1 = await agent.app.bsky.actor.getPreferences()\n        const v1Pref = res1.data.preferences.find((p) =>\n          AppBskyActorDefs.isSavedFeedsPref(p),\n        )\n        expect(v1Pref).toStrictEqual({\n          $type: 'app.bsky.actor.defs#savedFeedsPref',\n          pinned: [a, b],\n          saved: [a, b, c],\n        })\n\n        // v1 write occurs, d is added but not to v2\n        await agent.addSavedFeed(d)\n\n        const res3 = await agent.app.bsky.actor.getPreferences()\n        const v1Pref3 = res3.data.preferences.find((p) =>\n          AppBskyActorDefs.isSavedFeedsPref(p),\n        )\n        expect(v1Pref3).toStrictEqual({\n          $type: 'app.bsky.actor.defs#savedFeedsPref',\n          pinned: [a, b],\n          saved: [a, b, c, d],\n        })\n\n        // another new write to v2, pins e\n        await agent.addSavedFeeds([\n          {\n            type: 'feed',\n            value: e,\n            pinned: true,\n          },\n        ])\n\n        const res4 = await agent.app.bsky.actor.getPreferences()\n        const v1Pref4 = res4.data.preferences.find((p) =>\n          AppBskyActorDefs.isSavedFeedsPref(p),\n        )\n        // v1 pref got v2 write\n        expect(v1Pref4).toStrictEqual({\n          $type: 'app.bsky.actor.defs#savedFeedsPref',\n          pinned: [a, b, e],\n          saved: [a, b, c, d, e],\n        })\n\n        const final = await agent.getPreferences()\n        // d not here bc it was written with v1\n        expect(final.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n          { id: expect.any(String), type: 'feed', value: a, pinned: true },\n          { id: expect.any(String), type: 'feed', value: b, pinned: true },\n          { id: expect.any(String), type: 'feed', value: e, pinned: true },\n          { id: expect.any(String), type: 'feed', value: c, pinned: false },\n        ])\n      })\n\n      it(`filters out invalid values in v1 prefs`, async () => {\n        // v1 prefs must be valid AtUris, but they could be any type in theory\n        await agent.app.bsky.actor.putPreferences({\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#savedFeedsPref',\n              pinned: ['at://did:plc:fake/app.bsky.graph.follow/fake'],\n              saved: ['at://did:plc:fake/app.bsky.graph.follow/fake'],\n            },\n          ],\n        })\n        const prefs = await agent.getPreferences()\n        expect(prefs.savedFeeds).toStrictEqual([\n          {\n            id: expect.any(String),\n            type: 'timeline',\n            value: 'following',\n            pinned: true,\n          },\n        ])\n      })\n    })\n\n    describe('queued nudges', () => {\n      it('queueNudges & dismissNudges', async () => {\n        const agent = new AtpAgent({ service: network.pds.url })\n        await agent.createAccount({\n          handle: 'user11.test',\n          email: 'user11@test.com',\n          password: 'password',\n        })\n        await agent.bskyAppQueueNudges('first')\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.queuedNudges',\n          ['first'],\n        )\n        await agent.bskyAppQueueNudges(['second', 'third'])\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.queuedNudges',\n          ['first', 'second', 'third'],\n        )\n        await agent.bskyAppDismissNudges('second')\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.queuedNudges',\n          ['first', 'third'],\n        )\n        await agent.bskyAppDismissNudges(['first', 'third'])\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.queuedNudges',\n          [],\n        )\n      })\n    })\n\n    describe('guided tours', () => {\n      it('setActiveProgressGuide', async () => {\n        const agent = new AtpAgent({ service: network.pds.url })\n\n        await agent.createAccount({\n          handle: 'user12.test',\n          email: 'user12@test.com',\n          password: 'password',\n        })\n\n        await agent.bskyAppSetActiveProgressGuide({\n          guide: 'test-guide',\n          // @ts-expect-error unspecced field\n          numThings: 0,\n        })\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.activeProgressGuide.guide',\n          'test-guide',\n        )\n        await agent.bskyAppSetActiveProgressGuide({\n          guide: 'test-guide',\n          // @ts-expect-error unspecced field\n          numThings: 1,\n        })\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.activeProgressGuide.guide',\n          'test-guide',\n        )\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.activeProgressGuide.numThings',\n          1,\n        )\n        await agent.bskyAppSetActiveProgressGuide(undefined)\n        await expect(agent.getPreferences()).resolves.toHaveProperty(\n          'bskyAppState.activeProgressGuide',\n          undefined,\n        )\n      })\n    })\n\n    describe('nuxs', () => {\n      let agent: AtpAgent\n\n      const nux = {\n        id: 'a',\n        completed: false,\n        data: '{}',\n        expiresAt: new Date(Date.now() + 6e3).toISOString(),\n      }\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n\n        await agent.createAccount({\n          handle: 'nuxs.test',\n          email: 'nuxs@test.com',\n          password: 'password',\n        })\n      })\n\n      it('bskyAppUpsertNux', async () => {\n        // never duplicates\n        await agent.bskyAppUpsertNux(nux)\n        await agent.bskyAppUpsertNux(nux)\n        await agent.bskyAppUpsertNux(nux)\n\n        const prefs = await agent.getPreferences()\n        const nuxs = prefs.bskyAppState.nuxs\n\n        expect(nuxs.length).toEqual(1)\n        expect(nuxs.find((n) => n.id === nux.id)).toEqual(nux)\n      })\n\n      it('bskyAppUpsertNux completed', async () => {\n        // never duplicates\n        await agent.bskyAppUpsertNux({\n          ...nux,\n          completed: true,\n        })\n\n        const prefs = await agent.getPreferences()\n        const nuxs = prefs.bskyAppState.nuxs\n\n        expect(nuxs.length).toEqual(1)\n        expect(nuxs.find((n) => n.id === nux.id)?.completed).toEqual(true)\n      })\n\n      it('bskyAppRemoveNuxs', async () => {\n        await agent.bskyAppRemoveNuxs([nux.id])\n\n        const prefs = await agent.getPreferences()\n        const nuxs = prefs.bskyAppState.nuxs\n\n        expect(nuxs.length).toEqual(0)\n      })\n\n      it('bskyAppUpsertNux validates nux', async () => {\n        // @ts-expect-error\n        expect(() => agent.bskyAppUpsertNux({ name: 'a' })).rejects.toThrow()\n        expect(() =>\n          // @ts-expect-error\n          agent.bskyAppUpsertNux({ id: 'a', completed: false, foo: 'bar' }),\n        ).rejects.toThrow()\n      })\n    })\n\n    describe('setPostInteractionSettings', () => {\n      let agent: AtpAgent\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n\n        await agent.createAccount({\n          handle: 'pints.test',\n          email: 'pints@test.com',\n          password: 'password',\n        })\n      })\n\n      it('works', async () => {\n        const next: AppBskyActorDefs.PostInteractionSettingsPref = {\n          threadgateAllowRules: [\n            { $type: 'app.bsky.feed.threadgate#mentionRule' },\n          ],\n          postgateEmbeddingRules: [],\n        }\n\n        await agent.setPostInteractionSettings(next)\n\n        const prefs = await agent.getPreferences()\n\n        expect(prefs.postInteractionSettings).toEqual(next)\n      })\n\n      it('clears', async () => {\n        const next: AppBskyActorDefs.PostInteractionSettingsPref = {\n          threadgateAllowRules: [],\n          postgateEmbeddingRules: [],\n        }\n\n        await agent.setPostInteractionSettings(next)\n\n        const prefs = await agent.getPreferences()\n\n        expect(prefs.postInteractionSettings).toEqual(next)\n      })\n\n      /**\n       * Logic matches threadgate `allow` logic, where `undefined` means \"anyone\n       * can reply\" and an empty array means \"no one can reply\".\n       *\n       * Postgate `embeddingRules` behaves differently, where `undefined` and\n       * an empty array both mean \"no particular rules applied\".\n       *\n       * Both props are optional though, so for easier sharing of types, we\n       * allow `undefined`.\n       */\n      it('clears using undefined', async () => {\n        const next: AppBskyActorDefs.PostInteractionSettingsPref = {\n          threadgateAllowRules: undefined,\n          postgateEmbeddingRules: undefined,\n        }\n\n        await agent.setPostInteractionSettings(next)\n\n        const prefs = await agent.getPreferences()\n\n        expect(prefs.postInteractionSettings).toEqual(next)\n      })\n\n      it('validates inputs', async () => {\n        expect(() =>\n          agent.setPostInteractionSettings({\n            // @ts-expect-error we are testing invalid inputs\n            threadgateAllowRules: [{ key: 'string' }],\n            postgateEmbeddingRules: [],\n          }),\n        ).rejects.toThrow()\n      })\n    })\n\n    describe('setVerificationPrefs', () => {\n      let agent: AtpAgent\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n\n        await agent.createAccount({\n          handle: 'verification-prefs.test',\n          email: 'verification-prefs@test.com',\n          password: 'password',\n        })\n      })\n\n      it('default state', async () => {\n        const next: AppBskyActorDefs.VerificationPrefs = {\n          hideBadges: false,\n        }\n        const prefs = await agent.getPreferences()\n        expect(prefs.verificationPrefs).toEqual(next)\n      })\n\n      it('updates', async () => {\n        const next: AppBskyActorDefs.VerificationPrefs = {\n          hideBadges: true,\n        }\n        await agent.setVerificationPrefs(next)\n        const prefs = await agent.getPreferences()\n        expect(prefs.verificationPrefs).toEqual(next)\n      })\n    })\n\n    describe('updateLiveEventPreferences', () => {\n      let agent: AtpAgent\n\n      beforeAll(async () => {\n        agent = new AtpAgent({ service: network.pds.url })\n\n        await agent.createAccount({\n          handle: 'live-event-prefs.test',\n          email: 'live-event-prefs@test.com',\n          password: 'password',\n        })\n      })\n\n      it('default state', async () => {\n        const prefs = await agent.getPreferences()\n        expect(prefs.liveEventPreferences).toEqual({\n          hiddenFeedIds: [],\n          hideAllFeeds: false,\n        })\n      })\n\n      it('hideFeed adds a feed id', async () => {\n        await agent.updateLiveEventPreferences({\n          type: 'hideFeed',\n          id: 'feed1',\n        })\n        const prefs = await agent.getPreferences()\n        expect(prefs.liveEventPreferences).toEqual({\n          hiddenFeedIds: ['feed1'],\n          hideAllFeeds: false,\n        })\n      })\n\n      it('hideFeed adds another feed id', async () => {\n        await agent.updateLiveEventPreferences({\n          type: 'hideFeed',\n          id: 'feed2',\n        })\n        const prefs = await agent.getPreferences()\n        expect(prefs.liveEventPreferences).toEqual({\n          hiddenFeedIds: ['feed1', 'feed2'],\n          hideAllFeeds: false,\n        })\n      })\n\n      it('unhideFeed removes a feed id', async () => {\n        await agent.updateLiveEventPreferences({\n          type: 'unhideFeed',\n          id: 'feed1',\n        })\n        const prefs = await agent.getPreferences()\n        expect(prefs.liveEventPreferences).toEqual({\n          hiddenFeedIds: ['feed2'],\n          hideAllFeeds: false,\n        })\n      })\n\n      it('toggleHideAllFeeds toggles the flag', async () => {\n        await agent.updateLiveEventPreferences({ type: 'toggleHideAllFeeds' })\n        const prefs = await agent.getPreferences()\n        expect(prefs.liveEventPreferences).toEqual({\n          hiddenFeedIds: ['feed2'],\n          hideAllFeeds: true,\n        })\n      })\n\n      it('toggleHideAllFeeds toggles back', async () => {\n        await agent.updateLiveEventPreferences({ type: 'toggleHideAllFeeds' })\n        const prefs = await agent.getPreferences()\n        expect(prefs.liveEventPreferences).toEqual({\n          hiddenFeedIds: ['feed2'],\n          hideAllFeeds: false,\n        })\n      })\n    })\n\n    // end\n  })\n})\n\nconst byType = (a, b) => a.$type.localeCompare(b.$type)\n"
  },
  {
    "path": "packages/api/tests/dispatcher.test.ts",
    "content": "import assert from 'node:assert'\nimport { AddressInfo } from 'node:net'\nimport { getPdsEndpoint, isValidDidDoc } from '@atproto/common-web'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport {\n  AtpAgent,\n  AtpSessionData,\n  AtpSessionEvent,\n  BSKY_LABELER_DID,\n} from '../src'\nimport { createHeaderEchoServer } from './util/echo-server'\n\nconst getPdsEndpointUrl = (...args: Parameters<typeof getPdsEndpoint>) => {\n  const endpoint = getPdsEndpoint(...args)\n  return endpoint ? new URL(endpoint) : endpoint\n}\n\ndescribe('AtpAgent', () => {\n  let network: TestNetworkNoAppView\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'api_agent',\n      pds: {\n        enableDidDocWithSession: true,\n      },\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('clones correctly', () => {\n    const persistSession = (_evt: AtpSessionEvent, _sess?: AtpSessionData) => {}\n    const agent = new AtpAgent({ service: network.pds.url, persistSession })\n    const agent2 = agent.clone()\n    expect(agent2 instanceof AtpAgent).toBeTruthy()\n    expect(agent.serviceUrl).toEqual(agent2.serviceUrl)\n  })\n\n  it('creates a new session on account creation.', async () => {\n    const events: string[] = []\n    const sessions: (AtpSessionData | undefined)[] = []\n    const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n      events.push(evt)\n      sessions.push(sess)\n    }\n\n    const agent = new AtpAgent({ service: network.pds.url, persistSession })\n\n    const res = await agent.createAccount({\n      handle: 'user1.test',\n      email: 'user1@test.com',\n      password: 'password',\n    })\n\n    expect(agent.hasSession).toEqual(true)\n    expect(agent.session?.accessJwt).toEqual(res.data.accessJwt)\n    expect(agent.session?.refreshJwt).toEqual(res.data.refreshJwt)\n    expect(agent.session?.handle).toEqual(res.data.handle)\n    expect(agent.session?.did).toEqual(res.data.did)\n    expect(agent.session?.email).toEqual('user1@test.com')\n    expect(agent.session?.emailConfirmed).toEqual(false)\n    assert(isValidDidDoc(res.data.didDoc))\n    expect(agent.pdsUrl).toEqual(getPdsEndpointUrl(res.data.didDoc))\n\n    const { data: sessionInfo } = await agent.com.atproto.server.getSession({})\n    expect(sessionInfo).toMatchObject({\n      did: res.data.did,\n      handle: res.data.handle,\n      email: 'user1@test.com',\n      emailConfirmed: false,\n    })\n    expect(isValidDidDoc(sessionInfo.didDoc)).toBe(true)\n\n    expect(events.length).toEqual(1)\n    expect(events[0]).toEqual('create')\n    expect(sessions.length).toEqual(1)\n    expect(sessions[0]?.accessJwt).toEqual(agent.session?.accessJwt)\n  })\n\n  it('creates a new session on login.', async () => {\n    const events: string[] = []\n    const sessions: (AtpSessionData | undefined)[] = []\n    const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n      events.push(evt)\n      sessions.push(sess)\n    }\n\n    const agent1 = new AtpAgent({ service: network.pds.url, persistSession })\n\n    const email = 'user2@test.com'\n    await agent1.createAccount({\n      handle: 'user2.test',\n      email,\n      password: 'password',\n    })\n\n    const agent2 = new AtpAgent({ service: network.pds.url, persistSession })\n    const res1 = await agent2.login({\n      identifier: 'user2.test',\n      password: 'password',\n    })\n\n    expect(agent2.hasSession).toEqual(true)\n    expect(agent2.session?.accessJwt).toEqual(res1.data.accessJwt)\n    expect(agent2.session?.refreshJwt).toEqual(res1.data.refreshJwt)\n    expect(agent2.session?.handle).toEqual(res1.data.handle)\n    expect(agent2.session?.did).toEqual(res1.data.did)\n    expect(agent2.session?.email).toEqual('user2@test.com')\n    expect(agent2.session?.emailConfirmed).toEqual(false)\n    assert(isValidDidDoc(res1.data.didDoc))\n    expect(agent2.pdsUrl).toEqual(getPdsEndpointUrl(res1.data.didDoc))\n\n    const { data: sessionInfo } = await agent2.com.atproto.server.getSession({})\n    expect(sessionInfo).toMatchObject({\n      did: res1.data.did,\n      handle: res1.data.handle,\n      email,\n      emailConfirmed: false,\n    })\n    expect(isValidDidDoc(sessionInfo.didDoc)).toBe(true)\n\n    expect(events.length).toEqual(2)\n    expect(events[0]).toEqual('create')\n    expect(events[1]).toEqual('create')\n    expect(sessions.length).toEqual(2)\n    expect(sessions[0]?.accessJwt).toEqual(agent1.session?.accessJwt)\n    expect(sessions[1]?.accessJwt).toEqual(agent2.session?.accessJwt)\n  })\n\n  it('resumes an existing session.', async () => {\n    const events: string[] = []\n    const sessions: (AtpSessionData | undefined)[] = []\n    const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n      events.push(evt)\n      sessions.push(sess)\n    }\n\n    const agent1 = new AtpAgent({ service: network.pds.url, persistSession })\n\n    await agent1.createAccount({\n      handle: 'user3.test',\n      email: 'user3@test.com',\n      password: 'password',\n    })\n    if (!agent1.session) {\n      throw new Error('No session created')\n    }\n\n    const agent2 = new AtpAgent({ service: network.pds.url, persistSession })\n    const res1 = await agent2.resumeSession(agent1.session)\n\n    expect(agent2.hasSession).toEqual(true)\n    expect(agent2.session?.handle).toEqual(res1.data.handle)\n    expect(agent2.session?.did).toEqual(res1.data.did)\n    assert(isValidDidDoc(res1.data.didDoc))\n    expect(agent2.pdsUrl).toEqual(getPdsEndpointUrl(res1.data.didDoc))\n\n    const { data: sessionInfo } = await agent2.com.atproto.server.getSession({})\n    expect(sessionInfo).toMatchObject({\n      did: res1.data.did,\n      handle: res1.data.handle,\n      email: res1.data.email,\n      emailConfirmed: false,\n    })\n    expect(isValidDidDoc(sessionInfo.didDoc)).toBe(true)\n\n    expect(events.length).toEqual(2)\n    expect(events[0]).toEqual('create')\n    expect(events[1]).toEqual('update')\n    expect(sessions.length).toEqual(2)\n    expect(sessions[0]?.accessJwt).toEqual(agent1.session?.accessJwt)\n    expect(sessions[1]?.accessJwt).toEqual(agent2.session?.accessJwt)\n  })\n\n  it('refreshes existing session.', async () => {\n    const events: string[] = []\n    const sessions: (AtpSessionData | undefined)[] = []\n    const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n      events.push(evt)\n      sessions.push(sess)\n    }\n\n    const agent = new AtpAgent({ service: network.pds.url, persistSession })\n\n    // create an account and a session with it\n    await agent.createAccount({\n      handle: 'user4.test',\n      email: 'user4@test.com',\n      password: 'password',\n    })\n    if (!agent.session?.refreshJwt) {\n      throw new Error('No session created')\n    }\n    const session1 = agent.session\n    const origAccessJwt = session1.accessJwt\n\n    // wait 1 second so that a token refresh will issue a new access token\n    // (if the timestamp, which has 1 second resolution, is the same -- then the access token won't change)\n    await new Promise((r) => setTimeout(r, 1000))\n\n    // patch the fetch handler to fake an expired token error on the next request\n    agent.sessionManager.setFetch(\n      async (input: RequestInfo | URL, init?: RequestInit) => {\n        const req = new Request(input, init)\n        if (\n          req.headers.get('authorization') === `Bearer ${origAccessJwt}` &&\n          !req.url.includes('com.atproto.server.refreshSession')\n        ) {\n          return new Response(JSON.stringify({ error: 'ExpiredToken' }), {\n            status: 400,\n            headers: { 'Content-Type': 'application/json' },\n          })\n        }\n\n        return globalThis.fetch(req)\n      },\n    )\n\n    // put the agent through the auth flow\n    const res1 = await createPost(agent)\n\n    expect(res1.success).toEqual(true)\n    expect(agent.hasSession).toEqual(true)\n    expect(agent.session?.accessJwt).not.toEqual(session1.accessJwt)\n    expect(agent.session?.refreshJwt).not.toEqual(session1.refreshJwt)\n    expect(agent.session?.handle).toEqual(session1.handle)\n    expect(agent.session?.did).toEqual(session1.did)\n    expect(agent.session?.email).toEqual(session1.email)\n    expect(agent.session?.emailConfirmed).toEqual(session1.emailConfirmed)\n\n    expect(events.length).toEqual(2)\n    expect(events[0]).toEqual('create')\n    expect(events[1]).toEqual('update')\n    expect(sessions.length).toEqual(2)\n    expect(sessions[0]?.accessJwt).toEqual(origAccessJwt)\n    expect(sessions[1]?.accessJwt).toEqual(agent.session?.accessJwt)\n  })\n\n  it('dedupes session refreshes.', async () => {\n    const events: string[] = []\n    const sessions: (AtpSessionData | undefined)[] = []\n    const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n      events.push(evt)\n      sessions.push(sess)\n    }\n\n    const agent = new AtpAgent({ service: network.pds.url, persistSession })\n\n    // create an account and a session with it\n    await agent.createAccount({\n      handle: 'user5.test',\n      email: 'user5@test.com',\n      password: 'password',\n    })\n    if (!agent.session) {\n      throw new Error('No session created')\n    }\n    const session1 = agent.session\n    const origAccessJwt = session1.accessJwt\n\n    // wait 1 second so that a token refresh will issue a new access token\n    // (if the timestamp, which has 1 second resolution, is the same -- then the access token won't change)\n    await new Promise((r) => setTimeout(r, 1000))\n\n    // patch the fetch handler to fake an expired token error on the next request\n    let expiredCalls = 0\n    let refreshCalls = 0\n\n    agent.sessionManager.setFetch(\n      async (input: RequestInfo | URL, init?: RequestInit) => {\n        const req = new Request(input, init)\n        if (req.headers.get('authorization') === `Bearer ${origAccessJwt}`) {\n          expiredCalls++\n          return new Response(JSON.stringify({ error: 'ExpiredToken' }), {\n            status: 400,\n            headers: { 'Content-Type': 'application/json' },\n          })\n        }\n        if (req.url.includes('com.atproto.server.refreshSession')) {\n          refreshCalls++\n        }\n        return globalThis.fetch(req)\n      },\n    )\n\n    // put the agent through the auth flow\n    const [res1, res2, res3] = await Promise.all([\n      createPost(agent),\n      createPost(agent),\n      createPost(agent),\n    ])\n\n    expect(expiredCalls).toEqual(3)\n    expect(refreshCalls).toEqual(1)\n    expect(res1.success).toEqual(true)\n    expect(res2.success).toEqual(true)\n    expect(res3.success).toEqual(true)\n    expect(agent.hasSession).toEqual(true)\n    expect(agent.session?.accessJwt).not.toEqual(session1.accessJwt)\n    expect(agent.session?.refreshJwt).not.toEqual(session1.refreshJwt)\n    expect(agent.session?.handle).toEqual(session1.handle)\n    expect(agent.session?.did).toEqual(session1.did)\n    expect(agent.session?.email).toEqual(session1.email)\n    expect(agent.session?.emailConfirmed).toEqual(session1.emailConfirmed)\n\n    expect(events.length).toEqual(2)\n    expect(events[0]).toEqual('create')\n    expect(events[1]).toEqual('update')\n    expect(sessions.length).toEqual(2)\n    expect(sessions[0]?.accessJwt).toEqual(origAccessJwt)\n    expect(sessions[1]?.accessJwt).toEqual(agent.session?.accessJwt)\n  })\n\n  it('persists an empty session on login and resumeSession failures', async () => {\n    const events: string[] = []\n    const sessions: (AtpSessionData | undefined)[] = []\n    const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n      events.push(evt)\n      sessions.push(sess)\n    }\n\n    const agent = new AtpAgent({ service: network.pds.url, persistSession })\n\n    try {\n      await agent.login({\n        identifier: 'baduser.test',\n        password: 'password',\n      })\n    } catch (_e: any) {\n      // ignore\n    }\n    expect(agent.hasSession).toEqual(false)\n\n    try {\n      await agent.resumeSession({\n        accessJwt: 'bad',\n        refreshJwt: 'bad',\n        did: 'bad',\n        handle: 'bad',\n        active: true,\n      })\n    } catch (_e: any) {\n      // ignore\n    }\n    expect(agent.hasSession).toEqual(false)\n\n    expect(events.length).toEqual(2)\n    expect(events[0]).toEqual('create-failed')\n    expect(events[1]).toEqual('expired')\n    expect(sessions.length).toEqual(2)\n    expect(typeof sessions[0]).toEqual('undefined')\n    expect(typeof sessions[1]).toEqual('undefined')\n  })\n\n  it('does not modify the session on a failed refresh', async () => {\n    const events: { event: string; session: AtpSessionData | undefined }[] = []\n    const persistSession = (\n      event: AtpSessionEvent,\n      session?: AtpSessionData,\n    ) => {\n      events.push({ event, session: session && { ...session } })\n    }\n\n    const agent = new AtpAgent({ service: network.pds.url, persistSession })\n\n    // create an account and a session with it\n    await agent.createAccount({\n      handle: 'user6.test',\n      email: 'user6@test.com',\n      password: 'password',\n    })\n    if (!agent.session) {\n      throw new Error('No session created')\n    }\n    const session1 = agent.session\n    const origAccessJwt = session1.accessJwt\n\n    // patch the fetch handler to fake an expired token error on the next request\n    agent.sessionManager.setFetch(\n      async (input: RequestInfo | URL, init?: RequestInit) => {\n        const req = new Request(input, init)\n        if (req.headers.get('authorization') === `Bearer ${origAccessJwt}`) {\n          return new Response(JSON.stringify({ error: 'ExpiredToken' }), {\n            status: 400,\n            headers: { 'Content-Type': 'application/json' },\n          })\n        }\n        if (req.url.includes('com.atproto.server.refreshSession')) {\n          return new Response(undefined, { status: 500 })\n        }\n        return globalThis.fetch(req)\n      },\n    )\n\n    // put the agent through the auth flow\n    try {\n      await agent.app.bsky.feed.getTimeline()\n      throw new Error('Should have failed')\n    } catch (e: any) {\n      // the original error passes through\n      expect(e.status).toEqual(400)\n      expect(e.error).toEqual('ExpiredToken')\n    }\n\n    // still has session because it wasn't invalidated\n    expect(agent.hasSession).toEqual(true)\n\n    expect(events.length).toEqual(2)\n\n    expect(events[0].event).toEqual('create')\n    expect(events[0].session?.accessJwt).toEqual(origAccessJwt)\n    expect(events[1].event).toEqual('network-error')\n    expect(events[1].session?.accessJwt).toEqual(origAccessJwt)\n  })\n\n  describe('createAccount', () => {\n    it('persists an empty session on failure', async () => {\n      const events: string[] = []\n      const sessions: (AtpSessionData | undefined)[] = []\n      const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {\n        events.push(evt)\n        sessions.push(sess)\n      }\n\n      const agent = new AtpAgent({ service: network.pds.url, persistSession })\n\n      await expect(\n        agent.createAccount({\n          handle: '',\n          email: '',\n          password: 'password',\n        }),\n      ).rejects.toThrow()\n\n      expect(agent.hasSession).toEqual(false)\n      expect(agent.session).toEqual(undefined)\n      expect(events.length).toEqual(1)\n      expect(events[0]).toEqual('create-failed')\n      expect(sessions.length).toEqual(1)\n      expect(sessions[0]).toEqual(undefined)\n    })\n  })\n\n  describe('App labelers header', () => {\n    it('adds the labelers header as expected', async () => {\n      const server = await createHeaderEchoServer()\n      const port = (server.address() as AddressInfo).port\n      const agent = new AtpAgent({ service: `http://localhost:${port}` })\n      const agent2 = new AtpAgent({ service: `http://localhost:${port}` })\n\n      const res1 = await agent.com.atproto.server.describeServer()\n      expect(res1.data['atproto-accept-labelers']).toEqual(\n        `${BSKY_LABELER_DID};redact`,\n      )\n\n      AtpAgent.configure({ appLabelers: ['did:plc:test1', 'did:plc:test2'] })\n      const res2 = await agent.com.atproto.server.describeServer()\n      expect(res2.data['atproto-accept-labelers']).toEqual(\n        'did:plc:test1;redact, did:plc:test2;redact',\n      )\n      const res3 = await agent2.com.atproto.server.describeServer()\n      expect(res3.data['atproto-accept-labelers']).toEqual(\n        'did:plc:test1;redact, did:plc:test2;redact',\n      )\n      AtpAgent.configure({ appLabelers: [BSKY_LABELER_DID] })\n\n      await new Promise((r) => server.close(r))\n    })\n  })\n\n  describe('configureLabelers', () => {\n    it('adds the labelers header as expected', async () => {\n      const server = await createHeaderEchoServer()\n      const port = (server.address() as AddressInfo).port\n      const agent = new AtpAgent({ service: `http://localhost:${port}` })\n\n      agent.configureLabelers(['did:plc:test1'])\n      const res1 = await agent.com.atproto.server.describeServer()\n      expect(res1.data['atproto-accept-labelers']).toEqual(\n        `${BSKY_LABELER_DID};redact, did:plc:test1`,\n      )\n\n      agent.configureLabelers(['did:plc:test1', 'did:plc:test2'])\n      const res2 = await agent.com.atproto.server.describeServer()\n      expect(res2.data['atproto-accept-labelers']).toEqual(\n        `${BSKY_LABELER_DID};redact, did:plc:test1, did:plc:test2`,\n      )\n\n      await new Promise((r) => server.close(r))\n    })\n  })\n\n  describe('configureProxy', () => {\n    it('adds the proxy header as expected', async () => {\n      const server = await createHeaderEchoServer()\n      const port = (server.address() as AddressInfo).port\n      const agent = new AtpAgent({ service: `http://localhost:${port}` })\n\n      const res1 = await agent.com.atproto.server.describeServer()\n      expect(res1.data['atproto-proxy']).toBeFalsy()\n\n      agent.configureProxy('did:plc:test1#atproto_labeler')\n      const res2 = await agent.com.atproto.server.describeServer()\n      expect(res2.data['atproto-proxy']).toEqual(\n        'did:plc:test1#atproto_labeler',\n      )\n\n      const res3 = await agent\n        .withProxy('atproto_labeler', 'did:plc:test2')\n        .com.atproto.server.describeServer()\n      expect(res3.data['atproto-proxy']).toEqual(\n        'did:plc:test2#atproto_labeler',\n      )\n\n      await new Promise((r) => server.close(r))\n    })\n  })\n})\n\nconst createPost = async (agent: AtpAgent) => {\n  return agent.com.atproto.repo.createRecord({\n    repo: agent.accountDid,\n    collection: 'app.bsky.feed.post',\n    record: {\n      text: 'hello there',\n      createdAt: new Date().toISOString(),\n    },\n  })\n}\n"
  },
  {
    "path": "packages/api/tests/errors.test.ts",
    "content": "import { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { AtpAgent, ComAtprotoServerCreateAccount } from '..'\n\ndescribe('errors', () => {\n  let network: TestNetworkNoAppView\n  let client: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'known_errors',\n    })\n    client = network.pds.getClient()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('constructs the correct error instance', async () => {\n    const res = client.api.com.atproto.server.createAccount({\n      handle: 'admin.blah',\n      email: 'admin@test.com',\n      password: 'password',\n    })\n    await expect(res).rejects.toThrow(\n      ComAtprotoServerCreateAccount.UnsupportedDomainError,\n    )\n  })\n})\n"
  },
  {
    "path": "packages/api/tests/moderation-behaviors.test.ts",
    "content": "import { moderatePost, moderateProfile } from '../src'\nimport {\n  ModerationBehaviorSuiteRunner,\n  ModerationTestSuiteScenario,\n  SuiteConfigurations,\n  SuiteScenarios,\n  SuiteUsers,\n} from './util/moderation-behavior'\n\nconst USERS: SuiteUsers = {\n  self: {\n    blocking: false,\n    blockingByList: false,\n    blockedBy: false,\n    muted: false,\n    mutedByList: false,\n  },\n  alice: {\n    blocking: false,\n    blockingByList: false,\n    blockedBy: false,\n    muted: false,\n    mutedByList: false,\n  },\n  bob: {\n    blocking: true,\n    blockingByList: false,\n    blockedBy: false,\n    muted: false,\n    mutedByList: false,\n  },\n  carla: {\n    blocking: false,\n    blockingByList: false,\n    blockedBy: true,\n    muted: false,\n    mutedByList: false,\n  },\n  dan: {\n    blocking: false,\n    blockingByList: false,\n    blockedBy: false,\n    muted: true,\n    mutedByList: false,\n  },\n  elise: {\n    blocking: false,\n    blockingByList: false,\n    blockedBy: false,\n    muted: false,\n    mutedByList: true,\n  },\n  fern: {\n    blocking: true,\n    blockingByList: false,\n    blockedBy: true,\n    muted: false,\n    mutedByList: false,\n  },\n  georgia: {\n    blocking: false,\n    blockingByList: true,\n    blockedBy: false,\n    muted: false,\n    mutedByList: false,\n  },\n}\nconst CONFIGURATIONS: SuiteConfigurations = {\n  none: {},\n  'adult-disabled': {\n    adultContentEnabled: false,\n  },\n  'intolerant-hide': {\n    settings: { intolerance: 'hide' },\n  },\n  'intolerant-warn': {\n    settings: { intolerance: 'warn' },\n  },\n  'intolerant-ignore': {\n    settings: { intolerance: 'ignore' },\n  },\n  'porn-hide': {\n    adultContentEnabled: true,\n    settings: { porn: 'hide' },\n  },\n  'porn-warn': {\n    adultContentEnabled: true,\n    settings: { porn: 'warn' },\n  },\n  'porn-ignore': {\n    adultContentEnabled: true,\n    settings: { porn: 'ignore' },\n  },\n  'scam-hide': {\n    settings: { misrepresentation: 'hide' },\n  },\n  'scam-warn': {\n    settings: { misrepresentation: 'warn' },\n  },\n  'scam-ignore': {\n    settings: { misrepresentation: 'ignore' },\n  },\n  'intolerant-hide-scam-warn': {\n    settings: { intolerance: 'hide', misrepresentation: 'hide' },\n  },\n  'logged-out': {\n    authed: false,\n  },\n}\nconst SCENARIOS: SuiteScenarios = {\n  \"Imperative label ('!hide') on account\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!hide'] },\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['blur', 'noOverride'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!hide') on profile\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['!hide'] },\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!hide') on post\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['!hide'] },\n    behaviors: {\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!hide') on author profile\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['!hide'] },\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!hide') on author account\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['!hide'] },\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n\n  \"Imperative label ('!warn') on account\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!warn'] },\n    behaviors: {\n      profileList: ['blur'],\n      profileView: ['blur'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n  \"Imperative label ('!warn') on profile\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['!warn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n    },\n  },\n  \"Imperative label ('!warn') on post\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['!warn'] },\n    behaviors: {\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n  \"Imperative label ('!warn') on author profile\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['!warn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n    },\n  },\n  \"Imperative label ('!warn') on author account\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['!warn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n\n  \"Imperative label ('!no-unauthenticated') on account when logged out\": {\n    cfg: 'logged-out',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!no-unauthenticated'] },\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['blur', 'noOverride'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!no-unauthenticated') on profile when logged out\": {\n    cfg: 'logged-out',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['!no-unauthenticated'] },\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['blur', 'noOverride'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!no-unauthenticated') on post when logged out\": {\n    cfg: 'logged-out',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['!no-unauthenticated'] },\n    behaviors: {\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Imperative label ('!no-unauthenticated') on author profile when logged out\":\n    {\n      cfg: 'logged-out',\n      subject: 'post',\n      author: 'alice',\n      labels: { profile: ['!no-unauthenticated'] },\n      behaviors: {\n        avatar: ['blur', 'noOverride'],\n        banner: ['blur', 'noOverride'],\n        displayName: ['blur', 'noOverride'],\n        contentList: ['filter', 'blur', 'noOverride'],\n        contentView: ['blur', 'noOverride'],\n      },\n    },\n  \"Imperative label ('!no-unauthenticated') on author account when logged out\":\n    {\n      cfg: 'logged-out',\n      subject: 'post',\n      author: 'alice',\n      labels: { account: ['!no-unauthenticated'] },\n      behaviors: {\n        avatar: ['blur', 'noOverride'],\n        banner: ['blur', 'noOverride'],\n        displayName: ['blur', 'noOverride'],\n        contentList: ['filter', 'blur', 'noOverride'],\n        contentView: ['blur', 'noOverride'],\n      },\n    },\n\n  \"Imperative label ('!no-unauthenticated') on account when logged in\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!no-unauthenticated'] },\n    behaviors: {},\n  },\n  \"Imperative label ('!no-unauthenticated') on profile when logged in\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['!no-unauthenticated'] },\n    behaviors: {},\n  },\n  \"Imperative label ('!no-unauthenticated') on post when logged in\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['!no-unauthenticated'] },\n    behaviors: {},\n  },\n  \"Imperative label ('!no-unauthenticated') on author profile when logged in\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['!no-unauthenticated'] },\n    behaviors: {},\n  },\n  \"Imperative label ('!no-unauthenticated') on author account when logged in\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['!no-unauthenticated'] },\n    behaviors: {},\n  },\n\n  \"Blur-media label ('porn') on account (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {\n      profileList: ['filter'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['filter'],\n    },\n  },\n  \"Blur-media label ('porn') on profile (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on post (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['porn'] },\n    behaviors: {\n      contentList: ['filter'],\n      contentMedia: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on author profile (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on author account (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {\n      profileList: ['filter'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['filter'],\n    },\n  },\n\n  \"Blur-media label ('porn') on account (warn)\": {\n    cfg: 'porn-warn',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on profile (warn)\": {\n    cfg: 'porn-warn',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on post (warn)\": {\n    cfg: 'porn-warn',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['porn'] },\n    behaviors: {\n      contentMedia: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on author profile (warn)\": {\n    cfg: 'porn-warn',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n  },\n  \"Blur-media label ('porn') on author account (warn)\": {\n    cfg: 'porn-warn',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n  },\n\n  \"Blur-media label ('porn') on account (ignore)\": {\n    cfg: 'porn-ignore',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {},\n  },\n  \"Blur-media label ('porn') on profile (ignore)\": {\n    cfg: 'porn-ignore',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {},\n  },\n  \"Blur-media label ('porn') on post (ignore)\": {\n    cfg: 'porn-ignore',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['porn'] },\n    behaviors: {},\n  },\n  \"Blur-media label ('porn') on author profile (ignore)\": {\n    cfg: 'porn-ignore',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {},\n  },\n  \"Blur-media label ('porn') on author account (ignore)\": {\n    cfg: 'porn-ignore',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {},\n  },\n\n  'Adult-only label on account when adult content is disabled': {\n    cfg: 'adult-disabled',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {\n      profileList: ['filter'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter'],\n    },\n  },\n  'Adult-only label on profile when adult content is disabled': {\n    cfg: 'adult-disabled',\n    subject: 'profile',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {\n      profileList: [],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: [],\n    },\n  },\n  'Adult-only label on post when adult content is disabled': {\n    cfg: 'adult-disabled',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['porn'] },\n    behaviors: {\n      contentList: ['filter'],\n      contentMedia: ['blur', 'noOverride'],\n    },\n  },\n  'Adult-only label on author profile when adult content is disabled': {\n    cfg: 'adult-disabled',\n    subject: 'post',\n    author: 'alice',\n    labels: { profile: ['porn'] },\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: [],\n    },\n  },\n  'Adult-only label on author account when adult content is disabled': {\n    cfg: 'adult-disabled',\n    subject: 'post',\n    author: 'alice',\n    labels: { account: ['porn'] },\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter'],\n    },\n  },\n\n  'Self-profile: !hide on account': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'self',\n    labels: { account: ['!hide'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n      profileList: ['blur'],\n      profileView: ['blur'],\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n  'Self-profile: !hide on profile': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'self',\n    labels: { profile: ['!hide'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n    },\n  },\n\n  \"Self-post: Imperative label ('!hide') on post\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'self',\n    labels: { post: ['!hide'] },\n    behaviors: {\n      contentView: ['blur'],\n      contentList: ['blur'],\n    },\n  },\n  \"Self-post: Imperative label ('!hide') on author profile\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'self',\n    labels: { profile: ['!hide'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n    },\n  },\n  \"Self-post: Imperative label ('!hide') on author account\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'self',\n    labels: { account: ['!hide'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n\n  \"Self-post: Imperative label ('!warn') on post\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'self',\n    labels: { post: ['!warn'] },\n    behaviors: {\n      contentView: ['blur'],\n      contentList: ['blur'],\n    },\n  },\n  \"Self-post: Imperative label ('!warn') on author profile\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'self',\n    labels: { profile: ['!warn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      displayName: ['blur'],\n    },\n  },\n  \"Self-post: Imperative label ('!warn') on author account\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'self',\n    labels: { account: ['!warn'] },\n    behaviors: {\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n\n  'Mute/block: Blocking user': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'bob',\n    labels: {},\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['alert'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  'Post with blocked author': {\n    cfg: 'none',\n    subject: 'post',\n    author: 'bob',\n    labels: {},\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  'Post with author blocking user': {\n    cfg: 'none',\n    subject: 'post',\n    author: 'carla',\n    labels: {},\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n\n  'Mute/block: Blocking-by-list user': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'georgia',\n    labels: {},\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['alert'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n\n  'Mute/block: Blocked by user': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'carla',\n    labels: {},\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['alert'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n\n  'Mute/block: Muted user': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'dan',\n    labels: {},\n    behaviors: {\n      profileList: ['filter', 'inform'],\n      profileView: ['alert'],\n      contentList: ['filter', 'blur'],\n      contentView: ['inform'],\n    },\n  },\n\n  'Mute/block: Muted-by-list user': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'elise',\n    labels: {},\n    behaviors: {\n      profileList: ['filter', 'inform'],\n      profileView: ['alert'],\n      contentList: ['filter', 'blur'],\n      contentView: ['inform'],\n    },\n  },\n\n  'Merging: blocking & blocked-by user': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'fern',\n    labels: {},\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['alert'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n\n  'Post with muted author': {\n    cfg: 'none',\n    subject: 'post',\n    author: 'dan',\n    labels: {},\n    behaviors: {\n      contentList: ['filter', 'blur'],\n      contentView: ['inform'],\n    },\n  },\n\n  'Post with muted-by-list author': {\n    cfg: 'none',\n    subject: 'post',\n    author: 'elise',\n    labels: {},\n    behaviors: {\n      contentList: ['filter', 'blur'],\n      contentView: ['inform'],\n    },\n  },\n\n  \"Merging: '!hide' label on account of blocked user\": {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'bob',\n    labels: { account: ['!hide'] },\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['blur', 'alert', 'noOverride'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Merging: '!hide' and 'porn' labels on account (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!hide', 'porn'] },\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['blur', 'noOverride'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Merging: '!warn' and 'porn' labels on account (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!warn', 'porn'] },\n    behaviors: {\n      profileList: ['filter', 'blur'],\n      profileView: ['blur'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['filter', 'blur'],\n      contentView: ['blur'],\n    },\n  },\n  'Merging: !hide on account, !warn on profile': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!hide'], profile: ['!warn'] },\n    behaviors: {\n      profileList: ['filter', 'blur', 'noOverride'],\n      profileView: ['blur', 'noOverride'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  'Merging: !warn on account, !hide on profile': {\n    cfg: 'none',\n    subject: 'profile',\n    author: 'alice',\n    labels: { account: ['!warn'], profile: ['!hide'] },\n    behaviors: {\n      profileList: ['blur'],\n      profileView: ['blur'],\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      displayName: ['blur', 'noOverride'],\n      contentList: ['blur'],\n      contentView: ['blur'],\n    },\n  },\n  'Merging: post with blocking & blocked-by author': {\n    cfg: 'none',\n    subject: 'post',\n    author: 'fern',\n    labels: {},\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Merging: '!hide' label on post by blocked user\": {\n    cfg: 'none',\n    subject: 'post',\n    author: 'bob',\n    labels: { post: ['!hide'] },\n    behaviors: {\n      avatar: ['blur', 'noOverride'],\n      banner: ['blur', 'noOverride'],\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n    },\n  },\n  \"Merging: '!hide' and 'porn' labels on post (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['!hide', 'porn'] },\n    behaviors: {\n      contentList: ['filter', 'blur', 'noOverride'],\n      contentView: ['blur', 'noOverride'],\n      contentMedia: ['blur'],\n    },\n  },\n  \"Merging: '!warn' and 'porn' labels on post (hide)\": {\n    cfg: 'porn-hide',\n    subject: 'post',\n    author: 'alice',\n    labels: { post: ['!warn', 'porn'] },\n    behaviors: {\n      contentList: ['filter', 'blur'],\n      contentView: ['blur'],\n      contentMedia: ['blur'],\n    },\n  },\n}\n\nconst suite = new ModerationBehaviorSuiteRunner(\n  USERS,\n  CONFIGURATIONS,\n  SCENARIOS,\n)\n\ndescribe('Post moderation behaviors', () => {\n  const scenarios = Array.from(Object.entries(suite.scenarios)).filter(\n    ([name]) => !name.startsWith('//'),\n  )\n  it.each(scenarios)(\n    '%s',\n    (_name: string, scenario: ModerationTestSuiteScenario) => {\n      const res =\n        scenario.subject === 'profile'\n          ? moderateProfile(\n              suite.profileScenario(scenario),\n              suite.moderationOpts(scenario),\n            )\n          : moderatePost(\n              suite.postScenario(scenario),\n              suite.moderationOpts(scenario),\n            )\n      if (scenario.subject === 'profile') {\n        expect(res.ui('profileList')).toBeModerationResult(\n          scenario.behaviors.profileList,\n          'profileList',\n          JSON.stringify(res, null, 2),\n        )\n        expect(res.ui('profileView')).toBeModerationResult(\n          scenario.behaviors.profileView,\n          'profileView',\n          JSON.stringify(res, null, 2),\n        )\n      }\n      expect(res.ui('avatar')).toBeModerationResult(\n        scenario.behaviors.avatar,\n        'avatar',\n        JSON.stringify(res, null, 2),\n      )\n      expect(res.ui('banner')).toBeModerationResult(\n        scenario.behaviors.banner,\n        'banner',\n        JSON.stringify(res, null, 2),\n      )\n      expect(res.ui('displayName')).toBeModerationResult(\n        scenario.behaviors.displayName,\n        'displayName',\n        JSON.stringify(res, null, 2),\n      )\n      expect(res.ui('contentList')).toBeModerationResult(\n        scenario.behaviors.contentList,\n        'contentList',\n        JSON.stringify(res, null, 2),\n      )\n      expect(res.ui('contentView')).toBeModerationResult(\n        scenario.behaviors.contentView,\n        'contentView',\n        JSON.stringify(res, null, 2),\n      )\n      expect(res.ui('contentMedia')).toBeModerationResult(\n        scenario.behaviors.contentMedia,\n        'contentMedia',\n        JSON.stringify(res, null, 2),\n      )\n    },\n  )\n})\n"
  },
  {
    "path": "packages/api/tests/moderation-custom-labels.test.ts",
    "content": "import {\n  InterpretedLabelValueDefinition,\n  ModerationOpts,\n  interpretLabelValueDefinition,\n  mock,\n  moderatePost,\n  moderateProfile,\n} from '../src'\nimport './util/moderation-behavior'\n\ninterface ScenarioResult {\n  profileList?: string[]\n  profileView?: string[]\n  avatar?: string[]\n  banner?: string[]\n  displayName?: string[]\n  contentList?: string[]\n  contentView?: string[]\n  contentMedia?: string[]\n}\n\ninterface Scenario {\n  blurs: 'content' | 'media' | 'none'\n  severity: 'alert' | 'inform' | 'none'\n  account: ScenarioResult\n  profile: ScenarioResult\n  post: ScenarioResult\n}\n\nconst TESTS: Scenario[] = [\n  {\n    blurs: 'content',\n    severity: 'alert',\n    account: {\n      profileList: ['filter', 'alert'],\n      profileView: ['alert'],\n      contentList: ['filter', 'blur'],\n      contentView: ['alert'],\n    },\n    profile: {\n      profileList: ['alert'],\n      profileView: ['alert'],\n    },\n    post: {\n      contentList: ['filter', 'blur'],\n      contentView: ['alert'],\n    },\n  },\n  {\n    blurs: 'content',\n    severity: 'inform',\n    account: {\n      profileList: ['filter', 'inform'],\n      profileView: ['inform'],\n      contentList: ['filter', 'blur'],\n      contentView: ['inform'],\n    },\n    profile: {\n      profileList: ['inform'],\n      profileView: ['inform'],\n    },\n    post: {\n      contentList: ['filter', 'blur'],\n      contentView: ['inform'],\n    },\n  },\n  {\n    blurs: 'content',\n    severity: 'none',\n    account: {\n      profileList: ['filter'],\n      profileView: [],\n      contentList: ['filter', 'blur'],\n      contentView: [],\n    },\n    profile: {\n      profileList: [],\n      profileView: [],\n    },\n    post: {\n      contentList: ['filter', 'blur'],\n      contentView: [],\n    },\n  },\n\n  {\n    blurs: 'media',\n    severity: 'alert',\n    account: {\n      profileList: ['filter', 'alert'],\n      profileView: ['alert'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['filter'],\n    },\n    profile: {\n      profileList: ['alert'],\n      profileView: ['alert'],\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n    post: {\n      contentList: ['filter'],\n      contentMedia: ['blur'],\n    },\n  },\n  {\n    blurs: 'media',\n    severity: 'inform',\n    account: {\n      profileList: ['filter', 'inform'],\n      profileView: ['inform'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['filter'],\n    },\n    profile: {\n      profileList: ['inform'],\n      profileView: ['inform'],\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n    post: {\n      contentList: ['filter'],\n      contentMedia: ['blur'],\n    },\n  },\n  {\n    blurs: 'media',\n    severity: 'none',\n    account: {\n      profileList: ['filter'],\n      avatar: ['blur'],\n      banner: ['blur'],\n      contentList: ['filter'],\n    },\n    profile: {\n      avatar: ['blur'],\n      banner: ['blur'],\n    },\n    post: {\n      contentList: ['filter'],\n      contentMedia: ['blur'],\n    },\n  },\n\n  {\n    blurs: 'none',\n    severity: 'alert',\n    account: {\n      profileList: ['filter', 'alert'],\n      profileView: ['alert'],\n      contentList: ['filter', 'alert'],\n      contentView: ['alert'],\n    },\n    profile: {\n      profileList: ['alert'],\n      profileView: ['alert'],\n    },\n    post: {\n      contentList: ['filter', 'alert'],\n      contentView: ['alert'],\n    },\n  },\n  {\n    blurs: 'none',\n    severity: 'inform',\n    account: {\n      profileList: ['filter', 'inform'],\n      profileView: ['inform'],\n      contentList: ['filter', 'inform'],\n      contentView: ['inform'],\n    },\n    profile: {\n      profileList: ['inform'],\n      profileView: ['inform'],\n    },\n    post: {\n      contentList: ['filter', 'inform'],\n      contentView: ['inform'],\n    },\n  },\n  {\n    blurs: 'none',\n    severity: 'none',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n]\n\ndescribe('Moderation: custom labels', () => {\n  const scenarios = TESTS.flatMap((test) => [\n    {\n      blurs: test.blurs,\n      severity: test.severity,\n      target: 'post',\n      expected: test.post,\n    },\n    {\n      blurs: test.blurs,\n      severity: test.severity,\n      target: 'profile',\n      expected: test.profile,\n    },\n    {\n      blurs: test.blurs,\n      severity: test.severity,\n      target: 'account',\n      expected: test.account,\n    },\n  ])\n  it.each(scenarios)(\n    'blurs=$blurs, severity=$severity, target=$target',\n    ({ blurs, severity, target, expected }) => {\n      let res\n      if (target === 'post') {\n        res = moderatePost(\n          mock.postView({\n            record: {\n              $type: 'app.bsky.feed.post',\n              text: 'Hello',\n              createdAt: new Date().toISOString(),\n            },\n            author: mock.profileViewBasic({\n              handle: 'bob.test',\n              displayName: 'Bob',\n            }),\n            labels: [\n              mock.label({\n                val: 'custom',\n                uri: 'at://did:web:bob.test/app.bsky.feed.post/fake',\n                src: 'did:web:labeler.test',\n              }),\n            ],\n          }),\n          modOpts(blurs, severity),\n        )\n      } else if (target === 'profile') {\n        res = moderateProfile(\n          mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n            labels: [\n              mock.label({\n                val: 'custom',\n                uri: 'at://did:web:bob.test/app.bsky.actor.profile/self',\n                src: 'did:web:labeler.test',\n              }),\n            ],\n          }),\n          modOpts(blurs, severity),\n        )\n      } else {\n        res = moderateProfile(\n          mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n            labels: [\n              mock.label({\n                val: 'custom',\n                uri: 'did:web:bob.test',\n                src: 'did:web:labeler.test',\n              }),\n            ],\n          }),\n          modOpts(blurs, severity),\n        )\n      }\n      expect(res.ui('profileList')).toBeModerationResult(\n        expected.profileList || [],\n      )\n      expect(res.ui('profileView')).toBeModerationResult(\n        expected.profileView || [],\n      )\n      expect(res.ui('avatar')).toBeModerationResult(expected.avatar || [])\n      expect(res.ui('banner')).toBeModerationResult(expected.banner || [])\n      expect(res.ui('displayName')).toBeModerationResult(\n        expected.displayName || [],\n      )\n      expect(res.ui('contentList')).toBeModerationResult(\n        expected.contentList || [],\n      )\n      expect(res.ui('contentView')).toBeModerationResult(\n        expected.contentView || [],\n      )\n      expect(res.ui('contentMedia')).toBeModerationResult(\n        expected.contentMedia || [],\n      )\n    },\n  )\n})\n\nfunction modOpts(blurs: string, severity: string): ModerationOpts {\n  return {\n    userDid: 'did:web:alice.test',\n    prefs: {\n      adultContentEnabled: true,\n      labels: {},\n      labelers: [\n        {\n          did: 'did:web:labeler.test',\n          labels: { custom: 'hide' },\n        },\n      ],\n      mutedWords: [],\n      hiddenPosts: [],\n    },\n    labelDefs: {\n      'did:web:labeler.test': [makeCustomLabel(blurs, severity)],\n    },\n  }\n}\n\nfunction makeCustomLabel(\n  blurs: string,\n  severity: string,\n): InterpretedLabelValueDefinition {\n  return interpretLabelValueDefinition(\n    {\n      identifier: 'custom',\n      blurs,\n      severity,\n      defaultSetting: 'warn',\n      locales: [],\n    },\n    'did:web:labeler.test',\n  )\n}\n"
  },
  {
    "path": "packages/api/tests/moderation-mutewords.test.ts",
    "content": "import { RichText, mock, moderatePost } from '../src/'\nimport { matchMuteWords } from '../src/moderation/mutewords'\n\ndescribe(`matchMuteWords`, () => {\n  describe(`tags`, () => {\n    it(`match: outline tag`, () => {\n      const rt = new RichText({\n        text: `This is a post #inlineTag`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const muteWord = {\n        value: 'outlineTag',\n        targets: ['tag'],\n        actorTarget: 'all',\n      }\n      const match = matchMuteWords({\n        mutedWords: [muteWord],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: ['outlineTag'],\n      })\n\n      expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])\n    })\n\n    it(`match: inline tag`, () => {\n      const rt = new RichText({\n        text: `This is a post #inlineTag`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const muteWord = {\n        value: 'inlineTag',\n        targets: ['tag'],\n        actorTarget: 'all',\n      }\n      const match = matchMuteWords({\n        mutedWords: [muteWord],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: ['outlineTag'],\n      })\n\n      expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])\n    })\n\n    it(`match: content target matches inline tag`, () => {\n      const rt = new RichText({\n        text: `This is a post #inlineTag`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'inlineTag', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: ['outlineTag'],\n      })\n\n      expect(match).toBeTruthy()\n    })\n\n    it(`no match: only tag targets`, () => {\n      const rt = new RichText({\n        text: `This is a post`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'inlineTag', targets: ['tag'], actorTarget: 'all' },\n        ],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeUndefined()\n    })\n  })\n\n  describe(`early exits`, () => {\n    it(`match: single character 希`, () => {\n      /**\n       * @see https://bsky.app/profile/mukuuji.bsky.social/post/3klji4fvsdk2c\n       */\n      const rt = new RichText({\n        text: `改善希望です`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const muteWord = { value: '希', targets: ['content'], actorTarget: 'all' }\n      const match = matchMuteWords({\n        mutedWords: [muteWord],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])\n    })\n\n    it(`match: single char with length > 1 ☠︎`, () => {\n      const rt = new RichText({\n        text: `Idk why ☠︎ but maybe`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: '☠︎', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeTruthy()\n    })\n\n    it(`no match: long muted word, short post`, () => {\n      const rt = new RichText({\n        text: `hey`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'politics', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeUndefined()\n    })\n\n    it(`match: exact text`, () => {\n      const rt = new RichText({\n        text: `javascript`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'javascript', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeTruthy()\n    })\n  })\n\n  describe(`general content`, () => {\n    it(`match: word within post`, () => {\n      const rt = new RichText({\n        text: `This is a post about javascript`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const muteWord = {\n        value: 'javascript',\n        targets: ['content'],\n        actorTarget: 'all',\n      }\n      const match = matchMuteWords({\n        mutedWords: [muteWord],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toEqual([{ word: muteWord, predicate: muteWord.value }])\n    })\n\n    it(`no match: partial word`, () => {\n      const rt = new RichText({\n        text: `Use your brain, Eric`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [{ value: 'ai', targets: ['content'], actorTarget: 'all' }],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeUndefined()\n    })\n\n    it(`match: multiline`, () => {\n      const rt = new RichText({\n        text: `Use your\\n\\tbrain, Eric`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'brain', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeTruthy()\n    })\n\n    it(`match: :)`, () => {\n      const rt = new RichText({\n        text: `So happy :)`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const match = matchMuteWords({\n        mutedWords: [{ value: `:)`, targets: ['content'], actorTarget: 'all' }],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toBeTruthy()\n    })\n  })\n\n  describe(`punctuation semi-fuzzy`, () => {\n    describe(`yay!`, () => {\n      const rt = new RichText({\n        text: `We're federating, yay!`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: yay!`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'yay!', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: yay`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'yay', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`y!ppee!!`, () => {\n      const rt = new RichText({\n        text: `We're federating, y!ppee!!`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: y!ppee`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'y!ppee', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      // single exclamation point, source has double\n      it(`no match: y!ppee!`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'y!ppee!', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`apostrophes: Bluesky's`, () => {\n      const rt = new RichText({\n        text: `Yay, Bluesky's mutewords work`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: Bluesky's`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `Bluesky's`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: Bluesky`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'Bluesky', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: bluesky`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'bluesky', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: blueskys`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'blueskys', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`Why so S@assy?`, () => {\n      const rt = new RichText({\n        text: `Why so S@assy?`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: S@assy`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 'S@assy', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: s@assy`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: 's@assy', targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`New York Times`, () => {\n      const rt = new RichText({\n        text: `New York Times`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      // case insensitive\n      it(`match: new york times`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: 'new york times',\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`!command`, () => {\n      const rt = new RichText({\n        text: `Idk maybe a bot !command`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: !command`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `!command`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: command`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `command`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`no match: !command`, () => {\n        const rt = new RichText({\n          text: `Idk maybe a bot command`,\n        })\n        rt.detectFacetsWithoutResolution()\n\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `!command`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeUndefined()\n      })\n    })\n\n    describe(`and/or`, () => {\n      const rt = new RichText({\n        text: `Tomatoes are fruits and/or vegetables`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: and/or`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `and/or`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`no match: Andor`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `Andor`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeUndefined()\n      })\n    })\n\n    describe(`super-bad`, () => {\n      const rt = new RichText({\n        text: `I'm super-bad`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: super-bad`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `super-bad`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: super`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `super`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: bad`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `bad`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: super bad`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `super bad`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: superbad`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `superbad`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`idk_what_this_would_be`, () => {\n      const rt = new RichText({\n        text: `Weird post with idk_what_this_would_be`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: idk what this would be`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: `idk what this would be`,\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`no match: idk what this would be for`, () => {\n        // extra word\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: `idk what this would be for`,\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeUndefined()\n      })\n\n      it(`match: idk`, () => {\n        // extra word\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `idk`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: idkwhatthiswouldbe`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: `idkwhatthiswouldbe`,\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`parentheses`, () => {\n      const rt = new RichText({\n        text: `Post with context(iykyk)`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: context(iykyk)`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: `context(iykyk)`,\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: context`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `context`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: iykyk`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `iykyk`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: (iykyk)`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `(iykyk)`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n\n    describe(`🦋`, () => {\n      const rt = new RichText({\n        text: `Post with 🦋`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: 🦋`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            { value: `🦋`, targets: ['content'], actorTarget: 'all' },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n  })\n\n  describe(`phrases`, () => {\n    describe(`I like turtles, or how I learned to stop worrying and love the internet.`, () => {\n      const rt = new RichText({\n        text: `I like turtles, or how I learned to stop worrying and love the internet.`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      it(`match: stop worrying`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: 'stop worrying',\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n\n      it(`match: turtles, or how`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: 'turtles, or how',\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n  })\n\n  describe(`languages without spaces`, () => {\n    // I love turtles, or how I learned to stop worrying and love the internet\n    describe(`私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`, () => {\n      const rt = new RichText({\n        text: `私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      // internet\n      it(`match: インターネット`, () => {\n        const match = matchMuteWords({\n          mutedWords: [\n            {\n              value: 'インターネット',\n              targets: ['content'],\n              actorTarget: 'all',\n            },\n          ],\n          text: rt.text,\n          facets: rt.facets,\n          outlineTags: [],\n          languages: ['ja'],\n        })\n\n        expect(match).toBeTruthy()\n      })\n    })\n  })\n\n  describe(`facet with multiple features`, () => {\n    it(`multiple tags`, () => {\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'bad', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: 'tags',\n        facets: [\n          {\n            features: [\n              {\n                $type: 'app.bsky.richtext.facet#tag',\n                tag: 'good',\n              },\n              {\n                $type: 'app.bsky.richtext.facet#tag',\n                tag: 'bad',\n              },\n            ],\n            index: {\n              byteEnd: 4,\n              byteStart: 0,\n            },\n          },\n        ],\n      })\n      expect(match).toBeTruthy()\n    })\n\n    it(`other features`, () => {\n      const match = matchMuteWords({\n        mutedWords: [\n          { value: 'bad', targets: ['content'], actorTarget: 'all' },\n        ],\n        text: 'test',\n        facets: [\n          {\n            features: [\n              {\n                $type: 'com.example.richtext.facet#other',\n                // @ts-expect-error\n                foo: 'bar',\n              },\n              {\n                $type: 'app.bsky.richtext.facet#tag',\n                tag: 'bad',\n              },\n            ],\n            index: {\n              byteEnd: 4,\n              byteStart: 0,\n            },\n          },\n        ],\n      })\n      expect(match).toBeTruthy()\n    })\n  })\n\n  describe(`doesn't mute own post`, () => {\n    it(`does mute if it isn't own post`, () => {\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: 'Mute words!',\n          }),\n          author: mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n          }),\n          labels: [],\n        }),\n        {\n          userDid: 'did:web:alice.test',\n          prefs: {\n            adultContentEnabled: false,\n            labels: {},\n            labelers: [],\n            mutedWords: [\n              { value: 'words', targets: ['content'], actorTarget: 'all' },\n            ],\n            hiddenPosts: [],\n          },\n          labelDefs: {},\n        },\n      )\n      expect(res.causes[0].type).toBe('mute-word')\n    })\n\n    it(`doesn't mute own post when muted word is in text`, () => {\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: 'Mute words!',\n          }),\n          author: mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n          }),\n          labels: [],\n        }),\n        {\n          userDid: 'did:web:bob.test',\n          prefs: {\n            adultContentEnabled: false,\n            labels: {},\n            labelers: [],\n            mutedWords: [\n              { value: 'words', targets: ['content'], actorTarget: 'all' },\n            ],\n            hiddenPosts: [],\n          },\n          labelDefs: {},\n        },\n      )\n      expect(res.causes.length).toBe(0)\n    })\n\n    it(`doesn't mute own post when muted word is in tags`, () => {\n      const rt = new RichText({\n        text: `Mute #words!`,\n      })\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: rt.text,\n            facets: rt.facets,\n          }),\n          author: mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n          }),\n          labels: [],\n        }),\n        {\n          userDid: 'did:web:bob.test',\n          prefs: {\n            adultContentEnabled: false,\n            labels: {},\n            labelers: [],\n            mutedWords: [\n              { value: 'words', targets: ['tags'], actorTarget: 'all' },\n            ],\n            hiddenPosts: [],\n          },\n          labelDefs: {},\n        },\n      )\n      expect(res.causes.length).toBe(0)\n    })\n  })\n\n  describe(`timed mute words`, () => {\n    it(`non-expired word`, () => {\n      const now = Date.now()\n\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: 'Mute words!',\n          }),\n          author: mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n          }),\n          labels: [],\n        }),\n        {\n          userDid: 'did:web:alice.test',\n          prefs: {\n            adultContentEnabled: false,\n            labels: {},\n            labelers: [],\n            mutedWords: [\n              {\n                value: 'words',\n                targets: ['content'],\n                expiresAt: new Date(now + 1e3).toISOString(),\n                actorTarget: 'all',\n              },\n            ],\n            hiddenPosts: [],\n          },\n          labelDefs: {},\n        },\n      )\n\n      expect(res.causes[0].type).toBe('mute-word')\n    })\n\n    it(`expired word`, () => {\n      const now = Date.now()\n\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: 'Mute words!',\n          }),\n          author: mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n          }),\n          labels: [],\n        }),\n        {\n          userDid: 'did:web:alice.test',\n          prefs: {\n            adultContentEnabled: false,\n            labels: {},\n            labelers: [],\n            mutedWords: [\n              {\n                value: 'words',\n                targets: ['content'],\n                expiresAt: new Date(now - 1e3).toISOString(),\n                actorTarget: 'all',\n              },\n            ],\n            hiddenPosts: [],\n          },\n          labelDefs: {},\n        },\n      )\n\n      expect(res.causes.length).toBe(0)\n    })\n  })\n\n  describe(`actor-based mute words`, () => {\n    const viewer = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: false,\n        labels: {},\n        labelers: [],\n        mutedWords: [\n          {\n            value: 'words',\n            targets: ['content'],\n            actorTarget: 'exclude-following',\n          },\n        ],\n        hiddenPosts: [],\n      },\n      labelDefs: {},\n    }\n\n    it(`followed actor`, () => {\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: 'Mute words!',\n          }),\n          author: mock.profileViewBasic({\n            handle: 'bob.test',\n            displayName: 'Bob',\n            viewer: {\n              following: 'true',\n            },\n          }),\n          labels: [],\n        }),\n        viewer,\n      )\n      expect(res.causes.length).toBe(0)\n    })\n\n    it(`non-followed actor`, () => {\n      const res = moderatePost(\n        mock.postView({\n          record: mock.post({\n            text: 'Mute words!',\n          }),\n          author: mock.profileViewBasic({\n            handle: 'carla.test',\n            displayName: 'Carla',\n            viewer: {\n              following: undefined,\n            },\n          }),\n          labels: [],\n        }),\n        viewer,\n      )\n      expect(res.causes[0].type).toBe('mute-word')\n    })\n  })\n\n  describe(`returning MuteWordMatch`, () => {\n    it(`matches all`, () => {\n      const rt = new RichText({\n        text: `This is a post about javascript`,\n      })\n      rt.detectFacetsWithoutResolution()\n\n      const muteWord1 = {\n        value: 'post',\n        targets: ['content'],\n        actorTarget: 'all',\n      }\n      const muteWord2 = {\n        value: 'javascript',\n        targets: ['content'],\n        actorTarget: 'all',\n      }\n      const match = matchMuteWords({\n        mutedWords: [muteWord1, muteWord2],\n        text: rt.text,\n        facets: rt.facets,\n        outlineTags: [],\n      })\n\n      expect(match).toEqual([\n        { word: muteWord1, predicate: muteWord1.value },\n        { word: muteWord2, predicate: muteWord2.value },\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/api/tests/moderation-prefs.test.ts",
    "content": "import { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { DEFAULT_LABEL_SETTINGS } from '../src'\nimport { isContentLabelPref } from '../src/client/types/app/bsky/actor/defs'\nimport './util/moderation-behavior'\n\ndescribe('agent', () => {\n  let network: TestNetworkNoAppView\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'api_moderation_prefs',\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('migrates legacy content-label prefs (no mutations)', async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user1.test',\n      email: 'user1@test.com',\n      password: 'password',\n    })\n\n    await agent.app.bsky.actor.putPreferences({\n      preferences: [\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'porn',\n          visibility: 'show',\n        },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'nudity',\n          visibility: 'show',\n        },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'sexual',\n          visibility: 'show',\n        },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'graphic-media',\n          visibility: 'show',\n        },\n      ],\n    })\n    await expect(agent.getPreferences()).resolves.toStrictEqual({\n      feeds: {\n        pinned: undefined,\n        saved: undefined,\n      },\n      savedFeeds: expect.any(Array),\n      interests: { tags: [] },\n      moderationPrefs: {\n        adultContentEnabled: false,\n        labels: {\n          porn: 'ignore',\n          nudity: 'ignore',\n          sexual: 'ignore',\n          'graphic-media': 'ignore',\n        },\n        labelers: [...agent.appLabelers.map((did) => ({ did, labels: {} }))],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      birthDate: undefined,\n      feedViewPrefs: {\n        home: {\n          hideQuotePosts: false,\n          hideReplies: false,\n          hideRepliesByLikeCount: 0,\n          hideRepliesByUnfollowed: true,\n          hideReposts: false,\n        },\n      },\n      threadViewPrefs: {\n        sort: 'hotness',\n      },\n      bskyAppState: {\n        activeProgressGuide: undefined,\n        queuedNudges: [],\n        nuxs: [],\n      },\n      postInteractionSettings: {\n        threadgateAllowRules: undefined,\n        postgateEmbeddingRules: undefined,\n      },\n      verificationPrefs: {\n        hideBadges: false,\n      },\n      liveEventPreferences: {\n        hiddenFeedIds: [],\n        hideAllFeeds: false,\n      },\n    })\n  })\n\n  it('adds/removes moderation services', async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user5.test',\n      email: 'user5@test.com',\n      password: 'password',\n    })\n\n    await agent.addLabeler('did:plc:other')\n    expect(agent.labelers).toStrictEqual(['did:plc:other'])\n    await expect(agent.getPreferences()).resolves.toStrictEqual({\n      feeds: { pinned: undefined, saved: undefined },\n      savedFeeds: expect.any(Array),\n      interests: { tags: [] },\n      moderationPrefs: {\n        adultContentEnabled: false,\n        labels: DEFAULT_LABEL_SETTINGS,\n        labelers: [\n          ...agent.appLabelers.map((did) => ({ did, labels: {} })),\n          {\n            did: 'did:plc:other',\n            labels: {},\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      birthDate: undefined,\n      feedViewPrefs: {\n        home: {\n          hideReplies: false,\n          hideRepliesByUnfollowed: true,\n          hideRepliesByLikeCount: 0,\n          hideReposts: false,\n          hideQuotePosts: false,\n        },\n      },\n      threadViewPrefs: {\n        sort: 'hotness',\n      },\n      bskyAppState: {\n        activeProgressGuide: undefined,\n        queuedNudges: [],\n        nuxs: [],\n      },\n      postInteractionSettings: {\n        threadgateAllowRules: undefined,\n        postgateEmbeddingRules: undefined,\n      },\n      verificationPrefs: {\n        hideBadges: false,\n      },\n      liveEventPreferences: {\n        hiddenFeedIds: [],\n        hideAllFeeds: false,\n      },\n    })\n    expect(agent.labelers).toStrictEqual(['did:plc:other'])\n\n    await agent.removeLabeler('did:plc:other')\n    expect(agent.labelers).toStrictEqual([])\n    await expect(agent.getPreferences()).resolves.toStrictEqual({\n      feeds: { pinned: undefined, saved: undefined },\n      savedFeeds: expect.any(Array),\n      interests: { tags: [] },\n      moderationPrefs: {\n        adultContentEnabled: false,\n        labels: DEFAULT_LABEL_SETTINGS,\n        labelers: [...agent.appLabelers.map((did) => ({ did, labels: {} }))],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      birthDate: undefined,\n      feedViewPrefs: {\n        home: {\n          hideReplies: false,\n          hideRepliesByUnfollowed: true,\n          hideRepliesByLikeCount: 0,\n          hideReposts: false,\n          hideQuotePosts: false,\n        },\n      },\n      threadViewPrefs: {\n        sort: 'hotness',\n      },\n      bskyAppState: {\n        activeProgressGuide: undefined,\n        queuedNudges: [],\n        nuxs: [],\n      },\n      postInteractionSettings: {\n        threadgateAllowRules: undefined,\n        postgateEmbeddingRules: undefined,\n      },\n      verificationPrefs: {\n        hideBadges: false,\n      },\n      liveEventPreferences: {\n        hiddenFeedIds: [],\n        hideAllFeeds: false,\n      },\n    })\n    expect(agent.labelers).toStrictEqual([])\n  })\n\n  it('sets label preferences globally and per-moderator', async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user7.test',\n      email: 'user7@test.com',\n      password: 'password',\n    })\n\n    await agent.addLabeler('did:plc:other')\n    await agent.setContentLabelPref('porn', 'ignore')\n    await agent.setContentLabelPref('porn', 'hide', 'did:plc:other')\n    await agent.setContentLabelPref('x-custom', 'warn', 'did:plc:other')\n\n    await expect(agent.getPreferences()).resolves.toStrictEqual({\n      feeds: { pinned: undefined, saved: undefined },\n      savedFeeds: expect.any(Array),\n      interests: { tags: [] },\n      moderationPrefs: {\n        adultContentEnabled: false,\n        labels: { ...DEFAULT_LABEL_SETTINGS, porn: 'ignore', nsfw: 'ignore' },\n        labelers: [\n          ...agent.appLabelers.map((did) => ({ did, labels: {} })),\n          {\n            did: 'did:plc:other',\n            labels: {\n              porn: 'hide',\n              'x-custom': 'warn',\n            },\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      birthDate: undefined,\n      feedViewPrefs: {\n        home: {\n          hideReplies: false,\n          hideRepliesByUnfollowed: true,\n          hideRepliesByLikeCount: 0,\n          hideReposts: false,\n          hideQuotePosts: false,\n        },\n      },\n      threadViewPrefs: {\n        sort: 'hotness',\n      },\n      bskyAppState: {\n        activeProgressGuide: undefined,\n        queuedNudges: [],\n        nuxs: [],\n      },\n      postInteractionSettings: {\n        threadgateAllowRules: undefined,\n        postgateEmbeddingRules: undefined,\n      },\n      verificationPrefs: {\n        hideBadges: false,\n      },\n      liveEventPreferences: {\n        hiddenFeedIds: [],\n        hideAllFeeds: false,\n      },\n    })\n  })\n\n  it(`updates label pref`, async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user8.test',\n      email: 'user8@test.com',\n      password: 'password',\n    })\n\n    await agent.addLabeler('did:plc:other')\n    await agent.setContentLabelPref('porn', 'ignore')\n    await agent.setContentLabelPref('porn', 'ignore', 'did:plc:other')\n    await agent.setContentLabelPref('porn', 'hide')\n    await agent.setContentLabelPref('porn', 'hide', 'did:plc:other')\n\n    const { moderationPrefs } = await agent.getPreferences()\n    const labeler = moderationPrefs.labelers.find(\n      (l) => l.did === 'did:plc:other',\n    )\n\n    expect(moderationPrefs.labels.porn).toEqual('hide')\n    expect(labeler?.labels?.porn).toEqual('hide')\n  })\n\n  it(`double-write for legacy: 'graphic-media' in sync with 'gore'`, async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user9.test',\n      email: 'user9@test.com',\n      password: 'password',\n    })\n\n    await agent.setContentLabelPref('graphic-media', 'hide')\n    const a = await agent.getPreferences()\n\n    expect(a.moderationPrefs.labels.gore).toEqual('hide')\n    expect(a.moderationPrefs.labels['graphic-media']).toEqual('hide')\n\n    await agent.setContentLabelPref('graphic-media', 'warn')\n    const b = await agent.getPreferences()\n\n    expect(b.moderationPrefs.labels.gore).toEqual('warn')\n    expect(b.moderationPrefs.labels['graphic-media']).toEqual('warn')\n  })\n\n  it(`double-write for legacy: 'porn' in sync with 'nsfw'`, async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user10.test',\n      email: 'user10@test.com',\n      password: 'password',\n    })\n\n    await agent.setContentLabelPref('porn', 'hide')\n    const a = await agent.getPreferences()\n\n    expect(a.moderationPrefs.labels.nsfw).toEqual('hide')\n    expect(a.moderationPrefs.labels.porn).toEqual('hide')\n\n    await agent.setContentLabelPref('porn', 'warn')\n    const b = await agent.getPreferences()\n\n    expect(b.moderationPrefs.labels.nsfw).toEqual('warn')\n    expect(b.moderationPrefs.labels.porn).toEqual('warn')\n  })\n\n  it(`double-write for legacy: 'sexual' in sync with 'suggestive'`, async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user11.test',\n      email: 'user11@test.com',\n      password: 'password',\n    })\n\n    await agent.setContentLabelPref('sexual', 'hide')\n    const a = await agent.getPreferences()\n\n    expect(a.moderationPrefs.labels.sexual).toEqual('hide')\n    expect(a.moderationPrefs.labels.suggestive).toEqual('hide')\n\n    await agent.setContentLabelPref('sexual', 'warn')\n    const b = await agent.getPreferences()\n\n    expect(b.moderationPrefs.labels.sexual).toEqual('warn')\n    expect(b.moderationPrefs.labels.suggestive).toEqual('warn')\n  })\n\n  it(`double-write for legacy: filters out existing old label pref if double-written`, async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user12.test',\n      email: 'user12@test.com',\n      password: 'password',\n    })\n\n    await agent.setContentLabelPref('nsfw', 'hide')\n    await agent.setContentLabelPref('porn', 'hide')\n    const a = await agent.app.bsky.actor.getPreferences({})\n\n    const nsfwSettings = a.data.preferences.filter(\n      (pref) => isContentLabelPref(pref) && pref.label === 'nsfw',\n    )\n    expect(nsfwSettings.length).toEqual(1)\n  })\n\n  it(`remaps old values to new on read`, async () => {\n    const agent = network.pds.getClient()\n\n    await agent.createAccount({\n      handle: 'user13.test',\n      email: 'user13@test.com',\n      password: 'password',\n    })\n\n    await agent.setContentLabelPref('nsfw', 'hide')\n    await agent.setContentLabelPref('gore', 'hide')\n    await agent.setContentLabelPref('suggestive', 'hide')\n    const a = await agent.getPreferences()\n\n    expect(a.moderationPrefs.labels.porn).toEqual('hide')\n    expect(a.moderationPrefs.labels['graphic-media']).toEqual('hide')\n    expect(a.moderationPrefs.labels['sexual']).toEqual('hide')\n  })\n})\n"
  },
  {
    "path": "packages/api/tests/moderation-quoteposts.test.ts",
    "content": "import {\n  InterpretedLabelValueDefinition,\n  ModerationOpts,\n  interpretLabelValueDefinition,\n  mock,\n  moderatePost,\n} from '../src'\nimport './util/moderation-behavior'\n\ninterface ScenarioResult {\n  profileList?: string[]\n  profileView?: string[]\n  avatar?: string[]\n  banner?: string[]\n  displayName?: string[]\n  contentList?: string[]\n  contentView?: string[]\n  contentMedia?: string[]\n}\n\ninterface Scenario {\n  blurs: 'content' | 'media' | 'none'\n  severity: 'alert' | 'inform' | 'none'\n  account: ScenarioResult\n  profile: ScenarioResult\n  post: ScenarioResult\n}\n\nconst TESTS: Scenario[] = [\n  {\n    blurs: 'content',\n    severity: 'alert',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n  {\n    blurs: 'content',\n    severity: 'inform',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n  {\n    blurs: 'content',\n    severity: 'none',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n\n  {\n    blurs: 'media',\n    severity: 'alert',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n  {\n    blurs: 'media',\n    severity: 'inform',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n  {\n    blurs: 'media',\n    severity: 'none',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n\n  {\n    blurs: 'none',\n    severity: 'alert',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n  {\n    blurs: 'none',\n    severity: 'inform',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n  {\n    blurs: 'none',\n    severity: 'none',\n    account: {\n      profileList: ['filter'],\n      contentList: ['filter'],\n    },\n    profile: {},\n    post: {\n      contentList: ['filter'],\n    },\n  },\n]\n\ndescribe('Moderation: custom labels', () => {\n  const scenarios = TESTS.flatMap((test) => [\n    {\n      blurs: test.blurs,\n      severity: test.severity,\n      target: 'post',\n      expected: test.post,\n    },\n    {\n      blurs: test.blurs,\n      severity: test.severity,\n      target: 'profile',\n      expected: test.profile,\n    },\n    {\n      blurs: test.blurs,\n      severity: test.severity,\n      target: 'account',\n      expected: test.account,\n    },\n  ])\n  it.each(scenarios)(\n    'blurs=$blurs, severity=$severity, target=$target',\n    ({ blurs, severity, target, expected }) => {\n      let postLabels\n      let profileLabels\n      if (target === 'post') {\n        postLabels = [\n          mock.label({\n            val: 'custom',\n            uri: 'at://did:web:carla.test/app.bsky.feed.post/fake',\n            src: 'did:web:labeler.test',\n          }),\n        ]\n      } else if (target === 'profile') {\n        profileLabels = [\n          mock.label({\n            val: 'custom',\n            uri: 'at://did:web:carla.test/app.bsky.actor.profile/self',\n            src: 'did:web:labeler.test',\n          }),\n        ]\n      } else {\n        profileLabels = [\n          mock.label({\n            val: 'custom',\n            uri: 'did:web:carla.test',\n            src: 'did:web:labeler.test',\n          }),\n        ]\n      }\n\n      const post = mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        embed: mock.embedRecordView({\n          record: mock.post({\n            text: 'Quoted post text',\n          }),\n          labels: postLabels,\n          author: mock.profileViewBasic({\n            handle: 'carla.test',\n            displayName: 'Carla',\n            labels: profileLabels,\n          }),\n        }),\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n      })\n      const res = moderatePost(post, modOpts(blurs, severity))\n\n      expect(res.ui('profileList')).toBeModerationResult(\n        expected.profileList || [],\n      )\n      expect(res.ui('profileView')).toBeModerationResult(\n        expected.profileView || [],\n      )\n      expect(res.ui('avatar')).toBeModerationResult(expected.avatar || [])\n      expect(res.ui('banner')).toBeModerationResult(expected.banner || [])\n      expect(res.ui('displayName')).toBeModerationResult(\n        expected.displayName || [],\n      )\n      expect(res.ui('contentList')).toBeModerationResult(\n        expected.contentList || [],\n      )\n      expect(res.ui('contentView')).toBeModerationResult(\n        expected.contentView || [],\n      )\n      expect(res.ui('contentMedia')).toBeModerationResult(\n        expected.contentMedia || [],\n      )\n    },\n  )\n})\n\nfunction modOpts(blurs: string, severity: string): ModerationOpts {\n  return {\n    userDid: 'did:web:alice.test',\n    prefs: {\n      adultContentEnabled: true,\n      labels: {},\n      labelers: [\n        {\n          did: 'did:web:labeler.test',\n          labels: { custom: 'hide' },\n        },\n      ],\n      mutedWords: [],\n      hiddenPosts: [],\n    },\n    labelDefs: {\n      'did:web:labeler.test': [makeCustomLabel(blurs, severity)],\n    },\n  }\n}\n\nfunction makeCustomLabel(\n  blurs: string,\n  severity: string,\n): InterpretedLabelValueDefinition {\n  return interpretLabelValueDefinition(\n    {\n      identifier: 'custom',\n      blurs,\n      severity,\n      defaultSetting: 'warn',\n      locales: [],\n    },\n    'did:web:labeler.test',\n  )\n}\n"
  },
  {
    "path": "packages/api/tests/moderation.test.ts",
    "content": "import { ModerationOpts } from '../dist'\nimport {\n  interpretLabelValueDefinition,\n  mock,\n  moderatePost,\n  moderateProfile,\n} from '../src'\nimport './util/moderation-behavior'\n\ndescribe('Moderation', () => {\n  it('Applies self-labels on profiles according to the global preferences', () => {\n    // porn (hide)\n    const res1 = moderateProfile(\n      mock.profileViewBasic({\n        handle: 'bob.test',\n        displayName: 'Bob',\n        labels: [\n          {\n            src: 'did:web:bob.test',\n            uri: 'at://did:web:bob.test/app.bsky.actor.profile/self',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      {\n        userDid: 'did:web:alice.test',\n        prefs: {\n          adultContentEnabled: true,\n          labels: {\n            porn: 'hide',\n          },\n          labelers: [],\n          hiddenPosts: [],\n          mutedWords: [],\n        },\n      },\n    )\n    expect(res1.ui('avatar')).toBeModerationResult(\n      ['blur'],\n      'post avatar',\n      JSON.stringify(res1, null, 2),\n      true,\n    )\n\n    // porn (ignore)\n    const res2 = moderateProfile(\n      mock.profileViewBasic({\n        handle: 'bob.test',\n        displayName: 'Bob',\n        labels: [\n          {\n            src: 'did:web:bob.test',\n            uri: 'at://did:web:bob.test/app.bsky.actor.profile/self',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      {\n        userDid: 'did:web:alice.test',\n        prefs: {\n          adultContentEnabled: true,\n          labels: {\n            porn: 'ignore',\n          },\n          labelers: [],\n          hiddenPosts: [],\n          mutedWords: [],\n        },\n      },\n    )\n    expect(res2.ui('avatar')).toBeModerationResult(\n      [],\n      'post avatar',\n      JSON.stringify(res1, null, 2),\n      true,\n    )\n  })\n\n  it('Ignores labels from unsubscribed moderators or ignored labels for a moderator', () => {\n    // porn (moderator disabled)\n    const res1 = moderateProfile(\n      mock.profileViewBasic({\n        handle: 'bob.test',\n        displayName: 'Bob',\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.actor.profile/self',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      {\n        userDid: 'did:web:alice.test',\n        prefs: {\n          adultContentEnabled: true,\n          labels: {\n            porn: 'hide',\n          },\n          labelers: [],\n          hiddenPosts: [],\n          mutedWords: [],\n        },\n      },\n    )\n    for (const k of [\n      'profileList',\n      'profileView',\n      'avatar',\n      'banner',\n      'displayName',\n      'contentList',\n      'contentView',\n      'contentMedia',\n    ] as const) {\n      expect(res1.ui(k)).toBeModerationResult(\n        [],\n        k,\n        JSON.stringify(res1, null, 2),\n      )\n    }\n\n    // porn (label group disabled)\n    const res2 = moderateProfile(\n      mock.profileViewBasic({\n        handle: 'bob.test',\n        displayName: 'Bob',\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.actor.profile/self',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      {\n        userDid: 'did:web:alice.test',\n        prefs: {\n          adultContentEnabled: true,\n          labels: {\n            porn: 'ignore',\n          },\n          labelers: [\n            {\n              did: 'did:web:labeler.test',\n              labels: { porn: 'ignore' },\n            },\n          ],\n          hiddenPosts: [],\n          mutedWords: [],\n        },\n      },\n    )\n    for (const k of [\n      'profileList',\n      'profileView',\n      'avatar',\n      'banner',\n      'displayName',\n      'contentList',\n      'contentView',\n      'contentMedia',\n    ] as const) {\n      expect(res2.ui(k)).toBeModerationResult(\n        [],\n        k,\n        JSON.stringify(res2, null, 2),\n      )\n    }\n  })\n\n  it('Can manually apply hiding', () => {\n    const res1 = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [],\n      }),\n      {\n        userDid: 'did:web:alice.test',\n        prefs: {\n          adultContentEnabled: true,\n          labels: {},\n          labelers: [\n            {\n              did: 'did:web:labeler.test',\n              labels: {},\n            },\n          ],\n          hiddenPosts: [],\n          mutedWords: [],\n        },\n      },\n    )\n    res1.addHidden(true)\n    expect(res1.ui('contentList')).toBeModerationResult(\n      ['filter', 'blur'],\n      'contentList',\n    )\n    expect(res1.ui('contentView')).toBeModerationResult(['blur'], 'contentView')\n  })\n\n  it('Prioritizes filters and blurs correctly on merge', () => {\n    const res1 = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: '!hide',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      {\n        userDid: 'did:web:alice.test',\n        prefs: {\n          adultContentEnabled: true,\n          labels: {\n            porn: 'hide',\n          },\n          labelers: [\n            {\n              did: 'did:web:labeler.test',\n              labels: {},\n            },\n          ],\n          hiddenPosts: [],\n          mutedWords: [],\n        },\n      },\n    )\n    expect((res1.ui('contentList').filters[0] as any).label.val).toBe('!hide')\n    expect((res1.ui('contentList').filters[1] as any).label.val).toBe('porn')\n    expect((res1.ui('contentList').blurs[0] as any).label.val).toBe('!hide')\n    expect((res1.ui('contentMedia').blurs[0] as any).label.val).toBe('porn')\n  })\n\n  it('Prioritizes custom label definitions', () => {\n    const modOpts: ModerationOpts = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: true,\n        labels: { porn: 'warn' },\n        labelers: [\n          {\n            did: 'did:web:labeler.test',\n            labels: { porn: 'warn' },\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      labelDefs: {\n        'did:web:labeler.test': [\n          interpretLabelValueDefinition(\n            {\n              identifier: 'porn',\n              blurs: 'none',\n              severity: 'inform',\n              locales: [],\n              defaultSetting: 'warn',\n            },\n            'did:web:labeler.test',\n          ),\n        ],\n      },\n    }\n    const res = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n    expect(res.ui('profileList')).toBeModerationResult([])\n    expect(res.ui('profileView')).toBeModerationResult([])\n    expect(res.ui('avatar')).toBeModerationResult([])\n    expect(res.ui('banner')).toBeModerationResult([])\n    expect(res.ui('displayName')).toBeModerationResult([])\n    expect(res.ui('contentList')).toBeModerationResult(['inform'])\n    expect(res.ui('contentView')).toBeModerationResult(['inform'])\n    expect(res.ui('contentMedia')).toBeModerationResult([])\n  })\n\n  it('Doesnt allow custom behaviors to override imperative labels', () => {\n    const modOpts: ModerationOpts = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: true,\n        labels: {},\n        labelers: [\n          {\n            did: 'did:web:labeler.test',\n            labels: {},\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      labelDefs: {\n        'did:web:labeler.test': [\n          interpretLabelValueDefinition(\n            {\n              identifier: '!hide',\n              blurs: 'none',\n              severity: 'inform',\n              locales: [],\n              defaultSetting: 'warn',\n            },\n            'did:web:labeler.test',\n          ),\n        ],\n      },\n    }\n    const res = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: '!hide',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res.ui('profileList')).toBeModerationResult([])\n    expect(res.ui('profileView')).toBeModerationResult([])\n    expect(res.ui('avatar')).toBeModerationResult([])\n    expect(res.ui('banner')).toBeModerationResult([])\n    expect(res.ui('displayName')).toBeModerationResult([])\n    expect(res.ui('contentList')).toBeModerationResult([\n      'filter',\n      'blur',\n      'noOverride',\n    ])\n    expect(res.ui('contentView')).toBeModerationResult(['blur', 'noOverride'])\n    expect(res.ui('contentMedia')).toBeModerationResult([])\n  })\n\n  it('Ignores invalid label value names', () => {\n    const modOpts: ModerationOpts = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: true,\n        labels: {},\n        labelers: [\n          {\n            did: 'did:web:labeler.test',\n            labels: { BadLabel: 'hide', 'bad/label': 'hide' },\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      labelDefs: {\n        'did:web:labeler.test': [\n          interpretLabelValueDefinition(\n            {\n              identifier: 'BadLabel',\n              blurs: 'content',\n              severity: 'inform',\n              locales: [],\n              defaultSetting: 'warn',\n            },\n            'did:web:labeler.test',\n          ),\n          interpretLabelValueDefinition(\n            {\n              identifier: 'bad/label',\n              blurs: 'content',\n              severity: 'inform',\n              locales: [],\n              defaultSetting: 'warn',\n            },\n            'did:web:labeler.test',\n          ),\n        ],\n      },\n    }\n    const res = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'BadLabel',\n            cts: new Date().toISOString(),\n          },\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'bad/label',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res.ui('profileList')).toBeModerationResult([])\n    expect(res.ui('profileView')).toBeModerationResult([])\n    expect(res.ui('avatar')).toBeModerationResult([])\n    expect(res.ui('banner')).toBeModerationResult([])\n    expect(res.ui('displayName')).toBeModerationResult([])\n    expect(res.ui('contentList')).toBeModerationResult([])\n    expect(res.ui('contentView')).toBeModerationResult([])\n    expect(res.ui('contentMedia')).toBeModerationResult([])\n  })\n\n  it('Custom labels can set the default setting', () => {\n    const modOpts: ModerationOpts = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: true,\n        labels: {},\n        labelers: [\n          {\n            did: 'did:web:labeler.test',\n            labels: {},\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      labelDefs: {\n        'did:web:labeler.test': [\n          interpretLabelValueDefinition(\n            {\n              identifier: 'default-hide',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              locales: [],\n            },\n            'did:web:labeler.test',\n          ),\n          interpretLabelValueDefinition(\n            {\n              identifier: 'default-warn',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'warn',\n              locales: [],\n            },\n            'did:web:labeler.test',\n          ),\n          interpretLabelValueDefinition(\n            {\n              identifier: 'default-ignore',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'ignore',\n              locales: [],\n            },\n            'did:web:labeler.test',\n          ),\n        ],\n      },\n    }\n    const res1 = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'default-hide',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res1.ui('profileList')).toBeModerationResult([])\n    expect(res1.ui('profileView')).toBeModerationResult([])\n    expect(res1.ui('avatar')).toBeModerationResult([])\n    expect(res1.ui('banner')).toBeModerationResult([])\n    expect(res1.ui('displayName')).toBeModerationResult([])\n    expect(res1.ui('contentList')).toBeModerationResult(['filter', 'blur'])\n    expect(res1.ui('contentView')).toBeModerationResult(['inform'])\n    expect(res1.ui('contentMedia')).toBeModerationResult([])\n\n    const res2 = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'default-warn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res2.ui('profileList')).toBeModerationResult([])\n    expect(res2.ui('profileView')).toBeModerationResult([])\n    expect(res2.ui('avatar')).toBeModerationResult([])\n    expect(res2.ui('banner')).toBeModerationResult([])\n    expect(res2.ui('displayName')).toBeModerationResult([])\n    expect(res2.ui('contentList')).toBeModerationResult(['blur'])\n    expect(res2.ui('contentView')).toBeModerationResult(['inform'])\n    expect(res2.ui('contentMedia')).toBeModerationResult([])\n\n    const res3 = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'default-ignore',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res3.ui('profileList')).toBeModerationResult([])\n    expect(res3.ui('profileView')).toBeModerationResult([])\n    expect(res3.ui('avatar')).toBeModerationResult([])\n    expect(res3.ui('banner')).toBeModerationResult([])\n    expect(res3.ui('displayName')).toBeModerationResult([])\n    expect(res3.ui('contentList')).toBeModerationResult([])\n    expect(res3.ui('contentView')).toBeModerationResult([])\n    expect(res3.ui('contentMedia')).toBeModerationResult([])\n  })\n\n  it('Custom labels can require adult content to be enabled', () => {\n    const modOpts: ModerationOpts = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: false,\n        labels: { adult: 'ignore' },\n        labelers: [\n          {\n            did: 'did:web:labeler.test',\n            labels: {\n              adult: 'ignore',\n            },\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      labelDefs: {\n        'did:web:labeler.test': [\n          interpretLabelValueDefinition(\n            {\n              identifier: 'adult',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              adultOnly: true,\n              locales: [],\n            },\n            'did:web:labeler.test',\n          ),\n        ],\n      },\n    }\n    const res = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'adult',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res.ui('profileList')).toBeModerationResult([])\n    expect(res.ui('profileView')).toBeModerationResult([])\n    expect(res.ui('avatar')).toBeModerationResult([])\n    expect(res.ui('banner')).toBeModerationResult([])\n    expect(res.ui('displayName')).toBeModerationResult([])\n    expect(res.ui('contentList')).toBeModerationResult([\n      'filter',\n      'blur',\n      'noOverride',\n    ])\n    expect(res.ui('contentView')).toBeModerationResult(['blur', 'noOverride'])\n    expect(res.ui('contentMedia')).toBeModerationResult([])\n  })\n\n  it('Adult content disabled forces the preference to hide', () => {\n    const modOpts: ModerationOpts = {\n      userDid: 'did:web:alice.test',\n      prefs: {\n        adultContentEnabled: false,\n        labels: { porn: 'ignore' },\n        labelers: [\n          {\n            did: 'did:web:labeler.test',\n            labels: {},\n          },\n        ],\n        hiddenPosts: [],\n        mutedWords: [],\n      },\n      labelDefs: {},\n    }\n    const res = moderatePost(\n      mock.postView({\n        record: {\n          $type: 'app.bsky.feed.post',\n          text: 'Hello',\n          createdAt: new Date().toISOString(),\n        },\n        author: mock.profileViewBasic({\n          handle: 'bob.test',\n          displayName: 'Bob',\n        }),\n        labels: [\n          {\n            src: 'did:web:labeler.test',\n            uri: 'at://did:web:bob.test/app.bsky.post/fake',\n            val: 'porn',\n            cts: new Date().toISOString(),\n          },\n        ],\n      }),\n      modOpts,\n    )\n\n    expect(res.ui('profileList')).toBeModerationResult([])\n    expect(res.ui('profileView')).toBeModerationResult([])\n    expect(res.ui('avatar')).toBeModerationResult([])\n    expect(res.ui('banner')).toBeModerationResult([])\n    expect(res.ui('displayName')).toBeModerationResult([])\n    expect(res.ui('contentList')).toBeModerationResult(['filter'])\n    expect(res.ui('contentView')).toBeModerationResult([])\n    expect(res.ui('contentMedia')).toBeModerationResult(['blur', 'noOverride'])\n  })\n})\n"
  },
  {
    "path": "packages/api/tests/rich-text-detection.test.ts",
    "content": "import { AtpAgent, RichText, RichTextSegment } from '../src'\nimport {\n  isLink,\n  isMention,\n  isTag,\n} from '../src/client/types/app/bsky/richtext/facet'\n\ndescribe('detectFacets', () => {\n  const agent = new AtpAgent({ service: 'http://localhost' })\n\n  // Mock handle resolution\n  agent.com.atproto.identity.resolveHandle = async (params) => ({\n    success: true,\n    headers: {},\n    data: { did: `did:fake:${params?.handle}` },\n  })\n\n  const inputs = [\n    'no mention',\n    '@handle.com middle end',\n    'start @handle.com end',\n    'start middle @handle.com',\n    '@handle.com @handle.com @handle.com',\n    '@full123-chars.test',\n    'not@right',\n    '@handle.com!@#$chars',\n    '@handle.com\\n@handle.com',\n    'parenthetical (@handle.com)',\n    '👨‍👩‍👧‍👧 @handle.com 👨‍👩‍👧‍👧',\n\n    'start https://middle.com end',\n    'start https://middle.com/foo/bar end',\n    'start https://middle.com/foo/bar?baz=bux end',\n    'start https://middle.com/foo/bar?baz=bux#hash end',\n    'https://start.com/foo/bar?baz=bux#hash middle end',\n    'start middle https://end.com/foo/bar?baz=bux#hash',\n    'https://newline1.com\\nhttps://newline2.com',\n    '👨‍👩‍👧‍👧 https://middle.com 👨‍👩‍👧‍👧',\n\n    'start middle.com end',\n    'start middle.com/foo/bar end',\n    'start middle.com/foo/bar?baz=bux end',\n    'start middle.com/foo/bar?baz=bux#hash end',\n    'start.com/foo/bar?baz=bux#hash middle end',\n    'start middle end.com/foo/bar?baz=bux#hash',\n    'newline1.com\\nnewline2.com',\n    'a example.com/index.php php link',\n    'a trailing bsky.app: colon',\n\n    'not.. a..url ..here',\n    'e.g.',\n    'something-cool.jpg',\n    'website.com.jpg',\n    'e.g./foo',\n    'website.com.jpg/foo',\n\n    'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',\n    'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ',\n    'https://foo.com https://bar.com/whatever https://baz.com',\n    'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.',\n    'parenthentical (https://foo.com)',\n    'except for https://foo.com/thing_(cool)',\n  ]\n  const outputs: string[][][] = [\n    [['no mention']],\n    [['@handle.com', 'did:fake:handle.com'], [' middle end']],\n    [['start '], ['@handle.com', 'did:fake:handle.com'], [' end']],\n    [['start middle '], ['@handle.com', 'did:fake:handle.com']],\n    [\n      ['@handle.com', 'did:fake:handle.com'],\n      [' '],\n      ['@handle.com', 'did:fake:handle.com'],\n      [' '],\n      ['@handle.com', 'did:fake:handle.com'],\n    ],\n    [['@full123-chars.test', 'did:fake:full123-chars.test']],\n    [['not@right']],\n    [['@handle.com', 'did:fake:handle.com'], ['!@#$chars']],\n    [\n      ['@handle.com', 'did:fake:handle.com'],\n      ['\\n'],\n      ['@handle.com', 'did:fake:handle.com'],\n    ],\n    [['parenthetical ('], ['@handle.com', 'did:fake:handle.com'], [')']],\n    [['👨‍👩‍👧‍👧 '], ['@handle.com', 'did:fake:handle.com'], [' 👨‍👩‍👧‍👧']],\n\n    [['start '], ['https://middle.com', 'https://middle.com'], [' end']],\n    [\n      ['start '],\n      ['https://middle.com/foo/bar', 'https://middle.com/foo/bar'],\n      [' end'],\n    ],\n    [\n      ['start '],\n      [\n        'https://middle.com/foo/bar?baz=bux',\n        'https://middle.com/foo/bar?baz=bux',\n      ],\n      [' end'],\n    ],\n    [\n      ['start '],\n      [\n        'https://middle.com/foo/bar?baz=bux#hash',\n        'https://middle.com/foo/bar?baz=bux#hash',\n      ],\n      [' end'],\n    ],\n    [\n      [\n        'https://start.com/foo/bar?baz=bux#hash',\n        'https://start.com/foo/bar?baz=bux#hash',\n      ],\n      [' middle end'],\n    ],\n    [\n      ['start middle '],\n      [\n        'https://end.com/foo/bar?baz=bux#hash',\n        'https://end.com/foo/bar?baz=bux#hash',\n      ],\n    ],\n    [\n      ['https://newline1.com', 'https://newline1.com'],\n      ['\\n'],\n      ['https://newline2.com', 'https://newline2.com'],\n    ],\n    [['👨‍👩‍👧‍👧 '], ['https://middle.com', 'https://middle.com'], [' 👨‍👩‍👧‍👧']],\n\n    [['start '], ['middle.com', 'https://middle.com'], [' end']],\n    [\n      ['start '],\n      ['middle.com/foo/bar', 'https://middle.com/foo/bar'],\n      [' end'],\n    ],\n    [\n      ['start '],\n      ['middle.com/foo/bar?baz=bux', 'https://middle.com/foo/bar?baz=bux'],\n      [' end'],\n    ],\n    [\n      ['start '],\n      [\n        'middle.com/foo/bar?baz=bux#hash',\n        'https://middle.com/foo/bar?baz=bux#hash',\n      ],\n      [' end'],\n    ],\n    [\n      [\n        'start.com/foo/bar?baz=bux#hash',\n        'https://start.com/foo/bar?baz=bux#hash',\n      ],\n      [' middle end'],\n    ],\n    [\n      ['start middle '],\n      ['end.com/foo/bar?baz=bux#hash', 'https://end.com/foo/bar?baz=bux#hash'],\n    ],\n    [\n      ['newline1.com', 'https://newline1.com'],\n      ['\\n'],\n      ['newline2.com', 'https://newline2.com'],\n    ],\n    [\n      ['a '],\n      ['example.com/index.php', 'https://example.com/index.php'],\n      [' php link'],\n    ],\n    [['a trailing '], ['bsky.app', 'https://bsky.app'], [': colon']],\n\n    [['not.. a..url ..here']],\n    [['e.g.']],\n    [['something-cool.jpg']],\n    [['website.com.jpg']],\n    [['e.g./foo']],\n    [['website.com.jpg/foo']],\n\n    [\n      ['Classic article '],\n      [\n        'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',\n        'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',\n      ],\n    ],\n    [\n      ['Classic article '],\n      [\n        'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',\n        'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',\n      ],\n      [' '],\n    ],\n    [\n      ['https://foo.com', 'https://foo.com'],\n      [' '],\n      ['https://bar.com/whatever', 'https://bar.com/whatever'],\n      [' '],\n      ['https://baz.com', 'https://baz.com'],\n    ],\n    [\n      ['punctuation '],\n      ['https://foo.com', 'https://foo.com'],\n      [', '],\n      ['https://bar.com/whatever', 'https://bar.com/whatever'],\n      ['; '],\n      ['https://baz.com', 'https://baz.com'],\n      ['.'],\n    ],\n    [['parenthentical ('], ['https://foo.com', 'https://foo.com'], [')']],\n    [\n      ['except for '],\n      ['https://foo.com/thing_(cool)', 'https://foo.com/thing_(cool)'],\n    ],\n  ]\n  it('correctly handles a set of text inputs', async () => {\n    for (let i = 0; i < inputs.length; i++) {\n      const input = inputs[i]\n      const rt = new RichText({ text: input })\n      await rt.detectFacets(agent)\n      expect(Array.from(rt.segments(), segmentToOutput)).toEqual(outputs[i])\n    }\n  })\n\n  describe('correctly detects tags inline', () => {\n    const inputs: [\n      string,\n      string[],\n      { byteStart: number; byteEnd: number }[],\n    ][] = [\n      ['#a', ['a'], [{ byteStart: 0, byteEnd: 2 }]],\n      [\n        '#a #b',\n        ['a', 'b'],\n        [\n          { byteStart: 0, byteEnd: 2 },\n          { byteStart: 3, byteEnd: 5 },\n        ],\n      ],\n      ['#1', [], []],\n      ['#1a', ['1a'], [{ byteStart: 0, byteEnd: 3 }]],\n      ['#tag', ['tag'], [{ byteStart: 0, byteEnd: 4 }]],\n      ['body #tag', ['tag'], [{ byteStart: 5, byteEnd: 9 }]],\n      ['#tag body', ['tag'], [{ byteStart: 0, byteEnd: 4 }]],\n      ['body #tag body', ['tag'], [{ byteStart: 5, byteEnd: 9 }]],\n      ['body #1', [], []],\n      ['body #1a', ['1a'], [{ byteStart: 5, byteEnd: 8 }]],\n      ['body #a1', ['a1'], [{ byteStart: 5, byteEnd: 8 }]],\n      ['#', [], []],\n      ['#?', [], []],\n      ['text #', [], []],\n      ['text # text', [], []],\n      [\n        'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',\n        ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],\n        [{ byteStart: 5, byteEnd: 70 }],\n      ],\n      [\n        'body #thisisa65characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab',\n        [],\n        [],\n      ],\n      [\n        'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!',\n        ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],\n        [{ byteStart: 5, byteEnd: 70 }],\n      ],\n      [\n        'its a #double#rainbow',\n        ['double#rainbow'],\n        [{ byteStart: 6, byteEnd: 21 }],\n      ],\n      ['##hashash', ['#hashash'], [{ byteStart: 0, byteEnd: 9 }]],\n      ['##', [], []],\n      ['some #n0n3s@n5e!', ['n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]],\n      [\n        'works #with,punctuation',\n        ['with,punctuation'],\n        [{ byteStart: 6, byteEnd: 23 }],\n      ],\n      [\n        'strips trailing #punctuation, #like. #this!',\n        ['punctuation', 'like', 'this'],\n        [\n          { byteStart: 16, byteEnd: 28 },\n          { byteStart: 30, byteEnd: 35 },\n          { byteStart: 37, byteEnd: 42 },\n        ],\n      ],\n      [\n        'strips #multi_trailing___...',\n        ['multi_trailing'],\n        [{ byteStart: 7, byteEnd: 22 }],\n      ],\n      [\n        'works with #🦋 emoji, and #butter🦋fly',\n        ['🦋', 'butter🦋fly'],\n        [\n          { byteStart: 11, byteEnd: 16 },\n          { byteStart: 28, byteEnd: 42 },\n        ],\n      ],\n      [\n        '#same #same #but #diff',\n        ['same', 'same', 'but', 'diff'],\n        [\n          { byteStart: 0, byteEnd: 5 },\n          { byteStart: 6, byteEnd: 11 },\n          { byteStart: 12, byteEnd: 16 },\n          { byteStart: 17, byteEnd: 22 },\n        ],\n      ],\n      ['this #️⃣tag should not be a tag', [], []],\n      [\n        'this ##️⃣tag should be a tag',\n        ['#️⃣tag'],\n        [\n          {\n            byteStart: 5,\n            byteEnd: 16,\n          },\n        ],\n      ],\n      [\n        'this #t\\nag should be a tag',\n        ['t'],\n        [\n          {\n            byteStart: 5,\n            byteEnd: 7,\n          },\n        ],\n      ],\n      ['no match (\\\\u200B): #​', [], []],\n      ['no match (\\\\u200Ba): #​a', [], []],\n      ['match (a\\\\u200Bb): #a​b', ['a'], [{ byteStart: 18, byteEnd: 20 }]],\n      ['match (ab\\\\u200B): #ab​', ['ab'], [{ byteStart: 18, byteEnd: 21 }]],\n      ['no match (\\\\u20e2tag): #⃢tag', [], []],\n      ['no match (a\\\\u20e2b): #a⃢b', ['a'], [{ byteStart: 21, byteEnd: 23 }]],\n      [\n        'match full width number sign (tag): ＃tag',\n        ['tag'],\n        [{ byteStart: 36, byteEnd: 42 }],\n      ],\n      [\n        'match full width number sign (tag): ＃#️⃣tag',\n        ['#️⃣tag'],\n        [{ byteStart: 36, byteEnd: 49 }],\n      ],\n      ['no match 1?: #1?', [], []],\n    ]\n\n    it.each(inputs)('%s', async (input, tags, indices) => {\n      const rt = new RichText({ text: input })\n      await rt.detectFacets(agent)\n\n      const detectedTags: string[] = []\n      const detectedIndices: { byteStart: number; byteEnd: number }[] = []\n\n      for (const { facet } of rt.segments()) {\n        if (!facet) continue\n        for (const feature of facet.features) {\n          if (isTag(feature)) {\n            detectedTags.push(feature.tag)\n          }\n        }\n        detectedIndices.push(facet.index)\n      }\n\n      expect(detectedTags).toEqual(tags)\n      expect(detectedIndices).toEqual(indices)\n    })\n  })\n\n  describe('correctly detects cashtags inline', () => {\n    const inputs: [\n      string,\n      string[],\n      { byteStart: number; byteEnd: number }[],\n    ][] = [\n      ['$AAPL', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]],\n      ['$aapl', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]], // normalized to uppercase\n      ['$A', ['$A'], [{ byteStart: 0, byteEnd: 2 }]],\n      ['$a', ['$A'], [{ byteStart: 0, byteEnd: 2 }]], // single char normalized\n      [\n        '$BTC $ETH',\n        ['$BTC', '$ETH'],\n        [\n          { byteStart: 0, byteEnd: 4 },\n          { byteStart: 5, byteEnd: 9 },\n        ],\n      ],\n      ['$100', [], []], // starts with digit - not a cashtag\n      ['$GOOGL', ['$GOOGL'], [{ byteStart: 0, byteEnd: 6 }]], // 5 chars - max length\n      ['$TOOLONG', [], []], // >5 chars\n      ['check $LEGO now', ['$LEGO'], [{ byteStart: 6, byteEnd: 11 }]],\n      ['($GOOG)', ['$GOOG'], [{ byteStart: 1, byteEnd: 6 }]],\n      ['$AAPL.', ['$AAPL'], [{ byteStart: 0, byteEnd: 5 }]], // trailing punctuation\n      [\n        '$AAPL, $MSFT!',\n        ['$AAPL', '$MSFT'],\n        [\n          { byteStart: 0, byteEnd: 5 },\n          { byteStart: 7, byteEnd: 12 },\n        ],\n      ],\n      ['no$SPACE', [], []], // must have leading space or start\n      ['$', [], []], // just dollar sign\n      ['$ AAPL', [], []], // space after $\n      ['$123ABC', [], []], // starts with digit\n      ['$ABC12', ['$ABC12'], [{ byteStart: 0, byteEnd: 6 }]], // digits after letters OK (5 chars)\n      ['$ABC123', [], []], // 6 chars - too long\n    ]\n\n    it.each(inputs)('%s', (input, tags, indices) => {\n      const rt = new RichText({ text: input })\n      rt.detectFacetsWithoutResolution()\n\n      const detectedTags: string[] = []\n      const detectedIndices: { byteStart: number; byteEnd: number }[] = []\n\n      for (const { facet } of rt.segments()) {\n        if (!facet) continue\n        for (const feature of facet.features) {\n          if (isTag(feature) && feature.tag.startsWith('$')) {\n            detectedTags.push(feature.tag)\n          }\n        }\n        if (\n          facet.features.some(\n            (f) => isTag(f) && (f as any).tag?.startsWith('$'),\n          )\n        ) {\n          detectedIndices.push(facet.index)\n        }\n      }\n\n      expect(detectedTags).toEqual(tags)\n      expect(detectedIndices).toEqual(indices)\n    })\n  })\n})\n\nfunction segmentToOutput(segment: RichTextSegment): string[] {\n  if (segment.facet) {\n    return [\n      segment.text,\n      segment.facet?.features.map((f) => {\n        if (isMention(f)) return f.did\n        if (isLink(f)) return f.uri\n        return undefined\n      })?.[0] || '',\n    ]\n  }\n  return [segment.text]\n}\n"
  },
  {
    "path": "packages/api/tests/rich-text-sanitization.test.ts",
    "content": "import { Facet, RichText, UnicodeString, sanitizeRichText } from '../src'\n\ndescribe('sanitizeRichText: cleanNewlines', () => {\n  it('removes more than two consecutive new lines', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('removes more than two consecutive new lines w/fat unicode', () => {\n    const input = new RichText({\n      text: 'test👨‍👩‍👧‍👧\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n\\n\\ntest👨‍👩‍👧‍👧\\n\\n\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test👨‍👩‍👧‍👧\\n\\n👨‍👩‍👧‍👧test\\n\\ntest👨‍👩‍👧‍👧\\n\\ntest\\n\\n👨‍👩‍👧‍👧test',\n    )\n  })\n\n  it('removes more than two consecutive new lines with spaces', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n \\n \\n \\n \\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n  \\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('returns original string if there are no consecutive new lines', () => {\n    const input = new RichText({ text: 'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest' })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(String(input.unicodeText))\n  })\n\n  it('returns original string if there are no new lines', () => {\n    const input = new RichText({ text: 'test test          test test test' })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(String(input.unicodeText))\n  })\n\n  it('returns empty string if input is empty', () => {\n    const input = new RichText({ text: '' })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual('')\n  })\n\n  it('works with different types of new line characters', () => {\n    const input = new RichText({\n      text: 'test\\r\\ntest\\n\\rtest\\rtest\\n\\n\\n\\ntest\\n\\r \\n \\n \\n \\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\r\\ntest\\n\\rtest\\rtest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('removes more than two consecutive new lines with zero width space', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\u200B\\u200B\\n\\n\\n\\ntest\\n \\u200B\\u200B \\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('removes more than two consecutive new lines with zero width non-joiner', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\u200C\\u200C\\n\\n\\n\\ntest\\n \\u200C\\u200C \\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('removes more than two consecutive new lines with zero width joiner', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\u200D\\u200D\\n\\n\\n\\ntest\\n \\u200D\\u200D \\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('removes more than two consecutive new lines with soft hyphen', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\u00AD\\u00AD\\n\\n\\n\\ntest\\n \\u00AD\\u00AD \\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n\n  it('removes more than two consecutive new lines with word joiner', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\u2060\\u2060\\n\\n\\n\\ntest\\n \\u2060\\u2060 \\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n  })\n})\n\ndescribe('sanitizeRichText w/facets: cleanNewlines', () => {\n  it('preserves entities as expected', () => {\n    const input = new RichText({\n      text: 'test\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest\\n\\n\\n\\n\\n\\n\\ntest',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 13 }, features: [{ $type: '' }] },\n        { index: { byteStart: 13, byteEnd: 24 }, features: [{ $type: '' }] },\n        { index: { byteStart: 9, byteEnd: 15 }, features: [{ $type: '' }] },\n        { index: { byteStart: 4, byteEnd: 9 }, features: [{ $type: '' }] },\n      ],\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(facetToStr(String(input.unicodeText), input.facets?.[0])).toEqual(\n      'test\\n\\n\\n\\n\\ntest',\n    )\n    expect(facetToStr(String(input.unicodeText), input.facets?.[1])).toEqual(\n      '\\n\\n\\n\\n\\n',\n    )\n    expect(facetToStr(String(input.unicodeText), input.facets?.[2])).toEqual(\n      'test\\n\\n',\n    )\n    expect(facetToStr(String(input.unicodeText), input.facets?.[3])).toEqual(\n      '\\n\\n\\n\\n\\n\\n\\ntest',\n    )\n    expect(String(output.unicodeText)).toEqual(\n      'test\\n\\ntest\\n\\ntest\\n\\ntest\\n\\ntest',\n    )\n    expect(facetToStr(String(output.unicodeText), output.facets?.[0])).toEqual(\n      'test\\n\\ntest',\n    )\n    expect(facetToStr(String(output.unicodeText), output.facets?.[1])).toEqual(\n      'test',\n    )\n    expect(facetToStr(String(output.unicodeText), output.facets?.[2])).toEqual(\n      'test',\n    )\n    expect(output.facets?.[3]).toEqual(undefined)\n  })\n\n  it('preserves entities as expected w/fat unicode', () => {\n    const str = new UnicodeString(\n      '👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n',\n    )\n    let lastI = 0\n    const makeFacet = (match: string) => {\n      const i = str.utf16.indexOf(match, lastI)\n      lastI = i + match.length\n      const byteStart = str.utf16IndexToUtf8Index(i)\n      const byteEnd = byteStart + new UnicodeString(match).length\n      return {\n        index: { byteStart, byteEnd },\n        features: [{ $type: '' }],\n      }\n    }\n\n    const input = new RichText({\n      text: str.utf16,\n      facets: [\n        makeFacet('👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test'),\n        makeFacet('\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test'),\n        makeFacet('👨‍👩‍👧‍👧test\\n\\n'),\n        makeFacet('\\n\\n'),\n      ],\n    })\n    const output = sanitizeRichText(input, { cleanNewlines: true })\n    expect(facetToStr(String(input.unicodeText), input.facets?.[0])).toEqual(\n      '👨‍👩‍👧‍👧test\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test',\n    )\n    expect(facetToStr(String(input.unicodeText), input.facets?.[1])).toEqual(\n      '\\n\\n\\n\\n\\n👨‍👩‍👧‍👧test',\n    )\n    expect(facetToStr(String(input.unicodeText), input.facets?.[2])).toEqual(\n      '👨‍👩‍👧‍👧test\\n\\n',\n    )\n    expect(facetToStr(String(input.unicodeText), input.facets?.[3])).toEqual(\n      '\\n\\n',\n    )\n    expect(String(output.unicodeText)).toEqual(\n      '👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test\\n\\n',\n    )\n    expect(facetToStr(String(output.unicodeText), output.facets?.[0])).toEqual(\n      '👨‍👩‍👧‍👧test\\n\\n👨‍👩‍👧‍👧test',\n    )\n    expect(facetToStr(String(output.unicodeText), output.facets?.[1])).toEqual(\n      '👨‍👩‍👧‍👧test',\n    )\n    expect(facetToStr(String(output.unicodeText), output.facets?.[2])).toEqual(\n      '👨‍👩‍👧‍👧test',\n    )\n    expect(output.facets?.[3]).toEqual(undefined)\n  })\n})\n\nfunction facetToStr(str: string, ent?: Facet) {\n  if (!ent) {\n    return ''\n  }\n  return new UnicodeString(str).slice(ent.index.byteStart, ent.index.byteEnd)\n}\n"
  },
  {
    "path": "packages/api/tests/rich-text.test.ts",
    "content": "import { RichText } from '../src'\n\ndescribe('RichText', () => {\n  it('converts entities to facets correctly', () => {\n    const rt = new RichText({\n      text: 'test',\n      entities: [\n        {\n          index: { start: 0, end: 1 },\n          type: 'link',\n          value: 'https://example.com',\n        },\n        {\n          index: { start: 1, end: 2 },\n          type: 'mention',\n          value: 'did:plc:1234',\n        },\n        {\n          index: { start: 2, end: 3 },\n          type: 'other',\n          value: 'willbedropped',\n        },\n      ],\n    })\n    expect(rt.facets).toEqual([\n      {\n        $type: 'app.bsky.richtext.facet',\n        index: { byteStart: 0, byteEnd: 1 },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#link',\n            uri: 'https://example.com',\n          },\n        ],\n      },\n      {\n        $type: 'app.bsky.richtext.facet',\n        index: { byteStart: 1, byteEnd: 2 },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#mention',\n            did: 'did:plc:1234',\n          },\n        ],\n      },\n    ])\n  })\n\n  it('converts entity utf16 indices to facet utf8 indices', () => {\n    const rt = new RichText({\n      text: '👨‍👩‍👧‍👧👨‍👩‍👧‍👧👨‍👩‍👧‍👧',\n      entities: [\n        {\n          index: { start: 0, end: 11 },\n          type: 'link',\n          value: 'https://example.com',\n        },\n        {\n          index: { start: 11, end: 22 },\n          type: 'mention',\n          value: 'did:plc:1234',\n        },\n        {\n          index: { start: 22, end: 33 },\n          type: 'other',\n          value: 'willbedropped',\n        },\n      ],\n    })\n    expect(rt.facets).toEqual([\n      {\n        $type: 'app.bsky.richtext.facet',\n        index: { byteStart: 0, byteEnd: 25 },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#link',\n            uri: 'https://example.com',\n          },\n        ],\n      },\n      {\n        $type: 'app.bsky.richtext.facet',\n        index: { byteStart: 25, byteEnd: 50 },\n        features: [\n          {\n            $type: 'app.bsky.richtext.facet#mention',\n            did: 'did:plc:1234',\n          },\n        ],\n      },\n    ])\n  })\n\n  it('calculates bytelength and grapheme length correctly', () => {\n    {\n      const rt = new RichText({ text: 'Hello!' })\n      expect(rt.length).toBe(6)\n      expect(rt.graphemeLength).toBe(6)\n    }\n    {\n      const rt = new RichText({ text: '👨‍👩‍👧‍👧' })\n      expect(rt.length).toBe(25)\n      expect(rt.graphemeLength).toBe(1)\n    }\n    {\n      const rt = new RichText({ text: '👨‍👩‍👧‍👧🔥 good!✅' })\n      expect(rt.length).toBe(38)\n      expect(rt.graphemeLength).toBe(9)\n    }\n  })\n})\n\ndescribe('RichText#insert', () => {\n  const input = new RichText({\n    text: 'hello world',\n    facets: [\n      { index: { byteStart: 2, byteEnd: 7 }, features: [{ $type: '' }] },\n    ],\n  })\n\n  it('correctly adjusts facets (scenario A - before)', () => {\n    const output = input.clone().insert(0, 'test')\n    expect(output.text).toEqual('testhello world')\n    expect(output.facets?.[0].index.byteStart).toEqual(6)\n    expect(output.facets?.[0].index.byteEnd).toEqual(11)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('llo w')\n  })\n\n  it('correctly adjusts facets (scenario B - inner)', () => {\n    const output = input.clone().insert(4, 'test')\n    expect(output.text).toEqual('helltesto world')\n    expect(output.facets?.[0].index.byteStart).toEqual(2)\n    expect(output.facets?.[0].index.byteEnd).toEqual(11)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('lltesto w')\n  })\n\n  it('correctly adjusts facets (scenario C - after)', () => {\n    const output = input.clone().insert(8, 'test')\n    expect(output.text).toEqual('hello wotestrld')\n    expect(output.facets?.[0].index.byteStart).toEqual(2)\n    expect(output.facets?.[0].index.byteEnd).toEqual(7)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('llo w')\n  })\n})\n\ndescribe('RichText#insert w/fat unicode', () => {\n  const input = new RichText({\n    text: 'one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧',\n    facets: [\n      { index: { byteStart: 0, byteEnd: 28 }, features: [{ $type: '' }] },\n      { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },\n      { index: { byteStart: 58, byteEnd: 88 }, features: [{ $type: '' }] },\n    ],\n  })\n\n  it('correctly adjusts facets (scenario A - before)', () => {\n    const output = input.clone().insert(0, 'test')\n    expect(output.text).toEqual('testone👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('one👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[1].index.byteStart,\n        output.facets?.[1].index.byteEnd,\n      ),\n    ).toEqual('two👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[2].index.byteStart,\n        output.facets?.[2].index.byteEnd,\n      ),\n    ).toEqual('three👨‍👩‍👧‍👧')\n  })\n\n  it('correctly adjusts facets (scenario B - inner)', () => {\n    const output = input.clone().insert(3, 'test')\n    expect(output.text).toEqual('onetest👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('onetest👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[1].index.byteStart,\n        output.facets?.[1].index.byteEnd,\n      ),\n    ).toEqual('two👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[2].index.byteStart,\n        output.facets?.[2].index.byteEnd,\n      ),\n    ).toEqual('three👨‍👩‍👧‍👧')\n  })\n\n  it('correctly adjusts facets (scenario C - after)', () => {\n    const output = input.clone().insert(28, 'test')\n    expect(output.text).toEqual('one👨‍👩‍👧‍👧test two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('one👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[1].index.byteStart,\n        output.facets?.[1].index.byteEnd,\n      ),\n    ).toEqual('two👨‍👩‍👧‍👧')\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[2].index.byteStart,\n        output.facets?.[2].index.byteEnd,\n      ),\n    ).toEqual('three👨‍👩‍👧‍👧')\n  })\n})\n\ndescribe('RichText#delete', () => {\n  const input = new RichText({\n    text: 'hello world',\n    facets: [\n      { index: { byteStart: 2, byteEnd: 7 }, features: [{ $type: '' }] },\n    ],\n  })\n\n  it('correctly adjusts facets (scenario A - entirely outer)', () => {\n    const output = input.clone().delete(0, 9)\n    expect(output.text).toEqual('ld')\n    expect(output.facets?.length).toEqual(0)\n  })\n\n  it('correctly adjusts facets (scenario B - entirely after)', () => {\n    const output = input.clone().delete(7, 11)\n    expect(output.text).toEqual('hello w')\n    expect(output.facets?.[0].index.byteStart).toEqual(2)\n    expect(output.facets?.[0].index.byteEnd).toEqual(7)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('llo w')\n  })\n\n  it('correctly adjusts facets (scenario C - partially after)', () => {\n    const output = input.clone().delete(4, 11)\n    expect(output.text).toEqual('hell')\n    expect(output.facets?.[0].index.byteStart).toEqual(2)\n    expect(output.facets?.[0].index.byteEnd).toEqual(4)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('ll')\n  })\n\n  it('correctly adjusts facets (scenario D - entirely inner)', () => {\n    const output = input.clone().delete(3, 5)\n    expect(output.text).toEqual('hel world')\n    expect(output.facets?.[0].index.byteStart).toEqual(2)\n    expect(output.facets?.[0].index.byteEnd).toEqual(5)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('l w')\n  })\n\n  it('correctly adjusts facets (scenario E - partially before)', () => {\n    const output = input.clone().delete(1, 5)\n    expect(output.text).toEqual('h world')\n    expect(output.facets?.[0].index.byteStart).toEqual(1)\n    expect(output.facets?.[0].index.byteEnd).toEqual(3)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual(' w')\n  })\n\n  it('correctly adjusts facets (scenario F - entirely before)', () => {\n    const output = input.clone().delete(0, 2)\n    expect(output.text).toEqual('llo world')\n    expect(output.facets?.[0].index.byteStart).toEqual(0)\n    expect(output.facets?.[0].index.byteEnd).toEqual(5)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('llo w')\n  })\n})\n\ndescribe('RichText#delete w/fat unicode', () => {\n  const input = new RichText({\n    text: 'one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧',\n    facets: [\n      { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },\n    ],\n  })\n\n  it('correctly adjusts facets (scenario A - entirely outer)', () => {\n    const output = input.clone().delete(28, 58)\n    expect(output.text).toEqual('one👨‍👩‍👧‍👧three👨‍👩‍👧‍👧')\n    expect(output.facets?.length).toEqual(0)\n  })\n\n  it('correctly adjusts facets (scenario B - entirely after)', () => {\n    const output = input.clone().delete(57, 88)\n    expect(output.text).toEqual('one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧')\n    expect(output.facets?.[0].index.byteStart).toEqual(29)\n    expect(output.facets?.[0].index.byteEnd).toEqual(57)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('two👨‍👩‍👧‍👧')\n  })\n\n  it('correctly adjusts facets (scenario C - partially after)', () => {\n    const output = input.clone().delete(31, 88)\n    expect(output.text).toEqual('one👨‍👩‍👧‍👧 tw')\n    expect(output.facets?.[0].index.byteStart).toEqual(29)\n    expect(output.facets?.[0].index.byteEnd).toEqual(31)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('tw')\n  })\n\n  it('correctly adjusts facets (scenario D - entirely inner)', () => {\n    const output = input.clone().delete(30, 32)\n    expect(output.text).toEqual('one👨‍👩‍👧‍👧 t👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')\n    expect(output.facets?.[0].index.byteStart).toEqual(29)\n    expect(output.facets?.[0].index.byteEnd).toEqual(55)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('t👨‍👩‍👧‍👧')\n  })\n\n  it('correctly adjusts facets (scenario E - partially before)', () => {\n    const output = input.clone().delete(28, 31)\n    expect(output.text).toEqual('one👨‍👩‍👧‍👧o👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')\n    expect(output.facets?.[0].index.byteStart).toEqual(28)\n    expect(output.facets?.[0].index.byteEnd).toEqual(54)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('o👨‍👩‍👧‍👧')\n  })\n\n  it('correctly adjusts facets (scenario F - entirely before)', () => {\n    const output = input.clone().delete(0, 2)\n    expect(output.text).toEqual('e👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')\n    expect(output.facets?.[0].index.byteStart).toEqual(27)\n    expect(output.facets?.[0].index.byteEnd).toEqual(55)\n    expect(\n      output.unicodeText.slice(\n        output.facets?.[0].index.byteStart,\n        output.facets?.[0].index.byteEnd,\n      ),\n    ).toEqual('two👨‍👩‍👧‍👧')\n  })\n})\n\ndescribe('RichText#segments', () => {\n  it('produces an empty output for an empty input', () => {\n    const input = new RichText({ text: '' })\n    expect(Array.from(input.segments())).toEqual([{ text: '' }])\n  })\n\n  it('produces a single segment when no facets are present', () => {\n    const input = new RichText({ text: 'hello' })\n    expect(Array.from(input.segments())).toEqual([{ text: 'hello' }])\n  })\n\n  it('produces 3 segments with 1 entity in the middle', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 4, byteEnd: 7 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      { text: 'one ' },\n      {\n        text: 'two',\n        facet: {\n          index: { byteStart: 4, byteEnd: 7 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' three' },\n    ])\n  })\n\n  it('produces 2 segments with 1 entity in the byteStart', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 7 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      {\n        text: 'one two',\n        facet: {\n          index: { byteStart: 0, byteEnd: 7 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' three' },\n    ])\n  })\n\n  it('produces 2 segments with 1 entity in the end', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 4, byteEnd: 13 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      { text: 'one ' },\n      {\n        text: 'two three',\n        facet: {\n          index: { byteStart: 4, byteEnd: 13 },\n          features: [{ $type: '' }],\n        },\n      },\n    ])\n  })\n\n  it('produces 1 segments with 1 entity around the entire string', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 13 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      {\n        text: 'one two three',\n        facet: {\n          index: { byteStart: 0, byteEnd: 13 },\n          features: [{ $type: '' }],\n        },\n      },\n    ])\n  })\n\n  it('produces 5 segments with 3 facets covering each word', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },\n        { index: { byteStart: 4, byteEnd: 7 }, features: [{ $type: '' }] },\n        { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      {\n        text: 'one',\n        facet: {\n          index: { byteStart: 0, byteEnd: 3 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' ' },\n      {\n        text: 'two',\n        facet: {\n          index: { byteStart: 4, byteEnd: 7 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' ' },\n      {\n        text: 'three',\n        facet: {\n          index: { byteStart: 8, byteEnd: 13 },\n          features: [{ $type: '' }],\n        },\n      },\n    ])\n  })\n\n  it('uses utf8 indices', () => {\n    const input = new RichText({\n      text: 'one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 28 }, features: [{ $type: '' }] },\n        { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },\n        { index: { byteStart: 58, byteEnd: 88 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      {\n        text: 'one👨‍👩‍👧‍👧',\n        facet: {\n          index: { byteStart: 0, byteEnd: 28 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' ' },\n      {\n        text: 'two👨‍👩‍👧‍👧',\n        facet: {\n          index: { byteStart: 29, byteEnd: 57 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' ' },\n      {\n        text: 'three👨‍👩‍👧‍👧',\n        facet: {\n          index: { byteStart: 58, byteEnd: 88 },\n          features: [{ $type: '' }],\n        },\n      },\n    ])\n  })\n\n  it('correctly identifies mentions and links', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        {\n          index: { byteStart: 0, byteEnd: 3 },\n          features: [\n            {\n              $type: 'app.bsky.richtext.facet#mention',\n              did: 'did:plc:123',\n            },\n          ],\n        },\n        {\n          index: { byteStart: 4, byteEnd: 7 },\n          features: [\n            {\n              $type: 'app.bsky.richtext.facet#link',\n              uri: 'https://example.com',\n            },\n          ],\n        },\n        {\n          index: { byteStart: 8, byteEnd: 13 },\n          features: [{ $type: 'other' }],\n        },\n      ],\n    })\n    const segments = Array.from(input.segments())\n    expect(segments[0].isLink()).toBe(false)\n    expect(segments[0].isMention()).toBe(true)\n    expect(segments[1].isLink()).toBe(false)\n    expect(segments[1].isMention()).toBe(false)\n    expect(segments[2].isLink()).toBe(true)\n    expect(segments[2].isMention()).toBe(false)\n    expect(segments[3].isLink()).toBe(false)\n    expect(segments[3].isMention()).toBe(false)\n    expect(segments[4].isLink()).toBe(false)\n    expect(segments[4].isMention()).toBe(false)\n  })\n\n  it('skips facets that incorrectly overlap (left edge)', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },\n        { index: { byteStart: 2, byteEnd: 9 }, features: [{ $type: '' }] },\n        { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      {\n        text: 'one',\n        facet: {\n          index: { byteStart: 0, byteEnd: 3 },\n          features: [{ $type: '' }],\n        },\n      },\n      {\n        text: ' two ',\n      },\n      {\n        text: 'three',\n        facet: {\n          index: { byteStart: 8, byteEnd: 13 },\n          features: [{ $type: '' }],\n        },\n      },\n    ])\n  })\n\n  it('skips facets that incorrectly overlap (right edge)', () => {\n    const input = new RichText({\n      text: 'one two three',\n      facets: [\n        { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },\n        { index: { byteStart: 4, byteEnd: 9 }, features: [{ $type: '' }] },\n        { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },\n      ],\n    })\n    expect(Array.from(input.segments())).toEqual([\n      {\n        text: 'one',\n        facet: {\n          index: { byteStart: 0, byteEnd: 3 },\n          features: [{ $type: '' }],\n        },\n      },\n      { text: ' ' },\n      {\n        text: 'two t',\n        facet: {\n          index: { byteStart: 4, byteEnd: 9 },\n          features: [{ $type: '' }],\n        },\n      },\n      {\n        text: 'hree',\n      },\n    ])\n  })\n\n  it(\"doesn't duplicate text when negative-length facets are present\", () => {\n    const input = {\n      text: 'hello world',\n      facets: [\n        // invalid zero-length\n        {\n          features: [],\n          index: {\n            byteStart: 6,\n            byteEnd: 0,\n          },\n        },\n        // valid normal facet\n        {\n          features: [],\n          index: {\n            byteEnd: 11,\n            byteStart: 6,\n          },\n        },\n        // valid zero-length\n        {\n          features: [],\n          index: {\n            byteEnd: 0,\n            byteStart: 0,\n          },\n        },\n      ],\n    }\n\n    const rt = new RichText(input)\n\n    let output = ''\n    for (const segment of rt.segments()) {\n      output += segment.text\n    }\n\n    expect(output).toEqual(input.text)\n\n    // invalid one should have been removed\n    expect(rt.facets?.length).toEqual(2)\n  })\n})\n"
  },
  {
    "path": "packages/api/tests/util/echo-server.ts",
    "content": "import { once } from 'node:events'\nimport { createServer } from 'node:http'\n\nexport async function createHeaderEchoServer(port: number = 0) {\n  const server = createServer((req, res) => {\n    res.writeHead(200, undefined, { 'content-type': 'application/json' })\n    res.end(\n      JSON.stringify({\n        ...req.headers,\n        did: 'did:web:fake.com',\n        availableUserDomains: [],\n      }),\n    )\n  })\n\n  server.listen(port)\n\n  await once(server, 'listening')\n\n  return server\n}\n"
  },
  {
    "path": "packages/api/tests/util/moderation-behavior.ts",
    "content": "import {\n  ComAtprotoLabelDefs,\n  LabelPreference,\n  ModerationOpts,\n  ModerationUI,\n} from '../../src'\nimport { mock as m } from '../../src/mocker'\n\nexport type ModerationTestSuiteResultFlag =\n  | 'filter'\n  | 'blur'\n  | 'alert'\n  | 'inform'\n  | 'noOverride'\n\nexport interface ModerationTestSuiteScenario {\n  cfg: string\n  subject: 'post' | 'profile' | 'userlist' | 'feedgen'\n  author: string\n  quoteAuthor?: string\n  labels: {\n    post?: string[]\n    profile?: string[]\n    account?: string[]\n    quotedPost?: string[]\n    quotedAccount?: string[]\n  }\n  behaviors: {\n    profileList?: ModerationTestSuiteResultFlag[]\n    profileView?: ModerationTestSuiteResultFlag[]\n    avatar?: ModerationTestSuiteResultFlag[]\n    banner?: ModerationTestSuiteResultFlag[]\n    displayName?: ModerationTestSuiteResultFlag[]\n    contentList?: ModerationTestSuiteResultFlag[]\n    contentView?: ModerationTestSuiteResultFlag[]\n    contentMedia?: ModerationTestSuiteResultFlag[]\n  }\n}\n\nexport type SuiteUsers = Record<\n  string,\n  {\n    blocking: boolean\n    blockingByList: boolean\n    blockedBy: boolean\n    muted: boolean\n    mutedByList: boolean\n  }\n>\n\nexport type SuiteConfigurations = Record<\n  string,\n  {\n    authed?: boolean\n    adultContentEnabled?: boolean\n    settings?: Record<string, LabelPreference>\n  }\n>\n\nexport type SuiteScenarios = Record<string, ModerationTestSuiteScenario>\n\nexpect.extend({\n  toBeModerationResult(\n    actual: ModerationUI,\n    expected: ModerationTestSuiteResultFlag[] | undefined,\n    context = '',\n    stringifiedResult: string | undefined = undefined,\n    _ignoreCause = false,\n  ) {\n    const fail = (msg: string) => ({\n      pass: false,\n      message: () =>\n        `${msg}.${\n          stringifiedResult ? ` Full result: ${stringifiedResult}` : ''\n        }`,\n    })\n    // let cause = actual.causes?.type as string\n    // if (actual.cause?.type === 'label') {\n    //   cause = `label:${actual.cause.labelDef.id}`\n    // } else if (actual.cause?.type === 'muted') {\n    //   if (actual.cause.source.type === 'list') {\n    //     cause = 'muted-by-list'\n    //   }\n    // } else if (actual.cause?.type === 'blocking') {\n    //   if (actual.cause.source.type === 'list') {\n    //     cause = 'blocking-by-list'\n    //   }\n    // }\n    if (!expected) {\n      // if (!ignoreCause && actual.cause) {\n      //   return fail(`${context} expected to be a no-op, got ${cause}`)\n      // }\n      if (actual.inform) {\n        return fail(`${context} expected to be a no-op, got inform=true`)\n      }\n      if (actual.alert) {\n        return fail(`${context} expected to be a no-op, got alert=true`)\n      }\n      if (actual.blur) {\n        return fail(`${context} expected to be a no-op, got blur=true`)\n      }\n      if (actual.filter) {\n        return fail(`${context} expected to be a no-op, got filter=true`)\n      }\n      if (actual.noOverride) {\n        return fail(`${context} expected to be a no-op, got noOverride=true`)\n      }\n    } else {\n      // if (!ignoreCause && cause !== expected.cause) {\n      //   return fail(`${context} expected to be ${expected.cause}, got ${cause}`)\n      // }\n      const expectedInform = expected.includes('inform')\n      if (!!actual.inform !== expectedInform) {\n        return fail(\n          `${context} expected to be inform=${expectedInform}, got ${\n            actual.inform || false\n          }`,\n        )\n      }\n      const expectedAlert = expected.includes('alert')\n      if (!!actual.alert !== expectedAlert) {\n        return fail(\n          `${context} expected to be alert=${expectedAlert}, got ${\n            actual.alert || false\n          }`,\n        )\n      }\n      const expectedBlur = expected.includes('blur')\n      if (!!actual.blur !== expectedBlur) {\n        return fail(\n          `${context} expected to be blur=${expectedBlur}, got ${\n            actual.blur || false\n          }`,\n        )\n      }\n      const expectedFilter = expected.includes('filter')\n      if (!!actual.filter !== expectedFilter) {\n        return fail(\n          `${context} expected to be filter=${expectedFilter}, got ${\n            actual.filter || false\n          }`,\n        )\n      }\n      const expectedNoOverride = expected.includes('noOverride')\n      if (!!actual.noOverride !== expectedNoOverride) {\n        return fail(\n          `${context} expected to be noOverride=${expectedNoOverride}, got ${\n            actual.noOverride || false\n          }`,\n        )\n      }\n    }\n    return { pass: true, message: () => '' }\n  },\n})\n\nexport class ModerationBehaviorSuiteRunner {\n  constructor(\n    public users: SuiteUsers,\n    public configurations: SuiteConfigurations,\n    public scenarios: SuiteScenarios,\n  ) {}\n\n  postScenario(scenario: ModerationTestSuiteScenario) {\n    if (scenario.subject !== 'post') {\n      throw new Error('Scenario subject must be \"post\"')\n    }\n    const author = this.profileViewBasic(scenario.author, scenario.labels)\n    return m.postView({\n      record: m.post({\n        text: 'Post text',\n      }),\n      author,\n      labels: (scenario.labels.post || []).map((val) =>\n        m.label({ val, uri: `at://${author.did}/app.bsky.feed.post/fake` }),\n      ),\n      embed: scenario.quoteAuthor\n        ? m.embedRecordView({\n            record: m.post({\n              text: 'Quoted post text',\n            }),\n            labels: (scenario.labels.quotedPost || []).map((val) =>\n              m.label({\n                val,\n                uri: `at://${author.did}/app.bsky.feed.post/fake`,\n              }),\n            ),\n            author: this.profileViewBasic(scenario.quoteAuthor, {\n              account: scenario.labels.quotedAccount,\n            }),\n          })\n        : undefined,\n    })\n  }\n\n  profileScenario(scenario: ModerationTestSuiteScenario) {\n    if (scenario.subject !== 'profile') {\n      throw new Error('Scenario subject must be \"profile\"')\n    }\n    return this.profileViewBasic(scenario.author, scenario.labels)\n  }\n\n  profileViewBasic(\n    name: string,\n    scenarioLabels: ModerationTestSuiteScenario['labels'],\n  ) {\n    const def = this.users[name]\n\n    const labels: ComAtprotoLabelDefs.Label[] = []\n    if (scenarioLabels.account) {\n      for (const l of scenarioLabels.account) {\n        labels.push(m.label({ val: l, uri: `did:web:${name}` }))\n      }\n    }\n    if (scenarioLabels.profile) {\n      for (const l of scenarioLabels.profile) {\n        labels.push(\n          m.label({\n            val: l,\n            uri: `at://did:web:${name}/app.bsky.actor.profile/self`,\n          }),\n        )\n      }\n    }\n\n    return m.profileViewBasic({\n      handle: `${name}.test`,\n      labels,\n      viewer: m.actorViewerState({\n        muted: def.muted || def.mutedByList,\n        mutedByList: def.mutedByList\n          ? m.listViewBasic({ name: 'Fake List' })\n          : undefined,\n        blockedBy: def.blockedBy,\n        blocking:\n          def.blocking || def.blockingByList\n            ? 'at://did:web:self.test/app.bsky.graph.block/fake'\n            : undefined,\n        blockingByList: def.blockingByList\n          ? m.listViewBasic({ name: 'Fake List' })\n          : undefined,\n      }),\n    })\n  }\n\n  moderationOpts(scenario: ModerationTestSuiteScenario): ModerationOpts {\n    return {\n      userDid:\n        this.configurations[scenario.cfg].authed === false\n          ? ''\n          : 'did:web:self.test',\n      prefs: {\n        adultContentEnabled: Boolean(\n          this.configurations[scenario.cfg]?.adultContentEnabled,\n        ),\n        labels: this.configurations[scenario.cfg].settings || {},\n        labelers: [\n          {\n            did: 'did:plc:fake-labeler',\n            labels: {},\n          },\n        ],\n        mutedWords: [],\n        hiddenPosts: [],\n      },\n    }\n  }\n}\n"
  },
  {
    "path": "packages/api/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/api/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/api/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"types\": [\"jest\", \"./jest.d.ts\"],\n    \"noEmit\": true,\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/aws/CHANGELOG.md",
    "content": "# @atproto/aws\n\n## 0.2.31\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n  - @atproto/common@0.5.0\n  - @atproto/repo@0.8.11\n  - @atproto/crypto@0.4.4\n\n## 0.2.30\n\n### Patch Changes\n\n- Updated dependencies [[`8dd77bad2`](https://github.com/bluesky-social/atproto/commit/8dd77bad2fdee20e39d3787198d960c19d8df3d0)]:\n  - @atproto/repo@0.8.10\n\n## 0.2.29\n\n### Patch Changes\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Optimistically attempt to move files before checking for their existence, resulting in faster `makePermanent` calls\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `S3BlobStore`'s `deleteMany` now supports any number of input (and will process deletes by chunks internally)\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `@aws-sdk` dependencies\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Set a timeout (of 10 seconds by default) on every `S3BlobStore` requests\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/repo@0.8.9\n  - @atproto/common-web@0.4.3\n  - @atproto/common@0.4.12\n  - @atproto/crypto@0.4.4\n\n## 0.2.28\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.8.8\n\n## 0.2.27\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.8.7\n\n## 0.2.26\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/repo@0.8.6\n\n## 0.2.25\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.8.5\n\n## 0.2.24\n\n### Patch Changes\n\n- Updated dependencies [[`a8dee6af3`](https://github.com/bluesky-social/atproto/commit/a8dee6af33618d3072ebae7f23843242a32c926c)]:\n  - @atproto/repo@0.8.4\n\n## 0.2.23\n\n### Patch Changes\n\n- Updated dependencies [[`5fccbd2a1`](https://github.com/bluesky-social/atproto/commit/5fccbd2a14420e4a7c6f56ad9af4ecfe15a971e3)]:\n  - @atproto/repo@0.8.3\n\n## 0.2.22\n\n### Patch Changes\n\n- Updated dependencies [[`8bd45e2f8`](https://github.com/bluesky-social/atproto/commit/8bd45e2f898a87b3550c7f4a0c8312fad9cb4736)]:\n  - @atproto/repo@0.8.2\n\n## 0.2.21\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.11\n  - @atproto/repo@0.8.1\n  - @atproto/crypto@0.4.4\n\n## 0.2.20\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144), [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/repo@0.8.0\n  - @atproto/common@0.4.10\n  - @atproto/crypto@0.4.4\n\n## 0.2.19\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/repo@0.7.3\n  - @atproto/common@0.4.9\n  - @atproto/crypto@0.4.4\n\n## 0.2.18\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.7.2\n\n## 0.2.17\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.7.1\n\n## 0.2.16\n\n### Patch Changes\n\n- Updated dependencies [[`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f)]:\n  - @atproto/repo@0.7.0\n\n## 0.2.15\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.6.5\n\n## 0.2.14\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/common@0.4.8\n  - @atproto/crypto@0.4.4\n  - @atproto/repo@0.6.4\n\n## 0.2.13\n\n### Patch Changes\n\n- Updated dependencies [[`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/common@0.4.7\n  - @atproto/crypto@0.4.3\n  - @atproto/repo@0.6.3\n\n## 0.2.12\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n  - @atproto/repo@0.6.2\n\n## 0.2.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.6\n  - @atproto/repo@0.6.1\n  - @atproto/crypto@0.4.2\n\n## 0.2.10\n\n### Patch Changes\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a)]:\n  - @atproto/common@0.4.5\n  - @atproto/repo@0.6.0\n  - @atproto/crypto@0.4.2\n\n## 0.2.9\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.5.5\n\n## 0.2.8\n\n### Patch Changes\n\n- Updated dependencies [[`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0)]:\n  - @atproto/crypto@0.4.2\n  - @atproto/repo@0.5.4\n\n## 0.2.7\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]:\n  - @atproto/common@0.4.4\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.5.3\n\n## 0.2.6\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/common@0.4.3\n  - @atproto/repo@0.5.2\n  - @atproto/crypto@0.4.1\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/common@0.4.2\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.5.1\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442)]:\n  - @atproto/repo@0.5.0\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31)]:\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.4.3\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.4.2\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/common@0.4.1\n  - @atproto/crypto@0.4.0\n  - @atproto/repo@0.4.1\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common@0.4.0\n  - @atproto/crypto@0.4.0\n  - @atproto/repo@0.4.0\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.3.4\n  - @atproto/repo@0.3.9\n  - @atproto/crypto@0.3.0\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.3.8\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`fcf8e3faf`](https://github.com/bluesky-social/atproto/commit/fcf8e3faf311559162c3aa0d9af36f84951914bc)]:\n  - @atproto/repo@0.3.7\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.3.6\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n  - @atproto/repo@0.3.5\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/common@0.3.3\n  - @atproto/crypto@0.2.3\n  - @atproto/repo@0.3.4\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.3.3\n  - @atproto/common@0.3.2\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.3.2\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/repo@0.3.1\n"
  },
  {
    "path": "packages/aws/README.md",
    "content": "# AWS KMS\n\nA Keypair-compatible wrapper for AWS KMS.\n"
  },
  {
    "path": "packages/aws/package.json",
    "content": "{\n  \"name\": \"@atproto/aws\",\n  \"version\": \"0.2.31\",\n  \"license\": \"MIT\",\n  \"description\": \"Shared AWS cloud API helpers for atproto services\",\n  \"keywords\": [\n    \"atproto\",\n    \"aws\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/aws\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/repo\": \"workspace:^\",\n    \"@aws-sdk/client-cloudfront\": \"^3.879.0\",\n    \"@aws-sdk/client-kms\": \"^3.879.0\",\n    \"@aws-sdk/client-s3\": \"^3.879.0\",\n    \"@aws-sdk/lib-storage\": \"3.879.0\",\n    \"@noble/curves\": \"^1.7.0\",\n    \"key-encoder\": \"^2.0.3\",\n    \"multiformats\": \"^9.9.0\",\n    \"uint8arrays\": \"3.0.0\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/aws/src/bunny.ts",
    "content": "import { allFulfilled } from '@atproto/common'\nimport { ImageInvalidator } from './types'\n\nexport type BunnyConfig = {\n  accessKey: string\n  urlPrefix: string\n}\n\nconst API_PURGE_URL = 'https://api.bunny.net/purge'\n\nexport class BunnyInvalidator implements ImageInvalidator {\n  constructor(public cfg: BunnyConfig) {}\n  async invalidate(_subject: string, paths: string[]) {\n    await allFulfilled(\n      paths.map(async (path) =>\n        purgeUrl({\n          url: this.cfg.urlPrefix + path,\n          accessKey: this.cfg.accessKey,\n        }),\n      ),\n    )\n  }\n}\n\nexport default BunnyInvalidator\n\nasync function purgeUrl(opts: { accessKey: string; url: string }) {\n  const search = new URLSearchParams()\n  search.set('async', 'true')\n  search.set('url', opts.url)\n  await fetch(API_PURGE_URL + '?' + search.toString(), {\n    method: 'post',\n    headers: { AccessKey: opts.accessKey },\n  })\n}\n"
  },
  {
    "path": "packages/aws/src/cloudfront.ts",
    "content": "import * as aws from '@aws-sdk/client-cloudfront'\nimport { ImageInvalidator } from './types'\n\nexport type CloudfrontConfig = {\n  distributionId: string\n  pathPrefix?: string\n} & Omit<aws.CloudFrontClientConfig, 'apiVersion'>\n\nexport class CloudfrontInvalidator implements ImageInvalidator {\n  distributionId: string\n  pathPrefix: string\n  client: aws.CloudFront\n  constructor(cfg: CloudfrontConfig) {\n    const { distributionId, pathPrefix, ...rest } = cfg\n    this.distributionId = distributionId\n    this.pathPrefix = pathPrefix ?? ''\n    this.client = new aws.CloudFront({\n      ...rest,\n      apiVersion: '2020-05-31',\n    })\n  }\n  async invalidate(subject: string, paths: string[]) {\n    await this.client.createInvalidation({\n      DistributionId: this.distributionId,\n      InvalidationBatch: {\n        CallerReference: `cf-invalidator-${subject}-${Date.now()}`,\n        Paths: {\n          Quantity: paths.length,\n          Items: paths.map((path) => this.pathPrefix + path),\n        },\n      },\n    })\n  }\n}\n\nexport default CloudfrontInvalidator\n"
  },
  {
    "path": "packages/aws/src/index.ts",
    "content": "export * from './kms'\nexport * from './s3'\nexport * from './cloudfront'\nexport * from './bunny'\nexport * from './util'\nexport * from './types'\n"
  },
  {
    "path": "packages/aws/src/kms.ts",
    "content": "import * as aws from '@aws-sdk/client-kms'\nimport { secp256k1 as noble } from '@noble/curves/secp256k1'\nimport KeyEncoder from 'key-encoder'\nimport * as ui8 from 'uint8arrays'\nimport * as crypto from '@atproto/crypto'\n\nconst keyEncoder = new KeyEncoder('secp256k1')\n\nexport type KmsConfig = { keyId: string } & Omit<\n  aws.KMSClientConfig,\n  'apiVersion'\n>\n\nexport class KmsKeypair implements crypto.Keypair {\n  jwtAlg = crypto.SECP256K1_JWT_ALG\n\n  constructor(\n    private client: aws.KMS,\n    private keyId: string,\n    private publicKey: Uint8Array,\n  ) {}\n\n  static async load(cfg: KmsConfig) {\n    const { keyId, ...rest } = cfg\n    const client = new aws.KMS({\n      ...rest,\n      apiVersion: '2014-11-01',\n    })\n    const res = await client.getPublicKey({ KeyId: keyId })\n    if (!res.PublicKey) {\n      throw new Error('Could not find public key')\n    }\n    // public key comes back DER-encoded, so we translate it to raw 65 byte encoding\n    const rawPublicKeyHex = keyEncoder.encodePublic(\n      Buffer.from(res.PublicKey),\n      'der',\n      'raw',\n    )\n    const publicKey = ui8.fromString(rawPublicKeyHex, 'hex')\n    return new KmsKeypair(client, keyId, publicKey)\n  }\n\n  did(): string {\n    return crypto.formatDidKey(this.jwtAlg, this.publicKey)\n  }\n\n  async sign(msg: Uint8Array): Promise<Uint8Array> {\n    const res = await this.client.sign({\n      KeyId: this.keyId,\n      Message: msg,\n      SigningAlgorithm: 'ECDSA_SHA_256',\n    })\n    if (!res.Signature) {\n      throw new Error('Could not get signature')\n    }\n    // signature comes back DER encoded & not-normalized\n    // we translate to raw 64 byte encoding\n    // we also normalize s as no more than 1/2 prime order to pass strict verification\n    // (prevents duplicating a signature)\n    // more: https://github.com/bitcoin-core/secp256k1/blob/a1102b12196ea27f44d6201de4d25926a2ae9640/include/secp256k1.h#L530-L534\n    const sig = noble.Signature.fromDER(res.Signature)\n    const normalized = sig.normalizeS()\n    return normalized.toCompactRawBytes()\n  }\n}\n\nexport default KmsKeypair\n"
  },
  {
    "path": "packages/aws/src/s3.ts",
    "content": "import stream from 'node:stream'\nimport { NoSuchKey, S3, S3ClientConfig } from '@aws-sdk/client-s3'\nimport { Upload } from '@aws-sdk/lib-storage'\nimport { CID } from 'multiformats/cid'\nimport { SECOND, aggregateErrors, chunkArray } from '@atproto/common-web'\nimport { randomStr } from '@atproto/crypto'\nimport { BlobNotFoundError, BlobStore } from '@atproto/repo'\n\nexport type S3Config = {\n  bucket: string\n  /**\n   * The maximum time any request to S3 (including individual blob chunks\n   * uploads) can take, in milliseconds.\n   */\n  requestTimeoutMs?: number\n  /**\n   * The maximum total time a blob upload can take, in milliseconds.\n   */\n  uploadTimeoutMs?: number\n} & Omit<S3ClientConfig, 'apiVersion' | 'requestHandler'>\n\nexport class S3BlobStore implements BlobStore {\n  private client: S3\n  private bucket: string\n  private uploadTimeoutMs: number\n\n  constructor(\n    public did: string,\n    cfg: S3Config,\n  ) {\n    const {\n      bucket,\n      uploadTimeoutMs = 10 * SECOND,\n      requestTimeoutMs = uploadTimeoutMs,\n      ...rest\n    } = cfg\n    this.bucket = bucket\n    this.uploadTimeoutMs = uploadTimeoutMs\n    this.client = new S3({\n      ...rest,\n      apiVersion: '2006-03-01',\n      // Ensures that all requests timeout under \"requestTimeoutMs\".\n      //\n      // @NOTE This will also apply to the upload of each individual chunk\n      // when using Upload from @aws-sdk/lib-storage.\n      requestHandler: { requestTimeout: requestTimeoutMs },\n    })\n  }\n\n  static creator(cfg: S3Config) {\n    return (did: string) => {\n      return new S3BlobStore(did, cfg)\n    }\n  }\n\n  private genKey() {\n    return randomStr(32, 'base32')\n  }\n\n  private getTmpPath(key: string): string {\n    return `tmp/${this.did}/${key}`\n  }\n\n  private getStoredPath(cid: CID): string {\n    return `blocks/${this.did}/${cid.toString()}`\n  }\n\n  private getQuarantinedPath(cid: CID): string {\n    return `quarantine/${this.did}/${cid.toString()}`\n  }\n\n  private async uploadBytes(path: string, bytes: Uint8Array | stream.Readable) {\n    // @NOTE we use Upload rather than client.putObject because stream length is\n    // not known in advance. See also aws/aws-sdk-js-v3#2348.\n    //\n    // See also https://github.com/aws/aws-sdk-js-v3/issues/6426, wherein Upload\n    // may hang the s3 connection under certain circumstances. We don't have a\n    // good way to avoid this, so we use timeouts defensively on all s3\n    // requests.\n\n    const abortSignal = AbortSignal.timeout(this.uploadTimeoutMs)\n    const abortController = new AbortController()\n    abortSignal.addEventListener('abort', () => abortController.abort())\n\n    const upload = new Upload({\n      client: this.client,\n      params: {\n        Bucket: this.bucket,\n        Body: bytes,\n        Key: path,\n      },\n      // @ts-ignore native implementation fine in node >=15\n      abortController,\n    })\n\n    try {\n      await upload.done()\n    } catch (err) {\n      // Translate aws-sdk's abort error to something more specific\n      if (err instanceof Error && err.name === 'AbortError') {\n        throw new Error('Blob upload timed out', { cause: err })\n      }\n\n      throw err\n    }\n  }\n\n  async putTemp(bytes: Uint8Array | stream.Readable): Promise<string> {\n    const key = this.genKey()\n    await this.uploadBytes(this.getTmpPath(key), bytes)\n    return key\n  }\n\n  async makePermanent(key: string, cid: CID): Promise<void> {\n    try {\n      // @NOTE we normally call this method when we know the file is temporary.\n      // Because of this, we optimistically move the file, allowing to make\n      // fewer network requests in the happy path.\n      await this.move({\n        from: this.getTmpPath(key),\n        to: this.getStoredPath(cid),\n      })\n    } catch (err) {\n      // If the optimistic move failed because the temp file was not found,\n      // check if the permanent file already exists. If it does, we can assume\n      // that another process made the file permanent concurrently, and we can\n      // no-op.\n      if (err instanceof BlobNotFoundError) {\n        // Blob was not found from temp storage...\n        const alreadyHas = await this.hasStored(cid)\n        // already saved, so we no-op\n        if (alreadyHas) return\n      }\n\n      throw err\n    }\n  }\n\n  async putPermanent(\n    cid: CID,\n    bytes: Uint8Array | stream.Readable,\n  ): Promise<void> {\n    await this.uploadBytes(this.getStoredPath(cid), bytes)\n  }\n\n  async quarantine(cid: CID): Promise<void> {\n    await this.move({\n      from: this.getStoredPath(cid),\n      to: this.getQuarantinedPath(cid),\n    })\n  }\n\n  async unquarantine(cid: CID): Promise<void> {\n    await this.move({\n      from: this.getQuarantinedPath(cid),\n      to: this.getStoredPath(cid),\n    })\n  }\n\n  private async getObject(cid: CID) {\n    const res = await this.client.getObject({\n      Bucket: this.bucket,\n      Key: this.getStoredPath(cid),\n    })\n    if (res.Body) {\n      return res.Body\n    } else {\n      throw new BlobNotFoundError()\n    }\n  }\n\n  async getBytes(cid: CID): Promise<Uint8Array> {\n    const res = await this.getObject(cid)\n    return res.transformToByteArray()\n  }\n\n  async getStream(cid: CID): Promise<stream.Readable> {\n    const res = await this.getObject(cid)\n    return res as stream.Readable\n  }\n\n  async delete(cid: CID): Promise<void> {\n    await this.deleteKey(this.getStoredPath(cid))\n  }\n\n  async deleteMany(cids: CID[]): Promise<void> {\n    const errors: unknown[] = []\n    for (const chunk of chunkArray(cids, 500)) {\n      try {\n        const keys = chunk.map((cid) => this.getStoredPath(cid))\n        await this.deleteManyKeys(keys)\n      } catch (err) {\n        errors.push(err)\n      }\n    }\n    if (errors.length) throw aggregateErrors(errors)\n  }\n\n  async hasStored(cid: CID): Promise<boolean> {\n    return this.hasKey(this.getStoredPath(cid))\n  }\n\n  async hasTemp(key: string): Promise<boolean> {\n    return this.hasKey(this.getTmpPath(key))\n  }\n\n  private async hasKey(key: string) {\n    try {\n      const res = await this.client.headObject({\n        Bucket: this.bucket,\n        Key: key,\n      })\n      return res.$metadata.httpStatusCode === 200\n    } catch (err) {\n      return false\n    }\n  }\n\n  private async deleteKey(key: string) {\n    await this.client.deleteObject({\n      Bucket: this.bucket,\n      Key: key,\n    })\n  }\n\n  private async deleteManyKeys(keys: string[]) {\n    await this.client.deleteObjects({\n      Bucket: this.bucket,\n      Delete: {\n        Objects: keys.map((k) => ({ Key: k })),\n      },\n    })\n  }\n\n  private async move(keys: { from: string; to: string }) {\n    try {\n      await this.client.copyObject({\n        Bucket: this.bucket,\n        CopySource: `${this.bucket}/${keys.from}`,\n        Key: keys.to,\n      })\n    } catch (cause) {\n      if (cause instanceof NoSuchKey) {\n        // Already deleted, possibly by a concurrently running process\n        throw new BlobNotFoundError(undefined, { cause })\n      }\n\n      throw cause\n    }\n\n    try {\n      await this.client.deleteObject({\n        Bucket: this.bucket,\n        Key: keys.from,\n      })\n    } catch (err) {\n      if (err instanceof NoSuchKey) {\n        // Already deleted, possibly by a concurrently running process\n        return\n      }\n\n      throw err\n    }\n  }\n}\n\nexport default S3BlobStore\n"
  },
  {
    "path": "packages/aws/src/types.ts",
    "content": "// @NOTE keep in sync with same interface in bsky/src/image/invalidator.ts\n// this is separate to avoid the dependency on @atproto/bsky.\nexport interface ImageInvalidator {\n  invalidate(subject: string, paths: string[]): Promise<void>\n}\n"
  },
  {
    "path": "packages/aws/src/util.ts",
    "content": "import { allFulfilled } from '@atproto/common'\nimport { ImageInvalidator } from './types'\n\nexport class MultiImageInvalidator implements ImageInvalidator {\n  constructor(public invalidators: ImageInvalidator[]) {}\n  async invalidate(subject: string, paths: string[]) {\n    await allFulfilled(\n      this.invalidators.map((invalidator) =>\n        invalidator.invalidate(subject, paths),\n      ),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/aws/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/aws/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/bsky/CHANGELOG.md",
    "content": "# @atproto/bsky\n\n## 0.0.221\n\n### Patch Changes\n\n- [#4747](https://github.com/bluesky-social/atproto/pull/4747) [`3b41b81`](https://github.com/bluesky-social/atproto/commit/3b41b81e27e0aba55406642c07da01c290281647) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove deprecated handling from `getSuggestedFollowsByActor`\n\n- [#4767](https://github.com/bluesky-social/atproto/pull/4767) [`4ecde48`](https://github.com/bluesky-social/atproto/commit/4ecde4879ffd769fe2c7a0f1d4e3275c776114f4) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add feature gate to enable social proof on `getSuggestedFollowsByActor`\n\n- Updated dependencies []:\n  - @atproto/xrpc-server@0.10.17\n  - @atproto/common@0.5.15\n\n## 0.0.220\n\n### Patch Changes\n\n- [#4712](https://github.com/bluesky-social/atproto/pull/4712) [`383e157`](https://github.com/bluesky-social/atproto/commit/383e157021564a6fb51baac584dd3e4f988f1d33) Thanks [@devinivy](https://github.com/devinivy)! - remove format from img urls by default\n\n- [#4723](https://github.com/bluesky-social/atproto/pull/4723) [`7ed5704`](https://github.com/bluesky-social/atproto/commit/7ed57043c12aedb0faf6b7dc947adfcfff570b6d) Thanks [@devinivy](https://github.com/devinivy)! - switch default image format to jpeg temporarily\n\n- [#4746](https://github.com/bluesky-social/atproto/pull/4746) [`eaee3d4`](https://github.com/bluesky-social/atproto/commit/eaee3d430554436964d45f38bbeb1132ae9b8862) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Serialize `pagerank` float values in `debug` field on profiles.\n\n- [#4753](https://github.com/bluesky-social/atproto/pull/4753) [`ff42a3a`](https://github.com/bluesky-social/atproto/commit/ff42a3afc3a0d4146a6618a910fa612c7e878ea7) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Prefer returning `undefined` over year 0001 dates on bsky views\n\n- [#4762](https://github.com/bluesky-social/atproto/pull/4762) [`bc69b03`](https://github.com/bluesky-social/atproto/commit/bc69b03f53da3ec52bc3eed0738308f320386e75) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Improve zero-date handling\n\n- [#4755](https://github.com/bluesky-social/atproto/pull/4755) [`139b294`](https://github.com/bluesky-social/atproto/commit/139b2941d640bafa1e7d3a56e0608dc42bb0006c) Thanks [@devinivy](https://github.com/devinivy)! - Remove feature gate for including format in image URLs.\n\n- Updated dependencies [[`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa), [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f), [`192685f`](https://github.com/bluesky-social/atproto/commit/192685fca75a68c9c50a94817d3f27da7fc02f56)]:\n  - @atproto/api@0.19.4\n  - @atproto/syntax@0.5.1\n  - @atproto/repo@0.8.13\n  - @atproto/xrpc-server@0.10.16\n\n## 0.0.219\n\n### Patch Changes\n\n- [#4683](https://github.com/bluesky-social/atproto/pull/4683) [`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Introduce recIdStr field\n\n- Updated dependencies [[`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c), [`0e5df95`](https://github.com/bluesky-social/atproto/commit/0e5df95e3a8d81931524848d301cd43d1f12fb78)]:\n  - @atproto/api@0.19.2\n\n## 0.0.218\n\n### Patch Changes\n\n- [#4704](https://github.com/bluesky-social/atproto/pull/4704) [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Add feed to sendInteractions input\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`138f0a0`](https://github.com/bluesky-social/atproto/commit/138f0a0b374c0d78372d5095237061d46db75a32), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common@0.5.14\n  - @atproto/sync@0.1.40\n  - @atproto/xrpc-server@0.10.15\n  - @atproto/api@0.19.1\n  - @atproto/lexicon@0.6.2\n\n## 0.0.217\n\n### Patch Changes\n\n- Updated dependencies [[`450f085`](https://github.com/bluesky-social/atproto/commit/450f0856630fa08c20dc60fef8b5d2a07b9a2552)]:\n  - @atproto/api@0.19.0\n\n## 0.0.216\n\n### Patch Changes\n\n- [#4647](https://github.com/bluesky-social/atproto/pull/4647) [`978a99e`](https://github.com/bluesky-social/atproto/commit/978a99efad8393247449bebd88af1ac5b602842e) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Use correct `suggestionsAgent` method `getOnboardingSuggestedUsersSkeleton`\n\n## 0.0.215\n\n### Patch Changes\n\n- [#4594](https://github.com/bluesky-social/atproto/pull/4594) [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317) Thanks [@bnewbold](https://github.com/bnewbold)! - update germ networks lexicon\n\n- [#4594](https://github.com/bluesky-social/atproto/pull/4594) [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317) Thanks [@bnewbold](https://github.com/bnewbold)! - add `none` to germ declaration record\n\n- Updated dependencies [[`60f84eb`](https://github.com/bluesky-social/atproto/commit/60f84ebe47016828add07b143c403e331c58ee78), [`50dfbec`](https://github.com/bluesky-social/atproto/commit/50dfbec512682d35e8108b952e8f0533da71beef), [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317), [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317)]:\n  - @atproto/api@0.18.21\n\n## 0.0.214\n\n### Patch Changes\n\n- [#4591](https://github.com/bluesky-social/atproto/pull/4591) [`4f5c400`](https://github.com/bluesky-social/atproto/commit/4f5c4001271bbf38b30506efd30ebdabb969878f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Rename `platform` to `deviceName` on `draft` view, add maxLength.\n\n- Updated dependencies [[`4f5c400`](https://github.com/bluesky-social/atproto/commit/4f5c4001271bbf38b30506efd30ebdabb969878f)]:\n  - @atproto/api@0.18.20\n\n## 0.0.213\n\n### Patch Changes\n\n- [#4590](https://github.com/bluesky-social/atproto/pull/4590) [`25cea46`](https://github.com/bluesky-social/atproto/commit/25cea46aaa3d84521d1e977b67d3ac3581304ba1) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `deviceId` and `platform` to drafts as optional props\n\n- Updated dependencies [[`25cea46`](https://github.com/bluesky-social/atproto/commit/25cea46aaa3d84521d1e977b67d3ac3581304ba1), [`49b3806`](https://github.com/bluesky-social/atproto/commit/49b38069ed4b5bd1ef71e967c78e5123b1c1f6f1)]:\n  - @atproto/api@0.18.19\n  - @atproto/common@0.5.10\n  - @atproto/xrpc-server@0.10.11\n\n## 0.0.212\n\n### Patch Changes\n\n- [#4581](https://github.com/bluesky-social/atproto/pull/4581) [`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7) Thanks [@mozzius](https://github.com/mozzius)! - Add `presentation` to video embed as a hint to the client about how to display the video\n\n- Updated dependencies [[`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/api@0.18.18\n  - @atproto/did@0.3.0\n\n## 0.0.211\n\n### Patch Changes\n\n- [#4565](https://github.com/bluesky-social/atproto/pull/4565) [`cbd5837`](https://github.com/bluesky-social/atproto/commit/cbd5837f015e6b5e098a60098faea82e7f9419f3) Thanks [@cuducos](https://github.com/cuducos)! - Re-add `recId` to suggested users (now, as string)\n\n- [#4547](https://github.com/bluesky-social/atproto/pull/4547) [`d8e5363`](https://github.com/bluesky-social/atproto/commit/d8e53636c84da6dd3dd69e1d260f4fa617f3883c) Thanks [@cuducos](https://github.com/cuducos)! - Removes `recId` from suggested users — we need it as a string, so we're gonna re-add it as string (instead of integer) later.\n\n- [#4415](https://github.com/bluesky-social/atproto/pull/4415) [`9bdd358`](https://github.com/bluesky-social/atproto/commit/9bdd35881aa7efce6595ef708ba13d99c473d114) Thanks [@bnewbold](https://github.com/bnewbold)! - support for Germ Networks chat declaration records\n\n- [#4526](https://github.com/bluesky-social/atproto/pull/4526) [`e6e43f3`](https://github.com/bluesky-social/atproto/commit/e6e43f3ad3594e7cb24e2f3effe5ef4b1696c8ff) Thanks [@cuducos](https://github.com/cuducos)! - Swaps StatSig for GrowthBook in AppView\n\n- [#4576](https://github.com/bluesky-social/atproto/pull/4576) [`ce356cd`](https://github.com/bluesky-social/atproto/commit/ce356cde55c9ff46758d0a6f39397d6710509b40) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use requester DID to accurately perform personalized suggestions\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performances of `noUndefinedVals` utility\n\n- Updated dependencies [[`cbd5837`](https://github.com/bluesky-social/atproto/commit/cbd5837f015e6b5e098a60098faea82e7f9419f3), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`d8e5363`](https://github.com/bluesky-social/atproto/commit/d8e53636c84da6dd3dd69e1d260f4fa617f3883c), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`9bdd358`](https://github.com/bluesky-social/atproto/commit/9bdd35881aa7efce6595ef708ba13d99c473d114), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/api@0.18.17\n  - @atproto/syntax@0.4.3\n  - @atproto/lexicon@0.6.1\n  - @atproto/common@0.5.9\n  - @atproto/xrpc-server@0.10.10\n\n## 0.0.210\n\n### Patch Changes\n\n- [#4524](https://github.com/bluesky-social/atproto/pull/4524) [`6752056`](https://github.com/bluesky-social/atproto/commit/6752056f4666f1f85149d1c6821aed1ad8d88442) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `unregisterPush` to appview server routes.\n\n## 0.0.209\n\n### Patch Changes\n\n- [#4520](https://github.com/bluesky-social/atproto/pull/4520) [`d2ed731`](https://github.com/bluesky-social/atproto/commit/d2ed7311a20b8c990003628c932e3e5aa6569086) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `isDisabled` to `#statusView`\n\n- Updated dependencies [[`d2ed731`](https://github.com/bluesky-social/atproto/commit/d2ed7311a20b8c990003628c932e3e5aa6569086)]:\n  - @atproto/api@0.18.13\n\n## 0.0.208\n\n### Patch Changes\n\n- [`b329266`](https://github.com/bluesky-social/atproto/commit/b329266853b4867fbbcafc8845e479c888f8ac36) Thanks [@mary-ext](https://github.com/mary-ext)! - properly convert did:web to service endpoints in `.well-known/did.json`\n\n- Updated dependencies []:\n  - @atproto/common@0.5.7\n  - @atproto/xrpc-server@0.10.8\n\n## 0.0.207\n\n### Patch Changes\n\n- [#4516](https://github.com/bluesky-social/atproto/pull/4516) [`7750b91`](https://github.com/bluesky-social/atproto/commit/7750b91500eef6965a17bc8ec0b3ddfd6327485a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `uri` and `cid` to `#statusView`\n\n- Updated dependencies [[`7750b91`](https://github.com/bluesky-social/atproto/commit/7750b91500eef6965a17bc8ec0b3ddfd6327485a)]:\n  - @atproto/api@0.18.12\n\n## 0.0.206\n\n### Patch Changes\n\n- [#4513](https://github.com/bluesky-social/atproto/pull/4513) [`7ef8935`](https://github.com/bluesky-social/atproto/commit/7ef893563b25252ecf246e0d75e17855a7284e53) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `minAccessAge` to Age Assurance regional configs.\n\n- Updated dependencies [[`7ef8935`](https://github.com/bluesky-social/atproto/commit/7ef893563b25252ecf246e0d75e17855a7284e53)]:\n  - @atproto/api@0.18.11\n\n## 0.0.205\n\n### Patch Changes\n\n- [#4440](https://github.com/bluesky-social/atproto/pull/4440) [`63f97ae`](https://github.com/bluesky-social/atproto/commit/63f97ae9c1f57def2d489ab8ce7f83a84a7d1ba1) Thanks [@iwsmith](https://github.com/iwsmith)! - Add `recID` field to `getSuggestedUsers` and `getSuggestedUsersSkeleton`\n\n- Updated dependencies [[`63f97ae`](https://github.com/bluesky-social/atproto/commit/63f97ae9c1f57def2d489ab8ce7f83a84a7d1ba1)]:\n  - @atproto/api@0.18.10\n\n## 0.0.204\n\n### Patch Changes\n\n- [#4426](https://github.com/bluesky-social/atproto/pull/4426) [`ce497e8`](https://github.com/bluesky-social/atproto/commit/ce497e85437c7ced3147691fb877e1f76f6ff472) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add Age Assurance config for Virginia\n\n- Updated dependencies [[`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164), [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164)]:\n  - @atproto/api@0.18.9\n  - @atproto/common@0.5.4\n  - @atproto/xrpc-server@0.10.5\n\n## 0.0.203\n\n### Patch Changes\n\n- [#4460](https://github.com/bluesky-social/atproto/pull/4460) [`dd0fe8d`](https://github.com/bluesky-social/atproto/commit/dd0fe8d5e74e19b2cb37aa6a307b88f1f6bd1c9c) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add Age Assurance config for Tennessee\n\n## 0.0.202\n\n### Patch Changes\n\n- [#4441](https://github.com/bluesky-social/atproto/pull/4441) [`45928bf`](https://github.com/bluesky-social/atproto/commit/45928bfcd6d220216078d5106f134fc3a81f564b) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix optimistic response from `ageassurance.begin()`, retain existing `status` and `access` values if they exist.\n\n## 0.0.201\n\n### Patch Changes\n\n- [#4428](https://github.com/bluesky-social/atproto/pull/4428) [`6fab394`](https://github.com/bluesky-social/atproto/commit/6fab3940f6d09b4e9888e6c4140a70d3e4ebcb00) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Allow users to re-initiate Age Assurance so long as they're not in a `blocked` state.\n\n- Updated dependencies [[`39fa570`](https://github.com/bluesky-social/atproto/commit/39fa57080fa04aa547b093cfeaaced3e2e62fc41), [`f4cef84`](https://github.com/bluesky-social/atproto/commit/f4cef84494114ca927c66428920ca3dc24ad2b1e)]:\n  - @atproto/api@0.18.6\n\n## 0.0.200\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`380aa3b`](https://github.com/bluesky-social/atproto/commit/380aa3bfe73b5c4e59961c27ae988786b69c129d), [`308f432`](https://github.com/bluesky-social/atproto/commit/308f432f7aef196b4df0a6dc7c5367ab5a8b8964), [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/api@0.18.5\n  - @atproto/common@0.5.3\n  - @atproto/xrpc-server@0.10.3\n  - @atproto/repo@0.8.12\n  - @atproto/sync@0.1.39\n\n## 0.0.199\n\n### Patch Changes\n\n- [#4407](https://github.com/bluesky-social/atproto/pull/4407) [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds ageassurance namespace, methods, and utils for Age Assurance V2\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n  - @atproto/syntax@0.4.2\n  - @atproto/crypto@0.4.5\n  - @atproto/api@0.18.4\n  - @atproto/common@0.5.2\n  - @atproto/xrpc-server@0.10.2\n\n## 0.0.198\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/xrpc-server@0.10.0\n  - @atproto/common@0.5.0\n  - @atproto/did@0.2.2\n  - @atproto/api@0.18.2\n  - @atproto/identity@0.4.10\n  - @atproto/lexicon@0.5.2\n  - @atproto/repo@0.8.11\n  - @atproto-labs/xrpc-utils@0.0.24\n  - @atproto/sync@0.1.38\n  - @atproto/crypto@0.4.4\n\n## 0.0.197\n\n### Patch Changes\n\n- [#4344](https://github.com/bluesky-social/atproto/pull/4344) [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc) Thanks [@foysalit](https://github.com/foysalit)! - Add targetServices param to takedown events allowing mods to specify which service to apply takedown on\n\n- Updated dependencies [[`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e), [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc), [`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/api@0.18.1\n  - @atproto/xrpc-server@0.9.6\n  - @atproto-labs/xrpc-utils@0.0.23\n  - @atproto/sync@0.1.37\n\n## 0.0.196\n\n### Patch Changes\n\n- [#4333](https://github.com/bluesky-social/atproto/pull/4333) [`f8e56b387`](https://github.com/bluesky-social/atproto/commit/f8e56b387fcd3bc8405225c1bbdef66ca5dc1591) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Prevent usage of DPoP bound access tokens with bsky.social\n\n  Using DPoP bound access tokens against the bsky server would already fail, but\n  it would fail with a rather misleading error: \"InvalidToken: Bad token scope\",\n  because the `scope` on a DPoP bound access token is the actual OAuth scopes, not\n  the expected `com.atproto.access` string.\n\n  This change means the entryway explicit checks if the token is a DPoP bound\n  access token, and if it is, then it fails with an error \"Malformed token: DPoP\n  not supported\". A similar check is also done with Bearer tokens in the PDS.\n\n- [#4330](https://github.com/bluesky-social/atproto/pull/4330) [`3628cebfb`](https://github.com/bluesky-social/atproto/commit/3628cebfbb04ba49f326bbf411a2d15de2900302) Thanks [@mistydemeo](https://github.com/mistydemeo)! - adjust explicit-slurs regex\n\n## 0.0.195\n\n### Patch Changes\n\n- [#4269](https://github.com/bluesky-social/atproto/pull/4269) [`39b5c08e0`](https://github.com/bluesky-social/atproto/commit/39b5c08e0799468eba0c3bf50f4f5a8104c35f34) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Deprecate and remove `prioritizeFollowedUsers` setting from preferences response types and `getPostThreadV2` query params.\n\n- Updated dependencies [[`94ddc8219`](https://github.com/bluesky-social/atproto/commit/94ddc8219c144475df622137ab88895255136eda), [`756ab5d87`](https://github.com/bluesky-social/atproto/commit/756ab5d87fea75e8648a6bdd545d8b441bfb2dd6), [`39b5c08e0`](https://github.com/bluesky-social/atproto/commit/39b5c08e0799468eba0c3bf50f4f5a8104c35f34)]:\n  - @atproto/api@0.18.0\n  - @atproto/sync@0.1.36\n\n## 0.0.194\n\n### Patch Changes\n\n- Updated dependencies [[`15fe80c39`](https://github.com/bluesky-social/atproto/commit/15fe80c39ff428652dfaa6b30c0bdb59a145aac6)]:\n  - @atproto/api@0.17.7\n\n## 0.0.193\n\n### Patch Changes\n\n- [#4297](https://github.com/bluesky-social/atproto/pull/4297) [`cdb6b27fc`](https://github.com/bluesky-social/atproto/commit/cdb6b27fc6be1e858476d8c55fd0c37561b972b4) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `debug` field to `PostView` and `ProfileView*`s\n\n- Updated dependencies [[`7c1429fe3`](https://github.com/bluesky-social/atproto/commit/7c1429fe36226d0d57e57c037ba4221d2fbd57ee)]:\n  - @atproto/api@0.17.6\n\n## 0.0.192\n\n### Patch Changes\n\n- Updated dependencies [[`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031)]:\n  - @atproto/api@0.17.5\n\n## 0.0.191\n\n### Patch Changes\n\n- Updated dependencies [[`a8e307ef4`](https://github.com/bluesky-social/atproto/commit/a8e307ef4851b164ee38bb5149343631e329f143), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto/api@0.17.4\n  - @atproto-labs/fetch-node@0.2.0\n\n## 0.0.190\n\n### Patch Changes\n\n- Updated dependencies [[`386f583cf`](https://github.com/bluesky-social/atproto/commit/386f583cffa2c596a12be4e98dde498f3b8670f6)]:\n  - @atproto/api@0.17.3\n\n## 0.0.189\n\n### Patch Changes\n\n- Updated dependencies [[`1cb5b9b80`](https://github.com/bluesky-social/atproto/commit/1cb5b9b80c20a054f7fbacd89d0d440dc2241d81)]:\n  - @atproto/api@0.17.2\n\n## 0.0.188\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/api@0.17.1\n  - @atproto/did@0.2.1\n\n## 0.0.187\n\n### Patch Changes\n\n- Updated dependencies [[`dba2d30e2`](https://github.com/bluesky-social/atproto/commit/dba2d30e2c4ce0eb624f2139b485719d14474940), [`7f38ee03c`](https://github.com/bluesky-social/atproto/commit/7f38ee03c01357686a4ce54cdf8eed4e37074a58)]:\n  - @atproto/api@0.17.0\n\n## 0.0.186\n\n### Patch Changes\n\n- Updated dependencies [[`8dd77bad2`](https://github.com/bluesky-social/atproto/commit/8dd77bad2fdee20e39d3787198d960c19d8df3d0), [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33), [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33)]:\n  - @atproto/repo@0.8.10\n  - @atproto/api@0.16.11\n  - @atproto/sync@0.1.35\n\n## 0.0.185\n\n### Patch Changes\n\n- Updated dependencies [[`8dc4caf55`](https://github.com/bluesky-social/atproto/commit/8dc4caf55840578c835b4c851d4a599c15627a78)]:\n  - @atproto/api@0.16.10\n\n## 0.0.184\n\n### Patch Changes\n\n- Updated dependencies [[`ff30786af`](https://github.com/bluesky-social/atproto/commit/ff30786af6f72ad6506939bfca01a3f55a096c1c)]:\n  - @atproto/api@0.16.9\n\n## 0.0.183\n\n### Patch Changes\n\n- [#3881](https://github.com/bluesky-social/atproto/pull/3881) [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add expanded moderation report reasons as outlined in\n  [RFC-0009](https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity)\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523)]:\n  - @atproto/repo@0.8.9\n  - @atproto/api@0.16.8\n  - @atproto/sync@0.1.34\n  - @atproto/common@0.4.12\n  - @atproto/identity@0.4.9\n  - @atproto/lexicon@0.5.1\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.9.5\n  - @atproto-labs/xrpc-utils@0.0.22\n\n## 0.0.182\n\n### Patch Changes\n\n- Updated dependencies [[`09717f29a`](https://github.com/bluesky-social/atproto/commit/09717f29ac7ca742c9c3310980dbe4d112b7597f)]:\n  - @atproto/api@0.16.7\n\n## 0.0.181\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/did@0.2.0\n  - @atproto-labs/fetch-node@0.1.10\n  - @atproto/api@0.16.6\n  - @atproto/repo@0.8.8\n  - @atproto/sync@0.1.33\n  - @atproto/xrpc-server@0.9.4\n  - @atproto-labs/xrpc-utils@0.0.21\n\n## 0.0.180\n\n### Patch Changes\n\n- [#4142](https://github.com/bluesky-social/atproto/pull/4142) [`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - add com.atproto.temp.revokeAccountCredentials lexicon schema\n\n- Updated dependencies [[`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6)]:\n  - @atproto/api@0.16.5\n\n## 0.0.179\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/api@0.16.4\n  - @atproto/repo@0.8.7\n  - @atproto/sync@0.1.32\n  - @atproto/xrpc-server@0.9.3\n  - @atproto-labs/xrpc-utils@0.0.20\n\n## 0.0.178\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12), [`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/sync@0.1.31\n  - @atproto/repo@0.8.6\n  - @atproto/api@0.16.3\n  - @atproto/lexicon@0.4.13\n  - @atproto/xrpc-server@0.9.2\n  - @atproto-labs/xrpc-utils@0.0.19\n\n## 0.0.177\n\n### Patch Changes\n\n- Updated dependencies [[`c370d933b`](https://github.com/bluesky-social/atproto/commit/c370d933b76b4e15b83a82b40d1b6a32bd54add6)]:\n  - @atproto/api@0.16.2\n\n## 0.0.176\n\n### Patch Changes\n\n- [#3927](https://github.com/bluesky-social/atproto/pull/3927) [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef) Thanks [@foysalit](https://github.com/foysalit)! - Introduces ozone event timeline lexicons\n\n- Updated dependencies [[`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671)]:\n  - @atproto/xrpc-server@0.9.1\n  - @atproto/api@0.16.1\n  - @atproto-labs/xrpc-utils@0.0.18\n  - @atproto/sync@0.1.30\n\n## 0.0.175\n\n### Patch Changes\n\n- Updated dependencies [[`9751eebd7`](https://github.com/bluesky-social/atproto/commit/9751eebd718066984a91046b63e410caecd64022)]:\n  - @atproto/api@0.16.0\n\n## 0.0.174\n\n### Patch Changes\n\n- [#4058](https://github.com/bluesky-social/atproto/pull/4058) [`8787fd9de`](https://github.com/bluesky-social/atproto/commit/8787fd9dea769716412c9883e355cd496664bc6e) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Only allow initiating age assurance flow from certain states, return `InvalidInitiation` error if violated.\n\n- Updated dependencies [[`8787fd9de`](https://github.com/bluesky-social/atproto/commit/8787fd9dea769716412c9883e355cd496664bc6e), [`dc84906c8`](https://github.com/bluesky-social/atproto/commit/dc84906c865e8a97939a909dd3f75decde538363)]:\n  - @atproto/api@0.15.27\n\n## 0.0.173\n\n### Patch Changes\n\n- [#4050](https://github.com/bluesky-social/atproto/pull/4050) [`77c6dffd0`](https://github.com/bluesky-social/atproto/commit/77c6dffd0b3577a0fbdc5f0975c9eeb2a46b55c9) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add errors to app.bsky.unspecced.initAgeAssurance lexicon\n\n## 0.0.172\n\n### Patch Changes\n\n- [#4042](https://github.com/bluesky-social/atproto/pull/4042) [`2aecd2b29`](https://github.com/bluesky-social/atproto/commit/2aecd2b290849bf8fbef223464862732cc04d139) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Uppercase countryCode in `initAgeAssurance` method.\n\n- [#4041](https://github.com/bluesky-social/atproto/pull/4041) [`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add `unregisterPush` API\n\n- Updated dependencies [[`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82), [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e)]:\n  - @atproto/api@0.15.26\n\n## 0.0.171\n\n### Patch Changes\n\n- [#4028](https://github.com/bluesky-social/atproto/pull/4028) [`88c136427`](https://github.com/bluesky-social/atproto/commit/88c136427451a20d21812a1aa88a70cf21904138) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Age assurance compliance\n\n- Updated dependencies [[`88c136427`](https://github.com/bluesky-social/atproto/commit/88c136427451a20d21812a1aa88a70cf21904138)]:\n  - @atproto/api@0.15.25\n\n## 0.0.170\n\n### Patch Changes\n\n- Updated dependencies [[`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/xrpc-server@0.9.0\n  - @atproto/api@0.15.24\n  - @atproto/lexicon@0.4.12\n  - @atproto-labs/xrpc-utils@0.0.17\n  - @atproto/sync@0.1.29\n  - @atproto/repo@0.8.5\n\n## 0.0.169\n\n### Patch Changes\n\n- [#3991](https://github.com/bluesky-social/atproto/pull/3991) [`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8) Thanks [@foysalit](https://github.com/foysalit)! - Add modTool parameter to ozone events\n\n- Updated dependencies [[`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8)]:\n  - @atproto/api@0.15.23\n\n## 0.0.168\n\n### Patch Changes\n\n- Updated dependencies [[`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b)]:\n  - @atproto/api@0.15.22\n\n## 0.0.167\n\n### Patch Changes\n\n- Updated dependencies [[`d344723a1`](https://github.com/bluesky-social/atproto/commit/d344723a1018b2436b5453526397936bd587a2e2)]:\n  - @atproto/api@0.15.21\n\n## 0.0.166\n\n### Patch Changes\n\n- [#4005](https://github.com/bluesky-social/atproto/pull/4005) [`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7) Thanks [@mozzius](https://github.com/mozzius)! - add `subscribed-post` notification reason\n\n- Updated dependencies [[`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7)]:\n  - @atproto/api@0.15.20\n\n## 0.0.165\n\n### Patch Changes\n\n- Updated dependencies [[`376778a92`](https://github.com/bluesky-social/atproto/commit/376778a92f08fb6709c4cde736bfaca7393a72e1)]:\n  - @atproto/api@0.15.19\n\n## 0.0.164\n\n### Patch Changes\n\n- Updated dependencies [[`e3e31b2b9`](https://github.com/bluesky-social/atproto/commit/e3e31b2b9bf8c4de6b2d7fa992c3b3795686ea72)]:\n  - @atproto/api@0.15.18\n\n## 0.0.163\n\n### Patch Changes\n\n- [#3990](https://github.com/bluesky-social/atproto/pull/3990) [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add activity subscription lexicons\n\n- Updated dependencies [[`a8dee6af3`](https://github.com/bluesky-social/atproto/commit/a8dee6af33618d3072ebae7f23843242a32c926c), [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b)]:\n  - @atproto/repo@0.8.4\n  - @atproto/api@0.15.17\n  - @atproto/sync@0.1.28\n\n## 0.0.162\n\n### Patch Changes\n\n- Updated dependencies [[`5fccbd2a1`](https://github.com/bluesky-social/atproto/commit/5fccbd2a14420e4a7c6f56ad9af4ecfe15a971e3)]:\n  - @atproto/repo@0.8.3\n  - @atproto/sync@0.1.27\n\n## 0.0.161\n\n### Patch Changes\n\n- [#3966](https://github.com/bluesky-social/atproto/pull/3966) [`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef) Thanks [@mozzius](https://github.com/mozzius)! - Rename notification preference lexicon \"filter\" key to \"include\"\n\n- Updated dependencies [[`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef)]:\n  - @atproto/api@0.15.16\n\n## 0.0.160\n\n### Patch Changes\n\n- Updated dependencies [[`8bd45e2f8`](https://github.com/bluesky-social/atproto/commit/8bd45e2f898a87b3550c7f4a0c8312fad9cb4736)]:\n  - @atproto/repo@0.8.2\n  - @atproto/sync@0.1.26\n\n## 0.0.159\n\n### Patch Changes\n\n- Updated dependencies [[`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80), [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80)]:\n  - @atproto/api@0.15.15\n\n## 0.0.158\n\n### Patch Changes\n\n- Updated dependencies [[`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c)]:\n  - @atproto/xrpc-server@0.8.0\n  - @atproto-labs/xrpc-utils@0.0.16\n  - @atproto/sync@0.1.25\n\n## 0.0.157\n\n### Patch Changes\n\n- [#3901](https://github.com/bluesky-social/atproto/pull/3901) [`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1) Thanks [@mozzius](https://github.com/mozzius)! - Add notification preferences V2 lexicons\n\n- Updated dependencies [[`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1)]:\n  - @atproto/api@0.15.14\n\n## 0.0.156\n\n### Patch Changes\n\n- [#3929](https://github.com/bluesky-social/atproto/pull/3929) [`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Rename `getPostThreadHiddenV2` to `getPostThreadOtherV2` to better reflect the intent of the API.\n\n- Updated dependencies [[`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d)]:\n  - @atproto/api@0.15.13\n\n## 0.0.155\n\n### Patch Changes\n\n- Updated dependencies [[`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f)]:\n  - @atproto/xrpc-server@0.7.19\n  - @atproto-labs/xrpc-utils@0.0.15\n  - @atproto/sync@0.1.24\n\n## 0.0.154\n\n### Patch Changes\n\n- [#3912](https://github.com/bluesky-social/atproto/pull/3912) [`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Unify `getPostThreadV2` and `getPostThreadHiddenV2` responses under `app.bsky.unspecced.defs` namespace and a single interface via `threadItemPost`.\n\n- Updated dependencies [[`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187)]:\n  - @atproto/api@0.15.12\n\n## 0.0.153\n\n### Patch Changes\n\n- Updated dependencies [[`a978681fd`](https://github.com/bluesky-social/atproto/commit/a978681fde1c138a5298bae77e5dc36ce155f955)]:\n  - @atproto/api@0.15.11\n\n## 0.0.152\n\n### Patch Changes\n\n- Updated dependencies [[`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9), [`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9)]:\n  - @atproto/api@0.15.10\n\n## 0.0.151\n\n### Patch Changes\n\n- [#3882](https://github.com/bluesky-social/atproto/pull/3882) [`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159) Thanks [@mozzius](https://github.com/mozzius)! - add a \"via\" field to reposts and likes allowing a reference a repost, and then give a notification when a repost is liked or reposted.\n\n- Updated dependencies [[`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159)]:\n  - @atproto/api@0.15.9\n\n## 0.0.150\n\n### Patch Changes\n\n- [#3869](https://github.com/bluesky-social/atproto/pull/3869) [`80f402f36`](https://github.com/bluesky-social/atproto/commit/80f402f3663af08fd048300738d04c67aa2b9cb8) Thanks [@haileyok](https://github.com/haileyok)! - add `reqId` to feed interactions\n\n- Updated dependencies [[`80f402f36`](https://github.com/bluesky-social/atproto/commit/80f402f3663af08fd048300738d04c67aa2b9cb8), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`36dbd4155`](https://github.com/bluesky-social/atproto/commit/36dbd41551f74052a3f584719a1a7edd86eca201), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4)]:\n  - @atproto/api@0.15.8\n  - @atproto-labs/fetch-node@0.1.9\n\n## 0.0.149\n\n### Patch Changes\n\n- Updated dependencies [[`86b315388`](https://github.com/bluesky-social/atproto/commit/86b3153884099ceeb0cfdb9d2bfdd447c39fb35a)]:\n  - @atproto/api@0.15.7\n\n## 0.0.148\n\n### Patch Changes\n\n- [#3816](https://github.com/bluesky-social/atproto/pull/3816) [`ab4e72084`](https://github.com/bluesky-social/atproto/commit/ab4e72084dd0ea1eb12b45cbb913595434b88675) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Improve handle resolution mechanism\n\n- [#3700](https://github.com/bluesky-social/atproto/pull/3700) [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Consistenlty log errors\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba), [`3a65b68f7`](https://github.com/bluesky-social/atproto/commit/3a65b68f7dc63c8bfbea0ae615f8ae984272f2e4)]:\n  - @atproto/lexicon@0.4.11\n  - @atproto/xrpc-server@0.7.18\n  - @atproto/api@0.15.6\n  - @atproto/common@0.4.11\n  - @atproto/identity@0.4.8\n  - @atproto/repo@0.8.1\n  - @atproto-labs/xrpc-utils@0.0.14\n  - @atproto/sync@0.1.23\n  - @atproto/crypto@0.4.4\n\n## 0.0.147\n\n### Patch Changes\n\n- Updated dependencies [[`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9), [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9)]:\n  - @atproto/api@0.15.5\n  - @atproto/xrpc-server@0.7.17\n  - @atproto-labs/xrpc-utils@0.0.13\n  - @atproto/sync@0.1.22\n\n## 0.0.146\n\n### Patch Changes\n\n- Updated dependencies [[`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c), [`7af77f3ed`](https://github.com/bluesky-social/atproto/commit/7af77f3edfe52f77729f61de4188e8375f03b4ef)]:\n  - @atproto/xrpc-server@0.7.16\n  - @atproto/api@0.15.4\n  - @atproto-labs/xrpc-utils@0.0.12\n  - @atproto/sync@0.1.21\n\n## 0.0.145\n\n### Patch Changes\n\n- [#3772](https://github.com/bluesky-social/atproto/pull/3772) [`9ef52d829`](https://github.com/bluesky-social/atproto/commit/9ef52d82923c9c82a73f39690182bd7f75bbc67a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Hydrate verification views with an empty displayName\n\n## 0.0.144\n\n### Patch Changes\n\n- [#3773](https://github.com/bluesky-social/atproto/pull/3773) [`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add verification notifications\n\n- Updated dependencies [[`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9)]:\n  - @atproto/api@0.15.3\n\n## 0.0.143\n\n### Patch Changes\n\n- Updated dependencies [[`553c988f1`](https://github.com/bluesky-social/atproto/commit/553c988f1d226b3d2fbe94c117b088f5c82db794)]:\n  - @atproto/api@0.15.2\n\n## 0.0.142\n\n### Patch Changes\n\n- Updated dependencies [[`688268b6a`](https://github.com/bluesky-social/atproto/commit/688268b6a5ee30f0922ee152ffbd26583d164ae4), [`8d99915ce`](https://github.com/bluesky-social/atproto/commit/8d99915ce02c73b9b37bf121ccd2703fa14a906a)]:\n  - @atproto/api@0.15.1\n\n## 0.0.141\n\n### Patch Changes\n\n- Updated dependencies [[`23462184d`](https://github.com/bluesky-social/atproto/commit/23462184dc941ba2fc3b4d054985a53715585020)]:\n  - @atproto/api@0.15.0\n\n## 0.0.140\n\n### Patch Changes\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove reference to missing \"bin\" executable\n\n## 0.0.139\n\n### Patch Changes\n\n- Updated dependencies [[`fc61662d7`](https://github.com/bluesky-social/atproto/commit/fc61662d7b88597f78383e37ee54264a8bb4b670), [`ca07871c4`](https://github.com/bluesky-social/atproto/commit/ca07871c487abc99fe7b7f8671aa8d98eb5dc4bb)]:\n  - @atproto/api@0.14.22\n\n## 0.0.138\n\n### Patch Changes\n\n- [#3724](https://github.com/bluesky-social/atproto/pull/3724) [`8b7bf7e8f`](https://github.com/bluesky-social/atproto/commit/8b7bf7e8f0e5447c68633a87a2a3cff99f9e7e1c) Thanks [@haileyok](https://github.com/haileyok)! - Return `ProfileView` from `getSuggestedUsers` unspecced endpoint\n\n- Updated dependencies [[`8b7bf7e8f`](https://github.com/bluesky-social/atproto/commit/8b7bf7e8f0e5447c68633a87a2a3cff99f9e7e1c)]:\n  - @atproto/api@0.14.21\n\n## 0.0.137\n\n### Patch Changes\n\n- [#3713](https://github.com/bluesky-social/atproto/pull/3713) [`0e681d303`](https://github.com/bluesky-social/atproto/commit/0e681d3036fd0b35c6d2198638392051b2ce4c81) Thanks [@haileyok](https://github.com/haileyok)! - add `getSuggestedUsers` and `getSuggestedUsersSkeleton`\n\n- Updated dependencies [[`0e681d303`](https://github.com/bluesky-social/atproto/commit/0e681d3036fd0b35c6d2198638392051b2ce4c81)]:\n  - @atproto/api@0.14.20\n\n## 0.0.136\n\n### Patch Changes\n\n- [#3680](https://github.com/bluesky-social/atproto/pull/3680) [`efb302db1`](https://github.com/bluesky-social/atproto/commit/efb302db1a615b68795c725a22489dbd0400e011) Thanks [@haileyok](https://github.com/haileyok)! - Add unspecced `getSuggestedFeeds` and associated types\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144), [`efb302db1`](https://github.com/bluesky-social/atproto/commit/efb302db1a615b68795c725a22489dbd0400e011), [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/repo@0.8.0\n  - @atproto/api@0.14.19\n  - @atproto/common@0.4.10\n  - @atproto/identity@0.4.7\n  - @atproto/lexicon@0.4.10\n  - @atproto/sync@0.1.20\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.7.15\n  - @atproto-labs/xrpc-utils@0.0.11\n\n## 0.0.135\n\n### Patch Changes\n\n- [#3706](https://github.com/bluesky-social/atproto/pull/3706) [`04b6230cd`](https://github.com/bluesky-social/atproto/commit/04b6230cd2fbfe4a06cb00ab8ccb8e6c87c6c546) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Return `StarterPackView` instead of `StarterPackViewBasic` from `getSuggestedStarterPacks`\n\n- Updated dependencies [[`04b6230cd`](https://github.com/bluesky-social/atproto/commit/04b6230cd2fbfe4a06cb00ab8ccb8e6c87c6c546)]:\n  - @atproto/api@0.14.18\n\n## 0.0.134\n\n### Patch Changes\n\n- [#3673](https://github.com/bluesky-social/atproto/pull/3673) [`2b7efb6cb`](https://github.com/bluesky-social/atproto/commit/2b7efb6cb1c93a108570efdafe9d9ec3f1018dfa) Thanks [@haileyok](https://github.com/haileyok)! - Add `getTrends`, `getTrendsSkeleton`, and associated types\n\n- [#3677](https://github.com/bluesky-social/atproto/pull/3677) [`0eea698be`](https://github.com/bluesky-social/atproto/commit/0eea698bef76520ae4cc0e1f2efbb588a0459556) Thanks [@haileyok](https://github.com/haileyok)! - Add `getSuggestedStarterPacks`, `getSuggestedStarterPacksSkeleton`, and associated types\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`2b7efb6cb`](https://github.com/bluesky-social/atproto/commit/2b7efb6cb1c93a108570efdafe9d9ec3f1018dfa), [`b0a0f1484`](https://github.com/bluesky-social/atproto/commit/b0a0f1484378adeb5e2aa20b9b6ff2c2eca0f740), [`0eea698be`](https://github.com/bluesky-social/atproto/commit/0eea698bef76520ae4cc0e1f2efbb588a0459556), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/api@0.14.17\n  - @atproto/repo@0.7.3\n  - @atproto/common@0.4.9\n  - @atproto/sync@0.1.19\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.7.14\n  - @atproto-labs/xrpc-utils@0.0.10\n\n## 0.0.133\n\n### Patch Changes\n\n- [#3695](https://github.com/bluesky-social/atproto/pull/3695) [`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Fix last reaction lexicon\n\n- Updated dependencies [[`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c)]:\n  - @atproto/api@0.14.16\n\n## 0.0.132\n\n### Patch Changes\n\n- Updated dependencies [[`b4ab5011b`](https://github.com/bluesky-social/atproto/commit/b4ab5011bcc64f9f05122a8773806af8e0c13146)]:\n  - @atproto/api@0.14.15\n\n## 0.0.131\n\n### Patch Changes\n\n- [#3685](https://github.com/bluesky-social/atproto/pull/3685) [`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update chat reaction lexicon\n\n- Updated dependencies [[`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5)]:\n  - @atproto/api@0.14.14\n\n## 0.0.130\n\n### Patch Changes\n\n- Updated dependencies [[`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd)]:\n  - @atproto/api@0.14.13\n\n## 0.0.129\n\n### Patch Changes\n\n- [#3674](https://github.com/bluesky-social/atproto/pull/3674) [`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0) Thanks [@mozzius](https://github.com/mozzius)! - run codegen for changes in chat lexicon\n\n- Updated dependencies [[`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0)]:\n  - @atproto/api@0.14.12\n\n## 0.0.128\n\n### Patch Changes\n\n- Updated dependencies [[`d87ffc7bf`](https://github.com/bluesky-social/atproto/commit/d87ffc7bfe3c1e792dc84a320544eb2e053d61ce)]:\n  - @atproto/api@0.14.11\n\n## 0.0.127\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n  - @atproto/api@0.14.10\n  - @atproto/lexicon@0.4.9\n  - @atproto/sync@0.1.18\n  - @atproto/repo@0.7.2\n  - @atproto/xrpc-server@0.7.13\n  - @atproto-labs/xrpc-utils@0.0.9\n\n## 0.0.126\n\n### Patch Changes\n\n- Updated dependencies [[`b20907a70`](https://github.com/bluesky-social/atproto/commit/b20907a7056970ab627e6c661882cb16491801e2), [`d96b03956`](https://github.com/bluesky-social/atproto/commit/d96b03956d5c26c238f586c6bdf257c080f12746), [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952)]:\n  - @atproto/sync@0.1.17\n  - @atproto/api@0.14.9\n\n## 0.0.125\n\n### Patch Changes\n\n- Updated dependencies [[`38320191e`](https://github.com/bluesky-social/atproto/commit/38320191e559f8b928c6e951a9b4a6207240bfc1), [`6bcbb6d8c`](https://github.com/bluesky-social/atproto/commit/6bcbb6d8cd3696280935ff7892d8e191fd21fa49), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`dc6e4ecb0`](https://github.com/bluesky-social/atproto/commit/dc6e4ecb0e09bbf4bc7a79c6ac43fb6da4166200)]:\n  - @atproto/api@0.14.8\n  - @atproto/syntax@0.3.4\n  - @atproto-labs/fetch-node@0.1.8\n  - @atproto/lexicon@0.4.8\n  - @atproto/sync@0.1.16\n  - @atproto/repo@0.7.1\n  - @atproto/xrpc-server@0.7.12\n  - @atproto-labs/xrpc-utils@0.0.8\n\n## 0.0.124\n\n### Patch Changes\n\n- [#3579](https://github.com/bluesky-social/atproto/pull/3579) [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Include new labeler service record properties on the `labelerViewDetailed` reponse from the app view.\n\n- Updated dependencies [[`99e2809ca`](https://github.com/bluesky-social/atproto/commit/99e2809ca2ebf70acaa10254f140a8dd0fad4305), [`27b0a7be1`](https://github.com/bluesky-social/atproto/commit/27b0a7be1ed1b6e098114791d84ec9dc844db552), [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492)]:\n  - @atproto/api@0.14.7\n\n## 0.0.123\n\n### Patch Changes\n\n- Updated dependencies [[`44f81f2eb`](https://github.com/bluesky-social/atproto/commit/44f81f2eb9229e21aec4472b3a05e855396dbec5)]:\n  - @atproto/api@0.14.6\n\n## 0.0.122\n\n### Patch Changes\n\n- Updated dependencies [[`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f), [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a), [`6e382f67a`](https://github.com/bluesky-social/atproto/commit/6e382f67aa73532efadfea80ff96a27b526cb178)]:\n  - @atproto/repo@0.7.0\n  - @atproto/api@0.14.5\n  - @atproto/sync@0.1.15\n\n## 0.0.121\n\n### Patch Changes\n\n- Updated dependencies [[`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c)]:\n  - @atproto/api@0.14.4\n\n## 0.0.120\n\n### Patch Changes\n\n- Updated dependencies [[`22af31a89`](https://github.com/bluesky-social/atproto/commit/22af31a898476c5e317aea263af366bddda120d6), [`01874c4be`](https://github.com/bluesky-social/atproto/commit/01874c4be73a41ffb8fe28378f674949aa2c938f)]:\n  - @atproto/api@0.14.3\n\n## 0.0.119\n\n### Patch Changes\n\n- Updated dependencies [[`010f10c6f`](https://github.com/bluesky-social/atproto/commit/010f10c6f212f699ad42c0349a58bbcf2172e3cc), [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9)]:\n  - @atproto/api@0.14.2\n\n## 0.0.118\n\n### Patch Changes\n\n- [#3537](https://github.com/bluesky-social/atproto/pull/3537) [`3f58dd0e7`](https://github.com/bluesky-social/atproto/commit/3f58dd0e742cdfed9c3eae0118bc57f539de78f1) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply needs-review to individual records\n\n- Updated dependencies [[`ba5bb6e66`](https://github.com/bluesky-social/atproto/commit/ba5bb6e667fb58bbefd332844957de575e102ca3)]:\n  - @atproto/api@0.14.1\n\n## 0.0.117\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update Lexicon derived code to better reflect data typings. In particular, Lexicon derived interfaces will now explicitly include the `$type` property that can be present in the data.\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/api@0.14.0\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n  - @atproto/sync@0.1.14\n  - @atproto/repo@0.6.5\n  - @atproto/xrpc-server@0.7.11\n  - @atproto-labs/xrpc-utils@0.0.7\n\n## 0.0.116\n\n### Patch Changes\n\n- Updated dependencies [[`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153)]:\n  - @atproto/api@0.13.35\n\n## 0.0.115\n\n### Patch Changes\n\n- [#3496](https://github.com/bluesky-social/atproto/pull/3496) [`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add followerRule threadgate\n\n- Updated dependencies [[`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486), [`636951e47`](https://github.com/bluesky-social/atproto/commit/636951e4728cd52c2e5355eb93b47d7e869b67e9)]:\n  - @atproto/api@0.13.34\n\n## 0.0.114\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- [#3445](https://github.com/bluesky-social/atproto/pull/3445) [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Replace undefined values with empty strings in mock dataplane\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`87ed907a6`](https://github.com/bluesky-social/atproto/commit/87ed907a6b96b408c02c9af819cec8380a453254)]:\n  - @atproto-labs/fetch-node@0.1.7\n  - @atproto/xrpc-server@0.7.10\n  - @atproto/identity@0.4.6\n  - @atproto/lexicon@0.4.6\n  - @atproto/common@0.4.8\n  - @atproto/crypto@0.4.4\n  - @atproto/syntax@0.3.2\n  - @atproto/repo@0.6.4\n  - @atproto/sync@0.1.13\n  - @atproto/api@0.13.33\n  - @atproto/did@0.1.5\n  - @atproto-labs/xrpc-utils@0.0.6\n\n## 0.0.113\n\n### Patch Changes\n\n- Updated dependencies [[`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905), [`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/xrpc-server@0.7.9\n  - @atproto/api@0.13.32\n  - @atproto/did@0.1.4\n  - @atproto/common@0.4.7\n  - @atproto-labs/xrpc-utils@0.0.5\n  - @atproto/sync@0.1.12\n  - @atproto/crypto@0.4.3\n  - @atproto/repo@0.6.3\n\n## 0.0.112\n\n### Patch Changes\n\n- Updated dependencies [[`8c6c7813a`](https://github.com/bluesky-social/atproto/commit/8c6c7813a9c2110c8fe21acdca8f09554a1983ce)]:\n  - @atproto/api@0.13.31\n\n## 0.0.111\n\n### Patch Changes\n\n- [#3390](https://github.com/bluesky-social/atproto/pull/3390) [`c0a75d310`](https://github.com/bluesky-social/atproto/commit/c0a75d310aa92c067799a97d1acc5bd0543114c5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - posts_with_video filter in getAuthorFeed\n\n- Updated dependencies [[`e6e6aea38`](https://github.com/bluesky-social/atproto/commit/e6e6aea3814e3d0bb42a537f80d77947e85fa73f), [`c0a75d310`](https://github.com/bluesky-social/atproto/commit/c0a75d310aa92c067799a97d1acc5bd0543114c5)]:\n  - @atproto/api@0.13.30\n\n## 0.0.110\n\n### Patch Changes\n\n- Updated dependencies [[`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12)]:\n  - @atproto/xrpc-server@0.7.8\n  - @atproto-labs/xrpc-utils@0.0.4\n  - @atproto/sync@0.1.11\n\n## 0.0.109\n\n### Patch Changes\n\n- Updated dependencies [[`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6)]:\n  - @atproto/api@0.13.29\n\n## 0.0.108\n\n### Patch Changes\n\n- [#3389](https://github.com/bluesky-social/atproto/pull/3389) [`cbf17066f`](https://github.com/bluesky-social/atproto/commit/cbf17066f314fbc7f2e943127ee4a9f589f8bec2) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - add feedgen content mode lexicon spec\n\n- Updated dependencies [[`cbf17066f`](https://github.com/bluesky-social/atproto/commit/cbf17066f314fbc7f2e943127ee4a9f589f8bec2), [`9c0128193`](https://github.com/bluesky-social/atproto/commit/9c01281931a371304bcfa465005d7363c003bc5f)]:\n  - @atproto/api@0.13.28\n  - @atproto-labs/fetch-node@0.1.6\n\n## 0.0.107\n\n### Patch Changes\n\n- Updated dependencies [[`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61)]:\n  - @atproto/xrpc-server@0.7.7\n  - @atproto-labs/xrpc-utils@0.0.3\n  - @atproto/sync@0.1.10\n\n## 0.0.106\n\n### Patch Changes\n\n- Updated dependencies [[`e277158f7`](https://github.com/bluesky-social/atproto/commit/e277158f70a831b04fde3ec84b3c1eaa6ce82e9d)]:\n  - @atproto/api@0.13.27\n  - @atproto-labs/fetch-node@0.1.5\n\n## 0.0.105\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n  - @atproto/identity@0.4.5\n  - @atproto/repo@0.6.2\n  - @atproto/xrpc-server@0.7.6\n  - @atproto/sync@0.1.9\n  - @atproto-labs/xrpc-utils@0.0.2\n\n## 0.0.104\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performance when serving blobs\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on Axios\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto-labs/xrpc-utils@0.0.1\n  - @atproto/identity@0.4.4\n  - @atproto/api@0.13.26\n  - @atproto/common@0.4.6\n  - @atproto/lexicon@0.4.5\n  - @atproto/repo@0.6.1\n  - @atproto/sync@0.1.8\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.5\n\n## 0.0.103\n\n### Patch Changes\n\n- Updated dependencies [[`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a)]:\n  - @atproto/api@0.13.25\n\n## 0.0.102\n\n### Patch Changes\n\n- Updated dependencies [[`d90d999de`](https://github.com/bluesky-social/atproto/commit/d90d999defda01a9b04dbce129e254990062c283)]:\n  - @atproto/api@0.13.24\n\n## 0.0.101\n\n### Patch Changes\n\n- Updated dependencies [[`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7), [`9ea2cce9a`](https://github.com/bluesky-social/atproto/commit/9ea2cce9a4c0a08994a8cb5abc81dc4bc2221d0c)]:\n  - @atproto/api@0.13.23\n\n## 0.0.100\n\n### Patch Changes\n\n- Updated dependencies [[`f22383cee`](https://github.com/bluesky-social/atproto/commit/f22383cee8feb8b9f761c801ab6e07ad8dc019ed)]:\n  - @atproto/api@0.13.22\n\n## 0.0.99\n\n### Patch Changes\n\n- Updated dependencies [[`dced566de`](https://github.com/bluesky-social/atproto/commit/dced566de5079ef4208801db476a7e7416f5e5aa)]:\n  - @atproto/api@0.13.21\n\n## 0.0.98\n\n### Patch Changes\n\n- [#3222](https://github.com/bluesky-social/atproto/pull/3222) [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34) Thanks [@gaearon](https://github.com/gaearon)! - Add optional reasons param to listNotifications\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95), [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34), [`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a), [`0bec389a1`](https://github.com/bluesky-social/atproto/commit/0bec389a1c53adbcfab7b877df9b291d44d8ea33)]:\n  - @atproto/common@0.4.5\n  - @atproto/lexicon@0.4.4\n  - @atproto/api@0.13.20\n  - @atproto/repo@0.6.0\n  - @atproto/sync@0.1.7\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.4\n\n## 0.0.97\n\n### Patch Changes\n\n- Updated dependencies [[`ed2236220`](https://github.com/bluesky-social/atproto/commit/ed2236220900ab9a6132c525289cfdd959733a42)]:\n  - @atproto/api@0.13.19\n\n## 0.0.96\n\n### Patch Changes\n\n- [#3082](https://github.com/bluesky-social/atproto/pull/3082) [`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc) Thanks [@gaearon](https://github.com/gaearon)! - Add hotness as a thread sorting option\n\n- Updated dependencies [[`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc)]:\n  - @atproto/api@0.13.18\n\n## 0.0.95\n\n### Patch Changes\n\n- Updated dependencies [[`a4b528e5f`](https://github.com/bluesky-social/atproto/commit/a4b528e5f51c8bfca56b293b0059b88d138ec421), [`2e7aa211d`](https://github.com/bluesky-social/atproto/commit/2e7aa211d2cbc629899c7f87f1713b13b932750b)]:\n  - @atproto/api@0.13.17\n\n## 0.0.94\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`48d08a469`](https://github.com/bluesky-social/atproto/commit/48d08a469f75837e3b7e879d286d12780440b8b8), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`561431fe4`](https://github.com/bluesky-social/atproto/commit/561431fe4897e81767dc768e9a31020d09bf86ff)]:\n  - @atproto/syntax@0.3.1\n  - @atproto/api@0.13.16\n  - @atproto/lexicon@0.4.3\n  - @atproto/sync@0.1.6\n  - @atproto/repo@0.5.5\n  - @atproto/xrpc-server@0.7.3\n\n## 0.0.93\n\n### Patch Changes\n\n- [#2936](https://github.com/bluesky-social/atproto/pull/2936) [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Use custom verifySignatureWithKey with native node:crypto instead of @noble/curves to evaluate performance\n\n- Updated dependencies [[`d6f33b474`](https://github.com/bluesky-social/atproto/commit/d6f33b4742e0b94722a993efc7d18833d9416bb6), [`b6eeb81c6`](https://github.com/bluesky-social/atproto/commit/b6eeb81c6d454b5ae91b05a21fc1820274c1b429), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`839202a3d`](https://github.com/bluesky-social/atproto/commit/839202a3d2b01de25de900cec7540019545798c6), [`e680d55ca`](https://github.com/bluesky-social/atproto/commit/e680d55ca2d7f6b213e2a8693eba6be39163ba41), [`c4b5e5395`](https://github.com/bluesky-social/atproto/commit/c4b5e53957463c37dd16fdd1b897d4ab02ab8e84)]:\n  - @atproto/api@0.13.15\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.2\n  - @atproto/identity@0.4.3\n  - @atproto/repo@0.5.4\n  - @atproto/sync@0.1.5\n\n## 0.0.92\n\n### Patch Changes\n\n- [#2918](https://github.com/bluesky-social/atproto/pull/2918) [`209238769`](https://github.com/bluesky-social/atproto/commit/209238769c0bf38bf04f7fa9621eeb176b5c0ed8) Thanks [@devinivy](https://github.com/devinivy)! - add app.bsky.unspecced.getConfig endpoint\n\n- Updated dependencies [[`209238769`](https://github.com/bluesky-social/atproto/commit/209238769c0bf38bf04f7fa9621eeb176b5c0ed8), [`73f40e63a`](https://github.com/bluesky-social/atproto/commit/73f40e63abe3283efc0a27eef781c00b497caad1)]:\n  - @atproto/api@0.13.14\n\n## 0.0.91\n\n### Patch Changes\n\n- Updated dependencies [[`19e36afb2`](https://github.com/bluesky-social/atproto/commit/19e36afb2c13dbc7b1033eb3cab5e7fc6f496fdc)]:\n  - @atproto/api@0.13.13\n\n## 0.0.90\n\n### Patch Changes\n\n- Updated dependencies [[`d605577c2`](https://github.com/bluesky-social/atproto/commit/d605577c25d3e69c7cc0a1e858a4f009d1ea3096)]:\n  - @atproto/sync@0.1.4\n\n## 0.0.89\n\n### Patch Changes\n\n- Updated dependencies [[`22d039a22`](https://github.com/bluesky-social/atproto/commit/22d039a229e3ef08a793e1c98b473b1b8e18ac5e)]:\n  - @atproto/api@0.13.12\n\n## 0.0.88\n\n### Patch Changes\n\n- [#2853](https://github.com/bluesky-social/atproto/pull/2853) [`72549f442`](https://github.com/bluesky-social/atproto/commit/72549f442223c0c74594e111a9793e39b0c5ea2d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow using a handle as \"actor\" param in app.bsky.graph.getLists\n\n- [#2862](https://github.com/bluesky-social/atproto/pull/2862) [`08ed0a5a9`](https://github.com/bluesky-social/atproto/commit/08ed0a5a916685b2aaea783706e6d6287a2aa287) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing dev-dependency\n\n## 0.0.87\n\n### Patch Changes\n\n- Updated dependencies [[`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44)]:\n  - @atproto/api@0.13.11\n\n## 0.0.86\n\n### Patch Changes\n\n- Updated dependencies [[`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5)]:\n  - @atproto/api@0.13.10\n\n## 0.0.85\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13), [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c)]:\n  - @atproto/common@0.4.4\n  - @atproto/api@0.13.9\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.5.3\n  - @atproto/sync@0.1.3\n  - @atproto/xrpc-server@0.7.1\n\n## 0.0.84\n\n### Patch Changes\n\n- Updated dependencies [[`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/api@0.13.8\n  - @atproto/xrpc-server@0.7.0\n  - @atproto/lexicon@0.4.2\n  - @atproto/common@0.4.3\n  - @atproto/identity@0.4.2\n  - @atproto/repo@0.5.2\n  - @atproto/sync@0.1.2\n  - @atproto/crypto@0.4.1\n\n## 0.0.83\n\n### Patch Changes\n\n- [#2810](https://github.com/bluesky-social/atproto/pull/2810) [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add NUX API\n\n- Updated dependencies [[`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7)]:\n  - @atproto/api@0.13.7\n  - @atproto/common@0.4.2\n  - @atproto/xrpc-server@0.6.4\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.5.1\n  - @atproto/sync@0.1.1\n\n## 0.0.82\n\n### Patch Changes\n\n- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442), [`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442)]:\n  - @atproto/sync@0.1.0\n  - @atproto/repo@0.5.0\n\n## 0.0.81\n\n### Patch Changes\n\n- Updated dependencies [[`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d)]:\n  - @atproto/api@0.13.6\n\n## 0.0.80\n\n### Patch Changes\n\n- [#2751](https://github.com/bluesky-social/atproto/pull/2751) [`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14) Thanks [@devinivy](https://github.com/devinivy)! - Lexicons and support for video embeds within bsky posts.\n\n- Updated dependencies [[`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14)]:\n  - @atproto/api@0.13.5\n\n## 0.0.79\n\n### Patch Changes\n\n- [#2737](https://github.com/bluesky-social/atproto/pull/2737) [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `threadgate: ThreadgateView` to response from `getPostThread`\n\n- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f), [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/xrpc-server@0.6.3\n  - @atproto/api@0.13.4\n  - @atproto/crypto@0.4.1\n  - @atproto/identity@0.4.1\n  - @atproto/repo@0.4.3\n\n## 0.0.78\n\n### Patch Changes\n\n- [#2735](https://github.com/bluesky-social/atproto/pull/2735) [`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7) Thanks [@haileyok](https://github.com/haileyok)! - add `quoteCount` to embed view\n\n- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]:\n  - @atproto/api@0.13.3\n\n## 0.0.77\n\n### Patch Changes\n\n- [#2658](https://github.com/bluesky-social/atproto/pull/2658) [`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb) Thanks [@haileyok](https://github.com/haileyok)! - Adds `app.bsky.feed.getQuotes` lexicon and handlers\n\n- [#2675](https://github.com/bluesky-social/atproto/pull/2675) [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `postgate` records to power quote gating and detached quote posts, plus `hiddenReplies` to the `threadgate` record.\n\n- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]:\n  - @atproto/api@0.13.2\n\n## 0.0.76\n\n### Patch Changes\n\n- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]:\n  - @atproto/xrpc-server@0.6.2\n\n## 0.0.75\n\n### Patch Changes\n\n- Updated dependencies [[`22af354a5`](https://github.com/bluesky-social/atproto/commit/22af354a5db595d7cbc0e65f02601de3565337e1)]:\n  - @atproto/api@0.13.1\n\n## 0.0.74\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/lexicon@0.4.1\n  - @atproto/api@0.13.0\n  - @atproto/repo@0.4.2\n  - @atproto/xrpc-server@0.6.1\n\n## 0.0.73\n\n### Patch Changes\n\n- Updated dependencies [[`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c), [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c)]:\n  - @atproto/api@0.12.29\n  - @atproto/xrpc-server@0.6.0\n\n## 0.0.72\n\n### Patch Changes\n\n- [#2676](https://github.com/bluesky-social/atproto/pull/2676) [`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove `app.bsky.feed.detach` record, to be replaced by `app.bsky.feed.postgate` record in a future release.\n\n- Updated dependencies [[`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41)]:\n  - @atproto/api@0.12.28\n\n## 0.0.71\n\n### Patch Changes\n\n- [#2664](https://github.com/bluesky-social/atproto/pull/2664) [`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `app.bsky.feed.detach` record lexicons.\n\n- Updated dependencies [[`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec)]:\n  - @atproto/api@0.12.27\n\n## 0.0.70\n\n### Patch Changes\n\n- [#2276](https://github.com/bluesky-social/atproto/pull/2276) [`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Updates muted words lexicons to include new attributes `id`, `actorTarget`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words.\n\n- Updated dependencies [[`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7)]:\n  - @atproto/api@0.12.26\n\n## 0.0.69\n\n### Patch Changes\n\n- [#2648](https://github.com/bluesky-social/atproto/pull/2648) [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b) Thanks [@dholms](https://github.com/dholms)! - Support for priority notifications\n\n- Updated dependencies [[`12dcdb668`](https://github.com/bluesky-social/atproto/commit/12dcdb668c8ec0f8a89689c326ab3e9dbc6d2f3c), [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b)]:\n  - @atproto/api@0.12.25\n\n## 0.0.68\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Obfuscate request headers in logs using utils from @atproto/common\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/common@0.4.1\n  - @atproto/crypto@0.4.0\n  - @atproto/repo@0.4.1\n  - @atproto/xrpc-server@0.5.3\n\n## 0.0.67\n\n### Patch Changes\n\n- Updated dependencies [[`ed5810179`](https://github.com/bluesky-social/atproto/commit/ed5810179006f254f2035fe1f0e3c4798080cfe0), [`0529bec99`](https://github.com/bluesky-social/atproto/commit/0529bec99183439829a3553f45ac7203763144c3)]:\n  - @atproto/api@0.12.24\n\n## 0.0.66\n\n### Patch Changes\n\n- [#2492](https://github.com/bluesky-social/atproto/pull/2492) [`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863) Thanks [@pfrazee](https://github.com/pfrazee)! - Added bsky app state preference and improved protections against race conditions in preferences sdk\n\n- Updated dependencies [[`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863)]:\n  - @atproto/api@0.12.23\n\n## 0.0.65\n\n### Patch Changes\n\n- [#2553](https://github.com/bluesky-social/atproto/pull/2553) [`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02) Thanks [@devinivy](https://github.com/devinivy)! - Support for starter packs (app.bsky.graph.starterpack)\n\n- Updated dependencies [[`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02), [`615a96ddc`](https://github.com/bluesky-social/atproto/commit/615a96ddc2965251cfab060dfc43fc1a51ef4bff)]:\n  - @atproto/api@0.12.22\n  - @atproto/xrpc-server@0.5.2\n\n## 0.0.64\n\n### Patch Changes\n\n- Updated dependencies [[`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24)]:\n  - @atproto/api@0.12.21\n\n## 0.0.63\n\n### Patch Changes\n\n- Updated dependencies [[`ea0f10b5d`](https://github.com/bluesky-social/atproto/commit/ea0f10b5d0d334eb587032c54d5ace9ea811cf26)]:\n  - @atproto/api@0.12.20\n\n## 0.0.62\n\n### Patch Changes\n\n- Updated dependencies [[`7c1973841`](https://github.com/bluesky-social/atproto/commit/7c1973841dab416ae19435d37853aeea1f579d39)]:\n  - @atproto/api@0.12.19\n\n## 0.0.61\n\n### Patch Changes\n\n- Updated dependencies [[`58abcbd8b`](https://github.com/bluesky-social/atproto/commit/58abcbd8b6e42a1f66bda6acc3ee6a2c0894e546)]:\n  - @atproto/api@0.12.18\n\n## 0.0.60\n\n### Patch Changes\n\n- [#2426](https://github.com/bluesky-social/atproto/pull/2426) [`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.admin.searchAccounts lexicon to allow searching for accounts using email address\n\n- Updated dependencies [[`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd)]:\n  - @atproto/api@0.12.17\n\n## 0.0.59\n\n### Patch Changes\n\n- Updated dependencies [[`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46)]:\n  - @atproto/api@0.12.16\n\n## 0.0.58\n\n### Patch Changes\n\n- Updated dependencies [[`255d5ea1f`](https://github.com/bluesky-social/atproto/commit/255d5ea1f06726547cdbe59c83bd18f2d4746912)]:\n  - @atproto/api@0.12.15\n\n## 0.0.57\n\n### Patch Changes\n\n- Updated dependencies [[`c4af6a409`](https://github.com/bluesky-social/atproto/commit/c4af6a409ea2171c3cf1d0e7c8ed496794a3f049)]:\n  - @atproto/api@0.12.14\n\n## 0.0.56\n\n### Patch Changes\n\n- [#2522](https://github.com/bluesky-social/atproto/pull/2522) [`53551be6c`](https://github.com/bluesky-social/atproto/commit/53551be6cf092a9b4d2e132788b94ac0d4ffcecc) Thanks [@devinivy](https://github.com/devinivy)! - Set max-age CORS header to max practical value\n\n## 0.0.55\n\n### Patch Changes\n\n- Updated dependencies [[`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9)]:\n  - @atproto/api@0.12.13\n\n## 0.0.54\n\n### Patch Changes\n\n- [#2442](https://github.com/bluesky-social/atproto/pull/2442) [`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.label.queryLabels endpoint on appview and allow viewing external labels through ozone\n\n- Updated dependencies [[`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4)]:\n  - @atproto/api@0.12.12\n\n## 0.0.53\n\n### Patch Changes\n\n- Updated dependencies [[`06d2328ee`](https://github.com/bluesky-social/atproto/commit/06d2328eeb8d706018dbdf7cc7b9862dd65b96cb)]:\n  - @atproto/api@0.12.11\n\n## 0.0.52\n\n### Patch Changes\n\n- Updated dependencies [[`d32f7215f`](https://github.com/bluesky-social/atproto/commit/d32f7215f69bc87f50890d9cfdb09840c2fbaa41)]:\n  - @atproto/api@0.12.10\n\n## 0.0.51\n\n### Patch Changes\n\n- Updated dependencies [[`f83b4c8ca`](https://github.com/bluesky-social/atproto/commit/f83b4c8cad01cebc1b67caa6c7ebe45f07b2f318)]:\n  - @atproto/api@0.12.9\n\n## 0.0.50\n\n### Patch Changes\n\n- [`58f719cc1`](https://github.com/bluesky-social/atproto/commit/58f719cc1c8d0ebd5ad7cf11221372b671cd7857) Thanks [@devinivy](https://github.com/devinivy)! - Add grandparent author to feed item reply ref\n\n- Updated dependencies [[`58f719cc1`](https://github.com/bluesky-social/atproto/commit/58f719cc1c8d0ebd5ad7cf11221372b671cd7857)]:\n  - @atproto/api@0.12.8\n\n## 0.0.49\n\n### Patch Changes\n\n- Updated dependencies [[`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933)]:\n  - @atproto/api@0.12.7\n\n## 0.0.48\n\n### Patch Changes\n\n- Updated dependencies [[`b9b7c5821`](https://github.com/bluesky-social/atproto/commit/b9b7c582199d57d2fe0af8af5c8c411ed34f5b9d)]:\n  - @atproto/api@0.12.6\n\n## 0.0.47\n\n### Patch Changes\n\n- Updated dependencies [[`3424a1770`](https://github.com/bluesky-social/atproto/commit/3424a17703891f5678ec76ef97e696afb3288b22)]:\n  - @atproto/api@0.12.5\n\n## 0.0.46\n\n### Patch Changes\n\n- Updated dependencies [[`93a4a4df9`](https://github.com/bluesky-social/atproto/commit/93a4a4df9ce38f89a5d05e300d247b85fb007e05)]:\n  - @atproto/api@0.12.4\n\n## 0.0.45\n\n### Patch Changes\n\n- Updated dependencies [[`0edef0ec0`](https://github.com/bluesky-social/atproto/commit/0edef0ec01403fd6097a4d2875b68313f2f1261f), [`c6d758b8b`](https://github.com/bluesky-social/atproto/commit/c6d758b8b63f4ef50b2ab9afc62164e92a53e7f0)]:\n  - @atproto/api@0.12.3\n\n## 0.0.44\n\n### Patch Changes\n\n- Updated dependencies [[`cd4fcc709`](https://github.com/bluesky-social/atproto/commit/cd4fcc709fe8d725a4af769ce21f53711fe5622a)]:\n  - @atproto/xrpc-server@0.5.1\n\n## 0.0.43\n\n### Patch Changes\n\n- Updated dependencies [[`abc6f82da`](https://github.com/bluesky-social/atproto/commit/abc6f82da38abef2b1bbe8d9e41a0534a5418c9e)]:\n  - @atproto/api@0.12.2\n\n## 0.0.42\n\n### Patch Changes\n\n- [#2342](https://github.com/bluesky-social/atproto/pull/2342) [`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds the `associated` property to `profile` and `profile-basic` views, bringing them in line with `profile-detailed` views.\n\n- Updated dependencies [[`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5)]:\n  - @atproto/api@0.12.1\n\n## 0.0.41\n\n### Patch Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9), [`36f2e966c`](https://github.com/bluesky-social/atproto/commit/36f2e966cba6cc90ba4320520da5c7381cfb8086)]:\n  - @atproto/xrpc-server@0.5.0\n  - @atproto/identity@0.4.0\n  - @atproto/lexicon@0.4.0\n  - @atproto/common@0.4.0\n  - @atproto/crypto@0.4.0\n  - @atproto/syntax@0.3.0\n  - @atproto/repo@0.4.0\n  - @atproto/api@0.12.0\n\n## 0.0.40\n\n### Patch Changes\n\n- Updated dependencies [[`7dd9941b7`](https://github.com/bluesky-social/atproto/commit/7dd9941b73dbbd82601740e021cc87d765af60ca)]:\n  - @atproto/api@0.11.2\n\n## 0.0.39\n\n### Patch Changes\n\n- Updated dependencies [[`219480764`](https://github.com/bluesky-social/atproto/commit/2194807644cbdb0021e867437693300c1b0e55f5)]:\n  - @atproto/api@0.11.1\n\n## 0.0.38\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0), [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/identity@0.3.3\n  - @atproto/api@0.11.0\n  - @atproto/common@0.3.4\n  - @atproto/lexicon@0.3.3\n  - @atproto/repo@0.3.9\n  - @atproto/syntax@0.2.1\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.4\n\n## 0.0.37\n\n### Patch Changes\n\n- [#2279](https://github.com/bluesky-social/atproto/pull/2279) [`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655) Thanks [@gaearon](https://github.com/gaearon)! - Change Following feed prefs to only show replies from people you follow by default\n\n- Updated dependencies [[`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655)]:\n  - @atproto/api@0.10.5\n\n## 0.0.36\n\n### Patch Changes\n\n- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]:\n  - @atproto/api@0.10.4\n\n## 0.0.35\n\n### Patch Changes\n\n- Updated dependencies [[`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed)]:\n  - @atproto/api@0.10.3\n\n## 0.0.34\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410), [`43531905c`](https://github.com/bluesky-social/atproto/commit/43531905ce1aec6d36d9be5943782811ecca6e6d), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/api@0.10.2\n  - @atproto/lexicon@0.3.2\n  - @atproto/repo@0.3.8\n  - @atproto/xrpc-server@0.4.3\n\n## 0.0.33\n\n### Patch Changes\n\n- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]:\n  - @atproto/api@0.10.1\n\n## 0.0.32\n\n### Patch Changes\n\n- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]:\n  - @atproto/api@0.10.0\n\n## 0.0.31\n\n### Patch Changes\n\n- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]:\n  - @atproto/api@0.9.8\n\n## 0.0.30\n\n### Patch Changes\n\n- Updated dependencies [[`fcf8e3faf`](https://github.com/bluesky-social/atproto/commit/fcf8e3faf311559162c3aa0d9af36f84951914bc), [`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]:\n  - @atproto/repo@0.3.7\n  - @atproto/api@0.9.7\n\n## 0.0.29\n\n### Patch Changes\n\n- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]:\n  - @atproto/api@0.9.6\n\n## 0.0.28\n\n### Patch Changes\n\n- Updated dependencies [[`8994d363`](https://github.com/bluesky-social/atproto/commit/8994d3633adad1c02569d6d44ae896e18195e8e2)]:\n  - @atproto/api@0.9.5\n\n## 0.0.27\n\n### Patch Changes\n\n- Updated dependencies [[`4171c04a`](https://github.com/bluesky-social/atproto/commit/4171c04ad81c5734a4558bc41fa1c4f3a1aba18c)]:\n  - @atproto/api@0.9.4\n\n## 0.0.26\n\n### Patch Changes\n\n- Updated dependencies [[`5368245a`](https://github.com/bluesky-social/atproto/commit/5368245a6ef7095c86ad166fb04ff9bef27c3c3e)]:\n  - @atproto/api@0.9.3\n\n## 0.0.25\n\n### Patch Changes\n\n- Updated dependencies [[`15f38560`](https://github.com/bluesky-social/atproto/commit/15f38560b9e2dc3af8cf860826e7477234fe6a2d)]:\n  - @atproto/api@0.9.2\n\n## 0.0.24\n\n### Patch Changes\n\n- Updated dependencies [[`c6fc73ae`](https://github.com/bluesky-social/atproto/commit/c6fc73aee6c245d12f876abd11889b8dbd0ce2ed)]:\n  - @atproto/api@0.9.1\n\n## 0.0.23\n\n### Patch Changes\n\n- [#1988](https://github.com/bluesky-social/atproto/pull/1988) [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6) Thanks [@bnewbold](https://github.com/bnewbold)! - remove deprecated app.bsky.unspecced.getPopular endpoint\n\n- Updated dependencies [[`e43396af`](https://github.com/bluesky-social/atproto/commit/e43396af0973748dd2d034e88d35cf7ae8b4df2c), [`bf8d718c`](https://github.com/bluesky-social/atproto/commit/bf8d718cf918ac8d8a2cb1f57fde80535284642d), [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6)]:\n  - @atproto/api@0.9.0\n\n## 0.0.22\n\n### Patch Changes\n\n- Updated dependencies [[`14067733`](https://github.com/bluesky-social/atproto/commit/140677335f76b99129c1f593d9e11d64624386c6)]:\n  - @atproto/api@0.8.0\n\n## 0.0.21\n\n### Patch Changes\n\n- Updated dependencies [[`8f3f43cb`](https://github.com/bluesky-social/atproto/commit/8f3f43cb40f79ff7c52f81290daec55cfb000093)]:\n  - @atproto/api@0.7.4\n\n## 0.0.20\n\n### Patch Changes\n\n- Updated dependencies [[`7dec9df3`](https://github.com/bluesky-social/atproto/commit/7dec9df3b583ee8c06c0c6a7e32c259820dc84a5)]:\n  - @atproto/api@0.7.3\n\n## 0.0.19\n\n### Patch Changes\n\n- [#1776](https://github.com/bluesky-social/atproto/pull/1776) [`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Integrate `posts_and_author_threads` filter into `getAuthorFeed` implementation.\n\n- [#1776](https://github.com/bluesky-social/atproto/pull/1776) [`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `posts_and_author_threads` filter to `getAuthorFeed`\n\n- Updated dependencies [[`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a)]:\n  - @atproto/api@0.7.2\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies [[`60deea17`](https://github.com/bluesky-social/atproto/commit/60deea17622f7c574c18432a55ced4e1cdc1b3a1)]:\n  - @atproto/api@0.7.1\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`45352f9b`](https://github.com/bluesky-social/atproto/commit/45352f9b6d02aa405be94e9102424d983912ca5d)]:\n  - @atproto/api@0.7.0\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60), [`378fc613`](https://github.com/bluesky-social/atproto/commit/378fc6132f621ca517897c9467ed5bba134b3776)]:\n  - @atproto/syntax@0.1.5\n  - @atproto/api@0.6.24\n  - @atproto/lexicon@0.3.1\n  - @atproto/repo@0.3.6\n  - @atproto/xrpc-server@0.4.2\n\n## 0.0.15\n\n### Patch Changes\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.1\n  - @atproto/identity@0.3.2\n  - @atproto/repo@0.3.5\n  - @atproto/api@0.6.23\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`772736a0`](https://github.com/bluesky-social/atproto/commit/772736a01081f39504e1b19a1b3687783bb78f07)]:\n  - @atproto/api@0.6.23\n\n## 0.0.13\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/xrpc-server@0.4.0\n  - @atproto/identity@0.3.1\n  - @atproto/common@0.3.3\n  - @atproto/crypto@0.2.3\n  - @atproto/syntax@0.1.4\n  - @atproto/repo@0.3.4\n  - @atproto/api@0.6.22\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`9c98a5ba`](https://github.com/bluesky-social/atproto/commit/9c98a5baaf503b02238a6afe4f6e2b79c5181693), [`bb039d8e`](https://github.com/bluesky-social/atproto/commit/bb039d8e4ce5b7f70c4f3e86d1327e210ef24dc3), [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/api@0.6.21\n  - @atproto/identity@0.3.0\n  - @atproto/repo@0.3.3\n  - @atproto/common@0.3.2\n  - @atproto/lexicon@0.2.3\n  - @atproto/syntax@0.1.3\n  - @atproto/xrpc-server@0.3.3\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/api@0.6.20\n  - @atproto/common@0.3.1\n  - @atproto/identity@0.2.1\n  - @atproto/lexicon@0.2.2\n  - @atproto/repo@0.3.2\n  - @atproto/syntax@0.1.2\n  - @atproto/xrpc-server@0.3.2\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]:\n  - @atproto/api@0.6.19\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf)]:\n  - @atproto/api@0.6.18\n\n## 0.0.8\n\n### Patch Changes\n\n- [#1637](https://github.com/bluesky-social/atproto/pull/1637) [`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduce general support for tags on posts\n\n- Updated dependencies [[`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd)]:\n  - @atproto/api@0.6.17\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]:\n  - @atproto/api@0.6.16\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f)]:\n  - @atproto/api@0.6.15\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n  - @atproto/api@0.6.14\n  - @atproto/lexicon@0.2.1\n  - @atproto/repo@0.3.1\n  - @atproto/xrpc-server@0.3.1\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]:\n  - @atproto/api@0.6.13\n"
  },
  {
    "path": "packages/bsky/README.md",
    "content": "# @atproto/bsky: Bluesky AppView Service\n\nTypeScript implementation of the `app.bsky` Lexicons backing the https://bsky.app microblogging application.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/bsky)](https://www.npmjs.com/package/@atproto/bsky)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/bsky/bin/migration-create.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\n\nexport async function main() {\n  const now = new Date()\n  const prefix = now.toISOString().replace(/[^a-z0-9]/gi, '') // Order of migrations matches alphabetical order of their names\n  const name = process.argv[2]\n  if (!name || !name.match(/^[a-z0-9-]+$/)) {\n    process.exitCode = 1\n    return console.error(\n      'Must pass a migration name consisting of lowercase digits, numbers, and dashes.',\n    )\n  }\n  const filename = `${prefix}-${name}`\n  const dir = path.join(\n    __dirname,\n    '..',\n    'src',\n    'data-plane',\n    'server',\n    'db',\n    'migrations',\n  )\n\n  await fs.writeFile(path.join(dir, `${filename}.ts`), template, { flag: 'wx' })\n  await fs.writeFile(\n    path.join(dir, 'index.ts'),\n    `export * as _${prefix} from './${filename}'\\n`,\n    { flag: 'a' },\n  )\n}\n\nconst template = `import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Migration code\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  // Migration code\n}\n`\n\nmain()\n"
  },
  {
    "path": "packages/bsky/buf.gen.yaml",
    "content": "version: v1\nplugins:\n  - plugin: es\n    opt:\n      - target=ts\n      - import_extension=\n    out: src/proto\n  - plugin: connect-es\n    opt:\n      - target=ts\n      - import_extension=\n    out: src/proto\n"
  },
  {
    "path": "packages/bsky/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Bsky App View',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  transformIgnorePatterns: ['/node_modules/.pnpm/(?!(get-port)@)'],\n  testTimeout: 60000,\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/bsky/package.json",
    "content": "{\n  \"name\": \"@atproto/bsky\",\n  \"version\": \"0.0.221\",\n  \"license\": \"MIT\",\n  \"description\": \"Reference implementation of app.bsky App View (Bluesky API)\",\n  \"keywords\": [\n    \"atproto\",\n    \"bluesky\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/bsky\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"codegen\": \"lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/com/germnetwork/*\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"start\": \"node --enable-source-maps dist/bin.js\",\n    \"test\": \"../dev-infra/with-test-redis-and-db.sh jest\",\n    \"test:log\": \"tail -50 test.log | pino-pretty\",\n    \"test:updateSnapshot\": \"../dev-infra/with-test-redis-and-db.sh jest --updateSnapshot\",\n    \"migration:create\": \"ts-node ./bin/migration-create.ts\",\n    \"buf:gen\": \"buf generate ../bsync/proto && buf generate ./proto\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch-node\": \"workspace:^\",\n    \"@atproto-labs/xrpc-utils\": \"workspace:^\",\n    \"@atproto/api\": \"workspace:^\",\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/identity\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/repo\": \"workspace:^\",\n    \"@atproto/sync\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/xrpc-server\": \"workspace:^\",\n    \"@bufbuild/protobuf\": \"^1.5.0\",\n    \"@connectrpc/connect\": \"^1.1.4\",\n    \"@connectrpc/connect-express\": \"^1.1.4\",\n    \"@connectrpc/connect-node\": \"^1.1.4\",\n    \"@did-plc/lib\": \"^0.0.1\",\n    \"@growthbook/growthbook\": \"^1.6.2\",\n    \"@hapi/address\": \"^5.1.1\",\n    \"@types/http-errors\": \"^2.0.1\",\n    \"compression\": \"^1.7.4\",\n    \"cors\": \"^2.8.5\",\n    \"disposable-email-domains-js\": \"^1.5.0\",\n    \"etcd3\": \"^1.1.2\",\n    \"express\": \"^4.17.2\",\n    \"http-errors\": \"^2.0.0\",\n    \"http-terminator\": \"^3.2.0\",\n    \"ioredis\": \"^5.3.2\",\n    \"jose\": \"^5.0.1\",\n    \"key-encoder\": \"^2.0.3\",\n    \"kysely\": \"^0.22.0\",\n    \"leo-profanity\": \"^1.8.0\",\n    \"multiformats\": \"^9.9.0\",\n    \"murmurhash\": \"^2.0.1\",\n    \"p-queue\": \"^6.6.2\",\n    \"pg\": \"^8.10.0\",\n    \"pino\": \"^8.21.0\",\n    \"pino-http\": \"^8.2.1\",\n    \"sharp\": \"^0.33.5\",\n    \"structured-headers\": \"^1.0.1\",\n    \"typed-emitter\": \"^2.1.0\",\n    \"uint8arrays\": \"3.0.0\",\n    \"undici\": \"^6.19.8\",\n    \"zod\": \"3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@atproto/api\": \"workspace:^\",\n    \"@atproto/lex-cli\": \"workspace:^\",\n    \"@atproto/pds\": \"workspace:^\",\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"@bufbuild/buf\": \"^1.28.1\",\n    \"@bufbuild/protoc-gen-es\": \"^1.5.0\",\n    \"@connectrpc/protoc-gen-connect-es\": \"^1.1.4\",\n    \"@did-plc/server\": \"^0.0.1\",\n    \"@jest/globals\": \"28\",\n    \"@types/cors\": \"^2.8.12\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-serve-static-core\": \"^4.17.36\",\n    \"@types/pg\": \"^8.6.6\",\n    \"@types/qs\": \"^6.9.7\",\n    \"jest\": \"^28.1.2\",\n    \"ts-node\": \"^10.8.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/bsky/proto/bsky.proto",
    "content": "syntax = \"proto3\";\n\npackage bsky;\noption go_package = \"./;bsky\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n//\n// Read Path\n//\n\nmessage Record {\n  bytes record = 1;\n  string cid = 2;\n  google.protobuf.Timestamp indexed_at = 4;\n  bool taken_down = 5;\n  google.protobuf.Timestamp created_at = 6;\n  google.protobuf.Timestamp sorted_at = 7;\n  string takedown_ref = 8;\n  repeated string tags = 9;\n}\n\nmessage GetBlockRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetBlockRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetFeedGeneratorRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetFeedGeneratorRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetFollowRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetFollowRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetLikeRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetLikeRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetListBlockRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetListBlockRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetListItemRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetListItemRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetListRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetListRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage PostRecordMeta {\n  bool violates_thread_gate = 1;\n  bool has_media = 2;\n  bool is_reply = 3;\n  bool violates_embedding_rules = 4;\n  bool has_post_gate = 5;\n  bool has_thread_gate = 6;\n  bool has_video = 7;\n}\n\nmessage GetPostRecordsRequest {\n  repeated string uris = 1;\n  optional string process_dynamic_tags_for_view = 2;\n  optional string viewer_did = 3;\n}\n\nmessage GetPostRecordsResponse {\n  repeated Record records = 1;\n  repeated PostRecordMeta meta = 2;\n}\n\nmessage GetProfileRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetProfileRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetActorChatDeclarationRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetActorChatDeclarationRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetNotificationDeclarationRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetNotificationDeclarationRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetGermDeclarationRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetGermDeclarationRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetStatusRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetStatusRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetRepostRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetRepostRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetThreadGateRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetThreadGateRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetPostgateRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetPostgateRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetLabelerRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetLabelerRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage GetAllLabelersRequest {}\n\nmessage GetAllLabelersResponse {\n  repeated string uris = 1;\n  repeated Record records = 2;\n}\n\nmessage GetStarterPackRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetStarterPackRecordsResponse {\n  repeated Record records = 1;\n}\n\n//\n// Follows\n//\n\n// - Return follow uris where user A follows users B, C, D, …\n//     - E.g. for viewer state on `getProfiles`\nmessage GetActorFollowsActorsRequest {\n  string actor_did = 1;\n  repeated string target_dids = 2;\n}\n\nmessage GetActorFollowsActorsResponse {\n  repeated string uris = 1;\n}\n\n// - Return follow uris of users who follows user A\n//     - For `getFollowers` list\nmessage GetFollowersRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage FollowInfo {\n  string uri = 1;\n  string actor_did = 2;\n  string subject_did = 3;\n}\n\nmessage GetFollowersResponse {\n  repeated FollowInfo followers = 1;\n  string cursor = 2;\n}\n\n// - Return follow uris of users A follows\n//     - For `getFollows` list\nmessage GetFollowsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetFollowsResponse {\n  repeated FollowInfo follows = 1;\n  string cursor = 2;\n}\n\n//\n// Verification\n//\n\nmessage VerificationMeta {\n  string rkey = 1;\n  string handle = 2;\n  string display_name = 3;\n  google.protobuf.Timestamp sorted_at = 4;\n}\n\nmessage GetVerificationRecordsRequest {\n  repeated string uris = 1;\n}\n\nmessage GetVerificationRecordsResponse {\n  repeated Record records = 1;\n}\n\nmessage VerificationIssued {\n  string actor_did = 1;\n  string rkey = 2;\n  string subject_did = 3;\n  google.protobuf.Timestamp created_at = 7;\n  google.protobuf.Timestamp indexed_at = 8;\n  google.protobuf.Timestamp sorted_at = 9;\n}\n\nmessage GetVerificationsIssuedRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetVerificationsIssuedResponse {\n  repeated VerificationIssued verifications = 1;\n  string cursor = 2;\n}\n\nmessage VerificationReceived {\n  string actor_did = 1;\n  string rkey = 2;\n  string subject_did = 3;\n  google.protobuf.Timestamp created_at = 7;\n  google.protobuf.Timestamp indexed_at = 8;\n  google.protobuf.Timestamp sorted_at = 9;\n}\n\nmessage GetVerificationsReceivedRequest {\n  string subject_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetVerificationsReceivedResponse {\n  repeated VerificationReceived verifications = 1;\n  string cursor = 2;\n}\n\n//\n// Likes\n//\n\n// - return like uris where subject uri is subject A\n//     - `getLikes` list for a post\nmessage GetLikesBySubjectRequest {\n  RecordRef subject = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetLikesBySubjectResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\nmessage GetLikesBySubjectSortedRequest {\n  RecordRef subject = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetLikesBySubjectSortedResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\nmessage GetQuotesBySubjectSortedRequest {\n  RecordRef subject = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetQuotesBySubjectSortedResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n// - return like uris for user A on subject B, C, D...\n//     - viewer state on posts\nmessage GetLikesByActorAndSubjectsRequest {\n  string actor_did = 1;\n  repeated RecordRef refs = 2;\n}\n\nmessage GetLikesByActorAndSubjectsResponse {\n  repeated string uris = 1;\n}\n\n// - return recent like uris for user A\n//     - `getActorLikes` list for a user\nmessage GetActorLikesRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage LikeInfo {\n  string uri = 1;\n  string subject = 2;\n}\n\nmessage GetActorLikesResponse {\n  repeated LikeInfo likes = 1;\n  string cursor = 2;\n}\n\n//\n// Interactions\n//\nmessage GetInteractionCountsRequest {\n  repeated RecordRef refs = 1;\n  repeated string skip_cache_for_dids = 2;\n}\n\nmessage GetInteractionCountsResponse {\n  repeated int32 likes = 1;\n  repeated int32 reposts = 2;\n  repeated int32 replies = 3;\n  repeated int32 quotes = 4;\n  repeated int32 bookmarks = 5;\n}\n\nmessage GetCountsForUsersRequest {\n  repeated string dids = 1;\n}\n\nmessage GetCountsForUsersResponse {\n  repeated int32 posts = 1;\n  repeated int32 reposts = 2;\n  repeated int32 following = 3;\n  repeated int32 followers = 4;\n  repeated int32 lists = 5;\n  repeated int32 feeds = 6;\n  repeated int32 starter_packs = 7;\n  repeated int32 drafts = 8;\n}\n\nmessage GetStarterPackCountsRequest {\n  repeated RecordRef refs = 1;\n}\n\nmessage GetStarterPackCountsResponse {\n  repeated int32 joined_week = 1;\n  repeated int32 joined_all_time = 2;\n}\n\nmessage GetListCountsRequest {\n  repeated RecordRef refs = 1;\n}\n\nmessage GetListCountsResponse {\n  repeated int32 list_items = 1;\n}\n\nmessage GetNewUserCountForRangeRequest {\n  google.protobuf.Timestamp start = 1;\n  google.protobuf.Timestamp end = 2;\n}\n\nmessage GetNewUserCountForRangeResponse {\n  int32 count = 1;\n}\n\n//\n// Reposts\n//\n\n// - return repost uris where subject uri is subject A\n//     - `getReposts` list for a post\nmessage GetRepostsBySubjectRequest {\n  RecordRef subject = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetRepostsBySubjectResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n// - return repost uris for user A on subject B, C, D...\n//     - viewer state on posts\nmessage GetRepostsByActorAndSubjectsRequest {\n  string actor_did = 1;\n  repeated RecordRef refs = 2;\n}\n\nmessage RecordRef {\n  string uri = 1;\n  string cid = 2;\n}\n\nmessage GetRepostsByActorAndSubjectsResponse {\n  repeated string uris = 1;\n}\n\n// - return recent repost uris for user A\n//     - `getActorReposts` list for a user\nmessage GetActorRepostsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetActorRepostsResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n//\n// Profile\n//\n\n// - return actor information for dids A, B, C…\n//     - profile hydration\n//     - should this include handles?  apply repo takedown?\nmessage GetActorsRequest {\n  repeated string dids = 1;\n  repeated string skip_cache_for_dids = 2;\n  repeated string return_age_assurance_for_dids = 3;\n}\n\nmessage ActorInfo {\n  bool exists = 1;\n  string handle = 2;\n  Record profile = 3;\n  bool taken_down = 4;\n  string takedown_ref = 5;\n  google.protobuf.Timestamp tombstoned_at = 6;\n  bool labeler = 7;\n  string allow_incoming_chats_from = 8;\n  string upstream_status = 9;\n  google.protobuf.Timestamp created_at = 10;\n  bool priority_notifications = 11;\n  double pagerank = 12;\n  bool trusted_verifier = 13;\n  map<string, VerificationMeta> verified_by = 14;\n  // Tags being applied to the account itself\n  repeated string tags = 15;\n  // Tags being applied to the profile record\n  repeated string profile_tags = 16;\n  Record status_record = 17;\n  string allow_activity_subscriptions_from = 18;\n  optional AgeAssuranceStatus age_assurance_status = 19;\n  reserved 20; // DO NOT REMOVE - see dataplane repo\n  Record germ_record = 21;\n}\n\nmessage AgeAssuranceStatus {\n  string status = 1;\n  google.protobuf.Timestamp last_initiated_at = 2;\n  bool override_applied = 3;\n  string access = 4;\n}\n\nmessage GetActorsResponse {\n  repeated ActorInfo actors = 1;\n}\n\n// - return did for handle A\n//     - `resolveHandle`\n//     - answering queries where the query param is a handle\nmessage GetDidsByHandlesRequest {\n  repeated string handles = 1;\n  bool lookup_unidirectional = 2;\n}\n\nmessage GetDidsByHandlesResponse {\n  repeated string dids = 1;\n}\n\n//\n// Relationships\n//\n\n// - return relationships between user A and users B, C, D...\n//     - profile hydration\n//     - block application\nmessage GetRelationshipsRequest {\n  string actor_did = 1;\n  repeated string target_dids = 2;\n}\n\nmessage Relationships {\n  bool muted = 1;\n  string muted_by_list = 2;\n  string blocked_by = 3;\n  string blocking = 4;\n  string blocked_by_list = 5;\n  string blocking_by_list = 6;\n  string following = 7;\n  string followed_by = 8;\n}\n\nmessage GetRelationshipsResponse {\n  repeated Relationships relationships = 1;\n}\n\n// - return whether a block (bidrectionally and either direct or through a list) exists between two dids\n//     - enforcing 3rd party block violations\nmessage RelationshipPair {\n  string a = 1;\n  string b = 2;\n}\n\nmessage BlockExistence {\n  string blocked_by = 1;\n  string blocking = 2;\n  string blocked_by_list = 3;\n  string blocking_by_list = 4;\n}\n\nmessage GetBlockExistenceRequest {\n  repeated RelationshipPair pairs = 1;\n}\n\nmessage GetBlockExistenceResponse {\n  repeated bool exists = 1;\n  repeated BlockExistence blocks = 2;\n}\n\n\n//\n// Lists\n//\n\nmessage ListItemInfo {\n  string uri = 1;\n  string did = 2;\n}\n\n// - Return dids of users in list A\n//     - E.g. to view items in one of your mute lists\nmessage GetListMembersRequest {\n  string list_uri = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetListMembersResponse {\n  repeated ListItemInfo listitems = 1;\n  string cursor = 2;\n}\n\n// - Return list uris where user A in list B, C, D…\n//     - Used in thread reply gates\nmessage GetListMembershipRequest {\n  string actor_did = 1;\n  repeated string list_uris = 2;\n}\n\nmessage GetListMembershipResponse {\n  repeated string listitem_uris = 1;\n}\n\n// - Return number of items in list A\n//     - For aggregate\nmessage GetListCountRequest {\n  string list_uri = 1;\n}\n\nmessage GetListCountResponse {\n  int32 count = 1;\n}\n\n\n// - return list of uris of lists created by A\n//     - `getLists`\nmessage GetActorListsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetActorListsResponse {\n  repeated string list_uris = 1;\n  string cursor = 2;\n}\n\n//\n// Mutes\n//\n\n// - return boolean if user A has muted user B\n//     - hydrating mute state onto profiles\nmessage GetActorMutesActorRequest {\n  string actor_did = 1;\n  string target_did = 2;\n}\n\nmessage GetActorMutesActorResponse {\n  bool muted = 1;\n}\n\n// - return list of user dids of users who A mutes\n//     - `getMutes`\nmessage GetMutesRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetMutesResponse {\n  repeated string dids = 1;\n  string cursor = 2;\n}\n\n//\n// Mutelists\n//\n\n// - return list uri of *any* list through which user A has muted user B\n//     - hydrating mute state onto profiles\n//     - note: we only need *one* uri even if a user is muted by multiple lists\nmessage GetActorMutesActorViaListRequest {\n  string actor_did = 1;\n  string target_did = 2;\n}\n\nmessage GetActorMutesActorViaListResponse {\n  string list_uri = 1;\n}\n\n// - return boolean if actor A has subscribed to mutelist B\n//     - list view hydration\nmessage GetMutelistSubscriptionRequest {\n  string actor_did = 1;\n  string list_uri = 2;\n}\n\nmessage GetMutelistSubscriptionResponse {\n  bool subscribed = 1;\n}\n\n// - return list of list uris of mutelists that A subscribes to\n//     - `getListMutes`\nmessage GetMutelistSubscriptionsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetMutelistSubscriptionsResponse {\n  repeated string list_uris = 1;\n  string cursor = 2;\n}\n\n//\n// Thread Mutes\n//\n\nmessage GetThreadMutesOnSubjectsRequest {\n  string actor_did = 1;\n  repeated string thread_roots = 2;\n}\n\nmessage GetThreadMutesOnSubjectsResponse {\n  repeated bool muted = 1;\n}\n\n//\n// Blocks\n//\n\n// - Return block uri if there is a block between users A & B (bidirectional)\n//     - hydrating (& actioning) block state on profiles\n//     - handling 3rd party blocks\nmessage GetBidirectionalBlockRequest {\n  string actor_did = 1;\n  string target_did = 2;\n}\n\nmessage GetBidirectionalBlockResponse {\n  string block_uri = 1;\n}\n\n// - Return list of block uris and user dids of users who A blocks\n//     - `getBlocks`\nmessage GetBlocksRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetBlocksResponse {\n  repeated string block_uris = 1;\n  string cursor = 2;\n}\n\n//\n// Blocklists\n//\n\n// - Return list uri of ***any*** list through which users A & B have a block (bidirectional)\n//     - hydrating (& actioning) block state on profiles\n//     - handling 3rd party blocks\nmessage GetBidirectionalBlockViaListRequest {\n  string actor_did = 1;\n  string target_did = 2;\n}\n\nmessage GetBidirectionalBlockViaListResponse {\n  string list_uri = 1;\n}\n\n// - return boolean if user A has subscribed to blocklist B\n//     - list view hydration\nmessage GetBlocklistSubscriptionRequest {\n  string actor_did = 1;\n  string list_uri = 2;\n}\n\nmessage GetBlocklistSubscriptionResponse {\n  string listblock_uri = 1;\n}\n\n// - return list of list uris of Blockslists that A subscribes to\n//     - `getListBlocks`\nmessage GetBlocklistSubscriptionsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetBlocklistSubscriptionsResponse {\n  repeated string list_uris = 1;\n  string cursor = 2;\n}\n\n//\n// Notifications\n//\n\nmessage GetNotificationPreferencesRequest {\n  repeated string dids = 1;\n}\n\nmessage NotificationChannelList {\n  bool enabled = 1;\n}\n\nmessage NotificationChannelPush {\n  bool enabled = 1;\n}\n\nenum NotificationInclude {\n  NOTIFICATION_INCLUDE_UNSPECIFIED = 0;\n  NOTIFICATION_INCLUDE_ALL = 1;\n  NOTIFICATION_INCLUDE_FOLLOWS = 2;\n}\n\nmessage FilterableNotificationPreference {\n  NotificationInclude include = 1;\n  NotificationChannelList list = 2;\n  NotificationChannelPush push = 3;\n}\n\nmessage NotificationPreference {\n  NotificationChannelList list = 1;\n  NotificationChannelPush push = 2;\n}\n\nenum ChatNotificationInclude {\n  CHAT_NOTIFICATION_INCLUDE_UNSPECIFIED = 0;\n  CHAT_NOTIFICATION_INCLUDE_ALL = 1;\n  CHAT_NOTIFICATION_INCLUDE_ACCEPTED = 2;\n}\n\nmessage ChatNotificationPreference {\n  ChatNotificationInclude include = 1;\n  NotificationChannelPush push = 2;\n}\n\nmessage NotificationPreferences {\n  bytes entry = 1;\n  ChatNotificationPreference chat = 2;\n  FilterableNotificationPreference follow = 3;\n  FilterableNotificationPreference like = 4;\n  FilterableNotificationPreference like_via_repost = 5;\n  FilterableNotificationPreference mention = 6;\n  FilterableNotificationPreference quote = 7;\n  FilterableNotificationPreference reply = 8;\n  FilterableNotificationPreference repost = 9;\n  FilterableNotificationPreference repost_via_repost = 10;\n  NotificationPreference starterpack_joined = 11;\n  NotificationPreference subscribed_post = 12;\n  NotificationPreference unverified = 13;\n  NotificationPreference verified = 14;\n}\n\nmessage GetNotificationPreferencesResponse {\n  repeated NotificationPreferences preferences = 1;\n}\n\n// - list recent notifications for a user\n//     - notifications should include a uri for the record that caused the notif & a “reason” for the notification (reply, like, quotepost, etc)\n//     - this should include both read & unread notifs\nmessage GetNotificationsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n  bool priority = 4;\n}\n\nmessage Notification {\n  string recipient_did = 1;\n  string uri = 2;\n  string reason = 3;\n  string reason_subject = 4;\n  google.protobuf.Timestamp timestamp = 5;\n  bool priority = 6;\n}\n\nmessage GetNotificationsResponse {\n  repeated Notification notifications = 1;\n  string cursor = 2;\n}\n\n// - update a user’s “last seen time”\n//     - `updateSeen`\nmessage UpdateNotificationSeenRequest {\n  string actor_did = 1;\n  google.protobuf.Timestamp timestamp = 2;\n  bool priority = 3;\n}\n\nmessage UpdateNotificationSeenResponse {}\n\n// - get a user’s “last seen time”\n//     - hydrating read state onto notifications\nmessage GetNotificationSeenRequest {\n  string actor_did = 1;\n  bool priority = 2;\n}\n\nmessage GetNotificationSeenResponse {\n  google.protobuf.Timestamp timestamp = 1;\n}\n\n// - get a count of all unread notifications (notifications after `updateSeen`)\n//     - `getUnreadCount`\nmessage GetUnreadNotificationCountRequest {\n  string actor_did = 1;\n  bool priority = 2;\n}\n\nmessage GetUnreadNotificationCountResponse {\n  int32 count = 1;\n}\n\nmessage GetActivitySubscriptionDidsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetActivitySubscriptionDidsResponse {\n  repeated string dids = 1;\n  string cursor = 2;\n}\n\nmessage PostActivitySubscription {}\nmessage ReplyActivitySubscription {}\n\nmessage ActivitySubscription {\n  string actor_did = 1;\n  string namespace = 2;\n  string key = 3;\n  optional PostActivitySubscription post = 4;\n  optional ReplyActivitySubscription reply = 5;\n  string subject_did = 6;\n  google.protobuf.Timestamp indexed_at = 7;\n}\n\nmessage GetActivitySubscriptionsByActorAndSubjectsRequest {\n  string actor_did = 1;\n  repeated string subject_dids = 2;\n}\n\nmessage GetActivitySubscriptionsByActorAndSubjectsResponse {\n  repeated ActivitySubscription subscriptions = 1;\n}\n\n//\n// FeedGenerators\n//\n\n// - Return uris of feed generator records created by user A\n//     - `getActorFeeds`\nmessage GetActorFeedsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetActorFeedsResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n// - Returns a list of suggested feed generator uris for an actor, paginated\n//     - `getSuggestedFeeds`\n//     - This is currently just hardcoded in the Appview DB\nmessage GetSuggestedFeedsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetSuggestedFeedsResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\nmessage SearchFeedGeneratorsRequest {\n  string query = 1;\n  int32 limit = 2;\n}\n\nmessage SearchFeedGeneratorsResponse {\n  repeated string uris = 1;\n}\n\n// - Returns feed generator validity and online status with uris A, B, C…\n//     - Not currently being used, but could be worhthwhile.\nmessage GetFeedGeneratorStatusRequest {\n  repeated string uris = 1;\n}\n\nmessage GetFeedGeneratorStatusResponse {\n  repeated string status = 1;\n}\n\n//\n// Feeds\n//\n\nenum FeedType {\n  FEED_TYPE_UNSPECIFIED = 0;\n  FEED_TYPE_POSTS_AND_AUTHOR_THREADS = 1;\n  FEED_TYPE_POSTS_NO_REPLIES = 2;\n  FEED_TYPE_POSTS_WITH_MEDIA = 3;\n  FEED_TYPE_POSTS_WITH_VIDEO = 4;\n}\n\n// - Returns recent posts authored by a given DID, paginated\n//     - `getAuthorFeed`\n//     - Optionally: filter by if a post is/isn’t a reply and if a post has a media object in it\nmessage GetAuthorFeedRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n  FeedType feed_type = 4;\n}\n\nmessage AuthorFeedItem {\n  string uri = 1;\n  string cid = 2;\n  string repost = 3;\n  string repost_cid = 4;\n  bool posts_and_author_threads = 5;\n  bool posts_no_replies = 6;\n  bool posts_with_media = 7;\n  bool is_reply = 8;\n  bool is_repost = 9;\n  bool is_quote_post = 10;\n  bool posts_with_video = 11;\n}\n\nmessage GetAuthorFeedResponse {\n  repeated AuthorFeedItem items = 1;\n  string cursor = 2;\n}\n\n// - Returns recent posts authored by users followed by a given DID, paginated\n//     - `getTimeline`\nmessage GetTimelineRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n  bool exclude_replies = 4;\n  bool exclude_reposts = 5;\n  bool exclude_quotes = 6;\n}\n\nmessage GetTimelineResponse {\n  repeated TimelineFeedItem items = 1;\n  string cursor = 2;\n}\n\nmessage TimelineFeedItem {\n  string uri = 1;\n  string cid = 2;\n  string repost = 3;\n  string repost_cid = 4;\n  bool is_reply = 5;\n  bool is_repost = 6;\n  bool is_quote_post = 7;\n}\n\n// - Return recent post uris from users in list A\n//     - `getListFeed`\n//     - (This is essentially the same as `getTimeline` but instead of follows of a did, it is list items of a list)\nmessage GetListFeedRequest {\n  string list_uri = 1;\n  int32 limit = 2;\n  string cursor = 3;\n  bool exclude_replies = 4;\n  bool exclude_reposts = 5;\n  bool exclude_quotes = 6;\n}\n\nmessage GetListFeedResponse {\n  repeated TimelineFeedItem items = 1;\n  string cursor = 2;\n}\n\n//\n// Threads\n//\n\n// Return posts uris of any replies N levels above or M levels below post A\nmessage GetThreadRequest {\n  string post_uri = 1;\n  int32 above = 2;\n  int32 below = 3;\n}\n\nmessage GetThreadResponse {\n  repeated string uris = 1;\n}\n\n//\n// Search\n//\n\n// - Return DIDs of actors matching term, paginated\n//     - `searchActors` skeleton\nmessage SearchActorsRequest {\n  string term = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage SearchActorsResponse {\n  repeated string dids = 1;\n  string cursor = 2;\n}\n\n// - Return uris of posts matching term, paginated\n//     - `searchPosts` skeleton\nmessage SearchPostsRequest {\n  string term = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage SearchPostsResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n// - Return uris of starter packs matching term, paginated\n//     - `searchStarterPacks` skeleton\nmessage SearchStarterPacksRequest {\n  string term = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage SearchStarterPacksResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n//\n// Suggestions\n//\n\n// - Return DIDs of suggested follows for a user, excluding anyone they already follow\n//     - `getSuggestions`, `getSuggestedFollowsByActor`\nmessage GetFollowSuggestionsRequest {\n  string actor_did = 1;\n  string relative_to_did = 2;\n  int32 limit = 3;\n  string cursor = 4;\n}\n\nmessage GetFollowSuggestionsResponse {\n  repeated string dids = 1;\n  string cursor = 2;\n}\n\nmessage SuggestedEntity {\n  string tag = 1;\n  string subject = 2;\n  string subject_type = 3;\n  int64 priority = 4;\n}\n\nmessage GetSuggestedEntitiesRequest {\n  int32 limit = 1;\n  string cursor = 2;\n}\n\nmessage GetSuggestedEntitiesResponse {\n  repeated SuggestedEntity entities = 1;\n  string cursor = 2;\n}\n\n//\n// Labels\n//\n\n// - Get all labels on a subjects A, B, C (uri or did) issued by dids D, E, F…\n//     - label hydration on nearly every view\nmessage GetLabelsRequest {\n  repeated string subjects = 1;\n  repeated string issuers = 2;\n}\n\nmessage GetLabelsResponse {\n  repeated bytes labels = 1;\n}\n\n//\n// Starter packs\n//\n\nmessage GetActorStarterPacksRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetActorStarterPacksResponse {\n  repeated string uris = 1;\n  string cursor = 2;\n}\n\n//\n// Sync\n//\n\n// - Latest repo rev of user w/ DID\n//     - Read-after-write header in`getProfile`, `getProfiles`, `getActorLikes`, `getAuthorFeed`, `getListFeed`, `getPostThread`, `getTimeline`.  Could it be view dependent?\nmessage GetLatestRevRequest {\n  string actor_did = 1;\n}\n\nmessage GetLatestRevResponse {\n  string rev = 1;\n}\n\n\nmessage GetIdentityByDidRequest {\n  string did = 1;\n}\nmessage GetIdentityByDidResponse {\n  string did = 1;\n  string handle = 2;\n  bytes keys = 3;\n  bytes services = 4;\n  google.protobuf.Timestamp updated = 5;\n}\n\nmessage GetIdentityByHandleRequest {\n  string handle = 1;\n}\nmessage GetIdentityByHandleResponse {\n  string handle = 1;\n  string did = 2;\n  bytes keys = 3;\n  bytes services = 4;\n  google.protobuf.Timestamp updated = 5;\n}\n\n\n\n//\n// Moderation\n//\n\nmessage GetBlobTakedownRequest {\n  string did = 1;\n  string cid = 2;\n}\n\nmessage GetBlobTakedownResponse {\n  bool taken_down = 1;\n  string takedown_ref = 2;\n}\n\n\n\nmessage GetActorTakedownRequest {\n  string did = 1;\n}\n\nmessage GetActorTakedownResponse {\n  bool taken_down = 1;\n  string takedown_ref = 2;\n}\n\nmessage GetRecordTakedownRequest {\n  string record_uri = 1;\n}\n\nmessage GetRecordTakedownResponse {\n  bool taken_down = 1;\n  string takedown_ref = 2;\n}\n\n\n//\n// Bookmarks\n//\nmessage Bookmark {\n  StashRef ref = 1;\n  string subject_uri = 2;\n  string subject_cid = 3;\n  google.protobuf.Timestamp indexed_at = 4;\n}\n\nmessage GetBookmarksByActorAndSubjectsRequest {\n  string actor_did = 1;\n  repeated string uris = 2;\n}\n\nmessage GetBookmarksByActorAndSubjectsResponse {\n  repeated Bookmark bookmarks = 1;\n}\n\nmessage GetActorBookmarksRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage BookmarkInfo {\n  string key = 1; // stash key\n  string subject = 2;\n}\n\nmessage GetActorBookmarksResponse {\n  repeated BookmarkInfo bookmarks = 1;\n  string cursor = 2;\n}\n\n//\n// Drafts\n//\n\n\nmessage GetActorDraftsRequest {\n  string actor_did = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage DraftInfo {\n  string key = 1; // stash key\n  google.protobuf.Timestamp created_at = 2;\n  google.protobuf.Timestamp updated_at = 3;\n  bytes payload = 4;\n}\n\nmessage GetActorDraftsResponse {\n  repeated DraftInfo drafts = 1;\n  string cursor = 2;\n}\n\n\n// Polo-backed Graph Endpoints\n\n\n\n// GetFollowsFollowing gets the list of DIDs that the actor follows that also follow the targets\nmessage GetFollowsFollowingRequest {\n  string actor_did = 1;\n  repeated string target_dids = 2;\n}\n\nmessage FollowsFollowing {\n  string target_did = 1;\n  repeated string dids = 2;\n}\n\nmessage GetFollowsFollowingResponse {\n  repeated FollowsFollowing results = 1;\n}\n\nmessage GetSitemapIndexRequest {\n  SitemapPageType type = 1;\n}\n\nmessage GetSitemapIndexResponse {\n  // GZIP compressed XML sitemap\n  bytes sitemap = 1;\n}\n\n// Sitemap HTTP paths are typically of the form `/type/yyyy-mm-dd/N.xml.gz`, i.e. `/users/2025-01-01/1.xml.gz`\nmessage GetSitemapPageRequest {\n  SitemapPageType type = 1;\n  google.protobuf.Timestamp date = 2;\n  // One-indexed\n  int32 bucket = 3;\n}\n\nenum SitemapPageType {\n  SITEMAP_PAGE_TYPE_UNSPECIFIED = 0;\n  SITEMAP_PAGE_TYPE_USER = 1;\n}\n\nmessage GetSitemapPageResponse {\n  // GZIP compressed XML sitemap\n  bytes sitemap = 1;\n}\n\n// Ping\nmessage PingRequest {}\nmessage PingResponse {}\n\nmessage StashRef {\n  string actor_did = 1;\n  string namespace = 2;\n  string key = 3;\n}\n\nservice Service {\n  //\n  // Read Path\n  //\n\n  // Records\n  rpc GetBlockRecords(GetBlockRecordsRequest) returns (GetBlockRecordsResponse);\n  rpc GetFeedGeneratorRecords(GetFeedGeneratorRecordsRequest) returns (GetFeedGeneratorRecordsResponse);\n  rpc GetFollowRecords(GetFollowRecordsRequest) returns (GetFollowRecordsResponse);\n  rpc GetLikeRecords(GetLikeRecordsRequest) returns (GetLikeRecordsResponse);\n  rpc GetListBlockRecords(GetListBlockRecordsRequest) returns (GetListBlockRecordsResponse);\n  rpc GetListItemRecords(GetListItemRecordsRequest) returns (GetListItemRecordsResponse);\n  rpc GetListRecords(GetListRecordsRequest) returns (GetListRecordsResponse);\n  rpc GetPostRecords(GetPostRecordsRequest) returns (GetPostRecordsResponse);\n  rpc GetProfileRecords(GetProfileRecordsRequest) returns (GetProfileRecordsResponse);\n  rpc GetActorChatDeclarationRecords(GetActorChatDeclarationRecordsRequest) returns (GetActorChatDeclarationRecordsResponse);\n  rpc GetNotificationDeclarationRecords(GetNotificationDeclarationRecordsRequest) returns (GetNotificationDeclarationRecordsResponse);\n  rpc GetGermDeclarationRecords(GetGermDeclarationRecordsRequest) returns (GetGermDeclarationRecordsResponse);\n  rpc GetStatusRecords(GetStatusRecordsRequest) returns (GetStatusRecordsResponse);\n  rpc GetRepostRecords(GetRepostRecordsRequest) returns (GetRepostRecordsResponse);\n  rpc GetThreadGateRecords(GetThreadGateRecordsRequest) returns (GetThreadGateRecordsResponse);\n  rpc GetPostgateRecords(GetPostgateRecordsRequest) returns (GetPostgateRecordsResponse);\n  rpc GetLabelerRecords(GetLabelerRecordsRequest) returns (GetLabelerRecordsResponse);\n  rpc GetStarterPackRecords(GetStarterPackRecordsRequest) returns (GetStarterPackRecordsResponse);\n\n  // Follows\n  rpc GetActorFollowsActors(GetActorFollowsActorsRequest) returns (GetActorFollowsActorsResponse);\n  rpc GetFollowers(GetFollowersRequest) returns (GetFollowersResponse);\n  rpc GetFollows(GetFollowsRequest) returns (GetFollowsResponse);\n\n  // Verifications\n  rpc GetVerificationRecords(GetVerificationRecordsRequest) returns (GetVerificationRecordsResponse);\n  rpc GetVerificationsIssued(GetVerificationsIssuedRequest) returns (GetVerificationsIssuedResponse);\n  rpc GetVerificationsReceived(GetVerificationsReceivedRequest) returns (GetVerificationsReceivedResponse);\n\n  // Likes\n  rpc GetLikesBySubject(GetLikesBySubjectRequest) returns (GetLikesBySubjectResponse);\n  rpc GetLikesBySubjectSorted(GetLikesBySubjectSortedRequest) returns (GetLikesBySubjectSortedResponse);\n  rpc GetLikesByActorAndSubjects(GetLikesByActorAndSubjectsRequest) returns (GetLikesByActorAndSubjectsResponse);\n  rpc GetActorLikes(GetActorLikesRequest) returns (GetActorLikesResponse);\n\n  // Reposts\n  rpc GetRepostsBySubject(GetRepostsBySubjectRequest) returns (GetRepostsBySubjectResponse);\n  rpc GetRepostsByActorAndSubjects(GetRepostsByActorAndSubjectsRequest) returns (GetRepostsByActorAndSubjectsResponse);\n  rpc GetActorReposts(GetActorRepostsRequest) returns (GetActorRepostsResponse);\n\n  // Quotes\n  rpc GetQuotesBySubjectSorted(GetQuotesBySubjectSortedRequest) returns (GetQuotesBySubjectSortedResponse);\n\n  // Interaction Counts\n  rpc GetInteractionCounts(GetInteractionCountsRequest) returns (GetInteractionCountsResponse);\n  rpc GetCountsForUsers(GetCountsForUsersRequest) returns (GetCountsForUsersResponse);\n  rpc GetStarterPackCounts(GetStarterPackCountsRequest) returns (GetStarterPackCountsResponse);\n  rpc GetListCounts(GetListCountsRequest) returns (GetListCountsResponse);\n  rpc GetNewUserCountForRange(GetNewUserCountForRangeRequest) returns (GetNewUserCountForRangeResponse);\n\n  // Profile\n  rpc GetActors(GetActorsRequest) returns (GetActorsResponse);\n  rpc GetDidsByHandles(GetDidsByHandlesRequest) returns (GetDidsByHandlesResponse);\n\n  // Relationships\n  rpc GetRelationships(GetRelationshipsRequest) returns (GetRelationshipsResponse);\n  rpc GetBlockExistence(GetBlockExistenceRequest) returns (GetBlockExistenceResponse);\n\n  // Lists\n  rpc GetActorLists(GetActorListsRequest) returns (GetActorListsResponse);\n  rpc GetListMembers(GetListMembersRequest) returns (GetListMembersResponse);\n  rpc GetListMembership(GetListMembershipRequest) returns (GetListMembershipResponse);\n  rpc GetListCount(GetListCountRequest) returns (GetListCountResponse);\n\n  // Mutes\n  rpc GetActorMutesActor(GetActorMutesActorRequest) returns (GetActorMutesActorResponse);\n  rpc GetMutes(GetMutesRequest) returns (GetMutesResponse);\n\n  // Mutelists\n  rpc GetActorMutesActorViaList(GetActorMutesActorViaListRequest) returns (GetActorMutesActorViaListResponse);\n  rpc GetMutelistSubscription(GetMutelistSubscriptionRequest) returns (GetMutelistSubscriptionResponse);\n  rpc GetMutelistSubscriptions(GetMutelistSubscriptionsRequest) returns (GetMutelistSubscriptionsResponse);\n\n  // Thread Mutes\n  rpc GetThreadMutesOnSubjects(GetThreadMutesOnSubjectsRequest) returns (GetThreadMutesOnSubjectsResponse);\n\n  // Blocks\n  rpc GetBidirectionalBlock(GetBidirectionalBlockRequest) returns (GetBidirectionalBlockResponse);\n  rpc GetBlocks(GetBlocksRequest) returns (GetBlocksResponse);\n\n  // Blocklists\n  rpc GetBidirectionalBlockViaList(GetBidirectionalBlockViaListRequest) returns (GetBidirectionalBlockViaListResponse);\n  rpc GetBlocklistSubscription(GetBlocklistSubscriptionRequest) returns (GetBlocklistSubscriptionResponse);\n  rpc GetBlocklistSubscriptions(GetBlocklistSubscriptionsRequest) returns (GetBlocklistSubscriptionsResponse);\n\n  // Notifications\n  rpc GetNotificationPreferences(GetNotificationPreferencesRequest) returns (GetNotificationPreferencesResponse);\n  rpc GetNotifications(GetNotificationsRequest) returns (GetNotificationsResponse);\n  rpc GetNotificationSeen(GetNotificationSeenRequest) returns (GetNotificationSeenResponse);\n  rpc GetUnreadNotificationCount(GetUnreadNotificationCountRequest) returns (GetUnreadNotificationCountResponse);\n  rpc GetActivitySubscriptionDids(GetActivitySubscriptionDidsRequest) returns (GetActivitySubscriptionDidsResponse);\n  rpc GetActivitySubscriptionsByActorAndSubjects(GetActivitySubscriptionsByActorAndSubjectsRequest) returns (GetActivitySubscriptionsByActorAndSubjectsResponse);\n  rpc UpdateNotificationSeen(UpdateNotificationSeenRequest) returns (UpdateNotificationSeenResponse);\n\n  // FeedGenerators\n  rpc GetActorFeeds(GetActorFeedsRequest) returns (GetActorFeedsResponse);\n  rpc GetSuggestedFeeds(GetSuggestedFeedsRequest) returns (GetSuggestedFeedsResponse);\n  rpc GetFeedGeneratorStatus(GetFeedGeneratorStatusRequest) returns (GetFeedGeneratorStatusResponse);\n  rpc SearchFeedGenerators(SearchFeedGeneratorsRequest) returns (SearchFeedGeneratorsResponse);\n\n  // Feeds\n  rpc GetAuthorFeed(GetAuthorFeedRequest) returns (GetAuthorFeedResponse);\n  rpc GetTimeline(GetTimelineRequest) returns (GetTimelineResponse);\n  rpc GetListFeed(GetListFeedRequest) returns (GetListFeedResponse);\n\n  // Threads\n  rpc GetThread(GetThreadRequest) returns (GetThreadResponse);\n\n  // Search\n  rpc SearchActors(SearchActorsRequest) returns (SearchActorsResponse);\n  rpc SearchPosts(SearchPostsRequest) returns (SearchPostsResponse);\n  rpc SearchStarterPacks(SearchStarterPacksRequest) returns (SearchStarterPacksResponse);\n\n  // Suggestions\n  rpc GetFollowSuggestions(GetFollowSuggestionsRequest) returns (GetFollowSuggestionsResponse);\n  rpc GetSuggestedEntities(GetSuggestedEntitiesRequest) returns (GetSuggestedEntitiesResponse);\n\n  // Labels\n  rpc GetLabels(GetLabelsRequest) returns (GetLabelsResponse);\n  rpc GetAllLabelers(GetAllLabelersRequest) returns (GetAllLabelersResponse);\n\n  // Starter packs\n  rpc GetActorStarterPacks(GetActorStarterPacksRequest) returns (GetActorStarterPacksResponse);\n\n  // Sync\n  rpc GetLatestRev(GetLatestRevRequest) returns (GetLatestRevResponse);\n\n  // Moderation\n  rpc GetBlobTakedown(GetBlobTakedownRequest) returns (GetBlobTakedownResponse);\n  rpc GetRecordTakedown(GetRecordTakedownRequest) returns (GetRecordTakedownResponse);\n  rpc GetActorTakedown(GetActorTakedownRequest) returns (GetActorTakedownResponse);\n\n  // Bookmarks\n  // Returns bookmarks created by the actor for the specified URIs.\n  rpc GetBookmarksByActorAndSubjects(GetBookmarksByActorAndSubjectsRequest) returns (GetBookmarksByActorAndSubjectsResponse);\n  // Returns the bookmarks created by the actor.\n  rpc GetActorBookmarks(GetActorBookmarksRequest) returns (GetActorBookmarksResponse);\n\n  // Drafts\n  // Returns a page of drafts for a user.\n  rpc GetActorDrafts(GetActorDraftsRequest) returns (GetActorDraftsResponse);\n\n  // Identity\n  rpc GetIdentityByDid(GetIdentityByDidRequest) returns (GetIdentityByDidResponse);\n  rpc GetIdentityByHandle(GetIdentityByHandleRequest) returns (GetIdentityByHandleResponse);\n\n  // Graph\n  rpc GetFollowsFollowing(GetFollowsFollowingRequest) returns (GetFollowsFollowingResponse);\n\n  // Sitemaps\n  rpc GetSitemapIndex(GetSitemapIndexRequest) returns (GetSitemapIndexResponse);\n  rpc GetSitemapPage(GetSitemapPageRequest) returns (GetSitemapPageResponse);\n\n  // Ping\n  rpc Ping(PingRequest) returns (PingResponse);\n\n\n\n\n  //\n  // Write Path\n  //\n\n  // Moderation\n  rpc TakedownBlob(TakedownBlobRequest) returns (TakedownBlobResponse);\n  rpc TakedownRecord(TakedownRecordRequest) returns (TakedownRecordResponse);\n  rpc TakedownActor(TakedownActorRequest) returns (TakedownActorResponse);\n  rpc UpdateActorUpstreamStatus(UpdateActorUpstreamStatusRequest) returns (UpdateActorUpstreamStatusResponse);\n\n  rpc UntakedownBlob(UntakedownBlobRequest) returns (UntakedownBlobResponse);\n  rpc UntakedownRecord(UntakedownRecordRequest) returns (UntakedownRecordResponse);\n  rpc UntakedownActor(UntakedownActorRequest) returns (UntakedownActorResponse);\n\n  // Ingestion\n  rpc CreateActorMute(CreateActorMuteRequest) returns (CreateActorMuteResponse);\n  rpc DeleteActorMute(DeleteActorMuteRequest) returns (DeleteActorMuteResponse);\n  rpc ClearActorMutes(ClearActorMutesRequest) returns (ClearActorMutesResponse);\n\n  rpc CreateActorMutelistSubscription(CreateActorMutelistSubscriptionRequest) returns (CreateActorMutelistSubscriptionResponse);\n  rpc DeleteActorMutelistSubscription(DeleteActorMutelistSubscriptionRequest) returns (DeleteActorMutelistSubscriptionResponse);\n  rpc ClearActorMutelistSubscriptions(ClearActorMutelistSubscriptionsRequest) returns (ClearActorMutelistSubscriptionsResponse);\n\n  rpc CreateThreadMute(CreateThreadMuteRequest) returns (CreateThreadMuteResponse);\n  rpc DeleteThreadMute(DeleteThreadMuteRequest) returns (DeleteThreadMuteResponse);\n  rpc ClearThreadMutes(ClearThreadMutesRequest) returns (ClearThreadMutesResponse);\n}\n\n\n//\n// Write Path\n//\n\nmessage UpdateActorUpstreamStatusRequest {\n  string actor_did = 1;\n  bool active = 2;\n  string upstream_status = 3;\n}\n\nmessage UpdateActorUpstreamStatusResponse {\n}\n\nmessage TakedownActorRequest {\n  string did = 1;\n  string ref = 2;\n  google.protobuf.Timestamp seen = 3;\n}\n\nmessage TakedownActorResponse {\n}\n\nmessage UntakedownActorRequest {\n  string did = 1;\n  google.protobuf.Timestamp seen = 2;\n}\n\nmessage UntakedownActorResponse {\n}\n\nmessage TakedownBlobRequest {\n  string did = 1;\n  string cid = 2;\n  string ref = 3;\n  google.protobuf.Timestamp seen = 4;\n}\n\nmessage TakedownBlobResponse {}\n\nmessage UntakedownBlobRequest {\n  string did = 1;\n  string cid = 2;\n  google.protobuf.Timestamp seen = 3;\n}\n\nmessage UntakedownBlobResponse {}\n\nmessage TakedownRecordRequest {\n  string record_uri = 1;\n  string ref = 2;\n  google.protobuf.Timestamp seen = 3;\n}\n\nmessage TakedownRecordResponse {\n}\n\nmessage UntakedownRecordRequest {\n  string record_uri = 1;\n  google.protobuf.Timestamp seen = 2;\n}\n\nmessage UntakedownRecordResponse {\n}\n\nmessage CreateActorMuteRequest {\n  string actor_did = 1;\n  string subject_did = 2;\n}\n\nmessage CreateActorMuteResponse {}\n\nmessage DeleteActorMuteRequest {\n  string actor_did = 1;\n  string subject_did = 2;\n}\n\nmessage DeleteActorMuteResponse {}\n\nmessage ClearActorMutesRequest {\n  string actor_did = 1;\n}\n\nmessage ClearActorMutesResponse {}\n\nmessage CreateActorMutelistSubscriptionRequest {\n  string actor_did = 1;\n  string subject_uri = 2;\n}\n\nmessage CreateActorMutelistSubscriptionResponse {}\n\nmessage DeleteActorMutelistSubscriptionRequest {\n  string actor_did = 1;\n  string subject_uri = 2;\n}\n\nmessage DeleteActorMutelistSubscriptionResponse {}\n\nmessage ClearActorMutelistSubscriptionsRequest {\n  string actor_did = 1;\n}\n\nmessage ClearActorMutelistSubscriptionsResponse {}\n\nmessage CreateThreadMuteRequest {\n  string actor_did = 1;\n  string thread_root = 2;\n}\n\nmessage CreateThreadMuteResponse {}\n\nmessage DeleteThreadMuteRequest {\n  string actor_did = 1;\n  string thread_root = 2;\n}\n\nmessage DeleteThreadMuteResponse {}\n\nmessage ClearThreadMutesRequest {\n  string actor_did = 1;\n}\n\nmessage ClearThreadMutesResponse {}\n"
  },
  {
    "path": "packages/bsky/proto/courier.proto",
    "content": "syntax = \"proto3\";\n\npackage courier;\noption go_package = \"./;courier\";\n\nimport \"google/protobuf/struct.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\n//\n// Messages\n//\n\n// Ping\nmessage PingRequest {}\nmessage PingResponse {}\n\n// Notifications\n\nenum AppPlatform {\n  APP_PLATFORM_UNSPECIFIED = 0;\n  APP_PLATFORM_IOS = 1;\n  APP_PLATFORM_ANDROID = 2;\n  APP_PLATFORM_WEB = 3;\n}\n\nmessage Notification {\n  string id = 1;\n  string recipient_did = 2;\n  string title = 3;\n  string message = 4;\n  string collapse_key = 5;\n  bool always_deliver = 6;\n  google.protobuf.Timestamp timestamp = 7;\n  google.protobuf.Struct additional = 8;\n  bool client_controlled = 9;\n}\n\nmessage PushNotificationsRequest {\n  repeated Notification notifications = 1;\n}\n\nmessage PushNotificationsResponse {}\n\nmessage RegisterDeviceTokenRequest {\n  string did = 1;\n  string token = 2;\n  string app_id = 3;\n  AppPlatform platform = 4;\n  bool age_restricted = 5;\n}\n\nmessage RegisterDeviceTokenResponse {}\n\nmessage UnregisterDeviceTokenRequest {\n  string did = 1;\n  string token = 2;\n  string app_id = 3;\n  AppPlatform platform = 4;\n}\n\nmessage UnregisterDeviceTokenResponse {}\n\nmessage SetAgeRestrictedRequest {\n  string did = 1;\n  bool age_restricted = 2;\n}\n\nmessage SetAgeRestrictedResponse {}\n\nservice Service {\n  rpc Ping(PingRequest) returns (PingResponse);\n  rpc PushNotifications(PushNotificationsRequest) returns (PushNotificationsResponse);\n  rpc RegisterDeviceToken(RegisterDeviceTokenRequest) returns (RegisterDeviceTokenResponse);\n  rpc UnregisterDeviceToken(UnregisterDeviceTokenRequest) returns (UnregisterDeviceTokenResponse);\n  rpc SetAgeRestricted(SetAgeRestrictedRequest) returns (SetAgeRestrictedResponse);\n}\n"
  },
  {
    "path": "packages/bsky/proto/rolodex.proto",
    "content": "syntax = \"proto3\";\n\npackage rolodex;\noption go_package = \"./;rolodex\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n//\n// Messages\n//\n\n// Ping\nmessage PingRequest {}\n\nmessage PingResponse {}\n\n// GetSyncStatus\nmessage GetSyncStatusRequest {\n  string actor = 1;\n}\n\nmessage SyncStatus {\n  google.protobuf.Timestamp synced_at = 1;\n  int32 matches_count = 2;\n}\n\nmessage GetSyncStatusResponse {\n  SyncStatus status = 1;\n}\n\n// StartPhoneVerification\nmessage StartPhoneVerificationRequest {\n  string actor = 1;\n  string phone = 2;\n}\n\nmessage StartPhoneVerificationResponse {}\n\n// VerifyPhone\nmessage VerifyPhoneRequest {\n  string actor = 1;\n  string phone = 2;\n  string verification_code = 3;\n}\n\nmessage VerifyPhoneResponse {\n  string token = 1;\n}\n\n// ImportContacts\nmessage ImportContactsRequest {\n  string actor = 1;\n  string token = 2;\n  repeated string contacts = 3;\n}\n\nmessage ImportContactsMatch {\n  // To which index of the input contacts this contact corresponds.\n  int32 input_index = 1;\n  string subject = 2;\n}\n\nmessage ImportContactsResponse {\n  repeated ImportContactsMatch matches = 1;\n}\n\n// GetMatches\nmessage GetMatchesRequest {\n  string actor = 1;\n  int32 limit = 2;\n  string cursor = 3;\n}\n\nmessage GetMatchesResponse {\n  repeated string subjects = 1;\n  string cursor = 2;\n}\n\n// DismissMatch\nmessage DismissMatchRequest {\n  string actor = 1;\n  string subject = 2;\n}\n\nmessage DismissMatchResponse {\n  int32 matches_count = 1;\n}\n\n// RemoveData\nmessage RemoveDataRequest {\n  string actor = 1;\n}\n\nmessage RemoveDataResponse {\n  int32 contacts_count = 1;\n  int32 matches_count = 2;\n}\n\n//\n// Service\n//\n\nservice RolodexService {\n  rpc Ping(PingRequest) returns (PingResponse);\n\n  rpc GetSyncStatus(GetSyncStatusRequest) returns (GetSyncStatusResponse);\n\n  rpc StartPhoneVerification(StartPhoneVerificationRequest) returns (StartPhoneVerificationResponse);\n  rpc VerifyPhone(VerifyPhoneRequest) returns (VerifyPhoneResponse);\n  rpc ImportContacts(ImportContactsRequest) returns (ImportContactsResponse);\n\n  rpc GetMatches(GetMatchesRequest) returns (GetMatchesResponse);\n  rpc DismissMatch(DismissMatchRequest) returns (DismissMatchResponse);\n\n  rpc RemoveData(RemoveDataRequest) returns (RemoveDataResponse);\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/const.ts",
    "content": "import {\n  AppBskyAgeassuranceDefs,\n  ageAssuranceRuleIDs as ids,\n} from '@atproto/api'\n\n/**\n * Age assurance configuration defining rules for various regions.\n *\n * NOTE: These rules are matched in order, and the first matching rule\n * determines the access level granted.\n *\n * NOTE: all regions MUST have a default rule as the last rule.\n */\nexport const AGE_ASSURANCE_CONFIG: AppBskyAgeassuranceDefs.Config = {\n  regions: [\n    {\n      countryCode: 'GB',\n      regionCode: undefined,\n      minAccessAge: 13,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 13,\n          access: 'safe',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'AU',\n      regionCode: undefined,\n      minAccessAge: 16,\n      rules: [\n        {\n          $type: ids.IfAccountNewerThan,\n          date: '2025-12-10T00:00:00Z',\n          access: 'none',\n        },\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 16,\n          access: 'safe',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 16,\n          access: 'safe',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'US',\n      regionCode: 'SD',\n      minAccessAge: 13,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 13,\n          access: 'safe',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'US',\n      regionCode: 'WY',\n      minAccessAge: 13,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 13,\n          access: 'safe',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'US',\n      regionCode: 'OH',\n      minAccessAge: 13,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 13,\n          access: 'safe',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'US',\n      regionCode: 'MS',\n      minAccessAge: 18,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'US',\n      regionCode: 'VA',\n      minAccessAge: 16,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 16,\n          access: 'full',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 16,\n          access: 'full',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n    {\n      countryCode: 'US',\n      regionCode: 'TN',\n      minAccessAge: 18,\n      rules: [\n        {\n          $type: ids.IfAssuredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.IfDeclaredOverAge,\n          age: 18,\n          access: 'full',\n        },\n        {\n          $type: ids.Default,\n          access: 'none',\n        },\n      ],\n    },\n  ],\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/index.ts",
    "content": "import { Router, raw } from 'express'\nimport { AppContext } from '../../context'\nimport { webhookAuth } from '../kws/webhook'\nimport { handler as ageVerifiedRedirect } from './redirects/kws-age-verified'\nimport { AppContextWithAA } from './types'\nimport { handler as ageVerifiedWebhook } from './webhooks/kws-age-verified'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  assertAppContextWithAgeAssuranceClient(ctx)\n\n  const router = Router()\n  router.use(raw({ type: 'application/json' }))\n  router.post(\n    '/age-assurance/webhooks/kws-age-verified',\n    webhookAuth({\n      secret: ctx.cfg.kws.ageVerifiedWebhookSecret,\n    }),\n    ageVerifiedWebhook(ctx),\n  )\n  router.get(\n    '/age-assurance/redirects/kws-age-verified',\n    ageVerifiedRedirect(ctx),\n  )\n\n  return router\n}\n\nconst assertAppContextWithAgeAssuranceClient: (\n  ctx: AppContext,\n) => asserts ctx is AppContextWithAA = (ctx: AppContext) => {\n  if (!ctx.kwsClient) {\n    throw new Error('Tried to set up KWS router without kwsClient configured.')\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/kws/age-verified.ts",
    "content": "import { z } from 'zod'\n\n/**\n * Schema for KWS the `status` object on `age-verified` payloads.\n */\nexport const KWSAgeVerifiedStatusSchema = z.object({\n  verified: z.boolean(),\n  verifiedMinimumAge: z.number(),\n  transactionId: z.string().optional(),\n})\n\n/**\n * The KWS `status` object on `age-verified` payloads.\n */\nexport type KWSAgeVerifiedStatus = z.infer<typeof KWSAgeVerifiedStatusSchema>\n\nexport function serializeKWSAgeVerifiedStatus(\n  status: KWSAgeVerifiedStatus,\n): string {\n  return JSON.stringify(KWSAgeVerifiedStatusSchema.parse(status))\n}\n\n/**\n * Parse KWS `age-verified` status object.\n */\nexport const parseKWSAgeVerifiedStatus = (\n  raw: string,\n): KWSAgeVerifiedStatus => {\n  try {\n    const value = JSON.parse(raw)\n    return KWSAgeVerifiedStatusSchema.parse(value)\n  } catch (err) {\n    throw new Error(`Invalid KWS age-verified status: ${raw}`, {\n      cause: err,\n    })\n  }\n}\n\n/**\n * Schema for KWS `age-verified` webhooks.\n *\n * Note: we don't use `.strict()` here so that we avoid breaking if KWS adds\n * fields, and some fields below are not strictly typed since we're not using\n * them.\n */\nexport const KWSAgeVerifiedWebhookSchema = z.object({\n  name: z.string(),\n  time: z.string(), // ISO8601 timestamp, but don't validate here\n  orgId: z.string().uuid().optional(),\n  productId: z.string().uuid().optional(),\n  payload: z.object({\n    email: z.string(), // no need to validate here\n    externalPayload: z.string(),\n    status: KWSAgeVerifiedStatusSchema,\n  }),\n})\n\n/**\n * The raw KWS `age-verified` webhook body\n */\nexport type KWSWebhookAgeVerified = z.infer<typeof KWSAgeVerifiedWebhookSchema>\n\n/**\n * Parse KWS `age-verified` webhook body and its external payload.\n */\nexport const parseKWSAgeVerifiedWebhook = (\n  raw: string,\n): KWSWebhookAgeVerified => {\n  try {\n    const value: unknown = JSON.parse(raw)\n    return KWSAgeVerifiedWebhookSchema.parse(value)\n  } catch (err) {\n    throw new Error(`Invalid webhook body: ${raw}`, { cause: err })\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/kws/const.ts",
    "content": "/**\n * Supported languages for KWS Adult Verification. This list comes from KWS's\n * Age Verification Developer Guide PDF doc.\n */\nexport const KWS_SUPPORTED_LANGUAGES = new Set([\n  'en',\n  'ar',\n  'zh-Hans',\n  'nl',\n  'tl',\n  'fr',\n  'de',\n  'id',\n  'it',\n  'ja',\n  'ko',\n  'pl',\n  'pt-BR',\n  'pt',\n  'ru',\n  'es',\n  'th',\n  'tr',\n  'vi',\n])\n\n/**\n * Regions where our \"version 2\" using the `age-verified` KWS flow is\n * available. In these regions, we'll use a different KWS flow from the\n * existing `adult-verified` flow, pass along a different external payload, and\n * handle webhooks/redirects differently in the appview.\n */\nexport const KWS_V2_COUNTRIES = new Set(['AU'])\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/kws/external-payload.test.ts",
    "content": "import { describe, expect, it } from '@jest/globals'\nimport {\n  KWSExternalPayloadVersion,\n  parseKWSExternalPayloadV1WithV2Compat,\n  parseKWSExternalPayloadV2,\n  parseKWSExternalPayloadVersion,\n  serializeKWSExternalPayloadV1,\n  serializeKWSExternalPayloadV2,\n} from './external-payload'\n\ndescribe('parseKWSExternalPayloadVersion', () => {\n  it('should return V2 for \"2\"', () => {\n    const result = parseKWSExternalPayloadVersion('2')\n    expect(result).toBe('2')\n  })\n  it('should return V1 for unknown versions', () => {\n    const result = parseKWSExternalPayloadVersion('unknown')\n    expect(result).toBe('1')\n  })\n})\n\ndescribe('parseKWSExternalPayloadV1WithV2Compat', () => {\n  it('should parse V1 payload correctly', () => {\n    const payload = {\n      attemptId: '123',\n      actorDid: 'did:plc:123',\n    }\n    const serialized = serializeKWSExternalPayloadV1(payload)\n    const result = parseKWSExternalPayloadV1WithV2Compat(serialized)\n    expect(result).toEqual({\n      version: KWSExternalPayloadVersion.V1,\n      ...payload,\n    })\n  })\n  it('should parse V2 payload correctly', () => {\n    const payload = {\n      version: KWSExternalPayloadVersion.V2 as const,\n      attemptId: '123',\n      actorDid: 'did:plc:123',\n      countryCode: 'US',\n    }\n    const serialized = serializeKWSExternalPayloadV2(payload)\n    const result = parseKWSExternalPayloadV1WithV2Compat(serialized)\n    expect(result).toEqual(payload)\n  })\n})\n\ndescribe('serializeKWSExternalPayloadV2 & parseKWSExternalPayloadV2', () => {\n  const payload = {\n    version: KWSExternalPayloadVersion.V2 as const,\n    attemptId: '123',\n    actorDid: 'did:plc:123',\n    countryCode: 'US',\n    regionCode: 'CA',\n  }\n  it('compresses when serializing', () => {\n    const serialized = serializeKWSExternalPayloadV2(payload)\n    const comparison = JSON.stringify({\n      v: KWSExternalPayloadVersion.V2,\n      id: payload.attemptId,\n      did: payload.actorDid,\n      gc: payload.countryCode,\n      gr: payload.regionCode,\n    })\n    expect(serialized).toEqual(comparison)\n  })\n  it('decompresses when parsing', () => {\n    const serialized = serializeKWSExternalPayloadV2(payload)\n    const deserialized = parseKWSExternalPayloadV2(serialized)\n    expect(deserialized).toEqual(payload)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/kws/external-payload.ts",
    "content": "import { z } from 'zod'\n\nexport const KWS_EXTERNAL_PAYLOAD_CHAR_LIMIT = 250\n\n/**\n * Thrown when the provided external payload exceeds KWS's character limit.\n *\n * This is most commonly caused by DIDs that are too long, such as for\n * `did:web` DIDs. But it's very rare, and the client has special handling for\n * this case.\n */\nexport class KWSExternalPayloadTooLargeError extends Error {}\n\nexport enum KWSExternalPayloadVersion {\n  V1 = '1',\n  V2 = '2',\n}\n\nexport function parseKWSExternalPayloadVersion(raw: string) {\n  switch (raw) {\n    case KWSExternalPayloadVersion.V2:\n      return KWSExternalPayloadVersion.V2\n    default:\n      return KWSExternalPayloadVersion.V1\n  }\n}\n\nexport type KWSExternalPayloadV1 = {\n  actorDid: string\n  attemptId: string\n}\n\nexport const KWSExternalPayloadV1Schema = z.object({\n  actorDid: z.string(),\n  attemptId: z.string(),\n})\n\nexport function parseKWSExternalPayloadV1(raw: string): KWSExternalPayloadV1 {\n  try {\n    const value: unknown = JSON.parse(raw)\n    return KWSExternalPayloadV1Schema.parse(value)\n  } catch (err) {\n    throw new Error(`Failed to parse KWSExternalPayloadV1`, {\n      cause: err,\n    })\n  }\n}\n\nexport function serializeKWSExternalPayloadV1(\n  payload: KWSExternalPayloadV1,\n): string {\n  try {\n    return JSON.stringify(KWSExternalPayloadV1Schema.parse(payload))\n  } catch (err) {\n    throw new Error('Failed to serialize KWSExternalPayloadV1', { cause: err })\n  }\n}\n\n/**\n * During our migration from v1 to v2 of the KWS external payload, we'll be\n * sending v2 payloads on the v1 flow (the `adult-verified` email flow). We use\n * this utility to parse either v1 or v2 payloads in that flow.\n *\n * Check for the `version` field on the output of this method to discriminate\n * between the two types and handle them differently.\n */\nexport function parseKWSExternalPayloadV1WithV2Compat(\n  raw: string,\n):\n  | (KWSExternalPayloadV1 & { version: KWSExternalPayloadVersion.V1 })\n  | KWSExternalPayloadV2 {\n  const deserialized = JSON.parse(raw)\n  const v2 = deserialized.v === KWSExternalPayloadVersion.V2\n\n  if (v2) {\n    return parseKWSExternalPayloadV2(raw)\n  } else {\n    return {\n      ...parseKWSExternalPayloadV1(raw),\n      version: KWSExternalPayloadVersion.V1,\n    }\n  }\n}\n\n/***************************\n * KWS External Payload V2 *\n ***************************/\n\nexport type KWSExternalPayloadV2 = {\n  version: KWSExternalPayloadVersion.V2\n  attemptId: string\n  actorDid: string\n  countryCode: string\n  regionCode?: string\n}\n\nexport const KWSExternalPayloadV2Schema = z.object({\n  v: z.string(),\n  id: z.string(),\n  did: z.string(),\n  gc: z.string().length(2),\n  gr: z.string().optional(),\n})\n\nexport function serializeKWSExternalPayloadV2(\n  payload: KWSExternalPayloadV2,\n): string {\n  let compressed: z.infer<typeof KWSExternalPayloadV2Schema>\n  try {\n    compressed = KWSExternalPayloadV2Schema.parse({\n      v: KWSExternalPayloadVersion.V2, // version\n      id: payload.attemptId,\n      did: payload.actorDid,\n      gc: payload.countryCode, // geolocation country\n      gr: payload.regionCode, // geolocation region\n    })\n  } catch (err) {\n    throw new Error('Failed to serialize KWSExternalPayloadV2', { cause: err })\n  }\n\n  const serialized = JSON.stringify(compressed)\n\n  if (serialized.length > KWS_EXTERNAL_PAYLOAD_CHAR_LIMIT) {\n    throw new KWSExternalPayloadTooLargeError(\n      `Serialized external payload size ${serialized.length} exceeds limit of ${KWS_EXTERNAL_PAYLOAD_CHAR_LIMIT}`,\n    )\n  }\n\n  return serialized\n}\n\nexport function parseKWSExternalPayloadV2(raw: string): KWSExternalPayloadV2 {\n  try {\n    const deserialized = JSON.parse(raw)\n    const parsed = KWSExternalPayloadV2Schema.parse(deserialized)\n\n    return {\n      version: KWSExternalPayloadVersion.V2,\n      attemptId: parsed.id,\n      actorDid: parsed.did,\n      countryCode: parsed.gc,\n      regionCode: parsed.gr,\n    }\n  } catch (err) {\n    throw new Error(`Failed to parse KWSExternalPayloadV2`, {\n      cause: err,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/redirects/kws-age-verified.ts",
    "content": "import express, { RequestHandler } from 'express'\nimport { ageAssuranceLogger as logger } from '../../../logger'\nimport { getClientUa, validateSignature } from '../../kws/util'\nimport { AGE_ASSURANCE_CONFIG } from '../const'\nimport { parseKWSAgeVerifiedStatus } from '../kws/age-verified'\nimport {\n  type KWSExternalPayloadV2,\n  parseKWSExternalPayloadV2,\n} from '../kws/external-payload'\nimport { createEvent } from '../stash'\nimport { AppContextWithAA } from '../types'\nimport { computeAgeAssuranceAccessOrThrow } from '../util'\n\nfunction parseQueryParams(\n  ctx: AppContextWithAA,\n  req: express.Request,\n): {\n  status: string\n  externalPayload: string\n} {\n  try {\n    const status = String(req.query.status)\n    const externalPayload = String(req.query.externalPayload)\n    const signature = String(req.query.signature)\n\n    validateSignature(\n      ctx.cfg.kws.ageVerifiedRedirectSecret,\n      `${status}:${externalPayload}`,\n      signature,\n    )\n\n    return {\n      status,\n      externalPayload,\n    }\n  } catch (err) {\n    throw new Error('Invalid KWS API request', { cause: err })\n  }\n}\n\nexport const handler =\n  (ctx: AppContextWithAA): RequestHandler =>\n  async (req: express.Request, res: express.Response) => {\n    let externalPayload: KWSExternalPayloadV2 | undefined\n\n    try {\n      const query = parseQueryParams(ctx, req)\n      const { verified, verifiedMinimumAge } = parseKWSAgeVerifiedStatus(\n        query.status,\n      )\n      externalPayload = parseKWSExternalPayloadV2(query.externalPayload)\n      const { actorDid, attemptId, countryCode, regionCode } = externalPayload\n\n      /*\n       * KWS does not send unverified webhooks for age verification, so we\n       * expect all webhooks to be verified. This is just a sanity check.\n       */\n      if (!verified) {\n        const message =\n          'Expected KWS verification redirect to have verified status'\n        logger.error({}, message)\n        throw new Error(message)\n      }\n\n      const { access } = computeAgeAssuranceAccessOrThrow(\n        AGE_ASSURANCE_CONFIG,\n        {\n          countryCode,\n          regionCode,\n          verifiedMinimumAge,\n        },\n      )\n\n      await createEvent(ctx, actorDid, {\n        attemptId,\n        // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.\n        completeIp: req.ip,\n        completeUa: getClientUa(req),\n        countryCode,\n        regionCode,\n        status: 'assured',\n        access,\n      })\n\n      const q = new URLSearchParams({ actorDid, result: 'success' })\n\n      return res\n        .status(302)\n        .setHeader('Location', `${ctx.cfg.kws.redirectUrl}?${q}`)\n        .end()\n    } catch (err) {\n      logger.error(\n        { err, ...externalPayload },\n        'Failed to handle KWS verification redirect',\n      )\n\n      const q = new URLSearchParams({\n        ...(externalPayload ? { actorDid: externalPayload.actorDid } : {}),\n        result: 'unknown',\n      })\n\n      return res\n        .status(302)\n        .setHeader('Location', `${ctx.cfg.kws.redirectUrl}?${q}`)\n        .end()\n    }\n  }\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/stash.ts",
    "content": "import { TID } from '@atproto/common'\nimport { AppContext } from '../../context'\nimport { Event as AgeAssuranceEvent } from '../../lexicon/types/app/bsky/ageassurance/defs'\nimport { Namespaces } from '../../stash'\n\nexport async function createEvent(\n  ctx: AppContext,\n  actorDid: string,\n  event: Omit<AgeAssuranceEvent, 'createdAt'>,\n) {\n  const payload: AgeAssuranceEvent = {\n    createdAt: new Date().toISOString(),\n    ...event,\n  }\n  await ctx.stashClient.create({\n    actorDid: actorDid,\n    namespace: Namespaces.AppBskyAgeassuranceDefsEvent,\n    key: TID.nextStr(),\n    payload,\n  })\n  return payload\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/types.ts",
    "content": "import { KwsConfig, ServerConfig } from '../../config'\nimport { AppContext } from '../../context'\nimport { KwsClient } from '../../kws'\n\nexport type AppContextWithAA = AppContext & {\n  kwsClient: KwsClient\n  cfg: ServerConfig & {\n    kws: KwsConfig\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/util.ts",
    "content": "import {\n  type AppBskyAgeassuranceDefs,\n  computeAgeAssuranceRegionAccess,\n  getAgeAssuranceRegionConfig,\n} from '@atproto/api'\n\n/**\n * Compute age assurance access based on verified minimum age. Thrown errors\n * are internal errors, so handle them accordingly.\n */\nexport function computeAgeAssuranceAccessOrThrow(\n  config: AppBskyAgeassuranceDefs.Config,\n  {\n    countryCode,\n    regionCode,\n    verifiedMinimumAge,\n  }: {\n    countryCode: string\n    regionCode?: string\n    verifiedMinimumAge: number\n  },\n) {\n  const region = getAgeAssuranceRegionConfig(config, {\n    countryCode,\n    regionCode,\n  })\n\n  if (region) {\n    const result = computeAgeAssuranceRegionAccess(region, {\n      assuredAge: verifiedMinimumAge,\n      /*\n       * We don't care about this here, this is a client-only rule. If we have\n       * verified data, we can use that, and the account creation date is\n       * irrelevant.\n       */\n      accountCreatedAt: undefined,\n    })\n\n    if (result) {\n      return result\n    } else {\n      /*\n       * If we don't get a result, it's because none of the rules matched,\n       * which is a configuration error: there should always be a default\n       * rule.\n       */\n      throw new Error('Cound not compute age assurance region access')\n    }\n  } else {\n    /**\n     * If we had geolocation data, but we don't have a region config for this\n     * geolocation, then it means a user outside of our configured regions\n     * has completed age verification. In this case, we can't determine their\n     * access level, so we throw an error.\n     *\n     * This case is also guarded in `app.bsky.ageassurance.begin`.\n     */\n    throw new Error('Could not get config for region')\n  }\n}\n\nexport function createLocationString(countryCode: string, regionCode?: string) {\n  return regionCode\n    ? `${countryCode.toUpperCase()}-${regionCode.toUpperCase()}`\n    : countryCode.toUpperCase()\n}\n"
  },
  {
    "path": "packages/bsky/src/api/age-assurance/webhooks/kws-age-verified.ts",
    "content": "import express, { RequestHandler } from 'express'\nimport { ageAssuranceLogger as logger } from '../../../logger'\nimport { AGE_ASSURANCE_CONFIG } from '../const'\nimport {\n  type KWSWebhookAgeVerified,\n  parseKWSAgeVerifiedWebhook,\n} from '../kws/age-verified'\nimport { parseKWSExternalPayloadV2 } from '../kws/external-payload'\nimport { createEvent } from '../stash'\nimport { type AppContextWithAA } from '../types'\nimport { computeAgeAssuranceAccessOrThrow } from '../util'\n\nexport const handler =\n  (ctx: AppContextWithAA): RequestHandler =>\n  async (req: express.Request, res: express.Response) => {\n    let body: KWSWebhookAgeVerified\n    try {\n      body = parseKWSAgeVerifiedWebhook(req.body)\n    } catch (err) {\n      const message = 'Failed to parse KWS webhook body'\n      logger.error({ err }, message)\n      return res.status(400).json({ error: message })\n    }\n\n    const { status, externalPayload } = body.payload\n    const { verified, verifiedMinimumAge } = status\n    const { actorDid, countryCode, regionCode, attemptId } =\n      parseKWSExternalPayloadV2(externalPayload)\n\n    /*\n     * KWS does not send unverified webhooks for age verification, so we\n     * expect all webhooks to be verified. This is just a sanity check.\n     */\n    if (!verified) {\n      const message = 'Expected KWS webhook to have verified status'\n      logger.error({}, message)\n      return res.status(400).json({ error: message })\n    }\n\n    let result: ReturnType<typeof computeAgeAssuranceAccessOrThrow> | undefined\n    try {\n      result = computeAgeAssuranceAccessOrThrow(AGE_ASSURANCE_CONFIG, {\n        countryCode,\n        regionCode,\n        verifiedMinimumAge,\n      })\n    } catch (err) {\n      // internal errors\n      logger.error(\n        { err, attemptId, actorDid, countryCode, regionCode },\n        'Failed to compute age assurance access',\n      )\n    }\n\n    try {\n      if (result) {\n        await createEvent(ctx, actorDid, {\n          attemptId,\n          countryCode,\n          regionCode,\n          status: 'assured',\n          access: result.access,\n        })\n      }\n\n      return res.status(200).end()\n    } catch (err) {\n      const message = 'Failed to handle KWS webhook'\n      logger.error(\n        { err, attemptId, actorDid, countryCode, regionCode },\n        message,\n      )\n      return res.status(500).json({ error: message })\n    }\n  }\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/actor/getProfile.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getProfile'\nimport { createPipeline, noRules } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getProfile = createPipeline(skeleton, hydration, noRules, presentation)\n  server.app.bsky.actor.getProfile({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ auth, params, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n\n      const result = await getProfile({ ...params, hydrateCtx }, ctx)\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({\n          repoRev,\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: {\n  ctx: Context\n  params: Params\n}): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  const [did] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!did) {\n    throw new InvalidRequestError('Profile not found')\n  }\n  return { did }\n}\n\nconst hydration = async (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n}) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateProfilesDetailed(\n    [skeleton.did],\n    params.hydrateCtx.copy({\n      overrideIncludeTakedownsForActor: true,\n    }),\n  )\n}\n\nconst presentation = (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n  hydration: HydrationState\n}) => {\n  const { ctx, params, skeleton, hydration } = input\n  const profile = ctx.views.profileDetailed(skeleton.did, hydration)\n  if (!profile) {\n    throw new InvalidRequestError('Profile not found')\n  } else if (!params.hydrateCtx.includeTakedowns) {\n    if (ctx.views.actorIsTakendown(skeleton.did, hydration)) {\n      throw new InvalidRequestError(\n        'Account has been suspended',\n        'AccountTakedown',\n      )\n    } else if (ctx.views.actorIsDeactivated(skeleton.did, hydration)) {\n      throw new InvalidRequestError(\n        'Account is deactivated',\n        'AccountDeactivated',\n      )\n    }\n  }\n  return profile\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = { did: string }\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/actor/getProfiles.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getProfiles'\nimport { createPipeline, noRules } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getProfile = createPipeline(skeleton, hydration, noRules, presentation)\n  server.app.bsky.actor.getProfiles({\n    auth: ctx.authVerifier.standardOptionalParameterized({\n      lxmCheck: (method) => {\n        if (!method) return false\n        return (\n          method === ids.AppBskyActorGetProfiles ||\n          method.startsWith('chat.bsky.')\n        )\n      },\n    }),\n    handler: async ({ auth, params, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        viewer,\n        labelers,\n        includeTakedowns,\n      })\n\n      const result = await getProfile({ ...params, hydrateCtx }, ctx)\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({\n          repoRev,\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: {\n  ctx: Context\n  params: Params\n}): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  const dids = await ctx.hydrator.actor.getDidsDefined(params.actors)\n  return { dids }\n}\n\nconst hydration = async (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n}) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateProfilesDetailed(skeleton.dids, params.hydrateCtx)\n}\n\nconst presentation = (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = input\n  const profiles = mapDefined(skeleton.dids, (did) =>\n    ctx.views.profileDetailed(did, hydration),\n  )\n  return { profiles }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = { dids: string[] }\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/actor/getSuggestions.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { mapDefined, noUndefinedVals } from '@atproto/common'\nimport { HeadersMap } from '@atproto/xrpc'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getSuggestions'\nimport { createPipeline } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getSuggestions = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutes,\n    presentation,\n  )\n  server.app.bsky.actor.getSuggestions({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const { resHeaders: resultHeaders, ...result } = await getSuggestions(\n        { ...params, hydrateCtx, headers },\n        ctx,\n      )\n      const suggestionsResHeaders = noUndefinedVals({\n        'content-language': resultHeaders?.['content-language'],\n      })\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: {\n          ...suggestionsResHeaders,\n          ...resHeaders({ labelers: hydrateCtx.labelers }),\n        },\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = input\n  const viewer = params.hydrateCtx.viewer\n  if (ctx.suggestionsAgent) {\n    const res =\n      await ctx.suggestionsAgent.api.app.bsky.unspecced.getSuggestionsSkeleton(\n        {\n          viewer: viewer ?? undefined,\n          limit: params.limit,\n          cursor: params.cursor,\n        },\n        { headers: params.headers },\n      )\n    return {\n      dids: res.data.actors.map((a) => a.did),\n      cursor: res.data.cursor,\n      recId: res.data.recId,\n      recIdStr: res.data.recIdStr,\n      resHeaders: res.headers,\n    }\n  } else {\n    // @NOTE for appview swap moving to rkey-based cursors which are somewhat permissive, should not hard-break pagination\n    const suggestions = await ctx.dataplane.getFollowSuggestions({\n      actorDid: viewer ?? undefined,\n      cursor: params.cursor,\n      limit: params.limit,\n    })\n    let dids = suggestions.dids\n    if (viewer !== null) {\n      const follows = await ctx.dataplane.getActorFollowsActors({\n        actorDid: viewer,\n        targetDids: dids,\n      })\n      dids = dids.filter((did, i) => !follows.uris[i] && did !== viewer)\n    }\n    return { dids, cursor: parseString(suggestions.cursor) }\n  }\n}\n\nconst hydration = async (input: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateProfilesDetailed(skeleton.dids, params.hydrateCtx)\n}\n\nconst noBlocksOrMutes = (input: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = input\n  skeleton.dids = skeleton.dids.filter(\n    (did) =>\n      !ctx.views.viewerBlockExists(did, hydration) &&\n      !ctx.views.viewerMuteExists(did, hydration),\n  )\n  return skeleton\n}\n\nconst presentation = (input: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = input\n  const actors = mapDefined(skeleton.dids, (did) =>\n    ctx.views.profileKnownFollowers(did, hydration),\n  )\n  return {\n    actors,\n    cursor: skeleton.cursor,\n    recId: skeleton.recId,\n    recIdStr: skeleton.recIdStr,\n    resHeaders: skeleton.resHeaders,\n  }\n}\n\ntype Context = {\n  suggestionsAgent: AtpAgent | undefined\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n  headers: HeadersMap\n}\n\ntype Skeleton = {\n  dids: string[]\n  cursor?: string\n  recId?: number\n  recIdStr?: string\n  resHeaders?: HeadersMap\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/actor/searchActors.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/actor/searchActors'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const searchActors = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.actor.searchActors({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        viewer,\n        labelers,\n        includeTakedowns,\n      })\n      const results = await searchActors({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: results,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const term = params.q ?? params.term ?? ''\n\n  // @TODO\n  // add hits total\n\n  if (ctx.searchAgent) {\n    // @NOTE cursors won't change on appview swap\n    const { data: res } =\n      await ctx.searchAgent.app.bsky.unspecced.searchActorsSkeleton({\n        q: term,\n        cursor: params.cursor,\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n      })\n    return {\n      dids: res.actors.map(({ did }) => did),\n      cursor: parseString(res.cursor),\n    }\n  }\n\n  const res = await ctx.dataplane.searchActors({\n    term,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    dids: res.dids,\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateProfiles(skeleton.dids, params.hydrateCtx)\n}\n\nconst noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.dids = skeleton.dids.filter(\n    (did) => !ctx.views.viewerBlockExists(did, hydration),\n  )\n  return skeleton\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, skeleton, hydration } = inputs\n  const actors = mapDefined(skeleton.dids, (did) =>\n    ctx.views.profile(did, hydration),\n  )\n  return {\n    actors,\n    cursor: skeleton.cursor,\n  }\n}\n\ntype Context = {\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  searchAgent?: AtpAgent\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  dids: string[]\n  hitsTotal?: number\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/actor/searchActorsTypeahead'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const searchActorsTypeahead = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.actor.searchActorsTypeahead({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const results = await searchActorsTypeahead(\n        { ...params, hydrateCtx },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: results,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const term = params.q ?? params.term ?? ''\n\n  // @TODO\n  // add typeahead option\n  // add hits total\n\n  if (ctx.searchAgent) {\n    const { data: res } =\n      await ctx.searchAgent.app.bsky.unspecced.searchActorsSkeleton({\n        typeahead: true,\n        q: term,\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n      })\n    return {\n      dids: res.actors.map(({ did }) => did),\n      cursor: parseString(res.cursor),\n    }\n  }\n\n  const res = await ctx.dataplane.searchActors({\n    term,\n    limit: params.limit,\n  })\n  return {\n    dids: res.dids,\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateProfilesBasic(skeleton.dids, params.hydrateCtx)\n}\n\nconst noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => {\n  const { ctx, skeleton, hydration, params } = inputs\n  skeleton.dids = skeleton.dids.filter((did) => {\n    const actor = hydration.actors?.get(did)\n    if (!actor) return false\n    // Always display exact matches so that users can find profiles that they have blocked\n    const term = (params.q ?? params.term ?? '').toLowerCase()\n    const isExactMatch = actor.handle?.toLowerCase() === term\n    return isExactMatch || !ctx.views.viewerBlockExists(did, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, skeleton, hydration } = inputs\n  const actors = mapDefined(skeleton.dids, (did) =>\n    ctx.views.profileBasic(did, hydration),\n  )\n  return {\n    actors,\n  }\n}\n\ntype Context = {\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  searchAgent?: AtpAgent\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  dids: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/ageassurance/begin.ts",
    "content": "import crypto from 'node:crypto'\nimport { isEmailValid } from '@hapi/address'\nimport { isDisposableEmail } from 'disposable-email-domains-js'\nimport { getAgeAssuranceRegionConfig } from '@atproto/api'\nimport {\n  InvalidRequestError,\n  MethodNotImplementedError,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { InputSchema } from '../../../../lexicon/types/app/bsky/ageassurance/begin'\nimport { httpLogger as log } from '../../../../logger'\nimport { ActorInfo } from '../../../../proto/bsky_pb'\nimport { AGE_ASSURANCE_CONFIG } from '../../../age-assurance/const'\nimport {\n  KWS_SUPPORTED_LANGUAGES,\n  KWS_V2_COUNTRIES,\n} from '../../../age-assurance/kws/const'\nimport {\n  KWSExternalPayloadTooLargeError,\n  KWSExternalPayloadVersion,\n  serializeKWSExternalPayloadV2,\n} from '../../../age-assurance/kws/external-payload'\nimport { createEvent } from '../../../age-assurance/stash'\nimport { createLocationString } from '../../../age-assurance/util'\nimport { getClientUa } from '../../../kws/util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.ageassurance.begin({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input, req }) => {\n      if (!ctx.kwsClient) {\n        throw new MethodNotImplementedError(\n          'This service is not configured to support age assurance.',\n        )\n      }\n\n      const actorDid = auth.credentials.iss\n      const actorInfo = await getAgeVerificationState(ctx, actorDid)\n      const existingStatus = actorInfo?.ageAssuranceStatus?.status\n      const existingAccess = actorInfo?.ageAssuranceStatus?.access\n\n      if (existingStatus === 'blocked') {\n        throw new InvalidRequestError(\n          `Cannot initiate age assurance flow from current state: ${existingStatus}`,\n          'InvalidInitiation',\n        )\n      }\n\n      const attemptId = crypto.randomUUID()\n      const { email, language, countryCode, regionCode } = validateInput(\n        input.body,\n      )\n\n      let externalPayload: string\n      try {\n        externalPayload = serializeKWSExternalPayloadV2({\n          version: KWSExternalPayloadVersion.V2,\n          actorDid,\n          attemptId,\n          countryCode,\n          regionCode,\n        })\n      } catch (err) {\n        if (err instanceof KWSExternalPayloadTooLargeError) {\n          log.error({ err, actorDid }, err.message)\n          throw new InvalidRequestError(\n            'Age Assurance flow failed because DID is too long',\n            'DidTooLong',\n          )\n        }\n        throw err\n      }\n\n      /*\n       * Determine if age assurance config exists for this region. The calling\n       * application should already have checked for this, so this is just a\n       * safeguard.\n       */\n      const region = getAgeAssuranceRegionConfig(AGE_ASSURANCE_CONFIG, {\n        countryCode,\n        regionCode,\n      })\n      if (!region) {\n        const message = 'Age Assurance is not required in this region'\n        log.error({ actorDid, countryCode, regionCode }, message)\n        throw new InvalidRequestError(message, 'RegionNotSupported')\n      }\n\n      const location = createLocationString(countryCode, regionCode)\n\n      if (KWS_V2_COUNTRIES.has(region.countryCode)) {\n        // `age-verified` flow\n        await ctx.kwsClient.sendAgeVerifiedFlowEmail({\n          location,\n          email,\n          externalPayload,\n          language,\n        })\n      } else {\n        // `adult-verified` flow is what we've been using prior to `age-verified`\n        await ctx.kwsClient.sendAdultVerifiedFlowEmail({\n          location,\n          email,\n          externalPayload,\n          language,\n        })\n      }\n\n      // If we have existing status/access for this region, retain it.\n      const nextStatus =\n        existingStatus && existingStatus !== 'unknown'\n          ? existingStatus\n          : 'pending'\n      const nextAccess =\n        existingAccess && existingAccess !== 'unknown'\n          ? existingAccess\n          : 'unknown'\n\n      const event = await createEvent(ctx, actorDid, {\n        attemptId,\n        email,\n        // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.\n        initIp: req.ip,\n        initUa: getClientUa(req),\n        status: nextStatus,\n        access: nextAccess,\n        countryCode,\n        regionCode,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          lastInitiatedAt: event.createdAt,\n          status: nextStatus,\n          access: nextAccess,\n        },\n      }\n    },\n  })\n}\n\nfunction validateInput({ email, language, ...rest }: InputSchema): InputSchema {\n  if (!isEmailValid(email) || isDisposableEmail(email)) {\n    throw new InvalidRequestError(\n      'This email address is not supported, please use a different email.',\n      'InvalidEmail',\n    )\n  }\n\n  return {\n    email,\n    language: KWS_SUPPORTED_LANGUAGES.has(language) ? language : 'en',\n    ...rest,\n  }\n}\n\nasync function getAgeVerificationState(\n  ctx: AppContext,\n  actorDid: string,\n): Promise<ActorInfo | undefined> {\n  try {\n    const res = await ctx.dataplane.getActors({\n      dids: [actorDid],\n      returnAgeAssuranceForDids: [actorDid],\n      skipCacheForDids: [actorDid],\n    })\n\n    return res.actors[0]\n  } catch (err) {\n    return undefined\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/ageassurance/getConfig.ts",
    "content": "import { AGE_ASSURANCE_CONFIG } from '../../../../api/age-assurance/const'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.ageassurance.getConfig({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async () => {\n      return {\n        encoding: 'application/json',\n        body: AGE_ASSURANCE_CONFIG,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/ageassurance/getState.ts",
    "content": "import { UpstreamFailureError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ActorInfo } from '../../../../proto/bsky_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.ageassurance.getState({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth }) => {\n      const viewer = auth.credentials.iss\n      const actor = await getActorInfo(ctx, viewer)\n\n      return {\n        encoding: 'application/json',\n        body: {\n          state: {\n            lastInitiatedAt:\n              actor.ageAssuranceStatus?.lastInitiatedAt\n                ?.toDate()\n                .toISOString() || undefined,\n            status: actor.ageAssuranceStatus?.status || 'unknown',\n            access: actor.ageAssuranceStatus?.access || 'unknown',\n          },\n          metadata: {\n            accountCreatedAt:\n              actor.createdAt?.toDate().toISOString() || undefined,\n          },\n        },\n      }\n    },\n  })\n}\n\nconst getActorInfo = async (\n  ctx: AppContext,\n  actorDid: string,\n): Promise<ActorInfo> => {\n  try {\n    const res = await ctx.dataplane.getActors({\n      dids: [actorDid],\n      returnAgeAssuranceForDids: [actorDid],\n      skipCacheForDids: [actorDid],\n    })\n\n    return res.actors[0]\n  } catch (err) {\n    throw new UpstreamFailureError(\n      'Cannot get current age assurance state',\n      'GetAgeAssuranceStateFailed',\n      { cause: err },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/bookmark/createBookmark.ts",
    "content": "import { TID } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { Bookmark } from '../../../../lexicon/types/app/bsky/bookmark/defs'\nimport { Namespaces } from '../../../../stash'\nimport { validateUri } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.bookmark.createBookmark({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const actorDid = auth.credentials.iss\n      const { cid, uri } = input.body\n      validateUri(uri)\n\n      const res = await ctx.dataplane.getBookmarksByActorAndSubjects({\n        actorDid,\n        uris: [uri],\n      })\n      const [existing] = res.bookmarks\n      if (existing.ref?.key) {\n        // Idempotent, return without creating.\n        return\n      }\n\n      await ctx.stashClient.create({\n        actorDid,\n        namespace: Namespaces.AppBskyBookmarkDefsBookmark,\n        payload: {\n          subject: {\n            cid,\n            uri,\n          },\n        } satisfies Bookmark,\n        key: TID.nextStr(),\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/bookmark/deleteBookmark.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { Namespaces } from '../../../../stash'\nimport { validateUri } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.bookmark.deleteBookmark({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const actorDid = auth.credentials.iss\n      const { uri } = input.body\n      validateUri(uri)\n\n      const res = await ctx.dataplane.getBookmarksByActorAndSubjects({\n        actorDid,\n        uris: [uri],\n      })\n      const [existing] = res.bookmarks\n      if (!existing.ref?.key) {\n        // Idempotent, return without deleting.\n        return\n      }\n\n      await ctx.stashClient.delete({\n        actorDid,\n        namespace: Namespaces.AppBskyBookmarkDefsBookmark,\n        key: existing.ref.key,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/bookmark/getBookmarks.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/bookmark/getBookmarks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { BookmarkInfo } from '../../../../proto/bsky_pb'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getBookmarks = createPipeline(\n    skeleton,\n    hydration,\n    noRules, // Blocks are included and handled on views. Mutes are included.\n    presentation,\n  )\n  server.app.bsky.bookmark.getBookmarks({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n\n      const result = await getBookmarks(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { params, ctx } = input\n  const actorDid = params.hydrateCtx.viewer\n  const { bookmarks, cursor } = await ctx.hydrator.dataplane.getActorBookmarks({\n    actorDid: params.hydrateCtx.viewer,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    actorDid,\n    bookmarkInfos: bookmarks,\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { bookmarkInfos } = skeleton\n  return ctx.hydrator.hydrateBookmarks(bookmarkInfos, params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton } = input\n  const { bookmarkInfos, cursor } = skeleton\n  const bookmarkViews = mapDefined(bookmarkInfos, (bookmarkInfo) =>\n    ctx.views.bookmark(bookmarkInfo.key, hydration),\n  )\n  return { bookmarks: bookmarkViews, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  actorDid: string\n  bookmarkInfos: BookmarkInfo[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/bookmark/util.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport const validateUri = (uri: string) => {\n  const atUri = new AtUri(uri)\n  if (atUri.collection !== ids.AppBskyFeedPost) {\n    throw new InvalidRequestError(\n      `Only '${ids.AppBskyFeedPost}' records can be bookmarked`,\n      'UnsupportedCollection',\n    )\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/dismissMatch.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.contact.dismissMatch({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const actor = auth.credentials.iss\n      await callRolodexClient(\n        ctx.rolodexClient.dismissMatch({\n          actor,\n          subject: input.body.subject,\n        }),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {},\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/getMatches.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/contact/getMatches'\nimport {\n  HydrationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { RolodexClient } from '../../../../rolodex'\nimport { Views } from '../../../../views'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getMatches = createPipeline(skeleton, hydration, noBlocks, presentation)\n  server.app.bsky.contact.getMatches({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n\n      const result = await getMatches(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { params, ctx } = input\n  const actor = params.hydrateCtx.viewer\n  const { cursor, subjects } = await callRolodexClient(\n    ctx.rolodexClient.getMatches({\n      actor: params.hydrateCtx.viewer,\n      limit: params.limit,\n      cursor: params.cursor,\n    }),\n  )\n  return {\n    actor,\n    subjects,\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { subjects } = skeleton\n  return ctx.hydrator.hydrateProfiles(subjects, params.hydrateCtx)\n}\n\nconst noBlocks = (inputs: {\n  ctx: Context\n  skeleton: SkeletonState\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.subjects = skeleton.subjects.filter((subject) => {\n    return !ctx.views.viewerBlockExists(subject, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = input\n  const matches = mapDefined(skeleton.subjects, (did) =>\n    ctx.views.profile(did, hydration),\n  )\n  return { matches }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  rolodexClient: RolodexClient\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  actor: string\n  subjects: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/getSyncStatus.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { SyncStatus } from '../../../../lexicon/types/app/bsky/contact/defs'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.contact.getSyncStatus({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const actor = auth.credentials.iss\n      const res = await callRolodexClient(\n        ctx.rolodexClient.getSyncStatus({\n          actor,\n        }),\n      )\n\n      let syncStatus: SyncStatus | undefined\n      if (res.status && res.status.syncedAt) {\n        const syncedAt = res.status?.syncedAt?.toDate().toISOString()\n        syncStatus = {\n          matchesCount: res.status.matchesCount,\n          syncedAt,\n        }\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          syncStatus,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/importContacts.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { MatchAndContactIndex } from '../../../../lexicon/types/app/bsky/contact/defs'\nimport { InputSchema } from '../../../../lexicon/types/app/bsky/contact/importContacts'\nimport {\n  HydrationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { ImportContactsMatch } from '../../../../proto/rolodex_pb'\nimport { RolodexClient } from '../../../../rolodex'\nimport { Views } from '../../../../views'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const importContacts = createPipeline(\n    skeleton,\n    hydration,\n    noRules, //\n    presentation,\n  )\n  server.app.bsky.contact.importContacts({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth, req }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n\n      const result = await importContacts(\n        { ...input.body, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { params, ctx } = input\n  const actor = params.hydrateCtx.viewer\n  const { matches } = await callRolodexClient(\n    ctx.rolodexClient.importContacts({\n      actor: params.hydrateCtx.viewer,\n      contacts: params.contacts,\n      token: params.token,\n    }),\n  )\n  return {\n    actor,\n    matches,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { matches } = skeleton\n  const subjects = matches.map((m) => m.subject)\n  return ctx.hydrator.hydrateProfiles(subjects, params.hydrateCtx)\n}\n\nconst presentation = (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = input\n  const matchesAndContactIndexes = mapDefined(\n    skeleton.matches,\n    ({ subject, inputIndex }): MatchAndContactIndex | undefined => {\n      const profile = ctx.views.profile(subject, hydration)\n\n      if (!profile) {\n        return undefined\n      }\n\n      return {\n        contactIndex: inputIndex,\n        match: profile,\n      }\n    },\n  )\n  return { matchesAndContactIndexes }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  rolodexClient: RolodexClient\n  views: Views\n}\n\ntype Params = InputSchema & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  actor: string\n  matches: ImportContactsMatch[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/removeData.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.contact.removeData({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const actor = auth.credentials.iss\n      await callRolodexClient(\n        ctx.rolodexClient.removeData({\n          actor,\n        }),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {},\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/sendNotification.ts",
    "content": "import { TID } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { Notification } from '../../../../lexicon/types/app/bsky/contact/defs'\nimport { Namespaces } from '../../../../stash'\nimport { assertRolodexOrThrowUnimplemented } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.contact.sendNotification({\n    auth: ctx.authVerifier.role,\n    handler: async ({ input }) => {\n      // Assert rolodex even though we don't call it, it is a proxy to whether the app is configured with contact import support.\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const { from, to } = input.body\n\n      await ctx.stashClient.create({\n        actorDid: from,\n        namespace: Namespaces.AppBskyContactDefsNotification,\n        payload: {\n          from,\n          to,\n        } satisfies Notification,\n        key: TID.nextStr(),\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {},\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/startPhoneVerification.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.contact.startPhoneVerification({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const actor = auth.credentials.iss\n      await callRolodexClient(\n        ctx.rolodexClient.startPhoneVerification({\n          actor,\n          phone: input.body.phone,\n        }),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {},\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/util.ts",
    "content": "import { ConnectError } from '@connectrpc/connect'\nimport {\n  InternalServerError,\n  InvalidRequestError,\n  MethodNotImplementedError,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../../../..'\nimport { RolodexClient } from '../../../../rolodex'\n\nexport function assertRolodexOrThrowUnimplemented(\n  ctx: AppContext,\n): asserts ctx is AppContext & { rolodexClient: RolodexClient } {\n  if (!ctx.rolodexClient) {\n    throw new MethodNotImplementedError(\n      'This service is not configured to support contact imports.',\n    )\n  }\n}\n\n/**\n * Converts UPPERCASE_ERROR from Rolodex to PascalCase for XRPC.\n */\nfunction convertErrorName(reason: string): string {\n  switch (reason) {\n    case 'INVALID_DID':\n      return 'InvalidDid'\n    case 'INVALID_LIMIT':\n      return 'InvalidLimit'\n    case 'INVALID_CURSOR':\n      return 'InvalidCursor'\n    case 'INVALID_CONTACTS':\n      return 'InvalidContacts'\n    case 'TOO_MANY_CONTACTS':\n      return 'TooManyContacts'\n    case 'INVALID_TOKEN':\n      return 'InvalidToken'\n    case 'RATE_LIMIT_EXCEEDED':\n      return 'RateLimitExceeded'\n    case 'INVALID_PHONE':\n      return 'InvalidPhone'\n    case 'INVALID_CODE':\n      return 'InvalidCode'\n    case 'INTERNAL_ERROR':\n      return 'InternalError'\n    default:\n      return reason\n  }\n}\n\n/**\n * Helper to call Rolodex client methods and translate RPC errors to XRPC\n * errors.\n *\n * These `reason` values need to stay in sync with the Rolodex service\n */\nexport async function callRolodexClient<T>(caller: T) {\n  try {\n    return await caller\n  } catch (e) {\n    // might be something we want to handle\n    if (e instanceof ConnectError) {\n      /**\n       * https://connectrpc.com/docs/protocol#error-end-stream\n       */\n      const details = e.details?.at(0) as\n        | {\n            debug: {\n              reason: string\n              message: string\n            }\n          }\n        | undefined\n      const reason = details?.debug?.reason // e.g. INVALID_DID\n      // Handle known error reasons\n      if (reason) {\n        const errorName = convertErrorName(reason)\n        // NOTE: Don't leak e.message to the response.\n\n        if (reason === 'INTERNAL_ERROR') {\n          throw new InternalServerError('Upstream error', errorName, {\n            cause: e,\n          })\n        } else {\n          throw new InvalidRequestError('An error occurred', errorName, {\n            cause: e,\n          })\n        }\n      }\n    }\n    throw e\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/contact/verifyPhone.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.contact.verifyPhone({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      assertRolodexOrThrowUnimplemented(ctx)\n\n      const actor = auth.credentials.iss\n      const res = await callRolodexClient(\n        ctx.rolodexClient.verifyPhone({\n          actor,\n          verificationCode: input.body.code,\n          phone: input.body.phone,\n        }),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          token: res.token,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/draft/createDraft.ts",
    "content": "import { TID } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { DraftWithId } from '../../../../lexicon/types/app/bsky/draft/defs'\nimport { Namespaces } from '../../../../stash'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.draft.createDraft({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const actorDid = auth.credentials.iss\n      const { draft } = input.body\n\n      const res = await ctx.dataplane.getCountsForUsers({\n        dids: [actorDid],\n      })\n\n      const draftsCount = res.drafts[0]\n      if (draftsCount >= ctx.cfg.draftsLimit) {\n        throw new InvalidRequestError(\n          `Drafts limit reached`,\n          'DraftLimitReached',\n        )\n      }\n\n      const draftId = TID.nextStr()\n      const draftWithId: DraftWithId = {\n        id: draftId,\n        draft,\n      }\n\n      await ctx.stashClient.create({\n        actorDid,\n        namespace: Namespaces.AppBskyDraftDefsDraftWithId,\n        payload: draftWithId,\n        key: draftId,\n      })\n\n      return {\n        encoding: 'application/json' as const,\n        body: { id: draftId },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/draft/deleteDraft.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { Namespaces } from '../../../../stash'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.draft.deleteDraft({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const actorDid = auth.credentials.iss\n      const { id } = input.body\n\n      await ctx.stashClient.delete({\n        actorDid,\n        namespace: Namespaces.AppBskyDraftDefsDraftWithId,\n        key: id,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/draft/getDrafts.ts",
    "content": "import { jsonStringToLex } from '@atproto/lexicon'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport {\n  DraftView,\n  DraftWithId,\n} from '../../../../lexicon/types/app/bsky/draft/defs'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.draft.getDrafts({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth }) => {\n      const viewer = auth.credentials.iss\n\n      const { cursor, drafts } = await ctx.hydrator.dataplane.getActorDrafts({\n        actorDid: viewer,\n        limit: params.limit,\n        cursor: params.cursor,\n      })\n\n      const draftViews = drafts.map((d): DraftView => {\n        const draftWithId = jsonStringToLex(\n          Buffer.from(d.payload).toString('utf8'),\n        ) as DraftWithId\n        return {\n          id: draftWithId.id,\n          draft: draftWithId.draft,\n          // The date should always be present, but we avoid required fields on protobuf by convention,\n          // so requires a fallback value to please TS.\n          createdAt:\n            d.createdAt?.toDate().toISOString() ?? new Date(0).toISOString(),\n          updatedAt:\n            d.updatedAt?.toDate().toISOString() ?? new Date(0).toISOString(),\n        }\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor,\n          drafts: draftViews,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/draft/updateDraft.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { DraftWithId } from '../../../../lexicon/types/app/bsky/draft/defs'\nimport { Namespaces } from '../../../../stash'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.draft.updateDraft({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const actorDid = auth.credentials.iss\n      const { draft: draftWithId } = input.body\n\n      // NOTE: update drafts does not enforce limits, because if it did, we would not allow updating when the limit is reached,\n      // which is not the desired behavior.\n      // But this means the consumer of the stash operations can't do an upsert behavior on update, and needs instead to drop non-existent\n      // drafts. This avoid misusing the update as a create that does not check limits.\n\n      await ctx.stashClient.update({\n        actorDid,\n        namespace: Namespaces.AppBskyDraftDefsDraftWithId,\n        payload: draftWithId satisfies DraftWithId,\n        key: draftWithId.id,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getActorFeeds'\nimport { createPipeline, noRules } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getActorFeeds = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.feed.getActorFeeds({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const result = await getActorFeeds({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  if (clearlyBadCursor(params.cursor)) {\n    return { feedUris: [] }\n  }\n  const [did] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!did) {\n    throw new InvalidRequestError('Profile not found')\n  }\n  const feedsRes = await ctx.dataplane.getActorFeeds({\n    actorDid: did,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    feedUris: feedsRes.uris,\n    cursor: parseString(feedsRes.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  return await ctx.hydrator.hydrateFeedGens(\n    skeleton.feedUris,\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feeds = mapDefined(skeleton.feedUris, (uri) =>\n    ctx.views.feedGenerator(uri, hydration),\n  )\n  return {\n    feeds,\n    cursor: skeleton.cursor,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  dataplane: DataPlaneClient\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  feedUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getActorLikes.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { FeedItem } from '../../../../hydration/feed'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getActorLikes'\nimport { createPipeline } from '../../../../pipeline'\nimport { uriToDid as creatorFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getActorLikes = createPipeline(\n    skeleton,\n    hydration,\n    noPostBlocks,\n    presentation,\n  )\n  server.app.bsky.feed.getActorLikes({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n\n      const result = await getActorLikes({ ...params, hydrateCtx }, ctx)\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({\n          repoRev,\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  const { actor, limit, cursor } = params\n  const viewer = params.hydrateCtx.viewer\n  if (clearlyBadCursor(cursor)) {\n    return { items: [] }\n  }\n  const [actorDid] = await ctx.hydrator.actor.getDids([actor])\n  if (!actorDid || !viewer || viewer !== actorDid) {\n    throw new InvalidRequestError('Profile not found')\n  }\n\n  const likesRes = await ctx.dataplane.getActorLikes({\n    actorDid,\n    limit,\n    cursor,\n  })\n\n  const items = likesRes.likes.map((l) => ({ post: { uri: l.subject } }))\n\n  return {\n    items,\n    cursor: parseString(likesRes.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  return await ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx)\n}\n\nconst noPostBlocks = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.items = skeleton.items.filter((item) => {\n    const creator = creatorFromUri(item.post.uri)\n    return !ctx.views.viewerBlockExists(creator, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feed = mapDefined(skeleton.items, (item) =>\n    ctx.views.feedViewPost(item, hydration),\n  )\n  return {\n    feed,\n    cursor: skeleton.cursor,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  dataplane: DataPlaneClient\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  items: FeedItem[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { Actor } from '../../../../hydration/actor'\nimport { FeedItem, Post } from '../../../../hydration/feed'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n  mergeStates,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed'\nimport { createPipeline } from '../../../../pipeline'\nimport { FeedType } from '../../../../proto/bsky_pb'\nimport { safePinnedPost, uriToDid } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getAuthorFeed = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutedReposts,\n    presentation,\n  )\n  server.app.bsky.feed.getAuthorFeed({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n\n      const result = await getAuthorFeed({ ...params, hydrateCtx }, ctx)\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({\n          repoRev,\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst FILTER_TO_FEED_TYPE = {\n  posts_with_replies: undefined, // default: all posts, replies, and reposts\n  posts_no_replies: FeedType.POSTS_NO_REPLIES,\n  posts_with_media: FeedType.POSTS_WITH_MEDIA,\n  posts_and_author_threads: FeedType.POSTS_AND_AUTHOR_THREADS,\n  posts_with_video: FeedType.POSTS_WITH_VIDEO,\n}\n\nexport const skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  const [did] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!did) {\n    throw new InvalidRequestError('Profile not found')\n  }\n  const actors = await ctx.hydrator.actor.getActors([did], {\n    includeTakedowns: params.hydrateCtx.includeTakedowns,\n    skipCacheForDids: params.hydrateCtx.skipCacheForViewer,\n  })\n  const actor = actors.get(did)\n  if (!actor) {\n    throw new InvalidRequestError('Profile not found')\n  }\n  if (clearlyBadCursor(params.cursor)) {\n    return { actor, filter: params.filter, items: [] }\n  }\n\n  const pinnedPost = safePinnedPost(actor.profile?.pinnedPost)\n  const isFirstPageRequest = !params.cursor\n  const shouldInsertPinnedPost =\n    isFirstPageRequest &&\n    params.includePins &&\n    pinnedPost &&\n    uriToDid(pinnedPost.uri) === actor.did\n\n  const res = await ctx.dataplane.getAuthorFeed({\n    actorDid: did,\n    limit: params.limit,\n    cursor: params.cursor,\n    feedType: FILTER_TO_FEED_TYPE[params.filter],\n  })\n\n  let items: FeedItem[] = res.items.map((item) => ({\n    post: { uri: item.uri, cid: item.cid || undefined },\n    repost: item.repost\n      ? { uri: item.repost, cid: item.repostCid || undefined }\n      : undefined,\n  }))\n\n  if (shouldInsertPinnedPost && pinnedPost) {\n    const pinnedItem = {\n      post: {\n        uri: pinnedPost.uri,\n        cid: pinnedPost.cid,\n      },\n      authorPinned: true,\n    }\n\n    items = items.filter((item) => item.post.uri !== pinnedItem.post.uri)\n    items.unshift(pinnedItem)\n  }\n\n  return {\n    actor,\n    filter: params.filter,\n    items,\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}): Promise<HydrationState> => {\n  const { ctx, params, skeleton } = inputs\n  const [feedPostState, profileViewerState] = await Promise.all([\n    ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx),\n    ctx.hydrator.hydrateProfileViewers([skeleton.actor.did], params.hydrateCtx),\n  ])\n  return mergeStates(feedPostState, profileViewerState)\n}\n\nconst noBlocksOrMutedReposts = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}): Skeleton => {\n  const { ctx, skeleton, hydration } = inputs\n  const relationship = hydration.profileViewers?.get(skeleton.actor.did)\n  if (\n    relationship &&\n    (relationship.blocking || ctx.views.blockingByList(relationship, hydration))\n  ) {\n    throw new InvalidRequestError(\n      `Requester has blocked actor: ${skeleton.actor.did}`,\n      'BlockedActor',\n    )\n  }\n  if (\n    relationship &&\n    (relationship.blockedBy || ctx.views.blockedByList(relationship, hydration))\n  ) {\n    throw new InvalidRequestError(\n      `Requester is blocked by actor: ${skeleton.actor.did}`,\n      'BlockedByActor',\n    )\n  }\n\n  const checkBlocksAndMutes = (item: FeedItem) => {\n    const bam = ctx.views.feedItemBlocksAndMutes(item, hydration)\n    return (\n      !bam.authorBlocked &&\n      !bam.originatorBlocked &&\n      (!bam.authorMuted || bam.originatorMuted) // repost of muted content\n    )\n  }\n\n  if (skeleton.filter === 'posts_and_author_threads') {\n    // ensure replies are only included if the feed contains all\n    // replies up to the thread root (i.e. a complete self-thread.)\n    const selfThread = new SelfThreadTracker(skeleton.items, hydration)\n    skeleton.items = skeleton.items.filter((item) => {\n      return (\n        checkBlocksAndMutes(item) &&\n        (item.repost || item.authorPinned || selfThread.ok(item.post.uri))\n      )\n    })\n  } else {\n    skeleton.items = skeleton.items.filter(checkBlocksAndMutes)\n  }\n\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feed = mapDefined(skeleton.items, (item) =>\n    ctx.views.feedViewPost(item, hydration),\n  )\n  return { feed, cursor: skeleton.cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  dataplane: DataPlaneClient\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype Skeleton = {\n  actor: Actor\n  items: FeedItem[]\n  filter: QueryParams['filter']\n  cursor?: string\n}\n\nclass SelfThreadTracker {\n  feedUris = new Set<string>()\n  cache = new Map<string, boolean>()\n\n  constructor(\n    items: FeedItem[],\n    private hydration: HydrationState,\n  ) {\n    items.forEach((item) => {\n      if (!item.repost) {\n        this.feedUris.add(item.post.uri)\n      }\n    })\n  }\n\n  ok(uri: string, loop = new Set<string>()) {\n    // if we've already checked this uri, pull from the cache\n    if (this.cache.has(uri)) {\n      return this.cache.get(uri) ?? false\n    }\n    // loop detection\n    if (loop.has(uri)) {\n      this.cache.set(uri, false)\n      return false\n    } else {\n      loop.add(uri)\n    }\n    // cache through the result\n    const result = this._ok(uri, loop)\n    this.cache.set(uri, result)\n    return result\n  }\n\n  private _ok(uri: string, loop: Set<string>): boolean {\n    // must be in the feed to be in a self-thread\n    if (!this.feedUris.has(uri)) {\n      return false\n    }\n    // must be hydratable to be part of self-thread\n    const post = this.hydration.posts?.get(uri)\n    if (!post) {\n      return false\n    }\n    // root posts (no parent) are trivial case of self-thread\n    const parentUri = getParentUri(post)\n    if (parentUri === null) {\n      return true\n    }\n    // recurse w/ cache: this post is in a self-thread if its parent is.\n    return this.ok(parentUri, loop)\n  }\n}\n\nfunction getParentUri(post: Post) {\n  return post.record.reply?.parent.uri ?? null\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getFeed.ts",
    "content": "import { AppBskyFeedGetFeedSkeleton, AtpAgent } from '@atproto/api'\nimport { mapDefined, noUndefinedVals } from '@atproto/common'\nimport { ResponseType, XRPCError } from '@atproto/xrpc'\nimport {\n  InvalidRequestError,\n  ServerTimer,\n  UpstreamFailureError,\n  serverTimingHeader,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  Code,\n  getServiceEndpoint,\n  isDataplaneError,\n  unpackIdentityServices,\n} from '../../../../data-plane'\nimport { FeedItem } from '../../../../hydration/feed'\nimport { HydrateCtx } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { isSkeletonReasonRepost } from '../../../../lexicon/types/app/bsky/feed/defs'\nimport { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky/feed/getFeed'\nimport { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bsky/feed/getFeedSkeleton'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { GetIdentityByDidResponse } from '../../../../proto/bsky_pb'\nimport { BSKY_USER_AGENT, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getFeed = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutes,\n    presentation,\n  )\n  server.app.bsky.feed.getFeed({\n    auth: ctx.authVerifier.standardOptionalParameterized({\n      lxmCheck: (method) => {\n        return (\n          method === ids.AppBskyFeedGetFeedSkeleton ||\n          method === ids.AppBskyFeedGetFeed\n        )\n      },\n      skipAudCheck: true,\n    }),\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const headers = noUndefinedVals({\n        'user-agent': BSKY_USER_AGENT,\n        authorization: req.headers['authorization'],\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      // @NOTE feed cursors should not be affected by appview swap\n      const {\n        timerSkele,\n        timerHydr,\n        resHeaders: feedResHeaders,\n        ...result\n      } = await getFeed({ ...params, hydrateCtx, headers }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: {\n          ...(feedResHeaders ?? {}),\n          ...resHeaders({ labelers: hydrateCtx.labelers }),\n          'server-timing': serverTimingHeader([timerSkele, timerHydr]),\n        },\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  inputs: SkeletonFnInput<Context, Params>,\n): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  const timerSkele = new ServerTimer('skele').start()\n  const {\n    feedItems: algoItems,\n    reqId,\n    cursor,\n    resHeaders,\n    ...passthrough\n  } = await skeletonFromFeedGen(ctx, params)\n\n  return {\n    cursor,\n    items: algoItems,\n    reqId,\n    timerSkele: timerSkele.stop(),\n    timerHydr: new ServerTimer('hydr').start(),\n    resHeaders,\n    passthrough,\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  const timerHydr = new ServerTimer('hydr').start()\n  const hydration = await ctx.hydrator.hydrateFeedItems(\n    skeleton.items,\n    params.hydrateCtx,\n  )\n  skeleton.timerHydr = timerHydr.stop()\n  return hydration\n}\n\nconst noBlocksOrMutes = (inputs: RulesFnInput<Context, Params, Skeleton>) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.items = skeleton.items.filter((item) => {\n    const bam = ctx.views.feedItemBlocksAndMutes(item, hydration)\n    return (\n      !bam.authorBlocked &&\n      !bam.authorMuted &&\n      !bam.originatorBlocked &&\n      !bam.originatorMuted &&\n      !bam.ancestorAuthorBlocked\n    )\n  })\n\n  return skeleton\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feed = mapDefined(skeleton.items, (item) => {\n    const post = ctx.views.feedViewPost(item, hydration)\n    if (!post) return\n    return {\n      ...post,\n      feedContext: item.feedContext,\n    }\n  })\n  return {\n    feed: feed.map((fi) => ({ ...fi, reqId: skeleton.reqId })),\n    cursor: skeleton.cursor,\n    timerSkele: skeleton.timerSkele,\n    timerHydr: skeleton.timerHydr,\n    resHeaders: skeleton.resHeaders,\n    ...skeleton.passthrough,\n  }\n}\n\ntype Context = AppContext\n\ntype Params = GetFeedParams & {\n  hydrateCtx: HydrateCtx\n  headers: Record<string, string>\n}\n\ntype Skeleton = {\n  items: AlgoResponseItem[]\n  reqId?: string\n  passthrough: Record<string, unknown> // pass through additional items in feedgen response\n  resHeaders?: Record<string, string>\n  cursor?: string\n  timerSkele: ServerTimer\n  timerHydr: ServerTimer\n}\n\nconst skeletonFromFeedGen = async (\n  ctx: Context,\n  params: Params,\n): Promise<AlgoResponse> => {\n  const { feed, headers } = params\n  const found = await ctx.hydrator.feed.getFeedGens([feed], true)\n  const feedDid = found.get(feed)?.record.did\n  if (!feedDid) {\n    throw new InvalidRequestError('could not find feed')\n  }\n\n  let identity: GetIdentityByDidResponse\n  try {\n    identity = await ctx.dataplane.getIdentityByDid({ did: feedDid })\n  } catch (err) {\n    if (isDataplaneError(err, Code.NotFound)) {\n      throw new InvalidRequestError(`could not resolve identity: ${feedDid}`)\n    }\n    throw err\n  }\n\n  const services = unpackIdentityServices(identity.services)\n  const fgEndpoint = getServiceEndpoint(services, {\n    id: 'bsky_fg',\n    type: 'BskyFeedGenerator',\n  })\n  if (!fgEndpoint) {\n    throw new InvalidRequestError(\n      `invalid feed generator service details in did document: ${feedDid}`,\n    )\n  }\n\n  const agent = new AtpAgent({ service: fgEndpoint })\n\n  let skeleton: SkeletonOutput\n  let resHeaders: Record<string, string> | undefined = undefined\n  try {\n    // @TODO currently passthrough auth headers from pds\n    const result = await agent.api.app.bsky.feed.getFeedSkeleton(\n      {\n        feed: params.feed,\n        // The feedgen is not guaranteed to honor the limit, but we try it.\n        limit: params.limit,\n        cursor: params.cursor,\n      },\n      {\n        headers,\n      },\n    )\n\n    skeleton = result.data\n\n    if (result.data.cursor === params.cursor) {\n      // Prevents loops if the custom feed echoes the input cursor back.\n      skeleton.cursor = undefined\n    }\n\n    if (result.headers['content-language']) {\n      resHeaders = {\n        'content-language': result.headers['content-language'],\n      }\n    }\n  } catch (err) {\n    if (err instanceof AppBskyFeedGetFeedSkeleton.UnknownFeedError) {\n      throw new InvalidRequestError(err.message, 'UnknownFeed')\n    }\n    if (err instanceof XRPCError) {\n      if (err.status === ResponseType.Unknown) {\n        throw new UpstreamFailureError('feed unavailable')\n      }\n      if (err.status === ResponseType.InvalidResponse) {\n        throw new UpstreamFailureError(\n          'feed provided an invalid response',\n          'InvalidFeedResponse',\n        )\n      }\n    }\n    throw err\n  }\n\n  const { feed: feedSkele, ...skele } = skeleton\n  const feedItems = feedSkele.slice(0, params.limit).map((item) => ({\n    post: { uri: item.post },\n    repost: isSkeletonReasonRepost(item.reason)\n      ? { uri: item.reason.repost }\n      : undefined,\n    feedContext: item.feedContext,\n  }))\n\n  return { ...skele, resHeaders, feedItems }\n}\n\nexport type AlgoResponse = {\n  feedItems: AlgoResponseItem[]\n  resHeaders?: Record<string, string>\n  cursor?: string\n  reqId?: string\n}\n\nexport type AlgoResponseItem = FeedItem & {\n  feedContext?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  Code,\n  getServiceEndpoint,\n  isDataplaneError,\n  unpackIdentityServices,\n} from '../../../../data-plane'\nimport { Server } from '../../../../lexicon'\nimport { GetIdentityByDidResponse } from '../../../../proto/bsky_pb'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.feed.getFeedGenerator({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { feed } = params\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const hydration = await ctx.hydrator.hydrateFeedGens([feed], hydrateCtx)\n      const feedInfo = hydration.feedgens?.get(feed)\n      if (!feedInfo) {\n        throw new InvalidRequestError('could not find feed')\n      }\n\n      const feedDid = feedInfo.record.did\n      let identity: GetIdentityByDidResponse\n      try {\n        identity = await ctx.dataplane.getIdentityByDid({ did: feedDid })\n      } catch (err) {\n        if (isDataplaneError(err, Code.NotFound)) {\n          throw new InvalidRequestError(\n            `could not resolve identity: ${feedDid}`,\n          )\n        }\n        throw err\n      }\n\n      const services = unpackIdentityServices(identity.services)\n      const fgEndpoint = getServiceEndpoint(services, {\n        id: 'bsky_fg',\n        type: 'BskyFeedGenerator',\n      })\n      if (!fgEndpoint) {\n        throw new InvalidRequestError(\n          `invalid feed generator service details in did document: ${feedDid}`,\n        )\n      }\n\n      const feedView = ctx.views.feedGenerator(feed, hydration)\n      if (!feedView) {\n        throw new InvalidRequestError('could not find feed')\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          view: feedView,\n          // @TODO temporarily hard-coding to true while external feedgens catch-up on describeFeedGenerator\n          isOnline: true,\n          isValid: true,\n        },\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getFeedGenerators'\nimport { createPipeline, noRules } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getFeedGenerators = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.feed.getFeedGenerators({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const view = await getFeedGenerators({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: view,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: { params: Params }): Promise<Skeleton> => {\n  return {\n    feedUris: inputs.params.feeds,\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  return await ctx.hydrator.hydrateFeedGens(\n    skeleton.feedUris,\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feeds = mapDefined(skeleton.feedUris, (uri) =>\n    ctx.views.feedGenerator(uri, hydration),\n  )\n  return {\n    feeds,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  feedUris: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getLikes.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { normalizeDatetimeAlways } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getLikes'\nimport { RulesFnInput, createPipeline } from '../../../../pipeline'\nimport { uriToDid as creatorFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getLikes = createPipeline(skeleton, hydration, noBlocks, presentation)\n  server.app.bsky.feed.getLikes({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getLikes({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  const authorDid = creatorFromUri(params.uri)\n\n  if (clearlyBadCursor(params.cursor)) {\n    return { authorDid, likes: [] }\n  }\n  if (looksLikeNonSortedCursor(params.cursor)) {\n    throw new InvalidRequestError(\n      'Cursor appear to be out of date, please try reloading.',\n    )\n  }\n  const likesRes = await ctx.hydrator.dataplane.getLikesBySubjectSorted({\n    subject: { uri: params.uri, cid: params.cid },\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    authorDid,\n    likes: likesRes.uris,\n    cursor: parseString(likesRes.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  const likesState = await ctx.hydrator.hydrateLikes(\n    skeleton.authorDid,\n    skeleton.likes,\n    params.hydrateCtx,\n  )\n  return likesState\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, Skeleton>) => {\n  const { ctx, skeleton, hydration } = input\n\n  skeleton.likes = skeleton.likes.filter((likeUri) => {\n    const like = hydration.likes?.get(likeUri)\n    if (!like) return false\n    const likerDid = creatorFromUri(likeUri)\n    return (\n      !hydration.likeBlocks?.get(likeUri) &&\n      !ctx.views.viewerBlockExists(likerDid, hydration)\n    )\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, params, skeleton, hydration } = inputs\n  const likeViews = mapDefined(skeleton.likes, (uri) => {\n    const like = hydration.likes?.get(uri)\n    if (!like || !like.record) {\n      return\n    }\n    const creatorDid = creatorFromUri(uri)\n    const actor = ctx.views.profile(creatorDid, hydration)\n    if (!actor) {\n      return\n    }\n    return {\n      actor,\n      createdAt: normalizeDatetimeAlways(like.record.createdAt),\n      indexedAt: like.sortedAt.toISOString(),\n    }\n  })\n  return {\n    likes: likeViews,\n    cursor: skeleton.cursor,\n    uri: params.uri,\n    cid: params.cid,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  authorDid: string\n  likes: string[]\n  cursor?: string\n}\n\nconst looksLikeNonSortedCursor = (cursor: string | undefined) => {\n  // the old cursor values used with getLikesBySubject() were dids.\n  // we now use getLikesBySubjectSorted(), whose cursors look like timestamps.\n  return cursor?.startsWith('did:')\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getListFeed.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { FeedItem } from '../../../../hydration/feed'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n  mergeStates,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getListFeed'\nimport { createPipeline } from '../../../../pipeline'\nimport { uriToDid } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getListFeed = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutes,\n    presentation,\n  )\n  server.app.bsky.feed.getListFeed({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n\n      const result = await getListFeed({ ...params, hydrateCtx }, ctx)\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers, repoRev }),\n      }\n    },\n  })\n}\n\nexport const skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  if (clearlyBadCursor(params.cursor)) {\n    return { items: [] }\n  }\n  const res = await ctx.dataplane.getListFeed({\n    listUri: params.list,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    items: res.items.map((item) => ({\n      post: { uri: item.uri, cid: item.cid || undefined },\n      repost: item.repost\n        ? { uri: item.repost, cid: item.repostCid || undefined }\n        : undefined,\n    })),\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}): Promise<HydrationState> => {\n  const { ctx, params, skeleton } = inputs\n  const [feedItemsState, bidirectionalBlocks] = await Promise.all([\n    ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx),\n    getBlocks({ ctx, params, skeleton }),\n  ])\n  return mergeStates(feedItemsState, {\n    bidirectionalBlocks,\n  })\n}\n\nconst noBlocksOrMutes = (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}): Skeleton => {\n  const { ctx, params, skeleton, hydration } = inputs\n  skeleton.items = skeleton.items.filter((item) => {\n    const bam = ctx.views.feedItemBlocksAndMutes(item, hydration)\n    const creatorBlocks = hydration.bidirectionalBlocks?.get(\n      uriToDid(params.list),\n    )\n    return (\n      !bam.authorBlocked &&\n      !bam.authorMuted &&\n      !bam.originatorBlocked &&\n      !bam.originatorMuted &&\n      !bam.ancestorAuthorBlocked &&\n      !creatorBlocks?.get(uriToDid(item.post.uri))\n    )\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feed = mapDefined(skeleton.items, (item) =>\n    ctx.views.feedViewPost(item, hydration),\n  )\n  return { feed, cursor: skeleton.cursor }\n}\n\nconst getBlocks = async (input: {\n  ctx: Context\n  skeleton: Skeleton\n  params: Params\n}) => {\n  const { ctx, skeleton, params } = input\n  const pairs: Map<string, string[]> = new Map()\n  pairs.set(\n    uriToDid(params.list),\n    skeleton.items.map((item) => uriToDid(item.post.uri)),\n  )\n  return await ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx)\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  dataplane: DataPlaneClient\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  items: FeedItem[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getPostThread.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ServerConfig } from '../../../../config'\nimport { AppContext } from '../../../../context'\nimport { Code, DataPlaneClient, isDataplaneError } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { isNotFoundPost } from '../../../../lexicon/types/app/bsky/feed/defs'\nimport {\n  OutputSchema,\n  QueryParams,\n} from '../../../../lexicon/types/app/bsky/feed/getPostThread'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { postUriToThreadgateUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { ATPROTO_REPO_REV, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getPostThread = createPipeline(\n    skeleton,\n    hydration,\n    noRules, // handled in presentation: 3p block-violating replies are turned to #blockedPost, viewer blocks turned to #notFoundPost.\n    presentation,\n  )\n  server.app.bsky.feed.getPostThread({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req, res }) => {\n      const { viewer, includeTakedowns, include3pBlocks } =\n        ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n        include3pBlocks,\n      })\n\n      let result: OutputSchema\n      try {\n        result = await getPostThread({ ...params, hydrateCtx }, ctx)\n      } catch (err) {\n        const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n        if (repoRev) {\n          res.setHeader(ATPROTO_REPO_REV, repoRev)\n        }\n        throw err\n      }\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({\n          repoRev,\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const anchor = await ctx.hydrator.resolveUri(params.uri)\n  try {\n    const res = await ctx.dataplane.getThread({\n      postUri: anchor,\n      above: params.parentHeight,\n      below: getDepth(ctx, anchor, params),\n    })\n    return {\n      anchor,\n      uris: res.uris,\n    }\n  } catch (err) {\n    if (isDataplaneError(err, Code.NotFound)) {\n      return {\n        anchor,\n        uris: [],\n      }\n    } else {\n      throw err\n    }\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateThreadPosts(\n    skeleton.uris.map((uri) => ({ uri })),\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton, hydration } = inputs\n  const thread = ctx.views.thread(skeleton, hydration, {\n    height: params.parentHeight,\n    depth: getDepth(ctx, skeleton.anchor, params),\n  })\n  if (isNotFoundPost(thread)) {\n    // @TODO technically this could be returned as a NotFoundPost based on lexicon\n    throw new InvalidRequestError(\n      `Post not found: ${skeleton.anchor}`,\n      'NotFound',\n    )\n  }\n  const rootUri =\n    hydration.posts?.get(skeleton.anchor)?.record.reply?.root.uri ??\n    skeleton.anchor\n  const threadgate = ctx.views.threadgate(\n    postUriToThreadgateUri(rootUri),\n    hydration,\n  )\n  return { thread, threadgate }\n}\n\ntype Context = {\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  cfg: ServerConfig\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  anchor: string\n  uris: string[]\n}\n\nconst getDepth = (ctx: Context, anchor: string, params: Params) => {\n  let maxDepth = ctx.cfg.maxThreadDepth\n  if (ctx.cfg.bigThreadUris.has(anchor) && ctx.cfg.bigThreadDepth) {\n    maxDepth = ctx.cfg.bigThreadDepth\n  }\n  return maxDepth ? Math.min(maxDepth, params.depth) : params.depth\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getPosts.ts",
    "content": "import { dedupeStrs, mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPosts'\nimport { createPipeline } from '../../../../pipeline'\nimport { uriToDid as creatorFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation)\n  server.app.bsky.feed.getPosts({\n    auth: ctx.authVerifier.standardOptionalParameterized({\n      lxmCheck: (method) => {\n        if (!method) return false\n        return (\n          method === ids.AppBskyFeedGetPosts || method.startsWith('chat.bsky.')\n        )\n      },\n    }),\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n\n      const results = await getPosts({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: results,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: { params: Params }) => {\n  return { posts: dedupeStrs(inputs.params.uris) }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydratePosts(\n    skeleton.posts.map((uri) => ({ uri })),\n    params.hydrateCtx,\n  )\n}\n\nconst noBlocks = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.posts = skeleton.posts.filter((uri) => {\n    const creator = creatorFromUri(uri)\n    return !ctx.views.viewerBlockExists(creator, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const posts = mapDefined(skeleton.posts, (uri) =>\n    ctx.views.post(uri, hydration),\n  )\n  return { posts }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  posts: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getQuotes.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getQuotes'\nimport { createPipeline } from '../../../../pipeline'\nimport { uriToDid } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getQuotes = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrNeedsReview,\n    presentation,\n  )\n  server.app.bsky.feed.getQuotes({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getQuotes({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  if (clearlyBadCursor(params.cursor)) {\n    return { uris: [] }\n  }\n  const quotesRes = await ctx.hydrator.dataplane.getQuotesBySubjectSorted({\n    subject: { uri: params.uri, cid: params.cid },\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    uris: quotesRes.uris,\n    cursor: parseString(quotesRes.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  return await ctx.hydrator.hydratePosts(\n    skeleton.uris.map((uri) => ({ uri })),\n    params.hydrateCtx,\n  )\n}\n\nconst noBlocksOrNeedsReview = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.uris = skeleton.uris.filter((uri) => {\n    const authorDid = uriToDid(uri)\n    return (\n      !ctx.views.viewerBlockExists(authorDid, hydration) &&\n      !hydration.postBlocks?.get(uri)?.embed &&\n      ctx.views.viewerSeesNeedsReview({ did: authorDid, uri }, hydration)\n    )\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, params, skeleton, hydration } = inputs\n  const postViews = mapDefined(skeleton.uris, (uri) => {\n    return ctx.views.post(uri, hydration)\n  })\n  return {\n    posts: postViews,\n    cursor: skeleton.cursor,\n    uri: params.uri,\n    cid: params.cid,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  uris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getRepostedBy'\nimport { createPipeline } from '../../../../pipeline'\nimport { uriToDid as creatorFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getRepostedBy = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.feed.getRepostedBy({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getRepostedBy({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  if (clearlyBadCursor(params.cursor)) {\n    return { reposts: [] }\n  }\n  const res = await ctx.hydrator.dataplane.getRepostsBySubject({\n    subject: { uri: params.uri, cid: params.cid },\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    reposts: res.uris,\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}) => {\n  const { ctx, params, skeleton } = inputs\n  return await ctx.hydrator.hydrateReposts(skeleton.reposts, params.hydrateCtx)\n}\n\nconst noBlocks = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.reposts = skeleton.reposts.filter((uri) => {\n    const creator = creatorFromUri(uri)\n    return !ctx.views.viewerBlockExists(creator, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, params, skeleton, hydration } = inputs\n  const repostViews = mapDefined(skeleton.reposts, (uri) => {\n    const repost = hydration.reposts?.get(uri)\n    if (!repost?.record) {\n      return\n    }\n    const creatorDid = creatorFromUri(uri)\n    return ctx.views.profile(creatorDid, hydration)\n  })\n  return {\n    repostedBy: repostViews,\n    cursor: skeleton.cursor,\n    uri: params.uri,\n    cid: params.cid,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  reposts: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.feed.getSuggestedFeeds({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n\n      // @NOTE no need to coordinate the cursor for appview swap, as v1 doesn't use the cursor\n      const suggestedRes = await ctx.dataplane.getSuggestedFeeds({\n        actorDid: viewer ?? undefined,\n        limit: params.limit,\n        cursor: params.cursor,\n      })\n      const uris = suggestedRes.uris\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const hydration = await ctx.hydrator.hydrateFeedGens(uris, hydrateCtx)\n      const feedViews = mapDefined(uris, (uri) =>\n        ctx.views.feedGenerator(uri, hydration),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          feeds: feedViews,\n          cursor: parseString(suggestedRes.cursor),\n        },\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/getTimeline.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { FeedItem } from '../../../../hydration/feed'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getTimeline'\nimport { createPipeline } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getTimeline = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutes,\n    presentation,\n  )\n  server.app.bsky.feed.getTimeline({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n\n      const result = await getTimeline(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers, repoRev }),\n      }\n    },\n  })\n}\n\nexport const skeleton = async (inputs: {\n  ctx: Context\n  params: Params\n}): Promise<Skeleton> => {\n  const { ctx, params } = inputs\n  if (clearlyBadCursor(params.cursor)) {\n    return { items: [] }\n  }\n  const res = await ctx.dataplane.getTimeline({\n    actorDid: params.hydrateCtx.viewer,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    items: res.items.map((item) => ({\n      post: { uri: item.uri, cid: item.cid || undefined },\n      repost: item.repost\n        ? { uri: item.repost, cid: item.repostCid || undefined }\n        : undefined,\n    })),\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (inputs: {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}): Promise<HydrationState> => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx)\n}\n\nconst noBlocksOrMutes = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}): Skeleton => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.items = skeleton.items.filter((item) => {\n    const bam = ctx.views.feedItemBlocksAndMutes(item, hydration)\n    return (\n      !bam.authorBlocked &&\n      !bam.authorMuted &&\n      !bam.originatorBlocked &&\n      !bam.originatorMuted &&\n      !bam.ancestorAuthorBlocked\n    )\n  })\n  return skeleton\n}\n\nconst presentation = (inputs: {\n  ctx: Context\n  skeleton: Skeleton\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = inputs\n  const feed = mapDefined(skeleton.items, (item) =>\n    ctx.views.feedViewPost(item, hydration),\n  )\n  return { feed, cursor: skeleton.cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  dataplane: DataPlaneClient\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx & { viewer: string } }\n\ntype Skeleton = {\n  items: FeedItem[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/feed/searchPosts.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { mapDefined } from '@atproto/common'\nimport { ServerConfig } from '../../../../config'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport {\n  PostSearchQuery,\n  parsePostSearchQuery,\n} from '../../../../data-plane/server/util'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/feed/searchPosts'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { uriToDid as creatorFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const searchPosts = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrTagged,\n    presentation,\n  )\n  server.app.bsky.feed.searchPosts({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const { viewer, isModService } = ctx.authVerifier.parseCreds(auth)\n\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        features: ctx.featureGatesClient.scope(\n          ctx.featureGatesClient.parseUserContextFromHandler({\n            viewer,\n            req,\n          }),\n        ),\n      })\n      const results = await searchPosts(\n        { ...params, hydrateCtx, isModService },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: results,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const parsedQuery = parsePostSearchQuery(params.q, {\n    author: params.author,\n  })\n\n  if (ctx.searchAgent) {\n    // @NOTE cursors won't change on appview swap\n    const { data: res } =\n      await ctx.searchAgent.api.app.bsky.unspecced.searchPostsSkeleton({\n        q: params.q,\n        cursor: params.cursor,\n        limit: params.limit,\n        author: params.author,\n        domain: params.domain,\n        lang: params.lang,\n        mentions: params.mentions,\n        since: params.since,\n        sort: params.sort,\n        tag: params.tag,\n        until: params.until,\n        url: params.url,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n      })\n    return {\n      posts: res.posts.map(({ uri }) => uri),\n      cursor: parseString(res.cursor),\n      parsedQuery,\n    }\n  }\n\n  const res = await ctx.dataplane.searchPosts({\n    term: params.q,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    posts: res.uris,\n    cursor: parseString(res.cursor),\n    parsedQuery,\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydratePosts(\n    skeleton.posts.map((uri) => ({ uri })),\n    params.hydrateCtx,\n    undefined,\n    {\n      processDynamicTagsForView: params.hydrateCtx.features?.checkGate(\n        params.hydrateCtx.features.Gate.SearchFilteringExplorationEnable,\n      )\n        ? 'search'\n        : undefined,\n    },\n  )\n}\n\nconst noBlocksOrTagged = (inputs: RulesFnInput<Context, Params, Skeleton>) => {\n  const { ctx, params, skeleton, hydration } = inputs\n  const { parsedQuery } = skeleton\n\n  skeleton.posts = skeleton.posts.filter((uri) => {\n    const post = hydration.posts?.get(uri)\n    if (!post) return\n\n    const creator = creatorFromUri(uri)\n    const isCuratedSearch = params.sort === 'top'\n    const isPostByViewer = creator === params.hydrateCtx.viewer\n\n    // Cases to always show.\n    if (isPostByViewer) return true\n    if (params.isModService) return true\n\n    // Cases to never show.\n    if (ctx.views.viewerBlockExists(creator, hydration)) return false\n\n    let tagged = false\n    if (\n      params.hydrateCtx.features?.checkGate(\n        params.hydrateCtx.features.Gate.SearchFilteringExplorationEnable,\n      )\n    ) {\n      tagged = post.tags.has(ctx.cfg.visibilityTagHide)\n    } else {\n      tagged = [...ctx.cfg.searchTagsHide].some((t) => post.tags.has(t))\n    }\n\n    // Cases to conditionally show based on tagging.\n    if (isCuratedSearch && tagged) return false\n    if (!parsedQuery.author && tagged) return false\n    return true\n  })\n  return skeleton\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, skeleton, hydration } = inputs\n  const posts = mapDefined(skeleton.posts, (uri) => {\n    const post = hydration.posts?.get(uri)\n    if (!post) return\n\n    return ctx.views.post(uri, hydration)\n  })\n  return {\n    posts,\n    cursor: skeleton.cursor,\n    hitsTotal: skeleton.hitsTotal,\n  }\n}\n\ntype Context = {\n  cfg: ServerConfig\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  searchAgent?: AtpAgent\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n  isModService: boolean\n}\n\ntype Skeleton = {\n  posts: string[]\n  hitsTotal?: number\n  cursor?: string\n  parsedQuery: PostSearchQuery\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getActorStarterPacks.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getActorStarterPacks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getActorStarterPacks = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.graph.getActorStarterPacks({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getActorStarterPacks({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  const [did] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!did) {\n    throw new InvalidRequestError('Profile not found')\n  }\n  const starterPacks = await ctx.dataplane.getActorStarterPacks({\n    actorDid: did,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    starterPackUris: starterPacks.uris,\n    cursor: parseString(starterPacks.cursor),\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateStarterPacksBasic(\n    skeleton.starterPackUris,\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const starterPacks = mapDefined(skeleton.starterPackUris, (uri) =>\n    ctx.views.starterPackBasic(uri, hydration),\n  )\n  return {\n    starterPacks,\n    cursor: skeleton.cursor,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  dataplane: DataPlaneClient\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype SkeletonState = {\n  starterPackUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getBlocks.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getBlocks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getBlocks = createPipeline(skeleton, hydration, noRules, presentation)\n  server.app.bsky.graph.getBlocks({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const result = await getBlocks(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (clearlyBadCursor(params.cursor)) {\n    return { blockedDids: [] }\n  }\n  const { blockUris, cursor } = await ctx.hydrator.dataplane.getBlocks({\n    actorDid: params.hydrateCtx.viewer,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  const blocks = await ctx.hydrator.graph.getBlocks(blockUris)\n  const blockedDids = mapDefined(\n    blockUris,\n    (uri) => blocks.get(uri)?.record.subject,\n  )\n  return {\n    blockedDids,\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateProfiles(skeleton.blockedDids, params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton } = input\n  const { blockedDids, cursor } = skeleton\n  const blocks = mapDefined(blockedDids, (did) => {\n    return ctx.views.profile(did, hydration)\n  })\n  return { blocks, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  blockedDids: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getFollowers.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollowers'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { uriToDid as didFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getFollowers = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.graph.getFollowers({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n\n      const result = await getFollowers({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor])\n  if (!subjectDid) {\n    throw new InvalidRequestError(`Actor not found: ${params.actor}`)\n  }\n  if (clearlyBadCursor(params.cursor)) {\n    return { subjectDid, followUris: [] }\n  }\n  const { followers, cursor } = await ctx.hydrator.graph.getActorFollowers({\n    did: subjectDid,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    subjectDid,\n    followUris: followers.map((f) => f.uri),\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { followUris, subjectDid } = skeleton\n  const followState = await ctx.hydrator.hydrateFollows(\n    followUris,\n    params.hydrateCtx,\n  )\n  const dids = [subjectDid]\n  if (followState.follows) {\n    for (const [uri, follow] of followState.follows) {\n      if (follow) {\n        dids.push(didFromUri(uri))\n      }\n    }\n  }\n  const profileState = await ctx.hydrator.hydrateProfiles(\n    dids,\n    params.hydrateCtx,\n  )\n  return mergeStates(followState, profileState)\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, params, hydration, ctx } = input\n  const viewer = params.hydrateCtx.viewer\n  skeleton.followUris = skeleton.followUris.filter((followUri) => {\n    const followerDid = didFromUri(followUri)\n    return (\n      !hydration.followBlocks?.get(followUri) &&\n      (!viewer || !ctx.views.viewerBlockExists(followerDid, hydration))\n    )\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton, params } = input\n  const { subjectDid, followUris, cursor } = skeleton\n  const isNoHosted = (did: string) => ctx.views.actorIsNoHosted(did, hydration)\n\n  const subject = ctx.views.profile(subjectDid, hydration)\n  if (\n    !subject ||\n    (!params.hydrateCtx.includeTakedowns && isNoHosted(subjectDid))\n  ) {\n    throw new InvalidRequestError(`Actor not found: ${params.actor}`)\n  }\n\n  const followers = mapDefined(followUris, (followUri) => {\n    const followerDid = didFromUri(followUri)\n    if (!params.hydrateCtx.includeTakedowns && isNoHosted(followerDid)) {\n      return\n    }\n    return ctx.views.profile(didFromUri(followUri), hydration)\n  })\n\n  return { followers, subject, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = {\n  subjectDid: string\n  followUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getFollows.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollowers'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getFollows = createPipeline(skeleton, hydration, noBlocks, presentation)\n  server.app.bsky.graph.getFollows({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n\n      // @TODO ensure canViewTakedowns gets threaded through and applied properly\n      const result = await getFollows({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor])\n  if (!subjectDid) {\n    throw new InvalidRequestError(`Actor not found: ${params.actor}`)\n  }\n  if (clearlyBadCursor(params.cursor)) {\n    return { subjectDid, followUris: [] }\n  }\n  const { follows, cursor } = await ctx.hydrator.graph.getActorFollows({\n    did: subjectDid,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    subjectDid,\n    followUris: follows.map((f) => f.uri),\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { followUris, subjectDid } = skeleton\n  const followState = await ctx.hydrator.hydrateFollows(\n    followUris,\n    params.hydrateCtx,\n  )\n  const dids = [subjectDid]\n  if (followState.follows) {\n    for (const follow of followState.follows.values()) {\n      if (follow) {\n        dids.push(follow.record.subject)\n      }\n    }\n  }\n  const profileState = await ctx.hydrator.hydrateProfiles(\n    dids,\n    params.hydrateCtx,\n  )\n  return mergeStates(followState, profileState)\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, params, hydration, ctx } = input\n  const viewer = params.hydrateCtx.viewer\n  skeleton.followUris = skeleton.followUris.filter((followUri) => {\n    const follow = hydration.follows?.get(followUri)\n    if (!follow) return false\n    return (\n      !hydration.followBlocks?.get(followUri) &&\n      (!viewer ||\n        !ctx.views.viewerBlockExists(follow.record.subject, hydration))\n    )\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton, params } = input\n  const { subjectDid, followUris, cursor } = skeleton\n  const isNoHosted = (did: string) => ctx.views.actorIsNoHosted(did, hydration)\n\n  const subject = ctx.views.profile(subjectDid, hydration)\n  if (\n    !subject ||\n    (!params.hydrateCtx.includeTakedowns && isNoHosted(subjectDid))\n  ) {\n    throw new InvalidRequestError(`Actor not found: ${params.actor}`)\n  }\n\n  const follows = mapDefined(followUris, (followUri) => {\n    const followDid = hydration.follows?.get(followUri)?.record.subject\n    if (!followDid) return\n    if (!params.hydrateCtx.includeTakedowns && isNoHosted(followDid)) {\n      return\n    }\n    return ctx.views.profile(followDid, hydration)\n  })\n\n  return { follows, subject, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = {\n  subjectDid: string\n  followUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getKnownFollowers.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getKnownFollowers'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getKnownFollowers = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.graph.getKnownFollowers({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n\n      const result = await getKnownFollowers(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor])\n  if (!subjectDid) {\n    throw new InvalidRequestError(`Actor not found: ${params.actor}`)\n  }\n  if (clearlyBadCursor(params.cursor)) {\n    return { subjectDid, knownFollowers: [], cursor: undefined }\n  }\n\n  const res = await ctx.hydrator.dataplane.getFollowsFollowing({\n    actorDid: params.hydrateCtx.viewer,\n    targetDids: [subjectDid],\n  })\n  const result = res.results.at(0)\n  const knownFollowers = result ? result.dids.slice(0, params.limit) : []\n\n  return {\n    subjectDid,\n    knownFollowers,\n    cursor: undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { knownFollowers } = skeleton\n  const profilesState = await ctx.hydrator.hydrateProfiles(\n    knownFollowers.concat(skeleton.subjectDid),\n    params.hydrateCtx,\n  )\n  return profilesState\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, hydration, ctx } = input\n  skeleton.knownFollowers = skeleton.knownFollowers.filter((did) => {\n    return !ctx.views.viewerBlockExists(did, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton } = input\n  const { knownFollowers } = skeleton\n\n  const followers = mapDefined(knownFollowers, (did) => {\n    return ctx.views.profile(did, hydration)\n  })\n  const subject = ctx.views.profile(skeleton.subjectDid, hydration)!\n\n  return { subject, followers, cursor: undefined }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  subjectDid: string\n  knownFollowers: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getList.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getList'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { ListItemInfo } from '../../../../proto/bsky_pb'\nimport { uriToDid as didFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getList = createPipeline(skeleton, hydration, noBlocks, presentation)\n  server.app.bsky.graph.getList({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getList({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  if (clearlyBadCursor(params.cursor)) {\n    return { listUri: params.list, listitems: [] }\n  }\n  const { listitems, cursor } = await ctx.hydrator.dataplane.getListMembers({\n    listUri: params.list,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    listUri: params.list,\n    listitems,\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { listUri, listitems } = skeleton\n  const [listState, profileState] = await Promise.all([\n    ctx.hydrator.hydrateLists([listUri], params.hydrateCtx),\n    ctx.hydrator.hydrateProfiles(\n      listitems.map(({ did }) => did),\n      params.hydrateCtx,\n    ),\n  ])\n  const bidirectionalBlocks = await maybeGetBlocksForReferenceAndCurateList({\n    ctx,\n    params,\n    skeleton,\n    listState,\n  })\n  return mergeManyStates(listState, profileState, { bidirectionalBlocks })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, hydration } = input\n  const creator = didFromUri(skeleton.listUri)\n  const blocks = hydration.bidirectionalBlocks?.get(creator)\n  skeleton.listitems = skeleton.listitems.filter(({ did }) => {\n    return !blocks?.get(did)\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const { listUri, listitems, cursor } = skeleton\n  const list = ctx.views.list(listUri, hydration)\n  const items = mapDefined(listitems, ({ uri, did }) =>\n    ctx.views.listItemView(uri, did, hydration),\n  )\n  if (!list) {\n    throw new InvalidRequestError('List not found')\n  }\n  return { list, items, cursor }\n}\n\nconst maybeGetBlocksForReferenceAndCurateList = async (input: {\n  ctx: Context\n  listState: HydrationState\n  skeleton: SkeletonState\n  params: Params\n}) => {\n  const { ctx, params, listState, skeleton } = input\n  const { listitems } = skeleton\n  const { list } = params\n  const listRecord = listState.lists?.get(list)\n  const creator = didFromUri(list)\n  if (\n    params.hydrateCtx.viewer === creator ||\n    listRecord?.record.purpose === 'app.bsky.graph.defs#modlist'\n  ) {\n    return\n  }\n  const pairs: Map<string, string[]> = new Map()\n  pairs.set(\n    creator,\n    listitems.map(({ did }) => did),\n  )\n  return await ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx)\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = {\n  listUri: string\n  listitems: ListItemInfo[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getListBlocks.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getListBlocks = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.graph.getListBlocks({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const result = await getListBlocks(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  if (clearlyBadCursor(params.cursor)) {\n    return { listUris: [] }\n  }\n  const { listUris, cursor } =\n    await ctx.hydrator.dataplane.getBlocklistSubscriptions({\n      actorDid: params.hydrateCtx.viewer,\n      cursor: params.cursor,\n      limit: params.limit,\n    })\n  return { listUris, cursor: cursor || undefined }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  return await ctx.hydrator.hydrateLists(skeleton.listUris, params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const { listUris, cursor } = skeleton\n  const lists = mapDefined(listUris, (uri) => ctx.views.list(uri, hydration))\n  return { lists, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  listUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getListMutes.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getListMutes = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.graph.getListMutes({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const result = await getListMutes(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  if (clearlyBadCursor(params.cursor)) {\n    return { listUris: [] }\n  }\n  const { listUris, cursor } =\n    await ctx.hydrator.dataplane.getMutelistSubscriptions({\n      actorDid: params.hydrateCtx.viewer,\n      cursor: params.cursor,\n      limit: params.limit,\n    })\n  return { listUris, cursor: cursor || undefined }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  return await ctx.hydrator.hydrateLists(skeleton.listUris, params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const { listUris, cursor } = skeleton\n  const lists = mapDefined(listUris, (uri) => ctx.views.list(uri, hydration))\n  return { lists, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  listUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getLists.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport {\n  CURATELIST,\n  MODLIST,\n} from '../../../../lexicon/types/app/bsky/graph/defs'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getLists'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getLists = createPipeline(\n    skeleton,\n    hydration,\n    filterPurposes,\n    presentation,\n  )\n  server.app.bsky.graph.getLists({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req }) => {\n      const labelers = ctx.reqLabelers(req)\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getLists({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  if (clearlyBadCursor(params.cursor)) {\n    return { listUris: [] }\n  }\n\n  const [did] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!did) throw new InvalidRequestError('Profile not found')\n\n  const { listUris, cursor } = await ctx.hydrator.dataplane.getActorLists({\n    actorDid: did,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return { listUris, cursor: cursor || undefined }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { listUris } = skeleton\n  return ctx.hydrator.hydrateLists(listUris, params.hydrateCtx)\n}\n\nconst filterPurposes = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton, hydration, params } = input\n  const purposes = params.purposes || ['modlist', 'curatelist']\n\n  const acceptedPurposes = new Set()\n  if (purposes.includes('modlist')) acceptedPurposes.add(MODLIST)\n  if (purposes.includes(MODLIST)) acceptedPurposes.add(MODLIST)\n  if (purposes.includes('curatelist')) acceptedPurposes.add(CURATELIST)\n  if (purposes.includes(CURATELIST)) acceptedPurposes.add(CURATELIST)\n\n  // @NOTE: While we don't support filtering on the dataplane, this might result in empty pages.\n  // Despite the empty pages, the pagination still can enumerate all items for the specified filters.\n  skeleton.listUris = skeleton.listUris.filter((uri) => {\n    const list = hydration.lists?.get(uri)\n    return acceptedPurposes.has(list?.record.purpose)\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const { listUris, cursor } = skeleton\n  const lists = mapDefined(listUris, (uri) => {\n    return ctx.views.list(uri, hydration)\n  })\n  return { lists, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = {\n  listUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getListsWithMembership.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport {\n  CURATELIST,\n  MODLIST,\n} from '../../../../lexicon/types/app/bsky/graph/defs'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListsWithMembership'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getListsWithMembership = createPipeline(\n    skeleton,\n    hydration,\n    filterPurposes,\n    presentation,\n  )\n  server.app.bsky.graph.getListsWithMembership({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n      const result = await getListsWithMembership(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  const [actorDid] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!actorDid) throw new InvalidRequestError('Profile not found')\n\n  if (clearlyBadCursor(params.cursor)) {\n    return { actorDid, listUris: [] }\n  }\n\n  const { listUris, cursor } = await ctx.hydrator.dataplane.getActorLists({\n    actorDid: params.hydrateCtx.viewer,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return { actorDid, listUris, cursor: cursor || undefined }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { actorDid, listUris } = skeleton\n  return ctx.hydrator.hydrateListsMembership(\n    listUris,\n    actorDid,\n    params.hydrateCtx,\n  )\n}\n\nconst filterPurposes = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton, hydration, params } = input\n  const purposes = params.purposes || ['modlist', 'curatelist']\n\n  const acceptedPurposes = new Set()\n  if (purposes.includes('modlist')) acceptedPurposes.add(MODLIST)\n  if (purposes.includes(MODLIST)) acceptedPurposes.add(MODLIST)\n  if (purposes.includes('curatelist')) acceptedPurposes.add(CURATELIST)\n  if (purposes.includes(CURATELIST)) acceptedPurposes.add(CURATELIST)\n\n  // @NOTE: While we don't support filtering on the dataplane, this might result in empty pages.\n  // Despite the empty pages, the pagination still can enumerate all items for the specified filters.\n  skeleton.listUris = skeleton.listUris.filter((uri) => {\n    const list = hydration.lists?.get(uri)\n    return acceptedPurposes.has(list?.record.purpose)\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const { actorDid, listUris, cursor } = skeleton\n  const listsWithMembership = mapDefined(listUris, (uri) => {\n    const list = ctx.views.list(uri, hydration)\n    if (!list) return\n\n    const listItemUri = hydration.listMemberships\n      ?.get(uri)\n      ?.get(actorDid)?.actorListItemUri\n\n    return {\n      list,\n      listItem: listItemUri\n        ? ctx.views.listItemView(listItemUri, actorDid, hydration)\n        : undefined,\n    }\n  })\n  return { listsWithMembership, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  actorDid: string\n  listUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getMutes.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getMutes'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getMutes = createPipeline(skeleton, hydration, noRules, presentation)\n  server.app.bsky.graph.getMutes({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const result = await getMutes(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (clearlyBadCursor(params.cursor)) {\n    return { mutedDids: [] }\n  }\n  const { dids, cursor } = await ctx.hydrator.dataplane.getMutes({\n    actorDid: params.hydrateCtx.viewer,\n    cursor: params.cursor,\n    limit: params.limit,\n  })\n  return {\n    mutedDids: dids,\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { mutedDids } = skeleton\n  return ctx.hydrator.hydrateProfiles(mutedDids, params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton } = input\n  const { mutedDids, cursor } = skeleton\n  const mutes = mapDefined(mutedDids, (did) => {\n    return ctx.views.profile(did, hydration)\n  })\n  return { mutes, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  mutedDids: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getRelationships.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.getRelationships({\n    handler: async ({ params }) => {\n      const { actor, others = [] } = params\n      if (others.length < 1) {\n        return {\n          encoding: 'application/json',\n          body: {\n            actor,\n            relationships: [],\n          },\n        }\n      }\n      const res = await ctx.hydrator.actor.getProfileViewerStatesNaive(\n        others,\n        actor,\n      )\n      const relationships = others.map((did) => {\n        const subject = res.get(did)\n        return subject\n          ? {\n              $type: 'app.bsky.graph.defs#relationship',\n              did,\n              following: subject.following,\n              followedBy: subject.followedBy,\n              blocking: subject.blocking,\n              blockedBy: subject.blockedBy,\n              blockingByList: subject.blockingByList,\n              blockedByList: subject.blockedByList,\n            }\n          : {\n              $type: 'app.bsky.graph.defs#notFoundActor',\n              actor: did,\n              notFound: true,\n            }\n      })\n      return {\n        encoding: 'application/json',\n        body: {\n          actor,\n          relationships,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getStarterPack.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getStarterPack'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getStarterPack = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.graph.getStarterPack({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n      })\n      const result = await getStarterPack({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  const uri = await ctx.hydrator.resolveUri(params.starterPack)\n  return { uri }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateStarterPacks([skeleton.uri], params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  const starterPack = ctx.views.starterPack(skeleton.uri, hydration)\n  if (!starterPack) {\n    throw new InvalidRequestError('Starter pack not found')\n  }\n  return { starterPack }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = {\n  uri: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getStarterPacks.ts",
    "content": "import { dedupeStrs, mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  HydrationState,\n  Hydrator,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getStarterPacks'\nimport { createPipeline, noRules } from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getStarterPacks = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.graph.getStarterPacks({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        viewer,\n        labelers,\n        includeTakedowns,\n      })\n\n      const result = await getStarterPacks({ ...params, hydrateCtx }, ctx)\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: { params: Params }) => {\n  return { uris: inputs.params.uris }\n}\n\nconst hydration = async (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n}) => {\n  const { ctx, params, skeleton } = input\n  return ctx.hydrator.hydrateStarterPacksBasic(\n    dedupeStrs(skeleton.uris),\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (input: {\n  ctx: Context\n  params: Params\n  skeleton: SkeletonState\n  hydration: HydrationState\n}) => {\n  const { ctx, skeleton, hydration } = input\n  const starterPacks = mapDefined(skeleton.uris, (did) =>\n    ctx.views.starterPackBasic(did, hydration),\n  )\n  return { starterPacks }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx\n}\n\ntype SkeletonState = { uris: string[] }\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getStarterPacksWithMembership.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport {\n  OutputSchema,\n  QueryParams,\n} from '../../../../lexicon/types/app/bsky/graph/getStarterPacksWithMembership'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getStarterPacksWithMembership = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.graph.getStarterPacksWithMembership({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n      const result = await getStarterPacksWithMembership(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { ctx, params } = input\n  const [actorDid] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!actorDid) throw new InvalidRequestError('Profile not found')\n\n  if (clearlyBadCursor(params.cursor)) {\n    return { actorDid, starterPackUris: [] }\n  }\n\n  const { uris: starterPackUris, cursor } =\n    await ctx.hydrator.dataplane.getActorStarterPacks({\n      actorDid: params.hydrateCtx.viewer,\n      cursor: params.cursor,\n      limit: params.limit,\n    })\n\n  return { actorDid, starterPackUris, cursor: cursor || undefined }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { actorDid, starterPackUris } = skeleton\n  const spHydrationState = await ctx.hydrator.hydrateStarterPacks(\n    starterPackUris,\n    params.hydrateCtx,\n  )\n  const listUris = mapDefined(\n    starterPackUris,\n    (uri) => spHydrationState.starterPacks?.get(uri)?.record.list,\n  )\n  const listMembershipHydrationState =\n    await ctx.hydrator.hydrateListsMembership(\n      listUris,\n      actorDid,\n      params.hydrateCtx,\n    )\n  return mergeManyStates(spHydrationState, listMembershipHydrationState)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n): OutputSchema => {\n  const { ctx, skeleton, hydration } = input\n  const { actorDid, starterPackUris, cursor } = skeleton\n\n  const starterPacksWithMembership = mapDefined(starterPackUris, (spUri) => {\n    const listUri = hydration.starterPacks?.get(spUri)?.record.list\n    const starterPack = ctx.views.starterPack(spUri, hydration)\n    if (!listUri || !starterPack) return\n\n    const listItemUri = hydration.listMemberships\n      ?.get(listUri)\n      ?.get(actorDid)?.actorListItemUri\n\n    return {\n      starterPack,\n      listItem: listItemUri\n        ? ctx.views.listItemView(listItemUri, actorDid, hydration)\n        : undefined,\n    }\n  })\n  return { starterPacksWithMembership, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  actorDid: string\n  starterPackUris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { mapDefined, noUndefinedVals } from '@atproto/common'\nimport { HeadersMap } from '@atproto/xrpc'\nimport { InternalServerError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport {\n  OutputSchema,\n  QueryParams,\n} from '../../../../lexicon/types/app/bsky/graph/getSuggestedFollowsByActor'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getSuggestedFollowsByActor = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutes,\n    presentation,\n  )\n  server.app.bsky.graph.getSuggestedFollowsByActor({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        features: ctx.featureGatesClient.scope(\n          ctx.featureGatesClient.parseUserContextFromHandler({\n            viewer,\n            req,\n          }),\n        ),\n      })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n\n      let output: OutputSchema\n      let responseHeaders = {}\n\n      if (!ctx.suggestionsAgent) {\n        output = { suggestions: [] }\n      } else {\n        const { skeletonHeaders, ...result } = await getSuggestedFollowsByActor(\n          { ...params, hydrateCtx: hydrateCtx.copy({ viewer }), headers },\n          ctx,\n        )\n        output = result\n        responseHeaders = noUndefinedVals({\n          'content-language': skeletonHeaders?.['content-language'],\n        })\n      }\n\n      return {\n        encoding: 'application/json',\n        body: output,\n        headers: {\n          ...responseHeaders,\n          ...resHeaders({ labelers: hydrateCtx.labelers }),\n        },\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n\n  // handled above already, this branch should not be reached\n  if (!ctx.suggestionsAgent) {\n    throw new InternalServerError('Suggestions service not configured')\n  }\n\n  const [relativeToDid] = await ctx.hydrator.actor.getDids([params.actor])\n  if (!relativeToDid) {\n    throw new InvalidRequestError('Actor not found')\n  }\n\n  const res =\n    await ctx.suggestionsAgent.app.bsky.unspecced.getSuggestionsSkeleton(\n      {\n        viewer: params.hydrateCtx.viewer ?? undefined,\n        relativeToDid,\n      },\n      { headers: params.headers },\n    )\n  return {\n    recIdStr: res.data.recIdStr,\n    suggestedDids: res.data.actors.map((a) => a.did),\n    skeletonHeaders: res.headers,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { suggestedDids } = skeleton\n  if (\n    params.hydrateCtx.features.checkGate(\n      params.hydrateCtx.features.Gate.SuggestedUsersSocialProofEnable,\n    )\n  ) {\n    return ctx.hydrator.hydrateProfilesDetailed(\n      suggestedDids,\n      params.hydrateCtx,\n    )\n  } else {\n    return ctx.hydrator.hydrateProfiles(suggestedDids, params.hydrateCtx)\n  }\n}\n\nconst noBlocksOrMutes = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  skeleton.suggestedDids = skeleton.suggestedDids.filter(\n    (did) =>\n      !ctx.views.viewerBlockExists(did, hydration) &&\n      !ctx.views.viewerMuteExists(did, hydration),\n  )\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton } = input\n  const { suggestedDids, skeletonHeaders } = skeleton\n  const suggestions = mapDefined(suggestedDids, (did) =>\n    ctx.views.profileKnownFollowers(did, hydration),\n  )\n  return {\n    recIdStr: skeleton.recIdStr,\n    suggestions,\n    skeletonHeaders,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  suggestionsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n  headers: HeadersMap\n}\n\ntype SkeletonState = {\n  suggestedDids: string[]\n  recIdStr?: string\n  skeletonHeaders?: HeadersMap\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/muteActor.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { MuteOperation_Type } from '../../../../proto/bsync_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.muteActor({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const { actor } = input.body\n      const requester = auth.credentials.iss\n      const [did] = await ctx.hydrator.actor.getDids([actor])\n      if (!did) throw new InvalidRequestError('Actor not found')\n      await ctx.bsyncClient.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: requester,\n        subject: did,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/muteActorList.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { MuteOperation_Type } from '../../../../proto/bsync_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.muteActorList({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const { list } = input.body\n      const requester = auth.credentials.iss\n      await ctx.bsyncClient.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: requester,\n        subject: list,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/muteThread.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { MuteOperation_Type } from '../../../../proto/bsync_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.muteThread({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const { root } = input.body\n      const requester = auth.credentials.iss\n      await ctx.bsyncClient.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: requester,\n        subject: root,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/searchStarterPacks.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { DataPlaneClient } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/graph/searchStarterPacks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { uriToDid as creatorFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const searchStarterPacks = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.graph.searchStarterPacks({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        viewer,\n        labelers,\n        includeTakedowns,\n      })\n      const results = await searchStarterPacks({ ...params, hydrateCtx }, ctx)\n      return {\n        encoding: 'application/json',\n        body: results,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const { q } = params\n\n  if (ctx.searchAgent) {\n    // @NOTE cursors won't change on appview swap\n    const { data: res } =\n      await ctx.searchAgent.app.bsky.unspecced.searchStarterPacksSkeleton({\n        q,\n        cursor: params.cursor,\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n      })\n    return {\n      uris: res.starterPacks.map(({ uri }) => uri),\n      cursor: parseString(res.cursor),\n    }\n  }\n\n  const res = await ctx.dataplane.searchStarterPacks({\n    term: q,\n    limit: params.limit,\n    cursor: params.cursor,\n  })\n  return {\n    uris: res.uris,\n    cursor: parseString(res.cursor),\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateStarterPacksBasic(skeleton.uris, params.hydrateCtx)\n}\n\nconst noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => {\n  const { ctx, skeleton, hydration } = inputs\n  skeleton.uris = skeleton.uris.filter((uri) => {\n    const creator = creatorFromUri(uri)\n    return !ctx.views.viewerBlockExists(creator, hydration)\n  })\n  return skeleton\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, skeleton, hydration } = inputs\n  const starterPacks = mapDefined(skeleton.uris, (uri) =>\n    ctx.views.starterPackBasic(uri, hydration),\n  )\n  return {\n    starterPacks: starterPacks,\n    cursor: skeleton.cursor,\n  }\n}\n\ntype Context = {\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  searchAgent?: AtpAgent\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  uris: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/unmuteActor.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { MuteOperation_Type } from '../../../../proto/bsync_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.unmuteActor({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const { actor } = input.body\n      const requester = auth.credentials.iss\n      const [did] = await ctx.hydrator.actor.getDids([actor])\n      if (!did) throw new InvalidRequestError('Actor not found')\n      await ctx.bsyncClient.addMuteOperation({\n        type: MuteOperation_Type.REMOVE,\n        actorDid: requester,\n        subject: did,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { MuteOperation_Type } from '../../../../proto/bsync_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.unmuteActorList({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const { list } = input.body\n      const requester = auth.credentials.iss\n      await ctx.bsyncClient.addMuteOperation({\n        type: MuteOperation_Type.REMOVE,\n        actorDid: requester,\n        subject: list,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/graph/unmuteThread.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { MuteOperation_Type } from '../../../../proto/bsync_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.graph.unmuteThread({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const { root } = input.body\n      const requester = auth.credentials.iss\n      await ctx.bsyncClient.addMuteOperation({\n        type: MuteOperation_Type.REMOVE,\n        actorDid: requester,\n        subject: root,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/labeler/getServices.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.labeler.getServices({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ params, auth, req }) => {\n      const { dids, detailed } = params\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        viewer,\n        labelers,\n      })\n      const hydration = await ctx.hydrator.hydrateLabelers(dids, hydrateCtx)\n\n      const views = mapDefined(dids, (did) => {\n        if (detailed) {\n          const view = ctx.views.labelerDetailed(did, hydration)\n          if (!view) return\n          return {\n            ...view,\n            $type: 'app.bsky.labeler.defs#labelerViewDetailed',\n          }\n        } else {\n          const view = ctx.views.labeler(did, hydration)\n          if (!view) return\n          return {\n            ...view,\n            $type: 'app.bsky.labeler.defs#labelerView',\n          }\n        }\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          views,\n        },\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/getPreferences.ts",
    "content": "import assert from 'node:assert'\nimport { Un$Typed } from '@atproto/api'\nimport { UpstreamFailureError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'\nimport { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'\nimport { protobufToLex } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.getPreferences({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth }) => {\n      const actorDid = auth.credentials.iss\n      const preferences = await computePreferences(ctx, actorDid)\n      return {\n        encoding: 'application/json',\n        body: {\n          preferences,\n        },\n      }\n    },\n  })\n}\n\nconst computePreferences = async (\n  ctx: AppContext,\n  actorDid: string,\n): Promise<Un$Typed<Preferences>> => {\n  let res: GetNotificationPreferencesResponse\n  try {\n    res = await ctx.dataplane.getNotificationPreferences({\n      dids: [actorDid],\n    })\n  } catch (err) {\n    throw new UpstreamFailureError(\n      'cannot get current notification preferences',\n      'NotificationPreferencesFailed',\n      { cause: err },\n    )\n  }\n\n  assert(\n    res.preferences.length === 1,\n    `expected exactly one preferences entry, got ${res.preferences.length}`,\n  )\n\n  const currentPreferences = protobufToLex(res.preferences[0])\n  return currentPreferences\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/notification/getUnreadCount'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getUnreadCount = createPipeline(\n    skeleton,\n    hydration,\n    noRules,\n    presentation,\n  )\n  server.app.bsky.notification.getUnreadCount({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, params }) => {\n      const viewer = auth.credentials.iss\n      const result = await getUnreadCount({ ...params, viewer }, ctx)\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { params, ctx } = input\n  if (params.seenAt) {\n    throw new InvalidRequestError('The seenAt parameter is unsupported')\n  }\n  const priority = params.priority ?? (await getPriority(ctx, params.viewer))\n  const res = await ctx.hydrator.dataplane.getUnreadNotificationCount({\n    actorDid: params.viewer,\n    priority,\n  })\n  return {\n    count: res.count,\n  }\n}\n\nconst hydration = async (\n  _input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  return {}\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton } = input\n  return { count: skeleton.count }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  viewer: string\n}\n\ntype SkeletonState = {\n  count: number\n}\n\nconst getPriority = async (ctx: Context, did: string) => {\n  const actors = await ctx.hydrator.actor.getActors([did], {\n    skipCacheForDids: [did],\n  })\n  return !!actors.get(did)?.priorityNotifications\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/listActivitySubscriptions.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listActivitySubscriptions'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const listActivitySubscriptions = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.notification.listActivitySubscriptions({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n\n      const result = await listActivitySubscriptions(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  const actorDid = params.hydrateCtx.viewer\n  if (clearlyBadCursor(params.cursor)) {\n    return { actorDid, dids: [] }\n  }\n  const { dids, cursor } =\n    await ctx.hydrator.dataplane.getActivitySubscriptionDids({\n      actorDid: params.hydrateCtx.viewer,\n      limit: params.limit,\n      cursor: params.cursor,\n    })\n  return {\n    actorDid,\n    dids,\n    cursor: cursor || undefined,\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const { dids } = skeleton\n  const state = await ctx.hydrator.hydrateProfilesDetailed(\n    dids,\n    params.hydrateCtx,\n  )\n  return state\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, hydration, ctx } = input\n  skeleton.dids = skeleton.dids.filter(\n    (did) => !ctx.views.viewerBlockExists(did, hydration),\n  )\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, hydration, skeleton } = input\n  const { dids, cursor } = skeleton\n  const subscriptions = mapDefined(dids, (did) => {\n    return ctx.views.profile(did, hydration)\n  })\n  return { subscriptions, cursor }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  actorDid: string\n  dids: string[]\n  cursor?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/listNotifications.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ServerConfig } from '../../../../config'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { isRecord as isPostRecord } from '../../../../lexicon/types/app/bsky/feed/post'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listNotifications'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Notification } from '../../../../proto/bsky_pb'\nimport { uriToDid as didFromUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const listNotifications = createPipeline(\n    skeleton,\n    hydration,\n    noBlockOrMutesOrNeedsFiltering,\n    presentation,\n  )\n  server.app.bsky.notification.listNotifications({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ params, auth, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const result = await listNotifications(\n        { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n\nconst paginateNotifications = async (opts: {\n  ctx: Context\n  priority: boolean\n  reasons?: string[]\n  cursor?: string\n  limit: number\n  viewer: string\n}) => {\n  const { ctx, priority, reasons, limit, viewer } = opts\n\n  // if not filtering, then just pass through the response from dataplane\n  if (!reasons) {\n    const res = await ctx.hydrator.dataplane.getNotifications({\n      actorDid: viewer,\n      priority,\n      cursor: opts.cursor,\n      limit,\n    })\n    return {\n      notifications: res.notifications,\n      cursor: res.cursor,\n    }\n  }\n\n  let nextCursor: string | undefined = opts.cursor\n  let toReturn: Notification[] = []\n  const maxAttempts = 10\n  const attemptSize = Math.ceil(limit / 2)\n  for (let i = 0; i < maxAttempts; i++) {\n    const res = await ctx.hydrator.dataplane.getNotifications({\n      actorDid: viewer,\n      priority,\n      cursor: nextCursor,\n      limit,\n    })\n    const filtered = res.notifications.filter((notif) =>\n      reasons.includes(notif.reason),\n    )\n    toReturn = [...toReturn, ...filtered]\n    nextCursor = res.cursor ?? undefined\n    if (toReturn.length >= attemptSize || !nextCursor) {\n      break\n    }\n  }\n  return {\n    notifications: toReturn,\n    cursor: nextCursor,\n  }\n}\n\n/**\n * Applies a configurable delay to the datetime string of a cursor,\n * effectively allowing for a delay on listing the notifications.\n * This is useful to allow time for services to process notifications\n * before they are listed to the user.\n */\nexport const delayCursor = (\n  cursorStr: string | undefined,\n  delayMs: number,\n): string => {\n  const nowMinusDelay = Date.now() - delayMs\n  if (cursorStr === undefined) return new Date(nowMinusDelay).toISOString()\n  const cursor = new Date(cursorStr).getTime()\n  if (isNaN(cursor)) return cursorStr\n  return new Date(Math.min(cursor, nowMinusDelay)).toISOString()\n}\n\nconst skeleton = async (\n  input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n  const { params, ctx } = input\n  if (params.seenAt) {\n    throw new InvalidRequestError('The seenAt parameter is unsupported')\n  }\n\n  const originalCursor = params.cursor\n  const delayedCursor = delayCursor(\n    originalCursor,\n    ctx.cfg.notificationsDelayMs,\n  )\n  const viewer = params.hydrateCtx.viewer\n  const priority = params.priority ?? (await getPriority(ctx, viewer))\n  const [res, lastSeenRes] = await Promise.all([\n    paginateNotifications({\n      ctx,\n      priority,\n      reasons: params.reasons,\n      cursor: delayedCursor,\n      limit: params.limit,\n      viewer,\n    }),\n    ctx.hydrator.dataplane.getNotificationSeen({\n      actorDid: viewer,\n      priority,\n    }),\n  ])\n  // @NOTE for the first page of results if there's no last-seen time, consider top notification unread\n  // rather than all notifications. bit of a hack to be more graceful when seen times are out of sync.\n  let lastSeenDate = lastSeenRes.timestamp?.toDate()\n  if (!lastSeenDate && !originalCursor) {\n    lastSeenDate = res.notifications.at(0)?.timestamp?.toDate()\n  }\n  return {\n    notifs: res.notifications,\n    cursor: res.cursor || undefined,\n    priority,\n    lastSeenNotifs: lastSeenDate?.toISOString(),\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton, params, ctx } = input\n  return ctx.hydrator.hydrateNotifications(skeleton.notifs, params.hydrateCtx)\n}\n\nconst noBlockOrMutesOrNeedsFiltering = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton, hydration, ctx, params } = input\n  skeleton.notifs = skeleton.notifs.filter((item) => {\n    const did = didFromUri(item.uri)\n    if (\n      ctx.views.viewerBlockExists(did, hydration) ||\n      ctx.views.viewerMuteExists(did, hydration)\n    ) {\n      return false\n    }\n    // Filter out hidden replies only if the viewer owns\n    // the threadgate and they hid the reply.\n    if (item.reason === 'reply') {\n      const post = hydration.posts?.get(item.uri)\n      if (post) {\n        const rootPostUri = isPostRecord(post.record)\n          ? post.record.reply?.root.uri\n          : undefined\n        const isRootPostByViewer =\n          rootPostUri && didFromUri(rootPostUri) === params.hydrateCtx?.viewer\n        const isHiddenByThreadgate = isRootPostByViewer\n          ? ctx.views.replyIsHiddenByThreadgate(\n              item.uri,\n              rootPostUri,\n              hydration,\n            )\n          : false\n        if (isHiddenByThreadgate) {\n          return false\n        }\n      }\n    }\n    // Filter out notifications from users that have thread hide tags and are from people they\n    // are not following\n    if (\n      item.reason === 'reply' ||\n      item.reason === 'quote' ||\n      item.reason === 'mention'\n    ) {\n      const post = hydration.posts?.get(item.uri)\n      if (post) {\n        for (const [tag] of post.tags.entries()) {\n          if (ctx.cfg.threadTagsHide.has(tag)) {\n            if (!hydration.profileViewers?.get(did)?.following) {\n              return false\n            } else {\n              break\n            }\n          }\n        }\n      }\n    }\n    // Filter out notifications from users that need review unless moots\n    if (\n      item.reason === 'reply' ||\n      item.reason === 'quote' ||\n      item.reason === 'mention' ||\n      item.reason === 'like' ||\n      item.reason === 'follow'\n    ) {\n      if (!ctx.views.viewerSeesNeedsReview({ did, uri: item.uri }, hydration)) {\n        return false\n      }\n    }\n    return true\n  })\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton, hydration, ctx } = input\n  const { notifs, lastSeenNotifs, cursor } = skeleton\n  const notifications = mapDefined(notifs, (notif) =>\n    ctx.views.notification(notif, lastSeenNotifs, hydration),\n  )\n  return {\n    notifications,\n    cursor,\n    priority: skeleton.priority,\n    seenAt: skeleton.lastSeenNotifs,\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  cfg: ServerConfig\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string }\n}\n\ntype SkeletonState = {\n  notifs: Notification[]\n  priority: boolean\n  lastSeenNotifs?: string\n  cursor?: string\n}\n\nconst getPriority = async (ctx: Context, did: string) => {\n  const actors = await ctx.hydrator.actor.getActors([did], {\n    skipCacheForDids: [did],\n  })\n  return !!actors.get(did)?.priorityNotifications\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/putActivitySubscription.ts",
    "content": "import { TID } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { isActivitySubscriptionEnabled } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { Namespaces } from '../../../../stash'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.putActivitySubscription({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const actorDid = auth.credentials.iss\n      const { subject, activitySubscription } = input.body\n      if (actorDid === subject) {\n        throw new InvalidRequestError('Cannot subscribe to own activity')\n      }\n\n      const existingKey = await getExistingKey(ctx, actorDid, subject)\n      const enabled = isActivitySubscriptionEnabled(activitySubscription)\n\n      const stashInput = {\n        actorDid,\n        namespace:\n          Namespaces.AppBskyNotificationDefsSubjectActivitySubscription,\n        payload: {\n          subject,\n          activitySubscription,\n        },\n        key: existingKey ?? TID.nextStr(),\n      }\n\n      if (existingKey) {\n        if (enabled) {\n          await ctx.stashClient.update(stashInput)\n        } else {\n          await ctx.stashClient.delete(stashInput)\n        }\n      } else {\n        if (enabled) {\n          await ctx.stashClient.create(stashInput)\n        } else {\n          // no-op: subscription already doesn't exist\n        }\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          subject,\n          activitySubscription: enabled ? activitySubscription : undefined,\n        },\n      }\n    },\n  })\n}\n\nconst getExistingKey = async (\n  ctx: AppContext,\n  actorDid: string,\n  subject: string,\n): Promise<string | null> => {\n  const res = await ctx.dataplane.getActivitySubscriptionsByActorAndSubjects({\n    actorDid,\n    subjectDids: [subject],\n  })\n  const [existing] = res.subscriptions\n  const key = existing.key\n  return key || null\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/putPreferences.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.putPreferences({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const { priority } = input.body\n      const viewer = auth.credentials.iss\n      await ctx.bsyncClient.addNotifOperation({\n        actorDid: viewer,\n        priority,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/putPreferencesV2.ts",
    "content": "import assert from 'node:assert'\nimport { Un$Typed } from '@atproto/api'\nimport { UpstreamFailureError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'\nimport { HandlerInput } from '../../../../lexicon/types/app/bsky/notification/putPreferencesV2'\nimport { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'\nimport { Namespaces } from '../../../../stash'\nimport { protobufToLex } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.putPreferencesV2({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      const actorDid = auth.credentials.iss\n      const preferences = await computePreferences(ctx, actorDid, input)\n\n      // Notification preferences are created automatically on the dataplane on signup, so we just update.\n      await ctx.stashClient.update({\n        actorDid,\n        namespace: Namespaces.AppBskyNotificationDefsPreferences,\n        key: 'self',\n        payload: preferences,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          preferences,\n        },\n      }\n    },\n  })\n}\n\nconst computePreferences = async (\n  ctx: AppContext,\n  actorDid: string,\n  input: HandlerInput,\n): Promise<Un$Typed<Preferences>> => {\n  let res: GetNotificationPreferencesResponse\n  try {\n    res = await ctx.dataplane.getNotificationPreferences({\n      dids: [actorDid],\n    })\n  } catch (err) {\n    throw new UpstreamFailureError(\n      'cannot get current notification preferences',\n      'NotificationPreferencesFailed',\n      { cause: err },\n    )\n  }\n\n  assert(\n    res.preferences.length === 1,\n    `expected exactly one preferences entry, got ${res.preferences.length}`,\n  )\n\n  const currentPreferences = protobufToLex(res.preferences[0])\n  const preferences = { ...currentPreferences, ...input.body }\n  return preferences\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/registerPush.ts",
    "content": "import {\n  InvalidRequestError,\n  MethodNotImplementedError,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertLexPlatform, lexPlatformToProtoPlatform } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.registerPush({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      if (!ctx.courierClient) {\n        throw new MethodNotImplementedError(\n          'This service is not configured to support push token registration.',\n        )\n      }\n      const { token, platform, serviceDid, appId, ageRestricted } = input.body\n      const did = auth.credentials.iss\n      if (serviceDid !== auth.credentials.aud) {\n        throw new InvalidRequestError('Invalid serviceDid.')\n      }\n      try {\n        assertLexPlatform(platform)\n      } catch (err) {\n        throw new InvalidRequestError(\n          'Unsupported platform: must be \"ios\", \"android\", or \"web\".',\n        )\n      }\n      await ctx.courierClient.registerDeviceToken({\n        did,\n        token,\n        platform: lexPlatformToProtoPlatform(platform),\n        appId,\n        ageRestricted,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/unregisterPush.ts",
    "content": "import {\n  InvalidRequestError,\n  MethodNotImplementedError,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertLexPlatform, lexPlatformToProtoPlatform } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.unregisterPush({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input }) => {\n      if (!ctx.courierClient) {\n        throw new MethodNotImplementedError(\n          'This service is not configured to support push token registration.',\n        )\n      }\n      const { token, platform, serviceDid, appId } = input.body\n      const did = auth.credentials.iss\n      if (serviceDid !== auth.credentials.aud) {\n        throw new InvalidRequestError('Invalid serviceDid.')\n      }\n      try {\n        assertLexPlatform(platform)\n      } catch (err) {\n        throw new InvalidRequestError(\n          'Unsupported platform: must be \"ios\", \"android\", or \"web\".',\n        )\n      }\n      await ctx.courierClient.unregisterDeviceToken({\n        did,\n        token,\n        platform: lexPlatformToProtoPlatform(platform),\n        appId,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/updateSeen.ts",
    "content": "import { Struct, Timestamp } from '@bufbuild/protobuf'\nimport { v3 as murmurV3 } from 'murmurhash'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.notification.updateSeen({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const viewer = auth.credentials.iss\n      const seenAt = new Date(input.body.seenAt)\n      // For now we keep separate seen times behind the scenes for priority, but treat them as a single seen time.\n      await Promise.all([\n        ctx.dataplane.updateNotificationSeen({\n          actorDid: viewer,\n          timestamp: Timestamp.fromDate(seenAt),\n          priority: false,\n        }),\n        ctx.dataplane.updateNotificationSeen({\n          actorDid: viewer,\n          timestamp: Timestamp.fromDate(seenAt),\n          priority: true,\n        }),\n        ctx.courierClient?.pushNotifications({\n          notifications: [\n            {\n              id: getNotifId(viewer, seenAt),\n              clientControlled: true,\n              recipientDid: viewer,\n              alwaysDeliver: false,\n              collapseKey: 'mark-read-generic',\n              timestamp: Timestamp.fromDate(new Date()),\n              additional: Struct.fromJson({\n                reason: 'mark-read-generic',\n              }),\n            },\n          ],\n        }),\n      ])\n    },\n  })\n}\n\nfunction getNotifId(viewer: string, seenAt: Date) {\n  const key = ['mark-read-generic', viewer, seenAt.getTime().toString()].join(\n    '::',\n  )\n  return murmurV3(key).toString(16)\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/notification/util.ts",
    "content": "import { Un$Typed } from '@atproto/api'\nimport {\n  ChatPreference,\n  FilterablePreference,\n  Preference,\n  Preferences,\n} from '../../../../lexicon/types/app/bsky/notification/defs'\nimport {\n  ChatNotificationInclude,\n  ChatNotificationPreference,\n  FilterableNotificationPreference,\n  NotificationInclude,\n  NotificationPreference,\n  NotificationPreferences,\n} from '../../../../proto/bsky_pb'\nimport { AppPlatform } from '../../../../proto/courier_pb'\n\ntype DeepPartial<T> = T extends object\n  ? {\n      [P in keyof T]?: DeepPartial<T[P]>\n    }\n  : T\n\nconst ensureChatPreference = (\n  p?: DeepPartial<ChatPreference>,\n): ChatPreference => {\n  const includeValues = ['all', 'accepted']\n  return {\n    include:\n      typeof p?.include === 'string' && includeValues.includes(p.include)\n        ? p.include\n        : 'all',\n    push: p?.push ?? true,\n  }\n}\n\nconst ensureFilterablePreference = (\n  p?: DeepPartial<FilterablePreference>,\n): FilterablePreference => {\n  const includeValues = ['all', 'follows']\n  return {\n    include:\n      typeof p?.include === 'string' && includeValues.includes(p.include)\n        ? p.include\n        : 'all',\n    list: p?.list ?? true,\n    push: p?.push ?? true,\n  }\n}\n\nconst ensurePreference = (p?: DeepPartial<Preference>): Preference => {\n  return {\n    list: p?.list ?? true,\n    push: p?.push ?? true,\n  }\n}\n\nconst ensurePreferences = (\n  p: DeepPartial<Preferences>,\n): Un$Typed<Preferences> => {\n  return {\n    chat: ensureChatPreference(p.chat),\n    follow: ensureFilterablePreference(p.follow),\n    like: ensureFilterablePreference(p.like),\n    likeViaRepost: ensureFilterablePreference(p.likeViaRepost),\n    mention: ensureFilterablePreference(p.mention),\n    quote: ensureFilterablePreference(p.quote),\n    reply: ensureFilterablePreference(p.reply),\n    repost: ensureFilterablePreference(p.repost),\n    repostViaRepost: ensureFilterablePreference(p.repostViaRepost),\n    starterpackJoined: ensurePreference(p.starterpackJoined),\n    subscribedPost: ensurePreference(p.subscribedPost),\n    unverified: ensurePreference(p.unverified),\n    verified: ensurePreference(p.verified),\n  }\n}\n\nconst protobufChatPreferenceToLex = (\n  p?: DeepPartial<ChatNotificationPreference>,\n): DeepPartial<ChatPreference> => {\n  return {\n    include:\n      p?.include === ChatNotificationInclude.ACCEPTED ? 'accepted' : 'all',\n    push: p?.push?.enabled,\n  }\n}\n\nconst protobufFilterablePreferenceToLex = (\n  p?: DeepPartial<FilterableNotificationPreference>,\n): DeepPartial<FilterablePreference> => {\n  return {\n    include: p?.include === NotificationInclude.FOLLOWS ? 'follows' : 'all',\n    list: p?.list?.enabled,\n    push: p?.push?.enabled,\n  }\n}\n\nconst protobufPreferenceToLex = (\n  p?: DeepPartial<NotificationPreference>,\n): DeepPartial<Preference> => {\n  return {\n    list: p?.list?.enabled,\n    push: p?.push?.enabled,\n  }\n}\n\nexport const protobufToLex = (\n  res: DeepPartial<NotificationPreferences>,\n): Un$Typed<Preferences> => {\n  return ensurePreferences({\n    chat: protobufChatPreferenceToLex(res.chat),\n    follow: protobufFilterablePreferenceToLex(res.follow),\n    like: protobufFilterablePreferenceToLex(res.like),\n    likeViaRepost: protobufFilterablePreferenceToLex(res.likeViaRepost),\n    mention: protobufFilterablePreferenceToLex(res.mention),\n    quote: protobufFilterablePreferenceToLex(res.quote),\n    reply: protobufFilterablePreferenceToLex(res.reply),\n    repost: protobufFilterablePreferenceToLex(res.repost),\n    repostViaRepost: protobufFilterablePreferenceToLex(res.repostViaRepost),\n    starterpackJoined: protobufPreferenceToLex(res.starterpackJoined),\n    subscribedPost: protobufPreferenceToLex(res.subscribedPost),\n    unverified: protobufPreferenceToLex(res.unverified),\n    verified: protobufPreferenceToLex(res.verified),\n  })\n}\n\ntype LexPlatform = 'ios' | 'android' | 'web'\n\nexport function assertLexPlatform(\n  platform: string,\n): asserts platform is LexPlatform {\n  if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n    throw new Error('Unsupported platform: must be \"ios\", \"android\", or \"web\".')\n  }\n}\n\nexport const lexPlatformToProtoPlatform = (platform: string): AppPlatform =>\n  platform === 'ios'\n    ? AppPlatform.IOS\n    : platform === 'android'\n      ? AppPlatform.ANDROID\n      : AppPlatform.WEB\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getAgeAssuranceState.ts",
    "content": "import { UpstreamFailureError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ActorInfo } from '../../../../proto/bsky_pb'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.unspecced.getAgeAssuranceState({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth }) => {\n      const viewer = auth.credentials.iss\n      const actorInfo = await getAgeVerificationState(ctx, viewer)\n\n      return {\n        encoding: 'application/json',\n        body: {\n          lastInitiatedAt:\n            actorInfo.ageAssuranceStatus?.lastInitiatedAt\n              ?.toDate()\n              .toISOString() ?? undefined,\n          status: actorInfo.ageAssuranceStatus?.status ?? 'unknown',\n        },\n      }\n    },\n  })\n}\n\nconst getAgeVerificationState = async (\n  ctx: AppContext,\n  actorDid: string,\n): Promise<ActorInfo> => {\n  try {\n    const res = await ctx.dataplane.getActors({\n      dids: [actorDid],\n      returnAgeAssuranceForDids: [actorDid],\n      skipCacheForDids: [actorDid],\n    })\n\n    return res.actors[0]\n  } catch (err) {\n    throw new UpstreamFailureError(\n      'Cannot get current age assurance state',\n      'GetAgeAssuranceStateFailed',\n      { cause: err },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getConfig.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\n// THIS IS A TEMPORARY UNSPECCED ROUTE\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.unspecced.getConfig({\n    handler: async () => {\n      return {\n        encoding: 'application/json',\n        body: {\n          checkEmailConfirmed: ctx.cfg.clientCheckEmailConfirmed,\n          topicsEnabled: ctx.cfg.topicsEnabled,\n          liveNow: ctx.cfg.liveNowConfig,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts",
    "content": "import AtpAgent, { AtUri } from '@atproto/api'\nimport { dedupeStrs, mapDefined, noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getOnboardingSuggestedStarterPacks = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.unspecced.getOnboardingSuggestedStarterPacks({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const result = await getOnboardingSuggestedStarterPacks(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (ctx.topicsAgent) {\n    const res =\n      await ctx.topicsAgent.app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton(\n        {\n          limit: params.limit,\n          viewer: params.hydrateCtx.viewer ?? undefined,\n        },\n        {\n          headers: params.headers,\n        },\n      )\n\n    return res.data\n  } else {\n    throw new InternalServerError('Topics agent not available')\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  let dids: string[] = []\n  for (const uri of skeleton.starterPacks) {\n    let aturi: AtUri | undefined\n    try {\n      aturi = new AtUri(uri)\n    } catch {\n      continue\n    }\n    dids.push(aturi.hostname)\n  }\n  dids = dedupeStrs(dids)\n  const pairs: Map<string, string[]> = new Map()\n  const viewer = params.hydrateCtx.viewer\n  if (viewer) {\n    pairs.set(viewer, dids)\n  }\n  const [starterPacksState, bidirectionalBlocks] = await Promise.all([\n    ctx.hydrator.hydrateStarterPacks(skeleton.starterPacks, params.hydrateCtx),\n    ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx),\n  ])\n\n  return mergeManyStates(starterPacksState, { bidirectionalBlocks })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, params, hydration } = input\n  const viewer = params.hydrateCtx.viewer\n  if (!viewer) {\n    return skeleton\n  }\n\n  const blocks = hydration.bidirectionalBlocks?.get(viewer)\n  const filteredSkeleton: SkeletonState = {\n    starterPacks: skeleton.starterPacks.filter((uri) => {\n      let aturi: AtUri | undefined\n      try {\n        aturi = new AtUri(uri)\n      } catch {\n        return false\n      }\n      return !blocks?.get(aturi.hostname)\n    }),\n  }\n\n  return filteredSkeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n\n  return {\n    starterPacks: mapDefined(skeleton.starterPacks, (uri) =>\n      ctx.views.starterPack(uri, hydration),\n    ),\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string | null }\n  headers: Record<string, string>\n}\n\ntype SkeletonState = {\n  starterPacks: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../../../context'\nimport { parseString } from '../../../../hydration/util'\nimport { Server } from '../../../../lexicon'\nimport { clearlyBadCursor, resHeaders } from '../../../util'\n\n// THIS IS A TEMPORARY UNSPECCED ROUTE\n// @TODO currently mirrors getSuggestedFeeds and ignores the \"query\" param.\n// In the future may take into consideration popularity via likes w/ its own dataplane endpoint.\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.unspecced.getPopularFeedGenerators({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers })\n\n      if (clearlyBadCursor(params.cursor)) {\n        return {\n          encoding: 'application/json',\n          body: { feeds: [] },\n        }\n      }\n\n      let uris: string[]\n      let cursor: string | undefined\n\n      const query = params.query?.trim() ?? ''\n      if (query) {\n        const res = await ctx.dataplane.searchFeedGenerators({\n          query,\n          limit: params.limit,\n        })\n        uris = res.uris\n      } else {\n        const res = await ctx.dataplane.getSuggestedFeeds({\n          actorDid: viewer ?? undefined,\n          limit: params.limit,\n          cursor: params.cursor,\n        })\n        uris = res.uris\n        cursor = parseString(res.cursor)\n      }\n\n      const hydration = await ctx.hydrator.hydrateFeedGens(uris, hydrateCtx)\n      const feedViews = mapDefined(uris, (uri) =>\n        ctx.views.feedGenerator(uri, hydration),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          feeds: feedViews,\n          cursor,\n        },\n        headers: resHeaders({ labelers: hydrateCtx.labelers }),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getPostThreadOtherV2.ts",
    "content": "import { ServerConfig } from '../../../../config'\nimport { AppContext } from '../../../../context'\nimport { Code, DataPlaneClient, isDataplaneError } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getPostThreadOtherV2'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\n// No parents for hidden replies (it would be the anchor post).\nconst ABOVE = 0\n\n// For hidden replies we don't get more than the top-level replies.\n// To get nested replies, load the thread as one of the hidden replies as anchor.\nconst BELOW = 1\n\n// It doesn't really matter since BELOW is 1, so it will not be used.\nconst BRANCHING_FACTOR = 0\n\nexport default function (server: Server, ctx: AppContext) {\n  const getPostThreadOther = createPipeline(\n    skeleton,\n    hydration,\n    noRules, // handled in presentation: 3p block-violating replies are turned to #blockedPost, viewer blocks turned to #notFoundPost.\n    presentation,\n  )\n  server.app.bsky.unspecced.getPostThreadOtherV2({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns, include3pBlocks } =\n        ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n        include3pBlocks,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: await getPostThreadOther({ ...params, hydrateCtx }, ctx),\n        headers: resHeaders({\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const anchor = await ctx.hydrator.resolveUri(params.anchor)\n  try {\n    const res = await ctx.dataplane.getThread({\n      postUri: anchor,\n      above: ABOVE,\n      below: BELOW,\n    })\n    return {\n      anchor,\n      uris: res.uris,\n    }\n  } catch (err) {\n    if (isDataplaneError(err, Code.NotFound)) {\n      return {\n        anchor,\n        uris: [],\n      }\n    } else {\n      throw err\n    }\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateThreadPosts(\n    skeleton.uris.map((uri) => ({ uri })),\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, skeleton, hydration } = inputs\n  const thread = ctx.views.threadOtherV2(skeleton, hydration, {\n    below: BELOW,\n    branchingFactor: BRANCHING_FACTOR,\n  })\n  return { thread }\n}\n\ntype Context = {\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  cfg: ServerConfig\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  anchor: string\n  uris: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getPostThreadV2.ts",
    "content": "import { ServerConfig } from '../../../../config'\nimport { AppContext } from '../../../../context'\nimport { Code, DataPlaneClient, isDataplaneError } from '../../../../data-plane'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getPostThreadV2'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { postUriToThreadgateUri } from '../../../../util/uris'\nimport { Views } from '../../../../views'\nimport { resHeaders } from '../../../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getPostThread = createPipeline(\n    skeleton,\n    hydration,\n    noRules, // handled in presentation: 3p block-violating replies are turned to #blockedPost, viewer blocks turned to #notFoundPost.\n    presentation,\n  )\n  server.app.bsky.unspecced.getPostThreadV2({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth, req }) => {\n      const { viewer, includeTakedowns, include3pBlocks } =\n        ctx.authVerifier.parseCreds(auth)\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        includeTakedowns,\n        include3pBlocks,\n        features: ctx.featureGatesClient.scope(\n          ctx.featureGatesClient.parseUserContextFromHandler({\n            viewer,\n            req,\n          }),\n        ),\n      })\n\n      return {\n        encoding: 'application/json',\n        body: await getPostThread({ ...params, hydrateCtx }, ctx),\n        headers: resHeaders({\n          labelers: hydrateCtx.labelers,\n        }),\n      }\n    },\n  })\n}\n\nconst skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {\n  const { ctx, params } = inputs\n  const anchor = await ctx.hydrator.resolveUri(params.anchor)\n  try {\n    const res = await ctx.dataplane.getThread({\n      postUri: anchor,\n      above: calculateAbove(ctx, params),\n      below: calculateBelow(ctx, anchor, params),\n    })\n    return {\n      anchor,\n      uris: res.uris,\n    }\n  } catch (err) {\n    if (isDataplaneError(err, Code.NotFound)) {\n      return {\n        anchor,\n        uris: [],\n      }\n    } else {\n      throw err\n    }\n  }\n}\n\nconst hydration = async (\n  inputs: HydrationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton } = inputs\n  return ctx.hydrator.hydrateThreadPosts(\n    skeleton.uris.map((uri) => ({ uri })),\n    params.hydrateCtx,\n  )\n}\n\nconst presentation = (\n  inputs: PresentationFnInput<Context, Params, Skeleton>,\n) => {\n  const { ctx, params, skeleton, hydration } = inputs\n  const { hasOtherReplies, thread } = ctx.views.threadV2(skeleton, hydration, {\n    above: calculateAbove(ctx, params),\n    below: calculateBelow(ctx, skeleton.anchor, params),\n    branchingFactor: params.branchingFactor,\n    sort: params.sort,\n  })\n\n  const rootUri =\n    hydration.posts?.get(skeleton.anchor)?.record.reply?.root.uri ??\n    skeleton.anchor\n  const threadgate = ctx.views.threadgate(\n    postUriToThreadgateUri(rootUri),\n    hydration,\n  )\n  return { hasOtherReplies, thread, threadgate }\n}\n\ntype Context = {\n  dataplane: DataPlaneClient\n  hydrator: Hydrator\n  views: Views\n  cfg: ServerConfig\n}\n\ntype Params = QueryParams & { hydrateCtx: HydrateCtx }\n\ntype Skeleton = {\n  anchor: string\n  uris: string[]\n}\n\nconst calculateAbove = (ctx: Context, params: Params) => {\n  return params.above ? ctx.cfg.maxThreadParents : 0\n}\n\nconst calculateBelow = (ctx: Context, anchor: string, params: Params) => {\n  let maxDepth = ctx.cfg.maxThreadDepth\n  if (ctx.cfg.bigThreadUris.has(anchor) && ctx.cfg.bigThreadDepth) {\n    maxDepth = ctx.cfg.bigThreadDepth\n  }\n  return maxDepth ? Math.min(maxDepth, params.below) : params.below\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getSuggestedFeeds.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { mapDefined, noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getSuggestedFeeds'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  SkeletonFnInput,\n  createPipeline,\n  noRules,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getFeeds = createPipeline(skeleton, hydration, noRules, presentation)\n  server.app.bsky.unspecced.getSuggestedFeeds({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const result = await getFeeds(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (ctx.topicsAgent) {\n    const res =\n      await ctx.topicsAgent.app.bsky.unspecced.getSuggestedFeedsSkeleton(\n        {\n          limit: params.limit,\n          viewer: params.hydrateCtx.viewer ?? undefined,\n        },\n        {\n          headers: params.headers,\n        },\n      )\n\n    return res.data\n  } else {\n    throw new InternalServerError('Topics agent not available')\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  return await ctx.hydrator.hydrateFeedGens(skeleton.feeds, params.hydrateCtx)\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n\n  return {\n    feeds: mapDefined(skeleton.feeds, (uri) =>\n      ctx.views.feedGenerator(uri, hydration),\n    ),\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string | null }\n  headers: Record<string, string>\n}\n\ntype SkeletonState = {\n  feeds: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { dedupeStrs, mapDefined, noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getSuggestedOnboardingUsers'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getSuggestedOnboardingUsers = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrFollows,\n    presentation,\n  )\n  server.app.bsky.unspecced.getSuggestedOnboardingUsers({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n      })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const result = await getSuggestedOnboardingUsers(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (!ctx.suggestionsAgent)\n    throw new InternalServerError('Suggestions agent not available')\n\n  const res =\n    await ctx.suggestionsAgent.app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton(\n      {\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n        category: params.category,\n      },\n      {\n        headers: params.headers,\n      },\n    )\n\n  return res.data\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const dids = dedupeStrs(skeleton.dids)\n  const pairs: Map<string, string[]> = new Map()\n  const viewer = params.hydrateCtx.viewer\n  if (viewer) {\n    pairs.set(viewer, dids)\n  }\n  const [profilesState, bidirectionalBlocks] = await Promise.all([\n    ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx),\n    ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx),\n  ])\n\n  return mergeManyStates(profilesState, { bidirectionalBlocks })\n}\n\nconst noBlocksOrFollows = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, params, hydration } = input\n  const viewer = params.hydrateCtx.viewer\n  if (!viewer) {\n    return skeleton\n  }\n  const blocks = hydration.bidirectionalBlocks?.get(viewer)\n  return {\n    ...skeleton,\n    dids: skeleton.dids.filter((did) => {\n      const viewer = ctx.views.profileViewer(did, hydration)\n      return !blocks?.get(did) && !viewer?.following\n    }),\n  }\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  return {\n    recId: skeleton.recId,\n    recIdStr: skeleton.recIdStr,\n    actors: mapDefined(skeleton.dids, (did) =>\n      ctx.views.profile(did, hydration),\n    ),\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n  suggestionsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string | null }\n  headers: Record<string, string>\n  category?: string\n}\n\ntype SkeletonState = {\n  dids: string[]\n  recId?: string\n  recIdStr?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getSuggestedStarterPacks.ts",
    "content": "import AtpAgent, { AtUri } from '@atproto/api'\nimport { dedupeStrs, mapDefined, noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getSuggestedStarterPacks = createPipeline(\n    skeleton,\n    hydration,\n    noBlocks,\n    presentation,\n  )\n  server.app.bsky.unspecced.getSuggestedStarterPacks({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const result = await getSuggestedStarterPacks(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (ctx.topicsAgent) {\n    const res =\n      await ctx.topicsAgent.app.bsky.unspecced.getSuggestedStarterPacksSkeleton(\n        {\n          limit: params.limit,\n          viewer: params.hydrateCtx.viewer ?? undefined,\n        },\n        {\n          headers: params.headers,\n        },\n      )\n\n    return res.data\n  } else {\n    throw new InternalServerError('Topics agent not available')\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  let dids: string[] = []\n  for (const uri of skeleton.starterPacks) {\n    let aturi: AtUri | undefined\n    try {\n      aturi = new AtUri(uri)\n    } catch {\n      continue\n    }\n    dids.push(aturi.hostname)\n  }\n  dids = dedupeStrs(dids)\n  const pairs: Map<string, string[]> = new Map()\n  const viewer = params.hydrateCtx.viewer\n  if (viewer) {\n    pairs.set(viewer, dids)\n  }\n  const [starterPacksState, bidirectionalBlocks] = await Promise.all([\n    ctx.hydrator.hydrateStarterPacks(skeleton.starterPacks, params.hydrateCtx),\n    ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx),\n  ])\n\n  return mergeManyStates(starterPacksState, { bidirectionalBlocks })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, params, hydration } = input\n  const viewer = params.hydrateCtx.viewer\n  if (!viewer) {\n    return skeleton\n  }\n\n  const blocks = hydration.bidirectionalBlocks?.get(viewer)\n  const filteredSkeleton: SkeletonState = {\n    starterPacks: skeleton.starterPacks.filter((uri) => {\n      let aturi: AtUri | undefined\n      try {\n        aturi = new AtUri(uri)\n      } catch {\n        return false\n      }\n      return !blocks?.get(aturi.hostname)\n    }),\n  }\n\n  return filteredSkeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n\n  return {\n    starterPacks: mapDefined(skeleton.starterPacks, (uri) =>\n      ctx.views.starterPack(uri, hydration),\n    ),\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string | null }\n  headers: Record<string, string>\n}\n\ntype SkeletonState = {\n  starterPacks: string[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getSuggestedUsers.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { dedupeStrs, mapDefined, noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getSuggestedUsers'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getSuggestedUsers = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrFollows,\n    presentation,\n  )\n  server.app.bsky.unspecced.getSuggestedUsers({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({\n        labelers,\n        viewer,\n        features: ctx.featureGatesClient.scope(\n          ctx.featureGatesClient.parseUserContextFromHandler({\n            viewer,\n            req,\n          }),\n        ),\n      })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const result = await getSuggestedUsers(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\n// TODO: rename to `skeleton` once we can fully migrate to Discover\nconst skeletonFromDiscover = async (\n  input: SkeletonFnInput<Context, Params>,\n) => {\n  const { params, ctx } = input\n  if (!ctx.suggestionsAgent)\n    throw new InternalServerError('Suggestions agent not available')\n\n  const res =\n    await ctx.suggestionsAgent.app.bsky.unspecced.getSuggestedUsersSkeleton(\n      {\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n        category: params.category,\n      },\n      {\n        headers: params.headers,\n      },\n    )\n\n  return res.data\n}\n\nconst skeletonFromTopics = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (!ctx.topicsAgent)\n    throw new InternalServerError('Topics agent not available')\n\n  const res =\n    await ctx.topicsAgent.app.bsky.unspecced.getSuggestedUsersSkeleton(\n      {\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n        category: params.category,\n      },\n      {\n        headers: params.headers,\n      },\n    )\n\n  return res.data\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const useDiscover = input.params.hydrateCtx.features.checkGate(\n    input.params.hydrateCtx.features.Gate.SuggestedUsersDiscoverEnable,\n  )\n  const skeletonFn = useDiscover ? skeletonFromDiscover : skeletonFromTopics\n  return skeletonFn(input)\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  const dids = dedupeStrs(skeleton.dids)\n  const pairs: Map<string, string[]> = new Map()\n  const viewer = params.hydrateCtx.viewer\n  if (viewer) {\n    pairs.set(viewer, dids)\n  }\n  const [profilesState, bidirectionalBlocks] = await Promise.all([\n    ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx),\n    ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx),\n  ])\n\n  return mergeManyStates(profilesState, { bidirectionalBlocks })\n}\n\nconst noBlocksOrFollows = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, params, hydration } = input\n  const viewer = params.hydrateCtx.viewer\n  if (!viewer) {\n    return skeleton\n  }\n  const blocks = hydration.bidirectionalBlocks?.get(viewer)\n  return {\n    ...skeleton,\n    dids: skeleton.dids.filter((did) => {\n      const viewer = ctx.views.profileViewer(did, hydration)\n      return !blocks?.get(did) && !viewer?.following\n    }),\n  }\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n  return {\n    recId: skeleton.recId,\n    recIdStr: skeleton.recIdStr,\n    actors: mapDefined(skeleton.dids, (did) =>\n      ctx.views.profile(did, hydration),\n    ),\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n  suggestionsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string | null }\n  headers: Record<string, string>\n  category?: string\n}\n\ntype SkeletonState = {\n  dids: string[]\n  recId?: string\n  recIdStr?: string\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getTaggedSuggestions.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\n// THIS IS A TEMPORARY UNSPECCED ROUTE\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.unspecced.getTaggedSuggestions({\n    handler: async () => {\n      const res = await ctx.dataplane.getSuggestedEntities({})\n      const suggestions = res.entities.map((entity) => ({\n        tag: entity.tag,\n        subjectType: entity.subjectType,\n        subject: entity.subject,\n      }))\n      return {\n        encoding: 'application/json',\n        body: {\n          suggestions,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getTrendingTopics.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { TrendingTopic } from '../../../../lexicon/types/app/bsky/unspecced/defs'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getTrendingTopics'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getTrendingTopics = createPipeline(\n    skeleton,\n    hydration,\n    noBlocksOrMutes,\n    presentation,\n  )\n  server.app.bsky.unspecced.getTrendingTopics({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n      })\n      const result = await getTrendingTopics(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (ctx.topicsAgent) {\n    const res = await ctx.topicsAgent.app.bsky.unspecced.getTrendingTopics(\n      {\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n      },\n      {\n        headers: params.headers,\n      },\n    )\n    return res.data\n  } else {\n    throw new InternalServerError('Topics agent not available')\n  }\n}\n\nconst hydration = async (\n  _: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  return {}\n}\n\nconst noBlocksOrMutes = (\n  input: RulesFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton } = input\n  return skeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { skeleton } = input\n  return skeleton\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n}\n\ntype Params = Omit<QueryParams, 'viewer'> & {\n  hydrateCtx: HydrateCtx\n  headers: Record<string, string>\n}\n\ntype SkeletonState = {\n  topics: TrendingTopic[]\n  suggested: TrendingTopic[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/getTrends.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { dedupeStrs, mapDefined, noUndefinedVals } from '@atproto/common'\nimport { InternalServerError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport {\n  HydrateCtx,\n  Hydrator,\n  mergeManyStates,\n} from '../../../../hydration/hydrator'\nimport { Server } from '../../../../lexicon'\nimport { SkeletonTrend } from '../../../../lexicon/types/app/bsky/unspecced/defs'\nimport { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getTrends'\nimport {\n  HydrationFnInput,\n  PresentationFnInput,\n  RulesFnInput,\n  SkeletonFnInput,\n  createPipeline,\n} from '../../../../pipeline'\nimport { Views } from '../../../../views'\n\nexport default function (server: Server, ctx: AppContext) {\n  const getTrends = createPipeline(skeleton, hydration, noBlocks, presentation)\n  server.app.bsky.unspecced.getTrends({\n    auth: ctx.authVerifier.standardOptional,\n    handler: async ({ auth, params, req }) => {\n      const viewer = auth.credentials.iss\n      const labelers = ctx.reqLabelers(req)\n      const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer })\n      const headers = noUndefinedVals({\n        'accept-language': req.headers['accept-language'],\n        'x-bsky-topics': Array.isArray(req.headers['x-bsky-topics'])\n          ? req.headers['x-bsky-topics'].join(',')\n          : req.headers['x-bsky-topics'],\n      })\n      const result = await getTrends(\n        {\n          ...params,\n          hydrateCtx,\n          headers,\n        },\n        ctx,\n      )\n      return {\n        encoding: 'application/json',\n        body: result,\n      }\n    },\n  })\n}\n\nconst skeleton = async (input: SkeletonFnInput<Context, Params>) => {\n  const { params, ctx } = input\n  if (ctx.topicsAgent) {\n    const res = await ctx.topicsAgent.app.bsky.unspecced.getTrendsSkeleton(\n      {\n        limit: params.limit,\n        viewer: params.hydrateCtx.viewer ?? undefined,\n      },\n      {\n        headers: params.headers,\n      },\n    )\n    return res.data\n  } else {\n    throw new InternalServerError('Topics agent not available')\n  }\n}\n\nconst hydration = async (\n  input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, params, skeleton } = input\n  let dids: string[] = []\n  for (const trend of skeleton.trends) {\n    dids.push(...trend.dids)\n  }\n  dids = dedupeStrs(dids)\n  const pairs: Map<string, string[]> = new Map()\n  const viewer = params.hydrateCtx.viewer\n  if (viewer) {\n    pairs.set(viewer, dids)\n  }\n  const [profileState, bidirectionalBlocks] = await Promise.all([\n    ctx.hydrator.hydrateProfilesBasic(dids, params.hydrateCtx),\n    ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx),\n  ])\n\n  return mergeManyStates(profileState, { bidirectionalBlocks })\n}\n\nconst noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {\n  const { skeleton, params, hydration } = input\n  const viewer = params.hydrateCtx.viewer\n  if (!viewer) {\n    return skeleton\n  }\n\n  const blocks = hydration.bidirectionalBlocks?.get(viewer)\n  const filteredSkeleton: SkeletonState = {\n    trends: skeleton.trends.map((t) => ({\n      ...t,\n      dids: t.dids.filter((did) => !blocks?.get(did)),\n    })),\n  }\n\n  return filteredSkeleton\n}\n\nconst presentation = (\n  input: PresentationFnInput<Context, Params, SkeletonState>,\n) => {\n  const { ctx, skeleton, hydration } = input\n\n  return {\n    trends: skeleton.trends.map((t) => ({\n      topic: t.topic,\n      displayName: t.displayName,\n      link: t.link,\n      startedAt: t.startedAt,\n      postCount: t.postCount,\n      status: t.status,\n      category: t.category,\n      actors: mapDefined(t.dids, (did) =>\n        ctx.views.profileBasic(did, hydration),\n      ),\n    })),\n  }\n}\n\ntype Context = {\n  hydrator: Hydrator\n  views: Views\n  topicsAgent: AtpAgent | undefined\n}\n\ntype Params = QueryParams & {\n  hydrateCtx: HydrateCtx & { viewer: string | null }\n  headers: Record<string, string>\n}\n\ntype SkeletonState = {\n  trends: SkeletonTrend[]\n}\n"
  },
  {
    "path": "packages/bsky/src/api/app/bsky/unspecced/initAgeAssurance.ts",
    "content": "import crypto from 'node:crypto'\nimport { isEmailValid } from '@hapi/address'\nimport { isDisposableEmail } from 'disposable-email-domains-js'\nimport {\n  InvalidRequestError,\n  MethodNotImplementedError,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { KwsExternalPayloadError } from '../../../../kws'\nimport { Server } from '../../../../lexicon'\nimport { InputSchema } from '../../../../lexicon/types/app/bsky/unspecced/initAgeAssurance'\nimport { httpLogger as log } from '../../../../logger'\nimport { ActorInfo } from '../../../../proto/bsky_pb'\nimport { KwsExternalPayload } from '../../../kws/types'\nimport { createStashEvent, getClientUa } from '../../../kws/util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.unspecced.initAgeAssurance({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ auth, input, req }) => {\n      if (!ctx.kwsClient) {\n        throw new MethodNotImplementedError(\n          'This service is not configured to support age assurance.',\n        )\n      }\n\n      const actorDid = auth.credentials.iss\n\n      const actorInfo = await getAgeVerificationState(ctx, actorDid)\n\n      if (actorInfo?.ageAssuranceStatus) {\n        if (\n          actorInfo.ageAssuranceStatus.status !== 'unknown' &&\n          actorInfo.ageAssuranceStatus.status !== 'pending'\n        ) {\n          throw new InvalidRequestError(\n            `Cannot initiate age assurance flow from current state: ${actorInfo.ageAssuranceStatus.status}`,\n            'InvalidInitiation',\n          )\n        }\n      }\n\n      const { countryCode, email, language } = validateInput(input.body)\n\n      const attemptId = crypto.randomUUID()\n      // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.\n      const initIp = req.ip\n      const initUa = getClientUa(req)\n      const externalPayload: KwsExternalPayload = { actorDid, attemptId }\n\n      try {\n        await ctx.kwsClient.sendEmail({\n          countryCode: countryCode.toUpperCase(),\n          email,\n          externalPayload,\n          language,\n        })\n      } catch (err) {\n        if (err instanceof KwsExternalPayloadError) {\n          log.error(\n            { externalPayload },\n            'Age Assurance flow failed because external payload got too long, which is caused by the DID being too long',\n          )\n          throw new InvalidRequestError(\n            'Age Assurance flow failed because DID is too long',\n            'DidTooLong',\n          )\n        }\n        throw err\n      }\n\n      const event = await createStashEvent(ctx, {\n        actorDid,\n        attemptId,\n        email,\n        initIp,\n        initUa,\n        status: 'pending',\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          status: event.status,\n          lastInitiatedAt: event.createdAt,\n        },\n      }\n    },\n  })\n}\n\n// Supported languages for KWS Adult Verification.\n// This list comes from KWS's AV Developer Guide PDF doc.\nconst kwsAvSupportedLanguages = [\n  'en',\n  'ar',\n  'zh-Hans',\n  'nl',\n  'tl',\n  'fr',\n  'de',\n  'id',\n  'it',\n  'ja',\n  'ko',\n  'pl',\n  'pt-BR',\n  'pt',\n  'ru',\n  'es',\n  'th',\n  'tr',\n  'vi',\n]\n\nconst validateInput = (input: InputSchema): InputSchema => {\n  const { countryCode, email, language } = input\n\n  if (!isEmailValid(email) || isDisposableEmail(email)) {\n    throw new InvalidRequestError(\n      'This email address is not supported, please use a different email.',\n      'InvalidEmail',\n    )\n  }\n\n  return {\n    countryCode,\n    email,\n    language: kwsAvSupportedLanguages.includes(language) ? language : 'en',\n  }\n}\n\nconst getAgeVerificationState = async (\n  ctx: AppContext,\n  actorDid: string,\n): Promise<ActorInfo | undefined> => {\n  try {\n    const res = await ctx.dataplane.getActors({\n      dids: [actorDid],\n      returnAgeAssuranceForDids: [actorDid],\n      skipCacheForDids: [actorDid],\n    })\n\n    return res.actors[0]\n  } catch (err) {\n    return undefined\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/blob-dispatcher.ts",
    "content": "import { Agent, Dispatcher, Pool, RetryAgent } from 'undici'\nimport { isUnicastIp, unicastLookup } from '@atproto-labs/fetch-node'\nimport { ServerConfig } from '../config'\nimport { RETRYABLE_HTTP_STATUS_CODES } from '../util/retry'\n\nexport function createBlobDispatcher(cfg: ServerConfig): Dispatcher {\n  const baseDispatcher = new Agent({\n    allowH2: cfg.proxyAllowHTTP2, // This is experimental\n    headersTimeout: cfg.proxyHeadersTimeout,\n    maxResponseSize: cfg.proxyMaxResponseSize,\n    bodyTimeout: cfg.proxyBodyTimeout,\n    factory: cfg.disableSsrfProtection\n      ? undefined\n      : (origin, opts) => {\n          const { protocol, hostname } =\n            origin instanceof URL ? origin : new URL(origin)\n          if (protocol !== 'https:') {\n            throw new Error(`Forbidden protocol \"${protocol}\"`)\n          }\n          if (isUnicastIp(hostname) === false) {\n            throw new Error('Hostname resolved to non-unicast address')\n          }\n          return new Pool(origin, opts)\n        },\n    connect: {\n      lookup: cfg.disableSsrfProtection ? undefined : unicastLookup,\n    },\n  })\n\n  return cfg.proxyMaxRetries > 0\n    ? new RetryAgent(baseDispatcher, {\n        statusCodes: [...RETRYABLE_HTTP_STATUS_CODES],\n        methods: ['GET', 'HEAD'],\n        maxRetries: cfg.proxyMaxRetries,\n      })\n    : baseDispatcher\n}\n"
  },
  {
    "path": "packages/bsky/src/api/blob-resolver.ts",
    "content": "import { Duplex, Transform, Writable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport createError, { isHttpError } from 'http-errors'\nimport { CID } from 'multiformats/cid'\nimport { Dispatcher } from 'undici'\nimport {\n  VerifyCidError,\n  VerifyCidTransform,\n  createDecoders,\n} from '@atproto/common'\nimport { AtprotoDid, isAtprotoDid } from '@atproto/did'\nimport {\n  ACCEPT_ENCODING_COMPRESSED,\n  ACCEPT_ENCODING_UNCOMPRESSED,\n  buildProxiedContentEncoding,\n  formatAcceptHeader,\n} from '@atproto-labs/xrpc-utils'\nimport { ServerConfig } from '../config'\nimport { AppContext } from '../context'\nimport {\n  Code,\n  DataPlaneClient,\n  getServiceEndpoint,\n  isDataplaneError,\n  unpackIdentityServices,\n} from '../data-plane'\nimport { parseCid } from '../hydration/util'\nimport { httpLogger as log } from '../logger'\nimport { Middleware, proxyResponseHeaders, responseSignal } from '../util/http'\nimport { BSKY_USER_AGENT } from './util'\n\nexport function createMiddleware(ctx: AppContext): Middleware {\n  return async (req, res, next) => {\n    if (req.method !== 'GET' && req.method !== 'HEAD') return next()\n    if (!req.url?.startsWith('/blob/')) return next()\n    const { length, 2: didParam, 3: cidParam } = req.url.split('/')\n    if (length !== 4 || !didParam || !cidParam) return next()\n\n    // @TODO Check sec-fetch-* headers (e.g. to prevent files from being\n    // displayed as a web page) ?\n\n    try {\n      const streamOptions: StreamBlobOptions = {\n        did: didParam,\n        cid: cidParam,\n        signal: responseSignal(res),\n        // Because we will be verifying the CID, we need to ensure that the\n        // upstream response can be de-compressed. We do this by negotiating the\n        // \"accept-encoding\" header based on the downstream client's capabilities.\n        acceptEncoding: buildProxiedContentEncoding(\n          req.headers['accept-encoding'],\n          ctx.cfg.proxyPreferCompressed,\n        ),\n      }\n\n      await streamBlob(ctx, streamOptions, (upstream, { cid, did, url }) => {\n        const encoding = upstream.headers['content-encoding']\n        const verifier = createCidVerifier(cid, encoding)\n\n        const logError = (err: unknown) => {\n          log.warn(\n            { err, did, cid: cid.toString(), pds: url.origin },\n            'blob resolution failed during transmission',\n          )\n        }\n\n        const onError = (err: unknown) => {\n          // No need to pipe the data (verifier) into the response, as it is\n          // \"errored\". The response processing will continue in the \"catch\"\n          // block below (because streamBlob() will reject the promise in case\n          // of \"error\" event on the writable stream returned by the factory).\n          clearTimeout(graceTimer)\n          logError(err)\n        }\n\n        // Catch any error that occurs before the timer bellow is triggered.\n        // The promise returned by streamBlob() will be rejected as soon as\n        // the verifier errors.\n        verifier.on('error', onError)\n\n        // The way I/O work, it is likely that, in case of small payloads, the\n        // full upstream response is already buffered at this point. In order to\n        // return a 404 instead of a broken response stream, we allow the event\n        // loop to to process any pending I/O events before we start piping the\n        // bytes to the response. For larger payloads, the response will look\n        // like a 200 with a broken chunked response stream. The only way around\n        // that would be to buffer the entire response before piping it to the\n        // response, which will hurt latency (need the full payload) and memory\n        // usage (either RAM or DISK). Since this is more of an edge case, we\n        // allow the broken response stream to be sent.\n        const graceTimer = setTimeout(() => {\n          verifier.off('error', onError)\n\n          // Make sure that the content served from the bsky api domain cannot\n          // be used to perform XSS attacks (by serving HTML pages)\n          res.setHeader(\n            'Content-Security-Policy',\n            `default-src 'none'; sandbox`,\n          )\n          res.setHeader('X-Content-Type-Options', 'nosniff')\n          res.setHeader('X-Frame-Options', 'DENY')\n          res.setHeader('X-XSS-Protection', '0')\n\n          // @TODO Add a cache-control header ?\n          // @TODO Add content-disposition header (to force download) ?\n\n          proxyResponseHeaders(upstream, res)\n\n          // Force chunked encoding. This is required because the verifier will\n          // trigger an error *after* the last chunk has been passed through.\n          // Because the number of bytes sent will match the content-length, the\n          // HTTP response will be considered \"complete\" by the HTTP server. At\n          // this point, only trailers headers could indicate that an error\n          // occurred, but that is not the behavior we expect.\n          res.removeHeader('content-length')\n\n          // From this point on, triggering the next middleware (including any\n          // error handler) can be problematic because content-type,\n          // content-encoding, etc. headers have already been set. Because of\n          // this, we make sure that res.headersSent is set to true, preventing\n          // another error handler middleware from being called (from the catch\n          // block bellow). Not flushing the headers here would require to\n          // revert the headers set from this middleware (which we don't do for\n          // now).\n          res.flushHeaders()\n\n          // Pipe the verifier output into the HTTP response\n          void pipeline([verifier, res]).catch(logError)\n        }, 10) // 0 works too. Allow for additional data to come in for 10ms.\n\n        // Write the upstream response into the verifier.\n        return verifier\n      })\n    } catch (err) {\n      if (res.headersSent || res.destroyed) {\n        res.destroy()\n      } else if (err instanceof VerifyCidError) {\n        // @NOTE This only works because of the graceTimer above. It will also\n        // only be triggered for small payloads.\n        next(createError(404, err.message))\n      } else if (isHttpError(err)) {\n        next(err)\n      } else {\n        next(createError(502, 'Upstream Error', { cause: err }))\n      }\n    }\n  }\n}\n\nexport type StreamBlobOptions = {\n  cid: string\n  did: string\n  acceptEncoding?: string\n  signal?: AbortSignal\n}\n\nexport type StreamBlobFactory = (\n  data: Dispatcher.StreamFactoryData,\n  info: {\n    url: URL\n    did: AtprotoDid\n    cid: CID\n  },\n) => Writable\n\nexport async function streamBlob(\n  ctx: AppContext,\n  options: StreamBlobOptions,\n  factory: StreamBlobFactory,\n) {\n  const { did, cid } = parseBlobParams(options)\n  const url = await getBlobUrl(ctx.dataplane, did, cid)\n\n  const headers = getBlobHeaders(ctx.cfg, url)\n\n  headers.set(\n    'accept-encoding',\n    options.acceptEncoding ||\n      formatAcceptHeader(\n        ctx.cfg.proxyPreferCompressed\n          ? ACCEPT_ENCODING_COMPRESSED\n          : ACCEPT_ENCODING_UNCOMPRESSED,\n      ),\n  )\n\n  let headersReceived = false\n\n  return ctx.blobDispatcher\n    .stream(\n      {\n        method: 'GET',\n        origin: url.origin,\n        path: url.pathname + url.search,\n        headers,\n        signal: options.signal,\n        maxRedirections: 10,\n      },\n      (upstream) => {\n        headersReceived = true\n\n        if (upstream.statusCode !== 200) {\n          log.warn(\n            {\n              did,\n              cid: cid.toString(),\n              pds: url.origin,\n              status: upstream.statusCode,\n            },\n            `blob resolution failed upstream`,\n          )\n\n          throw upstream.statusCode >= 400 && upstream.statusCode < 500\n            ? createError(404, 'Blob not found', { cause: upstream }) // 4xx => 404\n            : createError(502, 'Upstream Error', { cause: upstream }) // !200 && !4xx => 502\n        }\n\n        return factory(upstream, { url, did, cid })\n      },\n    )\n    .catch((err) => {\n      // Is this a connection error, or a stream error ?\n      if (!headersReceived) {\n        // connection error, dns error, headers timeout, ...\n        log.warn(\n          { err, did, cid: cid.toString(), pds: url.origin },\n          'blob resolution failed during connection',\n        )\n\n        throw createError(502, 'Upstream Error', { cause: err })\n      }\n\n      throw err\n    })\n}\n\nfunction parseBlobParams(params: { cid: string; did: string }) {\n  const { cid, did } = params\n  if (!isAtprotoDid(did)) throw createError(400, 'Invalid did')\n  const cidObj = parseCid(cid)\n  if (!cidObj) throw createError(400, 'Invalid cid')\n  return { cid: cidObj, did }\n}\n\nasync function getBlobUrl(\n  dataplane: DataPlaneClient,\n  did: string,\n  cid: CID,\n): Promise<URL> {\n  const pds = await getBlobPds(dataplane, did, cid)\n\n  const url = new URL(`/xrpc/com.atproto.sync.getBlob`, pds)\n  url.searchParams.set('did', did)\n  url.searchParams.set('cid', cid.toString())\n\n  return url\n}\n\nasync function getBlobPds(\n  dataplane: DataPlaneClient,\n  did: string,\n  cid: CID,\n): Promise<string> {\n  const [identity, { takenDown }] = await Promise.all([\n    dataplane.getIdentityByDid({ did }).catch((err) => {\n      if (isDataplaneError(err, Code.NotFound)) {\n        return undefined\n      }\n      throw err\n    }),\n    dataplane.getBlobTakedown({ did, cid: cid.toString() }),\n  ])\n\n  if (takenDown) {\n    throw createError(404, 'Blob not found')\n  }\n\n  const services = identity && unpackIdentityServices(identity.services)\n  const pds =\n    services &&\n    getServiceEndpoint(services, {\n      id: 'atproto_pds',\n      type: 'AtprotoPersonalDataServer',\n    })\n\n  if (!pds) {\n    throw createError(404, 'Origin not found')\n  }\n\n  return pds\n}\n\nfunction getBlobHeaders(\n  {\n    blobRateLimitBypassKey: bypassKey,\n    blobRateLimitBypassHostname: bypassHostname,\n  }: ServerConfig,\n  url: URL,\n): Map<string, string> {\n  const headers = new Map<string, string>()\n\n  headers.set('user-agent', BSKY_USER_AGENT)\n\n  if (bypassKey && bypassHostname) {\n    const matchesUrl = bypassHostname.startsWith('.')\n      ? url.hostname.endsWith(bypassHostname)\n      : url.hostname === bypassHostname\n\n    if (matchesUrl) {\n      headers.set('x-ratelimit-bypass', bypassKey)\n    }\n  }\n\n  return headers\n}\n\n/**\n * This function creates a passthrough stream that will decompress (if needed)\n * and verify the CID of the input stream. The output data will be identical to\n * the input data.\n *\n * If you need the un-compressed data, you should use a decompress + verify\n * pipeline instead.\n */\nfunction createCidVerifier(cid: CID, encoding?: string | string[]): Duplex {\n  // If the upstream content is compressed, we do not want to return a\n  // de-compressed stream here. Indeed, the \"compression\" middleware will\n  // compress the response before it is sent downstream, if it is not already\n  // compressed. Because of this, it is preferable to return the content as-is\n  // to avoid re-compressing it.\n  //\n  // We do still want to be able to verify the CID, which requires decompressing\n  // the input bytes.\n  //\n  // To that end, we create a passthrough in order to \"tee\" the stream into two\n  // streams: one that will be sent, unaltered, downstream, and a pipeline that\n  // will be used to decompress & verify the CID (discarding de-compressed\n  // data).\n\n  const decoders = createDecoders(encoding)\n  const verifier = new VerifyCidTransform(cid)\n\n  // Optimization: If the content is not compressed, we don't need to \"tee\" the\n  // stream, we can use the verifier as simple passthrough.\n  if (!decoders.length) return verifier\n\n  const pipelineController = new AbortController()\n  const pipelineStreams: Duplex[] = [...decoders, verifier]\n  const pipelineInput = pipelineStreams[0]!\n\n  // Create a promise that will resolve if, and only if, the decoding and\n  // verification succeed.\n  const pipelinePromise: Promise<null | Error> = pipeline(pipelineStreams, {\n    signal: pipelineController.signal,\n  }).then(\n    () => null,\n    (err) => {\n      const error = asError(err)\n\n      // the data being processed by the pipeline is invalid (e.g. invalid\n      // compressed content, non-matching the CID, ...). If that occurs, we can\n      // destroy the passthrough (this allows not to wait for the \"flush\" event\n      // to propagate the error).\n      passthrough.destroy(error)\n\n      return error\n    },\n  )\n\n  // We don't care about the un-compressed data, we only use the verifier to\n  // detect any error through the pipelinePromise. We still need to pass the\n  // verifier into flowing mode to ensure that the pipelinePromise resolves.\n  verifier.resume()\n\n  const passthrough = new Transform({\n    transform(chunk, encoding, callback) {\n      pipelineInput.write(chunk, encoding)\n      callback(null, chunk)\n    },\n    flush(callback) {\n      // End the input stream, which will resolve the pipeline promise\n      pipelineInput.end()\n      // End the pass-through stream according to the result of the pipeline\n      pipelinePromise.then(callback)\n    },\n    destroy(err, callback) {\n      pipelineController.abort() // Causes pipeline() to destroy all streams\n      callback(err)\n    },\n  })\n\n  return passthrough\n}\n\nfunction asError(err: unknown): Error {\n  return err instanceof Error\n    ? err\n    : new Error('Processing failed', { cause: err })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { INVALID_HANDLE } from '@atproto/syntax'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.getAccountInfos({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ params, auth }) => {\n      const { dids } = params\n      const { includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n\n      const actors = await ctx.hydrator.actor.getActors(dids, {\n        includeTakedowns: true,\n        skipCacheForDids: dids,\n      })\n\n      const infos = mapDefined(dids, (did) => {\n        const info = actors.get(did)\n        if (!info) return\n        if (info.takedownRef && !includeTakedowns) return\n        const profileRecord =\n          !info.profileTakedownRef || includeTakedowns\n            ? info.profile\n            : undefined\n\n        return {\n          did,\n          handle: info.handle ?? INVALID_HANDLE,\n          relatedRecords: profileRecord ? [profileRecord] : undefined,\n          indexedAt: (info.sortedAt ?? new Date(0)).toISOString(),\n        }\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          infos,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSubjectStatus'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.getSubjectStatus({\n    auth: ctx.authVerifier.roleOrModService,\n    handler: async ({ params }) => {\n      const { did, uri, blob } = params\n\n      let body: OutputSchema | null = null\n      if (blob) {\n        if (!did) {\n          throw new InvalidRequestError(\n            'Must provide a did to request blob state',\n          )\n        }\n        const res = await ctx.dataplane.getBlobTakedown({\n          did,\n          cid: blob,\n        })\n        body = {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoBlobRef',\n            did: did,\n            cid: blob,\n          },\n          takedown: {\n            applied: res.takenDown,\n            ref: res.takedownRef ? 'TAKEDOWN' : undefined,\n          },\n        }\n      } else if (uri) {\n        const res = await ctx.hydrator.getRecord(uri, true)\n        if (res) {\n          body = {\n            subject: {\n              $type: 'com.atproto.repo.strongRef',\n              uri,\n              cid: res.cid,\n            },\n            takedown: {\n              applied: !!res.takedownRef,\n              ref: res.takedownRef || undefined,\n            },\n          }\n        }\n      } else if (did) {\n        const res = (\n          await ctx.hydrator.actor.getActors([did], {\n            includeTakedowns: true,\n            skipCacheForDids: [did],\n          })\n        ).get(did)\n        if (res) {\n          body = {\n            subject: {\n              $type: 'com.atproto.admin.defs#repoRef',\n              did: did,\n            },\n            takedown: {\n              applied: !!res.takedownRef,\n              ref: res.takedownRef || undefined,\n            },\n          }\n        }\n      } else {\n        throw new InvalidRequestError('No provided subject')\n      }\n      if (body === null) {\n        throw new InvalidRequestError('Subject not found', 'NotFound')\n      }\n      return {\n        encoding: 'application/json',\n        body,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport {\n  isRepoBlobRef,\n  isRepoRef,\n} from '../../../../lexicon/types/com/atproto/admin/defs'\nimport { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.updateSubjectStatus({\n    auth: ctx.authVerifier.roleOrModService,\n    handler: async ({ input, auth }) => {\n      const { canPerformTakedown } = ctx.authVerifier.parseCreds(auth)\n      if (!canPerformTakedown) {\n        throw new AuthRequiredError(\n          'Must be a full moderator to update subject state',\n        )\n      }\n      const now = new Date()\n      const { subject, takedown } = input.body\n      if (takedown) {\n        if (isRepoRef(subject)) {\n          if (takedown.applied) {\n            await ctx.dataplane.takedownActor({\n              did: subject.did,\n              ref: takedown.ref,\n              seen: Timestamp.fromDate(now),\n            })\n          } else {\n            await ctx.dataplane.untakedownActor({\n              did: subject.did,\n              seen: Timestamp.fromDate(now),\n            })\n          }\n        } else if (isStrongRef(subject)) {\n          if (takedown.applied) {\n            await ctx.dataplane.takedownRecord({\n              recordUri: subject.uri,\n              ref: takedown.ref,\n              seen: Timestamp.fromDate(now),\n            })\n          } else {\n            await ctx.dataplane.untakedownRecord({\n              recordUri: subject.uri,\n              seen: Timestamp.fromDate(now),\n            })\n          }\n        } else if (isRepoBlobRef(subject)) {\n          if (takedown.applied) {\n            await ctx.dataplane.takedownBlob({\n              did: subject.did,\n              cid: subject.cid,\n              ref: takedown.ref,\n              seen: Timestamp.fromDate(now),\n            })\n          } else {\n            await ctx.dataplane.untakedownBlob({\n              did: subject.did,\n              cid: subject.cid,\n              seen: Timestamp.fromDate(now),\n            })\n          }\n        } else {\n          throw new InvalidRequestError('Invalid subject')\n        }\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          subject,\n          takedown,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/identity/resolveHandle.ts",
    "content": "import * as ident from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.resolveHandle(async ({ req, params }) => {\n    const handle = ident.normalizeHandle(params.handle || req.hostname)\n\n    const [did] = await ctx.hydrator.actor.getDids([handle], {\n      lookupUnidirectional: true,\n    })\n    if (!did) {\n      throw new InvalidRequestError('Unable to resolve handle')\n    }\n\n    return {\n      encoding: 'application/json',\n      body: { did },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/label/queryLabels.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.label.queryLabels(async ({ params }) => {\n    const { uriPatterns, sources } = params\n\n    if (uriPatterns.find((uri) => uri.includes('*'))) {\n      throw new InvalidRequestError('wildcards not supported')\n    }\n\n    if (!sources?.length) {\n      throw new InvalidRequestError('source dids are required')\n    }\n\n    const labelMap = await ctx.hydrator.label.getLabelsForSubjects(\n      uriPatterns,\n      // If sources are provided, use them. Otherwise, use the labelers from the request header\n      {\n        dids: sources,\n        redact: new Set(),\n      },\n    )\n    const labels = uriPatterns.flatMap((uri) => labelMap.getBySubject(uri))\n\n    return {\n      encoding: 'application/json',\n      body: {\n        cursor: undefined,\n        labels,\n      },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/repo/getRecord.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.getRecord({\n    auth: ctx.authVerifier.optionalStandardOrRole,\n    handler: async ({ auth, params }) => {\n      const { repo, collection, rkey, cid } = params\n      const { includeTakedowns } = ctx.authVerifier.parseCreds(auth)\n      const [did] = await ctx.hydrator.actor.getDids([repo])\n      if (!did) {\n        throw new InvalidRequestError(`Could not find repo: ${repo}`)\n      }\n\n      const actors = await ctx.hydrator.actor.getActors([did], {\n        includeTakedowns,\n      })\n      if (!actors.get(did)) {\n        throw new InvalidRequestError(`Could not find repo: ${repo}`)\n      }\n\n      const uri = AtUri.make(did, collection, rkey).toString()\n      const result = await ctx.hydrator.getRecord(uri, includeTakedowns)\n\n      if (!result || (cid && result.cid !== cid)) {\n        throw new InvalidRequestError(\n          `Could not locate record: ${uri}`,\n          'RecordNotFound',\n        )\n      }\n\n      return {\n        encoding: 'application/json' as const,\n        body: {\n          uri: uri,\n          cid: result.cid,\n          value: result.record,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/com/atproto/temp/fetchLabels.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, _ctx: AppContext) {\n  server.com.atproto.temp.fetchLabels(async (_reqCtx) => {\n    throw new InvalidRequestError('not implemented on dataplane')\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/api/external.ts",
    "content": "import { Router } from 'express'\nimport { AppContext } from '../context'\nimport * as aaApi from './age-assurance'\nimport * as kwsApi from './kws'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  if (ctx.kwsClient) {\n    router.use('/kws', kwsApi.createRouter(ctx))\n    router.use(aaApi.createRouter(ctx))\n  }\n\n  return router\n}\n"
  },
  {
    "path": "packages/bsky/src/api/health.ts",
    "content": "import { Router } from 'express'\nimport { AppContext } from '../context'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  router.get('/', function (req, res) {\n    res.type('text/plain')\n    res.send(`\n  _         _\n | |       | |\n | |__  ___| | ___   _\n | '_ \\\\/ __| |/ / | | |\n | |_) \\\\__ \\\\   <| |_| |\n |_.__/|___/_|\\\\_\\\\\\\\__, |\n                  __/ |\n                 |___/\n\nThis is an AT Protocol Application View (AppView) for the \"bsky.app\" application.\n\nMost API routes are under /xrpc/\n\n      Code: https://github.com/bluesky-social/atproto\n  Protocol: https://atproto.com\n`)\n  })\n\n  router.get('/robots.txt', function (req, res) {\n    res.type('text/plain')\n    res.send(\n      '# Hello Friends!\\n\\n# Crawling the public parts of the API is allowed. HTTP 429 (\"backoff\") status codes are used for rate-limiting. Up to a handful concurrent requests should be ok.\\nUser-agent: *\\nAllow: /',\n    )\n  })\n\n  router.get('/xrpc/_health', async function (req, res) {\n    const { version } = ctx.cfg\n    try {\n      await ctx.dataplane.ping({})\n    } catch (err) {\n      req.log.error({ err }, 'failed health check')\n      return res.status(503).send({ version, error: 'Service Unavailable' })\n    }\n    res.send({ version })\n  })\n\n  return router\n}\n"
  },
  {
    "path": "packages/bsky/src/api/index.ts",
    "content": "import { AppContext } from '../context'\nimport { Server } from '../lexicon'\nimport getProfile from './app/bsky/actor/getProfile'\nimport getProfiles from './app/bsky/actor/getProfiles'\nimport getSuggestions from './app/bsky/actor/getSuggestions'\nimport searchActors from './app/bsky/actor/searchActors'\nimport searchActorsTypeahead from './app/bsky/actor/searchActorsTypeahead'\nimport aaBegin from './app/bsky/ageassurance/begin'\nimport aaGetConfig from './app/bsky/ageassurance/getConfig'\nimport aaGetState from './app/bsky/ageassurance/getState'\nimport createBookmark from './app/bsky/bookmark/createBookmark'\nimport deleteBookmark from './app/bsky/bookmark/deleteBookmark'\nimport getBookmarks from './app/bsky/bookmark/getBookmarks'\nimport dismissMatch from './app/bsky/contact/dismissMatch'\nimport getMatches from './app/bsky/contact/getMatches'\nimport getSyncStatus from './app/bsky/contact/getSyncStatus'\nimport importContacts from './app/bsky/contact/importContacts'\nimport removeData from './app/bsky/contact/removeData'\nimport sendNotification from './app/bsky/contact/sendNotification'\nimport startPhoneVerification from './app/bsky/contact/startPhoneVerification'\nimport verifyPhone from './app/bsky/contact/verifyPhone'\nimport createDraft from './app/bsky/draft/createDraft'\nimport deleteDraft from './app/bsky/draft/deleteDraft'\nimport getDrafts from './app/bsky/draft/getDrafts'\nimport updateDraft from './app/bsky/draft/updateDraft'\nimport getActorFeeds from './app/bsky/feed/getActorFeeds'\nimport getActorLikes from './app/bsky/feed/getActorLikes'\nimport getAuthorFeed from './app/bsky/feed/getAuthorFeed'\nimport getFeed from './app/bsky/feed/getFeed'\nimport getFeedGenerator from './app/bsky/feed/getFeedGenerator'\nimport getFeedGenerators from './app/bsky/feed/getFeedGenerators'\nimport getLikes from './app/bsky/feed/getLikes'\nimport getListFeed from './app/bsky/feed/getListFeed'\nimport getPostThread from './app/bsky/feed/getPostThread'\nimport getPosts from './app/bsky/feed/getPosts'\nimport getQuotes from './app/bsky/feed/getQuotes'\nimport getRepostedBy from './app/bsky/feed/getRepostedBy'\nimport getSuggestedFeeds from './app/bsky/feed/getSuggestedFeeds'\nimport getTimeline from './app/bsky/feed/getTimeline'\nimport searchPosts from './app/bsky/feed/searchPosts'\nimport getActorStarterPacks from './app/bsky/graph/getActorStarterPacks'\nimport getBlocks from './app/bsky/graph/getBlocks'\nimport getFollowers from './app/bsky/graph/getFollowers'\nimport getFollows from './app/bsky/graph/getFollows'\nimport getKnownFollowers from './app/bsky/graph/getKnownFollowers'\nimport getList from './app/bsky/graph/getList'\nimport getListBlocks from './app/bsky/graph/getListBlocks'\nimport getListMutes from './app/bsky/graph/getListMutes'\nimport getLists from './app/bsky/graph/getLists'\nimport getListsWithMembership from './app/bsky/graph/getListsWithMembership'\nimport getMutes from './app/bsky/graph/getMutes'\nimport getRelationships from './app/bsky/graph/getRelationships'\nimport getStarterPack from './app/bsky/graph/getStarterPack'\nimport getStarterPacks from './app/bsky/graph/getStarterPacks'\nimport getStarterPacksWithMembership from './app/bsky/graph/getStarterPacksWithMembership'\nimport getSuggestedFollowsByActor from './app/bsky/graph/getSuggestedFollowsByActor'\nimport muteActor from './app/bsky/graph/muteActor'\nimport muteActorList from './app/bsky/graph/muteActorList'\nimport muteThread from './app/bsky/graph/muteThread'\nimport searchStarterPacks from './app/bsky/graph/searchStarterPacks'\nimport unmuteActor from './app/bsky/graph/unmuteActor'\nimport unmuteActorList from './app/bsky/graph/unmuteActorList'\nimport unmuteThread from './app/bsky/graph/unmuteThread'\nimport getLabelerServices from './app/bsky/labeler/getServices'\nimport getPreferences from './app/bsky/notification/getPreferences'\nimport getUnreadCount from './app/bsky/notification/getUnreadCount'\nimport listActivitySubscriptions from './app/bsky/notification/listActivitySubscriptions'\nimport listNotifications from './app/bsky/notification/listNotifications'\nimport putActivitySubscription from './app/bsky/notification/putActivitySubscription'\nimport putPreferences from './app/bsky/notification/putPreferences'\nimport putPreferencesV2 from './app/bsky/notification/putPreferencesV2'\nimport registerPush from './app/bsky/notification/registerPush'\nimport unregisterPush from './app/bsky/notification/unregisterPush'\nimport updateSeen from './app/bsky/notification/updateSeen'\nimport getAgeAssuranceState from './app/bsky/unspecced/getAgeAssuranceState'\nimport getConfig from './app/bsky/unspecced/getConfig'\nimport getOnboardingSuggestedStarterPacks from './app/bsky/unspecced/getOnboardingSuggestedStarterPacks'\nimport getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators'\nimport getPostThreadOtherV2 from './app/bsky/unspecced/getPostThreadOtherV2'\nimport getPostThreadV2 from './app/bsky/unspecced/getPostThreadV2'\nimport getUnspeccedSuggestedFeeds from './app/bsky/unspecced/getSuggestedFeeds'\nimport getSuggestedOnboardingUsers from './app/bsky/unspecced/getSuggestedOnboardingUsers'\nimport getSuggestedStarterPacks from './app/bsky/unspecced/getSuggestedStarterPacks'\nimport getSuggestedUsers from './app/bsky/unspecced/getSuggestedUsers'\nimport getTaggedSuggestions from './app/bsky/unspecced/getTaggedSuggestions'\nimport getTrendingTopics from './app/bsky/unspecced/getTrendingTopics'\nimport getTrends from './app/bsky/unspecced/getTrends'\nimport initAgeAssurance from './app/bsky/unspecced/initAgeAssurance'\nimport getAccountInfos from './com/atproto/admin/getAccountInfos'\nimport getSubjectStatus from './com/atproto/admin/getSubjectStatus'\nimport updateSubjectStatus from './com/atproto/admin/updateSubjectStatus'\nimport resolveHandle from './com/atproto/identity/resolveHandle'\nimport queryLabels from './com/atproto/label/queryLabels'\nimport getRecord from './com/atproto/repo/getRecord'\nimport fetchLabels from './com/atproto/temp/fetchLabels'\n\nexport * as health from './health'\n\nexport * as wellKnown from './well-known'\n\nexport * as blobResolver from './blob-resolver'\n\nexport * as external from './external'\n\nexport * as sitemap from './sitemap'\n\nexport default function (server: Server, ctx: AppContext) {\n  // app.bsky\n  getTimeline(server, ctx)\n  createBookmark(server, ctx)\n  deleteBookmark(server, ctx)\n  getBookmarks(server, ctx)\n  createDraft(server, ctx)\n  deleteDraft(server, ctx)\n  getDrafts(server, ctx)\n  updateDraft(server, ctx)\n  dismissMatch(server, ctx)\n  getMatches(server, ctx)\n  getSyncStatus(server, ctx)\n  importContacts(server, ctx)\n  removeData(server, ctx)\n  sendNotification(server, ctx)\n  startPhoneVerification(server, ctx)\n  verifyPhone(server, ctx)\n  getActorFeeds(server, ctx)\n  getSuggestedFeeds(server, ctx)\n  getAuthorFeed(server, ctx)\n  getFeed(server, ctx)\n  getFeedGenerator(server, ctx)\n  getFeedGenerators(server, ctx)\n  getLikes(server, ctx)\n  getListFeed(server, ctx)\n  getQuotes(server, ctx)\n  getPostThread(server, ctx)\n  getPostThreadOtherV2(server, ctx)\n  getPostThreadV2(server, ctx)\n  getPosts(server, ctx)\n  searchPosts(server, ctx)\n  getActorLikes(server, ctx)\n  getProfile(server, ctx)\n  getProfiles(server, ctx)\n  getRepostedBy(server, ctx)\n  getActorStarterPacks(server, ctx)\n  getBlocks(server, ctx)\n  getListBlocks(server, ctx)\n  getFollowers(server, ctx)\n  getKnownFollowers(server, ctx)\n  getFollows(server, ctx)\n  getList(server, ctx)\n  getLists(server, ctx)\n  getListsWithMembership(server, ctx)\n  getListMutes(server, ctx)\n  getMutes(server, ctx)\n  getRelationships(server, ctx)\n  getStarterPack(server, ctx)\n  getStarterPacks(server, ctx)\n  getStarterPacksWithMembership(server, ctx)\n  searchStarterPacks(server, ctx)\n  muteActor(server, ctx)\n  unmuteActor(server, ctx)\n  muteActorList(server, ctx)\n  unmuteActorList(server, ctx)\n  muteThread(server, ctx)\n  unmuteThread(server, ctx)\n  getSuggestedFollowsByActor(server, ctx)\n  getTrendingTopics(server, ctx)\n  getTrends(server, ctx)\n  getOnboardingSuggestedStarterPacks(server, ctx)\n  getSuggestedOnboardingUsers(server, ctx)\n  getSuggestedStarterPacks(server, ctx)\n  getSuggestedUsers(server, ctx)\n  getUnspeccedSuggestedFeeds(server, ctx)\n  getLabelerServices(server, ctx)\n  searchActors(server, ctx)\n  searchActorsTypeahead(server, ctx)\n  getSuggestions(server, ctx)\n  getPreferences(server, ctx)\n  getUnreadCount(server, ctx)\n  listActivitySubscriptions(server, ctx)\n  listNotifications(server, ctx)\n  putActivitySubscription(server, ctx)\n  updateSeen(server, ctx)\n  putPreferences(server, ctx)\n  putPreferencesV2(server, ctx)\n  registerPush(server, ctx)\n  unregisterPush(server, ctx)\n  getConfig(server, ctx)\n  getPopularFeedGenerators(server, ctx)\n  getTaggedSuggestions(server, ctx)\n  getAgeAssuranceState(server, ctx)\n  initAgeAssurance(server, ctx)\n  aaGetConfig(server, ctx)\n  aaGetState(server, ctx)\n  aaBegin(server, ctx)\n  // com.atproto\n  getSubjectStatus(server, ctx)\n  updateSubjectStatus(server, ctx)\n  getAccountInfos(server, ctx)\n  resolveHandle(server, ctx)\n  getRecord(server, ctx)\n  fetchLabels(server, ctx)\n  queryLabels(server, ctx)\n  return server\n}\n"
  },
  {
    "path": "packages/bsky/src/api/kws/api.ts",
    "content": "import express, { RequestHandler } from 'express'\nimport { httpLogger as log } from '../../logger'\nimport { AGE_ASSURANCE_CONFIG } from '../age-assurance/const'\nimport {\n  KWSExternalPayloadVersion,\n  parseKWSExternalPayloadV1WithV2Compat,\n} from '../age-assurance/kws/external-payload'\nimport { createEvent } from '../age-assurance/stash'\nimport { computeAgeAssuranceAccessOrThrow } from '../age-assurance/util'\nimport { AppContextWithKwsClient } from './types'\nimport {\n  createStashEvent,\n  getClientUa,\n  parseStatus,\n  validateSignature,\n} from './util'\n\nfunction parseQueryParams(\n  ctx: AppContextWithKwsClient,\n  req: express.Request,\n): {\n  status: string\n  externalPayload: string\n} {\n  try {\n    const status = String(req.query.status)\n    const externalPayload = String(req.query.externalPayload)\n    const signature = String(req.query.signature)\n\n    validateSignature(\n      ctx.cfg.kws.verificationSecret,\n      `${status}:${externalPayload}`,\n      signature,\n    )\n\n    return {\n      status,\n      externalPayload,\n    }\n  } catch (err) {\n    throw new Error('Invalid KWS API request', { cause: err })\n  }\n}\n\nexport const verificationHandler =\n  (ctx: AppContextWithKwsClient): RequestHandler =>\n  async (req: express.Request, res: express.Response) => {\n    let actorDid: string | undefined\n    try {\n      const query = parseQueryParams(ctx, req)\n      const { verified } = parseStatus(query.status)\n      if (!verified) {\n        throw new Error(\n          'Unexpected KWS verification response call with unverified status',\n        )\n      }\n\n      // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.\n      const completeIp = req.ip\n      const completeUa = getClientUa(req)\n      const externalPayload = parseKWSExternalPayloadV1WithV2Compat(\n        query.externalPayload,\n      )\n      actorDid = externalPayload.actorDid\n\n      if (externalPayload.version === KWSExternalPayloadVersion.V2) {\n        const { countryCode, regionCode, attemptId } = externalPayload\n        const { access } = computeAgeAssuranceAccessOrThrow(\n          AGE_ASSURANCE_CONFIG,\n          {\n            countryCode: countryCode,\n            regionCode: regionCode,\n            verifiedMinimumAge: 18, // `adult-verified` is 18+ only\n          },\n        )\n        await createEvent(ctx, actorDid, {\n          attemptId,\n          status: 'assured',\n          access,\n          countryCode,\n          regionCode,\n          completeIp,\n          completeUa,\n        })\n      } else {\n        await createStashEvent(ctx, {\n          actorDid,\n          attemptId: externalPayload.attemptId,\n          status: 'assured',\n          completeIp,\n          completeUa,\n        })\n      }\n\n      return res\n        .status(302)\n        .setHeader(\n          'Location',\n          `${ctx.cfg.kws.redirectUrl}?${new URLSearchParams({ actorDid, result: 'success' })}`,\n        )\n        .end()\n    } catch (err) {\n      log.error({ err }, 'Failed to handle KWS verification response')\n\n      return res\n        .status(302)\n        .setHeader(\n          'Location',\n          `${ctx.cfg.kws.redirectUrl}?${new URLSearchParams({ ...(actorDid ? { actorDid } : {}), result: 'unknown' })}`,\n        )\n        .end()\n    }\n  }\n"
  },
  {
    "path": "packages/bsky/src/api/kws/index.ts",
    "content": "import { Router, raw } from 'express'\nimport { AppContext } from '../../context'\nimport { verificationHandler } from './api'\nimport { AppContextWithKwsClient } from './types'\nimport { webhookAuth, webhookHandler } from './webhook'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  assertAppContextWithAgeAssuranceClient(ctx)\n\n  const router = Router()\n  router.use(raw({ type: 'application/json' }))\n  router.post(\n    '/age-assurance-webhook',\n    webhookAuth({\n      secret: ctx.cfg.kws.webhookSecret,\n    }),\n    webhookHandler(ctx),\n  )\n  router.get('/age-assurance-verification', verificationHandler(ctx))\n  return router\n}\n\nconst assertAppContextWithAgeAssuranceClient: (\n  ctx: AppContext,\n) => asserts ctx is AppContextWithKwsClient = (ctx: AppContext) => {\n  if (!ctx.kwsClient) {\n    throw new Error('Tried to set up KWS router without kwsClient configured.')\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/api/kws/types.ts",
    "content": "import { z } from 'zod'\nimport { KwsConfig, ServerConfig } from '../../config'\nimport { AppContext } from '../../context'\nimport { KwsClient } from '../../kws'\n\nexport type AppContextWithKwsClient = AppContext & {\n  kwsClient: KwsClient\n  cfg: ServerConfig & {\n    kws: KwsConfig\n  }\n}\n\nexport type KwsExternalPayload = {\n  actorDid: string\n  attemptId: string\n}\n\n// `.strict()` because we control the payload structure.\nexport const externalPayloadSchema = z\n  .object({\n    actorDid: z.string(),\n    attemptId: z.string(),\n  })\n  .strict()\n\nexport type KwsStatus = {\n  verified: boolean\n}\n\nexport type KwsVerificationIntermediateQuery = {\n  externalPayload: string\n  status: string\n  signature: string\n}\n\n// Not `.strict()` to avoid breaking if KWS adds fields.\nexport const verificationIntermediateQuerySchema = z.object({\n  externalPayload: z.string(),\n  signature: z.string(),\n  status: z.string(),\n})\n\nexport type KwsVerificationQuery = {\n  externalPayload: KwsExternalPayload\n  signature: string\n  status: KwsStatus\n}\n\nexport type KwsWebhookBody = {\n  payload: {\n    externalPayload: KwsExternalPayload\n    status: KwsStatus\n  }\n}\n\n// Not `.strict()` to avoid breaking if KWS adds fields.\nexport const statusSchema = z.object({\n  verified: z.boolean(),\n})\n\n// Not `.strict()` to avoid breaking if KWS adds fields.\nexport const webhookBodyIntermediateSchema = z.object({\n  payload: z.object({\n    externalPayload: z.string(),\n    status: statusSchema,\n  }),\n})\n"
  },
  {
    "path": "packages/bsky/src/api/kws/util.ts",
    "content": "import crypto from 'node:crypto'\nimport express from 'express'\nimport { TID } from '@atproto/common'\nimport { AppContext } from '../../context'\nimport {\n  AgeAssuranceEvent,\n  AgeAssuranceState,\n} from '../../lexicon/types/app/bsky/unspecced/defs'\nimport { Namespaces } from '../../stash'\nimport {\n  KwsExternalPayload,\n  KwsStatus,\n  externalPayloadSchema,\n  statusSchema,\n} from './types'\n\nexport const createStashEvent = async (\n  ctx: AppContext,\n  {\n    actorDid,\n    attemptId,\n    email,\n    initIp,\n    initUa,\n    completeIp,\n    completeUa,\n    status,\n  }: {\n    actorDid: string\n    attemptId: string\n    email?: string\n    initIp?: string\n    initUa?: string\n    completeIp?: string\n    completeUa?: string\n    status: AgeAssuranceState['status']\n  },\n) => {\n  const stashPayload: AgeAssuranceEvent = {\n    createdAt: new Date().toISOString(),\n    email,\n    status,\n    attemptId,\n    initIp,\n    initUa,\n    completeIp,\n    completeUa,\n  }\n\n  await ctx.stashClient.create({\n    actorDid,\n    namespace: Namespaces.AppBskyUnspeccedDefsAgeAssuranceEvent,\n    key: TID.nextStr(),\n    payload: stashPayload,\n  })\n  return stashPayload\n}\n\nexport const validateSignature = (\n  key: string,\n  data: string,\n  signature: string,\n) => {\n  const expectedSignature = crypto\n    .createHmac('sha256', key)\n    .update(data)\n    .digest('hex')\n\n  const expectedSignatureBuf = Buffer.from(expectedSignature, 'hex')\n  const actualSignatureBuf = Buffer.from(signature, 'hex')\n\n  if (expectedSignatureBuf.length !== actualSignatureBuf.length) {\n    throw new Error(`Signature mismatch`)\n  }\n\n  if (!crypto.timingSafeEqual(expectedSignatureBuf, actualSignatureBuf)) {\n    throw new Error(`Signature mismatch`)\n  }\n}\n\nexport const serializeExternalPayload = (value: KwsExternalPayload): string => {\n  return JSON.stringify(value)\n}\n\nexport const parseExternalPayload = (\n  serialized: string,\n): KwsExternalPayload => {\n  try {\n    const value: unknown = JSON.parse(serialized)\n    return externalPayloadSchema.parse(value)\n  } catch (err) {\n    throw new Error(`Invalid external payload: ${serialized}`, { cause: err })\n  }\n}\n\nexport const parseStatus = (serialized: string): KwsStatus => {\n  try {\n    const value: unknown = JSON.parse(serialized)\n    return statusSchema.parse(value)\n  } catch (err) {\n    throw new Error(`Invalid status: ${serialized}`, { cause: err })\n  }\n}\n\nexport const kwsWwwAuthenticate = (): Record<string, string> => ({\n  'www-authenticate': `Signature realm=\"kws\"`,\n})\n\nexport const getClientUa = (req: express.Request): string | undefined => {\n  return req.headers['user-agent']\n}\n"
  },
  {
    "path": "packages/bsky/src/api/kws/webhook.ts",
    "content": "import express, { RequestHandler } from 'express'\nimport { httpLogger as log } from '../../logger'\nimport { AGE_ASSURANCE_CONFIG } from '../age-assurance/const'\nimport {\n  KWSExternalPayloadVersion,\n  parseKWSExternalPayloadV1WithV2Compat,\n} from '../age-assurance/kws/external-payload'\nimport { createEvent } from '../age-assurance/stash'\nimport { computeAgeAssuranceAccessOrThrow } from '../age-assurance/util'\nimport {\n  AppContextWithKwsClient,\n  KwsWebhookBody,\n  webhookBodyIntermediateSchema,\n} from './types'\nimport { createStashEvent, kwsWwwAuthenticate, validateSignature } from './util'\n\nexport const webhookAuth =\n  ({ secret }: { secret: string }): RequestHandler =>\n  (req: express.Request, res: express.Response, next: express.NextFunction) => {\n    const body: Buffer = req.body\n    const sigHeader = req.headers['x-kws-signature']\n    if (!sigHeader || typeof sigHeader !== 'string') {\n      return res.status(401).header(kwsWwwAuthenticate()).json({\n        success: false,\n        error:\n          'Invalid authentication for KWS webhook: missing signature header',\n      })\n    }\n\n    try {\n      const parts = sigHeader.split(',')\n      const timestamp = parts.find((p) => p.startsWith('t='))?.split('=')[1]\n      const signature = parts.find((p) => p.startsWith('v1='))?.split('=')[1]\n      if (typeof timestamp !== 'string' || typeof signature !== 'string') {\n        throw new Error('Invalid webhook signature format')\n      }\n\n      const data = `${timestamp}.${body}`\n      validateSignature(secret, data, signature)\n      next()\n    } catch (err) {\n      log.error({ err }, 'Invalid KWS webhook signature')\n      return res.status(401).header(kwsWwwAuthenticate()).json({\n        success: false,\n        error: 'Invalid authentication for KWS webhook: signature mismatch',\n      })\n    }\n  }\n\ntype AgeAssuranceWebhookIntermediateBody = {\n  payload: Omit<KwsWebhookBody['payload'], 'externalPayload'> & {\n    externalPayload: string\n  }\n}\n\nconst parseBody = (serialized: string): AgeAssuranceWebhookIntermediateBody => {\n  try {\n    const value: unknown = JSON.parse(serialized)\n    return webhookBodyIntermediateSchema.parse(value)\n  } catch (err) {\n    throw new Error(`Invalid webhook body: ${serialized}`, { cause: err })\n  }\n}\n\nexport const webhookHandler =\n  (ctx: AppContextWithKwsClient): RequestHandler =>\n  async (req: express.Request, res: express.Response) => {\n    let body: AgeAssuranceWebhookIntermediateBody\n    try {\n      body = parseBody(req.body)\n    } catch (err) {\n      log.error({ err }, 'Invalid KWS webhook body')\n      return res.status(400).json(err)\n    }\n\n    const { verified } = body.payload.status\n    if (!verified) {\n      throw new Error('Unexpected KWS webhook call with unverified status')\n    }\n\n    const externalPayload = parseKWSExternalPayloadV1WithV2Compat(\n      body.payload.externalPayload,\n    )\n    const isV2 = externalPayload.version === KWSExternalPayloadVersion.V2\n\n    let result: ReturnType<typeof computeAgeAssuranceAccessOrThrow> | undefined\n    if (isV2) {\n      const { attemptId, actorDid, countryCode, regionCode } = externalPayload\n      try {\n        result = computeAgeAssuranceAccessOrThrow(AGE_ASSURANCE_CONFIG, {\n          countryCode: countryCode,\n          regionCode: regionCode,\n          verifiedMinimumAge: 18, // `adult-verified` is 18+ only\n        })\n      } catch (err) {\n        // internal errors\n        log.error(\n          { err, attemptId, actorDid, countryCode, regionCode },\n          'Failed to compute age assurance access',\n        )\n      }\n    }\n\n    try {\n      if (isV2) {\n        if (result) {\n          const { attemptId, actorDid, countryCode, regionCode } =\n            externalPayload\n          await createEvent(ctx, actorDid, {\n            attemptId,\n            status: 'assured',\n            access: result.access,\n            countryCode,\n            regionCode,\n          })\n        } // else do nothing\n      } else {\n        const { attemptId, actorDid } = externalPayload\n        await createStashEvent(ctx, {\n          attemptId: attemptId,\n          actorDid: actorDid,\n          status: 'assured',\n        })\n      }\n      return res.status(200).end()\n    } catch (err) {\n      log.error({ err }, 'Failed to handle KWS webhook')\n      return res.status(500).json(err)\n    }\n  }\n"
  },
  {
    "path": "packages/bsky/src/api/sitemap.ts",
    "content": "import { Readable } from 'node:stream'\nimport { Timestamp } from '@bufbuild/protobuf'\nimport { Code, ConnectError } from '@connectrpc/connect'\nimport express, { RequestHandler, Router } from 'express'\nimport { AppContext } from '../context'\nimport { httpLogger as log } from '../logger'\nimport { SitemapPageType } from '../proto/bsky_pb'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n  router.get('/external/sitemap/users.xml.gz', userIndexHandler(ctx))\n  router.get(\n    '/external/sitemap/users/:date/:bucket.xml.gz',\n    userPageHandler(ctx),\n  )\n  return router\n}\n\nconst userIndexHandler =\n  (ctx: AppContext): RequestHandler =>\n  async (_req: express.Request, res: express.Response) => {\n    try {\n      const result = await ctx.dataplane.getSitemapIndex({\n        type: SitemapPageType.USER,\n      })\n      res.set('Content-Type', 'application/gzip')\n      res.set('Content-Encoding', 'gzip')\n      Readable.from(Buffer.from(result.sitemap)).pipe(res)\n    } catch (err) {\n      log.error({ err }, 'failed to get sitemap index')\n      return res.status(500).send('Internal Server Error')\n    }\n  }\n\nconst userPageHandler =\n  (ctx: AppContext): RequestHandler =>\n  async (req: express.Request, res: express.Response) => {\n    const { date, bucket } = req.params\n\n    // Parse date (YYYY-MM-DD format)\n    const dateParts = date.split('-')\n    if (dateParts.length !== 3) {\n      return res.status(400).send('Invalid date format. Expected YYYY-MM-DD')\n    }\n\n    const year = parseInt(dateParts[0], 10)\n    const month = parseInt(dateParts[1], 10)\n    const day = parseInt(dateParts[2], 10)\n\n    if (isNaN(year) || isNaN(month) || isNaN(day)) {\n      return res.status(400).send('Invalid date format. Expected YYYY-MM-DD')\n    }\n\n    // Parse bucket (1-indexed)\n    const bucketNum = parseInt(bucket, 10)\n    if (isNaN(bucketNum) || bucketNum < 1) {\n      return res.status(400).send('Invalid bucket number')\n    }\n\n    try {\n      const result = await ctx.dataplane.getSitemapPage({\n        type: SitemapPageType.USER,\n        date: Timestamp.fromDate(new Date(year, month - 1, day)),\n        bucket: bucketNum,\n      })\n      res.set('Content-Type', 'application/gzip')\n      res.set('Content-Encoding', 'gzip')\n      Readable.from(Buffer.from(result.sitemap)).pipe(res)\n    } catch (err) {\n      if (err instanceof ConnectError && err.code === Code.NotFound) {\n        return res.status(404).send('Sitemap page not found')\n      }\n      log.error({ err }, 'failed to get sitemap page')\n      return res.status(500).send('Internal Server Error')\n    }\n  }\n"
  },
  {
    "path": "packages/bsky/src/api/util.ts",
    "content": "import { ParsedLabelers, formatLabelerHeader } from '../util'\n\nexport const BSKY_USER_AGENT = 'BskyAppView'\nexport const ATPROTO_CONTENT_LABELERS = 'Atproto-Content-Labelers'\nexport const ATPROTO_REPO_REV = 'Atproto-Repo-Rev'\n\ntype ResHeaderOpts = {\n  labelers: ParsedLabelers\n  repoRev: string | null\n}\n\nexport const resHeaders = (\n  opts: Partial<ResHeaderOpts>,\n): Record<string, string> => {\n  const headers = {}\n  if (opts.labelers) {\n    headers[ATPROTO_CONTENT_LABELERS] = formatLabelerHeader(opts.labelers)\n  }\n  if (opts.repoRev) {\n    headers[ATPROTO_REPO_REV] = opts.repoRev\n  }\n  return headers\n}\n\nexport const clearlyBadCursor = (cursor?: string) => {\n  // hallmark of v1 cursor, highly unlikely in v2 cursors based on time or rkeys\n  return !!cursor?.includes('::')\n}\n"
  },
  {
    "path": "packages/bsky/src/api/well-known.ts",
    "content": "import { Router } from 'express'\nimport { didWebToUrl, isDidWeb } from '@atproto/did'\nimport { AppContext } from '../context'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  const did = ctx.cfg.serverDid\n  if (isDidWeb(did)) {\n    const serviceEndpoint = didWebToUrl(did).origin\n\n    router.get('/.well-known/did.json', (_req, res) => {\n      res.json({\n        '@context': [\n          'https://www.w3.org/ns/did/v1',\n          'https://w3id.org/security/multikey/v1',\n        ],\n        id: did,\n        verificationMethod: [\n          {\n            id: `${did}#atproto`,\n            type: 'Multikey',\n            controller: did,\n            publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''),\n          },\n        ],\n        service: [\n          {\n            id: '#bsky_notif',\n            type: 'BskyNotificationService',\n            serviceEndpoint,\n          },\n          {\n            id: '#bsky_appview',\n            type: 'BskyAppView',\n            serviceEndpoint,\n          },\n        ],\n      })\n    })\n  }\n\n  return router\n}\n"
  },
  {
    "path": "packages/bsky/src/auth-verifier.ts",
    "content": "import crypto, { KeyObject } from 'node:crypto'\nimport express from 'express'\nimport * as jose from 'jose'\nimport KeyEncoder from 'key-encoder'\nimport * as ui8 from 'uint8arrays'\nimport { SECP256K1_JWT_ALG, parseDidKey } from '@atproto/crypto'\nimport {\n  AuthRequiredError,\n  VerifySignatureWithKeyFn,\n  cryptoVerifySignatureWithKey,\n  parseReqNsid,\n  verifyJwt as verifyServiceJwt,\n} from '@atproto/xrpc-server'\nimport {\n  Code,\n  DataPlaneClient,\n  getKeyAsDidKey,\n  isDataplaneError,\n  unpackIdentityKeys,\n} from './data-plane'\nimport { GetIdentityByDidResponse } from './proto/bsky_pb'\n\ntype ReqCtx = {\n  req: express.Request\n}\n\ntype StandardAuthOpts = {\n  skipAudCheck?: boolean\n  lxmCheck?: (method?: string) => boolean\n}\n\nexport enum RoleStatus {\n  Valid,\n  Invalid,\n  Missing,\n}\n\ntype NullOutput = {\n  credentials: {\n    type: 'none'\n    iss: null\n  }\n}\n\ntype StandardOutput = {\n  credentials: {\n    type: 'standard'\n    aud: string\n    iss: string\n  }\n}\n\ntype RoleOutput = {\n  credentials: {\n    type: 'role'\n    admin: boolean\n  }\n}\n\ntype ModServiceOutput = {\n  credentials: {\n    type: 'mod_service'\n    aud: string\n    iss: string\n  }\n}\n\nconst ALLOWED_AUTH_SCOPES = new Set([\n  'com.atproto.access',\n  'com.atproto.appPass',\n  'com.atproto.appPassPrivileged',\n])\n\nexport type AuthVerifierOpts = {\n  ownDid: string\n  alternateAudienceDids: string[]\n  modServiceDid: string\n  adminPasses: string[]\n  entrywayJwtPublicKey?: KeyObject\n}\n\nexport class AuthVerifier {\n  public ownDid: string\n  public standardAudienceDids: Set<string>\n  public modServiceDid: string\n  private adminPasses: Set<string>\n  private entrywayJwtPublicKey?: KeyObject\n\n  constructor(\n    public dataplane: DataPlaneClient,\n    opts: AuthVerifierOpts,\n  ) {\n    this.ownDid = opts.ownDid\n    this.standardAudienceDids = new Set([\n      opts.ownDid,\n      ...opts.alternateAudienceDids,\n    ])\n    this.modServiceDid = opts.modServiceDid\n    this.adminPasses = new Set(opts.adminPasses)\n    this.entrywayJwtPublicKey = opts.entrywayJwtPublicKey\n  }\n\n  // verifiers (arrow fns to preserve scope)\n  standardOptionalParameterized =\n    (opts: StandardAuthOpts) =>\n    async (ctx: ReqCtx): Promise<StandardOutput | NullOutput> => {\n      // @TODO remove! basic auth + did supported just for testing.\n      if (isBasicToken(ctx.req)) {\n        const aud = this.ownDid\n        const iss = ctx.req.headers['appview-as-did']\n        if (typeof iss !== 'string' || !iss.startsWith('did:')) {\n          throw new AuthRequiredError('bad issuer')\n        }\n        if (!this.parseRoleCreds(ctx.req).admin) {\n          throw new AuthRequiredError('bad credentials')\n        }\n        return {\n          credentials: { type: 'standard', iss, aud },\n        }\n      } else if (isBearerToken(ctx.req)) {\n        // @NOTE temporarily accept entryway session tokens to shed load from PDS instances\n        const token = bearerTokenFromReq(ctx.req)\n        const header = token ? jose.decodeProtectedHeader(token) : undefined\n        if (header?.typ === 'at+jwt') {\n          // we should never use entryway session tokens in the case of flexible auth audiences (namely in the case of getFeed)\n          if (opts.skipAudCheck) {\n            throw new AuthRequiredError('Malformed token', 'InvalidToken')\n          }\n          return this.entrywaySession(ctx)\n        }\n\n        const { iss, aud } = await this.verifyServiceJwt(ctx, {\n          lxmCheck: opts.lxmCheck,\n          iss: null,\n          aud: null,\n        })\n        if (!opts.skipAudCheck && !this.standardAudienceDids.has(aud)) {\n          throw new AuthRequiredError(\n            'jwt audience does not match service did',\n            'BadJwtAudience',\n          )\n        }\n        return {\n          credentials: {\n            type: 'standard',\n            iss,\n            aud,\n          },\n        }\n      } else {\n        return this.nullCreds()\n      }\n    }\n\n  standardOptional: (ctx: ReqCtx) => Promise<StandardOutput | NullOutput> =\n    this.standardOptionalParameterized({})\n\n  standard = async (ctx: ReqCtx): Promise<StandardOutput> => {\n    const output = await this.standardOptional(ctx)\n    if (output.credentials.type === 'none') {\n      throw new AuthRequiredError(undefined, 'AuthMissing')\n    }\n    return output as StandardOutput\n  }\n\n  role = (ctx: ReqCtx): RoleOutput => {\n    const creds = this.parseRoleCreds(ctx.req)\n    if (creds.status !== RoleStatus.Valid) {\n      throw new AuthRequiredError()\n    }\n    return {\n      credentials: {\n        ...creds,\n        type: 'role',\n      },\n    }\n  }\n\n  standardOrRole = async (\n    ctx: ReqCtx,\n  ): Promise<StandardOutput | RoleOutput> => {\n    if (isBearerToken(ctx.req)) {\n      return this.standard(ctx)\n    } else {\n      return this.role(ctx)\n    }\n  }\n\n  optionalStandardOrRole = async (\n    ctx: ReqCtx,\n  ): Promise<StandardOutput | RoleOutput | NullOutput> => {\n    if (isBearerToken(ctx.req)) {\n      return await this.standard(ctx)\n    } else {\n      const creds = this.parseRoleCreds(ctx.req)\n      if (creds.status === RoleStatus.Valid) {\n        return {\n          credentials: {\n            ...creds,\n            type: 'role',\n          },\n        }\n      } else if (creds.status === RoleStatus.Missing) {\n        return this.nullCreds()\n      } else {\n        throw new AuthRequiredError()\n      }\n    }\n  }\n\n  // @NOTE this auth verifier method is not recommended to be implemented by most appviews\n  // this is a short term fix to remove proxy load from Bluesky's PDS and in line with possible\n  // future plans to have the client talk directly with the appview\n  entrywaySession = async (reqCtx: ReqCtx): Promise<StandardOutput> => {\n    const token = bearerTokenFromReq(reqCtx.req)\n    if (!token) {\n      throw new AuthRequiredError(undefined, 'AuthMissing')\n    }\n\n    // if entryway jwt key not configured then do not parsed these tokens\n    if (!this.entrywayJwtPublicKey) {\n      throw new AuthRequiredError('Malformed token', 'InvalidToken')\n    }\n\n    const res = await jose\n      .jwtVerify(token, this.entrywayJwtPublicKey)\n      .catch((err) => {\n        if (err?.['code'] === 'ERR_JWT_EXPIRED') {\n          throw new AuthRequiredError('Token has expired', 'ExpiredToken')\n        }\n        throw new AuthRequiredError(\n          'Token could not be verified',\n          'InvalidToken',\n        )\n      })\n\n    const { sub, aud, scope, cnf } = res.payload\n    if (typeof cnf !== 'undefined') {\n      // Proof-of-Possession (PoP) tokens are not allowed here\n      // https://www.rfc-editor.org/rfc/rfc7800.html\n      throw new AuthRequiredError(\n        'Malformed token: DPoP not supported',\n        'InvalidToken',\n      )\n    }\n    if (typeof sub !== 'string' || !sub.startsWith('did:')) {\n      throw new AuthRequiredError('Malformed token', 'InvalidToken')\n    } else if (\n      typeof aud !== 'string' ||\n      !aud.startsWith('did:web:') ||\n      !aud.endsWith('.bsky.network')\n    ) {\n      throw new AuthRequiredError('Bad token aud', 'InvalidToken')\n    } else if (typeof scope !== 'string' || !ALLOWED_AUTH_SCOPES.has(scope)) {\n      throw new AuthRequiredError('Bad token scope', 'InvalidToken')\n    }\n\n    return {\n      credentials: {\n        type: 'standard',\n        aud: this.ownDid,\n        iss: sub,\n      },\n    }\n  }\n\n  modService = async (reqCtx: ReqCtx): Promise<ModServiceOutput> => {\n    const { iss, aud } = await this.verifyServiceJwt(reqCtx, {\n      aud: this.ownDid,\n      iss: [this.modServiceDid, `${this.modServiceDid}#atproto_labeler`],\n    })\n    return { credentials: { type: 'mod_service', aud, iss } }\n  }\n\n  roleOrModService = async (\n    reqCtx: ReqCtx,\n  ): Promise<RoleOutput | ModServiceOutput> => {\n    if (isBearerToken(reqCtx.req)) {\n      return this.modService(reqCtx)\n    } else {\n      return this.role(reqCtx)\n    }\n  }\n\n  parseRoleCreds(req: express.Request) {\n    const parsed = parseBasicAuth(req.headers.authorization || '')\n    const { Missing, Valid, Invalid } = RoleStatus\n    if (!parsed) {\n      return { status: Missing, admin: false, moderator: false, triage: false }\n    }\n    const { username, password } = parsed\n    if (username === 'admin' && this.adminPasses.has(password)) {\n      return { status: Valid, admin: true }\n    }\n    return { status: Invalid, admin: false }\n  }\n\n  async verifyServiceJwt(\n    reqCtx: ReqCtx,\n    opts: {\n      iss: string[] | null\n      aud: string | null\n      lxmCheck?: (method?: string) => boolean\n    },\n  ) {\n    const getSigningKey = async (\n      iss: string,\n      _forceRefresh: boolean, // @TODO consider propagating to dataplane\n    ): Promise<string> => {\n      if (opts.iss !== null && !opts.iss.includes(iss)) {\n        throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')\n      }\n      const [did, serviceId] = iss.split('#')\n      const keyId =\n        serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto'\n      let identity: GetIdentityByDidResponse\n      try {\n        identity = await this.dataplane.getIdentityByDid({ did })\n      } catch (err) {\n        if (isDataplaneError(err, Code.NotFound)) {\n          throw new AuthRequiredError('identity unknown')\n        }\n        throw err\n      }\n      const keys = unpackIdentityKeys(identity.keys)\n      const didKey = getKeyAsDidKey(keys, { id: keyId })\n      if (!didKey) {\n        throw new AuthRequiredError('missing or bad key')\n      }\n      return didKey\n    }\n    const assertLxmCheck = () => {\n      const lxm = parseReqNsid(reqCtx.req)\n      if (\n        (opts.lxmCheck && !opts.lxmCheck(payload.lxm)) ||\n        (!opts.lxmCheck && payload.lxm !== lxm)\n      ) {\n        throw new AuthRequiredError(\n          payload.lxm !== undefined\n            ? `bad jwt lexicon method (\"lxm\"). must match: ${lxm}`\n            : `missing jwt lexicon method (\"lxm\"). must match: ${lxm}`,\n          'BadJwtLexiconMethod',\n        )\n      }\n    }\n\n    const jwtStr = bearerTokenFromReq(reqCtx.req)\n    if (!jwtStr) {\n      throw new AuthRequiredError('missing jwt', 'MissingJwt')\n    }\n    // if validating additional scopes, skip scope check in initial validation & follow up afterwards\n    const payload = await verifyServiceJwt(\n      jwtStr,\n      opts.aud,\n      null,\n      getSigningKey,\n      verifySignatureWithKey,\n    )\n    if (\n      !payload.iss.endsWith('#atproto_labeler') ||\n      payload.lxm !== undefined\n    ) {\n      // @TODO currently permissive of labelers who dont set lxm yet.\n      // we'll allow ozone self-hosters to upgrade before removing this condition.\n      assertLxmCheck()\n    }\n    return { iss: payload.iss, aud: payload.aud }\n  }\n\n  isModService(iss: string): boolean {\n    return [\n      this.modServiceDid,\n      `${this.modServiceDid}#atproto_labeler`,\n    ].includes(iss)\n  }\n\n  nullCreds(): NullOutput {\n    return {\n      credentials: {\n        type: 'none',\n        iss: null,\n      },\n    }\n  }\n\n  parseCreds(\n    creds: StandardOutput | RoleOutput | ModServiceOutput | NullOutput,\n  ) {\n    const viewer =\n      creds.credentials.type === 'standard' ? creds.credentials.iss : null\n    const includeTakedownsAnd3pBlocks =\n      (creds.credentials.type === 'role' && creds.credentials.admin) ||\n      creds.credentials.type === 'mod_service' ||\n      (creds.credentials.type === 'standard' &&\n        this.isModService(creds.credentials.iss))\n    const canPerformTakedown =\n      (creds.credentials.type === 'role' && creds.credentials.admin) ||\n      creds.credentials.type === 'mod_service'\n    const isModService =\n      creds.credentials.type === 'mod_service' ||\n      (creds.credentials.type === 'standard' &&\n        this.isModService(creds.credentials.iss))\n\n    return {\n      viewer,\n      includeTakedowns: includeTakedownsAnd3pBlocks,\n      include3pBlocks: includeTakedownsAnd3pBlocks,\n      canPerformTakedown,\n      isModService,\n    }\n  }\n}\n\n// HELPERS\n// ---------\n\nconst BEARER = 'Bearer '\nconst BASIC = 'Basic '\n\nconst isBearerToken = (req: express.Request): boolean => {\n  return req.headers.authorization?.startsWith(BEARER) ?? false\n}\n\nconst isBasicToken = (req: express.Request): boolean => {\n  return req.headers.authorization?.startsWith(BASIC) ?? false\n}\n\nconst bearerTokenFromReq = (req: express.Request) => {\n  const header = req.headers.authorization || ''\n  if (!header.startsWith(BEARER)) return null\n  return header.slice(BEARER.length).trim()\n}\n\nexport const parseBasicAuth = (\n  token: string,\n): { username: string; password: string } | null => {\n  if (!token.startsWith(BASIC)) return null\n  const b64 = token.slice(BASIC.length)\n  let parsed: string[]\n  try {\n    parsed = ui8.toString(ui8.fromString(b64, 'base64pad'), 'utf8').split(':')\n  } catch (err) {\n    return null\n  }\n  const [username, password] = parsed\n  if (!username || !password) return null\n  return { username, password }\n}\n\nexport const buildBasicAuth = (username: string, password: string): string => {\n  return (\n    BASIC +\n    ui8.toString(ui8.fromString(`${username}:${password}`, 'utf8'), 'base64pad')\n  )\n}\n\nconst keyEncoder = new KeyEncoder('secp256k1')\nexport const createPublicKeyObject = (publicKeyHex: string): KeyObject => {\n  const key = keyEncoder.encodePublic(publicKeyHex, 'raw', 'pem')\n  return crypto.createPublicKey({ format: 'pem', key })\n}\n\nconst verifySig = (\n  publicKey: Uint8Array,\n  data: Uint8Array,\n  sig: Uint8Array,\n) => {\n  const keyEncoder = new KeyEncoder('secp256k1')\n\n  const pemKey = keyEncoder.encodePublic(\n    ui8.toString(publicKey, 'hex'),\n    'raw',\n    'pem',\n  )\n  const key = crypto.createPublicKey({ format: 'pem', key: pemKey })\n\n  return crypto.verify(\n    'sha256',\n    data,\n    {\n      key,\n      dsaEncoding: 'ieee-p1363',\n    },\n    sig,\n  )\n}\n\nexport const verifySignatureWithKey: VerifySignatureWithKeyFn = async (\n  didKey: string,\n  msgBytes: Uint8Array,\n  sigBytes: Uint8Array,\n  alg: string,\n) => {\n  if (alg === SECP256K1_JWT_ALG) {\n    const parsed = parseDidKey(didKey)\n    if (alg !== parsed.jwtAlg) {\n      throw new Error(`Expected key alg ${alg}, got ${parsed.jwtAlg}`)\n    }\n\n    return verifySig(parsed.keyBytes, msgBytes, sigBytes)\n  }\n\n  return cryptoVerifySignatureWithKey(didKey, msgBytes, sigBytes, alg)\n}\n"
  },
  {
    "path": "packages/bsky/src/bsync.ts",
    "content": "import {\n  Code,\n  ConnectError,\n  Interceptor,\n  PromiseClient,\n  createPromiseClient,\n} from '@connectrpc/connect'\nimport {\n  ConnectTransportOptions,\n  createConnectTransport,\n} from '@connectrpc/connect-node'\nimport { Service } from './proto/bsync_connect'\n\nexport type BsyncClient = PromiseClient<typeof Service>\n\nexport const createBsyncClient = (\n  opts: ConnectTransportOptions,\n): BsyncClient => {\n  const transport = createConnectTransport(opts)\n  return createPromiseClient(Service, transport)\n}\n\nexport { Code }\n\nexport const isBsyncError = (\n  err: unknown,\n  code?: Code,\n): err is ConnectError => {\n  if (err instanceof ConnectError) {\n    return !code || err.code === code\n  }\n  return false\n}\n\nexport const authWithApiKey =\n  (apiKey: string): Interceptor =>\n  (next) =>\n  (req) => {\n    req.header.set('authorization', `Bearer ${apiKey}`)\n    return next(req)\n  }\n"
  },
  {
    "path": "packages/bsky/src/cache/read-through.ts",
    "content": "import { cacheLogger as log } from '../logger'\nimport { Redis } from '../redis'\n\nexport type CacheItem<T> = {\n  val: T | null // null here is for negative caching\n  updatedAt: number\n}\n\nexport type CacheOptions<T> = {\n  staleTTL: number\n  maxTTL: number\n  fetchMethod: (key: string) => Promise<T | null>\n  fetchManyMethod?: (keys: string[]) => Promise<Record<string, T | null>>\n}\n\nexport class ReadThroughCache<T> {\n  constructor(\n    public redis: Redis,\n    public opts: CacheOptions<T>,\n  ) {}\n\n  private async _fetchMany(keys: string[]): Promise<Record<string, T | null>> {\n    let result: Record<string, T | null> = {}\n    if (this.opts.fetchManyMethod) {\n      result = await this.opts.fetchManyMethod(keys)\n    } else {\n      const got = await Promise.all(keys.map((k) => this.opts.fetchMethod(k)))\n      for (let i = 0; i < keys.length; i++) {\n        result[keys[i]] = got[i] ?? null\n      }\n    }\n    // ensure caching negatives\n    for (const key of keys) {\n      result[key] ??= null\n    }\n    return result\n  }\n\n  private async fetchAndCache(key: string): Promise<T | null> {\n    const fetched = await this.opts.fetchMethod(key)\n    this.set(key, fetched).catch((err) =>\n      log.error({ err, key }, 'failed to set cache value'),\n    )\n    return fetched\n  }\n\n  private async fetchAndCacheMany(keys: string[]): Promise<Record<string, T>> {\n    const fetched = await this._fetchMany(keys)\n    this.setMany(fetched).catch((err) =>\n      log.error({ err, keys }, 'failed to set cache values'),\n    )\n    return removeNulls(fetched)\n  }\n\n  async get(key: string, opts?: { revalidate?: boolean }): Promise<T | null> {\n    if (opts?.revalidate) {\n      return this.fetchAndCache(key)\n    }\n    let cached: CacheItem<T> | null\n    try {\n      const got = await this.redis.get(key)\n      cached = got ? JSON.parse(got) : null\n    } catch (err) {\n      cached = null\n      log.warn({ key, err }, 'failed to fetch value from cache')\n    }\n    if (!cached || this.isExpired(cached)) {\n      return this.fetchAndCache(key)\n    }\n    if (this.isStale(cached)) {\n      this.fetchAndCache(key).catch((err) =>\n        log.warn({ key, err }, 'failed to refresh stale cache value'),\n      )\n    }\n    return cached.val\n  }\n\n  async getMany(\n    keys: string[],\n    opts?: { revalidate?: boolean },\n  ): Promise<Record<string, T>> {\n    if (opts?.revalidate) {\n      return this.fetchAndCacheMany(keys)\n    }\n    let cached: Record<string, string>\n    try {\n      cached = await this.redis.getMulti(keys)\n    } catch (err) {\n      cached = {}\n      log.warn({ keys, err }, 'failed to fetch values from cache')\n    }\n\n    const stale: string[] = []\n    const toFetch: string[] = []\n    const results: Record<string, T> = {}\n    for (const key of keys) {\n      const val = cached[key] ? (JSON.parse(cached[key]) as CacheItem<T>) : null\n      if (!val || this.isExpired(val)) {\n        toFetch.push(key)\n        continue\n      }\n      if (this.isStale(val)) {\n        stale.push(key)\n      }\n      if (val.val) {\n        results[key] = val.val\n      }\n    }\n    const fetched = await this.fetchAndCacheMany(toFetch)\n    this.fetchAndCacheMany(stale).catch((err) =>\n      log.warn({ keys, err }, 'failed to refresh stale cache values'),\n    )\n    return {\n      ...results,\n      ...fetched,\n    }\n  }\n\n  async set(key: string, val: T | null) {\n    await this.setMany({ [key]: val })\n  }\n\n  async setMany(vals: Record<string, T | null>) {\n    const items: Record<string, string> = {}\n    for (const key of Object.keys(vals)) {\n      items[key] = JSON.stringify({\n        val: vals[key],\n        updatedAt: Date.now(),\n      })\n    }\n    await this.redis.setMulti(items, this.opts.maxTTL)\n  }\n\n  async clearEntry(key: string) {\n    await this.redis.del(key)\n  }\n\n  isExpired(result: CacheItem<T>) {\n    return Date.now() > result.updatedAt + this.opts.maxTTL\n  }\n\n  isStale(result: CacheItem<T>) {\n    return Date.now() > result.updatedAt + this.opts.staleTTL\n  }\n}\n\nconst removeNulls = <T>(obj: Record<string, T | null>): Record<string, T> => {\n  return Object.entries(obj).reduce(\n    (acc, [key, val]) => {\n      if (val !== null) {\n        acc[key] = val\n      }\n      return acc\n    },\n    {} as Record<string, T>,\n  )\n}\n"
  },
  {
    "path": "packages/bsky/src/config.ts",
    "content": "import assert from 'node:assert'\nimport { noUndefinedVals } from '@atproto/common'\nimport { subLogger as log } from './logger'\n\ntype LiveNowConfig = {\n  did: string\n  domains: string[]\n}[]\n\nexport interface KwsConfig {\n  apiKey: string\n  apiOrigin: string\n  authOrigin: string\n  clientId: string\n  redirectUrl: string\n  userAgent: string\n  /**\n   * V1 secret used to validate `adult-verifieid` redirects\n   */\n  verificationSecret: string\n  /**\n   * V1 secret used to validate `adult-verified` webhooks\n   */\n  webhookSecret: string\n  /**\n   * V2 secret used to validate `age-verified` webhooks\n   */\n  ageVerifiedWebhookSecret: string\n  /**\n   * V2 secret used to validate `age-verified` redirects\n   */\n  ageVerifiedRedirectSecret: string\n}\n\nexport interface ServerConfigValues {\n  // service\n  version?: string\n  debugMode?: boolean\n  port?: number\n  publicUrl?: string\n  serverDid: string\n  alternateAudienceDids: string[]\n  entrywayJwtPublicKeyHex?: string\n  liveNowConfig?: LiveNowConfig\n  // external services\n  etcdHosts: string[]\n  dataplaneUrls: string[]\n  dataplaneUrlsEtcdKeyPrefix?: string\n  dataplaneHttpVersion?: '1.1' | '2'\n  dataplaneIgnoreBadTls?: boolean\n  bsyncUrl: string\n  bsyncApiKey?: string\n  bsyncHttpVersion?: '1.1' | '2'\n  bsyncIgnoreBadTls?: boolean\n  courierUrl?: string\n  courierApiKey?: string\n  courierHttpVersion?: '1.1' | '2'\n  courierIgnoreBadTls?: boolean\n  rolodexUrl?: string\n  rolodexApiKey?: string\n  rolodexHttpVersion?: '1.1' | '2'\n  rolodexIgnoreBadTls?: boolean\n  searchUrl?: string\n  searchTagsHide: Set<string>\n  suggestionsUrl?: string\n  suggestionsApiKey?: string\n  topicsUrl?: string\n  topicsApiKey?: string\n  cdnUrl?: string\n  videoPlaylistUrlPattern?: string\n  videoThumbnailUrlPattern?: string\n  blobRateLimitBypassKey?: string\n  blobRateLimitBypassHostname?: string\n  // identity\n  didPlcUrl: string\n  handleResolveNameservers?: string[]\n  // moderation and administration\n  modServiceDid: string\n  adminPasswords: string[]\n  labelsFromIssuerDids?: string[]\n  indexedAtEpoch?: Date\n  // misc/dev\n  blobCacheLocation?: string\n  eventProxyTrackingEndpoint?: string\n  growthBookApiHost?: string\n  growthBookClientKey?: string\n  // threads\n  bigThreadUris: Set<string>\n  bigThreadDepth?: number\n  maxThreadDepth?: number\n  maxThreadParents: number\n  threadTagsHide: Set<string>\n  threadTagsBumpDown: Set<string>\n  visibilityTagHide: string\n  visibilityTagRankPrefix: string\n  // notifications\n  notificationsDelayMs?: number\n  // client config\n  clientCheckEmailConfirmed?: boolean\n  topicsEnabled?: boolean\n  // http proxy agent\n  disableSsrfProtection?: boolean\n  proxyAllowHTTP2?: boolean\n  proxyHeadersTimeout?: number\n  proxyBodyTimeout?: number\n  proxyMaxResponseSize?: number\n  proxyMaxRetries?: number\n  proxyPreferCompressed?: boolean\n  kws?: KwsConfig\n  debugFieldAllowedDids: Set<string>\n  draftsLimit: number\n}\n\nexport class ServerConfig {\n  private assignedPort?: number\n  constructor(private cfg: ServerConfigValues) {}\n\n  static readEnv(overrides?: Partial<ServerConfigValues>) {\n    const version = process.env.BSKY_VERSION || undefined\n    const debugMode =\n      // Because security related features are disabled in development mode, this requires explicit opt-in.\n      process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'\n    const publicUrl = process.env.BSKY_PUBLIC_URL || undefined\n    const serverDid = process.env.BSKY_SERVER_DID || 'did:example:test'\n    const envPort = parseInt(process.env.BSKY_PORT || '', 10)\n    const port = isNaN(envPort) ? 2584 : envPort\n    const didPlcUrl = process.env.BSKY_DID_PLC_URL || 'http://localhost:2582'\n    const alternateAudienceDids = envList(process.env.BSKY_ALT_AUDIENCE_DIDS)\n    const entrywayJwtPublicKeyHex =\n      process.env.BSKY_ENTRYWAY_JWT_PUBLIC_KEY_HEX || undefined\n    let liveNowConfig: LiveNowConfig | undefined\n    if (process.env.BSKY_LIVE_NOW_CONFIG) {\n      try {\n        const parsed = JSON.parse(process.env.BSKY_LIVE_NOW_CONFIG)\n        if (isLiveNowConfig(parsed)) {\n          liveNowConfig = parsed\n        } else {\n          throw new Error('Live Now config failed format validation')\n        }\n      } catch (err) {\n        log.error({ err }, 'Invalid BSKY_LIVE_NOW_CONFIG')\n      }\n    }\n    const handleResolveNameservers = envList(\n      process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS,\n    )\n    const cdnUrl = process.env.BSKY_CDN_URL || process.env.BSKY_IMG_URI_ENDPOINT\n    // Values 0 through 16\n    const etcdHosts =\n      overrides?.etcdHosts ?? envList(process.env.BSKY_ETCD_HOSTS)\n    // e.g. https://video.invalid/watch/%s/%s/playlist.m3u8\n    const videoPlaylistUrlPattern = process.env.BSKY_VIDEO_PLAYLIST_URL_PATTERN\n    // e.g. https://video.invalid/watch/%s/%s/thumbnail.jpg\n    const videoThumbnailUrlPattern =\n      process.env.BSKY_VIDEO_THUMBNAIL_URL_PATTERN\n    const blobCacheLocation = process.env.BSKY_BLOB_CACHE_LOC\n    const searchUrl =\n      process.env.BSKY_SEARCH_URL ||\n      process.env.BSKY_SEARCH_ENDPOINT ||\n      undefined\n    const searchTagsHide = new Set(envList(process.env.BSKY_SEARCH_TAGS_HIDE))\n    const suggestionsUrl = process.env.BSKY_SUGGESTIONS_URL || undefined\n    const suggestionsApiKey = process.env.BSKY_SUGGESTIONS_API_KEY || undefined\n    const topicsUrl = process.env.BSKY_TOPICS_URL || undefined\n    const topicsApiKey = process.env.BSKY_TOPICS_API_KEY\n    const dataplaneUrls =\n      overrides?.dataplaneUrls ?? envList(process.env.BSKY_DATAPLANE_URLS)\n    const dataplaneUrlsEtcdKeyPrefix =\n      process.env.BSKY_DATAPLANE_URLS_ETCD_KEY_PREFIX || undefined\n    const dataplaneHttpVersion = process.env.BSKY_DATAPLANE_HTTP_VERSION || '2'\n    const dataplaneIgnoreBadTls =\n      process.env.BSKY_DATAPLANE_IGNORE_BAD_TLS === 'true'\n    assert(\n      !dataplaneUrlsEtcdKeyPrefix || etcdHosts.length,\n      'etcd prefix for dataplane urls may only be configured when there are etcd hosts',\n    )\n    assert(\n      dataplaneUrls.length || dataplaneUrlsEtcdKeyPrefix,\n      'dataplane urls are not configured directly nor with etcd',\n    )\n    assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2')\n    const labelsFromIssuerDids = envList(\n      process.env.BSKY_LABELS_FROM_ISSUER_DIDS,\n    )\n    const bsyncUrl = process.env.BSKY_BSYNC_URL || undefined\n    assert(bsyncUrl)\n    const bsyncApiKey = process.env.BSKY_BSYNC_API_KEY || undefined\n    const bsyncHttpVersion = process.env.BSKY_BSYNC_HTTP_VERSION || '2'\n    const bsyncIgnoreBadTls = process.env.BSKY_BSYNC_IGNORE_BAD_TLS === 'true'\n    assert(bsyncHttpVersion === '1.1' || bsyncHttpVersion === '2')\n    const courierUrl = process.env.BSKY_COURIER_URL || undefined\n    const courierApiKey = process.env.BSKY_COURIER_API_KEY || undefined\n    const courierHttpVersion = process.env.BSKY_COURIER_HTTP_VERSION || '2'\n    const courierIgnoreBadTls =\n      process.env.BSKY_COURIER_IGNORE_BAD_TLS === 'true'\n    assert(courierHttpVersion === '1.1' || courierHttpVersion === '2')\n    const rolodexUrl = process.env.BSKY_ROLODEX_URL || undefined\n    const rolodexApiKey = process.env.BSKY_ROLODEX_API_KEY || undefined\n    const rolodexHttpVersion = process.env.BSKY_ROLODEX_HTTP_VERSION || '2'\n    const rolodexIgnoreBadTls =\n      process.env.BSKY_ROLODEX_IGNORE_BAD_TLS === 'true'\n    assert(rolodexHttpVersion === '1.1' || rolodexHttpVersion === '2')\n    const blobRateLimitBypassKey =\n      process.env.BSKY_BLOB_RATE_LIMIT_BYPASS_KEY || undefined\n    // single domain would be e.g. \"mypds.com\", subdomains are supported with a leading dot e.g. \".mypds.com\"\n    const blobRateLimitBypassHostname =\n      process.env.BSKY_BLOB_RATE_LIMIT_BYPASS_HOSTNAME || undefined\n    assert(\n      !blobRateLimitBypassKey || blobRateLimitBypassHostname,\n      'must specify a hostname when using a blob rate limit bypass key',\n    )\n    const adminPasswords = envList(\n      process.env.BSKY_ADMIN_PASSWORDS || process.env.BSKY_ADMIN_PASSWORD,\n    )\n    const modServiceDid = process.env.MOD_SERVICE_DID\n    assert(modServiceDid)\n\n    const eventProxyTrackingEndpoint =\n      process.env.BSKY_EVENT_PROXY_TRACKING_ENDPOINT || undefined\n    const growthBookApiHost = process.env.BSKY_GROWTHBOOK_API_HOST || undefined\n    const growthBookClientKey =\n      process.env.NODE_ENV === 'test'\n        ? 'secret-key'\n        : process.env.BSKY_GROWTHBOOK_CLIENT_KEY || undefined\n    const clientCheckEmailConfirmed =\n      process.env.BSKY_CLIENT_CHECK_EMAIL_CONFIRMED === 'true'\n    const topicsEnabled = process.env.BSKY_TOPICS_ENABLED === 'true'\n    const indexedAtEpoch = process.env.BSKY_INDEXED_AT_EPOCH\n      ? new Date(process.env.BSKY_INDEXED_AT_EPOCH)\n      : undefined\n    assert(\n      !indexedAtEpoch || !isNaN(indexedAtEpoch.getTime()),\n      'invalid BSKY_INDEXED_AT_EPOCH',\n    )\n    const bigThreadUris = new Set(envList(process.env.BSKY_BIG_THREAD_URIS))\n    const bigThreadDepth = process.env.BSKY_BIG_THREAD_DEPTH\n      ? parseInt(process.env.BSKY_BIG_THREAD_DEPTH || '', 10)\n      : undefined\n    const maxThreadDepth = process.env.BSKY_MAX_THREAD_DEPTH\n      ? parseInt(process.env.BSKY_MAX_THREAD_DEPTH || '', 10)\n      : undefined\n    const maxThreadParents = process.env.BSKY_MAX_THREAD_PARENTS\n      ? parseInt(process.env.BSKY_MAX_THREAD_PARENTS || '', 10)\n      : 50\n    const threadTagsHide = new Set(envList(process.env.BSKY_THREAD_TAGS_HIDE))\n    const threadTagsBumpDown = new Set(\n      envList(process.env.BSKY_THREAD_TAGS_BUMP_DOWN),\n    )\n    const visibilityTagHide = process.env.BSKY_VISIBILITY_TAG_HIDE || ''\n    const visibilityTagRankPrefix =\n      process.env.BSKY_VISIBILITY_TAG_RANK_PREFIX || ''\n\n    const notificationsDelayMs = process.env.BSKY_NOTIFICATIONS_DELAY_MS\n      ? parseInt(process.env.BSKY_NOTIFICATIONS_DELAY_MS || '', 10)\n      : 0\n\n    const disableSsrfProtection = process.env.BSKY_DISABLE_SSRF_PROTECTION\n      ? process.env.BSKY_DISABLE_SSRF_PROTECTION === 'true'\n      : debugMode\n\n    const proxyAllowHTTP2 = process.env.BSKY_PROXY_ALLOW_HTTP2 === 'true'\n    const proxyHeadersTimeout =\n      parseInt(process.env.BSKY_PROXY_HEADERS_TIMEOUT || '', 10) || undefined\n    const proxyBodyTimeout =\n      parseInt(process.env.BSKY_PROXY_BODY_TIMEOUT || '', 10) || undefined\n    const proxyMaxResponseSize =\n      parseInt(process.env.BSKY_PROXY_MAX_RESPONSE_SIZE || '', 10) || undefined\n    const proxyMaxRetries =\n      parseInt(process.env.BSKY_PROXY_MAX_RETRIES || '', 10) || undefined\n    const proxyPreferCompressed =\n      process.env.BSKY_PROXY_PREFER_COMPRESSED === 'true'\n\n    let kws: KwsConfig | undefined\n    const kwsApiKey = process.env.BSKY_KWS_API_KEY\n    const kwsApiOrigin = process.env.BSKY_KWS_API_ORIGIN\n    const kwsAuthOrigin = process.env.BSKY_KWS_AUTH_ORIGIN\n    const kwsClientId = process.env.BSKY_KWS_CLIENT_ID\n    const kwsRedirectUrl = process.env.BSKY_KWS_REDIRECT_URL\n    const kwsUserAgent = process.env.BSKY_KWS_USER_AGENT\n    const kwsVerificationSecret = process.env.BSKY_KWS_VERIFICATION_SECRET\n    const kwsWebhookSecret = process.env.BSKY_KWS_WEBHOOK_SECRET\n    const kwsAgeVerifiedWebhookSecret =\n      process.env.BSKY_KWS_AGE_VERIFIED_WEBHOOK_SECRET\n    const kwsAgeVerifiedRedirectSecret =\n      process.env.BSKY_KWS_AGE_VERIFIED_REDIRECT_SECRET\n    if (\n      kwsApiKey ||\n      kwsApiOrigin ||\n      kwsAuthOrigin ||\n      kwsClientId ||\n      kwsRedirectUrl ||\n      kwsUserAgent ||\n      kwsVerificationSecret ||\n      kwsWebhookSecret ||\n      kwsAgeVerifiedWebhookSecret ||\n      kwsAgeVerifiedRedirectSecret\n    ) {\n      assert(\n        kwsApiOrigin &&\n          kwsAuthOrigin &&\n          kwsClientId &&\n          kwsRedirectUrl &&\n          kwsUserAgent &&\n          kwsVerificationSecret &&\n          kwsWebhookSecret &&\n          kwsApiKey &&\n          kwsAgeVerifiedWebhookSecret &&\n          kwsAgeVerifiedRedirectSecret,\n        'all KWS environment variables must be set if any are set',\n      )\n      kws = {\n        apiKey: kwsApiKey,\n        apiOrigin: kwsApiOrigin,\n        authOrigin: kwsAuthOrigin,\n        clientId: kwsClientId,\n        redirectUrl: kwsRedirectUrl,\n        userAgent: kwsUserAgent,\n        verificationSecret: kwsVerificationSecret,\n        webhookSecret: kwsWebhookSecret,\n        ageVerifiedWebhookSecret: kwsAgeVerifiedWebhookSecret,\n        ageVerifiedRedirectSecret: kwsAgeVerifiedRedirectSecret,\n      }\n    }\n\n    const debugFieldAllowedDids = new Set(\n      envList(process.env.BSKY_DEBUG_FIELD_ALLOWED_DIDS),\n    )\n\n    const draftsLimit = process.env.BSKY_DRAFTS_LIMIT\n      ? parseInt(process.env.BSKY_DRAFTS_LIMIT || '', 10)\n      : 500\n\n    return new ServerConfig({\n      version,\n      debugMode,\n      port,\n      publicUrl,\n      serverDid,\n      alternateAudienceDids,\n      entrywayJwtPublicKeyHex,\n      liveNowConfig,\n      etcdHosts,\n      dataplaneUrls,\n      dataplaneUrlsEtcdKeyPrefix,\n      dataplaneHttpVersion,\n      dataplaneIgnoreBadTls,\n      searchUrl,\n      searchTagsHide,\n      suggestionsUrl,\n      suggestionsApiKey,\n      topicsUrl,\n      topicsApiKey,\n      didPlcUrl,\n      labelsFromIssuerDids,\n      handleResolveNameservers,\n      cdnUrl,\n      videoPlaylistUrlPattern,\n      videoThumbnailUrlPattern,\n      blobCacheLocation,\n      bsyncUrl,\n      bsyncApiKey,\n      bsyncHttpVersion,\n      bsyncIgnoreBadTls,\n      courierUrl,\n      courierApiKey,\n      courierHttpVersion,\n      courierIgnoreBadTls,\n      rolodexUrl,\n      rolodexApiKey,\n      rolodexHttpVersion,\n      rolodexIgnoreBadTls,\n      blobRateLimitBypassKey,\n      blobRateLimitBypassHostname,\n      adminPasswords,\n      modServiceDid,\n      eventProxyTrackingEndpoint,\n      growthBookApiHost,\n      growthBookClientKey,\n      clientCheckEmailConfirmed,\n      topicsEnabled,\n      indexedAtEpoch,\n      bigThreadUris,\n      bigThreadDepth,\n      maxThreadDepth,\n      maxThreadParents,\n      threadTagsHide,\n      threadTagsBumpDown,\n      visibilityTagHide,\n      visibilityTagRankPrefix,\n      notificationsDelayMs,\n      disableSsrfProtection,\n      proxyAllowHTTP2,\n      proxyHeadersTimeout,\n      proxyBodyTimeout,\n      proxyMaxResponseSize,\n      proxyMaxRetries,\n      proxyPreferCompressed,\n      kws,\n      debugFieldAllowedDids,\n      draftsLimit,\n      ...noUndefinedVals(overrides ?? {}),\n    })\n  }\n\n  assignPort(port: number) {\n    assert(\n      !this.cfg.port || this.cfg.port === port,\n      'Conflicting port in config',\n    )\n    this.assignedPort = port\n  }\n\n  get version() {\n    return this.cfg.version\n  }\n\n  get debugMode() {\n    return !!this.cfg.debugMode\n  }\n\n  get port() {\n    return this.assignedPort || this.cfg.port\n  }\n\n  get publicUrl() {\n    return this.cfg.publicUrl\n  }\n\n  get serverDid() {\n    return this.cfg.serverDid\n  }\n\n  get alternateAudienceDids() {\n    return this.cfg.alternateAudienceDids\n  }\n\n  get entrywayJwtPublicKeyHex() {\n    return this.cfg.entrywayJwtPublicKeyHex\n  }\n\n  get liveNowConfig() {\n    return this.cfg.liveNowConfig\n  }\n\n  get etcdHosts() {\n    return this.cfg.etcdHosts\n  }\n\n  get dataplaneUrlsEtcdKeyPrefix() {\n    return this.cfg.dataplaneUrlsEtcdKeyPrefix\n  }\n\n  get dataplaneUrls() {\n    return this.cfg.dataplaneUrls\n  }\n\n  get dataplaneHttpVersion() {\n    return this.cfg.dataplaneHttpVersion\n  }\n\n  get dataplaneIgnoreBadTls() {\n    return this.cfg.dataplaneIgnoreBadTls\n  }\n\n  get bsyncUrl() {\n    return this.cfg.bsyncUrl\n  }\n\n  get bsyncApiKey() {\n    return this.cfg.bsyncApiKey\n  }\n\n  get bsyncHttpVersion() {\n    return this.cfg.bsyncHttpVersion\n  }\n\n  get bsyncIgnoreBadTls() {\n    return this.cfg.bsyncIgnoreBadTls\n  }\n\n  get courierUrl() {\n    return this.cfg.courierUrl\n  }\n\n  get courierApiKey() {\n    return this.cfg.courierApiKey\n  }\n\n  get courierHttpVersion() {\n    return this.cfg.courierHttpVersion\n  }\n\n  get courierIgnoreBadTls() {\n    return this.cfg.courierIgnoreBadTls\n  }\n\n  get rolodexUrl() {\n    return this.cfg.rolodexUrl\n  }\n  get rolodexApiKey() {\n    return this.cfg.rolodexApiKey\n  }\n  get rolodexHttpVersion() {\n    return this.cfg.rolodexHttpVersion\n  }\n  get rolodexIgnoreBadTls() {\n    return this.cfg.rolodexIgnoreBadTls\n  }\n\n  get searchUrl() {\n    return this.cfg.searchUrl\n  }\n\n  get searchTagsHide() {\n    return this.cfg.searchTagsHide\n  }\n\n  get suggestionsUrl() {\n    return this.cfg.suggestionsUrl\n  }\n\n  get suggestionsApiKey() {\n    return this.cfg.suggestionsApiKey\n  }\n\n  get topicsUrl() {\n    return this.cfg.topicsUrl\n  }\n\n  get topicsApiKey() {\n    return this.cfg.topicsApiKey\n  }\n\n  get cdnUrl() {\n    return this.cfg.cdnUrl\n  }\n\n  get videoPlaylistUrlPattern() {\n    return this.cfg.videoPlaylistUrlPattern\n  }\n\n  get videoThumbnailUrlPattern() {\n    return this.cfg.videoThumbnailUrlPattern\n  }\n\n  get blobRateLimitBypassKey() {\n    return this.cfg.blobRateLimitBypassKey\n  }\n\n  get blobRateLimitBypassHostname() {\n    return this.cfg.blobRateLimitBypassHostname\n  }\n\n  get didPlcUrl() {\n    return this.cfg.didPlcUrl\n  }\n\n  get handleResolveNameservers() {\n    return this.cfg.handleResolveNameservers\n  }\n\n  get adminPasswords() {\n    return this.cfg.adminPasswords\n  }\n\n  get modServiceDid() {\n    return this.cfg.modServiceDid\n  }\n\n  get labelsFromIssuerDids() {\n    return this.cfg.labelsFromIssuerDids ?? []\n  }\n\n  get blobCacheLocation() {\n    return this.cfg.blobCacheLocation\n  }\n\n  get eventProxyTrackingEndpoint() {\n    return this.cfg.eventProxyTrackingEndpoint\n  }\n\n  get growthBookApiHost() {\n    return this.cfg.growthBookApiHost\n  }\n\n  get growthBookClientKey() {\n    return this.cfg.growthBookClientKey\n  }\n\n  get clientCheckEmailConfirmed() {\n    return this.cfg.clientCheckEmailConfirmed\n  }\n\n  get topicsEnabled() {\n    return this.cfg.topicsEnabled\n  }\n\n  get indexedAtEpoch() {\n    return this.cfg.indexedAtEpoch\n  }\n\n  get bigThreadUris() {\n    return this.cfg.bigThreadUris\n  }\n\n  get bigThreadDepth() {\n    return this.cfg.bigThreadDepth\n  }\n\n  get maxThreadDepth() {\n    return this.cfg.maxThreadDepth\n  }\n\n  get maxThreadParents() {\n    return this.cfg.maxThreadParents\n  }\n\n  get threadTagsHide() {\n    return this.cfg.threadTagsHide\n  }\n\n  get threadTagsBumpDown() {\n    return this.cfg.threadTagsBumpDown\n  }\n\n  get visibilityTagHide() {\n    return this.cfg.visibilityTagHide\n  }\n\n  get visibilityTagRankPrefix() {\n    return this.cfg.visibilityTagRankPrefix\n  }\n\n  get notificationsDelayMs() {\n    return this.cfg.notificationsDelayMs ?? 0\n  }\n\n  get disableSsrfProtection(): boolean {\n    return this.cfg.disableSsrfProtection ?? false\n  }\n\n  get proxyAllowHTTP2(): boolean {\n    return this.cfg.proxyAllowHTTP2 ?? false\n  }\n\n  get proxyHeadersTimeout(): number {\n    return this.cfg.proxyHeadersTimeout ?? 30e3\n  }\n\n  get proxyBodyTimeout(): number {\n    return this.cfg.proxyBodyTimeout ?? 30e3\n  }\n\n  get proxyMaxResponseSize(): number {\n    return this.cfg.proxyMaxResponseSize ?? 10 * 1024 * 1024 // 10mb\n  }\n\n  get proxyMaxRetries(): number {\n    return this.cfg.proxyMaxRetries ?? 3\n  }\n\n  get proxyPreferCompressed(): boolean {\n    return this.cfg.proxyPreferCompressed ?? true\n  }\n\n  get kws() {\n    return this.cfg.kws\n  }\n\n  get debugFieldAllowedDids() {\n    return this.cfg.debugFieldAllowedDids\n  }\n\n  get draftsLimit() {\n    return this.cfg.draftsLimit\n  }\n}\n\nfunction envList(str: string | undefined): string[] {\n  if (str === undefined || str.length === 0) return []\n  return str.split(',')\n}\n\nfunction isLiveNowConfig(data: any): data is LiveNowConfig {\n  return (\n    Array.isArray(data) &&\n    data.every(\n      (item) =>\n        typeof item === 'object' &&\n        item !== null &&\n        typeof item.did === 'string' &&\n        Array.isArray(item.domains) &&\n        item.domains.every((domain: any) => typeof domain === 'string'),\n    )\n  )\n}\n"
  },
  {
    "path": "packages/bsky/src/context.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { Etcd3 } from 'etcd3'\nimport express from 'express'\nimport { Dispatcher } from 'undici'\nimport { AtpAgent } from '@atproto/api'\nimport { Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { AuthVerifier } from './auth-verifier'\nimport { BsyncClient } from './bsync'\nimport { ServerConfig } from './config'\nimport { CourierClient } from './courier'\nimport { DataPlaneClient, HostList } from './data-plane/client'\nimport { FeatureGatesClient } from './feature-gates'\nimport { Hydrator } from './hydration/hydrator'\nimport { KwsClient } from './kws'\nimport { httpLogger as log } from './logger'\nimport { RolodexClient } from './rolodex'\nimport { StashClient } from './stash'\nimport {\n  ParsedLabelers,\n  defaultLabelerHeader,\n  parseLabelerHeader,\n} from './util'\nimport { Views } from './views'\n\nexport class AppContext {\n  constructor(\n    private opts: {\n      cfg: ServerConfig\n      etcd: Etcd3 | undefined\n      dataplane: DataPlaneClient\n      dataplaneHostList: HostList\n      searchAgent: AtpAgent | undefined\n      suggestionsAgent: AtpAgent | undefined\n      topicsAgent: AtpAgent | undefined\n      hydrator: Hydrator\n      views: Views\n      signingKey: Keypair\n      idResolver: IdResolver\n      bsyncClient: BsyncClient\n      stashClient: StashClient\n      courierClient: CourierClient | undefined\n      rolodexClient: RolodexClient | undefined\n      authVerifier: AuthVerifier\n      featureGatesClient: FeatureGatesClient\n      blobDispatcher: Dispatcher\n      kwsClient: KwsClient | undefined\n    },\n  ) {}\n\n  get cfg(): ServerConfig {\n    return this.opts.cfg\n  }\n\n  get etcd() {\n    return this.opts.etcd\n  }\n\n  get dataplane(): DataPlaneClient {\n    return this.opts.dataplane\n  }\n\n  get dataplaneHostList(): HostList {\n    return this.opts.dataplaneHostList\n  }\n\n  get searchAgent(): AtpAgent | undefined {\n    return this.opts.searchAgent\n  }\n\n  get suggestionsAgent(): AtpAgent | undefined {\n    return this.opts.suggestionsAgent\n  }\n\n  get topicsAgent(): AtpAgent | undefined {\n    return this.opts.topicsAgent\n  }\n\n  get hydrator(): Hydrator {\n    return this.opts.hydrator\n  }\n\n  get views(): Views {\n    return this.opts.views\n  }\n\n  get signingKey(): Keypair {\n    return this.opts.signingKey\n  }\n\n  get plcClient(): plc.Client {\n    return new plc.Client(this.cfg.didPlcUrl)\n  }\n\n  get idResolver(): IdResolver {\n    return this.opts.idResolver\n  }\n\n  get bsyncClient(): BsyncClient {\n    return this.opts.bsyncClient\n  }\n\n  get stashClient(): StashClient {\n    return this.opts.stashClient\n  }\n\n  get courierClient(): CourierClient | undefined {\n    return this.opts.courierClient\n  }\n\n  get rolodexClient(): RolodexClient | undefined {\n    return this.opts.rolodexClient\n  }\n\n  get authVerifier(): AuthVerifier {\n    return this.opts.authVerifier\n  }\n\n  get featureGatesClient(): FeatureGatesClient {\n    return this.opts.featureGatesClient\n  }\n\n  get blobDispatcher(): Dispatcher {\n    return this.opts.blobDispatcher\n  }\n\n  get kwsClient(): KwsClient | undefined {\n    return this.opts.kwsClient\n  }\n\n  reqLabelers(req: express.Request): ParsedLabelers {\n    const val = req.header('atproto-accept-labelers')\n    let parsed: ParsedLabelers | null\n    try {\n      parsed = parseLabelerHeader(val)\n    } catch (err) {\n      parsed = null\n      log.info({ err, val }, 'failed to parse labeler header')\n    }\n    if (!parsed) return defaultLabelerHeader(this.cfg.labelsFromIssuerDids)\n    return parsed\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/courier.ts",
    "content": "import {\n  Code,\n  ConnectError,\n  Interceptor,\n  PromiseClient,\n  createPromiseClient,\n} from '@connectrpc/connect'\nimport {\n  ConnectTransportOptions,\n  createConnectTransport,\n} from '@connectrpc/connect-node'\nimport { Service } from './proto/courier_connect'\n\nexport type CourierClient = PromiseClient<typeof Service>\n\nexport const createCourierClient = (\n  opts: ConnectTransportOptions,\n): CourierClient => {\n  const transport = createConnectTransport(opts)\n  return createPromiseClient(Service, transport)\n}\n\nexport { Code }\n\nexport const isCourierError = (\n  err: unknown,\n  code?: Code,\n): err is ConnectError => {\n  if (err instanceof ConnectError) {\n    return !code || err.code === code\n  }\n  return false\n}\n\nexport const authWithApiKey =\n  (apiKey: string): Interceptor =>\n  (next) =>\n  (req) => {\n    req.header.set('authorization', `Bearer ${apiKey}`)\n    return next(req)\n  }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/bsync/index.ts",
    "content": "import assert from 'node:assert'\nimport events from 'node:events'\nimport http from 'node:http'\nimport { ConnectRouter } from '@connectrpc/connect'\nimport { expressConnectMiddleware } from '@connectrpc/connect-express'\nimport express from 'express'\nimport { TID } from '@atproto/common'\nimport { jsonStringToLex } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport { ids } from '../../lexicon/lexicons'\nimport { Event as AgeAssuranceV2Event } from '../../lexicon/types/app/bsky/ageassurance/defs'\nimport { Bookmark } from '../../lexicon/types/app/bsky/bookmark/defs'\nimport { SubjectActivitySubscription } from '../../lexicon/types/app/bsky/notification/defs'\nimport { AgeAssuranceEvent } from '../../lexicon/types/app/bsky/unspecced/defs'\nimport { httpLogger } from '../../logger'\nimport { Service } from '../../proto/bsync_connect'\nimport {\n  Method,\n  MuteOperation_Type,\n  PutOperationRequest,\n} from '../../proto/bsync_pb'\nimport { Namespaces } from '../../stash'\nimport { Database } from '../server/db'\nimport { countAll, excluded } from '../server/db/util'\n\nexport class MockBsync {\n  constructor(public server: http.Server) {}\n\n  static async create(db: Database, port: number) {\n    const app = express()\n    const routes = createRoutes(db)\n    app.use(expressConnectMiddleware({ routes }))\n    const server = app.listen(port)\n    await events.once(server, 'listening')\n    return new MockBsync(server)\n  }\n\n  async destroy() {\n    return new Promise<void>((resolve, reject) => {\n      this.server.close((err) => {\n        if (err) {\n          reject(err)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n}\n\nconst createRoutes = (db: Database) => (router: ConnectRouter) =>\n  router.service(Service, {\n    async addMuteOperation(req) {\n      const { type, actorDid, subject } = req\n      if (type === MuteOperation_Type.ADD) {\n        if (subject.startsWith('did:')) {\n          assert(actorDid !== subject, 'cannot mute yourself') // @TODO pass message through in http error\n          await db.db\n            .insertInto('mute')\n            .values({\n              mutedByDid: actorDid,\n              subjectDid: subject,\n              createdAt: new Date().toISOString(),\n            })\n            .onConflict((oc) => oc.doNothing())\n            .execute()\n        } else {\n          const uri = new AtUri(subject)\n          if (uri.collection === ids.AppBskyGraphList) {\n            await db.db\n              .insertInto('list_mute')\n              .values({\n                mutedByDid: actorDid,\n                listUri: subject,\n                createdAt: new Date().toISOString(),\n              })\n              .onConflict((oc) => oc.doNothing())\n              .execute()\n          } else {\n            await db.db\n              .insertInto('thread_mute')\n              .values({\n                mutedByDid: actorDid,\n                rootUri: subject,\n                createdAt: new Date().toISOString(),\n              })\n              .onConflict((oc) => oc.doNothing())\n              .execute()\n          }\n        }\n      } else if (type === MuteOperation_Type.REMOVE) {\n        if (subject.startsWith('did:')) {\n          await db.db\n            .deleteFrom('mute')\n            .where('mutedByDid', '=', actorDid)\n            .where('subjectDid', '=', subject)\n            .execute()\n        } else {\n          const uri = new AtUri(subject)\n          if (uri.collection === ids.AppBskyGraphList) {\n            await db.db\n              .deleteFrom('list_mute')\n              .where('mutedByDid', '=', actorDid)\n              .where('listUri', '=', subject)\n              .execute()\n          } else {\n            await db.db\n              .deleteFrom('thread_mute')\n              .where('mutedByDid', '=', actorDid)\n              .where('rootUri', '=', subject)\n              .execute()\n          }\n        }\n      } else if (type === MuteOperation_Type.CLEAR) {\n        await db.db\n          .deleteFrom('mute')\n          .where('mutedByDid', '=', actorDid)\n          .execute()\n        await db.db\n          .deleteFrom('list_mute')\n          .where('mutedByDid', '=', actorDid)\n          .execute()\n      }\n\n      return {}\n    },\n\n    async scanMuteOperations() {\n      throw new Error('not implemented')\n    },\n\n    async addNotifOperation(req) {\n      const { actorDid, priority } = req\n      if (priority !== undefined) {\n        await db.db\n          .insertInto('actor_state')\n          .values({\n            did: actorDid,\n            priorityNotifs: priority,\n            lastSeenNotifs: new Date().toISOString(),\n          })\n          .onConflict((oc) =>\n            oc.column('did').doUpdateSet({ priorityNotifs: priority }),\n          )\n          .execute()\n      }\n      return {}\n    },\n\n    async scanNotifOperations() {\n      throw new Error('not implemented')\n    },\n\n    async putOperation(req) {\n      const { actorDid, namespace, key, method, payload } = req\n      assert(\n        method === Method.CREATE ||\n          method === Method.UPDATE ||\n          method === Method.DELETE,\n        `Unsupported method: ${method}`,\n      )\n\n      const now = new Date().toISOString()\n\n      // Index all items into private_data.\n      await handleGenericOperation(db, req, now)\n\n      // Maintain bespoke indexes for certain namespaces.\n      try {\n        if (\n          namespace ===\n          Namespaces.AppBskyNotificationDefsSubjectActivitySubscription\n        ) {\n          await handleSubjectActivitySubscriptionOperation(db, req, now)\n        } else if (\n          namespace === Namespaces.AppBskyUnspeccedDefsAgeAssuranceEvent\n        ) {\n          await handleAgeAssuranceEventOperation(db, req, now)\n        } else if (namespace === Namespaces.AppBskyAgeassuranceDefsEvent) {\n          await handleAgeAssuranceV2EventOperation(db, req, now)\n        } else if (namespace === Namespaces.AppBskyBookmarkDefsBookmark) {\n          await handleBookmarkOperation(db, req, now)\n        } else if (namespace === Namespaces.AppBskyDraftDefsDraftWithId) {\n          await handleDraftOperation(db, req, now)\n        }\n      } catch (err) {\n        httpLogger.warn({ err, namespace }, 'mock bsync put operation failed')\n      }\n\n      return {\n        operation: {\n          id: TID.nextStr(),\n          actorDid,\n          namespace,\n          key,\n          method,\n          payload,\n        },\n      }\n    },\n\n    async scanOperations() {\n      throw new Error('not implemented')\n    },\n\n    async ping() {\n      return {}\n    },\n  })\n\n// upsert into or remove from private_data\nconst handleGenericOperation = async (\n  db: Database,\n  req: PutOperationRequest,\n  now: string,\n) => {\n  const { actorDid, namespace, key, method, payload } = req\n  if (method === Method.CREATE || method === Method.UPDATE) {\n    await db.db\n      .insertInto('private_data')\n      .values({\n        actorDid,\n        namespace,\n        key,\n        payload: Buffer.from(payload).toString('utf8'),\n        indexedAt: now,\n        updatedAt: now,\n      })\n      .onConflict((oc) =>\n        oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({\n          payload: excluded(db.db, 'payload'),\n          updatedAt: excluded(db.db, 'updatedAt'),\n        }),\n      )\n      .execute()\n  } else if (method === Method.DELETE) {\n    await db.db\n      .deleteFrom('private_data')\n      .where('actorDid', '=', actorDid)\n      .where('namespace', '=', namespace)\n      .where('key', '=', key)\n      .execute()\n  } else {\n    assert.fail(`unexpected method ${method}`)\n  }\n}\n\nconst handleSubjectActivitySubscriptionOperation = async (\n  db: Database,\n  req: PutOperationRequest,\n  now: string,\n) => {\n  const { actorDid, key, method, payload } = req\n\n  if (method === Method.DELETE) {\n    return db.db\n      .deleteFrom('activity_subscription')\n      .where('creator', '=', actorDid)\n      .where('key', '=', key)\n      .execute()\n  }\n\n  const parsed = jsonStringToLex(\n    Buffer.from(payload).toString('utf8'),\n  ) as SubjectActivitySubscription\n  const {\n    subject,\n    activitySubscription: { post, reply },\n  } = parsed\n\n  if (method === Method.CREATE) {\n    return db.db\n      .insertInto('activity_subscription')\n      .values({\n        creator: actorDid,\n        subjectDid: subject,\n        key,\n        indexedAt: now,\n        post,\n        reply,\n      })\n      .execute()\n  }\n\n  return db.db\n    .updateTable('activity_subscription')\n    .where('creator', '=', actorDid)\n    .where('key', '=', key)\n    .set({\n      indexedAt: now,\n      post,\n      reply,\n    })\n    .execute()\n}\n\nconst handleAgeAssuranceEventOperation = async (\n  db: Database,\n  req: PutOperationRequest,\n  _now: string,\n) => {\n  const { actorDid, method, payload } = req\n  if (method !== Method.CREATE) return\n\n  const parsed = jsonStringToLex(\n    Buffer.from(payload).toString('utf8'),\n  ) as AgeAssuranceEvent\n  const { status, createdAt } = parsed\n\n  const update = {\n    ageAssuranceStatus: status,\n    ageAssuranceLastInitiatedAt: status === 'pending' ? createdAt : undefined,\n  }\n\n  return db.db\n    .updateTable('actor')\n    .set(update)\n    .where('did', '=', actorDid)\n    .execute()\n}\n\nconst handleAgeAssuranceV2EventOperation = async (\n  db: Database,\n  req: PutOperationRequest,\n  _now: string,\n) => {\n  const { actorDid, method, payload } = req\n  if (method !== Method.CREATE) return\n\n  const parsed = jsonStringToLex(\n    Buffer.from(payload).toString('utf8'),\n  ) as AgeAssuranceV2Event\n  const { status, createdAt, access, countryCode, regionCode } = parsed\n\n  const update = {\n    ageAssuranceStatus: status,\n    ageAssuranceLastInitiatedAt: status === 'pending' ? createdAt : undefined,\n    ageAssuranceAccess: access,\n    ageAssuranceCountryCode: countryCode,\n    ageAssuranceRegionCode: regionCode,\n  }\n\n  return db.db\n    .updateTable('actor')\n    .set(update)\n    .where('did', '=', actorDid)\n    .execute()\n}\n\nconst handleBookmarkOperation = async (\n  db: Database,\n  req: PutOperationRequest,\n  now: string,\n) => {\n  const { actorDid, key, method, payload } = req\n\n  const updateAgg = (uri: string, dbTxn: Database) => {\n    return dbTxn.db\n      .insertInto('post_agg')\n      .values({\n        uri,\n        bookmarkCount: dbTxn.db\n          .selectFrom('bookmark')\n          .where('bookmark.subjectUri', '=', uri)\n          .select(countAll.as('count')),\n      })\n      .onConflict((oc) =>\n        oc\n          .column('uri')\n          .doUpdateSet({ bookmarkCount: excluded(dbTxn.db, 'bookmarkCount') }),\n      )\n      .execute()\n  }\n\n  if (method === Method.CREATE) {\n    const parsed = jsonStringToLex(\n      Buffer.from(payload).toString('utf8'),\n    ) as Bookmark\n    const {\n      subject: { uri, cid },\n    } = parsed\n\n    await db.transaction(async (dbTxn) => {\n      await dbTxn.db\n        .insertInto('bookmark')\n        .values({\n          creator: actorDid,\n          key,\n          indexedAt: now,\n          subjectUri: uri,\n          subjectCid: cid,\n        })\n        .execute()\n\n      await updateAgg(uri, dbTxn)\n    })\n  }\n\n  if (method === Method.DELETE) {\n    await db.transaction(async (dbTxn) => {\n      const bookmark = await dbTxn.db\n        .selectFrom('bookmark')\n        .selectAll()\n        .where('creator', '=', actorDid)\n        .where('key', '=', key)\n        .executeTakeFirst()\n\n      if (bookmark) {\n        await dbTxn.db\n          .deleteFrom('bookmark')\n          .where('creator', '=', actorDid)\n          .where('key', '=', key)\n          .execute()\n\n        await updateAgg(bookmark.subjectUri, dbTxn)\n      }\n    })\n  }\n}\n\nconst handleDraftOperation = async (\n  db: Database,\n  req: PutOperationRequest,\n  now: string,\n) => {\n  const { actorDid, key, method, payload } = req\n\n  if (method === Method.CREATE) {\n    const payloadString = Buffer.from(payload).toString('utf8')\n\n    await db.db\n      .insertInto('draft')\n      .values({\n        creator: actorDid,\n        key,\n        createdAt: now,\n        updatedAt: now,\n        payload: payloadString,\n      })\n      .execute()\n  }\n\n  if (method === Method.UPDATE) {\n    const payloadString = Buffer.from(payload).toString('utf8')\n\n    await db.db\n      .updateTable('draft')\n      .where('creator', '=', actorDid)\n      .where('key', '=', key)\n      .set({\n        updatedAt: now,\n        payload: payloadString,\n      })\n      .execute()\n  }\n\n  if (method === Method.DELETE) {\n    await db.db\n      .deleteFrom('draft')\n      .where('creator', '=', actorDid)\n      .where('key', '=', key)\n      .execute()\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/client/hosts.ts",
    "content": "import { Etcd3 } from 'etcd3'\nimport { EtcdMap } from '../../etcd'\nimport { dataplaneLogger as logger } from '../../logger'\n\n/**\n * Interface for a reactive list of hosts, i.e. for use with the dataplane client.\n */\nexport interface HostList {\n  get: () => Iterable<string>\n  onUpdate(handler: HostListHandler): void\n}\n\ntype HostListHandler = (hosts: Iterable<string>) => void\n\n/**\n * Maintains a reactive HostList based on a simple setter.\n */\nexport class BasicHostList implements HostList {\n  private hosts: Iterable<string>\n  private handlers: HostListHandler[] = []\n\n  constructor(hosts: Iterable<string>) {\n    this.hosts = hosts\n  }\n\n  get() {\n    return this.hosts\n  }\n\n  set(hosts: Iterable<string>) {\n    this.hosts = hosts\n    this.update()\n  }\n\n  private update() {\n    for (const handler of this.handlers) {\n      handler(this.hosts)\n    }\n  }\n\n  onUpdate(handler: HostListHandler) {\n    this.handlers.push(handler)\n  }\n}\n\n/**\n * Maintains a reactive HostList based on etcd key values under a given key prefix.\n * When fallback is provided, ensures that this fallback is used whenever no hosts are available.\n */\nexport class EtcdHostList implements HostList {\n  private kv: EtcdMap\n  private inner = new BasicHostList(new Set())\n  private fallback: Set<string>\n\n  constructor(etcd: Etcd3, prefix: string, fallback?: string[]) {\n    this.fallback = new Set(fallback)\n    this.kv = new EtcdMap(etcd, prefix)\n    this.update() // init fallback if necessary\n    this.kv.watcher.on('connected', (res) => {\n      logger.warn(\n        { watcherId: this.kv.watcher.id, header: res.header },\n        'EtcdHostList connected',\n      )\n    })\n    this.kv.watcher.on('disconnected', (err) => {\n      logger.warn(\n        { watcherId: this.kv.watcher.id, err },\n        'EtcdHostList disconnected',\n      )\n    })\n    this.kv.watcher.on('error', (err) => {\n      logger.error({ watcherId: this.kv.watcher.id, err }, 'EtcdHostList error')\n    })\n  }\n\n  async connect() {\n    await this.kv.connect()\n    this.update()\n    this.kv.onUpdate(() => this.update())\n  }\n\n  get() {\n    return this.inner.get()\n  }\n\n  private update() {\n    const hosts = new Set<string>()\n    for (const host of this.kv.values()) {\n      if (URL.canParse(host)) {\n        hosts.add(host)\n      }\n    }\n    if (hosts.size) {\n      this.inner.set(hosts)\n    } else if (this.fallback.size) {\n      this.inner.set(this.fallback)\n    }\n  }\n\n  onUpdate(handler: HostListHandler) {\n    this.inner.onUpdate(handler)\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/client/index.ts",
    "content": "import assert from 'node:assert'\nimport { randomInt } from 'node:crypto'\nimport {\n  Code,\n  ConnectError,\n  PromiseClient,\n  createPromiseClient,\n  makeAnyClient,\n} from '@connectrpc/connect'\nimport { createGrpcTransport } from '@connectrpc/connect-node'\nimport { Service } from '../../proto/bsky_connect'\nimport { HostList } from './hosts'\n\nexport * from './hosts'\nexport * from './util'\n\nexport type DataPlaneClient = PromiseClient<typeof Service>\ntype HttpVersion = '1.1' | '2'\nconst MAX_RETRIES = 3\n\nexport const createDataPlaneClient = (\n  hostList: HostList,\n  opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean },\n) => {\n  const clients = new DataPlaneClients(hostList, opts)\n  return makeAnyClient(Service, (method) => {\n    return async (...args) => {\n      let tries = 0\n      let error: unknown\n      let remainingClients = clients.get()\n      while (tries < MAX_RETRIES) {\n        const client = randomElement(remainingClients)\n        assert(client, 'no clients available')\n        try {\n          return await client[method.localName](...args)\n        } catch (err) {\n          if (\n            err instanceof ConnectError &&\n            (err.code === Code.Unavailable || err.code === Code.Aborted)\n          ) {\n            tries++\n            error = err\n            remainingClients = getRemainingClients(remainingClients, client)\n          } else {\n            throw err\n          }\n        }\n      }\n      assert(error)\n      throw error\n    }\n  }) as DataPlaneClient\n}\n\nexport { Code }\n\n/**\n * Uses a reactive HostList in order to maintain a pool of DataPlaneClients.\n * Each DataPlaneClient is cached per host so that it maintains connections\n * and other internal state when the underlying HostList is updated.\n */\nclass DataPlaneClients {\n  private clients: DataPlaneClient[] = []\n  private clientsByHost = new Map<string, DataPlaneClient>()\n\n  constructor(\n    private hostList: HostList,\n    private clientOpts: {\n      httpVersion?: HttpVersion\n      rejectUnauthorized?: boolean\n    },\n  ) {\n    this.refresh()\n    this.hostList.onUpdate(() => this.refresh())\n  }\n\n  get(): readonly DataPlaneClient[] {\n    return this.clients\n  }\n\n  private refresh() {\n    this.clients = []\n    for (const host of this.hostList.get()) {\n      let client = this.clientsByHost.get(host)\n      if (!client) {\n        client = this.createClient(host)\n        this.clientsByHost.set(host, client)\n      }\n      this.clients.push(client)\n    }\n  }\n\n  private createClient(host: string) {\n    return createBaseClient(host, this.clientOpts)\n  }\n}\n\nconst createBaseClient = (\n  baseUrl: string,\n  opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean },\n): DataPlaneClient => {\n  const { httpVersion = '2', rejectUnauthorized = true } = opts\n  const transport = createGrpcTransport({\n    baseUrl,\n    httpVersion,\n    acceptCompression: [],\n    nodeOptions: { rejectUnauthorized },\n  })\n  return createPromiseClient(Service, transport)\n}\n\nconst getRemainingClients = (\n  clients: readonly DataPlaneClient[],\n  lastClient: DataPlaneClient,\n) => {\n  if (clients.length < 2) return clients // no clients to choose from\n  return clients.filter((c) => c !== lastClient)\n}\n\nconst randomElement = <T>(arr: readonly T[]): T | undefined => {\n  if (arr.length === 0) return\n  return arr[randomInt(arr.length)]\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/client/util.ts",
    "content": "import { Code, ConnectError } from '@connectrpc/connect'\nimport * as ui8 from 'uint8arrays'\nimport { getDidKeyFromMultibase } from '@atproto/identity'\n\nexport const isDataplaneError = (\n  err: unknown,\n  code?: Code,\n): err is ConnectError => {\n  if (err instanceof ConnectError) {\n    return !code || err.code === code\n  }\n  return false\n}\n\nexport const unpackIdentityServices = (servicesBytes: Uint8Array) => {\n  const servicesStr = ui8.toString(servicesBytes, 'utf8')\n  if (!servicesStr) return {}\n  return JSON.parse(servicesStr) as UnpackedServices\n}\n\nexport const unpackIdentityKeys = (keysBytes: Uint8Array) => {\n  const keysStr = ui8.toString(keysBytes, 'utf8')\n  if (!keysStr) return {}\n  return JSON.parse(keysStr) as UnpackedKeys\n}\n\nexport const getServiceEndpoint = (\n  services: UnpackedServices,\n  opts: { id: string; type: string },\n) => {\n  const endpoint =\n    services[opts.id] &&\n    services[opts.id].Type === opts.type &&\n    validateUrl(services[opts.id].URL)\n  return endpoint || undefined\n}\n\nexport const getKeyAsDidKey = (keys: UnpackedKeys, opts: { id: string }) => {\n  const key =\n    keys[opts.id] &&\n    getDidKeyFromMultibase({\n      type: keys[opts.id].Type,\n      publicKeyMultibase: keys[opts.id].PublicKeyMultibase,\n    })\n  return key || undefined\n}\n\ntype UnpackedServices = Record<string, { Type: string; URL: string }>\n\ntype UnpackedKeys = Record<string, { Type: string; PublicKeyMultibase: string }>\n\nconst validateUrl = (urlStr: string): string | undefined => {\n  let url\n  try {\n    url = new URL(urlStr)\n  } catch {\n    return undefined\n  }\n  if (!['http:', 'https:'].includes(url.protocol)) {\n    return undefined\n  } else if (!url.hostname) {\n    return undefined\n  } else {\n    return urlStr\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/index.ts",
    "content": "export * from './server'\nexport * from './client'\nexport * from './bsync'\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/background.ts",
    "content": "import PQueue from 'p-queue'\nimport { dbLogger } from '../../logger'\nimport { Database } from './db'\n\n// A simple queue for in-process, out-of-band/backgrounded work\n\nexport class BackgroundQueue {\n  queue = new PQueue()\n  destroyed = false\n  constructor(public db: Database) {}\n\n  add(task: Task) {\n    if (this.destroyed) {\n      return\n    }\n    this.queue\n      .add(() => task(this.db))\n      .catch((err) => {\n        dbLogger.error({ err }, 'background queue task failed')\n      })\n  }\n\n  async processAll() {\n    await this.queue.onIdle()\n  }\n\n  // On destroy we stop accepting new tasks, but complete all pending/in-progress tasks.\n  // The application calls this only once http connections have drained (tasks no longer being added).\n  async destroy() {\n    this.destroyed = true\n    await this.queue.onIdle()\n  }\n}\n\ntype Task = (db: Database) => Promise<void>\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/database-schema.ts",
    "content": "import { Kysely } from 'kysely'\nimport * as activitySubscription from './tables/activity-subscription'\nimport * as actor from './tables/actor'\nimport * as actorBlock from './tables/actor-block'\nimport * as actorState from './tables/actor-state'\nimport * as actorSync from './tables/actor-sync'\nimport * as algo from './tables/algo'\nimport * as blobTakedown from './tables/blob-takedown'\nimport * as bookmark from './tables/bookmark'\nimport * as didCache from './tables/did-cache'\nimport * as draft from './tables/draft'\nimport * as duplicateRecord from './tables/duplicate-record'\nimport * as feedGenerator from './tables/feed-generator'\nimport * as feedItem from './tables/feed-item'\nimport * as follow from './tables/follow'\nimport * as label from './tables/label'\nimport * as labeler from './tables/labeler'\nimport * as like from './tables/like'\nimport * as list from './tables/list'\nimport * as listBlock from './tables/list-block'\nimport * as listItem from './tables/list-item'\nimport * as listMute from './tables/list-mute'\nimport * as mute from './tables/mute'\nimport * as notification from './tables/notification'\nimport * as notificationPushToken from './tables/notification-push-token'\nimport * as post from './tables/post'\nimport * as postAgg from './tables/post-agg'\nimport * as postEmbed from './tables/post-embed'\nimport * as postgate from './tables/post-gate'\nimport * as privateData from './tables/private-data'\nimport * as profile from './tables/profile'\nimport * as profileAgg from './tables/profile-agg'\nimport * as quote from './tables/quote'\nimport * as record from './tables/record'\nimport * as repost from './tables/repost'\nimport * as starterPack from './tables/starter-pack'\nimport * as subscription from './tables/subscription'\nimport * as suggestedFeed from './tables/suggested-feed'\nimport * as suggestedFollow from './tables/suggested-follow'\nimport * as taggedSuggestion from './tables/tagged-suggestion'\nimport * as threadgate from './tables/thread-gate'\nimport * as threadMute from './tables/thread-mute'\nimport * as verification from './tables/verification'\nimport * as viewParam from './tables/view-param'\n\nexport type DatabaseSchemaType = duplicateRecord.PartialDB &\n  profile.PartialDB &\n  profileAgg.PartialDB &\n  post.PartialDB &\n  postEmbed.PartialDB &\n  postAgg.PartialDB &\n  repost.PartialDB &\n  threadgate.PartialDB &\n  postgate.PartialDB &\n  feedItem.PartialDB &\n  follow.PartialDB &\n  like.PartialDB &\n  list.PartialDB &\n  listItem.PartialDB &\n  listMute.PartialDB &\n  listBlock.PartialDB &\n  mute.PartialDB &\n  actorBlock.PartialDB &\n  threadMute.PartialDB &\n  feedGenerator.PartialDB &\n  subscription.PartialDB &\n  actor.PartialDB &\n  actorState.PartialDB &\n  actorSync.PartialDB &\n  record.PartialDB &\n  notification.PartialDB &\n  notificationPushToken.PartialDB &\n  didCache.PartialDB &\n  label.PartialDB &\n  algo.PartialDB &\n  viewParam.PartialDB &\n  suggestedFollow.PartialDB &\n  suggestedFeed.PartialDB &\n  blobTakedown.PartialDB &\n  labeler.PartialDB &\n  starterPack.PartialDB &\n  taggedSuggestion.PartialDB &\n  quote.PartialDB &\n  verification.PartialDB &\n  privateData.PartialDB &\n  activitySubscription.PartialDB &\n  bookmark.PartialDB &\n  draft.PartialDB\n\nexport type DatabaseSchema = Kysely<DatabaseSchemaType>\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/db.ts",
    "content": "import assert from 'node:assert'\nimport EventEmitter from 'node:events'\nimport {\n  Kysely,\n  KyselyPlugin,\n  Migrator,\n  PluginTransformQueryArgs,\n  PluginTransformResultArgs,\n  PostgresDialect,\n  QueryResult,\n  RootOperationNode,\n  UnknownRow,\n} from 'kysely'\nimport { Pool as PgPool, types as pgTypes } from 'pg'\nimport TypedEmitter from 'typed-emitter'\nimport { dbLogger } from '../../../logger'\nimport { DatabaseSchema, DatabaseSchemaType } from './database-schema'\nimport * as migrations from './migrations'\nimport { CtxMigrationProvider } from './migrations/provider'\nimport { PgOptions } from './types'\n\nexport type { DatabaseSchema }\n\nexport class Database {\n  pool: PgPool\n  db: DatabaseSchema\n  migrator: Migrator\n  txEvt = new EventEmitter() as TxnEmitter\n  destroyed = false\n\n  constructor(\n    public opts: PgOptions,\n    instances?: { db: DatabaseSchema; pool: PgPool; migrator: Migrator },\n  ) {\n    // if instances are provided, use those\n    if (instances) {\n      this.db = instances.db\n      this.pool = instances.pool\n      this.migrator = instances.migrator\n      return\n    }\n\n    // else create a pool & connect\n    const { schema, url } = opts\n    const pool =\n      opts.pool ??\n      new PgPool({\n        connectionString: url,\n        max: opts.poolSize,\n        maxUses: opts.poolMaxUses,\n        idleTimeoutMillis: opts.poolIdleTimeoutMs,\n      })\n\n    // Select count(*) and other pg bigints as js integer\n    pgTypes.setTypeParser(pgTypes.builtins.INT8, (n) => parseInt(n, 10))\n\n    // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema)\n    if (schema && !/^[a-z_]+$/i.test(schema)) {\n      throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`)\n    }\n\n    pool.on('error', onPoolError)\n    pool.on('connect', (client) => {\n      client.on('error', onClientError)\n      // Used for trigram indexes, e.g. on actor search\n      client.query('SET pg_trgm.word_similarity_threshold TO .4;')\n      if (schema) {\n        // Shared objects such as extensions will go in the public schema\n        client.query(`SET search_path TO \"${schema}\",public;`)\n      }\n    })\n\n    this.pool = pool\n    this.db = new Kysely<DatabaseSchemaType>({\n      dialect: new PostgresDialect({ pool }),\n    })\n    this.migrator = new Migrator({\n      db: this.db,\n      migrationTableSchema: opts.schema,\n      provider: new CtxMigrationProvider(migrations, 'pg'),\n    })\n  }\n\n  get schema(): string | undefined {\n    return this.opts.schema\n  }\n\n  async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {\n    const leakyTxPlugin = new LeakyTxPlugin()\n    const { dbTxn, txRes } = await this.db\n      .withPlugin(leakyTxPlugin)\n      .transaction()\n      .execute(async (txn) => {\n        const dbTxn = new Database(this.opts, {\n          db: txn,\n          pool: this.pool,\n          migrator: this.migrator,\n        })\n        const txRes = await fn(dbTxn)\n          .catch(async (err) => {\n            leakyTxPlugin.endTx()\n            // ensure that all in-flight queries are flushed & the connection is open\n            await dbTxn.db.getExecutor().provideConnection(noopAsync)\n            throw err\n          })\n          .finally(() => leakyTxPlugin.endTx())\n        return { dbTxn, txRes }\n      })\n    dbTxn?.txEvt.emit('commit')\n    return txRes\n  }\n\n  get isTransaction() {\n    return this.db.isTransaction\n  }\n\n  assertTransaction() {\n    assert(this.isTransaction, 'Transaction required')\n  }\n\n  assertNotTransaction() {\n    assert(!this.isTransaction, 'Cannot be in a transaction')\n  }\n\n  onCommit(fn: () => void) {\n    this.assertTransaction()\n    this.txEvt.once('commit', fn)\n  }\n\n  async migrateToOrThrow(migration: string) {\n    if (this.schema) {\n      await this.db.schema.createSchema(this.schema).ifNotExists().execute()\n    }\n    const { error, results } = await this.migrator.migrateTo(migration)\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n\n  async migrateToLatestOrThrow() {\n    if (this.schema) {\n      await this.db.schema.createSchema(this.schema).ifNotExists().execute()\n    }\n    const { error, results } = await this.migrator.migrateToLatest()\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n\n  async close(): Promise<void> {\n    if (this.destroyed) return\n    await this.db.destroy()\n    this.destroyed = true\n  }\n}\n\nexport default Database\n\nconst onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error')\nconst onClientError = (err: Error) => dbLogger.error({ err }, 'db client error')\n\n// utils\n// -------\n\nclass LeakyTxPlugin implements KyselyPlugin {\n  private txOver = false\n\n  endTx() {\n    this.txOver = true\n  }\n\n  transformQuery(args: PluginTransformQueryArgs): RootOperationNode {\n    if (this.txOver) {\n      throw new Error('tx already failed')\n    }\n    return args.node\n  }\n\n  async transformResult(\n    args: PluginTransformResultArgs,\n  ): Promise<QueryResult<UnknownRow>> {\n    return args.result\n  }\n}\n\ntype TxnEmitter = TypedEmitter<TxnEvents>\n\ntype TxnEvents = {\n  commit: () => void\n}\n\nconst noopAsync = async () => {}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/index.ts",
    "content": "export * from './db'\nexport type { DatabaseSchema } from './db'\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230309T045948368Z-init.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\n// @TODO subject indexes, naming?\n// @TODO drop indexes in down()?\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  try {\n    // Add trigram support, supporting user search.\n    // Explicitly add to public schema, so the extension can be seen in all schemas.\n    await sql`create extension if not exists pg_trgm with schema public`.execute(\n      db,\n    )\n  } catch (err: unknown) {\n    // The \"if not exists\" isn't bulletproof against races, and we see test suites racing to\n    // create the extension. So we can just ignore errors indicating the extension already exists.\n    if (!err?.['detail']?.includes?.('(pg_trgm) already exists')) throw err\n  }\n\n  // duplicateRecords\n  await db.schema\n    .createTable('duplicate_record')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('duplicateOf', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .execute()\n\n  // profile\n  await db.schema\n    .createTable('profile')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('displayName', 'varchar')\n    .addColumn('description', 'varchar')\n    .addColumn('avatarCid', 'varchar')\n    .addColumn('bannerCid', 'varchar')\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .execute()\n  // for, eg, profile views\n  await db.schema\n    .createIndex('profile_creator_idx')\n    .on('profile')\n    .column('creator')\n    .execute()\n  await db.schema // Supports user search\n    .createIndex(`profile_display_name_tgrm_idx`)\n    .on('profile')\n    .using('gist')\n    .expression(sql`\"displayName\" gist_trgm_ops`)\n    .execute()\n\n  // post\n  await db.schema\n    .createTable('post')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('text', 'varchar', (col) => col.notNull())\n    .addColumn('replyRoot', 'varchar')\n    .addColumn('replyRootCid', 'varchar')\n    .addColumn('replyParent', 'varchar')\n    .addColumn('replyParentCid', 'varchar')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n  // for, eg, \"postsCount\" on profile views\n  await db.schema\n    .createIndex('post_creator_idx')\n    .on('post')\n    .column('creator')\n    .execute()\n  // for, eg, \"replyCount\" on posts in feed views\n  await db.schema\n    .createIndex('post_replyparent_idx')\n    .on('post')\n    .column('replyParent')\n    .execute()\n  await db.schema\n    .createIndex('post_order_by_idx')\n    .on('post')\n    .columns(['sortAt', 'cid'])\n    .execute()\n\n  // postEmbedImage\n  await db.schema\n    .createTable('post_embed_image')\n    .addColumn('postUri', 'varchar', (col) => col.notNull())\n    .addColumn('position', 'varchar', (col) => col.notNull())\n    .addColumn('imageCid', 'varchar', (col) => col.notNull())\n    .addColumn('alt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('post_embed_image_pkey', ['postUri', 'position'])\n    .execute()\n\n  // postEmbedExternal\n  await db.schema\n    .createTable('post_embed_external')\n    .addColumn('postUri', 'varchar', (col) => col.primaryKey())\n    .addColumn('uri', 'varchar', (col) => col.notNull())\n    .addColumn('title', 'varchar', (col) => col.notNull())\n    .addColumn('description', 'varchar', (col) => col.notNull())\n    .addColumn('thumbCid', 'varchar')\n    .execute()\n\n  // postEmbedRecord\n  await db.schema\n    .createTable('post_embed_record')\n    .addColumn('postUri', 'varchar', (col) => col.notNull())\n    .addColumn('embedUri', 'varchar', (col) => col.notNull())\n    .addColumn('embedCid', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('post_embed_record_pkey', ['postUri', 'embedUri'])\n    .execute()\n\n  // postHierarchy\n  await db.schema\n    .createTable('post_hierarchy')\n    .addColumn('uri', 'varchar', (col) => col.notNull())\n    .addColumn('ancestorUri', 'varchar', (col) => col.notNull())\n    .addColumn('depth', 'integer', (col) => col.notNull())\n    .addPrimaryKeyConstraint('post_hierarchy_pkey', ['uri', 'ancestorUri'])\n    .execute()\n  // Supports fetching all children for a post\n  await db.schema\n    .createIndex('post_hierarchy_ancestoruri_idx')\n    .on('post_hierarchy')\n    .column('ancestorUri')\n    .execute()\n\n  // repost\n  await db.schema\n    .createTable('repost')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addColumn('subjectCid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .addUniqueConstraint('repost_unique_subject', ['creator', 'subject'])\n    .execute()\n  // for, eg, \"repostCount\" on posts in feed views\n  await db.schema\n    .createIndex('repost_subject_idx')\n    .on('repost')\n    .column('subject')\n    .execute()\n  await db.schema\n    .createIndex('repost_order_by_idx')\n    .on('repost')\n    .columns(['sortAt', 'cid'])\n    .execute()\n\n  // feedItem\n  await db.schema\n    .createTable('feed_item')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('type', 'varchar', (col) => col.notNull())\n    .addColumn('postUri', 'varchar', (col) => col.notNull())\n    .addColumn('originatorDid', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .createIndex('feed_item_originator_idx')\n    .on('feed_item')\n    .column('originatorDid')\n    .execute()\n  await db.schema\n    .createIndex('feed_item_cursor_idx')\n    .on('feed_item')\n    .columns(['sortAt', 'cid'])\n    .execute()\n\n  // follow\n  await db.schema\n    .createTable('follow')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .addUniqueConstraint('follow_unique_subject', ['creator', 'subjectDid'])\n    .execute()\n  // for, eg, \"followersCount\" on profile views\n  await db.schema\n    .createIndex('follow_subjectdid_idx')\n    .on('follow')\n    .column('subjectDid')\n    .execute()\n\n  // like\n  await db.schema\n    .createTable('like')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addColumn('subjectCid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    // Aids in index uniqueness plus post like counts\n    .addUniqueConstraint('like_unique_subject', ['subject', 'creator'])\n    .execute()\n\n  // subscription\n  await db.schema\n    .createTable('subscription')\n    .addColumn('service', 'varchar', (col) => col.notNull())\n    .addColumn('method', 'varchar', (col) => col.notNull())\n    .addColumn('state', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('subscription_pkey', ['service', 'method'])\n    .execute()\n\n  // actor\n  await db.schema\n    .createTable('actor')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('handle', 'varchar', (col) => col.unique())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('takedownId', 'integer') // foreign key created in moderation-init migration\n    .execute()\n  await db.schema // Supports user search\n    .createIndex(`actor_handle_tgrm_idx`)\n    .on('actor')\n    .using('gist')\n    .expression(sql`\"handle\" gist_trgm_ops`)\n    .execute()\n\n  // actor sync state\n  await db.schema\n    .createTable('actor_sync')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('commitCid', 'varchar', (col) => col.notNull())\n    .addColumn('commitDataCid', 'varchar', (col) => col.notNull())\n    .addColumn('rebaseCount', 'integer', (col) => col.notNull())\n    .addColumn('tooBigCount', 'integer', (col) => col.notNull())\n    .execute()\n\n  //record\n  await db.schema\n    .createTable('record')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('json', 'text', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('takedownId', 'integer') // foreign key created in moderation-init migration\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  // record\n  await db.schema.dropTable('record').execute()\n  // actor\n  await db.schema.dropTable('actor').execute()\n  // subscription\n  await db.schema.dropTable('subscription').execute()\n  // like\n  await db.schema.dropTable('like').execute()\n  // follow\n  await db.schema.dropTable('follow').execute()\n  // feedItem\n  await db.schema.dropTable('feed_item').execute()\n  // repost\n  await db.schema.dropTable('repost').execute()\n  // postHierarchy\n  await db.schema.dropTable('post_hierarchy').execute()\n  // postEmbedRecord\n  await db.schema.dropTable('post_embed_record').execute()\n  // postEmbedExternal\n  await db.schema.dropTable('post_embed_external').execute()\n  // postEmbedImage\n  await db.schema.dropTable('post_embed_image').execute()\n  // post\n  await db.schema.dropTable('post').execute()\n  // profile\n  await db.schema.dropTable('profile').execute()\n  // duplicateRecords\n  await db.schema.dropTable('duplicate_record').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230408T152211201Z-notification-init.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Notifications\n  await db.schema\n    .createTable('notification')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('recordUri', 'varchar', (col) => col.notNull())\n    .addColumn('recordCid', 'varchar', (col) => col.notNull())\n    .addColumn('author', 'varchar', (col) => col.notNull())\n    .addColumn('reason', 'varchar', (col) => col.notNull())\n    .addColumn('reasonSubject', 'varchar')\n    .addColumn('sortAt', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .createIndex('notification_did_sortat_idx')\n    .on('notification')\n    .columns(['did', 'sortAt'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('notification').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230417T210628672Z-moderation-init.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // moderation actions\n  await db.schema\n    .createTable('moderation_action')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('action', 'varchar', (col) => col.notNull())\n    .addColumn('subjectType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar')\n    .addColumn('subjectCid', 'varchar')\n    .addColumn('reason', 'text', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('reversedAt', 'varchar')\n    .addColumn('reversedBy', 'varchar')\n    .addColumn('reversedReason', 'text')\n    .addColumn('createLabelVals', 'varchar')\n    .addColumn('negateLabelVals', 'varchar')\n    .execute()\n  await db.schema\n    .createTable('moderation_action_subject_blob')\n    .addColumn('actionId', 'integer', (col) =>\n      col.notNull().references('moderation_action.id'),\n    )\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('moderation_action_subject_blob_pkey', [\n      'actionId',\n      'cid',\n    ])\n    .execute()\n  await db.schema\n    .createIndex('moderation_action_subject_blob_cid_idx')\n    .on('moderation_action_subject_blob')\n    .column('cid')\n    .execute()\n\n  // moderation reports\n  await db.schema\n    .createTable('moderation_report')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('subjectType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar')\n    .addColumn('subjectCid', 'varchar')\n    .addColumn('reasonType', 'varchar', (col) => col.notNull())\n    .addColumn('reason', 'text')\n    .addColumn('reportedByDid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .execute()\n\n  // moderation report resolutions\n  await db.schema\n    .createTable('moderation_report_resolution')\n    .addColumn('reportId', 'integer', (col) =>\n      col.notNull().references('moderation_report.id'),\n    )\n    .addColumn('actionId', 'integer', (col) =>\n      col.notNull().references('moderation_action.id'),\n    )\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('moderation_report_resolution_pkey', [\n      'reportId',\n      'actionId',\n    ])\n    .execute()\n  await db.schema\n    .createIndex('moderation_report_resolution_action_id_idx')\n    .on('moderation_report_resolution')\n    .column('actionId')\n    .execute()\n\n  // labels\n  await db.schema\n    .createTable('label')\n    .addColumn('src', 'varchar', (col) => col.notNull())\n    .addColumn('uri', 'varchar', (col) => col.notNull())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('val', 'varchar', (col) => col.notNull())\n    .addColumn('neg', 'boolean', (col) => col.notNull())\n    .addColumn('cts', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('label_pkey', ['src', 'uri', 'cid', 'val'])\n    .execute()\n  await db.schema\n    .createIndex('label_uri_index')\n    .on('label')\n    .column('uri')\n    .execute()\n\n  // foreign keys\n  await db.schema\n    .alterTable('actor')\n    .addForeignKeyConstraint(\n      'actor_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_action',\n      ['id'],\n    )\n    .execute()\n  await db.schema\n    .alterTable('record')\n    .addForeignKeyConstraint(\n      'record_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_action',\n      ['id'],\n    )\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('record')\n    .dropConstraint('record_takedown_id_fkey')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .dropConstraint('actor_takedown_id_fkey')\n    .execute()\n  await db.schema.dropTable('label').execute()\n  await db.schema.dropTable('moderation_report_resolution').execute()\n  await db.schema.dropTable('moderation_report').execute()\n  await db.schema.dropTable('moderation_action_subject_blob').execute()\n  await db.schema.dropTable('moderation_action').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230420T211446071Z-did-cache.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('did_cache')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('doc', 'jsonb', (col) => col.notNull())\n    .addColumn('updatedAt', 'bigint', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('did_cache').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230427T194702079Z-notif-record-index.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Supports record deletion\n  await db.schema\n    .createIndex('notification_record_idx')\n    .on('notification')\n    .column('recordUri')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('notification_record_idx').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230605T144730094Z-post-profile-aggs.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('post_agg')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('likeCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .addColumn('replyCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .addColumn('repostCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .execute()\n  await db.schema\n    .createTable('profile_agg')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('followersCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .addColumn('followsCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .addColumn('postsCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('profile_agg').execute()\n  await db.schema.dropTable('post_agg').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230607T211442112Z-feed-generator-init.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('feed_generator')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('feedDid', 'varchar', (col) => col.notNull())\n    .addColumn('displayName', 'varchar')\n    .addColumn('description', 'varchar')\n    .addColumn('descriptionFacets', 'varchar')\n    .addColumn('avatarCid', 'varchar')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('feed_generator_creator_index')\n    .on('feed_generator')\n    .column('creator')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('feed_generator_creator_index').execute()\n  await db.schema.dropTable('feed_generator').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<any>): Promise<void> {\n  const { ref } = db.dynamic\n\n  // materialized views are difficult to change,\n  // so we parameterize them at runtime with contents of this table.\n  await db.schema\n    .createTable('view_param')\n    .addColumn('name', 'varchar', (col) => col.primaryKey())\n    .addColumn('value', 'varchar')\n    .execute()\n\n  await db\n    .insertInto('view_param')\n    .values([\n      { name: 'whats_hot_like_threshold', value: '2' },\n      { name: 'whats_hot_interval', value: '1day' },\n    ])\n    .execute()\n\n  // define view query for whats-hot feed\n  // tldr: scored by like count depreciated over time.\n\n  // From: https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d\n  // Score = (P-1) / (T+2)^G\n  // where,\n  // P = points of an item (and -1 is to negate submitters vote)\n  // T = time since submission (in hours)\n  // G = Gravity, defaults to 1.8 in news.arc\n\n  const likeCount = ref('post_agg.likeCount')\n  const indexedAt = ref('post.indexedAt')\n  const computeScore = sql<number>`round(1000000 * (${likeCount} / ((EXTRACT(epoch FROM AGE(now(), ${indexedAt}::timestamp)) / 3600 + 2) ^ 1.8)))`\n\n  const viewQb = db\n    .selectFrom('post')\n    .innerJoin('post_agg', 'post_agg.uri', 'post.uri')\n    .where(\n      'post.indexedAt',\n      '>',\n      db\n        .selectFrom('view_param')\n        .where('name', '=', 'whats_hot_interval')\n        .select(\n          sql`to_char(now() - value::interval, 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')`.as(\n            'val',\n          ),\n        ),\n    )\n    .where('post.replyParent', 'is', null)\n    .where(\n      'post_agg.likeCount',\n      '>',\n      db // helps cull result set that needs to be sorted\n        .selectFrom('view_param')\n        .where('name', '=', 'whats_hot_like_threshold')\n        .select(sql`value::integer`.as('val')),\n    )\n    .select(['post.uri as uri', 'post.cid as cid', computeScore.as('score')])\n\n  await db.schema\n    .createView('algo_whats_hot_view')\n    .materialized()\n    .as(viewQb)\n    .execute()\n\n  // unique index required for pg to refresh view w/ \"concurrently\" param.\n  await db.schema\n    .createIndex('algo_whats_hot_view_uri_idx')\n    .on('algo_whats_hot_view')\n    .column('uri')\n    .unique()\n    .execute()\n  await db.schema\n    .createIndex('algo_whats_hot_view_cursor_idx')\n    .on('algo_whats_hot_view')\n    .columns(['score', 'cid'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropView('algo_whats_hot_view').materialized().execute()\n  await db.schema.dropTable('view_param').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230608T201813132Z-mute-lists.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('list')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('name', 'varchar', (col) => col.notNull())\n    .addColumn('purpose', 'varchar', (col) => col.notNull())\n    .addColumn('description', 'varchar')\n    .addColumn('descriptionFacets', 'varchar')\n    .addColumn('avatarCid', 'varchar')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('list_creator_idx')\n    .on('list')\n    .column('creator')\n    .execute()\n\n  await db.schema\n    .createTable('list_item')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('listUri', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .addUniqueConstraint('list_item_unique_subject_in_list', [\n      'listUri',\n      'subjectDid',\n    ])\n    .execute()\n\n  await db.schema\n    .createIndex('list_item_creator_idx')\n    .on('list_item')\n    .column('creator')\n    .execute()\n\n  await db.schema\n    .createIndex('list_item_subject_idx')\n    .on('list_item')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createTable('list_mute')\n    .addColumn('listUri', 'varchar', (col) => col.notNull())\n    .addColumn('mutedByDid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('list_mute_pkey', ['mutedByDid', 'listUri'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('list_creator_idx').execute()\n  await db.schema.dropIndex('list_item_subject_idx').execute()\n  await db.schema.dropTable('list').execute()\n  await db.schema.dropTable('list_item').execute()\n  await db.schema.dropTable('list_mute').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230608T205147239Z-mutes.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('mute')\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('mutedByDid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('mute_pkey', ['mutedByDid', 'subjectDid'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('mute').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230609T153623961Z-blocks.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('actor_block')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .addUniqueConstraint('actor_block_unique_subject', [\n      'creator',\n      'subjectDid',\n    ])\n    .execute()\n  await db.schema\n    .createIndex('actor_block_subjectdid_idx')\n    .on('actor_block')\n    .column('subjectDid')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('actor_block_subjectdid_idx').execute()\n  await db.schema.dropTable('actor_block').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts",
    "content": "import { Kysely } from 'kysely'\n\n// Indexes to support efficient actor deletion/unindexing\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema // Also supports record deletes\n    .createIndex('duplicate_record_duplicate_of_idx')\n    .on('duplicate_record')\n    .column('duplicateOf')\n    .execute()\n  await db.schema\n    .createIndex('like_creator_idx')\n    .on('like')\n    .column('creator')\n    .execute()\n  await db.schema\n    .createIndex('notification_author_idx')\n    .on('notification')\n    .column('author')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('notification_author_idx').execute()\n  await db.schema.dropIndex('like_creator_idx').execute()\n  await db.schema.dropIndex('duplicate_record_duplicate_of_idx').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230610T203555962Z-suggested-follows.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('suggested_follow')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('order', 'integer', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('suggested_follow').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230611T215300060Z-actor-state.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('actor_state')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('lastSeenNotifs', 'varchar', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('actor_state').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230620T161134972Z-post-langs.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('post').addColumn('langs', 'jsonb').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('post').dropColumn('langs').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230627T212437895Z-optional-handle.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor')\n    .alterColumn('handle')\n    .dropNotNull()\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor')\n    .alterColumn('handle')\n    .setNotNull()\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('post_hierarchy').execute()\n  // recreate index that calculates e.g. \"replyCount\", turning it into a covering index\n  // for uri so that recursive query for post descendents can use an index-only scan.\n  await db.schema.dropIndex('post_replyparent_idx').execute()\n  await sql`create index \"post_replyparent_idx\" on \"post\" (\"replyParent\") include (\"uri\")`.execute(\n    db,\n  )\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('post_hierarchy')\n    .addColumn('uri', 'varchar', (col) => col.notNull())\n    .addColumn('ancestorUri', 'varchar', (col) => col.notNull())\n    .addColumn('depth', 'integer', (col) => col.notNull())\n    .addPrimaryKeyConstraint('post_hierarchy_pkey', ['uri', 'ancestorUri'])\n    .execute()\n  await db.schema\n    .createIndex('post_hierarchy_ancestoruri_idx')\n    .on('post_hierarchy')\n    .column('ancestorUri')\n    .execute()\n  await db.schema.dropIndex('post_replyparent_idx').execute()\n  await db.schema\n    .createIndex('post_replyparent_idx')\n    .on('post')\n    .column('replyParent')\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230703T045536691Z-feed-and-label-indices.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('label_cts_idx')\n    .on('label')\n    .column('cts')\n    .execute()\n  await db.schema.dropIndex('feed_item_originator_idx').execute()\n  await db.schema\n    .createIndex('feed_item_originator_cursor_idx')\n    .on('feed_item')\n    .columns(['originatorDid', 'sortAt', 'cid'])\n    .execute()\n  await db.schema\n    .createIndex('record_did_idx')\n    .on('record')\n    .column('did')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('label_cts_idx').execute()\n  await db.schema.dropIndex('feed_item_originator_cursor_idx').execute()\n  await db.schema\n    .createIndex('feed_item_originator_idx')\n    .on('feed_item')\n    .column('originatorDid')\n    .execute()\n  await db.schema.dropIndex('record_did_idx').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230720T164800037Z-posts-cursor-idx.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('post_creator_cursor_idx')\n    .on('post')\n    .columns(['creator', 'sortAt', 'cid'])\n    .execute()\n  await db.schema.dropIndex('post_creator_idx').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('post_creator_idx')\n    .on('post')\n    .column('creator')\n    .execute()\n  await db.schema.dropIndex('post_creator_cursor_idx').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // supports post deletion\n  await db.schema\n    .createIndex('feed_item_post_uri_idx')\n    .on('feed_item')\n    .column('postUri')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('feed_item_post_uri_idx').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230808T172902639Z-repo-rev.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor_sync')\n    .addColumn('repoRev', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor_sync').dropColumn('repoRev').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230810T203349843Z-action-duration.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_action')\n    .addColumn('durationInHours', 'integer')\n    .execute()\n  await db.schema\n    .alterTable('moderation_action')\n    .addColumn('expiresAt', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_action')\n    .dropColumn('durationInHours')\n    .execute()\n  await db.schema\n    .alterTable('moderation_action')\n    .dropColumn('expiresAt')\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230817T195936007Z-native-notifications.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('notification_push_token')\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('platform', 'varchar', (col) => col.notNull())\n    .addColumn('token', 'varchar', (col) => col.notNull())\n    .addColumn('appId', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('notification_push_token_pkey', ['did', 'token'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('notification_push_token').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230830T205507322Z-suggested-feeds.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('suggested_feed')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('order', 'integer', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('suggested_feed').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230904T211011773Z-block-lists.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('list_block')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .addUniqueConstraint('list_block_unique_subject', ['creator', 'subjectUri'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('list_block').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230906T222220386Z-thread-gating.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('thread_gate')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('postUri', 'varchar', (col) => col.notNull().unique())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .alterTable('post')\n    .addColumn('invalidReplyRoot', 'boolean')\n    .execute()\n  await db.schema\n    .alterTable('post')\n    .addColumn('violatesThreadGate', 'boolean')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('thread_gate').execute()\n  await db.schema.alterTable('post').dropColumn('invalidReplyRoot').execute()\n  await db.schema.alterTable('post').dropColumn('violatesThreadGate').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230920T213858047Z-add-tags-to-post.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('post').addColumn('tags', 'jsonb').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('post').dropColumn('tags').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20230929T192920807Z-record-cursor-indexes.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('like_creator_cursor_idx')\n    .on('like')\n    .columns(['creator', 'sortAt', 'cid'])\n    .execute()\n  await db.schema\n    .createIndex('follow_creator_cursor_idx')\n    .on('follow')\n    .columns(['creator', 'sortAt', 'cid'])\n    .execute()\n  await db.schema\n    .createIndex('follow_subject_cursor_idx')\n    .on('follow')\n    .columns(['subjectDid', 'sortAt', 'cid'])\n    .execute()\n\n  // drop old indices that are superseded by these\n  await db.schema.dropIndex('like_creator_idx').execute()\n  await db.schema.dropIndex('follow_subjectdid_idx').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('like_creator_idx')\n    .on('like')\n    .column('creator')\n    .execute()\n  await db.schema\n    .createIndex('follow_subjectdid_idx')\n    .on('follow')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema.dropIndex('like_creator_cursor_idx').execute()\n  await db.schema.dropIndex('follow_creator_cursor_idx').execute()\n  await db.schema.dropIndex('follow_subject_cursor_idx').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('moderation_event')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('action', 'varchar', (col) => col.notNull())\n    .addColumn('subjectType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar')\n    .addColumn('subjectCid', 'varchar')\n    .addColumn('comment', 'text')\n    .addColumn('meta', 'jsonb')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('reversedAt', 'varchar')\n    .addColumn('reversedBy', 'varchar')\n    .addColumn('durationInHours', 'integer')\n    .addColumn('expiresAt', 'varchar')\n    .addColumn('reversedReason', 'text')\n    .addColumn('createLabelVals', 'varchar')\n    .addColumn('negateLabelVals', 'varchar')\n    .addColumn('legacyRefId', 'integer')\n    .execute()\n  await db.schema\n    .createTable('moderation_subject_status')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n\n    // Identifiers\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    // Default to '' so that we can apply unique constraints on did and recordPath columns\n    .addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo(''))\n    .addColumn('blobCids', 'jsonb')\n    .addColumn('recordCid', 'varchar')\n\n    // human review team state\n    .addColumn('reviewState', 'varchar', (col) => col.notNull())\n    .addColumn('comment', 'varchar')\n    .addColumn('muteUntil', 'varchar')\n    .addColumn('lastReviewedAt', 'varchar')\n    .addColumn('lastReviewedBy', 'varchar')\n\n    // report state\n    .addColumn('lastReportedAt', 'varchar')\n\n    // visibility/intervention state\n    .addColumn('takendown', 'boolean', (col) => col.defaultTo(false).notNull())\n    .addColumn('suspendUntil', 'varchar')\n\n    // timestamps\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addUniqueConstraint('moderation_status_unique_idx', ['did', 'recordPath'])\n    .execute()\n\n  await db.schema\n    .createIndex('moderation_subject_status_blob_cids_idx')\n    .on('moderation_subject_status')\n    .using('gin')\n    .column('blobCids')\n    .execute()\n\n  // Move foreign keys from moderation_action to moderation_event\n  await db.schema\n    .alterTable('record')\n    .dropConstraint('record_takedown_id_fkey')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .dropConstraint('actor_takedown_id_fkey')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .addForeignKeyConstraint(\n      'actor_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_event',\n      ['id'],\n    )\n    .execute()\n  await db.schema\n    .alterTable('record')\n    .addForeignKeyConstraint(\n      'record_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_event',\n      ['id'],\n    )\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('moderation_event').execute()\n  await db.schema.dropTable('moderation_subject_status').execute()\n\n  // Revert foreign key constraints\n  await db.schema\n    .alterTable('record')\n    .dropConstraint('record_takedown_id_fkey')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .dropConstraint('actor_takedown_id_fkey')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .addForeignKeyConstraint(\n      'actor_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_action',\n      ['id'],\n    )\n    .execute()\n  await db.schema\n    .alterTable('record')\n    .addForeignKeyConstraint(\n      'record_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_action',\n      ['id'],\n    )\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20231220T225126090Z-blob-takedowns.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('blob_takedown')\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('takedownRef', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('blob_takedown_pkey', ['did', 'cid'])\n    .execute()\n\n  await db.schema\n    .alterTable('actor')\n    .dropConstraint('actor_takedown_id_fkey')\n    .execute()\n  await db.schema.alterTable('actor').dropColumn('takedownId').execute()\n  await db.schema\n    .alterTable('actor')\n    .addColumn('takedownRef', 'varchar')\n    .execute()\n\n  await db.schema\n    .alterTable('record')\n    .dropConstraint('record_takedown_id_fkey')\n    .execute()\n  await db.schema.alterTable('record').dropColumn('takedownId').execute()\n  await db.schema\n    .alterTable('record')\n    .addColumn('takedownRef', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('blob_takedown').execute()\n\n  await db.schema.alterTable('actor').dropColumn('takedownRef').execute()\n  await db.schema\n    .alterTable('actor')\n    .addColumn('takedownId', 'integer')\n    .execute()\n\n  await db.schema\n    .alterTable('actor')\n    .addForeignKeyConstraint(\n      'actor_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_event',\n      ['id'],\n    )\n    .execute()\n\n  await db.schema.alterTable('record').dropColumn('takedownRef').execute()\n  await db.schema\n    .alterTable('record')\n    .addColumn('takedownId', 'integer')\n    .execute()\n  await db.schema\n    .alterTable('record')\n    .addForeignKeyConstraint(\n      'record_takedown_id_fkey',\n      ['takedownId'],\n      'moderation_event',\n      ['id'],\n    )\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240124T023719200Z-tagged-suggestions.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('tagged_suggestion')\n    .addColumn('tag', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addColumn('subjectType', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('tagged_suggestion_pkey', ['tag', 'subject'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('tagged_suggestion').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240226T225725627Z-labelers.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('labeler')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n  await db.schema\n    .createIndex('labeler_order_by_idx')\n    .on('labeler')\n    .columns(['sortAt', 'cid'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('labeler').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240530T170337073Z-account-deactivation.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor')\n    .addColumn('upstreamStatus', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor').dropColumn('upstreamStatus').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('thread_mute')\n    .addColumn('rootUri', 'varchar', (col) => col.notNull())\n    .addColumn('mutedByDid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('thread_mute_pkey', ['rootUri', 'mutedByDid'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('thread_mute').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240606T222548219Z-starter-packs.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('starter_pack')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n  await db.schema\n    .createIndex('starter_pack_creator_order_by_idx')\n    .on('starter_pack')\n    .columns(['creator', 'sortAt', 'cid'])\n    .execute()\n  await db.schema\n    .alterTable('profile')\n    .addColumn('joinedViaStarterPackUri', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('profile')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .createIndex('profile_starter_pack_joined_idx')\n    .on('profile')\n    .columns(['joinedViaStarterPackUri', 'createdAt'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('starter_pack').execute()\n  await db.schema\n    .alterTable('profile')\n    .dropColumn('joinedViaStarterPackUri')\n    .execute()\n  await db.schema.alterTable('profile').dropColumn('createdAt').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240719T203853939Z-priority-notifs.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor_state')\n    .addColumn('priorityNotifs', 'boolean', (col) =>\n      col.notNull().defaultTo(false),\n    )\n    .execute()\n  await db.schema\n    .alterTable('actor_state')\n    .addColumn('lastSeenPriorityNotifs', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor_state')\n    .dropColumn('priorityNotifs')\n    .execute()\n  await db.schema\n    .alterTable('actor_state')\n    .dropColumn('lastSeenPriorityNotifs')\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('post_agg')\n    .addColumn('quoteCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('post_agg').dropColumn('quoteCount').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('quote')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addColumn('subjectCid', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n  await db.schema\n    .createIndex('quote_subject_cursor_idx')\n    .on('quote')\n    .columns(['subject', 'sortAt', 'cid'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('quote').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('post_gate')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('postUri', 'varchar', (col) => col.notNull().unique())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('post_gate').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('post')\n    .addColumn('violatesEmbeddingRules', 'boolean')\n    .execute()\n  await db.schema\n    .alterTable('post')\n    .addColumn('hasThreadGate', 'boolean')\n    .execute()\n  await db.schema\n    .alterTable('post')\n    .addColumn('hasPostGate', 'boolean')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('post')\n    .dropColumn('violatesEmbeddingRules')\n    .execute()\n  await db.schema.alterTable('post').dropColumn('hasThreadGate').execute()\n  await db.schema.alterTable('post').dropColumn('hasPostGate').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor_sync').dropColumn('commitDataCid').execute()\n  await db.schema.alterTable('actor_sync').dropColumn('rebaseCount').execute()\n  await db.schema.alterTable('actor_sync').dropColumn('tooBigCount').execute()\n  // Migration code\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor_sync')\n    .addColumn('commitDataCid', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .alterTable('actor_sync')\n    .addColumn('rebaseCount', 'integer', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .alterTable('actor_sync')\n    .addColumn('tooBigCount', 'integer', (col) => col.notNull())\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('profile')\n    .addColumn('pinnedPost', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('profile')\n    .addColumn('pinnedPostCid', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('profile').dropColumn('pinnedPost').execute()\n  await db.schema.alterTable('profile').dropColumn('pinnedPostCid').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20241114T153108102Z-add-starter-packs-name.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('starter_pack')\n    .addColumn('name', 'varchar')\n    .execute()\n\n  await db.schema // Supports starter pack search\n    .createIndex(`starter_pack_name_tgrm_idx`)\n    .on('starter_pack')\n    .using('gist')\n    .expression(sql`\"name\" gist_trgm_ops`)\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('starter_pack').dropColumn('name').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250116T222618297Z-post-embed-video.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // postEmbedVideo\n  await db.schema\n    .createTable('post_embed_video')\n    .addColumn('postUri', 'varchar', (col) => col.notNull())\n    .addColumn('videoCid', 'varchar', (col) => col.notNull())\n    .addColumn('alt', 'varchar')\n    .addPrimaryKeyConstraint('post_embed_video_pkey', ['postUri'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  // postEmbedVideo\n  await db.schema.dropTable('post_embed_video').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('label').addColumn('exp', 'varchar').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('label').dropColumn('exp').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250404T163421487Z-verifications.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('verification')\n    .addColumn('uri', 'varchar', (col) => col.notNull().primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('rkey', 'varchar', (col) => col.notNull())\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addUniqueConstraint('verification_unique_subject_creator', [\n      'subject',\n      'creator',\n    ])\n    .addColumn('handle', 'varchar', (col) => col.notNull())\n    .addColumn('displayName', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('sortedAt', 'varchar', (col) =>\n      col\n        .generatedAlwaysAs(sql`least(\"createdAt\", \"indexedAt\")`)\n        .stored()\n        .notNull(),\n    )\n    .execute()\n\n  await db.schema\n    .alterTable('actor')\n    .addColumn('trustedVerifier', 'boolean', (col) =>\n      col.notNull().defaultTo(false),\n    )\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor').dropColumn('trustedVerifier').execute()\n\n  await db.schema.dropTable('verification').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('like').addColumn('via', 'varchar').execute()\n  await db.schema.alterTable('like').addColumn('viaCid', 'varchar').execute()\n\n  await db.schema.alterTable('repost').addColumn('via', 'varchar').execute()\n  await db.schema.alterTable('repost').addColumn('viaCid', 'varchar').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('like').dropColumn('via').execute()\n  await db.schema.alterTable('like').dropColumn('viaCid').execute()\n\n  await db.schema.alterTable('repost').dropColumn('via').execute()\n  await db.schema.alterTable('repost').dropColumn('viaCid').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('record').addColumn('tags', 'jsonb').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('record').dropColumn('tags').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('private_data')\n    .addColumn('actorDid', 'varchar', (col) => col.notNull())\n    .addColumn('namespace', 'varchar', (col) => col.notNull())\n    .addColumn('key', 'varchar', (col) => col.notNull())\n    .addColumn('payload', 'text', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('private_data_pkey', [\n      'actorDid',\n      'namespace',\n      'key',\n    ])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('private_data').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('activity_subscription')\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('key', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('post', 'boolean', (col) => col.notNull())\n    .addColumn('reply', 'boolean', (col) => col.notNull())\n    .addPrimaryKeyConstraint('activity_subscription_pkey', ['creator', 'key'])\n    .addUniqueConstraint('activity_subscription_unique_creator_subject_did', [\n      'creator',\n      'subjectDid',\n    ])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('activity_subscription').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor')\n    .addColumn('ageAssuranceStatus', 'text')\n    .execute()\n\n  await db.schema\n    .alterTable('actor')\n    .addColumn('ageAssuranceLastInitiatedAt', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor').dropColumn('ageAssuranceStatus').execute()\n\n  await db.schema\n    .alterTable('actor')\n    .dropColumn('ageAssuranceLastInitiatedAt')\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250812T183735692Z-add-bookmarks.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('bookmark')\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('key', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar', (col) => col.notNull())\n    .addColumn('subjectCid', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    // Supports paginating over creator's bookmarks sorting by key.\n    .addPrimaryKeyConstraint('bookmark_pkey', ['creator', 'key'])\n    // Supports checking for bookmark presence by the creator on specific uris, and supports counting bookmarks by uri.\n    .addUniqueConstraint('bookmark_unique_uri_creator', [\n      'subjectUri',\n      'creator',\n    ])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('bookmark').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20250813T174955711Z-add-post-agg-bookmarks.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('post_agg')\n    .addColumn('bookmarkCount', 'bigint', (col) => col.notNull().defaultTo(0))\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('post_agg').dropColumn('bookmarkCount').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor')\n    .addColumn('ageAssuranceAccess', 'text')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .addColumn('ageAssuranceCountryCode', 'text')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .addColumn('ageAssuranceRegionCode', 'text')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor').dropColumn('ageAssuranceAccess').execute()\n  await db.schema\n    .alterTable('actor')\n    .dropColumn('ageAssuranceCountryCode')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .dropColumn('ageAssuranceRegionCode')\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/20260112T133951271Z-add-drafts.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('draft')\n    .addColumn('creator', 'varchar', (col) => col.notNull())\n    .addColumn('key', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('payload', 'text', (col) => col.notNull())\n    .addPrimaryKeyConstraint('draft_pkey', ['creator', 'key'])\n    .execute()\n\n  // Supports getting paginated drafts by updatedAt.\n  await db.schema\n    .createIndex('draft_creator_updated_at_key_idx')\n    .on('draft')\n    .columns(['creator', 'updatedAt', 'key'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('draft').execute()\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/index.ts",
    "content": "// NOTE this file can be edited by hand, but it is also appended to by the migration:create command.\n// It's important that every migration is exported from here with the proper name. We'd simplify\n// this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process.\n\nexport * as _20230309T045948368Z from './20230309T045948368Z-init'\nexport * as _20230408T152211201Z from './20230408T152211201Z-notification-init'\nexport * as _20230417T210628672Z from './20230417T210628672Z-moderation-init'\nexport * as _20230420T211446071Z from './20230420T211446071Z-did-cache'\nexport * as _20230427T194702079Z from './20230427T194702079Z-notif-record-index'\nexport * as _20230605T144730094Z from './20230605T144730094Z-post-profile-aggs'\nexport * as _20230607T211442112Z from './20230607T211442112Z-feed-generator-init'\nexport * as _20230608T155101190Z from './20230608T155101190Z-algo-whats-hot-view'\nexport * as _20230608T201813132Z from './20230608T201813132Z-mute-lists'\nexport * as _20230608T205147239Z from './20230608T205147239Z-mutes'\nexport * as _20230609T153623961Z from './20230609T153623961Z-blocks'\nexport * as _20230609T232122649Z from './20230609T232122649Z-actor-deletion-indexes'\nexport * as _20230610T203555962Z from './20230610T203555962Z-suggested-follows'\nexport * as _20230611T215300060Z from './20230611T215300060Z-actor-state'\nexport * as _20230620T161134972Z from './20230620T161134972Z-post-langs'\nexport * as _20230627T212437895Z from './20230627T212437895Z-optional-handle'\nexport * as _20230629T220835893Z from './20230629T220835893Z-remove-post-hierarchy'\nexport * as _20230703T045536691Z from './20230703T045536691Z-feed-and-label-indices'\nexport * as _20230720T164800037Z from './20230720T164800037Z-posts-cursor-idx'\nexport * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx'\nexport * as _20230808T172902639Z from './20230808T172902639Z-repo-rev'\nexport * as _20230810T203349843Z from './20230810T203349843Z-action-duration'\nexport * as _20230817T195936007Z from './20230817T195936007Z-native-notifications'\nexport * as _20230830T205507322Z from './20230830T205507322Z-suggested-feeds'\nexport * as _20230904T211011773Z from './20230904T211011773Z-block-lists'\nexport * as _20230906T222220386Z from './20230906T222220386Z-thread-gating'\nexport * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post'\nexport * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes'\nexport * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status'\nexport * as _20231220T225126090Z from './20231220T225126090Z-blob-takedowns'\nexport * as _20240124T023719200Z from './20240124T023719200Z-tagged-suggestions'\nexport * as _20240226T225725627Z from './20240226T225725627Z-labelers'\nexport * as _20240530T170337073Z from './20240530T170337073Z-account-deactivation'\nexport * as _20240606T171229898Z from './20240606T171229898Z-thread-mutes'\nexport * as _20240606T222548219Z from './20240606T222548219Z-starter-packs'\nexport * as _20240719T203853939Z from './20240719T203853939Z-priority-notifs'\nexport * as _20240723T220700077Z from './20240723T220700077Z-quotes-post-aggs'\nexport * as _20240723T220703655Z from './20240723T220703655Z-quotes'\nexport * as _20240801T193939827Z from './20240801T193939827Z-post-gate'\nexport * as _20240808T224251220Z from './20240808T224251220Z-post-gate-flags'\nexport * as _20240829T211238293Z from './20240829T211238293Z-simplify-actor-sync'\nexport * as _20240831T134810923Z from './20240831T134810923Z-pinned-posts'\nexport * as _20241114T153108102Z from './20241114T153108102Z-add-starter-packs-name'\nexport * as _20250116T222618297Z from './20250116T222618297Z-post-embed-video'\nexport * as _20250207T174822012Z from './20250207T174822012Z-add-label-exp'\nexport * as _20250404T163421487Z from './20250404T163421487Z-verifications'\nexport * as _20250526T023712742Z from './20250526T023712742Z-like-repost-via'\nexport * as _20250528T221913281Z from './20250528T221913281Z-add-record-tags'\nexport * as _20250602T190357447Z from './20250602T190357447Z-add-private-data'\nexport * as _20250611T140649895Z from './20250611T140649895Z-add-activity-subscription'\nexport * as _20250627T025331240Z from './20250627T025331240Z-add-actor-age-assurance-columns'\nexport * as _20250812T183735692Z from './20250812T183735692Z-add-bookmarks'\nexport * as _20250813T174955711Z from './20250813T174955711Z-add-post-agg-bookmarks'\nexport * as _20251120T004738098Z from './20251120T004738098Z-update-actor-age-assurance-v2'\nexport * as _20260112T133951271Z from './20260112T133951271Z-add-drafts'\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/migrations/provider.ts",
    "content": "import { Kysely, Migration, MigrationProvider } from 'kysely'\n\n// Passes a context argument to migrations. We use this to thread the dialect into migrations\n\nexport class CtxMigrationProvider<T> implements MigrationProvider {\n  constructor(\n    private migrations: Record<string, CtxMigration<T>>,\n    private ctx: T,\n  ) {}\n  async getMigrations(): Promise<Record<string, Migration>> {\n    const ctxMigrations: Record<string, Migration> = {}\n    Object.entries(this.migrations).forEach(([name, migration]) => {\n      ctxMigrations[name] = {\n        up: async (db) => await migration.up(db, this.ctx),\n        down: async (db) => await migration.down?.(db, this.ctx),\n      }\n    })\n    return ctxMigrations\n  }\n}\n\nexport interface CtxMigration<T> {\n  up(db: Kysely<unknown>, ctx: T): Promise<void>\n  down?(db: Kysely<unknown>, ctx: T): Promise<void>\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/pagination.ts",
    "content": "import { sql } from 'kysely'\nimport { ensureValidRecordKey } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AnyQb, DbRef } from './util'\n\ntype KeysetCursor = { primary: string; secondary: string }\ntype KeysetLabeledResult = {\n  primary: string | number\n  secondary: string | number\n}\n\n/**\n * The GenericKeyset is an abstract class that sets-up the interface and partial implementation\n * of a keyset-paginated cursor with two parts. There are three types involved:\n *  - Result: a raw result (i.e. a row from the db) containing data that will make-up a cursor.\n *    - E.g. { createdAt: '2022-01-01T12:00:00Z', cid: 'bafyx' }\n *  - LabeledResult: a Result processed such that the \"primary\" and \"secondary\" parts of the cursor are labeled.\n *    - E.g. { primary: '2022-01-01T12:00:00Z', secondary: 'bafyx' }\n *  - Cursor: the two string parts that make-up the packed/string cursor.\n *    - E.g. packed cursor '1641038400000__bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' }\n *\n * These types relate as such. Implementers define the relations marked with a *:\n *   Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor\n *                     ↳ SQL Condition\n */\nexport abstract class GenericKeyset<R, LR extends KeysetLabeledResult> {\n  constructor(\n    public primary: DbRef,\n    public secondary: DbRef,\n  ) {}\n  abstract labelResult(result: R): LR\n  abstract labeledResultToCursor(labeled: LR): KeysetCursor\n  abstract cursorToLabeledResult(cursor: KeysetCursor): LR\n  packFromResult(results: R | R[]): string | undefined {\n    const result = Array.isArray(results) ? results.at(-1) : results\n    if (!result) return\n    return this.pack(this.labelResult(result))\n  }\n  pack(labeled?: LR): string | undefined {\n    if (!labeled) return\n    const cursor = this.labeledResultToCursor(labeled)\n    return this.packCursor(cursor)\n  }\n  unpack(cursorStr?: string): LR | undefined {\n    const cursor = this.unpackCursor(cursorStr)\n    if (!cursor) return\n    return this.cursorToLabeledResult(cursor)\n  }\n  packCursor(cursor?: KeysetCursor): string | undefined {\n    if (!cursor) return\n    return `${cursor.primary}__${cursor.secondary}`\n  }\n  unpackCursor(cursorStr?: string): KeysetCursor | undefined {\n    if (!cursorStr) return\n    const result = cursorStr.split('__')\n    const [primary, secondary, ...others] = result\n    if (!primary || !secondary || others.length > 0) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary,\n      secondary,\n    }\n  }\n  getSql(labeled?: LR, direction?: 'asc' | 'desc', tryIndex?: boolean) {\n    if (labeled === undefined) return\n    if (tryIndex) {\n      // The tryIndex param will likely disappear and become the default implementation: here for now for gradual rollout query-by-query.\n      if (direction === 'asc') {\n        return sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}))`\n      } else {\n        return sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}))`\n      }\n    } else {\n      // @NOTE this implementation can struggle to use an index on (primary, secondary) for pagination due to the \"or\" usage.\n      if (direction === 'asc') {\n        return sql`((${this.primary} > ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} > ${labeled.secondary}))`\n      } else {\n        return sql`((${this.primary} < ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} < ${labeled.secondary}))`\n      }\n    }\n  }\n  paginate<QB extends AnyQb>(\n    qb: QB,\n    opts: {\n      limit?: number\n      cursor?: string\n      direction?: 'asc' | 'desc'\n      tryIndex?: boolean\n      // By default, pg does nullsFirst\n      nullsLast?: boolean\n    },\n  ): QB {\n    const { limit, cursor, direction = 'desc', tryIndex, nullsLast } = opts\n    const keysetSql = this.getSql(this.unpack(cursor), direction, tryIndex)\n    return qb\n      .if(!!limit, (q) => q.limit(limit as number))\n      .if(!nullsLast, (q) =>\n        q.orderBy(this.primary, direction).orderBy(this.secondary, direction),\n      )\n      .if(!!nullsLast, (q) =>\n        q\n          .orderBy(\n            direction === 'asc'\n              ? sql`${this.primary} asc nulls last`\n              : sql`${this.primary} desc nulls last`,\n          )\n          .orderBy(\n            direction === 'asc'\n              ? sql`${this.secondary} asc nulls last`\n              : sql`${this.secondary} desc nulls last`,\n          ),\n      )\n      .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB\n  }\n}\n\ntype SortAtCidResult = { sortAt: string; cid: string }\ntype TimeCidLabeledResult = KeysetCursor\n\nexport class TimeCidKeyset<\n  TimeCidResult = SortAtCidResult,\n> extends GenericKeyset<TimeCidResult, TimeCidLabeledResult> {\n  labelResult(result: TimeCidResult): TimeCidLabeledResult\n  labelResult<TimeCidResult extends SortAtCidResult>(result: TimeCidResult) {\n    return { primary: result.sortAt, secondary: result.cid }\n  }\n  labeledResultToCursor(labeled: TimeCidLabeledResult) {\n    return {\n      primary: new Date(labeled.primary).getTime().toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: KeysetCursor) {\n    const primaryDate = new Date(parseInt(cursor.primary, 10))\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n      secondary: cursor.secondary,\n    }\n  }\n}\n\nexport class CreatedAtDidKeyset extends TimeCidKeyset<{\n  createdAt: string\n  did: string // dids are treated identically to cids in TimeCidKeyset\n}> {\n  labelResult(result: { createdAt: string; did: string }) {\n    return { primary: result.createdAt, secondary: result.did }\n  }\n}\n\nexport class IndexedAtDidKeyset extends TimeCidKeyset<{\n  indexedAt: string\n  did: string // dids are treated identically to cids in TimeCidKeyset\n}> {\n  labelResult(result: { indexedAt: string; did: string }) {\n    return { primary: result.indexedAt, secondary: result.did }\n  }\n}\n\n/**\n * This is being deprecated. Use {@link GenericKeyset#paginate} instead.\n */\nexport const paginate = <\n  QB extends AnyQb,\n  K extends GenericKeyset<unknown, any>,\n>(\n  qb: QB,\n  opts: {\n    limit?: number\n    cursor?: string\n    direction?: 'asc' | 'desc'\n    keyset: K\n    tryIndex?: boolean\n    // By default, pg does nullsFirst\n    nullsLast?: boolean\n  },\n): QB => {\n  return opts.keyset.paginate(qb, opts)\n}\n\ntype SingleKeyCursor = {\n  primary: string\n}\n\ntype SingleKeyLabeledResult = {\n  primary: string | number\n}\n\n/**\n * GenericSingleKey is similar to {@link GenericKeyset} but for a single key cursor.\n */\nexport abstract class GenericSingleKey<R, LR extends SingleKeyLabeledResult> {\n  constructor(public primary: DbRef) {}\n  abstract labelResult(result: R): LR\n  abstract labeledResultToCursor(labeled: LR): SingleKeyCursor\n  abstract cursorToLabeledResult(cursor: SingleKeyCursor): LR\n  packFromResult(results: R | R[]): string | undefined {\n    const result = Array.isArray(results) ? results.at(-1) : results\n    if (!result) return\n    return this.pack(this.labelResult(result))\n  }\n  pack(labeled?: LR): string | undefined {\n    if (!labeled) return\n    const cursor = this.labeledResultToCursor(labeled)\n    return this.packCursor(cursor)\n  }\n  unpack(cursorStr?: string): LR | undefined {\n    const cursor = this.unpackCursor(cursorStr)\n    if (!cursor) return\n    return this.cursorToLabeledResult(cursor)\n  }\n  packCursor(cursor?: SingleKeyCursor): string | undefined {\n    if (!cursor) return\n    return cursor.primary\n  }\n  unpackCursor(cursorStr?: string): SingleKeyCursor | undefined {\n    if (!cursorStr) return\n    const result = cursorStr.split('__')\n    const [primary, ...others] = result\n    if (!primary || others.length > 0) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary,\n    }\n  }\n  getSql(labeled?: LR, direction?: 'asc' | 'desc') {\n    if (labeled === undefined) return\n    if (direction === 'asc') {\n      return sql`${this.primary} > ${labeled.primary}`\n    }\n    return sql`${this.primary} < ${labeled.primary}`\n  }\n  paginate<QB extends AnyQb>(\n    qb: QB,\n    opts: {\n      limit?: number\n      cursor?: string\n      direction?: 'asc' | 'desc'\n      // By default, pg does nullsFirst\n      nullsLast?: boolean\n    },\n  ): QB {\n    const { limit, cursor, direction = 'desc', nullsLast } = opts\n    const keySql = this.getSql(this.unpack(cursor), direction)\n    return qb\n      .if(!!limit, (q) => q.limit(limit as number))\n      .if(!nullsLast, (q) => q.orderBy(this.primary, direction))\n      .if(!!nullsLast, (q) =>\n        q.orderBy(\n          direction === 'asc'\n            ? sql`${this.primary} asc nulls last`\n            : sql`${this.primary} desc nulls last`,\n        ),\n      )\n      .if(!!keySql, (qb) => (keySql ? qb.where(keySql) : qb)) as QB\n  }\n}\n\ntype SortAtResult = { sortAt: string }\ntype TimeLabeledResult = SingleKeyCursor\n\nexport class IsoTimeKey<TimeResult = SortAtResult> extends GenericSingleKey<\n  TimeResult,\n  TimeLabeledResult\n> {\n  labelResult(result: TimeResult): TimeLabeledResult\n  labelResult<TimeResult extends SortAtResult>(result: TimeResult) {\n    return { primary: result.sortAt }\n  }\n  labeledResultToCursor(labeled: TimeLabeledResult) {\n    return {\n      primary: new Date(labeled.primary).toISOString(),\n    }\n  }\n  cursorToLabeledResult(cursor: SingleKeyCursor) {\n    const primaryDate = new Date(cursor.primary)\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n    }\n  }\n}\n\nexport class IsoSortAtKey extends IsoTimeKey<{\n  sortAt: string\n}> {\n  labelResult(result: { sortAt: string }) {\n    return { primary: result.sortAt }\n  }\n}\n\nexport class IsoUpdatedAtKey extends IsoTimeKey<{\n  updatedAt: string\n}> {\n  labelResult(result: { updatedAt: string }) {\n    return { primary: result.updatedAt }\n  }\n}\n\ntype KeyResult = { key: string }\ntype RkeyLabeledResult = SingleKeyCursor\n\nexport class RkeyKey<RkeyResult = KeyResult> extends GenericSingleKey<\n  RkeyResult,\n  RkeyLabeledResult\n> {\n  labelResult(result: RkeyResult): RkeyLabeledResult\n  labelResult<RkeyResult extends KeyResult>(result: RkeyResult) {\n    return { primary: result }\n  }\n  labeledResultToCursor(labeled: RkeyLabeledResult) {\n    return {\n      primary: labeled.primary,\n    }\n  }\n  cursorToLabeledResult(cursor: SingleKeyCursor) {\n    try {\n      ensureValidRecordKey(cursor.primary)\n      return {\n        primary: cursor.primary,\n      }\n    } catch {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n  }\n}\n\nexport class StashKeyKey extends RkeyKey<{\n  key: string\n}> {\n  labelResult(result: { key: string }) {\n    return { primary: result.key }\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/activity-subscription.ts",
    "content": "export const tableName = 'activity_subscription'\nexport interface ActivitySubscription {\n  creator: string\n  subjectDid: string\n  // key from the bsync stash.\n  key: string\n  indexedAt: string\n  post: boolean\n  reply: boolean\n}\n\nexport type PartialDB = { [tableName]: ActivitySubscription }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/actor-block.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'actor_block'\nexport interface ActorBlock {\n  uri: string\n  cid: string\n  creator: string\n  subjectDid: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: ActorBlock }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/actor-state.ts",
    "content": "export interface ActorState {\n  did: string\n  lastSeenNotifs: string\n  priorityNotifs: boolean\n  lastSeenPriorityNotifs: string | undefined\n}\n\nexport const tableName = 'actor_state'\n\nexport type PartialDB = { [tableName]: ActorState }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/actor-sync.ts",
    "content": "export interface ActorSync {\n  did: string\n  commitCid: string\n  repoRev: string | null\n}\n\nexport const tableName = 'actor_sync'\n\nexport type PartialDB = { [tableName]: ActorSync }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/actor.ts",
    "content": "import { Generated } from 'kysely'\n\nexport interface Actor {\n  did: string\n  handle: string | null\n  indexedAt: string\n  takedownRef: string | null\n  upstreamStatus: string | null\n  trustedVerifier: Generated<boolean>\n  ageAssuranceStatus: string | null\n  ageAssuranceLastInitiatedAt: string | null\n  ageAssuranceAccess: string | null\n  ageAssuranceCountryCode: string | null\n  ageAssuranceRegionCode: string | null\n}\n\nexport const tableName = 'actor'\n\nexport type PartialDB = { [tableName]: Actor }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/algo.ts",
    "content": "export const whatsHotViewTableName = 'algo_whats_hot_view'\n\nexport interface AlgoWhatsHotView {\n  uri: string\n  cid: string\n  score: number\n}\n\nexport type PartialDB = {\n  [whatsHotViewTableName]: AlgoWhatsHotView\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/blob-takedown.ts",
    "content": "export interface BlobTakedown {\n  did: string\n  cid: string\n  takedownRef: string\n}\n\nexport const tableName = 'blob_takedown'\n\nexport type PartialDB = { [tableName]: BlobTakedown }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/bookmark.ts",
    "content": "export interface Bookmark {\n  creator: string\n  key: string\n  subjectUri: string\n  subjectCid: string\n  indexedAt: string\n}\n\nexport const tableName = 'bookmark'\n\nexport type PartialDB = { [tableName]: Bookmark }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/did-cache.ts",
    "content": "import { DidDocument } from '@atproto/identity'\n\nexport interface DidCache {\n  did: string\n  doc: DidDocument\n  updatedAt: number\n}\n\nexport const tableName = 'did_cache'\n\nexport type PartialDB = {\n  [tableName]: DidCache\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/draft.ts",
    "content": "export interface Draft {\n  creator: string\n  key: string\n  createdAt: string\n  updatedAt: string\n  payload: string\n}\n\nexport const tableName = 'draft'\n\nexport type PartialDB = { [tableName]: Draft }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/duplicate-record.ts",
    "content": "export interface DuplicateRecord {\n  uri: string\n  cid: string\n  duplicateOf: string\n  indexedAt: string\n}\n\nexport const tableName = 'duplicate_record'\n\nexport type PartialDB = {\n  [tableName]: DuplicateRecord\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/feed-generator.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'feed_generator'\n\nexport interface FeedGenerator {\n  uri: string\n  cid: string\n  creator: string\n  feedDid: string\n  displayName: string\n  description: string | null\n  descriptionFacets: string | null\n  avatarCid: string | null\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = {\n  [tableName]: FeedGenerator\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/feed-item.ts",
    "content": "export const tableName = 'feed_item'\n\nexport interface FeedItem {\n  uri: string\n  cid: string\n  type: 'post' | 'repost'\n  postUri: string\n  originatorDid: string\n  sortAt: string\n}\n\nexport type PartialDB = { [tableName]: FeedItem }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/follow.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'follow'\n\nexport interface Follow {\n  uri: string\n  cid: string\n  creator: string\n  subjectDid: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: Follow }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/label.ts",
    "content": "export const tableName = 'label'\n\nexport interface Label {\n  src: string\n  uri: string\n  cid: string\n  val: string\n  neg: boolean\n  cts: string\n  exp: string | null\n}\n\nexport type PartialDB = { [tableName]: Label }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/labeler.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'labeler'\n\nexport interface Labeler {\n  uri: string\n  cid: string\n  creator: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = {\n  [tableName]: Labeler\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/like.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nconst tableName = 'like'\n\nexport interface Like {\n  uri: string\n  cid: string\n  creator: string\n  subject: string\n  subjectCid: string\n  via: string | null\n  viaCid: string | null\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: Like }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/list-block.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'list_block'\n\nexport interface ListBlock {\n  uri: string\n  cid: string\n  creator: string\n  subjectUri: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: ListBlock }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/list-item.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'list_item'\n\nexport interface ListItem {\n  uri: string\n  cid: string\n  creator: string\n  subjectDid: string\n  listUri: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: ListItem }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/list-mute.ts",
    "content": "export const tableName = 'list_mute'\n\nexport interface ListMute {\n  listUri: string\n  mutedByDid: string\n  createdAt: string\n}\n\nexport type PartialDB = { [tableName]: ListMute }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/list.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'list'\n\nexport interface List {\n  uri: string\n  cid: string\n  creator: string\n  name: string\n  purpose: string\n  description: string | null\n  descriptionFacets: string | null\n  avatarCid: string | null\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: List }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/mute.ts",
    "content": "export interface Mute {\n  subjectDid: string\n  mutedByDid: string\n  createdAt: string\n}\n\nexport const tableName = 'mute'\n\nexport type PartialDB = { [tableName]: Mute }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/notification-push-token.ts",
    "content": "export const tableName = 'notification_push_token'\n\nexport interface NotificationPushToken {\n  did: string\n  platform: 'ios' | 'android' | 'web'\n  token: string\n  appId: string\n}\n\nexport type PartialDB = { [tableName]: NotificationPushToken }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/notification.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const tableName = 'notification'\n\nexport interface Notification {\n  id: Generated<number>\n  did: string\n  recordUri: string\n  recordCid: string\n  author: string\n  reason: string\n  reasonSubject: string | null\n  sortAt: string\n}\n\nexport type PartialDB = { [tableName]: Notification }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/post-agg.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const tableName = 'post_agg'\n\nexport interface PostAgg {\n  uri: string\n  likeCount: Generated<number>\n  replyCount: Generated<number>\n  repostCount: Generated<number>\n  quoteCount: Generated<number>\n  bookmarkCount: Generated<number>\n}\n\nexport type PartialDB = {\n  [tableName]: PostAgg\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/post-embed.ts",
    "content": "export const imageTableName = 'post_embed_image'\nexport const externalTableName = 'post_embed_external'\nexport const recordTableName = 'post_embed_record'\nexport const videoTableName = 'post_embed_video'\n\nexport interface PostEmbedImage {\n  postUri: string\n  position: number\n  imageCid: string\n  alt: string\n}\n\nexport interface PostEmbedExternal {\n  postUri: string\n  uri: string\n  title: string\n  description: string\n  thumbCid: string | null\n}\n\nexport interface PostEmbedRecord {\n  postUri: string\n  embedUri: string\n  embedCid: string\n}\n\nexport interface PostEmbedVideo {\n  postUri: string\n  videoCid: string\n  alt: string | null\n}\n\nexport type PartialDB = {\n  [imageTableName]: PostEmbedImage\n  [externalTableName]: PostEmbedExternal\n  [recordTableName]: PostEmbedRecord\n  [videoTableName]: PostEmbedVideo\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/post-gate.ts",
    "content": "const tableName = 'post_gate'\n\nexport interface Postgate {\n  uri: string\n  cid: string\n  creator: string\n  postUri: string\n  createdAt: string\n  indexedAt: string\n}\n\nexport type PartialDB = { [tableName]: Postgate }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/post.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'post'\n\nexport interface Post {\n  uri: string\n  cid: string\n  creator: string\n  text: string\n  replyRoot: string | null\n  replyRootCid: string | null\n  replyParent: string | null\n  replyParentCid: string | null\n  langs: string[] | null\n  tags: string[] | null\n  invalidReplyRoot: boolean | null\n  violatesThreadGate: boolean | null\n  violatesEmbeddingRules: boolean | null\n  hasThreadGate: boolean | null\n  hasPostGate: boolean | null\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = {\n  [tableName]: Post\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/private-data.ts",
    "content": "export interface PrivateData {\n  actorDid: string\n  namespace: string\n  key: string\n  // JSON-encoded\n  payload: string\n  indexedAt: string\n  updatedAt: string\n}\n\nexport const tableName = 'private_data'\n\nexport type PartialDB = { [tableName]: PrivateData }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/profile-agg.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const tableName = 'profile_agg'\n\nexport interface ProfileAgg {\n  did: string\n  followersCount: Generated<number>\n  followsCount: Generated<number>\n  postsCount: Generated<number>\n}\n\nexport type PartialDB = {\n  [tableName]: ProfileAgg\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/profile.ts",
    "content": "export const tableName = 'profile'\n\nexport interface Profile {\n  uri: string\n  cid: string\n  creator: string\n  displayName: string | null\n  description: string | null\n  avatarCid: string | null\n  bannerCid: string | null\n  joinedViaStarterPackUri: string | null\n  pinnedPost: string | null\n  pinnedPostCid: string | null\n  createdAt: string\n  indexedAt: string\n}\nexport type PartialDB = { [tableName]: Profile }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/quote.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nconst tableName = 'quote'\n\nexport interface Quote {\n  uri: string\n  cid: string\n  subject: string\n  subjectCid: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: Quote }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/record.ts",
    "content": "import { ColumnType } from 'kysely'\n\nexport interface Record {\n  uri: string\n  cid: string\n  did: string\n  json: string\n  indexedAt: string\n  takedownRef: string | null\n  tags: ColumnType<string[] | null, string | undefined, string> | null\n}\n\nexport const tableName = 'record'\n\nexport type PartialDB = { [tableName]: Record }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/repost.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'repost'\n\nexport interface Repost {\n  uri: string\n  cid: string\n  creator: string\n  subject: string\n  subjectCid: string\n  via: string | null\n  viaCid: string | null\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = { [tableName]: Repost }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/starter-pack.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'starter_pack'\n\nexport interface StarterPack {\n  uri: string\n  cid: string\n  creator: string\n  name: string\n  createdAt: string\n  indexedAt: string\n  sortAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = {\n  [tableName]: StarterPack\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/subscription.ts",
    "content": "export const tableName = 'subscription'\n\nexport interface Subscription {\n  service: string\n  method: string\n  state: string\n}\n\nexport type PartialDB = { [tableName]: Subscription }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/suggested-feed.ts",
    "content": "export const tableName = 'suggested_feed'\n\nexport interface SuggestedFeed {\n  uri: string\n  order: number\n}\n\nexport type PartialDB = {\n  [tableName]: SuggestedFeed\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/suggested-follow.ts",
    "content": "export const tableName = 'suggested_follow'\n\nexport interface SuggestedFollow {\n  did: string\n  order: number\n}\n\nexport type PartialDB = {\n  [tableName]: SuggestedFollow\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/tagged-suggestion.ts",
    "content": "export const tableName = 'tagged_suggestion'\n\nexport interface TaggedSuggestion {\n  tag: string\n  subject: string\n  subjectType: string\n}\n\nexport type PartialDB = {\n  [tableName]: TaggedSuggestion\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/thread-gate.ts",
    "content": "const tableName = 'thread_gate'\n\nexport interface ThreadGate {\n  uri: string\n  cid: string\n  creator: string\n  postUri: string\n  createdAt: string\n  indexedAt: string\n}\n\nexport type PartialDB = { [tableName]: ThreadGate }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/thread-mute.ts",
    "content": "export const tableName = 'thread_mute'\n\nexport interface ThreadMute {\n  rootUri: string\n  mutedByDid: string\n  createdAt: string\n}\n\nexport type PartialDB = { [tableName]: ThreadMute }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/verification.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const tableName = 'verification'\n\nexport interface Verification {\n  uri: string\n  cid: string\n  rkey: string\n  creator: string\n  subject: string\n  handle: string\n  displayName: string\n  createdAt: string\n  indexedAt: string\n  sortedAt: GeneratedAlways<string>\n}\n\nexport type PartialDB = {\n  [tableName]: Verification\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/tables/view-param.ts",
    "content": "// @NOTE postgres-only\nexport const tableName = 'view_param'\n\n// materialized views are difficult to change,\n// so we parameterize them at runtime with contents of this table.\n// its contents are set in migrations, available param names are static.\nexport interface ViewParam {\n  name: string\n  value: string\n}\n\nexport type PartialDB = { [tableName]: ViewParam }\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/types.ts",
    "content": "import { Pool as PgPool } from 'pg'\n\nexport type PgOptions = {\n  url: string\n  pool?: PgPool\n  schema?: string\n  poolSize?: number\n  poolMaxUses?: number\n  poolIdleTimeoutMs?: number\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/db/util.ts",
    "content": "import {\n  DummyDriver,\n  DynamicModule,\n  ExpressionBuilder,\n  RawBuilder,\n  SelectQueryBuilder,\n  SqliteAdapter,\n  SqliteIntrospector,\n  SqliteQueryCompiler,\n  sql,\n} from 'kysely'\nimport { DatabaseSchema, DatabaseSchemaType } from './database-schema'\n\nexport const actorWhereClause = (actor: string) => {\n  if (actor.startsWith('did:')) {\n    return sql<0 | 1>`\"actor\".\"did\" = ${actor}`\n  } else {\n    return sql<0 | 1>`\"actor\".\"handle\" = ${actor}`\n  }\n}\n\n// Applies to actor or record table\nexport const notSoftDeletedClause = (alias: DbRef) => {\n  return sql`${alias}.\"takedownRef\" is null`\n}\n\nexport const softDeleted = (actorOrRecord: { takedownRef: string | null }) => {\n  return actorOrRecord.takedownRef !== null\n}\n\nexport const countAll = sql<number>`count(*)`\n\n// For use with doUpdateSet()\nexport const excluded = <T>(db: DatabaseSchema, col) => {\n  return sql<T>`${db.dynamic.ref(`excluded.${col}`)}`\n}\n\nexport const noMatch = sql`1 = 0`\n\n// Can be useful for large where-in clauses, to get the db to use a hash lookup on the list\nexport const valuesList = (vals: unknown[]) => {\n  return sql`(values (${sql.join(vals, sql`), (`)}))`\n}\n\nexport const dummyDialect = {\n  createAdapter() {\n    return new SqliteAdapter()\n  },\n  createDriver() {\n    return new DummyDriver()\n  },\n  createIntrospector(db) {\n    return new SqliteIntrospector(db)\n  },\n  createQueryCompiler() {\n    return new SqliteQueryCompiler()\n  },\n}\n\nexport type DbRef = RawBuilder | ReturnType<DynamicModule['ref']>\n\nexport type Subquery = ExpressionBuilder<DatabaseSchemaType, any>\n\nexport type AnyQb = SelectQueryBuilder<any, any, any>\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/index.ts",
    "content": "import events from 'node:events'\nimport http from 'node:http'\nimport { expressConnectMiddleware } from '@connectrpc/connect-express'\nimport express from 'express'\nimport { IdResolver, MemoryCache } from '@atproto/identity'\nimport { Database, DatabaseSchema } from './db'\nimport createRoutes from './routes'\n\nexport type { DatabaseSchema }\n\nexport { RepoSubscription } from './subscription'\n\nexport class DataPlaneServer {\n  constructor(\n    public server: http.Server,\n    public idResolver: IdResolver,\n  ) {}\n\n  static async create(db: Database, port: number, plcUrl?: string) {\n    const app = express()\n    const didCache = new MemoryCache()\n    const idResolver = new IdResolver({ plcUrl, didCache })\n    const routes = createRoutes(db, idResolver)\n    app.use(expressConnectMiddleware({ routes }))\n    const server = app.listen(port)\n    await events.once(server, 'listening')\n    return new DataPlaneServer(server, idResolver)\n  }\n\n  async destroy() {\n    return new Promise<void>((resolve, reject) => {\n      this.server.close((err) => {\n        if (err) {\n          reject(err)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/index.ts",
    "content": "import { Selectable, sql } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtpAgent, ComAtprotoSyncGetLatestCommit } from '@atproto/api'\nimport { DAY, HOUR } from '@atproto/common'\nimport { IdResolver, getPds } from '@atproto/identity'\nimport { ValidationError } from '@atproto/lexicon'\nimport {\n  VerifiedRepo,\n  WriteOpAction,\n  getAndParseRecord,\n  readCarWithRoot,\n  verifyRepo,\n} from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { subLogger } from '../../../logger'\nimport { retryXrpc } from '../../../util/retry'\nimport { BackgroundQueue } from '../background'\nimport { Database } from '../db'\nimport { Actor } from '../db/tables/actor'\nimport * as Block from './plugins/block'\nimport * as ChatDeclaration from './plugins/chat-declaration'\nimport * as FeedGenerator from './plugins/feed-generator'\nimport * as Follow from './plugins/follow'\nimport * as GermDeclaration from './plugins/germ-declaration'\nimport * as Labeler from './plugins/labeler'\nimport * as Like from './plugins/like'\nimport * as List from './plugins/list'\nimport * as ListBlock from './plugins/list-block'\nimport * as ListItem from './plugins/list-item'\nimport * as NotifDeclaration from './plugins/notif-declaration'\nimport * as Post from './plugins/post'\nimport * as Postgate from './plugins/post-gate'\nimport * as Profile from './plugins/profile'\nimport * as Repost from './plugins/repost'\nimport * as StarterPack from './plugins/starter-pack'\nimport * as Status from './plugins/status'\nimport * as Threadgate from './plugins/thread-gate'\nimport * as Verification from './plugins/verification'\nimport { RecordProcessor } from './processor'\n\nexport class IndexingService {\n  records: {\n    post: Post.PluginType\n    threadGate: Threadgate.PluginType\n    postGate: Postgate.PluginType\n    like: Like.PluginType\n    repost: Repost.PluginType\n    follow: Follow.PluginType\n    profile: Profile.PluginType\n    list: List.PluginType\n    listItem: ListItem.PluginType\n    listBlock: ListBlock.PluginType\n    block: Block.PluginType\n    feedGenerator: FeedGenerator.PluginType\n    starterPack: StarterPack.PluginType\n    labeler: Labeler.PluginType\n    notifDeclaration: NotifDeclaration.PluginType\n    chatDeclaration: ChatDeclaration.PluginType\n    germDeclaration: GermDeclaration.PluginType\n    verification: Verification.PluginType\n    status: Status.PluginType\n  }\n\n  constructor(\n    public db: Database,\n    public idResolver: IdResolver,\n    public background: BackgroundQueue,\n  ) {\n    this.records = {\n      post: Post.makePlugin(this.db, this.background),\n      threadGate: Threadgate.makePlugin(this.db, this.background),\n      postGate: Postgate.makePlugin(this.db, this.background),\n      like: Like.makePlugin(this.db, this.background),\n      repost: Repost.makePlugin(this.db, this.background),\n      follow: Follow.makePlugin(this.db, this.background),\n      profile: Profile.makePlugin(this.db, this.background),\n      list: List.makePlugin(this.db, this.background),\n      listItem: ListItem.makePlugin(this.db, this.background),\n      listBlock: ListBlock.makePlugin(this.db, this.background),\n      block: Block.makePlugin(this.db, this.background),\n      feedGenerator: FeedGenerator.makePlugin(this.db, this.background),\n      starterPack: StarterPack.makePlugin(this.db, this.background),\n      labeler: Labeler.makePlugin(this.db, this.background),\n      notifDeclaration: NotifDeclaration.makePlugin(this.db, this.background),\n      chatDeclaration: ChatDeclaration.makePlugin(this.db, this.background),\n      germDeclaration: GermDeclaration.makePlugin(this.db, this.background),\n      verification: Verification.makePlugin(this.db, this.background),\n      status: Status.makePlugin(this.db, this.background),\n    }\n  }\n\n  transact(txn: Database) {\n    txn.assertTransaction()\n    return new IndexingService(txn, this.idResolver, this.background)\n  }\n\n  async indexRecord(\n    uri: AtUri,\n    cid: CID,\n    obj: unknown,\n    action: WriteOpAction.Create | WriteOpAction.Update,\n    timestamp: string,\n    opts?: { disableNotifs?: boolean; disableLabels?: boolean },\n  ) {\n    this.db.assertNotTransaction()\n    await this.db.transaction(async (txn) => {\n      const indexingTx = this.transact(txn)\n      const indexer = indexingTx.findIndexerForCollection(uri.collection)\n      if (!indexer) return\n      if (action === WriteOpAction.Create) {\n        await indexer.insertRecord(uri, cid, obj, timestamp, opts)\n      } else {\n        await indexer.updateRecord(uri, cid, obj, timestamp)\n      }\n    })\n  }\n\n  async deleteRecord(uri: AtUri, cascading = false) {\n    this.db.assertNotTransaction()\n    await this.db.transaction(async (txn) => {\n      const indexingTx = this.transact(txn)\n      const indexer = indexingTx.findIndexerForCollection(uri.collection)\n      if (!indexer) return\n      await indexer.deleteRecord(uri, cascading)\n    })\n  }\n\n  async indexHandle(did: string, timestamp: string, force = false) {\n    this.db.assertNotTransaction()\n    const actor = await this.db.db\n      .selectFrom('actor')\n      .where('did', '=', did)\n      .selectAll()\n      .executeTakeFirst()\n    if (!force && !needsHandleReindex(actor, timestamp)) {\n      return\n    }\n    const atpData = await this.idResolver.did.resolveAtprotoData(did, true)\n    const handleToDid = await this.idResolver.handle.resolve(atpData.handle)\n\n    const handle: string | null =\n      did === handleToDid ? atpData.handle.toLowerCase() : null\n\n    const actorWithHandle =\n      handle !== null\n        ? await this.db.db\n            .selectFrom('actor')\n            .where('handle', '=', handle)\n            .selectAll()\n            .executeTakeFirst()\n        : null\n\n    // handle contention\n    if (handle && actorWithHandle && did !== actorWithHandle.did) {\n      await this.db.db\n        .updateTable('actor')\n        .where('actor.did', '=', actorWithHandle.did)\n        .set({ handle: null })\n        .execute()\n    }\n\n    const actorInfo = { handle, indexedAt: timestamp }\n    await this.db.db\n      .insertInto('actor')\n      .values({ did, ...actorInfo })\n      .onConflict((oc) => oc.column('did').doUpdateSet(actorInfo))\n      .returning('did')\n      .executeTakeFirst()\n  }\n\n  async indexRepo(did: string, commit?: string) {\n    this.db.assertNotTransaction()\n    const now = new Date().toISOString()\n    const { pds, signingKey } = await this.idResolver.did.resolveAtprotoData(\n      did,\n      true,\n    )\n    const { api } = new AtpAgent({ service: pds })\n\n    const { data: car } = await retryXrpc(() =>\n      api.com.atproto.sync.getRepo({ did }),\n    )\n    const { root, blocks } = await readCarWithRoot(car)\n    const verifiedRepo = await verifyRepo(blocks, root, did, signingKey)\n\n    const currRecords = await this.getCurrentRecords(did)\n    const repoRecords = formatCheckout(did, verifiedRepo)\n    const diff = findDiffFromCheckout(currRecords, repoRecords)\n\n    await Promise.all(\n      diff.map(async (op) => {\n        const { uri, cid } = op\n        try {\n          if (op.op === 'delete') {\n            await this.deleteRecord(uri)\n          } else {\n            const parsed = await getAndParseRecord(blocks, cid)\n            await this.indexRecord(\n              uri,\n              cid,\n              parsed.record,\n              op.op === 'create' ? WriteOpAction.Create : WriteOpAction.Update,\n              now,\n            )\n          }\n        } catch (err) {\n          if (err instanceof ValidationError) {\n            subLogger.warn(\n              { did, commit, uri: uri.toString(), cid: cid.toString() },\n              'skipping indexing of invalid record',\n            )\n          } else {\n            subLogger.error(\n              { err, did, commit, uri: uri.toString(), cid: cid.toString() },\n              'skipping indexing due to error processing record',\n            )\n          }\n        }\n      }),\n    )\n  }\n\n  async getCurrentRecords(did: string) {\n    const res = await this.db.db\n      .selectFrom('record')\n      .where('did', '=', did)\n      .select(['uri', 'cid'])\n      .execute()\n    return res.reduce(\n      (acc, cur) => {\n        acc[cur.uri] = {\n          uri: new AtUri(cur.uri),\n          cid: CID.parse(cur.cid),\n        }\n        return acc\n      },\n      {} as Record<string, { uri: AtUri; cid: CID }>,\n    )\n  }\n\n  async setCommitLastSeen(did: string, commit: CID, rev: string) {\n    const { ref } = this.db.db.dynamic\n    await this.db.db\n      .insertInto('actor_sync')\n      .values({\n        did,\n        commitCid: commit.toString(),\n        repoRev: rev ?? null,\n      })\n      .onConflict((oc) => {\n        const excluded = (col: string) => ref(`excluded.${col}`)\n        return oc.column('did').doUpdateSet({\n          commitCid: sql`${excluded('commitCid')}`,\n          repoRev: sql`${excluded('repoRev')}`,\n        })\n      })\n      .execute()\n  }\n\n  findIndexerForCollection(collection: string) {\n    const indexers = Object.values(\n      this.records as Record<string, RecordProcessor<unknown, unknown>>,\n    )\n    return indexers.find((indexer) => indexer.collection === collection)\n  }\n\n  async updateActorStatus(did: string, active: boolean, status: string = '') {\n    let upstreamStatus: string | null\n    if (active) {\n      upstreamStatus = null\n    } else if (['deactivated', 'suspended', 'takendown'].includes(status)) {\n      upstreamStatus = status\n    } else {\n      throw new Error(`Unrecognized account status: ${status}`)\n    }\n    await this.db.db\n      .updateTable('actor')\n      .set({ upstreamStatus })\n      .where('did', '=', did)\n      .execute()\n  }\n\n  async deleteActor(did: string) {\n    this.db.assertNotTransaction()\n    const actorIsHosted = await this.getActorIsHosted(did)\n    if (actorIsHosted === false) {\n      await this.db.db.deleteFrom('actor').where('did', '=', did).execute()\n      await this.unindexActor(did)\n      await this.db.db\n        .deleteFrom('notification')\n        .where('did', '=', did)\n        .execute()\n    }\n  }\n\n  private async getActorIsHosted(did: string) {\n    const doc = await this.idResolver.did.resolve(did, true)\n    const pds = doc && getPds(doc)\n    if (!pds) return false\n    const { api } = new AtpAgent({ service: pds })\n    try {\n      await retryXrpc(() => api.com.atproto.sync.getLatestCommit({ did }))\n      return true\n    } catch (err) {\n      if (err instanceof ComAtprotoSyncGetLatestCommit.RepoNotFoundError) {\n        return false\n      }\n      return null\n    }\n  }\n\n  async unindexActor(did: string) {\n    this.db.assertNotTransaction()\n    // per-record-type indexes\n    await this.db.db.deleteFrom('profile').where('creator', '=', did).execute()\n    await this.db.db.deleteFrom('follow').where('creator', '=', did).execute()\n    await this.db.db.deleteFrom('repost').where('creator', '=', did).execute()\n    await this.db.db.deleteFrom('like').where('creator', '=', did).execute()\n    await this.db.db\n      .deleteFrom('feed_generator')\n      .where('creator', '=', did)\n      .execute()\n    await this.db.db.deleteFrom('labeler').where('creator', '=', did).execute()\n    // lists\n    await this.db.db\n      .deleteFrom('list_item')\n      .where('creator', '=', did)\n      .execute()\n    await this.db.db.deleteFrom('list').where('creator', '=', did).execute()\n    // blocks\n    await this.db.db\n      .deleteFrom('actor_block')\n      .where('creator', '=', did)\n      .execute()\n    await this.db.db\n      .deleteFrom('list_block')\n      .where('creator', '=', did)\n      .execute()\n    // posts\n    const postByUser = (qb) =>\n      qb\n        .selectFrom('post')\n        .where('post.creator', '=', did)\n        .select('post.uri as uri')\n    await this.db.db\n      .deleteFrom('post_embed_image')\n      .where('post_embed_image.postUri', 'in', postByUser)\n      .execute()\n    await this.db.db\n      .deleteFrom('post_embed_external')\n      .where('post_embed_external.postUri', 'in', postByUser)\n      .execute()\n    await this.db.db\n      .deleteFrom('post_embed_record')\n      .where('post_embed_record.postUri', 'in', postByUser)\n      .execute()\n    await this.db.db.deleteFrom('post').where('creator', '=', did).execute()\n    await this.db.db\n      .deleteFrom('thread_gate')\n      .where('creator', '=', did)\n      .execute()\n    await this.db.db\n      .deleteFrom('post_gate')\n      .where('creator', '=', did)\n      .execute()\n    // notifications\n    await this.db.db\n      .deleteFrom('notification')\n      .where('notification.author', '=', did)\n      .execute()\n    // generic record indexes\n    await this.db.db\n      .deleteFrom('duplicate_record')\n      .where('duplicate_record.duplicateOf', 'in', (qb) =>\n        qb\n          .selectFrom('record')\n          .where('record.did', '=', did)\n          .select('record.uri as uri'),\n      )\n      .execute()\n    await this.db.db.deleteFrom('record').where('did', '=', did).execute()\n  }\n}\n\ntype UriAndCid = {\n  uri: AtUri\n  cid: CID\n}\n\ntype IndexOp =\n  | ({\n      op: 'create' | 'update'\n    } & UriAndCid)\n  | ({ op: 'delete' } & UriAndCid)\n\nconst findDiffFromCheckout = (\n  curr: Record<string, UriAndCid>,\n  checkout: Record<string, UriAndCid>,\n): IndexOp[] => {\n  const ops: IndexOp[] = []\n  for (const uri of Object.keys(checkout)) {\n    const record = checkout[uri]\n    if (!curr[uri]) {\n      ops.push({ op: 'create', ...record })\n    } else {\n      if (curr[uri].cid.equals(record.cid)) {\n        // no-op\n        continue\n      }\n      ops.push({ op: 'update', ...record })\n    }\n  }\n  for (const uri of Object.keys(curr)) {\n    const record = curr[uri]\n    if (!checkout[uri]) {\n      ops.push({ op: 'delete', ...record })\n    }\n  }\n  return ops\n}\n\nconst formatCheckout = (\n  did: string,\n  verifiedRepo: VerifiedRepo,\n): Record<string, UriAndCid> => {\n  const records: Record<string, UriAndCid> = {}\n  for (const create of verifiedRepo.creates) {\n    const uri = AtUri.make(did, create.collection, create.rkey)\n    records[uri.toString()] = {\n      uri,\n      cid: create.cid,\n    }\n  }\n  return records\n}\n\nconst needsHandleReindex = (\n  actor: Selectable<Actor> | undefined,\n  timestamp: string,\n) => {\n  if (!actor) return true\n  const timeDiff =\n    new Date(timestamp).getTime() - new Date(actor.indexedAt).getTime()\n  // revalidate daily\n  if (timeDiff > DAY) return true\n  // revalidate more aggressively for invalidated handles\n  if (actor.handle === null && timeDiff > HOUR) return true\n  return false\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/block.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Block from '../../../../lexicon/types/app/bsky/graph/block'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphBlock\ntype IndexedBlock = Selectable<DatabaseSchemaType['actor_block']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Block.Record,\n  timestamp: string,\n): Promise<IndexedBlock | null> => {\n  const inserted = await db\n    .insertInto('actor_block')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      subjectDid: obj.subject,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  obj: Block.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('actor_block')\n    .where('creator', '=', uri.host)\n    .where('subjectDid', '=', obj.subject)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedBlock | null> => {\n  const deleted = await db\n    .deleteFrom('actor_block')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<Block.Record, IndexedBlock>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/chat-declaration.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\n// @NOTE this indexer is a placeholder to ensure it gets indexed in the generic records table\n\nconst lexId = lex.ids.ChatBskyActorDeclaration\n\nconst insertFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n  _cid: CID,\n  _obj: unknown,\n  _timestamp: string,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<unknown, unknown>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  const processor = new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n  // @TODO use lexicon validation\n  processor.assertValidRecord = () => null\n  return processor\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/feed-generator.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as FeedGenerator from '../../../../lexicon/types/app/bsky/feed/generator'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyFeedGenerator\ntype IndexedFeedGenerator = Selectable<DatabaseSchemaType['feed_generator']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: FeedGenerator.Record,\n  timestamp: string,\n): Promise<IndexedFeedGenerator | null> => {\n  const inserted = await db\n    .insertInto('feed_generator')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      feedDid: obj.did,\n      displayName: obj.displayName,\n      description: obj.description,\n      descriptionFacets: obj.descriptionFacets\n        ? JSON.stringify(obj.descriptionFacets)\n        : undefined,\n      avatarCid: obj.avatar?.ref.toString(),\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedFeedGenerator | null> => {\n  const deleted = await db\n    .deleteFrom('feed_generator')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<\n  FeedGenerator.Record,\n  IndexedFeedGenerator\n>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/follow.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Follow from '../../../../lexicon/types/app/bsky/graph/follow'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { countAll, excluded } from '../../db/util'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphFollow\ntype IndexedFollow = Selectable<DatabaseSchemaType['follow']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Follow.Record,\n  timestamp: string,\n): Promise<IndexedFollow | null> => {\n  const inserted = await db\n    .insertInto('follow')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      subjectDid: obj.subject,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  obj: Follow.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('follow')\n    .where('creator', '=', uri.host)\n    .where('subjectDid', '=', obj.subject)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = (obj: IndexedFollow) => {\n  return [\n    {\n      did: obj.subjectDid,\n      author: obj.creator,\n      recordUri: obj.uri,\n      recordCid: obj.cid,\n      reason: 'follow' as const,\n      reasonSubject: null,\n      sortAt: obj.sortAt,\n    },\n  ]\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedFollow | null> => {\n  const deleted = await db\n    .deleteFrom('follow')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = (\n  deleted: IndexedFollow,\n  replacedBy: IndexedFollow | null,\n) => {\n  const toDelete = replacedBy ? [] : [deleted.uri]\n  return { notifs: [], toDelete }\n}\n\nconst updateAggregates = async (db: DatabaseSchema, follow: IndexedFollow) => {\n  const followersCountQb = db\n    .insertInto('profile_agg')\n    .values({\n      did: follow.subjectDid,\n      followersCount: db\n        .selectFrom('follow')\n        .where('follow.subjectDid', '=', follow.subjectDid)\n        .select(countAll.as('count')),\n    })\n    .onConflict((oc) =>\n      oc.column('did').doUpdateSet({\n        followersCount: excluded(db, 'followersCount'),\n      }),\n    )\n  const followsCountQb = db\n    .insertInto('profile_agg')\n    .values({\n      did: follow.creator,\n      followsCount: db\n        .selectFrom('follow')\n        .where('follow.creator', '=', follow.creator)\n        .select(countAll.as('count')),\n    })\n    .onConflict((oc) =>\n      oc.column('did').doUpdateSet({\n        followsCount: excluded(db, 'followsCount'),\n      }),\n    )\n  await Promise.all([followersCountQb.execute(), followsCountQb.execute()])\n}\n\nexport type PluginType = RecordProcessor<Follow.Record, IndexedFollow>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n    updateAggregates,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/germ-declaration.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\n// @NOTE this indexer is a placeholder to ensure it gets indexed in the generic records table\n\nconst lexId = lex.ids.ComGermnetworkDeclaration\n\nconst insertFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n  _cid: CID,\n  _obj: unknown,\n  _timestamp: string,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<unknown, unknown>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  const processor = new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n  // @TODO use lexicon validation\n  processor.assertValidRecord = () => null\n  return processor\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/labeler.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Labeler from '../../../../lexicon/types/app/bsky/labeler/service'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyLabelerService\ntype IndexedLabeler = Selectable<DatabaseSchemaType['labeler']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Labeler.Record,\n  timestamp: string,\n): Promise<IndexedLabeler | null> => {\n  if (uri.rkey !== 'self') return null\n  const inserted = await db\n    .insertInto('labeler')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedLabeler | null> => {\n  const deleted = await db\n    .deleteFrom('labeler')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<Labeler.Record, IndexedLabeler>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/like.ts",
    "content": "import { Insertable, Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Like from '../../../../lexicon/types/app/bsky/feed/like'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { Notification } from '../../db/tables/notification'\nimport { countAll, excluded } from '../../db/util'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyFeedLike\n\ntype Notif = Insertable<Notification>\ntype IndexedLike = Selectable<DatabaseSchemaType['like']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Like.Record,\n  timestamp: string,\n): Promise<IndexedLike | null> => {\n  const inserted = await db\n    .insertInto('like')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      subject: obj.subject.uri,\n      subjectCid: obj.subject.cid,\n      via: obj.via?.uri,\n      viaCid: obj.via?.cid,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  obj: Like.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('like')\n    .where('creator', '=', uri.host)\n    .where('subject', '=', obj.subject.uri)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = (obj: IndexedLike) => {\n  const subjectUri = new AtUri(obj.subject)\n  // prevent self-notifications\n  const isLikeFromSubjectUser = subjectUri.host === obj.creator\n  if (isLikeFromSubjectUser) {\n    return []\n  }\n\n  const notifs: Notif[] = [\n    // Notification to the author of the liked record.\n    {\n      did: subjectUri.host,\n      author: obj.creator,\n      recordUri: obj.uri,\n      recordCid: obj.cid,\n      reason: 'like' as const,\n      reasonSubject: subjectUri.toString(),\n      sortAt: obj.sortAt,\n    },\n  ]\n\n  if (obj.via) {\n    const viaUri = new AtUri(obj.via)\n    const isLikeFromViaSubjectUser = viaUri.host === obj.creator\n    // prevent self-notifications\n    if (!isLikeFromViaSubjectUser) {\n      notifs.push(\n        // Notification to the reposter via whose repost the like was made.\n        {\n          did: viaUri.host,\n          author: obj.creator,\n          recordUri: obj.uri,\n          recordCid: obj.cid,\n          reason: 'like-via-repost' as const,\n          reasonSubject: viaUri.toString(),\n          sortAt: obj.sortAt,\n        },\n      )\n    }\n  }\n\n  return notifs\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedLike | null> => {\n  const deleted = await db\n    .deleteFrom('like')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = (\n  deleted: IndexedLike,\n  replacedBy: IndexedLike | null,\n) => {\n  const toDelete = replacedBy ? [] : [deleted.uri]\n  return { notifs: [], toDelete }\n}\n\nconst updateAggregates = async (db: DatabaseSchema, like: IndexedLike) => {\n  const likeCountQb = db\n    .insertInto('post_agg')\n    .values({\n      uri: like.subject,\n      likeCount: db\n        .selectFrom('like')\n        .where('like.subject', '=', like.subject)\n        .select(countAll.as('count')),\n    })\n    .onConflict((oc) =>\n      oc.column('uri').doUpdateSet({ likeCount: excluded(db, 'likeCount') }),\n    )\n  await likeCountQb.execute()\n}\n\nexport type PluginType = RecordProcessor<Like.Record, IndexedLike>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n    updateAggregates,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/list-block.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as ListBlock from '../../../../lexicon/types/app/bsky/graph/listblock'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphListblock\ntype IndexedListBlock = Selectable<DatabaseSchemaType['list_block']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: ListBlock.Record,\n  timestamp: string,\n): Promise<IndexedListBlock | null> => {\n  const inserted = await db\n    .insertInto('list_block')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      subjectUri: obj.subject,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  obj: ListBlock.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('list_block')\n    .where('creator', '=', uri.host)\n    .where('subjectUri', '=', obj.subject)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedListBlock | null> => {\n  const deleted = await db\n    .deleteFrom('list_block')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<ListBlock.Record, IndexedListBlock>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/list-item.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as ListItem from '../../../../lexicon/types/app/bsky/graph/listitem'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphListitem\ntype IndexedListItem = Selectable<DatabaseSchemaType['list_item']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: ListItem.Record,\n  timestamp: string,\n): Promise<IndexedListItem | null> => {\n  const listUri = new AtUri(obj.list)\n  if (listUri.hostname !== uri.hostname) {\n    throw new InvalidRequestError(\n      'Creator of listitem does not match creator of list',\n    )\n  }\n  const inserted = await db\n    .insertInto('list_item')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      subjectDid: obj.subject,\n      listUri: obj.list,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  _uri: AtUri,\n  obj: ListItem.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('list_item')\n    .where('listUri', '=', obj.list)\n    .where('subjectDid', '=', obj.subject)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedListItem | null> => {\n  const deleted = await db\n    .deleteFrom('list_item')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<ListItem.Record, IndexedListItem>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/list.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as List from '../../../../lexicon/types/app/bsky/graph/list'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphList\ntype IndexedList = Selectable<DatabaseSchemaType['list']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: List.Record,\n  timestamp: string,\n): Promise<IndexedList | null> => {\n  const inserted = await db\n    .insertInto('list')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      name: obj.name,\n      purpose: obj.purpose,\n      description: obj.description,\n      descriptionFacets: obj.descriptionFacets\n        ? JSON.stringify(obj.descriptionFacets)\n        : undefined,\n      avatarCid: obj.avatar?.ref.toString(),\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedList | null> => {\n  const deleted = await db\n    .deleteFrom('list')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<List.Record, IndexedList>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/notif-declaration.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\n// @NOTE this indexer is a placeholder to ensure it gets indexed in the generic records table\nconst lexId = lex.ids.AppBskyNotificationDeclaration\n\nconst insertFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n  _cid: CID,\n  _obj: unknown,\n  _timestamp: string,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<unknown, unknown>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/post-gate.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Postgate from '../../../../lexicon/types/app/bsky/feed/postgate'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyFeedPostgate\ntype IndexedGate = DatabaseSchemaType['post_gate']\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Postgate.Record,\n  timestamp: string,\n): Promise<IndexedGate | null> => {\n  const postUri = new AtUri(obj.post)\n  if (postUri.host !== uri.host || postUri.rkey !== uri.rkey) {\n    throw new InvalidRequestError(\n      'Creator and rkey of post gate does not match its post',\n    )\n  }\n  const inserted = await db\n    .insertInto('post_gate')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      postUri: obj.post,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  await db\n    .updateTable('post')\n    .where('uri', '=', postUri.toString())\n    .set({ hasPostGate: true })\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  _uri: AtUri,\n  obj: Postgate.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('post_gate')\n    .where('postUri', '=', obj.post)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedGate | null> => {\n  const deleted = await db\n    .deleteFrom('post_gate')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  if (deleted) {\n    await db\n      .updateTable('post')\n      .where('uri', '=', deleted.postUri)\n      .set({ hasPostGate: false })\n      .executeTakeFirst()\n  }\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<Postgate.Record, IndexedGate>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/post.ts",
    "content": "import { Insertable, Selectable, sql } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { jsonStringToLex } from '@atproto/lexicon'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport { isMain as isEmbedExternal } from '../../../../lexicon/types/app/bsky/embed/external'\nimport { isMain as isEmbedImage } from '../../../../lexicon/types/app/bsky/embed/images'\nimport { isMain as isEmbedRecord } from '../../../../lexicon/types/app/bsky/embed/record'\nimport { isMain as isEmbedRecordWithMedia } from '../../../../lexicon/types/app/bsky/embed/recordWithMedia'\nimport { isMain as isEmbedVideo } from '../../../../lexicon/types/app/bsky/embed/video'\nimport {\n  Record as PostRecord,\n  ReplyRef,\n} from '../../../../lexicon/types/app/bsky/feed/post'\nimport { Record as PostgateRecord } from '../../../../lexicon/types/app/bsky/feed/postgate'\nimport { Record as GateRecord } from '../../../../lexicon/types/app/bsky/feed/threadgate'\nimport {\n  isLink,\n  isMention,\n} from '../../../../lexicon/types/app/bsky/richtext/facet'\nimport { $Typed } from '../../../../lexicon/util'\nimport {\n  postUriToPostgateUri,\n  postUriToThreadgateUri,\n  uriToDid,\n} from '../../../../util/uris'\nimport { RecordWithMedia } from '../../../../views/types'\nimport { parsePostgate } from '../../../../views/util'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { Notification } from '../../db/tables/notification'\nimport { countAll, excluded } from '../../db/util'\nimport {\n  getAncestorsAndSelfQb,\n  getDescendentsQb,\n  invalidReplyRoot as checkInvalidReplyRoot,\n  violatesThreadGate as checkViolatesThreadGate,\n} from '../../util'\nimport { RecordProcessor } from '../processor'\n\ntype Notif = Insertable<Notification>\ntype Post = Selectable<DatabaseSchemaType['post']>\ntype PostEmbedImage = DatabaseSchemaType['post_embed_image']\ntype PostEmbedExternal = DatabaseSchemaType['post_embed_external']\ntype PostEmbedRecord = DatabaseSchemaType['post_embed_record']\ntype PostEmbedVideo = DatabaseSchemaType['post_embed_video']\ntype PostAncestor = {\n  uri: string\n  height: number\n}\ntype PostDescendent = {\n  uri: string\n  depth: number\n  cid: string\n  creator: string\n  sortAt: string\n}\ntype IndexedPost = {\n  post: Post\n  facets?: { type: 'mention' | 'link'; value: string }[]\n  embeds?: (\n    | PostEmbedImage[]\n    | PostEmbedExternal\n    | PostEmbedRecord\n    | PostEmbedVideo\n  )[]\n  ancestors?: PostAncestor[]\n  descendents?: PostDescendent[]\n  threadgate?: GateRecord\n}\n\nconst lexId = lex.ids.AppBskyFeedPost\n\nconst REPLY_NOTIF_DEPTH = 5\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: PostRecord,\n  timestamp: string,\n): Promise<IndexedPost | null> => {\n  const post = {\n    uri: uri.toString(),\n    cid: cid.toString(),\n    creator: uri.host,\n    text: obj.text,\n    createdAt: normalizeDatetimeAlways(obj.createdAt),\n    replyRoot: obj.reply?.root?.uri || null,\n    replyRootCid: obj.reply?.root?.cid || null,\n    replyParent: obj.reply?.parent?.uri || null,\n    replyParentCid: obj.reply?.parent?.cid || null,\n    langs: obj.langs?.length\n      ? sql<string[]>`${JSON.stringify(obj.langs)}` // sidesteps kysely's array serialization, which is non-jsonb\n      : null,\n    tags: obj.tags?.length\n      ? sql<string[]>`${JSON.stringify(obj.tags)}` // sidesteps kysely's array serialization, which is non-jsonb\n      : null,\n    indexedAt: timestamp,\n  }\n  const [insertedPost] = await Promise.all([\n    db\n      .insertInto('post')\n      .values(post)\n      .onConflict((oc) => oc.doNothing())\n      .returningAll()\n      .executeTakeFirst(),\n    db\n      .insertInto('feed_item')\n      .values({\n        type: 'post',\n        uri: post.uri,\n        cid: post.cid,\n        postUri: post.uri,\n        originatorDid: post.creator,\n        sortAt:\n          post.indexedAt < post.createdAt ? post.indexedAt : post.createdAt,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .executeTakeFirst(),\n  ])\n  if (!insertedPost) {\n    return null // Post already indexed\n  }\n\n  if (obj.reply) {\n    const { invalidReplyRoot, violatesThreadGate } = await validateReply(\n      db,\n      uri.host,\n      obj.reply,\n    )\n    if (invalidReplyRoot || violatesThreadGate) {\n      Object.assign(insertedPost, { invalidReplyRoot, violatesThreadGate })\n      await db\n        .updateTable('post')\n        .where('uri', '=', post.uri)\n        .set({ invalidReplyRoot, violatesThreadGate })\n        .executeTakeFirst()\n    }\n  }\n\n  const facets = (obj.facets || [])\n    .flatMap((facet) => facet.features)\n    .flatMap((feature) => {\n      if (isMention(feature)) {\n        return {\n          type: 'mention' as const,\n          value: feature.did,\n        }\n      }\n      if (isLink(feature)) {\n        return {\n          type: 'link' as const,\n          value: feature.uri,\n        }\n      }\n      return []\n    })\n  // Embed indices\n  const embeds: (\n    | PostEmbedImage[]\n    | PostEmbedExternal\n    | PostEmbedRecord\n    | PostEmbedVideo\n  )[] = []\n  const postEmbeds = separateEmbeds(obj.embed)\n  for (const postEmbed of postEmbeds) {\n    if (isEmbedImage(postEmbed)) {\n      const { images } = postEmbed\n      const imagesEmbed = images.map((img, i) => ({\n        postUri: uri.toString(),\n        position: i,\n        imageCid: img.image.ref.toString(),\n        alt: img.alt,\n      }))\n      embeds.push(imagesEmbed)\n      await db.insertInto('post_embed_image').values(imagesEmbed).execute()\n    } else if (isEmbedExternal(postEmbed)) {\n      const { external } = postEmbed\n      const externalEmbed = {\n        postUri: uri.toString(),\n        uri: external.uri,\n        title: external.title,\n        description: external.description,\n        thumbCid: external.thumb?.ref.toString() || null,\n      }\n      embeds.push(externalEmbed)\n      await db.insertInto('post_embed_external').values(externalEmbed).execute()\n    } else if (isEmbedRecord(postEmbed)) {\n      const { record } = postEmbed\n      const embedUri = new AtUri(record.uri)\n      const recordEmbed = {\n        postUri: uri.toString(),\n        embedUri: record.uri,\n        embedCid: record.cid,\n      }\n      embeds.push(recordEmbed)\n      await db.insertInto('post_embed_record').values(recordEmbed).execute()\n\n      if (embedUri.collection === lex.ids.AppBskyFeedPost) {\n        const quote = {\n          uri: uri.toString(),\n          cid: cid.toString(),\n          subject: record.uri,\n          subjectCid: record.cid,\n          createdAt: normalizeDatetimeAlways(obj.createdAt),\n          indexedAt: timestamp,\n        }\n        await db\n          .insertInto('quote')\n          .values(quote)\n          .onConflict((oc) => oc.doNothing())\n          .returningAll()\n          .executeTakeFirst()\n\n        const quoteCountQb = db\n          .insertInto('post_agg')\n          .values({\n            uri: record.uri.toString(),\n            quoteCount: db\n              .selectFrom('quote')\n              .where('quote.subjectCid', '=', record.cid.toString())\n              .select(countAll.as('count')),\n          })\n          .onConflict((oc) =>\n            oc\n              .column('uri')\n              .doUpdateSet({ quoteCount: excluded(db, 'quoteCount') }),\n          )\n        await quoteCountQb.execute()\n\n        const { violatesEmbeddingRules } = await validatePostEmbed(\n          db,\n          embedUri.toString(),\n          uri.toString(),\n        )\n        Object.assign(insertedPost, {\n          violatesEmbeddingRules: violatesEmbeddingRules,\n        })\n        if (violatesEmbeddingRules) {\n          await db\n            .updateTable('post')\n            .where('uri', '=', insertedPost.uri)\n            .set({ violatesEmbeddingRules: violatesEmbeddingRules })\n            .executeTakeFirst()\n        }\n      }\n    } else if (isEmbedVideo(postEmbed)) {\n      const { video } = postEmbed\n      const videoEmbed = {\n        postUri: uri.toString(),\n        videoCid: video.ref.toString(),\n        // @NOTE: alt is required for image but not for video on the lexicon.\n        alt: postEmbed.alt ?? null,\n      }\n      embeds.push(videoEmbed)\n\n      await db.insertInto('post_embed_video').values(videoEmbed).execute()\n    }\n  }\n\n  const threadgate = await getThreadgateRecord(db, post.replyRoot || post.uri)\n  const ancestors = await getAncestorsAndSelfQb(db, {\n    uri: post.uri,\n    parentHeight: REPLY_NOTIF_DEPTH,\n  })\n    .selectFrom('ancestor')\n    .selectAll()\n    .execute()\n  const descendents = await getDescendentsQb(db, {\n    uri: post.uri,\n    depth: REPLY_NOTIF_DEPTH,\n  })\n    .selectFrom('descendent')\n    .innerJoin('post', 'post.uri', 'descendent.uri')\n    .selectAll('descendent')\n    .select(['cid', 'creator', 'sortAt'])\n    .execute()\n  return {\n    post: insertedPost,\n    facets,\n    embeds,\n    ancestors,\n    descendents,\n    threadgate,\n  }\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = (obj: IndexedPost) => {\n  const notifs: Notif[] = []\n  const notified = new Set([obj.post.creator])\n  const maybeNotify = (notif: Notif) => {\n    if (!notified.has(notif.did)) {\n      notified.add(notif.did)\n      notifs.push(notif)\n    }\n  }\n  for (const facet of obj.facets ?? []) {\n    if (facet.type === 'mention') {\n      maybeNotify({\n        did: facet.value,\n        reason: 'mention',\n        author: obj.post.creator,\n        recordUri: obj.post.uri,\n        recordCid: obj.post.cid,\n        sortAt: obj.post.sortAt,\n      })\n    }\n  }\n\n  if (!obj.post.violatesEmbeddingRules) {\n    for (const embed of obj.embeds ?? []) {\n      if ('embedUri' in embed) {\n        const embedUri = new AtUri(embed.embedUri)\n        if (embedUri.collection === lex.ids.AppBskyFeedPost) {\n          maybeNotify({\n            did: embedUri.host,\n            reason: 'quote',\n            reasonSubject: embedUri.toString(),\n            author: obj.post.creator,\n            recordUri: obj.post.uri,\n            recordCid: obj.post.cid,\n            sortAt: obj.post.sortAt,\n          })\n        }\n      }\n    }\n  }\n\n  if (obj.post.violatesThreadGate) {\n    // don't generate reply notifications when post violates threadgate\n    return notifs\n  }\n\n  const threadgateHiddenReplies = obj.threadgate?.hiddenReplies || []\n\n  // reply notifications\n\n  for (const ancestor of obj.ancestors ?? []) {\n    if (ancestor.uri === obj.post.uri) continue // no need to notify for own post\n    if (ancestor.height < REPLY_NOTIF_DEPTH) {\n      const ancestorUri = new AtUri(ancestor.uri)\n      maybeNotify({\n        did: ancestorUri.host,\n        reason: 'reply',\n        reasonSubject: ancestorUri.toString(),\n        author: obj.post.creator,\n        recordUri: obj.post.uri,\n        recordCid: obj.post.cid,\n        sortAt: obj.post.sortAt,\n      })\n      // found hidden reply, don't notify any higher ancestors\n      if (threadgateHiddenReplies.includes(ancestorUri.toString())) break\n    }\n  }\n\n  // descendents indicate out-of-order indexing: need to notify\n  // the current post and upwards.\n  for (const descendent of obj.descendents ?? []) {\n    for (const ancestor of obj.ancestors ?? []) {\n      const totalHeight = descendent.depth + ancestor.height\n      if (totalHeight < REPLY_NOTIF_DEPTH) {\n        const ancestorUri = new AtUri(ancestor.uri)\n        maybeNotify({\n          did: ancestorUri.host,\n          reason: 'reply',\n          reasonSubject: ancestorUri.toString(),\n          author: descendent.creator,\n          recordUri: descendent.uri,\n          recordCid: descendent.cid,\n          sortAt: descendent.sortAt,\n        })\n      }\n    }\n  }\n\n  return notifs\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedPost | null> => {\n  const uriStr = uri.toString()\n  const [deleted] = await Promise.all([\n    db\n      .deleteFrom('post')\n      .where('uri', '=', uriStr)\n      .returningAll()\n      .executeTakeFirst(),\n    db.deleteFrom('feed_item').where('postUri', '=', uriStr).executeTakeFirst(),\n  ])\n  await db.deleteFrom('quote').where('subject', '=', uriStr).execute()\n  const deletedEmbeds: (\n    | PostEmbedImage[]\n    | PostEmbedExternal\n    | PostEmbedRecord\n  )[] = []\n  const [deletedImgs, deletedExternals, deletedPosts] = await Promise.all([\n    db\n      .deleteFrom('post_embed_image')\n      .where('postUri', '=', uriStr)\n      .returningAll()\n      .execute(),\n    db\n      .deleteFrom('post_embed_external')\n      .where('postUri', '=', uriStr)\n      .returningAll()\n      .executeTakeFirst(),\n    db\n      .deleteFrom('post_embed_record')\n      .where('postUri', '=', uriStr)\n      .returningAll()\n      .executeTakeFirst(),\n  ])\n  if (deletedImgs.length) {\n    deletedEmbeds.push(deletedImgs)\n  }\n  if (deletedExternals) {\n    deletedEmbeds.push(deletedExternals)\n  }\n  if (deletedPosts) {\n    const embedUri = new AtUri(deletedPosts.embedUri)\n    deletedEmbeds.push(deletedPosts)\n\n    if (embedUri.collection === lex.ids.AppBskyFeedPost) {\n      await db.deleteFrom('quote').where('uri', '=', uriStr).execute()\n      await db\n        .insertInto('post_agg')\n        .values({\n          uri: deletedPosts.embedUri,\n          quoteCount: db\n            .selectFrom('quote')\n            .where('quote.subjectCid', '=', deletedPosts.embedCid.toString())\n            .select(countAll.as('count')),\n        })\n        .onConflict((oc) =>\n          oc\n            .column('uri')\n            .doUpdateSet({ quoteCount: excluded(db, 'quoteCount') }),\n        )\n        .execute()\n    }\n  }\n  return deleted\n    ? {\n        post: deleted,\n        facets: [], // Not used\n        embeds: deletedEmbeds,\n      }\n    : null\n}\n\nconst notifsForDelete = (\n  deleted: IndexedPost,\n  replacedBy: IndexedPost | null,\n) => {\n  const notifs = replacedBy ? notifsForInsert(replacedBy) : []\n  return {\n    notifs,\n    toDelete: [deleted.post.uri],\n  }\n}\n\nconst updateAggregates = async (db: DatabaseSchema, postIdx: IndexedPost) => {\n  const replyCountQb = postIdx.post.replyParent\n    ? db\n        .insertInto('post_agg')\n        .values({\n          uri: postIdx.post.replyParent,\n          replyCount: db\n            .selectFrom('post')\n            .where('post.replyParent', '=', postIdx.post.replyParent)\n            .where((qb) =>\n              qb\n                .where('post.violatesThreadGate', 'is', null)\n                .orWhere('post.violatesThreadGate', '=', false),\n            )\n            .select(countAll.as('count')),\n        })\n        .onConflict((oc) =>\n          oc\n            .column('uri')\n            .doUpdateSet({ replyCount: excluded(db, 'replyCount') }),\n        )\n    : null\n  const postsCountQb = db\n    .insertInto('profile_agg')\n    .values({\n      did: postIdx.post.creator,\n      postsCount: db\n        .selectFrom('post')\n        .where('post.creator', '=', postIdx.post.creator)\n        .select(countAll.as('count')),\n    })\n    .onConflict((oc) =>\n      oc.column('did').doUpdateSet({ postsCount: excluded(db, 'postsCount') }),\n    )\n  await Promise.all([replyCountQb?.execute(), postsCountQb.execute()])\n}\n\nexport type PluginType = RecordProcessor<PostRecord, IndexedPost>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n    updateAggregates,\n  })\n}\n\nexport default makePlugin\n\nfunction separateEmbeds(\n  embed: PostRecord['embed'],\n): Array<\n  | RecordWithMedia['media']\n  | $Typed<RecordWithMedia['record']>\n  | NonNullable<PostRecord['embed']>\n> {\n  if (!embed) {\n    return []\n  }\n  if (isEmbedRecordWithMedia(embed)) {\n    return [{ $type: lex.ids.AppBskyEmbedRecord, ...embed.record }, embed.media]\n  }\n  return [embed]\n}\n\nasync function validateReply(\n  db: DatabaseSchema,\n  creator: string,\n  reply: ReplyRef,\n) {\n  const replyRefs = await getReplyRefs(db, reply)\n  // check reply\n  const invalidReplyRoot =\n    !replyRefs.parent || checkInvalidReplyRoot(reply, replyRefs.parent)\n  // check interaction\n  const violatesThreadGate = await checkViolatesThreadGate(\n    db,\n    creator,\n    uriToDid(reply.root.uri),\n    replyRefs.root?.record ?? null,\n    replyRefs.gate?.record ?? null,\n  )\n  return {\n    invalidReplyRoot,\n    violatesThreadGate,\n  }\n}\n\nasync function getThreadgateRecord(db: DatabaseSchema, postUri: string) {\n  const threadgateRecordUri = postUriToThreadgateUri(postUri)\n  const results = await db\n    .selectFrom('record')\n    .where('record.uri', '=', threadgateRecordUri)\n    .selectAll()\n    .execute()\n  const threadgateRecord = results.find(\n    (ref) => ref.uri === threadgateRecordUri,\n  )\n  if (threadgateRecord) {\n    return jsonStringToLex(threadgateRecord.json) as GateRecord\n  }\n}\n\nasync function validatePostEmbed(\n  db: DatabaseSchema,\n  embedUri: string,\n  parentUri: string,\n) {\n  const postgateRecordUri = postUriToPostgateUri(embedUri)\n  const postgateRecord = await db\n    .selectFrom('record')\n    .where('record.uri', '=', postgateRecordUri)\n    .selectAll()\n    .executeTakeFirst()\n  if (!postgateRecord) {\n    return {\n      violatesEmbeddingRules: false,\n    }\n  }\n  const {\n    embeddingRules: { canEmbed },\n  } = parsePostgate({\n    gate: jsonStringToLex(postgateRecord.json) as PostgateRecord,\n    viewerDid: uriToDid(parentUri),\n    authorDid: uriToDid(embedUri),\n  })\n  if (canEmbed) {\n    return {\n      violatesEmbeddingRules: false,\n    }\n  }\n  return {\n    violatesEmbeddingRules: true,\n  }\n}\n\nasync function getReplyRefs(db: DatabaseSchema, reply: ReplyRef) {\n  const replyRoot = reply.root.uri\n  const replyParent = reply.parent.uri\n  const replyGate = postUriToThreadgateUri(replyRoot)\n  const results = await db\n    .selectFrom('record')\n    .where('record.uri', 'in', [replyRoot, replyGate, replyParent])\n    .leftJoin('post', 'post.uri', 'record.uri')\n    .selectAll('post')\n    .select(['record.uri', 'json'])\n    .execute()\n  const root = results.find((ref) => ref.uri === replyRoot)\n  const parent = results.find((ref) => ref.uri === replyParent)\n  const gate = results.find((ref) => ref.uri === replyGate)\n  return {\n    root: root && {\n      uri: root.uri,\n      invalidReplyRoot: root.invalidReplyRoot,\n      record: jsonStringToLex(root.json) as PostRecord,\n    },\n    parent: parent && {\n      uri: parent.uri,\n      invalidReplyRoot: parent.invalidReplyRoot,\n      record: jsonStringToLex(parent.json) as PostRecord,\n    },\n    gate: gate && {\n      uri: gate.uri,\n      record: jsonStringToLex(gate.json) as GateRecord,\n    },\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/profile.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Profile from '../../../../lexicon/types/app/bsky/actor/profile'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyActorProfile\ntype IndexedProfile = DatabaseSchemaType['profile']\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Profile.Record,\n  timestamp: string,\n): Promise<IndexedProfile | null> => {\n  if (uri.rkey !== 'self') return null\n  const inserted = await db\n    .insertInto('profile')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      displayName: obj.displayName,\n      description: obj.description,\n      avatarCid: obj.avatar?.ref.toString(),\n      bannerCid: obj.banner?.ref.toString(),\n      joinedViaStarterPackUri: obj.joinedViaStarterPack?.uri,\n      createdAt: obj.createdAt ?? new Date().toISOString(),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = (obj: IndexedProfile) => {\n  if (!obj.joinedViaStarterPackUri) return []\n  const starterPackUri = new AtUri(obj.joinedViaStarterPackUri)\n  return [\n    {\n      did: starterPackUri.host,\n      author: obj.creator,\n      recordUri: obj.uri,\n      recordCid: obj.cid,\n      reason: 'starterpack-joined' as const,\n      reasonSubject: obj.joinedViaStarterPackUri,\n      sortAt: obj.indexedAt,\n    },\n  ]\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedProfile | null> => {\n  const deleted = await db\n    .deleteFrom('profile')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<Profile.Record, IndexedProfile>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/repost.ts",
    "content": "import { Insertable, Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Repost from '../../../../lexicon/types/app/bsky/feed/repost'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { Notification } from '../../db/tables/notification'\nimport { countAll, excluded } from '../../db/util'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyFeedRepost\ntype Notif = Insertable<Notification>\ntype IndexedRepost = Selectable<DatabaseSchemaType['repost']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Repost.Record,\n  timestamp: string,\n): Promise<IndexedRepost | null> => {\n  const repost = {\n    uri: uri.toString(),\n    cid: cid.toString(),\n    creator: uri.host,\n    subject: obj.subject.uri,\n    subjectCid: obj.subject.cid,\n    via: obj.via?.uri,\n    viaCid: obj.via?.cid,\n    createdAt: normalizeDatetimeAlways(obj.createdAt),\n    indexedAt: timestamp,\n  }\n  const [inserted] = await Promise.all([\n    db\n      .insertInto('repost')\n      .values(repost)\n      .onConflict((oc) => oc.doNothing())\n      .returningAll()\n      .executeTakeFirst(),\n    db\n      .insertInto('feed_item')\n      .values({\n        type: 'repost',\n        uri: repost.uri,\n        cid: repost.cid,\n        postUri: repost.subject,\n        originatorDid: repost.creator,\n        sortAt:\n          repost.indexedAt < repost.createdAt\n            ? repost.indexedAt\n            : repost.createdAt,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .executeTakeFirst(),\n  ])\n\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  obj: Repost.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('repost')\n    .where('creator', '=', uri.host)\n    .where('subject', '=', obj.subject.uri)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = (obj: IndexedRepost) => {\n  const subjectUri = new AtUri(obj.subject)\n  // prevent self-notifications\n  const isRepostFromSubjectUser = subjectUri.host === obj.creator\n  if (isRepostFromSubjectUser) {\n    return []\n  }\n\n  const notifs: Notif[] = [\n    // Notification to the author of the reposted record.\n    {\n      did: subjectUri.host,\n      author: obj.creator,\n      recordUri: obj.uri,\n      recordCid: obj.cid,\n      reason: 'repost' as const,\n      reasonSubject: subjectUri.toString(),\n      sortAt: obj.sortAt,\n    },\n  ]\n\n  if (obj.via) {\n    const viaUri = new AtUri(obj.via)\n    const isRepostFromViaSubjectUser = viaUri.host === obj.creator\n    // prevent self-notifications\n    if (!isRepostFromViaSubjectUser) {\n      notifs.push(\n        // Notification to the reposter via whose repost the repost was made.\n        {\n          did: viaUri.host,\n          author: obj.creator,\n          recordUri: obj.uri,\n          recordCid: obj.cid,\n          reason: 'repost-via-repost' as const,\n          reasonSubject: viaUri.toString(),\n          sortAt: obj.sortAt,\n        },\n      )\n    }\n  }\n\n  return notifs\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedRepost | null> => {\n  const uriStr = uri.toString()\n  const [deleted] = await Promise.all([\n    db\n      .deleteFrom('repost')\n      .where('uri', '=', uriStr)\n      .returningAll()\n      .executeTakeFirst(),\n    db.deleteFrom('feed_item').where('uri', '=', uriStr).executeTakeFirst(),\n  ])\n  return deleted || null\n}\n\nconst notifsForDelete = (\n  deleted: IndexedRepost,\n  replacedBy: IndexedRepost | null,\n) => {\n  const toDelete = replacedBy ? [] : [deleted.uri]\n  return { notifs: [], toDelete }\n}\n\nconst updateAggregates = async (db: DatabaseSchema, repost: IndexedRepost) => {\n  const repostCountQb = db\n    .insertInto('post_agg')\n    .values({\n      uri: repost.subject,\n      repostCount: db\n        .selectFrom('repost')\n        .where('repost.subject', '=', repost.subject)\n        .select(countAll.as('count')),\n    })\n    .onConflict((oc) =>\n      oc\n        .column('uri')\n        .doUpdateSet({ repostCount: excluded(db, 'repostCount') }),\n    )\n  await repostCountQb.execute()\n}\n\nexport type PluginType = RecordProcessor<Repost.Record, IndexedRepost>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n    updateAggregates,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/starter-pack.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as StarterPack from '../../../../lexicon/types/app/bsky/graph/starterpack'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphStarterpack\ntype IndexedStarterPack = Selectable<DatabaseSchemaType['starter_pack']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: StarterPack.Record,\n  timestamp: string,\n): Promise<IndexedStarterPack | null> => {\n  const inserted = await db\n    .insertInto('starter_pack')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      name: obj.name,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedStarterPack | null> => {\n  const deleted = await db\n    .deleteFrom('starter_pack')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<StarterPack.Record, IndexedStarterPack>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/status.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\n// @NOTE this indexer is a placeholder to ensure it gets indexed in the generic records table\n\nconst lexId = lex.ids.AppBskyActorStatus\n\nconst insertFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n  _cid: CID,\n  _obj: unknown,\n  _timestamp: string,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst findDuplicate = async (): Promise<AtUri | null> => {\n  return null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  _db: DatabaseSchema,\n  uri: AtUri,\n): Promise<unknown | null> => {\n  if (uri.rkey !== 'self') return null\n  return true\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<unknown, unknown>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  const processor = new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n  return processor\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/thread-gate.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Threadgate from '../../../../lexicon/types/app/bsky/feed/threadgate'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyFeedThreadgate\ntype IndexedGate = DatabaseSchemaType['thread_gate']\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Threadgate.Record,\n  timestamp: string,\n): Promise<IndexedGate | null> => {\n  const postUri = new AtUri(obj.post)\n  if (postUri.host !== uri.host || postUri.rkey !== uri.rkey) {\n    throw new InvalidRequestError(\n      'Creator and rkey of thread gate does not match its post',\n    )\n  }\n  const inserted = await db\n    .insertInto('thread_gate')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      creator: uri.host,\n      postUri: obj.post,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  await db\n    .updateTable('post')\n    .where('uri', '=', postUri.toString())\n    .set({ hasThreadGate: true })\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  _uri: AtUri,\n  obj: Threadgate.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('thread_gate')\n    .where('postUri', '=', obj.post)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = () => {\n  return []\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedGate | null> => {\n  const deleted = await db\n    .deleteFrom('thread_gate')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  if (deleted) {\n    await db\n      .updateTable('post')\n      .where('uri', '=', deleted.postUri)\n      .set({ hasThreadGate: false })\n      .executeTakeFirst()\n  }\n  return deleted || null\n}\n\nconst notifsForDelete = () => {\n  return { notifs: [], toDelete: [] }\n}\n\nexport type PluginType = RecordProcessor<Threadgate.Record, IndexedGate>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/plugins/verification.ts",
    "content": "import { Selectable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'\nimport * as lex from '../../../../lexicon/lexicons'\nimport * as Verification from '../../../../lexicon/types/app/bsky/graph/verification'\nimport { BackgroundQueue } from '../../background'\nimport { Database } from '../../db'\nimport { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'\nimport { RecordProcessor } from '../processor'\n\nconst lexId = lex.ids.AppBskyGraphVerification\ntype IndexedVerification = Selectable<DatabaseSchemaType['verification']>\n\nconst insertFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  cid: CID,\n  obj: Verification.Record,\n  timestamp: string,\n): Promise<IndexedVerification | null> => {\n  const inserted = await db\n    .insertInto('verification')\n    .values({\n      uri: uri.toString(),\n      cid: cid.toString(),\n      rkey: uri.rkey,\n      creator: uri.host,\n      subject: obj.subject,\n      handle: obj.handle,\n      displayName: obj.displayName,\n      createdAt: normalizeDatetimeAlways(obj.createdAt),\n      indexedAt: timestamp,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .returningAll()\n    .executeTakeFirst()\n  return inserted || null\n}\n\nconst findDuplicate = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n  obj: Verification.Record,\n): Promise<AtUri | null> => {\n  const found = await db\n    .selectFrom('verification')\n    .where('subject', '=', obj.subject)\n    .where('creator', '=', uri.host)\n    .selectAll()\n    .executeTakeFirst()\n  return found ? new AtUri(found.uri) : null\n}\n\nconst notifsForInsert = (obj: IndexedVerification) => {\n  return [\n    {\n      did: obj.subject,\n      author: obj.creator,\n      recordUri: obj.uri,\n      recordCid: obj.cid,\n      reason: 'verified' as const,\n      reasonSubject: null,\n      sortAt: obj.sortedAt,\n    },\n  ]\n}\n\nconst deleteFn = async (\n  db: DatabaseSchema,\n  uri: AtUri,\n): Promise<IndexedVerification | null> => {\n  const deleted = await db\n    .deleteFrom('verification')\n    .where('uri', '=', uri.toString())\n    .returningAll()\n    .executeTakeFirst()\n  return deleted || null\n}\n\nconst notifsForDelete = (\n  deleted: IndexedVerification,\n  _replacedBy: IndexedVerification | null,\n) => {\n  return {\n    notifs: [\n      {\n        did: deleted.subject,\n        author: deleted.creator,\n        recordUri: deleted.uri,\n        recordCid: deleted.cid,\n        reason: 'unverified' as const,\n        reasonSubject: null,\n        sortAt: new Date().toISOString(),\n      },\n    ],\n    toDelete: [],\n  }\n}\n\nexport type PluginType = RecordProcessor<\n  Verification.Record,\n  IndexedVerification\n>\n\nexport const makePlugin = (\n  db: Database,\n  background: BackgroundQueue,\n): PluginType => {\n  return new RecordProcessor(db, background, {\n    lexId,\n    insertFn,\n    findDuplicate,\n    deleteFn,\n    notifsForInsert,\n    notifsForDelete,\n  })\n}\n\nexport default makePlugin\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/indexing/processor.ts",
    "content": "import { Insertable } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { chunkArray } from '@atproto/common'\nimport { jsonStringToLex, stringifyLex } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport { lexicons } from '../../../lexicon/lexicons'\nimport { BackgroundQueue } from '../background'\nimport { Database } from '../db'\nimport { DatabaseSchema } from '../db/database-schema'\nimport { Notification } from '../db/tables/notification'\n\n// @NOTE re: insertions and deletions. Due to how record updates are handled,\n// (insertFn) should have the same effect as (insertFn -> deleteFn -> insertFn).\ntype RecordProcessorParams<T, S> = {\n  lexId: string\n  insertFn: (\n    db: DatabaseSchema,\n    uri: AtUri,\n    cid: CID,\n    obj: T,\n    timestamp: string,\n  ) => Promise<S | null>\n  findDuplicate: (\n    db: DatabaseSchema,\n    uri: AtUri,\n    obj: T,\n  ) => Promise<AtUri | null>\n  deleteFn: (db: DatabaseSchema, uri: AtUri) => Promise<S | null>\n  notifsForInsert: (obj: S) => Notif[]\n  notifsForDelete: (\n    prev: S,\n    replacedBy: S | null,\n  ) => { notifs: Notif[]; toDelete: string[] }\n  updateAggregates?: (db: DatabaseSchema, obj: S) => Promise<void>\n}\n\ntype Notif = Insertable<Notification>\n\nexport class RecordProcessor<T, S> {\n  collection: string\n  db: DatabaseSchema\n  constructor(\n    private appDb: Database,\n    private background: BackgroundQueue,\n    private params: RecordProcessorParams<T, S>,\n  ) {\n    this.db = appDb.db\n    this.collection = this.params.lexId\n  }\n\n  matchesSchema(obj: unknown): obj is T {\n    try {\n      this.assertValidRecord(obj)\n      return true\n    } catch {\n      return false\n    }\n  }\n\n  assertValidRecord(obj: unknown): asserts obj is T {\n    lexicons.assertValidRecord(this.params.lexId, obj)\n  }\n\n  async insertRecord(\n    uri: AtUri,\n    cid: CID,\n    obj: unknown,\n    timestamp: string,\n    opts?: { disableNotifs?: boolean },\n  ) {\n    this.assertValidRecord(obj)\n    await this.db\n      .insertInto('record')\n      .values({\n        uri: uri.toString(),\n        cid: cid.toString(),\n        did: uri.host,\n        json: stringifyLex(obj),\n        indexedAt: timestamp,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n    const inserted = await this.params.insertFn(\n      this.db,\n      uri,\n      cid,\n      obj,\n      timestamp,\n    )\n    if (inserted) {\n      this.aggregateOnCommit(inserted)\n      if (!opts?.disableNotifs) {\n        await this.handleNotifs({ inserted })\n      }\n      return\n    }\n    // if duplicate, insert into duplicates table with no events\n    const found = await this.params.findDuplicate(this.db, uri, obj)\n    if (found && found.toString() !== uri.toString()) {\n      await this.db\n        .insertInto('duplicate_record')\n        .values({\n          uri: uri.toString(),\n          cid: cid.toString(),\n          duplicateOf: found.toString(),\n          indexedAt: timestamp,\n        })\n        .onConflict((oc) => oc.doNothing())\n        .execute()\n    }\n  }\n\n  // Currently using a very simple strategy for updates: purge the existing index\n  // for the uri then replace it. The main upside is that this allows the indexer\n  // for each collection to avoid bespoke logic for in-place updates, which isn't\n  // straightforward in the general case. We still get nice control over notifications.\n  async updateRecord(\n    uri: AtUri,\n    cid: CID,\n    obj: unknown,\n    timestamp: string,\n    opts?: { disableNotifs?: boolean },\n  ) {\n    this.assertValidRecord(obj)\n    await this.db\n      .updateTable('record')\n      .where('uri', '=', uri.toString())\n      .set({\n        cid: cid.toString(),\n        json: stringifyLex(obj),\n        indexedAt: timestamp,\n      })\n      .execute()\n    // If the updated record was a dupe, update dupe info for it\n    const dupe = await this.params.findDuplicate(this.db, uri, obj)\n    if (dupe) {\n      await this.db\n        .updateTable('duplicate_record')\n        .where('uri', '=', uri.toString())\n        .set({\n          cid: cid.toString(),\n          duplicateOf: dupe.toString(),\n          indexedAt: timestamp,\n        })\n        .execute()\n    } else {\n      await this.db\n        .deleteFrom('duplicate_record')\n        .where('uri', '=', uri.toString())\n        .execute()\n    }\n\n    const deleted = await this.params.deleteFn(this.db, uri)\n    if (!deleted) {\n      // If a record was updated but hadn't been indexed yet, treat it like a plain insert.\n      return this.insertRecord(uri, cid, obj, timestamp)\n    }\n    this.aggregateOnCommit(deleted)\n    const inserted = await this.params.insertFn(\n      this.db,\n      uri,\n      cid,\n      obj,\n      timestamp,\n    )\n    if (!inserted) {\n      throw new Error(\n        'Record update failed: removed from index but could not be replaced',\n      )\n    }\n    this.aggregateOnCommit(inserted)\n    if (!opts?.disableNotifs) {\n      await this.handleNotifs({ inserted, deleted })\n    }\n  }\n\n  async deleteRecord(uri: AtUri, cascading = false) {\n    await this.db\n      .deleteFrom('record')\n      .where('uri', '=', uri.toString())\n      .execute()\n    await this.db\n      .deleteFrom('duplicate_record')\n      .where('uri', '=', uri.toString())\n      .execute()\n    const deleted = await this.params.deleteFn(this.db, uri)\n    if (!deleted) return\n    this.aggregateOnCommit(deleted)\n    if (cascading) {\n      await this.db\n        .deleteFrom('duplicate_record')\n        .where('duplicateOf', '=', uri.toString())\n        .execute()\n      return this.handleNotifs({ deleted })\n    } else {\n      const found = await this.db\n        .selectFrom('duplicate_record')\n        .innerJoin('record', 'record.uri', 'duplicate_record.uri')\n        .where('duplicateOf', '=', uri.toString())\n        .orderBy('duplicate_record.indexedAt', 'asc')\n        .limit(1)\n        .selectAll()\n        .executeTakeFirst()\n\n      if (!found) {\n        return this.handleNotifs({ deleted })\n      }\n      const record = jsonStringToLex(found.json)\n      if (!this.matchesSchema(record)) {\n        return this.handleNotifs({ deleted })\n      }\n      const inserted = await this.params.insertFn(\n        this.db,\n        new AtUri(found.uri),\n        CID.parse(found.cid),\n        record,\n        found.indexedAt,\n      )\n      if (inserted) {\n        this.aggregateOnCommit(inserted)\n      }\n      await this.handleNotifs({ deleted, inserted: inserted ?? undefined })\n    }\n  }\n\n  async handleNotifs(op: { deleted?: S; inserted?: S }) {\n    let notifs: Notif[] = []\n    const runOnCommit: ((db: Database) => Promise<void>)[] = []\n    if (op.deleted) {\n      const forDelete = this.params.notifsForDelete(\n        op.deleted,\n        op.inserted ?? null,\n      )\n      if (forDelete.toDelete.length > 0) {\n        // Notifs can be deleted in background: they are expensive to delete and\n        // listNotifications already excludes notifs with missing records.\n        runOnCommit.push(async (db) => {\n          await db.db\n            .deleteFrom('notification')\n            .where('recordUri', 'in', forDelete.toDelete)\n            .execute()\n        })\n      }\n      notifs = forDelete.notifs\n    } else if (op.inserted) {\n      notifs = this.params.notifsForInsert(op.inserted)\n    }\n    for (const chunk of chunkArray(notifs, 500)) {\n      runOnCommit.push(async (db) => {\n        const filtered = await this.filterNotifsForThreadMutes(chunk)\n        await db.db.insertInto('notification').values(filtered).execute()\n      })\n    }\n    // Need to ensure notif deletion always happens before creation, otherwise delete may clobber in a race.\n    for (const fn of runOnCommit) {\n      await fn(this.appDb) // these could be backgrounded\n    }\n  }\n\n  async filterNotifsForThreadMutes(notifs: Notif[]): Promise<Notif[]> {\n    const isBlocked = await Promise.all(\n      notifs.map((n) => this.isNotifBlockedByThreadMute(n)),\n    )\n    return notifs.filter((_, i) => !isBlocked[i])\n  }\n\n  async isNotifBlockedByThreadMute(notif: Notif): Promise<boolean> {\n    const subject = notif.reasonSubject\n    if (!subject) return false\n    if (subject.startsWith('did:')) return false\n    const post = await this.db\n      .selectFrom('post')\n      .select(['uri', 'replyRoot'])\n      .where('uri', '=', subject)\n      .executeTakeFirst()\n    if (!post) return false\n    const threadRoot = post.replyRoot ?? post.uri\n    const threadMute = await this.db\n      .selectFrom('thread_mute')\n      .selectAll()\n      .where('mutedByDid', '=', notif.did)\n      .where('rootUri', '=', threadRoot)\n      .executeTakeFirst()\n    return !!threadMute\n  }\n\n  aggregateOnCommit(indexed: S) {\n    const { updateAggregates } = this.params\n    if (!updateAggregates) return\n    this.appDb.onCommit(() => {\n      this.background.add((db) => updateAggregates(db.db, indexed))\n    })\n  }\n}\n\nexport default RecordProcessor\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/activity-subscription.ts",
    "content": "import { PlainMessage } from '@bufbuild/protobuf'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport {\n  ActivitySubscription,\n  GetActivitySubscriptionsByActorAndSubjectsResponse,\n} from '../../../proto/bsky_pb'\nimport { Namespaces } from '../../../stash'\nimport { Database } from '../db'\nimport { StashKeyKey } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActivitySubscriptionsByActorAndSubjects(req) {\n    const { actorDid, subjectDids } = req\n    if (subjectDids.length === 0) {\n      return new GetActivitySubscriptionsByActorAndSubjectsResponse({\n        subscriptions: [],\n      })\n    }\n\n    const res = await db.db\n      .selectFrom('activity_subscription')\n      .selectAll()\n      .where('creator', '=', actorDid)\n      .where('subjectDid', 'in', subjectDids)\n      .execute()\n\n    const bySubject = keyBy(res, 'subjectDid')\n    const subscriptions = subjectDids.map(\n      (did): PlainMessage<ActivitySubscription> => {\n        const subject = bySubject.get(did)\n        if (!subject) {\n          return {\n            actorDid,\n            namespace:\n              Namespaces.AppBskyNotificationDefsSubjectActivitySubscription,\n            key: '',\n            post: undefined,\n            reply: undefined,\n            subjectDid: '',\n          }\n        }\n\n        return {\n          actorDid,\n          namespace:\n            Namespaces.AppBskyNotificationDefsSubjectActivitySubscription,\n          key: subject.key,\n          post: subject.post ? {} : undefined,\n          reply: subject.reply ? {} : undefined,\n          subjectDid: subject.subjectDid,\n        }\n      },\n    )\n\n    return {\n      subscriptions,\n    }\n  },\n\n  async getActivitySubscriptionDids(req) {\n    const { actorDid, cursor, limit } = req\n\n    let builder = db.db\n      .selectFrom('activity_subscription')\n      .selectAll()\n      .where('creator', '=', actorDid)\n\n    const { ref } = db.db.dynamic\n    const key = new StashKeyKey(ref('activity_subscription.key'))\n    builder = key.paginate(builder, {\n      cursor,\n      limit,\n    })\n    const res = await builder.execute()\n    const dids = res.map(({ subjectDid }) => subjectDid)\n    return {\n      dids,\n      cursor: key.packFromResult(res),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/blocks.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getBidirectionalBlock(req) {\n    const { actorDid, targetDid } = req\n    const res = await db.db\n      .selectFrom('actor_block')\n      .where((qb) =>\n        qb\n          .where('actor_block.creator', '=', actorDid)\n          .where('actor_block.subjectDid', '=', targetDid),\n      )\n      .orWhere((qb) =>\n        qb\n          .where('actor_block.creator', '=', targetDid)\n          .where('actor_block.subjectDid', '=', actorDid),\n      )\n      .limit(1)\n      .selectAll()\n      .executeTakeFirst()\n\n    return {\n      blockUri: res?.uri,\n    }\n  },\n\n  async getBlocks(req) {\n    const { actorDid, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('actor_block')\n      .where('actor_block.creator', '=', actorDid)\n      .selectAll()\n\n    const keyset = new TimeCidKeyset(\n      ref('actor_block.sortAt'),\n      ref('actor_block.cid'),\n    )\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const blocks = await builder.execute()\n    return {\n      blockUris: blocks.map((b) => b.uri),\n      cursor: keyset.packFromResult(blocks),\n    }\n  },\n\n  async getBidirectionalBlockViaList(req) {\n    const { actorDid, targetDid } = req\n    const res = await db.db\n      .selectFrom('list_block')\n      .innerJoin('list_item', 'list_item.listUri', 'list_block.subjectUri')\n      .where((qb) =>\n        qb\n          .where('list_block.creator', '=', actorDid)\n          .where('list_item.subjectDid', '=', targetDid),\n      )\n      .orWhere((qb) =>\n        qb\n          .where('list_block.creator', '=', targetDid)\n          .where('list_item.subjectDid', '=', actorDid),\n      )\n      .limit(1)\n      .selectAll('list_block')\n      .executeTakeFirst()\n\n    return {\n      listUri: res?.subjectUri,\n    }\n  },\n\n  async getBlocklistSubscription(req) {\n    const { actorDid, listUri } = req\n    const res = await db.db\n      .selectFrom('list_block')\n      .where('creator', '=', actorDid)\n      .where('subjectUri', '=', listUri)\n      .selectAll()\n      .limit(1)\n      .executeTakeFirst()\n    return {\n      listblockUri: res?.uri,\n    }\n  },\n\n  async getBlocklistSubscriptions(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('list')\n      .whereExists(\n        db.db\n          .selectFrom('list_block')\n          .where('list_block.creator', '=', actorDid)\n          .whereRef('list_block.subjectUri', '=', ref('list.uri'))\n          .selectAll(),\n      )\n      .selectAll('list')\n\n    const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n    const lists = await builder.execute()\n\n    return {\n      listUris: lists.map((l) => l.uri),\n      cursor: keyset.packFromResult(lists),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/bookmarks.ts",
    "content": "import { PlainMessage, Timestamp } from '@bufbuild/protobuf'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport {\n  Bookmark,\n  GetBookmarksByActorAndSubjectsResponse,\n} from '../../../proto/bsky_pb'\nimport { Namespaces } from '../../../stash'\nimport { Database } from '../db'\nimport { StashKeyKey } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorBookmarks(req) {\n    const { actorDid, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('bookmark')\n      .where('bookmark.creator', '=', actorDid)\n      .selectAll()\n\n    const key = new StashKeyKey(ref('bookmark.key'))\n    builder = key.paginate(builder, {\n      cursor,\n      limit,\n    })\n\n    const res = await builder.execute()\n    return {\n      bookmarks: res.map((b) => ({\n        key: b.key,\n        subject: b.subjectUri,\n      })),\n      cursor: key.packFromResult(res),\n    }\n  },\n\n  async getBookmarksByActorAndSubjects(req) {\n    const { actorDid, uris } = req\n\n    if (uris.length === 0) {\n      return new GetBookmarksByActorAndSubjectsResponse({\n        bookmarks: [],\n      })\n    }\n\n    const res = await db.db\n      .selectFrom('bookmark')\n      .where('bookmark.creator', '=', actorDid)\n      .where('bookmark.subjectUri', 'in', uris)\n      .selectAll()\n      .execute()\n\n    const byUri = keyBy(res, 'subjectUri')\n    const bookmarks = uris.map((did): PlainMessage<Bookmark> => {\n      const bookmark = byUri.get(did)\n      if (!bookmark) {\n        return {\n          ref: undefined,\n          subjectUri: '',\n          subjectCid: '',\n          indexedAt: undefined,\n        }\n      }\n\n      return {\n        ref: {\n          actorDid,\n          namespace: Namespaces.AppBskyBookmarkDefsBookmark,\n          key: bookmark.key,\n        },\n        subjectUri: bookmark.subjectUri,\n        subjectCid: bookmark.subjectCid,\n        indexedAt: Timestamp.fromDate(new Date(bookmark.indexedAt)),\n      }\n    })\n\n    return {\n      bookmarks,\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/drafts.ts",
    "content": "import { PlainMessage, Timestamp } from '@bufbuild/protobuf'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { DraftInfo } from '../../../proto/bsky_pb'\nimport { Database } from '../db'\nimport { IsoUpdatedAtKey } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorDrafts(req) {\n    const { actorDid, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('draft')\n      .where('draft.creator', '=', actorDid)\n      .selectAll()\n\n    const key = new IsoUpdatedAtKey(ref('draft.updatedAt'))\n    builder = key.paginate(builder, {\n      cursor,\n      limit,\n    })\n\n    const res = await builder.execute()\n    return {\n      drafts: res.map(\n        (d): PlainMessage<DraftInfo> => ({\n          key: d.key,\n          payload: Buffer.from(d.payload),\n          createdAt: Timestamp.fromDate(new Date(d.createdAt)),\n          updatedAt: Timestamp.fromDate(new Date(d.updatedAt)),\n        }),\n      ),\n      cursor: key.packFromResult(res),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/feed-gens.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorFeeds(req) {\n    const { actorDid, limit, cursor } = req\n\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('feed_generator')\n      .selectAll()\n      .where('feed_generator.creator', '=', actorDid)\n\n    const keyset = new TimeCidKeyset(\n      ref('feed_generator.createdAt'),\n      ref('feed_generator.cid'),\n    )\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n    const feeds = await builder.execute()\n\n    return {\n      uris: feeds.map((f) => f.uri),\n      cursor: keyset.packFromResult(feeds),\n    }\n  },\n\n  async getSuggestedFeeds(req) {\n    const feeds = await db.db\n      .selectFrom('suggested_feed')\n      .orderBy('suggested_feed.order', 'asc')\n      .if(!!req.cursor, (q) => q.where('order', '>', parseInt(req.cursor, 10)))\n      .limit(req.limit || 50)\n      .selectAll()\n      .execute()\n    return {\n      uris: feeds.map((f) => f.uri),\n      cursor: feeds.at(-1)?.order.toString(),\n    }\n  },\n\n  async searchFeedGenerators(req) {\n    const { ref } = db.db.dynamic\n    const limit = req.limit\n    const query = req.query.trim()\n    let builder = db.db\n      .selectFrom('feed_generator')\n      .if(!!query, (q) => q.where('displayName', 'ilike', `%${query}%`))\n      .selectAll()\n    const keyset = new TimeCidKeyset(\n      ref('feed_generator.createdAt'),\n      ref('feed_generator.cid'),\n    )\n    builder = paginate(builder, { limit, keyset })\n    const feeds = await builder.execute()\n    return {\n      uris: feeds.map((f) => f.uri),\n      cursor: keyset.packFromResult(feeds),\n    }\n  },\n\n  async getFeedGeneratorStatus() {\n    throw new Error('unimplemented')\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/feeds.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { FeedType } from '../../../proto/bsky_pb'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getAuthorFeed(req) {\n    const { actorDid, limit, cursor, feedType } = req\n    const { ref } = db.db.dynamic\n\n    // defaults to posts, reposts, and replies\n    let builder = db.db\n      .selectFrom('feed_item')\n      .innerJoin('post', 'post.uri', 'feed_item.postUri')\n      .selectAll('feed_item')\n      .where('originatorDid', '=', actorDid)\n\n    if (feedType === FeedType.POSTS_WITH_MEDIA) {\n      builder = builder\n        // only your own posts\n        .where('type', '=', 'post')\n        // only posts with media\n        .whereExists((qb) =>\n          qb\n            .selectFrom('post_embed_image')\n            .select('post_embed_image.postUri')\n            .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'),\n        )\n    } else if (feedType === FeedType.POSTS_WITH_VIDEO) {\n      builder = builder\n        // only your own posts\n        .where('type', '=', 'post')\n        // only posts with video\n        .whereExists((qb) =>\n          qb\n            .selectFrom('post_embed_video')\n            .select('post_embed_video.postUri')\n            .whereRef('post_embed_video.postUri', '=', 'feed_item.postUri'),\n        )\n    } else if (feedType === FeedType.POSTS_NO_REPLIES) {\n      builder = builder.where((qb) =>\n        qb.where('post.replyParent', 'is', null).orWhere('type', '=', 'repost'),\n      )\n    } else if (feedType === FeedType.POSTS_AND_AUTHOR_THREADS) {\n      builder = builder.where((qb) =>\n        qb\n          .where('type', '=', 'repost')\n          .orWhere('post.replyParent', 'is', null)\n          .orWhere('post.replyRoot', 'like', `at://${actorDid}/%`),\n      )\n    }\n\n    const keyset = new TimeCidKeyset(\n      ref('feed_item.sortAt'),\n      ref('feed_item.cid'),\n    )\n\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const feedItems = await builder.execute()\n\n    return {\n      items: feedItems.map(feedItemFromRow),\n      cursor: keyset.packFromResult(feedItems),\n    }\n  },\n\n  async getTimeline(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n\n    const keyset = new TimeCidKeyset(\n      ref('feed_item.sortAt'),\n      ref('feed_item.cid'),\n    )\n\n    let followQb = db.db\n      .selectFrom('feed_item')\n      .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid')\n      .where('follow.creator', '=', actorDid)\n      .selectAll('feed_item')\n\n    followQb = paginate(followQb, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    let selfQb = db.db\n      .selectFrom('feed_item')\n      .where('feed_item.originatorDid', '=', actorDid)\n      .selectAll('feed_item')\n\n    selfQb = paginate(selfQb, {\n      limit: Math.min(limit, 10),\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const [followRes, selfRes] = await Promise.all([\n      followQb.execute(),\n      selfQb.execute(),\n    ])\n\n    const feedItems = [...followRes, ...selfRes]\n      .sort((a, b) => {\n        if (a.sortAt > b.sortAt) return -1\n        if (a.sortAt < b.sortAt) return 1\n        return a.cid > b.cid ? -1 : 1\n      })\n      .slice(0, limit)\n\n    return {\n      items: feedItems.map(feedItemFromRow),\n      cursor: keyset.packFromResult(feedItems),\n    }\n  },\n\n  async getListFeed(req) {\n    const { listUri, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('post')\n      .selectAll('post')\n      .innerJoin('list_item', 'list_item.subjectDid', 'post.creator')\n      .where('list_item.listUri', '=', listUri)\n\n    const keyset = new TimeCidKeyset(ref('post.sortAt'), ref('post.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n    const feedItems = await builder.execute()\n\n    return {\n      items: feedItems.map((item) => ({ uri: item.uri, cid: item.cid })),\n      cursor: keyset.packFromResult(feedItems),\n    }\n  },\n})\n\n// @NOTE does not support additional fields in the protos specific to author feeds\n// and timelines. at the time of writing, hydration/view implementations do not rely on them.\nconst feedItemFromRow = (row: { postUri: string; uri: string }) => {\n  return {\n    uri: row.postUri,\n    repost: row.uri === row.postUri ? undefined : row.uri,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/follows.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { FollowsFollowing } from '../../../proto/bsky_pb'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorFollowsActors(req) {\n    const { actorDid, targetDids } = req\n    if (targetDids.length < 1) {\n      return { uris: [] }\n    }\n    const res = await db.db\n      .selectFrom('follow')\n      .where('follow.creator', '=', actorDid)\n      .where('follow.subjectDid', 'in', targetDids)\n      .selectAll()\n      .execute()\n    const bySubject = keyBy(res, 'subjectDid')\n    const uris = targetDids.map((did) => bySubject.get(did)?.uri ?? '')\n    return {\n      uris,\n    }\n  },\n  async getFollowers(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n    let followersReq = db.db\n      .selectFrom('follow')\n      .where('follow.subjectDid', '=', actorDid)\n      .innerJoin('actor as creator', 'creator.did', 'follow.creator')\n      .selectAll('creator')\n      .select([\n        'follow.uri as uri',\n        'follow.cid as cid',\n        'follow.creator as creatorDid',\n        'follow.subjectDid as subjectDid',\n        'follow.sortAt as sortAt',\n      ])\n\n    const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid'))\n    followersReq = paginate(followersReq, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const followers = await followersReq.execute()\n    return {\n      followers: followers.map((f) => ({\n        uri: f.uri,\n        actorDid: f.creatorDid,\n        subjectDid: f.subjectDid,\n      })),\n      cursor: keyset.packFromResult(followers),\n    }\n  },\n  async getFollows(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n\n    let followsReq = db.db\n      .selectFrom('follow')\n      .where('follow.creator', '=', actorDid)\n      .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid')\n      .selectAll('subject')\n      .select([\n        'follow.uri as uri',\n        'follow.cid as cid',\n        'follow.creator as creatorDid',\n        'follow.subjectDid as subjectDid',\n        'follow.sortAt as sortAt',\n      ])\n\n    const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid'))\n    followsReq = paginate(followsReq, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const follows = await followsReq.execute()\n\n    return {\n      follows: follows.map((f) => ({\n        uri: f.uri,\n        actorDid: f.creatorDid,\n        subjectDid: f.subjectDid,\n      })),\n      cursor: keyset.packFromResult(follows),\n    }\n  },\n\n  /**\n   * Return known followers of a given actor.\n   *\n   * Example:\n   *   - Alice follows Bob\n   *   - Bob follows Dan\n   *\n   *   If Alice (the viewer) looks at Dan's profile (the subject), she should see that Bob follows Dan\n   */\n  async getFollowsFollowing(req) {\n    const { actorDid: viewerDid, targetDids: subjectDids } = req\n\n    /*\n     * 1. Get all the people the Alice is following\n     * 2. Get all the people the Dan is followed by\n     * 3. Find the intersection\n     */\n\n    const results: FollowsFollowing[] = []\n\n    for (const subjectDid of subjectDids) {\n      const followsReq = db.db\n        .selectFrom('follow')\n        .where('follow.creator', '=', viewerDid)\n        .where(\n          'follow.subjectDid',\n          'in',\n          db.db\n            .selectFrom('follow')\n            .where('follow.subjectDid', '=', subjectDid)\n            .select(['creator']),\n        )\n        .select(['subjectDid'])\n      const rows = await followsReq.execute()\n      results.push(\n        new FollowsFollowing({\n          targetDid: subjectDid,\n          dids: rows.map((r) => r.subjectDid),\n        }),\n      )\n    }\n\n    return { results }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/identity.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { DidDocument, IdResolver, getDid, getHandle } from '@atproto/identity'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\n\nexport default (\n  _db: Database,\n  idResolver: IdResolver,\n): Partial<ServiceImpl<typeof Service>> => ({\n  async getIdentityByDid(req) {\n    const doc = await idResolver.did.resolve(req.did)\n    if (!doc) {\n      throw new ConnectError('identity not found', Code.NotFound)\n    }\n    return getResultFromDoc(doc)\n  },\n\n  async getIdentityByHandle(req) {\n    const did = await idResolver.handle.resolve(req.handle)\n    if (!did) {\n      throw new ConnectError('identity not found', Code.NotFound)\n    }\n    const doc = await idResolver.did.resolve(did)\n    if (!doc || did !== getDid(doc)) {\n      throw new ConnectError('identity not found', Code.NotFound)\n    }\n    return getResultFromDoc(doc)\n  },\n})\n\nconst getResultFromDoc = (doc: DidDocument) => {\n  const keys: Record<string, { Type: string; PublicKeyMultibase: string }> = {}\n  doc.verificationMethod?.forEach((method) => {\n    const id = method.id.split('#').at(1)\n    if (!id) return\n    keys[id] = {\n      Type: method.type,\n      PublicKeyMultibase: method.publicKeyMultibase || '',\n    }\n  })\n  const services: Record<string, { Type: string; URL: string }> = {}\n  doc.service?.forEach((service) => {\n    const id = service.id.split('#').at(1)\n    if (!id) return\n    if (typeof service.serviceEndpoint !== 'string') return\n    services[id] = {\n      Type: service.type,\n      URL: service.serviceEndpoint,\n    }\n  })\n  return {\n    did: getDid(doc),\n    handle: getHandle(doc),\n    keys: Buffer.from(JSON.stringify(keys)),\n    services: Buffer.from(JSON.stringify(services)),\n    updated: Timestamp.fromDate(new Date()),\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/index.ts",
    "content": "import { ConnectRouter } from '@connectrpc/connect'\nimport { IdResolver } from '@atproto/identity'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport activitySubscription from './activity-subscription'\nimport blocks from './blocks'\nimport bookmarks from './bookmarks'\nimport drafts from './drafts'\nimport feedGens from './feed-gens'\nimport feeds from './feeds'\nimport follows from './follows'\nimport identity from './identity'\nimport interactions from './interactions'\nimport labels from './labels'\nimport likes from './likes'\nimport lists from './lists'\nimport moderation from './moderation'\nimport mutes from './mutes'\nimport notifs from './notifs'\nimport profile from './profile'\nimport quotes from './quotes'\nimport records from './records'\nimport relationships from './relationships'\nimport reposts from './reposts'\nimport search from './search'\nimport sitemap from './sitemap'\nimport starterPacks from './starter-packs'\nimport suggestions from './suggestions'\nimport sync from './sync'\nimport threads from './threads'\n\nexport default (db: Database, idResolver: IdResolver) =>\n  (router: ConnectRouter) =>\n    router.service(Service, {\n      ...activitySubscription(db),\n      ...blocks(db),\n      ...bookmarks(db),\n      ...drafts(db),\n      ...feedGens(db),\n      ...feeds(db),\n      ...follows(db),\n      ...identity(db, idResolver),\n      ...interactions(db),\n      ...labels(db),\n      ...likes(db),\n      ...lists(db),\n      ...moderation(db),\n      ...mutes(db),\n      ...notifs(db),\n      ...profile(db),\n      ...quotes(db),\n      ...records(db),\n      ...relationships(db),\n      ...reposts(db),\n      ...search(db),\n      ...sitemap(),\n      ...suggestions(db),\n      ...sync(db),\n      ...threads(db),\n      ...starterPacks(db),\n\n      async ping() {\n        return {}\n      },\n    })\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/interactions.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { DAY, keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { countAll } from '../db/util'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getInteractionCounts(req) {\n    const uris = req.refs.map((ref) => ref.uri)\n    if (uris.length === 0) {\n      return { likes: [], replies: [], reposts: [], quotes: [] }\n    }\n    const res = await db.db\n      .selectFrom('post_agg')\n      .where('uri', 'in', uris)\n      .selectAll()\n      .execute()\n    const byUri = keyBy(res, 'uri')\n    return {\n      likes: uris.map((uri) => byUri.get(uri)?.likeCount ?? 0),\n      replies: uris.map((uri) => byUri.get(uri)?.replyCount ?? 0),\n      reposts: uris.map((uri) => byUri.get(uri)?.repostCount ?? 0),\n      quotes: uris.map((uri) => byUri.get(uri)?.quoteCount ?? 0),\n      bookmarks: uris.map((uri) => byUri.get(uri)?.bookmarkCount ?? 0),\n    }\n  },\n  async getCountsForUsers(req) {\n    if (req.dids.length === 0) {\n      return {}\n    }\n    const { ref } = db.db.dynamic\n    const res = await db.db\n      .selectFrom('profile_agg')\n      .where('did', 'in', req.dids)\n      .selectAll('profile_agg')\n      .select([\n        db.db\n          .selectFrom('feed_generator')\n          .whereRef('creator', '=', ref('profile_agg.did'))\n          .select(countAll.as('val'))\n          .as('feedGensCount'),\n        db.db\n          .selectFrom('list')\n          .whereRef('creator', '=', ref('profile_agg.did'))\n          .select(countAll.as('val'))\n          .as('listsCount'),\n        db.db\n          .selectFrom('starter_pack')\n          .whereRef('creator', '=', ref('profile_agg.did'))\n          .select(countAll.as('val'))\n          .as('starterPacksCount'),\n        db.db\n          .selectFrom('draft')\n          .whereRef('creator', '=', ref('profile_agg.did'))\n          .select(countAll.as('val'))\n          .as('draftsCount'),\n      ])\n      .execute()\n    const byDid = keyBy(res, 'did')\n    return {\n      followers: req.dids.map((uri) => byDid.get(uri)?.followersCount ?? 0),\n      following: req.dids.map((uri) => byDid.get(uri)?.followsCount ?? 0),\n      posts: req.dids.map((uri) => byDid.get(uri)?.postsCount ?? 0),\n      lists: req.dids.map((uri) => byDid.get(uri)?.listsCount ?? 0),\n      feeds: req.dids.map((uri) => byDid.get(uri)?.feedGensCount ?? 0),\n      starterPacks: req.dids.map(\n        (uri) => byDid.get(uri)?.starterPacksCount ?? 0,\n      ),\n      drafts: req.dids.map((uri) => byDid.get(uri)?.draftsCount ?? 0),\n    }\n  },\n  async getStarterPackCounts(req) {\n    const weekAgo = new Date(Date.now() - 7 * DAY)\n    const uris = req.refs.map((ref) => ref.uri)\n    if (uris.length === 0) {\n      return { joinedAllTime: [], joinedWeek: [] }\n    }\n    const countsAllTime = await db.db\n      .selectFrom('profile')\n      .where('joinedViaStarterPackUri', 'in', uris)\n      .select(['joinedViaStarterPackUri as uri', countAll.as('count')])\n      .groupBy('joinedViaStarterPackUri')\n      .execute()\n    const countsWeek = await db.db\n      .selectFrom('profile')\n      .where('joinedViaStarterPackUri', 'in', uris)\n      .where('createdAt', '>', weekAgo.toISOString())\n      .select(['joinedViaStarterPackUri as uri', countAll.as('count')])\n      .groupBy('joinedViaStarterPackUri')\n      .execute()\n    const countsWeekByUri = countsWeek.reduce((cur, item) => {\n      if (!item.uri) return cur\n      return cur.set(item.uri, item.count)\n    }, new Map<string, number>())\n    const countsAllTimeByUri = countsAllTime.reduce((cur, item) => {\n      if (!item.uri) return cur\n      return cur.set(item.uri, item.count)\n    }, new Map<string, number>())\n    return {\n      joinedWeek: uris.map((uri) => countsWeekByUri.get(uri) ?? 0),\n      joinedAllTime: uris.map((uri) => countsAllTimeByUri.get(uri) ?? 0),\n    }\n  },\n  async getListCounts(req) {\n    const uris = req.refs.map((ref) => ref.uri)\n    if (uris.length === 0) {\n      return { listItems: [] }\n    }\n    const countsListItems = await db.db\n      .selectFrom('list_item')\n      .where('listUri', 'in', uris)\n      .select(['listUri as uri', countAll.as('count')])\n      .groupBy('listUri')\n      .execute()\n    const countsByUri = keyBy(countsListItems, 'uri')\n    return {\n      listItems: uris.map((uri) => countsByUri.get(uri)?.count ?? 0),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/labels.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Selectable, sql } from 'kysely'\nimport * as ui8 from 'uint8arrays'\nimport { noUndefinedVals } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { Label } from '../db/tables/label'\n\ntype LabelRow = Selectable<Label>\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getLabels(req) {\n    const { subjects, issuers } = req\n    if (subjects.length === 0 || issuers.length === 0) {\n      return { labels: [] }\n    }\n\n    const res: LabelRow[] = await db.db\n      .selectFrom('label')\n      .where('uri', 'in', subjects)\n      .where('src', 'in', issuers)\n      .where((qb) =>\n        qb.where('exp', 'is', null).orWhere(sql`exp::timestamp > now()`),\n      )\n      .selectAll()\n      .execute()\n\n    const labelsBySubject = new Map<string, LabelRow[]>()\n    res.forEach((l) => {\n      const labels = labelsBySubject.get(l.uri) ?? []\n      labels.push(l)\n      labelsBySubject.set(l.uri, labels)\n    })\n\n    // intentionally duplicate label results, appview frontend should be defensive to this\n    const labels = subjects.flatMap((sub) => {\n      const labelsForSub = labelsBySubject.get(sub) ?? []\n      return labelsForSub.map((l) => {\n        const formatted = noUndefinedVals({\n          ...l,\n          exp: l.exp === null ? undefined : l.exp,\n          cid: l.cid === '' ? undefined : l.cid,\n          neg: l.neg === true ? true : undefined,\n        })\n        return ui8.fromString(JSON.stringify(formatted), 'utf8')\n      })\n    })\n\n    return { labels }\n  },\n\n  async getAllLabelers() {\n    throw new Error('not implemented')\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/likes.ts",
    "content": "import assert from 'node:assert'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getLikesBySubjectSorted(req) {\n    const { subject, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    if (!subject?.uri) {\n      return { uris: [] }\n    }\n\n    // @NOTE ignoring subject.cid\n    let builder = db.db\n      .selectFrom('like')\n      .where('like.subject', '=', subject?.uri)\n      .selectAll('like')\n\n    const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const likes = await builder.execute()\n\n    return {\n      uris: likes.map((l) => l.uri),\n      cursor: keyset.packFromResult(likes),\n    }\n  },\n\n  // @NOTE deprecated in favor of getLikesBySubjectSorted\n  async getLikesBySubject(req, context) {\n    assert(this.getLikesBySubjectSorted)\n    return this.getLikesBySubjectSorted(req, context)\n  },\n\n  async getLikesByActorAndSubjects(req) {\n    const { actorDid, refs } = req\n    if (refs.length === 0) {\n      return { uris: [] }\n    }\n    // @NOTE ignoring ref.cid\n    const res = await db.db\n      .selectFrom('like')\n      .where('creator', '=', actorDid)\n      .where(\n        'subject',\n        'in',\n        refs.map(({ uri }) => uri),\n      )\n      .selectAll()\n      .execute()\n    const bySubject = keyBy(res, 'subject')\n    const uris = refs.map(({ uri }) => bySubject.get(uri)?.uri ?? '')\n    return { uris }\n  },\n\n  async getActorLikes(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('like')\n      .where('like.creator', '=', actorDid)\n      .selectAll()\n\n    const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid'))\n\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const likes = await builder.execute()\n\n    return {\n      likes: likes.map((l) => ({\n        uri: l.uri,\n        subject: l.subject,\n      })),\n      cursor: keyset.packFromResult(likes),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/lists.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\nimport { countAll } from '../db/util'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorLists(req) {\n    const { actorDid, cursor, limit } = req\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('list')\n      .where('creator', '=', actorDid)\n      .selectAll()\n    const keyset = new TimeCidKeyset(ref('list.sortAt'), ref('list.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n    const lists = await builder.execute()\n    return {\n      listUris: lists.map((item) => item.uri),\n      cursor: keyset.packFromResult(lists),\n    }\n  },\n\n  async getListMembers(req) {\n    const { listUri, cursor, limit } = req\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('list_item')\n      .where('listUri', '=', listUri)\n      .selectAll()\n\n    const keyset = new TimeCidKeyset(\n      ref('list_item.sortAt'),\n      ref('list_item.cid'),\n    )\n\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const listItems = await builder.execute()\n    return {\n      listitems: listItems.map((item) => ({\n        uri: item.uri,\n        did: item.subjectDid,\n      })),\n      cursor: keyset.packFromResult(listItems),\n    }\n  },\n\n  async getListMembership(req) {\n    const { actorDid, listUris } = req\n    if (listUris.length === 0) {\n      return { listitemUris: [] }\n    }\n    const res = await db.db\n      .selectFrom('list_item')\n      .where('subjectDid', '=', actorDid)\n      .where('listUri', 'in', listUris)\n      .selectAll()\n      .execute()\n    const byListUri = keyBy(res, 'listUri')\n    const listitemUris = listUris.map((uri) => byListUri.get(uri)?.uri ?? '')\n    return {\n      listitemUris,\n    }\n  },\n\n  async getListCount(req) {\n    const res = await db.db\n      .selectFrom('list_item')\n      .select(countAll.as('count'))\n      .where('list_item.listUri', '=', req.listUri)\n      .executeTakeFirst()\n    return {\n      count: res?.count,\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/moderation.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorTakedown(req) {\n    const { did } = req\n    const res = await db.db\n      .selectFrom('actor')\n      .where('did', '=', did)\n      .select('takedownRef')\n      .executeTakeFirst()\n    return {\n      takenDown: !!res?.takedownRef,\n      takedownRef: res?.takedownRef || undefined,\n    }\n  },\n\n  async getBlobTakedown(req) {\n    const { did, cid } = req\n    const res = await db.db\n      .selectFrom('blob_takedown')\n      .where('did', '=', did)\n      .where('cid', '=', cid)\n      .select('takedownRef')\n      .executeTakeFirst()\n    return {\n      takenDown: !!res,\n      takedownRef: res?.takedownRef || undefined,\n    }\n  },\n\n  async getRecordTakedown(req) {\n    const { recordUri } = req\n    const res = await db.db\n      .selectFrom('record')\n      .where('uri', '=', recordUri)\n      .select('takedownRef')\n      .executeTakeFirst()\n    return {\n      takenDown: !!res?.takedownRef,\n      takedownRef: res?.takedownRef || undefined,\n    }\n  },\n\n  async takedownActor(req) {\n    const { did, ref } = req\n    await db.db\n      .updateTable('actor')\n      .set({ takedownRef: ref || 'TAKEDOWN' })\n      .where('did', '=', did)\n      .execute()\n  },\n\n  async takedownBlob(req) {\n    const { did, cid, ref } = req\n    await db.db\n      .insertInto('blob_takedown')\n      .values({\n        did,\n        cid,\n        takedownRef: ref || 'TAKEDOWN',\n      })\n      .execute()\n  },\n\n  async takedownRecord(req) {\n    const { recordUri, ref } = req\n    await db.db\n      .updateTable('record')\n      .set({ takedownRef: ref || 'TAKEDOWN' })\n      .where('uri', '=', recordUri)\n      .execute()\n  },\n\n  async untakedownActor(req) {\n    const { did } = req\n    await db.db\n      .updateTable('actor')\n      .set({ takedownRef: null })\n      .where('did', '=', did)\n      .execute()\n  },\n\n  async untakedownBlob(req) {\n    const { did, cid } = req\n    await db.db\n      .deleteFrom('blob_takedown')\n      .where('did', '=', did)\n      .where('cid', '=', cid)\n      .executeTakeFirst()\n  },\n\n  async untakedownRecord(req) {\n    const { recordUri } = req\n    await db.db\n      .updateTable('record')\n      .set({ takedownRef: null })\n      .where('uri', '=', recordUri)\n      .execute()\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/mutes.ts",
    "content": "import assert from 'node:assert'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { AtUri } from '@atproto/syntax'\nimport { ids } from '../../../lexicon/lexicons'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { CreatedAtDidKeyset, TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorMutesActor(req) {\n    const { actorDid, targetDid } = req\n    const res = await db.db\n      .selectFrom('mute')\n      .selectAll()\n      .where('mutedByDid', '=', actorDid)\n      .where('subjectDid', '=', targetDid)\n      .executeTakeFirst()\n    return {\n      muted: !!res,\n    }\n  },\n\n  async getMutes(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('mute')\n      .innerJoin('actor', 'actor.did', 'mute.subjectDid')\n      .where('mute.mutedByDid', '=', actorDid)\n      .selectAll('actor')\n      .select('mute.createdAt as createdAt')\n\n    const keyset = new CreatedAtDidKeyset(\n      ref('mute.createdAt'),\n      ref('mute.subjectDid'),\n    )\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const mutes = await builder.execute()\n\n    return {\n      dids: mutes.map((m) => m.did),\n      cursor: keyset.packFromResult(mutes),\n    }\n  },\n\n  async getActorMutesActorViaList(req) {\n    const { actorDid, targetDid } = req\n    const res = await db.db\n      .selectFrom('list_mute')\n      .innerJoin('list_item', 'list_item.listUri', 'list_mute.listUri')\n      .where('list_mute.mutedByDid', '=', actorDid)\n      .where('list_item.subjectDid', '=', targetDid)\n      .select('list_mute.listUri')\n      .limit(1)\n      .executeTakeFirst()\n    return {\n      listUri: res?.listUri,\n    }\n  },\n\n  async getMutelistSubscription(req) {\n    const { actorDid, listUri } = req\n    const res = await db.db\n      .selectFrom('list_mute')\n      .where('mutedByDid', '=', actorDid)\n      .where('listUri', '=', listUri)\n      .selectAll()\n      .limit(1)\n      .executeTakeFirst()\n    return {\n      subscribed: !!res,\n    }\n  },\n\n  async getMutelistSubscriptions(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('list')\n      .whereExists(\n        db.db\n          .selectFrom('list_mute')\n          .where('list_mute.mutedByDid', '=', actorDid)\n          .whereRef('list_mute.listUri', '=', ref('list.uri'))\n          .selectAll(),\n      )\n      .selectAll('list')\n\n    const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n    const lists = await builder.execute()\n\n    return {\n      listUris: lists.map((l) => l.uri),\n      cursor: keyset.packFromResult(lists),\n    }\n  },\n\n  async createActorMute(req) {\n    const { actorDid, subjectDid } = req\n    assert(actorDid !== subjectDid, 'cannot mute yourself') // @TODO pass message through in http error\n    await db.db\n      .insertInto('mute')\n      .values({\n        subjectDid,\n        mutedByDid: actorDid,\n        createdAt: new Date().toISOString(),\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  },\n\n  async deleteActorMute(req) {\n    const { actorDid, subjectDid } = req\n    assert(actorDid !== subjectDid, 'cannot mute yourself')\n    await db.db\n      .deleteFrom('mute')\n      .where('subjectDid', '=', subjectDid)\n      .where('mutedByDid', '=', actorDid)\n      .execute()\n  },\n\n  async clearActorMutes(req) {\n    const { actorDid } = req\n    await db.db.deleteFrom('mute').where('mutedByDid', '=', actorDid).execute()\n  },\n\n  async createActorMutelistSubscription(req) {\n    const { actorDid, subjectUri } = req\n    assert(isListUri(subjectUri), 'must mute a list')\n    await db.db\n      .insertInto('list_mute')\n      .values({\n        listUri: subjectUri,\n        mutedByDid: actorDid,\n        createdAt: new Date().toISOString(),\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  },\n\n  async deleteActorMutelistSubscription(req) {\n    const { actorDid, subjectUri } = req\n    assert(isListUri(subjectUri), 'must mute a list')\n    await db.db\n      .deleteFrom('list_mute')\n      .where('listUri', '=', subjectUri)\n      .where('mutedByDid', '=', actorDid)\n      .execute()\n  },\n\n  async clearActorMutelistSubscriptions(req) {\n    const { actorDid } = req\n    await db.db\n      .deleteFrom('list_mute')\n      .where('mutedByDid', '=', actorDid)\n      .execute()\n  },\n\n  async getThreadMutesOnSubjects(req) {\n    const { actorDid, threadRoots } = req\n    if (threadRoots.length === 0) {\n      return { muted: [] }\n    }\n    const res = await db.db\n      .selectFrom('thread_mute')\n      .selectAll()\n      .where('mutedByDid', '=', actorDid)\n      .where('rootUri', 'in', threadRoots)\n      .execute()\n    const byRootUri = keyBy(res, 'rootUri')\n    const muted = threadRoots.map((uri) => !!byRootUri.get(uri))\n    return { muted }\n  },\n})\n\nconst isListUri = (uri: string) =>\n  new AtUri(uri).collection === ids.AppBskyGraphList\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/notifs.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { keyBy } from '@atproto/common'\nimport { jsonStringToLex } from '@atproto/lexicon'\nimport {\n  ChatPreference,\n  FilterablePreference,\n  Preference,\n  Preferences,\n} from '../../../lexicon/types/app/bsky/notification/defs'\nimport { Service } from '../../../proto/bsky_connect'\nimport {\n  ChatNotificationInclude,\n  ChatNotificationPreference,\n  FilterableNotificationPreference,\n  NotificationInclude,\n  NotificationPreference,\n  NotificationPreferences,\n} from '../../../proto/bsky_pb'\nimport { Namespaces } from '../../../stash'\nimport { Database } from '../db'\nimport { IsoSortAtKey } from '../db/pagination'\nimport { countAll, notSoftDeletedClause } from '../db/util'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getNotifications(req) {\n    const { actorDid, limit, cursor, priority } = req\n    const { ref } = db.db.dynamic\n    const priorityFollowQb = db.db\n      .selectFrom('follow')\n      .select(sql<boolean>`${true}`.as('val'))\n      .where('creator', '=', actorDid)\n      .whereRef('subjectDid', '=', ref('notif.author'))\n      .limit(1)\n\n    let builder = db.db\n      .selectFrom('notification as notif')\n      .where('notif.did', '=', actorDid)\n      .where((clause) =>\n        clause\n          .where('reasonSubject', 'is', null)\n          .orWhereExists(\n            db.db\n              .selectFrom('record as subject')\n              .selectAll()\n              .whereRef('subject.uri', '=', ref('notif.reasonSubject')),\n          ),\n      )\n      .if(priority, (qb) => qb.whereExists(priorityFollowQb))\n      .select([\n        'notif.author as authorDid',\n        'notif.recordUri as uri',\n        'notif.recordCid as cid',\n        'notif.reason as reason',\n        'notif.reasonSubject as reasonSubject',\n        'notif.sortAt as sortAt',\n      ])\n      .select(priorityFollowQb.as('priority'))\n\n    const key = new IsoSortAtKey(ref('notif.sortAt'))\n    builder = key.paginate(builder, {\n      cursor,\n      limit,\n    })\n\n    const notifsRes = await builder.execute()\n    const notifications = notifsRes.map((notif) => ({\n      recipientDid: actorDid,\n      uri: notif.uri,\n      reason: notif.reason,\n      reasonSubject: notif.reasonSubject ?? undefined,\n      timestamp: Timestamp.fromDate(new Date(notif.sortAt)),\n      priority: notif.priority ?? false,\n    }))\n    return {\n      notifications,\n      cursor: key.packFromResult(notifsRes),\n    }\n  },\n\n  async getNotificationSeen(req) {\n    const { actorDid, priority } = req\n    const res = await db.db\n      .selectFrom('actor_state')\n      .where('did', '=', actorDid)\n      .selectAll()\n      .executeTakeFirst()\n    if (!res) {\n      return {}\n    }\n    const lastSeen =\n      priority && res.lastSeenPriorityNotifs\n        ? res.lastSeenPriorityNotifs\n        : res.lastSeenNotifs\n    return {\n      timestamp: Timestamp.fromDate(new Date(lastSeen)),\n    }\n  },\n\n  async getUnreadNotificationCount(req) {\n    const { actorDid, priority } = req\n    const { ref } = db.db.dynamic\n    const lastSeenRes = await db.db\n      .selectFrom('actor_state')\n      .where('did', '=', actorDid)\n      .selectAll()\n      .executeTakeFirst()\n    const lastSeen =\n      priority && lastSeenRes?.lastSeenPriorityNotifs\n        ? lastSeenRes.lastSeenPriorityNotifs\n        : lastSeenRes?.lastSeenNotifs\n\n    const result = await db.db\n      .selectFrom('notification')\n      .select(countAll.as('count'))\n      .innerJoin('actor', 'actor.did', 'notification.did')\n      .leftJoin('actor_state', 'actor_state.did', 'actor.did')\n      .innerJoin('record', 'record.uri', 'notification.recordUri')\n      .where(notSoftDeletedClause(ref('record')))\n      .where(notSoftDeletedClause(ref('actor')))\n      // Ensure to hit notification_did_sortat_idx, handling case where lastSeenNotifs is null.\n      .where('notification.did', '=', actorDid)\n      .where('notification.sortAt', '>', lastSeen ?? '')\n      .if(priority, (qb) =>\n        qb.whereExists(\n          db.db\n            .selectFrom('follow')\n            .select(sql<boolean>`${true}`.as('val'))\n            .where('creator', '=', actorDid)\n            .whereRef('subjectDid', '=', ref('notification.author')),\n        ),\n      )\n      .executeTakeFirst()\n\n    return {\n      count: result?.count,\n    }\n  },\n\n  async updateNotificationSeen(req) {\n    const { actorDid, timestamp, priority } = req\n    if (!timestamp) {\n      return\n    }\n    const timestampIso = timestamp.toDate().toISOString()\n    let builder = db.db\n      .updateTable('actor_state')\n      .where('did', '=', actorDid)\n      .returningAll()\n    if (priority) {\n      builder = builder.set({ lastSeenPriorityNotifs: timestampIso })\n    } else {\n      builder = builder.set({ lastSeenNotifs: timestampIso })\n    }\n    const updateRes = await builder.executeTakeFirst()\n    if (updateRes) {\n      return\n    }\n    await db.db\n      .insertInto('actor_state')\n      .values({\n        did: actorDid,\n        lastSeenNotifs: timestampIso,\n        priorityNotifs: priority,\n        lastSeenPriorityNotifs: priority ? timestampIso : undefined,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .executeTakeFirst()\n  },\n\n  async getNotificationPreferences(req) {\n    const { dids } = req\n    if (dids.length === 0) {\n      return { preferences: [] }\n    }\n\n    const res = await db.db\n      .selectFrom('private_data')\n      .selectAll()\n      .where('actorDid', 'in', dids)\n      .where('namespace', '=', Namespaces.AppBskyNotificationDefsPreferences)\n      .where('key', '=', 'self')\n      .execute()\n\n    const byDid = keyBy(res, 'actorDid')\n    const preferences = dids.map((did) => {\n      const row = byDid.get(did)\n      if (!row) {\n        return {}\n      }\n      const p = jsonStringToLex(row.payload) as Preferences\n      return notificationPreferencesLexToProtobuf(p, row.payload)\n    })\n\n    return { preferences }\n  },\n})\n\nexport const notificationPreferencesLexToProtobuf = (\n  p: Preferences,\n  json: string,\n): NotificationPreferences => {\n  const lexChatPreferenceToProtobuf = (\n    p: ChatPreference,\n  ): ChatNotificationPreference =>\n    new ChatNotificationPreference({\n      include:\n        p.include === 'accepted'\n          ? ChatNotificationInclude.ACCEPTED\n          : ChatNotificationInclude.ALL,\n      push: { enabled: p.push ?? true },\n    })\n\n  const lexFilterablePreferenceToProtobuf = (\n    p: FilterablePreference,\n  ): FilterableNotificationPreference =>\n    new FilterableNotificationPreference({\n      include:\n        p.include === 'follows'\n          ? NotificationInclude.FOLLOWS\n          : NotificationInclude.ALL,\n      list: { enabled: p.list ?? true },\n      push: { enabled: p.push ?? true },\n    })\n\n  const lexPreferenceToProtobuf = (p: Preference): NotificationPreference =>\n    new NotificationPreference({\n      list: { enabled: p.list ?? true },\n      push: { enabled: p.push ?? true },\n    })\n\n  return new NotificationPreferences({\n    entry: Buffer.from(json),\n    chat: lexChatPreferenceToProtobuf(p.chat),\n    follow: lexFilterablePreferenceToProtobuf(p.follow),\n    like: lexFilterablePreferenceToProtobuf(p.like),\n    likeViaRepost: lexFilterablePreferenceToProtobuf(p.likeViaRepost),\n    mention: lexFilterablePreferenceToProtobuf(p.mention),\n    quote: lexFilterablePreferenceToProtobuf(p.quote),\n    reply: lexFilterablePreferenceToProtobuf(p.reply),\n    repost: lexFilterablePreferenceToProtobuf(p.repost),\n    repostViaRepost: lexFilterablePreferenceToProtobuf(p.repostViaRepost),\n    starterpackJoined: lexPreferenceToProtobuf(p.starterpackJoined),\n    subscribedPost: lexPreferenceToProtobuf(p.subscribedPost),\n    unverified: lexPreferenceToProtobuf(p.unverified),\n    verified: lexPreferenceToProtobuf(p.verified),\n  })\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/profile.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { Selectable, sql } from 'kysely'\nimport {\n  AppBskyNotificationDeclaration,\n  ChatBskyActorDeclaration,\n} from '@atproto/api'\nimport { keyBy } from '@atproto/common'\nimport { parseRecordBytes } from '../../../hydration/util'\nimport { Service } from '../../../proto/bsky_connect'\nimport { VerificationMeta } from '../../../proto/bsky_pb'\nimport { Database } from '../db'\nimport { Verification } from '../db/tables/verification'\nimport { getRecords } from './records'\n\ntype VerifiedBy = {\n  [handle: string]: Pick<\n    VerificationMeta,\n    'rkey' | 'handle' | 'displayName' | 'sortedAt'\n  >\n}\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActors(req) {\n    const { dids, returnAgeAssuranceForDids } = req\n    if (dids.length === 0) {\n      return { actors: [] }\n    }\n    const profileUris = dids.map(\n      (did) => `at://${did}/app.bsky.actor.profile/self`,\n    )\n    const statusUris = dids.map(\n      (did) => `at://${did}/app.bsky.actor.status/self`,\n    )\n    const chatDeclarationUris = dids.map(\n      (did) => `at://${did}/chat.bsky.actor.declaration/self`,\n    )\n    const notifDeclarationUris = dids.map(\n      (did) => `at://${did}/app.bsky.notification.declaration/self`,\n    )\n    const germDeclarationUris = dids.map(\n      (did) => `at://${did}/com.germnetwork.declaration/self`,\n    )\n    const { ref } = db.db.dynamic\n    const [\n      handlesRes,\n      verificationsReceived,\n      profiles,\n      statuses,\n      chatDeclarations,\n      notifDeclarations,\n      germDeclarations,\n    ] = await Promise.all([\n      db.db\n        .selectFrom('actor')\n        .leftJoin('actor_state', 'actor_state.did', 'actor.did')\n        .where('actor.did', 'in', dids)\n        .selectAll('actor')\n        .select('actor_state.priorityNotifs')\n        .select([\n          db.db\n            .selectFrom('labeler')\n            .whereRef('creator', '=', ref('actor.did'))\n            .select(sql<true>`${true}`.as('val'))\n            .as('isLabeler'),\n        ])\n        .execute(),\n      db.db\n        .selectFrom('verification')\n        .selectAll('verification')\n        .innerJoin('actor', 'actor.did', 'verification.creator')\n        .where('verification.subject', 'in', dids)\n        .where('actor.trustedVerifier', '=', true)\n        .orderBy('sortedAt', 'asc')\n        .execute(),\n      getRecords(db)({ uris: profileUris }),\n      getRecords(db)({ uris: statusUris }),\n      getRecords(db)({ uris: chatDeclarationUris }),\n      getRecords(db)({ uris: notifDeclarationUris }),\n      getRecords(db)({ uris: germDeclarationUris }),\n    ])\n\n    const verificationsBySubjectDid = verificationsReceived.reduce(\n      (acc, cur) => {\n        const list = acc.get(cur.subject) ?? []\n        list.push(cur)\n        acc.set(cur.subject, list)\n        return acc\n      },\n      new Map<string, Selectable<Verification>[]>(),\n    )\n\n    const byDid = keyBy(handlesRes, 'did')\n    const actors = dids.map((did, i) => {\n      const row = byDid.get(did)\n\n      const status = statuses.records[i]\n\n      const chatDeclaration = parseRecordBytes<ChatBskyActorDeclaration.Record>(\n        chatDeclarations.records[i].record,\n      )\n\n      const germDeclaration = germDeclarations.records[i]\n\n      const verifications = verificationsBySubjectDid.get(did) ?? []\n      const verifiedBy: VerifiedBy = verifications.reduce((acc, cur) => {\n        acc[cur.creator] = {\n          rkey: cur.rkey,\n          handle: cur.handle,\n          displayName: cur.displayName,\n          sortedAt: Timestamp.fromDate(new Date(cur.sortedAt)),\n        }\n        return acc\n      }, {} as VerifiedBy)\n      const ageAssuranceForDids = new Set(returnAgeAssuranceForDids)\n\n      const activitySubscription = () => {\n        const record = parseRecordBytes<AppBskyNotificationDeclaration.Record>(\n          notifDeclarations.records[i].record,\n        )\n\n        // The dataplane is responsible for setting the default of \"followers\" (default according to the lexicon).\n        const defaultVal = 'followers'\n\n        if (typeof record?.allowSubscriptions !== 'string') {\n          return defaultVal\n        }\n\n        switch (record.allowSubscriptions) {\n          case 'followers':\n          case 'mutuals':\n          case 'none':\n            return record.allowSubscriptions\n          default:\n            return defaultVal\n        }\n      }\n\n      const ageAssuranceStatus = () => {\n        if (!ageAssuranceForDids.has(did)) {\n          return undefined\n        }\n\n        const status = row?.ageAssuranceStatus ?? 'unknown'\n        let access = row?.ageAssuranceAccess\n        if (!access || access === 'unknown') {\n          if (status === 'assured') {\n            access = 'full'\n          } else if (status === 'blocked') {\n            access = 'none'\n          } else {\n            access = 'unknown'\n          }\n        }\n\n        return {\n          lastInitiatedAt: row?.ageAssuranceLastInitiatedAt\n            ? Timestamp.fromDate(new Date(row?.ageAssuranceLastInitiatedAt))\n            : undefined,\n          status,\n          access,\n        }\n      }\n\n      return {\n        exists: !!row,\n        handle: row?.handle ?? undefined,\n        profile: profiles.records[i],\n        takenDown: !!row?.takedownRef,\n        takedownRef: row?.takedownRef || undefined,\n        tombstonedAt: undefined, // in current implementation, tombstoned actors are deleted\n        labeler: row?.isLabeler ?? false,\n        allowIncomingChatsFrom:\n          typeof chatDeclaration?.['allowIncoming'] === 'string'\n            ? chatDeclaration['allowIncoming']\n            : undefined,\n        upstreamStatus: row?.upstreamStatus ?? '',\n        createdAt: profiles.records[i].createdAt, // @NOTE profile creation date not trusted in production\n        priorityNotifications: row?.priorityNotifs ?? false,\n        trustedVerifier: row?.trustedVerifier ?? false,\n        verifiedBy,\n        statusRecord: status,\n        germRecord: germDeclaration,\n        tags: [],\n        profileTags: [],\n        allowActivitySubscriptionsFrom: activitySubscription(),\n        ageAssuranceStatus: ageAssuranceStatus(),\n      }\n    })\n    return { actors }\n  },\n\n  // @TODO handle req.lookupUnidirectional w/ networked handle resolution\n  async getDidsByHandles(req) {\n    const { handles } = req\n    if (handles.length === 0) {\n      return { dids: [] }\n    }\n    const res = await db.db\n      .selectFrom('actor')\n      .where('handle', 'in', handles)\n      .selectAll()\n      .execute()\n    const byHandle = keyBy(res, 'handle')\n    const dids = handles.map((handle) => byHandle.get(handle)?.did ?? '')\n    return { dids }\n  },\n\n  async updateActorUpstreamStatus(req) {\n    const { actorDid, upstreamStatus } = req\n    await db.db\n      .updateTable('actor')\n      .set({ upstreamStatus })\n      .where('did', '=', actorDid)\n      .execute()\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/quotes.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getQuotesBySubjectSorted(req) {\n    const { subject, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    if (!subject?.uri) return { uris: [] }\n\n    let builder = db.db\n      .selectFrom('quote')\n      .where('quote.subject', '=', subject.uri)\n      .select(['quote.uri', 'quote.cid', 'quote.sortAt'])\n\n    const keyset = new TimeCidKeyset(ref('quote.sortAt'), ref('quote.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const quotes = await builder.execute()\n\n    return {\n      uris: quotes.map((q) => q.uri),\n      cursor: keyset.packFromResult(quotes),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/records.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport * as ui8 from 'uint8arrays'\nimport { keyBy } from '@atproto/common'\nimport { AtUri } from '@atproto/syntax'\nimport { ids } from '../../../lexicon/lexicons'\nimport { Service } from '../../../proto/bsky_connect'\nimport { PostRecordMeta, Record } from '../../../proto/bsky_pb'\nimport { Database } from '../db'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  getBlockRecords: getRecords(db, ids.AppBskyGraphBlock),\n  getFeedGeneratorRecords: getRecords(db, ids.AppBskyFeedGenerator),\n  getFollowRecords: getRecords(db, ids.AppBskyGraphFollow),\n  getLikeRecords: getRecords(db, ids.AppBskyFeedLike),\n  getListBlockRecords: getRecords(db, ids.AppBskyGraphListblock),\n  getListItemRecords: getRecords(db, ids.AppBskyGraphListitem),\n  getListRecords: getRecords(db, ids.AppBskyGraphList),\n  getPostRecords: getPostRecords(db),\n  getProfileRecords: getRecords(db, ids.AppBskyActorProfile),\n  getRepostRecords: getRecords(db, ids.AppBskyFeedRepost),\n  getThreadGateRecords: getRecords(db, ids.AppBskyFeedThreadgate),\n  getPostgateRecords: getRecords(db, ids.AppBskyFeedPostgate),\n  getLabelerRecords: getRecords(db, ids.AppBskyLabelerService),\n  getActorChatDeclarationRecords: getRecords(db, ids.ChatBskyActorDeclaration),\n  getNotificationDeclarationRecords: getRecords(\n    db,\n    ids.AppBskyNotificationDeclaration,\n  ),\n  getGermDeclarationRecords: getRecords(db, ids.ComGermnetworkDeclaration),\n  getStarterPackRecords: getRecords(db, ids.AppBskyGraphStarterpack),\n  getVerificationRecords: getRecords(db, ids.AppBskyGraphVerification),\n  getStatusRecords: getRecords(db, ids.AppBskyActorStatus),\n})\n\nexport const getRecords =\n  (db: Database, collection?: string) =>\n  async (req: { uris: string[] }): Promise<{ records: Record[] }> => {\n    const validUris = collection\n      ? req.uris.filter((uri) => new AtUri(uri).collection === collection)\n      : req.uris\n    const res = validUris.length\n      ? await db.db\n          .selectFrom('record')\n          .selectAll()\n          .where('uri', 'in', validUris)\n          .execute()\n      : []\n    const byUri = keyBy(res, 'uri')\n    const records: Record[] = req.uris.map((uri) => {\n      const row = byUri.get(uri)\n      const json = row ? row.json : JSON.stringify(null)\n      const createdAtRaw = new Date(JSON.parse(json)?.['createdAt'])\n      const createdAt = !isNaN(createdAtRaw.getTime())\n        ? Timestamp.fromDate(createdAtRaw)\n        : undefined\n      const indexedAt = row?.indexedAt\n        ? Timestamp.fromDate(new Date(row?.indexedAt))\n        : undefined\n      const recordBytes = ui8.fromString(json, 'utf8')\n      return new Record({\n        record: recordBytes,\n        cid: row?.cid,\n        createdAt,\n        indexedAt,\n        sortedAt: compositeTime(createdAt, indexedAt),\n        takenDown: !!row?.takedownRef,\n        takedownRef: row?.takedownRef ?? undefined,\n        tags: row?.tags ?? undefined,\n      })\n    })\n    return { records }\n  }\n\nexport const getPostRecords = (db: Database) => {\n  const getBaseRecords = getRecords(db, ids.AppBskyFeedPost)\n  return async (req: {\n    uris: string[]\n  }): Promise<{ records: Record[]; meta: PostRecordMeta[] }> => {\n    const [{ records }, details] = await Promise.all([\n      getBaseRecords(req),\n      req.uris.length\n        ? await db.db\n            .selectFrom('post')\n            .where('uri', 'in', req.uris)\n            .select([\n              'uri',\n              'violatesThreadGate',\n              'violatesEmbeddingRules',\n              'hasThreadGate',\n              'hasPostGate',\n            ])\n            .execute()\n        : [],\n    ])\n    const byKey = keyBy(details, 'uri')\n    const meta = req.uris.map((uri) => {\n      return new PostRecordMeta({\n        violatesThreadGate: !!byKey.get(uri)?.violatesThreadGate,\n        violatesEmbeddingRules: !!byKey.get(uri)?.violatesEmbeddingRules,\n        hasThreadGate: !!byKey.get(uri)?.hasThreadGate,\n        hasPostGate: !!byKey.get(uri)?.hasPostGate,\n      })\n    })\n    return { records, meta }\n  }\n}\n\nconst compositeTime = (\n  ts1: Timestamp | undefined,\n  ts2: Timestamp | undefined,\n) => {\n  if (!ts1) return ts2\n  if (!ts2) return ts1\n  return ts1.toDate() < ts2.toDate() ? ts1 : ts2\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/relationships.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { valuesList } from '../db/util'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getRelationships(req) {\n    const { actorDid, targetDids } = req\n    if (targetDids.length === 0) {\n      return { relationships: [] }\n    }\n    const { ref } = db.db.dynamic\n    const res = await db.db\n      .selectFrom('actor')\n      .where('did', 'in', targetDids)\n      .select([\n        'actor.did',\n        db.db\n          .selectFrom('mute')\n          .where('mute.mutedByDid', '=', actorDid)\n          .whereRef('mute.subjectDid', '=', ref('actor.did'))\n          .select(sql<true>`${true}`.as('val'))\n          .as('muted'),\n        db.db\n          .selectFrom('list_item')\n          .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri')\n          .where('list_mute.mutedByDid', '=', actorDid)\n          .whereRef('list_item.subjectDid', '=', ref('actor.did'))\n          .select('list_item.listUri')\n          .as('mutedByList'),\n        db.db\n          .selectFrom('actor_block')\n          .where('actor_block.creator', '=', actorDid)\n          .whereRef('actor_block.subjectDid', '=', ref('actor.did'))\n          .select('uri')\n          .as('blocking'),\n        db.db\n          .selectFrom('actor_block')\n          .where('actor_block.subjectDid', '=', actorDid)\n          .whereRef('actor_block.creator', '=', ref('actor.did'))\n          .select('uri')\n          .as('blockedBy'),\n        db.db\n          .selectFrom('list_item')\n          .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri')\n          .where('list_block.creator', '=', actorDid)\n          .whereRef('list_item.subjectDid', '=', ref('actor.did'))\n          .select('list_item.listUri')\n          .as('blockingByList'),\n        db.db\n          .selectFrom('list_item')\n          .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri')\n          .where('list_item.subjectDid', '=', actorDid)\n          .whereRef('list_block.creator', '=', ref('actor.did'))\n          .select('list_item.listUri')\n          .as('blockedByList'),\n        db.db\n          .selectFrom('follow')\n          .where('follow.creator', '=', actorDid)\n          .whereRef('follow.subjectDid', '=', ref('actor.did'))\n          .select('uri')\n          .as('following'),\n        db.db\n          .selectFrom('follow')\n          .where('follow.subjectDid', '=', actorDid)\n          .whereRef('follow.creator', '=', ref('actor.did'))\n          .select('uri')\n          .as('followedBy'),\n      ])\n      .execute()\n    const byDid = keyBy(res, 'did')\n    const relationships = targetDids.map((did) => {\n      const row = byDid.get(did)\n      return {\n        muted: row?.muted ?? false,\n        mutedByList: row?.mutedByList ?? '',\n        blockedBy: row?.blockedBy ?? '',\n        blocking: row?.blocking ?? '',\n        blockedByList: row?.blockedByList ?? '',\n        blockingByList: row?.blockingByList ?? '',\n        following: row?.following ?? '',\n        followedBy: row?.followedBy ?? '',\n      }\n    })\n    return { relationships }\n  },\n\n  async getBlockExistence(req) {\n    const { pairs } = req\n    if (pairs.length === 0) {\n      return { exists: [], blocks: [] }\n    }\n    const { ref } = db.db.dynamic\n    const sourceRef = ref('pair.source')\n    const targetRef = ref('pair.target')\n    const values = valuesList(pairs.map((p) => sql`${p.a}, ${p.b}`))\n    const res = await db.db\n      .selectFrom(values.as(sql`pair (source, target)`))\n      .select([\n        sql<string>`${sourceRef}`.as('source'),\n        sql<string>`${targetRef}`.as('target'),\n        (eb) =>\n          eb\n            .selectFrom('actor_block')\n            .whereRef('actor_block.creator', '=', sourceRef)\n            .whereRef('actor_block.subjectDid', '=', targetRef)\n            .select('uri')\n            .as('blocking'),\n        (eb) =>\n          eb\n            .selectFrom('actor_block')\n            .whereRef('actor_block.creator', '=', targetRef)\n            .whereRef('actor_block.subjectDid', '=', sourceRef)\n            .select('uri')\n            .as('blockedBy'),\n        (eb) =>\n          eb\n            .selectFrom('list_item')\n            .innerJoin(\n              'list_block',\n              'list_block.subjectUri',\n              'list_item.listUri',\n            )\n            .whereRef('list_block.creator', '=', sourceRef)\n            .whereRef('list_item.subjectDid', '=', targetRef)\n            .select('list_item.listUri')\n            .as('blockingByList'),\n        (eb) =>\n          eb\n            .selectFrom('list_item')\n            .innerJoin(\n              'list_block',\n              'list_block.subjectUri',\n              'list_item.listUri',\n            )\n            .whereRef('list_block.creator', '=', targetRef)\n            .whereRef('list_item.subjectDid', '=', sourceRef)\n            .select('list_item.listUri')\n            .as('blockedByList'),\n      ])\n      .execute()\n    const getKey = (a, b) => [a, b].sort().join(',')\n    const lookup = res.reduce((acc, cur) => {\n      const key = getKey(cur.source, cur.target)\n      return acc.set(key, cur)\n    }, new Map<string, (typeof res)[0]>())\n    return {\n      exists: pairs.map((pair) => {\n        const item = lookup.get(getKey(pair.a, pair.b))\n        if (!item) return false\n        return !!(\n          item.blocking ||\n          item.blockedBy ||\n          item.blockingByList ||\n          item.blockedByList\n        )\n      }),\n      blocks: pairs.map((pair) => {\n        const item = lookup.get(getKey(pair.a, pair.b))\n        if (!item) return {}\n        return {\n          blockedBy: item.blockedBy || undefined,\n          blocking: item.blocking || undefined,\n          blockedByList: item.blockedByList || undefined,\n          blockingByList: item.blockingByList || undefined,\n        }\n      }),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/reposts.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { keyBy } from '@atproto/common'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getRepostsBySubject(req) {\n    const { subject, cursor, limit } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('repost')\n      .where('repost.subject', '=', subject?.uri ?? '')\n      .selectAll('repost')\n\n    const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const reposts = await builder.execute()\n\n    return {\n      uris: reposts.map((l) => l.uri),\n      cursor: keyset.packFromResult(reposts),\n    }\n  },\n\n  async getRepostsByActorAndSubjects(req) {\n    const { actorDid, refs } = req\n    if (refs.length === 0) {\n      return { uris: [] }\n    }\n    const res = await db.db\n      .selectFrom('repost')\n      .where('creator', '=', actorDid)\n      .where(\n        'subject',\n        'in',\n        refs.map(({ uri }) => uri),\n      )\n      .selectAll()\n      .execute()\n    const bySubject = keyBy(res, 'subject')\n    const uris = refs.map(({ uri }) => bySubject.get(uri)?.uri ?? '')\n    return { uris }\n  },\n\n  async getActorReposts(req) {\n    const { actorDid, limit, cursor } = req\n    const { ref } = db.db.dynamic\n\n    let builder = db.db\n      .selectFrom('repost')\n      .where('repost.creator', '=', actorDid)\n      .selectAll()\n\n    const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid'))\n\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n\n    const reposts = await builder.execute()\n\n    return {\n      uris: reposts.map((l) => l.uri),\n      cursor: keyset.packFromResult(reposts),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/search.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { IndexedAtDidKeyset, TimeCidKeyset, paginate } from '../db/pagination'\nimport { parsePostSearchQuery } from '../util'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  // @TODO actor search endpoints still fall back to search service\n  async searchActors(req) {\n    const { term, limit, cursor } = req\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('actor')\n      .where('actor.handle', 'like', `%${cleanQuery(term)}%`)\n      .selectAll()\n\n    const keyset = new IndexedAtDidKeyset(\n      ref('actor.indexedAt'),\n      ref('actor.did'),\n    )\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const res = await builder.execute()\n\n    return {\n      dids: res.map((row) => row.did),\n      cursor: keyset.packFromResult(res),\n    }\n  },\n\n  // @TODO post search endpoint still falls back to search service\n  async searchPosts(req) {\n    const { term, limit, cursor } = req\n    const { q, author } = parsePostSearchQuery(term)\n\n    let authorDid = author\n    if (author && !author?.startsWith('did:')) {\n      const res = await db.db\n        .selectFrom('actor')\n        .where('handle', '=', author)\n        .selectAll()\n        .executeTakeFirst()\n      authorDid = res?.did\n    }\n\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('post')\n      .where('post.text', 'like', `%${q}%`)\n      .selectAll()\n\n    if (authorDid) {\n      builder = builder.where('post.creator', '=', authorDid)\n    }\n\n    const keyset = new TimeCidKeyset(ref('post.sortAt'), ref('post.cid'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const res = await builder.execute()\n    return {\n      uris: res.map((row) => row.uri),\n      cursor: keyset.packFromResult(res),\n    }\n  },\n\n  async searchStarterPacks(req) {\n    const { term, limit, cursor } = req\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('starter_pack')\n      .where('starter_pack.name', 'ilike', `%${term}%`)\n      .selectAll()\n\n    const keyset = new TimeCidKeyset(\n      ref('starter_pack.sortAt'),\n      ref('starter_pack.cid'),\n    )\n\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n    })\n\n    const res = await builder.execute()\n\n    const cur = keyset.packFromResult(res)\n\n    return {\n      uris: res.map((row) => row.uri),\n      cursor: cur,\n    }\n  },\n})\n\n// Remove leading @ in case a handle is input that way\nconst cleanQuery = (query: string) => query.trim().replace(/^@/g, '')\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/sitemap.ts",
    "content": "import { gzipSync } from 'node:zlib'\nimport { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { GetSitemapPageRequest } from '../../../proto/bsky_pb'\n\nconst MOCK_SITEMAP_INDEX = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  <sitemap>\n    <loc>https://bsky.app/sitemap/users/2025-01-01/1.xml.gz</loc>\n  </sitemap>\n</sitemapindex>`\n\nconst MOCK_SITEMAP_PAGE = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  <url>\n    <loc>https://bsky.app/profile/test.bsky.social</loc>\n  </url>\n</urlset>`\n\nexport default (): Partial<ServiceImpl<typeof Service>> => ({\n  async getSitemapIndex() {\n    return {\n      sitemap: gzipSync(Buffer.from(MOCK_SITEMAP_INDEX)),\n    }\n  },\n  async getSitemapPage(req: GetSitemapPageRequest) {\n    const date = req.date?.toDate()\n    const isExpectedDate =\n      date &&\n      date.getFullYear() === 2025 &&\n      date.getMonth() === 0 &&\n      date.getDate() === 1\n    const isExpectedBucket = req.bucket === 1\n\n    if (!isExpectedDate || !isExpectedBucket) {\n      throw new ConnectError('Sitemap page not found', Code.NotFound)\n    }\n\n    return {\n      sitemap: gzipSync(Buffer.from(MOCK_SITEMAP_PAGE)),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/starter-packs.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { TimeCidKeyset, paginate } from '../db/pagination'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getActorStarterPacks(req) {\n    const { actorDid, limit, cursor } = req\n\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('starter_pack')\n      .selectAll()\n      .where('creator', '=', actorDid)\n\n    const keyset = new TimeCidKeyset(\n      ref('starter_pack.sortAt'),\n      ref('starter_pack.cid'),\n    )\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n    })\n    const starterPacks = await builder.execute()\n\n    return {\n      uris: starterPacks.map((sp) => sp.uri),\n      cursor: keyset.packFromResult(starterPacks),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/suggestions.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getFollowSuggestions(req) {\n    const { actorDid, relativeToDid, cursor, limit } = req\n    if (relativeToDid) {\n      return getFollowSuggestionsRelativeTo(db, {\n        actorDid,\n        relativeToDid,\n        cursor: cursor || undefined,\n        limit: limit || undefined,\n      })\n    } else {\n      return getFollowSuggestionsGlobal(db, {\n        actorDid,\n        cursor: cursor || undefined,\n        limit: limit || undefined,\n      })\n    }\n  },\n\n  async getSuggestedEntities() {\n    const entities = await db.db\n      .selectFrom('tagged_suggestion')\n      .selectAll()\n      .execute()\n    return {\n      entities,\n    }\n  },\n})\n\nconst getFollowSuggestionsGlobal = async (\n  db: Database,\n  input: { actorDid: string; cursor?: string; limit?: number },\n) => {\n  const alreadyIncluded = parseCursor(input.cursor)\n  const suggestions = await db.db\n    .selectFrom('suggested_follow')\n    .innerJoin('actor', 'actor.did', 'suggested_follow.did')\n    .if(alreadyIncluded.length > 0, (qb) =>\n      qb.where('suggested_follow.order', 'not in', alreadyIncluded),\n    )\n    .selectAll()\n    .orderBy('suggested_follow.order', 'asc')\n    .execute()\n\n  // always include first two\n  const firstTwo = suggestions.filter(\n    (row) => row.order === 1 || row.order === 2,\n  )\n  const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2)\n  const limited = firstTwo.concat(shuffle(rest)).slice(0, input.limit)\n\n  // if the result set ends up getting larger, consider using a seed included in the cursor for the randomized shuffle\n  const cursor =\n    limited.length > 0\n      ? limited\n          .map((row) => row.order.toString())\n          .concat(alreadyIncluded.map((id) => id.toString()))\n          .join(':')\n      : undefined\n\n  return {\n    dids: limited.map((s) => s.did),\n    cursor,\n  }\n}\n\nconst getFollowSuggestionsRelativeTo = async (\n  db: Database,\n  input: {\n    actorDid: string\n    relativeToDid: string\n    cursor?: string\n    limit?: number\n  },\n) => {\n  if (input.cursor) return { dids: [] }\n  const limit = input.limit ? Math.min(10, input.limit) : 10\n  const actorsViewerFollows = db.db\n    .selectFrom('follow')\n    .where('creator', '=', input.actorDid)\n    .select('subjectDid')\n  const mostLikedAccounts = await db.db\n    .selectFrom(\n      db.db\n        .selectFrom('like')\n        .where('creator', '=', input.relativeToDid)\n        .select(sql`split_part(subject, '/', 3)`.as('subjectDid'))\n        .orderBy('sortAt', 'desc')\n        .limit(1000) // limit to 1000\n        .as('likes'),\n    )\n    .select('likes.subjectDid as did')\n    .select((qb) => qb.fn.count('likes.subjectDid').as('count'))\n    .where('likes.subjectDid', 'not in', actorsViewerFollows)\n    .where('likes.subjectDid', 'not in', [input.actorDid, input.relativeToDid])\n    .groupBy('likes.subjectDid')\n    .orderBy('count', 'desc')\n    .limit(limit)\n    .execute()\n  const resultDids = mostLikedAccounts.map((a) => ({ did: a.did })) as {\n    did: string\n  }[]\n\n  if (resultDids.length < limit) {\n    // backfill with popular accounts followed by actor\n    const mostPopularAccountsActorFollows = await db.db\n      .selectFrom('follow')\n      .innerJoin('profile_agg', 'follow.subjectDid', 'profile_agg.did')\n      .select('follow.subjectDid as did')\n      .where('follow.creator', '=', input.actorDid)\n      .where('follow.subjectDid', '!=', input.relativeToDid)\n      .where('follow.subjectDid', 'not in', actorsViewerFollows)\n      .if(resultDids.length > 0, (qb) =>\n        qb.where(\n          'subjectDid',\n          'not in',\n          resultDids.map((a) => a.did),\n        ),\n      )\n      .orderBy('profile_agg.followersCount', 'desc')\n      .limit(limit)\n      .execute()\n\n    resultDids.push(...mostPopularAccountsActorFollows)\n  }\n\n  if (resultDids.length < limit) {\n    // backfill with suggested_follow table\n    const additional = await db.db\n      .selectFrom('suggested_follow')\n      .where(\n        'did',\n        'not in',\n        // exclude any we already have\n        resultDids\n          .map((a) => a.did)\n          .concat([input.actorDid, input.relativeToDid]),\n      )\n      // and aren't already followed by viewer\n      .where('did', 'not in', actorsViewerFollows)\n      .selectAll()\n      .execute()\n\n    resultDids.push(...additional)\n  }\n\n  return { dids: resultDids.map((x) => x.did) }\n}\n\nconst parseCursor = (cursor?: string): number[] => {\n  if (!cursor) {\n    return []\n  }\n  try {\n    return cursor\n      .split(':')\n      .map((id) => parseInt(id, 10))\n      .filter((id) => !isNaN(id))\n  } catch {\n    return []\n  }\n}\n\nconst shuffle = <T>(arr: T[]): T[] => {\n  return arr\n    .map((value) => ({ value, sort: Math.random() }))\n    .sort((a, b) => a.sort - b.sort)\n    .map(({ value }) => value)\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/sync.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getLatestRev(req) {\n    const res = await db.db\n      .selectFrom('actor_sync')\n      .where('did', '=', req.actorDid)\n      .select('repoRev')\n      .executeTakeFirst()\n    return {\n      rev: res?.repoRev ?? undefined,\n    }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/routes/threads.ts",
    "content": "import { ServiceImpl } from '@connectrpc/connect'\nimport { Service } from '../../../proto/bsky_connect'\nimport { Database } from '../db'\nimport { getAncestorsAndSelfQb, getDescendentsQb } from '../util'\n\nexport default (db: Database): Partial<ServiceImpl<typeof Service>> => ({\n  async getThread(req) {\n    const { postUri, above, below } = req\n    const [ancestors, descendents] = await Promise.all([\n      getAncestorsAndSelfQb(db.db, {\n        uri: postUri,\n        parentHeight: above,\n      })\n        .selectFrom('ancestor')\n        .selectAll()\n        .execute(),\n      getDescendentsQb(db.db, {\n        uri: postUri,\n        depth: below,\n      })\n        .selectFrom('descendent')\n        .innerJoin('post', 'post.uri', 'descendent.uri')\n        .orderBy('post.sortAt', 'desc')\n        .selectAll()\n        .execute(),\n    ])\n    const uris = [\n      ...ancestors.map((p) => p.uri),\n      ...descendents.map((p) => p.uri),\n    ]\n    return { uris }\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/subscription.ts",
    "content": "import { IdResolver } from '@atproto/identity'\nimport { WriteOpAction } from '@atproto/repo'\nimport { Event as FirehoseEvent, Firehose, MemoryRunner } from '@atproto/sync'\nimport { subLogger as log } from '../../logger'\nimport { BackgroundQueue } from './background'\nimport { Database } from './db'\nimport { IndexingService } from './indexing'\n\nexport class RepoSubscription {\n  firehose: Firehose\n  runner: MemoryRunner\n  background: BackgroundQueue\n  indexingSvc: IndexingService\n\n  constructor(\n    public opts: { service: string; db: Database; idResolver: IdResolver },\n  ) {\n    const { service, db, idResolver } = opts\n    this.background = new BackgroundQueue(db)\n    this.indexingSvc = new IndexingService(db, idResolver, this.background)\n\n    const { runner, firehose } = createFirehose({\n      idResolver,\n      service,\n      indexingSvc: this.indexingSvc,\n    })\n    this.runner = runner\n    this.firehose = firehose\n  }\n\n  start() {\n    this.firehose.start()\n  }\n\n  async restart() {\n    await this.destroy()\n    const { runner, firehose } = createFirehose({\n      idResolver: this.opts.idResolver,\n      service: this.opts.service,\n      indexingSvc: this.indexingSvc,\n    })\n    this.runner = runner\n    this.firehose = firehose\n    this.start()\n  }\n\n  async processAll() {\n    await this.runner.processAll()\n    await this.background.processAll()\n  }\n\n  async destroy() {\n    await this.firehose.destroy()\n    await this.runner.destroy()\n    await this.background.processAll()\n  }\n}\n\nconst createFirehose = (opts: {\n  idResolver: IdResolver\n  service: string\n  indexingSvc: IndexingService\n}) => {\n  const { idResolver, service, indexingSvc } = opts\n  const runner = new MemoryRunner({ startCursor: 0 })\n  const firehose = new Firehose({\n    idResolver,\n    runner,\n    service,\n    unauthenticatedHandles: true, // indexing service handles these\n    unauthenticatedCommits: true, // @TODO there seems to be a very rare issue where the authenticator thinks a block is missing in deletion ops\n    onError: (err) => log.error({ err }, 'error in subscription'),\n    handleEvent: async (evt: FirehoseEvent) => {\n      if (evt.event === 'identity') {\n        await indexingSvc.indexHandle(evt.did, evt.time, true)\n      } else if (evt.event === 'account') {\n        if (evt.active === false && evt.status === 'deleted') {\n          await indexingSvc.deleteActor(evt.did)\n        } else {\n          await indexingSvc.updateActorStatus(evt.did, evt.active, evt.status)\n        }\n      } else if (evt.event === 'sync') {\n        await Promise.all([\n          indexingSvc.setCommitLastSeen(evt.did, evt.cid, evt.rev),\n          indexingSvc.indexHandle(evt.did, evt.time),\n        ])\n      } else {\n        const indexFn =\n          evt.event === 'delete'\n            ? indexingSvc.deleteRecord(evt.uri)\n            : indexingSvc.indexRecord(\n                evt.uri,\n                evt.cid,\n                evt.record,\n                evt.event === 'create'\n                  ? WriteOpAction.Create\n                  : WriteOpAction.Update,\n                evt.time,\n              )\n        await Promise.all([\n          indexFn,\n          indexingSvc.setCommitLastSeen(evt.did, evt.commit, evt.rev),\n          indexingSvc.indexHandle(evt.did, evt.time),\n        ])\n      }\n    },\n  })\n  return { firehose, runner }\n}\n"
  },
  {
    "path": "packages/bsky/src/data-plane/server/util.ts",
    "content": "import { sql } from 'kysely'\nimport {\n  Record as PostRecord,\n  ReplyRef,\n} from '../../lexicon/types/app/bsky/feed/post'\nimport { Record as GateRecord } from '../../lexicon/types/app/bsky/feed/threadgate'\nimport { parseThreadGate } from '../../views/util'\nimport { DatabaseSchema } from './db/database-schema'\nimport { valuesList } from './db/util'\n\nexport const getDescendentsQb = (\n  db: DatabaseSchema,\n  opts: {\n    uri: string\n    depth: number // required, protects against cycles\n  },\n) => {\n  const { uri, depth } = opts\n  const query = db.withRecursive('descendent(uri, depth)', (cte) => {\n    return cte\n      .selectFrom('post')\n      .select(['post.uri as uri', sql<number>`1`.as('depth')])\n      .where(sql`1`, '<=', depth)\n      .where('replyParent', '=', uri)\n      .unionAll(\n        cte\n          .selectFrom('post')\n          .innerJoin('descendent', 'descendent.uri', 'post.replyParent')\n          .where('descendent.depth', '<', depth)\n          .select([\n            'post.uri as uri',\n            sql<number>`descendent.depth + 1`.as('depth'),\n          ]),\n      )\n  })\n  return query\n}\n\nexport const getAncestorsAndSelfQb = (\n  db: DatabaseSchema,\n  opts: {\n    uri: string\n    parentHeight: number // required, protects against cycles\n  },\n) => {\n  const { uri, parentHeight } = opts\n  const query = db.withRecursive(\n    'ancestor(uri, ancestorUri, height)',\n    (cte) => {\n      return cte\n        .selectFrom('post')\n        .select([\n          'post.uri as uri',\n          'post.replyParent as ancestorUri',\n          sql<number>`0`.as('height'),\n        ])\n        .where('uri', '=', uri)\n        .unionAll(\n          cte\n            .selectFrom('post')\n            .innerJoin('ancestor', 'ancestor.ancestorUri', 'post.uri')\n            .where('ancestor.height', '<', parentHeight)\n            .select([\n              'post.uri as uri',\n              'post.replyParent as ancestorUri',\n              sql<number>`ancestor.height + 1`.as('height'),\n            ]),\n        )\n    },\n  )\n  return query\n}\n\nexport const invalidReplyRoot = (\n  reply: ReplyRef,\n  parent: {\n    record: PostRecord\n    invalidReplyRoot: boolean | null\n  },\n) => {\n  const replyRoot = reply.root.uri\n  const replyParent = reply.parent.uri\n  // if parent is not a valid reply, transitively this is not a valid one either\n  if (parent.invalidReplyRoot) {\n    return true\n  }\n  // replying to root post: ensure the root looks correct\n  if (replyParent === replyRoot) {\n    return !!parent.record.reply\n  }\n  // replying to a reply: ensure the parent is a reply for the same root post\n  return parent.record.reply?.root.uri !== replyRoot\n}\nexport const violatesThreadGate = async (\n  db: DatabaseSchema,\n  replierDid: string,\n  ownerDid: string,\n  rootPost: PostRecord | null,\n  gate: GateRecord | null,\n) => {\n  const {\n    canReply,\n    allowFollower,\n    allowFollowing,\n    allowListUris = [],\n  } = parseThreadGate(replierDid, ownerDid, rootPost, gate)\n  if (canReply) {\n    return false\n  }\n  if (!allowFollower && !allowFollowing && !allowListUris?.length) {\n    return true\n  }\n  const { ref } = db.dynamic\n  const nullResult = sql<null>`${null}`\n  const check = await db\n    .selectFrom(valuesList([replierDid]).as(sql`subject (did)`))\n    .select([\n      allowFollower\n        ? db\n            .selectFrom('follow')\n            .where('subjectDid', '=', ownerDid)\n            .whereRef('creator', '=', ref('subject.did'))\n            .select('subjectDid')\n            .as('isFollower')\n        : nullResult.as('isFollower'),\n      allowFollowing\n        ? db\n            .selectFrom('follow')\n            .where('creator', '=', ownerDid)\n            .whereRef('subjectDid', '=', ref('subject.did'))\n            .select('creator')\n            .as('isFollowed')\n        : nullResult.as('isFollowed'),\n      allowListUris.length\n        ? db\n            .selectFrom('list_item')\n            .where('list_item.listUri', 'in', allowListUris)\n            .whereRef('list_item.subjectDid', '=', ref('subject.did'))\n            .limit(1)\n            .select('listUri')\n            .as('isInList')\n        : nullResult.as('isInList'),\n    ])\n    .executeTakeFirst()\n\n  if (allowFollowing && check?.isFollowed) {\n    return false\n  } else if (allowFollower && check?.isFollower) {\n    return false\n  } else if (allowListUris.length && check?.isInList) {\n    return false\n  }\n\n  return true\n}\n\n// @NOTE: This type is not complete with all supported options.\n// Only the ones that we needed to apply custom logic on are currently present.\nexport type PostSearchQuery = {\n  q: string\n  author: string | undefined\n}\n\nexport const parsePostSearchQuery = (\n  qParam: string,\n  params?: {\n    author?: string\n  },\n): PostSearchQuery => {\n  // Accept individual params, but give preference to options embedded in `q`.\n  let author = params?.author\n\n  const parts: string[] = []\n  let curr = ''\n  let quoted = false\n  for (const c of qParam) {\n    if (c === ' ' && !quoted) {\n      curr.trim() && parts.push(curr)\n      curr = ''\n      continue\n    }\n\n    if (c === '\"') {\n      quoted = !quoted\n    }\n    curr += c\n  }\n  curr.trim() && parts.push(curr)\n\n  const qParts: string[] = []\n  for (const p of parts) {\n    const tokens = p.split(':')\n    if (tokens[0] === 'did') {\n      author = p\n    } else if (tokens[0] === 'author' || tokens[0] === 'from') {\n      author = tokens[1]\n    } else {\n      qParts.push(p)\n    }\n  }\n\n  return {\n    q: qParts.join(' '),\n    author,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/error.ts",
    "content": "import { ErrorRequestHandler } from 'express'\nimport { XRPCError } from '@atproto/xrpc-server'\nimport { httpLogger as log } from './logger'\n\nexport const handler: ErrorRequestHandler = (err, _req, res, next) => {\n  log.error({ err }, 'unexpected internal server error')\n  if (res.headersSent) {\n    return next(err)\n  }\n  const serverError = XRPCError.fromError(err)\n  res.status(serverError.type).json(serverError.payload)\n}\n"
  },
  {
    "path": "packages/bsky/src/etcd.ts",
    "content": "import { once } from 'node:events'\nimport { Etcd3, Watcher } from 'etcd3'\n\n/**\n * A reactive map based on the keys and values stored within etcd under a given prefix.\n */\nexport class EtcdMap {\n  inner = new Map<string, VersionedValue>()\n  watcher: Watcher\n  connecting: Promise<void> | undefined\n  handlers: ((self: EtcdMap) => void)[] = []\n\n  constructor(\n    private etcd: Etcd3,\n    private prefix = '',\n  ) {\n    this.watcher = etcd.watch().prefix(prefix).watcher()\n    this.connecting = connectWatcher(this.watcher)\n  }\n\n  async connect() {\n    this.watcher.on('put', (kv) => {\n      const key = kv.key.toString()\n      const value = kv.value.toString()\n      const rev = revToInt(kv.mod_revision)\n      this.apply(key, { value, rev })\n    })\n    this.watcher.on('delete', (kv) => {\n      const key = kv.key.toString()\n      const value = null\n      const rev = revToInt(kv.mod_revision)\n      this.apply(key, { value, rev })\n    })\n    await this.connecting?.finally(() => {\n      this.connecting = undefined\n    })\n    const { kvs } = await this.etcd.getAll().prefix(this.prefix).exec()\n    for (const kv of kvs) {\n      const key = kv.key.toString()\n      const value = kv.value.toString()\n      const rev = revToInt(kv.mod_revision)\n      this.apply(key, { value, rev })\n    }\n  }\n\n  get(key: string) {\n    return this.inner.get(key)?.value ?? null\n  }\n\n  *values() {\n    for (const { value } of this.inner.values()) {\n      if (value !== null) {\n        yield value\n      }\n    }\n  }\n\n  onUpdate(handler: (self: EtcdMap) => void) {\n    this.handlers.push(handler)\n  }\n\n  private update() {\n    for (const handler of this.handlers) {\n      handler(this)\n    }\n  }\n\n  private apply(key, vv: VersionedValue) {\n    const curr = this.inner.get(key)\n    if (curr && curr.rev > vv.rev) return\n    this.inner.set(key, vv)\n    this.update()\n  }\n}\n\nfunction revToInt(rev: string) {\n  return parseInt(rev, 10)\n}\n\nasync function connectWatcher(watcher: Watcher) {\n  await Promise.race([\n    once(watcher, 'connected'),\n    once(watcher, 'error').then((err) => Promise.reject(err)),\n  ])\n}\n\ntype VersionedValue = {\n  rev: number\n  value: string | null\n}\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/README.md",
    "content": "# Feature Gates\n\nA thin wrapper around [GrowthBook](https://www.growthbook.io/) for feature flag\nevaluation and experiment tracking.\n\n## Usage\n\nFeature gates are accessible on `HydrationCtx` as `features`. A default \"scope\"\nis defined in `Hydrator.createContext`, which is called by every request\nhandler. The default scope supplies anonymous `deviceId` and `sessionId`\nidentifiers for targeting unauthenticated users.\n\nFeature gates can be checked via the following methods.\n\n```typescript\n// check a single gate\nconst enabled = ctx.features.checkGate(\n  ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n)\n\n// check multiple gates\nconst gates = ctx.features.checkGates([\n  ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n  ctx.features.Gate.SomeOtherGate,\n])\nconst enabled = gates.get(\n  ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n)\n```\n\n### User Context\n\nTo accurately gate features by user, we need additional context about that user.\nFor most use cases, we get this data from within the request handlers.\n\nHere, the `features` client will be scoped to the current user, allowing for\naccurate gate checks throughout the request lifecycle.\n\n```typescript\nconst features: ScopedFeatureGatesClient = ctx.featureGatesClient.scope(\n  ctx.featureGatesClient.parseUserContextFromHandler({\n    viewer,\n    req,\n  }),\n)\n\nconst hydrateCtx = await ctx.hydrator.createContext({\n  labelers,\n  viewer,\n  features,\n})\n```\n\n> [!NOTE]\n> Although `ctx.featureGatesClient` also has a `checkGates` method, the\n> intention is to use the `ScopedFeatureGatesClient` returned by `scope()` for\n> gate checks within the application.\n\nYou can also pass the user context directly to `checkGate` or `checkGates`. This\noverrides any default scope set on the client, allowing for flexibility in cases\nwhere user context is only available at the point of gate evaluation.\n\n```typescript\nconst enabled = ctx.featureGatesClient.checkGate(\n  ctx.featureGatesClient.Gate.ImageFeatureEnabled,\n  { did: imageAuthor.did },\n)\n```\n\n### Adding Gates\n\nSee `gates.ts` and add them to the enum. You can optionally prevent metrics from\nbeing emitted for a gate by adding the gate to the `IGNORE_METRICS_FOR_GATES`\nset.\n\n## Metrics\n\nCheck out the Growthbook docs for more info here. Basically, every feature eval\nfires the `onFeatureUsage` callback. Any feature that is part of an experiment\n_will also fire the `trackingCallback` callback._\n\nFor some use cases, this can be noisy and/or performance intensive, so make use\nof `IGNORE_METRICS_FOR_GATES` to silence metrics for specific gates.\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/gates.ts",
    "content": "/**\n * Enum of all gates in the system. This should be the single source of truth\n * for all gates, and should be used in all places where gates are checked or\n * defined.\n */\nexport enum Gate {\n  SuggestedUsersDiscoverEnable = 'suggested_users:discover_agent:enable',\n  SuggestedUsersSocialProofEnable = 'suggested_users:social_proof:enable',\n  ThreadsReplyRankingExplorationEnable = 'threads:reply_ranking_exploration:enable',\n  SearchFilteringExplorationEnable = 'search:filtering_exploration:enable',\n}\n\n/**\n * Set of gates that should be ignored when tracking gate evaluations for\n * analytics purposes. This is useful for gates that are not user-facing or are\n * overly noisy.\n */\nexport const IGNORE_METRICS_FOR_GATES: Set<Gate> = new Set([])\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/index.ts",
    "content": "import { GrowthBookClient } from '@growthbook/growthbook'\nimport type express from 'express'\nimport { featureGatesLogger } from '../logger'\nimport { Gate, IGNORE_METRICS_FOR_GATES } from './gates'\nimport { MetricsClient } from './metrics'\nimport {\n  CheckedFeatureGatesMap,\n  ScopedFeatureGatesClient,\n  UserContext,\n} from './types'\nimport {\n  extractUserContextFromGrowthbookUserContext,\n  mergeUserContexts,\n  normalizeUserContext,\n  parsedUserContextToTrackingMetadata,\n} from './utils'\n\n/**\n * We want this to be sufficiently high that we don't time out under\n * normal conditions, but not so high that it takes too long to boot\n * the server.\n */\nconst FETCH_TIMEOUT = 3e3 // 3 seconds\n\n/**\n * StatSig used to default to every 10s, but I think 1m is fine\n */\nconst REFETCH_INTERVAL = 60e3 // 1 minute\n\n/**\n * These need to match what the client sends\n */\nconst ANALYTICS_HEADER_DEVICE_ID = 'X-Bsky-Device-Id'\nconst ANALYTICS_HEADER_SESSION_ID = 'X-Bsky-Session-Id'\n\nexport { type ScopedFeatureGatesClient } from './types'\n\nexport class FeatureGatesClient {\n  private ready = false\n  private client: GrowthBookClient | undefined = undefined\n  private refreshInterval: NodeJS.Timeout | undefined = undefined\n  private metrics: MetricsClient\n\n  /**\n   * Easy access to the `Gate` enum for consumers of this class, so they don't\n   * need to import it separately.\n   */\n  Gate = Gate\n\n  constructor(\n    private config: {\n      growthBookApiHost?: string\n      growthBookClientKey?: string\n      eventProxyTrackingEndpoint?: string\n    },\n  ) {\n    this.metrics = new MetricsClient({\n      trackingEndpoint: config.eventProxyTrackingEndpoint,\n    })\n  }\n\n  async start() {\n    if (!this.config.growthBookApiHost || !this.config.growthBookClientKey) {\n      featureGatesLogger.info(\n        {},\n        'feature gates not configured, skipping initialization',\n      )\n      return\n    }\n\n    try {\n      this.client = new GrowthBookClient({\n        apiHost: this.config.growthBookApiHost,\n        clientKey: this.config.growthBookClientKey,\n        onFeatureUsage: (feature, result, userContext) => {\n          if (IGNORE_METRICS_FOR_GATES.has(feature as Gate)) return\n\n          this.metrics.track(\n            'feature:viewed',\n            {\n              featureId: feature,\n              featureResultValue: result.value,\n              experimentId: result.experiment?.key,\n              variationId: result.experimentResult?.key,\n            },\n            parsedUserContextToTrackingMetadata(\n              extractUserContextFromGrowthbookUserContext(userContext),\n            ),\n          )\n        },\n        trackingCallback: (experiment, result, userContext) => {\n          /**\n           * Experiments are only fired in a feature gate has an Experiment\n           * attached in Growthbook. Howerver, we want to be extra sure that a\n           * misconfigured experiment doesn't result in a huge increase in events, so we\n           * protect this here.\n           */\n          if (\n            result.featureId &&\n            IGNORE_METRICS_FOR_GATES.has(result.featureId as Gate)\n          )\n            return\n\n          this.metrics.track(\n            'experiment:viewed',\n            {\n              experimentId: experiment.key,\n              variationId: result.key,\n            },\n            parsedUserContextToTrackingMetadata(\n              extractUserContextFromGrowthbookUserContext(userContext),\n            ),\n          )\n        },\n      })\n\n      const { source, error } = await this.client.init({\n        timeout: FETCH_TIMEOUT,\n      })\n\n      /**\n       * This does not necessarily mean that the client completely failed,\n       * since it could just be that the request timed out. It may succeed\n       * after the timeout, or later during refreshes.\n       *\n       * @see https://docs.growthbook.io/lib/node#error-handling\n       */\n      if (error) {\n        featureGatesLogger.error(\n          { err: error, source },\n          'Client failed to initialize normally',\n        )\n      }\n\n      /**\n       * Set up periodic refresh of feature definitions\n       *\n       * @see https://docs.growthbook.io/lib/node#refreshing-features\n       */\n      this.refreshInterval = setInterval(async () => {\n        try {\n          await this.client?.refreshFeatures({\n            timeout: FETCH_TIMEOUT,\n          })\n        } catch (err) {\n          featureGatesLogger.error({ err }, 'Failed to refresh features')\n        }\n      }, REFETCH_INTERVAL)\n\n      /* Ready or not, here we come */\n      this.ready = true\n    } catch (err) {\n      featureGatesLogger.error({ err }, 'Client initialization failed')\n    }\n  }\n\n  destroy() {\n    if (this.ready) {\n      this.ready = false\n      if (this.refreshInterval) {\n        clearInterval(this.refreshInterval)\n      }\n    }\n    this.metrics.stop()\n  }\n\n  /**\n   * Evaluate multiple feature gates for a given user, returning a map of gate\n   * ID to boolean result.\n   */\n  private checkGates(\n    gates: Gate[],\n    userContext: UserContext,\n  ): CheckedFeatureGatesMap {\n    const gb = this.client\n    const attributes = normalizeUserContext(userContext)\n    if (!gb || !this.ready) return new Map(gates.map((g) => [g, false]))\n    return new Map(gates.map((g) => [g, gb.isOn(g, { attributes })]))\n  }\n\n  scope(scopedUserContext: UserContext): ScopedFeatureGatesClient {\n    /*\n     * Create initial deviceId and sessionId values for the scoped client, to\n     * be used throughout this request lifecycle.\n     */\n    const base = normalizeUserContext(scopedUserContext)\n    return {\n      Gate: this.Gate,\n      checkGates: (\n        gates: Gate[],\n        userContextOverrides?: Pick<UserContext, 'did'>,\n      ) => {\n        /*\n         * Merge the base user context with any overrides provided at check time. This\n         * allows us to set a base context for the request, but also override or add\n         * properties for specific gate checks if needed.\n         */\n        const userContext = mergeUserContexts(base, userContextOverrides)\n        return this.checkGates(gates, userContext)\n      },\n      checkGate: (gate: Gate, userContextOverrides?: UserContext) => {\n        const gatesMap = this.checkGates([gate], userContextOverrides || {})\n        return gatesMap.get(gate) || false\n      },\n    }\n  }\n\n  /**\n   * Parse properties available in XRPC handlers to `UserContext`. The returned\n   * proeprties are used as GrowthBook `attributes` as well as the metadata\n   * payload for our analytics events. This ensures that the same user properties\n   * are used for both feature gate targeting and analytics.\n   */\n  parseUserContextFromHandler({\n    viewer,\n    req,\n  }: {\n    /**\n     * The user's DID\n     */\n    viewer: string | null\n    /**\n     * The express request object, used to extract analytics headers for the user context\n     */\n    req: express.Request\n  }): UserContext {\n    const deviceId = req.header(ANALYTICS_HEADER_DEVICE_ID)\n    const sessionId = req.header(ANALYTICS_HEADER_SESSION_ID)\n\n    return normalizeUserContext({\n      did: viewer,\n      deviceId,\n      sessionId,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/metrics.test.ts",
    "content": "/// <reference types=\"jest\" />\nimport { featureGatesLogger } from '../logger'\nimport { MetricsClient } from './metrics'\n\njest.mock('../logger', () => ({\n  featureGatesLogger: {\n    error: jest.fn(),\n  },\n}))\n\ntype TestEvents = {\n  click: { button: string }\n  view: { screen: string }\n}\n\n// Helper to flush promises and timers\nconst flushPromises = () => new Promise((r) => setImmediate(r))\n\ndescribe('MetricsClient', () => {\n  let fetchMock: jest.Mock\n  let fetchRequests: { body: any }[]\n  let client: MetricsClient<TestEvents>\n\n  beforeEach(() => {\n    jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] })\n    fetchRequests = []\n    fetchMock = jest.fn().mockImplementation(async (_url, options) => {\n      const body = JSON.parse(options.body)\n      fetchRequests.push({ body })\n      return { ok: true, status: 200, text: async () => '' }\n    })\n    global.fetch = fetchMock\n  })\n\n  afterEach(() => {\n    client?.stop()\n    jest.useRealTimers()\n    jest.clearAllMocks()\n  })\n\n  it('flushes events on interval', async () => {\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n    client.track('click', { button: 'submit' })\n    client.track('view', { screen: 'home' })\n\n    expect(fetchRequests).toHaveLength(0)\n\n    // Advance past the 10 second interval\n    jest.advanceTimersByTime(10_000)\n    await flushPromises()\n\n    expect(fetchRequests).toHaveLength(1)\n    expect(fetchRequests[0].body.events).toHaveLength(2)\n    expect(fetchRequests[0].body.events[0].event).toBe('click')\n    expect(fetchRequests[0].body.events[1].event).toBe('view')\n  })\n\n  it('flushes when maxBatchSize is exceeded', async () => {\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n    client.maxBatchSize = 5\n\n    // Add events up to maxBatchSize (should not flush yet)\n    for (let i = 0; i < 5; i++) {\n      client.track('click', { button: `btn-${i}` })\n    }\n\n    expect(fetchRequests).toHaveLength(0)\n\n    // One more event should trigger flush (> maxBatchSize)\n    client.track('click', { button: 'btn-trigger' })\n    await flushPromises()\n\n    expect(fetchRequests).toHaveLength(1)\n    expect(fetchRequests[0].body.events).toHaveLength(6)\n  })\n\n  it('logs error on failed request', async () => {\n    fetchMock.mockImplementation(async () => {\n      return {\n        ok: false,\n        status: 500,\n        text: async () => 'Internal Server Error',\n      }\n    })\n\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n    client.track('click', { button: 'submit' })\n\n    // Trigger flush via interval\n    jest.advanceTimersByTime(10_000)\n    await flushPromises()\n\n    expect(fetchMock).toHaveBeenCalledTimes(1)\n    expect(featureGatesLogger.error).toHaveBeenCalledWith(\n      expect.objectContaining({\n        err: expect.any(Error),\n      }),\n      'Failed to send metrics',\n    )\n  })\n\n  it('handles fetch text() error gracefully', async () => {\n    fetchMock.mockImplementation(async () => {\n      return {\n        ok: false,\n        status: 500,\n        text: async () => {\n          throw new Error('Failed to read response')\n        },\n      }\n    })\n\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n    client.track('click', { button: 'submit' })\n\n    // Trigger flush - should not throw\n    jest.advanceTimersByTime(10_000)\n    await flushPromises()\n\n    expect(fetchMock).toHaveBeenCalledTimes(1)\n    expect(featureGatesLogger.error).toHaveBeenCalledWith(\n      expect.objectContaining({\n        err: expect.objectContaining({\n          message: expect.stringContaining('Unknown error'),\n        }),\n      }),\n      'Failed to send metrics',\n    )\n  })\n\n  it('flushes when stop() is called', async () => {\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n    client.track('click', { button: 'submit' })\n\n    expect(fetchRequests).toHaveLength(0)\n\n    // Stop should flush remaining events\n    client.stop()\n    await flushPromises()\n\n    expect(fetchRequests).toHaveLength(1)\n    expect(fetchRequests[0].body.events).toHaveLength(1)\n    expect(fetchRequests[0].body.events[0].event).toBe('click')\n  })\n\n  it('does not send if trackingEndpoint is not configured', async () => {\n    client = new MetricsClient<TestEvents>({})\n    client.track('click', { button: 'submit' })\n\n    // Trigger flush via interval\n    jest.advanceTimersByTime(10_000)\n    await flushPromises()\n\n    expect(fetchMock).not.toHaveBeenCalled()\n  })\n\n  it('start() is idempotent', async () => {\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n\n    // track() calls start() internally\n    client.track('click', { button: 'submit' })\n    client.start()\n    client.start()\n\n    // Advance past interval - should only flush once\n    jest.advanceTimersByTime(10_000)\n    await flushPromises()\n\n    expect(fetchRequests).toHaveLength(1)\n  })\n\n  it('does not flush if queue is empty', async () => {\n    client = new MetricsClient<TestEvents>({\n      trackingEndpoint: 'https://test.metrics.api',\n    })\n    client.start()\n\n    // Advance past interval with empty queue\n    jest.advanceTimersByTime(10_000)\n    await flushPromises()\n\n    expect(fetchMock).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/metrics.ts",
    "content": "import { featureGatesLogger } from '../logger'\n\ntype Events = {\n  'experiment:viewed': {\n    experimentId: string\n    variationId: string\n  }\n  'feature:viewed': {\n    featureId: string\n    featureResultValue: unknown\n    /** Only available if feature has experiment rules applied */\n    experimentId?: string\n    /** Only available if feature has experiment rules applied */\n    variationId?: string\n  }\n}\n\ntype Event<M extends Record<string, any>> = {\n  time: number\n  event: keyof M\n  payload: M[keyof M]\n  metadata: Record<string, any>\n}\n\nexport type Config = {\n  trackingEndpoint?: string\n}\n\nexport class MetricsClient<M extends Record<string, any> = Events> {\n  maxBatchSize = 100\n\n  private started: boolean = false\n  private queue: Event<M>[] = []\n  private flushInterval: NodeJS.Timeout | null = null\n  constructor(private config: Config) {}\n\n  start() {\n    if (this.started) return\n    this.started = true\n    this.flushInterval = setInterval(() => {\n      this.flush()\n    }, 10_000)\n  }\n\n  stop() {\n    if (this.flushInterval) {\n      clearInterval(this.flushInterval)\n      this.flushInterval = null\n    }\n    this.flush()\n  }\n\n  track<E extends keyof M>(\n    event: E,\n    payload: M[E],\n    metadata: Record<string, any> = {},\n  ) {\n    this.start()\n\n    const e = {\n      source: 'appview',\n      time: Date.now(),\n      event,\n      payload,\n      metadata,\n    }\n    this.queue.push(e)\n\n    if (this.queue.length > this.maxBatchSize) {\n      this.flush()\n    }\n  }\n\n  flush() {\n    if (!this.queue.length) return\n    const events = this.queue.splice(0, this.queue.length)\n    this.sendBatch(events)\n  }\n\n  private async sendBatch(events: Event<M>[]) {\n    if (!this.config.trackingEndpoint) return\n\n    try {\n      const res = await fetch(this.config.trackingEndpoint, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ events }),\n        keepalive: true,\n      })\n\n      if (!res.ok) {\n        const errorText = await res.text().catch(() => 'Unknown error')\n        featureGatesLogger.error(\n          { err: new Error(`${res.status} Failed to fetch - ${errorText}`) },\n          'Failed to send metrics',\n        )\n      } else {\n        // Drain response body to allow connection reuse.\n        await res.text().catch(() => {})\n      }\n    } catch (err) {\n      featureGatesLogger.error({ err }, 'Failed to send metrics')\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/types.ts",
    "content": "import { Gate } from './gates'\n\n/**\n * The user context passed to the feature gates client for evaluation and\n * tracking purposes.\n */\nexport type UserContext = {\n  did?: string | null\n  deviceId?: string | null\n  sessionId?: string | null\n}\n\n/**\n * User context that has been normalized to ensure all required properties are\n * present and have fallback values. This is the format that we expect to use\n * for all feature gate evaluations and analytics tracking.\n */\nexport type NormalizedUserContext = {\n  did?: string\n  deviceId: string\n  sessionId: string\n}\n\n/**\n * This loosely matches the metadata we send from the client for analytics\n * events. We want to make sure we have the same properties in both places so\n * that we can correlate feature gate evaluations with analytics events.\n *\n * @see https://github.com/bluesky-social/social-app/blob/76109a58dc7aafccdfbd07a81cbd9925e065d1c0/src/analytics/metadata.ts\n */\nexport type TrackingMetadata = {\n  base: {\n    deviceId: string\n    sessionId: string\n  }\n  session: {\n    did: string | undefined\n  }\n}\n\n/**\n * Pre-evaluated feature gates map, the result of\n * `ctx.FeatureGatesClient.checkGates()`\n */\nexport type CheckedFeatureGatesMap = Map<Gate, boolean>\n\n/**\n * The primary interface for interacting with feature gates in the system. This\n * client is designed to be used in the context of an XRPC handler, where we\n * can parse the user context from the request and then create a scoped client\n * for that request lifecycle. The client provides methods for checking multiple\n * gates at once, as well as checking individual gates with optional user\n * context overrides.\n */\nexport type ScopedFeatureGatesClient = {\n  Gate: typeof Gate\n  checkGates(\n    gates: Gate[],\n    userContextOverrides?: UserContext,\n  ): CheckedFeatureGatesMap\n  checkGate(gate: Gate, userContextOverrides?: UserContext): boolean\n}\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/utils.test.ts",
    "content": "/// <reference types=\"jest\" />\nimport { mergeUserContexts, normalizeUserContext } from './utils'\n\ndescribe('normalizeUserContext', () => {\n  it('defaults', () => {\n    const ctx = normalizeUserContext({})\n    expect(ctx.did).toBeUndefined()\n    expect(ctx.deviceId).toMatch(/^anon-/)\n    expect(ctx.sessionId).toMatch(/^anon-/)\n  })\n\n  it('with did', () => {\n    const ctx = normalizeUserContext({\n      did: 'did:example:123',\n    })\n    expect(ctx.did).toBe('did:example:123')\n    expect(ctx.deviceId).toBe('did:example:123')\n    expect(ctx.sessionId).toMatch(/^anon-/)\n  })\n\n  it('with did and deviceId', () => {\n    const ctx = normalizeUserContext({\n      did: 'did:example:123',\n      deviceId: 'device-456',\n    })\n    expect(ctx.did).toBe('did:example:123')\n    expect(ctx.deviceId).toBe('device-456')\n    expect(ctx.sessionId).toMatch(/^anon-/)\n  })\n\n  it('with only deviceId and sessionId', () => {\n    const ctx = normalizeUserContext({\n      deviceId: 'device-456',\n      sessionId: 'session-789',\n    })\n    expect(ctx.did).toBeUndefined()\n    expect(ctx.deviceId).toBe('device-456')\n    expect(ctx.sessionId).toBe('session-789')\n  })\n})\n\ndescribe('mergeUserContexts', () => {\n  it('anonymous base context, override with did', () => {\n    const base = normalizeUserContext({})\n    const merged = mergeUserContexts(base, { did: 'did:example:123' })\n    expect(merged.did).toBe('did:example:123')\n    expect(merged.deviceId).toBe('did:example:123')\n    expect(merged.sessionId).toBe(base.sessionId)\n  })\n\n  it('base context with did, override with different did', () => {\n    const base = normalizeUserContext({ did: 'did:example:123' })\n    const merged = mergeUserContexts(base, { did: 'did:example:456' })\n    expect(merged.did).toBe('did:example:456')\n    expect(merged.deviceId).toBe('did:example:456')\n    expect(merged.sessionId).toMatch(/^anon-/)\n  })\n\n  it('base context with did, override with same did', () => {\n    const base = normalizeUserContext({ did: 'did:example:123' })\n    const merged = mergeUserContexts(base, { did: 'did:example:123' })\n    expect(merged.did).toBe('did:example:123')\n    expect(merged.deviceId).toBe('did:example:123')\n    expect(merged.sessionId).toBe(base.sessionId)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/src/feature-gates/utils.ts",
    "content": "import crypto from 'node:crypto'\nimport { type UserContext as GrowthBookUserContext } from '@growthbook/growthbook'\nimport { NormalizedUserContext, TrackingMetadata, UserContext } from './types'\n\nexport function normalizeUserContext(\n  userContext: UserContext,\n): NormalizedUserContext {\n  const did = userContext.did ?? undefined\n  let deviceId = userContext.deviceId\n  let sessionId = userContext.sessionId\n\n  if (!deviceId) {\n    /*\n     * If we don't have a deviceId by other means, such as a request header,\n     * fall back to the DID. Our event proxy ensures ordering based on this\n     * deviceId (also called a stableId in the proxy), so if we have a DID, we\n     * want to use it to ensure client and server events are properly ordered.\n     *\n     * Without any better option for identifying the user, we generate a\n     * random deviceId.\n     */\n    deviceId = did ?? `anon-${crypto.randomUUID()}`\n  }\n\n  if (!sessionId) {\n    /*\n     * If we don't have a sessionId by other means, such as a request header,\n     * generate a random sessionId.\n     */\n    sessionId = `anon-${crypto.randomUUID()}`\n  }\n\n  return {\n    did,\n    deviceId,\n    sessionId,\n  }\n}\n\nexport function mergeUserContexts(\n  base: NormalizedUserContext,\n  overrides?: UserContext,\n): NormalizedUserContext {\n  const did = overrides?.did ?? base.did ?? undefined\n  let deviceId = overrides?.deviceId ?? base.deviceId\n  let sessionId = overrides?.sessionId ?? base.sessionId\n\n  let isDifferentDid = false\n\n  if (did && deviceId.startsWith('anon-')) {\n    /*\n     * If we have a DID, but the existing deviceId is anonymous, use the DID as\n     * the deviceId to ensure proper ordering of events in our event proxy.\n     * This matches the logic in `normalizeUserContext` where we fall back to\n     * the DID for the deviceId if we don't have a deviceId from other means.\n     */\n    deviceId = did\n  } else if (did && deviceId !== did) {\n    /*\n     * If we have both a DID and a deviceId, but they don't match, we may be\n     * overriding context to check a feature that is independent of a single\n     * request handler lifecycle.\n     *\n     * Example: a ScopedFeatureGatesClient was created in the root request\n     * handler with a user context that has a DID, but later on in the request\n     * lifecycle we may check a gate using the DID of the author of the image\n     * we're returning as part of the response.\n     */\n    deviceId = did\n    isDifferentDid = true\n  }\n\n  if (isDifferentDid) {\n    /*\n     * If we're merging in a different DID, we should also generate a new\n     * sessionId to avoid mixing events from different users under the same\n     * session.\n     */\n    sessionId = `anon-${crypto.randomUUID()}`\n  }\n\n  return {\n    did,\n    deviceId,\n    sessionId,\n  }\n}\n\n/**\n * Extract the `UserContext` from GrowthBook's own `UserContext`, which we\n * passed into `isOn` as `attributes`.\n */\nexport function extractUserContextFromGrowthbookUserContext(\n  userContext: GrowthBookUserContext,\n): NormalizedUserContext {\n  /*\n   * The values passed to Growthbook already should have been\n   * `NormalizedUserContext`, but for type safety we run them through the\n   * normalizer again to ensure we have all the required properties and\n   * fallbacks in place.\n   */\n  return normalizeUserContext({\n    did: userContext.attributes?.did,\n    deviceId: userContext.attributes?.deviceId,\n    sessionId: userContext.attributes?.sessionId,\n  })\n}\n\n/**\n * Convert the `UserContext` into the `TrackingMetadata` format that we\n * use for our analytics events. This ensures that we have the same user\n * properties as we do for events from our client app.\n */\nexport function parsedUserContextToTrackingMetadata(\n  userContext: NormalizedUserContext,\n): TrackingMetadata {\n  return {\n    base: {\n      deviceId: userContext.deviceId,\n      sessionId: userContext.sessionId,\n    },\n    session: {\n      did: userContext.did ?? undefined,\n    },\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/hydration/actor.ts",
    "content": "import { AppBskyNotificationDeclaration } from '@atproto/api'\nimport { mapDefined } from '@atproto/common'\nimport { DataPlaneClient } from '../data-plane/client'\nimport { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'\nimport { Record as StatusRecord } from '../lexicon/types/app/bsky/actor/status'\nimport { Record as NotificationDeclarationRecord } from '../lexicon/types/app/bsky/notification/declaration'\nimport { Record as ChatDeclarationRecord } from '../lexicon/types/chat/bsky/actor/declaration'\nimport { Record as GermDeclarationRecord } from '../lexicon/types/com/germnetwork/declaration'\nimport { ActivitySubscription, VerificationMeta } from '../proto/bsky_pb'\nimport {\n  HydrationMap,\n  RecordInfo,\n  isActivitySubscriptionEnabled,\n  parseDate,\n  parseRecord,\n  parseString,\n  safeTakedownRef,\n} from './util'\n\ntype AllowActivitySubscriptions = Extract<\n  AppBskyNotificationDeclaration.Record['allowSubscriptions'],\n  'followers' | 'mutuals' | 'none'\n>\n\nexport type Actor = {\n  did: string\n  handle?: string\n  profile?: ProfileRecord\n  profileCid?: string\n  profileTakedownRef?: string\n  sortedAt?: Date\n  indexedAt?: Date\n  takedownRef?: string\n  isLabeler: boolean\n  allowIncomingChatsFrom?: string\n  upstreamStatus?: string\n  createdAt?: Date\n  priorityNotifications: boolean\n  trustedVerifier?: boolean\n  verifications: VerificationHydrationState[]\n  status?: RecordInfo<StatusRecord>\n  germ?: RecordInfo<GermDeclarationRecord>\n  allowActivitySubscriptionsFrom: AllowActivitySubscriptions\n  /**\n   * Debug information for internal development\n   */\n  debug?: {\n    pagerank?: string\n    accountTags?: string[]\n    profileTags?: string[]\n    [key: string]: unknown\n  }\n}\n\nexport type VerificationHydrationState = {\n  issuer: string\n  uri: string\n  handle: string\n  displayName: string\n  createdAt: string\n}\n\nexport type VerificationMetaRequired = Required<VerificationMeta>\n\nexport type Actors = HydrationMap<Actor>\n\nexport type ChatDeclaration = RecordInfo<ChatDeclarationRecord>\nexport type ChatDeclarations = HydrationMap<ChatDeclaration>\n\nexport type GermDeclaration = RecordInfo<GermDeclarationRecord>\nexport type GermDeclarations = HydrationMap<GermDeclaration>\n\nexport type NotificationDeclaration = RecordInfo<NotificationDeclarationRecord>\nexport type NotificationDeclarations = HydrationMap<NotificationDeclaration>\n\nexport type Status = RecordInfo<StatusRecord>\nexport type Statuses = HydrationMap<Status>\n\nexport type ProfileViewerState = {\n  muted?: boolean\n  mutedByList?: string\n  blockedBy?: string\n  blocking?: string\n  blockedByList?: string\n  blockingByList?: string\n  following?: string\n  followedBy?: string\n}\n\nexport type ProfileViewerStates = HydrationMap<ProfileViewerState>\n\ntype ActivitySubscriptionState = {\n  post: boolean\n  reply: boolean\n}\n\nexport type ActivitySubscriptionStates = HydrationMap<\n  ActivitySubscriptionState | undefined\n>\n\ntype KnownFollowersState = {\n  count: number\n  followers: string[]\n}\n\nexport type KnownFollowersStates = HydrationMap<KnownFollowersState | undefined>\n\nexport type ProfileAgg = {\n  followers: number\n  follows: number\n  posts: number\n  lists: number\n  feeds: number\n  starterPacks: number\n}\n\nexport type ProfileAggs = HydrationMap<ProfileAgg>\n\nexport class ActorHydrator {\n  constructor(public dataplane: DataPlaneClient) {}\n\n  async getRepoRevSafe(did: string | null): Promise<string | null> {\n    if (!did) return null\n    try {\n      const res = await this.dataplane.getLatestRev({ actorDid: did })\n      return parseString(res.rev) ?? null\n    } catch {\n      return null\n    }\n  }\n\n  async getDids(\n    handleOrDids: string[],\n    opts?: { lookupUnidirectional?: boolean },\n  ): Promise<(string | undefined)[]> {\n    const handles = handleOrDids.filter((actor) => !actor.startsWith('did:'))\n    const res = handles.length\n      ? await this.dataplane.getDidsByHandles({\n          handles,\n          lookupUnidirectional: opts?.lookupUnidirectional,\n        })\n      : { dids: [] }\n    const didByHandle = handles.reduce(\n      (acc, cur, i) => {\n        const did = res.dids[i]\n        if (did && did.length > 0) {\n          return acc.set(cur, did)\n        }\n        return acc\n      },\n      new Map() as Map<string, string>,\n    )\n    return handleOrDids.map((id) =>\n      id.startsWith('did:') ? id : didByHandle.get(id),\n    )\n  }\n\n  async getDidsDefined(handleOrDids: string[]): Promise<string[]> {\n    const res = await this.getDids(handleOrDids)\n    // @ts-ignore\n    return res.filter((did) => did !== undefined)\n  }\n\n  async getActors(\n    dids: string[],\n    opts: {\n      includeTakedowns?: boolean\n      skipCacheForDids?: string[]\n    } = {},\n  ): Promise<Actors> {\n    const { includeTakedowns = false, skipCacheForDids } = opts\n    if (!dids.length) return new HydrationMap<Actor>()\n    const res = await this.dataplane.getActors({ dids, skipCacheForDids })\n    return dids.reduce((acc, did, i) => {\n      const actor = res.actors[i]\n      const isNoHosted =\n        actor.takenDown ||\n        (actor.upstreamStatus && actor.upstreamStatus !== 'active')\n      if (\n        !actor.exists ||\n        (isNoHosted && !includeTakedowns) ||\n        !!actor.tombstonedAt\n      ) {\n        return acc.set(did, null)\n      }\n\n      const profile = actor.profile?.record\n        ? parseRecord<ProfileRecord>(actor.profile, includeTakedowns)\n        : undefined\n\n      const status = actor.statusRecord\n        ? parseRecord<StatusRecord>(\n            actor.statusRecord,\n            /*\n             * Always true, we filter this out in the `Views.status()`. If we\n             * ever remove that filter, we'll want to reinstate this here.\n             */\n            true,\n          )\n        : undefined\n\n      const germ = actor.germRecord\n        ? parseRecord<GermDeclarationRecord>(actor.germRecord, includeTakedowns)\n        : undefined\n\n      const verifications = mapDefined(\n        Object.entries(actor.verifiedBy),\n        ([actorDid, verificationMeta]) => {\n          if (\n            verificationMeta.handle &&\n            verificationMeta.rkey &&\n            verificationMeta.sortedAt\n          ) {\n            return {\n              issuer: actorDid,\n              uri: `at://${actorDid}/app.bsky.graph.verification/${verificationMeta.rkey}`,\n              handle: verificationMeta.handle,\n              displayName: verificationMeta.displayName,\n              createdAt:\n                parseDate(verificationMeta.sortedAt)?.toISOString() ??\n                new Date(0).toISOString(),\n            }\n          }\n          // Filter out the verification meta that doesn't contain all info.\n          return undefined\n        },\n      )\n\n      const allowActivitySubscriptionsFrom = (\n        val: string,\n      ): AllowActivitySubscriptions => {\n        switch (val) {\n          case 'followers':\n          case 'mutuals':\n          case 'none':\n            return val\n          default:\n            // The dataplane should set the default of \"FOLLOWERS\". Just in case.\n            return 'followers'\n        }\n      }\n\n      const debug = {\n        pagerank: actor.pagerank ? actor.pagerank.toString() : undefined,\n        accountTags: actor.tags,\n        profileTags: actor.profileTags,\n      }\n\n      return acc.set(did, {\n        did,\n        handle: parseString(actor.handle),\n        profile: profile?.record,\n        profileCid: profile?.cid,\n        profileTakedownRef: profile?.takedownRef,\n        sortedAt: profile?.sortedAt,\n        indexedAt: profile?.indexedAt,\n        takedownRef: safeTakedownRef(actor),\n        isLabeler: actor.labeler ?? false,\n        allowIncomingChatsFrom: actor.allowIncomingChatsFrom || undefined,\n        upstreamStatus: actor.upstreamStatus || undefined,\n        createdAt: parseDate(actor.createdAt),\n        priorityNotifications: actor.priorityNotifications,\n        trustedVerifier: actor.trustedVerifier,\n        verifications,\n        status: status,\n        germ: germ,\n        allowActivitySubscriptionsFrom: allowActivitySubscriptionsFrom(\n          actor.allowActivitySubscriptionsFrom,\n        ),\n        debug,\n      })\n    }, new HydrationMap<Actor>())\n  }\n\n  async getChatDeclarations(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<ChatDeclarations> {\n    if (!uris.length) return new HydrationMap<ChatDeclaration>()\n    const res = await this.dataplane.getActorChatDeclarationRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<ChatDeclarationRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<ChatDeclaration>())\n  }\n\n  async getGermDeclarations(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<GermDeclarations> {\n    if (!uris.length) return new HydrationMap<GermDeclaration>()\n    const res = await this.dataplane.getGermDeclarationRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<GermDeclarationRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<GermDeclaration>())\n  }\n\n  async getNotificationDeclarations(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<NotificationDeclarations> {\n    if (!uris.length) return new HydrationMap<NotificationDeclaration>()\n    const res = await this.dataplane.getNotificationDeclarationRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<NotificationDeclarationRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<NotificationDeclaration>())\n  }\n\n  async getStatus(uris: string[], includeTakedowns = false): Promise<Statuses> {\n    if (!uris.length) return new HydrationMap<Status>()\n    const res = await this.dataplane.getStatusRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<StatusRecord>(res.records[i], includeTakedowns)\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Status>())\n  }\n\n  // \"naive\" because this method does not verify the existence of the list itself\n  // a later check in the main hydrator will remove list uris that have been deleted or\n  // repurposed to \"curate lists\"\n  async getProfileViewerStatesNaive(\n    dids: string[],\n    viewer: string,\n  ): Promise<ProfileViewerStates> {\n    if (!dids.length) return new HydrationMap<ProfileViewerState>()\n    const res = await this.dataplane.getRelationships({\n      actorDid: viewer,\n      targetDids: dids,\n    })\n\n    return dids.reduce((acc, did, i) => {\n      const rels = res.relationships[i]\n      if (viewer === did) {\n        // ignore self-follows, self-mutes, self-blocks, self-activity-subscriptions\n        return acc.set(did, {})\n      }\n      return acc.set(did, {\n        muted: rels.muted ?? false,\n        mutedByList: parseString(rels.mutedByList),\n        blockedBy: parseString(rels.blockedBy),\n        blocking: parseString(rels.blocking),\n        blockedByList: parseString(rels.blockedByList),\n        blockingByList: parseString(rels.blockingByList),\n        following: parseString(rels.following),\n        followedBy: parseString(rels.followedBy),\n      })\n    }, new HydrationMap<ProfileViewerState>())\n  }\n\n  async getKnownFollowers(\n    dids: string[],\n    viewer: string | null,\n  ): Promise<KnownFollowersStates> {\n    if (!viewer) return new HydrationMap<KnownFollowersState | undefined>()\n    const { results: knownFollowersResults } = await this.dataplane\n      .getFollowsFollowing(\n        {\n          actorDid: viewer,\n          targetDids: dids,\n        },\n        {\n          signal: AbortSignal.timeout(100),\n        },\n      )\n      .catch(() => ({ results: [] }))\n    return dids.reduce((acc, did, i) => {\n      const result = knownFollowersResults[i]?.dids\n      return acc.set(\n        did,\n        result && result.length > 0\n          ? {\n              count: result.length,\n              followers: result.slice(0, 5),\n            }\n          : undefined,\n      )\n    }, new HydrationMap<KnownFollowersState | undefined>())\n  }\n\n  async getActivitySubscriptions(\n    dids: string[],\n    viewer: string | null,\n  ): Promise<ActivitySubscriptionStates> {\n    if (!viewer) {\n      return new HydrationMap<ActivitySubscriptionState | undefined>()\n    }\n\n    const activitySubscription = (val: ActivitySubscription | undefined) => {\n      if (!val) return undefined\n\n      const result = {\n        post: !!val.post,\n        reply: !!val.reply,\n      }\n      if (!isActivitySubscriptionEnabled(result)) return undefined\n\n      return result\n    }\n\n    const { subscriptions } = await this.dataplane\n      .getActivitySubscriptionsByActorAndSubjects(\n        { actorDid: viewer, subjectDids: dids },\n        { signal: AbortSignal.timeout(100) },\n      )\n      .catch(() => ({ subscriptions: [] }))\n\n    return dids.reduce((acc, did, i) => {\n      return acc.set(did, activitySubscription(subscriptions[i]))\n    }, new HydrationMap<ActivitySubscriptionState | undefined>())\n  }\n\n  async getProfileAggregates(dids: string[]): Promise<ProfileAggs> {\n    if (!dids.length) return new HydrationMap<ProfileAgg>()\n    const counts = await this.dataplane.getCountsForUsers({ dids })\n    return dids.reduce((acc, did, i) => {\n      return acc.set(did, {\n        followers: counts.followers[i] ?? 0,\n        follows: counts.following[i] ?? 0,\n        posts: counts.posts[i] ?? 0,\n        lists: counts.lists[i] ?? 0,\n        feeds: counts.feeds[i] ?? 0,\n        starterPacks: counts.starterPacks[i] ?? 0,\n      })\n    }, new HydrationMap<ProfileAgg>())\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/hydration/feed.ts",
    "content": "import { dedupeStrs } from '@atproto/common'\nimport { DataPlaneClient } from '../data-plane/client'\nimport { Record as FeedGenRecord } from '../lexicon/types/app/bsky/feed/generator'\nimport { Record as LikeRecord } from '../lexicon/types/app/bsky/feed/like'\nimport { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post'\nimport { Record as PostgateRecord } from '../lexicon/types/app/bsky/feed/postgate'\nimport { Record as RepostRecord } from '../lexicon/types/app/bsky/feed/repost'\nimport { Record as ThreadgateRecord } from '../lexicon/types/app/bsky/feed/threadgate'\nimport {\n  postUriToPostgateUri,\n  postUriToThreadgateUri,\n  uriToDid as didFromUri,\n} from '../util/uris'\nimport {\n  HydrationMap,\n  ItemRef,\n  RecordInfo,\n  parseRecord,\n  parseString,\n  split,\n} from './util'\n\nexport type Post = RecordInfo<PostRecord> & {\n  violatesThreadGate: boolean\n  violatesEmbeddingRules: boolean\n  hasThreadGate: boolean\n  hasPostGate: boolean\n  tags: Set<string>\n  /**\n   * Debug information for internal development\n   */\n  debug?: {\n    tags?: string[]\n    [key: string]: unknown\n  }\n}\nexport type Posts = HydrationMap<Post>\n\nexport type PostViewerState = {\n  like?: string\n  repost?: string\n  bookmarked?: boolean\n  threadMuted?: boolean\n}\n\nexport type PostViewerStates = HydrationMap<PostViewerState>\n\nexport type ThreadContext = {\n  // Whether the root author has liked the post.\n  like?: string\n}\n\nexport type ThreadContexts = HydrationMap<ThreadContext>\n\nexport type PostAgg = {\n  likes: number\n  replies: number\n  reposts: number\n  quotes: number\n  bookmarks: number\n}\n\nexport type PostAggs = HydrationMap<PostAgg>\n\nexport type Like = RecordInfo<LikeRecord>\nexport type Likes = HydrationMap<Like>\n\nexport type Repost = RecordInfo<RepostRecord>\nexport type Reposts = HydrationMap<Repost>\n\nexport type FeedGenAgg = {\n  likes: number\n}\n\nexport type FeedGenAggs = HydrationMap<FeedGenAgg>\n\nexport type FeedGen = RecordInfo<FeedGenRecord>\nexport type FeedGens = HydrationMap<FeedGen>\n\nexport type FeedGenViewerState = {\n  like?: string\n}\n\nexport type FeedGenViewerStates = HydrationMap<FeedGenViewerState>\n\nexport type Threadgate = RecordInfo<ThreadgateRecord>\nexport type Threadgates = HydrationMap<Threadgate>\nexport type Postgate = RecordInfo<PostgateRecord>\nexport type Postgates = HydrationMap<Postgate>\n\nexport type ThreadRef = ItemRef & { threadRoot: string }\n\n// @NOTE the feed item types in the protos for author feeds and timelines\n// technically have additional fields, not supported by the mock dataplane.\nexport type FeedItem = {\n  post: ItemRef\n  repost?: ItemRef\n  /**\n   * If true, overrides the `reason` with `app.bsky.feed.defs#reasonPin`. Used\n   * only in author feeds.\n   */\n  authorPinned?: boolean\n}\n\nexport type GetPostsHydrationOptions = {\n  processDynamicTagsForView?: 'thread' | 'search'\n}\n\nexport class FeedHydrator {\n  constructor(public dataplane: DataPlaneClient) {}\n\n  async getPosts(\n    uris: string[],\n    includeTakedowns = false,\n    given = new HydrationMap<Post>(),\n    viewer?: string | null,\n    options: GetPostsHydrationOptions = {},\n  ): Promise<Posts> {\n    const [have, need] = split(uris, (uri) => given.has(uri))\n    const base = have.reduce(\n      (acc, uri) => acc.set(uri, given.get(uri) ?? null),\n      new HydrationMap<Post>(),\n    )\n    if (!need.length) return base\n    const res = await this.dataplane.getPostRecords(\n      options.processDynamicTagsForView\n        ? {\n            uris: need,\n            viewerDid: viewer ?? undefined,\n            processDynamicTagsForView: options.processDynamicTagsForView,\n          }\n        : {\n            uris: need,\n          },\n    )\n    return need.reduce((acc, uri, i) => {\n      const record = parseRecord<PostRecord>(res.records[i], includeTakedowns)\n      const violatesThreadGate = res.meta[i].violatesThreadGate\n      const violatesEmbeddingRules = res.meta[i].violatesEmbeddingRules\n      const hasThreadGate = res.meta[i].hasThreadGate\n      const hasPostGate = res.meta[i].hasPostGate\n      const tags = new Set<string>(res.records[i].tags ?? [])\n      const debug = { tags: Array.from(tags) }\n      return acc.set(\n        uri,\n        record\n          ? {\n              ...record,\n              violatesThreadGate,\n              violatesEmbeddingRules,\n              hasThreadGate,\n              hasPostGate,\n              tags,\n              debug,\n            }\n          : null,\n      )\n    }, base)\n  }\n\n  async getPostViewerStates(\n    refs: ThreadRef[],\n    viewer: string,\n  ): Promise<PostViewerStates> {\n    if (!refs.length) return new HydrationMap<PostViewerState>()\n    const threadRoots = refs.map((r) => r.threadRoot)\n    const [likes, reposts, bookmarks, threadMutesMap] = await Promise.all([\n      this.dataplane.getLikesByActorAndSubjects({\n        actorDid: viewer,\n        refs,\n      }),\n      this.dataplane.getRepostsByActorAndSubjects({\n        actorDid: viewer,\n        refs,\n      }),\n      this.dataplane.getBookmarksByActorAndSubjects({\n        actorDid: viewer,\n        uris: refs.map((r) => r.uri),\n      }),\n      this.getThreadMutes(threadRoots, viewer),\n    ])\n    return refs.reduce((acc, { uri, threadRoot }, i) => {\n      return acc.set(uri, {\n        like: parseString(likes.uris[i]),\n        repost: parseString(reposts.uris[i]),\n        // @NOTE: The dataplane contract is that the array position will be present,\n        // but the optional chaining is to ensure it works regardless of the dataplane being update to provide the data.\n        bookmarked: !!bookmarks.bookmarks.at(i)?.ref?.key,\n        threadMuted: threadMutesMap.get(threadRoot) ?? false,\n      })\n    }, new HydrationMap<PostViewerState>())\n  }\n\n  private async getThreadMutes(\n    threadRoots: string[],\n    viewer: string,\n  ): Promise<Map<string, boolean>> {\n    const deduped = dedupeStrs(threadRoots)\n    const threadMutes = await this.dataplane.getThreadMutesOnSubjects({\n      actorDid: viewer,\n      threadRoots: deduped,\n    })\n    return deduped.reduce((acc, cur, i) => {\n      return acc.set(cur, threadMutes.muted[i] ?? false)\n    }, new Map<string, boolean>())\n  }\n\n  async getThreadContexts(refs: ThreadRef[]): Promise<ThreadContexts> {\n    if (!refs.length) return new HydrationMap<ThreadContext>()\n\n    const refsByRootAuthor = refs.reduce((acc, ref) => {\n      const { threadRoot } = ref\n      const rootAuthor = didFromUri(threadRoot)\n      const existingValue = acc.get(rootAuthor) ?? []\n      return acc.set(rootAuthor, [...existingValue, ref])\n    }, new Map<string, ThreadRef[]>())\n    const refsByRootAuthorEntries = Array.from(refsByRootAuthor.entries())\n\n    const likesPromises = refsByRootAuthorEntries.map(\n      ([rootAuthor, refsForAuthor]) =>\n        this.dataplane.getLikesByActorAndSubjects({\n          actorDid: rootAuthor,\n          refs: refsForAuthor.map(({ uri, cid }) => ({ uri, cid })),\n        }),\n    )\n\n    const rootAuthorsLikes = await Promise.all(likesPromises)\n\n    const likesByUri = refsByRootAuthorEntries.reduce(\n      (acc, [_rootAuthor, refsForAuthor], i) => {\n        const likesForRootAuthor = rootAuthorsLikes[i]\n        refsForAuthor.forEach(({ uri }, j) => {\n          acc.set(uri, likesForRootAuthor.uris[j])\n        })\n        return acc\n      },\n      new Map<string, string>(),\n    )\n\n    return refs.reduce((acc, { uri }) => {\n      return acc.set(uri, {\n        like: parseString(likesByUri.get(uri)),\n      })\n    }, new HydrationMap<ThreadContext>())\n  }\n\n  async getPostAggregates(\n    refs: ItemRef[],\n    viewer: string | null,\n  ): Promise<PostAggs> {\n    if (!refs.length) return new HydrationMap<PostAgg>()\n    const counts = await this.dataplane.getInteractionCounts({\n      refs,\n      skipCacheForDids: viewer ? [viewer] : undefined,\n    })\n    return refs.reduce((acc, { uri }, i) => {\n      return acc.set(uri, {\n        likes: counts.likes[i] ?? 0,\n        reposts: counts.reposts[i] ?? 0,\n        replies: counts.replies[i] ?? 0,\n        quotes: counts.quotes[i] ?? 0,\n        bookmarks: counts.bookmarks[i] ?? 0,\n      })\n    }, new HydrationMap<PostAgg>())\n  }\n\n  async getFeedGens(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<FeedGens> {\n    if (!uris.length) return new HydrationMap<FeedGen>()\n    const res = await this.dataplane.getFeedGeneratorRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<FeedGenRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<FeedGen>())\n  }\n\n  async getFeedGenViewerStates(\n    uris: string[],\n    viewer: string,\n  ): Promise<FeedGenViewerStates> {\n    if (!uris.length) return new HydrationMap<FeedGenViewerState>()\n    const likes = await this.dataplane.getLikesByActorAndSubjects({\n      actorDid: viewer,\n      refs: uris.map((uri) => ({ uri })),\n    })\n    return uris.reduce((acc, uri, i) => {\n      return acc.set(uri, {\n        like: parseString(likes.uris[i]),\n      })\n    }, new HydrationMap<FeedGenViewerState>())\n  }\n\n  async getFeedGenAggregates(\n    refs: ItemRef[],\n    viewer: string | null,\n  ): Promise<FeedGenAggs> {\n    if (!refs.length) return new HydrationMap<FeedGenAgg>()\n    const counts = await this.dataplane.getInteractionCounts({\n      refs,\n      skipCacheForDids: viewer ? [viewer] : undefined,\n    })\n    return refs.reduce((acc, { uri }, i) => {\n      return acc.set(uri, {\n        likes: counts.likes[i] ?? 0,\n      })\n    }, new HydrationMap<FeedGenAgg>())\n  }\n\n  async getThreadgatesForPosts(\n    postUris: string[],\n    includeTakedowns = false,\n  ): Promise<Threadgates> {\n    if (!postUris.length) return new HydrationMap<Threadgate>()\n    const uris = postUris.map(postUriToThreadgateUri)\n    return this.getThreadgateRecords(uris, includeTakedowns)\n  }\n\n  async getThreadgateRecords(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<Threadgates> {\n    const res = await this.dataplane.getThreadGateRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<ThreadgateRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Threadgate>())\n  }\n\n  async getPostgatesForPosts(\n    postUris: string[],\n    includeTakedowns = false,\n  ): Promise<Postgates> {\n    if (!postUris.length) return new HydrationMap<Postgate>()\n    const uris = postUris.map(postUriToPostgateUri)\n    return this.getPostgateRecords(uris, includeTakedowns)\n  }\n\n  async getPostgateRecords(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<Postgates> {\n    const res = await this.dataplane.getPostgateRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<PostgateRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Postgate>())\n  }\n\n  async getLikes(uris: string[], includeTakedowns = false): Promise<Likes> {\n    if (!uris.length) return new HydrationMap<Like>()\n    const res = await this.dataplane.getLikeRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<LikeRecord>(res.records[i], includeTakedowns)\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Like>())\n  }\n\n  async getReposts(uris: string[], includeTakedowns = false): Promise<Reposts> {\n    if (!uris.length) return new HydrationMap<Repost>()\n    const res = await this.dataplane.getRepostRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<RepostRecord>(res.records[i], includeTakedowns)\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Repost>())\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/hydration/graph.ts",
    "content": "import { DataPlaneClient } from '../data-plane/client'\nimport { Record as BlockRecord } from '../lexicon/types/app/bsky/graph/block'\nimport { Record as FollowRecord } from '../lexicon/types/app/bsky/graph/follow'\nimport { Record as ListRecord } from '../lexicon/types/app/bsky/graph/list'\nimport { Record as ListItemRecord } from '../lexicon/types/app/bsky/graph/listitem'\nimport { Record as StarterPackRecord } from '../lexicon/types/app/bsky/graph/starterpack'\nimport { Record as VerificationRecord } from '../lexicon/types/app/bsky/graph/verification'\nimport { FollowInfo } from '../proto/bsky_pb'\nimport { HydrationMap, ItemRef, RecordInfo, parseRecord } from './util'\n\nexport type List = RecordInfo<ListRecord>\nexport type Lists = HydrationMap<List>\n\nexport type ListItem = RecordInfo<ListItemRecord>\nexport type ListItems = HydrationMap<ListItem>\n\nexport type ListViewerState = {\n  viewerMuted?: string\n  viewerListBlockUri?: string\n  viewerInList?: string\n}\n\nexport type ListViewerStates = HydrationMap<ListViewerState>\n\nexport type ListMembershipState = {\n  actorListItemUri?: string\n}\n// list uri => actor did => state\nexport type ListMembershipStates = HydrationMap<\n  HydrationMap<ListMembershipState>\n>\n\nexport type Follow = RecordInfo<FollowRecord>\nexport type Follows = HydrationMap<Follow>\n\nexport type Block = RecordInfo<BlockRecord>\n\nexport type StarterPack = RecordInfo<StarterPackRecord>\nexport type StarterPacks = HydrationMap<StarterPack>\n\nexport type Verification = RecordInfo<VerificationRecord>\nexport type Verifications = HydrationMap<Verification>\n\nexport type StarterPackAgg = {\n  joinedWeek: number\n  joinedAllTime: number\n  listItemSampleUris?: string[] // gets set during starter pack hydration (not for basic view)\n}\n\nexport type StarterPackAggs = HydrationMap<StarterPackAgg>\n\nexport type ListAgg = {\n  listItems: number\n}\n\nexport type ListAggs = HydrationMap<ListAgg>\n\nexport type RelationshipPair = [didA: string, didB: string]\n\nconst dedupePairs = (pairs: RelationshipPair[]): RelationshipPair[] => {\n  const deduped = pairs.reduce((acc, pair) => {\n    return acc.set(Blocks.key(...pair), pair)\n  }, new Map<string, RelationshipPair>())\n  return [...deduped.values()]\n}\n\nexport class Blocks {\n  _blocks: Map<string, BlockEntry> = new Map() // did:a,did:b -> block\n  constructor() {}\n\n  static key(didA: string, didB: string): string {\n    return [didA, didB].sort().join(',')\n  }\n\n  set(didA: string, didB: string, block: BlockEntry): Blocks {\n    const key = Blocks.key(didA, didB)\n    this._blocks.set(key, block)\n    return this\n  }\n\n  get(didA: string, didB: string): BlockEntry | null {\n    if (didA === didB) return null // ignore self-blocks\n    const key = Blocks.key(didA, didB)\n    return this._blocks.get(key) ?? null\n  }\n\n  merge(blocks: Blocks): Blocks {\n    blocks._blocks.forEach((block, key) => {\n      this._blocks.set(key, block)\n    })\n    return this\n  }\n}\n\n// No \"blocking\" vs. \"blocked\" directionality: only suitable for bidirectional block checks\nexport type BlockEntry = {\n  blockUri: string | undefined\n  blockListUri: string | undefined\n}\n\nexport class GraphHydrator {\n  constructor(public dataplane: DataPlaneClient) {}\n\n  async getLists(uris: string[], includeTakedowns = false): Promise<Lists> {\n    if (!uris.length) return new HydrationMap<List>()\n    const res = await this.dataplane.getListRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<ListRecord>(res.records[i], includeTakedowns)\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<List>())\n  }\n\n  async getListItems(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<ListItems> {\n    if (!uris.length) return new HydrationMap<ListItem>()\n    const res = await this.dataplane.getListItemRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<ListItemRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<ListItem>())\n  }\n\n  async getListViewerStates(\n    uris: string[],\n    viewer: string,\n  ): Promise<ListViewerStates> {\n    if (!uris.length) return new HydrationMap<ListViewerState>()\n    const mutesAndBlocks = await Promise.all(\n      uris.map((uri) => this.getMutesAndBlocks(uri, viewer)),\n    )\n    const listMemberships = await this.dataplane.getListMembership({\n      actorDid: viewer,\n      listUris: uris,\n    })\n    return uris.reduce((acc, uri, i) => {\n      return acc.set(uri, {\n        viewerMuted: mutesAndBlocks[i].muted ? uri : undefined,\n        viewerListBlockUri: mutesAndBlocks[i].listBlockUri || undefined,\n        viewerInList: listMemberships.listitemUris[i],\n      })\n    }, new HydrationMap<ListViewerState>())\n  }\n\n  private async getMutesAndBlocks(uri: string, viewer: string) {\n    const [muted, listBlockUri] = await Promise.all([\n      this.dataplane.getMutelistSubscription({\n        actorDid: viewer,\n        listUri: uri,\n      }),\n      this.dataplane.getBlocklistSubscription({\n        actorDid: viewer,\n        listUri: uri,\n      }),\n    ])\n    return {\n      muted: muted.subscribed,\n      listBlockUri: listBlockUri.listblockUri,\n    }\n  }\n\n  async getBidirectionalBlocks(pairs: RelationshipPair[]): Promise<Blocks> {\n    if (!pairs.length) return new Blocks()\n    const deduped = dedupePairs(pairs).map(([a, b]) => ({ a, b }))\n    const res = await this.dataplane.getBlockExistence({ pairs: deduped })\n    const blocks = new Blocks()\n    for (let i = 0; i < deduped.length; i++) {\n      const pair = deduped[i]\n      const block = res.blocks[i]\n      blocks.set(pair.a, pair.b, {\n        blockUri: block.blockedBy || block.blocking || undefined,\n        blockListUri: block.blockedByList || block.blockingByList || undefined,\n      })\n    }\n    return blocks\n  }\n\n  async getFollows(uris: string[], includeTakedowns = false): Promise<Follows> {\n    if (!uris.length) return new HydrationMap<Follow>()\n    const res = await this.dataplane.getFollowRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<FollowRecord>(res.records[i], includeTakedowns)\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Follow>())\n  }\n\n  async getVerifications(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<Verifications> {\n    if (!uris.length) return new HydrationMap<Verification>()\n    const res = await this.dataplane.getVerificationRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<VerificationRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Verification>())\n  }\n\n  async getBlocks(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<HydrationMap<Block>> {\n    if (!uris.length) return new HydrationMap<Block>()\n    const res = await this.dataplane.getBlockRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<BlockRecord>(res.records[i], includeTakedowns)\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<Block>())\n  }\n\n  async getActorFollows(input: {\n    did: string\n    cursor?: string\n    limit?: number\n  }): Promise<{ follows: FollowInfo[]; cursor: string }> {\n    const { did, cursor, limit } = input\n    const res = await this.dataplane.getFollows({\n      actorDid: did,\n      cursor,\n      limit,\n    })\n    return { follows: res.follows, cursor: res.cursor }\n  }\n\n  async getActorFollowers(input: {\n    did: string\n    cursor?: string\n    limit?: number\n  }): Promise<{ followers: FollowInfo[]; cursor: string }> {\n    const { did, cursor, limit } = input\n    const res = await this.dataplane.getFollowers({\n      actorDid: did,\n      cursor,\n      limit,\n    })\n    return { followers: res.followers, cursor: res.cursor }\n  }\n\n  async getStarterPacks(\n    uris: string[],\n    includeTakedowns = false,\n  ): Promise<StarterPacks> {\n    if (!uris.length) return new HydrationMap<StarterPack>()\n    const res = await this.dataplane.getStarterPackRecords({ uris })\n    return uris.reduce((acc, uri, i) => {\n      const record = parseRecord<StarterPackRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(uri, record ?? null)\n    }, new HydrationMap<StarterPack>())\n  }\n\n  async getStarterPackAggregates(refs: ItemRef[]) {\n    if (!refs.length) return new HydrationMap<StarterPackAgg>()\n    const counts = await this.dataplane.getStarterPackCounts({ refs })\n    return refs.reduce((acc, { uri }, i) => {\n      return acc.set(uri, {\n        joinedWeek: counts.joinedWeek[i] ?? 0,\n        joinedAllTime: counts.joinedAllTime[i] ?? 0,\n      })\n    }, new HydrationMap<StarterPackAgg>())\n  }\n\n  async getListAggregates(refs: ItemRef[]) {\n    if (!refs.length) return new HydrationMap<ListAgg>()\n    const counts = await this.dataplane.getListCounts({ refs })\n    return refs.reduce((acc, { uri }, i) => {\n      return acc.set(uri, {\n        listItems: counts.listItems[i] ?? 0,\n      })\n    }, new HydrationMap<ListAgg>())\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/hydration/hydrator.ts",
    "content": "import assert from 'node:assert'\nimport { mapDefined } from '@atproto/common'\nimport { AtUri } from '@atproto/syntax'\nimport { DataPlaneClient } from '../data-plane/client'\nimport { FeatureGatesClient, ScopedFeatureGatesClient } from '../feature-gates'\nimport { ids } from '../lexicon/lexicons'\nimport { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'\nimport { isMain as isEmbedRecord } from '../lexicon/types/app/bsky/embed/record'\nimport { isMain as isEmbedRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia'\nimport { isListRule as isThreadgateListRule } from '../lexicon/types/app/bsky/feed/threadgate'\nimport { hydrationLogger } from '../logger'\nimport {\n  Bookmark as BookmarkLex,\n  BookmarkInfo,\n  Notification,\n  RecordRef,\n} from '../proto/bsky_pb'\nimport { ParsedLabelers } from '../util'\nimport { uriToDid, uriToDid as didFromUri } from '../util/uris'\nimport {\n  ActivitySubscriptionStates,\n  ActorHydrator,\n  Actors,\n  KnownFollowersStates,\n  ProfileAggs,\n  ProfileViewerState,\n  ProfileViewerStates,\n} from './actor'\nimport {\n  FeedGenAggs,\n  FeedGenViewerStates,\n  FeedGens,\n  FeedHydrator,\n  FeedItem,\n  type GetPostsHydrationOptions,\n  Likes,\n  Post,\n  PostAggs,\n  PostViewerStates,\n  Postgates,\n  Posts,\n  Reposts,\n  ThreadContexts,\n  ThreadRef,\n  Threadgates,\n} from './feed'\nimport {\n  BlockEntry,\n  Follows,\n  GraphHydrator,\n  ListAggs,\n  ListItems,\n  ListMembershipState,\n  ListMembershipStates,\n  ListViewerStates,\n  Lists,\n  RelationshipPair,\n  StarterPackAggs,\n  StarterPacks,\n  Verifications,\n} from './graph'\nimport {\n  LabelHydrator,\n  LabelerAggs,\n  LabelerViewerStates,\n  Labelers,\n  Labels,\n} from './label'\nimport {\n  HydrationMap,\n  ItemRef,\n  RecordInfo,\n  mergeManyMaps,\n  mergeMaps,\n  mergeNestedMaps,\n  parseDate,\n  urisByCollection,\n} from './util'\n\nexport class HydrateCtx {\n  labelers = this.vals.labelers\n  viewer = this.vals.viewer !== null ? serviceRefToDid(this.vals.viewer) : null\n  includeTakedowns = this.vals.includeTakedowns\n  overrideIncludeTakedownsForActor = this.vals.overrideIncludeTakedownsForActor\n  include3pBlocks = this.vals.include3pBlocks\n  includeDebugField = this.vals.includeDebugField\n  features = this.vals.features\n  constructor(private vals: HydrateCtxVals) {}\n  // Convenience with use with dataplane.getActors cache control\n  get skipCacheForViewer() {\n    if (!this.viewer) return\n    return [this.viewer]\n  }\n  copy<V extends Partial<HydrateCtxVals>>(vals?: V): HydrateCtx & V {\n    return new HydrateCtx({ ...this.vals, ...vals }) as HydrateCtx & V\n  }\n}\n\nexport type HydrateCtxVals = {\n  labelers: ParsedLabelers\n  viewer: string | null\n  includeTakedowns?: boolean\n  overrideIncludeTakedownsForActor?: boolean\n  include3pBlocks?: boolean\n  includeDebugField?: boolean\n  features: ScopedFeatureGatesClient\n}\n\nexport type HydrationState = {\n  ctx?: HydrateCtx\n  actors?: Actors\n  profileViewers?: ProfileViewerStates\n  profileAggs?: ProfileAggs\n  posts?: Posts\n  postAggs?: PostAggs\n  postViewers?: PostViewerStates\n  threadContexts?: ThreadContexts\n  postBlocks?: PostBlocks\n  reposts?: Reposts\n  follows?: Follows\n  followBlocks?: FollowBlocks\n  threadgates?: Threadgates\n  postgates?: Postgates\n  lists?: Lists\n  listAggs?: ListAggs\n  listMemberships?: ListMembershipStates\n  listViewers?: ListViewerStates\n  listItems?: ListItems\n  likes?: Likes\n  likeBlocks?: LikeBlocks\n  labels?: Labels\n  feedgens?: FeedGens\n  feedgenViewers?: FeedGenViewerStates\n  feedgenAggs?: FeedGenAggs\n  starterPacks?: StarterPacks\n  starterPackAggs?: StarterPackAggs\n  labelers?: Labelers\n  labelerViewers?: LabelerViewerStates\n  labelerAggs?: LabelerAggs\n  knownFollowers?: KnownFollowersStates\n  activitySubscriptions?: ActivitySubscriptionStates\n  bidirectionalBlocks?: BidirectionalBlocks\n  verifications?: Verifications\n  bookmarks?: Bookmarks\n}\n\nexport type PostBlock = { embed: boolean; parent: boolean; root: boolean }\nexport type PostBlocks = HydrationMap<PostBlock>\ntype PostBlockPairs = {\n  embed?: RelationshipPair\n  parent?: RelationshipPair\n  root?: RelationshipPair\n}\n\nexport type LikeBlock = boolean\nexport type LikeBlocks = HydrationMap<LikeBlock>\n\nexport type FollowBlock = boolean\nexport type FollowBlocks = HydrationMap<FollowBlock>\n\nexport type BidirectionalBlocks = HydrationMap<HydrationMap<boolean>>\n\nexport type Bookmark = {\n  ref?: { key: string }\n  subjectUri: string\n  subjectCid: string\n  indexedAt?: Date\n}\n\n// actor DID -> stash key -> bookmark\nexport type Bookmarks = HydrationMap<HydrationMap<Bookmark>>\n\n/**\n * Additional config passed from `ServerConfig` to the `Hydrator` instance.\n * Values within this config object may be passed to other sub-hydrators.\n */\nexport type HydratorConfig = {\n  debugFieldAllowedDids: Set<string>\n  featureGatesClient: FeatureGatesClient\n}\n\nexport class Hydrator {\n  actor: ActorHydrator\n  feed: FeedHydrator\n  graph: GraphHydrator\n  label: LabelHydrator\n  serviceLabelers: Set<string>\n  config: HydratorConfig\n\n  constructor(\n    public dataplane: DataPlaneClient,\n    serviceLabelers: string[] = [],\n    config: HydratorConfig,\n  ) {\n    this.config = config\n    this.actor = new ActorHydrator(dataplane)\n    this.feed = new FeedHydrator(dataplane)\n    this.graph = new GraphHydrator(dataplane)\n    this.label = new LabelHydrator(dataplane)\n    this.serviceLabelers = new Set(serviceLabelers)\n  }\n\n  // app.bsky.actor.defs#profileView\n  // - profile viewer\n  //   - list basic\n  // Note: builds on the naive profile viewer hydrator and removes references to lists that have been deleted\n  async hydrateProfileViewers(\n    dids: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const viewer = ctx.viewer\n    if (!viewer) return {}\n    const profileViewers = await this.actor.getProfileViewerStatesNaive(\n      dids,\n      viewer,\n    )\n    const listUris: string[] = []\n    profileViewers.forEach((item) => {\n      listUris.push(...listUrisFromProfileViewer(item))\n    })\n    const listState = await this.hydrateListsBasic(listUris, ctx)\n    // if a list no longer exists or is not a mod list, then remove from viewer state\n    profileViewers.forEach((item) => {\n      removeNonModListsFromProfileViewer(item, listState)\n    })\n\n    return mergeStates(listState, {\n      profileViewers,\n      ctx,\n    })\n  }\n\n  // app.bsky.actor.defs#profileView\n  // - profile\n  //   - list basic\n  async hydrateProfiles(\n    dids: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    /**\n     * Special case here, we want to include takedowns in special cases, like\n     * `getProfile`, since we throw client-facing errors later in the pipeline.\n     */\n    const includeTakedowns =\n      ctx.includeTakedowns || ctx.overrideIncludeTakedownsForActor\n    const [actors, labels, profileViewersState] = await Promise.all([\n      this.actor.getActors(dids, {\n        includeTakedowns,\n        skipCacheForDids: ctx.skipCacheForViewer,\n      }),\n      this.label.getLabelsForSubjects(labelSubjectsForDid(dids), ctx.labelers),\n      this.hydrateProfileViewers(dids, ctx),\n    ])\n    if (!includeTakedowns) {\n      actionTakedownLabels(dids, actors, labels)\n    }\n    return mergeStates(profileViewersState ?? {}, {\n      actors,\n      labels,\n      ctx,\n    })\n  }\n\n  // app.bsky.actor.defs#profileViewBasic\n  // - profile basic\n  //   - profile\n  //     - list basic\n  async hydrateProfilesBasic(\n    dids: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    return this.hydrateProfiles(dids, ctx)\n  }\n\n  // app.bsky.actor.defs#profileViewDetailed\n  // - profile detailed\n  //   - profile\n  //     - list basic\n  //   - starterpack\n  //     - profile\n  //       - list basic\n  //     - labels\n  async hydrateProfilesDetailed(\n    dids: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    let knownFollowers: KnownFollowersStates = new HydrationMap()\n    try {\n      knownFollowers = await this.actor.getKnownFollowers(dids, ctx.viewer)\n    } catch (err) {\n      hydrationLogger.error(\n        { err },\n        'Failed to get known followers for profiles',\n      )\n    }\n\n    let activitySubscriptions: ActivitySubscriptionStates = new HydrationMap()\n    try {\n      activitySubscriptions = await this.actor.getActivitySubscriptions(\n        dids,\n        ctx.viewer,\n      )\n    } catch (err) {\n      hydrationLogger.error(\n        { err },\n        'Failed to get activity subscriptions state for profiles',\n      )\n    }\n\n    const subjectsToKnownFollowersMap = Array.from(\n      knownFollowers.keys(),\n    ).reduce((acc, did) => {\n      const known = knownFollowers.get(did)\n      if (known) {\n        acc.set(did, known.followers)\n      }\n      return acc\n    }, new Map<string, string[]>())\n    const allKnownFollowerDids = Array.from(knownFollowers.values())\n      .filter(Boolean)\n      .flatMap((f) => f!.followers)\n    const allDids = Array.from(new Set(dids.concat(allKnownFollowerDids)))\n    const [state, profileAggs, bidirectionalBlocks] = await Promise.all([\n      this.hydrateProfiles(allDids, ctx),\n      this.actor.getProfileAggregates(dids),\n      this.hydrateBidirectionalBlocks(subjectsToKnownFollowersMap, ctx),\n    ])\n    const starterPackUriSet = new Set<string>()\n    state.actors?.forEach((actor) => {\n      if (actor?.profile?.joinedViaStarterPack) {\n        starterPackUriSet.add(actor?.profile?.joinedViaStarterPack?.uri)\n      }\n    })\n    const starterPackState = await this.hydrateStarterPacksBasic(\n      [...starterPackUriSet],\n      ctx,\n    )\n    return mergeManyStates(state, starterPackState, {\n      profileAggs,\n      knownFollowers,\n      activitySubscriptions,\n      ctx,\n      bidirectionalBlocks,\n    })\n  }\n\n  // app.bsky.graph.defs#listView\n  // - list\n  //   - profile basic\n  async hydrateLists(uris: string[], ctx: HydrateCtx): Promise<HydrationState> {\n    const [listsState, profilesState] = await Promise.all([\n      this.hydrateListsBasic(uris, ctx, {\n        skipAuthors: true, // handled via author profile hydration\n      }),\n      this.hydrateProfilesBasic(uris.map(didFromUri), ctx),\n    ])\n    return mergeStates(listsState, profilesState)\n  }\n\n  // app.bsky.graph.defs#listViewBasic\n  // - list basic\n  async hydrateListsBasic(\n    uris: string[],\n    ctx: HydrateCtx,\n    opts?: { skipAuthors: boolean },\n  ): Promise<HydrationState> {\n    const includeAuthorDids = opts?.skipAuthors ? [] : uris.map(uriToDid)\n    const [lists, listAggs, listViewers, labels, actors] = await Promise.all([\n      this.graph.getLists(uris, ctx.includeTakedowns),\n      this.graph.getListAggregates(uris.map((uri) => ({ uri }))),\n      ctx.viewer ? this.graph.getListViewerStates(uris, ctx.viewer) : undefined,\n      this.label.getLabelsForSubjects(\n        [...uris, ...includeAuthorDids],\n        ctx.labelers,\n      ),\n      this.actor.getActors(includeAuthorDids, {\n        includeTakedowns: ctx.includeTakedowns,\n        skipCacheForDids: ctx.skipCacheForViewer,\n      }),\n    ])\n\n    if (!ctx.includeTakedowns) {\n      actionTakedownLabels(uris, lists, labels)\n      actionTakedownLabels(includeAuthorDids, actors, labels)\n    }\n\n    return { lists, listAggs, listViewers, labels, actors, ctx }\n  }\n\n  // app.bsky.graph.defs#listItemView\n  // - list item\n  //   - profile\n  //     - list basic\n  async hydrateListItems(\n    uris: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const listItems = await this.graph.getListItems(uris)\n    const dids: string[] = []\n    listItems.forEach((item) => {\n      if (item) {\n        dids.push(item.record.subject)\n      }\n    })\n    const profileState = await this.hydrateProfiles(dids, ctx)\n    return mergeStates(profileState, { listItems, ctx })\n  }\n\n  async hydrateListsMembership(\n    uris: string[],\n    did: string,\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const [\n      actorsHydrationState,\n      listsHydrationState,\n      { listitemUris: listItemUris },\n    ] = await Promise.all([\n      this.hydrateProfiles([did], ctx),\n      this.hydrateLists(uris, ctx),\n      this.dataplane.getListMembership({\n        actorDid: did,\n        listUris: uris,\n      }),\n    ])\n\n    // mapping uri -> did -> { actorListItemUri }\n    const listMemberships = new HydrationMap(\n      uris.map((uri, i) => {\n        const listItemUri = listItemUris[i]\n        return [\n          uri,\n          new HydrationMap<ListMembershipState>([\n            listItemUri\n              ? [did, { actorListItemUri: listItemUri }]\n              : [did, null],\n          ]),\n        ]\n      }),\n    )\n\n    return mergeManyStates(actorsHydrationState, listsHydrationState, {\n      listMemberships,\n      ctx,\n    })\n  }\n\n  // app.bsky.feed.defs#postView\n  // - post\n  //   - profile\n  //     - list basic\n  //   - list\n  //     - profile\n  //       - list basic\n  //   - feedgen\n  //     - profile\n  //       - list basic\n  //   - mod service\n  //     - profile\n  //       - list basic\n  async hydratePosts(\n    refs: ItemRef[],\n    ctx: HydrateCtx,\n    state: HydrationState = {},\n    options: Pick<GetPostsHydrationOptions, 'processDynamicTagsForView'> = {},\n  ): Promise<HydrationState> {\n    const uris = refs.map((ref) => ref.uri)\n\n    state.posts ??= new HydrationMap<Post>()\n    const addPostsToHydrationState = (posts: Posts) => {\n      posts.forEach((post, uri) => {\n        state.posts ??= new HydrationMap<Post>()\n        state.posts.set(uri, post)\n      })\n    }\n\n    // layer 0: the posts in the thread\n    const postsLayer0 = await this.feed.getPosts(\n      uris,\n      ctx.includeTakedowns,\n      state.posts,\n      ctx.viewer,\n      {\n        processDynamicTagsForView: options.processDynamicTagsForView,\n      },\n    )\n    addPostsToHydrationState(postsLayer0)\n\n    const additionalRootUris = rootUrisFromPosts(postsLayer0) // supports computing threadgates\n    const threadRootUris = new Set<string>()\n    for (const [uri, post] of postsLayer0) {\n      if (post) {\n        threadRootUris.add(rootUriFromPost(post) ?? uri)\n      }\n    }\n    const postUrisWithThreadgates = new Set<string>()\n    for (const uri of threadRootUris) {\n      const post = postsLayer0.get(uri)\n      /*\n       * Checking `post.hasThreadGate` is an optimization, which tells us that\n       * this post has a threadgate record associated with it. `hydratePosts`\n       * always hydrates root posts via `additionalRootUris`, so we try to\n       * check the optimization flag were possible. If the post is unavailable\n       * for whatever reason, we fall back to requesting threadgate records\n       * that may not exist.\n       */\n      if (!post || post.hasThreadGate) {\n        postUrisWithThreadgates.add(uri)\n      }\n    }\n\n    // layer 1: first level embeds plus thread roots we haven't fetched yet\n    const urisLayer1 = nestedRecordUrisFromPosts(postsLayer0)\n    const urisLayer1ByCollection = urisByCollection(urisLayer1)\n    const embedPostUrisLayer1 =\n      urisLayer1ByCollection.get(ids.AppBskyFeedPost) ?? []\n    const postsLayer1 = await this.feed.getPosts(\n      [...embedPostUrisLayer1, ...additionalRootUris],\n      ctx.includeTakedowns,\n      state.posts,\n    )\n    addPostsToHydrationState(postsLayer1)\n\n    // layer 2: second level embeds, ignoring any additional root uris we mixed-in to the previous layer\n    const urisLayer2 = nestedRecordUrisFromPosts(\n      postsLayer1,\n      embedPostUrisLayer1,\n    )\n    const urisLayer2ByCollection = urisByCollection(urisLayer2)\n    const embedPostUrisLayer2 =\n      urisLayer2ByCollection.get(ids.AppBskyFeedPost) ?? []\n\n    const [postsLayer2, threadgates] = await Promise.all([\n      this.feed.getPosts(\n        embedPostUrisLayer2,\n        ctx.includeTakedowns,\n        state.posts,\n      ),\n      this.feed.getThreadgatesForPosts([...postUrisWithThreadgates.values()]),\n    ])\n    addPostsToHydrationState(postsLayer2)\n\n    // collect list/feedgen embeds, lists in threadgates, post record hydration\n    const threadgateListUris = getListUrisFromThreadgates(threadgates)\n    const nestedListUris = [\n      ...(urisLayer1ByCollection.get(ids.AppBskyGraphList) ?? []),\n      ...(urisLayer2ByCollection.get(ids.AppBskyGraphList) ?? []),\n    ]\n    const nestedFeedGenUris = [\n      ...(urisLayer1ByCollection.get(ids.AppBskyFeedGenerator) ?? []),\n      ...(urisLayer2ByCollection.get(ids.AppBskyFeedGenerator) ?? []),\n    ]\n    const nestedLabelerDids = [\n      ...(urisLayer1ByCollection.get(ids.AppBskyLabelerService) ?? []),\n      ...(urisLayer2ByCollection.get(ids.AppBskyLabelerService) ?? []),\n    ].map(didFromUri)\n    const nestedStarterPackUris = [\n      ...(urisLayer1ByCollection.get(ids.AppBskyGraphStarterpack) ?? []),\n      ...(urisLayer2ByCollection.get(ids.AppBskyGraphStarterpack) ?? []),\n    ]\n    const posts =\n      mergeManyMaps(postsLayer0, postsLayer1, postsLayer2) ?? postsLayer0\n    const allPostUris = [...posts.keys()]\n    const allRefs = [\n      ...refs,\n      ...embedPostUrisLayer1.map(uriToRef), // supports aggregates on embed #viewRecords\n      ...embedPostUrisLayer2.map(uriToRef),\n    ]\n    const threadRefs = allRefs.map((ref) => ({\n      ...ref,\n      threadRoot: posts.get(ref.uri)?.record.reply?.root.uri ?? ref.uri,\n    }))\n    const postUrisWithPostgates = new Set<string>()\n    for (const [uri, post] of posts) {\n      if (post && post.hasPostGate) {\n        postUrisWithPostgates.add(uri)\n      }\n    }\n\n    const [\n      postAggs,\n      postViewers,\n      labels,\n      postBlocks,\n      profileState,\n      listState,\n      feedGenState,\n      labelerState,\n      starterPackState,\n      postgates,\n    ] = await Promise.all([\n      this.feed.getPostAggregates(allRefs, ctx.viewer),\n      ctx.viewer\n        ? this.feed.getPostViewerStates(threadRefs, ctx.viewer)\n        : undefined,\n      this.label.getLabelsForSubjects(allPostUris, ctx.labelers),\n      this.hydratePostBlocks(posts, ctx),\n      this.hydrateProfiles(allPostUris.map(didFromUri), ctx),\n      this.hydrateLists([...nestedListUris, ...threadgateListUris], ctx),\n      this.hydrateFeedGens(nestedFeedGenUris, ctx),\n      this.hydrateLabelers(nestedLabelerDids, ctx),\n      this.hydrateStarterPacksBasic(nestedStarterPackUris, ctx),\n      this.feed.getPostgatesForPosts([...postUrisWithPostgates.values()]),\n    ])\n    if (!ctx.includeTakedowns) {\n      actionTakedownLabels(allPostUris, posts, labels)\n    }\n    // combine all hydration state\n    return mergeManyStates(\n      profileState,\n      listState,\n      feedGenState,\n      labelerState,\n      starterPackState,\n      {\n        posts,\n        postAggs,\n        postViewers,\n        postBlocks,\n        labels,\n        threadgates,\n        postgates,\n        ctx,\n      },\n    )\n  }\n\n  private async hydratePostBlocks(\n    posts: Posts,\n    ctx: HydrateCtx,\n  ): Promise<PostBlocks> {\n    const postBlocks = new HydrationMap<PostBlock>()\n    const postBlocksPairs = new Map<string, PostBlockPairs>()\n    const relationships: RelationshipPair[] = []\n    for (const [uri, item] of posts) {\n      if (!item) continue\n      const post = item.record\n      const creator = didFromUri(uri)\n      const postBlockPairs: PostBlockPairs = {}\n      postBlocksPairs.set(uri, postBlockPairs)\n      // 3p block for replies\n      const parentUri = post.reply?.parent.uri\n      const parentDid = parentUri && didFromUri(parentUri)\n      if (parentDid && parentDid !== creator) {\n        const pair: RelationshipPair = [creator, parentDid]\n        relationships.push(pair)\n        postBlockPairs.parent = pair\n      }\n      const rootUri = post.reply?.root.uri\n      const rootDid = rootUri && didFromUri(rootUri)\n      if (rootDid && rootDid !== creator) {\n        const pair: RelationshipPair = [creator, rootDid]\n        relationships.push(pair)\n        postBlockPairs.root = pair\n      }\n      // 3p block for record embeds\n      for (const embedUri of nestedRecordUris(post)) {\n        const pair: RelationshipPair = [creator, didFromUri(embedUri)]\n        relationships.push(pair)\n        postBlockPairs.embed = pair\n      }\n    }\n    // replace embed/parent/root pairs with block state\n    const blocks = await this.hydrateBidirectionalBlocks(\n      pairsToMap(relationships),\n      ctx,\n    )\n    for (const [uri, { embed, parent, root }] of postBlocksPairs) {\n      postBlocks.set(uri, {\n        embed: !!embed && !!isBlocked(blocks, embed),\n        parent: !!parent && !!isBlocked(blocks, parent),\n        root: !!root && !!isBlocked(blocks, root),\n      })\n    }\n    return postBlocks\n  }\n\n  // app.bsky.feed.defs#feedViewPost\n  // - post (+ replies w/ reply parent author)\n  //   - profile\n  //     - list basic\n  //   - list\n  //     - profile\n  //       - list basic\n  //   - feedgen\n  //     - profile\n  //       - list basic\n  // - repost\n  //   - profile\n  //     - list basic\n  //   - post\n  //     - ...\n  async hydrateFeedItems(\n    items: FeedItem[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    // get posts, collect reply refs\n    const posts = await this.feed.getPosts(\n      items.map((item) => item.post.uri),\n      ctx.includeTakedowns,\n    )\n    const rootUris: string[] = []\n    const parentUris: string[] = []\n    const postAndReplyRefs: ItemRef[] = []\n    posts.forEach((post, uri) => {\n      if (!post) return\n      postAndReplyRefs.push({ uri, cid: post.cid })\n      if (post.record.reply) {\n        rootUris.push(post.record.reply.root.uri)\n        parentUris.push(post.record.reply.parent.uri)\n        postAndReplyRefs.push(post.record.reply.root, post.record.reply.parent)\n      }\n    })\n    // get replies, collect reply parent authors\n    const replies = await this.feed.getPosts(\n      [...rootUris, ...parentUris],\n      ctx.includeTakedowns,\n    )\n    const replyParentAuthors: string[] = []\n    parentUris.forEach((uri) => {\n      const parent = replies.get(uri)\n      if (!parent?.record.reply) return\n      replyParentAuthors.push(didFromUri(parent.record.reply.parent.uri))\n    })\n    // hydrate state for all posts, reposts, authors of reposts + reply parent authors\n    const repostUris = mapDefined(items, (item) => item.repost?.uri)\n    const [postState, repostProfileState, reposts] = await Promise.all([\n      this.hydratePosts(postAndReplyRefs, ctx, {\n        posts: posts.merge(replies), // avoids refetches of posts\n      }),\n      this.hydrateProfiles(\n        [...repostUris.map(didFromUri), ...replyParentAuthors],\n        ctx,\n      ),\n      this.feed.getReposts(repostUris, ctx.includeTakedowns),\n    ])\n    return mergeManyStates(postState, repostProfileState, {\n      reposts,\n      ctx,\n    })\n  }\n\n  // app.bsky.feed.defs#threadViewPost\n  // - post\n  //   - profile\n  //     - list basic\n  //   - list\n  //     - profile\n  //       - list basic\n  //   - feedgen\n  //     - profile\n  //       - list basic\n  async hydrateThreadPosts(\n    refs: ItemRef[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const postsState = await this.hydratePosts(refs, ctx, undefined, {\n      processDynamicTagsForView: ctx.features.checkGate(\n        ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n      )\n        ? 'thread'\n        : undefined,\n    })\n\n    const { posts } = postsState\n    const postsList = posts ? Array.from(posts.entries()) : []\n\n    const isDefined = (\n      entry: [string, Post | null],\n    ): entry is [string, Post] => {\n      const [, post] = entry\n      return !!post\n    }\n\n    const threadRefs: ThreadRef[] = postsList\n      .filter(isDefined)\n      .map(([uri, post]) => ({\n        uri,\n        cid: post.cid,\n        threadRoot: post.record.reply?.root.uri ?? uri,\n      }))\n\n    const threadContexts = await this.feed.getThreadContexts(threadRefs)\n\n    return mergeStates(postsState, { threadContexts })\n  }\n\n  // app.bsky.feed.defs#generatorView\n  // - feedgen\n  //   - profile\n  //     - list basic\n  async hydrateFeedGens(\n    uris: string[], // @TODO any way to get refs here?\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const [feedgens, feedgenAggs, feedgenViewers, profileState, labels] =\n      await Promise.all([\n        this.feed.getFeedGens(uris, ctx.includeTakedowns),\n        this.feed.getFeedGenAggregates(\n          uris.map((uri) => ({ uri })),\n          ctx.viewer,\n        ),\n        ctx.viewer\n          ? this.feed.getFeedGenViewerStates(uris, ctx.viewer)\n          : undefined,\n        this.hydrateProfiles(uris.map(didFromUri), ctx),\n        this.label.getLabelsForSubjects(uris, ctx.labelers),\n      ])\n    if (!ctx.includeTakedowns) {\n      actionTakedownLabels(uris, feedgens, labels)\n    }\n    return mergeStates(profileState, {\n      feedgens,\n      feedgenAggs,\n      feedgenViewers,\n      labels,\n      ctx,\n    })\n  }\n\n  // app.bsky.graph.defs#starterPackViewBasic\n  // - starterpack\n  //   - profile\n  //     - list basic\n  //  - labels\n  async hydrateStarterPacksBasic(\n    uris: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const [starterPacks, starterPackAggs, profileState, labels] =\n      await Promise.all([\n        this.graph.getStarterPacks(uris, ctx.includeTakedowns),\n        this.graph.getStarterPackAggregates(uris.map((uri) => ({ uri }))),\n        this.hydrateProfiles(uris.map(didFromUri), ctx),\n        this.label.getLabelsForSubjects(uris, ctx.labelers),\n      ])\n    if (!ctx.includeTakedowns) {\n      actionTakedownLabels(uris, starterPacks, labels)\n    }\n    return mergeStates(profileState, {\n      starterPacks,\n      starterPackAggs,\n      labels,\n      ctx,\n    })\n  }\n\n  // app.bsky.graph.defs#starterPackView\n  // - starterpack\n  //   - profile\n  //     - list basic\n  //   - feedgen\n  //     - profile\n  //       - list basic\n  //  - list basic\n  //  - list item\n  //    - profile\n  //      - list basic\n  //  - labels\n  async hydrateStarterPacks(\n    uris: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const starterPackState = await this.hydrateStarterPacksBasic(uris, ctx)\n    // gather feed and list uris\n    const feedUriSet = new Set<string>()\n    const listUriSet = new Set<string>()\n    starterPackState.starterPacks?.forEach((sp) => {\n      sp?.record.feeds?.forEach((feed) => feedUriSet.add(feed.uri))\n      if (sp?.record.list) {\n        listUriSet.add(sp?.record.list)\n      }\n    })\n    const feedUris = [...feedUriSet]\n    const listUris = [...listUriSet]\n    // hydrate feeds, lists, and their members\n    const [feedGenState, listState, ...listsMembers] = await Promise.all([\n      this.hydrateFeedGens(feedUris, ctx),\n      this.hydrateLists(listUris, ctx),\n      ...listUris.map((uri) =>\n        this.dataplane.getListMembers({ listUri: uri, limit: 50 }),\n      ),\n    ])\n    // collect list info\n    const listMembersByList = new Map(\n      listUris.map((uri, i) => [uri, listsMembers[i]]),\n    )\n    const listMemberDids = listsMembers.flatMap((lm) =>\n      lm.listitems.map((li) => li.did),\n    )\n    const listCreatorMemberPairs = [...listMembersByList.entries()].flatMap(\n      ([listUri, members]) => {\n        const creator = didFromUri(listUri)\n        return members.listitems.map(\n          (li): RelationshipPair => [creator, li.did],\n        )\n      },\n    )\n    const blocks = await this.hydrateBidirectionalBlocks(\n      pairsToMap(listCreatorMemberPairs),\n      ctx,\n    )\n    // sample top list items per starter pack based on their follows\n    const listMemberAggs = await this.actor.getProfileAggregates(listMemberDids)\n    const listItemUris: string[] = []\n    uris.forEach((uri) => {\n      const sp = starterPackState.starterPacks?.get(uri)\n      const agg = starterPackState.starterPackAggs?.get(uri)\n      if (!sp?.record.list || !agg) return\n      const members = listMembersByList.get(sp.record.list)\n      if (!members) return\n      const creator = didFromUri(sp.record.list)\n      // update aggregation with list items for top 12 most followed members\n      agg.listItemSampleUris = [\n        ...members.listitems.filter(\n          (li) =>\n            ctx.viewer === creator || !isBlocked(blocks, [creator, li.did]),\n        ),\n      ]\n        .sort((li1, li2) => {\n          const score1 = listMemberAggs.get(li1.did)?.followers ?? 0\n          const score2 = listMemberAggs.get(li2.did)?.followers ?? 0\n          return score2 - score1\n        })\n        .slice(0, 12)\n        .map((li) => li.uri)\n      listItemUris.push(...agg.listItemSampleUris)\n    })\n    // hydrate sampled list items\n    const listItemState = await this.hydrateListItems(listItemUris, ctx)\n    return mergeManyStates(\n      starterPackState,\n      feedGenState,\n      listState,\n      listItemState,\n    )\n  }\n\n  // app.bsky.feed.getLikes#like\n  // - like\n  //   - profile\n  //     - list basic\n  async hydrateLikes(\n    authorDid: string,\n    uris: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const [likes, profileState] = await Promise.all([\n      this.feed.getLikes(uris, ctx.includeTakedowns),\n      this.hydrateProfiles(uris.map(didFromUri), ctx),\n    ])\n\n    const pairs: RelationshipPair[] = []\n    for (const [uri, like] of likes) {\n      if (like) {\n        pairs.push([authorDid, didFromUri(uri)])\n      }\n    }\n    const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs), ctx)\n    const likeBlocks = new HydrationMap<LikeBlock>()\n    for (const [uri, like] of likes) {\n      if (like) {\n        likeBlocks.set(uri, isBlocked(blocks, [authorDid, didFromUri(uri)]))\n      } else {\n        likeBlocks.set(uri, null)\n      }\n    }\n\n    return mergeStates(profileState, { likes, likeBlocks, ctx })\n  }\n\n  // app.bsky.feed.getRepostedBy#repostedBy\n  // - repost\n  //   - profile\n  //     - list basic\n  async hydrateReposts(uris: string[], ctx: HydrateCtx) {\n    const [reposts, profileState] = await Promise.all([\n      this.feed.getReposts(uris, ctx.includeTakedowns),\n      this.hydrateProfiles(uris.map(didFromUri), ctx),\n    ])\n    return mergeStates(profileState, { reposts, ctx })\n  }\n\n  // app.bsky.notification.listNotifications#notification\n  // - notification\n  //   - profile\n  //     - list basic\n  async hydrateNotifications(\n    notifs: Notification[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const uris = notifs.map((notif) => notif.uri)\n    const collections = urisByCollection(uris)\n    const postUris = collections.get(ids.AppBskyFeedPost) ?? []\n    const likeUris = collections.get(ids.AppBskyFeedLike) ?? []\n    const repostUris = collections.get(ids.AppBskyFeedRepost) ?? []\n    const followUris = collections.get(ids.AppBskyGraphFollow) ?? []\n    const verificationUris = collections.get(ids.AppBskyGraphVerification) ?? []\n    const [\n      posts,\n      likes,\n      reposts,\n      follows,\n      verifications,\n      labels,\n      profileState,\n    ] = await Promise.all([\n      this.feed.getPosts(postUris), // reason: mention, reply, quote\n      this.feed.getLikes(likeUris), // reason: like\n      this.feed.getReposts(repostUris), // reason: repost\n      this.graph.getFollows(followUris), // reason: follow\n      this.graph.getVerifications(verificationUris), // reason: verified\n      this.label.getLabelsForSubjects(uris, ctx.labelers),\n      this.hydrateProfiles(uris.map(didFromUri), ctx),\n    ])\n    const viewerRootPostUris = new Set<string>()\n    for (const notif of notifs) {\n      if (notif.reason === 'reply') {\n        const post = posts.get(notif.uri)\n        if (post) {\n          const rootUri = post.record.reply?.root.uri\n          if (rootUri && didFromUri(rootUri) === ctx.viewer) {\n            viewerRootPostUris.add(rootUri)\n          }\n        }\n      }\n    }\n    const threadgates = await this.feed.getThreadgatesForPosts([\n      ...viewerRootPostUris.values(),\n    ])\n    actionTakedownLabels(postUris, posts, labels)\n    return mergeStates(profileState, {\n      posts,\n      likes,\n      reposts,\n      follows,\n      verifications,\n      labels,\n      threadgates,\n      ctx,\n    })\n  }\n\n  async hydrateBookmarks(\n    bookmarkInfos: BookmarkInfo[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const viewer = ctx.viewer\n    if (!viewer) return {}\n    const bookmarksRes = await this.dataplane.getBookmarksByActorAndSubjects({\n      actorDid: viewer,\n      uris: bookmarkInfos.map((b) => b.subject),\n    })\n\n    type BookmarkWithRef = BookmarkLex & { ref: RecordRef }\n    const bookmarks: BookmarkWithRef[] = bookmarksRes.bookmarks.filter(\n      (bookmark): bookmark is BookmarkWithRef => !!bookmark.ref?.key,\n    )\n    // mapping DID -> stash key -> bookmark\n    const bookmarksMap = new HydrationMap([\n      [\n        viewer,\n        new HydrationMap<Bookmark>(\n          bookmarks.map((bookmark) => {\n            const {\n              ref: { key },\n            } = bookmark\n            const processed: Bookmark = {\n              ref: bookmark.ref,\n              subjectUri: bookmark.subjectUri,\n              subjectCid: bookmark.subjectCid,\n              indexedAt: parseDate(bookmark.indexedAt),\n            }\n            return [key, processed]\n          }),\n        ),\n      ],\n    ])\n\n    // @NOTE: The `createBookmark` endpoint limits bookmarks to be of posts,\n    // so we can assume currently all subjects are posts.\n    const postsState = await this.hydratePosts(\n      bookmarks.map((bookmark) => ({ uri: bookmark.subjectUri })),\n      ctx,\n    )\n\n    return mergeStates(postsState, { bookmarks: bookmarksMap })\n  }\n\n  // provides partial hydration state within getFollows / getFollowers, mainly for applying rules\n  async hydrateFollows(\n    uris: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const follows = await this.graph.getFollows(uris)\n    const pairs: RelationshipPair[] = []\n    for (const [uri, follow] of follows) {\n      if (follow) {\n        pairs.push([didFromUri(uri), follow.record.subject])\n      }\n    }\n    const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs), ctx)\n    const followBlocks = new HydrationMap<FollowBlock>()\n    for (const [uri, follow] of follows) {\n      if (follow) {\n        followBlocks.set(\n          uri,\n          isBlocked(blocks, [didFromUri(uri), follow.record.subject]),\n        )\n      } else {\n        followBlocks.set(uri, null)\n      }\n    }\n    return { follows, followBlocks }\n  }\n\n  async hydrateBidirectionalBlocks(\n    didMap: Map<string, string[]>, // DID -> DID[]\n    ctx: HydrateCtx,\n  ): Promise<BidirectionalBlocks> {\n    const pairs: RelationshipPair[] = []\n    for (const [source, targets] of didMap) {\n      for (const target of targets) {\n        pairs.push([source, target])\n      }\n    }\n\n    const blocks = await this.graph.getBidirectionalBlocks(pairs)\n    const listUrisSet = new Set<string>()\n    for (const [source, targets] of didMap) {\n      for (const target of targets) {\n        const block = blocks.get(source, target)\n        if (block?.blockListUri) {\n          listUrisSet.add(block.blockListUri)\n        }\n      }\n    }\n    const listUris = [...listUrisSet]\n\n    // if a list no longer exists or is not a mod list, then remove from block entry\n    const listState = await this.hydrateListsBasic(listUris, ctx)\n    for (const [source, targets] of didMap) {\n      for (const target of targets) {\n        const block = blocks.get(source, target)\n        if (!isModList(block?.blockListUri, listState)) {\n          delete block?.blockListUri\n        }\n      }\n    }\n\n    const result: BidirectionalBlocks = new HydrationMap<\n      HydrationMap<boolean>\n    >()\n    for (const [source, targets] of didMap) {\n      const didBlocks = new HydrationMap<boolean>()\n      for (const target of targets) {\n        const block = blocks.get(source, target)\n\n        // If a list no longer exists or is not a mod list, then remove from block entry.\n        // isModList confirms the list exists in listState, which ensures it wasn't taken down.\n        if (!isModList(block?.blockListUri, listState)) {\n          delete block?.blockListUri\n        }\n\n        const blockEntry: BlockEntry = {\n          blockUri: block?.blockUri,\n          blockListUri:\n            block?.blockListUri &&\n            listState.actors?.get(uriToDid(block.blockListUri))\n              ? block.blockListUri\n              : undefined,\n        }\n\n        didBlocks.set(\n          target,\n          !!blockEntry.blockUri || !!blockEntry.blockListUri,\n        )\n      }\n      result.set(source, didBlocks)\n    }\n\n    return result\n  }\n\n  // app.bsky.labeler.def#labelerViewDetailed\n  // - labeler\n  //   - profile\n  //     - list basic\n  async hydrateLabelers(\n    dids: string[],\n    ctx: HydrateCtx,\n  ): Promise<HydrationState> {\n    const [labelers, labelerAggs, labelerViewers, profileState] =\n      await Promise.all([\n        this.label.getLabelers(dids, ctx.includeTakedowns),\n        this.label.getLabelerAggregates(dids, ctx.viewer),\n        ctx.viewer\n          ? this.label.getLabelerViewerStates(dids, ctx.viewer)\n          : undefined,\n        this.hydrateProfiles(dids, ctx),\n      ])\n    actionTakedownLabels(dids, labelers, profileState.labels ?? new Labels())\n    return mergeStates(profileState, {\n      labelers,\n      labelerAggs,\n      labelerViewers,\n      ctx,\n    })\n  }\n\n  // ad-hoc record hydration\n  // in com.atproto.repo.getRecord\n  async getRecord(uri: string, includeTakedowns = false) {\n    const parsed = new AtUri(uri)\n    const collection = parsed.collection\n    if (collection === ids.AppBskyFeedPost) {\n      return (\n        (await this.feed.getPosts([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyFeedRepost) {\n      return (\n        (await this.feed.getReposts([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyFeedLike) {\n      return (\n        (await this.feed.getLikes([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyGraphFollow) {\n      return (\n        (await this.graph.getFollows([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyGraphList) {\n      return (\n        (await this.graph.getLists([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyGraphListitem) {\n      return (\n        (await this.graph.getListItems([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyGraphBlock) {\n      return (\n        (await this.graph.getBlocks([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyGraphStarterpack) {\n      return (\n        (await this.graph.getStarterPacks([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyFeedGenerator) {\n      return (\n        (await this.feed.getFeedGens([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyFeedThreadgate) {\n      return (\n        (await this.feed.getThreadgateRecords([uri], includeTakedowns)).get(\n          uri,\n        ) ?? undefined\n      )\n    } else if (collection === ids.AppBskyFeedPostgate) {\n      return (\n        (await this.feed.getPostgateRecords([uri], includeTakedowns)).get(\n          uri,\n        ) ?? undefined\n      )\n    } else if (collection === ids.AppBskyLabelerService) {\n      if (parsed.rkey !== 'self') return\n      const did = parsed.hostname\n      return (\n        (await this.label.getLabelers([did], includeTakedowns)).get(did) ??\n        undefined\n      )\n    } else if (collection === ids.ChatBskyActorDeclaration) {\n      if (parsed.rkey !== 'self') return\n      return (\n        (await this.actor.getChatDeclarations([uri], includeTakedowns)).get(\n          uri,\n        ) ?? undefined\n      )\n    } else if (collection === ids.ComGermnetworkDeclaration) {\n      if (parsed.rkey !== 'self') return\n      return (\n        (await this.actor.getGermDeclarations([uri], includeTakedowns)).get(\n          uri,\n        ) ?? undefined\n      )\n    } else if (collection === ids.AppBskyNotificationDeclaration) {\n      if (parsed.rkey !== 'self') return\n      return (\n        (\n          await this.actor.getNotificationDeclarations([uri], includeTakedowns)\n        ).get(uri) ?? undefined\n      )\n    } else if (collection === ids.AppBskyActorStatus) {\n      if (parsed.rkey !== 'self') return\n      return (\n        (await this.actor.getStatus([uri], includeTakedowns)).get(uri) ??\n        undefined\n      )\n    } else if (collection === ids.AppBskyActorProfile) {\n      const did = parsed.hostname\n      const actor = (\n        await this.actor.getActors([did], { includeTakedowns })\n      ).get(did)\n      if (!actor?.profile || !actor?.profileCid) return undefined\n      const recordInfo: RecordInfo<ProfileRecord> = {\n        record: actor.profile,\n        cid: actor.profileCid,\n        sortedAt: actor.sortedAt ?? new Date(0), // @NOTE will be present since profile record is present\n        indexedAt: actor.indexedAt ?? new Date(0), // @NOTE will be present since profile record is present\n        takedownRef: actor.profileTakedownRef,\n      }\n\n      return recordInfo\n    }\n  }\n\n  async createContext(\n    vals: Omit<HydrateCtxVals, 'features'> & {\n      features?: ScopedFeatureGatesClient\n    },\n  ) {\n    // ensures we're only apply labelers that exist and are not taken down\n    const labelers = vals.labelers.dids\n    const nonServiceLabelers = labelers.filter(\n      (did) => !this.serviceLabelers.has(did),\n    )\n    const labelerActors = await this.actor.getActors(nonServiceLabelers, {\n      includeTakedowns: vals.includeTakedowns,\n    })\n    const availableDids = labelers.filter(\n      (did) => this.serviceLabelers.has(did) || !!labelerActors.get(did),\n    )\n    const availableLabelers = {\n      dids: availableDids,\n      redact: vals.labelers.redact,\n    }\n    const includeDebugField =\n      !!vals.viewer && this.config.debugFieldAllowedDids.has(vals.viewer)\n    return new HydrateCtx({\n      labelers: availableLabelers,\n      viewer: vals.viewer,\n      includeTakedowns: vals.includeTakedowns,\n      include3pBlocks: vals.include3pBlocks,\n      includeDebugField,\n      // create default anonymous scope\n      features: vals.features || this.config.featureGatesClient.scope({}),\n    })\n  }\n\n  async resolveUri(uriStr: string) {\n    const uri = new AtUri(uriStr)\n    const [did] = await this.actor.getDids([uri.host])\n    if (!did) return uriStr\n    uri.hostname = did\n    return uri.toString()\n  }\n}\n\n// service refs may look like \"did:plc:example#service_id\". we want to extract the did part \"did:plc:example\".\nconst serviceRefToDid = (serviceRef: string) => {\n  const idx = serviceRef.indexOf('#')\n  return idx !== -1 ? serviceRef.slice(0, idx) : serviceRef\n}\n\nconst listUrisFromProfileViewer = (item: ProfileViewerState | null) => {\n  const listUris: string[] = []\n  if (item?.mutedByList) {\n    listUris.push(item.mutedByList)\n  }\n  if (item?.blockingByList) {\n    listUris.push(item.blockingByList)\n  }\n  // blocked-by list does not appear in views, but will be used to evaluate the existence of a block between users.\n  if (item?.blockedByList) {\n    listUris.push(item.blockedByList)\n  }\n  return listUris\n}\n\nconst removeNonModListsFromProfileViewer = (\n  item: ProfileViewerState | null,\n  state: HydrationState,\n) => {\n  if (!isModList(item?.mutedByList, state)) {\n    delete item?.mutedByList\n  }\n  if (!isModList(item?.blockingByList, state)) {\n    delete item?.blockingByList\n  }\n  if (!isModList(item?.blockedByList, state)) {\n    delete item?.blockedByList\n  }\n}\n\nconst isModList = (\n  listUri: string | undefined,\n  state: HydrationState,\n): boolean => {\n  if (!listUri) return false\n  const list = state.lists?.get(listUri)\n  return list?.record.purpose === 'app.bsky.graph.defs#modlist'\n}\n\nconst labelSubjectsForDid = (dids: string[]) => {\n  return [\n    ...dids,\n    ...dids.map((did) =>\n      AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(),\n    ),\n  ]\n}\n\nconst rootUrisFromPosts = (posts: Posts): string[] => {\n  const uris: string[] = []\n  for (const item of posts.values()) {\n    const rootUri = item && rootUriFromPost(item)\n    if (rootUri) {\n      uris.push(rootUri)\n    }\n  }\n  return uris\n}\n\nconst rootUriFromPost = (post: Post): string | undefined => {\n  return post.record.reply?.root.uri\n}\n\nconst nestedRecordUrisFromPosts = (\n  posts: Posts,\n  fromUris?: string[],\n): string[] => {\n  const uris: string[] = []\n  const postUris = fromUris ?? posts.keys()\n  for (const uri of postUris) {\n    const item = posts.get(uri)\n    if (item) {\n      uris.push(...nestedRecordUris(item.record))\n    }\n  }\n  return uris\n}\n\nconst nestedRecordUris = (post: Post['record']): string[] => {\n  const uris: string[] = []\n  if (!post?.embed) return uris\n  if (isEmbedRecord(post.embed)) {\n    uris.push(post.embed.record.uri)\n  } else if (isEmbedRecordWithMedia(post.embed)) {\n    uris.push(post.embed.record.record.uri)\n  }\n  return uris\n}\n\nconst getListUrisFromThreadgates = (gates: Threadgates) => {\n  const uris: string[] = []\n  for (const gate of gates.values()) {\n    const listRules = gate?.record.allow?.filter(isThreadgateListRule) ?? []\n    for (const rule of listRules) {\n      uris.push(rule.list)\n    }\n  }\n  return uris\n}\n\nconst isBlocked = (blocks: BidirectionalBlocks, [a, b]: RelationshipPair) => {\n  return blocks.get(a)?.get(b) ?? false\n}\n\nconst pairsToMap = (pairs: RelationshipPair[]): Map<string, string[]> => {\n  const map = new Map<string, string[]>()\n  for (const [a, b] of pairs) {\n    const list = map.get(a) ?? []\n    list.push(b)\n    map.set(a, list)\n  }\n  return map\n}\n\nexport const mergeStates = (\n  stateA: HydrationState,\n  stateB: HydrationState,\n): HydrationState => {\n  assert(\n    !stateA.ctx?.viewer ||\n      !stateB.ctx?.viewer ||\n      stateA.ctx?.viewer === stateB.ctx?.viewer,\n    'incompatible viewers',\n  )\n  return {\n    ctx: stateA.ctx ?? stateB.ctx,\n    actors: mergeMaps(stateA.actors, stateB.actors),\n    profileAggs: mergeMaps(stateA.profileAggs, stateB.profileAggs),\n    profileViewers: mergeMaps(stateA.profileViewers, stateB.profileViewers),\n    posts: mergeMaps(stateA.posts, stateB.posts),\n    postAggs: mergeMaps(stateA.postAggs, stateB.postAggs),\n    postViewers: mergeMaps(stateA.postViewers, stateB.postViewers),\n    threadContexts: mergeMaps(stateA.threadContexts, stateB.threadContexts),\n    postBlocks: mergeMaps(stateA.postBlocks, stateB.postBlocks),\n    reposts: mergeMaps(stateA.reposts, stateB.reposts),\n    follows: mergeMaps(stateA.follows, stateB.follows),\n    followBlocks: mergeMaps(stateA.followBlocks, stateB.followBlocks),\n    threadgates: mergeMaps(stateA.threadgates, stateB.threadgates),\n    postgates: mergeMaps(stateA.postgates, stateB.postgates),\n    lists: mergeMaps(stateA.lists, stateB.lists),\n    listAggs: mergeMaps(stateA.listAggs, stateB.listAggs),\n    listMemberships: mergeNestedMaps(\n      stateA.listMemberships,\n      stateB.listMemberships,\n    ),\n    listViewers: mergeMaps(stateA.listViewers, stateB.listViewers),\n    listItems: mergeMaps(stateA.listItems, stateB.listItems),\n    likes: mergeMaps(stateA.likes, stateB.likes),\n    likeBlocks: mergeMaps(stateA.likeBlocks, stateB.likeBlocks),\n    labels: mergeMaps(stateA.labels, stateB.labels),\n    feedgens: mergeMaps(stateA.feedgens, stateB.feedgens),\n    feedgenAggs: mergeMaps(stateA.feedgenAggs, stateB.feedgenAggs),\n    feedgenViewers: mergeMaps(stateA.feedgenViewers, stateB.feedgenViewers),\n    starterPacks: mergeMaps(stateA.starterPacks, stateB.starterPacks),\n    starterPackAggs: mergeMaps(stateA.starterPackAggs, stateB.starterPackAggs),\n    labelers: mergeMaps(stateA.labelers, stateB.labelers),\n    labelerAggs: mergeMaps(stateA.labelerAggs, stateB.labelerAggs),\n    labelerViewers: mergeMaps(stateA.labelerViewers, stateB.labelerViewers),\n    knownFollowers: mergeMaps(stateA.knownFollowers, stateB.knownFollowers),\n    activitySubscriptions: mergeMaps(\n      stateA.activitySubscriptions,\n      stateB.activitySubscriptions,\n    ),\n    bidirectionalBlocks: mergeNestedMaps(\n      stateA.bidirectionalBlocks,\n      stateB.bidirectionalBlocks,\n    ),\n    verifications: mergeMaps(stateA.verifications, stateB.verifications),\n    bookmarks: mergeNestedMaps(stateA.bookmarks, stateB.bookmarks),\n  }\n}\n\nexport const mergeManyStates = (...states: HydrationState[]) => {\n  return states.reduce(mergeStates, {} as HydrationState)\n}\n\nconst actionTakedownLabels = <T>(\n  keys: string[],\n  hydrationMap: HydrationMap<T>,\n  labels: Labels,\n) => {\n  for (const key of keys) {\n    if (labels.get(key)?.isTakendown) {\n      hydrationMap.set(key, null)\n    }\n  }\n}\n\nconst uriToRef = (uri: string): ItemRef => {\n  return { uri }\n}\n"
  },
  {
    "path": "packages/bsky/src/hydration/label.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { DataPlaneClient } from '../data-plane/client'\nimport { ids } from '../lexicon/lexicons'\nimport { Record as LabelerRecord } from '../lexicon/types/app/bsky/labeler/service'\nimport { Label } from '../lexicon/types/com/atproto/label/defs'\nimport { ParsedLabelers } from '../util'\nimport {\n  HydrationMap,\n  Merges,\n  RecordInfo,\n  parseJsonBytes,\n  parseRecord,\n  parseString,\n} from './util'\n\nexport type { Label } from '../lexicon/types/com/atproto/label/defs'\n\nexport type SubjectLabels = {\n  isImpersonation: boolean\n  isTakendown: boolean\n  needsReview: boolean\n  labels: HydrationMap<Label> // src + val -> label\n}\n\nexport class Labels extends HydrationMap<SubjectLabels> implements Merges {\n  static key(label: Label) {\n    return `${label.src}::${label.val}`\n  }\n  merge(map: Labels): this {\n    map.forEach((theirs, key) => {\n      if (!theirs) return\n      const mine = this.get(key)\n      if (mine) {\n        mine.isTakendown = mine.isTakendown || theirs.isTakendown\n        mine.labels = mine.labels.merge(theirs.labels)\n      } else {\n        this.set(key, theirs)\n      }\n    })\n    return this\n  }\n  getBySubject(sub: string): Label[] {\n    const it = this.get(sub)?.labels.values()\n    if (!it) return []\n    const labels: Label[] = []\n    for (const label of it) {\n      if (label) labels.push(label)\n    }\n    return labels\n  }\n}\n\nexport type LabelerAgg = {\n  likes: number\n}\n\nexport type LabelerAggs = HydrationMap<LabelerAgg>\n\nexport type Labeler = RecordInfo<LabelerRecord>\nexport type Labelers = HydrationMap<Labeler>\n\nexport type LabelerViewerState = {\n  like?: string\n}\n\nexport type LabelerViewerStates = HydrationMap<LabelerViewerState>\n\nexport class LabelHydrator {\n  constructor(public dataplane: DataPlaneClient) {}\n\n  async getLabelsForSubjects(\n    subjects: string[],\n    labelers: ParsedLabelers,\n  ): Promise<Labels> {\n    if (!subjects.length || !labelers.dids.length) return new Labels()\n    const res = await this.dataplane.getLabels({\n      subjects,\n      issuers: labelers.dids,\n    })\n\n    return res.labels.reduce((acc, cur) => {\n      const parsed = parseJsonBytes(cur) as Label | undefined\n      if (!parsed || parsed.neg) return acc\n      const { sig: _, ...label } = parsed\n      let entry = acc.get(label.uri)\n      if (!entry) {\n        entry = {\n          isImpersonation: false,\n          isTakendown: false,\n          needsReview: false,\n          labels: new HydrationMap(),\n        }\n        acc.set(label.uri, entry)\n      }\n\n      const isActionableNeedsReview =\n        label.val === NEEDS_REVIEW_LABEL &&\n        !label.neg &&\n        labelers.redact.has(label.src)\n\n      // we action needs review labels on backend for now so don't send to client until client has proper logic for them\n      if (!isActionableNeedsReview) {\n        entry.labels.set(Labels.key(label), label)\n      }\n\n      if (\n        TAKEDOWN_LABELS.includes(label.val) &&\n        !label.neg &&\n        labelers.redact.has(label.src)\n      ) {\n        entry.isTakendown = true\n      }\n      if (isActionableNeedsReview) {\n        entry.needsReview = true\n      }\n      if (\n        label.val === IMPERSONATION_LABEL &&\n        !label.neg &&\n        labelers.redact.has(label.src)\n      ) {\n        entry.isImpersonation = true\n      }\n\n      return acc\n    }, new Labels())\n  }\n\n  async getLabelers(\n    dids: string[],\n    includeTakedowns = false,\n  ): Promise<Labelers> {\n    const res = await this.dataplane.getLabelerRecords({\n      uris: dids.map(labelerDidToUri),\n    })\n    return dids.reduce((acc, did, i) => {\n      const record = parseRecord<LabelerRecord>(\n        res.records[i],\n        includeTakedowns,\n      )\n      return acc.set(did, record ?? null)\n    }, new HydrationMap<Labeler>())\n  }\n\n  async getLabelerViewerStates(\n    dids: string[],\n    viewer: string,\n  ): Promise<LabelerViewerStates> {\n    const likes = await this.dataplane.getLikesByActorAndSubjects({\n      actorDid: viewer,\n      refs: dids.map((did) => ({ uri: labelerDidToUri(did) })),\n    })\n    return dids.reduce((acc, did, i) => {\n      return acc.set(did, {\n        like: parseString(likes.uris[i]),\n      })\n    }, new HydrationMap<LabelerViewerState>())\n  }\n\n  async getLabelerAggregates(\n    dids: string[],\n    viewer: string | null,\n  ): Promise<LabelerAggs> {\n    const refs = dids.map((did) => ({ uri: labelerDidToUri(did) }))\n    const counts = await this.dataplane.getInteractionCounts({\n      refs,\n      skipCacheForDids: viewer ? [viewer] : undefined,\n    })\n    return dids.reduce((acc, did, i) => {\n      return acc.set(did, {\n        likes: counts.likes[i] ?? 0,\n      })\n    }, new HydrationMap<LabelerAgg>())\n  }\n}\n\nconst labelerDidToUri = (did: string): string => {\n  return AtUri.make(did, ids.AppBskyLabelerService, 'self').toString()\n}\n\nconst IMPERSONATION_LABEL = 'impersonation'\nconst TAKEDOWN_LABELS = ['!takedown', '!suspend']\nconst NEEDS_REVIEW_LABEL = 'needs-review'\n"
  },
  {
    "path": "packages/bsky/src/hydration/util.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport { CID } from 'multiformats/cid'\nimport * as ui8 from 'uint8arrays'\nimport { jsonToLex } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport { lexicons } from '../lexicon/lexicons'\nimport { Record } from '../proto/bsky_pb'\n\nexport class HydrationMap<T> extends Map<string, T | null> implements Merges {\n  merge(map: HydrationMap<T>): this {\n    map.forEach((val, key) => {\n      this.set(key, val)\n    })\n    return this\n  }\n}\n\nexport interface Merges {\n  merge<T extends this>(map: T): this\n}\n\ntype UnknownRecord = { $type: string; [x: string]: unknown }\n\nexport type RecordInfo<T extends UnknownRecord> = {\n  record: T\n  cid: string\n  sortedAt: Date\n  indexedAt: Date\n  takedownRef: string | undefined\n}\n\nexport const mergeMaps = <V, M extends HydrationMap<V>>(\n  mapA?: M,\n  mapB?: M,\n): M | undefined => {\n  if (!mapA) return mapB\n  if (!mapB) return mapA\n  return mapA.merge(mapB)\n}\n\nexport const mergeNestedMaps = <V, M extends HydrationMap<HydrationMap<V>>>(\n  mapA?: M,\n  mapB?: M,\n): M | undefined => {\n  if (!mapA) return mapB\n  if (!mapB) return mapA\n\n  for (const [key, map] of mapB) {\n    const merged = mergeMaps(mapA.get(key) ?? undefined, map ?? undefined)\n    mapA.set(key, merged ?? null)\n  }\n\n  return mapA\n}\n\nexport const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => {\n  return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined)\n}\n\nexport type ItemRef = { uri: string; cid?: string }\n\nexport const parseRecord = <T extends UnknownRecord>(\n  entry: Record,\n  includeTakedowns: boolean,\n): RecordInfo<T> | undefined => {\n  if (!includeTakedowns && entry.takenDown) {\n    return undefined\n  }\n  const record = parseRecordBytes<T>(entry.record)\n  const cid = entry.cid\n  const sortedAt = parseDate(entry.sortedAt) ?? new Date(0)\n  const indexedAt = parseDate(entry.indexedAt) ?? new Date(0)\n  if (!record || !cid) return\n  if (!isValidRecord(record)) {\n    return\n  }\n  return {\n    record,\n    cid,\n    sortedAt,\n    indexedAt,\n    takedownRef: safeTakedownRef(entry),\n  }\n}\n\nconst isValidRecord = (json: unknown) => {\n  const lexRecord = jsonToLex(json)\n  if (typeof lexRecord?.['$type'] !== 'string') {\n    return false\n  }\n  try {\n    lexicons.assertValidRecord(lexRecord['$type'], lexRecord)\n    return true\n  } catch {\n    return false\n  }\n}\n\n// @NOTE not parsed into lex format, so will not match lexicon record types on CID and blob values.\nexport const parseRecordBytes = <T>(\n  bytes: Uint8Array | undefined,\n): T | undefined => {\n  return parseJsonBytes(bytes) as T\n}\n\nexport const parseJsonBytes = (bytes: Uint8Array | undefined): unknown => {\n  if (!bytes || bytes.byteLength === 0) return\n  const parsed = JSON.parse(ui8.toString(bytes, 'utf8'))\n  return parsed ?? undefined\n}\n\nexport const parseString = (str: string | undefined): string | undefined => {\n  return str && str.length > 0 ? str : undefined\n}\n\nexport const parseCid = (cidStr: string | undefined): CID | undefined => {\n  if (!cidStr || cidStr.length === 0) return\n  try {\n    return CID.parse(cidStr)\n  } catch {\n    return\n  }\n}\n\nexport const parseDate = (\n  timestamp: Timestamp | undefined,\n): Date | undefined => {\n  if (!timestamp) return undefined\n  const date = timestamp.toDate()\n  // Check for year 1 (0001-01-01 00:00:00 UTC) which is -62135596800000ms from epoch.\n  // The Go dataplane gives us those values as they come from the Go zero-value for dates.\n  if (date.getTime() === -62135596800000) return undefined\n  return date\n}\n\nexport const urisByCollection = (uris: string[]): Map<string, string[]> => {\n  const result = new Map<string, string[]>()\n  for (const uri of uris) {\n    const collection = new AtUri(uri).collection\n    const items = result.get(collection) ?? []\n    items.push(uri)\n    result.set(collection, items)\n  }\n  return result\n}\n\nexport const split = <T>(\n  items: T[],\n  predicate: (item: T) => boolean,\n): [T[], T[]] => {\n  const yes: T[] = []\n  const no: T[] = []\n  for (const item of items) {\n    if (predicate(item)) {\n      yes.push(item)\n    } else {\n      no.push(item)\n    }\n  }\n  return [yes, no]\n}\n\nexport const safeTakedownRef = (obj?: {\n  takenDown: boolean\n  takedownRef: string\n}): string | undefined => {\n  if (!obj) return\n  if (obj.takedownRef) return obj.takedownRef\n  if (obj.takenDown) return 'BSKY-TAKEDOWN-UNKNOWN'\n}\n\nexport const isActivitySubscriptionEnabled = ({\n  post,\n  reply,\n}: {\n  post: boolean\n  reply: boolean\n}): boolean => post || reply\n"
  },
  {
    "path": "packages/bsky/src/image/index.ts",
    "content": "export * from './sharp'\nexport type { ImageInfo, Options } from './util'\n"
  },
  {
    "path": "packages/bsky/src/image/invalidator.ts",
    "content": "import { BlobCache } from './server'\nimport { ImageUriBuilder } from './uri'\n\n// Invalidation is a general interface for propagating an image blob\n// takedown through any caches where a representation of it may be stored.\n// @NOTE this does not remove the blob from storage: just invalidates it from caches.\n// @NOTE keep in sync with same interface in aws/src/cloudfront.ts\nexport interface ImageInvalidator {\n  invalidate(subject: string, paths: string[]): Promise<void>\n}\n\nexport class ImageProcessingServerInvalidator implements ImageInvalidator {\n  constructor(private cache: BlobCache) {}\n  async invalidate(_subject: string, paths: string[]) {\n    const results = await Promise.allSettled(\n      paths.map(async (path) => {\n        const [, signature] = path.split('/')\n        if (!signature) throw new Error('Missing signature')\n        const options = ImageUriBuilder.getOptions(path)\n        const cacheKey = [\n          options.did,\n          options.cid.toString(),\n          options.preset,\n        ].join('::')\n        await this.cache.clear(cacheKey)\n      }),\n    )\n    const rejection = results.find(\n      (result): result is PromiseRejectedResult => result.status === 'rejected',\n    )\n    if (rejection) throw rejection.reason\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/image/logger.ts",
    "content": "import { subsystemLogger } from '@atproto/common'\n\nexport const logger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:image')\n\nexport default logger\n"
  },
  {
    "path": "packages/bsky/src/image/server.ts",
    "content": "import fsSync from 'node:fs'\nimport fs from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { Duplex, Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport createError, { isHttpError } from 'http-errors'\nimport {\n  VerifyCidError,\n  VerifyCidTransform,\n  cloneStream,\n  createDecoders,\n  isErrnoException,\n} from '@atproto/common'\nimport { BlobNotFoundError } from '@atproto/repo'\nimport { StreamBlobOptions, streamBlob } from '../api/blob-resolver'\nimport { AppContext } from '../context'\nimport { Middleware, responseSignal } from '../util/http'\nimport log from './logger'\nimport { createImageProcessor, createImageUpscaler } from './sharp'\nimport { BadPathError, ImageUriBuilder } from './uri'\nimport { Options, SharpInfo, formatsToMimes } from './util'\n\nexport function createMiddleware(\n  ctx: AppContext,\n  { prefix = '/' }: { prefix?: string } = {},\n): Middleware {\n  if (!prefix.startsWith('/') || !prefix.endsWith('/')) {\n    throw new TypeError('Prefix must start and end with a slash')\n  }\n\n  // If there is a CDN, we don't need to serve images\n  if (ctx.cfg.cdnUrl) {\n    return (req, res, next) => next()\n  }\n\n  const cache = new BlobDiskCache(ctx.cfg.blobCacheLocation)\n\n  return async (req, res, next) => {\n    if (res.destroyed) return\n    if (req.method !== 'GET' && req.method !== 'HEAD') return next()\n    if (!req.url?.startsWith(prefix)) return next()\n    const { 0: path, 1: _search } = req.url.slice(prefix.length - 1).split('?')\n    if (!path.startsWith('/') || path === '/') return next()\n\n    try {\n      const options = ImageUriBuilder.getOptions(path)\n\n      const cacheKey = [options.did, options.cid, options.preset].join('::')\n\n      // Cached flow\n\n      try {\n        const cachedImage = await cache.get(cacheKey)\n        res.statusCode = 200\n        res.setHeader('x-cache', 'hit')\n        res.setHeader('content-type', getMime(options.format))\n        res.setHeader('cache-control', `public, max-age=31536000`) // 1 year\n        res.setHeader('content-length', cachedImage.size)\n        await pipeline(cachedImage, res)\n        return\n      } catch (err) {\n        if (!(err instanceof BlobNotFoundError)) {\n          log.error({ cacheKey, err }, 'failed to serve cached image')\n        }\n\n        if (res.headersSent || res.destroyed) {\n          res.destroy()\n          return // nothing we can do...\n        } else {\n          // Ignore and move on to non-cached flow.\n          res.removeHeader('x-cache')\n          res.removeHeader('content-type')\n          res.removeHeader('cache-control')\n          res.removeHeader('content-length')\n        }\n      }\n\n      // Non-cached flow\n\n      const streamOptions: StreamBlobOptions = {\n        did: options.did,\n        cid: options.cid,\n        signal: responseSignal(res),\n      }\n\n      await streamBlob(ctx, streamOptions, (upstream, { did, cid, url }) => {\n        // Definitely not an image ? Let's fail right away.\n        if (isImageMime(upstream.headers['content-type']) === false) {\n          throw createError(400, 'Not an image')\n        }\n\n        // Let's transform (decompress, verify CID, upscale), process and respond\n\n        const transforms: Duplex[] = [\n          ...createDecoders(upstream.headers['content-encoding']),\n          new VerifyCidTransform(cid),\n          createImageUpscaler(options),\n        ]\n        const processor = createImageProcessor(options)\n\n        // Cache in the background\n        cache\n          .put(cacheKey, cloneStream(processor))\n          .catch((err) => log.error({ err }, 'failed to cache image'))\n\n        res.statusCode = 200\n        res.setHeader('cache-control', `public, max-age=31536000`) // 1 year\n        res.setHeader('x-cache', 'miss')\n        processor.once('info', ({ size, format }: SharpInfo) => {\n          const type = formatsToMimes.get(format) || 'application/octet-stream'\n\n          // @NOTE sharp does emit this in time to be set as a header\n          res.setHeader('content-length', size)\n          res.setHeader('content-type', type)\n        })\n\n        const streams = [...transforms, processor, res]\n        void pipeline(streams).catch((err: unknown) => {\n          log.warn(\n            { err, did, cid: cid.toString(), pds: url.origin },\n            'blob resolution failed during transmission',\n          )\n        })\n\n        return streams[0]!\n      })\n    } catch (err) {\n      if (res.headersSent || res.destroyed) {\n        res.destroy()\n      } else {\n        res.removeHeader('content-type')\n        res.removeHeader('content-length')\n        res.removeHeader('cache-control')\n        res.removeHeader('x-cache')\n\n        if (err instanceof BadPathError) {\n          next(createError(400, err))\n        } else if (err instanceof VerifyCidError) {\n          next(createError(404, 'Blob not found', err))\n        } else if (isHttpError(err)) {\n          next(err)\n        } else {\n          next(createError(502, 'Upstream Error', { cause: err }))\n        }\n      }\n    }\n  }\n}\n\nfunction isImageMime(\n  contentType: string | string[] | undefined,\n): undefined | boolean {\n  if (contentType == null || contentType === 'application/octet-stream') {\n    return undefined // maybe\n  }\n  if (Array.isArray(contentType)) {\n    if (contentType.length === 0) return undefined // should never happen\n    if (contentType.length === 1) return isImageMime(contentType[0])\n    return contentType.every(isImageMime) // Should we throw a 502 here?\n  }\n  return contentType.startsWith('image/')\n}\n\nfunction getMime(format: Options['format']) {\n  const mime = formatsToMimes.get(format)\n  if (!mime) throw new Error('Unknown format')\n  return mime\n}\n\nexport interface BlobCache {\n  get(fileId: string): Promise<Readable & { size: number }>\n  put(fileId: string, stream: Readable): Promise<void>\n  clear(fileId: string): Promise<void>\n  clearAll(): Promise<void>\n}\n\nexport class BlobDiskCache implements BlobCache {\n  tempDir: string\n  constructor(basePath?: string) {\n    this.tempDir = basePath || path.join(os.tmpdir(), 'bsky--processed-images')\n    if (!path.isAbsolute(this.tempDir)) {\n      throw new Error('Must provide an absolute path')\n    }\n    try {\n      fsSync.mkdirSync(this.tempDir, { recursive: true })\n    } catch (err) {\n      // All good if cache dir already exists\n      if (isErrnoException(err) && err.code === 'EEXIST') return\n    }\n  }\n\n  async get(fileId: string) {\n    try {\n      const handle = await fs.open(path.join(this.tempDir, fileId), 'r')\n      const { size } = await handle.stat()\n      if (size === 0) {\n        throw new BlobNotFoundError()\n      }\n      return Object.assign(handle.createReadStream(), { size })\n    } catch (err) {\n      if (isErrnoException(err) && err.code === 'ENOENT') {\n        throw new BlobNotFoundError()\n      }\n      throw err\n    }\n  }\n\n  async put(fileId: string, stream: Readable) {\n    const filename = path.join(this.tempDir, fileId)\n    try {\n      await fs.writeFile(filename, stream, { flag: 'wx' })\n    } catch (err) {\n      // Do not overwrite existing file, just ignore the error\n      if (isErrnoException(err) && err.code === 'EEXIST') return\n      throw err\n    }\n  }\n\n  async clear(fileId: string) {\n    const filename = path.join(this.tempDir, fileId)\n    await fs.rm(filename, { force: true })\n  }\n\n  async clearAll() {\n    await fs.rm(this.tempDir, { recursive: true, force: true })\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/image/sharp.ts",
    "content": "import { PassThrough, Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport sharp from 'sharp'\nimport { errHasMsg } from '@atproto/common'\nimport { ImageInfo, Options, formatsToMimes } from './util'\n\nexport type { Options }\n\n/**\n * Scale up to hit any specified minimum size\n */\nexport function createImageUpscaler({ min = false }: Options) {\n  // Due to the way sharp works, up-scaling must happen in a separate processor\n  // than down-scaling.\n  return typeof min !== 'boolean'\n    ? sharp().resize({\n        fit: 'outside',\n        width: min.width,\n        height: min.height,\n        withoutReduction: true,\n        withoutEnlargement: false,\n      })\n    : new PassThrough()\n}\n\n/**\n * Scale down (or possibly up if min is true) to desired size, then compress\n * to the desired format.\n */\nexport function createImageProcessor({\n  height,\n  width,\n  min = false,\n  fit = 'cover',\n  format,\n  quality = 100,\n}: Options) {\n  const processor = sharp().resize({\n    fit,\n    width,\n    height,\n    withoutEnlargement: min !== true,\n  })\n\n  if (format === 'jpeg') {\n    return processor.jpeg({ quality })\n  } else if (format === 'webp') {\n    return processor.webp({ quality })\n  } else {\n    throw new Error(`Unhandled case: ${format}`)\n  }\n}\n\nexport async function maybeGetInfo(\n  stream: Readable,\n): Promise<ImageInfo | null> {\n  try {\n    const processor = sharp()\n\n    const [{ size, height, width, format }] = await Promise.all([\n      processor.metadata(),\n      pipeline(stream, processor), // Handles error propagation\n    ])\n\n    if (size == null || height == null || width == null || format == null) {\n      return null\n    }\n\n    return {\n      height,\n      width,\n      size,\n      mime: formatsToMimes.get(format) ?? 'unknown',\n    }\n  } catch (err) {\n    if (errHasMsg(err, 'Input buffer contains unsupported image format')) {\n      return null\n    }\n    throw err\n  }\n}\n\nexport async function getInfo(stream: Readable): Promise<ImageInfo> {\n  const maybeInfo = await maybeGetInfo(stream)\n  if (!maybeInfo) {\n    throw new Error('could not obtain all image metadata')\n  }\n  return maybeInfo\n}\n"
  },
  {
    "path": "packages/bsky/src/image/uri.ts",
    "content": "import { Options } from './util'\n\n// @NOTE if there are any additions here, ensure to include them on ImageUriBuilder.presets\nexport type ImagePreset =\n  | 'avatar'\n  | 'banner'\n  | 'feed_thumbnail'\n  | 'feed_fullsize'\n\nconst PATH_REGEX = /^\\/(.+?)\\/plain\\/(.+?)\\/(.+?)(?:@(.+?))?$/\n\nexport class ImageUriBuilder {\n  constructor(public endpoint: string) {}\n\n  static presets: ImagePreset[] = [\n    'avatar',\n    'banner',\n    'feed_thumbnail',\n    'feed_fullsize',\n  ]\n\n  getPresetUri(id: ImagePreset, did: string, cid: string): string {\n    const options = presets[id]\n    if (!options) {\n      throw new Error(`Unrecognized requested common uri type: ${id}`)\n    }\n\n    return this.endpoint + ImageUriBuilder.getPath({ preset: id, did, cid })\n  }\n\n  static getPath(opts: { preset: ImagePreset } & BlobLocation) {\n    return `/${opts.preset}/plain/${opts.did}/${opts.cid}`\n  }\n\n  static getOptions(\n    path: string,\n  ): Options & BlobLocation & { preset: ImagePreset } {\n    const match = path.match(PATH_REGEX)\n    if (!match) {\n      throw new BadPathError('Invalid path')\n    }\n    const [, presetUnsafe, did, cid, formatUnsafe] = match\n    if (!(ImageUriBuilder.presets as string[]).includes(presetUnsafe)) {\n      throw new BadPathError('Invalid path: bad preset')\n    }\n    if (\n      formatUnsafe !== undefined &&\n      formatUnsafe !== 'jpeg' &&\n      formatUnsafe !== 'webp'\n    ) {\n      throw new BadPathError('Invalid path: bad format')\n    }\n    const preset = presetUnsafe as ImagePreset\n    const format = formatUnsafe as Options['format']\n    return {\n      ...presets[preset],\n      format: format ?? presets[preset].format,\n      did,\n      cid,\n      preset,\n    }\n  }\n}\n\ntype BlobLocation = { cid: string; did: string }\n\nexport class BadPathError extends Error {}\n\n// @NOTE these prefix settings don't get used anywhere in this package,\n// but they serve as soft documentation of the behavior in production.\nexport const presets: Record<ImagePreset, Options> = {\n  avatar: {\n    format: 'webp',\n    fit: 'cover',\n    height: 1000,\n    width: 1000,\n    min: true,\n  },\n  banner: {\n    format: 'webp',\n    fit: 'cover',\n    height: 1000,\n    width: 3000,\n    min: true,\n  },\n  feed_thumbnail: {\n    format: 'webp',\n    fit: 'inside',\n    height: 2000,\n    width: 2000,\n    min: true,\n  },\n  feed_fullsize: {\n    format: 'webp',\n    fit: 'inside',\n    height: 1000,\n    width: 1000,\n    min: true,\n  },\n}\n"
  },
  {
    "path": "packages/bsky/src/image/util.ts",
    "content": "import { FormatEnum, OutputInfo } from 'sharp'\n\nexport type ImageMime = `image/${string}`\n\nexport type Options = Dimensions & {\n  format: 'jpeg' | 'webp'\n  // When 'cover' (default), scale to fill given dimensions, cropping if necessary.\n  // When 'inside', scale to fit within given dimensions.\n  fit?: 'cover' | 'inside'\n  // When false (default), do not scale up.\n  // When true, scale up to hit dimensions given in options.\n  // Otherwise, scale up to hit specified min dimensions.\n  min?: Dimensions | boolean\n  // A number 1-100\n  quality?: number\n}\n\nexport type ImageInfo = Dimensions & {\n  size: number\n  mime: ImageMime | 'unknown'\n}\n\nexport type Dimensions = { height: number; width: number }\n\nexport const formatsToMimes = new Map<keyof FormatEnum, ImageMime>([\n  ['jpg', 'image/jpeg'],\n  ['jpeg', 'image/jpeg'],\n  ['png', 'image/png'],\n  ['gif', 'image/gif'],\n  ['svg', 'image/svg+xml'],\n  ['tif', 'image/tiff'],\n  ['tiff', 'image/tiff'],\n  ['webp', 'image/webp'],\n  ['avif', 'image/avif'],\n  ['heif', 'image/heif'],\n  ['jp2', 'image/jp2'],\n  ['jxl', 'image/jxl'],\n  ['webp', 'image/webp'],\n])\n\nexport type SharpInfo = OutputInfo & { format: keyof FormatEnum }\n"
  },
  {
    "path": "packages/bsky/src/index.ts",
    "content": "import events from 'node:events'\nimport http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport compression from 'compression'\nimport cors from 'cors'\nimport { Etcd3 } from 'etcd3'\nimport express from 'express'\nimport { HttpTerminator, createHttpTerminator } from 'http-terminator'\nimport { AtpAgent } from '@atproto/api'\nimport { DAY, SECOND } from '@atproto/common'\nimport { Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport API, { blobResolver, external, health, sitemap, wellKnown } from './api'\nimport { createBlobDispatcher } from './api/blob-dispatcher'\nimport { AuthVerifier, createPublicKeyObject } from './auth-verifier'\nimport { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync'\nimport { ServerConfig } from './config'\nimport { AppContext } from './context'\nimport { authWithApiKey as courierAuth, createCourierClient } from './courier'\nimport {\n  BasicHostList,\n  EtcdHostList,\n  createDataPlaneClient,\n} from './data-plane/client'\nimport * as error from './error'\nimport { FeatureGatesClient } from './feature-gates'\nimport { Hydrator } from './hydration/hydrator'\nimport * as imageServer from './image/server'\nimport { ImageUriBuilder } from './image/uri'\nimport { createKwsClient } from './kws'\nimport { createServer } from './lexicon'\nimport { loggerMiddleware } from './logger'\nimport { authWithApiKey as rolodexAuth, createRolodexClient } from './rolodex'\nimport { createStashClient } from './stash'\nimport { Views } from './views'\nimport { VideoUriBuilder } from './views/util'\n\nexport { ServerConfig } from './config'\nexport type { ServerConfigValues } from './config'\nexport { AppContext } from './context'\nexport * from './data-plane'\nexport { BackgroundQueue } from './data-plane/server/background'\nexport { Database } from './data-plane/server/db'\nexport { Redis } from './redis'\n\nexport class BskyAppView {\n  public ctx: AppContext\n  public app: express.Application\n  public server?: http.Server\n  private terminator?: HttpTerminator\n\n  constructor(opts: { ctx: AppContext; app: express.Application }) {\n    this.ctx = opts.ctx\n    this.app = opts.app\n  }\n\n  static create(opts: {\n    config: ServerConfig\n    signingKey: Keypair\n  }): BskyAppView {\n    const { config, signingKey } = opts\n    const app = express()\n    app.set('trust proxy', true)\n    app.use(cors({ maxAge: DAY / SECOND }))\n    app.use(loggerMiddleware)\n    app.use(compression())\n\n    // used solely for handle resolution: identity lookups occur on dataplane\n    const idResolver = new IdResolver({\n      plcUrl: config.didPlcUrl,\n      backupNameservers: config.handleResolveNameservers,\n    })\n    const imgUriBuilder = new ImageUriBuilder(\n      config.cdnUrl || `${config.publicUrl}/img`,\n    )\n    const videoUriBuilder = new VideoUriBuilder({\n      playlistUrlPattern:\n        config.videoPlaylistUrlPattern ||\n        `${config.publicUrl}/vid/%s/%s/playlist.m3u8`,\n      thumbnailUrlPattern:\n        config.videoThumbnailUrlPattern ||\n        `${config.publicUrl}/vid/%s/%s/thumbnail.jpg`,\n    })\n\n    const searchAgent = config.searchUrl\n      ? new AtpAgent({ service: config.searchUrl })\n      : undefined\n\n    const suggestionsAgent = config.suggestionsUrl\n      ? new AtpAgent({ service: config.suggestionsUrl })\n      : undefined\n    if (suggestionsAgent && config.suggestionsApiKey) {\n      suggestionsAgent.api.setHeader(\n        'authorization',\n        `Bearer ${config.suggestionsApiKey}`,\n      )\n    }\n\n    const topicsAgent = config.topicsUrl\n      ? new AtpAgent({ service: config.topicsUrl })\n      : undefined\n    if (topicsAgent && config.topicsApiKey) {\n      topicsAgent.api.setHeader(\n        'authorization',\n        `Bearer ${config.topicsApiKey}`,\n      )\n    }\n\n    const etcd = config.etcdHosts.length\n      ? new Etcd3({ hosts: config.etcdHosts })\n      : undefined\n\n    const dataplaneHostList =\n      etcd && config.dataplaneUrlsEtcdKeyPrefix\n        ? new EtcdHostList(\n            etcd,\n            config.dataplaneUrlsEtcdKeyPrefix,\n            config.dataplaneUrls,\n          )\n        : new BasicHostList(config.dataplaneUrls)\n\n    const featureGatesClient = new FeatureGatesClient({\n      growthBookApiHost: config.growthBookApiHost,\n      growthBookClientKey: config.growthBookClientKey,\n      eventProxyTrackingEndpoint: config.eventProxyTrackingEndpoint,\n    })\n\n    const dataplane = createDataPlaneClient(dataplaneHostList, {\n      httpVersion: config.dataplaneHttpVersion,\n      rejectUnauthorized: !config.dataplaneIgnoreBadTls,\n    })\n    const hydrator = new Hydrator(dataplane, config.labelsFromIssuerDids, {\n      debugFieldAllowedDids: config.debugFieldAllowedDids,\n      featureGatesClient,\n    })\n    const views = new Views({\n      imgUriBuilder: imgUriBuilder,\n      videoUriBuilder: videoUriBuilder,\n      indexedAtEpoch: config.indexedAtEpoch,\n      threadTagsBumpDown: [...config.threadTagsBumpDown],\n      threadTagsHide: [...config.threadTagsHide],\n      visibilityTagHide: config.visibilityTagHide,\n      visibilityTagRankPrefix: config.visibilityTagRankPrefix,\n    })\n\n    const bsyncClient = createBsyncClient({\n      baseUrl: config.bsyncUrl,\n      httpVersion: config.bsyncHttpVersion ?? '2',\n      nodeOptions: { rejectUnauthorized: !config.bsyncIgnoreBadTls },\n      interceptors: config.bsyncApiKey ? [bsyncAuth(config.bsyncApiKey)] : [],\n    })\n\n    const stashClient = createStashClient(bsyncClient)\n\n    const courierClient = config.courierUrl\n      ? createCourierClient({\n          baseUrl: config.courierUrl,\n          httpVersion: config.courierHttpVersion ?? '2',\n          nodeOptions: { rejectUnauthorized: !config.courierIgnoreBadTls },\n          interceptors: config.courierApiKey\n            ? [courierAuth(config.courierApiKey)]\n            : [],\n        })\n      : undefined\n\n    const rolodexClient = config.rolodexUrl\n      ? createRolodexClient({\n          baseUrl: config.rolodexUrl,\n          httpVersion: config.rolodexHttpVersion ?? '2',\n          nodeOptions: { rejectUnauthorized: !config.rolodexIgnoreBadTls },\n          interceptors: config.rolodexApiKey\n            ? [rolodexAuth(config.rolodexApiKey)]\n            : [],\n        })\n      : undefined\n\n    const kwsClient = config.kws ? createKwsClient(config.kws) : undefined\n\n    const entrywayJwtPublicKey = config.entrywayJwtPublicKeyHex\n      ? createPublicKeyObject(config.entrywayJwtPublicKeyHex)\n      : undefined\n    const authVerifier = new AuthVerifier(dataplane, {\n      ownDid: config.serverDid,\n      alternateAudienceDids: config.alternateAudienceDids,\n      modServiceDid: config.modServiceDid,\n      adminPasses: config.adminPasswords,\n      entrywayJwtPublicKey,\n    })\n\n    const blobDispatcher = createBlobDispatcher(config)\n\n    const ctx = new AppContext({\n      cfg: config,\n      etcd,\n      dataplane,\n      dataplaneHostList,\n      searchAgent,\n      suggestionsAgent,\n      topicsAgent,\n      hydrator,\n      views,\n      signingKey,\n      idResolver,\n      bsyncClient,\n      stashClient,\n      courierClient,\n      rolodexClient,\n      authVerifier,\n      featureGatesClient,\n      blobDispatcher,\n      kwsClient,\n    })\n\n    let server = createServer({\n      validateResponse: config.debugMode,\n      payload: {\n        jsonLimit: 100 * 1024, // 100kb\n        textLimit: 100 * 1024, // 100kb\n        blobLimit: 5 * 1024 * 1024, // 5mb\n      },\n    })\n\n    server = API(server, ctx)\n\n    app.use(health.createRouter(ctx))\n    app.use(wellKnown.createRouter(ctx))\n    app.use(blobResolver.createMiddleware(ctx))\n    app.use(imageServer.createMiddleware(ctx, { prefix: '/img/' }))\n\n    if (config.dataplaneUrls.length > 0 || config.dataplaneUrlsEtcdKeyPrefix) {\n      app.use(sitemap.createRouter(ctx))\n    }\n\n    app.use(server.xrpc.router)\n    app.use(error.handler)\n    app.use('/external', external.createRouter(ctx))\n\n    return new BskyAppView({ ctx, app })\n  }\n\n  async start(): Promise<http.Server> {\n    if (this.ctx.dataplaneHostList instanceof EtcdHostList) {\n      await this.ctx.dataplaneHostList.connect()\n    }\n    this.ctx.featureGatesClient.start() // lazy, no await\n    const server = this.app.listen(this.ctx.cfg.port)\n    this.server = server\n    server.keepAliveTimeout = 90000\n    this.terminator = createHttpTerminator({ server })\n    await events.once(server, 'listening')\n    const { port } = server.address() as AddressInfo\n    this.ctx.cfg.assignPort(port)\n    return server\n  }\n\n  async destroy(): Promise<void> {\n    this.ctx.featureGatesClient.destroy()\n    await this.terminator?.terminate()\n    await this.ctx.etcd?.close()\n  }\n}\n\nexport default BskyAppView\n"
  },
  {
    "path": "packages/bsky/src/kws.ts",
    "content": "import { z } from 'zod'\nimport { KwsExternalPayload } from './api/kws/types'\nimport { serializeExternalPayload } from './api/kws/util'\nimport { buildBasicAuth } from './auth-verifier'\nimport { KwsConfig } from './config'\nimport { httpLogger as log } from './logger'\n\nexport const createKwsClient = (cfg: KwsConfig): KwsClient => {\n  return new KwsClient(cfg)\n}\n\n// Not `.strict()` to avoid breaking if KWS adds fields.\nconst authResponseSchema = z.object({\n  access_token: z.string(),\n})\n\nconst EXTERNAL_PAYLOAD_CHAR_LIMIT = 200\n/**\n * Thrown when the provided external payload exceeds KWS's character limit.\n * This is most commonly caused by DIDs that are too long, such as for\n * `did:web` DIDs. But it's very rare, and the client has special handling for\n * this case.\n */\nexport class KwsExternalPayloadError extends Error {}\n\nexport type KWSSendEmailRequestCommon = {\n  email: string\n  location: string\n  language: string\n  externalPayload: string\n}\n\nexport type KWSSendEmailRequest =\n  | (KWSSendEmailRequestCommon & {\n      userContext: 'adult'\n    })\n  | (KWSSendEmailRequestCommon & {\n      userContext: 'age'\n      minimumAge: number\n    })\n\nexport class KwsClient {\n  constructor(public cfg: KwsConfig) {}\n\n  private async auth() {\n    try {\n      const res = await fetch(\n        `${this.cfg.authOrigin}/auth/realms/kws/protocol/openid-connect/token`,\n        {\n          method: 'POST',\n          headers: {\n            Accept: 'application/json',\n            'Content-Type': 'application/x-www-form-urlencoded',\n            Authorization: buildBasicAuth(this.cfg.clientId, this.cfg.apiKey),\n          },\n          body: new URLSearchParams({\n            grant_type: 'client_credentials',\n            scope: 'verification',\n          }),\n        },\n      )\n      if (!res.ok) {\n        const errorText = await res.text()\n        throw new Error(\n          `Failed to fetch age assurance access token: status: ${res.status}, statusText: ${res.statusText}, errorText: ${errorText}`,\n        )\n      }\n\n      const auth = await res.json()\n      const authResponse = authResponseSchema.parse(auth)\n      return authResponse.access_token\n    } catch (err) {\n      log.error({ err }, 'Failed to authenticate with KWS')\n      throw err\n    }\n  }\n\n  private async fetchWithAuth(\n    url: string,\n    init: RequestInit,\n  ): Promise<Response> {\n    const accessToken = await this.auth()\n\n    return fetch(url, {\n      ...init,\n      headers: {\n        ...(init.headers ?? {}),\n        Authorization: `Bearer ${accessToken}`,\n      },\n    })\n  }\n\n  /**\n   * @deprecated Use `sendAdultVerifiedFlowEmail` or `sendAgeVerifiedFlowEmail` instead.\n   */\n  async sendEmail({\n    countryCode,\n    email,\n    externalPayload,\n    language,\n  }: {\n    countryCode: string\n    email: string\n    externalPayload: KwsExternalPayload\n    language: string\n  }) {\n    const serializedExternalPayload = serializeExternalPayload(externalPayload)\n    if (serializedExternalPayload.length > EXTERNAL_PAYLOAD_CHAR_LIMIT) {\n      throw new KwsExternalPayloadError()\n    }\n\n    const res = await this.fetchWithAuth(\n      `${this.cfg.apiOrigin}/v1/verifications/send-email`,\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'User-Agent': this.cfg.userAgent,\n        },\n        body: JSON.stringify({\n          email,\n          externalPayload: serializedExternalPayload,\n          language,\n          location: countryCode,\n          userContext: 'adult',\n        }),\n      },\n    )\n\n    if (!res.ok) {\n      const errorText = await res.text()\n      log.error(\n        { status: res.status, statusText: res.statusText, errorText },\n        'Failed to send age assurance email',\n      )\n      throw new Error('Failed to send age assurance email')\n    }\n\n    return res.json()\n  }\n\n  /**\n   * Sends a KWS verification email with the given properties.\n   */\n  async email(props: KWSSendEmailRequest) {\n    const res = await this.fetchWithAuth(\n      `${this.cfg.apiOrigin}/v1/verifications/send-email`,\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'User-Agent': this.cfg.userAgent,\n        },\n        body: JSON.stringify(props),\n      },\n    )\n\n    if (!res.ok) {\n      const errorText = await res.text()\n      log.error(\n        {\n          status: res.status,\n          statusText: res.statusText,\n          errorText,\n          flow: props.userContext,\n        },\n        'Failed to send KWS email',\n      )\n      throw new Error('Failed to send KWS email')\n    }\n\n    return res.json()\n  }\n\n  /**\n   * Sends an email to the user initiating an `adult` verification flow, which\n   * results in `adult-verified` events/webhooks.\n   */\n  async sendAdultVerifiedFlowEmail(props: KWSSendEmailRequestCommon) {\n    return this.email({\n      ...props,\n      userContext: 'adult',\n    })\n  }\n\n  /**\n   * Sends an email to the user initiating an `age` verification flow, which\n   * results in `age-verified` events/webhooks.\n   */\n  async sendAgeVerifiedFlowEmail(props: KWSSendEmailRequestCommon) {\n    return this.email({\n      ...props,\n      userContext: 'age',\n      minimumAge: 16, // KWS required value\n    })\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/index.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type Auth,\n  type Options as XrpcOptions,\n  Server as XrpcServer,\n  type StreamConfigOrHandler,\n  type MethodConfigOrHandler,\n  createServer as createXrpcServer,\n} from '@atproto/xrpc-server'\nimport { schemas } from './lexicons.js'\nimport * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences.js'\nimport * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile.js'\nimport * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles.js'\nimport * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions.js'\nimport * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences.js'\nimport * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors.js'\nimport * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead.js'\nimport * as AppBskyAgeassuranceBegin from './types/app/bsky/ageassurance/begin.js'\nimport * as AppBskyAgeassuranceGetConfig from './types/app/bsky/ageassurance/getConfig.js'\nimport * as AppBskyAgeassuranceGetState from './types/app/bsky/ageassurance/getState.js'\nimport * as AppBskyBookmarkCreateBookmark from './types/app/bsky/bookmark/createBookmark.js'\nimport * as AppBskyBookmarkDeleteBookmark from './types/app/bsky/bookmark/deleteBookmark.js'\nimport * as AppBskyBookmarkGetBookmarks from './types/app/bsky/bookmark/getBookmarks.js'\nimport * as AppBskyContactDismissMatch from './types/app/bsky/contact/dismissMatch.js'\nimport * as AppBskyContactGetMatches from './types/app/bsky/contact/getMatches.js'\nimport * as AppBskyContactGetSyncStatus from './types/app/bsky/contact/getSyncStatus.js'\nimport * as AppBskyContactImportContacts from './types/app/bsky/contact/importContacts.js'\nimport * as AppBskyContactRemoveData from './types/app/bsky/contact/removeData.js'\nimport * as AppBskyContactSendNotification from './types/app/bsky/contact/sendNotification.js'\nimport * as AppBskyContactStartPhoneVerification from './types/app/bsky/contact/startPhoneVerification.js'\nimport * as AppBskyContactVerifyPhone from './types/app/bsky/contact/verifyPhone.js'\nimport * as AppBskyDraftCreateDraft from './types/app/bsky/draft/createDraft.js'\nimport * as AppBskyDraftDeleteDraft from './types/app/bsky/draft/deleteDraft.js'\nimport * as AppBskyDraftGetDrafts from './types/app/bsky/draft/getDrafts.js'\nimport * as AppBskyDraftUpdateDraft from './types/app/bsky/draft/updateDraft.js'\nimport * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator.js'\nimport * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds.js'\nimport * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes.js'\nimport * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed.js'\nimport * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed.js'\nimport * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator.js'\nimport * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators.js'\nimport * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'\nimport * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'\nimport * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'\nimport * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'\nimport * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'\nimport * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'\nimport * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'\nimport * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'\nimport * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline.js'\nimport * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts.js'\nimport * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions.js'\nimport * as AppBskyGraphGetActorStarterPacks from './types/app/bsky/graph/getActorStarterPacks.js'\nimport * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks.js'\nimport * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers.js'\nimport * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows.js'\nimport * as AppBskyGraphGetKnownFollowers from './types/app/bsky/graph/getKnownFollowers.js'\nimport * as AppBskyGraphGetList from './types/app/bsky/graph/getList.js'\nimport * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks.js'\nimport * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes.js'\nimport * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists.js'\nimport * as AppBskyGraphGetListsWithMembership from './types/app/bsky/graph/getListsWithMembership.js'\nimport * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes.js'\nimport * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelationships.js'\nimport * as AppBskyGraphGetStarterPack from './types/app/bsky/graph/getStarterPack.js'\nimport * as AppBskyGraphGetStarterPacks from './types/app/bsky/graph/getStarterPacks.js'\nimport * as AppBskyGraphGetStarterPacksWithMembership from './types/app/bsky/graph/getStarterPacksWithMembership.js'\nimport * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor.js'\nimport * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor.js'\nimport * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList.js'\nimport * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread.js'\nimport * as AppBskyGraphSearchStarterPacks from './types/app/bsky/graph/searchStarterPacks.js'\nimport * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'\nimport * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'\nimport * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'\nimport * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'\nimport * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'\nimport * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'\nimport * as AppBskyNotificationListActivitySubscriptions from './types/app/bsky/notification/listActivitySubscriptions.js'\nimport * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'\nimport * as AppBskyNotificationPutActivitySubscription from './types/app/bsky/notification/putActivitySubscription.js'\nimport * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'\nimport * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'\nimport * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'\nimport * as AppBskyNotificationUnregisterPush from './types/app/bsky/notification/unregisterPush.js'\nimport * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'\nimport * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'\nimport * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacks from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'\nimport * as AppBskyUnspeccedGetPostThreadOtherV2 from './types/app/bsky/unspecced/getPostThreadOtherV2.js'\nimport * as AppBskyUnspeccedGetPostThreadV2 from './types/app/bsky/unspecced/getPostThreadV2.js'\nimport * as AppBskyUnspeccedGetSuggestedFeeds from './types/app/bsky/unspecced/getSuggestedFeeds.js'\nimport * as AppBskyUnspeccedGetSuggestedFeedsSkeleton from './types/app/bsky/unspecced/getSuggestedFeedsSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedOnboardingUsers from './types/app/bsky/unspecced/getSuggestedOnboardingUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacks from './types/app/bsky/unspecced/getSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedUsers from './types/app/bsky/unspecced/getSuggestedUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedUsersSkeleton from './types/app/bsky/unspecced/getSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspecced/getSuggestionsSkeleton.js'\nimport * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions.js'\nimport * as AppBskyUnspeccedGetTrendingTopics from './types/app/bsky/unspecced/getTrendingTopics.js'\nimport * as AppBskyUnspeccedGetTrends from './types/app/bsky/unspecced/getTrends.js'\nimport * as AppBskyUnspeccedGetTrendsSkeleton from './types/app/bsky/unspecced/getTrendsSkeleton.js'\nimport * as AppBskyUnspeccedInitAgeAssurance from './types/app/bsky/unspecced/initAgeAssurance.js'\nimport * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton.js'\nimport * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton.js'\nimport * as AppBskyUnspeccedSearchStarterPacksSkeleton from './types/app/bsky/unspecced/searchStarterPacksSkeleton.js'\nimport * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus.js'\nimport * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits.js'\nimport * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo.js'\nimport * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount.js'\nimport * as ChatBskyActorExportAccountData from './types/chat/bsky/actor/exportAccountData.js'\nimport * as ChatBskyConvoAcceptConvo from './types/chat/bsky/convo/acceptConvo.js'\nimport * as ChatBskyConvoAddReaction from './types/chat/bsky/convo/addReaction.js'\nimport * as ChatBskyConvoDeleteMessageForSelf from './types/chat/bsky/convo/deleteMessageForSelf.js'\nimport * as ChatBskyConvoGetConvo from './types/chat/bsky/convo/getConvo.js'\nimport * as ChatBskyConvoGetConvoAvailability from './types/chat/bsky/convo/getConvoAvailability.js'\nimport * as ChatBskyConvoGetConvoForMembers from './types/chat/bsky/convo/getConvoForMembers.js'\nimport * as ChatBskyConvoGetLog from './types/chat/bsky/convo/getLog.js'\nimport * as ChatBskyConvoGetMessages from './types/chat/bsky/convo/getMessages.js'\nimport * as ChatBskyConvoLeaveConvo from './types/chat/bsky/convo/leaveConvo.js'\nimport * as ChatBskyConvoListConvos from './types/chat/bsky/convo/listConvos.js'\nimport * as ChatBskyConvoMuteConvo from './types/chat/bsky/convo/muteConvo.js'\nimport * as ChatBskyConvoRemoveReaction from './types/chat/bsky/convo/removeReaction.js'\nimport * as ChatBskyConvoSendMessage from './types/chat/bsky/convo/sendMessage.js'\nimport * as ChatBskyConvoSendMessageBatch from './types/chat/bsky/convo/sendMessageBatch.js'\nimport * as ChatBskyConvoUnmuteConvo from './types/chat/bsky/convo/unmuteConvo.js'\nimport * as ChatBskyConvoUpdateAllRead from './types/chat/bsky/convo/updateAllRead.js'\nimport * as ChatBskyConvoUpdateRead from './types/chat/bsky/convo/updateRead.js'\nimport * as ChatBskyModerationGetActorMetadata from './types/chat/bsky/moderation/getActorMetadata.js'\nimport * as ChatBskyModerationGetMessageContext from './types/chat/bsky/moderation/getMessageContext.js'\nimport * as ChatBskyModerationUpdateActorAccess from './types/chat/bsky/moderation/updateActorAccess.js'\nimport * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount.js'\nimport * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites.js'\nimport * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes.js'\nimport * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites.js'\nimport * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo.js'\nimport * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos.js'\nimport * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes.js'\nimport * as ComAtprotoAdminGetSubjectStatus from './types/com/atproto/admin/getSubjectStatus.js'\nimport * as ComAtprotoAdminSearchAccounts from './types/com/atproto/admin/searchAccounts.js'\nimport * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail.js'\nimport * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail.js'\nimport * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle.js'\nimport * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword.js'\nimport * as ComAtprotoAdminUpdateAccountSigningKey from './types/com/atproto/admin/updateAccountSigningKey.js'\nimport * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus.js'\nimport * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials.js'\nimport * as ComAtprotoIdentityRefreshIdentity from './types/com/atproto/identity/refreshIdentity.js'\nimport * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature.js'\nimport * as ComAtprotoIdentityResolveDid from './types/com/atproto/identity/resolveDid.js'\nimport * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle.js'\nimport * as ComAtprotoIdentityResolveIdentity from './types/com/atproto/identity/resolveIdentity.js'\nimport * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation.js'\nimport * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation.js'\nimport * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle.js'\nimport * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels.js'\nimport * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels.js'\nimport * as ComAtprotoLexiconResolveLexicon from './types/com/atproto/lexicon/resolveLexicon.js'\nimport * as ComAtprotoModerationCreateReport from './types/com/atproto/moderation/createReport.js'\nimport * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'\nimport * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js'\nimport * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js'\nimport * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js'\nimport * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js'\nimport * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js'\nimport * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js'\nimport * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js'\nimport * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js'\nimport * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'\nimport * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount.js'\nimport * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus.js'\nimport * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail.js'\nimport * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount.js'\nimport * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword.js'\nimport * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode.js'\nimport * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes.js'\nimport * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession.js'\nimport * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount.js'\nimport * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount.js'\nimport * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession.js'\nimport * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer.js'\nimport * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes.js'\nimport * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth.js'\nimport * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession.js'\nimport * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords.js'\nimport * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession.js'\nimport * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete.js'\nimport * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation.js'\nimport * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate.js'\nimport * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset.js'\nimport * as ComAtprotoServerReserveSigningKey from './types/com/atproto/server/reserveSigningKey.js'\nimport * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword.js'\nimport * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword.js'\nimport * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail.js'\nimport * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob.js'\nimport * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks.js'\nimport * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout.js'\nimport * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead.js'\nimport * as ComAtprotoSyncGetHostStatus from './types/com/atproto/sync/getHostStatus.js'\nimport * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit.js'\nimport * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord.js'\nimport * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo.js'\nimport * as ComAtprotoSyncGetRepoStatus from './types/com/atproto/sync/getRepoStatus.js'\nimport * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs.js'\nimport * as ComAtprotoSyncListHosts from './types/com/atproto/sync/listHosts.js'\nimport * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos.js'\nimport * as ComAtprotoSyncListReposByCollection from './types/com/atproto/sync/listReposByCollection.js'\nimport * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate.js'\nimport * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl.js'\nimport * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos.js'\nimport * as ComAtprotoTempAddReservedHandle from './types/com/atproto/temp/addReservedHandle.js'\nimport * as ComAtprotoTempCheckHandleAvailability from './types/com/atproto/temp/checkHandleAvailability.js'\nimport * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue.js'\nimport * as ComAtprotoTempDereferenceScope from './types/com/atproto/temp/dereferenceScope.js'\nimport * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels.js'\nimport * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification.js'\nimport * as ComAtprotoTempRevokeAccountCredentials from './types/com/atproto/temp/revokeAccountCredentials.js'\n\nexport const APP_BSKY_ACTOR = {\n  StatusLive: 'app.bsky.actor.status#live',\n}\nexport const APP_BSKY_FEED = {\n  DefsRequestLess: 'app.bsky.feed.defs#requestLess',\n  DefsRequestMore: 'app.bsky.feed.defs#requestMore',\n  DefsClickthroughItem: 'app.bsky.feed.defs#clickthroughItem',\n  DefsClickthroughAuthor: 'app.bsky.feed.defs#clickthroughAuthor',\n  DefsClickthroughReposter: 'app.bsky.feed.defs#clickthroughReposter',\n  DefsClickthroughEmbed: 'app.bsky.feed.defs#clickthroughEmbed',\n  DefsContentModeUnspecified: 'app.bsky.feed.defs#contentModeUnspecified',\n  DefsContentModeVideo: 'app.bsky.feed.defs#contentModeVideo',\n  DefsInteractionSeen: 'app.bsky.feed.defs#interactionSeen',\n  DefsInteractionLike: 'app.bsky.feed.defs#interactionLike',\n  DefsInteractionRepost: 'app.bsky.feed.defs#interactionRepost',\n  DefsInteractionReply: 'app.bsky.feed.defs#interactionReply',\n  DefsInteractionQuote: 'app.bsky.feed.defs#interactionQuote',\n  DefsInteractionShare: 'app.bsky.feed.defs#interactionShare',\n}\nexport const APP_BSKY_GRAPH = {\n  DefsModlist: 'app.bsky.graph.defs#modlist',\n  DefsCuratelist: 'app.bsky.graph.defs#curatelist',\n  DefsReferencelist: 'app.bsky.graph.defs#referencelist',\n}\nexport const COM_ATPROTO_MODERATION = {\n  DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam',\n  DefsReasonViolation: 'com.atproto.moderation.defs#reasonViolation',\n  DefsReasonMisleading: 'com.atproto.moderation.defs#reasonMisleading',\n  DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',\n  DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',\n  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',\n  DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',\n}\n\nexport function createServer(options?: XrpcOptions): Server {\n  return new Server(options)\n}\n\nexport class Server {\n  xrpc: XrpcServer\n  app: AppNS\n  chat: ChatNS\n  com: ComNS\n\n  constructor(options?: XrpcOptions) {\n    this.xrpc = createXrpcServer(schemas, options)\n    this.app = new AppNS(this)\n    this.chat = new ChatNS(this)\n    this.com = new ComNS(this)\n  }\n}\n\nexport class AppNS {\n  _server: Server\n  bsky: AppBskyNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.bsky = new AppBskyNS(server)\n  }\n}\n\nexport class AppBskyNS {\n  _server: Server\n  actor: AppBskyActorNS\n  ageassurance: AppBskyAgeassuranceNS\n  bookmark: AppBskyBookmarkNS\n  contact: AppBskyContactNS\n  draft: AppBskyDraftNS\n  embed: AppBskyEmbedNS\n  feed: AppBskyFeedNS\n  graph: AppBskyGraphNS\n  labeler: AppBskyLabelerNS\n  notification: AppBskyNotificationNS\n  richtext: AppBskyRichtextNS\n  unspecced: AppBskyUnspeccedNS\n  video: AppBskyVideoNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.actor = new AppBskyActorNS(server)\n    this.ageassurance = new AppBskyAgeassuranceNS(server)\n    this.bookmark = new AppBskyBookmarkNS(server)\n    this.contact = new AppBskyContactNS(server)\n    this.draft = new AppBskyDraftNS(server)\n    this.embed = new AppBskyEmbedNS(server)\n    this.feed = new AppBskyFeedNS(server)\n    this.graph = new AppBskyGraphNS(server)\n    this.labeler = new AppBskyLabelerNS(server)\n    this.notification = new AppBskyNotificationNS(server)\n    this.richtext = new AppBskyRichtextNS(server)\n    this.unspecced = new AppBskyUnspeccedNS(server)\n    this.video = new AppBskyVideoNS(server)\n  }\n}\n\nexport class AppBskyActorNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetPreferences.QueryParams,\n      AppBskyActorGetPreferences.HandlerInput,\n      AppBskyActorGetPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getProfile<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetProfile.QueryParams,\n      AppBskyActorGetProfile.HandlerInput,\n      AppBskyActorGetProfile.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getProfile' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getProfiles<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetProfiles.QueryParams,\n      AppBskyActorGetProfiles.HandlerInput,\n      AppBskyActorGetProfiles.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getProfiles' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetSuggestions.QueryParams,\n      AppBskyActorGetSuggestions.HandlerInput,\n      AppBskyActorGetSuggestions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getSuggestions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorPutPreferences.QueryParams,\n      AppBskyActorPutPreferences.HandlerInput,\n      AppBskyActorPutPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.putPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActors<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorSearchActors.QueryParams,\n      AppBskyActorSearchActors.HandlerInput,\n      AppBskyActorSearchActors.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.searchActors' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActorsTypeahead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorSearchActorsTypeahead.QueryParams,\n      AppBskyActorSearchActorsTypeahead.HandlerInput,\n      AppBskyActorSearchActorsTypeahead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.searchActorsTypeahead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyAgeassuranceNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  begin<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceBegin.QueryParams,\n      AppBskyAgeassuranceBegin.HandlerInput,\n      AppBskyAgeassuranceBegin.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.begin' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceGetConfig.QueryParams,\n      AppBskyAgeassuranceGetConfig.HandlerInput,\n      AppBskyAgeassuranceGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getState<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceGetState.QueryParams,\n      AppBskyAgeassuranceGetState.HandlerInput,\n      AppBskyAgeassuranceGetState.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.getState' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyBookmarkNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createBookmark<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkCreateBookmark.QueryParams,\n      AppBskyBookmarkCreateBookmark.HandlerInput,\n      AppBskyBookmarkCreateBookmark.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.createBookmark' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteBookmark<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkDeleteBookmark.QueryParams,\n      AppBskyBookmarkDeleteBookmark.HandlerInput,\n      AppBskyBookmarkDeleteBookmark.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.deleteBookmark' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBookmarks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkGetBookmarks.QueryParams,\n      AppBskyBookmarkGetBookmarks.HandlerInput,\n      AppBskyBookmarkGetBookmarks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.getBookmarks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyContactNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  dismissMatch<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactDismissMatch.QueryParams,\n      AppBskyContactDismissMatch.HandlerInput,\n      AppBskyContactDismissMatch.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.dismissMatch' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMatches<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactGetMatches.QueryParams,\n      AppBskyContactGetMatches.HandlerInput,\n      AppBskyContactGetMatches.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.getMatches' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSyncStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactGetSyncStatus.QueryParams,\n      AppBskyContactGetSyncStatus.HandlerInput,\n      AppBskyContactGetSyncStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.getSyncStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  importContacts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactImportContacts.QueryParams,\n      AppBskyContactImportContacts.HandlerInput,\n      AppBskyContactImportContacts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.importContacts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeData<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactRemoveData.QueryParams,\n      AppBskyContactRemoveData.HandlerInput,\n      AppBskyContactRemoveData.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.removeData' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendNotification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactSendNotification.QueryParams,\n      AppBskyContactSendNotification.HandlerInput,\n      AppBskyContactSendNotification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.sendNotification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  startPhoneVerification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactStartPhoneVerification.QueryParams,\n      AppBskyContactStartPhoneVerification.HandlerInput,\n      AppBskyContactStartPhoneVerification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.startPhoneVerification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  verifyPhone<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactVerifyPhone.QueryParams,\n      AppBskyContactVerifyPhone.HandlerInput,\n      AppBskyContactVerifyPhone.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.verifyPhone' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyDraftNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftCreateDraft.QueryParams,\n      AppBskyDraftCreateDraft.HandlerInput,\n      AppBskyDraftCreateDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.createDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftDeleteDraft.QueryParams,\n      AppBskyDraftDeleteDraft.HandlerInput,\n      AppBskyDraftDeleteDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.deleteDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getDrafts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftGetDrafts.QueryParams,\n      AppBskyDraftGetDrafts.HandlerInput,\n      AppBskyDraftGetDrafts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.getDrafts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftUpdateDraft.QueryParams,\n      AppBskyDraftUpdateDraft.HandlerInput,\n      AppBskyDraftUpdateDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.updateDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyEmbedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n\nexport class AppBskyFeedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  describeFeedGenerator<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedDescribeFeedGenerator.QueryParams,\n      AppBskyFeedDescribeFeedGenerator.HandlerInput,\n      AppBskyFeedDescribeFeedGenerator.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getActorFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetActorFeeds.QueryParams,\n      AppBskyFeedGetActorFeeds.HandlerInput,\n      AppBskyFeedGetActorFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getActorFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getActorLikes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetActorLikes.QueryParams,\n      AppBskyFeedGetActorLikes.HandlerInput,\n      AppBskyFeedGetActorLikes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAuthorFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetAuthorFeed.QueryParams,\n      AppBskyFeedGetAuthorFeed.HandlerInput,\n      AppBskyFeedGetAuthorFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeed.QueryParams,\n      AppBskyFeedGetFeed.HandlerInput,\n      AppBskyFeedGetFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedGenerator<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedGenerator.QueryParams,\n      AppBskyFeedGetFeedGenerator.HandlerInput,\n      AppBskyFeedGetFeedGenerator.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedGenerator' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedGenerators<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedGenerators.QueryParams,\n      AppBskyFeedGetFeedGenerators.HandlerInput,\n      AppBskyFeedGetFeedGenerators.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedGenerators' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedSkeleton.QueryParams,\n      AppBskyFeedGetFeedSkeleton.HandlerInput,\n      AppBskyFeedGetFeedSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLikes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetLikes.QueryParams,\n      AppBskyFeedGetLikes.HandlerInput,\n      AppBskyFeedGetLikes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getLikes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetListFeed.QueryParams,\n      AppBskyFeedGetListFeed.HandlerInput,\n      AppBskyFeedGetListFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getListFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetPostThread.QueryParams,\n      AppBskyFeedGetPostThread.HandlerInput,\n      AppBskyFeedGetPostThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetPosts.QueryParams,\n      AppBskyFeedGetPosts.HandlerInput,\n      AppBskyFeedGetPosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getPosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getQuotes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetQuotes.QueryParams,\n      AppBskyFeedGetQuotes.HandlerInput,\n      AppBskyFeedGetQuotes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getQuotes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepostedBy<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetRepostedBy.QueryParams,\n      AppBskyFeedGetRepostedBy.HandlerInput,\n      AppBskyFeedGetRepostedBy.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getRepostedBy' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetSuggestedFeeds.QueryParams,\n      AppBskyFeedGetSuggestedFeeds.HandlerInput,\n      AppBskyFeedGetSuggestedFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getSuggestedFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTimeline<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetTimeline.QueryParams,\n      AppBskyFeedGetTimeline.HandlerInput,\n      AppBskyFeedGetTimeline.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchPosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedSearchPosts.QueryParams,\n      AppBskyFeedSearchPosts.HandlerInput,\n      AppBskyFeedSearchPosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.searchPosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendInteractions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedSendInteractions.QueryParams,\n      AppBskyFeedSendInteractions.HandlerInput,\n      AppBskyFeedSendInteractions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.sendInteractions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyGraphNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getActorStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetActorStarterPacks.QueryParams,\n      AppBskyGraphGetActorStarterPacks.HandlerInput,\n      AppBskyGraphGetActorStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getActorStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetBlocks.QueryParams,\n      AppBskyGraphGetBlocks.HandlerInput,\n      AppBskyGraphGetBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFollowers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetFollowers.QueryParams,\n      AppBskyGraphGetFollowers.HandlerInput,\n      AppBskyGraphGetFollowers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getFollowers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFollows<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetFollows.QueryParams,\n      AppBskyGraphGetFollows.HandlerInput,\n      AppBskyGraphGetFollows.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getFollows' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getKnownFollowers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetKnownFollowers.QueryParams,\n      AppBskyGraphGetKnownFollowers.HandlerInput,\n      AppBskyGraphGetKnownFollowers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getKnownFollowers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetList.QueryParams,\n      AppBskyGraphGetList.HandlerInput,\n      AppBskyGraphGetList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListBlocks.QueryParams,\n      AppBskyGraphGetListBlocks.HandlerInput,\n      AppBskyGraphGetListBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListMutes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListMutes.QueryParams,\n      AppBskyGraphGetListMutes.HandlerInput,\n      AppBskyGraphGetListMutes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListMutes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLists<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetLists.QueryParams,\n      AppBskyGraphGetLists.HandlerInput,\n      AppBskyGraphGetLists.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getLists' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListsWithMembership<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListsWithMembership.QueryParams,\n      AppBskyGraphGetListsWithMembership.HandlerInput,\n      AppBskyGraphGetListsWithMembership.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListsWithMembership' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMutes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetMutes.QueryParams,\n      AppBskyGraphGetMutes.HandlerInput,\n      AppBskyGraphGetMutes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getMutes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRelationships<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetRelationships.QueryParams,\n      AppBskyGraphGetRelationships.HandlerInput,\n      AppBskyGraphGetRelationships.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getRelationships' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPack<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPack.QueryParams,\n      AppBskyGraphGetStarterPack.HandlerInput,\n      AppBskyGraphGetStarterPack.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPack' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPacks.QueryParams,\n      AppBskyGraphGetStarterPacks.HandlerInput,\n      AppBskyGraphGetStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPacksWithMembership<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPacksWithMembership.QueryParams,\n      AppBskyGraphGetStarterPacksWithMembership.HandlerInput,\n      AppBskyGraphGetStarterPacksWithMembership.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPacksWithMembership' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFollowsByActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetSuggestedFollowsByActor.QueryParams,\n      AppBskyGraphGetSuggestedFollowsByActor.HandlerInput,\n      AppBskyGraphGetSuggestedFollowsByActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getSuggestedFollowsByActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteActor.QueryParams,\n      AppBskyGraphMuteActor.HandlerInput,\n      AppBskyGraphMuteActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteActorList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteActorList.QueryParams,\n      AppBskyGraphMuteActorList.HandlerInput,\n      AppBskyGraphMuteActorList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteActorList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteThread.QueryParams,\n      AppBskyGraphMuteThread.HandlerInput,\n      AppBskyGraphMuteThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphSearchStarterPacks.QueryParams,\n      AppBskyGraphSearchStarterPacks.HandlerInput,\n      AppBskyGraphSearchStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.searchStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteActor.QueryParams,\n      AppBskyGraphUnmuteActor.HandlerInput,\n      AppBskyGraphUnmuteActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteActorList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteActorList.QueryParams,\n      AppBskyGraphUnmuteActorList.HandlerInput,\n      AppBskyGraphUnmuteActorList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteThread.QueryParams,\n      AppBskyGraphUnmuteThread.HandlerInput,\n      AppBskyGraphUnmuteThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyLabelerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getServices<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyLabelerGetServices.QueryParams,\n      AppBskyLabelerGetServices.HandlerInput,\n      AppBskyLabelerGetServices.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.labeler.getServices' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyNotificationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationGetPreferences.QueryParams,\n      AppBskyNotificationGetPreferences.HandlerInput,\n      AppBskyNotificationGetPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.getPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getUnreadCount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationGetUnreadCount.QueryParams,\n      AppBskyNotificationGetUnreadCount.HandlerInput,\n      AppBskyNotificationGetUnreadCount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.getUnreadCount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listActivitySubscriptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationListActivitySubscriptions.QueryParams,\n      AppBskyNotificationListActivitySubscriptions.HandlerInput,\n      AppBskyNotificationListActivitySubscriptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.listActivitySubscriptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listNotifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationListNotifications.QueryParams,\n      AppBskyNotificationListNotifications.HandlerInput,\n      AppBskyNotificationListNotifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.listNotifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putActivitySubscription<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutActivitySubscription.QueryParams,\n      AppBskyNotificationPutActivitySubscription.HandlerInput,\n      AppBskyNotificationPutActivitySubscription.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putActivitySubscription' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutPreferences.QueryParams,\n      AppBskyNotificationPutPreferences.HandlerInput,\n      AppBskyNotificationPutPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferencesV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutPreferencesV2.QueryParams,\n      AppBskyNotificationPutPreferencesV2.HandlerInput,\n      AppBskyNotificationPutPreferencesV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putPreferencesV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  registerPush<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationRegisterPush.QueryParams,\n      AppBskyNotificationRegisterPush.HandlerInput,\n      AppBskyNotificationRegisterPush.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.registerPush' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unregisterPush<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationUnregisterPush.QueryParams,\n      AppBskyNotificationUnregisterPush.HandlerInput,\n      AppBskyNotificationUnregisterPush.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.unregisterPush' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateSeen<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationUpdateSeen.QueryParams,\n      AppBskyNotificationUpdateSeen.HandlerInput,\n      AppBskyNotificationUpdateSeen.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.updateSeen' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyRichtextNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n\nexport class AppBskyUnspeccedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getAgeAssuranceState<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetAgeAssuranceState.QueryParams,\n      AppBskyUnspeccedGetAgeAssuranceState.HandlerInput,\n      AppBskyUnspeccedGetAgeAssuranceState.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getAgeAssuranceState' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetConfig.QueryParams,\n      AppBskyUnspeccedGetConfig.HandlerInput,\n      AppBskyUnspeccedGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedUsersSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPopularFeedGenerators<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPopularFeedGenerators.QueryParams,\n      AppBskyUnspeccedGetPopularFeedGenerators.HandlerInput,\n      AppBskyUnspeccedGetPopularFeedGenerators.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThreadOtherV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPostThreadOtherV2.QueryParams,\n      AppBskyUnspeccedGetPostThreadOtherV2.HandlerInput,\n      AppBskyUnspeccedGetPostThreadOtherV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPostThreadOtherV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThreadV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPostThreadV2.QueryParams,\n      AppBskyUnspeccedGetPostThreadV2.HandlerInput,\n      AppBskyUnspeccedGetPostThreadV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPostThreadV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedFeeds.QueryParams,\n      AppBskyUnspeccedGetSuggestedFeeds.HandlerInput,\n      AppBskyUnspeccedGetSuggestedFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeedsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedFeedsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedOnboardingUsers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.QueryParams,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.HandlerInput,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedOnboardingUsers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedStarterPacks.QueryParams,\n      AppBskyUnspeccedGetSuggestedStarterPacks.HandlerInput,\n      AppBskyUnspeccedGetSuggestedStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedUsers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedUsers.QueryParams,\n      AppBskyUnspeccedGetSuggestedUsers.HandlerInput,\n      AppBskyUnspeccedGetSuggestedUsers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedUsers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedUsersSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedUsersSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestionsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestionsSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestionsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestionsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestionsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTaggedSuggestions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTaggedSuggestions.QueryParams,\n      AppBskyUnspeccedGetTaggedSuggestions.HandlerInput,\n      AppBskyUnspeccedGetTaggedSuggestions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrendingTopics<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrendingTopics.QueryParams,\n      AppBskyUnspeccedGetTrendingTopics.HandlerInput,\n      AppBskyUnspeccedGetTrendingTopics.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrendingTopics' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrends<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrends.QueryParams,\n      AppBskyUnspeccedGetTrends.HandlerInput,\n      AppBskyUnspeccedGetTrends.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrends' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrendsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrendsSkeleton.QueryParams,\n      AppBskyUnspeccedGetTrendsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetTrendsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrendsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  initAgeAssurance<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedInitAgeAssurance.QueryParams,\n      AppBskyUnspeccedInitAgeAssurance.HandlerInput,\n      AppBskyUnspeccedInitAgeAssurance.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.initAgeAssurance' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActorsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchActorsSkeleton.QueryParams,\n      AppBskyUnspeccedSearchActorsSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchActorsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchActorsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchPostsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchPostsSkeleton.QueryParams,\n      AppBskyUnspeccedSearchPostsSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchPostsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchPostsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyVideoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getJobStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoGetJobStatus.QueryParams,\n      AppBskyVideoGetJobStatus.HandlerInput,\n      AppBskyVideoGetJobStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.getJobStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getUploadLimits<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoGetUploadLimits.QueryParams,\n      AppBskyVideoGetUploadLimits.HandlerInput,\n      AppBskyVideoGetUploadLimits.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.getUploadLimits' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  uploadVideo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoUploadVideo.QueryParams,\n      AppBskyVideoUploadVideo.HandlerInput,\n      AppBskyVideoUploadVideo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.uploadVideo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatNS {\n  _server: Server\n  bsky: ChatBskyNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.bsky = new ChatBskyNS(server)\n  }\n}\n\nexport class ChatBskyNS {\n  _server: Server\n  actor: ChatBskyActorNS\n  convo: ChatBskyConvoNS\n  moderation: ChatBskyModerationNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.actor = new ChatBskyActorNS(server)\n    this.convo = new ChatBskyConvoNS(server)\n    this.moderation = new ChatBskyModerationNS(server)\n  }\n}\n\nexport class ChatBskyActorNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyActorDeleteAccount.QueryParams,\n      ChatBskyActorDeleteAccount.HandlerInput,\n      ChatBskyActorDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.actor.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  exportAccountData<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyActorExportAccountData.QueryParams,\n      ChatBskyActorExportAccountData.HandlerInput,\n      ChatBskyActorExportAccountData.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.actor.exportAccountData' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatBskyConvoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  acceptConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoAcceptConvo.QueryParams,\n      ChatBskyConvoAcceptConvo.HandlerInput,\n      ChatBskyConvoAcceptConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.acceptConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  addReaction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoAddReaction.QueryParams,\n      ChatBskyConvoAddReaction.HandlerInput,\n      ChatBskyConvoAddReaction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.addReaction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteMessageForSelf<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoDeleteMessageForSelf.QueryParams,\n      ChatBskyConvoDeleteMessageForSelf.HandlerInput,\n      ChatBskyConvoDeleteMessageForSelf.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.deleteMessageForSelf' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvo.QueryParams,\n      ChatBskyConvoGetConvo.HandlerInput,\n      ChatBskyConvoGetConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvoAvailability<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvoAvailability.QueryParams,\n      ChatBskyConvoGetConvoAvailability.HandlerInput,\n      ChatBskyConvoGetConvoAvailability.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvoAvailability' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvoForMembers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvoForMembers.QueryParams,\n      ChatBskyConvoGetConvoForMembers.HandlerInput,\n      ChatBskyConvoGetConvoForMembers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvoForMembers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLog<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetLog.QueryParams,\n      ChatBskyConvoGetLog.HandlerInput,\n      ChatBskyConvoGetLog.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getLog' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMessages<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetMessages.QueryParams,\n      ChatBskyConvoGetMessages.HandlerInput,\n      ChatBskyConvoGetMessages.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getMessages' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  leaveConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoLeaveConvo.QueryParams,\n      ChatBskyConvoLeaveConvo.HandlerInput,\n      ChatBskyConvoLeaveConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.leaveConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listConvos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoListConvos.QueryParams,\n      ChatBskyConvoListConvos.HandlerInput,\n      ChatBskyConvoListConvos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.listConvos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoMuteConvo.QueryParams,\n      ChatBskyConvoMuteConvo.HandlerInput,\n      ChatBskyConvoMuteConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.muteConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeReaction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoRemoveReaction.QueryParams,\n      ChatBskyConvoRemoveReaction.HandlerInput,\n      ChatBskyConvoRemoveReaction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.removeReaction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendMessage<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoSendMessage.QueryParams,\n      ChatBskyConvoSendMessage.HandlerInput,\n      ChatBskyConvoSendMessage.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.sendMessage' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendMessageBatch<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoSendMessageBatch.QueryParams,\n      ChatBskyConvoSendMessageBatch.HandlerInput,\n      ChatBskyConvoSendMessageBatch.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.sendMessageBatch' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUnmuteConvo.QueryParams,\n      ChatBskyConvoUnmuteConvo.HandlerInput,\n      ChatBskyConvoUnmuteConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.unmuteConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAllRead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUpdateAllRead.QueryParams,\n      ChatBskyConvoUpdateAllRead.HandlerInput,\n      ChatBskyConvoUpdateAllRead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.updateAllRead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateRead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUpdateRead.QueryParams,\n      ChatBskyConvoUpdateRead.HandlerInput,\n      ChatBskyConvoUpdateRead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.updateRead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatBskyModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getActorMetadata<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationGetActorMetadata.QueryParams,\n      ChatBskyModerationGetActorMetadata.HandlerInput,\n      ChatBskyModerationGetActorMetadata.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.getActorMetadata' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMessageContext<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationGetMessageContext.QueryParams,\n      ChatBskyModerationGetMessageContext.HandlerInput,\n      ChatBskyModerationGetMessageContext.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.getMessageContext' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateActorAccess<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationUpdateActorAccess.QueryParams,\n      ChatBskyModerationUpdateActorAccess.HandlerInput,\n      ChatBskyModerationUpdateActorAccess.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.updateActorAccess' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComNS {\n  _server: Server\n  atproto: ComAtprotoNS\n  germnetwork: ComGermnetworkNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.atproto = new ComAtprotoNS(server)\n    this.germnetwork = new ComGermnetworkNS(server)\n  }\n}\n\nexport class ComAtprotoNS {\n  _server: Server\n  admin: ComAtprotoAdminNS\n  identity: ComAtprotoIdentityNS\n  label: ComAtprotoLabelNS\n  lexicon: ComAtprotoLexiconNS\n  moderation: ComAtprotoModerationNS\n  repo: ComAtprotoRepoNS\n  server: ComAtprotoServerNS\n  sync: ComAtprotoSyncNS\n  temp: ComAtprotoTempNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.admin = new ComAtprotoAdminNS(server)\n    this.identity = new ComAtprotoIdentityNS(server)\n    this.label = new ComAtprotoLabelNS(server)\n    this.lexicon = new ComAtprotoLexiconNS(server)\n    this.moderation = new ComAtprotoModerationNS(server)\n    this.repo = new ComAtprotoRepoNS(server)\n    this.server = new ComAtprotoServerNS(server)\n    this.sync = new ComAtprotoSyncNS(server)\n    this.temp = new ComAtprotoTempNS(server)\n  }\n}\n\nexport class ComAtprotoAdminNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDeleteAccount.QueryParams,\n      ComAtprotoAdminDeleteAccount.HandlerInput,\n      ComAtprotoAdminDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  disableAccountInvites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDisableAccountInvites.QueryParams,\n      ComAtprotoAdminDisableAccountInvites.HandlerInput,\n      ComAtprotoAdminDisableAccountInvites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  disableInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDisableInviteCodes.QueryParams,\n      ComAtprotoAdminDisableInviteCodes.HandlerInput,\n      ComAtprotoAdminDisableInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.disableInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  enableAccountInvites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminEnableAccountInvites.QueryParams,\n      ComAtprotoAdminEnableAccountInvites.HandlerInput,\n      ComAtprotoAdminEnableAccountInvites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInfo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetAccountInfo.QueryParams,\n      ComAtprotoAdminGetAccountInfo.HandlerInput,\n      ComAtprotoAdminGetAccountInfo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getAccountInfo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInfos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetAccountInfos.QueryParams,\n      ComAtprotoAdminGetAccountInfos.HandlerInput,\n      ComAtprotoAdminGetAccountInfos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getAccountInfos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetInviteCodes.QueryParams,\n      ComAtprotoAdminGetInviteCodes.HandlerInput,\n      ComAtprotoAdminGetInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSubjectStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetSubjectStatus.QueryParams,\n      ComAtprotoAdminGetSubjectStatus.HandlerInput,\n      ComAtprotoAdminGetSubjectStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getSubjectStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminSearchAccounts.QueryParams,\n      ComAtprotoAdminSearchAccounts.HandlerInput,\n      ComAtprotoAdminSearchAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.searchAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminSendEmail.QueryParams,\n      ComAtprotoAdminSendEmail.HandlerInput,\n      ComAtprotoAdminSendEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountEmail.QueryParams,\n      ComAtprotoAdminUpdateAccountEmail.HandlerInput,\n      ComAtprotoAdminUpdateAccountEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountHandle.QueryParams,\n      ComAtprotoAdminUpdateAccountHandle.HandlerInput,\n      ComAtprotoAdminUpdateAccountHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountPassword.QueryParams,\n      ComAtprotoAdminUpdateAccountPassword.HandlerInput,\n      ComAtprotoAdminUpdateAccountPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountSigningKey<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountSigningKey.QueryParams,\n      ComAtprotoAdminUpdateAccountSigningKey.HandlerInput,\n      ComAtprotoAdminUpdateAccountSigningKey.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountSigningKey' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateSubjectStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateSubjectStatus.QueryParams,\n      ComAtprotoAdminUpdateSubjectStatus.HandlerInput,\n      ComAtprotoAdminUpdateSubjectStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateSubjectStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoIdentityNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getRecommendedDidCredentials<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityGetRecommendedDidCredentials.QueryParams,\n      ComAtprotoIdentityGetRecommendedDidCredentials.HandlerInput,\n      ComAtprotoIdentityGetRecommendedDidCredentials.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.getRecommendedDidCredentials' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  refreshIdentity<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityRefreshIdentity.QueryParams,\n      ComAtprotoIdentityRefreshIdentity.HandlerInput,\n      ComAtprotoIdentityRefreshIdentity.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.refreshIdentity' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPlcOperationSignature<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityRequestPlcOperationSignature.QueryParams,\n      ComAtprotoIdentityRequestPlcOperationSignature.HandlerInput,\n      ComAtprotoIdentityRequestPlcOperationSignature.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.requestPlcOperationSignature' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveDid<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveDid.QueryParams,\n      ComAtprotoIdentityResolveDid.HandlerInput,\n      ComAtprotoIdentityResolveDid.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveDid' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveHandle.QueryParams,\n      ComAtprotoIdentityResolveHandle.HandlerInput,\n      ComAtprotoIdentityResolveHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveIdentity<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveIdentity.QueryParams,\n      ComAtprotoIdentityResolveIdentity.HandlerInput,\n      ComAtprotoIdentityResolveIdentity.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveIdentity' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  signPlcOperation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentitySignPlcOperation.QueryParams,\n      ComAtprotoIdentitySignPlcOperation.HandlerInput,\n      ComAtprotoIdentitySignPlcOperation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.signPlcOperation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  submitPlcOperation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentitySubmitPlcOperation.QueryParams,\n      ComAtprotoIdentitySubmitPlcOperation.HandlerInput,\n      ComAtprotoIdentitySubmitPlcOperation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.submitPlcOperation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityUpdateHandle.QueryParams,\n      ComAtprotoIdentityUpdateHandle.HandlerInput,\n      ComAtprotoIdentityUpdateHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.updateHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoLabelNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  queryLabels<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoLabelQueryLabels.QueryParams,\n      ComAtprotoLabelQueryLabels.HandlerInput,\n      ComAtprotoLabelQueryLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.label.queryLabels' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  subscribeLabels<A extends Auth = void>(\n    cfg: StreamConfigOrHandler<\n      A,\n      ComAtprotoLabelSubscribeLabels.QueryParams,\n      ComAtprotoLabelSubscribeLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore\n    return this._server.xrpc.streamMethod(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoLexiconNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  resolveLexicon<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoLexiconResolveLexicon.QueryParams,\n      ComAtprotoLexiconResolveLexicon.HandlerInput,\n      ComAtprotoLexiconResolveLexicon.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.lexicon.resolveLexicon' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createReport<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoModerationCreateReport.QueryParams,\n      ComAtprotoModerationCreateReport.HandlerInput,\n      ComAtprotoModerationCreateReport.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.moderation.createReport' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoRepoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  applyWrites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoApplyWrites.QueryParams,\n      ComAtprotoRepoApplyWrites.HandlerInput,\n      ComAtprotoRepoApplyWrites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.applyWrites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoCreateRecord.QueryParams,\n      ComAtprotoRepoCreateRecord.HandlerInput,\n      ComAtprotoRepoCreateRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.createRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoDeleteRecord.QueryParams,\n      ComAtprotoRepoDeleteRecord.HandlerInput,\n      ComAtprotoRepoDeleteRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.deleteRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  describeRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoDescribeRepo.QueryParams,\n      ComAtprotoRepoDescribeRepo.HandlerInput,\n      ComAtprotoRepoDescribeRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.describeRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoGetRecord.QueryParams,\n      ComAtprotoRepoGetRecord.HandlerInput,\n      ComAtprotoRepoGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  importRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoImportRepo.QueryParams,\n      ComAtprotoRepoImportRepo.HandlerInput,\n      ComAtprotoRepoImportRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.importRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listMissingBlobs<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoListMissingBlobs.QueryParams,\n      ComAtprotoRepoListMissingBlobs.HandlerInput,\n      ComAtprotoRepoListMissingBlobs.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.listMissingBlobs' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listRecords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoListRecords.QueryParams,\n      ComAtprotoRepoListRecords.HandlerInput,\n      ComAtprotoRepoListRecords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.listRecords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoPutRecord.QueryParams,\n      ComAtprotoRepoPutRecord.HandlerInput,\n      ComAtprotoRepoPutRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.putRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  uploadBlob<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoUploadBlob.QueryParams,\n      ComAtprotoRepoUploadBlob.HandlerInput,\n      ComAtprotoRepoUploadBlob.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoServerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  activateAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerActivateAccount.QueryParams,\n      ComAtprotoServerActivateAccount.HandlerInput,\n      ComAtprotoServerActivateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.activateAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkAccountStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCheckAccountStatus.QueryParams,\n      ComAtprotoServerCheckAccountStatus.HandlerInput,\n      ComAtprotoServerCheckAccountStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.checkAccountStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  confirmEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerConfirmEmail.QueryParams,\n      ComAtprotoServerConfirmEmail.HandlerInput,\n      ComAtprotoServerConfirmEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateAccount.QueryParams,\n      ComAtprotoServerCreateAccount.HandlerInput,\n      ComAtprotoServerCreateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createAppPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateAppPassword.QueryParams,\n      ComAtprotoServerCreateAppPassword.HandlerInput,\n      ComAtprotoServerCreateAppPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createAppPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createInviteCode<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateInviteCode.QueryParams,\n      ComAtprotoServerCreateInviteCode.HandlerInput,\n      ComAtprotoServerCreateInviteCode.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createInviteCode' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateInviteCodes.QueryParams,\n      ComAtprotoServerCreateInviteCodes.HandlerInput,\n      ComAtprotoServerCreateInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateSession.QueryParams,\n      ComAtprotoServerCreateSession.HandlerInput,\n      ComAtprotoServerCreateSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deactivateAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeactivateAccount.QueryParams,\n      ComAtprotoServerDeactivateAccount.HandlerInput,\n      ComAtprotoServerDeactivateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deactivateAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeleteAccount.QueryParams,\n      ComAtprotoServerDeleteAccount.HandlerInput,\n      ComAtprotoServerDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeleteSession.QueryParams,\n      ComAtprotoServerDeleteSession.HandlerInput,\n      ComAtprotoServerDeleteSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deleteSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  describeServer<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDescribeServer.QueryParams,\n      ComAtprotoServerDescribeServer.HandlerInput,\n      ComAtprotoServerDescribeServer.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.describeServer' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetAccountInviteCodes.QueryParams,\n      ComAtprotoServerGetAccountInviteCodes.HandlerInput,\n      ComAtprotoServerGetAccountInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getAccountInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getServiceAuth<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetServiceAuth.QueryParams,\n      ComAtprotoServerGetServiceAuth.HandlerInput,\n      ComAtprotoServerGetServiceAuth.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getServiceAuth' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetSession.QueryParams,\n      ComAtprotoServerGetSession.HandlerInput,\n      ComAtprotoServerGetSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listAppPasswords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerListAppPasswords.QueryParams,\n      ComAtprotoServerListAppPasswords.HandlerInput,\n      ComAtprotoServerListAppPasswords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.listAppPasswords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  refreshSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRefreshSession.QueryParams,\n      ComAtprotoServerRefreshSession.HandlerInput,\n      ComAtprotoServerRefreshSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.refreshSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestAccountDelete<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestAccountDelete.QueryParams,\n      ComAtprotoServerRequestAccountDelete.HandlerInput,\n      ComAtprotoServerRequestAccountDelete.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestAccountDelete' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestEmailConfirmation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestEmailConfirmation.QueryParams,\n      ComAtprotoServerRequestEmailConfirmation.HandlerInput,\n      ComAtprotoServerRequestEmailConfirmation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestEmailUpdate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestEmailUpdate.QueryParams,\n      ComAtprotoServerRequestEmailUpdate.HandlerInput,\n      ComAtprotoServerRequestEmailUpdate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPasswordReset<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestPasswordReset.QueryParams,\n      ComAtprotoServerRequestPasswordReset.HandlerInput,\n      ComAtprotoServerRequestPasswordReset.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestPasswordReset' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  reserveSigningKey<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerReserveSigningKey.QueryParams,\n      ComAtprotoServerReserveSigningKey.HandlerInput,\n      ComAtprotoServerReserveSigningKey.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.reserveSigningKey' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resetPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerResetPassword.QueryParams,\n      ComAtprotoServerResetPassword.HandlerInput,\n      ComAtprotoServerResetPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.resetPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeAppPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRevokeAppPassword.QueryParams,\n      ComAtprotoServerRevokeAppPassword.HandlerInput,\n      ComAtprotoServerRevokeAppPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerUpdateEmail.QueryParams,\n      ComAtprotoServerUpdateEmail.HandlerInput,\n      ComAtprotoServerUpdateEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.updateEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoSyncNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getBlob<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetBlob.QueryParams,\n      ComAtprotoSyncGetBlob.HandlerInput,\n      ComAtprotoSyncGetBlob.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getBlob' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetBlocks.QueryParams,\n      ComAtprotoSyncGetBlocks.HandlerInput,\n      ComAtprotoSyncGetBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getCheckout<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetCheckout.QueryParams,\n      ComAtprotoSyncGetCheckout.HandlerInput,\n      ComAtprotoSyncGetCheckout.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getCheckout' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getHead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetHead.QueryParams,\n      ComAtprotoSyncGetHead.HandlerInput,\n      ComAtprotoSyncGetHead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getHead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getHostStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetHostStatus.QueryParams,\n      ComAtprotoSyncGetHostStatus.HandlerInput,\n      ComAtprotoSyncGetHostStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getHostStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLatestCommit<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetLatestCommit.QueryParams,\n      ComAtprotoSyncGetLatestCommit.HandlerInput,\n      ComAtprotoSyncGetLatestCommit.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getLatestCommit' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRecord.QueryParams,\n      ComAtprotoSyncGetRecord.HandlerInput,\n      ComAtprotoSyncGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRepo.QueryParams,\n      ComAtprotoSyncGetRepo.HandlerInput,\n      ComAtprotoSyncGetRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepoStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRepoStatus.QueryParams,\n      ComAtprotoSyncGetRepoStatus.HandlerInput,\n      ComAtprotoSyncGetRepoStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRepoStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listBlobs<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListBlobs.QueryParams,\n      ComAtprotoSyncListBlobs.HandlerInput,\n      ComAtprotoSyncListBlobs.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listBlobs' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listHosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListHosts.QueryParams,\n      ComAtprotoSyncListHosts.HandlerInput,\n      ComAtprotoSyncListHosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listHosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListRepos.QueryParams,\n      ComAtprotoSyncListRepos.HandlerInput,\n      ComAtprotoSyncListRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listReposByCollection<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListReposByCollection.QueryParams,\n      ComAtprotoSyncListReposByCollection.HandlerInput,\n      ComAtprotoSyncListReposByCollection.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listReposByCollection' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  notifyOfUpdate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncNotifyOfUpdate.QueryParams,\n      ComAtprotoSyncNotifyOfUpdate.HandlerInput,\n      ComAtprotoSyncNotifyOfUpdate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.notifyOfUpdate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestCrawl<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncRequestCrawl.QueryParams,\n      ComAtprotoSyncRequestCrawl.HandlerInput,\n      ComAtprotoSyncRequestCrawl.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.requestCrawl' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  subscribeRepos<A extends Auth = void>(\n    cfg: StreamConfigOrHandler<\n      A,\n      ComAtprotoSyncSubscribeRepos.QueryParams,\n      ComAtprotoSyncSubscribeRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.subscribeRepos' // @ts-ignore\n    return this._server.xrpc.streamMethod(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoTempNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addReservedHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempAddReservedHandle.QueryParams,\n      ComAtprotoTempAddReservedHandle.HandlerInput,\n      ComAtprotoTempAddReservedHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.addReservedHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkHandleAvailability<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempCheckHandleAvailability.QueryParams,\n      ComAtprotoTempCheckHandleAvailability.HandlerInput,\n      ComAtprotoTempCheckHandleAvailability.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.checkHandleAvailability' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkSignupQueue<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempCheckSignupQueue.QueryParams,\n      ComAtprotoTempCheckSignupQueue.HandlerInput,\n      ComAtprotoTempCheckSignupQueue.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.checkSignupQueue' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  dereferenceScope<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempDereferenceScope.QueryParams,\n      ComAtprotoTempDereferenceScope.HandlerInput,\n      ComAtprotoTempDereferenceScope.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.dereferenceScope' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  fetchLabels<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempFetchLabels.QueryParams,\n      ComAtprotoTempFetchLabels.HandlerInput,\n      ComAtprotoTempFetchLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.fetchLabels' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPhoneVerification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempRequestPhoneVerification.QueryParams,\n      ComAtprotoTempRequestPhoneVerification.HandlerInput,\n      ComAtprotoTempRequestPhoneVerification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.requestPhoneVerification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeAccountCredentials<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempRevokeAccountCredentials.QueryParams,\n      ComAtprotoTempRevokeAccountCredentials.HandlerInput,\n      ComAtprotoTempRevokeAccountCredentials.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.revokeAccountCredentials' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComGermnetworkNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/lexicons.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type LexiconDoc,\n  Lexicons,\n  ValidationError,\n  type ValidationResult,\n} from '@atproto/lexicon'\nimport { type $Typed, is$typed, maybe$typed } from './util.js'\n\nexport const schemaDict = {\n  AppBskyActorDefs: {\n    lexicon: 1,\n    id: 'app.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileView: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileViewDetailed: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          website: {\n            type: 'string',\n            format: 'uri',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          banner: {\n            type: 'string',\n            format: 'uri',\n          },\n          followersCount: {\n            type: 'integer',\n          },\n          followsCount: {\n            type: 'integer',\n          },\n          postsCount: {\n            type: 'integer',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          joinedViaStarterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          pinnedPost: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileAssociated: {\n        type: 'object',\n        properties: {\n          lists: {\n            type: 'integer',\n          },\n          feedgens: {\n            type: 'integer',\n          },\n          starterPacks: {\n            type: 'integer',\n          },\n          labeler: {\n            type: 'boolean',\n          },\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedChat',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedActivitySubscription',\n          },\n          germ: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedGerm',\n          },\n        },\n      },\n      profileAssociatedChat: {\n        type: 'object',\n        required: ['allowIncoming'],\n        properties: {\n          allowIncoming: {\n            type: 'string',\n            knownValues: ['all', 'none', 'following'],\n          },\n        },\n      },\n      profileAssociatedGerm: {\n        type: 'object',\n        required: ['showButtonTo', 'messageMeUrl'],\n        properties: {\n          messageMeUrl: {\n            type: 'string',\n            format: 'uri',\n          },\n          showButtonTo: {\n            type: 'string',\n            knownValues: ['usersIFollow', 'everyone'],\n          },\n        },\n      },\n      profileAssociatedActivitySubscription: {\n        type: 'object',\n        required: ['allowSubscriptions'],\n        properties: {\n          allowSubscriptions: {\n            type: 'string',\n            knownValues: ['followers', 'mutuals', 'none'],\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.\",\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          mutedByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          blockedBy: {\n            type: 'boolean',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blockingByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          knownFollowers: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#knownFollowers',\n          },\n          activitySubscription: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n      knownFollowers: {\n        type: 'object',\n        description: \"The subject's followers whom you also follow\",\n        required: ['count', 'followers'],\n        properties: {\n          count: {\n            type: 'integer',\n          },\n          followers: {\n            type: 'array',\n            minLength: 0,\n            maxLength: 5,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      verificationState: {\n        type: 'object',\n        description:\n          'Represents the verification information about the user this object is attached to.',\n        required: ['verifications', 'verifiedStatus', 'trustedVerifierStatus'],\n        properties: {\n          verifications: {\n            type: 'array',\n            description:\n              'All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#verificationView',\n            },\n          },\n          verifiedStatus: {\n            type: 'string',\n            description: \"The user's status as a verified account.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n          trustedVerifierStatus: {\n            type: 'string',\n            description: \"The user's status as a trusted verifier.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n        },\n      },\n      verificationView: {\n        type: 'object',\n        description: 'An individual verification for an associated subject.',\n        required: ['issuer', 'uri', 'isValid', 'createdAt'],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          isValid: {\n            type: 'boolean',\n            description:\n              'True if the verification passes validation, otherwise false.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n        },\n      },\n      preferences: {\n        type: 'array',\n        items: {\n          type: 'union',\n          refs: [\n            'lex:app.bsky.actor.defs#adultContentPref',\n            'lex:app.bsky.actor.defs#contentLabelPref',\n            'lex:app.bsky.actor.defs#savedFeedsPref',\n            'lex:app.bsky.actor.defs#savedFeedsPrefV2',\n            'lex:app.bsky.actor.defs#personalDetailsPref',\n            'lex:app.bsky.actor.defs#declaredAgePref',\n            'lex:app.bsky.actor.defs#feedViewPref',\n            'lex:app.bsky.actor.defs#threadViewPref',\n            'lex:app.bsky.actor.defs#interestsPref',\n            'lex:app.bsky.actor.defs#mutedWordsPref',\n            'lex:app.bsky.actor.defs#hiddenPostsPref',\n            'lex:app.bsky.actor.defs#bskyAppStatePref',\n            'lex:app.bsky.actor.defs#labelersPref',\n            'lex:app.bsky.actor.defs#postInteractionSettingsPref',\n            'lex:app.bsky.actor.defs#verificationPrefs',\n            'lex:app.bsky.actor.defs#liveEventPreferences',\n          ],\n        },\n      },\n      adultContentPref: {\n        type: 'object',\n        required: ['enabled'],\n        properties: {\n          enabled: {\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      contentLabelPref: {\n        type: 'object',\n        required: ['label', 'visibility'],\n        properties: {\n          labelerDid: {\n            type: 'string',\n            description:\n              'Which labeler does this preference apply to? If undefined, applies globally.',\n            format: 'did',\n          },\n          label: {\n            type: 'string',\n          },\n          visibility: {\n            type: 'string',\n            knownValues: ['ignore', 'show', 'warn', 'hide'],\n          },\n        },\n      },\n      savedFeed: {\n        type: 'object',\n        required: ['id', 'type', 'value', 'pinned'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          type: {\n            type: 'string',\n            knownValues: ['feed', 'list', 'timeline'],\n          },\n          value: {\n            type: 'string',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      savedFeedsPrefV2: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#savedFeed',\n            },\n          },\n        },\n      },\n      savedFeedsPref: {\n        type: 'object',\n        required: ['pinned', 'saved'],\n        properties: {\n          pinned: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          saved: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          timelineIndex: {\n            type: 'integer',\n          },\n        },\n      },\n      personalDetailsPref: {\n        type: 'object',\n        properties: {\n          birthDate: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The birth date of account owner.',\n          },\n        },\n      },\n      declaredAgePref: {\n        type: 'object',\n        description:\n          \"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.\",\n        properties: {\n          isOverAge13: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 13 years of age.',\n          },\n          isOverAge16: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 16 years of age.',\n          },\n          isOverAge18: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 18 years of age.',\n          },\n        },\n      },\n      feedViewPref: {\n        type: 'object',\n        required: ['feed'],\n        properties: {\n          feed: {\n            type: 'string',\n            description:\n              'The URI of the feed, or an identifier which describes the feed.',\n          },\n          hideReplies: {\n            type: 'boolean',\n            description: 'Hide replies in the feed.',\n          },\n          hideRepliesByUnfollowed: {\n            type: 'boolean',\n            description:\n              'Hide replies in the feed if they are not by followed users.',\n            default: true,\n          },\n          hideRepliesByLikeCount: {\n            type: 'integer',\n            description:\n              'Hide replies in the feed if they do not have this number of likes.',\n          },\n          hideReposts: {\n            type: 'boolean',\n            description: 'Hide reposts in the feed.',\n          },\n          hideQuotePosts: {\n            type: 'boolean',\n            description: 'Hide quote posts in the feed.',\n          },\n        },\n      },\n      threadViewPref: {\n        type: 'object',\n        properties: {\n          sort: {\n            type: 'string',\n            description: 'Sorting mode for threads.',\n            knownValues: [\n              'oldest',\n              'newest',\n              'most-likes',\n              'random',\n              'hotness',\n            ],\n          },\n        },\n      },\n      interestsPref: {\n        type: 'object',\n        required: ['tags'],\n        properties: {\n          tags: {\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'string',\n              maxLength: 640,\n              maxGraphemes: 64,\n            },\n            description:\n              \"A list of tags which describe the account owner's interests gathered during onboarding.\",\n          },\n        },\n      },\n      mutedWordTarget: {\n        type: 'string',\n        knownValues: ['content', 'tag'],\n        maxLength: 640,\n        maxGraphemes: 64,\n      },\n      mutedWord: {\n        type: 'object',\n        description: 'A word that the account owner has muted.',\n        required: ['value', 'targets'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n            description: 'The muted word itself.',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          targets: {\n            type: 'array',\n            description: 'The intended targets of the muted word.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWordTarget',\n            },\n          },\n          actorTarget: {\n            type: 'string',\n            description:\n              'Groups of users to apply the muted word to. If undefined, applies to all users.',\n            knownValues: ['all', 'exclude-following'],\n            default: 'all',\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the muted word will expire and no longer be applied.',\n          },\n        },\n      },\n      mutedWordsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWord',\n            },\n            description: 'A list of words the account owner has muted.',\n          },\n        },\n      },\n      hiddenPostsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            description:\n              'A list of URIs of posts the account owner has hidden.',\n          },\n        },\n      },\n      labelersPref: {\n        type: 'object',\n        required: ['labelers'],\n        properties: {\n          labelers: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#labelerPrefItem',\n            },\n          },\n        },\n      },\n      labelerPrefItem: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      bskyAppStatePref: {\n        description:\n          \"A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.\",\n        type: 'object',\n        properties: {\n          activeProgressGuide: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#bskyAppProgressGuide',\n          },\n          queuedNudges: {\n            description:\n              'An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.',\n            type: 'array',\n            maxLength: 1000,\n            items: {\n              type: 'string',\n              maxLength: 100,\n            },\n          },\n          nuxs: {\n            description: 'Storage for NUXs the user has encountered.',\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#nux',\n            },\n          },\n        },\n      },\n      bskyAppProgressGuide: {\n        description:\n          'If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.',\n        type: 'object',\n        required: ['guide'],\n        properties: {\n          guide: {\n            type: 'string',\n            maxLength: 100,\n          },\n        },\n      },\n      nux: {\n        type: 'object',\n        description: 'A new user experiences (NUX) storage object',\n        required: ['id', 'completed'],\n        properties: {\n          id: {\n            type: 'string',\n            maxLength: 100,\n          },\n          completed: {\n            type: 'boolean',\n            default: false,\n          },\n          data: {\n            description:\n              'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.',\n            type: 'string',\n            maxLength: 3000,\n            maxGraphemes: 300,\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the NUX will expire and should be considered completed.',\n          },\n        },\n      },\n      verificationPrefs: {\n        type: 'object',\n        description: 'Preferences for how verified accounts appear in the app.',\n        required: [],\n        properties: {\n          hideBadges: {\n            description:\n              'Hide the blue check badges for verified accounts and trusted verifiers.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      liveEventPreferences: {\n        type: 'object',\n        description: 'Preferences for live events.',\n        properties: {\n          hiddenFeedIds: {\n            description:\n              'A list of feed IDs that the user has hidden from live events.',\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          hideAllFeeds: {\n            description: 'Whether to hide all feeds from live events.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      postInteractionSettingsPref: {\n        type: 'object',\n        description:\n          '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.',\n        required: [],\n        properties: {\n          threadgateAllowRules: {\n            description:\n              'Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n        },\n      },\n      statusView: {\n        type: 'object',\n        required: ['status', 'record'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          status: {\n            type: 'string',\n            description: 'The status for the account.',\n            knownValues: ['app.bsky.actor.status#live'],\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            description: 'An optional embed associated with the status.',\n            refs: ['lex:app.bsky.embed.external#view'],\n          },\n          expiresAt: {\n            type: 'string',\n            description:\n              'The date when this status will expire. The application might choose to no longer return the status after expiration.',\n            format: 'datetime',\n          },\n          isActive: {\n            type: 'boolean',\n            description:\n              'True if the status is not expired, false if it is expired. Only present if expiration was set.',\n          },\n          isDisabled: {\n            type: 'boolean',\n            description:\n              \"True if the user's go-live access has been disabled by a moderator, false otherwise.\",\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfile',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID of account to fetch profile of.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfiles: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfiles',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get detailed profile views of multiple actors.',\n        parameters: {\n          type: 'params',\n          required: ['actors'],\n          properties: {\n            actors: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['profiles'],\n            properties: {\n              profiles: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.profile',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account profile.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          properties: {\n            displayName: {\n              type: 'string',\n              maxGraphemes: 64,\n              maxLength: 640,\n            },\n            description: {\n              type: 'string',\n              description: 'Free-form profile description text.',\n              maxGraphemes: 256,\n              maxLength: 2560,\n            },\n            pronouns: {\n              type: 'string',\n              description: 'Free-form pronouns text.',\n              maxGraphemes: 20,\n              maxLength: 200,\n            },\n            website: {\n              type: 'string',\n              format: 'uri',\n            },\n            avatar: {\n              type: 'blob',\n              description:\n                \"Small image to be displayed next to posts from account. AKA, 'profile picture'\",\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            banner: {\n              type: 'blob',\n              description:\n                'Larger horizontal image to display behind profile view.',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values, specific to the Bluesky application, on the overall account.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            joinedViaStarterPack: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            pinnedPost: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Set the private preferences attached to the account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActors: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActors',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actors (profiles) matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActorsTypeahead: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActorsTypeahead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description: 'Search query prefix; not a full query string.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorStatus: {\n    lexicon: 1,\n    id: 'app.bsky.actor.status',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account status.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['status', 'createdAt'],\n          properties: {\n            status: {\n              type: 'string',\n              description: 'The status for the account.',\n              knownValues: ['app.bsky.actor.status#live'],\n            },\n            embed: {\n              type: 'union',\n              description: 'An optional embed associated with the status.',\n              refs: ['lex:app.bsky.embed.external'],\n            },\n            durationMinutes: {\n              type: 'integer',\n              description:\n                'The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.',\n              minimum: 1,\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      live: {\n        type: 'token',\n        description:\n          'Advertises an account as currently offering live content.',\n      },\n    },\n  },\n  AppBskyAgeassuranceBegin: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.begin',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate Age Assurance for an account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive Age Assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the Age Assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n              regionCode: {\n                type: 'string',\n                description:\n                  \"An optional ISO 3166-2 code of the user's region or state within the country.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#state',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n          {\n            name: 'RegionNotSupported',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyAgeassuranceDefs: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.defs',\n    defs: {\n      access: {\n        description:\n          \"The access level granted based on Age Assurance data we've processed.\",\n        type: 'string',\n        knownValues: ['unknown', 'none', 'safe', 'full'],\n      },\n      status: {\n        type: 'string',\n        description: 'The status of the Age Assurance process.',\n        knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n      },\n      state: {\n        type: 'object',\n        description: \"The user's computed Age Assurance state.\",\n        required: ['status', 'access'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#status',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      stateMetadata: {\n        type: 'object',\n        description:\n          'Additional metadata needed to compute Age Assurance state client-side.',\n        required: [],\n        properties: {\n          accountCreatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The account creation timestamp.',\n          },\n        },\n      },\n      config: {\n        type: 'object',\n        description: '',\n        required: ['regions'],\n        properties: {\n          regions: {\n            type: 'array',\n            description: 'The per-region Age Assurance configuration.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.ageassurance.defs#configRegion',\n            },\n          },\n        },\n      },\n      configRegion: {\n        type: 'object',\n        description: 'The Age Assurance configuration for a specific region.',\n        required: ['countryCode', 'minAccessAge', 'rules'],\n        properties: {\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code this configuration applies to.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.',\n          },\n          minAccessAge: {\n            type: 'integer',\n            description:\n              'The minimum age (as a whole integer) required to use Bluesky in this region.',\n          },\n          rules: {\n            type: 'array',\n            description:\n              '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.',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.ageassurance.defs#configRegionRuleDefault',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan',\n              ],\n            },\n          },\n        },\n      },\n      configRegionRuleDefault: {\n        type: 'object',\n        description: 'Age Assurance rule that applies by default.',\n        required: ['access'],\n        properties: {\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountNewerThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is equal-to or newer than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountOlderThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is older than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        description: 'Object used to store Age Assurance data in stash.',\n        required: ['createdAt', 'status', 'access', 'attemptId', 'countryCode'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the Age Assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n          access: {\n            description:\n              \"The access level granted based on Age Assurance data we've processed.\",\n            type: 'string',\n            knownValues: ['unknown', 'none', 'safe', 'full'],\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for Age Assurance.',\n          },\n          initIp: {\n            type: 'string',\n            description:\n              'The IP address used when initiating the Age Assurance flow.',\n          },\n          initUa: {\n            type: 'string',\n            description:\n              'The user agent used when initiating the Age Assurance flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description:\n              'The IP address used when completing the Age Assurance flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description:\n              'The user agent used when completing the Age Assurance flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns Age Assurance configuration for use on the client.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#config',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetState: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.',\n        parameters: {\n          type: 'params',\n          required: ['countryCode'],\n          properties: {\n            countryCode: {\n              type: 'string',\n            },\n            regionCode: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['state', 'metadata'],\n            properties: {\n              state: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#state',\n              },\n              metadata: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#stateMetadata',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkCreateBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.createBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkDefs: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.defs',\n    defs: {\n      bookmark: {\n        description: 'Object used to store bookmark data in stash.',\n        type: 'object',\n        required: ['subject'],\n        properties: {\n          subject: {\n            description:\n              'A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      bookmarkView: {\n        type: 'object',\n        required: ['subject', 'item'],\n        properties: {\n          subject: {\n            description: 'A strong ref to the bookmarked record.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          item: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#blockedPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#postView',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkDeleteBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.deleteBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkGetBookmarks: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.getBookmarks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Gets views of records bookmarked by the authenticated user. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['bookmarks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              bookmarks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.bookmark.defs#bookmarkView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDefs: {\n    lexicon: 1,\n    id: 'app.bsky.contact.defs',\n    defs: {\n      matchAndContactIndex: {\n        description:\n          '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.',\n        type: 'object',\n        required: ['match', 'contactIndex'],\n        properties: {\n          match: {\n            description: 'Profile of the matched user.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          contactIndex: {\n            description: 'The index of this match in the import contact input.',\n            type: 'integer',\n            minimum: 0,\n            maximum: 999,\n          },\n        },\n      },\n      syncStatus: {\n        type: 'object',\n        required: ['syncedAt', 'matchesCount'],\n        properties: {\n          syncedAt: {\n            description: 'Last date when contacts where imported.',\n            type: 'string',\n            format: 'datetime',\n          },\n          matchesCount: {\n            description:\n              '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.',\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n      notification: {\n        description:\n          'A stash object to be sent via bsync representing a notification to be created.',\n        type: 'object',\n        required: ['from', 'to'],\n        properties: {\n          from: {\n            description: 'The DID of who this notification comes from.',\n            type: 'string',\n            format: 'did',\n          },\n          to: {\n            description: 'The DID of who this notification should go to.',\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDismissMatch: {\n    lexicon: 1,\n    id: 'app.bsky.contact.dismissMatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                description: \"The subject's DID to dismiss the match with.\",\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetMatches: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getMatches',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matches'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              matches: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidLimit',\n          },\n          {\n            name: 'InvalidCursor',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetSyncStatus: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getSyncStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets the user's current contact import status. Requires authentication.\",\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              syncStatus: {\n                description:\n                  \"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.\",\n                type: 'ref',\n                ref: 'lex:app.bsky.contact.defs#syncStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactImportContacts: {\n    lexicon: 1,\n    id: 'app.bsky.contact.importContacts',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'contacts'],\n            properties: {\n              token: {\n                description:\n                  'JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.',\n                type: 'string',\n              },\n              contacts: {\n                description:\n                  \"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`.\",\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                minLength: 1,\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matchesAndContactIndexes'],\n            properties: {\n              matchesAndContactIndexes: {\n                description:\n                  'The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.',\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.contact.defs#matchAndContactIndex',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidContacts',\n          },\n          {\n            name: 'TooManyContacts',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactRemoveData: {\n    lexicon: 1,\n    id: 'app.bsky.contact.removeData',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactSendNotification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.sendNotification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'System endpoint to send notifications related to contact imports. Requires role authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['from', 'to'],\n            properties: {\n              from: {\n                description: 'The DID of who this notification comes from.',\n                type: 'string',\n                format: 'did',\n              },\n              to: {\n                description: 'The DID of who this notification should go to.',\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactStartPhoneVerification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.startPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          '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.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone'],\n            properties: {\n              phone: {\n                description: 'The phone number to receive the code via SMS.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactVerifyPhone: {\n    lexicon: 1,\n    id: 'app.bsky.contact.verifyPhone',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone', 'code'],\n            properties: {\n              phone: {\n                description:\n                  'The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n              code: {\n                description:\n                  'The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                description:\n                  'JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InvalidCode',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftCreateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.createDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draft',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'The ID of the created draft.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DraftLimitReached',\n            description:\n              'Trying to insert a new draft when the limit was already reached.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftDefs: {\n    lexicon: 1,\n    id: 'app.bsky.draft.defs',\n    defs: {\n      draftWithId: {\n        description:\n          'A draft with an identifier, used to store drafts in private storage (stash).',\n        type: 'object',\n        required: ['id', 'draft'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n        },\n      },\n      draft: {\n        description: 'A draft containing an array of draft posts.',\n        type: 'object',\n        required: ['posts'],\n        properties: {\n          deviceId: {\n            type: 'string',\n            description:\n              'UUIDv4 identifier of the device that created this draft.',\n            maxLength: 100,\n          },\n          deviceName: {\n            type: 'string',\n            description:\n              'The device and/or platform on which the draft was created.',\n            maxLength: 100,\n          },\n          posts: {\n            description: 'Array of draft posts that compose this draft.',\n            type: 'array',\n            minLength: 1,\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftPost',\n            },\n          },\n          langs: {\n            type: 'array',\n            description:\n              'Indicates human language of posts primary text content.',\n            maxLength: 3,\n            items: {\n              type: 'string',\n              format: 'language',\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Embedding rules for the postgates to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n          threadgateAllow: {\n            description:\n              'Allow-rules for the threadgate to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n        },\n      },\n      draftPost: {\n        description: 'One of the posts that compose a draft.',\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n            description:\n              '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.',\n          },\n          labels: {\n            type: 'union',\n            description:\n              'Self-label values for this post. Effectively content warnings.',\n            refs: ['lex:com.atproto.label.defs#selfLabels'],\n          },\n          embedImages: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedImage',\n            },\n            maxLength: 4,\n          },\n          embedVideos: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedVideo',\n            },\n            maxLength: 1,\n          },\n          embedExternals: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedExternal',\n            },\n            maxLength: 1,\n          },\n          embedRecords: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedRecord',\n            },\n            maxLength: 1,\n          },\n        },\n      },\n      draftView: {\n        description: 'View to present drafts data to users.',\n        type: 'object',\n        required: ['id', 'draft', 'createdAt', 'updatedAt'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n          createdAt: {\n            description: 'The time the draft was created.',\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            description: 'The time the draft was last updated.',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      draftEmbedLocalRef: {\n        type: 'object',\n        required: ['path'],\n        properties: {\n          path: {\n            type: 'string',\n            description:\n              'Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts.',\n            minLength: 1,\n            maxLength: 1024,\n          },\n        },\n      },\n      draftEmbedCaption: {\n        type: 'object',\n        required: ['lang', 'content'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          content: {\n            type: 'string',\n            maxLength: 10000,\n          },\n        },\n      },\n      draftEmbedImage: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n        },\n      },\n      draftEmbedVideo: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedCaption',\n            },\n            maxLength: 20,\n          },\n        },\n      },\n      draftEmbedExternal: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      draftEmbedRecord: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftDeleteDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.deleteDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Deletes a draft by ID. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftGetDrafts: {\n    lexicon: 1,\n    id: 'app.bsky.draft.getDrafts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets views of user drafts. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['drafts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              drafts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.draft.defs#draftView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftUpdateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.updateDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"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.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draftWithId',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.embed.defs',\n    defs: {\n      aspectRatio: {\n        type: 'object',\n        description:\n          'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n            minimum: 1,\n          },\n          height: {\n            type: 'integer',\n            minimum: 1,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedExternal: {\n    lexicon: 1,\n    id: 'app.bsky.embed.external',\n    defs: {\n      main: {\n        type: 'object',\n        description:\n          \"A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).\",\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#external',\n          },\n        },\n      },\n      external: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#viewExternal',\n          },\n        },\n      },\n      viewExternal: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedImages: {\n    lexicon: 1,\n    id: 'app.bsky.embed.images',\n    description: 'A set of images embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#image',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      image: {\n        type: 'object',\n        required: ['image', 'alt'],\n        properties: {\n          image: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#viewImage',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      viewImage: {\n        type: 'object',\n        required: ['thumb', 'fullsize', 'alt'],\n        properties: {\n          thumb: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.',\n          },\n          fullsize: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.',\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecord: {\n    lexicon: 1,\n    id: 'app.bsky.embed.record',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.record#viewRecord',\n              'lex:app.bsky.embed.record#viewNotFound',\n              'lex:app.bsky.embed.record#viewBlocked',\n              'lex:app.bsky.embed.record#viewDetached',\n              'lex:app.bsky.feed.defs#generatorView',\n              'lex:app.bsky.graph.defs#listView',\n              'lex:app.bsky.labeler.defs#labelerView',\n              'lex:app.bsky.graph.defs#starterPackViewBasic',\n            ],\n          },\n        },\n      },\n      viewRecord: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'value', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          value: {\n            type: 'unknown',\n            description: 'The record data itself.',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          embeds: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images#view',\n                'lex:app.bsky.embed.video#view',\n                'lex:app.bsky.embed.external#view',\n                'lex:app.bsky.embed.record#view',\n                'lex:app.bsky.embed.recordWithMedia#view',\n              ],\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      viewNotFound: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      viewBlocked: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      viewDetached: {\n        type: 'object',\n        required: ['uri', 'detached'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          detached: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecordWithMedia: {\n    lexicon: 1,\n    id: 'app.bsky.embed.recordWithMedia',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images',\n              'lex:app.bsky.embed.video',\n              'lex:app.bsky.embed.external',\n            ],\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record#view',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedVideo: {\n    lexicon: 1,\n    id: 'app.bsky.embed.video',\n    description: 'A video embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['video'],\n        properties: {\n          video: {\n            type: 'blob',\n            description:\n              'The mp4 video file. May be up to 100mb, formerly limited to 50mb.',\n            accept: ['video/mp4'],\n            maxSize: 100000000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.video#caption',\n            },\n            maxLength: 20,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the video, for accessibility.',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n      caption: {\n        type: 'object',\n        required: ['lang', 'file'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          file: {\n            type: 'blob',\n            accept: ['text/vtt'],\n            maxSize: 20000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['cid', 'playlist'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          playlist: {\n            type: 'string',\n            format: 'uri',\n          },\n          thumbnail: {\n            type: 'string',\n            format: 'uri',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.feed.defs',\n    defs: {\n      postView: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'record', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n              'lex:app.bsky.embed.record#view',\n              'lex:app.bsky.embed.recordWithMedia#view',\n            ],\n          },\n          bookmarkCount: {\n            type: 'integer',\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          threadgate: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadgateView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.\",\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          bookmarked: {\n            type: 'boolean',\n          },\n          threadMuted: {\n            type: 'boolean',\n          },\n          replyDisabled: {\n            type: 'boolean',\n          },\n          embeddingDisabled: {\n            type: 'boolean',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      threadContext: {\n        type: 'object',\n        description:\n          'Metadata about this post within the context of the thread it is in.',\n        properties: {\n          rootAuthorLike: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      feedViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#replyRef',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#reasonRepost',\n              'lex:app.bsky.feed.defs#reasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context provided by feed generator that may be passed back alongside interactions.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          grandparentAuthor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            description:\n              'When parent is a reply to another post, this is the author of that post.',\n          },\n        },\n      },\n      reasonRepost: {\n        type: 'object',\n        required: ['by', 'indexedAt'],\n        properties: {\n          by: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#threadViewPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          replies: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.defs#threadViewPost',\n                'lex:app.bsky.feed.defs#notFoundPost',\n                'lex:app.bsky.feed.defs#blockedPost',\n              ],\n            },\n          },\n          threadContext: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadContext',\n          },\n        },\n      },\n      notFoundPost: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      blockedPost: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      blockedAuthor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n        },\n      },\n      generatorView: {\n        type: 'object',\n        required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          acceptsInteractions: {\n            type: 'boolean',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#generatorViewerState',\n          },\n          contentMode: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#contentModeUnspecified',\n              'app.bsky.feed.defs#contentModeVideo',\n            ],\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      generatorViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonFeedPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#skeletonReasonRepost',\n              'lex:app.bsky.feed.defs#skeletonReasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context that will be passed through to client and may be passed to feed generator back alongside interactions.',\n            maxLength: 2000,\n          },\n        },\n      },\n      skeletonReasonRepost: {\n        type: 'object',\n        required: ['repost'],\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonReasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadgateView: {\n        type: 'object',\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          lists: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listViewBasic',\n            },\n          },\n        },\n      },\n      interaction: {\n        type: 'object',\n        properties: {\n          item: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          event: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#requestLess',\n              'app.bsky.feed.defs#requestMore',\n              'app.bsky.feed.defs#clickthroughItem',\n              'app.bsky.feed.defs#clickthroughAuthor',\n              'app.bsky.feed.defs#clickthroughReposter',\n              'app.bsky.feed.defs#clickthroughEmbed',\n              'app.bsky.feed.defs#interactionSeen',\n              'app.bsky.feed.defs#interactionLike',\n              'app.bsky.feed.defs#interactionRepost',\n              'app.bsky.feed.defs#interactionReply',\n              'app.bsky.feed.defs#interactionQuote',\n              'app.bsky.feed.defs#interactionShare',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      requestLess: {\n        type: 'token',\n        description:\n          'Request that less content like the given feed item be shown in the feed',\n      },\n      requestMore: {\n        type: 'token',\n        description:\n          'Request that more content like the given feed item be shown in the feed',\n      },\n      clickthroughItem: {\n        type: 'token',\n        description: 'User clicked through to the feed item',\n      },\n      clickthroughAuthor: {\n        type: 'token',\n        description: 'User clicked through to the author of the feed item',\n      },\n      clickthroughReposter: {\n        type: 'token',\n        description: 'User clicked through to the reposter of the feed item',\n      },\n      clickthroughEmbed: {\n        type: 'token',\n        description:\n          'User clicked through to the embedded content of the feed item',\n      },\n      contentModeUnspecified: {\n        type: 'token',\n        description: 'Declares the feed generator returns any types of posts.',\n      },\n      contentModeVideo: {\n        type: 'token',\n        description:\n          'Declares the feed generator returns posts containing app.bsky.embed.video embeds.',\n      },\n      interactionSeen: {\n        type: 'token',\n        description: 'Feed item was seen by user',\n      },\n      interactionLike: {\n        type: 'token',\n        description: 'User liked the feed item',\n      },\n      interactionRepost: {\n        type: 'token',\n        description: 'User reposted the feed item',\n      },\n      interactionReply: {\n        type: 'token',\n        description: 'User replied to the feed item',\n      },\n      interactionQuote: {\n        type: 'token',\n        description: 'User quoted the feed item',\n      },\n      interactionShare: {\n        type: 'token',\n        description: 'User shared the feed item',\n      },\n    },\n  },\n  AppBskyFeedDescribeFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.describeFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'feeds'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.describeFeedGenerator#feed',\n                },\n              },\n              links: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.describeFeedGenerator#links',\n              },\n            },\n          },\n        },\n      },\n      feed: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n          },\n          termsOfService: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.generator',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.',\n        key: 'any',\n        record: {\n          type: 'object',\n          required: ['did', 'displayName', 'createdAt'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            displayName: {\n              type: 'string',\n              maxGraphemes: 24,\n              maxLength: 240,\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            acceptsInteractions: {\n              type: 'boolean',\n              description:\n                'Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions',\n            },\n            labels: {\n              type: 'union',\n              description: 'Self-label values',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            contentMode: {\n              type: 'string',\n              knownValues: [\n                'app.bsky.feed.defs#contentModeUnspecified',\n                'app.bsky.feed.defs#contentModeVideo',\n              ],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a list of feeds (feed generator records) created by the actor (in the actor's repo).\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetAuthorFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getAuthorFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            filter: {\n              type: 'string',\n              description:\n                'Combinations of post/repost types to include in response.',\n              knownValues: [\n                'posts_with_replies',\n                'posts_no_replies',\n                'posts_with_media',\n                'posts_and_author_threads',\n                'posts_with_video',\n              ],\n              default: 'posts_with_replies',\n            },\n            includePins: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a hydrated feed from an actor's selected feed generator. Implemented by App View.\",\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator. Implemented by AppView.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the feed generator record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['view', 'isOnline', 'isValid'],\n            properties: {\n              view: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#generatorView',\n              },\n              isOnline: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service has been online recently, or else seems to be inactive.',\n              },\n              isValid: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service is compatible with the record declaration.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of feed generators.',\n        parameters: {\n          type: 'params',\n          required: ['feeds'],\n          properties: {\n            feeds: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference to feed generator record describing the specific feed being requested.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#skeletonFeedPost',\n                },\n              },\n              reqId: {\n                type: 'string',\n                description:\n                  'Unique identifier per request that may be passed back alongside interactions.',\n                maxLength: 100,\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get like records which reference a subject (by AT-URI and CID).',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the subject (eg, a post record).',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'CID of the subject record (aka, specific version of record), to filter likes.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'likes'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              likes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.getLikes#like',\n                },\n              },\n            },\n          },\n        },\n      },\n      like: {\n        type: 'object',\n        required: ['indexedAt', 'createdAt', 'actor'],\n        properties: {\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          actor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetListFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getListFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownList',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPostThread: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPostThread',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to post record.',\n            },\n            depth: {\n              type: 'integer',\n              description:\n                'How many levels of reply depth should be included in response.',\n              default: 6,\n              minimum: 0,\n              maximum: 1000,\n            },\n            parentHeight: {\n              type: 'integer',\n              description:\n                'How many levels of parent (and grandparent, etc) post to include.',\n              default: 80,\n              minimum: 0,\n              maximum: 1000,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.defs#threadViewPost',\n                  'lex:app.bsky.feed.defs#notFoundPost',\n                  'lex:app.bsky.feed.defs#blockedPost',\n                ],\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'NotFound',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.\",\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              description: 'List of post AT-URIs to return hydrated views for.',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetQuotes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getQuotes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of quotes for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to quotes of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'posts'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetRepostedBy: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getRepostedBy',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of reposts for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to reposts of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'repostedBy'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              repostedBy: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested feeds (feed generators) for the requesting account.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetTimeline: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.\",\n        parameters: {\n          type: 'params',\n          properties: {\n            algorithm: {\n              type: 'string',\n              description:\n                \"Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedLike: {\n    lexicon: 1,\n    id: 'app.bsky.feed.like',\n    defs: {\n      main: {\n        type: 'record',\n        description: \"Record declaring a 'like' of a piece of subject content.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.post',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'Record containing a Bluesky post.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['text', 'createdAt'],\n          properties: {\n            text: {\n              type: 'string',\n              maxLength: 3000,\n              maxGraphemes: 300,\n              description:\n                'The primary post content. May be an empty string, if there are embeds.',\n            },\n            entities: {\n              type: 'array',\n              description: 'DEPRECATED: replaced by app.bsky.richtext.facet.',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.post#entity',\n              },\n            },\n            facets: {\n              type: 'array',\n              description:\n                'Annotations of text (mentions, URLs, hashtags, etc)',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            reply: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.post#replyRef',\n            },\n            embed: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images',\n                'lex:app.bsky.embed.video',\n                'lex:app.bsky.embed.external',\n                'lex:app.bsky.embed.record',\n                'lex:app.bsky.embed.recordWithMedia',\n              ],\n            },\n            langs: {\n              type: 'array',\n              description:\n                'Indicates human language of post primary text content.',\n              maxLength: 3,\n              items: {\n                type: 'string',\n                format: 'language',\n              },\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values for this post. Effectively content warnings.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            tags: {\n              type: 'array',\n              description:\n                'Additional hashtags, in addition to any included in post text and facets.',\n              maxLength: 8,\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Client-declared timestamp when this post was originally created.',\n            },\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          parent: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      entity: {\n        type: 'object',\n        description: 'Deprecated: use facets instead.',\n        required: ['index', 'type', 'value'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.post#textSlice',\n          },\n          type: {\n            type: 'string',\n            description: \"Expected values are 'mention' and 'link'.\",\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n      textSlice: {\n        type: 'object',\n        description:\n          'Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.',\n        required: ['start', 'end'],\n        properties: {\n          start: {\n            type: 'integer',\n            minimum: 0,\n          },\n          end: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPostgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.postgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          'Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.',\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            detachedEmbeddingUris: {\n              type: 'array',\n              maxLength: 50,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description:\n                'List of AT-URIs embedding this post that the author has detached from.',\n            },\n            embeddingRules: {\n              description:\n                'List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: ['lex:app.bsky.feed.postgate#disableRule'],\n              },\n            },\n          },\n        },\n      },\n      disableRule: {\n        type: 'object',\n        description: 'Disables embedding of this post.',\n        properties: {},\n      },\n    },\n  },\n  AppBskyFeedRepost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.repost',\n    defs: {\n      main: {\n        description:\n          \"Record representing a 'repost' of an existing Bluesky post.\",\n        type: 'record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedSearchPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.searchPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedSendInteractions: {\n    lexicon: 1,\n    id: 'app.bsky.feed.sendInteractions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Send information about interactions with feed items back to the feed generator that served them.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['interactions'],\n            properties: {\n              feed: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              interactions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#interaction',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedThreadgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.threadgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          \"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.\",\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            allow: {\n              description:\n                'List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.threadgate#mentionRule',\n                  'lex:app.bsky.feed.threadgate#followerRule',\n                  'lex:app.bsky.feed.threadgate#followingRule',\n                  'lex:app.bsky.feed.threadgate#listRule',\n                ],\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            hiddenReplies: {\n              type: 'array',\n              maxLength: 300,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description: 'List of hidden reply URIs.',\n            },\n          },\n        },\n      },\n      mentionRule: {\n        type: 'object',\n        description: 'Allow replies from actors mentioned in your post.',\n        properties: {},\n      },\n      followerRule: {\n        type: 'object',\n        description: 'Allow replies from actors who follow you.',\n        properties: {},\n      },\n      followingRule: {\n        type: 'object',\n        description: 'Allow replies from actors you follow.',\n        properties: {},\n      },\n      listRule: {\n        type: 'object',\n        description: 'Allow replies from actors on a list.',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphBlock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.block',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'DID of the account to be blocked.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphDefs: {\n    lexicon: 1,\n    id: 'app.bsky.graph.defs',\n    defs: {\n      listViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'name', 'purpose'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'name', 'purpose', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listItemView: {\n        type: 'object',\n        required: ['uri', 'subject'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n      starterPackView: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          listItemsSample: {\n            type: 'array',\n            maxLength: 12,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listItemView',\n            },\n          },\n          feeds: {\n            type: 'array',\n            maxLength: 3,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.defs#generatorView',\n            },\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      starterPackViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listPurpose: {\n        type: 'string',\n        knownValues: [\n          'app.bsky.graph.defs#modlist',\n          'app.bsky.graph.defs#curatelist',\n          'app.bsky.graph.defs#referencelist',\n        ],\n      },\n      modlist: {\n        type: 'token',\n        description:\n          'A list of actors to apply an aggregate moderation action (mute/block) on.',\n      },\n      curatelist: {\n        type: 'token',\n        description:\n          'A list of actors used for curation purposes such as list feeds or interaction gating.',\n      },\n      referencelist: {\n        type: 'token',\n        description:\n          'A list of actors used for only for reference purposes such as within a starter pack.',\n      },\n      listViewerState: {\n        type: 'object',\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          blocked: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      notFoundActor: {\n        type: 'object',\n        description: 'indicates that a handle or DID could not be resolved',\n        required: ['actor', 'notFound'],\n        properties: {\n          actor: {\n            type: 'string',\n            format: 'at-identifier',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      relationship: {\n        type: 'object',\n        description:\n          'lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor follows this DID, this is the AT-URI of the follow record',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is followed by this DID, contains the AT-URI of the follow record',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID, this is the AT-URI of the block record',\n          },\n          blockedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID, contains the AT-URI of the block record',\n          },\n          blockingByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID via a block list, this is the AT-URI of the listblock record',\n          },\n          blockedByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphFollow: {\n    lexicon: 1,\n    id: 'app.bsky.graph.follow',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetActorStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getActorStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of starter packs created by the actor.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates which accounts the requesting account is currently blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blocks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blocks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollows: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollows',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which a specified account (actor) follows.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'follows'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              follows: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetKnownFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getKnownFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor) and are followed by the viewer.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getList',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets a 'view' (with additional context) of a specified list.\",\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the list record to hydrate.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list', 'items'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              list: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#listView',\n              },\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listItemView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get mod lists that the requesting account (actor) is blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetLists: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getLists',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to enumerate lists from.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListsWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListsWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['listsWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              listsWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getListsWithMembership#listWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      listWithMembership: {\n        description:\n          'A list and an optional list item indicating membership of a target user to that list.',\n        type: 'object',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['mutes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              mutes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetRelationships: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getRelationships',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates public relationships between one account, and a list of other accounts. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Primary account requesting relationships for.',\n            },\n            others: {\n              type: 'array',\n              description:\n                \"List of 'other' accounts to be related back to the primary.\",\n              maxLength: 30,\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['relationships'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              relationships: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.graph.defs#relationship',\n                    'lex:app.bsky.graph.defs#notFoundActor',\n                  ],\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ActorNotFound',\n            description:\n              'the primary actor at-identifier could not be resolved',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyGraphGetStarterPack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPack',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets a view of a starter pack.',\n        parameters: {\n          type: 'params',\n          required: ['starterPack'],\n          properties: {\n            starterPack: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the starter pack record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPack'],\n            properties: {\n              starterPack: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#starterPackView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get views for a list of starter packs.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacksWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacksWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacksWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacksWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      starterPackWithMembership: {\n        description:\n          'A starter pack and an optional list item indicating membership of a target user to that starter pack.',\n        type: 'object',\n        required: ['starterPack'],\n        properties: {\n          starterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetSuggestedFollowsByActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getSuggestedFollowsByActor',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n              isFallback: {\n                type: 'boolean',\n                description:\n                  'DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid',\n                default: false,\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.list',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'purpose', 'createdAt'],\n          properties: {\n            purpose: {\n              type: 'ref',\n              description:\n                'Defines the purpose of the list (aka, moderation-oriented or curration-oriented)',\n              ref: 'lex:app.bsky.graph.defs#listPurpose',\n            },\n            name: {\n              type: 'string',\n              maxLength: 64,\n              minLength: 1,\n              description: 'Display name for list; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListblock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listblock',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a block relationship against an entire an entire list of accounts (actors).',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the mod list record.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListitem: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listitem',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'list', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'The account which is included on the list.',\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to the list record (app.bsky.graph.list).',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphSearchStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.searchStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find starter packs matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphStarterpack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.starterpack',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record defining a starter pack of actors and feeds for new users.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'list', 'createdAt'],\n          properties: {\n            name: {\n              type: 'string',\n              maxGraphemes: 50,\n              maxLength: 500,\n              minLength: 1,\n              description: 'Display name for starter pack; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            feeds: {\n              type: 'array',\n              maxLength: 3,\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.starterpack#feedItem',\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      feedItem: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified list of accounts. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified thread. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphVerification: {\n    lexicon: 1,\n    id: 'app.bsky.graph.verification',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'handle', 'displayName', 'createdAt'],\n          properties: {\n            subject: {\n              description: 'DID of the subject the verification applies to.',\n              type: 'string',\n              format: 'did',\n            },\n            handle: {\n              description:\n                'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n              type: 'string',\n              format: 'handle',\n            },\n            displayName: {\n              description:\n                'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n              type: 'string',\n            },\n            createdAt: {\n              description: 'Date of when the verification was created.',\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerDefs: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.defs',\n    defs: {\n      labelerView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      labelerViewDetailed: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'policies', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          policies: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          reasonTypes: {\n            description:\n              \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#reasonType',\n            },\n          },\n          subjectTypes: {\n            description:\n              'The set of subject types (account, record, etc) this service accepts reports on.',\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#subjectType',\n            },\n          },\n          subjectCollections: {\n            type: 'array',\n            description:\n              'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n            items: {\n              type: 'string',\n              format: 'nsid',\n            },\n          },\n        },\n      },\n      labelerViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      labelerPolicies: {\n        type: 'object',\n        required: ['labelValues'],\n        properties: {\n          labelValues: {\n            type: 'array',\n            description:\n              'The label values which this labeler publishes. May include global or custom labels.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValue',\n            },\n          },\n          labelValueDefinitions: {\n            type: 'array',\n            description:\n              'Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinition',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerGetServices: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.getServices',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of labeler services.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            detailed: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['views'],\n            properties: {\n              views: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.labeler.defs#labelerView',\n                    'lex:app.bsky.labeler.defs#labelerViewDetailed',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerService: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.service',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of the existence of labeler service.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['policies', 'createdAt'],\n          properties: {\n            policies: {\n              type: 'ref',\n              ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            reasonTypes: {\n              description:\n                \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n            },\n            subjectTypes: {\n              description:\n                'The set of subject types (account, record, etc) this service accepts reports on.',\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#subjectType',\n              },\n            },\n            subjectCollections: {\n              type: 'array',\n              description:\n                'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDeclaration: {\n    lexicon: 1,\n    id: 'app.bsky.notification.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"A declaration of the user's choices related to notifications that can be produced by them.\",\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowSubscriptions'],\n          properties: {\n            allowSubscriptions: {\n              type: 'string',\n              description:\n                \"A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.\",\n              knownValues: ['followers', 'mutuals', 'none'],\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDefs: {\n    lexicon: 1,\n    id: 'app.bsky.notification.defs',\n    defs: {\n      recordDeleted: {\n        type: 'object',\n        properties: {},\n      },\n      chatPreference: {\n        type: 'object',\n        required: ['include', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'accepted'],\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      filterablePreference: {\n        type: 'object',\n        required: ['include', 'list', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'follows'],\n          },\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preference: {\n        type: 'object',\n        required: ['list', 'push'],\n        properties: {\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preferences: {\n        type: 'object',\n        required: [\n          'chat',\n          'follow',\n          'like',\n          'likeViaRepost',\n          'mention',\n          'quote',\n          'reply',\n          'repost',\n          'repostViaRepost',\n          'starterpackJoined',\n          'subscribedPost',\n          'unverified',\n          'verified',\n        ],\n        properties: {\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#chatPreference',\n          },\n          follow: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          like: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          likeViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          mention: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          quote: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repostViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          starterpackJoined: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          subscribedPost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          unverified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          verified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n        },\n      },\n      activitySubscription: {\n        type: 'object',\n        required: ['post', 'reply'],\n        properties: {\n          post: {\n            type: 'boolean',\n          },\n          reply: {\n            type: 'boolean',\n          },\n        },\n      },\n      subjectActivitySubscription: {\n        description:\n          'Object used to store activity subscription data in stash.',\n        type: 'object',\n        required: ['subject', 'activitySubscription'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get notification-related preferences for an account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetUnreadCount: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getUnreadCount',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Count the number of unread notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            priority: {\n              type: 'boolean',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['count'],\n            properties: {\n              count: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListActivitySubscriptions: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listActivitySubscriptions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subscriptions'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subscriptions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListNotifications: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listNotifications',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            reasons: {\n              description: 'Notification reasons to include in response.',\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'A reason that matches the reason property of #notification.',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            priority: {\n              type: 'boolean',\n            },\n            cursor: {\n              type: 'string',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['notifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              notifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.notification.listNotifications#notification',\n                },\n              },\n              priority: {\n                type: 'boolean',\n              },\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      notification: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'author',\n          'reason',\n          'record',\n          'isRead',\n          'indexedAt',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          reason: {\n            type: 'string',\n            description:\n              'The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.',\n            knownValues: [\n              'like',\n              'repost',\n              'follow',\n              'mention',\n              'reply',\n              'quote',\n              'starterpack-joined',\n              'verified',\n              'unverified',\n              'like-via-repost',\n              'repost-via-repost',\n              'subscribed-post',\n              'contact-match',\n            ],\n          },\n          reasonSubject: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          record: {\n            type: 'unknown',\n          },\n          isRead: {\n            type: 'boolean',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutActivitySubscription: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putActivitySubscription',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'activitySubscription'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['priority'],\n            properties: {\n              priority: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferencesV2: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferencesV2',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              chat: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#chatPreference',\n              },\n              follow: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              like: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              likeViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              mention: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              quote: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              reply: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repostViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              starterpackJoined: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              subscribedPost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              unverified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              verified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationRegisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.registerPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Register to receive push notifications, via a specified service, for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n              ageRestricted: {\n                type: 'boolean',\n                description: 'Set to true when the actor is age restricted',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUnregisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.unregisterPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUpdateSeen: {\n    lexicon: 1,\n    id: 'app.bsky.notification.updateSeen',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify server that the requesting account has seen notifications. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['seenAt'],\n            properties: {\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyRichtextFacet: {\n    lexicon: 1,\n    id: 'app.bsky.richtext.facet',\n    defs: {\n      main: {\n        type: 'object',\n        description: 'Annotation of a sub-string within rich text.',\n        required: ['index', 'features'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.richtext.facet#byteSlice',\n          },\n          features: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.richtext.facet#mention',\n                'lex:app.bsky.richtext.facet#link',\n                'lex:app.bsky.richtext.facet#tag',\n              ],\n            },\n          },\n        },\n      },\n      mention: {\n        type: 'object',\n        description:\n          \"Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.\",\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      link: {\n        type: 'object',\n        description:\n          'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      tag: {\n        type: 'object',\n        description:\n          \"Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').\",\n        required: ['tag'],\n        properties: {\n          tag: {\n            type: 'string',\n            maxLength: 640,\n            maxGraphemes: 64,\n          },\n        },\n      },\n      byteSlice: {\n        type: 'object',\n        description:\n          'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.',\n        required: ['byteStart', 'byteEnd'],\n        properties: {\n          byteStart: {\n            type: 'integer',\n            minimum: 0,\n          },\n          byteEnd: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.defs',\n    defs: {\n      skeletonSearchPost: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonSearchActor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      skeletonSearchStarterPack: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      trendingTopic: {\n        type: 'object',\n        required: ['topic', 'link'],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n        },\n      },\n      skeletonTrend: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'dids',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          dids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n      },\n      trendView: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'actors',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          actors: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      threadItemPost: {\n        type: 'object',\n        required: [\n          'post',\n          'moreParents',\n          'moreReplies',\n          'opThread',\n          'hiddenByThreadgate',\n          'mutedByViewer',\n        ],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          moreParents: {\n            type: 'boolean',\n            description:\n              'This post has more parents that were not present in the response. This is just a boolean, without the number of parents.',\n          },\n          moreReplies: {\n            type: 'integer',\n            description:\n              'This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate.',\n          },\n          opThread: {\n            type: 'boolean',\n            description:\n              'This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread.',\n          },\n          hiddenByThreadgate: {\n            type: 'boolean',\n            description:\n              'The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.',\n          },\n          mutedByViewer: {\n            type: 'boolean',\n            description:\n              'This is by an account muted by the viewer requesting it.',\n          },\n        },\n      },\n      threadItemNoUnauthenticated: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemNotFound: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemBlocked: {\n        type: 'object',\n        required: ['author'],\n        properties: {\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      ageAssuranceState: {\n        type: 'object',\n        description:\n          'The computed state of the age assurance process, returned to the user in question on certain authenticated requests.',\n        required: ['status'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description: 'Object used to store age assurance data in stash.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for AA.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetAgeAssuranceState: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getAgeAssuranceState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get miscellaneous runtime configuration.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              checkEmailConfirmed: {\n                type: 'boolean',\n              },\n              liveNow: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getConfig#liveNowConfig',\n                },\n              },\n            },\n          },\n        },\n      },\n      liveNowConfig: {\n        type: 'object',\n        required: ['did', 'domains'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          domains: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedOnboardingUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPopularFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPopularFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'An unspecced view of globally popular feed generators.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            query: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadOtherV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadOtherV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadOtherV2#threadItem',\n                },\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: ['lex:app.bsky.unspecced.defs#threadItemPost'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post.',\n            },\n            above: {\n              type: 'boolean',\n              description: 'Whether to include parents above the anchor.',\n              default: true,\n            },\n            below: {\n              type: 'integer',\n              description:\n                'How many levels of replies to include below the anchor.',\n              default: 6,\n              minimum: 0,\n              maximum: 20,\n            },\n            branchingFactor: {\n              type: 'integer',\n              description:\n                'Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).',\n              default: 10,\n              minimum: 0,\n              maximum: 100,\n            },\n            sort: {\n              type: 'string',\n              description: 'Sorting for the thread replies.',\n              knownValues: ['newest', 'oldest', 'top'],\n              default: 'oldest',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread', 'hasOtherReplies'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadV2#threadItem',\n                },\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n              hasOtherReplies: {\n                type: 'boolean',\n                description:\n                  'Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them.',\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.unspecced.defs#threadItemPost',\n              'lex:app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n              'lex:app.bsky.unspecced.defs#threadItemNotFound',\n              'lex:app.bsky.unspecced.defs#threadItemBlocked',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested feeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedOnboardingUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedOnboardingUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestionsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestionsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            relativeToDid: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n              relativeToDid: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.',\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTaggedSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTaggedSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggestions (feeds and users) tagged with categories',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',\n                },\n              },\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['tag', 'subjectType', 'subject'],\n        properties: {\n          tag: {\n            type: 'string',\n          },\n          subjectType: {\n            type: 'string',\n            knownValues: ['actor', 'feed'],\n          },\n          subject: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendingTopics: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendingTopics',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of trending topics',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['topics', 'suggested'],\n            properties: {\n              topics: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n              suggested: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrends: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrends',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get the current trends on the network',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonTrend',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedInitAgeAssurance: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.initAgeAssurance',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchActorsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchActorsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Actors (profile) search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            typeahead: {\n              type: 'boolean',\n              description: \"If true, acts as fast/simple 'typeahead' query.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchPostsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchPostsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Posts search, returns only skeleton',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                \"DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Starter Pack search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchStarterPack',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyVideoDefs: {\n    lexicon: 1,\n    id: 'app.bsky.video.defs',\n    defs: {\n      jobStatus: {\n        type: 'object',\n        required: ['jobId', 'did', 'state'],\n        properties: {\n          jobId: {\n            type: 'string',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          state: {\n            type: 'string',\n            description:\n              'The state of the video processing job. All values not listed as a known value indicate that the job is in process.',\n            knownValues: ['JOB_STATE_COMPLETED', 'JOB_STATE_FAILED'],\n          },\n          progress: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n            description: 'Progress within the current processing state.',\n          },\n          blob: {\n            type: 'blob',\n          },\n          error: {\n            type: 'string',\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetJobStatus: {\n    lexicon: 1,\n    id: 'app.bsky.video.getJobStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get status details for a video processing job.',\n        parameters: {\n          type: 'params',\n          required: ['jobId'],\n          properties: {\n            jobId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetUploadLimits: {\n    lexicon: 1,\n    id: 'app.bsky.video.getUploadLimits',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get video upload limits for the authenticated user.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canUpload'],\n            properties: {\n              canUpload: {\n                type: 'boolean',\n              },\n              remainingDailyVideos: {\n                type: 'integer',\n              },\n              remainingDailyBytes: {\n                type: 'integer',\n              },\n              message: {\n                type: 'string',\n              },\n              error: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoUploadVideo: {\n    lexicon: 1,\n    id: 'app.bsky.video.uploadVideo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Upload a video to be processed then stored on the PDS.',\n        input: {\n          encoding: 'video/mp4',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeclaration: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky chat account.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowIncoming'],\n          properties: {\n            allowIncoming: {\n              type: 'string',\n              knownValues: ['all', 'none', 'following'],\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          chatDisabled: {\n            type: 'boolean',\n            description:\n              'Set to true when the actor cannot actively participate in conversations',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeleteAccount: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorExportAccountData: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.exportAccountData',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/jsonl',\n        },\n      },\n    },\n  },\n  ChatBskyConvoAcceptConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.acceptConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rev: {\n                description:\n                  'Rev when the convo was accepted. If not present, the convo was already accepted.',\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoAddReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.addReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionLimitReached',\n            description:\n              \"Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent.\",\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.defs',\n    defs: {\n      messageRef: {\n        type: 'object',\n        required: ['did', 'messageId', 'convoId'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          convoId: {\n            type: 'string',\n          },\n          messageId: {\n            type: 'string',\n          },\n        },\n      },\n      messageInput: {\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record'],\n          },\n        },\n      },\n      messageView: {\n        type: 'object',\n        required: ['id', 'rev', 'text', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record#view'],\n          },\n          reactions: {\n            type: 'array',\n            description:\n              'Reactions to this message, in ascending order of creation time.',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.convo.defs#reactionView',\n            },\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      deletedMessageView: {\n        type: 'object',\n        required: ['id', 'rev', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      messageViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      reactionView: {\n        type: 'object',\n        required: ['value', 'sender', 'createdAt'],\n        properties: {\n          value: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionViewSender',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reactionViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      messageAndReactionView: {\n        type: 'object',\n        required: ['message', 'reaction'],\n        properties: {\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      convoView: {\n        type: 'object',\n        required: ['id', 'rev', 'members', 'muted', 'unreadCount'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          members: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.actor.defs#profileViewBasic',\n            },\n          },\n          lastMessage: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          lastReaction: {\n            type: 'union',\n            refs: ['lex:chat.bsky.convo.defs#messageAndReactionView'],\n          },\n          muted: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['request', 'accepted'],\n          },\n          unreadCount: {\n            type: 'integer',\n          },\n        },\n      },\n      logBeginConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logAcceptConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logLeaveConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logMuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logUnmuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logCreateMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logDeleteMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logReadMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logAddReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      logRemoveReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoDeleteMessageForSelf: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.deleteMessageForSelf',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#deletedMessageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvo',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoAvailability: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canChat'],\n            properties: {\n              canChat: {\n                type: 'boolean',\n              },\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoForMembers: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoForMembers',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetLog: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getLog',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: [],\n          properties: {\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['logs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              logs: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#logBeginConvo',\n                    'lex:chat.bsky.convo.defs#logAcceptConvo',\n                    'lex:chat.bsky.convo.defs#logLeaveConvo',\n                    'lex:chat.bsky.convo.defs#logMuteConvo',\n                    'lex:chat.bsky.convo.defs#logUnmuteConvo',\n                    'lex:chat.bsky.convo.defs#logCreateMessage',\n                    'lex:chat.bsky.convo.defs#logDeleteMessage',\n                    'lex:chat.bsky.convo.defs#logReadMessage',\n                    'lex:chat.bsky.convo.defs#logAddReaction',\n                    'lex:chat.bsky.convo.defs#logRemoveReaction',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetMessages: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getMessages',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoLeaveConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.leaveConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'rev'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              rev: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoListConvos: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.listConvos',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            readState: {\n              type: 'string',\n              knownValues: ['unread'],\n            },\n            status: {\n              type: 'string',\n              knownValues: ['request', 'accepted'],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              convos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#convoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoMuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.muteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoRemoveReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.removeReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoSendMessage: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessage',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'message'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageInput',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoSendMessageBatch: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessageBatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.sendMessageBatch#batchItem',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#messageView',\n                },\n              },\n            },\n          },\n        },\n      },\n      batchItem: {\n        type: 'object',\n        required: ['convoId', 'message'],\n        properties: {\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageInput',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUnmuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.unmuteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateAllRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateAllRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              status: {\n                type: 'string',\n                knownValues: ['request', 'accepted'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['updatedCount'],\n            properties: {\n              updatedCount: {\n                description: 'The count of updated convos.',\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetActorMetadata: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getActorMetadata',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['day', 'month', 'all'],\n            properties: {\n              day: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              month: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              all: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n            },\n          },\n        },\n      },\n      metadata: {\n        type: 'object',\n        required: [\n          'messagesSent',\n          'messagesReceived',\n          'convos',\n          'convosStarted',\n        ],\n        properties: {\n          messagesSent: {\n            type: 'integer',\n          },\n          messagesReceived: {\n            type: 'integer',\n          },\n          convos: {\n            type: 'integer',\n          },\n          convosStarted: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetMessageContext: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getMessageContext',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['messageId'],\n          properties: {\n            convoId: {\n              type: 'string',\n              description:\n                'Conversation that the message is from. NOTE: this field will eventually be required.',\n            },\n            messageId: {\n              type: 'string',\n            },\n            before: {\n              type: 'integer',\n              default: 5,\n            },\n            after: {\n              type: 'integer',\n              default: 5,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationUpdateActorAccess: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.updateActorAccess',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor', 'allowAccess'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              allowAccess: {\n                type: 'boolean',\n              },\n              ref: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDefs: {\n    lexicon: 1,\n    id: 'com.atproto.admin.defs',\n    defs: {\n      statusAttr: {\n        type: 'object',\n        required: ['applied'],\n        properties: {\n          applied: {\n            type: 'boolean',\n          },\n          ref: {\n            type: 'string',\n          },\n        },\n      },\n      accountView: {\n        type: 'object',\n        required: ['did', 'handle', 'indexedAt'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoRef: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      repoBlobRef: {\n        type: 'object',\n        required: ['did', 'cid'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      threatSignature: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.admin.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable an account from receiving new invite codes, but does not invalidate existing codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for disabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable some set of codes and/or all codes associated with a set of users.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminEnableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.enableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Re-enable an account's ability to receive invite codes.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for enabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfo: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about an account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfos: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['infos'],\n            properties: {\n              infos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get an admin view of invite codes.',\n        parameters: {\n          type: 'params',\n          properties: {\n            sort: {\n              type: 'string',\n              knownValues: ['recent', 'usage'],\n              default: 'recent',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 500,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getSubjectStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the service-specific admin status of a subject (account, record, or blob).',\n        parameters: {\n          type: 'params',\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            blob: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSearchAccounts: {\n    lexicon: 1,\n    id: 'com.atproto.admin.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of accounts that matches your search query.',\n        parameters: {\n          type: 'params',\n          properties: {\n            email: {\n              type: 'string',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSendEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.sendEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Send email to a user's account email address.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['recipientDid', 'content', 'senderDid'],\n            properties: {\n              recipientDid: {\n                type: 'string',\n                format: 'did',\n              },\n              content: {\n                type: 'string',\n              },\n              subject: {\n                type: 'string',\n              },\n              senderDid: {\n                type: 'string',\n                format: 'did',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  \"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sent'],\n            properties: {\n              sent: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account', 'email'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n                description: 'The handle or DID of the repo.',\n              },\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountHandle: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's handle.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'handle'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountPassword: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the password for a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Administrative action to update an account's signing key in their Did document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'signingKey'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              signingKey: {\n                type: 'string',\n                format: 'did',\n                description: 'Did-key formatted public key',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateSubjectStatus',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the service-specific admin status of a subject (account, record, or blob).',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityDefs: {\n    lexicon: 1,\n    id: 'com.atproto.identity.defs',\n    defs: {\n      identityInfo: {\n        type: 'object',\n        required: ['did', 'handle', 'didDoc'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.\",\n          },\n          didDoc: {\n            type: 'unknown',\n            description: 'The complete DID document for the identity.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityGetRecommendedDidCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.identity.getRecommendedDidCredentials',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Describe the credentials that should be included in the DID doc of an account that is migrating to this service.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rotationKeys: {\n                description:\n                  'Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.',\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityRefreshIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.refreshIdentity',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier'],\n            properties: {\n              identifier: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityRequestPlcOperationSignature: {\n    lexicon: 1,\n    id: 'com.atproto.identity.requestPlcOperationSignature',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to in order to request a signed PLC operation. Requires Auth.',\n      },\n    },\n  },\n  ComAtprotoIdentityResolveDid: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveDid',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves DID to DID document. Does not bi-directionally verify handle.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['didDoc'],\n            properties: {\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for the identity.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveHandle',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description: 'The handle to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveIdentity',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).',\n        parameters: {\n          type: 'params',\n          required: ['identifier'],\n          properties: {\n            identifier: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentitySignPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.signPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Signs a PLC operation to update some value(s) in the requesting DID's document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              token: {\n                description:\n                  'A token received through com.atproto.identity.requestPlcOperationSignature',\n                type: 'string',\n              },\n              rotationKeys: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n                description: 'A signed DID PLC operation.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentitySubmitPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.submitPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityUpdateHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.updateHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'The new handle.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelDefs: {\n    lexicon: 1,\n    id: 'com.atproto.label.defs',\n    defs: {\n      label: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto resource (eg, repo or record).',\n        required: ['src', 'uri', 'val', 'cts'],\n        properties: {\n          ver: {\n            type: 'integer',\n            description: 'The AT Protocol version of the label object.',\n          },\n          src: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the actor who created this label.',\n          },\n          uri: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'AT URI of the record, repository (account), or other resource that this label applies to.',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n            description:\n              \"Optionally, CID specifying the specific version of 'uri' resource this label applies to.\",\n          },\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n          neg: {\n            type: 'boolean',\n            description:\n              'If true, this is a negation label, overwriting a previous label.',\n          },\n          cts: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when this label was created.',\n          },\n          exp: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp at which this label expires (no longer applies).',\n          },\n          sig: {\n            type: 'bytes',\n            description: 'Signature of dag-cbor encoded label.',\n          },\n        },\n      },\n      selfLabels: {\n        type: 'object',\n        description:\n          'Metadata tags on an atproto record, published by the author within the record.',\n        required: ['values'],\n        properties: {\n          values: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#selfLabel',\n            },\n            maxLength: 10,\n          },\n        },\n      },\n      selfLabel: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',\n        required: ['val'],\n        properties: {\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n        },\n      },\n      labelValueDefinition: {\n        type: 'object',\n        description:\n          'Declares a label value and its expected interpretations and behaviors.',\n        required: ['identifier', 'severity', 'blurs', 'locales'],\n        properties: {\n          identifier: {\n            type: 'string',\n            description:\n              \"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).\",\n            maxLength: 100,\n            maxGraphemes: 100,\n          },\n          severity: {\n            type: 'string',\n            description:\n              \"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.\",\n            knownValues: ['inform', 'alert', 'none'],\n          },\n          blurs: {\n            type: 'string',\n            description:\n              \"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.\",\n            knownValues: ['content', 'media', 'none'],\n          },\n          defaultSetting: {\n            type: 'string',\n            description: 'The default setting for this label.',\n            knownValues: ['ignore', 'warn', 'hide'],\n            default: 'warn',\n          },\n          adultOnly: {\n            type: 'boolean',\n            description:\n              'Does the user need to have adult content enabled in order to configure this label?',\n          },\n          locales: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',\n            },\n          },\n        },\n      },\n      labelValueDefinitionStrings: {\n        type: 'object',\n        description:\n          'Strings which describe the label in the UI, localized into a specific language.',\n        required: ['lang', 'name', 'description'],\n        properties: {\n          lang: {\n            type: 'string',\n            description:\n              'The code of the language these strings are written in.',\n            format: 'language',\n          },\n          name: {\n            type: 'string',\n            description: 'A short human-readable name for the label.',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            description:\n              'A longer description of what the label means and why it might be applied.',\n            maxGraphemes: 10000,\n            maxLength: 100000,\n          },\n        },\n      },\n      labelValue: {\n        type: 'string',\n        knownValues: [\n          '!hide',\n          '!no-promote',\n          '!warn',\n          '!no-unauthenticated',\n          'dmca-violation',\n          'doxxing',\n          'porn',\n          'sexual',\n          'nudity',\n          'nsfl',\n          'gore',\n        ],\n      },\n    },\n  },\n  ComAtprotoLabelQueryLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.queryLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.',\n        parameters: {\n          type: 'params',\n          required: ['uriPatterns'],\n          properties: {\n            uriPatterns: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                \"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.\",\n            },\n            sources: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n              description:\n                'Optional list of label sources (DIDs) to filter on.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelSubscribeLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.subscribeLabels',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.label.subscribeLabels#labels',\n              'lex:com.atproto.label.subscribeLabels#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n        ],\n      },\n      labels: {\n        type: 'object',\n        required: ['seq', 'labels'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLexiconResolveLexicon: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.resolveLexicon',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Resolves an atproto lexicon (NSID) to a schema.',\n        parameters: {\n          type: 'params',\n          properties: {\n            nsid: {\n              format: 'nsid',\n              type: 'string',\n              description: 'The lexicon NSID to resolve.',\n            },\n          },\n          required: ['nsid'],\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n                description: 'The CID of the lexicon schema record.',\n              },\n              schema: {\n                type: 'ref',\n                ref: 'lex:com.atproto.lexicon.schema#main',\n                description: 'The resolved lexicon schema record.',\n              },\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n                description: 'The AT-URI of the lexicon schema record.',\n              },\n            },\n            required: ['uri', 'cid', 'schema'],\n          },\n        },\n        errors: [\n          {\n            description: 'No lexicon was resolved for the NSID.',\n            name: 'LexiconNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoLexiconSchema: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.schema',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).\",\n        key: 'nsid',\n        record: {\n          type: 'object',\n          required: ['lexicon'],\n          properties: {\n            lexicon: {\n              type: 'integer',\n              description:\n                \"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.\",\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationCreateReport: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.createReport',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['reasonType', 'subject'],\n            properties: {\n              reasonType: {\n                type: 'ref',\n                description:\n                  'Indicates the broad category of violation the report is for.',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n                description:\n                  'Additional context about the content and violation.',\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.createReport#modTool',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'id',\n              'reasonType',\n              'subject',\n              'reportedBy',\n              'createdAt',\n            ],\n            properties: {\n              id: {\n                type: 'integer',\n              },\n              reasonType: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              reportedBy: {\n                type: 'string',\n                format: 'did',\n              },\n              createdAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationDefs: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'com.atproto.moderation.defs#reasonSpam',\n          'com.atproto.moderation.defs#reasonViolation',\n          'com.atproto.moderation.defs#reasonMisleading',\n          'com.atproto.moderation.defs#reasonSexual',\n          'com.atproto.moderation.defs#reasonRude',\n          'com.atproto.moderation.defs#reasonOther',\n          'com.atproto.moderation.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonSpam: {\n        type: 'token',\n        description:\n          'Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.',\n      },\n      reasonViolation: {\n        type: 'token',\n        description:\n          'Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.',\n      },\n      reasonMisleading: {\n        type: 'token',\n        description:\n          'Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.',\n      },\n      reasonSexual: {\n        type: 'token',\n        description:\n          'Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.',\n      },\n      reasonRude: {\n        type: 'token',\n        description:\n          'Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.',\n      },\n      reasonOther: {\n        type: 'token',\n        description:\n          'Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.',\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      subjectType: {\n        type: 'string',\n        description: 'Tag describing a type of subject that might be reported.',\n        knownValues: ['account', 'record', 'chat'],\n      },\n    },\n  },\n  ComAtprotoRepoApplyWrites: {\n    lexicon: 1,\n    id: 'com.atproto.repo.applyWrites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'writes'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              writes: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#create',\n                    'lex:com.atproto.repo.applyWrites#update',\n                    'lex:com.atproto.repo.applyWrites#delete',\n                  ],\n                  closed: true,\n                },\n              },\n              swapCommit: {\n                type: 'string',\n                description:\n                  'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              results: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#createResult',\n                    'lex:com.atproto.repo.applyWrites#updateResult',\n                    'lex:com.atproto.repo.applyWrites#deleteResult',\n                  ],\n                  closed: true,\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that the 'swapCommit' parameter did not match current commit.\",\n          },\n        ],\n      },\n      create: {\n        type: 'object',\n        description: 'Operation which creates a new record.',\n        required: ['collection', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            maxLength: 512,\n            format: 'record-key',\n            description:\n              'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      update: {\n        type: 'object',\n        description: 'Operation which updates an existing record.',\n        required: ['collection', 'rkey', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      delete: {\n        type: 'object',\n        description: 'Operation which deletes an existing record.',\n        required: ['collection', 'rkey'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n        },\n      },\n      createResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      updateResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      deleteResult: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n    },\n  },\n  ComAtprotoRepoCreateRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.createRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Create a single new repository record. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'record'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record itself. Must contain a $type field.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that 'swapCommit' didn't match current repo commit.\",\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDefs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.defs',\n    defs: {\n      commitMeta: {\n        type: 'object',\n        required: ['cid', 'rev'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoDeleteRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.deleteRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDescribeRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.describeRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about an account and repository, including the list of collections. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'handle',\n              'did',\n              'didDoc',\n              'collections',\n              'handleIsCorrect',\n            ],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for this account.',\n              },\n              collections: {\n                type: 'array',\n                description:\n                  'List of all the collections (NSIDs) for which this repo contains at least one record.',\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              handleIsCorrect: {\n                type: 'boolean',\n                description:\n                  'Indicates if handle is currently valid (resolves bi-directionally)',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a single record from a repository. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection', 'rkey'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record collection.',\n            },\n            rkey: {\n              type: 'string',\n              description: 'The Record Key.',\n              format: 'record-key',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'The CID of the version of the record. If not specified, then return the most recent version.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'value'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              value: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoImportRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.importRepo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.',\n        input: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListMissingBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listMissingBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blobs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blobs: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob',\n                },\n              },\n            },\n          },\n        },\n      },\n      recordBlob: {\n        type: 'object',\n        required: ['cid', 'recordUri'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListRecords: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List a range of records in a repository, matching a specific collection. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record type.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n              description: 'The number of records to return.',\n            },\n            cursor: {\n              type: 'string',\n            },\n            reverse: {\n              type: 'boolean',\n              description: 'Flag to reverse the order of the returned records.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              records: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listRecords#record',\n                },\n              },\n            },\n          },\n        },\n      },\n      record: {\n        type: 'object',\n        required: ['uri', 'cid', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoPutRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.putRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey', 'record'],\n            nullable: ['swapRecord'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record to write.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoStrongRef: {\n    lexicon: 1,\n    id: 'com.atproto.repo.strongRef',\n    description: 'A URI with a content-hash fingerprint.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoUploadBlob: {\n    lexicon: 1,\n    id: 'com.atproto.repo.uploadBlob',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.',\n        input: {\n          encoding: '*/*',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blob'],\n            properties: {\n              blob: {\n                type: 'blob',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerActivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.activateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.\",\n      },\n    },\n  },\n  ComAtprotoServerCheckAccountStatus: {\n    lexicon: 1,\n    id: 'com.atproto.server.checkAccountStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'activated',\n              'validDid',\n              'repoCommit',\n              'repoRev',\n              'repoBlocks',\n              'indexedRecords',\n              'privateStateValues',\n              'expectedBlobs',\n              'importedBlobs',\n            ],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              validDid: {\n                type: 'boolean',\n              },\n              repoCommit: {\n                type: 'string',\n                format: 'cid',\n              },\n              repoRev: {\n                type: 'string',\n              },\n              repoBlocks: {\n                type: 'integer',\n              },\n              indexedRecords: {\n                type: 'integer',\n              },\n              privateStateValues: {\n                type: 'integer',\n              },\n              expectedBlobs: {\n                type: 'integer',\n              },\n              importedBlobs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerConfirmEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.confirmEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'token'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountNotFound',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InvalidEmail',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an account. Implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Requested handle for the account.',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Pre-existing atproto DID, being imported to a new account.',\n              },\n              inviteCode: {\n                type: 'string',\n              },\n              verificationCode: {\n                type: 'string',\n              },\n              verificationPhone: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n                description:\n                  'Initial account password. May need to meet instance-specific password strength requirements.',\n              },\n              recoveryKey: {\n                type: 'string',\n                description:\n                  'DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.',\n              },\n              plcOp: {\n                type: 'unknown',\n                description:\n                  'A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            description:\n              'Account login session returned on successful account creation.',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID of the new account.',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'Complete DID document.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidHandle',\n          },\n          {\n            name: 'InvalidPassword',\n          },\n          {\n            name: 'InvalidInviteCode',\n          },\n          {\n            name: 'HandleNotAvailable',\n          },\n          {\n            name: 'UnsupportedDomain',\n          },\n          {\n            name: 'UnresolvableDid',\n          },\n          {\n            name: 'IncompatibleDidDoc',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an App Password.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description:\n                  'A short name for the App Password, to help distinguish them.',\n              },\n              privileged: {\n                type: 'boolean',\n                description:\n                  \"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.createAppPassword#appPassword',\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'password', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          password: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCode: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCode',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an invite code.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['useCount'],\n            properties: {\n              useCount: {\n                type: 'integer',\n              },\n              forAccount: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['code'],\n            properties: {\n              code: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create invite codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codeCount', 'useCount'],\n            properties: {\n              codeCount: {\n                type: 'integer',\n                default: 1,\n              },\n              useCount: {\n                type: 'integer',\n              },\n              forAccounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.createInviteCodes#accountCodes',\n                },\n              },\n            },\n          },\n        },\n      },\n      accountCodes: {\n        type: 'object',\n        required: ['account', 'codes'],\n        properties: {\n          account: {\n            type: 'string',\n          },\n          codes: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.createSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an authentication session.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier', 'password'],\n            properties: {\n              identifier: {\n                type: 'string',\n                description:\n                  'Handle or other identifier supported by the server for the authenticating user.',\n              },\n              password: {\n                type: 'string',\n              },\n              authFactorToken: {\n                type: 'string',\n              },\n              allowTakendown: {\n                type: 'boolean',\n                description:\n                  'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'AuthFactorTokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeactivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deactivateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              deleteAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'A recommendation to server as to how long they should hold onto the deactivated account before deleting.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDefs: {\n    lexicon: 1,\n    id: 'com.atproto.server.defs',\n    defs: {\n      inviteCode: {\n        type: 'object',\n        required: [\n          'code',\n          'available',\n          'disabled',\n          'forAccount',\n          'createdBy',\n          'createdAt',\n          'uses',\n        ],\n        properties: {\n          code: {\n            type: 'string',\n          },\n          available: {\n            type: 'integer',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          forAccount: {\n            type: 'string',\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          uses: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCodeUse',\n            },\n          },\n        },\n      },\n      inviteCodeUse: {\n        type: 'object',\n        required: ['usedBy', 'usedAt'],\n        properties: {\n          usedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          usedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password', 'token'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeleteSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        errors: [\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDescribeServer: {\n    lexicon: 1,\n    id: 'com.atproto.server.describeServer',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Describes the server's account creation requirements and capabilities. Implemented by PDS.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'availableUserDomains'],\n            properties: {\n              inviteCodeRequired: {\n                type: 'boolean',\n                description:\n                  'If true, an invite code must be supplied to create an account on this instance.',\n              },\n              phoneVerificationRequired: {\n                type: 'boolean',\n                description:\n                  'If true, a phone verification token must be supplied to create an account on this instance.',\n              },\n              availableUserDomains: {\n                type: 'array',\n                description:\n                  'List of domain suffixes that can be used in account handles.',\n                items: {\n                  type: 'string',\n                },\n              },\n              links: {\n                type: 'ref',\n                description: 'URLs of service policy documents.',\n                ref: 'lex:com.atproto.server.describeServer#links',\n              },\n              contact: {\n                type: 'ref',\n                description: 'Contact information',\n                ref: 'lex:com.atproto.server.describeServer#contact',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n            format: 'uri',\n          },\n          termsOfService: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      contact: {\n        type: 'object',\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerGetAccountInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.getAccountInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get all invite codes for the current account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            includeUsed: {\n              type: 'boolean',\n              default: true,\n            },\n            createAvailable: {\n              type: 'boolean',\n              default: true,\n              description:\n                \"Controls whether any new 'earned' but not 'created' invites should be created.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateCreate',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetServiceAuth: {\n    lexicon: 1,\n    id: 'com.atproto.server.getServiceAuth',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a signed token on behalf of the requesting DID for the requested service.',\n        parameters: {\n          type: 'params',\n          required: ['aud'],\n          properties: {\n            aud: {\n              type: 'string',\n              format: 'did',\n              description:\n                'The DID of the service that the token will be used to authenticate with',\n            },\n            exp: {\n              type: 'integer',\n              description:\n                'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',\n            },\n            lxm: {\n              type: 'string',\n              format: 'nsid',\n              description:\n                'Lexicon (XRPC) method to bind the requested token to',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadExpiration',\n            description:\n              'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.getSession',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about the current auth session. Requires auth.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'did'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerListAppPasswords: {\n    lexicon: 1,\n    id: 'com.atproto.server.listAppPasswords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all App Passwords.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['passwords'],\n            properties: {\n              passwords: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.listAppPasswords#appPassword',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRefreshSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.refreshSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  \"Hosting status of the account. If not specified, then assume 'active'.\",\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRequestAccountDelete: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestAccountDelete',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account deletion via email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailConfirmation: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailConfirmation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to confirm ownership of email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Request a token in order to update email.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['tokenRequired'],\n            properties: {\n              tokenRequired: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRequestPasswordReset: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestPasswordReset',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account password reset via email.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerReserveSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.server.reserveSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID to reserve a key for.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['signingKey'],\n            properties: {\n              signingKey: {\n                type: 'string',\n                description:\n                  'The public key for the reserved signing key, in did:key serialization.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerResetPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.resetPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Reset a user account password using a token.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'password'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRevokeAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.revokeAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Revoke an App Password by name.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerUpdateEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.updateEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              token: {\n                type: 'string',\n                description:\n                  \"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.\",\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'TokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncDefs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.defs',\n    defs: {\n      hostStatus: {\n        type: 'string',\n        knownValues: ['active', 'idle', 'offline', 'throttled', 'banned'],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlob: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlob',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cid'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the account.',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description: 'The CID of the blob to fetch',\n            },\n          },\n        },\n        output: {\n          encoding: '*/*',\n        },\n        errors: [\n          {\n            name: 'BlobNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlocks: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cids'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            cids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'BlockNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetCheckout: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getCheckout',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'DEPRECATED - please use com.atproto.sync.getRepo instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoSyncGetHead: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED - please use com.atproto.sync.getLatestCommit instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HeadNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetHostStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHostStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns information about a specified upstream host, as consumed by the server. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          required: ['hostname'],\n          properties: {\n            hostname: {\n              type: 'string',\n              description:\n                'Hostname of the host (eg, PDS or relay) being queried.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n              },\n              seq: {\n                type: 'integer',\n                description:\n                  'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n              },\n              accountCount: {\n                type: 'integer',\n                description:\n                  'Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.',\n              },\n              status: {\n                type: 'ref',\n                ref: 'lex:com.atproto.sync.defs#hostStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetLatestCommit: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getLatestCommit',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the current commit CID & revision of the specified repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cid', 'rev'],\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'collection', 'rkey'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            rkey: {\n              type: 'string',\n              description: 'Record Key',\n              format: 'record-key',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepo: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.\",\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description:\n                \"The revision ('rev') of the repo to create a diff from.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepoStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepoStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'active'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: [\n                  'takendown',\n                  'suspended',\n                  'deleted',\n                  'deactivated',\n                  'desynchronized',\n                  'throttled',\n                ],\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n                description:\n                  'Optional field, the current rev of the repo, if active=true',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description: 'Optional revision of the repo to list blobs since.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cids'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              cids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListHosts: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listHosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 200,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hosts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hosts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listHosts#host',\n                },\n                description:\n                  'Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.',\n              },\n            },\n          },\n        },\n      },\n      host: {\n        type: 'object',\n        required: ['hostname'],\n        properties: {\n          hostname: {\n            type: 'string',\n            description: 'hostname of server; not a URL (no scheme)',\n          },\n          seq: {\n            type: 'integer',\n            description:\n              'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n          },\n          accountCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:com.atproto.sync.defs#hostStatus',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listRepos#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did', 'head', 'rev'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          head: {\n            type: 'string',\n            format: 'cid',\n            description: 'Current repo commit CID',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n          active: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListReposByCollection: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listReposByCollection',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DIDs which have records with the given collection NSID.',\n        parameters: {\n          type: 'params',\n          required: ['collection'],\n          properties: {\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            limit: {\n              type: 'integer',\n              description:\n                'Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.',\n              minimum: 1,\n              maximum: 2000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listReposByCollection#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncNotifyOfUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.sync.notifyOfUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (usually a PDS) that is notifying of update.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncRequestCrawl: {\n    lexicon: 1,\n    id: 'com.atproto.sync.requestCrawl',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (eg, PDS) that is requesting to be crawled.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostBanned',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncSubscribeRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.subscribeRepos',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.sync.subscribeRepos#commit',\n              'lex:com.atproto.sync.subscribeRepos#sync',\n              'lex:com.atproto.sync.subscribeRepos#identity',\n              'lex:com.atproto.sync.subscribeRepos#account',\n              'lex:com.atproto.sync.subscribeRepos#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n          {\n            name: 'ConsumerTooSlow',\n            description:\n              'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.',\n          },\n        ],\n      },\n      commit: {\n        type: 'object',\n        description:\n          'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.',\n        required: [\n          'seq',\n          'rebase',\n          'tooBig',\n          'repo',\n          'commit',\n          'rev',\n          'since',\n          'blocks',\n          'ops',\n          'blobs',\n          'time',\n        ],\n        nullable: ['since'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          rebase: {\n            type: 'boolean',\n            description: 'DEPRECATED -- unused',\n          },\n          tooBig: {\n            type: 'boolean',\n            description:\n              'DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.',\n          },\n          repo: {\n            type: 'string',\n            format: 'did',\n            description:\n              \"The repo this event comes from. Note that all other message types name this field 'did'.\",\n          },\n          commit: {\n            type: 'cid-link',\n            description: 'Repo commit object CID.',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.',\n          },\n          since: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the last emitted commit from this repo (if any).',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.\",\n            maxLength: 2000000,\n          },\n          ops: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.sync.subscribeRepos#repoOp',\n              description:\n                'List of repo mutation operations in this commit (eg, records created, updated, or deleted).',\n            },\n            maxLength: 200,\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'cid-link',\n              description:\n                'DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.',\n            },\n          },\n          prevData: {\n            type: 'cid-link',\n            description:\n              \"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.\",\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      sync: {\n        type: 'object',\n        description:\n          'Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.',\n        required: ['seq', 'did', 'blocks', 'rev', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description:\n              'The account this repo event corresponds to. Must match that in the commit object.',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.\",\n            maxLength: 10000,\n          },\n          rev: {\n            type: 'string',\n            description:\n              'The rev of the commit. This value must match that in the commit object.',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      identity: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.\",\n        required: ['seq', 'did', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.\",\n          },\n        },\n      },\n      account: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.\",\n        required: ['seq', 'did', 'time', 'active'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a reason for why the account is not active.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n      repoOp: {\n        type: 'object',\n        description: 'A repo operation, ie a mutation of a single record.',\n        required: ['action', 'path', 'cid'],\n        nullable: ['cid'],\n        properties: {\n          action: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          path: {\n            type: 'string',\n          },\n          cid: {\n            type: 'cid-link',\n            description:\n              'For creates and updates, the new record CID. For deletions, null.',\n          },\n          prev: {\n            type: 'cid-link',\n            description:\n              'For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempAddReservedHandle: {\n    lexicon: 1,\n    id: 'com.atproto.temp.addReservedHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a handle to the set of reserved handles.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckHandleAvailability: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkHandleAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description:\n                'Tentative handle. Will be checked for availability or used to build handle suggestions.',\n            },\n            email: {\n              type: 'string',\n              description:\n                'User-provided email. Might be used to build handle suggestions.',\n            },\n            birthDate: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'User-provided birth date. Might be used to build handle suggestions.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'result'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Echo of the input handle.',\n              },\n              result: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.temp.checkHandleAvailability#resultAvailable',\n                  'lex:com.atproto.temp.checkHandleAvailability#resultUnavailable',\n                ],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n            description: 'An invalid email was provided.',\n          },\n        ],\n      },\n      resultAvailable: {\n        type: 'object',\n        description: 'Indicates the provided handle is available.',\n        properties: {},\n      },\n      resultUnavailable: {\n        type: 'object',\n        description:\n          'Indicates the provided handle is unavailable and gives suggestions of available handles.',\n        required: ['suggestions'],\n        properties: {\n          suggestions: {\n            type: 'array',\n            description:\n              'List of suggested handles based on the provided inputs.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.temp.checkHandleAvailability#suggestion',\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['handle', 'method'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          method: {\n            type: 'string',\n            description:\n              'Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckSignupQueue: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkSignupQueue',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Check accounts location in signup queue.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['activated'],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              placeInQueue: {\n                type: 'integer',\n              },\n              estimatedTimeMs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempDereferenceScope: {\n    lexicon: 1,\n    id: 'com.atproto.temp.dereferenceScope',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Allows finding the oauth permission scope from a reference',\n        parameters: {\n          type: 'params',\n          required: ['scope'],\n          properties: {\n            scope: {\n              type: 'string',\n              description: \"The scope reference (starts with 'ref:')\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['scope'],\n            properties: {\n              scope: {\n                type: 'string',\n                description: 'The full oauth permission scope',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidScopeReference',\n            description: 'An invalid scope reference was provided.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoTempFetchLabels: {\n    lexicon: 1,\n    id: 'com.atproto.temp.fetchLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.',\n        parameters: {\n          type: 'params',\n          properties: {\n            since: {\n              type: 'integer',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRequestPhoneVerification: {\n    lexicon: 1,\n    id: 'com.atproto.temp.requestPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a verification code to be sent to the supplied phone number',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phoneNumber'],\n            properties: {\n              phoneNumber: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRevokeAccountCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.temp.revokeAccountCredentials',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComGermnetworkDeclaration: {\n    lexicon: 1,\n    id: 'com.germnetwork.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Germ Network account',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['version', 'currentKey'],\n          properties: {\n            version: {\n              type: 'string',\n              description:\n                'Semver version number, without pre-release or build information, for the format of opaque content',\n              minLength: 5,\n              maxLength: 14,\n            },\n            currentKey: {\n              type: 'bytes',\n              description:\n                'Opaque value, an ed25519 public key prefixed with a byte enum',\n            },\n            messageMe: {\n              type: 'ref',\n              description: 'Controls who can message this account',\n              ref: 'lex:com.germnetwork.declaration#messageMe',\n            },\n            keyPackage: {\n              type: 'bytes',\n              description:\n                'Opaque value, contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey',\n            },\n            continuityProofs: {\n              type: 'array',\n              description: 'Array of opaque values to allow for key rolling',\n              items: {\n                type: 'bytes',\n              },\n              maxLength: 1000,\n            },\n          },\n        },\n      },\n      messageMe: {\n        type: 'object',\n        required: ['showButtonTo', 'messageMeUrl'],\n        properties: {\n          messageMeUrl: {\n            type: 'string',\n            description:\n              'A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who wish to message each other',\n            format: 'uri',\n            minLength: 1,\n            maxLength: 2047,\n          },\n          showButtonTo: {\n            type: 'string',\n            knownValues: ['none', 'usersIFollow', 'everyone'],\n            description:\n              \"The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer.\",\n            minLength: 1,\n            maxLength: 100,\n          },\n        },\n      },\n    },\n  },\n} as const satisfies Record<string, LexiconDoc>\nexport const schemas = Object.values(schemaDict) satisfies LexiconDoc[]\nexport const lexicons: Lexicons = new Lexicons(schemas)\n\nexport function validate<T extends { $type: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType: true,\n): ValidationResult<T>\nexport function validate<T extends { $type?: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: false,\n): ValidationResult<T>\nexport function validate(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: boolean,\n): ValidationResult {\n  return (requiredType ? is$typed : maybe$typed)(v, id, hash)\n    ? lexicons.validate(`${id}#${hash}`, v)\n    : {\n        success: false,\n        error: new ValidationError(\n          `Must be an object with \"${hash === 'main' ? id : `${id}#${hash}`}\" $type property`,\n        ),\n      }\n}\n\nexport const ids = {\n  AppBskyActorDefs: 'app.bsky.actor.defs',\n  AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences',\n  AppBskyActorGetProfile: 'app.bsky.actor.getProfile',\n  AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles',\n  AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions',\n  AppBskyActorProfile: 'app.bsky.actor.profile',\n  AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',\n  AppBskyActorSearchActors: 'app.bsky.actor.searchActors',\n  AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',\n  AppBskyActorStatus: 'app.bsky.actor.status',\n  AppBskyAgeassuranceBegin: 'app.bsky.ageassurance.begin',\n  AppBskyAgeassuranceDefs: 'app.bsky.ageassurance.defs',\n  AppBskyAgeassuranceGetConfig: 'app.bsky.ageassurance.getConfig',\n  AppBskyAgeassuranceGetState: 'app.bsky.ageassurance.getState',\n  AppBskyBookmarkCreateBookmark: 'app.bsky.bookmark.createBookmark',\n  AppBskyBookmarkDefs: 'app.bsky.bookmark.defs',\n  AppBskyBookmarkDeleteBookmark: 'app.bsky.bookmark.deleteBookmark',\n  AppBskyBookmarkGetBookmarks: 'app.bsky.bookmark.getBookmarks',\n  AppBskyContactDefs: 'app.bsky.contact.defs',\n  AppBskyContactDismissMatch: 'app.bsky.contact.dismissMatch',\n  AppBskyContactGetMatches: 'app.bsky.contact.getMatches',\n  AppBskyContactGetSyncStatus: 'app.bsky.contact.getSyncStatus',\n  AppBskyContactImportContacts: 'app.bsky.contact.importContacts',\n  AppBskyContactRemoveData: 'app.bsky.contact.removeData',\n  AppBskyContactSendNotification: 'app.bsky.contact.sendNotification',\n  AppBskyContactStartPhoneVerification:\n    'app.bsky.contact.startPhoneVerification',\n  AppBskyContactVerifyPhone: 'app.bsky.contact.verifyPhone',\n  AppBskyDraftCreateDraft: 'app.bsky.draft.createDraft',\n  AppBskyDraftDefs: 'app.bsky.draft.defs',\n  AppBskyDraftDeleteDraft: 'app.bsky.draft.deleteDraft',\n  AppBskyDraftGetDrafts: 'app.bsky.draft.getDrafts',\n  AppBskyDraftUpdateDraft: 'app.bsky.draft.updateDraft',\n  AppBskyEmbedDefs: 'app.bsky.embed.defs',\n  AppBskyEmbedExternal: 'app.bsky.embed.external',\n  AppBskyEmbedImages: 'app.bsky.embed.images',\n  AppBskyEmbedRecord: 'app.bsky.embed.record',\n  AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',\n  AppBskyEmbedVideo: 'app.bsky.embed.video',\n  AppBskyFeedDefs: 'app.bsky.feed.defs',\n  AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator',\n  AppBskyFeedGenerator: 'app.bsky.feed.generator',\n  AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds',\n  AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes',\n  AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',\n  AppBskyFeedGetFeed: 'app.bsky.feed.getFeed',\n  AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator',\n  AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators',\n  AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton',\n  AppBskyFeedGetLikes: 'app.bsky.feed.getLikes',\n  AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed',\n  AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',\n  AppBskyFeedGetPosts: 'app.bsky.feed.getPosts',\n  AppBskyFeedGetQuotes: 'app.bsky.feed.getQuotes',\n  AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',\n  AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds',\n  AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline',\n  AppBskyFeedLike: 'app.bsky.feed.like',\n  AppBskyFeedPost: 'app.bsky.feed.post',\n  AppBskyFeedPostgate: 'app.bsky.feed.postgate',\n  AppBskyFeedRepost: 'app.bsky.feed.repost',\n  AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts',\n  AppBskyFeedSendInteractions: 'app.bsky.feed.sendInteractions',\n  AppBskyFeedThreadgate: 'app.bsky.feed.threadgate',\n  AppBskyGraphBlock: 'app.bsky.graph.block',\n  AppBskyGraphDefs: 'app.bsky.graph.defs',\n  AppBskyGraphFollow: 'app.bsky.graph.follow',\n  AppBskyGraphGetActorStarterPacks: 'app.bsky.graph.getActorStarterPacks',\n  AppBskyGraphGetBlocks: 'app.bsky.graph.getBlocks',\n  AppBskyGraphGetFollowers: 'app.bsky.graph.getFollowers',\n  AppBskyGraphGetFollows: 'app.bsky.graph.getFollows',\n  AppBskyGraphGetKnownFollowers: 'app.bsky.graph.getKnownFollowers',\n  AppBskyGraphGetList: 'app.bsky.graph.getList',\n  AppBskyGraphGetListBlocks: 'app.bsky.graph.getListBlocks',\n  AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes',\n  AppBskyGraphGetLists: 'app.bsky.graph.getLists',\n  AppBskyGraphGetListsWithMembership: 'app.bsky.graph.getListsWithMembership',\n  AppBskyGraphGetMutes: 'app.bsky.graph.getMutes',\n  AppBskyGraphGetRelationships: 'app.bsky.graph.getRelationships',\n  AppBskyGraphGetStarterPack: 'app.bsky.graph.getStarterPack',\n  AppBskyGraphGetStarterPacks: 'app.bsky.graph.getStarterPacks',\n  AppBskyGraphGetStarterPacksWithMembership:\n    'app.bsky.graph.getStarterPacksWithMembership',\n  AppBskyGraphGetSuggestedFollowsByActor:\n    'app.bsky.graph.getSuggestedFollowsByActor',\n  AppBskyGraphList: 'app.bsky.graph.list',\n  AppBskyGraphListblock: 'app.bsky.graph.listblock',\n  AppBskyGraphListitem: 'app.bsky.graph.listitem',\n  AppBskyGraphMuteActor: 'app.bsky.graph.muteActor',\n  AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList',\n  AppBskyGraphMuteThread: 'app.bsky.graph.muteThread',\n  AppBskyGraphSearchStarterPacks: 'app.bsky.graph.searchStarterPacks',\n  AppBskyGraphStarterpack: 'app.bsky.graph.starterpack',\n  AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor',\n  AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList',\n  AppBskyGraphUnmuteThread: 'app.bsky.graph.unmuteThread',\n  AppBskyGraphVerification: 'app.bsky.graph.verification',\n  AppBskyLabelerDefs: 'app.bsky.labeler.defs',\n  AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',\n  AppBskyLabelerService: 'app.bsky.labeler.service',\n  AppBskyNotificationDeclaration: 'app.bsky.notification.declaration',\n  AppBskyNotificationDefs: 'app.bsky.notification.defs',\n  AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',\n  AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',\n  AppBskyNotificationListActivitySubscriptions:\n    'app.bsky.notification.listActivitySubscriptions',\n  AppBskyNotificationListNotifications:\n    'app.bsky.notification.listNotifications',\n  AppBskyNotificationPutActivitySubscription:\n    'app.bsky.notification.putActivitySubscription',\n  AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',\n  AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',\n  AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',\n  AppBskyNotificationUnregisterPush: 'app.bsky.notification.unregisterPush',\n  AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',\n  AppBskyRichtextFacet: 'app.bsky.richtext.facet',\n  AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',\n  AppBskyUnspeccedGetAgeAssuranceState:\n    'app.bsky.unspecced.getAgeAssuranceState',\n  AppBskyUnspeccedGetConfig: 'app.bsky.unspecced.getConfig',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetPopularFeedGenerators:\n    'app.bsky.unspecced.getPopularFeedGenerators',\n  AppBskyUnspeccedGetPostThreadOtherV2:\n    'app.bsky.unspecced.getPostThreadOtherV2',\n  AppBskyUnspeccedGetPostThreadV2: 'app.bsky.unspecced.getPostThreadV2',\n  AppBskyUnspeccedGetSuggestedFeeds: 'app.bsky.unspecced.getSuggestedFeeds',\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton:\n    'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n  AppBskyUnspeccedGetSuggestedOnboardingUsers:\n    'app.bsky.unspecced.getSuggestedOnboardingUsers',\n  AppBskyUnspeccedGetSuggestedStarterPacks:\n    'app.bsky.unspecced.getSuggestedStarterPacks',\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetSuggestedUsers: 'app.bsky.unspecced.getSuggestedUsers',\n  AppBskyUnspeccedGetSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetSuggestionsSkeleton:\n    'app.bsky.unspecced.getSuggestionsSkeleton',\n  AppBskyUnspeccedGetTaggedSuggestions:\n    'app.bsky.unspecced.getTaggedSuggestions',\n  AppBskyUnspeccedGetTrendingTopics: 'app.bsky.unspecced.getTrendingTopics',\n  AppBskyUnspeccedGetTrends: 'app.bsky.unspecced.getTrends',\n  AppBskyUnspeccedGetTrendsSkeleton: 'app.bsky.unspecced.getTrendsSkeleton',\n  AppBskyUnspeccedInitAgeAssurance: 'app.bsky.unspecced.initAgeAssurance',\n  AppBskyUnspeccedSearchActorsSkeleton:\n    'app.bsky.unspecced.searchActorsSkeleton',\n  AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton',\n  AppBskyUnspeccedSearchStarterPacksSkeleton:\n    'app.bsky.unspecced.searchStarterPacksSkeleton',\n  AppBskyVideoDefs: 'app.bsky.video.defs',\n  AppBskyVideoGetJobStatus: 'app.bsky.video.getJobStatus',\n  AppBskyVideoGetUploadLimits: 'app.bsky.video.getUploadLimits',\n  AppBskyVideoUploadVideo: 'app.bsky.video.uploadVideo',\n  ChatBskyActorDeclaration: 'chat.bsky.actor.declaration',\n  ChatBskyActorDefs: 'chat.bsky.actor.defs',\n  ChatBskyActorDeleteAccount: 'chat.bsky.actor.deleteAccount',\n  ChatBskyActorExportAccountData: 'chat.bsky.actor.exportAccountData',\n  ChatBskyConvoAcceptConvo: 'chat.bsky.convo.acceptConvo',\n  ChatBskyConvoAddReaction: 'chat.bsky.convo.addReaction',\n  ChatBskyConvoDefs: 'chat.bsky.convo.defs',\n  ChatBskyConvoDeleteMessageForSelf: 'chat.bsky.convo.deleteMessageForSelf',\n  ChatBskyConvoGetConvo: 'chat.bsky.convo.getConvo',\n  ChatBskyConvoGetConvoAvailability: 'chat.bsky.convo.getConvoAvailability',\n  ChatBskyConvoGetConvoForMembers: 'chat.bsky.convo.getConvoForMembers',\n  ChatBskyConvoGetLog: 'chat.bsky.convo.getLog',\n  ChatBskyConvoGetMessages: 'chat.bsky.convo.getMessages',\n  ChatBskyConvoLeaveConvo: 'chat.bsky.convo.leaveConvo',\n  ChatBskyConvoListConvos: 'chat.bsky.convo.listConvos',\n  ChatBskyConvoMuteConvo: 'chat.bsky.convo.muteConvo',\n  ChatBskyConvoRemoveReaction: 'chat.bsky.convo.removeReaction',\n  ChatBskyConvoSendMessage: 'chat.bsky.convo.sendMessage',\n  ChatBskyConvoSendMessageBatch: 'chat.bsky.convo.sendMessageBatch',\n  ChatBskyConvoUnmuteConvo: 'chat.bsky.convo.unmuteConvo',\n  ChatBskyConvoUpdateAllRead: 'chat.bsky.convo.updateAllRead',\n  ChatBskyConvoUpdateRead: 'chat.bsky.convo.updateRead',\n  ChatBskyModerationGetActorMetadata: 'chat.bsky.moderation.getActorMetadata',\n  ChatBskyModerationGetMessageContext: 'chat.bsky.moderation.getMessageContext',\n  ChatBskyModerationUpdateActorAccess: 'chat.bsky.moderation.updateActorAccess',\n  ComAtprotoAdminDefs: 'com.atproto.admin.defs',\n  ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount',\n  ComAtprotoAdminDisableAccountInvites:\n    'com.atproto.admin.disableAccountInvites',\n  ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',\n  ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites',\n  ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo',\n  ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos',\n  ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes',\n  ComAtprotoAdminGetSubjectStatus: 'com.atproto.admin.getSubjectStatus',\n  ComAtprotoAdminSearchAccounts: 'com.atproto.admin.searchAccounts',\n  ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail',\n  ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail',\n  ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle',\n  ComAtprotoAdminUpdateAccountPassword:\n    'com.atproto.admin.updateAccountPassword',\n  ComAtprotoAdminUpdateAccountSigningKey:\n    'com.atproto.admin.updateAccountSigningKey',\n  ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus',\n  ComAtprotoIdentityDefs: 'com.atproto.identity.defs',\n  ComAtprotoIdentityGetRecommendedDidCredentials:\n    'com.atproto.identity.getRecommendedDidCredentials',\n  ComAtprotoIdentityRefreshIdentity: 'com.atproto.identity.refreshIdentity',\n  ComAtprotoIdentityRequestPlcOperationSignature:\n    'com.atproto.identity.requestPlcOperationSignature',\n  ComAtprotoIdentityResolveDid: 'com.atproto.identity.resolveDid',\n  ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle',\n  ComAtprotoIdentityResolveIdentity: 'com.atproto.identity.resolveIdentity',\n  ComAtprotoIdentitySignPlcOperation: 'com.atproto.identity.signPlcOperation',\n  ComAtprotoIdentitySubmitPlcOperation:\n    'com.atproto.identity.submitPlcOperation',\n  ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle',\n  ComAtprotoLabelDefs: 'com.atproto.label.defs',\n  ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels',\n  ComAtprotoLabelSubscribeLabels: 'com.atproto.label.subscribeLabels',\n  ComAtprotoLexiconResolveLexicon: 'com.atproto.lexicon.resolveLexicon',\n  ComAtprotoLexiconSchema: 'com.atproto.lexicon.schema',\n  ComAtprotoModerationCreateReport: 'com.atproto.moderation.createReport',\n  ComAtprotoModerationDefs: 'com.atproto.moderation.defs',\n  ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites',\n  ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord',\n  ComAtprotoRepoDefs: 'com.atproto.repo.defs',\n  ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord',\n  ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo',\n  ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',\n  ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo',\n  ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs',\n  ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',\n  ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',\n  ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',\n  ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',\n  ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount',\n  ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus',\n  ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',\n  ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',\n  ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',\n  ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',\n  ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes',\n  ComAtprotoServerCreateSession: 'com.atproto.server.createSession',\n  ComAtprotoServerDeactivateAccount: 'com.atproto.server.deactivateAccount',\n  ComAtprotoServerDefs: 'com.atproto.server.defs',\n  ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount',\n  ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession',\n  ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer',\n  ComAtprotoServerGetAccountInviteCodes:\n    'com.atproto.server.getAccountInviteCodes',\n  ComAtprotoServerGetServiceAuth: 'com.atproto.server.getServiceAuth',\n  ComAtprotoServerGetSession: 'com.atproto.server.getSession',\n  ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords',\n  ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',\n  ComAtprotoServerRequestAccountDelete:\n    'com.atproto.server.requestAccountDelete',\n  ComAtprotoServerRequestEmailConfirmation:\n    'com.atproto.server.requestEmailConfirmation',\n  ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',\n  ComAtprotoServerRequestPasswordReset:\n    'com.atproto.server.requestPasswordReset',\n  ComAtprotoServerReserveSigningKey: 'com.atproto.server.reserveSigningKey',\n  ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',\n  ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',\n  ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',\n  ComAtprotoSyncDefs: 'com.atproto.sync.defs',\n  ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',\n  ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',\n  ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',\n  ComAtprotoSyncGetHead: 'com.atproto.sync.getHead',\n  ComAtprotoSyncGetHostStatus: 'com.atproto.sync.getHostStatus',\n  ComAtprotoSyncGetLatestCommit: 'com.atproto.sync.getLatestCommit',\n  ComAtprotoSyncGetRecord: 'com.atproto.sync.getRecord',\n  ComAtprotoSyncGetRepo: 'com.atproto.sync.getRepo',\n  ComAtprotoSyncGetRepoStatus: 'com.atproto.sync.getRepoStatus',\n  ComAtprotoSyncListBlobs: 'com.atproto.sync.listBlobs',\n  ComAtprotoSyncListHosts: 'com.atproto.sync.listHosts',\n  ComAtprotoSyncListRepos: 'com.atproto.sync.listRepos',\n  ComAtprotoSyncListReposByCollection: 'com.atproto.sync.listReposByCollection',\n  ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate',\n  ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',\n  ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',\n  ComAtprotoTempAddReservedHandle: 'com.atproto.temp.addReservedHandle',\n  ComAtprotoTempCheckHandleAvailability:\n    'com.atproto.temp.checkHandleAvailability',\n  ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue',\n  ComAtprotoTempDereferenceScope: 'com.atproto.temp.dereferenceScope',\n  ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels',\n  ComAtprotoTempRequestPhoneVerification:\n    'com.atproto.temp.requestPhoneVerification',\n  ComAtprotoTempRevokeAccountCredentials:\n    'com.atproto.temp.revokeAccountCredentials',\n  ComGermnetworkDeclaration: 'com.germnetwork.declaration',\n} as const\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyNotificationDefs from '../notification/defs.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'app.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  createdAt?: string\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n\nexport interface ProfileView {\n  $type?: 'app.bsky.actor.defs#profileView'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  description?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileView = 'profileView'\n\nexport function isProfileView<V>(v: V) {\n  return is$typed(v, id, hashProfileView)\n}\n\nexport function validateProfileView<V>(v: V) {\n  return validate<ProfileView & V>(v, id, hashProfileView)\n}\n\nexport interface ProfileViewDetailed {\n  $type?: 'app.bsky.actor.defs#profileViewDetailed'\n  did: string\n  handle: string\n  displayName?: string\n  description?: string\n  pronouns?: string\n  website?: string\n  avatar?: string\n  banner?: string\n  followersCount?: number\n  followsCount?: number\n  postsCount?: number\n  associated?: ProfileAssociated\n  joinedViaStarterPack?: AppBskyGraphDefs.StarterPackViewBasic\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewDetailed = 'profileViewDetailed'\n\nexport function isProfileViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashProfileViewDetailed)\n}\n\nexport function validateProfileViewDetailed<V>(v: V) {\n  return validate<ProfileViewDetailed & V>(v, id, hashProfileViewDetailed)\n}\n\nexport interface ProfileAssociated {\n  $type?: 'app.bsky.actor.defs#profileAssociated'\n  lists?: number\n  feedgens?: number\n  starterPacks?: number\n  labeler?: boolean\n  chat?: ProfileAssociatedChat\n  activitySubscription?: ProfileAssociatedActivitySubscription\n  germ?: ProfileAssociatedGerm\n}\n\nconst hashProfileAssociated = 'profileAssociated'\n\nexport function isProfileAssociated<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociated)\n}\n\nexport function validateProfileAssociated<V>(v: V) {\n  return validate<ProfileAssociated & V>(v, id, hashProfileAssociated)\n}\n\nexport interface ProfileAssociatedChat {\n  $type?: 'app.bsky.actor.defs#profileAssociatedChat'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n}\n\nconst hashProfileAssociatedChat = 'profileAssociatedChat'\n\nexport function isProfileAssociatedChat<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedChat)\n}\n\nexport function validateProfileAssociatedChat<V>(v: V) {\n  return validate<ProfileAssociatedChat & V>(v, id, hashProfileAssociatedChat)\n}\n\nexport interface ProfileAssociatedGerm {\n  $type?: 'app.bsky.actor.defs#profileAssociatedGerm'\n  messageMeUrl: string\n  showButtonTo: 'usersIFollow' | 'everyone' | (string & {})\n}\n\nconst hashProfileAssociatedGerm = 'profileAssociatedGerm'\n\nexport function isProfileAssociatedGerm<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedGerm)\n}\n\nexport function validateProfileAssociatedGerm<V>(v: V) {\n  return validate<ProfileAssociatedGerm & V>(v, id, hashProfileAssociatedGerm)\n}\n\nexport interface ProfileAssociatedActivitySubscription {\n  $type?: 'app.bsky.actor.defs#profileAssociatedActivitySubscription'\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n}\n\nconst hashProfileAssociatedActivitySubscription =\n  'profileAssociatedActivitySubscription'\n\nexport function isProfileAssociatedActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedActivitySubscription)\n}\n\nexport function validateProfileAssociatedActivitySubscription<V>(v: V) {\n  return validate<ProfileAssociatedActivitySubscription & V>(\n    v,\n    id,\n    hashProfileAssociatedActivitySubscription,\n  )\n}\n\n/** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.actor.defs#viewerState'\n  muted?: boolean\n  mutedByList?: AppBskyGraphDefs.ListViewBasic\n  blockedBy?: boolean\n  blocking?: string\n  blockingByList?: AppBskyGraphDefs.ListViewBasic\n  following?: string\n  followedBy?: string\n  knownFollowers?: KnownFollowers\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** The subject's followers whom you also follow */\nexport interface KnownFollowers {\n  $type?: 'app.bsky.actor.defs#knownFollowers'\n  count: number\n  followers: ProfileViewBasic[]\n}\n\nconst hashKnownFollowers = 'knownFollowers'\n\nexport function isKnownFollowers<V>(v: V) {\n  return is$typed(v, id, hashKnownFollowers)\n}\n\nexport function validateKnownFollowers<V>(v: V) {\n  return validate<KnownFollowers & V>(v, id, hashKnownFollowers)\n}\n\n/** Represents the verification information about the user this object is attached to. */\nexport interface VerificationState {\n  $type?: 'app.bsky.actor.defs#verificationState'\n  /** All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included. */\n  verifications: VerificationView[]\n  /** The user's status as a verified account. */\n  verifiedStatus: 'valid' | 'invalid' | 'none' | (string & {})\n  /** The user's status as a trusted verifier. */\n  trustedVerifierStatus: 'valid' | 'invalid' | 'none' | (string & {})\n}\n\nconst hashVerificationState = 'verificationState'\n\nexport function isVerificationState<V>(v: V) {\n  return is$typed(v, id, hashVerificationState)\n}\n\nexport function validateVerificationState<V>(v: V) {\n  return validate<VerificationState & V>(v, id, hashVerificationState)\n}\n\n/** An individual verification for an associated subject. */\nexport interface VerificationView {\n  $type?: 'app.bsky.actor.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** True if the verification passes validation, otherwise false. */\n  isValid: boolean\n  /** Timestamp when the verification was created. */\n  createdAt: string\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n\nexport type Preferences = (\n  | $Typed<AdultContentPref>\n  | $Typed<ContentLabelPref>\n  | $Typed<SavedFeedsPref>\n  | $Typed<SavedFeedsPrefV2>\n  | $Typed<PersonalDetailsPref>\n  | $Typed<DeclaredAgePref>\n  | $Typed<FeedViewPref>\n  | $Typed<ThreadViewPref>\n  | $Typed<InterestsPref>\n  | $Typed<MutedWordsPref>\n  | $Typed<HiddenPostsPref>\n  | $Typed<BskyAppStatePref>\n  | $Typed<LabelersPref>\n  | $Typed<PostInteractionSettingsPref>\n  | $Typed<VerificationPrefs>\n  | $Typed<LiveEventPreferences>\n  | { $type: string }\n)[]\n\nexport interface AdultContentPref {\n  $type?: 'app.bsky.actor.defs#adultContentPref'\n  enabled: boolean\n}\n\nconst hashAdultContentPref = 'adultContentPref'\n\nexport function isAdultContentPref<V>(v: V) {\n  return is$typed(v, id, hashAdultContentPref)\n}\n\nexport function validateAdultContentPref<V>(v: V) {\n  return validate<AdultContentPref & V>(v, id, hashAdultContentPref)\n}\n\nexport interface ContentLabelPref {\n  $type?: 'app.bsky.actor.defs#contentLabelPref'\n  /** Which labeler does this preference apply to? If undefined, applies globally. */\n  labelerDid?: string\n  label: string\n  visibility: 'ignore' | 'show' | 'warn' | 'hide' | (string & {})\n}\n\nconst hashContentLabelPref = 'contentLabelPref'\n\nexport function isContentLabelPref<V>(v: V) {\n  return is$typed(v, id, hashContentLabelPref)\n}\n\nexport function validateContentLabelPref<V>(v: V) {\n  return validate<ContentLabelPref & V>(v, id, hashContentLabelPref)\n}\n\nexport interface SavedFeed {\n  $type?: 'app.bsky.actor.defs#savedFeed'\n  id: string\n  type: 'feed' | 'list' | 'timeline' | (string & {})\n  value: string\n  pinned: boolean\n}\n\nconst hashSavedFeed = 'savedFeed'\n\nexport function isSavedFeed<V>(v: V) {\n  return is$typed(v, id, hashSavedFeed)\n}\n\nexport function validateSavedFeed<V>(v: V) {\n  return validate<SavedFeed & V>(v, id, hashSavedFeed)\n}\n\nexport interface SavedFeedsPrefV2 {\n  $type?: 'app.bsky.actor.defs#savedFeedsPrefV2'\n  items: SavedFeed[]\n}\n\nconst hashSavedFeedsPrefV2 = 'savedFeedsPrefV2'\n\nexport function isSavedFeedsPrefV2<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPrefV2)\n}\n\nexport function validateSavedFeedsPrefV2<V>(v: V) {\n  return validate<SavedFeedsPrefV2 & V>(v, id, hashSavedFeedsPrefV2)\n}\n\nexport interface SavedFeedsPref {\n  $type?: 'app.bsky.actor.defs#savedFeedsPref'\n  pinned: string[]\n  saved: string[]\n  timelineIndex?: number\n}\n\nconst hashSavedFeedsPref = 'savedFeedsPref'\n\nexport function isSavedFeedsPref<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPref)\n}\n\nexport function validateSavedFeedsPref<V>(v: V) {\n  return validate<SavedFeedsPref & V>(v, id, hashSavedFeedsPref)\n}\n\nexport interface PersonalDetailsPref {\n  $type?: 'app.bsky.actor.defs#personalDetailsPref'\n  /** The birth date of account owner. */\n  birthDate?: string\n}\n\nconst hashPersonalDetailsPref = 'personalDetailsPref'\n\nexport function isPersonalDetailsPref<V>(v: V) {\n  return is$typed(v, id, hashPersonalDetailsPref)\n}\n\nexport function validatePersonalDetailsPref<V>(v: V) {\n  return validate<PersonalDetailsPref & V>(v, id, hashPersonalDetailsPref)\n}\n\n/** 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. */\nexport interface DeclaredAgePref {\n  $type?: 'app.bsky.actor.defs#declaredAgePref'\n  /** Indicates if the user has declared that they are over 13 years of age. */\n  isOverAge13?: boolean\n  /** Indicates if the user has declared that they are over 16 years of age. */\n  isOverAge16?: boolean\n  /** Indicates if the user has declared that they are over 18 years of age. */\n  isOverAge18?: boolean\n}\n\nconst hashDeclaredAgePref = 'declaredAgePref'\n\nexport function isDeclaredAgePref<V>(v: V) {\n  return is$typed(v, id, hashDeclaredAgePref)\n}\n\nexport function validateDeclaredAgePref<V>(v: V) {\n  return validate<DeclaredAgePref & V>(v, id, hashDeclaredAgePref)\n}\n\nexport interface FeedViewPref {\n  $type?: 'app.bsky.actor.defs#feedViewPref'\n  /** The URI of the feed, or an identifier which describes the feed. */\n  feed: string\n  /** Hide replies in the feed. */\n  hideReplies?: boolean\n  /** Hide replies in the feed if they are not by followed users. */\n  hideRepliesByUnfollowed: boolean\n  /** Hide replies in the feed if they do not have this number of likes. */\n  hideRepliesByLikeCount?: number\n  /** Hide reposts in the feed. */\n  hideReposts?: boolean\n  /** Hide quote posts in the feed. */\n  hideQuotePosts?: boolean\n}\n\nconst hashFeedViewPref = 'feedViewPref'\n\nexport function isFeedViewPref<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPref)\n}\n\nexport function validateFeedViewPref<V>(v: V) {\n  return validate<FeedViewPref & V>(v, id, hashFeedViewPref)\n}\n\nexport interface ThreadViewPref {\n  $type?: 'app.bsky.actor.defs#threadViewPref'\n  /** Sorting mode for threads. */\n  sort?:\n    | 'oldest'\n    | 'newest'\n    | 'most-likes'\n    | 'random'\n    | 'hotness'\n    | (string & {})\n}\n\nconst hashThreadViewPref = 'threadViewPref'\n\nexport function isThreadViewPref<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPref)\n}\n\nexport function validateThreadViewPref<V>(v: V) {\n  return validate<ThreadViewPref & V>(v, id, hashThreadViewPref)\n}\n\nexport interface InterestsPref {\n  $type?: 'app.bsky.actor.defs#interestsPref'\n  /** A list of tags which describe the account owner's interests gathered during onboarding. */\n  tags: string[]\n}\n\nconst hashInterestsPref = 'interestsPref'\n\nexport function isInterestsPref<V>(v: V) {\n  return is$typed(v, id, hashInterestsPref)\n}\n\nexport function validateInterestsPref<V>(v: V) {\n  return validate<InterestsPref & V>(v, id, hashInterestsPref)\n}\n\nexport type MutedWordTarget = 'content' | 'tag' | (string & {})\n\n/** A word that the account owner has muted. */\nexport interface MutedWord {\n  $type?: 'app.bsky.actor.defs#mutedWord'\n  id?: string\n  /** The muted word itself. */\n  value: string\n  /** The intended targets of the muted word. */\n  targets: MutedWordTarget[]\n  /** Groups of users to apply the muted word to. If undefined, applies to all users. */\n  actorTarget: 'all' | 'exclude-following' | (string & {})\n  /** The date and time at which the muted word will expire and no longer be applied. */\n  expiresAt?: string\n}\n\nconst hashMutedWord = 'mutedWord'\n\nexport function isMutedWord<V>(v: V) {\n  return is$typed(v, id, hashMutedWord)\n}\n\nexport function validateMutedWord<V>(v: V) {\n  return validate<MutedWord & V>(v, id, hashMutedWord)\n}\n\nexport interface MutedWordsPref {\n  $type?: 'app.bsky.actor.defs#mutedWordsPref'\n  /** A list of words the account owner has muted. */\n  items: MutedWord[]\n}\n\nconst hashMutedWordsPref = 'mutedWordsPref'\n\nexport function isMutedWordsPref<V>(v: V) {\n  return is$typed(v, id, hashMutedWordsPref)\n}\n\nexport function validateMutedWordsPref<V>(v: V) {\n  return validate<MutedWordsPref & V>(v, id, hashMutedWordsPref)\n}\n\nexport interface HiddenPostsPref {\n  $type?: 'app.bsky.actor.defs#hiddenPostsPref'\n  /** A list of URIs of posts the account owner has hidden. */\n  items: string[]\n}\n\nconst hashHiddenPostsPref = 'hiddenPostsPref'\n\nexport function isHiddenPostsPref<V>(v: V) {\n  return is$typed(v, id, hashHiddenPostsPref)\n}\n\nexport function validateHiddenPostsPref<V>(v: V) {\n  return validate<HiddenPostsPref & V>(v, id, hashHiddenPostsPref)\n}\n\nexport interface LabelersPref {\n  $type?: 'app.bsky.actor.defs#labelersPref'\n  labelers: LabelerPrefItem[]\n}\n\nconst hashLabelersPref = 'labelersPref'\n\nexport function isLabelersPref<V>(v: V) {\n  return is$typed(v, id, hashLabelersPref)\n}\n\nexport function validateLabelersPref<V>(v: V) {\n  return validate<LabelersPref & V>(v, id, hashLabelersPref)\n}\n\nexport interface LabelerPrefItem {\n  $type?: 'app.bsky.actor.defs#labelerPrefItem'\n  did: string\n}\n\nconst hashLabelerPrefItem = 'labelerPrefItem'\n\nexport function isLabelerPrefItem<V>(v: V) {\n  return is$typed(v, id, hashLabelerPrefItem)\n}\n\nexport function validateLabelerPrefItem<V>(v: V) {\n  return validate<LabelerPrefItem & V>(v, id, hashLabelerPrefItem)\n}\n\n/** A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this. */\nexport interface BskyAppStatePref {\n  $type?: 'app.bsky.actor.defs#bskyAppStatePref'\n  activeProgressGuide?: BskyAppProgressGuide\n  /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */\n  queuedNudges?: string[]\n  /** Storage for NUXs the user has encountered. */\n  nuxs?: Nux[]\n}\n\nconst hashBskyAppStatePref = 'bskyAppStatePref'\n\nexport function isBskyAppStatePref<V>(v: V) {\n  return is$typed(v, id, hashBskyAppStatePref)\n}\n\nexport function validateBskyAppStatePref<V>(v: V) {\n  return validate<BskyAppStatePref & V>(v, id, hashBskyAppStatePref)\n}\n\n/** If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress. */\nexport interface BskyAppProgressGuide {\n  $type?: 'app.bsky.actor.defs#bskyAppProgressGuide'\n  guide: string\n}\n\nconst hashBskyAppProgressGuide = 'bskyAppProgressGuide'\n\nexport function isBskyAppProgressGuide<V>(v: V) {\n  return is$typed(v, id, hashBskyAppProgressGuide)\n}\n\nexport function validateBskyAppProgressGuide<V>(v: V) {\n  return validate<BskyAppProgressGuide & V>(v, id, hashBskyAppProgressGuide)\n}\n\n/** A new user experiences (NUX) storage object */\nexport interface Nux {\n  $type?: 'app.bsky.actor.defs#nux'\n  id: string\n  completed: boolean\n  /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */\n  data?: string\n  /** The date and time at which the NUX will expire and should be considered completed. */\n  expiresAt?: string\n}\n\nconst hashNux = 'nux'\n\nexport function isNux<V>(v: V) {\n  return is$typed(v, id, hashNux)\n}\n\nexport function validateNux<V>(v: V) {\n  return validate<Nux & V>(v, id, hashNux)\n}\n\n/** Preferences for how verified accounts appear in the app. */\nexport interface VerificationPrefs {\n  $type?: 'app.bsky.actor.defs#verificationPrefs'\n  /** Hide the blue check badges for verified accounts and trusted verifiers. */\n  hideBadges: boolean\n}\n\nconst hashVerificationPrefs = 'verificationPrefs'\n\nexport function isVerificationPrefs<V>(v: V) {\n  return is$typed(v, id, hashVerificationPrefs)\n}\n\nexport function validateVerificationPrefs<V>(v: V) {\n  return validate<VerificationPrefs & V>(v, id, hashVerificationPrefs)\n}\n\n/** Preferences for live events. */\nexport interface LiveEventPreferences {\n  $type?: 'app.bsky.actor.defs#liveEventPreferences'\n  /** A list of feed IDs that the user has hidden from live events. */\n  hiddenFeedIds?: string[]\n  /** Whether to hide all feeds from live events. */\n  hideAllFeeds: boolean\n}\n\nconst hashLiveEventPreferences = 'liveEventPreferences'\n\nexport function isLiveEventPreferences<V>(v: V) {\n  return is$typed(v, id, hashLiveEventPreferences)\n}\n\nexport function validateLiveEventPreferences<V>(v: V) {\n  return validate<LiveEventPreferences & V>(v, id, hashLiveEventPreferences)\n}\n\n/** 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. */\nexport interface PostInteractionSettingsPref {\n  $type?: 'app.bsky.actor.defs#postInteractionSettingsPref'\n  /** Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  threadgateAllowRules?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n  /** Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashPostInteractionSettingsPref = 'postInteractionSettingsPref'\n\nexport function isPostInteractionSettingsPref<V>(v: V) {\n  return is$typed(v, id, hashPostInteractionSettingsPref)\n}\n\nexport function validatePostInteractionSettingsPref<V>(v: V) {\n  return validate<PostInteractionSettingsPref & V>(\n    v,\n    id,\n    hashPostInteractionSettingsPref,\n  )\n}\n\nexport interface StatusView {\n  $type?: 'app.bsky.actor.defs#statusView'\n  uri?: string\n  cid?: string\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  record: { [_ in string]: unknown }\n  embed?: $Typed<AppBskyEmbedExternal.View> | { $type: string }\n  /** The date when this status will expire. The application might choose to no longer return the status after expiration. */\n  expiresAt?: string\n  /** True if the status is not expired, false if it is expired. Only present if expiration was set. */\n  isActive?: boolean\n  /** True if the user's go-live access has been disabled by a moderator, false otherwise. */\n  isDisabled?: boolean\n}\n\nconst hashStatusView = 'statusView'\n\nexport function isStatusView<V>(v: V) {\n  return is$typed(v, id, hashStatusView)\n}\n\nexport function validateStatusView<V>(v: V) {\n  return validate<StatusView & V>(v, id, hashStatusView)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfile'\n\nexport type QueryParams = {\n  /** Handle or DID of account to fetch profile of. */\n  actor: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyActorDefs.ProfileViewDetailed\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfiles'\n\nexport type QueryParams = {\n  actors: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  profiles: AppBskyActorDefs.ProfileViewDetailed[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getSuggestions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.profile'\n\nexport interface Main {\n  $type: 'app.bsky.actor.profile'\n  displayName?: string\n  /** Free-form profile description text. */\n  description?: string\n  /** Free-form pronouns text. */\n  pronouns?: string\n  website?: string\n  /** Small image to be displayed next to posts from account. AKA, 'profile picture' */\n  avatar?: BlobRef\n  /** Larger horizontal image to display behind profile view. */\n  banner?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActors'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActorsTypeahead'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query prefix; not a full query string. */\n  q?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/actor/status.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.status'\n\nexport interface Main {\n  $type: 'app.bsky.actor.status'\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  embed?: $Typed<AppBskyEmbedExternal.Main> | { $type: string }\n  /** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. */\n  durationMinutes?: number\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Advertises an account as currently offering live content. */\nexport const LIVE = `${id}#live`\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/ageassurance/begin.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.begin'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive Age Assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the Age Assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n  /** An optional ISO 3166-2 code of the user's region or state within the country. */\n  regionCode?: string\n}\n\nexport type OutputSchema = AppBskyAgeassuranceDefs.State\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidEmail'\n    | 'DidTooLong'\n    | 'InvalidInitiation'\n    | 'RegionNotSupported'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/ageassurance/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.defs'\n\n/** The access level granted based on Age Assurance data we've processed. */\nexport type Access = 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n/** The status of the Age Assurance process. */\nexport type Status =\n  | 'unknown'\n  | 'pending'\n  | 'assured'\n  | 'blocked'\n  | (string & {})\n\n/** The user's computed Age Assurance state. */\nexport interface State {\n  $type?: 'app.bsky.ageassurance.defs#state'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  status: Status\n  access: Access\n}\n\nconst hashState = 'state'\n\nexport function isState<V>(v: V) {\n  return is$typed(v, id, hashState)\n}\n\nexport function validateState<V>(v: V) {\n  return validate<State & V>(v, id, hashState)\n}\n\n/** Additional metadata needed to compute Age Assurance state client-side. */\nexport interface StateMetadata {\n  $type?: 'app.bsky.ageassurance.defs#stateMetadata'\n  /** The account creation timestamp. */\n  accountCreatedAt?: string\n}\n\nconst hashStateMetadata = 'stateMetadata'\n\nexport function isStateMetadata<V>(v: V) {\n  return is$typed(v, id, hashStateMetadata)\n}\n\nexport function validateStateMetadata<V>(v: V) {\n  return validate<StateMetadata & V>(v, id, hashStateMetadata)\n}\n\nexport interface Config {\n  $type?: 'app.bsky.ageassurance.defs#config'\n  /** The per-region Age Assurance configuration. */\n  regions: ConfigRegion[]\n}\n\nconst hashConfig = 'config'\n\nexport function isConfig<V>(v: V) {\n  return is$typed(v, id, hashConfig)\n}\n\nexport function validateConfig<V>(v: V) {\n  return validate<Config & V>(v, id, hashConfig)\n}\n\n/** The Age Assurance configuration for a specific region. */\nexport interface ConfigRegion {\n  $type?: 'app.bsky.ageassurance.defs#configRegion'\n  /** The ISO 3166-1 alpha-2 country code this configuration applies to. */\n  countryCode: string\n  /** The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. */\n  regionCode?: string\n  /** The minimum age (as a whole integer) required to use Bluesky in this region. */\n  minAccessAge: number\n  /** 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. */\n  rules: (\n    | $Typed<ConfigRegionRuleDefault>\n    | $Typed<ConfigRegionRuleIfDeclaredOverAge>\n    | $Typed<ConfigRegionRuleIfDeclaredUnderAge>\n    | $Typed<ConfigRegionRuleIfAssuredOverAge>\n    | $Typed<ConfigRegionRuleIfAssuredUnderAge>\n    | $Typed<ConfigRegionRuleIfAccountNewerThan>\n    | $Typed<ConfigRegionRuleIfAccountOlderThan>\n    | { $type: string }\n  )[]\n}\n\nconst hashConfigRegion = 'configRegion'\n\nexport function isConfigRegion<V>(v: V) {\n  return is$typed(v, id, hashConfigRegion)\n}\n\nexport function validateConfigRegion<V>(v: V) {\n  return validate<ConfigRegion & V>(v, id, hashConfigRegion)\n}\n\n/** Age Assurance rule that applies by default. */\nexport interface ConfigRegionRuleDefault {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleDefault'\n  access: Access\n}\n\nconst hashConfigRegionRuleDefault = 'configRegionRuleDefault'\n\nexport function isConfigRegionRuleDefault<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleDefault)\n}\n\nexport function validateConfigRegionRuleDefault<V>(v: V) {\n  return validate<ConfigRegionRuleDefault & V>(\n    v,\n    id,\n    hashConfigRegionRuleDefault,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfDeclaredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredOverAge =\n  'configRegionRuleIfDeclaredOverAge'\n\nexport function isConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredOverAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves under a certain age. */\nexport interface ConfigRegionRuleIfDeclaredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredUnderAge =\n  'configRegionRuleIfDeclaredUnderAge'\n\nexport function isConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfAssuredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredOverAge = 'configRegionRuleIfAssuredOverAge'\n\nexport function isConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredOverAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be under a certain age. */\nexport interface ConfigRegionRuleIfAssuredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredUnderAge =\n  'configRegionRuleIfAssuredUnderAge'\n\nexport function isConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the account is equal-to or newer than a certain date. */\nexport interface ConfigRegionRuleIfAccountNewerThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountNewerThan =\n  'configRegionRuleIfAccountNewerThan'\n\nexport function isConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountNewerThan)\n}\n\nexport function validateConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountNewerThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountNewerThan,\n  )\n}\n\n/** Age Assurance rule that applies if the account is older than a certain date. */\nexport interface ConfigRegionRuleIfAccountOlderThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountOlderThan =\n  'configRegionRuleIfAccountOlderThan'\n\nexport function isConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountOlderThan)\n}\n\nexport function validateConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountOlderThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountOlderThan,\n  )\n}\n\n/** Object used to store Age Assurance data in stash. */\nexport interface Event {\n  $type?: 'app.bsky.ageassurance.defs#event'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the Age Assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n  /** The access level granted based on Age Assurance data we've processed. */\n  access: 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The email used for Age Assurance. */\n  email?: string\n  /** The IP address used when initiating the Age Assurance flow. */\n  initIp?: string\n  /** The user agent used when initiating the Age Assurance flow. */\n  initUa?: string\n  /** The IP address used when completing the Age Assurance flow. */\n  completeIp?: string\n  /** The user agent used when completing the Age Assurance flow. */\n  completeUa?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/ageassurance/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyAgeassuranceDefs.Config\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/ageassurance/getState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getState'\n\nexport type QueryParams = {\n  countryCode: string\n  regionCode?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  state: AppBskyAgeassuranceDefs.State\n  metadata: AppBskyAgeassuranceDefs.StateMetadata\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/bookmark/createBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.createBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n  cid: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnsupportedCollection'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/bookmark/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.defs'\n\n/** Object used to store bookmark data in stash. */\nexport interface Bookmark {\n  $type?: 'app.bsky.bookmark.defs#bookmark'\n  subject: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashBookmark = 'bookmark'\n\nexport function isBookmark<V>(v: V) {\n  return is$typed(v, id, hashBookmark)\n}\n\nexport function validateBookmark<V>(v: V) {\n  return validate<Bookmark & V>(v, id, hashBookmark)\n}\n\nexport interface BookmarkView {\n  $type?: 'app.bsky.bookmark.defs#bookmarkView'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  item:\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.PostView>\n    | { $type: string }\n}\n\nconst hashBookmarkView = 'bookmarkView'\n\nexport function isBookmarkView<V>(v: V) {\n  return is$typed(v, id, hashBookmarkView)\n}\n\nexport function validateBookmarkView<V>(v: V) {\n  return validate<BookmarkView & V>(v, id, hashBookmarkView)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/bookmark/deleteBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.deleteBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnsupportedCollection'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/bookmark/getBookmarks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyBookmarkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.getBookmarks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  bookmarks: AppBskyBookmarkDefs.BookmarkView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.defs'\n\n/** 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. */\nexport interface MatchAndContactIndex {\n  $type?: 'app.bsky.contact.defs#matchAndContactIndex'\n  match: AppBskyActorDefs.ProfileView\n  /** The index of this match in the import contact input. */\n  contactIndex: number\n}\n\nconst hashMatchAndContactIndex = 'matchAndContactIndex'\n\nexport function isMatchAndContactIndex<V>(v: V) {\n  return is$typed(v, id, hashMatchAndContactIndex)\n}\n\nexport function validateMatchAndContactIndex<V>(v: V) {\n  return validate<MatchAndContactIndex & V>(v, id, hashMatchAndContactIndex)\n}\n\nexport interface SyncStatus {\n  $type?: 'app.bsky.contact.defs#syncStatus'\n  /** Last date when contacts where imported. */\n  syncedAt: string\n  /** 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. */\n  matchesCount: number\n}\n\nconst hashSyncStatus = 'syncStatus'\n\nexport function isSyncStatus<V>(v: V) {\n  return is$typed(v, id, hashSyncStatus)\n}\n\nexport function validateSyncStatus<V>(v: V) {\n  return validate<SyncStatus & V>(v, id, hashSyncStatus)\n}\n\n/** A stash object to be sent via bsync representing a notification to be created. */\nexport interface Notification {\n  $type?: 'app.bsky.contact.defs#notification'\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/dismissMatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.dismissMatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The subject's DID to dismiss the match with. */\n  subject: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/getMatches.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getMatches'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  matches: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InvalidLimit' | 'InvalidCursor' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/getSyncStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getSyncStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  syncStatus?: AppBskyContactDefs.SyncStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/importContacts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.importContacts'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`. */\n  token: string\n  /** 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`. */\n  contacts: string[]\n}\n\nexport interface OutputSchema {\n  /** The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list. */\n  matchesAndContactIndexes: AppBskyContactDefs.MatchAndContactIndex[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidDid'\n    | 'InvalidContacts'\n    | 'TooManyContacts'\n    | 'InvalidToken'\n    | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/removeData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.removeData'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/sendNotification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.sendNotification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/startPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.startPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to receive the code via SMS. */\n  phone: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RateLimitExceeded' | 'InvalidDid' | 'InvalidPhone' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/contact/verifyPhone.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.verifyPhone'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`. */\n  phone: string\n  /** The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`. */\n  code: string\n}\n\nexport interface OutputSchema {\n  /** JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call. */\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'RateLimitExceeded'\n    | 'InvalidDid'\n    | 'InvalidPhone'\n    | 'InvalidCode'\n    | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/draft/createDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.createDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.Draft\n}\n\nexport interface OutputSchema {\n  /** The ID of the created draft. */\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DraftLimitReached'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/draft/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.defs'\n\n/** A draft with an identifier, used to store drafts in private storage (stash). */\nexport interface DraftWithId {\n  $type?: 'app.bsky.draft.defs#draftWithId'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n}\n\nconst hashDraftWithId = 'draftWithId'\n\nexport function isDraftWithId<V>(v: V) {\n  return is$typed(v, id, hashDraftWithId)\n}\n\nexport function validateDraftWithId<V>(v: V) {\n  return validate<DraftWithId & V>(v, id, hashDraftWithId)\n}\n\n/** A draft containing an array of draft posts. */\nexport interface Draft {\n  $type?: 'app.bsky.draft.defs#draft'\n  /** UUIDv4 identifier of the device that created this draft. */\n  deviceId?: string\n  /** The device and/or platform on which the draft was created. */\n  deviceName?: string\n  /** Array of draft posts that compose this draft. */\n  posts: DraftPost[]\n  /** Indicates human language of posts primary text content. */\n  langs?: string[]\n  /** Embedding rules for the postgates to be created when this draft is published. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n  /** Allow-rules for the threadgate to be created when this draft is published. */\n  threadgateAllow?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashDraft = 'draft'\n\nexport function isDraft<V>(v: V) {\n  return is$typed(v, id, hashDraft)\n}\n\nexport function validateDraft<V>(v: V) {\n  return validate<Draft & V>(v, id, hashDraft)\n}\n\n/** One of the posts that compose a draft. */\nexport interface DraftPost {\n  $type?: 'app.bsky.draft.defs#draftPost'\n  /** 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. */\n  text: string\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  embedImages?: DraftEmbedImage[]\n  embedVideos?: DraftEmbedVideo[]\n  embedExternals?: DraftEmbedExternal[]\n  embedRecords?: DraftEmbedRecord[]\n}\n\nconst hashDraftPost = 'draftPost'\n\nexport function isDraftPost<V>(v: V) {\n  return is$typed(v, id, hashDraftPost)\n}\n\nexport function validateDraftPost<V>(v: V) {\n  return validate<DraftPost & V>(v, id, hashDraftPost)\n}\n\n/** View to present drafts data to users. */\nexport interface DraftView {\n  $type?: 'app.bsky.draft.defs#draftView'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n  /** The time the draft was created. */\n  createdAt: string\n  /** The time the draft was last updated. */\n  updatedAt: string\n}\n\nconst hashDraftView = 'draftView'\n\nexport function isDraftView<V>(v: V) {\n  return is$typed(v, id, hashDraftView)\n}\n\nexport function validateDraftView<V>(v: V) {\n  return validate<DraftView & V>(v, id, hashDraftView)\n}\n\nexport interface DraftEmbedLocalRef {\n  $type?: 'app.bsky.draft.defs#draftEmbedLocalRef'\n  /** Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts. */\n  path: string\n}\n\nconst hashDraftEmbedLocalRef = 'draftEmbedLocalRef'\n\nexport function isDraftEmbedLocalRef<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedLocalRef)\n}\n\nexport function validateDraftEmbedLocalRef<V>(v: V) {\n  return validate<DraftEmbedLocalRef & V>(v, id, hashDraftEmbedLocalRef)\n}\n\nexport interface DraftEmbedCaption {\n  $type?: 'app.bsky.draft.defs#draftEmbedCaption'\n  lang: string\n  content: string\n}\n\nconst hashDraftEmbedCaption = 'draftEmbedCaption'\n\nexport function isDraftEmbedCaption<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedCaption)\n}\n\nexport function validateDraftEmbedCaption<V>(v: V) {\n  return validate<DraftEmbedCaption & V>(v, id, hashDraftEmbedCaption)\n}\n\nexport interface DraftEmbedImage {\n  $type?: 'app.bsky.draft.defs#draftEmbedImage'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n}\n\nconst hashDraftEmbedImage = 'draftEmbedImage'\n\nexport function isDraftEmbedImage<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedImage)\n}\n\nexport function validateDraftEmbedImage<V>(v: V) {\n  return validate<DraftEmbedImage & V>(v, id, hashDraftEmbedImage)\n}\n\nexport interface DraftEmbedVideo {\n  $type?: 'app.bsky.draft.defs#draftEmbedVideo'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n  captions?: DraftEmbedCaption[]\n}\n\nconst hashDraftEmbedVideo = 'draftEmbedVideo'\n\nexport function isDraftEmbedVideo<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedVideo)\n}\n\nexport function validateDraftEmbedVideo<V>(v: V) {\n  return validate<DraftEmbedVideo & V>(v, id, hashDraftEmbedVideo)\n}\n\nexport interface DraftEmbedExternal {\n  $type?: 'app.bsky.draft.defs#draftEmbedExternal'\n  uri: string\n}\n\nconst hashDraftEmbedExternal = 'draftEmbedExternal'\n\nexport function isDraftEmbedExternal<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedExternal)\n}\n\nexport function validateDraftEmbedExternal<V>(v: V) {\n  return validate<DraftEmbedExternal & V>(v, id, hashDraftEmbedExternal)\n}\n\nexport interface DraftEmbedRecord {\n  $type?: 'app.bsky.draft.defs#draftEmbedRecord'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashDraftEmbedRecord = 'draftEmbedRecord'\n\nexport function isDraftEmbedRecord<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedRecord)\n}\n\nexport function validateDraftEmbedRecord<V>(v: V) {\n  return validate<DraftEmbedRecord & V>(v, id, hashDraftEmbedRecord)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/draft/deleteDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.deleteDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/draft/getDrafts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.getDrafts'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  drafts: AppBskyDraftDefs.DraftView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/draft/updateDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.updateDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.DraftWithId\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/embed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.defs'\n\n/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */\nexport interface AspectRatio {\n  $type?: 'app.bsky.embed.defs#aspectRatio'\n  width: number\n  height: number\n}\n\nconst hashAspectRatio = 'aspectRatio'\n\nexport function isAspectRatio<V>(v: V) {\n  return is$typed(v, id, hashAspectRatio)\n}\n\nexport function validateAspectRatio<V>(v: V) {\n  return validate<AspectRatio & V>(v, id, hashAspectRatio)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/embed/external.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.external'\n\n/** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). */\nexport interface Main {\n  $type?: 'app.bsky.embed.external'\n  external: External\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface External {\n  $type?: 'app.bsky.embed.external#external'\n  uri: string\n  title: string\n  description: string\n  thumb?: BlobRef\n}\n\nconst hashExternal = 'external'\n\nexport function isExternal<V>(v: V) {\n  return is$typed(v, id, hashExternal)\n}\n\nexport function validateExternal<V>(v: V) {\n  return validate<External & V>(v, id, hashExternal)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.external#view'\n  external: ViewExternal\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewExternal {\n  $type?: 'app.bsky.embed.external#viewExternal'\n  uri: string\n  title: string\n  description: string\n  thumb?: string\n}\n\nconst hashViewExternal = 'viewExternal'\n\nexport function isViewExternal<V>(v: V) {\n  return is$typed(v, id, hashViewExternal)\n}\n\nexport function validateViewExternal<V>(v: V) {\n  return validate<ViewExternal & V>(v, id, hashViewExternal)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/embed/images.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.images'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.images'\n  images: Image[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Image {\n  $type?: 'app.bsky.embed.images#image'\n  image: BlobRef\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashImage = 'image'\n\nexport function isImage<V>(v: V) {\n  return is$typed(v, id, hashImage)\n}\n\nexport function validateImage<V>(v: V) {\n  return validate<Image & V>(v, id, hashImage)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.images#view'\n  images: ViewImage[]\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewImage {\n  $type?: 'app.bsky.embed.images#viewImage'\n  /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */\n  thumb: string\n  /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */\n  fullsize: string\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashViewImage = 'viewImage'\n\nexport function isViewImage<V>(v: V) {\n  return is$typed(v, id, hashViewImage)\n}\n\nexport function validateViewImage<V>(v: V) {\n  return validate<ViewImage & V>(v, id, hashViewImage)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/embed/record.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as AppBskyLabelerDefs from '../labeler/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\nimport type * as AppBskyEmbedRecordWithMedia from './recordWithMedia.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.record'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.record'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.record#view'\n  record:\n    | $Typed<ViewRecord>\n    | $Typed<ViewNotFound>\n    | $Typed<ViewBlocked>\n    | $Typed<ViewDetached>\n    | $Typed<AppBskyFeedDefs.GeneratorView>\n    | $Typed<AppBskyGraphDefs.ListView>\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyGraphDefs.StarterPackViewBasic>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewRecord {\n  $type?: 'app.bsky.embed.record#viewRecord'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  /** The record data itself. */\n  value: { [_ in string]: unknown }\n  labels?: ComAtprotoLabelDefs.Label[]\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  embeds?: (\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  )[]\n  indexedAt: string\n}\n\nconst hashViewRecord = 'viewRecord'\n\nexport function isViewRecord<V>(v: V) {\n  return is$typed(v, id, hashViewRecord)\n}\n\nexport function validateViewRecord<V>(v: V) {\n  return validate<ViewRecord & V>(v, id, hashViewRecord)\n}\n\nexport interface ViewNotFound {\n  $type?: 'app.bsky.embed.record#viewNotFound'\n  uri: string\n  notFound: true\n}\n\nconst hashViewNotFound = 'viewNotFound'\n\nexport function isViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashViewNotFound)\n}\n\nexport function validateViewNotFound<V>(v: V) {\n  return validate<ViewNotFound & V>(v, id, hashViewNotFound)\n}\n\nexport interface ViewBlocked {\n  $type?: 'app.bsky.embed.record#viewBlocked'\n  uri: string\n  blocked: true\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashViewBlocked = 'viewBlocked'\n\nexport function isViewBlocked<V>(v: V) {\n  return is$typed(v, id, hashViewBlocked)\n}\n\nexport function validateViewBlocked<V>(v: V) {\n  return validate<ViewBlocked & V>(v, id, hashViewBlocked)\n}\n\nexport interface ViewDetached {\n  $type?: 'app.bsky.embed.record#viewDetached'\n  uri: string\n  detached: true\n}\n\nconst hashViewDetached = 'viewDetached'\n\nexport function isViewDetached<V>(v: V) {\n  return is$typed(v, id, hashViewDetached)\n}\n\nexport function validateViewDetached<V>(v: V) {\n  return validate<ViewDetached & V>(v, id, hashViewDetached)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/embed/recordWithMedia.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedRecord from './record.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.recordWithMedia'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.recordWithMedia'\n  record: AppBskyEmbedRecord.Main\n  media:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | { $type: string }\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.recordWithMedia#view'\n  record: AppBskyEmbedRecord.View\n  media:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/embed/video.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.video'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.video'\n  /** The mp4 video file. May be up to 100mb, formerly limited to 50mb. */\n  video: BlobRef\n  captions?: Caption[]\n  /** Alt text description of the video, for accessibility. */\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Caption {\n  $type?: 'app.bsky.embed.video#caption'\n  lang: string\n  file: BlobRef\n}\n\nconst hashCaption = 'caption'\n\nexport function isCaption<V>(v: V) {\n  return is$typed(v, id, hashCaption)\n}\n\nexport function validateCaption<V>(v: V) {\n  return validate<Caption & V>(v, id, hashCaption)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.video#view'\n  cid: string\n  playlist: string\n  thumbnail?: string\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.defs'\n\nexport interface PostView {\n  $type?: 'app.bsky.feed.defs#postView'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  record: { [_ in string]: unknown }\n  embed?:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<AppBskyEmbedRecord.View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  bookmarkCount?: number\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  indexedAt: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  threadgate?: ThreadgateView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashPostView = 'postView'\n\nexport function isPostView<V>(v: V) {\n  return is$typed(v, id, hashPostView)\n}\n\nexport function validatePostView<V>(v: V) {\n  return validate<PostView & V>(v, id, hashPostView)\n}\n\n/** Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.feed.defs#viewerState'\n  repost?: string\n  like?: string\n  bookmarked?: boolean\n  threadMuted?: boolean\n  replyDisabled?: boolean\n  embeddingDisabled?: boolean\n  pinned?: boolean\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** Metadata about this post within the context of the thread it is in. */\nexport interface ThreadContext {\n  $type?: 'app.bsky.feed.defs#threadContext'\n  rootAuthorLike?: string\n}\n\nconst hashThreadContext = 'threadContext'\n\nexport function isThreadContext<V>(v: V) {\n  return is$typed(v, id, hashThreadContext)\n}\n\nexport function validateThreadContext<V>(v: V) {\n  return validate<ThreadContext & V>(v, id, hashThreadContext)\n}\n\nexport interface FeedViewPost {\n  $type?: 'app.bsky.feed.defs#feedViewPost'\n  post: PostView\n  reply?: ReplyRef\n  reason?: $Typed<ReasonRepost> | $Typed<ReasonPin> | { $type: string }\n  /** Context provided by feed generator that may be passed back alongside interactions. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashFeedViewPost = 'feedViewPost'\n\nexport function isFeedViewPost<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPost)\n}\n\nexport function validateFeedViewPost<V>(v: V) {\n  return validate<FeedViewPost & V>(v, id, hashFeedViewPost)\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.defs#replyRef'\n  root:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  parent:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  grandparentAuthor?: AppBskyActorDefs.ProfileViewBasic\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\nexport interface ReasonRepost {\n  $type?: 'app.bsky.feed.defs#reasonRepost'\n  by: AppBskyActorDefs.ProfileViewBasic\n  uri?: string\n  cid?: string\n  indexedAt: string\n}\n\nconst hashReasonRepost = 'reasonRepost'\n\nexport function isReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashReasonRepost)\n}\n\nexport function validateReasonRepost<V>(v: V) {\n  return validate<ReasonRepost & V>(v, id, hashReasonRepost)\n}\n\nexport interface ReasonPin {\n  $type?: 'app.bsky.feed.defs#reasonPin'\n}\n\nconst hashReasonPin = 'reasonPin'\n\nexport function isReasonPin<V>(v: V) {\n  return is$typed(v, id, hashReasonPin)\n}\n\nexport function validateReasonPin<V>(v: V) {\n  return validate<ReasonPin & V>(v, id, hashReasonPin)\n}\n\nexport interface ThreadViewPost {\n  $type?: 'app.bsky.feed.defs#threadViewPost'\n  post: PostView\n  parent?:\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  replies?: (\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  )[]\n  threadContext?: ThreadContext\n}\n\nconst hashThreadViewPost = 'threadViewPost'\n\nexport function isThreadViewPost<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPost)\n}\n\nexport function validateThreadViewPost<V>(v: V) {\n  return validate<ThreadViewPost & V>(v, id, hashThreadViewPost)\n}\n\nexport interface NotFoundPost {\n  $type?: 'app.bsky.feed.defs#notFoundPost'\n  uri: string\n  notFound: true\n}\n\nconst hashNotFoundPost = 'notFoundPost'\n\nexport function isNotFoundPost<V>(v: V) {\n  return is$typed(v, id, hashNotFoundPost)\n}\n\nexport function validateNotFoundPost<V>(v: V) {\n  return validate<NotFoundPost & V>(v, id, hashNotFoundPost)\n}\n\nexport interface BlockedPost {\n  $type?: 'app.bsky.feed.defs#blockedPost'\n  uri: string\n  blocked: true\n  author: BlockedAuthor\n}\n\nconst hashBlockedPost = 'blockedPost'\n\nexport function isBlockedPost<V>(v: V) {\n  return is$typed(v, id, hashBlockedPost)\n}\n\nexport function validateBlockedPost<V>(v: V) {\n  return validate<BlockedPost & V>(v, id, hashBlockedPost)\n}\n\nexport interface BlockedAuthor {\n  $type?: 'app.bsky.feed.defs#blockedAuthor'\n  did: string\n  viewer?: AppBskyActorDefs.ViewerState\n}\n\nconst hashBlockedAuthor = 'blockedAuthor'\n\nexport function isBlockedAuthor<V>(v: V) {\n  return is$typed(v, id, hashBlockedAuthor)\n}\n\nexport function validateBlockedAuthor<V>(v: V) {\n  return validate<BlockedAuthor & V>(v, id, hashBlockedAuthor)\n}\n\nexport interface GeneratorView {\n  $type?: 'app.bsky.feed.defs#generatorView'\n  uri: string\n  cid: string\n  did: string\n  creator: AppBskyActorDefs.ProfileView\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  likeCount?: number\n  acceptsInteractions?: boolean\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: GeneratorViewerState\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  indexedAt: string\n}\n\nconst hashGeneratorView = 'generatorView'\n\nexport function isGeneratorView<V>(v: V) {\n  return is$typed(v, id, hashGeneratorView)\n}\n\nexport function validateGeneratorView<V>(v: V) {\n  return validate<GeneratorView & V>(v, id, hashGeneratorView)\n}\n\nexport interface GeneratorViewerState {\n  $type?: 'app.bsky.feed.defs#generatorViewerState'\n  like?: string\n}\n\nconst hashGeneratorViewerState = 'generatorViewerState'\n\nexport function isGeneratorViewerState<V>(v: V) {\n  return is$typed(v, id, hashGeneratorViewerState)\n}\n\nexport function validateGeneratorViewerState<V>(v: V) {\n  return validate<GeneratorViewerState & V>(v, id, hashGeneratorViewerState)\n}\n\nexport interface SkeletonFeedPost {\n  $type?: 'app.bsky.feed.defs#skeletonFeedPost'\n  post: string\n  reason?:\n    | $Typed<SkeletonReasonRepost>\n    | $Typed<SkeletonReasonPin>\n    | { $type: string }\n  /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */\n  feedContext?: string\n}\n\nconst hashSkeletonFeedPost = 'skeletonFeedPost'\n\nexport function isSkeletonFeedPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonFeedPost)\n}\n\nexport function validateSkeletonFeedPost<V>(v: V) {\n  return validate<SkeletonFeedPost & V>(v, id, hashSkeletonFeedPost)\n}\n\nexport interface SkeletonReasonRepost {\n  $type?: 'app.bsky.feed.defs#skeletonReasonRepost'\n  repost: string\n}\n\nconst hashSkeletonReasonRepost = 'skeletonReasonRepost'\n\nexport function isSkeletonReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonRepost)\n}\n\nexport function validateSkeletonReasonRepost<V>(v: V) {\n  return validate<SkeletonReasonRepost & V>(v, id, hashSkeletonReasonRepost)\n}\n\nexport interface SkeletonReasonPin {\n  $type?: 'app.bsky.feed.defs#skeletonReasonPin'\n}\n\nconst hashSkeletonReasonPin = 'skeletonReasonPin'\n\nexport function isSkeletonReasonPin<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonPin)\n}\n\nexport function validateSkeletonReasonPin<V>(v: V) {\n  return validate<SkeletonReasonPin & V>(v, id, hashSkeletonReasonPin)\n}\n\nexport interface ThreadgateView {\n  $type?: 'app.bsky.feed.defs#threadgateView'\n  uri?: string\n  cid?: string\n  record?: { [_ in string]: unknown }\n  lists?: AppBskyGraphDefs.ListViewBasic[]\n}\n\nconst hashThreadgateView = 'threadgateView'\n\nexport function isThreadgateView<V>(v: V) {\n  return is$typed(v, id, hashThreadgateView)\n}\n\nexport function validateThreadgateView<V>(v: V) {\n  return validate<ThreadgateView & V>(v, id, hashThreadgateView)\n}\n\nexport interface Interaction {\n  $type?: 'app.bsky.feed.defs#interaction'\n  item?: string\n  event?:\n    | 'app.bsky.feed.defs#requestLess'\n    | 'app.bsky.feed.defs#requestMore'\n    | 'app.bsky.feed.defs#clickthroughItem'\n    | 'app.bsky.feed.defs#clickthroughAuthor'\n    | 'app.bsky.feed.defs#clickthroughReposter'\n    | 'app.bsky.feed.defs#clickthroughEmbed'\n    | 'app.bsky.feed.defs#interactionSeen'\n    | 'app.bsky.feed.defs#interactionLike'\n    | 'app.bsky.feed.defs#interactionRepost'\n    | 'app.bsky.feed.defs#interactionReply'\n    | 'app.bsky.feed.defs#interactionQuote'\n    | 'app.bsky.feed.defs#interactionShare'\n    | (string & {})\n  /** Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashInteraction = 'interaction'\n\nexport function isInteraction<V>(v: V) {\n  return is$typed(v, id, hashInteraction)\n}\n\nexport function validateInteraction<V>(v: V) {\n  return validate<Interaction & V>(v, id, hashInteraction)\n}\n\n/** Request that less content like the given feed item be shown in the feed */\nexport const REQUESTLESS = `${id}#requestLess`\n/** Request that more content like the given feed item be shown in the feed */\nexport const REQUESTMORE = `${id}#requestMore`\n/** User clicked through to the feed item */\nexport const CLICKTHROUGHITEM = `${id}#clickthroughItem`\n/** User clicked through to the author of the feed item */\nexport const CLICKTHROUGHAUTHOR = `${id}#clickthroughAuthor`\n/** User clicked through to the reposter of the feed item */\nexport const CLICKTHROUGHREPOSTER = `${id}#clickthroughReposter`\n/** User clicked through to the embedded content of the feed item */\nexport const CLICKTHROUGHEMBED = `${id}#clickthroughEmbed`\n/** Declares the feed generator returns any types of posts. */\nexport const CONTENTMODEUNSPECIFIED = `${id}#contentModeUnspecified`\n/** Declares the feed generator returns posts containing app.bsky.embed.video embeds. */\nexport const CONTENTMODEVIDEO = `${id}#contentModeVideo`\n/** Feed item was seen by user */\nexport const INTERACTIONSEEN = `${id}#interactionSeen`\n/** User liked the feed item */\nexport const INTERACTIONLIKE = `${id}#interactionLike`\n/** User reposted the feed item */\nexport const INTERACTIONREPOST = `${id}#interactionRepost`\n/** User replied to the feed item */\nexport const INTERACTIONREPLY = `${id}#interactionReply`\n/** User quoted the feed item */\nexport const INTERACTIONQUOTE = `${id}#interactionQuote`\n/** User shared the feed item */\nexport const INTERACTIONSHARE = `${id}#interactionShare`\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.describeFeedGenerator'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  feeds: Feed[]\n  links?: Links\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Feed {\n  $type?: 'app.bsky.feed.describeFeedGenerator#feed'\n  uri: string\n}\n\nconst hashFeed = 'feed'\n\nexport function isFeed<V>(v: V) {\n  return is$typed(v, id, hashFeed)\n}\n\nexport function validateFeed<V>(v: V) {\n  return validate<Feed & V>(v, id, hashFeed)\n}\n\nexport interface Links {\n  $type?: 'app.bsky.feed.describeFeedGenerator#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/generator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.generator'\n\nexport interface Main {\n  $type: 'app.bsky.feed.generator'\n  did: string\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  /** Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions */\n  acceptsInteractions?: boolean\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorFeeds'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorLikes'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BlockedActor' | 'BlockedByActor'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getAuthorFeed'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n  /** Combinations of post/repost types to include in response. */\n  filter:\n    | 'posts_with_replies'\n    | 'posts_no_replies'\n    | 'posts_with_media'\n    | 'posts_and_author_threads'\n    | 'posts_with_video'\n    | (string & {})\n  includePins: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BlockedActor' | 'BlockedByActor'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeed'\n\nexport type QueryParams = {\n  feed: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownFeed'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerator'\n\nexport type QueryParams = {\n  /** AT-URI of the feed generator record. */\n  feed: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  view: AppBskyFeedDefs.GeneratorView\n  /** Indicates whether the feed generator service has been online recently, or else seems to be inactive. */\n  isOnline: boolean\n  /** Indicates whether the feed generator service is compatible with the record declaration. */\n  isValid: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerators'\n\nexport type QueryParams = {\n  feeds: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedSkeleton'\n\nexport type QueryParams = {\n  /** Reference to feed generator record describing the specific feed being requested. */\n  feed: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.SkeletonFeedPost[]\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownFeed'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getLikes'\n\nexport type QueryParams = {\n  /** AT-URI of the subject (eg, a post record). */\n  uri: string\n  /** CID of the subject record (aka, specific version of record), to filter likes. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  likes: Like[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Like {\n  $type?: 'app.bsky.feed.getLikes#like'\n  indexedAt: string\n  createdAt: string\n  actor: AppBskyActorDefs.ProfileView\n}\n\nconst hashLike = 'like'\n\nexport function isLike<V>(v: V) {\n  return is$typed(v, id, hashLike)\n}\n\nexport function validateLike<V>(v: V) {\n  return validate<Like & V>(v, id, hashLike)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getListFeed'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownList'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPostThread'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. */\n  uri: string\n  /** How many levels of reply depth should be included in response. */\n  depth: number\n  /** How many levels of parent (and grandparent, etc) post to include. */\n  parentHeight: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  thread:\n    | $Typed<AppBskyFeedDefs.ThreadViewPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | { $type: string }\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'NotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPosts'\n\nexport type QueryParams = {\n  /** List of post AT-URIs to return hydrated views for. */\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getQuotes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getQuotes'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to quotes of specific version (by CID) of the post record. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getRepostedBy'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to reposts of specific version (by CID) of the post record. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  repostedBy: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getTimeline'\n\nexport type QueryParams = {\n  /** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. */\n  algorithm?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/like.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.like'\n\nexport interface Main {\n  $type: 'app.bsky.feed.like'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/post.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.post'\n\nexport interface Main {\n  $type: 'app.bsky.feed.post'\n  /** The primary post content. May be an empty string, if there are embeds. */\n  text: string\n  /** DEPRECATED: replaced by app.bsky.richtext.facet. */\n  entities?: Entity[]\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  reply?: ReplyRef\n  embed?:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | $Typed<AppBskyEmbedRecord.Main>\n    | $Typed<AppBskyEmbedRecordWithMedia.Main>\n    | { $type: string }\n  /** Indicates human language of post primary text content. */\n  langs?: string[]\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  /** Additional hashtags, in addition to any included in post text and facets. */\n  tags?: string[]\n  /** Client-declared timestamp when this post was originally created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.post#replyRef'\n  root: ComAtprotoRepoStrongRef.Main\n  parent: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\n/** Deprecated: use facets instead. */\nexport interface Entity {\n  $type?: 'app.bsky.feed.post#entity'\n  index: TextSlice\n  /** Expected values are 'mention' and 'link'. */\n  type: string\n  value: string\n}\n\nconst hashEntity = 'entity'\n\nexport function isEntity<V>(v: V) {\n  return is$typed(v, id, hashEntity)\n}\n\nexport function validateEntity<V>(v: V) {\n  return validate<Entity & V>(v, id, hashEntity)\n}\n\n/** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. */\nexport interface TextSlice {\n  $type?: 'app.bsky.feed.post#textSlice'\n  start: number\n  end: number\n}\n\nconst hashTextSlice = 'textSlice'\n\nexport function isTextSlice<V>(v: V) {\n  return is$typed(v, id, hashTextSlice)\n}\n\nexport function validateTextSlice<V>(v: V) {\n  return validate<TextSlice & V>(v, id, hashTextSlice)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/postgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.postgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.postgate'\n  createdAt: string\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of AT-URIs embedding this post that the author has detached from. */\n  detachedEmbeddingUris?: string[]\n  /** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  embeddingRules?: ($Typed<DisableRule> | { $type: string })[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Disables embedding of this post. */\nexport interface DisableRule {\n  $type?: 'app.bsky.feed.postgate#disableRule'\n}\n\nconst hashDisableRule = 'disableRule'\n\nexport function isDisableRule<V>(v: V) {\n  return is$typed(v, id, hashDisableRule)\n}\n\nexport function validateDisableRule<V>(v: V) {\n  return validate<DisableRule & V>(v, id, hashDisableRule)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/repost.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.repost'\n\nexport interface Main {\n  $type: 'app.bsky.feed.repost'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.searchPosts'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/sendInteractions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.sendInteractions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  feed?: string\n  interactions: AppBskyFeedDefs.Interaction[]\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.threadgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.threadgate'\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  allow?: (\n    | $Typed<MentionRule>\n    | $Typed<FollowerRule>\n    | $Typed<FollowingRule>\n    | $Typed<ListRule>\n    | { $type: string }\n  )[]\n  createdAt: string\n  /** List of hidden reply URIs. */\n  hiddenReplies?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Allow replies from actors mentioned in your post. */\nexport interface MentionRule {\n  $type?: 'app.bsky.feed.threadgate#mentionRule'\n}\n\nconst hashMentionRule = 'mentionRule'\n\nexport function isMentionRule<V>(v: V) {\n  return is$typed(v, id, hashMentionRule)\n}\n\nexport function validateMentionRule<V>(v: V) {\n  return validate<MentionRule & V>(v, id, hashMentionRule)\n}\n\n/** Allow replies from actors who follow you. */\nexport interface FollowerRule {\n  $type?: 'app.bsky.feed.threadgate#followerRule'\n}\n\nconst hashFollowerRule = 'followerRule'\n\nexport function isFollowerRule<V>(v: V) {\n  return is$typed(v, id, hashFollowerRule)\n}\n\nexport function validateFollowerRule<V>(v: V) {\n  return validate<FollowerRule & V>(v, id, hashFollowerRule)\n}\n\n/** Allow replies from actors you follow. */\nexport interface FollowingRule {\n  $type?: 'app.bsky.feed.threadgate#followingRule'\n}\n\nconst hashFollowingRule = 'followingRule'\n\nexport function isFollowingRule<V>(v: V) {\n  return is$typed(v, id, hashFollowingRule)\n}\n\nexport function validateFollowingRule<V>(v: V) {\n  return validate<FollowingRule & V>(v, id, hashFollowingRule)\n}\n\n/** Allow replies from actors on a list. */\nexport interface ListRule {\n  $type?: 'app.bsky.feed.threadgate#listRule'\n  list: string\n}\n\nconst hashListRule = 'listRule'\n\nexport function isListRule<V>(v: V) {\n  return is$typed(v, id, hashListRule)\n}\n\nexport function validateListRule<V>(v: V) {\n  return validate<ListRule & V>(v, id, hashListRule)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/block.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.block'\n\nexport interface Main {\n  $type: 'app.bsky.graph.block'\n  /** DID of the account to be blocked. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.defs'\n\nexport interface ListViewBasic {\n  $type?: 'app.bsky.graph.defs#listViewBasic'\n  uri: string\n  cid: string\n  name: string\n  purpose: ListPurpose\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt?: string\n}\n\nconst hashListViewBasic = 'listViewBasic'\n\nexport function isListViewBasic<V>(v: V) {\n  return is$typed(v, id, hashListViewBasic)\n}\n\nexport function validateListViewBasic<V>(v: V) {\n  return validate<ListViewBasic & V>(v, id, hashListViewBasic)\n}\n\nexport interface ListView {\n  $type?: 'app.bsky.graph.defs#listView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  name: string\n  purpose: ListPurpose\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt: string\n}\n\nconst hashListView = 'listView'\n\nexport function isListView<V>(v: V) {\n  return is$typed(v, id, hashListView)\n}\n\nexport function validateListView<V>(v: V) {\n  return validate<ListView & V>(v, id, hashListView)\n}\n\nexport interface ListItemView {\n  $type?: 'app.bsky.graph.defs#listItemView'\n  uri: string\n  subject: AppBskyActorDefs.ProfileView\n}\n\nconst hashListItemView = 'listItemView'\n\nexport function isListItemView<V>(v: V) {\n  return is$typed(v, id, hashListItemView)\n}\n\nexport function validateListItemView<V>(v: V) {\n  return validate<ListItemView & V>(v, id, hashListItemView)\n}\n\nexport interface StarterPackView {\n  $type?: 'app.bsky.graph.defs#starterPackView'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  list?: ListViewBasic\n  listItemsSample?: ListItemView[]\n  feeds?: AppBskyFeedDefs.GeneratorView[]\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackView = 'starterPackView'\n\nexport function isStarterPackView<V>(v: V) {\n  return is$typed(v, id, hashStarterPackView)\n}\n\nexport function validateStarterPackView<V>(v: V) {\n  return validate<StarterPackView & V>(v, id, hashStarterPackView)\n}\n\nexport interface StarterPackViewBasic {\n  $type?: 'app.bsky.graph.defs#starterPackViewBasic'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  listItemCount?: number\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackViewBasic = 'starterPackViewBasic'\n\nexport function isStarterPackViewBasic<V>(v: V) {\n  return is$typed(v, id, hashStarterPackViewBasic)\n}\n\nexport function validateStarterPackViewBasic<V>(v: V) {\n  return validate<StarterPackViewBasic & V>(v, id, hashStarterPackViewBasic)\n}\n\nexport type ListPurpose =\n  | 'app.bsky.graph.defs#modlist'\n  | 'app.bsky.graph.defs#curatelist'\n  | 'app.bsky.graph.defs#referencelist'\n  | (string & {})\n\n/** A list of actors to apply an aggregate moderation action (mute/block) on. */\nexport const MODLIST = `${id}#modlist`\n/** A list of actors used for curation purposes such as list feeds or interaction gating. */\nexport const CURATELIST = `${id}#curatelist`\n/** A list of actors used for only for reference purposes such as within a starter pack. */\nexport const REFERENCELIST = `${id}#referencelist`\n\nexport interface ListViewerState {\n  $type?: 'app.bsky.graph.defs#listViewerState'\n  muted?: boolean\n  blocked?: string\n}\n\nconst hashListViewerState = 'listViewerState'\n\nexport function isListViewerState<V>(v: V) {\n  return is$typed(v, id, hashListViewerState)\n}\n\nexport function validateListViewerState<V>(v: V) {\n  return validate<ListViewerState & V>(v, id, hashListViewerState)\n}\n\n/** indicates that a handle or DID could not be resolved */\nexport interface NotFoundActor {\n  $type?: 'app.bsky.graph.defs#notFoundActor'\n  actor: string\n  notFound: true\n}\n\nconst hashNotFoundActor = 'notFoundActor'\n\nexport function isNotFoundActor<V>(v: V) {\n  return is$typed(v, id, hashNotFoundActor)\n}\n\nexport function validateNotFoundActor<V>(v: V) {\n  return validate<NotFoundActor & V>(v, id, hashNotFoundActor)\n}\n\n/** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) */\nexport interface Relationship {\n  $type?: 'app.bsky.graph.defs#relationship'\n  did: string\n  /** if the actor follows this DID, this is the AT-URI of the follow record */\n  following?: string\n  /** if the actor is followed by this DID, contains the AT-URI of the follow record */\n  followedBy?: string\n  /** if the actor blocks this DID, this is the AT-URI of the block record */\n  blocking?: string\n  /** if the actor is blocked by this DID, contains the AT-URI of the block record */\n  blockedBy?: string\n  /** if the actor blocks this DID via a block list, this is the AT-URI of the listblock record */\n  blockingByList?: string\n  /** if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record */\n  blockedByList?: string\n}\n\nconst hashRelationship = 'relationship'\n\nexport function isRelationship<V>(v: V) {\n  return is$typed(v, id, hashRelationship)\n}\n\nexport function validateRelationship<V>(v: V) {\n  return validate<Relationship & V>(v, id, hashRelationship)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/follow.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.follow'\n\nexport interface Main {\n  $type: 'app.bsky.graph.follow'\n  subject: string\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getActorStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getActorStarterPacks'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getBlocks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blocks: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollows'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  follows: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getKnownFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getKnownFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getList'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the list record to hydrate. */\n  list: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  list: AppBskyGraphDefs.ListView\n  items: AppBskyGraphDefs.ListItemView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListBlocks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListMutes'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getLists'\n\nexport type QueryParams = {\n  /** The account (actor) to enumerate lists from. */\n  actor: string\n  limit: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getListsWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListsWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  listsWithMembership: ListWithMembership[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** A list and an optional list item indicating membership of a target user to that list. */\nexport interface ListWithMembership {\n  $type?: 'app.bsky.graph.getListsWithMembership#listWithMembership'\n  list: AppBskyGraphDefs.ListView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashListWithMembership = 'listWithMembership'\n\nexport function isListWithMembership<V>(v: V) {\n  return is$typed(v, id, hashListWithMembership)\n}\n\nexport function validateListWithMembership<V>(v: V) {\n  return validate<ListWithMembership & V>(v, id, hashListWithMembership)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getMutes'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  mutes: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getRelationships.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getRelationships'\n\nexport type QueryParams = {\n  /** Primary account requesting relationships for. */\n  actor: string\n  /** List of 'other' accounts to be related back to the primary. */\n  others?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actor?: string\n  relationships: (\n    | $Typed<AppBskyGraphDefs.Relationship>\n    | $Typed<AppBskyGraphDefs.NotFoundActor>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ActorNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getStarterPack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPack'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the starter pack record. */\n  starterPack: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPack: AppBskyGraphDefs.StarterPackView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacks'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacksWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacksWithMembership: StarterPackWithMembership[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** A starter pack and an optional list item indicating membership of a target user to that starter pack. */\nexport interface StarterPackWithMembership {\n  $type?: 'app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership'\n  starterPack: AppBskyGraphDefs.StarterPackView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashStarterPackWithMembership = 'starterPackWithMembership'\n\nexport function isStarterPackWithMembership<V>(v: V) {\n  return is$typed(v, id, hashStarterPackWithMembership)\n}\n\nexport function validateStarterPackWithMembership<V>(v: V) {\n  return validate<StarterPackWithMembership & V>(\n    v,\n    id,\n    hashStarterPackWithMembership,\n  )\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getSuggestedFollowsByActor'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: AppBskyActorDefs.ProfileView[]\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n  /** DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid */\n  isFallback?: boolean\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/list.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.list'\n\nexport interface Main {\n  $type: 'app.bsky.graph.list'\n  purpose: AppBskyGraphDefs.ListPurpose\n  /** Display name for list; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listblock'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listblock'\n  /** Reference (AT-URI) to the mod list record. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/listitem.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listitem'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listitem'\n  /** The account which is included on the list. */\n  subject: string\n  /** Reference (AT-URI) to the list record (app.bsky.graph.list). */\n  list: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/muteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/searchStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.searchStarterPacks'\n\nexport type QueryParams = {\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/starterpack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.starterpack'\n\nexport interface Main {\n  $type: 'app.bsky.graph.starterpack'\n  /** Display name for starter pack; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  feeds?: FeedItem[]\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface FeedItem {\n  $type?: 'app.bsky.graph.starterpack#feedItem'\n  uri: string\n}\n\nconst hashFeedItem = 'feedItem'\n\nexport function isFeedItem<V>(v: V) {\n  return is$typed(v, id, hashFeedItem)\n}\n\nexport function validateFeedItem<V>(v: V) {\n  return validate<FeedItem & V>(v, id, hashFeedItem)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/unmuteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/graph/verification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.verification'\n\nexport interface Main {\n  $type: 'app.bsky.graph.verification'\n  /** DID of the subject the verification applies to. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Date of when the verification was created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/labeler/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.defs'\n\nexport interface LabelerView {\n  $type?: 'app.bsky.labeler.defs#labelerView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabelerView = 'labelerView'\n\nexport function isLabelerView<V>(v: V) {\n  return is$typed(v, id, hashLabelerView)\n}\n\nexport function validateLabelerView<V>(v: V) {\n  return validate<LabelerView & V>(v, id, hashLabelerView)\n}\n\nexport interface LabelerViewDetailed {\n  $type?: 'app.bsky.labeler.defs#labelerViewDetailed'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  policies: LabelerPolicies\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n}\n\nconst hashLabelerViewDetailed = 'labelerViewDetailed'\n\nexport function isLabelerViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewDetailed)\n}\n\nexport function validateLabelerViewDetailed<V>(v: V) {\n  return validate<LabelerViewDetailed & V>(v, id, hashLabelerViewDetailed)\n}\n\nexport interface LabelerViewerState {\n  $type?: 'app.bsky.labeler.defs#labelerViewerState'\n  like?: string\n}\n\nconst hashLabelerViewerState = 'labelerViewerState'\n\nexport function isLabelerViewerState<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewerState)\n}\n\nexport function validateLabelerViewerState<V>(v: V) {\n  return validate<LabelerViewerState & V>(v, id, hashLabelerViewerState)\n}\n\nexport interface LabelerPolicies {\n  $type?: 'app.bsky.labeler.defs#labelerPolicies'\n  /** The label values which this labeler publishes. May include global or custom labels. */\n  labelValues: ComAtprotoLabelDefs.LabelValue[]\n  /** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */\n  labelValueDefinitions?: ComAtprotoLabelDefs.LabelValueDefinition[]\n}\n\nconst hashLabelerPolicies = 'labelerPolicies'\n\nexport function isLabelerPolicies<V>(v: V) {\n  return is$typed(v, id, hashLabelerPolicies)\n}\n\nexport function validateLabelerPolicies<V>(v: V) {\n  return validate<LabelerPolicies & V>(v, id, hashLabelerPolicies)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/labeler/getServices.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.getServices'\n\nexport type QueryParams = {\n  dids: string[]\n  detailed: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  views: (\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyLabelerDefs.LabelerViewDetailed>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/labeler/service.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.service'\n\nexport interface Main {\n  $type: 'app.bsky.labeler.service'\n  policies: AppBskyLabelerDefs.LabelerPolicies\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.declaration'\n\nexport interface Main {\n  $type: 'app.bsky.notification.declaration'\n  /** A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'. */\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.defs'\n\nexport interface RecordDeleted {\n  $type?: 'app.bsky.notification.defs#recordDeleted'\n}\n\nconst hashRecordDeleted = 'recordDeleted'\n\nexport function isRecordDeleted<V>(v: V) {\n  return is$typed(v, id, hashRecordDeleted)\n}\n\nexport function validateRecordDeleted<V>(v: V) {\n  return validate<RecordDeleted & V>(v, id, hashRecordDeleted)\n}\n\nexport interface ChatPreference {\n  $type?: 'app.bsky.notification.defs#chatPreference'\n  include: 'all' | 'accepted' | (string & {})\n  push: boolean\n}\n\nconst hashChatPreference = 'chatPreference'\n\nexport function isChatPreference<V>(v: V) {\n  return is$typed(v, id, hashChatPreference)\n}\n\nexport function validateChatPreference<V>(v: V) {\n  return validate<ChatPreference & V>(v, id, hashChatPreference)\n}\n\nexport interface FilterablePreference {\n  $type?: 'app.bsky.notification.defs#filterablePreference'\n  include: 'all' | 'follows' | (string & {})\n  list: boolean\n  push: boolean\n}\n\nconst hashFilterablePreference = 'filterablePreference'\n\nexport function isFilterablePreference<V>(v: V) {\n  return is$typed(v, id, hashFilterablePreference)\n}\n\nexport function validateFilterablePreference<V>(v: V) {\n  return validate<FilterablePreference & V>(v, id, hashFilterablePreference)\n}\n\nexport interface Preference {\n  $type?: 'app.bsky.notification.defs#preference'\n  list: boolean\n  push: boolean\n}\n\nconst hashPreference = 'preference'\n\nexport function isPreference<V>(v: V) {\n  return is$typed(v, id, hashPreference)\n}\n\nexport function validatePreference<V>(v: V) {\n  return validate<Preference & V>(v, id, hashPreference)\n}\n\nexport interface Preferences {\n  $type?: 'app.bsky.notification.defs#preferences'\n  chat: ChatPreference\n  follow: FilterablePreference\n  like: FilterablePreference\n  likeViaRepost: FilterablePreference\n  mention: FilterablePreference\n  quote: FilterablePreference\n  reply: FilterablePreference\n  repost: FilterablePreference\n  repostViaRepost: FilterablePreference\n  starterpackJoined: Preference\n  subscribedPost: Preference\n  unverified: Preference\n  verified: Preference\n}\n\nconst hashPreferences = 'preferences'\n\nexport function isPreferences<V>(v: V) {\n  return is$typed(v, id, hashPreferences)\n}\n\nexport function validatePreferences<V>(v: V) {\n  return validate<Preferences & V>(v, id, hashPreferences)\n}\n\nexport interface ActivitySubscription {\n  $type?: 'app.bsky.notification.defs#activitySubscription'\n  post: boolean\n  reply: boolean\n}\n\nconst hashActivitySubscription = 'activitySubscription'\n\nexport function isActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashActivitySubscription)\n}\n\nexport function validateActivitySubscription<V>(v: V) {\n  return validate<ActivitySubscription & V>(v, id, hashActivitySubscription)\n}\n\n/** Object used to store activity subscription data in stash. */\nexport interface SubjectActivitySubscription {\n  $type?: 'app.bsky.notification.defs#subjectActivitySubscription'\n  subject: string\n  activitySubscription: ActivitySubscription\n}\n\nconst hashSubjectActivitySubscription = 'subjectActivitySubscription'\n\nexport function isSubjectActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashSubjectActivitySubscription)\n}\n\nexport function validateSubjectActivitySubscription<V>(v: V) {\n  return validate<SubjectActivitySubscription & V>(\n    v,\n    id,\n    hashSubjectActivitySubscription,\n  )\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getUnreadCount'\n\nexport type QueryParams = {\n  priority?: boolean\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  count: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/listActivitySubscriptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listActivitySubscriptions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subscriptions: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listNotifications'\n\nexport type QueryParams = {\n  /** Notification reasons to include in response. */\n  reasons?: string[]\n  limit: number\n  priority?: boolean\n  cursor?: string\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  notifications: Notification[]\n  priority?: boolean\n  seenAt?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Notification {\n  $type?: 'app.bsky.notification.listNotifications#notification'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileView\n  /** The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. */\n  reason:\n    | 'like'\n    | 'repost'\n    | 'follow'\n    | 'mention'\n    | 'reply'\n    | 'quote'\n    | 'starterpack-joined'\n    | 'verified'\n    | 'unverified'\n    | 'like-via-repost'\n    | 'repost-via-repost'\n    | 'subscribed-post'\n    | 'contact-match'\n    | (string & {})\n  reasonSubject?: string\n  record: { [_ in string]: unknown }\n  isRead: boolean\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/putActivitySubscription.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putActivitySubscription'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject: string\n  activitySubscription: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface OutputSchema {\n  subject: string\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  priority: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/putPreferencesV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferencesV2'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  chat?: AppBskyNotificationDefs.ChatPreference\n  follow?: AppBskyNotificationDefs.FilterablePreference\n  like?: AppBskyNotificationDefs.FilterablePreference\n  likeViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  mention?: AppBskyNotificationDefs.FilterablePreference\n  quote?: AppBskyNotificationDefs.FilterablePreference\n  reply?: AppBskyNotificationDefs.FilterablePreference\n  repost?: AppBskyNotificationDefs.FilterablePreference\n  repostViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  starterpackJoined?: AppBskyNotificationDefs.Preference\n  subscribedPost?: AppBskyNotificationDefs.Preference\n  unverified?: AppBskyNotificationDefs.Preference\n  verified?: AppBskyNotificationDefs.Preference\n}\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.registerPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n  /** Set to true when the actor is age restricted */\n  ageRestricted?: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/unregisterPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.unregisterPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.updateSeen'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  seenAt: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.richtext.facet'\n\n/** Annotation of a sub-string within rich text. */\nexport interface Main {\n  $type?: 'app.bsky.richtext.facet'\n  index: ByteSlice\n  features: ($Typed<Mention> | $Typed<Link> | $Typed<Tag> | { $type: string })[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\n/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */\nexport interface Mention {\n  $type?: 'app.bsky.richtext.facet#mention'\n  did: string\n}\n\nconst hashMention = 'mention'\n\nexport function isMention<V>(v: V) {\n  return is$typed(v, id, hashMention)\n}\n\nexport function validateMention<V>(v: V) {\n  return validate<Mention & V>(v, id, hashMention)\n}\n\n/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */\nexport interface Link {\n  $type?: 'app.bsky.richtext.facet#link'\n  uri: string\n}\n\nconst hashLink = 'link'\n\nexport function isLink<V>(v: V) {\n  return is$typed(v, id, hashLink)\n}\n\nexport function validateLink<V>(v: V) {\n  return validate<Link & V>(v, id, hashLink)\n}\n\n/** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */\nexport interface Tag {\n  $type?: 'app.bsky.richtext.facet#tag'\n  tag: string\n}\n\nconst hashTag = 'tag'\n\nexport function isTag<V>(v: V) {\n  return is$typed(v, id, hashTag)\n}\n\nexport function validateTag<V>(v: V) {\n  return validate<Tag & V>(v, id, hashTag)\n}\n\n/** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */\nexport interface ByteSlice {\n  $type?: 'app.bsky.richtext.facet#byteSlice'\n  byteStart: number\n  byteEnd: number\n}\n\nconst hashByteSlice = 'byteSlice'\n\nexport function isByteSlice<V>(v: V) {\n  return is$typed(v, id, hashByteSlice)\n}\n\nexport function validateByteSlice<V>(v: V) {\n  return validate<ByteSlice & V>(v, id, hashByteSlice)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.defs'\n\nexport interface SkeletonSearchPost {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchPost'\n  uri: string\n}\n\nconst hashSkeletonSearchPost = 'skeletonSearchPost'\n\nexport function isSkeletonSearchPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchPost)\n}\n\nexport function validateSkeletonSearchPost<V>(v: V) {\n  return validate<SkeletonSearchPost & V>(v, id, hashSkeletonSearchPost)\n}\n\nexport interface SkeletonSearchActor {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchActor'\n  did: string\n}\n\nconst hashSkeletonSearchActor = 'skeletonSearchActor'\n\nexport function isSkeletonSearchActor<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchActor)\n}\n\nexport function validateSkeletonSearchActor<V>(v: V) {\n  return validate<SkeletonSearchActor & V>(v, id, hashSkeletonSearchActor)\n}\n\nexport interface SkeletonSearchStarterPack {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchStarterPack'\n  uri: string\n}\n\nconst hashSkeletonSearchStarterPack = 'skeletonSearchStarterPack'\n\nexport function isSkeletonSearchStarterPack<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchStarterPack)\n}\n\nexport function validateSkeletonSearchStarterPack<V>(v: V) {\n  return validate<SkeletonSearchStarterPack & V>(\n    v,\n    id,\n    hashSkeletonSearchStarterPack,\n  )\n}\n\nexport interface TrendingTopic {\n  $type?: 'app.bsky.unspecced.defs#trendingTopic'\n  topic: string\n  displayName?: string\n  description?: string\n  link: string\n}\n\nconst hashTrendingTopic = 'trendingTopic'\n\nexport function isTrendingTopic<V>(v: V) {\n  return is$typed(v, id, hashTrendingTopic)\n}\n\nexport function validateTrendingTopic<V>(v: V) {\n  return validate<TrendingTopic & V>(v, id, hashTrendingTopic)\n}\n\nexport interface SkeletonTrend {\n  $type?: 'app.bsky.unspecced.defs#skeletonTrend'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  dids: string[]\n}\n\nconst hashSkeletonTrend = 'skeletonTrend'\n\nexport function isSkeletonTrend<V>(v: V) {\n  return is$typed(v, id, hashSkeletonTrend)\n}\n\nexport function validateSkeletonTrend<V>(v: V) {\n  return validate<SkeletonTrend & V>(v, id, hashSkeletonTrend)\n}\n\nexport interface TrendView {\n  $type?: 'app.bsky.unspecced.defs#trendView'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nconst hashTrendView = 'trendView'\n\nexport function isTrendView<V>(v: V) {\n  return is$typed(v, id, hashTrendView)\n}\n\nexport function validateTrendView<V>(v: V) {\n  return validate<TrendView & V>(v, id, hashTrendView)\n}\n\nexport interface ThreadItemPost {\n  $type?: 'app.bsky.unspecced.defs#threadItemPost'\n  post: AppBskyFeedDefs.PostView\n  /** This post has more parents that were not present in the response. This is just a boolean, without the number of parents. */\n  moreParents: boolean\n  /** This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. */\n  moreReplies: number\n  /** This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. */\n  opThread: boolean\n  /** The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. */\n  hiddenByThreadgate: boolean\n  /** This is by an account muted by the viewer requesting it. */\n  mutedByViewer: boolean\n}\n\nconst hashThreadItemPost = 'threadItemPost'\n\nexport function isThreadItemPost<V>(v: V) {\n  return is$typed(v, id, hashThreadItemPost)\n}\n\nexport function validateThreadItemPost<V>(v: V) {\n  return validate<ThreadItemPost & V>(v, id, hashThreadItemPost)\n}\n\nexport interface ThreadItemNoUnauthenticated {\n  $type?: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated'\n}\n\nconst hashThreadItemNoUnauthenticated = 'threadItemNoUnauthenticated'\n\nexport function isThreadItemNoUnauthenticated<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNoUnauthenticated)\n}\n\nexport function validateThreadItemNoUnauthenticated<V>(v: V) {\n  return validate<ThreadItemNoUnauthenticated & V>(\n    v,\n    id,\n    hashThreadItemNoUnauthenticated,\n  )\n}\n\nexport interface ThreadItemNotFound {\n  $type?: 'app.bsky.unspecced.defs#threadItemNotFound'\n}\n\nconst hashThreadItemNotFound = 'threadItemNotFound'\n\nexport function isThreadItemNotFound<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNotFound)\n}\n\nexport function validateThreadItemNotFound<V>(v: V) {\n  return validate<ThreadItemNotFound & V>(v, id, hashThreadItemNotFound)\n}\n\nexport interface ThreadItemBlocked {\n  $type?: 'app.bsky.unspecced.defs#threadItemBlocked'\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashThreadItemBlocked = 'threadItemBlocked'\n\nexport function isThreadItemBlocked<V>(v: V) {\n  return is$typed(v, id, hashThreadItemBlocked)\n}\n\nexport function validateThreadItemBlocked<V>(v: V) {\n  return validate<ThreadItemBlocked & V>(v, id, hashThreadItemBlocked)\n}\n\n/** The computed state of the age assurance process, returned to the user in question on certain authenticated requests. */\nexport interface AgeAssuranceState {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceState'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n}\n\nconst hashAgeAssuranceState = 'ageAssuranceState'\n\nexport function isAgeAssuranceState<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceState)\n}\n\nexport function validateAgeAssuranceState<V>(v: V) {\n  return validate<AgeAssuranceState & V>(v, id, hashAgeAssuranceState)\n}\n\n/** Object used to store age assurance data in stash. */\nexport interface AgeAssuranceEvent {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The email used for AA. */\n  email?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getAgeAssuranceState'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  checkEmailConfirmed?: boolean\n  liveNow?: LiveNowConfig[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface LiveNowConfig {\n  $type?: 'app.bsky.unspecced.getConfig#liveNowConfig'\n  did: string\n  domains: string[]\n}\n\nconst hashLiveNowConfig = 'liveNowConfig'\n\nexport function isLiveNowConfig<V>(v: V) {\n  return is$typed(v, id, hashLiveNowConfig)\n}\n\nexport function validateLiveNowConfig<V>(v: V) {\n  return validate<LiveNowConfig & V>(v, id, hashLiveNowConfig)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPopularFeedGenerators'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  query?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadOtherV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post. */\n  anchor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of other thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadOtherV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post. */\n  anchor: string\n  /** Whether to include parents above the anchor. */\n  above: boolean\n  /** How many levels of replies to include below the anchor. */\n  below: number\n  /** Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated). */\n  branchingFactor: number\n  /** Sorting for the thread replies. */\n  sort: 'newest' | 'oldest' | 'top' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n  /** Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them. */\n  hasOtherReplies: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value:\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemPost>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNotFound>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemBlocked>\n    | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedFeedsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeedsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedOnboardingUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedOnboardingUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestionsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit: number\n  cursor?: string\n  /** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. */\n  relativeToDid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n  /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */\n  relativeToDid?: string\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTaggedSuggestions'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: Suggestion[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Suggestion {\n  $type?: 'app.bsky.unspecced.getTaggedSuggestions#suggestion'\n  tag: string\n  subjectType: 'actor' | 'feed' | (string & {})\n  subject: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getTrendingTopics.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendingTopics'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  topics: AppBskyUnspeccedDefs.TrendingTopic[]\n  suggested: AppBskyUnspeccedDefs.TrendingTopic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getTrends.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrends'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.TrendView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.SkeletonTrend[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.initAgeAssurance'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n}\n\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidEmail' | 'DidTooLong' | 'InvalidInitiation'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchActorsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  /** If true, acts as fast/simple 'typeahead' query. */\n  typeahead?: boolean\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchPostsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries. */\n  viewer?: string\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyUnspeccedDefs.SkeletonSearchPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/unspecced/searchStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  starterPacks: AppBskyUnspeccedDefs.SkeletonSearchStarterPack[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/video/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.defs'\n\nexport interface JobStatus {\n  $type?: 'app.bsky.video.defs#jobStatus'\n  jobId: string\n  did: string\n  /** The state of the video processing job. All values not listed as a known value indicate that the job is in process. */\n  state: 'JOB_STATE_COMPLETED' | 'JOB_STATE_FAILED' | (string & {})\n  /** Progress within the current processing state. */\n  progress?: number\n  blob?: BlobRef\n  error?: string\n  message?: string\n}\n\nconst hashJobStatus = 'jobStatus'\n\nexport function isJobStatus<V>(v: V) {\n  return is$typed(v, id, hashJobStatus)\n}\n\nexport function validateJobStatus<V>(v: V) {\n  return validate<JobStatus & V>(v, id, hashJobStatus)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/video/getJobStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getJobStatus'\n\nexport type QueryParams = {\n  jobId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/video/getUploadLimits.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getUploadLimits'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canUpload: boolean\n  remainingDailyVideos?: number\n  remainingDailyBytes?: number\n  message?: string\n  error?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/app/bsky/video/uploadVideo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.uploadVideo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport interface HandlerInput {\n  encoding: 'video/mp4'\n  body: stream.Readable\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/actor/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.declaration'\n\nexport interface Main {\n  $type: 'chat.bsky.actor.declaration'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'chat.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  avatar?: string\n  associated?: AppBskyActorDefs.ProfileAssociated\n  viewer?: AppBskyActorDefs.ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** Set to true when the actor cannot actively participate in conversations */\n  chatDisabled?: boolean\n  verification?: AppBskyActorDefs.VerificationState\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/actor/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.deleteAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/actor/exportAccountData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.exportAccountData'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/jsonl'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/acceptConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.acceptConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  /** Rev when the convo was accepted. If not present, the convo was already accepted. */\n  rev?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/addReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.addReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'ReactionMessageDeleted'\n    | 'ReactionLimitReached'\n    | 'ReactionInvalidValue'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../../../app/bsky/richtext/facet.js'\nimport type * as AppBskyEmbedRecord from '../../../app/bsky/embed/record.js'\nimport type * as ChatBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.defs'\n\nexport interface MessageRef {\n  $type?: 'chat.bsky.convo.defs#messageRef'\n  did: string\n  convoId: string\n  messageId: string\n}\n\nconst hashMessageRef = 'messageRef'\n\nexport function isMessageRef<V>(v: V) {\n  return is$typed(v, id, hashMessageRef)\n}\n\nexport function validateMessageRef<V>(v: V) {\n  return validate<MessageRef & V>(v, id, hashMessageRef)\n}\n\nexport interface MessageInput {\n  $type?: 'chat.bsky.convo.defs#messageInput'\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.Main> | { $type: string }\n}\n\nconst hashMessageInput = 'messageInput'\n\nexport function isMessageInput<V>(v: V) {\n  return is$typed(v, id, hashMessageInput)\n}\n\nexport function validateMessageInput<V>(v: V) {\n  return validate<MessageInput & V>(v, id, hashMessageInput)\n}\n\nexport interface MessageView {\n  $type?: 'chat.bsky.convo.defs#messageView'\n  id: string\n  rev: string\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.View> | { $type: string }\n  /** Reactions to this message, in ascending order of creation time. */\n  reactions?: ReactionView[]\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashMessageView = 'messageView'\n\nexport function isMessageView<V>(v: V) {\n  return is$typed(v, id, hashMessageView)\n}\n\nexport function validateMessageView<V>(v: V) {\n  return validate<MessageView & V>(v, id, hashMessageView)\n}\n\nexport interface DeletedMessageView {\n  $type?: 'chat.bsky.convo.defs#deletedMessageView'\n  id: string\n  rev: string\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashDeletedMessageView = 'deletedMessageView'\n\nexport function isDeletedMessageView<V>(v: V) {\n  return is$typed(v, id, hashDeletedMessageView)\n}\n\nexport function validateDeletedMessageView<V>(v: V) {\n  return validate<DeletedMessageView & V>(v, id, hashDeletedMessageView)\n}\n\nexport interface MessageViewSender {\n  $type?: 'chat.bsky.convo.defs#messageViewSender'\n  did: string\n}\n\nconst hashMessageViewSender = 'messageViewSender'\n\nexport function isMessageViewSender<V>(v: V) {\n  return is$typed(v, id, hashMessageViewSender)\n}\n\nexport function validateMessageViewSender<V>(v: V) {\n  return validate<MessageViewSender & V>(v, id, hashMessageViewSender)\n}\n\nexport interface ReactionView {\n  $type?: 'chat.bsky.convo.defs#reactionView'\n  value: string\n  sender: ReactionViewSender\n  createdAt: string\n}\n\nconst hashReactionView = 'reactionView'\n\nexport function isReactionView<V>(v: V) {\n  return is$typed(v, id, hashReactionView)\n}\n\nexport function validateReactionView<V>(v: V) {\n  return validate<ReactionView & V>(v, id, hashReactionView)\n}\n\nexport interface ReactionViewSender {\n  $type?: 'chat.bsky.convo.defs#reactionViewSender'\n  did: string\n}\n\nconst hashReactionViewSender = 'reactionViewSender'\n\nexport function isReactionViewSender<V>(v: V) {\n  return is$typed(v, id, hashReactionViewSender)\n}\n\nexport function validateReactionViewSender<V>(v: V) {\n  return validate<ReactionViewSender & V>(v, id, hashReactionViewSender)\n}\n\nexport interface MessageAndReactionView {\n  $type?: 'chat.bsky.convo.defs#messageAndReactionView'\n  message: MessageView\n  reaction: ReactionView\n}\n\nconst hashMessageAndReactionView = 'messageAndReactionView'\n\nexport function isMessageAndReactionView<V>(v: V) {\n  return is$typed(v, id, hashMessageAndReactionView)\n}\n\nexport function validateMessageAndReactionView<V>(v: V) {\n  return validate<MessageAndReactionView & V>(v, id, hashMessageAndReactionView)\n}\n\nexport interface ConvoView {\n  $type?: 'chat.bsky.convo.defs#convoView'\n  id: string\n  rev: string\n  members: ChatBskyActorDefs.ProfileViewBasic[]\n  lastMessage?:\n    | $Typed<MessageView>\n    | $Typed<DeletedMessageView>\n    | { $type: string }\n  lastReaction?: $Typed<MessageAndReactionView> | { $type: string }\n  muted: boolean\n  status?: 'request' | 'accepted' | (string & {})\n  unreadCount: number\n}\n\nconst hashConvoView = 'convoView'\n\nexport function isConvoView<V>(v: V) {\n  return is$typed(v, id, hashConvoView)\n}\n\nexport function validateConvoView<V>(v: V) {\n  return validate<ConvoView & V>(v, id, hashConvoView)\n}\n\nexport interface LogBeginConvo {\n  $type?: 'chat.bsky.convo.defs#logBeginConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogBeginConvo = 'logBeginConvo'\n\nexport function isLogBeginConvo<V>(v: V) {\n  return is$typed(v, id, hashLogBeginConvo)\n}\n\nexport function validateLogBeginConvo<V>(v: V) {\n  return validate<LogBeginConvo & V>(v, id, hashLogBeginConvo)\n}\n\nexport interface LogAcceptConvo {\n  $type?: 'chat.bsky.convo.defs#logAcceptConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogAcceptConvo = 'logAcceptConvo'\n\nexport function isLogAcceptConvo<V>(v: V) {\n  return is$typed(v, id, hashLogAcceptConvo)\n}\n\nexport function validateLogAcceptConvo<V>(v: V) {\n  return validate<LogAcceptConvo & V>(v, id, hashLogAcceptConvo)\n}\n\nexport interface LogLeaveConvo {\n  $type?: 'chat.bsky.convo.defs#logLeaveConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogLeaveConvo = 'logLeaveConvo'\n\nexport function isLogLeaveConvo<V>(v: V) {\n  return is$typed(v, id, hashLogLeaveConvo)\n}\n\nexport function validateLogLeaveConvo<V>(v: V) {\n  return validate<LogLeaveConvo & V>(v, id, hashLogLeaveConvo)\n}\n\nexport interface LogMuteConvo {\n  $type?: 'chat.bsky.convo.defs#logMuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogMuteConvo = 'logMuteConvo'\n\nexport function isLogMuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogMuteConvo)\n}\n\nexport function validateLogMuteConvo<V>(v: V) {\n  return validate<LogMuteConvo & V>(v, id, hashLogMuteConvo)\n}\n\nexport interface LogUnmuteConvo {\n  $type?: 'chat.bsky.convo.defs#logUnmuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogUnmuteConvo = 'logUnmuteConvo'\n\nexport function isLogUnmuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogUnmuteConvo)\n}\n\nexport function validateLogUnmuteConvo<V>(v: V) {\n  return validate<LogUnmuteConvo & V>(v, id, hashLogUnmuteConvo)\n}\n\nexport interface LogCreateMessage {\n  $type?: 'chat.bsky.convo.defs#logCreateMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogCreateMessage = 'logCreateMessage'\n\nexport function isLogCreateMessage<V>(v: V) {\n  return is$typed(v, id, hashLogCreateMessage)\n}\n\nexport function validateLogCreateMessage<V>(v: V) {\n  return validate<LogCreateMessage & V>(v, id, hashLogCreateMessage)\n}\n\nexport interface LogDeleteMessage {\n  $type?: 'chat.bsky.convo.defs#logDeleteMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogDeleteMessage = 'logDeleteMessage'\n\nexport function isLogDeleteMessage<V>(v: V) {\n  return is$typed(v, id, hashLogDeleteMessage)\n}\n\nexport function validateLogDeleteMessage<V>(v: V) {\n  return validate<LogDeleteMessage & V>(v, id, hashLogDeleteMessage)\n}\n\nexport interface LogReadMessage {\n  $type?: 'chat.bsky.convo.defs#logReadMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogReadMessage = 'logReadMessage'\n\nexport function isLogReadMessage<V>(v: V) {\n  return is$typed(v, id, hashLogReadMessage)\n}\n\nexport function validateLogReadMessage<V>(v: V) {\n  return validate<LogReadMessage & V>(v, id, hashLogReadMessage)\n}\n\nexport interface LogAddReaction {\n  $type?: 'chat.bsky.convo.defs#logAddReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogAddReaction = 'logAddReaction'\n\nexport function isLogAddReaction<V>(v: V) {\n  return is$typed(v, id, hashLogAddReaction)\n}\n\nexport function validateLogAddReaction<V>(v: V) {\n  return validate<LogAddReaction & V>(v, id, hashLogAddReaction)\n}\n\nexport interface LogRemoveReaction {\n  $type?: 'chat.bsky.convo.defs#logRemoveReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogRemoveReaction = 'logRemoveReaction'\n\nexport function isLogRemoveReaction<V>(v: V) {\n  return is$typed(v, id, hashLogRemoveReaction)\n}\n\nexport function validateLogRemoveReaction<V>(v: V) {\n  return validate<LogRemoveReaction & V>(v, id, hashLogRemoveReaction)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/deleteMessageForSelf.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.deleteMessageForSelf'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.DeletedMessageView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/getConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvo'\n\nexport type QueryParams = {\n  convoId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/getConvoAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoAvailability'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canChat: boolean\n  convo?: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/getConvoForMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoForMembers'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/getLog.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getLog'\n\nexport type QueryParams = {\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  logs: (\n    | $Typed<ChatBskyConvoDefs.LogBeginConvo>\n    | $Typed<ChatBskyConvoDefs.LogAcceptConvo>\n    | $Typed<ChatBskyConvoDefs.LogLeaveConvo>\n    | $Typed<ChatBskyConvoDefs.LogMuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogUnmuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogCreateMessage>\n    | $Typed<ChatBskyConvoDefs.LogDeleteMessage>\n    | $Typed<ChatBskyConvoDefs.LogReadMessage>\n    | $Typed<ChatBskyConvoDefs.LogAddReaction>\n    | $Typed<ChatBskyConvoDefs.LogRemoveReaction>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/getMessages.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getMessages'\n\nexport type QueryParams = {\n  convoId: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/leaveConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.leaveConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convoId: string\n  rev: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/listConvos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.listConvos'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  readState?: 'unread' | (string & {})\n  status?: 'request' | 'accepted' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  convos: ChatBskyConvoDefs.ConvoView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/muteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.muteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/removeReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.removeReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ReactionMessageDeleted' | 'ReactionInvalidValue'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/sendMessage.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessage'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.MessageView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/sendMessageBatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessageBatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  items: BatchItem[]\n}\n\nexport interface OutputSchema {\n  items: ChatBskyConvoDefs.MessageView[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface BatchItem {\n  $type?: 'chat.bsky.convo.sendMessageBatch#batchItem'\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nconst hashBatchItem = 'batchItem'\n\nexport function isBatchItem<V>(v: V) {\n  return is$typed(v, id, hashBatchItem)\n}\n\nexport function validateBatchItem<V>(v: V) {\n  return validate<BatchItem & V>(v, id, hashBatchItem)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/unmuteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.unmuteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/updateAllRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateAllRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  status?: 'request' | 'accepted' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** The count of updated convos. */\n  updatedCount: number\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/convo/updateRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId?: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/moderation/getActorMetadata.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getActorMetadata'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  day: Metadata\n  month: Metadata\n  all: Metadata\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Metadata {\n  $type?: 'chat.bsky.moderation.getActorMetadata#metadata'\n  messagesSent: number\n  messagesReceived: number\n  convos: number\n  convosStarted: number\n}\n\nconst hashMetadata = 'metadata'\n\nexport function isMetadata<V>(v: V) {\n  return is$typed(v, id, hashMetadata)\n}\n\nexport function validateMetadata<V>(v: V) {\n  return validate<Metadata & V>(v, id, hashMetadata)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/moderation/getMessageContext.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from '../convo/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getMessageContext'\n\nexport type QueryParams = {\n  /** Conversation that the message is from. NOTE: this field will eventually be required. */\n  convoId?: string\n  messageId: string\n  before: number\n  after: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/chat/bsky/moderation/updateActorAccess.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.updateActorAccess'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n  allowAccess: boolean\n  ref?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.defs'\n\nexport interface StatusAttr {\n  $type?: 'com.atproto.admin.defs#statusAttr'\n  applied: boolean\n  ref?: string\n}\n\nconst hashStatusAttr = 'statusAttr'\n\nexport function isStatusAttr<V>(v: V) {\n  return is$typed(v, id, hashStatusAttr)\n}\n\nexport function validateStatusAttr<V>(v: V) {\n  return validate<StatusAttr & V>(v, id, hashStatusAttr)\n}\n\nexport interface AccountView {\n  $type?: 'com.atproto.admin.defs#accountView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords?: { [_ in string]: unknown }[]\n  indexedAt: string\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  emailConfirmedAt?: string\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ThreatSignature[]\n}\n\nconst hashAccountView = 'accountView'\n\nexport function isAccountView<V>(v: V) {\n  return is$typed(v, id, hashAccountView)\n}\n\nexport function validateAccountView<V>(v: V) {\n  return validate<AccountView & V>(v, id, hashAccountView)\n}\n\nexport interface RepoRef {\n  $type?: 'com.atproto.admin.defs#repoRef'\n  did: string\n}\n\nconst hashRepoRef = 'repoRef'\n\nexport function isRepoRef<V>(v: V) {\n  return is$typed(v, id, hashRepoRef)\n}\n\nexport function validateRepoRef<V>(v: V) {\n  return validate<RepoRef & V>(v, id, hashRepoRef)\n}\n\nexport interface RepoBlobRef {\n  $type?: 'com.atproto.admin.defs#repoBlobRef'\n  did: string\n  cid: string\n  recordUri?: string\n}\n\nconst hashRepoBlobRef = 'repoBlobRef'\n\nexport function isRepoBlobRef<V>(v: V) {\n  return is$typed(v, id, hashRepoBlobRef)\n}\n\nexport function validateRepoBlobRef<V>(v: V) {\n  return validate<RepoBlobRef & V>(v, id, hashRepoBlobRef)\n}\n\nexport interface ThreatSignature {\n  $type?: 'com.atproto.admin.defs#threatSignature'\n  property: string\n  value: string\n}\n\nconst hashThreatSignature = 'threatSignature'\n\nexport function isThreatSignature<V>(v: V) {\n  return is$typed(v, id, hashThreatSignature)\n}\n\nexport function validateThreatSignature<V>(v: V) {\n  return validate<ThreatSignature & V>(v, id, hashThreatSignature)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for disabled invites. */\n  note?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codes?: string[]\n  accounts?: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.enableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for enabled invites. */\n  note?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoAdminDefs.AccountView\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  infos: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getInviteCodes'\n\nexport type QueryParams = {\n  sort: 'recent' | 'usage' | (string & {})\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getSubjectStatus'\n\nexport type QueryParams = {\n  did?: string\n  uri?: string\n  blob?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.searchAccounts'\n\nexport type QueryParams = {\n  email?: string\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.sendEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  recipientDid: string\n  content: string\n  subject?: string\n  senderDid: string\n  /** Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers */\n  comment?: string\n}\n\nexport interface OutputSchema {\n  sent: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo. */\n  account: string\n  email: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  handle: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  /** Did-key formatted public key */\n  signingKey: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateSubjectStatus'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.defs'\n\nexport interface IdentityInfo {\n  $type?: 'com.atproto.identity.defs#identityInfo'\n  did: string\n  /** The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document. */\n  handle: string\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nconst hashIdentityInfo = 'identityInfo'\n\nexport function isIdentityInfo<V>(v: V) {\n  return is$typed(v, id, hashIdentityInfo)\n}\n\nexport function validateIdentityInfo<V>(v: V) {\n  return validate<IdentityInfo & V>(v, id, hashIdentityInfo)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/getRecommendedDidCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.getRecommendedDidCredentials'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs. */\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/refreshIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.refreshIdentity'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  identifier: string\n}\n\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound' | 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/requestPlcOperationSignature.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.requestPlcOperationSignature'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/resolveDid.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveDid'\n\nexport type QueryParams = {\n  /** DID to resolve. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveHandle'\n\nexport type QueryParams = {\n  /** The handle to resolve. */\n  handle: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/resolveIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveIdentity'\n\nexport type QueryParams = {\n  /** Handle or DID to resolve. */\n  identifier: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound' | 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/signPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.signPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A token received through com.atproto.identity.requestPlcOperationSignature */\n  token?: string\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport interface OutputSchema {\n  /** A signed DID PLC operation. */\n  operation: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/submitPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.submitPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  operation: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.updateHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The new handle. */\n  handle: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/label/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.defs'\n\n/** Metadata tag on an atproto resource (eg, repo or record). */\nexport interface Label {\n  $type?: 'com.atproto.label.defs#label'\n  /** The AT Protocol version of the label object. */\n  ver?: number\n  /** DID of the actor who created this label. */\n  src: string\n  /** AT URI of the record, repository (account), or other resource that this label applies to. */\n  uri: string\n  /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */\n  cid?: string\n  /** The short string name of the value or type of this label. */\n  val: string\n  /** If true, this is a negation label, overwriting a previous label. */\n  neg?: boolean\n  /** Timestamp when this label was created. */\n  cts: string\n  /** Timestamp at which this label expires (no longer applies). */\n  exp?: string\n  /** Signature of dag-cbor encoded label. */\n  sig?: Uint8Array\n}\n\nconst hashLabel = 'label'\n\nexport function isLabel<V>(v: V) {\n  return is$typed(v, id, hashLabel)\n}\n\nexport function validateLabel<V>(v: V) {\n  return validate<Label & V>(v, id, hashLabel)\n}\n\n/** Metadata tags on an atproto record, published by the author within the record. */\nexport interface SelfLabels {\n  $type?: 'com.atproto.label.defs#selfLabels'\n  values: SelfLabel[]\n}\n\nconst hashSelfLabels = 'selfLabels'\n\nexport function isSelfLabels<V>(v: V) {\n  return is$typed(v, id, hashSelfLabels)\n}\n\nexport function validateSelfLabels<V>(v: V) {\n  return validate<SelfLabels & V>(v, id, hashSelfLabels)\n}\n\n/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */\nexport interface SelfLabel {\n  $type?: 'com.atproto.label.defs#selfLabel'\n  /** The short string name of the value or type of this label. */\n  val: string\n}\n\nconst hashSelfLabel = 'selfLabel'\n\nexport function isSelfLabel<V>(v: V) {\n  return is$typed(v, id, hashSelfLabel)\n}\n\nexport function validateSelfLabel<V>(v: V) {\n  return validate<SelfLabel & V>(v, id, hashSelfLabel)\n}\n\n/** Declares a label value and its expected interpretations and behaviors. */\nexport interface LabelValueDefinition {\n  $type?: 'com.atproto.label.defs#labelValueDefinition'\n  /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */\n  identifier: string\n  /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */\n  severity: 'inform' | 'alert' | 'none' | (string & {})\n  /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */\n  blurs: 'content' | 'media' | 'none' | (string & {})\n  /** The default setting for this label. */\n  defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})\n  /** Does the user need to have adult content enabled in order to configure this label? */\n  adultOnly?: boolean\n  locales: LabelValueDefinitionStrings[]\n}\n\nconst hashLabelValueDefinition = 'labelValueDefinition'\n\nexport function isLabelValueDefinition<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinition)\n}\n\nexport function validateLabelValueDefinition<V>(v: V) {\n  return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)\n}\n\n/** Strings which describe the label in the UI, localized into a specific language. */\nexport interface LabelValueDefinitionStrings {\n  $type?: 'com.atproto.label.defs#labelValueDefinitionStrings'\n  /** The code of the language these strings are written in. */\n  lang: string\n  /** A short human-readable name for the label. */\n  name: string\n  /** A longer description of what the label means and why it might be applied. */\n  description: string\n}\n\nconst hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'\n\nexport function isLabelValueDefinitionStrings<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinitionStrings)\n}\n\nexport function validateLabelValueDefinitionStrings<V>(v: V) {\n  return validate<LabelValueDefinitionStrings & V>(\n    v,\n    id,\n    hashLabelValueDefinitionStrings,\n  )\n}\n\nexport type LabelValue =\n  | '!hide'\n  | '!no-promote'\n  | '!warn'\n  | '!no-unauthenticated'\n  | 'dmca-violation'\n  | 'doxxing'\n  | 'porn'\n  | 'sexual'\n  | 'nudity'\n  | 'nsfl'\n  | 'gore'\n  | (string & {})\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.queryLabels'\n\nexport type QueryParams = {\n  /** List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI. */\n  uriPatterns: string[]\n  /** Optional list of label sources (DIDs) to filter on. */\n  sources?: string[]\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport { ErrorFrame } from '@atproto/xrpc-server'\nimport { IncomingMessage } from 'node:http'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.subscribeLabels'\n\nexport type QueryParams = {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\nexport type OutputSchema = $Typed<Labels> | $Typed<Info> | { $type: string }\nexport type HandlerError = ErrorFrame<'FutureCursor'>\nexport type HandlerOutput = HandlerError | OutputSchema\n\nexport interface Labels {\n  $type?: 'com.atproto.label.subscribeLabels#labels'\n  seq: number\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabels = 'labels'\n\nexport function isLabels<V>(v: V) {\n  return is$typed(v, id, hashLabels)\n}\n\nexport function validateLabels<V>(v: V) {\n  return validate<Labels & V>(v, id, hashLabels)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.label.subscribeLabels#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/lexicon/resolveLexicon.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLexiconSchema from './schema.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.resolveLexicon'\n\nexport type QueryParams = {\n  /** The lexicon NSID to resolve. */\n  nsid: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The CID of the lexicon schema record. */\n  cid: string\n  schema: ComAtprotoLexiconSchema.Main\n  /** The AT-URI of the lexicon schema record. */\n  uri: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'LexiconNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/lexicon/schema.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.schema'\n\nexport interface Main {\n  $type: 'com.atproto.lexicon.schema'\n  /** Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system. */\n  lexicon: number\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.createReport'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  /** Additional context about the content and violation. */\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  modTool?: ModTool\n}\n\nexport interface OutputSchema {\n  id: number\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  reportedBy: string\n  createdAt: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'com.atproto.moderation.createReport#modTool'\n  /** Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.defs'\n\nexport type ReasonType =\n  | 'com.atproto.moderation.defs#reasonSpam'\n  | 'com.atproto.moderation.defs#reasonViolation'\n  | 'com.atproto.moderation.defs#reasonMisleading'\n  | 'com.atproto.moderation.defs#reasonSexual'\n  | 'com.atproto.moderation.defs#reasonRude'\n  | 'com.atproto.moderation.defs#reasonOther'\n  | 'com.atproto.moderation.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`. */\nexport const REASONSPAM = `${id}#reasonSpam`\n/** Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`. */\nexport const REASONVIOLATION = `${id}#reasonViolation`\n/** Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`. */\nexport const REASONMISLEADING = `${id}#reasonMisleading`\n/** Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`. */\nexport const REASONSEXUAL = `${id}#reasonSexual`\n/** Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`. */\nexport const REASONRUDE = `${id}#reasonRude`\n/** Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`. */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n\n/** Tag describing a type of subject that might be reported. */\nexport type SubjectType = 'account' | 'record' | 'chat' | (string & {})\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.applyWrites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  writes: ($Typed<Create> | $Typed<Update> | $Typed<Delete>)[]\n  /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  results?: (\n    | $Typed<CreateResult>\n    | $Typed<UpdateResult>\n    | $Typed<DeleteResult>\n  )[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Operation which creates a new record. */\nexport interface Create {\n  $type?: 'com.atproto.repo.applyWrites#create'\n  collection: string\n  /** NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. */\n  rkey?: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashCreate = 'create'\n\nexport function isCreate<V>(v: V) {\n  return is$typed(v, id, hashCreate)\n}\n\nexport function validateCreate<V>(v: V) {\n  return validate<Create & V>(v, id, hashCreate)\n}\n\n/** Operation which updates an existing record. */\nexport interface Update {\n  $type?: 'com.atproto.repo.applyWrites#update'\n  collection: string\n  rkey: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashUpdate = 'update'\n\nexport function isUpdate<V>(v: V) {\n  return is$typed(v, id, hashUpdate)\n}\n\nexport function validateUpdate<V>(v: V) {\n  return validate<Update & V>(v, id, hashUpdate)\n}\n\n/** Operation which deletes an existing record. */\nexport interface Delete {\n  $type?: 'com.atproto.repo.applyWrites#delete'\n  collection: string\n  rkey: string\n}\n\nconst hashDelete = 'delete'\n\nexport function isDelete<V>(v: V) {\n  return is$typed(v, id, hashDelete)\n}\n\nexport function validateDelete<V>(v: V) {\n  return validate<Delete & V>(v, id, hashDelete)\n}\n\nexport interface CreateResult {\n  $type?: 'com.atproto.repo.applyWrites#createResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashCreateResult = 'createResult'\n\nexport function isCreateResult<V>(v: V) {\n  return is$typed(v, id, hashCreateResult)\n}\n\nexport function validateCreateResult<V>(v: V) {\n  return validate<CreateResult & V>(v, id, hashCreateResult)\n}\n\nexport interface UpdateResult {\n  $type?: 'com.atproto.repo.applyWrites#updateResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashUpdateResult = 'updateResult'\n\nexport function isUpdateResult<V>(v: V) {\n  return is$typed(v, id, hashUpdateResult)\n}\n\nexport function validateUpdateResult<V>(v: V) {\n  return validate<UpdateResult & V>(v, id, hashUpdateResult)\n}\n\nexport interface DeleteResult {\n  $type?: 'com.atproto.repo.applyWrites#deleteResult'\n}\n\nconst hashDeleteResult = 'deleteResult'\n\nexport function isDeleteResult<V>(v: V) {\n  return is$typed(v, id, hashDeleteResult)\n}\n\nexport function validateDeleteResult<V>(v: V) {\n  return validate<DeleteResult & V>(v, id, hashDeleteResult)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.createRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey?: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record itself. Must contain a $type field. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.defs'\n\nexport interface CommitMeta {\n  $type?: 'com.atproto.repo.defs#commitMeta'\n  cid: string\n  rev: string\n}\n\nconst hashCommitMeta = 'commitMeta'\n\nexport function isCommitMeta<V>(v: V) {\n  return is$typed(v, id, hashCommitMeta)\n}\n\nexport function validateCommitMeta<V>(v: V) {\n  return validate<CommitMeta & V>(v, id, hashCommitMeta)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.deleteRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Compare and swap with the previous record by CID. */\n  swapRecord?: string\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.describeRepo'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  /** The complete DID document for this account. */\n  didDoc: { [_ in string]: unknown }\n  /** List of all the collections (NSIDs) for which this repo contains at least one record. */\n  collections: string[]\n  /** Indicates if handle is currently valid (resolves bi-directionally) */\n  handleIsCorrect: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.getRecord'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** The CID of the version of the record. If not specified, then return the most recent version. */\n  cid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  value: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RecordNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/importRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.importRepo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface HandlerInput {\n  encoding: 'application/vnd.ipld.car'\n  body: stream.Readable\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/listMissingBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listMissingBlobs'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blobs: RecordBlob[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface RecordBlob {\n  $type?: 'com.atproto.repo.listMissingBlobs#recordBlob'\n  cid: string\n  recordUri: string\n}\n\nconst hashRecordBlob = 'recordBlob'\n\nexport function isRecordBlob<V>(v: V) {\n  return is$typed(v, id, hashRecordBlob)\n}\n\nexport function validateRecordBlob<V>(v: V) {\n  return validate<RecordBlob & V>(v, id, hashRecordBlob)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listRecords'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record type. */\n  collection: string\n  /** The number of records to return. */\n  limit: number\n  cursor?: string\n  /** Flag to reverse the order of the returned records. */\n  reverse?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  records: Record[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Record {\n  $type?: 'com.atproto.repo.listRecords#record'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashRecord = 'record'\n\nexport function isRecord<V>(v: V) {\n  return is$typed(v, id, hashRecord)\n}\n\nexport function validateRecord<V>(v: V) {\n  return validate<Record & V>(v, id, hashRecord)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.putRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record to write. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */\n  swapRecord?: string | null\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/strongRef.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.strongRef'\n\nexport interface Main {\n  $type?: 'com.atproto.repo.strongRef'\n  uri: string\n  cid: string\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.uploadBlob'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  blob: BlobRef\n}\n\nexport interface HandlerInput {\n  encoding: '*/*'\n  body: stream.Readable\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/activateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.activateAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/checkAccountStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.checkAccountStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  validDid: boolean\n  repoCommit: string\n  repoRev: string\n  repoBlocks: number\n  indexedRecords: number\n  privateStateValues: number\n  expectedBlobs: number\n  importedBlobs: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.confirmEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email?: string\n  /** Requested handle for the account. */\n  handle: string\n  /** Pre-existing atproto DID, being imported to a new account. */\n  did?: string\n  inviteCode?: string\n  verificationCode?: string\n  verificationPhone?: string\n  /** Initial account password. May need to meet instance-specific password strength requirements. */\n  password?: string\n  /** DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. */\n  recoveryKey?: string\n  /** A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. */\n  plcOp?: { [_ in string]: unknown }\n}\n\n/** Account login session returned on successful account creation. */\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  /** The DID of the new account. */\n  did: string\n  /** Complete DID document. */\n  didDoc?: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidHandle'\n    | 'InvalidPassword'\n    | 'InvalidInviteCode'\n    | 'HandleNotAvailable'\n    | 'UnsupportedDomain'\n    | 'UnresolvableDid'\n    | 'IncompatibleDidDoc'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A short name for the App Password, to help distinguish them. */\n  name: string\n  /** If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients. */\n  privileged?: boolean\n}\n\nexport type OutputSchema = AppPassword\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.createAppPassword#appPassword'\n  name: string\n  password: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCode'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  useCount: number\n  forAccount?: string\n}\n\nexport interface OutputSchema {\n  code: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codeCount: number\n  useCount: number\n  forAccounts?: string[]\n}\n\nexport interface OutputSchema {\n  codes: AccountCodes[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AccountCodes {\n  $type?: 'com.atproto.server.createInviteCodes#accountCodes'\n  account: string\n  codes: string[]\n}\n\nconst hashAccountCodes = 'accountCodes'\n\nexport function isAccountCodes<V>(v: V) {\n  return is$typed(v, id, hashAccountCodes)\n}\n\nexport function validateAccountCodes<V>(v: V) {\n  return validate<AccountCodes & V>(v, id, hashAccountCodes)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createSession'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Handle or other identifier supported by the server for the authenticating user. */\n  identifier: string\n  password: string\n  authFactorToken?: string\n  /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */\n  allowTakendown?: boolean\n}\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown' | 'AuthFactorTokenRequired'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/deactivateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deactivateAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A recommendation to server as to how long they should hold onto the deactivated account before deleting. */\n  deleteAfter?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.defs'\n\nexport interface InviteCode {\n  $type?: 'com.atproto.server.defs#inviteCode'\n  code: string\n  available: number\n  disabled: boolean\n  forAccount: string\n  createdBy: string\n  createdAt: string\n  uses: InviteCodeUse[]\n}\n\nconst hashInviteCode = 'inviteCode'\n\nexport function isInviteCode<V>(v: V) {\n  return is$typed(v, id, hashInviteCode)\n}\n\nexport function validateInviteCode<V>(v: V) {\n  return validate<InviteCode & V>(v, id, hashInviteCode)\n}\n\nexport interface InviteCodeUse {\n  $type?: 'com.atproto.server.defs#inviteCodeUse'\n  usedBy: string\n  usedAt: string\n}\n\nconst hashInviteCodeUse = 'inviteCodeUse'\n\nexport function isInviteCodeUse<V>(v: V) {\n  return is$typed(v, id, hashInviteCodeUse)\n}\n\nexport function validateInviteCodeUse<V>(v: V) {\n  return validate<InviteCodeUse & V>(v, id, hashInviteCodeUse)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidToken' | 'ExpiredToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.describeServer'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** If true, an invite code must be supplied to create an account on this instance. */\n  inviteCodeRequired?: boolean\n  /** If true, a phone verification token must be supplied to create an account on this instance. */\n  phoneVerificationRequired?: boolean\n  /** List of domain suffixes that can be used in account handles. */\n  availableUserDomains: string[]\n  links?: Links\n  contact?: Contact\n  did: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Links {\n  $type?: 'com.atproto.server.describeServer#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n\nexport interface Contact {\n  $type?: 'com.atproto.server.describeServer#contact'\n  email?: string\n}\n\nconst hashContact = 'contact'\n\nexport function isContact<V>(v: V) {\n  return is$typed(v, id, hashContact)\n}\n\nexport function validateContact<V>(v: V) {\n  return validate<Contact & V>(v, id, hashContact)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getAccountInviteCodes'\n\nexport type QueryParams = {\n  includeUsed: boolean\n  /** Controls whether any new 'earned' but not 'created' invites should be created. */\n  createAvailable: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateCreate'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/getServiceAuth.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getServiceAuth'\n\nexport type QueryParams = {\n  /** The DID of the service that the token will be used to authenticate with */\n  aud: string\n  /** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */\n  exp?: number\n  /** Lexicon (XRPC) method to bind the requested token to */\n  lxm?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  token: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadExpiration'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.listAppPasswords'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  passwords: AppPassword[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.listAppPasswords#appPassword'\n  name: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.refreshSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** Hosting status of the account. If not specified, then assume 'active'. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown' | 'InvalidToken' | 'ExpiredToken'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestAccountDelete'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailConfirmation'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailUpdate'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  tokenRequired: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestPasswordReset'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/reserveSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.reserveSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID to reserve a key for. */\n  did?: string\n}\n\nexport interface OutputSchema {\n  /** The public key for the reserved signing key, in did:key serialization. */\n  signingKey: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.resetPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  token: string\n  password: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.revokeAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  name: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.updateEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  emailAuthFactor?: boolean\n  /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */\n  token?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.defs'\n\nexport type HostStatus =\n  | 'active'\n  | 'idle'\n  | 'offline'\n  | 'throttled'\n  | 'banned'\n  | (string & {})\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlob'\n\nexport type QueryParams = {\n  /** The DID of the account. */\n  did: string\n  /** The CID of the blob to fetch */\n  cid: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: '*/*'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'BlobNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlocks'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  cids: string[]\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'BlockNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getCheckout'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHead'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  root: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HeadNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getHostStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHostStatus'\n\nexport type QueryParams = {\n  /** Hostname of the host (eg, PDS or relay) being queried. */\n  hostname: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  /** Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts. */\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HostNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getLatestCommit'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cid: string\n  rev: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRecord'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  collection: string\n  /** Record Key */\n  rkey: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'RecordNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepo'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** The revision ('rev') of the repo to create a diff from. */\n  since?: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/getRepoStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepoStatus'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  active: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n  /** Optional field, the current rev of the repo, if active=true */\n  rev?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listBlobs'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** Optional revision of the repo to list blobs since. */\n  since?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  cids: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/listHosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listHosts'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first. */\n  hosts: Host[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Host {\n  $type?: 'com.atproto.sync.listHosts#host'\n  /** hostname of server; not a URL (no scheme) */\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nconst hashHost = 'host'\n\nexport function isHost<V>(v: V) {\n  return is$typed(v, id, hashHost)\n}\n\nexport function validateHost<V>(v: V) {\n  return validate<Host & V>(v, id, hashHost)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listRepos'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listRepos#repo'\n  did: string\n  /** Current repo commit CID */\n  head: string\n  rev: string\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/listReposByCollection.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listReposByCollection'\n\nexport type QueryParams = {\n  collection: string\n  /** Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists. */\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listReposByCollection#repo'\n  did: string\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.notifyOfUpdate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (usually a PDS) that is notifying of update. */\n  hostname: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.requestCrawl'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (eg, PDS) that is requesting to be crawled. */\n  hostname: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HostBanned'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport { ErrorFrame } from '@atproto/xrpc-server'\nimport { IncomingMessage } from 'node:http'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.subscribeRepos'\n\nexport type QueryParams = {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\nexport type OutputSchema =\n  | $Typed<Commit>\n  | $Typed<Sync>\n  | $Typed<Identity>\n  | $Typed<Account>\n  | $Typed<Info>\n  | { $type: string }\nexport type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'>\nexport type HandlerOutput = HandlerError | OutputSchema\n\n/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */\nexport interface Commit {\n  $type?: 'com.atproto.sync.subscribeRepos#commit'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** DEPRECATED -- unused */\n  rebase: boolean\n  /** DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */\n  tooBig: boolean\n  /** The repo this event comes from. Note that all other message types name this field 'did'. */\n  repo: string\n  /** Repo commit object CID. */\n  commit: CID\n  /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */\n  rev: string\n  /** The rev of the last emitted commit from this repo (if any). */\n  since: string | null\n  /** CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list. */\n  blocks: Uint8Array\n  ops: RepoOp[]\n  blobs: CID[]\n  /** The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose. */\n  prevData?: CID\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashCommit = 'commit'\n\nexport function isCommit<V>(v: V) {\n  return is$typed(v, id, hashCommit)\n}\n\nexport function validateCommit<V>(v: V) {\n  return validate<Commit & V>(v, id, hashCommit)\n}\n\n/** Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository. */\nexport interface Sync {\n  $type?: 'com.atproto.sync.subscribeRepos#sync'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** The account this repo event corresponds to. Must match that in the commit object. */\n  did: string\n  /** CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'. */\n  blocks: Uint8Array\n  /** The rev of the commit. This value must match that in the commit object. */\n  rev: string\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashSync = 'sync'\n\nexport function isSync<V>(v: V) {\n  return is$typed(v, id, hashSync)\n}\n\nexport function validateSync<V>(v: V) {\n  return validate<Sync & V>(v, id, hashSync)\n}\n\n/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */\nexport interface Identity {\n  $type?: 'com.atproto.sync.subscribeRepos#identity'\n  seq: number\n  did: string\n  time: string\n  /** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */\n  handle?: string\n}\n\nconst hashIdentity = 'identity'\n\nexport function isIdentity<V>(v: V) {\n  return is$typed(v, id, hashIdentity)\n}\n\nexport function validateIdentity<V>(v: V) {\n  return validate<Identity & V>(v, id, hashIdentity)\n}\n\n/** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */\nexport interface Account {\n  $type?: 'com.atproto.sync.subscribeRepos#account'\n  seq: number\n  did: string\n  time: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  /** If active=false, this optional field indicates a reason for why the account is not active. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashAccount = 'account'\n\nexport function isAccount<V>(v: V) {\n  return is$typed(v, id, hashAccount)\n}\n\nexport function validateAccount<V>(v: V) {\n  return validate<Account & V>(v, id, hashAccount)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.sync.subscribeRepos#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n\n/** A repo operation, ie a mutation of a single record. */\nexport interface RepoOp {\n  $type?: 'com.atproto.sync.subscribeRepos#repoOp'\n  action: 'create' | 'update' | 'delete' | (string & {})\n  path: string\n  /** For creates and updates, the new record CID. For deletions, null. */\n  cid: CID | null\n  /** For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined. */\n  prev?: CID\n}\n\nconst hashRepoOp = 'repoOp'\n\nexport function isRepoOp<V>(v: V) {\n  return is$typed(v, id, hashRepoOp)\n}\n\nexport function validateRepoOp<V>(v: V) {\n  return validate<RepoOp & V>(v, id, hashRepoOp)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/addReservedHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.addReservedHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  handle: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/checkHandleAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkHandleAvailability'\n\nexport type QueryParams = {\n  /** Tentative handle. Will be checked for availability or used to build handle suggestions. */\n  handle: string\n  /** User-provided email. Might be used to build handle suggestions. */\n  email?: string\n  /** User-provided birth date. Might be used to build handle suggestions. */\n  birthDate?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Echo of the input handle. */\n  handle: string\n  result:\n    | $Typed<ResultAvailable>\n    | $Typed<ResultUnavailable>\n    | { $type: string }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidEmail'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Indicates the provided handle is available. */\nexport interface ResultAvailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultAvailable'\n}\n\nconst hashResultAvailable = 'resultAvailable'\n\nexport function isResultAvailable<V>(v: V) {\n  return is$typed(v, id, hashResultAvailable)\n}\n\nexport function validateResultAvailable<V>(v: V) {\n  return validate<ResultAvailable & V>(v, id, hashResultAvailable)\n}\n\n/** Indicates the provided handle is unavailable and gives suggestions of available handles. */\nexport interface ResultUnavailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'\n  /** List of suggested handles based on the provided inputs. */\n  suggestions: Suggestion[]\n}\n\nconst hashResultUnavailable = 'resultUnavailable'\n\nexport function isResultUnavailable<V>(v: V) {\n  return is$typed(v, id, hashResultUnavailable)\n}\n\nexport function validateResultUnavailable<V>(v: V) {\n  return validate<ResultUnavailable & V>(v, id, hashResultUnavailable)\n}\n\nexport interface Suggestion {\n  $type?: 'com.atproto.temp.checkHandleAvailability#suggestion'\n  handle: string\n  /** Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics. */\n  method: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkSignupQueue'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  placeInQueue?: number\n  estimatedTimeMs?: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/dereferenceScope.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.dereferenceScope'\n\nexport type QueryParams = {\n  /** The scope reference (starts with 'ref:') */\n  scope: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The full oauth permission scope */\n  scope: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidScopeReference'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/fetchLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.fetchLabels'\n\nexport type QueryParams = {\n  since?: number\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.requestPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  phoneNumber: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/atproto/temp/revokeAccountCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.revokeAccountCredentials'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/bsky/src/lexicon/types/com/germnetwork/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../lexicons'\nimport { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.germnetwork.declaration'\n\nexport interface Main {\n  $type: 'com.germnetwork.declaration'\n  /** Semver version number, without pre-release or build information, for the format of opaque content */\n  version: string\n  /** Opaque value, an ed25519 public key prefixed with a byte enum */\n  currentKey: Uint8Array\n  messageMe?: MessageMe\n  /** Opaque value, contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey */\n  keyPackage?: Uint8Array\n  /** Array of opaque values to allow for key rolling */\n  continuityProofs?: Uint8Array[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface MessageMe {\n  $type?: 'com.germnetwork.declaration#messageMe'\n  /** A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who wish to message each other */\n  messageMeUrl: string\n  /** The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer. */\n  showButtonTo: 'none' | 'usersIFollow' | 'everyone' | (string & {})\n}\n\nconst hashMessageMe = 'messageMe'\n\nexport function isMessageMe<V>(v: V) {\n  return is$typed(v, id, hashMessageMe)\n}\n\nexport function validateMessageMe<V>(v: V) {\n  return validate<MessageMe & V>(v, id, hashMessageMe)\n}\n"
  },
  {
    "path": "packages/bsky/src/lexicon/util.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\n\nimport { type ValidationResult } from '@atproto/lexicon'\n\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n\nexport type $Typed<V, T extends string = string> = V & { $type: T }\nexport type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>\n\nexport type $Type<Id extends string, Hash extends string> = Hash extends 'main'\n  ? Id\n  : `${Id}#${Hash}`\n\nfunction isObject<V>(v: V): v is V & object {\n  return v != null && typeof v === 'object'\n}\n\nfunction is$type<Id extends string, Hash extends string>(\n  $type: unknown,\n  id: Id,\n  hash: Hash,\n): $type is $Type<Id, Hash> {\n  return hash === 'main'\n    ? $type === id\n    : // $type === `${id}#${hash}`\n      typeof $type === 'string' &&\n        $type.length === id.length + 1 + hash.length &&\n        $type.charCodeAt(id.length) === 35 /* '#' */ &&\n        $type.startsWith(id) &&\n        $type.endsWith(hash)\n}\n\nexport type $TypedObject<\n  V,\n  Id extends string,\n  Hash extends string,\n> = V extends {\n  $type: $Type<Id, Hash>\n}\n  ? V\n  : V extends { $type?: string }\n    ? V extends { $type?: infer T extends $Type<Id, Hash> }\n      ? V & { $type: T }\n      : never\n    : V & { $type: $Type<Id, Hash> }\n\nexport function is$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is $TypedObject<V, Id, Hash> {\n  return isObject(v) && '$type' in v && is$type(v.$type, id, hash)\n}\n\nexport function maybe$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is V & object & { $type?: $Type<Id, Hash> } {\n  return (\n    isObject(v) &&\n    ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)\n  )\n}\n\nexport type Validator<R = unknown> = (v: unknown) => ValidationResult<R>\nexport type ValidatorParam<V extends Validator> =\n  V extends Validator<infer R> ? R : never\n\n/**\n * Utility function that allows to convert a \"validate*\" utility function into a\n * type predicate.\n */\nexport function asPredicate<V extends Validator>(validate: V) {\n  return function <T>(v: T): v is T & ValidatorParam<V> {\n    return validate(v).success\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/logger.ts",
    "content": "import { IncomingMessage } from 'node:http'\nimport { stdSerializers } from 'pino'\nimport { pinoHttp } from 'pino-http'\nimport { obfuscateHeaders, subsystemLogger } from '@atproto/common'\n\nexport const dbLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:db')\nexport const cacheLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:cache')\nexport const subLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:sub')\nexport const labelerLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:labeler')\nexport const hydrationLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:hydration')\nexport const featureGatesLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:featuregates')\nexport const dataplaneLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:dp')\nexport const ageAssuranceLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky:aa')\nexport const httpLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsky')\n\nexport const loggerMiddleware = pinoHttp({\n  logger: httpLogger,\n  serializers: {\n    err: (err: unknown) => ({\n      code: err?.['code'],\n      message: err?.['message'],\n    }),\n    req: (req: IncomingMessage) => {\n      const serialized = stdSerializers.req(req)\n      const headers = obfuscateHeaders(serialized.headers)\n      return { ...serialized, headers }\n    },\n  },\n})\n"
  },
  {
    "path": "packages/bsky/src/pipeline.ts",
    "content": "import { HydrationState } from './hydration/hydrator'\n\nexport function createPipeline<Params, Skeleton, View, Context>(\n  skeletonFn: (input: SkeletonFnInput<Context, Params>) => Promise<Skeleton>,\n  hydrationFn: (\n    input: HydrationFnInput<Context, Params, Skeleton>,\n  ) => Promise<HydrationState>,\n  rulesFn: (input: RulesFnInput<Context, Params, Skeleton>) => Skeleton,\n  presentationFn: (\n    input: PresentationFnInput<Context, Params, Skeleton>,\n  ) => View,\n) {\n  return async (params: Params, ctx: Context) => {\n    const skeleton = await skeletonFn({ ctx, params })\n    const hydration = await hydrationFn({ ctx, params, skeleton })\n    const appliedRules = rulesFn({ ctx, params, skeleton, hydration })\n    return presentationFn({ ctx, params, skeleton: appliedRules, hydration })\n  }\n}\n\nexport type SkeletonFnInput<Context, Params> = {\n  ctx: Context\n  params: Params\n}\n\nexport type HydrationFnInput<Context, Params, Skeleton> = {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n}\n\nexport type RulesFnInput<Context, Params, Skeleton> = {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}\n\nexport type PresentationFnInput<Context, Params, Skeleton> = {\n  ctx: Context\n  params: Params\n  skeleton: Skeleton\n  hydration: HydrationState\n}\n\nexport function noRules<S>(input: { skeleton: S }) {\n  return input.skeleton\n}\n"
  },
  {
    "path": "packages/bsky/src/proto/bsky_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.3.0 with parameter \"target=ts,import_extension=\"\n// @generated from file bsky.proto (package bsky, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { ClearActorMutelistSubscriptionsRequest, ClearActorMutelistSubscriptionsResponse, ClearActorMutesRequest, ClearActorMutesResponse, ClearThreadMutesRequest, ClearThreadMutesResponse, CreateActorMutelistSubscriptionRequest, CreateActorMutelistSubscriptionResponse, CreateActorMuteRequest, CreateActorMuteResponse, CreateThreadMuteRequest, CreateThreadMuteResponse, DeleteActorMutelistSubscriptionRequest, DeleteActorMutelistSubscriptionResponse, DeleteActorMuteRequest, DeleteActorMuteResponse, DeleteThreadMuteRequest, DeleteThreadMuteResponse, GetActivitySubscriptionDidsRequest, GetActivitySubscriptionDidsResponse, GetActivitySubscriptionsByActorAndSubjectsRequest, GetActivitySubscriptionsByActorAndSubjectsResponse, GetActorBookmarksRequest, GetActorBookmarksResponse, GetActorChatDeclarationRecordsRequest, GetActorChatDeclarationRecordsResponse, GetActorDraftsRequest, GetActorDraftsResponse, GetActorFeedsRequest, GetActorFeedsResponse, GetActorFollowsActorsRequest, GetActorFollowsActorsResponse, GetActorLikesRequest, GetActorLikesResponse, GetActorListsRequest, GetActorListsResponse, GetActorMutesActorRequest, GetActorMutesActorResponse, GetActorMutesActorViaListRequest, GetActorMutesActorViaListResponse, GetActorRepostsRequest, GetActorRepostsResponse, GetActorsRequest, GetActorsResponse, GetActorStarterPacksRequest, GetActorStarterPacksResponse, GetActorTakedownRequest, GetActorTakedownResponse, GetAllLabelersRequest, GetAllLabelersResponse, GetAuthorFeedRequest, GetAuthorFeedResponse, GetBidirectionalBlockRequest, GetBidirectionalBlockResponse, GetBidirectionalBlockViaListRequest, GetBidirectionalBlockViaListResponse, GetBlobTakedownRequest, GetBlobTakedownResponse, GetBlockExistenceRequest, GetBlockExistenceResponse, GetBlocklistSubscriptionRequest, GetBlocklistSubscriptionResponse, GetBlocklistSubscriptionsRequest, GetBlocklistSubscriptionsResponse, GetBlockRecordsRequest, GetBlockRecordsResponse, GetBlocksRequest, GetBlocksResponse, GetBookmarksByActorAndSubjectsRequest, GetBookmarksByActorAndSubjectsResponse, GetCountsForUsersRequest, GetCountsForUsersResponse, GetDidsByHandlesRequest, GetDidsByHandlesResponse, GetFeedGeneratorRecordsRequest, GetFeedGeneratorRecordsResponse, GetFeedGeneratorStatusRequest, GetFeedGeneratorStatusResponse, GetFollowersRequest, GetFollowersResponse, GetFollowRecordsRequest, GetFollowRecordsResponse, GetFollowsFollowingRequest, GetFollowsFollowingResponse, GetFollowsRequest, GetFollowsResponse, GetFollowSuggestionsRequest, GetFollowSuggestionsResponse, GetGermDeclarationRecordsRequest, GetGermDeclarationRecordsResponse, GetIdentityByDidRequest, GetIdentityByDidResponse, GetIdentityByHandleRequest, GetIdentityByHandleResponse, GetInteractionCountsRequest, GetInteractionCountsResponse, GetLabelerRecordsRequest, GetLabelerRecordsResponse, GetLabelsRequest, GetLabelsResponse, GetLatestRevRequest, GetLatestRevResponse, GetLikeRecordsRequest, GetLikeRecordsResponse, GetLikesByActorAndSubjectsRequest, GetLikesByActorAndSubjectsResponse, GetLikesBySubjectRequest, GetLikesBySubjectResponse, GetLikesBySubjectSortedRequest, GetLikesBySubjectSortedResponse, GetListBlockRecordsRequest, GetListBlockRecordsResponse, GetListCountRequest, GetListCountResponse, GetListCountsRequest, GetListCountsResponse, GetListFeedRequest, GetListFeedResponse, GetListItemRecordsRequest, GetListItemRecordsResponse, GetListMembershipRequest, GetListMembershipResponse, GetListMembersRequest, GetListMembersResponse, GetListRecordsRequest, GetListRecordsResponse, GetMutelistSubscriptionRequest, GetMutelistSubscriptionResponse, GetMutelistSubscriptionsRequest, GetMutelistSubscriptionsResponse, GetMutesRequest, GetMutesResponse, GetNewUserCountForRangeRequest, GetNewUserCountForRangeResponse, GetNotificationDeclarationRecordsRequest, GetNotificationDeclarationRecordsResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetNotificationSeenRequest, GetNotificationSeenResponse, GetNotificationsRequest, GetNotificationsResponse, GetPostgateRecordsRequest, GetPostgateRecordsResponse, GetPostRecordsRequest, GetPostRecordsResponse, GetProfileRecordsRequest, GetProfileRecordsResponse, GetQuotesBySubjectSortedRequest, GetQuotesBySubjectSortedResponse, GetRecordTakedownRequest, GetRecordTakedownResponse, GetRelationshipsRequest, GetRelationshipsResponse, GetRepostRecordsRequest, GetRepostRecordsResponse, GetRepostsByActorAndSubjectsRequest, GetRepostsByActorAndSubjectsResponse, GetRepostsBySubjectRequest, GetRepostsBySubjectResponse, GetSitemapIndexRequest, GetSitemapIndexResponse, GetSitemapPageRequest, GetSitemapPageResponse, GetStarterPackCountsRequest, GetStarterPackCountsResponse, GetStarterPackRecordsRequest, GetStarterPackRecordsResponse, GetStatusRecordsRequest, GetStatusRecordsResponse, GetSuggestedEntitiesRequest, GetSuggestedEntitiesResponse, GetSuggestedFeedsRequest, GetSuggestedFeedsResponse, GetThreadGateRecordsRequest, GetThreadGateRecordsResponse, GetThreadMutesOnSubjectsRequest, GetThreadMutesOnSubjectsResponse, GetThreadRequest, GetThreadResponse, GetTimelineRequest, GetTimelineResponse, GetUnreadNotificationCountRequest, GetUnreadNotificationCountResponse, GetVerificationRecordsRequest, GetVerificationRecordsResponse, GetVerificationsIssuedRequest, GetVerificationsIssuedResponse, GetVerificationsReceivedRequest, GetVerificationsReceivedResponse, PingRequest, PingResponse, SearchActorsRequest, SearchActorsResponse, SearchFeedGeneratorsRequest, SearchFeedGeneratorsResponse, SearchPostsRequest, SearchPostsResponse, SearchStarterPacksRequest, SearchStarterPacksResponse, TakedownActorRequest, TakedownActorResponse, TakedownBlobRequest, TakedownBlobResponse, TakedownRecordRequest, TakedownRecordResponse, UntakedownActorRequest, UntakedownActorResponse, UntakedownBlobRequest, UntakedownBlobResponse, UntakedownRecordRequest, UntakedownRecordResponse, UpdateActorUpstreamStatusRequest, UpdateActorUpstreamStatusResponse, UpdateNotificationSeenRequest, UpdateNotificationSeenResponse } from \"./bsky_pb\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n *\n * Read Path\n *\n *\n * @generated from service bsky.Service\n */\nexport const Service = {\n  typeName: \"bsky.Service\",\n  methods: {\n    /**\n     * Records\n     *\n     * @generated from rpc bsky.Service.GetBlockRecords\n     */\n    getBlockRecords: {\n      name: \"GetBlockRecords\",\n      I: GetBlockRecordsRequest,\n      O: GetBlockRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetFeedGeneratorRecords\n     */\n    getFeedGeneratorRecords: {\n      name: \"GetFeedGeneratorRecords\",\n      I: GetFeedGeneratorRecordsRequest,\n      O: GetFeedGeneratorRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetFollowRecords\n     */\n    getFollowRecords: {\n      name: \"GetFollowRecords\",\n      I: GetFollowRecordsRequest,\n      O: GetFollowRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetLikeRecords\n     */\n    getLikeRecords: {\n      name: \"GetLikeRecords\",\n      I: GetLikeRecordsRequest,\n      O: GetLikeRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListBlockRecords\n     */\n    getListBlockRecords: {\n      name: \"GetListBlockRecords\",\n      I: GetListBlockRecordsRequest,\n      O: GetListBlockRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListItemRecords\n     */\n    getListItemRecords: {\n      name: \"GetListItemRecords\",\n      I: GetListItemRecordsRequest,\n      O: GetListItemRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListRecords\n     */\n    getListRecords: {\n      name: \"GetListRecords\",\n      I: GetListRecordsRequest,\n      O: GetListRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetPostRecords\n     */\n    getPostRecords: {\n      name: \"GetPostRecords\",\n      I: GetPostRecordsRequest,\n      O: GetPostRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetProfileRecords\n     */\n    getProfileRecords: {\n      name: \"GetProfileRecords\",\n      I: GetProfileRecordsRequest,\n      O: GetProfileRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetActorChatDeclarationRecords\n     */\n    getActorChatDeclarationRecords: {\n      name: \"GetActorChatDeclarationRecords\",\n      I: GetActorChatDeclarationRecordsRequest,\n      O: GetActorChatDeclarationRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetNotificationDeclarationRecords\n     */\n    getNotificationDeclarationRecords: {\n      name: \"GetNotificationDeclarationRecords\",\n      I: GetNotificationDeclarationRecordsRequest,\n      O: GetNotificationDeclarationRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetGermDeclarationRecords\n     */\n    getGermDeclarationRecords: {\n      name: \"GetGermDeclarationRecords\",\n      I: GetGermDeclarationRecordsRequest,\n      O: GetGermDeclarationRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetStatusRecords\n     */\n    getStatusRecords: {\n      name: \"GetStatusRecords\",\n      I: GetStatusRecordsRequest,\n      O: GetStatusRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetRepostRecords\n     */\n    getRepostRecords: {\n      name: \"GetRepostRecords\",\n      I: GetRepostRecordsRequest,\n      O: GetRepostRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetThreadGateRecords\n     */\n    getThreadGateRecords: {\n      name: \"GetThreadGateRecords\",\n      I: GetThreadGateRecordsRequest,\n      O: GetThreadGateRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetPostgateRecords\n     */\n    getPostgateRecords: {\n      name: \"GetPostgateRecords\",\n      I: GetPostgateRecordsRequest,\n      O: GetPostgateRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetLabelerRecords\n     */\n    getLabelerRecords: {\n      name: \"GetLabelerRecords\",\n      I: GetLabelerRecordsRequest,\n      O: GetLabelerRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetStarterPackRecords\n     */\n    getStarterPackRecords: {\n      name: \"GetStarterPackRecords\",\n      I: GetStarterPackRecordsRequest,\n      O: GetStarterPackRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Follows\n     *\n     * @generated from rpc bsky.Service.GetActorFollowsActors\n     */\n    getActorFollowsActors: {\n      name: \"GetActorFollowsActors\",\n      I: GetActorFollowsActorsRequest,\n      O: GetActorFollowsActorsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetFollowers\n     */\n    getFollowers: {\n      name: \"GetFollowers\",\n      I: GetFollowersRequest,\n      O: GetFollowersResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetFollows\n     */\n    getFollows: {\n      name: \"GetFollows\",\n      I: GetFollowsRequest,\n      O: GetFollowsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Verifications\n     *\n     * @generated from rpc bsky.Service.GetVerificationRecords\n     */\n    getVerificationRecords: {\n      name: \"GetVerificationRecords\",\n      I: GetVerificationRecordsRequest,\n      O: GetVerificationRecordsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetVerificationsIssued\n     */\n    getVerificationsIssued: {\n      name: \"GetVerificationsIssued\",\n      I: GetVerificationsIssuedRequest,\n      O: GetVerificationsIssuedResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetVerificationsReceived\n     */\n    getVerificationsReceived: {\n      name: \"GetVerificationsReceived\",\n      I: GetVerificationsReceivedRequest,\n      O: GetVerificationsReceivedResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Likes\n     *\n     * @generated from rpc bsky.Service.GetLikesBySubject\n     */\n    getLikesBySubject: {\n      name: \"GetLikesBySubject\",\n      I: GetLikesBySubjectRequest,\n      O: GetLikesBySubjectResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetLikesBySubjectSorted\n     */\n    getLikesBySubjectSorted: {\n      name: \"GetLikesBySubjectSorted\",\n      I: GetLikesBySubjectSortedRequest,\n      O: GetLikesBySubjectSortedResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetLikesByActorAndSubjects\n     */\n    getLikesByActorAndSubjects: {\n      name: \"GetLikesByActorAndSubjects\",\n      I: GetLikesByActorAndSubjectsRequest,\n      O: GetLikesByActorAndSubjectsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetActorLikes\n     */\n    getActorLikes: {\n      name: \"GetActorLikes\",\n      I: GetActorLikesRequest,\n      O: GetActorLikesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Reposts\n     *\n     * @generated from rpc bsky.Service.GetRepostsBySubject\n     */\n    getRepostsBySubject: {\n      name: \"GetRepostsBySubject\",\n      I: GetRepostsBySubjectRequest,\n      O: GetRepostsBySubjectResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetRepostsByActorAndSubjects\n     */\n    getRepostsByActorAndSubjects: {\n      name: \"GetRepostsByActorAndSubjects\",\n      I: GetRepostsByActorAndSubjectsRequest,\n      O: GetRepostsByActorAndSubjectsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetActorReposts\n     */\n    getActorReposts: {\n      name: \"GetActorReposts\",\n      I: GetActorRepostsRequest,\n      O: GetActorRepostsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Quotes\n     *\n     * @generated from rpc bsky.Service.GetQuotesBySubjectSorted\n     */\n    getQuotesBySubjectSorted: {\n      name: \"GetQuotesBySubjectSorted\",\n      I: GetQuotesBySubjectSortedRequest,\n      O: GetQuotesBySubjectSortedResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Interaction Counts\n     *\n     * @generated from rpc bsky.Service.GetInteractionCounts\n     */\n    getInteractionCounts: {\n      name: \"GetInteractionCounts\",\n      I: GetInteractionCountsRequest,\n      O: GetInteractionCountsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetCountsForUsers\n     */\n    getCountsForUsers: {\n      name: \"GetCountsForUsers\",\n      I: GetCountsForUsersRequest,\n      O: GetCountsForUsersResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetStarterPackCounts\n     */\n    getStarterPackCounts: {\n      name: \"GetStarterPackCounts\",\n      I: GetStarterPackCountsRequest,\n      O: GetStarterPackCountsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListCounts\n     */\n    getListCounts: {\n      name: \"GetListCounts\",\n      I: GetListCountsRequest,\n      O: GetListCountsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetNewUserCountForRange\n     */\n    getNewUserCountForRange: {\n      name: \"GetNewUserCountForRange\",\n      I: GetNewUserCountForRangeRequest,\n      O: GetNewUserCountForRangeResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Profile\n     *\n     * @generated from rpc bsky.Service.GetActors\n     */\n    getActors: {\n      name: \"GetActors\",\n      I: GetActorsRequest,\n      O: GetActorsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetDidsByHandles\n     */\n    getDidsByHandles: {\n      name: \"GetDidsByHandles\",\n      I: GetDidsByHandlesRequest,\n      O: GetDidsByHandlesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Relationships\n     *\n     * @generated from rpc bsky.Service.GetRelationships\n     */\n    getRelationships: {\n      name: \"GetRelationships\",\n      I: GetRelationshipsRequest,\n      O: GetRelationshipsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetBlockExistence\n     */\n    getBlockExistence: {\n      name: \"GetBlockExistence\",\n      I: GetBlockExistenceRequest,\n      O: GetBlockExistenceResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Lists\n     *\n     * @generated from rpc bsky.Service.GetActorLists\n     */\n    getActorLists: {\n      name: \"GetActorLists\",\n      I: GetActorListsRequest,\n      O: GetActorListsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListMembers\n     */\n    getListMembers: {\n      name: \"GetListMembers\",\n      I: GetListMembersRequest,\n      O: GetListMembersResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListMembership\n     */\n    getListMembership: {\n      name: \"GetListMembership\",\n      I: GetListMembershipRequest,\n      O: GetListMembershipResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListCount\n     */\n    getListCount: {\n      name: \"GetListCount\",\n      I: GetListCountRequest,\n      O: GetListCountResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Mutes\n     *\n     * @generated from rpc bsky.Service.GetActorMutesActor\n     */\n    getActorMutesActor: {\n      name: \"GetActorMutesActor\",\n      I: GetActorMutesActorRequest,\n      O: GetActorMutesActorResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetMutes\n     */\n    getMutes: {\n      name: \"GetMutes\",\n      I: GetMutesRequest,\n      O: GetMutesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Mutelists\n     *\n     * @generated from rpc bsky.Service.GetActorMutesActorViaList\n     */\n    getActorMutesActorViaList: {\n      name: \"GetActorMutesActorViaList\",\n      I: GetActorMutesActorViaListRequest,\n      O: GetActorMutesActorViaListResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetMutelistSubscription\n     */\n    getMutelistSubscription: {\n      name: \"GetMutelistSubscription\",\n      I: GetMutelistSubscriptionRequest,\n      O: GetMutelistSubscriptionResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetMutelistSubscriptions\n     */\n    getMutelistSubscriptions: {\n      name: \"GetMutelistSubscriptions\",\n      I: GetMutelistSubscriptionsRequest,\n      O: GetMutelistSubscriptionsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Thread Mutes\n     *\n     * @generated from rpc bsky.Service.GetThreadMutesOnSubjects\n     */\n    getThreadMutesOnSubjects: {\n      name: \"GetThreadMutesOnSubjects\",\n      I: GetThreadMutesOnSubjectsRequest,\n      O: GetThreadMutesOnSubjectsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Blocks\n     *\n     * @generated from rpc bsky.Service.GetBidirectionalBlock\n     */\n    getBidirectionalBlock: {\n      name: \"GetBidirectionalBlock\",\n      I: GetBidirectionalBlockRequest,\n      O: GetBidirectionalBlockResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetBlocks\n     */\n    getBlocks: {\n      name: \"GetBlocks\",\n      I: GetBlocksRequest,\n      O: GetBlocksResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Blocklists\n     *\n     * @generated from rpc bsky.Service.GetBidirectionalBlockViaList\n     */\n    getBidirectionalBlockViaList: {\n      name: \"GetBidirectionalBlockViaList\",\n      I: GetBidirectionalBlockViaListRequest,\n      O: GetBidirectionalBlockViaListResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetBlocklistSubscription\n     */\n    getBlocklistSubscription: {\n      name: \"GetBlocklistSubscription\",\n      I: GetBlocklistSubscriptionRequest,\n      O: GetBlocklistSubscriptionResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetBlocklistSubscriptions\n     */\n    getBlocklistSubscriptions: {\n      name: \"GetBlocklistSubscriptions\",\n      I: GetBlocklistSubscriptionsRequest,\n      O: GetBlocklistSubscriptionsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Notifications\n     *\n     * @generated from rpc bsky.Service.GetNotificationPreferences\n     */\n    getNotificationPreferences: {\n      name: \"GetNotificationPreferences\",\n      I: GetNotificationPreferencesRequest,\n      O: GetNotificationPreferencesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetNotifications\n     */\n    getNotifications: {\n      name: \"GetNotifications\",\n      I: GetNotificationsRequest,\n      O: GetNotificationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetNotificationSeen\n     */\n    getNotificationSeen: {\n      name: \"GetNotificationSeen\",\n      I: GetNotificationSeenRequest,\n      O: GetNotificationSeenResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetUnreadNotificationCount\n     */\n    getUnreadNotificationCount: {\n      name: \"GetUnreadNotificationCount\",\n      I: GetUnreadNotificationCountRequest,\n      O: GetUnreadNotificationCountResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetActivitySubscriptionDids\n     */\n    getActivitySubscriptionDids: {\n      name: \"GetActivitySubscriptionDids\",\n      I: GetActivitySubscriptionDidsRequest,\n      O: GetActivitySubscriptionDidsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetActivitySubscriptionsByActorAndSubjects\n     */\n    getActivitySubscriptionsByActorAndSubjects: {\n      name: \"GetActivitySubscriptionsByActorAndSubjects\",\n      I: GetActivitySubscriptionsByActorAndSubjectsRequest,\n      O: GetActivitySubscriptionsByActorAndSubjectsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.UpdateNotificationSeen\n     */\n    updateNotificationSeen: {\n      name: \"UpdateNotificationSeen\",\n      I: UpdateNotificationSeenRequest,\n      O: UpdateNotificationSeenResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * FeedGenerators\n     *\n     * @generated from rpc bsky.Service.GetActorFeeds\n     */\n    getActorFeeds: {\n      name: \"GetActorFeeds\",\n      I: GetActorFeedsRequest,\n      O: GetActorFeedsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetSuggestedFeeds\n     */\n    getSuggestedFeeds: {\n      name: \"GetSuggestedFeeds\",\n      I: GetSuggestedFeedsRequest,\n      O: GetSuggestedFeedsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetFeedGeneratorStatus\n     */\n    getFeedGeneratorStatus: {\n      name: \"GetFeedGeneratorStatus\",\n      I: GetFeedGeneratorStatusRequest,\n      O: GetFeedGeneratorStatusResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.SearchFeedGenerators\n     */\n    searchFeedGenerators: {\n      name: \"SearchFeedGenerators\",\n      I: SearchFeedGeneratorsRequest,\n      O: SearchFeedGeneratorsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Feeds\n     *\n     * @generated from rpc bsky.Service.GetAuthorFeed\n     */\n    getAuthorFeed: {\n      name: \"GetAuthorFeed\",\n      I: GetAuthorFeedRequest,\n      O: GetAuthorFeedResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetTimeline\n     */\n    getTimeline: {\n      name: \"GetTimeline\",\n      I: GetTimelineRequest,\n      O: GetTimelineResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetListFeed\n     */\n    getListFeed: {\n      name: \"GetListFeed\",\n      I: GetListFeedRequest,\n      O: GetListFeedResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Threads\n     *\n     * @generated from rpc bsky.Service.GetThread\n     */\n    getThread: {\n      name: \"GetThread\",\n      I: GetThreadRequest,\n      O: GetThreadResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Search\n     *\n     * @generated from rpc bsky.Service.SearchActors\n     */\n    searchActors: {\n      name: \"SearchActors\",\n      I: SearchActorsRequest,\n      O: SearchActorsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.SearchPosts\n     */\n    searchPosts: {\n      name: \"SearchPosts\",\n      I: SearchPostsRequest,\n      O: SearchPostsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.SearchStarterPacks\n     */\n    searchStarterPacks: {\n      name: \"SearchStarterPacks\",\n      I: SearchStarterPacksRequest,\n      O: SearchStarterPacksResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Suggestions\n     *\n     * @generated from rpc bsky.Service.GetFollowSuggestions\n     */\n    getFollowSuggestions: {\n      name: \"GetFollowSuggestions\",\n      I: GetFollowSuggestionsRequest,\n      O: GetFollowSuggestionsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetSuggestedEntities\n     */\n    getSuggestedEntities: {\n      name: \"GetSuggestedEntities\",\n      I: GetSuggestedEntitiesRequest,\n      O: GetSuggestedEntitiesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Labels\n     *\n     * @generated from rpc bsky.Service.GetLabels\n     */\n    getLabels: {\n      name: \"GetLabels\",\n      I: GetLabelsRequest,\n      O: GetLabelsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetAllLabelers\n     */\n    getAllLabelers: {\n      name: \"GetAllLabelers\",\n      I: GetAllLabelersRequest,\n      O: GetAllLabelersResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Starter packs\n     *\n     * @generated from rpc bsky.Service.GetActorStarterPacks\n     */\n    getActorStarterPacks: {\n      name: \"GetActorStarterPacks\",\n      I: GetActorStarterPacksRequest,\n      O: GetActorStarterPacksResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Sync\n     *\n     * @generated from rpc bsky.Service.GetLatestRev\n     */\n    getLatestRev: {\n      name: \"GetLatestRev\",\n      I: GetLatestRevRequest,\n      O: GetLatestRevResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Moderation\n     *\n     * @generated from rpc bsky.Service.GetBlobTakedown\n     */\n    getBlobTakedown: {\n      name: \"GetBlobTakedown\",\n      I: GetBlobTakedownRequest,\n      O: GetBlobTakedownResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetRecordTakedown\n     */\n    getRecordTakedown: {\n      name: \"GetRecordTakedown\",\n      I: GetRecordTakedownRequest,\n      O: GetRecordTakedownResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetActorTakedown\n     */\n    getActorTakedown: {\n      name: \"GetActorTakedown\",\n      I: GetActorTakedownRequest,\n      O: GetActorTakedownResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Bookmarks\n     * Returns bookmarks created by the actor for the specified URIs.\n     *\n     * @generated from rpc bsky.Service.GetBookmarksByActorAndSubjects\n     */\n    getBookmarksByActorAndSubjects: {\n      name: \"GetBookmarksByActorAndSubjects\",\n      I: GetBookmarksByActorAndSubjectsRequest,\n      O: GetBookmarksByActorAndSubjectsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Returns the bookmarks created by the actor.\n     *\n     * @generated from rpc bsky.Service.GetActorBookmarks\n     */\n    getActorBookmarks: {\n      name: \"GetActorBookmarks\",\n      I: GetActorBookmarksRequest,\n      O: GetActorBookmarksResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Drafts\n     * Returns a page of drafts for a user.\n     *\n     * @generated from rpc bsky.Service.GetActorDrafts\n     */\n    getActorDrafts: {\n      name: \"GetActorDrafts\",\n      I: GetActorDraftsRequest,\n      O: GetActorDraftsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Identity\n     *\n     * @generated from rpc bsky.Service.GetIdentityByDid\n     */\n    getIdentityByDid: {\n      name: \"GetIdentityByDid\",\n      I: GetIdentityByDidRequest,\n      O: GetIdentityByDidResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetIdentityByHandle\n     */\n    getIdentityByHandle: {\n      name: \"GetIdentityByHandle\",\n      I: GetIdentityByHandleRequest,\n      O: GetIdentityByHandleResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Graph\n     *\n     * @generated from rpc bsky.Service.GetFollowsFollowing\n     */\n    getFollowsFollowing: {\n      name: \"GetFollowsFollowing\",\n      I: GetFollowsFollowingRequest,\n      O: GetFollowsFollowingResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Sitemaps\n     *\n     * @generated from rpc bsky.Service.GetSitemapIndex\n     */\n    getSitemapIndex: {\n      name: \"GetSitemapIndex\",\n      I: GetSitemapIndexRequest,\n      O: GetSitemapIndexResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.GetSitemapPage\n     */\n    getSitemapPage: {\n      name: \"GetSitemapPage\",\n      I: GetSitemapPageRequest,\n      O: GetSitemapPageResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Ping\n     *\n     * @generated from rpc bsky.Service.Ping\n     */\n    ping: {\n      name: \"Ping\",\n      I: PingRequest,\n      O: PingResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Moderation\n     *\n     * @generated from rpc bsky.Service.TakedownBlob\n     */\n    takedownBlob: {\n      name: \"TakedownBlob\",\n      I: TakedownBlobRequest,\n      O: TakedownBlobResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.TakedownRecord\n     */\n    takedownRecord: {\n      name: \"TakedownRecord\",\n      I: TakedownRecordRequest,\n      O: TakedownRecordResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.TakedownActor\n     */\n    takedownActor: {\n      name: \"TakedownActor\",\n      I: TakedownActorRequest,\n      O: TakedownActorResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.UpdateActorUpstreamStatus\n     */\n    updateActorUpstreamStatus: {\n      name: \"UpdateActorUpstreamStatus\",\n      I: UpdateActorUpstreamStatusRequest,\n      O: UpdateActorUpstreamStatusResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.UntakedownBlob\n     */\n    untakedownBlob: {\n      name: \"UntakedownBlob\",\n      I: UntakedownBlobRequest,\n      O: UntakedownBlobResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.UntakedownRecord\n     */\n    untakedownRecord: {\n      name: \"UntakedownRecord\",\n      I: UntakedownRecordRequest,\n      O: UntakedownRecordResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.UntakedownActor\n     */\n    untakedownActor: {\n      name: \"UntakedownActor\",\n      I: UntakedownActorRequest,\n      O: UntakedownActorResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Ingestion\n     *\n     * @generated from rpc bsky.Service.CreateActorMute\n     */\n    createActorMute: {\n      name: \"CreateActorMute\",\n      I: CreateActorMuteRequest,\n      O: CreateActorMuteResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.DeleteActorMute\n     */\n    deleteActorMute: {\n      name: \"DeleteActorMute\",\n      I: DeleteActorMuteRequest,\n      O: DeleteActorMuteResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.ClearActorMutes\n     */\n    clearActorMutes: {\n      name: \"ClearActorMutes\",\n      I: ClearActorMutesRequest,\n      O: ClearActorMutesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.CreateActorMutelistSubscription\n     */\n    createActorMutelistSubscription: {\n      name: \"CreateActorMutelistSubscription\",\n      I: CreateActorMutelistSubscriptionRequest,\n      O: CreateActorMutelistSubscriptionResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.DeleteActorMutelistSubscription\n     */\n    deleteActorMutelistSubscription: {\n      name: \"DeleteActorMutelistSubscription\",\n      I: DeleteActorMutelistSubscriptionRequest,\n      O: DeleteActorMutelistSubscriptionResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.ClearActorMutelistSubscriptions\n     */\n    clearActorMutelistSubscriptions: {\n      name: \"ClearActorMutelistSubscriptions\",\n      I: ClearActorMutelistSubscriptionsRequest,\n      O: ClearActorMutelistSubscriptionsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.CreateThreadMute\n     */\n    createThreadMute: {\n      name: \"CreateThreadMute\",\n      I: CreateThreadMuteRequest,\n      O: CreateThreadMuteResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.DeleteThreadMute\n     */\n    deleteThreadMute: {\n      name: \"DeleteThreadMute\",\n      I: DeleteThreadMuteRequest,\n      O: DeleteThreadMuteResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsky.Service.ClearThreadMutes\n     */\n    clearThreadMutes: {\n      name: \"ClearThreadMutes\",\n      I: ClearThreadMutesRequest,\n      O: ClearThreadMutesResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "packages/bsky/src/proto/bsky_pb.ts",
    "content": "// @generated by protoc-gen-es v1.6.0 with parameter \"target=ts,import_extension=\"\n// @generated from file bsky.proto (package bsky, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from \"@bufbuild/protobuf\";\nimport { Message, proto3, protoInt64, Timestamp } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from enum bsky.NotificationInclude\n */\nexport enum NotificationInclude {\n  /**\n   * @generated from enum value: NOTIFICATION_INCLUDE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: NOTIFICATION_INCLUDE_ALL = 1;\n   */\n  ALL = 1,\n\n  /**\n   * @generated from enum value: NOTIFICATION_INCLUDE_FOLLOWS = 2;\n   */\n  FOLLOWS = 2,\n}\n// Retrieve enum metadata with: proto3.getEnumType(NotificationInclude)\nproto3.util.setEnumType(NotificationInclude, \"bsky.NotificationInclude\", [\n  { no: 0, name: \"NOTIFICATION_INCLUDE_UNSPECIFIED\" },\n  { no: 1, name: \"NOTIFICATION_INCLUDE_ALL\" },\n  { no: 2, name: \"NOTIFICATION_INCLUDE_FOLLOWS\" },\n]);\n\n/**\n * @generated from enum bsky.ChatNotificationInclude\n */\nexport enum ChatNotificationInclude {\n  /**\n   * @generated from enum value: CHAT_NOTIFICATION_INCLUDE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: CHAT_NOTIFICATION_INCLUDE_ALL = 1;\n   */\n  ALL = 1,\n\n  /**\n   * @generated from enum value: CHAT_NOTIFICATION_INCLUDE_ACCEPTED = 2;\n   */\n  ACCEPTED = 2,\n}\n// Retrieve enum metadata with: proto3.getEnumType(ChatNotificationInclude)\nproto3.util.setEnumType(ChatNotificationInclude, \"bsky.ChatNotificationInclude\", [\n  { no: 0, name: \"CHAT_NOTIFICATION_INCLUDE_UNSPECIFIED\" },\n  { no: 1, name: \"CHAT_NOTIFICATION_INCLUDE_ALL\" },\n  { no: 2, name: \"CHAT_NOTIFICATION_INCLUDE_ACCEPTED\" },\n]);\n\n/**\n * @generated from enum bsky.FeedType\n */\nexport enum FeedType {\n  /**\n   * @generated from enum value: FEED_TYPE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: FEED_TYPE_POSTS_AND_AUTHOR_THREADS = 1;\n   */\n  POSTS_AND_AUTHOR_THREADS = 1,\n\n  /**\n   * @generated from enum value: FEED_TYPE_POSTS_NO_REPLIES = 2;\n   */\n  POSTS_NO_REPLIES = 2,\n\n  /**\n   * @generated from enum value: FEED_TYPE_POSTS_WITH_MEDIA = 3;\n   */\n  POSTS_WITH_MEDIA = 3,\n\n  /**\n   * @generated from enum value: FEED_TYPE_POSTS_WITH_VIDEO = 4;\n   */\n  POSTS_WITH_VIDEO = 4,\n}\n// Retrieve enum metadata with: proto3.getEnumType(FeedType)\nproto3.util.setEnumType(FeedType, \"bsky.FeedType\", [\n  { no: 0, name: \"FEED_TYPE_UNSPECIFIED\" },\n  { no: 1, name: \"FEED_TYPE_POSTS_AND_AUTHOR_THREADS\" },\n  { no: 2, name: \"FEED_TYPE_POSTS_NO_REPLIES\" },\n  { no: 3, name: \"FEED_TYPE_POSTS_WITH_MEDIA\" },\n  { no: 4, name: \"FEED_TYPE_POSTS_WITH_VIDEO\" },\n]);\n\n/**\n * @generated from enum bsky.SitemapPageType\n */\nexport enum SitemapPageType {\n  /**\n   * @generated from enum value: SITEMAP_PAGE_TYPE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: SITEMAP_PAGE_TYPE_USER = 1;\n   */\n  USER = 1,\n}\n// Retrieve enum metadata with: proto3.getEnumType(SitemapPageType)\nproto3.util.setEnumType(SitemapPageType, \"bsky.SitemapPageType\", [\n  { no: 0, name: \"SITEMAP_PAGE_TYPE_UNSPECIFIED\" },\n  { no: 1, name: \"SITEMAP_PAGE_TYPE_USER\" },\n]);\n\n/**\n * @generated from message bsky.Record\n */\nexport class Record extends Message<Record> {\n  /**\n   * @generated from field: bytes record = 1;\n   */\n  record = new Uint8Array(0);\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp indexed_at = 4;\n   */\n  indexedAt?: Timestamp;\n\n  /**\n   * @generated from field: bool taken_down = 5;\n   */\n  takenDown = false;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp created_at = 6;\n   */\n  createdAt?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp sorted_at = 7;\n   */\n  sortedAt?: Timestamp;\n\n  /**\n   * @generated from field: string takedown_ref = 8;\n   */\n  takedownRef = \"\";\n\n  /**\n   * @generated from field: repeated string tags = 9;\n   */\n  tags: string[] = [];\n\n  constructor(data?: PartialMessage<Record>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.Record\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"record\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"indexed_at\", kind: \"message\", T: Timestamp },\n    { no: 5, name: \"taken_down\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 6, name: \"created_at\", kind: \"message\", T: Timestamp },\n    { no: 7, name: \"sorted_at\", kind: \"message\", T: Timestamp },\n    { no: 8, name: \"takedown_ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 9, name: \"tags\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Record {\n    return new Record().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Record {\n    return new Record().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Record {\n    return new Record().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Record | PlainMessage<Record> | undefined, b: Record | PlainMessage<Record> | undefined): boolean {\n    return proto3.util.equals(Record, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlockRecordsRequest\n */\nexport class GetBlockRecordsRequest extends Message<GetBlockRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetBlockRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlockRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlockRecordsRequest {\n    return new GetBlockRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlockRecordsRequest {\n    return new GetBlockRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlockRecordsRequest {\n    return new GetBlockRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlockRecordsRequest | PlainMessage<GetBlockRecordsRequest> | undefined, b: GetBlockRecordsRequest | PlainMessage<GetBlockRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetBlockRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlockRecordsResponse\n */\nexport class GetBlockRecordsResponse extends Message<GetBlockRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetBlockRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlockRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlockRecordsResponse {\n    return new GetBlockRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlockRecordsResponse {\n    return new GetBlockRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlockRecordsResponse {\n    return new GetBlockRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlockRecordsResponse | PlainMessage<GetBlockRecordsResponse> | undefined, b: GetBlockRecordsResponse | PlainMessage<GetBlockRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetBlockRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFeedGeneratorRecordsRequest\n */\nexport class GetFeedGeneratorRecordsRequest extends Message<GetFeedGeneratorRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetFeedGeneratorRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFeedGeneratorRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFeedGeneratorRecordsRequest {\n    return new GetFeedGeneratorRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFeedGeneratorRecordsRequest {\n    return new GetFeedGeneratorRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFeedGeneratorRecordsRequest {\n    return new GetFeedGeneratorRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFeedGeneratorRecordsRequest | PlainMessage<GetFeedGeneratorRecordsRequest> | undefined, b: GetFeedGeneratorRecordsRequest | PlainMessage<GetFeedGeneratorRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetFeedGeneratorRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFeedGeneratorRecordsResponse\n */\nexport class GetFeedGeneratorRecordsResponse extends Message<GetFeedGeneratorRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetFeedGeneratorRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFeedGeneratorRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFeedGeneratorRecordsResponse {\n    return new GetFeedGeneratorRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFeedGeneratorRecordsResponse {\n    return new GetFeedGeneratorRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFeedGeneratorRecordsResponse {\n    return new GetFeedGeneratorRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFeedGeneratorRecordsResponse | PlainMessage<GetFeedGeneratorRecordsResponse> | undefined, b: GetFeedGeneratorRecordsResponse | PlainMessage<GetFeedGeneratorRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetFeedGeneratorRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFollowRecordsRequest\n */\nexport class GetFollowRecordsRequest extends Message<GetFollowRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetFollowRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowRecordsRequest {\n    return new GetFollowRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowRecordsRequest {\n    return new GetFollowRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowRecordsRequest {\n    return new GetFollowRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowRecordsRequest | PlainMessage<GetFollowRecordsRequest> | undefined, b: GetFollowRecordsRequest | PlainMessage<GetFollowRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetFollowRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFollowRecordsResponse\n */\nexport class GetFollowRecordsResponse extends Message<GetFollowRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetFollowRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowRecordsResponse {\n    return new GetFollowRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowRecordsResponse {\n    return new GetFollowRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowRecordsResponse {\n    return new GetFollowRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowRecordsResponse | PlainMessage<GetFollowRecordsResponse> | undefined, b: GetFollowRecordsResponse | PlainMessage<GetFollowRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetFollowRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLikeRecordsRequest\n */\nexport class GetLikeRecordsRequest extends Message<GetLikeRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetLikeRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikeRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikeRecordsRequest {\n    return new GetLikeRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikeRecordsRequest {\n    return new GetLikeRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikeRecordsRequest {\n    return new GetLikeRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikeRecordsRequest | PlainMessage<GetLikeRecordsRequest> | undefined, b: GetLikeRecordsRequest | PlainMessage<GetLikeRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetLikeRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLikeRecordsResponse\n */\nexport class GetLikeRecordsResponse extends Message<GetLikeRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetLikeRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikeRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikeRecordsResponse {\n    return new GetLikeRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikeRecordsResponse {\n    return new GetLikeRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikeRecordsResponse {\n    return new GetLikeRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikeRecordsResponse | PlainMessage<GetLikeRecordsResponse> | undefined, b: GetLikeRecordsResponse | PlainMessage<GetLikeRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetLikeRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListBlockRecordsRequest\n */\nexport class GetListBlockRecordsRequest extends Message<GetListBlockRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetListBlockRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListBlockRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListBlockRecordsRequest {\n    return new GetListBlockRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListBlockRecordsRequest {\n    return new GetListBlockRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListBlockRecordsRequest {\n    return new GetListBlockRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListBlockRecordsRequest | PlainMessage<GetListBlockRecordsRequest> | undefined, b: GetListBlockRecordsRequest | PlainMessage<GetListBlockRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetListBlockRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListBlockRecordsResponse\n */\nexport class GetListBlockRecordsResponse extends Message<GetListBlockRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetListBlockRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListBlockRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListBlockRecordsResponse {\n    return new GetListBlockRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListBlockRecordsResponse {\n    return new GetListBlockRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListBlockRecordsResponse {\n    return new GetListBlockRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListBlockRecordsResponse | PlainMessage<GetListBlockRecordsResponse> | undefined, b: GetListBlockRecordsResponse | PlainMessage<GetListBlockRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetListBlockRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListItemRecordsRequest\n */\nexport class GetListItemRecordsRequest extends Message<GetListItemRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetListItemRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListItemRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListItemRecordsRequest {\n    return new GetListItemRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListItemRecordsRequest {\n    return new GetListItemRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListItemRecordsRequest {\n    return new GetListItemRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListItemRecordsRequest | PlainMessage<GetListItemRecordsRequest> | undefined, b: GetListItemRecordsRequest | PlainMessage<GetListItemRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetListItemRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListItemRecordsResponse\n */\nexport class GetListItemRecordsResponse extends Message<GetListItemRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetListItemRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListItemRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListItemRecordsResponse {\n    return new GetListItemRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListItemRecordsResponse {\n    return new GetListItemRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListItemRecordsResponse {\n    return new GetListItemRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListItemRecordsResponse | PlainMessage<GetListItemRecordsResponse> | undefined, b: GetListItemRecordsResponse | PlainMessage<GetListItemRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetListItemRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListRecordsRequest\n */\nexport class GetListRecordsRequest extends Message<GetListRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetListRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListRecordsRequest {\n    return new GetListRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListRecordsRequest {\n    return new GetListRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListRecordsRequest {\n    return new GetListRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListRecordsRequest | PlainMessage<GetListRecordsRequest> | undefined, b: GetListRecordsRequest | PlainMessage<GetListRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetListRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListRecordsResponse\n */\nexport class GetListRecordsResponse extends Message<GetListRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetListRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListRecordsResponse {\n    return new GetListRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListRecordsResponse {\n    return new GetListRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListRecordsResponse {\n    return new GetListRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListRecordsResponse | PlainMessage<GetListRecordsResponse> | undefined, b: GetListRecordsResponse | PlainMessage<GetListRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetListRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.PostRecordMeta\n */\nexport class PostRecordMeta extends Message<PostRecordMeta> {\n  /**\n   * @generated from field: bool violates_thread_gate = 1;\n   */\n  violatesThreadGate = false;\n\n  /**\n   * @generated from field: bool has_media = 2;\n   */\n  hasMedia = false;\n\n  /**\n   * @generated from field: bool is_reply = 3;\n   */\n  isReply = false;\n\n  /**\n   * @generated from field: bool violates_embedding_rules = 4;\n   */\n  violatesEmbeddingRules = false;\n\n  /**\n   * @generated from field: bool has_post_gate = 5;\n   */\n  hasPostGate = false;\n\n  /**\n   * @generated from field: bool has_thread_gate = 6;\n   */\n  hasThreadGate = false;\n\n  /**\n   * @generated from field: bool has_video = 7;\n   */\n  hasVideo = false;\n\n  constructor(data?: PartialMessage<PostRecordMeta>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.PostRecordMeta\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"violates_thread_gate\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 2, name: \"has_media\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 3, name: \"is_reply\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 4, name: \"violates_embedding_rules\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 5, name: \"has_post_gate\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 6, name: \"has_thread_gate\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 7, name: \"has_video\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PostRecordMeta {\n    return new PostRecordMeta().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PostRecordMeta {\n    return new PostRecordMeta().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PostRecordMeta {\n    return new PostRecordMeta().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PostRecordMeta | PlainMessage<PostRecordMeta> | undefined, b: PostRecordMeta | PlainMessage<PostRecordMeta> | undefined): boolean {\n    return proto3.util.equals(PostRecordMeta, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetPostRecordsRequest\n */\nexport class GetPostRecordsRequest extends Message<GetPostRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: optional string process_dynamic_tags_for_view = 2;\n   */\n  processDynamicTagsForView?: string;\n\n  /**\n   * @generated from field: optional string viewer_did = 3;\n   */\n  viewerDid?: string;\n\n  constructor(data?: PartialMessage<GetPostRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetPostRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"process_dynamic_tags_for_view\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, opt: true },\n    { no: 3, name: \"viewer_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, opt: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPostRecordsRequest {\n    return new GetPostRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPostRecordsRequest {\n    return new GetPostRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPostRecordsRequest {\n    return new GetPostRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetPostRecordsRequest | PlainMessage<GetPostRecordsRequest> | undefined, b: GetPostRecordsRequest | PlainMessage<GetPostRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetPostRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetPostRecordsResponse\n */\nexport class GetPostRecordsResponse extends Message<GetPostRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  /**\n   * @generated from field: repeated bsky.PostRecordMeta meta = 2;\n   */\n  meta: PostRecordMeta[] = [];\n\n  constructor(data?: PartialMessage<GetPostRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetPostRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n    { no: 2, name: \"meta\", kind: \"message\", T: PostRecordMeta, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPostRecordsResponse {\n    return new GetPostRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPostRecordsResponse {\n    return new GetPostRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPostRecordsResponse {\n    return new GetPostRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetPostRecordsResponse | PlainMessage<GetPostRecordsResponse> | undefined, b: GetPostRecordsResponse | PlainMessage<GetPostRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetPostRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetProfileRecordsRequest\n */\nexport class GetProfileRecordsRequest extends Message<GetProfileRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetProfileRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetProfileRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetProfileRecordsRequest {\n    return new GetProfileRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetProfileRecordsRequest {\n    return new GetProfileRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetProfileRecordsRequest {\n    return new GetProfileRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetProfileRecordsRequest | PlainMessage<GetProfileRecordsRequest> | undefined, b: GetProfileRecordsRequest | PlainMessage<GetProfileRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetProfileRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetProfileRecordsResponse\n */\nexport class GetProfileRecordsResponse extends Message<GetProfileRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetProfileRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetProfileRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetProfileRecordsResponse {\n    return new GetProfileRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetProfileRecordsResponse {\n    return new GetProfileRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetProfileRecordsResponse {\n    return new GetProfileRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetProfileRecordsResponse | PlainMessage<GetProfileRecordsResponse> | undefined, b: GetProfileRecordsResponse | PlainMessage<GetProfileRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetProfileRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorChatDeclarationRecordsRequest\n */\nexport class GetActorChatDeclarationRecordsRequest extends Message<GetActorChatDeclarationRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetActorChatDeclarationRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorChatDeclarationRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorChatDeclarationRecordsRequest {\n    return new GetActorChatDeclarationRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorChatDeclarationRecordsRequest {\n    return new GetActorChatDeclarationRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorChatDeclarationRecordsRequest {\n    return new GetActorChatDeclarationRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorChatDeclarationRecordsRequest | PlainMessage<GetActorChatDeclarationRecordsRequest> | undefined, b: GetActorChatDeclarationRecordsRequest | PlainMessage<GetActorChatDeclarationRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorChatDeclarationRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorChatDeclarationRecordsResponse\n */\nexport class GetActorChatDeclarationRecordsResponse extends Message<GetActorChatDeclarationRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetActorChatDeclarationRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorChatDeclarationRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorChatDeclarationRecordsResponse {\n    return new GetActorChatDeclarationRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorChatDeclarationRecordsResponse {\n    return new GetActorChatDeclarationRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorChatDeclarationRecordsResponse {\n    return new GetActorChatDeclarationRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorChatDeclarationRecordsResponse | PlainMessage<GetActorChatDeclarationRecordsResponse> | undefined, b: GetActorChatDeclarationRecordsResponse | PlainMessage<GetActorChatDeclarationRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorChatDeclarationRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNotificationDeclarationRecordsRequest\n */\nexport class GetNotificationDeclarationRecordsRequest extends Message<GetNotificationDeclarationRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetNotificationDeclarationRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationDeclarationRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationDeclarationRecordsRequest {\n    return new GetNotificationDeclarationRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationDeclarationRecordsRequest {\n    return new GetNotificationDeclarationRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationDeclarationRecordsRequest {\n    return new GetNotificationDeclarationRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationDeclarationRecordsRequest | PlainMessage<GetNotificationDeclarationRecordsRequest> | undefined, b: GetNotificationDeclarationRecordsRequest | PlainMessage<GetNotificationDeclarationRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetNotificationDeclarationRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNotificationDeclarationRecordsResponse\n */\nexport class GetNotificationDeclarationRecordsResponse extends Message<GetNotificationDeclarationRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetNotificationDeclarationRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationDeclarationRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationDeclarationRecordsResponse {\n    return new GetNotificationDeclarationRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationDeclarationRecordsResponse {\n    return new GetNotificationDeclarationRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationDeclarationRecordsResponse {\n    return new GetNotificationDeclarationRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationDeclarationRecordsResponse | PlainMessage<GetNotificationDeclarationRecordsResponse> | undefined, b: GetNotificationDeclarationRecordsResponse | PlainMessage<GetNotificationDeclarationRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetNotificationDeclarationRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetGermDeclarationRecordsRequest\n */\nexport class GetGermDeclarationRecordsRequest extends Message<GetGermDeclarationRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetGermDeclarationRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetGermDeclarationRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetGermDeclarationRecordsRequest {\n    return new GetGermDeclarationRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetGermDeclarationRecordsRequest {\n    return new GetGermDeclarationRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetGermDeclarationRecordsRequest {\n    return new GetGermDeclarationRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetGermDeclarationRecordsRequest | PlainMessage<GetGermDeclarationRecordsRequest> | undefined, b: GetGermDeclarationRecordsRequest | PlainMessage<GetGermDeclarationRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetGermDeclarationRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetGermDeclarationRecordsResponse\n */\nexport class GetGermDeclarationRecordsResponse extends Message<GetGermDeclarationRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetGermDeclarationRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetGermDeclarationRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetGermDeclarationRecordsResponse {\n    return new GetGermDeclarationRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetGermDeclarationRecordsResponse {\n    return new GetGermDeclarationRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetGermDeclarationRecordsResponse {\n    return new GetGermDeclarationRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetGermDeclarationRecordsResponse | PlainMessage<GetGermDeclarationRecordsResponse> | undefined, b: GetGermDeclarationRecordsResponse | PlainMessage<GetGermDeclarationRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetGermDeclarationRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetStatusRecordsRequest\n */\nexport class GetStatusRecordsRequest extends Message<GetStatusRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetStatusRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetStatusRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetStatusRecordsRequest {\n    return new GetStatusRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetStatusRecordsRequest {\n    return new GetStatusRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetStatusRecordsRequest {\n    return new GetStatusRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetStatusRecordsRequest | PlainMessage<GetStatusRecordsRequest> | undefined, b: GetStatusRecordsRequest | PlainMessage<GetStatusRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetStatusRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetStatusRecordsResponse\n */\nexport class GetStatusRecordsResponse extends Message<GetStatusRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetStatusRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetStatusRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetStatusRecordsResponse {\n    return new GetStatusRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetStatusRecordsResponse {\n    return new GetStatusRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetStatusRecordsResponse {\n    return new GetStatusRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetStatusRecordsResponse | PlainMessage<GetStatusRecordsResponse> | undefined, b: GetStatusRecordsResponse | PlainMessage<GetStatusRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetStatusRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRepostRecordsRequest\n */\nexport class GetRepostRecordsRequest extends Message<GetRepostRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetRepostRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRepostRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRepostRecordsRequest {\n    return new GetRepostRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRepostRecordsRequest {\n    return new GetRepostRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRepostRecordsRequest {\n    return new GetRepostRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRepostRecordsRequest | PlainMessage<GetRepostRecordsRequest> | undefined, b: GetRepostRecordsRequest | PlainMessage<GetRepostRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetRepostRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRepostRecordsResponse\n */\nexport class GetRepostRecordsResponse extends Message<GetRepostRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetRepostRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRepostRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRepostRecordsResponse {\n    return new GetRepostRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRepostRecordsResponse {\n    return new GetRepostRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRepostRecordsResponse {\n    return new GetRepostRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRepostRecordsResponse | PlainMessage<GetRepostRecordsResponse> | undefined, b: GetRepostRecordsResponse | PlainMessage<GetRepostRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetRepostRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetThreadGateRecordsRequest\n */\nexport class GetThreadGateRecordsRequest extends Message<GetThreadGateRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetThreadGateRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetThreadGateRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetThreadGateRecordsRequest {\n    return new GetThreadGateRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetThreadGateRecordsRequest {\n    return new GetThreadGateRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetThreadGateRecordsRequest {\n    return new GetThreadGateRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetThreadGateRecordsRequest | PlainMessage<GetThreadGateRecordsRequest> | undefined, b: GetThreadGateRecordsRequest | PlainMessage<GetThreadGateRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetThreadGateRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetThreadGateRecordsResponse\n */\nexport class GetThreadGateRecordsResponse extends Message<GetThreadGateRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetThreadGateRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetThreadGateRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetThreadGateRecordsResponse {\n    return new GetThreadGateRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetThreadGateRecordsResponse {\n    return new GetThreadGateRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetThreadGateRecordsResponse {\n    return new GetThreadGateRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetThreadGateRecordsResponse | PlainMessage<GetThreadGateRecordsResponse> | undefined, b: GetThreadGateRecordsResponse | PlainMessage<GetThreadGateRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetThreadGateRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetPostgateRecordsRequest\n */\nexport class GetPostgateRecordsRequest extends Message<GetPostgateRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetPostgateRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetPostgateRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPostgateRecordsRequest {\n    return new GetPostgateRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPostgateRecordsRequest {\n    return new GetPostgateRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPostgateRecordsRequest {\n    return new GetPostgateRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetPostgateRecordsRequest | PlainMessage<GetPostgateRecordsRequest> | undefined, b: GetPostgateRecordsRequest | PlainMessage<GetPostgateRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetPostgateRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetPostgateRecordsResponse\n */\nexport class GetPostgateRecordsResponse extends Message<GetPostgateRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetPostgateRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetPostgateRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPostgateRecordsResponse {\n    return new GetPostgateRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPostgateRecordsResponse {\n    return new GetPostgateRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPostgateRecordsResponse {\n    return new GetPostgateRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetPostgateRecordsResponse | PlainMessage<GetPostgateRecordsResponse> | undefined, b: GetPostgateRecordsResponse | PlainMessage<GetPostgateRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetPostgateRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLabelerRecordsRequest\n */\nexport class GetLabelerRecordsRequest extends Message<GetLabelerRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetLabelerRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLabelerRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLabelerRecordsRequest {\n    return new GetLabelerRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLabelerRecordsRequest {\n    return new GetLabelerRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLabelerRecordsRequest {\n    return new GetLabelerRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLabelerRecordsRequest | PlainMessage<GetLabelerRecordsRequest> | undefined, b: GetLabelerRecordsRequest | PlainMessage<GetLabelerRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetLabelerRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLabelerRecordsResponse\n */\nexport class GetLabelerRecordsResponse extends Message<GetLabelerRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetLabelerRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLabelerRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLabelerRecordsResponse {\n    return new GetLabelerRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLabelerRecordsResponse {\n    return new GetLabelerRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLabelerRecordsResponse {\n    return new GetLabelerRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLabelerRecordsResponse | PlainMessage<GetLabelerRecordsResponse> | undefined, b: GetLabelerRecordsResponse | PlainMessage<GetLabelerRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetLabelerRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetAllLabelersRequest\n */\nexport class GetAllLabelersRequest extends Message<GetAllLabelersRequest> {\n  constructor(data?: PartialMessage<GetAllLabelersRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetAllLabelersRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetAllLabelersRequest {\n    return new GetAllLabelersRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetAllLabelersRequest {\n    return new GetAllLabelersRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetAllLabelersRequest {\n    return new GetAllLabelersRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetAllLabelersRequest | PlainMessage<GetAllLabelersRequest> | undefined, b: GetAllLabelersRequest | PlainMessage<GetAllLabelersRequest> | undefined): boolean {\n    return proto3.util.equals(GetAllLabelersRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetAllLabelersResponse\n */\nexport class GetAllLabelersResponse extends Message<GetAllLabelersResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: repeated bsky.Record records = 2;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetAllLabelersResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetAllLabelersResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetAllLabelersResponse {\n    return new GetAllLabelersResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetAllLabelersResponse {\n    return new GetAllLabelersResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetAllLabelersResponse {\n    return new GetAllLabelersResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetAllLabelersResponse | PlainMessage<GetAllLabelersResponse> | undefined, b: GetAllLabelersResponse | PlainMessage<GetAllLabelersResponse> | undefined): boolean {\n    return proto3.util.equals(GetAllLabelersResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetStarterPackRecordsRequest\n */\nexport class GetStarterPackRecordsRequest extends Message<GetStarterPackRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetStarterPackRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetStarterPackRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetStarterPackRecordsRequest {\n    return new GetStarterPackRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetStarterPackRecordsRequest {\n    return new GetStarterPackRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetStarterPackRecordsRequest {\n    return new GetStarterPackRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetStarterPackRecordsRequest | PlainMessage<GetStarterPackRecordsRequest> | undefined, b: GetStarterPackRecordsRequest | PlainMessage<GetStarterPackRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetStarterPackRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetStarterPackRecordsResponse\n */\nexport class GetStarterPackRecordsResponse extends Message<GetStarterPackRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetStarterPackRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetStarterPackRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetStarterPackRecordsResponse {\n    return new GetStarterPackRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetStarterPackRecordsResponse {\n    return new GetStarterPackRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetStarterPackRecordsResponse {\n    return new GetStarterPackRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetStarterPackRecordsResponse | PlainMessage<GetStarterPackRecordsResponse> | undefined, b: GetStarterPackRecordsResponse | PlainMessage<GetStarterPackRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetStarterPackRecordsResponse, a, b);\n  }\n}\n\n/**\n * - Return follow uris where user A follows users B, C, D, …\n *     - E.g. for viewer state on `getProfiles`\n *\n * @generated from message bsky.GetActorFollowsActorsRequest\n */\nexport class GetActorFollowsActorsRequest extends Message<GetActorFollowsActorsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string target_dids = 2;\n   */\n  targetDids: string[] = [];\n\n  constructor(data?: PartialMessage<GetActorFollowsActorsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorFollowsActorsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorFollowsActorsRequest {\n    return new GetActorFollowsActorsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorFollowsActorsRequest {\n    return new GetActorFollowsActorsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorFollowsActorsRequest {\n    return new GetActorFollowsActorsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorFollowsActorsRequest | PlainMessage<GetActorFollowsActorsRequest> | undefined, b: GetActorFollowsActorsRequest | PlainMessage<GetActorFollowsActorsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorFollowsActorsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorFollowsActorsResponse\n */\nexport class GetActorFollowsActorsResponse extends Message<GetActorFollowsActorsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetActorFollowsActorsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorFollowsActorsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorFollowsActorsResponse {\n    return new GetActorFollowsActorsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorFollowsActorsResponse {\n    return new GetActorFollowsActorsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorFollowsActorsResponse {\n    return new GetActorFollowsActorsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorFollowsActorsResponse | PlainMessage<GetActorFollowsActorsResponse> | undefined, b: GetActorFollowsActorsResponse | PlainMessage<GetActorFollowsActorsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorFollowsActorsResponse, a, b);\n  }\n}\n\n/**\n * - Return follow uris of users who follows user A\n *     - For `getFollowers` list\n *\n * @generated from message bsky.GetFollowersRequest\n */\nexport class GetFollowersRequest extends Message<GetFollowersRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetFollowersRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowersRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowersRequest {\n    return new GetFollowersRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowersRequest {\n    return new GetFollowersRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowersRequest {\n    return new GetFollowersRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowersRequest | PlainMessage<GetFollowersRequest> | undefined, b: GetFollowersRequest | PlainMessage<GetFollowersRequest> | undefined): boolean {\n    return proto3.util.equals(GetFollowersRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.FollowInfo\n */\nexport class FollowInfo extends Message<FollowInfo> {\n  /**\n   * @generated from field: string uri = 1;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject_did = 3;\n   */\n  subjectDid = \"\";\n\n  constructor(data?: PartialMessage<FollowInfo>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.FollowInfo\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): FollowInfo {\n    return new FollowInfo().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): FollowInfo {\n    return new FollowInfo().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): FollowInfo {\n    return new FollowInfo().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: FollowInfo | PlainMessage<FollowInfo> | undefined, b: FollowInfo | PlainMessage<FollowInfo> | undefined): boolean {\n    return proto3.util.equals(FollowInfo, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFollowersResponse\n */\nexport class GetFollowersResponse extends Message<GetFollowersResponse> {\n  /**\n   * @generated from field: repeated bsky.FollowInfo followers = 1;\n   */\n  followers: FollowInfo[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetFollowersResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowersResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"followers\", kind: \"message\", T: FollowInfo, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowersResponse {\n    return new GetFollowersResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowersResponse {\n    return new GetFollowersResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowersResponse {\n    return new GetFollowersResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowersResponse | PlainMessage<GetFollowersResponse> | undefined, b: GetFollowersResponse | PlainMessage<GetFollowersResponse> | undefined): boolean {\n    return proto3.util.equals(GetFollowersResponse, a, b);\n  }\n}\n\n/**\n * - Return follow uris of users A follows\n *     - For `getFollows` list\n *\n * @generated from message bsky.GetFollowsRequest\n */\nexport class GetFollowsRequest extends Message<GetFollowsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetFollowsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowsRequest {\n    return new GetFollowsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowsRequest {\n    return new GetFollowsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowsRequest {\n    return new GetFollowsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowsRequest | PlainMessage<GetFollowsRequest> | undefined, b: GetFollowsRequest | PlainMessage<GetFollowsRequest> | undefined): boolean {\n    return proto3.util.equals(GetFollowsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFollowsResponse\n */\nexport class GetFollowsResponse extends Message<GetFollowsResponse> {\n  /**\n   * @generated from field: repeated bsky.FollowInfo follows = 1;\n   */\n  follows: FollowInfo[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetFollowsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"follows\", kind: \"message\", T: FollowInfo, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowsResponse {\n    return new GetFollowsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowsResponse {\n    return new GetFollowsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowsResponse {\n    return new GetFollowsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowsResponse | PlainMessage<GetFollowsResponse> | undefined, b: GetFollowsResponse | PlainMessage<GetFollowsResponse> | undefined): boolean {\n    return proto3.util.equals(GetFollowsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.VerificationMeta\n */\nexport class VerificationMeta extends Message<VerificationMeta> {\n  /**\n   * @generated from field: string rkey = 1;\n   */\n  rkey = \"\";\n\n  /**\n   * @generated from field: string handle = 2;\n   */\n  handle = \"\";\n\n  /**\n   * @generated from field: string display_name = 3;\n   */\n  displayName = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp sorted_at = 4;\n   */\n  sortedAt?: Timestamp;\n\n  constructor(data?: PartialMessage<VerificationMeta>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.VerificationMeta\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"rkey\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"handle\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"display_name\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"sorted_at\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VerificationMeta {\n    return new VerificationMeta().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VerificationMeta {\n    return new VerificationMeta().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VerificationMeta {\n    return new VerificationMeta().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: VerificationMeta | PlainMessage<VerificationMeta> | undefined, b: VerificationMeta | PlainMessage<VerificationMeta> | undefined): boolean {\n    return proto3.util.equals(VerificationMeta, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetVerificationRecordsRequest\n */\nexport class GetVerificationRecordsRequest extends Message<GetVerificationRecordsRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetVerificationRecordsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetVerificationRecordsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVerificationRecordsRequest {\n    return new GetVerificationRecordsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVerificationRecordsRequest {\n    return new GetVerificationRecordsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVerificationRecordsRequest {\n    return new GetVerificationRecordsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetVerificationRecordsRequest | PlainMessage<GetVerificationRecordsRequest> | undefined, b: GetVerificationRecordsRequest | PlainMessage<GetVerificationRecordsRequest> | undefined): boolean {\n    return proto3.util.equals(GetVerificationRecordsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetVerificationRecordsResponse\n */\nexport class GetVerificationRecordsResponse extends Message<GetVerificationRecordsResponse> {\n  /**\n   * @generated from field: repeated bsky.Record records = 1;\n   */\n  records: Record[] = [];\n\n  constructor(data?: PartialMessage<GetVerificationRecordsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetVerificationRecordsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"records\", kind: \"message\", T: Record, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVerificationRecordsResponse {\n    return new GetVerificationRecordsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVerificationRecordsResponse {\n    return new GetVerificationRecordsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVerificationRecordsResponse {\n    return new GetVerificationRecordsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetVerificationRecordsResponse | PlainMessage<GetVerificationRecordsResponse> | undefined, b: GetVerificationRecordsResponse | PlainMessage<GetVerificationRecordsResponse> | undefined): boolean {\n    return proto3.util.equals(GetVerificationRecordsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.VerificationIssued\n */\nexport class VerificationIssued extends Message<VerificationIssued> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string rkey = 2;\n   */\n  rkey = \"\";\n\n  /**\n   * @generated from field: string subject_did = 3;\n   */\n  subjectDid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp created_at = 7;\n   */\n  createdAt?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp indexed_at = 8;\n   */\n  indexedAt?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp sorted_at = 9;\n   */\n  sortedAt?: Timestamp;\n\n  constructor(data?: PartialMessage<VerificationIssued>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.VerificationIssued\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"rkey\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 7, name: \"created_at\", kind: \"message\", T: Timestamp },\n    { no: 8, name: \"indexed_at\", kind: \"message\", T: Timestamp },\n    { no: 9, name: \"sorted_at\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VerificationIssued {\n    return new VerificationIssued().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VerificationIssued {\n    return new VerificationIssued().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VerificationIssued {\n    return new VerificationIssued().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: VerificationIssued | PlainMessage<VerificationIssued> | undefined, b: VerificationIssued | PlainMessage<VerificationIssued> | undefined): boolean {\n    return proto3.util.equals(VerificationIssued, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetVerificationsIssuedRequest\n */\nexport class GetVerificationsIssuedRequest extends Message<GetVerificationsIssuedRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetVerificationsIssuedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetVerificationsIssuedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVerificationsIssuedRequest {\n    return new GetVerificationsIssuedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVerificationsIssuedRequest {\n    return new GetVerificationsIssuedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVerificationsIssuedRequest {\n    return new GetVerificationsIssuedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetVerificationsIssuedRequest | PlainMessage<GetVerificationsIssuedRequest> | undefined, b: GetVerificationsIssuedRequest | PlainMessage<GetVerificationsIssuedRequest> | undefined): boolean {\n    return proto3.util.equals(GetVerificationsIssuedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetVerificationsIssuedResponse\n */\nexport class GetVerificationsIssuedResponse extends Message<GetVerificationsIssuedResponse> {\n  /**\n   * @generated from field: repeated bsky.VerificationIssued verifications = 1;\n   */\n  verifications: VerificationIssued[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetVerificationsIssuedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetVerificationsIssuedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"verifications\", kind: \"message\", T: VerificationIssued, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVerificationsIssuedResponse {\n    return new GetVerificationsIssuedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVerificationsIssuedResponse {\n    return new GetVerificationsIssuedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVerificationsIssuedResponse {\n    return new GetVerificationsIssuedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetVerificationsIssuedResponse | PlainMessage<GetVerificationsIssuedResponse> | undefined, b: GetVerificationsIssuedResponse | PlainMessage<GetVerificationsIssuedResponse> | undefined): boolean {\n    return proto3.util.equals(GetVerificationsIssuedResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.VerificationReceived\n */\nexport class VerificationReceived extends Message<VerificationReceived> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string rkey = 2;\n   */\n  rkey = \"\";\n\n  /**\n   * @generated from field: string subject_did = 3;\n   */\n  subjectDid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp created_at = 7;\n   */\n  createdAt?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp indexed_at = 8;\n   */\n  indexedAt?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp sorted_at = 9;\n   */\n  sortedAt?: Timestamp;\n\n  constructor(data?: PartialMessage<VerificationReceived>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.VerificationReceived\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"rkey\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 7, name: \"created_at\", kind: \"message\", T: Timestamp },\n    { no: 8, name: \"indexed_at\", kind: \"message\", T: Timestamp },\n    { no: 9, name: \"sorted_at\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VerificationReceived {\n    return new VerificationReceived().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VerificationReceived {\n    return new VerificationReceived().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VerificationReceived {\n    return new VerificationReceived().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: VerificationReceived | PlainMessage<VerificationReceived> | undefined, b: VerificationReceived | PlainMessage<VerificationReceived> | undefined): boolean {\n    return proto3.util.equals(VerificationReceived, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetVerificationsReceivedRequest\n */\nexport class GetVerificationsReceivedRequest extends Message<GetVerificationsReceivedRequest> {\n  /**\n   * @generated from field: string subject_did = 1;\n   */\n  subjectDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetVerificationsReceivedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetVerificationsReceivedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVerificationsReceivedRequest {\n    return new GetVerificationsReceivedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVerificationsReceivedRequest {\n    return new GetVerificationsReceivedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVerificationsReceivedRequest {\n    return new GetVerificationsReceivedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetVerificationsReceivedRequest | PlainMessage<GetVerificationsReceivedRequest> | undefined, b: GetVerificationsReceivedRequest | PlainMessage<GetVerificationsReceivedRequest> | undefined): boolean {\n    return proto3.util.equals(GetVerificationsReceivedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetVerificationsReceivedResponse\n */\nexport class GetVerificationsReceivedResponse extends Message<GetVerificationsReceivedResponse> {\n  /**\n   * @generated from field: repeated bsky.VerificationReceived verifications = 1;\n   */\n  verifications: VerificationReceived[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetVerificationsReceivedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetVerificationsReceivedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"verifications\", kind: \"message\", T: VerificationReceived, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetVerificationsReceivedResponse {\n    return new GetVerificationsReceivedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetVerificationsReceivedResponse {\n    return new GetVerificationsReceivedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetVerificationsReceivedResponse {\n    return new GetVerificationsReceivedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetVerificationsReceivedResponse | PlainMessage<GetVerificationsReceivedResponse> | undefined, b: GetVerificationsReceivedResponse | PlainMessage<GetVerificationsReceivedResponse> | undefined): boolean {\n    return proto3.util.equals(GetVerificationsReceivedResponse, a, b);\n  }\n}\n\n/**\n * - return like uris where subject uri is subject A\n *     - `getLikes` list for a post\n *\n * @generated from message bsky.GetLikesBySubjectRequest\n */\nexport class GetLikesBySubjectRequest extends Message<GetLikesBySubjectRequest> {\n  /**\n   * @generated from field: bsky.RecordRef subject = 1;\n   */\n  subject?: RecordRef;\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetLikesBySubjectRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikesBySubjectRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subject\", kind: \"message\", T: RecordRef },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikesBySubjectRequest {\n    return new GetLikesBySubjectRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikesBySubjectRequest {\n    return new GetLikesBySubjectRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikesBySubjectRequest {\n    return new GetLikesBySubjectRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikesBySubjectRequest | PlainMessage<GetLikesBySubjectRequest> | undefined, b: GetLikesBySubjectRequest | PlainMessage<GetLikesBySubjectRequest> | undefined): boolean {\n    return proto3.util.equals(GetLikesBySubjectRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLikesBySubjectResponse\n */\nexport class GetLikesBySubjectResponse extends Message<GetLikesBySubjectResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetLikesBySubjectResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikesBySubjectResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikesBySubjectResponse {\n    return new GetLikesBySubjectResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikesBySubjectResponse {\n    return new GetLikesBySubjectResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikesBySubjectResponse {\n    return new GetLikesBySubjectResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikesBySubjectResponse | PlainMessage<GetLikesBySubjectResponse> | undefined, b: GetLikesBySubjectResponse | PlainMessage<GetLikesBySubjectResponse> | undefined): boolean {\n    return proto3.util.equals(GetLikesBySubjectResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLikesBySubjectSortedRequest\n */\nexport class GetLikesBySubjectSortedRequest extends Message<GetLikesBySubjectSortedRequest> {\n  /**\n   * @generated from field: bsky.RecordRef subject = 1;\n   */\n  subject?: RecordRef;\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetLikesBySubjectSortedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikesBySubjectSortedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subject\", kind: \"message\", T: RecordRef },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikesBySubjectSortedRequest {\n    return new GetLikesBySubjectSortedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikesBySubjectSortedRequest {\n    return new GetLikesBySubjectSortedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikesBySubjectSortedRequest {\n    return new GetLikesBySubjectSortedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikesBySubjectSortedRequest | PlainMessage<GetLikesBySubjectSortedRequest> | undefined, b: GetLikesBySubjectSortedRequest | PlainMessage<GetLikesBySubjectSortedRequest> | undefined): boolean {\n    return proto3.util.equals(GetLikesBySubjectSortedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLikesBySubjectSortedResponse\n */\nexport class GetLikesBySubjectSortedResponse extends Message<GetLikesBySubjectSortedResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetLikesBySubjectSortedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikesBySubjectSortedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikesBySubjectSortedResponse {\n    return new GetLikesBySubjectSortedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikesBySubjectSortedResponse {\n    return new GetLikesBySubjectSortedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikesBySubjectSortedResponse {\n    return new GetLikesBySubjectSortedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikesBySubjectSortedResponse | PlainMessage<GetLikesBySubjectSortedResponse> | undefined, b: GetLikesBySubjectSortedResponse | PlainMessage<GetLikesBySubjectSortedResponse> | undefined): boolean {\n    return proto3.util.equals(GetLikesBySubjectSortedResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetQuotesBySubjectSortedRequest\n */\nexport class GetQuotesBySubjectSortedRequest extends Message<GetQuotesBySubjectSortedRequest> {\n  /**\n   * @generated from field: bsky.RecordRef subject = 1;\n   */\n  subject?: RecordRef;\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetQuotesBySubjectSortedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetQuotesBySubjectSortedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subject\", kind: \"message\", T: RecordRef },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetQuotesBySubjectSortedRequest {\n    return new GetQuotesBySubjectSortedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetQuotesBySubjectSortedRequest {\n    return new GetQuotesBySubjectSortedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetQuotesBySubjectSortedRequest {\n    return new GetQuotesBySubjectSortedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetQuotesBySubjectSortedRequest | PlainMessage<GetQuotesBySubjectSortedRequest> | undefined, b: GetQuotesBySubjectSortedRequest | PlainMessage<GetQuotesBySubjectSortedRequest> | undefined): boolean {\n    return proto3.util.equals(GetQuotesBySubjectSortedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetQuotesBySubjectSortedResponse\n */\nexport class GetQuotesBySubjectSortedResponse extends Message<GetQuotesBySubjectSortedResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetQuotesBySubjectSortedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetQuotesBySubjectSortedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetQuotesBySubjectSortedResponse {\n    return new GetQuotesBySubjectSortedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetQuotesBySubjectSortedResponse {\n    return new GetQuotesBySubjectSortedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetQuotesBySubjectSortedResponse {\n    return new GetQuotesBySubjectSortedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetQuotesBySubjectSortedResponse | PlainMessage<GetQuotesBySubjectSortedResponse> | undefined, b: GetQuotesBySubjectSortedResponse | PlainMessage<GetQuotesBySubjectSortedResponse> | undefined): boolean {\n    return proto3.util.equals(GetQuotesBySubjectSortedResponse, a, b);\n  }\n}\n\n/**\n * - return like uris for user A on subject B, C, D...\n *     - viewer state on posts\n *\n * @generated from message bsky.GetLikesByActorAndSubjectsRequest\n */\nexport class GetLikesByActorAndSubjectsRequest extends Message<GetLikesByActorAndSubjectsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated bsky.RecordRef refs = 2;\n   */\n  refs: RecordRef[] = [];\n\n  constructor(data?: PartialMessage<GetLikesByActorAndSubjectsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikesByActorAndSubjectsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"refs\", kind: \"message\", T: RecordRef, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikesByActorAndSubjectsRequest {\n    return new GetLikesByActorAndSubjectsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikesByActorAndSubjectsRequest {\n    return new GetLikesByActorAndSubjectsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikesByActorAndSubjectsRequest {\n    return new GetLikesByActorAndSubjectsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikesByActorAndSubjectsRequest | PlainMessage<GetLikesByActorAndSubjectsRequest> | undefined, b: GetLikesByActorAndSubjectsRequest | PlainMessage<GetLikesByActorAndSubjectsRequest> | undefined): boolean {\n    return proto3.util.equals(GetLikesByActorAndSubjectsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLikesByActorAndSubjectsResponse\n */\nexport class GetLikesByActorAndSubjectsResponse extends Message<GetLikesByActorAndSubjectsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetLikesByActorAndSubjectsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLikesByActorAndSubjectsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLikesByActorAndSubjectsResponse {\n    return new GetLikesByActorAndSubjectsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLikesByActorAndSubjectsResponse {\n    return new GetLikesByActorAndSubjectsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLikesByActorAndSubjectsResponse {\n    return new GetLikesByActorAndSubjectsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLikesByActorAndSubjectsResponse | PlainMessage<GetLikesByActorAndSubjectsResponse> | undefined, b: GetLikesByActorAndSubjectsResponse | PlainMessage<GetLikesByActorAndSubjectsResponse> | undefined): boolean {\n    return proto3.util.equals(GetLikesByActorAndSubjectsResponse, a, b);\n  }\n}\n\n/**\n * - return recent like uris for user A\n *     - `getActorLikes` list for a user\n *\n * @generated from message bsky.GetActorLikesRequest\n */\nexport class GetActorLikesRequest extends Message<GetActorLikesRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorLikesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorLikesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorLikesRequest {\n    return new GetActorLikesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorLikesRequest {\n    return new GetActorLikesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorLikesRequest {\n    return new GetActorLikesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorLikesRequest | PlainMessage<GetActorLikesRequest> | undefined, b: GetActorLikesRequest | PlainMessage<GetActorLikesRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorLikesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.LikeInfo\n */\nexport class LikeInfo extends Message<LikeInfo> {\n  /**\n   * @generated from field: string uri = 1;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string subject = 2;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<LikeInfo>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.LikeInfo\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): LikeInfo {\n    return new LikeInfo().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): LikeInfo {\n    return new LikeInfo().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): LikeInfo {\n    return new LikeInfo().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: LikeInfo | PlainMessage<LikeInfo> | undefined, b: LikeInfo | PlainMessage<LikeInfo> | undefined): boolean {\n    return proto3.util.equals(LikeInfo, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorLikesResponse\n */\nexport class GetActorLikesResponse extends Message<GetActorLikesResponse> {\n  /**\n   * @generated from field: repeated bsky.LikeInfo likes = 1;\n   */\n  likes: LikeInfo[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorLikesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorLikesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"likes\", kind: \"message\", T: LikeInfo, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorLikesResponse {\n    return new GetActorLikesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorLikesResponse {\n    return new GetActorLikesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorLikesResponse {\n    return new GetActorLikesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorLikesResponse | PlainMessage<GetActorLikesResponse> | undefined, b: GetActorLikesResponse | PlainMessage<GetActorLikesResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorLikesResponse, a, b);\n  }\n}\n\n/**\n *\n * Interactions\n *\n *\n * @generated from message bsky.GetInteractionCountsRequest\n */\nexport class GetInteractionCountsRequest extends Message<GetInteractionCountsRequest> {\n  /**\n   * @generated from field: repeated bsky.RecordRef refs = 1;\n   */\n  refs: RecordRef[] = [];\n\n  /**\n   * @generated from field: repeated string skip_cache_for_dids = 2;\n   */\n  skipCacheForDids: string[] = [];\n\n  constructor(data?: PartialMessage<GetInteractionCountsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetInteractionCountsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"refs\", kind: \"message\", T: RecordRef, repeated: true },\n    { no: 2, name: \"skip_cache_for_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetInteractionCountsRequest {\n    return new GetInteractionCountsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetInteractionCountsRequest {\n    return new GetInteractionCountsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetInteractionCountsRequest {\n    return new GetInteractionCountsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetInteractionCountsRequest | PlainMessage<GetInteractionCountsRequest> | undefined, b: GetInteractionCountsRequest | PlainMessage<GetInteractionCountsRequest> | undefined): boolean {\n    return proto3.util.equals(GetInteractionCountsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetInteractionCountsResponse\n */\nexport class GetInteractionCountsResponse extends Message<GetInteractionCountsResponse> {\n  /**\n   * @generated from field: repeated int32 likes = 1;\n   */\n  likes: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 reposts = 2;\n   */\n  reposts: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 replies = 3;\n   */\n  replies: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 quotes = 4;\n   */\n  quotes: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 bookmarks = 5;\n   */\n  bookmarks: number[] = [];\n\n  constructor(data?: PartialMessage<GetInteractionCountsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetInteractionCountsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"likes\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 2, name: \"reposts\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 3, name: \"replies\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 4, name: \"quotes\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 5, name: \"bookmarks\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetInteractionCountsResponse {\n    return new GetInteractionCountsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetInteractionCountsResponse {\n    return new GetInteractionCountsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetInteractionCountsResponse {\n    return new GetInteractionCountsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetInteractionCountsResponse | PlainMessage<GetInteractionCountsResponse> | undefined, b: GetInteractionCountsResponse | PlainMessage<GetInteractionCountsResponse> | undefined): boolean {\n    return proto3.util.equals(GetInteractionCountsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetCountsForUsersRequest\n */\nexport class GetCountsForUsersRequest extends Message<GetCountsForUsersRequest> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  constructor(data?: PartialMessage<GetCountsForUsersRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetCountsForUsersRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCountsForUsersRequest {\n    return new GetCountsForUsersRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCountsForUsersRequest {\n    return new GetCountsForUsersRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCountsForUsersRequest {\n    return new GetCountsForUsersRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetCountsForUsersRequest | PlainMessage<GetCountsForUsersRequest> | undefined, b: GetCountsForUsersRequest | PlainMessage<GetCountsForUsersRequest> | undefined): boolean {\n    return proto3.util.equals(GetCountsForUsersRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetCountsForUsersResponse\n */\nexport class GetCountsForUsersResponse extends Message<GetCountsForUsersResponse> {\n  /**\n   * @generated from field: repeated int32 posts = 1;\n   */\n  posts: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 reposts = 2;\n   */\n  reposts: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 following = 3;\n   */\n  following: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 followers = 4;\n   */\n  followers: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 lists = 5;\n   */\n  lists: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 feeds = 6;\n   */\n  feeds: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 starter_packs = 7;\n   */\n  starterPacks: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 drafts = 8;\n   */\n  drafts: number[] = [];\n\n  constructor(data?: PartialMessage<GetCountsForUsersResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetCountsForUsersResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"posts\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 2, name: \"reposts\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 3, name: \"following\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 4, name: \"followers\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 5, name: \"lists\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 6, name: \"feeds\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 7, name: \"starter_packs\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 8, name: \"drafts\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetCountsForUsersResponse {\n    return new GetCountsForUsersResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetCountsForUsersResponse {\n    return new GetCountsForUsersResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetCountsForUsersResponse {\n    return new GetCountsForUsersResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetCountsForUsersResponse | PlainMessage<GetCountsForUsersResponse> | undefined, b: GetCountsForUsersResponse | PlainMessage<GetCountsForUsersResponse> | undefined): boolean {\n    return proto3.util.equals(GetCountsForUsersResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetStarterPackCountsRequest\n */\nexport class GetStarterPackCountsRequest extends Message<GetStarterPackCountsRequest> {\n  /**\n   * @generated from field: repeated bsky.RecordRef refs = 1;\n   */\n  refs: RecordRef[] = [];\n\n  constructor(data?: PartialMessage<GetStarterPackCountsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetStarterPackCountsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"refs\", kind: \"message\", T: RecordRef, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetStarterPackCountsRequest {\n    return new GetStarterPackCountsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetStarterPackCountsRequest {\n    return new GetStarterPackCountsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetStarterPackCountsRequest {\n    return new GetStarterPackCountsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetStarterPackCountsRequest | PlainMessage<GetStarterPackCountsRequest> | undefined, b: GetStarterPackCountsRequest | PlainMessage<GetStarterPackCountsRequest> | undefined): boolean {\n    return proto3.util.equals(GetStarterPackCountsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetStarterPackCountsResponse\n */\nexport class GetStarterPackCountsResponse extends Message<GetStarterPackCountsResponse> {\n  /**\n   * @generated from field: repeated int32 joined_week = 1;\n   */\n  joinedWeek: number[] = [];\n\n  /**\n   * @generated from field: repeated int32 joined_all_time = 2;\n   */\n  joinedAllTime: number[] = [];\n\n  constructor(data?: PartialMessage<GetStarterPackCountsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetStarterPackCountsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"joined_week\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n    { no: 2, name: \"joined_all_time\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetStarterPackCountsResponse {\n    return new GetStarterPackCountsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetStarterPackCountsResponse {\n    return new GetStarterPackCountsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetStarterPackCountsResponse {\n    return new GetStarterPackCountsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetStarterPackCountsResponse | PlainMessage<GetStarterPackCountsResponse> | undefined, b: GetStarterPackCountsResponse | PlainMessage<GetStarterPackCountsResponse> | undefined): boolean {\n    return proto3.util.equals(GetStarterPackCountsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListCountsRequest\n */\nexport class GetListCountsRequest extends Message<GetListCountsRequest> {\n  /**\n   * @generated from field: repeated bsky.RecordRef refs = 1;\n   */\n  refs: RecordRef[] = [];\n\n  constructor(data?: PartialMessage<GetListCountsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListCountsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"refs\", kind: \"message\", T: RecordRef, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListCountsRequest {\n    return new GetListCountsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListCountsRequest {\n    return new GetListCountsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListCountsRequest {\n    return new GetListCountsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListCountsRequest | PlainMessage<GetListCountsRequest> | undefined, b: GetListCountsRequest | PlainMessage<GetListCountsRequest> | undefined): boolean {\n    return proto3.util.equals(GetListCountsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListCountsResponse\n */\nexport class GetListCountsResponse extends Message<GetListCountsResponse> {\n  /**\n   * @generated from field: repeated int32 list_items = 1;\n   */\n  listItems: number[] = [];\n\n  constructor(data?: PartialMessage<GetListCountsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListCountsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_items\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListCountsResponse {\n    return new GetListCountsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListCountsResponse {\n    return new GetListCountsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListCountsResponse {\n    return new GetListCountsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListCountsResponse | PlainMessage<GetListCountsResponse> | undefined, b: GetListCountsResponse | PlainMessage<GetListCountsResponse> | undefined): boolean {\n    return proto3.util.equals(GetListCountsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNewUserCountForRangeRequest\n */\nexport class GetNewUserCountForRangeRequest extends Message<GetNewUserCountForRangeRequest> {\n  /**\n   * @generated from field: google.protobuf.Timestamp start = 1;\n   */\n  start?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp end = 2;\n   */\n  end?: Timestamp;\n\n  constructor(data?: PartialMessage<GetNewUserCountForRangeRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNewUserCountForRangeRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"start\", kind: \"message\", T: Timestamp },\n    { no: 2, name: \"end\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNewUserCountForRangeRequest {\n    return new GetNewUserCountForRangeRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNewUserCountForRangeRequest {\n    return new GetNewUserCountForRangeRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNewUserCountForRangeRequest {\n    return new GetNewUserCountForRangeRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNewUserCountForRangeRequest | PlainMessage<GetNewUserCountForRangeRequest> | undefined, b: GetNewUserCountForRangeRequest | PlainMessage<GetNewUserCountForRangeRequest> | undefined): boolean {\n    return proto3.util.equals(GetNewUserCountForRangeRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNewUserCountForRangeResponse\n */\nexport class GetNewUserCountForRangeResponse extends Message<GetNewUserCountForRangeResponse> {\n  /**\n   * @generated from field: int32 count = 1;\n   */\n  count = 0;\n\n  constructor(data?: PartialMessage<GetNewUserCountForRangeResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNewUserCountForRangeResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNewUserCountForRangeResponse {\n    return new GetNewUserCountForRangeResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNewUserCountForRangeResponse {\n    return new GetNewUserCountForRangeResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNewUserCountForRangeResponse {\n    return new GetNewUserCountForRangeResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNewUserCountForRangeResponse | PlainMessage<GetNewUserCountForRangeResponse> | undefined, b: GetNewUserCountForRangeResponse | PlainMessage<GetNewUserCountForRangeResponse> | undefined): boolean {\n    return proto3.util.equals(GetNewUserCountForRangeResponse, a, b);\n  }\n}\n\n/**\n * - return repost uris where subject uri is subject A\n *     - `getReposts` list for a post\n *\n * @generated from message bsky.GetRepostsBySubjectRequest\n */\nexport class GetRepostsBySubjectRequest extends Message<GetRepostsBySubjectRequest> {\n  /**\n   * @generated from field: bsky.RecordRef subject = 1;\n   */\n  subject?: RecordRef;\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetRepostsBySubjectRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRepostsBySubjectRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subject\", kind: \"message\", T: RecordRef },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRepostsBySubjectRequest {\n    return new GetRepostsBySubjectRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRepostsBySubjectRequest {\n    return new GetRepostsBySubjectRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRepostsBySubjectRequest {\n    return new GetRepostsBySubjectRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRepostsBySubjectRequest | PlainMessage<GetRepostsBySubjectRequest> | undefined, b: GetRepostsBySubjectRequest | PlainMessage<GetRepostsBySubjectRequest> | undefined): boolean {\n    return proto3.util.equals(GetRepostsBySubjectRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRepostsBySubjectResponse\n */\nexport class GetRepostsBySubjectResponse extends Message<GetRepostsBySubjectResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetRepostsBySubjectResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRepostsBySubjectResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRepostsBySubjectResponse {\n    return new GetRepostsBySubjectResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRepostsBySubjectResponse {\n    return new GetRepostsBySubjectResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRepostsBySubjectResponse {\n    return new GetRepostsBySubjectResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRepostsBySubjectResponse | PlainMessage<GetRepostsBySubjectResponse> | undefined, b: GetRepostsBySubjectResponse | PlainMessage<GetRepostsBySubjectResponse> | undefined): boolean {\n    return proto3.util.equals(GetRepostsBySubjectResponse, a, b);\n  }\n}\n\n/**\n * - return repost uris for user A on subject B, C, D...\n *     - viewer state on posts\n *\n * @generated from message bsky.GetRepostsByActorAndSubjectsRequest\n */\nexport class GetRepostsByActorAndSubjectsRequest extends Message<GetRepostsByActorAndSubjectsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated bsky.RecordRef refs = 2;\n   */\n  refs: RecordRef[] = [];\n\n  constructor(data?: PartialMessage<GetRepostsByActorAndSubjectsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRepostsByActorAndSubjectsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"refs\", kind: \"message\", T: RecordRef, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRepostsByActorAndSubjectsRequest {\n    return new GetRepostsByActorAndSubjectsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRepostsByActorAndSubjectsRequest {\n    return new GetRepostsByActorAndSubjectsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRepostsByActorAndSubjectsRequest {\n    return new GetRepostsByActorAndSubjectsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRepostsByActorAndSubjectsRequest | PlainMessage<GetRepostsByActorAndSubjectsRequest> | undefined, b: GetRepostsByActorAndSubjectsRequest | PlainMessage<GetRepostsByActorAndSubjectsRequest> | undefined): boolean {\n    return proto3.util.equals(GetRepostsByActorAndSubjectsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.RecordRef\n */\nexport class RecordRef extends Message<RecordRef> {\n  /**\n   * @generated from field: string uri = 1;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  constructor(data?: PartialMessage<RecordRef>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.RecordRef\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RecordRef {\n    return new RecordRef().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RecordRef {\n    return new RecordRef().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RecordRef {\n    return new RecordRef().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: RecordRef | PlainMessage<RecordRef> | undefined, b: RecordRef | PlainMessage<RecordRef> | undefined): boolean {\n    return proto3.util.equals(RecordRef, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRepostsByActorAndSubjectsResponse\n */\nexport class GetRepostsByActorAndSubjectsResponse extends Message<GetRepostsByActorAndSubjectsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetRepostsByActorAndSubjectsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRepostsByActorAndSubjectsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRepostsByActorAndSubjectsResponse {\n    return new GetRepostsByActorAndSubjectsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRepostsByActorAndSubjectsResponse {\n    return new GetRepostsByActorAndSubjectsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRepostsByActorAndSubjectsResponse {\n    return new GetRepostsByActorAndSubjectsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRepostsByActorAndSubjectsResponse | PlainMessage<GetRepostsByActorAndSubjectsResponse> | undefined, b: GetRepostsByActorAndSubjectsResponse | PlainMessage<GetRepostsByActorAndSubjectsResponse> | undefined): boolean {\n    return proto3.util.equals(GetRepostsByActorAndSubjectsResponse, a, b);\n  }\n}\n\n/**\n * - return recent repost uris for user A\n *     - `getActorReposts` list for a user\n *\n * @generated from message bsky.GetActorRepostsRequest\n */\nexport class GetActorRepostsRequest extends Message<GetActorRepostsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorRepostsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorRepostsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorRepostsRequest {\n    return new GetActorRepostsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorRepostsRequest {\n    return new GetActorRepostsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorRepostsRequest {\n    return new GetActorRepostsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorRepostsRequest | PlainMessage<GetActorRepostsRequest> | undefined, b: GetActorRepostsRequest | PlainMessage<GetActorRepostsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorRepostsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorRepostsResponse\n */\nexport class GetActorRepostsResponse extends Message<GetActorRepostsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorRepostsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorRepostsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorRepostsResponse {\n    return new GetActorRepostsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorRepostsResponse {\n    return new GetActorRepostsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorRepostsResponse {\n    return new GetActorRepostsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorRepostsResponse | PlainMessage<GetActorRepostsResponse> | undefined, b: GetActorRepostsResponse | PlainMessage<GetActorRepostsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorRepostsResponse, a, b);\n  }\n}\n\n/**\n * - return actor information for dids A, B, C…\n *     - profile hydration\n *     - should this include handles?  apply repo takedown?\n *\n * @generated from message bsky.GetActorsRequest\n */\nexport class GetActorsRequest extends Message<GetActorsRequest> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  /**\n   * @generated from field: repeated string skip_cache_for_dids = 2;\n   */\n  skipCacheForDids: string[] = [];\n\n  /**\n   * @generated from field: repeated string return_age_assurance_for_dids = 3;\n   */\n  returnAgeAssuranceForDids: string[] = [];\n\n  constructor(data?: PartialMessage<GetActorsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"skip_cache_for_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 3, name: \"return_age_assurance_for_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorsRequest {\n    return new GetActorsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorsRequest {\n    return new GetActorsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorsRequest {\n    return new GetActorsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorsRequest | PlainMessage<GetActorsRequest> | undefined, b: GetActorsRequest | PlainMessage<GetActorsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ActorInfo\n */\nexport class ActorInfo extends Message<ActorInfo> {\n  /**\n   * @generated from field: bool exists = 1;\n   */\n  exists = false;\n\n  /**\n   * @generated from field: string handle = 2;\n   */\n  handle = \"\";\n\n  /**\n   * @generated from field: bsky.Record profile = 3;\n   */\n  profile?: Record;\n\n  /**\n   * @generated from field: bool taken_down = 4;\n   */\n  takenDown = false;\n\n  /**\n   * @generated from field: string takedown_ref = 5;\n   */\n  takedownRef = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp tombstoned_at = 6;\n   */\n  tombstonedAt?: Timestamp;\n\n  /**\n   * @generated from field: bool labeler = 7;\n   */\n  labeler = false;\n\n  /**\n   * @generated from field: string allow_incoming_chats_from = 8;\n   */\n  allowIncomingChatsFrom = \"\";\n\n  /**\n   * @generated from field: string upstream_status = 9;\n   */\n  upstreamStatus = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp created_at = 10;\n   */\n  createdAt?: Timestamp;\n\n  /**\n   * @generated from field: bool priority_notifications = 11;\n   */\n  priorityNotifications = false;\n\n  /**\n   * @generated from field: double pagerank = 12;\n   */\n  pagerank = 0;\n\n  /**\n   * @generated from field: bool trusted_verifier = 13;\n   */\n  trustedVerifier = false;\n\n  /**\n   * @generated from field: map<string, bsky.VerificationMeta> verified_by = 14;\n   */\n  verifiedBy: { [key: string]: VerificationMeta } = {};\n\n  /**\n   * Tags being applied to the account itself\n   *\n   * @generated from field: repeated string tags = 15;\n   */\n  tags: string[] = [];\n\n  /**\n   * Tags being applied to the profile record\n   *\n   * @generated from field: repeated string profile_tags = 16;\n   */\n  profileTags: string[] = [];\n\n  /**\n   * @generated from field: bsky.Record status_record = 17;\n   */\n  statusRecord?: Record;\n\n  /**\n   * @generated from field: string allow_activity_subscriptions_from = 18;\n   */\n  allowActivitySubscriptionsFrom = \"\";\n\n  /**\n   * @generated from field: optional bsky.AgeAssuranceStatus age_assurance_status = 19;\n   */\n  ageAssuranceStatus?: AgeAssuranceStatus;\n\n  /**\n   * @generated from field: bsky.Record germ_record = 21;\n   */\n  germRecord?: Record;\n\n  constructor(data?: PartialMessage<ActorInfo>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ActorInfo\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"exists\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 2, name: \"handle\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"profile\", kind: \"message\", T: Record },\n    { no: 4, name: \"taken_down\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 5, name: \"takedown_ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 6, name: \"tombstoned_at\", kind: \"message\", T: Timestamp },\n    { no: 7, name: \"labeler\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 8, name: \"allow_incoming_chats_from\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 9, name: \"upstream_status\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 10, name: \"created_at\", kind: \"message\", T: Timestamp },\n    { no: 11, name: \"priority_notifications\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 12, name: \"pagerank\", kind: \"scalar\", T: 1 /* ScalarType.DOUBLE */ },\n    { no: 13, name: \"trusted_verifier\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 14, name: \"verified_by\", kind: \"map\", K: 9 /* ScalarType.STRING */, V: {kind: \"message\", T: VerificationMeta} },\n    { no: 15, name: \"tags\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 16, name: \"profile_tags\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 17, name: \"status_record\", kind: \"message\", T: Record },\n    { no: 18, name: \"allow_activity_subscriptions_from\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 19, name: \"age_assurance_status\", kind: \"message\", T: AgeAssuranceStatus, opt: true },\n    { no: 21, name: \"germ_record\", kind: \"message\", T: Record },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ActorInfo {\n    return new ActorInfo().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ActorInfo {\n    return new ActorInfo().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ActorInfo {\n    return new ActorInfo().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ActorInfo | PlainMessage<ActorInfo> | undefined, b: ActorInfo | PlainMessage<ActorInfo> | undefined): boolean {\n    return proto3.util.equals(ActorInfo, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.AgeAssuranceStatus\n */\nexport class AgeAssuranceStatus extends Message<AgeAssuranceStatus> {\n  /**\n   * @generated from field: string status = 1;\n   */\n  status = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp last_initiated_at = 2;\n   */\n  lastInitiatedAt?: Timestamp;\n\n  /**\n   * @generated from field: bool override_applied = 3;\n   */\n  overrideApplied = false;\n\n  /**\n   * @generated from field: string access = 4;\n   */\n  access = \"\";\n\n  constructor(data?: PartialMessage<AgeAssuranceStatus>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.AgeAssuranceStatus\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"status\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"last_initiated_at\", kind: \"message\", T: Timestamp },\n    { no: 3, name: \"override_applied\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 4, name: \"access\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AgeAssuranceStatus {\n    return new AgeAssuranceStatus().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AgeAssuranceStatus {\n    return new AgeAssuranceStatus().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AgeAssuranceStatus {\n    return new AgeAssuranceStatus().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AgeAssuranceStatus | PlainMessage<AgeAssuranceStatus> | undefined, b: AgeAssuranceStatus | PlainMessage<AgeAssuranceStatus> | undefined): boolean {\n    return proto3.util.equals(AgeAssuranceStatus, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorsResponse\n */\nexport class GetActorsResponse extends Message<GetActorsResponse> {\n  /**\n   * @generated from field: repeated bsky.ActorInfo actors = 1;\n   */\n  actors: ActorInfo[] = [];\n\n  constructor(data?: PartialMessage<GetActorsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actors\", kind: \"message\", T: ActorInfo, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorsResponse {\n    return new GetActorsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorsResponse {\n    return new GetActorsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorsResponse {\n    return new GetActorsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorsResponse | PlainMessage<GetActorsResponse> | undefined, b: GetActorsResponse | PlainMessage<GetActorsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorsResponse, a, b);\n  }\n}\n\n/**\n * - return did for handle A\n *     - `resolveHandle`\n *     - answering queries where the query param is a handle\n *\n * @generated from message bsky.GetDidsByHandlesRequest\n */\nexport class GetDidsByHandlesRequest extends Message<GetDidsByHandlesRequest> {\n  /**\n   * @generated from field: repeated string handles = 1;\n   */\n  handles: string[] = [];\n\n  /**\n   * @generated from field: bool lookup_unidirectional = 2;\n   */\n  lookupUnidirectional = false;\n\n  constructor(data?: PartialMessage<GetDidsByHandlesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetDidsByHandlesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"handles\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"lookup_unidirectional\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetDidsByHandlesRequest {\n    return new GetDidsByHandlesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetDidsByHandlesRequest {\n    return new GetDidsByHandlesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetDidsByHandlesRequest {\n    return new GetDidsByHandlesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetDidsByHandlesRequest | PlainMessage<GetDidsByHandlesRequest> | undefined, b: GetDidsByHandlesRequest | PlainMessage<GetDidsByHandlesRequest> | undefined): boolean {\n    return proto3.util.equals(GetDidsByHandlesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetDidsByHandlesResponse\n */\nexport class GetDidsByHandlesResponse extends Message<GetDidsByHandlesResponse> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  constructor(data?: PartialMessage<GetDidsByHandlesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetDidsByHandlesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetDidsByHandlesResponse {\n    return new GetDidsByHandlesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetDidsByHandlesResponse {\n    return new GetDidsByHandlesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetDidsByHandlesResponse {\n    return new GetDidsByHandlesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetDidsByHandlesResponse | PlainMessage<GetDidsByHandlesResponse> | undefined, b: GetDidsByHandlesResponse | PlainMessage<GetDidsByHandlesResponse> | undefined): boolean {\n    return proto3.util.equals(GetDidsByHandlesResponse, a, b);\n  }\n}\n\n/**\n * - return relationships between user A and users B, C, D...\n *     - profile hydration\n *     - block application\n *\n * @generated from message bsky.GetRelationshipsRequest\n */\nexport class GetRelationshipsRequest extends Message<GetRelationshipsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string target_dids = 2;\n   */\n  targetDids: string[] = [];\n\n  constructor(data?: PartialMessage<GetRelationshipsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRelationshipsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRelationshipsRequest {\n    return new GetRelationshipsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRelationshipsRequest {\n    return new GetRelationshipsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRelationshipsRequest {\n    return new GetRelationshipsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRelationshipsRequest | PlainMessage<GetRelationshipsRequest> | undefined, b: GetRelationshipsRequest | PlainMessage<GetRelationshipsRequest> | undefined): boolean {\n    return proto3.util.equals(GetRelationshipsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.Relationships\n */\nexport class Relationships extends Message<Relationships> {\n  /**\n   * @generated from field: bool muted = 1;\n   */\n  muted = false;\n\n  /**\n   * @generated from field: string muted_by_list = 2;\n   */\n  mutedByList = \"\";\n\n  /**\n   * @generated from field: string blocked_by = 3;\n   */\n  blockedBy = \"\";\n\n  /**\n   * @generated from field: string blocking = 4;\n   */\n  blocking = \"\";\n\n  /**\n   * @generated from field: string blocked_by_list = 5;\n   */\n  blockedByList = \"\";\n\n  /**\n   * @generated from field: string blocking_by_list = 6;\n   */\n  blockingByList = \"\";\n\n  /**\n   * @generated from field: string following = 7;\n   */\n  following = \"\";\n\n  /**\n   * @generated from field: string followed_by = 8;\n   */\n  followedBy = \"\";\n\n  constructor(data?: PartialMessage<Relationships>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.Relationships\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"muted\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 2, name: \"muted_by_list\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"blocked_by\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"blocking\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"blocked_by_list\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 6, name: \"blocking_by_list\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 7, name: \"following\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 8, name: \"followed_by\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Relationships {\n    return new Relationships().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Relationships {\n    return new Relationships().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Relationships {\n    return new Relationships().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Relationships | PlainMessage<Relationships> | undefined, b: Relationships | PlainMessage<Relationships> | undefined): boolean {\n    return proto3.util.equals(Relationships, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRelationshipsResponse\n */\nexport class GetRelationshipsResponse extends Message<GetRelationshipsResponse> {\n  /**\n   * @generated from field: repeated bsky.Relationships relationships = 1;\n   */\n  relationships: Relationships[] = [];\n\n  constructor(data?: PartialMessage<GetRelationshipsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRelationshipsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"relationships\", kind: \"message\", T: Relationships, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRelationshipsResponse {\n    return new GetRelationshipsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRelationshipsResponse {\n    return new GetRelationshipsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRelationshipsResponse {\n    return new GetRelationshipsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRelationshipsResponse | PlainMessage<GetRelationshipsResponse> | undefined, b: GetRelationshipsResponse | PlainMessage<GetRelationshipsResponse> | undefined): boolean {\n    return proto3.util.equals(GetRelationshipsResponse, a, b);\n  }\n}\n\n/**\n * - return whether a block (bidrectionally and either direct or through a list) exists between two dids\n *     - enforcing 3rd party block violations\n *\n * @generated from message bsky.RelationshipPair\n */\nexport class RelationshipPair extends Message<RelationshipPair> {\n  /**\n   * @generated from field: string a = 1;\n   */\n  a = \"\";\n\n  /**\n   * @generated from field: string b = 2;\n   */\n  b = \"\";\n\n  constructor(data?: PartialMessage<RelationshipPair>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.RelationshipPair\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"a\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"b\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RelationshipPair {\n    return new RelationshipPair().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RelationshipPair {\n    return new RelationshipPair().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RelationshipPair {\n    return new RelationshipPair().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: RelationshipPair | PlainMessage<RelationshipPair> | undefined, b: RelationshipPair | PlainMessage<RelationshipPair> | undefined): boolean {\n    return proto3.util.equals(RelationshipPair, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.BlockExistence\n */\nexport class BlockExistence extends Message<BlockExistence> {\n  /**\n   * @generated from field: string blocked_by = 1;\n   */\n  blockedBy = \"\";\n\n  /**\n   * @generated from field: string blocking = 2;\n   */\n  blocking = \"\";\n\n  /**\n   * @generated from field: string blocked_by_list = 3;\n   */\n  blockedByList = \"\";\n\n  /**\n   * @generated from field: string blocking_by_list = 4;\n   */\n  blockingByList = \"\";\n\n  constructor(data?: PartialMessage<BlockExistence>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.BlockExistence\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"blocked_by\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"blocking\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"blocked_by_list\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"blocking_by_list\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): BlockExistence {\n    return new BlockExistence().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): BlockExistence {\n    return new BlockExistence().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): BlockExistence {\n    return new BlockExistence().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: BlockExistence | PlainMessage<BlockExistence> | undefined, b: BlockExistence | PlainMessage<BlockExistence> | undefined): boolean {\n    return proto3.util.equals(BlockExistence, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlockExistenceRequest\n */\nexport class GetBlockExistenceRequest extends Message<GetBlockExistenceRequest> {\n  /**\n   * @generated from field: repeated bsky.RelationshipPair pairs = 1;\n   */\n  pairs: RelationshipPair[] = [];\n\n  constructor(data?: PartialMessage<GetBlockExistenceRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlockExistenceRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"pairs\", kind: \"message\", T: RelationshipPair, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlockExistenceRequest {\n    return new GetBlockExistenceRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlockExistenceRequest {\n    return new GetBlockExistenceRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlockExistenceRequest {\n    return new GetBlockExistenceRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlockExistenceRequest | PlainMessage<GetBlockExistenceRequest> | undefined, b: GetBlockExistenceRequest | PlainMessage<GetBlockExistenceRequest> | undefined): boolean {\n    return proto3.util.equals(GetBlockExistenceRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlockExistenceResponse\n */\nexport class GetBlockExistenceResponse extends Message<GetBlockExistenceResponse> {\n  /**\n   * @generated from field: repeated bool exists = 1;\n   */\n  exists: boolean[] = [];\n\n  /**\n   * @generated from field: repeated bsky.BlockExistence blocks = 2;\n   */\n  blocks: BlockExistence[] = [];\n\n  constructor(data?: PartialMessage<GetBlockExistenceResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlockExistenceResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"exists\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */, repeated: true },\n    { no: 2, name: \"blocks\", kind: \"message\", T: BlockExistence, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlockExistenceResponse {\n    return new GetBlockExistenceResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlockExistenceResponse {\n    return new GetBlockExistenceResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlockExistenceResponse {\n    return new GetBlockExistenceResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlockExistenceResponse | PlainMessage<GetBlockExistenceResponse> | undefined, b: GetBlockExistenceResponse | PlainMessage<GetBlockExistenceResponse> | undefined): boolean {\n    return proto3.util.equals(GetBlockExistenceResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ListItemInfo\n */\nexport class ListItemInfo extends Message<ListItemInfo> {\n  /**\n   * @generated from field: string uri = 1;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string did = 2;\n   */\n  did = \"\";\n\n  constructor(data?: PartialMessage<ListItemInfo>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ListItemInfo\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ListItemInfo {\n    return new ListItemInfo().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ListItemInfo {\n    return new ListItemInfo().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ListItemInfo {\n    return new ListItemInfo().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ListItemInfo | PlainMessage<ListItemInfo> | undefined, b: ListItemInfo | PlainMessage<ListItemInfo> | undefined): boolean {\n    return proto3.util.equals(ListItemInfo, a, b);\n  }\n}\n\n/**\n * - Return dids of users in list A\n *     - E.g. to view items in one of your mute lists\n *\n * @generated from message bsky.GetListMembersRequest\n */\nexport class GetListMembersRequest extends Message<GetListMembersRequest> {\n  /**\n   * @generated from field: string list_uri = 1;\n   */\n  listUri = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetListMembersRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListMembersRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListMembersRequest {\n    return new GetListMembersRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListMembersRequest {\n    return new GetListMembersRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListMembersRequest {\n    return new GetListMembersRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListMembersRequest | PlainMessage<GetListMembersRequest> | undefined, b: GetListMembersRequest | PlainMessage<GetListMembersRequest> | undefined): boolean {\n    return proto3.util.equals(GetListMembersRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListMembersResponse\n */\nexport class GetListMembersResponse extends Message<GetListMembersResponse> {\n  /**\n   * @generated from field: repeated bsky.ListItemInfo listitems = 1;\n   */\n  listitems: ListItemInfo[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetListMembersResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListMembersResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"listitems\", kind: \"message\", T: ListItemInfo, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListMembersResponse {\n    return new GetListMembersResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListMembersResponse {\n    return new GetListMembersResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListMembersResponse {\n    return new GetListMembersResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListMembersResponse | PlainMessage<GetListMembersResponse> | undefined, b: GetListMembersResponse | PlainMessage<GetListMembersResponse> | undefined): boolean {\n    return proto3.util.equals(GetListMembersResponse, a, b);\n  }\n}\n\n/**\n * - Return list uris where user A in list B, C, D…\n *     - Used in thread reply gates\n *\n * @generated from message bsky.GetListMembershipRequest\n */\nexport class GetListMembershipRequest extends Message<GetListMembershipRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string list_uris = 2;\n   */\n  listUris: string[] = [];\n\n  constructor(data?: PartialMessage<GetListMembershipRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListMembershipRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"list_uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListMembershipRequest {\n    return new GetListMembershipRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListMembershipRequest {\n    return new GetListMembershipRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListMembershipRequest {\n    return new GetListMembershipRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListMembershipRequest | PlainMessage<GetListMembershipRequest> | undefined, b: GetListMembershipRequest | PlainMessage<GetListMembershipRequest> | undefined): boolean {\n    return proto3.util.equals(GetListMembershipRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListMembershipResponse\n */\nexport class GetListMembershipResponse extends Message<GetListMembershipResponse> {\n  /**\n   * @generated from field: repeated string listitem_uris = 1;\n   */\n  listitemUris: string[] = [];\n\n  constructor(data?: PartialMessage<GetListMembershipResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListMembershipResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"listitem_uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListMembershipResponse {\n    return new GetListMembershipResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListMembershipResponse {\n    return new GetListMembershipResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListMembershipResponse {\n    return new GetListMembershipResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListMembershipResponse | PlainMessage<GetListMembershipResponse> | undefined, b: GetListMembershipResponse | PlainMessage<GetListMembershipResponse> | undefined): boolean {\n    return proto3.util.equals(GetListMembershipResponse, a, b);\n  }\n}\n\n/**\n * - Return number of items in list A\n *     - For aggregate\n *\n * @generated from message bsky.GetListCountRequest\n */\nexport class GetListCountRequest extends Message<GetListCountRequest> {\n  /**\n   * @generated from field: string list_uri = 1;\n   */\n  listUri = \"\";\n\n  constructor(data?: PartialMessage<GetListCountRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListCountRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListCountRequest {\n    return new GetListCountRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListCountRequest {\n    return new GetListCountRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListCountRequest {\n    return new GetListCountRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListCountRequest | PlainMessage<GetListCountRequest> | undefined, b: GetListCountRequest | PlainMessage<GetListCountRequest> | undefined): boolean {\n    return proto3.util.equals(GetListCountRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListCountResponse\n */\nexport class GetListCountResponse extends Message<GetListCountResponse> {\n  /**\n   * @generated from field: int32 count = 1;\n   */\n  count = 0;\n\n  constructor(data?: PartialMessage<GetListCountResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListCountResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListCountResponse {\n    return new GetListCountResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListCountResponse {\n    return new GetListCountResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListCountResponse {\n    return new GetListCountResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListCountResponse | PlainMessage<GetListCountResponse> | undefined, b: GetListCountResponse | PlainMessage<GetListCountResponse> | undefined): boolean {\n    return proto3.util.equals(GetListCountResponse, a, b);\n  }\n}\n\n/**\n * - return list of uris of lists created by A\n *     - `getLists`\n *\n * @generated from message bsky.GetActorListsRequest\n */\nexport class GetActorListsRequest extends Message<GetActorListsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorListsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorListsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorListsRequest {\n    return new GetActorListsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorListsRequest {\n    return new GetActorListsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorListsRequest {\n    return new GetActorListsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorListsRequest | PlainMessage<GetActorListsRequest> | undefined, b: GetActorListsRequest | PlainMessage<GetActorListsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorListsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorListsResponse\n */\nexport class GetActorListsResponse extends Message<GetActorListsResponse> {\n  /**\n   * @generated from field: repeated string list_uris = 1;\n   */\n  listUris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorListsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorListsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorListsResponse {\n    return new GetActorListsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorListsResponse {\n    return new GetActorListsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorListsResponse {\n    return new GetActorListsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorListsResponse | PlainMessage<GetActorListsResponse> | undefined, b: GetActorListsResponse | PlainMessage<GetActorListsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorListsResponse, a, b);\n  }\n}\n\n/**\n * - return boolean if user A has muted user B\n *     - hydrating mute state onto profiles\n *\n * @generated from message bsky.GetActorMutesActorRequest\n */\nexport class GetActorMutesActorRequest extends Message<GetActorMutesActorRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string target_did = 2;\n   */\n  targetDid = \"\";\n\n  constructor(data?: PartialMessage<GetActorMutesActorRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorMutesActorRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorMutesActorRequest {\n    return new GetActorMutesActorRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorMutesActorRequest {\n    return new GetActorMutesActorRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorMutesActorRequest {\n    return new GetActorMutesActorRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorMutesActorRequest | PlainMessage<GetActorMutesActorRequest> | undefined, b: GetActorMutesActorRequest | PlainMessage<GetActorMutesActorRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorMutesActorRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorMutesActorResponse\n */\nexport class GetActorMutesActorResponse extends Message<GetActorMutesActorResponse> {\n  /**\n   * @generated from field: bool muted = 1;\n   */\n  muted = false;\n\n  constructor(data?: PartialMessage<GetActorMutesActorResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorMutesActorResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"muted\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorMutesActorResponse {\n    return new GetActorMutesActorResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorMutesActorResponse {\n    return new GetActorMutesActorResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorMutesActorResponse {\n    return new GetActorMutesActorResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorMutesActorResponse | PlainMessage<GetActorMutesActorResponse> | undefined, b: GetActorMutesActorResponse | PlainMessage<GetActorMutesActorResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorMutesActorResponse, a, b);\n  }\n}\n\n/**\n * - return list of user dids of users who A mutes\n *     - `getMutes`\n *\n * @generated from message bsky.GetMutesRequest\n */\nexport class GetMutesRequest extends Message<GetMutesRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetMutesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetMutesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMutesRequest {\n    return new GetMutesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMutesRequest {\n    return new GetMutesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMutesRequest {\n    return new GetMutesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMutesRequest | PlainMessage<GetMutesRequest> | undefined, b: GetMutesRequest | PlainMessage<GetMutesRequest> | undefined): boolean {\n    return proto3.util.equals(GetMutesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetMutesResponse\n */\nexport class GetMutesResponse extends Message<GetMutesResponse> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetMutesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetMutesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMutesResponse {\n    return new GetMutesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMutesResponse {\n    return new GetMutesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMutesResponse {\n    return new GetMutesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMutesResponse | PlainMessage<GetMutesResponse> | undefined, b: GetMutesResponse | PlainMessage<GetMutesResponse> | undefined): boolean {\n    return proto3.util.equals(GetMutesResponse, a, b);\n  }\n}\n\n/**\n * - return list uri of *any* list through which user A has muted user B\n *     - hydrating mute state onto profiles\n *     - note: we only need *one* uri even if a user is muted by multiple lists\n *\n * @generated from message bsky.GetActorMutesActorViaListRequest\n */\nexport class GetActorMutesActorViaListRequest extends Message<GetActorMutesActorViaListRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string target_did = 2;\n   */\n  targetDid = \"\";\n\n  constructor(data?: PartialMessage<GetActorMutesActorViaListRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorMutesActorViaListRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorMutesActorViaListRequest {\n    return new GetActorMutesActorViaListRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorMutesActorViaListRequest {\n    return new GetActorMutesActorViaListRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorMutesActorViaListRequest {\n    return new GetActorMutesActorViaListRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorMutesActorViaListRequest | PlainMessage<GetActorMutesActorViaListRequest> | undefined, b: GetActorMutesActorViaListRequest | PlainMessage<GetActorMutesActorViaListRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorMutesActorViaListRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorMutesActorViaListResponse\n */\nexport class GetActorMutesActorViaListResponse extends Message<GetActorMutesActorViaListResponse> {\n  /**\n   * @generated from field: string list_uri = 1;\n   */\n  listUri = \"\";\n\n  constructor(data?: PartialMessage<GetActorMutesActorViaListResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorMutesActorViaListResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorMutesActorViaListResponse {\n    return new GetActorMutesActorViaListResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorMutesActorViaListResponse {\n    return new GetActorMutesActorViaListResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorMutesActorViaListResponse {\n    return new GetActorMutesActorViaListResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorMutesActorViaListResponse | PlainMessage<GetActorMutesActorViaListResponse> | undefined, b: GetActorMutesActorViaListResponse | PlainMessage<GetActorMutesActorViaListResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorMutesActorViaListResponse, a, b);\n  }\n}\n\n/**\n * - return boolean if actor A has subscribed to mutelist B\n *     - list view hydration\n *\n * @generated from message bsky.GetMutelistSubscriptionRequest\n */\nexport class GetMutelistSubscriptionRequest extends Message<GetMutelistSubscriptionRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string list_uri = 2;\n   */\n  listUri = \"\";\n\n  constructor(data?: PartialMessage<GetMutelistSubscriptionRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetMutelistSubscriptionRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMutelistSubscriptionRequest {\n    return new GetMutelistSubscriptionRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionRequest {\n    return new GetMutelistSubscriptionRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionRequest {\n    return new GetMutelistSubscriptionRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMutelistSubscriptionRequest | PlainMessage<GetMutelistSubscriptionRequest> | undefined, b: GetMutelistSubscriptionRequest | PlainMessage<GetMutelistSubscriptionRequest> | undefined): boolean {\n    return proto3.util.equals(GetMutelistSubscriptionRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetMutelistSubscriptionResponse\n */\nexport class GetMutelistSubscriptionResponse extends Message<GetMutelistSubscriptionResponse> {\n  /**\n   * @generated from field: bool subscribed = 1;\n   */\n  subscribed = false;\n\n  constructor(data?: PartialMessage<GetMutelistSubscriptionResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetMutelistSubscriptionResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subscribed\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMutelistSubscriptionResponse {\n    return new GetMutelistSubscriptionResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionResponse {\n    return new GetMutelistSubscriptionResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionResponse {\n    return new GetMutelistSubscriptionResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMutelistSubscriptionResponse | PlainMessage<GetMutelistSubscriptionResponse> | undefined, b: GetMutelistSubscriptionResponse | PlainMessage<GetMutelistSubscriptionResponse> | undefined): boolean {\n    return proto3.util.equals(GetMutelistSubscriptionResponse, a, b);\n  }\n}\n\n/**\n * - return list of list uris of mutelists that A subscribes to\n *     - `getListMutes`\n *\n * @generated from message bsky.GetMutelistSubscriptionsRequest\n */\nexport class GetMutelistSubscriptionsRequest extends Message<GetMutelistSubscriptionsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetMutelistSubscriptionsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetMutelistSubscriptionsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMutelistSubscriptionsRequest {\n    return new GetMutelistSubscriptionsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionsRequest {\n    return new GetMutelistSubscriptionsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionsRequest {\n    return new GetMutelistSubscriptionsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMutelistSubscriptionsRequest | PlainMessage<GetMutelistSubscriptionsRequest> | undefined, b: GetMutelistSubscriptionsRequest | PlainMessage<GetMutelistSubscriptionsRequest> | undefined): boolean {\n    return proto3.util.equals(GetMutelistSubscriptionsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetMutelistSubscriptionsResponse\n */\nexport class GetMutelistSubscriptionsResponse extends Message<GetMutelistSubscriptionsResponse> {\n  /**\n   * @generated from field: repeated string list_uris = 1;\n   */\n  listUris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetMutelistSubscriptionsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetMutelistSubscriptionsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMutelistSubscriptionsResponse {\n    return new GetMutelistSubscriptionsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionsResponse {\n    return new GetMutelistSubscriptionsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMutelistSubscriptionsResponse {\n    return new GetMutelistSubscriptionsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMutelistSubscriptionsResponse | PlainMessage<GetMutelistSubscriptionsResponse> | undefined, b: GetMutelistSubscriptionsResponse | PlainMessage<GetMutelistSubscriptionsResponse> | undefined): boolean {\n    return proto3.util.equals(GetMutelistSubscriptionsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetThreadMutesOnSubjectsRequest\n */\nexport class GetThreadMutesOnSubjectsRequest extends Message<GetThreadMutesOnSubjectsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string thread_roots = 2;\n   */\n  threadRoots: string[] = [];\n\n  constructor(data?: PartialMessage<GetThreadMutesOnSubjectsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetThreadMutesOnSubjectsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"thread_roots\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetThreadMutesOnSubjectsRequest {\n    return new GetThreadMutesOnSubjectsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetThreadMutesOnSubjectsRequest {\n    return new GetThreadMutesOnSubjectsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetThreadMutesOnSubjectsRequest {\n    return new GetThreadMutesOnSubjectsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetThreadMutesOnSubjectsRequest | PlainMessage<GetThreadMutesOnSubjectsRequest> | undefined, b: GetThreadMutesOnSubjectsRequest | PlainMessage<GetThreadMutesOnSubjectsRequest> | undefined): boolean {\n    return proto3.util.equals(GetThreadMutesOnSubjectsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetThreadMutesOnSubjectsResponse\n */\nexport class GetThreadMutesOnSubjectsResponse extends Message<GetThreadMutesOnSubjectsResponse> {\n  /**\n   * @generated from field: repeated bool muted = 1;\n   */\n  muted: boolean[] = [];\n\n  constructor(data?: PartialMessage<GetThreadMutesOnSubjectsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetThreadMutesOnSubjectsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"muted\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetThreadMutesOnSubjectsResponse {\n    return new GetThreadMutesOnSubjectsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetThreadMutesOnSubjectsResponse {\n    return new GetThreadMutesOnSubjectsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetThreadMutesOnSubjectsResponse {\n    return new GetThreadMutesOnSubjectsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetThreadMutesOnSubjectsResponse | PlainMessage<GetThreadMutesOnSubjectsResponse> | undefined, b: GetThreadMutesOnSubjectsResponse | PlainMessage<GetThreadMutesOnSubjectsResponse> | undefined): boolean {\n    return proto3.util.equals(GetThreadMutesOnSubjectsResponse, a, b);\n  }\n}\n\n/**\n * - Return block uri if there is a block between users A & B (bidirectional)\n *     - hydrating (& actioning) block state on profiles\n *     - handling 3rd party blocks\n *\n * @generated from message bsky.GetBidirectionalBlockRequest\n */\nexport class GetBidirectionalBlockRequest extends Message<GetBidirectionalBlockRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string target_did = 2;\n   */\n  targetDid = \"\";\n\n  constructor(data?: PartialMessage<GetBidirectionalBlockRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBidirectionalBlockRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBidirectionalBlockRequest {\n    return new GetBidirectionalBlockRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBidirectionalBlockRequest {\n    return new GetBidirectionalBlockRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBidirectionalBlockRequest {\n    return new GetBidirectionalBlockRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBidirectionalBlockRequest | PlainMessage<GetBidirectionalBlockRequest> | undefined, b: GetBidirectionalBlockRequest | PlainMessage<GetBidirectionalBlockRequest> | undefined): boolean {\n    return proto3.util.equals(GetBidirectionalBlockRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBidirectionalBlockResponse\n */\nexport class GetBidirectionalBlockResponse extends Message<GetBidirectionalBlockResponse> {\n  /**\n   * @generated from field: string block_uri = 1;\n   */\n  blockUri = \"\";\n\n  constructor(data?: PartialMessage<GetBidirectionalBlockResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBidirectionalBlockResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"block_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBidirectionalBlockResponse {\n    return new GetBidirectionalBlockResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBidirectionalBlockResponse {\n    return new GetBidirectionalBlockResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBidirectionalBlockResponse {\n    return new GetBidirectionalBlockResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBidirectionalBlockResponse | PlainMessage<GetBidirectionalBlockResponse> | undefined, b: GetBidirectionalBlockResponse | PlainMessage<GetBidirectionalBlockResponse> | undefined): boolean {\n    return proto3.util.equals(GetBidirectionalBlockResponse, a, b);\n  }\n}\n\n/**\n * - Return list of block uris and user dids of users who A blocks\n *     - `getBlocks`\n *\n * @generated from message bsky.GetBlocksRequest\n */\nexport class GetBlocksRequest extends Message<GetBlocksRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetBlocksRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlocksRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlocksRequest {\n    return new GetBlocksRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlocksRequest {\n    return new GetBlocksRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlocksRequest {\n    return new GetBlocksRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlocksRequest | PlainMessage<GetBlocksRequest> | undefined, b: GetBlocksRequest | PlainMessage<GetBlocksRequest> | undefined): boolean {\n    return proto3.util.equals(GetBlocksRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlocksResponse\n */\nexport class GetBlocksResponse extends Message<GetBlocksResponse> {\n  /**\n   * @generated from field: repeated string block_uris = 1;\n   */\n  blockUris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetBlocksResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlocksResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"block_uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlocksResponse {\n    return new GetBlocksResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlocksResponse {\n    return new GetBlocksResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlocksResponse {\n    return new GetBlocksResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlocksResponse | PlainMessage<GetBlocksResponse> | undefined, b: GetBlocksResponse | PlainMessage<GetBlocksResponse> | undefined): boolean {\n    return proto3.util.equals(GetBlocksResponse, a, b);\n  }\n}\n\n/**\n * - Return list uri of ***any*** list through which users A & B have a block (bidirectional)\n *     - hydrating (& actioning) block state on profiles\n *     - handling 3rd party blocks\n *\n * @generated from message bsky.GetBidirectionalBlockViaListRequest\n */\nexport class GetBidirectionalBlockViaListRequest extends Message<GetBidirectionalBlockViaListRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string target_did = 2;\n   */\n  targetDid = \"\";\n\n  constructor(data?: PartialMessage<GetBidirectionalBlockViaListRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBidirectionalBlockViaListRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBidirectionalBlockViaListRequest {\n    return new GetBidirectionalBlockViaListRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBidirectionalBlockViaListRequest {\n    return new GetBidirectionalBlockViaListRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBidirectionalBlockViaListRequest {\n    return new GetBidirectionalBlockViaListRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBidirectionalBlockViaListRequest | PlainMessage<GetBidirectionalBlockViaListRequest> | undefined, b: GetBidirectionalBlockViaListRequest | PlainMessage<GetBidirectionalBlockViaListRequest> | undefined): boolean {\n    return proto3.util.equals(GetBidirectionalBlockViaListRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBidirectionalBlockViaListResponse\n */\nexport class GetBidirectionalBlockViaListResponse extends Message<GetBidirectionalBlockViaListResponse> {\n  /**\n   * @generated from field: string list_uri = 1;\n   */\n  listUri = \"\";\n\n  constructor(data?: PartialMessage<GetBidirectionalBlockViaListResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBidirectionalBlockViaListResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBidirectionalBlockViaListResponse {\n    return new GetBidirectionalBlockViaListResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBidirectionalBlockViaListResponse {\n    return new GetBidirectionalBlockViaListResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBidirectionalBlockViaListResponse {\n    return new GetBidirectionalBlockViaListResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBidirectionalBlockViaListResponse | PlainMessage<GetBidirectionalBlockViaListResponse> | undefined, b: GetBidirectionalBlockViaListResponse | PlainMessage<GetBidirectionalBlockViaListResponse> | undefined): boolean {\n    return proto3.util.equals(GetBidirectionalBlockViaListResponse, a, b);\n  }\n}\n\n/**\n * - return boolean if user A has subscribed to blocklist B\n *     - list view hydration\n *\n * @generated from message bsky.GetBlocklistSubscriptionRequest\n */\nexport class GetBlocklistSubscriptionRequest extends Message<GetBlocklistSubscriptionRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string list_uri = 2;\n   */\n  listUri = \"\";\n\n  constructor(data?: PartialMessage<GetBlocklistSubscriptionRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlocklistSubscriptionRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlocklistSubscriptionRequest {\n    return new GetBlocklistSubscriptionRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionRequest {\n    return new GetBlocklistSubscriptionRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionRequest {\n    return new GetBlocklistSubscriptionRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlocklistSubscriptionRequest | PlainMessage<GetBlocklistSubscriptionRequest> | undefined, b: GetBlocklistSubscriptionRequest | PlainMessage<GetBlocklistSubscriptionRequest> | undefined): boolean {\n    return proto3.util.equals(GetBlocklistSubscriptionRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlocklistSubscriptionResponse\n */\nexport class GetBlocklistSubscriptionResponse extends Message<GetBlocklistSubscriptionResponse> {\n  /**\n   * @generated from field: string listblock_uri = 1;\n   */\n  listblockUri = \"\";\n\n  constructor(data?: PartialMessage<GetBlocklistSubscriptionResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlocklistSubscriptionResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"listblock_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlocklistSubscriptionResponse {\n    return new GetBlocklistSubscriptionResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionResponse {\n    return new GetBlocklistSubscriptionResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionResponse {\n    return new GetBlocklistSubscriptionResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlocklistSubscriptionResponse | PlainMessage<GetBlocklistSubscriptionResponse> | undefined, b: GetBlocklistSubscriptionResponse | PlainMessage<GetBlocklistSubscriptionResponse> | undefined): boolean {\n    return proto3.util.equals(GetBlocklistSubscriptionResponse, a, b);\n  }\n}\n\n/**\n * - return list of list uris of Blockslists that A subscribes to\n *     - `getListBlocks`\n *\n * @generated from message bsky.GetBlocklistSubscriptionsRequest\n */\nexport class GetBlocklistSubscriptionsRequest extends Message<GetBlocklistSubscriptionsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetBlocklistSubscriptionsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlocklistSubscriptionsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlocklistSubscriptionsRequest {\n    return new GetBlocklistSubscriptionsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionsRequest {\n    return new GetBlocklistSubscriptionsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionsRequest {\n    return new GetBlocklistSubscriptionsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlocklistSubscriptionsRequest | PlainMessage<GetBlocklistSubscriptionsRequest> | undefined, b: GetBlocklistSubscriptionsRequest | PlainMessage<GetBlocklistSubscriptionsRequest> | undefined): boolean {\n    return proto3.util.equals(GetBlocklistSubscriptionsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlocklistSubscriptionsResponse\n */\nexport class GetBlocklistSubscriptionsResponse extends Message<GetBlocklistSubscriptionsResponse> {\n  /**\n   * @generated from field: repeated string list_uris = 1;\n   */\n  listUris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetBlocklistSubscriptionsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlocklistSubscriptionsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlocklistSubscriptionsResponse {\n    return new GetBlocklistSubscriptionsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionsResponse {\n    return new GetBlocklistSubscriptionsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlocklistSubscriptionsResponse {\n    return new GetBlocklistSubscriptionsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlocklistSubscriptionsResponse | PlainMessage<GetBlocklistSubscriptionsResponse> | undefined, b: GetBlocklistSubscriptionsResponse | PlainMessage<GetBlocklistSubscriptionsResponse> | undefined): boolean {\n    return proto3.util.equals(GetBlocklistSubscriptionsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNotificationPreferencesRequest\n */\nexport class GetNotificationPreferencesRequest extends Message<GetNotificationPreferencesRequest> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  constructor(data?: PartialMessage<GetNotificationPreferencesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationPreferencesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationPreferencesRequest {\n    return new GetNotificationPreferencesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationPreferencesRequest {\n    return new GetNotificationPreferencesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationPreferencesRequest {\n    return new GetNotificationPreferencesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationPreferencesRequest | PlainMessage<GetNotificationPreferencesRequest> | undefined, b: GetNotificationPreferencesRequest | PlainMessage<GetNotificationPreferencesRequest> | undefined): boolean {\n    return proto3.util.equals(GetNotificationPreferencesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.NotificationChannelList\n */\nexport class NotificationChannelList extends Message<NotificationChannelList> {\n  /**\n   * @generated from field: bool enabled = 1;\n   */\n  enabled = false;\n\n  constructor(data?: PartialMessage<NotificationChannelList>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.NotificationChannelList\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"enabled\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): NotificationChannelList {\n    return new NotificationChannelList().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): NotificationChannelList {\n    return new NotificationChannelList().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): NotificationChannelList {\n    return new NotificationChannelList().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: NotificationChannelList | PlainMessage<NotificationChannelList> | undefined, b: NotificationChannelList | PlainMessage<NotificationChannelList> | undefined): boolean {\n    return proto3.util.equals(NotificationChannelList, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.NotificationChannelPush\n */\nexport class NotificationChannelPush extends Message<NotificationChannelPush> {\n  /**\n   * @generated from field: bool enabled = 1;\n   */\n  enabled = false;\n\n  constructor(data?: PartialMessage<NotificationChannelPush>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.NotificationChannelPush\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"enabled\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): NotificationChannelPush {\n    return new NotificationChannelPush().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): NotificationChannelPush {\n    return new NotificationChannelPush().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): NotificationChannelPush {\n    return new NotificationChannelPush().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: NotificationChannelPush | PlainMessage<NotificationChannelPush> | undefined, b: NotificationChannelPush | PlainMessage<NotificationChannelPush> | undefined): boolean {\n    return proto3.util.equals(NotificationChannelPush, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.FilterableNotificationPreference\n */\nexport class FilterableNotificationPreference extends Message<FilterableNotificationPreference> {\n  /**\n   * @generated from field: bsky.NotificationInclude include = 1;\n   */\n  include = NotificationInclude.UNSPECIFIED;\n\n  /**\n   * @generated from field: bsky.NotificationChannelList list = 2;\n   */\n  list?: NotificationChannelList;\n\n  /**\n   * @generated from field: bsky.NotificationChannelPush push = 3;\n   */\n  push?: NotificationChannelPush;\n\n  constructor(data?: PartialMessage<FilterableNotificationPreference>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.FilterableNotificationPreference\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"include\", kind: \"enum\", T: proto3.getEnumType(NotificationInclude) },\n    { no: 2, name: \"list\", kind: \"message\", T: NotificationChannelList },\n    { no: 3, name: \"push\", kind: \"message\", T: NotificationChannelPush },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): FilterableNotificationPreference {\n    return new FilterableNotificationPreference().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): FilterableNotificationPreference {\n    return new FilterableNotificationPreference().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): FilterableNotificationPreference {\n    return new FilterableNotificationPreference().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: FilterableNotificationPreference | PlainMessage<FilterableNotificationPreference> | undefined, b: FilterableNotificationPreference | PlainMessage<FilterableNotificationPreference> | undefined): boolean {\n    return proto3.util.equals(FilterableNotificationPreference, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.NotificationPreference\n */\nexport class NotificationPreference extends Message<NotificationPreference> {\n  /**\n   * @generated from field: bsky.NotificationChannelList list = 1;\n   */\n  list?: NotificationChannelList;\n\n  /**\n   * @generated from field: bsky.NotificationChannelPush push = 2;\n   */\n  push?: NotificationChannelPush;\n\n  constructor(data?: PartialMessage<NotificationPreference>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.NotificationPreference\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list\", kind: \"message\", T: NotificationChannelList },\n    { no: 2, name: \"push\", kind: \"message\", T: NotificationChannelPush },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): NotificationPreference {\n    return new NotificationPreference().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): NotificationPreference {\n    return new NotificationPreference().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): NotificationPreference {\n    return new NotificationPreference().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: NotificationPreference | PlainMessage<NotificationPreference> | undefined, b: NotificationPreference | PlainMessage<NotificationPreference> | undefined): boolean {\n    return proto3.util.equals(NotificationPreference, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ChatNotificationPreference\n */\nexport class ChatNotificationPreference extends Message<ChatNotificationPreference> {\n  /**\n   * @generated from field: bsky.ChatNotificationInclude include = 1;\n   */\n  include = ChatNotificationInclude.UNSPECIFIED;\n\n  /**\n   * @generated from field: bsky.NotificationChannelPush push = 2;\n   */\n  push?: NotificationChannelPush;\n\n  constructor(data?: PartialMessage<ChatNotificationPreference>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ChatNotificationPreference\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"include\", kind: \"enum\", T: proto3.getEnumType(ChatNotificationInclude) },\n    { no: 2, name: \"push\", kind: \"message\", T: NotificationChannelPush },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ChatNotificationPreference {\n    return new ChatNotificationPreference().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ChatNotificationPreference {\n    return new ChatNotificationPreference().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ChatNotificationPreference {\n    return new ChatNotificationPreference().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ChatNotificationPreference | PlainMessage<ChatNotificationPreference> | undefined, b: ChatNotificationPreference | PlainMessage<ChatNotificationPreference> | undefined): boolean {\n    return proto3.util.equals(ChatNotificationPreference, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.NotificationPreferences\n */\nexport class NotificationPreferences extends Message<NotificationPreferences> {\n  /**\n   * @generated from field: bytes entry = 1;\n   */\n  entry = new Uint8Array(0);\n\n  /**\n   * @generated from field: bsky.ChatNotificationPreference chat = 2;\n   */\n  chat?: ChatNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference follow = 3;\n   */\n  follow?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference like = 4;\n   */\n  like?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference like_via_repost = 5;\n   */\n  likeViaRepost?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference mention = 6;\n   */\n  mention?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference quote = 7;\n   */\n  quote?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference reply = 8;\n   */\n  reply?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference repost = 9;\n   */\n  repost?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.FilterableNotificationPreference repost_via_repost = 10;\n   */\n  repostViaRepost?: FilterableNotificationPreference;\n\n  /**\n   * @generated from field: bsky.NotificationPreference starterpack_joined = 11;\n   */\n  starterpackJoined?: NotificationPreference;\n\n  /**\n   * @generated from field: bsky.NotificationPreference subscribed_post = 12;\n   */\n  subscribedPost?: NotificationPreference;\n\n  /**\n   * @generated from field: bsky.NotificationPreference unverified = 13;\n   */\n  unverified?: NotificationPreference;\n\n  /**\n   * @generated from field: bsky.NotificationPreference verified = 14;\n   */\n  verified?: NotificationPreference;\n\n  constructor(data?: PartialMessage<NotificationPreferences>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.NotificationPreferences\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"entry\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n    { no: 2, name: \"chat\", kind: \"message\", T: ChatNotificationPreference },\n    { no: 3, name: \"follow\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 4, name: \"like\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 5, name: \"like_via_repost\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 6, name: \"mention\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 7, name: \"quote\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 8, name: \"reply\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 9, name: \"repost\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 10, name: \"repost_via_repost\", kind: \"message\", T: FilterableNotificationPreference },\n    { no: 11, name: \"starterpack_joined\", kind: \"message\", T: NotificationPreference },\n    { no: 12, name: \"subscribed_post\", kind: \"message\", T: NotificationPreference },\n    { no: 13, name: \"unverified\", kind: \"message\", T: NotificationPreference },\n    { no: 14, name: \"verified\", kind: \"message\", T: NotificationPreference },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): NotificationPreferences {\n    return new NotificationPreferences().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): NotificationPreferences {\n    return new NotificationPreferences().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): NotificationPreferences {\n    return new NotificationPreferences().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: NotificationPreferences | PlainMessage<NotificationPreferences> | undefined, b: NotificationPreferences | PlainMessage<NotificationPreferences> | undefined): boolean {\n    return proto3.util.equals(NotificationPreferences, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNotificationPreferencesResponse\n */\nexport class GetNotificationPreferencesResponse extends Message<GetNotificationPreferencesResponse> {\n  /**\n   * @generated from field: repeated bsky.NotificationPreferences preferences = 1;\n   */\n  preferences: NotificationPreferences[] = [];\n\n  constructor(data?: PartialMessage<GetNotificationPreferencesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationPreferencesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"preferences\", kind: \"message\", T: NotificationPreferences, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationPreferencesResponse {\n    return new GetNotificationPreferencesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationPreferencesResponse {\n    return new GetNotificationPreferencesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationPreferencesResponse {\n    return new GetNotificationPreferencesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationPreferencesResponse | PlainMessage<GetNotificationPreferencesResponse> | undefined, b: GetNotificationPreferencesResponse | PlainMessage<GetNotificationPreferencesResponse> | undefined): boolean {\n    return proto3.util.equals(GetNotificationPreferencesResponse, a, b);\n  }\n}\n\n/**\n * - list recent notifications for a user\n *     - notifications should include a uri for the record that caused the notif & a “reason” for the notification (reply, like, quotepost, etc)\n *     - this should include both read & unread notifs\n *\n * @generated from message bsky.GetNotificationsRequest\n */\nexport class GetNotificationsRequest extends Message<GetNotificationsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: bool priority = 4;\n   */\n  priority = false;\n\n  constructor(data?: PartialMessage<GetNotificationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationsRequest {\n    return new GetNotificationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationsRequest {\n    return new GetNotificationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationsRequest {\n    return new GetNotificationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationsRequest | PlainMessage<GetNotificationsRequest> | undefined, b: GetNotificationsRequest | PlainMessage<GetNotificationsRequest> | undefined): boolean {\n    return proto3.util.equals(GetNotificationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.Notification\n */\nexport class Notification extends Message<Notification> {\n  /**\n   * @generated from field: string recipient_did = 1;\n   */\n  recipientDid = \"\";\n\n  /**\n   * @generated from field: string uri = 2;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string reason = 3;\n   */\n  reason = \"\";\n\n  /**\n   * @generated from field: string reason_subject = 4;\n   */\n  reasonSubject = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp timestamp = 5;\n   */\n  timestamp?: Timestamp;\n\n  /**\n   * @generated from field: bool priority = 6;\n   */\n  priority = false;\n\n  constructor(data?: PartialMessage<Notification>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.Notification\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"recipient_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"reason\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"reason_subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"timestamp\", kind: \"message\", T: Timestamp },\n    { no: 6, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Notification {\n    return new Notification().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Notification {\n    return new Notification().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Notification {\n    return new Notification().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Notification | PlainMessage<Notification> | undefined, b: Notification | PlainMessage<Notification> | undefined): boolean {\n    return proto3.util.equals(Notification, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNotificationsResponse\n */\nexport class GetNotificationsResponse extends Message<GetNotificationsResponse> {\n  /**\n   * @generated from field: repeated bsky.Notification notifications = 1;\n   */\n  notifications: Notification[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetNotificationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"notifications\", kind: \"message\", T: Notification, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationsResponse {\n    return new GetNotificationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationsResponse {\n    return new GetNotificationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationsResponse {\n    return new GetNotificationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationsResponse | PlainMessage<GetNotificationsResponse> | undefined, b: GetNotificationsResponse | PlainMessage<GetNotificationsResponse> | undefined): boolean {\n    return proto3.util.equals(GetNotificationsResponse, a, b);\n  }\n}\n\n/**\n * - update a user’s “last seen time”\n *     - `updateSeen`\n *\n * @generated from message bsky.UpdateNotificationSeenRequest\n */\nexport class UpdateNotificationSeenRequest extends Message<UpdateNotificationSeenRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp timestamp = 2;\n   */\n  timestamp?: Timestamp;\n\n  /**\n   * @generated from field: bool priority = 3;\n   */\n  priority = false;\n\n  constructor(data?: PartialMessage<UpdateNotificationSeenRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UpdateNotificationSeenRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"timestamp\", kind: \"message\", T: Timestamp },\n    { no: 3, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateNotificationSeenRequest {\n    return new UpdateNotificationSeenRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateNotificationSeenRequest {\n    return new UpdateNotificationSeenRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateNotificationSeenRequest {\n    return new UpdateNotificationSeenRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UpdateNotificationSeenRequest | PlainMessage<UpdateNotificationSeenRequest> | undefined, b: UpdateNotificationSeenRequest | PlainMessage<UpdateNotificationSeenRequest> | undefined): boolean {\n    return proto3.util.equals(UpdateNotificationSeenRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UpdateNotificationSeenResponse\n */\nexport class UpdateNotificationSeenResponse extends Message<UpdateNotificationSeenResponse> {\n  constructor(data?: PartialMessage<UpdateNotificationSeenResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UpdateNotificationSeenResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateNotificationSeenResponse {\n    return new UpdateNotificationSeenResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateNotificationSeenResponse {\n    return new UpdateNotificationSeenResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateNotificationSeenResponse {\n    return new UpdateNotificationSeenResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UpdateNotificationSeenResponse | PlainMessage<UpdateNotificationSeenResponse> | undefined, b: UpdateNotificationSeenResponse | PlainMessage<UpdateNotificationSeenResponse> | undefined): boolean {\n    return proto3.util.equals(UpdateNotificationSeenResponse, a, b);\n  }\n}\n\n/**\n * - get a user’s “last seen time”\n *     - hydrating read state onto notifications\n *\n * @generated from message bsky.GetNotificationSeenRequest\n */\nexport class GetNotificationSeenRequest extends Message<GetNotificationSeenRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: bool priority = 2;\n   */\n  priority = false;\n\n  constructor(data?: PartialMessage<GetNotificationSeenRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationSeenRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationSeenRequest {\n    return new GetNotificationSeenRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationSeenRequest {\n    return new GetNotificationSeenRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationSeenRequest {\n    return new GetNotificationSeenRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationSeenRequest | PlainMessage<GetNotificationSeenRequest> | undefined, b: GetNotificationSeenRequest | PlainMessage<GetNotificationSeenRequest> | undefined): boolean {\n    return proto3.util.equals(GetNotificationSeenRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetNotificationSeenResponse\n */\nexport class GetNotificationSeenResponse extends Message<GetNotificationSeenResponse> {\n  /**\n   * @generated from field: google.protobuf.Timestamp timestamp = 1;\n   */\n  timestamp?: Timestamp;\n\n  constructor(data?: PartialMessage<GetNotificationSeenResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetNotificationSeenResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"timestamp\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetNotificationSeenResponse {\n    return new GetNotificationSeenResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetNotificationSeenResponse {\n    return new GetNotificationSeenResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetNotificationSeenResponse {\n    return new GetNotificationSeenResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetNotificationSeenResponse | PlainMessage<GetNotificationSeenResponse> | undefined, b: GetNotificationSeenResponse | PlainMessage<GetNotificationSeenResponse> | undefined): boolean {\n    return proto3.util.equals(GetNotificationSeenResponse, a, b);\n  }\n}\n\n/**\n * - get a count of all unread notifications (notifications after `updateSeen`)\n *     - `getUnreadCount`\n *\n * @generated from message bsky.GetUnreadNotificationCountRequest\n */\nexport class GetUnreadNotificationCountRequest extends Message<GetUnreadNotificationCountRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: bool priority = 2;\n   */\n  priority = false;\n\n  constructor(data?: PartialMessage<GetUnreadNotificationCountRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetUnreadNotificationCountRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetUnreadNotificationCountRequest {\n    return new GetUnreadNotificationCountRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetUnreadNotificationCountRequest {\n    return new GetUnreadNotificationCountRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetUnreadNotificationCountRequest {\n    return new GetUnreadNotificationCountRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetUnreadNotificationCountRequest | PlainMessage<GetUnreadNotificationCountRequest> | undefined, b: GetUnreadNotificationCountRequest | PlainMessage<GetUnreadNotificationCountRequest> | undefined): boolean {\n    return proto3.util.equals(GetUnreadNotificationCountRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetUnreadNotificationCountResponse\n */\nexport class GetUnreadNotificationCountResponse extends Message<GetUnreadNotificationCountResponse> {\n  /**\n   * @generated from field: int32 count = 1;\n   */\n  count = 0;\n\n  constructor(data?: PartialMessage<GetUnreadNotificationCountResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetUnreadNotificationCountResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetUnreadNotificationCountResponse {\n    return new GetUnreadNotificationCountResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetUnreadNotificationCountResponse {\n    return new GetUnreadNotificationCountResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetUnreadNotificationCountResponse {\n    return new GetUnreadNotificationCountResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetUnreadNotificationCountResponse | PlainMessage<GetUnreadNotificationCountResponse> | undefined, b: GetUnreadNotificationCountResponse | PlainMessage<GetUnreadNotificationCountResponse> | undefined): boolean {\n    return proto3.util.equals(GetUnreadNotificationCountResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActivitySubscriptionDidsRequest\n */\nexport class GetActivitySubscriptionDidsRequest extends Message<GetActivitySubscriptionDidsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActivitySubscriptionDidsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActivitySubscriptionDidsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActivitySubscriptionDidsRequest {\n    return new GetActivitySubscriptionDidsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActivitySubscriptionDidsRequest {\n    return new GetActivitySubscriptionDidsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActivitySubscriptionDidsRequest {\n    return new GetActivitySubscriptionDidsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActivitySubscriptionDidsRequest | PlainMessage<GetActivitySubscriptionDidsRequest> | undefined, b: GetActivitySubscriptionDidsRequest | PlainMessage<GetActivitySubscriptionDidsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActivitySubscriptionDidsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActivitySubscriptionDidsResponse\n */\nexport class GetActivitySubscriptionDidsResponse extends Message<GetActivitySubscriptionDidsResponse> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActivitySubscriptionDidsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActivitySubscriptionDidsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActivitySubscriptionDidsResponse {\n    return new GetActivitySubscriptionDidsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActivitySubscriptionDidsResponse {\n    return new GetActivitySubscriptionDidsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActivitySubscriptionDidsResponse {\n    return new GetActivitySubscriptionDidsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActivitySubscriptionDidsResponse | PlainMessage<GetActivitySubscriptionDidsResponse> | undefined, b: GetActivitySubscriptionDidsResponse | PlainMessage<GetActivitySubscriptionDidsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActivitySubscriptionDidsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.PostActivitySubscription\n */\nexport class PostActivitySubscription extends Message<PostActivitySubscription> {\n  constructor(data?: PartialMessage<PostActivitySubscription>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.PostActivitySubscription\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PostActivitySubscription {\n    return new PostActivitySubscription().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PostActivitySubscription {\n    return new PostActivitySubscription().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PostActivitySubscription {\n    return new PostActivitySubscription().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PostActivitySubscription | PlainMessage<PostActivitySubscription> | undefined, b: PostActivitySubscription | PlainMessage<PostActivitySubscription> | undefined): boolean {\n    return proto3.util.equals(PostActivitySubscription, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ReplyActivitySubscription\n */\nexport class ReplyActivitySubscription extends Message<ReplyActivitySubscription> {\n  constructor(data?: PartialMessage<ReplyActivitySubscription>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ReplyActivitySubscription\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ReplyActivitySubscription {\n    return new ReplyActivitySubscription().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ReplyActivitySubscription {\n    return new ReplyActivitySubscription().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ReplyActivitySubscription {\n    return new ReplyActivitySubscription().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ReplyActivitySubscription | PlainMessage<ReplyActivitySubscription> | undefined, b: ReplyActivitySubscription | PlainMessage<ReplyActivitySubscription> | undefined): boolean {\n    return proto3.util.equals(ReplyActivitySubscription, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ActivitySubscription\n */\nexport class ActivitySubscription extends Message<ActivitySubscription> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 2;\n   */\n  namespace = \"\";\n\n  /**\n   * @generated from field: string key = 3;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: optional bsky.PostActivitySubscription post = 4;\n   */\n  post?: PostActivitySubscription;\n\n  /**\n   * @generated from field: optional bsky.ReplyActivitySubscription reply = 5;\n   */\n  reply?: ReplyActivitySubscription;\n\n  /**\n   * @generated from field: string subject_did = 6;\n   */\n  subjectDid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp indexed_at = 7;\n   */\n  indexedAt?: Timestamp;\n\n  constructor(data?: PartialMessage<ActivitySubscription>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ActivitySubscription\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"post\", kind: \"message\", T: PostActivitySubscription, opt: true },\n    { no: 5, name: \"reply\", kind: \"message\", T: ReplyActivitySubscription, opt: true },\n    { no: 6, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 7, name: \"indexed_at\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ActivitySubscription {\n    return new ActivitySubscription().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ActivitySubscription {\n    return new ActivitySubscription().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ActivitySubscription {\n    return new ActivitySubscription().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ActivitySubscription | PlainMessage<ActivitySubscription> | undefined, b: ActivitySubscription | PlainMessage<ActivitySubscription> | undefined): boolean {\n    return proto3.util.equals(ActivitySubscription, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActivitySubscriptionsByActorAndSubjectsRequest\n */\nexport class GetActivitySubscriptionsByActorAndSubjectsRequest extends Message<GetActivitySubscriptionsByActorAndSubjectsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string subject_dids = 2;\n   */\n  subjectDids: string[] = [];\n\n  constructor(data?: PartialMessage<GetActivitySubscriptionsByActorAndSubjectsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActivitySubscriptionsByActorAndSubjectsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActivitySubscriptionsByActorAndSubjectsRequest {\n    return new GetActivitySubscriptionsByActorAndSubjectsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActivitySubscriptionsByActorAndSubjectsRequest {\n    return new GetActivitySubscriptionsByActorAndSubjectsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActivitySubscriptionsByActorAndSubjectsRequest {\n    return new GetActivitySubscriptionsByActorAndSubjectsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActivitySubscriptionsByActorAndSubjectsRequest | PlainMessage<GetActivitySubscriptionsByActorAndSubjectsRequest> | undefined, b: GetActivitySubscriptionsByActorAndSubjectsRequest | PlainMessage<GetActivitySubscriptionsByActorAndSubjectsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActivitySubscriptionsByActorAndSubjectsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActivitySubscriptionsByActorAndSubjectsResponse\n */\nexport class GetActivitySubscriptionsByActorAndSubjectsResponse extends Message<GetActivitySubscriptionsByActorAndSubjectsResponse> {\n  /**\n   * @generated from field: repeated bsky.ActivitySubscription subscriptions = 1;\n   */\n  subscriptions: ActivitySubscription[] = [];\n\n  constructor(data?: PartialMessage<GetActivitySubscriptionsByActorAndSubjectsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActivitySubscriptionsByActorAndSubjectsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subscriptions\", kind: \"message\", T: ActivitySubscription, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActivitySubscriptionsByActorAndSubjectsResponse {\n    return new GetActivitySubscriptionsByActorAndSubjectsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActivitySubscriptionsByActorAndSubjectsResponse {\n    return new GetActivitySubscriptionsByActorAndSubjectsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActivitySubscriptionsByActorAndSubjectsResponse {\n    return new GetActivitySubscriptionsByActorAndSubjectsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActivitySubscriptionsByActorAndSubjectsResponse | PlainMessage<GetActivitySubscriptionsByActorAndSubjectsResponse> | undefined, b: GetActivitySubscriptionsByActorAndSubjectsResponse | PlainMessage<GetActivitySubscriptionsByActorAndSubjectsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActivitySubscriptionsByActorAndSubjectsResponse, a, b);\n  }\n}\n\n/**\n * - Return uris of feed generator records created by user A\n *     - `getActorFeeds`\n *\n * @generated from message bsky.GetActorFeedsRequest\n */\nexport class GetActorFeedsRequest extends Message<GetActorFeedsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorFeedsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorFeedsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorFeedsRequest {\n    return new GetActorFeedsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorFeedsRequest {\n    return new GetActorFeedsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorFeedsRequest {\n    return new GetActorFeedsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorFeedsRequest | PlainMessage<GetActorFeedsRequest> | undefined, b: GetActorFeedsRequest | PlainMessage<GetActorFeedsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorFeedsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorFeedsResponse\n */\nexport class GetActorFeedsResponse extends Message<GetActorFeedsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorFeedsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorFeedsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorFeedsResponse {\n    return new GetActorFeedsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorFeedsResponse {\n    return new GetActorFeedsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorFeedsResponse {\n    return new GetActorFeedsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorFeedsResponse | PlainMessage<GetActorFeedsResponse> | undefined, b: GetActorFeedsResponse | PlainMessage<GetActorFeedsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorFeedsResponse, a, b);\n  }\n}\n\n/**\n * - Returns a list of suggested feed generator uris for an actor, paginated\n *     - `getSuggestedFeeds`\n *     - This is currently just hardcoded in the Appview DB\n *\n * @generated from message bsky.GetSuggestedFeedsRequest\n */\nexport class GetSuggestedFeedsRequest extends Message<GetSuggestedFeedsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetSuggestedFeedsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSuggestedFeedsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSuggestedFeedsRequest {\n    return new GetSuggestedFeedsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSuggestedFeedsRequest {\n    return new GetSuggestedFeedsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSuggestedFeedsRequest {\n    return new GetSuggestedFeedsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSuggestedFeedsRequest | PlainMessage<GetSuggestedFeedsRequest> | undefined, b: GetSuggestedFeedsRequest | PlainMessage<GetSuggestedFeedsRequest> | undefined): boolean {\n    return proto3.util.equals(GetSuggestedFeedsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetSuggestedFeedsResponse\n */\nexport class GetSuggestedFeedsResponse extends Message<GetSuggestedFeedsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetSuggestedFeedsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSuggestedFeedsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSuggestedFeedsResponse {\n    return new GetSuggestedFeedsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSuggestedFeedsResponse {\n    return new GetSuggestedFeedsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSuggestedFeedsResponse {\n    return new GetSuggestedFeedsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSuggestedFeedsResponse | PlainMessage<GetSuggestedFeedsResponse> | undefined, b: GetSuggestedFeedsResponse | PlainMessage<GetSuggestedFeedsResponse> | undefined): boolean {\n    return proto3.util.equals(GetSuggestedFeedsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.SearchFeedGeneratorsRequest\n */\nexport class SearchFeedGeneratorsRequest extends Message<SearchFeedGeneratorsRequest> {\n  /**\n   * @generated from field: string query = 1;\n   */\n  query = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<SearchFeedGeneratorsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchFeedGeneratorsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"query\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchFeedGeneratorsRequest {\n    return new SearchFeedGeneratorsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchFeedGeneratorsRequest {\n    return new SearchFeedGeneratorsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchFeedGeneratorsRequest {\n    return new SearchFeedGeneratorsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchFeedGeneratorsRequest | PlainMessage<SearchFeedGeneratorsRequest> | undefined, b: SearchFeedGeneratorsRequest | PlainMessage<SearchFeedGeneratorsRequest> | undefined): boolean {\n    return proto3.util.equals(SearchFeedGeneratorsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.SearchFeedGeneratorsResponse\n */\nexport class SearchFeedGeneratorsResponse extends Message<SearchFeedGeneratorsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<SearchFeedGeneratorsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchFeedGeneratorsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchFeedGeneratorsResponse {\n    return new SearchFeedGeneratorsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchFeedGeneratorsResponse {\n    return new SearchFeedGeneratorsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchFeedGeneratorsResponse {\n    return new SearchFeedGeneratorsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchFeedGeneratorsResponse | PlainMessage<SearchFeedGeneratorsResponse> | undefined, b: SearchFeedGeneratorsResponse | PlainMessage<SearchFeedGeneratorsResponse> | undefined): boolean {\n    return proto3.util.equals(SearchFeedGeneratorsResponse, a, b);\n  }\n}\n\n/**\n * - Returns feed generator validity and online status with uris A, B, C…\n *     - Not currently being used, but could be worhthwhile.\n *\n * @generated from message bsky.GetFeedGeneratorStatusRequest\n */\nexport class GetFeedGeneratorStatusRequest extends Message<GetFeedGeneratorStatusRequest> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetFeedGeneratorStatusRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFeedGeneratorStatusRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFeedGeneratorStatusRequest {\n    return new GetFeedGeneratorStatusRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFeedGeneratorStatusRequest {\n    return new GetFeedGeneratorStatusRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFeedGeneratorStatusRequest {\n    return new GetFeedGeneratorStatusRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFeedGeneratorStatusRequest | PlainMessage<GetFeedGeneratorStatusRequest> | undefined, b: GetFeedGeneratorStatusRequest | PlainMessage<GetFeedGeneratorStatusRequest> | undefined): boolean {\n    return proto3.util.equals(GetFeedGeneratorStatusRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFeedGeneratorStatusResponse\n */\nexport class GetFeedGeneratorStatusResponse extends Message<GetFeedGeneratorStatusResponse> {\n  /**\n   * @generated from field: repeated string status = 1;\n   */\n  status: string[] = [];\n\n  constructor(data?: PartialMessage<GetFeedGeneratorStatusResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFeedGeneratorStatusResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"status\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFeedGeneratorStatusResponse {\n    return new GetFeedGeneratorStatusResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFeedGeneratorStatusResponse {\n    return new GetFeedGeneratorStatusResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFeedGeneratorStatusResponse {\n    return new GetFeedGeneratorStatusResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFeedGeneratorStatusResponse | PlainMessage<GetFeedGeneratorStatusResponse> | undefined, b: GetFeedGeneratorStatusResponse | PlainMessage<GetFeedGeneratorStatusResponse> | undefined): boolean {\n    return proto3.util.equals(GetFeedGeneratorStatusResponse, a, b);\n  }\n}\n\n/**\n * - Returns recent posts authored by a given DID, paginated\n *     - `getAuthorFeed`\n *     - Optionally: filter by if a post is/isn’t a reply and if a post has a media object in it\n *\n * @generated from message bsky.GetAuthorFeedRequest\n */\nexport class GetAuthorFeedRequest extends Message<GetAuthorFeedRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: bsky.FeedType feed_type = 4;\n   */\n  feedType = FeedType.UNSPECIFIED;\n\n  constructor(data?: PartialMessage<GetAuthorFeedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetAuthorFeedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"feed_type\", kind: \"enum\", T: proto3.getEnumType(FeedType) },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetAuthorFeedRequest {\n    return new GetAuthorFeedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetAuthorFeedRequest {\n    return new GetAuthorFeedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetAuthorFeedRequest {\n    return new GetAuthorFeedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetAuthorFeedRequest | PlainMessage<GetAuthorFeedRequest> | undefined, b: GetAuthorFeedRequest | PlainMessage<GetAuthorFeedRequest> | undefined): boolean {\n    return proto3.util.equals(GetAuthorFeedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.AuthorFeedItem\n */\nexport class AuthorFeedItem extends Message<AuthorFeedItem> {\n  /**\n   * @generated from field: string uri = 1;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  /**\n   * @generated from field: string repost = 3;\n   */\n  repost = \"\";\n\n  /**\n   * @generated from field: string repost_cid = 4;\n   */\n  repostCid = \"\";\n\n  /**\n   * @generated from field: bool posts_and_author_threads = 5;\n   */\n  postsAndAuthorThreads = false;\n\n  /**\n   * @generated from field: bool posts_no_replies = 6;\n   */\n  postsNoReplies = false;\n\n  /**\n   * @generated from field: bool posts_with_media = 7;\n   */\n  postsWithMedia = false;\n\n  /**\n   * @generated from field: bool is_reply = 8;\n   */\n  isReply = false;\n\n  /**\n   * @generated from field: bool is_repost = 9;\n   */\n  isRepost = false;\n\n  /**\n   * @generated from field: bool is_quote_post = 10;\n   */\n  isQuotePost = false;\n\n  /**\n   * @generated from field: bool posts_with_video = 11;\n   */\n  postsWithVideo = false;\n\n  constructor(data?: PartialMessage<AuthorFeedItem>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.AuthorFeedItem\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"repost\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"repost_cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"posts_and_author_threads\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 6, name: \"posts_no_replies\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 7, name: \"posts_with_media\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 8, name: \"is_reply\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 9, name: \"is_repost\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 10, name: \"is_quote_post\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 11, name: \"posts_with_video\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AuthorFeedItem {\n    return new AuthorFeedItem().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AuthorFeedItem {\n    return new AuthorFeedItem().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AuthorFeedItem {\n    return new AuthorFeedItem().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AuthorFeedItem | PlainMessage<AuthorFeedItem> | undefined, b: AuthorFeedItem | PlainMessage<AuthorFeedItem> | undefined): boolean {\n    return proto3.util.equals(AuthorFeedItem, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetAuthorFeedResponse\n */\nexport class GetAuthorFeedResponse extends Message<GetAuthorFeedResponse> {\n  /**\n   * @generated from field: repeated bsky.AuthorFeedItem items = 1;\n   */\n  items: AuthorFeedItem[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetAuthorFeedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetAuthorFeedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"items\", kind: \"message\", T: AuthorFeedItem, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetAuthorFeedResponse {\n    return new GetAuthorFeedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetAuthorFeedResponse {\n    return new GetAuthorFeedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetAuthorFeedResponse {\n    return new GetAuthorFeedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetAuthorFeedResponse | PlainMessage<GetAuthorFeedResponse> | undefined, b: GetAuthorFeedResponse | PlainMessage<GetAuthorFeedResponse> | undefined): boolean {\n    return proto3.util.equals(GetAuthorFeedResponse, a, b);\n  }\n}\n\n/**\n * - Returns recent posts authored by users followed by a given DID, paginated\n *     - `getTimeline`\n *\n * @generated from message bsky.GetTimelineRequest\n */\nexport class GetTimelineRequest extends Message<GetTimelineRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: bool exclude_replies = 4;\n   */\n  excludeReplies = false;\n\n  /**\n   * @generated from field: bool exclude_reposts = 5;\n   */\n  excludeReposts = false;\n\n  /**\n   * @generated from field: bool exclude_quotes = 6;\n   */\n  excludeQuotes = false;\n\n  constructor(data?: PartialMessage<GetTimelineRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetTimelineRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"exclude_replies\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 5, name: \"exclude_reposts\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 6, name: \"exclude_quotes\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetTimelineRequest {\n    return new GetTimelineRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetTimelineRequest {\n    return new GetTimelineRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetTimelineRequest {\n    return new GetTimelineRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetTimelineRequest | PlainMessage<GetTimelineRequest> | undefined, b: GetTimelineRequest | PlainMessage<GetTimelineRequest> | undefined): boolean {\n    return proto3.util.equals(GetTimelineRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetTimelineResponse\n */\nexport class GetTimelineResponse extends Message<GetTimelineResponse> {\n  /**\n   * @generated from field: repeated bsky.TimelineFeedItem items = 1;\n   */\n  items: TimelineFeedItem[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetTimelineResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetTimelineResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"items\", kind: \"message\", T: TimelineFeedItem, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetTimelineResponse {\n    return new GetTimelineResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetTimelineResponse {\n    return new GetTimelineResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetTimelineResponse {\n    return new GetTimelineResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetTimelineResponse | PlainMessage<GetTimelineResponse> | undefined, b: GetTimelineResponse | PlainMessage<GetTimelineResponse> | undefined): boolean {\n    return proto3.util.equals(GetTimelineResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TimelineFeedItem\n */\nexport class TimelineFeedItem extends Message<TimelineFeedItem> {\n  /**\n   * @generated from field: string uri = 1;\n   */\n  uri = \"\";\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  /**\n   * @generated from field: string repost = 3;\n   */\n  repost = \"\";\n\n  /**\n   * @generated from field: string repost_cid = 4;\n   */\n  repostCid = \"\";\n\n  /**\n   * @generated from field: bool is_reply = 5;\n   */\n  isReply = false;\n\n  /**\n   * @generated from field: bool is_repost = 6;\n   */\n  isRepost = false;\n\n  /**\n   * @generated from field: bool is_quote_post = 7;\n   */\n  isQuotePost = false;\n\n  constructor(data?: PartialMessage<TimelineFeedItem>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TimelineFeedItem\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"repost\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"repost_cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"is_reply\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 6, name: \"is_repost\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 7, name: \"is_quote_post\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TimelineFeedItem {\n    return new TimelineFeedItem().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TimelineFeedItem {\n    return new TimelineFeedItem().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TimelineFeedItem {\n    return new TimelineFeedItem().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TimelineFeedItem | PlainMessage<TimelineFeedItem> | undefined, b: TimelineFeedItem | PlainMessage<TimelineFeedItem> | undefined): boolean {\n    return proto3.util.equals(TimelineFeedItem, a, b);\n  }\n}\n\n/**\n * - Return recent post uris from users in list A\n *     - `getListFeed`\n *     - (This is essentially the same as `getTimeline` but instead of follows of a did, it is list items of a list)\n *\n * @generated from message bsky.GetListFeedRequest\n */\nexport class GetListFeedRequest extends Message<GetListFeedRequest> {\n  /**\n   * @generated from field: string list_uri = 1;\n   */\n  listUri = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: bool exclude_replies = 4;\n   */\n  excludeReplies = false;\n\n  /**\n   * @generated from field: bool exclude_reposts = 5;\n   */\n  excludeReposts = false;\n\n  /**\n   * @generated from field: bool exclude_quotes = 6;\n   */\n  excludeQuotes = false;\n\n  constructor(data?: PartialMessage<GetListFeedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListFeedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"list_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"exclude_replies\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 5, name: \"exclude_reposts\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 6, name: \"exclude_quotes\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListFeedRequest {\n    return new GetListFeedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListFeedRequest {\n    return new GetListFeedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListFeedRequest {\n    return new GetListFeedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListFeedRequest | PlainMessage<GetListFeedRequest> | undefined, b: GetListFeedRequest | PlainMessage<GetListFeedRequest> | undefined): boolean {\n    return proto3.util.equals(GetListFeedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetListFeedResponse\n */\nexport class GetListFeedResponse extends Message<GetListFeedResponse> {\n  /**\n   * @generated from field: repeated bsky.TimelineFeedItem items = 1;\n   */\n  items: TimelineFeedItem[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetListFeedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetListFeedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"items\", kind: \"message\", T: TimelineFeedItem, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetListFeedResponse {\n    return new GetListFeedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetListFeedResponse {\n    return new GetListFeedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetListFeedResponse {\n    return new GetListFeedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetListFeedResponse | PlainMessage<GetListFeedResponse> | undefined, b: GetListFeedResponse | PlainMessage<GetListFeedResponse> | undefined): boolean {\n    return proto3.util.equals(GetListFeedResponse, a, b);\n  }\n}\n\n/**\n * Return posts uris of any replies N levels above or M levels below post A\n *\n * @generated from message bsky.GetThreadRequest\n */\nexport class GetThreadRequest extends Message<GetThreadRequest> {\n  /**\n   * @generated from field: string post_uri = 1;\n   */\n  postUri = \"\";\n\n  /**\n   * @generated from field: int32 above = 2;\n   */\n  above = 0;\n\n  /**\n   * @generated from field: int32 below = 3;\n   */\n  below = 0;\n\n  constructor(data?: PartialMessage<GetThreadRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetThreadRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"post_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"above\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"below\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetThreadRequest {\n    return new GetThreadRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetThreadRequest {\n    return new GetThreadRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetThreadRequest {\n    return new GetThreadRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetThreadRequest | PlainMessage<GetThreadRequest> | undefined, b: GetThreadRequest | PlainMessage<GetThreadRequest> | undefined): boolean {\n    return proto3.util.equals(GetThreadRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetThreadResponse\n */\nexport class GetThreadResponse extends Message<GetThreadResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetThreadResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetThreadResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetThreadResponse {\n    return new GetThreadResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetThreadResponse {\n    return new GetThreadResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetThreadResponse {\n    return new GetThreadResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetThreadResponse | PlainMessage<GetThreadResponse> | undefined, b: GetThreadResponse | PlainMessage<GetThreadResponse> | undefined): boolean {\n    return proto3.util.equals(GetThreadResponse, a, b);\n  }\n}\n\n/**\n * - Return DIDs of actors matching term, paginated\n *     - `searchActors` skeleton\n *\n * @generated from message bsky.SearchActorsRequest\n */\nexport class SearchActorsRequest extends Message<SearchActorsRequest> {\n  /**\n   * @generated from field: string term = 1;\n   */\n  term = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<SearchActorsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchActorsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"term\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchActorsRequest {\n    return new SearchActorsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchActorsRequest {\n    return new SearchActorsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchActorsRequest {\n    return new SearchActorsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchActorsRequest | PlainMessage<SearchActorsRequest> | undefined, b: SearchActorsRequest | PlainMessage<SearchActorsRequest> | undefined): boolean {\n    return proto3.util.equals(SearchActorsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.SearchActorsResponse\n */\nexport class SearchActorsResponse extends Message<SearchActorsResponse> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<SearchActorsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchActorsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchActorsResponse {\n    return new SearchActorsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchActorsResponse {\n    return new SearchActorsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchActorsResponse {\n    return new SearchActorsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchActorsResponse | PlainMessage<SearchActorsResponse> | undefined, b: SearchActorsResponse | PlainMessage<SearchActorsResponse> | undefined): boolean {\n    return proto3.util.equals(SearchActorsResponse, a, b);\n  }\n}\n\n/**\n * - Return uris of posts matching term, paginated\n *     - `searchPosts` skeleton\n *\n * @generated from message bsky.SearchPostsRequest\n */\nexport class SearchPostsRequest extends Message<SearchPostsRequest> {\n  /**\n   * @generated from field: string term = 1;\n   */\n  term = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<SearchPostsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchPostsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"term\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchPostsRequest {\n    return new SearchPostsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchPostsRequest {\n    return new SearchPostsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchPostsRequest {\n    return new SearchPostsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchPostsRequest | PlainMessage<SearchPostsRequest> | undefined, b: SearchPostsRequest | PlainMessage<SearchPostsRequest> | undefined): boolean {\n    return proto3.util.equals(SearchPostsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.SearchPostsResponse\n */\nexport class SearchPostsResponse extends Message<SearchPostsResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<SearchPostsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchPostsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchPostsResponse {\n    return new SearchPostsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchPostsResponse {\n    return new SearchPostsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchPostsResponse {\n    return new SearchPostsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchPostsResponse | PlainMessage<SearchPostsResponse> | undefined, b: SearchPostsResponse | PlainMessage<SearchPostsResponse> | undefined): boolean {\n    return proto3.util.equals(SearchPostsResponse, a, b);\n  }\n}\n\n/**\n * - Return uris of starter packs matching term, paginated\n *     - `searchStarterPacks` skeleton\n *\n * @generated from message bsky.SearchStarterPacksRequest\n */\nexport class SearchStarterPacksRequest extends Message<SearchStarterPacksRequest> {\n  /**\n   * @generated from field: string term = 1;\n   */\n  term = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<SearchStarterPacksRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchStarterPacksRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"term\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchStarterPacksRequest {\n    return new SearchStarterPacksRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchStarterPacksRequest {\n    return new SearchStarterPacksRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchStarterPacksRequest {\n    return new SearchStarterPacksRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchStarterPacksRequest | PlainMessage<SearchStarterPacksRequest> | undefined, b: SearchStarterPacksRequest | PlainMessage<SearchStarterPacksRequest> | undefined): boolean {\n    return proto3.util.equals(SearchStarterPacksRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.SearchStarterPacksResponse\n */\nexport class SearchStarterPacksResponse extends Message<SearchStarterPacksResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<SearchStarterPacksResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SearchStarterPacksResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SearchStarterPacksResponse {\n    return new SearchStarterPacksResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SearchStarterPacksResponse {\n    return new SearchStarterPacksResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SearchStarterPacksResponse {\n    return new SearchStarterPacksResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SearchStarterPacksResponse | PlainMessage<SearchStarterPacksResponse> | undefined, b: SearchStarterPacksResponse | PlainMessage<SearchStarterPacksResponse> | undefined): boolean {\n    return proto3.util.equals(SearchStarterPacksResponse, a, b);\n  }\n}\n\n/**\n * - Return DIDs of suggested follows for a user, excluding anyone they already follow\n *     - `getSuggestions`, `getSuggestedFollowsByActor`\n *\n * @generated from message bsky.GetFollowSuggestionsRequest\n */\nexport class GetFollowSuggestionsRequest extends Message<GetFollowSuggestionsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string relative_to_did = 2;\n   */\n  relativeToDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 3;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 4;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetFollowSuggestionsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowSuggestionsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"relative_to_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 4, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowSuggestionsRequest {\n    return new GetFollowSuggestionsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowSuggestionsRequest {\n    return new GetFollowSuggestionsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowSuggestionsRequest {\n    return new GetFollowSuggestionsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowSuggestionsRequest | PlainMessage<GetFollowSuggestionsRequest> | undefined, b: GetFollowSuggestionsRequest | PlainMessage<GetFollowSuggestionsRequest> | undefined): boolean {\n    return proto3.util.equals(GetFollowSuggestionsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFollowSuggestionsResponse\n */\nexport class GetFollowSuggestionsResponse extends Message<GetFollowSuggestionsResponse> {\n  /**\n   * @generated from field: repeated string dids = 1;\n   */\n  dids: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetFollowSuggestionsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowSuggestionsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowSuggestionsResponse {\n    return new GetFollowSuggestionsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowSuggestionsResponse {\n    return new GetFollowSuggestionsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowSuggestionsResponse {\n    return new GetFollowSuggestionsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowSuggestionsResponse | PlainMessage<GetFollowSuggestionsResponse> | undefined, b: GetFollowSuggestionsResponse | PlainMessage<GetFollowSuggestionsResponse> | undefined): boolean {\n    return proto3.util.equals(GetFollowSuggestionsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.SuggestedEntity\n */\nexport class SuggestedEntity extends Message<SuggestedEntity> {\n  /**\n   * @generated from field: string tag = 1;\n   */\n  tag = \"\";\n\n  /**\n   * @generated from field: string subject = 2;\n   */\n  subject = \"\";\n\n  /**\n   * @generated from field: string subject_type = 3;\n   */\n  subjectType = \"\";\n\n  /**\n   * @generated from field: int64 priority = 4;\n   */\n  priority = protoInt64.zero;\n\n  constructor(data?: PartialMessage<SuggestedEntity>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.SuggestedEntity\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"tag\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject_type\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"priority\", kind: \"scalar\", T: 3 /* ScalarType.INT64 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SuggestedEntity {\n    return new SuggestedEntity().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SuggestedEntity {\n    return new SuggestedEntity().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SuggestedEntity {\n    return new SuggestedEntity().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SuggestedEntity | PlainMessage<SuggestedEntity> | undefined, b: SuggestedEntity | PlainMessage<SuggestedEntity> | undefined): boolean {\n    return proto3.util.equals(SuggestedEntity, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetSuggestedEntitiesRequest\n */\nexport class GetSuggestedEntitiesRequest extends Message<GetSuggestedEntitiesRequest> {\n  /**\n   * @generated from field: int32 limit = 1;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetSuggestedEntitiesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSuggestedEntitiesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSuggestedEntitiesRequest {\n    return new GetSuggestedEntitiesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSuggestedEntitiesRequest {\n    return new GetSuggestedEntitiesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSuggestedEntitiesRequest {\n    return new GetSuggestedEntitiesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSuggestedEntitiesRequest | PlainMessage<GetSuggestedEntitiesRequest> | undefined, b: GetSuggestedEntitiesRequest | PlainMessage<GetSuggestedEntitiesRequest> | undefined): boolean {\n    return proto3.util.equals(GetSuggestedEntitiesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetSuggestedEntitiesResponse\n */\nexport class GetSuggestedEntitiesResponse extends Message<GetSuggestedEntitiesResponse> {\n  /**\n   * @generated from field: repeated bsky.SuggestedEntity entities = 1;\n   */\n  entities: SuggestedEntity[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetSuggestedEntitiesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSuggestedEntitiesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"entities\", kind: \"message\", T: SuggestedEntity, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSuggestedEntitiesResponse {\n    return new GetSuggestedEntitiesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSuggestedEntitiesResponse {\n    return new GetSuggestedEntitiesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSuggestedEntitiesResponse {\n    return new GetSuggestedEntitiesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSuggestedEntitiesResponse | PlainMessage<GetSuggestedEntitiesResponse> | undefined, b: GetSuggestedEntitiesResponse | PlainMessage<GetSuggestedEntitiesResponse> | undefined): boolean {\n    return proto3.util.equals(GetSuggestedEntitiesResponse, a, b);\n  }\n}\n\n/**\n * - Get all labels on a subjects A, B, C (uri or did) issued by dids D, E, F…\n *     - label hydration on nearly every view\n *\n * @generated from message bsky.GetLabelsRequest\n */\nexport class GetLabelsRequest extends Message<GetLabelsRequest> {\n  /**\n   * @generated from field: repeated string subjects = 1;\n   */\n  subjects: string[] = [];\n\n  /**\n   * @generated from field: repeated string issuers = 2;\n   */\n  issuers: string[] = [];\n\n  constructor(data?: PartialMessage<GetLabelsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLabelsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subjects\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"issuers\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLabelsRequest {\n    return new GetLabelsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLabelsRequest {\n    return new GetLabelsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLabelsRequest {\n    return new GetLabelsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLabelsRequest | PlainMessage<GetLabelsRequest> | undefined, b: GetLabelsRequest | PlainMessage<GetLabelsRequest> | undefined): boolean {\n    return proto3.util.equals(GetLabelsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLabelsResponse\n */\nexport class GetLabelsResponse extends Message<GetLabelsResponse> {\n  /**\n   * @generated from field: repeated bytes labels = 1;\n   */\n  labels: Uint8Array[] = [];\n\n  constructor(data?: PartialMessage<GetLabelsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLabelsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"labels\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLabelsResponse {\n    return new GetLabelsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLabelsResponse {\n    return new GetLabelsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLabelsResponse {\n    return new GetLabelsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLabelsResponse | PlainMessage<GetLabelsResponse> | undefined, b: GetLabelsResponse | PlainMessage<GetLabelsResponse> | undefined): boolean {\n    return proto3.util.equals(GetLabelsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorStarterPacksRequest\n */\nexport class GetActorStarterPacksRequest extends Message<GetActorStarterPacksRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorStarterPacksRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorStarterPacksRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorStarterPacksRequest {\n    return new GetActorStarterPacksRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorStarterPacksRequest {\n    return new GetActorStarterPacksRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorStarterPacksRequest {\n    return new GetActorStarterPacksRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorStarterPacksRequest | PlainMessage<GetActorStarterPacksRequest> | undefined, b: GetActorStarterPacksRequest | PlainMessage<GetActorStarterPacksRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorStarterPacksRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorStarterPacksResponse\n */\nexport class GetActorStarterPacksResponse extends Message<GetActorStarterPacksResponse> {\n  /**\n   * @generated from field: repeated string uris = 1;\n   */\n  uris: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorStarterPacksResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorStarterPacksResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorStarterPacksResponse {\n    return new GetActorStarterPacksResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorStarterPacksResponse {\n    return new GetActorStarterPacksResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorStarterPacksResponse {\n    return new GetActorStarterPacksResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorStarterPacksResponse | PlainMessage<GetActorStarterPacksResponse> | undefined, b: GetActorStarterPacksResponse | PlainMessage<GetActorStarterPacksResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorStarterPacksResponse, a, b);\n  }\n}\n\n/**\n * - Latest repo rev of user w/ DID\n *     - Read-after-write header in`getProfile`, `getProfiles`, `getActorLikes`, `getAuthorFeed`, `getListFeed`, `getPostThread`, `getTimeline`.  Could it be view dependent?\n *\n * @generated from message bsky.GetLatestRevRequest\n */\nexport class GetLatestRevRequest extends Message<GetLatestRevRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  constructor(data?: PartialMessage<GetLatestRevRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLatestRevRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLatestRevRequest {\n    return new GetLatestRevRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLatestRevRequest {\n    return new GetLatestRevRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLatestRevRequest {\n    return new GetLatestRevRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLatestRevRequest | PlainMessage<GetLatestRevRequest> | undefined, b: GetLatestRevRequest | PlainMessage<GetLatestRevRequest> | undefined): boolean {\n    return proto3.util.equals(GetLatestRevRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetLatestRevResponse\n */\nexport class GetLatestRevResponse extends Message<GetLatestRevResponse> {\n  /**\n   * @generated from field: string rev = 1;\n   */\n  rev = \"\";\n\n  constructor(data?: PartialMessage<GetLatestRevResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetLatestRevResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"rev\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetLatestRevResponse {\n    return new GetLatestRevResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetLatestRevResponse {\n    return new GetLatestRevResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetLatestRevResponse {\n    return new GetLatestRevResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetLatestRevResponse | PlainMessage<GetLatestRevResponse> | undefined, b: GetLatestRevResponse | PlainMessage<GetLatestRevResponse> | undefined): boolean {\n    return proto3.util.equals(GetLatestRevResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetIdentityByDidRequest\n */\nexport class GetIdentityByDidRequest extends Message<GetIdentityByDidRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  constructor(data?: PartialMessage<GetIdentityByDidRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetIdentityByDidRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetIdentityByDidRequest {\n    return new GetIdentityByDidRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetIdentityByDidRequest {\n    return new GetIdentityByDidRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetIdentityByDidRequest {\n    return new GetIdentityByDidRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetIdentityByDidRequest | PlainMessage<GetIdentityByDidRequest> | undefined, b: GetIdentityByDidRequest | PlainMessage<GetIdentityByDidRequest> | undefined): boolean {\n    return proto3.util.equals(GetIdentityByDidRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetIdentityByDidResponse\n */\nexport class GetIdentityByDidResponse extends Message<GetIdentityByDidResponse> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string handle = 2;\n   */\n  handle = \"\";\n\n  /**\n   * @generated from field: bytes keys = 3;\n   */\n  keys = new Uint8Array(0);\n\n  /**\n   * @generated from field: bytes services = 4;\n   */\n  services = new Uint8Array(0);\n\n  /**\n   * @generated from field: google.protobuf.Timestamp updated = 5;\n   */\n  updated?: Timestamp;\n\n  constructor(data?: PartialMessage<GetIdentityByDidResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetIdentityByDidResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"handle\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"keys\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n    { no: 4, name: \"services\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n    { no: 5, name: \"updated\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetIdentityByDidResponse {\n    return new GetIdentityByDidResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetIdentityByDidResponse {\n    return new GetIdentityByDidResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetIdentityByDidResponse {\n    return new GetIdentityByDidResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetIdentityByDidResponse | PlainMessage<GetIdentityByDidResponse> | undefined, b: GetIdentityByDidResponse | PlainMessage<GetIdentityByDidResponse> | undefined): boolean {\n    return proto3.util.equals(GetIdentityByDidResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetIdentityByHandleRequest\n */\nexport class GetIdentityByHandleRequest extends Message<GetIdentityByHandleRequest> {\n  /**\n   * @generated from field: string handle = 1;\n   */\n  handle = \"\";\n\n  constructor(data?: PartialMessage<GetIdentityByHandleRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetIdentityByHandleRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"handle\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetIdentityByHandleRequest {\n    return new GetIdentityByHandleRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetIdentityByHandleRequest {\n    return new GetIdentityByHandleRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetIdentityByHandleRequest {\n    return new GetIdentityByHandleRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetIdentityByHandleRequest | PlainMessage<GetIdentityByHandleRequest> | undefined, b: GetIdentityByHandleRequest | PlainMessage<GetIdentityByHandleRequest> | undefined): boolean {\n    return proto3.util.equals(GetIdentityByHandleRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetIdentityByHandleResponse\n */\nexport class GetIdentityByHandleResponse extends Message<GetIdentityByHandleResponse> {\n  /**\n   * @generated from field: string handle = 1;\n   */\n  handle = \"\";\n\n  /**\n   * @generated from field: string did = 2;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: bytes keys = 3;\n   */\n  keys = new Uint8Array(0);\n\n  /**\n   * @generated from field: bytes services = 4;\n   */\n  services = new Uint8Array(0);\n\n  /**\n   * @generated from field: google.protobuf.Timestamp updated = 5;\n   */\n  updated?: Timestamp;\n\n  constructor(data?: PartialMessage<GetIdentityByHandleResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetIdentityByHandleResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"handle\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"keys\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n    { no: 4, name: \"services\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n    { no: 5, name: \"updated\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetIdentityByHandleResponse {\n    return new GetIdentityByHandleResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetIdentityByHandleResponse {\n    return new GetIdentityByHandleResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetIdentityByHandleResponse {\n    return new GetIdentityByHandleResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetIdentityByHandleResponse | PlainMessage<GetIdentityByHandleResponse> | undefined, b: GetIdentityByHandleResponse | PlainMessage<GetIdentityByHandleResponse> | undefined): boolean {\n    return proto3.util.equals(GetIdentityByHandleResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlobTakedownRequest\n */\nexport class GetBlobTakedownRequest extends Message<GetBlobTakedownRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  constructor(data?: PartialMessage<GetBlobTakedownRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlobTakedownRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlobTakedownRequest {\n    return new GetBlobTakedownRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlobTakedownRequest {\n    return new GetBlobTakedownRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlobTakedownRequest {\n    return new GetBlobTakedownRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlobTakedownRequest | PlainMessage<GetBlobTakedownRequest> | undefined, b: GetBlobTakedownRequest | PlainMessage<GetBlobTakedownRequest> | undefined): boolean {\n    return proto3.util.equals(GetBlobTakedownRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBlobTakedownResponse\n */\nexport class GetBlobTakedownResponse extends Message<GetBlobTakedownResponse> {\n  /**\n   * @generated from field: bool taken_down = 1;\n   */\n  takenDown = false;\n\n  /**\n   * @generated from field: string takedown_ref = 2;\n   */\n  takedownRef = \"\";\n\n  constructor(data?: PartialMessage<GetBlobTakedownResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBlobTakedownResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"taken_down\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 2, name: \"takedown_ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBlobTakedownResponse {\n    return new GetBlobTakedownResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBlobTakedownResponse {\n    return new GetBlobTakedownResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBlobTakedownResponse {\n    return new GetBlobTakedownResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBlobTakedownResponse | PlainMessage<GetBlobTakedownResponse> | undefined, b: GetBlobTakedownResponse | PlainMessage<GetBlobTakedownResponse> | undefined): boolean {\n    return proto3.util.equals(GetBlobTakedownResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorTakedownRequest\n */\nexport class GetActorTakedownRequest extends Message<GetActorTakedownRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  constructor(data?: PartialMessage<GetActorTakedownRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorTakedownRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorTakedownRequest {\n    return new GetActorTakedownRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorTakedownRequest {\n    return new GetActorTakedownRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorTakedownRequest {\n    return new GetActorTakedownRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorTakedownRequest | PlainMessage<GetActorTakedownRequest> | undefined, b: GetActorTakedownRequest | PlainMessage<GetActorTakedownRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorTakedownRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorTakedownResponse\n */\nexport class GetActorTakedownResponse extends Message<GetActorTakedownResponse> {\n  /**\n   * @generated from field: bool taken_down = 1;\n   */\n  takenDown = false;\n\n  /**\n   * @generated from field: string takedown_ref = 2;\n   */\n  takedownRef = \"\";\n\n  constructor(data?: PartialMessage<GetActorTakedownResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorTakedownResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"taken_down\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 2, name: \"takedown_ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorTakedownResponse {\n    return new GetActorTakedownResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorTakedownResponse {\n    return new GetActorTakedownResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorTakedownResponse {\n    return new GetActorTakedownResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorTakedownResponse | PlainMessage<GetActorTakedownResponse> | undefined, b: GetActorTakedownResponse | PlainMessage<GetActorTakedownResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorTakedownResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRecordTakedownRequest\n */\nexport class GetRecordTakedownRequest extends Message<GetRecordTakedownRequest> {\n  /**\n   * @generated from field: string record_uri = 1;\n   */\n  recordUri = \"\";\n\n  constructor(data?: PartialMessage<GetRecordTakedownRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRecordTakedownRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"record_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRecordTakedownRequest {\n    return new GetRecordTakedownRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRecordTakedownRequest {\n    return new GetRecordTakedownRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRecordTakedownRequest {\n    return new GetRecordTakedownRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRecordTakedownRequest | PlainMessage<GetRecordTakedownRequest> | undefined, b: GetRecordTakedownRequest | PlainMessage<GetRecordTakedownRequest> | undefined): boolean {\n    return proto3.util.equals(GetRecordTakedownRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetRecordTakedownResponse\n */\nexport class GetRecordTakedownResponse extends Message<GetRecordTakedownResponse> {\n  /**\n   * @generated from field: bool taken_down = 1;\n   */\n  takenDown = false;\n\n  /**\n   * @generated from field: string takedown_ref = 2;\n   */\n  takedownRef = \"\";\n\n  constructor(data?: PartialMessage<GetRecordTakedownResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetRecordTakedownResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"taken_down\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 2, name: \"takedown_ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetRecordTakedownResponse {\n    return new GetRecordTakedownResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetRecordTakedownResponse {\n    return new GetRecordTakedownResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetRecordTakedownResponse {\n    return new GetRecordTakedownResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetRecordTakedownResponse | PlainMessage<GetRecordTakedownResponse> | undefined, b: GetRecordTakedownResponse | PlainMessage<GetRecordTakedownResponse> | undefined): boolean {\n    return proto3.util.equals(GetRecordTakedownResponse, a, b);\n  }\n}\n\n/**\n *\n * Bookmarks\n *\n *\n * @generated from message bsky.Bookmark\n */\nexport class Bookmark extends Message<Bookmark> {\n  /**\n   * @generated from field: bsky.StashRef ref = 1;\n   */\n  ref?: StashRef;\n\n  /**\n   * @generated from field: string subject_uri = 2;\n   */\n  subjectUri = \"\";\n\n  /**\n   * @generated from field: string subject_cid = 3;\n   */\n  subjectCid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp indexed_at = 4;\n   */\n  indexedAt?: Timestamp;\n\n  constructor(data?: PartialMessage<Bookmark>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.Bookmark\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"ref\", kind: \"message\", T: StashRef },\n    { no: 2, name: \"subject_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject_cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"indexed_at\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Bookmark {\n    return new Bookmark().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Bookmark {\n    return new Bookmark().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Bookmark {\n    return new Bookmark().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Bookmark | PlainMessage<Bookmark> | undefined, b: Bookmark | PlainMessage<Bookmark> | undefined): boolean {\n    return proto3.util.equals(Bookmark, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBookmarksByActorAndSubjectsRequest\n */\nexport class GetBookmarksByActorAndSubjectsRequest extends Message<GetBookmarksByActorAndSubjectsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string uris = 2;\n   */\n  uris: string[] = [];\n\n  constructor(data?: PartialMessage<GetBookmarksByActorAndSubjectsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBookmarksByActorAndSubjectsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"uris\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBookmarksByActorAndSubjectsRequest {\n    return new GetBookmarksByActorAndSubjectsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBookmarksByActorAndSubjectsRequest {\n    return new GetBookmarksByActorAndSubjectsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBookmarksByActorAndSubjectsRequest {\n    return new GetBookmarksByActorAndSubjectsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBookmarksByActorAndSubjectsRequest | PlainMessage<GetBookmarksByActorAndSubjectsRequest> | undefined, b: GetBookmarksByActorAndSubjectsRequest | PlainMessage<GetBookmarksByActorAndSubjectsRequest> | undefined): boolean {\n    return proto3.util.equals(GetBookmarksByActorAndSubjectsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetBookmarksByActorAndSubjectsResponse\n */\nexport class GetBookmarksByActorAndSubjectsResponse extends Message<GetBookmarksByActorAndSubjectsResponse> {\n  /**\n   * @generated from field: repeated bsky.Bookmark bookmarks = 1;\n   */\n  bookmarks: Bookmark[] = [];\n\n  constructor(data?: PartialMessage<GetBookmarksByActorAndSubjectsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetBookmarksByActorAndSubjectsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"bookmarks\", kind: \"message\", T: Bookmark, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetBookmarksByActorAndSubjectsResponse {\n    return new GetBookmarksByActorAndSubjectsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetBookmarksByActorAndSubjectsResponse {\n    return new GetBookmarksByActorAndSubjectsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetBookmarksByActorAndSubjectsResponse {\n    return new GetBookmarksByActorAndSubjectsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetBookmarksByActorAndSubjectsResponse | PlainMessage<GetBookmarksByActorAndSubjectsResponse> | undefined, b: GetBookmarksByActorAndSubjectsResponse | PlainMessage<GetBookmarksByActorAndSubjectsResponse> | undefined): boolean {\n    return proto3.util.equals(GetBookmarksByActorAndSubjectsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorBookmarksRequest\n */\nexport class GetActorBookmarksRequest extends Message<GetActorBookmarksRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorBookmarksRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorBookmarksRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorBookmarksRequest {\n    return new GetActorBookmarksRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorBookmarksRequest {\n    return new GetActorBookmarksRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorBookmarksRequest {\n    return new GetActorBookmarksRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorBookmarksRequest | PlainMessage<GetActorBookmarksRequest> | undefined, b: GetActorBookmarksRequest | PlainMessage<GetActorBookmarksRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorBookmarksRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.BookmarkInfo\n */\nexport class BookmarkInfo extends Message<BookmarkInfo> {\n  /**\n   * stash key\n   *\n   * @generated from field: string key = 1;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: string subject = 2;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<BookmarkInfo>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.BookmarkInfo\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): BookmarkInfo {\n    return new BookmarkInfo().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): BookmarkInfo {\n    return new BookmarkInfo().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): BookmarkInfo {\n    return new BookmarkInfo().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: BookmarkInfo | PlainMessage<BookmarkInfo> | undefined, b: BookmarkInfo | PlainMessage<BookmarkInfo> | undefined): boolean {\n    return proto3.util.equals(BookmarkInfo, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorBookmarksResponse\n */\nexport class GetActorBookmarksResponse extends Message<GetActorBookmarksResponse> {\n  /**\n   * @generated from field: repeated bsky.BookmarkInfo bookmarks = 1;\n   */\n  bookmarks: BookmarkInfo[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorBookmarksResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorBookmarksResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"bookmarks\", kind: \"message\", T: BookmarkInfo, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorBookmarksResponse {\n    return new GetActorBookmarksResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorBookmarksResponse {\n    return new GetActorBookmarksResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorBookmarksResponse {\n    return new GetActorBookmarksResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorBookmarksResponse | PlainMessage<GetActorBookmarksResponse> | undefined, b: GetActorBookmarksResponse | PlainMessage<GetActorBookmarksResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorBookmarksResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorDraftsRequest\n */\nexport class GetActorDraftsRequest extends Message<GetActorDraftsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorDraftsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorDraftsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorDraftsRequest {\n    return new GetActorDraftsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorDraftsRequest {\n    return new GetActorDraftsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorDraftsRequest {\n    return new GetActorDraftsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorDraftsRequest | PlainMessage<GetActorDraftsRequest> | undefined, b: GetActorDraftsRequest | PlainMessage<GetActorDraftsRequest> | undefined): boolean {\n    return proto3.util.equals(GetActorDraftsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DraftInfo\n */\nexport class DraftInfo extends Message<DraftInfo> {\n  /**\n   * stash key\n   *\n   * @generated from field: string key = 1;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp created_at = 2;\n   */\n  createdAt?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp updated_at = 3;\n   */\n  updatedAt?: Timestamp;\n\n  /**\n   * @generated from field: bytes payload = 4;\n   */\n  payload = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<DraftInfo>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DraftInfo\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"created_at\", kind: \"message\", T: Timestamp },\n    { no: 3, name: \"updated_at\", kind: \"message\", T: Timestamp },\n    { no: 4, name: \"payload\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DraftInfo {\n    return new DraftInfo().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DraftInfo {\n    return new DraftInfo().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DraftInfo {\n    return new DraftInfo().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DraftInfo | PlainMessage<DraftInfo> | undefined, b: DraftInfo | PlainMessage<DraftInfo> | undefined): boolean {\n    return proto3.util.equals(DraftInfo, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetActorDraftsResponse\n */\nexport class GetActorDraftsResponse extends Message<GetActorDraftsResponse> {\n  /**\n   * @generated from field: repeated bsky.DraftInfo drafts = 1;\n   */\n  drafts: DraftInfo[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetActorDraftsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetActorDraftsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"drafts\", kind: \"message\", T: DraftInfo, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetActorDraftsResponse {\n    return new GetActorDraftsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetActorDraftsResponse {\n    return new GetActorDraftsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetActorDraftsResponse {\n    return new GetActorDraftsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetActorDraftsResponse | PlainMessage<GetActorDraftsResponse> | undefined, b: GetActorDraftsResponse | PlainMessage<GetActorDraftsResponse> | undefined): boolean {\n    return proto3.util.equals(GetActorDraftsResponse, a, b);\n  }\n}\n\n/**\n * GetFollowsFollowing gets the list of DIDs that the actor follows that also follow the targets\n *\n * @generated from message bsky.GetFollowsFollowingRequest\n */\nexport class GetFollowsFollowingRequest extends Message<GetFollowsFollowingRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: repeated string target_dids = 2;\n   */\n  targetDids: string[] = [];\n\n  constructor(data?: PartialMessage<GetFollowsFollowingRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowsFollowingRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"target_dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowsFollowingRequest {\n    return new GetFollowsFollowingRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowsFollowingRequest {\n    return new GetFollowsFollowingRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowsFollowingRequest {\n    return new GetFollowsFollowingRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowsFollowingRequest | PlainMessage<GetFollowsFollowingRequest> | undefined, b: GetFollowsFollowingRequest | PlainMessage<GetFollowsFollowingRequest> | undefined): boolean {\n    return proto3.util.equals(GetFollowsFollowingRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.FollowsFollowing\n */\nexport class FollowsFollowing extends Message<FollowsFollowing> {\n  /**\n   * @generated from field: string target_did = 1;\n   */\n  targetDid = \"\";\n\n  /**\n   * @generated from field: repeated string dids = 2;\n   */\n  dids: string[] = [];\n\n  constructor(data?: PartialMessage<FollowsFollowing>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.FollowsFollowing\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"target_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"dids\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): FollowsFollowing {\n    return new FollowsFollowing().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): FollowsFollowing {\n    return new FollowsFollowing().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): FollowsFollowing {\n    return new FollowsFollowing().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: FollowsFollowing | PlainMessage<FollowsFollowing> | undefined, b: FollowsFollowing | PlainMessage<FollowsFollowing> | undefined): boolean {\n    return proto3.util.equals(FollowsFollowing, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetFollowsFollowingResponse\n */\nexport class GetFollowsFollowingResponse extends Message<GetFollowsFollowingResponse> {\n  /**\n   * @generated from field: repeated bsky.FollowsFollowing results = 1;\n   */\n  results: FollowsFollowing[] = [];\n\n  constructor(data?: PartialMessage<GetFollowsFollowingResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetFollowsFollowingResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"results\", kind: \"message\", T: FollowsFollowing, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetFollowsFollowingResponse {\n    return new GetFollowsFollowingResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetFollowsFollowingResponse {\n    return new GetFollowsFollowingResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetFollowsFollowingResponse {\n    return new GetFollowsFollowingResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetFollowsFollowingResponse | PlainMessage<GetFollowsFollowingResponse> | undefined, b: GetFollowsFollowingResponse | PlainMessage<GetFollowsFollowingResponse> | undefined): boolean {\n    return proto3.util.equals(GetFollowsFollowingResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetSitemapIndexRequest\n */\nexport class GetSitemapIndexRequest extends Message<GetSitemapIndexRequest> {\n  /**\n   * @generated from field: bsky.SitemapPageType type = 1;\n   */\n  type = SitemapPageType.UNSPECIFIED;\n\n  constructor(data?: PartialMessage<GetSitemapIndexRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSitemapIndexRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"type\", kind: \"enum\", T: proto3.getEnumType(SitemapPageType) },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSitemapIndexRequest {\n    return new GetSitemapIndexRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSitemapIndexRequest {\n    return new GetSitemapIndexRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSitemapIndexRequest {\n    return new GetSitemapIndexRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSitemapIndexRequest | PlainMessage<GetSitemapIndexRequest> | undefined, b: GetSitemapIndexRequest | PlainMessage<GetSitemapIndexRequest> | undefined): boolean {\n    return proto3.util.equals(GetSitemapIndexRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetSitemapIndexResponse\n */\nexport class GetSitemapIndexResponse extends Message<GetSitemapIndexResponse> {\n  /**\n   * GZIP compressed XML sitemap\n   *\n   * @generated from field: bytes sitemap = 1;\n   */\n  sitemap = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<GetSitemapIndexResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSitemapIndexResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"sitemap\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSitemapIndexResponse {\n    return new GetSitemapIndexResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSitemapIndexResponse {\n    return new GetSitemapIndexResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSitemapIndexResponse {\n    return new GetSitemapIndexResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSitemapIndexResponse | PlainMessage<GetSitemapIndexResponse> | undefined, b: GetSitemapIndexResponse | PlainMessage<GetSitemapIndexResponse> | undefined): boolean {\n    return proto3.util.equals(GetSitemapIndexResponse, a, b);\n  }\n}\n\n/**\n * Sitemap HTTP paths are typically of the form `/type/yyyy-mm-dd/N.xml.gz`, i.e. `/users/2025-01-01/1.xml.gz`\n *\n * @generated from message bsky.GetSitemapPageRequest\n */\nexport class GetSitemapPageRequest extends Message<GetSitemapPageRequest> {\n  /**\n   * @generated from field: bsky.SitemapPageType type = 1;\n   */\n  type = SitemapPageType.UNSPECIFIED;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp date = 2;\n   */\n  date?: Timestamp;\n\n  /**\n   * One-indexed\n   *\n   * @generated from field: int32 bucket = 3;\n   */\n  bucket = 0;\n\n  constructor(data?: PartialMessage<GetSitemapPageRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSitemapPageRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"type\", kind: \"enum\", T: proto3.getEnumType(SitemapPageType) },\n    { no: 2, name: \"date\", kind: \"message\", T: Timestamp },\n    { no: 3, name: \"bucket\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSitemapPageRequest {\n    return new GetSitemapPageRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSitemapPageRequest {\n    return new GetSitemapPageRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSitemapPageRequest {\n    return new GetSitemapPageRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSitemapPageRequest | PlainMessage<GetSitemapPageRequest> | undefined, b: GetSitemapPageRequest | PlainMessage<GetSitemapPageRequest> | undefined): boolean {\n    return proto3.util.equals(GetSitemapPageRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.GetSitemapPageResponse\n */\nexport class GetSitemapPageResponse extends Message<GetSitemapPageResponse> {\n  /**\n   * GZIP compressed XML sitemap\n   *\n   * @generated from field: bytes sitemap = 1;\n   */\n  sitemap = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<GetSitemapPageResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.GetSitemapPageResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"sitemap\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSitemapPageResponse {\n    return new GetSitemapPageResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSitemapPageResponse {\n    return new GetSitemapPageResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSitemapPageResponse {\n    return new GetSitemapPageResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSitemapPageResponse | PlainMessage<GetSitemapPageResponse> | undefined, b: GetSitemapPageResponse | PlainMessage<GetSitemapPageResponse> | undefined): boolean {\n    return proto3.util.equals(GetSitemapPageResponse, a, b);\n  }\n}\n\n/**\n * Ping\n *\n * @generated from message bsky.PingRequest\n */\nexport class PingRequest extends Message<PingRequest> {\n  constructor(data?: PartialMessage<PingRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.PingRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingRequest {\n    return new PingRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingRequest | PlainMessage<PingRequest> | undefined, b: PingRequest | PlainMessage<PingRequest> | undefined): boolean {\n    return proto3.util.equals(PingRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.PingResponse\n */\nexport class PingResponse extends Message<PingResponse> {\n  constructor(data?: PartialMessage<PingResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.PingResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingResponse {\n    return new PingResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingResponse | PlainMessage<PingResponse> | undefined, b: PingResponse | PlainMessage<PingResponse> | undefined): boolean {\n    return proto3.util.equals(PingResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.StashRef\n */\nexport class StashRef extends Message<StashRef> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 2;\n   */\n  namespace = \"\";\n\n  /**\n   * @generated from field: string key = 3;\n   */\n  key = \"\";\n\n  constructor(data?: PartialMessage<StashRef>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.StashRef\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): StashRef {\n    return new StashRef().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): StashRef {\n    return new StashRef().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StashRef {\n    return new StashRef().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: StashRef | PlainMessage<StashRef> | undefined, b: StashRef | PlainMessage<StashRef> | undefined): boolean {\n    return proto3.util.equals(StashRef, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UpdateActorUpstreamStatusRequest\n */\nexport class UpdateActorUpstreamStatusRequest extends Message<UpdateActorUpstreamStatusRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: bool active = 2;\n   */\n  active = false;\n\n  /**\n   * @generated from field: string upstream_status = 3;\n   */\n  upstreamStatus = \"\";\n\n  constructor(data?: PartialMessage<UpdateActorUpstreamStatusRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UpdateActorUpstreamStatusRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"active\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 3, name: \"upstream_status\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateActorUpstreamStatusRequest {\n    return new UpdateActorUpstreamStatusRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateActorUpstreamStatusRequest {\n    return new UpdateActorUpstreamStatusRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateActorUpstreamStatusRequest {\n    return new UpdateActorUpstreamStatusRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UpdateActorUpstreamStatusRequest | PlainMessage<UpdateActorUpstreamStatusRequest> | undefined, b: UpdateActorUpstreamStatusRequest | PlainMessage<UpdateActorUpstreamStatusRequest> | undefined): boolean {\n    return proto3.util.equals(UpdateActorUpstreamStatusRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UpdateActorUpstreamStatusResponse\n */\nexport class UpdateActorUpstreamStatusResponse extends Message<UpdateActorUpstreamStatusResponse> {\n  constructor(data?: PartialMessage<UpdateActorUpstreamStatusResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UpdateActorUpstreamStatusResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateActorUpstreamStatusResponse {\n    return new UpdateActorUpstreamStatusResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateActorUpstreamStatusResponse {\n    return new UpdateActorUpstreamStatusResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateActorUpstreamStatusResponse {\n    return new UpdateActorUpstreamStatusResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UpdateActorUpstreamStatusResponse | PlainMessage<UpdateActorUpstreamStatusResponse> | undefined, b: UpdateActorUpstreamStatusResponse | PlainMessage<UpdateActorUpstreamStatusResponse> | undefined): boolean {\n    return proto3.util.equals(UpdateActorUpstreamStatusResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TakedownActorRequest\n */\nexport class TakedownActorRequest extends Message<TakedownActorRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string ref = 2;\n   */\n  ref = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp seen = 3;\n   */\n  seen?: Timestamp;\n\n  constructor(data?: PartialMessage<TakedownActorRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TakedownActorRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"seen\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TakedownActorRequest {\n    return new TakedownActorRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TakedownActorRequest {\n    return new TakedownActorRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TakedownActorRequest {\n    return new TakedownActorRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TakedownActorRequest | PlainMessage<TakedownActorRequest> | undefined, b: TakedownActorRequest | PlainMessage<TakedownActorRequest> | undefined): boolean {\n    return proto3.util.equals(TakedownActorRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TakedownActorResponse\n */\nexport class TakedownActorResponse extends Message<TakedownActorResponse> {\n  constructor(data?: PartialMessage<TakedownActorResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TakedownActorResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TakedownActorResponse {\n    return new TakedownActorResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TakedownActorResponse {\n    return new TakedownActorResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TakedownActorResponse {\n    return new TakedownActorResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TakedownActorResponse | PlainMessage<TakedownActorResponse> | undefined, b: TakedownActorResponse | PlainMessage<TakedownActorResponse> | undefined): boolean {\n    return proto3.util.equals(TakedownActorResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UntakedownActorRequest\n */\nexport class UntakedownActorRequest extends Message<UntakedownActorRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp seen = 2;\n   */\n  seen?: Timestamp;\n\n  constructor(data?: PartialMessage<UntakedownActorRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UntakedownActorRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"seen\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UntakedownActorRequest {\n    return new UntakedownActorRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UntakedownActorRequest {\n    return new UntakedownActorRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UntakedownActorRequest {\n    return new UntakedownActorRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UntakedownActorRequest | PlainMessage<UntakedownActorRequest> | undefined, b: UntakedownActorRequest | PlainMessage<UntakedownActorRequest> | undefined): boolean {\n    return proto3.util.equals(UntakedownActorRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UntakedownActorResponse\n */\nexport class UntakedownActorResponse extends Message<UntakedownActorResponse> {\n  constructor(data?: PartialMessage<UntakedownActorResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UntakedownActorResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UntakedownActorResponse {\n    return new UntakedownActorResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UntakedownActorResponse {\n    return new UntakedownActorResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UntakedownActorResponse {\n    return new UntakedownActorResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UntakedownActorResponse | PlainMessage<UntakedownActorResponse> | undefined, b: UntakedownActorResponse | PlainMessage<UntakedownActorResponse> | undefined): boolean {\n    return proto3.util.equals(UntakedownActorResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TakedownBlobRequest\n */\nexport class TakedownBlobRequest extends Message<TakedownBlobRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  /**\n   * @generated from field: string ref = 3;\n   */\n  ref = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp seen = 4;\n   */\n  seen?: Timestamp;\n\n  constructor(data?: PartialMessage<TakedownBlobRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TakedownBlobRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"seen\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TakedownBlobRequest {\n    return new TakedownBlobRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TakedownBlobRequest {\n    return new TakedownBlobRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TakedownBlobRequest {\n    return new TakedownBlobRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TakedownBlobRequest | PlainMessage<TakedownBlobRequest> | undefined, b: TakedownBlobRequest | PlainMessage<TakedownBlobRequest> | undefined): boolean {\n    return proto3.util.equals(TakedownBlobRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TakedownBlobResponse\n */\nexport class TakedownBlobResponse extends Message<TakedownBlobResponse> {\n  constructor(data?: PartialMessage<TakedownBlobResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TakedownBlobResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TakedownBlobResponse {\n    return new TakedownBlobResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TakedownBlobResponse {\n    return new TakedownBlobResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TakedownBlobResponse {\n    return new TakedownBlobResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TakedownBlobResponse | PlainMessage<TakedownBlobResponse> | undefined, b: TakedownBlobResponse | PlainMessage<TakedownBlobResponse> | undefined): boolean {\n    return proto3.util.equals(TakedownBlobResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UntakedownBlobRequest\n */\nexport class UntakedownBlobRequest extends Message<UntakedownBlobRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string cid = 2;\n   */\n  cid = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp seen = 3;\n   */\n  seen?: Timestamp;\n\n  constructor(data?: PartialMessage<UntakedownBlobRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UntakedownBlobRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"cid\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"seen\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UntakedownBlobRequest {\n    return new UntakedownBlobRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UntakedownBlobRequest {\n    return new UntakedownBlobRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UntakedownBlobRequest {\n    return new UntakedownBlobRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UntakedownBlobRequest | PlainMessage<UntakedownBlobRequest> | undefined, b: UntakedownBlobRequest | PlainMessage<UntakedownBlobRequest> | undefined): boolean {\n    return proto3.util.equals(UntakedownBlobRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UntakedownBlobResponse\n */\nexport class UntakedownBlobResponse extends Message<UntakedownBlobResponse> {\n  constructor(data?: PartialMessage<UntakedownBlobResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UntakedownBlobResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UntakedownBlobResponse {\n    return new UntakedownBlobResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UntakedownBlobResponse {\n    return new UntakedownBlobResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UntakedownBlobResponse {\n    return new UntakedownBlobResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UntakedownBlobResponse | PlainMessage<UntakedownBlobResponse> | undefined, b: UntakedownBlobResponse | PlainMessage<UntakedownBlobResponse> | undefined): boolean {\n    return proto3.util.equals(UntakedownBlobResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TakedownRecordRequest\n */\nexport class TakedownRecordRequest extends Message<TakedownRecordRequest> {\n  /**\n   * @generated from field: string record_uri = 1;\n   */\n  recordUri = \"\";\n\n  /**\n   * @generated from field: string ref = 2;\n   */\n  ref = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp seen = 3;\n   */\n  seen?: Timestamp;\n\n  constructor(data?: PartialMessage<TakedownRecordRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TakedownRecordRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"record_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"ref\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"seen\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TakedownRecordRequest {\n    return new TakedownRecordRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TakedownRecordRequest {\n    return new TakedownRecordRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TakedownRecordRequest {\n    return new TakedownRecordRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TakedownRecordRequest | PlainMessage<TakedownRecordRequest> | undefined, b: TakedownRecordRequest | PlainMessage<TakedownRecordRequest> | undefined): boolean {\n    return proto3.util.equals(TakedownRecordRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.TakedownRecordResponse\n */\nexport class TakedownRecordResponse extends Message<TakedownRecordResponse> {\n  constructor(data?: PartialMessage<TakedownRecordResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.TakedownRecordResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): TakedownRecordResponse {\n    return new TakedownRecordResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): TakedownRecordResponse {\n    return new TakedownRecordResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): TakedownRecordResponse {\n    return new TakedownRecordResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: TakedownRecordResponse | PlainMessage<TakedownRecordResponse> | undefined, b: TakedownRecordResponse | PlainMessage<TakedownRecordResponse> | undefined): boolean {\n    return proto3.util.equals(TakedownRecordResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UntakedownRecordRequest\n */\nexport class UntakedownRecordRequest extends Message<UntakedownRecordRequest> {\n  /**\n   * @generated from field: string record_uri = 1;\n   */\n  recordUri = \"\";\n\n  /**\n   * @generated from field: google.protobuf.Timestamp seen = 2;\n   */\n  seen?: Timestamp;\n\n  constructor(data?: PartialMessage<UntakedownRecordRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UntakedownRecordRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"record_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"seen\", kind: \"message\", T: Timestamp },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UntakedownRecordRequest {\n    return new UntakedownRecordRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UntakedownRecordRequest {\n    return new UntakedownRecordRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UntakedownRecordRequest {\n    return new UntakedownRecordRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UntakedownRecordRequest | PlainMessage<UntakedownRecordRequest> | undefined, b: UntakedownRecordRequest | PlainMessage<UntakedownRecordRequest> | undefined): boolean {\n    return proto3.util.equals(UntakedownRecordRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.UntakedownRecordResponse\n */\nexport class UntakedownRecordResponse extends Message<UntakedownRecordResponse> {\n  constructor(data?: PartialMessage<UntakedownRecordResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.UntakedownRecordResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UntakedownRecordResponse {\n    return new UntakedownRecordResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UntakedownRecordResponse {\n    return new UntakedownRecordResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UntakedownRecordResponse {\n    return new UntakedownRecordResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UntakedownRecordResponse | PlainMessage<UntakedownRecordResponse> | undefined, b: UntakedownRecordResponse | PlainMessage<UntakedownRecordResponse> | undefined): boolean {\n    return proto3.util.equals(UntakedownRecordResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.CreateActorMuteRequest\n */\nexport class CreateActorMuteRequest extends Message<CreateActorMuteRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject_did = 2;\n   */\n  subjectDid = \"\";\n\n  constructor(data?: PartialMessage<CreateActorMuteRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.CreateActorMuteRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateActorMuteRequest {\n    return new CreateActorMuteRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateActorMuteRequest {\n    return new CreateActorMuteRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateActorMuteRequest {\n    return new CreateActorMuteRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: CreateActorMuteRequest | PlainMessage<CreateActorMuteRequest> | undefined, b: CreateActorMuteRequest | PlainMessage<CreateActorMuteRequest> | undefined): boolean {\n    return proto3.util.equals(CreateActorMuteRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.CreateActorMuteResponse\n */\nexport class CreateActorMuteResponse extends Message<CreateActorMuteResponse> {\n  constructor(data?: PartialMessage<CreateActorMuteResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.CreateActorMuteResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateActorMuteResponse {\n    return new CreateActorMuteResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateActorMuteResponse {\n    return new CreateActorMuteResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateActorMuteResponse {\n    return new CreateActorMuteResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: CreateActorMuteResponse | PlainMessage<CreateActorMuteResponse> | undefined, b: CreateActorMuteResponse | PlainMessage<CreateActorMuteResponse> | undefined): boolean {\n    return proto3.util.equals(CreateActorMuteResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DeleteActorMuteRequest\n */\nexport class DeleteActorMuteRequest extends Message<DeleteActorMuteRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject_did = 2;\n   */\n  subjectDid = \"\";\n\n  constructor(data?: PartialMessage<DeleteActorMuteRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DeleteActorMuteRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteActorMuteRequest {\n    return new DeleteActorMuteRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteActorMuteRequest {\n    return new DeleteActorMuteRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteActorMuteRequest {\n    return new DeleteActorMuteRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteActorMuteRequest | PlainMessage<DeleteActorMuteRequest> | undefined, b: DeleteActorMuteRequest | PlainMessage<DeleteActorMuteRequest> | undefined): boolean {\n    return proto3.util.equals(DeleteActorMuteRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DeleteActorMuteResponse\n */\nexport class DeleteActorMuteResponse extends Message<DeleteActorMuteResponse> {\n  constructor(data?: PartialMessage<DeleteActorMuteResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DeleteActorMuteResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteActorMuteResponse {\n    return new DeleteActorMuteResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteActorMuteResponse {\n    return new DeleteActorMuteResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteActorMuteResponse {\n    return new DeleteActorMuteResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteActorMuteResponse | PlainMessage<DeleteActorMuteResponse> | undefined, b: DeleteActorMuteResponse | PlainMessage<DeleteActorMuteResponse> | undefined): boolean {\n    return proto3.util.equals(DeleteActorMuteResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ClearActorMutesRequest\n */\nexport class ClearActorMutesRequest extends Message<ClearActorMutesRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  constructor(data?: PartialMessage<ClearActorMutesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ClearActorMutesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ClearActorMutesRequest {\n    return new ClearActorMutesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ClearActorMutesRequest {\n    return new ClearActorMutesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ClearActorMutesRequest {\n    return new ClearActorMutesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ClearActorMutesRequest | PlainMessage<ClearActorMutesRequest> | undefined, b: ClearActorMutesRequest | PlainMessage<ClearActorMutesRequest> | undefined): boolean {\n    return proto3.util.equals(ClearActorMutesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ClearActorMutesResponse\n */\nexport class ClearActorMutesResponse extends Message<ClearActorMutesResponse> {\n  constructor(data?: PartialMessage<ClearActorMutesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ClearActorMutesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ClearActorMutesResponse {\n    return new ClearActorMutesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ClearActorMutesResponse {\n    return new ClearActorMutesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ClearActorMutesResponse {\n    return new ClearActorMutesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ClearActorMutesResponse | PlainMessage<ClearActorMutesResponse> | undefined, b: ClearActorMutesResponse | PlainMessage<ClearActorMutesResponse> | undefined): boolean {\n    return proto3.util.equals(ClearActorMutesResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.CreateActorMutelistSubscriptionRequest\n */\nexport class CreateActorMutelistSubscriptionRequest extends Message<CreateActorMutelistSubscriptionRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject_uri = 2;\n   */\n  subjectUri = \"\";\n\n  constructor(data?: PartialMessage<CreateActorMutelistSubscriptionRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.CreateActorMutelistSubscriptionRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateActorMutelistSubscriptionRequest {\n    return new CreateActorMutelistSubscriptionRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateActorMutelistSubscriptionRequest {\n    return new CreateActorMutelistSubscriptionRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateActorMutelistSubscriptionRequest {\n    return new CreateActorMutelistSubscriptionRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: CreateActorMutelistSubscriptionRequest | PlainMessage<CreateActorMutelistSubscriptionRequest> | undefined, b: CreateActorMutelistSubscriptionRequest | PlainMessage<CreateActorMutelistSubscriptionRequest> | undefined): boolean {\n    return proto3.util.equals(CreateActorMutelistSubscriptionRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.CreateActorMutelistSubscriptionResponse\n */\nexport class CreateActorMutelistSubscriptionResponse extends Message<CreateActorMutelistSubscriptionResponse> {\n  constructor(data?: PartialMessage<CreateActorMutelistSubscriptionResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.CreateActorMutelistSubscriptionResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateActorMutelistSubscriptionResponse {\n    return new CreateActorMutelistSubscriptionResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateActorMutelistSubscriptionResponse {\n    return new CreateActorMutelistSubscriptionResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateActorMutelistSubscriptionResponse {\n    return new CreateActorMutelistSubscriptionResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: CreateActorMutelistSubscriptionResponse | PlainMessage<CreateActorMutelistSubscriptionResponse> | undefined, b: CreateActorMutelistSubscriptionResponse | PlainMessage<CreateActorMutelistSubscriptionResponse> | undefined): boolean {\n    return proto3.util.equals(CreateActorMutelistSubscriptionResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DeleteActorMutelistSubscriptionRequest\n */\nexport class DeleteActorMutelistSubscriptionRequest extends Message<DeleteActorMutelistSubscriptionRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject_uri = 2;\n   */\n  subjectUri = \"\";\n\n  constructor(data?: PartialMessage<DeleteActorMutelistSubscriptionRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DeleteActorMutelistSubscriptionRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject_uri\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteActorMutelistSubscriptionRequest {\n    return new DeleteActorMutelistSubscriptionRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteActorMutelistSubscriptionRequest {\n    return new DeleteActorMutelistSubscriptionRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteActorMutelistSubscriptionRequest {\n    return new DeleteActorMutelistSubscriptionRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteActorMutelistSubscriptionRequest | PlainMessage<DeleteActorMutelistSubscriptionRequest> | undefined, b: DeleteActorMutelistSubscriptionRequest | PlainMessage<DeleteActorMutelistSubscriptionRequest> | undefined): boolean {\n    return proto3.util.equals(DeleteActorMutelistSubscriptionRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DeleteActorMutelistSubscriptionResponse\n */\nexport class DeleteActorMutelistSubscriptionResponse extends Message<DeleteActorMutelistSubscriptionResponse> {\n  constructor(data?: PartialMessage<DeleteActorMutelistSubscriptionResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DeleteActorMutelistSubscriptionResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteActorMutelistSubscriptionResponse {\n    return new DeleteActorMutelistSubscriptionResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteActorMutelistSubscriptionResponse {\n    return new DeleteActorMutelistSubscriptionResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteActorMutelistSubscriptionResponse {\n    return new DeleteActorMutelistSubscriptionResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteActorMutelistSubscriptionResponse | PlainMessage<DeleteActorMutelistSubscriptionResponse> | undefined, b: DeleteActorMutelistSubscriptionResponse | PlainMessage<DeleteActorMutelistSubscriptionResponse> | undefined): boolean {\n    return proto3.util.equals(DeleteActorMutelistSubscriptionResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ClearActorMutelistSubscriptionsRequest\n */\nexport class ClearActorMutelistSubscriptionsRequest extends Message<ClearActorMutelistSubscriptionsRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  constructor(data?: PartialMessage<ClearActorMutelistSubscriptionsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ClearActorMutelistSubscriptionsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ClearActorMutelistSubscriptionsRequest {\n    return new ClearActorMutelistSubscriptionsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ClearActorMutelistSubscriptionsRequest {\n    return new ClearActorMutelistSubscriptionsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ClearActorMutelistSubscriptionsRequest {\n    return new ClearActorMutelistSubscriptionsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ClearActorMutelistSubscriptionsRequest | PlainMessage<ClearActorMutelistSubscriptionsRequest> | undefined, b: ClearActorMutelistSubscriptionsRequest | PlainMessage<ClearActorMutelistSubscriptionsRequest> | undefined): boolean {\n    return proto3.util.equals(ClearActorMutelistSubscriptionsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ClearActorMutelistSubscriptionsResponse\n */\nexport class ClearActorMutelistSubscriptionsResponse extends Message<ClearActorMutelistSubscriptionsResponse> {\n  constructor(data?: PartialMessage<ClearActorMutelistSubscriptionsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ClearActorMutelistSubscriptionsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ClearActorMutelistSubscriptionsResponse {\n    return new ClearActorMutelistSubscriptionsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ClearActorMutelistSubscriptionsResponse {\n    return new ClearActorMutelistSubscriptionsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ClearActorMutelistSubscriptionsResponse {\n    return new ClearActorMutelistSubscriptionsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ClearActorMutelistSubscriptionsResponse | PlainMessage<ClearActorMutelistSubscriptionsResponse> | undefined, b: ClearActorMutelistSubscriptionsResponse | PlainMessage<ClearActorMutelistSubscriptionsResponse> | undefined): boolean {\n    return proto3.util.equals(ClearActorMutelistSubscriptionsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.CreateThreadMuteRequest\n */\nexport class CreateThreadMuteRequest extends Message<CreateThreadMuteRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string thread_root = 2;\n   */\n  threadRoot = \"\";\n\n  constructor(data?: PartialMessage<CreateThreadMuteRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.CreateThreadMuteRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"thread_root\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateThreadMuteRequest {\n    return new CreateThreadMuteRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateThreadMuteRequest {\n    return new CreateThreadMuteRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateThreadMuteRequest {\n    return new CreateThreadMuteRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: CreateThreadMuteRequest | PlainMessage<CreateThreadMuteRequest> | undefined, b: CreateThreadMuteRequest | PlainMessage<CreateThreadMuteRequest> | undefined): boolean {\n    return proto3.util.equals(CreateThreadMuteRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.CreateThreadMuteResponse\n */\nexport class CreateThreadMuteResponse extends Message<CreateThreadMuteResponse> {\n  constructor(data?: PartialMessage<CreateThreadMuteResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.CreateThreadMuteResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateThreadMuteResponse {\n    return new CreateThreadMuteResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateThreadMuteResponse {\n    return new CreateThreadMuteResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateThreadMuteResponse {\n    return new CreateThreadMuteResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: CreateThreadMuteResponse | PlainMessage<CreateThreadMuteResponse> | undefined, b: CreateThreadMuteResponse | PlainMessage<CreateThreadMuteResponse> | undefined): boolean {\n    return proto3.util.equals(CreateThreadMuteResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DeleteThreadMuteRequest\n */\nexport class DeleteThreadMuteRequest extends Message<DeleteThreadMuteRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string thread_root = 2;\n   */\n  threadRoot = \"\";\n\n  constructor(data?: PartialMessage<DeleteThreadMuteRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DeleteThreadMuteRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"thread_root\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteThreadMuteRequest {\n    return new DeleteThreadMuteRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteThreadMuteRequest {\n    return new DeleteThreadMuteRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteThreadMuteRequest {\n    return new DeleteThreadMuteRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteThreadMuteRequest | PlainMessage<DeleteThreadMuteRequest> | undefined, b: DeleteThreadMuteRequest | PlainMessage<DeleteThreadMuteRequest> | undefined): boolean {\n    return proto3.util.equals(DeleteThreadMuteRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.DeleteThreadMuteResponse\n */\nexport class DeleteThreadMuteResponse extends Message<DeleteThreadMuteResponse> {\n  constructor(data?: PartialMessage<DeleteThreadMuteResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.DeleteThreadMuteResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteThreadMuteResponse {\n    return new DeleteThreadMuteResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteThreadMuteResponse {\n    return new DeleteThreadMuteResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteThreadMuteResponse {\n    return new DeleteThreadMuteResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteThreadMuteResponse | PlainMessage<DeleteThreadMuteResponse> | undefined, b: DeleteThreadMuteResponse | PlainMessage<DeleteThreadMuteResponse> | undefined): boolean {\n    return proto3.util.equals(DeleteThreadMuteResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ClearThreadMutesRequest\n */\nexport class ClearThreadMutesRequest extends Message<ClearThreadMutesRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  constructor(data?: PartialMessage<ClearThreadMutesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ClearThreadMutesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ClearThreadMutesRequest {\n    return new ClearThreadMutesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ClearThreadMutesRequest {\n    return new ClearThreadMutesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ClearThreadMutesRequest {\n    return new ClearThreadMutesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ClearThreadMutesRequest | PlainMessage<ClearThreadMutesRequest> | undefined, b: ClearThreadMutesRequest | PlainMessage<ClearThreadMutesRequest> | undefined): boolean {\n    return proto3.util.equals(ClearThreadMutesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsky.ClearThreadMutesResponse\n */\nexport class ClearThreadMutesResponse extends Message<ClearThreadMutesResponse> {\n  constructor(data?: PartialMessage<ClearThreadMutesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsky.ClearThreadMutesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ClearThreadMutesResponse {\n    return new ClearThreadMutesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ClearThreadMutesResponse {\n    return new ClearThreadMutesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ClearThreadMutesResponse {\n    return new ClearThreadMutesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ClearThreadMutesResponse | PlainMessage<ClearThreadMutesResponse> | undefined, b: ClearThreadMutesResponse | PlainMessage<ClearThreadMutesResponse> | undefined): boolean {\n    return proto3.util.equals(ClearThreadMutesResponse, a, b);\n  }\n}\n\n"
  },
  {
    "path": "packages/bsky/src/proto/bsync_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.3.0 with parameter \"target=ts,import_extension=\"\n// @generated from file bsync.proto (package bsync, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { AddMuteOperationRequest, AddMuteOperationResponse, AddNotifOperationRequest, AddNotifOperationResponse, PingRequest, PingResponse, PutOperationRequest, PutOperationResponse, ScanMuteOperationsRequest, ScanMuteOperationsResponse, ScanNotifOperationsRequest, ScanNotifOperationsResponse, ScanOperationsRequest, ScanOperationsResponse } from \"./bsync_pb\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from service bsync.Service\n */\nexport const Service = {\n  typeName: \"bsync.Service\",\n  methods: {\n    /**\n     * Sync\n     *\n     * @generated from rpc bsync.Service.AddMuteOperation\n     */\n    addMuteOperation: {\n      name: \"AddMuteOperation\",\n      I: AddMuteOperationRequest,\n      O: AddMuteOperationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.ScanMuteOperations\n     */\n    scanMuteOperations: {\n      name: \"ScanMuteOperations\",\n      I: ScanMuteOperationsRequest,\n      O: ScanMuteOperationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.AddNotifOperation\n     */\n    addNotifOperation: {\n      name: \"AddNotifOperation\",\n      I: AddNotifOperationRequest,\n      O: AddNotifOperationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.ScanNotifOperations\n     */\n    scanNotifOperations: {\n      name: \"ScanNotifOperations\",\n      I: ScanNotifOperationsRequest,\n      O: ScanNotifOperationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.PutOperation\n     */\n    putOperation: {\n      name: \"PutOperation\",\n      I: PutOperationRequest,\n      O: PutOperationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.ScanOperations\n     */\n    scanOperations: {\n      name: \"ScanOperations\",\n      I: ScanOperationsRequest,\n      O: ScanOperationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Ping\n     *\n     * @generated from rpc bsync.Service.Ping\n     */\n    ping: {\n      name: \"Ping\",\n      I: PingRequest,\n      O: PingResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "packages/bsky/src/proto/bsync_pb.ts",
    "content": "// @generated by protoc-gen-es v1.6.0 with parameter \"target=ts,import_extension=\"\n// @generated from file bsync.proto (package bsync, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from \"@bufbuild/protobuf\";\nimport { Message, proto3 } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from enum bsync.Method\n */\nexport enum Method {\n  /**\n   * @generated from enum value: METHOD_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: METHOD_CREATE = 1;\n   */\n  CREATE = 1,\n\n  /**\n   * @generated from enum value: METHOD_UPDATE = 2;\n   */\n  UPDATE = 2,\n\n  /**\n   * @generated from enum value: METHOD_DELETE = 3;\n   */\n  DELETE = 3,\n}\n// Retrieve enum metadata with: proto3.getEnumType(Method)\nproto3.util.setEnumType(Method, \"bsync.Method\", [\n  { no: 0, name: \"METHOD_UNSPECIFIED\" },\n  { no: 1, name: \"METHOD_CREATE\" },\n  { no: 2, name: \"METHOD_UPDATE\" },\n  { no: 3, name: \"METHOD_DELETE\" },\n]);\n\n/**\n * @generated from message bsync.MuteOperation\n */\nexport class MuteOperation extends Message<MuteOperation> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: bsync.MuteOperation.Type type = 2;\n   */\n  type = MuteOperation_Type.UNSPECIFIED;\n\n  /**\n   * @generated from field: string actor_did = 3;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject = 4;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<MuteOperation>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.MuteOperation\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"type\", kind: \"enum\", T: proto3.getEnumType(MuteOperation_Type) },\n    { no: 3, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): MuteOperation {\n    return new MuteOperation().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): MuteOperation {\n    return new MuteOperation().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): MuteOperation {\n    return new MuteOperation().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: MuteOperation | PlainMessage<MuteOperation> | undefined, b: MuteOperation | PlainMessage<MuteOperation> | undefined): boolean {\n    return proto3.util.equals(MuteOperation, a, b);\n  }\n}\n\n/**\n * @generated from enum bsync.MuteOperation.Type\n */\nexport enum MuteOperation_Type {\n  /**\n   * @generated from enum value: TYPE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: TYPE_ADD = 1;\n   */\n  ADD = 1,\n\n  /**\n   * @generated from enum value: TYPE_REMOVE = 2;\n   */\n  REMOVE = 2,\n\n  /**\n   * @generated from enum value: TYPE_CLEAR = 3;\n   */\n  CLEAR = 3,\n}\n// Retrieve enum metadata with: proto3.getEnumType(MuteOperation_Type)\nproto3.util.setEnumType(MuteOperation_Type, \"bsync.MuteOperation.Type\", [\n  { no: 0, name: \"TYPE_UNSPECIFIED\" },\n  { no: 1, name: \"TYPE_ADD\" },\n  { no: 2, name: \"TYPE_REMOVE\" },\n  { no: 3, name: \"TYPE_CLEAR\" },\n]);\n\n/**\n * @generated from message bsync.AddMuteOperationRequest\n */\nexport class AddMuteOperationRequest extends Message<AddMuteOperationRequest> {\n  /**\n   * @generated from field: bsync.MuteOperation.Type type = 1;\n   */\n  type = MuteOperation_Type.UNSPECIFIED;\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject = 3;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<AddMuteOperationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddMuteOperationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"type\", kind: \"enum\", T: proto3.getEnumType(MuteOperation_Type) },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddMuteOperationRequest {\n    return new AddMuteOperationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddMuteOperationRequest {\n    return new AddMuteOperationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddMuteOperationRequest {\n    return new AddMuteOperationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddMuteOperationRequest | PlainMessage<AddMuteOperationRequest> | undefined, b: AddMuteOperationRequest | PlainMessage<AddMuteOperationRequest> | undefined): boolean {\n    return proto3.util.equals(AddMuteOperationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.AddMuteOperationResponse\n */\nexport class AddMuteOperationResponse extends Message<AddMuteOperationResponse> {\n  /**\n   * @generated from field: bsync.MuteOperation operation = 1;\n   */\n  operation?: MuteOperation;\n\n  constructor(data?: PartialMessage<AddMuteOperationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddMuteOperationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operation\", kind: \"message\", T: MuteOperation },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddMuteOperationResponse {\n    return new AddMuteOperationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddMuteOperationResponse {\n    return new AddMuteOperationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddMuteOperationResponse {\n    return new AddMuteOperationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddMuteOperationResponse | PlainMessage<AddMuteOperationResponse> | undefined, b: AddMuteOperationResponse | PlainMessage<AddMuteOperationResponse> | undefined): boolean {\n    return proto3.util.equals(AddMuteOperationResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanMuteOperationsRequest\n */\nexport class ScanMuteOperationsRequest extends Message<ScanMuteOperationsRequest> {\n  /**\n   * @generated from field: string cursor = 1;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<ScanMuteOperationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanMuteOperationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanMuteOperationsRequest {\n    return new ScanMuteOperationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanMuteOperationsRequest {\n    return new ScanMuteOperationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanMuteOperationsRequest {\n    return new ScanMuteOperationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanMuteOperationsRequest | PlainMessage<ScanMuteOperationsRequest> | undefined, b: ScanMuteOperationsRequest | PlainMessage<ScanMuteOperationsRequest> | undefined): boolean {\n    return proto3.util.equals(ScanMuteOperationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanMuteOperationsResponse\n */\nexport class ScanMuteOperationsResponse extends Message<ScanMuteOperationsResponse> {\n  /**\n   * @generated from field: repeated bsync.MuteOperation operations = 1;\n   */\n  operations: MuteOperation[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<ScanMuteOperationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanMuteOperationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operations\", kind: \"message\", T: MuteOperation, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanMuteOperationsResponse {\n    return new ScanMuteOperationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanMuteOperationsResponse {\n    return new ScanMuteOperationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanMuteOperationsResponse {\n    return new ScanMuteOperationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanMuteOperationsResponse | PlainMessage<ScanMuteOperationsResponse> | undefined, b: ScanMuteOperationsResponse | PlainMessage<ScanMuteOperationsResponse> | undefined): boolean {\n    return proto3.util.equals(ScanMuteOperationsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.NotifOperation\n */\nexport class NotifOperation extends Message<NotifOperation> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: optional bool priority = 3;\n   */\n  priority?: boolean;\n\n  constructor(data?: PartialMessage<NotifOperation>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.NotifOperation\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */, opt: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): NotifOperation {\n    return new NotifOperation().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): NotifOperation {\n    return new NotifOperation().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): NotifOperation {\n    return new NotifOperation().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: NotifOperation | PlainMessage<NotifOperation> | undefined, b: NotifOperation | PlainMessage<NotifOperation> | undefined): boolean {\n    return proto3.util.equals(NotifOperation, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.AddNotifOperationRequest\n */\nexport class AddNotifOperationRequest extends Message<AddNotifOperationRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: optional bool priority = 2;\n   */\n  priority?: boolean;\n\n  constructor(data?: PartialMessage<AddNotifOperationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddNotifOperationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */, opt: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddNotifOperationRequest {\n    return new AddNotifOperationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddNotifOperationRequest {\n    return new AddNotifOperationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddNotifOperationRequest {\n    return new AddNotifOperationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddNotifOperationRequest | PlainMessage<AddNotifOperationRequest> | undefined, b: AddNotifOperationRequest | PlainMessage<AddNotifOperationRequest> | undefined): boolean {\n    return proto3.util.equals(AddNotifOperationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.AddNotifOperationResponse\n */\nexport class AddNotifOperationResponse extends Message<AddNotifOperationResponse> {\n  /**\n   * @generated from field: bsync.NotifOperation operation = 1;\n   */\n  operation?: NotifOperation;\n\n  constructor(data?: PartialMessage<AddNotifOperationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddNotifOperationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operation\", kind: \"message\", T: NotifOperation },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddNotifOperationResponse {\n    return new AddNotifOperationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddNotifOperationResponse {\n    return new AddNotifOperationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddNotifOperationResponse {\n    return new AddNotifOperationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddNotifOperationResponse | PlainMessage<AddNotifOperationResponse> | undefined, b: AddNotifOperationResponse | PlainMessage<AddNotifOperationResponse> | undefined): boolean {\n    return proto3.util.equals(AddNotifOperationResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanNotifOperationsRequest\n */\nexport class ScanNotifOperationsRequest extends Message<ScanNotifOperationsRequest> {\n  /**\n   * @generated from field: string cursor = 1;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<ScanNotifOperationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanNotifOperationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanNotifOperationsRequest {\n    return new ScanNotifOperationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanNotifOperationsRequest {\n    return new ScanNotifOperationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanNotifOperationsRequest {\n    return new ScanNotifOperationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanNotifOperationsRequest | PlainMessage<ScanNotifOperationsRequest> | undefined, b: ScanNotifOperationsRequest | PlainMessage<ScanNotifOperationsRequest> | undefined): boolean {\n    return proto3.util.equals(ScanNotifOperationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanNotifOperationsResponse\n */\nexport class ScanNotifOperationsResponse extends Message<ScanNotifOperationsResponse> {\n  /**\n   * @generated from field: repeated bsync.NotifOperation operations = 1;\n   */\n  operations: NotifOperation[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<ScanNotifOperationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanNotifOperationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operations\", kind: \"message\", T: NotifOperation, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanNotifOperationsResponse {\n    return new ScanNotifOperationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanNotifOperationsResponse {\n    return new ScanNotifOperationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanNotifOperationsResponse {\n    return new ScanNotifOperationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanNotifOperationsResponse | PlainMessage<ScanNotifOperationsResponse> | undefined, b: ScanNotifOperationsResponse | PlainMessage<ScanNotifOperationsResponse> | undefined): boolean {\n    return proto3.util.equals(ScanNotifOperationsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.Operation\n */\nexport class Operation extends Message<Operation> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 3;\n   */\n  namespace = \"\";\n\n  /**\n   * @generated from field: string key = 4;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: bsync.Method method = 5;\n   */\n  method = Method.UNSPECIFIED;\n\n  /**\n   * @generated from field: bytes payload = 6;\n   */\n  payload = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<Operation>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.Operation\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"method\", kind: \"enum\", T: proto3.getEnumType(Method) },\n    { no: 6, name: \"payload\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Operation {\n    return new Operation().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Operation {\n    return new Operation().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Operation {\n    return new Operation().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Operation | PlainMessage<Operation> | undefined, b: Operation | PlainMessage<Operation> | undefined): boolean {\n    return proto3.util.equals(Operation, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.PutOperationRequest\n */\nexport class PutOperationRequest extends Message<PutOperationRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 2;\n   */\n  namespace = \"\";\n\n  /**\n   * @generated from field: string key = 3;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: bsync.Method method = 4;\n   */\n  method = Method.UNSPECIFIED;\n\n  /**\n   * @generated from field: bytes payload = 5;\n   */\n  payload = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<PutOperationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PutOperationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"method\", kind: \"enum\", T: proto3.getEnumType(Method) },\n    { no: 5, name: \"payload\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PutOperationRequest {\n    return new PutOperationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PutOperationRequest {\n    return new PutOperationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PutOperationRequest {\n    return new PutOperationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PutOperationRequest | PlainMessage<PutOperationRequest> | undefined, b: PutOperationRequest | PlainMessage<PutOperationRequest> | undefined): boolean {\n    return proto3.util.equals(PutOperationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.PutOperationResponse\n */\nexport class PutOperationResponse extends Message<PutOperationResponse> {\n  /**\n   * @generated from field: bsync.Operation operation = 1;\n   */\n  operation?: Operation;\n\n  constructor(data?: PartialMessage<PutOperationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PutOperationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operation\", kind: \"message\", T: Operation },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PutOperationResponse {\n    return new PutOperationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PutOperationResponse {\n    return new PutOperationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PutOperationResponse {\n    return new PutOperationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PutOperationResponse | PlainMessage<PutOperationResponse> | undefined, b: PutOperationResponse | PlainMessage<PutOperationResponse> | undefined): boolean {\n    return proto3.util.equals(PutOperationResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanOperationsRequest\n */\nexport class ScanOperationsRequest extends Message<ScanOperationsRequest> {\n  /**\n   * @generated from field: string cursor = 1;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<ScanOperationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanOperationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanOperationsRequest {\n    return new ScanOperationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanOperationsRequest {\n    return new ScanOperationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanOperationsRequest {\n    return new ScanOperationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanOperationsRequest | PlainMessage<ScanOperationsRequest> | undefined, b: ScanOperationsRequest | PlainMessage<ScanOperationsRequest> | undefined): boolean {\n    return proto3.util.equals(ScanOperationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanOperationsResponse\n */\nexport class ScanOperationsResponse extends Message<ScanOperationsResponse> {\n  /**\n   * @generated from field: repeated bsync.Operation operations = 1;\n   */\n  operations: Operation[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<ScanOperationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanOperationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operations\", kind: \"message\", T: Operation, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanOperationsResponse {\n    return new ScanOperationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanOperationsResponse {\n    return new ScanOperationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanOperationsResponse {\n    return new ScanOperationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanOperationsResponse | PlainMessage<ScanOperationsResponse> | undefined, b: ScanOperationsResponse | PlainMessage<ScanOperationsResponse> | undefined): boolean {\n    return proto3.util.equals(ScanOperationsResponse, a, b);\n  }\n}\n\n/**\n * Ping\n *\n * @generated from message bsync.PingRequest\n */\nexport class PingRequest extends Message<PingRequest> {\n  constructor(data?: PartialMessage<PingRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PingRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingRequest {\n    return new PingRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingRequest | PlainMessage<PingRequest> | undefined, b: PingRequest | PlainMessage<PingRequest> | undefined): boolean {\n    return proto3.util.equals(PingRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.PingResponse\n */\nexport class PingResponse extends Message<PingResponse> {\n  constructor(data?: PartialMessage<PingResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PingResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingResponse {\n    return new PingResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingResponse | PlainMessage<PingResponse> | undefined, b: PingResponse | PlainMessage<PingResponse> | undefined): boolean {\n    return proto3.util.equals(PingResponse, a, b);\n  }\n}\n\n"
  },
  {
    "path": "packages/bsky/src/proto/courier_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.3.0 with parameter \"target=ts,import_extension=\"\n// @generated from file courier.proto (package courier, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { PingRequest, PingResponse, PushNotificationsRequest, PushNotificationsResponse, RegisterDeviceTokenRequest, RegisterDeviceTokenResponse, SetAgeRestrictedRequest, SetAgeRestrictedResponse, UnregisterDeviceTokenRequest, UnregisterDeviceTokenResponse } from \"./courier_pb\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from service courier.Service\n */\nexport const Service = {\n  typeName: \"courier.Service\",\n  methods: {\n    /**\n     * @generated from rpc courier.Service.Ping\n     */\n    ping: {\n      name: \"Ping\",\n      I: PingRequest,\n      O: PingResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc courier.Service.PushNotifications\n     */\n    pushNotifications: {\n      name: \"PushNotifications\",\n      I: PushNotificationsRequest,\n      O: PushNotificationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc courier.Service.RegisterDeviceToken\n     */\n    registerDeviceToken: {\n      name: \"RegisterDeviceToken\",\n      I: RegisterDeviceTokenRequest,\n      O: RegisterDeviceTokenResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc courier.Service.UnregisterDeviceToken\n     */\n    unregisterDeviceToken: {\n      name: \"UnregisterDeviceToken\",\n      I: UnregisterDeviceTokenRequest,\n      O: UnregisterDeviceTokenResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc courier.Service.SetAgeRestricted\n     */\n    setAgeRestricted: {\n      name: \"SetAgeRestricted\",\n      I: SetAgeRestrictedRequest,\n      O: SetAgeRestrictedResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "packages/bsky/src/proto/courier_pb.ts",
    "content": "// @generated by protoc-gen-es v1.6.0 with parameter \"target=ts,import_extension=\"\n// @generated from file courier.proto (package courier, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from \"@bufbuild/protobuf\";\nimport { Message, proto3, Struct, Timestamp } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from enum courier.AppPlatform\n */\nexport enum AppPlatform {\n  /**\n   * @generated from enum value: APP_PLATFORM_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: APP_PLATFORM_IOS = 1;\n   */\n  IOS = 1,\n\n  /**\n   * @generated from enum value: APP_PLATFORM_ANDROID = 2;\n   */\n  ANDROID = 2,\n\n  /**\n   * @generated from enum value: APP_PLATFORM_WEB = 3;\n   */\n  WEB = 3,\n}\n// Retrieve enum metadata with: proto3.getEnumType(AppPlatform)\nproto3.util.setEnumType(AppPlatform, \"courier.AppPlatform\", [\n  { no: 0, name: \"APP_PLATFORM_UNSPECIFIED\" },\n  { no: 1, name: \"APP_PLATFORM_IOS\" },\n  { no: 2, name: \"APP_PLATFORM_ANDROID\" },\n  { no: 3, name: \"APP_PLATFORM_WEB\" },\n]);\n\n/**\n * Ping\n *\n * @generated from message courier.PingRequest\n */\nexport class PingRequest extends Message<PingRequest> {\n  constructor(data?: PartialMessage<PingRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.PingRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingRequest {\n    return new PingRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingRequest | PlainMessage<PingRequest> | undefined, b: PingRequest | PlainMessage<PingRequest> | undefined): boolean {\n    return proto3.util.equals(PingRequest, a, b);\n  }\n}\n\n/**\n * @generated from message courier.PingResponse\n */\nexport class PingResponse extends Message<PingResponse> {\n  constructor(data?: PartialMessage<PingResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.PingResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingResponse {\n    return new PingResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingResponse | PlainMessage<PingResponse> | undefined, b: PingResponse | PlainMessage<PingResponse> | undefined): boolean {\n    return proto3.util.equals(PingResponse, a, b);\n  }\n}\n\n/**\n * @generated from message courier.Notification\n */\nexport class Notification extends Message<Notification> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: string recipient_did = 2;\n   */\n  recipientDid = \"\";\n\n  /**\n   * @generated from field: string title = 3;\n   */\n  title = \"\";\n\n  /**\n   * @generated from field: string message = 4;\n   */\n  message = \"\";\n\n  /**\n   * @generated from field: string collapse_key = 5;\n   */\n  collapseKey = \"\";\n\n  /**\n   * @generated from field: bool always_deliver = 6;\n   */\n  alwaysDeliver = false;\n\n  /**\n   * @generated from field: google.protobuf.Timestamp timestamp = 7;\n   */\n  timestamp?: Timestamp;\n\n  /**\n   * @generated from field: google.protobuf.Struct additional = 8;\n   */\n  additional?: Struct;\n\n  /**\n   * @generated from field: bool client_controlled = 9;\n   */\n  clientControlled = false;\n\n  constructor(data?: PartialMessage<Notification>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.Notification\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"recipient_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"title\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"message\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"collapse_key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 6, name: \"always_deliver\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n    { no: 7, name: \"timestamp\", kind: \"message\", T: Timestamp },\n    { no: 8, name: \"additional\", kind: \"message\", T: Struct },\n    { no: 9, name: \"client_controlled\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Notification {\n    return new Notification().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Notification {\n    return new Notification().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Notification {\n    return new Notification().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Notification | PlainMessage<Notification> | undefined, b: Notification | PlainMessage<Notification> | undefined): boolean {\n    return proto3.util.equals(Notification, a, b);\n  }\n}\n\n/**\n * @generated from message courier.PushNotificationsRequest\n */\nexport class PushNotificationsRequest extends Message<PushNotificationsRequest> {\n  /**\n   * @generated from field: repeated courier.Notification notifications = 1;\n   */\n  notifications: Notification[] = [];\n\n  constructor(data?: PartialMessage<PushNotificationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.PushNotificationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"notifications\", kind: \"message\", T: Notification, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PushNotificationsRequest {\n    return new PushNotificationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PushNotificationsRequest {\n    return new PushNotificationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PushNotificationsRequest {\n    return new PushNotificationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PushNotificationsRequest | PlainMessage<PushNotificationsRequest> | undefined, b: PushNotificationsRequest | PlainMessage<PushNotificationsRequest> | undefined): boolean {\n    return proto3.util.equals(PushNotificationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message courier.PushNotificationsResponse\n */\nexport class PushNotificationsResponse extends Message<PushNotificationsResponse> {\n  constructor(data?: PartialMessage<PushNotificationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.PushNotificationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PushNotificationsResponse {\n    return new PushNotificationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PushNotificationsResponse {\n    return new PushNotificationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PushNotificationsResponse {\n    return new PushNotificationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PushNotificationsResponse | PlainMessage<PushNotificationsResponse> | undefined, b: PushNotificationsResponse | PlainMessage<PushNotificationsResponse> | undefined): boolean {\n    return proto3.util.equals(PushNotificationsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message courier.RegisterDeviceTokenRequest\n */\nexport class RegisterDeviceTokenRequest extends Message<RegisterDeviceTokenRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string token = 2;\n   */\n  token = \"\";\n\n  /**\n   * @generated from field: string app_id = 3;\n   */\n  appId = \"\";\n\n  /**\n   * @generated from field: courier.AppPlatform platform = 4;\n   */\n  platform = AppPlatform.UNSPECIFIED;\n\n  /**\n   * @generated from field: bool age_restricted = 5;\n   */\n  ageRestricted = false;\n\n  constructor(data?: PartialMessage<RegisterDeviceTokenRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.RegisterDeviceTokenRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"token\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"app_id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"platform\", kind: \"enum\", T: proto3.getEnumType(AppPlatform) },\n    { no: 5, name: \"age_restricted\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RegisterDeviceTokenRequest {\n    return new RegisterDeviceTokenRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RegisterDeviceTokenRequest {\n    return new RegisterDeviceTokenRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RegisterDeviceTokenRequest {\n    return new RegisterDeviceTokenRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: RegisterDeviceTokenRequest | PlainMessage<RegisterDeviceTokenRequest> | undefined, b: RegisterDeviceTokenRequest | PlainMessage<RegisterDeviceTokenRequest> | undefined): boolean {\n    return proto3.util.equals(RegisterDeviceTokenRequest, a, b);\n  }\n}\n\n/**\n * @generated from message courier.RegisterDeviceTokenResponse\n */\nexport class RegisterDeviceTokenResponse extends Message<RegisterDeviceTokenResponse> {\n  constructor(data?: PartialMessage<RegisterDeviceTokenResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.RegisterDeviceTokenResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RegisterDeviceTokenResponse {\n    return new RegisterDeviceTokenResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RegisterDeviceTokenResponse {\n    return new RegisterDeviceTokenResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RegisterDeviceTokenResponse {\n    return new RegisterDeviceTokenResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: RegisterDeviceTokenResponse | PlainMessage<RegisterDeviceTokenResponse> | undefined, b: RegisterDeviceTokenResponse | PlainMessage<RegisterDeviceTokenResponse> | undefined): boolean {\n    return proto3.util.equals(RegisterDeviceTokenResponse, a, b);\n  }\n}\n\n/**\n * @generated from message courier.UnregisterDeviceTokenRequest\n */\nexport class UnregisterDeviceTokenRequest extends Message<UnregisterDeviceTokenRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: string token = 2;\n   */\n  token = \"\";\n\n  /**\n   * @generated from field: string app_id = 3;\n   */\n  appId = \"\";\n\n  /**\n   * @generated from field: courier.AppPlatform platform = 4;\n   */\n  platform = AppPlatform.UNSPECIFIED;\n\n  constructor(data?: PartialMessage<UnregisterDeviceTokenRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.UnregisterDeviceTokenRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"token\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"app_id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"platform\", kind: \"enum\", T: proto3.getEnumType(AppPlatform) },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UnregisterDeviceTokenRequest {\n    return new UnregisterDeviceTokenRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UnregisterDeviceTokenRequest {\n    return new UnregisterDeviceTokenRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UnregisterDeviceTokenRequest {\n    return new UnregisterDeviceTokenRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UnregisterDeviceTokenRequest | PlainMessage<UnregisterDeviceTokenRequest> | undefined, b: UnregisterDeviceTokenRequest | PlainMessage<UnregisterDeviceTokenRequest> | undefined): boolean {\n    return proto3.util.equals(UnregisterDeviceTokenRequest, a, b);\n  }\n}\n\n/**\n * @generated from message courier.UnregisterDeviceTokenResponse\n */\nexport class UnregisterDeviceTokenResponse extends Message<UnregisterDeviceTokenResponse> {\n  constructor(data?: PartialMessage<UnregisterDeviceTokenResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.UnregisterDeviceTokenResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UnregisterDeviceTokenResponse {\n    return new UnregisterDeviceTokenResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UnregisterDeviceTokenResponse {\n    return new UnregisterDeviceTokenResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UnregisterDeviceTokenResponse {\n    return new UnregisterDeviceTokenResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: UnregisterDeviceTokenResponse | PlainMessage<UnregisterDeviceTokenResponse> | undefined, b: UnregisterDeviceTokenResponse | PlainMessage<UnregisterDeviceTokenResponse> | undefined): boolean {\n    return proto3.util.equals(UnregisterDeviceTokenResponse, a, b);\n  }\n}\n\n/**\n * @generated from message courier.SetAgeRestrictedRequest\n */\nexport class SetAgeRestrictedRequest extends Message<SetAgeRestrictedRequest> {\n  /**\n   * @generated from field: string did = 1;\n   */\n  did = \"\";\n\n  /**\n   * @generated from field: bool age_restricted = 2;\n   */\n  ageRestricted = false;\n\n  constructor(data?: PartialMessage<SetAgeRestrictedRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.SetAgeRestrictedRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"age_restricted\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SetAgeRestrictedRequest {\n    return new SetAgeRestrictedRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SetAgeRestrictedRequest {\n    return new SetAgeRestrictedRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SetAgeRestrictedRequest {\n    return new SetAgeRestrictedRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SetAgeRestrictedRequest | PlainMessage<SetAgeRestrictedRequest> | undefined, b: SetAgeRestrictedRequest | PlainMessage<SetAgeRestrictedRequest> | undefined): boolean {\n    return proto3.util.equals(SetAgeRestrictedRequest, a, b);\n  }\n}\n\n/**\n * @generated from message courier.SetAgeRestrictedResponse\n */\nexport class SetAgeRestrictedResponse extends Message<SetAgeRestrictedResponse> {\n  constructor(data?: PartialMessage<SetAgeRestrictedResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"courier.SetAgeRestrictedResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SetAgeRestrictedResponse {\n    return new SetAgeRestrictedResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SetAgeRestrictedResponse {\n    return new SetAgeRestrictedResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SetAgeRestrictedResponse {\n    return new SetAgeRestrictedResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SetAgeRestrictedResponse | PlainMessage<SetAgeRestrictedResponse> | undefined, b: SetAgeRestrictedResponse | PlainMessage<SetAgeRestrictedResponse> | undefined): boolean {\n    return proto3.util.equals(SetAgeRestrictedResponse, a, b);\n  }\n}\n\n"
  },
  {
    "path": "packages/bsky/src/proto/rolodex_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.3.0 with parameter \"target=ts,import_extension=\"\n// @generated from file rolodex.proto (package rolodex, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { DismissMatchRequest, DismissMatchResponse, GetMatchesRequest, GetMatchesResponse, GetSyncStatusRequest, GetSyncStatusResponse, ImportContactsRequest, ImportContactsResponse, PingRequest, PingResponse, RemoveDataRequest, RemoveDataResponse, StartPhoneVerificationRequest, StartPhoneVerificationResponse, VerifyPhoneRequest, VerifyPhoneResponse } from \"./rolodex_pb\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from service rolodex.RolodexService\n */\nexport const RolodexService = {\n  typeName: \"rolodex.RolodexService\",\n  methods: {\n    /**\n     * @generated from rpc rolodex.RolodexService.Ping\n     */\n    ping: {\n      name: \"Ping\",\n      I: PingRequest,\n      O: PingResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.GetSyncStatus\n     */\n    getSyncStatus: {\n      name: \"GetSyncStatus\",\n      I: GetSyncStatusRequest,\n      O: GetSyncStatusResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.StartPhoneVerification\n     */\n    startPhoneVerification: {\n      name: \"StartPhoneVerification\",\n      I: StartPhoneVerificationRequest,\n      O: StartPhoneVerificationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.VerifyPhone\n     */\n    verifyPhone: {\n      name: \"VerifyPhone\",\n      I: VerifyPhoneRequest,\n      O: VerifyPhoneResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.ImportContacts\n     */\n    importContacts: {\n      name: \"ImportContacts\",\n      I: ImportContactsRequest,\n      O: ImportContactsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.GetMatches\n     */\n    getMatches: {\n      name: \"GetMatches\",\n      I: GetMatchesRequest,\n      O: GetMatchesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.DismissMatch\n     */\n    dismissMatch: {\n      name: \"DismissMatch\",\n      I: DismissMatchRequest,\n      O: DismissMatchResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc rolodex.RolodexService.RemoveData\n     */\n    removeData: {\n      name: \"RemoveData\",\n      I: RemoveDataRequest,\n      O: RemoveDataResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "packages/bsky/src/proto/rolodex_pb.ts",
    "content": "// @generated by protoc-gen-es v1.6.0 with parameter \"target=ts,import_extension=\"\n// @generated from file rolodex.proto (package rolodex, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from \"@bufbuild/protobuf\";\nimport { Message, proto3, Timestamp } from \"@bufbuild/protobuf\";\n\n/**\n * Ping\n *\n * @generated from message rolodex.PingRequest\n */\nexport class PingRequest extends Message<PingRequest> {\n  constructor(data?: PartialMessage<PingRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.PingRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingRequest {\n    return new PingRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingRequest | PlainMessage<PingRequest> | undefined, b: PingRequest | PlainMessage<PingRequest> | undefined): boolean {\n    return proto3.util.equals(PingRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.PingResponse\n */\nexport class PingResponse extends Message<PingResponse> {\n  constructor(data?: PartialMessage<PingResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.PingResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingResponse {\n    return new PingResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingResponse | PlainMessage<PingResponse> | undefined, b: PingResponse | PlainMessage<PingResponse> | undefined): boolean {\n    return proto3.util.equals(PingResponse, a, b);\n  }\n}\n\n/**\n * GetSyncStatus\n *\n * @generated from message rolodex.GetSyncStatusRequest\n */\nexport class GetSyncStatusRequest extends Message<GetSyncStatusRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  constructor(data?: PartialMessage<GetSyncStatusRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.GetSyncStatusRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSyncStatusRequest {\n    return new GetSyncStatusRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSyncStatusRequest {\n    return new GetSyncStatusRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSyncStatusRequest {\n    return new GetSyncStatusRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSyncStatusRequest | PlainMessage<GetSyncStatusRequest> | undefined, b: GetSyncStatusRequest | PlainMessage<GetSyncStatusRequest> | undefined): boolean {\n    return proto3.util.equals(GetSyncStatusRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.SyncStatus\n */\nexport class SyncStatus extends Message<SyncStatus> {\n  /**\n   * @generated from field: google.protobuf.Timestamp synced_at = 1;\n   */\n  syncedAt?: Timestamp;\n\n  /**\n   * @generated from field: int32 matches_count = 2;\n   */\n  matchesCount = 0;\n\n  constructor(data?: PartialMessage<SyncStatus>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.SyncStatus\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"synced_at\", kind: \"message\", T: Timestamp },\n    { no: 2, name: \"matches_count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): SyncStatus {\n    return new SyncStatus().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): SyncStatus {\n    return new SyncStatus().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): SyncStatus {\n    return new SyncStatus().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: SyncStatus | PlainMessage<SyncStatus> | undefined, b: SyncStatus | PlainMessage<SyncStatus> | undefined): boolean {\n    return proto3.util.equals(SyncStatus, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.GetSyncStatusResponse\n */\nexport class GetSyncStatusResponse extends Message<GetSyncStatusResponse> {\n  /**\n   * @generated from field: rolodex.SyncStatus status = 1;\n   */\n  status?: SyncStatus;\n\n  constructor(data?: PartialMessage<GetSyncStatusResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.GetSyncStatusResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"status\", kind: \"message\", T: SyncStatus },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetSyncStatusResponse {\n    return new GetSyncStatusResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetSyncStatusResponse {\n    return new GetSyncStatusResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetSyncStatusResponse {\n    return new GetSyncStatusResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetSyncStatusResponse | PlainMessage<GetSyncStatusResponse> | undefined, b: GetSyncStatusResponse | PlainMessage<GetSyncStatusResponse> | undefined): boolean {\n    return proto3.util.equals(GetSyncStatusResponse, a, b);\n  }\n}\n\n/**\n * StartPhoneVerification\n *\n * @generated from message rolodex.StartPhoneVerificationRequest\n */\nexport class StartPhoneVerificationRequest extends Message<StartPhoneVerificationRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  /**\n   * @generated from field: string phone = 2;\n   */\n  phone = \"\";\n\n  constructor(data?: PartialMessage<StartPhoneVerificationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.StartPhoneVerificationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"phone\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): StartPhoneVerificationRequest {\n    return new StartPhoneVerificationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): StartPhoneVerificationRequest {\n    return new StartPhoneVerificationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StartPhoneVerificationRequest {\n    return new StartPhoneVerificationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: StartPhoneVerificationRequest | PlainMessage<StartPhoneVerificationRequest> | undefined, b: StartPhoneVerificationRequest | PlainMessage<StartPhoneVerificationRequest> | undefined): boolean {\n    return proto3.util.equals(StartPhoneVerificationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.StartPhoneVerificationResponse\n */\nexport class StartPhoneVerificationResponse extends Message<StartPhoneVerificationResponse> {\n  constructor(data?: PartialMessage<StartPhoneVerificationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.StartPhoneVerificationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): StartPhoneVerificationResponse {\n    return new StartPhoneVerificationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): StartPhoneVerificationResponse {\n    return new StartPhoneVerificationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StartPhoneVerificationResponse {\n    return new StartPhoneVerificationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: StartPhoneVerificationResponse | PlainMessage<StartPhoneVerificationResponse> | undefined, b: StartPhoneVerificationResponse | PlainMessage<StartPhoneVerificationResponse> | undefined): boolean {\n    return proto3.util.equals(StartPhoneVerificationResponse, a, b);\n  }\n}\n\n/**\n * VerifyPhone\n *\n * @generated from message rolodex.VerifyPhoneRequest\n */\nexport class VerifyPhoneRequest extends Message<VerifyPhoneRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  /**\n   * @generated from field: string phone = 2;\n   */\n  phone = \"\";\n\n  /**\n   * @generated from field: string verification_code = 3;\n   */\n  verificationCode = \"\";\n\n  constructor(data?: PartialMessage<VerifyPhoneRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.VerifyPhoneRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"phone\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"verification_code\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VerifyPhoneRequest {\n    return new VerifyPhoneRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VerifyPhoneRequest {\n    return new VerifyPhoneRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VerifyPhoneRequest {\n    return new VerifyPhoneRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: VerifyPhoneRequest | PlainMessage<VerifyPhoneRequest> | undefined, b: VerifyPhoneRequest | PlainMessage<VerifyPhoneRequest> | undefined): boolean {\n    return proto3.util.equals(VerifyPhoneRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.VerifyPhoneResponse\n */\nexport class VerifyPhoneResponse extends Message<VerifyPhoneResponse> {\n  /**\n   * @generated from field: string token = 1;\n   */\n  token = \"\";\n\n  constructor(data?: PartialMessage<VerifyPhoneResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.VerifyPhoneResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"token\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VerifyPhoneResponse {\n    return new VerifyPhoneResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VerifyPhoneResponse {\n    return new VerifyPhoneResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VerifyPhoneResponse {\n    return new VerifyPhoneResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: VerifyPhoneResponse | PlainMessage<VerifyPhoneResponse> | undefined, b: VerifyPhoneResponse | PlainMessage<VerifyPhoneResponse> | undefined): boolean {\n    return proto3.util.equals(VerifyPhoneResponse, a, b);\n  }\n}\n\n/**\n * ImportContacts\n *\n * @generated from message rolodex.ImportContactsRequest\n */\nexport class ImportContactsRequest extends Message<ImportContactsRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  /**\n   * @generated from field: string token = 2;\n   */\n  token = \"\";\n\n  /**\n   * @generated from field: repeated string contacts = 3;\n   */\n  contacts: string[] = [];\n\n  constructor(data?: PartialMessage<ImportContactsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.ImportContactsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"token\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"contacts\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ImportContactsRequest {\n    return new ImportContactsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ImportContactsRequest {\n    return new ImportContactsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ImportContactsRequest {\n    return new ImportContactsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ImportContactsRequest | PlainMessage<ImportContactsRequest> | undefined, b: ImportContactsRequest | PlainMessage<ImportContactsRequest> | undefined): boolean {\n    return proto3.util.equals(ImportContactsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.ImportContactsMatch\n */\nexport class ImportContactsMatch extends Message<ImportContactsMatch> {\n  /**\n   * To which index of the input contacts this contact corresponds.\n   *\n   * @generated from field: int32 input_index = 1;\n   */\n  inputIndex = 0;\n\n  /**\n   * @generated from field: string subject = 2;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<ImportContactsMatch>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.ImportContactsMatch\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"input_index\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 2, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ImportContactsMatch {\n    return new ImportContactsMatch().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ImportContactsMatch {\n    return new ImportContactsMatch().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ImportContactsMatch {\n    return new ImportContactsMatch().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ImportContactsMatch | PlainMessage<ImportContactsMatch> | undefined, b: ImportContactsMatch | PlainMessage<ImportContactsMatch> | undefined): boolean {\n    return proto3.util.equals(ImportContactsMatch, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.ImportContactsResponse\n */\nexport class ImportContactsResponse extends Message<ImportContactsResponse> {\n  /**\n   * @generated from field: repeated rolodex.ImportContactsMatch matches = 1;\n   */\n  matches: ImportContactsMatch[] = [];\n\n  constructor(data?: PartialMessage<ImportContactsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.ImportContactsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"matches\", kind: \"message\", T: ImportContactsMatch, repeated: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ImportContactsResponse {\n    return new ImportContactsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ImportContactsResponse {\n    return new ImportContactsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ImportContactsResponse {\n    return new ImportContactsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ImportContactsResponse | PlainMessage<ImportContactsResponse> | undefined, b: ImportContactsResponse | PlainMessage<ImportContactsResponse> | undefined): boolean {\n    return proto3.util.equals(ImportContactsResponse, a, b);\n  }\n}\n\n/**\n * GetMatches\n *\n * @generated from message rolodex.GetMatchesRequest\n */\nexport class GetMatchesRequest extends Message<GetMatchesRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  /**\n   * @generated from field: string cursor = 3;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetMatchesRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.GetMatchesRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 3, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMatchesRequest {\n    return new GetMatchesRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMatchesRequest {\n    return new GetMatchesRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMatchesRequest {\n    return new GetMatchesRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMatchesRequest | PlainMessage<GetMatchesRequest> | undefined, b: GetMatchesRequest | PlainMessage<GetMatchesRequest> | undefined): boolean {\n    return proto3.util.equals(GetMatchesRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.GetMatchesResponse\n */\nexport class GetMatchesResponse extends Message<GetMatchesResponse> {\n  /**\n   * @generated from field: repeated string subjects = 1;\n   */\n  subjects: string[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<GetMatchesResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.GetMatchesResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"subjects\", kind: \"scalar\", T: 9 /* ScalarType.STRING */, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetMatchesResponse {\n    return new GetMatchesResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetMatchesResponse {\n    return new GetMatchesResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetMatchesResponse {\n    return new GetMatchesResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: GetMatchesResponse | PlainMessage<GetMatchesResponse> | undefined, b: GetMatchesResponse | PlainMessage<GetMatchesResponse> | undefined): boolean {\n    return proto3.util.equals(GetMatchesResponse, a, b);\n  }\n}\n\n/**\n * DismissMatch\n *\n * @generated from message rolodex.DismissMatchRequest\n */\nexport class DismissMatchRequest extends Message<DismissMatchRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  /**\n   * @generated from field: string subject = 2;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<DismissMatchRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.DismissMatchRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DismissMatchRequest {\n    return new DismissMatchRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DismissMatchRequest {\n    return new DismissMatchRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DismissMatchRequest {\n    return new DismissMatchRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DismissMatchRequest | PlainMessage<DismissMatchRequest> | undefined, b: DismissMatchRequest | PlainMessage<DismissMatchRequest> | undefined): boolean {\n    return proto3.util.equals(DismissMatchRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.DismissMatchResponse\n */\nexport class DismissMatchResponse extends Message<DismissMatchResponse> {\n  /**\n   * @generated from field: int32 matches_count = 1;\n   */\n  matchesCount = 0;\n\n  constructor(data?: PartialMessage<DismissMatchResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.DismissMatchResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"matches_count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DismissMatchResponse {\n    return new DismissMatchResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DismissMatchResponse {\n    return new DismissMatchResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DismissMatchResponse {\n    return new DismissMatchResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DismissMatchResponse | PlainMessage<DismissMatchResponse> | undefined, b: DismissMatchResponse | PlainMessage<DismissMatchResponse> | undefined): boolean {\n    return proto3.util.equals(DismissMatchResponse, a, b);\n  }\n}\n\n/**\n * RemoveData\n *\n * @generated from message rolodex.RemoveDataRequest\n */\nexport class RemoveDataRequest extends Message<RemoveDataRequest> {\n  /**\n   * @generated from field: string actor = 1;\n   */\n  actor = \"\";\n\n  constructor(data?: PartialMessage<RemoveDataRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.RemoveDataRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RemoveDataRequest {\n    return new RemoveDataRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RemoveDataRequest {\n    return new RemoveDataRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RemoveDataRequest {\n    return new RemoveDataRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: RemoveDataRequest | PlainMessage<RemoveDataRequest> | undefined, b: RemoveDataRequest | PlainMessage<RemoveDataRequest> | undefined): boolean {\n    return proto3.util.equals(RemoveDataRequest, a, b);\n  }\n}\n\n/**\n * @generated from message rolodex.RemoveDataResponse\n */\nexport class RemoveDataResponse extends Message<RemoveDataResponse> {\n  /**\n   * @generated from field: int32 contacts_count = 1;\n   */\n  contactsCount = 0;\n\n  /**\n   * @generated from field: int32 matches_count = 2;\n   */\n  matchesCount = 0;\n\n  constructor(data?: PartialMessage<RemoveDataResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"rolodex.RemoveDataResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"contacts_count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n    { no: 2, name: \"matches_count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RemoveDataResponse {\n    return new RemoveDataResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RemoveDataResponse {\n    return new RemoveDataResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RemoveDataResponse {\n    return new RemoveDataResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: RemoveDataResponse | PlainMessage<RemoveDataResponse> | undefined, b: RemoveDataResponse | PlainMessage<RemoveDataResponse> | undefined): boolean {\n    return proto3.util.equals(RemoveDataResponse, a, b);\n  }\n}\n\n"
  },
  {
    "path": "packages/bsky/src/redis.ts",
    "content": "import assert from 'node:assert'\nimport { Redis as RedisDriver } from 'ioredis'\n\nexport class Redis {\n  driver: RedisDriver\n  namespace?: string\n  constructor(opts: RedisOptions) {\n    if ('sentinel' in opts) {\n      assert(opts.sentinel && Array.isArray(opts.hosts) && opts.hosts.length)\n      this.driver = new RedisDriver({\n        name: opts.sentinel,\n        sentinels: opts.hosts.map((h) => addressParts(h, 26379)),\n        password: opts.password,\n        db: opts.db,\n        commandTimeout: opts.commandTimeout,\n      })\n    } else if ('host' in opts) {\n      assert(opts.host)\n      this.driver = new RedisDriver({\n        ...addressParts(opts.host),\n        password: opts.password,\n        db: opts.db,\n        commandTimeout: opts.commandTimeout,\n      })\n    } else {\n      assert(opts.driver)\n      this.driver = opts.driver\n    }\n    this.namespace = opts.namespace\n  }\n\n  withNamespace(namespace: string): Redis {\n    return new Redis({ driver: this.driver, namespace })\n  }\n\n  async readStreams(\n    streams: StreamRef[],\n    opts: { count: number; blockMs?: number },\n  ) {\n    const allRead = await this.driver.xreadBuffer(\n      'COUNT',\n      opts.count, // events per stream\n      'BLOCK',\n      opts.blockMs ?? 1000, // millis\n      'STREAMS',\n      ...streams.map((s) => this.ns(s.key)),\n      ...streams.map((s) => s.cursor),\n    )\n    const results: StreamOutput[] = []\n    for (const [key, messages] of allRead ?? []) {\n      const result: StreamOutput = {\n        key: this.rmns(key.toString()),\n        messages: [],\n      }\n      results.push(result)\n      for (const [seqBuf, values] of messages) {\n        const message = { cursor: seqBuf.toString(), contents: {} }\n        result.messages.push(message)\n        for (let i = 0; i < values.length; ++i) {\n          if (i % 2 === 0) continue\n          const field = values[i - 1].toString()\n          message.contents[field] = values[i]\n        }\n      }\n    }\n    return results\n  }\n\n  async addToStream(\n    key: string,\n    id: number | string,\n    fields: [key: string, value: string | Buffer][],\n  ) {\n    await this.driver.xadd(this.ns(key), id, ...fields.flat())\n  }\n\n  async addMultiToStream(\n    evts: {\n      key: string\n      id: number | string\n      fields: [key: string, value: string | Buffer][]\n    }[],\n  ) {\n    const pipeline = this.driver.pipeline()\n    for (const { key, id, fields } of evts) {\n      pipeline.xadd(this.ns(key), id, ...fields.flat())\n    }\n    return (await pipeline.exec()) ?? []\n  }\n\n  async trimStream(key: string, cursor: number | string) {\n    await this.driver.xtrim(this.ns(key), 'MINID', cursor)\n  }\n\n  async streamLengths(keys: string[]) {\n    const pipeline = this.driver.pipeline()\n    for (const key of keys) {\n      pipeline.xlen(this.ns(key))\n    }\n    const results = await pipeline.exec()\n    return (results ?? []).map(([, len = 0]) => Number(len))\n  }\n\n  async get(key: string) {\n    return await this.driver.get(this.ns(key))\n  }\n\n  async set(key: string, val: string | number, ttlMs?: number) {\n    if (ttlMs !== undefined) {\n      await this.driver.set(this.ns(key), val, 'PX', ttlMs)\n    } else {\n      await this.driver.set(this.ns(key), val)\n    }\n  }\n\n  async getMulti(keys: string[]) {\n    const namespaced = keys.map((k) => this.ns(k))\n    const got = await this.driver.mget(...namespaced)\n    const results = {}\n    for (let i = 0; i < keys.length; i++) {\n      const key = keys[i]\n      results[key] = got[i]\n    }\n    return results\n  }\n\n  async setMulti(vals: Record<string, string | number>, ttlMs?: number) {\n    if (Object.keys(vals).length === 0) {\n      return\n    }\n    let builder = this.driver.multi({ pipeline: true })\n    for (const key of Object.keys(vals)) {\n      if (ttlMs !== undefined) {\n        builder = builder.set(this.ns(key), vals[key], 'PX', ttlMs)\n      } else {\n        builder = builder.set(this.ns(key), vals[key])\n      }\n    }\n    await builder.exec()\n  }\n\n  async del(key: string) {\n    return await this.driver.del(this.ns(key))\n  }\n\n  async expire(key: string, seconds: number) {\n    return await this.driver.expire(this.ns(key), seconds)\n  }\n\n  async zremrangebyscore(key: string, min: number, max: number) {\n    return await this.driver.zremrangebyscore(this.ns(key), min, max)\n  }\n\n  async zcount(key: string, min: number, max: number) {\n    return await this.driver.zcount(this.ns(key), min, max)\n  }\n\n  async zadd(key: string, score: number, member: number | string) {\n    return await this.driver.zadd(this.ns(key), score, member)\n  }\n\n  async destroy() {\n    await this.driver.quit()\n  }\n\n  // namespace redis keys\n  ns(key: string) {\n    return this.namespace ? `${this.namespace}:${key}` : key\n  }\n\n  // remove namespace from redis key\n  rmns(key: string) {\n    return this.namespace && key.startsWith(`${this.namespace}:`)\n      ? key.replace(`${this.namespace}:`, '')\n      : key\n  }\n}\n\ntype StreamRef = { key: string; cursor: string | number }\n\ntype StreamOutput = {\n  key: string\n  messages: { cursor: string; contents: Record<string, Buffer | undefined> }[]\n}\n\nexport type RedisOptions = (\n  | { driver: RedisDriver }\n  | { host: string }\n  | { sentinel: string; hosts: string[] }\n) & {\n  password?: string\n  namespace?: string\n  db?: number\n  commandTimeout?: number\n}\n\nexport function addressParts(\n  addr: string,\n  defaultPort = 6379,\n): { host: string; port: number } {\n  const [host, portStr, ...others] = addr.split(':')\n  const port = portStr ? parseInt(portStr, 10) : defaultPort\n  assert(host && !isNaN(port) && !others.length, `invalid address: ${addr}`)\n  return { host, port }\n}\n"
  },
  {
    "path": "packages/bsky/src/rolodex.ts",
    "content": "import {\n  Code,\n  ConnectError,\n  Interceptor,\n  PromiseClient,\n  createPromiseClient,\n} from '@connectrpc/connect'\nimport {\n  ConnectTransportOptions,\n  createConnectTransport,\n} from '@connectrpc/connect-node'\nimport { RolodexService } from './proto/rolodex_connect'\n\n// Rolodex is the service that does contact imports following https://docs.bsky.app/blog/contact-import-rfc.\nexport type RolodexClient = PromiseClient<typeof RolodexService>\n\nexport const createRolodexClient = (\n  opts: ConnectTransportOptions,\n): RolodexClient => {\n  const transport = createConnectTransport(opts)\n  return createPromiseClient(RolodexService, transport)\n}\n\nexport { Code }\n\nexport const isRolodexError = (\n  err: unknown,\n  code?: Code,\n): err is ConnectError => {\n  if (err instanceof ConnectError) {\n    return !code || err.code === code\n  }\n  return false\n}\n\nexport const authWithApiKey =\n  (apiKey: string): Interceptor =>\n  (next) =>\n  (req) => {\n    req.header.set('authorization', `Bearer ${apiKey}`)\n    return next(req)\n  }\n"
  },
  {
    "path": "packages/bsky/src/stash.ts",
    "content": "import { LexValue, stringifyLex } from '@atproto/lexicon'\nimport { BsyncClient } from './bsync'\nimport { lexicons } from './lexicon/lexicons'\nimport { Event as AgeAssuranceEventV2 } from './lexicon/types/app/bsky/ageassurance/defs'\nimport { Bookmark } from './lexicon/types/app/bsky/bookmark/defs'\nimport { Notification } from './lexicon/types/app/bsky/contact/defs'\nimport { DraftWithId } from './lexicon/types/app/bsky/draft/defs'\nimport {\n  Preferences,\n  SubjectActivitySubscription,\n} from './lexicon/types/app/bsky/notification/defs'\nimport { AgeAssuranceEvent } from './lexicon/types/app/bsky/unspecced/defs'\nimport { Method } from './proto/bsync_pb'\n\ntype PickNSID<T extends { $type?: string }> = Exclude<T['$type'], undefined>\n\nexport const Namespaces = {\n  AppBskyAgeassuranceDefsEvent:\n    'app.bsky.ageassurance.defs#event' satisfies PickNSID<AgeAssuranceEventV2>,\n  AppBskyBookmarkDefsBookmark:\n    'app.bsky.bookmark.defs#bookmark' satisfies PickNSID<Bookmark>,\n  AppBskyContactDefsNotification:\n    'app.bsky.contact.defs#notification' satisfies PickNSID<Notification>,\n  AppBskyDraftDefsDraftWithId:\n    'app.bsky.draft.defs#draftWithId' satisfies PickNSID<DraftWithId>,\n  AppBskyNotificationDefsPreferences:\n    'app.bsky.notification.defs#preferences' satisfies PickNSID<Preferences>,\n  AppBskyNotificationDefsSubjectActivitySubscription:\n    'app.bsky.notification.defs#subjectActivitySubscription' satisfies PickNSID<SubjectActivitySubscription>,\n  AppBskyUnspeccedDefsAgeAssuranceEvent:\n    'app.bsky.unspecced.defs#ageAssuranceEvent' satisfies PickNSID<AgeAssuranceEvent>,\n}\n\nexport type Namespace = (typeof Namespaces)[keyof typeof Namespaces]\n\nexport const createStashClient = (bsyncClient: BsyncClient): StashClient => {\n  return new StashClient(bsyncClient)\n}\n\n// An abstraction over the BsyncClient, that uses the bsync `PutOperation` RPC\n// to store private data, which can be indexed by the dataplane and queried by the appview.\nexport class StashClient {\n  constructor(private readonly bsyncClient: BsyncClient) {}\n\n  create(input: CreateInput) {\n    this.validateLexicon(input.namespace, input.payload)\n    return this.putOperation(Method.CREATE, input)\n  }\n\n  update(input: UpdateInput) {\n    this.validateLexicon(input.namespace, input.payload)\n    return this.putOperation(Method.UPDATE, input)\n  }\n\n  delete(input: DeleteInput) {\n    return this.putOperation(Method.DELETE, { ...input, payload: undefined })\n  }\n\n  private validateLexicon(namespace: Namespace, payload: LexValue) {\n    const result = lexicons.validate(namespace, payload)\n    if (!result.success) {\n      throw result.error\n    }\n  }\n\n  private async putOperation(method: Method, input: PutOperationInput) {\n    const { actorDid, namespace, key, payload } = input\n    await this.bsyncClient.putOperation({\n      actorDid,\n      namespace,\n      key,\n      method,\n      payload: payload\n        ? Buffer.from(\n            stringifyLex({\n              $type: namespace,\n              ...payload,\n            }),\n          )\n        : undefined,\n    })\n  }\n}\n\ntype PutOperationInput = {\n  actorDid: string\n  namespace: Namespace\n  key: string\n  payload: LexValue | undefined\n}\n\ntype CreateInput = {\n  actorDid: string\n  namespace: Namespace\n  key: string\n  payload: LexValue\n}\n\ntype UpdateInput = CreateInput\n\ntype DeleteInput = {\n  actorDid: string\n  namespace: Namespace\n  key: string\n}\n"
  },
  {
    "path": "packages/bsky/src/util/debug.ts",
    "content": "export const debugCatch = <Func extends (...args: any[]) => any>(fn: Func) => {\n  return async (...args: Parameters<Func>) => {\n    try {\n      return (await fn(...args)) as Awaited<ReturnType<Func>>\n    } catch (err) {\n      console.error(err)\n      throw err\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/util/http.ts",
    "content": "import { IncomingMessage, ServerResponse } from 'node:http'\nimport createHttpError from 'http-errors'\nimport { IncomingHttpHeaders } from 'undici/types/header'\n\ntype NextFunction = (err?: unknown) => void\n\nexport type Middleware = (\n  req: IncomingMessage,\n  res: ServerResponse,\n  next: NextFunction,\n) => void\n\nexport type ResponseData = { statusCode: number; headers: IncomingHttpHeaders }\n\nconst RESPONSE_HEADERS_TO_PROXY = new Set([\n  'content-type',\n  'content-length',\n  'content-encoding',\n  'content-language',\n  'cache-control',\n  'last-modified',\n  'etag',\n  'expires',\n  'retry-after',\n  'vary', // Might vary based on \"accept\" headers\n] as const satisfies (keyof IncomingHttpHeaders)[])\n\nexport function proxyResponseHeaders(data: ResponseData, res: ServerResponse) {\n  res.statusCode = data.statusCode >= 500 ? 502 : data.statusCode\n  for (const name of RESPONSE_HEADERS_TO_PROXY) {\n    const val = data.headers[name]\n    if (val) res.setHeader(name, val)\n  }\n}\n\nexport function responseSignal(res: ServerResponse): AbortSignal {\n  if (res.destroyed) throw createHttpError(499, 'Client Disconnected')\n  const controller = new AbortController()\n  res.once('close', () => controller.abort())\n  return controller.signal\n}\n"
  },
  {
    "path": "packages/bsky/src/util/retry.ts",
    "content": "import { createRetryable } from '@atproto/common'\nimport { ResponseType, XRPCError } from '@atproto/xrpc'\n\nexport const RETRYABLE_HTTP_STATUS_CODES = new Set([\n  408, 425, 429, 500, 502, 503, 504, 522, 524,\n])\n\nexport const retryXrpc = createRetryable((err: unknown) => {\n  if (err instanceof XRPCError) {\n    if (err.status === ResponseType.Unknown) return true\n    return RETRYABLE_HTTP_STATUS_CODES.has(err.status)\n  }\n  return false\n})\n"
  },
  {
    "path": "packages/bsky/src/util/uris.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { ids } from '../lexicon/lexicons'\nimport {\n  Main as StrongRef,\n  validateMain as validateStrongRef,\n} from '../lexicon/types/com/atproto/repo/strongRef'\n\n/**\n * Convert a post URI to a threadgate URI. If the URI is not a valid\n * post URI, return URI unchanged. Threadgate lookups will then fail.\n */\nexport function postUriToThreadgateUri(postUri: string) {\n  const urip = new AtUri(postUri)\n  if (urip.collection === ids.AppBskyFeedPost) {\n    urip.collection = ids.AppBskyFeedThreadgate\n  }\n  return urip.toString()\n}\n\n/**\n * Convert a post URI to a postgate URI. If the URI is not a valid\n * post URI, return URI unchanged. Postgate lookups will then fail.\n */\nexport function postUriToPostgateUri(postUri: string) {\n  const urip = new AtUri(postUri)\n  if (urip.collection === ids.AppBskyFeedPost) {\n    urip.collection = ids.AppBskyFeedPostgate\n  }\n  return urip.toString()\n}\n\nexport function uriToDid(uri: string) {\n  return new AtUri(uri).hostname\n}\n\n// @TODO temp fix for proliferation of invalid pinned post values\nexport function safePinnedPost(value: unknown) {\n  if (!value || typeof value !== 'object') {\n    return\n  }\n  const validated = validateStrongRef(value)\n  if (!validated.success) {\n    return\n  }\n  return validated.value as StrongRef\n}\n"
  },
  {
    "path": "packages/bsky/src/util.ts",
    "content": "import { parseList } from 'structured-headers'\n\nexport type ParsedLabelers = {\n  dids: string[]\n  redact: Set<string>\n}\n\nexport const parseLabelerHeader = (\n  header: string | undefined,\n): ParsedLabelers | null => {\n  // An empty header is valid, so we shouldn't return null\n  // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2\n  if (header === undefined) return null\n  const labelerDids = new Set<string>()\n  const redactDids = new Set<string>()\n  const parsed = parseList(header)\n  for (const item of parsed) {\n    const did = item[0].toString()\n    if (!did) {\n      return null\n    }\n    labelerDids.add(did)\n    const redact = item[1].get('redact')?.valueOf()\n    if (redact === true) {\n      redactDids.add(did)\n    }\n  }\n  return {\n    dids: [...labelerDids],\n    redact: redactDids,\n  }\n}\n\nexport const defaultLabelerHeader = (dids: string[]): ParsedLabelers => {\n  return {\n    dids,\n    redact: new Set(dids),\n  }\n}\n\nexport const formatLabelerHeader = (parsed: ParsedLabelers): string => {\n  const parts = parsed.dids.map((did) =>\n    parsed.redact.has(did) ? `${did};redact` : did,\n  )\n  return parts.join(',')\n}\n"
  },
  {
    "path": "packages/bsky/src/views/index.ts",
    "content": "import { HOUR, MINUTE, mapDefined } from '@atproto/common'\nimport { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'\nimport { Actor, ProfileViewerState } from '../hydration/actor'\nimport { FeedItem, Like, Post, Repost } from '../hydration/feed'\nimport { Follow, Verification } from '../hydration/graph'\nimport { HydrationState } from '../hydration/hydrator'\nimport { Label } from '../hydration/label'\nimport { RecordInfo } from '../hydration/util'\nimport { ImageUriBuilder } from '../image/uri'\nimport { ids } from '../lexicon/lexicons'\nimport {\n  KnownFollowers,\n  ProfileAssociatedActivitySubscription,\n  ProfileView,\n  ProfileViewBasic,\n  ProfileViewDetailed,\n  StatusView,\n  VerificationState,\n  VerificationView,\n  ViewerState as ProfileViewer,\n} from '../lexicon/types/app/bsky/actor/defs'\nimport {\n  Record as ProfileRecord,\n  isRecord as isProfileRecord,\n} from '../lexicon/types/app/bsky/actor/profile'\nimport { BookmarkView } from '../lexicon/types/app/bsky/bookmark/defs'\nimport {\n  BlockedPost,\n  FeedViewPost,\n  GeneratorView,\n  NotFoundPost,\n  PostView,\n  ReasonPin,\n  ReasonRepost,\n  ReplyRef,\n  ThreadViewPost,\n  ThreadgateView,\n  isPostView,\n} from '../lexicon/types/app/bsky/feed/defs'\nimport { Record as LikeRecord } from '../lexicon/types/app/bsky/feed/like'\nimport {\n  Record as PostRecord,\n  isRecord as isPostRecord,\n} from '../lexicon/types/app/bsky/feed/post'\nimport { Record as RepostRecord } from '../lexicon/types/app/bsky/feed/repost'\nimport { isListRule } from '../lexicon/types/app/bsky/feed/threadgate'\nimport {\n  ListItemView,\n  ListView,\n  ListViewBasic,\n  StarterPackView,\n  StarterPackViewBasic,\n} from '../lexicon/types/app/bsky/graph/defs'\nimport { Record as FollowRecord } from '../lexicon/types/app/bsky/graph/follow'\nimport { Record as VerificationRecord } from '../lexicon/types/app/bsky/graph/verification'\nimport {\n  LabelerView,\n  LabelerViewDetailed,\n} from '../lexicon/types/app/bsky/labeler/defs'\nimport {\n  Record as LabelerRecord,\n  isRecord as isLabelerRecord,\n} from '../lexicon/types/app/bsky/labeler/service'\nimport {\n  ActivitySubscription,\n  RecordDeleted as NotificationRecordDeleted,\n} from '../lexicon/types/app/bsky/notification/defs'\nimport { ThreadItem as ThreadOtherItem } from '../lexicon/types/app/bsky/unspecced/getPostThreadOtherV2'\nimport {\n  QueryParams as GetPostThreadV2QueryParams,\n  ThreadItem,\n} from '../lexicon/types/app/bsky/unspecced/getPostThreadV2'\nimport { isSelfLabels } from '../lexicon/types/com/atproto/label/defs'\nimport { $Typed, Un$Typed } from '../lexicon/util'\nimport { Notification } from '../proto/bsky_pb'\nimport {\n  postUriToPostgateUri,\n  postUriToThreadgateUri,\n  safePinnedPost,\n  uriToDid,\n  uriToDid as creatorFromUri,\n} from '../util/uris'\nimport {\n  ThreadItemValueBlocked,\n  ThreadItemValueNoUnauthenticated,\n  ThreadItemValueNotFound,\n  ThreadItemValuePost,\n  ThreadOtherAnchorPostNode,\n  ThreadOtherItemValuePost,\n  ThreadOtherPostNode,\n  ThreadTree,\n  ThreadTreeVisible,\n  sortTrimFlattenThreadTree,\n} from './threads-v2'\nimport {\n  Embed,\n  EmbedBlocked,\n  EmbedDetached,\n  EmbedNotFound,\n  EmbedView,\n  ExternalEmbed,\n  ExternalEmbedView,\n  ImagesEmbed,\n  ImagesEmbedView,\n  MaybePostView,\n  NotificationView,\n  PostEmbedView,\n  RecordEmbed,\n  RecordEmbedView,\n  RecordEmbedViewInternal,\n  RecordWithMedia,\n  RecordWithMediaView,\n  VideoEmbed,\n  VideoEmbedView,\n  isExternalEmbed,\n  isImagesEmbed,\n  isRecordEmbed,\n  isRecordWithMedia,\n  isVideoEmbed,\n} from './types'\nimport {\n  VideoUriBuilder,\n  cidFromBlobJson,\n  parsePostgate,\n  parseThreadGate,\n} from './util'\n\nconst notificationDeletedRecord = {\n  $type: 'app.bsky.notification.defs#recordDeleted' as const,\n}\n\n// Pre-computed CID for the `notificationDeletedRecord`.\nconst notificationDeletedRecordCid =\n  'bafyreidad6nyekfa4a67yfb573ptxiv6s7kyxyg2ra6qbbemcruadvtuim'\n\nexport class Views {\n  public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder\n  public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder\n  public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch\n  private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown\n  private threadTagsHide: readonly string[] = this.opts.threadTagsHide\n  private visibilityTagHide: string = this.opts.visibilityTagHide\n  private visibilityTagRankPrefix: string = this.opts.visibilityTagRankPrefix\n  constructor(\n    private opts: {\n      imgUriBuilder: ImageUriBuilder\n      videoUriBuilder: VideoUriBuilder\n      indexedAtEpoch: Date | undefined\n      threadTagsBumpDown: readonly string[]\n      threadTagsHide: readonly string[]\n      visibilityTagHide: string\n      visibilityTagRankPrefix: string\n    },\n  ) {}\n\n  // Actor\n  // ------------\n\n  actorIsNoHosted(did: string, state: HydrationState): boolean {\n    return (\n      this.actorIsDeactivated(did, state) || this.actorIsTakendown(did, state)\n    )\n  }\n\n  actorIsDeactivated(did: string, state: HydrationState): boolean {\n    if (state.actors?.get(did)?.upstreamStatus === 'deactivated') return true\n    return false\n  }\n\n  actorIsTakendown(did: string, state: HydrationState): boolean {\n    const actor = state.actors?.get(did)\n    if (actor?.takedownRef) return true\n    if (actor?.upstreamStatus === 'takendown') return true\n    if (actor?.upstreamStatus === 'suspended') return true\n    if (state.labels?.get(did)?.isTakendown) return true\n    return false\n  }\n\n  noUnauthenticatedPost(state: HydrationState, post: PostView): boolean {\n    const isNoUnauthenticated = post.author.labels?.some(\n      (l) => l.val === '!no-unauthenticated',\n    )\n    return !state.ctx?.viewer && !!isNoUnauthenticated\n  }\n\n  viewerBlockExists(did: string, state: HydrationState): boolean {\n    const viewer = state.profileViewers?.get(did)\n    if (!viewer) return false\n    return !!(\n      viewer.blockedBy ||\n      viewer.blocking ||\n      this.blockedByList(viewer, state) ||\n      this.blockingByList(viewer, state)\n    )\n  }\n\n  viewerMuteExists(did: string, state: HydrationState): boolean {\n    const viewer = state.profileViewers?.get(did)\n    if (!viewer) return false\n    return !!(viewer.muted || this.mutedByList(viewer, state))\n  }\n\n  blockingByList(viewer: ProfileViewerState, state: HydrationState) {\n    return (\n      viewer.blockingByList && this.recordActive(viewer.blockingByList, state)\n    )\n  }\n\n  blockedByList(viewer: ProfileViewerState, state: HydrationState) {\n    return (\n      viewer.blockedByList && this.recordActive(viewer.blockedByList, state)\n    )\n  }\n\n  mutedByList(viewer: ProfileViewerState, state: HydrationState) {\n    return viewer.mutedByList && this.recordActive(viewer.mutedByList, state)\n  }\n\n  recordActive(uri: string, state: HydrationState) {\n    const did = uriToDid(uri)\n    const actor = state.actors?.get(did)\n    if (!actor || this.actorIsTakendown(did, state)) {\n      // actor may not be present when takedowns are eagerly applied during hydration.\n      // so it's important to _try_ to hydrate the actor for records checked this way.\n      return\n    }\n    return uri\n  }\n\n  viewerSeesNeedsReview(\n    { did, uri }: { did?: string; uri?: string },\n    state: HydrationState,\n  ): boolean {\n    const { labels, profileViewers, ctx } = state\n    did = did || (uri && uriToDid(uri))\n    if (!did) {\n      return true\n    }\n    if (\n      labels?.get(did)?.needsReview ||\n      (uri && labels?.get(uri)?.needsReview)\n    ) {\n      // content marked as needs review\n      return ctx?.viewer === did || !!profileViewers?.get(did)?.following\n    }\n    return true\n  }\n\n  replyIsHiddenByThreadgate(\n    replyUri: string,\n    rootPostUri: string,\n    state: HydrationState,\n  ) {\n    const threadgateUri = postUriToThreadgateUri(rootPostUri)\n    const threadgate = state.threadgates?.get(threadgateUri)\n    return !!threadgate?.record?.hiddenReplies?.includes(replyUri)\n  }\n\n  profileDetailed(\n    did: string,\n    state: HydrationState,\n  ): Un$Typed<ProfileViewDetailed> | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor) return\n    const baseView = this.profile(did, state)\n    if (!baseView) return\n    const knownFollowers = this.knownFollowers(did, state)\n    const profileAggs = state.profileAggs?.get(did)\n\n    return {\n      ...baseView,\n      website: this.profileWebsite(did, state),\n      viewer: baseView.viewer\n        ? {\n            ...baseView.viewer,\n            knownFollowers,\n          }\n        : undefined,\n      banner: actor.profile?.banner\n        ? this.imgUriBuilder.getPresetUri(\n            'banner',\n            did,\n            cidFromBlobJson(actor.profile.banner),\n          )\n        : undefined,\n      followersCount: profileAggs?.followers ?? 0,\n      followsCount: profileAggs?.follows ?? 0,\n      postsCount: profileAggs?.posts ?? 0,\n      associated: {\n        lists: profileAggs?.lists,\n        feedgens: profileAggs?.feeds,\n        starterPacks: profileAggs?.starterPacks,\n        labeler: actor.isLabeler,\n        // @TODO apply default chat policy?\n        chat: actor.allowIncomingChatsFrom\n          ? { allowIncoming: actor.allowIncomingChatsFrom }\n          : undefined,\n        activitySubscription: this.profileAssociatedActivitySubscription(actor),\n        germ: actor.germ?.record.messageMe\n          ? {\n              showButtonTo: actor.germ.record.messageMe.showButtonTo,\n              messageMeUrl: actor.germ.record.messageMe.messageMeUrl,\n            }\n          : undefined,\n      },\n      joinedViaStarterPack: actor.profile?.joinedViaStarterPack\n        ? this.starterPackBasic(actor.profile.joinedViaStarterPack.uri, state)\n        : undefined,\n      pinnedPost: safePinnedPost(actor.profile?.pinnedPost),\n    }\n  }\n  profile(\n    did: string,\n    state: HydrationState,\n  ): Un$Typed<ProfileView> | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor) return\n    const basicView = this.profileBasic(did, state)\n    if (!basicView) return\n    return {\n      ...basicView,\n      description: actor.profile?.description || undefined,\n      indexedAt:\n        actor.indexedAt && actor.sortedAt\n          ? this.indexedAt({\n              sortedAt: actor.sortedAt,\n              indexedAt: actor.indexedAt,\n            }).toISOString()\n          : undefined,\n    }\n  }\n\n  profileBasic(\n    did: string,\n    state: HydrationState,\n  ): Un$Typed<ProfileViewBasic> | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor) return\n    const profileUri = AtUri.make(\n      did,\n      ids.AppBskyActorProfile,\n      'self',\n    ).toString()\n    const labels = [\n      ...(state.labels?.getBySubject(did) ?? []),\n      ...(state.labels?.getBySubject(profileUri) ?? []),\n      ...this.selfLabels({\n        uri: profileUri,\n        cid: actor.profileCid?.toString(),\n        record: actor.profile,\n      }),\n    ]\n    return {\n      did,\n      handle: actor.handle ?? INVALID_HANDLE,\n      displayName: actor.profile?.displayName,\n      pronouns: actor.profile?.pronouns,\n      avatar: actor.profile?.avatar\n        ? this.imgUriBuilder.getPresetUri(\n            'avatar',\n            did,\n            cidFromBlobJson(actor.profile.avatar),\n          )\n        : undefined,\n      // associated.feedgens and associated.lists info not necessarily included\n      // on profile and profile-basic views, but should be on profile-detailed.\n      associated: {\n        labeler: actor.isLabeler ? true : undefined,\n        // @TODO apply default chat policy?\n        chat: actor.allowIncomingChatsFrom\n          ? { allowIncoming: actor.allowIncomingChatsFrom }\n          : undefined,\n        activitySubscription: this.profileAssociatedActivitySubscription(actor),\n        germ: actor.germ?.record.messageMe\n          ? {\n              showButtonTo: actor.germ.record.messageMe.showButtonTo,\n              messageMeUrl: actor.germ.record.messageMe.messageMeUrl,\n            }\n          : undefined,\n      },\n      viewer: this.profileViewer(did, state),\n      labels,\n      createdAt: actor.createdAt?.toISOString(),\n      verification: this.verification(did, state),\n      status: this.status(did, state),\n      debug: state.ctx?.includeDebugField ? actor.debug : undefined,\n    }\n  }\n\n  profileAssociatedActivitySubscription(\n    actor: Actor,\n  ): ProfileAssociatedActivitySubscription {\n    return { allowSubscriptions: actor.allowActivitySubscriptionsFrom }\n  }\n\n  profileKnownFollowers(\n    did: string,\n    state: HydrationState,\n  ): ProfileView | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor) return\n    const baseView = this.profile(did, state)\n    if (!baseView) return\n    const knownFollowers = this.knownFollowers(did, state)\n    return {\n      ...baseView,\n      viewer: baseView.viewer\n        ? {\n            ...baseView.viewer,\n            knownFollowers,\n          }\n        : undefined,\n    }\n  }\n\n  profileViewer(did: string, state: HydrationState): ProfileViewer | undefined {\n    const viewer = state.profileViewers?.get(did)\n    if (!viewer) return\n    const blockedByList = this.blockedByList(viewer, state)\n    const blockedByUri = viewer.blockedBy || blockedByList\n    const blockingByList = this.blockingByList(viewer, state)\n    const blockingUri = viewer.blocking || blockingByList\n    const block = !!blockedByUri || !!blockingUri\n    const mutedByList = this.mutedByList(viewer, state)\n    return {\n      muted: !!(viewer.muted || mutedByList),\n      mutedByList: mutedByList ? this.listBasic(mutedByList, state) : undefined,\n      blockedBy: !!blockedByUri,\n      blocking: blockingUri,\n      blockingByList: blockingByList\n        ? this.listBasic(blockingByList, state)\n        : undefined,\n      following: viewer.following && !block ? viewer.following : undefined,\n      followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined,\n      activitySubscription: this.profileViewerActivitySubscription(\n        viewer,\n        did,\n        state,\n      ),\n    }\n  }\n\n  profileViewerActivitySubscription(\n    profileViewer: ProfileViewerState,\n    did: string,\n    state: HydrationState,\n  ): ActivitySubscription | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor) return undefined\n\n    const activitySubscription = state.activitySubscriptions?.get(did)\n    if (!activitySubscription) return undefined\n\n    const allowFrom = actor.allowActivitySubscriptionsFrom\n    const actorFollowsViewer = !!profileViewer.followedBy\n    const viewerFollowsActor = !!profileViewer.following\n    if (\n      (allowFrom === 'followers' && viewerFollowsActor) ||\n      (allowFrom === 'mutuals' && actorFollowsViewer && viewerFollowsActor)\n    ) {\n      return activitySubscription\n    }\n    return undefined\n  }\n\n  profileWebsite(did: string, state: HydrationState): string | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor?.profile?.website) return\n    const { website } = actor.profile\n\n    // The record property accepts any URI, but we don't want\n    // to pass the client any schemes other than HTTPS.\n    return website.startsWith('https://') ? website : undefined\n  }\n\n  knownFollowers(\n    did: string,\n    state: HydrationState,\n  ): KnownFollowers | undefined {\n    const knownFollowers = state.knownFollowers?.get(did)\n    if (!knownFollowers) return\n    const blocks = state.bidirectionalBlocks?.get(did)\n    const followers = mapDefined(knownFollowers.followers, (followerDid) => {\n      if (this.viewerBlockExists(followerDid, state)) {\n        return undefined\n      }\n      if (blocks?.get(followerDid)) {\n        return undefined\n      }\n      if (this.actorIsNoHosted(followerDid, state)) {\n        // @TODO only needed right now to work around getProfile's { includeTakedowns: true }\n        return undefined\n      }\n      return this.profileBasic(followerDid, state)\n    })\n    return { count: knownFollowers.count, followers }\n  }\n\n  verification(\n    did: string,\n    state: HydrationState,\n  ): VerificationState | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor) return\n\n    // Currently, the handle comes as \"handle.invalid\" from the production dataplane.\n    // But the contract allows for empty handle, so we cover both cases.\n    if (!actor.handle || actor.handle === INVALID_HANDLE) return\n\n    const isImpersonation = state.labels?.get(did)?.isImpersonation\n\n    const verifications: VerificationView[] = actor.verifications.map(\n      ({ issuer, uri, displayName, handle, createdAt }) => {\n        // @NOTE: We don't factor-in impersonation when evaluating the validity of each verification,\n        // only in the overall profile verification validity.\n        const isValid =\n          !!displayName &&\n          displayName === actor.profile?.displayName &&\n          !!handle &&\n          handle === actor.handle\n\n        return {\n          issuer,\n          uri,\n          isValid,\n          createdAt,\n        }\n      },\n    )\n    const hasValidVerification = verifications.some((v) => v.isValid)\n\n    const verifiedStatus = verifications.length\n      ? hasValidVerification && !isImpersonation\n        ? 'valid'\n        : 'invalid'\n      : 'none'\n    const trustedVerifierStatus = actor.trustedVerifier\n      ? isImpersonation\n        ? 'invalid'\n        : 'valid'\n      : 'none'\n\n    if (\n      verifications.length === 0 &&\n      verifiedStatus === 'none' &&\n      trustedVerifierStatus === 'none'\n    ) {\n      return undefined\n    }\n\n    return {\n      verifications,\n      verifiedStatus,\n      trustedVerifierStatus,\n    }\n  }\n\n  status(did: string, state: HydrationState): StatusView | undefined {\n    const actor = state.actors?.get(did)\n    if (!actor?.status) return\n\n    const isViewerStatusOwner = did === state.ctx?.viewer\n    const { status } = actor\n    const { record, sortedAt, cid, takedownRef } = status\n    const isTakenDown = !!takedownRef\n\n    /*\n     * Manual filter for takendown status records. If this is ever removed, we\n     * need to reinstate `includeTakedowns` handling in the `Actor.getActors`\n     * hydrator.\n     */\n    if (isTakenDown && !isViewerStatusOwner) {\n      return undefined\n    }\n\n    const uri = AtUri.make(did, ids.AppBskyActorStatus, 'self').toString()\n\n    const minDuration = 5 * MINUTE\n    const maxDuration = 4 * HOUR\n\n    const expiresAtMs = record.durationMinutes\n      ? sortedAt.getTime() +\n        Math.max(\n          Math.min(record.durationMinutes * MINUTE, maxDuration),\n          minDuration,\n        )\n      : undefined\n    const expiresAt = expiresAtMs\n      ? new Date(expiresAtMs).toISOString()\n      : undefined\n\n    const isActive = expiresAtMs ? expiresAtMs > Date.now() : undefined\n\n    const response: StatusView = {\n      uri,\n      cid,\n      record: record,\n      status: record.status,\n      embed: isExternalEmbed(record.embed)\n        ? this.externalEmbed(did, record.embed)\n        : undefined,\n      expiresAt,\n      isActive,\n    }\n\n    if (isViewerStatusOwner) {\n      response.isDisabled = isTakenDown\n    }\n\n    return response\n  }\n\n  blockedProfileViewer(\n    did: string,\n    state: HydrationState,\n  ): ProfileViewer | undefined {\n    const viewer = state.profileViewers?.get(did)\n    if (!viewer) return\n    const blockedByUri = viewer.blockedBy || this.blockedByList(viewer, state)\n    const blockingUri = viewer.blocking || this.blockingByList(viewer, state)\n    return {\n      blockedBy: !!blockedByUri,\n      blocking: blockingUri,\n    }\n  }\n\n  // Graph\n  // ------------\n\n  list(uri: string, state: HydrationState): Un$Typed<ListView> | undefined {\n    const creatorDid = creatorFromUri(uri)\n    const list = state.lists?.get(uri)\n    if (!list) return\n    const creator = this.profile(creatorDid, state)\n    if (!creator) return\n    const basicView = this.listBasic(uri, state)\n    if (!basicView) return\n\n    return {\n      ...basicView,\n      creator,\n      description: list.record.description,\n      descriptionFacets: list.record.descriptionFacets,\n      indexedAt: this.indexedAt(list).toISOString(),\n    }\n  }\n\n  listBasic(\n    uri: string,\n    state: HydrationState,\n  ): Un$Typed<ListViewBasic> | undefined {\n    const list = state.lists?.get(uri)\n    if (!list) {\n      return undefined\n    }\n    const listAgg = state.listAggs?.get(uri)\n    const listViewer = state.listViewers?.get(uri)\n    const labels = state.labels?.getBySubject(uri) ?? []\n    const creator = creatorFromUri(uri)\n    return {\n      uri,\n      cid: list.cid,\n      name: list.record.name,\n      purpose: list.record.purpose,\n      avatar: list.record.avatar\n        ? this.imgUriBuilder.getPresetUri(\n            'avatar',\n            creator,\n            cidFromBlobJson(list.record.avatar),\n          )\n        : undefined,\n      listItemCount: listAgg?.listItems ?? 0,\n      indexedAt: this.indexedAt(list).toISOString(),\n      labels,\n      viewer: listViewer\n        ? {\n            muted: !!listViewer.viewerMuted,\n            blocked: listViewer.viewerListBlockUri,\n          }\n        : undefined,\n    }\n  }\n\n  listItemView(\n    uri: string,\n    did: string,\n    state: HydrationState,\n  ): Un$Typed<ListItemView> | undefined {\n    const subject = this.profile(did, state)\n    if (!subject) return\n    return { uri, subject }\n  }\n\n  starterPackBasic(\n    uri: string,\n    state: HydrationState,\n  ): Un$Typed<StarterPackViewBasic> | undefined {\n    const sp = state.starterPacks?.get(uri)\n    if (!sp) return\n    const parsedUri = new AtUri(uri)\n    const creator = this.profileBasic(parsedUri.hostname, state)\n    if (!creator) return\n    const agg = state.starterPackAggs?.get(uri)\n    const labels = state.labels?.getBySubject(uri) ?? []\n    return {\n      uri,\n      cid: sp.cid,\n      record: sp.record,\n      creator,\n      joinedAllTimeCount: agg?.joinedAllTime ?? 0,\n      joinedWeekCount: agg?.joinedWeek ?? 0,\n      labels,\n      indexedAt: this.indexedAt(sp).toISOString(),\n    }\n  }\n\n  starterPack(\n    uri: string,\n    state: HydrationState,\n  ): Un$Typed<StarterPackView> | undefined {\n    const sp = state.starterPacks?.get(uri)\n    const basicView = this.starterPackBasic(uri, state)\n    if (!sp || !basicView) return\n    const agg = state.starterPackAggs?.get(uri)\n    const feeds = mapDefined(sp.record.feeds ?? [], (feed) =>\n      this.feedGenerator(feed.uri, state),\n    )\n    const list = this.listBasic(sp.record.list, state)\n    const listItemsSample = mapDefined(agg?.listItemSampleUris ?? [], (uri) => {\n      const li = state.listItems?.get(uri)\n      if (!li) return\n      const subject = this.profile(li.record.subject, state)\n      if (!subject) return\n      return { uri, subject }\n    })\n    return {\n      ...basicView,\n      feeds,\n      list,\n      listItemsSample,\n    }\n  }\n\n  // Labels\n  // ------------\n\n  selfLabels({\n    uri,\n    cid,\n    record,\n  }: {\n    uri?: string\n    cid?: string\n    record?:\n      | PostRecord\n      | LikeRecord\n      | RepostRecord\n      | FollowRecord\n      | ProfileRecord\n      | LabelerRecord\n      | VerificationRecord\n      | NotificationRecordDeleted\n  }): Label[] {\n    if (!uri || !cid || !record) return []\n\n    // Only these have a \"labels\" property:\n    if (\n      !isPostRecord(record) &&\n      !isProfileRecord(record) &&\n      !isLabelerRecord(record)\n    ) {\n      return []\n    }\n\n    // Ignore if no labels defines\n    if (!isSelfLabels(record.labels) || !record.labels.values.length) {\n      return []\n    }\n\n    const src = creatorFromUri(uri) // record creator\n    const cts =\n      typeof record.createdAt === 'string'\n        ? normalizeDatetimeAlways(record.createdAt)\n        : new Date(0).toISOString()\n    return record.labels.values.map(({ val }) => {\n      return { src, uri, cid, val, cts }\n    })\n  }\n\n  labeler(\n    did: string,\n    state: HydrationState,\n  ): Un$Typed<LabelerView> | undefined {\n    const labeler = state.labelers?.get(did)\n    if (!labeler) return\n    const creator = this.profile(did, state)\n    if (!creator) return\n    const viewer = state.labelerViewers?.get(did)\n    const aggs = state.labelerAggs?.get(did)\n\n    const uri = AtUri.make(did, ids.AppBskyLabelerService, 'self').toString()\n    const labels = [\n      ...(state.labels?.getBySubject(uri) ?? []),\n      ...this.selfLabels({\n        uri,\n        cid: labeler.cid.toString(),\n        record: labeler.record,\n      }),\n    ]\n\n    return {\n      uri,\n      cid: labeler.cid.toString(),\n      creator,\n      likeCount: aggs?.likes ?? 0,\n      viewer: viewer\n        ? {\n            like: viewer.like,\n          }\n        : undefined,\n      indexedAt: this.indexedAt(labeler).toISOString(),\n      labels,\n    }\n  }\n\n  labelerDetailed(\n    did: string,\n    state: HydrationState,\n  ): Un$Typed<LabelerViewDetailed> | undefined {\n    const baseView = this.labeler(did, state)\n    if (!baseView) return\n    const labeler = state.labelers?.get(did)\n    if (!labeler) return\n\n    return {\n      ...baseView,\n      policies: labeler.record.policies,\n      reasonTypes: labeler.record.reasonTypes,\n      subjectTypes: labeler.record.subjectTypes,\n      subjectCollections: labeler.record.subjectCollections,\n    }\n  }\n\n  // Feed\n  // ------------\n\n  feedItemBlocksAndMutes(\n    item: FeedItem,\n    state: HydrationState,\n  ): {\n    originatorMuted: boolean\n    originatorBlocked: boolean\n    authorMuted: boolean\n    authorBlocked: boolean\n    ancestorAuthorBlocked: boolean\n  } {\n    const authorDid = creatorFromUri(item.post.uri)\n    const originatorDid = item.repost\n      ? creatorFromUri(item.repost.uri)\n      : authorDid\n    const post = state.posts?.get(item.post.uri)\n    const parentUri = post?.record.reply?.parent.uri\n    const parentAuthorDid = parentUri && creatorFromUri(parentUri)\n    const parent = parentUri ? state.posts?.get(parentUri) : undefined\n    const grandparentUri = parent?.record.reply?.parent.uri\n    const grandparentAuthorDid =\n      grandparentUri && creatorFromUri(grandparentUri)\n    return {\n      originatorMuted: this.viewerMuteExists(originatorDid, state),\n      originatorBlocked: this.viewerBlockExists(originatorDid, state),\n      authorMuted: this.viewerMuteExists(authorDid, state),\n      authorBlocked: this.viewerBlockExists(authorDid, state),\n      ancestorAuthorBlocked:\n        (!!parentAuthorDid && this.viewerBlockExists(parentAuthorDid, state)) ||\n        (!!grandparentAuthorDid &&\n          this.viewerBlockExists(grandparentAuthorDid, state)),\n    }\n  }\n\n  feedGenerator(\n    uri: string,\n    state: HydrationState,\n  ): Un$Typed<GeneratorView> | undefined {\n    const feedgen = state.feedgens?.get(uri)\n    if (!feedgen) return\n    const creatorDid = creatorFromUri(uri)\n    const creator = this.profile(creatorDid, state)\n    if (!creator) return\n    const viewer = state.feedgenViewers?.get(uri)\n    const aggs = state.feedgenAggs?.get(uri)\n    const labels = state.labels?.getBySubject(uri) ?? []\n\n    return {\n      uri,\n      cid: feedgen.cid,\n      did: feedgen.record.did,\n      creator,\n      displayName: feedgen.record.displayName,\n      description: feedgen.record.description,\n      descriptionFacets: feedgen.record.descriptionFacets,\n      avatar: feedgen.record.avatar\n        ? this.imgUriBuilder.getPresetUri(\n            'avatar',\n            creatorDid,\n            cidFromBlobJson(feedgen.record.avatar),\n          )\n        : undefined,\n      likeCount: aggs?.likes ?? 0,\n      acceptsInteractions: feedgen.record.acceptsInteractions,\n      labels,\n      viewer: viewer\n        ? {\n            like: viewer.like,\n          }\n        : undefined,\n      contentMode: feedgen.record.contentMode,\n      indexedAt: this.indexedAt(feedgen).toISOString(),\n    }\n  }\n\n  threadgate(\n    uri: string,\n    state: HydrationState,\n  ): Un$Typed<ThreadgateView> | undefined {\n    const gate = state.threadgates?.get(uri)\n    if (!gate) return\n    return {\n      uri,\n      cid: gate.cid,\n      record: gate.record,\n      lists: mapDefined(gate.record.allow ?? [], (rule) => {\n        if (!isListRule(rule)) return\n        return this.listBasic(rule.list, state)\n      }),\n    }\n  }\n\n  post(\n    uri: string,\n    state: HydrationState,\n    depth = 0,\n  ): Un$Typed<PostView> | undefined {\n    const post = state.posts?.get(uri)\n    if (!post) return\n    const parsedUri = new AtUri(uri)\n    const authorDid = parsedUri.hostname\n    const author = this.profileBasic(authorDid, state)\n    if (!author) return\n    const aggs = state.postAggs?.get(uri)\n    const viewer = state.postViewers?.get(uri)\n    const threadgateUri = postUriToThreadgateUri(uri)\n    const labels = [\n      ...(state.labels?.getBySubject(uri) ?? []),\n      ...this.selfLabels({\n        uri,\n        cid: post.cid,\n        record: post.record,\n      }),\n    ]\n    return {\n      uri,\n      cid: post.cid,\n      author,\n      record: post.record,\n      embed:\n        depth < 2 && post.record.embed\n          ? this.embed(uri, post.record.embed, state, depth + 1)\n          : undefined,\n      bookmarkCount: aggs?.bookmarks ?? 0,\n      replyCount: aggs?.replies ?? 0,\n      repostCount: aggs?.reposts ?? 0,\n      likeCount: aggs?.likes ?? 0,\n      quoteCount: aggs?.quotes ?? 0,\n      indexedAt: this.indexedAt(post).toISOString(),\n      viewer: viewer\n        ? {\n            repost: viewer.repost,\n            like: viewer.like,\n            bookmarked: viewer.bookmarked,\n            threadMuted: viewer.threadMuted,\n            replyDisabled: this.userReplyDisabled(uri, state),\n            embeddingDisabled: this.userPostEmbeddingDisabled(uri, state),\n            pinned: this.viewerPinned(uri, state, authorDid),\n          }\n        : undefined,\n      labels,\n      threadgate: !post.record.reply // only hydrate gate on root post\n        ? this.threadgate(threadgateUri, state)\n        : undefined,\n      debug: state.ctx?.includeDebugField\n        ? { post: post.debug, author: author.debug }\n        : undefined,\n    }\n  }\n\n  feedViewPost(\n    item: FeedItem,\n    state: HydrationState,\n  ): Un$Typed<FeedViewPost> | undefined {\n    const postInfo = state.posts?.get(item.post.uri)\n    let reason: $Typed<ReasonRepost> | $Typed<ReasonPin> | undefined\n    if (item.authorPinned) {\n      reason = this.reasonPin()\n    } else if (item.repost) {\n      const repost = state.reposts?.get(item.repost.uri)\n      if (!repost) return\n      if (repost.record.subject.uri !== item.post.uri) return\n      reason = this.reasonRepost(item.repost.uri, repost, state)\n      if (!reason) return\n    }\n    const post = this.post(item.post.uri, state)\n    if (!post) return\n    const reply = !postInfo?.violatesThreadGate\n      ? this.replyRef(item.post.uri, state)\n      : undefined\n    return {\n      post,\n      reason,\n      reply,\n    }\n  }\n\n  replyRef(uri: string, state: HydrationState): Un$Typed<ReplyRef> | undefined {\n    const postRecord = state.posts?.get(uri.toString())?.record\n    if (!postRecord?.reply) return\n    let root = this.maybePost(postRecord.reply.root.uri, state)\n    let parent = this.maybePost(postRecord.reply.parent.uri, state)\n    if (!state.ctx?.include3pBlocks) {\n      const childBlocks = state.postBlocks?.get(uri)\n      const parentBlocks = state.postBlocks?.get(parent.uri)\n      // if child blocks parent, block parent\n      if (isPostView(parent) && childBlocks?.parent) {\n        parent = this.blockedPost(parent.uri, parent.author.did, state)\n      }\n      // if child or parent blocks root, block root\n      if (isPostView(root) && (childBlocks?.root || parentBlocks?.root)) {\n        root = this.blockedPost(root.uri, root.author.did, state)\n      }\n    }\n    let grandparentAuthor: ProfileViewBasic | undefined\n    if (\n      isPostView(parent) &&\n      isPostRecord(parent.record) &&\n      parent.record.reply\n    ) {\n      grandparentAuthor = this.profileBasic(\n        // @ts-expect-error isValidPostRecord(parent.record) should be used but the \"parent\" is not IPDL decoded\n        creatorFromUri(parent.record.reply.parent.uri),\n        state,\n      )\n    }\n    return {\n      root,\n      parent,\n      grandparentAuthor,\n    }\n  }\n\n  maybePost(uri: string, state: HydrationState): $Typed<MaybePostView> {\n    const post = this.post(uri, state)\n    if (!post) {\n      return this.notFoundPost(uri)\n    }\n    if (this.viewerBlockExists(post.author.did, state)) {\n      return this.blockedPost(uri, post.author.did, state)\n    }\n    return {\n      ...post,\n      $type: 'app.bsky.feed.defs#postView',\n    }\n  }\n\n  blockedPost(\n    uri: string,\n    authorDid: string,\n    state: HydrationState,\n  ): $Typed<BlockedPost> {\n    return {\n      $type: 'app.bsky.feed.defs#blockedPost',\n      uri,\n      blocked: true,\n      author: {\n        did: authorDid,\n        viewer: this.blockedProfileViewer(authorDid, state),\n      },\n    }\n  }\n\n  notFoundPost(uri: string): $Typed<NotFoundPost> {\n    return {\n      $type: 'app.bsky.feed.defs#notFoundPost',\n      uri,\n      notFound: true,\n    }\n  }\n\n  reasonRepost(\n    uri: string,\n    repost: Repost,\n    state: HydrationState,\n  ): $Typed<ReasonRepost> | undefined {\n    const creatorDid = creatorFromUri(uri)\n    const creator = this.profileBasic(creatorDid, state)\n    if (!creator) return\n    return {\n      $type: 'app.bsky.feed.defs#reasonRepost',\n      by: creator,\n      uri,\n      cid: repost.cid,\n      indexedAt: this.indexedAt(repost).toISOString(),\n    }\n  }\n\n  reasonPin(): $Typed<ReasonPin> {\n    return {\n      $type: 'app.bsky.feed.defs#reasonPin',\n    }\n  }\n\n  // Bookmarks\n  // ------------\n  bookmark(\n    key: string,\n    state: HydrationState,\n  ): Un$Typed<BookmarkView> | undefined {\n    const viewer = state.ctx?.viewer\n    if (!viewer) return\n\n    const bookmark = state.bookmarks?.get(viewer)?.get(key)\n    if (!bookmark) return\n\n    const atUri = new AtUri(bookmark.subjectUri)\n    if (atUri.collection !== ids.AppBskyFeedPost) return\n\n    const item = this.maybePost(bookmark.subjectUri, state)\n    return {\n      createdAt: bookmark.indexedAt?.toISOString(),\n      subject: {\n        uri: bookmark.subjectUri,\n        cid: bookmark.subjectCid,\n      },\n      item,\n    }\n  }\n\n  // Threads\n  // ------------\n\n  thread(\n    skele: { anchor: string; uris: string[] },\n    state: HydrationState,\n    opts: { height: number; depth: number },\n  ): $Typed<ThreadViewPost> | $Typed<NotFoundPost> | $Typed<BlockedPost> {\n    const { anchor, uris } = skele\n    const post = this.post(anchor, state)\n    const postInfo = state.posts?.get(anchor)\n    if (!postInfo || !post) return this.notFoundPost(anchor)\n    if (this.viewerBlockExists(post.author.did, state)) {\n      return this.blockedPost(anchor, post.author.did, state)\n    }\n    const includedPosts = new Set<string>([anchor])\n    const childrenByParentUri: Record<string, string[]> = {}\n    uris.forEach((uri) => {\n      const post = state.posts?.get(uri)\n      const parentUri = post?.record.reply?.parent.uri\n      if (!parentUri) return\n      if (includedPosts.has(uri)) return\n      includedPosts.add(uri)\n      childrenByParentUri[parentUri] ??= []\n      childrenByParentUri[parentUri].push(uri)\n    })\n    const rootUri = getRootUri(anchor, postInfo)\n    const violatesThreadGate = postInfo.violatesThreadGate\n\n    return {\n      $type: 'app.bsky.feed.defs#threadViewPost',\n      post,\n      parent: !violatesThreadGate\n        ? this.threadParent(anchor, rootUri, state, opts.height)\n        : undefined,\n      replies: !violatesThreadGate\n        ? this.threadReplies(\n            anchor,\n            rootUri,\n            childrenByParentUri,\n            state,\n            opts.depth,\n          )\n        : undefined,\n      threadContext: {\n        rootAuthorLike: state.threadContexts?.get(post.uri)?.like,\n      },\n    }\n  }\n\n  threadParent(\n    childUri: string,\n    rootUri: string,\n    state: HydrationState,\n    height: number,\n  ):\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | undefined {\n    if (height < 1) return undefined\n    const parentUri = state.posts?.get(childUri)?.record.reply?.parent.uri\n    if (!parentUri) return undefined\n    if (\n      !state.ctx?.include3pBlocks &&\n      state.postBlocks?.get(childUri)?.parent\n    ) {\n      return this.blockedPost(parentUri, creatorFromUri(parentUri), state)\n    }\n    const post = this.post(parentUri, state)\n    const postInfo = state.posts?.get(parentUri)\n    if (!postInfo || !post) return this.notFoundPost(parentUri)\n    if (rootUri !== getRootUri(parentUri, postInfo)) return // outside thread boundary\n    if (this.viewerBlockExists(post.author.did, state)) {\n      return this.blockedPost(parentUri, post.author.did, state)\n    }\n    return {\n      $type: 'app.bsky.feed.defs#threadViewPost',\n      post,\n      parent: this.threadParent(parentUri, rootUri, state, height - 1),\n      threadContext: {\n        rootAuthorLike: state.threadContexts?.get(post.uri)?.like,\n      },\n    }\n  }\n\n  threadReplies(\n    parentUri: string,\n    rootUri: string,\n    childrenByParentUri: Record<string, string[]>,\n    state: HydrationState,\n    depth: number,\n  ): ($Typed<ThreadViewPost> | $Typed<BlockedPost>)[] | undefined {\n    if (depth < 1) return undefined\n    const childrenUris = childrenByParentUri[parentUri] ?? []\n    return mapDefined(childrenUris, (uri) => {\n      const postInfo = state.posts?.get(uri)\n      if (postInfo?.violatesThreadGate) {\n        return undefined\n      }\n      if (!state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent) {\n        return undefined\n      }\n      const post = this.post(uri, state)\n      if (!postInfo || !post) {\n        // in the future we might consider keeping a placeholder for deleted\n        // posts that have replies under them, but not supported at the moment.\n        // this case is mostly likely hit when a takedown was applied to a post.\n        return undefined\n      }\n      if (rootUri !== getRootUri(uri, postInfo)) return // outside thread boundary\n      if (this.viewerBlockExists(post.author.did, state)) {\n        return this.blockedPost(uri, post.author.did, state)\n      }\n      if (!this.viewerSeesNeedsReview({ uri, did: post.author.did }, state)) {\n        return undefined\n      }\n      return {\n        $type: 'app.bsky.feed.defs#threadViewPost',\n        post,\n        replies: this.threadReplies(\n          uri,\n          rootUri,\n          childrenByParentUri,\n          state,\n          depth - 1,\n        ),\n        threadContext: {\n          rootAuthorLike: state.threadContexts?.get(post.uri)?.like,\n        },\n      }\n    })\n  }\n\n  // Threads V2\n  // ------------\n\n  threadV2(\n    skeleton: { anchor: string; uris: string[] },\n    state: HydrationState,\n    {\n      above,\n      below,\n      branchingFactor,\n      sort,\n    }: {\n      above: number\n      below: number\n      branchingFactor: number\n      sort: GetPostThreadV2QueryParams['sort']\n    },\n  ): { hasOtherReplies: boolean; thread: ThreadItem[] } {\n    const { anchor: anchorUri, uris } = skeleton\n\n    // Not found.\n    const postView = this.post(anchorUri, state)\n    const post = state.posts?.get(anchorUri)\n    if (!post || !postView) {\n      return {\n        hasOtherReplies: false,\n        thread: [\n          this.threadV2ItemNotFound({\n            uri: anchorUri,\n            depth: 0,\n          }),\n        ],\n      }\n    }\n\n    // Blocked (only 1p for anchor).\n    if (this.viewerBlockExists(postView.author.did, state)) {\n      return {\n        hasOtherReplies: false,\n        thread: [\n          this.threadV2ItemBlocked({\n            uri: anchorUri,\n            depth: 0,\n            authorDid: postView.author.did,\n            state,\n          }),\n        ],\n      }\n    }\n\n    const childrenByParentUri = this.groupThreadChildrenByParent(\n      anchorUri,\n      uris,\n      state,\n    )\n    const rootUri = getRootUri(anchorUri, post)\n    const opDid = uriToDid(rootUri)\n    const authorDid = postView.author.did\n    const isOPPost = authorDid === opDid\n    const anchorViolatesThreadGate = post.violatesThreadGate\n\n    // Builds the parent tree, and whether it is a contiguous OP thread.\n    const parentTree = !anchorViolatesThreadGate\n      ? this.threadV2Parent(\n          {\n            childUri: anchorUri,\n            opDid,\n            rootUri,\n\n            above,\n            depth: -1,\n          },\n          state,\n        )\n      : undefined\n\n    const { tree: parent, isOPThread: isOPThreadFromRootToParent } =\n      parentTree ?? { tree: undefined, isOPThread: false }\n\n    const isOPThread = parent\n      ? isOPThreadFromRootToParent && isOPPost\n      : isOPPost\n\n    const anchorDepth = 0 // The depth of the anchor post is always 0.\n    let anchorTree: ThreadTree\n    let hasOtherReplies = false\n\n    if (this.noUnauthenticatedPost(state, postView)) {\n      anchorTree = {\n        type: 'noUnauthenticated',\n        item: this.threadV2ItemNoUnauthenticated({\n          uri: anchorUri,\n          depth: anchorDepth,\n        }),\n        parent,\n      }\n    } else {\n      const { replies, hasOtherReplies: hasOtherRepliesShadow } =\n        !anchorViolatesThreadGate\n          ? this.threadV2Replies(\n              {\n                parentUri: anchorUri,\n                isOPThread,\n                opDid,\n                rootUri,\n                childrenByParentUri,\n                below,\n                depth: 1,\n                branchingFactor,\n              },\n              state,\n            )\n          : { replies: undefined, hasOtherReplies: false }\n      hasOtherReplies = hasOtherRepliesShadow\n\n      anchorTree = {\n        type: 'post',\n        item: this.threadV2ItemPost({\n          depth: anchorDepth,\n          isOPThread,\n          postView,\n          repliesAllowance: Infinity, // While we don't have pagination.\n          uri: anchorUri,\n        }),\n        tags: post.tags,\n        hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,\n        parent,\n        replies,\n      }\n    }\n\n    const thread = sortTrimFlattenThreadTree(\n      anchorTree,\n      {\n        opDid,\n        branchingFactor,\n        sort,\n        viewer: state.ctx?.viewer ?? null,\n        threadTagsBumpDown: this.threadTagsBumpDown,\n        threadTagsHide: this.threadTagsHide,\n        visibilityTagRankPrefix: this.visibilityTagRankPrefix,\n      },\n      state.ctx?.features.checkGate(\n        state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n      ),\n    )\n\n    return {\n      hasOtherReplies,\n      thread,\n    }\n  }\n\n  private threadV2Parent(\n    {\n      childUri,\n      opDid,\n      rootUri,\n      above,\n      depth,\n    }: {\n      childUri: string\n      opDid: string\n      rootUri: string\n      above: number\n      depth: number\n    },\n    state: HydrationState,\n  ): { tree: ThreadTreeVisible; isOPThread: boolean } | undefined {\n    // Reached the `above` limit.\n    if (Math.abs(depth) > above) {\n      return undefined\n    }\n\n    // Not found.\n    const uri = state.posts?.get(childUri)?.record.reply?.parent.uri\n    if (!uri) {\n      return undefined\n    }\n    const postView = this.post(uri, state)\n    const post = state.posts?.get(uri)\n    if (!post || !postView) {\n      return {\n        tree: {\n          type: 'notFound',\n          item: this.threadV2ItemNotFound({ uri, depth }),\n        },\n        isOPThread: false,\n      }\n    }\n    if (rootUri !== getRootUri(uri, post)) {\n      // Outside thread boundary.\n      return undefined\n    }\n\n    // Blocked (1p and 3p for parent).\n    const authorDid = postView.author.did\n    const has1pBlock = this.viewerBlockExists(authorDid, state)\n    const has3pBlock =\n      !state.ctx?.include3pBlocks && state.postBlocks?.get(childUri)?.parent\n    if (has1pBlock || has3pBlock) {\n      return {\n        tree: {\n          type: 'blocked',\n          item: this.threadV2ItemBlocked({\n            uri,\n            depth,\n            authorDid,\n            state,\n          }),\n        },\n        isOPThread: false,\n      }\n    }\n\n    // Recurse up.\n    const parentTree = this.threadV2Parent(\n      {\n        childUri: uri,\n        opDid,\n        rootUri,\n        above,\n        depth: depth - 1,\n      },\n      state,\n    )\n    const { tree: parent, isOPThread: isOPThreadFromRootToParent } =\n      parentTree ?? { tree: undefined, isOPThread: false }\n\n    const isOPPost = authorDid === opDid\n    const isOPThread = parent\n      ? isOPThreadFromRootToParent && isOPPost\n      : isOPPost\n\n    if (this.noUnauthenticatedPost(state, postView)) {\n      return {\n        tree: {\n          type: 'noUnauthenticated',\n          item: this.threadV2ItemNoUnauthenticated({\n            uri,\n            depth,\n          }),\n          parent,\n        },\n        isOPThread,\n      }\n    }\n\n    const parentUri = post.record.reply?.parent.uri\n    const hasMoreParents = !!parentUri && !parent\n\n    return {\n      tree: {\n        type: 'post',\n        item: this.threadV2ItemPost({\n          depth,\n          isOPThread,\n          moreParents: hasMoreParents,\n          postView,\n          uri,\n        }),\n        tags: post.tags,\n        hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,\n        parent,\n        replies: undefined,\n      },\n      isOPThread,\n    }\n  }\n\n  private threadV2Replies(\n    {\n      parentUri,\n      isOPThread: isOPThreadFromRootToParent,\n      opDid,\n      rootUri,\n      childrenByParentUri,\n      below,\n      depth,\n      branchingFactor,\n    }: {\n      parentUri: string\n      isOPThread: boolean\n      opDid: string\n      rootUri: string\n      childrenByParentUri: Record<string, string[]>\n      below: number\n      depth: number\n      branchingFactor: number\n    },\n    state: HydrationState,\n  ): { replies: ThreadTreeVisible[] | undefined; hasOtherReplies: boolean } {\n    // Reached the `below` limit.\n    if (depth > below) {\n      return { replies: undefined, hasOtherReplies: false }\n    }\n\n    const childrenUris = childrenByParentUri[parentUri] ?? []\n    let hasOtherReplies = false\n    const replies = mapDefined(childrenUris, (uri) => {\n      const replyInclusion = this.checkThreadV2ReplyInclusion({\n        uri,\n        rootUri,\n        state,\n      })\n      if (!replyInclusion) {\n        return undefined\n      }\n      const { authorDid, post, postView } = replyInclusion\n\n      // Hidden.\n      const { isOther } = this.isOtherThreadPost(\n        { post, postView, rootUri, uri },\n        state,\n      )\n      if (isOther) {\n        // Only care about anchor replies\n        if (depth === 1) {\n          hasOtherReplies = true\n        }\n        return undefined\n      }\n\n      // Recurse down.\n      const isOPThread = isOPThreadFromRootToParent && authorDid === opDid\n      const { replies: nestedReplies } = this.threadV2Replies(\n        {\n          parentUri: uri,\n          isOPThread,\n          opDid,\n          rootUri,\n          childrenByParentUri,\n          below,\n          depth: depth + 1,\n          branchingFactor,\n        },\n        state,\n      )\n\n      const reachedDepth = depth === below\n      const repliesAllowance = reachedDepth ? 0 : branchingFactor\n\n      const tree: ThreadTree = {\n        type: 'post',\n        item: this.threadV2ItemPost({\n          depth,\n          isOPThread,\n          postView,\n          repliesAllowance,\n          uri,\n        }),\n        tags: post.tags,\n        hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,\n        parent: undefined,\n        replies: nestedReplies,\n      }\n\n      return tree\n    })\n\n    return {\n      replies,\n      hasOtherReplies,\n    }\n  }\n\n  private threadV2ItemPost({\n    depth,\n    isOPThread,\n    moreParents,\n    postView,\n    repliesAllowance,\n    uri,\n  }: {\n    depth: number\n    isOPThread: boolean\n    moreParents?: boolean\n    postView: PostView\n    repliesAllowance?: number\n    uri: string\n  }): ThreadItemValuePost {\n    const moreReplies =\n      repliesAllowance === undefined\n        ? 0\n        : Math.max((postView.replyCount ?? 0) - repliesAllowance, 0)\n\n    return {\n      uri,\n      depth,\n      value: {\n        $type: 'app.bsky.unspecced.defs#threadItemPost',\n        post: postView,\n        moreParents: moreParents ?? false,\n        moreReplies,\n        opThread: isOPThread,\n        hiddenByThreadgate: false, // Hidden posts are handled by threadOtherV2\n        mutedByViewer: false, // Hidden posts are handled by threadOtherV2\n      },\n    }\n  }\n\n  private threadV2ItemNoUnauthenticated({\n    uri,\n    depth,\n  }: {\n    uri: string\n    depth: number\n  }): ThreadItemValueNoUnauthenticated {\n    return {\n      uri,\n      depth,\n      value: {\n        $type: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n      },\n    }\n  }\n\n  private threadV2ItemNotFound({\n    uri,\n    depth,\n  }: {\n    uri: string\n    depth: number\n  }): ThreadItemValueNotFound {\n    return {\n      uri,\n      depth,\n      value: {\n        $type: 'app.bsky.unspecced.defs#threadItemNotFound',\n      },\n    }\n  }\n\n  private threadV2ItemBlocked({\n    uri,\n    depth,\n    authorDid,\n    state,\n  }: {\n    uri: string\n    depth: number\n    authorDid: string\n    state: HydrationState\n  }): ThreadItemValueBlocked {\n    return {\n      uri,\n      depth,\n      value: {\n        $type: 'app.bsky.unspecced.defs#threadItemBlocked',\n        author: {\n          did: authorDid,\n          viewer: this.blockedProfileViewer(authorDid, state),\n        },\n      },\n    }\n  }\n\n  threadOtherV2(\n    skeleton: { anchor: string; uris: string[] },\n    state: HydrationState,\n    {\n      below,\n      branchingFactor,\n    }: {\n      below: number\n      branchingFactor: number\n    },\n  ): ThreadOtherItem[] {\n    const { anchor: anchorUri, uris } = skeleton\n\n    // Not found.\n    const postView = this.post(anchorUri, state)\n    const post = state.posts?.get(anchorUri)\n    if (!post || !postView) {\n      return []\n    }\n\n    // Blocked (only 1p for anchor).\n    if (this.viewerBlockExists(postView.author.did, state)) {\n      return []\n    }\n\n    const childrenByParentUri = this.groupThreadChildrenByParent(\n      anchorUri,\n      uris,\n      state,\n    )\n    const rootUri = getRootUri(anchorUri, post)\n    const opDid = uriToDid(rootUri)\n\n    const anchorTree: ThreadOtherAnchorPostNode = {\n      type: 'hiddenAnchor',\n      item: this.threadOtherV2ItemPostAnchor({ depth: 0, uri: anchorUri }),\n      replies: this.threadOtherV2Replies(\n        {\n          parentUri: anchorUri,\n          rootUri,\n          childrenByParentUri,\n          below,\n          depth: 1,\n        },\n        state,\n      ),\n    }\n\n    return sortTrimFlattenThreadTree(\n      anchorTree,\n      {\n        opDid,\n        branchingFactor,\n        viewer: state.ctx?.viewer ?? null,\n        threadTagsBumpDown: this.threadTagsBumpDown,\n        threadTagsHide: this.threadTagsHide,\n        visibilityTagRankPrefix: this.visibilityTagRankPrefix,\n      },\n      state.ctx?.features.checkGate(\n        state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n      ),\n    )\n  }\n\n  private threadOtherV2Replies(\n    {\n      parentUri,\n      rootUri,\n      childrenByParentUri,\n      below,\n      depth,\n    }: {\n      parentUri: string\n      rootUri: string\n      childrenByParentUri: Record<string, string[]>\n      below: number\n      depth: number\n    },\n    state: HydrationState,\n  ): ThreadOtherPostNode[] | undefined {\n    // Reached the `below` limit.\n    if (depth > below) {\n      return undefined\n    }\n\n    const childrenUris = childrenByParentUri[parentUri] ?? []\n    return mapDefined(childrenUris, (uri) => {\n      const replyInclusion = this.checkThreadV2ReplyInclusion({\n        uri,\n        rootUri,\n        state,\n      })\n      if (!replyInclusion) {\n        return undefined\n      }\n      const { post, postView } = replyInclusion\n\n      // Other posts to pull out\n      const { isOther, hiddenByThreadgate, mutedByViewer } =\n        this.isOtherThreadPost({ post, postView, rootUri, uri }, state)\n      if (isOther) {\n        // Only show hidden anchor replies, not all hidden.\n        if (depth > 1) {\n          return undefined\n        }\n      } else if (depth === 1) {\n        // Don't include non-hidden anchor replies.\n        return undefined\n      }\n\n      // Recurse down.\n      const replies = this.threadOtherV2Replies(\n        {\n          parentUri: uri,\n          rootUri,\n          childrenByParentUri,\n          below,\n          depth: depth + 1,\n        },\n        state,\n      )\n\n      const item = this.threadOtherV2ItemPost({\n        depth,\n        hiddenByThreadgate,\n        mutedByViewer,\n        postView,\n        uri,\n      })\n\n      const tree: ThreadOtherPostNode = {\n        type: 'hiddenPost',\n        item: item,\n        tags: post.tags,\n        replies,\n      }\n\n      return tree\n    })\n  }\n\n  private threadOtherV2ItemPostAnchor({\n    depth,\n    uri,\n  }: {\n    depth: number\n    uri: string\n  }): ThreadOtherAnchorPostNode['item'] {\n    return {\n      uri,\n      depth,\n      // In hidden replies, the anchor value is undefined, so it doesn't include the anchor in the result.\n      // This is helpful so we can use the same internal structure for hidden and non-hidden, while omitting anchor for hidden.\n      value: undefined,\n    }\n  }\n\n  private threadOtherV2ItemPost({\n    depth,\n    hiddenByThreadgate,\n    mutedByViewer,\n    postView,\n    uri,\n  }: {\n    depth: number\n    hiddenByThreadgate: boolean\n    mutedByViewer: boolean\n    postView: PostView\n    uri: string\n  }): ThreadOtherItemValuePost {\n    const base = this.threadOtherV2ItemPostAnchor({ depth, uri })\n    return {\n      ...base,\n      value: {\n        $type: 'app.bsky.unspecced.defs#threadItemPost',\n        post: postView,\n        hiddenByThreadgate,\n        mutedByViewer,\n        moreParents: false, // \"Other\" replies don't have parents.\n        moreReplies: 0, // \"Other\" replies don't have replies hydrated.\n        opThread: false, // \"Other\" replies don't contain OP threads.\n      },\n    }\n  }\n\n  private checkThreadV2ReplyInclusion({\n    uri,\n    rootUri,\n    state,\n  }: {\n    uri: string\n    rootUri: string\n    state: HydrationState\n  }): {\n    authorDid: string\n    post: Post\n    postView: PostView\n  } | null {\n    // Not found.\n    const post = state.posts?.get(uri)\n    if (post?.violatesThreadGate) {\n      return null\n    }\n    const postView = this.post(uri, state)\n    if (!post || !postView) {\n      return null\n    }\n    const authorDid = postView.author.did\n    if (rootUri !== getRootUri(uri, post)) {\n      // outside thread boundary\n      return null\n    }\n\n    // Blocked (1p and 3p for replies).\n    const has1pBlock = this.viewerBlockExists(authorDid, state)\n    const has3pBlock =\n      !state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent\n    if (has1pBlock || has3pBlock) {\n      return null\n    }\n    if (!this.viewerSeesNeedsReview({ uri, did: authorDid }, state)) {\n      return null\n    }\n\n    // No unauthenticated.\n    if (this.noUnauthenticatedPost(state, postView)) {\n      return null\n    }\n\n    return { authorDid, post, postView }\n  }\n\n  private isOtherThreadPost(\n    {\n      post,\n      postView,\n      rootUri,\n      uri,\n    }: {\n      post: Post\n      postView: PostView\n      rootUri: string\n      uri: string\n    },\n    state: HydrationState,\n  ): {\n    isOther: boolean\n    hiddenByTag: boolean\n    hiddenByThreadgate: boolean\n    mutedByViewer: boolean\n  } {\n    const opDid = creatorFromUri(rootUri)\n    const authorDid = creatorFromUri(uri)\n\n    let hiddenByTag = false\n    if (\n      state.ctx?.features.checkGate(\n        state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,\n      )\n    ) {\n      hiddenByTag = authorDid !== opDid && post.tags.has(this.visibilityTagHide)\n    } else {\n      const showBecauseFollowing = !!postView.author.viewer?.following\n      hiddenByTag =\n        authorDid !== opDid &&\n        authorDid !== state.ctx?.viewer &&\n        !showBecauseFollowing &&\n        this.threadTagsHide.some((t) => post.tags.has(t))\n    }\n\n    const hiddenByThreadgate =\n      state.ctx?.viewer !== authorDid &&\n      this.replyIsHiddenByThreadgate(uri, rootUri, state)\n\n    const mutedByViewer = this.viewerMuteExists(authorDid, state)\n    const isPushPin =\n      isPostRecord(post.record) && post.record.text.trim() === '📌'\n\n    return {\n      isOther: hiddenByTag || hiddenByThreadgate || mutedByViewer || isPushPin,\n      hiddenByTag,\n      hiddenByThreadgate,\n      mutedByViewer,\n    }\n  }\n\n  private groupThreadChildrenByParent(\n    anchorUri: string,\n    uris: string[],\n    state: HydrationState,\n  ): Record<string, string[]> {\n    // Groups children of each parent.\n    const includedPosts = new Set<string>([anchorUri])\n    const childrenByParentUri: Record<string, string[]> = {}\n    uris.forEach((uri) => {\n      const post = state.posts?.get(uri)\n      const parentUri = post?.record.reply?.parent.uri\n      if (!parentUri) return\n      if (includedPosts.has(uri)) return\n      includedPosts.add(uri)\n      childrenByParentUri[parentUri] ??= []\n      childrenByParentUri[parentUri].push(uri)\n    })\n    return childrenByParentUri\n  }\n\n  // Embeds\n  // ------------\n\n  embed(\n    postUri: string,\n    embed: Embed | { $type: string },\n    state: HydrationState,\n    depth: number,\n  ): $Typed<EmbedView> | undefined {\n    if (isImagesEmbed(embed)) {\n      return this.imagesEmbed(creatorFromUri(postUri), embed)\n    } else if (isVideoEmbed(embed)) {\n      return this.videoEmbed(creatorFromUri(postUri), embed)\n    } else if (isExternalEmbed(embed)) {\n      return this.externalEmbed(creatorFromUri(postUri), embed)\n    } else if (isRecordEmbed(embed)) {\n      return this.recordEmbed(postUri, embed, state, depth)\n    } else if (isRecordWithMedia(embed)) {\n      return this.recordWithMediaEmbed(postUri, embed, state, depth)\n    } else {\n      return undefined\n    }\n  }\n\n  imagesEmbed(did: string, embed: ImagesEmbed): $Typed<ImagesEmbedView> {\n    const imgViews = embed.images.map((img) => ({\n      thumb: this.imgUriBuilder.getPresetUri(\n        'feed_thumbnail',\n        did,\n        cidFromBlobJson(img.image),\n      ),\n      fullsize: this.imgUriBuilder.getPresetUri(\n        'feed_fullsize',\n        did,\n        cidFromBlobJson(img.image),\n      ),\n      alt: img.alt,\n      aspectRatio: img.aspectRatio,\n    }))\n    return {\n      $type: 'app.bsky.embed.images#view',\n      images: imgViews,\n    }\n  }\n\n  videoEmbed(did: string, embed: VideoEmbed): $Typed<VideoEmbedView> {\n    const cid = cidFromBlobJson(embed.video)\n    return {\n      $type: 'app.bsky.embed.video#view',\n      cid,\n      playlist: this.videoUriBuilder.playlist({ did, cid }),\n      thumbnail: this.videoUriBuilder.thumbnail({ did, cid }),\n      alt: embed.alt,\n      aspectRatio: embed.aspectRatio,\n      presentation: embed.presentation,\n    }\n  }\n\n  externalEmbed(did: string, embed: ExternalEmbed): $Typed<ExternalEmbedView> {\n    const { uri, title, description, thumb } = embed.external\n    return {\n      $type: 'app.bsky.embed.external#view',\n      external: {\n        uri,\n        title,\n        description,\n        thumb: thumb\n          ? this.imgUriBuilder.getPresetUri(\n              'feed_thumbnail',\n              did,\n              cidFromBlobJson(thumb),\n            )\n          : undefined,\n      },\n    }\n  }\n\n  embedNotFound(uri: string): {\n    $type: 'app.bsky.embed.record#view'\n    record: $Typed<EmbedNotFound>\n  } {\n    return {\n      $type: 'app.bsky.embed.record#view',\n      record: {\n        $type: 'app.bsky.embed.record#viewNotFound',\n        uri,\n        notFound: true,\n      },\n    }\n  }\n\n  embedDetached(uri: string): {\n    $type: 'app.bsky.embed.record#view'\n    record: $Typed<EmbedDetached>\n  } {\n    return {\n      $type: 'app.bsky.embed.record#view',\n      record: {\n        $type: 'app.bsky.embed.record#viewDetached',\n        uri,\n        detached: true,\n      },\n    }\n  }\n\n  embedBlocked(\n    uri: string,\n    state: HydrationState,\n  ): {\n    $type: 'app.bsky.embed.record#view'\n    record: $Typed<EmbedBlocked>\n  } {\n    const creator = creatorFromUri(uri)\n    return {\n      $type: 'app.bsky.embed.record#view',\n      record: {\n        $type: 'app.bsky.embed.record#viewBlocked',\n        uri,\n        blocked: true,\n        author: {\n          did: creator,\n          viewer: this.blockedProfileViewer(creator, state),\n        },\n      },\n    }\n  }\n\n  embedPostView(\n    uri: string,\n    state: HydrationState,\n    depth: number,\n  ): $Typed<PostEmbedView> | undefined {\n    const postView = this.post(uri, state, depth)\n    if (!postView) return\n    return {\n      $type: 'app.bsky.embed.record#viewRecord',\n      uri: postView.uri,\n      cid: postView.cid,\n      author: postView.author,\n      value: postView.record,\n      labels: postView.labels,\n      likeCount: postView.likeCount,\n      replyCount: postView.replyCount,\n      repostCount: postView.repostCount,\n      quoteCount: postView.quoteCount,\n      indexedAt: postView.indexedAt,\n      embeds: depth > 1 ? undefined : postView.embed ? [postView.embed] : [],\n    }\n  }\n\n  recordEmbed(\n    postUri: string,\n    embed: RecordEmbed,\n    state: HydrationState,\n    depth: number,\n    withTypeTag: false,\n  ): RecordEmbedView\n  recordEmbed(\n    postUri: string,\n    embed: RecordEmbed,\n    state: HydrationState,\n    depth: number,\n    withTypeTag?: true,\n  ): $Typed<RecordEmbedView>\n  recordEmbed(\n    postUri: string,\n    embed: RecordEmbed,\n    state: HydrationState,\n    depth: number,\n    withTypeTag = true,\n  ): RecordEmbedView {\n    const uri = embed.record.uri\n    const parsedUri = new AtUri(uri)\n    if (\n      this.viewerBlockExists(parsedUri.hostname, state) ||\n      (!state.ctx?.include3pBlocks && state.postBlocks?.get(postUri)?.embed)\n    ) {\n      return this.embedBlocked(uri, state)\n    }\n\n    const post = state.posts?.get(postUri)\n    if (post?.violatesEmbeddingRules) {\n      return this.embedDetached(uri)\n    }\n\n    if (parsedUri.collection === ids.AppBskyFeedPost) {\n      const view = this.embedPostView(uri, state, depth)\n      if (!view) return this.embedNotFound(uri)\n      const postgateRecordUri = postUriToPostgateUri(parsedUri.toString())\n      const postgate = state.postgates?.get(postgateRecordUri)\n      if (postgate?.record?.detachedEmbeddingUris?.includes(postUri)) {\n        return this.embedDetached(uri)\n      }\n      return this.recordEmbedWrapper(view, withTypeTag)\n    } else if (parsedUri.collection === ids.AppBskyFeedGenerator) {\n      const view = this.feedGenerator(uri, state)\n      if (!view) return this.embedNotFound(uri)\n      return this.recordEmbedWrapper(\n        { ...view, $type: 'app.bsky.feed.defs#generatorView' },\n        withTypeTag,\n      )\n    } else if (parsedUri.collection === ids.AppBskyGraphList) {\n      const view = this.list(uri, state)\n      if (!view) return this.embedNotFound(uri)\n      return this.recordEmbedWrapper(\n        { ...view, $type: 'app.bsky.graph.defs#listView' },\n        withTypeTag,\n      )\n    } else if (parsedUri.collection === ids.AppBskyLabelerService) {\n      const view = this.labeler(parsedUri.hostname, state)\n      if (!view) return this.embedNotFound(uri)\n      return this.recordEmbedWrapper(\n        { ...view, $type: 'app.bsky.labeler.defs#labelerView' },\n        withTypeTag,\n      )\n    } else if (parsedUri.collection === ids.AppBskyGraphStarterpack) {\n      const view = this.starterPackBasic(uri, state)\n      if (!view) return this.embedNotFound(uri)\n      return this.recordEmbedWrapper(\n        { ...view, $type: 'app.bsky.graph.defs#starterPackViewBasic' },\n        withTypeTag,\n      )\n    }\n    return this.embedNotFound(uri)\n  }\n\n  private recordEmbedWrapper<T extends $Typed<RecordEmbedViewInternal>>(\n    record: T,\n    withTypeTag: boolean,\n  ) {\n    return {\n      $type: withTypeTag ? ('app.bsky.embed.record#view' as const) : undefined,\n      record,\n    } satisfies RecordEmbedView\n  }\n\n  recordWithMediaEmbed(\n    postUri: string,\n    embed: RecordWithMedia,\n    state: HydrationState,\n    depth: number,\n  ): $Typed<RecordWithMediaView> | undefined {\n    const creator = creatorFromUri(postUri)\n    let mediaEmbed:\n      | $Typed<ImagesEmbedView>\n      | $Typed<VideoEmbedView>\n      | $Typed<ExternalEmbedView>\n    if (isImagesEmbed(embed.media)) {\n      mediaEmbed = this.imagesEmbed(creator, embed.media)\n    } else if (isVideoEmbed(embed.media)) {\n      mediaEmbed = this.videoEmbed(creator, embed.media)\n    } else if (isExternalEmbed(embed.media)) {\n      mediaEmbed = this.externalEmbed(creator, embed.media)\n    } else {\n      return\n    }\n    return {\n      $type: 'app.bsky.embed.recordWithMedia#view',\n      media: mediaEmbed,\n      record: this.recordEmbed(postUri, embed.record, state, depth, false),\n    }\n  }\n\n  userReplyDisabled(uri: string, state: HydrationState): boolean | undefined {\n    const post = state.posts?.get(uri)\n    if (post?.violatesThreadGate) {\n      return true\n    }\n    const rootUriStr: string = post?.record.reply?.root.uri ?? uri\n    const gate = state.threadgates?.get(\n      postUriToThreadgateUri(rootUriStr),\n    )?.record\n    const viewer = state.ctx?.viewer\n    if (!gate || !viewer) {\n      return undefined\n    }\n    const rootPost = state.posts?.get(rootUriStr)?.record\n    const ownerDid = creatorFromUri(rootUriStr)\n    const {\n      canReply,\n      allowFollower,\n      allowFollowing,\n      allowListUris = [],\n    } = parseThreadGate(viewer, ownerDid, rootPost ?? null, gate)\n    if (canReply) {\n      return false\n    }\n    if (allowFollower && state.profileViewers?.get(ownerDid)?.following) {\n      return false\n    }\n    if (allowFollowing && state.profileViewers?.get(ownerDid)?.followedBy) {\n      return false\n    }\n    for (const listUri of allowListUris) {\n      const list = state.listViewers?.get(listUri)\n      if (list?.viewerInList) {\n        return false\n      }\n    }\n    return true\n  }\n\n  userPostEmbeddingDisabled(\n    uri: string,\n    state: HydrationState,\n  ): boolean | undefined {\n    const post = state.posts?.get(uri)\n    if (!post) {\n      return true\n    }\n    const postgateRecordUri = postUriToPostgateUri(uri)\n    const gate = state.postgates?.get(postgateRecordUri)?.record\n    const viewerDid = state.ctx?.viewer ?? undefined\n    const {\n      embeddingRules: { canEmbed },\n    } = parsePostgate({\n      gate,\n      viewerDid,\n      authorDid: creatorFromUri(uri),\n    })\n    if (canEmbed) {\n      return false\n    }\n    return true\n  }\n\n  viewerPinned(uri: string, state: HydrationState, authorDid: string) {\n    if (!state.ctx?.viewer || state.ctx.viewer !== authorDid) return\n    const actor = state.actors?.get(authorDid)\n    if (!actor) return\n    const pinnedPost = safePinnedPost(actor.profile?.pinnedPost)\n    if (!pinnedPost) return undefined\n    return pinnedPost.uri === uri\n  }\n\n  notification(\n    notif: Notification,\n    lastSeenAt: string | undefined,\n    state: HydrationState,\n  ): Un$Typed<NotificationView> | undefined {\n    if (!notif.timestamp || !notif.reason) return\n    const uri = new AtUri(notif.uri)\n    const authorDid = uri.hostname\n    const author = this.profile(authorDid, state)\n    if (!author) return\n\n    let recordInfo:\n      | Post\n      | Like\n      | Repost\n      | Follow\n      | RecordInfo<ProfileRecord>\n      | Verification\n      | Pick<RecordInfo<Required<NotificationRecordDeleted>>, 'cid' | 'record'>\n      | undefined\n      | null\n\n    if (uri.collection === ids.AppBskyFeedPost) {\n      recordInfo = state.posts?.get(notif.uri)\n    } else if (uri.collection === ids.AppBskyFeedLike) {\n      recordInfo = state.likes?.get(notif.uri)\n    } else if (uri.collection === ids.AppBskyFeedRepost) {\n      recordInfo = state.reposts?.get(notif.uri)\n    } else if (uri.collection === ids.AppBskyGraphFollow) {\n      recordInfo = state.follows?.get(notif.uri)\n    } else if (uri.collection === ids.AppBskyGraphVerification) {\n      // When a verification record is removed, the record won't be found,\n      // both for the `verified` and `unverified` notifications.\n      recordInfo = state.verifications?.get(notif.uri) ?? {\n        record: notificationDeletedRecord,\n        cid: notificationDeletedRecordCid,\n      }\n    } else if (uri.collection === ids.AppBskyActorProfile) {\n      const actor = state.actors?.get(authorDid)\n      recordInfo =\n        actor && actor.profile && actor.profileCid\n          ? {\n              record: actor.profile,\n              cid: actor.profileCid,\n              sortedAt: actor.sortedAt ?? new Date(0), // @NOTE will be present since profile record is present\n              indexedAt: actor.indexedAt ?? new Date(0), // @NOTE will be present since profile record is present\n              takedownRef: actor.profileTakedownRef,\n            }\n          : undefined\n    }\n    if (!recordInfo) return\n\n    const labels = state.labels?.getBySubject(notif.uri) ?? []\n    const selfLabels = this.selfLabels({\n      uri: notif.uri,\n      cid: recordInfo.cid,\n      record: recordInfo.record,\n    })\n    const indexedAt = notif.timestamp.toDate().toISOString()\n    return {\n      uri: notif.uri,\n      cid: recordInfo.cid,\n      author,\n      reason: notif.reason,\n      reasonSubject: notif.reasonSubject || undefined,\n      record: recordInfo.record,\n      // @NOTE works with a hack in listNotifications so that when there's no last-seen time,\n      // the user's first notification is marked unread, and all previous read. in this case,\n      // the last seen time will be equal to the first notification's indexed time.\n      isRead: lastSeenAt ? lastSeenAt > indexedAt : true,\n      indexedAt: notif.timestamp.toDate().toISOString(),\n      labels: [...labels, ...selfLabels],\n    }\n  }\n\n  indexedAt({ sortedAt, indexedAt }: { sortedAt: Date; indexedAt: Date }) {\n    if (!this.indexedAtEpoch) return sortedAt\n    return indexedAt && indexedAt > this.indexedAtEpoch ? indexedAt : sortedAt\n  }\n}\n\nconst getRootUri = (uri: string, post: Post): string => {\n  return post.record.reply?.root.uri ?? uri\n}\n"
  },
  {
    "path": "packages/bsky/src/views/threads-v2.ts",
    "content": "import { HydrateCtx } from '../hydration/hydrator'\nimport {\n  ThreadItemBlocked,\n  ThreadItemNoUnauthenticated,\n  ThreadItemNotFound,\n  ThreadItemPost,\n} from '../lexicon/types/app/bsky/unspecced/defs'\nimport { ThreadItem as ThreadOtherItem } from '../lexicon/types/app/bsky/unspecced/getPostThreadOtherV2'\nimport {\n  QueryParams as GetPostThreadV2QueryParams,\n  ThreadItem,\n} from '../lexicon/types/app/bsky/unspecced/getPostThreadV2'\nimport { $Typed } from '../lexicon/util'\n\ntype ThreadMaybeOtherPostNode = ThreadPostNode | ThreadOtherPostNode\ntype ThreadNodeWithReplies =\n  | ThreadPostNode\n  | ThreadOtherPostNode\n  | ThreadOtherAnchorPostNode\n\ntype ThreadItemValue<T extends ThreadItem['value']> = Omit<\n  ThreadItem,\n  'value'\n> & {\n  value: T\n}\n\nexport type ThreadItemValueBlocked = ThreadItemValue<$Typed<ThreadItemBlocked>>\n\nexport type ThreadItemValueNoUnauthenticated = ThreadItemValue<\n  $Typed<ThreadItemNoUnauthenticated>\n>\n\nexport type ThreadItemValueNotFound = ThreadItemValue<\n  $Typed<ThreadItemNotFound>\n>\n\nexport type ThreadItemValuePost = ThreadItemValue<$Typed<ThreadItemPost>>\n\ntype ThreadBlockedNode = {\n  type: 'blocked'\n  item: ThreadItemValueBlocked\n}\ntype ThreadNoUnauthenticatedNode = {\n  type: 'noUnauthenticated'\n  parent: ThreadTree | undefined\n  item: ThreadItemValueNoUnauthenticated\n}\n\ntype ThreadNotFoundNode = {\n  type: 'notFound'\n  item: ThreadItemValueNotFound\n}\n\ntype ThreadPostNode = {\n  type: 'post'\n  item: ThreadItemValuePost\n  tags: Set<string>\n  hasOPLike: boolean\n  parent: ThreadTree | undefined\n  replies: ThreadTree[] | undefined\n}\n\ntype ThreadOtherItemValue<T extends ThreadOtherItem['value']> = Omit<\n  ThreadOtherItem,\n  'value'\n> & {\n  value: T\n}\n\nexport type ThreadOtherItemValuePost = ThreadOtherItemValue<\n  $Typed<ThreadItemPost>\n>\n\n// This is an intermediary type that doesn't map to the views.\n// It is useful to differentiate between the anchor post and the replies for the hidden case,\n// while also differentiating between hidden and visible cases.\nexport type ThreadOtherAnchorPostNode = {\n  type: 'hiddenAnchor'\n  item: Omit<ThreadOtherItem, 'value'> & { value: undefined }\n  replies: ThreadOtherPostNode[] | undefined\n}\n\nexport type ThreadOtherPostNode = {\n  type: 'hiddenPost'\n  item: ThreadOtherItemValuePost\n  tags: Set<string>\n  replies: ThreadOtherPostNode[] | undefined\n}\n\nconst isNodeWithReplies = (node: ThreadTree): node is ThreadNodeWithReplies =>\n  'replies' in node && node.replies !== undefined\n\nconst isPostNode = (node: ThreadTree): node is ThreadMaybeOtherPostNode =>\n  node.type === 'post' || node.type === 'hiddenPost'\n\nexport type ThreadTreeVisible =\n  | ThreadBlockedNode\n  | ThreadNoUnauthenticatedNode\n  | ThreadNotFoundNode\n  | ThreadPostNode\n\nexport type ThreadTreeOther = ThreadOtherAnchorPostNode | ThreadOtherPostNode\n\nexport type ThreadTree = ThreadTreeVisible | ThreadTreeOther\n\n/** This function mutates the tree parameter. */\nexport function sortTrimFlattenThreadTree(\n  anchorTree: ThreadTree,\n  options: SortTrimFlattenOptions,\n  useExploration?: boolean,\n) {\n  const sortedAnchorTree = useExploration\n    ? sortTrimThreadTreeExploration(anchorTree, options)\n    : sortTrimThreadTree(anchorTree, options)\n\n  return flattenTree(sortedAnchorTree)\n}\n\ntype SortTrimFlattenOptions = {\n  branchingFactor: GetPostThreadV2QueryParams['branchingFactor']\n  opDid: string\n  sort?: GetPostThreadV2QueryParams['sort']\n  viewer: HydrateCtx['viewer']\n  threadTagsBumpDown: readonly string[]\n  threadTagsHide: readonly string[]\n  visibilityTagRankPrefix: string\n}\n\n/** This function mutates the tree parameter. */\nfunction sortTrimThreadTree(\n  n: ThreadTree,\n  opts: SortTrimFlattenOptions,\n): ThreadTree {\n  if (!isNodeWithReplies(n)) {\n    return n\n  }\n  const node: ThreadNodeWithReplies = n\n\n  if (node.replies) {\n    node.replies.sort((an: ThreadTree, bn: ThreadTree) => {\n      if (!isPostNode(an)) {\n        return 1\n      }\n      if (!isPostNode(bn)) {\n        return -1\n      }\n      const aNode: ThreadMaybeOtherPostNode = an\n      const bNode: ThreadMaybeOtherPostNode = bn\n\n      // First applies bumping.\n      const bump = applyBumping(aNode, bNode, opts)\n      if (bump !== null) {\n        return bump\n      }\n\n      // Then applies sorting.\n      return applySorting(aNode, bNode, opts)\n    })\n\n    // Trimming: after sorting, apply branching factor to all levels of replies except the anchor direct replies.\n    if (node.item.depth !== 0) {\n      node.replies = node.replies.slice(0, opts.branchingFactor)\n    }\n\n    node.replies.forEach((reply) => sortTrimThreadTree(reply, opts))\n  }\n\n  return node\n}\n\nfunction applyBumping(\n  aNode: ThreadMaybeOtherPostNode,\n  bNode: ThreadMaybeOtherPostNode,\n  opts: SortTrimFlattenOptions,\n): number | null {\n  if (!isPostNode(aNode)) {\n    return null\n  }\n  if (!isPostNode(bNode)) {\n    return null\n  }\n\n  const { opDid, viewer, threadTagsBumpDown, threadTagsHide } = opts\n\n  type BumpDirection = 'up' | 'down'\n  type BumpPredicateFn = (i: ThreadMaybeOtherPostNode) => boolean\n\n  const maybeBump = (\n    bump: BumpDirection,\n    predicateFn: BumpPredicateFn,\n  ): number | null => {\n    const aPredicate = predicateFn(aNode)\n    const bPredicate = predicateFn(bNode)\n    if (aPredicate && bPredicate) {\n      return applySorting(aNode, bNode, opts)\n    } else if (aPredicate) {\n      return bump === 'up' ? -1 : 1\n    } else if (bPredicate) {\n      return bump === 'up' ? 1 : -1\n    }\n    return null\n  }\n\n  // The order of the bumps determines the priority with which they are applied.\n  // Bumps-up applied first make the item appear higher in the list than later bumps-up.\n  // Bumps-down applied first make the item appear lower in the list than later bumps-down.\n  const bumps: [BumpDirection, BumpPredicateFn][] = [\n    /*\n      General bumps.\n    */\n    // OP replies.\n    ['up', (i) => i.item.value.post.author.did === opDid],\n    // Viewer replies.\n    ['up', (i) => i.item.value.post.author.did === viewer],\n\n    /*\n      Bumps within visible replies.\n    */\n    // Followers posts.\n    [\n      'up',\n      (i) => i.type === 'post' && !!i.item.value.post.author.viewer?.following,\n    ],\n    // Bump-down tags.\n    [\n      'down',\n      (i) => i.type === 'post' && threadTagsBumpDown.some((t) => i.tags.has(t)),\n    ],\n\n    /*\n      Bumps within hidden replies.\n      This determines the order of hidden replies:\n        1. hidden by threadgate.\n        2. hidden by tags.\n        3. muted by viewer.\n    */\n    // Muted account by the viewer.\n    ['down', (i) => i.type === 'hiddenPost' && i.item.value.mutedByViewer],\n    // Hidden by tags.\n    [\n      'down',\n      (i) =>\n        i.type === 'hiddenPost' && threadTagsHide.some((t) => i.tags.has(t)),\n    ],\n    // Hidden by threadgate.\n    ['down', (i) => i.type === 'hiddenPost' && i.item.value.hiddenByThreadgate],\n  ]\n\n  for (const [bump, predicateFn] of bumps) {\n    const bumpResult = maybeBump(bump, predicateFn)\n    if (bumpResult !== null) {\n      return bumpResult\n    }\n  }\n\n  return null\n}\n\nfunction applySorting(\n  aNode: ThreadMaybeOtherPostNode,\n  bNode: ThreadMaybeOtherPostNode,\n  opts: SortTrimFlattenOptions,\n): number {\n  const a = aNode.item.value\n  const b = bNode.item.value\n\n  // Only customize sort for visible posts.\n  if (aNode.type === 'post' && bNode.type === 'post') {\n    const { sort } = opts\n\n    if (sort === 'oldest') {\n      return a.post.indexedAt.localeCompare(b.post.indexedAt)\n    }\n    if (sort === 'top') {\n      const aLikes = a.post.likeCount ?? 0\n      const bLikes = b.post.likeCount ?? 0\n      const aTop = topSortValue(aLikes, aNode.hasOPLike)\n      const bTop = topSortValue(bLikes, bNode.hasOPLike)\n      if (aTop !== bTop) {\n        return bTop - aTop\n      }\n    }\n  }\n\n  // Fallback to newest.\n  return b.post.indexedAt.localeCompare(a.post.indexedAt)\n}\n\nfunction topSortValue(likeCount: number, hasOPLike: boolean): number {\n  return Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)\n}\n\nfunction flattenTree(tree: ThreadTree) {\n  return [\n    // All parents above.\n    ...Array.from(\n      flattenInDirection({\n        tree,\n        direction: 'up',\n      }),\n    ),\n\n    // The anchor.\n    // In the case of hidden replies, the anchor item itself is undefined.\n    ...(tree.item.value ? [tree.item] : []),\n\n    // All replies below.\n    ...Array.from(\n      flattenInDirection({\n        tree,\n        direction: 'down',\n      }),\n    ),\n  ]\n}\n\nfunction* flattenInDirection({\n  tree,\n  direction,\n}: {\n  tree: ThreadTree\n  direction: 'up' | 'down'\n}) {\n  if (tree.type === 'noUnauthenticated') {\n    if (direction === 'up') {\n      if (tree.parent) {\n        // Unfold all parents above.\n        yield* flattenTree(tree.parent)\n      }\n    }\n  }\n\n  if (tree.type === 'post') {\n    if (direction === 'up') {\n      if (tree.parent) {\n        // Unfold all parents above.\n        yield* flattenTree(tree.parent)\n      }\n    } else {\n      // Unfold all replies below.\n      if (tree.replies?.length) {\n        for (const reply of tree.replies) {\n          yield* flattenTree(reply)\n        }\n      }\n    }\n  }\n\n  // For the first level of hidden replies, the items are undefined.\n  if (tree.type === 'hiddenAnchor' || tree.type === 'hiddenPost') {\n    if (direction === 'down') {\n      // Unfold all replies below.\n      if (tree.replies?.length) {\n        for (const reply of tree.replies) {\n          yield* flattenTree(reply)\n        }\n      }\n    }\n  }\n}\n\nexport function sortTrimThreadTreeExploration(\n  n: ThreadTree,\n  opts: SortTrimFlattenOptions,\n): ThreadTree {\n  if (!isNodeWithReplies(n)) {\n    return n\n  }\n  const node: ThreadNodeWithReplies = n\n\n  if (node.replies) {\n    node.replies.sort((an: ThreadTree, bn: ThreadTree) => {\n      if (!isPostNode(an)) {\n        return 1\n      }\n      if (!isPostNode(bn)) {\n        return -1\n      }\n      const aNode: ThreadMaybeOtherPostNode = an\n      const bNode: ThreadMaybeOtherPostNode = bn\n\n      // First applies bumping.\n      const bump = applyBumpingExploration(aNode, bNode, opts)\n      if (bump !== null) {\n        return bump\n      }\n\n      // Then applies sorting.\n      return applySortingExploration(aNode, bNode, opts)\n    })\n\n    // Trimming: after sorting, apply branching factor to all levels of replies except the anchor direct replies.\n    if (node.item.depth !== 0) {\n      node.replies = node.replies.slice(0, opts.branchingFactor)\n    }\n\n    node.replies.forEach((reply) => sortTrimThreadTreeExploration(reply, opts))\n  }\n\n  return node\n}\n\nfunction applyBumpingExploration(\n  aNode: ThreadMaybeOtherPostNode,\n  bNode: ThreadMaybeOtherPostNode,\n  opts: SortTrimFlattenOptions,\n): number | null {\n  if (!isPostNode(aNode)) {\n    return null\n  }\n  if (!isPostNode(bNode)) {\n    return null\n  }\n\n  const { opDid, viewer } = opts\n\n  type BumpDirection = 'up' | 'down'\n  type BumpPredicateFn = (i: ThreadMaybeOtherPostNode) => boolean\n\n  const maybeBump = (\n    bump: BumpDirection,\n    predicateFn: BumpPredicateFn,\n  ): number | null => {\n    const aPredicate = predicateFn(aNode)\n    const bPredicate = predicateFn(bNode)\n    if (aPredicate && bPredicate) {\n      return applySortingExploration(aNode, bNode, opts)\n    } else if (aPredicate) {\n      return bump === 'up' ? -1 : 1\n    } else if (bPredicate) {\n      return bump === 'up' ? 1 : -1\n    }\n    return null\n  }\n\n  // The order of the bumps determines the priority with which they are applied.\n  // Bumps-up applied first make the item appear higher in the list than later bumps-up.\n  // Bumps-down applied first make the item appear lower in the list than later bumps-down.\n  const bumps: [BumpDirection, BumpPredicateFn][] = [\n    /*\n      General bumps.\n    */\n    // OP replies.\n    ['up', (i) => i.item.value.post.author.did === opDid],\n    // Viewer replies.\n    ['up', (i) => i.item.value.post.author.did === viewer],\n  ]\n\n  for (const [bump, predicateFn] of bumps) {\n    const bumpResult = maybeBump(bump, predicateFn)\n    if (bumpResult !== null) {\n      return bumpResult\n    }\n  }\n\n  return null\n}\n\nfunction applySortingExploration(\n  aNode: ThreadMaybeOtherPostNode,\n  bNode: ThreadMaybeOtherPostNode,\n  opts: SortTrimFlattenOptions,\n): number {\n  const { visibilityTagRankPrefix: rp } = opts\n\n  const a = aNode.item.value\n  const ar = !rp ? 0 : parseRankFromTag(rp, findRankTag(aNode.tags, rp))\n  const b = bNode.item.value\n  const br = !rp ? 0 : parseRankFromTag(rp, findRankTag(bNode.tags, rp))\n\n  // Only customize sort for visible posts.\n  if (aNode.type === 'post' && bNode.type === 'post') {\n    const { sort } = opts\n\n    if (sort === 'oldest') {\n      return a.post.indexedAt.localeCompare(b.post.indexedAt)\n    }\n    if (sort === 'top') {\n      const aLikes = a.post.likeCount ?? 0\n      const bLikes = b.post.likeCount ?? 0\n      const aTop = topSortValue(aLikes, aNode.hasOPLike)\n      const bTop = topSortValue(bLikes, bNode.hasOPLike)\n      const aRank = aTop + ar\n      const bRank = bTop + br\n      if (aRank !== bRank) {\n        return bRank - aRank\n      }\n    }\n  }\n\n  // Fallback to newest.\n  return b.post.indexedAt.localeCompare(a.post.indexedAt)\n}\n\nfunction findRankTag(tags: Set<string>, prefix: string) {\n  return Array.from(tags.values()).find((tag) => tag.startsWith(prefix))\n}\n\nfunction parseRankFromTag(prefix: string, tag?: string) {\n  if (!tag) return 0\n\n  try {\n    const rank = parseInt(tag.slice(prefix.length), 10)\n    if (typeof rank !== 'number' || isNaN(rank)) {\n      return 0\n    }\n    return rank\n  } catch (e) {\n    return 0\n  }\n}\n"
  },
  {
    "path": "packages/bsky/src/views/types.ts",
    "content": "import {\n  Main as ExternalEmbed,\n  View as ExternalEmbedView,\n} from '../lexicon/types/app/bsky/embed/external'\nimport {\n  Main as ImagesEmbed,\n  View as ImagesEmbedView,\n} from '../lexicon/types/app/bsky/embed/images'\nimport {\n  Main as RecordEmbed,\n  View as RecordEmbedView,\n  ViewRecord as PostEmbedView,\n} from '../lexicon/types/app/bsky/embed/record'\nimport {\n  Main as RecordWithMedia,\n  View as RecordWithMediaView,\n} from '../lexicon/types/app/bsky/embed/recordWithMedia'\nimport {\n  Main as VideoEmbed,\n  View as VideoEmbedView,\n} from '../lexicon/types/app/bsky/embed/video'\nimport {\n  BlockedPost,\n  GeneratorView,\n  NotFoundPost,\n  PostView,\n} from '../lexicon/types/app/bsky/feed/defs'\nimport {\n  ListView,\n  StarterPackViewBasic,\n} from '../lexicon/types/app/bsky/graph/defs'\nimport { LabelerView } from '../lexicon/types/app/bsky/labeler/defs'\n\nexport type {\n  Main as ImagesEmbed,\n  View as ImagesEmbedView,\n} from '../lexicon/types/app/bsky/embed/images'\nexport { isMain as isImagesEmbed } from '../lexicon/types/app/bsky/embed/images'\nexport type {\n  Main as VideoEmbed,\n  View as VideoEmbedView,\n} from '../lexicon/types/app/bsky/embed/video'\nexport { isMain as isVideoEmbed } from '../lexicon/types/app/bsky/embed/video'\nexport type {\n  Main as ExternalEmbed,\n  View as ExternalEmbedView,\n} from '../lexicon/types/app/bsky/embed/external'\nexport { isMain as isExternalEmbed } from '../lexicon/types/app/bsky/embed/external'\nexport type {\n  Main as RecordEmbed,\n  View as RecordEmbedView,\n  ViewBlocked as EmbedBlocked,\n  ViewDetached as EmbedDetached,\n  ViewNotFound as EmbedNotFound,\n  ViewRecord as PostEmbedView,\n} from '../lexicon/types/app/bsky/embed/record'\nexport { isMain as isRecordEmbed } from '../lexicon/types/app/bsky/embed/record'\nexport type {\n  Main as RecordWithMedia,\n  View as RecordWithMediaView,\n} from '../lexicon/types/app/bsky/embed/recordWithMedia'\nexport { isMain as isRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia'\nexport type { View as RecordWithMediaEmbedView } from '../lexicon/types/app/bsky/embed/recordWithMedia'\nexport type {\n  BlockedPost,\n  GeneratorView,\n  NotFoundPost,\n  PostView,\n} from '../lexicon/types/app/bsky/feed/defs'\nexport type { ListView } from '../lexicon/types/app/bsky/graph/defs'\n\nexport type { Notification as NotificationView } from '../lexicon/types/app/bsky/notification/listNotifications'\n\nexport type Embed =\n  | ImagesEmbed\n  | VideoEmbed\n  | ExternalEmbed\n  | RecordEmbed\n  | RecordWithMedia\n\nexport type EmbedView =\n  | ImagesEmbedView\n  | VideoEmbedView\n  | ExternalEmbedView\n  | RecordEmbedView\n  | RecordWithMediaView\n\nexport type MaybePostView = PostView | NotFoundPost | BlockedPost\n\nexport type RecordEmbedViewInternal =\n  | PostEmbedView\n  | GeneratorView\n  | ListView\n  | LabelerView\n  | StarterPackViewBasic\n"
  },
  {
    "path": "packages/bsky/src/views/util.ts",
    "content": "import * as util from 'node:util'\nimport { BlobRef } from '@atproto/lexicon'\nimport { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post'\nimport {\n  Record as PostgateRecord,\n  isDisableRule as isPostgateDisableRule,\n} from '../lexicon/types/app/bsky/feed/postgate'\nimport {\n  Record as GateRecord,\n  isFollowerRule,\n  isFollowingRule,\n  isListRule,\n  isMentionRule,\n} from '../lexicon/types/app/bsky/feed/threadgate'\nimport { isMention } from '../lexicon/types/app/bsky/richtext/facet'\n\nexport const parseThreadGate = (\n  replierDid: string,\n  ownerDid: string,\n  rootPost: PostRecord | null,\n  gate: GateRecord | null,\n): ParsedThreadGate => {\n  if (replierDid === ownerDid) {\n    return { canReply: true }\n  }\n  // if gate.allow is unset then *any* reply is allowed, if it is an empty array then *no* reply is allowed\n  if (!gate || !gate.allow) {\n    return { canReply: true }\n  }\n\n  const allowMentions = gate.allow.some(isMentionRule)\n  const allowFollower = gate.allow.some(isFollowerRule)\n  const allowFollowing = gate.allow.some(isFollowingRule)\n  const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list)\n\n  // check mentions first since it's quick and synchronous\n  if (allowMentions) {\n    const isMentioned = rootPost?.facets?.some((facet) => {\n      return facet.features.some(\n        (item) => isMention(item) && item.did === replierDid,\n      )\n    })\n    if (isMentioned) {\n      return {\n        canReply: true,\n        allowMentions,\n        allowFollower,\n        allowFollowing,\n        allowListUris,\n      }\n    }\n  }\n  return { allowMentions, allowFollower, allowFollowing, allowListUris }\n}\n\ntype ParsedThreadGate = {\n  canReply?: boolean\n  allowMentions?: boolean\n  allowFollower?: boolean\n  allowFollowing?: boolean\n  allowListUris?: string[]\n}\n\nexport const cidFromBlobJson = (json: BlobRef) => {\n  if (json instanceof BlobRef) {\n    return json.ref.toString()\n  }\n  // @NOTE below handles the fact that parseRecordBytes() produces raw json rather than lexicon values\n  if (json['$type'] === 'blob') {\n    return (json['ref']?.['$link'] ?? '') as string\n  }\n  return (json['cid'] ?? '') as string\n}\n\nexport const parsePostgate = ({\n  gate,\n  viewerDid,\n  authorDid,\n}: {\n  gate: PostgateRecord | undefined\n  viewerDid: string | undefined\n  authorDid: string\n}): ParsedPostgate => {\n  if (viewerDid === authorDid) {\n    return { embeddingRules: { canEmbed: true } }\n  }\n  // default state is unset, allow everyone\n  if (!gate || !gate.embeddingRules) {\n    return { embeddingRules: { canEmbed: true } }\n  }\n\n  const disabled = gate.embeddingRules.some(isPostgateDisableRule)\n  if (disabled) {\n    return { embeddingRules: { canEmbed: false } }\n  }\n\n  return { embeddingRules: { canEmbed: true } }\n}\n\ntype ParsedPostgate = {\n  embeddingRules: {\n    canEmbed: boolean\n  }\n}\n\nexport class VideoUriBuilder {\n  constructor(\n    private opts: {\n      playlistUrlPattern: string // e.g. https://hostname/vid/%s/%s/playlist.m3u8\n      thumbnailUrlPattern: string // e.g. https://hostname/vid/%s/%s/thumbnail.jpg\n    },\n  ) {}\n  playlist({ did, cid }: { did: string; cid: string }) {\n    return util.format(\n      this.opts.playlistUrlPattern,\n      encodeURIComponent(did),\n      encodeURIComponent(cid),\n    )\n  }\n  thumbnail({ did, cid }: { did: string; cid: string }) {\n    return util.format(\n      this.opts.thumbnailUrlPattern,\n      encodeURIComponent(did),\n      encodeURIComponent(cid),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/bsky/test.env",
    "content": "LOG_ENABLED=true\nLOG_DESTINATION=test.log\n"
  },
  {
    "path": "packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`feed generation does not embed taken-down feed generator records in posts 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.record#view\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.embed.record#viewNotFound\",\n      \"notFound\": true,\n      \"uri\": \"record(1)\",\n    },\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.record\",\n      \"record\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n    },\n    \"text\": \"weird feed\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n  \"viewer\": Object {\n    \"bookmarked\": false,\n    \"embeddingDisabled\": false,\n    \"threadMuted\": false,\n  },\n}\n`;\n\nexports[`feed generation does not embed taken-down starter pack records in posts 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.record#view\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.embed.record#viewNotFound\",\n      \"notFound\": true,\n      \"uri\": \"record(1)\",\n    },\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.record\",\n      \"record\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n    },\n    \"text\": \"annoying starter pack\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n  \"viewer\": Object {\n    \"bookmarked\": false,\n    \"embeddingDisabled\": false,\n    \"threadMuted\": false,\n  },\n}\n`;\n\nexports[`feed generation embeds feed generator records in posts 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.record#view\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.defs#generatorView\",\n      \"cid\": \"cids(2)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(3)\",\n            \"uri\": \"record(4)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(3)\",\n            \"uri\": \"record(4)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(3)\",\n          \"following\": \"record(2)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"Provides all feed candidates\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"All\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"uri\": \"record(1)\",\n      \"viewer\": Object {\n        \"like\": \"record(5)\",\n      },\n    },\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.record\",\n      \"record\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n    },\n    \"text\": \"cool feed!\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n  \"viewer\": Object {\n    \"bookmarked\": false,\n    \"embeddingDisabled\": false,\n    \"threadMuted\": false,\n  },\n}\n`;\n\nexports[`feed generation embeds starter pack records in posts 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.record#view\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.defs#starterPackViewBasic\",\n      \"cid\": \"cids(2)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(2)\",\n            \"uri\": \"record(5)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(2)\",\n            \"uri\": \"record(5)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(4)\",\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"joinedAllTimeCount\": 0,\n      \"joinedWeekCount\": 0,\n      \"labels\": Array [],\n      \"record\": Object {\n        \"$type\": \"app.bsky.graph.starterpack\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"\",\n        \"descriptionFacets\": Array [],\n        \"feeds\": Array [],\n        \"list\": \"record(2)\",\n        \"name\": \"awesome starter pack!\",\n      },\n      \"uri\": \"record(1)\",\n    },\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.record\",\n      \"record\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n    },\n    \"text\": \"sick starter pack!\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n  \"viewer\": Object {\n    \"bookmarked\": false,\n    \"embeddingDisabled\": false,\n    \"threadMuted\": false,\n  },\n}\n`;\n\nexports[`feed generation getActorFeeds fetches feed generators by actor. 1`] = `\nArray [\n  Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Provides odd-indexed feed candidates\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Odd\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"acceptsInteractions\": true,\n    \"cid\": \"cids(3)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Has acceptsInteractions set to true\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Accepts Interactions\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(4)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"cid\": \"cids(4)\",\n    \"contentMode\": \"app.bsky.feed.defs#contentModeVideo\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Has a contentMode specified\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Content mode video\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(5)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"cid\": \"cids(5)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Provides all feed candidates when authed\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Needs Auth\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(6)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"cid\": \"cids(6)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Echoes back the same cursor it received\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Bad Pagination Cursor\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(7)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"cid\": \"cids(7)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Provides all feed candidates, blindly ignoring pagination limit\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Bad Pagination Limit\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(8)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"cid\": \"cids(8)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Provides even-indexed feed candidates\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Even\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(9)\",\n    \"viewer\": Object {},\n  },\n  Object {\n    \"cid\": \"cids(9)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Provides all feed candidates\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"All\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 2,\n    \"uri\": \"record(10)\",\n    \"viewer\": Object {\n      \"like\": \"record(11)\",\n    },\n  },\n]\n`;\n\nexports[`feed generation getFeed paginates, handling replies and reposts. 1`] = `\nArray [\n  Object {\n    \"feedContext\": \"item-0\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-1\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(4)\",\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-2\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(4)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(2)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(4)\",\n                \"following\": \"record(3)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(3)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(2)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(8)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-4\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(4)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(10)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-5\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(6)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(12)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(7)\",\n              \"following\": \"record(6)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(4)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(2)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(4)\",\n                      \"following\": \"record(3)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(3)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(2)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(5)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(5)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(6)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(3)\",\n                  \"uri\": \"record(2)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(5)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(11)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(4)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(13)\",\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n]\n`;\n\nexports[`feed generation getFeed resolves basic feed contents without auth. 1`] = `\nArray [\n  Object {\n    \"feedContext\": \"item-0\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-2\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(4)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n            },\n            \"cid\": \"cids(6)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(3)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(4)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(6)\",\n              \"uri\": \"record(3)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-4\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(5)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(5)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(4)\",\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(5)\",\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(5)\",\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n]\n`;\n\nexports[`feed generation getFeed resolves basic feed contents. 1`] = `\nArray [\n  Object {\n    \"feedContext\": \"item-0\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-2\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(4)\",\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(4)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(7)\",\n                \"following\": \"record(6)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(6)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(5)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(4)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(6)\",\n              \"uri\": \"record(5)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(8)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n  Object {\n    \"feedContext\": \"item-4\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(4)\",\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(10)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    \"reqId\": \"req-id-abc-def-ghi\",\n  },\n]\n`;\n\nexports[`feed generation getFeedGenerator describes a feed gen & returns acceptsInteractions when true 1`] = `\nObject {\n  \"isOnline\": true,\n  \"isValid\": true,\n  \"view\": Object {\n    \"acceptsInteractions\": true,\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Has acceptsInteractions set to true\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Accepts Interactions\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {},\n  },\n}\n`;\n\nexports[`feed generation getFeedGenerator describes a feed gen & returns content mode 1`] = `\nObject {\n  \"isOnline\": true,\n  \"isValid\": true,\n  \"view\": Object {\n    \"cid\": \"cids(0)\",\n    \"contentMode\": \"app.bsky.feed.defs#contentModeVideo\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Has a contentMode specified\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Content mode video\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {},\n  },\n}\n`;\n\nexports[`feed generation getFeedGenerator describes a feed gen & returns online status 1`] = `\nObject {\n  \"isOnline\": true,\n  \"isValid\": true,\n  \"view\": Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"Provides all feed candidates\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"All\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 2,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"like\": \"record(4)\",\n    },\n  },\n}\n`;\n\nexports[`feed generation getFeedGenerators describes multiple feed gens 1`] = `\nObject {\n  \"feeds\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"Provides even-indexed feed candidates\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"Even\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {},\n    },\n    Object {\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"Provides all feed candidates\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"All\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"like\": \"record(5)\",\n      },\n    },\n  ],\n}\n`;\n\nexports[`feed generation getSuggestedFeeds returns list of suggested feed generators 1`] = `\nObject {\n  \"cursor\": \"4\",\n  \"feeds\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"Provides all feed candidates\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"All\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"like\": \"record(4)\",\n      },\n    },\n    Object {\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"Provides even-indexed feed candidates\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"Even\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {},\n    },\n    Object {\n      \"cid\": \"cids(4)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"Provides all feed candidates, blindly ignoring pagination limit\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"Bad Pagination Limit\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {},\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/_util.ts",
    "content": "import { Server } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { type Express } from 'express'\nimport { CID } from 'multiformats/cid'\nimport { AppBskyFeedGetPostThread } from '@atproto/api'\nimport { lexToJson } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport {\n  isView as isEmbedRecordView,\n  isViewRecord,\n} from '../src/lexicon/types/app/bsky/embed/record'\nimport { isView as isEmbedRecordWithMediaView } from '../src/lexicon/types/app/bsky/embed/recordWithMedia'\nimport {\n  FeedViewPost,\n  PostView,\n  isPostView,\n  isReasonRepost,\n  isThreadViewPost,\n} from '../src/lexicon/types/app/bsky/feed/defs'\nimport {\n  LabelerView,\n  isLabelerView,\n  isLabelerViewDetailed,\n} from '../src/lexicon/types/app/bsky/labeler/defs'\nimport { $Typed } from '../src/lexicon/util'\n\ntype ThreadViewPost = Extract<\n  AppBskyFeedGetPostThread.OutputSchema['thread'],\n  { post: { uri: string } }\n>\n\nexport function assertIsThreadViewPost(\n  value: unknown,\n): asserts value is ThreadViewPost {\n  expect(value).toMatchObject({\n    $type: 'app.bsky.feed.defs#threadViewPost',\n  })\n}\n\n// Swap out identifiers and dates with stable\n// values for the purpose of snapshot testing\nexport const forSnapshot = (obj: unknown) => {\n  const records = { [kTake]: 'record' }\n  const collections = { [kTake]: 'collection' }\n  const users = { [kTake]: 'user' }\n  const cids = { [kTake]: 'cids' }\n  const unknown = { [kTake]: 'unknown' }\n  const toWalk = lexToJson(obj as any) // remove any blobrefs/cids\n  return mapLeafValues(toWalk, (item) => {\n    const asCid = CID.asCID(item)\n    if (asCid !== null) {\n      return take(cids, asCid.toString())\n    }\n    if (typeof item !== 'string') {\n      return item\n    }\n    const str = item.startsWith('did:plc:') ? `at://${item}` : item\n    if (str.startsWith('at://')) {\n      const uri = new AtUri(str)\n      if (uri.rkey) {\n        return take(records, str)\n      }\n      if (uri.collection) {\n        return take(collections, str)\n      }\n      if (uri.hostname) {\n        return take(users, str)\n      }\n      return take(unknown, str)\n    }\n    if (str.match(/^\\d{4}-\\d{2}-\\d{2}T/)) {\n      if (str.match(/\\d{6}Z$/)) {\n        return constantDate.replace('Z', '000Z') // e.g. microseconds in record createdAt\n      } else if (str.endsWith('+00:00')) {\n        return constantDate.replace('Z', '+00:00') // e.g. timezone in record createdAt\n      } else {\n        return constantDate\n      }\n    }\n    if (str.match(/^\\d+__bafy/)) {\n      return constantKeysetCursor\n    }\n    if (str.match(/\\/img\\/[^/]+\\/.+\\/did:plc:[^/]+\\/[^/@]+(?:@[\\w]+)?$/)) {\n      // Match image urls, stripping optional format suffix (e.g. @webp) for stable snapshots\n      const match = str.match(\n        /\\/img\\/[^/]+\\/.+\\/(did:plc:[^/]+)\\/([^/@]+)(?:@[\\w]+)?$/,\n      )\n      if (!match) return str\n      const [, did, cid] = match\n      return str\n        .replace(did, take(users, did))\n        .replace(new RegExp(`${cid}(?:@\\\\w+)?`), take(cids, cid))\n    }\n    if (str.match(/\\/vid\\/did%3Aplc%3A[^/]+\\/[^/]+\\/[^/]+$/)) {\n      // Match video urls\n      const match = str.match(/\\/vid\\/(did%3Aplc%3A[^/]+)\\/([^/]+)\\/[^/]+$/)\n      if (!match) return str\n      const [, did, cid] = match\n      return str\n        .replace(did, take(users, decodeURIComponent(did)))\n        .replace(cid, take(cids, cid))\n    }\n    let isCid: boolean\n    try {\n      CID.parse(str)\n      isCid = true\n    } catch (_err) {\n      isCid = false\n    }\n    if (isCid) {\n      return take(cids, str)\n    }\n    return item\n  })\n}\n\n// Feed testing utils\n\nexport const getOriginator = (item: FeedViewPost) => {\n  if (isReasonRepost(item.reason)) {\n    return item.reason.by.did\n  } else {\n    return item.post.author.did\n  }\n}\n\n// Useful for remapping ids in snapshot testing, to make snapshots deterministic.\n// E.g. you may use this to map this:\n//   [{ uri: 'did://rad'}, { uri: 'did://bad' }, { uri: 'did://rad'}]\n// to this:\n//   [{ uri: '0'}, { uri: '1' }, { uri: '0'}]\nconst kTake = Symbol('take')\nexport function take(obj: Record<string, number>, value: string): string\nexport function take(\n  obj: Record<string, number>,\n  value?: string,\n): string | undefined\nexport function take(\n  obj: { [s: string]: number; [kTake]?: string },\n  value: string | undefined,\n): string | undefined {\n  if (value === undefined) {\n    return\n  }\n  if (!(value in obj)) {\n    obj[value] = Object.keys(obj).length\n  }\n  const kind = obj[kTake]\n  return typeof kind === 'string'\n    ? `${kind}(${obj[value]})`\n    : String(obj[value])\n}\n\nexport const constantDate = new Date(0).toISOString()\nexport const constantKeysetCursor = '0000000000000__bafycid'\n\nconst mapLeafValues = (\n  obj: unknown,\n  fn: (val: unknown) => unknown,\n): unknown => {\n  if (Array.isArray(obj)) {\n    return obj.map((item) => mapLeafValues(item, fn))\n  }\n  if (obj && typeof obj === 'object') {\n    return Object.entries(obj).reduce(\n      (collect, [name, value]) =>\n        Object.assign(collect, { [name]: mapLeafValues(value, fn) }),\n      {},\n    )\n  }\n  return fn(obj)\n}\n\nexport const paginateAll = async <T extends { cursor?: string }>(\n  fn: (cursor?: string) => Promise<T>,\n  limit = Infinity,\n): Promise<T[]> => {\n  const results: T[] = []\n  let cursor\n  do {\n    const res = await fn(cursor)\n    results.push(res)\n    cursor = res.cursor\n  } while (cursor && results.length < limit)\n  return results\n}\n\n// @NOTE mutates\nexport const stripViewer = <T extends { viewer?: unknown }>(\n  val: T,\n): Omit<T, 'viewer'> => {\n  delete val.viewer\n  return val\n}\n\n// @NOTE mutates\nexport function stripViewerFromPost(\n  postUnknown: object,\n  withType?: false,\n): PostView\nexport function stripViewerFromPost(\n  postUnknown: object,\n  withType: true,\n): $Typed<PostView>\nexport function stripViewerFromPost(\n  postUnknown: object,\n  withType = false,\n): PostView {\n  if ('$type' in postUnknown && !isPostView(postUnknown)) {\n    throw new Error('Expected post view')\n  }\n  const post = postUnknown as PostView\n  if (withType) {\n    post.$type = 'app.bsky.feed.defs#postView'\n  } else {\n    delete post.$type\n  }\n  post.author = stripViewer(post.author)\n\n  const recordEmbed = extractRecordEmbed(post.embed)\n\n  if (recordEmbed) {\n    recordEmbed.author = stripViewer(recordEmbed.author)\n    recordEmbed.embeds?.forEach((deepEmbed) => {\n      const deepRecordEmbed = extractRecordEmbed(deepEmbed)\n      if (deepRecordEmbed) {\n        deepRecordEmbed.author = stripViewer(deepRecordEmbed.author)\n      }\n    })\n  }\n  return stripViewer(post)\n}\n\nconst extractRecordEmbed = (embed: PostView['embed']) =>\n  isEmbedRecordView(embed)\n    ? isViewRecord(embed.record)\n      ? embed.record\n      : undefined\n    : isEmbedRecordWithMediaView(embed)\n      ? isViewRecord(embed.record.record)\n        ? embed.record.record\n        : undefined\n      : undefined\n\n// @NOTE mutates\nexport const stripViewerFromThread = <T>(threadUnknown: T): T => {\n  if (!isThreadViewPost(threadUnknown)) return threadUnknown\n\n  const thread = threadUnknown as typeof threadUnknown & ThreadViewPost\n\n  // @ts-expect-error \"viewer\" does not exist on type 'ThreadViewPost'\n  delete thread.viewer\n\n  thread.post = stripViewerFromPost(thread.post)\n  if (isThreadViewPost(thread.parent)) {\n    thread.parent = stripViewerFromThread(thread.parent)\n  }\n  if (thread.replies) {\n    thread.replies = thread.replies.map(stripViewerFromThread)\n  }\n  return thread\n}\n\n// @NOTE mutates\nexport const stripViewerFromLabeler = (serviceUnknown: object): LabelerView => {\n  if (\n    '$type' in serviceUnknown &&\n    !isLabelerView(serviceUnknown) &&\n    !isLabelerViewDetailed(serviceUnknown)\n  ) {\n    throw new Error('Expected mod service view')\n  }\n  const labeler = serviceUnknown as LabelerView\n  labeler.creator = stripViewer(labeler.creator)\n  return stripViewer(labeler)\n}\n\nexport async function startServer(app: Express) {\n  return new Promise<{\n    origin: string\n    server: Server\n    stop: () => Promise<void>\n  }>((resolve, reject) => {\n    const onListen = () => {\n      const port = (server.address() as AddressInfo).port\n      resolve({\n        server,\n        origin: `http://localhost:${port}`,\n        stop: () => stopServer(server),\n      })\n      cleanup()\n    }\n    const onError = (err: Error) => {\n      reject(err)\n      cleanup()\n    }\n    const cleanup = () => {\n      server.removeListener('listening', onListen)\n      server.removeListener('error', onError)\n    }\n\n    const server = app\n      .listen(0)\n      .once('listening', onListen)\n      .once('error', onError)\n  })\n}\n\nexport async function stopServer(server: Server) {\n  return new Promise<void>((resolve, reject) => {\n    server.close((err) => {\n      if (err) {\n        reject(err)\n      } else {\n        resolve()\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "packages/bsky/tests/admin/admin-auth.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env'\nimport { createServiceAuthHeaders } from '@atproto/xrpc-server'\nimport { ids } from '../../src/lexicon/lexicons'\nimport {\n  RepoRef,\n  isRepoRef,\n} from '../../src/lexicon/types/com/atproto/admin/defs'\nimport { $Typed } from '../../src/lexicon/util'\n\ndescribe('admin auth', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let repoSubject: $Typed<RepoRef>\n\n  const modServiceDid = 'did:example:mod'\n  const altModDid = 'did:example:alt'\n  let modServiceKey: Secp256k1Keypair\n  let bskyDid: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_admin_auth',\n      bsky: {\n        modServiceDid,\n      },\n    })\n\n    bskyDid = network.bsky.ctx.cfg.serverDid\n\n    modServiceKey = await Secp256k1Keypair.create()\n    const origResolve = network.bsky.dataplane.idResolver.did.resolve\n    network.bsky.dataplane.idResolver.did.resolve = async function (\n      did: string,\n      forceRefresh?: boolean,\n    ) {\n      if (did === modServiceDid || did === altModDid) {\n        return {\n          '@context': [\n            'https://www.w3.org/ns/did/v1',\n            'https://w3id.org/security/multikey/v1',\n            'https://w3id.org/security/suites/secp256k1-2019/v1',\n          ],\n          id: did,\n          verificationMethod: [\n            {\n              id: `${did}#atproto`,\n              type: 'Multikey',\n              controller: did,\n              publicKeyMultibase: modServiceKey.did().replace('did:key:', ''),\n            },\n          ],\n        }\n      }\n      return origResolve.call(this, did, forceRefresh)\n    }\n\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    await network.processAll()\n    repoSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('allows service auth requests from the configured appview did', async () => {\n    const updateHeaders = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: bskyDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: modServiceKey,\n    })\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...updateHeaders,\n        encoding: 'application/json',\n      },\n    )\n\n    const getHeaders = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: bskyDid,\n      lxm: ids.ComAtprotoAdminGetSubjectStatus,\n      keypair: modServiceKey,\n    })\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      { did: repoSubject.did },\n      getHeaders,\n    )\n    assert(isRepoRef(res.data.subject))\n    expect(res.data.subject.did).toBe(repoSubject.did)\n    expect(res.data.takedown?.applied).toBe(true)\n  })\n\n  it('does not allow requests from another did', async () => {\n    const headers = await createServiceAuthHeaders({\n      iss: altModDid,\n      aud: bskyDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: modServiceKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow('Untrusted issuer')\n  })\n\n  it('does not allow requests from an authenticated user', async () => {\n    const aliceKey = await network.pds.ctx.actorStore.keypair(sc.dids.alice)\n    const headers = await createServiceAuthHeaders({\n      iss: sc.dids.alice,\n      aud: bskyDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: aliceKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow('Untrusted issuer')\n  })\n\n  it('does not allow requests with a bad signature', async () => {\n    const badKey = await Secp256k1Keypair.create()\n    const headers = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: bskyDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: badKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow(\n      'jwt signature does not match jwt issuer',\n    )\n  })\n\n  it('does not allow requests with a bad aud', async () => {\n    // repo subject is bob, so we set alice as the audience\n    const headers = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: sc.dids.alice,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: modServiceKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow(\n      'jwt audience does not match service did',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/admin/moderation.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { ImageRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport {\n  RepoBlobRef,\n  RepoRef,\n  isRepoBlobRef,\n  isRepoRef,\n} from '../../src/lexicon/types/com/atproto/admin/defs'\nimport {\n  Main as StrongRef,\n  isMain as isStrongRef,\n} from '../../src/lexicon/types/com/atproto/repo/strongRef'\nimport { $Typed } from '../../src/lexicon/util'\n\ndescribe('moderation', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let repoSubject: $Typed<RepoRef>\n  let recordSubject: $Typed<StrongRef>\n  let blobSubject: $Typed<RepoBlobRef>\n  let blobRef: ImageRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_moderation',\n    })\n\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    repoSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n    const post = sc.posts[sc.dids.carol][0]\n    recordSubject = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: post.ref.uriStr,\n      cid: post.ref.cidStr,\n    }\n    blobRef = post.images[1]\n    blobSubject = {\n      $type: 'com.atproto.admin.defs#repoBlobRef',\n      did: sc.dids.carol,\n      cid: blobRef.image.ref.toString(),\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('takes down accounts', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.bsky.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        did: repoSubject.did,\n      },\n      { headers: network.bsky.adminAuthHeaders() },\n    )\n    assert(isRepoRef(res.data.subject))\n    expect(res.data.subject.did).toEqual(sc.dids.bob)\n    expect(res.data.takedown?.applied).toBe(true)\n    // expect(res.data.takedown?.ref).toBe('test-repo') @TODO add these checks back in once takedown refs make it into dataplane\n  })\n\n  it('restores takendown accounts', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.bsky.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        did: repoSubject.did,\n      },\n      { headers: network.bsky.adminAuthHeaders() },\n    )\n    assert(isRepoRef(res.data.subject))\n    expect(res.data.subject.did).toEqual(sc.dids.bob)\n    expect(res.data.takedown?.applied).toBe(false)\n    expect(res.data.takedown?.ref).toBeUndefined()\n  })\n\n  it('takes down records', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: recordSubject,\n        takedown: { applied: true, ref: 'test-record' },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.bsky.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        uri: recordSubject.uri,\n      },\n      { headers: network.bsky.adminAuthHeaders() },\n    )\n    assert(isStrongRef(res.data.subject))\n    expect(res.data.subject.uri).toEqual(recordSubject.uri)\n    expect(res.data.takedown?.applied).toBe(true)\n    // expect(res.data.takedown?.ref).toBe('test-record')\n  })\n\n  it('restores takendown records', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: recordSubject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.bsky.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        uri: recordSubject.uri,\n      },\n      { headers: network.bsky.adminAuthHeaders() },\n    )\n    assert(isStrongRef(res.data.subject))\n    expect(res.data.subject.uri).toEqual(recordSubject.uri)\n    expect(res.data.takedown?.applied).toBe(false)\n    expect(res.data.takedown?.ref).toBeUndefined()\n  })\n\n  describe('blob takedown', () => {\n    let blobUri: string\n    let imageUri: string\n\n    beforeAll(async () => {\n      blobUri = `${network.bsky.url}/blob/${blobSubject.did}/${blobSubject.cid}`\n      imageUri = network.bsky.ctx.views.imgUriBuilder\n        .getPresetUri('feed_thumbnail', blobSubject.did, blobSubject.cid)\n        .replace(network.bsky.ctx.cfg.publicUrl || '', network.bsky.url)\n      // Warm image server cache\n      await fetch(imageUri)\n      const cached = await fetch(imageUri)\n      expect(cached.headers.get('x-cache')).toEqual('hit')\n    })\n\n    it('takes down blobs', async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: blobSubject,\n          takedown: { applied: true, ref: 'test-blob' },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.bsky.adminAuthHeaders(),\n        },\n      )\n      const res = await agent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: blobSubject.did,\n          blob: blobSubject.cid,\n        },\n        { headers: network.bsky.adminAuthHeaders() },\n      )\n      assert(isRepoBlobRef(res.data.subject))\n      expect(res.data.subject.did).toEqual(blobSubject.did)\n      expect(res.data.subject.cid).toEqual(blobSubject.cid)\n      expect(res.data.takedown?.applied).toBe(true)\n      // expect(res.data.takedown?.ref).toBe('test-blob')\n    })\n\n    it('prevents resolution of blob', async () => {\n      const resolveBlob = await fetch(blobUri)\n      expect(resolveBlob.status).toEqual(404)\n      expect(await resolveBlob.json()).toEqual({\n        error: 'NotFoundError',\n        message: 'Blob not found',\n      })\n    })\n\n    it('restores blob when takedown is removed', async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: blobSubject,\n          takedown: { applied: false },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.bsky.adminAuthHeaders(),\n        },\n      )\n\n      // Can resolve blob\n      const resolveBlob = await fetch(blobUri)\n      expect(resolveBlob.status).toEqual(200)\n\n      // Can fetch through image server\n      const fetchImage = await fetch(imageUri)\n      expect(fetchImage.status).toEqual(200)\n      const size = Number(fetchImage.headers.get('content-length'))\n      expect(size).toBeGreaterThan(9000)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/auth.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport { SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env'\nimport { createServiceJwt } from '@atproto/xrpc-server'\nimport { ids } from '../src/lexicon/lexicons'\n\ndescribe('auth', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_auth',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  // @TODO invalidations do not originate from appview frontends: requires identity event on the repo stream.\n  it.skip('handles signing key change for service auth.', async () => {\n    const issuer = sc.dids.alice\n    const attemptWithKey = async (keypair: Keypair) => {\n      const jwt = await createServiceJwt({\n        iss: issuer,\n        aud: network.bsky.ctx.cfg.serverDid,\n        lxm: ids.AppBskyActorGetProfile,\n        keypair,\n      })\n      return agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.carol },\n        { headers: { authorization: `Bearer ${jwt}` } },\n      )\n    }\n    const origSigningKey = await network.pds.ctx.actorStore.keypair(issuer)\n    const newSigningKey = await Secp256k1Keypair.create({ exportable: true })\n    // confirm original signing key works\n    await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined()\n    // confirm next signing key doesn't work yet\n    await expect(attemptWithKey(newSigningKey)).rejects.toThrow(\n      'jwt signature does not match jwt issuer',\n    )\n    // update to new signing key\n    await network.plc\n      .getClient()\n      .updateAtprotoKey(\n        issuer,\n        network.pds.ctx.plcRotationKey,\n        newSigningKey.did(),\n      )\n    // old signing key still works due to did doc cache\n    await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined()\n    // new signing key works\n    await expect(attemptWithKey(newSigningKey)).resolves.toBeDefined()\n    // old signing key no longer works after cache is updated\n    await expect(attemptWithKey(origSigningKey)).rejects.toThrow(\n      'jwt signature does not match jwt issuer',\n    )\n  })\n\n  it('throws if the user key is incorrect', async () => {\n    const bobKeypair = await network.pds.ctx.actorStore.keypair(bob)\n\n    const jwt = await createServiceJwt({\n      iss: alice,\n      aud: network.bsky.ctx.cfg.serverDid,\n      lxm: ids.AppBskyFeedGetTimeline,\n      keypair: bobKeypair,\n    })\n\n    await expect(\n      agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.carol },\n        {\n          headers: { authorization: `Bearer ${jwt}` },\n        },\n      ),\n    ).rejects.toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/blob-resolver.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { request } from 'undici'\nimport { cidForCbor, verifyCidForBytes } from '@atproto/common'\nimport { randomBytes } from '@atproto/crypto'\nimport { TestNetwork, basicSeed } from '@atproto/dev-env'\n\ndescribe('blob resolver', () => {\n  let network: TestNetwork\n  let fileDid: string\n  let fileCid: CID\n  let fileSize: number\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_blob_resolver',\n    })\n    const sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    fileDid = sc.dids.carol\n    fileCid = sc.posts[fileDid][0].images[0].image.ref\n    fileSize = sc.posts[fileDid][0].images[0].image.size\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('resolves blob with good signature check.', async () => {\n    const response = await request(\n      new URL(`/blob/${fileDid}/${fileCid.toString()}`, network.bsky.url),\n    )\n    expect(response.statusCode).toEqual(200)\n    expect(response.headers['content-type']).toEqual('image/jpeg')\n    expect(response.headers['content-security-policy']).toEqual(\n      `default-src 'none'; sandbox`,\n    )\n    expect(response.headers['x-content-type-options']).toEqual('nosniff')\n\n    const bytes = new Uint8Array(await response.body.arrayBuffer())\n    await expect(verifyCidForBytes(fileCid, bytes)).resolves.toBeUndefined()\n  })\n\n  it('404s on missing blob.', async () => {\n    const badCid = await cidForCbor({ unknown: true })\n    const response = await request(\n      new URL(`/blob/${fileDid}/${badCid.toString()}`, network.bsky.url),\n    )\n    expect(response.statusCode).toEqual(404)\n    await expect(response.body.json()).resolves.toEqual({\n      error: 'NotFoundError',\n      message: 'Blob not found',\n    })\n  })\n\n  it('404s on missing identity.', async () => {\n    const nonExistingDid = `did:plc:${'a'.repeat(24)}`\n\n    const response = await request(\n      new URL(\n        `/blob/${nonExistingDid}/${fileCid.toString()}`,\n        network.bsky.url,\n      ),\n    )\n    expect(response.statusCode).toEqual(404)\n    await expect(response.body.json()).resolves.toEqual({\n      error: 'NotFoundError',\n      message: 'Origin not found',\n    })\n  })\n\n  it('400s on invalid did.', async () => {\n    const response = await request(\n      new URL(`/blob/did::/${fileCid.toString()}`, network.bsky.url),\n    )\n    expect(response.statusCode).toEqual(400)\n    await expect(response.body.json()).resolves.toEqual({\n      error: 'BadRequestError',\n      message: 'Invalid did',\n    })\n  })\n\n  it('400s on invalid cid.', async () => {\n    const response = await request(\n      new URL(`/blob/${fileDid}/barfy`, network.bsky.url),\n    )\n    expect(response.statusCode).toEqual(400)\n    await expect(response.body.json()).resolves.toEqual({\n      error: 'BadRequestError',\n      message: 'Invalid cid',\n    })\n  })\n\n  it('400s on missing file.', async () => {\n    const missingCid = await cidForCbor('missing-file')\n\n    const response = await request(\n      new URL(`/blob/${fileDid}/${missingCid}`, network.bsky.url),\n    )\n    expect(response.statusCode).toEqual(404)\n    await expect(response.body.json()).resolves.toEqual({\n      error: 'NotFoundError',\n      message: 'Blob not found',\n    })\n  })\n\n  it('replaces the file with invalid bytes.', async () => {\n    await network.pds.ctx.blobstore(fileDid).delete(fileCid)\n    await network.pds.ctx\n      .blobstore(fileDid)\n      .putPermanent(fileCid, randomBytes(fileSize))\n  })\n\n  it('fails to fetch bytes on blob with bad signature check.', async () => {\n    const response = await request(\n      new URL(`/blob/${fileDid}/${fileCid.toString()}`, network.bsky.url),\n    )\n\n    expect(response.statusCode).toEqual(404)\n    await expect(response.body.json()).resolves.toEqual({\n      error: 'NotFoundError',\n      message: 'Bad cid check',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/__snapshots__/indexing.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`indexing indexRepo updates indexes when records change. 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"description\": \"freshening things up\",\n    \"did\": \"user(0)\",\n    \"followersCount\": 2,\n    \"followsCount\": 2,\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"postsCount\": 5,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"knownFollowers\": Object {\n        \"count\": 1,\n        \"followers\": Array [\n          Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(1)\",\n            \"displayName\": \"bobby\",\n            \"handle\": \"bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(1)\",\n              \"following\": \"record(0)\",\n              \"muted\": false,\n            },\n          },\n        ],\n      },\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"cursor\": \"0000000000000__bafycid\",\n    \"feed\": Array [\n      Object {\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"fresh post!\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n      Object {\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(2)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(4)\",\n                \"uri\": \"record(5)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(4)\",\n              },\n            },\n            \"text\": \"thanks bob\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"reply\": Object {\n          \"grandparentAuthor\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"parent\": Object {\n            \"$type\": \"app.bsky.feed.defs#postView\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(1)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(1)\",\n                \"following\": \"record(0)\",\n                \"muted\": false,\n              },\n            },\n            \"bookmarkCount\": 0,\n            \"cid\": \"cids(4)\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.images#view\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(2)/cids(5)\",\n                  \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(2)/cids(5)\",\n                },\n              ],\n            },\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"did:example:labeler\",\n                \"uri\": \"record(5)\",\n                \"val\": \"test-label\",\n              },\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"did:example:labeler\",\n                \"uri\": \"record(5)\",\n                \"val\": \"test-label-2\",\n              },\n            ],\n            \"likeCount\": 0,\n            \"quoteCount\": 0,\n            \"record\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"embed\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(5)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                ],\n              },\n              \"reply\": Object {\n                \"parent\": Object {\n                  \"cid\": \"cids(3)\",\n                  \"uri\": \"record(4)\",\n                },\n                \"root\": Object {\n                  \"cid\": \"cids(3)\",\n                  \"uri\": \"record(4)\",\n                },\n              },\n              \"text\": \"hear that label_me label_me_2\",\n            },\n            \"replyCount\": 1,\n            \"repostCount\": 0,\n            \"uri\": \"record(5)\",\n            \"viewer\": Object {\n              \"bookmarked\": false,\n              \"embeddingDisabled\": false,\n              \"threadMuted\": false,\n            },\n          },\n          \"root\": Object {\n            \"$type\": \"app.bsky.feed.defs#postView\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(0)\",\n              \"handle\": \"alice.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"muted\": false,\n              },\n            },\n            \"bookmarkCount\": 0,\n            \"cid\": \"cids(3)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 3,\n            \"quoteCount\": 0,\n            \"record\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n              \"text\": \"again\",\n            },\n            \"replyCount\": 2,\n            \"repostCount\": 1,\n            \"uri\": \"record(4)\",\n            \"viewer\": Object {\n              \"bookmarked\": false,\n              \"embeddingDisabled\": false,\n              \"threadMuted\": false,\n            },\n          },\n        },\n      },\n      Object {\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(6)\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.record#view\",\n            \"record\": Object {\n              \"$type\": \"app.bsky.embed.record#viewRecord\",\n              \"author\": Object {\n                \"associated\": Object {\n                  \"activitySubscription\": Object {\n                    \"allowSubscriptions\": \"followers\",\n                  },\n                  \"chat\": Object {\n                    \"allowIncoming\": \"none\",\n                  },\n                },\n                \"did\": \"user(3)\",\n                \"handle\": \"dan.test\",\n                \"labels\": Array [],\n                \"viewer\": Object {\n                  \"blockedBy\": false,\n                  \"following\": \"record(8)\",\n                  \"muted\": false,\n                },\n              },\n              \"cid\": \"cids(7)\",\n              \"embeds\": Array [\n                Object {\n                  \"$type\": \"app.bsky.embed.record#view\",\n                  \"record\": Object {\n                    \"$type\": \"app.bsky.embed.record#viewRecord\",\n                    \"author\": Object {\n                      \"associated\": Object {\n                        \"activitySubscription\": Object {\n                          \"allowSubscriptions\": \"followers\",\n                        },\n                      },\n                      \"did\": \"user(4)\",\n                      \"handle\": \"carol.test\",\n                      \"labels\": Array [],\n                      \"viewer\": Object {\n                        \"blockedBy\": false,\n                        \"followedBy\": \"record(10)\",\n                        \"muted\": false,\n                      },\n                    },\n                    \"cid\": \"cids(8)\",\n                    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"labels\": Array [],\n                    \"likeCount\": 2,\n                    \"quoteCount\": 1,\n                    \"replyCount\": 0,\n                    \"repostCount\": 0,\n                    \"uri\": \"record(9)\",\n                    \"value\": Object {\n                      \"$type\": \"app.bsky.feed.post\",\n                      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                      \"embed\": Object {\n                        \"$type\": \"app.bsky.embed.recordWithMedia\",\n                        \"media\": Object {\n                          \"$type\": \"app.bsky.embed.images\",\n                          \"images\": Array [\n                            Object {\n                              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                              \"image\": Object {\n                                \"$type\": \"blob\",\n                                \"mimeType\": \"image/jpeg\",\n                                \"ref\": Object {\n                                  \"$link\": \"cids(5)\",\n                                },\n                                \"size\": 4114,\n                              },\n                            },\n                            Object {\n                              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                              \"image\": Object {\n                                \"$type\": \"blob\",\n                                \"mimeType\": \"image/jpeg\",\n                                \"ref\": Object {\n                                  \"$link\": \"cids(9)\",\n                                },\n                                \"size\": 12736,\n                              },\n                            },\n                          ],\n                        },\n                        \"record\": Object {\n                          \"record\": Object {\n                            \"cid\": \"cids(10)\",\n                            \"uri\": \"record(11)\",\n                          },\n                        },\n                      },\n                      \"text\": \"hi im carol\",\n                    },\n                  },\n                },\n              ],\n              \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n              \"labels\": Array [],\n              \"likeCount\": 0,\n              \"quoteCount\": 1,\n              \"replyCount\": 0,\n              \"repostCount\": 1,\n              \"uri\": \"record(7)\",\n              \"value\": Object {\n                \"$type\": \"app.bsky.feed.post\",\n                \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                \"embed\": Object {\n                  \"$type\": \"app.bsky.embed.record\",\n                  \"record\": Object {\n                    \"cid\": \"cids(8)\",\n                    \"uri\": \"record(9)\",\n                  },\n                },\n                \"facets\": Array [\n                  Object {\n                    \"features\": Array [\n                      Object {\n                        \"$type\": \"app.bsky.richtext.facet#mention\",\n                        \"did\": \"user(0)\",\n                      },\n                    ],\n                    \"index\": Object {\n                      \"byteEnd\": 18,\n                      \"byteStart\": 0,\n                    },\n                  },\n                ],\n                \"text\": \"@alice.bluesky.xyz is the best\",\n              },\n            },\n          },\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(6)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(6)\",\n              \"val\": \"test-label\",\n            },\n          ],\n          \"likeCount\": 2,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(7)\",\n                \"uri\": \"record(7)\",\n              },\n            },\n            \"text\": \"yoohoo label_me\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(6)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n      Object {\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(4)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n      Object {\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(11)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(11)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(12)\",\n              \"val\": \"self-label\",\n            },\n          ],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Object {\n              \"$type\": \"com.atproto.label.defs#selfLabels\",\n              \"values\": Array [\n                Object {\n                  \"val\": \"self-label\",\n                },\n              ],\n            },\n            \"text\": \"hey there\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(12)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    ],\n  },\n  Object {\n    \"cursor\": \"0000000000000__bafycid\",\n    \"follows\": Array [\n      Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"following\": \"record(0)\",\n          \"muted\": false,\n        },\n      },\n    ],\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"description\": \"freshening things up\",\n      \"did\": \"user(0)\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`indexing indexes posts. 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(2)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 9,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@bob.test how are you?\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"replies\": Array [],\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`indexing indexes posts. 2`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(2)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 11,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@carol.test how are you?\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"replies\": Array [],\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`indexing indexes posts. 3`] = `\nObject {\n  \"createNotifications\": Array [\n    Object {\n      \"author\": \"user(1)\",\n      \"did\": \"user(0)\",\n      \"id\": 0,\n      \"reason\": \"mention\",\n      \"reasonSubject\": null,\n      \"recordCid\": \"cids(0)\",\n      \"recordUri\": \"record(0)\",\n      \"sortAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  ],\n  \"deleteNotifications\": Array [],\n  \"updateNotifications\": Array [\n    Object {\n      \"author\": \"user(1)\",\n      \"did\": \"user(2)\",\n      \"id\": 0,\n      \"reason\": \"mention\",\n      \"reasonSubject\": null,\n      \"recordCid\": \"cids(1)\",\n      \"recordUri\": \"record(0)\",\n      \"sortAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  ],\n}\n`;\n\nexports[`indexing indexes profiles. 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"chat\": Object {\n      \"allowIncoming\": \"none\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"did\": \"user(0)\",\n  \"displayName\": \"dan\",\n  \"followersCount\": 0,\n  \"followsCount\": 0,\n  \"handle\": \"dan.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"postsCount\": 0,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`indexing indexes profiles. 2`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"chat\": Object {\n      \"allowIncoming\": \"none\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"did\": \"user(0)\",\n  \"displayName\": \"danny\",\n  \"followersCount\": 0,\n  \"followsCount\": 0,\n  \"handle\": \"dan.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"postsCount\": 0,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`indexing indexes profiles. 3`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"chat\": Object {\n      \"allowIncoming\": \"none\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"did\": \"user(0)\",\n  \"followersCount\": 0,\n  \"followsCount\": 0,\n  \"handle\": \"dan.test\",\n  \"labels\": Array [],\n  \"postsCount\": 0,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"muted\": false,\n  },\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/db.test.ts",
    "content": "import { sql } from 'kysely'\nimport { wait } from '@atproto/common'\nimport { TestNetwork } from '@atproto/dev-env'\nimport { Database } from '../../src'\n\ndescribe('db', () => {\n  let network: TestNetwork\n  let db: Database\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_db',\n    })\n    db = network.bsky.db\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('handles client errors without crashing.', async () => {\n    const tryKillConnection = db.transaction(async (dbTxn) => {\n      const result = await sql`select pg_backend_pid() as pid;`.execute(\n        dbTxn.db,\n      )\n      const pid = result.rows[0]?.['pid'] as number\n      await sql`select pg_terminate_backend(${pid});`.execute(db.db)\n      await sql`select 1;`.execute(dbTxn.db)\n    })\n    // This should throw, but no unhandled error\n    await expect(tryKillConnection).rejects.toThrow()\n  })\n\n  it('handles pool errors without crashing.', async () => {\n    const conn1 = await db.pool.connect()\n    const conn2 = await db.pool.connect()\n    const result = await conn1.query('select pg_backend_pid() as pid;')\n    const conn1pid: number = result.rows[0].pid\n    conn1.release()\n    await wait(100) // let release apply, conn is now idle on pool.\n    await conn2.query(`select pg_terminate_backend(${conn1pid});`)\n    conn2.release()\n  })\n\n  describe('transaction()', () => {\n    it('commits changes', async () => {\n      const result = await db.transaction(async (dbTxn) => {\n        return await dbTxn.db\n          .insertInto('actor')\n          .values({\n            did: 'x',\n            handle: 'x',\n            indexedAt: 'bad-date',\n          })\n          .returning('did')\n          .executeTakeFirst()\n      })\n\n      if (!result) {\n        return expect(result).toBeTruthy()\n      }\n\n      expect(result.did).toEqual('x')\n\n      const row = await db.db\n        .selectFrom('actor')\n        .select(['did', 'handle', 'indexedAt'])\n        .where('did', '=', 'x')\n        .executeTakeFirst()\n\n      expect(row).toEqual({\n        did: 'x',\n        handle: 'x',\n        indexedAt: 'bad-date',\n      })\n    })\n\n    it('rolls-back changes on failure', async () => {\n      const promise = db.transaction(async (dbTxn) => {\n        await dbTxn.db\n          .insertInto('actor')\n          .values({\n            did: 'y',\n            handle: 'y',\n            indexedAt: 'bad-date',\n          })\n          .returning('did')\n          .executeTakeFirst()\n\n        throw new Error('Oops!')\n      })\n\n      await expect(promise).rejects.toThrow('Oops!')\n\n      const row = await db.db\n        .selectFrom('actor')\n        .selectAll()\n        .where('did', '=', 'y')\n        .executeTakeFirst()\n\n      expect(row).toBeUndefined()\n    })\n\n    it('indicates isTransaction', async () => {\n      expect(db.isTransaction).toEqual(false)\n\n      await db.transaction(async (dbTxn) => {\n        expect(db.isTransaction).toEqual(false)\n        expect(dbTxn.isTransaction).toEqual(true)\n      })\n\n      expect(db.isTransaction).toEqual(false)\n    })\n\n    it('asserts transaction', async () => {\n      expect(() => db.assertTransaction()).toThrow('Transaction required')\n\n      await db.transaction(async (dbTxn) => {\n        expect(() => dbTxn.assertTransaction()).not.toThrow()\n      })\n    })\n\n    it('does not allow leaky transactions', async () => {\n      let leakedTx: Database | undefined\n\n      const tx = db.transaction(async (dbTxn) => {\n        leakedTx = dbTxn\n        await dbTxn.db\n          .insertInto('actor')\n          .values({ handle: 'a', did: 'a', indexedAt: 'bad-date' })\n          .execute()\n        throw new Error('test tx failed')\n      })\n      await expect(tx).rejects.toThrow('test tx failed')\n\n      const attempt = leakedTx?.db\n        .insertInto('actor')\n        .values({ handle: 'b', did: 'b', indexedAt: 'bad-date' })\n        .execute()\n      await expect(attempt).rejects.toThrow('tx already failed')\n\n      const res = await db.db\n        .selectFrom('actor')\n        .selectAll()\n        .where('did', 'in', ['a', 'b'])\n        .execute()\n\n      expect(res.length).toBe(0)\n    })\n\n    it('ensures all inflight queries are rolled back', async () => {\n      let promise: Promise<unknown> | undefined = undefined\n      const names: string[] = []\n      try {\n        await db.transaction(async (dbTxn) => {\n          const queries: Promise<unknown>[] = []\n          for (let i = 0; i < 20; i++) {\n            const name = `user${i}`\n            const query = dbTxn.db\n              .insertInto('actor')\n              .values({\n                handle: name,\n                did: name,\n                indexedAt: 'bad-date',\n              })\n              .execute()\n            names.push(name)\n            queries.push(query)\n          }\n          promise = Promise.allSettled(queries)\n          throw new Error()\n        })\n      } catch (err) {\n        expect(err).toBeDefined()\n      }\n      if (promise) {\n        await promise\n      }\n\n      const res = await db.db\n        .selectFrom('actor')\n        .selectAll()\n        .where('did', 'in', names)\n        .execute()\n      expect(res.length).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/duplicate-records.test.ts",
    "content": "import { TID, cidForCbor } from '@atproto/common'\nimport { TestNetwork } from '@atproto/dev-env'\nimport { WriteOpAction } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport * as lex from '../../src/lexicon/lexicons'\n\ntype Database = TestNetwork['bsky']['db']\n\ndescribe('duplicate record', () => {\n  let network: TestNetwork\n  let did: string\n  let db: Database\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_duplicates',\n    })\n    db = network.bsky.db\n    did = 'did:example:alice'\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const countRecords = async (db: Database, table: string) => {\n    const got = await db.db\n      .selectFrom(table as any)\n      .selectAll()\n      .where('creator', '=', did)\n      .execute()\n    return got.length\n  }\n\n  it('dedupes reposts', async () => {\n    const subject = AtUri.make(did, lex.ids.AppBskyFeedPost, TID.nextStr())\n    const subjectCid = await cidForCbor({ test: 'blah' })\n    const coll = lex.ids.AppBskyFeedRepost\n    const uris: AtUri[] = []\n    for (let i = 0; i < 5; i++) {\n      const repost = {\n        $type: coll,\n        subject: {\n          uri: subject.toString(),\n          cid: subjectCid.toString(),\n        },\n        createdAt: new Date().toISOString(),\n      }\n      const uri = AtUri.make(did, coll, TID.nextStr())\n      const cid = await cidForCbor(repost)\n      await network.bsky.sub.indexingSvc.indexRecord(\n        uri,\n        cid,\n        repost,\n        WriteOpAction.Create,\n        repost.createdAt,\n      )\n      uris.push(uri)\n    }\n\n    let count = await countRecords(db, 'repost')\n    expect(count).toBe(1)\n\n    await network.bsky.sub.indexingSvc.deleteRecord(uris[0], false)\n\n    count = await countRecords(db, 'repost')\n    expect(count).toBe(1)\n\n    await network.bsky.sub.indexingSvc.deleteRecord(uris[1], true)\n\n    count = await countRecords(db, 'repost')\n    expect(count).toBe(0)\n  })\n\n  it('dedupes like', async () => {\n    const subject = AtUri.make(did, lex.ids.AppBskyFeedPost, TID.nextStr())\n    const subjectCid = await cidForCbor({ test: 'blah' })\n    const coll = lex.ids.AppBskyFeedLike\n    const uris: AtUri[] = []\n    for (let i = 0; i < 5; i++) {\n      const like = {\n        $type: coll,\n        subject: {\n          uri: subject.toString(),\n          cid: subjectCid.toString(),\n        },\n        createdAt: new Date().toISOString(),\n      }\n      const uri = AtUri.make(did, coll, TID.nextStr())\n      const cid = await cidForCbor(like)\n      await network.bsky.sub.indexingSvc.indexRecord(\n        uri,\n        cid,\n        like,\n        WriteOpAction.Create,\n        like.createdAt,\n      )\n      uris.push(uri)\n    }\n\n    let count = await countRecords(db, 'like')\n    expect(count).toBe(1)\n\n    await network.bsky.sub.indexingSvc.deleteRecord(uris[0], false)\n\n    count = await countRecords(db, 'like')\n    expect(count).toBe(1)\n    const got = await db.db\n      .selectFrom('like')\n      .where('creator', '=', did)\n      .selectAll()\n      .executeTakeFirst()\n    expect(got?.uri).toEqual(uris[1].toString())\n\n    await network.bsky.sub.indexingSvc.deleteRecord(uris[1], true)\n\n    count = await countRecords(db, 'like')\n    expect(count).toBe(0)\n  })\n\n  it('dedupes follows', async () => {\n    const coll = lex.ids.AppBskyGraphFollow\n    const uris: AtUri[] = []\n    for (let i = 0; i < 5; i++) {\n      const follow = {\n        $type: coll,\n        subject: 'did:example:bob',\n        createdAt: new Date().toISOString(),\n      }\n      const uri = AtUri.make(did, coll, TID.nextStr())\n      const cid = await cidForCbor(follow)\n      await network.bsky.sub.indexingSvc.indexRecord(\n        uri,\n        cid,\n        follow,\n        WriteOpAction.Create,\n        follow.createdAt,\n      )\n      uris.push(uri)\n    }\n\n    let count = await countRecords(db, 'follow')\n    expect(count).toBe(1)\n\n    await network.bsky.sub.indexingSvc.deleteRecord(uris[0], false)\n\n    count = await countRecords(db, 'follow')\n    expect(count).toBe(1)\n\n    await network.bsky.sub.indexingSvc.deleteRecord(uris[1], true)\n\n    count = await countRecords(db, 'follow')\n    expect(count).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/handle-invalidation.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { DAY } from '@atproto/common'\nimport { SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('handle invalidation', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let alice: string\n  let bob: string\n\n  const mockHandles = {}\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_handle_invalidation',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    await network.processAll()\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n\n    const origResolve = network.bsky.dataplane.idResolver.handle.resolve\n    network.bsky.dataplane.idResolver.handle.resolve = async (\n      handle: string,\n    ) => {\n      if (mockHandles[handle] === null) {\n        return undefined\n      } else if (mockHandles[handle]) {\n        return mockHandles[handle]\n      }\n      return origResolve(handle)\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const backdateIndexedAt = async (did: string) => {\n    const TWO_DAYS_AGO = new Date(Date.now() - 2 * DAY).toISOString()\n    await network.bsky.db.db\n      .updateTable('actor')\n      .set({ indexedAt: TWO_DAYS_AGO })\n      .where('did', '=', did)\n      .execute()\n  }\n\n  it('indexes an account with no proper handle', async () => {\n    mockHandles['eve.test'] = null\n    const eveAccnt = await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@test.com',\n      password: 'eve-pass',\n    })\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: eveAccnt.did },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(res.data.handle).toEqual('handle.invalid')\n  })\n\n  it('invalidates out of date handles', async () => {\n    await backdateIndexedAt(alice)\n\n    const aliceHandle = sc.accounts[alice].handle\n    // alice's handle no longer resolves\n    mockHandles[aliceHandle] = null\n    await sc.post(alice, 'blah')\n    await network.processAll()\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(res.data.handle).toEqual('handle.invalid')\n  })\n\n  it('revalidates an out of date handle', async () => {\n    await backdateIndexedAt(alice)\n    const aliceHandle = sc.accounts[alice].handle\n    // alice's handle no longer resolves\n    delete mockHandles[aliceHandle]\n\n    await sc.post(alice, 'blah')\n    await network.processAll()\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(res.data.handle).toEqual(sc.accounts[alice].handle)\n  })\n\n  it('deals with handle contention', async () => {\n    await backdateIndexedAt(bob)\n    // update alices handle so that the pds will let bob take her old handle\n    await network.pds.ctx.accountManager.updateHandle(alice, 'not-alice.test')\n\n    await pdsAgent.api.com.atproto.identity.updateHandle(\n      {\n        handle: sc.accounts[alice].handle,\n      },\n      { headers: sc.getHeaders(bob), encoding: 'application/json' },\n    )\n    await network.processAll()\n\n    const aliceRes = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(aliceRes.data.handle).toEqual('handle.invalid')\n\n    const bobRes = await agent.api.app.bsky.actor.getProfile(\n      { actor: bob },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(bobRes.data.handle).toEqual(sc.accounts[alice].handle)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/indexing.test.ts",
    "content": "import { sql } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport {\n  AppBskyActorProfile,\n  AppBskyFeedLike,\n  AppBskyFeedPost,\n  AppBskyFeedRepost,\n  AppBskyGraphFollow,\n  AtpAgent,\n} from '@atproto/api'\nimport { TID, cidForCbor } from '@atproto/common'\nimport { SeedClient, TestNetwork, basicSeed, usersSeed } from '@atproto/dev-env'\nimport { repoPrepare } from '@atproto/pds'\nimport { WriteOpAction } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { Database } from '../../src/data-plane/server/db'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { forSnapshot } from '../_util'\n\ndescribe('indexing', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let db: Database\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_indexing',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    db = network.bsky.db\n    await usersSeed(sc)\n    // Data in tests is not processed from subscription\n    await network.processAll()\n    await network.bsky.sub.destroy()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('indexes posts.', async () => {\n    const createdAt = new Date().toISOString()\n    const createRecord = await prepareCreate({\n      did: sc.dids.alice,\n      collection: ids.AppBskyFeedPost,\n      record: {\n        $type: ids.AppBskyFeedPost,\n        text: '@bob.test how are you?',\n        facets: [\n          {\n            index: { byteStart: 0, byteEnd: 9 },\n            features: [\n              {\n                $type: `${ids.AppBskyRichtextFacet}#mention`,\n                did: sc.dids.bob,\n              },\n            ],\n          },\n        ],\n        createdAt,\n      } as AppBskyFeedPost.Record,\n    })\n    const [uri] = createRecord\n    const updateRecord = await prepareUpdate({\n      did: sc.dids.alice,\n      collection: ids.AppBskyFeedPost,\n      rkey: uri.rkey,\n      record: {\n        $type: ids.AppBskyFeedPost,\n        text: '@carol.test how are you?',\n        facets: [\n          {\n            index: { byteStart: 0, byteEnd: 11 },\n            features: [\n              {\n                $type: `${ids.AppBskyRichtextFacet}#mention`,\n                did: sc.dids.carol,\n              },\n            ],\n          },\n        ],\n        createdAt,\n      } as AppBskyFeedPost.Record,\n    })\n    const deleteRecord = prepareDelete({\n      did: sc.dids.alice,\n      collection: ids.AppBskyFeedPost,\n      rkey: uri.rkey,\n    })\n\n    // Create\n    await network.bsky.sub.indexingSvc.indexRecord(...createRecord)\n\n    const getAfterCreate = await agent.api.app.bsky.feed.getPostThread(\n      { uri: uri.toString() },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot()\n    const createNotifications = await getNotifications(db, uri)\n\n    // Update\n    await network.bsky.sub.indexingSvc.indexRecord(...updateRecord)\n\n    const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread(\n      { uri: uri.toString() },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot()\n    const updateNotifications = await getNotifications(db, uri)\n\n    // Delete\n    await network.bsky.sub.indexingSvc.deleteRecord(...deleteRecord)\n\n    const getAfterDelete = agent.api.app.bsky.feed.getPostThread(\n      { uri: uri.toString() },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    await expect(getAfterDelete).rejects.toThrow(/Post not found:/)\n    const deleteNotifications = await getNotifications(db, uri)\n\n    expect(\n      forSnapshot({\n        createNotifications,\n        updateNotifications,\n        deleteNotifications,\n      }),\n    ).toMatchSnapshot()\n  })\n\n  it('indexes profiles.', async () => {\n    const createRecord = await prepareCreate({\n      did: sc.dids.dan,\n      collection: ids.AppBskyActorProfile,\n      rkey: 'self',\n      record: {\n        $type: ids.AppBskyActorProfile,\n        displayName: 'dan',\n      } as AppBskyActorProfile.Record,\n    })\n    const [uri] = createRecord\n    const updateRecord = await prepareUpdate({\n      did: sc.dids.dan,\n      collection: ids.AppBskyActorProfile,\n      rkey: uri.rkey,\n      record: {\n        $type: ids.AppBskyActorProfile,\n        displayName: 'danny',\n      } as AppBskyActorProfile.Record,\n    })\n    const deleteRecord = prepareDelete({\n      did: sc.dids.dan,\n      collection: ids.AppBskyActorProfile,\n      rkey: uri.rkey,\n    })\n\n    // Create\n    await network.bsky.sub.indexingSvc.indexRecord(...createRecord)\n\n    const getAfterCreate = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.dan },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot()\n\n    // Update\n    await network.bsky.sub.indexingSvc.indexRecord(...updateRecord)\n\n    const getAfterUpdate = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.dan },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot()\n\n    // Delete\n    await network.bsky.sub.indexingSvc.deleteRecord(...deleteRecord)\n\n    const getAfterDelete = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.dan },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(forSnapshot(getAfterDelete.data)).toMatchSnapshot()\n  })\n\n  it('handles post aggregations out of order.', async () => {\n    const createdAt = new Date().toISOString()\n    const originalPost = await prepareCreate({\n      did: sc.dids.alice,\n      collection: ids.AppBskyFeedPost,\n      record: {\n        $type: ids.AppBskyFeedPost,\n        text: 'original post',\n        createdAt,\n      } as AppBskyFeedPost.Record,\n    })\n    const originalPostRef = {\n      uri: originalPost[0].toString(),\n      cid: originalPost[1].toString(),\n    }\n    const reply = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyFeedPost,\n      record: {\n        $type: ids.AppBskyFeedPost,\n        text: 'reply post',\n        reply: {\n          root: originalPostRef,\n          parent: originalPostRef,\n        },\n        createdAt,\n      } as AppBskyFeedPost.Record,\n    })\n    const like = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyFeedLike,\n      record: {\n        $type: ids.AppBskyFeedLike,\n        subject: originalPostRef,\n        createdAt,\n      } as AppBskyFeedLike.Record,\n    })\n    const repost = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyFeedRepost,\n      record: {\n        $type: ids.AppBskyFeedRepost,\n        subject: originalPostRef,\n        createdAt,\n      } as AppBskyFeedRepost.Record,\n    })\n    // reply, like, and repost indexed orior to the original post\n    await network.bsky.sub.indexingSvc.indexRecord(...reply)\n    await network.bsky.sub.indexingSvc.indexRecord(...like)\n    await network.bsky.sub.indexingSvc.indexRecord(...repost)\n    await network.bsky.sub.indexingSvc.indexRecord(...originalPost)\n    await network.bsky.sub.background.processAll()\n    const agg = await db.db\n      .selectFrom('post_agg')\n      .selectAll()\n      .where('uri', '=', originalPostRef.uri)\n      .executeTakeFirst()\n    expect(agg).toEqual({\n      uri: originalPostRef.uri,\n      bookmarkCount: 0,\n      replyCount: 1,\n      repostCount: 1,\n      likeCount: 1,\n      quoteCount: 0,\n    })\n    // Cleanup\n    const del = (uri: AtUri) => {\n      return prepareDelete({\n        did: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n    }\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(reply[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(like[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(repost[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(originalPost[0]))\n  })\n\n  it('does not notify user of own like or repost', async () => {\n    const createdAt = new Date().toISOString()\n\n    const originalPost = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyFeedPost,\n      record: {\n        $type: ids.AppBskyFeedPost,\n        text: 'original post',\n        createdAt,\n      } as AppBskyFeedPost.Record,\n    })\n\n    const originalPostRef = {\n      uri: originalPost[0].toString(),\n      cid: originalPost[1].toString(),\n    }\n\n    // own actions\n    const ownLike = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyFeedLike,\n      record: {\n        $type: ids.AppBskyFeedLike,\n        subject: originalPostRef,\n        createdAt,\n      } as AppBskyFeedLike.Record,\n    })\n    const ownRepost = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyFeedRepost,\n      record: {\n        $type: ids.AppBskyFeedRepost,\n        subject: originalPostRef,\n        createdAt,\n      } as AppBskyFeedRepost.Record,\n    })\n\n    // other actions\n    const aliceLike = await prepareCreate({\n      did: sc.dids.alice,\n      collection: ids.AppBskyFeedLike,\n      record: {\n        $type: ids.AppBskyFeedLike,\n        subject: originalPostRef,\n        createdAt,\n      } as AppBskyFeedLike.Record,\n    })\n    const aliceRepost = await prepareCreate({\n      did: sc.dids.alice,\n      collection: ids.AppBskyFeedRepost,\n      record: {\n        $type: ids.AppBskyFeedRepost,\n        subject: originalPostRef,\n        createdAt,\n      } as AppBskyFeedRepost.Record,\n    })\n\n    await network.bsky.sub.indexingSvc.indexRecord(...originalPost)\n    await network.bsky.sub.indexingSvc.indexRecord(...ownLike)\n    await network.bsky.sub.indexingSvc.indexRecord(...ownRepost)\n    await network.bsky.sub.indexingSvc.indexRecord(...aliceLike)\n    await network.bsky.sub.indexingSvc.indexRecord(...aliceRepost)\n    await network.bsky.sub.background.processAll()\n\n    const {\n      data: { notifications },\n    } = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    expect(notifications).toHaveLength(2)\n    expect(\n      notifications.every((n) => {\n        return n.author.did !== sc.dids.bob\n      }),\n    ).toBeTruthy()\n\n    // Cleanup\n    const del = (uri: AtUri) => {\n      return prepareDelete({\n        did: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n    }\n\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(ownLike[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(ownRepost[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(aliceLike[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(aliceRepost[0]))\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(originalPost[0]))\n  })\n\n  it('handles profile aggregations out of order.', async () => {\n    const createdAt = new Date().toISOString()\n    const unknownDid = 'did:example:unknown'\n    const follow = await prepareCreate({\n      did: sc.dids.bob,\n      collection: ids.AppBskyGraphFollow,\n      record: {\n        $type: ids.AppBskyGraphFollow,\n        subject: unknownDid,\n        createdAt,\n      } as AppBskyGraphFollow.Record,\n    })\n    await network.bsky.sub.indexingSvc.indexRecord(...follow)\n    await network.bsky.sub.background.processAll()\n    const agg = await db.db\n      .selectFrom('profile_agg')\n      .select(['did', 'followersCount'])\n      .where('did', '=', unknownDid)\n      .executeTakeFirst()\n    expect(agg).toEqual({\n      did: unknownDid,\n      followersCount: 1,\n    })\n    // Cleanup\n    const del = (uri: AtUri) => {\n      return prepareDelete({\n        did: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n    }\n    await network.bsky.sub.indexingSvc.deleteRecord(...del(follow[0]))\n  })\n\n  describe('indexRepo', () => {\n    beforeAll(async () => {\n      await network.bsky.sub.restart()\n      await basicSeed(sc, false)\n      await network.processAll()\n      await network.bsky.sub.destroy()\n      await network.bsky.sub.background.processAll()\n    })\n\n    it('preserves indexes when no record changes.', async () => {\n      // Mark originals\n      const { data: origProfile } = await agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      const { data: origFeed } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n      const { data: origFollows } = await agent.api.app.bsky.graph.getFollows(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyGraphGetFollows,\n          ),\n        },\n      )\n      // Index\n      const { data: commit } =\n        await pdsAgent.api.com.atproto.sync.getLatestCommit({\n          did: sc.dids.alice,\n        })\n      await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid)\n      await network.bsky.sub.background.processAll()\n      // Check\n      const { data: profile } = await agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      const { data: feed } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n      const { data: follows } = await agent.api.app.bsky.graph.getFollows(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyGraphGetFollows,\n          ),\n        },\n      )\n      expect(forSnapshot([origProfile, origFeed, origFollows])).toEqual(\n        forSnapshot([profile, feed, follows]),\n      )\n    })\n\n    it('updates indexes when records change.', async () => {\n      // Update profile\n      await pdsAgent.api.com.atproto.repo.putRecord(\n        {\n          repo: sc.dids.alice,\n          collection: ids.AppBskyActorProfile,\n          rkey: 'self',\n          record: { description: 'freshening things up' },\n        },\n        { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n      )\n      // Add post\n      const newPost = await sc.post(sc.dids.alice, 'fresh post!')\n      // Remove a follow\n      const removedFollow = sc.follows[sc.dids.alice][sc.dids.carol]\n      await pdsAgent.api.app.bsky.graph.follow.delete(\n        { repo: sc.dids.alice, rkey: removedFollow.uri.rkey },\n        sc.getHeaders(sc.dids.alice),\n      )\n      // Index\n      const { data: commit } =\n        await pdsAgent.api.com.atproto.sync.getLatestCommit({\n          did: sc.dids.alice,\n        })\n      await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid)\n      await network.bsky.sub.background.processAll()\n      // Check\n      const { data: profile } = await agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      const { data: feed } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n      const { data: follows } = await agent.api.app.bsky.graph.getFollows(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyGraphGetFollows,\n          ),\n        },\n      )\n      expect(profile.description).toEqual('freshening things up')\n      expect(feed.feed[0].post.uri).toEqual(newPost.ref.uriStr)\n      expect(feed.feed[0].post.cid).toEqual(newPost.ref.cidStr)\n      expect(follows.follows.map(({ did }) => did)).not.toContain(sc.dids.carol)\n      expect(forSnapshot([profile, feed, follows])).toMatchSnapshot()\n    })\n\n    it('skips invalid records.', async () => {\n      const { accountManager } = network.pds.ctx\n      // const { db: pdsDb, services: pdsServices } = network.pds.ctx\n      // Create a good and a bad post record\n      const writes = await Promise.all([\n        repoPrepare.prepareCreate({\n          did: sc.dids.alice,\n          collection: ids.AppBskyFeedPost,\n          record: { text: 'valid', createdAt: new Date().toISOString() },\n        }),\n        repoPrepare.prepareCreate({\n          did: sc.dids.alice,\n          collection: ids.AppBskyFeedPost,\n          record: { text: 0 },\n          validate: false,\n        }),\n      ])\n      const writeCommit = await network.pds.ctx.actorStore.transact(\n        sc.dids.alice,\n        (store) => store.repo.processWrites(writes),\n      )\n      await accountManager.updateRepoRoot(\n        sc.dids.alice,\n        writeCommit.cid,\n        writeCommit.rev,\n      )\n      await network.pds.ctx.sequencer.sequenceCommit(sc.dids.alice, writeCommit)\n      // Index\n      const { data: commit } =\n        await pdsAgent.api.com.atproto.sync.getLatestCommit({\n          did: sc.dids.alice,\n        })\n      await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid)\n      // Check\n      const getGoodPost = agent.api.app.bsky.feed.getPostThread(\n        { uri: writes[0].uri.toString(), depth: 0 },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      await expect(getGoodPost).resolves.toBeDefined()\n      const getBadPost = agent.api.app.bsky.feed.getPostThread(\n        { uri: writes[1].uri.toString(), depth: 0 },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      await expect(getBadPost).rejects.toThrow('Post not found')\n    })\n  })\n\n  describe('indexHandle', () => {\n    const getIndexedHandle = async (did) => {\n      const res = await agent.api.app.bsky.actor.getProfile(\n        { actor: did },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      return res.data.handle\n    }\n\n    it('indexes handle for a fresh did', async () => {\n      const now = new Date().toISOString()\n      const sessionAgent = new AtpAgent({ service: network.pds.url })\n      const {\n        data: { did },\n      } = await sessionAgent.createAccount({\n        email: 'did1@test.com',\n        handle: 'did1.test',\n        password: 'password',\n      })\n      await expect(getIndexedHandle(did)).rejects.toThrow('Profile not found')\n      await network.bsky.sub.indexingSvc.indexHandle(did, now)\n      await expect(getIndexedHandle(did)).resolves.toEqual('did1.test')\n    })\n\n    it('reindexes handle for existing did when forced', async () => {\n      const now = new Date().toISOString()\n      const sessionAgent = network.pds.getClient()\n      const {\n        data: { did },\n      } = await sessionAgent.createAccount({\n        email: 'did2@test.com',\n        handle: 'did2.test',\n        password: 'password',\n      })\n      await network.bsky.sub.indexingSvc.indexHandle(did, now)\n      await expect(getIndexedHandle(did)).resolves.toEqual('did2.test')\n      await sessionAgent.com.atproto.identity.updateHandle({\n        handle: 'did2-updated.test',\n      })\n      await network.bsky.sub.indexingSvc.indexHandle(did, now)\n      await expect(getIndexedHandle(did)).resolves.toEqual('did2.test') // Didn't update, not forced\n      await network.bsky.sub.indexingSvc.indexHandle(did, now, true)\n      await expect(getIndexedHandle(did)).resolves.toEqual('did2-updated.test')\n    })\n\n    it('handles profile aggregations out of order', async () => {\n      const now = new Date().toISOString()\n      const agent = network.pds.getClient()\n      await agent.createAccount({\n        email: 'did3@test.com',\n        handle: 'did3.test',\n        password: 'password',\n      })\n      const did = agent.accountDid\n      const follow = await prepareCreate({\n        did: sc.dids.bob,\n        collection: ids.AppBskyGraphFollow,\n        record: {\n          $type: ids.AppBskyGraphFollow,\n          subject: did,\n          createdAt: now,\n        } as AppBskyGraphFollow.Record,\n      })\n      await network.bsky.sub.indexingSvc.indexRecord(...follow)\n      await network.bsky.sub.indexingSvc.indexHandle(did, now)\n      await network.bsky.sub.background.processAll()\n      const agg = await db.db\n        .selectFrom('profile_agg')\n        .select(['did', 'followersCount'])\n        .where('did', '=', did)\n        .executeTakeFirst()\n      expect(agg).toEqual({\n        did,\n        followersCount: 1,\n      })\n    })\n  })\n\n  describe('deleteActor', () => {\n    it('does not unindex actor when they are still being hosted by their pds', async () => {\n      const { data: profileBefore } = await agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      // Attempt indexing tombstone\n      await network.bsky.sub.indexingSvc.deleteActor(sc.dids.alice)\n      const { data: profileAfter } = await agent.api.app.bsky.actor.getProfile(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      expect(profileAfter).toEqual(profileBefore)\n    })\n\n    it('unindexes actor when they are no longer hosted by their pds', async () => {\n      const { alice } = sc.dids\n      const getProfileBefore = agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      await expect(getProfileBefore).resolves.toBeDefined()\n      // Delete account on pds\n      const token = await network.pds.ctx.accountManager.createEmailToken(\n        alice,\n        'delete_account',\n      )\n      await pdsAgent.api.com.atproto.server.deleteAccount({\n        token,\n        did: alice,\n        password: sc.accounts[alice].password,\n      })\n      await network.pds.ctx.backgroundQueue.processAll()\n      // Index tombstone\n      await network.bsky.sub.indexingSvc.deleteActor(alice)\n      const getProfileAfter = agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      await expect(getProfileAfter).rejects.toThrow('Profile not found')\n    })\n  })\n\n  async function getNotifications(db: Database, uri: AtUri) {\n    return await db.db\n      .selectFrom('notification')\n      .selectAll()\n      .select(sql`0`.as('id')) // Ignore notification ids in comparisons\n      .where('recordUri', '=', uri.toString())\n      .orderBy('sortAt')\n      .execute()\n  }\n})\n\nasync function prepareCreate(opts: {\n  did: string\n  collection: string\n  rkey?: string\n  record: unknown\n  timestamp?: string\n}): Promise<[AtUri, CID, unknown, WriteOpAction.Create, string]> {\n  const rkey = opts.rkey ?? TID.nextStr()\n  return [\n    AtUri.make(opts.did, opts.collection, rkey),\n    await cidForCbor(opts.record),\n    opts.record,\n    WriteOpAction.Create,\n    opts.timestamp ?? new Date().toISOString(),\n  ]\n}\n\nasync function prepareUpdate(opts: {\n  did: string\n  collection: string\n  rkey: string\n  record: unknown\n  timestamp?: string\n}): Promise<[AtUri, CID, unknown, WriteOpAction.Update, string]> {\n  return [\n    AtUri.make(opts.did, opts.collection, opts.rkey),\n    await cidForCbor(opts.record),\n    opts.record,\n    WriteOpAction.Update,\n    opts.timestamp ?? new Date().toISOString(),\n  ]\n}\n\nfunction prepareDelete(opts: {\n  did: string\n  collection: string\n  rkey: string\n}): [AtUri] {\n  return [AtUri.make(opts.did, opts.collection, opts.rkey)]\n}\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/subscription.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { cborDecode, cborEncode } from '@atproto/common'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { CommitDataWithOps, sequencer } from '@atproto/pds'\nimport { DatabaseSchemaType } from '../../src/data-plane/server/db/database-schema'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { forSnapshot } from '../_util'\n\ntype Database = TestNetwork['bsky']['db']\n\ndescribe('sync', () => {\n  let network: TestNetwork\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_subscription_repo',\n    })\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('indexes permit history being replayed.', async () => {\n    const { db } = network.bsky\n\n    // Generate some modifications and dupes\n    const { alice, bob, carol, dan } = sc.dids\n    await sc.follow(alice, bob)\n    await sc.follow(carol, alice)\n    await sc.follow(bob, alice)\n    await sc.follow(dan, bob)\n    await sc.like(dan, sc.posts[alice][1].ref) // Identical\n    await sc.like(alice, sc.posts[carol][0].ref) // Identical\n    await updateProfile(pdsAgent, alice, { displayName: 'ali!' })\n    await updateProfile(pdsAgent, bob, { displayName: 'robert!' })\n\n    await network.processAll()\n\n    // Table comparator\n    const getTableDump = async () => {\n      const [actor, post, profile, like, follow, dupes] = await Promise.all([\n        dumpTable(db, 'actor', ['did']),\n        dumpTable(db, 'post', ['uri']),\n        dumpTable(db, 'profile', ['uri']),\n        dumpTable(db, 'like', ['creator', 'subject']),\n        dumpTable(db, 'follow', ['creator', 'subjectDid']),\n        dumpTable(db, 'duplicate_record', ['uri']),\n      ])\n      return { actor, post, profile, like, follow, dupes }\n    }\n\n    // Mark originals\n    const originalTableDump = await getTableDump()\n\n    // Reprocess repos via sync subscription, on top of existing indices\n    await network.bsky.sub.restart()\n    await network.processAll()\n\n    // Permissive of indexedAt times changing\n    expect(forSnapshot(await getTableDump())).toEqual(\n      forSnapshot(originalTableDump),\n    )\n  })\n\n  it('indexes actor when commit is unprocessable.', async () => {\n    // mock sequencing to create an unprocessable commit event\n    const sequenceCommitOrig = network.pds.ctx.sequencer.sequenceCommit\n    network.pds.ctx.sequencer.sequenceCommit = async function (\n      did: string,\n      commitData: CommitDataWithOps,\n    ) {\n      const seqEvt = await sequencer.formatSeqCommit(did, commitData)\n      const evt = cborDecode(seqEvt.event) as sequencer.CommitEvt\n      evt.blocks = new Uint8Array() // bad blocks\n      seqEvt.event = cborEncode(evt)\n      return await network.pds.ctx.sequencer.sequenceEvt(seqEvt)\n    }\n    // create account and index the initial commit event\n    await sc.createAccount('jack', {\n      handle: 'jack.test',\n      email: 'jack@test.com',\n      password: 'password',\n    })\n    await network.processAll()\n    // confirm jack was indexed as an actor despite the bad event\n    const actors = await dumpTable(network.bsky.db, 'actor', ['did'])\n    expect(actors.map((a) => a.handle)).toContain('jack.test')\n    network.pds.ctx.sequencer.sequenceCommit = sequenceCommitOrig\n  })\n\n  async function updateProfile(\n    agent: AtpAgent,\n    did: string,\n    record: Record<string, unknown>,\n  ) {\n    return await agent.api.com.atproto.repo.putRecord(\n      {\n        repo: did,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        record,\n      },\n      { headers: sc.getHeaders(did), encoding: 'application/json' },\n    )\n  }\n})\n\nasync function dumpTable<T extends keyof DatabaseSchemaType>(\n  db: Database,\n  tableName: T,\n  pkeys: (keyof DatabaseSchemaType[T] & string)[],\n) {\n  const { ref } = db.db.dynamic\n  let builder = db.db.selectFrom(tableName).selectAll()\n  pkeys.forEach((key) => {\n    builder = builder.orderBy(ref(key))\n  })\n  return await builder.execute()\n}\n"
  },
  {
    "path": "packages/bsky/tests/data-plane/thread-mutes.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('thread mutes', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let bob: string\n\n  let rootPost: RecordRef\n  let replyPost: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_thread_mutes',\n    })\n    sc = network.getSeedClient()\n    agent = network.bsky.getClient()\n    await usersSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    rootPost = (await sc.post(alice, 'root post')).ref\n    replyPost = (await sc.reply(alice, rootPost, rootPost, 'first reply')).ref\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('mutes threads', async () => {\n    await agent.api.app.bsky.graph.muteThread(\n      { root: rootPost.uriStr },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphMuteThread,\n        ),\n      },\n    )\n  })\n\n  it('notes that threads are muted in viewer state', async () => {\n    const res = await agent.api.app.bsky.feed.getPosts(\n      {\n        uris: [rootPost.uriStr, replyPost.uriStr],\n      },\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts),\n      },\n    )\n    expect(res.data.posts[0].viewer?.threadMuted).toBe(true)\n    expect(res.data.posts[1].viewer?.threadMuted).toBe(true)\n  })\n\n  it('prevents notifs from replies', async () => {\n    await sc.reply(bob, rootPost, rootPost, 'reply')\n    await sc.reply(bob, rootPost, replyPost, 'reply')\n    await network.processAll()\n\n    const notifsRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(notifsRes.data.notifications.length).toBe(0)\n  })\n\n  it('prevents notifs from quote posts', async () => {\n    await sc.post(bob, 'quote', undefined, undefined, rootPost)\n    await sc.post(bob, 'quote', undefined, undefined, replyPost)\n    await network.processAll()\n\n    const notifsRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(notifsRes.data.notifications.length).toBe(0)\n  })\n\n  it('prevents notifs from likes', async () => {\n    await sc.like(bob, rootPost)\n    await sc.like(bob, replyPost)\n    await network.processAll()\n\n    const notifsRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(notifsRes.data.notifications.length).toBe(0)\n  })\n\n  it('prevents notifs from reposts', async () => {\n    await sc.repost(bob, rootPost)\n    await sc.repost(bob, replyPost)\n    await network.processAll()\n\n    const notifsRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(notifsRes.data.notifications.length).toBe(0)\n  })\n\n  it('unmutes threads', async () => {\n    await agent.api.app.bsky.graph.unmuteThread(\n      { root: rootPost.uriStr },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphUnmuteThread,\n        ),\n      },\n    )\n  })\n\n  it('no longer notes that threads are muted in viewer state after unmuting', async () => {\n    const res = await agent.api.app.bsky.feed.getPosts(\n      {\n        uris: [rootPost.uriStr, replyPost.uriStr],\n      },\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts),\n      },\n    )\n    expect(res.data.posts[0].viewer?.threadMuted).toBe(false)\n    expect(res.data.posts[1].viewer?.threadMuted).toBe(false)\n  })\n\n  it('sends notifications after unmuting', async () => {\n    await sc.reply(bob, rootPost, rootPost, 'new reply')\n    await sc.reply(bob, rootPost, replyPost, 'new reply')\n    await sc.like(bob, rootPost)\n    await sc.repost(bob, replyPost)\n    await network.processAll()\n\n    const notifsRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(notifsRes.data.notifications.length).toBe(4)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/entryway-auth.test.ts",
    "content": "import assert from 'node:assert'\nimport * as nodeCrypto from 'node:crypto'\nimport * as jose from 'jose'\nimport KeyEncoder from 'key-encoder'\nimport * as ui8 from 'uint8arrays'\nimport { AtUri, AtpAgent } from '@atproto/api'\nimport { MINUTE } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\n\nconst keyEncoder = new KeyEncoder('secp256k1')\n\nconst derivePrivKey = async (\n  keypair: crypto.ExportableKeypair,\n): Promise<nodeCrypto.KeyObject> => {\n  const privKeyRaw = await keypair.export()\n  const privKeyEncoded = keyEncoder.encodePrivate(\n    ui8.toString(privKeyRaw, 'hex'),\n    'raw',\n    'pem',\n  )\n  return nodeCrypto.createPrivateKey(privKeyEncoded)\n}\n\n// @NOTE temporary measure, see note on entrywaySession in bsky/src/auth-verifier.ts\ndescribe('entryway auth', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let alice: string\n  let jwtPrivKey: nodeCrypto.KeyObject\n\n  beforeAll(async () => {\n    const keypair = await crypto.Secp256k1Keypair.create({ exportable: true })\n    jwtPrivKey = await derivePrivKey(keypair)\n    const entrywayJwtPublicKeyHex = ui8.toString(\n      keypair.publicKeyBytes(),\n      'hex',\n    )\n\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_entryway_auth',\n      bsky: {\n        entrywayJwtPublicKeyHex,\n      },\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('works', async () => {\n    const signer = new jose.SignJWT({ scope: 'com.atproto.access' })\n      .setSubject(alice)\n      .setIssuedAt()\n      .setExpirationTime('60mins')\n      .setAudience('did:web:fake.server.bsky.network')\n      .setProtectedHeader({\n        typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n        alg: 'ES256K',\n      })\n    const token = await signer.sign(jwtPrivKey)\n    const res = await agent.app.bsky.actor.getProfile(\n      { actor: sc.dids.bob },\n      { headers: { authorization: `Bearer ${token}` } },\n    )\n    expect(res.data.did).toEqual(sc.dids.bob)\n    // ensure this request is personalized for alice\n    const followingUri = res.data.viewer?.following\n    assert(followingUri)\n    const parsed = new AtUri(followingUri)\n    expect(parsed.hostname).toEqual(alice)\n  })\n\n  it('does not work on bad scopes', async () => {\n    const signer = new jose.SignJWT({ scope: 'com.atproto.refresh' })\n      .setSubject(alice)\n      .setIssuedAt()\n      .setExpirationTime('60mins')\n      .setAudience('did:web:fake.server.bsky.network')\n      .setProtectedHeader({\n        typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n        alg: 'ES256K',\n      })\n    const token = await signer.sign(jwtPrivKey)\n    const attempt = agent.app.bsky.actor.getProfile(\n      { actor: sc.dids.bob },\n      { headers: { authorization: `Bearer ${token}` } },\n    )\n    await expect(attempt).rejects.toThrow('Bad token scope')\n  })\n\n  it('does not work on expired tokens', async () => {\n    const time = Math.floor((Date.now() - 5 * MINUTE) / 1000)\n    const signer = new jose.SignJWT({ scope: 'com.atproto.access' })\n      .setSubject(alice)\n      .setIssuedAt()\n      .setExpirationTime(time)\n      .setAudience('did:web:fake.server.bsky.network')\n      .setProtectedHeader({\n        typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n        alg: 'ES256K',\n      })\n    const token = await signer.sign(jwtPrivKey)\n    const attempt = agent.app.bsky.actor.getProfile(\n      { actor: sc.dids.bob },\n      { headers: { authorization: `Bearer ${token}` } },\n    )\n    await expect(attempt).rejects.toThrow('Token has expired')\n  })\n\n  it('does not work on bad auds', async () => {\n    const signer = new jose.SignJWT({ scope: 'com.atproto.access' })\n      .setSubject(alice)\n      .setIssuedAt()\n      .setExpirationTime('60mins')\n      .setAudience('did:web:my.personal.pds.com')\n      .setProtectedHeader({\n        typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n        alg: 'ES256K',\n      })\n    const token = await signer.sign(jwtPrivKey)\n    const attempt = agent.app.bsky.actor.getProfile(\n      { actor: sc.dids.bob },\n      { headers: { authorization: `Bearer ${token}` } },\n    )\n    await expect(attempt).rejects.toThrow('Bad token aud')\n  })\n\n  it('does not work with bad signatures', async () => {\n    const fakeKey = await crypto.Secp256k1Keypair.create({ exportable: true })\n    const fakeJwtKey = await derivePrivKey(fakeKey)\n    const signer = new jose.SignJWT({ scope: 'com.atproto.access' })\n      .setSubject(alice)\n      .setIssuedAt()\n      .setExpirationTime('60mins')\n      .setAudience('did:web:my.personal.pds.com')\n      .setProtectedHeader({\n        typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n        alg: 'ES256K',\n      })\n    const token = await signer.sign(fakeJwtKey)\n    const attempt = agent.app.bsky.actor.getProfile(\n      { actor: sc.dids.bob },\n      { headers: { authorization: `Bearer ${token}` } },\n    )\n    await expect(attempt).rejects.toThrow('Token could not be verified')\n  })\n\n  it('does not work on flexible aud routes', async () => {\n    const signer = new jose.SignJWT({ scope: 'com.atproto.access' })\n      .setSubject(alice)\n      .setIssuedAt()\n      .setExpirationTime('60mins')\n      .setAudience('did:web:fake.server.bsky.network')\n      .setProtectedHeader({\n        typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n        alg: 'ES256K',\n      })\n    const token = await signer.sign(jwtPrivKey)\n    const feedUri = AtUri.make(alice, 'app.bsky.feed.generator', 'fake-feed')\n    const attempt = agent.app.bsky.feed.getFeed(\n      { feed: feedUri.toString() },\n      { headers: { authorization: `Bearer ${token}` } },\n    )\n    await expect(attempt).rejects.toThrow('Malformed token')\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/etcd.test.ts",
    "content": "import EventEmitter from 'node:events'\nimport { Etcd3, IKeyValue } from 'etcd3'\nimport { EtcdHostList } from '../src'\nimport { EtcdMap } from '../src/etcd'\n\ndescribe('etcd', () => {\n  describe('EtcdMap', () => {\n    it('initializes values based on current keys', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: '1' })\n      etcd.watcher.set('service/b', { value: '2' })\n      etcd.watcher.set('service/c', { value: '3' })\n      const map = new EtcdMap(etcd as unknown as Etcd3)\n      await map.connect()\n      expect(map.get('service/a')).toBe('1')\n      expect(map.get('service/b')).toBe('2')\n      expect(map.get('service/c')).toBe('3')\n      expect([...map.values()]).toEqual(['1', '2', '3'])\n    })\n\n    it('maintains key updates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: '1' })\n      etcd.watcher.set('service/b', { value: '2' })\n      etcd.watcher.set('service/c', { value: '3' })\n      const map = new EtcdMap(etcd as unknown as Etcd3)\n      await map.connect()\n      etcd.watcher.set('service/b', { value: '4' })\n      expect(map.get('service/a')).toBe('1')\n      expect(map.get('service/b')).toBe('4')\n      expect(map.get('service/c')).toBe('3')\n      expect([...map.values()]).toEqual(['1', '4', '3'])\n    })\n\n    it('maintains key creates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: '1' })\n      const map = new EtcdMap(etcd as unknown as Etcd3)\n      await map.connect()\n      etcd.watcher.set('service/b', { value: '2' })\n      expect(map.get('service/a')).toBe('1')\n      expect(map.get('service/b')).toBe('2')\n      expect([...map.values()]).toEqual(['1', '2'])\n    })\n\n    it('maintains key deletions', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: '1' })\n      etcd.watcher.set('service/b', { value: '2' })\n      const map = new EtcdMap(etcd as unknown as Etcd3)\n      await map.connect()\n      etcd.watcher.del('service/b')\n      expect(map.get('service/a')).toBe('1')\n      expect(map.get('service/b')).toBe(null)\n      expect([...map.values()]).toEqual(['1'])\n    })\n\n    it('notifies of updates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: '1' })\n      etcd.watcher.set('service/b', { value: '2' })\n      const map = new EtcdMap(etcd as unknown as Etcd3)\n      await map.connect()\n      const states: string[][] = [[...map.values()]]\n      map.onUpdate((update) => {\n        states.push([...update.values()])\n      })\n      etcd.watcher.set('service/c', { value: '3' })\n      etcd.watcher.del('service/b')\n      etcd.watcher.set('service/a', { value: '4' })\n      expect(states).toEqual([\n        ['1', '2'],\n        ['1', '2', '3'],\n        ['1', '3'],\n        ['4', '3'],\n      ])\n    })\n\n    it('ignores out-of-order updates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: '1' })\n      const map = new EtcdMap(etcd as unknown as Etcd3)\n      await map.connect()\n      const states: string[][] = [[...map.values()]]\n      map.onUpdate((update) => {\n        states.push([...update.values()])\n      })\n      etcd.watcher.set('service/a', { value: '2' })\n      etcd.watcher.set('service/a', { value: '3', overrideRev: 1 }) // old rev\n      etcd.watcher.set('service/a', { value: '4' })\n      expect(map.get('service/a')).toBe('4')\n      expect(states).toEqual([['1'], ['2'], ['4']]) // never witnessed 3\n    })\n  })\n\n  describe('EtcdHostList', () => {\n    it('initializes values based on current keys', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')\n      await hostList.connect()\n      expect([...hostList.get()]).toEqual([\n        'http://192.168.1.1',\n        'http://192.168.1.2',\n        'http://192.168.1.3',\n      ])\n    })\n\n    it('maintains key updates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')\n      await hostList.connect()\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.4' })\n      expect([...hostList.get()]).toEqual([\n        'http://192.168.1.1',\n        'http://192.168.1.4',\n        'http://192.168.1.3',\n      ])\n    })\n\n    it('maintains key creates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')\n      await hostList.connect()\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      expect([...hostList.get()]).toEqual([\n        'http://192.168.1.1',\n        'http://192.168.1.2',\n      ])\n    })\n\n    it('maintains key deletions', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')\n      await hostList.connect()\n      etcd.watcher.del('service/b')\n      expect([...hostList.get()]).toEqual(['http://192.168.1.1'])\n    })\n\n    it('notifies of updates', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')\n      await hostList.connect()\n      const states: string[][] = [[...hostList.get()]]\n      hostList.onUpdate((updated) => {\n        expect([...updated]).toEqual([...hostList.get()])\n        states.push([...updated])\n      })\n      etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })\n      etcd.watcher.del('service/b')\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.4' })\n      expect(states).toEqual([\n        ['http://192.168.1.1', 'http://192.168.1.2'],\n        ['http://192.168.1.1', 'http://192.168.1.2', 'http://192.168.1.3'],\n        ['http://192.168.1.1', 'http://192.168.1.3'],\n        ['http://192.168.1.4', 'http://192.168.1.3'],\n      ])\n    })\n\n    it('ignores bad host values', async () => {\n      const etcd = new MockEtcd()\n      etcd.watcher.set('service/a', { value: 'not-a-host' })\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')\n      await hostList.connect()\n      expect([...hostList.get()]).toEqual(['http://192.168.1.2'])\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      etcd.watcher.set('service/c', { value: 'not-a-host' })\n      expect([...hostList.get()]).toEqual([\n        'http://192.168.1.1',\n        'http://192.168.1.2',\n      ])\n      etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })\n      expect([...hostList.get()]).toEqual([\n        'http://192.168.1.1',\n        'http://192.168.1.2',\n        'http://192.168.1.3',\n      ])\n    })\n\n    it('falls back to static host list when uninitialized or no keys available', async () => {\n      const etcd = new MockEtcd()\n      const hostList = new EtcdHostList(etcd as unknown as Etcd3, '', [\n        'http://10.0.0.1',\n        'http://10.0.0.2',\n      ])\n      etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })\n      expect([...hostList.get()]).toEqual([\n        'http://10.0.0.1',\n        'http://10.0.0.2',\n      ])\n      await hostList.connect()\n      const states: string[][] = [[...hostList.get()]]\n      hostList.onUpdate((updated) => {\n        states.push([...updated])\n      })\n      etcd.watcher.del('service/a')\n      etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })\n      expect(states).toEqual([\n        ['http://192.168.1.1'],\n        ['http://10.0.0.1', 'http://10.0.0.2'],\n        ['http://192.168.1.2'],\n      ])\n    })\n  })\n})\n\nclass MockEtcd {\n  public watcher = new MockWatcher()\n  watch() {\n    const watcher = this.watcher\n    return {\n      prefix() {\n        return {\n          watcher() {\n            return watcher\n          },\n        }\n      },\n    }\n  }\n  getAll() {\n    const watcher = this.watcher\n    return {\n      prefix() {\n        return {\n          async exec(): Promise<{ kvs: IKeyValue[] }> {\n            return { kvs: watcher.getAll() }\n          },\n        }\n      },\n    }\n  }\n}\n\nclass MockWatcher extends EventEmitter {\n  rev = 1\n  kvs: IKeyValue[] = []\n  constructor() {\n    super()\n    process.nextTick(() => this.emit('connected', {}))\n  }\n  get(key: string): IKeyValue | null {\n    const found = this.kvs.find((kv) => kv.key.toString() === key)\n    return found ?? null\n  }\n  getAll(): IKeyValue[] {\n    return [...this.kvs]\n  }\n  set(\n    key: string,\n    { value, overrideRev }: { value: string; overrideRev?: number },\n  ) {\n    const found = this.kvs.find((kv) => kv.key.toString() === key)\n    const rev = overrideRev ?? ++this.rev\n    if (found) {\n      found.value = Buffer.from(value)\n      found.mod_revision = rev.toString()\n      found.version = (parseInt(found.version, 10) + 1).toString()\n      this.emit('put', found)\n    } else {\n      const created = {\n        key: Buffer.from(key),\n        value: Buffer.from(value),\n        create_revision: rev.toString(),\n        mod_revision: rev.toString(),\n        version: '1',\n        lease: '0',\n      }\n      this.kvs.push(created)\n      this.emit('put', created)\n    }\n  }\n  del(key: string) {\n    const foundIdx = this.kvs.findIndex((kv) => kv.key.toString() === key)\n    if (foundIdx === -1) return\n    const [deleted] = this.kvs.splice(foundIdx, 1)\n    const rev = ++this.rev\n    deleted.value = Buffer.from('')\n    deleted.mod_revision = rev.toString()\n    deleted.create_revision = '0'\n    deleted.version = '0'\n    this.emit('delete', deleted)\n  }\n  on(evt: 'connected', listener: (res: unknown) => void): any\n  on(evt: 'put', listener: (kv: IKeyValue) => void): any\n  on(evt: 'delete', listener: (kv: IKeyValue) => void): any\n  on(evt: string, listener: (...args: any[]) => void) {\n    super.on(evt, listener)\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/feed-generation.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtUri, AtpAgent } from '@atproto/api'\nimport { TID } from '@atproto/common'\nimport {\n  RecordRef,\n  SeedClient,\n  TestFeedGen,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { XRPCError } from '@atproto/xrpc'\nimport { AuthRequiredError, MethodHandler } from '@atproto/xrpc-server'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  FeedViewPost,\n  SkeletonFeedPost,\n} from '../src/lexicon/types/app/bsky/feed/defs'\nimport { OutputSchema as GetActorFeedsOutputSchema } from '../src/lexicon/types/app/bsky/feed/getActorFeeds'\nimport { OutputSchema as GetFeedOutputSchema } from '../src/lexicon/types/app/bsky/feed/getFeed'\nimport * as AppBskyFeedGetFeedSkeleton from '../src/lexicon/types/app/bsky/feed/getFeedSkeleton'\nimport { forSnapshot, paginateAll } from './_util'\n\ndescribe('feed generation', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let gen: TestFeedGen\n\n  let alice: string\n  let feedUriAll: string\n  let feedUriAllRef: RecordRef\n  let feedUriEven: string\n  let feedUriOdd: string // Unsupported by feed gen\n  let feedUriBadPaginationLimit: string\n  let feedUriBadPaginationCursor: string\n  let feedUriPrime: string // Taken-down\n  let feedUriPrimeRef: RecordRef\n  let feedUriNeedsAuth: string\n  let feedUriContentModeVideo: string\n  let feedUriAcceptsInteractions: string\n  let starterPackRef: { uri: string; cid: string }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_feed_generation',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    const allUri = AtUri.make(alice, 'app.bsky.feed.generator', 'all')\n    const feedUriBadPaginationLimit = AtUri.make(\n      alice,\n      'app.bsky.feed.generator',\n      'bad-pagination-limit',\n    )\n    const feedUriBadPaginationCursor = AtUri.make(\n      alice,\n      'app.bsky.feed.generator',\n      'bad-pagination-cursor',\n    )\n    const evenUri = AtUri.make(alice, 'app.bsky.feed.generator', 'even')\n    const primeUri = AtUri.make(alice, 'app.bsky.feed.generator', 'prime')\n    const needsAuthUri = AtUri.make(\n      alice,\n      'app.bsky.feed.generator',\n      'needs-auth',\n    )\n    const acceptsInteractionsUri = AtUri.make(\n      alice,\n      'app.bsky.feed.generator',\n      'accepts-interactions',\n    )\n    gen = await network.createFeedGen({\n      [allUri.toString()]: feedGenHandler('all'),\n      [evenUri.toString()]: feedGenHandler('even'),\n      [feedUriBadPaginationLimit.toString()]: feedGenHandler(\n        'bad-pagination-limit',\n      ),\n      [feedUriBadPaginationCursor.toString()]: feedGenHandler(\n        'bad-pagination-cursor',\n      ),\n      [primeUri.toString()]: feedGenHandler('prime'),\n      [needsAuthUri.toString()]: feedGenHandler('needs-auth'),\n      [acceptsInteractionsUri.toString()]: feedGenHandler(\n        'accepts-interactions',\n      ),\n    })\n\n    const feedSuggestions = [\n      { uri: allUri.toString(), order: 1 },\n      { uri: evenUri.toString(), order: 2 },\n      { uri: feedUriBadPaginationLimit.toString(), order: 3 },\n      { uri: primeUri.toString(), order: 4 },\n    ]\n    await network.bsky.db.db\n      .insertInto('suggested_feed')\n      .values(feedSuggestions)\n      .execute()\n  })\n\n  afterAll(async () => {\n    await gen.close()\n    await network.close()\n  })\n\n  it('feed gen records can be created.', async () => {\n    const all = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'all' },\n      {\n        did: gen.did,\n        displayName: 'All',\n        description: 'Provides all feed candidates',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    const even = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'even' },\n      {\n        did: gen.did,\n        displayName: 'Even',\n        description: 'Provides even-indexed feed candidates',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    // Unsupported by feed gen\n    const odd = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'odd' },\n      {\n        did: gen.did,\n        displayName: 'Temp', // updated in next test\n        description: 'Temp', // updated in next test\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n\n    const badPaginationLimit =\n      await pdsAgent.api.app.bsky.feed.generator.create(\n        { repo: alice, rkey: 'bad-pagination-limit' },\n        {\n          did: gen.did,\n          displayName: 'Bad Pagination Limit',\n          description:\n            'Provides all feed candidates, blindly ignoring pagination limit',\n          createdAt: new Date().toISOString(),\n        },\n        sc.getHeaders(alice),\n      )\n    const badPaginationCursor =\n      await pdsAgent.api.app.bsky.feed.generator.create(\n        { repo: alice, rkey: 'bad-pagination-cursor' },\n        {\n          did: gen.did,\n          displayName: 'Bad Pagination Cursor',\n          description: 'Echoes back the same cursor it received',\n          createdAt: new Date().toISOString(),\n        },\n        sc.getHeaders(alice),\n      )\n\n    // Taken-down\n    const prime = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'prime' },\n      {\n        did: gen.did,\n        displayName: 'Prime',\n        description: 'Provides prime-indexed feed candidates',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    const needsAuth = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'needs-auth' },\n      {\n        did: gen.did,\n        displayName: 'Needs Auth',\n        description: 'Provides all feed candidates when authed',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    const contentModeVideo = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'content-mode-video' },\n      {\n        did: gen.did,\n        displayName: 'Content mode video',\n        description: 'Has a contentMode specified',\n        createdAt: new Date().toISOString(),\n        contentMode: 'app.bsky.feed.defs#contentModeVideo',\n      },\n      sc.getHeaders(alice),\n    )\n    const acceptsInteraction =\n      await pdsAgent.api.app.bsky.feed.generator.create(\n        { repo: alice, rkey: 'accepts-interactions' },\n        {\n          did: gen.did,\n          displayName: 'Accepts Interactions',\n          description: 'Has acceptsInteractions set to true',\n          acceptsInteractions: true,\n          createdAt: new Date().toISOString(),\n        },\n        sc.getHeaders(alice),\n      )\n    await network.processAll()\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: prime.uri,\n    })\n\n    feedUriAll = all.uri\n    feedUriAllRef = new RecordRef(all.uri, all.cid)\n    feedUriEven = even.uri\n    feedUriOdd = odd.uri\n    feedUriBadPaginationLimit = badPaginationLimit.uri\n    feedUriBadPaginationCursor = badPaginationCursor.uri\n    feedUriPrime = prime.uri\n    feedUriPrimeRef = new RecordRef(prime.uri, prime.cid)\n    feedUriNeedsAuth = needsAuth.uri\n    feedUriContentModeVideo = contentModeVideo.uri\n    feedUriAcceptsInteractions = acceptsInteraction.uri\n  })\n\n  it('feed gen records can be updated', async () => {\n    await pdsAgent.api.com.atproto.repo.putRecord(\n      {\n        repo: alice,\n        collection: ids.AppBskyFeedGenerator,\n        rkey: 'odd',\n        record: {\n          did: gen.did,\n          displayName: 'Odd',\n          description: 'Provides odd-indexed feed candidates',\n          createdAt: new Date().toISOString(),\n        },\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await network.processAll()\n  })\n\n  it('getActorFeeds fetches feed generators by actor.', async () => {\n    // add some likes\n    await sc.like(sc.dids.bob, feedUriAllRef)\n    await sc.like(sc.dids.carol, feedUriAllRef)\n    await network.processAll()\n\n    const results = (results: GetActorFeedsOutputSchema[]) =>\n      results.flatMap((res) => res.feeds)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.feed.getActorFeeds(\n        { actor: alice, cursor, limit: 2 },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetActorFeeds,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = results(await paginateAll(paginator))\n\n    expect(paginatedAll.length).toEqual(8)\n    expect(paginatedAll[0].uri).toEqual(feedUriOdd)\n    expect(paginatedAll[1].uri).toEqual(feedUriAcceptsInteractions)\n    expect(paginatedAll[2].uri).toEqual(feedUriContentModeVideo)\n    expect(paginatedAll[3].uri).toEqual(feedUriNeedsAuth)\n    expect(paginatedAll[4].uri).toEqual(feedUriBadPaginationCursor)\n    expect(paginatedAll[5].uri).toEqual(feedUriBadPaginationLimit)\n    expect(paginatedAll[6].uri).toEqual(feedUriEven)\n    expect(paginatedAll[7].uri).toEqual(feedUriAll)\n    expect(paginatedAll.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down\n    expect(forSnapshot(paginatedAll)).toMatchSnapshot()\n  })\n\n  it('embeds feed generator records in posts', async () => {\n    const res = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.bob },\n      {\n        text: 'cool feed!',\n        embed: {\n          $type: 'app.bsky.embed.record',\n          record: feedUriAllRef.raw,\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.bob),\n    )\n    await network.processAll()\n    const view = await agent.api.app.bsky.feed.getPosts(\n      { uris: [res.uri] },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n    expect(view.data.posts.length).toBe(1)\n    expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()\n  })\n\n  it('does not embed taken-down feed generator records in posts', async () => {\n    const res = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.bob },\n      {\n        text: 'weird feed',\n        embed: {\n          $type: 'app.bsky.embed.record',\n          record: feedUriPrimeRef.raw,\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.bob),\n    )\n    await network.processAll()\n    const view = await agent.api.app.bsky.feed.getPosts(\n      { uris: [res.uri] },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n    expect(view.data.posts.length).toBe(1)\n    expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()\n  })\n\n  it('embeds starter pack records in posts', async () => {\n    const listRes = await pdsAgent.api.app.bsky.graph.list.create(\n      {\n        repo: sc.dids.alice,\n      },\n      {\n        name: 'awesome starter pack!',\n        description: '',\n        descriptionFacets: [],\n        avatar: undefined,\n        createdAt: new Date().toISOString(),\n        purpose: 'app.bsky.graph.defs#referencelist',\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    const starterPackRes = await pdsAgent.api.app.bsky.graph.starterpack.create(\n      {\n        repo: sc.dids.alice,\n      },\n      {\n        name: 'awesome starter pack!',\n        description: '',\n        descriptionFacets: [],\n        feeds: [],\n        list: listRes.uri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    starterPackRef = {\n      uri: starterPackRes.uri,\n      cid: starterPackRes.cid,\n    }\n    const res = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.bob },\n      {\n        text: 'sick starter pack!',\n        embed: {\n          $type: 'app.bsky.embed.record',\n          record: starterPackRef,\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.bob),\n    )\n    await network.processAll()\n    const view = await agent.api.app.bsky.feed.getPosts(\n      { uris: [res.uri] },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n    expect(view.data.posts.length).toBe(1)\n    expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()\n  })\n\n  it('does not embed taken-down starter pack records in posts', async () => {\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: starterPackRef.uri,\n    })\n    const res = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.bob },\n      {\n        text: 'annoying starter pack',\n        embed: {\n          $type: 'app.bsky.embed.record',\n          record: starterPackRef,\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.bob),\n    )\n    await network.processAll()\n    const view = await agent.api.app.bsky.feed.getPosts(\n      { uris: [res.uri] },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n    expect(view.data.posts.length).toBe(1)\n    expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()\n  })\n\n  describe('getFeedGenerator', () => {\n    it('describes a feed gen & returns online status', async () => {\n      const resEven = await agent.api.app.bsky.feed.getFeedGenerator(\n        { feed: feedUriAll },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetFeedGenerator,\n          ),\n        },\n      )\n      expect(forSnapshot(resEven.data)).toMatchSnapshot()\n      expect(resEven.data.isOnline).toBe(true)\n      expect(resEven.data.isValid).toBe(true)\n    })\n\n    it('describes a feed gen & returns content mode', async () => {\n      const resEven = await agent.api.app.bsky.feed.getFeedGenerator(\n        { feed: feedUriContentModeVideo },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetFeedGenerator,\n          ),\n        },\n      )\n      expect(forSnapshot(resEven.data)).toMatchSnapshot()\n      expect(resEven.data.view.contentMode).toBe(\n        'app.bsky.feed.defs#contentModeVideo',\n      )\n    })\n\n    it('describes a feed gen & returns acceptsInteractions when true', async () => {\n      const resAcceptsInteractions =\n        await agent.api.app.bsky.feed.getFeedGenerator(\n          { feed: feedUriAcceptsInteractions },\n          {\n            headers: await network.serviceHeaders(\n              sc.dids.bob,\n              ids.AppBskyFeedGetFeedGenerator,\n            ),\n          },\n        )\n      expect(forSnapshot(resAcceptsInteractions.data)).toMatchSnapshot()\n      expect(resAcceptsInteractions.data.view.acceptsInteractions).toBe(true)\n    })\n\n    it('does not describe taken-down feed', async () => {\n      const tryGetFeed = agent.api.app.bsky.feed.getFeedGenerator(\n        { feed: feedUriPrime },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetFeedGenerator,\n          ),\n        },\n      )\n      await expect(tryGetFeed).rejects.toThrow('could not find feed')\n    })\n\n    // @TODO temporarily skipping while external feedgens catch-up on describeFeedGenerator\n    it.skip('handles an unsupported algo', async () => {\n      const resOdd = await agent.api.app.bsky.feed.getFeedGenerator(\n        { feed: feedUriOdd },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetFeedGenerator,\n          ),\n        },\n      )\n      expect(resOdd.data.isOnline).toBe(true)\n      expect(resOdd.data.isValid).toBe(false)\n    })\n\n    // @TODO temporarily skipping while external feedgens catch-up on describeFeedGenerator\n    it.skip('handles an offline feed', async () => {\n      // make an invalid feed gen in bob's repo\n      const allUriBob = AtUri.make(\n        sc.dids.bob,\n        'app.bsky.feed.generator',\n        'all',\n      )\n      const bobFg = await network.createFeedGen({\n        [allUriBob.toString()]: feedGenHandler('all'),\n      })\n\n      await pdsAgent.api.app.bsky.feed.generator.create(\n        { repo: sc.dids.bob, rkey: 'all' },\n        {\n          did: bobFg.did,\n          displayName: 'All by bob',\n          description: 'Provides all feed candidates - by bob',\n          createdAt: new Date().toISOString(),\n        },\n        sc.getHeaders(sc.dids.bob),\n      )\n      await network.processAll()\n\n      // now take it offline\n      await bobFg.close()\n\n      const res = await agent.api.app.bsky.feed.getFeedGenerator(\n        {\n          feed: allUriBob.toString(),\n        },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyFeedGetFeedGenerator,\n          ),\n        },\n      )\n      expect(res.data.isOnline).toBe(false)\n      expect(res.data.isValid).toBe(false)\n    })\n  })\n\n  describe('getFeedGenerators', () => {\n    it('describes multiple feed gens', async () => {\n      const resEven = await agent.api.app.bsky.feed.getFeedGenerators(\n        { feeds: [feedUriEven, feedUriAll, feedUriPrime] },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetFeedGenerators,\n          ),\n        },\n      )\n      expect(forSnapshot(resEven.data)).toMatchSnapshot()\n      expect(resEven.data.feeds.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down\n    })\n  })\n\n  describe('getSuggestedFeeds', () => {\n    it('returns list of suggested feed generators', async () => {\n      const resEven = await agent.api.app.bsky.feed.getSuggestedFeeds(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyFeedGetSuggestedFeeds,\n          ),\n        },\n      )\n      expect(forSnapshot(resEven.data)).toMatchSnapshot()\n      expect(resEven.data.feeds.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down\n    })\n  })\n\n  describe('getPopularFeedGenerators', () => {\n    it('gets popular feed generators', async () => {\n      const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyUnspeccedGetPopularFeedGenerators,\n          ),\n        },\n      )\n      expect(res.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down\n      expect(res.data.feeds.map((f) => f.uri)).toEqual([\n        feedUriAll,\n        feedUriEven,\n        feedUriBadPaginationLimit,\n      ])\n    })\n\n    it('searches feed generators', async () => {\n      const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators(\n        { query: 'all' },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyUnspeccedGetPopularFeedGenerators,\n          ),\n        },\n      )\n      expect(res.data.feeds.map((f) => f.uri)).toEqual([feedUriAll])\n    })\n\n    it('paginates', async () => {\n      const resFull =\n        await agent.api.app.bsky.unspecced.getPopularFeedGenerators(\n          {},\n          {\n            headers: await network.serviceHeaders(\n              sc.dids.bob,\n              ids.AppBskyUnspeccedGetPopularFeedGenerators,\n            ),\n          },\n        )\n      const resOne =\n        await agent.api.app.bsky.unspecced.getPopularFeedGenerators(\n          { limit: 2 },\n          {\n            headers: await network.serviceHeaders(\n              sc.dids.bob,\n              ids.AppBskyUnspeccedGetPopularFeedGenerators,\n            ),\n          },\n        )\n      const resTwo =\n        await agent.api.app.bsky.unspecced.getPopularFeedGenerators(\n          { cursor: resOne.data.cursor },\n          {\n            headers: await network.serviceHeaders(\n              sc.dids.bob,\n              ids.AppBskyUnspeccedGetPopularFeedGenerators,\n            ),\n          },\n        )\n      expect([...resOne.data.feeds, ...resTwo.data.feeds]).toEqual(\n        resFull.data.feeds,\n      )\n    })\n  })\n\n  describe('getFeed', () => {\n    it('resolves basic feed contents.', async () => {\n      const feed = await agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriEven },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeed,\n            gen.did,\n          ),\n        },\n      )\n      expect(feed.data.feed.map((item) => item.post.uri)).toEqual([\n        sc.posts[sc.dids.alice][0].ref.uriStr,\n        sc.posts[sc.dids.carol][0].ref.uriStr,\n        sc.replies[sc.dids.carol][0].ref.uriStr,\n      ])\n      expect(forSnapshot(feed.data.feed)).toMatchSnapshot()\n    })\n\n    it('resolves basic feed contents without auth.', async () => {\n      const feed = await agent.api.app.bsky.feed.getFeed({ feed: feedUriEven })\n      expect(feed.data.feed.map((item) => item.post.uri)).toEqual([\n        sc.posts[sc.dids.alice][0].ref.uriStr,\n        sc.posts[sc.dids.carol][0].ref.uriStr,\n        sc.replies[sc.dids.carol][0].ref.uriStr,\n      ])\n      expect(forSnapshot(feed.data.feed)).toMatchSnapshot()\n    })\n\n    it('paginates, handling replies and reposts.', async () => {\n      const results = (results: GetFeedOutputSchema[]) =>\n        results.flatMap((res) => res.feed)\n      const paginator = async (cursor?: string) => {\n        const res = await agent.api.app.bsky.feed.getFeed(\n          { feed: feedUriAll, cursor, limit: 2 },\n          {\n            headers: await network.serviceHeaders(\n              alice,\n              ids.AppBskyFeedGetFeed,\n              gen.did,\n            ),\n          },\n        )\n        return res.data\n      }\n\n      const paginatedAll: FeedViewPost[] = results(await paginateAll(paginator))\n\n      // Unknown post uri is omitted\n      expect(paginatedAll.map((item) => item.post.uri)).toEqual([\n        sc.posts[sc.dids.alice][0].ref.uriStr,\n        sc.posts[sc.dids.bob][0].ref.uriStr,\n        sc.posts[sc.dids.carol][0].ref.uriStr,\n        sc.replies[sc.dids.carol][0].ref.uriStr,\n        sc.posts[sc.dids.dan][1].ref.uriStr,\n      ])\n      expect(forSnapshot(paginatedAll)).toMatchSnapshot()\n    })\n\n    it('paginates, handling feed not respecting limit.', async () => {\n      const res = await agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriBadPaginationLimit, limit: 3 },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeed,\n            gen.did,\n          ),\n        },\n      )\n      // refused to respect pagination limit, so it got cut short by appview but the cursor remains.\n      expect(res.data.feed.length).toBeLessThanOrEqual(3)\n      expect(parseInt(res.data.cursor || '', 10)).toBeGreaterThanOrEqual(3)\n      expect(res.data.feed.map((item) => item.post.uri)).toEqual([\n        sc.posts[sc.dids.alice][0].ref.uriStr,\n        sc.posts[sc.dids.bob][0].ref.uriStr,\n        sc.posts[sc.dids.carol][0].ref.uriStr,\n      ])\n    })\n\n    it('fails on unknown feed.', async () => {\n      const tryGetFeed = agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriOdd },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeed,\n            gen.did,\n          ),\n        },\n      )\n      await expect(tryGetFeed).rejects.toMatchObject({\n        error: 'UnknownFeed',\n      })\n    })\n\n    it('returns empty cursor with feeds that echo back the same cursor from the param.', async () => {\n      const res = await agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriBadPaginationCursor, cursor: '1', limit: 2 },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeed,\n            gen.did,\n          ),\n        },\n      )\n\n      expect(res.data.cursor).toBeUndefined()\n      expect(res.data.feed).toHaveLength(2)\n    })\n\n    it('resolves contents of taken-down feed.', async () => {\n      const tryGetFeed = agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriPrime },\n        {\n          headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetFeed),\n        },\n      )\n      await expect(tryGetFeed).resolves.toBeDefined()\n    })\n\n    it('receives proper auth details.', async () => {\n      const feed = await agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriEven },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeedSkeleton,\n            gen.did,\n          ),\n        },\n      )\n      expect(feed.data['$auth']?.['aud']).toEqual(gen.did)\n      expect(feed.data['$auth']?.['iss']).toEqual(alice)\n      expect(feed.data['$auth']?.['lxm']).toEqual(\n        ids.AppBskyFeedGetFeedSkeleton,\n      )\n    })\n\n    it('passes through auth error from feed.', async () => {\n      const tryGetFeed = agent.api.app.bsky.feed.getFeed({\n        feed: feedUriNeedsAuth,\n      })\n      const err = await tryGetFeed.catch((err) => err)\n      assert(err instanceof XRPCError)\n      expect(err.status).toBe(401)\n      expect(err.message).toBe('This feed requires auth')\n    })\n\n    it('provides timing info in server-timing header.', async () => {\n      const result = await agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriEven },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeed,\n            gen.did,\n          ),\n        },\n      )\n      expect(result.headers['server-timing']).toMatch(\n        /^skele;dur=\\d+, hydr;dur=\\d+$/,\n      )\n    })\n\n    it('returns an upstream failure error when the feed is down.', async () => {\n      await gen.close() // @NOTE must be last test\n      const tryGetFeed = agent.api.app.bsky.feed.getFeed(\n        { feed: feedUriEven },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetFeed,\n            gen.did,\n          ),\n        },\n      )\n      await expect(tryGetFeed).rejects.toThrow('feed unavailable')\n    })\n  })\n\n  const feedGenHandler =\n    (\n      feedName:\n        | 'even'\n        | 'all'\n        | 'prime'\n        | 'bad-pagination-limit'\n        | 'bad-pagination-cursor'\n        | 'needs-auth'\n        | 'accepts-interactions',\n    ): MethodHandler<\n      void,\n      AppBskyFeedGetFeedSkeleton.QueryParams,\n      AppBskyFeedGetFeedSkeleton.HandlerInput,\n      AppBskyFeedGetFeedSkeleton.HandlerOutput\n    > =>\n    async ({ req, params }) => {\n      if (feedName === 'needs-auth' && !req.headers.authorization) {\n        throw new AuthRequiredError('This feed requires auth')\n      }\n      const { limit, cursor } = params\n      const candidates: SkeletonFeedPost[] = [\n        { post: sc.posts[sc.dids.alice][0].ref.uriStr },\n        { post: sc.posts[sc.dids.bob][0].ref.uriStr },\n        { post: sc.posts[sc.dids.carol][0].ref.uriStr },\n        { post: `at://did:plc:unknown/app.bsky.feed.post/${TID.nextStr()}` }, // Doesn't exist\n        { post: sc.replies[sc.dids.carol][0].ref.uriStr }, // Reply\n        // Repost (accurate)\n        {\n          post: sc.posts[sc.dids.dan][1].ref.uriStr,\n          reason: {\n            $type: 'app.bsky.feed.defs#skeletonReasonRepost',\n            repost: sc.reposts[sc.dids.carol][0].uriStr,\n          },\n        },\n        // Repost (inaccurate)\n        {\n          post: sc.posts[alice][1].ref.uriStr,\n          reason: {\n            $type: 'app.bsky.feed.defs#skeletonReasonRepost',\n            repost: sc.reposts[sc.dids.carol][0].uriStr,\n          },\n        },\n      ].map((item, i) => ({ ...item, feedContext: `item-${i}` })) // add a deterministic context to test passthrough\n      const offset = cursor ? parseInt(cursor, 10) : 0\n      const fullFeed = candidates.filter((_, i) => {\n        if (feedName === 'even') {\n          return i % 2 === 0\n        }\n        if (feedName === 'prime') {\n          return [2, 3, 5, 7, 11, 13].includes(i)\n        }\n        return true\n      })\n      const feedResults =\n        feedName === 'bad-pagination-limit'\n          ? fullFeed.slice(offset) // does not respect limit\n          : fullFeed.slice(offset, offset + limit)\n      const lastResult = feedResults.at(-1)\n      const cursorResult =\n        feedName === 'bad-pagination-cursor'\n          ? cursor\n          : lastResult\n            ? (fullFeed.indexOf(lastResult) + 1).toString()\n            : undefined\n\n      return {\n        encoding: 'application/json',\n        body: {\n          feed: feedResults,\n          cursor: cursorResult,\n          reqId: 'req-id-abc-def-ghi',\n          $auth: jwtBody(req.headers.authorization), // for testing purposes\n        },\n      }\n    }\n})\n\nconst jwtBody = (authHeader?: string): Record<string, unknown> | undefined => {\n  if (!authHeader?.startsWith('Bearer')) return undefined\n  const jwt = authHeader.replace('Bearer ', '')\n  const [, bodyb64] = jwt.split('.')\n  const body = JSON.parse(Buffer.from(bodyb64, 'base64').toString())\n  if (!body || typeof body !== 'object') return undefined\n  return body\n}\n"
  },
  {
    "path": "packages/bsky/tests/hydration/util.test.ts",
    "content": "import { Timestamp } from '@bufbuild/protobuf'\nimport {\n  HydrationMap,\n  mergeManyMaps,\n  mergeMaps,\n  mergeNestedMaps,\n  parseDate,\n} from '../../src/hydration/util'\n\nconst mapToObj = (map: HydrationMap<any>) => {\n  const obj: Record<string, any> = {}\n  for (const [key, value] of map) {\n    obj[key] = value\n  }\n  return obj\n}\n\ndescribe('hydration util', () => {\n  it(`mergeMaps: merges two maps`, () => {\n    const compare = new HydrationMap<string>()\n    compare.set('a', 'a')\n    compare.set('b', 'b')\n\n    const a = new HydrationMap<string>().set('a', 'a')\n    const b = new HydrationMap<string>().set('b', 'b')\n    const merged = mergeMaps(a, b)\n\n    expect(mapToObj(merged!)).toEqual(mapToObj(compare))\n  })\n\n  it(`mergeManyMaps: merges three maps`, () => {\n    const compare = new HydrationMap<string>()\n    compare.set('a', 'a')\n    compare.set('b', 'b')\n    compare.set('c', 'c')\n\n    const a = new HydrationMap<string>().set('a', 'a')\n    const b = new HydrationMap<string>().set('b', 'b')\n    const c = new HydrationMap<string>().set('c', 'c')\n    const merged = mergeManyMaps(a, b, c)\n\n    expect(mapToObj(merged!)).toEqual(mapToObj(compare))\n  })\n\n  it(`mergeNestedMaps: merges two nested maps`, () => {\n    const compare = new HydrationMap<HydrationMap<string>>()\n    const compareA = new HydrationMap<string>().set('a', 'a')\n    const compareB = new HydrationMap<string>().set('b', 'b')\n    compare.set('a', compareA)\n    compare.set('b', compareB)\n\n    const a = new HydrationMap<HydrationMap<string>>().set(\n      'a',\n      new HydrationMap<string>().set('a', 'a'),\n    )\n    const b = new HydrationMap<HydrationMap<string>>().set(\n      'b',\n      new HydrationMap<string>().set('b', 'b'),\n    )\n    const merged = mergeNestedMaps(a, b)\n\n    expect(mapToObj(merged!)).toEqual(mapToObj(compare))\n  })\n\n  it(`mergeNestedMaps: merges two nested maps with common keys`, () => {\n    const compare = new HydrationMap<HydrationMap<boolean>>()\n    const compareA = new HydrationMap<boolean>()\n    compareA.set('b', true)\n    compareA.set('c', true)\n    compare.set('a', compareA)\n\n    const a = new HydrationMap<HydrationMap<boolean>>().set(\n      'a',\n      new HydrationMap<boolean>().set('b', true),\n    )\n    const b = new HydrationMap<HydrationMap<boolean>>().set(\n      'a',\n      new HydrationMap<boolean>().set('c', true),\n    )\n    const merged = mergeNestedMaps(a, b)\n\n    expect(mapToObj(merged!)).toEqual(mapToObj(compare))\n  })\n\n  describe('parseDate', () => {\n    it('returns undefined for undefined input', () => {\n      expect(parseDate(undefined)).toBeUndefined()\n    })\n\n    it('returns undefined for Go zero-value date (year 0001)', () => {\n      // Go zero-value for time.Time is 0001-01-01 00:00:00 UTC\n      // which is -62135596800000ms from epoch\n      const goZeroDate = new Date(-62135596800000)\n      const goZeroTimestamp = Timestamp.fromDate(goZeroDate)\n      expect(parseDate(goZeroTimestamp)).toBeUndefined()\n    })\n\n    it('returns the date for valid dates', () => {\n      const validDate = new Date('2024-01-01T00:00:00Z')\n      const validTimestamp = Timestamp.fromDate(validDate)\n      expect(parseDate(validTimestamp)).toEqual(validDate)\n    })\n\n    it('returns the date for dates close to but not equal to Go zero-value', () => {\n      const nearZeroDate = new Date(-62135596800000 + 1000) // 1 second after\n      const nearZeroTimestamp = Timestamp.fromDate(nearZeroDate)\n      expect(parseDate(nearZeroTimestamp)).toEqual(nearZeroDate)\n    })\n\n    it('returns the date for epoch (1970-01-01)', () => {\n      const epochDate = new Date(0)\n      const epochTimestamp = Timestamp.fromDate(epochDate)\n      expect(parseDate(epochTimestamp)).toEqual(epochDate)\n    })\n\n    it('returns the date for recent dates', () => {\n      const recentDate = new Date('2026-03-17T00:00:00Z')\n      const recentTimestamp = Timestamp.fromDate(recentDate)\n      expect(parseDate(recentTimestamp)).toEqual(recentDate)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/image/server.test.ts",
    "content": "import { Readable } from 'node:stream'\nimport { CID } from 'multiformats/cid'\nimport { cidForCbor } from '@atproto/common'\nimport { TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { getInfo } from '../../src/image/sharp'\nimport { ImageUriBuilder } from '../../src/image/uri'\n\ndescribe('image processing server', () => {\n  let network: TestNetwork\n  let fileDid: string\n  let fileCid: CID\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_image_processing_server',\n    })\n    const sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    fileDid = sc.dids.carol\n    fileCid = sc.posts[fileDid][0].images[0].image.ref\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('processes image from blob resolver.', async () => {\n    const res = await fetch(\n      new URL(\n        `/img${ImageUriBuilder.getPath({\n          preset: 'feed_fullsize',\n          did: fileDid,\n          cid: fileCid.toString(),\n        })}`,\n        network.bsky.url,\n      ),\n    )\n\n    const bytes = new Uint8Array(await res.arrayBuffer())\n    const info = await getInfo(Readable.from([bytes]))\n\n    expect(info).toEqual({\n      height: 580,\n      width: 1000,\n      mime: 'image/webp',\n      size: 46594,\n    })\n    expect(Object.fromEntries(res.headers)).toEqual(\n      expect.objectContaining({\n        'content-type': 'image/webp',\n        'cache-control': 'public, max-age=31536000',\n        'content-length': '46594',\n      }),\n    )\n  })\n\n  it('caches results.', async () => {\n    const path = ImageUriBuilder.getPath({\n      preset: 'avatar',\n      did: fileDid,\n      cid: fileCid.toString(),\n    })\n    const url = new URL(`/img${path}`, network.bsky.url)\n\n    const res1 = await fetch(url)\n    expect(res1.headers.get('x-cache')).toEqual('miss')\n    const bytes1 = new Uint8Array(await res1.arrayBuffer())\n    const res2 = await fetch(url)\n    expect(res2.headers.get('x-cache')).toEqual('hit')\n    const bytes2 = new Uint8Array(await res2.arrayBuffer())\n    const res3 = await fetch(url)\n    expect(res3.headers.get('x-cache')).toEqual('hit')\n    const bytes3 = new Uint8Array(await res3.arrayBuffer())\n    expect(Buffer.compare(bytes1, bytes2)).toEqual(0)\n    expect(Buffer.compare(bytes1, bytes3)).toEqual(0)\n  })\n\n  it('errors on missing file.', async () => {\n    const missingCid = await cidForCbor('missing-file')\n\n    const path = ImageUriBuilder.getPath({\n      preset: 'feed_fullsize',\n      did: fileDid,\n      cid: missingCid.toString(),\n    })\n\n    const url = new URL(`/img${path}`, network.bsky.url)\n\n    const res = await fetch(url)\n    expect(res.status).toEqual(404)\n    await expect(res.json()).resolves.toMatchObject({\n      message: 'Blob not found',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/image/sharp.test.ts",
    "content": "import { createReadStream } from 'node:fs'\nimport { pipeline } from 'node:stream/promises'\nimport {\n  Options,\n  createImageProcessor,\n  createImageUpscaler,\n  getInfo,\n} from '../../src/image/sharp'\n\ndescribe('sharp image processor', () => {\n  it('scales up to cover.', async () => {\n    const result = await processFixture('key-landscape-small.jpg', {\n      format: 'jpeg',\n      fit: 'cover',\n      width: 500,\n      height: 500,\n      min: true,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 500,\n        width: 500,\n      }),\n    )\n  })\n\n  it('scales up to inside (landscape).', async () => {\n    const result = await processFixture('key-landscape-small.jpg', {\n      format: 'jpeg',\n      fit: 'inside',\n      width: 500,\n      height: 500,\n      min: true,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 290,\n        width: 500,\n      }),\n    )\n  })\n\n  it('scales up to inside (portrait).', async () => {\n    const result = await processFixture('key-portrait-small.jpg', {\n      format: 'jpeg',\n      fit: 'inside',\n      width: 500,\n      height: 500,\n      min: true,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 500,\n        width: 290,\n      }),\n    )\n  })\n\n  it('scales up to min.', async () => {\n    const result = await processFixture('key-landscape-small.jpg', {\n      format: 'jpeg',\n      width: 500,\n      height: 500,\n      min: { height: 200, width: 200 },\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 200,\n        width: 345,\n      }),\n    )\n  })\n\n  it('does not scale image up when min is false.', async () => {\n    const result = await processFixture('key-landscape-small.jpg', {\n      format: 'jpeg',\n      width: 500,\n      height: 500,\n      min: false,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 87,\n        width: 150,\n        mime: 'image/jpeg',\n      }),\n    )\n  })\n\n  it('scales down to cover.', async () => {\n    const result = await processFixture('key-landscape-large.jpg', {\n      format: 'jpeg',\n      fit: 'cover',\n      width: 500,\n      height: 500,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 500,\n        width: 500,\n      }),\n    )\n  })\n\n  it('scales down to inside (landscape).', async () => {\n    const result = await processFixture('key-landscape-large.jpg', {\n      format: 'jpeg',\n      fit: 'inside',\n      width: 500,\n      height: 500,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 290,\n        width: 500,\n      }),\n    )\n  })\n\n  it('scales down to inside (portrait).', async () => {\n    const result = await processFixture('key-portrait-large.jpg', {\n      format: 'jpeg',\n      fit: 'inside',\n      width: 500,\n      height: 500,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 500,\n        width: 290,\n      }),\n    )\n  })\n\n  it('converts jpeg to png.', async () => {\n    const result = await processFixture('key-landscape-small.jpg', {\n      format: 'webp',\n      width: 500,\n      height: 500,\n      min: false,\n    })\n    expect(result).toEqual(\n      expect.objectContaining({\n        height: 87,\n        width: 150,\n        size: expect.any(Number),\n        mime: 'image/webp',\n      }),\n    )\n  })\n\n  it('controls quality (jpeg).', async () => {\n    const high = await processFixture('key-portrait-small.jpg', {\n      format: 'jpeg',\n      width: 500,\n      height: 500,\n      quality: 90,\n    })\n    const low = await processFixture('key-portrait-small.jpg', {\n      format: 'jpeg',\n      width: 500,\n      height: 500,\n      quality: 10,\n    })\n    expect(high.size).toBeGreaterThan(1000)\n    expect(low.size).toBeLessThan(1000)\n  })\n\n  it('controls quality (webp).', async () => {\n    const high = await processFixture('key-portrait-small.jpg', {\n      format: 'webp',\n      width: 500,\n      height: 500,\n      quality: 80,\n    })\n    const low = await processFixture('key-portrait-small.jpg', {\n      format: 'webp',\n      width: 500,\n      height: 500,\n      quality: 10,\n    })\n    expect(high.size).toBeGreaterThan(1000)\n    expect(low.size).toBeLessThan(1000)\n  })\n\n  async function processFixture(fixture: string, options: Options) {\n    const image = createReadStream(`../dev-env/assets/${fixture}`)\n    const upscaler = createImageUpscaler(options)\n    const processor = createImageProcessor(options)\n\n    const [info] = await Promise.all([\n      getInfo(processor),\n      pipeline([image, upscaler, processor]),\n    ])\n\n    return info\n  }\n})\n"
  },
  {
    "path": "packages/bsky/tests/image/uri.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { cidForCbor } from '@atproto/common'\nimport { BadPathError, ImageUriBuilder } from '../../src/image/uri'\n\ndescribe('image uri builder', () => {\n  const endpoint = 'https://example.com/img'\n  let uriBuilder: ImageUriBuilder\n  let cid: CID\n  const did = 'did:plc:xyz'\n\n  beforeAll(async () => {\n    uriBuilder = new ImageUriBuilder(endpoint)\n    cid = await cidForCbor('test cid')\n  })\n\n  it('generates paths.', () => {\n    expect(\n      ImageUriBuilder.getPath({ preset: 'banner', did, cid: cid.toString() }),\n    ).toEqual(`/banner/plain/${did}/${cid.toString()}`)\n    expect(\n      ImageUriBuilder.getPath({\n        preset: 'feed_thumbnail',\n        did,\n        cid: cid.toString(),\n      }),\n    ).toEqual(`/feed_thumbnail/plain/${did}/${cid.toString()}`)\n  })\n\n  it('generates uris.', () => {\n    expect(uriBuilder.getPresetUri('banner', did, cid.toString())).toEqual(\n      `https://example.com/img/banner/plain/${did}/${cid.toString()}`,\n    )\n    expect(\n      uriBuilder.getPresetUri('feed_thumbnail', did, cid.toString()),\n    ).toEqual(\n      `https://example.com/img/feed_thumbnail/plain/${did}/${cid.toString()}`,\n    )\n  })\n\n  it('parses options.', () => {\n    expect(\n      ImageUriBuilder.getOptions(`/banner/plain/${did}/${cid.toString()}@jpeg`),\n    ).toEqual({\n      did: 'did:plc:xyz',\n      cid: cid.toString(),\n      fit: 'cover',\n      format: 'jpeg',\n      height: 1000,\n      min: true,\n      preset: 'banner',\n      width: 3000,\n    })\n    expect(\n      ImageUriBuilder.getOptions(\n        `/feed_thumbnail/plain/${did}/${cid.toString()}@jpeg`,\n      ),\n    ).toEqual({\n      did: 'did:plc:xyz',\n      cid: cid.toString(),\n      fit: 'inside',\n      format: 'jpeg',\n      height: 2000,\n      min: true,\n      preset: 'feed_thumbnail',\n      width: 2000,\n    })\n    expect(\n      ImageUriBuilder.getOptions(\n        `/feed_thumbnail/plain/${did}/${cid.toString()}`,\n      ),\n    ).toEqual({\n      did: 'did:plc:xyz',\n      cid: cid.toString(),\n      fit: 'inside',\n      format: 'webp',\n      height: 2000,\n      min: true,\n      preset: 'feed_thumbnail',\n      width: 2000,\n    })\n  })\n\n  it('errors on bad url pattern.', () => {\n    expect(tryGetOptions(`/a`)).toThrow(new BadPathError('Invalid path'))\n    expect(tryGetOptions(`/banner/plain/${did}@jpeg`)).toThrow(\n      new BadPathError('Invalid path'),\n    )\n  })\n\n  it('errors on bad preset.', () => {\n    expect(\n      tryGetOptions(`/bad_banner/plain/${did}/${cid.toString()}@jpeg`),\n    ).toThrow(new BadPathError('Invalid path: bad preset'))\n  })\n\n  it('errors on bad format.', () => {\n    expect(tryGetOptions(`/banner/plain/${did}/${cid.toString()}@gif`)).toThrow(\n      new BadPathError('Invalid path: bad format'),\n    )\n  })\n\n  function tryGetOptions(path: string) {\n    return () => ImageUriBuilder.getOptions(path)\n  }\n})\n"
  },
  {
    "path": "packages/bsky/tests/label-hydration.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { MINUTE } from '@atproto/common'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\n\ndescribe('label hydration', () => {\n  let network: TestNetwork\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let bob: string\n  let carol: string\n  let labelerDid: string\n  let labeler2Did: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_label_hydration',\n    })\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    labelerDid = network.bsky.ctx.cfg.labelsFromIssuerDids[0]\n    labeler2Did = network.bsky.ctx.cfg.labelsFromIssuerDids[1]\n    await createLabel({ src: alice, uri: carol, cid: '', val: 'spam' })\n    await createLabel({ src: bob, uri: carol, cid: '', val: 'impersonation' })\n    await createLabel({\n      src: labelerDid,\n      uri: carol,\n      cid: '',\n      val: 'misleading',\n    })\n    await createLabel({\n      src: labeler2Did,\n      uri: carol,\n      cid: '',\n      val: 'expired',\n      exp: new Date(Date.now() - MINUTE).toISOString(),\n    })\n    await createLabel({\n      src: labeler2Did,\n      uri: carol,\n      cid: '',\n      val: 'not-expired',\n      exp: new Date(Date.now() + MINUTE).toISOString(),\n    })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('hydrates labels based on a supplied labeler header', async () => {\n    AtpAgent.configure({ appLabelers: [alice] })\n    pdsAgent.configureLabelers([labeler2Did])\n    const res = await pdsAgent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      {\n        headers: sc.getHeaders(bob),\n      },\n    )\n    expect(res.data.labels?.length).toBe(2)\n    assert(res.data.labels)\n\n    const sortedLabels = res.data.labels.sort((a, b) =>\n      a.src.localeCompare(b.src),\n    )\n    const sortedExpected = [\n      { src: labeler2Did, val: 'not-expired' },\n      { src: alice, val: 'spam' },\n    ].sort((a, b) => a.src.localeCompare(b.src))\n\n    expect(sortedLabels[0].src).toBe(sortedExpected[0].src)\n    expect(sortedLabels[0].val).toBe(sortedExpected[0].val)\n\n    expect(sortedLabels[1].src).toBe(sortedExpected[1].src)\n    expect(sortedLabels[1].val).toBe(sortedExpected[1].val)\n\n    expect(res.headers['atproto-content-labelers']).toEqual(\n      `${alice};redact,${labeler2Did}`,\n    )\n  })\n\n  it('hydrates labels based on multiple a supplied labelers', async () => {\n    AtpAgent.configure({ appLabelers: [bob] })\n    pdsAgent.configureLabelers([alice])\n\n    const res = await pdsAgent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      {\n        headers: {\n          'atproto-accept-labelers': labelerDid,\n          ...sc.getHeaders(bob),\n        },\n      },\n    )\n    expect(res.data.labels?.length).toBe(3)\n    expect(res.data.labels?.find((l) => l.src === alice)?.val).toEqual('spam')\n    expect(res.data.labels?.find((l) => l.src === bob)?.val).toEqual(\n      'impersonation',\n    )\n    expect(res.data.labels?.find((l) => l.src === labelerDid)?.val).toEqual(\n      'misleading',\n    )\n    const labelerHeaderDids = res.headers['atproto-content-labelers']\n      ?.split(',')\n      .sort()\n\n    expect(labelerHeaderDids).toEqual(\n      [alice, `${bob};redact`, labelerDid].sort(),\n    )\n  })\n\n  it('defaults to service labels when no labeler header is provided', async () => {\n    const res = await fetch(\n      `${network.pds.url}/xrpc/app.bsky.actor.getProfile?actor=${carol}`,\n      { headers: sc.getHeaders(bob) },\n    )\n    const data = await res.json()\n\n    expect(data.labels?.length).toBe(2)\n    assert(data.labels)\n\n    const sortedLabels = data.labels.sort((a, b) => a.src.localeCompare(b.src))\n    const sortedExpected = [\n      { src: labeler2Did, val: 'not-expired' },\n      { src: labelerDid, val: 'misleading' },\n    ].sort((a, b) => a.src.localeCompare(b.src))\n\n    expect(sortedLabels[0].src).toBe(sortedExpected[0].src)\n    expect(sortedLabels[0].val).toBe(sortedExpected[0].val)\n\n    expect(sortedLabels[1].src).toBe(sortedExpected[1].src)\n    expect(sortedLabels[1].val).toBe(sortedExpected[1].val)\n\n    expect(res.headers.get('atproto-content-labelers')).toEqual(\n      network.bsky.ctx.cfg.labelsFromIssuerDids\n        .map((did) => `${did};redact`)\n        .join(','),\n    )\n  })\n\n  it('hydrates labels without duplication', async () => {\n    AtpAgent.configure({ appLabelers: [alice] })\n    pdsAgent.configureLabelers([])\n    const res = await pdsAgent.api.app.bsky.actor.getProfiles(\n      { actors: [carol, carol] },\n      { headers: sc.getHeaders(bob) },\n    )\n    const { labels = [] } = res.data.profiles[0]\n    expect(labels.map((l) => ({ val: l.val, src: l.src }))).toEqual([\n      { src: alice, val: 'spam' },\n    ])\n  })\n\n  it('does not hydrate labels from takendown labeler', async () => {\n    AtpAgent.configure({ appLabelers: [alice, sc.dids.dan] })\n    pdsAgent.configureLabelers([])\n    await network.bsky.ctx.dataplane.takedownActor({ did: alice })\n    const res = await pdsAgent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      { headers: sc.getHeaders(bob) },\n    )\n    const { labels = [] } = res.data\n    expect(labels).toEqual([])\n    expect(res.headers['atproto-content-labelers']).toEqual(\n      `${sc.dids.dan};redact`, // does not include alice\n    )\n    await network.bsky.ctx.dataplane.untakedownActor({ did: alice })\n  })\n\n  it('hydrates labels onto list views.', async () => {\n    AtpAgent.configure({ appLabelers: [labelerDid] })\n    pdsAgent.configureLabelers([])\n\n    const list = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: alice },\n      {\n        name: \"alice's modlist\",\n        purpose: 'app.bsky.graph.defs#modlist',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n    await createLabel({ uri: list.uri, cid: list.cid, val: 'spam' })\n    const res = await pdsAgent.api.app.bsky.graph.getList(\n      { list: list.uri },\n      { headers: sc.getHeaders(alice) },\n    )\n    const [label, ...others] = res.data.list.labels ?? []\n    expect(label?.src).toBe(labelerDid)\n    expect(label?.val).toBe('spam')\n    expect(others.length).toBe(0)\n  })\n\n  it('hydrates labels onto feed generator views.', async () => {\n    const feedgen = await pdsAgent.api.app.bsky.feed.generator.create(\n      { repo: alice },\n      {\n        displayName: \"alice's feedgen\",\n        did: alice,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n    await createLabel({ uri: feedgen.uri, cid: feedgen.cid, val: 'spam' })\n    const res = await pdsAgent.api.app.bsky.feed.getFeedGenerators(\n      { feeds: [feedgen.uri] },\n      { headers: sc.getHeaders(alice) },\n    )\n    expect(res.data.feeds.length).toBe(1)\n    const [label, ...others] = res.data.feeds[0].labels ?? []\n    expect(label?.src).toBe(labelerDid)\n    expect(label?.val).toBe('spam')\n    expect(others.length).toBe(0)\n  })\n\n  const createLabel = async (opts: {\n    src?: string\n    uri: string\n    cid: string\n    val: string\n    exp?: string\n  }) => {\n    await network.bsky.db.db\n      .insertInto('label')\n      .values({\n        uri: opts.uri,\n        cid: opts.cid,\n        val: opts.val,\n        cts: new Date().toISOString(),\n        exp: opts.exp ?? null,\n        neg: false,\n        src: opts.src ?? labelerDid,\n      })\n      .execute()\n  }\n})\n"
  },
  {
    "path": "packages/bsky/tests/postgates.test.ts",
    "content": "import AtpAgent, { AppBskyEmbedRecord } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { Users, postgatesSeed } from './seed/postgates'\n\ndescribe('postgates', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let users: Users\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_tests_postgates',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n\n    const result = await postgatesSeed(sc)\n    users = result.users\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe(`quotee <-> quoter`, () => {\n    it(`quotee detaches own post from quoter`, async () => {\n      const quoteePost = await sc.post(users.quotee.did, `post`)\n      const quoterPost = await sc.post(\n        users.quoter.did,\n        `quote post`,\n        undefined,\n        undefined,\n        quoteePost.ref,\n      )\n      await pdsAgent.api.app.bsky.feed.postgate.create(\n        {\n          repo: users.quotee.did,\n          rkey: quoteePost.ref.uri.rkey,\n        },\n        {\n          post: quoteePost.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          detachedEmbeddingUris: [quoterPost.ref.uriStr],\n        },\n        sc.getHeaders(users.quotee.did),\n      )\n      await network.processAll()\n\n      const root = await agent.api.app.bsky.feed.getPostThread(\n        { uri: quoterPost.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            users.viewer.did,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(\n        // @ts-ignore I know more than you\n        AppBskyEmbedRecord.isViewDetached(root.data.thread.post.embed.record),\n      ).toBe(true)\n    })\n\n    it(`postgate made by bystander has no effect`, async () => {\n      const quoteePost = await sc.post(users.quotee.did, `post`)\n      const quoterPost = await sc.post(\n        users.quoter.did,\n        `quote post`,\n        undefined,\n        undefined,\n        quoteePost.ref,\n      )\n      await pdsAgent.api.app.bsky.feed.postgate.create(\n        {\n          repo: users.viewer.did,\n          rkey: quoteePost.ref.uri.rkey,\n        },\n        {\n          post: quoteePost.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          detachedEmbeddingUris: [quoterPost.ref.uriStr],\n        },\n        sc.getHeaders(users.viewer.did),\n      )\n      await network.processAll()\n\n      const root = await agent.api.app.bsky.feed.getPostThread(\n        { uri: quoterPost.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            users.viewer.did,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(\n        // @ts-ignore I know more than you\n        AppBskyEmbedRecord.isViewDetached(root.data.thread.post.embed.record),\n      ).toBe(false)\n    })\n  })\n\n  describe(`embeddingRules`, () => {\n    it(`disables quoteposts`, async () => {\n      const quoteePost = await sc.post(users.quotee.did, `post`)\n      await pdsAgent.api.app.bsky.feed.postgate.create(\n        {\n          repo: users.quotee.did,\n          rkey: quoteePost.ref.uri.rkey,\n        },\n        {\n          post: quoteePost.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }],\n        },\n        sc.getHeaders(users.quotee.did),\n      )\n      await network.processAll()\n\n      const root = await agent.api.app.bsky.feed.getPostThread(\n        { uri: quoteePost.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            users.viewer.did,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(\n        // @ts-ignore I know more than you\n        root.data.thread.post.viewer.embeddingDisabled,\n      ).toBe(true)\n    })\n\n    it(`quotepost created after quotes disabled hides embed`, async () => {\n      const quoteePost = await sc.post(users.quotee.did, `post`)\n      await pdsAgent.api.app.bsky.feed.postgate.create(\n        {\n          repo: users.quotee.did,\n          rkey: quoteePost.ref.uri.rkey,\n        },\n        {\n          post: quoteePost.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }],\n        },\n        sc.getHeaders(users.quotee.did),\n      )\n      await network.processAll()\n\n      const quoterPost = await sc.post(\n        users.quoter.did,\n        `quote post`,\n        undefined,\n        undefined,\n        quoteePost.ref,\n      )\n      await network.processAll()\n\n      const root = await agent.api.app.bsky.feed.getPostThread(\n        { uri: quoterPost.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            users.viewer.did,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(\n        // @ts-ignore I know more than you\n        AppBskyEmbedRecord.isViewDetached(root.data.thread.post.embed.record),\n      ).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/query-labels.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\n\ndescribe('label hydration', () => {\n  let network: TestNetwork\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let bob: string\n  let carol: string\n  let labelerDid: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_query_labels',\n    })\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    labelerDid = network.bsky.ctx.cfg.labelsFromIssuerDids[0]\n    await createLabel({ src: alice, uri: carol, cid: '', val: 'spam' })\n    await createLabel({ src: bob, uri: carol, cid: '', val: 'impersonation' })\n    await createLabel({\n      src: labelerDid,\n      uri: carol,\n      cid: '',\n      val: 'misleading',\n    })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('returns labels based for a subject', async () => {\n    const { data } = await pdsAgent.api.com.atproto.label.queryLabels(\n      { uriPatterns: [carol], sources: [alice] },\n      {\n        headers: sc.getHeaders(bob),\n      },\n    )\n    expect(data.labels?.length).toBe(1)\n    expect(data.labels?.[0].src).toBe(alice)\n    expect(data.labels?.[0].val).toBe('spam')\n  })\n\n  it('returns labels from supplied labelers as param', async () => {\n    const { data } = await pdsAgent.api.com.atproto.label.queryLabels(\n      { uriPatterns: [carol], sources: [alice, labelerDid] },\n      {\n        headers: sc.getHeaders(bob),\n      },\n    )\n    expect(data.labels?.length).toBe(2)\n    expect(data.labels?.find((l) => l.src === alice)?.val).toEqual('spam')\n    expect(data.labels?.find((l) => l.src === labelerDid)?.val).toEqual(\n      'misleading',\n    )\n  })\n\n  const createLabel = async (opts: {\n    src?: string\n    uri: string\n    cid: string\n    val: string\n  }) => {\n    await network.bsky.db.db\n      .insertInto('label')\n      .values({\n        uri: opts.uri,\n        cid: opts.cid,\n        val: opts.val,\n        cts: new Date().toISOString(),\n        exp: null,\n        neg: false,\n        src: opts.src ?? labelerDid,\n      })\n      .execute()\n  }\n})\n"
  },
  {
    "path": "packages/bsky/tests/redis-cache.test.ts",
    "content": "import { wait } from '@atproto/common'\nimport { Redis } from '../src/'\nimport { ReadThroughCache } from '../src/cache/read-through'\n\ndescribe('redis cache', () => {\n  let redis: Redis\n\n  beforeAll(async () => {\n    redis = new Redis({ host: process.env.REDIS_HOST || '' })\n  })\n\n  afterAll(async () => {\n    await redis.destroy()\n  })\n\n  it('caches according to namespace', async () => {\n    const ns1 = redis.withNamespace('ns1')\n    const ns2 = redis.withNamespace('ns2')\n    await Promise.all([\n      ns1.set('key', 'a'),\n      ns2.set('key', 'b'),\n      redis.set('key', 'c'),\n    ])\n    const got = await Promise.all([\n      ns1.get('key'),\n      ns2.get('key'),\n      redis.get('key'),\n    ])\n    expect(got[0]).toEqual('a')\n    expect(got[1]).toEqual('b')\n    expect(got[2]).toEqual('c')\n\n    await Promise.all([\n      ns1.setMulti({ key1: 'a', key2: 'b' }),\n      ns2.setMulti({ key1: 'c', key2: 'd' }),\n      redis.setMulti({ key1: 'e', key2: 'f' }),\n    ])\n    const gotMany = await Promise.all([\n      ns1.getMulti(['key1', 'key2']),\n      ns2.getMulti(['key1', 'key2']),\n      redis.getMulti(['key1', 'key2']),\n    ])\n    expect(gotMany[0]['key1']).toEqual('a')\n    expect(gotMany[0]['key2']).toEqual('b')\n    expect(gotMany[1]['key1']).toEqual('c')\n    expect(gotMany[1]['key2']).toEqual('d')\n    expect(gotMany[2]['key1']).toEqual('e')\n    expect(gotMany[2]['key2']).toEqual('f')\n  })\n\n  it('caches values when empty', async () => {\n    const vals = {\n      '1': 'a',\n      '2': 'b',\n      '3': 'c',\n    }\n    let hits = 0\n    const cache = new ReadThroughCache<string>(redis.withNamespace('test1'), {\n      staleTTL: 60000,\n      maxTTL: 60000,\n      fetchMethod: async (key) => {\n        hits++\n        return vals[key]\n      },\n    })\n    const got = await Promise.all([\n      cache.get('1'),\n      cache.get('2'),\n      cache.get('3'),\n    ])\n    expect(got[0]).toEqual('a')\n    expect(got[1]).toEqual('b')\n    expect(got[2]).toEqual('c')\n    expect(hits).toBe(3)\n\n    const refetched = await Promise.all([\n      cache.get('1'),\n      cache.get('2'),\n      cache.get('3'),\n    ])\n    expect(refetched[0]).toEqual('a')\n    expect(refetched[1]).toEqual('b')\n    expect(refetched[2]).toEqual('c')\n    expect(hits).toBe(3)\n  })\n\n  it('skips and refreshes cache when requested', async () => {\n    let val = 'a'\n    let hits = 0\n    const cache = new ReadThroughCache<string>(redis.withNamespace('test2'), {\n      staleTTL: 60000,\n      maxTTL: 60000,\n      fetchMethod: async () => {\n        hits++\n        return val\n      },\n    })\n\n    const try1 = await cache.get('1')\n    expect(try1).toEqual('a')\n    expect(hits).toBe(1)\n\n    val = 'b'\n\n    const try2 = await cache.get('1')\n    expect(try2).toEqual('a')\n    expect(hits).toBe(1)\n\n    const try3 = await cache.get('1', { revalidate: true })\n    expect(try3).toEqual('b')\n    expect(hits).toBe(2)\n\n    const try4 = await cache.get('1')\n    expect(try4).toEqual('b')\n    expect(hits).toBe(2)\n  })\n\n  it('accurately reports stale entries & refreshes the cache', async () => {\n    let val = 'a'\n    let hits = 0\n    const cache = new ReadThroughCache<string>(redis.withNamespace('test3'), {\n      staleTTL: 1,\n      maxTTL: 60000,\n      fetchMethod: async () => {\n        hits++\n        return val\n      },\n    })\n\n    const try1 = await cache.get('1')\n    expect(try1).toEqual('a')\n\n    await wait(5)\n\n    val = 'b'\n\n    const try2 = await cache.get('1')\n    // cache gives us stale value while it revalidates\n    expect(try2).toEqual('a')\n\n    await wait(5)\n\n    const try3 = await cache.get('1')\n    expect(try3).toEqual('b')\n    expect(hits).toEqual(3)\n  })\n\n  it('does not return expired dids & refreshes the cache', async () => {\n    let val = 'a'\n    let hits = 0\n    const cache = new ReadThroughCache<string>(redis.withNamespace('test4'), {\n      staleTTL: 0,\n      maxTTL: 1,\n      fetchMethod: async () => {\n        hits++\n        return val\n      },\n    })\n\n    const try1 = await cache.get('1')\n    expect(try1).toEqual('a')\n\n    await wait(5)\n\n    val = 'b'\n\n    const try2 = await cache.get('1')\n    expect(try2).toEqual('b')\n    expect(hits).toBe(2)\n  })\n\n  it('caches negative values', async () => {\n    let val: string | null = null\n    let hits = 0\n    const cache = new ReadThroughCache<string>(redis.withNamespace('test5'), {\n      staleTTL: 60000,\n      maxTTL: 60000,\n      fetchMethod: async () => {\n        hits++\n        return val\n      },\n    })\n\n    const try1 = await cache.get('1')\n    expect(try1).toEqual(null)\n    expect(hits).toBe(1)\n\n    val = 'b'\n\n    const try2 = await cache.get('1')\n    // returns cached negative value\n    expect(try2).toEqual(null)\n    expect(hits).toBe(1)\n\n    const try3 = await cache.get('1', { revalidate: true })\n    expect(try3).toEqual('b')\n    expect(hits).toEqual(2)\n\n    const try4 = await cache.get('1')\n    expect(try4).toEqual('b')\n    expect(hits).toEqual(2)\n  })\n\n  it('times out and fails open', async () => {\n    let val = 'a'\n    let hits = 0\n    const cache = new ReadThroughCache<string>(redis.withNamespace('test6'), {\n      staleTTL: 60000,\n      maxTTL: 60000,\n      fetchMethod: async () => {\n        hits++\n        return val\n      },\n    })\n\n    const try1 = await cache.get('1')\n    expect(try1).toEqual('a')\n\n    const orig = cache.redis.driver.get\n    cache.redis.driver.get = async (key) => {\n      await wait(600)\n      return orig(key)\n    }\n\n    val = 'b'\n\n    const try2 = await cache.get('1')\n    expect(try2).toEqual('b')\n    expect(hits).toBe(2)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/seed/feed-hidden-replies.ts",
    "content": "import { SeedClient, TestNetwork, TestNetworkNoAppView } from '@atproto/dev-env'\n\nexport type User = {\n  id: string\n  did: string\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUser(name: string): User {\n  return {\n    id: name,\n    // @ts-ignore overwritten below\n    did: undefined,\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name} label_me`,\n    selfLabels: undefined,\n  }\n}\n\nconst users = {\n  viewer: createUser('viewer'),\n\n  poster: createUser('poster'),\n  replier: createUser('replier'),\n  reposter: createUser('reposter'),\n}\n\nexport type Users = typeof users\n\nexport async function feedHiddenRepliesSeed(\n  sc: SeedClient<TestNetwork | TestNetworkNoAppView>,\n) {\n  const u = structuredClone(users)\n\n  await sc.createAccount('poster', u.poster)\n  await sc.createAccount('replier', u.replier)\n  await sc.createAccount('viewer', u.viewer)\n  await sc.createAccount('reposter', u.reposter)\n\n  Object.values(u).forEach((user) => {\n    u[user.id].did = sc.dids[user.id]\n  })\n\n  await sc.follow(u.viewer.did, u.poster.did)\n  await sc.follow(u.viewer.did, u.replier.did)\n  await sc.follow(u.viewer.did, u.reposter.did)\n\n  await sc.network.processAll()\n\n  return {\n    users: u,\n    seedClient: sc,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/seed/get-suggested-starter-packs.ts",
    "content": "import { SeedClient, TestNetwork, TestNetworkNoAppView } from '@atproto/dev-env'\n\nexport type User = {\n  id: string\n  did: string\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUser(name: string): User {\n  return {\n    id: name,\n    // @ts-ignore overwritten below\n    did: undefined,\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name} label_me`,\n    selfLabels: undefined,\n  }\n}\n\nconst users = {\n  creator: createUser('creator'),\n  poster: createUser('poster'),\n\n  viewer: createUser('viewer'),\n  viewerBlocker: createUser('viewerBlocker'),\n}\n\nexport type Users = typeof users\nexport type StarterPacks = SeedClient['starterpacks']\n\nexport async function starterPacksSeed(\n  sc: SeedClient<TestNetwork | TestNetworkNoAppView>,\n) {\n  const u = structuredClone(users)\n\n  await sc.createAccount('creator', u.creator)\n  await sc.createAccount('poster', u.poster)\n  await sc.createAccount('viewer', u.viewer)\n  await sc.createAccount('viewerBlocker', u.viewerBlocker)\n\n  Object.values(u).forEach((user) => {\n    u[user.id].did = sc.dids[user.id]\n  })\n\n  await sc.createStarterPack(u.creator.did, 'test', [u.poster.did])\n  await sc.block(u.viewerBlocker.did, u.creator.did)\n\n  await sc.network.processAll()\n\n  return {\n    users: u,\n    starterpacks: sc.starterpacks,\n    seedClient: sc,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/seed/get-trends.ts",
    "content": "import { SeedClient, TestNetwork, TestNetworkNoAppView } from '@atproto/dev-env'\n\nexport type User = {\n  id: string\n  did: string\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUser(name: string): User {\n  return {\n    id: name,\n    // @ts-ignore overwritten below\n    did: undefined,\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name} label_me`,\n    selfLabels: undefined,\n  }\n}\n\nconst users = {\n  trender: createUser('trender'),\n\n  posterA: createUser('posterA'),\n  posterB: createUser('posterB'),\n  posterC: createUser('posterC'),\n  posterD: createUser('posterD'),\n\n  viewer: createUser('viewer'),\n  viewerBlocker: createUser('viewerBlocker'),\n}\n\nexport type Users = typeof users\nexport type Feeds = SeedClient['feedgens']\n\nexport async function trendsSeed(\n  sc: SeedClient<TestNetwork | TestNetworkNoAppView>,\n) {\n  const u = structuredClone(users)\n\n  await sc.createAccount('trender', u.trender)\n  await sc.createAccount('posterA', u.posterA)\n  await sc.createAccount('posterB', u.posterB)\n  await sc.createAccount('posterC', u.posterC)\n  await sc.createAccount('posterD', u.posterD)\n  await sc.createAccount('viewer', u.viewer)\n  await sc.createAccount('viewerBlocker', u.viewerBlocker)\n\n  Object.values(u).forEach((user) => {\n    u[user.id].did = sc.dids[user.id]\n  })\n\n  await sc.createFeedGen(u.trender.did, 'did:web:example.com', 'trendA')\n  await sc.block(u.viewerBlocker.did, u.posterC.did)\n\n  await sc.network.processAll()\n\n  return {\n    users: u,\n    feeds: sc.feedgens,\n    seedClient: sc,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/seed/known-followers.ts",
    "content": "import { SeedClient, TestNetwork, TestNetworkNoAppView } from '@atproto/dev-env'\n\nexport type User = {\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUser(name: string): User {\n  return {\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name} label_me`,\n    selfLabels: undefined,\n  }\n}\n\nconst users = {\n  /*\n   * Base test. One known follower.\n   */\n  base_sub: createUser('base-sub'),\n  base_res_1: createUser('base-res-1'),\n  base_view: createUser('base-view'),\n\n  /*\n   * First-part block of a single known follower.\n   */\n  fp_block_sub: createUser('fp-block-sub'),\n  fp_block_res_1: createUser('fp-block-res-1'),\n  fp_block_view: createUser('fp-block-view'),\n\n  /*\n   * Second-party block of a single known follower.\n   */\n  sp_block_sub: createUser('sp-block-sub'),\n  sp_block_res_1: createUser('sp-block-res-1'),\n  sp_block_view: createUser('sp-block-view'),\n\n  /*\n   * Mix of known followers and blocks.\n   */\n  mix_sub_1: createUser('mix-sub-1'),\n  mix_sub_2: createUser('mix-sub-2'),\n  mix_sub_3: createUser('mix-sub-3'),\n  mix_res: createUser('mix-res'),\n  mix_fp_block_res: createUser('mix-fp-block-res'),\n  mix_sp_block_res: createUser('mix-sp-block-res'),\n  mix_view: createUser('mix-view'),\n}\n\nexport async function knownFollowersSeed(\n  sc: SeedClient<TestNetwork | TestNetworkNoAppView>,\n) {\n  await sc.createAccount('base_sub', users.base_sub)\n  await sc.createAccount('base_res_1', users.base_res_1)\n  await sc.createAccount('base_view', users.base_view)\n\n  await sc.createAccount('fp_block_sub', users.fp_block_sub)\n  await sc.createAccount('fp_block_res_1', users.fp_block_res_1)\n  await sc.createAccount('fp_block_view', users.fp_block_view)\n\n  await sc.createAccount('sp_block_sub', users.sp_block_sub)\n  await sc.createAccount('sp_block_res_1', users.sp_block_res_1)\n  await sc.createAccount('sp_block_view', users.sp_block_view)\n\n  await sc.createAccount('mix_sub_1', users.mix_sub_1)\n  await sc.createAccount('mix_sub_2', users.mix_sub_2)\n  await sc.createAccount('mix_sub_3', users.mix_sub_3)\n  await sc.createAccount('mix_res', users.mix_res)\n  await sc.createAccount('mix_fp_block_res', users.mix_fp_block_res)\n  await sc.createAccount('mix_sp_block_res', users.mix_sp_block_res)\n  await sc.createAccount('mix_view', users.mix_view)\n\n  const dids = sc.dids\n\n  await sc.follow(dids.base_res_1, dids.base_sub)\n  await sc.follow(dids.base_view, dids.base_res_1)\n\n  await sc.follow(dids.fp_block_res_1, dids.fp_block_sub)\n  await sc.follow(dids.fp_block_view, dids.fp_block_res_1)\n\n  await sc.follow(dids.sp_block_res_1, dids.sp_block_sub)\n  await sc.follow(dids.sp_block_view, dids.sp_block_res_1)\n\n  await sc.follow(dids.mix_res, dids.mix_sub_1)\n  await sc.follow(dids.mix_res, dids.mix_sub_2)\n  await sc.follow(dids.mix_res, dids.mix_sub_3)\n  await sc.follow(dids.mix_fp_block_res, dids.mix_sub_1)\n  await sc.follow(dids.mix_fp_block_res, dids.mix_sub_2)\n  await sc.follow(dids.mix_fp_block_res, dids.mix_sub_3)\n  await sc.follow(dids.mix_sp_block_res, dids.mix_sub_1)\n  await sc.follow(dids.mix_sp_block_res, dids.mix_sub_2)\n  await sc.follow(dids.mix_sp_block_res, dids.mix_sub_3)\n  await sc.follow(dids.mix_view, dids.mix_res)\n  await sc.follow(dids.mix_view, dids.mix_fp_block_res)\n  await sc.follow(dids.mix_view, dids.mix_sp_block_res)\n\n  await sc.network.processAll()\n\n  return {\n    users,\n    seedClient: sc,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/seed/postgates.ts",
    "content": "import { SeedClient, TestNetwork, TestNetworkNoAppView } from '@atproto/dev-env'\n\nexport type User = {\n  id: string\n  did: string\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUser(name: string): User {\n  return {\n    id: name,\n    // @ts-ignore overwritten below\n    did: undefined,\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name} label_me`,\n    selfLabels: undefined,\n  }\n}\n\nconst users = {\n  viewer: createUser('viewer'),\n\n  quotee: createUser('quotee'),\n  quoter: createUser('quoter'),\n}\n\nexport type Users = typeof users\n\nexport async function postgatesSeed(\n  sc: SeedClient<TestNetwork | TestNetworkNoAppView>,\n) {\n  const u = structuredClone(users)\n\n  await sc.createAccount('quotee', u.quotee)\n  await sc.createAccount('quoter', u.quoter)\n  await sc.createAccount('viewer', u.viewer)\n\n  Object.values(u).forEach((user) => {\n    u[user.id].did = sc.dids[user.id]\n  })\n\n  await sc.network.processAll()\n\n  return {\n    users: u,\n    seedClient: sc,\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/server.test.ts",
    "content": "import { once } from 'node:events'\nimport { AddressInfo } from 'node:net'\nimport { finished } from 'node:stream/promises'\nimport express from 'express'\nimport { request } from 'undici'\nimport { TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { handler as errorHandler } from '../src/error'\nimport { startServer } from './_util'\n\ndescribe('server', () => {\n  let network: TestNetwork\n  let alice: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_server',\n    })\n    const sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('preserves 404s.', async () => {\n    const response = await fetch(`${network.bsky.url}/unknown`)\n    expect(response.status).toEqual(404)\n  })\n\n  it('error handler turns unknown errors into 500s.', async () => {\n    const app = express()\n    app.get('/oops', () => {\n      throw new Error('Oops!')\n    })\n    app.use(errorHandler)\n    const { origin, stop } = await startServer(app)\n    try {\n      const response = await fetch(new URL(`/oops`, origin))\n      expect(response.status).toEqual(500)\n      await expect(response.json()).resolves.toEqual({\n        error: 'InternalServerError',\n        message: 'Internal Server Error',\n      })\n    } finally {\n      await stop()\n    }\n  })\n\n  it('healthcheck succeeds when database is available.', async () => {\n    const response = await fetch(`${network.bsky.url}/xrpc/_health`)\n    expect(response.status).toEqual(200)\n    await expect(response.json()).resolves.toEqual({ version: 'unknown' })\n  })\n\n  // TODO(bsky) check on a different endpoint that accepts json, currently none.\n  it.skip('limits size of json input.', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/xrpc/com.atproto.repo.createRecord`,\n      {\n        method: 'POST',\n        body: '\"' + 'x'.repeat(150 * 1024) + '\"', // ~150kb\n        headers: { 'Content-Type': 'application/json' },\n      },\n    )\n\n    expect(response.status).toEqual(413)\n    await expect(response.json()).resolves.toEqual({\n      error: 'PayloadTooLargeError',\n      message: 'request entity too large',\n    })\n  })\n\n  it('compresses large json responses', async () => {\n    const res = await request(\n      `${network.bsky.url}/xrpc/app.bsky.feed.getTimeline`,\n      {\n        headers: {\n          ...(await network.serviceHeaders(alice, 'app.bsky.feed.getTimeline')),\n          'accept-encoding': 'gzip',\n        },\n      },\n    )\n\n    await finished(res.body.resume())\n\n    expect(res.headers['content-encoding']).toEqual('gzip')\n  })\n\n  it('does not compress small payloads', async () => {\n    const res = await request(`${network.bsky.url}/xrpc/_health`, {\n      headers: { 'accept-encoding': 'gzip' },\n    })\n\n    await finished(res.body.resume())\n\n    expect(res.headers['content-encoding']).toBeUndefined()\n  })\n\n  it('healthcheck fails when dataplane is unavailable.', async () => {\n    const { port } = network.bsky.dataplane.server.address() as AddressInfo\n    await network.bsky.dataplane.destroy()\n\n    try {\n      const response = await fetch(`${network.bsky.url}/xrpc/_health`)\n      expect(response.status).toEqual(503)\n      await expect(response.json()).resolves.toEqual({\n        version: 'unknown',\n        error: 'Service Unavailable',\n      })\n    } finally {\n      // restart dataplane server to allow test suite to cleanup\n      network.bsky.dataplane.server.listen(port)\n      await once(network.bsky.dataplane.server, 'listening')\n    }\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/sitemap.test.ts",
    "content": "import { TestNetwork } from '@atproto/dev-env'\n\ndescribe('sitemap', () => {\n  let network: TestNetwork\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_sitemap',\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('returns sitemap index', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/external/sitemap/users.xml.gz`,\n    )\n    expect(response.status).toEqual(200)\n    expect(response.headers.get('content-type')).toEqual('application/gzip')\n    expect(response.headers.get('content-encoding')).toEqual('gzip')\n\n    // fetch automatically decompresses gzip when Content-Encoding is set\n    const xml = await response.text()\n\n    expect(xml).toContain('<?xml version=\"1.0\" encoding=\"UTF-8\"?>')\n    expect(xml).toContain('<sitemapindex')\n    expect(xml).toContain('</sitemapindex>')\n  })\n\n  it('returns sitemap page', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/external/sitemap/users/2025-01-01/1.xml.gz`,\n    )\n    expect(response.status).toEqual(200)\n    expect(response.headers.get('content-type')).toEqual('application/gzip')\n    expect(response.headers.get('content-encoding')).toEqual('gzip')\n\n    // fetch automatically decompresses gzip when Content-Encoding is set\n    const xml = await response.text()\n\n    expect(xml).toContain('<?xml version=\"1.0\" encoding=\"UTF-8\"?>')\n    expect(xml).toContain('<urlset')\n    expect(xml).toContain('</urlset>')\n  })\n\n  it('returns 400 for invalid date format', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/external/sitemap/users/invalid-date/1.xml.gz`,\n    )\n    expect(response.status).toEqual(400)\n  })\n\n  it('returns 400 for invalid bucket number', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/external/sitemap/users/2025-01-01/0.xml.gz`,\n    )\n    expect(response.status).toEqual(400)\n  })\n\n  it('returns 400 for non-numeric bucket', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/external/sitemap/users/2025-01-01/abc.xml.gz`,\n    )\n    expect(response.status).toEqual(400)\n  })\n\n  it('returns 404 for non-existent sitemap page', async () => {\n    const response = await fetch(\n      `${network.bsky.url}/external/sitemap/users/2024-01-01/1.xml.gz`,\n    )\n    expect(response.status).toEqual(404)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/stash.test.ts",
    "content": "import { TestNetwork } from '@atproto/dev-env'\nimport { ProfileAssociatedChat } from '../dist/lexicon/types/app/bsky/actor/defs'\nimport { StashClient } from '../dist/stash'\nimport { Namespace } from '../src/stash'\n\ntype Database = TestNetwork['bsky']['db']\n\ndescribe('private data', () => {\n  let network: TestNetwork\n  let stashClient: StashClient\n  let db: Database\n\n  const actorDid = 'did:plc:example'\n  // This lexicon has nothing special other than being simple, convenient to use in a test.\n  const namespace = 'app.bsky.actor.defs#profileAssociatedChat' as Namespace\n  const key = 'self'\n\n  const validPayload0: ProfileAssociatedChat = { allowIncoming: 'all' }\n  const validPayload1: ProfileAssociatedChat = { allowIncoming: 'following' }\n  const invalidPayload: ProfileAssociatedChat = {\n    invalid: 'all',\n  } as unknown as ProfileAssociatedChat\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_private_data',\n    })\n    db = network.bsky.db\n    stashClient = network.bsky.ctx.stashClient\n  })\n\n  afterEach(async () => {\n    await clearPrivateData(db)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('create', () => {\n    it('creates entry', async () => {\n      await stashClient.create({\n        actorDid,\n        namespace,\n        key,\n        payload: validPayload0,\n      })\n      await network.processAll()\n\n      const dbResult = await db.db\n        .selectFrom('private_data')\n        .selectAll()\n        .where('actorDid', '=', actorDid)\n        .where('namespace', '=', namespace)\n        .where('key', '=', key)\n        .executeTakeFirstOrThrow()\n      expect(dbResult).toStrictEqual({\n        actorDid,\n        namespace,\n        key,\n        payload: JSON.stringify({ $type: namespace, ...validPayload0 }),\n        indexedAt: expect.any(String),\n        updatedAt: expect.any(String),\n      })\n    })\n\n    it('validates lexicon', async () => {\n      expect(() =>\n        stashClient.create({\n          actorDid,\n          namespace,\n          key,\n          payload: invalidPayload,\n        }),\n      ).toThrow('Object must have the property \"allowIncoming\"')\n    })\n  })\n\n  describe('update', () => {\n    it('updates entry', async () => {\n      await stashClient.create({\n        actorDid,\n        namespace,\n        key,\n        payload: validPayload0,\n      })\n      await network.processAll()\n\n      await stashClient.update({\n        actorDid,\n        namespace,\n        key,\n        payload: validPayload1,\n      })\n      await network.processAll()\n\n      const dbResult = await db.db\n        .selectFrom('private_data')\n        .selectAll()\n        .where('actorDid', '=', actorDid)\n        .where('namespace', '=', namespace)\n        .where('key', '=', key)\n        .executeTakeFirstOrThrow()\n      expect(dbResult).toStrictEqual({\n        actorDid,\n        namespace,\n        key,\n        payload: JSON.stringify({ $type: namespace, ...validPayload1 }),\n        indexedAt: expect.any(String),\n        updatedAt: expect.any(String),\n      })\n    })\n\n    it('validates lexicon', async () => {\n      expect(() =>\n        stashClient.update({\n          actorDid,\n          namespace,\n          key,\n          payload: invalidPayload,\n        }),\n      ).toThrow('Object must have the property \"allowIncoming\"')\n    })\n  })\n\n  describe('delete', () => {\n    it('deletes entry', async () => {\n      await stashClient.create({\n        actorDid,\n        namespace,\n        key,\n        payload: validPayload0,\n      })\n      await network.processAll()\n\n      await stashClient.delete({\n        actorDid,\n        namespace,\n        key,\n      })\n      await network.processAll()\n\n      const dbResult = await db.db\n        .selectFrom('private_data')\n        .selectAll()\n        .where('actorDid', '=', actorDid)\n        .where('namespace', '=', namespace)\n        .where('key', '=', key)\n        .executeTakeFirst()\n      expect(dbResult).toBe(undefined)\n    })\n  })\n})\n\nconst clearPrivateData = async (db: Database) => {\n  await db.db.deleteFrom('private_data').execute()\n}\n"
  },
  {
    "path": "packages/bsky/tests/utils.test.ts",
    "content": "import {\n  PostSearchQuery,\n  parsePostSearchQuery,\n} from '../src/data-plane/server/util'\n\ndescribe('parsePostSearchQuery', () => {\n  type TestCase = {\n    input: string\n    output: PostSearchQuery\n  }\n\n  const tests: TestCase[] = [\n    {\n      input: `bluesky `,\n      output: { q: `bluesky`, author: undefined },\n    },\n    {\n      input: ` bluesky  from:esb.lol`,\n      output: { q: `bluesky`, author: `esb.lol` },\n    },\n    {\n      input: `bluesky \"from:esb.lol\"`,\n      output: { q: `bluesky \"from:esb.lol\"`, author: undefined },\n    },\n    {\n      input: `bluesky mentions:@esb.lol `,\n      output: { q: `bluesky mentions:@esb.lol`, author: undefined },\n    },\n    {\n      input: `bluesky lang:\"en\"`,\n      output: { q: `bluesky lang:\"en\"`, author: undefined },\n    },\n    {\n      input: `bluesky \"literal\" \"from:invalid\" did:test:123 `,\n      output: {\n        q: `bluesky \"literal\" \"from:invalid\"`,\n        author: `did:test:123`,\n      },\n    },\n  ]\n\n  it.each(tests)(`'$input' -> '$output'`, ({ input, output }) => {\n    expect(parsePostSearchQuery(input)).toEqual(output)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds actor search views search gives relevant results 1`] = `\nArray [\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Carlton Abernathy IV\",\n    \"handle\": \"aliya-hodkiewicz.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"did\": \"user(2)\",\n    \"handle\": \"cara-wiegand69.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"did\": \"user(3)\",\n    \"handle\": \"carlos6.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg\",\n    \"did\": \"user(4)\",\n    \"displayName\": \"Latoya Windler\",\n    \"handle\": \"carolina-mcderm77.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg\",\n    \"did\": \"user(6)\",\n    \"displayName\": \"Rachel Kshlerin\",\n    \"handle\": \"cayla-marquardt39.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg\",\n    \"did\": \"user(8)\",\n    \"displayName\": \"Carol Littel\",\n    \"handle\": \"eudora-dietrich4.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(11)/cids(0)@jpeg\",\n    \"did\": \"user(10)\",\n    \"displayName\": \"Sadie Carter\",\n    \"handle\": \"shane-torphy52.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n]\n`;\n\nexports[`pds actor search views typeahead gives relevant results 1`] = `\nArray [\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Carlton Abernathy IV\",\n    \"handle\": \"aliya-hodkiewicz.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"did\": \"user(2)\",\n    \"handle\": \"cara-wiegand69.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"did\": \"user(3)\",\n    \"handle\": \"carlos6.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg\",\n    \"did\": \"user(4)\",\n    \"displayName\": \"Latoya Windler\",\n    \"handle\": \"carolina-mcderm77.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg\",\n    \"did\": \"user(6)\",\n    \"displayName\": \"Rachel Kshlerin\",\n    \"handle\": \"cayla-marquardt39.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg\",\n    \"did\": \"user(8)\",\n    \"displayName\": \"Carol Littel\",\n    \"handle\": \"eudora-dietrich4.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(11)/cids(0)@jpeg\",\n    \"did\": \"user(10)\",\n    \"displayName\": \"Sadie Carter\",\n    \"handle\": \"shane-torphy52.test\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(5)\",\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(3)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(3)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(8)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(7)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(11)\",\n                    \"following\": \"record(10)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(8)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(9)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(5)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(9)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(10)\",\n                        \"uri\": \"record(12)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(7)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(8)\",\n                \"uri\": \"record(9)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(6)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(6)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(7)\",\n            \"uri\": \"record(7)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(11)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(13)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 2`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(0)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(0)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(2)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(1)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(4)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(2)\",\n              \"uri\": \"record(4)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(4)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(2)\",\n              \"uri\": \"record(4)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(3)\",\n            \"following\": \"record(2)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(1)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(5)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(4)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(2)\",\n              \"uri\": \"record(4)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(4)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(2)\",\n              \"uri\": \"record(4)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(3)\",\n            \"following\": \"record(2)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(1)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(5)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"text\": \"bobby boy here\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(7)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 3`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(1)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(4)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(3)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(4)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(2)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(2)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(3)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(4)\",\n                  \"uri\": \"record(2)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(1)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"repost\": \"record(4)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(4)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(6)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(6)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(9)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(9)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(6)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(10)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(9)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(9)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(6)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(10)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(1)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(4)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(3)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(4)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(2)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(3)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(4)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(1)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 4`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(3)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"repost\": \"record(5)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(5)\",\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(8)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(4)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(4)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(7)\",\n          \"repost\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(7)\",\n        \"repost\": \"record(6)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(6)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(5)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(9)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(3)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"following\": \"record(8)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(11)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(11)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(10)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(6)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(10)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(11)\",\n                  \"uri\": \"record(11)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(12)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"dan here!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(12)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views pins cannot pin someone else's post 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(7)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(8)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(12)\",\n            \"uri\": \"record(11)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(11)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(13)\",\n            \"following\": \"record(12)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(12)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(13)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(13)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(12)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(11)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(12)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(11)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(13)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(11)\",\n              \"uri\": \"record(10)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(11)\",\n              \"uri\": \"record(10)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(11)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(11)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"pinned\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(14)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(16)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(15)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(19)\",\n                    \"following\": \"record(18)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(16)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(17)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(13)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(17)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(18)\",\n                        \"uri\": \"record(20)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(15)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(16)\",\n                \"uri\": \"record(17)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(14)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(14)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(15)\",\n            \"uri\": \"record(15)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(14)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(19)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(19)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(21)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(21)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views pins params.includePins = false 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": true,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(7)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(11)\",\n            \"uri\": \"record(10)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(10)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(8)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(12)\",\n            \"following\": \"record(11)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(11)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(12)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(12)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(11)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(10)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(11)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(10)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(12)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(10)\",\n              \"uri\": \"record(9)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(10)\",\n              \"uri\": \"record(9)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(10)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"pinned\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(13)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(15)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(14)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(18)\",\n                    \"following\": \"record(17)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(15)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(16)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(12)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(16)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(17)\",\n                        \"uri\": \"record(19)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(14)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(15)\",\n                \"uri\": \"record(16)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(13)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(13)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(14)\",\n            \"uri\": \"record(14)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(18)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(18)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(20)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(20)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views pins params.includePins = true, pin is NOT in first page of results 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": true,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonPin\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views pins params.includePins = true, pin is NOT in first page of results 2`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": true,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(7)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(7)\",\n            \"uri\": \"record(6)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(9)\",\n            \"following\": \"record(8)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(9)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(9)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(7)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(7)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(9)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(7)\",\n              \"uri\": \"record(6)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(7)\",\n              \"uri\": \"record(6)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(7)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(7)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(6)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"pinned\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(12)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(11)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(15)\",\n                    \"following\": \"record(14)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(12)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(13)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(9)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(13)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(14)\",\n                        \"uri\": \"record(16)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(11)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(12)\",\n                \"uri\": \"record(13)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(10)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(10)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(11)\",\n            \"uri\": \"record(11)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(15)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(15)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(17)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(17)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views pins params.includePins = true, pin is in first page of results 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": true,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonPin\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"not pinned post\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(7)\",\n            \"uri\": \"record(6)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(6)\",\n            \"uri\": \"record(5)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(7)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(8)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(8)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(7)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(6)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(7)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(6)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(8)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(6)\",\n              \"uri\": \"record(5)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(6)\",\n              \"uri\": \"record(5)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(6)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(6)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"pinned\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(11)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(10)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(14)\",\n                    \"following\": \"record(13)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(11)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(12)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(8)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(12)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(13)\",\n                        \"uri\": \"record(15)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(10)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(11)\",\n                \"uri\": \"record(12)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(9)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(9)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(10)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(14)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(14)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(16)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(16)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"pinned\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`pds author feed views reflects fetching user's state in the feed. 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(5)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(4)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(4)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 3,\n        \"repostCount\": 1,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(7)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(8)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(10)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(5)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(9)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(10)\",\n                        \"uri\": \"record(11)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(9)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(8)\",\n                \"uri\": \"record(10)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(6)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(8)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(7)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(8)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(12)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 3,\n      \"repostCount\": 1,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(6)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(11)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(13)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds views with blocking from block lists blocks record embeds 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(3)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewBlocked\",\n                \"author\": Object {\n                  \"did\": \"user(3)\",\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"blocking\": \"record(5)\",\n                  },\n                },\n                \"blocked\": true,\n                \"uri\": \"record(4)\",\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(3)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(4)\",\n                \"uri\": \"record(4)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(0)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(3)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`pds views with blocking from block lists blocks thread parent 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"parent\": Object {\n      \"$type\": \"app.bsky.feed.defs#blockedPost\",\n      \"author\": Object {\n        \"did\": \"user(2)\",\n        \"viewer\": Object {\n          \"blockedBy\": true,\n        },\n      },\n      \"blocked\": true,\n      \"uri\": \"record(4)\",\n    },\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"alice replies to dan\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"replies\": Array [],\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`pds views with blocking from block lists blocks thread reply 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(4)\",\n        \"repost\": \"record(3)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"replies\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.defs#blockedPost\",\n        \"author\": Object {\n          \"did\": \"user(2)\",\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"blocking\": \"record(6)\",\n          },\n        },\n        \"blocked\": true,\n        \"uri\": \"record(5)\",\n      },\n      Object {\n        \"$type\": \"app.bsky.feed.defs#blockedPost\",\n        \"author\": Object {\n          \"did\": \"user(3)\",\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"blocking\": \"record(6)\",\n          },\n        },\n        \"blocked\": true,\n        \"uri\": \"record(7)\",\n      },\n    ],\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`pds views with blocking from block lists returns a users own list blocks 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"blah blah\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 0,\n      \"name\": \"new list\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"blocked\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"big list of blocks\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 3,\n      \"name\": \"alice blocks\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"blocked\": \"record(5)\",\n        \"muted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`pds views with blocking from block lists returns lists associated with a user 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"blah blah\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 0,\n      \"name\": \"new list\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"big list of blocks\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 3,\n      \"name\": \"alice blocks\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"blocked\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`pds views with blocking from block lists returns the contents of a list 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"items\": Array [\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(4)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"blocking\": \"record(0)\",\n          \"blockingByList\": Object {\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n            \"cid\": \"cids(0)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"listItemCount\": 3,\n            \"name\": \"alice blocks\",\n            \"purpose\": \"app.bsky.graph.defs#modlist\",\n            \"uri\": \"record(0)\",\n            \"viewer\": Object {\n              \"blocked\": \"record(1)\",\n              \"muted\": false,\n            },\n          },\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(5)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"blocking\": \"record(0)\",\n          \"blockingByList\": Object {\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n            \"cid\": \"cids(0)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"listItemCount\": 3,\n            \"name\": \"alice blocks\",\n            \"purpose\": \"app.bsky.graph.defs#modlist\",\n            \"uri\": \"record(0)\",\n            \"viewer\": Object {\n              \"blocked\": \"record(1)\",\n              \"muted\": false,\n            },\n          },\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n  ],\n  \"list\": Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"big list of blocks\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"alice blocks\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"blocked\": \"record(1)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds views with blocking blocks record embeds 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(3)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewBlocked\",\n                \"author\": Object {\n                  \"did\": \"user(3)\",\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"blocking\": \"record(5)\",\n                  },\n                },\n                \"blocked\": true,\n                \"uri\": \"record(4)\",\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(3)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(4)\",\n                \"uri\": \"record(4)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(0)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(3)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`pds views with blocking blocks thread parent 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"parent\": Object {\n      \"$type\": \"app.bsky.feed.defs#blockedPost\",\n      \"author\": Object {\n        \"did\": \"user(2)\",\n        \"viewer\": Object {\n          \"blockedBy\": true,\n        },\n      },\n      \"blocked\": true,\n      \"uri\": \"record(4)\",\n    },\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"alice replies to dan\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"replies\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(3)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(4)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(4)\",\n              },\n            },\n            \"text\": \"carol replies to alice's reply to dan\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(5)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"threadContext\": Object {},\n      },\n    ],\n    \"threadContext\": Object {},\n  },\n}\n`;\n\nexports[`pds views with blocking blocks thread reply 1`] = `\nObject {\n  \"thread\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(4)\",\n        \"repost\": \"record(3)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"replies\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.defs#blockedPost\",\n        \"author\": Object {\n          \"did\": \"user(2)\",\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"blocking\": \"record(6)\",\n          },\n        },\n        \"blocked\": true,\n        \"uri\": \"record(5)\",\n      },\n      Object {\n        \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(3)\",\n            \"displayName\": \"bobby\",\n            \"handle\": \"bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(8)\",\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images#view\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(4)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(4)\",\n              },\n            ],\n          },\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(3)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(7)\",\n              \"val\": \"test-label\",\n            },\n            Object {\n              \"cid\": \"cids(3)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(7)\",\n              \"val\": \"test-label-2\",\n            },\n          ],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(4)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n              ],\n            },\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"hear that label_me label_me_2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(7)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"threadContext\": Object {},\n      },\n    ],\n    \"threadContext\": Object {},\n  },\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/bookmarks.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`appview bookmarks views listing shows posts and blocked posts correctly 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"item\": Object {\n      \"$type\": \"app.bsky.feed.defs#postView\",\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 2,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(1)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(1)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#view\",\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewBlocked\",\n            \"author\": Object {\n              \"did\": \"user(2)\",\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"blocking\": \"record(4)\",\n              },\n            },\n            \"blocked\": true,\n            \"uri\": \"record(3)\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(1)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": true,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(5)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"subject\": Object {\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"item\": Object {\n      \"$type\": \"app.bsky.feed.defs#blockedPost\",\n      \"author\": Object {\n        \"did\": \"user(2)\",\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"blocking\": \"record(4)\",\n        },\n      },\n      \"blocked\": true,\n      \"uri\": \"record(3)\",\n    },\n    \"subject\": Object {\n      \"cid\": \"cids(3)\",\n      \"uri\": \"record(3)\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"item\": Object {\n      \"$type\": \"app.bsky.feed.defs#postView\",\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(6)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(3)\",\n            \"uri\": \"record(7)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(6)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(3)\",\n            \"uri\": \"record(7)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 2,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(3)\",\n          \"uri\": \"record(6)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": true,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"subject\": Object {\n      \"cid\": \"cids(4)\",\n      \"uri\": \"record(6)\",\n    },\n  },\n]\n`;\n\nexports[`appview bookmarks views listing shows posts and blocked posts correctly 2`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"item\": Object {\n      \"$type\": \"app.bsky.feed.defs#postView\",\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 2,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(1)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(1)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(4)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(2)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(3)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(2)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(1)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": true,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(3)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"subject\": Object {\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"item\": Object {\n      \"$type\": \"app.bsky.feed.defs#blockedPost\",\n      \"author\": Object {\n        \"did\": \"user(4)\",\n        \"viewer\": Object {\n          \"blockedBy\": true,\n        },\n      },\n      \"blocked\": true,\n      \"uri\": \"record(4)\",\n    },\n    \"subject\": Object {\n      \"cid\": \"cids(5)\",\n      \"uri\": \"record(4)\",\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/follows.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds follow views fetches followers 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"followers\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-eve\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-eve\",\n      \"handle\": \"eve.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-dan\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-dan\",\n      \"handle\": \"dan.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(3)\",\n        \"following\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-bob\",\n      \"did\": \"user(4)\",\n      \"displayName\": \"display-bob\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(5)\",\n        \"following\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-carol\",\n      \"did\": \"user(6)\",\n      \"displayName\": \"display-carol\",\n      \"handle\": \"carol.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(7)\",\n        \"following\": \"record(6)\",\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(9)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-alice\",\n    \"did\": \"user(8)\",\n    \"displayName\": \"display-alice\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches followers 2`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"followers\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-dan\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-dan\",\n      \"handle\": \"dan.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-bob\",\n    \"did\": \"user(4)\",\n    \"displayName\": \"display-bob\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(3)\",\n      \"following\": \"record(2)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches followers 3`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"followers\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-eve\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-eve\",\n      \"handle\": \"eve.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-bob\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-bob\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(3)\",\n        \"following\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(4)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-carol\",\n    \"did\": \"user(6)\",\n    \"displayName\": \"display-carol\",\n    \"handle\": \"carol.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(5)\",\n      \"following\": \"record(4)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches followers 4`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"followers\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-dan\",\n    \"did\": \"user(2)\",\n    \"displayName\": \"display-dan\",\n    \"handle\": \"dan.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(1)\",\n      \"following\": \"record(0)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches followers 5`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"followers\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-dan\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-dan\",\n      \"handle\": \"dan.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-eve\",\n    \"did\": \"user(4)\",\n    \"displayName\": \"display-eve\",\n    \"handle\": \"eve.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(3)\",\n      \"following\": \"record(2)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches follows 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"follows\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-eve\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-eve\",\n      \"handle\": \"eve.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-dan\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-dan\",\n      \"handle\": \"dan.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(3)\",\n        \"following\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-carol\",\n      \"did\": \"user(4)\",\n      \"displayName\": \"display-carol\",\n      \"handle\": \"carol.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(5)\",\n        \"following\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-bob\",\n      \"did\": \"user(6)\",\n      \"displayName\": \"display-bob\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(7)\",\n        \"following\": \"record(6)\",\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(9)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-alice\",\n    \"did\": \"user(8)\",\n    \"displayName\": \"display-alice\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches follows 2`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"follows\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-carol\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-carol\",\n      \"handle\": \"carol.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-bob\",\n    \"did\": \"user(4)\",\n    \"displayName\": \"display-bob\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(3)\",\n      \"following\": \"record(2)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches follows 3`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"follows\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-carol\",\n    \"did\": \"user(2)\",\n    \"displayName\": \"display-carol\",\n    \"handle\": \"carol.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(1)\",\n      \"following\": \"record(0)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches follows 4`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"follows\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-eve\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-eve\",\n      \"handle\": \"eve.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-bob\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-bob\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(3)\",\n        \"following\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(4)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-dan\",\n    \"did\": \"user(6)\",\n    \"displayName\": \"display-dan\",\n    \"handle\": \"dan.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(5)\",\n      \"following\": \"record(4)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches follows 5`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"follows\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-carol\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"display-carol\",\n      \"handle\": \"carol.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"descript-alice\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"display-alice\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"descript-eve\",\n    \"did\": \"user(4)\",\n    \"displayName\": \"display-eve\",\n    \"handle\": \"eve.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(3)\",\n      \"following\": \"record(2)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`pds follow views fetches relationships between users 1`] = `\nObject {\n  \"actor\": \"user(0)\",\n  \"relationships\": Array [\n    Object {\n      \"$type\": \"app.bsky.graph.defs#relationship\",\n      \"did\": \"user(1)\",\n      \"followedBy\": \"record(1)\",\n      \"following\": \"record(0)\",\n    },\n    Object {\n      \"$type\": \"app.bsky.graph.defs#relationship\",\n      \"did\": \"user(0)\",\n    },\n    Object {\n      \"$type\": \"app.bsky.graph.defs#relationship\",\n      \"did\": \"user(2)\",\n      \"following\": \"record(2)\",\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/labeler-service.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`labeler service views fetches labelers 1`] = `\nObject {\n  \"views\": Array [\n    Object {\n      \"$type\": \"app.bsky.labeler.defs#labelerView\",\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"labeler\": true,\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"like\": \"record(4)\",\n      },\n    },\n    Object {\n      \"$type\": \"app.bsky.labeler.defs#labelerView\",\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"labeler\": true,\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {},\n    },\n  ],\n}\n`;\n\nexports[`labeler service views fetches labelers detailed 1`] = `\nObject {\n  \"views\": Array [\n    Object {\n      \"$type\": \"app.bsky.labeler.defs#labelerViewDetailed\",\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"labeler\": true,\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 1,\n      \"policies\": Object {\n        \"labelValues\": Array [\n          \"spam\",\n          \"!hide\",\n          \"scam\",\n          \"impersonation\",\n        ],\n      },\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"like\": \"record(4)\",\n      },\n    },\n    Object {\n      \"$type\": \"app.bsky.labeler.defs#labelerViewDetailed\",\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"labeler\": true,\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"policies\": Object {\n        \"labelValues\": Array [\n          \"nudity\",\n          \"sexual\",\n          \"porn\",\n        ],\n      },\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {},\n    },\n  ],\n}\n`;\n\nexports[`labeler service views returns additional labeler data 1`] = `\nObject {\n  \"views\": Array [\n    Object {\n      \"$type\": \"app.bsky.labeler.defs#labelerViewDetailed\",\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"labeler\": true,\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"policies\": Object {\n        \"labelValues\": Array [\n          \"spam\",\n          \"!hide\",\n          \"scam\",\n          \"impersonation\",\n        ],\n      },\n      \"reasonTypes\": Array [\n        \"com.atproto.moderation.defs#reasonOther\",\n      ],\n      \"subjectCollections\": Array [\n        \"app.bsky.feed.post\",\n      ],\n      \"subjectTypes\": Array [\n        \"record\",\n      ],\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {},\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/likes.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds like views fetches post likes 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"likes\": Array [\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"eve.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(1)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(0)\",\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(0)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(4)\",\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  ],\n  \"uri\": \"record(5)\",\n}\n`;\n\nexports[`pds like views fetches reply likes 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"likes\": Array [\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"following\": \"record(0)\",\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  ],\n  \"uri\": \"record(2)\",\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`list feed views fetches list feed 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(5)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(4)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(4)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(5)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(5)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(5)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(6)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(4)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(7)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(8)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(10)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(5)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(9)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(10)\",\n                        \"uri\": \"record(11)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(9)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(8)\",\n                \"uri\": \"record(10)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(6)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(8)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(7)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(8)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(12)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"text\": \"bobby boy here\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(6)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(11)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(12)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(12)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(14)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(14)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/lists.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`bsky actor likes feed views does include users with creator block relationship in reference lists for creator 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"frankie.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": true,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(3)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(2)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does include users with creator block relationship in reference lists for creator 2`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"frankie.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": true,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(3)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(2)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include reference lists in getActorLists 1`] = `\nArray [\n  Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"mod2\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"cid\": \"cids(1)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"mod1\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(1)\",\n  },\n  Object {\n    \"cid\": \"cids(2)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 2,\n    \"name\": \"mod0\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(2)\",\n  },\n  Object {\n    \"cid\": \"cids(3)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"cur2\",\n    \"purpose\": \"app.bsky.graph.defs#curatelist\",\n    \"uri\": \"record(3)\",\n  },\n  Object {\n    \"cid\": \"cids(4)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"cur1\",\n    \"purpose\": \"app.bsky.graph.defs#curatelist\",\n    \"uri\": \"record(4)\",\n  },\n  Object {\n    \"cid\": \"cids(5)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"cur0\",\n    \"purpose\": \"app.bsky.graph.defs#curatelist\",\n    \"uri\": \"record(5)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include users with creator block relationship in reference and curate lists for signed-out viewers 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include users with creator block relationship in reference and curate lists for signed-out viewers 2`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, in-list viewers 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"blocking\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, in-list viewers 2`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"blocking\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, not-in-list viewers 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does not include users with creator block relationship in reference lists for non-creator, not-in-list viewers 2`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views does return all users regardless of creator block relationship in moderation lists 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"greta.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"frankie.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(1)\",\n  },\n]\n`;\n\nexports[`bsky actor likes feed views supports using a handle as getList actor param 1`] = `\nArray [\n  Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"mod2\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"cid\": \"cids(1)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"mod1\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(1)\",\n  },\n  Object {\n    \"cid\": \"cids(2)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 2,\n    \"name\": \"mod0\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(2)\",\n  },\n  Object {\n    \"cid\": \"cids(3)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"cur2\",\n    \"purpose\": \"app.bsky.graph.defs#curatelist\",\n    \"uri\": \"record(3)\",\n  },\n  Object {\n    \"cid\": \"cids(4)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 0,\n    \"name\": \"cur1\",\n    \"purpose\": \"app.bsky.graph.defs#curatelist\",\n    \"uri\": \"record(4)\",\n  },\n  Object {\n    \"cid\": \"cids(5)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"cur0\",\n    \"purpose\": \"app.bsky.graph.defs#curatelist\",\n    \"uri\": \"record(5)\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`bsky views with mutes from mute lists embeds lists in posts 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.record#view\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.defs#listView\",\n      \"cid\": \"cids(4)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"description\": \"new descript\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 3,\n      \"name\": \"updated alice mutes\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.record\",\n      \"record\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"text\": \"list embed!\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n  \"viewer\": Object {\n    \"bookmarked\": false,\n    \"embeddingDisabled\": false,\n    \"threadMuted\": false,\n  },\n}\n`;\n\nexports[`bsky views with mutes from mute lists flags mutes in threads 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(4)\",\n      \"repost\": \"record(3)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": true,\n            \"mutedByList\": Object {\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n              \"cid\": \"cids(4)\",\n              \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n              \"labels\": Array [],\n              \"listItemCount\": 3,\n              \"name\": \"alice mutes\",\n              \"purpose\": \"app.bsky.graph.defs#modlist\",\n              \"uri\": \"record(6)\",\n              \"viewer\": Object {\n                \"muted\": true,\n              },\n            },\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"of course\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {},\n    },\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(10)\",\n            \"muted\": true,\n            \"mutedByList\": Object {\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n              \"cid\": \"cids(4)\",\n              \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n              \"labels\": Array [],\n              \"listItemCount\": 3,\n              \"name\": \"alice mutes\",\n              \"purpose\": \"app.bsky.graph.defs#modlist\",\n              \"uri\": \"record(6)\",\n              \"viewer\": Object {\n                \"muted\": true,\n              },\n            },\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(5)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(5)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(9)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(5)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(9)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {},\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`bsky views with mutes from mute lists returns a users own list mutes 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"blah blah\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 0,\n      \"name\": \"new list\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"muted\": true,\n      },\n    },\n    Object {\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"big list of mutes\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 3,\n      \"name\": \"alice mutes\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"muted\": true,\n      },\n    },\n  ],\n}\n`;\n\nexports[`bsky views with mutes from mute lists returns lists associated with a user 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"blah blah\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 0,\n      \"name\": \"new list\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"big list of mutes\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 3,\n      \"name\": \"alice mutes\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"muted\": true,\n      },\n    },\n  ],\n}\n`;\n\nexports[`bsky views with mutes from mute lists returns the contents of a list 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"items\": Array [\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(3)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(5)\",\n          \"muted\": true,\n          \"mutedByList\": Object {\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n            \"cid\": \"cids(0)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"listItemCount\": 3,\n            \"name\": \"alice mutes\",\n            \"purpose\": \"app.bsky.graph.defs#modlist\",\n            \"uri\": \"record(0)\",\n            \"viewer\": Object {\n              \"muted\": true,\n            },\n          },\n        },\n      },\n      \"uri\": \"record(4)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(7)\",\n          \"muted\": true,\n          \"mutedByList\": Object {\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n            \"cid\": \"cids(0)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"listItemCount\": 3,\n            \"name\": \"alice mutes\",\n            \"purpose\": \"app.bsky.graph.defs#modlist\",\n            \"uri\": \"record(0)\",\n            \"viewer\": Object {\n              \"muted\": true,\n            },\n          },\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n  ],\n  \"list\": Object {\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(0)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"big list of mutes\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"alice mutes\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"muted\": true,\n    },\n  },\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`mute views fetches mutes for the logged-in user. 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"Dr. Lowell DuBuque\",\n    \"handle\": \"elta48.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(2)\",\n    \"displayName\": \"Sally Funk\",\n    \"handle\": \"magnus53.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(4)\",\n    \"handle\": \"nicolas-krajcik10.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(5)\",\n    \"displayName\": \"Patrick Sawayn\",\n    \"handle\": \"jeffrey-sawayn87.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(8)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(7)\",\n    \"displayName\": \"Kim Streich\",\n    \"handle\": \"adrienne49.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(10)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(9)\",\n    \"displayName\": \"Carlton Abernathy IV\",\n    \"handle\": \"aliya-hodkiewicz.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(11)\",\n    \"handle\": \"carol.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(0)\",\n      \"muted\": true,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(13)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(12)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(1)\",\n      \"muted\": true,\n    },\n  },\n]\n`;\n\nexports[`mute views flags mutes in threads 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(4)\",\n            \"following\": \"record(3)\",\n            \"muted\": true,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"of course\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {},\n    },\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": true,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {},\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`notification views does not generate self notifications for likes via own repost 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(4)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(5)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(5)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(5)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(5)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(3)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n]\n`;\n\nexports[`notification views does not generate self notifications for likes via own repost 2`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like-via-repost\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(4)\",\n        \"uri\": \"record(5)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n\nexports[`notification views fetches notifications omitting mentions and replies for taken-down posts 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(2)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(5)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(6)\",\n        \"following\": \"record(7)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(5)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"follow\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.follow\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": \"user(2)\",\n    },\n    \"uri\": \"record(6)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(6)\",\n        \"following\": \"record(7)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(6)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"reply\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(1)\",\n          \"uri\": \"record(2)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"indeed\",\n    },\n    \"uri\": \"record(8)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(6)\",\n        \"following\": \"record(7)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(7)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(10)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(8)\",\n        \"uri\": \"record(10)\",\n      },\n    },\n    \"uri\": \"record(9)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(6)\",\n        \"following\": \"record(7)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(9)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(11)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(11)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(12)\",\n        \"following\": \"record(13)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(10)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"follow\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.follow\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": \"user(2)\",\n    },\n    \"uri\": \"record(12)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(11)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(12)\",\n        \"following\": \"record(13)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(12)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(12)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"record(14)\",\n        \"val\": \"test-label\",\n      },\n      Object {\n        \"cid\": \"cids(12)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"record(14)\",\n        \"val\": \"test-label-2\",\n      },\n    ],\n    \"reason\": \"reply\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"image\": Object {\n              \"$type\": \"blob\",\n              \"mimeType\": \"image/jpeg\",\n              \"ref\": Object {\n                \"$link\": \"cids(13)\",\n              },\n              \"size\": 4114,\n            },\n          },\n        ],\n      },\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"hear that label_me label_me_2\",\n    },\n    \"uri\": \"record(14)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(11)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(12)\",\n        \"following\": \"record(13)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(14)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(10)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(8)\",\n        \"uri\": \"record(10)\",\n      },\n    },\n    \"uri\": \"record(15)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(11)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(12)\",\n        \"following\": \"record(13)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(15)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(16)\",\n  },\n]\n`;\n\nexports[`notification views fetches notifications with default priority 1`] = `\nObject {\n  \"cursor\": \"1970-01-01T00:00:00.000Z\",\n  \"notifications\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"repost-via-repost\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.repost\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"via\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"uri\": \"record(0)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like-via-repost\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"via\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like\",\n      \"reasonSubject\": \"record(8)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(7)\",\n          \"uri\": \"record(8)\",\n        },\n      },\n      \"uri\": \"record(7)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(8)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"follow\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.graph.follow\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": \"user(2)\",\n      },\n      \"uri\": \"record(2)\",\n    },\n  ],\n  \"priority\": true,\n  \"seenAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`notification views fetches notifications with explicit priority 1`] = `\nObject {\n  \"cursor\": \"1970-01-01T00:00:00.000Z\",\n  \"notifications\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [],\n      \"reason\": \"repost-via-repost\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.repost\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"via\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"uri\": \"record(0)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like-via-repost\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"via\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like\",\n      \"reasonSubject\": \"record(8)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(7)\",\n          \"uri\": \"record(8)\",\n        },\n      },\n      \"uri\": \"record(7)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(8)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"follow\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.graph.follow\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": \"user(2)\",\n      },\n      \"uri\": \"record(2)\",\n    },\n  ],\n  \"priority\": true,\n  \"seenAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`notification views fetches notifications with explicit priority 2`] = `\nObject {\n  \"cursor\": \"1970-01-01T00:00:00.000Z\",\n  \"notifications\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [],\n      \"reason\": \"repost-via-repost\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.repost\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"via\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"uri\": \"record(0)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like-via-repost\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"via\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like\",\n      \"reasonSubject\": \"record(9)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"subject\": Object {\n          \"cid\": \"cids(7)\",\n          \"uri\": \"record(9)\",\n        },\n      },\n      \"uri\": \"record(7)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(8)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"like\",\n      \"reasonSubject\": \"record(9)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.like\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"subject\": Object {\n          \"cid\": \"cids(7)\",\n          \"uri\": \"record(9)\",\n        },\n      },\n      \"uri\": \"record(10)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(4)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"quote\",\n      \"reasonSubject\": \"record(9)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(7)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"uri\": \"record(5)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(9)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"follow\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.graph.follow\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"subject\": \"user(5)\",\n      },\n      \"uri\": \"record(8)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": true,\n      \"labels\": Array [],\n      \"reason\": \"follow\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.graph.follow\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"subject\": \"user(5)\",\n      },\n      \"uri\": \"record(2)\",\n    },\n  ],\n  \"priority\": false,\n  \"seenAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`notification views fetches notifications without a last-seen 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(2)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"mention\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record\",\n        \"record\": Object {\n          \"cid\": \"cids(5)\",\n          \"uri\": \"record(6)\",\n        },\n      },\n      \"facets\": Array [\n        Object {\n          \"features\": Array [\n            Object {\n              \"$type\": \"app.bsky.richtext.facet#mention\",\n              \"did\": \"user(1)\",\n            },\n          ],\n          \"index\": Object {\n            \"byteEnd\": 18,\n            \"byteStart\": 0,\n          },\n        },\n      ],\n      \"text\": \"@alice.bluesky.xyz is the best\",\n    },\n    \"uri\": \"record(5)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(6)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(7)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(8)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(7)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"follow\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.follow\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": \"user(1)\",\n    },\n    \"uri\": \"record(8)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(8)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(8)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"reply\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(1)\",\n          \"uri\": \"record(2)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"indeed\",\n    },\n    \"uri\": \"record(10)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(8)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(9)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"reply\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"of course\",\n    },\n    \"uri\": \"record(11)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(8)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(10)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(13)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(11)\",\n        \"uri\": \"record(13)\",\n      },\n    },\n    \"uri\": \"record(12)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(8)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(12)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(14)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(14)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(15)\",\n        \"following\": \"record(16)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(13)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"follow\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.follow\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": \"user(1)\",\n    },\n    \"uri\": \"record(15)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(14)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(15)\",\n        \"following\": \"record(16)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(15)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(15)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"record(17)\",\n        \"val\": \"test-label\",\n      },\n      Object {\n        \"cid\": \"cids(15)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"record(17)\",\n        \"val\": \"test-label-2\",\n      },\n    ],\n    \"reason\": \"reply\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"image\": Object {\n              \"$type\": \"blob\",\n              \"mimeType\": \"image/jpeg\",\n              \"ref\": Object {\n                \"$link\": \"cids(16)\",\n              },\n              \"size\": 4114,\n            },\n          },\n        ],\n      },\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"hear that label_me label_me_2\",\n    },\n    \"uri\": \"record(17)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(14)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(15)\",\n        \"following\": \"record(16)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(17)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(13)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(11)\",\n        \"uri\": \"record(13)\",\n      },\n    },\n    \"uri\": \"record(18)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(14)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(3)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(15)\",\n        \"following\": \"record(16)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(18)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(19)\",\n  },\n]\n`;\n\nexports[`notification views filters notifications by multiple reasons 1`] = `\nObject {\n  \"notifications\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [],\n      \"reason\": \"reply\",\n      \"reasonSubject\": \"record(3)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(2)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"indeed\",\n      },\n      \"uri\": \"record(0)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [],\n      \"reason\": \"reply\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"uri\": \"record(5)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"following\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(6)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(6)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"reason\": \"reply\",\n      \"reasonSubject\": \"record(4)\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(6)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"uri\": \"record(6)\",\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(10)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [],\n      \"reason\": \"mention\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(11)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(4)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"uri\": \"record(9)\",\n    },\n  ],\n  \"priority\": false,\n  \"seenAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`notification views filters notifications by reason 1`] = `\nObject {\n  \"notifications\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"isRead\": false,\n      \"labels\": Array [],\n      \"reason\": \"mention\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(1)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"uri\": \"record(0)\",\n    },\n  ],\n  \"priority\": false,\n  \"seenAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`notification views generates notifications for likes 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(5)\",\n        \"following\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(2)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(6)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(6)\",\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(1)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(5)\",\n        \"following\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(7)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(6)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(10)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(5)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(6)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(6)\",\n      },\n    },\n    \"uri\": \"record(8)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(6)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(10)\",\n        \"following\": \"record(9)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(7)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(11)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for likes via repost 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like\",\n    \"reasonSubject\": \"record(3)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(4)\",\n        \"uri\": \"record(3)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for likes via repost 2`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"like-via-repost\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.like\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(4)\",\n        \"uri\": \"record(5)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for quotes 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(1)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(3)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(2)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"follow\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.follow\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": \"user(3)\",\n    },\n    \"uri\": \"record(2)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(3)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(5)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(5)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"record(4)\",\n        \"val\": \"test-label\",\n      },\n    ],\n    \"reason\": \"quote\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record\",\n        \"record\": Object {\n          \"cid\": \"cids(1)\",\n          \"uri\": \"record(1)\",\n        },\n      },\n      \"text\": \"yoohoo label_me\",\n    },\n    \"uri\": \"record(4)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for reposts 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(2)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(2)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for reposts via repost 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(1)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(3)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(4)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(4)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(3)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(2)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"repost\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(1)\",\n        \"uri\": \"record(1)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(0)\",\n        \"uri\": \"record(0)\",\n      },\n    },\n    \"uri\": \"record(2)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for reposts via repost 2`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"repost-via-repost\",\n    \"reasonSubject\": \"record(4)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.repost\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"subject\": Object {\n        \"cid\": \"cids(4)\",\n        \"uri\": \"record(5)\",\n      },\n      \"via\": Object {\n        \"cid\": \"cids(3)\",\n        \"uri\": \"record(4)\",\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for verification created and removed 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"verified\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.verification\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"subject\": \"user(2)\",\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n\nexports[`notification views generates notifications for verification created and removed 2`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"unverified\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.notification.defs#recordDeleted\",\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"verified\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.notification.defs#recordDeleted\",\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n\nexports[`notification views handles hide tag filters filters posts with hide tag 1`] = `Array []`;\n\nexports[`notification views handles hide tag filters shows posts with hide tag if they are followed 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"eve.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"reply\",\n    \"reasonSubject\": \"record(2)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(1)\",\n          \"uri\": \"record(2)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(1)\",\n          \"uri\": \"record(2)\",\n        },\n      },\n      \"text\": \"no thanks\",\n    },\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/posts.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds posts views embeds video with record. 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n    \"media\": Object {\n      \"$type\": \"app.bsky.embed.video#view\",\n      \"alt\": \"alt text\",\n      \"aspectRatio\": Object {\n        \"height\": 3,\n        \"width\": 4,\n      },\n      \"cid\": \"cids(3)\",\n      \"playlist\": \"https://bsky.public.url/vid/user(1)/cids(3)/playlist.m3u8\",\n      \"thumbnail\": \"https://bsky.public.url/vid/user(1)/cids(3)/thumbnail.jpg\",\n    },\n    \"record\": Object {\n      \"record\": Object {\n        \"$type\": \"app.bsky.embed.record#viewRecord\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n        \"cid\": \"cids(4)\",\n        \"embeds\": Array [],\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 1,\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(2)\",\n        \"value\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"text\": \"embedded\",\n        },\n      },\n    },\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.recordWithMedia\",\n      \"media\": Object {\n        \"$type\": \"app.bsky.embed.video\",\n        \"alt\": \"alt text\",\n        \"aspectRatio\": Object {\n          \"height\": 3,\n          \"width\": 4,\n        },\n        \"video\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/mp4\",\n          \"ref\": Object {\n            \"$link\": \"cids(3)\",\n          },\n          \"size\": 13,\n        },\n      },\n      \"record\": Object {\n        \"record\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(2)\",\n        },\n      },\n    },\n    \"text\": \"video\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`pds posts views embeds video. 1`] = `\nObject {\n  \"author\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n  },\n  \"bookmarkCount\": 0,\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.video#view\",\n    \"alt\": \"alt text\",\n    \"aspectRatio\": Object {\n      \"height\": 3,\n      \"width\": 4,\n    },\n    \"cid\": \"cids(3)\",\n    \"playlist\": \"https://bsky.public.url/vid/user(1)/cids(3)/playlist.m3u8\",\n    \"thumbnail\": \"https://bsky.public.url/vid/user(1)/cids(3)/thumbnail.jpg\",\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"likeCount\": 0,\n  \"quoteCount\": 0,\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.video\",\n      \"alt\": \"alt text\",\n      \"aspectRatio\": Object {\n        \"height\": 3,\n        \"width\": 4,\n      },\n      \"video\": Object {\n        \"$type\": \"blob\",\n        \"mimeType\": \"image/mp4\",\n        \"ref\": Object {\n          \"$link\": \"cids(3)\",\n        },\n        \"size\": 13,\n      },\n    },\n    \"text\": \"video\",\n  },\n  \"replyCount\": 0,\n  \"repostCount\": 0,\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`pds posts views fetches posts 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(0)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(0)\",\n        \"val\": \"self-label\",\n      },\n    ],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Object {\n        \"$type\": \"com.atproto.label.defs#selfLabels\",\n        \"values\": Array [\n          Object {\n            \"val\": \"self-label\",\n          },\n        ],\n      },\n      \"text\": \"hey there\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(3)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(2)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(5)\",\n        \"following\": \"record(4)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 1,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"langs\": Array [\n        \"en-US\",\n        \"i-klingon\",\n      ],\n      \"text\": \"bob back at it again!\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(3)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(4)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(8)\",\n        \"following\": \"record(7)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(5)\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n      \"media\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)\",\n          },\n          Object {\n            \"alt\": \"../dev-env/assets/key-alt.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(7)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(7)\",\n          },\n        ],\n      },\n      \"record\": Object {\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(2)\",\n            \"displayName\": \"bobby\",\n            \"handle\": \"bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(5)\",\n              \"following\": \"record(4)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(4)\",\n          \"embeds\": Array [],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(3)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"langs\": Array [\n              \"en-US\",\n              \"i-klingon\",\n            ],\n            \"text\": \"bob back at it again!\",\n          },\n        },\n      },\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 2,\n    \"quoteCount\": 1,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(6)\",\n                },\n                \"size\": 4114,\n              },\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(7)\",\n                },\n                \"size\": 12736,\n              },\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n        },\n      },\n      \"text\": \"hi im carol\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(6)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(9)\",\n      \"threadMuted\": false,\n    },\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"chat\": Object {\n          \"allowIncoming\": \"none\",\n        },\n      },\n      \"did\": \"user(6)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(11)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(8)\",\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.record#view\",\n      \"record\": Object {\n        \"$type\": \"app.bsky.embed.record#viewRecord\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(4)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"cid\": \"cids(5)\",\n        \"embeds\": Array [\n          Object {\n            \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n            \"media\": Object {\n              \"$type\": \"app.bsky.embed.images#view\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)\",\n                  \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)\",\n                },\n                Object {\n                  \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                  \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(7)\",\n                  \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(7)\",\n                },\n              ],\n            },\n            \"record\": Object {\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"did\": \"user(2)\",\n                  \"displayName\": \"bobby\",\n                  \"handle\": \"bob.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(5)\",\n                    \"following\": \"record(4)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(4)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [],\n                \"likeCount\": 0,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(3)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"langs\": Array [\n                    \"en-US\",\n                    \"i-klingon\",\n                  ],\n                  \"text\": \"bob back at it again!\",\n                },\n              },\n            },\n          },\n        ],\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 2,\n        \"quoteCount\": 1,\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(6)\",\n        \"value\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.recordWithMedia\",\n            \"media\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(6)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n                Object {\n                  \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(7)\",\n                    },\n                    \"size\": 12736,\n                  },\n                },\n              ],\n            },\n            \"record\": Object {\n              \"record\": Object {\n                \"cid\": \"cids(4)\",\n                \"uri\": \"record(3)\",\n              },\n            },\n          },\n          \"text\": \"hi im carol\",\n        },\n      },\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 1,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record\",\n        \"record\": Object {\n          \"cid\": \"cids(5)\",\n          \"uri\": \"record(6)\",\n        },\n      },\n      \"facets\": Array [\n        Object {\n          \"features\": Array [\n            Object {\n              \"$type\": \"app.bsky.richtext.facet#mention\",\n              \"did\": \"user(0)\",\n            },\n          ],\n          \"index\": Object {\n            \"byteEnd\": 18,\n            \"byteStart\": 0,\n          },\n        },\n      ],\n      \"text\": \"@alice.bluesky.xyz is the best\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 1,\n    \"uri\": \"record(10)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(9)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(10)\",\n          \"uri\": \"record(13)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(2)\",\n        },\n      },\n      \"text\": \"thanks bob\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 1,\n    \"uri\": \"record(12)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/profile.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds profile views fetches multiple profiles 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"its me!\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"followersCount\": 2,\n    \"followsCount\": 3,\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"postsCount\": 4,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(1)\",\n      \"following\": \"record(0)\",\n      \"knownFollowers\": Object {\n        \"count\": 1,\n        \"followers\": Array [\n          Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(2)\",\n              \"muted\": false,\n            },\n          },\n        ],\n      },\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(3)\",\n    \"displayName\": \"bobby\",\n    \"followersCount\": 2,\n    \"followsCount\": 2,\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"postsCount\": 3,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"knownFollowers\": Object {\n        \"count\": 1,\n        \"followers\": Array [\n          Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(1)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(3)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(1)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(3)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(1)\",\n              \"following\": \"record(0)\",\n              \"muted\": false,\n            },\n          },\n        ],\n      },\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"did\": \"user(2)\",\n    \"followersCount\": 2,\n    \"followsCount\": 1,\n    \"handle\": \"carol.test\",\n    \"labels\": Array [],\n    \"postsCount\": 2,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"following\": \"record(2)\",\n      \"knownFollowers\": Object {\n        \"count\": 1,\n        \"followers\": Array [\n          Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(1)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(3)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(1)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(3)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(1)\",\n              \"following\": \"record(0)\",\n              \"muted\": false,\n            },\n          },\n        ],\n      },\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"chat\": Object {\n        \"allowIncoming\": \"none\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"did\": \"user(5)\",\n    \"followersCount\": 1,\n    \"followsCount\": 1,\n    \"handle\": \"dan.test\",\n    \"labels\": Array [],\n    \"postsCount\": 2,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(4)\",\n      \"knownFollowers\": Object {\n        \"count\": 1,\n        \"followers\": Array [\n          Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(1)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(3)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(1)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(3)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(1)\",\n              \"following\": \"record(0)\",\n              \"muted\": false,\n            },\n          },\n        ],\n      },\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(7)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"It's me, eve\",\n    \"did\": \"user(6)\",\n    \"displayName\": \"eve\",\n    \"followersCount\": 0,\n    \"followsCount\": 0,\n    \"handle\": \"eve.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"postsCount\": 0,\n    \"pronouns\": \"They/them\",\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(9)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"It's me, frank\",\n    \"did\": \"user(8)\",\n    \"displayName\": \"frank\",\n    \"followersCount\": 0,\n    \"followsCount\": 0,\n    \"handle\": \"frank.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"postsCount\": 0,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n    \"website\": \"https://frank.example.com\",\n  },\n]\n`;\n\nexports[`pds profile views fetches other's profile, with a follow 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"description\": \"its me!\",\n  \"did\": \"user(0)\",\n  \"displayName\": \"ali\",\n  \"followersCount\": 2,\n  \"followsCount\": 3,\n  \"handle\": \"alice.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [\n    Object {\n      \"cid\": \"cids(1)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(0)\",\n      \"uri\": \"record(3)\",\n      \"val\": \"self-label-a\",\n    },\n    Object {\n      \"cid\": \"cids(1)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(0)\",\n      \"uri\": \"record(3)\",\n      \"val\": \"self-label-b\",\n    },\n  ],\n  \"postsCount\": 4,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"followedBy\": \"record(1)\",\n    \"following\": \"record(0)\",\n    \"knownFollowers\": Object {\n      \"count\": 1,\n      \"followers\": Array [\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(2)\",\n            \"muted\": false,\n          },\n        },\n      ],\n    },\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`pds profile views fetches other's profile, without a follow 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"chat\": Object {\n      \"allowIncoming\": \"none\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"did\": \"user(0)\",\n  \"followersCount\": 1,\n  \"followsCount\": 1,\n  \"handle\": \"dan.test\",\n  \"labels\": Array [],\n  \"postsCount\": 2,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"followedBy\": \"record(0)\",\n    \"knownFollowers\": Object {\n      \"count\": 1,\n      \"followers\": Array [\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n      ],\n    },\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`pds profile views fetches own profile 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"description\": \"its me!\",\n  \"did\": \"user(0)\",\n  \"displayName\": \"ali\",\n  \"followersCount\": 2,\n  \"followsCount\": 3,\n  \"handle\": \"alice.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [\n    Object {\n      \"cid\": \"cids(1)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(0)\",\n      \"uri\": \"record(4)\",\n      \"val\": \"self-label-a\",\n    },\n    Object {\n      \"cid\": \"cids(1)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(0)\",\n      \"uri\": \"record(4)\",\n      \"val\": \"self-label-b\",\n    },\n  ],\n  \"postsCount\": 4,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"knownFollowers\": Object {\n      \"count\": 2,\n      \"followers\": Array [\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(1)\",\n            \"following\": \"record(0)\",\n            \"muted\": false,\n          },\n        },\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(4)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(3)\",\n            \"following\": \"record(2)\",\n            \"muted\": false,\n          },\n        },\n      ],\n    },\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`pds profile views germ returns germ record if it does exist 1`] = `\nObject {\n  \"messageMeUrl\": \"https://chat.example.com/start-conversation\",\n  \"showButtonTo\": \"everyone\",\n}\n`;\n\nexports[`pds profile views presents avatars & banners 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n  \"banner\": \"https://bsky.public.url/img/banner/plain/user(1)/cids(1)\",\n  \"description\": \"new descript\",\n  \"did\": \"user(0)\",\n  \"displayName\": \"ali\",\n  \"followersCount\": 2,\n  \"followsCount\": 3,\n  \"handle\": \"alice.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"postsCount\": 4,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"knownFollowers\": Object {\n      \"count\": 2,\n      \"followers\": Array [\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(1)\",\n            \"following\": \"record(0)\",\n            \"muted\": false,\n          },\n        },\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(4)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(3)\",\n            \"following\": \"record(2)\",\n            \"muted\": false,\n          },\n        },\n      ],\n    },\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`pds profile views returns empty profile for actor that exists but has no profile 1`] = `\nObject {\n  \"profiles\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"followersCount\": 2,\n      \"followsCount\": 2,\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 3,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"knownFollowers\": Object {\n          \"count\": 1,\n          \"followers\": Array [\n            Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n                \"chat\": Object {\n                  \"allowIncoming\": \"none\",\n                },\n              },\n              \"did\": \"user(2)\",\n              \"handle\": \"dan.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"following\": \"record(2)\",\n                \"muted\": false,\n              },\n            },\n          ],\n        },\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"did\": \"user(3)\",\n      \"followersCount\": 0,\n      \"followsCount\": 0,\n      \"handle\": \"noprofile.test\",\n      \"labels\": Array [],\n      \"postsCount\": 0,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`pds profile views returns empty profile if actor exists but has no profile 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"did\": \"user(0)\",\n  \"followersCount\": 0,\n  \"followsCount\": 0,\n  \"handle\": \"noprofile.test\",\n  \"labels\": Array [],\n  \"postsCount\": 0,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`pds profile views status returns active status when within the duration 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.external#view\",\n    \"external\": Object {\n      \"description\": \"testLink\",\n      \"title\": \"TestImage\",\n      \"uri\": \"https://example.com\",\n    },\n  },\n  \"expiresAt\": \"1970-01-01T00:00:00.000Z\",\n  \"isActive\": true,\n  \"isDisabled\": false,\n  \"record\": Object {\n    \"$type\": \"app.bsky.actor.status\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"durationMinutes\": 10,\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.external\",\n      \"external\": Object {\n        \"description\": \"testLink\",\n        \"title\": \"TestImage\",\n        \"uri\": \"https://example.com\",\n      },\n    },\n    \"status\": \"app.bsky.actor.status#live\",\n  },\n  \"status\": \"app.bsky.actor.status#live\",\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`pds profile views status when outside the duration returns inactive status 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.external#view\",\n    \"external\": Object {\n      \"description\": \"testLink\",\n      \"title\": \"TestImage\",\n      \"uri\": \"https://example.com\",\n    },\n  },\n  \"expiresAt\": \"1970-01-01T00:00:00.000Z\",\n  \"isActive\": false,\n  \"isDisabled\": false,\n  \"record\": Object {\n    \"$type\": \"app.bsky.actor.status\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"durationMinutes\": 10,\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.external\",\n      \"external\": Object {\n        \"description\": \"testLink\",\n        \"title\": \"TestImage\",\n        \"uri\": \"https://example.com\",\n      },\n    },\n    \"status\": \"app.bsky.actor.status#live\",\n  },\n  \"status\": \"app.bsky.actor.status#live\",\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`pds profile views status when taken down it returns the live status with isDisabled=true for status owner 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"embed\": Object {\n    \"$type\": \"app.bsky.embed.external#view\",\n    \"external\": Object {\n      \"description\": \"testLink\",\n      \"title\": \"TestImage\",\n      \"uri\": \"https://example.com\",\n    },\n  },\n  \"expiresAt\": \"1970-01-01T00:00:00.000Z\",\n  \"isActive\": true,\n  \"isDisabled\": true,\n  \"record\": Object {\n    \"$type\": \"app.bsky.actor.status\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"durationMinutes\": 10,\n    \"embed\": Object {\n      \"$type\": \"app.bsky.embed.external\",\n      \"external\": Object {\n        \"description\": \"testLink\",\n        \"title\": \"TestImage\",\n        \"uri\": \"https://example.com\",\n      },\n    },\n    \"status\": \"app.bsky.actor.status#live\",\n  },\n  \"status\": \"app.bsky.actor.status#live\",\n  \"uri\": \"record(0)\",\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/quotes.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds quote views decrements quote count when a quote is deleted 1`] = `\nObject {\n  \"posts\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(0)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(0)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(2)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(1)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`pds quote views does not return post when quote is deleted 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"posts\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"eve.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(2)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(1)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(3)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(1)\",\n                \"uri\": \"record(2)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(3)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(1)\",\n                \"uri\": \"record(2)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(1)\",\n          \"embeds\": Array [],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label\",\n            },\n          ],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Object {\n              \"$type\": \"com.atproto.label.defs#selfLabels\",\n              \"values\": Array [\n                Object {\n                  \"val\": \"self-label\",\n                },\n              ],\n            },\n            \"text\": \"hey there\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"text\": \"qUoTe 2\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  ],\n  \"uri\": \"record(1)\",\n}\n`;\n\nexports[`pds quote views fetches post quotes 1`] = `\nObject {\n  \"cursor\": \"0000000000000__bafycid\",\n  \"posts\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"eve.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(2)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(1)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(3)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(1)\",\n                \"uri\": \"record(2)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(3)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(1)\",\n                \"uri\": \"record(2)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(1)\",\n          \"embeds\": Array [],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label\",\n            },\n          ],\n          \"likeCount\": 0,\n          \"quoteCount\": 2,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Object {\n              \"$type\": \"com.atproto.label.defs#selfLabels\",\n              \"values\": Array [\n                Object {\n                  \"val\": \"self-label\",\n                },\n              ],\n            },\n            \"text\": \"hey there\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"text\": \"qUoTe 2\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"eve.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(2)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(1)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(3)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(1)\",\n                \"uri\": \"record(2)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(3)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(1)\",\n                \"uri\": \"record(2)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(1)\",\n          \"embeds\": Array [],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label\",\n            },\n          ],\n          \"likeCount\": 0,\n          \"quoteCount\": 2,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Object {\n              \"$type\": \"com.atproto.label.defs#selfLabels\",\n              \"values\": Array [\n                Object {\n                  \"val\": \"self-label\",\n                },\n              ],\n            },\n            \"text\": \"hey there\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"text\": \"qUoTe 1\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  ],\n  \"uri\": \"record(1)\",\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/reposts.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`pds repost views fetches reposted-by for a post 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(0)\",\n    \"handle\": \"eve.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"chat\": Object {\n        \"allowIncoming\": \"none\",\n      },\n    },\n    \"did\": \"user(1)\",\n    \"handle\": \"dan.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"following\": \"record(0)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(2)\",\n    \"handle\": \"carol.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(2)\",\n      \"following\": \"record(1)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(3)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(4)\",\n      \"following\": \"record(3)\",\n      \"muted\": false,\n    },\n  },\n]\n`;\n\nexports[`pds repost views fetches reposted-by for a reply 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(0)\",\n    \"handle\": \"eve.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"chat\": Object {\n        \"allowIncoming\": \"none\",\n      },\n    },\n    \"did\": \"user(1)\",\n    \"handle\": \"dan.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"following\": \"record(0)\",\n      \"muted\": false,\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/starter-packs.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`starter packs does include users with creator block relationship in list sample for creator 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"greta.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(2)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(3)\",\n      \"handle\": \"frankie.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": true,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n]\n`;\n\nexports[`starter packs does not include users with creator block relationship in list sample for non-creator, in-list viewers 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"blocking\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"greta.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n]\n`;\n\nexports[`starter packs does not include users with creator block relationship in list sample for non-creator, not-in-list viewers 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"greta.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"uri\": \"record(4)\",\n  },\n]\n`;\n\nexports[`starter packs does not include users with creator block relationship in list sample for signed-out viewers 1`] = `\nArray [\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"subject\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(2)\",\n      \"handle\": \"greta.test\",\n      \"labels\": Array [],\n    },\n    \"uri\": \"record(2)\",\n  },\n]\n`;\n\nexports[`starter packs generates notifications 1`] = `\nArray [\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"New here\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"Newskie 3\",\n      \"handle\": \"newskie3.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": false,\n    \"labels\": Array [],\n    \"reason\": \"starterpack-joined\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.actor.profile\",\n      \"avatar\": Object {\n        \"$type\": \"blob\",\n        \"mimeType\": \"image/jpeg\",\n        \"ref\": Object {\n          \"$link\": \"cids(1)\",\n        },\n        \"size\": 3976,\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"New here\",\n      \"displayName\": \"Newskie 3\",\n      \"joinedViaStarterPack\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n      \"labels\": Object {\n        \"$type\": \"com.atproto.label.defs#selfLabels\",\n        \"values\": Array [],\n      },\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"New here\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"Newskie 2\",\n      \"handle\": \"newskie2.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(3)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"starterpack-joined\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.actor.profile\",\n      \"avatar\": Object {\n        \"$type\": \"blob\",\n        \"mimeType\": \"image/jpeg\",\n        \"ref\": Object {\n          \"$link\": \"cids(1)\",\n        },\n        \"size\": 3976,\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"New here\",\n      \"displayName\": \"Newskie 2\",\n      \"joinedViaStarterPack\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n      \"labels\": Object {\n        \"$type\": \"com.atproto.label.defs#selfLabels\",\n        \"values\": Array [],\n      },\n    },\n    \"uri\": \"record(2)\",\n  },\n  Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"New here\",\n      \"did\": \"user(4)\",\n      \"displayName\": \"Newskie 1\",\n      \"handle\": \"newskie1.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"isRead\": true,\n    \"labels\": Array [],\n    \"reason\": \"starterpack-joined\",\n    \"reasonSubject\": \"record(1)\",\n    \"record\": Object {\n      \"$type\": \"app.bsky.actor.profile\",\n      \"avatar\": Object {\n        \"$type\": \"blob\",\n        \"mimeType\": \"image/jpeg\",\n        \"ref\": Object {\n          \"$link\": \"cids(1)\",\n        },\n        \"size\": 3976,\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"New here\",\n      \"displayName\": \"Newskie 1\",\n      \"joinedViaStarterPack\": Object {\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n      \"labels\": Object {\n        \"$type\": \"com.atproto.label.defs#selfLabels\",\n        \"values\": Array [],\n      },\n    },\n    \"uri\": \"record(3)\",\n  },\n]\n`;\n\nexports[`starter packs gets actor starter packs. 1`] = `\nArray [\n  Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"joinedAllTimeCount\": 0,\n    \"joinedWeekCount\": 0,\n    \"labels\": Array [],\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.starterpack\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"feeds\": Array [],\n      \"list\": \"record(1)\",\n      \"name\": \"alice's about to get blocked starter pack\",\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"cid\": \"cids(3)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"joinedAllTimeCount\": 0,\n    \"joinedWeekCount\": 0,\n    \"labels\": Array [],\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.starterpack\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"feeds\": Array [],\n      \"list\": \"record(4)\",\n      \"name\": \"alice's empty starter pack\",\n    },\n    \"uri\": \"record(3)\",\n  },\n  Object {\n    \"cid\": \"cids(4)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"joinedAllTimeCount\": 3,\n    \"joinedWeekCount\": 3,\n    \"labels\": Array [],\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.starterpack\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"feeds\": Array [\n        Object {\n          \"uri\": \"record(7)\",\n        },\n      ],\n      \"list\": \"record(6)\",\n      \"name\": \"alice's starter pack\",\n    },\n    \"uri\": \"record(5)\",\n  },\n]\n`;\n\nexports[`starter packs gets starter pack details 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"creator\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n  },\n  \"feeds\": Array [\n    Object {\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n      },\n      \"did\": \"did:web:example.com\",\n      \"displayName\": \"alice's feedgen\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(2)\",\n    },\n  ],\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"joinedAllTimeCount\": 3,\n  \"joinedWeekCount\": 3,\n  \"labels\": Array [],\n  \"list\": Object {\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"n/a\",\n    \"purpose\": \"app.bsky.graph.defs#referencelist\",\n    \"uri\": \"record(1)\",\n  },\n  \"listItemsSample\": Array [\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n      },\n      \"uri\": \"record(4)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n      },\n      \"uri\": \"record(5)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(5)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n      },\n      \"uri\": \"record(6)\",\n    },\n  ],\n  \"record\": Object {\n    \"$type\": \"app.bsky.graph.starterpack\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"feeds\": Array [\n      Object {\n        \"uri\": \"record(2)\",\n      },\n    ],\n    \"list\": \"record(1)\",\n    \"name\": \"alice's starter pack\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`starter packs gets starter pack details 2`] = `\nArray [\n  Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"joinedAllTimeCount\": 0,\n    \"joinedWeekCount\": 0,\n    \"labels\": Array [],\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.starterpack\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"feeds\": Array [],\n      \"list\": \"record(1)\",\n      \"name\": \"alice's empty starter pack\",\n    },\n    \"uri\": \"record(0)\",\n  },\n  Object {\n    \"cid\": \"cids(3)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n    },\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"joinedAllTimeCount\": 3,\n    \"joinedWeekCount\": 3,\n    \"labels\": Array [],\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.starterpack\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"feeds\": Array [\n        Object {\n          \"uri\": \"record(5)\",\n        },\n      ],\n      \"list\": \"record(4)\",\n      \"name\": \"alice's starter pack\",\n    },\n    \"uri\": \"record(3)\",\n  },\n]\n`;\n\nexports[`starter packs gets starter pack used on profile detail 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"creator\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n  },\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"joinedAllTimeCount\": 3,\n  \"joinedWeekCount\": 3,\n  \"labels\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.graph.starterpack\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"feeds\": Array [\n      Object {\n        \"uri\": \"record(2)\",\n      },\n    ],\n    \"list\": \"record(1)\",\n    \"name\": \"alice's starter pack\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/thread-v2.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`appview thread views v2 simple thread returns thread anchored on 1 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": -1,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(1)\",\n            \"handle\": \"simple-alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"1\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`appview thread views v2 simple thread returns thread anchored on 2 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": -1,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(1)\",\n            \"handle\": \"simple-bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 1,\n      \"uri\": \"record(2)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"simple-alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(2)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(1)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"2.0\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`appview thread views v2 simple thread returns thread anchored on 2.0 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": -2,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": -1,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(1)\",\n            \"handle\": \"simple-bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(2)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"simple-alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(2)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(1)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"2.0\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`appview thread views v2 simple thread returns thread anchored on 3 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": -1,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(1)\",\n            \"handle\": \"simple-carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"3\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`appview thread views v2 simple thread returns thread anchored on r 0 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": -1,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"0\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 1,\n      \"uri\": \"record(2)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(2)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(1)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"0.0\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`appview thread views v2 simple thread returns thread anchored on r 0.0 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": -2,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": -1,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"0\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(2)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(2)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(1)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"0.0\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`appview thread views v2 simple thread returns thread anchored on root 1`] = `\nObject {\n  \"hasOtherReplies\": false,\n  \"thread\": Array [\n    Object {\n      \"depth\": 0,\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(0)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"text\": \"root\",\n          },\n          \"replyCount\": 4,\n          \"repostCount\": 0,\n          \"uri\": \"record(0)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 1,\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(1)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"0\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 2,\n      \"uri\": \"record(2)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": true,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"simple-op.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(2)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(1)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"0.0\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 1,\n      \"uri\": \"record(3)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(1)\",\n            \"handle\": \"simple-alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"1\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 1,\n      \"uri\": \"record(4)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"simple-bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(4)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(4)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 2,\n      \"uri\": \"record(5)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(1)\",\n            \"handle\": \"simple-alice.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(5)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(4)\",\n                \"uri\": \"record(4)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"2.0\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(5)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"depth\": 1,\n      \"uri\": \"record(6)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.unspecced.defs#threadItemPost\",\n        \"hiddenByThreadgate\": false,\n        \"moreParents\": false,\n        \"moreReplies\": 0,\n        \"mutedByViewer\": false,\n        \"opThread\": false,\n        \"post\": Object {\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(3)\",\n            \"handle\": \"simple-carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(6)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(0)\",\n                \"uri\": \"record(0)\",\n              },\n            },\n            \"text\": \"3\",\n          },\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(6)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/thread.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`appview thread views fetches ancestors 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"parent\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"parent\": Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(7)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {},\n    },\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(5)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(5)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 1,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(5)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(4)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"threadContext\": Object {\n      \"rootAuthorLike\": \"record(8)\",\n    },\n  },\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 1,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"thanks bob\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 2,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"repost\": \"record(6)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {\n    \"rootAuthorLike\": \"record(9)\",\n  },\n}\n`;\n\nexports[`appview thread views fetches deep post thread 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(4)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"of course\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"replies\": Array [],\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(7)\",\n      },\n    },\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(8)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(8)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(8)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"replies\": Array [\n        Object {\n          \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n          \"post\": Object {\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(0)\",\n              \"displayName\": \"ali\",\n              \"handle\": \"alice.test\",\n              \"labels\": Array [\n                Object {\n                  \"cid\": \"cids(2)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(0)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"cid\": \"cids(2)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(0)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-b\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(2)\",\n                \"following\": \"record(1)\",\n                \"muted\": false,\n              },\n            },\n            \"bookmarkCount\": 0,\n            \"cid\": \"cids(6)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 1,\n            \"quoteCount\": 0,\n            \"record\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"reply\": Object {\n                \"parent\": Object {\n                  \"cid\": \"cids(4)\",\n                  \"uri\": \"record(8)\",\n                },\n                \"root\": Object {\n                  \"cid\": \"cids(0)\",\n                  \"uri\": \"record(0)\",\n                },\n              },\n              \"text\": \"thanks bob\",\n            },\n            \"replyCount\": 0,\n            \"repostCount\": 2,\n            \"uri\": \"record(9)\",\n            \"viewer\": Object {\n              \"bookmarked\": false,\n              \"embeddingDisabled\": false,\n              \"repost\": \"record(10)\",\n              \"threadMuted\": false,\n            },\n          },\n          \"replies\": Array [],\n          \"threadContext\": Object {\n            \"rootAuthorLike\": \"record(11)\",\n          },\n        },\n      ],\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(12)\",\n      },\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views fetches shallow post thread 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(4)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"of course\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(7)\",\n      },\n    },\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(8)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(8)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(8)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(9)\",\n      },\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views fetches thread with handle in uri 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(4)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"of course\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(7)\",\n      },\n    },\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(8)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(8)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(8)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(9)\",\n      },\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views handles deleted posts correctly 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"text\": \"Deletion thread\",\n    },\n    \"replyCount\": 1,\n    \"repostCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"Reply\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"replies\": Array [\n        Object {\n          \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n          \"post\": Object {\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(0)\",\n              \"displayName\": \"ali\",\n              \"handle\": \"alice.test\",\n              \"labels\": Array [\n                Object {\n                  \"cid\": \"cids(2)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(0)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"cid\": \"cids(2)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(0)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-b\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(2)\",\n                \"following\": \"record(1)\",\n                \"muted\": false,\n              },\n            },\n            \"bookmarkCount\": 0,\n            \"cid\": \"cids(4)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 0,\n            \"record\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"reply\": Object {\n                \"parent\": Object {\n                  \"cid\": \"cids(3)\",\n                  \"uri\": \"record(4)\",\n                },\n                \"root\": Object {\n                  \"cid\": \"cids(0)\",\n                  \"uri\": \"record(0)\",\n                },\n              },\n              \"text\": \"Reply reply\",\n            },\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(5)\",\n            \"viewer\": Object {\n              \"bookmarked\": false,\n              \"embeddingDisabled\": false,\n              \"threadMuted\": false,\n            },\n          },\n          \"replies\": Array [],\n          \"threadContext\": Object {},\n        },\n      ],\n      \"threadContext\": Object {},\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views handles deleted posts correctly 2`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"text\": \"Deletion thread\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views handles deleted posts correctly 3`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"parent\": Object {\n    \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n    \"notFound\": true,\n    \"uri\": \"record(5)\",\n  },\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"Reply reply\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views listblock doesn't apply listblock if list was taken down by private takedown 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#blockedPost\",\n  \"author\": Object {\n    \"did\": \"user(0)\",\n    \"viewer\": Object {\n      \"blockedBy\": true,\n    },\n  },\n  \"blocked\": true,\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`appview thread views listblock doesn't apply listblock if list was taken down by private takedown 2`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"parent\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(2)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"I'm carol\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"threadContext\": Object {},\n  },\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(2)\",\n          \"uri\": \"record(2)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(2)\",\n          \"uri\": \"record(2)\",\n        },\n      },\n      \"text\": \"hi carol\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views listblock doesn't apply listblock if list was taken down by takedown label 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#blockedPost\",\n  \"author\": Object {\n    \"did\": \"user(0)\",\n    \"viewer\": Object {\n      \"blockedBy\": true,\n    },\n  },\n  \"blocked\": true,\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`appview thread views listblock doesn't apply listblock if list was taken down by takedown label 2`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"parent\": Object {\n    \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(2)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"I'm carol\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"threadContext\": Object {},\n  },\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(2)\",\n          \"uri\": \"record(2)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(2)\",\n          \"uri\": \"record(2)\",\n        },\n      },\n      \"text\": \"hi carol\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views takedown blocks ancestors by actor 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"parent\": Object {\n    \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n    \"notFound\": true,\n    \"uri\": \"record(5)\",\n  },\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 1,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"thanks bob\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 2,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"repost\": \"record(6)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {\n    \"rootAuthorLike\": \"record(7)\",\n  },\n}\n`;\n\nexports[`appview thread views takedown blocks ancestors by record 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"parent\": Object {\n    \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n    \"notFound\": true,\n    \"uri\": \"record(5)\",\n  },\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 1,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"reply\": Object {\n        \"parent\": Object {\n          \"cid\": \"cids(4)\",\n          \"uri\": \"record(5)\",\n        },\n        \"root\": Object {\n          \"cid\": \"cids(3)\",\n          \"uri\": \"record(4)\",\n        },\n      },\n      \"text\": \"thanks bob\",\n    },\n    \"replyCount\": 0,\n    \"repostCount\": 2,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"repost\": \"record(6)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [],\n  \"threadContext\": Object {\n    \"rootAuthorLike\": \"record(7)\",\n  },\n}\n`;\n\nexports[`appview thread views takedown blocks replies by actor 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(4)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(4)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"replies\": Array [\n        Object {\n          \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n          \"post\": Object {\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(0)\",\n              \"displayName\": \"ali\",\n              \"handle\": \"alice.test\",\n              \"labels\": Array [\n                Object {\n                  \"cid\": \"cids(2)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(0)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"cid\": \"cids(2)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(0)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-b\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(2)\",\n                \"following\": \"record(1)\",\n                \"muted\": false,\n              },\n            },\n            \"bookmarkCount\": 0,\n            \"cid\": \"cids(5)\",\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 1,\n            \"quoteCount\": 0,\n            \"record\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"reply\": Object {\n                \"parent\": Object {\n                  \"cid\": \"cids(3)\",\n                  \"uri\": \"record(5)\",\n                },\n                \"root\": Object {\n                  \"cid\": \"cids(0)\",\n                  \"uri\": \"record(0)\",\n                },\n              },\n              \"text\": \"thanks bob\",\n            },\n            \"replyCount\": 0,\n            \"repostCount\": 2,\n            \"uri\": \"record(6)\",\n            \"viewer\": Object {\n              \"bookmarked\": false,\n              \"embeddingDisabled\": false,\n              \"repost\": \"record(7)\",\n              \"threadMuted\": false,\n            },\n          },\n          \"replies\": Array [],\n          \"threadContext\": Object {\n            \"rootAuthorLike\": \"record(8)\",\n          },\n        },\n      ],\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(9)\",\n      },\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n\nexports[`appview thread views takedown blocks replies by record 1`] = `\nObject {\n  \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n  \"post\": Object {\n    \"author\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"bookmarkCount\": 0,\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 3,\n    \"quoteCount\": 0,\n    \"record\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n      \"text\": \"again\",\n    },\n    \"replyCount\": 2,\n    \"repostCount\": 1,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"bookmarked\": false,\n      \"embeddingDisabled\": false,\n      \"like\": \"record(4)\",\n      \"threadMuted\": false,\n    },\n  },\n  \"replies\": Array [\n    Object {\n      \"$type\": \"app.bsky.feed.defs#threadViewPost\",\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(3)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(5)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 1,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(4)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"replies\": Array [],\n      \"threadContext\": Object {\n        \"rootAuthorLike\": \"record(6)\",\n      },\n    },\n  ],\n  \"threadContext\": Object {},\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/threadgating.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`views with thread gating applies gate after root post is deleted. 1`] = `undefined`;\n\nexports[`views with thread gating applies gate for empty rules. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for follower rule. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#followerRule\",\n      },\n    ],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for following rule. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#followingRule\",\n      },\n    ],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for list rule. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(1)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 1,\n      \"name\": \"list a\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"cid\": \"cids(2)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 1,\n      \"name\": \"list b\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n  ],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#listRule\",\n        \"list\": \"record(2)\",\n      },\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#listRule\",\n        \"list\": \"record(3)\",\n      },\n    ],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for mention rule. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#mentionRule\",\n      },\n    ],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for missing rules, takes no action. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for multiple rules. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#mentionRule\",\n      },\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#followerRule\",\n      },\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#followingRule\",\n      },\n    ],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating applies gate for unknown list rule. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [\n      Object {\n        \"$type\": \"app.bsky.feed.threadgate#listRule\",\n        \"list\": \"record(1)\",\n      },\n    ],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`views with thread gating does not apply gate to original poster. 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"lists\": Array [],\n  \"record\": Object {\n    \"$type\": \"app.bsky.feed.threadgate\",\n    \"allow\": Array [],\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"post\": \"record(1)\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`timeline views blocks posts, reposts, replies by actor takedown 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(5)\",\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n        \"notFound\": true,\n        \"uri\": \"record(3)\",\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(6)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n        \"notFound\": true,\n        \"uri\": \"record(3)\",\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(4)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(8)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewNotFound\",\n                \"notFound\": true,\n                \"uri\": \"record(9)\",\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(8)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(9)\",\n                \"uri\": \"record(9)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(7)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(7)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(8)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(7)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewNotFound\",\n          \"notFound\": true,\n          \"uri\": \"record(9)\",\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(8)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"dan here!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(11)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(11)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(11)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`timeline views blocks posts, reposts, replies by record takedown. 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(5)\",\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n        \"notFound\": true,\n        \"uri\": \"record(3)\",\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(6)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#notFoundPost\",\n        \"notFound\": true,\n        \"uri\": \"record(3)\",\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(9)\",\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(7)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewNotFound\",\n          \"notFound\": true,\n          \"uri\": \"record(11)\",\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(8)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(10)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(11)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(14)\",\n          \"following\": \"record(13)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"text\": \"bobby boy here\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(12)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"dan here!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(15)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(9)\",\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(12)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(13)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(13)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(14)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(14)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(4)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(14)\",\n                \"following\": \"record(13)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(15)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(15)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"did:example:labeler\",\n                \"uri\": \"record(17)\",\n                \"val\": \"test-label-3\",\n              },\n            ],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(17)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(12)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(16)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(13)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(14)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(15)\",\n              \"uri\": \"record(17)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(16)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(18)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(14)\",\n          \"following\": \"record(13)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(15)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(15)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(17)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(17)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(16)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(16)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(19)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(19)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 1`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(5)\",\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(3)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(3)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(8)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(5)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(12)\",\n              \"following\": \"record(11)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(9)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(3)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(7)\",\n                      \"following\": \"record(6)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(11)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [\n                    Object {\n                      \"cid\": \"cids(11)\",\n                      \"cts\": \"1970-01-01T00:00:00.000Z\",\n                      \"src\": \"did:example:labeler\",\n                      \"uri\": \"record(13)\",\n                      \"val\": \"test-label-3\",\n                    },\n                  ],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(13)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(9)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(10)\",\n              \"val\": \"test-label-3\",\n            },\n          ],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(10)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(6)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(10)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(11)\",\n                  \"uri\": \"record(13)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(5)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(12)\",\n          \"following\": \"record(11)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(12)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(14)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(3)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(3)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(5)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(12)\",\n          \"following\": \"record(11)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(13)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(15)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(3)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(3)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(6)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(14)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(4)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(8)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(5)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"followedBy\": \"record(12)\",\n                    \"following\": \"record(11)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(9)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [\n                  Object {\n                    \"cid\": \"cids(9)\",\n                    \"cts\": \"1970-01-01T00:00:00.000Z\",\n                    \"src\": \"did:example:labeler\",\n                    \"uri\": \"record(10)\",\n                    \"val\": \"test-label-3\",\n                  },\n                ],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(10)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(6)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(10)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(11)\",\n                        \"uri\": \"record(13)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(9)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(9)\",\n                \"uri\": \"record(10)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(0)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(14)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(16)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(8)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(16)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(15)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"text\": \"bobby boy here\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(17)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(5)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(12)\",\n              \"following\": \"record(11)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(9)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(3)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(7)\",\n                      \"following\": \"record(6)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(11)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [\n                    Object {\n                      \"cid\": \"cids(11)\",\n                      \"cts\": \"1970-01-01T00:00:00.000Z\",\n                      \"src\": \"did:example:labeler\",\n                      \"uri\": \"record(13)\",\n                      \"val\": \"test-label-3\",\n                    },\n                  ],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(13)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(9)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(10)\",\n              \"val\": \"test-label-3\",\n            },\n          ],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(10)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(6)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(10)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(11)\",\n                  \"uri\": \"record(13)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(16)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"dan here!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(18)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(5)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(12)\",\n          \"following\": \"record(11)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(3)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(7)\",\n                \"following\": \"record(6)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(11)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(11)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"did:example:labeler\",\n                \"uri\": \"record(13)\",\n                \"val\": \"test-label-3\",\n              },\n            ],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(13)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(9)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(10)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(10)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(11)\",\n              \"uri\": \"record(13)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(19)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(11)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(13)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(17)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(17)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(20)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(20)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 2`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"following\": \"record(3)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(1)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(4)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(4)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [\n                    Object {\n                      \"cid\": \"cids(4)\",\n                      \"cts\": \"1970-01-01T00:00:00.000Z\",\n                      \"src\": \"did:example:labeler\",\n                      \"uri\": \"record(4)\",\n                      \"val\": \"test-label-3\",\n                    },\n                  ],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(4)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(2)\",\n              \"val\": \"test-label-3\",\n            },\n          ],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(2)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(2)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(3)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(4)\",\n                  \"uri\": \"record(4)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(2)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(1)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(5)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"following\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(10)\",\n            \"uri\": \"record(11)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(6)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"following\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(4)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(10)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(10)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(11)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(10)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(11)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(9)\",\n              \"uri\": \"record(10)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(9)\",\n              \"uri\": \"record(10)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(11)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(12)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(12)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(12)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(10)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(10)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(11)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(10)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(11)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(2)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(10)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(11)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(12)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(9)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(8)\",\n            \"following\": \"record(7)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(12)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"following\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(12)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(1)\",\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(0)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(2)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"following\": \"record(3)\",\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(1)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [\n                  Object {\n                    \"cid\": \"cids(1)\",\n                    \"cts\": \"1970-01-01T00:00:00.000Z\",\n                    \"src\": \"did:example:labeler\",\n                    \"uri\": \"record(2)\",\n                    \"val\": \"test-label-3\",\n                  },\n                ],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(2)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(2)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(3)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(4)\",\n                        \"uri\": \"record(4)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(0)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(2)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(1)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(12)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(14)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(0)\",\n            \"uri\": \"record(0)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(14)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(15)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(13)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"text\": \"bobby boy here\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(16)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"following\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(12)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(3)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(1)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(4)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(4)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"did:example:labeler\",\n                \"uri\": \"record(4)\",\n                \"val\": \"test-label-3\",\n              },\n            ],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(4)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(2)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(3)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(4)\",\n              \"uri\": \"record(4)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(17)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(4)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(9)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(8)\",\n          \"following\": \"record(7)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(14)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(14)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(18)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(18)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 3`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(0)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(2)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(1)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(4)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(3)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(4)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [\n                    Object {\n                      \"cid\": \"cids(4)\",\n                      \"cts\": \"1970-01-01T00:00:00.000Z\",\n                      \"src\": \"did:example:labeler\",\n                      \"uri\": \"record(2)\",\n                      \"val\": \"test-label-3\",\n                    },\n                  ],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(2)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(1)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(1)\",\n              \"val\": \"test-label-3\",\n            },\n          ],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(1)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(2)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(3)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(4)\",\n                  \"uri\": \"record(2)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(1)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(1)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"repost\": \"record(4)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(6)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(4)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(10)\",\n            \"uri\": \"record(10)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(5)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(4)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(3)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(10)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(10)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(10)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(10)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(10)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(9)\",\n              \"uri\": \"record(9)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(9)\",\n              \"uri\": \"record(9)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(8)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(8)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(11)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(11)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(9)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(9)\",\n            \"uri\": \"record(9)\",\n          },\n        },\n        \"text\": \"of course\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(12)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(8)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(8)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(11)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(1)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(8)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(8)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(1)\",\n              \"uri\": \"record(8)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(11)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(12)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n              \"chat\": Object {\n                \"allowIncoming\": \"none\",\n              },\n            },\n            \"did\": \"user(0)\",\n            \"handle\": \"dan.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(0)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.record#view\",\n              \"record\": Object {\n                \"$type\": \"app.bsky.embed.record#viewRecord\",\n                \"author\": Object {\n                  \"associated\": Object {\n                    \"activitySubscription\": Object {\n                      \"allowSubscriptions\": \"followers\",\n                    },\n                  },\n                  \"did\": \"user(2)\",\n                  \"handle\": \"carol.test\",\n                  \"labels\": Array [],\n                  \"viewer\": Object {\n                    \"blockedBy\": false,\n                    \"muted\": false,\n                  },\n                },\n                \"cid\": \"cids(1)\",\n                \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                \"labels\": Array [\n                  Object {\n                    \"cid\": \"cids(1)\",\n                    \"cts\": \"1970-01-01T00:00:00.000Z\",\n                    \"src\": \"did:example:labeler\",\n                    \"uri\": \"record(1)\",\n                    \"val\": \"test-label-3\",\n                  },\n                ],\n                \"likeCount\": 2,\n                \"quoteCount\": 1,\n                \"replyCount\": 0,\n                \"repostCount\": 0,\n                \"uri\": \"record(1)\",\n                \"value\": Object {\n                  \"$type\": \"app.bsky.feed.post\",\n                  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"embed\": Object {\n                    \"$type\": \"app.bsky.embed.recordWithMedia\",\n                    \"media\": Object {\n                      \"$type\": \"app.bsky.embed.images\",\n                      \"images\": Array [\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(2)\",\n                            },\n                            \"size\": 4114,\n                          },\n                        },\n                        Object {\n                          \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                          \"image\": Object {\n                            \"$type\": \"blob\",\n                            \"mimeType\": \"image/jpeg\",\n                            \"ref\": Object {\n                              \"$link\": \"cids(3)\",\n                            },\n                            \"size\": 12736,\n                          },\n                        },\n                      ],\n                    },\n                    \"record\": Object {\n                      \"record\": Object {\n                        \"cid\": \"cids(4)\",\n                        \"uri\": \"record(2)\",\n                      },\n                    },\n                  },\n                  \"text\": \"hi im carol\",\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 1,\n          \"uri\": \"record(0)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.record\",\n              \"record\": Object {\n                \"cid\": \"cids(1)\",\n                \"uri\": \"record(1)\",\n              },\n            },\n            \"facets\": Array [\n              Object {\n                \"features\": Array [\n                  Object {\n                    \"$type\": \"app.bsky.richtext.facet#mention\",\n                    \"did\": \"user(1)\",\n                  },\n                ],\n                \"index\": Object {\n                  \"byteEnd\": 18,\n                  \"byteStart\": 0,\n                },\n              },\n            ],\n            \"text\": \"@alice.bluesky.xyz is the best\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(12)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(13)\",\n          \"val\": \"test-label\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(0)\",\n            \"uri\": \"record(0)\",\n          },\n        },\n        \"text\": \"yoohoo label_me\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(14)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(11)\",\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(1)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(5)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(4)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(3)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(4)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"did:example:labeler\",\n                \"uri\": \"record(2)\",\n                \"val\": \"test-label-3\",\n              },\n            ],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(2)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(1)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(3)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(4)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(1)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(5)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(8)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(8)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(7)\",\n          \"following\": \"record(6)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(13)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(13)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(15)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(15)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n\nexports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 4`] = `\nArray [\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(4)\",\n            \"uri\": \"record(4)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(3)\",\n          },\n        },\n        \"text\": \"thanks bob\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"repost\": \"record(5)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(5)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(5)\",\n    },\n    \"reply\": Object {\n      \"grandparentAuthor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(8)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(4)\",\n            \"val\": \"test-label\",\n          },\n          Object {\n            \"cid\": \"cids(4)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"did:example:labeler\",\n            \"uri\": \"record(4)\",\n            \"val\": \"test-label-2\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(4)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(7)\",\n          \"repost\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(3)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 3,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n        \"text\": \"again\",\n      },\n      \"replyCount\": 2,\n      \"repostCount\": 1,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(7)\",\n        \"repost\": \"record(6)\",\n        \"threadMuted\": false,\n      },\n    },\n    \"reason\": Object {\n      \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n      \"by\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"cid\": \"cids(7)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"uri\": \"record(6)\",\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(4)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.images#view\",\n        \"images\": Array [\n          Object {\n            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n            \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n            \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n          },\n        ],\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(4)\",\n          \"val\": \"test-label\",\n        },\n        Object {\n          \"cid\": \"cids(4)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(4)\",\n          \"val\": \"test-label-2\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(6)\",\n                },\n                \"size\": 4114,\n              },\n            },\n          ],\n        },\n        \"reply\": Object {\n          \"parent\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(3)\",\n          },\n          \"root\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(3)\",\n          },\n        },\n        \"text\": \"hear that label_me label_me_2\",\n      },\n      \"replyCount\": 1,\n      \"repostCount\": 0,\n      \"uri\": \"record(4)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    \"reply\": Object {\n      \"parent\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(7)\",\n          \"repost\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n      \"root\": Object {\n        \"$type\": \"app.bsky.feed.defs#postView\",\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(2)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(7)\",\n          \"repost\": \"record(6)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(8)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n        \"text\": \"bobby boy here\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(9)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(9)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.record#view\",\n        \"record\": Object {\n          \"$type\": \"app.bsky.embed.record#viewRecord\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"did\": \"user(5)\",\n            \"handle\": \"carol.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"cid\": \"cids(10)\",\n          \"embeds\": Array [\n            Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images#view\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(11)\",\n                    \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(11)\",\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"did\": \"user(3)\",\n                    \"displayName\": \"bobby\",\n                    \"handle\": \"bob.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"following\": \"record(8)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(12)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [\n                    Object {\n                      \"cid\": \"cids(12)\",\n                      \"cts\": \"1970-01-01T00:00:00.000Z\",\n                      \"src\": \"did:example:labeler\",\n                      \"uri\": \"record(12)\",\n                      \"val\": \"test-label-3\",\n                    },\n                  ],\n                  \"likeCount\": 0,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(12)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"langs\": Array [\n                      \"en-US\",\n                      \"i-klingon\",\n                    ],\n                    \"text\": \"bob back at it again!\",\n                  },\n                },\n              },\n            },\n          ],\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(10)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"record(11)\",\n              \"val\": \"test-label-3\",\n            },\n          ],\n          \"likeCount\": 2,\n          \"quoteCount\": 1,\n          \"replyCount\": 0,\n          \"repostCount\": 0,\n          \"uri\": \"record(11)\",\n          \"value\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.recordWithMedia\",\n              \"media\": Object {\n                \"$type\": \"app.bsky.embed.images\",\n                \"images\": Array [\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(6)\",\n                      },\n                      \"size\": 4114,\n                    },\n                  },\n                  Object {\n                    \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                    \"image\": Object {\n                      \"$type\": \"blob\",\n                      \"mimeType\": \"image/jpeg\",\n                      \"ref\": Object {\n                        \"$link\": \"cids(11)\",\n                      },\n                      \"size\": 12736,\n                    },\n                  },\n                ],\n              },\n              \"record\": Object {\n                \"record\": Object {\n                  \"cid\": \"cids(12)\",\n                  \"uri\": \"record(12)\",\n                },\n              },\n            },\n            \"text\": \"hi im carol\",\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record\",\n          \"record\": Object {\n            \"cid\": \"cids(10)\",\n            \"uri\": \"record(11)\",\n          },\n        },\n        \"facets\": Array [\n          Object {\n            \"features\": Array [\n              Object {\n                \"$type\": \"app.bsky.richtext.facet#mention\",\n                \"did\": \"user(0)\",\n              },\n            ],\n            \"index\": Object {\n              \"byteEnd\": 18,\n              \"byteStart\": 0,\n            },\n          },\n        ],\n        \"text\": \"@alice.bluesky.xyz is the best\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 1,\n      \"uri\": \"record(10)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(13)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 0,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"text\": \"dan here!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(13)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n  Object {\n    \"post\": Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(12)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(12)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"record(12)\",\n          \"val\": \"test-label-3\",\n        },\n      ],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(12)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/bsky/tests/views/account-deactivation.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('bsky account deactivation', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_account_deactivation',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    const pdsAgent = network.pds.getClient()\n    await pdsAgent.com.atproto.server.deactivateAccount(\n      {},\n      { encoding: 'application/json', headers: sc.getHeaders(alice) },\n    )\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('does not return deactivated profiles', async () => {\n    const attempt = agent.api.app.bsky.actor.getProfile({\n      actor: alice,\n    })\n    await expect(attempt).rejects.toThrow('Account is deactivated')\n    const res = await agent.api.app.bsky.actor.getProfiles({\n      actors: [sc.dids.alice, sc.dids.bob, sc.dids.carol],\n    })\n    expect(res.data.profiles.length).toBe(2)\n    expect(res.data.profiles.some((p) => p.did === alice)).toBe(false)\n  })\n\n  it('does not return deactivated accounts in follows', async () => {\n    const follows = await agent.api.app.bsky.graph.getFollows({\n      actor: sc.dids.bob,\n    })\n    expect(follows.data.follows.some((f) => f.did === alice)).toBe(false)\n    const followers = await agent.api.app.bsky.graph.getFollowers({\n      actor: sc.dids.bob,\n    })\n    expect(followers.data.followers.some((f) => f.did === alice)).toBe(false)\n  })\n\n  it('does not return posts from deactivated accounts', async () => {\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      sc.posts[sc.dids.carol][0].ref.uriStr,\n      sc.posts[sc.dids.dan][1].ref.uriStr,\n      sc.replies[sc.dids.alice][0].ref.uriStr,\n    ]\n    const res = await agent.api.app.bsky.feed.getPosts({ uris })\n\n    expect(res.data.posts.length).toBe(3)\n    expect(res.data.posts.some((p) => p.author.did === alice)).toBe(false)\n  })\n\n  it('does not return posts from deactivated in timelines', async () => {\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(res.data.feed.some((p) => p.post.author.did === alice)).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/actor-likes.test.ts",
    "content": "import { AtUri, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('bsky actor likes feed views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_actor_likes',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('returns posts liked by actor', async () => {\n    const {\n      data: { feed: bobLikes },\n    } = await agent.api.app.bsky.feed.getActorLikes(\n      { actor: sc.accounts[bob].handle },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetActorLikes,\n        ),\n      },\n    )\n\n    expect(bobLikes).toHaveLength(3)\n\n    await expect(\n      agent.api.app.bsky.feed.getActorLikes(\n        { actor: sc.accounts[bob].handle },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetActorLikes,\n          ),\n        },\n      ),\n    ).rejects.toThrow('Profile not found')\n  })\n\n  it('viewer has blocked author of liked post(s)', async () => {\n    const bobBlocksAlice = await pdsAgent.api.app.bsky.graph.block.create(\n      {\n        repo: bob, // bob blocks alice\n      },\n      {\n        subject: alice,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n\n    await network.processAll()\n\n    const {\n      data: { feed },\n    } = await agent.api.app.bsky.feed.getActorLikes(\n      { actor: sc.accounts[bob].handle },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetActorLikes,\n        ),\n      },\n    )\n\n    expect(\n      feed.every((item) => {\n        return item.post.author.did !== alice\n      }),\n    ).toBe(true)\n\n    // unblock\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: bob, rkey: new AtUri(bobBlocksAlice.uri).rkey },\n      sc.getHeaders(bob),\n    )\n  })\n\n  it('liked post author has blocked viewer', async () => {\n    const aliceBlockBob = await pdsAgent.api.app.bsky.graph.block.create(\n      {\n        repo: alice, // alice blocks bob\n      },\n      {\n        subject: bob,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const {\n      data: { feed },\n    } = await agent.api.app.bsky.feed.getActorLikes(\n      { actor: sc.accounts[bob].handle },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetActorLikes,\n        ),\n      },\n    )\n\n    expect(\n      feed.every((item) => {\n        return item.post.author.did !== alice\n      }),\n    ).toBe(true)\n\n    // unblock\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(aliceBlockBob.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/actor-search.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { wait } from '@atproto/common'\nimport { SeedClient, TestNetwork, usersBulkSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as SearchActorsOutputSchema } from '../../src/lexicon/types/app/bsky/actor/searchActors'\nimport { forSnapshot, paginateAll, stripViewer } from '../_util'\n\n// @NOTE skipped to help with CI failures\n// The search code is not used in production & we should switch it out for tests on the search proxy interface\ndescribe.skip('pds actor search views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let headers: { [s: string]: string }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_actor_search',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n\n    await wait(50) // allow pending sub to be established\n    await network.bsky.sub.destroy()\n    await usersBulkSeed(sc)\n\n    // Skip did/handle resolution for expediency\n    const { db } = network.bsky\n    const now = new Date().toISOString()\n    await db.db\n      .insertInto('actor')\n      .values(\n        Object.entries(sc.dids).map(([handle, did]) => ({\n          did,\n          handle,\n          indexedAt: now,\n        })),\n      )\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n\n    // Process remaining profiles\n    await network.bsky.sub.restart()\n    await network.processAll(50000)\n    headers = await network.serviceHeaders(\n      Object.values(sc.dids)[0],\n      ids.AppBskyActorSearchActorsTypeahead,\n    )\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('typeahead gives relevant results', async () => {\n    const result = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      { term: 'car' },\n      { headers },\n    )\n\n    const handles = result.data.actors.map((u) => u.handle)\n\n    const shouldContain = [\n      'cara-wiegand69.test',\n      'eudora-dietrich4.test', // Carol Littel\n      'shane-torphy52.test', // Sadie Carter\n      'aliya-hodkiewicz.test', // Carlton Abernathy IV\n      'carlos6.test',\n      'carolina-mcderm77.test',\n    ]\n\n    shouldContain.forEach((handle) => expect(handles).toContain(handle))\n    expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match\n\n    const shouldNotContain = [\n      'sven70.test',\n      'hilario84.test',\n      'santa-hermann78.test',\n      'dylan61.test',\n      'preston-harris.test',\n      'loyce95.test',\n      'melyna-zboncak.test',\n    ]\n\n    shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle))\n\n    const sorted = result.data.actors.sort((a, b) =>\n      a.handle > b.handle ? 1 : -1,\n    )\n    expect(forSnapshot(sorted)).toMatchSnapshot()\n  })\n\n  it('typeahead gives empty result set when provided empty term', async () => {\n    const result = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      { term: '' },\n      { headers },\n    )\n\n    expect(result.data.actors).toEqual([])\n  })\n\n  it('typeahead applies limit', async () => {\n    const full = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      { term: 'p' },\n      { headers },\n    )\n\n    expect(full.data.actors.length).toBeGreaterThan(5)\n\n    const limited = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      { term: 'p', limit: 5 },\n      { headers },\n    )\n\n    // @NOTE it's expected that searchActorsTypeahead doesn't have stable pagination\n\n    const limitedIndexInFull = limited.data.actors.map((needle) => {\n      return full.data.actors.findIndex(\n        (haystack) => needle.did === haystack.did,\n      )\n    })\n\n    // subset exists in full and is monotonic\n    expect(limitedIndexInFull.every((idx) => idx !== -1)).toEqual(true)\n    expect(limitedIndexInFull).toEqual(\n      [...limitedIndexInFull].sort((a, b) => a - b),\n    )\n  })\n\n  it('typeahead gives results unauthed', async () => {\n    const { data: authed } =\n      await agent.api.app.bsky.actor.searchActorsTypeahead(\n        { term: 'car' },\n        { headers },\n      )\n    const { data: unauthed } =\n      await agent.api.app.bsky.actor.searchActorsTypeahead({\n        term: 'car',\n      })\n    expect(unauthed.actors.length).toBeGreaterThan(0)\n    expect(unauthed.actors).toEqual(authed.actors.map(stripViewer))\n  })\n\n  it('search gives relevant results', async () => {\n    const result = await agent.api.app.bsky.actor.searchActors(\n      { term: 'car' },\n      { headers },\n    )\n\n    const handles = result.data.actors.map((u) => u.handle)\n\n    const shouldContain = [\n      'cara-wiegand69.test',\n      'eudora-dietrich4.test', // Carol Littel\n      'shane-torphy52.test', // Sadie Carter\n      'aliya-hodkiewicz.test', // Carlton Abernathy IV\n      'carlos6.test',\n      'carolina-mcderm77.test',\n    ]\n\n    shouldContain.forEach((handle) => expect(handles).toContain(handle))\n    expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match\n\n    const shouldNotContain = [\n      'sven70.test',\n      'hilario84.test',\n      'santa-hermann78.test',\n      'dylan61.test',\n      'preston-harris.test',\n      'loyce95.test',\n      'melyna-zboncak.test',\n    ]\n\n    shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle))\n\n    const sorted = result.data.actors.sort((a, b) =>\n      a.handle > b.handle ? 1 : -1,\n    )\n    expect(forSnapshot(sorted)).toMatchSnapshot()\n  })\n\n  it('search gives empty result set when provided empty term', async () => {\n    const result = await agent.api.app.bsky.actor.searchActors(\n      { term: '' },\n      { headers },\n    )\n\n    expect(result.data.actors).toEqual([])\n  })\n\n  it('paginates', async () => {\n    const results = (results: SearchActorsOutputSchema[]) =>\n      results.flatMap((res) => res.actors)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.actor.searchActors(\n        { term: 'p', cursor, limit: 3 },\n        { headers },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.actors.length).toBeLessThanOrEqual(3),\n    )\n\n    const full = await agent.api.app.bsky.actor.searchActors(\n      { term: 'p' },\n      { headers },\n    )\n\n    expect(full.data.actors.length).toBeGreaterThan(5)\n    const sortedFull = results([full.data]).sort((a, b) =>\n      a.handle > b.handle ? 1 : -1,\n    )\n    const sortedPaginated = results(paginatedAll).sort((a, b) =>\n      a.handle > b.handle ? 1 : -1,\n    )\n    expect(sortedPaginated).toEqual(sortedFull)\n  })\n\n  it('search handles bad input', async () => {\n    // Mostly for sqlite's benefit, since it uses LIKE and these are special characters that will\n    // get stripped. This input triggers a special case where there are no \"safe\" words for sqlite to search on.\n    const result = await agent.api.app.bsky.actor.searchActors(\n      { term: ' % _ ' },\n      { headers },\n    )\n\n    expect(result.data.actors).toEqual([])\n  })\n\n  it('search gives results unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.actor.searchActors(\n      { term: 'car' },\n      { headers },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.actor.searchActors({\n      term: 'car',\n    })\n    expect(unauthed.actors.length).toBeGreaterThan(0)\n    expect(unauthed.actors).toEqual(authed.actors.map(stripViewer))\n  })\n\n  it('search blocks by actor takedown', async () => {\n    await network.bsky.server.ctx.dataplane.takedownActor({\n      did: sc.dids['cara-wiegand69.test'],\n    })\n    const result = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      { term: 'car' },\n      { headers },\n    )\n    const handles = result.data.actors.map((u) => u.handle)\n    expect(handles).toContain('carlos6.test')\n    expect(handles).toContain('carolina-mcderm77.test')\n    expect(handles).not.toContain('cara-wiegand69.test')\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/age-assurance-v2.test.ts",
    "content": "import crypto from 'node:crypto'\nimport { once } from 'node:events'\nimport { Server, createServer } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport express, { Application, json } from 'express'\nimport {\n  AppBskyAgeassuranceDefs,\n  AtpAgent,\n  ageAssuranceRuleIDs as ruleIds,\n} from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport {\n  type KWSWebhookAgeVerified,\n  serializeKWSAgeVerifiedStatus,\n} from '../../src/api/age-assurance/kws/age-verified'\nimport {\n  KWSExternalPayloadVersion,\n  serializeKWSExternalPayloadV1,\n  serializeKWSExternalPayloadV2,\n} from '../../src/api/age-assurance/kws/external-payload'\nimport { KwsWebhookBody } from '../../src/api/kws/types'\nimport { ids } from '../../src/lexicon/lexicons'\nimport * as AppBskyAgeassuranceBegin from '../../src/lexicon/types/app/bsky/ageassurance/begin'\nimport * as AppBskyAgeassuranceGetState from '../../src/lexicon/types/app/bsky/ageassurance/getState'\n\ntype Database = TestNetwork['bsky']['db']\n\nconst BSKY_REDIRECT_URL = 'http://bsky'\n\njest.mock('../../dist/api/age-assurance/const.js', () => {\n  const AGE_ASSURANCE_CONFIG: AppBskyAgeassuranceDefs.Config = {\n    regions: [\n      {\n        countryCode: 'AA',\n        regionCode: undefined,\n        minAccessAge: 13,\n        rules: [\n          {\n            $type: ruleIds.IfAssuredOverAge,\n            age: 18,\n            access: 'full',\n          },\n          {\n            $type: ruleIds.Default,\n            access: 'safe',\n          },\n        ],\n      },\n      {\n        countryCode: 'BB',\n        regionCode: undefined,\n        minAccessAge: 13,\n        rules: [\n          {\n            $type: ruleIds.IfAssuredOverAge,\n            age: 18,\n            access: 'full',\n          },\n          {\n            $type: ruleIds.Default,\n            access: 'safe',\n          },\n        ],\n      },\n    ],\n  }\n  return {\n    AGE_ASSURANCE_CONFIG,\n  }\n})\n\njest.mock('../../dist/api/age-assurance/kws/const.js', () => {\n  const actual = jest.requireActual('../../dist/api/age-assurance/kws/const.js')\n  const KWS_V2_COUNTRIES = new Set(['AA'])\n  return {\n    ...actual,\n    KWS_V2_COUNTRIES,\n  }\n})\n\ndescribe('age assurance v2 views', () => {\n  let network: TestNetwork\n  let db: Database\n  let agent: AtpAgent\n  let sc: SeedClient\n  let kws: MockKwsServer\n\n  const kwsOauthMock = jest.fn()\n  const kwsSendAgeVerifiedFlowEmailMock = jest.fn()\n  const kwsSendAdultVerifiedFlowEmailMock = jest.fn()\n  const actor = {\n    did: '',\n    email: '',\n  }\n\n  beforeAll(async () => {\n    kws = new MockKwsServer({\n      oauthMock: kwsOauthMock,\n      sendAgeVerifiedFlowEmailMock: kwsSendAgeVerifiedFlowEmailMock,\n      sendAdultVerifiedFlowEmailMock: kwsSendAdultVerifiedFlowEmailMock,\n    })\n    await kws.listen()\n\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_age_assurance_v_two',\n      bsky: {\n        kws: {\n          apiKey: 'apiKey',\n          apiOrigin: kws.url,\n          authOrigin: kws.url,\n          clientId: 'clientId',\n          redirectUrl: BSKY_REDIRECT_URL,\n          userAgent: 'userAgent',\n          verificationSecret: kws.verificationSecret,\n          webhookSecret: kws.webhookSecret,\n          ageVerifiedWebhookSecret: kws.ageVerifiedWebhookSecret,\n          ageVerifiedRedirectSecret: kws.ageVerifiedRedirectSecret,\n        },\n      },\n    })\n\n    kws.setBskyBaseUrl(network.bsky.url)\n\n    db = network.bsky.db\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n\n    await basicSeed(sc)\n    await network.processAll()\n\n    actor.did = sc.dids.alice\n    actor.email = sc.accounts[actor.did].email\n  })\n\n  beforeEach(async () => {\n    // Default mocks for KWS endpoints.\n    kwsOauthMock.mockImplementation(\n      (_req: express.Request, res: express.Response) =>\n        res.json({\n          access_token:\n            'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.INVALID',\n          expires_in: 3600,\n        }),\n    )\n    kwsSendAgeVerifiedFlowEmailMock.mockImplementation(\n      (_req: express.Request, res: express.Response) => {\n        res.json({})\n      },\n    )\n    kwsSendAdultVerifiedFlowEmailMock.mockImplementation(\n      (_req: express.Request, res: express.Response) => {\n        res.json({})\n      },\n    )\n  })\n\n  afterEach(async () => {\n    jest.resetAllMocks()\n    await clearPrivateData(db)\n    await clearActorAgeAssurance(db)\n  })\n\n  afterAll(async () => {\n    await network.close()\n    await kws.stop()\n  })\n\n  const getState = async (params: AppBskyAgeassuranceGetState.QueryParams) => {\n    const { data } = await agent.app.bsky.ageassurance.getState(params, {\n      headers: await network.serviceHeaders(\n        actor.did,\n        ids.AppBskyAgeassuranceGetState,\n      ),\n    })\n    return data\n  }\n\n  const beginAgeAssurance = async (\n    params: Omit<AppBskyAgeassuranceBegin.InputSchema, 'email' | 'language'> & {\n      email?: string\n    },\n  ) => {\n    const { data } = await agent.app.bsky.ageassurance.begin(\n      {\n        ...params,\n        email: params.email || sc.accounts[actor.did].email,\n        language: 'en',\n      },\n      {\n        headers: await network.serviceHeaders(\n          actor.did,\n          ids.AppBskyAgeassuranceBegin,\n        ),\n      },\n    )\n    return data\n  }\n\n  describe('app.bsky.ageassurance.getState', () => {\n    it('initially returns defaults', async () => {\n      const { state, metadata } = await getState({\n        countryCode: 'US',\n        regionCode: undefined,\n      })\n      expect(metadata.accountCreatedAt).toBeDefined()\n      expect(state).toEqual({\n        lastInitatedAt: undefined,\n        status: 'unknown',\n        access: 'unknown',\n      })\n    })\n  })\n\n  describe('app.bsky.ageassurance.begin', () => {\n    it('fails if region not supported', async () => {\n      const call = beginAgeAssurance({\n        countryCode: 'XX',\n      })\n      await expect(call).rejects.toHaveProperty('error', 'RegionNotSupported')\n    })\n\n    it('fails if email is invalid', async () => {\n      const call = beginAgeAssurance({\n        email: 'invalid-email',\n        countryCode: 'XX',\n      })\n      await expect(call).rejects.toHaveProperty('error', 'InvalidEmail')\n    })\n\n    it('succeeds for V2 country', async () => {\n      const res = await beginAgeAssurance({\n        countryCode: 'AA',\n      })\n      await network.processAll()\n      const { state } = await getState({\n        countryCode: 'AA',\n      })\n      expect(kwsSendAgeVerifiedFlowEmailMock).toHaveBeenCalledTimes(1)\n      expect(res).toEqual(state)\n      expect(state.lastInitiatedAt).toBeDefined()\n      expect(state.status).toEqual('pending')\n      expect(state.access).toEqual('unknown')\n    })\n\n    it('succeeds for V1 country', async () => {\n      const res = await beginAgeAssurance({\n        countryCode: 'BB',\n      })\n      await network.processAll()\n      const { state } = await getState({\n        countryCode: 'BB',\n      })\n      expect(kwsSendAdultVerifiedFlowEmailMock).toHaveBeenCalledTimes(1)\n      expect(res).toEqual(state)\n      expect(state.lastInitiatedAt).toBeDefined()\n      expect(state.status).toEqual('pending')\n      expect(state.access).toEqual('unknown')\n    })\n  })\n\n  describe('external handlers', () => {\n    describe('V2 redirects', () => {\n      it('redirects with result=unknown if we fail to parse the status object', async () => {\n        const res = await kws.redirectV2({\n          externalPayload: serializeKWSExternalPayloadV2({\n            version: KWSExternalPayloadVersion.V2,\n            actorDid: actor.did,\n            attemptId: crypto.randomUUID(),\n            countryCode: 'AA',\n          }),\n          status: JSON.stringify({\n            verified: true,\n            verifiedMinimumAge: '18', // will fail parsing\n          }),\n        })\n        expect(res.status).toBe(302)\n        expect(res.headers.get('Location')).toBe(\n          `${BSKY_REDIRECT_URL}?result=unknown`,\n        )\n      })\n\n      it('redirects with result=unknown if status is not verified', async () => {\n        const res = await kws.redirectV2({\n          externalPayload: serializeKWSExternalPayloadV2({\n            version: KWSExternalPayloadVersion.V2,\n            actorDid: actor.did,\n            attemptId: crypto.randomUUID(),\n            countryCode: 'AA',\n          }),\n          status: serializeKWSAgeVerifiedStatus({\n            verified: false,\n            verifiedMinimumAge: 18,\n          }),\n        })\n        expect(res.status).toBe(302)\n        expect(res.headers.get('Location')).toBe(\n          `${BSKY_REDIRECT_URL}?actorDid=${encodeURIComponent(actor.did)}&result=unknown`,\n        )\n      })\n\n      // this also covers any other thrown errors\n      it('redirects with result=unknown if access check throws', async () => {\n        const res = await kws.redirectV2({\n          externalPayload: serializeKWSExternalPayloadV2({\n            version: KWSExternalPayloadVersion.V2,\n            actorDid: actor.did,\n            attemptId: crypto.randomUUID(),\n            countryCode: 'XX', // should never reach KWS anyway\n          }),\n          status: serializeKWSAgeVerifiedStatus({\n            verified: true,\n            verifiedMinimumAge: 18,\n          }),\n        })\n        expect(res.status).toBe(302)\n        expect(res.headers.get('Location')).toBe(\n          `${BSKY_REDIRECT_URL}?actorDid=${encodeURIComponent(actor.did)}&result=unknown`,\n        )\n      })\n\n      it('success', async () => {\n        await beginAgeAssurance({\n          countryCode: 'AA',\n        })\n        await network.processAll()\n        await kws.redirectV2({\n          externalPayload: serializeKWSExternalPayloadV2({\n            version: KWSExternalPayloadVersion.V2,\n            actorDid: actor.did,\n            attemptId: crypto.randomUUID(),\n            countryCode: 'AA',\n          }),\n          status: serializeKWSAgeVerifiedStatus({\n            verified: true,\n            verifiedMinimumAge: 18,\n          }),\n        })\n        await network.processAll()\n        const { state } = await getState({\n          countryCode: 'AA',\n        })\n        expect(state.lastInitiatedAt).toBeDefined()\n        expect(state.status).toEqual('assured')\n        expect(state.access).toEqual('full')\n      })\n    })\n\n    describe('V2 webhooks', () => {\n      it('returns 400 if we fail to parse the external payload', async () => {\n        const res = await kws.webhookV2({\n          name: 'age-verified',\n          time: new Date().toISOString(),\n          orgId: crypto.randomUUID(),\n          productId: crypto.randomUUID(),\n          payload: {\n            email: actor.email,\n            externalPayload: serializeKWSExternalPayloadV2({\n              version: KWSExternalPayloadVersion.V2,\n              actorDid: actor.did,\n              attemptId: crypto.randomUUID(),\n              countryCode: 'AA',\n            }),\n            status: {\n              verified: true,\n              // @ts-ignore testing invalid payload\n              verifiedMinimumAge: '18',\n            },\n          },\n        })\n        expect(res.status).toBe(400)\n        await expect(res.json()).resolves.toHaveProperty(\n          'error',\n          'Failed to parse KWS webhook body',\n        )\n      })\n\n      it('returns 400 if status is not verified', async () => {\n        const res = await kws.webhookV2({\n          name: 'age-verified',\n          time: new Date().toISOString(),\n          orgId: crypto.randomUUID(),\n          productId: crypto.randomUUID(),\n          payload: {\n            email: actor.email,\n            externalPayload: serializeKWSExternalPayloadV2({\n              version: KWSExternalPayloadVersion.V2,\n              actorDid: actor.did,\n              attemptId: crypto.randomUUID(),\n              countryCode: 'AA',\n            }),\n            status: {\n              verified: false,\n              verifiedMinimumAge: 18,\n            },\n          },\n        })\n        expect(res.status).toBe(400)\n        await expect(res.json()).resolves.toHaveProperty(\n          'error',\n          'Expected KWS webhook to have verified status',\n        )\n      })\n\n      it('returns 200, but AA state unchanged due to invalid region', async () => {\n        const res = await kws.webhookV2({\n          name: 'age-verified',\n          time: new Date().toISOString(),\n          orgId: crypto.randomUUID(),\n          productId: crypto.randomUUID(),\n          payload: {\n            email: actor.email,\n            externalPayload: serializeKWSExternalPayloadV2({\n              version: KWSExternalPayloadVersion.V2,\n              actorDid: actor.did,\n              attemptId: crypto.randomUUID(),\n              countryCode: 'XX',\n            }),\n            status: {\n              verified: true,\n              verifiedMinimumAge: 18,\n            },\n          },\n        })\n        await network.processAll()\n        expect(res.status).toBe(200)\n        const { state } = await getState({\n          countryCode: 'XX',\n        })\n        expect(state.status).toEqual('unknown') // we never began, so it's still unknown\n      })\n\n      it('success', async () => {\n        await beginAgeAssurance({\n          countryCode: 'AA',\n        })\n        await network.processAll()\n        await kws.webhookV2({\n          name: 'age-verified',\n          time: new Date().toISOString(),\n          orgId: crypto.randomUUID(),\n          productId: crypto.randomUUID(),\n          payload: {\n            email: actor.email,\n            externalPayload: serializeKWSExternalPayloadV2({\n              version: KWSExternalPayloadVersion.V2,\n              actorDid: actor.did,\n              attemptId: crypto.randomUUID(),\n              countryCode: 'AA',\n            }),\n            status: {\n              verified: true,\n              verifiedMinimumAge: 18,\n            },\n          },\n        })\n        await network.processAll()\n        const { state } = await getState({\n          countryCode: 'AA',\n        })\n        expect(state.lastInitiatedAt).toBeDefined()\n        expect(state.status).toEqual('assured')\n        expect(state.access).toEqual('full')\n      })\n    })\n\n    describe('V1 compat', () => {\n      it('works via webhook', async () => {\n        await beginAgeAssurance({\n          countryCode: 'BB',\n        })\n        await network.processAll()\n        await kws.webhookV1({\n          payload: {\n            externalPayload: serializeKWSExternalPayloadV2({\n              version: KWSExternalPayloadVersion.V2,\n              actorDid: actor.did,\n              attemptId: crypto.randomUUID(),\n              countryCode: 'BB',\n            }),\n            status: {\n              verified: true,\n            },\n          },\n        })\n        await network.processAll()\n        const { state } = await getState({\n          countryCode: 'BB',\n        })\n        expect(state.lastInitiatedAt).toBeDefined()\n        expect(state.status).toEqual('assured')\n        expect(state.access).toEqual('full')\n      })\n\n      it('works via redirect', async () => {\n        await beginAgeAssurance({\n          countryCode: 'BB',\n        })\n        await network.processAll()\n        await kws.redirectV1({\n          externalPayload: serializeKWSExternalPayloadV2({\n            version: KWSExternalPayloadVersion.V2,\n            actorDid: actor.did,\n            attemptId: crypto.randomUUID(),\n            countryCode: 'BB',\n          }),\n          status: JSON.stringify({\n            verified: true,\n          }),\n        })\n        await network.processAll()\n        const { state } = await getState({\n          countryCode: 'BB',\n        })\n        expect(state.lastInitiatedAt).toBeDefined()\n        expect(state.status).toEqual('assured')\n        expect(state.access).toEqual('full')\n      })\n    })\n  })\n\n  describe('misc', () => {\n    /*\n     * This is a silly test, but it did help me uncover a local data-plane\n     * implementation bug. Let's leave it here for additional safety.\n     */\n    it('expects access to be safe', async () => {\n      await kws.redirectV2({\n        externalPayload: serializeKWSExternalPayloadV2({\n          version: KWSExternalPayloadVersion.V2,\n          actorDid: actor.did,\n          attemptId: crypto.randomUUID(),\n          countryCode: 'AA',\n        }),\n        status: serializeKWSAgeVerifiedStatus({\n          verified: true,\n          verifiedMinimumAge: 16,\n        }),\n      })\n      await network.processAll()\n      const { state } = await getState({\n        countryCode: 'AA',\n      })\n      expect(state.status).toEqual('assured')\n      expect(state.access).toEqual('safe')\n    })\n\n    /**\n     * We only block re-init if the user is in a `blocked` state, which is not\n     * testable using the local dataplane at the moment. The test below\n     * reflects v1 handling.\n     *\n     * Skipping for now, but this handling is implemented in v2.\n     */\n    it.skip('cannot re-init from terminal state', async () => {\n      await kws.redirectV2({\n        externalPayload: serializeKWSExternalPayloadV2({\n          version: KWSExternalPayloadVersion.V2,\n          actorDid: actor.did,\n          attemptId: crypto.randomUUID(),\n          countryCode: 'AA',\n        }),\n        status: serializeKWSAgeVerifiedStatus({\n          verified: true,\n          verifiedMinimumAge: 18,\n        }),\n      })\n      await network.processAll()\n      const call = beginAgeAssurance({\n        countryCode: 'AA',\n      })\n      await expect(call).rejects.toHaveProperty('error', 'InvalidInitiation')\n    })\n\n    it('re-init from terminal state retains existing status', async () => {\n      await kws.redirectV2({\n        externalPayload: serializeKWSExternalPayloadV2({\n          version: KWSExternalPayloadVersion.V2,\n          actorDid: actor.did,\n          attemptId: crypto.randomUUID(),\n          countryCode: 'AA',\n        }),\n        status: serializeKWSAgeVerifiedStatus({\n          verified: true,\n          verifiedMinimumAge: 16,\n        }),\n      })\n      await network.processAll()\n      const { state } = await getState({\n        countryCode: 'AA',\n      })\n      expect(state.status).toEqual('assured')\n      expect(state.access).toEqual('safe')\n      const res = await beginAgeAssurance({\n        countryCode: 'AA',\n      })\n      expect(res.status).toEqual('assured')\n      expect(res.access).toEqual('safe')\n    })\n\n    /*\n     * This tests local dataplane behavior, but the actual prod implementation\n     * lives in the dataplane repo, obviously.\n     */\n    it('dataplane converts v1 to v2 state at read time', async () => {\n      await beginAgeAssurance({\n        countryCode: 'BB',\n      })\n      await network.processAll()\n      await kws.webhookV1({\n        payload: {\n          externalPayload: serializeKWSExternalPayloadV1({\n            actorDid: actor.did,\n            attemptId: crypto.randomUUID(),\n          }),\n          status: {\n            verified: true,\n          },\n        },\n      })\n      await network.processAll()\n      const { state } = await getState({\n        countryCode: 'BB',\n      })\n      expect(state.lastInitiatedAt).toBeDefined()\n      expect(state.status).toEqual('assured')\n      expect(state.access).toEqual('full')\n    })\n  })\n})\n\nconst clearPrivateData = async (db: Database) => {\n  await db.db.deleteFrom('private_data').execute()\n}\n\nconst clearActorAgeAssurance = async (db: Database) => {\n  await db.db\n    .updateTable('actor')\n    .set({\n      ageAssuranceStatus: null,\n      ageAssuranceLastInitiatedAt: null,\n      ageAssuranceAccess: null,\n      ageAssuranceCountryCode: null,\n      ageAssuranceRegionCode: null,\n    })\n    .execute()\n}\n\nclass MockKwsServer {\n  verificationSecret = 'verificationSecret' // unused here\n  webhookSecret = 'webhookSecret' // unused here\n  ageVerifiedWebhookSecret = 'ageVerifiedWebhookSecret'\n  ageVerifiedRedirectSecret = 'ageVerifiedRedirectSecret'\n\n  private app: Application\n  private server: Server\n  private bskyUrlBase = ''\n\n  constructor({\n    oauthMock,\n    sendAgeVerifiedFlowEmailMock,\n    sendAdultVerifiedFlowEmailMock,\n  }: {\n    oauthMock: jest.Mock\n    sendAgeVerifiedFlowEmailMock: jest.Mock\n    sendAdultVerifiedFlowEmailMock: jest.Mock\n  }) {\n    this.app = express()\n      .use(json())\n      .post('/auth/realms/kws/protocol/openid-connect/token', (_, res) =>\n        oauthMock(_, res),\n      )\n      .post('/v1/verifications/send-email', (req, res) => {\n        const body = req.body\n        if (body.userContext === 'age') {\n          return sendAgeVerifiedFlowEmailMock(req, res)\n        } else if (body.userContext === 'adult') {\n          return sendAdultVerifiedFlowEmailMock(req, res)\n        }\n      })\n\n    this.server = createServer(this.app)\n  }\n\n  async listen(port?: number) {\n    this.server.listen(port)\n    await once(this.server, 'listening')\n  }\n\n  async stop() {\n    this.server.close()\n    await once(this.server, 'close')\n  }\n\n  setBskyBaseUrl(url: string) {\n    this.bskyUrlBase = url\n  }\n\n  redirectV1({\n    externalPayload,\n    status,\n  }: {\n    externalPayload: string\n    status: string\n  }) {\n    const sig = crypto\n      .createHmac('sha256', this.verificationSecret)\n      .update(`${status}:${externalPayload}`)\n      .digest('hex')\n\n    const queryString = new URLSearchParams({\n      externalPayload,\n      signature: sig,\n      status,\n    }).toString()\n\n    return fetch(\n      `${this.bskyUrlBase}/external/kws/age-assurance-verification?${queryString}`,\n      {\n        method: 'GET',\n        redirect: 'manual',\n      },\n    )\n  }\n\n  redirectV2({\n    externalPayload,\n    status,\n  }: {\n    externalPayload: string\n    status: string\n  }) {\n    const sig = crypto\n      .createHmac('sha256', this.ageVerifiedRedirectSecret)\n      .update(`${status}:${externalPayload}`)\n      .digest('hex')\n\n    const queryString = new URLSearchParams({\n      externalPayload,\n      signature: sig,\n      status,\n    }).toString()\n\n    return fetch(\n      `${this.bskyUrlBase}/external/age-assurance/redirects/kws-age-verified?${queryString}`,\n      {\n        method: 'GET',\n        redirect: 'manual',\n      },\n    )\n  }\n\n  webhookV1(\n    body: Omit<KwsWebhookBody, 'payload'> & {\n      payload: Omit<KwsWebhookBody['payload'], 'externalPayload'> & {\n        externalPayload: string\n      }\n    },\n  ): Promise<Response> {\n    const bodyBuffer = Buffer.from(JSON.stringify(body))\n\n    const timestamp = new Date().valueOf()\n    const sig = crypto\n      .createHmac('sha256', this.webhookSecret)\n      .update(`${timestamp}.${bodyBuffer}`)\n      .digest('hex')\n\n    return fetch(`${this.bskyUrlBase}/external/kws/age-assurance-webhook`, {\n      method: 'POST',\n      body: bodyBuffer,\n      headers: {\n        'x-kws-signature': `t=${timestamp},v1=${sig}`,\n        'Content-Type': 'application/json',\n      },\n    })\n  }\n\n  webhookV2(body: KWSWebhookAgeVerified): Promise<Response> {\n    const bodyBuffer = Buffer.from(JSON.stringify(body))\n\n    const timestamp = new Date().valueOf()\n    const sig = crypto\n      .createHmac('sha256', this.ageVerifiedWebhookSecret)\n      .update(`${timestamp}.${bodyBuffer}`)\n      .digest('hex')\n\n    return fetch(\n      `${this.bskyUrlBase}/external/age-assurance/webhooks/kws-age-verified`,\n      {\n        method: 'POST',\n        body: bodyBuffer,\n        headers: {\n          'x-kws-signature': `t=${timestamp},v1=${sig}`,\n          'Content-Type': 'application/json',\n        },\n      },\n    )\n  }\n\n  get url() {\n    const address = this.server.address() as AddressInfo\n    return `http://localhost:${address.port}`\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/age-assurance.test.ts",
    "content": "import crypto from 'node:crypto'\nimport { once } from 'node:events'\nimport { Server, createServer } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport express, { Application } from 'express'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport {\n  KwsExternalPayload,\n  KwsVerificationQuery,\n  KwsWebhookBody,\n} from '../../src/api/kws/types'\nimport {\n  parseExternalPayload,\n  serializeExternalPayload,\n} from '../../src/api/kws/util'\nimport { ids } from '../../src/lexicon/lexicons'\n\ntype Database = TestNetwork['bsky']['db']\n\ndescribe('age assurance views', () => {\n  const verificationSecret = 'verificationSecret'\n  const webhookSecret = 'webhookSecret'\n  const attemptId = crypto.randomUUID()\n  const redirectUrl = 'https://bsky.app/intent/age-assurance'\n\n  let network: TestNetwork\n  let db: Database\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let actorDid: string\n\n  let kwsServer: MockKwsServer\n  const authMock = jest.fn()\n  const sendEmailMock = jest.fn()\n\n  beforeAll(async () => {\n    kwsServer = new MockKwsServer({\n      verificationSecret,\n      webhookSecret,\n      authMock,\n      sendEmailMock,\n    })\n    await kwsServer.listen()\n\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_age_assurance',\n      bsky: {\n        kws: {\n          apiKey: 'apiKey',\n          apiOrigin: kwsServer.url,\n          authOrigin: kwsServer.url,\n          clientId: 'clientId',\n          redirectUrl,\n          userAgent: 'userAgent',\n          verificationSecret,\n          webhookSecret,\n          ageVerifiedWebhookSecret: 'ageVerifiedWebhookSecret',\n          ageVerifiedRedirectSecret: 'ageVerifiedRedirectSecret',\n        },\n      },\n    })\n    db = network.bsky.db\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    actorDid = sc.dids.alice\n  })\n\n  beforeEach(async () => {\n    // Default mocks for KWS endpoints.\n    authMock.mockImplementation(\n      (_req: express.Request, res: express.Response) =>\n        res.json({\n          access_token:\n            'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.INVALID',\n          expires_in: 3600,\n        }),\n    )\n    sendEmailMock.mockImplementation(\n      (_req: express.Request, res: express.Response) => {\n        res.json({})\n      },\n    )\n  })\n\n  afterEach(async () => {\n    jest.resetAllMocks()\n    await clearPrivateData(db)\n    await clearActorAgeAssurance(db)\n  })\n\n  afterAll(async () => {\n    await network.close()\n    await kwsServer.stop()\n  })\n\n  const getAgeAssurance = async (did: string) => {\n    const { data } = await agent.app.bsky.unspecced.getAgeAssuranceState(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          did,\n          ids.AppBskyUnspeccedGetAgeAssuranceState,\n        ),\n      },\n    )\n    return data\n  }\n\n  const initAgeAssurance = async (did: string, email?: string) => {\n    const { data } = await agent.app.bsky.unspecced.initAgeAssurance(\n      {\n        email: email ?? sc.accounts[did].email,\n        language: 'en',\n        countryCode: 'CC',\n      },\n      {\n        headers: await network.serviceHeaders(\n          did,\n          ids.AppBskyUnspeccedInitAgeAssurance,\n        ),\n      },\n    )\n    return data\n  }\n\n  describe('parsing external payload', () => {\n    it('fails if actorDid is missing', () => {\n      const serialized = JSON.stringify({\n        attemptId,\n      } satisfies Partial<KwsExternalPayload>)\n\n      expect(() => parseExternalPayload(serialized)).toThrow(\n        `Invalid external payload`,\n      )\n    })\n\n    it('fails if attemptId is missing', () => {\n      const serialized = JSON.stringify({\n        actorDid,\n      } satisfies Partial<KwsExternalPayload>)\n\n      expect(() => parseExternalPayload(serialized)).toThrow(\n        `Invalid external payload`,\n      )\n    })\n\n    it('fails if extra field is present', () => {\n      const serialized = JSON.stringify({\n        actorDid,\n        attemptId,\n        extra: 'field',\n      } satisfies KwsExternalPayload & { extra: string })\n\n      expect(() => parseExternalPayload(serialized)).toThrow(\n        `Invalid external payload`,\n      )\n    })\n\n    it('does not fail if all fields are set', () => {\n      const externalPayload: KwsExternalPayload = {\n        actorDid,\n        attemptId,\n      }\n      const serialized = JSON.stringify(externalPayload)\n\n      const parsed = parseExternalPayload(serialized)\n      expect(parsed).toStrictEqual(externalPayload)\n    })\n  })\n\n  it('fetches AA state correctly if user never did the flow', async () => {\n    const aliceState = await getAgeAssurance(actorDid)\n    expect(aliceState).toEqual({\n      status: 'unknown',\n    })\n  })\n\n  it('validates email used for AA flow', async () => {\n    await expect(initAgeAssurance(actorDid, 'invalid-email')).rejects.toThrow(\n      'This email address is not supported,',\n    )\n  })\n\n  it('ensures user cannot re-init flow from terminal state', async () => {\n    const actor = sc.dids.bob\n    const state0 = await getAgeAssurance(actor)\n    expect(state0).toStrictEqual({\n      status: 'unknown',\n    })\n\n    const init1 = await initAgeAssurance(actor)\n    expect(init1).toStrictEqual({\n      status: 'pending',\n      lastInitiatedAt: expect.any(String),\n    })\n\n    const init2 = await initAgeAssurance(actor)\n    expect(init2).toStrictEqual({\n      status: 'pending',\n      lastInitiatedAt: expect.any(String),\n    })\n\n    /**\n     * Can re-init flow if the state is pending.\n     */\n    expect(sendEmailMock).toHaveBeenCalledTimes(2)\n\n    const externalPayload: KwsExternalPayload = {\n      actorDid: actor,\n      attemptId,\n    }\n    const status = { verified: true }\n    await kwsServer.callVerificationResponse(network.bsky.url, {\n      externalPayload,\n      status,\n    })\n    const finalizedState = await getAgeAssurance(actor)\n    expect(finalizedState).toStrictEqual({\n      status: 'assured',\n      lastInitiatedAt: expect.any(String),\n    })\n\n    await expect(initAgeAssurance(actor)).rejects.toThrowError(\n      `Cannot initiate age assurance flow from current state: assured`,\n    )\n  })\n\n  describe('verification response flow', () => {\n    it('performs the AA flow', async () => {\n      const state0 = await getAgeAssurance(actorDid)\n      expect(state0).toStrictEqual({\n        status: 'unknown',\n      })\n\n      const state1 = await initAgeAssurance(actorDid)\n      expect(state1).toStrictEqual({\n        status: 'pending',\n        lastInitiatedAt: expect.any(String),\n      })\n      expect(sendEmailMock).toHaveBeenCalledTimes(1)\n\n      const externalPayload: KwsExternalPayload = {\n        actorDid,\n        attemptId,\n      }\n      const status = { verified: true }\n      const verificationRes = await kwsServer.callVerificationResponse(\n        network.bsky.url,\n        { externalPayload, status },\n      )\n      expect(verificationRes.status).toBe(302)\n      expect(verificationRes.headers.get('Location')).toBe(\n        `${redirectUrl}?actorDid=${encodeURIComponent(actorDid)}&result=success`,\n      )\n\n      const state2 = await getAgeAssurance(actorDid)\n      expect(state2).toStrictEqual({\n        status: 'assured',\n        lastInitiatedAt: expect.any(String),\n      })\n    })\n\n    it('does not assure if the verification response has status not verified', async () => {\n      await initAgeAssurance(actorDid)\n\n      const externalPayload: KwsExternalPayload = {\n        actorDid,\n        attemptId,\n      }\n      const status = { verified: false }\n      const verificationRes = await kwsServer.callVerificationResponse(\n        network.bsky.url,\n        { externalPayload, status },\n      )\n      expect(verificationRes.status).toBe(302)\n      expect(verificationRes.headers.get('Location')).toBe(\n        `${redirectUrl}?result=unknown`,\n      )\n\n      const state = await getAgeAssurance(actorDid)\n      expect(state).toStrictEqual({\n        status: 'pending',\n        lastInitiatedAt: expect.any(String),\n      })\n    })\n  })\n\n  describe('webhook flow', () => {\n    it('performs the AA flow', async () => {\n      const state0 = await getAgeAssurance(actorDid)\n      expect(state0).toStrictEqual({\n        status: 'unknown',\n      })\n\n      const state1 = await initAgeAssurance(actorDid)\n      expect(state1).toStrictEqual({\n        status: 'pending',\n        lastInitiatedAt: expect.any(String),\n      })\n      expect(sendEmailMock).toHaveBeenCalledTimes(1)\n\n      const webhookRes = await kwsServer.callWebhook(network.bsky.url, {\n        payload: {\n          externalPayload: {\n            actorDid,\n            attemptId,\n          },\n          status: {\n            verified: true,\n          },\n        },\n      })\n      expect(webhookRes.status).toBe(200)\n\n      const state2 = await getAgeAssurance(actorDid)\n      expect(state2).toStrictEqual({\n        status: 'assured',\n        lastInitiatedAt: expect.any(String),\n      })\n    })\n\n    it('does not assure if the webhook has status not verified', async () => {\n      await initAgeAssurance(actorDid)\n\n      const webhookRes = await kwsServer.callWebhook(network.bsky.url, {\n        payload: {\n          externalPayload: {\n            actorDid,\n            attemptId,\n          },\n          status: {\n            verified: false,\n          },\n        },\n      })\n      expect(webhookRes.status).toBe(500)\n\n      const state = await getAgeAssurance(actorDid)\n      expect(state).toStrictEqual({\n        status: 'pending',\n        lastInitiatedAt: expect.any(String),\n      })\n    })\n  })\n})\n\nconst clearPrivateData = async (db: Database) => {\n  await db.db.deleteFrom('private_data').execute()\n}\n\nconst clearActorAgeAssurance = async (db: Database) => {\n  await db.db\n    .updateTable('actor')\n    .set({\n      ageAssuranceStatus: null,\n      ageAssuranceLastInitiatedAt: null,\n    })\n    .execute()\n}\n\nclass MockKwsServer {\n  private verificationSecret: string\n  private webhookSecret: string\n  private app: Application\n  private server: Server\n\n  constructor({\n    verificationSecret,\n    webhookSecret,\n    authMock,\n    sendEmailMock,\n  }: {\n    verificationSecret: string\n    webhookSecret: string\n    authMock: jest.Mock\n    sendEmailMock: jest.Mock\n  }) {\n    this.verificationSecret = verificationSecret\n    this.webhookSecret = webhookSecret\n\n    this.app = express()\n      .post('/auth/realms/kws/protocol/openid-connect/token', (req, res) =>\n        authMock(req, res),\n      )\n      .post('/v1/verifications/send-email', (req, res) =>\n        sendEmailMock(req, res),\n      )\n\n    this.server = createServer(this.app)\n  }\n\n  async listen(port?: number) {\n    this.server.listen(port)\n    await once(this.server, 'listening')\n  }\n\n  async stop() {\n    this.server.close()\n    await once(this.server, 'close')\n  }\n\n  callVerificationResponse(\n    bskyUrl: string,\n    query: Omit<KwsVerificationQuery, 'signature'>,\n  ) {\n    const externalPayloadJson = JSON.stringify(query.externalPayload)\n    const statusJson = JSON.stringify(query.status)\n\n    const sig = crypto\n      .createHmac('sha256', this.verificationSecret)\n      .update(`${statusJson}:${externalPayloadJson}`)\n      .digest('hex')\n\n    const queryString = new URLSearchParams({\n      externalPayload: externalPayloadJson,\n      signature: sig,\n      status: statusJson,\n    }).toString()\n\n    return fetch(\n      `${bskyUrl}/external/kws/age-assurance-verification?${queryString}`,\n      {\n        method: 'GET',\n        redirect: 'manual',\n      },\n    )\n  }\n\n  callWebhook(bskyUrl: string, body: KwsWebhookBody): Promise<Response> {\n    const withSerializedExternalPayload = {\n      ...body,\n      payload: {\n        ...body.payload,\n        externalPayload: serializeExternalPayload(body.payload.externalPayload),\n      },\n    }\n    const bodyBuffer = Buffer.from(\n      JSON.stringify(withSerializedExternalPayload),\n    )\n\n    const timestamp = new Date().valueOf()\n    const sig = crypto\n      .createHmac('sha256', this.webhookSecret)\n      .update(`${timestamp}.${bodyBuffer}`)\n      .digest('hex')\n\n    return fetch(`${bskyUrl}/external/kws/age-assurance-webhook`, {\n      method: 'POST',\n      body: bodyBuffer,\n      headers: {\n        'x-kws-signature': `t=${timestamp},v1=${sig}`,\n        'Content-Type': 'application/json',\n      },\n    })\n  }\n\n  get url() {\n    const address = this.server.address() as AddressInfo\n    return `http://localhost:${address.port}`\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/author-feed.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, authorFeedSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport {\n  Record as Profile,\n  validateRecord as validatePostRecord,\n} from '../../src/lexicon/types/app/bsky/actor/profile'\nimport { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images'\nimport { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia'\nimport { isView as isVideoEmbed } from '../../src/lexicon/types/app/bsky/embed/video'\nimport {\n  isPostView,\n  isReasonPin,\n} from '../../src/lexicon/types/app/bsky/feed/defs'\nimport { OutputSchema as GetAuthorFeedOutputSchema } from '../../src/lexicon/types/app/bsky/feed/getAuthorFeed'\nimport {\n  ReplyRef,\n  isRecord,\n  validateReplyRef,\n} from '../../src/lexicon/types/app/bsky/feed/post'\nimport { asPredicate } from '../../src/lexicon/util'\nimport { uriToDid } from '../../src/util/uris'\nimport { VideoEmbed } from '../../src/views/types'\nimport {\n  forSnapshot,\n  paginateAll,\n  stripViewer,\n  stripViewerFromPost,\n} from '../_util'\n\nconst isValidReplyRef = asPredicate(validateReplyRef)\nconst isValidProfile = asPredicate(validatePostRecord)\n\ndescribe('pds author feed views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n  let eve: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_author_feed',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await authorFeedSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    eve = sc.dids.eve\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  // @TODO(bsky) blocked by actor takedown via labels.\n  // @TODO(bsky) blocked by record takedown via labels.\n\n  it('fetches full author feeds for self (sorted, minimal viewer state).', async () => {\n    const aliceForAlice = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceForAlice.data.feed)).toMatchSnapshot()\n\n    const bobForBob = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[bob].handle },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(forSnapshot(bobForBob.data.feed)).toMatchSnapshot()\n\n    const carolForCarol = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[carol].handle },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(forSnapshot(carolForCarol.data.feed)).toMatchSnapshot()\n\n    const danForDan = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[dan].handle },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(forSnapshot(danForDan.data.feed)).toMatchSnapshot()\n  })\n\n  it(\"reflects fetching user's state in the feed.\", async () => {\n    const aliceForCarol = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    aliceForCarol.data.feed.forEach((postView) => {\n      const { viewer, uri } = postView.post\n      expect(viewer?.like).toEqual(sc.likes[carol][uri]?.toString())\n      expect(viewer?.repost).toEqual(sc.reposts[carol][uri]?.toString())\n    })\n\n    expect(forSnapshot(aliceForCarol.data.feed)).toMatchSnapshot()\n  })\n\n  it('paginates', async () => {\n    const results = (results: GetAuthorFeedOutputSchema[]) =>\n      results.flatMap((res) => res.feed)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.feed.getAuthorFeed(\n        {\n          actor: sc.accounts[alice].handle,\n          cursor,\n          limit: 2,\n        },\n        {\n          headers: await network.serviceHeaders(\n            dan,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.feed.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(full.data.feed.length).toEqual(4)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches results unauthed.', async () => {\n    const { data: authed } = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: sc.accounts[alice].handle,\n    })\n    expect(unauthed.feed.length).toBeGreaterThan(0)\n    expect(unauthed.feed).toEqual(\n      authed.feed.map((item) => {\n        const result = {\n          ...item,\n          post: stripViewerFromPost(item.post),\n        }\n        if (item.reply) {\n          result.reply = {\n            parent: stripViewerFromPost(item.reply.parent, true),\n            root: stripViewerFromPost(item.reply.root, true),\n            grandparentAuthor:\n              item.reply.grandparentAuthor &&\n              stripViewer(item.reply.grandparentAuthor),\n          }\n        }\n        return result\n      }),\n    )\n  })\n\n  it('non-admins blocked by actor takedown.', async () => {\n    const { data: preBlock } = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(preBlock.feed.length).toBeGreaterThan(0)\n\n    await network.bsky.ctx.dataplane.takedownActor({\n      did: alice,\n    })\n\n    const attemptAsUser = agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    await expect(attemptAsUser).rejects.toThrow('Profile not found')\n\n    const attemptAsAdmin = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: alice },\n      { headers: network.bsky.adminAuthHeaders() },\n    )\n    expect(attemptAsAdmin.data.feed.length).toEqual(preBlock.feed.length)\n\n    // Cleanup\n    await network.bsky.ctx.dataplane.untakedownActor({\n      did: alice,\n    })\n  })\n\n  it('blocked by record takedown.', async () => {\n    const { data: preBlock } = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n\n    expect(preBlock.feed.length).toBeGreaterThan(0)\n\n    const post = preBlock.feed[0].post\n\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: post.uri,\n    })\n\n    const [{ data: postBlockAsUser }, { data: postBlockAsAdmin }] =\n      await Promise.all([\n        agent.api.app.bsky.feed.getAuthorFeed(\n          { actor: alice },\n          {\n            headers: await network.serviceHeaders(\n              carol,\n              ids.AppBskyFeedGetAuthorFeed,\n            ),\n          },\n        ),\n        agent.api.app.bsky.feed.getAuthorFeed(\n          { actor: alice },\n          { headers: network.bsky.adminAuthHeaders() },\n        ),\n      ])\n\n    expect(postBlockAsUser.feed.length).toEqual(preBlock.feed.length - 1)\n    expect(postBlockAsUser.feed.map((item) => item.post.uri)).not.toContain(\n      post.uri,\n    )\n    expect(postBlockAsAdmin.feed.length).toEqual(preBlock.feed.length)\n    expect(postBlockAsAdmin.feed.map((item) => item.post.uri)).toContain(\n      post.uri,\n    )\n\n    // Cleanup\n    await network.bsky.ctx.dataplane.untakedownRecord({\n      recordUri: post.uri,\n    })\n  })\n\n  it('can filter by posts_with_media', async () => {\n    const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: carol,\n      filter: 'posts_with_media',\n    })\n\n    expect(carolFeed.feed.length).toBeGreaterThan(0)\n    assert(\n      carolFeed.feed.every(({ post }) => {\n        const isRecordWithActorMedia =\n          isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media)\n        const isActorMedia = isImageEmbed(post.embed)\n        const isFromActor = post.author.did === carol\n\n        return (isRecordWithActorMedia || isActorMedia) && isFromActor\n      }),\n    )\n\n    const { data: bobFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: bob,\n      filter: 'posts_with_media',\n    })\n\n    assert(\n      bobFeed.feed.every(({ post }) => {\n        return isImageEmbed(post.embed) && post.author.did === bob\n      }),\n    )\n\n    const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: dan,\n      filter: 'posts_with_media',\n    })\n\n    expect(danFeed.feed.length).toEqual(0)\n  })\n\n  it('can filter by posts_with_video', async () => {\n    const { data: carolFeedBefore } =\n      await agent.api.app.bsky.feed.getAuthorFeed({\n        actor: carol,\n        filter: 'posts_with_video',\n      })\n    expect(carolFeedBefore.feed).toHaveLength(0)\n\n    const { data: video } = await pdsAgent.api.com.atproto.repo.uploadBlob(\n      Buffer.from('notarealvideo'),\n      {\n        headers: sc.getHeaders(sc.dids.carol),\n        encoding: 'image/mp4',\n      },\n    )\n\n    await sc.post(carol, 'video post', undefined, undefined, undefined, {\n      embed: {\n        $type: 'app.bsky.embed.video',\n        video: video.blob,\n        alt: 'alt text',\n        aspectRatio: { height: 3, width: 4 },\n      } satisfies VideoEmbed,\n    })\n    await network.processAll()\n\n    const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: carol,\n      filter: 'posts_with_video',\n    })\n\n    expect(carolFeed.feed).toHaveLength(1)\n    expect(\n      carolFeed.feed.every(({ post }) => {\n        const isRecordWithActorMedia =\n          isEmbedRecordWithMedia(post.embed) && isVideoEmbed(post.embed?.media)\n        const isActorMedia = isVideoEmbed(post.embed)\n        const isFromActor = post.author.did === carol\n\n        return (isRecordWithActorMedia || isActorMedia) && isFromActor\n      }),\n    ).toBeTruthy()\n\n    const { data: bobFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: bob,\n      filter: 'posts_with_video',\n    })\n\n    expect(\n      bobFeed.feed.every(({ post }) => {\n        return isVideoEmbed(post.embed) && post.author.did === bob\n      }),\n    ).toBeTruthy()\n\n    const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: dan,\n      filter: 'posts_with_video',\n    })\n\n    expect(danFeed.feed.length).toEqual(0)\n  })\n\n  it('filters by posts_no_replies', async () => {\n    const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: carol,\n      filter: 'posts_no_replies',\n    })\n\n    assert(\n      carolFeed.feed.every(({ post }) => {\n        return (\n          (isRecord(post.record) && !post.record.reply) ||\n          (isRecord(post.record) && post.record.reply)\n        )\n      }),\n    )\n\n    const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: dan,\n      filter: 'posts_no_replies',\n    })\n\n    expect(\n      danFeed.feed.every(({ post }) => {\n        return (\n          (isRecord(post.record) && !post.record.reply) ||\n          (isRecord(post.record) && post.record.reply)\n        )\n      }),\n    ).toBeTruthy()\n  })\n\n  it('posts_and_author_threads includes self-replies', async () => {\n    const { data: eveFeed } = await agent.api.app.bsky.feed.getAuthorFeed({\n      actor: eve,\n      filter: 'posts_and_author_threads',\n    })\n\n    expect(eveFeed.feed.length).toEqual(6)\n    expect(\n      eveFeed.feed.some(({ post }) => {\n        const replyByEve =\n          isRecord(post.record) && post.record.reply && post.author.did === eve\n        return replyByEve\n      }),\n    ).toBeTruthy()\n    // does not include eve's replies to fred, even within her own thread.\n    expect(\n      eveFeed.feed.every(({ post, reply }) => {\n        if (\n          !post ||\n          !isRecord(post.record) ||\n          !isValidReplyRef(post.record.reply)\n        ) {\n          return true // not a reply\n        }\n        const replyToEve = isReplyTo(post.record.reply, eve)\n        const replyToReplyByEve =\n          reply &&\n          isPostView(reply.parent) &&\n          isRecord(reply.parent.record) &&\n          (!isValidReplyRef(reply.parent.record.reply) ||\n            isReplyTo(reply.parent.record.reply, eve))\n        return replyToEve && replyToReplyByEve\n      }),\n    ).toBeTruthy()\n    // reposts are preserved\n    expect(\n      eveFeed.feed.some(({ post, reason }) => {\n        const repostOfOther =\n          reason && isRecord(post.record) && post.author.did !== eve\n        return repostOfOther\n      }),\n    ).toBeTruthy()\n  })\n\n  describe('pins', () => {\n    async function createAndPinPost() {\n      const post = await sc.post(alice, 'pinned post')\n      await network.processAll()\n\n      const profile = await pdsAgent.com.atproto.repo.getRecord({\n        repo: alice,\n        collection: 'app.bsky.actor.profile',\n        rkey: 'self',\n      })\n\n      assert(isValidProfile(profile.data.value))\n\n      const newProfile: Profile = {\n        ...profile.data.value,\n        pinnedPost: {\n          uri: post.ref.uriStr,\n          cid: post.ref.cid.toString(),\n        },\n      }\n\n      await sc.updateProfile(alice, newProfile)\n\n      await network.processAll()\n\n      return post\n    }\n\n    it('params.includePins = true, pin is in first page of results', async () => {\n      await sc.post(alice, 'not pinned post')\n      const post = await createAndPinPost()\n      await sc.post(alice, 'not pinned post')\n      await network.processAll()\n      const { data } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.accounts[alice].handle, includePins: true },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n\n      const pinnedPosts = data.feed.filter(\n        (item) => item.post.uri === post.ref.uriStr,\n      )\n      expect(pinnedPosts.length).toEqual(1)\n\n      const pinnedPost = data.feed.at(0)\n      expect(pinnedPost?.post?.uri).toEqual(post.ref.uriStr)\n      assert(pinnedPost?.post?.viewer?.pinned)\n      assert(isReasonPin(pinnedPost?.reason))\n\n      const notPinnedPost = data.feed.at(1)\n      expect(notPinnedPost?.post?.viewer?.pinned).toBeFalsy()\n      expect(forSnapshot(data.feed)).toMatchSnapshot()\n    })\n\n    it('params.includePins = true, pin is NOT in first page of results', async () => {\n      const post = await createAndPinPost()\n      await sc.post(alice, 'not pinned post')\n      await sc.post(alice, 'not pinned post')\n      await network.processAll()\n      const { data: page1 } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.accounts[alice].handle, includePins: true, limit: 2 },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n\n      // exists with `reason`\n      const pinnedPost = page1.feed.find(\n        (item) => item.post.uri === post.ref.uriStr,\n      )\n      expect(pinnedPost?.post?.uri).toEqual(post.ref.uriStr)\n      assert(pinnedPost?.post?.viewer?.pinned)\n      assert(isReasonPin(pinnedPost?.reason))\n      expect(forSnapshot(page1.feed)).toMatchSnapshot()\n\n      const { data: page2 } = await agent.api.app.bsky.feed.getAuthorFeed(\n        {\n          actor: sc.accounts[alice].handle,\n          includePins: true,\n          cursor: page1.cursor,\n        },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n\n      // exists without `reason`\n      const laterPinnedPost = page2.feed.find(\n        (item) => item.post.uri === post.ref.uriStr,\n      )\n      expect(laterPinnedPost?.post?.uri).toEqual(post.ref.uriStr)\n      assert(laterPinnedPost?.post?.viewer?.pinned)\n      expect(isReasonPin(laterPinnedPost?.reason)).toBeFalsy()\n      expect(forSnapshot(page2.feed)).toMatchSnapshot()\n    })\n\n    it('params.includePins = false', async () => {\n      const post = await createAndPinPost()\n      const { data } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.accounts[alice].handle },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n\n      // exists without `reason`\n      const pinnedPost = data.feed.find(\n        (item) => item.post.uri === post.ref.uriStr,\n      )\n      expect(isReasonPin(pinnedPost?.reason)).toBeFalsy()\n      expect(forSnapshot(data.feed)).toMatchSnapshot()\n    })\n\n    it(\"cannot pin someone else's post\", async () => {\n      const bobPost = await sc.post(bob, 'pinned post')\n      await sc.post(alice, 'not pinned post')\n      await network.processAll()\n\n      const profile = await pdsAgent.com.atproto.repo.getRecord({\n        repo: alice,\n        collection: 'app.bsky.actor.profile',\n        rkey: 'self',\n      })\n\n      assert(isValidProfile(profile.data.value))\n\n      const newProfile: Profile = {\n        ...profile.data.value,\n        pinnedPost: {\n          uri: bobPost.ref.uriStr,\n          cid: bobPost.ref.cid.toString(),\n        },\n      }\n\n      await sc.updateProfile(alice, newProfile)\n\n      await network.processAll()\n\n      const { data } = await agent.api.app.bsky.feed.getAuthorFeed(\n        { actor: sc.accounts[alice].handle },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetAuthorFeed,\n          ),\n        },\n      )\n\n      const pinnedPost = data.feed.find(\n        (item) => item.post.uri === bobPost.ref.uriStr,\n      )\n      expect(pinnedPost).toBeUndefined()\n      expect(forSnapshot(data.feed)).toMatchSnapshot()\n    })\n  })\n})\n\nfunction isReplyTo(reply: ReplyRef, did: string) {\n  return uriToDid(reply.root.uri) === did && uriToDid(reply.parent.uri) === did\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/block-lists.test.ts",
    "content": "import { AtUri, AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { forSnapshot } from '../_util'\n\ndescribe('pds views with blocking from block lists', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let aliceReplyToDan: { ref: RecordRef }\n\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'views_block_lists',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    // add follows to ensure blocks work even w follows\n    await sc.follow(carol, dan)\n    await sc.follow(dan, carol)\n    aliceReplyToDan = await sc.reply(\n      alice,\n      sc.posts[dan][0].ref,\n      sc.posts[dan][0].ref,\n      'alice replies to dan',\n    )\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  let listUri: string\n\n  it('creates a list with some items', async () => {\n    const avatar = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    // alice creates block list with bob & carol that dan uses\n    const list = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: alice },\n      {\n        name: 'alice blocks',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: 'big list of blocks',\n        avatar: avatar.image,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    listUri = list.uri\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: alice },\n      {\n        subject: sc.dids.bob,\n        list: list.uri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: alice },\n      {\n        subject: sc.dids.carol,\n        list: list.uri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: alice },\n      {\n        subject: sc.dids.dan,\n        list: list.uri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n  })\n\n  it('uses a list for blocks', async () => {\n    await pdsAgent.api.app.bsky.graph.listblock.create(\n      { repo: dan },\n      {\n        subject: listUri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(dan),\n    )\n    await network.processAll()\n  })\n\n  it('blocks thread post', async () => {\n    const { carol, dan } = sc.dids\n    const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[carol][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(threadAlice.thread).toEqual(\n      expect.objectContaining({\n        $type: 'app.bsky.feed.defs#blockedPost',\n        uri: sc.posts[carol][0].ref.uriStr,\n        blocked: true,\n      }),\n    )\n    const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[dan][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(threadCarol.thread).toEqual(\n      expect.objectContaining({\n        $type: 'app.bsky.feed.defs#blockedPost',\n        uri: sc.posts[dan][0].ref.uriStr,\n        blocked: true,\n      }),\n    )\n  })\n\n  it('blocks thread reply', async () => {\n    // Contains reply by carol\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread)).toMatchSnapshot()\n  })\n\n  it('blocks thread parent', async () => {\n    // Parent is a post by dan\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: aliceReplyToDan.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread)).toMatchSnapshot()\n  })\n\n  it('blocks record embeds', async () => {\n    // Contains a deep embed of carol's post, blocked by dan\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 0, uri: sc.posts[alice][2].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread)).toMatchSnapshot()\n  })\n\n  it('errors on getting author feed', async () => {\n    const attempt1 = agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    await expect(attempt1).rejects.toMatchObject({\n      error: 'BlockedActor',\n    })\n\n    const attempt2 = agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    await expect(attempt2).rejects.toMatchObject({\n      error: 'BlockedByActor',\n    })\n  })\n\n  it('strips blocked users out of getTimeline', async () => {\n    const resCarol = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(\n      resCarol.data.feed.some((post) => post.post.author.did === dan),\n    ).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      resDan.data.feed.some((post) =>\n        [bob, carol].includes(post.post.author.did),\n      ),\n    ).toBeFalsy()\n  })\n\n  it('returns block status on getProfile', async () => {\n    const resCarol = await agent.api.app.bsky.actor.getProfile(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(resCarol.data.viewer?.blocking).toBeUndefined()\n    expect(resCarol.data.viewer?.blockingByList).toBeUndefined()\n    expect(resCarol.data.viewer?.blockedBy).toBe(true)\n\n    const resDan = await agent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),\n      },\n    )\n    expect(resDan.data.viewer?.blocking).toBeDefined()\n    expect(resDan.data.viewer?.blockingByList?.uri).toEqual(\n      resDan.data.viewer?.blocking,\n    )\n    expect(resDan.data.viewer?.blockedBy).toBe(false)\n  })\n\n  it('returns block status on getProfiles', async () => {\n    const resCarol = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [alice, dan] },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorGetProfiles,\n        ),\n      },\n    )\n    expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined()\n    expect(resCarol.data.profiles[0].viewer?.blockingByList).toBeUndefined()\n    expect(resCarol.data.profiles[0].viewer?.blockedBy).toBe(false)\n    expect(resCarol.data.profiles[1].viewer?.blocking).toBeUndefined()\n    expect(resCarol.data.profiles[1].viewer?.blockingByList).toBeUndefined()\n    expect(resCarol.data.profiles[1].viewer?.blockedBy).toBe(true)\n\n    const resDan = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [alice, carol] },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles),\n      },\n    )\n    expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined()\n    expect(resDan.data.profiles[0].viewer?.blockingByList).toBeUndefined()\n    expect(resDan.data.profiles[0].viewer?.blockedBy).toBe(false)\n    expect(resDan.data.profiles[1].viewer?.blocking).toBeDefined()\n    expect(resDan.data.profiles[1].viewer?.blockingByList?.uri).toEqual(\n      resDan.data.profiles[1].viewer?.blocking,\n    )\n    expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false)\n  })\n\n  it('ignores self-blocks', async () => {\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: dan }, // dan subscribes to list that contains himself\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),\n      },\n    )\n    expect(res.data.viewer?.blocking).toBeUndefined()\n    expect(res.data.viewer?.blockingByList).toBeUndefined()\n    expect(res.data.viewer?.blockedBy).toBe(false)\n  })\n\n  it('does not return notifs for blocked accounts', async () => {\n    const resCarol = await agent.api.app.bsky.notification.listNotifications(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      resCarol.data.notifications.some((notif) => notif.author.did === dan),\n    ).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.notification.listNotifications(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      resDan.data.notifications.some((notif) => notif.author.did === carol),\n    ).toBeFalsy()\n  })\n\n  it('does not return blocked accounts in actor search', async () => {\n    const resCarol = await agent.api.app.bsky.actor.searchActors(\n      {\n        term: 'dan.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorSearchActors,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.actor.searchActors(\n      {\n        term: 'carol.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorSearchActors,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()\n  })\n\n  it('does not return blocked accounts in actor search typeahead', async () => {\n    const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'dan.tes',\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'carol.tes',\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()\n  })\n\n  it('does return blocked accounts in actor search typeahead when term is exact handle', async () => {\n    const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'dan.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeTruthy()\n\n    const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'carol.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeTruthy()\n  })\n\n  it('does not return blocked accounts in get suggestions', async () => {\n    // unfollow so they _would_ show up in suggestions if not for block\n    await sc.unfollow(carol, dan)\n    await sc.unfollow(dan, carol)\n    await network.processAll()\n\n    const resCarol = await agent.api.app.bsky.actor.getSuggestions(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.actor.getSuggestions(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()\n  })\n\n  it('returns the contents of a list', async () => {\n    const res = await agent.api.app.bsky.graph.getList(\n      { list: listUri },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates getList', async () => {\n    const full = await agent.api.app.bsky.graph.getList(\n      { list: listUri },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    const first = await agent.api.app.bsky.graph.getList(\n      { list: listUri, limit: 1 },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    const second = await agent.api.app.bsky.graph.getList(\n      { list: listUri, cursor: first.data.cursor },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    const combined = [...first.data.items, ...second.data.items]\n    expect(combined).toEqual(full.data.items)\n  })\n\n  let otherListUri: string\n\n  it('returns lists associated with a user', async () => {\n    const listRes = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: alice },\n      {\n        name: 'new list',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: 'blah blah',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    otherListUri = listRes.uri\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.graph.getLists(\n      { actor: alice },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates getLists', async () => {\n    const full = await agent.api.app.bsky.graph.getLists(\n      { actor: alice },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    const first = await agent.api.app.bsky.graph.getLists(\n      { actor: alice, limit: 1 },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    const second = await agent.api.app.bsky.graph.getLists(\n      { actor: alice, cursor: first.data.cursor },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    const combined = [...first.data.lists, ...second.data.lists]\n    expect(combined).toEqual(full.data.lists)\n  })\n\n  it('returns a users own list blocks', async () => {\n    await pdsAgent.api.app.bsky.graph.listblock.create(\n      { repo: dan },\n      {\n        subject: otherListUri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(dan),\n    )\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.graph.getListBlocks(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListBlocks,\n        ),\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates getListBlocks', async () => {\n    const full = await agent.api.app.bsky.graph.getListBlocks(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListBlocks,\n        ),\n      },\n    )\n    const first = await agent.api.app.bsky.graph.getListBlocks(\n      { limit: 1 },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListBlocks,\n        ),\n      },\n    )\n    const second = await agent.api.app.bsky.graph.getListBlocks(\n      { cursor: first.data.cursor },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListBlocks,\n        ),\n      },\n    )\n    const combined = [...first.data.lists, ...second.data.lists]\n    expect(combined).toEqual(full.data.lists)\n  })\n\n  it('does not apply \"curate\" blocklists', async () => {\n    const parsedUri = new AtUri(listUri)\n    await pdsAgent.api.com.atproto.repo.putRecord(\n      {\n        repo: parsedUri.hostname,\n        collection: parsedUri.collection,\n        rkey: parsedUri.rkey,\n        record: {\n          name: 'curate list',\n          purpose: 'app.bsky.graph.defs#curatelist',\n          createdAt: new Date().toISOString(),\n        },\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await network.processAll()\n\n    const resCarol = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(\n      resCarol.data.feed.some((post) => post.post.author.did === dan),\n    ).toBeTruthy()\n\n    const resDan = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      resDan.data.feed.some((post) =>\n        [bob, carol].includes(post.post.author.did),\n      ),\n    ).toBeTruthy()\n  })\n\n  it('does not apply deleted blocklists (whose items are still around)', async () => {\n    const parsedUri = new AtUri(listUri)\n    await pdsAgent.api.app.bsky.graph.list.delete(\n      {\n        repo: parsedUri.hostname,\n        rkey: parsedUri.rkey,\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n\n    const resCarol = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(\n      resCarol.data.feed.some((post) => post.post.author.did === dan),\n    ).toBeTruthy()\n\n    const resDan = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      resDan.data.feed.some((post) =>\n        [bob, carol].includes(post.post.author.did),\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/blocks.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtUri, AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { isView as isRecordEmbedView } from '../../src/lexicon/types/app/bsky/embed/record'\nimport { isPostView } from '../../src/lexicon/types/app/bsky/feed/defs'\nimport { assertIsThreadViewPost, forSnapshot } from '../_util'\n\ndescribe('pds views with blocking', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let danBlockCarol: { uri: string }\n  let aliceReplyToDan: { ref: RecordRef }\n  let carolReplyToDan: { ref: RecordRef }\n\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n  let danBlockUri: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_block',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    // add follows to ensure blocks work even w follows\n    await sc.follow(carol, dan)\n    await sc.follow(dan, carol)\n    aliceReplyToDan = await sc.reply(\n      alice,\n      sc.posts[dan][0].ref,\n      sc.posts[dan][0].ref,\n      'alice replies to dan',\n    )\n    const _carolReplyToAliceReplyToDan = await sc.reply(\n      carol,\n      sc.posts[dan][0].ref,\n      aliceReplyToDan.ref,\n      \"carol replies to alice's reply to dan\",\n    )\n    carolReplyToDan = await sc.reply(\n      carol,\n      sc.posts[dan][0].ref,\n      sc.posts[dan][0].ref,\n      'carol replies to dan',\n    )\n    // dan blocks carol\n    danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dan },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(dan),\n    )\n    danBlockUri = danBlockCarol.uri\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('blocks thread post', async () => {\n    const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[carol][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(threadAlice).toEqual({\n      thread: {\n        $type: 'app.bsky.feed.defs#blockedPost',\n        uri: sc.posts[carol][0].ref.uriStr,\n        blocked: true,\n        author: {\n          did: carol,\n          viewer: {\n            blockedBy: false,\n            blocking: danBlockUri,\n          },\n        },\n      },\n    })\n    const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[dan][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(threadCarol).toEqual({\n      thread: {\n        $type: 'app.bsky.feed.defs#blockedPost',\n        uri: sc.posts[dan][0].ref.uriStr,\n        blocked: true,\n        author: {\n          did: dan,\n          viewer: {\n            blockedBy: true,\n          },\n        },\n      },\n    })\n  })\n\n  it('blocks thread reply', async () => {\n    // Contains reply by carol\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread)).toMatchSnapshot()\n  })\n\n  it('loads blocked reply as anchor with blocked parent', async () => {\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: carolReplyToDan.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    assertIsThreadViewPost(thread.thread)\n\n    expect(thread.thread.post.uri).toEqual(carolReplyToDan.ref.uriStr)\n    expect(thread.thread.parent).toMatchObject({\n      $type: 'app.bsky.feed.defs#blockedPost',\n      uri: sc.posts[dan][0].ref.uriStr,\n    })\n  })\n\n  it('blocks thread parent', async () => {\n    // Parent is a post by dan\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: aliceReplyToDan.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread)).toMatchSnapshot()\n  })\n\n  it('blocks record embeds', async () => {\n    // Contains a deep embed of carol's post, blocked by dan\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 0, uri: sc.posts[alice][2].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread)).toMatchSnapshot()\n  })\n\n  it('errors on getting author feed', async () => {\n    const attempt1 = agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    await expect(attempt1).rejects.toMatchObject({\n      error: 'BlockedActor',\n    })\n\n    const attempt2 = agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    await expect(attempt2).rejects.toMatchObject({\n      error: 'BlockedByActor',\n    })\n  })\n\n  it('strips blocked users out of getTimeline', async () => {\n    const resCarol = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    // dan's posts don't appear, nor alice's reply to dan, nor carol's reply to alice (which was a reply to dan)\n    expect(\n      resCarol.data.feed.some(\n        (post) =>\n          post.post.author.did === dan ||\n          (isPostView(post.reply?.parent) &&\n            post.reply.parent.author.did === dan) ||\n          post.reply?.grandparentAuthor?.did === dan,\n      ),\n    ).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      resDan.data.feed.some(\n        (post) =>\n          post.post.author.did === carol ||\n          (isPostView(post.reply?.parent) &&\n            post.reply.parent.author.did === carol) ||\n          post.reply?.grandparentAuthor?.did === carol,\n      ),\n    ).toBeFalsy()\n  })\n\n  it('strips blocked users out of getListFeed', async () => {\n    const listRef = await sc.createList(alice, 'test list', 'curate')\n    await sc.addToList(alice, alice, listRef)\n    await sc.addToList(alice, carol, listRef)\n    await sc.addToList(alice, dan, listRef)\n\n    const resCarol = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr, limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetListFeed,\n        ),\n      },\n    )\n    expect(\n      resCarol.data.feed.some((post) => post.post.author.did === dan),\n    ).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr, limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetListFeed),\n      },\n    )\n    expect(\n      resDan.data.feed.some((post) => post.post.author.did === carol),\n    ).toBeFalsy()\n  })\n\n  it('returns block status on getProfile', async () => {\n    const resCarol = await agent.api.app.bsky.actor.getProfile(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(resCarol.data.viewer?.blocking).toBeUndefined()\n    expect(resCarol.data.viewer?.blockedBy).toBe(true)\n\n    const resDan = await agent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),\n      },\n    )\n    expect(resDan.data.viewer?.blocking).toBeDefined()\n    expect(resDan.data.viewer?.blockedBy).toBe(false)\n  })\n\n  it('unsets viewer follow state when blocked', async () => {\n    // there are follows between carol and dan\n    const { data: profile } = await agent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),\n      },\n    )\n    expect(profile.viewer?.following).toBeUndefined()\n    expect(profile.viewer?.followedBy).toBeUndefined()\n    const { data: result } = await agent.api.app.bsky.graph.getBlocks(\n      {},\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) },\n    )\n    const blocked = result.blocks.find((block) => block.did === carol)\n    expect(blocked).toBeDefined()\n    expect(blocked?.viewer?.following).toBeUndefined()\n    expect(blocked?.viewer?.followedBy).toBeUndefined()\n  })\n\n  it('returns block status on getProfiles', async () => {\n    const resCarol = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [alice, dan] },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorGetProfiles,\n        ),\n      },\n    )\n    expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined()\n    expect(resCarol.data.profiles[0].viewer?.blockingByList).toBeUndefined()\n    expect(resCarol.data.profiles[0].viewer?.blockedBy).toBe(false)\n    expect(resCarol.data.profiles[1].viewer?.blocking).toBeUndefined()\n    expect(resCarol.data.profiles[1].viewer?.blockingByList).toBeUndefined()\n    expect(resCarol.data.profiles[1].viewer?.blockedBy).toBe(true)\n\n    const resDan = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [alice, carol] },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles),\n      },\n    )\n    expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined()\n    expect(resDan.data.profiles[0].viewer?.blockingByList).toBeUndefined()\n    expect(resDan.data.profiles[0].viewer?.blockedBy).toBe(false)\n    expect(resDan.data.profiles[1].viewer?.blocking).toBeDefined()\n    expect(resDan.data.profiles[1].viewer?.blockingByList).toBeUndefined()\n    expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false)\n  })\n\n  it('does not return block violating follows', async () => {\n    const resCarol = await agent.api.app.bsky.graph.getFollows(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n    expect(resCarol.data.follows.some((f) => f.did === dan)).toBe(false)\n\n    const resDan = await agent.api.app.bsky.graph.getFollows(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n    expect(resDan.data.follows.some((f) => f.did === carol)).toBe(false)\n  })\n\n  it('does not return block violating followers', async () => {\n    const resCarol = await agent.api.app.bsky.graph.getFollowers(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n    expect(resCarol.data.followers.some((f) => f.did === dan)).toBe(false)\n\n    const resDan = await agent.api.app.bsky.graph.getFollowers(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n    expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false)\n  })\n\n  it('does not return posts from blocked users', async () => {\n    const alicePost = sc.posts[alice][0].ref.uriStr\n    const carolPost = sc.posts[carol][0].ref.uriStr\n    const danPost = sc.posts[dan][0].ref.uriStr\n\n    const resCarol = await agent.api.app.bsky.feed.getPosts(\n      { uris: [alicePost, carolPost, danPost] },\n      { headers: await network.serviceHeaders(carol, ids.AppBskyFeedGetPosts) },\n    )\n    expect(resCarol.data.posts.some((p) => p.uri === alicePost)).toBe(true)\n    expect(resCarol.data.posts.some((p) => p.uri === carolPost)).toBe(true)\n    expect(resCarol.data.posts.some((p) => p.uri === danPost)).toBe(false)\n\n    const resDan = await agent.api.app.bsky.feed.getPosts(\n      { uris: [alicePost, carolPost, danPost] },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetPosts) },\n    )\n    expect(resDan.data.posts.some((p) => p.uri === alicePost)).toBe(true)\n    expect(resDan.data.posts.some((p) => p.uri === carolPost)).toBe(false)\n    expect(resDan.data.posts.some((p) => p.uri === danPost)).toBe(true)\n  })\n\n  it('does not return notifs for blocked accounts', async () => {\n    const resCarol = await agent.api.app.bsky.notification.listNotifications(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      resCarol.data.notifications.some((notif) => notif.author.did === dan),\n    ).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.notification.listNotifications(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      resDan.data.notifications.some((notif) => notif.author.did === carol),\n    ).toBeFalsy()\n  })\n\n  it('does not return blocked accounts in actor search', async () => {\n    const resCarol = await agent.api.app.bsky.actor.searchActors(\n      {\n        term: 'dan.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorSearchActors,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.actor.searchActors(\n      {\n        term: 'carol.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorSearchActors,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()\n  })\n\n  it('does not return blocked accounts in actor search typeahead', async () => {\n    const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'dan.tes',\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'carol.tes',\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()\n  })\n\n  it('does return blocked accounts in actor search typeahead when term is exact handle', async () => {\n    const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'dan.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeTruthy()\n\n    const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: 'carol.test',\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorSearchActorsTypeahead,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeTruthy()\n  })\n\n  it('does not return blocked accounts in get suggestions', async () => {\n    // unfollow so they _would_ show up in suggestions if not for block\n    await sc.unfollow(carol, dan)\n    await sc.unfollow(dan, carol)\n    await network.processAll()\n\n    const resCarol = await agent.api.app.bsky.actor.getSuggestions(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()\n\n    const resDan = await agent.api.app.bsky.actor.getSuggestions(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()\n  })\n\n  it('does not serve blocked replies', async () => {\n    const getThreadPostUri = (r) => r?.['post']?.['uri']\n    // reply then block\n    const { data: replyThenBlock } =\n      await agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: sc.posts[dan][0].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n    assertIsThreadViewPost(replyThenBlock.thread)\n\n    expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([\n      aliceReplyToDan.ref.uriStr,\n    ])\n\n    // unblock\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey },\n      sc.getHeaders(dan),\n    )\n    await network.processAll()\n    const { data: unblock } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[dan][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    assertIsThreadViewPost(unblock.thread)\n    expect(unblock.thread.replies?.map(getThreadPostUri)).toEqual([\n      carolReplyToDan.ref.uriStr,\n      aliceReplyToDan.ref.uriStr,\n    ])\n\n    // block then reply\n    danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dan },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(dan),\n    )\n    const carolReplyToDan2 = await sc.reply(\n      carol,\n      sc.posts[dan][1].ref,\n      sc.posts[dan][1].ref,\n      'carol replies to dan again',\n    )\n    await network.processAll()\n    const { data: blockThenReply } =\n      await agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: sc.posts[dan][0].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n    assertIsThreadViewPost(blockThenReply.thread)\n\n    expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([\n      aliceReplyToDan.ref.uriStr,\n    ])\n\n    // cleanup\n    await pdsAgent.api.app.bsky.feed.post.delete(\n      { repo: carol, rkey: carolReplyToDan2.ref.uri.rkey },\n      sc.getHeaders(carol),\n    )\n    await network.processAll()\n  })\n\n  it('does not serve blocked embeds to third-party', async () => {\n    // embed then block\n    const { data: embedThenBlock } =\n      await agent.api.app.bsky.feed.getPostThread(\n        { depth: 0, uri: sc.posts[dan][1].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n    assertIsThreadViewPost(embedThenBlock.thread)\n\n    assert(isRecordEmbedView(embedThenBlock.thread.post.embed))\n    expect(embedThenBlock.thread.post.embed.record).toMatchObject({\n      $type: 'app.bsky.embed.record#viewBlocked',\n    })\n\n    // unblock\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey },\n      sc.getHeaders(dan),\n    )\n    await network.processAll()\n    const { data: unblock } = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 0, uri: sc.posts[dan][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    assertIsThreadViewPost(unblock.thread)\n\n    assert(isRecordEmbedView(unblock.thread.post?.embed))\n    expect(unblock.thread.post?.embed.record).toMatchObject({\n      $type: 'app.bsky.embed.record#viewRecord',\n    })\n\n    // block then embed\n    danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dan },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(dan),\n    )\n    const carolEmbedsDan = await sc.post(\n      carol,\n      'carol embeds dan',\n      undefined,\n      undefined,\n      sc.posts[dan][0].ref,\n    )\n    await network.processAll()\n    const { data: blockThenEmbed } =\n      await agent.api.app.bsky.feed.getPostThread(\n        { depth: 0, uri: carolEmbedsDan.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n    assertIsThreadViewPost(blockThenEmbed.thread)\n    assert(isRecordEmbedView(blockThenEmbed.thread.post.embed))\n    expect(blockThenEmbed.thread.post.embed.record).toMatchObject({\n      $type: 'app.bsky.embed.record#viewBlocked',\n    })\n\n    // cleanup\n    await pdsAgent.api.app.bsky.feed.post.delete(\n      { repo: carol, rkey: carolEmbedsDan.ref.uri.rkey },\n      sc.getHeaders(carol),\n    )\n    await network.processAll()\n  })\n\n  it('applies third-party blocking rules in feeds.', async () => {\n    // alice follows carol and dan, block exists between carol and dan.\n    const replyBlockedUri = carolReplyToDan.ref.uriStr\n    const replyBlockedParentUri = sc.posts[dan][0].ref.uriStr\n    const embedBlockedUri = sc.posts[dan][1].ref.uriStr\n    const { data: timeline } = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    const replyBlockedPost = timeline.feed.find(\n      (item) => item.post.uri === replyBlockedUri,\n    )\n    expect(replyBlockedPost?.reply).toMatchObject({\n      root: {\n        $type: 'app.bsky.feed.defs#blockedPost',\n        uri: replyBlockedParentUri,\n      },\n      parent: {\n        $type: 'app.bsky.feed.defs#blockedPost',\n        uri: replyBlockedParentUri,\n      },\n    })\n    const embedBlockedPost = timeline.feed.find(\n      (item) => item.post.uri === embedBlockedUri,\n    )\n    assert(embedBlockedPost)\n    assert(isRecordEmbedView(embedBlockedPost.post.embed))\n    expect(embedBlockedPost.post.embed.record).toMatchObject({\n      $type: 'app.bsky.embed.record#viewBlocked',\n    })\n  })\n\n  it('returns a list of blocks', async () => {\n    await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dan },\n      { createdAt: new Date().toISOString(), subject: alice },\n      sc.getHeaders(dan),\n    )\n\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.graph.getBlocks(\n      {},\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) },\n    )\n    const dids = res.data.blocks.map((block) => block.did).sort()\n    expect(dids).toEqual([alice, carol].sort())\n  })\n\n  it('paginates getBlocks', async () => {\n    const full = await agent.api.app.bsky.graph.getBlocks(\n      {},\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) },\n    )\n    const first = await agent.api.app.bsky.graph.getBlocks(\n      { limit: 1 },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) },\n    )\n    const second = await agent.api.app.bsky.graph.getBlocks(\n      { cursor: first.data.cursor },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetBlocks) },\n    )\n    const combined = [...first.data.blocks, ...second.data.blocks]\n    expect(combined).toEqual(full.data.blocks)\n  })\n\n  it('returns knownFollowers with blocks filtered', async () => {\n    const carolForAlice = await agent.api.app.bsky.actor.getProfile(\n      { actor: bob },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    const knownFollowers = carolForAlice.data.viewer?.knownFollowers\n    expect(knownFollowers?.count).toBe(1)\n    expect(knownFollowers?.followers).toHaveLength(0)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/bookmarks.test.ts",
    "content": "import assert from 'node:assert'\nimport {\n  $Typed,\n  AppBskyBookmarkCreateBookmark,\n  AppBskyBookmarkDeleteBookmark,\n  AppBskyFeedDefs,\n  AtpAgent,\n} from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { BookmarkView } from '../../src/lexicon/types/app/bsky/bookmark/defs'\nimport { OutputSchema as GetBookmarksOutputSchema } from '../../src/lexicon/types/app/bsky/bookmark/getBookmarks'\nimport { PostView } from '../../src/lexicon/types/app/bsky/feed/defs'\nimport { forSnapshot, paginateAll } from '../_util'\n\ntype Database = TestNetwork['bsky']['db']\n\ndescribe('appview bookmarks views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let db: Database\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_bookmarks',\n    })\n    db = network.bsky.db\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n  })\n\n  afterEach(async () => {\n    jest.resetAllMocks()\n    await clearPrivateData(db)\n    await clearBookmarks(db)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const get = async (actor: string, limit?: number, cursor?: string) =>\n    agent.app.bsky.bookmark.getBookmarks(\n      { limit, cursor },\n      {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyBookmarkGetBookmarks,\n        ),\n      },\n    )\n\n  const create = async (actor: string, ref: RecordRef) =>\n    agent.app.bsky.bookmark.createBookmark(\n      { cid: ref.cidStr, uri: ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyBookmarkCreateBookmark,\n        ),\n      },\n    )\n\n  const del = async (actor: string, ref: RecordRef) =>\n    agent.app.bsky.bookmark.deleteBookmark(\n      { uri: ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyBookmarkDeleteBookmark,\n        ),\n      },\n    )\n\n  const getPost = async (actor: string, ref: RecordRef) => {\n    const { data } = await agent.app.bsky.feed.getPosts(\n      { uris: [ref.uriStr] },\n      {\n        headers: await network.serviceHeaders(actor, ids.AppBskyFeedGetPosts),\n      },\n    )\n\n    return data.posts[0]\n  }\n\n  describe('creation', () => {\n    it('creates bookmarks', async () => {\n      await create(alice, sc.posts[alice][0].ref)\n      await create(alice, sc.posts[bob][0].ref)\n      await create(alice, sc.posts[carol][0].ref)\n\n      await create(bob, sc.posts[bob][0].ref)\n      await create(bob, sc.posts[carol][0].ref)\n\n      const { data: dataAlice } = await get(alice)\n      expect(dataAlice.bookmarks).toHaveLength(3)\n\n      const { data: dataBob } = await get(bob)\n      expect(dataBob.bookmarks).toHaveLength(2)\n    })\n\n    it('is idempotent', async () => {\n      const uri = sc.posts[alice][0].ref\n\n      await create(alice, uri)\n      const { data: data0 } = await get(alice)\n      expect(data0.bookmarks).toHaveLength(1)\n\n      await create(alice, uri)\n      const { data: data1 } = await get(alice)\n      expect(data1.bookmarks).toHaveLength(1)\n    })\n\n    it('fails on unsupported collections', async () => {\n      const followRef = sc.follows[alice][bob]\n      await expect(create(alice, followRef)).rejects.toThrow(\n        AppBskyBookmarkCreateBookmark.UnsupportedCollectionError,\n      )\n    })\n  })\n\n  describe('deletion', () => {\n    it('removes bookmarks', async () => {\n      await create(alice, sc.posts[alice][0].ref)\n      await create(alice, sc.posts[bob][0].ref)\n      await create(alice, sc.posts[carol][0].ref)\n\n      const { data: dataBefore } = await get(alice)\n      expect(dataBefore.bookmarks).toHaveLength(3)\n\n      await del(alice, sc.posts[alice][0].ref)\n      await del(alice, sc.posts[carol][0].ref)\n\n      const { data: dataAfter } = await get(alice)\n      expect(dataAfter.bookmarks).toHaveLength(1)\n    })\n\n    it('is idempotent', async () => {\n      const uri = sc.posts[alice][0].ref\n      await create(alice, uri)\n\n      await del(alice, uri)\n      const { data: data0 } = await get(alice)\n      expect(data0.bookmarks).toHaveLength(0)\n\n      await del(alice, uri)\n      const { data: data1 } = await get(alice)\n      expect(data1.bookmarks).toHaveLength(0)\n    })\n\n    it('fails on unsupported collections', async () => {\n      const followRef = sc.follows[alice][bob]\n      await expect(del(alice, followRef)).rejects.toThrow(\n        AppBskyBookmarkDeleteBookmark.UnsupportedCollectionError,\n      )\n    })\n  })\n\n  describe('listing', () => {\n    it('gets empty bookmarks', async () => {\n      const { data } = await get(alice)\n      expect(data.bookmarks).toHaveLength(0)\n    })\n\n    it('includes the bookmarked viewer state', async () => {\n      const ref = sc.posts[bob][0].ref\n\n      const postBefore = await getPost(alice, ref)\n      expect(postBefore.viewer?.bookmarked).toBe(false)\n\n      await create(alice, ref)\n      const postAfterCreate = await getPost(alice, ref)\n      expect(postAfterCreate.viewer?.bookmarked).toBe(true)\n      const postAfterCreateForBob = await getPost(bob, ref)\n      expect(postAfterCreateForBob.viewer?.bookmarked).toBe(false)\n\n      await del(alice, ref)\n      const postAfterDel = await getPost(alice, ref)\n      expect(postAfterDel.viewer?.bookmarked).toBe(false)\n    })\n\n    it('includes the bookmark counts', async () => {\n      const uri = sc.posts[bob][0].ref\n\n      const postBefore = await getPost(alice, uri)\n      expect(postBefore.bookmarkCount).toBe(0)\n\n      await create(alice, uri)\n      await create(carol, uri)\n      const postAfterCreate = await getPost(alice, uri)\n      expect(postAfterCreate.bookmarkCount).toBe(2)\n      const postAfterCreateForBob = await getPost(bob, uri)\n      expect(postAfterCreateForBob.bookmarkCount).toBe(2)\n\n      await del(alice, uri)\n      const postAfterAliceDel = await getPost(alice, uri)\n      expect(postAfterAliceDel.bookmarkCount).toBe(1)\n\n      await del(carol, uri)\n      const postAfterCarolDel = await getPost(carol, uri)\n      expect(postAfterCarolDel.bookmarkCount).toBe(0)\n    })\n\n    it('paginates bookmarks in descending order', async () => {\n      await create(alice, sc.posts[alice][0].ref)\n      await create(alice, sc.posts[alice][1].ref)\n      await create(alice, sc.posts[bob][0].ref)\n      await create(alice, sc.posts[bob][1].ref)\n      await create(alice, sc.posts[carol][0].ref)\n      await create(alice, sc.posts[dan][0].ref)\n      await create(alice, sc.posts[dan][1].ref)\n\n      const results = (out: GetBookmarksOutputSchema[]) =>\n        out.flatMap((res) => res.bookmarks)\n\n      const paginator = async (cursor?: string) => {\n        const res = await get(alice, 2, cursor)\n        return res.data\n      }\n\n      const fullRes = await get(alice)\n      expect(fullRes.data.bookmarks.length).toBe(7)\n\n      const paginatedRes = await paginateAll(paginator)\n      paginatedRes.forEach((res) =>\n        expect(res.bookmarks.length).toBeLessThanOrEqual(2),\n      )\n\n      const full = results([fullRes.data])\n      assertPostViews(full)\n\n      const paginated = results(paginatedRes)\n      assertPostViews(paginated)\n\n      // Check items are the same.\n      const sort = (\n        a: { item: $Typed<PostView> },\n        b: { item: $Typed<PostView> },\n      ) => (a.item.uri > b.item.uri ? 1 : -1)\n      expect([...paginated].sort(sort)).toEqual([...full].sort(sort))\n\n      // Check pagination ordering.\n      expect(paginated.at(0)?.subject).toStrictEqual({\n        uri: sc.posts[dan][1].ref.uriStr,\n        cid: sc.posts[dan][1].ref.cidStr,\n      })\n      expect(paginated.at(-1)?.subject).toStrictEqual({\n        uri: sc.posts[alice][0].ref.uriStr,\n        cid: sc.posts[alice][0].ref.cidStr,\n      })\n    })\n\n    it('shows posts and blocked posts correctly', async () => {\n      await create(alice, sc.posts[alice][0].ref)\n      await create(alice, sc.posts[bob][0].ref)\n      await create(alice, sc.posts[carol][0].ref)\n\n      await create(bob, sc.posts[alice][0].ref)\n      await create(bob, sc.posts[carol][0].ref)\n\n      await sc.block(alice, bob)\n      await network.processAll()\n\n      const {\n        data: { bookmarks: bookmarksA },\n      } = await get(alice)\n      expect(bookmarksA).toHaveLength(3)\n      expect(bookmarksA[0].item.$type).toBe('app.bsky.feed.defs#postView')\n      expect(bookmarksA[1].item.$type).toBe('app.bsky.feed.defs#blockedPost')\n      expect(bookmarksA[2].item.$type).toBe('app.bsky.feed.defs#postView')\n      expect(forSnapshot(bookmarksA)).toMatchSnapshot()\n\n      const {\n        data: { bookmarks: bookmarksB },\n      } = await get(bob)\n      expect(bookmarksB).toHaveLength(2)\n      expect(bookmarksB[0].item.$type).toBe('app.bsky.feed.defs#postView')\n      expect(bookmarksB[1].item.$type).toBe('app.bsky.feed.defs#blockedPost')\n      expect(forSnapshot(bookmarksB)).toMatchSnapshot()\n    })\n  })\n})\n\nconst clearPrivateData = async (db: Database) => {\n  await db.db.deleteFrom('private_data').execute()\n}\n\nconst clearBookmarks = async (db: Database) => {\n  await db.db.deleteFrom('bookmark').execute()\n}\n\nfunction assertPostViews(\n  bookmarks: GetBookmarksOutputSchema['bookmarks'],\n): asserts bookmarks is (BookmarkView & { item: $Typed<PostView> })[] {\n  bookmarks.forEach((b) => {\n    assert(\n      AppBskyFeedDefs.isPostView(b.item),\n      `Expected bookmark to be a post view`,\n    )\n  })\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/drafts.test.ts",
    "content": "import { AppBskyDraftCreateDraft, AtpAgent } from '@atproto/api'\nimport { TID } from '@atproto/common'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport {\n  Draft,\n  DraftView,\n  DraftWithId,\n} from '../../src/lexicon/types/app/bsky/draft/defs'\nimport { OutputSchema as GetDraftsOutputSchema } from '../../src/lexicon/types/app/bsky/draft/getDrafts'\nimport { paginateAll } from '../_util'\n\ntype Database = TestNetwork['bsky']['db']\n\nconst LIMIT = 10\n\ndescribe('appview drafts views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let db: Database\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_drafts',\n      bsky: {\n        draftsLimit: LIMIT,\n      },\n    })\n    db = network.bsky.db\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n  })\n\n  afterEach(async () => {\n    jest.resetAllMocks()\n    await clearDrafts(db)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const makeDraft = (): Draft => ({\n    posts: [{ text: 'Hello, world!' }],\n  })\n\n  const get = async (actor: string, limit?: number, cursor?: string) =>\n    agent.app.bsky.draft.getDrafts(\n      { limit, cursor },\n      {\n        headers: await network.serviceHeaders(actor, ids.AppBskyDraftGetDrafts),\n      },\n    )\n\n  const create = async (actor: string, draft: Draft) =>\n    agent.app.bsky.draft.createDraft(\n      { draft },\n      {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyDraftCreateDraft,\n        ),\n      },\n    )\n\n  const update = async (actor: string, draftWithId: DraftWithId) =>\n    agent.app.bsky.draft.updateDraft(\n      { draft: draftWithId },\n      {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyDraftUpdateDraft,\n        ),\n      },\n    )\n\n  const del = async (actor: string, id: string) =>\n    agent.app.bsky.draft.deleteDraft(\n      { id },\n      {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyDraftDeleteDraft,\n        ),\n      },\n    )\n\n  describe('creation', () => {\n    it('creates drafts', async () => {\n      const res1 = await create(alice, makeDraft())\n      const res2 = await create(alice, makeDraft())\n      const res3 = await create(alice, makeDraft())\n\n      expect(res1.data.id).toBeDefined()\n      expect(res2.data.id).toBeDefined()\n      expect(res3.data.id).toBeDefined()\n      expect(new Set([res1.data.id, res2.data.id, res3.data.id]).size).toBe(3)\n\n      await create(bob, makeDraft())\n      await create(bob, makeDraft())\n\n      const { data: dataAlice } = await get(alice)\n      expect(dataAlice.drafts).toHaveLength(3)\n\n      const { data: dataBob } = await get(bob)\n      expect(dataBob.drafts).toHaveLength(2)\n    })\n\n    it('creates drafts with multiple posts (threads)', async () => {\n      const draft: Draft = {\n        posts: [\n          { text: 'First post in thread' },\n          { text: 'Second post in thread' },\n          { text: 'Third post in thread' },\n        ],\n      }\n\n      await create(alice, draft)\n      const { data } = await get(alice)\n      expect(data.drafts).toHaveLength(1)\n      expect(data.drafts[0].draft.posts).toHaveLength(3)\n      expect(data.drafts[0].draft.posts[0].text).toBe('First post in thread')\n      expect(data.drafts[0].draft.posts[2].text).toBe('Third post in thread')\n    })\n\n    it('limits the drafts', async () => {\n      // Consume the limit.\n      for (let i = 0; i < LIMIT; i++) {\n        await create(alice, makeDraft())\n        await network.processAll()\n      }\n\n      // Try to go over the limit.\n      await expect(create(alice, makeDraft())).rejects.toThrow(\n        AppBskyDraftCreateDraft.DraftLimitReachedError,\n      )\n    })\n  })\n\n  describe('update', () => {\n    it('updates an existing draft', async () => {\n      const draft1: Draft = { posts: [{ text: 'First version' }] }\n\n      await create(alice, draft1)\n      const { data: data0 } = await get(alice)\n      expect(data0.drafts).toHaveLength(1)\n      expect(data0.drafts[0].draft.posts[0].text).toBe('First version')\n\n      const draftId = data0.drafts[0].id\n      const draft2: DraftWithId = {\n        id: draftId,\n        draft: { posts: [{ text: 'Updated version' }] },\n      }\n\n      await update(alice, draft2)\n      const { data: data1 } = await get(alice)\n      expect(data1.drafts).toHaveLength(1)\n      expect(data1.drafts[0].draft.posts[0].text).toBe('Updated version')\n    })\n\n    it('silently ignores updates to non-existing drafts', async () => {\n      const nonExistingDraft: DraftWithId = {\n        id: TID.nextStr(),\n        draft: { posts: [{ text: 'This draft does not exist' }] },\n      }\n\n      await update(alice, nonExistingDraft)\n      const { data } = await get(alice)\n      expect(data.drafts).toHaveLength(0)\n    })\n  })\n\n  describe('deletion', () => {\n    it('removes drafts', async () => {\n      await create(alice, makeDraft())\n      await create(alice, makeDraft())\n      await create(alice, makeDraft())\n\n      const { data: dataBefore } = await get(alice)\n      expect(dataBefore.drafts).toHaveLength(3)\n\n      const draft1Id = dataBefore.drafts[0].id\n      const draft2Id = dataBefore.drafts[1].id\n      const draft3Id = dataBefore.drafts[2].id\n\n      await del(alice, draft1Id)\n      await del(alice, draft3Id)\n\n      const { data: dataAfter } = await get(alice)\n      expect(dataAfter.drafts).toHaveLength(1)\n      expect(dataAfter.drafts[0].id).toBe(draft2Id)\n    })\n\n    it('is idempotent', async () => {\n      await create(alice, makeDraft())\n\n      const { data: data0 } = await get(alice)\n      expect(data0.drafts).toHaveLength(1)\n      const draftId = data0.drafts[0].id\n\n      await del(alice, draftId)\n      const { data: data1 } = await get(alice)\n      expect(data1.drafts).toHaveLength(0)\n\n      await del(alice, draftId)\n      const { data: data2 } = await get(alice)\n      expect(data2.drafts).toHaveLength(0)\n    })\n  })\n\n  describe('listing', () => {\n    it('gets empty drafts', async () => {\n      const { data } = await get(alice)\n      expect(data.drafts).toHaveLength(0)\n    })\n\n    it('drafts are private to each user', async () => {\n      await create(alice, makeDraft())\n      await create(alice, makeDraft())\n      await create(bob, makeDraft())\n\n      const { data: dataAlice } = await get(alice)\n      expect(dataAlice.drafts).toHaveLength(2)\n\n      const { data: dataBob } = await get(bob)\n      expect(dataBob.drafts).toHaveLength(1)\n    })\n\n    it('includes timestamps', async () => {\n      const beforeCreate = new Date()\n      await create(alice, makeDraft())\n      const afterCreate = new Date()\n\n      const { data } = await get(alice)\n      expect(data.drafts).toHaveLength(1)\n\n      const createdAt = new Date(data.drafts[0].createdAt)\n      expect(createdAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime())\n      expect(createdAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime())\n\n      const updatedAt = new Date(data.drafts[0].updatedAt)\n      expect(updatedAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime())\n      expect(updatedAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime())\n    })\n\n    it('paginates drafts in descending order', async () => {\n      for (let i = 0; i < 7; i++) {\n        await create(alice, makeDraft())\n      }\n\n      const results = (out: GetDraftsOutputSchema[]) =>\n        out.flatMap((res) => res.drafts)\n\n      const paginator = async (cursor?: string) => {\n        const res = await get(alice, 2, cursor)\n        return res.data\n      }\n\n      const fullRes = await get(alice)\n      expect(fullRes.data.drafts.length).toBe(7)\n\n      const paginatedRes = await paginateAll(paginator)\n      paginatedRes.forEach((res) =>\n        expect(res.drafts.length).toBeLessThanOrEqual(2),\n      )\n\n      const full = results([fullRes.data])\n      const paginated = results(paginatedRes)\n\n      // Check items are the same.\n      const sort = (a: DraftView, b: DraftView) => (a.id > b.id ? 1 : -1)\n      expect([...paginated].sort(sort)).toEqual([...full].sort(sort))\n\n      // Check pagination ordering (most recent first).\n      expect(paginated.at(0)?.id).toBe(full.at(0)?.id)\n      expect(paginated.at(-1)?.id).toBe(full.at(-1)?.id)\n    })\n  })\n})\n\nconst clearDrafts = async (db: Database) => {\n  await db.db.deleteFrom('draft').execute()\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/feed-hidden-replies.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { Users, feedHiddenRepliesSeed } from '../seed/feed-hidden-replies'\n\ndescribe('feed hidden replies', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let users: Users\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_tests_feed_hidden_replies',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n\n    const result = await feedHiddenRepliesSeed(sc)\n    users = result.users\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe(`notifications`, () => {\n    it(`[A] -> [B] : B is hidden`, async () => {\n      const A = await sc.post(users.poster.did, `A`)\n\n      await network.processAll()\n\n      const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`)\n      const C = await sc.reply(users.replier.did, A.ref, A.ref, `C`)\n\n      await pdsAgent.api.app.bsky.feed.threadgate.create(\n        {\n          repo: A.ref.uri.host,\n          rkey: A.ref.uri.rkey,\n        },\n        {\n          post: A.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          hiddenReplies: [B.ref.uriStr],\n        },\n        sc.getHeaders(A.ref.uri.host),\n      )\n\n      await network.processAll()\n\n      const {\n        data: { notifications },\n      } = await agent.api.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            users.poster.did,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n\n      const notificationB = notifications.find((item) => {\n        return item.uri === B.ref.uriStr\n      })\n      const notificationC = notifications.find((item) => {\n        return item.uri === C.ref.uriStr\n      })\n\n      expect(notificationB).toBeUndefined()\n      expect(notificationC).toBeDefined()\n    })\n\n    it(`[A] -> [B] -> [C] : B is hidden, C results in no notification for A, notification for B`, async () => {\n      const A = await sc.post(users.poster.did, `A`)\n      await network.processAll()\n      const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`)\n\n      await pdsAgent.api.app.bsky.feed.threadgate.create(\n        {\n          repo: A.ref.uri.host,\n          rkey: A.ref.uri.rkey,\n        },\n        {\n          post: A.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          hiddenReplies: [B.ref.uriStr],\n        },\n        sc.getHeaders(A.ref.uri.host),\n      )\n\n      await network.processAll()\n\n      const C = await sc.reply(users.viewer.did, A.ref, B.ref, `C`)\n\n      await network.processAll()\n\n      const {\n        data: { notifications: posterNotifications },\n      } = await agent.api.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            users.poster.did,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n\n      const posterNotificationB = posterNotifications.find((item) => {\n        return item.uri === B.ref.uriStr\n      })\n      const posterNotificationC = posterNotifications.find((item) => {\n        return item.uri === C.ref.uriStr\n      })\n\n      expect(posterNotificationB).toBeUndefined()\n      expect(posterNotificationC).toBeUndefined()\n\n      const {\n        data: { notifications: replierNotifications },\n      } = await agent.api.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            users.replier.did,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n\n      const replierNotificationC = replierNotifications.find((item) => {\n        return item.uri === C.ref.uriStr\n      })\n\n      expect(replierNotificationC).toBeDefined()\n    })\n\n    it(`[A] -> [B] -> [C] -> [D] : C is hidden, D results in no notification for A or B, notification for C, C exists in B's notifications`, async () => {\n      const A = await sc.post(users.poster.did, `A`)\n      await network.processAll()\n      const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`)\n      await network.processAll()\n      const C = await sc.reply(users.viewer.did, A.ref, B.ref, `C`)\n      await network.processAll()\n\n      const {\n        data: { notifications: posterNotificationsBefore },\n      } = await agent.api.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            users.poster.did,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n\n      const posterNotificationCBefore = posterNotificationsBefore.find(\n        (item) => {\n          return item.uri === C.ref.uriStr\n        },\n      )\n\n      expect(posterNotificationCBefore).toBeDefined()\n\n      await pdsAgent.api.app.bsky.feed.threadgate.create(\n        {\n          repo: A.ref.uri.host,\n          rkey: A.ref.uri.rkey,\n        },\n        {\n          post: A.ref.uriStr,\n          createdAt: new Date().toISOString(),\n          hiddenReplies: [C.ref.uriStr],\n        },\n        sc.getHeaders(A.ref.uri.host),\n      )\n      await network.processAll()\n      const D = await sc.reply(users.viewer.did, A.ref, C.ref, `D`)\n      await network.processAll()\n\n      const {\n        data: { notifications: posterNotifications },\n      } = await agent.api.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            users.poster.did,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n\n      const posterNotificationB = posterNotifications.find((item) => {\n        return item.uri === B.ref.uriStr\n      })\n      const posterNotificationC = posterNotifications.find((item) => {\n        return item.uri === C.ref.uriStr\n      })\n      const posterNotificationD = posterNotifications.find((item) => {\n        return item.uri === D.ref.uriStr\n      })\n\n      expect(posterNotificationB).toBeDefined()\n      expect(posterNotificationC).toBeUndefined() // hidden bc OP\n      expect(posterNotificationD).toBeUndefined() // hidden bc no propogation\n\n      const {\n        data: { notifications: replierNotifications },\n      } = await agent.api.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            users.replier.did,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n\n      const replierNotificationC = replierNotifications.find((item) => {\n        return item.uri === C.ref.uriStr\n      })\n      const replierNotificationD = replierNotifications.find((item) => {\n        return item.uri === D.ref.uriStr\n      })\n\n      expect(replierNotificationC).toBeDefined() // not hidden bc not OP\n      expect(replierNotificationD).toBeUndefined() // hidden bc no propogation\n\n      await pdsAgent.api.app.bsky.feed.threadgate.delete(\n        {\n          repo: A.ref.uri.host,\n          rkey: A.ref.uri.rkey,\n        },\n        sc.getHeaders(A.ref.uri.host),\n      )\n      await network.processAll()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/feed-view-post.test.ts",
    "content": "import assert from 'node:assert'\nimport { AppBskyFeedDefs, AtUri, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\n/**\n * The frontend computes feed slices for display using at-most one\n * `FeedViewPost` slice. If that slice results in an \"orphaned\" thread e.g.\n * parent or root is blocked, that slice is tossed out and the next\n * `FeedViewPost` slice is considered. That process continues until a\n * contiguous `root -> child` slice can be found.\n *\n * For the tests below, we test with up to 4 slices: one root and up to 3\n * replies. Some tests focus on the first slice, and others ensure that at\n * least one contiguous slice is returned.\n */\nconst LIMIT = 4\n\ndescribe('pds thread views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_feed_view_post',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n\n    await sc.follow(carol, alice)\n    await sc.follow(carol, bob)\n    await sc.follow(carol, dan)\n    await sc.follow(dan, alice)\n    await sc.follow(dan, bob)\n    await sc.follow(dan, carol)\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it(`[A] -> [B], A blocks B, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: bob },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)\n    const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr)\n\n    expect(sliceA).toBeDefined()\n    expect(sliceB).toBeDefined()\n\n    if (!sliceA || !sliceB) {\n      throw new Error('sliceA or sliceB is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.parent))\n    assert(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.root))\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n\n  it(`[A] -> [B] -> [C], A blocks B, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: bob },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)\n\n    expect(sliceC).toBeDefined()\n    expect(sliceC?.reply).toBeDefined()\n\n    if (!sliceC || !sliceC.reply) {\n      throw new Error('sliceC is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isPostView(sliceC.reply.parent))\n    expect(sliceC.reply.parent.uri).toEqual(B.ref.uriStr)\n    assert(AppBskyFeedDefs.isBlockedPost(sliceC.reply.root))\n    expect(sliceC.reply.root.uri).toEqual(A.ref.uriStr)\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n\n  it(`[A] -> [B] -> [C], C blocks A, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: carol },\n      { createdAt: new Date().toISOString(), subject: alice },\n      sc.getHeaders(carol),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      // make sure we process all slices in this test\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr)\n    const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)\n\n    expect(sliceB).toBeUndefined()\n    expect(sliceC).toBeUndefined()\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: carol, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(carol),\n    )\n  })\n\n  it(`[A] -> [B] -> [C], C blocks B, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: carol },\n      { createdAt: new Date().toISOString(), subject: bob },\n      sc.getHeaders(carol),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      // make sure we process all slices in this test\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)\n    const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)\n\n    expect(sliceA).toBeDefined()\n    expect(sliceC).toBeUndefined()\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: carol, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(carol),\n    )\n  })\n\n  it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    await network.processAll()\n    const D = await sc.reply(dan, A.ref, C.ref, `D`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)\n\n    expect(sliceD).toBeDefined()\n    expect(sliceD?.reply).toBeDefined()\n\n    if (!sliceD || !sliceD.reply) {\n      throw new Error('sliceD is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.parent))\n    expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)\n    assert(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root))\n    expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n\n  it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    await network.processAll()\n    const D = await sc.reply(dan, A.ref, C.ref, `D`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n\n    const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)\n\n    expect(sliceD).toBeDefined()\n    expect(sliceD?.reply).toBeDefined()\n\n    if (!sliceD || !sliceD.reply) {\n      throw new Error('sliceD is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.parent))\n    expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)\n    assert(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root))\n    expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n\n  it(`[A] -> [B] -> [C] -> [D], A blocks B, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    await network.processAll()\n    const D = await sc.reply(dan, A.ref, C.ref, `D`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: bob },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)\n\n    expect(sliceD).toBeDefined()\n    expect(sliceD?.reply).toBeDefined()\n\n    if (!sliceD || !sliceD.reply) {\n      throw new Error('sliceD is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.parent))\n    expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)\n    /*\n     * We don't walk the reply ancestors past whats available in the ReplyRef\n     */\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.root))\n    expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n\n  it(`[A] -> [B] -> [C] -> [D], B blocks C, viewed as D`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    await network.processAll()\n    const D = await sc.reply(dan, A.ref, C.ref, `D`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: bob },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(bob),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n\n    const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)\n\n    expect(sliceD).toBeDefined()\n    expect(sliceD?.reply).toBeDefined()\n\n    if (!sliceD || !sliceD.reply) {\n      throw new Error('sliceD is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.parent))\n    expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)\n\n    /*\n     * We don't walk the reply ancestors past whats available in the ReplyRef\n     */\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.root))\n    expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: bob, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(bob),\n    )\n  })\n\n  it(`[A] -> [B] -> [C] -> [D], A blocks D, viewed as C`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    await network.processAll()\n    const D = await sc.reply(dan, A.ref, C.ref, `D`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: dan },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)\n    const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)\n\n    expect(sliceD).toBeDefined()\n    expect(sliceC).toBeDefined()\n\n    if (!sliceD || !sliceC) {\n      throw new Error('sliceD or sliceC is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isBlockedPost(sliceD.reply?.root))\n    assert(AppBskyFeedDefs.isPostView(sliceC.reply?.parent))\n    assert(AppBskyFeedDefs.isPostView(sliceC.reply?.root))\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n\n  it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D, A/B/C are outside first page`, async () => {\n    const A = await sc.post(alice, `A`)\n    await network.processAll()\n    const B = await sc.reply(bob, A.ref, A.ref, `B`)\n    await network.processAll()\n    const C = await sc.reply(carol, A.ref, B.ref, `C`)\n    await network.processAll()\n\n    // push A/B/C to send page of results\n    await sc.post(alice, `Aa`)\n    await sc.post(alice, `Ab`)\n    await sc.post(alice, `Ac`)\n    await sc.post(alice, `Ad`)\n\n    await network.processAll()\n\n    const D = await sc.reply(dan, A.ref, C.ref, `D`)\n    const block = await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: alice },\n      { createdAt: new Date().toISOString(), subject: carol },\n      sc.getHeaders(alice),\n    )\n\n    await network.processAll()\n\n    const timeline = await agent.api.app.bsky.feed.getTimeline(\n      { limit: LIMIT },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n\n    const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)\n    const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)\n\n    expect(sliceD).toBeDefined()\n    expect(sliceD?.reply).toBeDefined()\n    // not in first page of results\n    expect(sliceA).toBeUndefined()\n\n    if (!sliceD || !sliceD.reply) {\n      throw new Error('sliceD is undefined')\n    }\n\n    assert(AppBskyFeedDefs.isPostView(sliceD.reply.parent))\n    expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)\n    assert(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root))\n    expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)\n\n    await pdsAgent.api.app.bsky.graph.block.delete(\n      { repo: alice, rkey: new AtUri(block.uri).rkey },\n      sc.getHeaders(alice),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/follows.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, followsSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as GetFollowersOutputSchema } from '../../src/lexicon/types/app/bsky/graph/getFollowers'\nimport { OutputSchema as GetFollowsOutputSchema } from '../../src/lexicon/types/app/bsky/graph/getFollows'\nimport { forSnapshot, paginateAll, stripViewer } from '../_util'\n\ndescribe('pds follow views', () => {\n  let agent: AtpAgent\n  let network: TestNetwork\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_follows',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await followsSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  // TODO(bsky) blocks followers by actor takedown via labels\n  // TODO(bsky) blocks follows by actor takedown via labels\n\n  it('fetches followers', async () => {\n    const aliceFollowers = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot()\n\n    const bobFollowers = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.bob },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(forSnapshot(bobFollowers.data)).toMatchSnapshot()\n\n    const carolFollowers = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.carol },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(forSnapshot(carolFollowers.data)).toMatchSnapshot()\n\n    const danFollowers = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.dan },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(forSnapshot(danFollowers.data)).toMatchSnapshot()\n\n    const eveFollowers = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.eve },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(forSnapshot(eveFollowers.data)).toMatchSnapshot()\n  })\n\n  it('fetches followers by handle', async () => {\n    const byDid = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n    const byHandle = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n    expect(byHandle.data).toEqual(byDid.data)\n  })\n\n  it('paginates followers', async () => {\n    const results = (results: GetFollowersOutputSchema[]) =>\n      results.flatMap((res) => res.followers)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.graph.getFollowers(\n        {\n          actor: sc.dids.alice,\n          cursor,\n          limit: 2,\n        },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyGraphGetFollowers,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.followers.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(full.data.followers.length).toEqual(4)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches followers unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.graph.getFollowers({\n      actor: sc.dids.alice,\n    })\n    expect(unauthed.followers.length).toBeGreaterThan(0)\n    expect(unauthed.followers).toEqual(authed.followers.map(stripViewer))\n  })\n\n  it('blocks followers by actor takedown', async () => {\n    await network.bsky.ctx.dataplane.takedownActor({\n      did: sc.dids.dan,\n    })\n\n    const aliceFollowers = await agent.api.app.bsky.graph.getFollowers(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollowers,\n        ),\n      },\n    )\n\n    expect(aliceFollowers.data.followers.map((f) => f.did)).not.toContain(\n      sc.dids.dan,\n    )\n\n    await network.bsky.ctx.dataplane.untakedownActor({\n      did: sc.dids.dan,\n    })\n  })\n\n  it('fetches follows', async () => {\n    const aliceFollowers = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot()\n\n    const bobFollowers = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.bob },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(forSnapshot(bobFollowers.data)).toMatchSnapshot()\n\n    const carolFollowers = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.carol },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(forSnapshot(carolFollowers.data)).toMatchSnapshot()\n\n    const danFollowers = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.dan },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(forSnapshot(danFollowers.data)).toMatchSnapshot()\n\n    const eveFollowers = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.eve },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(forSnapshot(eveFollowers.data)).toMatchSnapshot()\n  })\n\n  it('fetches follows by handle', async () => {\n    const byDid = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n    const byHandle = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n    expect(byHandle.data).toEqual(byDid.data)\n  })\n\n  it('paginates follows', async () => {\n    const results = (results: GetFollowsOutputSchema[]) =>\n      results.flatMap((res) => res.follows)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.graph.getFollows(\n        {\n          actor: sc.dids.alice,\n          cursor,\n          limit: 2,\n        },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyGraphGetFollows,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.follows.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(full.data.follows.length).toEqual(4)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches follows unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.graph.getFollows({\n      actor: sc.dids.alice,\n    })\n    expect(unauthed.follows.length).toBeGreaterThan(0)\n    expect(unauthed.follows).toEqual(authed.follows.map(stripViewer))\n  })\n\n  it('blocks follows by actor takedown', async () => {\n    await network.bsky.ctx.dataplane.takedownActor({\n      did: sc.dids.dan,\n    })\n\n    const aliceFollows = await agent.api.app.bsky.graph.getFollows(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n\n    expect(aliceFollows.data.follows.map((f) => f.did)).not.toContain(\n      sc.dids.dan,\n    )\n\n    await network.bsky.ctx.dataplane.untakedownActor({\n      did: sc.dids.dan,\n    })\n  })\n\n  it('fetches relationships between users', async () => {\n    const res = await agent.api.app.bsky.graph.getRelationships({\n      actor: sc.dids.bob,\n      others: [sc.dids.alice, sc.dids.bob, sc.dids.carol],\n    })\n    expect(res.data.actor).toEqual(sc.dids.bob)\n    expect(res.data.relationships.length).toBe(3)\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/get-config.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { TestNetwork } from '@atproto/dev-env'\n\ndescribe('get config', () => {\n  describe('when live now is NOT configured', () => {\n    let network: TestNetwork\n    let agent: AtpAgent\n\n    beforeAll(async () => {\n      network = await TestNetwork.create({\n        dbPostgresSchema: 'bsky_tests_live_now_config_off',\n      })\n      agent = network.bsky.getClient()\n\n      await network.processAll()\n    })\n\n    afterAll(async () => {\n      await network.close()\n    })\n\n    it('omits the live now config', async () => {\n      const res = await agent.app.bsky.unspecced.getConfig()\n\n      expect(res.data).not.toHaveProperty('liveNow')\n    })\n  })\n\n  describe('when live now is configured', () => {\n    const liveNowConfig = [\n      {\n        did: 'did:plc:asdf123',\n        domains: ['example.com', 'atproto.com'],\n      },\n      {\n        did: 'did:plc:sdfg234',\n        domains: ['example.com'],\n      },\n    ]\n\n    let network: TestNetwork\n    let agent: AtpAgent\n\n    beforeAll(async () => {\n      network = await TestNetwork.create({\n        dbPostgresSchema: 'bsky_tests_live_now_config_on',\n        bsky: {\n          liveNowConfig,\n        },\n      })\n      agent = network.bsky.getClient()\n\n      await network.processAll()\n    })\n\n    afterAll(async () => {\n      await network.close()\n    })\n\n    it(`returns the config`, async () => {\n      const res = await agent.app.bsky.unspecced.getConfig()\n\n      expect(res.data.liveNow).toEqual(liveNowConfig)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/get-suggested-onboarding-users.test.ts",
    "content": "import { once } from 'node:events'\nimport { Server, createServer } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport express, { Application } from 'express'\nimport AtpAgent from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema } from '../../src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton'\n\ntype User = {\n  id: string\n  did: string\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUser(name: string): User {\n  return {\n    id: name,\n    // @ts-ignore overwritten below\n    did: undefined,\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name}`,\n    selfLabels: undefined,\n  }\n}\n\nconst users = {\n  suggestedUser: createUser('suggested-user'),\n  viewer: createUser('viewer'),\n  viewerBlocker: createUser('viewer-blocker'),\n  followedUser: createUser('followed-user'),\n}\n\ntype Users = typeof users\n\nasync function seed(sc: SeedClient) {\n  const u = structuredClone(users)\n\n  for (const [key, user] of Object.entries(u)) {\n    await sc.createAccount(key, user)\n    u[key].did = sc.dids[key]\n  }\n\n  await sc.block(u.viewerBlocker.did, u.suggestedUser.did)\n  await sc.follow(u.viewer.did, u.followedUser.did)\n\n  await sc.network.processAll()\n\n  return { users: u }\n}\n\ndescribe('getSuggestedOnboardingUsers', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let seededUsers: Users\n  let mockServer: MockServer\n\n  beforeAll(async () => {\n    mockServer = new MockServer()\n    await mockServer.listen()\n\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_tests_get_suggested_onboarding_users',\n      bsky: {\n        suggestionsUrl: mockServer.url,\n        suggestionsApiKey: 'test',\n      },\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n\n    const result = await seed(sc)\n    seededUsers = result.users\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n    await mockServer.stop()\n  })\n\n  describe(`basic handling`, () => {\n    beforeAll(() => {\n      mockServer.mockedDids.set('suggestedUser', seededUsers.suggestedUser.did)\n    })\n\n    afterAll(() => {\n      mockServer.mockedDids.delete('suggestedUser')\n    })\n\n    it(`returns users for non-blocking viewer`, async () => {\n      const { data } =\n        await agent.app.bsky.unspecced.getSuggestedOnboardingUsers(undefined, {\n          headers: await network.serviceHeaders(\n            seededUsers.viewer.did,\n            ids.AppBskyUnspeccedGetSuggestedOnboardingUsers,\n          ),\n        })\n      const actor = data.actors.find(\n        (a) => a.did === seededUsers.suggestedUser.did,\n      )\n      expect(actor).toBeDefined()\n    })\n\n    it(`does not return user if blocked by viewer`, async () => {\n      const { data } =\n        await agent.app.bsky.unspecced.getSuggestedOnboardingUsers(undefined, {\n          headers: await network.serviceHeaders(\n            seededUsers.viewerBlocker.did,\n            ids.AppBskyUnspeccedGetSuggestedOnboardingUsers,\n          ),\n        })\n      const actor = data.actors.find(\n        (a) => a.did === seededUsers.suggestedUser.did,\n      )\n      expect(actor).not.toBeDefined()\n    })\n\n    it(`does not return users that viewer already follows`, async () => {\n      mockServer.mockedDids.set('followedUser', seededUsers.followedUser.did)\n      const { data } =\n        await agent.app.bsky.unspecced.getSuggestedOnboardingUsers(undefined, {\n          headers: await network.serviceHeaders(\n            seededUsers.viewer.did,\n            ids.AppBskyUnspeccedGetSuggestedOnboardingUsers,\n          ),\n        })\n      const actor = data.actors.find(\n        (a) => a.did === seededUsers.followedUser.did,\n      )\n      expect(actor).not.toBeDefined()\n      mockServer.mockedDids.delete('followedUser')\n    })\n  })\n})\n\nclass MockServer {\n  app: Application\n  server: Server\n\n  mockedDids = new Map<string, string>()\n\n  constructor() {\n    this.app = this.createApp()\n    this.server = createServer(this.app)\n  }\n\n  async listen(port?: number) {\n    this.server.listen(port)\n    await once(this.server, 'listening')\n  }\n\n  async stop() {\n    this.server.close()\n    await once(this.server, 'close')\n  }\n\n  get url() {\n    const address = this.server.address() as AddressInfo\n    return `http://localhost:${address.port}`\n  }\n\n  private createApp() {\n    const app = express()\n    app.get(\n      '/xrpc/app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n      (req, res) => {\n        const skeleton: OutputSchema = {\n          dids: Array.from(this.mockedDids.values()),\n        }\n        return res.json(skeleton)\n      },\n    )\n    return app\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/get-suggested-starter-packs.test.ts",
    "content": "import { once } from 'node:events'\nimport { Server, createServer } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport express, { Application } from 'express'\nimport AtpAgent from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema } from '../../src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton'\nimport {\n  StarterPacks,\n  Users,\n  starterPacksSeed,\n} from '../seed/get-suggested-starter-packs'\n\ndescribe('getSuggestedStarterPacks', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let users: Users\n  let mockServer: MockServer\n  let starterpacks: StarterPacks\n\n  beforeAll(async () => {\n    mockServer = new MockServer()\n    await mockServer.listen()\n\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_tests_get_suggested_starter_packs',\n      bsky: {\n        topicsUrl: mockServer.url,\n        topicsApiKey: 'test',\n      },\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n\n    const result = await starterPacksSeed(sc)\n    users = result.users\n    starterpacks = result.starterpacks\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n    await mockServer.stop()\n  })\n\n  describe(`basic handling`, () => {\n    beforeAll(() => {\n      const pack = Object.values(starterpacks[users.creator.did])[0]\n      mockServer.mockedStarterPackUris.set('a', pack.ref.uriStr)\n    })\n\n    afterAll(() => {\n      mockServer.mockedStarterPackUris.delete('a')\n    })\n\n    it(`returns pack for non-blocking user`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getSuggestedStarterPacks(\n        undefined,\n        {\n          headers: await network.serviceHeaders(\n            users.viewer.did,\n            ids.AppBskyUnspeccedGetSuggestedStarterPacks,\n          ),\n        },\n      )\n      const sp = data.starterPacks[0]\n      expect(sp).toBeDefined()\n    })\n\n    it(`does not return pack if creator blocked by viewer`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getSuggestedStarterPacks(\n        undefined,\n        {\n          headers: await network.serviceHeaders(\n            users.viewerBlocker.did,\n            ids.AppBskyUnspeccedGetSuggestedStarterPacks,\n          ),\n        },\n      )\n      const sp = data.starterPacks[0]\n      expect(sp).not.toBeDefined()\n    })\n  })\n})\n\nclass MockServer {\n  app: Application\n  server: Server\n\n  mockedStarterPackUris = new Map<string, OutputSchema['starterPacks'][0]>()\n\n  constructor() {\n    this.app = this.createApp()\n    this.server = createServer(this.app)\n  }\n\n  async listen(port?: number) {\n    this.server.listen(port)\n    await once(this.server, 'listening')\n  }\n\n  async stop() {\n    this.server.close()\n    await once(this.server, 'close')\n  }\n\n  get url() {\n    const address = this.server.address() as AddressInfo\n    return `http://localhost:${address.port}`\n  }\n\n  private createApp() {\n    const app = express()\n    app.get(\n      '/xrpc/app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n      (req, res) => {\n        const skeleton: OutputSchema = {\n          starterPacks: Array.from(this.mockedStarterPackUris.values()),\n        }\n        return res.json(skeleton)\n      },\n    )\n    return app\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/get-trends.test.ts",
    "content": "import assert from 'node:assert'\nimport { once } from 'node:events'\nimport { Server, createServer } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport express, { Application } from 'express'\nimport AtpAgent from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema } from '../../src/lexicon/types/app/bsky/unspecced/getTrendsSkeleton'\nimport { Users, trendsSeed } from '../seed/get-trends'\n\ndescribe('getTrends', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let users: Users\n  let mockTrendServer: MockTrendsServer\n\n  beforeAll(async () => {\n    mockTrendServer = new MockTrendsServer()\n    await mockTrendServer.listen()\n\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_tests_get_trends_test_b',\n      bsky: {\n        topicsUrl: mockTrendServer.url,\n        topicsApiKey: 'test',\n      },\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n\n    const result = await trendsSeed(sc)\n    users = result.users\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n    await mockTrendServer.stop()\n  })\n\n  describe(`basic handling`, () => {\n    beforeAll(() => {\n      mockTrendServer.mockedTrendSkeletons.set('a', {\n        topic: 'a',\n        displayName: 'A',\n        link: '/test',\n        startedAt: new Date().toISOString(),\n        postCount: 3,\n        dids: [users.posterA.did, users.posterB.did, users.posterC.did],\n      })\n    })\n\n    afterAll(() => {\n      mockTrendServer.mockedTrendSkeletons.delete('a')\n    })\n\n    it(`returns all users for non-blocked user`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getTrends(undefined, {\n        headers: await network.serviceHeaders(\n          users.viewer.did,\n          ids.AppBskyUnspeccedGetTrends,\n        ),\n      })\n      const trendA = data.trends.find((t) => t.topic === 'a')\n\n      assert(trendA)\n\n      expect(trendA.actors.map((a) => a.did)).toEqual([\n        users.posterA.did,\n        users.posterB.did,\n        users.posterC.did,\n      ])\n    })\n\n    it(`does not return user blocked by viewer`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getTrends(undefined, {\n        headers: await network.serviceHeaders(\n          users.viewerBlocker.did,\n          ids.AppBskyUnspeccedGetTrends,\n        ),\n      })\n      const trendA = data.trends.find((t) => t.topic === 'a')\n\n      assert(trendA)\n\n      expect(trendA.actors.map((a) => a.did)).toEqual([\n        users.posterA.did,\n        users.posterB.did,\n      ])\n    })\n  })\n})\n\nclass MockTrendsServer {\n  app: Application\n  server: Server\n\n  mockedTrendSkeletons = new Map<string, OutputSchema['trends'][0]>()\n\n  constructor() {\n    this.app = this.createApp()\n    this.server = createServer(this.app)\n  }\n\n  async listen(port?: number) {\n    this.server.listen(port)\n    await once(this.server, 'listening')\n  }\n\n  async stop() {\n    this.server.close()\n    await once(this.server, 'close')\n  }\n\n  get url() {\n    const address = this.server.address() as AddressInfo\n    return `http://localhost:${address.port}`\n  }\n\n  private createApp() {\n    const app = express()\n    app.get('/xrpc/app.bsky.unspecced.getTrendsSkeleton', (req, res) => {\n      const skeleton: OutputSchema = {\n        trends: Array.from(this.mockedTrendSkeletons.values()),\n      }\n      return res.json(skeleton)\n    })\n    return app\n  }\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/known-followers.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { knownFollowersSeed } from '../seed/known-followers'\n\ndescribe('known followers (social proof)', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let seedClient: SeedClient\n\n  let dids: Record<string, string>\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_known_followers',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    seedClient = network.getSeedClient()\n\n    await knownFollowersSeed(seedClient)\n\n    dids = seedClient.dids\n\n    /*\n     * First-party block\n     */\n    await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dids.fp_block_view },\n      { createdAt: new Date().toISOString(), subject: dids.fp_block_res_1 },\n      seedClient.getHeaders(dids.fp_block_view),\n    )\n\n    /*\n     * Second-party block\n     */\n    await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dids.sp_block_sub },\n      { createdAt: new Date().toISOString(), subject: dids.sp_block_res_1 },\n      seedClient.getHeaders(dids.sp_block_sub),\n    )\n\n    /*\n     * Mix of blocks and non\n     */\n    await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dids.mix_view },\n      { createdAt: new Date().toISOString(), subject: dids.mix_fp_block_res },\n      seedClient.getHeaders(dids.mix_view),\n    )\n    await pdsAgent.api.app.bsky.graph.block.create(\n      { repo: dids.mix_sub_1 },\n      { createdAt: new Date().toISOString(), subject: dids.mix_sp_block_res },\n      seedClient.getHeaders(dids.mix_sub_1),\n    )\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  /*\n   * Note that this test arbitrarily uses `getFollows` bc atm it returns\n   * `ProfileViewBasic`. This method could be updated one day to return\n   * `knownFollowers`, in which case this test would begin failing.\n   */\n  it('basic profile views do not return knownFollowers', async () => {\n    const { data } = await agent.api.app.bsky.graph.getFollows(\n      { actor: dids.base_res_1 },\n      {\n        headers: await network.serviceHeaders(\n          dids.base_view,\n          ids.AppBskyGraphGetFollows,\n        ),\n      },\n    )\n    const follow = data.follows[0]\n\n    expect(follow.viewer?.knownFollowers).toBeFalsy()\n  })\n\n  it('getKnownFollowers: returns data', async () => {\n    const { data } = await agent.api.app.bsky.graph.getKnownFollowers(\n      { actor: dids.base_sub },\n      {\n        headers: await network.serviceHeaders(\n          dids.base_view,\n          ids.AppBskyGraphGetKnownFollowers,\n        ),\n      },\n    )\n\n    expect(data.subject.did).toBe(dids.base_sub)\n    expect(data.followers.length).toBe(1)\n    expect(data.followers[0].did).toBe(dids.base_res_1)\n  })\n\n  it('getProfile: returns knownFollowers', async () => {\n    const { data } = await agent.api.app.bsky.actor.getProfile(\n      { actor: dids.base_sub },\n      {\n        headers: await network.serviceHeaders(\n          dids.base_view,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    const knownFollowers = data.viewer?.knownFollowers\n    expect(knownFollowers?.count).toBe(1)\n    expect(knownFollowers?.followers).toHaveLength(1)\n    expect(knownFollowers?.followers[0].did).toBe(dids.base_res_1)\n  })\n\n  it('getProfile: filters 1st-party blocks', async () => {\n    const { data } = await agent.api.app.bsky.actor.getProfile(\n      { actor: dids.fp_block_sub },\n      {\n        headers: await network.serviceHeaders(\n          dids.fp_block_view,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    const knownFollowers = data.viewer?.knownFollowers\n    expect(knownFollowers?.count).toBe(1)\n    expect(knownFollowers?.followers).toHaveLength(0)\n  })\n\n  it('getProfile: filters second-party blocks', async () => {\n    const result = await agent.api.app.bsky.actor.getProfile(\n      { actor: dids.sp_block_sub },\n      {\n        headers: await network.serviceHeaders(\n          dids.sp_block_view,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    const knownFollowers = result.data.viewer?.knownFollowers\n    expect(knownFollowers?.count).toBe(1)\n    expect(knownFollowers?.followers).toHaveLength(0)\n  })\n\n  it('getProfiles: filters second-party blocks', async () => {\n    const result = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [dids.sp_block_sub] },\n      {\n        headers: await network.serviceHeaders(\n          dids.sp_block_view,\n          ids.AppBskyActorGetProfiles,\n        ),\n      },\n    )\n\n    expect(result.data.profiles).toHaveLength(1)\n    const profile = result.data.profiles[0]\n    const knownFollowers = profile.viewer?.knownFollowers\n    expect(knownFollowers?.count).toBe(1)\n    expect(knownFollowers?.followers).toHaveLength(0)\n  })\n\n  it('getProfiles: mix of results', async () => {\n    const result = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [dids.mix_sub_1, dids.mix_sub_2, dids.mix_sub_3] },\n      {\n        headers: await network.serviceHeaders(\n          dids.mix_view,\n          ids.AppBskyActorGetProfiles,\n        ),\n      },\n    )\n\n    expect(result.data.profiles).toHaveLength(3)\n\n    const [sub_1, sub_2, sub_3] = result.data.profiles\n\n    const sub_1_kf = sub_1.viewer?.knownFollowers\n    expect(sub_1_kf?.count).toBe(3)\n    expect(sub_1_kf?.followers).toHaveLength(1)\n\n    const sub_2_kf = sub_2.viewer?.knownFollowers\n    expect(sub_2_kf?.count).toBe(3)\n    expect(sub_2_kf?.followers).toHaveLength(2)\n\n    const sub_3_kf = sub_3.viewer?.knownFollowers\n    expect(sub_3_kf?.count).toBe(3)\n    expect(sub_3_kf?.followers).toHaveLength(2)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/labeler-service.test.ts",
    "content": "import assert from 'node:assert'\nimport {\n  AppBskyLabelerDefs,\n  AtpAgent,\n  ComAtprotoModerationDefs,\n} from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { isView as isRecordEmbedView } from '../../src/lexicon/types/app/bsky/embed/record'\nimport { forSnapshot, stripViewerFromLabeler } from '../_util'\n\ndescribe('labeler service views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n\n  let aliceService: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_labeler_service',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n\n    const aliceRes = await pdsAgent.api.com.atproto.repo.createRecord(\n      {\n        repo: alice,\n        collection: ids.AppBskyLabelerService,\n        rkey: 'self',\n        record: {\n          policies: {\n            labelValues: ['spam', '!hide', 'scam', 'impersonation'],\n          },\n          createdAt: new Date().toISOString(),\n        },\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await pdsAgent.api.com.atproto.repo.createRecord(\n      {\n        repo: bob,\n        collection: ids.AppBskyLabelerService,\n        rkey: 'self',\n        record: {\n          policies: {\n            labelValues: ['nudity', 'sexual', 'porn'],\n          },\n          createdAt: new Date().toISOString(),\n        },\n      },\n      { headers: sc.getHeaders(bob), encoding: 'application/json' },\n    )\n\n    aliceService = new RecordRef(aliceRes.data.uri, aliceRes.data.cid)\n\n    await sc.like(bob, aliceService)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fetches labelers', async () => {\n    const view = await agent.api.app.bsky.labeler.getServices(\n      { dids: [alice, bob, 'did:example:missing'] },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyLabelerGetServices,\n        ),\n      },\n    )\n\n    expect(forSnapshot(view.data)).toMatchSnapshot()\n  })\n\n  it('fetches labelers detailed', async () => {\n    const view = await agent.api.app.bsky.labeler.getServices(\n      { dids: [alice, bob, 'did:example:missing'], detailed: true },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyLabelerGetServices,\n        ),\n      },\n    )\n\n    expect(forSnapshot(view.data)).toMatchSnapshot()\n  })\n\n  it('fetches labelers unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.labeler.getServices(\n      { dids: [alice] },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyLabelerGetServices,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.labeler.getServices({\n      dids: [alice],\n    })\n    expect(unauthed.views).toEqual(authed.views.map(stripViewerFromLabeler))\n  })\n\n  it('fetches multiple labelers unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.labeler.getServices(\n      {\n        dids: [alice, bob, 'did:example:missing'],\n      },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyLabelerGetServices,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.labeler.getServices({\n      dids: [alice, bob, 'did:example:missing'],\n    })\n    expect(unauthed.views.length).toBeGreaterThan(0)\n    expect(unauthed.views).toEqual(authed.views.map(stripViewerFromLabeler))\n  })\n\n  it('renders a post embed of a labeler', async () => {\n    const postRes = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.bob },\n      {\n        text: 'check out this labeler',\n        embed: {\n          $type: 'app.bsky.embed.record',\n          record: aliceService.raw,\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.bob),\n    )\n\n    await network.processAll()\n\n    const postViews = await agent.api.app.bsky.feed.getPosts({\n      uris: [postRes.uri],\n    })\n    const serviceViews = await agent.api.app.bsky.labeler.getServices({\n      dids: [alice],\n    })\n    assert(isRecordEmbedView(postViews.data.posts[0].embed))\n    expect(postViews.data.posts[0].embed.record).toMatchObject(\n      serviceViews.data.views[0],\n    )\n  })\n\n  it('renders profile as labeler in non-detailed profile views', async () => {\n    const { data: res } = await agent.api.app.bsky.actor.searchActors(\n      { q: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyActorSearchActors,\n        ),\n      },\n    )\n    expect(res.actors.length).toBe(1)\n    expect(res.actors[0].associated?.labeler).toBe(true)\n  })\n\n  it('blocked by labeler takedown', async () => {\n    await network.bsky.ctx.dataplane.takedownActor({ did: alice })\n    const res = await agent.api.app.bsky.labeler.getServices(\n      { dids: [alice, bob] },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyLabelerGetServices,\n        ),\n      },\n    )\n    expect(res.data.views.length).toBe(1)\n    // @ts-ignore\n    expect(res.data.views[0].creator.did).toEqual(bob)\n\n    // Cleanup\n    await network.bsky.ctx.dataplane.untakedownActor({ did: alice })\n  })\n\n  it(`returns additional labeler data`, async () => {\n    await pdsAgent.api.com.atproto.repo.createRecord(\n      {\n        repo: carol,\n        collection: ids.AppBskyLabelerService,\n        rkey: 'self',\n        record: {\n          policies: {\n            labelValues: ['spam', '!hide', 'scam', 'impersonation'],\n          },\n          createdAt: new Date().toISOString(),\n          reasonTypes: [ComAtprotoModerationDefs.REASONOTHER],\n          subjectTypes: ['record'],\n          subjectCollections: ['app.bsky.feed.post'],\n        },\n      },\n      { headers: sc.getHeaders(carol), encoding: 'application/json' },\n    )\n    await network.processAll()\n\n    const view = await agent.api.app.bsky.labeler.getServices(\n      { dids: [carol], detailed: true },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyLabelerGetServices,\n        ),\n      },\n    )\n\n    const labelerView = view.data.views[0]\n    expect(AppBskyLabelerDefs.isLabelerViewDetailed(labelerView)).toBe(true)\n    // for TS only\n    if (!AppBskyLabelerDefs.isLabelerViewDetailed(labelerView)) return\n    expect(labelerView).toBeTruthy()\n    expect(labelerView.reasonTypes).toEqual([\n      ComAtprotoModerationDefs.REASONOTHER,\n    ])\n    expect(labelerView.subjectTypes).toEqual(['record'])\n    expect(labelerView.subjectCollections).toEqual(['app.bsky.feed.post'])\n    expect(forSnapshot(view.data)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/labels-needs-review.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { isThreadViewPost } from '../../src/lexicon/types/app/bsky/feed/defs'\n\ndescribe('bsky needs-review labels', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_needs_review_labels',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    await sc.createAccount('geoff', {\n      email: 'geoff@test.com',\n      handle: 'geoff.test',\n      password: 'geoff',\n    })\n\n    await sc.reply(\n      sc.dids.geoff,\n      sc.posts[sc.dids.alice][0].ref,\n      sc.posts[sc.dids.alice][0].ref,\n      'my name geoff',\n    )\n\n    await sc.post(\n      sc.dids.geoff,\n      'her name alice',\n      undefined,\n      undefined,\n      sc.posts[sc.dids.alice][0].ref,\n    )\n\n    await sc.follow(sc.dids.bob, sc.dids.geoff)\n\n    await network.processAll()\n\n    AtpAgent.configure({ appLabelers: [network.ozone.ctx.cfg.service.did] })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('account-level', () => {\n    beforeAll(async () => {\n      await network.bsky.db.db\n        .insertInto('label')\n        .values({\n          src: network.ozone.ctx.cfg.service.did,\n          uri: sc.dids.geoff,\n          cid: '',\n          val: 'needs-review',\n          exp: null,\n          neg: false,\n          cts: new Date().toISOString(),\n        })\n        .execute()\n    })\n\n    afterAll(async () => {\n      await network.bsky.db.db\n        .deleteFrom('label')\n        .where('src', '=', network.ozone.ctx.cfg.service.did)\n        .execute()\n    })\n\n    it('applies to thread replies.', async () => {\n      const {\n        data: { thread },\n      } = await agent.app.bsky.feed.getPostThread({\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      })\n      assert(isThreadViewPost(thread))\n      expect(\n        thread.replies?.some((reply) => {\n          return (\n            isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(false)\n    })\n\n    it('applies to quote lists.', async () => {\n      const {\n        data: { posts },\n      } = await agent.app.bsky.feed.getQuotes({\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      })\n      expect(\n        posts.some((post) => {\n          return post.author.did === sc.dids.geoff\n        }),\n      ).toBe(false)\n    })\n\n    it('applies to reply, quote, and mention notifications.', async () => {\n      const {\n        data: { notifications },\n      } = await agent.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n      expect(\n        notifications.some((notif) => {\n          return notif.reason === 'reply' && notif.author.did === sc.dids.geoff\n        }),\n      ).toBe(false)\n      expect(\n        notifications.some((notif) => {\n          return notif.reason === 'quote' && notif.author.did === sc.dids.geoff\n        }),\n      ).toBe(false)\n      expect(\n        notifications.some((notif) => {\n          return (\n            notif.reason === 'mention' && notif.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(false)\n    })\n\n    it('does not apply to self.', async () => {\n      const {\n        data: { thread },\n      } = await agent.app.bsky.feed.getPostThread(\n        {\n          uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.geoff,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      assert(isThreadViewPost(thread))\n      expect(\n        thread.replies?.some((reply) => {\n          return (\n            isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(true)\n    })\n\n    it('does not apply to followers.', async () => {\n      const {\n        data: { thread },\n      } = await agent.app.bsky.feed.getPostThread(\n        {\n          uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob, // follows geoff\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      assert(isThreadViewPost(thread))\n      expect(\n        thread.replies?.some((reply) => {\n          return (\n            isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(true)\n    })\n  })\n\n  describe('record-level', () => {\n    beforeAll(async () => {\n      const geoffPostUris = [\n        ...sc.posts[sc.dids.geoff],\n        ...sc.replies[sc.dids.geoff],\n      ].map((post) => post.ref.uriStr)\n      await network.bsky.db.db\n        .insertInto('label')\n        .values(\n          geoffPostUris.map((uri) => ({\n            src: network.ozone.ctx.cfg.service.did,\n            uri,\n            cid: '',\n            val: 'needs-review',\n            exp: null,\n            neg: false,\n            cts: new Date().toISOString(),\n          })),\n        )\n        .execute()\n    })\n\n    it('applies to thread replies.', async () => {\n      const {\n        data: { thread },\n      } = await agent.app.bsky.feed.getPostThread({\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      })\n      assert(isThreadViewPost(thread))\n      expect(\n        thread.replies?.some((reply) => {\n          return (\n            isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(false)\n    })\n\n    it('applies to quote lists.', async () => {\n      const {\n        data: { posts },\n      } = await agent.app.bsky.feed.getQuotes({\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      })\n      expect(\n        posts.some((post) => {\n          return post.author.did === sc.dids.geoff\n        }),\n      ).toBe(false)\n    })\n\n    it('applies to reply, quote, and mention notifications.', async () => {\n      const {\n        data: { notifications },\n      } = await agent.app.bsky.notification.listNotifications(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n      expect(\n        notifications.some((notif) => {\n          return notif.reason === 'reply' && notif.author.did === sc.dids.geoff\n        }),\n      ).toBe(false)\n      expect(\n        notifications.some((notif) => {\n          return notif.reason === 'quote' && notif.author.did === sc.dids.geoff\n        }),\n      ).toBe(false)\n      expect(\n        notifications.some((notif) => {\n          return (\n            notif.reason === 'mention' && notif.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(false)\n    })\n\n    it('does not apply to self.', async () => {\n      const {\n        data: { thread },\n      } = await agent.app.bsky.feed.getPostThread(\n        {\n          uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.geoff,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      assert(isThreadViewPost(thread))\n      expect(\n        thread.replies?.some((reply) => {\n          return (\n            isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(true)\n    })\n\n    it('does not apply to followers.', async () => {\n      const {\n        data: { thread },\n      } = await agent.app.bsky.feed.getPostThread(\n        {\n          uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob, // follows geoff\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      assert(isThreadViewPost(thread))\n      expect(\n        thread.replies?.some((reply) => {\n          return (\n            isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff\n          )\n        }),\n      ).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/labels-takedown.test.ts",
    "content": "import assert from 'node:assert'\nimport { AppBskyLabelerDefs, AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('bsky takedown labels', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  let takendownSubjects: string[]\n\n  let aliceListRef: RecordRef\n  let carolListRef: RecordRef\n  let aliceGenRef: RecordRef\n  let bobGenRef: RecordRef\n  let carolGenRef: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_takedown_labels',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    aliceListRef = await sc.createList(sc.dids.alice, 'alice list', 'mod')\n    // carol blocks dan via alice's (takendown) list\n    await sc.addToList(sc.dids.alice, sc.dids.dan, aliceListRef)\n    await pdsAgent.app.bsky.graph.listblock.create(\n      { repo: sc.dids.carol },\n      {\n        subject: aliceListRef.uriStr,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    carolListRef = await sc.createList(sc.dids.carol, 'carol list', 'mod')\n    // alice blocks dan via carol's list, and carol is takendown\n    await sc.addToList(sc.dids.carol, sc.dids.dan, carolListRef)\n    await pdsAgent.app.bsky.graph.listblock.create(\n      { repo: sc.dids.alice },\n      {\n        subject: carolListRef.uriStr,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    aliceGenRef = await sc.createFeedGen(\n      sc.dids.alice,\n      'did:web:example.com',\n      'alice generator',\n    )\n    bobGenRef = await sc.createFeedGen(\n      sc.dids.bob,\n      'did:web:example.com',\n      'bob generator',\n    )\n    carolGenRef = await sc.createFeedGen(\n      sc.dids.carol,\n      'did:web:example.com',\n      'carol generator',\n    )\n\n    // labelers\n    await sc.createAccount('labeler1', {\n      email: 'lab1@test.com',\n      handle: 'lab1.test',\n      password: 'lab1',\n    })\n    await sc.agent.api.com.atproto.repo.createRecord(\n      {\n        repo: sc.dids.labeler1,\n        collection: ids.AppBskyLabelerService,\n        rkey: 'self',\n        record: {\n          policies: { labelValues: ['spam'] },\n          createdAt: new Date().toISOString(),\n        },\n      },\n      {\n        headers: sc.getHeaders(sc.dids.labeler1),\n        encoding: 'application/json',\n      },\n    )\n    await sc.createAccount('labeler2', {\n      email: 'lab2@test.com',\n      handle: 'lab2.test',\n      password: 'lab2',\n    })\n    await sc.agent.api.com.atproto.repo.createRecord(\n      {\n        repo: sc.dids.labeler2,\n        collection: ids.AppBskyLabelerService,\n        rkey: 'self',\n        record: {\n          policies: { labelValues: ['spam'] },\n          createdAt: new Date().toISOString(),\n        },\n      },\n      {\n        headers: sc.getHeaders(sc.dids.labeler2),\n        encoding: 'application/json',\n      },\n    )\n\n    await network.processAll()\n\n    takendownSubjects = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.dids.carol,\n      aliceListRef.uriStr,\n      aliceGenRef.uriStr,\n      sc.dids.labeler1,\n    ]\n    const src = network.ozone.ctx.cfg.service.did\n    const cts = new Date().toISOString()\n    const labels = takendownSubjects.map((uri) => ({\n      src,\n      uri,\n      cid: '',\n      val: '!takedown',\n      exp: null,\n      neg: false,\n      cts,\n    }))\n    AtpAgent.configure({ appLabelers: [src] })\n\n    await network.bsky.db.db.insertInto('label').values(labels).execute()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('takesdown profiles', async () => {\n    const attempt = agent.api.app.bsky.actor.getProfile({\n      actor: sc.dids.carol,\n    })\n    await expect(attempt).rejects.toThrow('Account has been suspended')\n    const res = await agent.api.app.bsky.actor.getProfiles({\n      actors: [sc.dids.alice, sc.dids.bob, sc.dids.carol],\n    })\n    expect(res.data.profiles.length).toBe(2)\n    expect(res.data.profiles.some((p) => p.did === sc.dids.carol)).toBe(false)\n  })\n\n  it('takesdown posts', async () => {\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      sc.posts[sc.dids.carol][0].ref.uriStr,\n      sc.posts[sc.dids.dan][1].ref.uriStr,\n      sc.replies[sc.dids.alice][0].ref.uriStr,\n    ]\n    const res = await agent.api.app.bsky.feed.getPosts({ uris })\n\n    expect(res.data.posts.length).toBe(4)\n    expect(res.data.posts.some((p) => p.author.did === sc.dids.carol)).toBe(\n      false,\n    )\n    expect(\n      res.data.posts.some(\n        (p) => p.uri === sc.posts[sc.dids.alice][0].ref.uriStr,\n      ),\n    ).toBe(false)\n  })\n\n  it('takesdown lists', async () => {\n    // record takedown\n    const attempt1 = agent.api.app.bsky.graph.getList({\n      list: aliceListRef.uriStr,\n    })\n    await expect(attempt1).rejects.toThrow('List not found')\n\n    // actor takedown\n    const attempt2 = agent.api.app.bsky.graph.getList({\n      list: carolListRef.uriStr,\n    })\n    await expect(attempt2).rejects.toThrow('List not found')\n  })\n\n  it('halts application of mod lists', async () => {\n    const { data: profile } = await agent.app.bsky.actor.getProfile(\n      {\n        actor: sc.dids.dan, // blocked via alice's takendown list\n      },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(profile.did).toBe(sc.dids.dan)\n    expect(profile.viewer).not.toBeUndefined()\n    expect(profile.viewer?.blockedBy).toBe(false)\n    expect(profile.viewer?.blocking).toBeUndefined()\n    expect(profile.viewer?.blockingByList).toBeUndefined()\n  })\n\n  it('author takedown halts application of mod lists', async () => {\n    const { data: profile } = await agent.app.bsky.actor.getProfile(\n      {\n        actor: sc.dids.dan, // blocked via carol's list, and carol is takendown\n      },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(profile.did).toBe(sc.dids.dan)\n    expect(profile.viewer).not.toBeUndefined()\n    expect(profile.viewer?.blockedBy).toBe(false)\n    expect(profile.viewer?.blocking).toBeUndefined()\n    expect(profile.viewer?.blockingByList).toBeUndefined()\n  })\n\n  it('takesdown feed generators', async () => {\n    const res = await agent.api.app.bsky.feed.getFeedGenerators({\n      feeds: [aliceGenRef.uriStr, bobGenRef.uriStr, carolGenRef.uriStr],\n    })\n    expect(res.data.feeds.length).toBe(1)\n    expect(res.data.feeds.at(0)?.uri).toEqual(bobGenRef.uriStr)\n  })\n\n  it('takesdown labelers', async () => {\n    const res = await agent.api.app.bsky.labeler.getServices({\n      dids: [sc.dids.labeler1, sc.dids.labeler2],\n    })\n    expect(res.data.views.length).toBe(1)\n    assert(AppBskyLabelerDefs.isLabelerView(res.data.views[0]))\n    expect(res.data.views[0].creator.did).toBe(sc.dids.labeler2)\n  })\n\n  it('only applies if the relevant labeler is configured', async () => {\n    AtpAgent.configure({ appLabelers: ['did:web:example.com'] })\n    const res = await agent.api.app.bsky.actor.getProfile({\n      actor: sc.dids.carol,\n    })\n    expect(res.data.did).toEqual(sc.dids.carol)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/likes.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, likesSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as GetLikesOutputSchema } from '../../src/lexicon/types/app/bsky/feed/getLikes'\nimport { constantDate, forSnapshot, paginateAll, stripViewer } from '../_util'\n\ndescribe('pds like views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let frankie: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_likes',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await likesSeed(sc)\n    await sc.createAccount('frankie', {\n      handle: 'frankie.test',\n      email: 'frankie@frankie.com',\n      password: 'password',\n    })\n    await network.processAll()\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    frankie = sc.dids.frankie\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getCursors = (items: { createdAt?: string }[]) =>\n    items.map((item) => item.createdAt ?? constantDate)\n\n  const getSortedCursors = (items: { createdAt?: string }[]) =>\n    getCursors(items).sort((a, b) => tstamp(b) - tstamp(a))\n\n  const tstamp = (x: string) => new Date(x).getTime()\n\n  it('fetches post likes', async () => {\n    const alicePost = await agent.api.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(forSnapshot(alicePost.data)).toMatchSnapshot()\n    expect(getCursors(alicePost.data.likes)).toEqual(\n      getSortedCursors(alicePost.data.likes),\n    )\n  })\n\n  it('fetches reply likes', async () => {\n    const bobReply = await agent.api.app.bsky.feed.getLikes(\n      { uri: sc.replies[bob][0].ref.uriStr },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(forSnapshot(bobReply.data)).toMatchSnapshot()\n    expect(getCursors(bobReply.data.likes)).toEqual(\n      getSortedCursors(bobReply.data.likes),\n    )\n  })\n\n  it('paginates', async () => {\n    const results = (results: GetLikesOutputSchema[]) =>\n      results.flatMap((res) => res.likes)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.feed.getLikes(\n        {\n          uri: sc.posts[alice][1].ref.uriStr,\n          cursor,\n          limit: 2,\n        },\n        {\n          headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.likes.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(full.data.likes.length).toEqual(4)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches post likes unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.feed.getLikes({\n      uri: sc.posts[alice][1].ref.uriStr,\n    })\n    expect(unauthed.likes.length).toBeGreaterThan(0)\n    expect(unauthed.likes).toEqual(\n      authed.likes.map((like) => {\n        return {\n          ...like,\n          actor: stripViewer(like.actor),\n        }\n      }),\n    )\n  })\n\n  it(`author viewer doesn't see likes by user the author blocked`, async () => {\n    await sc.like(frankie, sc.posts[alice][1].ref)\n    await network.processAll()\n\n    const beforeBlock = await agent.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(beforeBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([\n      sc.dids.frankie,\n      sc.dids.eve,\n      sc.dids.dan,\n      sc.dids.carol,\n      sc.dids.bob,\n    ])\n\n    await sc.block(alice, frankie)\n    await network.processAll()\n\n    const afterBlock = await agent.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(afterBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([\n      sc.dids.eve,\n      sc.dids.dan,\n      sc.dids.carol,\n      sc.dids.bob,\n    ])\n  })\n\n  it(`non-author viewer doesn't see likes by user the author blocked and by user the viewer blocked `, async () => {\n    await sc.unblock(alice, frankie)\n    await network.processAll()\n\n    const beforeBlock = await agent.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(beforeBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([\n      sc.dids.frankie,\n      sc.dids.eve,\n      sc.dids.dan,\n      sc.dids.carol,\n      sc.dids.bob,\n    ])\n\n    await sc.block(alice, frankie)\n    await sc.block(bob, carol)\n    await network.processAll()\n\n    const afterBlock = await agent.app.bsky.feed.getLikes(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetLikes) },\n    )\n\n    expect(afterBlock.data.likes.map((like) => like.actor.did)).toStrictEqual([\n      sc.dids.eve,\n      sc.dids.dan,\n      sc.dids.bob,\n    ])\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/list-feed.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as GetListFeedOutputSchema } from '../../src/lexicon/types/app/bsky/feed/getListFeed'\nimport {\n  forSnapshot,\n  paginateAll,\n  stripViewer,\n  stripViewerFromPost,\n} from '../_util'\n\ndescribe('list feed views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n\n  let listRef: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_list_feed',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    listRef = await sc.createList(alice, 'test list', 'curate')\n    await sc.addToList(alice, alice, listRef)\n    await sc.addToList(alice, bob, listRef)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fetches list feed', async () => {\n    const res = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetListFeed,\n        ),\n      },\n    )\n    expect(forSnapshot(res.data.feed)).toMatchSnapshot()\n\n    // all posts are from alice or bob\n    expect(\n      res.data.feed.every((row) => [alice, bob].includes(row.post.author.did)),\n    ).toBeTruthy()\n  })\n\n  it('paginates', async () => {\n    const results = (results: GetListFeedOutputSchema[]) =>\n      results.flatMap((res) => res.feed)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.feed.getListFeed(\n        {\n          list: listRef.uriStr,\n          cursor,\n          limit: 2,\n        },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetListFeed,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.feed.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetListFeed,\n        ),\n      },\n    )\n\n    expect(full.data.feed.length).toEqual(7)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches results unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetListFeed,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.feed.getListFeed({\n      list: listRef.uriStr,\n    })\n    expect(unauthed.feed.length).toBeGreaterThan(0)\n    expect(unauthed.feed).toEqual(\n      authed.feed.map((item) => {\n        const result = {\n          ...item,\n          post: stripViewerFromPost(item.post),\n        }\n        if (item.reply) {\n          result.reply = {\n            parent: stripViewerFromPost(item.reply.parent, true),\n            root: stripViewerFromPost(item.reply.root, true),\n          }\n\n          if (item.reply.grandparentAuthor) {\n            result.reply.grandparentAuthor = stripViewer(\n              item.reply.grandparentAuthor,\n            )\n          }\n        }\n        return result\n      }),\n    )\n  })\n\n  it('works for empty lists', async () => {\n    const emptyList = await sc.createList(alice, 'empty list', 'curate')\n    const res = await agent.api.app.bsky.feed.getListFeed({\n      list: emptyList.uriStr,\n    })\n\n    expect(res.data.feed.length).toEqual(0)\n  })\n\n  it('blocks posts by actor takedown', async () => {\n    await network.bsky.ctx.dataplane.takedownActor({\n      did: bob,\n    })\n\n    const res = await agent.api.app.bsky.feed.getListFeed({\n      list: listRef.uriStr,\n    })\n    const hasBob = res.data.feed.some((item) => item.post.author.did === bob)\n    expect(hasBob).toBe(false)\n\n    // Cleanup\n    await network.bsky.ctx.dataplane.untakedownActor({\n      did: bob,\n    })\n  })\n\n  it('blocks posts by record takedown.', async () => {\n    const postRef = sc.replies[bob][0].ref // Post and reply parent\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: postRef.uriStr,\n    })\n\n    const res = await agent.api.app.bsky.feed.getListFeed({\n      list: listRef.uriStr,\n    })\n    const hasPost = res.data.feed.some(\n      (item) => item.post.uri === postRef.uriStr,\n    )\n    expect(hasPost).toBe(false)\n\n    // Cleanup\n    await network.bsky.ctx.dataplane.untakedownRecord({\n      recordUri: postRef.uriStr,\n    })\n  })\n\n  it('does not return posts with creator blocks', async () => {\n    await sc.block(bob, alice)\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.feed.getListFeed({\n      list: listRef.uriStr,\n    })\n\n    const hasBob = res.data.feed.some((item) => item.post.author.did === bob)\n    expect(hasBob).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/lists.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as GetListsOutputSchema } from '../../src/lexicon/types/app/bsky/graph/getLists'\nimport {\n  ListWithMembership,\n  OutputSchema as GetListsWithMembershipOutputSchema,\n} from '../../src/lexicon/types/app/bsky/graph/getListsWithMembership'\nimport { forSnapshot, paginateAll } from '../_util'\n\ndescribe('bsky actor likes feed views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let blockList: string\n  let curateList: string\n  let referenceList: string\n  let eveListItemCur: string\n  let frankieListItemCur: string\n  let frankieListItemMod: string\n  let gretaListItemMod: string\n\n  let alice: string\n  let eve: string\n  let frankie: string\n  let greta: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_actor_lists',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@eve.com',\n      password: 'hunter2',\n    })\n    await sc.createAccount('frankie', {\n      handle: 'frankie.test',\n      email: 'frankie@frankie.com',\n      password: '2hunter2real',\n    })\n    await sc.createAccount('greta', {\n      handle: 'greta.test',\n      email: 'greta@greta.com',\n      password: 'hunter4real',\n    })\n\n    const newRefList = await sc.createList(sc.dids.eve, 'ref0', 'reference')\n    await sc.addToList(sc.dids.eve, sc.dids.eve, newRefList)\n    await sc.addToList(sc.dids.eve, sc.dids.bob, newRefList)\n    await sc.addToList(sc.dids.eve, sc.dids.frankie, newRefList)\n\n    const newCurList = await sc.createList(sc.dids.eve, 'cur0', 'curate')\n    await sc.createList(sc.dids.eve, 'cur1', 'curate')\n    await sc.createList(sc.dids.eve, 'cur2', 'curate')\n    const newEveListItemCur = await sc.addToList(\n      sc.dids.eve,\n      sc.dids.eve,\n      newCurList,\n    )\n    await sc.addToList(sc.dids.eve, sc.dids.bob, newCurList)\n    const newFrankieListItemCur = await sc.addToList(\n      sc.dids.eve,\n      sc.dids.frankie,\n      newCurList,\n    )\n\n    const newBlockList = await sc.createList(sc.dids.eve, 'mod0', 'mod')\n    await sc.createList(sc.dids.eve, 'mod1', 'mod')\n    await sc.createList(sc.dids.eve, 'mod2', 'mod')\n    const newFrankieListItemMod = await sc.addToList(\n      sc.dids.eve,\n      sc.dids.frankie,\n      newBlockList,\n    )\n    const newGretaListItemMod = await sc.addToList(\n      sc.dids.eve,\n      sc.dids.greta,\n      newBlockList,\n    )\n\n    await sc.block(sc.dids.frankie, sc.dids.greta)\n    await sc.block(sc.dids.frankie, sc.dids.eve)\n\n    await network.processAll()\n    blockList = newBlockList.uriStr\n    curateList = newCurList.uriStr\n    referenceList = newRefList.uriStr\n\n    eveListItemCur = newEveListItemCur.uriStr\n    frankieListItemCur = newFrankieListItemCur.uriStr\n    frankieListItemMod = newFrankieListItemMod.uriStr\n    gretaListItemMod = newGretaListItemMod.uriStr\n\n    alice = sc.dids.alice\n    eve = sc.dids.eve\n    frankie = sc.dids.frankie\n    greta = sc.dids.greta\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('does not include reference lists in getActorLists', async () => {\n    const view = await agent.app.bsky.graph.getLists({\n      actor: eve,\n    })\n    expect(view.data.lists.length).toBe(6)\n    expect(forSnapshot(view.data.lists)).toMatchSnapshot()\n  })\n\n  it('supports using a handle as getList actor param', async () => {\n    const view = await agent.app.bsky.graph.getLists({\n      actor: 'eve.test',\n    })\n    expect(view.data.lists.length).toBe(6)\n    expect(forSnapshot(view.data.lists)).toMatchSnapshot()\n  })\n\n  it('allows filtering by list purpose', async () => {\n    const viewCurate = await agent.app.bsky.graph.getLists({\n      actor: eve,\n      purposes: ['curatelist'],\n    })\n    expect(viewCurate.data.lists.length).toBe(3)\n\n    const viewMod = await agent.app.bsky.graph.getLists({\n      actor: eve,\n      purposes: ['modlist'],\n    })\n    expect(viewMod.data.lists.length).toBe(3)\n\n    const viewAll = await agent.app.bsky.graph.getLists({\n      actor: eve,\n      purposes: ['curatelist', 'modlist'],\n    })\n    expect(viewAll.data.lists.length).toBe(6)\n  })\n\n  it.each([\n    { expected: 6, purposes: [] },\n    { expected: 6, purposes: ['curatelist', 'modlist'] },\n    { expected: 3, purposes: ['curatelist'] },\n    { expected: 3, purposes: ['modlist'] },\n    { expected: 0, purposes: ['referencelist'] }, // not supported on getLists.\n  ])(\n    'paginates for purposes filter: $purposes',\n    async ({ expected, purposes }) => {\n      const results = (out: GetListsOutputSchema[]) =>\n        out.flatMap((res) => res.lists)\n      const paginator = async (cursor?: string) => {\n        const res = await agent.app.bsky.graph.getLists(\n          { actor: eve, purposes, limit: 2, cursor },\n          {\n            headers: await network.serviceHeaders(\n              eve,\n              ids.AppBskyGraphGetLists,\n            ),\n          },\n        )\n        return res.data\n      }\n\n      const paginatedAll = await paginateAll(paginator)\n      paginatedAll.forEach((res) =>\n        expect(res.lists.length).toBeLessThanOrEqual(2),\n      )\n\n      const full = await agent.app.bsky.graph.getLists(\n        { actor: eve, purposes },\n        {\n          headers: await network.serviceHeaders(eve, ids.AppBskyGraphGetLists),\n        },\n      )\n      expect(full.data.lists.length).toBe(expected)\n\n      const sortedFull = results([full.data]).sort((a, b) =>\n        a.uri > b.uri ? 1 : -1,\n      )\n      const sortedPaginated = results(paginatedAll).sort((a, b) =>\n        a.uri > b.uri ? 1 : -1,\n      )\n      expect(sortedPaginated).toEqual(sortedFull)\n    },\n  )\n\n  it('does not include users with creator block relationship in reference lists for non-creator, in-list viewers', async () => {\n    const curView = await agent.app.bsky.graph.getList(\n      {\n        list: curateList,\n      },\n      {\n        headers: await network.serviceHeaders(frankie, ids.AppBskyGraphGetList),\n      },\n    )\n    expect(curView.data.items.length).toBe(2)\n    expect(forSnapshot(curView.data.items)).toMatchSnapshot()\n\n    const refView = await agent.app.bsky.graph.getList(\n      { list: referenceList },\n      {\n        headers: await network.serviceHeaders(frankie, ids.AppBskyGraphGetList),\n      },\n    )\n    expect(refView.data.items.length).toBe(2)\n    expect(forSnapshot(refView.data.items)).toMatchSnapshot()\n  })\n\n  it('does not include users with creator block relationship in reference lists for non-creator, not-in-list viewers', async () => {\n    const curView = await agent.app.bsky.graph.getList(\n      {\n        list: curateList,\n      },\n      { headers: await network.serviceHeaders(greta, ids.AppBskyGraphGetList) },\n    )\n    expect(curView.data.items.length).toBe(2)\n    expect(forSnapshot(curView.data.items)).toMatchSnapshot()\n\n    const refView = await agent.app.bsky.graph.getList(\n      { list: referenceList },\n      { headers: await network.serviceHeaders(greta, ids.AppBskyGraphGetList) },\n    )\n    expect(refView.data.items.length).toBe(2)\n    expect(forSnapshot(refView.data.items)).toMatchSnapshot()\n  })\n\n  it('does not include users with creator block relationship in reference and curate lists for signed-out viewers', async () => {\n    const curView = await agent.app.bsky.graph.getList({\n      list: curateList,\n    })\n    expect(curView.data.items.length).toBe(2)\n    expect(forSnapshot(curView.data.items)).toMatchSnapshot()\n\n    const refView = await agent.app.bsky.graph.getList({\n      list: referenceList,\n    })\n    expect(refView.data.items.length).toBe(2)\n    expect(forSnapshot(refView.data.items)).toMatchSnapshot()\n  })\n\n  it('does include users with creator block relationship in reference lists for creator', async () => {\n    const curView = await agent.app.bsky.graph.getList(\n      { list: curateList },\n      { headers: await network.serviceHeaders(eve, ids.AppBskyGraphGetList) },\n    )\n    expect(curView.data.items.length).toBe(3)\n    expect(forSnapshot(curView.data.items)).toMatchSnapshot()\n\n    const refView = await agent.app.bsky.graph.getList(\n      { list: referenceList },\n      { headers: await network.serviceHeaders(eve, ids.AppBskyGraphGetList) },\n    )\n    expect(refView.data.items.length).toBe(3)\n    expect(forSnapshot(refView.data.items)).toMatchSnapshot()\n  })\n\n  it('does return all users regardless of creator block relationship in moderation lists', async () => {\n    const view = await agent.app.bsky.graph.getList(\n      { list: blockList },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetList) },\n    )\n    expect(view.data.items.length).toBe(2)\n    expect(forSnapshot(view.data.items)).toMatchSnapshot()\n  })\n\n  describe('list membership', () => {\n    const uriSort = (a: string, b: string) => (a > b ? 1 : -1)\n    const membershipsUris = (lwms: ListWithMembership[]): string[] =>\n      lwms\n        .map((lwm) => lwm.listItem?.uri)\n        .filter((li): li is string => typeof li === 'string')\n        .sort(uriSort)\n\n    it('returns all lists by the user', async () => {\n      const view = await agent.app.bsky.graph.getListsWithMembership(\n        { actor: frankie },\n        {\n          headers: await network.serviceHeaders(\n            eve,\n            ids.AppBskyGraphGetListsWithMembership,\n          ),\n        },\n      )\n      expect(view.data.listsWithMembership.length).toBe(6)\n    })\n\n    it('finds self membership', async () => {\n      const view = await agent.app.bsky.graph.getListsWithMembership(\n        { actor: eve },\n        {\n          headers: await network.serviceHeaders(\n            eve,\n            ids.AppBskyGraphGetListsWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.listsWithMembership.length).toBe(6)\n      const memberships = membershipsUris(view.data.listsWithMembership)\n      const expectedMemberships = [eveListItemCur].sort(uriSort)\n      expect(memberships).toEqual(expectedMemberships)\n    })\n\n    it('finds membership in curatelist and modlist if actor is in both and purpose filter includes both', async () => {\n      const view = await agent.app.bsky.graph.getListsWithMembership(\n        { actor: frankie },\n        {\n          headers: await network.serviceHeaders(\n            eve,\n            ids.AppBskyGraphGetListsWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.listsWithMembership.length).toBe(6)\n      const memberships = membershipsUris(view.data.listsWithMembership)\n      const expectedMemberships = [frankieListItemCur, frankieListItemMod].sort(\n        uriSort,\n      )\n      expect(memberships).toEqual(expectedMemberships)\n    })\n\n    it('finds modlist membership filtering by modlist', async () => {\n      const view = await agent.app.bsky.graph.getListsWithMembership(\n        { actor: greta, purposes: ['modlist'] },\n        {\n          headers: await network.serviceHeaders(\n            eve,\n            ids.AppBskyGraphGetListsWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.listsWithMembership.length).toBe(3)\n      const memberships = membershipsUris(view.data.listsWithMembership)\n      const expectedMemberships = [gretaListItemMod].sort(uriSort)\n      expect(memberships).toEqual(expectedMemberships)\n    })\n\n    it('does not find modlist membership filtering by curatelist', async () => {\n      const view = await agent.app.bsky.graph.getListsWithMembership(\n        { actor: greta, purposes: ['curatelist'] },\n        {\n          headers: await network.serviceHeaders(\n            eve,\n            ids.AppBskyGraphGetListsWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.listsWithMembership.length).toBe(3)\n      const memberships = membershipsUris(view.data.listsWithMembership)\n      expect(memberships.length).toBe(0)\n    })\n\n    it.each([\n      { expected: 6, purposes: [] },\n      { expected: 6, purposes: ['curatelist', 'modlist'] },\n      { expected: 3, purposes: ['curatelist'] },\n      { expected: 3, purposes: ['modlist'] },\n      { expected: 0, purposes: ['referencelist'] }, // not supported on getLists.\n    ])(\n      'paginates for purposes filter: $purposes',\n      async ({ expected, purposes }) => {\n        const results = (out: GetListsWithMembershipOutputSchema[]) =>\n          out.flatMap((res) => res.listsWithMembership)\n        const paginator = async (cursor?: string) => {\n          const res = await agent.app.bsky.graph.getListsWithMembership(\n            { actor: eve, purposes, limit: 2, cursor },\n            {\n              headers: await network.serviceHeaders(\n                eve,\n                ids.AppBskyGraphGetListsWithMembership,\n              ),\n            },\n          )\n          return res.data\n        }\n\n        const paginatedAll = await paginateAll(paginator)\n        paginatedAll.forEach((res) =>\n          expect(res.listsWithMembership.length).toBeLessThanOrEqual(2),\n        )\n\n        const full = await agent.app.bsky.graph.getListsWithMembership(\n          { actor: eve, purposes },\n          {\n            headers: await network.serviceHeaders(\n              eve,\n              ids.AppBskyGraphGetListsWithMembership,\n            ),\n          },\n        )\n        expect(full.data.listsWithMembership.length).toBe(expected)\n\n        const sortedFull = results([full.data]).sort((a, b) =>\n          a.list.uri > b.list.uri ? 1 : -1,\n        )\n        const sortedPaginated = results(paginatedAll).sort((a, b) =>\n          a.list.uri > b.list.uri ? 1 : -1,\n        )\n        expect(sortedPaginated).toEqual(sortedFull)\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/mute-lists.test.ts",
    "content": "import { AtUri, AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { forSnapshot } from '../_util'\n\ndescribe('bsky views with mutes from mute lists', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_mute_lists',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    // add follows to ensure mutes work even w follows\n    await sc.follow(carol, dan)\n    await sc.follow(dan, carol)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  let listUri: string\n  let listCid: string\n\n  it('creates a list with some items', async () => {\n    const avatar = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    // alice creates mute list with bob & carol that dan uses\n    const list = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: alice },\n      {\n        name: 'alice mutes',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: 'big list of mutes',\n        avatar: avatar.image,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    listUri = list.uri\n    listCid = list.cid\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: alice },\n      {\n        subject: sc.dids.bob,\n        list: list.uri,\n        reason: 'because',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: alice },\n      {\n        subject: sc.dids.carol,\n        list: list.uri,\n        reason: 'idk',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: alice },\n      {\n        subject: sc.dids.dan,\n        list: list.uri,\n        reason: 'idk',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n  })\n\n  it('uses a list for mutes', async () => {\n    await agent.api.app.bsky.graph.muteActorList(\n      {\n        list: listUri,\n      },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphMuteActorList,\n        ),\n      },\n    )\n  })\n\n  it('flags mutes in threads', async () => {\n    const res = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(res.data.thread)).toMatchSnapshot()\n  })\n\n  it('does not show reposted content from a muted account in author feed', async () => {\n    await sc.repost(alice, sc.posts[carol][0].ref)\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBe(false)\n  })\n\n  it('removes content from muted users on getTimeline', async () => {\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBe(false)\n  })\n\n  it('removes content from muted users on getListFeed', async () => {\n    const listRef = await sc.createList(bob, 'test list', 'curate')\n    await sc.addToList(alice, bob, listRef)\n    await sc.addToList(alice, carol, listRef)\n    await sc.addToList(alice, dan, listRef)\n    const res = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetListFeed,\n        ),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBe(false)\n  })\n\n  it('returns mute status on getProfile', async () => {\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: carol },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),\n      },\n    )\n    expect(res.data.viewer?.muted).toBe(true)\n    expect(res.data.viewer?.mutedByList?.uri).toBe(listUri)\n  })\n\n  it('returns mute status on getProfiles', async () => {\n    const res = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [alice, carol] },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles),\n      },\n    )\n    expect(res.data.profiles[0].viewer?.muted).toBe(false)\n    expect(res.data.profiles[0].viewer?.mutedByList).toBeUndefined()\n    expect(res.data.profiles[1].viewer?.muted).toBe(true)\n    expect(res.data.profiles[1].viewer?.mutedByList?.uri).toEqual(listUri)\n  })\n\n  it('ignores self-mutes', async () => {\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: dan }, // dan subscribes to list that contains himself\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),\n      },\n    )\n    expect(res.data.viewer?.muted).toBe(false)\n    expect(res.data.viewer?.mutedByList).toBeUndefined()\n  })\n\n  it('does not return notifs for muted accounts', async () => {\n    const res = await agent.api.app.bsky.notification.listNotifications(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      res.data.notifications.some((notif) =>\n        [bob, carol].includes(notif.author.did),\n      ),\n    ).toBeFalsy()\n  })\n\n  it('flags muted accounts in get suggestions', async () => {\n    // unfollow so they _would_ show up in suggestions if not for mute\n    await sc.unfollow(dan, carol)\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.actor.getSuggestions(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    for (const actor of res.data.actors) {\n      if ([bob, carol].includes(actor.did)) {\n        expect(actor.viewer?.muted).toBe(true)\n        expect(actor.viewer?.mutedByList?.uri).toEqual(listUri)\n      } else {\n        expect(actor.viewer?.muted).toBe(false)\n        expect(actor.viewer?.mutedByList).toBeUndefined()\n      }\n    }\n  })\n\n  it('returns the contents of a list', async () => {\n    const res = await agent.api.app.bsky.graph.getList(\n      { list: listUri },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates getList', async () => {\n    const full = await agent.api.app.bsky.graph.getList(\n      { list: listUri },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    const first = await agent.api.app.bsky.graph.getList(\n      { list: listUri, limit: 1 },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    const second = await agent.api.app.bsky.graph.getList(\n      { list: listUri, cursor: first.data.cursor },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },\n    )\n    const combined = [...first.data.items, ...second.data.items]\n    expect(combined).toEqual(full.data.items)\n  })\n\n  let otherListUri: string\n\n  it('returns lists associated with a user', async () => {\n    const listRes = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: alice },\n      {\n        name: 'new list',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: 'blah blah',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    otherListUri = listRes.uri\n\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.graph.getLists(\n      { actor: alice },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates getLists', async () => {\n    const full = await agent.api.app.bsky.graph.getLists(\n      { actor: alice },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    const first = await agent.api.app.bsky.graph.getLists(\n      { actor: alice, limit: 1 },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    const second = await agent.api.app.bsky.graph.getLists(\n      { actor: alice, cursor: first.data.cursor },\n      { headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },\n    )\n    const combined = [...first.data.lists, ...second.data.lists]\n    expect(combined).toEqual(full.data.lists)\n  })\n\n  it('returns a users own list mutes', async () => {\n    await agent.api.app.bsky.graph.muteActorList(\n      {\n        list: otherListUri,\n      },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphMuteActorList,\n        ),\n      },\n    )\n\n    const res = await agent.api.app.bsky.graph.getListMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListMutes,\n        ),\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates getListMutes', async () => {\n    const full = await agent.api.app.bsky.graph.getListMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListMutes,\n        ),\n      },\n    )\n    const first = await agent.api.app.bsky.graph.getListMutes(\n      { limit: 1 },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListMutes,\n        ),\n      },\n    )\n    const second = await agent.api.app.bsky.graph.getListMutes(\n      { cursor: first.data.cursor },\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListMutes,\n        ),\n      },\n    )\n    const combined = [...first.data.lists, ...second.data.lists]\n    expect(combined).toEqual(full.data.lists)\n  })\n\n  it('allows unsubscribing from a mute list', async () => {\n    await agent.api.app.bsky.graph.unmuteActorList(\n      {\n        list: otherListUri,\n      },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphUnmuteActorList,\n        ),\n      },\n    )\n\n    const res = await agent.api.app.bsky.graph.getListMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          dan,\n          ids.AppBskyGraphGetListMutes,\n        ),\n      },\n    )\n    expect(res.data.lists.length).toBe(1)\n  })\n\n  it('updates list', async () => {\n    const uri = new AtUri(listUri)\n    await pdsAgent.api.com.atproto.repo.putRecord(\n      {\n        repo: uri.hostname,\n        collection: uri.collection,\n        rkey: uri.rkey,\n        record: {\n          name: 'updated alice mutes',\n          purpose: 'app.bsky.graph.defs#modlist',\n          description: 'new descript',\n          createdAt: new Date().toISOString(),\n        },\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n\n    await network.processAll()\n\n    const got = await agent.api.app.bsky.graph.getList(\n      { list: listUri },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetList) },\n    )\n    expect(got.data.list.name).toBe('updated alice mutes')\n    expect(got.data.list.description).toBe('new descript')\n    expect(got.data.list.avatar).toBeUndefined()\n    expect(got.data.items.length).toBe(3)\n  })\n\n  it('embeds lists in posts', async () => {\n    const postRef = await sc.post(\n      alice,\n      'list embed!',\n      undefined,\n      undefined,\n      new RecordRef(listUri, listCid),\n    )\n    await network.processAll()\n    const res = await agent.api.app.bsky.feed.getPosts(\n      { uris: [postRef.ref.uriStr] },\n      { headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts) },\n    )\n    expect(res.data.posts.length).toBe(1)\n    expect(forSnapshot(res.data.posts[0])).toMatchSnapshot()\n  })\n\n  it('does not apply \"curate\" blocklists', async () => {\n    const parsedUri = new AtUri(listUri)\n    await pdsAgent.api.com.atproto.repo.putRecord(\n      {\n        repo: parsedUri.hostname,\n        collection: parsedUri.collection,\n        rkey: parsedUri.rkey,\n        record: {\n          name: 'curate list',\n          purpose: 'app.bsky.graph.defs#curatelist',\n          createdAt: new Date().toISOString(),\n        },\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBeTruthy()\n  })\n\n  it('does not apply deleted blocklists (whose items are still around)', async () => {\n    const parsedUri = new AtUri(listUri)\n    await pdsAgent.api.app.bsky.graph.list.delete(\n      {\n        repo: parsedUri.hostname,\n        rkey: parsedUri.rkey,\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/mutes.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n  usersBulkSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as GetMutesOutputSchema } from '../../src/lexicon/types/app/bsky/graph/getMutes'\nimport { forSnapshot, paginateAll } from '../_util'\n\ndescribe('mute views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  let mutes: string[]\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_mutes',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await usersBulkSeed(sc, 10)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    mutes = [\n      bob,\n      carol,\n      'aliya-hodkiewicz.test',\n      'adrienne49.test',\n      'jeffrey-sawayn87.test',\n      'nicolas-krajcik10.test',\n      'magnus53.test',\n      'elta48.test',\n    ]\n    await network.processAll()\n    for (const did of mutes) {\n      await agent.api.app.bsky.graph.muteActor(\n        { actor: did },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyGraphMuteActor,\n          ),\n          encoding: 'application/json',\n        },\n      )\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('flags mutes in threads', async () => {\n    const res = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(res.data.thread)).toMatchSnapshot()\n  })\n\n  it('does not show reposted content from a muted account in author feed', async () => {\n    await sc.repost(dan, sc.posts[bob][0].ref)\n    await sc.repost(dan, sc.posts[bob][1].ref)\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBe(false)\n  })\n\n  it('removes content from muted users on getTimeline', async () => {\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 100 },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBe(false)\n  })\n\n  it('removes content from muted users on getListFeed', async () => {\n    const listRef = await sc.createList(bob, 'test list', 'curate')\n    await sc.addToList(alice, bob, listRef)\n    await sc.addToList(alice, carol, listRef)\n    await sc.addToList(alice, dan, listRef)\n    const res = await agent.api.app.bsky.feed.getListFeed(\n      { list: listRef.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetListFeed,\n        ),\n      },\n    )\n    expect(\n      res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),\n    ).toBe(false)\n  })\n\n  it('returns mute status on getProfile', async () => {\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: bob },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n    expect(res.data.viewer?.muted).toBe(true)\n  })\n\n  it('returns mute status on getProfiles', async () => {\n    const res = await agent.api.app.bsky.actor.getProfiles(\n      { actors: [bob, carol, dan] },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfiles,\n        ),\n      },\n    )\n    expect(res.data.profiles[0].viewer?.muted).toBe(true)\n    expect(res.data.profiles[1].viewer?.muted).toBe(true)\n    expect(res.data.profiles[2].viewer?.muted).toBe(false)\n  })\n\n  it('does not return notifs for muted accounts', async () => {\n    const res = await agent.api.app.bsky.notification.listNotifications(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      res.data.notifications.some((notif) =>\n        [bob, carol].includes(notif.author.did),\n      ),\n    ).toBeFalsy()\n  })\n\n  it('flags muted accounts in get suggestions', async () => {\n    // unfollow so they _would_ show up in suggestions if not for mute\n    await sc.unfollow(alice, bob)\n    await sc.unfollow(alice, carol)\n\n    await network.processAll()\n\n    const res = await agent.api.app.bsky.actor.getSuggestions(\n      {\n        limit: 100,\n      },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    for (const actor of res.data.actors) {\n      if (mutes.includes(actor.did) || mutes.includes(actor.handle)) {\n        expect(actor.viewer?.muted).toBe(true)\n      } else {\n        expect(actor.viewer?.muted).toBe(false)\n      }\n    }\n  })\n\n  it('fetches mutes for the logged-in user.', async () => {\n    const { data: view } = await agent.api.app.bsky.graph.getMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes),\n      },\n    )\n    expect(forSnapshot(view.mutes)).toMatchSnapshot()\n  })\n\n  it('paginates.', async () => {\n    const results = (results: GetMutesOutputSchema[]) =>\n      results.flatMap((res) => res.mutes)\n    const paginator = async (cursor?: string) => {\n      const { data: view } = await agent.api.app.bsky.graph.getMutes(\n        { cursor, limit: 2 },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyGraphGetMutes,\n          ),\n        },\n      )\n      return view\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.mutes.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.graph.getMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes),\n      },\n    )\n\n    expect(full.data.mutes.length).toEqual(8)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('removes mute.', async () => {\n    const { data: initial } = await agent.api.app.bsky.graph.getMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes),\n      },\n    )\n    expect(initial.mutes.length).toEqual(8)\n    expect(initial.mutes.map((m) => m.handle)).toContain('elta48.test')\n\n    await agent.api.app.bsky.graph.unmuteActor(\n      { actor: sc.dids['elta48.test'] },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyGraphUnmuteActor,\n        ),\n        encoding: 'application/json',\n      },\n    )\n\n    const { data: final } = await agent.api.app.bsky.graph.getMutes(\n      {},\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetMutes),\n      },\n    )\n    expect(final.mutes.length).toEqual(7)\n    expect(final.mutes.map((m) => m.handle)).not.toContain('elta48.test')\n\n    await agent.api.app.bsky.graph.muteActor(\n      { actor: sc.dids['elta48.test'] },\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyGraphMuteActor),\n        encoding: 'application/json',\n      },\n    )\n  })\n\n  it('does not allow muting self.', async () => {\n    const promise = agent.api.app.bsky.graph.muteActor(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyGraphMuteActor),\n        encoding: 'application/json',\n      },\n    )\n    await expect(promise).rejects.toThrow() // @TODO check error message w/ grpc error passthru\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/notifications.test.ts",
    "content": "import { AppBskyNotificationDeclaration, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { TAG_HIDE } from '@atproto/dev-env/dist/seed/thread-v2'\nimport { delayCursor } from '../../src/api/app/bsky/notification/listNotifications'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { ProfileView } from '../../src/lexicon/types/app/bsky/actor/defs'\nimport {\n  ActivitySubscription,\n  ChatPreference,\n  FilterablePreference,\n  Preference,\n  Preferences,\n} from '../../src/lexicon/types/app/bsky/notification/defs'\nimport {\n  OutputSchema as ListActivitySubscriptionsOutputSchema,\n  QueryParams,\n} from '../../src/lexicon/types/app/bsky/notification/listActivitySubscriptions'\nimport {\n  Notification,\n  OutputSchema as ListNotificationsOutputSchema,\n} from '../../src/lexicon/types/app/bsky/notification/listNotifications'\nimport { InputSchema } from '../../src/lexicon/types/app/bsky/notification/putPreferencesV2'\nimport { Namespaces } from '../../src/stash'\nimport { forSnapshot, paginateAll } from '../_util'\n\ntype Database = TestNetwork['bsky']['db']\n\ndescribe('notification views', () => {\n  let network: TestNetwork\n  let db: Database\n\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n  let eve: string\n  let fred: string\n  let greg: string\n  let han: string\n  let blocked: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_notifications',\n      bsky: {\n        threadTagsHide: new Set([TAG_HIDE]),\n      },\n    })\n    db = network.bsky.db\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.bsky.db.db\n      .updateTable('actor')\n      .set({ trustedVerifier: true })\n      .where('did', '=', alice)\n      .execute()\n    await sc.createAccount('eve', {\n      email: 'eve@test.com',\n      handle: 'eve.test',\n      password: 'eve-pass',\n    })\n    await sc.createAccount('fred', {\n      email: 'fred@test.com',\n      handle: 'fred.test',\n      password: 'fred-pass',\n    })\n    await sc.createAccount('greg', {\n      email: 'greg@test.com',\n      handle: 'greg.test',\n      password: 'greg-pass',\n    })\n    await sc.createAccount('han', {\n      email: 'han@test.com',\n      handle: 'han.test',\n      password: 'han-pass',\n    })\n    await sc.createAccount('blocked', {\n      email: 'blocked@test.com',\n      handle: 'blocked.test',\n      password: 'blocked-pass',\n    })\n\n    await network.processAll()\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    eve = sc.dids.eve\n    fred = sc.dids.fred\n    greg = sc.dids.greg\n    han = sc.dids.han\n    blocked = sc.dids.blocked\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const sortNotifs = (notifs: Notification[]) => {\n    // Need to sort because notification ordering is not well-defined\n    return notifs.sort((a, b) => {\n      const stableUriA = a.uri.replace(\n        /\\/did:plc:.+?\\//,\n        `/${a.author.handle}/`,\n      )\n      const stableUriB = b.uri.replace(\n        /\\/did:plc:.+?\\//,\n        `/${b.author.handle}/`,\n      )\n      if (stableUriA === stableUriB) {\n        return a.indexedAt > b.indexedAt ? -1 : 1\n      }\n      return stableUriA > stableUriB ? -1 : 1\n    })\n  }\n\n  it('fetches notification count without a last-seen', async () => {\n    const notifCountAlice =\n      await agent.api.app.bsky.notification.getUnreadCount(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyNotificationGetUnreadCount,\n          ),\n        },\n      )\n\n    expect(notifCountAlice.data.count).toBe(12)\n\n    const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyNotificationGetUnreadCount,\n        ),\n      },\n    )\n\n    expect(notifCountBob.data.count).toBeGreaterThanOrEqual(3)\n  })\n\n  it('generates notifications for all reply ancestors', async () => {\n    // Add to reply chain, post ancestors: alice -> bob -> alice -> carol.\n    // Should have added one notification for each of alice and bob.\n    await sc.reply(\n      sc.dids.carol,\n      sc.posts[alice][1].ref,\n      sc.replies[alice][0].ref,\n      'indeed',\n    )\n    await network.processAll()\n\n    const notifCountAlice =\n      await agent.api.app.bsky.notification.getUnreadCount(\n        {},\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyNotificationGetUnreadCount,\n          ),\n        },\n      )\n\n    expect(notifCountAlice.data.count).toBe(13)\n\n    const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyNotificationGetUnreadCount,\n        ),\n      },\n    )\n\n    expect(notifCountBob.data.count).toBeGreaterThanOrEqual(4)\n  })\n\n  it('does not give notifs for a deleted subject', async () => {\n    const root = await sc.post(sc.dids.alice, 'root')\n    const first = await sc.reply(sc.dids.bob, root.ref, root.ref, 'first')\n    await sc.deletePost(sc.dids.alice, root.ref.uri)\n    const second = await sc.reply(sc.dids.carol, root.ref, first.ref, 'second')\n    await network.processAll()\n\n    const notifsAlice = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    const hasNotif = notifsAlice.data.notifications.some(\n      (notif) => notif.uri === second.ref.uriStr,\n    )\n    expect(hasNotif).toBe(false)\n\n    // cleanup\n    await sc.deletePost(sc.dids.bob, first.ref.uri)\n    await sc.deletePost(sc.dids.carol, second.ref.uri)\n    await network.processAll()\n  })\n\n  it('generates notifications for quotes', async () => {\n    // Dan was quoted by alice\n    const notifsDan = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      forSnapshot(sortNotifs(notifsDan.data.notifications)),\n    ).toMatchSnapshot()\n  })\n\n  it('generates notifications for likes', async () => {\n    const notifsAlice = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const na = sortNotifs(\n      notifsAlice.data.notifications.filter((n) => n.reason === 'like'),\n    )\n    expect(na).toHaveLength(5)\n    expect(forSnapshot(na)).toMatchSnapshot()\n  })\n\n  it('generates notifications for reposts', async () => {\n    const notifsAlice = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const na = sortNotifs(\n      notifsAlice.data.notifications.filter((n) => n.reason === 'repost'),\n    )\n    expect(na).toHaveLength(2)\n    expect(forSnapshot(na)).toMatchSnapshot()\n  })\n\n  it('generates notifications for likes via repost', async () => {\n    const op = dan\n    const reposter = carol\n    const liker = alice\n    await sc.like(liker, sc.posts[op][1].ref, {\n      via: sc.reposts[reposter][0].raw,\n    })\n    await network.processAll()\n\n    const notifsOp = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          op,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const no = sortNotifs(\n      notifsOp.data.notifications.filter((n) => n.reason === 'like'),\n    )\n    // Like from `alice` in this test.\n    expect(no).toHaveLength(1)\n    expect(forSnapshot(no)).toMatchSnapshot()\n\n    const notifsReposter = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          reposter,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const nr = sortNotifs(\n      notifsReposter.data.notifications.filter(\n        (n) => n.reason === 'like-via-repost',\n      ),\n    )\n    // Like from `alice` in this test.\n    expect(nr).toHaveLength(1)\n    expect(forSnapshot(nr)).toMatchSnapshot()\n  })\n\n  it('does not generate self notifications for likes via own repost', async () => {\n    const op = dan\n    const reposter = carol\n    await sc.like(reposter, sc.posts[op][1].ref, {\n      via: sc.reposts[reposter][0].raw,\n    })\n    await network.processAll()\n\n    const notifsOp = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          op,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const no = sortNotifs(\n      notifsOp.data.notifications.filter((n) => n.reason === 'like'),\n    )\n    // Like from `alice` in previous test + `carol` on this test.\n    expect(no).toHaveLength(2)\n    expect(forSnapshot(no)).toMatchSnapshot()\n\n    const notifsReposter = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          reposter,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const nr = sortNotifs(\n      notifsReposter.data.notifications.filter(\n        (n) => n.reason === 'like-via-repost',\n      ),\n    )\n    // Like from `alice` in previous test.\n    expect(nr).toHaveLength(1)\n    expect(forSnapshot(nr)).toMatchSnapshot()\n  })\n\n  it('generates notifications for reposts via repost', async () => {\n    const op = dan\n    const reposter = carol\n    const reReposter = alice\n    await sc.repost(reReposter, sc.posts[op][1].ref, {\n      via: sc.reposts[reposter][0].raw,\n    })\n    await network.processAll()\n\n    const notifsOp = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          op,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const no = sortNotifs(\n      notifsOp.data.notifications.filter((n) => n.reason === 'repost'),\n    )\n    // Repost from `carol` in seeds + `alice` on this test.\n    expect(no).toHaveLength(2)\n    expect(forSnapshot(no)).toMatchSnapshot()\n\n    const notifsReposter = await agent.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          reposter,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const nr = sortNotifs(\n      notifsReposter.data.notifications.filter(\n        (n) => n.reason === 'repost-via-repost',\n      ),\n    )\n    // Repost from `alice` in this test.\n    expect(nr).toHaveLength(1)\n    expect(forSnapshot(nr)).toMatchSnapshot()\n  })\n\n  it('generates notifications for verification created and removed', async () => {\n    await sc.verify(\n      sc.dids.alice,\n      sc.dids.bob,\n      sc.accounts[sc.dids.bob].handle,\n      sc.profiles[sc.dids.bob].displayName,\n    )\n    await network.processAll()\n    const notifsBob1 = await agent.app.bsky.notification.listNotifications(\n      { reasons: ['verified', 'unverified'] },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      forSnapshot(sortNotifs(notifsBob1.data.notifications)),\n    ).toMatchSnapshot()\n\n    await sc.unverify(sc.dids.alice, sc.dids.bob)\n    await network.processAll()\n    const notifsBob2 = await agent.app.bsky.notification.listNotifications(\n      { reasons: ['verified', 'unverified'] },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(\n      forSnapshot(sortNotifs(notifsBob2.data.notifications)),\n    ).toMatchSnapshot()\n  })\n\n  it('fetches notifications without a last-seen', async () => {\n    const notifRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const notifs = notifRes.data.notifications\n    expect(notifs.length).toBe(13)\n\n    const readStates = notifs.map((notif) => notif.isRead)\n    expect(readStates).toEqual(notifs.map((_, i) => i !== 0)) // only first appears unread\n\n    expect(forSnapshot(sortNotifs(notifs))).toMatchSnapshot()\n  })\n\n  it('paginates', async () => {\n    const results = (results: ListNotificationsOutputSchema[]) =>\n      sortNotifs(results.flatMap((res) => res.notifications))\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.notification.listNotifications(\n        { cursor, limit: 6 },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.notifications.length).toBeLessThanOrEqual(6),\n    )\n\n    const full = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    expect(full.data.notifications.length).toEqual(13)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches notification count with a last-seen', async () => {\n    const full = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    const seenAt = full.data.notifications[3].indexedAt\n    await agent.api.app.bsky.notification.updateSeen(\n      { seenAt },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationUpdateSeen,\n        ),\n        encoding: 'application/json',\n      },\n    )\n    const full2 = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(full2.data.notifications.length).toBe(full.data.notifications.length)\n    expect(full2.data.seenAt).toEqual(seenAt)\n\n    const notifCount = await agent.api.app.bsky.notification.getUnreadCount(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationGetUnreadCount,\n        ),\n      },\n    )\n\n    expect(notifCount.data.count).toBe(\n      full.data.notifications.filter((n) => n.indexedAt > seenAt).length,\n    )\n    expect(notifCount.data.count).toBeGreaterThan(0)\n\n    // reset last-seen\n    await agent.api.app.bsky.notification.updateSeen(\n      { seenAt: new Date(0).toISOString() },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationUpdateSeen,\n        ),\n        encoding: 'application/json',\n      },\n    )\n  })\n\n  it('fetches notifications with a last-seen', async () => {\n    const full = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    const seenAt = full.data.notifications[3].indexedAt\n    await agent.api.app.bsky.notification.updateSeen(\n      { seenAt },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationUpdateSeen,\n        ),\n        encoding: 'application/json',\n      },\n    )\n    const notifRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    const notifs = notifRes.data.notifications\n    expect(notifs.length).toBe(13)\n\n    const readStates = notifs.map((notif) => notif.isRead)\n    expect(readStates).toEqual(notifs.map((n) => n.indexedAt < seenAt))\n    // reset last-seen\n    await agent.api.app.bsky.notification.updateSeen(\n      { seenAt: new Date(0).toISOString() },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationUpdateSeen,\n        ),\n        encoding: 'application/json',\n      },\n    )\n  })\n\n  it('fetches notifications omitting mentions and replies for taken-down posts', async () => {\n    const postRef1 = sc.replies[sc.dids.carol][0].ref // Reply\n    const postRef2 = sc.posts[sc.dids.dan][1].ref // Mention\n    await Promise.all(\n      [postRef1, postRef2].map((postRef) =>\n        network.bsky.ctx.dataplane.takedownRecord({\n          recordUri: postRef.uriStr,\n        }),\n      ),\n    )\n\n    const notifRes = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    const notifCount = await agent.api.app.bsky.notification.getUnreadCount(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationGetUnreadCount,\n        ),\n      },\n    )\n\n    const notifs = sortNotifs(notifRes.data.notifications)\n    expect(notifs.length).toBe(11)\n    expect(forSnapshot(notifs)).toMatchSnapshot()\n    expect(notifCount.data.count).toBe(11)\n\n    // Cleanup\n    await Promise.all(\n      [postRef1, postRef2].map((postRef) =>\n        network.bsky.ctx.dataplane.untakedownRecord({\n          recordUri: postRef.uriStr,\n        }),\n      ),\n    )\n  })\n\n  it('fetches notifications with explicit priority', async () => {\n    const priority = await agent.api.app.bsky.notification.listNotifications(\n      { priority: true },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    // only notifs from follow (alice)\n    expect(\n      priority.data.notifications.every(\n        (notif) => ![sc.dids.bob, sc.dids.dan].includes(notif.author.did),\n      ),\n    ).toBe(true)\n    expect(forSnapshot(priority.data)).toMatchSnapshot()\n    const noPriority = await agent.api.app.bsky.notification.listNotifications(\n      { priority: false },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(forSnapshot(noPriority.data)).toMatchSnapshot()\n  })\n\n  it('fetches notifications with default priority', async () => {\n    await agent.api.app.bsky.notification.putPreferences(\n      { priority: true },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyNotificationPutPreferences,\n        ),\n      },\n    )\n    await network.processAll()\n    const notifs = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    // only notifs from follow (alice)\n    expect(\n      notifs.data.notifications.every(\n        (notif) => ![sc.dids.bob, sc.dids.dan].includes(notif.author.did),\n      ),\n    ).toBe(true)\n    expect(forSnapshot(notifs.data)).toMatchSnapshot()\n    await agent.api.app.bsky.notification.putPreferences(\n      { priority: false },\n      {\n        encoding: 'application/json',\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyNotificationPutPreferences,\n        ),\n      },\n    )\n    await network.processAll()\n  })\n\n  it('filters notifications by reason', async () => {\n    const res = await agent.app.bsky.notification.listNotifications(\n      {\n        reasons: ['mention'],\n      },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(res.data.notifications.length).toBe(1)\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('filters notifications by multiple reasons', async () => {\n    const res = await agent.app.bsky.notification.listNotifications(\n      {\n        reasons: ['mention', 'reply'],\n      },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(res.data.notifications.length).toBe(4)\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('paginates filtered notifications', async () => {\n    const results = (results: ListNotificationsOutputSchema[]) =>\n      sortNotifs(results.flatMap((res) => res.notifications))\n    const paginator = async (cursor?: string) => {\n      const res = await agent.app.bsky.notification.listNotifications(\n        { reasons: ['mention', 'reply'], cursor, limit: 2 },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.notifications.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.app.bsky.notification.listNotifications(\n      { reasons: ['mention', 'reply'] },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n\n    expect(full.data.notifications.length).toBe(4)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  describe('handles hide tag filters', () => {\n    beforeAll(async () => {\n      const danPost = await sc.post(sc.dids.dan, 'hello friends')\n      await network.processAll()\n      const eveReply = await sc.reply(\n        sc.dids.eve,\n        danPost.ref,\n        danPost.ref,\n        'no thanks',\n      )\n      await network.processAll()\n      await createTag(db, { uri: eveReply.ref.uri.toString(), val: TAG_HIDE })\n    })\n\n    it('filters posts with hide tag', async () => {\n      const results = await agent.app.bsky.notification.listNotifications(\n        { reasons: ['reply'] },\n        {\n          headers: await network.serviceHeaders(\n            dan,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n      expect(results.data.notifications.length).toEqual(0)\n      expect(forSnapshot(results.data.notifications)).toMatchSnapshot()\n    })\n\n    it('shows posts with hide tag if they are followed', async () => {\n      await sc.follow(dan, eve)\n      await network.processAll()\n      const results = await agent.app.bsky.notification.listNotifications(\n        { reasons: ['reply'] },\n        {\n          headers: await network.serviceHeaders(\n            dan,\n            ids.AppBskyNotificationListNotifications,\n          ),\n        },\n      )\n      expect(results.data.notifications.length).toEqual(1)\n      expect(forSnapshot(results.data.notifications)).toMatchSnapshot()\n    })\n  })\n\n  describe('notifications delay', () => {\n    const notificationsDelayMs = 5_000\n\n    let delayNetwork: TestNetwork\n    let delayAgent: AtpAgent\n    let delaySc: SeedClient\n    let delayAlice: string\n\n    beforeAll(async () => {\n      delayNetwork = await TestNetwork.create({\n        bsky: {\n          notificationsDelayMs,\n        },\n        dbPostgresSchema: 'bsky_views_notifications_delay',\n      })\n      delayAgent = delayNetwork.bsky.getClient()\n      delaySc = delayNetwork.getSeedClient()\n      await basicSeed(delaySc)\n      await delayNetwork.processAll()\n      delayAlice = delaySc.dids.alice\n\n      // Add to reply chain, post ancestors: alice -> bob -> alice -> carol.\n      // Should have added one notification for each of alice and bob.\n      await delaySc.reply(\n        delaySc.dids.carol,\n        delaySc.posts[delayAlice][1].ref,\n        delaySc.replies[delayAlice][0].ref,\n        'indeed',\n      )\n      await delayNetwork.processAll()\n\n      // @NOTE: Use fake timers after inserting seed data,\n      // to avoid inserting all notifications with the same timestamp.\n      jest.useFakeTimers({\n        doNotFake: [\n          'nextTick',\n          'performance',\n          'setImmediate',\n          'setInterval',\n          'setTimeout',\n        ],\n      })\n    })\n\n    afterAll(async () => {\n      jest.useRealTimers()\n      await delayNetwork.close()\n    })\n\n    it('paginates', async () => {\n      const firstNotification = await delayNetwork.bsky.db.db\n        .selectFrom('notification')\n        .selectAll()\n        .limit(1)\n        .orderBy('sortAt', 'asc')\n        .executeTakeFirstOrThrow()\n      // Sets the system time to when the first notification happened.\n      // At this point we won't have any notifications that already crossed the delay threshold.\n      jest.setSystemTime(new Date(firstNotification.sortAt))\n\n      const results = (results: ListNotificationsOutputSchema[]) =>\n        sortNotifs(results.flatMap((res) => res.notifications))\n      const paginator = async (cursor?: string) => {\n        const res =\n          await delayAgent.api.app.bsky.notification.listNotifications(\n            { cursor, limit: 6 },\n            {\n              headers: await delayNetwork.serviceHeaders(\n                delayAlice,\n                ids.AppBskyNotificationListNotifications,\n              ),\n            },\n          )\n        return res.data\n      }\n\n      const paginatedAllBeforeDelay = await paginateAll(paginator)\n      paginatedAllBeforeDelay.forEach((res) =>\n        expect(res.notifications.length).toBe(0),\n      )\n      const fullBeforeDelay =\n        await delayAgent.api.app.bsky.notification.listNotifications(\n          {},\n          {\n            headers: await delayNetwork.serviceHeaders(\n              delayAlice,\n              ids.AppBskyNotificationListNotifications,\n            ),\n          },\n        )\n\n      expect(fullBeforeDelay.data.notifications.length).toEqual(0)\n      expect(results(paginatedAllBeforeDelay)).toEqual(\n        results([fullBeforeDelay.data]),\n      )\n\n      const lastNotification = await delayNetwork.bsky.db.db\n        .selectFrom('notification')\n        .selectAll()\n        .limit(1)\n        .orderBy('sortAt', 'desc')\n        .executeTakeFirstOrThrow()\n      // Sets the system time to when the last notification happened and the delay has elapsed.\n      // At this point we all notifications already crossed the delay threshold.\n      jest.setSystemTime(\n        new Date(\n          new Date(lastNotification.sortAt).getTime() +\n            notificationsDelayMs +\n            1,\n        ),\n      )\n\n      const paginatedAllAfterDelay = await paginateAll(paginator)\n      paginatedAllAfterDelay.forEach((res) =>\n        expect(res.notifications.length).toBeLessThanOrEqual(6),\n      )\n      const fullAfterDelay =\n        await delayAgent.api.app.bsky.notification.listNotifications(\n          {},\n          {\n            headers: await delayNetwork.serviceHeaders(\n              delayAlice,\n              ids.AppBskyNotificationListNotifications,\n            ),\n          },\n        )\n\n      expect(fullAfterDelay.data.notifications.length).toEqual(13)\n      expect(results(paginatedAllAfterDelay)).toEqual(\n        results([fullAfterDelay.data]),\n      )\n    })\n\n    describe('cursor delay', () => {\n      const delay0s = 0\n      const delay5s = 5_000\n\n      const now = '2021-01-01T01:00:00.000Z'\n      const nowMinus2s = '2021-01-01T00:59:58.000Z'\n      const nowMinus5s = '2021-01-01T00:59:55.000Z'\n      const nowMinus8s = '2021-01-01T00:59:52.000Z'\n\n      beforeAll(async () => {\n        jest.useFakeTimers({ doNotFake: ['performance'] })\n        jest.setSystemTime(new Date(now))\n      })\n\n      afterAll(async () => {\n        jest.useRealTimers()\n      })\n\n      describe('for undefined cursor', () => {\n        it('returns now minus delay', async () => {\n          const delayedCursor = delayCursor(undefined, delay5s)\n          expect(delayedCursor).toBe(nowMinus5s)\n        })\n\n        it('returns now if delay is 0', async () => {\n          const delayedCursor = delayCursor(undefined, delay0s)\n          expect(delayedCursor).toBe(now)\n        })\n      })\n\n      describe('for defined cursor', () => {\n        it('returns original cursor if delay is 0', async () => {\n          const originalCursor = nowMinus2s\n          const delayedCursor = delayCursor(originalCursor, delay0s)\n          expect(delayedCursor).toBe(originalCursor)\n        })\n\n        it('returns \"now minus delay\" for cursor that is after that', async () => {\n          // Cursor is \"now - 2s\", should become \"now - 5s\"\n          const originalCursor = nowMinus2s\n          const cursor = delayCursor(originalCursor, delay5s)\n          expect(cursor).toBe(nowMinus5s)\n        })\n\n        it('returns original cursor for cursor that is before \"now minus delay\"', async () => {\n          // Cursor is \"now - 8s\", should stay like that.\n          const originalCursor = nowMinus8s\n          const cursor = delayCursor(originalCursor, delay5s)\n          expect(cursor).toBe(originalCursor)\n        })\n\n        it('passes through a non-date cursor', async () => {\n          const originalCursor = '123_abc'\n          const cursor = delayCursor(originalCursor, delay5s)\n          expect(cursor).toBe(originalCursor)\n        })\n      })\n    })\n  })\n\n  describe('preferences v2', () => {\n    beforeEach(async () => {\n      await clearPrivateData(db)\n    })\n\n    // Defaults\n    const fp: FilterablePreference = {\n      include: 'all',\n      list: true,\n      push: true,\n    }\n    const p: Preference = {\n      list: true,\n      push: true,\n    }\n    const cp: ChatPreference = {\n      include: 'all',\n      push: true,\n    }\n\n    it('gets preferences filling up with the defaults', async () => {\n      const actorDid = sc.dids.carol\n\n      const getAndAssert = async (\n        expectedApi: Preferences,\n        expectedDb: Preferences | undefined,\n      ) => {\n        const { data } = await agent.app.bsky.notification.getPreferences(\n          {},\n          {\n            headers: await network.serviceHeaders(\n              actorDid,\n              ids.AppBskyNotificationGetPreferences,\n            ),\n          },\n        )\n        expect(data.preferences).toStrictEqual(expectedApi)\n\n        const dbResult = await db.db\n          .selectFrom('private_data')\n          .selectAll()\n          .where('actorDid', '=', actorDid)\n          .where(\n            'namespace',\n            '=',\n            Namespaces.AppBskyNotificationDefsPreferences,\n          )\n          .where('key', '=', 'self')\n          .executeTakeFirst()\n        if (dbResult === undefined) {\n          expect(dbResult).toBe(expectedDb)\n        } else {\n          expect(dbResult).toStrictEqual({\n            actorDid: actorDid,\n            namespace: Namespaces.AppBskyNotificationDefsPreferences,\n            key: 'self',\n            indexedAt: expect.any(String),\n            payload: expect.anything(), // Better to compare payload parsed.\n            updatedAt: expect.any(String),\n          })\n          expect(JSON.parse(dbResult.payload)).toStrictEqual({\n            $type: Namespaces.AppBskyNotificationDefsPreferences,\n            ...expectedDb,\n          })\n        }\n      }\n\n      const expectedApi0: Preferences = {\n        chat: cp,\n        follow: fp,\n        like: fp,\n        likeViaRepost: fp,\n        mention: fp,\n        quote: fp,\n        reply: fp,\n        repost: fp,\n        repostViaRepost: fp,\n        starterpackJoined: p,\n        subscribedPost: p,\n        unverified: p,\n        verified: p,\n      }\n      // The user has no preferences set yet, so nothing stored.\n      const expectedDb0 = undefined\n      await getAndAssert(expectedApi0, expectedDb0)\n\n      await agent.app.bsky.notification.putPreferencesV2(\n        { verified: { list: false, push: false } },\n        {\n          encoding: 'application/json',\n          headers: await network.serviceHeaders(\n            actorDid,\n            ids.AppBskyNotificationPutPreferencesV2,\n          ),\n        },\n      )\n      await network.processAll()\n\n      const expectedApi1: Preferences = {\n        chat: cp,\n        follow: fp,\n        like: fp,\n        likeViaRepost: fp,\n        mention: fp,\n        quote: fp,\n        reply: fp,\n        repost: fp,\n        repostViaRepost: fp,\n        starterpackJoined: p,\n        subscribedPost: p,\n        unverified: p,\n        verified: { list: false, push: false },\n      }\n      // Stored all the defaults.\n      const expectedDb1 = expectedApi1\n      await getAndAssert(expectedApi1, expectedDb1)\n    })\n\n    it('stores the preferences setting the defaults', async () => {\n      const actorDid = sc.dids.carol\n\n      const putAndAssert = async (\n        input: InputSchema,\n        expected: Preferences,\n      ) => {\n        const { data } = await agent.app.bsky.notification.putPreferencesV2(\n          input,\n          {\n            encoding: 'application/json',\n            headers: await network.serviceHeaders(\n              actorDid,\n              ids.AppBskyNotificationPutPreferencesV2,\n            ),\n          },\n        )\n        await network.processAll()\n        expect(data.preferences).toStrictEqual(expected)\n\n        const dbResult = await db.db\n          .selectFrom('private_data')\n          .selectAll()\n          .where('actorDid', '=', actorDid)\n          .where(\n            'namespace',\n            '=',\n            Namespaces.AppBskyNotificationDefsPreferences,\n          )\n          .where('key', '=', 'self')\n          .executeTakeFirstOrThrow()\n        expect(dbResult).toStrictEqual({\n          actorDid: actorDid,\n          namespace: Namespaces.AppBskyNotificationDefsPreferences,\n          key: 'self',\n          indexedAt: expect.any(String),\n          payload: expect.anything(), // Better to compare payload parsed.\n          updatedAt: expect.any(String),\n        })\n        expect(JSON.parse(dbResult.payload)).toStrictEqual({\n          $type: Namespaces.AppBskyNotificationDefsPreferences,\n          ...expected,\n        })\n      }\n\n      const input0 = {\n        chat: {\n          push: false,\n          include: 'accepted',\n        },\n      }\n      const expected0: Preferences = {\n        chat: input0.chat,\n        follow: fp,\n        like: fp,\n        likeViaRepost: fp,\n        mention: fp,\n        quote: fp,\n        reply: fp,\n        repost: fp,\n        repostViaRepost: fp,\n        starterpackJoined: p,\n        subscribedPost: p,\n        unverified: p,\n        verified: p,\n      }\n      await putAndAssert(input0, expected0)\n\n      const input1 = {\n        mention: {\n          list: false,\n          push: false,\n          include: 'follows',\n        },\n      }\n      const expected1: Preferences = {\n        // Kept from the previous call.\n        chat: input0.chat,\n        follow: fp,\n        like: fp,\n        likeViaRepost: fp,\n        mention: input1.mention,\n        quote: fp,\n        reply: fp,\n        repost: fp,\n        repostViaRepost: fp,\n        starterpackJoined: p,\n        subscribedPost: p,\n        unverified: p,\n        verified: p,\n      }\n      await putAndAssert(input1, expected1)\n    })\n  })\n\n  describe('activity subscriptions', () => {\n    const sortProfiles = (profiles: ProfileView[]) => {\n      return profiles.sort((a, b) => (a.handle > b.handle ? 1 : -1))\n    }\n\n    const declare = async (actor: string, value: string) => {\n      await pdsAgent.com.atproto.repo.createRecord(\n        {\n          repo: actor,\n          collection: ids.AppBskyNotificationDeclaration,\n          rkey: 'self',\n          record: {\n            allowSubscriptions: value,\n          } as AppBskyNotificationDeclaration.Record,\n        },\n        { headers: sc.getHeaders(actor), encoding: 'application/json' },\n      )\n    }\n\n    const put = async (\n      actor: string,\n      subject: string,\n      val: ActivitySubscription,\n    ) =>\n      agent.app.bsky.notification.putActivitySubscription(\n        {\n          subject,\n          activitySubscription: val,\n        },\n        {\n          headers: await network.serviceHeaders(\n            actor,\n            ids.AppBskyNotificationPutActivitySubscription,\n          ),\n        },\n      )\n\n    const list = async (actor: string, params?: QueryParams) =>\n      agent.app.bsky.notification.listActivitySubscriptions(params ?? {}, {\n        headers: await network.serviceHeaders(\n          actor,\n          ids.AppBskyNotificationListActivitySubscriptions,\n        ),\n      })\n\n    const associatedAllowSub = async (actor: string, subject: string) => {\n      const { data } = await agent.app.bsky.actor.getProfile(\n        { actor: subject },\n        {\n          headers: await network.serviceHeaders(\n            actor,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      return data.associated?.activitySubscription?.allowSubscriptions\n    }\n\n    const viewerActivitySub = async (actor: string, subject: string) => {\n      const { data } = await agent.app.bsky.actor.getProfile(\n        { actor: subject },\n        {\n          headers: await network.serviceHeaders(\n            actor,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      return data.viewer?.activitySubscription\n    }\n\n    beforeAll(async () => {\n      // 'none' declaration.\n      await declare(bob, 'none')\n\n      // 'mutuals' declaration and both follow.\n      await declare(carol, 'mutuals')\n      await sc.follow(alice, carol)\n      await sc.follow(carol, alice)\n\n      // 'mutuals' declaration but only actor follows.\n      await declare(dan, 'mutuals')\n      await sc.follow(alice, dan)\n\n      // 'mutuals' declaration but only subject follows.\n      await declare(eve, 'mutuals')\n      await sc.follow(eve, alice)\n\n      // 'followers' declaration and viewer follows.\n      await declare(fred, 'followers')\n      await sc.follow(alice, fred)\n\n      // 'followers' declaration but viewer does not follow.\n      await declare(greg, 'followers')\n\n      // blocked.\n      await declare(blocked, 'followers')\n      await sc.block(alice, blocked)\n\n      await network.processAll()\n    })\n\n    beforeEach(async () => {\n      await clearActivitySubscription(db)\n    })\n\n    it('lists an empty list of subscriptions', async () => {\n      const actorDid = alice\n\n      const { data } = await list(actorDid)\n\n      expect(data.cursor).toBeUndefined()\n      expect(data.subscriptions).toHaveLength(0)\n    })\n\n    it('does not allow subscribing to self', async () => {\n      const actorDid = alice\n      const promise = put(actorDid, actorDid, { post: true, reply: false })\n\n      await expect(promise).rejects.toThrow('Cannot subscribe to own activity')\n    })\n\n    it('inserts a subscription entry if it does not exist', async () => {\n      const actorDid = alice\n      const subjectDid = fred\n      const val = { post: true, reply: false }\n\n      const { data: createData } = await put(actorDid, subjectDid, val)\n      expect(createData).toStrictEqual({\n        subject: subjectDid,\n        activitySubscription: val,\n      })\n\n      const { data: listData } = await list(actorDid)\n      expect(listData).toEqual({\n        cursor: expect.any(String),\n        subscriptions: [\n          expect.objectContaining({\n            did: subjectDid,\n            viewer: expect.objectContaining({ activitySubscription: val }),\n          }),\n        ],\n      })\n    })\n\n    it('updates a subscription entry if it exists', async () => {\n      const actorDid = alice\n      const subjectDid = fred\n      const valCreate = { post: true, reply: false }\n      const valUpdate = { post: false, reply: true }\n\n      const { data: createData } = await put(actorDid, subjectDid, valCreate)\n      expect(createData).toStrictEqual({\n        subject: subjectDid,\n        activitySubscription: valCreate,\n      })\n\n      const { data: updateData } = await put(actorDid, subjectDid, valUpdate)\n      expect(updateData).toStrictEqual({\n        subject: subjectDid,\n        activitySubscription: valUpdate,\n      })\n\n      const { data: listData } = await list(actorDid)\n      expect(listData).toEqual({\n        cursor: expect.any(String),\n        subscriptions: [\n          expect.objectContaining({\n            did: subjectDid,\n            viewer: expect.objectContaining({\n              activitySubscription: valUpdate,\n            }),\n          }),\n        ],\n      })\n    })\n\n    it('deletes a subscription entry when all options are turned off', async () => {\n      const actorDid = alice\n      const subjectDid = fred\n      const valCreate = { post: true, reply: false }\n      const valDelete = { post: false, reply: false }\n\n      await put(actorDid, subjectDid, valCreate)\n      const { data: list0 } = await list(actorDid)\n      expect(list0.subscriptions).toHaveLength(1)\n\n      await put(actorDid, subjectDid, valDelete)\n      const { data: list1 } = await list(actorDid)\n      expect(list1.subscriptions).toHaveLength(0)\n    })\n\n    it('paginates', async () => {\n      const actorDid = alice\n      const limit = 2\n      const val = { post: true, reply: false }\n\n      await put(actorDid, bob, val)\n      await put(actorDid, carol, val)\n      await put(actorDid, dan, val)\n      await put(actorDid, eve, val)\n      await put(actorDid, fred, val)\n      await put(actorDid, blocked, val) // blocked is removed from the list.\n\n      const results = (results: ListActivitySubscriptionsOutputSchema[]) =>\n        sortProfiles(\n          results.flatMap(\n            (res: ListActivitySubscriptionsOutputSchema) => res.subscriptions,\n          ),\n        )\n      const paginator = async (cursor?: string) => {\n        const { data } = await list(actorDid, { cursor, limit })\n        return data\n      }\n\n      const paginatedAll = await paginateAll(paginator)\n      paginatedAll.forEach((res) =>\n        expect(res.subscriptions.length).toBeLessThanOrEqual(limit),\n      )\n\n      const full = await list(actorDid)\n      expect(full.data.subscriptions.length).toEqual(5)\n      expect(results(paginatedAll)).toEqual(results([full.data]))\n    })\n\n    it('gets the declaration record', async () => {\n      const declaration = await pdsAgent.com.atproto.repo.getRecord({\n        repo: carol,\n        collection: 'app.bsky.notification.declaration',\n        rkey: 'self',\n      })\n\n      expect(declaration.data.value.allowSubscriptions).toEqual('mutuals')\n    })\n\n    describe('activity subscription declaration', () => {\n      it('includes the declaration in the profile view', async () => {\n        await expect(associatedAllowSub(alice, bob)).resolves.toBe('none')\n        await expect(associatedAllowSub(alice, carol)).resolves.toBe('mutuals')\n        await expect(associatedAllowSub(alice, dan)).resolves.toBe('mutuals')\n        await expect(associatedAllowSub(alice, eve)).resolves.toBe('mutuals')\n        await expect(associatedAllowSub(alice, fred)).resolves.toBe('followers')\n        await expect(associatedAllowSub(alice, greg)).resolves.toBe('followers')\n      })\n    })\n\n    describe('activity subscription viewer state', () => {\n      it('includes the relationship in the profile view', async () => {\n        const viewer = alice\n        const val = { post: true, reply: true }\n\n        // 'none' declaration.\n        await put(viewer, bob, val)\n        await expect(viewerActivitySub(viewer, bob)).resolves.toBeUndefined()\n\n        // 'mutuals' declaration and both follow.\n        await put(viewer, carol, val)\n        await expect(viewerActivitySub(viewer, carol)).resolves.toStrictEqual(\n          val,\n        )\n\n        // 'mutuals' declaration but only actor follows.\n        await put(viewer, dan, val)\n        await expect(viewerActivitySub(viewer, dan)).resolves.toBeUndefined()\n\n        // 'mutuals' declaration but only subject follows.\n        await put(viewer, eve, val)\n        await expect(viewerActivitySub(viewer, eve)).resolves.toBeUndefined()\n\n        // 'followers' declaration and viewer follows.\n        await put(viewer, fred, val)\n        await expect(viewerActivitySub(viewer, carol)).resolves.toStrictEqual(\n          val,\n        )\n\n        // 'followers' declaration but viewer does not follow.\n        await expect(viewerActivitySub(viewer, greg)).resolves.toBeUndefined()\n\n        // no declaration\n        await expect(viewerActivitySub(viewer, han)).resolves.toBeUndefined()\n      })\n    })\n  })\n})\n\nconst clearPrivateData = async (db: Database) => {\n  await db.db.deleteFrom('private_data').execute()\n}\n\nconst clearActivitySubscription = async (db: Database) => {\n  await db.db.deleteFrom('activity_subscription').execute()\n}\n\nconst createTag = async (\n  db: Database,\n  opts: {\n    uri: string\n    val: string\n  },\n) => {\n  await db.db\n    .updateTable('record')\n    .set({\n      tags: JSON.stringify([opts.val]),\n    })\n    .where('uri', '=', opts.uri)\n    .returningAll()\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/post-search.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { QueryParams as SearchPostsQueryParams } from '@atproto/api/src/client/types/app/bsky/feed/searchPosts'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { DatabaseSchema } from '../../src'\nimport { ids } from '../../src/lexicon/lexicons'\n\nconst TAG_HIDE = 'hide'\n\ndescribe('appview search', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let ozoneAgent: AtpAgent\n  let sc: SeedClient\n  let post0: Awaited<ReturnType<SeedClient['post']>>\n  let post1: Awaited<ReturnType<SeedClient['post']>>\n  let post2: Awaited<ReturnType<SeedClient['post']>>\n  let allResults: string[]\n  let nonTaggedResults: string[]\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_search',\n      bsky: {\n        searchTagsHide: new Set([TAG_HIDE]),\n      },\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    ozoneAgent = network.ozone.getClient()\n    await basicSeed(sc)\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n\n    post0 = await sc.post(alice, 'good doggo')\n    post1 = await sc.post(alice, 'bad doggo')\n    post2 = await sc.post(alice, 'cute doggo')\n    await network.processAll()\n\n    await createTag(network.bsky.db.db, {\n      uri: post1.ref.uriStr,\n      val: TAG_HIDE,\n    })\n\n    allResults = [post2.ref.uriStr, post1.ref.uriStr, post0.ref.uriStr]\n    nonTaggedResults = [post2.ref.uriStr, post0.ref.uriStr]\n  })\n\n  afterAll(async () => {\n    await deleteTags(network.bsky.db.db, {\n      uri: post1.ref.uriStr,\n    })\n\n    await network.close()\n  })\n\n  describe(`post search with 'top' sort`, () => {\n    type TestCase = {\n      name: string\n      viewer: () => string\n      queryParams: () => SearchPostsQueryParams\n      expectedPostUris: () => string[]\n    }\n\n    const tests: TestCase[] = [\n      // 'top' cases\n      {\n        name: `with 'top' sort, finds only non-tagged posts`,\n        viewer: () => carol,\n        queryParams: () => ({ q: 'doggo', sort: 'top' }),\n        expectedPostUris: () => nonTaggedResults,\n      },\n      {\n        name: `with 'top' sort, includes tagged posts from the viewer`,\n        viewer: () => alice,\n        queryParams: () => ({ q: 'doggo', sort: 'top' }),\n        expectedPostUris: () => allResults,\n      },\n      {\n        name: `with 'top' sort, finds only non-tagged posts, even specifying author`,\n        viewer: () => carol,\n        queryParams: () => ({ q: `doggo`, author: alice, sort: 'top' }),\n        expectedPostUris: () => nonTaggedResults,\n      },\n      {\n        name: `with 'top' sort, finds only non-tagged posts, even specifying from:`,\n        viewer: () => carol,\n        queryParams: () => ({\n          q: `doggo from:${sc.accounts[alice].handle}`,\n          sort: 'top',\n        }),\n        expectedPostUris: () => nonTaggedResults,\n      },\n      {\n        name: `with 'top' sort, finds only non-tagged posts, even specifying DID`,\n        viewer: () => carol,\n        queryParams: () => ({ q: `doggo ${alice}`, sort: 'top' }),\n        expectedPostUris: () => nonTaggedResults,\n      },\n      {\n        name: `with 'top' sort, finds no posts if specifying user who didn't post the term`,\n        viewer: () => carol,\n        queryParams: () => ({ q: `doggo ${bob}`, sort: 'top' }),\n        expectedPostUris: () => [],\n      },\n\n      // 'latest' cases\n      {\n        name: `with 'latest' sort, finds only non-tagged posts`,\n        viewer: () => carol,\n        queryParams: () => ({ q: 'doggo', sort: 'latest' }),\n        expectedPostUris: () => nonTaggedResults,\n      },\n      {\n        name: `with 'latest' sort, includes tagged posts from the viewer`,\n        viewer: () => alice,\n        queryParams: () => ({ q: 'doggo', sort: 'latest' }),\n        expectedPostUris: () => allResults,\n      },\n      {\n        name: `with 'latest' sort, finds all posts if specifying author`,\n        viewer: () => carol,\n        queryParams: () => ({\n          q: `doggo`,\n          author: alice,\n          sort: 'latest',\n        }),\n        expectedPostUris: () => allResults,\n      },\n      {\n        name: `with 'latest' sort, finds all posts if specifying from:`,\n        viewer: () => carol,\n        queryParams: () => ({\n          q: `doggo from:${sc.accounts[alice].handle}`,\n          sort: 'latest',\n        }),\n        expectedPostUris: () => allResults,\n      },\n      {\n        name: `with 'latest' sort, finds all posts if specifying DID`,\n        viewer: () => carol,\n        queryParams: () => ({ q: `doggo ${alice}`, sort: 'latest' }),\n        expectedPostUris: () => allResults,\n      },\n      {\n        name: `with 'latest' sort, finds no posts if specifying user who didn't post the term`,\n        viewer: () => carol,\n        queryParams: () => ({ q: `doggo ${bob}`, sort: 'latest' }),\n        expectedPostUris: () => [],\n      },\n    ]\n\n    it.each(tests)(\n      '$name',\n      async ({ viewer, queryParams, expectedPostUris }) => {\n        const res = await agent.app.bsky.feed.searchPosts(queryParams(), {\n          headers: await network.serviceHeaders(\n            viewer(),\n            ids.AppBskyFeedSearchPosts,\n          ),\n        })\n        expect(res.data.posts.map((p) => p.uri)).toStrictEqual(\n          expectedPostUris(),\n        )\n      },\n    )\n\n    it('mod service finds even tagged posts', async () => {\n      const resTop = await ozoneAgent.app.bsky.feed.searchPosts(\n        { q: 'doggo', sort: 'top' },\n        { headers: await network.ozone.modHeaders(ids.AppBskyFeedSearchPosts) },\n      )\n      const resLatest = await ozoneAgent.app.bsky.feed.searchPosts(\n        { q: 'doggo', sort: 'latest' },\n        { headers: await network.ozone.modHeaders(ids.AppBskyFeedSearchPosts) },\n      )\n\n      expect(resTop.data.posts.map((p) => p.uri)).toStrictEqual(allResults)\n      expect(resLatest.data.posts.map((p) => p.uri)).toStrictEqual(allResults)\n    })\n  })\n})\n\nconst createTag = async (\n  db: DatabaseSchema,\n  opts: {\n    uri: string\n    val: string\n  },\n) => {\n  await db\n    .updateTable('record')\n    .set({\n      tags: JSON.stringify([opts.val]),\n    })\n    .where('uri', '=', opts.uri)\n    .returningAll()\n    .execute()\n}\n\nconst deleteTags = async (\n  db: DatabaseSchema,\n  opts: {\n    uri: string\n  },\n) => {\n  await db\n    .updateTable('record')\n    .set({\n      tags: JSON.stringify([]),\n    })\n    .where('uri', '=', opts.uri)\n    .returningAll()\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/posts-debug.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('post views w/ debug field', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_posts_debug',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterEach(() => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.clear()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it(`does not include debug field for unauthed requests`, async () => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.add(sc.dids.bob)\n\n    const uris = [sc.posts[sc.dids.alice][0].ref.uriStr]\n    const posts = await agent.api.app.bsky.feed.getPosts({ uris })\n\n    const post = posts.data.posts.at(0)\n    expect(post?.debug).not.toBeDefined()\n  })\n\n  it(`includes debug field for configured user`, async () => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.add(sc.dids.bob)\n\n    const uris = [sc.posts[sc.dids.alice][0].ref.uriStr]\n    const posts = await agent.api.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n\n    const post = posts.data.posts.at(0)\n    expect(post?.debug).toBeDefined()\n    expect(typeof post?.debug).toBe('object')\n  })\n\n  it(`doesn't include debug field for other users`, async () => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.add(sc.dids.carol)\n\n    const uris = [sc.posts[sc.dids.alice][0].ref.uriStr]\n    const posts = await agent.api.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n\n    const post = posts.data.posts.at(0)\n    expect(post?.debug).not.toBeDefined()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/posts.test.ts",
    "content": "import { AppBskyFeedPost, AtpAgent, Un$Typed } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { RecordWithMedia } from '../../dist/views/types'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { RecordEmbed, VideoEmbed } from '../../src/views/types'\nimport { forSnapshot, stripViewerFromPost } from '../_util'\n\ndescribe('pds posts views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_posts',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@eve.com',\n      password: 'hunter2',\n    })\n    await sc.post(sc.dids.eve, 'post will go down')\n\n    await sc.createAccount('frankie', {\n      handle: 'frankie.test',\n      email: 'frankie@frankie.com',\n      password: 'hunter2',\n    })\n    await sc.post(sc.dids.frankie, 'account will go down')\n\n    await network.processAll()\n\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: sc.posts[sc.dids.eve][0].ref.uriStr,\n    })\n\n    await network.bsky.ctx.dataplane.takedownActor({\n      did: sc.dids.frankie,\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fetches posts', async () => {\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      sc.posts[sc.dids.carol][0].ref.uriStr,\n      sc.posts[sc.dids.dan][1].ref.uriStr,\n      sc.replies[sc.dids.alice][0].ref.uriStr,\n    ]\n    const posts = await agent.api.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n\n    expect(posts.data.posts.length).toBe(uris.length)\n    expect(forSnapshot(posts.data.posts)).toMatchSnapshot()\n  })\n\n  it(`omits not-found posts`, async () => {\n    // This is a valid post AT-URI (from a prod post), but it shouldn't exist in the test env.\n    const badPostUri =\n      'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3m5yqexldn22q'\n\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      badPostUri,\n    ]\n    const posts = await agent.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n\n    expect(posts.data.posts.length).toBe(uris.length - 1)\n    expect(posts.data.posts.map((p) => p.uri).includes(badPostUri)).toBe(false)\n  })\n\n  it(`omits taken-down posts`, async () => {\n    // Taken-down post.\n    const badPostUri = sc.posts[sc.dids.eve][0].ref.uriStr\n\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      badPostUri,\n    ]\n    const posts = await agent.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n\n    expect(posts.data.posts.length).toBe(uris.length - 1)\n    expect(posts.data.posts.map((p) => p.uri).includes(badPostUri)).toBe(false)\n  })\n\n  it(`omits posts by taken-down accounts`, async () => {\n    // Taken-down account.\n    const badPostUri = sc.posts[sc.dids.frankie][0].ref.uriStr\n\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      badPostUri,\n    ]\n    const posts = await agent.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n\n    expect(posts.data.posts.length).toBe(uris.length - 1)\n    expect(posts.data.posts.map((p) => p.uri).includes(badPostUri)).toBe(false)\n  })\n\n  it('fetches posts unauthed', async () => {\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][1].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      sc.posts[sc.dids.carol][0].ref.uriStr,\n      sc.posts[sc.dids.dan][1].ref.uriStr,\n      sc.replies[sc.dids.alice][0].ref.uriStr,\n    ]\n\n    const authed = await agent.api.app.bsky.feed.getPosts(\n      { uris },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPosts,\n        ),\n      },\n    )\n    const unauthed = await agent.api.app.bsky.feed.getPosts({\n      uris,\n    })\n    const stripped = authed.data.posts.map((p) => stripViewerFromPost(p))\n    expect(unauthed.data.posts).toEqual(stripped)\n  })\n\n  it('handles repeat uris', async () => {\n    const uris = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n    ]\n\n    const posts = await agent.api.app.bsky.feed.getPosts({ uris })\n\n    expect(posts.data.posts.length).toBe(2)\n    const receivedUris = posts.data.posts.map((p) => p.uri).sort()\n    const expected = [\n      sc.posts[sc.dids.alice][0].ref.uriStr,\n      sc.posts[sc.dids.bob][0].ref.uriStr,\n    ].sort()\n    expect(receivedUris).toEqual(expected)\n  })\n\n  it('allows for creating posts with tags', async () => {\n    const post: Un$Typed<AppBskyFeedPost.Record> = {\n      text: 'hello world',\n      tags: ['javascript', 'hehe'],\n      createdAt: new Date().toISOString(),\n    }\n\n    const { uri } = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.alice },\n      post,\n      sc.getHeaders(sc.dids.alice),\n    )\n\n    await network.processAll()\n\n    const { data } = await agent.api.app.bsky.feed.getPosts({ uris: [uri] })\n\n    expect(data.posts.length).toBe(1)\n    // @ts-ignore we know it's a post record\n    expect(data.posts[0].record.tags).toEqual(['javascript', 'hehe'])\n  })\n\n  it('embeds video.', async () => {\n    const { data: video } = await pdsAgent.api.com.atproto.repo.uploadBlob(\n      Buffer.from('notarealvideo'),\n      {\n        headers: sc.getHeaders(sc.dids.alice),\n        encoding: 'image/mp4',\n      },\n    )\n    const { uri } = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.alice },\n      {\n        text: 'video',\n        createdAt: new Date().toISOString(),\n        embed: {\n          $type: 'app.bsky.embed.video',\n          video: video.blob,\n          alt: 'alt text',\n          aspectRatio: { height: 3, width: 4 },\n        } satisfies VideoEmbed,\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    await network.processAll()\n    const { data } = await agent.app.bsky.feed.getPosts({ uris: [uri] })\n    expect(data.posts.length).toBe(1)\n    expect(forSnapshot(data.posts[0])).toMatchSnapshot()\n  })\n\n  it('embeds video with record.', async () => {\n    const { data: video } = await pdsAgent.api.com.atproto.repo.uploadBlob(\n      Buffer.from('notarealvideo'),\n      {\n        headers: sc.getHeaders(sc.dids.alice),\n        encoding: 'image/mp4',\n      },\n    )\n    const embedRecord = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.alice },\n      {\n        text: 'embedded',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    const { uri } = await pdsAgent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.alice },\n      {\n        text: 'video',\n        createdAt: new Date().toISOString(),\n        embed: {\n          $type: 'app.bsky.embed.recordWithMedia',\n          record: {\n            record: {\n              uri: embedRecord.uri,\n              cid: embedRecord.cid,\n            },\n          } satisfies RecordEmbed,\n          media: {\n            $type: 'app.bsky.embed.video',\n            video: video.blob,\n            alt: 'alt text',\n            aspectRatio: { height: 3, width: 4 },\n          } satisfies VideoEmbed,\n        } satisfies RecordWithMedia,\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    await network.processAll()\n    const { data } = await agent.app.bsky.feed.getPosts({ uris: [uri] })\n    expect(data.posts.length).toBe(1)\n    expect(forSnapshot(data.posts[0])).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/profile-debug.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\n\ndescribe('profile views w/ debug field', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_profile_debug',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n  })\n\n  afterEach(() => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.clear()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it(`does not include debug field for unauthed requests`, async () => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.add(sc.dids.bob)\n\n    const { data: profile } = await agent.api.app.bsky.actor.getProfile({\n      actor: sc.dids.alice,\n    })\n\n    expect(profile.debug).not.toBeDefined()\n  })\n\n  it(`includes debug field for configured user`, async () => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.add(sc.dids.bob)\n\n    const { data: profile } = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    expect(profile.debug).toBeDefined()\n    expect(typeof profile.debug).toBe('object')\n  })\n\n  it(`doesn't include debug field for other users`, async () => {\n    network.bsky.ctx.cfg.debugFieldAllowedDids.add(sc.dids.carol)\n\n    const { data: profile } = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.alice },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    expect(profile.debug).not.toBeDefined()\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/profile.test.ts",
    "content": "import assert from 'node:assert'\nimport fs from 'node:fs/promises'\nimport { Timestamp } from '@bufbuild/protobuf'\nimport {\n  AppBskyEmbedExternal,\n  AtpAgent,\n  ComGermnetworkDeclaration,\n} from '@atproto/api'\nimport { HOUR, MINUTE } from '@atproto/common'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { forSnapshot, stripViewer } from '../_util'\n\ndescribe('pds profile views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let dan: string\n  let eve: string\n  let frank: string\n  let noprofile: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_profile',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@test.com',\n      password: 'eve-pass',\n    })\n    await sc.createProfile(\n      sc.dids.eve,\n      'eve',\n      `It's me, eve`,\n      undefined,\n      undefined,\n      {\n        pronouns: 'They/them',\n        // Not allowing that to go through, even though is a valid URL.\n        website: 'wss://jetstream1.us-east.bsky.network',\n      },\n    )\n\n    await sc.createAccount('frank', {\n      handle: 'frank.test',\n      email: 'frank@test.com',\n      password: 'frank-pass',\n    })\n    await sc.createProfile(\n      sc.dids.frank,\n      'frank',\n      `It's me, frank`,\n      undefined,\n      undefined,\n      {\n        website: 'https://frank.example.com',\n      },\n    )\n\n    await sc.createAccount('noprofile', {\n      handle: 'noprofile.test',\n      email: 'noprofile@test.com',\n      password: 'noprofile-pass',\n    })\n\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    dan = sc.dids.dan\n    eve = sc.dids.eve\n    frank = sc.dids.frank\n    noprofile = sc.dids.noprofile\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  // @TODO(bsky) blocked by actor takedown via labels.\n\n  it('fetches own profile', async () => {\n    const aliceForAlice = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot()\n  })\n\n  it('returns empty profile if actor exists but has no profile', async () => {\n    const res = await agent.app.bsky.actor.getProfile(\n      { actor: noprofile },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('returns empty profile for actor that exists but has no profile', async () => {\n    const res = await agent.app.bsky.actor.getProfiles(\n      { actors: [bob, noprofile] },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfiles,\n        ),\n      },\n    )\n\n    expect(res.data.profiles).toHaveLength(2)\n    expect(res.data.profiles[0].did).toBe(bob)\n    expect(res.data.profiles[1].did).toBe(noprofile)\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('reflects self-labels', async () => {\n    const aliceForBob = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    const labels = aliceForBob.data.labels\n      ?.filter((label) => label.src === alice)\n      .map((label) => label.val)\n      .sort()\n\n    expect(labels).toEqual(['self-label-a', 'self-label-b'])\n  })\n\n  it(\"fetches other's profile, with a follow\", async () => {\n    const aliceForBob = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    expect(forSnapshot(aliceForBob.data)).toMatchSnapshot()\n  })\n\n  it(\"fetches other's profile, without a follow\", async () => {\n    const danForBob = await agent.api.app.bsky.actor.getProfile(\n      { actor: dan },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    expect(forSnapshot(danForBob.data)).toMatchSnapshot()\n  })\n\n  it('fetches multiple profiles', async () => {\n    const {\n      data: { profiles },\n    } = await agent.api.app.bsky.actor.getProfiles(\n      {\n        actors: [\n          alice,\n          'bob.test',\n          'did:example:missing',\n          'carol.test',\n          dan,\n          eve,\n          frank,\n          'missing.test',\n        ],\n      },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfiles),\n      },\n    )\n\n    expect(profiles.map((p) => p.handle)).toEqual([\n      'alice.test',\n      'bob.test',\n      'carol.test',\n      'dan.test',\n      'eve.test',\n      'frank.test',\n    ])\n\n    expect(forSnapshot(profiles)).toMatchSnapshot()\n  })\n\n  it('presents avatars & banners', async () => {\n    const avatarImg = await fs.readFile(\n      '../dev-env/assets/key-portrait-small.jpg',\n    )\n    const bannerImg = await fs.readFile(\n      '../dev-env/assets/key-landscape-small.jpg',\n    )\n    const avatarRes = await pdsAgent.api.com.atproto.repo.uploadBlob(\n      avatarImg,\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'image/jpeg',\n      },\n    )\n    const bannerRes = await pdsAgent.api.com.atproto.repo.uploadBlob(\n      bannerImg,\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'image/jpeg',\n      },\n    )\n\n    await updateProfile(alice, {\n      displayName: 'ali',\n      description: 'new descript',\n      avatar: avatarRes.data.blob,\n      banner: bannerRes.data.blob,\n    })\n    await network.processAll()\n\n    const aliceForAlice = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyActorGetProfile,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot()\n  })\n\n  it('fetches profile by handle', async () => {\n    const byDid = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    const byHandle = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.accounts[alice].handle },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    expect(byHandle.data).toEqual(byDid.data)\n  })\n\n  it('fetches profile unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.actor.getProfile({\n      actor: alice,\n    })\n    expect(unauthed).toEqual(stripViewer(authed))\n  })\n\n  it('fetches multiple profiles unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.actor.getProfiles(\n      {\n        actors: [alice, 'bob.test', 'missing.test'],\n      },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfiles),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.actor.getProfiles({\n      actors: [alice, 'bob.test', 'missing.test'],\n    })\n    expect(unauthed.profiles.length).toBeGreaterThan(0)\n    expect(unauthed.profiles).toEqual(authed.profiles.map(stripViewer))\n  })\n\n  it('blocked by actor takedown', async () => {\n    await network.bsky.ctx.dataplane.takedownActor({\n      did: alice,\n    })\n    const promise = agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    await expect(promise).rejects.toThrow('Account has been suspended')\n\n    // Cleanup\n    await network.bsky.ctx.dataplane.untakedownActor({\n      did: alice,\n    })\n  })\n\n  describe('status', () => {\n    const embed: AppBskyEmbedExternal.Main = {\n      $type: 'app.bsky.embed.external',\n      external: {\n        uri: 'https://example.com',\n        title: 'TestImage',\n        description: 'testLink',\n      },\n    }\n\n    it(`omits status if doesn't exist`, async () => {\n      const { data } = await agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      expect(data.status).toBeUndefined()\n    })\n\n    it('returns active status when within the duration', async () => {\n      await sc.agent.com.atproto.repo.createRecord(\n        {\n          repo: alice,\n          collection: ids.AppBskyActorStatus,\n          rkey: 'self',\n          record: {\n            status: 'app.bsky.actor.status#live',\n            embed,\n            durationMinutes: 10,\n            createdAt: new Date().toISOString(),\n          },\n        },\n        {\n          headers: sc.getHeaders(alice),\n          encoding: 'application/json',\n        },\n      )\n      await network.processAll()\n\n      const { data } = await agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      expect(forSnapshot(data.status)).toMatchSnapshot()\n    })\n\n    it('limits the minimum duration', async () => {\n      await sc.agent.com.atproto.repo.putRecord(\n        {\n          repo: alice,\n          collection: ids.AppBskyActorStatus,\n          rkey: 'self',\n          record: {\n            status: 'app.bsky.actor.status#live',\n            embed,\n            durationMinutes: 1,\n            createdAt: new Date().toISOString(),\n          },\n        },\n        {\n          headers: sc.getHeaders(alice),\n          encoding: 'application/json',\n        },\n      )\n      await network.processAll()\n\n      const { data } = await agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n\n      assert(data.status)\n      const createdAt = new Date(data.status.record.createdAt as string)\n      const expiresAt = new Date(data.status.expiresAt as string)\n      expect(expiresAt.getTime() - createdAt.getTime()).toBe(5 * MINUTE)\n    })\n\n    it('limits the maximum duration', async () => {\n      await sc.agent.com.atproto.repo.putRecord(\n        {\n          repo: alice,\n          collection: ids.AppBskyActorStatus,\n          rkey: 'self',\n          record: {\n            status: 'app.bsky.actor.status#live',\n            embed,\n            durationMinutes: 1_440, // 1 day in minutes\n            createdAt: new Date().toISOString(),\n          },\n        },\n        {\n          headers: sc.getHeaders(alice),\n          encoding: 'application/json',\n        },\n      )\n      await network.processAll()\n\n      const { data } = await agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n\n      assert(data.status)\n      const createdAt = new Date(data.status.record.createdAt as string)\n      const expiresAt = new Date(data.status.expiresAt as string)\n      expect(expiresAt.getTime() - createdAt.getTime()).toBe(4 * HOUR)\n    })\n\n    describe('when outside the duration', () => {\n      const now = '2021-01-01T01:00:00.000Z'\n      const nowPlus15M = '2021-01-01T01:15:00.000Z'\n\n      beforeAll(() => {\n        jest.useFakeTimers({\n          doNotFake: [\n            'nextTick',\n            'performance',\n            'setImmediate',\n            'setInterval',\n            'setTimeout',\n          ],\n        })\n        jest.setSystemTime(new Date(now))\n      })\n\n      afterAll(async () => {\n        jest.useRealTimers()\n      })\n\n      it('returns inactive status', async () => {\n        await sc.agent.com.atproto.repo.putRecord(\n          {\n            repo: alice,\n            collection: ids.AppBskyActorStatus,\n            rkey: 'self',\n            record: {\n              status: 'app.bsky.actor.status#live',\n              embed,\n              durationMinutes: 10,\n              createdAt: new Date().toISOString(),\n            },\n          },\n          {\n            headers: sc.getHeaders(alice),\n            encoding: 'application/json',\n          },\n        )\n        await network.processAll()\n\n        jest.setSystemTime(new Date(nowPlus15M))\n\n        const { data } = await agent.api.app.bsky.actor.getProfile(\n          { actor: alice },\n          {\n            headers: await network.serviceHeaders(\n              alice,\n              ids.AppBskyActorGetProfile,\n            ),\n          },\n        )\n\n        // Doesn't need `forSnapshot` because the dates are already mocked.\n        expect(forSnapshot(data.status)).toMatchSnapshot()\n      })\n    })\n\n    describe('when taken down', () => {\n      beforeAll(async () => {\n        const res = await sc.agent.com.atproto.repo.putRecord(\n          {\n            repo: alice,\n            collection: ids.AppBskyActorStatus,\n            rkey: 'self',\n            record: {\n              status: 'app.bsky.actor.status#live',\n              embed,\n              durationMinutes: 10,\n              createdAt: new Date().toISOString(),\n            },\n          },\n          {\n            headers: sc.getHeaders(alice),\n            encoding: 'application/json',\n          },\n        )\n        await network.processAll()\n\n        await network.bsky.ctx.dataplane.takedownRecord({\n          recordUri: res.data.uri,\n        })\n        await network.processAll()\n      })\n\n      it('it returns the live status with isDisabled=true for status owner', async () => {\n        const { data } = await agent.api.app.bsky.actor.getProfile(\n          { actor: alice },\n          {\n            headers: await network.serviceHeaders(\n              alice,\n              ids.AppBskyActorGetProfile,\n            ),\n          },\n        )\n\n        expect(data.status?.isDisabled).toBe(true)\n        expect(forSnapshot(data.status)).toMatchSnapshot()\n      })\n\n      it('it does not return the live status for non-owner', async () => {\n        const { data } = await agent.api.app.bsky.actor.getProfile(\n          { actor: alice },\n          {\n            headers: await network.serviceHeaders(\n              bob,\n              ids.AppBskyActorGetProfile,\n            ),\n          },\n        )\n\n        expect(forSnapshot(data.status)).toBeUndefined()\n      })\n    })\n  })\n\n  describe('germ', () => {\n    const germDeclaration: ComGermnetworkDeclaration.Main = {\n      $type: ids.ComGermnetworkDeclaration,\n      version: '0.1.0',\n      currentKey: new Uint8Array([0o01, 0o02, 0o03]),\n      messageMe: {\n        messageMeUrl: 'https://chat.example.com/start-conversation',\n        showButtonTo: 'everyone',\n      },\n    }\n\n    it(`omits germ record if doesn't exist`, async () => {\n      const { data } = await agent.api.app.bsky.actor.getProfile(\n        { actor: alice },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      expect(data.associated?.germ).toBeUndefined()\n    })\n\n    it('returns germ record if it does exist', async () => {\n      await sc.agent.com.atproto.repo.createRecord(\n        {\n          repo: bob,\n          collection: ids.ComGermnetworkDeclaration,\n          rkey: 'self',\n          record: germDeclaration,\n        },\n        {\n          headers: sc.getHeaders(bob),\n          encoding: 'application/json',\n        },\n      )\n      await network.processAll()\n\n      const { data } = await agent.api.app.bsky.actor.getProfile(\n        { actor: bob },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyActorGetProfile,\n          ),\n        },\n      )\n      expect(data.associated?.germ?.showButtonTo).toEqual('everyone')\n      expect(forSnapshot(data.associated?.germ)).toMatchSnapshot()\n    })\n  })\n\n  it('filters out Go zero-value dates from dataplane', async () => {\n    // Spy on the dataplane getActors method\n    const getActorsSpy = jest.spyOn(network.bsky.ctx.dataplane, 'getActors')\n\n    // Call the original implementation but modify the result\n    getActorsSpy.mockImplementationOnce(async (req) => {\n      // Call the real method\n      const result = await network.bsky.ctx.dataplane.getActors(req)\n\n      // Modify the result to inject a Go zero-value date\n      if (result.actors.length > 0 && result.actors[0]) {\n        const actor = result.actors[0]\n        // Create a Timestamp with Go zero-value (0001-01-01 00:00:00 UTC)\n        const goZeroDate = new Date(-62135596800000)\n        actor.createdAt = Timestamp.fromDate(goZeroDate)\n      }\n\n      return result\n    })\n\n    const { data } = await agent.app.bsky.actor.getProfile(\n      { actor: alice },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyActorGetProfile),\n      },\n    )\n\n    // The createdAt should be undefined because the hydration layer filters it out\n    expect(data.createdAt).toBeUndefined()\n\n    // Clean up\n    getActorsSpy.mockRestore()\n  })\n\n  async function updateProfile(did: string, record: Record<string, unknown>) {\n    return await pdsAgent.api.com.atproto.repo.putRecord(\n      {\n        repo: did,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        record,\n      },\n      { headers: sc.getHeaders(did), encoding: 'application/json' },\n    )\n  }\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/quotes.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { SeedClient, TestNetwork, quotesSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { forSnapshot } from '../_util'\n\ndescribe('pds quote views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let eve: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_quotes',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await quotesSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    eve = sc.dids.eve\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fetches post quotes', async () => {\n    const alicePostQuotes = await agent.api.app.bsky.feed.getQuotes(\n      { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },\n      { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) },\n    )\n\n    expect(alicePostQuotes.data.posts.length).toBe(2)\n    expect(forSnapshot(alicePostQuotes.data)).toMatchSnapshot()\n  })\n\n  it('does not return post in list when the quote author has a block', async () => {\n    await sc.block(eve, carol)\n    await network.processAll()\n\n    const quotes = await agent.api.app.bsky.feed.getQuotes(\n      { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },\n      {\n        headers: await network.serviceHeaders(carol, ids.AppBskyFeedGetQuotes),\n      },\n    )\n\n    expect(quotes.data.posts.length).toBe(0)\n    await sc.unblock(eve, carol)\n  })\n\n  it('utilizes limit parameter and cursor', async () => {\n    const alicePostQuotes1 = await agent.api.app.bsky.feed.getQuotes(\n      { uri: sc.posts[alice][1].ref.uriStr, limit: 3 },\n      { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) },\n    )\n\n    expect(alicePostQuotes1.data.posts.length).toBe(3)\n    expect(alicePostQuotes1.data.cursor).toBeDefined()\n\n    const alicePostQuotes2 = await agent.api.app.bsky.feed.getQuotes(\n      {\n        uri: sc.posts[alice][1].ref.uriStr,\n        limit: 3,\n        cursor: alicePostQuotes1.data.cursor,\n      },\n      { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) },\n    )\n\n    expect(alicePostQuotes2.data.posts.length).toBe(2)\n  })\n\n  it('does not return post when quote is deleted', async () => {\n    await sc.deletePost(eve, sc.posts[eve][0].ref.uri)\n    await network.processAll()\n\n    const alicePostQuotes = await agent.api.app.bsky.feed.getQuotes(\n      { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetQuotes),\n      },\n    )\n\n    expect(alicePostQuotes.data.posts.length).toBe(1)\n    expect(forSnapshot(alicePostQuotes.data)).toMatchSnapshot()\n  })\n\n  it('does not return any quotes when the quoted post is deleted', async () => {\n    await sc.deletePost(alice, sc.posts[alice][0].ref.uri)\n    await network.processAll()\n\n    const alicePostQuotesAfter = await agent.api.app.bsky.feed.getQuotes(\n      { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },\n      {\n        headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetQuotes),\n      },\n    )\n\n    expect(alicePostQuotesAfter.data.posts.length).toBe(0)\n  })\n\n  it('decrements quote count when a quote is deleted', async () => {\n    await sc.deletePost(eve, sc.posts[eve][2].ref.uri)\n    await network.processAll()\n\n    const bobPost = await agent.api.app.bsky.feed.getPosts(\n      { uris: [sc.replies[bob][0].ref.uriStr] },\n      { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetPosts) },\n    )\n\n    expect(bobPost.data.posts[0].quoteCount).toEqual(0)\n    expect(forSnapshot(bobPost.data)).toMatchSnapshot()\n  })\n\n  it('does not return post in list when the embed is blocked', async () => {\n    await sc.block(carol, eve)\n    await network.processAll()\n\n    const quotes = await agent.api.app.bsky.feed.getQuotes(\n      { uri: sc.posts[carol][1].ref.uriStr },\n      { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetQuotes) },\n    )\n\n    expect(quotes.data.posts.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/reposts.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, repostsSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { OutputSchema as GetRepostedByOutputSchema } from '../../src/lexicon/types/app/bsky/feed/getRepostedBy'\nimport { forSnapshot, paginateAll, stripViewer } from '../_util'\n\ndescribe('pds repost views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_reposts',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await repostsSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fetches reposted-by for a post', async () => {\n    const view = await agent.api.app.bsky.feed.getRepostedBy(\n      { uri: sc.posts[alice][2].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetRepostedBy,\n        ),\n      },\n    )\n    expect(view.data.uri).toEqual(sc.posts[sc.dids.alice][2].ref.uriStr)\n    expect(forSnapshot(view.data.repostedBy)).toMatchSnapshot()\n  })\n\n  it('fetches reposted-by for a reply', async () => {\n    const view = await agent.api.app.bsky.feed.getRepostedBy(\n      { uri: sc.replies[bob][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetRepostedBy,\n        ),\n      },\n    )\n    expect(view.data.uri).toEqual(sc.replies[sc.dids.bob][0].ref.uriStr)\n    expect(forSnapshot(view.data.repostedBy)).toMatchSnapshot()\n  })\n\n  it('paginates', async () => {\n    const results = (results: GetRepostedByOutputSchema[]) =>\n      results.flatMap((res) => res.repostedBy)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.feed.getRepostedBy(\n        {\n          uri: sc.posts[alice][2].ref.uriStr,\n          cursor,\n          limit: 2,\n        },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetRepostedBy,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.repostedBy.length).toBeLessThanOrEqual(2),\n    )\n\n    const full = await agent.api.app.bsky.feed.getRepostedBy(\n      { uri: sc.posts[alice][2].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetRepostedBy,\n        ),\n      },\n    )\n\n    expect(full.data.repostedBy.length).toEqual(4)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('fetches reposted-by unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.feed.getRepostedBy(\n      { uri: sc.posts[alice][2].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetRepostedBy,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.feed.getRepostedBy({\n      uri: sc.posts[alice][2].ref.uriStr,\n    })\n    expect(unauthed.repostedBy.length).toBeGreaterThan(0)\n    expect(unauthed.repostedBy).toEqual(authed.repostedBy.map(stripViewer))\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/starter-packs.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent, asPredicate } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { validateRecord as validateProfileRecord } from '../../src/lexicon/types/app/bsky/actor/profile'\nimport {\n  OutputSchema as GetStarterPacksWithMembershipOutputSchema,\n  StarterPackWithMembership,\n} from '../../src/lexicon/types/app/bsky/graph/getStarterPacksWithMembership'\nimport { forSnapshot, paginateAll } from '../_util'\n\nconst isValidProfile = asPredicate(validateProfileRecord)\n\ndescribe('starter packs', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let sp1: RecordRef\n  let sp2: RecordRef\n  let sp3: RecordRef\n  let sp4: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_starter_packs',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    const feedgen = await sc.createFeedGen(\n      sc.dids.alice,\n      'did:web:example.com',\n      \"alice's feedgen\",\n    )\n    sp1 = await sc.createStarterPack(\n      sc.dids.alice,\n      \"alice's starter pack\",\n      [sc.dids.bob, sc.dids.carol, sc.dids.dan],\n      [feedgen.uriStr],\n    )\n    sp2 = await sc.createStarterPack(\n      sc.dids.alice,\n      \"alice's empty starter pack\",\n      [],\n      [],\n    )\n    for (const n of [1, 2, 3]) {\n      const { did } = await sc.createAccount(`newskie${n}`, {\n        handle: `newskie${n}.test`,\n        email: `newskie${n}@test.com`,\n        password: `newskie${n}-pass`,\n      })\n      await sc.createProfile(did, `Newskie ${n}`, 'New here', [], sp1)\n    }\n\n    await sc.createAccount('frankie', {\n      handle: 'frankie.test',\n      email: 'frankie@frankie.com',\n      password: 'password',\n    })\n    await sc.createAccount('greta', {\n      handle: 'greta.test',\n      email: 'greta@greta.com',\n      password: 'password',\n    })\n    sp3 = await sc.createStarterPack(\n      sc.dids.alice,\n      \"alice's about to get blocked starter pack\",\n      [sc.dids.alice, sc.dids.frankie, sc.dids.greta],\n      [],\n    )\n    await sc.block(sc.dids.frankie, sc.dids.alice)\n\n    sp4 = await sc.createStarterPack(\n      sc.dids.bob,\n      \"bob's starter pack in case you block alice\",\n      [sc.dids.alice, sc.dids.frankie],\n      [],\n    )\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('gets actor starter packs.', async () => {\n    const { data } = await agent.api.app.bsky.graph.getActorStarterPacks({\n      actor: sc.dids.alice,\n    })\n    expect(data.starterPacks).toHaveLength(3)\n    expect(forSnapshot(data.starterPacks)).toMatchSnapshot()\n  })\n\n  it('gets starter pack used on profile detail', async () => {\n    const { data: profile } = await agent.api.app.bsky.actor.getProfile({\n      actor: sc.dids.newskie1,\n    })\n    expect(forSnapshot(profile.joinedViaStarterPack)).toMatchSnapshot()\n  })\n\n  it('gets starter pack details', async () => {\n    const {\n      data: { starterPack },\n    } = await agent.api.app.bsky.graph.getStarterPack({\n      // resolve w/ handle in uri\n      starterPack: sp1.uriStr,\n    })\n    expect(forSnapshot(starterPack)).toMatchSnapshot()\n  })\n\n  it('gets starter pack details with handle in uri', async () => {\n    const {\n      data: { starterPack },\n    } = await agent.api.app.bsky.graph.getStarterPack({\n      // resolve w/ handle in uri\n      starterPack: sp1.uriStr.replace(\n        sc.dids.alice,\n        sc.accounts[sc.dids.alice].handle,\n      ),\n    })\n    expect(starterPack.uri).toBe(sp1.uriStr)\n  })\n\n  it('gets starter pack details', async () => {\n    const {\n      data: { starterPacks },\n    } = await agent.api.app.bsky.graph.getStarterPacks({\n      uris: [sp2.uriStr, sp1.uriStr],\n    })\n    expect(forSnapshot(starterPacks)).toMatchSnapshot()\n  })\n\n  it('generates notifications', async () => {\n    const {\n      data: { notifications },\n    } = await agent.api.app.bsky.notification.listNotifications(\n      { limit: 3 }, // three most recent\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    expect(notifications).toHaveLength(3)\n    notifications.forEach((notif) => {\n      expect(notif.reason).toBe('starterpack-joined')\n      expect(notif.reasonSubject).toBe(sp1.uriStr)\n      expect(notif.uri).toMatch(/\\/app\\.bsky\\.actor\\.profile\\/self$/)\n      assert(isValidProfile(notif.record), 'record is not profile')\n      expect(notif.record.joinedViaStarterPack?.uri).toBe(sp1.uriStr)\n    })\n    expect(forSnapshot(notifications)).toMatchSnapshot()\n  })\n\n  it('does not include users with creator block relationship in list sample for non-creator, in-list viewers', async () => {\n    const view = await agent.api.app.bsky.graph.getStarterPack(\n      {\n        starterPack: sp3.uriStr,\n      },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.frankie,\n          ids.AppBskyGraphGetStarterPack,\n        ),\n      },\n    )\n    expect(view.data.starterPack.listItemsSample?.length).toBe(2)\n    expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot()\n  })\n\n  it('does not include users with creator block relationship in list sample for non-creator, not-in-list viewers', async () => {\n    const view = await agent.api.app.bsky.graph.getStarterPack(\n      {\n        starterPack: sp3.uriStr,\n      },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyGraphGetStarterPack,\n        ),\n      },\n    )\n    expect(view.data.starterPack.listItemsSample?.length).toBe(2)\n    expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot()\n  })\n\n  it('does not include users with creator block relationship in list sample for signed-out viewers', async () => {\n    const view = await agent.api.app.bsky.graph.getStarterPack({\n      starterPack: sp3.uriStr,\n    })\n    expect(view.data.starterPack.listItemsSample?.length).toBe(2)\n    expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot()\n  })\n\n  it('does include users with creator block relationship in list sample for creator', async () => {\n    const view = await agent.api.app.bsky.graph.getStarterPack(\n      { starterPack: sp3.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyGraphGetStarterPack,\n        ),\n      },\n    )\n    expect(view.data.starterPack.listItemsSample?.length).toBe(3)\n    expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot()\n  })\n\n  describe('searchStarterPacks', () => {\n    it('searches starter packs and returns paginated', async () => {\n      const { data: page0 } = await agent.app.bsky.graph.searchStarterPacks({\n        q: 'starter',\n        limit: 3,\n      })\n\n      expect(page0.starterPacks).toMatchObject([\n        expect.objectContaining({ uri: sp4.uriStr }),\n        expect.objectContaining({ uri: sp3.uriStr }),\n        expect.objectContaining({ uri: sp2.uriStr }),\n      ])\n\n      const { data: page1 } = await agent.api.app.bsky.graph.searchStarterPacks(\n        {\n          q: 'starter',\n          limit: 3,\n          cursor: page0.cursor,\n        },\n      )\n\n      expect(page1.starterPacks).toMatchObject([\n        expect.objectContaining({ uri: sp1.uriStr }),\n      ])\n    })\n\n    it('filters by the search term', async () => {\n      const { data } = await agent.app.bsky.graph.searchStarterPacks({\n        q: 'In CaSe',\n        limit: 3,\n      })\n\n      expect(data.starterPacks).toMatchObject([\n        expect.objectContaining({ uri: sp4.uriStr }),\n      ])\n    })\n\n    it('does not include starter packs with creator block relationship for non-creator viewers', async () => {\n      const { data } = await agent.app.bsky.graph.searchStarterPacks(\n        { q: 'starter', limit: 3 },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.frankie,\n            ids.AppBskyGraphSearchStarterPacks,\n          ),\n        },\n      )\n\n      expect(data.starterPacks).toMatchObject([\n        expect.objectContaining({ uri: sp4.uriStr }),\n      ])\n    })\n  })\n\n  describe('starter pack membership', () => {\n    const membershipsUris = (lwms: StarterPackWithMembership[]): string[] =>\n      lwms\n        .map((spwm) => spwm.listItem?.uri)\n        .filter((li): li is string => typeof li === 'string')\n\n    it('returns all SPs by the user', async () => {\n      const view = await agent.app.bsky.graph.getStarterPacksWithMembership(\n        { actor: sc.dids.bob },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyGraphGetStarterPacksWithMembership,\n          ),\n        },\n      )\n      expect(view.data.starterPacksWithMembership.length).toBe(3)\n    })\n\n    it('finds self membership', async () => {\n      const view = await agent.app.bsky.graph.getStarterPacksWithMembership(\n        { actor: sc.dids.alice },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyGraphGetStarterPacksWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.starterPacksWithMembership.length).toBe(3)\n      const memberships = membershipsUris(view.data.starterPacksWithMembership)\n      expect(memberships.length).toBe(1)\n    })\n\n    it(`finds other user's membership`, async () => {\n      const view = await agent.app.bsky.graph.getStarterPacksWithMembership(\n        { actor: sc.dids.bob },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.alice,\n            ids.AppBskyGraphGetStarterPacksWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.starterPacksWithMembership.length).toBe(3)\n      const memberships = membershipsUris(view.data.starterPacksWithMembership)\n      expect(memberships.length).toBe(1)\n    })\n\n    it('finds that user has no memberships', async () => {\n      // @NOTE: dan is not in bob's SP.\n      const view = await agent.app.bsky.graph.getStarterPacksWithMembership(\n        { actor: sc.dids.dan },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.bob,\n            ids.AppBskyGraphGetStarterPacksWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.starterPacksWithMembership.length).toBe(1)\n      const memberships = membershipsUris(view.data.starterPacksWithMembership)\n      expect(memberships.length).toBe(0)\n    })\n\n    it('finds empty list of SPs if user has none', async () => {\n      const view = await agent.app.bsky.graph.getStarterPacksWithMembership(\n        { actor: sc.dids.bob },\n        {\n          headers: await network.serviceHeaders(\n            sc.dids.carol,\n            ids.AppBskyGraphGetStarterPacksWithMembership,\n          ),\n        },\n      )\n\n      expect(view.data.starterPacksWithMembership.length).toBe(0)\n    })\n\n    it('paginates SPs with memberships', async () => {\n      const viewer = sc.dids.alice\n      const actor = sc.dids.bob\n\n      const results = (out: GetStarterPacksWithMembershipOutputSchema[]) =>\n        out.flatMap((res) => res.starterPacksWithMembership)\n      const paginator = async (cursor?: string) => {\n        const res = await agent.app.bsky.graph.getStarterPacksWithMembership(\n          { actor, limit: 2, cursor },\n          {\n            headers: await network.serviceHeaders(\n              viewer,\n              ids.AppBskyGraphGetStarterPacksWithMembership,\n            ),\n          },\n        )\n        return res.data\n      }\n\n      const paginatedAll = await paginateAll(paginator)\n      paginatedAll.forEach((res) =>\n        expect(res.starterPacksWithMembership.length).toBeLessThanOrEqual(2),\n      )\n\n      const full = await agent.app.bsky.graph.getStarterPacksWithMembership(\n        { actor },\n        {\n          headers: await network.serviceHeaders(\n            viewer,\n            ids.AppBskyGraphGetStarterPacksWithMembership,\n          ),\n        },\n      )\n      expect(full.data.starterPacksWithMembership.length).toBe(3)\n\n      const sortedFull = results([full.data]).sort((a, b) =>\n        a.starterPack.uri > b.starterPack.uri ? 1 : -1,\n      )\n      const sortedPaginated = results(paginatedAll).sort((a, b) =>\n        a.starterPack.uri > b.starterPack.uri ? 1 : -1,\n      )\n      expect(sortedPaginated).toEqual(sortedFull)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/suggestions.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { stripViewer } from '../_util'\n\ndescribe('pds user search views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_suggestions',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    const suggestions = [\n      { did: sc.dids.alice, order: 1 },\n      { did: sc.dids.bob, order: 2 },\n      { did: sc.dids.carol, order: 3 },\n      { did: sc.dids.dan, order: 4 },\n    ]\n\n    await network.bsky.db.db\n      .insertInto('suggested_follow')\n      .values(suggestions)\n      .execute()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('actor suggestion gives users', async () => {\n    const result = await agent.api.app.bsky.actor.getSuggestions(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n\n    // does not include carol, because she is requesting\n    expect(result.data.actors.length).toBe(2)\n    expect(result.data.actors[0].handle).toEqual('bob.test')\n    expect(result.data.actors[0].displayName).toEqual('bobby')\n    expect(result.data.actors[1].handle).toEqual('dan.test')\n    expect(result.data.actors[1].displayName).toBeUndefined()\n  })\n\n  it('does not suggest followed users', async () => {\n    const result = await agent.api.app.bsky.actor.getSuggestions(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n\n    // alice follows everyone\n    expect(result.data.actors.length).toBe(0)\n  })\n\n  it('paginates', async () => {\n    const result1 = await agent.api.app.bsky.actor.getSuggestions(\n      { limit: 2 },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(result1.data.actors.length).toBe(1)\n    expect(result1.data.actors[0].handle).toEqual('bob.test')\n\n    const result2 = await agent.api.app.bsky.actor.getSuggestions(\n      { limit: 2, cursor: result1.data.cursor },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(result2.data.actors.length).toBe(1)\n    expect(result2.data.actors[0].handle).toEqual('dan.test')\n\n    const result3 = await agent.api.app.bsky.actor.getSuggestions(\n      { limit: 2, cursor: result2.data.cursor },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    expect(result3.data.actors.length).toBe(0)\n    expect(result3.data.cursor).toBeUndefined()\n  })\n\n  it('fetches suggestions unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.actor.getSuggestions(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyActorGetSuggestions,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.actor.getSuggestions({})\n    const omitViewerFollows = ({ did }) => {\n      return did !== sc.dids.carol && !sc.follows[sc.dids.carol][did]\n    }\n    expect(unauthed.actors.length).toBeGreaterThan(0)\n    expect(unauthed.actors.filter(omitViewerFollows)).toEqual(\n      authed.actors.map(stripViewer),\n    )\n  })\n\n  it('returns tagged suggestions', async () => {\n    const suggestions = [\n      {\n        tag: 'test',\n        subject: 'did:example:test',\n        subjectType: 'actor',\n      },\n      {\n        tag: 'another',\n        subject: 'at://did:example:another/app.bsky.feed.generator/my-feed',\n        subjectType: 'feed',\n      },\n    ]\n    await network.bsky.db.db\n      .insertInto('tagged_suggestion')\n      .values(suggestions)\n      .execute()\n    const res = await agent.api.app.bsky.unspecced.getTaggedSuggestions()\n    expect(res.data.suggestions).toEqual(suggestions)\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/thread-v2.test.ts",
    "content": "import assert from 'node:assert'\nimport { AppBskyUnspeccedDefs, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, seedThreadV2 } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { ThreadItemPost } from '../../src/lexicon/types/app/bsky/unspecced/defs'\nimport { OutputSchema as OutputSchemaHiddenThread } from '../../src/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2'\nimport {\n  OutputSchema as OutputSchemaThread,\n  QueryParams as QueryParamsThread,\n} from '../../src/lexicon/types/app/bsky/unspecced/getPostThreadV2'\nimport {\n  ThreadItemValuePost,\n  ThreadOtherItemValuePost,\n} from '../../src/views/threads-v2'\nimport { forSnapshot } from '../_util'\n\ntype PostProps = Pick<ThreadItemPost, 'moreReplies' | 'opThread'>\nconst props = (overrides: Partial<PostProps> = {}): PostProps => ({\n  moreReplies: 0,\n  opThread: false,\n  ...overrides,\n})\n\ntype PostPropsHidden = Pick<\n  ThreadItemPost,\n  'hiddenByThreadgate' | 'mutedByViewer'\n>\nconst propsHidden = (\n  overrides: Partial<PostPropsHidden> = {},\n): PostPropsHidden => ({\n  hiddenByThreadgate: false,\n  mutedByViewer: false,\n  ...overrides,\n})\n\ndescribe('appview thread views v2', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let labelerDid: string\n  let sc: SeedClient<TestNetwork>\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      bsky: {\n        maxThreadParents: 15,\n        threadTagsBumpDown: new Set([seedThreadV2.TAG_BUMP_DOWN]),\n        threadTagsHide: new Set([seedThreadV2.TAG_HIDE]),\n      },\n      dbPostgresSchema: 'bsky_views_thread_v_two',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    labelerDid = network.bsky.ctx.cfg.modServiceDid\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('not found anchor', () => {\n    it('returns not found error', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2({\n        anchor: 'at://did:plc:123/app.bsky.feed.post/456',\n      })\n      const { thread: t } = data\n\n      expect(t).toEqual([\n        expect.objectContaining({\n          depth: 0,\n          value: {\n            $type: 'app.bsky.unspecced.defs#threadItemNotFound',\n          },\n        }),\n      ])\n    })\n  })\n\n  describe('simple thread', () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.simple>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.simple(sc)\n      await network.processAll()\n    })\n\n    it('returns thread anchored on root', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.root.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: 0, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: 1, uri: seed.r['0'].ref.uriStr }),\n        expect.objectContaining({ depth: 2, uri: seed.r['0.0'].ref.uriStr }),\n        expect.objectContaining({ depth: 1, uri: seed.r['1'].ref.uriStr }),\n        expect.objectContaining({ depth: 1, uri: seed.r['2'].ref.uriStr }),\n        expect.objectContaining({ depth: 2, uri: seed.r['2.0'].ref.uriStr }),\n        expect.objectContaining({ depth: 1, uri: seed.r['3'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n\n    it('returns thread anchored on r 0', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.r['0'].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: -1, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: 0, uri: seed.r['0'].ref.uriStr }),\n        expect.objectContaining({ depth: 1, uri: seed.r['0.0'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n\n    it('returns thread anchored on r 0.0', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.r['0.0'].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: -2, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: -1, uri: seed.r['0'].ref.uriStr }),\n        expect.objectContaining({ depth: 0, uri: seed.r['0.0'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n\n    it('returns thread anchored on 1', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.r['1'].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: -1, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: 0, uri: seed.r['1'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n\n    it('returns thread anchored on 2', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.r['2'].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: -1, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: 0, uri: seed.r['2'].ref.uriStr }),\n        expect.objectContaining({ depth: 1, uri: seed.r['2.0'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n\n    it('returns thread anchored on 2.0', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.r['2.0'].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: -2, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: -1, uri: seed.r['2'].ref.uriStr }),\n        expect.objectContaining({ depth: 0, uri: seed.r['2.0'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n\n    it('returns thread anchored on 3', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.r['3'].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({ depth: -1, uri: seed.root.ref.uriStr }),\n        expect.objectContaining({ depth: 0, uri: seed.r['3'].ref.uriStr }),\n      ])\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n  })\n\n  describe('long thread', () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.long>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.long(sc)\n      await network.processAll()\n    })\n\n    describe('calculating depth', () => {\n      type Case = {\n        postKey: string\n      }\n\n      const cases: Case[] = [\n        { postKey: 'root' },\n        { postKey: '0' },\n        { postKey: '0.0' },\n        { postKey: '0.0.0' },\n        { postKey: '0.0.0.0' },\n        { postKey: '0.0.0.0.0' },\n        { postKey: '0.0.1' },\n        { postKey: '1' },\n        { postKey: '2' },\n        { postKey: '3' },\n        { postKey: '4' },\n        { postKey: '4.0' },\n        { postKey: '4.0.0' },\n        { postKey: '4.0.0.0' },\n        { postKey: '4.0.0.0.0' },\n        { postKey: '5' },\n        { postKey: '6' },\n        { postKey: '7' },\n      ]\n\n      it.each(cases)(\n        'calculates the depths anchored at $postKey',\n        async ({ postKey }) => {\n          const post = postKey === 'root' ? seed.root : seed.r[postKey]\n          const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n            { anchor: post.ref.uriStr },\n            {\n              headers: await network.serviceHeaders(\n                seed.users.op.did,\n                ids.AppBskyUnspeccedGetPostThreadV2,\n              ),\n            },\n          )\n          const { thread: t, hasOtherReplies } = data\n\n          assertPosts(t)\n          expect(hasOtherReplies).toBe(false)\n          const anchorIndex = t.findIndex((i) => i.uri === post.ref.uriStr)\n          const anchorPost = t[anchorIndex]\n\n          const parents = t.slice(0, anchorIndex)\n          const children = t.slice(anchorIndex + 1, t.length)\n\n          parents.forEach((parent) => {\n            expect(parent.depth).toBeLessThan(0)\n          })\n          expect(anchorPost.depth).toEqual(0)\n          children.forEach((child) => {\n            expect(child.depth).toBeGreaterThan(0)\n          })\n        },\n      )\n    })\n  })\n\n  describe('deep thread', () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.deep>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.deep(sc)\n      await network.processAll()\n    })\n\n    describe('above', () => {\n      it('returns the ancestors above if true (default)', async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          {\n            anchor: seed.r['0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0'].ref.uriStr,\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toHaveLength(16) // anchor + 15 ancestors, as limited by `maxThreadParents`.\n\n        const first = t.at(0)\n        expect(first!.uri).toBe(seed.r['0.0.0'].ref.uriStr)\n        expect(first!.value.moreParents).toBe(true)\n\n        const second = t.at(1)\n        expect(second!.uri).toBe(seed.r['0.0.0.0'].ref.uriStr)\n        expect(second!.value.moreParents).toBe(false)\n\n        const last = t.at(-1)\n        expect(last!.uri).toBe(\n          seed.r['0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0'].ref.uriStr,\n        )\n        expect(last!.value.moreParents).toBe(false)\n      })\n\n      it(`does not return ancestors if false`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          {\n            anchor: seed.r['0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0'].ref.uriStr,\n            above: false,\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toHaveLength(1)\n\n        const first = t.at(0)\n        expect(first!.uri).toBe(\n          seed.r['0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0'].ref.uriStr,\n        )\n      })\n    })\n\n    describe('below', () => {\n      it('limits to the below count', async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          {\n            anchor: seed.root.ref.uriStr,\n            below: 10,\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toHaveLength(11)\n        const first = t.at(0)\n        expect(first!.uri).toBe(seed.root.ref.uriStr)\n      })\n\n      it(`does not fulfill the below count if there are not enough items in the thread`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          {\n            anchor: seed.r['0.0.0.0.0.0.0.0.0.0.0.0.0.0.0'].ref.uriStr,\n            above: false,\n            below: 10,\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toHaveLength(4)\n\n        const first = t.at(0)\n        expect(first!.uri).toBe(\n          seed.r['0.0.0.0.0.0.0.0.0.0.0.0.0.0.0'].ref.uriStr,\n        )\n      })\n    })\n  })\n\n  describe('branching factor', () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.branchingFactor>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.branchingFactor(sc)\n      await network.processAll()\n    })\n\n    type Case =\n      | {\n          branchingFactor: number\n          sort: QueryParamsThread['sort']\n          postKeys: string[]\n        }\n      | {\n          branchingFactor: number\n          sort: QueryParamsThread['sort']\n          // For higher branching factors it gets too verbose to write all posts.\n          length: number\n        }\n    const cases: Case[] = [\n      {\n        branchingFactor: 1,\n        sort: 'oldest',\n        postKeys: [\n          'root',\n          '0',\n          '0.0',\n          '0.0.0',\n          '1',\n          '1.0',\n          '1.0.0',\n          '2',\n          '2.0',\n          '2.0.0',\n          '3',\n          '3.0',\n          '3.0.0',\n        ],\n      },\n      {\n        branchingFactor: 1,\n        sort: 'newest',\n        postKeys: [\n          'root',\n          '3',\n          '3.3',\n          '3.3.3',\n          '2',\n          '2.3',\n          '2.3.3',\n          '1',\n          '1.3',\n          '1.3.3',\n          '0',\n          '0.3',\n          '0.3.3',\n        ],\n      },\n      {\n        branchingFactor: 2,\n        sort: 'oldest',\n        postKeys: [\n          'root',\n          '0',\n          '0.0',\n          '0.0.0',\n          '0.0.1',\n          '0.1',\n          '0.1.0',\n          '0.1.1',\n          '1',\n          '1.0',\n          '1.0.0',\n          '1.1',\n          '1.1.0',\n          '1.1.1',\n          '2',\n          '2.0',\n          '2.0.0',\n          '2.0.1',\n          '2.1',\n          '2.1.0',\n          '2.1.1',\n          '3',\n          '3.0',\n          '3.0.0',\n          '3.0.1',\n          '3.1',\n          '3.1.0',\n          '3.1.1',\n        ],\n      },\n      {\n        branchingFactor: 2,\n        sort: 'newest',\n        postKeys: [\n          'root',\n          '3',\n          '3.3',\n          '3.3.3',\n          '3.3.2',\n          '3.2',\n          '3.2.3',\n          '3.2.2',\n          '2',\n          '2.3',\n          '2.3.3',\n          '2.3.2',\n          '2.2',\n          '2.2.3',\n          '2.2.2',\n          '1',\n          '1.3',\n          '1.3.3',\n          '1.3.2',\n          '1.2',\n          '1.2.3',\n          '1.2.2',\n          '0',\n          '0.3',\n          '0.3.3',\n          '0.3.2',\n          '0.2',\n          '0.2.3',\n          '0.2.2',\n        ],\n      },\n      {\n        branchingFactor: 3,\n        sort: 'newest',\n        length: 53,\n      },\n      {\n        branchingFactor: 4,\n        sort: 'newest',\n        length: 82,\n      },\n      {\n        branchingFactor: 5,\n        sort: 'newest',\n        // The seeds have 1 post with 5 replies, so it is +1 compared to branchingFactor 4.\n        length: 83,\n      },\n    ]\n\n    it.each(cases)(\n      'returns all top-level replies and limits nested to branching factor of $branchingFactor when sorting by $sort',\n      async (args) => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          {\n            anchor: seed.root.ref.uriStr,\n            sort: 'sort' in args ? args.sort : undefined,\n            branchingFactor: args.branchingFactor,\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        if ('length' in args) {\n          expect(data.thread).toHaveLength(args.length)\n        } else {\n          const tUris = t.map((i) => i.uri)\n          const postUris = args.postKeys.map((k) =>\n            k === 'root' ? seed.root.ref.uriStr : seed.r[k].ref.uriStr,\n          )\n          expect(tUris).toEqual(postUris)\n        }\n      },\n    )\n  })\n\n  describe('annotate more replies', () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.annotateMoreReplies>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.annotateMoreReplies(sc)\n      await network.processAll()\n    })\n\n    it('annotates correctly both in cases of trimmed replies by depth and by branching factor reached', async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        {\n          anchor: seed.root.ref.uriStr,\n          below: 4,\n          branchingFactor: 2,\n        },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      assertPosts(t)\n      expect(hasOtherReplies).toBe(false)\n      expect(t).toEqual([\n        expect.objectContaining({\n          uri: seed.root.ref.uriStr,\n          value: expect.objectContaining(props({ opThread: true })),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0.0.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0.0.0.0'].ref.uriStr,\n          value: expect.objectContaining(props({ moreReplies: 5 })),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0.1'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0.1.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['0.1.0.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1'].ref.uriStr,\n          value: expect.objectContaining(props({ moreReplies: 1 })),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.0'].ref.uriStr,\n          value: expect.objectContaining(props({ moreReplies: 3 })),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.0.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.0.1'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.1'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.1.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.1.1'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n      ])\n    })\n  })\n\n  describe(`annotate OP thread`, () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.annotateOP>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.annotateOP(sc)\n      await network.processAll()\n    })\n\n    type Case = {\n      postKey: string\n      length: number\n      opThreadPostKeys: string[]\n    }\n\n    const cases: Case[] = [\n      {\n        postKey: 'root',\n        length: 9,\n        opThreadPostKeys: ['root', '0', '0.0', '0.0.0', '2'],\n      },\n      {\n        postKey: '0',\n        length: 4,\n        opThreadPostKeys: ['root', '0', '0.0', '0.0.0'],\n      },\n      {\n        postKey: '0.0',\n        length: 4,\n        opThreadPostKeys: ['root', '0', '0.0', '0.0.0'],\n      },\n      {\n        postKey: '0.0.0',\n        length: 4,\n        opThreadPostKeys: ['root', '0', '0.0', '0.0.0'],\n      },\n      {\n        postKey: '1',\n        length: 3,\n        opThreadPostKeys: ['root'],\n      },\n      {\n        postKey: '1.0',\n        length: 3,\n        opThreadPostKeys: ['root'],\n      },\n      {\n        postKey: '2',\n        length: 4,\n        opThreadPostKeys: ['root', '2'],\n      },\n      {\n        postKey: '2.0',\n        length: 4,\n        opThreadPostKeys: ['root', '2'],\n      },\n      {\n        postKey: '2.0.0',\n        length: 4,\n        opThreadPostKeys: ['root', '2'],\n      },\n    ]\n\n    it.each(cases)(\n      `annotates OP threads correctly anchored at $postKey`,\n      async ({ postKey, length, opThreadPostKeys: opThreadPosts }) => {\n        const post = postKey === 'root' ? seed.root : seed.r[postKey]\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: post.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        const opThreadPostsUris = new Set(\n          opThreadPosts.map((k) =>\n            k === 'root' ? seed.root.ref.uriStr : seed.r[k].ref.uriStr,\n          ),\n        )\n\n        expect(t).toHaveLength(length)\n        t.forEach((i) => {\n          expect(i.value.opThread).toBe(opThreadPostsUris.has(i.uri))\n        })\n      },\n    )\n  })\n\n  describe('bumping and sorting', () => {\n    describe('sorting', () => {\n      let seed: Awaited<ReturnType<typeof seedThreadV2.sort>>\n\n      beforeAll(async () => {\n        seed = await seedThreadV2.sort(sc)\n        await network.processAll()\n      })\n\n      type Case = {\n        sort: QueryParamsThread['sort']\n        postKeys: string[]\n      }\n\n      const cases: Case[] = [\n        {\n          sort: 'newest',\n          postKeys: [\n            'root',\n            '2',\n            '2.2',\n            '2.1',\n            '2.0',\n            '1',\n            '1.2',\n            '1.1',\n            '1.0',\n            '0',\n            '0.2',\n            '0.1',\n            '0.0',\n          ],\n        },\n        {\n          sort: 'oldest',\n          postKeys: [\n            'root',\n            '0',\n            '0.0',\n            '0.1',\n            '0.2',\n            '1',\n            '1.0',\n            '1.1',\n            '1.2',\n            '2',\n            '2.0',\n            '2.1',\n            '2.2',\n          ],\n        },\n        {\n          sort: 'top',\n          postKeys: [\n            'root',\n            '1',\n            '1.1',\n            '1.0',\n            '1.2',\n            '2',\n            '2.0',\n            '2.1',\n            '2.2',\n            '0',\n            '0.2',\n            '0.1',\n            '0.0',\n          ],\n        },\n      ]\n\n      it.each(cases)(\n        'sorts by $sort in all levels',\n        async ({ sort: sort, postKeys }) => {\n          const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n            {\n              anchor: seed.root.ref.uriStr,\n              sort,\n            },\n            {\n              headers: await network.serviceHeaders(\n                seed.users.op.did,\n                ids.AppBskyUnspeccedGetPostThreadV2,\n              ),\n            },\n          )\n          const { thread: t, hasOtherReplies } = data\n\n          assertPosts(t)\n          expect(hasOtherReplies).toBe(false)\n          const tUris = t.map((i) => i.uri)\n          const postUris = postKeys.map((k) =>\n            k === 'root' ? seed.root.ref.uriStr : seed.r[k].ref.uriStr,\n          )\n          expect(tUris).toEqual(postUris)\n        },\n      )\n    })\n\n    describe('bumping', () => {\n      describe('sorting within bumped post groups', () => {\n        let seed: Awaited<ReturnType<typeof seedThreadV2.bumpGroupSorting>>\n\n        beforeAll(async () => {\n          seed = await seedThreadV2.bumpGroupSorting(sc)\n          await network.processAll()\n        })\n\n        type Case = {\n          sort: QueryParamsThread['sort']\n          postKeys: string[]\n        }\n\n        const cases: Case[] = [\n          {\n            sort: 'newest',\n            postKeys: ['root', '5', '3', '1', '7', '4', '0', '6', '2'],\n          },\n          {\n            sort: 'oldest',\n            postKeys: ['root', '1', '3', '5', '0', '4', '7', '2', '6'],\n          },\n        ]\n\n        it.each(cases)(\n          'sorts by $sort inside each bumped group',\n          async ({ sort: sort, postKeys }) => {\n            const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n              {\n                anchor: seed.root.ref.uriStr,\n                sort,\n              },\n              {\n                headers: await network.serviceHeaders(\n                  seed.users.viewer.did,\n                  ids.AppBskyUnspeccedGetPostThreadV2,\n                ),\n              },\n            )\n            const { thread: t, hasOtherReplies } = data\n\n            assertPosts(t)\n            expect(hasOtherReplies).toBe(false)\n            const tUris = t.map((i) => i.uri)\n            const postUris = postKeys.map((k) =>\n              k === 'root' ? seed.root.ref.uriStr : seed.r[k].ref.uriStr,\n            )\n            expect(tUris).toEqual(postUris)\n          },\n        )\n      })\n\n      describe('OP and viewer', () => {\n        let seed: Awaited<ReturnType<typeof seedThreadV2.bumpOpAndViewer>>\n\n        beforeAll(async () => {\n          seed = await seedThreadV2.bumpOpAndViewer(sc)\n          await network.processAll()\n        })\n\n        type Case = {\n          sort: QueryParamsThread['sort']\n          postKeys: string[]\n        }\n\n        const cases: Case[] = [\n          {\n            sort: 'newest',\n            postKeys: [\n              'root',\n              '3', // op\n              '3.2', // op\n              '3.0', // viewer\n              '3.4',\n              '3.3',\n              '3.1',\n              '4', // viewer\n              '4.2', // op\n              '4.3', // viewer\n              '4.4',\n              '4.1',\n              '4.0',\n              '2',\n              '2.2', // op\n              '2.0', // viewer\n              '2.4',\n              '2.3',\n              '2.1',\n              '1',\n              '1.2', // op\n              '1.3', // viewer\n              '1.4',\n              '1.1',\n              '1.0',\n              '0',\n              '0.4', // op\n              '0.3', // viewer\n              '0.2',\n              '0.1',\n              '0.0',\n            ],\n          },\n          {\n            sort: 'oldest',\n            postKeys: [\n              'root',\n              '3', // op\n              '3.2', // op\n              '3.0', // viewer\n              '3.1',\n              '3.3',\n              '3.4',\n              '4', // viewer\n              '4.2', // op\n              '4.3', // viewer\n              '4.0',\n              '4.1',\n              '4.4',\n              '0',\n              '0.4', // op\n              '0.3', // viewer\n              '0.0',\n              '0.1',\n              '0.2',\n              '1',\n              '1.2', // op\n              '1.3', // viewer\n              '1.0',\n              '1.1',\n              '1.4',\n              '2',\n              '2.2', // op\n              '2.0', // viewer\n              '2.1',\n              '2.3',\n              '2.4',\n            ],\n          },\n          {\n            sort: 'top',\n            postKeys: [\n              'root',\n              '3', // op\n              '3.2', // op\n              '3.0', // viewer\n              '3.4',\n              '3.3',\n              '3.1',\n              '4', // viewer\n              '4.2', // op\n              '4.3', // viewer\n              '4.1',\n              '4.0',\n              '4.4',\n              '1',\n              '1.2', // op\n              '1.3', // viewer\n              '1.1',\n              '1.0',\n              '1.4',\n              '2',\n              '2.2', // op\n              '2.0', // viewer\n              '2.1',\n              '2.4',\n              '2.3',\n              '0',\n              '0.4', // op\n              '0.3', // viewer\n              '0.2',\n              '0.1',\n              '0.0',\n            ],\n          },\n        ]\n\n        it.each(cases)(\n          'bumps up OP and viewer and sorts by $sort in all levels',\n          async ({ sort: sort, postKeys }) => {\n            const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n              {\n                anchor: seed.root.ref.uriStr,\n                sort,\n              },\n              {\n                headers: await network.serviceHeaders(\n                  seed.users.viewer.did,\n                  ids.AppBskyUnspeccedGetPostThreadV2,\n                ),\n              },\n            )\n            const { thread: t, hasOtherReplies } = data\n\n            assertPosts(t)\n            expect(hasOtherReplies).toBe(false)\n            const tUris = t.map((i) => i.uri)\n            const postUris = postKeys.map((k) =>\n              k === 'root' ? seed.root.ref.uriStr : seed.r[k].ref.uriStr,\n            )\n            expect(tUris).toEqual(postUris)\n          },\n        )\n      })\n\n      describe('followers', () => {\n        let seed: Awaited<ReturnType<typeof seedThreadV2.bumpFollows>>\n\n        beforeAll(async () => {\n          seed = await seedThreadV2.bumpFollows(sc)\n          await network.processAll()\n        })\n\n        const threadForPostAndViewer = async (post: string, viewer: string) => {\n          const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n            {\n              anchor: post,\n              sort: 'newest',\n            },\n            {\n              headers: await network.serviceHeaders(\n                viewer,\n                ids.AppBskyUnspeccedGetPostThreadV2,\n              ),\n            },\n          )\n          const { thread: t, hasOtherReplies } = data\n\n          assertPosts(t)\n          expect(hasOtherReplies).toBe(false)\n          return t\n        }\n\n        it('bumps up followed users', async () => {\n          const t1 = await threadForPostAndViewer(\n            seed.root.ref.uriStr,\n            seed.users.viewerF.did,\n          )\n          expect(t1).toEqual([\n            expect.objectContaining({ uri: seed.root.ref.uriStr }), // root\n            expect.objectContaining({ uri: seed.r['3'].ref.uriStr }), // op reply\n            expect.objectContaining({ uri: seed.r['4'].ref.uriStr }), // viewer reply\n            expect.objectContaining({ uri: seed.r['1'].ref.uriStr }), // newest followed reply\n            expect.objectContaining({ uri: seed.r['0'].ref.uriStr }), // oldest followed reply\n            expect.objectContaining({ uri: seed.r['5'].ref.uriStr }), // newest non-followed reply\n            expect.objectContaining({ uri: seed.r['2'].ref.uriStr }), // oldest non-followed reply\n          ])\n\n          const t2 = await threadForPostAndViewer(\n            seed.root.ref.uriStr,\n            seed.users.viewerNoF.did,\n          )\n          expect(t2).toEqual([\n            expect.objectContaining({ uri: seed.root.ref.uriStr }), // root\n            expect.objectContaining({ uri: seed.r['3'].ref.uriStr }), // op reply\n            expect.objectContaining({ uri: seed.r['5'].ref.uriStr }), // viewer reply\n            // newest to oldest\n            expect.objectContaining({ uri: seed.r['4'].ref.uriStr }),\n            expect.objectContaining({ uri: seed.r['2'].ref.uriStr }),\n            expect.objectContaining({ uri: seed.r['1'].ref.uriStr }),\n            expect.objectContaining({ uri: seed.r['0'].ref.uriStr }),\n          ])\n        })\n      })\n    })\n  })\n\n  describe(`blocks, deletions, no-unauthenticated`, () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.blockDeletionAuth>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.blockDeletionAuth(sc, labelerDid)\n      await network.processAll()\n    })\n\n    describe(`1p blocks`, () => {\n      it(`blocked reply is omitted from replies`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `blocked`, who was blocked by `blocker`, author of '0'.\n              seed.users.blocked.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        assertPosts(t)\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({ uri: seed.root.ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3.0.0'].ref.uriStr }),\n        ])\n      })\n\n      it(`blocked anchor returns lone blocked view`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['0'].ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `blocked`, who was blocked by `blocker`, author of '0'.\n              seed.users.blocked.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.r['0'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemBlocked',\n            }),\n          }),\n        ])\n      })\n\n      it(`blocked parent is replaced by blocked view`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['0.0'].ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `blocked`, who was blocked by `blocker`, author of '0'.\n              seed.users.blocked.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.r['0'].ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemBlocked',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['0.0'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n    })\n\n    describe(`3p blocks`, () => {\n      it(`blocked reply is omitted from replies`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `alice` who is a 3rd party between `op` and `opBlocked`.\n              seed.users.alice.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        assertPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({ uri: seed.root.ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['0.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3.0.0'].ref.uriStr }),\n        ])\n      })\n\n      it(`blocked anchor returns post with blocked parent and non-blocked descendants`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['1'].ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `alice` who is a 3rd party between `op` and `opBlocked`.\n              seed.users.alice.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemBlocked',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['1'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n          // 1.0 is blocked, but 1.1 is not\n          expect.objectContaining({\n            uri: seed.r['1.1'].ref.uriStr,\n            depth: 1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n\n      it(`blocked parent is replaced by blocked view`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['1.0'].ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `alice` who is a 3rd party between `op` and `opBlocked`.\n              seed.users.alice.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.r['1'].ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemBlocked',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['1.0'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n\n      it(`blocked root is replaced by blocked view`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['1.1'].ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Use `alice` who is a 3rd party between `op` and `opBlocked`.\n              seed.users.alice.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            depth: -2,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemBlocked',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['1'].ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['1.1'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n    })\n\n    describe(`deleted posts`, () => {\n      it(`deleted reply is omitted from replies`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        assertPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({ uri: seed.root.ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['0.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['3.0.0'].ref.uriStr }),\n        ])\n      })\n\n      it(`deleted parent is replaced by not found view`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['2.0'].ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.r['2'].ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemNotFound',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['2.0'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n    })\n\n    describe('no-unauthenticated', () => {\n      it(`no-unauthenticated reply is omitted from replies`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: {\n              'atproto-accept-labelers': `${labelerDid}`,\n            },\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['0'].ref.uriStr,\n            depth: 1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['0.0'].ref.uriStr,\n            depth: 2,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n\n      it(`no-unauthenticated anchor returns no-unauthenticated view without breaking the parent chain`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['3'].ref.uriStr },\n          {\n            headers: {\n              'atproto-accept-labelers': `${labelerDid}`,\n            },\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['3'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n            }),\n          }),\n        ])\n      })\n\n      it(`no-unauthenticated parent is replaced by no-unauthenticated view without breaking the parent chain`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.r['3.0.0'].ref.uriStr },\n          {\n            headers: {\n              'atproto-accept-labelers': `${labelerDid}`,\n            },\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            depth: -3,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['3'].ref.uriStr,\n            depth: -2,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['3.0'].ref.uriStr,\n            depth: -1,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n            }),\n          }),\n          expect.objectContaining({\n            uri: seed.r['3.0.0'].ref.uriStr,\n            depth: 0,\n            value: expect.objectContaining({\n              $type: 'app.bsky.unspecced.defs#threadItemPost',\n            }),\n          }),\n        ])\n      })\n    })\n  })\n\n  describe(`mutes`, () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.mutes>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.mutes(sc)\n      await network.processAll()\n    })\n\n    describe('omitting muted replies', () => {\n      it(`muted reply is omitted in top-level replies and in nested replies`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Fetching as `op` mutes `opMuted`.\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(true)\n        assertPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            value: expect.objectContaining(props({ opThread: true })),\n          }),\n          expect.objectContaining({\n            uri: seed.r['1'].ref.uriStr,\n            value: expect.objectContaining(props()),\n          }),\n          // 1.0 is a nested muted reply, so it is omitted.\n          expect.objectContaining({\n            uri: seed.r['1.1'].ref.uriStr,\n            value: expect.objectContaining(props()),\n          }),\n        ])\n      })\n\n      it(`top-level muted replies are returned when fetching hidden, sorted by newest`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadOtherV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Fetching as `op` mutes `opMuted`.\n              seed.users.op.did,\n              ids.AppBskyUnspeccedGetPostThreadOtherV2,\n            ),\n          },\n        )\n        const { thread: t } = data\n\n        assertHiddenPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.r['0'].ref.uriStr,\n            value: expect.objectContaining(\n              propsHidden({ mutedByViewer: true }),\n            ),\n          }),\n          // No nested replies for hidden.\n        ])\n      })\n    })\n\n    describe('OP mutes', () => {\n      it(`mutes by OP don't mute for 3p`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Fetching as `muter` mutes `muted`.\n              seed.users.muter.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(true)\n        assertPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.root.ref.uriStr,\n            value: expect.objectContaining(props({ opThread: true })),\n          }),\n          expect.objectContaining({\n            uri: seed.r['0'].ref.uriStr,\n            value: expect.objectContaining(props()),\n          }),\n          expect.objectContaining({\n            uri: seed.r['0.0'].ref.uriStr,\n            value: expect.objectContaining(props()),\n          }),\n          // 0.1 is a nested muted reply, so it is omitted.\n        ])\n      })\n\n      it(`fetches hidden replies includes own mutes, not OP mutes, sorted by newest`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadOtherV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              // Fetching as `muter` mutes `muted`.\n              seed.users.muter.did,\n              ids.AppBskyUnspeccedGetPostThreadOtherV2,\n            ),\n          },\n        )\n        const { thread: t } = data\n\n        assertHiddenPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({\n            uri: seed.r['1'].ref.uriStr,\n            value: expect.objectContaining(\n              propsHidden({ mutedByViewer: true }),\n            ),\n          }),\n          // No nested replies for hidden.\n        ])\n      })\n\n      it(`mutes by OP don't affect the muted user`, async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          { anchor: seed.root.ref.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.opMuted.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(false)\n        assertPosts(t)\n        // No muted posts by `opMuted`, gets the full thread.\n        expect(t.length).toBe(1 + Object.keys(seed.r).length) // root + replies\n      })\n    })\n  })\n\n  describe(`threadgated`, () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.threadgated>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.threadgated(sc)\n      await network.processAll()\n    })\n\n    it(`threadgated reply is omitted in top-level replies and in nested replies`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.root.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      expect(hasOtherReplies).toBe(true)\n      assertPosts(t)\n      expect(t).toEqual([\n        expect.objectContaining({\n          uri: seed.root.ref.uriStr,\n          value: expect.objectContaining(props({ opThread: true })),\n        }),\n        expect.objectContaining({\n          uri: seed.r['2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // OP reply bumped up.\n        expect.objectContaining({\n          uri: seed.r['2.2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['2.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // 2.1 is a nested hidden reply, so it is omitted.\n      ])\n    })\n\n    it(`top-level threadgated replies are returned to OP when fetching hidden, sorted by newest`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadOtherV2(\n        { anchor: seed.root.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            seed.users.op.did,\n            ids.AppBskyUnspeccedGetPostThreadOtherV2,\n          ),\n        },\n      )\n      const { thread: t } = data\n\n      assertHiddenPosts(t)\n      expect(t).toEqual([\n        expect.objectContaining({\n          uri: seed.r['1'].ref.uriStr,\n          value: expect.objectContaining(\n            propsHidden({ hiddenByThreadgate: true }),\n          ),\n        }),\n        // No nested replies for hidden.\n\n        // Mutes come after hidden.\n        expect.objectContaining({\n          uri: seed.r['0'].ref.uriStr,\n          value: expect.objectContaining(propsHidden({ mutedByViewer: true })),\n        }),\n      ])\n    })\n\n    it(`author of hidden reply does not see it as hidden`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.root.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            // `alice` does not get its own reply as hidden.\n            seed.users.alice.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      expect(hasOtherReplies).toBe(false)\n      assertPosts(t)\n      expect(t).toEqual([\n        expect.objectContaining({\n          uri: seed.root.ref.uriStr,\n          value: expect.objectContaining(props({ opThread: true })),\n        }),\n\n        // alice does not see its own reply as hidden.\n        expect.objectContaining({\n          uri: seed.r['1'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // OP reply bumped up.\n        expect.objectContaining({\n          uri: seed.r['1.2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['1.1'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n\n        // `opMuted` is not muted by `alice`.\n        expect.objectContaining({\n          uri: seed.r['0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n\n        expect.objectContaining({\n          uri: seed.r['2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // OP reply bumped up.\n        expect.objectContaining({\n          uri: seed.r['2.2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['2.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // 2.1 is a nested hidden reply, so it is omitted.\n      ])\n    })\n\n    it(`other viewers are affected by threadgate-hidden replies by OP`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n        { anchor: seed.root.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            // `viewer` also gets the replies as hidden.\n            seed.users.viewer.did,\n            ids.AppBskyUnspeccedGetPostThreadV2,\n          ),\n        },\n      )\n      const { thread: t, hasOtherReplies } = data\n\n      expect(hasOtherReplies).toBe(true)\n      assertPosts(t)\n      expect(t).toEqual([\n        expect.objectContaining({\n          uri: seed.root.ref.uriStr,\n          value: expect.objectContaining(props({ opThread: true })),\n        }),\n        // `opMuted` doesn't see itself as muted, just `op` does.\n        expect.objectContaining({\n          uri: seed.r['0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n\n        expect.objectContaining({\n          uri: seed.r['2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // OP reply bumped up.\n        expect.objectContaining({\n          uri: seed.r['2.2'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        expect.objectContaining({\n          uri: seed.r['2.0'].ref.uriStr,\n          value: expect.objectContaining(props()),\n        }),\n        // 2.1 is a nested hidden reply, so it is omitted.\n      ])\n    })\n\n    it(`top-level threadgated replies are returned to other viewers when fetching hidden, sorted by newest`, async () => {\n      const { data } = await agent.app.bsky.unspecced.getPostThreadOtherV2(\n        { anchor: seed.root.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            // `viewer` also gets the replies as hidden.\n            seed.users.viewer.did,\n            ids.AppBskyUnspeccedGetPostThreadOtherV2,\n          ),\n        },\n      )\n      const { thread: t } = data\n\n      assertHiddenPosts(t)\n      expect(t).toEqual([\n        expect.objectContaining({\n          uri: seed.r['1'].ref.uriStr,\n          value: expect.objectContaining(\n            propsHidden({ hiddenByThreadgate: true }),\n          ),\n        }),\n        // No nested replies for hidden.\n      ])\n    })\n  })\n\n  describe('tags', () => {\n    let seed: Awaited<ReturnType<typeof seedThreadV2.tags>>\n\n    beforeAll(async () => {\n      seed = await seedThreadV2.tags(sc)\n      await network.processAll()\n    })\n\n    describe('when prioritizing followed users', () => {\n      it('considers tags for bumping down and hiding', async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadV2(\n          {\n            anchor: seed.root.ref.uriStr,\n            sort: 'newest',\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.viewer.did,\n              ids.AppBskyUnspeccedGetPostThreadV2,\n            ),\n          },\n        )\n        const { thread: t, hasOtherReplies } = data\n\n        expect(hasOtherReplies).toBe(true)\n        assertPosts(t)\n        expect(t).toEqual([\n          expect.objectContaining({ uri: seed.root.ref.uriStr }),\n          // OP (down overridden).\n          expect.objectContaining({ uri: seed.r['3'].ref.uriStr }),\n          // Viewer (hide overridden).\n          expect.objectContaining({ uri: seed.r['4'].ref.uriStr }),\n          // Following (hide overridden).\n          expect.objectContaining({ uri: seed.r['5'].ref.uriStr }),\n          // Fot following.\n          expect.objectContaining({ uri: seed.r['0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['0.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['0.1'].ref.uriStr }),\n          // Down.\n          expect.objectContaining({ uri: seed.r['1'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['1.0'].ref.uriStr }),\n          expect.objectContaining({ uri: seed.r['1.1'].ref.uriStr }),\n        ])\n      })\n\n      it('finds the hidden by tag', async () => {\n        const { data } = await agent.app.bsky.unspecced.getPostThreadOtherV2(\n          {\n            anchor: seed.root.ref.uriStr,\n          },\n          {\n            headers: await network.serviceHeaders(\n              seed.users.viewer.did,\n              ids.AppBskyUnspeccedGetPostThreadOtherV2,\n            ),\n          },\n        )\n        const { thread: t } = data\n\n        assertHiddenPosts(t)\n        expect(t).toEqual([\n          // Hide.\n          expect.objectContaining({ uri: seed.r['2'].ref.uriStr }),\n        ])\n      })\n    })\n  })\n})\n\nfunction assertPosts(\n  t: OutputSchemaThread['thread'],\n): asserts t is ThreadItemValuePost[] {\n  t.forEach((i) => {\n    assert(\n      AppBskyUnspeccedDefs.isThreadItemPost(i.value),\n      `Expected thread item to have a post as value`,\n    )\n  })\n}\n\nfunction assertHiddenPosts(\n  t: OutputSchemaHiddenThread['thread'],\n): asserts t is ThreadOtherItemValuePost[] {\n  t.forEach((i) => {\n    assert(\n      AppBskyUnspeccedDefs.isThreadItemPost(i.value),\n      `Expected thread item to have a hidden post as value`,\n    )\n  })\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/thread.test.ts",
    "content": "import assert from 'node:assert'\nimport { AppBskyFeedGetPostThread, AtUri, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { isThreadViewPost } from '../../src/lexicon/types/app/bsky/feed/defs'\nimport {\n  assertIsThreadViewPost,\n  forSnapshot,\n  stripViewerFromThread,\n} from '../_util'\n\ndescribe('appview thread views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_thread',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n\n    await sc.like(alice, sc.replies[alice][0].ref)\n    await sc.like(alice, sc.replies[bob][0].ref)\n    await sc.like(alice, sc.replies[carol][0].ref)\n  })\n\n  beforeAll(async () => {\n    // Add a repost of a reply so that we can confirm myState in the thread\n    await sc.repost(bob, sc.replies[alice][0].ref)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fetches deep post thread', async () => {\n    const thread = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n  })\n\n  it('fetches shallow post thread', async () => {\n    const thread = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n  })\n\n  it('fetches thread with handle in uri', async () => {\n    const thread = await agent.api.app.bsky.feed.getPostThread(\n      {\n        depth: 1,\n        uri: sc.posts[alice][1].ref.uriStr.replace(\n          `at://${alice}`,\n          `at://${sc.accounts[alice].handle}`,\n        ),\n      },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n  })\n\n  it('fetches ancestors', async () => {\n    const thread = await agent.api.app.bsky.feed.getPostThread(\n      { depth: 1, uri: sc.replies[alice][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n  })\n\n  it('fails for an unknown post', async () => {\n    const promise = agent.api.app.bsky.feed.getPostThread(\n      { uri: 'at://did:example:fake/does.not.exist/self' },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    await expect(promise).rejects.toThrow(\n      AppBskyFeedGetPostThread.NotFoundError,\n    )\n  })\n\n  it('fetches post thread unauthed', async () => {\n    const { data: authed } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][1].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    const { data: unauthed } = await agent.api.app.bsky.feed.getPostThread({\n      uri: sc.posts[alice][1].ref.uriStr,\n    })\n    expect(unauthed.thread).toEqual(stripViewerFromThread(authed.thread))\n  })\n\n  it('handles deleted posts correctly', async () => {\n    const alice = sc.dids.alice\n    const bob = sc.dids.bob\n\n    const indexes = {\n      aliceRoot: -1,\n      bobReply: -1,\n      aliceReplyReply: -1,\n    }\n\n    await sc.post(alice, 'Deletion thread')\n    indexes.aliceRoot = sc.posts[alice].length - 1\n\n    await sc.reply(\n      bob,\n      sc.posts[alice][indexes.aliceRoot].ref,\n      sc.posts[alice][indexes.aliceRoot].ref,\n      'Reply',\n    )\n    indexes.bobReply = sc.replies[bob].length - 1\n    await sc.reply(\n      alice,\n      sc.posts[alice][indexes.aliceRoot].ref,\n      sc.replies[bob][indexes.bobReply].ref,\n      'Reply reply',\n    )\n    indexes.aliceReplyReply = sc.replies[alice].length - 1\n    await network.processAll()\n\n    const thread1 = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread1.data.thread)).toMatchSnapshot()\n\n    await sc.deletePost(bob, sc.replies[bob][indexes.bobReply].ref.uri)\n    await network.processAll()\n\n    const thread2 = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread2.data.thread)).toMatchSnapshot()\n\n    const thread3 = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.replies[alice][indexes.aliceReplyReply].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    expect(forSnapshot(thread3.data.thread)).toMatchSnapshot()\n  })\n\n  it('omits parents and replies w/ different root than anchor post.', async () => {\n    const badRoot = sc.posts[alice][0]\n    const goodRoot = await sc.post(alice, 'good root')\n    const goodReply1 = await sc.reply(\n      alice,\n      goodRoot.ref,\n      goodRoot.ref,\n      'good reply 1',\n    )\n    const goodReply2 = await sc.reply(\n      alice,\n      goodRoot.ref,\n      goodReply1.ref,\n      'good reply 2',\n    )\n    const badReply = await sc.reply(\n      alice,\n      badRoot.ref,\n      goodReply1.ref,\n      'bad reply',\n    )\n    await network.processAll()\n    // good reply doesn't have replies w/ different root\n    const { data: goodReply1Thread } =\n      await agent.api.app.bsky.feed.getPostThread(\n        { uri: goodReply1.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n    assertIsThreadViewPost(goodReply1Thread.thread)\n    assertIsThreadViewPost(goodReply1Thread.thread.parent)\n    expect(goodReply1Thread.thread.parent.post.uri).toEqual(goodRoot.ref.uriStr)\n    expect(\n      goodReply1Thread.thread.replies?.map((r) => {\n        assertIsThreadViewPost(r)\n        return r.post.uri\n      }),\n    ).toEqual([\n      goodReply2.ref.uriStr, // does not contain badReply\n    ])\n    expect(goodReply1Thread.thread.parent.replies).toBeUndefined()\n    // bad reply doesn't have a parent, which would have a different root\n    const { data: badReplyThread } =\n      await agent.api.app.bsky.feed.getPostThread(\n        { uri: badReply.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            alice,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n    assertIsThreadViewPost(badReplyThread.thread)\n    expect(badReplyThread.thread.parent).toBeUndefined() // is not goodReply1\n  })\n\n  it('reflects self-labels', async () => {\n    const { data: thread } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][0].ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n\n    assertIsThreadViewPost(thread.thread)\n\n    const post = thread.thread.post\n\n    const postSelfLabels = post.labels\n      ?.filter((label) => label.src === alice)\n      .map((label) => label.val)\n\n    expect(postSelfLabels).toEqual(['self-label'])\n\n    const authorSelfLabels = post.author.labels\n      ?.filter((label) => label.src === alice)\n      .map((label) => label.val)\n      .sort()\n\n    expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b'])\n  })\n\n  describe('takedown', () => {\n    it('blocks post by actor', async () => {\n      await network.bsky.ctx.dataplane.takedownActor({\n        did: alice,\n      })\n\n      // Same as shallow post thread test, minus alice\n      const promise = agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: sc.posts[alice][1].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      await expect(promise).rejects.toThrow(\n        AppBskyFeedGetPostThread.NotFoundError,\n      )\n\n      // Cleanup\n      await network.bsky.ctx.dataplane.untakedownActor({\n        did: alice,\n      })\n    })\n\n    it('blocks replies by actor', async () => {\n      await network.bsky.ctx.dataplane.takedownActor({\n        did: carol,\n      })\n\n      // Same as deep post thread test, minus carol\n      const thread = await agent.api.app.bsky.feed.getPostThread(\n        { uri: sc.posts[alice][1].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n\n      // Cleanup\n      await network.bsky.ctx.dataplane.untakedownActor({\n        did: carol,\n      })\n    })\n\n    it('blocks ancestors by actor', async () => {\n      await network.bsky.ctx.dataplane.takedownActor({\n        did: bob,\n      })\n\n      // Same as ancestor post thread test, minus bob\n      const thread = await agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: sc.replies[alice][0].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n\n      // Cleanup\n      await network.bsky.ctx.dataplane.untakedownActor({\n        did: bob,\n      })\n    })\n\n    it('blocks post by record', async () => {\n      const postRef = sc.posts[alice][1].ref\n      await network.bsky.ctx.dataplane.takedownRecord({\n        recordUri: postRef.uriStr,\n      })\n\n      const promise = agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: postRef.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      await expect(promise).rejects.toThrow(\n        AppBskyFeedGetPostThread.NotFoundError,\n      )\n\n      // Cleanup\n      await network.bsky.ctx.dataplane.untakedownRecord({\n        recordUri: postRef.uriStr,\n      })\n    })\n\n    it('blocks ancestors by record', async () => {\n      const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: sc.replies[alice][0].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      assert(isThreadViewPost(threadPreTakedown.data.thread))\n      assert(isThreadViewPost(threadPreTakedown.data.thread.parent))\n\n      const parent = threadPreTakedown.data.thread.parent.post\n\n      await network.bsky.ctx.dataplane.takedownRecord({\n        recordUri: parent.uri,\n      })\n\n      // Same as ancestor post thread test, minus parent post\n      const thread = await agent.api.app.bsky.feed.getPostThread(\n        { depth: 1, uri: sc.replies[alice][0].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n\n      // Cleanup\n      await network.bsky.ctx.dataplane.untakedownRecord({\n        recordUri: parent.uri,\n      })\n    })\n\n    it('blocks replies by record', async () => {\n      const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread(\n        { uri: sc.posts[alice][1].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      assert(isThreadViewPost(threadPreTakedown.data.thread))\n      assert(isThreadViewPost(threadPreTakedown.data.thread.replies?.[0]))\n      assert(isThreadViewPost(threadPreTakedown.data.thread.replies?.[1]))\n      assert(\n        isThreadViewPost(\n          threadPreTakedown.data.thread.replies?.[1].replies?.[0],\n        ),\n      )\n\n      const post1 = threadPreTakedown.data.thread.replies?.[0].post\n      const post2 = threadPreTakedown.data.thread.replies?.[1].replies?.[0].post\n\n      await Promise.all(\n        [post1, post2].map((post) =>\n          network.bsky.ctx.dataplane.takedownRecord({\n            recordUri: post.uri,\n          }),\n        ),\n      )\n\n      // Same as deep post thread test, minus some replies\n      const thread = await agent.api.app.bsky.feed.getPostThread(\n        { uri: sc.posts[alice][1].ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            bob,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(forSnapshot(thread.data.thread)).toMatchSnapshot()\n\n      // Cleanup\n      await Promise.all(\n        [post1, post2].map((post) =>\n          network.bsky.ctx.dataplane.untakedownRecord({\n            recordUri: post.uri,\n          }),\n        ),\n      )\n    })\n  })\n\n  describe('listblock', () => {\n    it(`doesn't apply listblock if list was taken down by private takedown`, async () => {\n      const post = await sc.post(carol, `I'm carol`)\n      const reply = await sc.reply(bob, post.ref, post.ref, `hi carol`)\n\n      // alice creates a block list that includes carol\n      const listRef = await sc.createList(alice, 'alice blocks', 'mod')\n      await sc.addToList(alice, carol, listRef)\n      // bob subscribes to that list\n      const listblockRef = await pdsAgent.app.bsky.graph.listblock.create(\n        { repo: bob },\n        {\n          subject: listRef.uriStr,\n          createdAt: new Date().toISOString(),\n        },\n        sc.getHeaders(bob),\n      )\n      await network.processAll()\n\n      const threadBeforeListTakedown = await agent.app.bsky.feed.getPostThread(\n        { depth: 1, uri: reply.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      expect(\n        forSnapshot(threadBeforeListTakedown.data.thread),\n      ).toMatchSnapshot()\n\n      // Takedown the list\n      await network.bsky.ctx.dataplane.takedownRecord({\n        recordUri: listRef.uriStr,\n      })\n      await network.processAll()\n\n      const threadAfterListTakedown = await agent.app.bsky.feed.getPostThread(\n        { depth: 1, uri: reply.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      expect(forSnapshot(threadAfterListTakedown.data.thread)).toMatchSnapshot()\n\n      // Cleanup\n      await pdsAgent.app.bsky.graph.listblock.delete(\n        { repo: bob, rkey: new AtUri(listblockRef.uri).rkey },\n        sc.getHeaders(bob),\n      )\n      await network.bsky.ctx.dataplane.untakedownRecord({\n        recordUri: listRef.uriStr,\n      })\n      await network.processAll()\n    })\n\n    it(`doesn't apply listblock if list was taken down by takedown label`, async () => {\n      const post = await sc.post(carol, `I'm carol`)\n      const reply = await sc.reply(bob, post.ref, post.ref, `hi carol`)\n\n      // alice creates a block list that includes carol\n      const listRef = await sc.createList(alice, 'alice blocks', 'mod')\n      await sc.addToList(alice, carol, listRef)\n      // bob subscribes to that list\n      const listblockRef = await pdsAgent.app.bsky.graph.listblock.create(\n        { repo: bob },\n        {\n          subject: listRef.uriStr,\n          createdAt: new Date().toISOString(),\n        },\n        sc.getHeaders(bob),\n      )\n\n      await network.processAll()\n\n      const threadBeforeListTakedown = await agent.app.bsky.feed.getPostThread(\n        { depth: 1, uri: reply.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n      expect(\n        forSnapshot(threadBeforeListTakedown.data.thread),\n      ).toMatchSnapshot()\n\n      // Takedown the list\n      const src = network.ozone.ctx.cfg.service.did\n      const label = {\n        src,\n        uri: listRef.uriStr,\n        cid: '',\n        val: '!takedown',\n        exp: null,\n        neg: false,\n        cts: new Date().toISOString(),\n      }\n      AtpAgent.configure({ appLabelers: [src] })\n      await network.bsky.db.db.insertInto('label').values(label).execute()\n\n      const threadAfterListTakedown = await agent.app.bsky.feed.getPostThread(\n        { depth: 1, uri: reply.ref.uriStr },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetPostThread,\n          ),\n        },\n      )\n\n      expect(forSnapshot(threadAfterListTakedown.data.thread)).toMatchSnapshot()\n\n      // Cleanup\n      await pdsAgent.app.bsky.graph.listblock.delete(\n        { repo: bob, rkey: new AtUri(listblockRef.uri).rkey },\n        sc.getHeaders(bob),\n      )\n      await network.bsky.db.db\n        .deleteFrom('label')\n        .where('src', '=', src)\n        .where('uri', '=', listRef.uriStr)\n        .execute()\n      await network.processAll()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/bsky/tests/views/threadgating.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport {\n  isNotFoundPost,\n  isThreadViewPost,\n} from '../../src/lexicon/types/app/bsky/feed/defs'\nimport { forSnapshot } from '../_util'\n\ndescribe('views with thread gating', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_thread_gating',\n    })\n    agent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@eve.com',\n      password: 'hunter2',\n    })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  // check that replyDisabled state is applied correctly in a simple method like getPosts\n  const checkReplyDisabled = async (\n    uri: string,\n    user: string,\n    blocked: boolean | undefined,\n  ) => {\n    const res = await agent.api.app.bsky.feed.getPosts(\n      { uris: [uri] },\n      { headers: await network.serviceHeaders(user, ids.AppBskyFeedGetPosts) },\n    )\n    expect(res.data.posts[0].viewer?.replyDisabled).toBe(blocked)\n  }\n\n  it('applies gate for empty rules.', async () => {\n    const post = await sc.post(sc.dids.carol, 'empty rules')\n    const { uri: threadgateUri } =\n      await pdsAgent.api.app.bsky.feed.threadgate.create(\n        { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n        { post: post.ref.uriStr, createdAt: iso(), allow: [] },\n        sc.getHeaders(sc.dids.carol),\n      )\n    await network.processAll()\n    await sc.reply(sc.dids.alice, post.ref, post.ref, 'empty rules reply')\n    await network.processAll()\n    const {\n      data: { thread, threadgate },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(thread))\n    expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()\n    expect(thread.post.viewer?.replyDisabled).toBe(true)\n    expect(thread.replies?.length).toEqual(0)\n    expect(threadgate?.uri).toEqual(threadgateUri)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)\n  })\n\n  it('does not generate notifications when post violates threadgate.', async () => {\n    const post = await sc.post(sc.dids.carol, 'notifications')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      { post: post.ref.uriStr, createdAt: iso(), allow: [] },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    const reply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'notifications reply',\n    )\n    await network.processAll()\n    const {\n      data: { notifications },\n    } = await agent.api.app.bsky.notification.listNotifications(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyNotificationListNotifications,\n        ),\n      },\n    )\n    const notificationFromReply = notifications.find(\n      (notif) => notif.uri === reply.ref.uriStr,\n    )\n    expect(notificationFromReply).toBeUndefined()\n  })\n\n  it('applies gate for mention rule.', async () => {\n    const post = await sc.post(\n      sc.dids.carol,\n      'mention rules @carol.test @dan.test',\n      [\n        {\n          index: { byteStart: 14, byteEnd: 25 },\n          features: [\n            { $type: 'app.bsky.richtext.facet#mention', did: sc.dids.carol },\n          ],\n        },\n        {\n          index: { byteStart: 26, byteEnd: 35 },\n          features: [\n            { $type: 'app.bsky.richtext.facet#mention', did: sc.dids.dan },\n          ],\n        },\n      ],\n    )\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [{ $type: 'app.bsky.feed.threadgate#mentionRule' }],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'mention rule reply disallow',\n    )\n    const danReply = await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      post.ref,\n      'mention rule reply allow',\n    )\n    await network.processAll()\n    const {\n      data: { thread: aliceThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(aliceThread))\n    expect(aliceThread.post.viewer?.replyDisabled).toBe(true)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)\n    const {\n      data: { thread: danThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(danThread))\n    expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()\n    expect(danThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)\n    const [reply, ...otherReplies] = danThread.replies ?? []\n    assert(isThreadViewPost(reply))\n    expect(otherReplies.length).toEqual(0)\n    expect(reply.post.uri).toEqual(danReply.ref.uriStr)\n  })\n\n  it('applies gate for following rule.', async () => {\n    const post = await sc.post(sc.dids.carol, 'following rule')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    // carol only follows alice\n    await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      post.ref,\n      'following rule reply disallow',\n    )\n    const aliceReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'following rule reply allow',\n    )\n    await network.processAll()\n    const {\n      data: { thread: danThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(danThread))\n    expect(danThread.post.viewer?.replyDisabled).toBe(true)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, true)\n    const {\n      data: { thread: aliceThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(aliceThread))\n    expect(forSnapshot(aliceThread.post.threadgate)).toMatchSnapshot()\n    expect(aliceThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)\n    const [reply, ...otherReplies] = aliceThread.replies ?? []\n    assert(isThreadViewPost(reply))\n    expect(otherReplies.length).toEqual(0)\n    expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)\n  })\n\n  it('applies gate for follower rule.', async () => {\n    const post = await sc.post(sc.dids.carol, 'follower rule')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [{ $type: 'app.bsky.feed.threadgate#followerRule' }],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n\n    // dan does not follow carol, can't reply\n    await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      post.ref,\n      'follower rule reply disallow',\n    )\n\n    // alice follows carol, can reply\n    const aliceReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'follower rule reply allow',\n    )\n    await network.processAll()\n    const {\n      data: { thread: danThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(danThread))\n    expect(danThread.post.viewer?.replyDisabled).toBe(true)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, true)\n    const {\n      data: { thread: aliceThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(aliceThread))\n    expect(forSnapshot(aliceThread.post.threadgate)).toMatchSnapshot()\n    expect(aliceThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)\n    const [reply, ...otherReplies] = aliceThread.replies ?? []\n    assert(isThreadViewPost(reply))\n    expect(otherReplies.length).toEqual(0)\n    expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)\n  })\n\n  it('applies gate for list rule.', async () => {\n    const post = await sc.post(sc.dids.carol, 'list rule')\n    // setup lists to allow alice and dan\n    const listA = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: sc.dids.carol },\n      {\n        name: 'list a',\n        purpose: 'app.bsky.graph.defs#modlist',\n        createdAt: iso(),\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: sc.dids.carol },\n      {\n        list: listA.uri,\n        subject: sc.dids.alice,\n        createdAt: iso(),\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    const listB = await pdsAgent.api.app.bsky.graph.list.create(\n      { repo: sc.dids.carol },\n      {\n        name: 'list b',\n        purpose: 'app.bsky.graph.defs#modlist',\n        createdAt: iso(),\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await pdsAgent.api.app.bsky.graph.listitem.create(\n      { repo: sc.dids.carol },\n      {\n        list: listB.uri,\n        subject: sc.dids.dan,\n        createdAt: iso(),\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [\n          { $type: 'app.bsky.feed.threadgate#listRule', list: listA.uri },\n          { $type: 'app.bsky.feed.threadgate#listRule', list: listB.uri },\n        ],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    //\n    await sc.reply(sc.dids.bob, post.ref, post.ref, 'list rule reply disallow')\n    const aliceReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'list rule reply allow (list a)',\n    )\n    const danReply = await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      post.ref,\n      'list rule reply allow (list b)',\n    )\n    await network.processAll()\n    const {\n      data: { thread: bobThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(bobThread))\n    expect(bobThread.post.viewer?.replyDisabled).toBe(true)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.bob, true)\n    const {\n      data: { thread: aliceThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(aliceThread))\n    expect(aliceThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)\n    const {\n      data: { thread: danThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(danThread))\n    expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()\n    expect(danThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)\n    const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? []\n    assert(isThreadViewPost(reply1))\n    assert(isThreadViewPost(reply2))\n    expect(otherReplies.length).toEqual(0)\n    expect([reply1.post.uri, reply2.post.uri].sort()).toEqual(\n      [danReply.ref.uriStr, aliceReply.ref.uriStr].sort(),\n    )\n  })\n\n  it('applies gate for unknown list rule.', async () => {\n    const post = await sc.post(sc.dids.carol, 'unknown list rules')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [\n          {\n            $type: 'app.bsky.feed.threadgate#listRule',\n            list: post.ref.uriStr, // bad list link, references a post\n          },\n        ],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'unknown list rules reply',\n    )\n    await network.processAll()\n    const {\n      data: { thread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(thread))\n    expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()\n    expect(thread.post.viewer?.replyDisabled).toBe(true)\n    expect(thread.replies?.length).toEqual(0)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)\n  })\n\n  it('applies gate for multiple rules.', async () => {\n    const post = await sc.post(sc.dids.carol, 'multi rules @dan.test', [\n      {\n        index: { byteStart: 12, byteEnd: 21 },\n        features: [\n          { $type: 'app.bsky.richtext.facet#mention', did: sc.dids.dan },\n        ],\n      },\n    ])\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [\n          { $type: 'app.bsky.feed.threadgate#mentionRule' },\n          { $type: 'app.bsky.feed.threadgate#followerRule' },\n          { $type: 'app.bsky.feed.threadgate#followingRule' },\n        ],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n\n    await sc.reply(sc.dids.eve, post.ref, post.ref, 'multi rule reply disallow')\n    const bobReply = await sc.reply(\n      sc.dids.bob,\n      post.ref,\n      post.ref,\n      'multi rule reply allow (follower)',\n    )\n    const aliceReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'multi rule reply allow (following)',\n    )\n    const danReply = await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      post.ref,\n      'multi rule reply allow (mention)',\n    )\n    await network.processAll()\n\n    const {\n      data: { thread: eveThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.eve,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(eveThread))\n    // eve cannot interact\n    expect(eveThread.post.viewer?.replyDisabled).toBe(true)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.eve, true)\n\n    const {\n      data: { thread: bobThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.bob,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(bobThread))\n    // bob follows carol, followers can reply\n    expect(bobThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.bob, false)\n\n    const {\n      data: { thread: aliceThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(aliceThread))\n    // carol follows alice, followed users can reply\n    expect(aliceThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)\n\n    const {\n      data: { thread: danThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(danThread))\n    expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()\n    // dan was mentioned, mentioned users can reply\n    expect(danThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)\n\n    const [reply1, reply2, reply3, ...otherReplies] = aliceThread.replies ?? []\n    assert(isThreadViewPost(reply1))\n    assert(isThreadViewPost(reply2))\n    assert(isThreadViewPost(reply3))\n    expect(otherReplies.length).toEqual(0)\n    expect([reply1.post.uri, reply2.post.uri, reply3.post.uri].sort()).toEqual(\n      [aliceReply.ref.uriStr, danReply.ref.uriStr, bobReply.ref.uriStr].sort(),\n    )\n  })\n\n  it('applies gate for missing rules, takes no action.', async () => {\n    const post = await sc.post(sc.dids.carol, 'missing rules')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      { post: post.ref.uriStr, createdAt: iso() },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    const aliceReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'missing rules reply',\n    )\n    await network.processAll()\n    const {\n      data: { thread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(thread))\n    expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()\n    expect(thread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)\n    const [reply, ...otherReplies] = thread.replies ?? []\n    assert(isThreadViewPost(reply))\n    expect(otherReplies.length).toEqual(0)\n    expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)\n  })\n\n  it('applies gate after root post is deleted.', async () => {\n    // @NOTE also covers rule application more than one level deep\n    const post = await sc.post(sc.dids.carol, 'following rule w/ post deletion')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    // carol only follows alice\n    const orphanedReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      post.ref,\n      'following rule reply allow',\n    )\n    await pdsAgent.api.app.bsky.feed.post.delete(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      orphanedReply.ref,\n      'following rule reply disallow',\n    )\n    const aliceReply = await sc.reply(\n      sc.dids.alice,\n      post.ref,\n      orphanedReply.ref,\n      'following rule reply allow',\n    )\n    await network.processAll()\n    const {\n      data: { thread: danThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: orphanedReply.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.dan,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(danThread))\n    expect(danThread.post.viewer?.replyDisabled).toBe(true)\n    await checkReplyDisabled(orphanedReply.ref.uriStr, sc.dids.dan, true)\n    const {\n      data: { thread: aliceThread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: orphanedReply.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(aliceThread))\n    assert(\n      isNotFoundPost(aliceThread.parent) &&\n        aliceThread.parent.uri === post.ref.uriStr,\n    )\n    expect(aliceThread.post.threadgate).toMatchSnapshot()\n    expect(aliceThread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(orphanedReply.ref.uriStr, sc.dids.alice, false)\n    const [reply, ...otherReplies] = aliceThread.replies ?? []\n    assert(isThreadViewPost(reply))\n    expect(otherReplies.length).toEqual(0)\n    expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)\n  })\n\n  it('does not apply gate to original poster.', async () => {\n    const post = await sc.post(sc.dids.carol, 'empty rules')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      { post: post.ref.uriStr, createdAt: iso(), allow: [] },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    const selfReply = await sc.reply(\n      sc.dids.carol,\n      post.ref,\n      post.ref,\n      'empty rules reply allow',\n    )\n    await network.processAll()\n    const {\n      data: { thread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: post.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.carol,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(thread))\n    expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()\n    expect(thread.post.viewer?.replyDisabled).toBe(false)\n    await checkReplyDisabled(post.ref.uriStr, sc.dids.carol, false)\n    const [reply, ...otherReplies] = thread.replies ?? []\n    assert(isThreadViewPost(reply))\n    expect(otherReplies.length).toEqual(0)\n    expect(reply.post.uri).toEqual(selfReply.ref.uriStr)\n  })\n\n  it('displays gated posts in feed and thread anchor without reply context.', async () => {\n    const post = await sc.post(sc.dids.carol, 'following rule')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: post.ref.uri.rkey },\n      {\n        post: post.ref.uriStr,\n        createdAt: iso(),\n        allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }],\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    // carol only follows alice\n    const badReply = await sc.reply(\n      sc.dids.dan,\n      post.ref,\n      post.ref,\n      'following rule reply disallow',\n    )\n    // going to ensure this one doesn't appear in badReply's thread\n    await sc.reply(sc.dids.alice, post.ref, badReply.ref, 'reply to disallowed')\n    await network.processAll()\n    // check thread view\n    const {\n      data: { thread },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: badReply.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(thread))\n    expect(thread.post.viewer?.replyDisabled).toBe(true) // nobody can reply to this, not even alice.\n    expect(thread.replies).toBeUndefined()\n    expect(thread.parent).toBeUndefined()\n    expect(thread.post.threadgate).toBeUndefined()\n    await checkReplyDisabled(badReply.ref.uriStr, sc.dids.alice, true)\n    // check feed view\n    const {\n      data: { feed },\n    } = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: sc.dids.dan },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetAuthorFeed,\n        ),\n      },\n    )\n    const [feedItem] = feed\n    expect(feedItem.post.uri).toEqual(badReply.ref.uriStr)\n    expect(feedItem.post.threadgate).toBeUndefined()\n    expect(feedItem.reply).toBeUndefined()\n  })\n\n  it('does not apply gate unless it matches post rkey.', async () => {\n    const postA = await sc.post(sc.dids.carol, 'ungated a')\n    const postB = await sc.post(sc.dids.carol, 'ungated b')\n    await pdsAgent.api.app.bsky.feed.threadgate.create(\n      { repo: sc.dids.carol, rkey: postA.ref.uri.rkey },\n      { post: postB.ref.uriStr, createdAt: iso(), allow: [] },\n      sc.getHeaders(sc.dids.carol),\n    )\n    await network.processAll()\n    await sc.reply(sc.dids.alice, postA.ref, postA.ref, 'ungated reply')\n    await sc.reply(sc.dids.alice, postB.ref, postB.ref, 'ungated reply')\n    await network.processAll()\n    const {\n      data: { thread: threadA },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: postA.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(threadA))\n    expect(threadA.post.threadgate).toBeUndefined()\n    expect(threadA.post.viewer?.replyDisabled).toBeUndefined()\n    expect(threadA.replies?.length).toEqual(1)\n    await checkReplyDisabled(postA.ref.uriStr, sc.dids.alice, undefined)\n    const {\n      data: { thread: threadB },\n    } = await agent.api.app.bsky.feed.getPostThread(\n      { uri: postB.ref.uriStr },\n      {\n        headers: await network.serviceHeaders(\n          sc.dids.alice,\n          ids.AppBskyFeedGetPostThread,\n        ),\n      },\n    )\n    assert(isThreadViewPost(threadB))\n    expect(threadB.post.threadgate).toBeUndefined()\n    expect(threadB.post.viewer?.replyDisabled).toBe(undefined)\n    await checkReplyDisabled(postB.ref.uriStr, sc.dids.alice, undefined)\n    expect(threadB.replies?.length).toEqual(1)\n  })\n})\n\nconst iso = (date = new Date()) => date.toISOString()\n"
  },
  {
    "path": "packages/bsky/tests/views/timeline.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport {\n  EXAMPLE_LABELER,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { Database } from '../../src'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs'\nimport { OutputSchema as GetTimelineOutputSchema } from '../../src/lexicon/types/app/bsky/feed/getTimeline'\nimport { forSnapshot, getOriginator, paginateAll } from '../_util'\n\nconst REVERSE_CHRON = 'reverse-chronological'\n\ndescribe('timeline views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_home_feed',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    // covers label hydration on embeds\n    const { db } = network.bsky\n    await createLabel(db, {\n      val: 'test-label-3',\n      uri: sc.posts[bob][0].ref.uriStr,\n      cid: sc.posts[bob][0].ref.cidStr,\n    })\n    await createLabel(db, {\n      val: 'test-label-3',\n      uri: sc.posts[carol][0].ref.uriStr,\n      cid: sc.posts[carol][0].ref.cidStr,\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  // @TODO(bsky) blocks posts, reposts, replies by actor takedown via labels\n  // @TODO(bsky) blocks posts, reposts, replies by record takedown via labels\n\n  it(\"fetches authenticated user's home feed w/ reverse-chronological algorithm\", async () => {\n    const expectOriginatorFollowedBy = (did) => (item: FeedViewPost) => {\n      const originator = getOriginator(item)\n      // The user expects to see posts & reposts from themselves and follows\n      if (did !== originator) {\n        expect(sc.follows[did]).toHaveProperty(originator)\n      }\n    }\n\n    const aliceTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot()\n    aliceTL.data.feed.forEach(expectOriginatorFollowedBy(alice))\n\n    const bobTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetTimeline),\n      },\n    )\n\n    expect(forSnapshot(bobTL.data.feed)).toMatchSnapshot()\n    bobTL.data.feed.forEach(expectOriginatorFollowedBy(bob))\n\n    const carolTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    expect(forSnapshot(carolTL.data.feed)).toMatchSnapshot()\n    carolTL.data.feed.forEach(expectOriginatorFollowedBy(carol))\n\n    const danTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),\n      },\n    )\n\n    expect(forSnapshot(danTL.data.feed)).toMatchSnapshot()\n    danTL.data.feed.forEach(expectOriginatorFollowedBy(dan))\n  })\n\n  it(\"fetches authenticated user's home feed w/ default algorithm\", async () => {\n    const defaultTL = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    const reverseChronologicalTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(defaultTL.data.feed).toEqual(reverseChronologicalTL.data.feed)\n  })\n\n  it('paginates reverse-chronological feed', async () => {\n    const results = (results: GetTimelineOutputSchema[]) =>\n      results.flatMap((res) => res.feed)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.app.bsky.feed.getTimeline(\n        {\n          algorithm: REVERSE_CHRON,\n          cursor,\n          limit: 4,\n        },\n        {\n          headers: await network.serviceHeaders(\n            carol,\n            ids.AppBskyFeedGetTimeline,\n          ),\n        },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.feed.length).toBeLessThanOrEqual(4),\n    )\n\n    const full = await agent.api.app.bsky.feed.getTimeline(\n      {\n        algorithm: REVERSE_CHRON,\n      },\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    expect(full.data.feed.length).toEqual(7)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('agrees what the first item is for limit=1 and other limits', async () => {\n    const { data: timeline } = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 10 },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    const { data: timelineLimit1 } = await agent.api.app.bsky.feed.getTimeline(\n      { limit: 1 },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(timeline.feed.length).toBeGreaterThan(1)\n    expect(timelineLimit1.feed.length).toEqual(1)\n    expect(timelineLimit1.feed[0].post.uri).toBe(timeline.feed[0].post.uri)\n  })\n\n  it('reflects self-labels', async () => {\n    const carolTL = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      {\n        headers: await network.serviceHeaders(\n          carol,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    const alicePost = carolTL.data.feed.find(\n      ({ post }) => post.uri === sc.posts[alice][0].ref.uriStr,\n    )?.post\n\n    assert(alicePost, 'post does not exist')\n\n    const postSelfLabels = alicePost.labels\n      ?.filter((label) => label.src === alice)\n      .map((label) => label.val)\n\n    expect(postSelfLabels).toEqual(['self-label'])\n\n    const authorSelfLabels = alicePost.author.labels\n      ?.filter((label) => label.src === alice)\n      .map((label) => label.val)\n      .sort()\n\n    expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b'])\n  })\n\n  it('blocks posts, reposts, replies by actor takedown', async () => {\n    await Promise.all(\n      [bob, carol].map((did) =>\n        network.bsky.ctx.dataplane.takedownActor({ did }),\n      ),\n    )\n\n    const aliceTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot()\n\n    // Cleanup\n    await Promise.all(\n      [bob, carol].map((did) =>\n        network.bsky.ctx.dataplane.untakedownActor({ did }),\n      ),\n    )\n  })\n\n  it('blocks posts, reposts, replies by record takedown.', async () => {\n    const postRef1 = sc.posts[dan][1].ref // Repost\n    const postRef2 = sc.replies[bob][0].ref // Post and reply parent\n    await Promise.all(\n      [postRef1, postRef2].map((postRef) =>\n        network.bsky.ctx.dataplane.takedownRecord({\n          recordUri: postRef.uriStr,\n        }),\n      ),\n    )\n\n    const aliceTL = await agent.api.app.bsky.feed.getTimeline(\n      { algorithm: REVERSE_CHRON },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n\n    expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot()\n\n    // Cleanup\n    await Promise.all(\n      [postRef1, postRef2].map((postRef) =>\n        network.bsky.ctx.dataplane.untakedownRecord({\n          recordUri: postRef.uriStr,\n        }),\n      ),\n    )\n  })\n\n  it('fails open on clearly bad cursor.', async () => {\n    const { data: timeline } = await agent.api.app.bsky.feed.getTimeline(\n      { cursor: '90210::bafycid' },\n      {\n        headers: await network.serviceHeaders(\n          alice,\n          ids.AppBskyFeedGetTimeline,\n        ),\n      },\n    )\n    expect(timeline).toEqual({ feed: [] })\n  })\n})\n\nconst createLabel = async (\n  db: Database,\n  opts: { uri: string; cid: string; val: string },\n) => {\n  await db.db\n    .insertInto('label')\n    .values({\n      uri: opts.uri,\n      cid: opts.cid,\n      val: opts.val,\n      cts: new Date().toISOString(),\n      exp: null,\n      neg: false,\n      src: EXAMPLE_LABELER,\n    })\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsky/tests/views/verification.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, verificationsSeed } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport { VerificationState } from '../../src/lexicon/types/app/bsky/actor/defs'\n\ninterface ProfileViewTestCase {\n  description: string\n  // The DIDs are only set during test setup, so data that depends on those DIDs\n  // needs to be lazily evaluated by using a function.\n  getDid: () => string\n  getExpected: () => VerificationState | undefined\n  getExpectedUrisPrefixes?: () => string[]\n}\n\ndescribe('verification views', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let labelerDid: string\n  let sc: SeedClient<TestNetwork>\n\n  // account dids, for convenience\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n  let eve: string\n  let frank: string\n  let gus: string\n  let impersonator: string\n  let verifier1: string\n  let verifier2: string\n  let verifier3: string\n  let handleinvalid: string\n  let handleempty: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'bsky_views_verification',\n    })\n    agent = network.bsky.getClient()\n    sc = network.getSeedClient()\n\n    await verificationsSeed(sc)\n    await network.processAll()\n\n    labelerDid = network.bsky.ctx.cfg.modServiceDid\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    eve = sc.dids.eve\n    frank = sc.dids.frank\n    gus = sc.dids.gus\n    impersonator = sc.dids.impersonator\n    verifier1 = sc.dids.verifier1\n    verifier2 = sc.dids.verifier2\n    verifier3 = sc.dids.verifier3\n    handleinvalid = sc.dids.handleinvalid\n    handleempty = sc.dids.handleempty\n\n    await network.bsky.db.db\n      .updateTable('actor')\n      .set({ trustedVerifier: true })\n      .where('did', 'in', [verifier1, verifier2, verifier3])\n      .execute()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('profile views', () => {\n    const testCases: ProfileViewTestCase[] = [\n      {\n        description: 'returns trusted verifier that has verifications',\n        getDid: () => verifier1,\n        getExpected: () => ({\n          verifications: [\n            {\n              createdAt: expect.any(String),\n              isValid: true,\n              issuer: verifier2,\n              uri: expect.any(String),\n            },\n          ],\n          verifiedStatus: 'valid',\n          trustedVerifierStatus: 'valid',\n        }),\n        getExpectedUrisPrefixes: () => [\n          `at://${verifier2}/app.bsky.graph.verification/`,\n        ],\n      },\n      {\n        description: 'returns trusted verifier that has no verifications',\n        getDid: () => verifier2,\n        getExpected: () => ({\n          verifications: [],\n          verifiedStatus: 'none',\n          trustedVerifierStatus: 'valid',\n        }),\n      },\n      {\n        description: 'returns trusted verifier with impersonation',\n        getDid: () => verifier3,\n        getExpected: () => ({\n          verifications: [],\n          verifiedStatus: 'none',\n          trustedVerifierStatus: 'invalid',\n        }),\n      },\n      {\n        description: 'returns verified with multiple verifications',\n        getDid: () => bob,\n        getExpected: () => ({\n          verifications: [\n            {\n              createdAt: expect.any(String),\n              isValid: true,\n              issuer: verifier1,\n              uri: expect.any(String),\n            },\n            {\n              createdAt: expect.any(String),\n              isValid: true,\n              issuer: verifier2,\n              uri: expect.any(String),\n            },\n          ],\n          verifiedStatus: 'valid',\n          trustedVerifierStatus: 'none',\n        }),\n        getExpectedUrisPrefixes: () => [\n          `at://${verifier1}/app.bsky.graph.verification/`,\n          `at://${verifier2}/app.bsky.graph.verification/`,\n        ],\n      },\n      {\n        description: 'returns verified with mixed valid/invalid verifications',\n        getDid: () => carol,\n        getExpected: () => ({\n          verifications: [\n            {\n              createdAt: expect.any(String),\n              isValid: true,\n              issuer: verifier1,\n              uri: expect.any(String),\n            },\n            {\n              createdAt: expect.any(String),\n              isValid: false,\n              issuer: verifier2,\n              uri: expect.any(String),\n            },\n          ],\n          verifiedStatus: 'valid',\n          trustedVerifierStatus: 'none',\n        }),\n        getExpectedUrisPrefixes: () => [\n          `at://${verifier1}/app.bsky.graph.verification/`,\n          `at://${verifier2}/app.bsky.graph.verification/`,\n        ],\n      },\n      {\n        description: 'returns verified excluding non-verifier verifications',\n        getDid: () => dan,\n        getExpected: () => ({\n          verifications: [\n            {\n              createdAt: expect.any(String),\n              isValid: true,\n              issuer: verifier1,\n              uri: expect.any(String),\n            },\n            // It has a verification by a non-verifier, which is not included.\n          ],\n          verifiedStatus: 'valid',\n          trustedVerifierStatus: 'none',\n        }),\n        getExpectedUrisPrefixes: () => [\n          `at://${verifier1}/app.bsky.graph.verification/`,\n        ],\n      },\n      {\n        description: 'returns undefined for user with no verifications at all',\n        getDid: () => eve,\n        getExpected: () => undefined,\n      },\n      {\n        description:\n          'returns unverified with only invalid verifications from verifiers',\n        getDid: () => frank,\n        getExpected: () => ({\n          verifications: [\n            {\n              createdAt: expect.any(String),\n              isValid: false,\n              issuer: verifier2,\n              uri: expect.any(String),\n            },\n          ],\n          verifiedStatus: 'invalid',\n          trustedVerifierStatus: 'none',\n        }),\n        getExpectedUrisPrefixes: () => [\n          `at://${verifier2}/app.bsky.graph.verification/`,\n        ],\n      },\n      {\n        description:\n          'returns unverified for user with only verifications by non-verifiers',\n        getDid: () => gus,\n        getExpected: () => undefined,\n      },\n      {\n        description:\n          'returns invalid verified for impersonator, but includes verifications',\n        getDid: () => impersonator,\n        getExpected: () => ({\n          verifications: [\n            {\n              createdAt: expect.any(String),\n              isValid: true,\n              issuer: verifier1,\n              uri: expect.any(String),\n            },\n          ],\n          verifiedStatus: 'invalid',\n          trustedVerifierStatus: 'none',\n        }),\n        getExpectedUrisPrefixes: () => [\n          `at://${verifier1}/app.bsky.graph.verification/`,\n        ],\n      },\n      {\n        description:\n          'returns undefined for user with invalid handle even if they have verifications',\n        getDid: () => handleinvalid,\n        getExpected: () => undefined,\n      },\n      {\n        description:\n          'returns undefined for user with empty handle even if they have verifications',\n        getDid: () => handleempty,\n        getExpected: () => undefined,\n      },\n    ]\n\n    it.each(testCases)(\n      '$description',\n      async ({ getDid, getExpected, getExpectedUrisPrefixes = () => [] }) => {\n        const profile = await getProfile(getDid())\n\n        expect(profile.verification).toStrictEqual(getExpected())\n\n        const urlPrefixes = getExpectedUrisPrefixes()\n        profile.verification &&\n          expect(urlPrefixes.length).toBe(\n            profile.verification.verifications.length,\n          )\n        urlPrefixes.forEach((prefix, i) => {\n          assert(profile.verification)\n          expect(\n            profile.verification.verifications[i].uri.startsWith(prefix),\n          ).toBe(true)\n        })\n      },\n    )\n  })\n\n  const getProfile = async (actor: string) => {\n    const res = await agent.app.bsky.actor.getProfile(\n      { actor },\n      {\n        headers: {\n          ...(await network.serviceHeaders(alice, ids.AppBskyActorGetProfile)),\n          'atproto-accept-labelers': `${labelerDid};redact`,\n        },\n      },\n    )\n    return res.data\n  }\n})\n"
  },
  {
    "path": "packages/bsky/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/bsky/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/bsky/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/bsync/CHANGELOG.md",
    "content": "# @atproto/bsync\n\n## 0.0.25\n\n### Patch Changes\n\n- [#4745](https://github.com/bluesky-social/atproto/pull/4745) [`efb2b58`](https://github.com/bluesky-social/atproto/commit/efb2b58fb27a242d5308bf15916ee222daa2019b) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `delete-operations` file and new `deleteOperationsByActorAndNamespace` handler\n\n- Updated dependencies [[`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f)]:\n  - @atproto/syntax@0.5.1\n\n## 0.0.24\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common@0.5.14\n\n## 0.0.23\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common@0.5.0\n\n## 0.0.22\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.12\n\n## 0.0.21\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/syntax@0.4.1\n\n## 0.0.20\n\n### Patch Changes\n\n- [#3912](https://github.com/bluesky-social/atproto/pull/3912) [`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Unify `getPostThreadV2` and `getPostThreadHiddenV2` responses under `app.bsky.unspecced.defs` namespace and a single interface via `threadItemPost`.\n\n## 0.0.19\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.11\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common@0.4.10\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/common@0.4.9\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n\n## 0.0.15\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/syntax@0.3.4\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/syntax@0.3.3\n\n## 0.0.13\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/common@0.4.8\n  - @atproto/syntax@0.3.2\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/common@0.4.7\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.6\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4)]:\n  - @atproto/common@0.4.5\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/syntax@0.3.1\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]:\n  - @atproto/common@0.4.4\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/common@0.4.3\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/common@0.4.2\n\n## 0.0.5\n\n### Patch Changes\n\n- [#2648](https://github.com/bluesky-social/atproto/pull/2648) [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b) Thanks [@dholms](https://github.com/dholms)! - Support for priority notifications\n\n## 0.0.4\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Obfuscate request headers in logs using utils from @atproto/common\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/common@0.4.1\n\n## 0.0.3\n\n### Patch Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common@0.4.0\n  - @atproto/syntax@0.3.0\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.3.4\n  - @atproto/syntax@0.2.1\n\n## 0.0.1\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4)]:\n  - @atproto/syntax@0.2.0\n"
  },
  {
    "path": "packages/bsync/README.md",
    "content": "# @atproto/bsync: Synchronizing Service for the Bluesky AppView\n\nThis is an optional service that may be used to synchronize certain state between otherwise independent AppViews.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/bsync)](https://www.npmjs.com/package/@atproto/bsync)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/bsync/bin/migration-create.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\n\nexport async function main() {\n  const now = new Date()\n  const prefix = now.toISOString().replace(/[^a-z0-9]/gi, '') // Order of migrations matches alphabetical order of their names\n  const name = process.argv[2]\n  if (!name || !name.match(/^[a-z0-9-]+$/)) {\n    process.exitCode = 1\n    return console.error(\n      'Must pass a migration name consisting of lowercase digits, numbers, and dashes.',\n    )\n  }\n  const filename = `${prefix}-${name}`\n  const dir = path.join(__dirname, '..', 'src', 'db', 'migrations')\n\n  await fs.writeFile(path.join(dir, `${filename}.ts`), template, { flag: 'wx' })\n  await fs.writeFile(\n    path.join(dir, 'index.ts'),\n    `export * as _${prefix} from './${filename}'\\n`,\n    { flag: 'a' },\n  )\n}\n\nconst template = `import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Migration code\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  // Migration code\n}\n`\n\nmain()\n"
  },
  {
    "path": "packages/bsync/buf.gen.yaml",
    "content": "version: v1\nplugins:\n  - plugin: es\n    opt:\n      - target=ts\n      - import_extension=\n    out: src/proto\n  - plugin: connect-es\n    opt:\n      - target=ts\n      - import_extension=\n    out: src/proto\n"
  },
  {
    "path": "packages/bsync/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Bsync',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/bsync/package.json",
    "content": "{\n  \"name\": \"@atproto/bsync\",\n  \"version\": \"0.0.25\",\n  \"license\": \"MIT\",\n  \"description\": \"Sychronizing service for app.bsky App View (Bluesky API)\",\n  \"keywords\": [\n    \"atproto\",\n    \"bluesky\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/bsync\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"start\": \"node --enable-source-maps dist/bin.js\",\n    \"test\": \"../dev-infra/with-test-db.sh jest\",\n    \"test:log\": \"tail -50 test.log | pino-pretty\",\n    \"test:updateSnapshot\": \"jest --updateSnapshot\",\n    \"migration:create\": \"ts-node ./bin/migration-create.ts\",\n    \"buf:gen\": \"buf generate proto\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@bufbuild/protobuf\": \"^1.5.0\",\n    \"@connectrpc/connect\": \"^1.1.4\",\n    \"@connectrpc/connect-node\": \"^1.1.4\",\n    \"http-terminator\": \"^3.2.0\",\n    \"kysely\": \"^0.22.0\",\n    \"pg\": \"^8.10.0\",\n    \"pino-http\": \"^8.2.1\",\n    \"typed-emitter\": \"^2.1.0\"\n  },\n  \"devDependencies\": {\n    \"@bufbuild/buf\": \"^1.28.1\",\n    \"@bufbuild/protoc-gen-es\": \"^1.5.0\",\n    \"@connectrpc/protoc-gen-connect-es\": \"^1.1.4\",\n    \"@types/pg\": \"^8.6.6\",\n    \"get-port\": \"^5.1.1\",\n    \"jest\": \"^28.1.2\",\n    \"ts-node\": \"^10.8.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/bsync/proto/bsync.proto",
    "content": "syntax = \"proto3\";\n\npackage bsync;\noption go_package = \"./;bsync\";\n\n//\n// Sync\n//\n\n\nmessage MuteOperation {\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    TYPE_ADD = 1;\n    TYPE_REMOVE = 2;\n    TYPE_CLEAR = 3;\n  }\n  string id = 1;\n  Type type = 2;\n  string actor_did = 3;\n  string subject = 4;\n}\n\nmessage AddMuteOperationRequest {\n  MuteOperation.Type type = 1;\n  string actor_did = 2;\n  string subject = 3;\n}\n\nmessage AddMuteOperationResponse {\n  MuteOperation operation = 1;\n}\n\nmessage ScanMuteOperationsRequest {\n  string cursor = 1;\n  int32 limit = 2;\n}\n\nmessage ScanMuteOperationsResponse {\n  repeated MuteOperation operations = 1;\n  string cursor = 2;\n}\n\nmessage NotifOperation {\n  string id = 1;\n  string actor_did = 2;\n  optional bool priority = 3;\n}\n\nmessage AddNotifOperationRequest {\n  string actor_did = 1;\n  optional bool priority = 2;\n}\n\nmessage AddNotifOperationResponse {\n  NotifOperation operation = 1;\n}\n\nmessage ScanNotifOperationsRequest {\n  string cursor = 1;\n  int32 limit = 2;\n}\n\nmessage ScanNotifOperationsResponse {\n  repeated NotifOperation operations = 1;\n  string cursor = 2;\n}\n\n\nenum Method {\n  METHOD_UNSPECIFIED = 0;\n  METHOD_CREATE = 1;\n  METHOD_UPDATE = 2;\n  METHOD_DELETE = 3;\n}\n\nmessage Operation {\n  string id = 1;\n  string actor_did = 2;\n  string namespace = 3;\n  string key = 4;\n  Method method = 5;\n  bytes payload = 6;\n}\n\nmessage PutOperationRequest {\n  string actor_did = 1;\n  string namespace = 2;\n  string key = 3;\n  Method method = 4;\n  bytes payload = 5;\n}\n\nmessage PutOperationResponse {\n  Operation operation = 1;\n}\n\nmessage ScanOperationsRequest {\n  string cursor = 1;\n  int32 limit = 2;\n}\n\nmessage ScanOperationsResponse {\n  repeated Operation operations = 1;\n  string cursor = 2;\n}\n\nmessage DeleteOperationsByActorAndNamespaceRequest {\n  string actor_did = 1;\n  string namespace = 2;\n}\n\nmessage DeleteOperationsByActorAndNamespaceResponse {\n  int32 deleted_count = 1;\n}\n\n\n// Ping\nmessage PingRequest {}\nmessage PingResponse {}\n\n\nservice Service {\n  // Sync\n  rpc AddMuteOperation(AddMuteOperationRequest) returns (AddMuteOperationResponse);\n  rpc ScanMuteOperations(ScanMuteOperationsRequest) returns (ScanMuteOperationsResponse);\n  rpc AddNotifOperation(AddNotifOperationRequest) returns (AddNotifOperationResponse);\n  rpc ScanNotifOperations(ScanNotifOperationsRequest) returns (ScanNotifOperationsResponse);\n  rpc PutOperation(PutOperationRequest) returns (PutOperationResponse);\n  rpc ScanOperations(ScanOperationsRequest) returns (ScanOperationsResponse);\n  rpc DeleteOperationsByActorAndNamespace(DeleteOperationsByActorAndNamespaceRequest) returns (DeleteOperationsByActorAndNamespaceResponse);\n  // Ping\n  rpc Ping(PingRequest) returns (PingResponse);\n}\n"
  },
  {
    "path": "packages/bsync/src/client.ts",
    "content": "import {\n  Interceptor,\n  PromiseClient,\n  createPromiseClient,\n} from '@connectrpc/connect'\nimport {\n  ConnectTransportOptions,\n  createConnectTransport,\n} from '@connectrpc/connect-node'\nimport { Service } from './proto/bsync_connect'\n\nexport type BsyncClient = PromiseClient<typeof Service>\n\nexport const createClient = (opts: ConnectTransportOptions): BsyncClient => {\n  const transport = createConnectTransport(opts)\n  return createPromiseClient(Service, transport)\n}\n\nexport const authWithApiKey =\n  (apiKey: string): Interceptor =>\n  (next) =>\n  (req) => {\n    req.header.set('authorization', `Bearer ${apiKey}`)\n    return next(req)\n  }\n"
  },
  {
    "path": "packages/bsync/src/config.ts",
    "content": "import assert from 'node:assert'\nimport { envBool, envInt, envList, envStr } from '@atproto/common'\n\nexport const envToCfg = (env: ServerEnvironment): ServerConfig => {\n  const serviceCfg: ServerConfig['service'] = {\n    port: env.port ?? 2585,\n    version: env.version ?? 'unknown',\n    longPollTimeoutMs: env.longPollTimeoutMs ?? 10000,\n  }\n\n  assert(env.dbUrl, 'missing postgres url')\n  const dbCfg: ServerConfig['db'] = {\n    url: env.dbUrl,\n    schema: env.dbSchema,\n    poolSize: env.dbPoolSize,\n    poolMaxUses: env.dbPoolMaxUses,\n    poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs,\n    migrate: env.dbMigrate,\n  }\n\n  assert(env.apiKeys.length > 0, 'missing api keys')\n  const authCfg: ServerConfig['auth'] = {\n    apiKeys: new Set(env.apiKeys),\n  }\n\n  return {\n    service: serviceCfg,\n    db: dbCfg,\n    auth: authCfg,\n  }\n}\n\nexport type ServerConfig = {\n  service: ServiceConfig\n  db: DatabaseConfig\n  auth: AuthConfig\n}\n\ntype ServiceConfig = {\n  port: number\n  version?: string\n  longPollTimeoutMs: number\n}\n\ntype DatabaseConfig = {\n  url: string\n  schema?: string\n  poolSize?: number\n  poolMaxUses?: number\n  poolIdleTimeoutMs?: number\n  migrate?: boolean\n}\n\ntype AuthConfig = {\n  apiKeys: Set<string>\n}\n\nexport const readEnv = (): ServerEnvironment => {\n  return {\n    // service\n    port: envInt('BSYNC_PORT'),\n    version: envStr('BSYNC_VERSION'),\n    longPollTimeoutMs: envInt('BSYNC_LONG_POLL_TIMEOUT_MS'),\n    // database\n    dbUrl: envStr('BSYNC_DB_POSTGRES_URL'),\n    dbSchema: envStr('BSYNC_DB_POSTGRES_SCHEMA'),\n    dbPoolSize: envInt('BSYNC_DB_POOL_SIZE'),\n    dbPoolMaxUses: envInt('BSYNC_DB_POOL_MAX_USES'),\n    dbPoolIdleTimeoutMs: envInt('BSYNC_DB_POOL_IDLE_TIMEOUT_MS'),\n    dbMigrate: envBool('BSYNC_DB_MIGRATE'),\n    // secrets\n    apiKeys: envList('BSYNC_API_KEYS'),\n  }\n}\n\nexport type ServerEnvironment = {\n  // service\n  port?: number\n  version?: string\n  longPollTimeoutMs?: number\n  // database\n  dbUrl?: string\n  dbSchema?: string\n  dbPoolSize?: number\n  dbPoolMaxUses?: number\n  dbPoolIdleTimeoutMs?: number\n  dbMigrate?: boolean\n  // secrets\n  apiKeys: string[]\n}\n"
  },
  {
    "path": "packages/bsync/src/context.ts",
    "content": "import { EventEmitter } from 'node:stream'\nimport TypedEventEmitter from 'typed-emitter'\nimport { ServerConfig } from './config'\nimport { Database } from './db'\nimport { createMuteOpChannel } from './db/schema/mute_op'\nimport { createNotifOpChannel } from './db/schema/notif_op'\nimport { createOperationChannel } from './db/schema/operation'\n\nexport type AppContextOptions = {\n  db: Database\n  cfg: ServerConfig\n  shutdown: AbortSignal\n}\n\nexport class AppContext {\n  db: Database\n  cfg: ServerConfig\n  shutdown: AbortSignal\n  events: TypedEventEmitter<AppEvents>\n\n  constructor(opts: AppContextOptions) {\n    this.db = opts.db\n    this.cfg = opts.cfg\n    this.shutdown = opts.shutdown\n    this.events = new EventEmitter() as TypedEventEmitter<AppEvents>\n  }\n\n  static async fromConfig(\n    cfg: ServerConfig,\n    shutdown: AbortSignal,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<AppContext> {\n    const db = new Database({\n      url: cfg.db.url,\n      schema: cfg.db.schema,\n      poolSize: cfg.db.poolSize,\n      poolMaxUses: cfg.db.poolMaxUses,\n      poolIdleTimeoutMs: cfg.db.poolIdleTimeoutMs,\n    })\n    return new AppContext({ db, cfg, shutdown, ...overrides })\n  }\n}\n\nexport type AppEvents = {\n  [createMuteOpChannel]: () => void\n  [createNotifOpChannel]: () => void\n  [createOperationChannel]: () => void\n}\n"
  },
  {
    "path": "packages/bsync/src/db/index.ts",
    "content": "import assert from 'node:assert'\nimport { EventEmitter } from 'node:stream'\nimport {\n  Kysely,\n  KyselyPlugin,\n  Migrator,\n  PluginTransformQueryArgs,\n  PluginTransformResultArgs,\n  PostgresDialect,\n  QueryResult,\n  RootOperationNode,\n  UnknownRow,\n} from 'kysely'\nimport { Pool as PgPool, types as pgTypes } from 'pg'\nimport TypedEmitter from 'typed-emitter'\nimport { dbLogger } from '../logger'\nimport * as migrations from './migrations'\nimport { DbMigrationProvider } from './migrations/provider'\nimport { DatabaseSchema, DatabaseSchemaType } from './schema'\nimport { PgOptions } from './types'\n\nexport class Database {\n  pool: PgPool\n  db: DatabaseSchema\n  migrator: Migrator\n  txEvt = new EventEmitter() as TxnEmitter\n  destroyed = false\n\n  constructor(\n    public opts: PgOptions,\n    instances?: { db: DatabaseSchema; pool: PgPool },\n  ) {\n    // if instances are provided, use those\n    if (instances) {\n      this.db = instances.db\n      this.pool = instances.pool\n    } else {\n      // else create a pool & connect\n      const { schema, url } = opts\n      const pool =\n        opts.pool ??\n        new PgPool({\n          connectionString: url,\n          max: opts.poolSize,\n          maxUses: opts.poolMaxUses,\n          idleTimeoutMillis: opts.poolIdleTimeoutMs,\n        })\n\n      // Select count(*) and other pg bigints as js integer\n      pgTypes.setTypeParser(pgTypes.builtins.INT8, (n) => parseInt(n, 10))\n\n      // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema)\n      if (schema && !/^[a-z_]+$/i.test(schema)) {\n        throw new Error(\n          `Postgres schema must only contain [A-Za-z_]: ${schema}`,\n        )\n      }\n\n      pool.on('error', onPoolError)\n      pool.on('connect', (client) => {\n        client.on('error', onClientError)\n        if (schema) {\n          // Shared objects such as extensions will go in the public schema\n          client.query(`SET search_path TO \"${schema}\",public;`)\n        }\n      })\n\n      this.pool = pool\n      this.db = new Kysely<DatabaseSchemaType>({\n        dialect: new PostgresDialect({ pool }),\n      })\n    }\n\n    this.migrator = new Migrator({\n      db: this.db,\n      migrationTableSchema: opts.schema,\n      provider: new DbMigrationProvider(migrations),\n    })\n  }\n\n  get schema(): string | undefined {\n    return this.opts.schema\n  }\n\n  get isTransaction() {\n    return this.db.isTransaction\n  }\n\n  assertTransaction() {\n    assert(this.isTransaction, 'Transaction required')\n  }\n\n  assertNotTransaction() {\n    assert(!this.isTransaction, 'Cannot be in a transaction')\n  }\n\n  async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {\n    const leakyTxPlugin = new LeakyTxPlugin()\n    const { dbTxn, txRes } = await this.db\n      .withPlugin(leakyTxPlugin)\n      .transaction()\n      .execute(async (txn) => {\n        const dbTxn = new Database(this.opts, {\n          db: txn,\n          pool: this.pool,\n        })\n        const txRes = await fn(dbTxn)\n          .catch(async (err) => {\n            leakyTxPlugin.endTx()\n            // ensure that all in-flight queries are flushed & the connection is open\n            await dbTxn.db.getExecutor().provideConnection(noopAsync)\n            throw err\n          })\n          .finally(() => leakyTxPlugin.endTx())\n        return { dbTxn, txRes }\n      })\n    dbTxn?.txEvt.emit('commit')\n    return txRes\n  }\n\n  onCommit(fn: () => void) {\n    this.assertTransaction()\n    this.txEvt.once('commit', fn)\n  }\n\n  async close(): Promise<void> {\n    if (this.destroyed) return\n    await this.db.destroy()\n    this.destroyed = true\n  }\n\n  async migrateToOrThrow(migration: string) {\n    if (this.schema) {\n      await this.db.schema.createSchema(this.schema).ifNotExists().execute()\n    }\n    const { error, results } = await this.migrator.migrateTo(migration)\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n\n  async migrateToLatestOrThrow() {\n    if (this.schema) {\n      await this.db.schema.createSchema(this.schema).ifNotExists().execute()\n    }\n    const { error, results } = await this.migrator.migrateToLatest()\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n}\n\nexport default Database\n\nconst onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error')\nconst onClientError = (err: Error) => dbLogger.error({ err }, 'db client error')\n\n// utils\n// -------\n\nclass LeakyTxPlugin implements KyselyPlugin {\n  private txOver = false\n\n  endTx() {\n    this.txOver = true\n  }\n\n  transformQuery(args: PluginTransformQueryArgs): RootOperationNode {\n    if (this.txOver) {\n      throw new Error('tx already failed')\n    }\n    return args.node\n  }\n\n  async transformResult(\n    args: PluginTransformResultArgs,\n  ): Promise<QueryResult<UnknownRow>> {\n    return args.result\n  }\n}\n\ntype TxnEmitter = TypedEmitter<TxnEvents>\n\ntype TxnEvents = {\n  commit: () => void\n}\n\nconst noopAsync = async () => {}\n"
  },
  {
    "path": "packages/bsync/src/db/migrations/20240108T220751294Z-init.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('mute_op')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('type', 'int2', (col) => col.notNull()) // integer enum: 0->add, 1->remove, 2->clear\n    .addColumn('actorDid', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'timestamptz', (col) =>\n      col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),\n    )\n    .execute()\n  await db.schema\n    .createTable('mute_item')\n    .addColumn('actorDid', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar', (col) => col.notNull())\n    .addColumn('fromId', 'bigint', (col) => col.notNull())\n    .addPrimaryKeyConstraint('mute_item_pkey', ['actorDid', 'subject'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('mute_item').execute()\n  await db.schema.dropTable('mute_op').execute()\n}\n"
  },
  {
    "path": "packages/bsync/src/db/migrations/20240717T224303472Z-notif-ops.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('notif_op')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('actorDid', 'varchar', (col) => col.notNull())\n    .addColumn('priority', 'boolean')\n    .addColumn('createdAt', 'timestamptz', (col) =>\n      col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),\n    )\n    .execute()\n  await db.schema\n    .createTable('notif_item')\n    .addColumn('actorDid', 'varchar', (col) => col.primaryKey())\n    .addColumn('priority', 'boolean', (col) => col.notNull())\n    .addColumn('fromId', 'bigint', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('notif_item').execute()\n  await db.schema.dropTable('notif_op').execute()\n}\n"
  },
  {
    "path": "packages/bsync/src/db/migrations/20250527T022203400Z-add-operation.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('operation')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('collection', 'varchar', (col) => col.notNull())\n    .addColumn('actorDid', 'varchar', (col) => col.notNull())\n    .addColumn('rkey', 'varchar', (col) => col.notNull())\n    .addColumn('method', 'int2', (col) => col.notNull())\n    .addColumn('payload', sql`bytea`)\n    .addColumn('createdAt', 'timestamptz', (col) =>\n      col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),\n    )\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('operation').execute()\n}\n"
  },
  {
    "path": "packages/bsync/src/db/migrations/20250603T163446567Z-alter-operation.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('operation')\n    .renameColumn('collection', 'namespace')\n    .execute()\n\n  await db.schema.alterTable('operation').renameColumn('rkey', 'key').execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('operation')\n    .renameColumn('namespace', 'collection')\n    .execute()\n\n  await db.schema.alterTable('operation').renameColumn('key', 'rkey').execute()\n}\n"
  },
  {
    "path": "packages/bsync/src/db/migrations/index.ts",
    "content": "// NOTE this file can be edited by hand, but it is also appended to by the migration:create command.\n// It's important that every migration is exported from here with the proper name. We'd simplify\n// this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process.\n\nexport * as _20240108T220751294Z from './20240108T220751294Z-init'\nexport * as _20240717T224303472Z from './20240717T224303472Z-notif-ops'\nexport * as _20250527T022203400Z from './20250527T022203400Z-add-operation'\nexport * as _20250603T163446567Z from './20250603T163446567Z-alter-operation'\n"
  },
  {
    "path": "packages/bsync/src/db/migrations/provider.ts",
    "content": "import { Migration, MigrationProvider } from 'kysely'\n\nexport class DbMigrationProvider implements MigrationProvider {\n  constructor(private migrations: Record<string, Migration>) {}\n  async getMigrations(): Promise<Record<string, Migration>> {\n    return this.migrations\n  }\n}\n"
  },
  {
    "path": "packages/bsync/src/db/schema/index.ts",
    "content": "import { Kysely } from 'kysely'\nimport * as muteItem from './mute_item'\nimport * as muteOp from './mute_op'\nimport * as notifItem from './notif_item'\nimport * as notifOp from './notif_op'\nimport * as op from './operation'\n\nexport type DatabaseSchemaType = muteItem.PartialDB &\n  muteOp.PartialDB &\n  notifItem.PartialDB &\n  notifOp.PartialDB &\n  op.PartialDB\n\nexport type DatabaseSchema = Kysely<DatabaseSchemaType>\n\nexport default DatabaseSchema\n"
  },
  {
    "path": "packages/bsync/src/db/schema/mute_item.ts",
    "content": "import { Selectable } from 'kysely'\n\nexport interface MuteItem {\n  actorDid: string\n  subject: string // did or aturi for list\n  fromId: number\n}\n\nexport type MuteItemEntry = Selectable<MuteItem>\n\nexport const tableName = 'mute_item'\n\nexport type PartialDB = { [tableName]: MuteItem }\n"
  },
  {
    "path": "packages/bsync/src/db/schema/mute_op.ts",
    "content": "import { GeneratedAlways, Selectable } from 'kysely'\nimport { MuteOperation_Type } from '../../proto/bsync_pb'\n\nexport interface MuteOp {\n  id: GeneratedAlways<number>\n  type: MuteOperation_Type // integer enum: 0->add, 1->remove, 2->clear\n  actorDid: string\n  subject: string // did or aturi for list\n  createdAt: GeneratedAlways<Date>\n}\n\nexport type MuteOpEntry = Selectable<MuteOp>\n\nexport const tableName = 'mute_op'\n\nexport type PartialDB = { [tableName]: MuteOp }\n\nexport const createMuteOpChannel = 'mute_op_create' // used with listen/notify\n"
  },
  {
    "path": "packages/bsync/src/db/schema/notif_item.ts",
    "content": "import { Selectable } from 'kysely'\n\nexport interface NotifItem {\n  actorDid: string\n  priority: boolean\n  fromId: number\n}\n\nexport type NotifItemEntry = Selectable<NotifItem>\n\nexport const tableName = 'notif_item'\n\nexport type PartialDB = { [tableName]: NotifItem }\n"
  },
  {
    "path": "packages/bsync/src/db/schema/notif_op.ts",
    "content": "import { GeneratedAlways, Selectable } from 'kysely'\n\nexport interface NotifOp {\n  id: GeneratedAlways<number>\n  actorDid: string\n  priority: boolean | null\n  createdAt: GeneratedAlways<Date>\n}\n\nexport type NotifOpEntry = Selectable<NotifOp>\n\nexport const tableName = 'notif_op'\n\nexport type PartialDB = { [tableName]: NotifOp }\n\nexport const createNotifOpChannel = 'notif_op_create' // used with listen/notify\n"
  },
  {
    "path": "packages/bsync/src/db/schema/operation.ts",
    "content": "import { GeneratedAlways } from 'kysely'\nimport { Method } from '../../proto/bsync_pb'\n\nexport type OperationMethod = Method.CREATE | Method.UPDATE | Method.DELETE\n\nexport interface Operation {\n  id: GeneratedAlways<number>\n  actorDid: string\n  namespace: string\n  key: string\n  method: OperationMethod\n  payload: Uint8Array\n  createdAt: GeneratedAlways<Date>\n}\n\nexport const tableName = 'operation'\n\nexport type PartialDB = { [tableName]: Operation }\n\nexport const createOperationChannel = 'operation_create' // used with listen/notify\n"
  },
  {
    "path": "packages/bsync/src/db/types.ts",
    "content": "import { DynamicModule, RawBuilder, SelectQueryBuilder } from 'kysely'\nimport { Pool as PgPool } from 'pg'\n\nexport type DbRef = RawBuilder | ReturnType<DynamicModule['ref']>\n\nexport type AnyQb = SelectQueryBuilder<any, any, any>\n\nexport type PgOptions = {\n  url: string\n  pool?: PgPool\n  schema?: string\n  poolSize?: number\n  poolMaxUses?: number\n  poolIdleTimeoutMs?: number\n}\n"
  },
  {
    "path": "packages/bsync/src/index.ts",
    "content": "import events from 'node:events'\nimport http from 'node:http'\nimport { connectNodeAdapter } from '@connectrpc/connect-node'\nimport { HttpTerminator, createHttpTerminator } from 'http-terminator'\nimport { ServerConfig } from './config'\nimport { AppContext, AppContextOptions } from './context'\nimport { createMuteOpChannel } from './db/schema/mute_op'\nimport { createNotifOpChannel } from './db/schema/notif_op'\nimport { createOperationChannel } from './db/schema/operation'\nimport { dbLogger, loggerMiddleware } from './logger'\nimport routes from './routes'\n\nexport * from './config'\nexport * from './client'\nexport { Database } from './db'\nexport { AppContext } from './context'\nexport { httpLogger } from './logger'\n\nexport class BsyncService {\n  public ctx: AppContext\n  public server: http.Server\n  private ac: AbortController\n  private terminator: HttpTerminator\n  private dbStatsInterval?: NodeJS.Timeout\n\n  constructor(opts: {\n    ctx: AppContext\n    server: http.Server\n    ac: AbortController\n  }) {\n    this.ctx = opts.ctx\n    this.server = opts.server\n    this.ac = opts.ac\n    this.terminator = createHttpTerminator({ server: this.server })\n  }\n\n  static async create(\n    cfg: ServerConfig,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<BsyncService> {\n    const ac = new AbortController()\n    const ctx = await AppContext.fromConfig(cfg, ac.signal, overrides)\n    const handler = connectNodeAdapter({\n      routes: routes(ctx),\n      shutdownSignal: ac.signal,\n    })\n    const server = http.createServer((req, res) => {\n      loggerMiddleware(req, res)\n      if (isHealth(req.url)) {\n        res.statusCode = 200\n        res.setHeader('content-type', 'application/json')\n        return res.end(JSON.stringify({ version: cfg.service.version }))\n      }\n      handler(req, res)\n    })\n    return new BsyncService({ ctx, server, ac })\n  }\n\n  async start(): Promise<http.Server> {\n    if (this.dbStatsInterval) {\n      throw new Error(`${this.constructor.name} already started`)\n    }\n    this.dbStatsInterval = setInterval(() => {\n      dbLogger.info(\n        {\n          idleCount: this.ctx.db.pool.idleCount,\n          totalCount: this.ctx.db.pool.totalCount,\n          waitingCount: this.ctx.db.pool.waitingCount,\n        },\n        'db pool stats',\n      )\n    }, 10000)\n    await this.setupAppEvents()\n    this.server.listen(this.ctx.cfg.service.port)\n    this.server.keepAliveTimeout = 90000\n    await events.once(this.server, 'listening')\n    return this.server\n  }\n\n  async destroy(): Promise<void> {\n    this.ac.abort()\n    await this.terminator.terminate()\n    await this.ctx.db.close()\n    clearInterval(this.dbStatsInterval)\n    this.dbStatsInterval = undefined\n  }\n\n  async setupAppEvents() {\n    const conn = await this.ctx.db.pool.connect()\n    this.ac.signal.addEventListener('abort', () => conn.release(), {\n      once: true,\n    })\n    // if these error, unhandled rejection should cause process to exit\n    conn.query(`listen ${createMuteOpChannel}`)\n    conn.query(`listen ${createNotifOpChannel}`)\n    conn.query(`listen ${createOperationChannel}`)\n    conn.on('notification', (notif) => {\n      if (notif.channel === createMuteOpChannel) {\n        this.ctx.events.emit(createMuteOpChannel)\n      }\n      if (notif.channel === createNotifOpChannel) {\n        this.ctx.events.emit(createNotifOpChannel)\n      }\n      if (notif.channel === createOperationChannel) {\n        this.ctx.events.emit(createOperationChannel)\n      }\n    })\n  }\n}\n\nexport default BsyncService\n\nconst isHealth = (urlStr: string | undefined) => {\n  if (!urlStr) return false\n  const url = new URL(urlStr, 'http://host')\n  return url.pathname === '/_health'\n}\n"
  },
  {
    "path": "packages/bsync/src/logger.ts",
    "content": "import { type IncomingMessage } from 'node:http'\nimport { pinoHttp, stdSerializers } from 'pino-http'\nimport { obfuscateHeaders, subsystemLogger } from '@atproto/common'\n\nexport const dbLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsync:db')\nexport const httpLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('bsync')\n\nexport const loggerMiddleware = pinoHttp({\n  logger: httpLogger,\n  redact: {\n    paths: ['req.headers.authorization'],\n  },\n  serializers: {\n    err: (err: unknown) => ({\n      code: err?.['code'],\n      message: err?.['message'],\n    }),\n    req: (req: IncomingMessage) => {\n      const serialized = stdSerializers.req(req)\n      const headers = obfuscateHeaders(serialized.headers)\n      return { ...serialized, headers }\n    },\n  },\n})\n"
  },
  {
    "path": "packages/bsync/src/proto/bsync_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.3.0 with parameter \"target=ts,import_extension=\"\n// @generated from file bsync.proto (package bsync, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { AddMuteOperationRequest, AddMuteOperationResponse, AddNotifOperationRequest, AddNotifOperationResponse, DeleteOperationsByActorAndNamespaceRequest, DeleteOperationsByActorAndNamespaceResponse, PingRequest, PingResponse, PutOperationRequest, PutOperationResponse, ScanMuteOperationsRequest, ScanMuteOperationsResponse, ScanNotifOperationsRequest, ScanNotifOperationsResponse, ScanOperationsRequest, ScanOperationsResponse } from \"./bsync_pb\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from service bsync.Service\n */\nexport const Service = {\n  typeName: \"bsync.Service\",\n  methods: {\n    /**\n     * Sync\n     *\n     * @generated from rpc bsync.Service.AddMuteOperation\n     */\n    addMuteOperation: {\n      name: \"AddMuteOperation\",\n      I: AddMuteOperationRequest,\n      O: AddMuteOperationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.ScanMuteOperations\n     */\n    scanMuteOperations: {\n      name: \"ScanMuteOperations\",\n      I: ScanMuteOperationsRequest,\n      O: ScanMuteOperationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.AddNotifOperation\n     */\n    addNotifOperation: {\n      name: \"AddNotifOperation\",\n      I: AddNotifOperationRequest,\n      O: AddNotifOperationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.ScanNotifOperations\n     */\n    scanNotifOperations: {\n      name: \"ScanNotifOperations\",\n      I: ScanNotifOperationsRequest,\n      O: ScanNotifOperationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.PutOperation\n     */\n    putOperation: {\n      name: \"PutOperation\",\n      I: PutOperationRequest,\n      O: PutOperationResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.ScanOperations\n     */\n    scanOperations: {\n      name: \"ScanOperations\",\n      I: ScanOperationsRequest,\n      O: ScanOperationsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc bsync.Service.DeleteOperationsByActorAndNamespace\n     */\n    deleteOperationsByActorAndNamespace: {\n      name: \"DeleteOperationsByActorAndNamespace\",\n      I: DeleteOperationsByActorAndNamespaceRequest,\n      O: DeleteOperationsByActorAndNamespaceResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Ping\n     *\n     * @generated from rpc bsync.Service.Ping\n     */\n    ping: {\n      name: \"Ping\",\n      I: PingRequest,\n      O: PingResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "packages/bsync/src/proto/bsync_pb.ts",
    "content": "// @generated by protoc-gen-es v1.6.0 with parameter \"target=ts,import_extension=\"\n// @generated from file bsync.proto (package bsync, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from \"@bufbuild/protobuf\";\nimport { Message, proto3 } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from enum bsync.Method\n */\nexport enum Method {\n  /**\n   * @generated from enum value: METHOD_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: METHOD_CREATE = 1;\n   */\n  CREATE = 1,\n\n  /**\n   * @generated from enum value: METHOD_UPDATE = 2;\n   */\n  UPDATE = 2,\n\n  /**\n   * @generated from enum value: METHOD_DELETE = 3;\n   */\n  DELETE = 3,\n}\n// Retrieve enum metadata with: proto3.getEnumType(Method)\nproto3.util.setEnumType(Method, \"bsync.Method\", [\n  { no: 0, name: \"METHOD_UNSPECIFIED\" },\n  { no: 1, name: \"METHOD_CREATE\" },\n  { no: 2, name: \"METHOD_UPDATE\" },\n  { no: 3, name: \"METHOD_DELETE\" },\n]);\n\n/**\n * @generated from message bsync.MuteOperation\n */\nexport class MuteOperation extends Message<MuteOperation> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: bsync.MuteOperation.Type type = 2;\n   */\n  type = MuteOperation_Type.UNSPECIFIED;\n\n  /**\n   * @generated from field: string actor_did = 3;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject = 4;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<MuteOperation>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.MuteOperation\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"type\", kind: \"enum\", T: proto3.getEnumType(MuteOperation_Type) },\n    { no: 3, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): MuteOperation {\n    return new MuteOperation().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): MuteOperation {\n    return new MuteOperation().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): MuteOperation {\n    return new MuteOperation().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: MuteOperation | PlainMessage<MuteOperation> | undefined, b: MuteOperation | PlainMessage<MuteOperation> | undefined): boolean {\n    return proto3.util.equals(MuteOperation, a, b);\n  }\n}\n\n/**\n * @generated from enum bsync.MuteOperation.Type\n */\nexport enum MuteOperation_Type {\n  /**\n   * @generated from enum value: TYPE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: TYPE_ADD = 1;\n   */\n  ADD = 1,\n\n  /**\n   * @generated from enum value: TYPE_REMOVE = 2;\n   */\n  REMOVE = 2,\n\n  /**\n   * @generated from enum value: TYPE_CLEAR = 3;\n   */\n  CLEAR = 3,\n}\n// Retrieve enum metadata with: proto3.getEnumType(MuteOperation_Type)\nproto3.util.setEnumType(MuteOperation_Type, \"bsync.MuteOperation.Type\", [\n  { no: 0, name: \"TYPE_UNSPECIFIED\" },\n  { no: 1, name: \"TYPE_ADD\" },\n  { no: 2, name: \"TYPE_REMOVE\" },\n  { no: 3, name: \"TYPE_CLEAR\" },\n]);\n\n/**\n * @generated from message bsync.AddMuteOperationRequest\n */\nexport class AddMuteOperationRequest extends Message<AddMuteOperationRequest> {\n  /**\n   * @generated from field: bsync.MuteOperation.Type type = 1;\n   */\n  type = MuteOperation_Type.UNSPECIFIED;\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string subject = 3;\n   */\n  subject = \"\";\n\n  constructor(data?: PartialMessage<AddMuteOperationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddMuteOperationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"type\", kind: \"enum\", T: proto3.getEnumType(MuteOperation_Type) },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"subject\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddMuteOperationRequest {\n    return new AddMuteOperationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddMuteOperationRequest {\n    return new AddMuteOperationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddMuteOperationRequest {\n    return new AddMuteOperationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddMuteOperationRequest | PlainMessage<AddMuteOperationRequest> | undefined, b: AddMuteOperationRequest | PlainMessage<AddMuteOperationRequest> | undefined): boolean {\n    return proto3.util.equals(AddMuteOperationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.AddMuteOperationResponse\n */\nexport class AddMuteOperationResponse extends Message<AddMuteOperationResponse> {\n  /**\n   * @generated from field: bsync.MuteOperation operation = 1;\n   */\n  operation?: MuteOperation;\n\n  constructor(data?: PartialMessage<AddMuteOperationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddMuteOperationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operation\", kind: \"message\", T: MuteOperation },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddMuteOperationResponse {\n    return new AddMuteOperationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddMuteOperationResponse {\n    return new AddMuteOperationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddMuteOperationResponse {\n    return new AddMuteOperationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddMuteOperationResponse | PlainMessage<AddMuteOperationResponse> | undefined, b: AddMuteOperationResponse | PlainMessage<AddMuteOperationResponse> | undefined): boolean {\n    return proto3.util.equals(AddMuteOperationResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanMuteOperationsRequest\n */\nexport class ScanMuteOperationsRequest extends Message<ScanMuteOperationsRequest> {\n  /**\n   * @generated from field: string cursor = 1;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<ScanMuteOperationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanMuteOperationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanMuteOperationsRequest {\n    return new ScanMuteOperationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanMuteOperationsRequest {\n    return new ScanMuteOperationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanMuteOperationsRequest {\n    return new ScanMuteOperationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanMuteOperationsRequest | PlainMessage<ScanMuteOperationsRequest> | undefined, b: ScanMuteOperationsRequest | PlainMessage<ScanMuteOperationsRequest> | undefined): boolean {\n    return proto3.util.equals(ScanMuteOperationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanMuteOperationsResponse\n */\nexport class ScanMuteOperationsResponse extends Message<ScanMuteOperationsResponse> {\n  /**\n   * @generated from field: repeated bsync.MuteOperation operations = 1;\n   */\n  operations: MuteOperation[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<ScanMuteOperationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanMuteOperationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operations\", kind: \"message\", T: MuteOperation, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanMuteOperationsResponse {\n    return new ScanMuteOperationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanMuteOperationsResponse {\n    return new ScanMuteOperationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanMuteOperationsResponse {\n    return new ScanMuteOperationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanMuteOperationsResponse | PlainMessage<ScanMuteOperationsResponse> | undefined, b: ScanMuteOperationsResponse | PlainMessage<ScanMuteOperationsResponse> | undefined): boolean {\n    return proto3.util.equals(ScanMuteOperationsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.NotifOperation\n */\nexport class NotifOperation extends Message<NotifOperation> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: optional bool priority = 3;\n   */\n  priority?: boolean;\n\n  constructor(data?: PartialMessage<NotifOperation>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.NotifOperation\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */, opt: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): NotifOperation {\n    return new NotifOperation().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): NotifOperation {\n    return new NotifOperation().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): NotifOperation {\n    return new NotifOperation().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: NotifOperation | PlainMessage<NotifOperation> | undefined, b: NotifOperation | PlainMessage<NotifOperation> | undefined): boolean {\n    return proto3.util.equals(NotifOperation, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.AddNotifOperationRequest\n */\nexport class AddNotifOperationRequest extends Message<AddNotifOperationRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: optional bool priority = 2;\n   */\n  priority?: boolean;\n\n  constructor(data?: PartialMessage<AddNotifOperationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddNotifOperationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"priority\", kind: \"scalar\", T: 8 /* ScalarType.BOOL */, opt: true },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddNotifOperationRequest {\n    return new AddNotifOperationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddNotifOperationRequest {\n    return new AddNotifOperationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddNotifOperationRequest {\n    return new AddNotifOperationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddNotifOperationRequest | PlainMessage<AddNotifOperationRequest> | undefined, b: AddNotifOperationRequest | PlainMessage<AddNotifOperationRequest> | undefined): boolean {\n    return proto3.util.equals(AddNotifOperationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.AddNotifOperationResponse\n */\nexport class AddNotifOperationResponse extends Message<AddNotifOperationResponse> {\n  /**\n   * @generated from field: bsync.NotifOperation operation = 1;\n   */\n  operation?: NotifOperation;\n\n  constructor(data?: PartialMessage<AddNotifOperationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.AddNotifOperationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operation\", kind: \"message\", T: NotifOperation },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddNotifOperationResponse {\n    return new AddNotifOperationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): AddNotifOperationResponse {\n    return new AddNotifOperationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): AddNotifOperationResponse {\n    return new AddNotifOperationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: AddNotifOperationResponse | PlainMessage<AddNotifOperationResponse> | undefined, b: AddNotifOperationResponse | PlainMessage<AddNotifOperationResponse> | undefined): boolean {\n    return proto3.util.equals(AddNotifOperationResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanNotifOperationsRequest\n */\nexport class ScanNotifOperationsRequest extends Message<ScanNotifOperationsRequest> {\n  /**\n   * @generated from field: string cursor = 1;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<ScanNotifOperationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanNotifOperationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanNotifOperationsRequest {\n    return new ScanNotifOperationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanNotifOperationsRequest {\n    return new ScanNotifOperationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanNotifOperationsRequest {\n    return new ScanNotifOperationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanNotifOperationsRequest | PlainMessage<ScanNotifOperationsRequest> | undefined, b: ScanNotifOperationsRequest | PlainMessage<ScanNotifOperationsRequest> | undefined): boolean {\n    return proto3.util.equals(ScanNotifOperationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanNotifOperationsResponse\n */\nexport class ScanNotifOperationsResponse extends Message<ScanNotifOperationsResponse> {\n  /**\n   * @generated from field: repeated bsync.NotifOperation operations = 1;\n   */\n  operations: NotifOperation[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<ScanNotifOperationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanNotifOperationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operations\", kind: \"message\", T: NotifOperation, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanNotifOperationsResponse {\n    return new ScanNotifOperationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanNotifOperationsResponse {\n    return new ScanNotifOperationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanNotifOperationsResponse {\n    return new ScanNotifOperationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanNotifOperationsResponse | PlainMessage<ScanNotifOperationsResponse> | undefined, b: ScanNotifOperationsResponse | PlainMessage<ScanNotifOperationsResponse> | undefined): boolean {\n    return proto3.util.equals(ScanNotifOperationsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.Operation\n */\nexport class Operation extends Message<Operation> {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id = \"\";\n\n  /**\n   * @generated from field: string actor_did = 2;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 3;\n   */\n  namespace = \"\";\n\n  /**\n   * @generated from field: string key = 4;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: bsync.Method method = 5;\n   */\n  method = Method.UNSPECIFIED;\n\n  /**\n   * @generated from field: bytes payload = 6;\n   */\n  payload = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<Operation>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.Operation\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"id\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 5, name: \"method\", kind: \"enum\", T: proto3.getEnumType(Method) },\n    { no: 6, name: \"payload\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Operation {\n    return new Operation().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Operation {\n    return new Operation().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Operation {\n    return new Operation().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: Operation | PlainMessage<Operation> | undefined, b: Operation | PlainMessage<Operation> | undefined): boolean {\n    return proto3.util.equals(Operation, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.PutOperationRequest\n */\nexport class PutOperationRequest extends Message<PutOperationRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 2;\n   */\n  namespace = \"\";\n\n  /**\n   * @generated from field: string key = 3;\n   */\n  key = \"\";\n\n  /**\n   * @generated from field: bsync.Method method = 4;\n   */\n  method = Method.UNSPECIFIED;\n\n  /**\n   * @generated from field: bytes payload = 5;\n   */\n  payload = new Uint8Array(0);\n\n  constructor(data?: PartialMessage<PutOperationRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PutOperationRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 3, name: \"key\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 4, name: \"method\", kind: \"enum\", T: proto3.getEnumType(Method) },\n    { no: 5, name: \"payload\", kind: \"scalar\", T: 12 /* ScalarType.BYTES */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PutOperationRequest {\n    return new PutOperationRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PutOperationRequest {\n    return new PutOperationRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PutOperationRequest {\n    return new PutOperationRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PutOperationRequest | PlainMessage<PutOperationRequest> | undefined, b: PutOperationRequest | PlainMessage<PutOperationRequest> | undefined): boolean {\n    return proto3.util.equals(PutOperationRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.PutOperationResponse\n */\nexport class PutOperationResponse extends Message<PutOperationResponse> {\n  /**\n   * @generated from field: bsync.Operation operation = 1;\n   */\n  operation?: Operation;\n\n  constructor(data?: PartialMessage<PutOperationResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PutOperationResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operation\", kind: \"message\", T: Operation },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PutOperationResponse {\n    return new PutOperationResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PutOperationResponse {\n    return new PutOperationResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PutOperationResponse {\n    return new PutOperationResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PutOperationResponse | PlainMessage<PutOperationResponse> | undefined, b: PutOperationResponse | PlainMessage<PutOperationResponse> | undefined): boolean {\n    return proto3.util.equals(PutOperationResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanOperationsRequest\n */\nexport class ScanOperationsRequest extends Message<ScanOperationsRequest> {\n  /**\n   * @generated from field: string cursor = 1;\n   */\n  cursor = \"\";\n\n  /**\n   * @generated from field: int32 limit = 2;\n   */\n  limit = 0;\n\n  constructor(data?: PartialMessage<ScanOperationsRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanOperationsRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"limit\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanOperationsRequest {\n    return new ScanOperationsRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanOperationsRequest {\n    return new ScanOperationsRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanOperationsRequest {\n    return new ScanOperationsRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanOperationsRequest | PlainMessage<ScanOperationsRequest> | undefined, b: ScanOperationsRequest | PlainMessage<ScanOperationsRequest> | undefined): boolean {\n    return proto3.util.equals(ScanOperationsRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.ScanOperationsResponse\n */\nexport class ScanOperationsResponse extends Message<ScanOperationsResponse> {\n  /**\n   * @generated from field: repeated bsync.Operation operations = 1;\n   */\n  operations: Operation[] = [];\n\n  /**\n   * @generated from field: string cursor = 2;\n   */\n  cursor = \"\";\n\n  constructor(data?: PartialMessage<ScanOperationsResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.ScanOperationsResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"operations\", kind: \"message\", T: Operation, repeated: true },\n    { no: 2, name: \"cursor\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ScanOperationsResponse {\n    return new ScanOperationsResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ScanOperationsResponse {\n    return new ScanOperationsResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ScanOperationsResponse {\n    return new ScanOperationsResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: ScanOperationsResponse | PlainMessage<ScanOperationsResponse> | undefined, b: ScanOperationsResponse | PlainMessage<ScanOperationsResponse> | undefined): boolean {\n    return proto3.util.equals(ScanOperationsResponse, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.DeleteOperationsByActorAndNamespaceRequest\n */\nexport class DeleteOperationsByActorAndNamespaceRequest extends Message<DeleteOperationsByActorAndNamespaceRequest> {\n  /**\n   * @generated from field: string actor_did = 1;\n   */\n  actorDid = \"\";\n\n  /**\n   * @generated from field: string namespace = 2;\n   */\n  namespace = \"\";\n\n  constructor(data?: PartialMessage<DeleteOperationsByActorAndNamespaceRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.DeleteOperationsByActorAndNamespaceRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"actor_did\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n    { no: 2, name: \"namespace\", kind: \"scalar\", T: 9 /* ScalarType.STRING */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteOperationsByActorAndNamespaceRequest {\n    return new DeleteOperationsByActorAndNamespaceRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceRequest {\n    return new DeleteOperationsByActorAndNamespaceRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceRequest {\n    return new DeleteOperationsByActorAndNamespaceRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteOperationsByActorAndNamespaceRequest | PlainMessage<DeleteOperationsByActorAndNamespaceRequest> | undefined, b: DeleteOperationsByActorAndNamespaceRequest | PlainMessage<DeleteOperationsByActorAndNamespaceRequest> | undefined): boolean {\n    return proto3.util.equals(DeleteOperationsByActorAndNamespaceRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.DeleteOperationsByActorAndNamespaceResponse\n */\nexport class DeleteOperationsByActorAndNamespaceResponse extends Message<DeleteOperationsByActorAndNamespaceResponse> {\n  /**\n   * @generated from field: int32 deleted_count = 1;\n   */\n  deletedCount = 0;\n\n  constructor(data?: PartialMessage<DeleteOperationsByActorAndNamespaceResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.DeleteOperationsByActorAndNamespaceResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n    { no: 1, name: \"deleted_count\", kind: \"scalar\", T: 5 /* ScalarType.INT32 */ },\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteOperationsByActorAndNamespaceResponse {\n    return new DeleteOperationsByActorAndNamespaceResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceResponse {\n    return new DeleteOperationsByActorAndNamespaceResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteOperationsByActorAndNamespaceResponse {\n    return new DeleteOperationsByActorAndNamespaceResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: DeleteOperationsByActorAndNamespaceResponse | PlainMessage<DeleteOperationsByActorAndNamespaceResponse> | undefined, b: DeleteOperationsByActorAndNamespaceResponse | PlainMessage<DeleteOperationsByActorAndNamespaceResponse> | undefined): boolean {\n    return proto3.util.equals(DeleteOperationsByActorAndNamespaceResponse, a, b);\n  }\n}\n\n/**\n * Ping\n *\n * @generated from message bsync.PingRequest\n */\nexport class PingRequest extends Message<PingRequest> {\n  constructor(data?: PartialMessage<PingRequest>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PingRequest\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingRequest {\n    return new PingRequest().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingRequest {\n    return new PingRequest().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingRequest | PlainMessage<PingRequest> | undefined, b: PingRequest | PlainMessage<PingRequest> | undefined): boolean {\n    return proto3.util.equals(PingRequest, a, b);\n  }\n}\n\n/**\n * @generated from message bsync.PingResponse\n */\nexport class PingResponse extends Message<PingResponse> {\n  constructor(data?: PartialMessage<PingResponse>) {\n    super();\n    proto3.util.initPartial(data, this);\n  }\n\n  static readonly runtime: typeof proto3 = proto3;\n  static readonly typeName = \"bsync.PingResponse\";\n  static readonly fields: FieldList = proto3.util.newFieldList(() => [\n  ]);\n\n  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PingResponse {\n    return new PingResponse().fromBinary(bytes, options);\n  }\n\n  static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJson(jsonValue, options);\n  }\n\n  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PingResponse {\n    return new PingResponse().fromJsonString(jsonString, options);\n  }\n\n  static equals(a: PingResponse | PlainMessage<PingResponse> | undefined, b: PingResponse | PlainMessage<PingResponse> | undefined): boolean {\n    return proto3.util.equals(PingResponse, a, b);\n  }\n}\n\n"
  },
  {
    "path": "packages/bsync/src/routes/add-mute-operation.ts",
    "content": "import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { AtUri } from '@atproto/syntax'\nimport { AppContext } from '../context'\nimport { Database } from '../db'\nimport { createMuteOpChannel } from '../db/schema/mute_op'\nimport { Service } from '../proto/bsync_connect'\nimport { AddMuteOperationResponse, MuteOperation_Type } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidAtUri, isValidDid } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  async addMuteOperation(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db } = ctx\n    const op = validMuteOp(req)\n    const id = await db.transaction(async (txn) => {\n      // create mute op\n      const id = await createMuteOp(txn, op)\n      // update mute state\n      if (op.type === MuteOperation_Type.ADD) {\n        await addMuteItem(txn, id, op)\n      } else if (op.type === MuteOperation_Type.REMOVE) {\n        await removeMuteItem(txn, op)\n      } else if (op.type === MuteOperation_Type.CLEAR) {\n        await clearMuteItems(txn, op)\n      } else {\n        const exhaustiveCheck: never = op.type\n        throw new Error(`unreachable: ${exhaustiveCheck}`)\n      }\n      return id\n    })\n    return new AddMuteOperationResponse({\n      operation: {\n        id: String(id),\n        type: op.type,\n        actorDid: op.actorDid,\n        subject: op.subject,\n      },\n    })\n  },\n})\n\nconst createMuteOp = async (db: Database, op: MuteOpInfo) => {\n  const { ref } = db.db.dynamic\n  const { id } = await db.db\n    .insertInto('mute_op')\n    .values({\n      type: op.type,\n      actorDid: op.actorDid,\n      subject: op.subject,\n    })\n    .returning('id')\n    .executeTakeFirstOrThrow()\n  await sql`notify ${ref(createMuteOpChannel)}`.execute(db.db) // emitted transactionally\n  return id\n}\n\nconst addMuteItem = async (db: Database, fromId: number, op: MuteOpInfo) => {\n  const { ref } = db.db.dynamic\n  await db.db\n    .insertInto('mute_item')\n    .values({\n      actorDid: op.actorDid,\n      subject: op.subject,\n      fromId,\n    })\n    .onConflict((oc) =>\n      oc\n        .constraint('mute_item_pkey')\n        .doUpdateSet({ fromId: sql`${ref('excluded.fromId')}` }),\n    )\n    .execute()\n}\n\nconst removeMuteItem = async (db: Database, op: MuteOpInfo) => {\n  await db.db\n    .deleteFrom('mute_item')\n    .where('actorDid', '=', op.actorDid)\n    .where('subject', '=', op.subject)\n    .execute()\n}\n\nconst clearMuteItems = async (db: Database, op: MuteOpInfo) => {\n  await db.db\n    .deleteFrom('mute_item')\n    .where('actorDid', '=', op.actorDid)\n    .execute()\n}\n\nconst validMuteOp = (op: MuteOpInfo): MuteOpInfoValid => {\n  if (!Object.values(MuteOperation_Type).includes(op.type)) {\n    throw new ConnectError('bad mute operation type', Code.InvalidArgument)\n  }\n  if (op.type === MuteOperation_Type.UNSPECIFIED) {\n    throw new ConnectError(\n      'unspecified mute operation type',\n      Code.InvalidArgument,\n    )\n  }\n  if (!isValidDid(op.actorDid)) {\n    throw new ConnectError(\n      'actor_did must be a valid did',\n      Code.InvalidArgument,\n    )\n  }\n  if (op.type === MuteOperation_Type.CLEAR) {\n    if (op.subject !== '') {\n      throw new ConnectError(\n        'subject must not be set on a clear op',\n        Code.InvalidArgument,\n      )\n    }\n  } else {\n    if (isValidDid(op.subject)) {\n      // all good\n    } else if (isValidAtUri(op.subject)) {\n      const uri = new AtUri(op.subject)\n      if (\n        uri.collection !== 'app.bsky.graph.list' &&\n        uri.collection !== 'app.bsky.feed.post'\n      ) {\n        throw new ConnectError(\n          'subject aturis must reference a list or post record',\n          Code.InvalidArgument,\n        )\n      }\n    } else {\n      throw new ConnectError(\n        'subject must be a did or aturi on add or remove op',\n        Code.InvalidArgument,\n      )\n    }\n  }\n  return op as MuteOpInfoValid // op.type has been checked\n}\n\ntype MuteOpInfo = {\n  type: MuteOperation_Type\n  actorDid: string\n  subject: string\n}\n\ntype MuteOpInfoValid = {\n  type:\n    | MuteOperation_Type.ADD\n    | MuteOperation_Type.REMOVE\n    | MuteOperation_Type.CLEAR\n  actorDid: string\n  subject: string\n}\n"
  },
  {
    "path": "packages/bsync/src/routes/add-notif-operation.ts",
    "content": "import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { AppContext } from '../context'\nimport { Database } from '../db'\nimport { createNotifOpChannel } from '../db/schema/notif_op'\nimport { Service } from '../proto/bsync_connect'\nimport { AddNotifOperationResponse } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidDid } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  async addNotifOperation(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db } = ctx\n    const { actorDid, priority } = req\n    if (!isValidDid(actorDid)) {\n      throw new ConnectError(\n        'actor_did must be a valid did',\n        Code.InvalidArgument,\n      )\n    }\n    const id = await db.transaction(async (txn) => {\n      // create notif op\n      const id = await createNotifOp(txn, actorDid, priority)\n      // update notif state\n      if (priority !== undefined) {\n        await updateNotifItem(txn, id, actorDid, priority)\n      }\n      return id\n    })\n    return new AddNotifOperationResponse({\n      operation: {\n        id: String(id),\n        actorDid,\n        priority,\n      },\n    })\n  },\n})\n\nconst createNotifOp = async (\n  db: Database,\n  actorDid: string,\n  priority: boolean | undefined,\n) => {\n  const { ref } = db.db.dynamic\n  const { id } = await db.db\n    .insertInto('notif_op')\n    .values({\n      actorDid,\n      priority,\n    })\n    .returning('id')\n    .executeTakeFirstOrThrow()\n  await sql`notify ${ref(createNotifOpChannel)}`.execute(db.db) // emitted transactionally\n  return id\n}\n\nconst updateNotifItem = async (\n  db: Database,\n  fromId: number,\n  actorDid: string,\n  priority: boolean,\n) => {\n  const { ref } = db.db.dynamic\n  await db.db\n    .insertInto('notif_item')\n    .values({\n      actorDid,\n      priority,\n      fromId,\n    })\n    .onConflict((oc) =>\n      oc.column('actorDid').doUpdateSet({\n        priority: sql`${ref('excluded.priority')}`,\n        fromId: sql`${ref('excluded.fromId')}`,\n      }),\n    )\n    .execute()\n}\n"
  },
  {
    "path": "packages/bsync/src/routes/auth.ts",
    "content": "import { Code, ConnectError, HandlerContext } from '@connectrpc/connect'\nimport { AppContext } from '../context'\n\nconst BEARER = 'Bearer '\n\nexport const authWithApiKey = (ctx: AppContext, handlerCtx: HandlerContext) => {\n  const authorization = handlerCtx.requestHeader.get('authorization')\n  if (!authorization?.startsWith(BEARER)) {\n    throw new ConnectError('missing auth', Code.Unauthenticated)\n  }\n  const key = authorization.slice(BEARER.length)\n  if (!ctx.cfg.auth.apiKeys.has(key)) {\n    throw new ConnectError('invalid api key', Code.Unauthenticated)\n  }\n}\n"
  },
  {
    "path": "packages/bsync/src/routes/delete-operations.ts",
    "content": "import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { AppContext } from '../context'\nimport { Service } from '../proto/bsync_connect'\nimport { DeleteOperationsByActorAndNamespaceResponse } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidDid, validateNamespace } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  /**\n   * This method is responsible for deleting log rows from the bsync db, it has\n   * no other downstream effects. This method is called from the dataplane in\n   * response to a data deletion request initiated by a moderator in Ozone.\n   * It's the final step of the deletion process, basically cleaning up the\n   * breadcrumbs that resulted in the state we store in the dataplane.\n   */\n  async deleteOperationsByActorAndNamespace(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db } = ctx\n\n    try {\n      validateNamespace(req.namespace)\n    } catch (error) {\n      throw new ConnectError(\n        'requested namespace for deletion is invalid NSID',\n        Code.InvalidArgument,\n      )\n    }\n    if (!isValidDid(req.actorDid)) {\n      throw new ConnectError(\n        'requested actor_did for deletion is invalid DID',\n        Code.InvalidArgument,\n      )\n    }\n\n    const deletedRows = await db.db\n      .deleteFrom('operation')\n      .where('actorDid', '=', req.actorDid)\n      .where('namespace', '=', req.namespace)\n      .returning('id')\n      .execute()\n    return new DeleteOperationsByActorAndNamespaceResponse({\n      deletedCount: deletedRows.length,\n    })\n  },\n})\n"
  },
  {
    "path": "packages/bsync/src/routes/index.ts",
    "content": "import { ConnectRouter } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { AppContext } from '../context'\nimport { Service } from '../proto/bsync_connect'\nimport addMuteOperation from './add-mute-operation'\nimport addNotifOperation from './add-notif-operation'\nimport deleteOperations from './delete-operations'\nimport putOperation from './put-operation'\nimport scanMuteOperations from './scan-mute-operations'\nimport scanNotifOperations from './scan-notif-operations'\nimport scanOperations from './scan-operations'\n\nexport default (ctx: AppContext) => (router: ConnectRouter) => {\n  return router.service(Service, {\n    ...addMuteOperation(ctx),\n    ...scanMuteOperations(ctx),\n    ...addNotifOperation(ctx),\n    ...scanNotifOperations(ctx),\n    ...putOperation(ctx),\n    ...scanOperations(ctx),\n    ...deleteOperations(ctx),\n    async ping() {\n      const { db } = ctx\n      await sql`select 1`.execute(db.db)\n      return {}\n    },\n  })\n}\n"
  },
  {
    "path": "packages/bsync/src/routes/put-operation.ts",
    "content": "import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'\nimport { sql } from 'kysely'\nimport { ensureValidRecordKey } from '@atproto/syntax'\nimport { AppContext } from '../context'\nimport { Database } from '../db'\nimport { OperationMethod, createOperationChannel } from '../db/schema/operation'\nimport { Service } from '../proto/bsync_connect'\nimport {\n  Method,\n  PutOperationRequest,\n  PutOperationResponse,\n} from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { isValidDid, validateNamespace } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  async putOperation(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db } = ctx\n    const op = validateOp(req)\n    const id = await db.transaction(async (txn) => {\n      return putOp(txn, op)\n    })\n    return new PutOperationResponse({\n      operation: {\n        id: String(id),\n        actorDid: op.actorDid,\n        namespace: op.namespace,\n        key: op.key,\n        method: op.method,\n        payload: op.payload,\n      },\n    })\n  },\n})\n\nconst putOp = async (db: Database, op: Operation) => {\n  const { ref } = db.db.dynamic\n  const { id } = await db.db\n    .insertInto('operation')\n    .values({\n      actorDid: op.actorDid,\n      namespace: op.namespace,\n      key: op.key,\n      method: op.method,\n      payload: op.payload,\n    })\n    .returning('id')\n    .executeTakeFirstOrThrow()\n  await sql`notify ${ref(createOperationChannel)}`.execute(db.db) // emitted transactionally\n  return id\n}\n\nconst validateOp = (req: PutOperationRequest): Operation => {\n  try {\n    validateNamespace(req.namespace)\n  } catch (error) {\n    throw new ConnectError(\n      'operation namespace is invalid NSID',\n      Code.InvalidArgument,\n    )\n  }\n\n  if (!isValidDid(req.actorDid)) {\n    throw new ConnectError(\n      'operation actor_did is invalid DID',\n      Code.InvalidArgument,\n    )\n  }\n\n  try {\n    ensureValidRecordKey(req.key)\n  } catch (error) {\n    throw new ConnectError('operation key is required', Code.InvalidArgument)\n  }\n\n  if (\n    req.method !== Method.CREATE &&\n    req.method !== Method.UPDATE &&\n    req.method !== Method.DELETE\n  ) {\n    throw new ConnectError('operation method is invalid', Code.InvalidArgument)\n  }\n\n  if (req.method === Method.CREATE || req.method === Method.UPDATE) {\n    try {\n      JSON.parse(new TextDecoder().decode(req.payload))\n    } catch (error) {\n      throw new ConnectError(\n        'payload must be a valid JSON when method is CREATE or UPDATE',\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  if (req.method === Method.DELETE && req.payload.length > 0) {\n    throw new ConnectError(\n      'cannot specify a payload when method is DELETE',\n      Code.InvalidArgument,\n    )\n  }\n\n  return req as Operation\n}\n\ntype Operation = {\n  actorDid: string\n  namespace: string\n  key: string\n  payload: Uint8Array\n  method: OperationMethod\n}\n"
  },
  {
    "path": "packages/bsync/src/routes/scan-mute-operations.ts",
    "content": "import { once } from 'node:events'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { AppContext } from '../context'\nimport { createMuteOpChannel } from '../db/schema/mute_op'\nimport { Service } from '../proto/bsync_connect'\nimport { ScanMuteOperationsResponse } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { combineSignals, validCursor } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  async scanMuteOperations(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db, events } = ctx\n    const limit = req.limit || 1000\n    const cursor = validCursor(req.cursor)\n    const nextMuteOpPromise = once(events, createMuteOpChannel, {\n      signal: combineSignals(\n        ctx.shutdown,\n        AbortSignal.timeout(ctx.cfg.service.longPollTimeoutMs),\n      ),\n    })\n    nextMuteOpPromise.catch(() => null) // ensure timeout is always handled\n\n    const nextMuteOpPageQb = db.db\n      .selectFrom('mute_op')\n      .selectAll()\n      .where('id', '>', cursor ?? -1)\n      .orderBy('id', 'asc')\n      .limit(limit)\n\n    let ops = await nextMuteOpPageQb.execute()\n\n    if (!ops.length) {\n      // if there were no ops on the page, wait for an event then try again.\n      try {\n        await nextMuteOpPromise\n      } catch (err) {\n        ctx.shutdown.throwIfAborted()\n        return new ScanMuteOperationsResponse({\n          operations: [],\n          cursor: req.cursor,\n        })\n      }\n      ops = await nextMuteOpPageQb.execute()\n      if (!ops.length) {\n        return new ScanMuteOperationsResponse({\n          operations: [],\n          cursor: req.cursor,\n        })\n      }\n    }\n\n    const lastOp = ops[ops.length - 1]\n\n    return new ScanMuteOperationsResponse({\n      operations: ops.map((op) => ({\n        id: op.id.toString(),\n        type: op.type,\n        actorDid: op.actorDid,\n        subject: op.subject,\n      })),\n      cursor: lastOp.id.toString(),\n    })\n  },\n})\n"
  },
  {
    "path": "packages/bsync/src/routes/scan-notif-operations.ts",
    "content": "import { once } from 'node:events'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { AppContext } from '../context'\nimport { createNotifOpChannel } from '../db/schema/notif_op'\nimport { Service } from '../proto/bsync_connect'\nimport { ScanNotifOperationsResponse } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { combineSignals, validCursor } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  async scanNotifOperations(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db, events } = ctx\n    const limit = req.limit || 1000\n    const cursor = validCursor(req.cursor)\n    const nextNotifOpPromise = once(events, createNotifOpChannel, {\n      signal: combineSignals(\n        ctx.shutdown,\n        AbortSignal.timeout(ctx.cfg.service.longPollTimeoutMs),\n      ),\n    })\n    nextNotifOpPromise.catch(() => null) // ensure timeout is always handled\n\n    const nextNotifOpPageQb = db.db\n      .selectFrom('notif_op')\n      .selectAll()\n      .where('id', '>', cursor ?? -1)\n      .orderBy('id', 'asc')\n      .limit(limit)\n\n    let ops = await nextNotifOpPageQb.execute()\n\n    if (!ops.length) {\n      // if there were no ops on the page, wait for an event then try again.\n      try {\n        await nextNotifOpPromise\n      } catch (err) {\n        ctx.shutdown.throwIfAborted()\n        return new ScanNotifOperationsResponse({\n          operations: [],\n          cursor: req.cursor,\n        })\n      }\n      ops = await nextNotifOpPageQb.execute()\n      if (!ops.length) {\n        return new ScanNotifOperationsResponse({\n          operations: [],\n          cursor: req.cursor,\n        })\n      }\n    }\n\n    const lastOp = ops[ops.length - 1]\n\n    return new ScanNotifOperationsResponse({\n      operations: ops.map((op) => ({\n        id: op.id.toString(),\n        actorDid: op.actorDid,\n        priority: op.priority ?? undefined,\n      })),\n      cursor: lastOp.id.toString(),\n    })\n  },\n})\n"
  },
  {
    "path": "packages/bsync/src/routes/scan-operations.ts",
    "content": "import { once } from 'node:events'\nimport { ServiceImpl } from '@connectrpc/connect'\nimport { AppContext } from '../context'\nimport { createOperationChannel } from '../db/schema/operation'\nimport { Service } from '../proto/bsync_connect'\nimport { ScanOperationsResponse } from '../proto/bsync_pb'\nimport { authWithApiKey } from './auth'\nimport { combineSignals, validCursor } from './util'\n\nexport default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({\n  async scanOperations(req, handlerCtx) {\n    authWithApiKey(ctx, handlerCtx)\n    const { db, events } = ctx\n    const limit = req.limit || 1000\n    const cursor = validCursor(req.cursor)\n    const nextOpPromise = once(events, createOperationChannel, {\n      signal: combineSignals(\n        ctx.shutdown,\n        AbortSignal.timeout(ctx.cfg.service.longPollTimeoutMs),\n      ),\n    })\n    nextOpPromise.catch(() => null) // ensure timeout is always handled\n\n    const nextOpPageQb = db.db\n      .selectFrom('operation')\n      .selectAll()\n      .where('id', '>', cursor ?? -1)\n      .orderBy('id', 'asc')\n      .limit(limit)\n\n    let ops = await nextOpPageQb.execute()\n\n    if (!ops.length) {\n      // if there were no ops on the page, wait for an event then try again.\n      try {\n        await nextOpPromise\n      } catch (err) {\n        ctx.shutdown.throwIfAborted()\n        return new ScanOperationsResponse({\n          operations: [],\n          cursor: req.cursor,\n        })\n      }\n      ops = await nextOpPageQb.execute()\n      if (!ops.length) {\n        return new ScanOperationsResponse({\n          operations: [],\n          cursor: req.cursor,\n        })\n      }\n    }\n\n    const lastOp = ops[ops.length - 1]\n\n    return new ScanOperationsResponse({\n      operations: ops.map((op) => ({\n        id: op.id.toString(),\n        actorDid: op.actorDid,\n        namespace: op.namespace,\n        key: op.key,\n        method: op.method,\n        payload: op.payload,\n      })),\n      cursor: lastOp.id.toString(),\n    })\n  },\n})\n"
  },
  {
    "path": "packages/bsync/src/routes/util.ts",
    "content": "import { Code, ConnectError } from '@connectrpc/connect'\nimport {\n  InvalidDidError,\n  ensureValidAtUri,\n  ensureValidDid,\n  ensureValidNsid,\n} from '@atproto/syntax'\n\nexport const validCursor = (cursor: string): number | null => {\n  if (cursor === '') return null\n  const int = parseInt(cursor, 10)\n  if (isNaN(int) || int < 0) {\n    throw new ConnectError('invalid cursor', Code.InvalidArgument)\n  }\n  return int\n}\n\nexport const combineSignals = (a: AbortSignal, b: AbortSignal) => {\n  const controller = new AbortController()\n  for (const signal of [a, b]) {\n    if (signal.aborted) {\n      controller.abort()\n      return signal\n    }\n    signal.addEventListener('abort', () => controller.abort(signal.reason), {\n      // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625\n      signal: controller.signal,\n    })\n  }\n  return controller.signal\n}\n\nexport const isValidDid = (did: string) => {\n  try {\n    ensureValidDid(did)\n    return true\n  } catch (err) {\n    if (err instanceof InvalidDidError) {\n      return false\n    }\n    throw err\n  }\n}\n\nexport const isValidAtUri = (uri: string) => {\n  try {\n    ensureValidAtUri(uri)\n    return true\n  } catch {\n    return false\n  }\n}\n\nexport const validateNamespace = (namespace: string): void => {\n  const parts = namespace.split('#')\n\n  if (parts.length !== 1 && parts.length !== 2) {\n    throw new Error('namespace must be in the format \"nsid[#fragment]\"')\n  }\n\n  const [nsid, fragment] = parts\n\n  ensureValidNsid(nsid)\n  if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {\n    throw new Error('namespace fragment must be a valid identifier')\n  }\n}\n"
  },
  {
    "path": "packages/bsync/tests/delete-operations.test.ts",
    "content": "import getPort from 'get-port'\nimport {\n  BsyncClient,\n  BsyncService,\n  Database,\n  authWithApiKey,\n  createClient,\n  envToCfg,\n} from '../src'\nimport { Method } from '../src/proto/bsync_pb'\n\ndescribe('operations', () => {\n  let bsync: BsyncService\n  let client: BsyncClient\n\n  const validPayload0 = Buffer.from(JSON.stringify({ value: 0 }))\n  const validPayload1 = Buffer.from(JSON.stringify({ value: 1 }))\n\n  beforeAll(async () => {\n    bsync = await BsyncService.create(\n      envToCfg({\n        port: await getPort(),\n        dbUrl: process.env.DB_POSTGRES_URL,\n        dbSchema: 'bsync_delete_operations',\n        apiKeys: ['key-1'],\n        longPollTimeoutMs: 500,\n      }),\n    )\n    await bsync.ctx.db.migrateToLatestOrThrow()\n    await bsync.start()\n    client = createClient({\n      httpVersion: '1.1',\n      baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      interceptors: [authWithApiKey('key-1')],\n    })\n  })\n\n  afterAll(async () => {\n    await bsync.destroy()\n  })\n\n  beforeEach(async () => {\n    await clearOps(bsync.ctx.db)\n  })\n\n  it('deletes', async () => {\n    const res1 = await client.putOperation({\n      actorDid: 'did:example:a',\n      namespace: 'app.bsky.some.col',\n      key: 'key1',\n      method: Method.CREATE,\n      payload: validPayload0,\n    })\n    const res2 = await client.putOperation({\n      actorDid: 'did:example:a',\n      namespace: 'app.bsky.other.col#id',\n      key: 'key1',\n      method: Method.UPDATE,\n      payload: validPayload1,\n    })\n\n    expect(res1.operation?.id).toBe('1')\n    expect(res2.operation?.id).toBe('2')\n    expect(await dumpOps(bsync.ctx.db)).toStrictEqual([\n      {\n        id: 1,\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.some.col',\n        key: 'key1',\n        method: Method.CREATE,\n        payload: validPayload0,\n        createdAt: expect.any(Date),\n      },\n      {\n        id: 2,\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.other.col#id',\n        key: 'key1',\n        method: Method.UPDATE,\n        payload: validPayload1,\n        createdAt: expect.any(Date),\n      },\n    ])\n\n    await client.deleteOperationsByActorAndNamespace({\n      actorDid: 'did:example:a',\n      namespace: 'app.bsky.some.col',\n    })\n    await client.deleteOperationsByActorAndNamespace({\n      actorDid: 'did:example:a',\n      namespace: 'app.bsky.other.col#id',\n    })\n\n    expect(await dumpOps(bsync.ctx.db)).toStrictEqual([])\n  })\n})\n\nconst dumpOps = async (db: Database) => {\n  return db.db\n    .selectFrom('operation')\n    .selectAll()\n    .orderBy('id', 'asc')\n    .execute()\n}\n\nconst clearOps = async (db: Database) => {\n  await db.db.deleteFrom('operation').execute()\n}\n"
  },
  {
    "path": "packages/bsync/tests/mutes.test.ts",
    "content": "import { Code, ConnectError } from '@connectrpc/connect'\nimport getPort from 'get-port'\nimport { wait } from '@atproto/common'\nimport {\n  BsyncClient,\n  BsyncService,\n  Database,\n  authWithApiKey,\n  createClient,\n  envToCfg,\n} from '../src'\nimport { MuteOperation, MuteOperation_Type } from '../src/proto/bsync_pb'\n\ndescribe('mutes', () => {\n  let bsync: BsyncService\n  let client: BsyncClient\n\n  beforeAll(async () => {\n    bsync = await BsyncService.create(\n      envToCfg({\n        port: await getPort(),\n        dbUrl: process.env.DB_POSTGRES_URL,\n        dbSchema: 'bsync_mutes',\n        apiKeys: ['key-1'],\n        longPollTimeoutMs: 500,\n      }),\n    )\n    await bsync.ctx.db.migrateToLatestOrThrow()\n    await bsync.start()\n    client = createClient({\n      httpVersion: '1.1',\n      baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      interceptors: [authWithApiKey('key-1')],\n    })\n  })\n\n  afterAll(async () => {\n    await bsync.destroy()\n  })\n\n  beforeEach(async () => {\n    await clearMutes(bsync.ctx.db)\n  })\n\n  describe('addMuteOperation', () => {\n    it('adds mute operations to add mutes.', async () => {\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:b',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:c',\n      })\n      // dupe has no effect\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:c',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:b',\n        subject: 'did:example:c',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:c',\n        subject: 'at://did:example:d/app.bsky.graph.list/rkey1',\n      })\n      expect(await dumpMuteState(bsync.ctx.db)).toEqual({\n        'did:example:a': ['did:example:b', 'did:example:c'],\n        'did:example:b': ['did:example:c'],\n        'did:example:c': ['at://did:example:d/app.bsky.graph.list/rkey1'],\n      })\n    })\n\n    it('adds mute operations to remove mutes.', async () => {\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:b',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:c',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:b',\n        subject: 'did:example:c',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.REMOVE,\n        actorDid: 'did:example:a',\n        subject: 'did:example:c',\n      })\n      // removes nothing\n      await client.addMuteOperation({\n        type: MuteOperation_Type.REMOVE,\n        actorDid: 'did:example:b',\n        subject: 'did:example:d',\n      })\n      expect(await dumpMuteState(bsync.ctx.db)).toEqual({\n        'did:example:a': ['did:example:b'],\n        'did:example:b': ['did:example:c'],\n      })\n    })\n\n    it('adds mute operations to clear mutes.', async () => {\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:b',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:c',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:b',\n        subject: 'did:example:c',\n      })\n      await client.addMuteOperation({\n        type: MuteOperation_Type.CLEAR,\n        actorDid: 'did:example:a',\n      })\n      expect(await dumpMuteState(bsync.ctx.db)).toEqual({\n        'did:example:b': ['did:example:c'],\n      })\n    })\n\n    it('fails on bad inputs', async () => {\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.ADD,\n          actorDid: 'did:example:a',\n          subject: 'invalid',\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'subject must be a did or aturi on add or remove op',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.ADD,\n          actorDid: 'did:example:a',\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'subject must be a did or aturi on add or remove op',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.ADD,\n          actorDid: 'did:example:a',\n          subject: 'at://did:example:b/bad.collection/rkey1',\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'subject must be a did or aturi on add or remove op',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.ADD,\n          actorDid: 'invalid',\n          subject: 'did:example:b',\n        }),\n      ).rejects.toEqual(\n        new ConnectError('actor_did must be a valid did', Code.InvalidArgument),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.REMOVE,\n          actorDid: 'did:example:a',\n          subject: 'invalid',\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'subject must be a did or aturi on add or remove op',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.CLEAR,\n          actorDid: 'did:example:a',\n          subject: 'did:example:b',\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'subject must not be set on a clear op',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: MuteOperation_Type.CLEAR,\n          actorDid: 'invalid',\n        }),\n      ).rejects.toEqual(\n        new ConnectError('actor_did must be a valid did', Code.InvalidArgument),\n      )\n      await expect(\n        client.addMuteOperation({\n          type: 100 as any,\n          actorDid: 'did:example:a',\n          subject: 'did:example:b',\n        }),\n      ).rejects.toEqual(\n        new ConnectError('bad mute operation type', Code.InvalidArgument),\n      )\n    })\n\n    it('requires auth', async () => {\n      // unauthed\n      const unauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      })\n      const tryAddMuteOperation1 = unauthedClient.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:b',\n      })\n      await expect(tryAddMuteOperation1).rejects.toEqual(\n        new ConnectError('missing auth', Code.Unauthenticated),\n      )\n      // bad auth\n      const badauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n        interceptors: [authWithApiKey('key-bad')],\n      })\n      const tryAddMuteOperation2 = badauthedClient.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:b',\n      })\n      await expect(tryAddMuteOperation2).rejects.toEqual(\n        new ConnectError('invalid api key', Code.Unauthenticated),\n      )\n    })\n  })\n\n  describe('scanMuteOperations', () => {\n    it('requires auth', async () => {\n      // unauthed\n      const unauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      })\n      const tryScanMuteOperations1 = unauthedClient.scanMuteOperations({})\n      await expect(tryScanMuteOperations1).rejects.toEqual(\n        new ConnectError('missing auth', Code.Unauthenticated),\n      )\n      // bad auth\n      const badauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n        interceptors: [authWithApiKey('key-bad')],\n      })\n      const tryScanMuteOperations2 = badauthedClient.scanMuteOperations({})\n      await expect(tryScanMuteOperations2).rejects.toEqual(\n        new ConnectError('invalid api key', Code.Unauthenticated),\n      )\n    })\n\n    it('pages over created mute ops.', async () => {\n      // add 100 mute ops\n      for (let i = 0; i < 10; ++i) {\n        for (let j = 0; j < 8; ++j) {\n          await client.addMuteOperation({\n            type: MuteOperation_Type.ADD,\n            actorDid: `did:example:${i}`,\n            subject: `did:example:${j}`,\n          })\n        }\n        for (let j = 0; j < 2; ++j) {\n          await client.addMuteOperation({\n            type: MuteOperation_Type.ADD,\n            actorDid: `did:example:${i}`,\n            subject: `at://did:example:0/app.bsky.graph.list/rkey${j}`,\n          })\n        }\n      }\n\n      let cursor: string | undefined\n      const operations: MuteOperation[] = []\n      do {\n        const res = await client.scanMuteOperations({\n          cursor,\n          limit: 30,\n        })\n        operations.push(...res.operations)\n        cursor = res.operations.length ? res.cursor : undefined\n      } while (cursor)\n\n      expect(operations.length).toEqual(100)\n      const operationIds = operations.map((op) => parseInt(op.id, 10))\n      const ascending = (a: number, b: number) => a - b\n      expect(operationIds).toEqual([...operationIds].sort(ascending))\n    })\n\n    it('supports long-poll, finding an operation.', async () => {\n      const scanPromise = client.scanMuteOperations({})\n      await wait(100) // would be complete by now if it wasn't long-polling for an item\n      const { operation } = await client.addMuteOperation({\n        type: MuteOperation_Type.ADD,\n        actorDid: 'did:example:a',\n        subject: 'did:example:b',\n      })\n      const res = await scanPromise\n      expect(res.operations.length).toEqual(1)\n      expect(res.operations[0]).toEqual(operation)\n      expect(res.cursor).toEqual(operation?.id)\n    })\n\n    it('supports long-poll, not finding an operation.', async () => {\n      const res = await client.scanMuteOperations({})\n      expect(res.cursor).toEqual('')\n      expect(res.operations).toEqual([])\n    })\n  })\n})\n\nconst dumpMuteState = async (db: Database) => {\n  const items = await db.db.selectFrom('mute_item').selectAll().execute()\n  const result: Record<string, string[]> = {}\n  items.forEach((item) => {\n    result[item.actorDid] ??= []\n    result[item.actorDid].push(item.subject)\n  })\n  Object.values(result).forEach((subjects) => subjects.sort())\n  return result\n}\n\nconst clearMutes = async (db: Database) => {\n  await db.db.deleteFrom('mute_item').execute()\n  await db.db.deleteFrom('mute_op').execute()\n}\n"
  },
  {
    "path": "packages/bsync/tests/notifications.test.ts",
    "content": "import { Code, ConnectError } from '@connectrpc/connect'\nimport getPort from 'get-port'\nimport { wait } from '@atproto/common'\nimport {\n  BsyncClient,\n  BsyncService,\n  Database,\n  authWithApiKey,\n  createClient,\n  envToCfg,\n} from '../src'\nimport { NotifOperation } from '../src/proto/bsync_pb'\n\ndescribe('notifications', () => {\n  let bsync: BsyncService\n  let client: BsyncClient\n\n  beforeAll(async () => {\n    bsync = await BsyncService.create(\n      envToCfg({\n        port: await getPort(),\n        dbUrl: process.env.DB_POSTGRES_URL,\n        dbSchema: 'bsync_notifications',\n        apiKeys: ['key-1'],\n        longPollTimeoutMs: 500,\n      }),\n    )\n    await bsync.ctx.db.migrateToLatestOrThrow()\n    await bsync.start()\n    client = createClient({\n      httpVersion: '1.1',\n      baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      interceptors: [authWithApiKey('key-1')],\n    })\n  })\n\n  afterAll(async () => {\n    await bsync.destroy()\n  })\n\n  beforeEach(async () => {\n    await clearNotifs(bsync.ctx.db)\n  })\n\n  describe('addNotifOperation', () => {\n    it('adds notif operations to set priority.', async () => {\n      // true + true\n      await client.addNotifOperation({\n        actorDid: 'did:example:a',\n        priority: true,\n      })\n      await client.addNotifOperation({\n        actorDid: 'did:example:a',\n        priority: true,\n      })\n      // true + none\n      await client.addNotifOperation({\n        actorDid: 'did:example:b',\n        priority: true,\n      })\n      await client.addNotifOperation({\n        actorDid: 'did:example:b',\n      })\n      // true + false\n      await client.addNotifOperation({\n        actorDid: 'did:example:c',\n        priority: true,\n      })\n      await client.addNotifOperation({\n        actorDid: 'did:example:c',\n        priority: false,\n      })\n      // false + true\n      await client.addNotifOperation({\n        actorDid: 'did:example:d',\n        priority: false,\n      })\n      await client.addNotifOperation({\n        actorDid: 'did:example:d',\n        priority: true,\n      })\n      expect(await dumpNotifState(bsync.ctx.db)).toEqual({\n        'did:example:a': true,\n        'did:example:b': true,\n        'did:example:c': false,\n        'did:example:d': true,\n      })\n    })\n\n    it('fails on bad inputs', async () => {\n      await expect(\n        client.addNotifOperation({\n          actorDid: 'invalid',\n          priority: true,\n        }),\n      ).rejects.toEqual(\n        new ConnectError('actor_did must be a valid did', Code.InvalidArgument),\n      )\n    })\n\n    it('requires auth', async () => {\n      // unauthed\n      const unauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      })\n      const tryAddNotifOperation1 = unauthedClient.addNotifOperation({\n        actorDid: 'did:example:a',\n      })\n      await expect(tryAddNotifOperation1).rejects.toEqual(\n        new ConnectError('missing auth', Code.Unauthenticated),\n      )\n      // bad auth\n      const badauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n        interceptors: [authWithApiKey('key-bad')],\n      })\n      const tryAddNotifOperation2 = badauthedClient.addNotifOperation({\n        actorDid: 'did:example:a',\n      })\n      await expect(tryAddNotifOperation2).rejects.toEqual(\n        new ConnectError('invalid api key', Code.Unauthenticated),\n      )\n    })\n  })\n\n  describe('scanNotifOperations', () => {\n    it('requires auth', async () => {\n      // unauthed\n      const unauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      })\n      const tryScanNotifOperations1 = unauthedClient.scanNotifOperations({})\n      await expect(tryScanNotifOperations1).rejects.toEqual(\n        new ConnectError('missing auth', Code.Unauthenticated),\n      )\n      // bad auth\n      const badauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n        interceptors: [authWithApiKey('key-bad')],\n      })\n      const tryScanNotifOperations2 = badauthedClient.scanNotifOperations({})\n      await expect(tryScanNotifOperations2).rejects.toEqual(\n        new ConnectError('invalid api key', Code.Unauthenticated),\n      )\n    })\n\n    it('pages over created notif ops.', async () => {\n      // add 100 notif ops\n      for (let i = 0; i < 100; ++i) {\n        await client.addNotifOperation({\n          actorDid: `did:example:${i}`,\n          priority: i % 2 === 0,\n        })\n      }\n\n      let cursor: string | undefined\n      const operations: NotifOperation[] = []\n      do {\n        const res = await client.scanNotifOperations({\n          cursor,\n          limit: 30,\n        })\n        operations.push(...res.operations)\n        cursor = res.operations.length ? res.cursor : undefined\n      } while (cursor)\n\n      expect(operations.length).toEqual(100)\n      const operationIds = operations.map((op) => parseInt(op.id, 10))\n      const ascending = (a: number, b: number) => a - b\n      expect(operationIds).toEqual([...operationIds].sort(ascending))\n    })\n\n    it('supports long-poll, finding an operation.', async () => {\n      const scanPromise = client.scanNotifOperations({})\n      await wait(100) // would be complete by now if it wasn't long-polling for an item\n      const { operation } = await client.addNotifOperation({\n        actorDid: 'did:example:a',\n      })\n      const res = await scanPromise\n      expect(res.operations.length).toEqual(1)\n      expect(res.operations[0]).toEqual(operation)\n      expect(res.cursor).toEqual(operation?.id)\n    })\n\n    it('supports long-poll, not finding an operation.', async () => {\n      const res = await client.scanNotifOperations({})\n      expect(res.cursor).toEqual('')\n      expect(res.operations).toEqual([])\n    })\n  })\n})\n\nconst dumpNotifState = async (db: Database) => {\n  const items = await db.db.selectFrom('notif_item').selectAll().execute()\n  const result: Record<string, boolean> = {}\n  items.forEach((item) => {\n    result[item.actorDid] = item.priority\n  })\n  return result\n}\n\nconst clearNotifs = async (db: Database) => {\n  await db.db.deleteFrom('notif_item').execute()\n  await db.db.deleteFrom('notif_op').execute()\n}\n"
  },
  {
    "path": "packages/bsync/tests/operations.test.ts",
    "content": "import assert from 'node:assert'\nimport { Code, ConnectError } from '@connectrpc/connect'\nimport getPort from 'get-port'\nimport { wait } from '@atproto/common'\nimport {\n  BsyncClient,\n  BsyncService,\n  Database,\n  authWithApiKey,\n  createClient,\n  envToCfg,\n} from '../src'\nimport { Method, Operation } from '../src/proto/bsync_pb'\n\ndescribe('operations', () => {\n  let bsync: BsyncService\n  let client: BsyncClient\n\n  const validPayload0 = Buffer.from(JSON.stringify({ value: 0 }))\n  const validPayload1 = Buffer.from(JSON.stringify({ value: 1 }))\n  const invalidPayload = Buffer.from('{invalid json}')\n\n  beforeAll(async () => {\n    bsync = await BsyncService.create(\n      envToCfg({\n        port: await getPort(),\n        dbUrl: process.env.DB_POSTGRES_URL,\n        dbSchema: 'bsync_operations',\n        apiKeys: ['key-1'],\n        longPollTimeoutMs: 500,\n      }),\n    )\n    await bsync.ctx.db.migrateToLatestOrThrow()\n    await bsync.start()\n    client = createClient({\n      httpVersion: '1.1',\n      baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      interceptors: [authWithApiKey('key-1')],\n    })\n  })\n\n  afterAll(async () => {\n    await bsync.destroy()\n  })\n\n  beforeEach(async () => {\n    await clearOps(bsync.ctx.db)\n  })\n\n  describe('putOperation', () => {\n    it('requires auth.', async () => {\n      // unauthed\n      const unauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      })\n      const tryPutOperation1 = unauthedClient.putOperation({\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.some.col',\n        key: 'key1',\n        method: Method.CREATE,\n        payload: validPayload0,\n      })\n      await expect(tryPutOperation1).rejects.toEqual(\n        new ConnectError('missing auth', Code.Unauthenticated),\n      )\n      // bad auth\n      const badauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n        interceptors: [authWithApiKey('key-bad')],\n      })\n      const tryPutOperation2 = badauthedClient.putOperation({\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.some.col',\n        key: 'key1',\n        method: Method.CREATE,\n        payload: validPayload0,\n      })\n      await expect(tryPutOperation2).rejects.toEqual(\n        new ConnectError('invalid api key', Code.Unauthenticated),\n      )\n    })\n\n    it('fails on bad inputs.', async () => {\n      await expect(\n        client.putOperation({\n          actorDid: 'did:example:a',\n          namespace: 'bad-namespace',\n          key: 'key1',\n          method: Method.CREATE,\n          payload: validPayload0,\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'operation namespace is invalid NSID',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.putOperation({\n          actorDid: 'bad-did',\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.CREATE,\n          payload: validPayload0,\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'operation actor_did is invalid DID',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.putOperation({\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.some.col',\n          key: '',\n          method: Method.CREATE,\n          payload: validPayload0,\n        }),\n      ).rejects.toEqual(\n        new ConnectError('operation key is required', Code.InvalidArgument),\n      )\n      await expect(\n        client.putOperation({\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.UNSPECIFIED,\n          payload: validPayload0,\n        }),\n      ).rejects.toEqual(\n        new ConnectError('operation method is invalid', Code.InvalidArgument),\n      )\n      await expect(\n        client.putOperation({\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.CREATE,\n          payload: invalidPayload,\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'payload must be a valid JSON when method is CREATE or UPDATE',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.putOperation({\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.UPDATE,\n          payload: invalidPayload,\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'payload must be a valid JSON when method is CREATE or UPDATE',\n          Code.InvalidArgument,\n        ),\n      )\n      await expect(\n        client.putOperation({\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.DELETE,\n          payload: validPayload0,\n        }),\n      ).rejects.toEqual(\n        new ConnectError(\n          'cannot specify a payload when method is DELETE',\n          Code.InvalidArgument,\n        ),\n      )\n    })\n\n    it('puts operations.', async () => {\n      const res1 = await client.putOperation({\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.some.col',\n        key: 'key1',\n        method: Method.CREATE,\n        payload: validPayload0,\n      })\n      const res2 = await client.putOperation({\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.other.col#id',\n        key: 'key1',\n        method: Method.UPDATE,\n        payload: validPayload1,\n      })\n\n      expect(res1.operation?.id).toBe('1')\n      expect(res2.operation?.id).toBe('2')\n      expect(await dumpOps(bsync.ctx.db)).toStrictEqual([\n        {\n          id: 1,\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.CREATE,\n          payload: validPayload0,\n          createdAt: expect.any(Date),\n        },\n        {\n          id: 2,\n          actorDid: 'did:example:a',\n          namespace: 'app.bsky.other.col#id',\n          key: 'key1',\n          method: Method.UPDATE,\n          payload: validPayload1,\n          createdAt: expect.any(Date),\n        },\n      ])\n    })\n\n    it('returns the operations on creation.', async () => {\n      const res = await client.putOperation({\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.some.col',\n        key: 'key1',\n        method: Method.CREATE,\n        payload: validPayload0,\n      })\n\n      const op = res.operation\n      assert(op)\n      // Compare each field individually to avoid custom serialization by proto response objects.\n      expect(op.id).toBe('3')\n      expect(op.actorDid).toBe('did:example:a')\n      expect(op.namespace).toBe('app.bsky.some.col')\n      expect(op.key).toBe('key1')\n      expect(op.method).toBe(Method.CREATE)\n      expect(op.payload).toEqual(new Uint8Array(validPayload0))\n    })\n  })\n\n  describe('scanOperations', () => {\n    it('requires auth.', async () => {\n      // unauthed\n      const unauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n      })\n      const tryScanOperations1 = unauthedClient.scanOperations({})\n      await expect(tryScanOperations1).rejects.toEqual(\n        new ConnectError('missing auth', Code.Unauthenticated),\n      )\n      // bad auth\n      const badauthedClient = createClient({\n        httpVersion: '1.1',\n        baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,\n        interceptors: [authWithApiKey('key-bad')],\n      })\n      const tryScanOperations2 = badauthedClient.scanOperations({})\n      await expect(tryScanOperations2).rejects.toEqual(\n        new ConnectError('invalid api key', Code.Unauthenticated),\n      )\n    })\n\n    it('pages over created ops.', async () => {\n      // add 100 ops\n      for (let i = 0; i < 100; ++i) {\n        await client.putOperation({\n          actorDid: `did:example:${i}`,\n          namespace: 'app.bsky.some.col',\n          key: 'key1',\n          method: Method.CREATE,\n          payload: validPayload0,\n        })\n      }\n\n      let cursor: string | undefined\n      const operations: Operation[] = []\n      do {\n        const res = await client.scanOperations({\n          cursor,\n          limit: 30,\n        })\n        operations.push(...res.operations)\n        cursor = res.operations.length ? res.cursor : undefined\n      } while (cursor)\n\n      expect(operations.length).toEqual(100)\n      const operationIds = operations.map((op) => parseInt(op.id, 10))\n      const ascending = (a: number, b: number) => a - b\n      expect(operationIds).toEqual([...operationIds].sort(ascending))\n    })\n\n    it('supports long-poll, finding an operation.', async () => {\n      const scanPromise = client.scanOperations({})\n      await wait(100) // would be complete by now if it wasn't long-polling for an item\n      const { operation } = await client.putOperation({\n        actorDid: 'did:example:a',\n        namespace: 'app.bsky.some.col',\n        key: 'key1',\n        method: Method.CREATE,\n        payload: validPayload0,\n      })\n      const res = await scanPromise\n      expect(res.operations.length).toEqual(1)\n      expect(res.operations[0]).toEqual(operation)\n      expect(res.cursor).toEqual(operation?.id)\n    })\n\n    it('supports long-poll, not finding an operation.', async () => {\n      const res = await client.scanOperations({})\n      expect(res.cursor).toEqual('')\n      expect(res.operations).toEqual([])\n    })\n  })\n})\n\nconst dumpOps = async (db: Database) => {\n  return db.db\n    .selectFrom('operation')\n    .selectAll()\n    .orderBy('id', 'asc')\n    .execute()\n}\n\nconst clearOps = async (db: Database) => {\n  await db.db.deleteFrom('operation').execute()\n}\n"
  },
  {
    "path": "packages/bsync/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/bsync/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/bsync/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/common/CHANGELOG.md",
    "content": "# @atproto/common\n\n## 0.5.15\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-data@0.0.14\n  - @atproto/common-web@0.4.19\n  - @atproto/lex-cbor@0.0.15\n\n## 0.5.14\n\n### Patch Changes\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unnecessary validation of already properly formatted ISO date string\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-data@0.0.13\n  - @atproto/common-web@0.4.18\n  - @atproto/lex-cbor@0.0.14\n\n## 0.5.13\n\n### Patch Changes\n\n- Updated dependencies [[`66b7295`](https://github.com/bluesky-social/atproto/commit/66b72950e8bcb39cac3382116bd282b3bb692f16)]:\n  - @atproto/lex-cbor@0.0.13\n\n## 0.5.12\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n  - @atproto/common-web@0.4.17\n  - @atproto/lex-cbor@0.0.12\n\n## 0.5.11\n\n### Patch Changes\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-cbor@0.0.11\n  - @atproto/lex-data@0.0.11\n  - @atproto/common-web@0.4.16\n\n## 0.5.10\n\n### Patch Changes\n\n- [`49b3806`](https://github.com/bluesky-social/atproto/commit/49b38069ed4b5bd1ef71e967c78e5123b1c1f6f1) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update dependency on `@atproto/common-web`\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n  - @atproto/common-web@0.4.15\n  - @atproto/lex-cbor@0.0.10\n\n## 0.5.9\n\n### Patch Changes\n\n- Updated dependencies [[`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`ecf5921`](https://github.com/bluesky-social/atproto/commit/ecf59214d59d9d2530c197c0679d26e76c6a60ef), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/common-web@0.4.13\n  - @atproto/lex-cbor@0.0.9\n  - @atproto/lex-data@0.0.9\n\n## 0.5.8\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/common-web@0.4.12\n  - @atproto/lex-cbor@0.0.8\n\n## 0.5.7\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-cbor@0.0.7\n  - @atproto/common-web@0.4.11\n\n## 0.5.6\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n  - @atproto/common-web@0.4.10\n  - @atproto/lex-cbor@0.0.6\n\n## 0.5.5\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n  - @atproto/common-web@0.4.9\n  - @atproto/lex-cbor@0.0.5\n\n## 0.5.4\n\n### Patch Changes\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n  - @atproto/lex-cbor@0.0.4\n  - @atproto/common-web@0.4.8\n\n## 0.5.3\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`7e1d458`](https://github.com/bluesky-social/atproto/commit/7e1d45877bca0f615e7b1313cfcc66823b3de758)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/common-web@0.4.7\n  - @atproto/lex-cbor@0.0.3\n\n## 0.5.2\n\n### Patch Changes\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4)]:\n  - @atproto/lex-data@0.0.2\n  - @atproto/lex-cbor@0.0.2\n  - @atproto/common-web@0.4.6\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-cbor@0.0.1\n  - @atproto/lex-data@0.0.1\n  - @atproto/common-web@0.4.5\n\n## 0.5.0\n\n### Minor Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `utf8ToB64Url` and `b64UrlToUtf8` utilities\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `sha256ToCid` utility\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate \"ipld\" functions (use `@atproto/lex-data`, `@atproto/lex-json` and `@atproto/lex-cbor` instead)\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n\n## 0.4.12\n\n### Patch Changes\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/common-web@0.4.3\n\n## 0.4.11\n\n### Patch Changes\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/common-web@0.4.2\n\n## 0.4.10\n\n### Patch Changes\n\n- [#3672](https://github.com/bluesky-social/atproto/pull/3672) [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144) Thanks [@dholms](https://github.com/dholms)! - Add DASL CID parser\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common-web@0.4.1\n\n## 0.4.9\n\n### Patch Changes\n\n- [#2519](https://github.com/bluesky-social/atproto/pull/2519) [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f) Thanks [@dholms](https://github.com/dholms)! - Add renameIfExists function\n\n## 0.4.8\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39)]:\n  - @atproto/common-web@0.4.0\n\n## 0.4.7\n\n### Patch Changes\n\n- [#3481](https://github.com/bluesky-social/atproto/pull/3481) [`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4) Thanks [@dholms](https://github.com/dholms)! - Parse safe uint64 as number\n\n## 0.4.6\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/common-web@0.3.2\n\n## 0.4.5\n\n### Patch Changes\n\n- [#3178](https://github.com/bluesky-social/atproto/pull/3178) [`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor adaptation of VerifyCidTransform\n\n## 0.4.4\n\n### Patch Changes\n\n- [#2834](https://github.com/bluesky-social/atproto/pull/2834) [`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow contentEncoding to be an array for consistency with typing of headers\n\n## 0.4.3\n\n### Patch Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - add streamToNodeBuffer utility to convert Uint8Array (async) iterables to Buffer\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]:\n  - @atproto/common-web@0.3.1\n\n## 0.4.2\n\n### Patch Changes\n\n- [#2464](https://github.com/bluesky-social/atproto/pull/2464) [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor optimization\n\n## 0.4.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add obfuscation utilities\n\n## 0.4.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common-web@0.3.0\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n\n## 0.3.3\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/common-web@0.2.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/common-web@0.2.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/common-web@0.2.1\n"
  },
  {
    "path": "packages/common/README.md",
    "content": "# @atproto/common\n\nShared TypeScript code for other `@atproto/*` packages. This package is oriented towards writing servers, and is not designed to be browser-compatible.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/common)](https://www.npmjs.com/package/@atproto/common)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/common/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Common',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/common/package.json",
    "content": "{\n  \"name\": \"@atproto/common\",\n  \"version\": \"0.5.15\",\n  \"license\": \"MIT\",\n  \"description\": \"Shared web-platform-friendly code for atproto libraries\",\n  \"keywords\": [\n    \"atproto\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/common\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"multiformats\": \"^9.9.0\",\n    \"pino\": \"^8.21.0\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\",\n    \"uint8arrays\": \"3.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/common/src/buffers.ts",
    "content": "export function ui8ToBuffer(bytes: Uint8Array): Buffer {\n  return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n}\n\nexport function ui8ToArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n  return bytes.buffer.slice(\n    bytes.byteOffset,\n    bytes.byteLength + bytes.byteOffset,\n  )\n}\n"
  },
  {
    "path": "packages/common/src/dates.ts",
    "content": "// Normalize date strings to simplified ISO so that the lexical sort preserves temporal sort.\n// Rather than failing on an invalid date format, returns valid unix epoch.\nexport function toSimplifiedISOSafe(dateStr: string) {\n  const date = new Date(dateStr)\n  if (isNaN(date.getTime())) {\n    return new Date(0).toISOString()\n  }\n  const iso = date.toISOString()\n\n  // Date.toISOString() always returns `YYYY-MM-DDTHH:mm:ss.sssZ` or\n  // `±YYYYYY-MM-DDTHH:mm:ss.sssZ`\n  // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)\n  // However, the leading `±` and 6 digit year can break lexical sorting, so we\n  // need to catch those cases and return a safe value.\n  if (iso.startsWith('-') || iso.startsWith('+')) {\n    return new Date(0).toISOString()\n  }\n\n  return iso // YYYY-MM-DDTHH:mm:ss.sssZ\n}\n"
  },
  {
    "path": "packages/common/src/env.ts",
    "content": "import { parseIntWithFallback } from '@atproto/common-web'\n\nexport const envInt = (name: string): number | undefined => {\n  const str = process.env[name]\n  return parseIntWithFallback(str, undefined)\n}\n\nexport const envStr = (name: string): string | undefined => {\n  const str = process.env[name]\n  if (str === undefined || str.length === 0) return undefined\n  return str\n}\n\nexport const envBool = (name: string): boolean | undefined => {\n  const str = process.env[name]\n  if (str === 'true' || str === '1') return true\n  if (str === 'false' || str === '0') return false\n  return undefined\n}\n\nexport const envList = (name: string): string[] => {\n  const str = process.env[name]\n  if (str === undefined || str.length === 0) return []\n  return str.split(',')\n}\n"
  },
  {
    "path": "packages/common/src/fs.ts",
    "content": "import { constants } from 'node:fs'\nimport fs from 'node:fs/promises'\nimport { isErrnoException } from '@atproto/common-web'\n\nexport const fileExists = async (location: string): Promise<boolean> => {\n  try {\n    await fs.access(location, constants.F_OK)\n    return true\n  } catch (err) {\n    if (isErrnoException(err) && err.code === 'ENOENT') {\n      return false\n    }\n    throw err\n  }\n}\n\nexport const readIfExists = async (\n  filepath: string,\n): Promise<Uint8Array | undefined> => {\n  try {\n    return await fs.readFile(filepath)\n  } catch (err) {\n    if (isErrnoException(err) && err.code === 'ENOENT') {\n      return\n    }\n    throw err\n  }\n}\n\nexport const rmIfExists = async (\n  filepath: string,\n  recursive = false,\n): Promise<void> => {\n  try {\n    await fs.rm(filepath, { recursive })\n  } catch (err) {\n    if (isErrnoException(err) && err.code === 'ENOENT') {\n      return\n    }\n    throw err\n  }\n}\n\nexport const renameIfExists = async (\n  oldPath: string,\n  newPath: string,\n): Promise<void> => {\n  try {\n    await fs.rename(oldPath, newPath)\n  } catch (err) {\n    if (isErrnoException(err) && err.code === 'ENOENT') {\n      return\n    }\n    throw err\n  }\n}\n"
  },
  {
    "path": "packages/common/src/index.ts",
    "content": "export * from '@atproto/common-web'\nexport * from './buffers'\nexport * from './dates'\nexport * from './env'\nexport * from './fs'\nexport * from './ipld'\nexport * from './ipld-multi'\nexport * from './logger'\nexport * from './obfuscate'\nexport * from './streams'\n"
  },
  {
    "path": "packages/common/src/ipld-multi.ts",
    "content": "import { decodeAll } from '@atproto/lex-cbor'\nimport { LexValue } from '@atproto/lex-data'\n\n/**\n * @deprecated Use {@link decodeAll} from `@atproto/lex-cbor` instead.\n */\nexport function cborDecodeMulti(encoded: Uint8Array): LexValue[] {\n  return Array.from(decodeAll(encoded))\n}\n"
  },
  {
    "path": "packages/common/src/ipld.ts",
    "content": "import { createHash } from 'node:crypto'\nimport { Transform } from 'node:stream'\nimport { Block, ByteView, encode as encodeBlock } from 'multiformats/block'\nimport { sha256 as hasher } from 'multiformats/hashes/sha2'\nimport { cidForLex, decode, encode } from '@atproto/lex-cbor'\nimport {\n  CBOR_DATA_CODEC,\n  type CID,\n  Cid,\n  LexValue,\n  asMultiformatsCID,\n  // eslint-disable-next-line\n  cidForCbor,\n  cidForRawHash,\n  decodeCid,\n  isCidForBytes,\n  isTypedLexMap,\n  validateCidString,\n} from '@atproto/lex-data'\n\n/**\n * @deprecated Use {@link encode} from `@atproto/lex-cbor` instead.\n */\nconst cborEncodeLegacy = encode as <T = unknown>(data: T) => ByteView<T>\nexport { cborEncodeLegacy as cborEncode }\n\n/**\n * @deprecated Use {@link decode} from `@atproto/lex-cbor` instead.\n */\nconst cborDecodeLegacy = decode as <T = unknown>(bytes: ByteView<T>) => T\nexport { cborDecodeLegacy as cborDecode }\n\n/**\n * @deprecated Use {@link encode} and {@link cidForCbor} from `@atproto/lex-cbor` instead.\n */\nexport async function dataToCborBlock<T>(value: T): Promise<Block<T>> {\n  return encodeBlock<T, 0x71, 0x12>({\n    value,\n    codec: {\n      name: 'at-cbor', // Not actually used\n      code: CBOR_DATA_CODEC,\n      encode: encode as (data: T) => ByteView<T>,\n    },\n    hasher,\n  })\n}\n\n/**\n * @deprecated Use {@link cidForLex} from `@atproto/lex-cbor` instead.\n */\nasync function cidForCborLegacy(data: unknown): Promise<CID> {\n  return asMultiformatsCID(await cidForLex(data as LexValue))\n}\nexport { cidForCborLegacy as cidForCbor }\n\n/**\n * @deprecated Use {@link validateCidString} from '@atproto/lex-data' instead.\n */\nexport async function isValidCid(cidStr: string): Promise<boolean> {\n  // @NOTE we keep the wrapper function to return a Promise (for backward\n  // compatibility).\n  return validateCidString(cidStr)\n}\n\n/**\n * @deprecated Use {@link decode} from `@atproto/lex-cbor`, and {@link isTypedLexMap} from `@atproto/lex-data` instead.\n */\nexport function cborBytesToRecord(bytes: Uint8Array): Record<string, unknown> {\n  const data = decode(bytes) as LexValue\n  if (isTypedLexMap(data)) return data\n\n  throw new Error(`Expected record with $type property`)\n}\n\n/**\n * @deprecated Use {@link isCidForBytes} from `@atproto/lex-cbor` instead.\n */\nexport async function verifyCidForBytes(\n  cid: Cid,\n  bytes: Uint8Array,\n): Promise<void> {\n  if (!(await isCidForBytes(cid, bytes))) {\n    throw new Error(`Not a valid CID for bytes (${cid.toString()})`)\n  }\n}\n\n/**\n * @deprecated Use {@link cidForRawHash} from `@atproto/lex-cbor` instead.\n */\nexport function sha256RawToCid(hash: Uint8Array): CID {\n  return asMultiformatsCID(cidForRawHash(hash))\n}\n\n/**\n * @deprecated Use {@link decodeCid} from `@atproto/lex-cbor` instead.\n */\nexport function parseCidFromBytes(bytes: Uint8Array): CID {\n  return asMultiformatsCID(decodeCid(bytes, { flavor: 'dasl' }))\n}\n\nexport class VerifyCidTransform extends Transform {\n  constructor(public cid: Cid) {\n    const hasher = createHash('sha256')\n    super({\n      transform(chunk, encoding, callback) {\n        hasher.update(chunk)\n        callback(null, chunk)\n      },\n      flush(callback) {\n        try {\n          const actual = sha256RawToCid(hasher.digest())\n          if (actual.equals(cid)) {\n            return callback()\n          } else {\n            return callback(new VerifyCidError(cid, actual))\n          }\n        } catch (err) {\n          return callback(asError(err))\n        }\n      },\n    })\n  }\n}\n\nconst asError = (err: unknown): Error =>\n  err instanceof Error ? err : new Error('Unexpected error', { cause: err })\n\nexport class VerifyCidError extends Error {\n  constructor(\n    public expected: Cid,\n    public actual: Cid,\n  ) {\n    super('Bad cid check')\n  }\n}\n"
  },
  {
    "path": "packages/common/src/logger.ts",
    "content": "import { destination, pino } from 'pino'\n\nconst allSystemsEnabled = !process.env.LOG_SYSTEMS\nconst enabledSystems = (process.env.LOG_SYSTEMS || '')\n  .replace(',', ' ')\n  .split(' ')\n\nconst enabledEnv = process.env.LOG_ENABLED\nconst enabled =\n  enabledEnv === 'true' || enabledEnv === 't' || enabledEnv === '1'\n\nconst level = process.env.LOG_LEVEL || 'info'\n\nconst config = {\n  enabled,\n  level,\n}\n\nconst rootLogger = process.env.LOG_DESTINATION\n  ? pino(config, destination(process.env.LOG_DESTINATION))\n  : pino(config)\n\nconst subsystems: Record<string, pino.Logger> = {}\n\nexport const subsystemLogger = (name: string): pino.Logger => {\n  if (subsystems[name]) return subsystems[name]\n  const subsystemEnabled =\n    enabled && (allSystemsEnabled || enabledSystems.indexOf(name) > -1)\n\n  // can't disable child loggers, so we just set their level to fatal to effectively turn them off\n  subsystems[name] = rootLogger.child(\n    { name },\n    { level: subsystemEnabled ? level : 'silent' },\n  )\n  return subsystems[name]\n}\n"
  },
  {
    "path": "packages/common/src/obfuscate.ts",
    "content": "export function obfuscateEmail(email: string) {\n  const [local, domain] = email.split('@')\n  return `${obfuscateWord(local)}@${obfuscateWord(domain)}`\n}\n\nexport function obfuscateWord(word: string) {\n  return `${word.charAt(0)}***${word.charAt(word.length - 1)}`\n}\n\nexport function obfuscateHeaders(headers: Record<string, string>) {\n  const obfuscatedHeaders: Record<string, string> = {}\n  for (const key in headers) {\n    if (key.toLowerCase() === 'authorization') {\n      obfuscatedHeaders[key] = obfuscateAuthHeader(headers[key])\n    } else if (key.toLowerCase() === 'dpop') {\n      obfuscatedHeaders[key] = obfuscateJwt(headers[key]) || 'Invalid'\n    } else {\n      obfuscatedHeaders[key] = headers[key]\n    }\n  }\n  return obfuscatedHeaders\n}\n\nexport function obfuscateAuthHeader(authHeader: string): string {\n  // This is a hot path (runs on every request). Avoid using split() or regex.\n\n  const spaceIdx = authHeader.indexOf(' ')\n  if (spaceIdx === -1) return 'Invalid'\n\n  const type = authHeader.slice(0, spaceIdx)\n  switch (type.toLowerCase()) {\n    case 'bearer':\n    case 'dpop':\n      return `${type} ${obfuscateBearer(authHeader.slice(spaceIdx + 1))}`\n    case 'basic':\n      return `${type} ${obfuscateBasic(authHeader.slice(spaceIdx + 1)) || 'Invalid'}`\n    default:\n      return `Invalid`\n  }\n}\n\nexport function obfuscateBasic(token: string): null | string {\n  if (!token) return null\n  const buffer = Buffer.from(token, 'base64')\n  if (!buffer.length) return null // Buffer.from will silently ignore invalid base64 chars\n  const authHeader = buffer.toString('utf8')\n  const colIdx = authHeader.indexOf(':')\n  if (colIdx === -1) return null\n  const username = authHeader.slice(0, colIdx)\n  return `${username}:***`\n}\n\nexport function obfuscateBearer(token: string): string {\n  return obfuscateJwt(token) || obfuscateToken(token)\n}\n\nexport function obfuscateToken(token: string): string {\n  if (token.length >= 12) return obfuscateWord(token)\n  return token ? '***' : ''\n}\n\nexport function obfuscateJwt(token: string): null | string {\n  const firstDot = token.indexOf('.')\n  if (firstDot === -1) return null\n\n  const secondDot = token.indexOf('.', firstDot + 1)\n  if (secondDot === -1) return null\n\n  // Expected to be missing\n  const thirdDot = token.indexOf('.', secondDot + 1)\n  if (thirdDot !== -1) return null\n\n  try {\n    const payloadEnc = token.slice(firstDot + 1, secondDot)\n    const payloadJson = Buffer.from(payloadEnc, 'base64').toString('utf8')\n    const payload = JSON.parse(payloadJson)\n    if (typeof payload.sub === 'string') return payload.sub\n  } catch {\n    // Invalid JWT\n    return null\n  }\n\n  // Strip the signature\n  return token.slice(0, secondDot) + '.obfuscated'\n}\n"
  },
  {
    "path": "packages/common/src/streams.ts",
    "content": "import {\n  Duplex,\n  PassThrough,\n  Readable,\n  Stream,\n  Transform,\n  TransformCallback,\n  pipeline,\n} from 'node:stream'\nimport { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib'\n\nexport const forwardStreamErrors = (...streams: Stream[]) => {\n  for (let i = 1; i < streams.length; ++i) {\n    const prev = streams[i - 1]\n    const next = streams[i]\n\n    prev.once('error', (err) => next.emit('error', err))\n  }\n}\n\nexport const cloneStream = (stream: Readable): Readable => {\n  const passthrough = new PassThrough()\n  forwardStreamErrors(stream, passthrough)\n  return stream.pipe(passthrough)\n}\n\nexport const streamSize = async (stream: Readable): Promise<number> => {\n  let size = 0\n  for await (const chunk of stream) {\n    size += Buffer.byteLength(chunk)\n  }\n  return size\n}\n\nexport const streamToBytes = async (stream: AsyncIterable<Uint8Array>) =>\n  // @NOTE Though Buffer is a sub-class of Uint8Array, we have observed\n  // inconsistencies when using a Buffer in place of Uint8Array. For this\n  // reason, we convert the Buffer to a Uint8Array.\n  new Uint8Array(await streamToNodeBuffer(stream))\n\n// streamToBuffer identifier name already taken by @atproto/common-web\nexport const streamToNodeBuffer = async (\n  stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n): Promise<Buffer> => {\n  const chunks: Uint8Array[] = []\n  let totalLength = 0 // keep track of total length for Buffer.concat\n  for await (const chunk of stream) {\n    if (chunk instanceof Uint8Array) {\n      chunks.push(chunk)\n      totalLength += Buffer.byteLength(chunk)\n    } else {\n      throw new TypeError('expected Uint8Array')\n    }\n  }\n  return Buffer.concat(chunks, totalLength)\n}\n\nexport const byteIterableToStream = (\n  iter: AsyncIterable<Uint8Array>,\n): Readable => {\n  return Readable.from(iter, { objectMode: false })\n}\n\nexport const bytesToStream = (bytes: Uint8Array): Readable => {\n  const stream = new Readable()\n  stream.push(bytes)\n  stream.push(null)\n  return stream\n}\n\nexport class MaxSizeChecker extends Transform {\n  totalSize = 0\n  constructor(\n    public maxSize: number,\n    public createError: () => Error,\n  ) {\n    super()\n  }\n  _transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback) {\n    this.totalSize += chunk.length\n    if (this.totalSize > this.maxSize) {\n      cb(this.createError())\n    } else {\n      cb(null, chunk)\n    }\n  }\n}\n\nexport function decodeStream(\n  stream: Readable,\n  contentEncoding?: string | string[],\n): Readable\nexport function decodeStream(\n  stream: AsyncIterable<Uint8Array>,\n  contentEncoding?: string | string[],\n): AsyncIterable<Uint8Array> | Readable\nexport function decodeStream(\n  stream: Readable | AsyncIterable<Uint8Array>,\n  contentEncoding?: string | string[],\n): Readable | AsyncIterable<Uint8Array> {\n  const decoders = createDecoders(contentEncoding)\n  if (decoders.length === 0) return stream\n  return pipeline([stream as Readable, ...decoders], () => {}) as Duplex\n}\n\n/**\n * Create a series of decoding streams based on the content-encoding header. The\n * resulting streams should be piped together to decode the content.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9110#section-8.4.1}\n */\nexport function createDecoders(contentEncoding?: string | string[]): Duplex[] {\n  const decoders: Duplex[] = []\n\n  if (contentEncoding?.length) {\n    const encodings: string[] = Array.isArray(contentEncoding)\n      ? contentEncoding.flatMap(commaSplit)\n      : contentEncoding.split(',')\n    for (const encoding of encodings) {\n      const normalizedEncoding = normalizeEncoding(encoding)\n\n      // @NOTE\n      // > The default (identity) encoding [...] is used only in the\n      // > Accept-Encoding header, and SHOULD NOT be used in the\n      // > Content-Encoding header.\n      if (normalizedEncoding === 'identity') continue\n\n      decoders.push(createDecoder(normalizedEncoding))\n    }\n  }\n\n  return decoders.reverse()\n}\n\nfunction commaSplit(header: string): string[] {\n  return header.split(',')\n}\n\nfunction normalizeEncoding(encoding: string) {\n  // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1\n  // > All content-coding values are case-insensitive...\n  return encoding.trim().toLowerCase()\n}\n\nfunction createDecoder(normalizedEncoding: string): Duplex {\n  switch (normalizedEncoding) {\n    // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2\n    case 'gzip':\n    case 'x-gzip':\n      return createGunzip()\n    case 'deflate':\n      return createInflate()\n    case 'br':\n      return createBrotliDecompress()\n    case 'identity':\n      return new PassThrough()\n    default:\n      throw new TypeError(\n        `Unsupported content-encoding: \"${normalizedEncoding}\"`,\n      )\n  }\n}\n"
  },
  {
    "path": "packages/common/tests/ipld-multi.test.ts",
    "content": "import * as ui8 from 'uint8arrays'\nimport { CID } from '@atproto/lex-data'\nimport { cborDecodeMulti, cborEncode } from '../src'\n\ndescribe('ipld decode multi', () => {\n  it('decodes concatenated dag-cbor messages', async () => {\n    const one = {\n      a: 123,\n      b: CID.parse(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n    const two = {\n      c: new Uint8Array([1, 2, 3]),\n      d: CID.parse(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n    const encoded = ui8.concat([cborEncode(one), cborEncode(two)])\n    const decoded = cborDecodeMulti(encoded)\n    expect(decoded.length).toBe(2)\n    expect(decoded[0]).toEqual(one)\n    expect(decoded[1]).toEqual(two)\n  })\n\n  it('parses safe ints as number', async () => {\n    const one = {\n      test: Number.MAX_SAFE_INTEGER,\n    }\n    const encoded = cborEncode(one)\n    const decoded = cborDecodeMulti(encoded)\n    expect(Number.isInteger(decoded[0]?.['test'])).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/common/tests/ipld-vectors.ts",
    "content": "import { parseCid } from '@atproto/lex-data'\n\nexport const vectors = [\n  {\n    name: 'basic',\n    json: {\n      string: 'abc',\n      unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',\n      integer: 123,\n      bool: true,\n      null: null,\n      array: ['abc', 'def', 'ghi'],\n      object: {\n        string: 'abc',\n        number: 123,\n        bool: true,\n        arr: ['abc', 'def', 'ghi'],\n      },\n    },\n    ipld: {\n      string: 'abc',\n      unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',\n      integer: 123,\n      bool: true,\n      null: null,\n      array: ['abc', 'def', 'ghi'],\n      object: {\n        string: 'abc',\n        number: 123,\n        bool: true,\n        arr: ['abc', 'def', 'ghi'],\n      },\n    },\n    cbor: new Uint8Array([\n      167, 100, 98, 111, 111, 108, 245, 100, 110, 117, 108, 108, 246, 101, 97,\n      114, 114, 97, 121, 131, 99, 97, 98, 99, 99, 100, 101, 102, 99, 103, 104,\n      105, 102, 111, 98, 106, 101, 99, 116, 164, 99, 97, 114, 114, 131, 99, 97,\n      98, 99, 99, 100, 101, 102, 99, 103, 104, 105, 100, 98, 111, 111, 108, 245,\n      102, 110, 117, 109, 98, 101, 114, 24, 123, 102, 115, 116, 114, 105, 110,\n      103, 99, 97, 98, 99, 102, 115, 116, 114, 105, 110, 103, 99, 97, 98, 99,\n      103, 105, 110, 116, 101, 103, 101, 114, 24, 123, 103, 117, 110, 105, 99,\n      111, 100, 101, 120, 47, 97, 126, 195, 182, 195, 177, 194, 169, 226, 189,\n      152, 226, 152, 142, 240, 147, 139, 147, 240, 159, 152, 128, 240, 159, 145,\n      168, 226, 128, 141, 240, 159, 145, 169, 226, 128, 141, 240, 159, 145, 167,\n      226, 128, 141, 240, 159, 145, 167,\n    ]),\n    cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',\n  },\n  {\n    name: 'ipld',\n    json: {\n      a: {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      b: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      },\n      c: {\n        $type: 'blob',\n        ref: {\n          $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n        },\n        mimeType: 'image/jpeg',\n        size: 10000,\n      },\n    },\n    ipld: {\n      a: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n      b: new Uint8Array([\n        156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,\n        65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,\n        204, 141,\n      ]),\n      c: {\n        $type: 'blob',\n        ref: parseCid(\n          'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n        ),\n        mimeType: 'image/jpeg',\n        size: 10000,\n      },\n    },\n    cbor: new Uint8Array([\n      163, 97, 97, 216, 42, 88, 37, 0, 1, 113, 18, 32, 101, 6, 42, 90, 90, 0,\n      252, 22, 215, 60, 105, 68, 35, 124, 203, 193, 91, 28, 74, 114, 52, 72,\n      147, 54, 137, 29, 9, 23, 65, 162, 57, 208, 97, 98, 88, 32, 156, 81, 17,\n      142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65, 60, 242, 11,\n      98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204, 141, 97, 99,\n      164, 99, 114, 101, 102, 216, 42, 88, 37, 0, 1, 85, 18, 32, 66, 88, 207,\n      255, 120, 246, 19, 105, 118, 151, 86, 63, 146, 108, 145, 229, 211, 87, 77,\n      46, 162, 90, 231, 237, 146, 214, 235, 252, 35, 163, 136, 158, 100, 115,\n      105, 122, 101, 25, 39, 16, 101, 36, 116, 121, 112, 101, 100, 98, 108, 111,\n      98, 104, 109, 105, 109, 101, 84, 121, 112, 101, 106, 105, 109, 97, 103,\n      101, 47, 106, 112, 101, 103,\n    ]),\n    cid: 'bafyreihldkhcwijkde7gx4rpkkuw7pl6lbyu5gieunyc7ihactn5bkd2nm',\n  },\n  {\n    name: 'ipldArray',\n    json: [\n      {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      {\n        $link: 'bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke',\n      },\n      {\n        $link: 'bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy',\n      },\n    ],\n    ipld: [\n      parseCid('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),\n      parseCid('bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q'),\n      parseCid('bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke'),\n      parseCid('bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy'),\n    ],\n    cbor: new Uint8Array([\n      132, 216, 42, 88, 37, 0, 1, 113, 18, 32, 101, 6, 42, 90, 90, 0, 252, 22,\n      215, 60, 105, 68, 35, 124, 203, 193, 91, 28, 74, 114, 52, 72, 147, 54,\n      137, 29, 9, 23, 65, 162, 57, 208, 216, 42, 88, 37, 0, 1, 113, 18, 32, 206,\n      188, 253, 200, 24, 248, 158, 85, 31, 33, 95, 133, 103, 145, 125, 120, 196,\n      209, 14, 220, 33, 139, 148, 27, 165, 214, 150, 172, 255, 213, 142, 244,\n      216, 42, 88, 37, 0, 1, 113, 18, 32, 8, 206, 26, 37, 182, 8, 114, 225, 240,\n      224, 175, 74, 102, 104, 101, 188, 145, 99, 35, 55, 249, 209, 206, 95, 5,\n      220, 164, 199, 101, 33, 191, 81, 216, 42, 88, 37, 0, 1, 113, 18, 32, 163,\n      229, 185, 49, 71, 179, 93, 47, 159, 186, 73, 155, 92, 82, 150, 179, 147,\n      12, 56, 8, 177, 42, 60, 74, 164, 100, 227, 215, 175, 174, 41, 126,\n    ]),\n    cid: 'bafyreiaj3udmqlqrcbjxjayzuxwp64gt64olcbjfrkldzoqponpru6gq4m',\n  },\n  {\n    name: 'ipldNested',\n    json: {\n      a: {\n        b: [\n          {\n            d: [\n              {\n                $link:\n                  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              },\n              {\n                $link:\n                  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              },\n            ],\n            e: [\n              {\n                $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n              },\n              {\n                $bytes: 'iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas',\n              },\n            ],\n          },\n        ],\n      },\n    },\n    ipld: {\n      a: {\n        b: [\n          {\n            d: [\n              parseCid(\n                'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              ),\n              parseCid(\n                'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              ),\n            ],\n            e: [\n              new Uint8Array([\n                156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174,\n                161, 253, 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238,\n                190, 176, 26, 194, 204, 141,\n              ]),\n              new Uint8Array([\n                136, 79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237,\n                244, 244, 178, 194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14,\n                221, 125, 181, 171,\n              ]),\n            ],\n          },\n        ],\n      },\n    },\n    cbor: new Uint8Array([\n      161, 97, 97, 161, 97, 98, 129, 162, 97, 100, 130, 216, 42, 88, 37, 0, 1,\n      113, 18, 32, 101, 6, 42, 90, 90, 0, 252, 22, 215, 60, 105, 68, 35, 124,\n      203, 193, 91, 28, 74, 114, 52, 72, 147, 54, 137, 29, 9, 23, 65, 162, 57,\n      208, 216, 42, 88, 37, 0, 1, 113, 18, 32, 101, 6, 42, 90, 90, 0, 252, 22,\n      215, 60, 105, 68, 35, 124, 203, 193, 91, 28, 74, 114, 52, 72, 147, 54,\n      137, 29, 9, 23, 65, 162, 57, 208, 97, 101, 130, 88, 32, 156, 81, 17, 142,\n      242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65, 60, 242, 11, 98,\n      238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204, 141, 88, 32, 136,\n      79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237, 244, 244, 178,\n      194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14, 221, 125, 181, 171,\n    ]),\n    cid: 'bafyreid3imdulnhgeytpf6uk7zahjvrsqlofkmm5b5ub2maw4kqus6jp4i',\n  },\n  {\n    name: 'poorlyFormatted',\n    json: {\n      a: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      b: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      c: {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n        another: 'bad value',\n      },\n      d: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        another: 'bad value',\n      },\n      e: {\n        '/': 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      f: {\n        '/': {\n          bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        },\n      },\n    },\n    ipld: {\n      a: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      b: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      c: {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n        another: 'bad value',\n      },\n      d: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        another: 'bad value',\n      },\n      e: {\n        '/': 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      f: {\n        '/': {\n          bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        },\n      },\n    },\n    cbor: new Uint8Array([\n      166, 97, 97, 120, 59, 98, 97, 102, 121, 114, 101, 105, 100, 102, 97, 121,\n      118, 102, 117, 119, 113, 97, 55, 113, 108, 110, 111, 112, 100, 106, 105,\n      113, 114, 120, 122, 115, 54, 98, 108, 109, 111, 101, 117, 52, 114, 117,\n      106, 99, 106, 116, 110, 99, 105, 53, 98, 101, 108, 117, 100, 105, 114,\n      122, 50, 97, 97, 98, 120, 43, 110, 70, 69, 82, 106, 118, 76, 76, 105, 119,\n      57, 113, 109, 52, 53, 74, 114, 113, 72, 57, 81, 84, 122, 121, 67, 50, 76,\n      117, 49, 88, 98, 52, 110, 101, 54, 43, 115, 66, 114, 67, 122, 73, 48, 97,\n      99, 162, 101, 36, 108, 105, 110, 107, 120, 59, 98, 97, 102, 121, 114, 101,\n      105, 103, 111, 120, 116, 54, 52, 113, 103, 104, 121, 116, 122, 107, 114,\n      54, 105, 107, 55, 113, 118, 116, 122, 99, 55, 108, 121, 121, 116, 105,\n      113, 53, 120, 98, 98, 114, 111, 107, 98, 120, 106, 111, 119, 115, 50, 119,\n      112, 55, 118, 109, 111, 54, 113, 103, 97, 110, 111, 116, 104, 101, 114,\n      105, 98, 97, 100, 32, 118, 97, 108, 117, 101, 97, 100, 162, 102, 36, 98,\n      121, 116, 101, 115, 120, 43, 110, 70, 69, 82, 106, 118, 76, 76, 105, 119,\n      57, 113, 109, 52, 53, 74, 114, 113, 72, 57, 81, 84, 122, 121, 67, 50, 76,\n      117, 49, 88, 98, 52, 110, 101, 54, 43, 115, 66, 114, 67, 122, 73, 48, 103,\n      97, 110, 111, 116, 104, 101, 114, 105, 98, 97, 100, 32, 118, 97, 108, 117,\n      101, 97, 101, 161, 97, 47, 120, 59, 98, 97, 102, 121, 114, 101, 105, 103,\n      111, 120, 116, 54, 52, 113, 103, 104, 121, 116, 122, 107, 114, 54, 105,\n      107, 55, 113, 118, 116, 122, 99, 55, 108, 121, 121, 116, 105, 113, 53,\n      120, 98, 98, 114, 111, 107, 98, 120, 106, 111, 119, 115, 50, 119, 112, 55,\n      118, 109, 111, 54, 113, 97, 102, 161, 97, 47, 161, 101, 98, 121, 116, 101,\n      115, 120, 43, 110, 70, 69, 82, 106, 118, 76, 76, 105, 119, 57, 113, 109,\n      52, 53, 74, 114, 113, 72, 57, 81, 84, 122, 121, 67, 50, 76, 117, 49, 88,\n      98, 52, 110, 101, 54, 43, 115, 66, 114, 67, 122, 73, 48,\n    ]),\n    cid: 'bafyreico7wgbbfe6dpfsuednrtrlh6t2yjl6xq5rf32gl3pgwhwxk77cn4',\n  },\n]\n"
  },
  {
    "path": "packages/common/tests/ipld.test.ts",
    "content": "import * as ui8 from 'uint8arrays'\nimport {\n  cborDecode,\n  cborEncode,\n  cidForCbor,\n  ipldEquals,\n  ipldToJson,\n  jsonToIpld,\n} from '../src'\nimport { vectors } from './ipld-vectors'\n\ndescribe('ipld', () => {\n  for (const vector of vectors) {\n    it(`passes test vector: ${vector.name}`, async () => {\n      const ipld = jsonToIpld(vector.json)\n      const json = ipldToJson(ipld)\n      const cbor = cborEncode(ipld)\n      const ipldAgain = cborDecode(cbor)\n      const jsonAgain = ipldToJson(ipldAgain)\n      const cid = await cidForCbor(ipld)\n      expect(json).toEqual(vector.json)\n      expect(jsonAgain).toEqual(vector.json)\n      expect(ipldEquals(ipld, vector.ipld)).toBeTruthy()\n      expect(ipldEquals(ipldAgain, vector.ipld)).toBeTruthy()\n      expect(ui8.equals(cbor, vector.cbor)).toBeTruthy()\n      expect(cid.toString()).toEqual(vector.cid)\n    })\n  }\n})\n"
  },
  {
    "path": "packages/common/tests/streams.test.ts",
    "content": "import { PassThrough, Readable } from 'node:stream'\nimport * as streams from '../src/streams'\n\ndescribe('streams', () => {\n  describe('forwardStreamErrors', () => {\n    it('forwards errors through a set of streams', () => {\n      const streamA = new PassThrough()\n      const streamB = new PassThrough()\n      let streamBError: Error | null = null\n      const err = new Error('foo')\n\n      streamB.on('error', (err) => {\n        streamBError = err\n      })\n\n      streams.forwardStreamErrors(streamA, streamB)\n\n      streamA.emit('error', err)\n\n      expect(streamBError).toBe(err)\n    })\n  })\n\n  describe('cloneStream', () => {\n    it('should clone stream', () => {\n      const stream = new PassThrough()\n      let clonedError: Error | undefined\n      let clonedData: string | undefined\n\n      const cloned = streams.cloneStream(stream)\n\n      cloned.on('data', (data) => {\n        clonedData = String(data)\n      })\n      cloned.on('error', (err) => {\n        clonedError = err\n      })\n\n      stream.emit('data', 'foo')\n      stream.emit('error', new Error('foo error'))\n\n      expect(clonedData).toEqual('foo')\n      expect(clonedError?.message).toEqual('foo error')\n    })\n  })\n\n  describe('streamSize', () => {\n    it('reads entire stream and computes size', async () => {\n      const stream = Readable.from(['f', 'o', 'o'])\n\n      const size = await streams.streamSize(stream)\n\n      expect(size).toBe(3)\n    })\n\n    it('returns 0 for empty streams', async () => {\n      const stream = Readable.from([])\n      const size = await streams.streamSize(stream)\n\n      expect(size).toBe(0)\n    })\n  })\n\n  describe('streamToNodeBuffer', () => {\n    it('converts stream to byte array', async () => {\n      const stream = Readable.from(Buffer.from('foo'))\n      const bytes = await streams.streamToNodeBuffer(stream)\n\n      expect(bytes[0]).toBe('f'.charCodeAt(0))\n      expect(bytes[1]).toBe('o'.charCodeAt(0))\n      expect(bytes[2]).toBe('o'.charCodeAt(0))\n      expect(bytes.length).toBe(3)\n    })\n\n    it('converts async iterable to byte array', async () => {\n      const iterable = (async function* () {\n        yield Buffer.from('b')\n        yield Buffer.from('a')\n        yield new Uint8Array(['r'.charCodeAt(0)])\n      })()\n      const bytes = await streams.streamToNodeBuffer(iterable)\n\n      expect(bytes[0]).toBe('b'.charCodeAt(0))\n      expect(bytes[1]).toBe('a'.charCodeAt(0))\n      expect(bytes[2]).toBe('r'.charCodeAt(0))\n      expect(bytes.length).toBe(3)\n    })\n\n    it('throws error for non Uint8Array chunks', async () => {\n      const iterable: AsyncIterable<any> = (async function* () {\n        yield Buffer.from('b')\n        yield Buffer.from('a')\n        yield 'r'\n      })()\n\n      await expect(streams.streamToNodeBuffer(iterable)).rejects.toThrow(\n        'expected Uint8Array',\n      )\n    })\n  })\n\n  describe('byteIterableToStream', () => {\n    it('converts byte iterable to stream', async () => {\n      const iterable: AsyncIterable<Uint8Array> = {\n        async *[Symbol.asyncIterator]() {\n          yield new Uint8Array([0xa, 0xb])\n        },\n      }\n\n      const stream = streams.byteIterableToStream(iterable)\n\n      for await (const chunk of stream) {\n        expect(chunk[0]).toBe(0xa)\n        expect(chunk[1]).toBe(0xb)\n      }\n    })\n  })\n\n  describe('bytesToStream', () => {\n    it('converts byte array to readable stream', async () => {\n      const bytes = new Uint8Array([0xa, 0xb])\n      const stream = streams.bytesToStream(bytes)\n\n      for await (const chunk of stream) {\n        expect(chunk[0]).toBe(0xa)\n        expect(chunk[1]).toBe(0xb)\n      }\n    })\n  })\n\n  describe('MaxSizeChecker', () => {\n    it('destroys once max size is met', async () => {\n      const stream = new Readable()\n      const err = new Error('foo')\n      const checker = new streams.MaxSizeChecker(1, () => err)\n      let lastError: Error | undefined\n\n      const outStream = stream.pipe(checker)\n\n      outStream.on('error', (err) => {\n        lastError = err\n      })\n\n      const waitForStream = new Promise<void>((resolve) => {\n        stream.on('end', () => {\n          resolve()\n        })\n      })\n\n      expect(checker.totalSize).toBe(0)\n\n      stream.push(new Uint8Array([0xa]))\n      stream.push(new Uint8Array([0xb]))\n      stream.push(null)\n\n      await waitForStream\n\n      expect(checker.totalSize).toBe(2)\n      expect(checker.destroyed).toBe(true)\n      expect(lastError).toBe(err)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/common/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/common/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/common/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/common-web/CHANGELOG.md",
    "content": "# @atproto/common-web\n\n## 0.4.19\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-data@0.0.14\n  - @atproto/lex-json@0.0.14\n\n## 0.4.18\n\n### Patch Changes\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add possibility to `reject` `Deferrable` objects. Also allow specifying a generic for the Promise's value.\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `dedupeStrs` to preserve the type of input values which are more specific than `string`\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-json@0.0.13\n\n## 0.4.17\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n  - @atproto/lex-json@0.0.12\n\n## 0.4.16\n\n### Patch Changes\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-json@0.0.11\n  - @atproto/lex-data@0.0.11\n\n## 0.4.15\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-json@0.0.10\n\n## 0.4.14\n\n### Patch Changes\n\n- [#4580](https://github.com/bluesky-social/atproto/pull/4580) [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate the `didDocument` schema in favor of `@atproto/did`\n\n- [#4580](https://github.com/bluesky-social/atproto/pull/4580) [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow (and validate) `publicKeyJwk` properties in DID document verification methods\n\n- [#4580](https://github.com/bluesky-social/atproto/pull/4580) [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Validate DID document's `@context` and `authentication` properties.\n\n## 0.4.13\n\n### Patch Changes\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `noUndefinedVals` from `@atproto/common-web` instead of locally defined `stripUndefineds`\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/syntax@0.4.3\n  - @atproto/lex-json@0.0.9\n  - @atproto/lex-data@0.0.9\n\n## 0.4.12\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/lex-json@0.0.8\n\n## 0.4.11\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-json@0.0.7\n\n## 0.4.10\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n  - @atproto/lex-json@0.0.6\n\n## 0.4.9\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n  - @atproto/lex-json@0.0.5\n\n## 0.4.8\n\n### Patch Changes\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n  - @atproto/lex-json@0.0.4\n\n## 0.4.7\n\n### Patch Changes\n\n- [#4422](https://github.com/bluesky-social/atproto/pull/4422) [`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Restore `utf8ToB64Url` and `b64UrlToUtf8` utilities\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/lex-json@0.0.3\n\n## 0.4.6\n\n### Patch Changes\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4)]:\n  - @atproto/lex-data@0.0.2\n  - @atproto/lex-json@0.0.2\n\n## 0.4.5\n\n### Patch Changes\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-data@0.0.1\n  - @atproto/lex-json@0.0.1\n\n## 0.4.4\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use native base64 encoding/decoding when available\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix computation of grapheme length\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate \"ipld\" functions (use `@atproto/lex-data`, `@atproto/lex-json` and `@atproto/lex-cbor` instead)\n\n## 0.4.3\n\n### Patch Changes\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `aggregateErrors` utility\n\n## 0.4.2\n\n### Patch Changes\n\n- [#3798](https://github.com/bluesky-social/atproto/pull/3798) [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Optimize parsing of `CID` values\n\n## 0.4.1\n\n### Patch Changes\n\n- [#3672](https://github.com/bluesky-social/atproto/pull/3672) [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144) Thanks [@dholms](https://github.com/dholms)! - Add CAR Header type\n\n## 0.4.0\n\n### Minor Changes\n\n- [#3445](https://github.com/bluesky-social/atproto/pull/3445) [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Make keyBy typing stricter\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n## 0.3.2\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `createRetryable` utility function\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `allFulfilled` utility\n\n## 0.3.1\n\n### Patch Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add omit() utility\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - DID document parsing optimization\n\n- [#2835](https://github.com/bluesky-social/atproto/pull/2835) [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - ponyfill URL.canParse\n\n## 0.3.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n## 0.2.4\n\n### Patch Changes\n\n- [#2302](https://github.com/bluesky-social/atproto/pull/2302) [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0) Thanks [@dholms](https://github.com/dholms)! - Added methods for parsing labeler verification methods and services in DID Documents\n\n## 0.2.3\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n## 0.2.2\n\n### Patch Changes\n\n- [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7) Thanks [@devinivy](https://github.com/devinivy)! - Allow pds to serve did doc with credentials, API client to respect PDS listed in the did doc.\n\n## 0.2.1\n\n### Patch Changes\n\n- [#1568](https://github.com/bluesky-social/atproto/pull/1568) [`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b) Thanks [@dholms](https://github.com/dholms)! - Added lessThanAgoMs utility\n"
  },
  {
    "path": "packages/common-web/README.md",
    "content": "# @atproto/common-web\n\nShared TypeScript code for other `@atproto/*` packages, which is web-friendly (runs in-browser).\n\n[![NPM](https://img.shields.io/npm/v/@atproto/common)](https://www.npmjs.com/package/@atproto/common-web)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/common-web/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Common Web',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/common-web/package.json",
    "content": "{\n  \"name\": \"@atproto/common-web\",\n  \"version\": \"0.4.19\",\n  \"license\": \"MIT\",\n  \"description\": \"Shared web-platform-friendly code for atproto libraries\",\n  \"keywords\": [\n    \"atproto\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/common-web\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-json\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/common-web/src/arrays.ts",
    "content": "export function keyBy<T, K extends keyof T>(\n  arr: readonly T[],\n  key: K,\n): Map<T[K], T> {\n  return arr.reduce((acc, cur) => {\n    acc.set(cur[key], cur)\n    return acc\n  }, new Map<T[K], T>())\n}\n\nexport const mapDefined = <T, S>(\n  arr: T[],\n  fn: (obj: T) => S | undefined,\n): S[] => {\n  const output: S[] = []\n  for (const item of arr) {\n    const val = fn(item)\n    if (val !== undefined) {\n      output.push(val)\n    }\n  }\n  return output\n}\n"
  },
  {
    "path": "packages/common-web/src/async.ts",
    "content": "import { aggregateErrors, bailableWait } from './util'\n\n// reads values from a generator into a list\n// breaks when isDone signals `true` AND `waitFor` completes OR when a max length is reached\n// NOTE: does not signal generator to close. it *will* continue to produce values\nexport const readFromGenerator = async <T>(\n  gen: AsyncGenerator<T>,\n  isDone: (last?: T) => Promise<boolean> | boolean,\n  waitFor: Promise<unknown> = Promise.resolve(),\n  maxLength = Number.MAX_SAFE_INTEGER,\n): Promise<T[]> => {\n  const evts: T[] = []\n  let bail: undefined | (() => void)\n  let hasBroke = false\n  const awaitDone = async () => {\n    if (await isDone(evts.at(-1))) {\n      return true\n    }\n    const bailable = bailableWait(20)\n    await bailable.wait()\n    bail = bailable.bail\n    if (hasBroke) return false\n    return await awaitDone()\n  }\n  const breakOn: Promise<void> = new Promise((resolve) => {\n    waitFor.then(() => {\n      awaitDone().then(() => resolve())\n    })\n  })\n\n  try {\n    while (evts.length < maxLength) {\n      const maybeEvt = await Promise.race([gen.next(), breakOn])\n      if (!maybeEvt) break\n      const evt = maybeEvt as IteratorResult<T>\n      if (evt.done) break\n      evts.push(evt.value)\n    }\n  } finally {\n    hasBroke = true\n    bail && bail()\n  }\n  return evts\n}\n\nexport type Deferrable<T = void> = {\n  resolve: (value: T | PromiseLike<T>) => void\n  reject: (reason?: unknown) => void\n  complete: Promise<T>\n}\n\nexport function createDeferrable<T = void>(): Deferrable<T> {\n  let res: (value: T | PromiseLike<T>) => void\n  let rej: (reason?: unknown) => void\n  const promise = new Promise<T>((resolve, reject) => {\n    res = resolve\n    rej = reject\n  })\n  return { resolve: res!, reject: rej!, complete: promise }\n}\n\nexport const createDeferrables = (count: number): Deferrable[] => {\n  const list: Deferrable[] = []\n  for (let i = 0; i < count; i++) {\n    list.push(createDeferrable())\n  }\n  return list\n}\n\nexport const allComplete = async (deferrables: Deferrable[]): Promise<void> => {\n  await Promise.all(deferrables.map((d) => d.complete))\n}\n\nexport class AsyncBuffer<T> {\n  private buffer: T[] = []\n  private promise: Promise<void>\n  private resolve: () => void\n  private closed = false\n  private toThrow: unknown | undefined\n\n  constructor(public maxSize?: number) {\n    // Initializing to satisfy types/build, immediately reset by resetPromise()\n    this.promise = Promise.resolve()\n    this.resolve = () => null\n    this.resetPromise()\n  }\n\n  get curr(): T[] {\n    return this.buffer\n  }\n\n  get size(): number {\n    return this.buffer.length\n  }\n\n  get isClosed(): boolean {\n    return this.closed\n  }\n\n  resetPromise() {\n    this.promise = new Promise<void>((r) => (this.resolve = r))\n  }\n\n  push(item: T) {\n    this.buffer.push(item)\n    this.resolve()\n  }\n\n  pushMany(items: T[]) {\n    items.forEach((i) => this.buffer.push(i))\n    this.resolve()\n  }\n\n  async *events(): AsyncGenerator<T> {\n    while (true) {\n      if (this.closed && this.buffer.length === 0) {\n        if (this.toThrow) {\n          throw this.toThrow\n        } else {\n          return\n        }\n      }\n      await this.promise\n      if (this.toThrow) {\n        throw this.toThrow\n      }\n      if (this.maxSize && this.size > this.maxSize) {\n        throw new AsyncBufferFullError(this.maxSize)\n      }\n      const [first, ...rest] = this.buffer\n      if (first) {\n        this.buffer = rest\n        yield first\n      } else {\n        this.resetPromise()\n      }\n    }\n  }\n\n  throw(err: unknown) {\n    this.toThrow = err\n    this.closed = true\n    this.resolve()\n  }\n\n  close() {\n    this.closed = true\n    this.resolve()\n  }\n}\n\nexport class AsyncBufferFullError extends Error {\n  constructor(maxSize: number) {\n    super(`ReachedMaxBufferSize: ${maxSize}`)\n  }\n}\n\n/**\n * Utility function that behaves like {@link Promise.allSettled} but returns the\n * same result as {@link Promise.all} in case every promise is fulfilled, and\n * throws an {@link AggregateError} if there are more than one errors.\n */\nexport function allFulfilled<T extends readonly unknown[] | []>(\n  promises: T,\n): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>\nexport function allFulfilled<T>(\n  promises: Iterable<T | PromiseLike<T>>,\n): Promise<Awaited<T>[]>\nexport function allFulfilled(\n  promises: Iterable<Promise<unknown>>,\n): Promise<unknown[]> {\n  return Promise.allSettled(promises).then(handleAllSettledErrors)\n}\n\nexport function handleAllSettledErrors<\n  T extends readonly PromiseSettledResult<unknown>[] | [],\n>(\n  results: T,\n): {\n  -readonly [P in keyof T]: T[P] extends PromiseSettledResult<infer U>\n    ? U\n    : never\n}\nexport function handleAllSettledErrors<T>(\n  results: PromiseSettledResult<T>[],\n): T[]\nexport function handleAllSettledErrors(\n  results: PromiseSettledResult<unknown>[],\n): unknown[] {\n  if (results.every(isFulfilledResult)) return results.map(extractValue)\n\n  const errors = results.filter(isRejectedResult).map(extractReason)\n  throw aggregateErrors(errors)\n}\n\nexport function isRejectedResult(\n  result: PromiseSettledResult<unknown>,\n): result is PromiseRejectedResult {\n  return result.status === 'rejected'\n}\n\nfunction extractReason(result: PromiseRejectedResult): unknown {\n  return result.reason\n}\n\nexport function isFulfilledResult<T>(\n  result: PromiseSettledResult<T>,\n): result is PromiseFulfilledResult<T> {\n  return result.status === 'fulfilled'\n}\n\nfunction extractValue<T>(result: PromiseFulfilledResult<T>): T {\n  return result.value\n}\n"
  },
  {
    "path": "packages/common-web/src/check.ts",
    "content": "// Explicitly not using \"zod\" types here to avoid mismatching types due to\n// version differences.\n\nexport interface Checkable<T> {\n  parse: (obj: unknown) => T\n  safeParse: (\n    obj: unknown,\n  ) => { success: true; data: T } | { success: false; error: Error }\n}\n\nexport interface Def<T> {\n  name: string\n  schema: Checkable<T>\n}\n\nexport const is = <T>(obj: unknown, def: Checkable<T>): obj is T => {\n  return def.safeParse(obj).success\n}\n\nexport const create =\n  <T>(def: Checkable<T>) =>\n  (v: unknown): v is T =>\n    def.safeParse(v).success\n\nexport const assure = <T>(def: Checkable<T>, obj: unknown): T => {\n  return def.parse(obj)\n}\n\nexport const isObject = (obj: unknown): obj is Record<string, unknown> => {\n  return typeof obj === 'object' && obj !== null\n}\n"
  },
  {
    "path": "packages/common-web/src/did-doc.ts",
    "content": "import { z } from 'zod'\n\n// Parsing atproto data\n// --------\n\nexport const isValidDidDoc = (doc: unknown): doc is DidDocument => {\n  return didDocument.safeParse(doc).success\n}\n\nexport const getDid = (doc: DidDocument): string => {\n  const id = doc.id\n  if (typeof id !== 'string') {\n    throw new Error('No `id` on document')\n  }\n  return id\n}\n\nexport const getHandle = (doc: DidDocument): string | undefined => {\n  const aka = doc.alsoKnownAs\n  if (aka) {\n    for (let i = 0; i < aka.length; i++) {\n      const alias = aka[i]\n      if (alias.startsWith('at://')) {\n        // strip off \"at://\" prefix\n        return alias.slice(5)\n      }\n    }\n  }\n  return undefined\n}\n\n// @NOTE we parse to type/publicKeyMultibase to avoid the dependency on @atproto/crypto\nexport const getSigningKey = (\n  doc: DidDocument,\n): { type: string; publicKeyMultibase: string } | undefined => {\n  return getVerificationMaterial(doc, 'atproto')\n}\n\nexport const getVerificationMaterial = (\n  doc: DidDocument,\n  keyId: string,\n): { type: string; publicKeyMultibase: string } | undefined => {\n  // /!\\ Hot path\n\n  const key = findItemById(doc, 'verificationMethod', `#${keyId}`)\n  if (!key) {\n    return undefined\n  }\n\n  if (!key.publicKeyMultibase) {\n    return undefined\n  }\n\n  return {\n    type: key.type,\n    publicKeyMultibase: key.publicKeyMultibase,\n  }\n}\n\nexport const getSigningDidKey = (doc: DidDocument): string | undefined => {\n  const parsed = getSigningKey(doc)\n  if (!parsed) return\n  return `did:key:${parsed.publicKeyMultibase}`\n}\n\nexport const getPdsEndpoint = (doc: DidDocument): string | undefined => {\n  return getServiceEndpoint(doc, {\n    id: '#atproto_pds',\n    type: 'AtprotoPersonalDataServer',\n  })\n}\n\nexport const getFeedGenEndpoint = (doc: DidDocument): string | undefined => {\n  return getServiceEndpoint(doc, {\n    id: '#bsky_fg',\n    type: 'BskyFeedGenerator',\n  })\n}\n\nexport const getNotifEndpoint = (doc: DidDocument): string | undefined => {\n  return getServiceEndpoint(doc, {\n    id: '#bsky_notif',\n    type: 'BskyNotificationService',\n  })\n}\n\nexport const getServiceEndpoint = (\n  doc: DidDocument,\n  opts: { id: string; type?: string },\n) => {\n  // /!\\ Hot path\n\n  const service = findItemById(doc, 'service', opts.id)\n  if (!service) {\n    return undefined\n  }\n\n  if (opts.type && service.type !== opts.type) {\n    return undefined\n  }\n\n  if (typeof service.serviceEndpoint !== 'string') {\n    return undefined\n  }\n\n  return validateUrl(service.serviceEndpoint)\n}\n\nfunction findItemById<\n  D extends DidDocument,\n  T extends 'verificationMethod' | 'service',\n>(doc: D, type: T, id: string): NonNullable<D[T]>[number] | undefined\nfunction findItemById(\n  doc: DidDocument,\n  type: 'verificationMethod' | 'service',\n  id: string,\n) {\n  // /!\\ Hot path\n\n  const items = doc[type]\n  if (items) {\n    for (let i = 0; i < items.length; i++) {\n      const item = items[i]\n      const itemId = item.id\n\n      if (\n        itemId[0] === '#'\n          ? itemId === id\n          : // Optimized version of: itemId === `${doc.id}${id}`\n            itemId.length === doc.id.length + id.length &&\n            itemId[doc.id.length] === '#' &&\n            itemId.endsWith(id) &&\n            itemId.startsWith(doc.id) // <== We could probably skip this check\n      ) {\n        return item\n      }\n    }\n  }\n  return undefined\n}\n\n// Check protocol and hostname to prevent potential SSRF\nconst validateUrl = (urlStr: string): string | undefined => {\n  if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://')) {\n    return undefined\n  }\n\n  if (!canParseUrl(urlStr)) {\n    return undefined\n  }\n\n  return urlStr\n}\n\nconst canParseUrl =\n  URL.canParse ??\n  // URL.canParse is not available in Node.js < 18.17.0\n  ((urlStr: string): boolean => {\n    try {\n      new URL(urlStr)\n      return true\n    } catch {\n      return false\n    }\n  })\n\n// Types\n// --------\n\nconst verificationMethod = z.object({\n  id: z.string(),\n  type: z.string(),\n  controller: z.string(),\n  publicKeyJwk: z.record(z.string(), z.unknown()).optional(),\n  publicKeyMultibase: z.string().optional(),\n})\n\nconst service = z.object({\n  id: z.string(),\n  type: z.string(),\n  serviceEndpoint: z.union([z.string(), z.record(z.unknown())]),\n})\n\n/**\n * @deprecated Use `DidDocument` from `@atproto/did` instead as it applies\n * stricter (and more spec-compliant) validation.\n */\nexport const didDocument = z.object({\n  '@context': z\n    .union([\n      z.literal('https://www.w3.org/ns/did/v1'),\n      z.array(z.string().url()),\n    ])\n    .optional(),\n  id: z.string(),\n  alsoKnownAs: z.array(z.string()).optional(),\n  verificationMethod: z.array(verificationMethod).optional(),\n  authentication: z.array(z.union([z.string(), verificationMethod])).optional(),\n  service: z.array(service).optional(),\n})\n\nexport type DidDocument = z.infer<typeof didDocument>\n"
  },
  {
    "path": "packages/common-web/src/index.ts",
    "content": "export * as check from './check'\nexport * as util from './util'\n\nexport * from './arrays'\nexport * from './async'\nexport * from './util'\nexport * from './tid'\nexport * from './ipld'\nexport * from './retry'\nexport * from './types'\nexport * from './times'\nexport * from './strings'\nexport * from './did-doc'\n"
  },
  {
    "path": "packages/common-web/src/ipld.ts",
    "content": "import { LexValue, lexEquals } from '@atproto/lex-data'\nimport { JsonValue, jsonToLex, lexToJson } from '@atproto/lex-json'\n\n/**\n * @deprecated Use {@link JsonValue} from `@atproto/lex-cbor` instead.\n */\nexport type LegacyJsonValue = unknown\n\nexport type { LegacyJsonValue as JsonValue }\n\n/**\n * @deprecated Use {@link LexValue} from `@atproto/lex-cbor` instead.\n */\nexport type IpldValue = unknown\n\n/**\n * Converts a JSON-compatible value to an IPLD-compatible value.\n * @deprecated Use {@link jsonToLex} from `@atproto/lex-cbor` instead.\n */\nexport const jsonToIpld = (val: LegacyJsonValue): IpldValue => {\n  return jsonToLex(val as JsonValue, { strict: false })\n}\n\n/**\n * Converts an IPLD-compatible value to a JSON-compatible value.\n * @deprecated Use {@link lexToJson} from `@atproto/lex-cbor` instead.\n */\nexport const ipldToJson = (val: IpldValue): LegacyJsonValue => {\n  // Legacy behavior(s)\n  if (val === undefined) return val\n  if (Number.isNaN(val)) return val\n\n  return lexToJson(val as LexValue)\n}\n\n/**\n * Compares two IPLD-compatible values for deep equality.\n * @deprecated Use {@link lexEquals} from `@atproto/lex-cbor` instead.\n */\nexport const ipldEquals = (a: IpldValue, b: IpldValue): boolean => {\n  if (!lexEquals(a as LexValue, b as LexValue)) return false\n\n  // @NOTE The previous implementation used \"===\" which treats NaN as unequal to\n  // NaN.\n  if (Number.isNaN(a)) return false\n\n  return true\n}\n"
  },
  {
    "path": "packages/common-web/src/retry.ts",
    "content": "import { wait } from './util'\n\nexport type RetryOptions = {\n  maxRetries?: number\n  getWaitMs?: (n: number) => number | null\n}\n\nexport async function retry<T>(\n  fn: () => Promise<T>,\n  opts: RetryOptions & {\n    retryable?: (err: unknown) => boolean\n  } = {},\n): Promise<T> {\n  const { maxRetries = 3, retryable = () => true, getWaitMs = backoffMs } = opts\n  let retries = 0\n  let doneError: unknown\n  while (!doneError) {\n    try {\n      return await fn()\n    } catch (err) {\n      const waitMs = getWaitMs(retries)\n      const willRetry =\n        retries < maxRetries && waitMs !== null && retryable(err)\n      if (willRetry) {\n        retries += 1\n        if (waitMs !== 0) {\n          await wait(waitMs)\n        }\n      } else {\n        doneError = err\n      }\n    }\n  }\n  throw doneError\n}\n\nexport function createRetryable(retryable: (err: unknown) => boolean) {\n  return async <T>(fn: () => Promise<T>, opts?: RetryOptions) =>\n    retry(fn, { ...opts, retryable })\n}\n\n// Waits exponential backoff with max and jitter: ~100, ~200, ~400, ~800, ~1000, ~1000, ...\nexport function backoffMs(n: number, multiplier = 100, max = 1000) {\n  const exponentialMs = Math.pow(2, n) * multiplier\n  const ms = Math.min(exponentialMs, max)\n  return jitter(ms)\n}\n\n// Adds randomness +/-15% of value\nfunction jitter(value: number) {\n  const delta = value * 0.15\n  return value + randomRange(-delta, delta)\n}\n\nfunction randomRange(from: number, to: number) {\n  const rand = Math.random() * (to - from)\n  return rand + from\n}\n"
  },
  {
    "path": "packages/common-web/src/strings.ts",
    "content": "import { fromBase64, graphemeLen, toBase64, utf8Len } from '@atproto/lex-data'\nimport {\n  LanguageTag,\n  isValidLanguage,\n  parseLanguageString,\n} from '@atproto/syntax'\n\n/**\n * @deprecated Use {@link graphemeLen} from `@atproto/lex-data` instead.\n */\nconst graphemeLenLegacy = graphemeLen\nexport { graphemeLenLegacy as graphemeLen }\n\n/**\n * @deprecated Use {@link utf8Len} from `@atproto/lex-data` instead.\n */\nconst utf8LenLegacy = utf8Len\nexport { utf8LenLegacy as utf8Len }\n\n/**\n * @deprecated Use {@link LanguageTag} from `@atproto/lex-data` instead.\n */\ntype LanguageTagLegacy = LanguageTag\nexport type { LanguageTagLegacy as LanguageTag }\n\n/**\n * @deprecated Use {@link parseLanguageString} from `@atproto/syntax` instead.\n */\nconst parseLanguageLegacy = parseLanguageString\nexport { parseLanguageLegacy as parseLanguage }\n\n/**\n * @deprecated Use {@link isLanguageString} from `@atproto/syntax` instead.\n */\nexport const validateLanguage = isValidLanguage\n\n/**\n * @deprecated Use {@link toBase64} from `@atproto/lex-data` instead.\n */\nexport const utf8ToB64Url = (utf8: string): string => {\n  return toBase64(new TextEncoder().encode(utf8), 'base64url')\n}\n\n/**\n * @deprecated Use {@link fromBase64} from `@atproto/lex-data` instead.\n */\nexport const b64UrlToUtf8 = (b64: string): string => {\n  return new TextDecoder().decode(fromBase64(b64, 'base64url'))\n}\n"
  },
  {
    "path": "packages/common-web/src/tid.ts",
    "content": "import { s32decode, s32encode } from './util'\n\nconst TID_LEN = 13\n\nlet lastTimestamp = 0\nlet timestampCount = 0\nlet clockid: number | null = null\n\nfunction dedash(str: string): string {\n  return str.replaceAll('-', '')\n}\n\nexport class TID {\n  str: string\n\n  constructor(str: string) {\n    const noDashes = dedash(str)\n    if (noDashes.length !== TID_LEN) {\n      throw new Error(`Poorly formatted TID: ${noDashes.length} length`)\n    }\n    this.str = noDashes\n  }\n\n  static next(prev?: TID): TID {\n    // javascript does not have microsecond precision\n    // instead, we append a counter to the timestamp to indicate if multiple timestamps were created within the same millisecond\n    // take max of current time & last timestamp to prevent tids moving backwards if system clock drifts backwards\n    const time = Math.max(Date.now(), lastTimestamp)\n    if (time === lastTimestamp) {\n      timestampCount++\n    }\n    lastTimestamp = time\n    const timestamp = time * 1000 + timestampCount\n    // the bottom 32 clock ids can be randomized & are not guaranteed to be collision resistant\n    // we use the same clockid for all tids coming from this machine\n    if (clockid === null) {\n      clockid = Math.floor(Math.random() * 32)\n    }\n    const tid = TID.fromTime(timestamp, clockid)\n    if (!prev || tid.newerThan(prev)) {\n      return tid\n    }\n    return TID.fromTime(prev.timestamp() + 1, clockid)\n  }\n\n  static nextStr(prev?: string): string {\n    return TID.next(prev ? new TID(prev) : undefined).toString()\n  }\n\n  static fromTime(timestamp: number, clockid: number): TID {\n    // base32 encode with encoding variant sort (s32)\n    const str = `${s32encode(timestamp)}${s32encode(clockid).padStart(2, '2')}`\n    return new TID(str)\n  }\n\n  static fromStr(str: string): TID {\n    return new TID(str)\n  }\n\n  static oldestFirst(a: TID, b: TID): number {\n    return a.compareTo(b)\n  }\n\n  static newestFirst(a: TID, b: TID): number {\n    return b.compareTo(a)\n  }\n\n  static is(str: string): boolean {\n    return dedash(str).length === TID_LEN\n  }\n\n  timestamp(): number {\n    return s32decode(this.str.slice(0, 11))\n  }\n\n  clockid(): number {\n    return s32decode(this.str.slice(11, 13))\n  }\n\n  formatted(): string {\n    const str = this.toString()\n    return `${str.slice(0, 4)}-${str.slice(4, 7)}-${str.slice(\n      7,\n      11,\n    )}-${str.slice(11, 13)}`\n  }\n\n  toString(): string {\n    return this.str\n  }\n\n  // newer > older\n  compareTo(other: TID): number {\n    if (this.str > other.str) return 1\n    if (this.str < other.str) return -1\n    return 0\n  }\n\n  equals(other: TID): boolean {\n    return this.str === other.str\n  }\n\n  newerThan(other: TID): boolean {\n    return this.compareTo(other) > 0\n  }\n\n  olderThan(other: TID): boolean {\n    return this.compareTo(other) < 0\n  }\n}\n"
  },
  {
    "path": "packages/common-web/src/times.ts",
    "content": "export const SECOND = 1000\nexport const MINUTE = SECOND * 60\nexport const HOUR = MINUTE * 60\nexport const DAY = HOUR * 24\n\nexport const lessThanAgoMs = (time: Date, range: number) => {\n  return Date.now() < time.getTime() + range\n}\n\nexport const addHoursToDate = (hours: number, startingDate?: Date): Date => {\n  // When date is passed, clone before calling `setHours()` so that we are not mutating the original date\n  const currentDate = startingDate ? new Date(startingDate) : new Date()\n  currentDate.setHours(currentDate.getHours() + hours)\n  return currentDate\n}\n"
  },
  {
    "path": "packages/common-web/src/types.ts",
    "content": "import { z } from 'zod'\nimport { CID } from '@atproto/lex-data'\nimport { Def } from './check'\n\nconst cidSchema = z.unknown().transform((obj, ctx): CID => {\n  const cid = CID.asCID(obj)\n\n  if (cid == null) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      message: 'Not a valid CID',\n    })\n    return z.NEVER\n  }\n\n  return cid\n})\n\nconst carHeader = z.object({\n  version: z.literal(1),\n  roots: z.array(cidSchema),\n})\nexport type CarHeader = z.infer<typeof carHeader>\n\nexport const schema = {\n  cid: cidSchema,\n  carHeader,\n  bytes: z.instanceof(Uint8Array),\n  string: z.string(),\n  array: z.array(z.unknown()),\n  map: z.record(z.string(), z.unknown()),\n  unknown: z.unknown(),\n}\n\nexport const def = {\n  cid: {\n    name: 'cid',\n    schema: schema.cid,\n  } as Def<CID>,\n  carHeader: {\n    name: 'CAR header',\n    schema: schema.carHeader,\n  } as Def<CarHeader>,\n  bytes: {\n    name: 'bytes',\n    schema: schema.bytes,\n  } as Def<Uint8Array>,\n  string: {\n    name: 'string',\n    schema: schema.string,\n  } as Def<string>,\n  map: {\n    name: 'map',\n    schema: schema.map,\n  } as Def<Record<string, unknown>>,\n  unknown: {\n    name: 'unknown',\n    schema: schema.unknown,\n  } as Def<unknown>,\n}\n\nexport type ArrayEl<A> = A extends readonly (infer T)[] ? T : never\n\nexport type NotEmptyArray<T> = [T, ...T[]]\n"
  },
  {
    "path": "packages/common-web/src/util.ts",
    "content": "export const noUndefinedVals = <T>(\n  obj: Record<string, T | undefined>,\n): Record<string, T> => {\n  for (const k of Object.keys(obj)) {\n    if (obj[k] === undefined) {\n      delete obj[k]\n    }\n  }\n  return obj as Record<string, T>\n}\n\nexport function aggregateErrors(\n  errors: unknown[],\n  message?: string,\n): Error | AggregateError {\n  if (errors.length === 1) {\n    return errors[0] instanceof Error\n      ? errors[0]\n      : new Error(message ?? stringifyError(errors[0]), { cause: errors[0] })\n  } else {\n    return new AggregateError(\n      errors,\n      message ?? `Multiple errors: ${errors.map(stringifyError).join('\\n')}`,\n    )\n  }\n}\n\nfunction stringifyError(reason: unknown): string {\n  if (reason instanceof Error) {\n    return reason.message\n  }\n  return String(reason)\n}\n\n/**\n * Returns a shallow copy of the object without the specified keys. If the input\n * is nullish, it returns the input.\n */\nexport function omit<\n  T extends undefined | null | Record<string, unknown>,\n  K extends keyof NonNullable<T>,\n>(\n  object: T,\n  rejectedKeys: readonly K[],\n): T extends undefined ? undefined : T extends null ? null : Omit<T, K>\nexport function omit(\n  src: undefined | null | Record<string, unknown>,\n  rejectedKeys: readonly string[],\n): undefined | null | Record<string, unknown> {\n  // Hot path\n\n  if (!src) return src\n\n  const dst = {}\n  const srcKeys = Object.keys(src)\n  for (let i = 0; i < srcKeys.length; i++) {\n    const key = srcKeys[i]\n    if (!rejectedKeys.includes(key)) {\n      dst[key] = src[key]\n    }\n  }\n  return dst\n}\n\nexport const jitter = (maxMs: number) => {\n  return Math.round((Math.random() - 0.5) * maxMs * 2)\n}\n\nexport const wait = (ms: number) => {\n  return new Promise((res) => setTimeout(res, ms))\n}\n\nexport type BailableWait = {\n  bail: () => void\n  wait: () => Promise<void>\n}\n\nexport const bailableWait = (ms: number): BailableWait => {\n  let bail\n  const waitPromise = new Promise<void>((res) => {\n    const timeout = setTimeout(res, ms)\n    bail = () => {\n      clearTimeout(timeout)\n      res()\n    }\n  })\n  return { bail, wait: () => waitPromise }\n}\n\nexport const flattenUint8Arrays = (arrs: Uint8Array[]): Uint8Array => {\n  const length = arrs.reduce((acc, cur) => {\n    return acc + cur.length\n  }, 0)\n  const flattened = new Uint8Array(length)\n  let offset = 0\n  arrs.forEach((arr) => {\n    flattened.set(arr, offset)\n    offset += arr.length\n  })\n  return flattened\n}\n\nexport const streamToBuffer = async (\n  stream: AsyncIterable<Uint8Array>,\n): Promise<Uint8Array> => {\n  const arrays: Uint8Array[] = []\n  for await (const chunk of stream) {\n    arrays.push(chunk)\n  }\n  return flattenUint8Arrays(arrays)\n}\n\nconst S32_CHAR = '234567abcdefghijklmnopqrstuvwxyz'\n\nexport const s32encode = (i: number): string => {\n  let s = ''\n  while (i) {\n    const c = i % 32\n    i = Math.floor(i / 32)\n    s = S32_CHAR.charAt(c) + s\n  }\n  return s\n}\n\nexport const s32decode = (s: string): number => {\n  let i = 0\n  for (const c of s) {\n    i = i * 32 + S32_CHAR.indexOf(c)\n  }\n  return i\n}\n\nexport const asyncFilter = async <T>(\n  arr: T[],\n  fn: (t: T) => Promise<boolean>,\n) => {\n  const results = await Promise.all(arr.map((t) => fn(t)))\n  return arr.filter((_, i) => results[i])\n}\n\nexport const isErrnoException = (\n  err: unknown,\n): err is NodeJS.ErrnoException => {\n  return !!err && err['code']\n}\n\nexport const errHasMsg = (err: unknown, msg: string): boolean => {\n  return !!err && typeof err === 'object' && err['message'] === msg\n}\n\nexport const chunkArray = <T>(arr: T[], chunkSize: number): T[][] => {\n  return arr.reduce((acc, cur, i) => {\n    const chunkI = Math.floor(i / chunkSize)\n    if (!acc[chunkI]) {\n      acc[chunkI] = []\n    }\n    acc[chunkI].push(cur)\n    return acc\n  }, [] as T[][])\n}\n\nexport const range = (num: number): number[] => {\n  const nums: number[] = []\n  for (let i = 0; i < num; i++) {\n    nums.push(i)\n  }\n  return nums\n}\n\nexport const dedupeStrs = <T extends string>(strs: Iterable<T>): T[] => {\n  return [...new Set(strs)]\n}\n\nexport const parseIntWithFallback = <T>(\n  value: string | undefined,\n  fallback: T,\n): number | T => {\n  const parsed = parseInt(value || '', 10)\n  return isNaN(parsed) ? fallback : parsed\n}\n"
  },
  {
    "path": "packages/common-web/tests/check.test.ts",
    "content": "import { ZodError } from 'zod'\nimport { check } from '../src/index'\n\ndescribe('check', () => {\n  describe('is', () => {\n    it('checks object against definition', () => {\n      const checkable: check.Checkable<boolean> = {\n        parse(obj) {\n          return Boolean(obj)\n        },\n        safeParse(obj) {\n          return {\n            success: true,\n            data: Boolean(obj),\n          }\n        },\n      }\n\n      expect(check.is(true, checkable)).toBe(true)\n    })\n\n    it('handles failed checks', () => {\n      const checkable: check.Checkable<boolean> = {\n        parse(obj) {\n          return Boolean(obj)\n        },\n        safeParse() {\n          return {\n            success: false,\n            error: new ZodError([]),\n          }\n        },\n      }\n\n      expect(check.is(true, checkable)).toBe(false)\n    })\n  })\n\n  describe('assure', () => {\n    it('returns value on success', () => {\n      const checkable: check.Checkable<boolean> = {\n        parse(obj) {\n          return Boolean(obj)\n        },\n        safeParse(obj) {\n          return {\n            success: true,\n            data: Boolean(obj),\n          }\n        },\n      }\n\n      expect(check.assure(checkable, true)).toEqual(true)\n    })\n\n    it('throws on failure', () => {\n      const err = new Error('foo')\n      const checkable: check.Checkable<boolean> = {\n        parse() {\n          throw err\n        },\n        safeParse() {\n          throw err\n        },\n      }\n\n      expect(() => check.assure(checkable, true)).toThrow(err)\n    })\n  })\n\n  describe('isObject', () => {\n    const falseTestValues: unknown[] = [null, undefined, 'foo', 123, true]\n\n    for (const obj of falseTestValues) {\n      it(`returns false for ${obj}`, () => {\n        expect(check.isObject(obj)).toBe(false)\n      })\n    }\n\n    it('returns true for objects', () => {\n      expect(check.isObject({})).toBe(true)\n    })\n\n    it('returns true for instances of classes', () => {\n      const obj = new (class {})()\n      expect(check.isObject(obj)).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/common-web/tests/retry.test.ts",
    "content": "import { retry } from '../src/index'\n\ndescribe('retry', () => {\n  describe('retry()', () => {\n    it('retries until max retries', async () => {\n      let fnCalls = 0\n      let waitMsCalls = 0\n      const fn = async () => {\n        fnCalls++\n        throw new Error(`Oops ${fnCalls}!`)\n      }\n      const getWaitMs = (retries) => {\n        waitMsCalls++\n        expect(retries).toEqual(waitMsCalls - 1)\n        return 0\n      }\n      await expect(retry(fn, { maxRetries: 13, getWaitMs })).rejects.toThrow(\n        'Oops 14!',\n      )\n      expect(fnCalls).toEqual(14)\n      expect(waitMsCalls).toEqual(14)\n    })\n\n    it('retries until max wait', async () => {\n      let fnCalls = 0\n      let waitMsCalls = 0\n      const fn = async () => {\n        fnCalls++\n        throw new Error(`Oops ${fnCalls}!`)\n      }\n      const getWaitMs = (retries) => {\n        waitMsCalls++\n        expect(retries).toEqual(waitMsCalls - 1)\n        if (retries === 13) {\n          return null\n        }\n        return 0\n      }\n      await expect(\n        retry(fn, { maxRetries: Infinity, getWaitMs }),\n      ).rejects.toThrow('Oops 14!')\n      expect(fnCalls).toEqual(14)\n      expect(waitMsCalls).toEqual(14)\n    })\n\n    it('retries until non-retryable error', async () => {\n      let fnCalls = 0\n      let waitMsCalls = 0\n      const fn = async () => {\n        fnCalls++\n        throw new Error(`Oops ${fnCalls}!`)\n      }\n      const getWaitMs = (retries) => {\n        waitMsCalls++\n        expect(retries).toEqual(waitMsCalls - 1)\n        return 0\n      }\n      const retryable = (err: unknown) => err?.['message'] !== 'Oops 14!'\n      await expect(\n        retry(fn, { maxRetries: Infinity, getWaitMs, retryable }),\n      ).rejects.toThrow('Oops 14!')\n      expect(fnCalls).toEqual(14)\n      expect(waitMsCalls).toEqual(14)\n    })\n\n    it('returns latest result after retries', async () => {\n      let fnCalls = 0\n      const fn = async () => {\n        fnCalls++\n        if (fnCalls < 14) {\n          throw new Error(`Oops ${fnCalls}!`)\n        }\n        return 'ok'\n      }\n      const getWaitMs = () => 0\n      const result = await retry(fn, { maxRetries: Infinity, getWaitMs })\n      expect(result).toBe('ok')\n      expect(fnCalls).toBe(14)\n    })\n\n    it('returns result immediately on success', async () => {\n      let fnCalls = 0\n      const fn = async () => {\n        fnCalls++\n        return 'ok'\n      }\n      const getWaitMs = () => 0\n      const result = await retry(fn, { maxRetries: Infinity, getWaitMs })\n      expect(result).toBe('ok')\n      expect(fnCalls).toBe(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/common-web/tests/tid.test.ts",
    "content": "import { TID } from '../src/tid'\n\ndescribe('TIDs', () => {\n  it('creates a new TID', () => {\n    const tid = TID.next()\n    const str = tid.toString()\n    expect(typeof str).toEqual('string')\n    expect(str.length).toEqual(13)\n  })\n\n  it('parses a TID', () => {\n    const tid = TID.next()\n    const str = tid.toString()\n    const parsed = TID.fromStr(str)\n    expect(parsed.timestamp()).toEqual(tid.timestamp())\n    expect(parsed.clockid()).toEqual(tid.clockid())\n  })\n\n  it('throws if invalid tid passed', () => {\n    expect(() => new TID('')).toThrow('Poorly formatted TID: 0 length')\n  })\n\n  describe('nextStr', () => {\n    it('returns next tid as a string', () => {\n      const str = TID.nextStr()\n      expect(typeof str).toEqual('string')\n      expect(str.length).toEqual(13)\n    })\n\n    it('returns a next tid larger than a provided prev', () => {\n      const prev = TID.fromTime((Date.now() + 5000) * 1000, 0).toString()\n      const str = TID.nextStr(prev)\n      expect(str > prev).toBe(true)\n    })\n  })\n\n  describe('newestFirst', () => {\n    it('sorts tids newest first', () => {\n      const oldest = TID.next()\n      const newest = TID.next()\n\n      const tids = [oldest, newest]\n\n      tids.sort(TID.newestFirst)\n\n      expect(tids).toEqual([newest, oldest])\n    })\n  })\n\n  describe('oldestFirst', () => {\n    it('sorts tids oldest first', () => {\n      const oldest = TID.next()\n      const newest = TID.next()\n\n      const tids = [newest, oldest]\n\n      tids.sort(TID.oldestFirst)\n\n      expect(tids).toEqual([oldest, newest])\n    })\n  })\n\n  describe('is', () => {\n    it('true for valid tids', () => {\n      const tid = TID.next()\n      const asStr = tid.toString()\n\n      expect(TID.is(asStr)).toBe(true)\n    })\n\n    it('false for invalid tids', () => {\n      expect(TID.is('')).toBe(false)\n    })\n  })\n\n  describe('equals', () => {\n    it('true when same tid', () => {\n      const tid = TID.next()\n      expect(tid.equals(tid)).toBe(true)\n    })\n\n    it('true when different instance, same tid', () => {\n      const tid0 = TID.next()\n      const tid1 = new TID(tid0.toString())\n\n      expect(tid0.equals(tid1)).toBe(true)\n    })\n\n    it('false when different tid', () => {\n      const tid0 = TID.next()\n      const tid1 = TID.next()\n\n      expect(tid0.equals(tid1)).toBe(false)\n    })\n  })\n\n  describe('newerThan', () => {\n    it('true for newer tid', () => {\n      const tid0 = TID.next()\n      const tid1 = TID.next()\n\n      expect(tid1.newerThan(tid0)).toBe(true)\n    })\n\n    it('false for older tid', () => {\n      const tid0 = TID.next()\n      const tid1 = TID.next()\n\n      expect(tid0.newerThan(tid1)).toBe(false)\n    })\n\n    it('false for identical tids', () => {\n      const tid0 = TID.next()\n      const tid1 = new TID(tid0.toString())\n\n      expect(tid0.newerThan(tid1)).toBe(false)\n    })\n  })\n\n  describe('olderThan', () => {\n    it('true for older tid', () => {\n      const tid0 = TID.next()\n      const tid1 = TID.next()\n\n      expect(tid0.olderThan(tid1)).toBe(true)\n    })\n\n    it('false for newer tid', () => {\n      const tid0 = TID.next()\n      const tid1 = TID.next()\n\n      expect(tid1.olderThan(tid0)).toBe(false)\n    })\n\n    it('false for identical tids', () => {\n      const tid0 = TID.next()\n      const tid1 = new TID(tid0.toString())\n\n      expect(tid0.olderThan(tid1)).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/common-web/tests/util.test.ts",
    "content": "import { util } from '../src/index'\n\ndescribe('util', () => {\n  describe('noUndefinedVals', () => {\n    it('removes undefined top-level keys', () => {\n      const obj: Record<string, unknown> = {\n        foo: 123,\n        bar: undefined,\n      }\n\n      const result = util.noUndefinedVals(obj)\n\n      expect(result).toBe(obj)\n      expect(result).toEqual({\n        foo: 123,\n      })\n    })\n\n    it('handles empty objects', () => {\n      expect(util.noUndefinedVals({})).toEqual({})\n    })\n\n    it('leaves deep values intact', () => {\n      const obj: Record<string, unknown> = {\n        foo: 123,\n        bar: {\n          baz: undefined,\n        },\n      }\n      const result = util.noUndefinedVals(obj)\n\n      expect(result).toEqual({\n        foo: 123,\n        bar: {\n          baz: undefined,\n        },\n      })\n    })\n  })\n\n  describe('flattenUint8Arrays', () => {\n    it('flattens to single array of values', () => {\n      const arr = [new Uint8Array([0xa, 0xb]), new Uint8Array([0xc, 0xd])]\n\n      const flat = util.flattenUint8Arrays(arr)\n\n      expect([...flat]).toEqual([0xa, 0xb, 0xc, 0xd])\n    })\n\n    it('flattens empty arrays', () => {\n      const arr = [new Uint8Array(0), new Uint8Array(0)]\n      const flat = util.flattenUint8Arrays(arr)\n\n      expect(flat.length).toBe(0)\n    })\n  })\n\n  describe('streamToBuffer', () => {\n    it('reads iterable into array', async () => {\n      const iterable: AsyncIterable<Uint8Array> = {\n        async *[Symbol.asyncIterator]() {\n          yield new Uint8Array([0xa, 0xb])\n          yield new Uint8Array([0xc, 0xd])\n        },\n      }\n      const buffer = await util.streamToBuffer(iterable)\n\n      expect([...buffer]).toEqual([0xa, 0xb, 0xc, 0xd])\n    })\n  })\n\n  describe('asyncFilter', () => {\n    it('filters array values', async () => {\n      const result = await util.asyncFilter([0, 1, 2], (n) =>\n        Promise.resolve(n === 0),\n      )\n\n      expect(result).toEqual([0])\n    })\n  })\n\n  describe('range', () => {\n    it('generates numeric range', () => {\n      expect(util.range(4)).toEqual([0, 1, 2, 3])\n    })\n  })\n\n  describe('dedupeStrs', () => {\n    it('removes duplicates', () => {\n      expect(util.dedupeStrs(['a', 'a', 'b'])).toEqual(['a', 'b'])\n    })\n  })\n\n  describe('parseIntWithFallback', () => {\n    it('accepts undefined', () => {\n      expect(util.parseIntWithFallback(undefined, -10)).toBe(-10)\n    })\n\n    it('parses numbers', () => {\n      expect(util.parseIntWithFallback('100', -10)).toBe(100)\n    })\n\n    it('supports non-numeric fallbacks', () => {\n      expect(util.parseIntWithFallback(undefined, 'foo')).toBe('foo')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/common-web/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/common-web/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/common-web/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/crypto/CHANGELOG.md",
    "content": "# @atproto/crypto\n\n## 0.4.5\n\n### Patch Changes\n\n- [#4384](https://github.com/bluesky-social/atproto/pull/4384) [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `formatDidKey` return type to `did:key:${string}`\n\n## 0.4.4\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n## 0.4.3\n\n### Patch Changes\n\n- [#3335](https://github.com/bluesky-social/atproto/pull/3335) [`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277) Thanks [@dholms](https://github.com/dholms)! - Update noble crypto libraries\n\n## 0.4.2\n\n### Patch Changes\n\n- [#2936](https://github.com/bluesky-social/atproto/pull/2936) [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Export utils\n\n## 0.4.1\n\n### Patch Changes\n\n- [#2743](https://github.com/bluesky-social/atproto/pull/2743) [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add \"`jwtAlg`\" option to `verifySignature()` function\n\n## 0.4.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n## 0.3.0\n\n### Minor Changes\n\n- [#1839](https://github.com/bluesky-social/atproto/pull/1839) [`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5) Thanks [@dholms](https://github.com/dholms)! - Prevent signature malleability through DER-encoded signatures\n\n## 0.2.3\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n"
  },
  {
    "path": "packages/crypto/README.md",
    "content": "# @atproto/crypto\n\nTypeScript library providing basic cryptographic helpers as needed in [atproto](https://atproto.com).\n\n[![NPM](https://img.shields.io/npm/v/@atproto/crypto)](https://www.npmjs.com/package/@atproto/crypto)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\nThis package implements the two currently supported cryptographic systems:\n\n- P-256 elliptic curve: aka \"NIST P-256\", aka secp256r1 (note the r), aka prime256v1\n- K-256 elliptic curve: aka \"NIST K-256\", aka secp256k1 (note the k)\n\nThe details of cryptography in atproto are described in [the specification](https://atproto.com/specs/cryptography). This includes string encodings, validity of \"low-S\" signatures, byte representation \"compression\", hashing, and more.\n\n## Usage\n\n```typescript\nimport { verifySignature, Secp256k1Keypair, P256Keypair } from '@atproto/crypto'\n\n// generate a new random K-256 private key\nconst keypair = await Secp256k1Keypair.create({ exportable: true })\n\n// sign binary data, resulting signature bytes.\n// SHA-256 hash of data is what actually gets signed.\n// signature output is often base64-encoded.\nconst data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])\nconst sig = await keypair.sign(data)\n\n// serialize the public key as a did:key string, which includes key type metadata\nconst pubDidKey = keypair.did()\nconsole.log(pubDidKey)\n\n// output would look something like: 'did:key:zQ3shVRtgqTRHC7Lj4DYScoDgReNpsDp3HBnuKBKt1FSXKQ38'\n\n// verify signature using public key\nconst ok = verifySignature(pubDidKey, data, sig)\nif (!ok) {\n  throw new Error('Uh oh, something is fishy')\n} else {\n  console.log('Success')\n}\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/crypto/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Crypto',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/crypto/package.json",
    "content": "{\n  \"name\": \"@atproto/crypto\",\n  \"version\": \"0.4.5\",\n  \"license\": \"MIT\",\n  \"description\": \"Library for cryptographic keys and signing in atproto\",\n  \"keywords\": [\n    \"atproto\",\n    \"cryptography\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/crypto\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest \",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@noble/curves\": \"^1.7.0\",\n    \"@noble/hashes\": \"^1.6.1\",\n    \"uint8arrays\": \"3.0.0\"\n  },\n  \"devDependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/crypto/src/const.ts",
    "content": "export const P256_DID_PREFIX = new Uint8Array([0x80, 0x24])\nexport const SECP256K1_DID_PREFIX = new Uint8Array([0xe7, 0x01])\nexport const BASE58_MULTIBASE_PREFIX = 'z'\nexport const DID_KEY_PREFIX = 'did:key:'\n\nexport const P256_JWT_ALG = 'ES256'\nexport const SECP256K1_JWT_ALG = 'ES256K'\n"
  },
  {
    "path": "packages/crypto/src/did.ts",
    "content": "import * as uint8arrays from 'uint8arrays'\nimport { BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX } from './const'\nimport { plugins } from './plugins'\nimport { extractMultikey, extractPrefixedBytes, hasPrefix } from './utils'\n\nexport type ParsedMultikey = {\n  jwtAlg: string\n  keyBytes: Uint8Array\n}\n\nexport const parseMultikey = (multikey: string): ParsedMultikey => {\n  const prefixedBytes = extractPrefixedBytes(multikey)\n  const plugin = plugins.find((p) => hasPrefix(prefixedBytes, p.prefix))\n  if (!plugin) {\n    throw new Error('Unsupported key type')\n  }\n  const keyBytes = plugin.decompressPubkey(\n    prefixedBytes.slice(plugin.prefix.length),\n  )\n  return {\n    jwtAlg: plugin.jwtAlg,\n    keyBytes,\n  }\n}\n\nexport const formatMultikey = (\n  jwtAlg: string,\n  keyBytes: Uint8Array,\n): string => {\n  const plugin = plugins.find((p) => p.jwtAlg === jwtAlg)\n  if (!plugin) {\n    throw new Error('Unsupported key type')\n  }\n  const prefixedBytes = uint8arrays.concat([\n    plugin.prefix,\n    plugin.compressPubkey(keyBytes),\n  ])\n  return (\n    BASE58_MULTIBASE_PREFIX + uint8arrays.toString(prefixedBytes, 'base58btc')\n  )\n}\n\nexport const parseDidKey = (did: string): ParsedMultikey => {\n  const multikey = extractMultikey(did)\n  return parseMultikey(multikey)\n}\n\nexport function formatDidKey(\n  jwtAlg: string,\n  keyBytes: Uint8Array,\n): `did:key:${string}` {\n  return `${DID_KEY_PREFIX}${formatMultikey(jwtAlg, keyBytes)}` as const\n}\n"
  },
  {
    "path": "packages/crypto/src/index.ts",
    "content": "export * from './const'\nexport * from './did'\nexport * from './multibase'\nexport * from './random'\nexport * from './sha'\nexport * from './types'\nexport * from './verify'\nexport * from './utils'\n\nexport * from './p256/keypair'\nexport * from './p256/plugin'\n\nexport * from './secp256k1/keypair'\nexport * from './secp256k1/plugin'\n"
  },
  {
    "path": "packages/crypto/src/multibase.ts",
    "content": "import * as uint8arrays from 'uint8arrays'\nimport { SupportedEncodings } from 'uint8arrays/to-string'\n\nexport const multibaseToBytes = (mb: string): Uint8Array => {\n  const base = mb[0]\n  const key = mb.slice(1)\n  switch (base) {\n    case 'f':\n      return uint8arrays.fromString(key, 'base16')\n    case 'F':\n      return uint8arrays.fromString(key, 'base16upper')\n    case 'b':\n      return uint8arrays.fromString(key, 'base32')\n    case 'B':\n      return uint8arrays.fromString(key, 'base32upper')\n    case 'z':\n      return uint8arrays.fromString(key, 'base58btc')\n    case 'm':\n      return uint8arrays.fromString(key, 'base64')\n    case 'u':\n      return uint8arrays.fromString(key, 'base64url')\n    case 'U':\n      return uint8arrays.fromString(key, 'base64urlpad')\n    default:\n      throw new Error(`Unsupported multibase: :${mb}`)\n  }\n}\n\nexport const bytesToMultibase = (\n  mb: Uint8Array,\n  encoding: SupportedEncodings,\n): string => {\n  switch (encoding) {\n    case 'base16':\n      return 'f' + uint8arrays.toString(mb, encoding)\n    case 'base16upper':\n      return 'F' + uint8arrays.toString(mb, encoding)\n    case 'base32':\n      return 'b' + uint8arrays.toString(mb, encoding)\n    case 'base32upper':\n      return 'B' + uint8arrays.toString(mb, encoding)\n    case 'base58btc':\n      return 'z' + uint8arrays.toString(mb, encoding)\n    case 'base64':\n      return 'm' + uint8arrays.toString(mb, encoding)\n    case 'base64url':\n      return 'u' + uint8arrays.toString(mb, encoding)\n    case 'base64urlpad':\n      return 'U' + uint8arrays.toString(mb, encoding)\n    default:\n      throw new Error(`Unsupported multibase: :${encoding}`)\n  }\n}\n"
  },
  {
    "path": "packages/crypto/src/p256/encoding.ts",
    "content": "import { p256 } from '@noble/curves/p256'\n\nexport const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {\n  const point = p256.ProjectivePoint.fromHex(pubkeyBytes)\n  return point.toRawBytes(true)\n}\n\nexport const decompressPubkey = (compressed: Uint8Array): Uint8Array => {\n  if (compressed.length !== 33) {\n    throw new Error('Expected 33 byte compress pubkey')\n  }\n  const point = p256.ProjectivePoint.fromHex(compressed)\n  return point.toRawBytes(false)\n}\n"
  },
  {
    "path": "packages/crypto/src/p256/keypair.ts",
    "content": "import { p256 } from '@noble/curves/p256'\nimport { sha256 } from '@noble/hashes/sha256'\nimport {\n  fromString as ui8FromString,\n  toString as ui8ToString,\n} from 'uint8arrays'\nimport { SupportedEncodings } from 'uint8arrays/to-string'\nimport { P256_JWT_ALG } from '../const'\nimport * as did from '../did'\nimport { Keypair } from '../types'\n\nexport type P256KeypairOptions = {\n  exportable: boolean\n}\n\nexport class P256Keypair implements Keypair {\n  jwtAlg = P256_JWT_ALG\n  private publicKey: Uint8Array\n\n  constructor(\n    private privateKey: Uint8Array,\n    private exportable: boolean,\n  ) {\n    this.publicKey = p256.getPublicKey(privateKey)\n  }\n\n  static async create(\n    opts?: Partial<P256KeypairOptions>,\n  ): Promise<P256Keypair> {\n    const { exportable = false } = opts || {}\n    const privKey = p256.utils.randomPrivateKey()\n    return new P256Keypair(privKey, exportable)\n  }\n\n  static async import(\n    privKey: Uint8Array | string,\n    opts?: Partial<P256KeypairOptions>,\n  ): Promise<P256Keypair> {\n    const { exportable = false } = opts || {}\n    const privKeyBytes =\n      typeof privKey === 'string' ? ui8FromString(privKey, 'hex') : privKey\n    return new P256Keypair(privKeyBytes, exportable)\n  }\n\n  publicKeyBytes(): Uint8Array {\n    return this.publicKey\n  }\n\n  publicKeyStr(encoding: SupportedEncodings = 'base64pad'): string {\n    return ui8ToString(this.publicKey, encoding)\n  }\n\n  did(): string {\n    return did.formatDidKey(this.jwtAlg, this.publicKey)\n  }\n\n  async sign(msg: Uint8Array): Promise<Uint8Array> {\n    const msgHash = await sha256(msg)\n    // return raw 64 byte sig not DER-encoded\n    const sig = await p256.sign(msgHash, this.privateKey, { lowS: true })\n    return sig.toCompactRawBytes()\n  }\n\n  async export(): Promise<Uint8Array> {\n    if (!this.exportable) {\n      throw new Error('Private key is not exportable')\n    }\n    return this.privateKey\n  }\n}\n"
  },
  {
    "path": "packages/crypto/src/p256/operations.ts",
    "content": "import { p256 } from '@noble/curves/p256'\nimport { sha256 } from '@noble/hashes/sha256'\nimport { equals as ui8equals } from 'uint8arrays'\nimport { P256_DID_PREFIX } from '../const'\nimport { VerifyOptions } from '../types'\nimport { extractMultikey, extractPrefixedBytes, hasPrefix } from '../utils'\n\nexport const verifyDidSig = async (\n  did: string,\n  data: Uint8Array,\n  sig: Uint8Array,\n  opts?: VerifyOptions,\n): Promise<boolean> => {\n  const prefixedBytes = extractPrefixedBytes(extractMultikey(did))\n  if (!hasPrefix(prefixedBytes, P256_DID_PREFIX)) {\n    throw new Error(`Not a P-256 did:key: ${did}`)\n  }\n  const keyBytes = prefixedBytes.slice(P256_DID_PREFIX.length)\n  return verifySig(keyBytes, data, sig, opts)\n}\n\nexport const verifySig = async (\n  publicKey: Uint8Array,\n  data: Uint8Array,\n  sig: Uint8Array,\n  opts?: VerifyOptions,\n): Promise<boolean> => {\n  const allowMalleable = opts?.allowMalleableSig ?? false\n  const msgHash = await sha256(data)\n  return p256.verify(sig, msgHash, publicKey, {\n    format: allowMalleable ? undefined : 'compact', // prevent DER-encoded signatures\n    lowS: !allowMalleable,\n  })\n}\n\nexport const isCompactFormat = (sig: Uint8Array) => {\n  try {\n    const parsed = p256.Signature.fromCompact(sig)\n    return ui8equals(parsed.toCompactRawBytes(), sig)\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/crypto/src/p256/plugin.ts",
    "content": "import { P256_DID_PREFIX, P256_JWT_ALG } from '../const'\nimport { DidKeyPlugin } from '../types'\nimport { compressPubkey, decompressPubkey } from './encoding'\nimport { verifyDidSig } from './operations'\n\nexport const p256Plugin: DidKeyPlugin = {\n  prefix: P256_DID_PREFIX,\n  jwtAlg: P256_JWT_ALG,\n  verifySignature: verifyDidSig,\n\n  compressPubkey,\n  decompressPubkey,\n}\n"
  },
  {
    "path": "packages/crypto/src/plugins.ts",
    "content": "import { p256Plugin } from './p256/plugin'\nimport { secp256k1Plugin } from './secp256k1/plugin'\n\nexport const plugins = [p256Plugin, secp256k1Plugin]\n"
  },
  {
    "path": "packages/crypto/src/random.ts",
    "content": "import * as noble from '@noble/hashes/utils'\nimport * as uint8arrays from 'uint8arrays'\nimport { SupportedEncodings } from 'uint8arrays/to-string'\nimport { sha256 } from './sha'\n\nexport const randomBytes = noble.randomBytes\n\nexport const randomStr = (\n  byteLength: number,\n  encoding: SupportedEncodings,\n): string => {\n  const bytes = randomBytes(byteLength)\n  return uint8arrays.toString(bytes, encoding)\n}\n\nexport const randomIntFromSeed = async (\n  seed: string,\n  high: number,\n  low = 0,\n): Promise<number> => {\n  const hash = await sha256(seed)\n  const number = Buffer.from(hash).readUintBE(0, 6)\n  const range = high - low\n  const normalized = number % range\n  return normalized + low\n}\n"
  },
  {
    "path": "packages/crypto/src/secp256k1/encoding.ts",
    "content": "import { secp256k1 as k256 } from '@noble/curves/secp256k1'\n\nexport const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {\n  const point = k256.ProjectivePoint.fromHex(pubkeyBytes)\n  return point.toRawBytes(true)\n}\n\nexport const decompressPubkey = (compressed: Uint8Array): Uint8Array => {\n  if (compressed.length !== 33) {\n    throw new Error('Expected 33 byte compress pubkey')\n  }\n  const point = k256.ProjectivePoint.fromHex(compressed)\n  return point.toRawBytes(false)\n}\n"
  },
  {
    "path": "packages/crypto/src/secp256k1/keypair.ts",
    "content": "import { secp256k1 as k256 } from '@noble/curves/secp256k1'\nimport { sha256 } from '@noble/hashes/sha256'\nimport {\n  fromString as ui8FromString,\n  toString as ui8ToString,\n} from 'uint8arrays'\nimport { SupportedEncodings } from 'uint8arrays/to-string'\nimport { SECP256K1_JWT_ALG } from '../const'\nimport * as did from '../did'\nimport { Keypair } from '../types'\n\nexport type Secp256k1KeypairOptions = {\n  exportable: boolean\n}\n\nexport class Secp256k1Keypair implements Keypair {\n  jwtAlg = SECP256K1_JWT_ALG\n  private publicKey: Uint8Array\n\n  constructor(\n    private privateKey: Uint8Array,\n    private exportable: boolean,\n  ) {\n    this.publicKey = k256.getPublicKey(privateKey)\n  }\n\n  static async create(\n    opts?: Partial<Secp256k1KeypairOptions>,\n  ): Promise<Secp256k1Keypair> {\n    const { exportable = false } = opts || {}\n    const privKey = k256.utils.randomPrivateKey()\n    return new Secp256k1Keypair(privKey, exportable)\n  }\n\n  static async import(\n    privKey: Uint8Array | string,\n    opts?: Partial<Secp256k1KeypairOptions>,\n  ): Promise<Secp256k1Keypair> {\n    const { exportable = false } = opts || {}\n    const privKeyBytes =\n      typeof privKey === 'string' ? ui8FromString(privKey, 'hex') : privKey\n    return new Secp256k1Keypair(privKeyBytes, exportable)\n  }\n\n  publicKeyBytes(): Uint8Array {\n    return this.publicKey\n  }\n\n  publicKeyStr(encoding: SupportedEncodings = 'base64pad'): string {\n    return ui8ToString(this.publicKey, encoding)\n  }\n\n  did(): string {\n    return did.formatDidKey(this.jwtAlg, this.publicKey)\n  }\n\n  async sign(msg: Uint8Array): Promise<Uint8Array> {\n    const msgHash = await sha256(msg)\n    // return raw 64 byte sig not DER-encoded\n    const sig = await k256.sign(msgHash, this.privateKey, { lowS: true })\n    return sig.toCompactRawBytes()\n  }\n\n  async export(): Promise<Uint8Array> {\n    if (!this.exportable) {\n      throw new Error('Private key is not exportable')\n    }\n    return this.privateKey\n  }\n}\n"
  },
  {
    "path": "packages/crypto/src/secp256k1/operations.ts",
    "content": "import { secp256k1 as k256 } from '@noble/curves/secp256k1'\nimport { sha256 } from '@noble/hashes/sha256'\nimport * as ui8 from 'uint8arrays'\nimport { SECP256K1_DID_PREFIX } from '../const'\nimport { VerifyOptions } from '../types'\nimport { extractMultikey, extractPrefixedBytes, hasPrefix } from '../utils'\n\nexport const verifyDidSig = async (\n  did: string,\n  data: Uint8Array,\n  sig: Uint8Array,\n  opts?: VerifyOptions,\n): Promise<boolean> => {\n  const prefixedBytes = extractPrefixedBytes(extractMultikey(did))\n  if (!hasPrefix(prefixedBytes, SECP256K1_DID_PREFIX)) {\n    throw new Error(`Not a secp256k1 did:key: ${did}`)\n  }\n  const keyBytes = prefixedBytes.slice(SECP256K1_DID_PREFIX.length)\n  return verifySig(keyBytes, data, sig, opts)\n}\n\nexport const verifySig = async (\n  publicKey: Uint8Array,\n  data: Uint8Array,\n  sig: Uint8Array,\n  opts?: VerifyOptions,\n): Promise<boolean> => {\n  const allowMalleable = opts?.allowMalleableSig ?? false\n  const msgHash = await sha256(data)\n  return k256.verify(sig, msgHash, publicKey, {\n    format: allowMalleable ? undefined : 'compact', // prevent DER-encoded signatures\n    lowS: !allowMalleable,\n  })\n}\n\nexport const isCompactFormat = (sig: Uint8Array) => {\n  try {\n    const parsed = k256.Signature.fromCompact(sig)\n    return ui8.equals(parsed.toCompactRawBytes(), sig)\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/crypto/src/secp256k1/plugin.ts",
    "content": "import { SECP256K1_DID_PREFIX, SECP256K1_JWT_ALG } from '../const'\nimport { DidKeyPlugin } from '../types'\nimport { compressPubkey, decompressPubkey } from './encoding'\nimport { verifyDidSig } from './operations'\n\nexport const secp256k1Plugin: DidKeyPlugin = {\n  prefix: SECP256K1_DID_PREFIX,\n  jwtAlg: SECP256K1_JWT_ALG,\n  verifySignature: verifyDidSig,\n\n  compressPubkey,\n  decompressPubkey,\n}\n"
  },
  {
    "path": "packages/crypto/src/sha.ts",
    "content": "import * as noble from '@noble/hashes/sha256'\nimport * as uint8arrays from 'uint8arrays'\n\n// takes either bytes of utf8 input\n// @TODO this can be sync\nexport const sha256 = async (\n  input: Uint8Array | string,\n): Promise<Uint8Array> => {\n  const bytes =\n    typeof input === 'string' ? uint8arrays.fromString(input, 'utf8') : input\n  return noble.sha256(bytes)\n}\n\n// @TODO this can be sync\nexport const sha256Hex = async (\n  input: Uint8Array | string,\n): Promise<string> => {\n  const hash = await sha256(input)\n  return uint8arrays.toString(hash, 'hex')\n}\n"
  },
  {
    "path": "packages/crypto/src/types.ts",
    "content": "export interface Signer {\n  jwtAlg: string\n  sign(msg: Uint8Array): Promise<Uint8Array>\n}\n\nexport interface Didable {\n  did(): string\n}\n\nexport interface Keypair extends Signer, Didable {}\n\nexport interface ExportableKeypair extends Keypair {\n  export(): Promise<Uint8Array>\n}\n\nexport type DidKeyPlugin = {\n  prefix: Uint8Array\n  jwtAlg: string\n  verifySignature: (\n    did: string,\n    msg: Uint8Array,\n    data: Uint8Array,\n    opts?: VerifyOptions,\n  ) => Promise<boolean>\n\n  compressPubkey: (uncompressed: Uint8Array) => Uint8Array\n  decompressPubkey: (compressed: Uint8Array) => Uint8Array\n}\n\nexport type VerifyOptions = {\n  allowMalleableSig?: boolean\n}\n"
  },
  {
    "path": "packages/crypto/src/utils.ts",
    "content": "import * as uint8arrays from 'uint8arrays'\nimport { BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX } from './const'\n\nexport const extractMultikey = (did: string): string => {\n  if (!did.startsWith(DID_KEY_PREFIX)) {\n    throw new Error(`Incorrect prefix for did:key: ${did}`)\n  }\n  return did.slice(DID_KEY_PREFIX.length)\n}\n\nexport const extractPrefixedBytes = (multikey: string): Uint8Array => {\n  if (!multikey.startsWith(BASE58_MULTIBASE_PREFIX)) {\n    throw new Error(`Incorrect prefix for multikey: ${multikey}`)\n  }\n  return uint8arrays.fromString(\n    multikey.slice(BASE58_MULTIBASE_PREFIX.length),\n    'base58btc',\n  )\n}\n\nexport const hasPrefix = (bytes: Uint8Array, prefix: Uint8Array): boolean => {\n  return uint8arrays.equals(prefix, bytes.subarray(0, prefix.byteLength))\n}\n"
  },
  {
    "path": "packages/crypto/src/verify.ts",
    "content": "import * as uint8arrays from 'uint8arrays'\nimport { parseDidKey } from './did'\nimport { plugins } from './plugins'\nimport { VerifyOptions } from './types'\n\nexport const verifySignature = (\n  didKey: string,\n  data: Uint8Array,\n  sig: Uint8Array,\n  opts?: VerifyOptions & {\n    jwtAlg?: string\n  },\n): Promise<boolean> => {\n  const parsed = parseDidKey(didKey)\n  if (opts?.jwtAlg && opts.jwtAlg !== parsed.jwtAlg) {\n    throw new Error(`Expected key alg ${opts.jwtAlg}, got ${parsed.jwtAlg}`)\n  }\n  const plugin = plugins.find((p) => p.jwtAlg === parsed.jwtAlg)\n  if (!plugin) {\n    throw new Error(`Unsupported signature alg: ${parsed.jwtAlg}`)\n  }\n  return plugin.verifySignature(didKey, data, sig, opts)\n}\n\nexport const verifySignatureUtf8 = async (\n  didKey: string,\n  data: string,\n  sig: string,\n  opts?: VerifyOptions,\n): Promise<boolean> => {\n  const dataBytes = uint8arrays.fromString(data, 'utf8')\n  const sigBytes = uint8arrays.fromString(sig, 'base64url')\n  return verifySignature(didKey, dataBytes, sigBytes, opts)\n}\n"
  },
  {
    "path": "packages/crypto/tests/did.test.ts",
    "content": "import * as uint8arrays from 'uint8arrays'\nimport { P256Keypair, Secp256k1Keypair } from '../src'\nimport * as did from '../src/did'\n\ndescribe('secp256k1 did:key', () => {\n  it('derives the correct DID from the privatekey', async () => {\n    for (const vector of secpTestVectors) {\n      const keypair = await Secp256k1Keypair.import(vector.seed)\n      const did = keypair.did()\n      expect(did).toEqual(vector.id)\n    }\n  })\n\n  it('converts between bytes and did', async () => {\n    for (const vector of secpTestVectors) {\n      const keypair = await Secp256k1Keypair.import(vector.seed)\n      const didKey = did.formatDidKey('ES256K', keypair.publicKeyBytes())\n      expect(didKey).toEqual(vector.id)\n      const { jwtAlg, keyBytes } = did.parseDidKey(didKey)\n      expect(jwtAlg).toBe('ES256K')\n      expect(uint8arrays.equals(keyBytes, keypair.publicKeyBytes())).toBeTruthy\n    }\n  })\n})\n\ndescribe('P-256 did:key', () => {\n  it('derives the correct DID from the JWK', async () => {\n    for (const vector of p256TestVectors) {\n      const bytes = uint8arrays.fromString(vector.privateKeyBase58, 'base58btc')\n      const keypair = await P256Keypair.import(bytes)\n      const did = keypair.did()\n      expect(did).toEqual(vector.id)\n    }\n  })\n\n  it('converts between bytes and did', async () => {\n    for (const vector of p256TestVectors) {\n      const bytes = uint8arrays.fromString(vector.privateKeyBase58, 'base58btc')\n      const keypair = await P256Keypair.import(bytes)\n      const didKey = did.formatDidKey('ES256', keypair.publicKeyBytes())\n      expect(didKey).toEqual(vector.id)\n      const { jwtAlg, keyBytes } = did.parseDidKey(didKey)\n      expect(jwtAlg).toBe('ES256')\n      expect(uint8arrays.equals(keyBytes, keypair.publicKeyBytes())).toBeTruthy\n    }\n  })\n})\n\n// did:key secp256k1 test vectors from W3C\n// https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json\nconst secpTestVectors = [\n  {\n    seed: '9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c',\n    id: 'did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme',\n  },\n  {\n    seed: 'f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed',\n    id: 'did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2',\n  },\n  {\n    seed: '6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02',\n    id: 'did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N',\n  },\n  {\n    seed: 'c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15',\n    id: 'did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy',\n  },\n  {\n    seed: '175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133',\n    id: 'did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj',\n  },\n]\n\n// did:key p-256 test vectors from W3C\n// https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/nist-curves.json\nconst p256TestVectors = [\n  {\n    privateKeyBase58: '9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp',\n    id: 'did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb',\n  },\n]\n"
  },
  {
    "path": "packages/crypto/tests/key-compression.test.ts",
    "content": "import * as did from '../src/did'\nimport * as p256Encoding from '../src/p256/encoding'\nimport { P256Keypair } from '../src/p256/keypair'\nimport * as secpEncoding from '../src/secp256k1/encoding'\nimport { Secp256k1Keypair } from '../src/secp256k1/keypair'\n\ndescribe('public key compression', () => {\n  describe('secp256k1', () => {\n    let keyBytes: Uint8Array\n    let compressed: Uint8Array\n\n    it('compresses a key to the correct length', async () => {\n      const keypair = await Secp256k1Keypair.create()\n      const parsed = did.parseDidKey(keypair.did())\n      keyBytes = parsed.keyBytes\n      compressed = secpEncoding.compressPubkey(keyBytes)\n      expect(compressed.length).toBe(33)\n    })\n\n    it('decompresses a key to the original', async () => {\n      const decompressed = secpEncoding.decompressPubkey(compressed)\n      expect(decompressed.length).toBe(65)\n      expect(decompressed).toEqual(keyBytes)\n    })\n\n    it('works consistently', async () => {\n      const pubkeys: Uint8Array[] = []\n      for (let i = 0; i < 100; i++) {\n        const key = await Secp256k1Keypair.create()\n        const parsed = did.parseDidKey(key.did())\n        pubkeys.push(parsed.keyBytes)\n      }\n      const compressed = pubkeys.map(secpEncoding.compressPubkey)\n      const decompressed = compressed.map(secpEncoding.decompressPubkey)\n      expect(pubkeys).toEqual(decompressed)\n    })\n  })\n\n  describe('P-256', () => {\n    let keyBytes: Uint8Array\n    let compressed: Uint8Array\n\n    it('compresses a key to the correct length', async () => {\n      const keypair = await P256Keypair.create()\n      const parsed = did.parseDidKey(keypair.did())\n      keyBytes = parsed.keyBytes\n      compressed = p256Encoding.compressPubkey(keyBytes)\n      expect(compressed.length).toBe(33)\n    })\n\n    it('decompresses a key to the original', async () => {\n      const decompressed = p256Encoding.decompressPubkey(compressed)\n      expect(decompressed.length).toBe(65)\n      expect(decompressed).toEqual(keyBytes)\n    })\n\n    it('works consistently', async () => {\n      const pubkeys: Uint8Array[] = []\n      for (let i = 0; i < 100; i++) {\n        const key = await P256Keypair.create()\n        const parsed = did.parseDidKey(key.did())\n        pubkeys.push(parsed.keyBytes)\n      }\n      const compressed = pubkeys.map(p256Encoding.compressPubkey)\n      const decompressed = compressed.map(p256Encoding.decompressPubkey)\n      expect(pubkeys).toEqual(decompressed)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/crypto/tests/keypairs.test.ts",
    "content": "import { randomBytes } from '../src'\nimport { P256Keypair } from '../src/p256/keypair'\nimport * as p256 from '../src/p256/operations'\nimport { Secp256k1Keypair } from '../src/secp256k1/keypair'\nimport * as secp from '../src/secp256k1/operations'\n\ndescribe('keypairs', () => {\n  describe('secp256k1', () => {\n    let keypair: Secp256k1Keypair\n    let imported: Secp256k1Keypair\n\n    it('has the same DID on import', async () => {\n      keypair = await Secp256k1Keypair.create({ exportable: true })\n      const exported = await keypair.export()\n      imported = await Secp256k1Keypair.import(exported, { exportable: true })\n\n      expect(keypair.did()).toBe(imported.did())\n    })\n\n    it('produces a valid signature', async () => {\n      const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])\n      const sig = await imported.sign(data)\n\n      const validSig = await secp.verifyDidSig(keypair.did(), data, sig)\n\n      expect(validSig).toBeTruthy()\n    })\n\n    it('produces a valid signature on a typed array of a large arraybuffer', async () => {\n      const bytes = await randomBytes(8192)\n      const arrBuf = bytes.buffer\n      const sliceView = new Uint8Array(arrBuf, 1024, 1024)\n      expect(sliceView.buffer.byteLength).toBe(8192)\n      const sig = await imported.sign(sliceView)\n      const validSig = await secp.verifyDidSig(keypair.did(), sliceView, sig)\n      expect(validSig).toBeTruthy()\n    })\n  })\n\n  describe('P-256', () => {\n    let keypair: P256Keypair\n    let imported: P256Keypair\n\n    it('has the same DID on import', async () => {\n      keypair = await P256Keypair.create({ exportable: true })\n      const exported = await keypair.export()\n      imported = await P256Keypair.import(exported, { exportable: true })\n\n      expect(keypair.did()).toBe(imported.did())\n    })\n\n    it('produces a valid signature', async () => {\n      const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])\n      const sig = await imported.sign(data)\n\n      const validSig = await p256.verifyDidSig(keypair.did(), data, sig)\n\n      expect(validSig).toBeTruthy()\n    })\n\n    it('produces a valid signature on a typed array of a large arraybuffer', async () => {\n      const bytes = await randomBytes(8192)\n      const arrBuf = bytes.buffer\n      const sliceView = new Uint8Array(arrBuf, 1024, 1024)\n      expect(sliceView.buffer.byteLength).toBe(8192)\n      const sig = await imported.sign(sliceView)\n      const validSig = await p256.verifyDidSig(keypair.did(), sliceView, sig)\n      expect(validSig).toBeTruthy()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/crypto/tests/random.test.ts",
    "content": "import { randomIntFromSeed } from '../src'\n\ndescribe('randomIntFromSeed()', () => {\n  it('has good distribution for low bucket count.', async () => {\n    const counts: [zero: number, one: number] = [0, 0]\n    const salt = Math.random()\n    for (let i = 0; i < 10000; ++i) {\n      const int = await randomIntFromSeed(`${i}${salt}`, 2)\n      counts[int]++\n    }\n    const [zero, one] = counts\n    expect(zero + one).toEqual(10000)\n    expect(Math.max(zero, one) / Math.min(zero, one)).toBeLessThan(1.1)\n  })\n})\n"
  },
  {
    "path": "packages/crypto/tests/signatures.test.ts",
    "content": "import fs from 'node:fs'\nimport { p256 as nobleP256 } from '@noble/curves/p256'\nimport { secp256k1 as nobleK256 } from '@noble/curves/secp256k1'\nimport * as uint8arrays from 'uint8arrays'\nimport { cborEncode } from '@atproto/common'\nimport {\n  P256_JWT_ALG,\n  SECP256K1_JWT_ALG,\n  bytesToMultibase,\n  multibaseToBytes,\n  parseDidKey,\n  sha256,\n} from '../src'\nimport { P256Keypair } from '../src/p256/keypair'\nimport * as p256 from '../src/p256/operations'\nimport { Secp256k1Keypair } from '../src/secp256k1/keypair'\nimport * as secp from '../src/secp256k1/operations'\n\ndescribe('signatures', () => {\n  let vectors: TestVector[]\n\n  beforeAll(() => {\n    vectors = JSON.parse(\n      fs.readFileSync(`${__dirname}/signature-fixtures.json`).toString(),\n    )\n  })\n\n  it('verifies secp256k1 and P-256 test vectors', async () => {\n    for (const vector of vectors) {\n      const messageBytes = uint8arrays.fromString(\n        vector.messageBase64,\n        'base64',\n      )\n      const signatureBytes = uint8arrays.fromString(\n        vector.signatureBase64,\n        'base64',\n      )\n      const keyBytes = multibaseToBytes(vector.publicKeyMultibase)\n      const didKey = parseDidKey(vector.publicKeyDid)\n      expect(uint8arrays.equals(keyBytes, didKey.keyBytes))\n      if (vector.algorithm === P256_JWT_ALG) {\n        const verified = await p256.verifySig(\n          keyBytes,\n          messageBytes,\n          signatureBytes,\n        )\n        expect(verified).toEqual(vector.validSignature)\n      } else if (vector.algorithm === SECP256K1_JWT_ALG) {\n        const verified = await secp.verifySig(\n          keyBytes,\n          messageBytes,\n          signatureBytes,\n        )\n        expect(verified).toEqual(vector.validSignature)\n      } else {\n        throw new Error('Unsupported test vector')\n      }\n    }\n  })\n\n  it('verifies high-s signatures with explicit option', async () => {\n    const highSVectors = vectors.filter((vec) => vec.tags.includes('high-s'))\n    expect(highSVectors.length).toBeGreaterThanOrEqual(2)\n    for (const vector of highSVectors) {\n      const messageBytes = uint8arrays.fromString(\n        vector.messageBase64,\n        'base64',\n      )\n      const signatureBytes = uint8arrays.fromString(\n        vector.signatureBase64,\n        'base64',\n      )\n      const keyBytes = multibaseToBytes(vector.publicKeyMultibase)\n      const didKey = parseDidKey(vector.publicKeyDid)\n      expect(uint8arrays.equals(keyBytes, didKey.keyBytes))\n      if (vector.algorithm === P256_JWT_ALG) {\n        const verified = await p256.verifySig(\n          keyBytes,\n          messageBytes,\n          signatureBytes,\n          { allowMalleableSig: true },\n        )\n        expect(verified).toEqual(true)\n        expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement\n      } else if (vector.algorithm === SECP256K1_JWT_ALG) {\n        const verified = await secp.verifySig(\n          keyBytes,\n          messageBytes,\n          signatureBytes,\n          { allowMalleableSig: true },\n        )\n        expect(verified).toEqual(true)\n        expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement\n      } else {\n        throw new Error('Unsupported test vector')\n      }\n    }\n  })\n\n  it('verifies der-encoded signatures with explicit option', async () => {\n    const DERVectors = vectors.filter((vec) => vec.tags.includes('der-encoded'))\n    expect(DERVectors.length).toBeGreaterThanOrEqual(2)\n    for (const vector of DERVectors) {\n      const messageBytes = uint8arrays.fromString(\n        vector.messageBase64,\n        'base64',\n      )\n      const signatureBytes = uint8arrays.fromString(\n        vector.signatureBase64,\n        'base64',\n      )\n      const keyBytes = multibaseToBytes(vector.publicKeyMultibase)\n      const didKey = parseDidKey(vector.publicKeyDid)\n      expect(uint8arrays.equals(keyBytes, didKey.keyBytes))\n      if (vector.algorithm === P256_JWT_ALG) {\n        const verified = await p256.verifySig(\n          keyBytes,\n          messageBytes,\n          signatureBytes,\n          { allowMalleableSig: true },\n        )\n        expect(verified).toEqual(true)\n        expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement\n      } else if (vector.algorithm === SECP256K1_JWT_ALG) {\n        const verified = await secp.verifySig(\n          keyBytes,\n          messageBytes,\n          signatureBytes,\n          { allowMalleableSig: true },\n        )\n        expect(verified).toEqual(true)\n        expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement\n      } else {\n        throw new Error('Unsupported test vector')\n      }\n    }\n  })\n})\n\n// @ts-expect-error\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nasync function generateTestVectors(): Promise<TestVector[]> {\n  const p256Key = await P256Keypair.create({ exportable: true })\n  const secpKey = await Secp256k1Keypair.create({ exportable: true })\n  const messageBytes = cborEncode({ hello: 'world' })\n  const messageBase64 = uint8arrays.toString(messageBytes, 'base64')\n  return [\n    {\n      messageBase64,\n      algorithm: P256_JWT_ALG, // \"ES256\" / ecdsa p-256\n      publicKeyDid: p256Key.did(),\n      publicKeyMultibase: bytesToMultibase(\n        p256Key.publicKeyBytes(),\n        'base58btc',\n      ),\n      signatureBase64: uint8arrays.toString(\n        await p256Key.sign(messageBytes),\n        'base64',\n      ),\n      validSignature: true,\n      tags: [],\n    },\n    {\n      messageBase64,\n      algorithm: SECP256K1_JWT_ALG, // \"ES256K\" / secp256k\n      publicKeyDid: secpKey.did(),\n      publicKeyMultibase: bytesToMultibase(\n        secpKey.publicKeyBytes(),\n        'base58btc',\n      ),\n      signatureBase64: uint8arrays.toString(\n        await secpKey.sign(messageBytes),\n        'base64',\n      ),\n      validSignature: true,\n      tags: [],\n    },\n    // these vectors test to ensure we don't allow high-s signatures\n    {\n      messageBase64,\n      algorithm: P256_JWT_ALG, // \"ES256\" / ecdsa p-256\n      publicKeyDid: p256Key.did(),\n      publicKeyMultibase: bytesToMultibase(\n        p256Key.publicKeyBytes(),\n        'base58btc',\n      ),\n      signatureBase64: await makeHighSSig(\n        messageBytes,\n        await p256Key.export(),\n        P256_JWT_ALG,\n      ),\n      validSignature: false,\n      tags: ['high-s'],\n    },\n    {\n      messageBase64,\n      algorithm: SECP256K1_JWT_ALG, // \"ES256K\" / secp256k\n      publicKeyDid: secpKey.did(),\n      publicKeyMultibase: bytesToMultibase(\n        secpKey.publicKeyBytes(),\n        'base58btc',\n      ),\n      signatureBase64: await makeHighSSig(\n        messageBytes,\n        await secpKey.export(),\n        SECP256K1_JWT_ALG,\n      ),\n      validSignature: false,\n      tags: ['high-s'],\n    },\n    // these vectors test to ensure we don't allow der-encoded signatures\n    {\n      messageBase64,\n      algorithm: P256_JWT_ALG, // \"ES256\" / ecdsa p-256\n      publicKeyDid: p256Key.did(),\n      publicKeyMultibase: bytesToMultibase(\n        p256Key.publicKeyBytes(),\n        'base58btc',\n      ),\n      signatureBase64: await makeDerEncodedSig(\n        messageBytes,\n        await p256Key.export(),\n        P256_JWT_ALG,\n      ),\n      validSignature: false,\n      tags: ['der-encoded'],\n    },\n    {\n      messageBase64,\n      algorithm: SECP256K1_JWT_ALG, // \"ES256K\" / secp256k\n      publicKeyDid: secpKey.did(),\n      publicKeyMultibase: bytesToMultibase(\n        secpKey.publicKeyBytes(),\n        'base58btc',\n      ),\n      signatureBase64: await makeDerEncodedSig(\n        messageBytes,\n        await secpKey.export(),\n        SECP256K1_JWT_ALG,\n      ),\n      validSignature: false,\n      tags: ['der-encoded'],\n    },\n  ]\n}\n\nasync function makeHighSSig(\n  msgBytes: Uint8Array,\n  keyBytes: Uint8Array,\n  alg: string,\n): Promise<string> {\n  const hash = await sha256(msgBytes)\n\n  let sig: string | undefined\n  do {\n    if (alg === SECP256K1_JWT_ALG) {\n      const attempt = await nobleK256.sign(hash, keyBytes, { lowS: false })\n      if (attempt.hasHighS()) {\n        sig = uint8arrays.toString(attempt.toCompactRawBytes(), 'base64')\n      }\n    } else {\n      const attempt = await nobleP256.sign(hash, keyBytes, { lowS: false })\n      if (attempt.hasHighS()) {\n        sig = uint8arrays.toString(attempt.toCompactRawBytes(), 'base64')\n      }\n    }\n  } while (sig === undefined)\n  return sig\n}\n\nasync function makeDerEncodedSig(\n  msgBytes: Uint8Array,\n  keyBytes: Uint8Array,\n  alg: string,\n): Promise<string> {\n  const hash = await sha256(msgBytes)\n\n  let sig: string\n  if (alg === SECP256K1_JWT_ALG) {\n    const attempt = await nobleK256.sign(hash, keyBytes, { lowS: true })\n    sig = uint8arrays.toString(attempt.toDERRawBytes(), 'base64')\n  } else {\n    const attempt = await nobleP256.sign(hash, keyBytes, { lowS: true })\n    sig = uint8arrays.toString(attempt.toDERRawBytes(), 'base64')\n  }\n  return sig\n}\n\ntype TestVector = {\n  algorithm: string\n  publicKeyDid: string\n  publicKeyMultibase: string\n  messageBase64: string\n  signatureBase64: string\n  validSignature: boolean\n  tags: string[]\n}\n"
  },
  {
    "path": "packages/crypto/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/crypto/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/crypto/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/dev-env/CHANGELOG.md",
    "content": "# @atproto/dev-env\n\n## 0.3.215\n\n### Patch Changes\n\n- Updated dependencies [[`3b41b81`](https://github.com/bluesky-social/atproto/commit/3b41b81e27e0aba55406642c07da01c290281647), [`4ecde48`](https://github.com/bluesky-social/atproto/commit/4ecde4879ffd769fe2c7a0f1d4e3275c776114f4)]:\n  - @atproto/bsky@0.0.221\n  - @atproto/xrpc-server@0.10.17\n  - @atproto/common-web@0.4.19\n  - @atproto/pds@0.4.216\n\n## 0.3.214\n\n### Patch Changes\n\n- [#4763](https://github.com/bluesky-social/atproto/pull/4763) [`c89225f`](https://github.com/bluesky-social/atproto/commit/c89225f9724f281308e59b8f32fbe4e420fb4f8c) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Create chat declarations for dev-env\n\n- Updated dependencies [[`383e157`](https://github.com/bluesky-social/atproto/commit/383e157021564a6fb51baac584dd3e4f988f1d33), [`efb2b58`](https://github.com/bluesky-social/atproto/commit/efb2b58fb27a242d5308bf15916ee222daa2019b), [`7ed5704`](https://github.com/bluesky-social/atproto/commit/7ed57043c12aedb0faf6b7dc947adfcfff570b6d), [`eaee3d4`](https://github.com/bluesky-social/atproto/commit/eaee3d430554436964d45f38bbeb1132ae9b8862), [`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa), [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f), [`ff42a3a`](https://github.com/bluesky-social/atproto/commit/ff42a3afc3a0d4146a6618a910fa612c7e878ea7), [`bc69b03`](https://github.com/bluesky-social/atproto/commit/bc69b03f53da3ec52bc3eed0738308f320386e75), [`139b294`](https://github.com/bluesky-social/atproto/commit/139b2941d640bafa1e7d3a56e0608dc42bb0006c)]:\n  - @atproto/bsky@0.0.220\n  - @atproto/pds@0.4.215\n  - @atproto/bsync@0.0.25\n  - @atproto/ozone@0.1.167\n  - @atproto/api@0.19.4\n  - @atproto/syntax@0.5.1\n  - @atproto/xrpc-server@0.10.16\n\n## 0.3.213\n\n### Patch Changes\n\n- Updated dependencies [[`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c), [`0e5df95`](https://github.com/bluesky-social/atproto/commit/0e5df95e3a8d81931524848d301cd43d1f12fb78)]:\n  - @atproto/ozone@0.1.166\n  - @atproto/bsky@0.0.219\n  - @atproto/api@0.19.2\n  - @atproto/pds@0.4.214\n\n## 0.3.212\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`d5f4224`](https://github.com/bluesky-social/atproto/commit/d5f4224f73894a62d23d2375950cdadce6f130f4), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`138f0a0`](https://github.com/bluesky-social/atproto/commit/138f0a0b374c0d78372d5095237061d46db75a32), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/pds@0.4.213\n  - @atproto/common-web@0.4.18\n  - @atproto/sync@0.1.40\n  - @atproto/xrpc-server@0.10.15\n  - @atproto/ozone@0.1.165\n  - @atproto/bsky@0.0.218\n  - @atproto/api@0.19.1\n  - @atproto/bsync@0.0.24\n  - @atproto/lexicon@0.6.2\n\n## 0.3.211\n\n### Patch Changes\n\n- Updated dependencies [[`450f085`](https://github.com/bluesky-social/atproto/commit/450f0856630fa08c20dc60fef8b5d2a07b9a2552)]:\n  - @atproto/api@0.19.0\n  - @atproto/bsky@0.0.217\n  - @atproto/ozone@0.1.164\n  - @atproto/pds@0.4.212\n\n## 0.3.210\n\n### Patch Changes\n\n- Updated dependencies [[`978a99e`](https://github.com/bluesky-social/atproto/commit/978a99efad8393247449bebd88af1ac5b602842e)]:\n  - @atproto/bsky@0.0.216\n  - @atproto/pds@0.4.209\n\n## 0.3.209\n\n### Patch Changes\n\n- Updated dependencies [[`60f84eb`](https://github.com/bluesky-social/atproto/commit/60f84ebe47016828add07b143c403e331c58ee78), [`50dfbec`](https://github.com/bluesky-social/atproto/commit/50dfbec512682d35e8108b952e8f0533da71beef), [`8711f6e`](https://github.com/bluesky-social/atproto/commit/8711f6e1b870d27080a4cb7e56e58bf538ab5778), [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317), [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317)]:\n  - @atproto/api@0.18.21\n  - @atproto/pds@0.4.209\n  - @atproto/bsky@0.0.215\n\n## 0.3.208\n\n### Patch Changes\n\n- Updated dependencies [[`4f5c400`](https://github.com/bluesky-social/atproto/commit/4f5c4001271bbf38b30506efd30ebdabb969878f)]:\n  - @atproto/bsky@0.0.214\n  - @atproto/api@0.18.20\n  - @atproto/pds@0.4.207\n\n## 0.3.207\n\n### Patch Changes\n\n- Updated dependencies [[`25cea46`](https://github.com/bluesky-social/atproto/commit/25cea46aaa3d84521d1e977b67d3ac3581304ba1)]:\n  - @atproto/bsky@0.0.213\n  - @atproto/api@0.18.19\n  - @atproto/pds@0.4.207\n  - @atproto/common-web@0.4.15\n  - @atproto/xrpc-server@0.10.11\n\n## 0.3.206\n\n### Patch Changes\n\n- Updated dependencies [[`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/ozone@0.1.162\n  - @atproto/bsky@0.0.212\n  - @atproto/api@0.18.18\n  - @atproto/pds@0.4.206\n  - @atproto/common-web@0.4.14\n\n## 0.3.205\n\n### Patch Changes\n\n- Updated dependencies [[`cbd5837`](https://github.com/bluesky-social/atproto/commit/cbd5837f015e6b5e098a60098faea82e7f9419f3), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`d8e5363`](https://github.com/bluesky-social/atproto/commit/d8e53636c84da6dd3dd69e1d260f4fa617f3883c), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`9bdd358`](https://github.com/bluesky-social/atproto/commit/9bdd35881aa7efce6595ef708ba13d99c473d114), [`e6e43f3`](https://github.com/bluesky-social/atproto/commit/e6e43f3ad3594e7cb24e2f3effe5ef4b1696c8ff), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`ce356cd`](https://github.com/bluesky-social/atproto/commit/ce356cde55c9ff46758d0a6f39397d6710509b40), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`3fbec80`](https://github.com/bluesky-social/atproto/commit/3fbec803ed188cef9baa998ac3e66ccb8c0f1e5c), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/api@0.18.17\n  - @atproto/bsky@0.0.211\n  - @atproto/syntax@0.4.3\n  - @atproto/common-web@0.4.13\n  - @atproto/lexicon@0.6.1\n  - @atproto/pds@0.4.205\n  - @atproto/xrpc-server@0.10.10\n\n## 0.3.204\n\n### Patch Changes\n\n- Updated dependencies [[`6752056`](https://github.com/bluesky-social/atproto/commit/6752056f4666f1f85149d1c6821aed1ad8d88442)]:\n  - @atproto/bsky@0.0.210\n  - @atproto/pds@0.4.203\n\n## 0.3.203\n\n### Patch Changes\n\n- Updated dependencies [[`d2ed731`](https://github.com/bluesky-social/atproto/commit/d2ed7311a20b8c990003628c932e3e5aa6569086)]:\n  - @atproto/bsky@0.0.209\n  - @atproto/api@0.18.13\n  - @atproto/pds@0.4.203\n\n## 0.3.202\n\n### Patch Changes\n\n- Updated dependencies [[`b329266`](https://github.com/bluesky-social/atproto/commit/b329266853b4867fbbcafc8845e479c888f8ac36)]:\n  - @atproto/bsky@0.0.208\n  - @atproto/common-web@0.4.11\n  - @atproto/pds@0.4.203\n  - @atproto/xrpc-server@0.10.8\n\n## 0.3.201\n\n### Patch Changes\n\n- Updated dependencies [[`7750b91`](https://github.com/bluesky-social/atproto/commit/7750b91500eef6965a17bc8ec0b3ddfd6327485a)]:\n  - @atproto/bsky@0.0.207\n  - @atproto/api@0.18.12\n  - @atproto/pds@0.4.202\n\n## 0.3.200\n\n### Patch Changes\n\n- Updated dependencies [[`7ef8935`](https://github.com/bluesky-social/atproto/commit/7ef893563b25252ecf246e0d75e17855a7284e53)]:\n  - @atproto/bsky@0.0.206\n  - @atproto/api@0.18.11\n  - @atproto/pds@0.4.202\n\n## 0.3.199\n\n### Patch Changes\n\n- Updated dependencies [[`63f97ae`](https://github.com/bluesky-social/atproto/commit/63f97ae9c1f57def2d489ab8ce7f83a84a7d1ba1)]:\n  - @atproto/api@0.18.10\n  - @atproto/bsky@0.0.205\n  - @atproto/pds@0.4.202\n\n## 0.3.198\n\n### Patch Changes\n\n- Updated dependencies [[`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164), [`ce497e8`](https://github.com/bluesky-social/atproto/commit/ce497e85437c7ced3147691fb877e1f76f6ff472), [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164), [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164)]:\n  - @atproto/api@0.18.9\n  - @atproto/bsky@0.0.204\n  - @atproto/pds@0.4.200\n  - @atproto/common-web@0.4.8\n  - @atproto/xrpc-server@0.10.5\n\n## 0.3.197\n\n### Patch Changes\n\n- Updated dependencies [[`dd0fe8d`](https://github.com/bluesky-social/atproto/commit/dd0fe8d5e74e19b2cb37aa6a307b88f1f6bd1c9c)]:\n  - @atproto/bsky@0.0.203\n  - @atproto/pds@0.4.199\n\n## 0.3.196\n\n### Patch Changes\n\n- Updated dependencies [[`45928bf`](https://github.com/bluesky-social/atproto/commit/45928bfcd6d220216078d5106f134fc3a81f564b)]:\n  - @atproto/bsky@0.0.202\n  - @atproto/pds@0.4.199\n\n## 0.3.195\n\n### Patch Changes\n\n- Updated dependencies [[`39fa570`](https://github.com/bluesky-social/atproto/commit/39fa57080fa04aa547b093cfeaaced3e2e62fc41), [`f4cef84`](https://github.com/bluesky-social/atproto/commit/f4cef84494114ca927c66428920ca3dc24ad2b1e), [`6fab394`](https://github.com/bluesky-social/atproto/commit/6fab3940f6d09b4e9888e6c4140a70d3e4ebcb00)]:\n  - @atproto/api@0.18.6\n  - @atproto/pds@0.4.199\n  - @atproto/bsky@0.0.201\n\n## 0.3.194\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`380aa3b`](https://github.com/bluesky-social/atproto/commit/380aa3bfe73b5c4e59961c27ae988786b69c129d), [`cfa01ed`](https://github.com/bluesky-social/atproto/commit/cfa01edb9cd769b49327b8875b890d84fa8956d2), [`308f432`](https://github.com/bluesky-social/atproto/commit/308f432f7aef196b4df0a6dc7c5367ab5a8b8964), [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763), [`7eb99f2`](https://github.com/bluesky-social/atproto/commit/7eb99f2ac7049ddf8aea050e77e0236c1277909a)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/common-web@0.4.7\n  - @atproto/api@0.18.5\n  - @atproto/ozone@0.1.160\n  - @atproto/pds@0.4.198\n  - @atproto/xrpc-server@0.10.3\n  - @atproto/bsky@0.0.200\n  - @atproto/sync@0.1.39\n\n## 0.3.193\n\n### Patch Changes\n\n- Updated dependencies [[`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7)]:\n  - @atproto/syntax@0.4.2\n  - @atproto/crypto@0.4.5\n  - @atproto/bsky@0.0.199\n  - @atproto/api@0.18.4\n  - @atproto/ozone@0.1.159\n  - @atproto/pds@0.4.197\n  - @atproto/common-web@0.4.6\n  - @atproto/xrpc-server@0.10.2\n\n## 0.3.192\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n  - @atproto/xrpc-server@0.10.0\n  - @atproto/api@0.18.2\n  - @atproto/identity@0.4.10\n  - @atproto/lexicon@0.5.2\n  - @atproto/bsky@0.0.198\n  - @atproto/ozone@0.1.157\n  - @atproto/pds@0.4.195\n  - @atproto/sync@0.1.38\n  - @atproto/bsync@0.0.23\n  - @atproto/crypto@0.4.4\n\n## 0.3.191\n\n### Patch Changes\n\n- [#4344](https://github.com/bluesky-social/atproto/pull/4344) [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc) Thanks [@foysalit](https://github.com/foysalit)! - Add targetServices param to takedown events allowing mods to specify which service to apply takedown on\n\n- Updated dependencies [[`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e), [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc), [`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/ozone@0.1.156\n  - @atproto/api@0.18.1\n  - @atproto/pds@0.4.194\n  - @atproto/bsky@0.0.197\n  - @atproto/xrpc-server@0.9.6\n  - @atproto/sync@0.1.37\n\n## 0.3.190\n\n### Patch Changes\n\n- Updated dependencies [[`f8e56b387`](https://github.com/bluesky-social/atproto/commit/f8e56b387fcd3bc8405225c1bbdef66ca5dc1591), [`3628cebfb`](https://github.com/bluesky-social/atproto/commit/3628cebfbb04ba49f326bbf411a2d15de2900302), [`82e75bf6c`](https://github.com/bluesky-social/atproto/commit/82e75bf6c1b31daa834386edce35c8aa4c787229)]:\n  - @atproto/bsky@0.0.196\n  - @atproto/ozone@0.1.155\n  - @atproto/pds@0.4.193\n\n## 0.3.189\n\n### Patch Changes\n\n- Updated dependencies [[`94ddc8219`](https://github.com/bluesky-social/atproto/commit/94ddc8219c144475df622137ab88895255136eda), [`756ab5d87`](https://github.com/bluesky-social/atproto/commit/756ab5d87fea75e8648a6bdd545d8b441bfb2dd6), [`39b5c08e0`](https://github.com/bluesky-social/atproto/commit/39b5c08e0799468eba0c3bf50f4f5a8104c35f34)]:\n  - @atproto/api@0.18.0\n  - @atproto/sync@0.1.36\n  - @atproto/bsky@0.0.195\n  - @atproto/ozone@0.1.154\n  - @atproto/pds@0.4.192\n\n## 0.3.188\n\n### Patch Changes\n\n- Updated dependencies [[`15fe80c39`](https://github.com/bluesky-social/atproto/commit/15fe80c39ff428652dfaa6b30c0bdb59a145aac6)]:\n  - @atproto/api@0.17.7\n  - @atproto/bsky@0.0.194\n  - @atproto/ozone@0.1.153\n  - @atproto/pds@0.4.191\n\n## 0.3.187\n\n### Patch Changes\n\n- Updated dependencies [[`7c1429fe3`](https://github.com/bluesky-social/atproto/commit/7c1429fe36226d0d57e57c037ba4221d2fbd57ee), [`cdb6b27fc`](https://github.com/bluesky-social/atproto/commit/cdb6b27fc6be1e858476d8c55fd0c37561b972b4)]:\n  - @atproto/api@0.17.6\n  - @atproto/bsky@0.0.193\n  - @atproto/ozone@0.1.152\n  - @atproto/pds@0.4.190\n\n## 0.3.186\n\n### Patch Changes\n\n- [#4279](https://github.com/bluesky-social/atproto/pull/4279) [`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031) Thanks [@foysalit](https://github.com/foysalit)! - Add strike system to ozone\n\n- Updated dependencies [[`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031)]:\n  - @atproto/ozone@0.1.151\n  - @atproto/api@0.17.5\n  - @atproto/pds@0.4.189\n  - @atproto/bsky@0.0.192\n\n## 0.3.185\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.188\n  - @atproto/bsky@0.0.191\n  - @atproto/ozone@0.1.150\n\n## 0.3.184\n\n### Patch Changes\n\n- Updated dependencies [[`a8e307ef4`](https://github.com/bluesky-social/atproto/commit/a8e307ef4851b164ee38bb5149343631e329f143)]:\n  - @atproto/api@0.17.4\n  - @atproto/pds@0.4.187\n  - @atproto/bsky@0.0.191\n  - @atproto/ozone@0.1.150\n\n## 0.3.183\n\n### Patch Changes\n\n- Updated dependencies [[`ca768fe1b`](https://github.com/bluesky-social/atproto/commit/ca768fe1b0ba1662140b6eea550683d8675fa56e), [`386f583cf`](https://github.com/bluesky-social/atproto/commit/386f583cffa2c596a12be4e98dde498f3b8670f6)]:\n  - @atproto/ozone@0.1.149\n  - @atproto/api@0.17.3\n  - @atproto/bsky@0.0.190\n  - @atproto/pds@0.4.186\n\n## 0.3.182\n\n### Patch Changes\n\n- Updated dependencies [[`1cb5b9b80`](https://github.com/bluesky-social/atproto/commit/1cb5b9b80c20a054f7fbacd89d0d440dc2241d81)]:\n  - @atproto/ozone@0.1.148\n  - @atproto/api@0.17.2\n  - @atproto/bsky@0.0.189\n  - @atproto/pds@0.4.185\n\n## 0.3.181\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd)]:\n  - @atproto/api@0.17.1\n  - @atproto/ozone@0.1.147\n  - @atproto/pds@0.4.184\n  - @atproto/bsky@0.0.188\n\n## 0.3.180\n\n### Patch Changes\n\n- Updated dependencies [[`dba2d30e2`](https://github.com/bluesky-social/atproto/commit/dba2d30e2c4ce0eb624f2139b485719d14474940), [`7f38ee03c`](https://github.com/bluesky-social/atproto/commit/7f38ee03c01357686a4ce54cdf8eed4e37074a58)]:\n  - @atproto/api@0.17.0\n  - @atproto/bsky@0.0.187\n  - @atproto/ozone@0.1.146\n  - @atproto/pds@0.4.183\n\n## 0.3.179\n\n### Patch Changes\n\n- Updated dependencies [[`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33), [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33)]:\n  - @atproto/api@0.16.11\n  - @atproto/bsky@0.0.186\n  - @atproto/pds@0.4.182\n  - @atproto/sync@0.1.35\n  - @atproto/ozone@0.1.145\n\n## 0.3.178\n\n### Patch Changes\n\n- [#4224](https://github.com/bluesky-social/atproto/pull/4224) [`8dc4caf55`](https://github.com/bluesky-social/atproto/commit/8dc4caf55840578c835b4c851d4a599c15627a78) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Accept profile creation overrides\n\n- [#4181](https://github.com/bluesky-social/atproto/pull/4181) [`86bb25768`](https://github.com/bluesky-social/atproto/commit/86bb25768d9099f06c45960bd46df72120406690) Thanks [@rafaeleyng](https://github.com/rafaeleyng)! - Use static DID for bsky on dev-env\n\n- Updated dependencies [[`8dc4caf55`](https://github.com/bluesky-social/atproto/commit/8dc4caf55840578c835b4c851d4a599c15627a78), [`0c20539c7`](https://github.com/bluesky-social/atproto/commit/0c20539c7185f6070d4337dbda3da92c39a3434f)]:\n  - @atproto/api@0.16.10\n  - @atproto/pds@0.4.181\n  - @atproto/bsky@0.0.185\n  - @atproto/ozone@0.1.144\n\n## 0.3.177\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.180\n  - @atproto/bsky@0.0.184\n  - @atproto/ozone@0.1.143\n\n## 0.3.176\n\n### Patch Changes\n\n- Updated dependencies [[`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05), [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05)]:\n  - @atproto/pds@0.4.179\n  - @atproto/bsky@0.0.184\n  - @atproto/ozone@0.1.143\n\n## 0.3.175\n\n### Patch Changes\n\n- Updated dependencies [[`ff30786af`](https://github.com/bluesky-social/atproto/commit/ff30786af6f72ad6506939bfca01a3f55a096c1c)]:\n  - @atproto/api@0.16.9\n  - @atproto/pds@0.4.178\n  - @atproto/bsky@0.0.184\n  - @atproto/ozone@0.1.143\n\n## 0.3.174\n\n### Patch Changes\n\n- Updated dependencies [[`55cc15cdd`](https://github.com/bluesky-social/atproto/commit/55cc15cdd664865d53f027e63708226012dc39ef)]:\n  - @atproto/ozone@0.1.142\n\n## 0.3.173\n\n### Patch Changes\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523), [`6d7bf4bff`](https://github.com/bluesky-social/atproto/commit/6d7bf4bffc3fee7a1fca488e6b75699385a04f37), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/pds@0.4.177\n  - @atproto/ozone@0.1.141\n  - @atproto/bsky@0.0.183\n  - @atproto/api@0.16.8\n  - @atproto/common-web@0.4.3\n  - @atproto/sync@0.1.34\n  - @atproto/identity@0.4.9\n  - @atproto/lexicon@0.5.1\n  - @atproto/bsync@0.0.22\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.9.5\n\n## 0.3.172\n\n### Patch Changes\n\n- Updated dependencies [[`09717f29a`](https://github.com/bluesky-social/atproto/commit/09717f29ac7ca742c9c3310980dbe4d112b7597f)]:\n  - @atproto/api@0.16.7\n  - @atproto/bsky@0.0.182\n  - @atproto/ozone@0.1.140\n  - @atproto/pds@0.4.176\n\n## 0.3.171\n\n### Patch Changes\n\n- Updated dependencies [[`d54d278ab`](https://github.com/bluesky-social/atproto/commit/d54d278abd679fbb44ff795d02b53b7caab31301)]:\n  - @atproto/pds@0.4.175\n  - @atproto/bsky@0.0.181\n  - @atproto/ozone@0.1.139\n\n## 0.3.170\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.174\n  - @atproto/bsky@0.0.181\n  - @atproto/ozone@0.1.139\n\n## 0.3.169\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disable thread v2 mock data\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/api@0.16.6\n  - @atproto/bsky@0.0.181\n  - @atproto/ozone@0.1.139\n  - @atproto/pds@0.4.173\n  - @atproto/sync@0.1.33\n  - @atproto/xrpc-server@0.9.4\n  - @atproto/bsync@0.0.21\n\n## 0.3.168\n\n### Patch Changes\n\n- Updated dependencies [[`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6)]:\n  - @atproto/ozone@0.1.138\n  - @atproto/bsky@0.0.180\n  - @atproto/api@0.16.5\n  - @atproto/pds@0.4.172\n\n## 0.3.167\n\n### Patch Changes\n\n- [#4145](https://github.com/bluesky-social/atproto/pull/4145) [`0f2944305`](https://github.com/bluesky-social/atproto/commit/0f2944305f8479ee51c5ae5a418ee8d67cabcaaf) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Expose AppView DID in dev-env\n\n## 0.3.166\n\n### Patch Changes\n\n- Updated dependencies [[`e1967c1c2`](https://github.com/bluesky-social/atproto/commit/e1967c1c2abb03bb84de424d01042c40a599b5b3), [`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59), [`c0126f4a8`](https://github.com/bluesky-social/atproto/commit/c0126f4a84940826a1d7511802cdc69260ed46df)]:\n  - @atproto/pds@0.4.171\n  - @atproto/lexicon@0.4.14\n  - @atproto/bsky@0.0.179\n  - @atproto/ozone@0.1.137\n  - @atproto/api@0.16.4\n  - @atproto/sync@0.1.32\n  - @atproto/xrpc-server@0.9.3\n\n## 0.3.165\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12), [`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/sync@0.1.31\n  - @atproto/ozone@0.1.136\n  - @atproto/api@0.16.3\n  - @atproto/pds@0.4.170\n  - @atproto/lexicon@0.4.13\n  - @atproto/bsky@0.0.178\n  - @atproto/xrpc-server@0.9.2\n\n## 0.3.164\n\n### Patch Changes\n\n- Updated dependencies [[`369a20116`](https://github.com/bluesky-social/atproto/commit/369a2011615bd98a2a45c8600be45228d857a524), [`369a20116`](https://github.com/bluesky-social/atproto/commit/369a2011615bd98a2a45c8600be45228d857a524), [`75162ffb9`](https://github.com/bluesky-social/atproto/commit/75162ffb9e35bf56b3f3cb19a12ebd495bdc0af8)]:\n  - @atproto/pds@0.4.169\n  - @atproto/bsky@0.0.177\n  - @atproto/ozone@0.1.135\n\n## 0.3.163\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.168\n  - @atproto/bsky@0.0.177\n  - @atproto/ozone@0.1.135\n\n## 0.3.162\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.167\n  - @atproto/bsky@0.0.177\n  - @atproto/ozone@0.1.135\n\n## 0.3.161\n\n### Patch Changes\n\n- Updated dependencies [[`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add), [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add)]:\n  - @atproto/pds@0.4.166\n  - @atproto/bsky@0.0.177\n  - @atproto/ozone@0.1.135\n\n## 0.3.160\n\n### Patch Changes\n\n- Updated dependencies [[`c370d933b`](https://github.com/bluesky-social/atproto/commit/c370d933b76b4e15b83a82b40d1b6a32bd54add6)]:\n  - @atproto/api@0.16.2\n  - @atproto/bsky@0.0.177\n  - @atproto/ozone@0.1.135\n  - @atproto/pds@0.4.165\n\n## 0.3.159\n\n### Patch Changes\n\n- Updated dependencies [[`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671)]:\n  - @atproto/xrpc-server@0.9.1\n  - @atproto/ozone@0.1.134\n  - @atproto/bsky@0.0.176\n  - @atproto/api@0.16.1\n  - @atproto/pds@0.4.164\n  - @atproto/sync@0.1.30\n\n## 0.3.158\n\n### Patch Changes\n\n- Updated dependencies [[`9751eebd7`](https://github.com/bluesky-social/atproto/commit/9751eebd718066984a91046b63e410caecd64022)]:\n  - @atproto/api@0.16.0\n  - @atproto/bsky@0.0.175\n  - @atproto/ozone@0.1.133\n  - @atproto/pds@0.4.163\n\n## 0.3.157\n\n### Patch Changes\n\n- Updated dependencies [[`8787fd9de`](https://github.com/bluesky-social/atproto/commit/8787fd9dea769716412c9883e355cd496664bc6e), [`dc84906c8`](https://github.com/bluesky-social/atproto/commit/dc84906c865e8a97939a909dd3f75decde538363)]:\n  - @atproto/bsky@0.0.174\n  - @atproto/api@0.15.27\n  - @atproto/pds@0.4.162\n  - @atproto/ozone@0.1.132\n\n## 0.3.156\n\n### Patch Changes\n\n- Updated dependencies [[`77c6dffd0`](https://github.com/bluesky-social/atproto/commit/77c6dffd0b3577a0fbdc5f0975c9eeb2a46b55c9)]:\n  - @atproto/bsky@0.0.173\n  - @atproto/pds@0.4.161\n\n## 0.3.155\n\n### Patch Changes\n\n- [#4048](https://github.com/bluesky-social/atproto/pull/4048) [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e) Thanks [@foysalit](https://github.com/foysalit)! - Add externalId to ozone events for deduping events per subject and event type\n\n- Updated dependencies [[`2aecd2b29`](https://github.com/bluesky-social/atproto/commit/2aecd2b290849bf8fbef223464862732cc04d139), [`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82), [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e), [`5ae998797`](https://github.com/bluesky-social/atproto/commit/5ae9987972b7ab8f9d6740886ed56b552d5664dd)]:\n  - @atproto/bsky@0.0.172\n  - @atproto/ozone@0.1.131\n  - @atproto/api@0.15.26\n  - @atproto/pds@0.4.161\n\n## 0.3.154\n\n### Patch Changes\n\n- Updated dependencies [[`88c136427`](https://github.com/bluesky-social/atproto/commit/88c136427451a20d21812a1aa88a70cf21904138)]:\n  - @atproto/api@0.15.25\n  - @atproto/bsky@0.0.171\n  - @atproto/ozone@0.1.130\n  - @atproto/pds@0.4.160\n\n## 0.3.153\n\n### Patch Changes\n\n- Updated dependencies [[`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1), [`ad18fc171`](https://github.com/bluesky-social/atproto/commit/ad18fc171e5d6acfb29694352a101f577689e0ad), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/xrpc-server@0.9.0\n  - @atproto/api@0.15.24\n  - @atproto/ozone@0.1.129\n  - @atproto/pds@0.4.159\n  - @atproto/lexicon@0.4.12\n  - @atproto/bsky@0.0.170\n  - @atproto/sync@0.1.29\n\n## 0.3.152\n\n### Patch Changes\n\n- Updated dependencies [[`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8)]:\n  - @atproto/ozone@0.1.128\n  - @atproto/bsky@0.0.169\n  - @atproto/api@0.15.23\n  - @atproto/pds@0.4.158\n\n## 0.3.151\n\n### Patch Changes\n\n- Updated dependencies [[`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b)]:\n  - @atproto/ozone@0.1.127\n  - @atproto/api@0.15.22\n  - @atproto/pds@0.4.157\n  - @atproto/bsky@0.0.168\n\n## 0.3.150\n\n### Patch Changes\n\n- Updated dependencies [[`d344723a1`](https://github.com/bluesky-social/atproto/commit/d344723a1018b2436b5453526397936bd587a2e2)]:\n  - @atproto/api@0.15.21\n  - @atproto/bsky@0.0.167\n  - @atproto/ozone@0.1.126\n  - @atproto/pds@0.4.156\n\n## 0.3.149\n\n### Patch Changes\n\n- Updated dependencies [[`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7)]:\n  - @atproto/ozone@0.1.125\n  - @atproto/bsky@0.0.166\n  - @atproto/api@0.15.20\n  - @atproto/pds@0.4.155\n\n## 0.3.148\n\n### Patch Changes\n\n- Updated dependencies [[`376778a92`](https://github.com/bluesky-social/atproto/commit/376778a92f08fb6709c4cde736bfaca7393a72e1)]:\n  - @atproto/api@0.15.19\n  - @atproto/bsky@0.0.165\n  - @atproto/ozone@0.1.124\n  - @atproto/pds@0.4.154\n\n## 0.3.147\n\n### Patch Changes\n\n- Updated dependencies [[`e3e31b2b9`](https://github.com/bluesky-social/atproto/commit/e3e31b2b9bf8c4de6b2d7fa992c3b3795686ea72)]:\n  - @atproto/api@0.15.18\n  - @atproto/bsky@0.0.164\n  - @atproto/ozone@0.1.123\n  - @atproto/pds@0.4.153\n\n## 0.3.146\n\n### Patch Changes\n\n- Updated dependencies [[`f792b9193`](https://github.com/bluesky-social/atproto/commit/f792b919386341d0dc4dcf873506f088af61ae16), [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b)]:\n  - @atproto/pds@0.4.152\n  - @atproto/ozone@0.1.122\n  - @atproto/bsky@0.0.163\n  - @atproto/api@0.15.17\n  - @atproto/sync@0.1.28\n\n## 0.3.145\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.151\n  - @atproto/bsky@0.0.162\n  - @atproto/sync@0.1.27\n  - @atproto/ozone@0.1.121\n\n## 0.3.144\n\n### Patch Changes\n\n- Updated dependencies [[`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef)]:\n  - @atproto/ozone@0.1.121\n  - @atproto/bsky@0.0.161\n  - @atproto/api@0.15.16\n  - @atproto/pds@0.4.150\n\n## 0.3.143\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.149\n  - @atproto/bsky@0.0.160\n  - @atproto/sync@0.1.26\n  - @atproto/ozone@0.1.120\n\n## 0.3.142\n\n### Patch Changes\n\n- Updated dependencies [[`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80), [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80)]:\n  - @atproto/api@0.15.15\n  - @atproto/pds@0.4.148\n  - @atproto/bsky@0.0.159\n  - @atproto/ozone@0.1.120\n\n## 0.3.141\n\n### Patch Changes\n\n- Updated dependencies [[`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c)]:\n  - @atproto/xrpc-server@0.8.0\n  - @atproto/pds@0.4.147\n  - @atproto/bsky@0.0.158\n  - @atproto/ozone@0.1.119\n  - @atproto/sync@0.1.25\n\n## 0.3.140\n\n### Patch Changes\n\n- Updated dependencies [[`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1)]:\n  - @atproto/ozone@0.1.118\n  - @atproto/bsky@0.0.157\n  - @atproto/api@0.15.14\n  - @atproto/pds@0.4.146\n\n## 0.3.139\n\n### Patch Changes\n\n- Updated dependencies [[`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d), [`598fcb693`](https://github.com/bluesky-social/atproto/commit/598fcb693d154fe4222f84a3ad24ed3d0b19c58d)]:\n  - @atproto/bsky@0.0.156\n  - @atproto/api@0.15.13\n  - @atproto/pds@0.4.145\n  - @atproto/ozone@0.1.117\n\n## 0.3.138\n\n### Patch Changes\n\n- Updated dependencies [[`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f), [`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05)]:\n  - @atproto/xrpc-server@0.7.19\n  - @atproto/pds@0.4.144\n  - @atproto/bsky@0.0.155\n  - @atproto/ozone@0.1.116\n  - @atproto/sync@0.1.24\n\n## 0.3.137\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.143\n  - @atproto/bsky@0.0.154\n  - @atproto/ozone@0.1.115\n\n## 0.3.136\n\n### Patch Changes\n\n- Updated dependencies [[`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187)]:\n  - @atproto/bsync@0.0.20\n  - @atproto/bsky@0.0.154\n  - @atproto/api@0.15.12\n  - @atproto/pds@0.4.142\n  - @atproto/ozone@0.1.115\n\n## 0.3.135\n\n### Patch Changes\n\n- Updated dependencies [[`a978681fd`](https://github.com/bluesky-social/atproto/commit/a978681fde1c138a5298bae77e5dc36ce155f955), [`06bf684a4`](https://github.com/bluesky-social/atproto/commit/06bf684a4a3fd2b8c73d2729e4951cedca8cba5e)]:\n  - @atproto/api@0.15.11\n  - @atproto/pds@0.4.141\n  - @atproto/bsky@0.0.153\n  - @atproto/ozone@0.1.114\n\n## 0.3.134\n\n### Patch Changes\n\n- Updated dependencies [[`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9), [`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9)]:\n  - @atproto/api@0.15.10\n  - @atproto/bsky@0.0.152\n  - @atproto/ozone@0.1.113\n  - @atproto/pds@0.4.140\n\n## 0.3.133\n\n### Patch Changes\n\n- [#3882](https://github.com/bluesky-social/atproto/pull/3882) [`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159) Thanks [@mozzius](https://github.com/mozzius)! - add a \"via\" field to reposts and likes allowing a reference a repost, and then give a notification when a repost is liked or reposted.\n\n- Updated dependencies [[`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159)]:\n  - @atproto/ozone@0.1.112\n  - @atproto/bsky@0.0.151\n  - @atproto/api@0.15.9\n  - @atproto/pds@0.4.139\n\n## 0.3.132\n\n### Patch Changes\n\n- Updated dependencies [[`80f402f36`](https://github.com/bluesky-social/atproto/commit/80f402f3663af08fd048300738d04c67aa2b9cb8), [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337), [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337), [`eab7c9fb8`](https://github.com/bluesky-social/atproto/commit/eab7c9fb8a9fed4017455ea06666c919aea61336)]:\n  - @atproto/bsky@0.0.150\n  - @atproto/api@0.15.8\n  - @atproto/pds@0.4.138\n  - @atproto/ozone@0.1.111\n\n## 0.3.131\n\n### Patch Changes\n\n- Updated dependencies [[`86b315388`](https://github.com/bluesky-social/atproto/commit/86b3153884099ceeb0cfdb9d2bfdd447c39fb35a), [`efc64ba92`](https://github.com/bluesky-social/atproto/commit/efc64ba92511933c2100b45c0e7f4bcae9199240)]:\n  - @atproto/api@0.15.7\n  - @atproto/pds@0.4.137\n  - @atproto/bsky@0.0.149\n  - @atproto/ozone@0.1.110\n\n## 0.3.130\n\n### Patch Changes\n\n- Updated dependencies [[`088d06204`](https://github.com/bluesky-social/atproto/commit/088d06204f779412b94ae3363ff548a6c8d1299a)]:\n  - @atproto/pds@0.4.136\n  - @atproto/bsky@0.0.148\n  - @atproto/ozone@0.1.109\n\n## 0.3.129\n\n### Patch Changes\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`ab4e72084`](https://github.com/bluesky-social/atproto/commit/ab4e72084dd0ea1eb12b45cbb913595434b88675), [`3301a2697`](https://github.com/bluesky-social/atproto/commit/3301a2697f2ad32d4912ba4781c515755cf1386e), [`eccbce278`](https://github.com/bluesky-social/atproto/commit/eccbce278da72c3fbbf8fbbcfcafe76ae28dcd6c), [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba), [`3a65b68f7`](https://github.com/bluesky-social/atproto/commit/3a65b68f7dc63c8bfbea0ae615f8ae984272f2e4)]:\n  - @atproto/common-web@0.4.2\n  - @atproto/bsky@0.0.148\n  - @atproto/pds@0.4.135\n  - @atproto/ozone@0.1.109\n  - @atproto/lexicon@0.4.11\n  - @atproto/xrpc-server@0.7.18\n  - @atproto/api@0.15.6\n  - @atproto/identity@0.4.8\n  - @atproto/sync@0.1.23\n  - @atproto/bsync@0.0.19\n  - @atproto/crypto@0.4.4\n\n## 0.3.128\n\n### Patch Changes\n\n- [#3765](https://github.com/bluesky-social/atproto/pull/3765) [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9) Thanks [@foysalit](https://github.com/foysalit)! - Add verification lexicons to ozone\n\n- Updated dependencies [[`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9), [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9)]:\n  - @atproto/ozone@0.1.108\n  - @atproto/api@0.15.5\n  - @atproto/pds@0.4.134\n  - @atproto/xrpc-server@0.7.17\n  - @atproto/bsky@0.0.147\n  - @atproto/sync@0.1.22\n\n## 0.3.127\n\n### Patch Changes\n\n- Updated dependencies [[`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c), [`7af77f3ed`](https://github.com/bluesky-social/atproto/commit/7af77f3edfe52f77729f61de4188e8375f03b4ef)]:\n  - @atproto/xrpc-server@0.7.16\n  - @atproto/ozone@0.1.107\n  - @atproto/api@0.15.4\n  - @atproto/bsky@0.0.146\n  - @atproto/pds@0.4.133\n  - @atproto/sync@0.1.21\n\n## 0.3.126\n\n### Patch Changes\n\n- Updated dependencies [[`9ef52d829`](https://github.com/bluesky-social/atproto/commit/9ef52d82923c9c82a73f39690182bd7f75bbc67a)]:\n  - @atproto/bsky@0.0.145\n  - @atproto/pds@0.4.132\n\n## 0.3.125\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.132\n  - @atproto/bsky@0.0.144\n  - @atproto/ozone@0.1.106\n\n## 0.3.124\n\n### Patch Changes\n\n- [#3773](https://github.com/bluesky-social/atproto/pull/3773) [`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add verification notifications\n\n- Updated dependencies [[`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9)]:\n  - @atproto/bsky@0.0.144\n  - @atproto/api@0.15.3\n  - @atproto/pds@0.4.131\n  - @atproto/ozone@0.1.106\n\n## 0.3.123\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.130\n  - @atproto/bsky@0.0.143\n  - @atproto/ozone@0.1.105\n\n## 0.3.122\n\n### Patch Changes\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use white as primary contrast color\n\n- Updated dependencies [[`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`6db9faa4a`](https://github.com/bluesky-social/atproto/commit/6db9faa4a121b573fa73e29e41dd1c183543f7f8)]:\n  - @atproto/pds@0.4.129\n  - @atproto/bsky@0.0.143\n  - @atproto/ozone@0.1.105\n\n## 0.3.121\n\n### Patch Changes\n\n- Updated dependencies [[`553c988f1`](https://github.com/bluesky-social/atproto/commit/553c988f1d226b3d2fbe94c117b088f5c82db794)]:\n  - @atproto/api@0.15.2\n  - @atproto/bsky@0.0.143\n  - @atproto/ozone@0.1.105\n  - @atproto/pds@0.4.128\n\n## 0.3.120\n\n### Patch Changes\n\n- Updated dependencies [[`688268b6a`](https://github.com/bluesky-social/atproto/commit/688268b6a5ee30f0922ee152ffbd26583d164ae4), [`8d99915ce`](https://github.com/bluesky-social/atproto/commit/8d99915ce02c73b9b37bf121ccd2703fa14a906a)]:\n  - @atproto/api@0.15.1\n  - @atproto/pds@0.4.127\n  - @atproto/bsky@0.0.142\n  - @atproto/ozone@0.1.104\n\n## 0.3.119\n\n### Patch Changes\n\n- Updated dependencies [[`23462184d`](https://github.com/bluesky-social/atproto/commit/23462184dc941ba2fc3b4d054985a53715585020)]:\n  - @atproto/api@0.15.0\n  - @atproto/bsky@0.0.141\n  - @atproto/ozone@0.1.103\n  - @atproto/pds@0.4.126\n\n## 0.3.118\n\n### Patch Changes\n\n- Updated dependencies [[`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f), [`0759f0fee`](https://github.com/bluesky-social/atproto/commit/0759f0feeded73cfc15d4eb4231bd74354076ea4)]:\n  - @atproto/ozone@0.1.102\n  - @atproto/bsky@0.0.140\n  - @atproto/pds@0.4.125\n\n## 0.3.117\n\n### Patch Changes\n\n- Updated dependencies [[`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e)]:\n  - @atproto/pds@0.4.124\n  - @atproto/bsky@0.0.139\n  - @atproto/ozone@0.1.101\n\n## 0.3.116\n\n### Patch Changes\n\n- Updated dependencies [[`fc61662d7`](https://github.com/bluesky-social/atproto/commit/fc61662d7b88597f78383e37ee54264a8bb4b670), [`ca07871c4`](https://github.com/bluesky-social/atproto/commit/ca07871c487abc99fe7b7f8671aa8d98eb5dc4bb)]:\n  - @atproto/api@0.14.22\n  - @atproto/bsky@0.0.139\n  - @atproto/ozone@0.1.101\n  - @atproto/pds@0.4.123\n\n## 0.3.115\n\n### Patch Changes\n\n- Updated dependencies [[`8b7bf7e8f`](https://github.com/bluesky-social/atproto/commit/8b7bf7e8f0e5447c68633a87a2a3cff99f9e7e1c)]:\n  - @atproto/bsky@0.0.138\n  - @atproto/api@0.14.21\n  - @atproto/pds@0.4.122\n  - @atproto/ozone@0.1.100\n\n## 0.3.114\n\n### Patch Changes\n\n- Updated dependencies [[`0e681d303`](https://github.com/bluesky-social/atproto/commit/0e681d3036fd0b35c6d2198638392051b2ce4c81)]:\n  - @atproto/bsky@0.0.137\n  - @atproto/api@0.14.20\n  - @atproto/pds@0.4.121\n  - @atproto/ozone@0.1.99\n\n## 0.3.113\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144), [`efb302db1`](https://github.com/bluesky-social/atproto/commit/efb302db1a615b68795c725a22489dbd0400e011)]:\n  - @atproto/common-web@0.4.1\n  - @atproto/bsky@0.0.136\n  - @atproto/api@0.14.19\n  - @atproto/identity@0.4.7\n  - @atproto/lexicon@0.4.10\n  - @atproto/pds@0.4.120\n  - @atproto/sync@0.1.20\n  - @atproto/ozone@0.1.98\n  - @atproto/bsync@0.0.18\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.7.15\n\n## 0.3.112\n\n### Patch Changes\n\n- Updated dependencies [[`04b6230cd`](https://github.com/bluesky-social/atproto/commit/04b6230cd2fbfe4a06cb00ab8ccb8e6c87c6c546)]:\n  - @atproto/bsky@0.0.135\n  - @atproto/api@0.14.18\n  - @atproto/pds@0.4.119\n  - @atproto/ozone@0.1.97\n\n## 0.3.111\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`2b7efb6cb`](https://github.com/bluesky-social/atproto/commit/2b7efb6cb1c93a108570efdafe9d9ec3f1018dfa), [`b0a0f1484`](https://github.com/bluesky-social/atproto/commit/b0a0f1484378adeb5e2aa20b9b6ff2c2eca0f740), [`0eea698be`](https://github.com/bluesky-social/atproto/commit/0eea698bef76520ae4cc0e1f2efbb588a0459556), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/api@0.14.17\n  - @atproto/bsky@0.0.134\n  - @atproto/pds@0.4.118\n  - @atproto/ozone@0.1.96\n  - @atproto/sync@0.1.19\n  - @atproto/bsync@0.0.17\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.7.14\n\n## 0.3.110\n\n### Patch Changes\n\n- Updated dependencies [[`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c)]:\n  - @atproto/ozone@0.1.95\n  - @atproto/bsky@0.0.133\n  - @atproto/api@0.14.16\n  - @atproto/pds@0.4.117\n\n## 0.3.109\n\n### Patch Changes\n\n- Updated dependencies [[`b4ab5011b`](https://github.com/bluesky-social/atproto/commit/b4ab5011bcc64f9f05122a8773806af8e0c13146)]:\n  - @atproto/api@0.14.15\n  - @atproto/pds@0.4.116\n  - @atproto/bsky@0.0.132\n  - @atproto/ozone@0.1.94\n\n## 0.3.108\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.115\n  - @atproto/bsky@0.0.131\n  - @atproto/ozone@0.1.93\n\n## 0.3.107\n\n### Patch Changes\n\n- Updated dependencies [[`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5)]:\n  - @atproto/ozone@0.1.93\n  - @atproto/bsky@0.0.131\n  - @atproto/api@0.14.14\n  - @atproto/pds@0.4.114\n\n## 0.3.106\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.113\n  - @atproto/bsky@0.0.130\n  - @atproto/ozone@0.1.92\n\n## 0.3.105\n\n### Patch Changes\n\n- Updated dependencies [[`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd)]:\n  - @atproto/ozone@0.1.92\n  - @atproto/api@0.14.13\n  - @atproto/pds@0.4.112\n  - @atproto/bsky@0.0.130\n\n## 0.3.104\n\n### Patch Changes\n\n- Updated dependencies [[`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0)]:\n  - @atproto/ozone@0.1.91\n  - @atproto/bsky@0.0.129\n  - @atproto/api@0.14.12\n  - @atproto/pds@0.4.111\n\n## 0.3.103\n\n### Patch Changes\n\n- Updated dependencies [[`d87ffc7bf`](https://github.com/bluesky-social/atproto/commit/d87ffc7bfe3c1e792dc84a320544eb2e053d61ce)]:\n  - @atproto/api@0.14.11\n  - @atproto/bsky@0.0.128\n  - @atproto/ozone@0.1.90\n  - @atproto/pds@0.4.110\n\n## 0.3.102\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.109\n  - @atproto/bsky@0.0.127\n  - @atproto/ozone@0.1.89\n\n## 0.3.101\n\n### Patch Changes\n\n- Updated dependencies [[`03fc0aa27`](https://github.com/bluesky-social/atproto/commit/03fc0aa270884523719e67bea701ef19e2dd5696), [`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/pds@0.4.108\n  - @atproto/syntax@0.4.0\n  - @atproto/bsky@0.0.127\n  - @atproto/ozone@0.1.89\n  - @atproto/api@0.14.10\n  - @atproto/bsync@0.0.16\n  - @atproto/lexicon@0.4.9\n  - @atproto/sync@0.1.18\n  - @atproto/xrpc-server@0.7.13\n\n## 0.3.100\n\n### Patch Changes\n\n- Updated dependencies [[`0ae7f416e`](https://github.com/bluesky-social/atproto/commit/0ae7f416e8055fe4d05b283449e44457161f6a93)]:\n  - @atproto/pds@0.4.107\n  - @atproto/api@0.14.9\n  - @atproto/bsky@0.0.126\n  - @atproto/ozone@0.1.88\n\n## 0.3.99\n\n### Patch Changes\n\n- [#3587](https://github.com/bluesky-social/atproto/pull/3587) [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952) Thanks [@foysalit](https://github.com/foysalit)! - Add searchable handle and displayName to ozone team members\n\n- Updated dependencies [[`b20907a70`](https://github.com/bluesky-social/atproto/commit/b20907a7056970ab627e6c661882cb16491801e2), [`d96b03956`](https://github.com/bluesky-social/atproto/commit/d96b03956d5c26c238f586c6bdf257c080f12746), [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952), [`eab9c003f`](https://github.com/bluesky-social/atproto/commit/eab9c003f838d43f0135ded9d3ede3f449997597)]:\n  - @atproto/sync@0.1.17\n  - @atproto/ozone@0.1.88\n  - @atproto/api@0.14.9\n  - @atproto/pds@0.4.106\n  - @atproto/bsky@0.0.126\n\n## 0.3.98\n\n### Patch Changes\n\n- Updated dependencies [[`38320191e`](https://github.com/bluesky-social/atproto/commit/38320191e559f8b928c6e951a9b4a6207240bfc1), [`6bcbb6d8c`](https://github.com/bluesky-social/atproto/commit/6bcbb6d8cd3696280935ff7892d8e191fd21fa49), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`dc6e4ecb0`](https://github.com/bluesky-social/atproto/commit/dc6e4ecb0e09bbf4bc7a79c6ac43fb6da4166200), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/api@0.14.8\n  - @atproto/pds@0.4.105\n  - @atproto/syntax@0.3.4\n  - @atproto/bsky@0.0.125\n  - @atproto/ozone@0.1.87\n  - @atproto/bsync@0.0.15\n  - @atproto/lexicon@0.4.8\n  - @atproto/sync@0.1.16\n  - @atproto/xrpc-server@0.7.12\n\n## 0.3.97\n\n### Patch Changes\n\n- Updated dependencies [[`d4e14b7bd`](https://github.com/bluesky-social/atproto/commit/d4e14b7bdc7752476757ecfe96343d146411b784), [`99e2809ca`](https://github.com/bluesky-social/atproto/commit/99e2809ca2ebf70acaa10254f140a8dd0fad4305), [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492), [`27b0a7be1`](https://github.com/bluesky-social/atproto/commit/27b0a7be1ed1b6e098114791d84ec9dc844db552), [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492), [`5cce76670`](https://github.com/bluesky-social/atproto/commit/5cce7667058981561340107e0124093203e796e3)]:\n  - @atproto/pds@0.4.104\n  - @atproto/api@0.14.7\n  - @atproto/bsky@0.0.124\n  - @atproto/ozone@0.1.86\n\n## 0.3.96\n\n### Patch Changes\n\n- Updated dependencies [[`82d5a2d36`](https://github.com/bluesky-social/atproto/commit/82d5a2d3617c40caab7a18e46c709c4b3c48e7f8)]:\n  - @atproto/pds@0.4.103\n  - @atproto/bsky@0.0.123\n  - @atproto/ozone@0.1.85\n\n## 0.3.95\n\n### Patch Changes\n\n- Updated dependencies [[`44f81f2eb`](https://github.com/bluesky-social/atproto/commit/44f81f2eb9229e21aec4472b3a05e855396dbec5)]:\n  - @atproto/api@0.14.6\n  - @atproto/bsky@0.0.123\n  - @atproto/ozone@0.1.85\n  - @atproto/pds@0.4.102\n\n## 0.3.94\n\n### Patch Changes\n\n- Updated dependencies [[`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f), [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a), [`6e382f67a`](https://github.com/bluesky-social/atproto/commit/6e382f67aa73532efadfea80ff96a27b526cb178)]:\n  - @atproto/api@0.14.5\n  - @atproto/pds@0.4.101\n  - @atproto/ozone@0.1.84\n  - @atproto/bsky@0.0.122\n  - @atproto/sync@0.1.15\n\n## 0.3.93\n\n### Patch Changes\n\n- Updated dependencies [[`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c)]:\n  - @atproto/ozone@0.1.83\n  - @atproto/api@0.14.4\n  - @atproto/pds@0.4.100\n  - @atproto/bsky@0.0.121\n\n## 0.3.92\n\n### Patch Changes\n\n- Updated dependencies [[`22af31a89`](https://github.com/bluesky-social/atproto/commit/22af31a898476c5e317aea263af366bddda120d6), [`01874c4be`](https://github.com/bluesky-social/atproto/commit/01874c4be73a41ffb8fe28378f674949aa2c938f)]:\n  - @atproto/api@0.14.3\n  - @atproto/bsky@0.0.120\n  - @atproto/ozone@0.1.82\n  - @atproto/pds@0.4.99\n\n## 0.3.91\n\n### Patch Changes\n\n- Updated dependencies [[`7449f8607`](https://github.com/bluesky-social/atproto/commit/7449f8607c1be948a0b55611c21075757c3d7261)]:\n  - @atproto/ozone@0.1.81\n\n## 0.3.90\n\n### Patch Changes\n\n- Updated dependencies [[`010f10c6f`](https://github.com/bluesky-social/atproto/commit/010f10c6f212f699ad42c0349a58bbcf2172e3cc), [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9)]:\n  - @atproto/api@0.14.2\n  - @atproto/ozone@0.1.80\n  - @atproto/pds@0.4.98\n  - @atproto/bsky@0.0.119\n\n## 0.3.89\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.97\n  - @atproto/bsky@0.0.118\n  - @atproto/ozone@0.1.79\n\n## 0.3.88\n\n### Patch Changes\n\n- Updated dependencies [[`3f58dd0e7`](https://github.com/bluesky-social/atproto/commit/3f58dd0e742cdfed9c3eae0118bc57f539de78f1), [`20e57bacf`](https://github.com/bluesky-social/atproto/commit/20e57bacf9bb2ae8a118eadbfc291f3213b8dc2f), [`ba5bb6e66`](https://github.com/bluesky-social/atproto/commit/ba5bb6e667fb58bbefd332844957de575e102ca3), [`6ea9c961a`](https://github.com/bluesky-social/atproto/commit/6ea9c961af964cd9b0d00b5073c695c5e0b3345a)]:\n  - @atproto/bsky@0.0.118\n  - @atproto/ozone@0.1.79\n  - @atproto/api@0.14.1\n  - @atproto/pds@0.4.96\n\n## 0.3.87\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/api@0.14.0\n  - @atproto/ozone@0.1.78\n  - @atproto/bsky@0.0.117\n  - @atproto/pds@0.4.95\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n  - @atproto/bsync@0.0.14\n  - @atproto/sync@0.1.14\n  - @atproto/xrpc-server@0.7.11\n\n## 0.3.86\n\n### Patch Changes\n\n- Updated dependencies [[`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153)]:\n  - @atproto/ozone@0.1.77\n  - @atproto/api@0.13.35\n  - @atproto/pds@0.4.94\n  - @atproto/bsky@0.0.116\n\n## 0.3.85\n\n### Patch Changes\n\n- Updated dependencies [[`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486), [`636951e47`](https://github.com/bluesky-social/atproto/commit/636951e4728cd52c2e5355eb93b47d7e869b67e9), [`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486)]:\n  - @atproto/api@0.13.34\n  - @atproto/bsky@0.0.115\n  - @atproto/ozone@0.1.76\n  - @atproto/pds@0.4.93\n\n## 0.3.84\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39), [`53a577fd4`](https://github.com/bluesky-social/atproto/commit/53a577fd4bfb1c7301e14db85b42f4758b053dee), [`87ed907a6`](https://github.com/bluesky-social/atproto/commit/87ed907a6b96b408c02c9af819cec8380a453254)]:\n  - @atproto/xrpc-server@0.7.10\n  - @atproto/common-web@0.4.0\n  - @atproto/identity@0.4.6\n  - @atproto/lexicon@0.4.6\n  - @atproto/crypto@0.4.4\n  - @atproto/syntax@0.3.2\n  - @atproto/bsync@0.0.13\n  - @atproto/ozone@0.1.75\n  - @atproto/bsky@0.0.114\n  - @atproto/sync@0.1.13\n  - @atproto/api@0.13.33\n  - @atproto/pds@0.4.92\n\n## 0.3.83\n\n### Patch Changes\n\n- Updated dependencies [[`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`da7a831a7`](https://github.com/bluesky-social/atproto/commit/da7a831a7318343ba1ee98de3811ba337c043dbd), [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905), [`8810885b8`](https://github.com/bluesky-social/atproto/commit/8810885b8e7fa0377e6c000c091eec1dd85ed261), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`8810885b8`](https://github.com/bluesky-social/atproto/commit/8810885b8e7fa0377e6c000c091eec1dd85ed261), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75)]:\n  - @atproto/xrpc-server@0.7.9\n  - @atproto/ozone@0.1.74\n  - @atproto/api@0.13.32\n  - @atproto/pds@0.4.91\n  - @atproto/bsky@0.0.113\n  - @atproto/sync@0.1.12\n  - @atproto/bsync@0.0.12\n  - @atproto/crypto@0.4.3\n\n## 0.3.82\n\n### Patch Changes\n\n- Updated dependencies [[`8c6c7813a`](https://github.com/bluesky-social/atproto/commit/8c6c7813a9c2110c8fe21acdca8f09554a1983ce)]:\n  - @atproto/api@0.13.31\n  - @atproto/pds@0.4.90\n  - @atproto/bsky@0.0.112\n  - @atproto/ozone@0.1.73\n\n## 0.3.81\n\n### Patch Changes\n\n- Updated dependencies [[`1ada2d093`](https://github.com/bluesky-social/atproto/commit/1ada2d093427e45b6d59a16cf146bf5282560c7b), [`e6e6aea38`](https://github.com/bluesky-social/atproto/commit/e6e6aea3814e3d0bb42a537f80d77947e85fa73f), [`c0a75d310`](https://github.com/bluesky-social/atproto/commit/c0a75d310aa92c067799a97d1acc5bd0543114c5)]:\n  - @atproto/ozone@0.1.72\n  - @atproto/api@0.13.30\n  - @atproto/bsky@0.0.111\n  - @atproto/pds@0.4.89\n\n## 0.3.80\n\n### Patch Changes\n\n- Updated dependencies [[`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12)]:\n  - @atproto/xrpc-server@0.7.8\n  - @atproto/bsky@0.0.110\n  - @atproto/ozone@0.1.71\n  - @atproto/pds@0.4.88\n  - @atproto/sync@0.1.11\n\n## 0.3.79\n\n### Patch Changes\n\n- Updated dependencies [[`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6), [`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6)]:\n  - @atproto/ozone@0.1.70\n  - @atproto/api@0.13.29\n  - @atproto/pds@0.4.87\n  - @atproto/bsky@0.0.109\n\n## 0.3.78\n\n### Patch Changes\n\n- Updated dependencies [[`cbf17066f`](https://github.com/bluesky-social/atproto/commit/cbf17066f314fbc7f2e943127ee4a9f589f8bec2)]:\n  - @atproto/bsky@0.0.108\n  - @atproto/api@0.13.28\n  - @atproto/pds@0.4.86\n  - @atproto/ozone@0.1.69\n\n## 0.3.77\n\n### Patch Changes\n\n- Updated dependencies [[`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61)]:\n  - @atproto/xrpc-server@0.7.7\n  - @atproto/bsky@0.0.107\n  - @atproto/ozone@0.1.68\n  - @atproto/pds@0.4.85\n  - @atproto/sync@0.1.10\n\n## 0.3.76\n\n### Patch Changes\n\n- [#3344](https://github.com/bluesky-social/atproto/pull/3344) [`48a0e9d60`](https://github.com/bluesky-social/atproto/commit/48a0e9d6060c2dc93899f13f2fc7cc76c04fbcd9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly dispose of unused http responses\n\n- Updated dependencies [[`48a0e9d60`](https://github.com/bluesky-social/atproto/commit/48a0e9d6060c2dc93899f13f2fc7cc76c04fbcd9), [`e277158f7`](https://github.com/bluesky-social/atproto/commit/e277158f70a831b04fde3ec84b3c1eaa6ce82e9d)]:\n  - @atproto/ozone@0.1.67\n  - @atproto/api@0.13.27\n  - @atproto/bsky@0.0.106\n  - @atproto/pds@0.4.84\n\n## 0.3.75\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n  - @atproto/bsky@0.0.105\n  - @atproto/identity@0.4.5\n  - @atproto/ozone@0.1.66\n  - @atproto/pds@0.4.83\n  - @atproto/xrpc-server@0.7.6\n  - @atproto/sync@0.1.9\n\n## 0.3.74\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on Axios\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/bsky@0.0.104\n  - @atproto/common-web@0.3.2\n  - @atproto/identity@0.4.4\n  - @atproto/ozone@0.1.65\n  - @atproto/pds@0.4.82\n  - @atproto/api@0.13.26\n  - @atproto/lexicon@0.4.5\n  - @atproto/sync@0.1.8\n  - @atproto/bsync@0.0.11\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.5\n\n## 0.3.73\n\n### Patch Changes\n\n- [#3271](https://github.com/bluesky-social/atproto/pull/3271) [`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a) Thanks [@foysalit](https://github.com/foysalit)! - Allow setting policy names with takedown actions and when querying events\n\n- Updated dependencies [[`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a)]:\n  - @atproto/ozone@0.1.64\n  - @atproto/api@0.13.25\n  - @atproto/bsky@0.0.103\n  - @atproto/pds@0.4.81\n\n## 0.3.72\n\n### Patch Changes\n\n- Updated dependencies [[`d90d999de`](https://github.com/bluesky-social/atproto/commit/d90d999defda01a9b04dbce129e254990062c283), [`6d1ad3783`](https://github.com/bluesky-social/atproto/commit/6d1ad37836f275e03bc115e944a3195b82f3398d)]:\n  - @atproto/ozone@0.1.63\n  - @atproto/api@0.13.24\n  - @atproto/bsky@0.0.102\n  - @atproto/pds@0.4.80\n\n## 0.3.71\n\n### Patch Changes\n\n- Updated dependencies [[`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7), [`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7), [`9ea2cce9a`](https://github.com/bluesky-social/atproto/commit/9ea2cce9a4c0a08994a8cb5abc81dc4bc2221d0c), [`b4674a61a`](https://github.com/bluesky-social/atproto/commit/b4674a61a92ca96f89ac06e705e08c2e6af07e1b)]:\n  - @atproto/pds@0.4.79\n  - @atproto/api@0.13.23\n  - @atproto/ozone@0.1.62\n  - @atproto/bsky@0.0.101\n\n## 0.3.70\n\n### Patch Changes\n\n- Updated dependencies [[`f22383cee`](https://github.com/bluesky-social/atproto/commit/f22383cee8feb8b9f761c801ab6e07ad8dc019ed)]:\n  - @atproto/api@0.13.22\n  - @atproto/bsky@0.0.100\n  - @atproto/ozone@0.1.61\n  - @atproto/pds@0.4.78\n\n## 0.3.69\n\n### Patch Changes\n\n- [#3266](https://github.com/bluesky-social/atproto/pull/3266) [`638f5a831`](https://github.com/bluesky-social/atproto/commit/638f5a8312136e8666d9390a73cb273459ffe082) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Fix avatar path resolution\n\n## 0.3.68\n\n### Patch Changes\n\n- Updated dependencies [[`dced566de`](https://github.com/bluesky-social/atproto/commit/dced566de5079ef4208801db476a7e7416f5e5aa)]:\n  - @atproto/api@0.13.21\n  - @atproto/bsky@0.0.99\n  - @atproto/ozone@0.1.60\n  - @atproto/pds@0.4.77\n\n## 0.3.67\n\n### Patch Changes\n\n- Updated dependencies [[`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95), [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34), [`0bec389a1`](https://github.com/bluesky-social/atproto/commit/0bec389a1c53adbcfab7b877df9b291d44d8ea33)]:\n  - @atproto/pds@0.4.76\n  - @atproto/lexicon@0.4.4\n  - @atproto/ozone@0.1.59\n  - @atproto/bsky@0.0.98\n  - @atproto/api@0.13.20\n  - @atproto/sync@0.1.7\n  - @atproto/bsync@0.0.10\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.4\n\n## 0.3.66\n\n### Patch Changes\n\n- Updated dependencies [[`ed2236220`](https://github.com/bluesky-social/atproto/commit/ed2236220900ab9a6132c525289cfdd959733a42)]:\n  - @atproto/ozone@0.1.58\n  - @atproto/api@0.13.19\n  - @atproto/pds@0.4.75\n  - @atproto/bsky@0.0.97\n\n## 0.3.65\n\n### Patch Changes\n\n- Updated dependencies [[`1e367cba2`](https://github.com/bluesky-social/atproto/commit/1e367cba2bd1ff5560c2ec5c2a5d348cd9342b65), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`1e367cba2`](https://github.com/bluesky-social/atproto/commit/1e367cba2bd1ff5560c2ec5c2a5d348cd9342b65)]:\n  - @atproto/pds@0.4.74\n  - @atproto/bsky@0.0.96\n  - @atproto/ozone@0.1.57\n\n## 0.3.64\n\n### Patch Changes\n\n- Updated dependencies [[`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc)]:\n  - @atproto/ozone@0.1.57\n  - @atproto/bsky@0.0.96\n  - @atproto/api@0.13.18\n  - @atproto/pds@0.4.73\n\n## 0.3.63\n\n### Patch Changes\n\n- Updated dependencies [[`a4b528e5f`](https://github.com/bluesky-social/atproto/commit/a4b528e5f51c8bfca56b293b0059b88d138ec421), [`2e7aa211d`](https://github.com/bluesky-social/atproto/commit/2e7aa211d2cbc629899c7f87f1713b13b932750b), [`90399c859`](https://github.com/bluesky-social/atproto/commit/90399c85955301babc689c293bd3e7e1a94505a3)]:\n  - @atproto/api@0.13.17\n  - @atproto/pds@0.4.72\n  - @atproto/bsky@0.0.95\n  - @atproto/ozone@0.1.56\n\n## 0.3.62\n\n### Patch Changes\n\n- Updated dependencies [[`24423fc2d`](https://github.com/bluesky-social/atproto/commit/24423fc2dd394c99a29dbe4419b356090ef19546), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`48d08a469`](https://github.com/bluesky-social/atproto/commit/48d08a469f75837e3b7e879d286d12780440b8b8), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`561431fe4`](https://github.com/bluesky-social/atproto/commit/561431fe4897e81767dc768e9a31020d09bf86ff)]:\n  - @atproto/pds@0.4.71\n  - @atproto/syntax@0.3.1\n  - @atproto/ozone@0.1.55\n  - @atproto/api@0.13.16\n  - @atproto/lexicon@0.4.3\n  - @atproto/bsky@0.0.94\n  - @atproto/bsync@0.0.9\n  - @atproto/sync@0.1.6\n  - @atproto/xrpc-server@0.7.3\n\n## 0.3.61\n\n### Patch Changes\n\n- Updated dependencies [[`d6f33b474`](https://github.com/bluesky-social/atproto/commit/d6f33b4742e0b94722a993efc7d18833d9416bb6), [`b6eeb81c6`](https://github.com/bluesky-social/atproto/commit/b6eeb81c6d454b5ae91b05a21fc1820274c1b429), [`9e18ab6a3`](https://github.com/bluesky-social/atproto/commit/9e18ab6a35f47e0a9cee76221bfa0817c8a624a1), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`9ffeb5216`](https://github.com/bluesky-social/atproto/commit/9ffeb5216ab29919a2c1f3cc18af26c21a077d4a), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`839202a3d`](https://github.com/bluesky-social/atproto/commit/839202a3d2b01de25de900cec7540019545798c6), [`e680d55ca`](https://github.com/bluesky-social/atproto/commit/e680d55ca2d7f6b213e2a8693eba6be39163ba41), [`c4b5e5395`](https://github.com/bluesky-social/atproto/commit/c4b5e53957463c37dd16fdd1b897d4ab02ab8e84), [`9ffeb5216`](https://github.com/bluesky-social/atproto/commit/9ffeb5216ab29919a2c1f3cc18af26c21a077d4a)]:\n  - @atproto/ozone@0.1.54\n  - @atproto/api@0.13.15\n  - @atproto/pds@0.4.70\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.2\n  - @atproto/bsky@0.0.93\n  - @atproto/identity@0.4.3\n  - @atproto/sync@0.1.5\n\n## 0.3.60\n\n### Patch Changes\n\n- Updated dependencies [[`209238769`](https://github.com/bluesky-social/atproto/commit/209238769c0bf38bf04f7fa9621eeb176b5c0ed8), [`73f40e63a`](https://github.com/bluesky-social/atproto/commit/73f40e63abe3283efc0a27eef781c00b497caad1)]:\n  - @atproto/bsky@0.0.92\n  - @atproto/api@0.13.14\n  - @atproto/pds@0.4.69\n  - @atproto/ozone@0.1.53\n\n## 0.3.59\n\n### Patch Changes\n\n- Updated dependencies [[`19e36afb2`](https://github.com/bluesky-social/atproto/commit/19e36afb2c13dbc7b1033eb3cab5e7fc6f496fdc)]:\n  - @atproto/ozone@0.1.52\n  - @atproto/api@0.13.13\n  - @atproto/bsky@0.0.91\n  - @atproto/pds@0.4.68\n\n## 0.3.58\n\n### Patch Changes\n\n- Updated dependencies [[`c1b0e176a`](https://github.com/bluesky-social/atproto/commit/c1b0e176adbc5108bff49d74fbae18de60e86732)]:\n  - @atproto/pds@0.4.67\n  - @atproto/bsky@0.0.90\n  - @atproto/ozone@0.1.51\n\n## 0.3.57\n\n### Patch Changes\n\n- Updated dependencies [[`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`d605577c2`](https://github.com/bluesky-social/atproto/commit/d605577c25d3e69c7cc0a1e858a4f009d1ea3096)]:\n  - @atproto/pds@0.4.66\n  - @atproto/sync@0.1.4\n  - @atproto/bsky@0.0.90\n  - @atproto/ozone@0.1.51\n\n## 0.3.56\n\n### Patch Changes\n\n- Updated dependencies [[`22d039a22`](https://github.com/bluesky-social/atproto/commit/22d039a229e3ef08a793e1c98b473b1b8e18ac5e)]:\n  - @atproto/ozone@0.1.51\n  - @atproto/api@0.13.12\n  - @atproto/bsky@0.0.89\n  - @atproto/pds@0.4.65\n\n## 0.3.55\n\n### Patch Changes\n\n- Updated dependencies [[`72549f442`](https://github.com/bluesky-social/atproto/commit/72549f442223c0c74594e111a9793e39b0c5ea2d), [`08ed0a5a9`](https://github.com/bluesky-social/atproto/commit/08ed0a5a916685b2aaea783706e6d6287a2aa287), [`08ed0a5a9`](https://github.com/bluesky-social/atproto/commit/08ed0a5a916685b2aaea783706e6d6287a2aa287)]:\n  - @atproto/bsky@0.0.88\n  - @atproto/ozone@0.1.50\n  - @atproto/pds@0.4.64\n\n## 0.3.54\n\n### Patch Changes\n\n- Updated dependencies [[`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44)]:\n  - @atproto/api@0.13.11\n  - @atproto/bsky@0.0.87\n  - @atproto/ozone@0.1.49\n  - @atproto/pds@0.4.63\n\n## 0.3.53\n\n### Patch Changes\n\n- Updated dependencies [[`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5)]:\n  - @atproto/ozone@0.1.48\n  - @atproto/api@0.13.10\n  - @atproto/bsky@0.0.86\n  - @atproto/pds@0.4.62\n\n## 0.3.52\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13), [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c)]:\n  - @atproto/pds@0.4.61\n  - @atproto/ozone@0.1.47\n  - @atproto/api@0.13.9\n  - @atproto/bsky@0.0.85\n  - @atproto/bsync@0.0.8\n  - @atproto/crypto@0.4.1\n  - @atproto/sync@0.1.3\n  - @atproto/xrpc-server@0.7.1\n\n## 0.3.51\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93), [`922b94ce3`](https://github.com/bluesky-social/atproto/commit/922b94ce379d861faaa5cf8448b7a44f04e474d3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a06634ae5`](https://github.com/bluesky-social/atproto/commit/a06634ae576217d53ef7ea7f8cbfa9faa8662634), [`b298bfd28`](https://github.com/bluesky-social/atproto/commit/b298bfd280c5de8b38b843fd852e6d2739a776d8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/common-web@0.3.1\n  - @atproto/api@0.13.8\n  - @atproto/pds@0.4.60\n  - @atproto/xrpc-server@0.7.0\n  - @atproto/lexicon@0.4.2\n  - @atproto/bsky@0.0.84\n  - @atproto/ozone@0.1.46\n  - @atproto/identity@0.4.2\n  - @atproto/sync@0.1.2\n  - @atproto/bsync@0.0.7\n  - @atproto/crypto@0.4.1\n\n## 0.3.50\n\n### Patch Changes\n\n- Updated dependencies [[`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7)]:\n  - @atproto/api@0.13.7\n  - @atproto/ozone@0.1.45\n  - @atproto/xrpc-server@0.6.4\n  - @atproto/bsky@0.0.83\n  - @atproto/pds@0.4.59\n  - @atproto/bsync@0.0.6\n  - @atproto/crypto@0.4.1\n  - @atproto/sync@0.1.1\n\n## 0.3.49\n\n### Patch Changes\n\n- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442), [`642c7ae96`](https://github.com/bluesky-social/atproto/commit/642c7ae968b0dd2bfb448aa6eba0c1fd9312d909)]:\n  - @atproto/sync@0.1.0\n  - @atproto/ozone@0.1.44\n  - @atproto/bsky@0.0.82\n  - @atproto/pds@0.4.58\n\n## 0.3.48\n\n### Patch Changes\n\n- Updated dependencies [[`325859b8b`](https://github.com/bluesky-social/atproto/commit/325859b8bff8dcfdd1eb8cabd51bffedb03aad87), [`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d)]:\n  - @atproto/ozone@0.1.43\n  - @atproto/api@0.13.6\n  - @atproto/bsky@0.0.81\n  - @atproto/pds@0.4.57\n\n## 0.3.47\n\n### Patch Changes\n\n- Updated dependencies [[`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14)]:\n  - @atproto/bsky@0.0.80\n  - @atproto/api@0.13.5\n  - @atproto/pds@0.4.56\n  - @atproto/ozone@0.1.42\n\n## 0.3.46\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/pds@0.4.55\n  - @atproto/bsky@0.0.79\n  - @atproto/ozone@0.1.41\n\n## 0.3.45\n\n### Patch Changes\n\n- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f), [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7)]:\n  - @atproto/xrpc-server@0.6.3\n  - @atproto/pds@0.4.54\n  - @atproto/api@0.13.4\n  - @atproto/bsky@0.0.79\n  - @atproto/crypto@0.4.1\n  - @atproto/ozone@0.1.41\n  - @atproto/identity@0.4.1\n\n## 0.3.44\n\n### Patch Changes\n\n- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]:\n  - @atproto/bsky@0.0.78\n  - @atproto/api@0.13.3\n  - @atproto/pds@0.4.53\n  - @atproto/ozone@0.1.40\n\n## 0.3.43\n\n### Patch Changes\n\n- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]:\n  - @atproto/bsky@0.0.77\n  - @atproto/api@0.13.2\n  - @atproto/pds@0.4.52\n  - @atproto/ozone@0.1.39\n\n## 0.3.42\n\n### Patch Changes\n\n- Updated dependencies [[`f9a2f3ed1`](https://github.com/bluesky-social/atproto/commit/f9a2f3ed172ae1a8dc1cca0e893e13eac2e4955d)]:\n  - @atproto/pds@0.4.51\n  - @atproto/bsky@0.0.76\n  - @atproto/ozone@0.1.38\n\n## 0.3.41\n\n### Patch Changes\n\n- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]:\n  - @atproto/xrpc-server@0.6.2\n  - @atproto/pds@0.4.50\n  - @atproto/ozone@0.1.38\n  - @atproto/bsky@0.0.76\n\n## 0.3.40\n\n### Patch Changes\n\n- Updated dependencies [[`22af354a5`](https://github.com/bluesky-social/atproto/commit/22af354a5db595d7cbc0e65f02601de3565337e1)]:\n  - @atproto/api@0.13.1\n  - @atproto/bsky@0.0.75\n  - @atproto/ozone@0.1.37\n  - @atproto/pds@0.4.49\n\n## 0.3.39\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/lexicon@0.4.1\n  - @atproto/api@0.13.0\n  - @atproto/bsky@0.0.74\n  - @atproto/ozone@0.1.36\n  - @atproto/pds@0.4.48\n  - @atproto/xrpc-server@0.6.1\n\n## 0.3.38\n\n### Patch Changes\n\n- Updated dependencies [[`269cbc87c`](https://github.com/bluesky-social/atproto/commit/269cbc87c5ec9d65d1d479269ac5e91dffbb186c)]:\n  - @atproto/pds@0.4.47\n  - @atproto/bsky@0.0.73\n  - @atproto/ozone@0.1.35\n\n## 0.3.37\n\n### Patch Changes\n\n- Updated dependencies [[`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c), [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c)]:\n  - @atproto/api@0.12.29\n  - @atproto/xrpc-server@0.6.0\n  - @atproto/pds@0.4.46\n  - @atproto/bsky@0.0.73\n  - @atproto/ozone@0.1.35\n\n## 0.3.36\n\n### Patch Changes\n\n- Updated dependencies [[`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41)]:\n  - @atproto/ozone@0.1.34\n  - @atproto/bsky@0.0.72\n  - @atproto/api@0.12.28\n  - @atproto/pds@0.4.45\n\n## 0.3.35\n\n### Patch Changes\n\n- Updated dependencies [[`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec)]:\n  - @atproto/ozone@0.1.33\n  - @atproto/bsky@0.0.71\n  - @atproto/api@0.12.27\n  - @atproto/pds@0.4.44\n\n## 0.3.34\n\n### Patch Changes\n\n- Updated dependencies [[`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7)]:\n  - @atproto/ozone@0.1.32\n  - @atproto/bsky@0.0.70\n  - @atproto/api@0.12.26\n  - @atproto/pds@0.4.43\n\n## 0.3.33\n\n### Patch Changes\n\n- Updated dependencies [[`12dcdb668`](https://github.com/bluesky-social/atproto/commit/12dcdb668c8ec0f8a89689c326ab3e9dbc6d2f3c), [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b)]:\n  - @atproto/api@0.12.25\n  - @atproto/bsync@0.0.5\n  - @atproto/bsky@0.0.69\n  - @atproto/ozone@0.1.31\n  - @atproto/pds@0.4.42\n\n## 0.3.32\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Adapt to changes from @atproto/oauth-provider\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/pds@0.4.41\n  - @atproto/ozone@0.1.30\n  - @atproto/bsync@0.0.4\n  - @atproto/bsky@0.0.68\n  - @atproto/crypto@0.4.0\n  - @atproto/xrpc-server@0.5.3\n\n## 0.3.31\n\n### Patch Changes\n\n- Updated dependencies [[`ed5810179`](https://github.com/bluesky-social/atproto/commit/ed5810179006f254f2035fe1f0e3c4798080cfe0), [`0529bec99`](https://github.com/bluesky-social/atproto/commit/0529bec99183439829a3553f45ac7203763144c3), [`2f40203fb`](https://github.com/bluesky-social/atproto/commit/2f40203fb453934aaf5d353b680d89b8a1febd0f)]:\n  - @atproto/api@0.12.24\n  - @atproto/ozone@0.1.29\n  - @atproto/bsky@0.0.67\n  - @atproto/pds@0.4.40\n\n## 0.3.30\n\n### Patch Changes\n\n- Updated dependencies [[`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863)]:\n  - @atproto/ozone@0.1.28\n  - @atproto/bsky@0.0.66\n  - @atproto/api@0.12.23\n  - @atproto/pds@0.4.39\n\n## 0.3.29\n\n### Patch Changes\n\n- [#2553](https://github.com/bluesky-social/atproto/pull/2553) [`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02) Thanks [@devinivy](https://github.com/devinivy)! - Support for starter packs (app.bsky.graph.starterpack)\n\n- Updated dependencies [[`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02), [`615a96ddc`](https://github.com/bluesky-social/atproto/commit/615a96ddc2965251cfab060dfc43fc1a51ef4bff)]:\n  - @atproto/ozone@0.1.27\n  - @atproto/bsky@0.0.65\n  - @atproto/api@0.12.22\n  - @atproto/pds@0.4.38\n  - @atproto/xrpc-server@0.5.2\n\n## 0.3.28\n\n### Patch Changes\n\n- [#2460](https://github.com/bluesky-social/atproto/pull/2460) [`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24) Thanks [@foysalit](https://github.com/foysalit)! - Add DB backed team member management for ozone\n\n- Updated dependencies [[`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24)]:\n  - @atproto/ozone@0.1.26\n  - @atproto/api@0.12.21\n  - @atproto/pds@0.4.37\n  - @atproto/bsky@0.0.64\n\n## 0.3.27\n\n### Patch Changes\n\n- Updated dependencies [[`ea0f10b5d`](https://github.com/bluesky-social/atproto/commit/ea0f10b5d0d334eb587032c54d5ace9ea811cf26)]:\n  - @atproto/api@0.12.20\n  - @atproto/bsky@0.0.63\n  - @atproto/ozone@0.1.25\n  - @atproto/pds@0.4.36\n\n## 0.3.26\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto/pds@0.4.35\n  - @atproto/bsky@0.0.62\n  - @atproto/ozone@0.1.24\n\n## 0.3.25\n\n### Patch Changes\n\n- Updated dependencies [[`7c1973841`](https://github.com/bluesky-social/atproto/commit/7c1973841dab416ae19435d37853aeea1f579d39)]:\n  - @atproto/api@0.12.19\n  - @atproto/bsky@0.0.62\n  - @atproto/ozone@0.1.24\n  - @atproto/pds@0.4.34\n\n## 0.3.24\n\n### Patch Changes\n\n- Updated dependencies [[`58abcbd8b`](https://github.com/bluesky-social/atproto/commit/58abcbd8b6e42a1f66bda6acc3ee6a2c0894e546)]:\n  - @atproto/api@0.12.18\n  - @atproto/bsky@0.0.61\n  - @atproto/ozone@0.1.23\n  - @atproto/pds@0.4.33\n\n## 0.3.23\n\n### Patch Changes\n\n- Updated dependencies [[`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd), [`d8e2fefa9`](https://github.com/bluesky-social/atproto/commit/d8e2fefa98581edb3837e567657aa41a1cdb21f6)]:\n  - @atproto/ozone@0.1.22\n  - @atproto/bsky@0.0.60\n  - @atproto/api@0.12.17\n  - @atproto/pds@0.4.32\n\n## 0.3.22\n\n### Patch Changes\n\n- [#2539](https://github.com/bluesky-social/atproto/pull/2539) [`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46) Thanks [@dholms](https://github.com/dholms)! - Allow updating deactivation state through admin.updateSubjectStatus\n\n- Updated dependencies [[`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46)]:\n  - @atproto/api@0.12.16\n  - @atproto/pds@0.4.31\n  - @atproto/bsky@0.0.59\n  - @atproto/ozone@0.1.21\n\n## 0.3.21\n\n### Patch Changes\n\n- Updated dependencies [[`255d5ea1f`](https://github.com/bluesky-social/atproto/commit/255d5ea1f06726547cdbe59c83bd18f2d4746912)]:\n  - @atproto/api@0.12.15\n  - @atproto/pds@0.4.30\n  - @atproto/bsky@0.0.58\n  - @atproto/ozone@0.1.20\n\n## 0.3.20\n\n### Patch Changes\n\n- Updated dependencies [[`c4af6a409`](https://github.com/bluesky-social/atproto/commit/c4af6a409ea2171c3cf1d0e7c8ed496794a3f049)]:\n  - @atproto/api@0.12.14\n  - @atproto/bsky@0.0.57\n  - @atproto/ozone@0.1.19\n  - @atproto/pds@0.4.29\n\n## 0.3.19\n\n### Patch Changes\n\n- Updated dependencies [[`53551be6c`](https://github.com/bluesky-social/atproto/commit/53551be6cf092a9b4d2e132788b94ac0d4ffcecc)]:\n  - @atproto/ozone@0.1.18\n  - @atproto/bsky@0.0.56\n  - @atproto/pds@0.4.28\n\n## 0.3.18\n\n### Patch Changes\n\n- Updated dependencies [[`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9), [`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9)]:\n  - @atproto/pds@0.4.27\n  - @atproto/api@0.12.13\n  - @atproto/bsky@0.0.55\n  - @atproto/ozone@0.1.17\n\n## 0.3.17\n\n### Patch Changes\n\n- Updated dependencies [[`0cc5ef70f`](https://github.com/bluesky-social/atproto/commit/0cc5ef70f4e5a8e24983051d5f5ad8ee27be8684)]:\n  - @atproto/pds@0.4.26\n  - @atproto/bsky@0.0.54\n  - @atproto/ozone@0.1.16\n\n## 0.3.16\n\n### Patch Changes\n\n- Updated dependencies [[`0e8acb9fb`](https://github.com/bluesky-social/atproto/commit/0e8acb9fbaf3edcebd8e4f8fe4a381ede0206895)]:\n  - @atproto/pds@0.4.25\n  - @atproto/bsky@0.0.54\n  - @atproto/ozone@0.1.16\n\n## 0.3.15\n\n### Patch Changes\n\n- Updated dependencies [[`cf25a60e2`](https://github.com/bluesky-social/atproto/commit/cf25a60e25b7531a359f0849729209a55193f7d6), [`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4)]:\n  - @atproto/pds@0.4.24\n  - @atproto/api@0.12.12\n  - @atproto/ozone@0.1.16\n  - @atproto/bsky@0.0.54\n\n## 0.3.14\n\n### Patch Changes\n\n- Updated dependencies [[`06d2328ee`](https://github.com/bluesky-social/atproto/commit/06d2328eeb8d706018dbdf7cc7b9862dd65b96cb)]:\n  - @atproto/api@0.12.11\n  - @atproto/bsky@0.0.53\n  - @atproto/ozone@0.1.15\n  - @atproto/pds@0.4.23\n\n## 0.3.13\n\n### Patch Changes\n\n- Updated dependencies [[`d32f7215f`](https://github.com/bluesky-social/atproto/commit/d32f7215f69bc87f50890d9cfdb09840c2fbaa41)]:\n  - @atproto/api@0.12.10\n  - @atproto/pds@0.4.22\n  - @atproto/bsky@0.0.52\n  - @atproto/ozone@0.1.14\n\n## 0.3.12\n\n### Patch Changes\n\n- Updated dependencies [[`f36585013`](https://github.com/bluesky-social/atproto/commit/f365850139ffb2b5e63facfd95eedf0b87d01ee7)]:\n  - @atproto/pds@0.4.21\n  - @atproto/bsky@0.0.51\n  - @atproto/ozone@0.1.13\n\n## 0.3.11\n\n### Patch Changes\n\n- Updated dependencies [[`f83b4c8ca`](https://github.com/bluesky-social/atproto/commit/f83b4c8cad01cebc1b67caa6c7ebe45f07b2f318)]:\n  - @atproto/api@0.12.9\n  - @atproto/bsky@0.0.51\n  - @atproto/ozone@0.1.13\n  - @atproto/pds@0.4.20\n\n## 0.3.10\n\n### Patch Changes\n\n- Updated dependencies [[`58f719cc1`](https://github.com/bluesky-social/atproto/commit/58f719cc1c8d0ebd5ad7cf11221372b671cd7857)]:\n  - @atproto/api@0.12.8\n  - @atproto/bsky@0.0.50\n  - @atproto/ozone@0.1.12\n  - @atproto/pds@0.4.19\n\n## 0.3.9\n\n### Patch Changes\n\n- Updated dependencies [[`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933)]:\n  - @atproto/ozone@0.1.11\n  - @atproto/api@0.12.7\n  - @atproto/pds@0.4.18\n  - @atproto/bsky@0.0.49\n\n## 0.3.8\n\n### Patch Changes\n\n- Updated dependencies [[`b9b7c5821`](https://github.com/bluesky-social/atproto/commit/b9b7c582199d57d2fe0af8af5c8c411ed34f5b9d)]:\n  - @atproto/api@0.12.6\n  - @atproto/bsky@0.0.48\n  - @atproto/ozone@0.1.10\n  - @atproto/pds@0.4.17\n\n## 0.3.7\n\n### Patch Changes\n\n- Updated dependencies [[`3424a1770`](https://github.com/bluesky-social/atproto/commit/3424a17703891f5678ec76ef97e696afb3288b22)]:\n  - @atproto/api@0.12.5\n  - @atproto/bsky@0.0.47\n  - @atproto/ozone@0.1.9\n  - @atproto/pds@0.4.16\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [[`93a4a4df9`](https://github.com/bluesky-social/atproto/commit/93a4a4df9ce38f89a5d05e300d247b85fb007e05)]:\n  - @atproto/api@0.12.4\n  - @atproto/pds@0.4.15\n  - @atproto/bsky@0.0.46\n  - @atproto/ozone@0.1.8\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`0edef0ec0`](https://github.com/bluesky-social/atproto/commit/0edef0ec01403fd6097a4d2875b68313f2f1261f), [`c6d758b8b`](https://github.com/bluesky-social/atproto/commit/c6d758b8b63f4ef50b2ab9afc62164e92a53e7f0)]:\n  - @atproto/api@0.12.3\n  - @atproto/bsky@0.0.45\n  - @atproto/ozone@0.1.7\n  - @atproto/pds@0.4.14\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies [[`cd4fcc709`](https://github.com/bluesky-social/atproto/commit/cd4fcc709fe8d725a4af769ce21f53711fe5622a)]:\n  - @atproto/xrpc-server@0.5.1\n  - @atproto/bsky@0.0.44\n  - @atproto/ozone@0.1.6\n  - @atproto/pds@0.4.13\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [[`d77ac35d4`](https://github.com/bluesky-social/atproto/commit/d77ac35d484925d90169e6a1047cddfbe90923bc)]:\n  - @atproto/pds@0.4.12\n  - @atproto/bsky@0.0.43\n  - @atproto/ozone@0.1.5\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`abc6f82da`](https://github.com/bluesky-social/atproto/commit/abc6f82da38abef2b1bbe8d9e41a0534a5418c9e)]:\n  - @atproto/api@0.12.2\n  - @atproto/bsky@0.0.43\n  - @atproto/ozone@0.1.5\n  - @atproto/pds@0.4.11\n\n## 0.3.1\n\n### Patch Changes\n\n- [#2342](https://github.com/bluesky-social/atproto/pull/2342) [`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds the `associated` property to `profile` and `profile-basic` views, bringing them in line with `profile-detailed` views.\n\n- Updated dependencies [[`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5)]:\n  - @atproto/ozone@0.1.4\n  - @atproto/bsky@0.0.42\n  - @atproto/api@0.12.1\n  - @atproto/pds@0.4.10\n\n## 0.3.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9), [`36f2e966c`](https://github.com/bluesky-social/atproto/commit/36f2e966cba6cc90ba4320520da5c7381cfb8086)]:\n  - @atproto/xrpc-server@0.5.0\n  - @atproto/common-web@0.3.0\n  - @atproto/identity@0.4.0\n  - @atproto/lexicon@0.4.0\n  - @atproto/crypto@0.4.0\n  - @atproto/syntax@0.3.0\n  - @atproto/api@0.12.0\n  - @atproto/bsync@0.0.3\n  - @atproto/ozone@0.1.3\n  - @atproto/bsky@0.0.41\n  - @atproto/pds@0.4.9\n\n## 0.2.41\n\n### Patch Changes\n\n- Updated dependencies [[`7dd9941b7`](https://github.com/bluesky-social/atproto/commit/7dd9941b73dbbd82601740e021cc87d765af60ca)]:\n  - @atproto/api@0.11.2\n  - @atproto/bsky@0.0.40\n  - @atproto/ozone@0.1.2\n  - @atproto/pds@0.4.8\n\n## 0.2.40\n\n### Patch Changes\n\n- Updated dependencies [[`b95c3955d`](https://github.com/bluesky-social/atproto/commit/b95c3955d0b8263a44a3d2a46b35b1831d9e504a)]:\n  - @atproto/ozone@0.1.1\n  - @atproto/api@0.11.1\n  - @atproto/bsky@0.0.39\n  - @atproto/pds@0.4.7\n\n## 0.2.39\n\n### Patch Changes\n\n- Updated dependencies [[`971d3e4c2`](https://github.com/bluesky-social/atproto/commit/971d3e4c26ecfda746e83d458391715752ea7064), [`219480764`](https://github.com/bluesky-social/atproto/commit/2194807644cbdb0021e867437693300c1b0e55f5), [`971d3e4c2`](https://github.com/bluesky-social/atproto/commit/971d3e4c26ecfda746e83d458391715752ea7064)]:\n  - @atproto/ozone@0.1.0\n  - @atproto/api@0.11.1\n  - @atproto/pds@0.4.7\n  - @atproto/bsky@0.0.39\n\n## 0.2.38\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0), [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n  - @atproto/identity@0.3.3\n  - @atproto/api@0.11.0\n  - @atproto/lexicon@0.3.3\n  - @atproto/syntax@0.2.1\n  - @atproto/bsky@0.0.38\n  - @atproto/ozone@0.0.17\n  - @atproto/pds@0.4.6\n  - @atproto/bsync@0.0.2\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.4\n\n## 0.2.37\n\n### Patch Changes\n\n- Updated dependencies [[`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655)]:\n  - @atproto/ozone@0.0.16\n  - @atproto/bsky@0.0.37\n  - @atproto/api@0.10.5\n  - @atproto/pds@0.4.5\n\n## 0.2.36\n\n### Patch Changes\n\n- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]:\n  - @atproto/api@0.10.4\n  - @atproto/bsky@0.0.36\n  - @atproto/ozone@0.0.15\n  - @atproto/pds@0.4.4\n\n## 0.2.35\n\n### Patch Changes\n\n- Updated dependencies [[`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed)]:\n  - @atproto/api@0.10.3\n  - @atproto/bsky@0.0.35\n  - @atproto/ozone@0.0.14\n  - @atproto/pds@0.4.3\n\n## 0.2.34\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410), [`43531905c`](https://github.com/bluesky-social/atproto/commit/43531905ce1aec6d36d9be5943782811ecca6e6d), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/api@0.10.2\n  - @atproto/bsky@0.0.34\n  - @atproto/bsync@0.0.1\n  - @atproto/lexicon@0.3.2\n  - @atproto/ozone@0.0.13\n  - @atproto/pds@0.4.2\n  - @atproto/xrpc-server@0.4.3\n\n## 0.2.33\n\n### Patch Changes\n\n- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]:\n  - @atproto/api@0.10.1\n  - @atproto/bsky@0.0.33\n  - @atproto/ozone@0.0.12\n  - @atproto/pds@0.4.1\n\n## 0.2.32\n\n### Patch Changes\n\n- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]:\n  - @atproto/api@0.10.0\n  - @atproto/bsky@0.0.32\n  - @atproto/ozone@0.0.11\n  - @atproto/pds@0.4.0\n\n## 0.2.31\n\n### Patch Changes\n\n- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]:\n  - @atproto/api@0.9.8\n  - @atproto/ozone@0.0.10\n  - @atproto/bsky@0.0.31\n  - @atproto/pds@0.3.19\n\n## 0.2.30\n\n### Patch Changes\n\n- Updated dependencies [[`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]:\n  - @atproto/api@0.9.7\n  - @atproto/bsky@0.0.30\n  - @atproto/pds@0.3.18\n  - @atproto/ozone@0.0.9\n\n## 0.2.29\n\n### Patch Changes\n\n- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]:\n  - @atproto/api@0.9.6\n  - @atproto/bsky@0.0.29\n  - @atproto/ozone@0.0.8\n  - @atproto/pds@0.3.17\n\n## 0.2.28\n\n### Patch Changes\n\n- Updated dependencies [[`8994d363`](https://github.com/bluesky-social/atproto/commit/8994d3633adad1c02569d6d44ae896e18195e8e2)]:\n  - @atproto/api@0.9.5\n  - @atproto/bsky@0.0.28\n  - @atproto/ozone@0.0.7\n  - @atproto/pds@0.3.16\n\n## 0.2.27\n\n### Patch Changes\n\n- Updated dependencies [[`4171c04a`](https://github.com/bluesky-social/atproto/commit/4171c04ad81c5734a4558bc41fa1c4f3a1aba18c)]:\n  - @atproto/api@0.9.4\n  - @atproto/bsky@0.0.27\n  - @atproto/ozone@0.0.6\n  - @atproto/pds@0.3.15\n\n## 0.2.26\n\n### Patch Changes\n\n- Updated dependencies [[`5368245a`](https://github.com/bluesky-social/atproto/commit/5368245a6ef7095c86ad166fb04ff9bef27c3c3e)]:\n  - @atproto/api@0.9.3\n  - @atproto/bsky@0.0.26\n  - @atproto/ozone@0.0.5\n  - @atproto/pds@0.3.14\n\n## 0.2.25\n\n### Patch Changes\n\n- Updated dependencies [[`15f38560`](https://github.com/bluesky-social/atproto/commit/15f38560b9e2dc3af8cf860826e7477234fe6a2d)]:\n  - @atproto/api@0.9.2\n  - @atproto/bsky@0.0.25\n  - @atproto/ozone@0.0.4\n  - @atproto/pds@0.3.13\n\n## 0.2.24\n\n### Patch Changes\n\n- Updated dependencies [[`c6fc73ae`](https://github.com/bluesky-social/atproto/commit/c6fc73aee6c245d12f876abd11889b8dbd0ce2ed)]:\n  - @atproto/api@0.9.1\n  - @atproto/bsky@0.0.24\n  - @atproto/ozone@0.0.3\n  - @atproto/pds@0.3.12\n\n## 0.2.23\n\n### Patch Changes\n\n- Updated dependencies [[`e43396af`](https://github.com/bluesky-social/atproto/commit/e43396af0973748dd2d034e88d35cf7ae8b4df2c), [`bf8d718c`](https://github.com/bluesky-social/atproto/commit/bf8d718cf918ac8d8a2cb1f57fde80535284642d), [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6)]:\n  - @atproto/api@0.9.0\n  - @atproto/bsky@0.0.23\n  - @atproto/pds@0.3.11\n  - @atproto/ozone@0.0.2\n\n## 0.2.22\n\n### Patch Changes\n\n- Updated dependencies [[`14067733`](https://github.com/bluesky-social/atproto/commit/140677335f76b99129c1f593d9e11d64624386c6)]:\n  - @atproto/api@0.8.0\n  - @atproto/bsky@0.0.22\n  - @atproto/pds@0.3.10\n\n## 0.2.21\n\n### Patch Changes\n\n- Updated dependencies [[`8f3f43cb`](https://github.com/bluesky-social/atproto/commit/8f3f43cb40f79ff7c52f81290daec55cfb000093)]:\n  - @atproto/api@0.7.4\n  - @atproto/bsky@0.0.21\n  - @atproto/pds@0.3.9\n\n## 0.2.20\n\n### Patch Changes\n\n- Updated dependencies [[`7dec9df3`](https://github.com/bluesky-social/atproto/commit/7dec9df3b583ee8c06c0c6a7e32c259820dc84a5)]:\n  - @atproto/api@0.7.3\n  - @atproto/bsky@0.0.20\n  - @atproto/pds@0.3.8\n\n## 0.2.19\n\n### Patch Changes\n\n- Updated dependencies [[`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a), [`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a)]:\n  - @atproto/bsky@0.0.19\n  - @atproto/api@0.7.2\n  - @atproto/pds@0.3.7\n\n## 0.2.18\n\n### Patch Changes\n\n- Updated dependencies [[`60deea17`](https://github.com/bluesky-social/atproto/commit/60deea17622f7c574c18432a55ced4e1cdc1b3a1)]:\n  - @atproto/api@0.7.1\n  - @atproto/bsky@0.0.18\n  - @atproto/pds@0.3.6\n\n## 0.2.17\n\n### Patch Changes\n\n- Updated dependencies [[`45352f9b`](https://github.com/bluesky-social/atproto/commit/45352f9b6d02aa405be94e9102424d983912ca5d)]:\n  - @atproto/api@0.7.0\n  - @atproto/bsky@0.0.17\n  - @atproto/pds@0.3.5\n\n## 0.2.16\n\n### Patch Changes\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60), [`378fc613`](https://github.com/bluesky-social/atproto/commit/378fc6132f621ca517897c9467ed5bba134b3776)]:\n  - @atproto/syntax@0.1.5\n  - @atproto/api@0.6.24\n  - @atproto/bsky@0.0.16\n  - @atproto/lexicon@0.3.1\n  - @atproto/pds@0.3.4\n  - @atproto/xrpc-server@0.4.2\n\n## 0.2.15\n\n### Patch Changes\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.1\n  - @atproto/bsky@0.0.15\n  - @atproto/identity@0.3.2\n  - @atproto/pds@0.3.3\n  - @atproto/api@0.6.23\n\n## 0.2.14\n\n### Patch Changes\n\n- Updated dependencies [[`772736a0`](https://github.com/bluesky-social/atproto/commit/772736a01081f39504e1b19a1b3687783bb78f07)]:\n  - @atproto/api@0.6.23\n  - @atproto/bsky@0.0.14\n  - @atproto/pds@0.3.2\n\n## 0.2.13\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/xrpc-server@0.4.0\n  - @atproto/common-web@0.2.3\n  - @atproto/identity@0.3.1\n  - @atproto/crypto@0.2.3\n  - @atproto/syntax@0.1.4\n  - @atproto/bsky@0.0.13\n  - @atproto/api@0.6.22\n  - @atproto/pds@0.3.1\n\n## 0.2.12\n\n### Patch Changes\n\n- Updated dependencies [[`9c98a5ba`](https://github.com/bluesky-social/atproto/commit/9c98a5baaf503b02238a6afe4f6e2b79c5181693), [`bb039d8e`](https://github.com/bluesky-social/atproto/commit/bb039d8e4ce5b7f70c4f3e86d1327e210ef24dc3), [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/api@0.6.21\n  - @atproto/identity@0.3.0\n  - @atproto/common-web@0.2.2\n  - @atproto/bsky@0.0.12\n  - @atproto/pds@0.3.0\n  - @atproto/lexicon@0.2.3\n  - @atproto/syntax@0.1.3\n  - @atproto/xrpc-server@0.3.3\n\n## 0.2.11\n\n### Patch Changes\n\n- [#1568](https://github.com/bluesky-social/atproto/pull/1568) [`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b) Thanks [@dholms](https://github.com/dholms)! - Added email verification and update flows\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b), [`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/api@0.6.20\n  - @atproto/pds@0.1.20\n  - @atproto/common-web@0.2.1\n  - @atproto/bsky@0.0.11\n  - @atproto/identity@0.2.1\n  - @atproto/syntax@0.1.2\n  - @atproto/xrpc-server@0.3.2\n\n## 0.2.10\n\n### Patch Changes\n\n- Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]:\n  - @atproto/api@0.6.19\n  - @atproto/bsky@0.0.10\n  - @atproto/pds@0.1.19\n\n## 0.2.9\n\n### Patch Changes\n\n- Updated dependencies [[`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf)]:\n  - @atproto/api@0.6.18\n  - @atproto/bsky@0.0.9\n  - @atproto/pds@0.1.18\n\n## 0.2.8\n\n### Patch Changes\n\n- Updated dependencies [[`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd)]:\n  - @atproto/bsky@0.0.8\n  - @atproto/api@0.6.17\n  - @atproto/pds@0.1.17\n\n## 0.2.7\n\n### Patch Changes\n\n- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]:\n  - @atproto/api@0.6.16\n  - @atproto/bsky@0.0.7\n  - @atproto/pds@0.1.16\n\n## 0.2.6\n\n### Patch Changes\n\n- Updated dependencies [[`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f)]:\n  - @atproto/api@0.6.15\n  - @atproto/bsky@0.0.6\n  - @atproto/pds@0.1.15\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n  - @atproto/api@0.6.14\n  - @atproto/bsky@0.0.5\n  - @atproto/pds@0.1.14\n  - @atproto/xrpc-server@0.3.1\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]:\n  - @atproto/api@0.6.13\n  - @atproto/bsky@0.0.4\n  - @atproto/pds@0.1.13\n"
  },
  {
    "path": "packages/dev-env/README.md",
    "content": "# @atproto/dev-env: Local Developer Environment\n\nA command-line application for developers to construct and manage development environments.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/dev-env)](https://www.npmjs.com/package/@atproto/dev-env)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## REPL API\n\nThe following methods are available in the REPL.\n\n### `status()`\n\nList the currently active servers.\n\n### `startPds(port?: number)`\n\nCreate a new PDS instance. Data is stored in memory.\n\n### `stop(port: number)`\n\nStop the server at the given port.\n\n### `mkuser(handle: string, pdsPort?: number)`\n\nCreate a new user.\n\n### `user(handle: string): ServiceClient`\n\nGet the `ServiceClient` for the given user.\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/dev-env/package.json",
    "content": "{\n  \"name\": \"@atproto/dev-env\",\n  \"version\": \"0.3.215\",\n  \"license\": \"MIT\",\n  \"description\": \"Local development environment helper for atproto development\",\n  \"keywords\": [\n    \"atproto\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/dev-env\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"bin\": \"dist/bin.js\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"start\": \"../dev-infra/with-test-redis-and-db.sh node --enable-source-maps dist/bin.js\",\n    \"dev\": \"../dev-infra/with-test-redis-and-db.sh node --enable-source-maps --watch dist/bin.js\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/api\": \"workspace:^\",\n    \"@atproto/bsky\": \"workspace:^\",\n    \"@atproto/bsync\": \"workspace:^\",\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/identity\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/ozone\": \"workspace:^\",\n    \"@atproto/pds\": \"workspace:^\",\n    \"@atproto/sync\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/xrpc-server\": \"workspace:^\",\n    \"@did-plc/lib\": \"^0.0.1\",\n    \"@did-plc/server\": \"^0.0.1\",\n    \"dotenv\": \"^16.0.3\",\n    \"express\": \"^4.18.2\",\n    \"get-port\": \"^5.1.1\",\n    \"multiformats\": \"^9.9.0\",\n    \"uint8arrays\": \"3.0.0\",\n    \"undici\": \"^6.14.1\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.13\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/bin.ts",
    "content": "import './env'\nimport { generateMockSetup } from './mock'\nimport { TestNetwork } from './network'\nimport { mockMailer } from './util'\n\nconst run = async () => {\n  console.log(`\n██████╗\n██╔═══██╗\n██║██╗██║\n██║██║██║\n╚█║████╔╝\n ╚╝╚═══╝  protocol\n\n[ created by Bluesky ]`)\n\n  const network = await TestNetwork.create({\n    pds: {\n      port: 2583,\n      hostname: 'localhost',\n      enableDidDocWithSession: true,\n    },\n    bsky: {\n      dbPostgresSchema: 'bsky',\n      port: 2584,\n      publicUrl: 'http://localhost:2584',\n    },\n    plc: { port: 2582 },\n    ozone: {\n      port: 2587,\n      chatUrl: 'http://localhost:2590', // must run separate chat service\n      chatDid: 'did:example:chat',\n      dbMaterializedViewRefreshIntervalMs: 30_000,\n    },\n    introspect: { port: 2581 },\n  })\n  mockMailer(network.pds)\n  await generateMockSetup(network)\n\n  if (network.introspect) {\n    console.log(\n      `🔍 Dev-env introspection server http://localhost:${network.introspect.port}`,\n    )\n  }\n  console.log(`👤 DID Placeholder server http://localhost:${network.plc.port}`)\n  console.log(`🌞 Main PDS http://localhost:${network.pds.port}`)\n  console.log(\n    `🔨 Lexicon authority DID ${network.pds.ctx.cfg.lexicon.didAuthority}`,\n  )\n  console.log(`🗼 Ozone server http://localhost:${network.ozone.port}`)\n  console.log(`🗼 Ozone service DID ${network.ozone.ctx.cfg.service.did}`)\n  console.log(`🌅 Bsky Appview http://localhost:${network.bsky.port}`)\n  console.log(`🌅 Bsky Appview DID ${network.bsky.serverDid}`)\n  for (const fg of network.feedGens) {\n    console.log(`🤖 Feed Generator (${fg.did}) http://localhost:${fg.port}`)\n  }\n}\n\nrun()\n"
  },
  {
    "path": "packages/dev-env/src/bsky.ts",
    "content": "import { Client as PlcClient } from '@did-plc/lib'\nimport getPort from 'get-port'\nimport * as ui8 from 'uint8arrays'\nimport { AtpAgent } from '@atproto/api'\nimport * as bsky from '@atproto/bsky'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { ADMIN_PASSWORD, EXAMPLE_LABELER } from './const'\nimport { BskyConfig } from './types'\nexport * from '@atproto/bsky'\n\nexport class TestBsky {\n  constructor(\n    public url: string,\n    public port: number,\n    public db: bsky.Database,\n    public server: bsky.BskyAppView,\n    public dataplane: bsky.DataPlaneServer,\n    public bsync: bsky.MockBsync,\n    public sub: bsky.RepoSubscription,\n    public serverDid: string,\n  ) {}\n\n  static async create(cfg: BskyConfig): Promise<TestBsky> {\n    const serviceKeypair = cfg.privateKey\n      ? await Secp256k1Keypair.import(cfg.privateKey)\n      : await Secp256k1Keypair.create()\n    const plcClient = new PlcClient(cfg.plcUrl)\n\n    const port = cfg.port || (await getPort())\n    const url = `http://localhost:${port}`\n    const serverDid = await plcClient.createDid({\n      signingKey: serviceKeypair.did(),\n      rotationKeys: [serviceKeypair.did()],\n      handle: 'bsky.test',\n      pds: `http://localhost:${port}`,\n      signer: serviceKeypair,\n    })\n\n    const endpoint = `http://localhost:${port}`\n\n    await plcClient.updateData(serverDid, serviceKeypair, (x) => {\n      x.services['bsky_notif'] = {\n        type: 'BskyNotificationService',\n        endpoint,\n      }\n      x.services['bsky_appview'] = {\n        type: 'BskyAppView',\n        endpoint,\n      }\n      return x\n    })\n\n    // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections.\n    const db = new bsky.Database({\n      url: cfg.dbPostgresUrl,\n      schema: cfg.dbPostgresSchema,\n      poolSize: 10,\n    })\n\n    const dataplanePort = await getPort()\n    const dataplane = await bsky.DataPlaneServer.create(\n      db,\n      dataplanePort,\n      cfg.plcUrl,\n    )\n\n    const bsyncPort = await getPort()\n    const bsync = await bsky.MockBsync.create(db, bsyncPort)\n\n    const config = new bsky.ServerConfig({\n      version: 'unknown',\n      port,\n      didPlcUrl: cfg.plcUrl,\n      publicUrl: 'https://bsky.public.url',\n      serverDid,\n      alternateAudienceDids: [],\n      dataplaneUrls: [`http://localhost:${dataplanePort}`],\n      dataplaneHttpVersion: '1.1',\n      bsyncUrl: `http://localhost:${bsyncPort}`,\n      bsyncHttpVersion: '1.1',\n      modServiceDid: cfg.modServiceDid ?? 'did:example:invalidMod',\n      labelsFromIssuerDids: [EXAMPLE_LABELER],\n      bigThreadUris: new Set(),\n      maxThreadParents: cfg.maxThreadParents ?? 50,\n      disableSsrfProtection: true,\n      searchTagsHide: new Set(),\n      threadTagsBumpDown: new Set(),\n      threadTagsHide: new Set(),\n      visibilityTagHide: '',\n      visibilityTagRankPrefix: '',\n      debugFieldAllowedDids: new Set(),\n      draftsLimit: 500,\n      ...cfg,\n      adminPasswords: [ADMIN_PASSWORD],\n      etcdHosts: [],\n    })\n\n    // Separate migration db in case migration changes some connection state that we need in the tests, e.g. \"alter database ... set ...\"\n    const migrationDb = new bsky.Database({\n      url: cfg.dbPostgresUrl,\n      schema: cfg.dbPostgresSchema,\n    })\n    if (cfg.migration) {\n      await migrationDb.migrateToOrThrow(cfg.migration)\n    } else {\n      await migrationDb.migrateToLatestOrThrow()\n    }\n    await migrationDb.close()\n\n    // api server\n    const server = bsky.BskyAppView.create({\n      config,\n      signingKey: serviceKeypair,\n    })\n\n    const sub = new bsky.RepoSubscription({\n      service: cfg.repoProvider,\n      db,\n      idResolver: dataplane.idResolver,\n    })\n\n    await server.start()\n\n    sub.start()\n\n    return new TestBsky(url, port, db, server, dataplane, bsync, sub, serverDid)\n  }\n\n  get ctx(): bsky.AppContext {\n    return this.server.ctx\n  }\n\n  getClient(): AtpAgent {\n    const agent = new AtpAgent({ service: this.url })\n    agent.configureLabelers([EXAMPLE_LABELER])\n    return agent\n  }\n\n  adminAuth(): string {\n    const [password] = this.ctx.cfg.adminPasswords\n    return (\n      'Basic ' +\n      ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad')\n    )\n  }\n\n  adminAuthHeaders() {\n    return {\n      authorization: this.adminAuth(),\n    }\n  }\n\n  async close() {\n    await this.server.destroy()\n    await this.bsync.destroy()\n    await this.dataplane.destroy()\n    await this.sub.destroy()\n    await this.db.close()\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/bsync.ts",
    "content": "import getPort from 'get-port'\nimport * as bsync from '@atproto/bsync'\nimport { BsyncConfig } from './types'\n\nexport class TestBsync {\n  constructor(\n    public url: string,\n    public port: number,\n    public service: bsync.BsyncService,\n  ) {}\n\n  static async create(cfg: BsyncConfig): Promise<TestBsync> {\n    const port = cfg.port || (await getPort())\n    const url = `http://localhost:${port}`\n\n    const config = bsync.envToCfg({\n      port,\n      apiKeys: cfg.apiKeys ?? ['api-key'],\n      ...cfg,\n    })\n\n    const service = await bsync.BsyncService.create(config)\n    await service.ctx.db.migrateToLatestOrThrow()\n    await service.start()\n\n    return new TestBsync(url, port, service)\n  }\n\n  get ctx(): bsync.AppContext {\n    return this.service.ctx\n  }\n\n  async close() {\n    await this.service.destroy()\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/const.ts",
    "content": "export const ADMIN_PASSWORD = 'admin-pass'\nexport const JWT_SECRET = 'jwt-secret'\nexport const EXAMPLE_LABELER = 'did:example:labeler'\n"
  },
  {
    "path": "packages/dev-env/src/env.ts",
    "content": "// NOTE: this file should be imported first, particularly before `@atproto/common` (for logging), to ensure that environment variables are respected in library code\nimport dotenv from 'dotenv'\n\nconst env = process.env.ENV\nif (env) {\n  dotenv.config({ path: `./.${env}.env` })\n} else {\n  dotenv.config()\n}\n"
  },
  {
    "path": "packages/dev-env/src/feed-gen.ts",
    "content": "import events from 'node:events'\nimport http from 'node:http'\nimport * as plc from '@did-plc/lib'\nimport express from 'express'\nimport getPort from 'get-port'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { SkeletonHandler, createLexiconServer } from '@atproto/pds'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\n\nexport class TestFeedGen {\n  destroyed = false\n\n  constructor(\n    public port: number,\n    public server: http.Server,\n    public did: string,\n  ) {}\n\n  static async create(\n    plcUrl: string,\n    feeds: Record<string, SkeletonHandler>,\n  ): Promise<TestFeedGen> {\n    const port = await getPort()\n    const did = await createFgDid(plcUrl, port)\n    const app = express()\n    const lexServer = createLexiconServer()\n\n    lexServer.app.bsky.feed.getFeedSkeleton(async (args) => {\n      const handler = feeds[args.params.feed]\n      if (!handler) {\n        throw new InvalidRequestError('unknown feed', 'UnknownFeed')\n      }\n      return handler(args)\n    })\n\n    lexServer.app.bsky.feed.describeFeedGenerator(async () => {\n      return {\n        encoding: 'application/json',\n        body: {\n          did,\n          feeds: Object.keys(feeds).map((uri) => ({\n            uri,\n          })),\n        },\n      }\n    })\n\n    app.use(lexServer.xrpc.router)\n    const server = app.listen(port)\n    await events.once(server, 'listening')\n    return new TestFeedGen(port, server, did)\n  }\n\n  close(): Promise<void> {\n    return new Promise((resolve, reject) => {\n      if (this.destroyed) return resolve()\n      this.server.close((err) => {\n        if (err) return reject(err)\n        this.destroyed = true\n        resolve()\n      })\n    })\n  }\n}\n\nconst createFgDid = async (plcUrl: string, port: number): Promise<string> => {\n  const keypair = await Secp256k1Keypair.create()\n  const plcClient = new plc.Client(plcUrl)\n  const op = await plc.signOperation(\n    {\n      type: 'plc_operation',\n      verificationMethods: {\n        atproto: keypair.did(),\n      },\n      rotationKeys: [keypair.did()],\n      alsoKnownAs: [],\n      services: {\n        bsky_fg: {\n          type: 'BskyFeedGenerator',\n          endpoint: `http://localhost:${port}`,\n        },\n      },\n      prev: null,\n    },\n    keypair,\n  )\n  const did = await plc.didForCreateOp(op)\n  await plcClient.sendOperation(did, op)\n  return did\n}\n"
  },
  {
    "path": "packages/dev-env/src/index.ts",
    "content": "export * from './bsky'\nexport * from './bsync'\nexport * from './network'\nexport * from './network-no-appview'\nexport * from './ozone'\nexport * from './pds'\nexport * from './plc'\nexport * from './ozone'\nexport * from './feed-gen'\nexport * from './seed'\nexport * from './moderator-client'\nexport * from './types'\nexport * from './util'\nexport * from './const'\n\nimport * as seedThreadV2 from './seed/thread-v2.js'\nexport { seedThreadV2 }\n"
  },
  {
    "path": "packages/dev-env/src/introspect.ts",
    "content": "import events from 'node:events'\nimport http from 'node:http'\nimport express from 'express'\nimport { TestBsky } from './bsky'\nimport { TestOzone } from './ozone'\nimport { TestPds } from './pds'\nimport { TestPlc } from './plc'\n\nexport class IntrospectServer {\n  constructor(\n    public port: number,\n    public server: http.Server,\n  ) {}\n\n  static async start(\n    port: number,\n    plc: TestPlc,\n    pds: TestPds,\n    bsky: TestBsky,\n    ozone: TestOzone,\n  ) {\n    const app = express()\n    app.get('/', (_req, res) => {\n      res.status(200).send({\n        plc: {\n          url: plc.url,\n        },\n        pds: {\n          url: pds.url,\n          did: pds.ctx.cfg.service.did,\n        },\n        bsky: {\n          url: bsky.url,\n          did: bsky.ctx.cfg.serverDid,\n        },\n        ozone: {\n          url: ozone.url,\n          did: ozone.ctx.cfg.service.did,\n        },\n        db: {\n          url: ozone.ctx.cfg.db.postgresUrl,\n        },\n      })\n    })\n    const server = app.listen(port)\n    await events.once(server, 'listening')\n    return new IntrospectServer(port, server)\n  }\n\n  async close() {\n    this.server.close()\n    await events.once(this.server, 'close')\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/mock/data.ts",
    "content": "export const postTexts = [\n  `Nervous?\nYes. Very.\nFirst time?\nNo, I've been nervous lots of times.`,\n  `I am serious...and don't call me Shirley.`,\n  `Looks like I picked the wrong week to quit smoking.`,\n  `Looks like I picked the wrong week to quit drinking.`,\n  `Looks like I picked the wrong week to quit amphetamines.`,\n  `Looks like I picked the wrong week to quit sniffing glue.`,\n  `Captain, how soon can we land?\nI can't tell.\nYou can tell me, I'm a doctor.`,\n  `Ladies and gentlemen, this is your stewardess speaking... We regret any inconvenience the sudden cabin movement might have caused, and we hope you enjoy the rest of your flight... By the way, is there anyone on board who knows how to fly a plane? `,\n  `Joey, have you ever been in a turkish prison?`,\n  `These people need to go to a hospital.\nWhat is it?\nIt's a big place where sick people go, but that's not important right now.`,\n  `I just want to tell you both good luck. We're all counting on you.`,\n  `Joey, do you like movies about gladiators?`,\n  `Captain, maybe we ought to turn on the searchlights now.\nNo… that’s just what they’ll be expecting us to do.`,\n  `Johnny, what can you make out of this?\n  This? Why, I can make a hat or a brooch or a pterodactyl…`,\n  `I need the best man on this. Someone who knows that plane inside and out and won’t crack under pressure. How about Mister Rogers?`,\n  `What was it we had for dinner tonight?\nWell, we had a choice of steak or fish.\nYes, yes, I remember, I had lasagna.`,\n  `Jim never vomits at home.`,\n  `The life of everyone on board depends upon just one thing: finding someone back there who can not only fly this plane, but who didn't have fish for dinner.`,\n  `Shanna, they bought their tickets, they knew what they were getting into. I say, let 'em crash.`,\n]\n\nexport const replyTexts = [\n  'Wow, so true!',\n  'Haha ikr',\n  'lol',\n  'What does this mean for pet owners in the midterms?',\n  'is it cool if I DM?',\n  \"This is sort of accurate, but honestly it misses a huge part of the issue at hand. There are just so many factors and I don't think a vibe is enough to cover it.\",\n  'Wen token',\n  'fire',\n  'a/s/l?',\n  'finally! decentralization!',\n  'ugh when will hashtags get supported in this app',\n]\n"
  },
  {
    "path": "packages/dev-env/src/mock/img/blur-hash-avatar-b64.ts",
    "content": "export default 'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC'\n"
  },
  {
    "path": "packages/dev-env/src/mock/img/labeled-img-b64.ts",
    "content": "export default 'iVBORw0KGgoAAAANSUhEUgAAAjIAAAGKCAYAAAAWvavcAAAMa2lDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnluSkJDQAghICb0jUgNICaEFkN5thCSQUGJMCCr2sqjg2kUUK7oqomBZaXbsyqLY+2JBRVkXdbGh8iYkoOu+8r3zfXPvnzNn/lPuTO49AGh+4Eok+agWAAXiQmlCeDAjLT2DQXoGEIACCsCBA5cnk7Di4qIBlMH73+XdDWgN5aqzguuf8/9VdPgCGQ8AZCzEWXwZrwDi4wDg63kSaSEARIXecnKhRIFnQ6wrhQFCvEqBc5R4pwJnKfHhAZukBDbElwFQo3K50hwANO5BPaOIlwN5ND5D7Crmi8QAaDpBHMATcvkQK2J3KiiYqMAVENtBewnEMB7AzPqOM+dv/FlD/FxuzhBW5jUgaiEimSSfO/X/LM3/loJ8+aAPGzioQmlEgiJ/WMNbeROjFJgKcbc4KyZWUWuIP4j4yroDgFKE8ohkpT1qzJOxYf2APsSufG5IFMTGEIeJ82OiVfqsbFEYB2K4W9ApokJOEsQGEC8UyEITVTabpRMTVL7Q+mwpm6XSn+NKB/wqfD2Q5yWzVPxvhAKOih/TKBYmpUJMgdiqSJQSA7EGxC6yvMQolc2oYiE7ZtBGKk9QxG8FcYJAHB6s5MeKsqVhCSr70gLZYL7YZqGIE6PC+wuFSRHK+mCneNyB+GEu2GWBmJU8yCOQpUUP5sIXhIQqc8eeC8TJiSqeD5LC4ATlWpwiyY9T2eMWgvxwhd4CYg9ZUaJqLZ5SCDenkh/PlhTGJSnjxItzuZFxynjwZSAasEEIYAA5HFlgIsgForbuhm74SzkTBrhACnKAADirNIMrUgdmxPCaCIrBHxAJgGxoXfDArAAUQf2XIa3y6gyyB2aLBlbkgacQF4AokA9/ywdWiYe8pYAnUCP6h3cuHDwYbz4civl/rx/UftOwoCZapZEPemRoDloSQ4khxAhiGNEeN8IDcD88Gl6D4HDDmbjPYB7f7AlPCe2ER4TrhA7C7QmiudIfohwNOiB/mKoWWd/XAreBnJ54MO4P2SEzro8bAWfcA/ph4YHQsyfUslVxK6rC+IH7bxl89zRUdmRXMkoeRg4i2/24UsNBw3OIRVHr7+ujjDVrqN7soZkf/bO/qz4f3qN+tMQWYgews9gJ7Dx2GGsADOwY1oi1YkcUeGh3PRnYXYPeEgbiyYM8on/446p8Kiopc61x7XL9rJwrFEwpVBw89kTJVKkoR1jIYMG3g4DBEfNcnBhurm7uACjeNcq/r7fxA+8QRL/1m27e7wD4H+vv7z/0TRd5DIB93vD4N33T2TEB0FYH4FwTTy4tUupwxYUA/yU04UkzBKbAEtjBfNyAF/ADQSAURIJYkATSwXhYZSHc51IwGUwHc0AJKAPLwGqwDmwCW8FOsAfsBw3gMDgBzoCL4DK4Du7C3dMJXoIe8A70IQhCQmgIHTFEzBBrxBFxQ5hIABKKRCMJSDqSieQgYkSOTEfmIWXICmQdsgWpRvYhTcgJ5DzSjtxGHiJdyBvkE4qhVFQXNUFt0BEoE2WhUWgSOg7NQSehxeh8dAlagVahu9F69AR6Eb2OdqAv0V4MYOqYPmaOOWNMjI3FYhlYNibFZmKlWDlWhdVizfA5X8U6sG7sI07E6TgDd4Y7OAJPxnn4JHwmvhhfh+/E6/FT+FX8Id6DfyXQCMYER4IvgUNII+QQJhNKCOWE7YSDhNPwLHUS3hGJRH2iLdEbnsV0Yi5xGnExcQOxjnic2E58TOwlkUiGJEeSPymWxCUVkkpIa0m7ScdIV0idpA9q6mpmam5qYWoZamK1uWrlarvUjqpdUXum1kfWIluTfcmxZD55KnkpeRu5mXyJ3Enuo2hTbCn+lCRKLmUOpYJSSzlNuUd5q66ubqHuox6vLlKfrV6hvlf9nPpD9Y9UHaoDlU0dS5VTl1B3UI9Tb1Pf0mg0G1oQLYNWSFtCq6adpD2gfdCga7hocDT4GrM0KjXqNa5ovNIka1prsjTHaxZrlmse0Lyk2a1F1rLRYmtxtWZqVWo1ad3U6tWma4/UjtUu0F6svUv7vPZzHZKOjU6oDl9nvs5WnZM6j+kY3ZLOpvPo8+jb6KfpnbpEXVtdjm6ubpnuHt023R49HT0PvRS9KXqVekf0OvQxfRt9jn6+/lL9/fo39D8NMxnGGiYYtmhY7bArw94bDDcIMhAYlBrUGVw3+GTIMAw1zDNcbthgeN8IN3IwijeabLTR6LRR93Dd4X7DecNLh+8ffscYNXYwTjCeZrzVuNW418TUJNxEYrLW5KRJt6m+aZBprukq06OmXWZ0swAzkdkqs2NmLxh6DBYjn1HBOMXoMTc2jzCXm28xbzPvs7C1SLaYa1Fncd+SYsm0zLZcZdli2WNlZjXaarpVjdUda7I101povcb6rPV7G1ubVJsFNg02z20NbDm2xbY1tvfsaHaBdpPsquyu2RPtmfZ59hvsLzugDp4OQodKh0uOqKOXo8hxg2O7E8HJx0nsVOV005nqzHIucq5xfuii7xLtMtelweXVCKsRGSOWjzg74qurp2u+6zbXuyN1RkaOnDuyeeQbNwc3nlul2zV3mnuY+yz3RvfXHo4eAo+NHrc86Z6jPRd4tnh+8fL2knrVenV5W3lneq/3vsnUZcYxFzPP+RB8gn1m+Rz2+ejr5Vvou9/3Tz9nvzy/XX7PR9mOEozaNuqxv4U/13+Lf0cAIyAzYHNAR6B5IDewKvBRkGUQP2h70DOWPSuXtZv1Ktg1WBp8MPg925c9g308BAsJDykNaQvVCU0OXRf6IMwiLCesJqwn3DN8WvjxCEJEVMTyiJscEw6PU83pifSOnBF5KooalRi1LupRtEO0NLp5NDo6cvTK0fdirGPEMQ2xIJYTuzL2fpxt3KS4Q/HE+Lj4yvinCSMTpiecTaQnTkjclfguKThpadLdZLtkeXJLimbK2JTqlPepIakrUjvSRqTNSLuYbpQuSm/MIGWkZGzP6B0TOmb1mM6xnmNLxt4YZztuyrjz443G548/MkFzAnfCgUxCZmrmrszP3FhuFbc3i5O1PquHx+at4b3kB/FX8bsE/oIVgmfZ/tkrsp/n+OeszOkSBgrLhd0itmid6HVuRO6m3Pd5sXk78vrzU/PrCtQKMguaxDriPPGpiaYTp0xslzhKSiQdk3wnrZ7UI42SbpchsnGyxkJd+FHfKreT/yR/WBRQVFn0YXLK5ANTtKeIp7ROdZi6aOqz4rDiX6bh03jTWqabT58z/eEM1owtM5GZWTNbZlnOmj+rc3b47J1zKHPy5vw213Xuirl/zUud1zzfZP7s+Y9/Cv+ppkSjRFpyc4Hfgk0L8YWihW2L3BetXfS1lF96ocy1rLzs82Le4gs/j/y54uf+JdlL2pZ6Ld24jLhMvOzG8sDlO1doryhe8Xjl6JX1qxirSlf9tXrC6vPlHuWb1lDWyNd0VERXNK61Wrts7ed1wnXXK4Mr69Ybr1+0/v0G/oYrG4M21m4y2VS26dNm0eZbW8K31FfZVJVvJW4t2vp0W8q2s78wf6nebrS9bPuXHeIdHTsTdp6q9q6u3mW8a2kNWiOv6do9dvflPSF7Gmuda7fU6deV7QV75Xtf7Mvcd2N/1P6WA8wDtb9a/7r+IP1gaT1SP7W+p0HY0NGY3tjeFNnU0uzXfPCQy6Edh80PVx7RO7L0KOXo/KP9x4qP9R6XHO8+kXPiccuElrsn005eOxV/qu101OlzZ8LOnDzLOnvsnP+5w+d9zzddYF5ouOh1sb7Vs/Xgb56/HWzzaqu/5H2p8bLP5eb2Ue1HrwReOXE15OqZa5xrF6/HXG+/kXzj1s2xNztu8W89v51/+/Wdojt9d2ffI9wrva91v/yB8YOq3+1/r+vw6jjyMORh66PER3cf8x6/fCJ78rlz/lPa0/JnZs+qn7s9P9wV1nX5xZgXnS8lL/u6S/7Q/mP9K7tXv/4Z9GdrT1pP52vp6/43i98avt3xl8dfLb1xvQ/eFbzre1/6wfDDzo/Mj2c/pX561jf5M+lzxRf7L81fo77e6y/o75dwpdyBTwEMDjQ7G4A3OwCgpQNAh30bZYyyFxwQRNm/DiDwn7CyXxwQLwBq4fd7fDf8urkJwN5tsP2C/JqwV42jAZDkA1B396GhElm2u5uSiwr7FMKD/v63sGcjrQTgy7L+/r6q/v4vW2GwsHc8Llb2oAohwp5hc8yXrIIs8G9E2Z9+l+OPd6CIwAP8eP8Xl2WQqwlokcIAAACKZVhJZk1NACoAAAAIAAQBGgAFAAAAAQAAAD4BGwAFAAAAAQAAAEYBKAADAAAAAQACAACHaQAEAAAAAQAAAE4AAAAAAAAAkAAAAAEAAACQAAAAAQADkoYABwAAABIAAAB4oAIABAAAAAEAAAIyoAMABAAAAAEAAAGKAAAAAEFTQ0lJAAAAU2NyZWVuc2hvdN8jVV0AAAAJcEhZcwAAFiUAABYlAUlSJPAAAAHWaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjM5NDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj41NjI8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpVc2VyQ29tbWVudD5TY3JlZW5zaG90PC9leGlmOlVzZXJDb21tZW50PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KJyU8MAAAABxpRE9UAAAAAgAAAAAAAADFAAAAKAAAAMUAAADFAAAcHX695a0AABvpSURBVHgB7N0J8FXz/8fxj1JCFEmWVJYZkxYTkq0QaRRFUtKoZMm+JURJ9lJkDVG2rJE9yjTGltBURhokiUyWtCnZ/7/3mf/n2/d+v/eez+eec+695317fmb49L1n+5zH57u8zvY5mzVp0uQ/Q0mlwLg774jUrquHXhNpORZCAAEEEEBAm8BmBBltXUZ7EUAAAQQQQMAKEGSsBDUCCCCAAAIIqBMgyKjrMhqMAAIIIIAAAlaAIGMlqBFAAAEEEEBAnQBBRl2X0WAEEEAAAQQQsAIEGStBjQACCCCAAALqBAgy6rqMBiOAAAIIIICAFSDIWAlqBBBAAAEEEFAnQJBR12U0GAEEEEAAAQSsAEHGSlAjgAACCCCAgDoBgoy6LqPBCCCAAAIIIGAFCDJWghoBBBBAAAEE1AkQZNR1GQ1GAAEEEEAAAStAkLES1AgggAACCCCgToAgo67LaDACCCCAAAIIWAGCjJWgRgABBBBAAAF1AgQZdV1GgxFAAAEEEEDAChBkrAQ1AggggAACCKgTIMio6zIajAACCCCAAAJWgCBjJagRQAABBBBAQJ0AQUZdl9FgBBBAAAEEELACBBkrQY0AAggggAAC6gQIMuq6jAYjgAACCCCAgBUgyFgJagQQQAABBBBQJ0CQUddlNBgBBBBAAAEErABBxkpQI4AAAggggIA6AYKMui6jwQgggAACCCBgBQgyVoIaAQQQQAABBNQJEGTUdRkNRgABBBBAAAErQJCxEtQIIIAAAgggoE6AIKOuy2gwAggggAACCFgBgoyVoEYAAQQQQAABdQIEGXVdRoMRQAABBBBAwAoQZKwENQIIIIAAAgioEyDIqOsyGowAAggggAACVoAgYyWoEUAAAQQQQECdAEFGXZfRYAQQQAABBBCwAgQZK0GNAAIIIIAAAuoECDLquowGI4AAAggggIAVIMhYCWoEEEAAAQQQUCdAkFHXZTQYAQQQQAABBKwAQcZKUCOAAAIIIICAOgGCjLouo8EIIIAAAgggYAUIMlaCGgEEEEAAAQTUCRBk1HUZDUYAAQQQQAABK0CQsRLUCCCAAAIIIKBOgCCjrstoMAIIIIAAAghYAYKMlaBGAAEEEEAAAXUCBBl1XUaDEUAAAQQQQMAKEGSsBDUCCCCAAAIIqBMgyKjrMhqMAAIIIIAAAlaAIGMlqBFAAAEEEEBAnQBBRl2X0WAEEEAAAQQQsAIEGStBjQACCCCAAALqBAgy6rqMBiOAAAIIIICAFSDIWAlqBBBAAAEEEFAnQJBR12U0GAEEEEAAAQSsAEHGSlAjgAACCCCAgDoBgoy6LqPBCCCAAAIIIGAFCDJWghoBBBBAAAEE1AkQZNR1GQ1GAAEEEEAAAStAkLES1AgggAACCCCgToAgo67LaDACCCCAAAIIWAGCjJWgRgABBBBAAAF1AgQZdV1GgxFAAAEEEEDAChBkrAQ1AggggAACCKgTIMio6zIajAACCCCAAAJWgCBjJagRQAABBBBAQJ0AQUZdl9FgBBBAAAEEELACBBkrQY0AAggggAAC6gQIMuq6jAYjgAACCCCAgBUgyFgJagQQQAABBBBQJ0CQUddlNBgBBBBAAAEErABBxkpQI4AAAggggIA6AYKMui6jwQgggAACCCBgBQgyVoIaAQQQQAABBNQJEGTUdRkNRgABBBBAAAErQJCxEtQIIIAAAgggoE6AIKOuy2gwAggggAACCFgBgoyVoEYAAQQQQAABdQIEGXVdRoMRQAABBBBAwAoQZKwENQIIIIAAAgioEyDIqOsyGowAAggggAACVoAgYyWoEUAAAQQQQECdAEFGXZfRYAQQQAABBBCwAgQZK0GNAAIIIIAAAuoECDLquowGI4AAAggggIAVIMhYCWoEEEAAAQQQUCdAkFHXZTQYAQQQQAABBKwAQcZKUCOAAAIIIICAOgGCjLouo8EIIIAAAgggYAUIMlaCGgEEEEAAAQTUCRBk1HUZDUYAAQQQQAABK0CQsRLUCCCAAAIIIKBOgCCjrstoMAIIIIAAAghYAYKMlaBGAAEEEEAAAXUCBBl1XUaDEUAAAQQQQMAKEGSsBDUCCCCAAAIIqBMgyKjrMhqMAAIIIIAAAlaAIGMlqBFAAAEEEEBAnQBBRl2X0WAEEEAAAQQQsAIEGStBjQACCCCAAALqBAgy6rqMBiOAAAIIIICAFSDIWAlqBBBAAAEEEFAnQJBR12U0GAEEEEAAAQSsAEHGSlAjgAACCCCAgDoBgoy6LqPBCCCAAAIIIGAFCDJWghoBBBBAAAEE1AkQZNR1GQ1GAAEEEEAAAStAkLES1AgggAACCCCgToAgo67LaDACCCCAAAIIWAGCjJWgRgABBBBAAAF1AgQZdV1GgxFAAAEEEEDAChBkrAQ1AggggAACCKgTIMio6zIajAACCCCAAAJWgCBjJagRQAABBBBAQJ0AQUZdl9FgBBBAAAEEELACBBkrQY0AAggggAAC6gQIMuq6jAYjgAACCCCAgBUgyFgJagQQQAABBBBQJ0CQUddlNBgBBBBAAAEErABBxkpQI4AAAggggIA6AYKMui6jwQgggAACCCBgBQgyVoIaAQQQQAABBNQJEGTUdRkNRgABBBBAAAErQJCxEtQIIIAAAgggoE6AIKOuy2gwAggggAACCFgBgoyVoEYAAQQQQAABdQIEGXVdRoMRQAABBBBAwAoQZKwENQIIIIAAAgioEyDIqOsyGowAAggggAACVoAgYyWoEUAAAQQQQECdAEFGXZfRYAQQQAABBBCwAgQZK0GNAAIIIIAAAuoECDLquowGI4AAAggggIAVIMhYCWoEEEAAAQQQUCdAkFHXZTQYAQQQQAABBKwAQcZKUCOAAAIIIICAOgGCjLouo8EIIIAAAgggYAUIMlaCGgEEEEAAAQTUCRBk1HUZDUYAAQQQQAABK0CQsRLUCCCAAAIIIKBOgCCjrstoMAIIIIAAAghYAYKMlaBGAAEEEEAAAXUCBBl1XUaDEUAAAQQQQMAKEGSsBDUCCCCAAAIIqBMgyKjrMhqMAAIIIIAAAlaAIGMlqBFAAAEEEEBAnQBBRl2X0WAEEEAAAQQQsAIEGStBjQACCCCAAALqBAgy6rqMBiOAAAIIIICAFSDIWAlqBBBAAAEEEFAnQJBR12U0GAEEEEAAAQSsAEHGSlAjgAACCCCAgDoBgoy6LqPBCCCAAAIIIGAFCDJWghoBBBBAAAEE1AkQZNR1GQ1GAAEEEEAAAStAkLES1AgggAACCCCgToAgo67LaDACOgW23HJLs+uuu5pddtkl+G+HHXYIdmTlypVG/vvll1/MokWLzK+//qpzB2k1AgiURIAgUxJ2v41uvfXWpm7dun4z/2+uP/74w6xatcp7/nxnlD9E2267behiGzZsMKtXrw6dJ6mJnTt3NltttZXX6qZPn27WrVvnNW+cmdq2bWsaN24cuoovvvjCfP7556HzRJnYvn17Y8NBruXnzJljli5dmmtyop/vtttupkePHqZdu3amXr16ZrPNNvNav3wfL1u2zMyfP9+88MILZsWKFV7LuWZq3bq12XPPPUNnW7JkiZk7d27oPFEnduzYMXAIW/6jjz4K9j3bPG3atDHNmjXLNknFZ/LzJz+HUho2bGgOO+wwZ7vl50R+XopdjjjiCLPddtuFbnbx4sXB92joTEwsigBBpijM0TZyww03mP3339974f/++8/06dPHrFmzxnuZfGY8//zzTdeuXZ2LDB482CxcuNA5X5wZGjVqZCZNmuS9ismTJxv5r9DlgQceMPIHPKx8/PHHZsSIEWGzRJr25JNPmvr164cuO2XKFDNx4sTQeeJMlCA1cOBAc8ABB+QVwsO2+f3335vnnnvOzJgxI2w257TbbrvNtGjRInQ++aN56aWXhs4TdaKEsjp16oQu/vjjj5unnnoq6zx33HGH2XvvvbNO0/DhX3/9Zbp37x40VfZD9sdV5MDopJNOMvK7rVilZcuWZvTo0c7NSeA+8cQTnfMxQ+EFCDKFN468hXyDjGzo7bff9vohjNIo3yAzZswYM3PmzCib8F7mjDPOCH7B+S7w3XffmUGDBvnOHnm+TTnISJ/IGRjfMy/5Iv/+++/m/vvvjxxoCDL5iic7f+UgI2seO3asad68uXMjU6dONRMmTHDOl9QMjzzyiNlxxx2dq3viiSeMHDxQSi9AkCl9H+RsQZQg888//5iePXsGl5lyrjjihDQFGTmrsNNOO3nviRzRydHg33//7b1MlBk3xSAjR7DDhg1zXnaM4pltmR9++MHccsst5uuvv842OednBJmcNEWZUDXINGjQwDz22GPO4Cu/00455ZSiXBqWy3+XX36500Mu4Z966qnO+ZihOAIEmeI4R9pKlCAjG3r99dfNPffcE2mbYQulJcjIvTpyiSTfI/9x48ZVXKMP28840za1IHPZZZeZo48+Og5ZpGXXrl1revfundeyBJm8uBKfuWqQkQ1ccMEFpkuXLs5tzZo1y8jvw0IX+b3ic9/d8OHDjdxvRkmHAEEmHf2QtRVRg4ycdZBT/EmffUhLkJEzK1EuEy1YsMAMGTIkq3VSH25KQUaOXOUIthTlt99+M7169cpr0wSZvLgSnzlbkKlRo0ZwUOK6d0jOqMqly+XLlyfeLrvCvn37GvnPVQp5H5Vr20zPLkCQye6Sik+jBhlp/LPPPmvkWm+SJS1B5q677jJ77bVX3ruW7Rdp3itxLLCpBJlLLrnEHHPMMQ6Nwk0myBTOtlBrzvXzJ99H8v3kKl9++aXXfK71ZJteu3btIFBtvvnm2SZXfCaBqn///sFQARUf8o+SCxBkSt4FuRsQJ8jIHfVyVibJu/3TEGTkctLLL79satasmRsuZMrQoUML+sjkphBkfC8HVO0Gefx23rx5wePFcp+L/Cf9KePK7LzzzsFj661atTLbbLNN1UWrfU2QqUaS+g9yBRlpuO89b1dccYX57LPPEt9X37OLb775prnzzjsT3z4rjCdAkInnV9Cl4wQZaZg8niyPrSZV0hBkDj74YCPXp6OW999/39x0001RF3cuV+5BRu5nkCDjWyRIy9goTz/9tPd4IHIT9wknnGCOOuooI2MpZSubYpDZb7/9Ej0L1qFDh2y0GZ/JWZCkLufIDbLy1Fm2IuP73H333dkmZXz2008/mQEDBmR8FveL7bff3shj76577uRRcLmcmfQl+7jtZ3ljCDIp/i6IG2TkCPjkk09ObA/TEGTSZlIVt5yDjNxkLYGkVq1aVXc769cyWu9VV11l5NH3qEWeDJEnVqqe8t8Ug0xUw1zLvfLKK84zmzLWS9zxe3Jtv+rnvj/bt99+u3nrrbeqLh7561tvvdXIYImuIpe033jjDddsTC+BAEGmBOi+m/T9wQ5bnxzlTJs2LWwW72lpCDKuQcVk8DTXyLpnnnlmcFnDe8fzmLGcg0w+34/yh0b+4CRR5P4FuYdCRlu1hSBjJaLXaQsycvZNgrLrsvH69euDISai7/nGJX3PBMlZKRnokZJOAYJMOvslaFU+fzhy7UaS4x2UOsjIDb5yVBRW5B6Ym2++OfQ0sfwCHz9+fNhqIk8r1yAj966MGjXKy+W9994L+sBr5jxmkiAjT53JJQCCTB5wOWZNW5CRZvbr1y84A5ejyRUfy+jHcjkobnnwwQedBz6yjYsuuih4D1jc7bF8YQQIMoVxTWStSQQZaYgMHvbuu+/GblOpg4wMHd+pU6ec+yHXrrt16xaMAiovJ8xVfv755+DJg1zT43xerkFGjpRd79kSt0KPoNy0adPgTI/czJ7vgGTaH7+O832Zbdk0Bhlp5zPPPOO84Vt+1uWyuXwfRC0HHXSQufbaa52Ly3gxce7Lc26AGWILEGRiExZuBUkFmaT+cJc6yLjeJSQjvV544YXmrLPOcr4DRX4JFuIlkuUYZORMiDwt4io2XMirBApZZMAyuSFYXtqXTyHIZGqlNcj4Boy4r2Nx/T4RLQlMEpjlDCAlvQIEmfT2TTCSpc9LI3/88UcjL1EMK9dcc03st/qWMsjIkwXybpOwYl+416RJk5xPR9jlk36iy663HIOMjBK9xx572F3MWT/66KPB0XTOGUo8gSCT2QFpDTLSSp/vuThjusjLHuWAx1XkTKS8RoGSbgGCTIr7x/eMjLyk0fV+ELkJ9uyzz461t6UMMqeddlrwZu+wHZAjJ7knSIrrpuBvvvnGyP4kXcotyMgNmDK4ouvRVDkLI28pTnMhyGT2TpqDjJxxe/jhh53fd1FG65Yn4GRYii222CITpMpXa9as8bpfp8pifFkCAYJMCdB9N+kbZC6++OLgZjS5Az+syD0mMrx21FLKICNvvw2776XqzZ+uRyrlaO64445LdMBAcS23IHP66ad7PcIvo0hL4ElzIchk9k6ag4y0VC5nVn5SLbP1G7+S339fffXVxg8c/zrvvPOCn33HbGbkyJFm9uzZrtmYngIBgkwKOiFXE/IJMnKUMXbs2FyrCj6395CEzhQysVRBRsYtefHFF0OPzj788ENz/fXXV7T+2GOPDe6Xqfggyz9Gjx5t5Dp7kqXcgozPfQTyduLjjz8+ScaCrIsgk8ma9iAjj93LmRPXuEUyQrQMqeBT5IZ1eeLJdYYx7u9Kn7YwT3ICBJnkLBNfUz5BRo5IXGctpIHnnHOOWbp0aaS2lirI+IQSGa1XRu21RU4by+WlsF9Y8+fPN/K4dpKlnIKMvDrgoYcecvLITbf5jPbrXGGBZiDIZMKmPchIa33vZfF9MvO6664zBx54YCZEla/kbG2hX1BZZZN8GVOAIBMTsJCL5xtk5MZgWSasyHtKfJ5AybaOUgUZOdPUvHnzbE0KPst1mUhu/m3QoEHO5f78889gKPycM0SYUE5BRu55kV/orqLhspLsA0Emsyc1BBlpsevnWOZZu3at6d27t/wzZ5FL0zJuTNjBjSw8c+ZMI/cdUvQIEGRS3Ff5BhnZFXlypGHDhjn3Ks6d/qUKMvKSyKpD1FfewWXLlmV9AsE17oysY/DgwWbhwoWVVxfr3+UUZHyOXgVLXiEgN0amvRBkMntIS5Bp2bKlkcvAruJ6ak5GOXfdRygHNzI0g7zgkqJHgCCT4r6KEmTkRXDyfpuw8sknn3gNBFV1HaUIMj5nmaZOnRpcVqva3hYtWgRH4VU/r/x13LEoKq9L/l1OQcYVimV/NT3ZQZCRHttYtAQZabEEGQk0YUXCR8+ePbOGkH333TcYGDRseZl23333mVdffdU1G9NTJkCQSVmHVG5OlCAjy7tu0JSzMn369Mn7KLoUQUZG3pQBssLKoEGDcr6Y0HU2J+k/xOUUZHz+0MlTcHLmS0MhyGT2kk//FvOlkZmty/yqfv36ZvLkyc7LQtOnTzfjxo3LXPh/X/mE8qQGDq22cT4ouABBpuDE0TcQNch06dLFefPlO++8Y+QR5XxKKYLM888/b+Sty7nKhg0bTI8ePXJNDt7NJO9oCiv9+/c38kssiVIuQUbG8Zg4caKTJMmXQzo3FnMGgkwmoKYgIy33eWxaDtJkPKnVq1dX7Gznzp2NPKLtKnGHp3Ctn+mFEyDIFM429pqjBhnZ8JQpU4wM5Z6ryCOzvXr1MvkMJ1/sIOMzQu+nn34aeilN9nHAgAG5GILPJSzJ4FtJlHIJMt27dzdypstVZH9feukl12ypmO4TZCTQys9OIYqMJBt2r5ds045OXYjtV12ntiBTo0aN4HHssAMb2ce5c+caGclcitzYK/3pWmbevHnm6quvDpbhf/oECDIp7rM4QcbnD/i0adOM3ADnW4odZOSRXjm7FFak/bIfuUq9evWCcSNyTZfPly9fbgYOHBg2i/e0cgky5557rtfYMPkORuYNWYAZfYJMATab1yoJMuFcHTt2dI5iLmuQ799vv/02OIiR34VhRQ7q+vbtm/el9rB1Mq24AgSZ4nrntbU4QUaORGQclbBhuOWFaHJZRmqfUuwg43rsUk4jS/tdb8B1vbnZdz0+RuUSZHxHVZXRkf/9918fmpLPQ5DJ7AJtZ2Rs62VsIxnjKKzIWFlyqUjepO06CyZnbHwuo4Ztj2mlFSDIlNY/dOtxgoys2Gd4+Xx+iIsZZGQETgkgYWXFihVG3sHkKsOGDTOHHHJI6GxJXSIplyDj870nAbBr166hrmmaSJDJ7A2tQWb33Xc39957b+bOZPlK3i/XuHHjLFM2frRu3brgErt8L1P0ChBkUtx3Pn9MpPm5Tu/LkYiclQk7IpGzGXJWw+cHuZhBRsYm6devX2jvzJgxw8hTFa7Srl07M2LEiNDZFi1aFLyvKnQmj4nlEmRcgxAKhZzJ69atm4dKOmYhyGT2g9YgI3vhO8ZR5h5X/6rqiODV5+ATDQIEmRT3UtwgI7vmc5+JayApS1TMIDN+/HjTtGlTu+mste9gdnKZTcaGCBvRM6k/yuUSZGQ8jWbNmmV1tx8WYmRku+5C1ASZTFXNQUYeZJAztmEHaZl7W/2rJUuWBE9CVZ/CJ9oECDIp7rEkgozcrS9vJa5Zs2bOPV2/fn0wkFTOGf5/QrGCjPxykidhkgweMjS56zSzeM+aNcvFEDq9XILMpEmTTKNGjUL3VZ54k9cYaCkEmcye0hxkZE/kBl35L0qRM9DyFJm8cJKiX4Agk+I+TCLIyO5deeWV5vDDDw/dU7nm/Nprr4XOU6wg4/NkQr6XgnzuF5ozZ44ZPnx4qIFrYrkEGdegiuIQdzBBORPoeizW5S1hSsYB8ikEmUwl7UFG9sZ1I3/mHm/8Kso4WhuX5l9pEyDIpK1HKrUnqSDj8+p6nz9KxQoyMlBf69atK0lU/2e+Lyr0eZOza3C96q2o/km5BBk5i1e3bt3qO1jpk5UrV0Y+IpbVSHAOO+tWaVM5/5nPDcc+QUZu/pQxRQpRDj30UOdqefzaSZQxQ9u2bc3IkSMzPnN9Ia8ykPcpyaVRSnkIEGRS3I9JBRnZRflhlx/6sCIBQo5UcpViBRm5rFSrVq1czQg+j/KiQtcowbJiuado8eLFodsOm1guQaYYZ2TSGGQK+coFufG+Tp06Yd8+DIgXqpN9os/LICsvOWHCBCPvZ6OUjwBBJsV9mWSQkTdiy1mMsCNg17tGihFkWrVqZUaNGhXaKz5nj7KtQJ5QaNOmTbZJFZ/5PglVsUCVf5RLkJFxNeQ1BWEl7j0yBJnqupyRqW7i+kS+T2Vk7rDfbXYdvkM22PmpdQgQZFLcT0kGGdlNCQgSFMKK3CMi94pkK8UIMkOHDjXt27fPtvmKzz744ANz4403Vnzt+49OnTo5X3C4atWq4F0tvuusOl+5BBm5Z0rG6wgrcZ9aIshU1yXIVDfx+WTIkCHmyCOPdM4q8y1YsMA5HzPoEiDIpLi/kg4yPu8uWrZsWXA3fzaWYgQZn3sz5DLZ7NmzszUx9LPatWsHp5RdR27y0jkJNFFKuQSZMWPGmH322SeUIO4j6wSZ6rwEmeomPp906NAh9J1rso587qfy2SbzpEfg/wAAAP//mlRinwAAF9lJREFU7d0HjDVl1QfwBxsIKgpSgoABUUJAjYqKITZiQUVADCoajEGxICUWFGIUsVDEAhYUpNpQsEQsVDFRE0CCJiAaVEjEBhpFQSFWvu/c79t1d9+9M3Puu3f3LP4mMbv3zrkz5/6elft/5848s87WW299V7OUFHj3u9/dHve4x/X2dthhh7Wf/exnvXVR8OEPf7htt912nbVveMMb2vXXX79Gzetf//r2vOc9b43nFz7x/ve/v1122WULn+59vPnmm7czzjijs+6uu+5qe+yxR4ufkyxnn31222STTTpfes4557RPf/rTnTXjVp5yyiltq622Grd69PxVV13VjjrqqM6aSVZ+7nOfaw984AM7X/rFL36x1zg2cPTRR7fHP/7xnduKMRjy9zBuI9/4xjfaOuusM271oOczPZxwwgltxx137Nxu/N3H3/80li9/+cttvfXW69x0/N3F399yLF/72tfaPe95z85dfehDH2qXXHJJZ02FlU95ylPaEUcc0dlK5m+lc0NWlhNYR5ApNyazDU0jyGy//fYt/uPUtdx4443t4IMPXqNk2kHmNa95Tdtrr73W2O/cJ371q1+1V7/61XOfSv1+yCGHtOc85zmdr/n1r3/dDjzwwM6acSvvLkHm8MMPb09/+tPHvc3Z5yNU/vvf/559nPklglffB3vf+syHkyAzf3QEmfkeHq1eAUGm8NhNI8jE2z311FPblltu2fnOX/va17abbrppXs20g8xZZ53VNt1003n7XPjgvPPOa2eeeebCpwc/HhLk4sNx7733bv/4xz8Gb3em8O4SZGL899xzz5m3NfZn5mjg2I10rPjSl77U7nvf+46tEGTG0vSuEGR6iRSsEgFBpvBATSvIPPaxj23vec97Ot/5j3/84/bmN795Xs00g8z666/f4muPvuVvf/tbu/POO/vKOtf3ff0SL/7IRz7SLrjggs7tLLby4x//eHvoQx+62KrZ51byq6Vzzz23RWDsW+KoVRy96ltOPvnk9vWvf72vbOL1gszEdL0vFGR6iRSsEgFBpvBATSvIxFvuO/oR/9I94IAD2i233DIrNM0gs88++7RXvepVs/ta6V9+8pOftDe96U3pNj760Y+2bbfdtvN1KxlkPvvZz7b4X9+y8cYbDzpP6KKLLmonnXRS3+YmXi/ITEzX+0JBppdIwSoREGQKD9Q0g8yTn/zkduSRR3a++6uvvrq9/e1vn62ZZpCJIyAPe9jDZve10r/885//HPTVysI+h5xMvZJB5lOf+lT7/Oc/v7DtRR+ff/757V73utei62aeXOzI3cy6pfgpyCyF4uLbEGQWd/Hs6hMQZAqP2TSDTLztvqtc4qjMfvvt12677baR0rSCTFy5Eh+afVdQLPdQRYiLMJdZ4kTqOA+na1kYELtqM+v6xjO2dfrpp7cIB0OWOBdps8026yy99dZb28te9rLOmrVZKcisjV73awWZbh9rV4+AIFN4rKYdZHbfffd26KGHdgp897vfbccee+yoZlpBZsjRoc4mp7TyiiuuaO9617tSWz/++OPbIx/5yM7X/PCHP2xve9vbOmsmWRlHWh7wgAd0vjTO4YkPsCHLO97xjrbLLrv0lr7whS9c6/OWxu1EkBkns/bPCzJrb2gLNQQEmRrjsGgX0w4ysdO4CmiDDTZYdP/xZByV2Xfffdsdd9zRphVk3vve97bHPOYxY3tYqRVxUnF8SGeWIR/+11xzTe+cF5l9ztR+4QtfaPe///1nHi7687jjjmvf+c53Fl238MkXvOAFgy5Dn+YJv4LMwlFZuseCzNJZ2tLKCggyK+vfufflCDLxQf3KV76ys4+ZEzqnFWS+8pWvtHXXXbezh5VaGXPb/PKXvxy8+5hM7ZnPfGZn/XXXXddinpalXvpCaewvJg2LIDVkia+VhlzqPs3zZASZISM1WY0gM5mbV9UTEGTqjclsR8sRZOL8lJhxtCtIxImvEXhiIrohM7lmZvYdMq9LgNx8882zLkv1S8zw23dezje/+c0WVyINXSIU9h3F+fnPf977ld7Q/c2tGzJzbDaYxUyzcQVT1/L3v/99NO9OV82k6wSZSeX6XyfI9BupWB0CgkzhcVqOIBNv/xWveEV70Yte1CkxE3aWOsjEXDW77bZb575/97vfjXrsLJpg5Tvf+c72hCc8ofOVf/jDH9r+++/fWTN35fOf//z2ute9bu5Ta/z+m9/8ZiqXmg/5YIqvi2IunqFLnMg75GTeuAQ7jtwt9SLILLXof7Y35O/FLQr+4+W3ugKCTN2xacsVZOIS2wgqXZfaxr+6L7300vbc5z63VyxzRGbICaoxMV1cnr3Uy9CTjCPk/eUvfxm0+5122qm9733v66z905/+1F760pd21kyyMo4edS3/+te/WgStzBKz6sZEhX33RIor217ykpdkNj2oVpAZxDRRkSAzEZsXFRQQZAoOykxLyxVkYn9Dzn/54x//2DbaaKOZ9sb+HBpkHvzgB7eY16RvmdY0+Pe4xz1GV/D0fUhn5l6JD/748O1aMtPqd21n7rqHP/zhvRPT/fWvfx2duD33dUN+HzI3Tmxn6LgP2edMjSAzI7H0PwWZpTe1xZUREGRWxn3QXpczyMQ5MvEv775zRoY0PvQDbchXWnG/o74bSQ7paVzNJz/5yfaQhzxk3OrR87/4xS96vy6au4EhE8mNu8P43O1kfh9ybk7cOyvuoZRdhtxZOLZ5++23j76GinOqlmoRZJZKcs3tCDJrmnhmdQoIMoXHbTmDTDAMveNxH9nQIHPaaae1LbbYonNz119/fYsP/WktQwJAHEGJr2SG3uV5SDi69tpr21vf+tYle1tDrlham6/oPvOZzww6GrfU4yXILNmfyBobEmTWIPHEKhUQZAoP3HIHmZhM7Zxzzuk9H6KP7IMf/ODofJquujgCFOfl9H2tk5mJtmt/49Ztvvnm7Ywzzhi3evb5oeEsXhA3W4ybLvYtmW12bSsC0VOf+tSuktG6mIQvJuObZBl6dVlsO3ulV1c/gkyXztqtE2TWzs+r6wgIMnXGYo1OljvIRANHHXVUe+ITn7hGL5knhgSZPfbYox100EG9m40TSGdukdBbPGFB34dlbDYziV3cXTtuzNgX0uLk2wMPPHCtLi1/xjOe0d74xjf2vvM4WbnvyrS+jcQtG570pCf1lY3WX3bZZe0DH/jAaELFQS8YU9Q3NpnzjU444YS24447jtnT/z291EeU5u5syOXxcbl7/GNiORZBZjmU7WM5BASZ5VCecB8rEWTiBNyzzz6790O46y0NCTJD7kk0rSthFvY+ZGbh7FwpQ95f9BHnk8Q8NRdffPHCtnofx0nQz372s3vromDoXa+7Nnaf+9ynnXvuuS1+Dlni6qy4MWmcY5Rdttpqq9FXndttt13nSwWZTp7OlYJMJ4+Vq0hAkCk8WCsRZIIjprF/1KMeNbHMkCAz5ITY733ve+2YY46ZuI+hL4x5bGI+m77lLW95S/vRj37UVzZav8MOO4yOSAwq/t+in/70py1uMXD55Zd3vuTe97736BL4OLryoAc9qLN2ZmWEpX322WcUmmaem/RnzFqcOWcpgsYNN9wwuinot771rd4jNDH5XozFox/96EEtCjKDmBYtEmQWZfHkKhQQZAoP2koFmfjX8CmnnDKxTF+QiUnoYjK6viVqvv/97/eVrfX6mD/nq1/9au9RqLk30Byy0zjSsu222w4pna2J0HHjjTe2mIgvJgKMr4Tikvc4Uhbn82y55Za9fc5u7P9/iaMoZ5111sKnJ3788pe/fKI5Y+K9/fa3v21xGf/vf//70c8NN9xwdML3pptu2uL39dZbL9WXIJPimlcsyMzj8GAVCwgyhQdvpYJMkJx44ontEY94xEQ6fUEmAkrfjLqZD6iJmlzwojjhN4JC15I9zyROaI5zHu53v/t1bXaq6zLn9mQaGXK1V2Z7k9ZmJvlzjsx8ZUFmvodHq1dAkCk8disZZIZMsDaOri/I9J3AGduNGzXGfYGWa4n5Vfbcc8/e3R1wwAGpk3Pj8vJPfOITnbMm9+50woI46hFz9UQonMYy1Gwa+45txhGe+P/IVVddNWgXgsx8JkFmvodHq1dAkCk8disZZIIlvl6Kr5myS1eQ2WabbdrHPvax3k0u9dchfTvceuutR4Gjry6+gsp+7Rbnexx99NGDT5Lt62HI+ggxcRn4tK/4imAXN8nsu0JrSM+ZmltuuWV0rk6cUDx0EWTmSwky8z08Wr0CgkzhsVvpIBMfwMcee2xaqCvIDL3SJnvkI93kIi8YcnnspDewjK+X4iTq7Dkzi7TZ+1RcARVfDS7XEkedIqj1zZC8FP3E0aU4aTj+xrKLIDNfTJCZ7+HR6hUQZAqP3UoHmaA588wz22abbZZS6goycRlw39U2d9555+hf+amdLkHxkKu14oN03333bXfcccdEe3zxi188OlE2zp9Z6uXWW28dfcBfffXVS73pQduLO6PH14FdNx8dtKFFisI9TvyOu2xnjsLM3ZQgM1ejje4z1ndLEne/nm/mUU0BQabmuIy6ijk44g7Nfcv+++8/usqlr26S9bvuumuLGWEzS1eQGXLZdcw+m91npr9xtbvvvns79NBDx62efT5zGfbsixb8EvePilATk+et7RLnE5166qltpQLM3P4joMVkh3GZdnwtubZfOUWo/cEPfjCaa+fPf/7z3F2lfx/yD4Mrr7xydHQpvfEBLxgS4pdqtucB7Yxubho3Oe1aYtbouJ1G9WXIzNMRhiNsW+5+AoLM3W9MvaNVJBDn5jztaU9rO++88+iDv+9ITfzHOO5iHXOzxBGKb3/72xMfoZg2U8x5ExP2PetZz2pxeXV8aMbRmnHhJt5b3CT05ptvbldccUW78MILUydWT/v92D4BAjUFBJma46Kr/2KB9ddffzRvzCabbNI22GCDUVCJc3Nibpn4oF/tS7ynmA8nwk18RRfBJU7ejauQLAQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgKCDJZMfUECBAgQIBAGQFBpsxQaIQAAQIECBDICggyWTH1BAgQIECAQBkBQabMUGiEAAECBAgQyAoIMlkx9QQIECBAgEAZAUGmzFBohAABAgQIEMgK/A951lPeQIDuUAAAAABJRU5ErkJggg=='\n"
  },
  {
    "path": "packages/dev-env/src/mock/index.ts",
    "content": "import { AtpAgent, COM_ATPROTO_MODERATION } from '@atproto/api'\nimport { Database } from '@atproto/bsky'\nimport { AtUri } from '@atproto/syntax'\nimport { EXAMPLE_LABELER, RecordRef, TestNetwork } from '../index'\nimport { postTexts, replyTexts } from './data'\nimport blurHashB64 from './img/blur-hash-avatar-b64'\nimport labeledImgB64 from './img/labeled-img-b64'\n\n// NOTE\n// deterministic date generator\n// we use this to ensure the mock dataset is always the same\n// which is very useful when testing\n// (not everything is currently deterministic but it could be)\nfunction* dateGen(): Generator<string, never> {\n  let start = 1657846031914\n  while (true) {\n    yield new Date(start).toISOString()\n    start += 1e3\n  }\n}\n\nexport async function generateMockSetup(env: TestNetwork) {\n  const date = dateGen()\n\n  const rand = (n: number) => Math.floor(Math.random() * n)\n  const picka = <T>(arr: Array<T>): T => {\n    if (arr.length) {\n      return arr[rand(arr.length)] || arr[0]\n    }\n    throw new Error('Not found')\n  }\n\n  const users = [\n    {\n      email: 'alice@test.com',\n      handle: `alice.test`,\n      password: 'hunter2',\n      displayName: 'Alice',\n      description: 'Test user 0',\n    },\n    {\n      email: 'bob@test.com',\n      handle: `bob.test`,\n      password: 'hunter2',\n      displayName: 'Bob',\n      description: 'Test user 1',\n    },\n    {\n      email: 'carla@test.com',\n      handle: `carla.test`,\n      password: 'hunter2',\n      displayName: 'Carla',\n      description: 'Test user 2',\n    },\n    {\n      email: 'triage@test.com',\n      handle: 'triage.test',\n      password: 'triage-pass',\n    },\n    {\n      email: 'mod@test.com',\n      handle: 'mod.test',\n      password: 'mod-pass',\n    },\n    {\n      email: 'admin-mod@test.com',\n      handle: 'admin-mod.test',\n      password: 'admin-mod-pass',\n    },\n    {\n      email: 'labeler@test.com',\n      handle: 'labeler.test',\n      password: 'hunter2',\n      displayName: 'Test Labeler',\n      description: 'Labeling things across the atmosphere',\n    },\n  ]\n\n  const userAgents = await Promise.all(\n    users.map(async (user) => {\n      const client: AtpAgent = env.pds.getClient()\n      await client.createAccount(user)\n      client.assertAuthenticated()\n      if (user.displayName || user.description) {\n        await client.app.bsky.actor.profile.create(\n          { repo: client.did },\n          {\n            displayName: user.displayName,\n            description: user.description,\n          },\n        )\n      }\n      return client\n    }),\n  )\n\n  const [alice, bob, carla, triage, mod, adminMod, labeler] = userAgents\n\n  // Create chat declarations for all users\n  for (const user of userAgents) {\n    await user.chat.bsky.actor.declaration.create(\n      { repo: user.did },\n      { allowIncoming: 'all' },\n    )\n  }\n\n  // Add moderator roles\n  await env.ozone.addTriageDid(triage.did)\n  await env.ozone.addModeratorDid(mod.did)\n  await env.ozone.addAdminDid(adminMod.did)\n\n  // Report one user\n  const reporter = picka(userAgents)\n  await reporter.com.atproto.moderation.createReport({\n    reasonType: picka([\n      COM_ATPROTO_MODERATION.DefsReasonSpam,\n      COM_ATPROTO_MODERATION.DefsReasonOther,\n    ]),\n    reason: picka([\"Didn't look right to me\", undefined, undefined]),\n    subject: {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: picka(userAgents).did,\n    },\n  })\n\n  // everybody follows everybody\n  const follow = async (author: AtpAgent, subject: AtpAgent) => {\n    await author.app.bsky.graph.follow.create(\n      { repo: author.assertDid },\n      {\n        subject: subject.assertDid,\n        createdAt: date.next().value,\n      },\n    )\n  }\n  await follow(alice, bob)\n  await follow(alice, carla)\n  await follow(bob, alice)\n  await follow(bob, carla)\n  await follow(carla, alice)\n  await follow(carla, bob)\n\n  // a set of posts and reposts\n  const posts: { uri: string; cid: string }[] = []\n  for (let i = 0; i < postTexts.length; i++) {\n    const author = picka(userAgents)\n    const post = await author.app.bsky.feed.post.create(\n      { repo: author.did },\n      {\n        text: postTexts[i],\n        createdAt: date.next().value,\n      },\n    )\n    posts.push(post)\n    if (rand(10) === 0) {\n      const reposter = picka(userAgents)\n      await reposter.app.bsky.feed.repost.create(\n        { repo: reposter.did },\n        {\n          subject: picka(posts),\n          createdAt: date.next().value,\n        },\n      )\n    }\n    if (rand(6) === 0) {\n      const reporter = picka(userAgents)\n      await reporter.com.atproto.moderation.createReport({\n        reasonType: picka([\n          COM_ATPROTO_MODERATION.DefsReasonSpam,\n          COM_ATPROTO_MODERATION.DefsReasonOther,\n        ]),\n        reason: picka([\"Didn't look right to me\", undefined, undefined]),\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.uri,\n          cid: post.cid,\n        },\n      })\n    }\n  }\n\n  // make some naughty posts & label them\n  const file = Buffer.from(labeledImgB64, 'base64')\n  const uploadedImg = await bob.com.atproto.repo.uploadBlob(file, {\n    encoding: 'image/png',\n  })\n  const labeledPost = await bob.app.bsky.feed.post.create(\n    { repo: bob.assertDid },\n    {\n      text: 'naughty post',\n      embed: {\n        $type: 'app.bsky.embed.images',\n        images: [\n          {\n            image: uploadedImg.data.blob,\n            alt: 'naughty naughty',\n          },\n        ],\n      },\n      createdAt: date.next().value,\n    },\n  )\n\n  const filteredPost = await bob.app.bsky.feed.post.create(\n    { repo: bob.assertDid },\n    {\n      text: 'really bad post should be deleted',\n      createdAt: date.next().value,\n    },\n  )\n\n  await createLabel(env.bsky.db, {\n    uri: labeledPost.uri,\n    cid: labeledPost.cid,\n    val: 'nudity',\n  })\n  await createLabel(env.bsky.db, {\n    uri: filteredPost.uri,\n    cid: filteredPost.cid,\n    val: 'dmca-violation',\n  })\n\n  // a set of replies\n  for (let i = 0; i < 100; i++) {\n    const targetUri = picka(posts).uri\n    const urip = new AtUri(targetUri)\n    const target = await alice.app.bsky.feed.post.get({\n      repo: urip.host,\n      rkey: urip.rkey,\n    })\n    const author = picka(userAgents)\n    posts.push(\n      await author.app.bsky.feed.post.create(\n        { repo: author.did },\n        {\n          text: picka(replyTexts),\n          reply: {\n            root: target.value.reply ? target.value.reply.root : target,\n            parent: target,\n          },\n          createdAt: date.next().value,\n        },\n      ),\n    )\n  }\n\n  // a set of likes\n  for (const post of posts) {\n    for (const user of userAgents) {\n      if (rand(3) === 0) {\n        await user.app.bsky.feed.like.create(\n          { repo: user.did },\n          {\n            subject: post,\n            createdAt: date.next().value,\n          },\n        )\n      }\n    }\n  }\n\n  // a couple feed generators that returns some posts\n  const fg1Uri = AtUri.make(\n    alice.assertDid,\n    'app.bsky.feed.generator',\n    'alice-favs',\n  )\n  const fg1 = await env.createFeedGen({\n    [fg1Uri.toString()]: async () => {\n      const feed = posts\n        .filter(() => rand(2) === 0)\n        .map((post) => ({ post: post.uri }))\n      return {\n        encoding: 'application/json',\n        body: {\n          feed,\n        },\n      }\n    },\n  })\n  const avatarImg = Buffer.from(blurHashB64, 'base64')\n  const avatarRes = await alice.com.atproto.repo.uploadBlob(avatarImg, {\n    encoding: 'image/png',\n  })\n  const fgAliceRes = await alice.app.bsky.feed.generator.create(\n    { repo: alice.assertDid, rkey: fg1Uri.rkey },\n    {\n      did: fg1.did,\n      displayName: 'alices feed',\n      description: 'all my fav stuff',\n      avatar: avatarRes.data.blob,\n      createdAt: date.next().value,\n    },\n  )\n\n  await alice.app.bsky.feed.post.create(\n    { repo: alice.assertDid },\n    {\n      text: 'check out my algorithm!',\n      embed: {\n        $type: 'app.bsky.embed.record',\n        record: fgAliceRes,\n      },\n      createdAt: date.next().value,\n    },\n  )\n  for (const user of [alice, bob, carla]) {\n    await user.app.bsky.feed.like.create(\n      { repo: user.did },\n      {\n        subject: fgAliceRes,\n        createdAt: date.next().value,\n      },\n    )\n  }\n\n  const fg2Uri = AtUri.make(\n    bob.assertDid,\n    'app.bsky.feed.generator',\n    'bob-redux',\n  )\n  const fg2 = await env.createFeedGen({\n    [fg2Uri.toString()]: async () => {\n      const feed = posts\n        .filter(() => rand(2) === 0)\n        .map((post) => ({ post: post.uri }))\n      return {\n        encoding: 'application/json',\n        body: {\n          feed,\n        },\n      }\n    },\n  })\n  const fgBobRes = await bob.app.bsky.feed.generator.create(\n    { repo: bob.assertDid, rkey: fg2Uri.rkey },\n    {\n      did: fg2.did,\n      displayName: 'Bobby boy hot new algo',\n      createdAt: date.next().value,\n    },\n  )\n\n  await alice.app.bsky.feed.post.create(\n    { repo: alice.assertDid },\n    {\n      text: `bobs feed is neat too`,\n      embed: {\n        $type: 'app.bsky.embed.record',\n        record: fgBobRes,\n      },\n      createdAt: date.next().value,\n    },\n  )\n\n  const fg3Uri = AtUri.make(\n    carla.assertDid,\n    'app.bsky.feed.generator',\n    'carla-intr-algo',\n  )\n  const fg3 = await env.createFeedGen({\n    [fg3Uri.toString()]: async () => {\n      const feed = posts\n        .filter(() => rand(2) === 0)\n        .map((post) => ({ post: post.uri }))\n      return {\n        encoding: 'application/json',\n        body: {\n          feed,\n        },\n      }\n    },\n  })\n  const fgCarlaRes = await carla.app.bsky.feed.generator.create(\n    { repo: carla.assertDid, rkey: fg3Uri.rkey },\n    {\n      did: fg3.did,\n      displayName: `Acceptin' Generator`,\n      acceptsInteractions: true,\n      createdAt: date.next().value,\n    },\n  )\n\n  await alice.app.bsky.feed.post.create(\n    { repo: alice.assertDid },\n    {\n      text: `carla accepts interactions on her feed`,\n      embed: {\n        $type: 'app.bsky.embed.record',\n        record: fgCarlaRes,\n      },\n      createdAt: date.next().value,\n    },\n  )\n\n  // create labeler service\n  {\n    await labeler.app.bsky.labeler.service.create(\n      { repo: labeler.did, rkey: 'self' },\n      {\n        policies: {\n          labelValues: [\n            '!hide',\n            'porn',\n            'rude',\n            'spam',\n            'spider',\n            'misinfo',\n            'cool',\n            'curate',\n          ],\n          labelValueDefinitions: [\n            {\n              identifier: 'rude',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'warn',\n              adultOnly: true,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Rude',\n                  description: 'Just such a jerk, you wouldnt believe it.',\n                },\n              ],\n            },\n            {\n              identifier: 'spam',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Spam',\n                  description:\n                    'Low quality posts that dont add to the conversation.',\n                },\n              ],\n            },\n            {\n              identifier: 'spider',\n              blurs: 'media',\n              severity: 'alert',\n              defaultSetting: 'warn',\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Spider!',\n                  description: 'Oh no its a spider.',\n                },\n              ],\n            },\n            {\n              identifier: 'cool',\n              blurs: 'none',\n              severity: 'inform',\n              defaultSetting: 'warn',\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Cool',\n                  description: 'The coolest peeps in the atmosphere.',\n                },\n              ],\n            },\n            {\n              identifier: 'curate',\n              blurs: 'none',\n              severity: 'none',\n              defaultSetting: 'warn',\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Curation filter',\n                  description: 'We just dont want to see it as much.',\n                },\n              ],\n            },\n          ],\n        },\n        createdAt: date.next().value,\n      },\n    )\n    await createLabel(env.bsky.db, {\n      uri: alice.assertDid,\n      cid: '',\n      val: 'rude',\n      src: labeler.did,\n    })\n    await createLabel(env.bsky.db, {\n      uri: `at://${alice.assertDid}/app.bsky.feed.generator/alice-favs`,\n      cid: '',\n      val: 'cool',\n      src: labeler.did,\n    })\n    await createLabel(env.bsky.db, {\n      uri: bob.assertDid,\n      cid: '',\n      val: 'cool',\n      src: labeler.did,\n    })\n    await createLabel(env.bsky.db, {\n      uri: carla.assertDid,\n      cid: '',\n      val: 'spam',\n      src: labeler.did,\n    })\n  }\n\n  // Create lists and add people to the lists\n  {\n    const flowerLovers = await alice.app.bsky.graph.list.create(\n      { repo: alice.assertDid },\n      {\n        name: 'Flower Lovers',\n        purpose: 'app.bsky.graph.defs#curatelist',\n        createdAt: new Date().toISOString(),\n        description: 'A list of posts about flowers',\n      },\n    )\n    const labelHaters = await bob.app.bsky.graph.list.create(\n      { repo: bob.assertDid },\n      {\n        name: 'Label Haters',\n        purpose: 'app.bsky.graph.defs#modlist',\n        createdAt: new Date().toISOString(),\n        description: 'A list of people who hate labels',\n      },\n    )\n    await alice.app.bsky.graph.listitem.create(\n      { repo: alice.assertDid },\n      {\n        subject: bob.assertDid,\n        createdAt: new Date().toISOString(),\n        list: new RecordRef(flowerLovers.uri, flowerLovers.cid).uriStr,\n      },\n    )\n    await bob.app.bsky.graph.listitem.create(\n      { repo: bob.assertDid },\n      {\n        subject: alice.assertDid,\n        createdAt: new Date().toISOString(),\n        list: new RecordRef(labelHaters.uri, labelHaters.cid).uriStr,\n      },\n    )\n  }\n\n  await setVerifier(env.bsky.db, alice.assertDid)\n\n  // @TODO These are useful when testing complex threads, but don't need to be enabled all the time. We could make it configurable.\n  // import * as seedThreadV2 from '../seed/thread-v2'\n  // const sc = env.getSeedClient()\n  // await seedThreadV2.simple(sc)\n  // await seedThreadV2.long(sc)\n  // await seedThreadV2.deep(sc)\n  // await seedThreadV2.branchingFactor(sc)\n  // await seedThreadV2.annotateMoreReplies(sc)\n  // await seedThreadV2.annotateOP(sc)\n  // await seedThreadV2.sort(sc)\n  // await seedThreadV2.bumpOpAndViewer(sc)\n  // await seedThreadV2.bumpGroupSorting(sc)\n  // await seedThreadV2.bumpFollows(sc)\n  // await seedThreadV2.blockDeletionAuth(sc, env.bsky.ctx.cfg.modServiceDid)\n  // await seedThreadV2.mutes(sc)\n  // await seedThreadV2.threadgated(sc)\n  // await seedThreadV2.tags(sc)\n}\n\nconst createLabel = async (\n  db: Database,\n  opts: { uri: string; cid: string; val: string; src?: string },\n) => {\n  await db.db\n    .insertInto('label')\n    .values({\n      uri: opts.uri,\n      cid: opts.cid,\n      val: opts.val,\n      cts: new Date().toISOString(),\n      neg: false,\n      src: opts.src ?? EXAMPLE_LABELER,\n    })\n    .execute()\n}\n\nconst setVerifier = async (db: Database, did: string) => {\n  await db.db\n    .updateTable('actor')\n    .set({ trustedVerifier: true })\n    .where('did', '=', did)\n    .execute()\n}\n"
  },
  {
    "path": "packages/dev-env/src/moderator-client.ts",
    "content": "import {\n  AtpAgent,\n  ToolsOzoneModerationDefs,\n  ToolsOzoneModerationEmitEvent as EmitModerationEvent,\n  ToolsOzoneModerationQueryEvents as QueryModerationEvents,\n  ToolsOzoneModerationQueryStatuses as QueryModerationStatuses,\n  ToolsOzoneSettingRemoveOptions,\n  ToolsOzoneSettingUpsertOption,\n} from '@atproto/api'\nimport { TestOzone } from './ozone'\n\ntype TakeActionInput = EmitModerationEvent.InputSchema\ntype QueryStatusesParams = QueryModerationStatuses.QueryParams\ntype QueryEventsParams = QueryModerationEvents.QueryParams\ntype ModLevel = 'admin' | 'moderator' | 'triage'\n\nexport class ModeratorClient {\n  agent: AtpAgent\n  constructor(public ozone: TestOzone) {\n    this.agent = ozone.getClient()\n  }\n\n  async getEvent(id: number, role?: ModLevel) {\n    const result = await this.agent.tools.ozone.moderation.getEvent(\n      { id },\n      {\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.moderation.getEvent',\n          role,\n        ),\n      },\n    )\n    return result.data\n  }\n\n  async queryStatuses(input: QueryStatusesParams, role?: ModLevel) {\n    const result = await this.agent.tools.ozone.moderation.queryStatuses(\n      input,\n      {\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.moderation.queryStatuses',\n          role,\n        ),\n      },\n    )\n    return result.data\n  }\n\n  async getReporterStats(dids: string[]) {\n    const result = await this.agent.tools.ozone.moderation.getReporterStats(\n      { dids },\n      {\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.moderation.getReporterStats',\n          'admin',\n        ),\n      },\n    )\n    return result.data\n  }\n\n  async queryEvents(input: QueryEventsParams, role?: ModLevel) {\n    const result = await this.agent.tools.ozone.moderation.queryEvents(input, {\n      headers: await this.ozone.modHeaders(\n        'tools.ozone.moderation.queryEvents',\n        role,\n      ),\n    })\n    return result.data\n  }\n\n  async emitEvent(\n    opts: {\n      event: TakeActionInput['event']\n      subject: TakeActionInput['subject']\n      subjectBlobCids?: TakeActionInput['subjectBlobCids']\n      reason?: string\n      createdBy?: string\n      meta?: unknown\n      modTool?: ToolsOzoneModerationDefs.ModTool\n      externalId?: string\n    },\n    role?: ModLevel,\n  ) {\n    const {\n      event,\n      subject,\n      subjectBlobCids,\n      createdBy = 'did:example:admin',\n      modTool,\n      externalId,\n    } = opts\n    const result = await this.agent.tools.ozone.moderation.emitEvent(\n      {\n        event,\n        subject,\n        subjectBlobCids,\n        createdBy,\n        modTool,\n        externalId,\n      },\n      {\n        encoding: 'application/json',\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.moderation.emitEvent',\n          role,\n        ),\n      },\n    )\n    return result.data\n  }\n\n  async reverseAction(\n    opts: {\n      id: number\n      subject: TakeActionInput['subject']\n      reason?: string\n      createdBy?: string\n      modTool?: ToolsOzoneModerationDefs.ModTool\n    },\n    role?: ModLevel,\n  ) {\n    const {\n      subject,\n      reason = 'X',\n      createdBy = 'did:example:admin',\n      modTool,\n    } = opts\n    const result = await this.agent.tools.ozone.moderation.emitEvent(\n      {\n        subject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n          comment: reason,\n        },\n        createdBy,\n        modTool,\n      },\n      {\n        encoding: 'application/json',\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.moderation.emitEvent',\n          role,\n        ),\n      },\n    )\n    return result.data\n  }\n\n  async performTakedown(\n    opts: {\n      subject: TakeActionInput['subject']\n      subjectBlobCids?: TakeActionInput['subjectBlobCids']\n      durationInHours?: number\n      acknowledgeAccountSubjects?: boolean\n      reason?: string\n      policies?: string[]\n    },\n    role?: ModLevel,\n  ) {\n    const { durationInHours, acknowledgeAccountSubjects, policies, ...rest } =\n      opts\n    return this.emitEvent(\n      {\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTakedown',\n          acknowledgeAccountSubjects,\n          durationInHours,\n          policies,\n        },\n        ...rest,\n      },\n      role,\n    )\n  }\n\n  async performReverseTakedown(\n    opts: {\n      subject: TakeActionInput['subject']\n      subjectBlobCids?: TakeActionInput['subjectBlobCids']\n      reason?: string\n    },\n    role?: ModLevel,\n  ) {\n    return this.emitEvent(\n      {\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        },\n        ...opts,\n      },\n      role,\n    )\n  }\n\n  async upsertSettingOption(\n    setting: ToolsOzoneSettingUpsertOption.InputSchema,\n    callerRole: 'admin' | 'moderator' | 'triage' = 'admin',\n  ) {\n    const { data } = await this.agent.tools.ozone.setting.upsertOption(\n      setting,\n      {\n        encoding: 'application/json',\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.setting.upsertOption',\n          callerRole,\n        ),\n      },\n    )\n\n    return data\n  }\n\n  async removeSettingOptions(\n    params: ToolsOzoneSettingRemoveOptions.InputSchema,\n    callerRole: 'admin' | 'moderator' | 'triage' = 'admin',\n  ) {\n    const { data } = await this.agent.tools.ozone.setting.removeOptions(\n      params,\n      {\n        encoding: 'application/json',\n        headers: await this.ozone.modHeaders(\n          'tools.ozone.setting.removeOptions',\n          callerRole,\n        ),\n      },\n    )\n\n    return data\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/network-no-appview.ts",
    "content": "import { SkeletonHandler } from '@atproto/pds'\nimport { TestFeedGen } from './feed-gen'\nimport { TestPds } from './pds'\nimport { TestPlc } from './plc'\nimport { SeedClient } from './seed/client'\nimport { TestServerParams } from './types'\nimport { mockNetworkUtilities } from './util'\n\nexport class TestNetworkNoAppView {\n  feedGens: TestFeedGen[] = []\n  constructor(\n    public plc: TestPlc,\n    public pds: TestPds,\n  ) {}\n\n  static async create(\n    params: Partial<TestServerParams> = {},\n  ): Promise<TestNetworkNoAppView> {\n    const plc = await TestPlc.create(params.plc ?? {})\n    const pds = await TestPds.create({\n      didPlcUrl: plc.url,\n      ...params.pds,\n    })\n\n    mockNetworkUtilities(pds)\n\n    return new TestNetworkNoAppView(plc, pds)\n  }\n\n  async createFeedGen(\n    feeds: Record<string, SkeletonHandler>,\n  ): Promise<TestFeedGen> {\n    const fg = await TestFeedGen.create(this.plc.url, feeds)\n    this.feedGens.push(fg)\n    return fg\n  }\n\n  getSeedClient(): SeedClient<typeof this> {\n    const agent = this.pds.getClient()\n    return new SeedClient(this, agent)\n  }\n\n  async processAll() {\n    await this.pds.processAll()\n  }\n\n  async close() {\n    await Promise.all(this.feedGens.map((fg) => fg.close()))\n    await this.pds.close()\n    await this.plc.close()\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/network.ts",
    "content": "import assert from 'node:assert'\nimport getPort from 'get-port'\nimport * as uint8arrays from 'uint8arrays'\nimport { wait } from '@atproto/common-web'\nimport { createServiceJwt } from '@atproto/xrpc-server'\nimport { TestBsky } from './bsky'\nimport { EXAMPLE_LABELER } from './const'\nimport { IntrospectServer } from './introspect'\nimport { TestNetworkNoAppView } from './network-no-appview'\nimport { TestOzone } from './ozone'\nimport { TestPds } from './pds'\nimport { TestPlc } from './plc'\nimport { LexiconAuthorityProfile } from './service-profile-lexicon'\nimport { OzoneServiceProfile } from './service-profile-ozone'\nimport { TestServerParams } from './types'\nimport { mockNetworkUtilities } from './util'\n\nconst ADMIN_USERNAME = 'admin'\nconst ADMIN_PASSWORD = 'admin-pass'\n\nexport class TestNetwork extends TestNetworkNoAppView {\n  constructor(\n    public plc: TestPlc,\n    public pds: TestPds,\n    public bsky: TestBsky,\n    public ozone: TestOzone,\n    public introspect?: IntrospectServer,\n  ) {\n    super(plc, pds)\n  }\n\n  static async create(\n    params: Partial<TestServerParams> = {},\n  ): Promise<TestNetwork> {\n    const redisHost = process.env.REDIS_HOST\n    const dbPostgresUrl = params.dbPostgresUrl || process.env.DB_POSTGRES_URL\n    assert(dbPostgresUrl, 'Missing postgres url for tests')\n    assert(redisHost, 'Missing redis host for tests')\n    const dbPostgresSchema =\n      params.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA\n\n    const plc = await TestPlc.create(params.plc ?? {})\n\n    const bskyPort = params.bsky?.port ?? (await getPort())\n    const pdsPort = params.pds?.port ?? (await getPort())\n    const ozonePort = params.ozone?.port ?? (await getPort())\n\n    const thirdPartyPds = await TestPds.create({\n      didPlcUrl: plc.url,\n      ...params.pds,\n      inviteRequired: false,\n      port: await getPort(),\n    })\n\n    const ozoneUrl = `http://localhost:${ozonePort}`\n\n    // @TODO (?) rework the ServiceProfile to live on a separate PDS instead of\n    // requiring to migrate to the main PDS\n    const ozoneServiceProfile = await OzoneServiceProfile.create(\n      thirdPartyPds,\n      ozoneUrl,\n    )\n    const lexiconAuthorityProfile =\n      await LexiconAuthorityProfile.create(thirdPartyPds)\n\n    const bsky = await TestBsky.create({\n      port: bskyPort,\n      plcUrl: plc.url,\n      pdsPort,\n      rolodexUrl: process.env.BSKY_ROLODEX_URL,\n      rolodexIgnoreBadTls: true,\n      repoProvider: `ws://localhost:${pdsPort}`,\n      dbPostgresSchema: `appview_${dbPostgresSchema}`,\n      dbPostgresUrl,\n      redisHost,\n      modServiceDid: ozoneServiceProfile.did,\n      labelsFromIssuerDids: [ozoneServiceProfile.did, EXAMPLE_LABELER],\n      // Using a static private key results in a static DID, which is useful for e2e tests with the social-app repo.\n      privateKey:\n        '3f916c70dc69e4c5e83877f013325b11ecac31742e6a42f5c4fb240d0703d9d5=',\n      ...params.bsky,\n    })\n\n    const pds = await TestPds.create({\n      port: pdsPort,\n      didPlcUrl: plc.url,\n      bskyAppViewUrl: bsky.url,\n      bskyAppViewDid: bsky.ctx.cfg.serverDid,\n      modServiceUrl: ozoneUrl,\n      modServiceDid: ozoneServiceProfile.did,\n      lexiconDidAuthority: lexiconAuthorityProfile.did,\n      ...params.pds,\n    })\n\n    // mock before any events start flowing from pds so that we don't miss e.g. any handle resolutions.\n    mockNetworkUtilities(pds, bsky)\n\n    const ozone = await TestOzone.create({\n      port: ozonePort,\n      plcUrl: plc.url,\n      signingKey: ozoneServiceProfile.key,\n      serverDid: ozoneServiceProfile.did,\n      dbPostgresSchema: `ozone_${dbPostgresSchema || 'db'}`,\n      dbPostgresUrl,\n      appviewUrl: bsky.url,\n      appviewDid: bsky.ctx.cfg.serverDid,\n      appviewPushEvents: true,\n      pdsUrl: pds.url,\n      pdsDid: pds.ctx.cfg.service.did,\n      verifierDid: ozoneServiceProfile.did,\n      verifierUrl: pds.url,\n      verifierPassword: 'temp',\n      ...params.ozone,\n    })\n\n    await ozoneServiceProfile.migrateTo(pds)\n    await ozoneServiceProfile.createRecords()\n\n    await lexiconAuthorityProfile.migrateTo(pds)\n    await lexiconAuthorityProfile.createRecords()\n\n    await ozone.addAdminDid(ozoneServiceProfile.did)\n    await ozone.createPolicies()\n\n    await thirdPartyPds.processAll()\n    await pds.processAll()\n    await ozone.processAll()\n    await bsky.sub.processAll()\n    await thirdPartyPds.close()\n\n    // Weird but if we do this before pds.processAll() somehow appview loses this user and tests in different parts fail because appview doesn't return this user in various contexts anymore\n    const ozoneVerifierPassword =\n      await ozoneServiceProfile.createAppPasswordForVerification()\n    if (ozone.daemon.ctx.cfg.verifier) {\n      ozone.daemon.ctx.cfg.verifier.password = ozoneVerifierPassword\n    }\n\n    let introspect: IntrospectServer | undefined = undefined\n    if (params.introspect?.port) {\n      introspect = await IntrospectServer.start(\n        params.introspect.port,\n        plc,\n        pds,\n        bsky,\n        ozone,\n      )\n    }\n\n    return new TestNetwork(plc, pds, bsky, ozone, introspect)\n  }\n\n  async processFullSubscription(timeout = 5000) {\n    const sub = this.bsky.sub\n    const start = Date.now()\n    const lastSeq = await this.pds.ctx.sequencer.curr()\n    if (!lastSeq) return\n    while (Date.now() - start < timeout) {\n      await sub.processAll()\n      const runnerCursor = await sub.runner.getCursor()\n      // if subscription claims to be done, ensure we are at the most recent cursor from PDS, else wait to process again\n      // (the subscription may claim to be finished before the PDS has even emitted it's event)\n      if (runnerCursor && runnerCursor >= lastSeq) {\n        return\n      }\n      await wait(5)\n    }\n    throw new Error(`Sequence was not processed within ${timeout}ms`)\n  }\n\n  async processAll(timeout?: number) {\n    await this.pds.processAll()\n    await this.ozone.processAll()\n    await this.processFullSubscription(timeout)\n  }\n\n  async serviceHeaders(did: string, lxm: string, aud?: string) {\n    const keypair = await this.pds.ctx.actorStore.keypair(did)\n    const jwt = await createServiceJwt({\n      iss: did,\n      aud: aud ?? this.bsky.ctx.cfg.serverDid,\n      lxm,\n      keypair,\n    })\n    return { authorization: `Bearer ${jwt}` }\n  }\n\n  async adminHeaders({\n    username = ADMIN_USERNAME,\n    password = ADMIN_PASSWORD,\n  }: {\n    username?: string\n    password?: string\n  }) {\n    return {\n      authorization:\n        'Basic ' +\n        uint8arrays.toString(\n          uint8arrays.fromString(`${username}:${password}`, 'utf8'),\n          'base64pad',\n        ),\n    }\n  }\n\n  async close() {\n    await Promise.all(this.feedGens.map((fg) => fg.close()))\n    await this.ozone.close()\n    await this.bsky.close()\n    await this.pds.close()\n    await this.plc.close()\n    await this.introspect?.close()\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/ozone.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport getPort from 'get-port'\nimport * as ui8 from 'uint8arrays'\nimport { AtpAgent } from '@atproto/api'\nimport { Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport * as ozone from '@atproto/ozone'\nimport { createServiceJwt } from '@atproto/xrpc-server'\nimport { ADMIN_PASSWORD, EXAMPLE_LABELER } from './const'\nimport { ModeratorClient } from './moderator-client'\nimport { DidAndKey, OzoneConfig } from './types'\nimport { createDidAndKey } from './util'\n\nexport class TestOzone {\n  constructor(\n    public url: string,\n    public port: number,\n    public server: ozone.OzoneService,\n    public daemon: ozone.OzoneDaemon,\n    public adminAccnt: DidAndKey,\n    public moderatorAccnt: DidAndKey,\n    public triageAccnt: DidAndKey,\n  ) {}\n\n  static async create(config: OzoneConfig): Promise<TestOzone> {\n    const serviceKeypair =\n      config.signingKey ?? (await Secp256k1Keypair.create({ exportable: true }))\n    const signingKeyHex = ui8.toString(await serviceKeypair.export(), 'hex')\n    let serverDid = config.serverDid\n    if (!serverDid) {\n      serverDid = await createOzoneDid(config.plcUrl, serviceKeypair)\n    }\n\n    const admin = await createDidAndKey({\n      plcUrl: config.plcUrl,\n      handle: 'admin.ozone',\n      pds: 'https://pds.invalid',\n    })\n\n    const moderator = await createDidAndKey({\n      plcUrl: config.plcUrl,\n      handle: 'moderator.ozone',\n      pds: 'https://pds.invalid',\n    })\n\n    const triage = await createDidAndKey({\n      plcUrl: config.plcUrl,\n      handle: 'triage.ozone',\n      pds: 'https://pds.invalid',\n    })\n\n    const port = config.port || (await getPort())\n    const url = `http://localhost:${port}`\n\n    const env: ozone.OzoneEnvironment = {\n      devMode: true,\n      version: '0.0.0',\n      port,\n      didPlcUrl: config.plcUrl,\n      publicUrl: url,\n      serverDid,\n      signingKeyHex,\n      ...config,\n      adminPassword: ADMIN_PASSWORD,\n      adminDids: [...(config.adminDids ?? []), admin.did],\n      moderatorDids: [\n        ...(config.moderatorDids ?? []),\n        config.appviewDid,\n        moderator.did,\n      ],\n      triageDids: [...(config.triageDids ?? []), triage.did],\n    }\n\n    // Separate migration db in case migration changes some connection state that we need in the tests, e.g. \"alter database ... set ...\"\n    const migrationDb = new ozone.Database({\n      schema: config.dbPostgresSchema,\n      url: config.dbPostgresUrl,\n    })\n    if (config.migration) {\n      await migrationDb.migrateToOrThrow(config.migration)\n    } else {\n      await migrationDb.migrateToLatestOrThrow()\n    }\n    await migrationDb.close()\n\n    const cfg = ozone.envToCfg(env)\n    const secrets = ozone.envToSecrets(env)\n\n    // api server\n    const server = await ozone.OzoneService.create(cfg, secrets, {\n      imgInvalidator: config.imgInvalidator,\n    })\n    await server.start()\n\n    const daemon = await ozone.OzoneDaemon.create(cfg, secrets)\n    await daemon.start()\n    // don't do event reversal in dev-env\n    await daemon.ctx.eventReverser.destroy()\n\n    return new TestOzone(url, port, server, daemon, admin, moderator, triage)\n  }\n\n  get ctx(): ozone.AppContext {\n    return this.server.ctx\n  }\n\n  getClient(): AtpAgent {\n    const agent = new AtpAgent({ service: this.url })\n    agent.configureLabelers([EXAMPLE_LABELER])\n    return agent\n  }\n\n  getModClient() {\n    return new ModeratorClient(this)\n  }\n\n  async addAdminDid(did: string) {\n    await this.ctx.teamService(this.ctx.db).create({\n      did,\n      disabled: false,\n      handle: null,\n      displayName: null,\n      lastUpdatedBy: this.ctx.cfg.service.did,\n      role: 'tools.ozone.team.defs#roleAdmin',\n    })\n    this.ctx.cfg.access.admins.push(did)\n  }\n\n  async addModeratorDid(did: string) {\n    await this.ctx.teamService(this.ctx.db).create({\n      did,\n      disabled: false,\n      handle: null,\n      displayName: null,\n      lastUpdatedBy: this.ctx.cfg.service.did,\n      role: 'tools.ozone.team.defs#roleModerator',\n    })\n    this.ctx.cfg.access.moderators.push(did)\n  }\n\n  async addTriageDid(did: string) {\n    await this.ctx.teamService(this.ctx.db).create({\n      did,\n      disabled: false,\n      handle: null,\n      displayName: null,\n      lastUpdatedBy: this.ctx.cfg.service.did,\n      role: 'tools.ozone.team.defs#roleTriage',\n    })\n    this.ctx.cfg.access.triage.push(did)\n  }\n\n  async createPolicies() {\n    const now = new Date()\n    const defaultOptions = {\n      managerRole: 'tools.ozone.team.defs#roleAdmin' as const,\n      scope: 'instance' as const,\n      did: this.ctx.cfg.service.did,\n      lastUpdatedBy: this.ctx.cfg.service.did,\n      createdBy: this.ctx.cfg.service.did,\n      createdAt: now,\n      updatedAt: now,\n    }\n    await this.ctx.settingService(this.ctx.db).upsert({\n      ...defaultOptions,\n      key: 'tools.ozone.setting.severityLevels',\n      value: {\n        'sev-2': {\n          strikeCount: 2,\n          expiryInDays: 90,\n        },\n        'sev-4': {\n          strikeCount: 4,\n          expiryInDays: 365,\n        },\n        'sev-7': {\n          needsTakedown: true,\n          description: 'Sever violation, immedate account takedown',\n        },\n        'custom-sev': {\n          strikeCount: 4,\n          firstOccurrenceStrikeCount: 8,\n          description: 'First offense harsher penalty, on subsequent less',\n        },\n      },\n      description: 'Severity levels and strike count mapping for policies',\n    })\n    await this.ctx.settingService(this.ctx.db).upsert({\n      ...defaultOptions,\n      key: 'tools.ozone.setting.policyList',\n      value: {\n        'policy-one': {\n          name: 'Policy One',\n          description: 'Policy for handling user behavior',\n          severityLevels: {\n            'sev-1': {\n              description: 'Minor infraction',\n              isDefault: true,\n            },\n            'sev-2': {\n              description: 'Moderate infraction',\n              isDefault: false,\n            },\n          },\n        },\n        'policy-two': {\n          name: 'Policy Two',\n          description: 'Policy for handling user action',\n          severityLevels: {\n            'sev-4': {\n              description: 'Moderate infraction',\n              isDefault: false,\n            },\n            'sev-5': {\n              description: 'Severe infraction',\n              isDefault: false,\n            },\n          },\n        },\n      },\n      description: 'Moderation policies to be associated with actions',\n    })\n  }\n\n  async modHeaders(\n    lxm: string,\n    role: 'admin' | 'moderator' | 'triage' = 'moderator',\n  ) {\n    const account =\n      role === 'admin'\n        ? this.adminAccnt\n        : role === 'moderator'\n          ? this.moderatorAccnt\n          : this.triageAccnt\n    const jwt = await createServiceJwt({\n      iss: account.did,\n      aud: this.ctx.cfg.service.did,\n      lxm,\n      keypair: account.key,\n    })\n    return { authorization: `Bearer ${jwt}` }\n  }\n\n  async processAll() {\n    await this.ctx.backgroundQueue.processAll()\n    await this.daemon.processAll()\n  }\n\n  async close() {\n    await this.daemon.destroy()\n    await this.server.destroy()\n  }\n}\n\nexport const createOzoneDid = async (\n  plcUrl: string,\n  keypair: Keypair,\n): Promise<string> => {\n  const plcClient = new plc.Client(plcUrl)\n  const plcOp = await plc.signOperation(\n    {\n      type: 'plc_operation',\n      alsoKnownAs: [],\n      rotationKeys: [keypair.did()],\n      verificationMethods: {\n        atproto_label: keypair.did(),\n      },\n      services: {\n        atproto_labeler: {\n          type: 'AtprotoLabeler',\n          endpoint: 'https://ozone.public.url',\n        },\n      },\n      prev: null,\n    },\n    keypair,\n  )\n  const did = await plc.didForCreateOp(plcOp)\n  await plcClient.sendOperation(did, plcOp)\n  return did\n}\n"
  },
  {
    "path": "packages/dev-env/src/pds.ts",
    "content": "import fs from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport getPort from 'get-port'\nimport * as ui8 from 'uint8arrays'\nimport { AtpAgent } from '@atproto/api'\nimport { Secp256k1Keypair, randomStr } from '@atproto/crypto'\nimport * as pds from '@atproto/pds'\nimport { createSecretKeyObject } from '@atproto/pds'\nimport { ADMIN_PASSWORD, EXAMPLE_LABELER, JWT_SECRET } from './const'\nimport { PdsConfig } from './types'\n\nexport class TestPds {\n  constructor(\n    public url: string,\n    public port: number,\n    public server: pds.PDS,\n  ) {}\n\n  static async create(config: PdsConfig): Promise<TestPds> {\n    const plcRotationKey = await Secp256k1Keypair.create({ exportable: true })\n    const plcRotationPriv = ui8.toString(await plcRotationKey.export(), 'hex')\n    const recoveryKey = (await Secp256k1Keypair.create()).did()\n\n    const port = config.port || (await getPort())\n    const url = `http://localhost:${port}`\n\n    const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, 'base32'))\n    const dataDirectory = path.join(os.tmpdir(), randomStr(8, 'base32'))\n    await fs.mkdir(dataDirectory, { recursive: true })\n\n    const env: pds.ServerEnvironment = {\n      devMode: true,\n      port,\n      dataDirectory: dataDirectory,\n      blobstoreDiskLocation: blobstoreLoc,\n      recoveryDidKey: recoveryKey,\n      adminPassword: ADMIN_PASSWORD,\n      jwtSecret: JWT_SECRET,\n      // @NOTE \".example\" will not actually work and is only used to display\n      // multiple domains in the sing-up UI\n      serviceHandleDomains: ['.test', '.example'],\n      bskyAppViewUrl: 'https://appview.invalid',\n      bskyAppViewDid: 'did:example:invalid',\n      bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s',\n      modServiceUrl: 'https://moderator.invalid',\n      modServiceDid: 'did:example:invalid',\n      plcRotationKeyK256PrivateKeyHex: plcRotationPriv,\n      inviteRequired: false,\n      disableSsrfProtection: true,\n      serviceName: 'Development PDS',\n      primaryColor: '#f0828d',\n      primaryColorContrast: '#fff', // Bad contrast for a11y (WCAG AA)\n      errorColor: 'rgb(238, 0, 78)', // rgb() notation should work too\n      logoUrl:\n        // Using a \"data:\" instead of a real URL to avoid making CORS requests in dev.\n        // License: https://uxwing.com/license/\n        // Source: https://uxwing.com/bee-icon/\n        `data:image/svg+xml;base64,${Buffer.from('<svg xmlns=\"http://www.w3.org/2000/svg\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" image-rendering=\"optimizeQuality\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" viewBox=\"0 0 503 511.623\"><path fill=\"#FFB9B9\" d=\"M379.75 85.311l90.022 89.879C503.264 128.804 502.534 31.13 476.188 0c-27.441 31.966-59.103 60.767-96.438 85.311z\"/><path fill=\"#E2828D\" d=\"M399.445 104.976l70.327 70.214c26.443-36.622 31.549-105.205 19.563-147.778-26.692 28.309-56.413 54.344-89.89 77.564z\"/><path fill=\"#FFB9B9\" d=\"M119.595 85.311L29.573 175.19C-3.919 128.804-3.189 31.13 23.156 0c27.441 31.966 59.103 60.767 96.439 85.311z\"/><path fill=\"#E2828D\" d=\"M99.899 104.976L29.573 175.19C3.13 138.568-1.976 69.985 10.01 27.412c26.692 28.309 56.413 54.344 89.889 77.564z\"/><path fill=\"#FFB9B9\" d=\"M251.5 51.303c138.898 0 251.5 103.046 251.5 230.16 0 127.114-112.602 230.16-251.5 230.16C112.6 511.623 0 408.577 0 281.463c0-127.114 112.6-230.16 251.5-230.16z\"/><path fill=\"#331400\" d=\"M138.142 188.245c16.387 0 29.672 13.283 29.672 29.672 0 16.389-13.285 29.673-29.672 29.673-16.389 0-29.675-13.284-29.675-29.673 0-16.389 13.286-29.672 29.675-29.672zM360.695 188.245c16.389 0 29.674 13.283 29.674 29.672 0 16.389-13.285 29.673-29.674 29.673-16.387 0-29.673-13.284-29.673-29.673 0-16.389 13.286-29.672 29.673-29.672z\"/><path fill=\"#F0828D\" fill-rule=\"nonzero\" d=\"M251.5 255.548c37.407 0 71.438 11.136 96.213 29.138 25.886 18.808 41.905 45.125 41.905 74.487 0 29.36-16.017 55.679-41.908 74.49-24.772 18.001-58.805 29.138-96.21 29.138-37.405 0-71.438-11.137-96.21-29.138-25.891-18.811-41.908-45.13-41.908-74.49 0-29.362 16.019-55.679 41.905-74.487 24.775-18.002 58.808-29.138 96.213-29.138z\"/><circle fill=\"#A5414B\" cx=\"203.259\" cy=\"358.515\" r=\"29.673\"/><circle fill=\"#A5414B\" cx=\"298.744\" cy=\"358.515\" r=\"29.673\"/></svg>', 'utf8').toString('base64')}`,\n      homeUrl: 'https://bsky.social/',\n      termsOfServiceUrl: 'https://bsky.social/about/support/tos',\n      privacyPolicyUrl: 'https://bsky.social/about/support/privacy-policy',\n      supportUrl: 'https://blueskyweb.zendesk.com/hc/en-us',\n      ...config,\n    }\n    const cfg = pds.envToCfg(env)\n    const secrets = pds.envToSecrets(env)\n\n    const server = await pds.PDS.create(cfg, secrets)\n\n    await server.start()\n\n    return new TestPds(url, port, server)\n  }\n\n  get ctx(): pds.AppContext {\n    return this.server.ctx\n  }\n\n  getClient(): AtpAgent {\n    const agent = new AtpAgent({ service: this.url })\n    agent.configureLabelers([EXAMPLE_LABELER])\n    return agent\n  }\n\n  adminAuth(): string {\n    return (\n      'Basic ' +\n      ui8.toString(\n        ui8.fromString(`admin:${ADMIN_PASSWORD}`, 'utf8'),\n        'base64pad',\n      )\n    )\n  }\n\n  adminAuthHeaders() {\n    return {\n      authorization: this.adminAuth(),\n    }\n  }\n\n  jwtSecretKey() {\n    return createSecretKeyObject(JWT_SECRET)\n  }\n\n  async processAll() {\n    await this.ctx.backgroundQueue.processAll()\n  }\n\n  async close() {\n    await this.server.destroy()\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/plc.ts",
    "content": "import { Client as PlcClient } from '@did-plc/lib'\nimport * as plc from '@did-plc/server'\nimport getPort from 'get-port'\nimport { PlcConfig } from './types'\n\nexport class TestPlc {\n  constructor(\n    public url: string,\n    public port: number,\n    public server: plc.PlcServer,\n  ) {}\n\n  static async create(cfg: PlcConfig): Promise<TestPlc> {\n    const db = plc.Database.mock()\n    const port = cfg.port || (await getPort())\n    const url = `http://localhost:${port}`\n    const server = plc.PlcServer.create({ db, port, ...cfg })\n    await server.start()\n    return new TestPlc(url, port, server)\n  }\n\n  get ctx(): plc.AppContext {\n    return this.server.ctx\n  }\n\n  getClient(): PlcClient {\n    return new PlcClient(this.url)\n  }\n\n  async close() {\n    await this.server.destroy()\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/author-feed.ts",
    "content": "import basicSeed from './basic'\nimport { SeedClient } from './client'\n\nexport default async (sc: SeedClient) => {\n  await basicSeed(sc)\n  await sc.createAccount('eve', {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  })\n  await sc.createAccount('fred', {\n    email: 'fred@test.com',\n    handle: 'fred.test',\n    password: 'fred-pass',\n  })\n\n  const alice = sc.dids.alice\n  const eve = sc.dids.eve\n  const fred = sc.dids.fred\n\n  /*\n   * Self thread\n   */\n  await sc.post(eve, evePosts[0])\n  await sc.reply(\n    eve,\n    sc.posts[eve][0].ref,\n    sc.posts[eve][0].ref,\n    eveOwnThreadReplies[0],\n  )\n  await sc.reply(\n    eve,\n    sc.posts[eve][0].ref,\n    sc.replies[eve][0].ref,\n    eveOwnThreadReplies[1],\n  )\n  await sc.reply(\n    eve,\n    sc.posts[eve][0].ref,\n    sc.replies[eve][1].ref,\n    eveOwnThreadReplies[2],\n  )\n\n  /**\n   * Two replies to Alice\n   */\n  await sc.reply(\n    eve,\n    sc.posts[alice][1].ref,\n    sc.posts[alice][1].ref,\n    eveAliceReplies[0],\n  )\n  await sc.reply(\n    eve,\n    sc.posts[alice][1].ref,\n    sc.replies[eve][3].ref,\n    eveAliceReplies[1],\n  )\n\n  /**\n   * Two replies to Fred, who replied to Eve's root post. This creates a\n   * \"detached\" thread, where one Fred post breaks the continuity.\n   */\n  await sc.post(eve, evePosts[1])\n  const fredReply = await sc.reply(\n    fred,\n    sc.posts[eve][1].ref,\n    sc.posts[eve][1].ref,\n    fredReplies[0],\n  )\n  await sc.reply(\n    eve,\n    sc.posts[eve][1].ref,\n    sc.replies[fred][0].ref,\n    eveFredReplies[0],\n  )\n  await sc.reply(\n    eve,\n    sc.posts[eve][1].ref,\n    sc.replies[eve][4].ref,\n    eveFredReplies[1],\n  )\n\n  // a repost for eve's feed\n  await sc.repost(eve, fredReply.ref)\n\n  return sc\n}\n\nconst evePosts = ['eve own thread', 'eve detached thread']\nconst eveOwnThreadReplies = [\n  'eve own reply 1',\n  'eve own reply 2',\n  'eve own reply 3',\n]\nconst eveAliceReplies = ['eve reply to alice 1', 'eve reply to alice 2']\nconst eveFredReplies = ['eve reply to fred 1', 'eve reply to fred 2']\nconst fredReplies = ['fred reply to eve 1']\n"
  },
  {
    "path": "packages/dev-env/src/seed/basic.ts",
    "content": "import { TestBsky } from '../bsky'\nimport { EXAMPLE_LABELER } from '../const'\nimport { TestNetwork } from '../network'\nimport { TestNetworkNoAppView } from '../network-no-appview'\nimport { SeedClient } from './client'\nimport usersSeed from './users'\n\nexport default async (\n  sc: SeedClient<TestNetwork | TestNetworkNoAppView>,\n  users = true,\n) => {\n  if (users) await usersSeed(sc)\n\n  const alice = sc.dids.alice\n  const bob = sc.dids.bob\n  const carol = sc.dids.carol\n  const dan = sc.dids.dan\n  const createdAtMicroseconds = () => ({\n    createdAt: new Date().toISOString().replace('Z', '000Z'), // microseconds\n  })\n  const createdAtTimezone = () => ({\n    createdAt: new Date().toISOString().replace('Z', '+00:00'), // iso timezone format\n  })\n\n  await sc.follow(alice, bob)\n  await sc.follow(alice, carol)\n  await sc.follow(alice, dan)\n  await sc.follow(carol, alice)\n  await sc.follow(bob, alice)\n  await sc.follow(bob, carol, createdAtMicroseconds())\n  await sc.follow(dan, bob, createdAtTimezone())\n  await sc.post(alice, posts.alice[0], undefined, undefined, undefined, {\n    labels: {\n      $type: 'com.atproto.label.defs#selfLabels',\n      values: [{ val: 'self-label' }],\n    },\n  })\n  await sc.post(bob, posts.bob[0], undefined, undefined, undefined, {\n    langs: ['en-US', 'i-klingon'],\n  })\n  const img1 = await sc.uploadFile(\n    carol,\n    '../dev-env/assets/key-landscape-small.jpg',\n    'image/jpeg',\n  )\n  const img2 = await sc.uploadFile(\n    carol,\n    '../dev-env/assets/key-alt.jpg',\n    'image/jpeg',\n  )\n  await sc.post(\n    carol,\n    posts.carol[0],\n    undefined,\n    [img1, img2], // Contains both images and a quote\n    sc.posts[bob][0].ref,\n  )\n  await sc.post(dan, posts.dan[0])\n  await sc.post(\n    dan,\n    posts.dan[1],\n    [\n      {\n        index: { byteStart: 0, byteEnd: 18 },\n        features: [\n          {\n            $type: `app.bsky.richtext.facet#mention`,\n            did: alice,\n          },\n        ],\n      },\n    ],\n    undefined,\n    sc.posts[carol][0].ref, // This post contains an images embed\n  )\n  await sc.post(\n    alice,\n    posts.alice[1],\n    undefined,\n    undefined,\n    undefined,\n    createdAtMicroseconds(),\n  )\n  await sc.post(\n    bob,\n    posts.bob[1],\n    undefined,\n    undefined,\n    undefined,\n    createdAtTimezone(),\n  )\n  await sc.post(\n    alice,\n    posts.alice[2],\n    undefined,\n    undefined,\n    sc.posts[dan][1].ref, // This post contains a record embed which contains an images embed\n  )\n  await sc.like(bob, sc.posts[alice][1].ref)\n  await sc.like(bob, sc.posts[alice][2].ref)\n  await sc.like(carol, sc.posts[alice][1].ref)\n  await sc.like(carol, sc.posts[alice][2].ref)\n  await sc.like(dan, sc.posts[alice][1].ref)\n  await sc.like(alice, sc.posts[carol][0].ref, createdAtMicroseconds())\n  await sc.like(bob, sc.posts[carol][0].ref, createdAtTimezone())\n\n  const replyImg = await sc.uploadFile(\n    bob,\n    '../dev-env/assets/key-landscape-small.jpg',\n    'image/jpeg',\n  )\n  // must ensure ordering of replies in indexing\n  await sc.network.processAll()\n  await sc.reply(\n    bob,\n    sc.posts[alice][1].ref,\n    sc.posts[alice][1].ref,\n    replies.bob[0],\n    undefined,\n    [replyImg],\n  )\n  await sc.reply(\n    carol,\n    sc.posts[alice][1].ref,\n    sc.posts[alice][1].ref,\n    replies.carol[0],\n  )\n  await sc.network.processAll()\n  const alicesReplyToBob = await sc.reply(\n    alice,\n    sc.posts[alice][1].ref,\n    sc.replies[bob][0].ref,\n    replies.alice[0],\n  )\n  await sc.repost(carol, sc.posts[dan][1].ref)\n  await sc.repost(dan, sc.posts[alice][1].ref)\n  await sc.repost(dan, alicesReplyToBob.ref)\n\n  if (sc.network instanceof TestNetwork) {\n    const bsky = sc.network.bsky\n    await createLabel(bsky, {\n      val: 'test-label',\n      uri: sc.posts[alice][2].ref.uriStr,\n      cid: sc.posts[alice][2].ref.cidStr,\n    })\n    await createLabel(bsky, {\n      val: 'test-label',\n      uri: sc.replies[bob][0].ref.uriStr,\n      cid: sc.replies[bob][0].ref.cidStr,\n    })\n    await createLabel(bsky, {\n      val: 'test-label-2',\n      uri: sc.replies[bob][0].ref.uriStr,\n      cid: sc.replies[bob][0].ref.cidStr,\n    })\n  }\n\n  return sc\n}\n\nexport const posts = {\n  alice: ['hey there', 'again', 'yoohoo label_me'],\n  bob: ['bob back at it again!', 'bobby boy here', 'yoohoo'],\n  carol: ['hi im carol'],\n  dan: ['dan here!', '@alice.bluesky.xyz is the best'],\n}\n\nexport const replies = {\n  alice: ['thanks bob'],\n  bob: ['hear that label_me label_me_2'],\n  carol: ['of course'],\n}\n\nconst createLabel = async (\n  bsky: TestBsky,\n  opts: { uri: string; cid: string; val: string },\n) => {\n  await bsky.db.db\n    .insertInto('label')\n    .values({\n      uri: opts.uri,\n      cid: opts.cid,\n      val: opts.val,\n      cts: new Date().toISOString(),\n      neg: false,\n      src: EXAMPLE_LABELER, // this did is also configured on labelsFromIssuerDids\n    })\n    .execute()\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/client.ts",
    "content": "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { CID } from 'multiformats/cid'\nimport {\n  AppBskyActorProfile,\n  AppBskyFeedLike,\n  AppBskyFeedPost,\n  AppBskyFeedRepost,\n  AppBskyGraphBlock,\n  AppBskyGraphFollow,\n  AppBskyGraphList,\n  AppBskyGraphVerification,\n  AppBskyRichtextFacet,\n  AtpAgent,\n  ComAtprotoModerationCreateReport,\n} from '@atproto/api'\nimport { BlobRef } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport { TestNetworkNoAppView } from '../network-no-appview'\n\n// Makes it simple to create data via the XRPC client,\n// and keeps track of all created data in memory for convenience.\n\nlet AVATAR_IMG: Uint8Array | undefined\n\n// AVATAR_PATH is defined in a non-CWD-dependant way, so this works\n// for any consumer of this package, even outside the atproto repo.\nconst AVATAR_PATH = path.resolve(\n  __dirname,\n  '../../assets/key-portrait-small.jpg',\n)\n\nexport type ImageRef = {\n  image: BlobRef\n  alt: string\n}\n\nexport class RecordRef {\n  uri: AtUri\n  cid: CID\n\n  constructor(uri: AtUri | string, cid: CID | string) {\n    this.uri = new AtUri(uri.toString())\n    this.cid = CID.parse(cid.toString())\n  }\n\n  get raw(): { uri: string; cid: string } {\n    return {\n      uri: this.uri.toString(),\n      cid: this.cid.toString(),\n    }\n  }\n\n  get uriStr(): string {\n    return this.uri.toString()\n  }\n\n  get cidStr(): string {\n    return this.cid.toString()\n  }\n}\n\nexport class SeedClient<\n  Network extends TestNetworkNoAppView = TestNetworkNoAppView,\n> {\n  accounts: Record<\n    string,\n    {\n      did: string\n      accessJwt: string\n      refreshJwt: string\n      handle: string\n      email: string\n      password: string\n    }\n  >\n  profiles: Record<\n    string,\n    {\n      displayName: string\n      description: string\n      avatar: { cid: string; mimeType: string }\n      joinedViaStarterPack: RecordRef | undefined\n      ref: RecordRef\n    }\n  >\n  follows: Record<string, Record<string, RecordRef>>\n  blocks: Record<string, Record<string, RecordRef>>\n  mutes: Record<string, Set<string>>\n  posts: Record<\n    string,\n    { text: string; ref: RecordRef; images: ImageRef[]; quote?: RecordRef }[]\n  >\n  likes: Record<string, Record<string, AtUri>>\n  replies: Record<\n    string,\n    { text: string; ref: RecordRef; images: ImageRef[] }[]\n  >\n  reposts: Record<string, RecordRef[]>\n  lists: Record<\n    string,\n    Record<string, { ref: RecordRef; items: Record<string, RecordRef> }>\n  >\n  feedgens: Record<\n    string,\n    Record<string, { ref: RecordRef; items: Record<string, RecordRef> }>\n  >\n  starterpacks: Record<\n    string,\n    Record<\n      string,\n      {\n        ref: RecordRef\n        name: string\n        list: RecordRef\n        feeds: string[]\n      }\n    >\n  >\n\n  verifications: Record<string, Record<string, AtUri>>\n\n  dids: Record<string, string>\n\n  constructor(\n    public network: Network,\n    public agent: AtpAgent,\n  ) {\n    this.accounts = {}\n    this.profiles = {}\n    this.follows = {}\n    this.blocks = {}\n    this.mutes = {}\n    this.posts = {}\n    this.likes = {}\n    this.replies = {}\n    this.reposts = {}\n    this.lists = {}\n    this.feedgens = {}\n    this.starterpacks = {}\n    this.verifications = {}\n    this.dids = {}\n  }\n\n  async createAccount(\n    shortName: string,\n    params: {\n      handle: string\n      email: string\n      password: string\n      inviteCode?: string\n    },\n  ) {\n    const { data: account } =\n      await this.agent.com.atproto.server.createAccount(params)\n    this.dids[shortName] = account.did\n    this.accounts[account.did] = {\n      ...account,\n      email: params.email,\n      password: params.password,\n    }\n    return this.accounts[account.did]\n  }\n\n  async updateHandle(by: string, handle: string) {\n    await this.agent.com.atproto.identity.updateHandle(\n      { handle },\n      { encoding: 'application/json', headers: this.getHeaders(by) },\n    )\n  }\n\n  async createProfile(\n    by: string,\n    displayName: string,\n    description: string,\n    selfLabels?: string[],\n    joinedViaStarterPack?: RecordRef,\n    overrides?: Partial<AppBskyActorProfile.Record>,\n  ): Promise<{\n    displayName: string\n    description: string\n    avatar: { cid: string; mimeType: string }\n    ref: RecordRef\n    joinedViaStarterPack?: RecordRef\n  }> {\n    AVATAR_IMG ??= await fs.readFile(AVATAR_PATH)\n\n    let avatarBlob\n    {\n      const res = await this.agent.com.atproto.repo.uploadBlob(AVATAR_IMG, {\n        encoding: 'image/jpeg',\n        headers: this.getHeaders(by),\n      } as any)\n      avatarBlob = res.data.blob\n    }\n\n    {\n      const res = await this.agent.app.bsky.actor.profile.create(\n        { repo: by },\n        {\n          displayName,\n          description,\n          avatar: avatarBlob,\n          labels: selfLabels\n            ? {\n                $type: 'com.atproto.label.defs#selfLabels',\n                values: selfLabels.map((val) => ({ val })),\n              }\n            : undefined,\n          joinedViaStarterPack: joinedViaStarterPack?.raw,\n          createdAt: new Date().toISOString(),\n          ...overrides,\n        },\n        this.getHeaders(by),\n      )\n      this.profiles[by] = {\n        displayName,\n        description,\n        avatar: avatarBlob,\n        joinedViaStarterPack,\n        ref: new RecordRef(res.uri, res.cid),\n      }\n    }\n    return this.profiles[by]\n  }\n\n  async updateProfile(by: string, record: Record<string, unknown>) {\n    const res = await this.agent.com.atproto.repo.putRecord(\n      {\n        repo: by,\n        collection: 'app.bsky.actor.profile',\n        rkey: 'self',\n        record,\n      },\n      { headers: this.getHeaders(by), encoding: 'application/json' },\n    )\n    this.profiles[by] = {\n      ...(this.profiles[by] ?? {}),\n      ...record,\n      ref: new RecordRef(res.data.uri, res.data.cid),\n    }\n    return this.profiles[by]\n  }\n\n  async follow(\n    from: string,\n    to: string,\n    overrides?: Partial<AppBskyGraphFollow.Record>,\n  ) {\n    const res = await this.agent.app.bsky.graph.follow.create(\n      { repo: from },\n      {\n        subject: to,\n        createdAt: new Date().toISOString(),\n        ...overrides,\n      },\n      this.getHeaders(from),\n    )\n    this.follows[from] ??= {}\n    this.follows[from][to] = new RecordRef(res.uri, res.cid)\n    return this.follows[from][to]\n  }\n\n  async unfollow(from: string, to: string) {\n    const follow = this.follows[from][to]\n    if (!follow) {\n      throw new Error('follow does not exist')\n    }\n    await this.agent.app.bsky.graph.follow.delete(\n      { repo: from, rkey: follow.uri.rkey },\n      this.getHeaders(from),\n    )\n    delete this.follows[from][to]\n  }\n\n  async block(\n    from: string,\n    to: string,\n    overrides?: Partial<AppBskyGraphBlock.Record>,\n  ) {\n    const res = await this.agent.app.bsky.graph.block.create(\n      { repo: from },\n      {\n        subject: to,\n        createdAt: new Date().toISOString(),\n        ...overrides,\n      },\n      this.getHeaders(from),\n    )\n    this.blocks[from] ??= {}\n    this.blocks[from][to] = new RecordRef(res.uri, res.cid)\n    return this.blocks[from][to]\n  }\n\n  async unblock(from: string, to: string) {\n    const block = this.blocks[from][to]\n    if (!block) {\n      throw new Error('block does not exist')\n    }\n    await this.agent.app.bsky.graph.block.delete(\n      { repo: from, rkey: block.uri.rkey },\n      this.getHeaders(from),\n    )\n    delete this.blocks[from][to]\n  }\n\n  async mute(from: string, to: string) {\n    await this.agent.app.bsky.graph.muteActor(\n      {\n        actor: to,\n      },\n      { headers: this.getHeaders(from) },\n    )\n    this.mutes[from] ??= new Set()\n    this.mutes[from].add(to)\n    return this.mutes[from][to]\n  }\n\n  async post(\n    by: string,\n    text: string,\n    facets?: AppBskyRichtextFacet.Main[],\n    images?: ImageRef[],\n    quote?: RecordRef,\n    overrides?: Partial<AppBskyFeedPost.Record>,\n  ) {\n    const imageEmbed = images && {\n      $type: 'app.bsky.embed.images',\n      images,\n    }\n    const recordEmbed = quote && {\n      record: { uri: quote.uriStr, cid: quote.cidStr },\n    }\n    const embed =\n      imageEmbed && recordEmbed\n        ? {\n            $type: 'app.bsky.embed.recordWithMedia',\n            record: recordEmbed,\n            media: imageEmbed,\n          }\n        : recordEmbed\n          ? { $type: 'app.bsky.embed.record', ...recordEmbed }\n          : imageEmbed\n    const res = await this.agent.app.bsky.feed.post.create(\n      { repo: by },\n      {\n        text: text,\n        facets,\n        embed,\n        createdAt: new Date().toISOString(),\n        ...overrides,\n      },\n      this.getHeaders(by),\n    )\n    this.posts[by] ??= []\n    const post = {\n      text,\n      ref: new RecordRef(res.uri, res.cid),\n      images: images ?? [],\n      quote,\n    }\n    this.posts[by].push(post)\n    return post\n  }\n\n  async deletePost(by: string, uri: AtUri) {\n    await this.agent.app.bsky.feed.post.delete(\n      {\n        repo: by,\n        rkey: uri.rkey,\n      },\n      this.getHeaders(by),\n    )\n  }\n\n  async uploadFile(\n    by: string,\n    filePath: string,\n    encoding: string,\n  ): Promise<ImageRef> {\n    const file = await fs.readFile(filePath)\n    const res = await this.agent.com.atproto.repo.uploadBlob(file, {\n      headers: this.getHeaders(by),\n      encoding,\n    } as any)\n    return { image: res.data.blob, alt: filePath }\n  }\n\n  async like(\n    by: string,\n    subject: RecordRef,\n    overrides?: Partial<AppBskyFeedLike.Record>,\n  ) {\n    const res = await this.agent.app.bsky.feed.like.create(\n      { repo: by },\n      {\n        subject: subject.raw,\n        createdAt: new Date().toISOString(),\n        ...overrides,\n      },\n      this.getHeaders(by),\n    )\n    this.likes[by] ??= {}\n    this.likes[by][subject.uriStr] = new AtUri(res.uri)\n    return this.likes[by][subject.uriStr]\n  }\n\n  async reply(\n    by: string,\n    root: RecordRef,\n    parent: RecordRef,\n    text: string,\n    facets?: AppBskyRichtextFacet.Main[],\n    images?: ImageRef[],\n    overrides?: Partial<AppBskyFeedPost.Record>,\n  ) {\n    const embed = images\n      ? {\n          $type: 'app.bsky.embed.images',\n          images,\n        }\n      : undefined\n    const res = await this.agent.app.bsky.feed.post.create(\n      { repo: by },\n      {\n        text: text,\n        reply: {\n          root: root.raw,\n          parent: parent.raw,\n        },\n        facets,\n        embed,\n        createdAt: new Date().toISOString(),\n        ...overrides,\n      },\n      this.getHeaders(by),\n    )\n    this.replies[by] ??= []\n    const reply = {\n      text,\n      ref: new RecordRef(res.uri, res.cid),\n      images: images ?? [],\n    }\n    this.replies[by].push(reply)\n    return reply\n  }\n\n  async repost(\n    by: string,\n    subject: RecordRef,\n    overrides?: Partial<AppBskyFeedRepost.Record>,\n  ) {\n    const res = await this.agent.app.bsky.feed.repost.create(\n      { repo: by },\n      {\n        subject: subject.raw,\n        createdAt: new Date().toISOString(),\n        ...overrides,\n      },\n      this.getHeaders(by),\n    )\n    this.reposts[by] ??= []\n    const repost = new RecordRef(res.uri, res.cid)\n    this.reposts[by].push(repost)\n    return repost\n  }\n\n  async createList(\n    by: string,\n    name: string,\n    purpose: 'mod' | 'curate' | 'reference',\n    overrides?: Partial<AppBskyGraphList.Record>,\n  ) {\n    const res = await this.agent.app.bsky.graph.list.create(\n      { repo: by },\n      {\n        name,\n        purpose:\n          purpose === 'mod'\n            ? 'app.bsky.graph.defs#modlist'\n            : purpose === 'curate'\n              ? 'app.bsky.graph.defs#curatelist'\n              : 'app.bsky.graph.defs#referencelist',\n        createdAt: new Date().toISOString(),\n        ...(overrides || {}),\n      },\n      this.getHeaders(by),\n    )\n    this.lists[by] ??= {}\n    const ref = new RecordRef(res.uri, res.cid)\n    this.lists[by][ref.uriStr] = {\n      ref: ref,\n      items: {},\n    }\n    return ref\n  }\n\n  async createFeedGen(by: string, feedDid: string, name: string) {\n    const res = await this.agent.app.bsky.feed.generator.create(\n      { repo: by },\n      {\n        did: feedDid,\n        displayName: name,\n        createdAt: new Date().toISOString(),\n      },\n      this.getHeaders(by),\n    )\n    this.feedgens[by] ??= {}\n    const ref = new RecordRef(res.uri, res.cid)\n    this.feedgens[by][ref.uriStr] = {\n      ref: ref,\n      items: {},\n    }\n    return ref\n  }\n\n  async createStarterPack(\n    by: string,\n    name: string,\n    actors: string[],\n    feeds?: string[],\n  ) {\n    const list = await this.createList(by, 'n/a', 'reference')\n    for (const did of actors) {\n      await this.addToList(by, did, list)\n    }\n    const res = await this.agent.app.bsky.graph.starterpack.create(\n      { repo: by },\n      {\n        name,\n        list: list.uriStr,\n        feeds: feeds?.map((uri) => ({ uri })),\n        createdAt: new Date().toISOString(),\n      },\n      this.getHeaders(by),\n    )\n    this.starterpacks[by] ??= {}\n    const ref = new RecordRef(res.uri, res.cid)\n    this.starterpacks[by][ref.uriStr] = {\n      ref: ref,\n      list,\n      feeds: feeds ?? [],\n      name,\n    }\n    return ref\n  }\n\n  async addToList(by: string, subject: string, list: RecordRef) {\n    const res = await this.agent.app.bsky.graph.listitem.create(\n      { repo: by },\n      { subject, list: list.uriStr, createdAt: new Date().toISOString() },\n      this.getHeaders(by),\n    )\n    const ref = new RecordRef(res.uri, res.cid)\n    const found = (this.lists[by] ?? {})[list.uriStr]\n    if (found) {\n      found.items[subject] = ref\n    }\n    return ref\n  }\n\n  async rmFromList(by: string, subject: string, list: RecordRef) {\n    const foundList = (this.lists[by] ?? {})[list.uriStr] ?? {}\n    if (!foundList) return\n    const foundItem = foundList.items[subject]\n    if (!foundItem) return\n    await this.agent.app.bsky.graph.listitem.delete(\n      { repo: by, rkey: foundItem.uri.rkey },\n      this.getHeaders(by),\n    )\n    delete foundList.items[subject]\n  }\n\n  async createReport(opts: {\n    reasonType: ComAtprotoModerationCreateReport.InputSchema['reasonType']\n    subject: ComAtprotoModerationCreateReport.InputSchema['subject']\n    reason?: string\n    reportedBy: string\n  }) {\n    const { reasonType, subject, reason, reportedBy } = opts\n    const result = await this.agent.com.atproto.moderation.createReport(\n      { reasonType, subject, reason },\n      {\n        encoding: 'application/json',\n        headers: this.getHeaders(reportedBy),\n      },\n    )\n    return result.data\n  }\n\n  async verify(\n    by: string,\n    subject: string,\n    handle: string,\n    displayName: string,\n    overrides?: Partial<AppBskyGraphVerification.Record>,\n  ) {\n    const res = await this.agent.app.bsky.graph.verification.create(\n      { repo: by },\n      {\n        subject,\n        createdAt: new Date().toISOString(),\n        handle,\n        displayName,\n        ...overrides,\n      },\n      this.getHeaders(by),\n    )\n    this.verifications[by] ??= {}\n    this.verifications[by][subject] = new AtUri(res.uri)\n    return this.verifications[by][subject]\n  }\n\n  async unverify(by: string, subject: string) {\n    const verification = this.verifications[by]?.[subject]\n    if (!verification) {\n      throw new Error('verification does not exist')\n    }\n\n    await this.agent.app.bsky.graph.verification.delete(\n      { repo: by, rkey: verification.rkey },\n      this.getHeaders(by),\n    )\n    delete this.verifications[by][subject]\n  }\n\n  getHeaders(did: string) {\n    return SeedClient.getHeaders(this.accounts[did].accessJwt)\n  }\n\n  static getHeaders(jwt: string) {\n    return { authorization: `Bearer ${jwt}` }\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/follows.ts",
    "content": "import { SeedClient } from './client'\n\nexport default async (sc: SeedClient) => {\n  await sc.createAccount('alice', users.alice)\n  await sc.createAccount('bob', users.bob)\n  await sc.createAccount('carol', users.carol)\n  await sc.createAccount('dan', users.dan)\n  await sc.createAccount('eve', users.eve)\n  for (const name in sc.dids) {\n    await sc.createProfile(sc.dids[name], `display-${name}`, `descript-${name}`)\n  }\n  const alice = sc.dids.alice\n  const bob = sc.dids.bob\n  const carol = sc.dids.carol\n  const dan = sc.dids.dan\n  const eve = sc.dids.eve\n  await sc.follow(alice, bob)\n  await sc.follow(alice, carol)\n  await sc.follow(alice, dan)\n  await sc.follow(alice, eve)\n  await sc.follow(carol, alice)\n  await sc.follow(bob, alice)\n  await sc.follow(bob, carol)\n  await sc.follow(dan, alice)\n  await sc.follow(dan, bob)\n  await sc.follow(dan, eve)\n  await sc.follow(eve, alice)\n  await sc.follow(eve, carol)\n}\n\nconst users = {\n  alice: {\n    email: 'alice@test.com',\n    handle: 'alice.test',\n    password: 'alice-pass',\n  },\n  bob: {\n    email: 'bob@test.com',\n    handle: 'bob.test',\n    password: 'bob-pass',\n  },\n  carol: {\n    email: 'carol@test.com',\n    handle: 'carol.test',\n    password: 'carol-pass',\n  },\n  dan: {\n    email: 'dan@test.com',\n    handle: 'dan.test',\n    password: 'dan-pass',\n  },\n  eve: {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  },\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/index.ts",
    "content": "export * from './client'\n\nexport { default as authorFeedSeed } from './author-feed'\nexport { default as basicSeed } from './basic'\nexport { default as followsSeed } from './follows'\nexport { default as likesSeed } from './likes'\nexport { default as quotesSeed } from './quotes'\nexport { default as repostsSeed } from './reposts'\nexport { default as usersBulkSeed } from './users-bulk'\nexport { default as usersSeed } from './users'\nexport { default as verificationsSeed } from './verifications'\n"
  },
  {
    "path": "packages/dev-env/src/seed/likes.ts",
    "content": "import basicSeed from './basic'\nimport { SeedClient } from './client'\n\nexport default async (sc: SeedClient) => {\n  await basicSeed(sc)\n  await sc.createAccount('eve', {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  })\n  await sc.like(sc.dids.eve, sc.posts[sc.dids.alice][1].ref)\n  await sc.like(sc.dids.carol, sc.replies[sc.dids.bob][0].ref)\n\n  // give alice > 100 likes\n  for (let i = 0; i < 50; i++) {\n    const [b, c, d] = await Promise.all([\n      sc.post(sc.dids.bob, `bob post ${i}`),\n      sc.post(sc.dids.carol, `carol post ${i}`),\n      sc.post(sc.dids.dan, `dan post ${i}`),\n    ])\n    await Promise.all(\n      [\n        sc.like(sc.dids.alice, b.ref), // likes 50 of bobs posts\n        i < 45 && sc.like(sc.dids.alice, c.ref), // likes 45 of carols posts\n        i < 40 && sc.like(sc.dids.alice, d.ref), // likes 40 of dans posts\n      ].filter(Boolean),\n    )\n  }\n\n  // couple more NPCs for suggested follows\n  await sc.createAccount('fred', {\n    email: 'fred@test.com',\n    handle: 'fred.test',\n    password: 'fred-pass',\n  })\n  await sc.createAccount('gina', {\n    email: 'gina@test.com',\n    handle: 'gina.test',\n    password: 'gina-pass',\n  })\n\n  return sc\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/quotes.ts",
    "content": "import { default as basicSeed } from './basic'\nimport { SeedClient } from './client'\n\nexport default async (sc: SeedClient) => {\n  await basicSeed(sc)\n  await sc.createAccount('eve', {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  })\n\n  await sc.post(\n    sc.dids.eve,\n    'qUoTe 1',\n    undefined,\n    undefined,\n    sc.posts[sc.dids.alice][0].ref,\n  )\n  await sc.post(\n    sc.dids.eve,\n    'qUoTe 2',\n    undefined,\n    undefined,\n    sc.posts[sc.dids.alice][0].ref,\n  )\n\n  await sc.post(\n    sc.dids.eve,\n    'qUoTe 3',\n    undefined,\n    undefined,\n    sc.replies[sc.dids.bob][0].ref,\n  )\n\n  const carolPost = await sc.post(sc.dids.carol, 'post')\n  await sc.post(sc.dids.eve, 'qUoTe 4', undefined, undefined, carolPost.ref)\n\n  const spamPosts: Promise<any>[] = []\n  for (let i = 0; i < 5; i++) {\n    spamPosts.push(\n      sc.post(\n        sc.dids.eve,\n        `MASSIVE QUOTE SPAM ${i + 1}`,\n        undefined,\n        undefined,\n        sc.posts[sc.dids.alice][1].ref,\n      ),\n    )\n  }\n  await Promise.all(spamPosts)\n\n  return sc\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/reposts.ts",
    "content": "import basicSeed from './basic'\nimport { SeedClient } from './client'\n\nexport default async (sc: SeedClient) => {\n  await basicSeed(sc)\n  await sc.createAccount('eve', {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  })\n  await sc.repost(sc.dids.bob, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.carol, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.dan, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.eve, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.dan, sc.replies[sc.dids.bob][0].ref)\n  await sc.repost(sc.dids.eve, sc.replies[sc.dids.bob][0].ref)\n  return sc\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/thread-v2.ts",
    "content": "import { AppBskyFeedPost } from '@atproto/api'\nimport type { DatabaseSchema } from '@atproto/bsky'\nimport { TestNetwork } from '../network'\nimport { TestNetworkNoAppView } from '../network-no-appview'\nimport { RecordRef, SeedClient } from './client'\n\ntype User = {\n  id: string\n  did: string\n  email: string\n  handle: string\n  password: string\n  displayName: string\n  description: string\n  selfLabels: undefined\n}\n\nfunction createUserStub(name: string): User {\n  return {\n    id: name,\n    // @ts-ignore overwritten during seeding\n    did: undefined,\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: `${name}-pass`,\n    displayName: name,\n    description: `hi im ${name} label_me`,\n    selfLabels: undefined,\n  }\n}\n\nasync function createUsers<T extends readonly string[]>(\n  seedClient: SeedClient<TestNetwork | TestNetworkNoAppView>,\n  prefix: string,\n  handles: T,\n) {\n  const stubs = handles.reduce((acc, handle) => {\n    acc[handle] = createUserStub(`${prefix}-${handle}`)\n    return acc\n  }, {}) as Record<(typeof handles)[number], User>\n  const users = await Promise.all(\n    handles\n      .map((h) => prefix + '-' + h)\n      .map(async (handle) => {\n        const user = createUserStub(handle)\n        await seedClient.createAccount(handle, user)\n        user.did = seedClient.dids[handle]\n        return user\n      }),\n  )\n  return users.reduce((acc, user) => {\n    const id = user.id.split('-')[1]\n    acc[id].did = user.did\n    return acc\n  }, stubs)\n}\n\ntype ReplyFn = (\n  replyAuthor: User,\n  overridesOrCb?: Partial<AppBskyFeedPost.Record> | ReplyCb,\n  maybeReplyCb?: ReplyCb,\n) => Promise<void>\n\ntype ReplyCb = (r: ReplyFn) => Promise<void>\n\nexport const TAG_BUMP_DOWN = 'down'\nexport const TAG_HIDE = 'hide'\n\nconst rootReplyFnBuilder = <T extends TestNetworkNoAppView>(\n  sc: SeedClient<T>,\n  root: RecordRef,\n  parent: RecordRef,\n  prevBreadcrumbs: string,\n  posts: Record<\n    string,\n    | Awaited<ReturnType<SeedClient['post']>>\n    | Awaited<ReturnType<SeedClient['reply']>>\n  >,\n): ReplyFn => {\n  let index = 0\n  return async (\n    replyAuthor: User,\n    overridesOrCb?: Partial<AppBskyFeedPost.Record> | ReplyCb,\n    maybeReplyCb?: ReplyCb,\n  ) => {\n    let overrides: Partial<AppBskyFeedPost.Record> | undefined\n    let replyCb: ReplyCb | undefined\n    if (overridesOrCb && typeof overridesOrCb === 'function') {\n      replyCb = overridesOrCb\n    } else {\n      overrides = overridesOrCb\n      replyCb = maybeReplyCb\n    }\n\n    const breadcrumbs = prevBreadcrumbs\n      ? `${prevBreadcrumbs}.${index++}`\n      : `${index++}`\n    const text = breadcrumbs\n    const reply = await sc.reply(\n      replyAuthor.did,\n      root,\n      parent,\n      text,\n      undefined,\n      undefined,\n      overrides,\n    )\n    posts[breadcrumbs] = reply\n    // Await for this post to be processed before replying to it.\n    replyCb && (await sc.network.processAll())\n    await replyCb?.(rootReplyFnBuilder(sc, root, reply.ref, breadcrumbs, posts))\n  }\n}\n\nconst createThread = async <T extends TestNetworkNoAppView>(\n  sc: SeedClient<T>,\n  rootAuthor: User,\n  overridesOrCb?: Partial<AppBskyFeedPost.Record> | ReplyCb,\n  maybeReplyCb?: ReplyCb,\n) => {\n  let overrides: Partial<AppBskyFeedPost.Record> | undefined\n  let replyCb: ReplyCb | undefined\n  if (overridesOrCb && typeof overridesOrCb === 'function') {\n    replyCb = overridesOrCb\n  } else {\n    overrides = overridesOrCb\n    replyCb = maybeReplyCb\n  }\n\n  const replies: Record<string, Awaited<ReturnType<SeedClient['reply']>>> = {}\n  const breadcrumbs = ''\n  const text = 'root'\n  const root = await sc.post(\n    rootAuthor.did,\n    text,\n    undefined,\n    undefined,\n    undefined,\n    overrides,\n  )\n  // Await for this post to be processed before replying to it.\n  replyCb && (await sc.network.processAll())\n  await replyCb?.(\n    rootReplyFnBuilder(sc, root.ref, root.ref, breadcrumbs, replies),\n  )\n  return { root, replies }\n}\n\nexport async function simple(sc: SeedClient<TestNetwork>, prefix = 'simple') {\n  const users = await createUsers(sc, prefix, [\n    'op',\n    'alice',\n    'bob',\n    'carol',\n  ] as const)\n  const { op, alice, bob, carol } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(op, async (r) => {\n      await r(op)\n    })\n    await r(alice)\n    await r(bob, async (r) => {\n      await r(alice)\n    })\n    await r(carol)\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function long(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'long', [\n    'op',\n    'alice',\n    'bob',\n    'carol',\n    'dan',\n  ] as const)\n  const { op, alice, bob, carol, dan } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(op, async (r) => {\n      await r(op, async (r) => {\n        await r(op, async (r) => {\n          await r(op, async (r) => {\n            await r(op)\n          })\n        })\n        await r(op)\n      })\n    })\n\n    await r(alice)\n    await r(bob)\n    await r(carol)\n\n    await r(op, async (r) => {\n      await r(op, async (r) => {\n        await r(alice, async (r) => {\n          await r(op, async (r) => {\n            await r(op)\n          })\n        })\n      })\n    })\n\n    await r(alice)\n    await r(bob)\n    await r(carol)\n  })\n\n  await sc.like(op.did, r['5'].ref)\n  await sc.like(bob.did, r['5'].ref)\n  await sc.like(carol.did, r['5'].ref)\n  await sc.like(dan.did, r['5'].ref)\n\n  await sc.like(op.did, r['6'].ref)\n  await sc.like(alice.did, r['6'].ref)\n  await sc.like(carol.did, r['6'].ref)\n\n  await sc.like(op.did, r['7'].ref)\n  await sc.like(bob.did, r['7'].ref)\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function deep(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'deep', ['op'] as const)\n  const { op } = users\n\n  let counter = 0\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    const recursiveReply = async (rFn: ReplyFn) => {\n      if (counter < 18) {\n        counter++\n        await rFn(op, async (r) => recursiveReply(r))\n      }\n    }\n    await recursiveReply(r)\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function branchingFactor(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'bf', ['op', 'bob'] as const)\n  const { op, bob } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(bob, async (r) => {\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n    })\n    await r(bob, async (r) => {\n      await r(bob, async (r) => {\n        // This is the only case in this seed where a reply has 1 reply instead of 4,\n        // to have cases of different lengths in the same tree.\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n    })\n    await r(bob, async (r) => {\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n    })\n    await r(bob, async (r) => {\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        // This is the only case in this seed where a reply has 5 replies instead of 4,\n        // to have cases of different lengths in the same tree.\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n      await r(bob, async (r) => {\n        await r(bob)\n        await r(bob)\n        await r(bob)\n        await r(bob)\n      })\n    })\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function annotateMoreReplies(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'mr', ['op', 'alice'] as const)\n  const { op, alice } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(alice, async (r) => {\n      await r(alice, async (r) => {\n        await r(alice, async (r) => {\n          await r(alice, async (r) => {\n            // more replies... (below = 4)\n            await r(alice, async (r) => {\n              await r(alice)\n            })\n            await r(alice)\n            await r(alice, async (r) => {\n              await r(alice, async (r) => {\n                await r(alice)\n              })\n            })\n            await r(alice)\n            await r(alice)\n          })\n        })\n      })\n      await r(alice, async (r) => {\n        await r(alice, async (r) => {\n          await r(alice)\n        })\n      })\n    })\n    await r(alice, async (r) => {\n      await r(alice, async (r) => {\n        await r(alice)\n        await r(alice)\n        // more replies... (branchingFactor = 2)\n        await r(alice)\n        await r(alice)\n        await r(alice)\n      })\n      await r(alice, async (r) => {\n        await r(alice)\n        await r(alice)\n      })\n      // more replies... (branchingFactor = 2)\n      await r(alice)\n    })\n    await r(alice) // anchor reply not limited by branchingFactor\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function annotateOP(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'op', ['op', 'alice', 'bob'] as const)\n  const { op, alice, bob } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(op, async (r) => {\n      await r(op, async (r) => {\n        await r(op)\n      })\n    })\n    await r(alice, async (r) => {\n      await r(alice)\n    })\n    await r(op, async (r) => {\n      await r(bob, async (r) => {\n        await r(op)\n      })\n    })\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function sort(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'sort', [\n    'op',\n    'alice',\n    'bob',\n    'carol',\n  ] as const)\n  const { op, alice, bob, carol } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    // 0 likes\n    await r(alice, async (r) => {\n      await r(carol) // 0 likes\n      await r(alice) // 2 likes\n      await r(bob) // 1 like\n    })\n    // 3 likes\n    await r(carol, async (r) => {\n      await r(bob) // 1 like\n      await r(carol) // 2 likes\n      await r(alice) // 0 likes\n    })\n    // 2 likes\n    await r(bob, async (r) => {\n      await r(bob) // 2 likes\n      await r(alice) // 1 like\n      await r(carol) // 0 likes\n    })\n  })\n\n  // likes depth 1\n  await sc.like(alice.did, r['2'].ref)\n  await sc.like(carol.did, r['2'].ref)\n  await sc.like(op.did, r['1'].ref) // op like\n  await sc.like(bob.did, r['1'].ref)\n  await sc.like(carol.did, r['1'].ref)\n\n  // likes depth 2\n  await sc.like(bob.did, r['0.1'].ref)\n  await sc.like(carol.did, r['0.1'].ref)\n  await sc.like(op.did, r['0.2'].ref) // op like\n  await sc.like(bob.did, r['1.1'].ref)\n  await sc.like(carol.did, r['1.1'].ref)\n  await sc.like(bob.did, r['1.0'].ref)\n  await sc.like(bob.did, r['2.0'].ref)\n  await sc.like(carol.did, r['2.0'].ref)\n  await sc.like(bob.did, r['2.1'].ref)\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function bumpOpAndViewer(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'bumpOV', [\n    'op',\n    'viewer',\n    'alice',\n    'bob',\n    'carol',\n  ] as const)\n  const { op, viewer, alice, bob, carol } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    // 1 like\n    await r(alice, async (r) => {\n      await r(carol) // 0 likes\n      await r(alice) // 2 likes\n      await r(bob) // 1 like\n      await r(viewer) // 0 likes\n      await r(op) // 0 likes\n    })\n    // 3 likes\n    await r(carol, async (r) => {\n      await r(bob) // 1 like\n      await r(carol) // 2 likes\n      await r(op) // 0 likes\n      await r(viewer) // 1 like\n      await r(alice) // 0 likes\n    })\n    // 2 likes\n    await r(bob, async (r) => {\n      await r(viewer) // 0 likes\n      await r(bob) // 4 likes\n      await r(op) // 0 likes\n      await r(alice) // 1 like\n      await r(carol) // 1 like\n    })\n    // 0 likes\n    await r(op, async (r) => {\n      await r(viewer) // 0 likes\n      await r(bob) // 0 likes\n      await r(op) // 0 likes\n      await r(alice) // 0 likes\n      await r(carol) // 0 likes\n    })\n    // 0 likes\n    await r(viewer, async (r) => {\n      await r(bob) // 1 like\n      await r(carol) // 1 like\n      await r(op) // 0 likes\n      await r(viewer) // 0 likes\n      await r(alice) // 0 likes\n    })\n  })\n\n  // likes depth 1\n  await sc.like(alice.did, r['2'].ref)\n  await sc.like(carol.did, r['2'].ref)\n  await sc.like(viewer.did, r['0'].ref)\n  await sc.like(op.did, r['1'].ref) // op like\n  await sc.like(bob.did, r['1'].ref)\n  await sc.like(carol.did, r['1'].ref)\n\n  // likes depth 2\n  await sc.like(bob.did, r['0.1'].ref)\n  await sc.like(carol.did, r['0.1'].ref)\n  await sc.like(op.did, r['0.2'].ref) // op like\n  await sc.like(bob.did, r['1.1'].ref)\n  await sc.like(carol.did, r['1.1'].ref)\n  await sc.like(bob.did, r['1.0'].ref)\n  await sc.like(alice.did, r['2.1'].ref)\n  await sc.like(bob.did, r['2.1'].ref)\n  await sc.like(carol.did, r['2.1'].ref)\n  await sc.like(viewer.did, r['2.1'].ref)\n  await sc.like(bob.did, r['1.3'].ref)\n  await sc.like(bob.did, r['2.3'].ref)\n  await sc.like(viewer.did, r['2.4'].ref)\n  await sc.like(viewer.did, r['4.0'].ref)\n  await sc.like(alice.did, r['4.1'].ref)\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function bumpGroupSorting(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'bumpGS', [\n    'op',\n    'viewer',\n    'alice',\n  ] as const)\n  const { op, viewer, alice } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(viewer)\n    await r(op)\n    await r(alice)\n    await r(op)\n    await r(viewer)\n    await r(op)\n    await r(alice)\n    await r(viewer)\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function bumpFollows(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'bumpF', [\n    'op',\n    'viewerF',\n    'viewerNoF',\n    'alice',\n    'bob',\n    'carol',\n  ] as const)\n\n  const { op, viewerF, viewerNoF, alice, bob, carol } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(alice)\n    await r(bob)\n    await r(carol)\n    await r(op)\n    await r(viewerF)\n    await r(viewerNoF)\n  })\n\n  await sc.follow(viewerF.did, alice.did)\n  await sc.follow(viewerF.did, bob.did)\n  // Does not follow carol.\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function blockDeletionAuth(\n  sc: SeedClient<TestNetwork>,\n  labelerDid: string,\n) {\n  const users = await createUsers(sc, 'bda', [\n    'op',\n    'opBlocked',\n    'alice',\n    'auth',\n    'blocker',\n    'blocked',\n  ] as const)\n\n  const { op, opBlocked, alice, auth, blocker, blocked } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    // 1p block, hidden for `blocked`.\n    await r(blocker, async (r) => {\n      await r(alice)\n    })\n\n    // 3p block, hidden for all.\n    await r(opBlocked, async (r) => {\n      await r(op)\n      await r(alice)\n    })\n\n    // Deleted, hidden for all.\n    await r(alice, async (r) => {\n      await r(alice)\n    })\n\n    // User configured to only be seen by authenticated users.\n    // Requires the test sets a `!no-unauthenticated` label for this user.\n    await r(auth, async (r) => {\n      // Another auth-only to show that the parent chain is preserved in the thread.\n      await r(auth, async (r) => {\n        await r(alice)\n      })\n    })\n  })\n\n  await sc.deletePost(alice.did, r['2'].ref.uri)\n  await sc.block(blocker.did, blocked.did)\n  await sc.block(op.did, opBlocked.did)\n\n  const db = sc.network.bsky.db.db\n  await createLabel(db, {\n    src: labelerDid,\n    uri: auth.did,\n    cid: '',\n    val: '!no-unauthenticated',\n  })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function mutes(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'mutes', [\n    'op',\n    'opMuted',\n    'alice',\n    'muted',\n    'muter',\n  ] as const)\n\n  const { op, opMuted, alice, muted, muter } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(opMuted, async (r) => {\n      await r(alice)\n      await r(muted)\n    })\n\n    await r(muted, async (r) => {\n      await r(opMuted)\n      await r(alice)\n    })\n  })\n\n  await sc.mute(op.did, opMuted.did)\n  await sc.mute(muter.did, muted.did)\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function threadgated(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'tg', [\n    'op',\n    'opMuted',\n    'viewer',\n    'alice',\n    'bob',\n  ] as const)\n\n  const { op, opMuted, alice, bob } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    // Muted moves down below threadgated.\n    await r(opMuted)\n\n    // Threadgated moves down.\n    await r(alice, async (r) => {\n      await r(alice)\n      await r(bob)\n      await r(op) // OP moves up.\n    })\n\n    await r(bob, async (r) => {\n      await r(alice)\n      await r(bob) // Threadgated is omitted if fetched from the root.\n      await r(op) // OP moves down.\n    })\n  })\n\n  await sc.agent.app.bsky.feed.threadgate.create(\n    {\n      repo: op.did,\n      rkey: root.ref.uri.rkey,\n    },\n    {\n      post: root.ref.uriStr,\n      createdAt: new Date().toISOString(),\n      hiddenReplies: [r['1'].ref.uriStr, r['2.1'].ref.uriStr],\n    },\n    sc.getHeaders(op.did),\n  )\n\n  // Just throw a mute there to test the prioritization between muted and threadgated.\n  await sc.mute(op.did, opMuted.did)\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nexport async function tags(sc: SeedClient<TestNetwork>) {\n  const users = await createUsers(sc, 'tags', [\n    'op',\n    'alice',\n    'down',\n    'following',\n    'hide',\n    'viewer',\n  ] as const)\n\n  const { op, alice, down, following, hide, viewer } = users\n\n  const { root, replies: r } = await createThread(sc, op, async (r) => {\n    await r(alice, async (r) => {\n      await r(alice)\n      await r(down)\n      await r(hide)\n    })\n    await r(down, async (r) => {\n      await r(alice)\n      await r(down)\n      await r(hide)\n    })\n    await r(hide, async (r) => {\n      await r(alice)\n      await r(down)\n      await r(hide)\n    })\n    await r(op)\n    await r(viewer)\n    await r(following)\n  })\n\n  await sc.network.processAll()\n\n  await sc.follow(viewer.did, following.did)\n\n  const db = sc.network.bsky.db.db\n  await createTag(db, { uri: r['1'].ref.uriStr, val: TAG_BUMP_DOWN })\n  await createTag(db, { uri: r['0.1'].ref.uriStr, val: TAG_BUMP_DOWN })\n  await createTag(db, { uri: r['1.1'].ref.uriStr, val: TAG_BUMP_DOWN })\n  await createTag(db, { uri: r['2.1'].ref.uriStr, val: TAG_BUMP_DOWN })\n\n  await createTag(db, { uri: r['2'].ref.uriStr, val: TAG_HIDE })\n  await createTag(db, { uri: r['0.2'].ref.uriStr, val: TAG_HIDE })\n  await createTag(db, { uri: r['1.2'].ref.uriStr, val: TAG_HIDE })\n  await createTag(db, { uri: r['2.2'].ref.uriStr, val: TAG_HIDE })\n\n  // Neither tag affect op, viewer.\n  await createTag(db, { uri: r['3'].ref.uriStr, val: TAG_BUMP_DOWN })\n  await createTag(db, { uri: r['4'].ref.uriStr, val: TAG_HIDE })\n\n  // Tags affect following depending on the config to prioritize following.\n  await createTag(db, { uri: r['5'].ref.uriStr, val: TAG_HIDE })\n\n  return {\n    seedClient: sc,\n    users,\n    root,\n    r,\n  }\n}\n\nconst createLabel = async (\n  db: DatabaseSchema,\n  opts: {\n    src: string\n    uri: string\n    cid: string\n    val: string\n    exp?: string\n  },\n) => {\n  await db\n    .insertInto('label')\n    .values({\n      uri: opts.uri,\n      cid: opts.cid,\n      val: opts.val,\n      cts: new Date().toISOString(),\n      exp: opts.exp ?? null,\n      neg: false,\n      src: opts.src,\n    })\n    .execute()\n}\n\nconst createTag = async (\n  db: DatabaseSchema,\n  opts: {\n    uri: string\n    val: string\n  },\n) => {\n  await db\n    .updateTable('record')\n    .set({\n      tags: JSON.stringify([opts.val]),\n    })\n    .where('uri', '=', opts.uri)\n    .returningAll()\n    .execute()\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/users-bulk.ts",
    "content": "import { chunkArray } from '@atproto/common-web'\nimport { SeedClient } from './client'\n\nexport default async (sc: SeedClient, max = Infinity) => {\n  // @TODO when these are run in parallel, seem to get an intermittent\n  // \"TypeError: fetch failed\" while running the tests.\n  const userSubset = users.slice(0, Math.min(max, users.length))\n  const chunks = chunkArray(userSubset, 50)\n  for (const chunk of chunks) {\n    await Promise.all(\n      chunk.map(async (user) => {\n        const { handle, displayName } = user\n        await sc.createAccount(handle, {\n          handle: handle,\n          password: 'password',\n          email: `${handle}@bsky.app`,\n        })\n        if (displayName !== null) {\n          await sc.createProfile(sc.dids[handle], displayName, '')\n        }\n      }),\n    )\n  }\n  return sc\n}\n\nconst users = [\n  { handle: 'silas77.test', displayName: 'Tanya Denesik' },\n  { handle: 'nicolas-krajcik10.test', displayName: null },\n  { handle: 'lennie-strosin.test', displayName: null },\n  { handle: 'aliya-hodkiewicz.test', displayName: 'Carlton Abernathy IV' },\n  { handle: 'jeffrey-sawayn87.test', displayName: 'Patrick Sawayn' },\n  { handle: 'kaycee66.test', displayName: null },\n  { handle: 'adrienne49.test', displayName: 'Kim Streich' },\n  { handle: 'magnus53.test', displayName: 'Sally Funk' },\n  { handle: 'charles-spencer.test', displayName: null },\n  { handle: 'elta48.test', displayName: 'Dr. Lowell DuBuque' },\n  { handle: 'tressa-senger.test', displayName: null },\n  { handle: 'marietta-zboncak.test', displayName: null },\n  { handle: 'alexander-hickle.test', displayName: 'Winifred Harber' },\n  { handle: 'rodger-maggio24.test', displayName: 'Yolanda VonRueden' },\n  { handle: 'janiya48.test', displayName: 'Miss Terrell Ziemann' },\n  { handle: 'cayla-marquardt39.test', displayName: 'Rachel Kshlerin' },\n  { handle: 'jonathan-green.test', displayName: 'Erica Mertz' },\n  { handle: 'brycen-smith.test', displayName: null },\n  { handle: 'leonel-koch43.test', displayName: 'Karl Bosco IV' },\n  { handle: 'fidel-rath.test', displayName: null },\n  { handle: 'raleigh-metz.test', displayName: null },\n  { handle: 'kim41.test', displayName: null },\n  { handle: 'roderick-dibbert.test', displayName: null },\n  { handle: 'alec-bergnaum.test', displayName: 'Cody Berge' },\n  { handle: 'sven70.test', displayName: null },\n  { handle: 'ola-oconnell.test', displayName: null },\n  { handle: 'chauncey-klein.test', displayName: 'Kelvin Klein' },\n  { handle: 'ariel-krajcik.test', displayName: null },\n  { handle: 'murphy35.test', displayName: 'Mrs. Clifford Mertz' },\n  { handle: 'joshuah-parker11.test', displayName: null },\n  { handle: 'dewitt-wunsch.test', displayName: null },\n  { handle: 'kelton-nitzsche43.test', displayName: null },\n  { handle: 'dock-mann91.test', displayName: 'Miss Danielle Weber' },\n  { handle: 'herman-gleichner95.test', displayName: 'Kelli Schinner III' },\n  { handle: 'gerda-marquardt.test', displayName: 'Myron Wolf' },\n  { handle: 'jamil-batz.test', displayName: null },\n  { handle: 'hilario84.test', displayName: null },\n  { handle: 'kayli-bode.test', displayName: 'Miss Floyd McClure' },\n  { handle: 'elouise28.test', displayName: 'Alberta Fay' },\n  { handle: 'leann49.test', displayName: null },\n  { handle: 'javon24.test', displayName: null },\n  { handle: 'polly-shanahan45.test', displayName: null },\n  { handle: 'rosamond38.test', displayName: 'Karl Goyette' },\n  { handle: 'fredrick-mueller.test', displayName: null },\n  { handle: 'reina-runte33.test', displayName: 'Pablo Schmidt' },\n  { handle: 'bianka33.test', displayName: null },\n  { handle: 'carlos6.test', displayName: null },\n  { handle: 'jermain-smith.test', displayName: 'Eileen Stroman' },\n  { handle: 'gina97.test', displayName: null },\n  { handle: 'kiera97.test', displayName: null },\n  { handle: 'savannah-botsford.test', displayName: 'Darnell Kuvalis' },\n  { handle: 'lilliana-waters.test', displayName: null },\n  { handle: 'hailey-stroman.test', displayName: 'Elsa Schaden' },\n  { handle: 'dortha-terry.test', displayName: 'Nicole Bradtke' },\n  { handle: 'hank-powlowski32.test', displayName: null },\n  { handle: 'ervin-daugherty.test', displayName: null },\n  { handle: 'nannie18.test', displayName: null },\n  { handle: 'gilberto-watsica65.test', displayName: 'Ms. Ida Wilderman' },\n  { handle: 'kara-zieme58.test', displayName: 'Andres Towne' },\n  { handle: 'crystal-boyle.test', displayName: null },\n  { handle: 'tobin63.test', displayName: 'Alex Johnson' },\n  { handle: 'isai-kunze72.test', displayName: 'Marion Dickinson' },\n  { handle: 'paris-swift.test', displayName: null },\n  { handle: 'nestor90.test', displayName: 'Travis Hoppe' },\n  { handle: 'aliyah-flatley12.test', displayName: 'Loren Krajcik' },\n  { handle: 'maiya42.test', displayName: null },\n  { handle: 'dovie33.test', displayName: null },\n  { handle: 'kendra-ledner80.test', displayName: 'Sergio Hane' },\n  { handle: 'greyson-tromp3.test', displayName: null },\n  { handle: 'precious-fay.test', displayName: null },\n  { handle: 'kiana-schmitt39.test', displayName: null },\n  { handle: 'rhianna-stamm29.test', displayName: null },\n  { handle: 'tiara-mohr.test', displayName: null },\n  { handle: 'eleazar-balist70.test', displayName: 'Gordon Weissnat' },\n  { handle: 'bettie-bogisich96.test', displayName: null },\n  { handle: 'lura-jacobi55.test', displayName: null },\n  { handle: 'santa-hermann78.test', displayName: 'Melissa Johnson' },\n  { handle: 'dylan61.test', displayName: null },\n  { handle: 'ryley-kerluke.test', displayName: 'Alexander Purdy' },\n  { handle: 'moises-bins8.test', displayName: null },\n  { handle: 'angelita-schaef27.test', displayName: null },\n  { handle: 'natasha83.test', displayName: 'Dean Romaguera' },\n  { handle: 'sydni48.test', displayName: null },\n  { handle: 'darrion91.test', displayName: 'Jeanette Weimann' },\n  { handle: 'reynold-ortiz.test', displayName: null },\n  { handle: 'hassie-schuppe.test', displayName: 'Rita Zieme' },\n  { handle: 'clark-stehr8.test', displayName: 'Sammy Larkin' },\n  { handle: 'preston-harris.test', displayName: 'Ms. Bradford Thiel' },\n  { handle: 'benedict-schulist.test', displayName: 'Todd Stark' },\n  { handle: 'alden-wolff22.test', displayName: null },\n  { handle: 'joel-gulgowski.test', displayName: null },\n  { handle: 'joanie56.test', displayName: 'Ms. Darin Cole' },\n  { handle: 'israel-hermann0.test', displayName: 'Wilbur Schuster' },\n  { handle: 'tracy56.test', displayName: null },\n  { handle: 'kyle72.test', displayName: null },\n  { handle: 'gunnar-dare70.test', displayName: 'Mrs. Angelo Keeling' },\n  { handle: 'justus58.test', displayName: null },\n  { handle: 'brooke24.test', displayName: 'Clint Ward' },\n  { handle: 'angela-morissette.test', displayName: 'Jim Kertzmann' },\n  { handle: 'amy-bins.test', displayName: 'Angelina Hills' },\n  { handle: 'susanna81.test', displayName: null },\n  { handle: 'jailyn-hettinger50.test', displayName: 'Sheldon Ratke' },\n  { handle: 'wendell-hansen54.test', displayName: null },\n  { handle: 'jennyfer-spinka.test', displayName: 'Leticia Blick' },\n  { handle: 'alexandrea31.test', displayName: 'Leslie Von' },\n  { handle: 'hazle-davis.test', displayName: 'Ella Farrell' },\n  { handle: 'alta6.test', displayName: null },\n  { handle: 'sherwood4.test', displayName: 'Dr. Hattie Nienow I' },\n  { handle: 'marilie24.test', displayName: 'Gene Howell' },\n  { handle: 'jimmie-feeney82.test', displayName: null },\n  { handle: 'trisha-ohara.test', displayName: null },\n  { handle: 'jake-schuster33.test', displayName: 'Raymond Price' },\n  { handle: 'shane-torphy52.test', displayName: 'Sadie Carter' },\n  { handle: 'nakia-kuphal8.test', displayName: null },\n  { handle: 'lea-trantow.test', displayName: null },\n  { handle: 'joel62.test', displayName: 'Veronica Nitzsche' },\n  { handle: 'roosevelt33.test', displayName: 'Jay Moen' },\n  { handle: 'talon-okeefe85.test', displayName: null },\n  { handle: 'herman-dare.test', displayName: 'Eric White' },\n  { handle: 'flavio-fay.test', displayName: 'John Lindgren' },\n  { handle: 'elyse-prosacco.test', displayName: null },\n  { handle: 'jessyca-wiegand23.test', displayName: 'Debra Lockman' },\n  { handle: 'ara-spencer41.test', displayName: null },\n  { handle: 'frederic-fadel.test', displayName: null },\n  { handle: 'zora-gerlach.test', displayName: 'Noel Hansen' },\n  { handle: 'spencer4.test', displayName: 'Marjorie Gorczany' },\n  { handle: 'gage-wilkinson33.test', displayName: 'Preston Schoen V' },\n  { handle: 'kiley-runolfsson1.test', displayName: null },\n  { handle: 'ramona80.test', displayName: 'Sylvia Dietrich' },\n  { handle: 'rashad97.test', displayName: null },\n  { handle: 'kylie76.test', displayName: 'Josefina Pfeffer' },\n  { handle: 'alisha-zieme.test', displayName: null },\n  { handle: 'claud79.test', displayName: null },\n  { handle: 'jairo-kuvalis.test', displayName: 'Derrick Jacobson' },\n  { handle: 'delfina-emard.test', displayName: null },\n  { handle: 'waino-gutmann20.test', displayName: 'Wesley Kemmer' },\n  { handle: 'arvid-hermiston49.test', displayName: 'Vernon Towne PhD' },\n  { handle: 'hans79.test', displayName: 'Rex Hartmann' },\n  { handle: 'karlee-greenholt40.test', displayName: null },\n  { handle: 'nels-cummings.test', displayName: null },\n  { handle: 'andrew-maggio.test', displayName: null },\n  { handle: 'stephany75.test', displayName: null },\n  { handle: 'alba-lueilwitz.test', displayName: null },\n  { handle: 'fermin47.test', displayName: null },\n  { handle: 'milo-quitzon3.test', displayName: null },\n  { handle: 'eudora-dietrich4.test', displayName: 'Carol Littel' },\n  { handle: 'uriel-witting12.test', displayName: 'Sophia Schmidt' },\n  { handle: 'reuben-stracke48.test', displayName: 'Darrell Walker MD' },\n  { handle: 'letitia-sawayn11.test', displayName: 'Mrs. Sophie Reilly' },\n  { handle: 'macy-schaden.test', displayName: 'Lindsey Klein' },\n  { handle: 'imelda61.test', displayName: 'Shannon Beier' },\n  { handle: 'oswald-bailey.test', displayName: 'Angel Mann' },\n  { handle: 'pattie-fisher34.test', displayName: null },\n  { handle: 'loyce95.test', displayName: 'Claude Tromp' },\n  { handle: 'melyna-zboncak.test', displayName: null },\n  { handle: 'rowan-parisian.test', displayName: 'Mr. Veronica Feeney' },\n  { handle: 'lois-blanda20.test', displayName: 'Todd Rolfson' },\n  { handle: 'turner-bali76.test', displayName: null },\n  { handle: 'dee-hoppe65.test', displayName: null },\n  { handle: 'nikko-rosenbaum60.test', displayName: 'Joann Gutmann' },\n  { handle: 'cornell-rom53.test', displayName: null },\n  { handle: 'zack3.test', displayName: null },\n  { handle: 'fredrick41.test', displayName: 'Julius Kreiger' },\n  { handle: 'elwyn62.test', displayName: null },\n  { handle: 'isaias-hirthe37.test', displayName: 'Louis Cremin' },\n  { handle: 'ronaldo36.test', displayName: null },\n  { handle: 'jesse34.test', displayName: 'Bridget Schulist' },\n  { handle: 'darrel-mills17.test', displayName: null },\n  { handle: 'euna-mayert92.test', displayName: 'Grant Lang II' },\n  { handle: 'terrell92.test', displayName: null },\n  { handle: 'alyson-bogisich.test', displayName: 'Dana MacGyver' },\n  { handle: 'nicolas65.test', displayName: null },\n  { handle: 'bernita8.test', displayName: null },\n  { handle: 'gunner23.test', displayName: 'Maggie DuBuque' },\n  { handle: 'phoebe80.test', displayName: null },\n  { handle: 'cory-cruickshank.test', displayName: null },\n  { handle: 'conor-price.test', displayName: 'Ralph Daugherty III' },\n  { handle: 'rae91.test', displayName: null },\n  { handle: 'abigale-cronin.test', displayName: null },\n  { handle: 'aileen-reilly90.test', displayName: 'Charles Stanton' },\n  { handle: 'adrianna-hansen6.test', displayName: 'Elbert Langworth IV' },\n  { handle: 'pierre54.test', displayName: null },\n  { handle: 'jaida-stark62.test', displayName: 'Justin Stoltenberg MD' },\n  { handle: 'wade-witting.test', displayName: null },\n  { handle: 'yvonne-predovic5.test', displayName: 'Gregory Hamill' },\n  { handle: 'spencer-dubuque.test', displayName: null },\n  { handle: 'randi44.test', displayName: null },\n  { handle: 'maye-grimes.test', displayName: null },\n  { handle: 'margarette-effertz.test', displayName: null },\n  { handle: 'aimee98.test', displayName: null },\n  { handle: 'jaren-veum0.test', displayName: 'Dr. Omar Wolff' },\n  { handle: 'ariel-abbott54.test', displayName: 'Emanuel Powlowski' },\n  { handle: 'mercedes23.test', displayName: null },\n  { handle: 'jarrett-orn.test', displayName: null },\n  { handle: 'damion88.test', displayName: null },\n  { handle: 'nayeli-koss73.test', displayName: 'Johnny Lang' },\n  { handle: 'cara-wiegand69.test', displayName: null },\n  { handle: 'gideon-ohara51.test', displayName: null },\n  { handle: 'carolina-mcderm77.test', displayName: 'Latoya Windler' },\n  { handle: 'danyka90.test', displayName: 'Hope Kub' },\n]\n"
  },
  {
    "path": "packages/dev-env/src/seed/users.ts",
    "content": "import { SeedClient } from './client'\n\nexport default async (sc: SeedClient) => {\n  await sc.createAccount('alice', users.alice)\n  await sc.createAccount('bob', users.bob)\n  await sc.createAccount('carol', users.carol)\n  await sc.createAccount('dan', users.dan)\n\n  await sc.createProfile(\n    sc.dids.alice,\n    users.alice.displayName,\n    users.alice.description,\n    users.alice.selfLabels,\n  )\n  await sc.createProfile(\n    sc.dids.bob,\n    users.bob.displayName,\n    users.bob.description,\n    users.bob.selfLabels,\n  )\n\n  await sc.agent.api.chat.bsky.actor.declaration.create(\n    { repo: sc.dids.dan },\n    { allowIncoming: 'none' },\n    sc.getHeaders(sc.dids.dan),\n  )\n\n  return sc\n}\n\nconst users = {\n  alice: {\n    email: 'alice@test.com',\n    handle: 'alice.test',\n    password: 'alice-pass',\n    displayName: 'ali',\n    description: 'its me!',\n    selfLabels: ['self-label-a', 'self-label-b'],\n  },\n  bob: {\n    email: 'bob@test.com',\n    handle: 'bob.test',\n    password: 'bob-pass',\n    displayName: 'bobby',\n    description: 'hi im bob label_me',\n    selfLabels: undefined,\n  },\n  carol: {\n    email: 'carol@test.com',\n    handle: 'carol.test',\n    password: 'carol-pass',\n    displayName: undefined,\n    description: undefined,\n    selfLabels: undefined,\n  },\n  dan: {\n    email: 'dan@test.com',\n    handle: 'dan.test',\n    password: 'dan-pass',\n    displayName: undefined,\n    description: undefined,\n    selfLabels: undefined,\n  },\n}\n"
  },
  {
    "path": "packages/dev-env/src/seed/verifications.ts",
    "content": "import { INVALID_HANDLE } from '@atproto/syntax'\nimport { TestNetwork } from '../network'\nimport { SeedClient } from './client'\n\nexport default async (sc: SeedClient<TestNetwork>) => {\n  const labelerDid = sc.network.bsky.ctx.cfg.modServiceDid\n\n  await sc.createAccount('alice', users.alice)\n  await sc.createAccount('bob', users.bob)\n  await sc.createAccount('carol', users.carol)\n  await sc.createAccount('dan', users.dan)\n  await sc.createAccount('eve', users.eve)\n  await sc.createAccount('frank', users.frank)\n  await sc.createAccount('gus', users.gus)\n\n  // This user should be set a label 'impersonation` in the tests.\n  await sc.createAccount('impersonator', users.impersonator)\n\n  await sc.createAccount('nonverifier', users.nonverifier)\n\n  // These users should be set as verifiers via DB in the test,\n  // so their verifications are trusted by the app.\n  await sc.createAccount('verifier1', users.verifier1)\n  await sc.createAccount('verifier2', users.verifier2)\n  // This user should be set a label 'impersonation` in the tests.\n  await sc.createAccount('verifier3', users.verifier3)\n\n  await sc.createAccount('handleinvalid', users.handleinvalid)\n  await sc.createAccount('handleempty', users.handleempty)\n\n  for (const name in sc.dids) {\n    await sc.createProfile(sc.dids[name], `display-${name}`, `descript-${name}`)\n  }\n\n  // alice: the viewer, has no verifications at all.\n  // NOOP\n\n  // bob: has verifications by multiple verifiers.\n  await sc.verify(\n    sc.dids.verifier1,\n    sc.dids.bob,\n    sc.accounts[sc.dids.bob].handle,\n    sc.profiles[sc.dids.bob].displayName,\n  )\n  await sc.verify(\n    sc.dids.verifier2,\n    sc.dids.bob,\n    sc.accounts[sc.dids.bob].handle,\n    sc.profiles[sc.dids.bob].displayName,\n  )\n\n  // carol: has non-broken and broken verifications by verifiers.\n  await sc.verify(\n    sc.dids.verifier1,\n    sc.dids.carol,\n    sc.accounts[sc.dids.carol].handle,\n    sc.profiles[sc.dids.carol].displayName,\n  )\n  await sc.verify(\n    sc.dids.verifier2,\n    sc.dids.carol,\n    'carol.old.handle', // Broken: this was the handle during verification and it changed later.\n    sc.profiles[sc.dids.carol].displayName,\n  )\n\n  // dan: has verifications by verifiers and non verifiers.\n  await sc.verify(\n    sc.dids.verifier1,\n    sc.dids.dan,\n    sc.accounts[sc.dids.dan].handle,\n    sc.profiles[sc.dids.dan].displayName,\n  )\n  await sc.verify(\n    sc.dids.nonverifier,\n    sc.dids.dan,\n    sc.accounts[sc.dids.dan].handle,\n    sc.profiles[sc.dids.dan].displayName,\n  )\n\n  // eve: has no verifications at all.\n  // NOOP\n\n  // frank: has only broken verifications by verifiers.\n  await sc.verify(\n    sc.dids.verifier2,\n    sc.dids.frank,\n    sc.accounts[sc.dids.frank].handle,\n    'frank-old-name', // Broken: this was the name during verification and it changed later.\n  )\n\n  // gus: has only verifications by non verifiers.\n  await sc.verify(\n    sc.dids.nonverifier,\n    sc.dids.gus,\n    sc.accounts[sc.dids.gus].handle,\n    sc.profiles[sc.dids.gus].displayName,\n  )\n\n  // impersonator: has verification by verifier but should get the impersonator label during tests.\n  await sc.verify(\n    sc.dids.verifier1,\n    sc.dids.impersonator,\n    sc.accounts[sc.dids.impersonator].handle,\n    sc.profiles[sc.dids.impersonator].displayName,\n  )\n\n  // verifier1: is verifier and has verification by other verifier. Should show as a verifier, not just as verified.\n  await sc.verify(\n    sc.dids.verifier2,\n    sc.dids.verifier1,\n    sc.accounts[sc.dids.verifier1].handle,\n    sc.profiles[sc.dids.verifier1].displayName,\n  )\n\n  // verifier2: is verifier and has no verification by other verifier.\n  // NOOP\n\n  // handleinvalid: has verification but handle is set to invalid.\n  await sc.verify(\n    sc.dids.verifier1,\n    sc.dids.handleinvalid,\n    sc.accounts[sc.dids.handleinvalid].handle,\n    sc.profiles[sc.dids.handleinvalid].displayName,\n  )\n\n  // Process all events to sync actors to appview before modifying the DB\n  await sc.network.processAll()\n\n  // Do DB updates to change actors as needed for each case.\n  await createLabel(sc, labelerDid, {\n    src: labelerDid,\n    uri: sc.dids.impersonator,\n    cid: '',\n    val: 'impersonation',\n  })\n\n  await createLabel(sc, labelerDid, {\n    src: labelerDid,\n    uri: sc.dids.verifier3,\n    cid: '',\n    val: 'impersonation',\n  })\n\n  await sc.network.bsky.db.db\n    .updateTable('actor')\n    .set({ handle: INVALID_HANDLE })\n    .where('did', '=', sc.dids.handleinvalid)\n    .execute()\n  await sc.network.bsky.db.db\n    .updateTable('actor')\n    .set({ handle: null })\n    .where('did', '=', sc.dids.handleempty)\n    .execute()\n\n  return sc\n}\n\nconst users = {\n  alice: {\n    email: 'alice@test.com',\n    handle: 'alice.test',\n    password: 'alice-pass',\n  },\n  bob: {\n    email: 'bob@test.com',\n    handle: 'bob.test',\n    password: 'bob-pass',\n  },\n  carol: {\n    email: 'carol@test.com',\n    handle: 'carol.test',\n    password: 'carol-pass',\n  },\n  dan: {\n    email: 'dan@test.com',\n    handle: 'dan.test',\n    password: 'dan-pass',\n  },\n  eve: {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  },\n  frank: {\n    email: 'frank@test.com',\n    handle: 'frank.test',\n    password: 'frank-pass',\n  },\n  gus: {\n    email: 'gus@test.com',\n    handle: 'gus.test',\n    password: 'gus-pass',\n  },\n  impersonator: {\n    email: 'impersonator@test.com',\n    handle: 'impersonator.test',\n    password: 'impersonator-pass',\n  },\n  verifier1: {\n    email: 'verifier1@test.com',\n    handle: 'verifier1.test',\n    password: 'verifier1-pass',\n  },\n  verifier2: {\n    email: 'verifier2@test.com',\n    handle: 'verifier2.test',\n    password: 'verifier2-pass',\n  },\n  verifier3: {\n    email: 'verifier3@test.com',\n    handle: 'verifier3.test',\n    password: 'verifier3-pass',\n  },\n  nonverifier: {\n    email: 'nonverifier@test.com',\n    handle: 'nonverifier.test',\n    password: 'nonverifier-pass',\n  },\n  handleinvalid: {\n    email: 'handleinvalid@test.com',\n    handle: 'handleinvalid.test',\n    password: 'handleinvalid-pass',\n  },\n  handleempty: {\n    email: 'handleempty@test.com',\n    handle: 'handleempty.test',\n    password: 'handleempty-pass',\n  },\n}\n\nconst createLabel = async (\n  sc: SeedClient<TestNetwork>,\n  labelerDid: string,\n  opts: {\n    src?: string\n    uri: string\n    cid: string\n    val: string\n    exp?: string\n  },\n) => {\n  await sc.network.bsky.db.db\n    .insertInto('label')\n    .values({\n      uri: opts.uri,\n      cid: opts.cid,\n      val: opts.val,\n      cts: new Date().toISOString(),\n      exp: opts.exp ?? null,\n      neg: false,\n      src: opts.src ?? labelerDid,\n    })\n    .execute()\n}\n"
  },
  {
    "path": "packages/dev-env/src/service-profile-lexicon.ts",
    "content": "import { LexiconDoc } from '@atproto/lexicon'\nimport { TestPds } from './pds'\nimport { ServiceProfile } from './service-profile'\n\nconst LEXICONS: readonly LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'com.atproto.moderation.basePermissions',\n    defs: {\n      main: {\n        type: 'permission-set',\n        title: 'Moderation',\n        'title:lang': { fr: 'Modération' },\n        detail: 'Create moderation reports',\n        'detail:lang': {\n          'fr-FR': 'Créer des rapports de modération',\n        },\n        permissions: [\n          {\n            type: 'permission',\n            resource: 'rpc',\n            aud: '*',\n            lxm: ['com.atproto.moderation.createReport'],\n          },\n        ],\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.calendar.basePermissions',\n    defs: {\n      main: {\n        type: 'permission-set',\n        title: 'Calendar',\n        'title:lang': { fr: 'Calendrier' },\n        detail: 'Manage your events and RSVPs',\n        'detail:lang': {\n          'fr-BE': 'Gérer vos événements et réponses',\n        },\n        permissions: [\n          {\n            type: 'permission',\n            resource: 'rpc',\n            inheritAud: true,\n            lxm: [\n              'com.example.calendar.listEvents',\n              'com.example.calendar.getEventDetails',\n              'com.example.calendar.getEventRsvps',\n            ],\n          },\n          {\n            type: 'permission',\n            resource: 'repo',\n            collection: [\n              'com.example.calendar.event',\n              'com.example.calendar.rsvp',\n            ],\n          },\n          {\n            type: 'permission',\n            resource: 'blob',\n            accept: ['image/*', 'video/*'],\n          },\n        ],\n      },\n    },\n  },\n]\n\nexport class LexiconAuthorityProfile extends ServiceProfile {\n  public static async create(\n    pds: TestPds,\n    userDetails = {\n      email: 'lex-authority@test.com',\n      handle: 'lex-authority.test',\n      password: 'hunter2',\n    },\n  ) {\n    const client = pds.getClient()\n    await client.createAccount(userDetails)\n\n    return new LexiconAuthorityProfile(pds, client, userDetails)\n  }\n\n  async createRecords() {\n    await this.client.app.bsky.actor.profile.create(\n      { repo: this.did },\n      {\n        displayName: 'Lexicon Authority',\n        description: `the repo containing all the lexicons that can be resolved in dev`,\n      },\n    )\n\n    for (const doc of LEXICONS) {\n      await this.client.com.atproto.repo.createRecord({\n        repo: this.did,\n        collection: 'com.atproto.lexicon.schema',\n        rkey: doc.id,\n        record: doc,\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/service-profile-ozone.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { TestPds } from './pds'\nimport {\n  ServiceMigrationOptions,\n  ServiceProfile,\n  ServiceUserDetails,\n} from './service-profile'\n\nexport class OzoneServiceProfile extends ServiceProfile {\n  static async create(\n    pds: TestPds,\n    ozoneUrl: string,\n    userDetails = {\n      email: 'mod-authority@test.com',\n      handle: 'mod-authority.test',\n      password: 'hunter2',\n    },\n  ) {\n    const client = pds.getClient()\n    await client.createAccount(userDetails)\n\n    const key = await Secp256k1Keypair.create({ exportable: true })\n\n    return new OzoneServiceProfile(pds, client, userDetails, ozoneUrl, key)\n  }\n\n  protected constructor(\n    pds: TestPds,\n    client: AtpAgent,\n    userDetails: ServiceUserDetails,\n    readonly ozoneUrl: string,\n    readonly key: Secp256k1Keypair,\n  ) {\n    super(pds, client, userDetails)\n  }\n\n  async createAppPasswordForVerification() {\n    const { data } = await this.client.com.atproto.server.createAppPassword({\n      name: 'ozone-verifier',\n    })\n    return data.password\n  }\n\n  async migrateTo(pds: TestPds, options: ServiceMigrationOptions = {}) {\n    await super.migrateTo(pds, {\n      ...options,\n      services: {\n        ...options.services,\n        atproto_labeler: {\n          type: 'AtprotoLabeler',\n          endpoint: this.ozoneUrl,\n        },\n      },\n      verificationMethods: {\n        ...options.verificationMethods,\n        atproto_label: this.key.did(),\n      },\n    })\n  }\n\n  async createRecords() {\n    await this.client.app.bsky.actor.profile.create(\n      { repo: this.did },\n      {\n        displayName: 'Dev-env Moderation',\n        description: `The pretend version of mod.bsky.app`,\n      },\n    )\n\n    await this.client.app.bsky.labeler.service.create(\n      { repo: this.did, rkey: 'self' },\n      {\n        policies: {\n          labelValues: [\n            '!hide',\n            '!warn',\n            'porn',\n            'sexual',\n            'nudity',\n            'sexual-figurative',\n            'graphic-media',\n            'self-harm',\n            'sensitive',\n            'extremist',\n            'intolerant',\n            'threat',\n            'rude',\n            'illicit',\n            'security',\n            'unsafe-link',\n            'impersonation',\n            'misinformation',\n            'scam',\n            'engagement-farming',\n            'spam',\n            'rumor',\n            'misleading',\n            'inauthentic',\n          ],\n          labelValueDefinitions: [\n            {\n              identifier: 'spam',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Spam',\n                  description:\n                    'Unwanted, repeated, or unrelated actions that bother users.',\n                },\n              ],\n            },\n            {\n              identifier: 'impersonation',\n              blurs: 'none',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Impersonation',\n                  description:\n                    'Pretending to be someone else without permission.',\n                },\n              ],\n            },\n            {\n              identifier: 'scam',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Scam',\n                  description: 'Scams, phishing & fraud.',\n                },\n              ],\n            },\n            {\n              identifier: 'intolerant',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'warn',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Intolerance',\n                  description: 'Discrimination against protected groups.',\n                },\n              ],\n            },\n            {\n              identifier: 'self-harm',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'warn',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Self-Harm',\n                  description:\n                    'Promotes self-harm, including graphic images, glorifying discussions, or triggering stories.',\n                },\n              ],\n            },\n            {\n              identifier: 'security',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Security Concerns',\n                  description:\n                    'May be unsafe and could harm your device, steal your info, or get your account hacked.',\n                },\n              ],\n            },\n            {\n              identifier: 'misleading',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'warn',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Misleading',\n                  description:\n                    'Altered images/videos, deceptive links, or false statements.',\n                },\n              ],\n            },\n            {\n              identifier: 'threat',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Threats',\n                  description:\n                    'Promotes violence or harm towards others, including threats, incitement, or advocacy of harm.',\n                },\n              ],\n            },\n            {\n              identifier: 'unsafe-link',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Unsafe link',\n                  description:\n                    'Links to harmful sites with malware, phishing, or violating content that risk security and privacy.',\n                },\n              ],\n            },\n            {\n              identifier: 'illicit',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Illicit',\n                  description:\n                    'Promoting or selling potentially illicit goods, services, or activities.',\n                },\n              ],\n            },\n            {\n              identifier: 'misinformation',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'warn',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Misinformation',\n                  description:\n                    'Spreading false or misleading info, including unverified claims and harmful conspiracy theories.',\n                },\n              ],\n            },\n            {\n              identifier: 'rumor',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'warn',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Rumor',\n                  description:\n                    'Approach with caution, as these claims lack evidence from credible sources.',\n                },\n              ],\n            },\n            {\n              identifier: 'rude',\n              blurs: 'content',\n              severity: 'inform',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Rude',\n                  description:\n                    'Rude or impolite, including crude language and disrespectful comments, without constructive purpose.',\n                },\n              ],\n            },\n            {\n              identifier: 'extremist',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Extremist',\n                  description:\n                    'Radical views advocating violence, hate, or discrimination against individuals or groups.',\n                },\n              ],\n            },\n            {\n              identifier: 'sensitive',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'warn',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Sensitive',\n                  description:\n                    'May be upsetting, covering topics like substance abuse or mental health issues, cautioning sensitive viewers.',\n                },\n              ],\n            },\n            {\n              identifier: 'engagement-farming',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Engagement Farming',\n                  description:\n                    'Insincere content or bulk actions aimed at gaining followers, including frequent follows, posts, and likes.',\n                },\n              ],\n            },\n            {\n              identifier: 'inauthentic',\n              blurs: 'content',\n              severity: 'alert',\n              defaultSetting: 'hide',\n              adultOnly: false,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Inauthentic Account',\n                  description: 'Bot or a person pretending to be someone else.',\n                },\n              ],\n            },\n            {\n              identifier: 'sexual-figurative',\n              blurs: 'media',\n              severity: 'none',\n              defaultSetting: 'show',\n              adultOnly: true,\n              locales: [\n                {\n                  lang: 'en',\n                  name: 'Sexually Suggestive (Cartoon)',\n                  description:\n                    'Art with explicit or suggestive sexual themes, including provocative imagery or partial nudity.',\n                },\n              ],\n            },\n          ],\n        },\n        createdAt: new Date().toISOString(),\n      },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/service-profile.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { TestPds } from './pds'\n\nexport type ServiceUserDetails = {\n  email: string\n  handle: string\n  password: string\n}\n\nexport type ServiceMigrationOptions = {\n  services?: Record<string, unknown>\n  verificationMethods?: Record<string, unknown>\n}\n\nexport class ServiceProfile {\n  protected constructor(\n    protected pds: TestPds,\n    /** @note assumes the session is already authenticated */\n    protected client: AtpAgent,\n    protected userDetails: ServiceUserDetails,\n  ) {}\n\n  get did() {\n    return this.client.assertDid\n  }\n\n  async migrateTo(newPds: TestPds, options: ServiceMigrationOptions = {}) {\n    const newClient = newPds.getClient()\n\n    const newPdsDesc = await newClient.com.atproto.server.describeServer()\n    const serviceAuth = await this.client.com.atproto.server.getServiceAuth({\n      aud: newPdsDesc.data.did,\n      lxm: 'com.atproto.server.createAccount',\n    })\n\n    const inviteCode = newPds.ctx.cfg.invites.required\n      ? await newClient.com.atproto.server\n          .createInviteCode(\n            { useCount: 1 },\n            {\n              encoding: 'application/json',\n              headers: newPds.adminAuthHeaders(),\n            },\n          )\n          .then((res) => res.data.code)\n      : undefined\n\n    await newClient.createAccount(\n      {\n        ...this.userDetails,\n        inviteCode,\n        did: this.did,\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: `Bearer ${serviceAuth.data.token}` },\n      },\n    )\n\n    // The session manager will use the \"didDoc\" in the result of\n    // \"createAccount\" in order to setup the pdsUrl. However, since are in the\n    // process of migrating, that didDoc references the old PDS. In order to\n    // avoid calling the old PDS, let's clear the pdsUrl, which will result in\n    // the (new) serviceUrl being used.\n    newClient.sessionManager.pdsUrl = undefined\n\n    const newDidCredentialsRes =\n      await newClient.com.atproto.identity.getRecommendedDidCredentials()\n\n    await this.client.com.atproto.identity.requestPlcOperationSignature()\n    const { token } = await this.pds.ctx.accountManager.db.db\n      .selectFrom('email_token')\n      .select('token')\n      .where('did', '=', this.did)\n      .where('purpose', '=', 'plc_operation')\n      .executeTakeFirstOrThrow()\n\n    const op = { ...newDidCredentialsRes.data, token }\n    Object.assign((op.services ??= {}), options.services)\n    Object.assign((op.verificationMethods ??= {}), options.verificationMethods)\n\n    const signedPlcOperation =\n      await this.client.com.atproto.identity.signPlcOperation(op)\n\n    await newClient.com.atproto.identity.submitPlcOperation({\n      operation: signedPlcOperation.data.operation,\n    })\n\n    await newClient.com.atproto.server.activateAccount()\n\n    this.pds = newPds\n    this.client = newClient\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/src/types.ts",
    "content": "import * as bsky from '@atproto/bsky'\nimport * as bsync from '@atproto/bsync'\nimport { ExportableKeypair, Keypair } from '@atproto/crypto'\nimport * as ozone from '@atproto/ozone'\nimport * as pds from '@atproto/pds'\n\nexport type IntrospectConfig = {\n  port?: number\n}\n\nexport type PlcConfig = {\n  port?: number\n  version?: string\n}\n\nexport type PdsConfig = Partial<pds.ServerEnvironment> & {\n  didPlcUrl: string\n  migration?: string\n}\n\nexport type BskyConfig = Partial<bsky.ServerConfig> & {\n  plcUrl: string\n  repoProvider: string\n  dbPostgresUrl: string\n  dbPostgresSchema: string\n  redisHost: string\n  pdsPort: number\n  migration?: string\n  privateKey?: string\n}\n\nexport type BsyncConfig = Partial<bsync.ServerEnvironment> & {\n  dbUrl: string\n}\n\nexport type OzoneConfig = Partial<ozone.OzoneEnvironment> & {\n  plcUrl: string\n  appviewUrl: string\n  appviewDid: string\n  dbPostgresUrl: string\n  migration?: string\n  signingKey?: ExportableKeypair\n  imgInvalidator?: ozone.ImageInvalidator\n}\n\nexport type TestServerParams = {\n  dbPostgresUrl: string\n  dbPostgresSchema: string\n  pds: Partial<PdsConfig>\n  plc: Partial<PlcConfig>\n  bsky: Partial<BskyConfig>\n  ozone: Partial<OzoneConfig>\n  introspect: Partial<IntrospectConfig>\n}\n\nexport type DidAndKey = {\n  did: string\n  key: Keypair\n}\n"
  },
  {
    "path": "packages/dev-env/src/util.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { request } from 'undici'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { TestBsky } from './bsky'\nimport { TestPds } from './pds'\nimport { DidAndKey } from './types'\n\nexport const mockNetworkUtilities = (pds: TestPds, bsky?: TestBsky) => {\n  mockResolvers(pds.ctx.idResolver, pds)\n  if (bsky) {\n    mockResolvers(bsky.ctx.idResolver, pds)\n    mockResolvers(bsky.dataplane.idResolver, pds)\n  }\n}\n\nexport const mockResolvers = (idResolver: IdResolver, pds: TestPds) => {\n  // Map pds public url to its local url when resolving from plc\n  const origResolveDid = idResolver.did.resolveNoCache\n  idResolver.did.resolveNoCache = async (did: string) => {\n    const result = await (origResolveDid.call(\n      idResolver.did,\n      did,\n    ) as ReturnType<typeof origResolveDid>)\n    const service = result?.service?.find((svc) => svc.id === '#atproto_pds')\n    if (typeof service?.serviceEndpoint === 'string') {\n      service.serviceEndpoint = service.serviceEndpoint.replace(\n        pds.ctx.cfg.service.publicUrl,\n        `http://localhost:${pds.port}`,\n      )\n    }\n    return result\n  }\n\n  const origResolveHandleDns = idResolver.handle.resolveDns\n  idResolver.handle.resolve = async (handle: string) => {\n    const isPdsHandle = pds.ctx.cfg.identity.serviceHandleDomains.some(\n      (domain) => handle.endsWith(domain),\n    )\n    if (!isPdsHandle) {\n      return origResolveHandleDns.call(idResolver.handle, handle)\n    }\n\n    const url = new URL(`/.well-known/atproto-did`, pds.url)\n    try {\n      const res = await request(url, { headers: { host: handle } })\n      if (res.statusCode !== 200) {\n        await res.body.dump()\n        return undefined\n      }\n\n      return res.body.text()\n    } catch (err) {\n      return undefined\n    }\n  }\n}\n\nexport const mockMailer = (pds: TestPds) => {\n  const mailer = pds.ctx.mailer\n  const _origSendMail = mailer.transporter.sendMail\n  mailer.transporter.sendMail = async (opts) => {\n    const result = await _origSendMail.call(mailer.transporter, opts)\n    console.log(`✉️ Email: ${JSON.stringify(result, null, 2)}`)\n    return result\n  }\n}\n\nconst usedLockIds = new Set()\nexport const uniqueLockId = () => {\n  let lockId: number\n  do {\n    lockId = 1000 + Math.ceil(1000 * Math.random())\n  } while (usedLockIds.has(lockId))\n  usedLockIds.add(lockId)\n  return lockId\n}\n\nexport const createDidAndKey = async (opts: {\n  plcUrl: string\n  handle: string\n  pds: string\n}): Promise<DidAndKey> => {\n  const { plcUrl, handle, pds } = opts\n  const key = await Secp256k1Keypair.create({ exportable: true })\n  const did = await new plc.Client(plcUrl).createDid({\n    signingKey: key.did(),\n    rotationKeys: [key.did()],\n    handle,\n    pds,\n    signer: key,\n  })\n  return {\n    key,\n    did,\n  }\n}\n"
  },
  {
    "path": "packages/dev-env/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/dev-env/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/dev-infra/README.md",
    "content": "# dev-infra\n\nHelpers for working with postgres and redis locally. Previously known as `pg`.\n\n## Usage\n\n### `with-test-db.sh`\n\nThis script allows you to run any command with a fresh, ephemeral/single-use postgres database available. When the script starts a Dockerized postgres container starts-up, and when the script completes that container is removed.\n\nThe environment variable `DB_POSTGRES_URL` will be set with a connection string that can be used to connect to the database. The [`PG*` environment variables](https://www.postgresql.org/docs/current/libpq-envars.html) that are recognized by libpq (i.e. used by the `psql` client) are also set.\n\n**Example**\n\n```\n$ ./with-test-db.sh psql -c 'select 1;'\n[+] Running 1/1\n ⠿ Container pg-db_test-1  Healthy                                                           1.8s\n\n ?column?\n----------\n        1\n(1 row)\n\n\n[+] Running 1/1\n ⠿ Container pg-db_test-1  Stopped                                                           0.1s\nGoing to remove pg-db_test-1\n[+] Running 1/0\n ⠿ Container pg-db_test-1  Removed\n```\n\n### `with-redis-and-test-db.sh`\n\nThis script is similar to `with-test-db.sh`, but in addition to an ephemeral/single-use postgres database it also provides a single-use redis instance. When the script starts, Dockerized postgres and redis containers start-up, and when the script completes the containers are removed.\n\nThe environment variables `DB_POSTGRES_URL` and `REDIS_HOST` will be set with a connection strings that can be used to connect to postgres and redis respectively.\n\n### `docker-compose.yaml`\n\nThe Docker compose file can be used to run containerized versions of postgres either for single use (as is used by `with-test-db.sh`), or for longer-term use. These are setup as separate services named `db_test` and `db` respectively. In both cases the database is available on the host machine's `localhost` and credentials are:\n\n- Username: pg\n- Password: password\n\nHowever, each service uses a different port, documented below, to avoid conflicts.\n\n#### `db_test` service for single use\n\nThe single-use `db_test` service does not have any persistent storage. When the container is removed, data in the database disappears with it.\n\nThis service runs on port `5433`.\n\n```\n$ docker compose up db_test   # start container\n$ docker compose stop db_test # stop container\n$ docker compose rm db_test   # remove container\n```\n\n#### `db` service for persistent use\n\nThe `db` service has persistent storage on the host machine managed by Docker under a volume named `pg_atp_db`. When the container is removed, data in the database will remain on the host machine. In order to start fresh, you would need to remove the volume.\n\nThis service runs on port `5432`.\n\n```\n$ docker compose up db -d    # start container\n$ docker compose stop db     # stop container\n$ docker compose rm db       # remove container\n$ docker volume rm pg_atp_db # remove volume\n```\n\n#### `redis_test` service for single use\n\nThe single-use `redis_test` service does not have any persistent storage. When the container is removed, the data in redis disappears with it.\n\nThis service runs on port `6380`.\n\n#### `redis` service for persistent use\n\nThe `redis` service has persistent storage on the host machine managed by Docker under a volume named `atp_redis`. When the container is removed, the data in redis will remain on the host machine. In order to start fresh, you would need to remove the volume.\n\nThis service runs on port `6379`.\n"
  },
  {
    "path": "packages/dev-infra/_common.sh",
    "content": "#!/usr/bin/env sh\n\n# Exit if any command fails\nset -e\n\nget_container_id() {\n  local compose_file=$1\n  local service=$2\n  if [ -z \"${compose_file}\" ] || [ -z \"${service}\" ]; then\n    echo \"usage: get_container_id <compose_file> <service>\"\n    exit 1\n  fi\n\n # first line of jq normalizes for docker compose breaking change, see docker/compose#10958\n  docker compose --file $compose_file ps --format json --status running \\\n    | jq -sc '.[] | if type==\"array\" then .[] else . end' | jq -s \\\n    | jq -r '.[]? | select(.Service == \"'${service}'\") | .ID'\n}\n\n# Exports all environment variables\nexport_env() {\n  export_pg_env\n  export_redis_env\n}\n\n# Exports postgres environment variables\nexport_pg_env() {\n  # Based on creds in compose.yaml\n  export PGPORT=5433\n  export PGHOST=localhost\n  export PGUSER=pg\n  export PGPASSWORD=password\n  export PGDATABASE=postgres\n  export DB_POSTGRES_URL=\"postgresql://pg:password@127.0.0.1:5433/postgres\"\n}\n\n# Exports redis environment variables\nexport_redis_env() {\n  export REDIS_HOST=\"127.0.0.1:6380\"\n}\n\npg_clear() {\n  local pg_uri=$1\n\n  for schema_name in `psql \"${pg_uri}\" -c \"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE 'information_schema';\" -t`; do\n    psql \"${pg_uri}\" -c \"DROP SCHEMA \\\"${schema_name}\\\" CASCADE;\"\n  done\n}\n\npg_init() {\n  local pg_uri=$1\n\n  psql \"${pg_uri}\" -c \"CREATE SCHEMA IF NOT EXISTS \\\"public\\\";\"\n}\n\nredis_clear() {\n  local redis_uri=$1\n  redis-cli -u \"${redis_uri}\" flushall\n}\n\nmain_native() {\n  local services=${SERVICES}\n  local postgres_url_env_var=`[[ $services == *\"db_test\"* ]] && echo \"DB_TEST_POSTGRES_URL\" || echo \"DB_POSTGRES_URL\"`\n  local redis_host_env_var=`[[ $services == *\"redis_test\"* ]] && echo \"REDIS_TEST_HOST\" || echo \"REDIS_HOST\"`\n\n  postgres_url=\"${!postgres_url_env_var}\"\n  redis_host=\"${!redis_host_env_var}\"\n\n  if [ -n \"${postgres_url}\" ]; then\n    echo \"Using ${postgres_url_env_var} (${postgres_url}) to connect to postgres.\"\n    pg_init \"${postgres_url}\"\n  else\n    echo \"Postgres connection string missing did you set ${postgres_url_env_var}?\"\n    exit 1\n  fi\n\n  if [ -n \"${redis_host}\" ]; then\n    echo \"Using ${redis_host_env_var} (${redis_host}) to connect to Redis.\"\n  else\n    echo \"Redis connection string missing did you set ${redis_host_env_var}?\"\n    echo \"Continuing without Redis...\"\n  fi\n\n  cleanup() {\n    local services=$@\n\n    if [ -n \"${redis_host}\" ] && [[ $services == *\"redis_test\"* ]]; then\n      redis_clear \"redis://${redis_host}\" &> /dev/null\n    fi\n\n    if [ -n \"${postgres_url}\" ] && [[ $services == *\"db_test\"* ]]; then\n      pg_clear \"${postgres_url}\" &> /dev/null\n    fi\n  }\n\n  # trap SIGINT and performs cleanup\n  trap \"on_sigint ${services}\" INT\n  on_sigint() {\n    cleanup $@\n    exit $?\n  }\n\n  # Run the arguments as a command\n  DB_POSTGRES_URL=\"${postgres_url}\" \\\n  REDIS_HOST=\"${redis_host}\" \\\n  \"$@\"\n  code=$?\n\n  cleanup ${services}\n\n  exit ${code}\n}\n\nmain_docker() {\n  # Expect a SERVICES env var to be set with the docker service names\n  local services=${SERVICES}\n\n  dir=$(dirname $0)\n  compose_file=\"${dir}/docker-compose.yaml\"\n\n  # whether this particular script started the container(s)\n  started_container=false\n\n  # performs cleanup as necessary, i.e. taking down containers\n  # if this script started them\n  cleanup() {\n    local services=$@\n    echo # newline\n    if $started_container; then\n      docker compose --file $compose_file rm --force --stop --volumes ${services}\n    fi\n  }\n\n  # trap SIGINT and performs cleanup\n  trap \"on_sigint ${services}\" INT\n  on_sigint() {\n    cleanup $@\n    exit $?\n  }\n\n  # check if all services are running already\n  not_running=false\n  for service in $services; do\n    container_id=$(get_container_id $compose_file $service)\n    if [ -z $container_id ]; then\n      not_running=true\n      break\n    fi\n  done\n\n  # if any are missing, recreate all services\n  if $not_running; then\n    started_container=true\n    docker compose --file $compose_file up --wait --force-recreate ${services}\n  else\n    echo \"all services ${services} are already running\"\n  fi\n\n  # do not exit when following commands fail, so we can intercept exit code & tear down docker\n  set +e\n\n  # setup environment variables and run args\n  export_env\n  \"$@\"\n  # save return code for later\n  code=$?\n\n  # performs cleanup as necessary\n  cleanup ${services}\n  exit ${code}\n}\n\n# Main entry point\nmain() {\n  if ! docker ps >/dev/null 2>&1; then\n    echo \"Docker unavailable. Running on host.\"\n    main_native $@\n  else\n    main_docker $@\n  fi\n}\n"
  },
  {
    "path": "packages/dev-infra/docker-compose.yaml",
    "content": "services:\n  # An ephermerally-stored postgres database for single-use test runs\n  db_test: &db_test\n    image: postgres:14.4-alpine\n    environment:\n      - POSTGRES_USER=pg\n      - POSTGRES_PASSWORD=password\n    ports:\n      - '5433:5432'\n    # Healthcheck ensures db is queryable when `docker-compose up --wait` completes\n    healthcheck:\n      test: 'pg_isready -U pg'\n      interval: 500ms\n      timeout: 10s\n      retries: 20\n  # A persistently-stored postgres database\n  db:\n    <<: *db_test\n    ports:\n      - '5432:5432'\n    healthcheck:\n      disable: true\n    volumes:\n      - atp_db:/var/lib/postgresql/data\n  # An ephermerally-stored redis cache for single-use test runs\n  redis_test: &redis_test\n    image: redis:7.0-alpine\n    ports:\n      - '6380:6379'\n    # Healthcheck ensures redis is queryable when `docker-compose up --wait` completes\n    healthcheck:\n      test: ['CMD-SHELL', '[ \"$$(redis-cli ping)\" = \"PONG\" ]']\n      interval: 500ms\n      timeout: 10s\n      retries: 20\n  # A persistently-stored redis cache\n  redis:\n    <<: *redis_test\n    command: redis-server --save 60 1 --loglevel warning\n    ports:\n      - '6379:6379'\n    healthcheck:\n      disable: true\n    volumes:\n      - atp_redis:/data\nvolumes:\n  atp_db:\n  atp_redis:\n"
  },
  {
    "path": "packages/dev-infra/with-test-db.sh",
    "content": "#!/usr/bin/env sh\n\n# Example usage:\n# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'\n\ndir=$(dirname $0)\n. ${dir}/_common.sh\n\nSERVICES=\"db_test\" main \"$@\"\n"
  },
  {
    "path": "packages/dev-infra/with-test-redis-and-db.sh",
    "content": "#!/usr/bin/env sh\n\n# Example usage:\n# ./with-test-redis-and-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'\n# ./with-test-redis-and-db.sh redis-cli -h localhost -p 6380 ping\n\ndir=$(dirname $0)\n. ${dir}/_common.sh\n\nSERVICES=\"db_test redis_test\" main \"$@\"\n"
  },
  {
    "path": "packages/did/CHANGELOG.md",
    "content": "# @atproto/did\n\n## 0.3.0\n\n### Minor Changes\n\n- [#4580](https://github.com/bluesky-social/atproto/pull/4580) [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow DID document to omit the `@context` root property\n\n- [#4580](https://github.com/bluesky-social/atproto/pull/4580) [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disallow string values in `verificationMethod` array (as per spec)\n\n## 0.2.4\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose new `AtprotoDidDocument` type\n\n## 0.2.3\n\n### Patch Changes\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export `AtprotoData` type\n\n- [#4384](https://github.com/bluesky-social/atproto/pull/4384) [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `matchesIdentifier` and `extractAtprotoData` utilities.\n\n## 0.2.2\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `extractPdsUrl` utility\n\n## 0.2.1\n\n### Patch Changes\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor typing improvements\n\n## 0.2.0\n\n### Minor Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce proper formatting of relative uris fragments\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export `AtprotoAudience` type and validation function\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Small performance improvement\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Small performance improvement\n\n## 0.1.5\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n## 0.1.4\n\n### Patch Changes\n\n- [#3454](https://github.com/bluesky-social/atproto/pull/3454) [`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix encoding and decoding of did:web\n\n## 0.1.3\n\n### Patch Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add atprotoDidSchema to validate Atproto supported DID's using zod\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2776](https://github.com/bluesky-social/atproto/pull/2776) [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disallow path component in Web DID's (as per spec)\n\n- [#2776](https://github.com/bluesky-social/atproto/pull/2776) [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly parse localhost did:web\n\n- [#2776](https://github.com/bluesky-social/atproto/pull/2776) [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Code optimizations and documentation. Rename `check*` utility function to `assert*`.\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose atproto specific types and utilities\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n"
  },
  {
    "path": "packages/did/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'PDS',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  // Jest requires all ESM dependencies to be transpiled (even if they are\n  // dynamically import()ed).\n  transformIgnorePatterns: [\n    `/node_modules/.pnpm/(?!(get-port|lande|toygrad)@)`,\n  ],\n  testTimeout: 60000,\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/did/package.json",
    "content": "{\n  \"name\": \"@atproto/did\",\n  \"version\": \"0.3.0\",\n  \"license\": \"MIT\",\n  \"description\": \"DID resolution and verification library\",\n  \"keywords\": [\n    \"atproto\",\n    \"did\",\n    \"validation\",\n    \"types\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/did\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@swc/jest\": \"^0.2.24\",\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"jest\"\n  }\n}\n"
  },
  {
    "path": "packages/did/src/atproto.ts",
    "content": "import { z } from 'zod'\nimport { DidDocument, DidService } from './did-document.js'\nimport { DidError, InvalidDidError } from './did-error.js'\nimport { Did } from './did.js'\nimport { canParse, isFragment } from './lib/uri.js'\nimport {\n  DID_PLC_PREFIX,\n  DID_WEB_PREFIX,\n  assertDidPlc,\n  assertDidWeb,\n  isDidPlc,\n  isDidWeb,\n} from './methods.js'\nimport { Identifier, matchesIdentifier } from './utils.js'\n\n// This file contains atproto-specific DID validation utilities.\n\nexport type AtprotoIdentityDidMethods = 'plc' | 'web'\nexport type AtprotoDid = Did<AtprotoIdentityDidMethods>\nexport type AtprotoDidDocument = DidDocument<AtprotoIdentityDidMethods>\n\nexport const atprotoDidSchema = z\n  .string()\n  .refine(isAtprotoDid, `Atproto only allows \"plc\" and \"web\" DID methods`)\n\nexport function isAtprotoDid(input: unknown): input is AtprotoDid {\n  return isDidPlc(input) || isAtprotoDidWeb(input)\n}\n\nexport function asAtprotoDid<T>(input: T) {\n  assertAtprotoDid(input)\n  return input\n}\n\nexport function assertAtprotoDid(input: unknown): asserts input is AtprotoDid {\n  if (typeof input !== 'string') {\n    throw new InvalidDidError(typeof input, `DID must be a string`)\n  } else if (input.startsWith(DID_PLC_PREFIX)) {\n    assertDidPlc(input)\n  } else if (input.startsWith(DID_WEB_PREFIX)) {\n    assertAtprotoDidWeb(input)\n  } else {\n    throw new InvalidDidError(\n      input,\n      `Atproto only allows \"plc\" and \"web\" DID methods`,\n    )\n  }\n}\n\nexport function assertAtprotoDidWeb(\n  input: unknown,\n): asserts input is Did<'web'> {\n  assertDidWeb(input)\n\n  if (isDidWebWithPath(input)) {\n    throw new InvalidDidError(\n      input,\n      `Atproto does not allow path components in Web DIDs`,\n    )\n  }\n\n  if (isDidWebWithHttpsPort(input)) {\n    throw new InvalidDidError(\n      input,\n      `Atproto does not allow port numbers in Web DIDs, except for localhost`,\n    )\n  }\n}\n\n/**\n * @see {@link https://atproto.com/specs/did#blessed-did-methods}\n */\nexport function isAtprotoDidWeb(input: unknown): input is Did<'web'> {\n  if (!isDidWeb(input)) {\n    return false\n  }\n\n  if (isDidWebWithPath(input)) {\n    return false\n  }\n\n  if (isDidWebWithHttpsPort(input)) {\n    return false\n  }\n\n  return true\n}\n\nfunction isDidWebWithPath(did: Did<'web'>): boolean {\n  return did.includes(':', DID_WEB_PREFIX.length)\n}\n\nfunction isLocalhostDid(did: Did<'web'>): boolean {\n  return (\n    did === 'did:web:localhost' ||\n    did.startsWith('did:web:localhost:') ||\n    did.startsWith('did:web:localhost%3A')\n  )\n}\n\nfunction isDidWebWithHttpsPort(did: Did<'web'>): boolean {\n  if (isLocalhostDid(did)) return false\n\n  const pathIdx = did.indexOf(':', DID_WEB_PREFIX.length)\n\n  const hasPort =\n    pathIdx === -1\n      ? // No path component, check if there's a port separator anywhere after\n        // the \"did:web:\" prefix\n        did.includes('%3A', DID_WEB_PREFIX.length)\n      : // There is a path component; if there is an encoded colon *before* it,\n        // then there is a port number\n        did.lastIndexOf('%3A', pathIdx) !== -1\n\n  return hasPort\n}\n\nexport type AtprotoAudience = `${AtprotoDid}#${string}`\nexport const isAtprotoAudience = (value: unknown): value is AtprotoAudience => {\n  if (typeof value !== 'string') return false\n  const hashIndex = value.indexOf('#')\n  if (hashIndex === -1) return false\n  if (value.indexOf('#', hashIndex + 1) !== -1) return false\n  return (\n    isFragment(value, hashIndex + 1) && isAtprotoDid(value.slice(0, hashIndex))\n  )\n}\n\nexport type AtprotoData<\n  M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods,\n> = {\n  did: Did<M>\n  aka?: string\n  key?: AtprotoVerificationMethod<M>\n  pds?: AtprotoPersonalDataServerService<M>\n}\n\nexport function extractAtprotoData<M extends AtprotoIdentityDidMethods>(\n  document: DidDocument<M>,\n): AtprotoData<M> {\n  return {\n    did: document.id,\n    aka: document.alsoKnownAs?.find(isAtprotoAka)?.slice(5),\n    key: document.verificationMethod?.find(\n      isAtprotoVerificationMethod<M>,\n      document,\n    ),\n    pds: document.service?.find(\n      isAtprotoPersonalDataServerService<M>,\n      document,\n    ),\n  }\n}\n\nexport function extractPdsUrl(document: AtprotoDidDocument): URL {\n  const service = document.service?.find(\n    isAtprotoPersonalDataServerService,\n    document,\n  )\n\n  if (!service) {\n    throw new DidError(\n      document.id,\n      `Document ${document.id} does not contain a (valid) #atproto_pds service URL`,\n      'did-service-not-found',\n    )\n  }\n\n  return new URL(service.serviceEndpoint)\n}\n\nexport type AtprotoAka = `at://${string}`\nexport function isAtprotoAka(value: string): value is AtprotoAka {\n  return value.startsWith('at://')\n}\n\nexport type AtprotoPersonalDataServerService<\n  M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods,\n> = DidService & {\n  id: Identifier<Did<M>, 'atproto_pds'>\n  type: 'AtprotoPersonalDataServer'\n  serviceEndpoint: string\n}\n\nexport function isAtprotoPersonalDataServerService<\n  M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods,\n>(\n  this: DidDocument<M>,\n  service: null | undefined | DidService,\n): service is AtprotoPersonalDataServerService<M> {\n  return (\n    service?.type === 'AtprotoPersonalDataServer' &&\n    typeof service.serviceEndpoint === 'string' &&\n    canParse(service.serviceEndpoint) &&\n    matchesIdentifier(this.id, 'atproto_pds', service.id)\n  )\n}\n\nexport const ATPROTO_VERIFICATION_METHOD_TYPES = Object.freeze([\n  'EcdsaSecp256r1VerificationKey2019',\n  'EcdsaSecp256k1VerificationKey2019',\n  'Multikey',\n] as const)\nexport type SupportedAtprotoVerificationMethodType =\n  (typeof ATPROTO_VERIFICATION_METHOD_TYPES)[number]\n\ntype VerificationMethod = NonNullable<DidDocument['verificationMethod']>[number]\nexport type AtprotoVerificationMethod<\n  M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods,\n> = Extract<VerificationMethod, object> & {\n  id: Identifier<Did<M>, 'atproto'>\n  type: SupportedAtprotoVerificationMethodType\n  publicKeyMultibase: string\n}\n\nexport function isAtprotoVerificationMethod<\n  M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods,\n>(\n  this: DidDocument<M>,\n  method:\n    | null\n    | undefined\n    | NonNullable<DidDocument<M>['verificationMethod']>[number],\n): method is AtprotoVerificationMethod<M> {\n  return (\n    typeof method === 'object' &&\n    typeof method?.publicKeyMultibase === 'string' &&\n    (ATPROTO_VERIFICATION_METHOD_TYPES as readonly unknown[]).includes(\n      method.type,\n    ) &&\n    matchesIdentifier(this.id, 'atproto', method.id)\n  )\n}\n"
  },
  {
    "path": "packages/did/src/did-document.ts",
    "content": "import { z } from 'zod'\nimport { Did, didSchema } from './did.js'\nimport { isFragment } from './lib/uri.js'\n\n/**\n * RFC3968 compliant URI\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc3986}\n */\nconst rfc3968UriSchema = z.string().url('RFC3968 compliant URI')\n\nconst didControllerSchema = z.union([didSchema, z.array(didSchema)])\n\n/**\n * @note this schema is too permissive\n */\nconst didRelativeUriSchema = z.union([\n  rfc3968UriSchema.refine(\n    (value) => {\n      const fragmentIndex = value.indexOf('#')\n      if (fragmentIndex === -1) return false\n      return isFragment(value, fragmentIndex + 1)\n    },\n    {\n      message: 'Missing or invalid fragment in RFC3968 URI',\n    },\n  ),\n  z\n    .string()\n    .refine((value) => value.charCodeAt(0) === 35 /* # */, {\n      message: 'Fragment must start with #',\n    })\n    .refine((value) => isFragment(value, 1), {\n      message: 'Invalid char in URI fragment',\n    }),\n])\n\n/**\n * @see {@link https://www.w3.org/TR/did-1.0/#verification-material Verification Material}\n */\nconst didVerificationMethodSchema = z.object({\n  id: didRelativeUriSchema,\n  type: z.string().min(1),\n  controller: didControllerSchema,\n  publicKeyJwk: z.record(z.string(), z.unknown()).optional(),\n  publicKeyMultibase: z.string().optional(),\n})\n\n/**\n * The value of the id property MUST be a URI conforming to [RFC3986]. A\n * conforming producer MUST NOT produce multiple service entries with the same\n * id. A conforming consumer MUST produce an error if it detects multiple\n * service entries with the same id.\n *\n * @note Normally, only rfc3968UriSchema should be allowed here. However, the\n *   did:plc uses relative URI. For this reason, we also allow relative URIs\n *   here.\n */\nconst didServiceIdSchema = didRelativeUriSchema\n\n/**\n * The value of the type property MUST be a string or a set of strings. In order\n * to maximize interoperability, the service type and its associated properties\n * SHOULD be registered in the DID Specification Registries\n * [DID-SPEC-REGISTRIES].\n */\nconst didServiceTypeSchema = z.union([z.string(), z.array(z.string())])\n\n/**\n * The value of the serviceEndpoint property MUST be a string, a map, or a set\n * composed of one or more strings and/or maps. All string values MUST be valid\n * URIs conforming to [RFC3986] and normalized according to the Normalization\n * and Comparison rules in RFC3986 and to any normalization rules in its\n * applicable URI scheme specification.\n */\nconst didServiceEndpointSchema = z.union([\n  rfc3968UriSchema,\n  z.record(z.string(), rfc3968UriSchema),\n  z\n    .array(z.union([rfc3968UriSchema, z.record(z.string(), rfc3968UriSchema)]))\n    .nonempty(),\n])\n\n/**\n * Each service map MUST contain id, type, and serviceEndpoint properties.\n * @see {@link https://www.w3.org/TR/did-core/#services}\n */\nconst didServiceSchema = z.object({\n  id: didServiceIdSchema,\n  type: didServiceTypeSchema,\n  serviceEndpoint: didServiceEndpointSchema,\n})\n\nexport type DidService = z.infer<typeof didServiceSchema>\n\n/**\n * @see {@link https://www.w3.org/TR/did-1.0/#referring-to-verification-methods Referring to Verification Methods}\n */\nconst verificationMethodReference = z.union([\n  //\n  didRelativeUriSchema,\n  didVerificationMethodSchema,\n])\n\n/**\n * @note This schema is incomplete\n * @see {@link https://www.w3.org/TR/did-core/#production-0}\n */\nexport const didDocumentSchema = z.object({\n  '@context': z\n    .union([\n      z.literal('https://www.w3.org/ns/did/v1'),\n      z\n        .array(z.string().url())\n        .nonempty()\n        .refine((data) => data[0] === 'https://www.w3.org/ns/did/v1', {\n          message: 'First @context must be https://www.w3.org/ns/did/v1',\n        }),\n    ])\n    // @NOTE @context is required by producers, but optional for consumers.\n    .optional(),\n  id: didSchema,\n  controller: didControllerSchema.optional(),\n  alsoKnownAs: z.array(rfc3968UriSchema).optional(),\n  service: z.array(didServiceSchema).optional(),\n  authentication: z.array(verificationMethodReference).optional(),\n  verificationMethod: z.array(didVerificationMethodSchema).optional(),\n})\n\nexport type DidDocument<Method extends string = string> = z.infer<\n  typeof didDocumentSchema\n> & { id: Did<Method> }\n\n// @TODO: add other refinements ?\nexport const didDocumentValidator = didDocumentSchema\n  // Ensure that every service id is unique\n  .superRefine(({ id: did, service }, ctx) => {\n    if (service) {\n      const visited = new Set()\n\n      for (let i = 0; i < service.length; i++) {\n        const current = service[i]\n\n        const serviceId = current.id.startsWith('#')\n          ? `${did}${current.id}`\n          : current.id\n\n        if (!visited.has(serviceId)) {\n          visited.add(serviceId)\n        } else {\n          ctx.addIssue({\n            code: z.ZodIssueCode.custom,\n            message: `Duplicate service id (${current.id}) found in the document`,\n            path: ['service', i, 'id'],\n          })\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "packages/did/src/did-error.ts",
    "content": "export class DidError extends Error {\n  constructor(\n    public readonly did: string,\n    message: string,\n    public readonly code: string,\n    public readonly status = 400,\n    cause?: unknown,\n  ) {\n    super(message, { cause })\n  }\n\n  /**\n   * For compatibility with error handlers in common HTTP frameworks.\n   */\n  get statusCode() {\n    return this.status\n  }\n\n  override toString() {\n    return `${this.constructor.name} ${this.code} (${this.did}): ${this.message}`\n  }\n\n  static from(cause: unknown, did: string): DidError {\n    if (cause instanceof DidError) {\n      return cause\n    }\n\n    const message =\n      cause instanceof Error\n        ? cause.message\n        : typeof cause === 'string'\n          ? cause\n          : 'An unknown error occurred'\n\n    const status =\n      (typeof cause?.['statusCode'] === 'number'\n        ? cause['statusCode']\n        : undefined) ??\n      (typeof cause?.['status'] === 'number' ? cause['status'] : undefined)\n\n    return new DidError(did, message, 'did-unknown-error', status, cause)\n  }\n}\n\nexport class InvalidDidError extends DidError {\n  constructor(did: string, message: string, cause?: unknown) {\n    super(did, message, 'did-invalid', 400, cause)\n  }\n}\n"
  },
  {
    "path": "packages/did/src/did.ts",
    "content": "import { z } from 'zod'\nimport { DidError, InvalidDidError } from './did-error.js'\n\nconst DID_PREFIX = 'did:'\nconst DID_PREFIX_LENGTH = DID_PREFIX.length\nexport { DID_PREFIX }\n\n/**\n * Type representation of a Did, with method.\n *\n * ```bnf\n * did                = \"did:\" method-name \":\" method-specific-id\n * method-name        = 1*method-char\n * method-char        = %x61-7A / DIGIT\n * method-specific-id = *( *idchar \":\" ) 1*idchar\n * idchar             = ALPHA / DIGIT / \".\" / \"-\" / \"_\" / pct-encoded\n * pct-encoded        = \"%\" HEXDIG HEXDIG\n * ```\n *\n * @example\n * ```ts\n * type DidWeb = Did<'web'> // `did:web:${string}`\n * type DidCustom = Did<'web' | 'plc'> // `did:${'web' | 'plc'}:${string}`\n * type DidNever = Did<' invalid 🥴 '> // never\n * type DidFoo = Did<'foo' | ' invalid 🥴 '> // `did:foo:${string}`\n * ```\n *\n * @see {@link https://www.w3.org/TR/did-core/#did-syntax}\n */\nexport type Did<M extends string = string> = `did:${AsDidMethod<M>}:${string}`\n\n/**\n * DID Method\n */\nexport type AsDidMethod<M> = string extends M\n  ? string // can't know...\n  : AsDidMethodInternal<M, ''>\n\ntype AlphanumericChar = DigitChar | LowerAlphaChar\ntype DigitChar = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'\ntype LowerAlphaChar =\n  | 'a'\n  | 'b'\n  | 'c'\n  | 'd'\n  | 'e'\n  | 'f'\n  | 'g'\n  | 'h'\n  | 'i'\n  | 'j'\n  | 'k'\n  | 'l'\n  | 'm'\n  | 'n'\n  | 'o'\n  | 'p'\n  | 'q'\n  | 'r'\n  | 's'\n  | 't'\n  | 'u'\n  | 'v'\n  | 'w'\n  | 'x'\n  | 'y'\n  | 'z'\n\ntype AsDidMethodInternal<\n  S,\n  Acc extends string,\n> = S extends `${infer H}${infer T}`\n  ? H extends AlphanumericChar\n    ? AsDidMethodInternal<T, `${Acc}${H}`>\n    : never\n  : Acc extends ''\n    ? never\n    : Acc\n\n/**\n * DID Method-name check function.\n *\n * Check if the input is a valid DID method name, at the position between\n * `start` (inclusive) and `end` (exclusive).\n */\nexport function assertDidMethod(\n  input: string,\n  start = 0,\n  end = input.length,\n): void {\n  if (\n    !Number.isFinite(end) ||\n    !Number.isFinite(start) ||\n    end < start ||\n    end > input.length\n  ) {\n    throw new TypeError('Invalid start or end position')\n  }\n  if (end === start) {\n    throw new InvalidDidError(input, `Empty method name`)\n  }\n\n  let c: number\n  for (let i = start; i < end; i++) {\n    c = input.charCodeAt(i)\n    if (\n      (c < 0x61 || c > 0x7a) && // a-z\n      (c < 0x30 || c > 0x39) // 0-9\n    ) {\n      throw new InvalidDidError(\n        input,\n        `Invalid character at position ${i} in DID method name`,\n      )\n    }\n  }\n}\n\n/**\n * This method assumes the input is a valid Did\n */\nexport function extractDidMethod<D extends Did>(did: D) {\n  const msidSep = did.indexOf(':', DID_PREFIX_LENGTH)\n  const method = did.slice(DID_PREFIX_LENGTH, msidSep)\n  return method as D extends Did<infer M> ? M : string\n}\n\n/**\n * DID Method-specific identifier check function.\n *\n * Check if the input is a valid DID method-specific identifier, at the position\n * between `start` (inclusive) and `end` (exclusive).\n */\nexport function assertDidMsid(\n  input: string,\n  start = 0,\n  end = input.length,\n): void {\n  if (\n    !Number.isFinite(end) ||\n    !Number.isFinite(start) ||\n    end < start ||\n    end > input.length\n  ) {\n    throw new TypeError('Invalid start or end position')\n  }\n  if (end === start) {\n    throw new InvalidDidError(input, `DID method-specific id must not be empty`)\n  }\n\n  let c: number\n  for (let i = start; i < end; i++) {\n    c = input.charCodeAt(i)\n\n    // Check for frequent chars first\n    if (\n      (c < 0x61 || c > 0x7a) && // a-z\n      (c < 0x41 || c > 0x5a) && // A-Z\n      (c < 0x30 || c > 0x39) && // 0-9\n      c !== 0x2e && // .\n      c !== 0x2d && // -\n      c !== 0x5f // _\n    ) {\n      // Less frequent chars are checked here\n\n      // \":\"\n      if (c === 0x3a) {\n        if (i === end - 1) {\n          throw new InvalidDidError(input, `DID cannot end with \":\"`)\n        }\n        continue\n      }\n\n      // pct-encoded\n      if (c === 0x25) {\n        c = input.charCodeAt(++i)\n        if ((c < 0x30 || c > 0x39) && (c < 0x41 || c > 0x46)) {\n          throw new InvalidDidError(\n            input,\n            `Invalid pct-encoded character at position ${i}`,\n          )\n        }\n\n        c = input.charCodeAt(++i)\n        if ((c < 0x30 || c > 0x39) && (c < 0x41 || c > 0x46)) {\n          throw new InvalidDidError(\n            input,\n            `Invalid pct-encoded character at position ${i}`,\n          )\n        }\n\n        // There must always be 2 HEXDIG after a \"%\"\n        if (i >= end) {\n          throw new InvalidDidError(\n            input,\n            `Incomplete pct-encoded character at position ${i - 2}`,\n          )\n        }\n\n        continue\n      }\n\n      throw new InvalidDidError(\n        input,\n        `Disallowed character in DID at position ${i}`,\n      )\n    }\n  }\n}\n\nexport function assertDid(input: unknown): asserts input is Did {\n  if (typeof input !== 'string') {\n    throw new InvalidDidError(typeof input, `DID must be a string`)\n  }\n\n  const { length } = input\n  if (length > 2048) {\n    throw new InvalidDidError(input, `DID is too long (2048 chars max)`)\n  }\n\n  if (!input.startsWith(DID_PREFIX)) {\n    throw new InvalidDidError(input, `DID requires \"${DID_PREFIX}\" prefix`)\n  }\n\n  const idSep = input.indexOf(':', DID_PREFIX_LENGTH)\n  if (idSep === -1) {\n    throw new InvalidDidError(input, `Missing colon after method name`)\n  }\n\n  assertDidMethod(input, DID_PREFIX_LENGTH, idSep)\n  assertDidMsid(input, idSep + 1, length)\n}\n\nexport function isDid(input: unknown): input is Did {\n  try {\n    assertDid(input)\n    return true\n  } catch (err) {\n    if (err instanceof DidError) {\n      return false\n    }\n\n    // Unexpected TypeError (should never happen)\n    throw err\n  }\n}\n\nexport function asDid<T>(input: T) {\n  assertDid(input)\n  return input\n}\n\nexport const didSchema = z\n  .string()\n  .superRefine((value: string, ctx: z.RefinementCtx): value is Did => {\n    try {\n      assertDid(value)\n      return true\n    } catch (err) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: err instanceof Error ? err.message : 'Unexpected error',\n      })\n      return false\n    }\n  })\n"
  },
  {
    "path": "packages/did/src/index.ts",
    "content": "export * from './atproto.js'\nexport * from './did-document.js'\nexport * from './did-error.js'\nexport * from './did.js'\nexport * from './methods.js'\nexport * from './utils.js'\n"
  },
  {
    "path": "packages/did/src/lib/uri.ts",
    "content": "/**\n * @see {@link https://www.w3.org/TR/did-1.0/#dfn-did-fragments}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc3986#section-3.5}\n */\nexport function isFragment(\n  value: string,\n  startIdx = 0,\n  endIdx = value.length,\n): boolean {\n  let charCode: number\n  for (let i = startIdx; i < endIdx; i++) {\n    charCode = value.charCodeAt(i)\n\n    // fragment    = *( pchar / \"/\" / \"?\" )\n    // pchar       = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n    // unreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n    // pct-encoded = \"%\" HEXDIG HEXDIG\n    // sub-delims  = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n    if (\n      (charCode >= 65 /* A */ && charCode <= 90) /* Z */ ||\n      (charCode >= 97 /* a */ && charCode <= 122) /* z */ ||\n      (charCode >= 48 /* 0 */ && charCode <= 57) /* 9 */ ||\n      charCode === 45 /* \"-\" */ ||\n      charCode === 46 /* \".\" */ ||\n      charCode === 95 /* \"_\" */ ||\n      charCode === 126 /* \"~\" */\n    ) {\n      // unreserved\n    } else if (\n      charCode === 33 /* \"!\" */ ||\n      charCode === 36 /* \"$\" */ ||\n      charCode === 38 /* \"&\" */ ||\n      charCode === 39 /* \"'\" */ ||\n      charCode === 40 /* \"(\" */ ||\n      charCode === 41 /* \")\" */ ||\n      charCode === 42 /* \"*\" */ ||\n      charCode === 43 /* \"+\" */ ||\n      charCode === 44 /* \",\" */ ||\n      charCode === 59 /* \";\" */ ||\n      charCode === 61 /* \"=\" */\n    ) {\n      // sub-delims\n    } else if (charCode === 58 /* \":\" */ || charCode === 64 /* \"@\" */) {\n      // pchar extra\n    } else if (charCode === 47 /* \"/\" */ || charCode === 63 /* \"?\" */) {\n      // fragment extra\n    } else if (charCode === 37 /* \"%\" */) {\n      // pct-enc\n      if (i + 2 >= endIdx) return false\n      if (!isHexDigit(value.charCodeAt(i + 1))) return false\n      if (!isHexDigit(value.charCodeAt(i + 2))) return false\n      i += 2\n    } else {\n      return false\n    }\n  }\n\n  return true\n}\n\nexport function isHexDigit(code: number): boolean {\n  return (\n    (code >= 48 && code <= 57) || // 0-9\n    (code >= 65 && code <= 70) || // A-F\n    (code >= 97 && code <= 102) // a-f\n  )\n}\n\nexport const canParse =\n  URL.canParse?.bind(URL) ??\n  ((url, base) => {\n    try {\n      new URL(url, base)\n      return true\n    } catch {\n      return false\n    }\n  })\n"
  },
  {
    "path": "packages/did/src/methods/plc.ts",
    "content": "import { InvalidDidError } from '../did-error.js'\nimport { Did } from '../did.js'\n\nconst DID_PLC_PREFIX = `did:plc:`\nconst DID_PLC_PREFIX_LENGTH = DID_PLC_PREFIX.length\nconst DID_PLC_LENGTH = 32\n\nexport { DID_PLC_PREFIX }\n\nexport function isDidPlc(input: unknown): input is Did<'plc'> {\n  // Optimization: equivalent to try/catch around \"assertDidPlc\"\n  if (typeof input !== 'string') return false\n  if (input.length !== DID_PLC_LENGTH) return false\n  if (!input.startsWith(DID_PLC_PREFIX)) return false\n  for (let i = DID_PLC_PREFIX_LENGTH; i < DID_PLC_LENGTH; i++) {\n    if (!isBase32Char(input.charCodeAt(i))) return false\n  }\n  return true\n}\n\nexport function asDidPlc<T>(input: T) {\n  assertDidPlc(input)\n  return input\n}\n\nexport function assertDidPlc(input: unknown): asserts input is Did<'plc'> {\n  if (typeof input !== 'string') {\n    throw new InvalidDidError(typeof input, `DID must be a string`)\n  }\n\n  if (!input.startsWith(DID_PLC_PREFIX)) {\n    throw new InvalidDidError(input, `Invalid did:plc prefix`)\n  }\n\n  if (input.length !== DID_PLC_LENGTH) {\n    throw new InvalidDidError(\n      input,\n      `did:plc must be ${DID_PLC_LENGTH} characters long`,\n    )\n  }\n\n  // The following check is not necessary, as the check below is more strict:\n\n  // assertDidMsid(input, DID_PLC_PREFIX.length)\n\n  for (let i = DID_PLC_PREFIX_LENGTH; i < DID_PLC_LENGTH; i++) {\n    if (!isBase32Char(input.charCodeAt(i))) {\n      throw new InvalidDidError(input, `Invalid character at position ${i}`)\n    }\n  }\n}\n\nconst isBase32Char = (c: number): boolean =>\n  (c >= 0x61 && c <= 0x7a) || (c >= 0x32 && c <= 0x37) // [a-z2-7]\n"
  },
  {
    "path": "packages/did/src/methods/web.ts",
    "content": "import { InvalidDidError } from '../did-error.js'\nimport { Did, assertDidMsid } from '../did.js'\nimport { canParse } from '../lib/uri.js'\n\nexport const DID_WEB_PREFIX = `did:web:` satisfies Did<'web'>\n\n/**\n * This function checks if the input is a valid Web DID, as per DID spec.\n */\nexport function isDidWeb(input: unknown): input is Did<'web'> {\n  // Optimization: make cheap checks first\n  if (typeof input !== 'string') return false\n  if (!input.startsWith(DID_WEB_PREFIX)) return false\n  if (input.charAt(DID_WEB_PREFIX.length) === ':') return false\n\n  try {\n    assertDidMsid(input, DID_WEB_PREFIX.length)\n  } catch {\n    return false\n  }\n\n  return canParse(buildDidWebUrl(input as Did<'web'>))\n}\n\nexport function asDidWeb<T>(input: T) {\n  assertDidWeb(input)\n  return input\n}\n\nexport function assertDidWeb(input: unknown): asserts input is Did<'web'> {\n  if (typeof input !== 'string') {\n    throw new InvalidDidError(typeof input, `DID must be a string`)\n  }\n\n  if (!input.startsWith(DID_WEB_PREFIX)) {\n    throw new InvalidDidError(input, `Invalid did:web prefix`)\n  }\n\n  if (input.charAt(DID_WEB_PREFIX.length) === ':') {\n    throw new InvalidDidError(input, 'did:web MSID must not start with a colon')\n  }\n\n  // Make sure every char is valid (per DID spec)\n  assertDidMsid(input, DID_WEB_PREFIX.length)\n\n  if (!canParse(buildDidWebUrl(input as Did<'web'>))) {\n    throw new InvalidDidError(input, 'Invalid Web DID')\n  }\n}\n\nexport function didWebToUrl(did: Did<'web'>) {\n  try {\n    return new URL(buildDidWebUrl(did)) as URL & {\n      protocol: 'http:' | 'https:'\n    }\n  } catch (cause) {\n    throw new InvalidDidError(did, 'Invalid Web DID', cause)\n  }\n}\n\nexport function urlToDidWeb(url: URL): Did<'web'> {\n  const port = url.port ? `%3A${url.port}` : ''\n  const path = url.pathname === '/' ? '' : url.pathname.replaceAll('/', ':')\n\n  return `did:web:${url.hostname}${port}${path}`\n}\n\nexport function buildDidWebUrl(did: Did<'web'>): string {\n  const hostIdx = DID_WEB_PREFIX.length\n  const pathIdx = did.indexOf(':', hostIdx)\n\n  const hostEnc =\n    pathIdx === -1 ? did.slice(hostIdx) : did.slice(hostIdx, pathIdx)\n  const host = hostEnc.replaceAll('%3A', ':')\n  const path = pathIdx === -1 ? '' : did.slice(pathIdx).replaceAll(':', '/')\n  const proto =\n    host.startsWith('localhost') &&\n    (host.length === 9 || host.charCodeAt(9) === 58) /* ':' */\n      ? 'http'\n      : 'https'\n\n  return `${proto}://${host}${path}`\n}\n"
  },
  {
    "path": "packages/did/src/methods.ts",
    "content": "export * from './methods/plc.js'\nexport * from './methods/web.js'\n"
  },
  {
    "path": "packages/did/src/utils.ts",
    "content": "export type Identifier<D extends string, I extends string> =\n  | `#${I}`\n  | `${D}#${I}`\nexport function matchesIdentifier<D extends string, I extends string>(\n  did: D,\n  id: I,\n  candidate: string,\n): candidate is Identifier<D, I> {\n  // optimized implementation of:\n  // return candidate === `#${id}` || candidate === `${did}#${id}`\n\n  return candidate.charCodeAt(0) === 35 // '#'\n    ? candidate.length === id.length + 1 && candidate.endsWith(id)\n    : candidate.length === id.length + 1 + did.length &&\n        candidate.charCodeAt(did.length) === 35 && // '#'\n        candidate.startsWith(did) &&\n        candidate.endsWith(id)\n}\n"
  },
  {
    "path": "packages/did/tests/methods/plc.test.ts",
    "content": "import { InvalidDidError } from '../../src/did-error.js'\nimport { Did } from '../../src/did.js'\nimport { asDidPlc, assertDidPlc, isDidPlc } from '../../src/methods/plc.js'\n\nconst VALID: Did<'plc'>[] = [\n  'did:plc:l3rouwludahu3ui3bt66mfvj',\n  'did:plc:aaaaaaaaaaaaaaaaaaaaaaaa',\n  'did:plc:zzzzzzzzzzzzzzzzzzzzzzzz',\n]\n\nconst INVALID: [value: unknown, message: string][] = [\n  ['did:plc:l3rouwludahu3ui3bt66mfv0', 'Invalid character at position 31'],\n  ['did:plc:l3rouwludahu3ui3bt66mfv1', 'Invalid character at position 31'],\n  ['did:plc:l3rouwludahu3ui3bt66mfv9', 'Invalid character at position 31'],\n  ['did:plc:l3rouwludahu3ui3bt66mfv', 'did:plc must be 32 characters long'],\n  ['did:plc:l3rouwludahu3ui3bt66mfvja', 'did:plc must be 32 characters long'],\n  ['did:plc:example.com:', 'did:plc must be 32 characters long'],\n  ['did:plc:exam%3Aple.com%3A8080', 'did:plc must be 32 characters long'],\n  [3, 'DID must be a string'],\n  [{ toString: () => 'did:plc:foo.com' }, 'DID must be a string'],\n  [[''], 'DID must be a string'],\n  ['random-string', 'Invalid did:plc prefix'],\n  ['did plc', 'Invalid did:plc prefix'],\n  ['lorem ipsum dolor sit', 'Invalid did:plc prefix'],\n]\n\ndescribe('isDidPlc', () => {\n  it('returns true for various valid dids', () => {\n    for (const did of VALID) {\n      expect(isDidPlc(did)).toBe(true)\n    }\n  })\n\n  it('returns false for invalid dids', () => {\n    for (const [did] of INVALID) {\n      expect(isDidPlc(did)).toBe(false)\n    }\n  })\n})\n\ndescribe('assertDidPlc', () => {\n  it('does not throw on valid dids', () => {\n    for (const did of VALID) {\n      expect(() => assertDidPlc(did)).not.toThrow()\n    }\n  })\n\n  it('throws if called with non string argument', () => {\n    for (const [val, message] of INVALID) {\n      expect(() => assertDidPlc(val)).toThrowError(\n        new InvalidDidError(\n          typeof val === 'string' ? val : typeof val,\n          message,\n        ),\n      )\n    }\n  })\n})\n\ndescribe('asDidPlc', () => {\n  it('returns the input for valid dids', () => {\n    for (const did of VALID) {\n      expect(asDidPlc(did)).toBe(did)\n    }\n  })\n\n  it('throws if called with invalid dids', () => {\n    for (const [val] of INVALID) {\n      expect(() => asDidPlc(val)).toThrowError(InvalidDidError)\n    }\n  })\n})\n"
  },
  {
    "path": "packages/did/tests/methods/web.test.ts",
    "content": "import { InvalidDidError } from '../../src/did-error.js'\nimport { Did } from '../../src/did.js'\nimport {\n  asDidWeb,\n  assertDidWeb,\n  didWebToUrl,\n  isDidWeb,\n  urlToDidWeb,\n} from '../../src/methods/web.js'\n\nconst VALID: [Did<'web'>, string][] = [\n  ['did:web:example.com', 'https://example.com/'],\n  ['did:web:sub.example.com', 'https://sub.example.com/'],\n  ['did:web:example.com%3A8080', 'https://example.com:8080/'],\n  [\n    'did:web:example.com:path:to:resource',\n    'https://example.com/path/to/resource',\n  ],\n  [\n    'did:web:example.com%3A8080:path:to:resource',\n    'https://example.com:8080/path/to/resource',\n  ],\n  [\n    'did:web:xn--b48h.com%3A8080:path:to:resource',\n    'https://🙃.com:8080/path/to/resource',\n  ],\n]\n\nconst INVALID: [value: unknown, message: string][] = [\n  ['did:web:', 'DID method-specific id must not be empty'],\n  ['did:web:foo@example.com', 'Disallowed character in DID at position 11'],\n  ['did:web::example.com', 'did:web MSID must not start with a colon'],\n  ['did:web:example.com:', 'DID cannot end with \":\"'],\n  ['did:web:exam%3Aple.com%3A8080', 'Invalid Web DID'],\n  [3, 'DID must be a string'],\n  [{ toString: () => 'did:web:foo.com' }, 'DID must be a string'],\n  [[''], 'DID must be a string'],\n  ['random-string', 'Invalid did:web prefix'],\n  ['did web', 'Invalid did:web prefix'],\n  ['lorem ipsum dolor sit', 'Invalid did:web prefix'],\n]\n\ndescribe('isDidWeb', () => {\n  it('returns true for various valid dids', () => {\n    for (const [did] of VALID) {\n      expect(isDidWeb(did)).toBe(true)\n    }\n  })\n\n  it('returns false for invalid dids', () => {\n    for (const did of INVALID) {\n      expect(isDidWeb(did)).toBe(false)\n    }\n  })\n})\n\ndescribe('assertDidWeb', () => {\n  it('does not throw on valid dids', () => {\n    for (const [did] of VALID) {\n      expect(() => assertDidWeb(did)).not.toThrow()\n    }\n  })\n\n  it('throws if called with non string argument', () => {\n    for (const [val, message] of INVALID) {\n      expect(() => assertDidWeb(val)).toThrowError(\n        new InvalidDidError(\n          typeof val === 'string' ? val : typeof val,\n          message,\n        ),\n      )\n    }\n  })\n})\n\ndescribe('didWebToUrl', () => {\n  it('converts valid did:web to URL', () => {\n    for (const [did, url] of VALID) {\n      expect(didWebToUrl(did)).toStrictEqual(new URL(url))\n    }\n  })\n})\n\ndescribe('urlToDidWeb', () => {\n  it('converts URL to valid did:web', () => {\n    for (const [did, url] of VALID) {\n      expect(urlToDidWeb(new URL(url))).toBe(did)\n    }\n  })\n})\n\ndescribe('asDidWeb', () => {\n  it('returns the input for valid dids', () => {\n    for (const [did] of VALID) {\n      expect(asDidWeb(did)).toBe(did)\n    }\n  })\n\n  it('throws if called with invalid dids', () => {\n    for (const [val, message] of INVALID) {\n      expect(() => asDidWeb(val)).toThrowError(\n        new InvalidDidError(\n          typeof val === 'string' ? val : typeof val,\n          message,\n        ),\n      )\n    }\n  })\n})\n"
  },
  {
    "path": "packages/did/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/did/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/identity/CHANGELOG.md",
    "content": "# @atproto/identity\n\n## 0.4.12\n\n### Patch Changes\n\n- [#4669](https://github.com/bluesky-social/atproto/pull/4669) [`dc9644b`](https://github.com/bluesky-social/atproto/commit/dc9644bbeb1892931809568895162d823e4743d2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix lint warning\n\n## 0.4.11\n\n### Patch Changes\n\n- [#4609](https://github.com/bluesky-social/atproto/pull/4609) [`00e6dbd`](https://github.com/bluesky-social/atproto/commit/00e6dbdcea295cfa3dff7eb7517420039cc3e821) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `method.resolveNoCheck is not a function` error when using speciallt forged did method\n\n- Updated dependencies []:\n  - @atproto/common-web@0.4.16\n\n## 0.4.10\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n  - @atproto/crypto@0.4.4\n\n## 0.4.9\n\n### Patch Changes\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/common-web@0.4.3\n  - @atproto/crypto@0.4.4\n\n## 0.4.8\n\n### Patch Changes\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/common-web@0.4.2\n  - @atproto/crypto@0.4.4\n\n## 0.4.7\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common-web@0.4.1\n  - @atproto/crypto@0.4.4\n\n## 0.4.6\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/common-web@0.4.0\n  - @atproto/crypto@0.4.4\n\n## 0.4.5\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n\n## 0.4.4\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on Axios\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/common-web@0.3.2\n  - @atproto/crypto@0.4.2\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies [[`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0)]:\n  - @atproto/crypto@0.4.2\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]:\n  - @atproto/common-web@0.3.1\n  - @atproto/crypto@0.4.1\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31)]:\n  - @atproto/crypto@0.4.1\n\n## 0.4.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common-web@0.3.0\n  - @atproto/crypto@0.4.0\n\n## 0.3.3\n\n### Patch Changes\n\n- [#2302](https://github.com/bluesky-social/atproto/pull/2302) [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0) Thanks [@dholms](https://github.com/dholms)! - Added methods for parsing labeler verification methods and services in DID Documents\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n  - @atproto/crypto@0.3.0\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n\n## 0.3.1\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/common-web@0.2.3\n  - @atproto/crypto@0.2.3\n\n## 0.3.0\n\n### Minor Changes\n\n- [#1773](https://github.com/bluesky-social/atproto/pull/1773) [`bb039d8e`](https://github.com/bluesky-social/atproto/commit/bb039d8e4ce5b7f70c4f3e86d1327e210ef24dc3) Thanks [@dholms](https://github.com/dholms)! - Pass stale did doc into refresh cache functions\n\n### Patch Changes\n\n- [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7) Thanks [@devinivy](https://github.com/devinivy)! - Allow pds to serve did doc with credentials, API client to respect PDS listed in the did doc.\n\n- Updated dependencies [[`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/common-web@0.2.2\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/common-web@0.2.1\n"
  },
  {
    "path": "packages/identity/README.md",
    "content": "# @atproto/identity\n\nTypeScript library for decentralized identities in [atproto](https://atproto.com) using DIDs and handles\n\n[![NPM](https://img.shields.io/npm/v/@atproto/identity)](https://www.npmjs.com/package/@atproto/identity)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Example\n\nResolving a Handle and verifying against DID document:\n\n```typescript\nconst didres = new DidResolver({})\nconst hdlres = new HandleResolver({})\n\nconst handle = 'atproto.com'\nconst did = await hdlres.resolve(handle)\n\nif (did == undefined) {\n  throw new Error('expected handle to resolve')\n}\nconsole.log(did) // did:plc:ewvi7nxzyoun6zhxrhs64oiz\n\nconst doc = await didres.resolve(did)\nconsole.log(doc)\n\n// additional resolutions of same DID will be cached for some time, unless forceRefresh flag is used\nconst doc2 = await didres.resolve(did, true)\n\n// helper methods use the same cache\nconst data = await didres.resolveAtprotoData(did)\n\nif (data.handle != handle) {\n  throw new Error('invalid handle (did not match DID document)')\n}\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/identity/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Identity',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  transformIgnorePatterns: ['/node_modules/.pnpm/(?!(get-port)@)'],\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/identity/package.json",
    "content": "{\n  \"name\": \"@atproto/identity\",\n  \"version\": \"0.4.12\",\n  \"license\": \"MIT\",\n  \"description\": \"Library for decentralized identities in atproto using DIDs and handles\",\n  \"keywords\": [\n    \"atproto\",\n    \"did\",\n    \"identity\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/identity\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test:log\": \"cat test.log | pino-pretty\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@did-plc/lib\": \"^0.0.1\",\n    \"@did-plc/server\": \"^0.0.1\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.18.2\",\n    \"get-port\": \"^6.1.2\",\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/did/atproto-data.ts",
    "content": "import {\n  getDid,\n  getFeedGenEndpoint,\n  getHandle,\n  getNotifEndpoint,\n  getPdsEndpoint,\n  getSigningKey,\n} from '@atproto/common-web'\nimport * as crypto from '@atproto/crypto'\nimport { AtprotoData, DidDocument } from '../types'\n\nexport {\n  getDid,\n  getFeedGenEndpoint as getFeedGen,\n  getHandle,\n  getNotifEndpoint as getNotif,\n  getPdsEndpoint as getPds,\n}\n\nexport const getKey = (doc: DidDocument): string | undefined => {\n  const key = getSigningKey(doc)\n  if (!key) return undefined\n  return getDidKeyFromMultibase(key)\n}\n\nexport const getDidKeyFromMultibase = (key: {\n  type: string\n  publicKeyMultibase: string\n}): string | undefined => {\n  const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n  let didKey: string | undefined = undefined\n  if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n    didKey = crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n  } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n    didKey = crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n  } else if (key.type === 'Multikey') {\n    const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n    didKey = crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n  }\n  return didKey\n}\n\nexport const parseToAtprotoDocument = (\n  doc: DidDocument,\n): Partial<AtprotoData> => {\n  const did = getDid(doc)\n  return {\n    did,\n    signingKey: getKey(doc),\n    handle: getHandle(doc),\n    pds: getPdsEndpoint(doc),\n  }\n}\n\nexport const ensureAtpDocument = (doc: DidDocument): AtprotoData => {\n  const { did, signingKey, handle, pds } = parseToAtprotoDocument(doc)\n  if (!did) {\n    throw new Error(`Could not parse id from doc: ${doc}`)\n  }\n  if (!signingKey) {\n    throw new Error(`Could not parse signingKey from doc: ${doc}`)\n  }\n  if (!handle) {\n    throw new Error(`Could not parse handle from doc: ${doc}`)\n  }\n  if (!pds) {\n    throw new Error(`Could not parse pds from doc: ${doc}`)\n  }\n  return { did, signingKey, handle, pds }\n}\n\nexport const ensureAtprotoKey = (doc: DidDocument): string => {\n  const { signingKey } = parseToAtprotoDocument(doc)\n  if (!signingKey) {\n    throw new Error(`Could not parse signingKey from doc: ${doc}`)\n  }\n  return signingKey\n}\n"
  },
  {
    "path": "packages/identity/src/did/base-resolver.ts",
    "content": "import { check } from '@atproto/common-web'\nimport * as crypto from '@atproto/crypto'\nimport { DidNotFoundError, PoorlyFormattedDidDocumentError } from '../errors'\nimport {\n  AtprotoData,\n  CacheResult,\n  DidCache,\n  DidDocument,\n  didDocument,\n} from '../types'\nimport * as atprotoData from './atproto-data'\n\nexport abstract class BaseResolver {\n  constructor(public cache?: DidCache) {}\n\n  abstract resolveNoCheck(did: string): Promise<unknown | null>\n\n  validateDidDoc(did: string, val: unknown): DidDocument {\n    if (!check.is(val, didDocument)) {\n      throw new PoorlyFormattedDidDocumentError(did, val)\n    }\n    if (val.id !== did) {\n      throw new PoorlyFormattedDidDocumentError(did, val)\n    }\n    return val\n  }\n\n  async resolveNoCache(did: string): Promise<DidDocument | null> {\n    const got = await this.resolveNoCheck(did)\n    if (got === null) return null\n    return this.validateDidDoc(did, got)\n  }\n\n  async refreshCache(did: string, prevResult?: CacheResult): Promise<void> {\n    await this.cache?.refreshCache(\n      did,\n      () => this.resolveNoCache(did),\n      prevResult,\n    )\n  }\n\n  async resolve(\n    did: string,\n    forceRefresh = false,\n  ): Promise<DidDocument | null> {\n    let fromCache: CacheResult | null = null\n    if (this.cache && !forceRefresh) {\n      fromCache = await this.cache.checkCache(did)\n      if (fromCache && !fromCache.expired) {\n        if (fromCache?.stale) {\n          await this.refreshCache(did, fromCache)\n        }\n        return fromCache.doc\n      }\n    }\n\n    const got = await this.resolveNoCache(did)\n    if (got === null) {\n      await this.cache?.clearEntry(did)\n      return null\n    }\n    await this.cache?.cacheDid(did, got, fromCache ?? undefined)\n    return got\n  }\n\n  async ensureResolve(did: string, forceRefresh = false): Promise<DidDocument> {\n    const result = await this.resolve(did, forceRefresh)\n    if (result === null) {\n      throw new DidNotFoundError(did)\n    }\n    return result\n  }\n\n  async resolveAtprotoData(\n    did: string,\n    forceRefresh = false,\n  ): Promise<AtprotoData> {\n    const didDocument = await this.ensureResolve(did, forceRefresh)\n    return atprotoData.ensureAtpDocument(didDocument)\n  }\n\n  async resolveAtprotoKey(did: string, forceRefresh = false): Promise<string> {\n    if (did.startsWith('did:key:')) {\n      return did\n    } else {\n      const didDocument = await this.ensureResolve(did, forceRefresh)\n      return atprotoData.ensureAtprotoKey(didDocument)\n    }\n  }\n\n  async verifySignature(\n    did: string,\n    data: Uint8Array,\n    sig: Uint8Array,\n    forceRefresh = false,\n  ): Promise<boolean> {\n    const signingKey = await this.resolveAtprotoKey(did, forceRefresh)\n    return crypto.verifySignature(signingKey, data, sig)\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/did/did-resolver.ts",
    "content": "import { PoorlyFormattedDidError, UnsupportedDidMethodError } from '../errors'\nimport { DidResolverOpts } from '../types'\nimport { BaseResolver } from './base-resolver'\nimport { DidPlcResolver } from './plc-resolver'\nimport { DidWebResolver } from './web-resolver'\n\nexport class DidResolver extends BaseResolver {\n  methods: Map<string, BaseResolver>\n\n  constructor(opts: DidResolverOpts) {\n    super(opts.didCache)\n    const { timeout = 3000, plcUrl = 'https://plc.directory' } = opts\n    // do not pass cache to sub-methods or we will be double caching\n    this.methods = new Map([\n      ['plc', new DidPlcResolver(plcUrl, timeout)],\n      ['web', new DidWebResolver(timeout)],\n    ])\n  }\n\n  async resolveNoCheck(did: string): Promise<unknown> {\n    if (!did.startsWith('did:')) {\n      throw new PoorlyFormattedDidError(did)\n    }\n    const methodSepIdx = did.indexOf(':', 4)\n    if (methodSepIdx === -1) {\n      throw new PoorlyFormattedDidError(did)\n    }\n    const methodName = did.slice(4, methodSepIdx)\n    const method = this.methods.get(methodName)\n    if (!method) {\n      throw new UnsupportedDidMethodError(did)\n    }\n    return method.resolveNoCheck(did)\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/did/index.ts",
    "content": "export * from './web-resolver'\nexport * from './plc-resolver'\nexport * from './did-resolver'\nexport * from './atproto-data'\nexport * from './memory-cache'\n"
  },
  {
    "path": "packages/identity/src/did/memory-cache.ts",
    "content": "import { DAY, HOUR } from '@atproto/common-web'\nimport { CacheResult, DidCache, DidDocument } from '../types'\n\ntype CacheVal = {\n  doc: DidDocument\n  updatedAt: number\n}\n\nexport class MemoryCache implements DidCache {\n  public staleTTL: number\n  public maxTTL: number\n  constructor(staleTTL?: number, maxTTL?: number) {\n    this.staleTTL = staleTTL ?? HOUR\n    this.maxTTL = maxTTL ?? DAY\n  }\n\n  public cache: Map<string, CacheVal> = new Map()\n\n  async cacheDid(did: string, doc: DidDocument): Promise<void> {\n    this.cache.set(did, { doc, updatedAt: Date.now() })\n  }\n\n  async refreshCache(\n    did: string,\n    getDoc: () => Promise<DidDocument | null>,\n  ): Promise<void> {\n    const doc = await getDoc()\n    if (doc) {\n      await this.cacheDid(did, doc)\n    }\n  }\n\n  async checkCache(did: string): Promise<CacheResult | null> {\n    const val = this.cache.get(did)\n    if (!val) return null\n    const now = Date.now()\n    const expired = now > val.updatedAt + this.maxTTL\n    const stale = now > val.updatedAt + this.staleTTL\n    return {\n      ...val,\n      did,\n      stale,\n      expired,\n    }\n  }\n\n  async clearEntry(did: string): Promise<void> {\n    this.cache.delete(did)\n  }\n\n  async clear(): Promise<void> {\n    this.cache.clear()\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/did/plc-resolver.ts",
    "content": "import { DidCache } from '../types'\nimport { BaseResolver } from './base-resolver'\nimport { timed } from './util'\n\nexport class DidPlcResolver extends BaseResolver {\n  constructor(\n    public plcUrl: string,\n    public timeout: number,\n    public cache?: DidCache,\n  ) {\n    super(cache)\n  }\n\n  async resolveNoCheck(did: string): Promise<unknown> {\n    return timed(this.timeout, async (signal) => {\n      const url = new URL(`/${encodeURIComponent(did)}`, this.plcUrl)\n      const res = await fetch(url, {\n        redirect: 'error',\n        headers: { accept: 'application/did+ld+json,application/json' },\n        signal,\n      })\n\n      // Positively not found, versus due to e.g. network error\n      if (res.status === 404) return null\n\n      if (!res.ok) {\n        throw Object.assign(new Error(res.statusText), { status: res.status })\n      }\n\n      return res.json()\n    })\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/did/util.ts",
    "content": "export async function timed<F extends (signal: AbortSignal) => unknown>(\n  ms: number,\n  fn: F,\n): Promise<Awaited<ReturnType<F>>> {\n  const abortController = new AbortController()\n  const timer = setTimeout(() => abortController.abort(), ms)\n  const signal = abortController.signal\n\n  try {\n    return (await fn(signal)) as Awaited<ReturnType<F>>\n  } finally {\n    clearTimeout(timer)\n    abortController.abort()\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/did/web-resolver.ts",
    "content": "import { PoorlyFormattedDidError, UnsupportedDidWebPathError } from '../errors'\nimport { DidCache } from '../types'\nimport { BaseResolver } from './base-resolver'\nimport { timed } from './util'\n\nexport const DOC_PATH = '/.well-known/did.json'\n\nexport class DidWebResolver extends BaseResolver {\n  constructor(\n    public timeout: number,\n    public cache?: DidCache,\n  ) {\n    super(cache)\n  }\n\n  async resolveNoCheck(did: string): Promise<unknown> {\n    const parsedId = did.split(':').slice(2).join(':')\n    const parts = parsedId.split(':').map(decodeURIComponent)\n    let path: string\n    if (parts.length < 1) {\n      throw new PoorlyFormattedDidError(did)\n    } else if (parts.length === 1) {\n      path = parts[0] + DOC_PATH\n    } else {\n      // how we *would* resolve a did:web with path, if atproto supported it\n      //path = parts.join('/') + '/did.json'\n      throw new UnsupportedDidWebPathError(did)\n    }\n\n    const url = new URL(`https://${path}`)\n    if (url.hostname === 'localhost') {\n      url.protocol = 'http'\n    }\n\n    return timed(this.timeout, async (signal) => {\n      const res = await fetch(url, {\n        signal,\n        redirect: 'error',\n        headers: { accept: 'application/did+ld+json,application/json' },\n      })\n\n      // Positively not found, versus due to e.g. network error\n      if (!res.ok) return null\n\n      return res.json()\n    })\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/errors.ts",
    "content": "export class DidNotFoundError extends Error {\n  constructor(public did: string) {\n    super(`Could not resolve DID: ${did}`)\n  }\n}\n\nexport class PoorlyFormattedDidError extends Error {\n  constructor(public did: string) {\n    super(`Poorly formatted DID: ${did}`)\n  }\n}\n\nexport class UnsupportedDidMethodError extends Error {\n  constructor(public did: string) {\n    super(`Unsupported DID method: ${did}`)\n  }\n}\n\nexport class PoorlyFormattedDidDocumentError extends Error {\n  constructor(\n    public did: string,\n    public doc: unknown,\n  ) {\n    super(`Poorly formatted DID Document: ${doc}`)\n  }\n}\n\nexport class UnsupportedDidWebPathError extends Error {\n  constructor(public did: string) {\n    super(`Unsupported did:web paths: ${did}`)\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/handle/index.ts",
    "content": "import dns from 'node:dns/promises'\nimport { HandleResolverOpts } from '../types'\n\nconst SUBDOMAIN = '_atproto'\nconst PREFIX = 'did='\n\nexport class HandleResolver {\n  public timeout: number\n  private backupNameservers: string[] | undefined\n  private backupNameserverIps: string[] | undefined\n\n  constructor(opts: HandleResolverOpts = {}) {\n    this.timeout = opts.timeout ?? 3000\n    this.backupNameservers = opts.backupNameservers\n  }\n\n  async resolve(handle: string): Promise<string | undefined> {\n    const dnsPromise = this.resolveDns(handle)\n    const httpAbort = new AbortController()\n    const httpPromise = this.resolveHttp(handle, httpAbort.signal).catch(\n      () => undefined,\n    )\n\n    const dnsRes = await dnsPromise\n    if (dnsRes) {\n      httpAbort.abort()\n      return dnsRes\n    }\n    const res = await httpPromise\n    if (res) {\n      return res\n    }\n    return this.resolveDnsBackup(handle)\n  }\n\n  async resolveDns(handle: string): Promise<string | undefined> {\n    let chunkedResults: string[][]\n    try {\n      chunkedResults = await dns.resolveTxt(`${SUBDOMAIN}.${handle}`)\n    } catch (err) {\n      return undefined\n    }\n    return this.parseDnsResult(chunkedResults)\n  }\n\n  async resolveHttp(\n    handle: string,\n    signal?: AbortSignal,\n  ): Promise<string | undefined> {\n    const url = new URL('/.well-known/atproto-did', `https://${handle}`)\n    try {\n      const res = await fetch(url, { signal })\n      const did = (await res.text()).split('\\n')[0].trim()\n      if (typeof did === 'string' && did.startsWith('did:')) {\n        return did\n      }\n      return undefined\n    } catch (err) {\n      return undefined\n    }\n  }\n\n  async resolveDnsBackup(handle: string): Promise<string | undefined> {\n    let chunkedResults: string[][]\n    try {\n      const backupIps = await this.getBackupNameserverIps()\n      if (!backupIps || backupIps.length < 1) return undefined\n      const resolver = new dns.Resolver()\n      resolver.setServers(backupIps)\n      chunkedResults = await resolver.resolveTxt(`${SUBDOMAIN}.${handle}`)\n    } catch (err) {\n      return undefined\n    }\n    return this.parseDnsResult(chunkedResults)\n  }\n\n  parseDnsResult(chunkedResults: string[][]): string | undefined {\n    const results = chunkedResults.map((chunks) => chunks.join(''))\n    const found = results.filter((i) => i.startsWith(PREFIX))\n    if (found.length !== 1) {\n      return undefined\n    }\n    return found[0].slice(PREFIX.length)\n  }\n\n  private async getBackupNameserverIps(): Promise<string[] | undefined> {\n    if (!this.backupNameservers) {\n      return undefined\n    } else if (!this.backupNameserverIps) {\n      const responses = await Promise.allSettled(\n        this.backupNameservers.map((h) => dns.lookup(h)),\n      )\n      for (const res of responses) {\n        if (res.status === 'fulfilled') {\n          this.backupNameserverIps ??= []\n          this.backupNameserverIps.push(res.value.address)\n        }\n      }\n    }\n    return this.backupNameserverIps\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/id-resolver.ts",
    "content": "import { DidResolver } from './did/did-resolver'\nimport { HandleResolver } from './handle'\nimport { IdentityResolverOpts } from './types'\n\nexport class IdResolver {\n  public handle: HandleResolver\n  public did: DidResolver\n\n  constructor(opts: IdentityResolverOpts = {}) {\n    const { timeout = 3000, plcUrl, didCache } = opts\n    this.handle = new HandleResolver({\n      timeout,\n      backupNameservers: opts.backupNameservers,\n    })\n    this.did = new DidResolver({ timeout, plcUrl, didCache })\n  }\n}\n"
  },
  {
    "path": "packages/identity/src/index.ts",
    "content": "export * from './did'\nexport * from './handle'\nexport * from './id-resolver'\nexport * from './errors'\nexport * from './types'\n"
  },
  {
    "path": "packages/identity/src/types.ts",
    "content": "import { DidDocument } from '@atproto/common-web'\n\nexport { didDocument } from '@atproto/common-web'\nexport type { DidDocument } from '@atproto/common-web'\n\nexport type IdentityResolverOpts = {\n  timeout?: number\n  plcUrl?: string\n  didCache?: DidCache\n  backupNameservers?: string[]\n}\n\nexport type HandleResolverOpts = {\n  timeout?: number\n  backupNameservers?: string[]\n}\n\nexport type DidResolverOpts = {\n  timeout?: number\n  plcUrl?: string\n  didCache?: DidCache\n}\n\nexport type AtprotoData = {\n  did: string\n  signingKey: string\n  handle: string\n  pds: string\n}\n\nexport type CacheResult = {\n  did: string\n  doc: DidDocument\n  updatedAt: number\n  stale: boolean\n  expired: boolean\n}\n\nexport interface DidCache {\n  cacheDid(\n    did: string,\n    doc: DidDocument,\n    prevResult?: CacheResult,\n  ): Promise<void>\n  checkCache(did: string): Promise<CacheResult | null>\n  refreshCache(\n    did: string,\n    getDoc: () => Promise<DidDocument | null>,\n    prevResult?: CacheResult,\n  ): Promise<void>\n  clearEntry(did: string): Promise<void>\n  clear(): Promise<void>\n}\n"
  },
  {
    "path": "packages/identity/test.env",
    "content": "LOG_ENABLED=true\nLOG_DESTINATION=test.log"
  },
  {
    "path": "packages/identity/tests/did-cache.test.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { Database as DidPlcDb, PlcServer } from '@did-plc/server'\nimport getPort from 'get-port'\nimport { wait } from '@atproto/common-web'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { DidResolver } from '../src'\nimport { MemoryCache } from '../src/did/memory-cache'\n\ndescribe('did cache', () => {\n  let close: () => Promise<void>\n  let plcUrl: string\n  let did: string\n\n  let didCache: MemoryCache\n  let didResolver: DidResolver\n\n  beforeAll(async () => {\n    const plcDB = DidPlcDb.mock()\n    const plcPort = await getPort()\n    const plcServer = PlcServer.create({ db: plcDB, port: plcPort })\n    await plcServer.start()\n\n    plcUrl = 'http://localhost:' + plcPort\n\n    const signingKey = await Secp256k1Keypair.create()\n    const rotationKey = await Secp256k1Keypair.create()\n    const plcClient = new plc.Client(plcUrl)\n    did = await plcClient.createDid({\n      signingKey: signingKey.did(),\n      handle: 'alice.test',\n      pds: 'https://bsky.social',\n      rotationKeys: [rotationKey.did()],\n      signer: rotationKey,\n    })\n\n    didCache = new MemoryCache()\n    didResolver = new DidResolver({ plcUrl, didCache })\n\n    close = async () => {\n      await plcServer.destroy()\n    }\n  })\n\n  afterAll(async () => {\n    await close()\n  })\n\n  it('caches dids on lookup', async () => {\n    const resolved = await didResolver.resolve(did)\n    expect(resolved?.id).toBe(did)\n\n    const cached = await didResolver.cache?.checkCache(did)\n    expect(cached?.did).toBe(did)\n    expect(cached?.doc).toEqual(resolved)\n  })\n\n  it('clears cache and repopulates', async () => {\n    await didResolver.cache?.clear()\n    await didResolver.resolve(did)\n\n    const cached = await didResolver.cache?.checkCache(did)\n    expect(cached?.did).toBe(did)\n    expect(cached?.doc.id).toEqual(did)\n  })\n\n  it('accurately reports stale dids & refreshes the cache', async () => {\n    const didCache = new MemoryCache(1)\n    const shortCacheResolver = new DidResolver({ plcUrl, didCache })\n    const doc = await shortCacheResolver.resolve(did)\n\n    // let's mess with the cached doc so we get something different\n    await didCache.cacheDid(did, { ...doc, id: 'did:example:alice' })\n    await wait(5)\n\n    // first check the cache & see that we have the stale value\n    const cached = await shortCacheResolver.cache?.checkCache(did)\n    expect(cached?.stale).toBe(true)\n    expect(cached?.doc.id).toEqual('did:example:alice')\n    // see that the resolver gives us the stale value while it revalidates\n    const staleGet = await shortCacheResolver.resolve(did)\n    expect(staleGet?.id).toEqual('did:example:alice')\n\n    // since it revalidated, ensure we have the new value\n    const updatedCache = await shortCacheResolver.cache?.checkCache(did)\n    expect(updatedCache?.doc.id).toEqual(did)\n    const updatedGet = await shortCacheResolver.resolve(did)\n    expect(updatedGet?.id).toEqual(did)\n  })\n\n  it('does not return expired dids & refreshes the cache', async () => {\n    const didCache = new MemoryCache(0, 1)\n    const shortExpireResolver = new DidResolver({ plcUrl, didCache })\n    const doc = await shortExpireResolver.resolve(did)\n\n    // again, we mess with the cached doc so we get something different\n    await didCache.cacheDid(did, { ...doc, id: 'did:example:alice' })\n    await wait(5)\n\n    // see that the resolver does not return expired value & instead force refreshes\n    const staleGet = await shortExpireResolver.resolve(did)\n    expect(staleGet?.id).toEqual(did)\n  })\n})\n"
  },
  {
    "path": "packages/identity/tests/did-document.test.ts",
    "content": "import { DidResolver, ensureAtpDocument } from '../src'\n\ndescribe('did parsing', () => {\n  it('throws on bad DID document', async () => {\n    const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'\n    const docJson = `{\n  \"ideep\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co\",\n  \"blah\": [\n    \"https://dholms.xyz\"\n  ],\n  \"zoot\": [\n    {\n      \"id\": \"#elsewhere\",\n      \"type\": \"EcdsaSecp256k1VerificationKey2019\",\n      \"controller\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co\",\n      \"publicKeyMultibase\": \"zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR\"\n    }\n  ],\n  \"yarg\": [ ]\n}`\n    const resolver = new DidResolver({})\n    expect(() => {\n      resolver.validateDidDoc(did, JSON.parse(docJson))\n    }).toThrow()\n  })\n\n  it('parses legacy DID format, extracts atpData', async () => {\n    const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'\n    const docJson = `{\n  \"@context\": [\n    \"https://www.w3.org/ns/did/v1\",\n    \"https://w3id.org/security/suites/secp256k1-2019/v1\"\n  ],\n  \"id\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co\",\n  \"alsoKnownAs\": [\n    \"at://dholms.xyz\"\n  ],\n  \"verificationMethod\": [\n    {\n      \"id\": \"#atproto\",\n      \"type\": \"EcdsaSecp256k1VerificationKey2019\",\n      \"controller\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co\",\n      \"publicKeyMultibase\": \"zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR\"\n    }\n  ],\n  \"service\": [\n    {\n      \"id\": \"#atproto_pds\",\n      \"type\": \"AtprotoPersonalDataServer\",\n      \"serviceEndpoint\": \"https://bsky.social\"\n    }\n  ]\n}`\n    const resolver = new DidResolver({})\n    const doc = resolver.validateDidDoc(did, JSON.parse(docJson))\n    const atpData = ensureAtpDocument(doc)\n    expect(atpData.did).toEqual(did)\n    expect(atpData.handle).toEqual('dholms.xyz')\n    expect(atpData.pds).toEqual('https://bsky.social')\n    expect(atpData.signingKey).toEqual(\n      'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF',\n    )\n  })\n\n  it('parses newer Multikey DID format, extracts atpData', async () => {\n    const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'\n    const docJson = `{\n  \"@context\": [\n    \"https://www.w3.org/ns/did/v1\",\n    \"https://w3id.org/security/multikey/v1\",\n    \"https://w3id.org/security/suites/secp256k1-2019/v1\"\n  ],\n  \"id\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co\",\n  \"alsoKnownAs\": [\n    \"at://dholms.xyz\"\n  ],\n  \"verificationMethod\": [\n    {\n      \"id\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co#atproto\",\n      \"type\": \"Multikey\",\n      \"controller\": \"did:plc:yk4dd2qkboz2yv6tpubpc6co\",\n      \"publicKeyMultibase\": \"zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"\n    }\n  ],\n  \"service\": [\n    {\n      \"id\": \"#atproto_pds\",\n      \"type\": \"AtprotoPersonalDataServer\",\n      \"serviceEndpoint\": \"https://bsky.social\"\n    }\n  ]\n}`\n    const resolver = new DidResolver({})\n    const doc = resolver.validateDidDoc(did, JSON.parse(docJson))\n    const atpData = ensureAtpDocument(doc)\n    expect(atpData.did).toEqual(did)\n    expect(atpData.handle).toEqual('dholms.xyz')\n    expect(atpData.pds).toEqual('https://bsky.social')\n    expect(atpData.signingKey).toEqual(\n      'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/identity/tests/did-resolver.test.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { Database as DidPlcDb, PlcServer } from '@did-plc/server'\nimport getPort from 'get-port'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { DidDocument, DidResolver } from '../src'\nimport { DidWebDb } from './web/db'\nimport { DidWebServer } from './web/server'\n\ndescribe('did resolver', () => {\n  let close: () => Promise<void>\n  let webServer: DidWebServer\n  let plcUrl: string\n  let resolver: DidResolver\n\n  beforeAll(async () => {\n    const webDb = DidWebDb.memory()\n    webServer = DidWebServer.create(webDb, await getPort())\n    await new Promise((resolve, reject) => {\n      webServer._httpServer?.on('listening', resolve)\n      webServer._httpServer?.on('error', reject)\n    })\n\n    const plcDB = DidPlcDb.mock()\n    const plcPort = await getPort()\n    const plcServer = PlcServer.create({ db: plcDB, port: plcPort })\n    await plcServer.start()\n\n    plcUrl = 'http://localhost:' + plcPort\n    resolver = new DidResolver({ plcUrl })\n\n    close = async () => {\n      await webServer.close()\n      await plcServer.destroy()\n    }\n  })\n\n  afterAll(async () => {\n    await close()\n  })\n\n  const handle = 'alice.test'\n  const pds = 'https://service.test'\n  let signingKey: Secp256k1Keypair\n  let rotationKey: Secp256k1Keypair\n  let webDid: string\n  let plcDid: string\n  let didWebDoc: DidDocument\n  let didPlcDoc: DidDocument\n\n  it('creates the did on did:web & did:plc', async () => {\n    signingKey = await Secp256k1Keypair.create()\n    rotationKey = await Secp256k1Keypair.create()\n    const client = new plc.Client(plcUrl)\n    plcDid = await client.createDid({\n      signingKey: signingKey.did(),\n      handle,\n      pds,\n      rotationKeys: [rotationKey.did()],\n      signer: rotationKey,\n    })\n    didPlcDoc = await client.getDocument(plcDid)\n    const domain = encodeURIComponent(`localhost:${webServer.port}`)\n    webDid = `did:web:${domain}`\n    didWebDoc = {\n      ...didPlcDoc,\n      id: webDid,\n    }\n\n    await webServer.put(didWebDoc)\n  })\n\n  it('resolve valid did:web', async () => {\n    const didRes = await resolver.ensureResolve(webDid)\n    expect(didRes).toEqual(didWebDoc)\n  })\n\n  it('resolve valid atpData from did:web', async () => {\n    const atpData = await resolver.resolveAtprotoData(webDid)\n    expect(atpData.did).toEqual(webDid)\n    expect(atpData.handle).toEqual(handle)\n    expect(atpData.pds).toEqual(pds)\n    expect(atpData.signingKey).toEqual(signingKey.did())\n    expect(atpData.handle).toEqual(handle)\n  })\n\n  it('throws on malformed did:webs', async () => {\n    await expect(resolver.ensureResolve(`did:web:asdf`)).rejects.toThrow()\n    await expect(resolver.ensureResolve(`did:web:`)).rejects.toThrow()\n    await expect(resolver.ensureResolve(``)).rejects.toThrow()\n  })\n\n  it('throws on did:web with path components', async () => {\n    await expect(\n      resolver.ensureResolve(`did:web:example.com:u:bob`),\n    ).rejects.toThrow()\n  })\n\n  it('resolve valid did:plc', async () => {\n    const didRes = await resolver.ensureResolve(plcDid)\n    expect(didRes).toEqual(didPlcDoc)\n  })\n\n  it('resolve valid atpData from did:plc', async () => {\n    const atpData = await resolver.resolveAtprotoData(plcDid)\n    expect(atpData.did).toEqual(plcDid)\n    expect(atpData.handle).toEqual(handle)\n    expect(atpData.pds).toEqual(pds)\n    expect(atpData.signingKey).toEqual(signingKey.did())\n    expect(atpData.handle).toEqual(handle)\n  })\n\n  it('throws on malformed did:plc', async () => {\n    await expect(resolver.ensureResolve(`did:plc:asdf`)).rejects.toThrow()\n    await expect(resolver.ensureResolve(`did:plc`)).rejects.toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/identity/tests/handle-resolver.test.ts",
    "content": "import { HandleResolver } from '../src'\n\njest.mock('node:dns/promises', () => {\n  return {\n    resolveTxt: (handle: string) => {\n      if (handle === '_atproto.simple.test') {\n        return [['did=did:example:simpleDid']]\n      }\n      if (handle === '_atproto.noisy.test') {\n        return [\n          ['blah blah blah'],\n          ['did:example:fakeDid'],\n          ['atproto=did:example:fakeDid'],\n          ['did=did:example:noisyDid'],\n          [\n            'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',\n            'apsodfiuweproiasudfpoasidfu',\n          ],\n        ]\n      }\n      if (handle === '_atproto.bad.test') {\n        return [\n          ['blah blah blah'],\n          ['did:example:fakeDid'],\n          ['atproto=did:example:fakeDid'],\n          [\n            'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',\n            'apsodfiuweproiasudfpoasidfu',\n          ],\n        ]\n      }\n      if (handle === '_atproto.multi.test') {\n        return [['did=did:example:firstDid'], ['did=did:example:secondDid']]\n      }\n    },\n  }\n})\n\ndescribe('handle resolver', () => {\n  let resolver: HandleResolver\n\n  beforeAll(async () => {\n    resolver = new HandleResolver()\n  })\n\n  it('handles a simple DNS resolution', async () => {\n    const did = await resolver.resolveDns('simple.test')\n    expect(did).toBe('did:example:simpleDid')\n  })\n\n  it('handles a noisy DNS resolution', async () => {\n    const did = await resolver.resolveDns('noisy.test')\n    expect(did).toBe('did:example:noisyDid')\n  })\n\n  it('handles a bad DNS resolution', async () => {\n    const did = await resolver.resolveDns('bad.test')\n    expect(did).toBeUndefined()\n  })\n\n  it('throws on multiple dids under same domain', async () => {\n    const did = await resolver.resolveDns('multi.test')\n    expect(did).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "packages/identity/tests/web/db.ts",
    "content": "import { DidDocument } from '../../src/types'\n\ninterface DidStore {\n  put(key: string, val: string): Promise<void>\n  del(key: string): Promise<void>\n  get(key: string): Promise<string>\n}\n\nclass MemoryStore implements DidStore {\n  private store: Record<string, string> = {}\n\n  async put(key: string, val: string): Promise<void> {\n    this.store[key] = val\n  }\n\n  async del(key: string): Promise<void> {\n    this.assertHas(key)\n    delete this.store[key]\n  }\n\n  async get(key: string): Promise<string> {\n    this.assertHas(key)\n    return this.store[key]\n  }\n\n  assertHas(key: string): void {\n    if (!this.store[key]) {\n      throw new Error(`No object with key: ${key}`)\n    }\n  }\n}\n\nexport class DidWebDb {\n  constructor(private store: DidStore) {}\n\n  static memory(): DidWebDb {\n    const store = new MemoryStore()\n    return new DidWebDb(store)\n  }\n\n  async put(didPath: string, didDoc: DidDocument): Promise<void> {\n    await this.store.put(didPath, JSON.stringify(didDoc))\n  }\n\n  async get(didPath: string): Promise<DidDocument | null> {\n    try {\n      const got = await this.store.get(didPath)\n      return JSON.parse(got)\n    } catch (err) {\n      console.log(`Could not get did with path ${didPath}: ${err}`)\n      return null\n    }\n  }\n\n  async has(didPath: string): Promise<boolean> {\n    const got = await this.get(didPath)\n    return got !== null\n  }\n\n  async del(didPath: string): Promise<void> {\n    await this.store.del(didPath)\n  }\n}\n"
  },
  {
    "path": "packages/identity/tests/web/server.ts",
    "content": "import http from 'node:http'\nimport cors from 'cors'\nimport express, { Router, json } from 'express'\nimport { DidDocument } from '../../src'\nimport { DidWebDb } from './db'\n\nconst DOC_PATH = '/.well-known/did.json'\n\nconst routes = Router()\n\n// Get DID Doc\nroutes.get('/*', async (req, res) => {\n  const db = res.locals.db\n  const got = await db.get(req.url)\n  if (got === null) {\n    return res.status(404).send('Not found')\n  }\n  res.type('application/did+ld+json')\n  res.send(JSON.stringify(got))\n})\n\n// Write DID\nroutes.post('/', async (req, res) => {\n  const { didDoc } = req.body\n  if (!didDoc) {\n    return res.status(400)\n  }\n  // @TODO add in some proof\n  // @TODO validate didDoc body\n  const db = res.locals.db\n  const path = idToPath(didDoc.id)\n  await db.put(path, didDoc)\n  res.status(200).send()\n})\n\nconst idToPath = (id: string): string => {\n  const idp = id.split(':').slice(3)\n  let path =\n    idp.length > 0\n      ? idp.map(decodeURIComponent).join('/') + '/did.json'\n      : DOC_PATH\n\n  if (!path.startsWith('/')) path = `/${path}`\n  return path\n}\n\nexport class DidWebServer {\n  port: number\n  private _db: DidWebDb\n  _app: express.Application\n  _httpServer: http.Server | null = null\n\n  constructor(_app: express.Application, _db: DidWebDb, port: number) {\n    this._app = _app\n    this._db = _db\n    this.port = port\n  }\n\n  static create(db: DidWebDb, port: number): DidWebServer {\n    const app = express()\n\n    app.use(cors())\n    app.use(json())\n    app.use((_req, res, next) => {\n      res.locals.db = db\n      next()\n    })\n    app.use('/', routes)\n\n    const server = new DidWebServer(app, db, port)\n    server._httpServer = app.listen(port)\n    return server\n  }\n\n  async getByPath(didPath?: string): Promise<DidDocument | null> {\n    if (!didPath) return null\n    return this._db.get(didPath)\n  }\n\n  async getById(did?: string): Promise<DidDocument | null> {\n    if (!did) return null\n    const path = idToPath(did)\n    return this.getByPath(path)\n  }\n\n  async put(didDoc: DidDocument) {\n    await this._db.put(idToPath(didDoc.id), didDoc)\n  }\n\n  async delete(didOrDoc: string | DidDocument) {\n    const did = typeof didOrDoc === 'string' ? didOrDoc : didOrDoc.id\n    const path = idToPath(did)\n    await this._db.del(path)\n  }\n\n  close(): Promise<void> {\n    return new Promise((resolve) => {\n      if (this._httpServer) {\n        this._httpServer.close(() => resolve())\n      } else {\n        resolve()\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/identity/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/identity/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/identity/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/CHANGELOG.md",
    "content": "# @atproto-labs/did-resolver\n\n## 0.2.6\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n\n## 0.2.3\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `createDidResolver` utility\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/did@0.2.2\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/did@0.2.1\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/did@0.2.0\n  - @atproto-labs/simple-store@0.3.0\n  - @atproto-labs/simple-store-memory@0.1.4\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove export of internal utilities\n\n## 0.1.13\n\n### Patch Changes\n\n- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4)]:\n  - @atproto-labs/pipe@0.1.1\n  - @atproto-labs/fetch@0.2.3\n\n## 0.1.12\n\n### Patch Changes\n\n- Updated dependencies [[`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4)]:\n  - @atproto-labs/simple-store@0.2.0\n  - @atproto-labs/simple-store-memory@0.1.3\n\n## 0.1.11\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto-labs/fetch@0.2.2\n\n## 0.1.10\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/simple-store-memory@0.1.2\n  - @atproto-labs/simple-store@0.1.2\n  - @atproto-labs/fetch@0.2.1\n  - @atproto/did@0.1.5\n\n## 0.1.9\n\n### Patch Changes\n\n- [#3454](https://github.com/bluesky-social/atproto/pull/3454) [`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error descriptions\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87)]:\n  - @atproto/did@0.1.4\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2), [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2)]:\n  - @atproto-labs/fetch@0.2.0\n\n## 0.1.7\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure proper escaping when building PLC url\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f)]:\n  - @atproto-labs/fetch@0.1.2\n\n## 0.1.5\n\n### Patch Changes\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add \"allowHttp\" did:web method option\n\n- Updated dependencies [[`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2)]:\n  - @atproto/did@0.1.3\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto-labs/fetch@0.1.1\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]:\n  - @atproto/did@0.1.2\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/did@0.1.1\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use distinct type names to prevent conflicts\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto-labs/simple-store@0.1.1\n  - @atproto-labs/simple-store-memory@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/simple-store-memory@0.1.0\n  - @atproto-labs/simple-store@0.1.0\n  - @atproto-labs/fetch@0.1.0\n  - @atproto-labs/pipe@0.1.0\n  - @atproto/did@0.1.0\n"
  },
  {
    "path": "packages/internal/did-resolver/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/did-resolver\",\n  \"version\": \"0.2.6\",\n  \"license\": \"MIT\",\n  \"description\": \"DID resolution and verification library\",\n  \"keywords\": [\n    \"atproto\",\n    \"did\",\n    \"resolver\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/did-resolver\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch\": \"workspace:^\",\n    \"@atproto-labs/pipe\": \"workspace:^\",\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto-labs/simple-store-memory\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/create-did-resolver.ts",
    "content": "import { AtprotoIdentityDidMethods } from '@atproto/did'\nimport { DidCache, DidResolverCached } from './did-cache.js'\nimport {\n  DidResolverCommon,\n  DidResolverCommonOptions,\n} from './did-resolver-common.js'\nimport { DidResolver } from './did-resolver.js'\n\nexport type { AtprotoIdentityDidMethods }\n\nexport type CreateDidResolverOptions = {\n  didResolver?: DidResolver<AtprotoIdentityDidMethods>\n  didCache?: DidCache\n} & Partial<DidResolverCommonOptions>\n\nexport function createDidResolver(\n  options: CreateDidResolverOptions,\n): DidResolver<AtprotoIdentityDidMethods> {\n  const { didResolver, didCache } = options\n\n  if (didResolver instanceof DidResolverCached && !didCache) {\n    return didResolver\n  }\n\n  return new DidResolverCached(\n    didResolver ?? new DidResolverCommon(options),\n    didCache,\n  )\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/did-cache-memory.ts",
    "content": "import { Did, DidDocument } from '@atproto/did'\nimport {\n  SimpleStoreMemory,\n  SimpleStoreMemoryOptions,\n} from '@atproto-labs/simple-store-memory'\nimport { DidCache } from './did-cache.js'\n\nconst DEFAULT_TTL = 3600 * 1000 // 1 hour\nconst DEFAULT_MAX_SIZE = 50 * 1024 * 1024 // ~50MB\n\nexport type DidCacheMemoryOptions = SimpleStoreMemoryOptions<Did, DidDocument>\n\nexport class DidCacheMemory\n  extends SimpleStoreMemory<Did, DidDocument>\n  implements DidCache\n{\n  constructor(options?: DidCacheMemoryOptions) {\n    super(\n      options?.max == null\n        ? { ttl: DEFAULT_TTL, maxSize: DEFAULT_MAX_SIZE, ...options }\n        : { ttl: DEFAULT_TTL, ...options },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/did-cache.ts",
    "content": "import { Did, DidDocument } from '@atproto/did'\nimport { CachedGetter, SimpleStore } from '@atproto-labs/simple-store'\nimport { DidCacheMemory } from './did-cache-memory.js'\nimport { DidMethod, ResolveDidOptions } from './did-method.js'\nimport { DidResolver, ResolvedDocument } from './did-resolver.js'\n\nexport type { DidMethod, ResolveDidOptions, ResolvedDocument }\n\nexport type DidCache = SimpleStore<Did, DidDocument>\n\nexport type DidResolverCachedOptions = { cache?: DidCache }\n\nexport class DidResolverCached<M extends string = string>\n  implements DidResolver<M>\n{\n  protected readonly getter: CachedGetter<Did, DidDocument>\n  constructor(\n    resolver: DidResolver<M>,\n    cache: DidCache = new DidCacheMemory(),\n  ) {\n    this.getter = new CachedGetter<Did, DidDocument>(\n      (did, options) => resolver.resolve(did, options),\n      cache,\n    )\n  }\n\n  public async resolve<D extends Did>(did: D, options?: ResolveDidOptions) {\n    return this.getter.get(did, options) as Promise<ResolvedDocument<D, M>>\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/did-method.ts",
    "content": "import { Did, DidDocument } from '@atproto/did'\n\nexport type ResolveDidOptions = {\n  signal?: AbortSignal\n  noCache?: boolean\n}\n\nexport interface DidMethod<Method extends string> {\n  resolve: (\n    did: Did<Method>,\n    options?: ResolveDidOptions,\n  ) => DidDocument | PromiseLike<DidDocument>\n}\n\nexport type DidMethods<M extends string> = {\n  [K in M]: DidMethod<K>\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/did-resolver-base.ts",
    "content": "import { ZodError } from 'zod'\nimport { Did, DidError, extractDidMethod } from '@atproto/did'\nimport { FetchError, FetchResponseError } from '@atproto-labs/fetch'\nimport { DidMethod, DidMethods, ResolveDidOptions } from './did-method.js'\nimport { DidResolver, ResolvedDocument } from './did-resolver.js'\n\nexport type { DidMethod, ResolveDidOptions, ResolvedDocument }\n\nexport class DidResolverBase<M extends string = string>\n  implements DidResolver<M>\n{\n  protected readonly methods: Map<string, DidMethod<M>>\n\n  constructor(methods: DidMethods<M>) {\n    this.methods = new Map(Object.entries(methods))\n  }\n\n  async resolve<D extends Did>(\n    did: D,\n    options?: ResolveDidOptions,\n  ): Promise<ResolvedDocument<D, M>> {\n    options?.signal?.throwIfAborted()\n\n    const method = extractDidMethod(did)\n    const resolver = this.methods.get(method)\n    if (!resolver) {\n      throw new DidError(\n        did,\n        `Unsupported DID method`,\n        'did-method-invalid',\n        400,\n      )\n    }\n\n    try {\n      const document = await resolver.resolve(did as Did<M>, options)\n      if (document.id !== did) {\n        throw new DidError(\n          did,\n          `DID document id (${document.id}) does not match DID`,\n          'did-document-id-mismatch',\n          400,\n        )\n      }\n\n      return document as ResolvedDocument<D, M>\n    } catch (err) {\n      if (err instanceof FetchResponseError) {\n        const status = err.response.status >= 500 ? 502 : err.response.status\n        throw new DidError(did, err.message, 'did-fetch-error', status, err)\n      }\n\n      if (err instanceof FetchError) {\n        throw new DidError(did, err.message, 'did-fetch-error', 400, err)\n      }\n\n      if (err instanceof ZodError) {\n        throw new DidError(\n          did,\n          err.message,\n          'did-document-format-error',\n          503,\n          err,\n        )\n      }\n\n      throw DidError.from(err, did)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/did-resolver-common.ts",
    "content": "import { DidResolverBase } from './did-resolver-base.js'\nimport { DidPlcMethod, DidPlcMethodOptions } from './methods/plc.js'\nimport { DidWebMethod, DidWebMethodOptions } from './methods/web.js'\nimport { Simplify } from './util.js'\n\nexport type DidResolverCommonOptions = Simplify<\n  DidPlcMethodOptions & DidWebMethodOptions\n>\n\nexport class DidResolverCommon\n  extends DidResolverBase<'plc' | 'web'>\n  implements DidResolverBase<'plc' | 'web'>\n{\n  constructor(options?: DidResolverCommonOptions) {\n    super({\n      plc: new DidPlcMethod(options),\n      web: new DidWebMethod(options),\n    })\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/did-resolver.ts",
    "content": "import { Did, DidDocument } from '@atproto/did'\nimport { ResolveDidOptions } from './did-method.js'\n\nexport type ResolvedDocument<D extends Did, M extends string = string> =\n  D extends Did<infer N>\n    ? DidDocument<N extends string ? M : N extends M ? N : never>\n    : never\n\nexport interface DidResolver<M extends string = string> {\n  resolve<D extends Did>(\n    did: D,\n    options?: ResolveDidOptions,\n  ): Promise<ResolvedDocument<D, M>>\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/index.ts",
    "content": "export * from '@atproto/did'\n\nexport * from './create-did-resolver.js'\nexport * from './did-cache-memory.js'\nexport * from './did-cache.js'\nexport * from './did-method.js'\nexport * from './did-resolver-common.js'\nexport * from './did-resolver.js'\nexport * from './methods.js'\n"
  },
  {
    "path": "packages/internal/did-resolver/src/methods/plc.ts",
    "content": "import { Did, assertDidPlc, didDocumentValidator } from '@atproto/did'\nimport {\n  Fetch,\n  bindFetch,\n  fetchJsonProcessor,\n  fetchJsonZodProcessor,\n  fetchOkProcessor,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\nimport { DidMethod, ResolveDidOptions } from '../did-method.js'\n\nconst fetchSuccessHandler = pipe(\n  fetchOkProcessor(),\n  fetchJsonProcessor(/^application\\/(did\\+ld\\+)?json$/),\n  fetchJsonZodProcessor(didDocumentValidator),\n)\n\nexport type DidPlcMethodOptions = {\n  /**\n   * @default globalThis.fetch\n   */\n  fetch?: Fetch\n\n  /**\n   * @default 'https://plc.directory/'\n   */\n  plcDirectoryUrl?: string | URL\n}\n\nexport class DidPlcMethod implements DidMethod<'plc'> {\n  protected readonly fetch: Fetch<unknown>\n\n  public readonly plcDirectoryUrl: URL\n\n  constructor(options?: DidPlcMethodOptions) {\n    this.plcDirectoryUrl = new URL(\n      options?.plcDirectoryUrl || 'https://plc.directory/',\n    )\n    this.fetch = bindFetch(options?.fetch)\n  }\n\n  async resolve(did: Did<'plc'>, options?: ResolveDidOptions) {\n    // Although the did should start with `did:plc:` (thanks to typings), we\n    // should still check if the msid is valid.\n    assertDidPlc(did)\n\n    // Should never throw\n    const url = new URL(`/${encodeURIComponent(did)}`, this.plcDirectoryUrl)\n\n    return this.fetch(url, {\n      redirect: 'error',\n      headers: { accept: 'application/did+ld+json,application/json' },\n      signal: options?.signal,\n    }).then(fetchSuccessHandler)\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/methods/web.ts",
    "content": "import { Did, DidError, didDocumentValidator, didWebToUrl } from '@atproto/did'\nimport {\n  Fetch,\n  bindFetch,\n  fetchJsonProcessor,\n  fetchJsonZodProcessor,\n  fetchOkProcessor,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\nimport { DidMethod, ResolveDidOptions } from '../did-method.js'\n\nconst fetchSuccessHandler = pipe(\n  fetchOkProcessor(),\n  fetchJsonProcessor(/^application\\/(did\\+ld\\+)?json$/),\n  fetchJsonZodProcessor(didDocumentValidator),\n)\n\nexport type DidWebMethodOptions = {\n  fetch?: Fetch\n  /** @default true */\n  allowHttp?: boolean\n}\n\nexport class DidWebMethod implements DidMethod<'web'> {\n  protected readonly fetch: Fetch<unknown>\n  protected readonly allowHttp: boolean\n\n  constructor({\n    fetch = globalThis.fetch,\n    allowHttp = true,\n  }: DidWebMethodOptions = {}) {\n    this.fetch = bindFetch(fetch)\n    this.allowHttp = allowHttp\n  }\n\n  async resolve(did: Did<'web'>, options?: ResolveDidOptions) {\n    const didDocumentUrl = buildDidWebDocumentUrl(did)\n\n    if (!this.allowHttp && didDocumentUrl.protocol === 'http:') {\n      throw new DidError(\n        did,\n        'Resolution of \"http\" did:web is not allowed',\n        'did-web-http-not-allowed',\n      )\n    }\n\n    // Note we do not explicitly check for \"localhost\" here. Instead, we rely on\n    // the injected 'fetch' function to handle the URL. If the URL is\n    // \"localhost\", or resolves to a private IP address, the fetch function is\n    // responsible for handling it.\n\n    return this.fetch(didDocumentUrl, {\n      redirect: 'error',\n      headers: { accept: 'application/did+ld+json,application/json' },\n      signal: options?.signal,\n    }).then(fetchSuccessHandler)\n  }\n}\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8615}\n * @see {@link https://w3c-ccg.github.io/did-method-web/#create-register}\n */\nexport function buildDidWebDocumentUrl(did: Did<'web'>) {\n  const url = didWebToUrl(did) // Will throw if the DID is invalid\n\n  // Note: DID cannot end with an `:`, so they cannot end with a `/`. This is\n  // true unless when there is no path at all, in which case the URL constructor\n  // will set the pathname to `/`.\n\n  // https://w3c-ccg.github.io/did-method-web/#read-resolve\n  if (url.pathname === '/') {\n    return new URL(`/.well-known/did.json`, url)\n  } else {\n    return new URL(`${url.pathname}/did.json`, url)\n  }\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/src/methods.ts",
    "content": "export * from './methods/plc.js'\nexport * from './methods/web.js'\n"
  },
  {
    "path": "packages/internal/did-resolver/src/util.ts",
    "content": "export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n"
  },
  {
    "path": "packages/internal/did-resolver/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src/**/*.ts\"]\n}\n"
  },
  {
    "path": "packages/internal/did-resolver/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/fetch/CHANGELOG.md",
    "content": "# @atproto-labs/fetch\n\n## 0.2.3\n\n### Patch Changes\n\n- [#3821](https://github.com/bluesky-social/atproto/pull/3821) [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `follow` redirect mode, when explicitly set.\n\n- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4)]:\n  - @atproto-labs/pipe@0.1.1\n\n## 0.2.2\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improved error response parsing\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove explicit dependency on \"zod\". Improved typing of `fetchJsonZodProcessor` function.\n\n## 0.2.1\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3343](https://github.com/bluesky-social/atproto/pull/3343) [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix typo in `ResponseTranformer` and `fetchResponseJsonTranformer`\n\n### Patch Changes\n\n- [#3343](https://github.com/bluesky-social/atproto/pull/3343) [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Response mime type check is now case-insensitive (as per rfc2616)\n\n## 0.1.2\n\n### Patch Changes\n\n- [#3135](https://github.com/bluesky-social/atproto/pull/3135) [`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Support parsing of more fetch() errors\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose extractUrl utility\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add redirectCheckRequestTransform utility to prevent request redirects\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow customizing fetch logging function\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/pipe@0.1.0\n"
  },
  {
    "path": "packages/internal/fetch/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/fetch\",\n  \"version\": \"0.2.3\",\n  \"license\": \"MIT\",\n  \"description\": \"Isomorphic wrapper utilities for fetch API\",\n  \"keywords\": [\n    \"atproto\",\n    \"fetch\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/fetch\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/pipe\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/fetch/src/fetch-error.ts",
    "content": "export abstract class FetchError extends Error {\n  constructor(\n    public readonly statusCode: number,\n    message?: string,\n    options?: ErrorOptions,\n  ) {\n    super(message, options)\n  }\n\n  get expose() {\n    return true\n  }\n}\n"
  },
  {
    "path": "packages/internal/fetch/src/fetch-request.ts",
    "content": "import { FetchError } from './fetch-error.js'\nimport { asRequest } from './fetch.js'\nimport { extractUrl, isIp } from './util.js'\n\nexport class FetchRequestError extends FetchError {\n  constructor(\n    public readonly request: Request,\n    statusCode?: number,\n    message?: string,\n    options?: ErrorOptions,\n  ) {\n    if (statusCode == null || !message) {\n      const info = extractInfo(extractRootCause(options?.cause))\n      statusCode ??= info[0]\n      message ||= info[1]\n    }\n\n    super(statusCode, message, options)\n  }\n\n  get expose() {\n    // A 500 request error means that the request was not made due to an infra,\n    // programming or server side issue. The message should no be exposed to\n    // downstream clients.\n    return this.statusCode !== 500\n  }\n\n  static from(request: Request, cause: unknown): FetchRequestError {\n    if (cause instanceof FetchRequestError) return cause\n    return new FetchRequestError(request, undefined, undefined, { cause })\n  }\n}\n\nfunction extractRootCause(err: unknown): unknown {\n  // Unwrap the Network error from undici (i.e. Node's internal fetch() implementation)\n  // https://github.com/nodejs/undici/blob/3274c975947ce11a08508743df026f73598bfead/lib/web/fetch/index.js#L223-L228\n  if (\n    err instanceof TypeError &&\n    err.message === 'fetch failed' &&\n    err.cause !== undefined\n  ) {\n    return err.cause\n  }\n\n  return err\n}\n\nfunction extractInfo(err: unknown): [statusCode: number, message: string] {\n  if (typeof err === 'string' && err.length > 0) {\n    return [500, err]\n  }\n\n  if (!(err instanceof Error)) {\n    return [500, 'Failed to fetch']\n  }\n\n  // Undici fetch() \"network\" errors\n  switch (err.message) {\n    case 'failed to fetch the data URL':\n      return [400, err.message]\n    case 'unexpected redirect':\n    case 'cors failure':\n    case 'blocked':\n    case 'proxy authentication required':\n      // These cases could be represented either as a 4xx user error (invalid\n      // URL provided), or as a 5xx server error (server didn't behave as\n      // expected).\n      return [502, err.message]\n  }\n\n  // NodeJS errors\n  const code = err['code']\n  if (typeof code === 'string') {\n    switch (true) {\n      case code === 'ENOTFOUND':\n        return [400, 'Invalid hostname']\n      case code === 'ECONNREFUSED':\n        return [502, 'Connection refused']\n      case code === 'DEPTH_ZERO_SELF_SIGNED_CERT':\n        return [502, 'Self-signed certificate']\n      case code.startsWith('ERR_TLS'):\n        return [502, 'TLS error']\n      case code.startsWith('ECONN'):\n        return [502, 'Connection error']\n      default:\n        return [500, `${code} error`]\n    }\n  }\n\n  return [500, err.message]\n}\n\nexport function protocolCheckRequestTransform(protocols: {\n  'about:'?: boolean\n  'blob:'?: boolean\n  'data:'?: boolean\n  'file:'?: boolean\n  'http:'?: boolean | { allowCustomPort: boolean }\n  'https:'?: boolean | { allowCustomPort: boolean }\n}) {\n  return (input: Request | string | URL, init?: RequestInit) => {\n    const { protocol, port } = extractUrl(input)\n\n    const request = asRequest(input, init)\n\n    const config: undefined | boolean | { allowCustomPort?: boolean } =\n      Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined\n\n    if (!config) {\n      throw new FetchRequestError(\n        request,\n        400,\n        `Forbidden protocol \"${protocol}\"`,\n      )\n    } else if (config === true) {\n      // Safe to proceed\n    } else if (!config['allowCustomPort'] && port !== '') {\n      throw new FetchRequestError(\n        request,\n        400,\n        `Custom ${protocol} ports not allowed`,\n      )\n    }\n\n    return request\n  }\n}\n\nexport function explicitRedirectCheckRequestTransform() {\n  return (input: Request | string | URL, init?: RequestInit): Request => {\n    const request = asRequest(input, init)\n\n    // We want to avoid the case where the user of this code forgot to explicit\n    // a redirect strategy.\n    if (init?.redirect != null) return request\n\n    // Sadly, if the `input` is a request, and `init` was omitted, there is no\n    // way to tell if the `redirect === 'follow'` value comes from the user, or\n    // fetch's default. In order to prevent accidental omission, this case is\n    // forbidden.\n    if (request.redirect === 'follow') {\n      throw new FetchRequestError(\n        request,\n        500,\n        'Request redirect must be \"error\" or \"manual\"',\n      )\n    }\n\n    return request\n  }\n}\n\nexport function requireHostHeaderTransform() {\n  return (input: Request | string | URL, init?: RequestInit) => {\n    // Note that fetch() will automatically add the Host header from the URL and\n    // discard any Host header manually set in the request.\n\n    const { protocol, hostname } = extractUrl(input)\n\n    const request = asRequest(input, init)\n\n    // \"Host\" header only makes sense in the context of an HTTP request\n    if (protocol !== 'http:' && protocol !== 'https:') {\n      throw new FetchRequestError(\n        request,\n        400,\n        `\"${protocol}\" requests are not allowed`,\n      )\n    }\n\n    if (!hostname || isIp(hostname)) {\n      throw new FetchRequestError(request, 400, 'Invalid hostname')\n    }\n\n    return request\n  }\n}\n\nexport const DEFAULT_FORBIDDEN_DOMAIN_NAMES = [\n  'example.com',\n  '*.example.com',\n  'example.org',\n  '*.example.org',\n  'example.net',\n  '*.example.net',\n  'googleusercontent.com',\n  '*.googleusercontent.com',\n]\n\nexport function forbiddenDomainNameRequestTransform(\n  denyList: Iterable<string> = DEFAULT_FORBIDDEN_DOMAIN_NAMES,\n) {\n  const denySet = new Set<string>(denyList)\n\n  // Optimization: if no forbidden domain names are provided, we can skip the\n  // check entirely.\n  if (denySet.size === 0) {\n    return asRequest\n  }\n\n  return async (input: Request | string | URL, init?: RequestInit) => {\n    const { hostname } = extractUrl(input)\n\n    const request = asRequest(input, init)\n\n    // Full domain name check\n    if (denySet.has(hostname)) {\n      throw new FetchRequestError(request, 403, 'Forbidden hostname')\n    }\n\n    // Sub domain name check\n    let curDot = hostname.indexOf('.')\n    while (curDot !== -1) {\n      const subdomain = hostname.slice(curDot + 1)\n      if (denySet.has(`*.${subdomain}`)) {\n        throw new FetchRequestError(request, 403, 'Forbidden hostname')\n      }\n      curDot = hostname.indexOf('.', curDot + 1)\n    }\n\n    return request\n  }\n}\n"
  },
  {
    "path": "packages/internal/fetch/src/fetch-response.ts",
    "content": "import { Transformer, pipe } from '@atproto-labs/pipe'\nimport { FetchError } from './fetch-error.js'\nimport { TransformedResponse } from './transformed-response.js'\nimport {\n  Json,\n  MaxBytesTransformStream,\n  cancelBody,\n  ifString,\n  logCancellationError,\n} from './util.js'\n\n/**\n * media-type     = type \"/\" subtype *( \";\" parameter )\n * type           = token\n * subtype        = token\n * token          = 1*<any CHAR except CTLs or separators>\n * separators     = \"(\" | \")\" | \"<\" | \">\" | \"@\"\n *                | \",\" | \";\" | \":\" | \"\\\" | <\">\n *                | \"/\" | \"[\" | \"]\" | \"?\" | \"=\"\n *                | \"{\" | \"}\" | SP | HT\n * CTL            = <any US-ASCII control character (octets 0 - 31) and DEL (127)>\n * SP             = <US-ASCII SP, space (32)>\n * HT             = <US-ASCII HT, horizontal-tab (9)>\n * @note The type, subtype, and parameter attribute names are case-insensitive.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc2616#autoid-23}\n */\nconst JSON_MIME = /^application\\/(?:[^()<>@,;:/[\\]\\\\?={} \\t]+\\+)?json$/i\n\nexport type ResponseTransformer = Transformer<Response>\nexport type ResponseMessageGetter = Transformer<Response, string | undefined>\n\nexport class FetchResponseError extends FetchError {\n  constructor(\n    public readonly response: Response,\n    statusCode: number = response.status,\n    message: string = response.statusText,\n    options?: ErrorOptions,\n  ) {\n    super(statusCode, message, options)\n  }\n\n  static async from(\n    response: Response,\n    customMessage: string | ResponseMessageGetter = extractResponseMessage,\n    statusCode = response.status,\n    options?: ErrorOptions,\n  ) {\n    const message =\n      typeof customMessage === 'string'\n        ? customMessage\n        : typeof customMessage === 'function'\n          ? await customMessage(response)\n          : undefined\n\n    return new FetchResponseError(response, statusCode, message, options)\n  }\n}\n\nconst extractResponseMessage: ResponseMessageGetter = async (response) => {\n  const mimeType = extractMime(response)\n  if (!mimeType) return undefined\n\n  try {\n    if (mimeType === 'text/plain') {\n      return await response.text()\n    } else if (JSON_MIME.test(mimeType)) {\n      const json: unknown = await response.json()\n\n      if (typeof json === 'string') return json\n      if (typeof json === 'object' && json != null) {\n        const errorDescription = ifString(json['error_description'])\n        if (errorDescription) return errorDescription\n\n        const error = ifString(json['error'])\n        if (error) return error\n\n        const message = ifString(json['message'])\n        if (message) return message\n      }\n    }\n  } catch {\n    // noop\n  }\n\n  return undefined\n}\n\nexport async function peekJson(\n  response: Response,\n  maxSize = Infinity,\n): Promise<undefined | Json> {\n  const type = extractMime(response)\n  if (type !== 'application/json') return undefined\n  checkLength(response, maxSize)\n\n  // 1) Clone the request so we can consume the body\n  const clonedResponse = response.clone()\n\n  // 2) Make sure the request's body is not too large\n  const limitedResponse =\n    response.body && maxSize < Infinity\n      ? new TransformedResponse(\n          clonedResponse,\n          new MaxBytesTransformStream(maxSize),\n        )\n      : // Note: some runtimes (e.g. react-native) don't expose a body property\n        clonedResponse\n\n  // 3) Parse the JSON\n  return limitedResponse.json()\n}\n\nexport function checkLength(response: Response, maxBytes: number) {\n  // Note: negation accounts for invalid value types (NaN, non numbers)\n  if (!(maxBytes >= 0)) {\n    throw new TypeError('maxBytes must be a non-negative number')\n  }\n  const length = extractLength(response)\n  if (length != null && length > maxBytes) {\n    throw new FetchResponseError(response, 502, 'Response too large')\n  }\n  return length\n}\n\nexport function extractLength(response: Response) {\n  const contentLength = response.headers.get('Content-Length')\n  if (contentLength == null) return undefined\n  if (!/^\\d+$/.test(contentLength)) {\n    throw new FetchResponseError(response, 502, 'Invalid Content-Length')\n  }\n  const length = Number(contentLength)\n  if (!Number.isSafeInteger(length)) {\n    throw new FetchResponseError(response, 502, 'Content-Length too large')\n  }\n  return length\n}\n\nexport function extractMime(response: Response) {\n  const contentType = response.headers.get('Content-Type')\n  if (contentType == null) return undefined\n\n  return contentType.split(';', 1)[0]!.trim()\n}\n\n/**\n * If the transformer results in an error, ensure that the response body is\n * consumed as, in some environments (Node 👀), the response will not\n * automatically be GC'd.\n *\n * @see {@link https://undici.nodejs.org/#/?id=garbage-collection}\n * @param [onCancellationError] - Callback to handle any async body cancelling\n * error. Defaults to logging the error. Do not use `null` if the request is\n * cloned.\n */\nexport function cancelBodyOnError<T>(\n  transformer: Transformer<Response, T>,\n  onCancellationError: null | ((err: unknown) => void) = logCancellationError,\n): (response: Response) => Promise<T> {\n  return async (response) => {\n    try {\n      return await transformer(response)\n    } catch (err) {\n      await cancelBody(response, onCancellationError ?? undefined)\n      throw err\n    }\n  }\n}\n\nexport function fetchOkProcessor(\n  customMessage?: string | ResponseMessageGetter,\n): ResponseTransformer {\n  return cancelBodyOnError((response) => {\n    return fetchOkTransformer(response, customMessage)\n  })\n}\n\nexport async function fetchOkTransformer(\n  response: Response,\n  customMessage?: string | ResponseMessageGetter,\n) {\n  if (response.ok) return response\n  throw await FetchResponseError.from(response, customMessage)\n}\n\nexport function fetchMaxSizeProcessor(maxBytes: number): ResponseTransformer {\n  if (maxBytes === Infinity) return (response) => response\n  if (!Number.isFinite(maxBytes) || maxBytes < 0) {\n    throw new TypeError('maxBytes must be a 0, Infinity or a positive number')\n  }\n  return cancelBodyOnError((response) => {\n    return fetchResponseMaxSizeChecker(response, maxBytes)\n  })\n}\n\nexport function fetchResponseMaxSizeChecker(\n  response: Response,\n  maxBytes: number,\n): Response {\n  if (maxBytes === Infinity) return response\n  checkLength(response, maxBytes)\n\n  // Some engines (react-native 👀) don't expose a body property. In that case,\n  // we will only rely on the Content-Length header.\n  if (!response.body) return response\n\n  const transform = new MaxBytesTransformStream(maxBytes)\n  return new TransformedResponse(response, transform)\n}\n\nexport type MimeTypeCheckFn = (mimeType: string) => boolean\nexport type MimeTypeCheck = string | RegExp | MimeTypeCheckFn\n\nexport function fetchTypeProcessor(\n  expectedMime: MimeTypeCheck,\n  contentTypeRequired = true,\n): ResponseTransformer {\n  const isExpected: MimeTypeCheckFn =\n    typeof expectedMime === 'string'\n      ? (mimeType) => mimeType === expectedMime\n      : expectedMime instanceof RegExp\n        ? (mimeType) => expectedMime.test(mimeType)\n        : expectedMime\n\n  return cancelBodyOnError((response) => {\n    return fetchResponseTypeChecker(response, isExpected, contentTypeRequired)\n  })\n}\n\nexport async function fetchResponseTypeChecker(\n  response: Response,\n  isExpectedMime: MimeTypeCheckFn,\n  contentTypeRequired = true,\n): Promise<Response> {\n  const mimeType = extractMime(response)\n  if (mimeType) {\n    if (!isExpectedMime(mimeType.toLowerCase())) {\n      throw await FetchResponseError.from(\n        response,\n        `Unexpected response Content-Type (${mimeType})`,\n        502,\n      )\n    }\n  } else if (contentTypeRequired) {\n    throw await FetchResponseError.from(\n      response,\n      'Missing response Content-Type header',\n      502,\n    )\n  }\n\n  return response\n}\n\nexport type ParsedJsonResponse<T = Json> = {\n  response: Response\n  json: T\n}\n\nexport async function fetchResponseJsonTransformer<T = Json>(\n  response: Response,\n): Promise<ParsedJsonResponse<T>> {\n  try {\n    const json = (await response.json()) as T\n    return { response, json }\n  } catch (cause) {\n    throw new FetchResponseError(\n      response,\n      502,\n      'Unable to parse response as JSON',\n      { cause },\n    )\n  }\n}\n\nexport function fetchJsonProcessor<T = Json>(\n  expectedMime: MimeTypeCheck = JSON_MIME,\n  contentTypeRequired = true,\n): Transformer<Response, ParsedJsonResponse<T>> {\n  return pipe(\n    fetchTypeProcessor(expectedMime, contentTypeRequired),\n    cancelBodyOnError(fetchResponseJsonTransformer<T>),\n  )\n}\n\nexport type SyncValidationSchema<S, P = unknown> = {\n  parse(value: unknown, params?: P): S\n}\n\nexport type AsyncValidationSchema<S, P = unknown> = {\n  parseAsync(value: unknown, params?: P): Promise<S>\n}\n\nexport function fetchJsonValidatorProcessor<S, P = unknown>(\n  schema: SyncValidationSchema<S, P> | AsyncValidationSchema<S, P>,\n  params?: P,\n): Transformer<ParsedJsonResponse, S> {\n  if ('parseAsync' in schema && typeof schema.parseAsync === 'function') {\n    return async (jsonResponse: ParsedJsonResponse): Promise<S> =>\n      schema.parseAsync(jsonResponse.json, params)\n  }\n\n  if ('parse' in schema && typeof schema.parse === 'function') {\n    return async (jsonResponse: ParsedJsonResponse): Promise<S> =>\n      schema.parse(jsonResponse.json, params)\n  }\n\n  // Needed for type safety (and allows fool proofing the usage of this function)\n  throw new TypeError('Invalid schema')\n}\n\n/** @note Use {@link fetchJsonValidatorProcessor} instead */\nexport const fetchJsonZodProcessor = fetchJsonValidatorProcessor\n"
  },
  {
    "path": "packages/internal/fetch/src/fetch-wrap.ts",
    "content": "import { FetchRequestError } from './fetch-request.js'\nimport { Fetch, FetchContext, toRequestTransformer } from './fetch.js'\nimport { TransformedResponse } from './transformed-response.js'\nimport { padLines, stringifyMessage } from './util.js'\n\ntype LogFn<Args extends unknown[]> = (...args: Args) => void | PromiseLike<void>\n\nexport function loggedFetch<C = FetchContext>({\n  fetch = globalThis.fetch as Fetch<C>,\n  logRequest = true as boolean | LogFn<[request: Request]>,\n  logResponse = true as boolean | LogFn<[response: Response, request: Request]>,\n  logError = true as boolean | LogFn<[error: unknown, request: Request]>,\n}) {\n  const onRequest =\n    logRequest === true\n      ? async (request) => {\n          const requestMessage = await stringifyMessage(request)\n          console.info(\n            `> ${request.method} ${request.url}\\n${padLines(requestMessage, '  ')}`,\n          )\n        }\n      : logRequest || undefined\n\n  const onResponse =\n    logResponse === true\n      ? async (response) => {\n          const responseMessage = await stringifyMessage(response.clone())\n          console.info(\n            `< HTTP/1.1 ${response.status} ${response.statusText}\\n${padLines(responseMessage, '  ')}`,\n          )\n        }\n      : logResponse || undefined\n\n  const onError =\n    logError === true\n      ? async (error) => {\n          console.error(`< Error:`, error)\n        }\n      : logError || undefined\n\n  if (!onRequest && !onResponse && !onError) return fetch\n\n  return toRequestTransformer(async function (\n    this: C,\n    request,\n  ): Promise<Response> {\n    if (onRequest) await onRequest(request)\n\n    try {\n      const response = await fetch.call(this, request)\n\n      if (onResponse) await onResponse(response, request)\n\n      return response\n    } catch (error) {\n      if (onError) await onError(error, request)\n\n      throw error\n    }\n  })\n}\n\nexport const timedFetch = <C = FetchContext>(\n  timeout = 60e3,\n  fetch: Fetch<C> = globalThis.fetch,\n): Fetch<C> => {\n  if (timeout === Infinity) return fetch\n  if (!Number.isFinite(timeout) || timeout <= 0) {\n    throw new TypeError('Timeout must be positive')\n  }\n  return toRequestTransformer(async function (\n    this: C,\n    request,\n  ): Promise<Response> {\n    const controller = new AbortController()\n    const signal = controller.signal\n\n    const abort = () => {\n      controller.abort()\n    }\n    const cleanup = () => {\n      clearTimeout(timer)\n      request.signal?.removeEventListener('abort', abort)\n    }\n\n    const timer = setTimeout(abort, timeout)\n    if (typeof timer === 'object') timer.unref?.() // only on node\n    request.signal?.addEventListener('abort', abort)\n\n    signal.addEventListener('abort', cleanup)\n\n    const response = await fetch.call(this, request, { signal })\n\n    if (!response.body) {\n      cleanup()\n      return response\n    } else {\n      // Cleanup the timer & event listeners when the body stream is closed\n      const transform = new TransformStream({ flush: cleanup })\n      return new TransformedResponse(response, transform)\n    }\n  })\n}\n\n/**\n * Wraps a fetch function to bind it to a specific context, and wrap any thrown\n * errors into a FetchRequestError.\n *\n * @example\n *\n * ```ts\n * class MyClient {\n *   constructor(private fetch = globalThis.fetch) {}\n *\n *   async get(url: string) {\n *     // This will generate an error, because the context used is not a\n *     // FetchContext (it's a MyClient instance).\n *     return this.fetch(url)\n *   }\n * }\n * ```\n *\n * @example\n *\n * ```ts\n * class MyClient {\n *   private fetch: Fetch<unknown>\n *\n *   constructor(fetch = globalThis.fetch) {\n *     this.fetch = bindFetch(fetch)\n *   }\n *\n *   async get(url: string) {\n *     return this.fetch(url) // no more error\n *   }\n * }\n * ```\n */\nexport function bindFetch<C = FetchContext>(\n  fetch: Fetch<C> = globalThis.fetch,\n  context: C = globalThis as C,\n) {\n  return toRequestTransformer(async (request) => {\n    try {\n      return await fetch.call(context, request)\n    } catch (err) {\n      throw FetchRequestError.from(request, err)\n    }\n  })\n}\n"
  },
  {
    "path": "packages/internal/fetch/src/fetch.ts",
    "content": "import { ThisParameterOverride } from './util.js'\n\nexport type FetchContext = void | null | typeof globalThis\n\nexport type FetchBound = (\n  input: string | URL | Request,\n  init?: RequestInit,\n) => Promise<Response>\n\n// NOT using \"typeof globalThis.fetch\" here because \"globalThis.fetch\" does not\n// have a \"this\" parameter, while runtimes do ensure that \"fetch\" is called with\n// the correct \"this\" parameter (either null, undefined, or window).\n\nexport type Fetch<C = FetchContext> = ThisParameterOverride<C, FetchBound>\n\nexport type SimpleFetchBound = (input: Request) => Promise<Response>\nexport type SimpleFetch<C = FetchContext> = ThisParameterOverride<\n  C,\n  SimpleFetchBound\n>\n\nexport function toRequestTransformer<C, O>(\n  requestTransformer: (this: C, input: Request) => O,\n): ThisParameterOverride<\n  C,\n  (input: string | URL | Request, init?: RequestInit) => O\n> {\n  return function (this: C, input, init) {\n    return requestTransformer.call(this, asRequest(input, init))\n  }\n}\n\nexport function asRequest(\n  input: string | URL | Request,\n  init?: RequestInit,\n): Request {\n  if (!init && input instanceof Request) return input\n  return new Request(input, init)\n}\n"
  },
  {
    "path": "packages/internal/fetch/src/index.ts",
    "content": "export * from './fetch-error.js'\nexport * from './fetch-request.js'\nexport * from './fetch-response.js'\nexport * from './fetch-wrap.js'\nexport * from './fetch.js'\nexport * from './util.js'\n"
  },
  {
    "path": "packages/internal/fetch/src/transformed-response.ts",
    "content": "export class TransformedResponse extends Response {\n  #response: Response\n\n  constructor(response: Response, transform: TransformStream) {\n    if (!response.body) {\n      throw new TypeError('Response body is not available')\n    }\n    if (response.bodyUsed) {\n      throw new TypeError('Response body is already used')\n    }\n\n    super(response.body.pipeThrough(transform), {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n    })\n\n    this.#response = response\n  }\n\n  /**\n   * Some props can't be set through ResponseInit, so we need to proxy them\n   */\n  get url() {\n    return this.#response.url\n  }\n  get redirected() {\n    return this.#response.redirected\n  }\n  get type() {\n    return this.#response.type\n  }\n  get statusText() {\n    return this.#response.statusText\n  }\n}\n"
  },
  {
    "path": "packages/internal/fetch/src/util.ts",
    "content": "// @TODO: Move some of these to a shared package ?\n\nexport type JsonScalar = string | number | boolean | null\nexport type Json = JsonScalar | Json[] | { [key: string]: undefined | Json }\nexport type JsonObject = { [key: string]: Json }\nexport type JsonArray = Json[]\n\nexport type ThisParameterOverride<\n  C,\n  Fn extends (...a: any) => any,\n> = Fn extends (...args: infer P) => infer R\n  ? ((this: C, ...args: P) => R) & {\n      bind(context: C): (...args: P) => R\n    }\n  : never\n\nexport function isIp(hostname: string) {\n  // IPv4\n  if (hostname.match(/^\\d+\\.\\d+\\.\\d+\\.\\d+$/)) return true\n\n  // IPv6\n  if (hostname.startsWith('[') && hostname.endsWith(']')) return true\n\n  return false\n}\n\nexport const ifString = <V>(v: V) => (typeof v === 'string' ? v : undefined)\n\nexport class MaxBytesTransformStream extends TransformStream<\n  Uint8Array,\n  Uint8Array\n> {\n  constructor(maxBytes: number) {\n    // Note: negation accounts for invalid value types (NaN, non numbers)\n    if (!(maxBytes >= 0)) {\n      throw new TypeError('maxBytes must be a non-negative number')\n    }\n\n    let bytesRead = 0\n\n    super({\n      transform: (\n        chunk: Uint8Array,\n        ctrl: TransformStreamDefaultController<Uint8Array>,\n      ) => {\n        if ((bytesRead += chunk.length) <= maxBytes) {\n          ctrl.enqueue(chunk)\n        } else {\n          ctrl.error(new Error('Response too large'))\n        }\n      },\n    })\n  }\n}\n\nconst LINE_BREAK = /\\r?\\n/g\nexport function padLines(input: string, pad: string) {\n  if (!input) return input\n  return pad + input.replace(LINE_BREAK, `$&${pad}`)\n}\n\n/**\n * @param [onCancellationError] - Callback that will trigger to asynchronously\n * handle any error that occurs while cancelling the response body. Providing\n * this will speed up the process and avoid potential deadlocks. Defaults to\n * awaiting the cancellation operation. use `\"log\"` to log the error.\n * @see {@link https://undici.nodejs.org/#/?id=garbage-collection}\n * @note awaiting this function's result, when no `onCancellationError` is\n * provided, might result in a dead lock. Indeed, if the response was cloned(),\n * the response.body.cancel() method will not resolve until the other response's\n * body is consumed/cancelled.\n *\n * @example\n * ```ts\n * // Make sure response was not cloned, or that every cloned response was\n * // consumed/cancelled before awaiting this function's result.\n * await cancelBody(response)\n * ```\n * @example\n * ```ts\n * await cancelBody(response, (err) => {\n *   // No biggie, let's just log the error\n *   console.warn('Failed to cancel response body', err)\n * })\n * ```\n * @example\n * ```ts\n * // Will generate an \"unhandledRejection\" if an error occurs while cancelling\n * // the response body. This will likely crash the process.\n * await cancelBody(response, (err) => { throw err })\n * ```\n */\nexport async function cancelBody(\n  body: Body,\n  onCancellationError?: 'log' | ((err: unknown) => void),\n): Promise<void> {\n  if (\n    body.body &&\n    !body.bodyUsed &&\n    !body.body.locked &&\n    // Support for alternative fetch implementations\n    typeof body.body.cancel === 'function'\n  ) {\n    if (typeof onCancellationError === 'function') {\n      void body.body.cancel().catch(onCancellationError)\n    } else if (onCancellationError === 'log') {\n      void body.body.cancel().catch(logCancellationError)\n    } else {\n      await body.body.cancel()\n    }\n  }\n}\n\nexport function logCancellationError(err: unknown): void {\n  console.warn('Failed to cancel response body', err)\n}\n\nexport async function stringifyMessage(input: Body & { headers: Headers }) {\n  try {\n    const headers = stringifyHeaders(input.headers)\n    const payload = await stringifyBody(input)\n    return headers && payload ? `${headers}\\n${payload}` : headers || payload\n  } finally {\n    void cancelBody(input, 'log')\n  }\n}\n\nfunction stringifyHeaders(headers: Headers) {\n  return Array.from(headers)\n    .map(([name, value]) => `${name}: ${value}`)\n    .join('\\n')\n}\n\nasync function stringifyBody(body: Body) {\n  try {\n    const blob = await body.blob()\n    if (blob.type?.startsWith('text/')) {\n      const text = await blob.text()\n      return JSON.stringify(text)\n    }\n\n    if (/application\\/(?:\\w+\\+)?json/.test(blob.type)) {\n      const text = await blob.text()\n      return text.includes('\\n') ? JSON.stringify(JSON.parse(text)) : text\n    }\n\n    return `[Body size: ${blob.size}, type: ${JSON.stringify(blob.type)} ]`\n  } catch {\n    return '[Body could not be read]'\n  }\n}\n\nexport const extractUrl = (input: Request | string | URL) =>\n  typeof input === 'string'\n    ? new URL(input)\n    : input instanceof URL\n      ? input\n      : new URL(input.url)\n"
  },
  {
    "path": "packages/internal/fetch/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/internal/fetch/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/fetch-node/CHANGELOG.md",
    "content": "# @atproto-labs/fetch-node\n\n## 0.2.0\n\n### Minor Changes\n\n- [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `isLocalHostname` export\n\n## 0.1.10\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow forcing the use of keep-alive agent on older NodeJs version when unicast protection is active\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use keep-alive connection when unicast protection is enabled on NodeJS >= 20\n\n## 0.1.9\n\n### Patch Changes\n\n- [#3821](https://github.com/bluesky-social/atproto/pull/3821) [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow explicit `follow` mode in safe fetch wrap.\n\n- [#3819](https://github.com/bluesky-social/atproto/pull/3819) [`36dbd4155`](https://github.com/bluesky-social/atproto/commit/36dbd41551f74052a3f584719a1a7edd86eca201) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix potential memory leak\n\n- [#3821](https://github.com/bluesky-social/atproto/pull/3821) [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow disabling the need for an explicit `redirect` mode\n\n- [#3818](https://github.com/bluesky-social/atproto/pull/3818) [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on the Public Suffix List\n\n- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4)]:\n  - @atproto-labs/pipe@0.1.1\n  - @atproto-labs/fetch@0.2.3\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto-labs/fetch@0.2.2\n\n## 0.1.7\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/fetch@0.2.1\n\n## 0.1.6\n\n### Patch Changes\n\n- [#3379](https://github.com/bluesky-social/atproto/pull/3379) [`9c0128193`](https://github.com/bluesky-social/atproto/commit/9c01281931a371304bcfa465005d7363c003bc5f) Thanks [@devinivy](https://github.com/devinivy)! - Unicast checks should permit PSL domains.\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2), [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2)]:\n  - @atproto-labs/fetch@0.2.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f)]:\n  - @atproto-labs/fetch@0.1.2\n\n## 0.1.3\n\n### Patch Changes\n\n- [#2865](https://github.com/bluesky-social/atproto/pull/2865) [`80450cbf2`](https://github.com/bluesky-social/atproto/commit/80450cbf2ca27967ee9fe1a5f4bc590b26f1e6b2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not use HTTP2 connection when performing \"safe fetch\" HTTP requests\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2854](https://github.com/bluesky-social/atproto/pull/2854) [`8943c1008`](https://github.com/bluesky-social/atproto/commit/8943c10082702bbc0fc150237c6cc421251afd51) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disable use of HTTP2 when checking SSRF IP\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent bypass of ssrf ip verification\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose IP filtering utilities\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto-labs/fetch@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/fetch@0.1.0\n  - @atproto-labs/pipe@0.1.0\n"
  },
  {
    "path": "packages/internal/fetch-node/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/fetch-node\",\n  \"version\": \"0.2.0\",\n  \"license\": \"MIT\",\n  \"description\": \"SSRF protection for fetch() in Node.js\",\n  \"keywords\": [\n    \"atproto\",\n    \"fetch\",\n    \"node\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/fetch-node\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch\": \"workspace:^\",\n    \"@atproto-labs/pipe\": \"workspace:^\",\n    \"ipaddr.js\": \"^2.1.0\",\n    \"undici\": \"^6.14.1\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/fetch-node/src/index.ts",
    "content": "export * from '@atproto-labs/fetch'\n\nexport * from './safe.js'\nexport * from './unicast.js'\nexport * from './util.js'\n"
  },
  {
    "path": "packages/internal/fetch-node/src/safe.ts",
    "content": "import {\n  DEFAULT_FORBIDDEN_DOMAIN_NAMES,\n  Fetch,\n  asRequest,\n  explicitRedirectCheckRequestTransform,\n  fetchMaxSizeProcessor,\n  forbiddenDomainNameRequestTransform,\n  protocolCheckRequestTransform,\n  requireHostHeaderTransform,\n  timedFetch,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\nimport { UnicastFetchWrapOptions, unicastFetchWrap } from './unicast.js'\n\nexport type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {\n  responseMaxSize?: number\n  ssrfProtection?: boolean\n  allowCustomPort?: boolean\n  allowData?: boolean\n  allowHttp?: boolean\n  allowIpHost?: boolean\n  allowPrivateIps?: boolean\n  timeout?: number\n  forbiddenDomainNames?: Iterable<string>\n  /**\n   * When `false`, a {@link RequestInit['redirect']} value must be explicitly\n   * provided as second argument to the returned function or requests will fail.\n   *\n   * @default false\n   */\n  allowImplicitRedirect?: boolean\n}\n\n/**\n * Wrap a fetch function with safety checks so that it can be safely used\n * with user provided input (URL).\n *\n * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}\n *\n * @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`\n * (default), then the returned function **must** be called setting the second\n * argument's `redirect` property to one of the allowed values. Otherwise, if\n * the returned fetch function is called with a `Request` object (and no\n * explicit `redirect` init object), then the verification code will not be able\n * to determine if the `redirect` property was explicitly set or based on the\n * default value (`follow`), causing it to preventively block the request (throw\n * an error). For this reason, unless you set\n * {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should\n * **not** wrap the returned function into another function that creates a\n * {@link Request} object before passing it to the function (as a e.g. a logging\n * function would).\n */\nexport function safeFetchWrap<C>({\n  fetch = globalThis.fetch as Fetch<C>,\n  dangerouslyForceKeepAliveAgent = false,\n  responseMaxSize = 512 * 1024, // 512kB\n  ssrfProtection = true,\n  allowCustomPort = !ssrfProtection,\n  allowData = false,\n  allowHttp = !ssrfProtection,\n  allowIpHost = true,\n  allowPrivateIps = !ssrfProtection,\n  timeout = 10e3,\n  forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,\n  allowImplicitRedirect = false,\n}: SafeFetchWrapOptions<C> = {}) {\n  return pipe(\n    /**\n     * Require explicit {@link RequestInit['redirect']} mode\n     */\n    allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),\n\n    /**\n     * Only requests that will be issued with a \"Host\" header are allowed.\n     */\n    allowIpHost ? asRequest : requireHostHeaderTransform(),\n\n    /**\n     * Prevent using http:, file: or data: protocols.\n     */\n    protocolCheckRequestTransform({\n      'about:': false,\n      'data:': allowData,\n      'file:': false,\n      'http:': allowHttp && { allowCustomPort },\n      'https:': { allowCustomPort },\n    }),\n\n    /**\n     * Disallow fetching from domains we know are not atproto/OIDC client\n     * implementation. Note that other domains can be blocked by providing a\n     * custom fetch function combined with another\n     * forbiddenDomainNameRequestTransform.\n     */\n    forbiddenDomainNameRequestTransform(forbiddenDomainNames),\n\n    /**\n     * Since we will be fetching from the network based on user provided\n     * input, let's mitigate resource exhaustion attacks by setting a timeout.\n     */\n    timedFetch(\n      timeout,\n\n      /**\n       * Since we will be fetching from the network based on user provided\n       * input, we need to make sure that the request is not vulnerable to SSRF\n       * attacks.\n       */\n      allowPrivateIps\n        ? fetch\n        : unicastFetchWrap({ fetch, dangerouslyForceKeepAliveAgent }),\n    ),\n\n    /**\n     * Since we will be fetching user owned data, we need to make sure that an\n     * attacker cannot force us to download a large amounts of data.\n     */\n    fetchMaxSizeProcessor(responseMaxSize),\n  ) satisfies Fetch<unknown>\n}\n"
  },
  {
    "path": "packages/internal/fetch-node/src/unicast.ts",
    "content": "import dns, { LookupAddress } from 'node:dns'\nimport { LookupFunction } from 'node:net'\nimport ipaddr from 'ipaddr.js'\nimport { Agent, Client } from 'undici'\nimport {\n  Fetch,\n  FetchContext,\n  FetchRequestError,\n  asRequest,\n  extractUrl,\n} from '@atproto-labs/fetch'\nimport { isUnicastIp } from './util.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nexport type UnicastFetchWrapOptions<C = FetchContext> = {\n  fetch?: Fetch<C>\n\n  /**\n   * ## ‼️ important security feature use with care\n   *\n   * On older NodeJS version, the `dispatcher` init option is ignored when\n   * creating a new Request instance. It can only be passed through the fetch\n   * function directly.\n   *\n   * Since this is a security feature, we need to ensure that the unicastLookup\n   * function is called to resolve the hostname to a unicast IP address.\n   *\n   * However, in the case a custom \"fetch\" function is passed here (fetch !==\n   * globalThis.fetch), we have no guarantee that the dispatcher will be used to\n   * make the request. Because of this, in such a case, we will use a one-time\n   * use dispatcher that checks that the provided fetch function indeed made use\n   * of the \"unicastLookup\" when a custom dispatch init function is used.\n   *\n   * Sadly, this means that we cannot use \"keepAlive\" connections, as the method\n   * used to ensure that \"unicastLookup\" gets called requires to create a new\n   * dispatcher for each request.\n   *\n   * If you can guarantee that the provided fetch function will make use of the\n   * \"dispatcher\" init option, you can set this flag to true, which will enable\n   * the use of a single agent (with keep-alive) for all requests.\n   *\n   * @default false\n   * @note This option has no effect on Node.js versions >= 20\n   */\n  dangerouslyForceKeepAliveAgent?: boolean\n}\n\n// @TODO support other runtimes ?\nconst SUPPORTS_REQUEST_INIT_DISPATCHER =\n  Number(process.versions.node.split('.')[0]) >= 20\n\n/**\n * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}\n */\nexport function unicastFetchWrap<C = FetchContext>({\n  fetch = globalThis.fetch,\n  dangerouslyForceKeepAliveAgent = false,\n}: UnicastFetchWrapOptions<C>): Fetch<C> {\n  if (\n    SUPPORTS_REQUEST_INIT_DISPATCHER ||\n    dangerouslyForceKeepAliveAgent ||\n    fetch === globalThis.fetch\n  ) {\n    const dispatcher = new Agent({\n      connect: { lookup: unicastLookup },\n    })\n\n    return async function (input, init): Promise<Response> {\n      if (init?.dispatcher) {\n        throw new FetchRequestError(\n          asRequest(input, init),\n          500,\n          'SSRF protection cannot be used with a custom request dispatcher',\n        )\n      }\n\n      const url = extractUrl(input)\n\n      if (url.hostname && isUnicastIp(url.hostname) === false) {\n        throw new FetchRequestError(\n          asRequest(input, init),\n          400,\n          'Hostname is a non-unicast address',\n        )\n      }\n\n      if (SUPPORTS_REQUEST_INIT_DISPATCHER) {\n        // @ts-expect-error non-standard option\n        const request = new Request(input, { ...init, dispatcher })\n        return fetch.call(this, request)\n      } else {\n        // @ts-expect-error non-standard option\n        return fetch.call(this, input, { ...init, dispatcher })\n      }\n    }\n  } else {\n    return async function (input, init): Promise<Response> {\n      if (init?.dispatcher) {\n        throw new FetchRequestError(\n          asRequest(input, init),\n          500,\n          'SSRF protection cannot be used with a custom request dispatcher',\n        )\n      }\n\n      const url = extractUrl(input)\n\n      if (!url.hostname) {\n        return fetch.call(this, input, init)\n      }\n\n      switch (isUnicastIp(url.hostname)) {\n        case true: {\n          // hostname is a unicast address, safe to proceed.\n          return fetch.call(this, input, init)\n        }\n\n        case false: {\n          throw new FetchRequestError(\n            asRequest(input, init),\n            400,\n            'Hostname is a non-unicast address',\n          )\n        }\n\n        case undefined: {\n          // hostname is a domain name, let's create a new dispatcher that\n          // will 1) use the unicastLookup function to resolve the hostname\n          // and 2) allow us to check that the lookup function was indeed\n          // called.\n\n          let didLookup = false\n          const dispatcher = new Client(url.origin, {\n            // Do *not* enable H2 here, as it will cause an error (the\n            // client will terminate the connection before the response is\n            // consumed).\n            // https://github.com/nodejs/undici/issues/3671\n            connect: {\n              keepAlive: false, // Client will be used once\n              lookup(...args) {\n                didLookup = true\n                unicastLookup(...args)\n              },\n            },\n          })\n\n          try {\n            const headers = new Headers(init?.headers)\n            headers.set('connection', 'close') // Proactively close the connection\n\n            const response = await fetch.call(this, input, {\n              ...init,\n              headers,\n              // @ts-expect-error non-standard option\n              dispatcher,\n            })\n\n            if (!didLookup) {\n              // We need to ensure that the body is discarded. We can either\n              // consume the whole body (for await loop) in order to keep the\n              // socket alive, or cancel the request. Since we sent \"connection:\n              // close\", there is no point in consuming the whole response\n              // (which would cause un-necessary bandwidth).\n              //\n              // https://undici.nodejs.org/#/?id=garbage-collection\n              await response.body?.cancel()\n\n              // If you encounter this error, either upgrade to Node.js >=21 or\n              // make sure that the dispatcher passed through the requestInit\n              // object ends up being used to make the request.\n\n              // eslint-disable-next-line no-unsafe-finally\n              throw new FetchRequestError(\n                asRequest(input, init),\n                500,\n                'Unable to enforce SSRF protection',\n              )\n            }\n\n            return response\n          } finally {\n            // Free resources (we cannot await here since the response was not\n            // consumed yet).\n            void dispatcher.close().catch((err) => {\n              // No biggie, but let's still log it\n              console.warn('Failed to close dispatcher', err)\n            })\n          }\n        }\n      }\n    }\n  }\n}\n\nexport function unicastLookup(\n  hostname: string,\n  options: dns.LookupOptions,\n  callback: Parameters<LookupFunction>[2],\n) {\n  if (isLocalHostname(hostname)) {\n    callback(new Error('Hostname is not a public domain'), [])\n    return\n  }\n\n  dns.lookup(hostname, options, (err, address, family) => {\n    if (err) {\n      callback(err, address, family)\n    } else {\n      const ips = Array.isArray(address)\n        ? address.map(parseLookupAddress)\n        : [parseLookupAddress({ address, family })]\n\n      if (ips.some(isNotUnicast)) {\n        callback(\n          new Error('Hostname resolved to non-unicast address'),\n          address,\n          family,\n        )\n      } else {\n        callback(null, address, family)\n      }\n    }\n  })\n}\n\n/**\n * @param hostname - a syntactically valid hostname\n * @returns whether the hostname is a name typically used for on locale area networks.\n * @note **DO NOT** use for security reasons. Only as heuristic.\n */\nfunction isLocalHostname(hostname: string): boolean {\n  const parts = hostname.split('.')\n  if (parts.length < 2) return true\n\n  const tld = parts.at(-1)!.toLowerCase()\n  return (\n    tld === 'test' ||\n    tld === 'local' ||\n    tld === 'localhost' ||\n    tld === 'invalid' ||\n    tld === 'example'\n  )\n}\n\nfunction isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean {\n  return ip.range() !== 'unicast'\n}\n\nfunction parseLookupAddress({\n  address,\n  family,\n}: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 {\n  const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address)\n\n  if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {\n    return ip.toIPv4Address()\n  } else {\n    return ip\n  }\n}\n"
  },
  {
    "path": "packages/internal/fetch-node/src/util.ts",
    "content": "import ipaddr from 'ipaddr.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nfunction parseIpHostname(\n  hostname: string,\n): ipaddr.IPv4 | ipaddr.IPv6 | undefined {\n  if (IPv4.isIPv4(hostname)) {\n    return IPv4.parse(hostname)\n  }\n\n  if (hostname.startsWith('[') && hostname.endsWith(']')) {\n    return IPv6.parse(hostname.slice(1, -1))\n  }\n\n  return undefined\n}\n\nexport function isUnicastIp(hostname: string): boolean | undefined {\n  const ip = parseIpHostname(hostname)\n  return ip ? ip.range() === 'unicast' : undefined\n}\n"
  },
  {
    "path": "packages/internal/fetch-node/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/internal/fetch-node/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/CHANGELOG.md",
    "content": "# @atproto-labs/handle-resolver\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n\n## 0.3.3\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export `createHandleResolver` util\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/did@0.2.2\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/did@0.2.1\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/did@0.2.0\n  - @atproto-labs/simple-store@0.3.0\n  - @atproto-labs/simple-store-memory@0.1.4\n\n## 0.3.0\n\n### Minor Changes\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `HandleResolverError` instances instead of `TypeError`\n\n### Patch Changes\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export `asResolvedHandle` utility\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `AppViewHandleResolver` to `XrpcHandleResolver`\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `XrpcHandleResolver.from` static method\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4)]:\n  - @atproto-labs/simple-store@0.2.0\n  - @atproto-labs/simple-store-memory@0.1.3\n\n## 0.1.7\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/simple-store-memory@0.1.2\n  - @atproto-labs/simple-store@0.1.2\n  - @atproto/did@0.1.5\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87)]:\n  - @atproto/did@0.1.4\n\n## 0.1.5\n\n### Patch Changes\n\n- [#3046](https://github.com/bluesky-social/atproto/pull/3046) [`a200e5095`](https://github.com/bluesky-social/atproto/commit/a200e50951d297c3f9670e96027262196bc29b0b) Thanks [@sgarciac](https://github.com/sgarciac)! - This change makes the DoH handle resolver accept a wider range of content types for DoH API calls.\n\n  While there is no agreed upon MIME type for DoH's JSON Schema, this change supports a reasonable\n  set that include those used by major DoH providers such as Google and Cloudflare.\n\n## 0.1.4\n\n### Patch Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use fetch()'s \"cache\" option instead of headers to force caching behavior\n\n- Updated dependencies [[`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2)]:\n  - @atproto/did@0.1.3\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]:\n  - @atproto/did@0.1.2\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Updated to use \"AtprotoDid\" utils from @atproto/did\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/did@0.1.1\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use distinct type names to prevent conflicts\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto-labs/simple-store@0.1.1\n  - @atproto-labs/simple-store-memory@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/simple-store-memory@0.1.0\n  - @atproto-labs/simple-store@0.1.0\n  - @atproto/did@0.1.0\n"
  },
  {
    "path": "packages/internal/handle-resolver/README.md",
    "content": "# Universal Handle Resolver implementation for ATPROTO\n\nThis package provides a handle resolver implementation for ATPROTO. It is used\nto resolve handles to their corresponding DID.\n\nThis package is meant to be used in any JavaScript environment that support the\n`fetch()` function. Because APTORO handle resolution requires DNS resolution,\nyou will need to provide your own DNS resolution function when using this\npackage.\n\nThere are two main classes in this package:\n\n- `AtprotoHandleResolver` This implements the official ATPROTO handle resolution\n  algorithm (and requires a DNS resolver).\n- `AppViewHandleResolver` This uses HTTP requests to the Bluesky AppView\n  (bsky.app) to provide handle resolution.\n\n## Usage\n\n### From a front-end app\n\nSince the ATPROTO handle resolution algorithm requires DNS resolution, and the\nbrowser does not provide a built-in DNS resolver, this package offers two\noptions:\n\n- Delegate handle resolution to an AppView (`AppViewHandleResolver`). This is\n  the recommended approach for front-end apps.\n- Use a DNS-over-HTTPS (DoH) server (`DohHandleResolver`). Prefer this method\n  if you don't own an AppView and already have a DoH server that you trust.\n\nUsing an AppView:\n\n> [!CAUTION]\n> Use the Bluesky owned AppView (`https://api.bsky.app/`), or PDS\n> (`https://bsky.social/`), at your own risk. Using these servers in a\n> third-party application might expose your users' data (IP address) to Bluesky.\n> Bluesky might log the data sent to it when your app is resolving handles.\n> Bluesky might also change the API, or terms or use, at any time without\n> notice. Make sure you are compliant with the Bluesky terms of use as well as\n> any laws and regulations that apply to your use case.\n\n```ts\nimport { AppViewHandleResolver } from '@atproto-labs/handle-resolver'\n\nconst resolver = new AppViewHandleResolver({\n  service: 'https://my-app-view.com/',\n})\nconst did = await resolver.resolve('my-handle.bsky.social')\n```\n\nUsing DNS-over-HTTPS (DoH) for DNS resolution:\n\n> [!CAUTION]\n> Using a DoH server that you don't own might expose your users' data to\n> the DoH server provider. The DoH server provider might log the data sent to it\n> by your app, allowing them to track which handles are being resolved by your\n> users. In the browser, it is recommended to use a DoH server that you own and\n> control. Or to implement your own AppView and use the `AppViewHandleResolver`\n> class.\n\n> [!NOTE]\n> Using the `DohHandleResolver` requires a DNS-over-HTTPS server that\n> supports the DNS-over-HTTPS protocol with \"application/dns-json\" responses.\n\n```ts\nimport { DohHandleResolver } from '@atproto-labs/handle-resolver'\n\n// Also works with 'https://cloudflare-dns.com/dns-query'\nconst resolver = new DohHandleResolver('https://dns.google/resolve', {\n  // Optional: Custom fetch function that will be used both for DNS resolution\n  // and well-known resolution.\n  fetch: globalThis.fetch.bind(globalThis),\n})\n\nconst did = await resolver.resolve('my-handle.bsky.social')\n```\n\n### From a Node.js app\n\n> [!NOTE]\n> On a Node.js backend, you will probably want to use the\n> \"@atproto-labs/handle-resolver-node\" package. The example below applies to\n> Node.js code running on a user's machine (e.g. through Electron).\n\n```ts\nimport { AtprotoHandleResolver } from '@atproto-labs/handle-resolver'\nimport { resolveTxt } from 'node:dns/promises'\n\nconst resolver = new AtprotoHandleResolver({\n  // Optional: Custom fetch function (used for well-known resolution)\n  fetch: globalThis.fetch.bind(globalThis),\n\n  resolveTxt: async (domain: string) =>\n    resolveTxt(domain).then((chunks) => chunks.join('')),\n})\n```\n\n### Caching\n\nUsing a default, in-memory cache, in which items expire after 10 minutes:\n\n```ts\nimport {\n  AppViewHandleResolver,\n  CachedHandleResolver,\n  HandleResolver,\n  HandleCache,\n} from '@atproto-labs/handle-resolver'\n\n// See previous examples for creating a resolver\ndeclare const sourceResolver: HandleResolver\n\nconst resolver = new CachedHandleResolver(sourceResolver)\nconst did = await resolver.resolve('my-handle.bsky.social')\nconst did = await resolver.resolve('my-handle.bsky.social') // Result from cache\nconst did = await resolver.resolve('my-handle.bsky.social') // Result from cache\n```\n\nUsing a custom cache:\n\n```ts\nimport {\n  AppViewHandleResolver,\n  CachedHandleResolver,\n  HandleResolver,\n  HandleCache,\n} from '@atproto-labs/handle-resolver'\n\n// See previous examples for creating a resolver\ndeclare const sourceResolver: HandleResolver\n\nconst cache: HandleCache = {\n  set(handle, did): Promise<void> {\n    /* TODO */\n  },\n  get(handle): Promise<undefined | string> {\n    /* TODO */\n  },\n  del(handle): Promise<void> {\n    /* TODO */\n  },\n}\n\nconst resolver = new CachedHandleResolver(sourceResolver, cache)\nconst did = await resolver.resolve('my-handle.bsky.social')\nconst did = await resolver.resolve('my-handle.bsky.social') // Result from cache\nconst did = await resolver.resolve('my-handle.bsky.social') // Result from cache\n```\n"
  },
  {
    "path": "packages/internal/handle-resolver/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/handle-resolver\",\n  \"version\": \"0.3.6\",\n  \"license\": \"MIT\",\n  \"description\": \"Isomorphic ATProto handle to DID resolver\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"handle\",\n    \"identity\",\n    \"browser\",\n    \"node\",\n    \"isomorphic\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/handle-resolver\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto-labs/simple-store-memory\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/atproto-doh-handle-resolver.ts",
    "content": "import {\n  AtprotoHandleResolver,\n  AtprotoHandleResolverOptions,\n} from './atproto-handle-resolver.js'\nimport { HandleResolverError } from './handle-resolver-error.js'\nimport { ResolveTxt } from './internal-resolvers/dns-handle-resolver.js'\nimport { HandleResolver } from './types.js'\n\nexport type AtprotoDohHandleResolverOptions = Omit<\n  AtprotoHandleResolverOptions,\n  'resolveTxt' | 'resolveTxtFallback'\n> & {\n  dohEndpoint: string | URL\n}\n\nexport class AtprotoDohHandleResolver\n  extends AtprotoHandleResolver\n  implements HandleResolver\n{\n  constructor(options: AtprotoDohHandleResolverOptions) {\n    super({\n      ...options,\n      resolveTxt: dohResolveTxtFactory(options),\n      resolveTxtFallback: undefined,\n    })\n  }\n}\n\n/**\n * Resolver for DNS-over-HTTPS (DoH) handles. Only works with servers supporting\n * Google Flavoured \"application/dns-json\" queries.\n *\n * @see {@link https://developers.google.com/speed/public-dns/docs/doh/json}\n * @see {@link https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/}\n * @todo Add support for DoH using application/dns-message (?)\n */\nfunction dohResolveTxtFactory({\n  dohEndpoint,\n  fetch = globalThis.fetch,\n}: AtprotoDohHandleResolverOptions): ResolveTxt {\n  return async (hostname) => {\n    const url = new URL(dohEndpoint)\n    url.searchParams.set('type', 'TXT')\n    url.searchParams.set('name', hostname)\n\n    const response = await fetch(url, {\n      method: 'GET',\n      headers: { accept: 'application/dns-json' },\n      redirect: 'follow',\n    })\n    try {\n      const contentType = response.headers.get('content-type')?.trim()\n      if (!response.ok) {\n        const message = contentType?.startsWith('text/plain')\n          ? await response.text()\n          : `Failed to resolve ${hostname}`\n        throw new HandleResolverError(message)\n      } else if (contentType?.match(/application\\/(dns-)?json/i) == null) {\n        throw new HandleResolverError('Unexpected response from DoH server')\n      }\n\n      const result = asResult(await response.json())\n      return result.Answer?.filter(isAnswerTxt).map(extractTxtData) ?? null\n    } finally {\n      // Make sure to always cancel the response body as some engines (Node 👀)\n      // do not do this automatically.\n      // https://undici.nodejs.org/#/?id=garbage-collection\n      if (response.bodyUsed === false) {\n        // Handle rejection asynchronously\n        void response.body?.cancel().catch(onCancelError)\n      }\n    }\n  }\n}\n\nfunction onCancelError(err: unknown) {\n  if (!(err instanceof DOMException) || err.name !== 'AbortError') {\n    console.error('An error occurred while cancelling the response body:', err)\n  }\n}\n\ntype Result = { Status: number; Answer?: Answer[] }\nfunction isResult(result: unknown): result is Result {\n  if (typeof result !== 'object' || result === null) return false\n  if (!('Status' in result) || typeof result.Status !== 'number') return false\n  if ('Answer' in result && !isArrayOf(result.Answer, isAnswer)) return false\n  return true\n}\nfunction asResult(result: unknown): Result {\n  if (isResult(result)) return result\n  throw new HandleResolverError('Invalid DoH response')\n}\n\nfunction isArrayOf<T>(\n  value: unknown,\n  predicate: (v: unknown) => v is T,\n): value is T[] {\n  return Array.isArray(value) && value.every(predicate)\n}\n\ntype Answer = { name: string; type: number; data: string; TTL: number }\nfunction isAnswer(answer: unknown): answer is Answer {\n  return (\n    typeof answer === 'object' &&\n    answer !== null &&\n    'name' in answer &&\n    typeof answer.name === 'string' &&\n    'type' in answer &&\n    typeof answer.type === 'number' &&\n    'data' in answer &&\n    typeof answer.data === 'string' &&\n    'TTL' in answer &&\n    typeof answer.TTL === 'number'\n  )\n}\n\ntype AnswerTxt = Answer & { type: 16 }\nfunction isAnswerTxt(answer: Answer): answer is AnswerTxt {\n  return answer.type === 16\n}\n\nfunction extractTxtData(answer: AnswerTxt): string {\n  return answer.data.replace(/^\"|\"$/g, '').replace(/\\\\\"/g, '\"')\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/atproto-handle-resolver.ts",
    "content": "import {\n  DnsHandleResolver,\n  ResolveTxt,\n} from './internal-resolvers/dns-handle-resolver.js'\nimport {\n  WellKnownHandleResolver,\n  WellKnownHandleResolverOptions,\n} from './internal-resolvers/well-known-handler-resolver.js'\nimport {\n  HandleResolver,\n  ResolveHandleOptions,\n  ResolvedHandle,\n} from './types.js'\n\nexport type { ResolveTxt }\nexport type AtprotoHandleResolverOptions = WellKnownHandleResolverOptions & {\n  resolveTxt: ResolveTxt\n  resolveTxtFallback?: ResolveTxt\n}\n\nconst noop = () => {}\n\n/**\n * Implementation of the official ATPROTO handle resolution strategy.\n * This implementation relies on two primitives:\n * - HTTP Well-Known URI resolution (requires a `fetch()` implementation)\n * - DNS TXT record resolution (requires a `resolveTxt()` function)\n */\nexport class AtprotoHandleResolver implements HandleResolver {\n  private readonly httpResolver: HandleResolver\n  private readonly dnsResolver: HandleResolver\n  private readonly dnsResolverFallback?: HandleResolver\n\n  constructor(options: AtprotoHandleResolverOptions) {\n    this.httpResolver = new WellKnownHandleResolver(options)\n    this.dnsResolver = new DnsHandleResolver(options.resolveTxt)\n    this.dnsResolverFallback = options.resolveTxtFallback\n      ? new DnsHandleResolver(options.resolveTxtFallback)\n      : undefined\n  }\n\n  async resolve(\n    handle: string,\n    options?: ResolveHandleOptions,\n  ): Promise<ResolvedHandle> {\n    options?.signal?.throwIfAborted()\n\n    const abortController = new AbortController()\n    const { signal } = abortController\n    options?.signal?.addEventListener('abort', () => abortController.abort(), {\n      signal,\n    })\n\n    const wrappedOptions = { ...options, signal }\n\n    try {\n      const dnsPromise = this.dnsResolver.resolve(handle, wrappedOptions)\n      const httpPromise = this.httpResolver.resolve(handle, wrappedOptions)\n\n      // Prevent uncaught promise rejection\n      httpPromise.catch(noop)\n\n      const dnsRes = await dnsPromise\n      if (dnsRes) return dnsRes\n\n      signal.throwIfAborted()\n\n      const res = await httpPromise\n      if (res) return res\n\n      signal.throwIfAborted()\n\n      return this.dnsResolverFallback?.resolve(handle, wrappedOptions) ?? null\n    } finally {\n      // Cancel pending requests, and remove \"abort\" listener on incoming signal\n      abortController.abort()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/cached-handle-resolver.ts",
    "content": "import { CachedGetter, SimpleStore } from '@atproto-labs/simple-store'\nimport { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'\nimport {\n  HandleResolver,\n  ResolveHandleOptions,\n  ResolvedHandle,\n} from './types.js'\n\nexport type HandleCache = SimpleStore<string, ResolvedHandle>\n\nexport class CachedHandleResolver implements HandleResolver {\n  private getter: CachedGetter<string, ResolvedHandle>\n\n  constructor(\n    /**\n     * The resolver that will be used to resolve handles.\n     */\n    resolver: HandleResolver,\n    cache: HandleCache = new SimpleStoreMemory<string, ResolvedHandle>({\n      max: 1000,\n      ttl: 10 * 60e3,\n    }),\n  ) {\n    this.getter = new CachedGetter<string, ResolvedHandle>(\n      (handle, options) => resolver.resolve(handle, options),\n      cache,\n    )\n  }\n\n  async resolve(\n    handle: string,\n    options?: ResolveHandleOptions,\n  ): Promise<ResolvedHandle> {\n    return this.getter.get(handle, options)\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/create-handle-resolver.ts",
    "content": "import { CachedHandleResolver, HandleCache } from './cached-handle-resolver.js'\nimport { HandleResolver } from './types.js'\nimport {\n  XrpcHandleResolver,\n  XrpcHandleResolverOptions,\n} from './xrpc-handle-resolver.js'\n\nexport type CreateHandleResolverOptions = {\n  handleResolver: URL | string | HandleResolver\n  handleCache?: HandleCache\n} & Partial<XrpcHandleResolverOptions>\n\nexport function createHandleResolver(\n  options: CreateHandleResolverOptions,\n): HandleResolver {\n  const { handleResolver, handleCache } = options\n\n  if (handleResolver instanceof CachedHandleResolver && !handleCache) {\n    return handleResolver\n  }\n\n  return new CachedHandleResolver(\n    typeof handleResolver === 'string' || handleResolver instanceof URL\n      ? new XrpcHandleResolver(handleResolver, options)\n      : handleResolver,\n    handleCache,\n  )\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/handle-resolver-error.ts",
    "content": "export class HandleResolverError extends Error {\n  name = 'HandleResolverError'\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/index.ts",
    "content": "export * from './handle-resolver-error.js'\nexport * from './types.js'\n\n// Main Handle Resolvers strategies\nexport * from './xrpc-handle-resolver.js'\nexport * from './atproto-doh-handle-resolver.js'\nexport * from './atproto-handle-resolver.js'\n\n// Handle Resolver Caching utility\nexport * from './cached-handle-resolver.js'\n\n// utils\nexport * from './create-handle-resolver.js'\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/internal-resolvers/dns-handle-resolver.ts",
    "content": "import { HandleResolver, ResolvedHandle, isResolvedHandle } from '../types'\n\nconst SUBDOMAIN = '_atproto'\nconst PREFIX = 'did='\n\n/**\n * DNS TXT record resolver. Return `null` if the hostname successfully does not\n * resolve to a valid DID. Throw an error if an unexpected error occurs.\n */\nexport type ResolveTxt = (hostname: string) => Promise<null | string[]>\n\nexport class DnsHandleResolver implements HandleResolver {\n  constructor(protected resolveTxt: ResolveTxt) {}\n\n  async resolve(handle: string): Promise<ResolvedHandle> {\n    const results = await this.resolveTxt.call(null, `${SUBDOMAIN}.${handle}`)\n\n    if (!results) return null\n\n    for (let i = 0; i < results.length; i++) {\n      // If the line does not start with \"did=\", skip it\n      if (!results[i].startsWith(PREFIX)) continue\n\n      // Ensure no other entry starting with \"did=\" follows\n      for (let j = i + 1; j < results.length; j++) {\n        if (results[j].startsWith(PREFIX)) return null\n      }\n\n      // Note: No trimming (to be consistent with spec)\n      const did = results[i].slice(PREFIX.length)\n\n      // Invalid DBS record\n      return isResolvedHandle(did) ? did : null\n    }\n\n    return null\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/internal-resolvers/well-known-handler-resolver.ts",
    "content": "import {\n  HandleResolver,\n  ResolveHandleOptions,\n  ResolvedHandle,\n  isResolvedHandle,\n} from '../types.js'\n\nexport type WellKnownHandleResolverOptions = {\n  /**\n   * Fetch function to use for HTTP requests. Allows customizing the request\n   * behavior, e.g. adding headers, setting a timeout, mocking, etc. The\n   * provided fetch function will be wrapped with a safeFetchWrap function that\n   * adds SSRF protection.\n   *\n   * @default `globalThis.fetch`\n   */\n  fetch?: typeof globalThis.fetch\n}\n\nexport class WellKnownHandleResolver implements HandleResolver {\n  protected readonly fetch: typeof globalThis.fetch\n\n  constructor(options?: WellKnownHandleResolverOptions) {\n    this.fetch = options?.fetch ?? globalThis.fetch\n  }\n\n  public async resolve(\n    handle: string,\n    options?: ResolveHandleOptions,\n  ): Promise<ResolvedHandle> {\n    const url = new URL('/.well-known/atproto-did', `https://${handle}`)\n\n    try {\n      const response = await this.fetch.call(null, url, {\n        cache: options?.noCache ? 'no-cache' : undefined,\n        signal: options?.signal,\n        redirect: 'error',\n      })\n      const text = await response.text()\n      const firstLine = text.split('\\n')[0]!.trim()\n\n      if (isResolvedHandle(firstLine)) return firstLine\n\n      return null\n    } catch (err) {\n      // The the request failed, assume the handle does not resolve to a DID,\n      // unless the failure was due to the signal being aborted.\n      options?.signal?.throwIfAborted()\n\n      return null\n    }\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/types.ts",
    "content": "import { AtprotoDid, isAtprotoDid } from '@atproto/did'\nexport type { AtprotoDid, AtprotoIdentityDidMethods } from '@atproto/did'\n\nexport type ResolveHandleOptions = {\n  signal?: AbortSignal\n  noCache?: boolean\n}\n\n/**\n * @see {@link https://atproto.com/specs/did#blessed-did-methods}\n */\nexport type ResolvedHandle = null | AtprotoDid\n\n/**\n * @see {@link https://atproto.com/specs/did#blessed-did-methods}\n */\nexport function isResolvedHandle(value: unknown): value is ResolvedHandle {\n  return value === null || isAtprotoDid(value)\n}\n\nexport function asResolvedHandle<T>(value: T): null | (T & AtprotoDid) {\n  return isResolvedHandle(value) ? value : null\n}\n\nexport interface HandleResolver {\n  /**\n   * @returns the DID that corresponds to the given handle, or `null` if no DID\n   * is found. `null` should only be returned if no unexpected behavior occurred\n   * during the resolution process.\n   * @throws Error if the resolution method fails due to an unexpected error, or\n   * if the resolution is aborted ({@link ResolveHandleOptions}).\n   */\n  resolve(\n    handle: string,\n    options?: ResolveHandleOptions,\n  ): Promise<ResolvedHandle>\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/src/xrpc-handle-resolver.ts",
    "content": "import { z } from 'zod'\nimport { HandleResolverError } from './handle-resolver-error.js'\nimport {\n  HandleResolver,\n  ResolveHandleOptions,\n  ResolvedHandle,\n  isResolvedHandle,\n} from './types.js'\n\nexport const xrpcErrorSchema = z.object({\n  error: z.string(),\n  message: z.string().optional(),\n})\n\nexport type XrpcHandleResolverOptions = {\n  /**\n   * Fetch function to use for HTTP requests. Allows customizing the request\n   * behavior, e.g. adding headers, setting a timeout, mocking, etc.\n   *\n   * @default globalThis.fetch\n   */\n  fetch?: typeof globalThis.fetch\n}\n\nexport class XrpcHandleResolver implements HandleResolver {\n  /**\n   * URL of the atproto lexicon server. This is the base URL where the\n   * `com.atproto.identity.resolveHandle` XRPC method is located.\n   */\n  protected readonly serviceUrl: URL\n  protected readonly fetch: typeof globalThis.fetch\n\n  constructor(service: URL | string, options?: XrpcHandleResolverOptions) {\n    this.serviceUrl = new URL(service)\n    this.fetch = options?.fetch ?? globalThis.fetch\n  }\n\n  public async resolve(\n    handle: string,\n    options?: ResolveHandleOptions,\n  ): Promise<ResolvedHandle> {\n    const url = new URL(\n      '/xrpc/com.atproto.identity.resolveHandle',\n      this.serviceUrl,\n    )\n    url.searchParams.set('handle', handle)\n\n    const response = await this.fetch.call(null, url, {\n      cache: options?.noCache ? 'no-cache' : undefined,\n      signal: options?.signal,\n      redirect: 'error',\n    })\n    const payload = await response.json()\n\n    // The response should either be\n    // - 400 Bad Request with { error: 'InvalidRequest', message: 'Unable to resolve handle' }\n    // - 200 OK with { did: NonNullable<ResolvedHandle> }\n    // Any other response is considered unexpected behavior an should throw an error.\n\n    if (response.status === 400) {\n      const { error, data } = xrpcErrorSchema.safeParse(payload)\n      if (error) {\n        throw new HandleResolverError(\n          `Invalid response from resolveHandle method: ${error.message}`,\n          { cause: error },\n        )\n      }\n      if (\n        data.error === 'InvalidRequest' &&\n        data.message === 'Unable to resolve handle'\n      ) {\n        return null\n      }\n    }\n\n    if (!response.ok) {\n      throw new HandleResolverError(\n        'Invalid status code from resolveHandle method',\n      )\n    }\n\n    const value: unknown = payload?.did\n\n    if (!isResolvedHandle(value)) {\n      throw new HandleResolverError(\n        'Invalid DID returned from resolveHandle method',\n      )\n    }\n\n    return value\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/CHANGELOG.md",
    "content": "# @atproto-labs/handle-resolver-node\n\n## 0.1.25\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n  - @atproto-labs/handle-resolver@0.3.6\n\n## 0.1.24\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n  - @atproto-labs/handle-resolver@0.3.5\n\n## 0.1.23\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n  - @atproto-labs/handle-resolver@0.3.4\n\n## 0.1.22\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto-labs/handle-resolver@0.3.3\n  - @atproto/did@0.2.2\n\n## 0.1.21\n\n### Patch Changes\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto-labs/fetch-node@0.2.0\n\n## 0.1.20\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/did@0.2.1\n  - @atproto-labs/handle-resolver@0.3.2\n\n## 0.1.19\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/did@0.2.0\n  - @atproto-labs/fetch-node@0.1.10\n  - @atproto-labs/handle-resolver@0.3.1\n\n## 0.1.18\n\n### Patch Changes\n\n- Updated dependencies [[`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47)]:\n  - @atproto-labs/handle-resolver@0.3.0\n\n## 0.1.17\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto-labs/handle-resolver@0.2.0\n\n## 0.1.16\n\n### Patch Changes\n\n- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`36dbd4155`](https://github.com/bluesky-social/atproto/commit/36dbd41551f74052a3f584719a1a7edd86eca201), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4)]:\n  - @atproto-labs/fetch-node@0.1.9\n\n## 0.1.15\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver@0.1.8\n\n## 0.1.14\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/fetch-node@0.1.8\n\n## 0.1.13\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/handle-resolver@0.1.7\n  - @atproto-labs/fetch-node@0.1.7\n  - @atproto/did@0.1.5\n\n## 0.1.12\n\n### Patch Changes\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87)]:\n  - @atproto/did@0.1.4\n  - @atproto-labs/handle-resolver@0.1.6\n\n## 0.1.11\n\n### Patch Changes\n\n- Updated dependencies [[`9c0128193`](https://github.com/bluesky-social/atproto/commit/9c01281931a371304bcfa465005d7363c003bc5f)]:\n  - @atproto-labs/fetch-node@0.1.6\n\n## 0.1.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/fetch-node@0.1.5\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [[`a200e5095`](https://github.com/bluesky-social/atproto/commit/a200e50951d297c3f9670e96027262196bc29b0b)]:\n  - @atproto-labs/handle-resolver@0.1.5\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/fetch-node@0.1.4\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2)]:\n  - @atproto-labs/handle-resolver@0.1.4\n  - @atproto/did@0.1.3\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`80450cbf2`](https://github.com/bluesky-social/atproto/commit/80450cbf2ca27967ee9fe1a5f4bc590b26f1e6b2)]:\n  - @atproto-labs/fetch-node@0.1.3\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`8943c1008`](https://github.com/bluesky-social/atproto/commit/8943c10082702bbc0fc150237c6cc421251afd51)]:\n  - @atproto-labs/fetch-node@0.1.2\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto-labs/fetch-node@0.1.1\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]:\n  - @atproto/did@0.1.2\n  - @atproto-labs/handle-resolver@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto-labs/handle-resolver@0.1.2\n  - @atproto/did@0.1.1\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use distinct type names to prevent conflicts\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto-labs/handle-resolver@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/handle-resolver@0.1.0\n  - @atproto-labs/fetch-node@0.1.0\n  - @atproto/did@0.1.0\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/handle-resolver-node\",\n  \"version\": \"0.1.25\",\n  \"license\": \"MIT\",\n  \"description\": \"Node specific ATProto handle to DID resolver\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"handle\",\n    \"identity\",\n    \"node\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/handle-resolver-node\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch-node\": \"workspace:^\",\n    \"@atproto-labs/handle-resolver\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/src/atproto-handle-resolver-node.ts",
    "content": "import { Fetch, safeFetchWrap } from '@atproto-labs/fetch-node'\nimport {\n  AtprotoHandleResolver,\n  HandleResolver,\n} from '@atproto-labs/handle-resolver'\nimport {\n  nodeResolveTxtDefault,\n  nodeResolveTxtFactory,\n} from './node-resolve-txt-factory.js'\n\nexport type AtprotoHandleResolverNodeOptions = {\n  /**\n   * List of backup nameservers to use in case the primary ones fail. Will\n   * default to no fallback nameservers.\n   */\n  fallbackNameservers?: string[]\n\n  /**\n   * Fetch function to use for HTTP requests. Allows customizing the request\n   * behavior, e.g. adding headers, setting a timeout, mocking, etc. The\n   * provided fetch function will be wrapped with a safeFetchWrap function that\n   * adds SSRF protection.\n   *\n   * @default `globalThis.fetch`\n   */\n  fetch?: Fetch\n}\n\nexport class AtprotoHandleResolverNode\n  extends AtprotoHandleResolver\n  implements HandleResolver\n{\n  constructor({\n    fetch = globalThis.fetch,\n    fallbackNameservers,\n  }: AtprotoHandleResolverNodeOptions = {}) {\n    super({\n      fetch: safeFetchWrap({\n        fetch,\n        timeout: 3000, // 3 seconds\n        ssrfProtection: true,\n        responseMaxSize: 10 * 1048, // DID are max 2048 characters, 10kb for safety\n      }),\n      resolveTxt: nodeResolveTxtDefault,\n      resolveTxtFallback: fallbackNameservers?.length\n        ? nodeResolveTxtFactory(fallbackNameservers)\n        : undefined,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/src/index.ts",
    "content": "export * from './atproto-handle-resolver-node.js'\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/src/node-resolve-txt-factory.ts",
    "content": "import { Resolver, lookup, resolveTxt } from 'node:dns/promises'\nimport { isIP } from 'node:net'\nimport { ResolveTxt } from '@atproto-labs/handle-resolver'\n\nexport const nodeResolveTxtDefault: ResolveTxt = (hostname) =>\n  resolveTxt(hostname).then(groupChunks, handleError)\n\nexport function nodeResolveTxtFactory(nameservers: string[]): ResolveTxt {\n  // Optimization\n  if (!nameservers.length) return async () => null\n\n  // Build the resolver asynchronously (will be awaited on every use)\n  const resolverPromise: Promise<Resolver | null> = Promise.all<string[]>(\n    nameservers.map((nameserver) => {\n      const [domain, port = null] = nameserver.split(':', 2)\n\n      if (port !== null && !/^\\d+$/.test(port)) {\n        throw new TypeError(`Invalid name server \"${nameserver}\"`)\n      }\n\n      return isIP(domain) === 4 || isBracedIPv6(domain)\n        ? [nameserver] // No need to lookup\n        : lookup(domain, { all: true }).then(\n            (r) => r.map((a) => appendPort(a.address, port)),\n            // Let's just ignore failed nameservers resolution\n            (_err) => [],\n          )\n    }),\n  ).then((results) => {\n    const backupIps = results.flat(1)\n    // No resolver if no valid IP\n    if (!backupIps.length) return null\n\n    const resolver = new Resolver()\n    resolver.setServers(backupIps)\n    return resolver\n  })\n\n  // Avoid uncaught promise rejection\n  void resolverPromise.catch(() => {\n    // Should never happen though...\n  })\n\n  return async (hostname) => {\n    const resolver = await resolverPromise\n    return resolver\n      ? resolver.resolveTxt(hostname).then(groupChunks, handleError)\n      : null\n  }\n}\n\nfunction isBracedIPv6(address: string): boolean {\n  return (\n    address.startsWith('[') &&\n    address.endsWith(']') &&\n    isIP(address.slice(1, -1)) === 6\n  )\n}\n\nfunction groupChunks(results: string[][]): string[] {\n  return results.map((chunks) => chunks.join(''))\n}\n\nfunction handleError(err: unknown) {\n  // Invalid argument type (e.g. hostname is a number)\n  if (err instanceof TypeError) throw err\n\n  // If the hostname does not resolve, return null\n  if (err instanceof Error) {\n    if (err['code'] === 'ENOTFOUND') return null\n\n    // Hostname is not a valid domain name\n    if (err['code'] === 'EBADNAME') throw err\n\n    // DNS server unreachable\n    // if (err['code'] === 'ETIMEOUT') throw err\n  }\n\n  // Historically, errors were not thrown here. A \"null\" value indicates to the\n  // AtprotoHandleResolver that it should try the fallback resolver.\n\n  // @TODO We might want to re-visit this to only apply when an unexpected error\n  // occurs (by throwing here). For now, let's keep the same behavior as before.\n\n  // throw err\n\n  return null\n}\n\nfunction appendPort(address: string, port: string | null): string {\n  switch (isIP(address)) {\n    case 4:\n      return port ? `${address}:${port}` : address\n    case 6:\n      return port ? `[${address}]:${port}` : `[${address}]`\n    default:\n      throw new TypeError(`Invalid IP address \"${address}\"`)\n  }\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/internal/handle-resolver-node/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/CHANGELOG.md",
    "content": "# @atproto-labs/identity-resolver\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.2.6\n  - @atproto-labs/handle-resolver@0.3.6\n\n## 0.3.5\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Re-export types internally used by `IdentityInfo`\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.2.5\n  - @atproto-labs/handle-resolver@0.3.5\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.2.4\n  - @atproto-labs/handle-resolver@0.3.4\n\n## 0.3.3\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `createIdentityResolver` util\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto-labs/handle-resolver@0.3.3\n  - @atproto-labs/did-resolver@0.2.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.2.2\n  - @atproto-labs/handle-resolver@0.3.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.2.1\n  - @atproto-labs/handle-resolver@0.3.1\n\n## 0.3.0\n\n### Minor Changes\n\n- [#3982](https://github.com/bluesky-social/atproto/pull/3982) [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Identity resolver's `resolve()` method returns value consistent with `com.atproto.identity.resolveIdentity`\n\n- [#3982](https://github.com/bluesky-social/atproto/pull/3982) [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `IdentityResolver` is now an interface. The `IdentityResolverProto` class is the default implementation for the `IdentityResolver` interface.\n\n### Patch Changes\n\n- [#3982](https://github.com/bluesky-social/atproto/pull/3982) [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export `HANDLE_INVALID` constant\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return the whole did document when resolving an identity\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Reject did documents containing invalid `alsoKnownAs` ATProto handles\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `IdentityResolverError` instances instead of `TypeError`\n\n### Patch Changes\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Perform a bi-directional check when resolving identity from did\n\n- [#3977](https://github.com/bluesky-social/atproto/pull/3977) [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow non-normalized handles in did documents\n\n- Updated dependencies [[`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47)]:\n  - @atproto-labs/handle-resolver@0.3.0\n\n## 0.1.19\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto-labs/handle-resolver@0.2.0\n  - @atproto-labs/did-resolver@0.2.0\n\n## 0.1.18\n\n### Patch Changes\n\n- [#3933](https://github.com/bluesky-social/atproto/pull/3933) [`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return atproto handle in identity resolution result\n\n## 0.1.17\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.13\n\n## 0.1.16\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.12\n  - @atproto-labs/handle-resolver@0.1.8\n\n## 0.1.15\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n\n## 0.1.14\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/syntax@0.3.4\n  - @atproto-labs/did-resolver@0.1.11\n\n## 0.1.13\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/syntax@0.3.3\n\n## 0.1.12\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/handle-resolver@0.1.7\n  - @atproto-labs/did-resolver@0.1.10\n  - @atproto/syntax@0.3.2\n\n## 0.1.11\n\n### Patch Changes\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87)]:\n  - @atproto-labs/did-resolver@0.1.9\n  - @atproto-labs/handle-resolver@0.1.6\n\n## 0.1.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.8\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto-labs/did-resolver@0.1.7\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`a200e5095`](https://github.com/bluesky-social/atproto/commit/a200e50951d297c3f9670e96027262196bc29b0b)]:\n  - @atproto-labs/handle-resolver@0.1.5\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.6\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/syntax@0.3.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2)]:\n  - @atproto-labs/did-resolver@0.1.5\n  - @atproto-labs/handle-resolver@0.1.4\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.3\n  - @atproto-labs/handle-resolver@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose getDocumentFromDid and getDocumentFromHandle as public methods on IdentityResolver\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto-labs/handle-resolver@0.1.2\n  - @atproto-labs/did-resolver@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use distinct type names to prevent conflicts\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto-labs/handle-resolver@0.1.1\n  - @atproto-labs/did-resolver@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/handle-resolver@0.1.0\n  - @atproto-labs/did-resolver@0.1.0\n"
  },
  {
    "path": "packages/internal/identity-resolver/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/identity-resolver\",\n  \"version\": \"0.3.6\",\n  \"license\": \"MIT\",\n  \"description\": \"A library resolving ATPROTO identities\",\n  \"keywords\": [\n    \"atproto\",\n    \"identity\",\n    \"isomorphic\",\n    \"resolver\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/identity-resolver\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/did-resolver\": \"workspace:^\",\n    \"@atproto-labs/handle-resolver\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/atproto-identity-resolver.ts",
    "content": "import {\n  AtprotoDid,\n  AtprotoIdentityDidMethods,\n  DidDocument,\n  DidResolver,\n  ResolveDidOptions,\n  isAtprotoDid,\n} from '@atproto-labs/did-resolver'\nimport {\n  HandleResolver,\n  ResolveHandleOptions,\n} from '@atproto-labs/handle-resolver'\nimport { HANDLE_INVALID } from './constants.js'\nimport { IdentityResolverError } from './identity-resolver-error.js'\nimport {\n  IdentityInfo,\n  IdentityResolver,\n  ResolveIdentityOptions,\n} from './identity-resolver.js'\nimport { asNormalizedHandle, extractNormalizedHandle } from './util.js'\n\n// @TODO Move this to its own package as soon as we have a distinct\n// implementation based on XRPC calls to the\n// \"com.atproto.identity.resolveIdentity\" method.\n\n/**\n * Implementation of the official ATPROTO identity resolution strategy.\n * This implementation relies on two primitives:\n * - DID resolution (using the `DidResolver` interface)\n * - Handle resolution (using the `HandleResolver` interface)\n */\nexport class AtprotoIdentityResolver implements IdentityResolver {\n  constructor(\n    protected readonly didResolver: DidResolver<AtprotoIdentityDidMethods>,\n    protected readonly handleResolver: HandleResolver,\n  ) {}\n\n  public async resolve(\n    input: string,\n    options?: ResolveIdentityOptions,\n  ): Promise<IdentityInfo> {\n    return isAtprotoDid(input)\n      ? this.resolveFromDid(input, options)\n      : this.resolveFromHandle(input, options)\n  }\n\n  public async resolveFromDid(\n    did: AtprotoDid,\n    options?: ResolveDidOptions,\n  ): Promise<IdentityInfo> {\n    const document = await this.getDocumentFromDid(did, options)\n\n    options?.signal?.throwIfAborted()\n\n    // We will only return the document's handle alias if it resolves to the\n    // same DID as the input.\n    const handle = extractNormalizedHandle(document)\n    const resolvedDid = handle\n      ? await this.handleResolver\n          .resolve(handle, options)\n          .catch(() => undefined) // Ignore errors (temporarily unavailable)\n      : undefined\n\n    return {\n      did: document.id,\n      didDoc: document,\n      handle: handle && resolvedDid === did ? handle : HANDLE_INVALID,\n    }\n  }\n\n  public async resolveFromHandle(\n    handle: string,\n    options?: ResolveHandleOptions,\n  ): Promise<IdentityInfo> {\n    const document = await this.getDocumentFromHandle(handle, options)\n\n    // @NOTE bi-directional resolution enforced in getDocumentFromHandle()\n\n    return {\n      did: document.id,\n      didDoc: document,\n      handle: extractNormalizedHandle(document) || HANDLE_INVALID,\n    }\n  }\n\n  public async getDocumentFromDid(\n    did: AtprotoDid,\n    options?: ResolveDidOptions,\n  ): Promise<DidDocument<AtprotoIdentityDidMethods>> {\n    return this.didResolver.resolve(did, options)\n  }\n\n  public async getDocumentFromHandle(\n    input: string,\n    options?: ResolveHandleOptions,\n  ): Promise<DidDocument<AtprotoIdentityDidMethods>> {\n    const handle = asNormalizedHandle(input)\n    if (!handle) {\n      throw new IdentityResolverError(`Invalid handle \"${input}\" provided.`)\n    }\n\n    const did = await this.handleResolver.resolve(handle, options)\n\n    if (!did) {\n      throw new IdentityResolverError(\n        `Handle \"${handle}\" does not resolve to a DID`,\n      )\n    }\n\n    options?.signal?.throwIfAborted()\n\n    // Note: Not using \"return this.resolveDid(did, options)\" to make the extra\n    // check for the handle in the DID document:\n\n    const document = await this.didResolver.resolve(did, options)\n\n    // Enforce bi-directional resolution\n    if (handle !== extractNormalizedHandle(document)) {\n      throw new IdentityResolverError(\n        `Did document for \"${did}\" does not include the handle \"${handle}\"`,\n      )\n    }\n\n    return document\n  }\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/constants.ts",
    "content": "export const HANDLE_INVALID = 'handle.invalid'\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/create-identity-resolver.ts",
    "content": "import {\n  CreateDidResolverOptions,\n  createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport {\n  CreateHandleResolverOptions,\n  createHandleResolver,\n} from '@atproto-labs/handle-resolver'\nimport { AtprotoIdentityResolver } from './atproto-identity-resolver.js'\nimport { IdentityResolver } from './identity-resolver.js'\n\nexport type CreateIdentityResolverOptions = {\n  identityResolver?: IdentityResolver\n} & Partial<CreateDidResolverOptions & CreateHandleResolverOptions>\n\nexport function createIdentityResolver(\n  options: CreateIdentityResolverOptions,\n): IdentityResolver {\n  if ('identityResolver' in options && options.identityResolver != null) {\n    return options.identityResolver\n  }\n\n  if ('handleResolver' in options && options.handleResolver != null) {\n    const didResolver = createDidResolver(options)\n    const handleResolver = createHandleResolver(\n      options as typeof options & {\n        handleResolver: NonNullable<(typeof options)['handleResolver']>\n      },\n    )\n    return new AtprotoIdentityResolver(didResolver, handleResolver)\n  }\n\n  throw new TypeError('identityResolver or handleResolver option is required')\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/identity-resolver-error.ts",
    "content": "export class IdentityResolverError extends Error {\n  name = 'IdentityResolverError'\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/identity-resolver.ts",
    "content": "import { AtprotoDid, AtprotoDidDocument } from '@atproto-labs/did-resolver'\nimport { HANDLE_INVALID } from './constants'\n\nexport type { AtprotoDid, AtprotoDidDocument }\n\n// Consistent with `com.atproto.identity.defs#identityInfo` returned by\n// `com.atproto.identity.resolveIdentity` endpoint.\nexport type IdentityInfo = {\n  did: AtprotoDid\n  didDoc: AtprotoDidDocument\n\n  /**\n   * Will be 'handle.invalid' if the handle does not resolve to the\n   * same DID as the input, or if the handle is not present in the DID\n   * document.\n   */\n  handle: typeof HANDLE_INVALID | string\n}\n\nexport type ResolveIdentityOptions = {\n  signal?: AbortSignal\n  noCache?: boolean\n}\n\nexport interface IdentityResolver {\n  resolve(\n    identifier: string,\n    options?: ResolveIdentityOptions,\n  ): Promise<IdentityInfo>\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/index.ts",
    "content": "export * from './atproto-identity-resolver.js'\nexport * from './constants.js'\nexport * from './create-identity-resolver.js'\nexport * from './identity-resolver-error.js'\nexport * from './identity-resolver.js'\nexport * from './util.js'\n"
  },
  {
    "path": "packages/internal/identity-resolver/src/util.ts",
    "content": "import {\n  AtprotoIdentityDidMethods,\n  DidDocument,\n} from '@atproto-labs/did-resolver'\n\n/**\n * Extract the raw, un-validated, Atproto handle from a DID document.\n */\nexport function extractAtprotoHandle(\n  document: DidDocument<AtprotoIdentityDidMethods>,\n): string | undefined {\n  if (document.alsoKnownAs) {\n    for (const h of document.alsoKnownAs) {\n      if (h.startsWith('at://')) {\n        // strip off \"at://\" prefix\n        return h.slice(5)\n      }\n    }\n  }\n  return undefined\n}\n\n/**\n * Extracts a validated, normalized Atproto handle from a DID document.\n */\nexport function extractNormalizedHandle(\n  document: DidDocument<AtprotoIdentityDidMethods>,\n): string | undefined {\n  const handle = extractAtprotoHandle(document)\n  if (!handle) return undefined\n  return asNormalizedHandle(handle)\n}\n\nexport function asNormalizedHandle(input: string): string | undefined {\n  const handle = normalizeHandle(input)\n  return isValidHandle(handle) ? handle : undefined\n}\n\nexport function normalizeHandle(handle: string): string {\n  return handle.toLowerCase()\n}\n\nexport function isValidHandle(handle: string): boolean {\n  return (\n    handle.length > 0 &&\n    handle.length < 254 &&\n    /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(\n      handle,\n    )\n  )\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/internal/identity-resolver/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/pipe/CHANGELOG.md",
    "content": "# @atproto-labs/pipe\n\n## 0.1.1\n\n### Patch Changes\n\n- [#3821](https://github.com/bluesky-social/atproto/pull/3821) [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow first pipeline function to have more than one argument\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n"
  },
  {
    "path": "packages/internal/pipe/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/pipe\",\n  \"version\": \"0.1.1\",\n  \"license\": \"MIT\",\n  \"description\": \"Library for combining multiple functions into a single function.\",\n  \"keywords\": [\n    \"atproto\",\n    \"transformer\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/pipe\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/pipe/src/index.ts",
    "content": "export { pipe, pipeTwo } from './pipe.js'\nexport type * from './type.js'\n"
  },
  {
    "path": "packages/internal/pipe/src/pipe.ts",
    "content": "import { Fn, Transformer } from './type.js'\n\ntype PipelineArgs<T> = T extends [\n  Fn<infer A extends readonly unknown[], any>,\n  ...any[],\n]\n  ? A\n  : never\n\ntype PipelineOutput<T> = T extends [...any[], Fn<any, infer O>] ? O : never\n\ntype PipelineRecursive<\n  F extends readonly Transformer<any>[],\n  Acc extends any[],\n> = F extends readonly [Transformer<infer I, infer O>]\n  ? [...Acc, Transformer<I, O>]\n  : F extends readonly [Transformer<infer A, any>, ...infer Tail]\n    ? Tail extends readonly [Transformer<infer B, any>, ...any[]]\n      ? PipelineRecursive<Tail, [...Acc, Transformer<A, B>]>\n      : never\n    : never\n\ntype Pipeline<F extends readonly [Fn<any, any>, ...Transformer<any>[]]> =\n  F extends readonly [Fn<infer A, infer O>]\n    ? [Fn<A, O>]\n    : F extends readonly [Fn<infer A, any>, ...infer Tail]\n      ? Tail extends readonly [Transformer<infer B, any>, ...any[]]\n        ? PipelineRecursive<Tail, [Fn<A, B>]>\n        : never\n      : never\n\n/**\n * This utility function allows to properly type a pipeline of transformers.\n *\n * @example\n * ```ts\n * // Will be typed as \"(input: string) => Promise<number>\"\n * const parse = pipe(\n *   async (input: string) => JSON.parse(input),\n *   async (input: unknown) => {\n *     if (typeof input === 'number') return input\n *     throw new TypeError('Invalid input')\n *   },\n *   (input: number) => input * 2,\n * )\n * ```\n */\nexport function pipe<T extends readonly [Fn<any, any>, ...Transformer<any>[]]>(\n  ...pipeline: Pipeline<T> extends T ? T : Pipeline<T>\n) {\n  return pipeline.reduce(pipeTwo) as (\n    ...args: PipelineArgs<T>\n  ) => Promise<PipelineOutput<T>>\n}\n\nexport function pipeTwo<A extends readonly unknown[], O, X = unknown>(\n  first: Fn<A, X>,\n  second: Transformer<X, O>,\n): (...args: A) => Promise<O> {\n  return async (...args) => second(await first(...args))\n}\n"
  },
  {
    "path": "packages/internal/pipe/src/type.ts",
    "content": "/**\n * Generic, potentially async, function. Parametrized for convenience reasons.\n */\nexport type Fn<A extends readonly unknown[], O> = (\n  ...args: A\n) => O | PromiseLike<O>\n\n/**\n * Single input, single output, potentially async transformer function.\n */\nexport type Transformer<I, O = I> = (...args: [I]) => O | PromiseLike<O>\n"
  },
  {
    "path": "packages/internal/pipe/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/internal/pipe/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/rollup-plugin-bundle-manifest/CHANGELOG.md",
    "content": "# @atproto-labs/rollup-plugin-bundle-manifest\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export plugin as named export\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing of plugin\n\n## 0.1.1\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n"
  },
  {
    "path": "packages/internal/rollup-plugin-bundle-manifest/README.md",
    "content": "# @atproto-labs/rollup-plugin-bundle-manifest\n\nThis Rollup plugin allows to generate a (JSON) manifest containing the output\nfiles of a Rollup build. The manifest will look as follows:\n\n```json\n{\n  \"main.js\": {\n    \"type\": \"chunk\",\n    \"mime\": \"application/javascript\",\n    \"dynamicImports\": [],\n    \"isDynamicEntry\": false,\n    \"isEntry\": true,\n    \"isImplicitEntry\": false,\n    \"name\": \"main\",\n    \"sha256\": \"<sha256-hash>\",\n    \"data\": \"<base64-encoded-contents>\"\n  },\n  \"main.js.map\": {\n    \"type\": \"asset\",\n    \"mime\": \"application/json\",\n    \"sha256\": \"<sha256-hash>\",\n    \"data\": \"<base64-encoded-contents>\"\n  },\n  \"main.css\": {\n    \"type\": \"asset\",\n    \"mime\": \"text/css\",\n    \"sha256\": \"<sha256-hash>\",\n    \"data\": \"<base64-encoded-contents>\"\n  }\n  // ... more entries as needed\n}\n```\n\nThis manifest will typically be useful for a backend service that serves the\nfrontend assets, as it can be used to determine the correct `Content-Type` and\nand file integrity (via the SHA-256 hash), without having to read the files\nthemselves.\n\n## Usage\n\n```js\n// rollup.config.js\n\nimport bundleManifest from '@atproto-labs/rollup-plugin-bundle-manifest'\n\nexport default {\n  input: 'src/index.js',\n  output: {\n    dir: 'dist',\n    format: 'es',\n  },\n  plugins: [\n    bundleManifest({\n      name: 'bundle-manifest.json',\n\n      // Optional: should the asset data be embedded (as base64 string) in the manifest?\n      data: false,\n    }),\n  ],\n}\n```\n\n## Options\n\n- `name` (string): The name of the manifest file. Defaults to `bundle-manifest.json`.\n- `data` (boolean): Whether to embed the asset data in the manifest. Defaults to `false`.\n\n## Example\n\n```js\nconst assetManifest = require('./dist/bundle-manifest.json')\n\nconst app = express()\n\napp.use((req, res, next) => {\n  const asset = assetManifest[req.path.slice(1)]\n  if (!asset) return next()\n\n  res.setHeader('Content-Type', asset.mime)\n  res.setHeader('Content-Length', asset.data.length)\n\n  res.end(Buffer.from(asset.data, 'base64'))\n})\n\napp.use((req, res, next) => {\n  res.setHeader(\n    'Content-Security-Policy',\n    buildCSP(assetManifest), // Not provided here\n  )\n\n  // Serve the index.html file\n  res.sendFile('index.html')\n})\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/internal/rollup-plugin-bundle-manifest/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/rollup-plugin-bundle-manifest\",\n  \"version\": \"0.2.0\",\n  \"license\": \"MIT\",\n  \"description\": \"Library for generating a manifest of bundled files from a Rollup build\",\n  \"keywords\": [\n    \"atproto\",\n    \"rollup\",\n    \"manifest\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/rollup-plugin-bundle-manifest\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"dependencies\": {\n    \"mime\": \"^3.0.0\"\n  },\n  \"peerDependencies\": {\n    \"rollup\": \"^4.0.0\"\n  },\n  \"devDependencies\": {\n    \"rollup\": \"^4.10.0\",\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/rollup-plugin-bundle-manifest/src/index.ts",
    "content": "import { createHash } from 'node:crypto'\nimport { extname } from 'node:path'\nimport mime from 'mime'\nimport type { Plugin } from 'rollup'\n\ntype AssetItem = {\n  type: 'asset'\n  mime?: string\n  sha256: string\n  data?: string\n}\n\ntype ChunkItem = {\n  type: 'chunk'\n  mime: string\n  sha256: string\n  dynamicImports: string[]\n  isDynamicEntry: boolean\n  isEntry: boolean\n  isImplicitEntry: boolean\n  name: string\n  data?: string\n}\n\nexport type ManifestItem = AssetItem | ChunkItem\n\nexport type Manifest = Record<string, ManifestItem>\n\nexport function bundleManifest({\n  name = 'bundle-manifest.json',\n  data = false,\n}: {\n  name?: string\n  data?: boolean\n} = {}): Plugin<never> {\n  return {\n    name: 'bundle-manifest',\n    generateBundle(outputOptions, bundle) {\n      const manifest: Manifest = {}\n\n      for (const [fileName, chunk] of Object.entries(bundle)) {\n        if (chunk.type === 'asset') {\n          manifest[fileName] = {\n            type: chunk.type,\n            data: data\n              ? Buffer.from(chunk.source).toString('base64')\n              : undefined,\n            mime: mime.getType(extname(fileName)) || undefined,\n            sha256: createHash('sha256').update(chunk.source).digest('base64'),\n          }\n        }\n\n        if (chunk.type === 'chunk') {\n          manifest[fileName] = {\n            type: chunk.type,\n            data: data ? Buffer.from(chunk.code).toString('base64') : undefined,\n            mime: 'application/javascript',\n            sha256: createHash('sha256').update(chunk.code).digest('base64'),\n            dynamicImports: chunk.dynamicImports,\n            isDynamicEntry: chunk.isDynamicEntry,\n            isEntry: chunk.isEntry,\n            isImplicitEntry: chunk.isImplicitEntry,\n            name: chunk.name,\n          }\n        }\n      }\n\n      this.emitFile({\n        type: 'asset',\n        fileName: name,\n        source: JSON.stringify(manifest, null, 2),\n      })\n    },\n  }\n}\n"
  },
  {
    "path": "packages/internal/rollup-plugin-bundle-manifest/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/internal/rollup-plugin-bundle-manifest/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/simple-store/CHANGELOG.md",
    "content": "# @atproto-labs/simple-store\n\n## 0.3.0\n\n### Minor Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `Key` type to be anything (except nullable values)\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `Getter` function's option argument to `GetterOptions`\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3776](https://github.com/bluesky-social/atproto/pull/3776) [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `bind` method from `CachedGetter`\n\n### Patch Changes\n\n- [#3776](https://github.com/bluesky-social/atproto/pull/3776) [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `context` getter option to `CachedGetter` class\n\n## 0.1.2\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use distinct type names to prevent conflicts\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose reason for deletion\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n"
  },
  {
    "path": "packages/internal/simple-store/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/simple-store\",\n  \"version\": \"0.3.0\",\n  \"license\": \"MIT\",\n  \"description\": \"Simple store interfaces & utilities\",\n  \"keywords\": [\n    \"cache\",\n    \"isomorphic\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/simple-store\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/simple-store/src/cached-getter.ts",
    "content": "import { GetOptions, Key, SimpleStore, Value } from './simple-store.js'\nimport { Awaitable, ContextOptions } from './util.js'\n\nexport type { GetOptions }\nexport type GetCachedOptions<C = void> = ContextOptions<C> & {\n  signal?: AbortSignal\n\n  /**\n   * Do not use the cache to get the value. Always get a new value from the\n   * getter function.\n   *\n   * @default false\n   */\n  noCache?: boolean\n\n  /**\n   * When getting a value from the cache, allow the value to be returned even if\n   * it is stale.\n   *\n   * Has no effect if the `isStale` option was not provided to the CachedGetter.\n   *\n   * @default true // If the CachedGetter has an isStale option\n   * @default false // If no isStale option was provided to the CachedGetter\n   */\n  allowStale?: boolean\n}\n\nexport type GetterOptions<C = void> = {\n  context: C extends void ? undefined : C\n  noCache: boolean\n  signal?: AbortSignal\n}\n\nexport type Getter<K extends Key, V extends Value, C = void> = (\n  key: K,\n  options: GetterOptions<C>,\n  storedValue: undefined | V,\n) => Awaitable<V>\n\nexport type CachedGetterOptions<K extends Key, V extends Value> = {\n  isStale?: (key: K, value: V) => boolean | PromiseLike<boolean>\n  onStoreError?: (err: unknown, key: K, value: V) => void | PromiseLike<void>\n  deleteOnError?: (\n    err: unknown,\n    key: K,\n    value: V,\n  ) => boolean | PromiseLike<boolean>\n}\n\ntype PendingItem<V> = Promise<{ value: V; isFresh: boolean }>\n\nconst returnTrue = () => true\nconst returnFalse = () => false\n\n/**\n * Wrapper utility that uses a store to speed up the retrieval of values from an\n * (expensive) getter function.\n */\nexport class CachedGetter<\n  K extends Key = string,\n  V extends Value = Value,\n  C = void,\n> {\n  private readonly pending = new Map<K, PendingItem<V>>()\n\n  constructor(\n    readonly getter: Getter<K, V, C>,\n    readonly store: SimpleStore<K, V>,\n    readonly options: CachedGetterOptions<K, V> = {},\n  ) {}\n\n  async get(\n    key: C extends void ? K : never,\n    options?: GetCachedOptions<C>,\n  ): Promise<V>\n  async get(\n    key: C extends void ? never : K,\n    options: GetCachedOptions<C>,\n  ): Promise<V>\n  async get(\n    key: K,\n    {\n      signal,\n      context,\n      allowStale = false,\n      noCache = false,\n    } = {} as GetCachedOptions<C>,\n  ): Promise<V> {\n    signal?.throwIfAborted()\n\n    const { isStale, deleteOnError } = this.options\n\n    const allowStored: (value: V) => Awaitable<boolean> = noCache\n      ? returnFalse // Never allow stored values to be returned\n      : allowStale || isStale == null\n        ? returnTrue // Always allow stored values to be returned\n        : async (value: V) => !(await isStale(key, value))\n\n    // As long as concurrent requests are made for the same key, only one\n    // request will be made to the getStored & getter functions at a time. This\n    // works because there is no async operation between the while() loop and\n    // the pending.set() call below. Because of the single threaded nature of\n    // JavaScript, the pending item will be set before the next iteration of the\n    // while loop of any concurrent request.\n    let previousExecutionFlow: undefined | PendingItem<V>\n    while ((previousExecutionFlow = this.pending.get(key))) {\n      try {\n        // If a concurrent request is already in progress, wait for it to finish\n        const { isFresh, value } = await previousExecutionFlow\n\n        // Use the concurrent request's result if it is fresh\n        if (isFresh) return value\n        // Use the concurrent request's result if not fresh (loaded from the\n        // store), and matches the conditions for using a stored value.\n        if (await allowStored(value)) return value\n      } catch {\n        // Ignore errors from previous execution flows (they will have been\n        // propagated by that flow).\n      }\n\n      // Break the loop if the signal was aborted\n      signal?.throwIfAborted()\n    }\n\n    const currentExecutionFlow: PendingItem<V> = Promise.resolve()\n      .then(async () => {\n        const storedValue = await this.getStored(key, { signal })\n\n        if (storedValue !== undefined && (await allowStored(storedValue))) {\n          // Use the stored value as return value for the current execution\n          // flow. Notify other concurrent execution flows (that should be\n          // \"stuck\" in the loop before until this promise resolves) that we got\n          // a value, but that it came from the store (isFresh = false).\n          return { isFresh: false, value: storedValue }\n        }\n\n        return Promise.resolve()\n          .then(async () => {\n            const options = { signal, noCache, context } as GetterOptions<C>\n            return this.getter.call(null, key, options, storedValue)\n          })\n          .catch(async (err) => {\n            if (storedValue !== undefined) {\n              try {\n                if (await deleteOnError?.(err, key, storedValue)) {\n                  await this.delStored(key, err)\n                }\n              } catch (error) {\n                throw new AggregateError(\n                  [err, error],\n                  'Error while deleting stored value',\n                )\n              }\n            }\n            throw err\n          })\n          .then(async (value) => {\n            // The value should be stored even is the signal was aborted.\n            await this.setStored(key, value)\n            return { isFresh: true, value }\n          })\n      })\n      .finally(() => {\n        this.pending.delete(key)\n      })\n\n    if (this.pending.has(key)) {\n      // This should never happen. Indeed, there must not be any 'await'\n      // statement between this and the loop iteration check meaning that\n      // this.pending.get returned undefined. It is there to catch bugs that\n      // would occur in future changes to the code.\n      throw new Error('Concurrent request for the same key')\n    }\n\n    this.pending.set(key, currentExecutionFlow)\n\n    const { value } = await currentExecutionFlow\n    return value\n  }\n\n  async getStored(key: K, options?: GetOptions): Promise<V | undefined> {\n    try {\n      return await this.store.get(key, options)\n    } catch (err) {\n      return undefined\n    }\n  }\n\n  async setStored(key: K, value: V): Promise<void> {\n    try {\n      await this.store.set(key, value)\n    } catch (err) {\n      const onStoreError = this.options?.onStoreError\n      await onStoreError?.(err, key, value)\n    }\n  }\n\n  async delStored(key: K, _cause?: unknown): Promise<void> {\n    await this.store.del(key)\n  }\n}\n"
  },
  {
    "path": "packages/internal/simple-store/src/index.ts",
    "content": "export * from './cached-getter.js'\nexport * from './simple-store.js'\nexport * from './util.js'\n"
  },
  {
    "path": "packages/internal/simple-store/src/simple-store.ts",
    "content": "import { Awaitable } from './util.js'\n\nexport type Key = NonNullable<unknown>\nexport type Value = NonNullable<unknown> | null\n\nexport type GetOptions = { signal?: AbortSignal }\n\nexport interface SimpleStore<K extends Key, V extends Value> {\n  /**\n   * @return undefined if the key is not in the store (which is why Value cannot contain \"undefined\").\n   */\n  get: (key: K, options?: GetOptions) => Awaitable<undefined | V>\n  set: (key: K, value: V) => Awaitable<void>\n  del: (key: K) => Awaitable<void>\n  clear?: () => Awaitable<void>\n}\n"
  },
  {
    "path": "packages/internal/simple-store/src/util.ts",
    "content": "export type Awaitable<V> = V | PromiseLike<V>\n\nexport type ContextOptions<C> = C extends void | undefined\n  ? { context?: undefined }\n  : { context: C }\n"
  },
  {
    "path": "packages/internal/simple-store/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/internal/simple-store/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/simple-store-memory/CHANGELOG.md",
    "content": "# @atproto-labs/simple-store-memory\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto-labs/simple-store@0.3.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4)]:\n  - @atproto-labs/simple-store@0.2.0\n\n## 0.1.2\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/simple-store@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto-labs/simple-store@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/simple-store@0.1.0\n"
  },
  {
    "path": "packages/internal/simple-store-memory/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/simple-store-memory\",\n  \"version\": \"0.1.4\",\n  \"license\": \"MIT\",\n  \"description\": \"Memory based simple-store implementation\",\n  \"keywords\": [\n    \"cache\",\n    \"isomorphic\",\n    \"memory\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/simple-store-memory\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"lru-cache\": \"^10.2.0\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/simple-store-memory/src/index.ts",
    "content": "import { LRUCache } from 'lru-cache'\nimport { Key, SimpleStore, Value } from '@atproto-labs/simple-store'\nimport { roughSizeOfObject } from './util.js'\n\nexport type SimpleStoreMemoryOptions<K extends Key, V extends Value> = {\n  /**\n   * The maximum number of entries in the cache.\n   */\n  max?: number\n\n  /**\n   * The time-to-live of a cache entry, in milliseconds.\n   */\n  ttl?: number\n\n  /**\n   * Whether to automatically prune expired entries.\n   */\n  ttlAutopurge?: boolean\n\n  /**\n   * The maximum total size of the cache, in units defined by the sizeCalculation\n   * function.\n   *\n   * @default No limit\n   */\n  maxSize?: number\n\n  /**\n   * The maximum size of a single cache entry, in units defined by the\n   * sizeCalculation function.\n   *\n   * @default No limit\n   */\n  maxEntrySize?: number\n\n  /**\n   * A function that returns the size of a value. The size is used to determine\n   * when the cache should be pruned, based on `maxSize`.\n   *\n   * @default The (rough) size in bytes used in memory.\n   */\n  sizeCalculation?: (value: V, key: K) => number\n} & ( // Memory is not infinite, so at least one pruning option is required.\n  | { max: number }\n  | { maxSize: number }\n  | { ttl: number; ttlAutopurge: boolean }\n)\n\n// LRUCache does not allow storing \"null\", so we use a symbol to represent it.\nconst nullSymbol = Symbol('nullItem')\ntype AsLruValue<V extends Value> = V extends null\n  ? typeof nullSymbol\n  : Exclude<V, null>\nconst toLruValue = <V extends Value>(value: V) =>\n  (value === null ? nullSymbol : value) as AsLruValue<V>\nconst fromLruValue = <V extends Value>(value: AsLruValue<V>) =>\n  (value === nullSymbol ? null : value) as V\n\nexport class SimpleStoreMemory<K extends Key, V extends Value>\n  implements SimpleStore<K, V>\n{\n  #cache: LRUCache<K, AsLruValue<V>>\n\n  constructor({ sizeCalculation, ...options }: SimpleStoreMemoryOptions<K, V>) {\n    this.#cache = new LRUCache<K, AsLruValue<V>>({\n      ...options,\n      allowStale: false,\n      updateAgeOnGet: false,\n      updateAgeOnHas: false,\n      sizeCalculation: sizeCalculation\n        ? (value, key) => sizeCalculation(fromLruValue(value), key)\n        : options.maxEntrySize != null || options.maxSize != null\n          ? // maxEntrySize and maxSize require a size calculation function.\n            roughSizeOfObject\n          : undefined,\n    })\n  }\n\n  get(key: K): V | undefined {\n    const value = this.#cache.get(key)\n    if (value === undefined) return undefined\n\n    return fromLruValue(value)\n  }\n\n  set(key: K, value: V): void {\n    this.#cache.set(key, toLruValue(value))\n  }\n\n  del(key: K): void {\n    this.#cache.delete(key)\n  }\n\n  clear(): void {\n    this.#cache.clear()\n  }\n}\n"
  },
  {
    "path": "packages/internal/simple-store-memory/src/util.ts",
    "content": "const knownSizes = new WeakMap<object, number>()\n\n/**\n * @see {@link https://stackoverflow.com/a/11900218/356537}\n */\nexport function roughSizeOfObject(value: unknown): number {\n  const objectList = new Set()\n  const stack = [value] // This would be more efficient using a circular buffer\n  let bytes = 0\n\n  while (stack.length) {\n    const value = stack.pop()\n\n    // > All objects on the heap start with a shape descriptor, which takes one\n    // > pointer size (usually 4 bytes these days, thanks to \"pointer\n    // > compression\" on 64-bit platforms).\n\n    switch (typeof value) {\n      // Types are ordered by frequency\n      case 'string':\n        // https://stackoverflow.com/a/68791382/356537\n        bytes += 12 + 4 * Math.ceil(value.length / 4)\n        break\n      case 'number':\n        bytes += 12 // Shape descriptor + double\n        break\n      case 'boolean':\n        bytes += 4 // Shape descriptor\n        break\n      case 'object':\n        bytes += 4 // Shape descriptor\n\n        if (value === null) {\n          break\n        }\n\n        if (knownSizes.has(value)) {\n          bytes += knownSizes.get(value)!\n          break\n        }\n\n        if (objectList.has(value)) continue\n        objectList.add(value)\n\n        if (Array.isArray(value)) {\n          bytes += 4\n          stack.push(...value)\n        } else {\n          bytes += 8\n          const keys = Object.getOwnPropertyNames(value)\n          for (let i = 0; i < keys.length; i++) {\n            bytes += 4\n            const key = keys[i]\n            const val = value[key]\n            if (val !== undefined) stack.push(val)\n            stack.push(key)\n          }\n        }\n        break\n      case 'function':\n        bytes += 8 // Shape descriptor + pointer (assuming functions are shared)\n        break\n      case 'symbol':\n        bytes += 8 // Shape descriptor + pointer\n        break\n      case 'bigint':\n        bytes += 16 // Shape descriptor + BigInt\n        break\n    }\n  }\n\n  if (typeof value === 'object' && value !== null) {\n    knownSizes.set(value, bytes)\n  }\n\n  return bytes\n}\n"
  },
  {
    "path": "packages/internal/simple-store-memory/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/internal/simple-store-memory/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/simple-store-redis/CHANGELOG.md",
    "content": "# @atproto-labs/simple-store-redis\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4149](https://github.com/bluesky-social/atproto/pull/4149) [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Initial implementation of redis based `SimpleStore` implementation\n"
  },
  {
    "path": "packages/internal/simple-store-redis/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/simple-store-redis\",\n  \"version\": \"0.0.1\",\n  \"license\": \"MIT\",\n  \"description\": \"Redis based simple-store implementation\",\n  \"keywords\": [\n    \"cache\",\n    \"node\",\n    \"memory\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/simple-store-redis\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/simple-store\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"peerDependencies\": {\n    \"ioredis\": \"^5.3.2\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/simple-store-redis/src/index.ts",
    "content": "import type { Redis } from 'ioredis'\nimport {\n  Awaitable,\n  GetOptions,\n  SimpleStore,\n  Value,\n} from '@atproto-labs/simple-store'\n\nexport type { Awaitable, GetOptions, SimpleStore, Value }\n\nexport type Encoder<K extends string, V extends Value> = (\n  value: V,\n  key: K,\n) => Awaitable<string>\nexport type Decoder<K extends string, V extends Value> = (\n  value: string,\n  key: K,\n) => Awaitable<V>\n\nexport const defaultEncoder: Encoder<any, Value> = (value) =>\n  JSON.stringify(value)\nexport const defaultDecoder: Decoder<any, Value> = (value) => JSON.parse(value)\n\nexport type SimpleStoreRedisOptions<K extends string, V extends Value> = {\n  keyPrefix: string\n  /** In milliseconds */\n  ttl?: number\n  /** @default JSON.stringify */\n  encode?: Encoder<K, V>\n  /** @default JSON.parse */\n  decode?: Decoder<K, V>\n}\n\nexport class SimpleStoreRedis<K extends string, V extends Value>\n  implements SimpleStore<K, V>\n{\n  constructor(\n    protected readonly redis: Redis,\n    protected readonly options: SimpleStoreRedisOptions<K, V>,\n  ) {\n    if (!options.keyPrefix) {\n      throw new TypeError(`keyPrefix must be a non-empty string`)\n    }\n    if (options.ttl != null && options.ttl <= 0) {\n      throw new TypeError(`ttl must be greater than 0`)\n    }\n  }\n\n  protected encodeKey(key: K): string {\n    return `${this.options.keyPrefix}${key satisfies string}`\n  }\n\n  async get(key: K, options?: GetOptions): Promise<V | undefined> {\n    const eKey = this.encodeKey(key)\n    const eValue = await this.redis.get(eKey)\n    if (eValue == null) return undefined\n    options?.signal?.throwIfAborted()\n    const { decode = defaultDecoder as Decoder<any, V> } = this.options\n    return decode(eValue, key)\n  }\n\n  async set(key: K, value: V): Promise<void> {\n    const eKey = this.encodeKey(key)\n    const { encode = defaultEncoder, ttl } = this.options\n    const eValue = await encode(value, key)\n    if (ttl) await this.redis.set(eKey, eValue, 'PX', ttl)\n    else await this.redis.set(eKey, eValue)\n  }\n\n  async del(key: K): Promise<void> {\n    const eKey = this.encodeKey(key)\n    await this.redis.del(eKey)\n  }\n}\n"
  },
  {
    "path": "packages/internal/simple-store-redis/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/internal/simple-store-redis/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/internal/xrpc-utils/CHANGELOG.md",
    "content": "# @atproto-labs/xrpc-utils\n\n## 0.0.24\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/xrpc-server@0.10.0\n  - @atproto/xrpc@0.7.6\n\n## 0.0.23\n\n### Patch Changes\n\n- Updated dependencies [[`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/xrpc-server@0.9.6\n\n## 0.0.22\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc-server@0.9.5\n  - @atproto/xrpc@0.7.5\n\n## 0.0.21\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/xrpc@0.7.4\n  - @atproto/xrpc-server@0.9.4\n\n## 0.0.20\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.3\n  - @atproto/xrpc-server@0.9.3\n\n## 0.0.19\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.2\n  - @atproto/xrpc-server@0.9.2\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies [[`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671)]:\n  - @atproto/xrpc-server@0.9.1\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/xrpc-server@0.9.0\n  - @atproto/xrpc@0.7.1\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c)]:\n  - @atproto/xrpc-server@0.8.0\n\n## 0.0.15\n\n### Patch Changes\n\n- Updated dependencies [[`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f)]:\n  - @atproto/xrpc-server@0.7.19\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba)]:\n  - @atproto/xrpc@0.7.0\n  - @atproto/xrpc-server@0.7.18\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies [[`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9)]:\n  - @atproto/xrpc-server@0.7.17\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c)]:\n  - @atproto/xrpc-server@0.7.16\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc-server@0.7.15\n  - @atproto/xrpc@0.6.12\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc-server@0.7.14\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.6.11\n  - @atproto/xrpc-server@0.7.13\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.6.10\n  - @atproto/xrpc-server@0.7.12\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.6.9\n  - @atproto/xrpc-server@0.7.11\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/xrpc-server@0.7.10\n  - @atproto/xrpc@0.6.8\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9)]:\n  - @atproto/xrpc-server@0.7.9\n  - @atproto/xrpc@0.6.7\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12)]:\n  - @atproto/xrpc-server@0.7.8\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61)]:\n  - @atproto/xrpc-server@0.7.7\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc-server@0.7.6\n\n## 0.0.1\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - New utility package to work with xrpc-server\n\n- Updated dependencies []:\n  - @atproto/xrpc-server@0.7.5\n  - @atproto/xrpc@0.6.6\n"
  },
  {
    "path": "packages/internal/xrpc-utils/package.json",
    "content": "{\n  \"name\": \"@atproto-labs/xrpc-utils\",\n  \"version\": \"0.0.24\",\n  \"license\": \"MIT\",\n  \"description\": \"XRPC server utilities for Node.JS\",\n  \"keywords\": [\n    \"atproto\",\n    \"node\",\n    \"xrpc\",\n    \"server\",\n    \"utilities\",\n    \"content\",\n    \"negotiation\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/internal/xrpc-utils\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./accept\": {\n      \"types\": \"./dist/accept.d.ts\",\n      \"default\": \"./dist/accept.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"@atproto/xrpc-server\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/internal/xrpc-utils/src/accept.ts",
    "content": "import { ResponseType } from '@atproto/xrpc'\nimport {\n  InvalidRequestError,\n  XRPCError as XRPCServerError,\n} from '@atproto/xrpc-server'\n\nexport type AcceptFlags = { q: number }\nexport type Accept = [name: string, flags: AcceptFlags]\n\nexport const ACCEPT_ENCODING_COMPRESSED: readonly [Accept, ...Accept[]] = [\n  ['gzip', { q: 1.0 }],\n  ['deflate', { q: 0.9 }],\n  ['br', { q: 0.8 }],\n  ['identity', { q: 0.1 }],\n]\n\nexport const ACCEPT_ENCODING_UNCOMPRESSED: readonly [Accept, ...Accept[]] = [\n  ['identity', { q: 1.0 }],\n  ['gzip', { q: 0.3 }],\n  ['deflate', { q: 0.2 }],\n  ['br', { q: 0.1 }],\n]\n\n// accept-encoding defaults to \"identity with lowest priority\"\nconst ACCEPT_ENC_DEFAULT = ['identity', { q: 0.001 }] as const satisfies Accept\nconst ACCEPT_FORBID_STAR = ['*', { q: 0 }] as const satisfies Accept\n\nexport function buildProxiedContentEncoding(\n  acceptHeader: undefined | string | string[],\n  preferCompressed: boolean,\n): string {\n  return negotiateContentEncoding(\n    acceptHeader,\n    preferCompressed\n      ? ACCEPT_ENCODING_COMPRESSED\n      : ACCEPT_ENCODING_UNCOMPRESSED,\n  )\n}\n\nexport function negotiateContentEncoding(\n  acceptHeader: undefined | string | string[],\n  preferences: readonly Accept[],\n): string {\n  const acceptMap = Object.fromEntries<undefined | AcceptFlags>(\n    parseAcceptEncoding(acceptHeader),\n  )\n\n  // Make sure the default (identity) is covered by the preferences\n  if (!preferences.some(coversIdentityAccept)) {\n    preferences = [...preferences, ACCEPT_ENC_DEFAULT]\n  }\n\n  const common = preferences.filter(([name]) => {\n    const acceptQ = (acceptMap[name] ?? acceptMap['*'])?.q\n    // Per HTTP/1.1, \"identity\" is always acceptable unless explicitly rejected\n    if (name === 'identity') {\n      return acceptQ == null || acceptQ > 0\n    } else {\n      return acceptQ != null && acceptQ > 0\n    }\n  })\n\n  // Since \"identity\" was present in the preferences, a missing \"identity\" in\n  // the common array means that the client explicitly rejected it. Let's reflect\n  // this by adding it to the common array.\n  if (!common.some(coversIdentityAccept)) {\n    common.push(ACCEPT_FORBID_STAR)\n  }\n\n  // If no common encodings are acceptable, throw a 406 Not Acceptable error\n  if (!common.some(isAllowedAccept)) {\n    throw new XRPCServerError(\n      ResponseType.NotAcceptable,\n      'this service does not support any of the requested encodings',\n    )\n  }\n\n  return formatAcceptHeader(common as [Accept, ...Accept[]])\n}\n\nfunction coversIdentityAccept([name]: Accept): boolean {\n  return name === 'identity' || name === '*'\n}\n\nfunction isAllowedAccept([, flags]: Accept): boolean {\n  return flags.q > 0\n}\n\n/**\n * @see {@link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values}\n */\nexport function formatAcceptHeader(\n  accept: readonly [Accept, ...Accept[]],\n): string {\n  return accept.map(formatAcceptPart).join(',')\n}\n\nfunction formatAcceptPart([name, flags]: Accept): string {\n  return `${name};q=${flags.q}`\n}\n\nfunction parseAcceptEncoding(\n  acceptEncodings: undefined | string | string[],\n): Accept[] {\n  if (!acceptEncodings?.length) return []\n\n  return Array.isArray(acceptEncodings)\n    ? acceptEncodings.flatMap(parseAcceptEncoding)\n    : acceptEncodings.split(',').map(parseAcceptEncodingDefinition)\n}\n\nfunction parseAcceptEncodingDefinition(def: string): Accept {\n  const { length, 0: encoding, 1: params } = def.trim().split(';', 3)\n\n  if (length > 2) {\n    throw new InvalidRequestError(`Invalid accept-encoding: \"${def}\"`)\n  }\n\n  if (!encoding || encoding.includes('=')) {\n    throw new InvalidRequestError(`Invalid accept-encoding: \"${def}\"`)\n  }\n\n  const flags = { q: 1 }\n  if (length === 2) {\n    const { length, 0: key, 1: value } = params.split('=', 3)\n    if (length !== 2) {\n      throw new InvalidRequestError(`Invalid accept-encoding: \"${def}\"`)\n    }\n\n    if (key === 'q' || key === 'Q') {\n      const q = parseFloat(value)\n      if (q === 0 || (Number.isFinite(q) && q <= 1 && q >= 0.001)) {\n        flags.q = q\n      } else {\n        throw new InvalidRequestError(`Invalid accept-encoding: \"${def}\"`)\n      }\n    } else {\n      throw new InvalidRequestError(`Invalid accept-encoding: \"${def}\"`)\n    }\n  }\n\n  return [encoding.toLowerCase(), flags]\n}\n"
  },
  {
    "path": "packages/internal/xrpc-utils/src/index.ts",
    "content": "export * from './accept.js'\n"
  },
  {
    "path": "packages/internal/xrpc-utils/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/internal/xrpc-utils/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/lex/lex/.gitignore",
    "content": "tests/lexicons\n"
  },
  {
    "path": "packages/lex/lex/CHANGELOG.md",
    "content": "# @atproto/lex\n\n## 0.0.22\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-client@0.0.17\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-data@0.0.14\n  - @atproto/lex-builder@0.0.19\n  - @atproto/lex-installer@0.0.22\n  - @atproto/lex-json@0.0.14\n\n## 0.0.21\n\n### Patch Changes\n\n- Updated dependencies [[`5a2f884`](https://github.com/bluesky-social/atproto/commit/5a2f8847efd91252971fa243d21bd52ada7aa8f4), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-client@0.0.16\n  - @atproto/lex-schema@0.0.15\n  - @atproto/lex-builder@0.0.18\n  - @atproto/lex-installer@0.0.21\n\n## 0.0.20\n\n### Patch Changes\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update readme\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-client@0.0.15\n  - @atproto/lex-installer@0.0.20\n  - @atproto/lex-builder@0.0.17\n  - @atproto/lex-json@0.0.13\n\n## 0.0.19\n\n### Patch Changes\n\n- [#4672](https://github.com/bluesky-social/atproto/pull/4672) [`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve readme\n\n- Updated dependencies [[`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2)]:\n  - @atproto/lex-client@0.0.14\n  - @atproto/lex-installer@0.0.19\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-client@0.0.13\n  - @atproto/lex-installer@0.0.18\n\n## 0.0.17\n\n### Patch Changes\n\n- [#4639](https://github.com/bluesky-social/atproto/pull/4639) [`009c4af`](https://github.com/bluesky-social/atproto/commit/009c4afd3643d4edf4bd05065ec93cab74610bfe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add test cases for `knownValues` strings\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`cd9deb6`](https://github.com/bluesky-social/atproto/commit/cd9deb6f210b91661595398cb2ef70bc40eccabe), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-builder@0.0.16\n  - @atproto/lex-data@0.0.12\n  - @atproto/lex-client@0.0.13\n  - @atproto/lex-installer@0.0.17\n  - @atproto/lex-json@0.0.12\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`619068f`](https://github.com/bluesky-social/atproto/commit/619068fb81203b3b43b632892bdcb0a5067f7fe4)]:\n  - @atproto/lex-builder@0.0.15\n  - @atproto/lex-client@0.0.12\n  - @atproto/lex-installer@0.0.16\n\n## 0.0.15\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update README\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-client@0.0.12\n  - @atproto/lex-json@0.0.11\n  - @atproto/lex-installer@0.0.15\n  - @atproto/lex-data@0.0.11\n  - @atproto/lex-builder@0.0.14\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ability to set default `headers` when creating XRPC agent\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-client@0.0.11\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-installer@0.0.14\n  - @atproto/lex-json@0.0.10\n  - @atproto/lex-schema@0.0.11\n  - @atproto/lex-builder@0.0.13\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-installer@0.0.13\n\n## 0.0.12\n\n### Patch Changes\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `TypedObject` to `Unknown$TypedObject`\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix inability to assign (object containing) open union results to `LexMap` type\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add new `Unknown$Type` type to represent records an object's unknown `$type` property (typically from open unions).\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `UnknownObjectOutput` to `UnknownObject`\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/lex-client@0.0.10\n  - @atproto/lex-json@0.0.9\n  - @atproto/lex-data@0.0.9\n  - @atproto/lex-builder@0.0.12\n  - @atproto/lex-installer@0.0.12\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/lex-client@0.0.9\n  - @atproto/lex-installer@0.0.11\n  - @atproto/lex-json@0.0.8\n  - @atproto/lex-schema@0.0.9\n  - @atproto/lex-builder@0.0.11\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-builder@0.0.10\n  - @atproto/lex-client@0.0.8\n  - @atproto/lex-installer@0.0.10\n  - @atproto/lex-json@0.0.7\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add an `indexFile` option that allows generating an \"index.ts\" file that re-exports every tld namespaces.\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export everything from `@atproto/lex-data` and `@atproto/lex-json`\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-builder@0.0.9\n  - @atproto/lex-client@0.0.7\n  - @atproto/lex-data@0.0.6\n  - @atproto/lex-schema@0.0.7\n  - @atproto/lex-installer@0.0.9\n  - @atproto/lex-json@0.0.6\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n  - @atproto/lex-builder@0.0.8\n  - @atproto/lex-client@0.0.6\n  - @atproto/lex-installer@0.0.8\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `main` export in `package.json`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Re-expect everything from `@atproto/lex-client`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Re-export everything from `@atproto/lex-schema`\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-client@0.0.5\n  - @atproto/lex-schema@0.0.5\n  - @atproto/lex-builder@0.0.7\n  - @atproto/lex-installer@0.0.7\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`e39ca11`](https://github.com/bluesky-social/atproto/commit/e39ca114accac65070dcdd424a181821aad6d99d)]:\n  - @atproto/lex-builder@0.0.6\n  - @atproto/lex-client@0.0.4\n  - @atproto/lex-installer@0.0.6\n  - @atproto/lex-schema@0.0.4\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework object validation logic to work without `options` argument\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename schema methods `validate`, `check` and `maybe` to `safeParse`, `matches` and `ifMatches` respectively.\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- [#4398](https://github.com/bluesky-social/atproto/pull/4398) [`a17d2e8`](https://github.com/bluesky-social/atproto/commit/a17d2e8a59ceb00fa8197642e0767fcc776d5b70) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `ignoreInvalidLexicons` option when building lexicon schemas\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` option to `const` and `enum` schemas\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`2d13d05`](https://github.com/bluesky-social/atproto/commit/2d13d05ab06576703742b1b638d2f243b6b2915f), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`0f2fc65`](https://github.com/bluesky-social/atproto/commit/0f2fc6592f0c89d26ac7a2ef70b12cbd15a18d05), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`a17d2e8`](https://github.com/bluesky-social/atproto/commit/a17d2e8a59ceb00fa8197642e0767fcc776d5b70), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba)]:\n  - @atproto/lex-schema@0.0.3\n  - @atproto/lex-installer@0.0.5\n  - @atproto/lex-builder@0.0.5\n  - @atproto/lex-client@0.0.3\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4380](https://github.com/bluesky-social/atproto/pull/4380) [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix description of `--allowLegacyBlobs` argument\n\n- [#4380](https://github.com/bluesky-social/atproto/pull/4380) [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `--build`/`--no-build` argument from the `install` command\n\n- [#4380](https://github.com/bluesky-social/atproto/pull/4380) [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ability to configure file extenstion and import file extension in `lex build`\n\n- Updated dependencies [[`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4), [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4), [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4)]:\n  - @atproto/lex-schema@0.0.2\n  - @atproto/lex-builder@0.0.4\n  - @atproto/lex-installer@0.0.4\n  - @atproto/lex-client@0.0.2\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4374](https://github.com/bluesky-social/atproto/pull/4374) [`5ffd612`](https://github.com/bluesky-social/atproto/commit/5ffd6129909071e979c30f31266119865ab582b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing files from package.json\n\n- Updated dependencies []:\n  - @atproto/lex-builder@0.0.3\n  - @atproto/lex-installer@0.0.3\n  - @atproto/lex-client@0.0.1\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4372](https://github.com/bluesky-social/atproto/pull/4372) [`7456f53`](https://github.com/bluesky-social/atproto/commit/7456f53e45fb3eef2f3bbdf2513da2d8ab078d80) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix error when running `install` command\n\n- Updated dependencies [[`7456f53`](https://github.com/bluesky-social/atproto/commit/7456f53e45fb3eef2f3bbdf2513da2d8ab078d80)]:\n  - @atproto/lex-builder@0.0.2\n  - @atproto/lex-client@0.0.1\n  - @atproto/lex-installer@0.0.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-builder@0.0.1\n  - @atproto/lex-client@0.0.1\n  - @atproto/lex-installer@0.0.1\n  - @atproto/lex-schema@0.0.1\n"
  },
  {
    "path": "packages/lex/lex/README.md",
    "content": "> [!IMPORTANT]\n>\n> This package is currently in **preview**. The API and features are subject to change before the stable release. See the [Changelog](./CHANGELOG.md) for version history.\n\nType-safe Lexicon tooling for AT Protocol data.\n\n- Fetch and manage Lexicon schemas, generate TypeScript validators\n- Compile-time and runtime type safety for AT Protocol data structures\n- Fully typed XRPC client with authentication support\n- Tree-shaking and composition friendly\n\n```typescript\n// Build data with generated builders and validators\n\nconst newPost = app.bsky.feed.post.$build({\n  text: 'Hello, world!',\n  createdAt: new Date().toISOString(),\n})\n\napp.bsky.actor.profile.$validate({\n  $type: 'app.bsky.actor.profile',\n  displayName: 'Ha'.repeat(32) + '!',\n}) // Error: grapheme too big (maximum 64, got 65) at $.displayName\n```\n\n```typescript\n// Trivially make type-safe XRPC requests towards a service\n\nconst profile = await xrpc('https://api.bsky.app', app.bsky.actor.getProfile, {\n  params: { actor: 'pfrazee.com' },\n})\n```\n\n```typescript\n// Manipulate records with the Client API in the context of an authenticated session\n\nconst client = new Client(oauthSession)\n\nawait client.create(app.bsky.feed.post, {\n  text: 'Hello, world!',\n  createdAt: new Date().toISOString(),\n})\n\nconst posts = await client.list(app.bsky.feed.post, { limit: 10 })\n```\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [Quick Start](#quick-start)\n- [Lexicon Schemas](#lexicon-schemas)\n- [TypeScript Schemas](#typescript-schemas)\n  - [Generated Schema Structure](#generated-schema-structure)\n  - [Type definitions](#type-definitions)\n  - [Building data](#building-data)\n  - [Validation Helpers](#validation-helpers)\n- [Data Model](#data-model)\n  - [Types](#types)\n  - [JSON Encoding](#json-encoding)\n  - [CBOR Encoding](#cbor-encoding)\n- [Making simple XRPC Requests](#making-simple-xrpc-requests)\n- [Client API](#client-api)\n  - [Creating a Client](#creating-a-client)\n  - [Core Methods](#core-methods)\n  - [Error Handling](#error-handling)\n  - [Authentication Methods](#authentication-methods)\n  - [Labeler Configuration](#labeler-configuration)\n  - [Low-Level XRPC](#low-level-xrpc)\n- [Utilities](#utilities)\n  - [Datetime Strings](#datetime-strings)\n- [Advanced Usage](#advanced-usage)\n  - [Workflow Integration](#workflow-integration)\n  - [Tree-Shaking](#tree-shaking)\n  - [Blob references](#blob-references)\n  - [Actions](#actions)\n  - [Creating a Client from Another Client](#creating-a-client-from-another-client)\n  - [Building Library-Style APIs with Actions](#building-library-style-apis-with-actions)\n  - [Standard Schema Compatibility](#standard-schema-compatibility)\n- [License](#license)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## Quick Start\n\n**1. Install Lexicons**\n\nInstall the Lexicon schemas you need for your application:\n\n```bash\nlex install app.bsky.feed.post app.bsky.feed.like\n```\n\nThis creates:\n\n- `lexicons.json` - manifest tracking installed Lexicons and their versions (CIDs)\n- `lexicons/` - directory containing the Lexicon JSON files\n\n> [!NOTE]\n>\n> The `lex` command might conflict with other binaries installed on your system.\n> If that happens, you can also run the CLI using `ts-lex`, `pnpm exec lex` or\n> `npx @atproto/lex`.\n\n**2. Verify and commit installed Lexicons**\n\nMake sure to commit the `lexicons.json` manifest and the `lexicons/` directory containing the JSON files to version control.\n\n```bash\ngit add lexicons.json lexicons/\ngit commit -m \"Install Lexicons\"\n```\n\n**3. Build TypeScript schemas**\n\nGenerate TypeScript schemas from the installed Lexicons:\n\n```bash\nlex build\n```\n\nThis generates TypeScript files in `./src/lexicons` (by default) with type-safe validation, type guards, and builder utilities.\n\n> [!TIP]\n>\n> If you wish to customize the output location or any other build options, pass the appropriate flags to the `lex build` command. See the [TypeScript Schemas](#typescript-schemas) section for available options.\n\n> [!NOTE]\n>\n> The generated TypeScript files don't need to be committed to version control. Instead, they can be generated during your project's build step. See [Workflow Integration](#workflow-integration) for details.\n>\n> To avoid committing generated files, add the output directory to your `.gitignore`:\n>\n> ```bash\n> echo \"./src/lexicons\" >> .gitignore\n> ```\n\n**4. Use in your code**\n\n```typescript\nimport { xrpc } from '@atproto/lex'\nimport { app } from './lexicons/index.js'\n\nconst profile = await xrpc('https://api.bsky.app', app.bsky.actor.getProfile, {\n  params: { actor: 'pfrazee.com' },\n})\n```\n\n## Lexicon Schemas\n\nThe `lex install` command fetches Lexicon schemas from the Atmosphere network and manages them locally (in the `lexicons/` directory by default). It also updates the `lexicons.json` manifest file to track installed Lexicons and their versions.\n\n```bash\n# Install Lexicons and update lexicons.json (default behavior)\nlex install app.bsky.feed.post\n\n# Install all Lexicons from lexicons.json manifest\nlex install\n\n# Install specific Lexicons without updating manifest\nlex install --no-save app.bsky.feed.post app.bsky.actor.profile\n\n# Update (re-fetch) all installed Lexicons to latest versions\nlex install --update\n\n# Fetch any missing Lexicons and verify against manifest\nlex install --ci\n```\n\nOptions:\n\n- `--manifest <path>` - Path to lexicons.json manifest file (default: `./lexicons.json`)\n- `--no-save` - Don't update lexicons.json with installed lexicons (save is enabled by default)\n- `--update` - Update all installed lexicons to their latest versions by re-resolving and re-installing them\n- `--ci` - Error if the installed lexicons do not match the CIDs in the lexicons.json manifest\n- `--lexicons <dir>` - Directory containing lexicon JSON files (default: `./lexicons`)\n\n## TypeScript Schemas\n\nAfter installing Lexicon JSON files, use the `lex build` command to generate TypeScript schemas. These generated schemas provide type-safe validation, type guards, and builder utilities for working with AT Protocol data structures.\n\n```bash\nlex build --lexicons ./lexicons --out ./src/lexicons\n```\n\nOptions:\n\n- `--lexicons <dir>` - Directory containing lexicon JSON files (default: `./lexicons`)\n- `--out <dir>` - Output directory for generated TypeScript (default: `./src/lexicons`)\n- `--clear` - Clear output directory before generating\n- `--override` - Override existing files (has no effect with --clear)\n- `--no-pretty` - Don't run prettier on generated files (prettier is enabled by default)\n- `--ignore-errors` - How to handle errors when processing input files\n- `--pure-annotations` - Add `/*#__PURE__*/` annotations for tree-shaking tools. Set this to true if you are using generated lexicons in a library\n- `--exclude <patterns...>` - List of strings or regex patterns to exclude lexicon documents by their IDs\n- `--include <patterns...>` - List of strings or regex patterns to include lexicon documents by their IDs\n- `--lib <package>` - Package name of the library to import the lex schema utility \"l\" from (default: `@atproto/lex`)\n- `--allowLegacyBlobs` - Allow generating schemas that accept legacy blob references (disabled by default; enable this if you encounter issues while processing records created a long time ago)\n- `--importExt <ext>` - File extension to use for import statements in generated files (default: `.js`). Use `--importExt \"\"` to generate extension-less imports\n- `--fileExt <ext>` - File extension to use for generated files (default: `.ts`)\n- `--indexFile` - Generate an \"index\" file that re-exports all root-level namespaces (disabled by default)\n\n### Generated Schema Structure\n\nEach Lexicon generates a TypeScript module with:\n\n- **Type definitions** - TypeScript types extracted from the schema\n- **Schema instances** - Runtime validation objects with methods\n- **Exported utilities** - Convenience functions for common operations\n\n### Type definitions\n\nYou can extract TypeScript types from the generated schemas for use in you application:\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nfunction renderPost(p: app.bsky.feed.post.Main) {\n  console.log(p.$type) // 'app.bsky.feed.post'\n  console.log(p.text)\n}\n```\n\n### Building data\n\nIt is recommended to use the generated builders to create data that conforms to the schema. This ensures that all required fields are present.\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\n// variable type will be inferred as \"app.bsky.feed.post.Main\"\nconst post = app.bsky.feed.post.$build({\n  // No need to specify $type when using $build\n  text: 'Hello, world!',\n  createdAt: l.toDatetimeString(new Date()),\n})\n```\n\n### Validation Helpers\n\nEach schema provides multiple validation methods:\n\n#### `$nsid` - Namespace Identifier\n\nReturns the NSID of the schema:\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nconsole.log(app.bsky.feed.defs.$nsid) // 'app.bsky.feed.defs'\n```\n\n#### `$type` - Type Identifier\n\nReturns the `$type` string of the schema (for record and object schemas):\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nconsole.log(app.bsky.feed.post.$type) // 'app.bsky.feed.post'\nconsole.log(app.bsky.actor.defs.profileViewBasic.$type) // 'app.bsky.actor.defs#profileViewBasic'\n```\n\n#### `$check(data)` - Type Guard\n\nReturns `true` if data matches the schema, `false` otherwise. Acts as a TypeScript type guard:\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\nconst data = {\n  $type: 'app.bsky.feed.post',\n  text: 'Hello!',\n  createdAt: l.toDatetimeString(new Date()),\n}\n\nif (app.bsky.feed.post.$check(data)) {\n  // TypeScript knows data is a Post here\n  console.log(data.text)\n}\n```\n\n#### `$parse(data)` - Parse and Validate\n\nValidates and returns typed data, throwing an error if validation fails:\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\ntry {\n  const post = app.bsky.feed.post.$main.$parse({\n    $type: 'app.bsky.feed.post',\n    text: 'Hello!',\n    createdAt: l.toDatetimeString(new Date()),\n  })\n  // post is now typed and validated\n  console.log(post.text)\n} catch (error) {\n  console.error('Validation failed:', error)\n}\n```\n\n> [!NOTE]\n>\n> The `$parse` method will apply defaults defined in the schema for optional fields, as well as data coercion (e.g., CID strings to Cid types). This means that the returned value might be different from the input data if defaults were applied. Use `$validate()` for value validation.\n\n#### `$validate(data)` - Validate a value against the schema\n\nValidates an existing value against a schema, returning the value itself if, and only if, it already matches the schema (ie. without applying defaults or coercion).\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\nconst value = {\n  $type: 'app.bsky.feed.post',\n  text: 'Hello!',\n  createdAt: l.toDatetimeString(new Date()),\n}\n\n// Throws if no valid\nconst result = app.bsky.feed.post.$validate(value)\n\nvalue === result // true\n```\n\n#### `$safeParse(data, options?)` - Parse a value against a schema and get the resulting value\n\nReturns a detailed validation result object without throwing:\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\nconst result = app.bsky.feed.post.$safeParse({\n  $type: 'app.bsky.feed.post',\n  text: 'Hello!',\n  createdAt: l.toDatetimeString(new Date()),\n})\n\nif (result.success) {\n  console.log('Valid post:', result.value)\n} else {\n  console.error('Validation failed:', result.error)\n}\n```\n\nAll schema methods that perform validation (`$parse`, `$safeParse`, `$validate`, `$safeValidate`) accept an optional `{ strict }` option. When `strict` is `false`, validation becomes more lenient: datetime string format checks are relaxed (e.g. datetimes without timezones are accepted; other string formats remain strict), blob MIME type and size constraints are not enforced, and non-raw CIDs are allowed in blob references. This is primarily used internally by the XRPC client when `strictResponseProcessing` is disabled, but can also be used directly:\n\n```typescript\n// Strict mode (default) - rejects datetime without timezone\napp.bsky.feed.post.$safeParse(data) // { strict: true } is the default\n\n// Non-strict mode - accepts more lenient data\napp.bsky.feed.post.$safeParse(data, { strict: false })\n```\n\n#### `$build(data)` - Build with Defaults\n\nBuilds data without needing to specify the `$type` property, and properly types the result:\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\n// The type of the \"like\" variable will be \"app.bsky.feed.like.Main\"\nconst like = app.bsky.feed.like.$build({\n  subject: {\n    uri: 'at://did:plc:abc/app.bsky.feed.post/123',\n    cid: 'bafyrei...',\n  },\n  createdAt: l.toDatetimeString(new Date()),\n})\n```\n\n#### `$isTypeOf(data)` - Type Discriminator\n\nDiscriminates (pre-validated) data based on its `$type` property, without re-validating. This is especially useful when working with union types:\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\ndeclare const data:\n  | app.bsky.feed.post.Main\n  | app.bsky.feed.like.Main\n  | l.Unknown$TypedObject\n\n// Discriminate by $type without re-validating\nif (app.bsky.feed.post.$isTypeOf(data)) {\n  // data is a post\n}\n```\n\n## Data Model\n\nThe AT Protocol uses a [data model](https://atproto.com/specs/data-model) that extends JSON with two additional data structures: **CIDs** (content-addressed links) and **bytes** (for raw data). This data model can be encoded either as JSON for XRPC (HTTP API) or as [CBOR](https://dasl.ing/drisl.html) for storage and authentication (see [`@atproto/lex-cbor`](../lex-cbor)).\n\n### Types\n\nThe package exports TypeScript types and type guards for working with the data model:\n\n```typescript\nimport type {\n  LexValue,\n  LexMap,\n  LexScalar,\n  TypedLexMap,\n  Cid,\n} from '@atproto/lex'\nimport { isLexValue, isLexMap, isTypedLexMap, isCid } from '@atproto/lex'\n\n// LexScalar: number | string | boolean | null | Cid | Uint8Array\n// LexValue:  LexScalar | LexValue[] | { [key: string]?: LexValue }\n// LexMap:    { [key: string]?: LexValue }\n// TypedLexMap: LexMap & { $type: string }\n// Cid: Content Identifier (link by hash)\n\nif (isTypedLexMap(data)) {\n  console.log(data.$type) // some string\n}\n```\n\n### JSON Encoding\n\nIn JSON, CIDs are represented as `{\"$link\": \"bafyrei...\"}` and bytes as `{\"$bytes\": \"base64...\"}`. This package provides utilities to parse and stringify data model values to/from JSON:\n\n```typescript\nimport { Cid, lexParse, lexStringify, jsonToLex, lexToJson } from '@atproto/lex'\n\n// Parse JSON string → data model (decodes $link and $bytes)\nconst parsed = lexParse<{\n  ref: Cid\n  data: Uint8Array\n}>(`{\n  \"ref\": { \"$link\": \"bafyrei...\" },\n  \"data\": { \"$bytes\": \"SGVsbG8sIHdvcmxkIQ==\" }\n}`)\n\nassert(isCid(parsed.ref))\nassert(parsed.data instanceof Uint8Array)\n\nconst someCid = lexParse<Cid>('{\"$link\": \"bafyrei...\"}')\nconst someBytes = lexParse<Uint8Array>('{\"$bytes\": \"SGVsbG8sIHdvcmxkIQ==\"}')\n\n// Data model → JSON string (encodes CIDs and bytes)\nconst json = lexStringify({ ref: someCid, data: someBytes })\n\n// Convert between parsed JSON objects and data model values\nconst lex = jsonToLex({\n  ref: { $link: 'bafyrei...' }, // Converted to Cid\n  data: { $bytes: 'SGVsbG8sIHdvcmxkIQ==' }, // Converted to Uint8Array\n})\n\nconst obj = lexToJson({\n  ref: someCid, // Converted to { $link: string }\n  data: someBytes, // Converted to { $bytes: string }\n})\n```\n\n### CBOR Encoding\n\nUse `@atproto/lex-cbor` to encode/decode the data model to/from CBOR ([DRISL](https://dasl.ing/drisl.html)) format for storage and authentication:\n\n```typescript\nimport { encode, decode } from '@atproto/lex-cbor'\nimport type { LexValue } from '@atproto/lex'\n\n// Encode data model to CBOR bytes\nconst cborBytes = encode(someLexValue)\n\n// Decode CBOR bytes to data model\nconst lexValue: LexValue = decode(cborBytes)\n```\n\n## Making simple XRPC Requests\n\n[XRPC](https://atproto.com/specs/xrpc) (short for \"Lexicon RPC\") is the set of HTTP conventions used by AT Protocol for client-server and server-server communication. Endpoints follow the pattern `/xrpc/<nsid>`, where the NSID maps to a Lexicon schema that defines the request and response types. XRPC has three method types: **queries** (HTTP GET) for read operations, **procedures** (HTTP POST) for mutations and **subscriptions** (WebSockets) for real-time updates.\n\nThe `xrpc()` and `xrpcSafe()` functions can be used to make simple XRPC requests. They are typically used in places that don't require an authenticated session, or when more granular control over the request/response is needed. For most use cases, the `Client` API provides a more ergonomic way to work with XRPC in the context of an authenticated session.\n\n```typescript\nimport { xrpc, xrpcSafe } from '@atproto/lex'\nimport * as com from './lexicons/com.js'\n\nconst response = await xrpc(\n  'https://bsky.network',\n  com.atproto.identity.resolveHandle,\n  {\n    params: { handle: 'atproto.com' },\n    headers: { 'user-agent': 'MyApp/1.0.0' },\n  },\n)\n\nresponse.status // number\nresponse.headers // Headers\nresponse.body.did // `did:${string}:${string}`\n\n// Or use the safe variant (returns errors instead of throwing)\nconst result = await xrpcSafe(\n  'https://bsky.network',\n  com.atproto.identity.resolveHandle,\n  {\n    params: { handle: 'atproto.com' },\n    signal: AbortSignal.timeout(5000), // Abort after 5 seconds\n  },\n)\n\nif (result.success) {\n  console.log(result.body)\n} else {\n  console.error(result.error) // XRPC error code\n  console.error(result.message) // Error message\n}\n```\n\nBoth `xrpc()` and `xrpcSafe()` accept `validateRequest`, `validateResponse`, and `strictResponseProcessing` options to control validation and strictness per-call. See [Validation and Strictness Options](#validation-and-strictness-options) for details.\n\n## Client API\n\nThe `Client` class provides high-level helpers for common AT Protocol \"repo\" operations: `create()`, `get()`, `put()`, `delete()`, `list()`, `uploadBlob()`, and more. A `Client` instance is typically useful for making requests in the context of an authenticated user session, as it automatically handles headers and provides default values based on the authenticated user's DID.\n\nA `Client` instance is also useful to encapsulate configuration for a specific service, by specifying the `service` option (for proxying) and `labelers` option (for content labeling). Additionally, a `Client` can be used as an `Agent` for another `Client`, allowing you to compose headers and configuration across multiple services.\n\n### Creating a Client\n\n#### Unauthenticated Client\n\nJust provide the service URL:\n\n```typescript\nimport { Client } from '@atproto/lex'\n\nconst client = new Client('https://public.api.bsky.app')\n```\n\n#### Authenticated Client with OAuth\n\n```typescript\nimport { Client } from '@atproto/lex'\nimport { OAuthClient } from '@atproto/oauth-client-node'\n\n// Setup OAuth client (see @atproto/oauth-client documentation)\nconst oauthClient = new OAuthClient({\n  /* ... */\n})\nconst session = await oauthClient.restore(userDid)\n\n// Create authenticated client\nconst client = new Client(session)\n```\n\nFor detailed OAuth setup, see the [@atproto/oauth-client](../../../oauth/oauth-client) documentation.\n\n#### Authenticated Client with Password\n\nFor CLI tools, scripts, and bots, you can use password-based authentication with [`@atproto/lex-password-session`](../lex-password-session):\n\n```typescript\nimport { Client } from '@atproto/lex'\nimport { PasswordSession } from '@atproto/lex-password-session'\n\nconst session = await PasswordSession.login({\n  service: 'https://bsky.social',\n  identifier: 'alice.bsky.social',\n  password: 'xxxx-xxxx-xxxx-xxxx', // App password\n  onUpdated: (data) => saveToStorage(data),\n  onDeleted: (data) => clearStorage(data.did),\n})\n\nconst client = new Client(session)\n```\n\nFor detailed password session setup, see the [@atproto/lex-password-session](../lex-password-session) documentation.\n\n#### Client with Service Proxy (authenticated only)\n\n```typescript\nimport { Client } from '@atproto/lex'\n\n// Route requests through a specific service\nconst client = new Client(session, {\n  service: 'did:web:api.bsky.app#bsky_appview',\n})\n```\n\n#### Validation and Strictness Options\n\nThe `Client` constructor accepts options to control request/response validation and how invalid Lex data is handled. These defaults apply to all XRPC calls made through the client, and can be overridden per-call via `client.call()`, `client.xrpc()` or `client.xrpcSafe()`.\n\n```typescript\nconst client = new Client(session, {\n  // Validate requests against the method's input schema (default: false)\n  validateRequest: true,\n\n  // Validate responses against the method's output schema (default: true)\n  validateResponse: true,\n\n  // Strictly process responses according to Lex encoding rules. When set to\n  // false, accepts responses containing invalid Lex data such as floats or\n  // malformed $bytes/$link objects (default: true)\n  strictResponseProcessing: false,\n})\n```\n\n- **`validateRequest`** — When `true`, outgoing request bodies are validated against the Lexicon input schema before sending. Useful in development to catch errors early. Default: `false`.\n- **`validateResponse`** — When `true`, incoming response bodies are validated against the Lexicon output schema. Disabling this can improve performance when you trust the upstream service. Default: `true`.\n- **`strictResponseProcessing`** — When `true` (default), the client will strictly process responses according to Lex encoding rules, rejecting responses containing invalid Lex data (e.g. floating-point numbers, malformed `$bytes` or `$link` objects). When `false`, the client accepts such responses in a lenient mode: invalid values are returned as-is rather than being rejected or converted, `datetime` string format checks become more lenient (e.g. datetimes without timezones are accepted) while other string formats remain strict, blob MIME type and size constraints are not enforced, and legacy blob references are coerced into standard `BlobRef` objects. Default: `true`.\n\n### Core Methods\n\n#### `client.call()`\n\nCall procedures or queries defined in Lexicons.\n\n```typescript\nimport * as app from './lexicons/app.js'\n\n// Query (GET request)\nconst profile = await client.call(app.bsky.actor.getProfile, {\n  actor: 'pfrazee.com',\n})\n\n// Procedure (POST request)\nconst result = await client.call(app.bsky.feed.sendInteractions, {\n  interactions: [\n    /* ... */\n  ],\n})\n\n// With options\nconst timeline = await client.call(\n  app.bsky.feed.getTimeline,\n  {\n    limit: 50,\n  },\n  {\n    signal: abortSignal,\n  },\n)\n```\n\n#### `client.create()`\n\nCreate a new record un the authenticated user's repo.\n\n```typescript\nimport { l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\nconst result = await client.create(app.bsky.feed.post, {\n  text: 'Hello, world!',\n  createdAt: l.toDatetimeString(new Date()),\n})\n\nconsole.log(result.uri) // at://did:plc:...\nconsole.log(result.cid)\n```\n\nOptions:\n\n- `rkey` - Custom record key (auto-generated if not provided)\n- `validate` - Validate record against schema before creating\n- `swapCommit` - CID for optimistic concurrency control\n\n#### `client.get()`\n\nRetrieve a record.\n\n```typescript\nimport * as app from './lexicons/app.js'\n\n// No need to specify the \"rkey\" for records with literal keys (e.g. profile)\nconst profile = await client.get(app.bsky.actor.profile)\n\nconsole.log(profile.displayName)\nconsole.log(profile.description)\n```\n\nFor records with non-literal keys:\n\n```typescript\nconst post = await client.get(app.bsky.feed.post, {\n  rkey: '3jxf7z2k3q2',\n})\n```\n\n#### `client.put()`\n\nUpdate an existing record.\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nawait client.put(app.bsky.actor.profile, {\n  displayName: 'New Name',\n  description: 'Updated bio',\n})\n```\n\nOptions:\n\n- `rkey` - Record key (required for non-literal keys)\n- `swapCommit` - Expected repo commit CID\n- `swapRecord` - Expected record CID\n\n#### `client.delete()`\n\nDelete a record.\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nawait client.delete(app.bsky.feed.post, {\n  rkey: '3jxf7z2k3q2',\n})\n```\n\n#### `client.list()`\n\nList records in a collection.\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nconst result = await client.list(app.bsky.feed.post, {\n  limit: 50,\n  reverse: true,\n})\n\nfor (const record of result.records) {\n  console.log(record.uri, record.value.text)\n}\n\n// Pagination\nif (result.cursor) {\n  const nextPage = await client.list(app.bsky.feed.post, {\n    cursor: result.cursor,\n    limit: 50,\n  })\n}\n```\n\n### Error Handling\n\nBy default, all client methods throw errors when requests fail. For more ergonomic error handling, the client provides \"Safe\" variants that return errors instead of throwing them.\n\n#### Safe Methods\n\nThe `xrpcSafe()` method catches errors and returns them as part of the result type instead of throwing:\n\n#### XrpcFailure Type\n\nThe `xrpcSafe()` method returns a union type that includes the success case (`XrpcResponse`) and failure cases (`XrpcFailure`):\n\n```typescript\nimport {\n  Client,\n  XrpcResponseError,\n  XrpcUpstreamError,\n  XrpcInternalError,\n} from '@atproto/lex'\nimport * as com from './lexicons/com.js'\n\nconst client = new Client(session)\n\n// Using a safe method\nconst result = await client.xrpcSafe(com.atproto.identity.resolveHandle, {\n  params: { handle: 'alice.bsky.social' },\n})\n\nif (result.success) {\n  // Handle success\n  console.log(result.body)\n} else {\n  // Handle failure - result is an XrpcFailure\n  if (result instanceof XrpcResponseError) {\n    // The server returned a valid XRPC error response\n    result.error // string (e.g. \"HandleNotFound\", \"AuthenticationRequired\", etc.)\n    result.message // string\n    result.response.status // number\n    result.response.headers // Headers\n    result.payload // { body: { error: string, message?: string }; encoding: string }\n  } else if (result instanceof XrpcUpstreamError) {\n    // The response was not a valid XRPC response (e.g. malformed JSON,\n    // data does not match schema, connection dropped)\n    result.error // \"UpstreamFailure\"\n    result.message // string\n    result.response.status // number\n    result.response.headers // Headers\n    result.payload // null | { body: unknown; encoding: string }\n  } else if (result instanceof XrpcInternalError) {\n    // Something went wrong on the client side (network error, etc.)\n    result.error // \"InternalServerError\"\n    result.message // string\n  }\n\n  // All XrpcFailure types have these properties:\n  result.shouldRetry() // boolean - whether the error is transient\n\n  if (result.matchesSchemaErrors()) {\n    // Check if the error matches a declared error in the schema.\n    // TypeScript knows this is a declared error for the method.\n    result.error // \"HandleNotFound\"\n  }\n}\n```\n\nThe `XrpcFailure<M>` type is a union of three error classes:\n\n1. **`XrpcResponseError`** - The server returned a valid XRPC error response (non-2xx with proper error payload)\n\n2. **`XrpcUpstreamError`** - The response was invalid or unprocessable (malformed JSON, schema mismatch, incomplete response)\n\n3. **`XrpcInternalError`** - Client-side errors (network failures, timeouts, etc.)\n\n### Authentication Methods\n\n#### `client.did`\n\nGet the authenticated user's DID.\n\n```typescript\nconst did = client.did // Returns Did | undefined\n```\n\n#### `client.assertAuthenticated()`\n\nAssert that the client is authenticated (throws if not).\n\n```typescript\nclient.assertAuthenticated()\n// After this call, TypeScript knows client.did is defined\nconst did = client.did // Type: Did (not undefined)\n```\n\n#### `client.assertDid`\n\nGet the authenticated user's DID, asserting that the client is authenticated.\n\n```typescript\nconst did = client.assertDid // Type: Did (throws if not authenticated)\n```\n\nThis is equivalent to calling `client.assertAuthenticated()` followed by accessing `client.did`, but provides a more concise way to get the DID when you know authentication is required.\n\n### Labeler Configuration\n\nConfigure content labelers for moderation.\n\n```typescript\nimport { Client } from '@atproto/lex'\n\n// Global app-level labelers\nClient.configure({\n  appLabelers: ['did:plc:labeler1', 'did:plc:labeler2'],\n})\n\n// Client-specific labelers\nconst client = new Client(session, {\n  labelers: ['did:plc:labeler3'],\n})\n\n// Add labelers dynamically\nclient.addLabelers(['did:plc:labeler4'])\n\n// Replace all labelers\nclient.setLabelers(['did:plc:labeler5'])\n\n// Clear labelers\nclient.clearLabelers()\n```\n\n### Low-Level XRPC\n\nFor advanced use cases, use `client.xrpc()` to get the full response (headers, status, body):\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nconst response = await client.xrpc(app.bsky.feed.getTimeline, {\n  params: { limit: 50 },\n  signal: abortSignal,\n  headers: { 'custom-header': 'value' },\n})\n\nconsole.log(response.status)\nconsole.log(response.headers)\nconsole.log(response.body)\n```\n\nValidation and strictness options (`validateRequest`, `validateResponse`, `strictResponseProcessing`) can also be passed per-call to override the client defaults:\n\n```typescript\nconst response = await client.xrpc(app.bsky.feed.getTimeline, {\n  params: { limit: 50 },\n  strictResponseProcessing: false, // Accept non-strict Lex data for this call\n  validateResponse: false, // Skip schema validation for this call\n})\n```\n\n## Utilities\n\nVarious utilities for working with CIDs, datetime strings, string lengths, language tags, and low-level JSON encoding are exported from the package:\n\n```typescript\nimport {\n  // CID utilities\n  parseCid, // Parse CID string (throws on invalid)\n  ifCid, // Coerce to Cid or null\n  isCid, // Type guard for Cid values\n\n  // Datetime string utilities\n  toDatetimeString, // Convert Date to DatetimeString (throws on invalid)\n  asDatetimeString, // Cast string to DatetimeString (throws on invalid)\n  isDatetimeString, // Type guard for DatetimeString\n  ifDatetimeString, // Returns DatetimeString or undefined\n\n  // Blob references\n  BlobRef, // { $type: 'blob', ref: Cid, mimeType: string, size: number }\n  isBlobRef, // Type guard for BlobRef objects\n\n  // Equality\n  lexEquals, // Deep equality (handles CIDs and bytes)\n\n  // String length for Lexicon validation\n  graphemeLen, // Count user-perceived characters\n  utf8Len, // Count UTF-8 bytes\n\n  // Language tag validation (BCP-47)\n  isLanguageString, // Validate language tags (e.g., 'en', 'pt-BR')\n\n  // Low-level JSON encoding helpers\n  parseLexLink, // { $link: string } → Cid\n  encodeLexLink, // Cid → { $link: string }\n  parseLexBytes, // { $bytes: string } → Uint8Array\n  encodeLexBytes, // Uint8Array → { $bytes: string }\n} from '@atproto/lex'\n\nconst cid = parseCid('bafyreiabc...')\ngraphemeLen('👨‍👩‍👧‍👦') // 1\nutf8Len('👨‍👩‍👧‍👦') // 25\nisLanguageString('en-US') // true\n```\n\n### Datetime Strings\n\nMany AT Protocol records (such as posts, likes, and follows) include a `createdAt` field that expects a valid `DatetimeString`. While `new Date().toISOString()` produces a string that looks like a valid datetime, it is not guaranteed to always conform to the AT Protocol's [datetime format requirements](https://atproto.com/specs/lexicon#datetime) (for example, `Date` objects representing dates before year 10 or after year 9999 will produce non-conforming strings). To ensure correctness and type safety, use the `DatetimeString` utilities exported from `@atproto/lex`:\n\n- **`toDatetimeString(date: Date)`** - Converts a `Date` object into a valid `DatetimeString`, throwing an `InvalidDatetimeError` if the date cannot be represented as a valid AT Protocol datetime.\n- **`asDatetimeString(input: string)`** - Validates and casts an arbitrary string to `DatetimeString`, throwing an `InvalidDatetimeError` if the string does not conform.\n- **`isDatetimeString(input)`** - Type guard that returns `true` if the input is a valid `DatetimeString`.\n- **`ifDatetimeString(input)`** - Returns the input as a `DatetimeString` if valid, or `undefined` otherwise.\n- **`currentDatetimeString()`** - Returns the current date and time as `DatetimeString`.\n\n```typescript\nimport { l } from '@atproto/lex'\n\n// Convert a Date object to a DatetimeString (or throws)\nconst someDate = new Date('2024-01-15T12:30:00Z')\nconst now = l.toDatetimeString(someDate)\n\n// Get the current datetime as a DatetimeString\nconst now = l.currentDatetimeString()\n\n// Validate and cast an existing string\nconst dt = l.asDatetimeString('2024-01-15T12:30:00.000Z')\n\n// Type guard for conditional checks\nif (l.isDatetimeString(someString)) {\n  // someString is now typed as DatetimeString\n}\n```\n\n## Advanced Usage\n\n### Workflow Integration\n\n#### Development Workflow\n\nAdd these scripts to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"update-lexicons\": \"lex install --update --save\",\n    \"postinstall\": \"lex install --ci\",\n    \"prebuild\": \"lex build\",\n    \"build\": \"# Your build command here\"\n  }\n}\n```\n\nThis ensures that:\n\n1. Lexicons are verified against the manifest after every `npm install` or `pnpm install`.\n2. TypeScript schemas are built before your project is built.\n3. You can easily update lexicons with `npm run update-lexicons` or `pnpm update-lexicons`.\n\n### Tree-Shaking\n\nThe generated TypeScript is optimized for tree-shaking. Import only what you need:\n\n```typescript\n// Import specific methods\nimport { post } from './lexicons/app/bsky/feed/post.js'\nimport { getProfile } from './lexicons/app/bsky/actor/getProfile.js'\n\n// Or use namespace imports (still tree-shakeable)\nimport * as app from './lexicons/app.js'\n```\n\nFor library authors, use `--pure-annotations` when building:\n\n```bash\nlex build --pure-annotations\n```\n\nThis will make the generated code more easily tree-shakeable from places that import your library.\n\n### Blob references\n\nIn AT Protocol, binary data (blobs) are referenced using `BlobRef`, which include metadata like MIME type and size. These references are what allow PDSs to determine which binary data (\"files\") is referenced by records.\n\n```typescript\nimport { BlobRef, isBlobRef } from '@atproto/lex'\n\nconst blobRef: BlobRef = {\n  $type: 'blob',\n  ref: parseCid('bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'),\n  mimeType: 'image/png',\n  size: 12345,\n}\n\nif (isBlobRef(blobRef)) {\n  console.log('Valid BlobRef:', blobRef.mimeType, blobRef.size)\n}\n```\n\n> [!NOTE]\n>\n> Historically, references to blobs were represented as simple objects with the following structure:\n>\n> ```typescript\n> type LegacyBlobRef = {\n>   cid: string\n>   mimeType: string\n> }\n> ```\n>\n> These should no longer be used for new records, but existing records using this format might still be encountered. To handle legacy blob references when validating data, enable the `--allowLegacyBlobs` flag when generating TypeScript schemas with `lex build`. You can use `isLegacyBlobRef()` from `@atproto/lex` to discriminate legacy blob references.\n>\n> When using non-strict validation (e.g. `$safeParse(data, { strict: false })`), legacy blob references are automatically coerced into standard `BlobRef` objects with `size: -1`, even without `--allowLegacyBlobs`.\n\n### Actions\n\nActions are composable functions that combine multiple XRPC calls into higher-level operations. They can be invoked using `client.call()` just like Lexicon methods, making them a powerful tool for building library-style APIs on top of the low-level client.\n\n#### What are Actions?\n\nAn `Action` is a function with this signature:\n\n```typescript\ntype Action<Input, Output> = (\n  client: Client,\n  input: Input,\n  options: CallOptions,\n) => Output | Promise<Output>\n```\n\nActions receive:\n\n- `client` - The Client instance (to make XRPC calls)\n- `input` - The input data for the action\n- `options` - Call options (signal)\n\n#### Using Actions\n\nActions are called using `client.call()`, the same method used for XRPC queries and procedures:\n\n```typescript\nimport { Action, Client, l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\n// Define an action\nexport const likePost: Action<\n  { uri: string; cid: string },\n  { uri: string; cid: string }\n> = async (client, { uri, cid }, options) => {\n  client.assertAuthenticated()\n\n  const result = await client.create(\n    app.bsky.feed.like,\n    {\n      subject: { uri, cid },\n      createdAt: l.toDatetimeString(new Date()),\n    },\n    options,\n  )\n\n  return result\n}\n\n// Use the action\nconst client = new Client(session)\nconst like = await client.call(likePost, {\n  uri: 'at://did:plc:abc/app.bsky.feed.post/123',\n  cid: 'bafyreiabc...',\n})\n```\n\n#### Composing Multiple Operations\n\nActions excel at combining multiple XRPC calls:\n\n```typescript\nimport { Action, Client } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\ntype Preference = app.bsky.actor.defs.Preferences[number]\n\n// Action that reads, modifies, and writes preferences\nconst upsertPreference: Action<Preference, Preference[]> = async (\n  client,\n  newPref,\n  options,\n) => {\n  // Read current preferences\n  const { preferences } = await client.call(\n    app.bsky.actor.getPreferences,\n    options,\n  )\n\n  // Update the preference list\n  const updated = [\n    ...preferences.filter((p) => p.$type !== newPref.$type),\n    newPref,\n  ]\n\n  // Save updated preferences\n  await client.call(\n    app.bsky.actor.putPreferences,\n    { preferences: updated },\n    options,\n  )\n\n  return updated\n}\n\n// Use it\nawait client.call(\n  upsertPreference,\n  app.bsky.actor.defs.adultContentPref.build({ enabled: true }),\n)\n```\n\n#### Higher-Order Actions\n\nActions can call other actions, enabling powerful composition:\n\n```typescript\nimport { Action } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\ntype Preference = app.bsky.actor.defs.Preferences[number]\n\n// Low-level action: update preferences with a function\nconst updatePreferences: Action<\n  (prefs: Preference[]) => Preference[] | false,\n  Preference[]\n> = async (client, updateFn, options) => {\n  const { preferences } = await client.call(\n    app.bsky.actor.getPreferences,\n    options,\n  )\n\n  const updated = updateFn(preferences)\n  if (updated === false) return preferences\n\n  await client.call(\n    app.bsky.actor.putPreferences,\n    { preferences: updated },\n    options,\n  )\n\n  return updated\n}\n\n// Higher-level action: upsert a specific preference\nconst upsertPreference: Action<Preference, Preference[]> = async (\n  client,\n  pref,\n  options,\n) => {\n  return updatePreferences(\n    client,\n    (prefs) => [...prefs.filter((p) => p.$type !== pref.$type), pref],\n    options,\n  )\n}\n\n// Even higher-level: enable adult content\nconst enableAdultContent: Action<void, Preference[]> = async (\n  client,\n  _,\n  options,\n) => {\n  return upsertPreference(\n    client,\n    app.bsky.actor.defs.adultContentPref.build({ enabled: true }),\n    options,\n  )\n}\n\n// Use the high-level action\nawait client.call(enableAdultContent)\n```\n\n### Creating a Client from Another Client\n\nYou can create a new `Client` instance from an existing client. The new client will share the same underlying configuration (authentication, headers, labelers, service proxy), with the ability to override specific settings.\n\n> [!NOTE]\n>\n> When you create a client from another client, the child client inherits the base client's configuration. On every request, the child client merges its own configuration with the base client's current configuration, with the child's settings taking precedence. Changes to the base client's configuration (like `baseClient.setLabelers()`) will be reflected in child client requests, but changes to child clients do not affect the base client.\n\n```typescript\nimport { Client } from '@atproto/lex'\n\n// Base client with authentication\nconst baseClient = new Client(session)\n\nbaseClient.setLabelers(['did:plc:labelerA', 'did:plc:labelerB'])\nbaseClient.headers.set('x-app-version', '1.0.0')\n\n// Create a new client with additional configuration that will get merged with\n// baseClient's settings on every request.\nconst configuredClient = new Client(baseClient, {\n  labelers: ['did:plc:labelerC'],\n  headers: { 'x-trace-id': 'abc123' },\n})\n```\n\nThis pattern is particularly useful when you need to:\n\n- Configure labelers after authentication\n- Add application-specific headers\n- Create multiple clients with different configurations from the same session\n\n**Example: Configuring labelers after sign-in**\n\n```typescript\nimport { Client } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\nasync function createBaseClient(session: OAuthSession) {\n  // Create base client\n  const client = new Client(session, {\n    service: 'did:web:api.bsky.app#bsky_appview',\n  })\n\n  // Fetch user preferences\n  const { preferences } = await client.call(app.bsky.actor.getPreferences)\n\n  // Extract labeler preferences\n  const labelerPref = preferences.findLast((p) =>\n    app.bsky.actor.defs.labelersPref.check(p),\n  )\n  const labelers = labelerPref?.labelers.map((l) => l.did) ?? []\n\n  // Configure the client with the user's preferred labelers\n  client.setLabelers(labelers)\n\n  return client\n}\n\n// Usage\nconst baseClient = await createBaseClient(session)\n\n// Create a new client with a different service, but reusing the labelers\n// from the base client.\nconst otherClient = new Client(baseClient, {\n  service: 'did:web:com.example.other#other_service',\n})\n\n// Whenever you update labelers on the base client, the other client will automatically\n// receive the same updates, since they share the same labeler set.\n```\n\n### Building Library-Style APIs with Actions\n\nActions enable you to create high-level, convenience APIs similar to [@atproto/api](https://www.npmjs.com/package/@atproto/api)'s `Agent` class. Here are patterns for common operations:\n\n#### Creating Posts\n\n```typescript\nimport { Action, l } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\ntype PostInput = Partial<app.bsky.feed.post.Main> &\n  Omit<app.bsky.feed.post.Main, 'createdAt'>\n\nexport const post: Action<PostInput, { uri: string; cid: string }> = async (\n  client,\n  record,\n  options,\n) => {\n  return client.create(\n    app.bsky.feed.post,\n    {\n      ...record,\n      createdAt: record.createdAt || l.currentDatetimeString(),\n    },\n    options,\n  )\n}\n\n// Usage\nawait client.call(post, {\n  text: 'Hello, AT Protocol!',\n  langs: ['en'],\n})\n```\n\n#### Following Users\n\n```typescript\nimport { Action, l } from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport * as app from './lexicons/app.js'\n\nexport const follow: Action<\n  { did: string },\n  { uri: string; cid: string }\n> = async (client, { did }, options) => {\n  return client.create(\n    app.bsky.graph.follow,\n    {\n      subject: did,\n      createdAt: l.currentDatetimeString(),\n    },\n    options,\n  )\n}\n\nexport const unfollow: Action<{ followUri: string }, void> = async (\n  client,\n  { followUri },\n  options,\n) => {\n  const uri = new AtUri(followUri)\n  await client.delete(app.bsky.graph.follow, {\n    ...options,\n    rkey: uri.rkey,\n  })\n}\n\n// Usage\nconst { uri } = await client.call(follow, { did: 'did:plc:abc123' })\nawait client.call(unfollow, { followUri: uri })\n```\n\n#### Updating Profile with Retry Logic\n\n```typescript\nimport { Action, XrpcResponseError } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\nimport * as com from './lexicons/com.js'\n\ntype ProfileUpdate = Partial<Omit<app.bsky.actor.profile.Main, '$type'>>\n\nexport const updateProfile: Action<ProfileUpdate, void> = async (\n  client,\n  updates,\n  options,\n) => {\n  const maxRetries = 5\n  for (let attempt = 0; ; attempt++) {\n    try {\n      // Get current profile and its CID\n      const res = await client.xrpc(com.atproto.repo.getRecord, {\n        ...options,\n        params: {\n          repo: client.assertDid,\n          collection: 'app.bsky.actor.profile',\n          rkey: 'self',\n        },\n      })\n\n      const current = app.bsky.actor.profile.main.validate(res.body.record)\n\n      // Merge updates with current profile (if valid)\n      const updated = app.bsky.actor.profile.main.build({\n        ...(current.success ? current.value : undefined),\n        ...updates,\n      })\n\n      // Save with optimistic concurrency control\n      await client.put(app.bsky.actor.profile, updated, {\n        ...options,\n        swapRecord: res?.body.cid ?? null,\n      })\n\n      return\n    } catch (error) {\n      // Retry on swap/concurrent modification errors\n      if (\n        error instanceof XrpcResponseError &&\n        error.name === 'SwapError' &&\n        attempt < maxRetries - 1\n      ) {\n        continue\n      }\n\n      throw error\n    }\n  }\n}\n\n// Usage\nawait client.call(updateProfile, {\n  displayName: 'Alice',\n  description: 'Software engineer',\n})\n```\n\n#### Packaging Actions as a Library\n\nCreate a collection of actions for your application:\n\n```typescript\n// actions.ts\nimport { Action, Client } from '@atproto/lex'\nimport * as app from './lexicons/app.js'\n\nexport const post: Action</* ... */> = async (client, input, options) => {\n  /* ... */\n}\nexport const like: Action</* ... */> = async (client, input, options) => {\n  /* ... */\n}\nexport const follow: Action</* ... */> = async (client, input, options) => {\n  /* ... */\n}\nexport const updateProfile: Action</* ... */> = async (\n  client,\n  input,\n  options,\n) => {\n  /* ... */\n}\n```\n\nUsage:\n\n```typescript\nimport * as actions from './actions.js'\n\nawait client.call(actions.post, { text: 'Hello!' })\n```\n\n#### Best Practices for Actions\n\n1. **Type Safety**: Always provide explicit type parameters for `Action<Input, Output>`\n2. **Authentication**: Use `client.assertAuthenticated()` when auth is required\n3. **Abort Signals**: Check `options.signal?.throwIfAborted()` between long operations\n4. **Composition**: Build complex actions from simpler ones\n5. **Retries**: Implement retry logic for operations with optimistic concurrency control\n6. **Tree-shaking**: Export actions individually to allow tree-shaking (instead of bundling them in a single class)\n\n### Standard Schema Compatibility\n\nAll generated schemas implement the [Standard Schema](https://standardschema.dev/) interface (`StandardSchemaV1`), which means they can be used with any library or framework that supports Standard Schema, such as form validation libraries, API frameworks, and more.\n\nEvery `Schema` instance exposes a `~standard` property conforming to the spec:\n\n```typescript\nimport * as app from './lexicons/app.js'\n\n// Use with any Standard Schema-compatible library\nconst schema = app.bsky.feed.post\n\nschema['~standard'].version // 1\nschema['~standard'].vendor // '@atproto/lex-schema'\n\n// Validate using the Standard Schema interface\nconst result = schema['~standard'].validate(someData)\n\nif ('value' in result) {\n  console.log(result.value) // Parsed and validated data\n} else {\n  console.error(result.issues)\n}\n```\n\nWhen validated through the Standard Schema interface, schemas operate in \"parse\" mode, meaning transformations like defaults and coercions are applied to the output.\n\n## License\n\nMIT or Apache2\n"
  },
  {
    "path": "packages/lex/lex/bin/lex",
    "content": "#!/usr/bin/env node\n\n/* eslint-env node */\n/* eslint-disable @typescript-eslint/no-var-requires */\n\n// This file is referenced by the \"bin\" field in package.json. Because of that,\n// we need this file to exist on disk even before the project is built, so it is\n// written in plain JS. This allows package managers to properly link the CLI\n// command when the monorepo is being setup (during initial \"pnpm install\").\n\nconst yargs = require('yargs')\nconst { hideBin } = require('yargs/helpers')\nconst { build } = require('@atproto/lex-builder')\nconst { install } = require('@atproto/lex-installer')\n\nyargs(hideBin(process.argv))\n  .strict()\n  .command(\n    ['build', 'b'],\n    'Generate TypeScript lexicon schema files from JSON lexicon definitions',\n    (yargs) => {\n      return yargs.strict().options({\n        lexicons: {\n          type: 'string',\n          demandOption: true,\n          default: './lexicons',\n          describe: 'directory containing lexicon JSON files',\n        },\n        out: {\n          type: 'string',\n          demandOption: true,\n          default: './src/lexicons',\n          describe: 'output directory for generated TS files',\n        },\n        clear: {\n          type: 'boolean',\n          default: false,\n          describe: 'clear output directory before generating files',\n        },\n        override: {\n          type: 'boolean',\n          default: false,\n          describe: 'override existing files (has no effect with --clear)',\n        },\n        pretty: {\n          type: 'boolean',\n          default: true,\n          describe: 'run prettier on generated files',\n        },\n        'ignore-errors': {\n          type: 'boolean',\n          default: false,\n          describe: 'how to handle errors when processing input files',\n        },\n        'pure-annotations': {\n          type: 'boolean',\n          default: false,\n          describe:\n            'adds `/*#__PURE__*/` annotations for tree-shaking tools. Set this to true if you are using generated lexicons in a library.',\n        },\n        exclude: {\n          array: true,\n          type: 'string',\n          describe:\n            'list of strings or regex patterns to exclude lexicon documents by their IDs',\n        },\n        include: {\n          array: true,\n          type: 'string',\n          describe:\n            'list of strings or regex patterns to include lexicon documents by their IDs',\n        },\n        lib: {\n          type: 'string',\n          default: '@atproto/lex',\n          describe:\n            'package name of the library to import the lex schema utility \"l\" from',\n        },\n        allowLegacyBlobs: {\n          type: 'boolean',\n          default: false,\n          describe:\n            'generate schemas that accept legacy blob references (disabled by default; enable this if you encounter issues while processing records created a long time ago)',\n        },\n        importExt: {\n          type: 'string',\n          default: '.js',\n          describe:\n            'file extension to use for import statements in generated files (e.g. \".ts\", \".mts\", \".cts\"). Use --import-ext \"\" to generate extension-less imports.',\n        },\n        fileExt: {\n          type: 'string',\n          default: '.ts',\n          describe:\n            'file extension to use for generated files (e.g. \".ts\", \".mts\", \".cts\")',\n        },\n        indexFile: {\n          type: 'boolean',\n          default: false,\n          describe:\n            'generate an \"index.<fileExt>\" file that exports all root-level namespaces',\n        },\n        ignoreInvalidLexicons: {\n          type: 'boolean',\n          default: false,\n          describe:\n            'skip over invalid lexicon files instead of exiting with an error',\n        },\n      })\n    },\n    async (argv) => {\n      await build(argv)\n    },\n  )\n  .command(\n    ['install [nsid..]', 'i [nsid..]'],\n    'Fetch and install lexicon documents',\n    (yargs) => {\n      return yargs.strict().options({\n        manifest: {\n          type: 'string',\n          default: './lexicons.json',\n          describe: 'path to lexicons.json manifest file',\n        },\n        save: {\n          alias: 's',\n          type: 'boolean',\n          default: true,\n          describe:\n            'Updates lexicons.json with installed lexicons (use --no-save to disable)',\n        },\n        update: {\n          type: 'boolean',\n          default: false,\n          describe:\n            'update all installed lexicons to their latest versions by re-resolving and re-installing them',\n        },\n        ci: {\n          type: 'boolean',\n          default: false,\n          describe:\n            'error if the installed lexicons do not match the CIDs in the lexicons.json manifest',\n        },\n        lexicons: {\n          type: 'string',\n          demandOption: true,\n          default: './lexicons',\n          describe: 'directory containing lexicon JSON files',\n        },\n      })\n    },\n    async (argv) => {\n      await install({\n        add: argv.nsid,\n        save: argv.save,\n        ci: argv.ci,\n        update: argv.update,\n        lexicons: argv.lexicons,\n        manifest: argv.manifest,\n      })\n    },\n  )\n  .parseAsync()\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/4-2/unsafeDefs.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.4-2.unsafeDefs\",\n  \"defs\": {\n    \"ob-je-c$t\": {\n      \"type\": \"object\",\n      \"properties\": {}\n    },\n    \"ff-ri-n.g\": { \"type\": \"string\" },\n    \"9\": { \"type\": \"integer\", \"enum\": [9] }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/arrayLength.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.arrayLength\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"array\": {\n            \"type\": \"array\",\n            \"minLength\": 2,\n            \"maxLength\": 4,\n            \"items\": { \"type\": \"integer\" }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/atIdentifier.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.atIdentifier\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"atIdentifier\": { \"type\": \"string\", \"format\": \"at-identifier\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/atUri.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.atUri\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"atUri\": { \"type\": \"string\", \"format\": \"at-uri\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/boolConst.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.boolConst\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"boolean\": {\n            \"type\": \"boolean\",\n            \"const\": false\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/byteLength.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.byteLength\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"bytes\": {\n            \"type\": \"bytes\",\n            \"minLength\": 2,\n            \"maxLength\": 4\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/cid.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.cid\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"cid\": { \"type\": \"string\", \"format\": \"cid\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/datetime.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.datetime\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"datetime\": { \"type\": \"string\", \"format\": \"datetime\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/default.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.default\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"boolean\"],\n        \"properties\": {\n          \"boolean\": { \"type\": \"boolean\", \"default\": false },\n          \"integer\": { \"type\": \"integer\", \"default\": 0 },\n          \"string\": { \"type\": \"string\", \"default\": \"\" },\n          \"object\": { \"type\": \"ref\", \"ref\": \"#object\" }\n        }\n      }\n    },\n    \"object\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"boolean\": { \"type\": \"boolean\", \"default\": true },\n        \"integer\": { \"type\": \"integer\", \"default\": 1 },\n        \"string\": { \"type\": \"string\", \"default\": \"x\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/did.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.did\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"did\": { \"type\": \"string\", \"format\": \"did\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/handle.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.handle\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"handle\": { \"type\": \"string\", \"format\": \"handle\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/integerConst.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.integerConst\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"integer\": {\n            \"type\": \"integer\",\n            \"const\": 0\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/integerEnum.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.integerEnum\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"integer\": {\n            \"type\": \"integer\",\n            \"enum\": [1, 2]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/integerRange.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.integerRange\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"integer\": {\n            \"type\": \"integer\",\n            \"minimum\": 2,\n            \"maximum\": 4\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/kitchenSink.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.kitchenSink\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A record\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\n          \"object\",\n          \"array\",\n          \"boolean\",\n          \"integer\",\n          \"string\",\n          \"bytes\",\n          \"cidLink\"\n        ],\n        \"properties\": {\n          \"object\": { \"type\": \"ref\", \"ref\": \"#object\" },\n          \"array\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"boolean\": { \"type\": \"boolean\" },\n          \"integer\": { \"type\": \"integer\" },\n          \"string\": { \"type\": \"string\" },\n          \"bytes\": { \"type\": \"bytes\" },\n          \"cidLink\": { \"type\": \"cid-link\" }\n        }\n      }\n    },\n    \"object\": {\n      \"type\": \"object\",\n      \"required\": [\"object\", \"array\", \"boolean\", \"integer\", \"string\"],\n      \"properties\": {\n        \"object\": { \"type\": \"ref\", \"ref\": \"#subobject\" },\n        \"array\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"boolean\": { \"type\": \"boolean\" },\n        \"integer\": { \"type\": \"integer\" },\n        \"string\": { \"type\": \"string\" }\n      }\n    },\n    \"subobject\": {\n      \"type\": \"object\",\n      \"required\": [\"boolean\"],\n      \"properties\": {\n        \"boolean\": { \"type\": \"boolean\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/language.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.language\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"language\": { \"type\": \"string\", \"format\": \"language\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/nsid.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.nsid\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"nsid\": { \"type\": \"string\", \"format\": \"nsid\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/optional.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.optional\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"object\": {\n            \"type\": \"ref\",\n            \"ref\": \"com.example.kitchenSink#object\"\n          },\n          \"array\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"boolean\": { \"type\": \"boolean\" },\n          \"integer\": { \"type\": \"integer\" },\n          \"string\": { \"type\": \"string\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/parametersEnum.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.parametersEnum\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"booleanCst\", \"integerCst\"],\n        \"properties\": {\n          \"booleanCst\": { \"type\": \"boolean\", \"const\": true },\n          \"integerCst\": { \"type\": \"integer\", \"const\": 42 },\n          \"integerEnum\": { \"type\": \"integer\", \"enum\": [1, 2, 3] },\n          \"stringCst\": { \"type\": \"string\", \"const\": \"foo\" },\n          \"stringEnum\": { \"type\": \"string\", \"enum\": [\"foo\", \"bar\", \"baz\"] },\n\n          \"arrayFalse\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"boolean\", \"const\": false }\n          },\n          \"arrayIntCst\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"integer\", \"const\": 42 }\n          },\n          \"arrayIntEnum\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"integer\", \"enum\": [4, 5, 6] }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/procedure.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.procedure\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"description\": \"A procedure\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"boolean\", \"integer\"],\n        \"properties\": {\n          \"boolean\": { \"type\": \"boolean\" },\n          \"integer\": { \"type\": \"integer\" },\n          \"string\": { \"type\": \"string\" },\n          \"array\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        }\n      },\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": { \"type\": \"ref\", \"ref\": \"com.example.kitchenSink#object\" }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": { \"type\": \"ref\", \"ref\": \"com.example.kitchenSink#object\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/procedureKnownValues.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.procedureKnownValues\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"procedure\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"properties\": {\n          \"status\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"active\", \"inactive\"]\n          }\n        }\n      },\n      \"input\": {\n        \"encoding\": \"application/json\",\n        \"schema\": {\n          \"type\": \"object\",\n          \"required\": [\"role\"],\n          \"properties\": {\n            \"role\": {\n              \"type\": \"string\",\n              \"knownValues\": [\"admin\", \"user\"]\n            },\n            \"did\": {\n              \"type\": \"string\",\n              \"format\": \"did\",\n              \"knownValues\": [\"did:example:123\", \"did:example:456\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/query.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.query\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"query\",\n      \"description\": \"A query\",\n      \"parameters\": {\n        \"type\": \"params\",\n        \"required\": [\"boolean\", \"integer\"],\n        \"properties\": {\n          \"boolean\": { \"type\": \"boolean\" },\n          \"integer\": { \"type\": \"integer\" },\n          \"string\": { \"type\": \"string\" },\n          \"array\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"def\": { \"type\": \"integer\", \"default\": 0 }\n        }\n      },\n      \"output\": {\n        \"encoding\": \"application/json\",\n        \"schema\": { \"type\": \"ref\", \"ref\": \"com.example.kitchenSink#object\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/stringConst.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.stringConst\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"string\": {\n            \"type\": \"string\",\n            \"const\": \"a\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/stringEnum.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.stringEnum\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"string\": {\n            \"type\": \"string\",\n            \"enum\": [\"a\", \"b\"]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/stringKnownValues.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.stringKnownValues\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"myKey\": {\n            \"type\": \"string\",\n            \"knownValues\": [\"foo\", \"bar\"]\n          }\n        },\n        \"required\": [\"myKey\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/stringLength.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.stringLength\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"string\": {\n            \"type\": \"string\",\n            \"minLength\": 2,\n            \"maxLength\": 4\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/stringLengthGrapheme.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.stringLengthGrapheme\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"string\": {\n            \"type\": \"string\",\n            \"minGraphemes\": 2,\n            \"maxGraphemes\": 4\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/stringLengthNoMinLength.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.stringLengthNoMinLength\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"string\": {\n            \"type\": \"string\",\n            \"maxLength\": 4\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/subscription.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.subscription\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"subscription\",\n      \"description\": \"A subscription\",\n      \"message\": {\n        \"schema\": {\n          \"type\": \"union\",\n          \"refs\": [\"#message\", \"#params\"]\n        }\n      }\n    },\n    \"message\": {\n      \"type\": \"object\",\n      \"description\": \"A definition with a name (message) that might cause conflict with generated utility types for subscriptions\",\n      \"required\": [\"string\"],\n      \"properties\": {\n        \"string\": { \"type\": \"string\" }\n      }\n    },\n    \"params\": {\n      \"type\": \"object\",\n      \"description\": \"A definition with a name (params) that might cause conflict with generated utility types for methods\",\n      \"required\": [\"integer\"],\n      \"properties\": {\n        \"integer\": { \"type\": \"integer\" }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/token.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.token\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"token\",\n      \"description\": \"Main token definition\"\n    },\n    \"myToken\": {\n      \"type\": \"token\",\n      \"description\": \"A custom token\"\n    },\n    \"anotherToken\": {\n      \"type\": \"token\",\n      \"description\": \"Another custom token\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/union.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.union\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A record\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"unionOpen\", \"unionClosed\"],\n        \"properties\": {\n          \"unionOpen\": {\n            \"type\": \"union\",\n            \"refs\": [\n              \"com.example.kitchenSink#object\",\n              \"com.example.kitchenSink#subobject\"\n            ]\n          },\n          \"unionClosed\": {\n            \"type\": \"union\",\n            \"closed\": true,\n            \"refs\": [\n              \"com.example.kitchenSink#object\",\n              \"com.example.kitchenSink#subobject\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/unknown.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.unknown\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"description\": \"A record\",\n      \"key\": \"tid\",\n      \"record\": {\n        \"type\": \"object\",\n        \"required\": [\"unknown\"],\n        \"properties\": {\n          \"unknown\": { \"type\": \"unknown\" },\n          \"optUnknown\": { \"type\": \"unknown\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/lexicons/com/example/uri.json",
    "content": "{\n  \"lexicon\": 1,\n  \"id\": \"com.example.uri\",\n  \"defs\": {\n    \"main\": {\n      \"type\": \"record\",\n      \"key\": \"any\",\n      \"record\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"uri\": { \"type\": \"string\", \"format\": \"uri\" }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/package.json",
    "content": "{\n  \"name\": \"@atproto/lex\",\n  \"version\": \"0.0.22\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon tooling for AT\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"lex\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex\"\n  },\n  \"files\": [\n    \"./dist\",\n    \"./bin\",\n    \"./CHANGELOG.md\"\n  ],\n  \"bin\": {\n    \"ts-lex\": \"./bin/lex\",\n    \"lex\": \"./bin/lex\"\n  },\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-builder\": \"workspace:^\",\n    \"@atproto/lex-client\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-json\": \"workspace:^\",\n    \"@atproto/lex-installer\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\",\n    \"yargs\": \"^17.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/yargs\": \"^17.0.33\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"prebuild\": \"./bin/lex build --clear --lexicons ./lexicons --out ./tests/lexicons --lib @atproto/lex-schema -- ignore additional npm args\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/src/index.ts",
    "content": "/**\n * The `@atproto/lex` package provides utilities for working with ATProtocol\n * lexicons, including data types, JSON encoding/decoding, schema validation,\n * and HTTP client functionality.\n *\n * @packageDocumentation\n */\n\nexport {\n  /**\n   * The Client class is the primary interface for interacting with AT Protocol\n   * services though an authenticated session. It provides methods for making\n   * XRPC requests, handling records, and managing blobs.\n   */\n  Client,\n  /**\n   * The `xrpc` function is a low-level utility for making XRPC requests towards\n   * a specific service. It allows for detailed control over the request,\n   * including custom parameters, body, and headers. This function is useful for\n   * advanced use cases where the higher-level `Client` methods may not provide\n   * enough flexibility.\n   */\n  xrpc,\n  /**\n   * The `xrpcSafe` function is a wrapper around `xrpc` that provides additional\n   * safety checks and error handling. It ensures that the request is properly\n   * formed and that any errors are caught and handled gracefully. This function\n   * is recommended for most use cases, as it provides a safer interface for\n   * making XRPC requests.\n   */\n  xrpcSafe,\n} from '@atproto/lex-client'\nexport * from '@atproto/lex-client'\n\nexport {\n  /**\n   * The {@link l} namespace (from `@atproto/lex-schema`) provides an imperative API for building schemas:\n   *\n   * ### Primitive Types\n   * - {@link l.string | l.string()} - String values with optional format/length constraints\n   * - {@link l.integer | l.integer()} - Integer values with optional min/max constraints\n   * - {@link l.boolean | l.boolean()} - Boolean values\n   * - {@link l.bytes | l.bytes()} - Binary data (Uint8Array)\n   * - {@link l.cid | l.cid()} - Content Identifier values\n   * - {@link l.blob | l.blob()} - Blob references with mime type and size\n   *\n   * ### Composite Types\n   * - {@link l.object | l.object()} - Objects with defined property schemas\n   * - {@link l.array | l.array()} - Arrays with element type validation\n   * - {@link l.union | l.union()} - Union of multiple possible types\n   * - {@link l.ref | l.ref()} - Reference to another schema definition\n   * - {@link l.literal | l.literal()} - Literal constant values\n   * - {@link l.enum | l.enum()} - Enum of allowed string values\n   * - {@link l.typedRef | l.typedRef()} - Reference to a {@link l.typedObject | l.typedObject()}\n   * - {@link l.typedUnion | l.typedUnion()} - Discriminated union between multiple {@link l.typedRef | l.typedRef()} or {@link l.typedObject | l.typedObject()} types\n   *\n   * ### Modifiers\n   * - {@link l.optional | l.optional()} - Mark a property as optional\n   * - {@link l.nullable | l.nullable()} - Allow null values\n   * - {@link l.withDefault | l.withDefault()} - Provide a default value\n   *\n   * ### Lexicon Definitions\n   * - {@link l.typedObject | l.typedObject()} - Define a typed object with a `$type` property\n   * - {@link l.record | l.record()} - Define a Lexicon record type\n   * - {@link l.query | l.query()} - Define a Lexicon query method\n   * - {@link l.procedure | l.procedure()} - Define a Lexicon procedure method\n   * - {@link l.subscription | l.subscription()} - Define a Lexicon subscription method\n   */\n  l,\n} from '@atproto/lex-schema'\nexport * from '@atproto/lex-schema'\n\nexport {\n  /**\n   * The `LexMap` type represents an object with string keys and `LexValue` values.\n   * It is used to represent arbitrary objects in Lexicon schemas, where the\n   * properties are not predefined. This type allows for flexible data structures\n   * while still ensuring that all values conform to the `LexValue` type.\n   */\n  type LexMap,\n  /**\n   * The `LexValue` type represents any valid value that can be used in a\n   * Lexicon schema. It is a union of all the primitive and composite types\n   * defined in `@atproto/lex-data`, including strings, integers, booleans,\n   * bytes, CIDs, blob references, objects, arrays, and maps. This type is used\n   * throughout the library to represent data that conforms to Lexicon schemas.\n   */\n  type LexValue,\n} from '@atproto/lex-data'\nexport * from '@atproto/lex-data'\n\nexport {\n  /**\n   * The `jsonToLex` function takes a plain JavaScript object (typically parsed from\n   * JSON) and converts it back into a LexValue, reconstructing any complex types as needed. This is useful\n   * for processing data received from the network or loaded from JSON storage.\n   */\n  jsonToLex,\n  /**\n   * The `lexParse` function takes a JSON string and parses it into a LexValue. It\n   * performs the necessary conversions to reconstruct complex LexValue types from\n   * their JSON representations.\n   */\n  lexParse,\n  /**\n   * The `lexStringify` function takes a LexValue and serializes it to a JSON string.\n   * It handles the conversion of complex LexValue types (like BlobRef and Cid) into\n   * a JSON-friendly format.\n   */\n  lexStringify,\n  /**\n   * The `lexToJson` function converts a LexValue into a plain JavaScript object\n   * that can be safely serialized to JSON. This is useful for preparing data to be\n   * sent over the network or stored in a JSON format.\n   */\n  lexToJson,\n} from '@atproto/lex-json'\nexport * from '@atproto/lex-json'\n"
  },
  {
    "path": "packages/lex/lex/tests/array.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('array', () => {\n  it('Applies array length constraints', () => {\n    com.example.arrayLength.$parse({\n      $type: 'com.example.arrayLength',\n      array: [1, 2, 3],\n    })\n    expect(() =>\n      com.example.arrayLength.$parse({\n        $type: 'com.example.arrayLength',\n        array: [1],\n      }),\n    ).toThrow('array too small (minimum 2, got 1) at $.array')\n    expect(() =>\n      com.example.arrayLength.$parse({\n        $type: 'com.example.arrayLength',\n        array: [1, 2, 3, 4, 5],\n      }),\n    ).toThrow('array too big (maximum 4, got 5) at $.array')\n  })\n\n  it('Applies array item constraints', () => {\n    expect(() =>\n      com.example.arrayLength.$parse({\n        $type: 'com.example.arrayLength',\n        array: [1, '2', 3],\n      }),\n    ).toThrow('Expected integer value type (got string) at $.array[1]')\n    expect(() =>\n      com.example.arrayLength.$parse({\n        $type: 'com.example.arrayLength',\n        array: [1, undefined, 3],\n      }),\n    ).toThrow('Expected integer value type (got undefined) at $.array[1]')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/boolean.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('boolean', () => {\n  it('Applies boolean const constraint', () => {\n    com.example.boolConst.$parse({\n      $type: 'com.example.boolConst',\n      boolean: false,\n    })\n\n    expect(() =>\n      com.example.boolConst.$parse({\n        $type: 'com.example.boolConst',\n        boolean: true,\n      }),\n    ).toThrow('Expected false (got true) at $.boolean')\n\n    expect(() =>\n      com.example.boolConst.$parse({\n        $type: 'com.example.boolConst',\n        boolean: 'true',\n      }),\n    ).toThrow('Expected false (got \"true\") at $.boolean')\n\n    expect(() =>\n      com.example.boolConst.$parse({\n        $type: 'com.example.boolConst',\n        boolean: 1,\n      }),\n    ).toThrow('Expected false (got 1) at $.boolean')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/byte.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.byteLength', () => {\n  it('Applies bytes length constraints', () => {\n    com.example.byteLength.$parse({\n      $type: 'com.example.byteLength',\n      bytes: new Uint8Array([1, 2, 3]),\n    })\n    expect(() =>\n      com.example.byteLength.$parse({\n        $type: 'com.example.byteLength',\n        bytes: new Uint8Array([1]),\n      }),\n    ).toThrow('bytes too small (minimum 2, got 1) at $.bytes')\n    expect(() =>\n      com.example.byteLength.$parse({\n        $type: 'com.example.byteLength',\n        bytes: new Uint8Array([1, 2, 3, 4, 5]),\n      }),\n    ).toThrow('bytes too big (maximum 4, got 5) at $.bytes')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/defaults.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { l } from '@atproto/lex-schema'\nimport * as com from './lexicons/com.js'\n\ndescribe('defaults', () => {\n  it('Generates default properties', () => {\n    const result = com.example.default.$parse({\n      $type: 'com.example.default',\n      object: {},\n    })\n    expect(result).toStrictEqual({\n      $type: 'com.example.default',\n      boolean: false,\n      integer: 0,\n      string: '',\n      object: {\n        boolean: true,\n        integer: 1,\n        string: 'x',\n      },\n    })\n    expect(result).not.toHaveProperty('datetime')\n  })\n\n  it('properly handles defaults', () => {\n    const int = l.integer()\n    expect(int.safeParse(undefined).success).toBe(false)\n    expect(int.safeValidate(undefined).success).toBe(false)\n\n    const intOpt = l.optional(int)\n    expect(intOpt.parse(undefined)).toBe(undefined)\n    expect(intOpt.validate(undefined)).toBe(undefined)\n\n    const intOptOpt = l.optional(l.optional(int))\n    expect(intOptOpt.parse(undefined)).toBe(undefined)\n    expect(intOptOpt.validate(undefined)).toBe(undefined)\n\n    const intDef = l.withDefault(int, 42)\n    expect(intDef.parse(undefined)).toBe(42)\n    expect(intDef.safeValidate(undefined).success).toBe(false)\n\n    const intDefOpt = l.optional(intDef)\n    expect(intDefOpt.parse(undefined)).toBe(42)\n    expect(intDefOpt.validate(undefined)).toBe(undefined)\n\n    const intDefOptOpt = l.optional(l.optional(intDef))\n    expect(intDefOptOpt.parse(undefined)).toBe(42)\n    expect(intDefOptOpt.validate(undefined)).toBe(undefined)\n\n    const mySchema = l.object({\n      foo: l.optional(l.withDefault(l.string(), 'aze')),\n    })\n\n    expect(mySchema.parse({})).toStrictEqual({ foo: 'aze' })\n    expect(mySchema.validate({})).toStrictEqual({})\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/integer.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('integer', () => {\n  it('Applies integer range constraint', () => {\n    com.example.integerRange.$parse({\n      $type: 'com.example.integerRange',\n      integer: 2,\n    })\n    expect(() =>\n      com.example.integerRange.$parse({\n        $type: 'com.example.integerRange',\n        integer: 1,\n      }),\n    ).toThrow('integer too small (minimum 2, got 1) at $.integer')\n    expect(() =>\n      com.example.integerRange.$parse({\n        $type: 'com.example.integerRange',\n        integer: 5,\n      }),\n    ).toThrow('integer too big (maximum 4, got 5) at $.integer')\n  })\n\n  it('Applies integer enum constraint', () => {\n    com.example.integerEnum.$parse({\n      $type: 'com.example.integerEnum',\n      integer: 2,\n    })\n    expect(() =>\n      com.example.integerEnum.$parse({\n        $type: 'com.example.integerEnum',\n        integer: 0,\n      }),\n    ).toThrow('Expected one of 1 or 2 (got 0) at $.integer')\n  })\n\n  it('Applies integer const constraint', () => {\n    com.example.integerConst.$parse({\n      $type: 'com.example.integerConst',\n      integer: 0,\n    })\n    expect(() =>\n      com.example.integerConst.$parse({\n        $type: 'com.example.integerConst',\n        integer: 1,\n      }),\n    ).toThrow('Expected 0 (got 1) at $.integer')\n  })\n\n  it('Applies integer whole-number constraint', () => {\n    expect(() =>\n      com.example.integerRange.$parse({\n        $type: 'com.example.integerRange',\n        integer: 2.5,\n      }),\n    ).toThrow('Expected integer value type (got float) at $.integer')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/parameters.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.parametersEnum', () => {\n  it('Passes valid parameters', () => {\n    const paramResult = com.example.parametersEnum.$params.fromURLSearchParams([\n      ['booleanCst', 'true'],\n      ['integerCst', '42'],\n      ['integerEnum', '2'],\n      ['stringCst', 'foo'],\n      ['stringEnum', 'bar'],\n      ['arrayFalse', 'false'],\n      ['arrayFalse', 'false'],\n      ['arrayFalse', 'false'],\n      ['arrayIntCst', '42'],\n      ['arrayIntEnum', '5'],\n    ])\n    expect(paramResult).toStrictEqual({\n      booleanCst: true,\n      integerCst: 42,\n      integerEnum: 2,\n      stringCst: 'foo',\n      stringEnum: 'bar',\n      arrayFalse: [false, false, false],\n      arrayIntCst: [42],\n      arrayIntEnum: [5],\n    })\n  })\n\n  it('properly types params', () => {\n    expectTypeOf<com.example.parametersEnum.$Params>().toMatchObjectType<{\n      booleanCst: true\n      integerCst: 42\n      integerEnum?: 1 | 2 | 3\n      stringCst?: 'foo'\n      stringEnum?: 'foo' | 'bar' | 'baz'\n      arrayFalse?: false[]\n      arrayIntCst?: 42[]\n      arrayIntEnum?: (4 | 5 | 6)[]\n    }>()\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/procedure.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from 'vitest'\nimport { UnknownString } from '@atproto/lex-schema'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.procedure', () => {\n  it('Passes valid parameters', () => {\n    const paramResult = com.example.procedure.$params.$parse({\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      array: ['x', 'y'],\n      def: 1,\n    })\n    expect(paramResult).toStrictEqual({\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      array: ['x', 'y'],\n      def: 1,\n    })\n  })\n\n  it('Passes valid inputs', () => {\n    com.example.procedure.$input.schema.$parse({\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      float: 123.45,\n      integer: 123,\n      string: 'string',\n    })\n  })\n\n  it('Validates input property type', () => {\n    expect(() => {\n      com.example.procedure.$input.schema.$parse({\n        object: { boolean: 'string' },\n        array: ['one', 'two'],\n        boolean: true,\n        float: 123.45,\n        integer: 123,\n        string: 'string',\n      })\n    }).toThrow('Expected boolean value type (got string) at $.object.boolean')\n  })\n\n  it('Rejects missing properties', () => {\n    expect(() => {\n      com.example.procedure.$input.schema.$parse({})\n    }).toThrow('Missing required key \"object\" at $')\n  })\n\n  it('Passes valid outputs', () => {\n    com.example.procedure.$output.schema.$parse({\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      float: 123.45,\n      integer: 123,\n      string: 'string',\n    })\n  })\n\n  it('Rejects invalid output', () => {\n    expect(() => {\n      com.example.procedure.$output.schema.$parse({})\n    }).toThrow('Missing required key \"object\" at $')\n  })\n\n  it('properly types knownValues in params', () => {\n    expectTypeOf<com.example.procedureKnownValues.$Params>().toMatchObjectType<{\n      status?: 'active' | 'inactive' | UnknownString\n    }>()\n  })\n\n  it('properly types knownValues in input body', () => {\n    expectTypeOf<com.example.procedureKnownValues.$InputBody>().toMatchObjectType<{\n      role: 'admin' | 'user' | UnknownString\n    }>()\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/query.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.query', () => {\n  describe('parameters', () => {\n    it('passes valid parameters', () => {\n      const queryResult = com.example.query.$params.$parse({\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        array: ['x', 'y'],\n      })\n      expect(queryResult).toStrictEqual({\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        array: ['x', 'y'],\n        def: 0,\n      })\n    })\n\n    it('preserves unknown parameters', () => {\n      const queryResult = com.example.query.$params.$parse({\n        boolean: true,\n        integer: 123,\n        unknown: 'property',\n      })\n      expect(queryResult).toStrictEqual({\n        boolean: true,\n        integer: 123,\n        def: 0,\n        unknown: 'property',\n      })\n    })\n\n    it('passes valid parameters', () => {\n      com.example.query.$params.$parse({\n        boolean: true,\n        integer: 123,\n      })\n    })\n\n    it('rejects missing parameters', () => {\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: true,\n        }),\n      ).toThrow('Missing required key \"integer\" at $')\n    })\n\n    it('rejects undefined parameters', () => {\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: true,\n          integer: undefined,\n        }),\n      ).toThrow('Expected integer value type (got undefined) at $.integer')\n    })\n\n    it('rejects invalid parameter value', () => {\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: 'string',\n          integer: 123,\n          string: 'string',\n        }),\n      ).toThrow('Expected boolean value type (got string) at $.boolean')\n    })\n\n    it('rejects invalid parameter type', () => {\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: true,\n          integer: 123,\n          float: 123.45,\n        }),\n      ).toThrow(\n        'Expected one of boolean, integer, string or array value type (got float) at $.float',\n      )\n\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: true,\n          integer: 123,\n          array: 'x',\n        }),\n      ).toThrow('Expected array value type (got string) at $.array')\n\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: true,\n          integer: 123,\n          array: 3,\n        }),\n      ).toThrow('Expected array value type (got integer) at $.array')\n\n      expect(() =>\n        com.example.query.$params.$parse({\n          boolean: true,\n          integer: 123,\n          array: NaN,\n        }),\n      ).toThrow('Expected array value type (got NaN) at $.array')\n    })\n\n    it('properly infers the type of default parameters', () => {\n      function returnDef(params: com.example.query.$Params): number {\n        return params.def\n      }\n\n      const parsed = com.example.query.$params.parse({\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        array: ['x', 'y'],\n      })\n\n      expect(returnDef(parsed)).toBe(0)\n    })\n  })\n\n  describe('output', () => {\n    it('Passes valid outputs', () => {\n      com.example.query.$output.schema.$parse({\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        float: 123.45,\n        integer: 123,\n        string: 'string',\n      })\n    })\n\n    it('Rejects invalid output', () => {\n      expect(() => {\n        com.example.query.$output.schema.$parse({\n          object: { boolean: 'string' },\n          array: ['one', 'two'],\n          boolean: true,\n          float: 123.45,\n          integer: 123,\n          string: 'string',\n        })\n      }).toThrow('Expected boolean value type (got string) at $.object.boolean')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/string.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from 'vitest'\nimport { UnknownString } from '@atproto/lex-schema'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.stringLength', () => {\n  describe('valid cases', () => {\n    for (const string of [\n      'ab',\n      'abc',\n      'abcd',\n      '\\u0301', // Combining acute accent (2 bytes)\n      'a\\u0301', // 'a' + combining acute accent (1 + 2 bytes = 3 bytes)\n      'aé', // 'a' (1 byte) + 'é' (2 bytes) = 3 bytes\n      '一', // CJK character (3 bytes)\n      '\\uD83D', // Unpaired high surrogate (3 bytes)\n      'éé', // 'é' + 'é' (2 + 2 bytes = 4 bytes)\n      'aaé', // 1 + 1 + 2 = 4 bytes\n      '👋', // 4 bytes\n    ]) {\n      it(`accepts valid ${JSON.stringify(string)}`, () => {\n        com.example.stringLength.$parse({\n          $type: 'com.example.stringLength',\n          string,\n        })\n      })\n    }\n  })\n\n  describe('invalid cases', () => {\n    for (const { string, error } of [\n      { string: '', error: 'string too small (minimum 2, got 0) at $.string' },\n      {\n        string: 'a',\n        error: 'string too small (minimum 2, got 1) at $.string',\n      },\n      {\n        string: 'abcde',\n        error: 'string too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: 'a\\u0301\\u0301', // 1 + (2 * 2) = 5 bytes\n        error: 'string too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: '\\uD83D\\uD83D', // Two unpaired high surrogates (3 * 2 = 6 bytes)\n        error: 'string too big (maximum 4, got 6) at $.string',\n      },\n      {\n        string: 'ééé', // 2 + 2 + 2 bytes = 6 bytes\n        error: 'string too big (maximum 4, got 6) at $.string',\n      },\n      {\n        string: '👋a', // 4 + 1 bytes = 5 bytes\n        error: 'string too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: '👨👨', // 4 + 4 = 8 bytes\n        error: 'string too big (maximum 4, got 8) at $.string',\n      },\n      {\n        string: '👨‍👩‍👧‍👧', // 4 emojis × 4 bytes + 3 ZWJs × 3 bytes = 25 bytes\n        error: 'string too big (maximum 4, got 25) at $.string',\n      },\n    ]) {\n      it(`rejects invalid ${JSON.stringify(string)}`, () => {\n        expect(() =>\n          com.example.stringLength.$parse({\n            $type: 'com.example.stringLength',\n            string,\n          }),\n        ).toThrow(error)\n      })\n    }\n  })\n})\n\ndescribe('com.example.stringLengthNoMinLength', () => {\n  describe('valid cases', () => {\n    for (const string of [\n      // Shorter than two UTF8 characters\n      '',\n      'a',\n      // Two to four UTF8 characters\n      'ab',\n      '\\u0301', // Combining acute accent (2 bytes)\n      'a\\u0301', // 'a' + combining acute accent (1 + 2 bytes = 3 bytes)\n      'aé', // 'a' (1 byte) + 'é' (2 bytes) = 3 bytes\n      'abc',\n      '一', // CJK character (3 bytes)\n      '\\uD83D', // Unpaired high surrogate (3 bytes)\n      'abcd',\n      'éé', // 'é' + 'é' (2 + 2 bytes = 4 bytes)\n      'aaé', // 1 + 1 + 2 = 4 bytes\n      '👋', // 4 bytes\n    ]) {\n      it(`accepts valid ${JSON.stringify(string)}`, () => {\n        com.example.stringLengthNoMinLength.$parse({\n          $type: 'com.example.stringLengthNoMinLength',\n          string,\n        })\n      })\n    }\n  })\n\n  describe('invalid cases', () => {\n    for (const { string, error } of [\n      {\n        string: 'abcde',\n        error: 'string too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: 'a\\u0301\\u0301', // 1 + (2 * 2) = 5 bytes\n        error: 'string too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: '\\uD83D\\uD83D', // Two unpaired high surrogates (3 * 2 = 6 bytes)\n        error: 'string too big (maximum 4, got 6) at $.string',\n      },\n      {\n        string: 'ééé', // 2 + 2 + 2 bytes = 6 bytes\n        error: 'string too big (maximum 4, got 6) at $.string',\n      },\n      {\n        string: '👋a', // 4 + 1 bytes = 5 bytes\n        error: 'string too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: '👨👨', // 4 + 4 = 8 bytes\n        error: 'string too big (maximum 4, got 8) at $.string',\n      },\n      {\n        string: '👨‍👩‍👧‍👧', // 4 emojis × 4 bytes + 3 ZWJs × 3 bytes = 25 bytes\n        error: 'string too big (maximum 4, got 25) at $.string',\n      },\n    ]) {\n      it(`rejects invalid ${JSON.stringify(string)}`, () => {\n        expect(() =>\n          com.example.stringLengthNoMinLength.$parse({\n            $type: 'com.example.stringLengthNoMinLength',\n            string,\n          }),\n        ).toThrow(error)\n      })\n    }\n  })\n})\n\ndescribe('com.example.stringKnownValues', () => {\n  it('properly types known string values', () => {\n    expectTypeOf<com.example.stringKnownValues.Main>().not.toMatchObjectType<{\n      myKey: string\n    }>()\n    expectTypeOf<com.example.stringKnownValues.Main>().not.toMatchObjectType<{\n      myKey: UnknownString\n    }>()\n    expectTypeOf<com.example.stringKnownValues.Main>().toMatchObjectType<{\n      myKey: 'foo' | 'bar' | UnknownString\n    }>()\n\n    expectTypeOf<\n      com.example.stringKnownValues.Main['myKey']\n    >().not.toEqualTypeOf<string>()\n    expectTypeOf<\n      com.example.stringKnownValues.Main['myKey']\n    >().not.toEqualTypeOf<UnknownString>()\n    expectTypeOf<com.example.stringKnownValues.Main['myKey']>().toEqualTypeOf<\n      'foo' | 'bar' | UnknownString\n    >()\n  })\n})\n\ndescribe('com.example.stringLengthGrapheme', () => {\n  describe('valid cases', () => {\n    for (const string of [\n      'ab',\n      'a\\u0301b', // 'áb' with combining accent\n      'a\\u0301b\\u0301', // 'áb́'\n      '😀😀',\n      '12👨‍👩‍👧‍👧',\n      'abcd',\n      'a\\u0301b\\u0301c\\u0301d\\u0301', // 'áb́ćd́'\n    ]) {\n      it(`accepts valid ${JSON.stringify(string)}`, () => {\n        com.example.stringLengthGrapheme.$parse({\n          $type: 'com.example.stringLengthGrapheme',\n          string,\n        })\n      })\n    }\n  })\n\n  describe('invalid cases', () => {\n    for (const { string, error } of [\n      // Shorter than two graphemes\n      {\n        string: '',\n        error: 'grapheme too small (minimum 2, got 0) at $.string',\n      },\n      {\n        string: '\\u0301\\u0301\\u0301', // Three combining acute accents\n        error: 'grapheme too small (minimum 2, got 1) at $.string',\n      },\n      {\n        string: 'a',\n        error: 'grapheme too small (minimum 2, got 1) at $.string',\n      },\n      {\n        string: 'a\\u0301\\u0301\\u0301\\u0301', // 'á́́́' ('a' with four combining acute accents)\n        error: 'grapheme too small (minimum 2, got 1) at $.string',\n      },\n      {\n        string: '5\\uFE0F', // '5️' with emoji presentation\n        error: 'grapheme too small (minimum 2, got 1) at $.string',\n      },\n      {\n        string: '👨‍👩‍👧‍👧',\n        error: 'grapheme too small (minimum 2, got 1) at $.string',\n      },\n      // Longer than four graphemes\n      {\n        string: 'abcde',\n        error: 'grapheme too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: 'a\\u0301b\\u0301c\\u0301d\\u0301e\\u0301', // 'áb́ćd́é'\n        error: 'grapheme too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: '😀😀😀😀😀',\n        error: 'grapheme too big (maximum 4, got 5) at $.string',\n      },\n      {\n        string: 'ab😀de',\n        error: 'grapheme too big (maximum 4, got 5) at $.string',\n      },\n    ]) {\n      it(`rejects invalid ${JSON.stringify(string)}`, () => {\n        expect(() =>\n          com.example.stringLengthGrapheme.$parse({\n            $type: 'com.example.stringLengthGrapheme',\n            string,\n          }),\n        ).toThrow(error)\n      })\n    }\n  })\n})\n\ndescribe('com.example.stringEnum', () => {\n  it('Applies string enum constraint', () => {\n    com.example.stringEnum.$parse({\n      $type: 'com.example.stringEnum',\n      string: 'a',\n    })\n    expect(() =>\n      com.example.stringEnum.$parse({\n        $type: 'com.example.stringEnum',\n        string: 'c',\n      }),\n    ).toThrow('Expected one of \"a\" or \"b\" (got \"c\") at $.string')\n  })\n})\ndescribe('com.example.stringConst', () => {\n  it('Applies string const constraint', () => {\n    com.example.stringConst.$parse({\n      $type: 'com.example.stringConst',\n      string: 'a',\n    })\n    expect(() =>\n      com.example.stringConst.$parse({\n        $type: 'com.example.stringConst',\n        string: 'b',\n      }),\n    ).toThrow('Expected \"a\" (got \"b\") at $.string')\n  })\n})\ndescribe('com.example.datetime', () => {\n  it('Applies datetime formatting constraint', () => {\n    for (const datetime of [\n      '2022-12-12T00:50:36.809Z',\n      '2022-12-12T00:50:36Z',\n      '2022-12-12T00:50:36.8Z',\n      '2022-12-12T00:50:36.80Z',\n      '2022-12-12T00:50:36+00:00',\n      '2022-12-12T00:50:36.8+00:00',\n      '2022-12-11T19:50:36-05:00',\n      '2022-12-11T19:50:36.8-05:00',\n      '2022-12-11T19:50:36.80-05:00',\n      '2022-12-11T19:50:36.809-05:00',\n    ]) {\n      com.example.datetime.$parse({\n        $type: 'com.example.datetime',\n        datetime,\n      })\n    }\n    expect(() =>\n      com.example.datetime.$parse({\n        $type: 'com.example.datetime',\n        datetime: 'bad date',\n      }),\n    ).toThrow('Invalid datetime (got \"bad date\") at $.datetime')\n  })\n})\ndescribe('com.example.uri', () => {\n  it('Applies uri formatting constraint', () => {\n    for (const uri of [\n      'https://example.com',\n      'https://example.com/with/path',\n      'https://example.com/with/path?and=query',\n      'at://bsky.social',\n      'did:example:test',\n    ]) {\n      com.example.uri.$parse({\n        $type: 'com.example.uri',\n        uri,\n      })\n    }\n    expect(() =>\n      com.example.uri.$parse({\n        $type: 'com.example.uri',\n        uri: 'not a uri',\n      }),\n    ).toThrow('Invalid uri (got \"not a uri\") at $.uri')\n  })\n})\ndescribe('com.example.atUri', () => {\n  it('Applies at-uri formatting constraint', () => {\n    com.example.atUri.$parse({\n      $type: 'com.example.atUri',\n      atUri: 'at://did:web:example.com/com.example.test/self',\n    })\n    expect(() =>\n      com.example.atUri.$parse({\n        $type: 'com.example.atUri',\n        atUri: 'http://not-atproto.com',\n      }),\n    ).toThrow('Invalid at-uri (got \"http://not-atproto.com\") at $.atUri')\n  })\n})\ndescribe('com.example.did', () => {\n  it('Applies did formatting constraint', () => {\n    com.example.did.$parse({\n      $type: 'com.example.did',\n      did: 'did:web:example.com',\n    })\n    com.example.did.$parse({\n      $type: 'com.example.did',\n      did: 'did:plc:12345678abcdefghijklmnop',\n    })\n\n    expect(() =>\n      com.example.did.$parse({\n        $type: 'com.example.did',\n        did: 'bad did',\n      }),\n    ).toThrow('Invalid DID (got \"bad did\") at $.did')\n    expect(() =>\n      com.example.did.$parse({\n        $type: 'com.example.did',\n        did: 'did:short',\n      }),\n    ).toThrow('Invalid DID (got \"did:short\") at $.did')\n  })\n})\ndescribe('com.example.handle', () => {\n  it('Applies handle formatting constraint', () => {\n    com.example.handle.$parse({\n      $type: 'com.example.handle',\n      handle: 'test.bsky.social',\n    })\n    com.example.handle.$parse({\n      $type: 'com.example.handle',\n      handle: 'bsky.test',\n    })\n\n    expect(() =>\n      com.example.handle.$parse({\n        $type: 'com.example.handle',\n        handle: 'bad handle',\n      }),\n    ).toThrow('Invalid handle (got \"bad handle\") at $.handle')\n    expect(() =>\n      com.example.handle.$parse({\n        $type: 'com.example.handle',\n        handle: '-bad-.test',\n      }),\n    ).toThrow('Invalid handle (got \"-bad-.test\") at $.handle')\n  })\n})\ndescribe('com.example.atIdentifier', () => {\n  it('Applies at-identifier formatting constraint', () => {\n    com.example.atIdentifier.$parse({\n      $type: 'com.example.atIdentifier',\n      atIdentifier: 'bsky.test',\n    })\n    com.example.atIdentifier.$parse({\n      $type: 'com.example.atIdentifier',\n      atIdentifier: 'did:plc:12345678abcdefghijklmnop',\n    })\n\n    expect(() =>\n      com.example.atIdentifier.$parse({\n        $type: 'com.example.atIdentifier',\n        atIdentifier: 'bad id',\n      }),\n    ).toThrow('Invalid AT identifier (got \"bad id\") at $.atIdentifier')\n    expect(() =>\n      com.example.atIdentifier.$parse({\n        $type: 'com.example.atIdentifier',\n        atIdentifier: '-bad-.test',\n      }),\n    ).toThrow('Invalid AT identifier (got \"-bad-.test\") at $.atIdentifier')\n  })\n})\ndescribe('com.example.nsid', () => {\n  it('Applies nsid formatting constraint', () => {\n    com.example.nsid.$parse({\n      $type: 'com.example.nsid',\n      nsid: 'com.atproto.test',\n    })\n    com.example.nsid.$parse({\n      $type: 'com.example.nsid',\n      nsid: 'app.bsky.nested.test',\n    })\n\n    expect(() =>\n      com.example.nsid.$parse({\n        $type: 'com.example.nsid',\n        nsid: 'bad nsid',\n      }),\n    ).toThrow('Invalid NSID (got \"bad nsid\") at $.nsid')\n    expect(() =>\n      com.example.nsid.$parse({\n        $type: 'com.example.nsid',\n        nsid: 'com.bad-.foo',\n      }),\n    ).toThrow('Invalid NSID (got \"com.bad-.foo\") at $.nsid')\n  })\n})\ndescribe('com.example.cid', () => {\n  it('Applies cid formatting constraint', () => {\n    com.example.cid.$parse({\n      $type: 'com.example.cid',\n      cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    })\n    expect(() =>\n      com.example.cid.$parse({\n        $type: 'com.example.cid',\n        cid: 'abapsdofiuwrpoiasdfuaspdfoiu',\n      }),\n    ).toThrow(\n      'Invalid CID string (got \"abapsdofiuwrpoiasdfuaspdfoiu\") at $.cid',\n    )\n  })\n})\ndescribe('com.example.language', () => {\n  it('Applies language formatting constraint', () => {\n    com.example.language.$parse({\n      $type: 'com.example.language',\n      language: 'en-US-boont',\n    })\n    expect(() =>\n      com.example.language.$parse({\n        $type: 'com.example.language',\n        language: 'not-a-language-',\n      }),\n    ).toThrow('Invalid language (got \"not-a-language-\") at $.language')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/token.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { l } from '@atproto/lex-schema'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.token', () => {\n  for (const hash of ['main', 'myToken', 'anotherToken'] as const) {\n    const token = l.$type(`com.example.token`, hash)\n    describe(token, () => {\n      it('identifies the token correctly', () => {\n        expect(com.example.token[hash].$matches(token)).toBe(true)\n      })\n\n      it('parses the token correctly', () => {\n        expect(com.example.token[hash].$parse(token)).toBe(token)\n      })\n\n      it('json serializes the token correctly', () => {\n        expect(com.example.token[hash].toJSON()).toBe(token)\n      })\n\n      it('stringifies the token correctly', () => {\n        expect(com.example.token[hash].toString()).toBe(token)\n      })\n    })\n  }\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/union.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('union', () => {\n  it('Handles unions correctly', () => {\n    com.example.union.$parse({\n      $type: 'com.example.union',\n      unionOpen: {\n        $type: 'com.example.kitchenSink#object',\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n      },\n      unionClosed: {\n        $type: 'com.example.kitchenSink#subobject',\n        boolean: true,\n      },\n    })\n    com.example.union.$parse({\n      $type: 'com.example.union',\n      unionOpen: {\n        $type: 'com.example.other',\n      },\n      unionClosed: {\n        $type: 'com.example.kitchenSink#subobject',\n        boolean: true,\n      },\n    })\n    expect(() =>\n      com.example.union.$parse({\n        $type: 'com.example.union',\n        unionOpen: {},\n        unionClosed: {},\n      }),\n    ).toThrow(\n      'Expected an object which includes the \"$type\" property value type (got object) at $.unionOpen',\n    )\n    expect(() =>\n      com.example.union.$parse({\n        $type: 'com.example.union',\n        unionOpen: {\n          $type: 'com.example.other',\n        },\n        unionClosed: {\n          $type: 'com.example.other',\n          boolean: true,\n        },\n      }),\n    ).toThrow(\n      'Expected one of \"com.example.kitchenSink#object\" or \"com.example.kitchenSink#subobject\" (got \"com.example.other\") at $.unionClosed.$type',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/unknown.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('unknown', () => {\n  it('Handles unknowns correctly', () => {\n    com.example.unknown.$parse({\n      $type: 'com.example.unknown',\n      unknown: { foo: 'bar' },\n    })\n    expect(() =>\n      com.example.unknown.$parse({\n        $type: 'com.example.unknown',\n      }),\n    ).toThrow('Missing required key \"unknown\" at $')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tests/unsafe.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport * as com from './lexicons/com.js'\n\ndescribe('com.example.4-2.unsafeDefs', () => {\n  it('allows accessing defs with unsafe characters', () => {\n    const input = {\n      $type: 'com.example.4-2.unsafeDefs#ob-je-c$t',\n      foo: 'bar',\n    }\n    const result = com.example['4-2'].unsafeDefs['ob-je-c$t'].$parse({\n      ...input,\n    })\n    expect(result).toStrictEqual(input)\n  })\n\n  it('allows accessing defs that are reserved words', () => {\n    const input = 9\n    const result = com.example['4-2'].unsafeDefs['9'].$parse(input)\n    expect(result).toBe(input)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-builder/.gitignore",
    "content": "tests/lexicons\n"
  },
  {
    "path": "packages/lex/lex-builder/CHANGELOG.md",
    "content": "# @atproto/lex-builder\n\n## 0.0.19\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-document@0.0.17\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies [[`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-schema@0.0.15\n  - @atproto/lex-document@0.0.16\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766)]:\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-document@0.0.15\n\n## 0.0.16\n\n### Patch Changes\n\n- [#4659](https://github.com/bluesky-social/atproto/pull/4659) [`cd9deb6`](https://github.com/bluesky-social/atproto/commit/cd9deb6f210b91661595398cb2ef70bc40eccabe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add a `$` in front of the method type utilities to prevent name conflicts with local definitions\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow customizing the binary type of `Input` and `Output` generated helpers\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-document@0.0.14\n\n## 0.0.15\n\n### Patch Changes\n\n- [#4610](https://github.com/bluesky-social/atproto/pull/4610) [`619068f`](https://github.com/bluesky-social/atproto/commit/619068fb81203b3b43b632892bdcb0a5067f7fe4) Thanks [@gaearon](https://github.com/gaearon)! - Export `LexDefBuilder` class and add `moduleSpecifier` option to `RefResolverOptions` for custom external import resolution\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-document@0.0.13\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-document@0.0.12\n  - @atproto/lex-schema@0.0.11\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/lex-document@0.0.11\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-document@0.0.10\n  - @atproto/lex-schema@0.0.9\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve naming of exported types\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-document@0.0.9\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Better prevent use of conflicting identifiers in generated code\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add an `indexFile` option that allows generating an \"index.ts\" file that re-exports every tld namespaces.\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure generated \"import\" identifiers start with a lower case letter\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid escaping export identifier when it is a known global\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-schema@0.0.7\n  - @atproto/lex-document@0.0.8\n\n## 0.0.8\n\n### Patch Changes\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `jsonPayload` for json payloads\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n  - @atproto/lex-document@0.0.7\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Refactor JSDoc generation to use ts-morph's native `JSDocStructure`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build additional `$lxm` utility variable for xrpc methods\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-schema@0.0.5\n  - @atproto/lex-document@0.0.6\n\n## 0.0.6\n\n### Patch Changes\n\n- [#4413](https://github.com/bluesky-social/atproto/pull/4413) [`e39ca11`](https://github.com/bluesky-social/atproto/commit/e39ca114accac65070dcdd424a181821aad6d99d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for NodeJS version <18.18, 19.x, <20.4 and 21.x\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lex-document@0.0.5\n  - @atproto/lex-schema@0.0.4\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework object validation logic to work without `options` argument\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `l.nullable` for nullable object properties and `l.optional` for optional object properties in lex schemas.\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use string formats from `@atproto/syntax`\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `knownValues` from string options (as it had not runtime effect)\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename schema methods `validate`, `check` and `maybe` to `safeParse`, `matches` and `ifMatches` respectively.\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- [#4398](https://github.com/bluesky-social/atproto/pull/4398) [`a17d2e8`](https://github.com/bluesky-social/atproto/commit/a17d2e8a59ceb00fa8197642e0767fcc776d5b70) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `ignoreInvalidLexicons` option when building lexicon schemas\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Strip unknown properties from schema options\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` option to `const` and `enum` schemas\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`03a2a4b`](https://github.com/bluesky-social/atproto/commit/03a2a4bb3814ced7ad1d4fe6c94b5348a3bbc097), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba)]:\n  - @atproto/lex-schema@0.0.3\n  - @atproto/lex-document@0.0.4\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4380](https://github.com/bluesky-social/atproto/pull/4380) [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ability to configure file extenstion and import file extension in `lex build`\n\n- Updated dependencies [[`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4)]:\n  - @atproto/lex-schema@0.0.2\n  - @atproto/lex-document@0.0.3\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`5ffd612`](https://github.com/bluesky-social/atproto/commit/5ffd6129909071e979c30f31266119865ab582b6)]:\n  - @atproto/lex-document@0.0.2\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4372](https://github.com/bluesky-social/atproto/pull/4372) [`7456f53`](https://github.com/bluesky-social/atproto/commit/7456f53e45fb3eef2f3bbdf2513da2d8ab078d80) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix error when running `install` command\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-document@0.0.1\n  - @atproto/lex-schema@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-builder/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-builder\",\n  \"version\": \"0.0.19\",\n  \"license\": \"MIT\",\n  \"description\": \"TypeScript schema builder for AT Lexicons\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"build\",\n    \"lex\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-builder\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"require\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-document\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"prettier\": \"^3.2.5\",\n    \"ts-morph\": \"^27.0.0\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@ts-morph/common\": \"^0.28.0\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/filter.ts",
    "content": "/**\n * Options for building a filter function to include/exclude lexicon documents.\n */\nexport type BuildFilterOptions = {\n  /**\n   * Pattern(s) for lexicon NSIDs to include.\n   *\n   * Supports glob patterns with `*` as a wildcard that matches one or more\n   * characters. If not specified, all lexicons are included by default.\n   *\n   * @example\n   * ```ts\n   * { include: 'app.bsky.*' }           // Include all app.bsky lexicons\n   * { include: ['com.atproto.*', 'app.bsky.*'] }  // Include multiple patterns\n   * ```\n   */\n  include?: string | string[]\n  /**\n   * Pattern(s) for lexicon NSIDs to exclude.\n   *\n   * Supports glob patterns with `*` as a wildcard. Exclusions are applied\n   * after inclusions.\n   *\n   * @example\n   * ```ts\n   * { exclude: '*.internal.*' }         // Exclude internal lexicons\n   * { exclude: ['*.test', '*.mock'] }   // Exclude test and mock lexicons\n   * ```\n   */\n  exclude?: string | string[]\n}\n\n/**\n * A function that tests whether a lexicon NSID should be included.\n *\n * @param input - The lexicon NSID to test\n * @returns `true` if the NSID passes the filter, `false` otherwise\n */\nexport type Filter = (input: string) => boolean\n\n/**\n * Builds a filter function from include/exclude patterns.\n *\n * The returned filter returns `true` for NSIDs that match any include pattern\n * (or all NSIDs if no include patterns are specified) AND do not match any\n * exclude pattern.\n *\n * @param options - The filter options containing include/exclude patterns\n * @returns A filter function that can be applied to lexicon NSIDs\n *\n * @example\n * ```ts\n * const filter = buildFilter({\n *   include: 'app.bsky.*',\n *   exclude: '*.internal.*',\n * })\n *\n * filter('app.bsky.feed.post')     // true\n * filter('app.bsky.internal.foo')  // false\n * filter('com.atproto.repo')       // false (not included)\n * ```\n */\nexport function buildFilter(options: BuildFilterOptions): Filter {\n  const include = createMatcher(options.include, () => true)\n  const exclude = createMatcher(options.exclude, () => false)\n\n  return (id) => include(id) && !exclude(id)\n}\n\nfunction createMatcher(\n  pattern: undefined | string | string[],\n  fallback: Filter,\n): Filter {\n  if (!pattern?.length) {\n    return fallback\n  } else if (Array.isArray(pattern)) {\n    return pattern.map(buildMatcher).reduce(combineFilters)\n  } else {\n    return buildMatcher(pattern)\n  }\n}\n\nfunction combineFilters(a: Filter, b: Filter): Filter {\n  return (input: string) => a(input) || b(input)\n}\n\nfunction buildMatcher(pattern: string): Filter {\n  if (pattern.includes('*')) {\n    const regex = new RegExp(\n      `^${pattern.replaceAll('.', '\\\\.').replaceAll('*', '.+')}$`,\n    )\n    return (input: string) => regex.test(input)\n  }\n\n  return (input: string) => pattern === input\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/filtered-indexer.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'\nimport { FilteredIndexer } from './filtered-indexer.js'\n\nclass DummyIndexer implements LexiconIndexer, AsyncIterable<LexiconDocument> {\n  readonly docs: Map<string, LexiconDocument>\n\n  constructor(docs: LexiconDocument[]) {\n    this.docs = new Map(docs.map((doc) => [doc.id, doc]))\n  }\n\n  async get(id: string): Promise<LexiconDocument> {\n    const doc = this.docs.get(id)\n    if (!doc) {\n      throw new Error(`Document not found: ${id}`)\n    }\n    return doc\n  }\n\n  async *[Symbol.asyncIterator]() {\n    for (const doc of this.docs.values()) {\n      yield doc\n    }\n  }\n}\n\ndescribe('FilteredIndexer', () => {\n  const docs: LexiconDocument[] = [\n    {\n      lexicon: 1,\n      id: 'com.example.alpha',\n      defs: {},\n    },\n    {\n      lexicon: 1,\n      id: 'com.example.beta',\n      defs: {},\n    },\n    {\n      lexicon: 1,\n      id: 'org.sample.gamma',\n      defs: {},\n    },\n  ]\n\n  it('yields only filtered documents', async () => {\n    const indexer = new DummyIndexer(docs)\n    const filter = (id: string) => id.startsWith('com.example.')\n    const filteredIndexer = new FilteredIndexer(indexer, filter)\n\n    const yieldedDocs = []\n    for await (const doc of filteredIndexer) {\n      yieldedDocs.push(doc)\n    }\n\n    expect(yieldedDocs).toHaveLength(2)\n    expect(yieldedDocs.map((d) => d.id)).toEqual([\n      'com.example.alpha',\n      'com.example.beta',\n    ])\n  })\n\n  it('bypasses filter for requested documents', async () => {\n    const indexer = new DummyIndexer(docs)\n    const filter = (id: string) => id.startsWith('com.example.')\n    const filteredIndexer = new FilteredIndexer(indexer, filter)\n\n    // Request a document that would normally be filtered out\n    const requestedDoc = await filteredIndexer.get('org.sample.gamma')\n    expect(requestedDoc.id).toBe('org.sample.gamma')\n\n    const yieldedDocs = []\n    for await (const doc of filteredIndexer) {\n      yieldedDocs.push(doc)\n    }\n\n    expect(yieldedDocs).toHaveLength(3)\n    expect(yieldedDocs.map((d) => d.id)).toEqual([\n      'com.example.alpha',\n      'com.example.beta',\n      'org.sample.gamma',\n    ])\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-builder/src/filtered-indexer.ts",
    "content": "import { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'\nimport { Filter } from './filter.js'\n\n/**\n * A lexicon indexer that filters documents based on a provided filter.\n *\n * If a document was filtered out but later requested via `get()`, the filter\n * will be bypassed for that document.\n */\nexport class FilteredIndexer implements LexiconIndexer, AsyncDisposable {\n  protected readonly returned = new Set<string>()\n\n  constructor(\n    readonly indexer: LexiconIndexer & AsyncIterable<LexiconDocument>,\n    readonly filter: Filter,\n  ) {}\n\n  async get(id: string): Promise<LexiconDocument> {\n    this.returned.add(id)\n    return this.indexer.get(id)\n  }\n\n  async *[Symbol.asyncIterator]() {\n    const returned = new Set<string>()\n\n    for await (const doc of this.indexer) {\n      if (returned.has(doc.id)) {\n        // Should never happen\n        throw new Error(`Duplicate lexicon document id: ${doc.id}`)\n      }\n\n      if (this.returned.has(doc.id) || this.filter(doc.id)) {\n        this.returned.add(doc.id)\n        returned.add(doc.id)\n        yield doc\n      }\n    }\n\n    // When we yield control back to the caller, there may be requests (.get())\n    // for documents that were initially ignored (filtered out). We won't be\n    // done iterating until every document that may have been requested when the\n    // control was yielded to the caller has been returned.\n\n    let returnedAny: boolean\n    do {\n      returnedAny = false\n      for (const id of this.returned) {\n        if (!returned.has(id)) {\n          yield await this.indexer.get(id)\n          returned.add(id)\n          returnedAny = true\n        }\n      }\n    } while (returnedAny)\n  }\n\n  async [Symbol.asyncDispose](): Promise<void> {\n    await this.indexer[Symbol.asyncDispose]?.()\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/formatter.ts",
    "content": "import { Options as PrettierOptions, format as prettierFormat } from 'prettier'\n\nconst DEFAULT_FORMAT_OPTIONS: PrettierOptions = {\n  parser: 'typescript',\n  tabWidth: 2,\n  semi: false,\n  singleQuote: true,\n  trailingComma: 'all',\n}\n\nconst DEFAULT_BANNER = `/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */`\n\n/**\n * Options for configuring the code formatter.\n */\nexport type FormatterOptions = {\n  /**\n   * Whether to format the generated code with Prettier.\n   *\n   * - `false`: No formatting (default)\n   * - `true`: Format with default Prettier options\n   * - `PrettierOptions`: Format with custom Prettier configuration\n   *\n   * @default false\n   */\n  pretty?: boolean | PrettierOptions\n  /**\n   * A banner comment to prepend to each generated file.\n   *\n   * @default '/* THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT. *\\/'\n   */\n  banner?: string\n}\n\n/**\n * Formats generated TypeScript code with optional Prettier formatting\n * and banner comments.\n *\n * @example\n * ```ts\n * const formatter = new Formatter({ pretty: true })\n * const formatted = await formatter.format(generatedCode)\n * ```\n */\nexport class Formatter {\n  /** The banner comment to prepend to formatted code. */\n  readonly banner: string\n  /** Prettier options, or `null` if formatting is disabled. */\n  readonly prettierOptions: PrettierOptions | null\n\n  /**\n   * Creates a new Formatter instance.\n   *\n   * @param options - Formatting configuration options\n   */\n  constructor(options: FormatterOptions = {}) {\n    this.banner = options?.banner ?? DEFAULT_BANNER\n\n    this.prettierOptions =\n      options?.pretty === true\n        ? DEFAULT_FORMAT_OPTIONS\n        : options?.pretty || null\n  }\n\n  /**\n   * Formats the given code string.\n   *\n   * Applies Prettier formatting if enabled, and prepends the banner comment.\n   *\n   * @param code - The TypeScript code to format\n   * @returns The formatted code with banner\n   */\n  async format(code: string) {\n    const bannerPadding =\n      this.banner && !this.banner.endsWith('\\n') ? '\\n\\n' : ''\n    const codePretty = this.prettierOptions\n      ? await prettierFormat(code, this.prettierOptions)\n      : code\n    return `${this.banner}${bannerPadding}${codePretty}`\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/index.ts",
    "content": "// Must be first\nimport './polyfill.js'\n\nimport {\n  LexBuilder,\n  LexBuilderLoadOptions,\n  LexBuilderOptions,\n  LexBuilderSaveOptions,\n} from './lex-builder.js'\n\nexport * from './lex-builder.js'\nexport * from './lex-def-builder.js'\nexport * from './lexicon-directory-indexer.js'\n\n/**\n * Combined options for building a TypeScript project from Lexicon documents.\n *\n * This type merges all configuration options needed for the complete build\n * process, including builder configuration, loading options, and save options.\n *\n * @see {@link LexBuilderOptions} for builder configuration\n * @see {@link LexBuilderLoadOptions} for lexicon loading options\n * @see {@link LexBuilderSaveOptions} for output save options\n */\nexport type TsProjectBuildOptions = LexBuilderOptions &\n  LexBuilderLoadOptions &\n  LexBuilderSaveOptions\n\n/**\n * Builds TypeScript schemas from Lexicon documents.\n *\n * This is the main entry point for programmatic usage of the lex-builder\n * package. It creates a new {@link LexBuilder} instance, loads lexicon\n * documents from the specified directory, and saves the generated TypeScript\n * files to the output directory.\n *\n * @param options - Combined build options including source directory, output\n *   directory, and generation settings\n *\n * @example\n * ```ts\n * import { build } from '@atproto/lex-builder'\n *\n * await build({\n *   lexicons: './lexicons',\n *   out: './src/generated',\n *   pretty: true,\n *   clear: true,\n * })\n * ```\n */\nexport async function build(options: TsProjectBuildOptions) {\n  const builder = new LexBuilder(options)\n  await builder.load(options)\n  await builder.save(options)\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/lex-builder.ts",
    "content": "import assert from 'node:assert'\nimport { mkdir, rm, stat, writeFile } from 'node:fs/promises'\nimport { join, resolve } from 'node:path'\nimport { IndentationText, Project } from 'ts-morph'\nimport { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'\nimport { BuildFilterOptions, buildFilter } from './filter.js'\nimport { FilteredIndexer } from './filtered-indexer.js'\nimport { Formatter, FormatterOptions } from './formatter.js'\nimport { LexDefBuilder, LexDefBuilderOptions } from './lex-def-builder.js'\nimport {\n  LexiconDirectoryIndexer,\n  LexiconDirectoryIndexerOptions,\n} from './lexicon-directory-indexer.js'\nimport { asNamespaceExport } from './ts-lang.js'\n\n/**\n * Configuration options for the {@link LexBuilder} class.\n *\n * Extends {@link LexDefBuilderOptions} with additional settings for\n * controlling the generated TypeScript project structure.\n *\n * @see {@link LexDefBuilderOptions} for definition generation options\n */\nexport type LexBuilderOptions = LexDefBuilderOptions & {\n  /**\n   * Whether to generate an index file at the root exporting all top-level\n   * namespaces.\n   *\n   * @note This could theoretically cause name conflicts if a\n   * @default false\n   */\n  indexFile?: boolean\n  /**\n   * The file extension to use for import specifiers in the generated code.\n   *\n   * @default '.js'\n   */\n  importExt?: string\n  /**\n   * The file extension to use for generated TypeScript files.\n   *\n   * @default '.ts'\n   */\n  fileExt?: string\n}\n\n/**\n * Options for loading lexicon documents into the builder.\n *\n * Combines directory indexing options with filtering options to control\n * which lexicon documents are processed.\n *\n * @see {@link LexiconDirectoryIndexerOptions} for directory scanning options\n * @see {@link BuildFilterOptions} for include/exclude filtering\n */\nexport type LexBuilderLoadOptions = LexiconDirectoryIndexerOptions &\n  BuildFilterOptions\n\n/**\n * Options for saving generated TypeScript files.\n *\n * Combines formatting options with output directory configuration.\n */\nexport type LexBuilderSaveOptions = FormatterOptions & {\n  /**\n   * The output directory path where generated TypeScript files will be written.\n   */\n  out: string\n  /**\n   * Whether to clear the output directory before writing files.\n   *\n   * When `true`, the entire output directory is deleted before writing new files.\n   *\n   * @default false\n   */\n  clear?: boolean\n  /**\n   * Whether to allow overwriting existing files.\n   *\n   * When `false`, an error is thrown if any output file already exists.\n   *\n   * @default false\n   */\n  override?: boolean\n}\n\n/**\n * Main builder class for generating TypeScript schemas from Lexicon documents.\n *\n * The LexBuilder orchestrates the entire code generation process:\n * 1. Loading and indexing lexicon documents from the filesystem\n * 2. Generating TypeScript type definitions and runtime schemas\n * 3. Creating namespace export trees for convenient imports\n * 4. Saving formatted output files\n *\n * @example\n * ```ts\n * const builder = new LexBuilder({ indexFile: true, pretty: true })\n *\n * // Load lexicons from a directory\n * await builder.load({ lexicons: './lexicons' })\n *\n * // Save generated TypeScript to output directory\n * await builder.save({ out: './src/generated', clear: true })\n * ```\n */\nexport class LexBuilder {\n  readonly #imported = new Set<string>()\n  readonly #project = new Project({\n    useInMemoryFileSystem: true,\n    manipulationSettings: { indentationText: IndentationText.TwoSpaces },\n  })\n\n  constructor(private readonly options: LexBuilderOptions = {}) {}\n\n  get fileExt() {\n    return this.options.fileExt ?? '.ts'\n  }\n\n  get importExt() {\n    return this.options.importExt ?? '.js'\n  }\n\n  public async load(options: LexBuilderLoadOptions) {\n    await using indexer = new FilteredIndexer(\n      new LexiconDirectoryIndexer(options),\n      buildFilter(options),\n    )\n\n    for await (const doc of indexer) {\n      if (!this.#imported.has(doc.id)) {\n        this.#imported.add(doc.id)\n      } else {\n        throw new Error(`Duplicate lexicon document id: ${doc.id}`)\n      }\n\n      await this.createDefsFile(doc, indexer)\n      await this.createExportTree(doc)\n    }\n  }\n\n  public async save(options: LexBuilderSaveOptions) {\n    const files = this.#project.getSourceFiles()\n\n    const destination = resolve(options.out)\n\n    if (options.clear) {\n      await rm(destination, { recursive: true, force: true })\n    } else if (!options.override) {\n      await Promise.all(\n        files.map(async (f) =>\n          assertNotFileExists(join(destination, f.getFilePath())),\n        ),\n      )\n    }\n\n    const formatter = new Formatter(options)\n\n    await Promise.all(\n      Array.from(files, async (file) => {\n        const filePath = join(destination, file.getFilePath())\n        const content = await formatter.format(file.getFullText())\n        await mkdir(join(filePath, '..'), { recursive: true })\n        await rm(filePath, { recursive: true, force: true })\n        await writeFile(filePath, content, 'utf8')\n      }),\n    )\n  }\n\n  private createFile(path: string) {\n    return this.#project.createSourceFile(path)\n  }\n\n  private getFile(path: string) {\n    return this.#project.getSourceFile(path) || this.createFile(path)\n  }\n\n  private async createExportTree(doc: LexiconDocument) {\n    const namespaces = doc.id.split('.')\n\n    if (this.options.indexFile) {\n      const indexFile = this.getFile(`/index${this.fileExt}`)\n\n      const tldNs = namespaces[0]!\n      assert(\n        tldNs !== 'index',\n        'The \"indexFile\" options cannot be used with namespaces using a \".index\" tld.',\n      )\n      const tldNsSpecifier = `./${tldNs}${this.importExt}`\n      if (!indexFile.getExportDeclaration(tldNsSpecifier)) {\n        indexFile.addExportDeclaration({\n          moduleSpecifier: tldNsSpecifier,\n          namespaceExport: asNamespaceExport(tldNs),\n        })\n      }\n    }\n\n    // First create the parent namespaces\n    for (let i = 0; i < namespaces.length - 1; i++) {\n      const currentNs = namespaces[i]\n      const childNs = namespaces[i + 1]\n\n      const path = join('/', ...namespaces.slice(0, i + 1))\n      const file = this.getFile(`${path}${this.fileExt}`)\n\n      const childModuleSpecifier = `./${currentNs}/${childNs}${this.importExt}`\n      const dec = file.getExportDeclaration(childModuleSpecifier)\n      if (!dec) {\n        file.addExportDeclaration({\n          moduleSpecifier: childModuleSpecifier,\n          namespaceExport: asNamespaceExport(childNs),\n        })\n      }\n    }\n\n    // The child file exports the schemas (as *)\n    const path = join('/', ...namespaces)\n    const file = this.getFile(`${path}${this.fileExt}`)\n\n    file.addExportDeclaration({\n      moduleSpecifier: `./${namespaces.at(-1)}.defs${this.importExt}`,\n    })\n\n    // @NOTE Individual exports exports from the defs file might conflict with\n    // child namespaces. For this reason, we also add a namespace export for the\n    // defs (export * as $defs from './xyz.defs'). This is an escape hatch\n    // allowing to still access the definitions if a hash get shadowed by a\n    // child namespace.\n    file.addExportDeclaration({\n      moduleSpecifier: `./${namespaces.at(-1)}.defs${this.importExt}`,\n      namespaceExport: '$defs',\n    })\n  }\n\n  private async createDefsFile(\n    doc: LexiconDocument,\n    indexer: LexiconIndexer,\n  ): Promise<void> {\n    const path = join('/', ...doc.id.split('.'))\n    const file = this.createFile(`${path}.defs${this.fileExt}`)\n\n    const fileBuilder = new LexDefBuilder(this.options, file, doc, indexer)\n    await fileBuilder.build()\n  }\n}\n\nasync function assertNotFileExists(file: string): Promise<void> {\n  try {\n    await stat(file)\n    throw new Error(`File already exists: ${file}`)\n  } catch (err) {\n    if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return\n    throw err\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/lex-def-builder.ts",
    "content": "import {\n  JSDocStructure,\n  OptionalKind,\n  SourceFile,\n  VariableDeclarationKind,\n} from 'ts-morph'\nimport {\n  LexiconArray,\n  LexiconArrayItems,\n  LexiconBlob,\n  LexiconBoolean,\n  LexiconBytes,\n  LexiconCid,\n  LexiconDocument,\n  LexiconError,\n  LexiconIndexer,\n  LexiconInteger,\n  LexiconObject,\n  LexiconParameters,\n  LexiconPayload,\n  LexiconPermissionSet,\n  LexiconProcedure,\n  LexiconQuery,\n  LexiconRecord,\n  LexiconRef,\n  LexiconRefUnion,\n  LexiconString,\n  LexiconSubscription,\n  LexiconToken,\n  LexiconUnknown,\n} from '@atproto/lex-document'\nimport { l } from '@atproto/lex-schema'\nimport {\n  RefResolver,\n  RefResolverOptions,\n  ResolvedRef,\n  getPublicIdentifiers,\n} from './ref-resolver.js'\nimport { asNamespaceExport } from './ts-lang.js'\n\n/**\n * Configuration options for the {@link LexDefBuilder} class.\n *\n * @see {@link RefResolverOptions} for reference resolution options\n */\nexport type LexDefBuilderOptions = RefResolverOptions & {\n  /**\n   * The module specifier to use for importing the lexicon schema library.\n   *\n   * @default '@atproto/lex-schema'\n   */\n  lib?: string\n  /**\n   * Whether to allow legacy blob references in the generated schemas.\n   *\n   * When `true`, blob types will accept both modern `BlobRef` and legacy\n   * `LegacyBlobRef` formats.\n   *\n   * @default false\n   */\n  allowLegacyBlobs?: boolean\n  /**\n   * Whether to add `#__PURE__` annotations to function calls.\n   *\n   * These annotations help bundlers with tree-shaking by marking\n   * side-effect-free function calls.\n   *\n   * @default false\n   */\n  pureAnnotations?: boolean\n}\n\n/**\n * Builds TypeScript type definitions and runtime schemas from a single\n * Lexicon document.\n *\n * This class is responsible for generating the `.defs.ts` files that contain:\n * - Type aliases for each lexicon definition\n * - Runtime schema validators using `@atproto/lex-schema`\n * - Utility functions for type checking and validation\n * - Proper import statements for cross-references\n *\n * Each lexicon definition type (record, object, query, procedure, etc.)\n * is handled with specialized code generation logic.\n */\nexport class LexDefBuilder {\n  private readonly refResolver: RefResolver\n\n  constructor(\n    private readonly options: LexDefBuilderOptions,\n    private readonly file: SourceFile,\n    private readonly doc: LexiconDocument,\n    indexer: LexiconIndexer,\n  ) {\n    this.refResolver = new RefResolver(doc, file, indexer, options)\n  }\n\n  private pure(code: string) {\n    return this.options.pureAnnotations ? markPure(code) : code\n  }\n\n  async build() {\n    this.file.addVariableStatement({\n      declarationKind: VariableDeclarationKind.Const,\n      declarations: [\n        { name: '$nsid', initializer: JSON.stringify(this.doc.id) },\n      ],\n    })\n\n    this.file.addExportDeclaration({\n      namedExports: [{ name: '$nsid' }],\n    })\n\n    const defs = Object.keys(this.doc.defs)\n    if (defs.length) {\n      const moduleSpecifier = this.options?.lib ?? '@atproto/lex-schema'\n      this.file\n        .addImportDeclaration({ moduleSpecifier })\n        .addNamedImports([{ name: 'l' }])\n\n      for (const hash of defs) {\n        await this.addDef(hash)\n      }\n    }\n  }\n\n  private addUtils(definitions: Record<string, undefined | string>) {\n    const entries = Object.entries(definitions).filter(\n      (e): e is [(typeof e)[0], NonNullable<(typeof e)[1]>] => e[1] != null,\n    )\n    if (entries.length) {\n      this.file.addVariableStatement({\n        isExported: true,\n        declarationKind: VariableDeclarationKind.Const,\n        declarations: entries.map(([name, initializer]) => ({\n          name,\n          initializer,\n        })),\n      })\n    }\n  }\n\n  private async addDef(hash: string) {\n    const def = Object.hasOwn(this.doc.defs, hash) ? this.doc.defs[hash] : null\n    if (def == null) return\n\n    switch (def.type) {\n      case 'permission-set':\n        return this.addPermissionSet(hash, def)\n      case 'procedure':\n        return this.addProcedure(hash, def)\n      case 'query':\n        return this.addQuery(hash, def)\n      case 'subscription':\n        return this.addSubscription(hash, def)\n      case 'record':\n        return this.addRecord(hash, def)\n      case 'token':\n        return this.addToken(hash, def)\n      case 'object':\n        return this.addObject(hash, def)\n      case 'array':\n        return this.addArray(hash, def)\n      default:\n        await this.addSchema(hash, def, {\n          type: await this.compileContainedType(def),\n          schema: await this.compileContainedSchema(def),\n          validationUtils: true,\n        })\n    }\n  }\n\n  private async addPermissionSet(hash: string, def: LexiconPermissionSet) {\n    const permission = def.permissions.map((def) => {\n      const options = stringifyOptions(def, undefined, ['resource', 'type'])\n      return this.pure(\n        `l.permission(${JSON.stringify(def.resource)}, ${options})`,\n      )\n    })\n\n    const options = stringifyOptions(def, [\n      'title',\n      'title:lang',\n      'detail',\n      'detail:lang',\n    ] satisfies (keyof l.PermissionSetOptions)[])\n\n    await this.addSchema(hash, def, {\n      schema: this.pure(\n        `l.permissionSet($nsid, [${permission.join(',')}], ${options})`,\n      ),\n    })\n  }\n\n  private async addProcedure(hash: string, def: LexiconProcedure) {\n    if (hash !== 'main') {\n      throw new Error(`Definition ${hash} cannot be of type ${def.type}`)\n    }\n\n    // @TODO Build the types instead of using an inferred type.\n\n    const ref = await this.addSchema(hash, def, {\n      schema: this.pure(`\n        l.procedure(\n          $nsid,\n          ${await this.compileParamsSchema(def.parameters)},\n          ${await this.compilePayload(def.input)},\n          ${await this.compilePayload(def.output)},\n          ${await this.compileErrors(def.errors)}\n        )\n      `),\n    })\n\n    this.addMethodTypeUtils(ref, def)\n    this.addUtils({\n      $lxm: this.pure(`${ref.varName}.nsid`),\n      $params: this.pure(`${ref.varName}.parameters`),\n      $input: this.pure(`${ref.varName}.input`),\n      $output: this.pure(`${ref.varName}.output`),\n    })\n  }\n\n  private async addQuery(hash: string, def: LexiconQuery) {\n    if (hash !== 'main') {\n      throw new Error(`Definition ${hash} cannot be of type ${def.type}`)\n    }\n\n    // @TODO Build the types instead of using an inferred type.\n\n    const ref = await this.addSchema(hash, def, {\n      schema: this.pure(`\n        l.query(\n          $nsid,\n          ${await this.compileParamsSchema(def.parameters)},\n          ${await this.compilePayload(def.output)},\n          ${await this.compileErrors(def.errors)}\n        )\n      `),\n    })\n\n    this.addMethodTypeUtils(ref, def)\n    this.addUtils({\n      $lxm: this.pure(`${ref.varName}.nsid`),\n      $params: `${ref.varName}.parameters`,\n      $output: `${ref.varName}.output`,\n    })\n  }\n\n  private async addSubscription(hash: string, def: LexiconSubscription) {\n    if (hash !== 'main') {\n      throw new Error(`Definition ${hash} cannot be of type ${def.type}`)\n    }\n\n    // @TODO Build the types instead of using an inferred type.\n\n    const ref = await this.addSchema(hash, def, {\n      schema: this.pure(`\n        l.subscription(\n          $nsid,\n          ${await this.compileParamsSchema(def.parameters)},\n          ${await this.compileBodySchema(def.message?.schema)},\n          ${await this.compileErrors(def.errors)}\n        )\n      `),\n    })\n\n    this.addMethodTypeUtils(ref, def)\n    this.addUtils({\n      $lxm: this.pure(`${ref.varName}.nsid`),\n      $params: `${ref.varName}.parameters`,\n      $message: `${ref.varName}.message`,\n    })\n  }\n\n  addMethodTypeUtils(\n    ref: ResolvedRef,\n    def: LexiconProcedure | LexiconQuery | LexiconSubscription,\n  ) {\n    this.file.addTypeAlias({\n      isExported: true,\n      name: '$Params',\n      type: `l.InferMethodParams<typeof ${ref.varName}>`,\n      docs: compileDocs(def.parameters?.description),\n    })\n\n    if (def.type === 'procedure') {\n      this.file.addTypeAlias({\n        isExported: true,\n        name: '$Input<B = l.BinaryData>',\n        type: `l.InferMethodInput<typeof ${ref.varName}, B>`,\n        docs: compileDocs(def.input?.description),\n      })\n\n      this.file.addTypeAlias({\n        isExported: true,\n        name: '$InputBody<B = l.BinaryData>',\n        type: `l.InferMethodInputBody<typeof ${ref.varName}, B>`,\n        docs: compileDocs(def.input?.description),\n      })\n    }\n\n    if (def.type === 'procedure' || def.type === 'query') {\n      this.file.addTypeAlias({\n        isExported: true,\n        name: '$Output<B = l.BinaryData>',\n        type: `l.InferMethodOutput<typeof ${ref.varName}, B>`,\n        docs: compileDocs(def.output?.description),\n      })\n\n      this.file.addTypeAlias({\n        isExported: true,\n        name: '$OutputBody<B = l.BinaryData>',\n        type: `l.InferMethodOutputBody<typeof ${ref.varName}, B>`,\n        docs: compileDocs(def.output?.description),\n      })\n    }\n\n    if (def.type === 'subscription') {\n      this.file.addTypeAlias({\n        isExported: true,\n        name: '$Message',\n        type: `l.InferSubscriptionMessage<typeof ${ref.varName}>`,\n        docs: compileDocs(def.message?.description),\n      })\n    }\n  }\n\n  private async addRecord(hash: string, def: LexiconRecord) {\n    if (hash !== 'main') {\n      throw new Error(`Definition ${hash} cannot be of type ${def.type}`)\n    }\n\n    const key = JSON.stringify(def.key ?? 'any')\n    const objectSchema = await this.compileObjectSchema(def.record)\n\n    const properties = await this.compilePropertiesTypes(def.record)\n    properties.unshift(`$type: ${JSON.stringify(l.$type(this.doc.id, hash))}`)\n\n    await this.addSchema(hash, def, {\n      type: `{ ${properties.join(';')} }`,\n      schema: (ref) =>\n        this.pure(\n          `l.record<${key}, ${ref.typeName}>(${key}, $nsid, ${objectSchema})`,\n        ),\n      objectUtils: true,\n      validationUtils: true,\n    })\n  }\n\n  private async addObject(hash: string, def: LexiconObject) {\n    const objectSchema = await this.compileObjectSchema(def)\n\n    const properties = await this.compilePropertiesTypes(def)\n    properties.unshift(`$type?: ${JSON.stringify(l.$type(this.doc.id, hash))}`)\n\n    await this.addSchema(hash, def, {\n      type: `{ ${properties.join(';')} }`,\n      schema: (ref) =>\n        this.pure(\n          `l.typedObject<${ref.typeName}>($nsid, ${JSON.stringify(hash)}, ${objectSchema})`,\n        ),\n      objectUtils: true,\n      validationUtils: true,\n    })\n  }\n\n  private async addToken(hash: string, def: LexiconToken) {\n    await this.addSchema(hash, def, {\n      schema: this.pure(`l.token($nsid, ${JSON.stringify(hash)})`),\n      type: JSON.stringify(l.$type(this.doc.id, hash)),\n      validationUtils: true,\n    })\n  }\n\n  private async addArray(hash: string, def: LexiconArray) {\n    // @TODO It could be nice to expose the array item type as a separate type.\n    // This was not done (yet) as there is no easy way to name it to avoid\n    // collisions.\n\n    const itemSchema = await this.compileContainedSchema(def.items)\n    const options = stringifyOptions(def, [\n      'minLength',\n      'maxLength',\n    ] satisfies (keyof l.ArraySchemaOptions)[])\n\n    await this.addSchema(hash, def, {\n      type: `(${await this.compileContainedType(def.items)})[]`,\n      // @NOTE Not using compileArraySchema to allow specifying the generic\n      // parameter to l.array<>.\n      schema: (ref) =>\n        this.pure(\n          `l.array<${ref.typeName}[number]>(${itemSchema}, ${options})`,\n        ),\n      validationUtils: true,\n    })\n  }\n\n  private async addSchema(\n    hash: string,\n    def: { description?: string },\n    {\n      type,\n      schema,\n      objectUtils,\n      validationUtils,\n    }: {\n      type?: string | ((ref: ResolvedRef) => string)\n      schema?: string | ((ref: ResolvedRef) => string)\n      objectUtils?: boolean\n      validationUtils?: boolean\n    },\n  ): Promise<ResolvedRef> {\n    const ref = await this.refResolver.resolveLocal(hash)\n    const pub = getPublicIdentifiers(hash)\n\n    if (type) {\n      this.file.addTypeAlias({\n        name: ref.typeName,\n        type: typeof type === 'function' ? type(ref) : type,\n        docs: compileDocs(def.description),\n      })\n\n      this.file.addExportDeclaration({\n        isTypeOnly: true,\n        namedExports: [\n          {\n            name: ref.typeName,\n            alias:\n              ref.typeName === pub.typeName\n                ? undefined\n                : asNamespaceExport(pub.typeName),\n          },\n        ],\n      })\n    }\n\n    if (schema) {\n      this.file.addVariableStatement({\n        declarationKind: VariableDeclarationKind.Const,\n        declarations: [\n          {\n            name: ref.varName,\n            initializer: typeof schema === 'function' ? schema(ref) : schema,\n          },\n        ],\n        docs: compileDocs(def.description),\n      })\n\n      this.file.addExportDeclaration({\n        namedExports: [\n          {\n            name: ref.varName,\n            alias:\n              ref.varName === pub.varName\n                ? undefined\n                : asNamespaceExport(pub.varName),\n          },\n        ],\n      })\n    }\n\n    if (hash === 'main' && objectUtils) {\n      this.addUtils({\n        $isTypeOf: markPure(`${ref.varName}.isTypeOf.bind(${ref.varName})`),\n        $build: markPure(`${ref.varName}.build.bind(${ref.varName})`),\n        $type: markPure(`${ref.varName}.$type`),\n      })\n    }\n\n    if (hash === 'main' && validationUtils) {\n      this.addUtils({\n        $assert: markPure(`${ref.varName}.assert.bind(${ref.varName})`),\n        $check: markPure(`${ref.varName}.check.bind(${ref.varName})`),\n        $cast: markPure(`${ref.varName}.cast.bind(${ref.varName})`),\n        $ifMatches: markPure(`${ref.varName}.ifMatches.bind(${ref.varName})`),\n        $matches: markPure(`${ref.varName}.matches.bind(${ref.varName})`),\n        $parse: markPure(`${ref.varName}.parse.bind(${ref.varName})`),\n        $safeParse: markPure(`${ref.varName}.safeParse.bind(${ref.varName})`),\n        $validate: markPure(`${ref.varName}.validate.bind(${ref.varName})`),\n        $safeValidate: markPure(\n          `${ref.varName}.safeValidate.bind(${ref.varName})`,\n        ),\n      })\n    }\n\n    return ref\n  }\n\n  private async compilePayload(def: LexiconPayload | undefined) {\n    if (!def) return this.pure(`l.payload()`)\n\n    // Special case for JSON object payloads\n    if (def.encoding === 'application/json' && def.schema?.type === 'object') {\n      const properties = await this.compilePropertiesSchemas(def.schema)\n      return this.pure(`l.jsonPayload({${properties.join(',')}})`)\n    }\n\n    const encodedEncoding = JSON.stringify(def.encoding)\n    if (def.schema) {\n      const bodySchema = await this.compileBodySchema(def.schema)\n      return this.pure(`l.payload(${encodedEncoding}, ${bodySchema})`)\n    } else {\n      return this.pure(`l.payload(${encodedEncoding})`)\n    }\n  }\n\n  private async compileBodySchema(\n    def?: LexiconRef | LexiconRefUnion | LexiconObject,\n  ): Promise<string> {\n    if (!def) return 'undefined'\n    if (def.type === 'object') return this.compileObjectSchema(def)\n    return this.compileContainedSchema(def)\n  }\n\n  private async compileParamsSchema(def: undefined | LexiconParameters) {\n    if (!def) return this.pure(`l.params()`)\n\n    const properties = await this.compilePropertiesSchemas(def)\n    return this.pure(\n      properties.length === 0\n        ? `l.params()`\n        : `l.params({${properties.join(',')}})`,\n    )\n  }\n\n  private async compileErrors(defs?: readonly LexiconError[]) {\n    if (!defs?.length) return ''\n    return JSON.stringify(defs.map((d) => d.name))\n  }\n\n  private async compileObjectSchema(def: LexiconObject): Promise<string> {\n    const properties = await this.compilePropertiesSchemas(def)\n    return this.pure(`l.object({${properties.join(',')}})`)\n  }\n\n  private async compilePropertiesSchemas(options: {\n    properties: Record<string, LexiconArray | LexiconArrayItems>\n    required?: readonly string[]\n    nullable?: readonly string[]\n  }): Promise<string[]> {\n    for (const opt of ['required', 'nullable'] as const) {\n      if (options[opt]) {\n        for (const prop of options[opt]) {\n          if (!Object.hasOwn(options.properties, prop)) {\n            throw new Error(`No schema found for ${opt} property \"${prop}\"`)\n          }\n        }\n      }\n    }\n\n    return Promise.all(\n      Object.entries(options.properties).map((entry) => {\n        return this.compilePropertyEntrySchema(entry, options)\n      }),\n    )\n  }\n\n  private async compilePropertiesTypes(options: {\n    properties: Record<string, LexiconArray | LexiconArrayItems>\n    required?: readonly string[]\n    nullable?: readonly string[]\n  }) {\n    return Promise.all(\n      Object.entries(options.properties).map((entry) => {\n        return this.compilePropertyEntryType(entry, options)\n      }),\n    )\n  }\n\n  private async compilePropertyEntrySchema(\n    [key, def]: [string, LexiconArray | LexiconArrayItems],\n    options: {\n      required?: readonly string[]\n      nullable?: readonly string[]\n    },\n  ) {\n    const isNullable = options.nullable?.includes(key)\n    const isRequired = options.required?.includes(key)\n\n    let schema = await this.compileContainedSchema(def)\n\n    if (isNullable) {\n      schema = this.pure(`l.nullable(${schema})`)\n    }\n\n    if (!isRequired) {\n      schema = this.pure(`l.optional(${schema})`)\n    }\n\n    return `${JSON.stringify(key)}:${schema}`\n  }\n\n  private async compilePropertyEntryType(\n    [key, def]: [string, LexiconArray | LexiconArrayItems],\n    options: {\n      required?: readonly string[]\n      nullable?: readonly string[]\n    },\n  ) {\n    const isNullable = options.nullable?.includes(key)\n    const isRequired = options.required?.includes(key)\n\n    const optional = isRequired ? '' : '?'\n    const append = isNullable ? ' | null' : ''\n\n    const jsDoc = compileLeadingTrivia(def.description) || ''\n    const name = JSON.stringify(key)\n    const type = await this.compileContainedType(def)\n\n    return `${jsDoc}${name}${optional}:${type}${append}`\n  }\n\n  private async compileContainedSchema(\n    def: LexiconArray | LexiconArrayItems,\n  ): Promise<string> {\n    switch (def.type) {\n      case 'unknown':\n        return this.compileUnknownSchema(def)\n      case 'boolean':\n        return this.compileBooleanSchema(def)\n      case 'integer':\n        return this.compileIntegerSchema(def)\n      case 'string':\n        return this.compileStringSchema(def)\n      case 'bytes':\n        return this.compileBytesSchema(def)\n      case 'blob':\n        return this.compileBlobSchema(def)\n      case 'cid-link':\n        return this.compileCidLinkSchema(def)\n      case 'ref':\n        return this.compileRefSchema(def)\n      case 'union':\n        return this.compileRefUnionSchema(def)\n      case 'array':\n        return this.compileArraySchema(def)\n      default:\n        // @ts-expect-error\n        throw new Error(`Unsupported def type: ${def.type}`)\n    }\n  }\n\n  private async compileContainedType(\n    def: LexiconArray | LexiconArrayItems,\n  ): Promise<string> {\n    switch (def.type) {\n      case 'unknown':\n        return this.compileUnknownType(def)\n      case 'boolean':\n        return this.compileBooleanType(def)\n      case 'integer':\n        return this.compileIntegerType(def)\n      case 'string':\n        return this.compileStringType(def)\n      case 'bytes':\n        return this.compileBytesType(def)\n      case 'blob':\n        return this.compileBlobType(def)\n      case 'cid-link':\n        return this.compileCidLinkType(def)\n      case 'ref':\n        return this.compileRefType(def)\n      case 'union':\n        return this.compileRefUnionType(def)\n      case 'array':\n        return this.compileArrayType(def)\n      default:\n        // @ts-expect-error\n        throw new Error(`Unsupported def type: ${def.type}`)\n    }\n  }\n\n  private async compileArraySchema(def: LexiconArray): Promise<string> {\n    const itemSchema = await this.compileContainedSchema(def.items)\n    const options = stringifyOptions(def, [\n      'minLength',\n      'maxLength',\n    ] satisfies (keyof l.ArraySchemaOptions)[])\n    return this.pure(`l.array(${itemSchema}, ${options})`)\n  }\n\n  private async compileArrayType(def: LexiconArray): Promise<string> {\n    return `(${await this.compileContainedType(def.items)})[]`\n  }\n\n  private async compileUnknownSchema(_def: LexiconUnknown): Promise<string> {\n    return this.pure(`l.lexMap()`)\n  }\n\n  private async compileUnknownType(_def: LexiconUnknown): Promise<string> {\n    return `l.LexMap`\n  }\n\n  private withDefault(schema: string, defaultValue: unknown) {\n    if (defaultValue === undefined) return schema\n\n    return this.pure(\n      `l.withDefault(${schema}, ${JSON.stringify(defaultValue)})`,\n    )\n  }\n\n  private async compileBooleanSchema(def: LexiconBoolean): Promise<string> {\n    const schema = l.boolean()\n\n    if (def.default !== undefined) {\n      schema.check(def.default)\n    }\n\n    if (hasConst(def)) return this.compileConstSchema(def)\n\n    return this.withDefault(this.pure(`l.boolean()`), def.default)\n  }\n\n  private async compileBooleanType(def: LexiconBoolean): Promise<string> {\n    if (hasConst(def)) return this.compileConstType(def)\n    return 'boolean'\n  }\n\n  private async compileIntegerSchema(def: LexiconInteger): Promise<string> {\n    const schema = l.integer(def)\n\n    if (hasConst(def)) {\n      schema.check(def.const)\n    }\n\n    if (hasEnum(def)) {\n      for (const val of def.enum) schema.check(val)\n    }\n\n    if (def.default !== undefined) {\n      schema.check(def.default)\n    }\n\n    if (hasConst(def)) return this.compileConstSchema(def)\n    if (hasEnum(def)) return this.compileEnumSchema(def)\n\n    const options = stringifyOptions(def, [\n      'maximum',\n      'minimum',\n    ] satisfies (keyof l.IntegerSchemaOptions)[])\n\n    return this.withDefault(this.pure(`l.integer(${options})`), def.default)\n  }\n\n  private async compileIntegerType(def: LexiconInteger): Promise<string> {\n    if (hasConst(def)) return this.compileConstType(def)\n    if (hasEnum(def)) return this.compileEnumType(def)\n\n    return 'number'\n  }\n\n  private async compileStringSchema(def: LexiconString): Promise<string> {\n    const schema = l.string(def)\n\n    if (hasConst(def)) {\n      schema.check(def.const)\n    }\n\n    if (hasEnum(def)) {\n      for (const val of def.enum) schema.check(val)\n    }\n\n    if (def.default !== undefined) {\n      schema.check(def.default)\n    }\n\n    if (hasConst(def)) return this.compileConstSchema(def)\n    if (hasEnum(def)) return this.compileEnumSchema(def)\n\n    const runtimeOptions = [\n      'format',\n      'maxGraphemes',\n      'minGraphemes',\n      'maxLength',\n      'minLength',\n      // We don't want to include knownValues in the schema options **at\n      // runtime** as it has no effect and only causes bloat:\n      // \"knownValues\",\n    ] as const satisfies (keyof l.StringSchemaOptions)[]\n\n    const options = stringifyOptions(def, runtimeOptions)\n\n    // We *do* however need knownValues for the inferred type, so we include it\n    // as the generic parameter. We only do this if the def has knownValues,\n    // otherwise we let TypeScript infer the options generic by not defining it.\n    const generic = def.knownValues\n      ? stringifyOptions(def, [\n          ...runtimeOptions,\n          'knownValues',\n        ] satisfies (keyof l.StringSchemaOptions)[])\n      : undefined\n\n    return this.withDefault(\n      this.pure(`l.string${generic ? `<${generic}>` : ''}(${options})`),\n      def.default,\n    )\n  }\n\n  private async compileStringType(def: LexiconString): Promise<string> {\n    if (hasConst(def)) return this.compileConstType(def)\n    if (hasEnum(def)) return this.compileEnumType(def)\n\n    switch (def.format) {\n      case undefined:\n        break\n      case 'datetime':\n        return 'l.DatetimeString'\n      case 'uri':\n        return 'l.UriString'\n      case 'at-uri':\n        return 'l.AtUriString'\n      case 'did':\n        return 'l.DidString'\n      case 'handle':\n        return 'l.HandleString'\n      case 'at-identifier':\n        return 'l.AtIdentifierString'\n      case 'nsid':\n        return 'l.NsidString'\n      case 'tid':\n        return 'l.TidString'\n      case 'cid':\n        return 'l.CidString'\n      case 'language':\n        return 'l.LanguageString'\n      case 'record-key':\n        return 'l.RecordKeyString'\n      default:\n        throw new Error(`Unknown string format: ${def.format}`)\n    }\n\n    if (def.knownValues?.length) {\n      return (\n        def.knownValues.map((v) => JSON.stringify(v)).join(' | ') +\n        ' | l.UnknownString'\n      )\n    }\n\n    return 'string'\n  }\n\n  private async compileBytesSchema(def: LexiconBytes): Promise<string> {\n    const options = stringifyOptions(def, [\n      'minLength',\n      'maxLength',\n    ] satisfies (keyof l.BytesSchemaOptions)[])\n    return this.pure(`l.bytes(${options})`)\n  }\n\n  private async compileBytesType(_def: LexiconBytes): Promise<string> {\n    return 'Uint8Array'\n  }\n\n  private async compileBlobSchema(def: LexiconBlob): Promise<string> {\n    const opts = { ...def, allowLegacy: this.options.allowLegacyBlobs === true }\n    const options = stringifyOptions(opts, [\n      'maxSize',\n      'accept',\n      'allowLegacy',\n    ] satisfies (keyof l.BlobSchemaOptions)[])\n    return this.pure(`l.blob(${options})`)\n  }\n\n  private async compileBlobType(_def: LexiconBlob): Promise<string> {\n    return this.options.allowLegacyBlobs\n      ? 'l.BlobRef | l.LegacyBlobRef'\n      : 'l.BlobRef'\n  }\n\n  private async compileCidLinkSchema(_def: LexiconCid): Promise<string> {\n    return this.pure(`l.cid()`)\n  }\n\n  private async compileCidLinkType(_def: LexiconCid): Promise<string> {\n    return 'l.Cid'\n  }\n\n  private async compileRefSchema(def: LexiconRef): Promise<string> {\n    const { varName, typeName } = await this.refResolver.resolve(def.ref)\n    // @NOTE \"as any\" is needed in schemas with circular refs as TypeScript\n    // cannot infer the type of a value that depends on its initializer type\n    return this.pure(`l.ref<${typeName}>((() => ${varName}) as any)`)\n  }\n\n  private async compileRefType(def: LexiconRef): Promise<string> {\n    const ref = await this.refResolver.resolve(def.ref)\n    return ref.typeName\n  }\n\n  private async compileRefUnionSchema(def: LexiconRefUnion): Promise<string> {\n    if (def.refs.length === 0 && def.closed) {\n      return this.pure(`l.never()`)\n    }\n\n    const refs = await Promise.all(\n      def.refs.map(async (ref: string) => {\n        const { varName, typeName } = await this.refResolver.resolve(ref)\n        // @NOTE \"as any\" is needed in schemas with circular refs as TypeScript\n        // cannot infer the type of a value that depends on its initializer type\n        return this.pure(`l.typedRef<${typeName}>((() => ${varName}) as any)`)\n      }),\n    )\n\n    return this.pure(\n      `l.typedUnion([${refs.join(',')}], ${def.closed ?? false})`,\n    )\n  }\n\n  private async compileRefUnionType(def: LexiconRefUnion): Promise<string> {\n    const types = await Promise.all(\n      def.refs.map(async (ref) => {\n        const { typeName } = await this.refResolver.resolve(ref)\n        return `l.$Typed<${typeName}>`\n      }),\n    )\n    if (!def.closed) types.push('l.Unknown$TypedObject')\n    return types.join(' | ') || 'never'\n  }\n\n  private async compileConstSchema<\n    T extends null | number | string | boolean,\n  >(def: { const: T; enum?: readonly T[]; default?: T }): Promise<string> {\n    if (hasEnum(def) && !def.enum.includes(def.const)) {\n      return this.pure(`l.never()`)\n    }\n\n    const result = this.pure(`l.literal(${JSON.stringify(def.const)})`)\n\n    return this.withDefault(result, def.default)\n  }\n\n  private async compileConstType<\n    T extends null | number | string | boolean,\n  >(def: { const: T; enum?: readonly T[] }): Promise<string> {\n    if (hasEnum(def) && !def.enum.includes(def.const)) {\n      return 'never'\n    }\n    return JSON.stringify(def.const)\n  }\n\n  private async compileEnumSchema<T extends null | number | string>(def: {\n    enum: readonly T[]\n    default?: T\n  }): Promise<string> {\n    if (def.enum.length === 0) {\n      return this.pure(`l.never()`)\n    }\n\n    const result =\n      def.enum.length === 1\n        ? this.pure(`l.literal(${JSON.stringify(def.enum[0])})`)\n        : this.pure(`l.enum(${JSON.stringify(def.enum)})`)\n\n    return this.withDefault(result, def.default)\n  }\n\n  private async compileEnumType<T extends null | number | string>(def: {\n    enum: readonly T[]\n  }): Promise<string> {\n    return def.enum.map((v) => JSON.stringify(v)).join(' | ') || 'never'\n  }\n}\n\ntype ParsedDescription = OptionalKind<JSDocStructure> & {\n  description?: string\n  tags?: { tagName: string; text?: string }[]\n}\n\nfunction parseDescription(description: string): ParsedDescription {\n  if (/deprecated/i.test(description)) {\n    const deprecationMatch = description.match(\n      /(\\s*deprecated\\s*(?:--?|:)?\\s*([^-]*)(?:-+)?)/i,\n    )\n    if (deprecationMatch) {\n      const { 1: match, 2: deprecationNotice } = deprecationMatch\n      return {\n        description: description.replace(match, '').trim() || undefined,\n        tags: [{ tagName: 'deprecated', text: deprecationNotice?.trim() }],\n      }\n    } else {\n      return {\n        description: description.trim() || undefined,\n        tags: [{ tagName: 'deprecated' }],\n      }\n    }\n  }\n\n  return {\n    description: description.trim() || undefined,\n  }\n}\n\nfunction compileLeadingTrivia(description?: string) {\n  if (!description) return undefined\n  const parsed = parseDescription(description)\n  if (!parsed.description && !parsed.tags?.length) return undefined\n  const tags = parsed.tags\n    ?.map(({ tagName, text }) => (text ? `@${tagName} ${text}` : `@${tagName}`))\n    ?.join('\\n')\n  const text = `\\n${[parsed.description, tags].filter(Boolean).join('\\n\\n')}`\n  return `\\n\\n/**${text.replaceAll('\\n', '\\n * ')}\\n */\\n`\n}\n\nfunction compileDocs(description?: string) {\n  if (!description) return undefined\n  return [parseDescription(description)]\n}\n\nfunction stringifyOptions<O extends Record<string, unknown>>(\n  obj: O,\n  include?: (keyof O)[],\n  exclude?: (keyof O)[],\n) {\n  const filtered = Object.entries(obj).filter(\n    ([k]) => (!include || include.includes(k)) && !exclude?.includes(k),\n  )\n  return filtered.length ? JSON.stringify(Object.fromEntries(filtered)) : ''\n}\n\nfunction hasConst<T extends { const?: unknown }>(\n  def: T,\n): def is T & { const: NonNullable<T['const']> } {\n  return def.const != null\n}\n\nfunction hasEnum<T extends { enum?: readonly unknown[] }>(\n  def: T,\n): def is T & { enum: unknown[] } {\n  return def.enum != null\n}\n\nfunction markPure<T extends string>(v: T): `/*#__PURE__*/ ${T}` {\n  return `/*#__PURE__*/ ${v}`\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/lexicon-directory-indexer.ts",
    "content": "import { readFile, readdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport {\n  LexiconDocument,\n  LexiconIterableIndexer,\n  lexiconDocumentSchema,\n} from '@atproto/lex-document'\n\n/**\n * Options for the {@link LexiconDirectoryIndexer}.\n *\n * @see {@link ReadLexiconsOptions} for available options\n */\nexport type LexiconDirectoryIndexerOptions = ReadLexiconsOptions\n\n/**\n * Indexes lexicon documents from a filesystem directory.\n *\n * This class recursively scans a directory for JSON files, parses them as\n * lexicon documents, and provides an iterable interface for processing them.\n * It extends {@link LexiconIterableIndexer} to support both iteration and\n * lookup by NSID.\n */\nexport class LexiconDirectoryIndexer extends LexiconIterableIndexer {\n  constructor(options: LexiconDirectoryIndexerOptions) {\n    super(readLexicons(options))\n  }\n}\n\ntype ReadLexiconsOptions = {\n  lexicons: string\n  ignoreInvalidLexicons?: boolean\n}\n\nasync function* readLexicons(\n  options: ReadLexiconsOptions,\n): AsyncGenerator<LexiconDocument, void, unknown> {\n  for await (const filePath of listFiles(options.lexicons)) {\n    if (filePath.endsWith('.json')) {\n      try {\n        const data = await readFile(filePath, 'utf8')\n        yield lexiconDocumentSchema.parse(JSON.parse(data))\n      } catch (cause) {\n        const message = `Error parsing lexicon document ${filePath}`\n        if (options.ignoreInvalidLexicons) console.error(`${message}:`, cause)\n        else throw new Error(message, { cause })\n      }\n    }\n  }\n}\n\nasync function* listFiles(dir: string): AsyncGenerator<string> {\n  const dirents = await readdir(dir, { withFileTypes: true }).catch((err) => {\n    if ((err as any)?.code === 'ENOENT') return []\n    throw err\n  })\n  for (const dirent of dirents) {\n    const res = join(dir, dirent.name)\n    if (dirent.isDirectory()) {\n      yield* listFiles(res)\n    } else if (dirent.isFile() || dirent.isSymbolicLink()) {\n      yield res\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/polyfill.ts",
    "content": "// Node <18.18, 19.x, <20.4 and 21.x do not have these symbols defined\n\n// @ts-expect-error\nSymbol.asyncDispose ??= Symbol.for('nodejs.asyncDispose')\n\n// @ts-expect-error\nSymbol.dispose ??= Symbol.for('nodejs.dispose')\n"
  },
  {
    "path": "packages/lex/lex-builder/src/ref-resolver.test.ts",
    "content": "import { Project } from 'ts-morph'\nimport { describe, expect, it } from 'vitest'\nimport { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'\nimport { RefResolver } from './ref-resolver.js'\n\nclass DummyIndexer implements LexiconIndexer, AsyncIterable<LexiconDocument> {\n  readonly docs: Map<string, LexiconDocument>\n\n  constructor(docs: LexiconDocument[]) {\n    this.docs = new Map(docs.map((doc) => [doc.id, doc]))\n  }\n\n  async get(id: string): Promise<LexiconDocument> {\n    const doc = this.docs.get(id)\n    if (!doc) {\n      throw new Error(`Document not found: ${id}`)\n    }\n    return doc\n  }\n\n  async *[Symbol.asyncIterator]() {\n    for (const doc of this.docs.values()) {\n      yield doc\n    }\n  }\n}\n\ndescribe('RefResolver', () => {\n  const docs: LexiconDocument[] = [\n    {\n      lexicon: 1,\n      id: 'com.example.foo',\n      defs: {\n        main: { type: 'token' },\n      },\n    },\n    {\n      lexicon: 1,\n      id: 'com.example.bar',\n      defs: {\n        main: { type: 'token' },\n      },\n    },\n  ]\n\n  it('uses default relative path for external refs', async () => {\n    const project = new Project({ useInMemoryFileSystem: true })\n    const file = project.createSourceFile('/com/example/foo.defs.ts')\n    const indexer = new DummyIndexer(docs)\n    const resolver = new RefResolver(docs[0], file, indexer, {})\n\n    await resolver.resolve('com.example.bar#main')\n\n    const imports = file.getImportDeclarations()\n    expect(imports).toHaveLength(1)\n    expect(imports[0].getModuleSpecifierValue()).toBe('./bar.defs.js')\n  })\n\n  it('uses custom moduleSpecifier when provided', async () => {\n    const project = new Project({ useInMemoryFileSystem: true })\n    const file = project.createSourceFile('/com/example/foo.defs.ts')\n    const indexer = new DummyIndexer(docs)\n    const resolver = new RefResolver(docs[0], file, indexer, {\n      moduleSpecifier: (nsid) => `https://lex.example.com/${nsid}.ts`,\n    })\n\n    await resolver.resolve('com.example.bar#main')\n\n    const imports = file.getImportDeclarations()\n    expect(imports).toHaveLength(1)\n    expect(imports[0].getModuleSpecifierValue()).toBe(\n      'https://lex.example.com/com.example.bar.ts',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-builder/src/ref-resolver.ts",
    "content": "import assert from 'node:assert'\nimport { join } from 'node:path'\nimport { SourceFile } from 'ts-morph'\nimport { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'\nimport {\n  isGlobalIdentifier,\n  isJsKeyword,\n  isSafeLocalIdentifier,\n  isValidJsIdentifier,\n} from './ts-lang.js'\nimport {\n  asRelativePath,\n  memoize,\n  startsWithLower,\n  toCamelCase,\n  toPascalCase,\n  ucFirst,\n} from './util.js'\n\n/**\n * Configuration options for the {@link RefResolver} class.\n */\nexport type RefResolverOptions = {\n  /**\n   * The file extension to use for import specifiers when resolving\n   * external references.\n   *\n   * @default '.js'\n   */\n  importExt?: string\n  moduleSpecifier?: (nsid: string) => string\n}\n\n/**\n * Represents a resolved lexicon reference as TypeScript identifiers.\n *\n * Contains the variable name (for runtime schema) and type name (for\n * TypeScript type) that can be used to reference a lexicon definition.\n */\nexport type ResolvedRef = {\n  /**\n   * The variable name for the runtime schema.\n   *\n   * For local definitions, this is a simple identifier.\n   * For external definitions, this may be a qualified name like `ns.varName`\n   * or bracket notation like `ns[\"varName\"]` for unsafe identifiers.\n   */\n  varName: string\n  /**\n   * The type name for the TypeScript type alias.\n   *\n   * Always a valid TypeScript identifier, either simple or qualified.\n   */\n  typeName: string\n}\n\n/**\n * Resolves lexicon references to TypeScript identifiers.\n *\n * This class handles the resolution of `ref` types in lexicon documents,\n * converting lexicon reference strings (like `com.example.foo#bar`) into\n * valid TypeScript identifiers. It automatically manages:\n *\n * - Local references within the same document\n * - External references to other lexicon documents\n * - Import statement generation for external references\n * - Conflict avoidance with keywords, globals, and existing declarations\n *\n * Results are memoized to ensure consistent identifiers for the same\n * reference throughout a file.\n *\n * @example\n * ```ts\n * const resolver = new RefResolver(doc, sourceFile, indexer, options)\n *\n * // Resolve a local reference\n * const local = await resolver.resolve('#myDef')\n *\n * // Resolve an external reference\n * const external = await resolver.resolve('com.example.other#def')\n * ```\n */\nexport class RefResolver {\n  constructor(\n    private doc: LexiconDocument,\n    private file: SourceFile,\n    private indexer: LexiconIndexer,\n    private options: RefResolverOptions,\n  ) {}\n\n  public readonly resolve = memoize(\n    async (ref: string): Promise<ResolvedRef> => {\n      const [nsid, hash = 'main'] = ref.split('#')\n\n      if (nsid === '' || nsid === this.doc.id) {\n        return this.resolveLocal(hash)\n      } else {\n        // @NOTE: Normalize (#main fragment) to ensure proper memoization\n        const fullRef = `${nsid}#${hash}`\n        return this.resolveExternal(fullRef)\n      }\n    },\n  )\n\n  #defCounters = new Map<string, number>()\n  private nextSafeDefinitionIdentifier(name: string) {\n    // use camelCase version of the hash as base name\n    const nameSafe =\n      startsWithLower(name) && isValidJsIdentifier(name)\n        ? name\n        : toCamelCase(name).replace(/^[0-9]+/g, '') || 'def'\n\n    const count = this.#defCounters.get(nameSafe) ?? 0\n    this.#defCounters.set(nameSafe, count + 1)\n\n    // @NOTE We don't need to check against local declarations in the file here\n    // since we are using a naming system that should guarantee no other\n    // identifier has a <nameSafe>$<number> format (\"$\" cannot appear in\n    // hashes so only *we* are generating such identifiers).\n\n    const identifier = `${nameSafe}$${count}`\n\n    assert(\n      isValidJsIdentifier(identifier),\n      `Unable to generate safe identifier for: \"${name}\"`,\n    )\n\n    return identifier\n  }\n\n  /**\n   * Resolves a local definition hash to TypeScript identifiers.\n   *\n   * This method generates safe, non-conflicting identifiers for definitions\n   * within the current document. It handles edge cases like:\n   * - Hash names that are JavaScript keywords\n   * - Hash names that conflict with global identifiers\n   * - Multiple hashes that would produce the same identifier\n   *\n   * @param hash - The definition hash (e.g., 'main', 'record', 'myType')\n   * @returns A promise resolving to the TypeScript identifiers\n   * @throws Error if the hash does not exist in the document\n   * @throws Error if conflicting type names are detected\n   *\n   * @note The returned `typeName` and `varName` are *both* guaranteed to be\n   * valid TypeScript identifiers.\n   */\n  public readonly resolveLocal = memoize(\n    async (hash: string): Promise<ResolvedRef> => {\n      const hashes = Object.keys(this.doc.defs)\n\n      if (!hashes.includes(hash)) {\n        throw new Error(`Definition ${hash} not found in ${this.doc.id}`)\n      }\n\n      // Because we are using predictable \"public\" identifiers for type names,\n      // we need to ensure there are no conflicts between different definitions\n      // in the same lexicon document.\n      //\n      // @NOTE It should be possible to implement a way to generate\n      // non-conflicting type names for all public (type) identifiers in a\n      // project. However, this would add a lot of complexity to the code\n      // generation process, and the likelihood of such conflicts happening in\n      // practice is very low, so we opt for a simpler approach of just throwing\n      // an error if a conflict is detected.\n      const pub = getPublicIdentifiers(hash)\n      for (const otherHash of hashes) {\n        if (otherHash === hash) continue\n        const otherPub = getPublicIdentifiers(otherHash)\n        if (otherPub.typeName === pub.typeName) {\n          throw new Error(\n            `Conflicting type names for definitions #${hash} and #${otherHash} in ${this.doc.id}`,\n          )\n        }\n      }\n\n      // Try to keep and identifier that resembles the original hash as identifier\n      const safeIdentifier = asSafeDefinitionIdentifier(hash)\n\n      // If the safe identifier is not conflicting with other definition names,\n      // or reserved words, we can use it as-is. Otherwise, we need to generate\n      // a unique safe identifier.\n      const varName = safeIdentifier\n        ? !hashes.some((otherHash) => {\n            if (otherHash === hash) return false\n            const otherIdentifier = asSafeDefinitionIdentifier(otherHash)\n            return otherIdentifier === safeIdentifier\n          })\n          ? // Safe identifier can be used as-is as it does not conflict with\n            // other definition names\n            safeIdentifier\n          : // In order to keep identifiers stable, we use the safe identifier\n            // as base, and append a counter to avoid conflicts\n            this.nextSafeDefinitionIdentifier(safeIdentifier)\n        : // hash only contained unsafe characters, generate a safe one\n          this.nextSafeDefinitionIdentifier(hash)\n\n      const typeName = ucFirst(varName)\n      assert(isSafeLocalIdentifier(typeName), 'Expected safe type identifier')\n      assert(varName !== typeName, 'Variable and type name should be different')\n\n      return { varName, typeName }\n    },\n  )\n\n  /**\n   * @note Since this is a memoized function, and is used to generate the name\n   * of local variables, we should avoid returning different results for\n   * similar, but non strictly equal, inputs (eg. normalized / non-normalized).\n   * @see {@link resolve}\n   */\n  private readonly resolveExternal = memoize(\n    async (fullRef: string): Promise<ResolvedRef> => {\n      const [nsid, hash] = fullRef.split('#')\n      const moduleSpecifier = this.options.moduleSpecifier\n        ? this.options.moduleSpecifier(nsid)\n        : `${asRelativePath(\n            this.file.getDirectoryPath(),\n            join('/', ...nsid.split('.')),\n          )}.defs${this.options.importExt ?? '.js'}`\n\n      // Lets first make sure the referenced lexicon exists\n      const srcDoc = await this.indexer.get(nsid)\n      const srcDef = Object.hasOwn(srcDoc.defs, hash) ? srcDoc.defs[hash] : null\n      if (!srcDef) {\n        throw new Error(\n          `Missing def \"${hash}\" in \"${nsid}\" (referenced from ${this.doc.id})`,\n        )\n      }\n\n      const publicIds = getPublicIdentifiers(hash)\n\n      if (!isValidJsIdentifier(publicIds.typeName)) {\n        // If <typeName> is not a valid identifier, we cannot access the type\n        // using dot notation (<nsIdentifier>.<typeName>). Note that, unlike js\n        // variables, types cannot be accessed using string indexing (like:\n        // <nsIdentifier>['<typeName>']) because it generates TypeScript errors:\n        //\n        // > \"Cannot use namespace '<nsIdentifier>' as a type.\"\n\n        // Instead the generated code should look like:\n        // import { \"<unsafeTypeName>\" as <safeIdentifier> } from './<moduleSpecifier>'\n\n        // Because it requires more complex management of local variables names,\n        // and we don't expect this to actually happen with properly designed\n        // lexicons documents, we do not support this for now.\n\n        throw new Error(\n          'Import of definitions with unsafe type names is not supported',\n        )\n      }\n\n      // import * as <nsIdentifier> from './<moduleSpecifier>'\n      const nsIdentifier = this.getNsIdentifier(nsid, moduleSpecifier)\n\n      return {\n        varName: isValidJsIdentifier(publicIds.varName)\n          ? `${nsIdentifier}.${publicIds.varName}`\n          : `${nsIdentifier}[${JSON.stringify(publicIds.varName)}]`,\n        typeName: `${nsIdentifier}.${publicIds.typeName}`,\n      }\n    },\n  )\n\n  private getNsIdentifier(nsid: string, moduleSpecifier: string) {\n    const namespaceImportDeclaration =\n      this.file.getImportDeclaration(\n        (imp) =>\n          !imp.isTypeOnly() &&\n          imp.getModuleSpecifierValue() === moduleSpecifier &&\n          imp.getNamespaceImport() != null,\n      ) ||\n      this.file.addImportDeclaration({\n        moduleSpecifier,\n        namespaceImport: this.computeSafeNamespaceIdentifierFor(nsid),\n      })\n\n    return namespaceImportDeclaration.getNamespaceImport()!.getText()\n  }\n\n  #nsIdentifiersCounters = new Map<string, number>()\n  private computeSafeNamespaceIdentifierFor(nsid: string) {\n    const baseName = nsidToIdentifier(nsid) || 'NS'\n\n    let name = baseName\n    while (this.isConflictingIdentifier(name)) {\n      const count = this.#nsIdentifiersCounters.get(baseName) ?? 0\n      this.#nsIdentifiersCounters.set(baseName, count + 1)\n      name = `${baseName}$$${count}`\n    }\n\n    return name\n  }\n\n  private isConflictingIdentifier(name: string) {\n    return (\n      this.conflictsWithKeywords(name) ||\n      this.conflictsWithUtils(name) ||\n      this.conflictsWithLocalDefs(name) ||\n      this.conflictsWithLocalDeclarations(name) ||\n      this.conflictsWithImports(name)\n    )\n  }\n\n  private conflictsWithKeywords(name: string) {\n    return isJsKeyword(name) || isGlobalIdentifier(name)\n  }\n\n  private conflictsWithUtils(name: string) {\n    // Do not allow \"Main\" as imported ns identifier since it has a special\n    // meaning in the context of lexicon definitions.\n    if (name === 'Main') return true\n\n    // When \"useRecordExport\" returns true, an export named \"Record\" will be\n    // used in addition to the hash named export. So we need to make sure both\n    // names are not conflicting with local variables.\n    if (name === 'Record') return true\n\n    // Utility functions generated for lexicon schemas are prefixed with \"$\"\n    return name.startsWith('$')\n  }\n\n  private conflictsWithLocalDefs(name: string) {\n    return Object.keys(this.doc.defs).some((hash) => {\n      const identifier = toCamelCase(hash)\n\n      // A safe identifier will be generated, no risk of conflict.\n      if (!identifier) return false\n\n      // The imported name conflicts with a local definition name\n      if (identifier === name || `_${identifier}` === name) return true\n\n      // The imported name conflicts with the type name of a local definition\n      const typeName = ucFirst(identifier)\n      if (typeName === name || `_${typeName}` === name) return true\n\n      return false\n    })\n  }\n\n  private conflictsWithLocalDeclarations(name: string) {\n    return (\n      this.file.getVariableDeclarations().some((v) => v.getName() === name) ||\n      this.file\n        .getVariableStatements()\n        .some((vs) => vs.getDeclarations().some((d) => d.getName() === name)) ||\n      this.file.getTypeAliases().some((t) => t.getName() === name) ||\n      this.file.getInterfaces().some((i) => i.getName() === name) ||\n      this.file.getClasses().some((c) => c.getName() === name) ||\n      this.file.getFunctions().some((f) => f.getName() === name) ||\n      this.file.getEnums().some((e) => e.getName() === name)\n    )\n  }\n\n  private conflictsWithImports(name: string) {\n    return this.file.getImportDeclarations().some(\n      (imp) =>\n        // import name from '...'\n        imp.getDefaultImport()?.getText() === name ||\n        // import * as name from '...'\n        imp.getNamespaceImport()?.getText() === name ||\n        imp.getNamedImports().some(\n          (named) =>\n            // import { name } from '...'\n            // import { foo as name } from '...'\n            (named.getAliasNode()?.getText() ?? named.getName()) === name,\n        ),\n    )\n  }\n}\n\n/**\n * @see {@link https://atproto.com/specs/nsid NSID syntax spec}\n */\nfunction nsidToIdentifier(nsid: string) {\n  const parts = nsid.split('.')\n\n  // By default, try to keep only to the last two segments of the NSID as\n  // contextual information. If those do not form a safe identifier (typically\n  // because they start with a digit), try with more segments until we reach the\n  // full NSID.\n  for (let i = 2; i < parts.length; i++) {\n    const identifier = toPascalCase(parts.slice(-i).join('.'))\n    if (isSafeLocalIdentifier(identifier)) return identifier\n  }\n\n  return undefined\n}\n\n/**\n * Generates predictable public identifiers for a given definition hash.\n *\n * This function creates the \"public\" names that will be exported from\n * generated files. The variable name uses the original hash, while the\n * type name is converted to PascalCase.\n *\n * @param hash - The definition hash (e.g., 'main', 'myType')\n * @returns The public identifiers for the definition\n *\n * @note The returned `typeName` is guaranteed to be a valid TypeScript\n * identifier. `varName` may not be a valid identifier (eg. if the hash contains\n * unsafe characters), and may need to be accessed using string indexing.\n */\nexport function getPublicIdentifiers(hash: string): ResolvedRef {\n  const varName = hash\n\n  // @NOTE we try to circumvent the issue of unsafe type names described in\n  // `RefResolver.resolveExternal` by ensuring that type names are always safe\n  // identifiers, even if it means changing them from the original hash.\n  let typeName = toPascalCase(hash)\n\n  if (varName === typeName || !isValidJsIdentifier(typeName)) {\n    typeName = `TypeOf${typeName}`\n  }\n\n  assert(\n    isValidJsIdentifier(typeName),\n    `Unable to generate a predictable safe identifier for \"${hash}\"`,\n  )\n\n  return { varName, typeName }\n}\n\nfunction asSafeDefinitionIdentifier(name: string) {\n  if (\n    startsWithLower(name) &&\n    isSafeLocalIdentifier(name) &&\n    isSafeLocalIdentifier(ucFirst(name))\n  ) {\n    return name\n  }\n  const camel = toCamelCase(name)\n  if (isSafeLocalIdentifier(camel) && isSafeLocalIdentifier(ucFirst(camel))) {\n    return camel\n  }\n  return undefined\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/ts-lang.ts",
    "content": "/**\n * Set of JavaScript reserved keywords and future reserved words.\n *\n * These identifiers cannot be used as variable or type names in generated code.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar}\n */\nconst JS_KEYWORDS = new Set([\n  'abstract',\n  'arguments',\n  'as',\n  'async',\n  'await',\n  'boolean',\n  'break',\n  'byte',\n  'case',\n  'catch',\n  'char',\n  'class',\n  'const',\n  'continue',\n  'debugger',\n  'default',\n  'delete',\n  'do',\n  'double',\n  'else',\n  'enum',\n  'eval',\n  'export',\n  'extends',\n  'false',\n  'final',\n  'finally',\n  'float',\n  'for',\n  'from',\n  'function',\n  'get',\n  'goto',\n  'if',\n  'implements',\n  'import',\n  'in',\n  'instanceof',\n  'int',\n  'interface',\n  'let',\n  'long',\n  'native',\n  'new',\n  'null',\n  'of',\n  'package',\n  'private',\n  'protected',\n  'public',\n  'return',\n  'set',\n  'short',\n  'static',\n  'super',\n  'switch',\n  'synchronized',\n  'this',\n  'throw',\n  'throws',\n  'transient',\n  'true',\n  'try',\n  'typeof',\n  'undefined',\n  'using',\n  'var',\n  'void',\n  'volatile',\n  'while',\n  'with',\n  'yield',\n])\n\n/**\n * Checks if a word is a JavaScript reserved keyword.\n *\n * @param word - The identifier to check\n * @returns `true` if the word is a reserved keyword\n */\nexport function isJsKeyword(word: string) {\n  return JS_KEYWORDS.has(word)\n}\n\n// Only important to list var/type names that are likely to be used in the\n// generated code files.\nconst GLOBAL_IDENTIFIERS = new Set([\n  // import { l } from \"@atproto/lex-schema\"\n  'l',\n  // JS Globals\n  'self',\n  'globalThis',\n  // ESM\n  'import',\n  // CommonJS\n  '__dirname',\n  '__filename',\n  'require',\n  'module',\n  'exports',\n  // TS Primitives\n  'any',\n  'bigint',\n  'boolean',\n  'declare',\n  'never',\n  'null',\n  'number',\n  'object',\n  'string',\n  'symbol',\n  'undefined',\n  'unknown',\n  'void',\n  // TS Utility types\n  'Record',\n  'Partial',\n  'Readonly',\n  'Pick',\n  'Omit',\n  'Exclude',\n  'Extract',\n  'InstanceType',\n  'ReturnType',\n  'Required',\n  'ThisType',\n  'Uppercase',\n  'Lowercase',\n  'Capitalize',\n  'Uncapitalize',\n])\n\n/**\n * Checks if a word is a global identifier that should be avoided.\n *\n * This includes JavaScript globals, TypeScript built-in types, and\n * identifiers commonly used in the generated code.\n *\n * @param word - The identifier to check\n * @returns `true` if the word is a global identifier\n */\nexport function isGlobalIdentifier(word: string) {\n  return GLOBAL_IDENTIFIERS.has(word)\n}\n\n/**\n * Checks if a name is safe to use as a local identifier.\n *\n * A safe local identifier is a valid JavaScript identifier that does not\n * conflict with global identifiers.\n *\n * @param name - The identifier to check\n * @returns `true` if the name is safe to use locally\n */\nexport function isSafeLocalIdentifier(name: string) {\n  return !isGlobalIdentifier(name) && isValidJsIdentifier(name)\n}\n\n/**\n * Checks if a name is a valid JavaScript identifier.\n *\n * Valid identifiers start with a letter, underscore, or dollar sign,\n * followed by any combination of letters, digits, underscores, or dollar\n * signs. Reserved keywords are not valid identifiers.\n *\n * @param name - The string to check\n * @returns `true` if the name is a valid identifier\n */\nexport function isValidJsIdentifier(name: string) {\n  return (\n    name.length > 0 &&\n    !isJsKeyword(name) &&\n    /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)\n  )\n}\n\n/**\n * Converts a name to a valid namespace export identifier.\n *\n * If the name is a valid JavaScript identifier, it is returned as-is.\n * Otherwise, it is returned as a quoted string for use in export statements\n * like `export { foo as \"unsafe-name\" }`.\n *\n * @param name - The export name\n * @returns The name as a valid export identifier\n */\nexport function asNamespaceExport(name: string) {\n  return isValidJsIdentifier(name) ? name : JSON.stringify(name)\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/src/util.ts",
    "content": "import { relative } from 'node:path'\n\nexport function memoize<T extends (arg: string) => NonNullable<unknown> | null>(\n  fn: T,\n): T {\n  const cache = new Map<string, NonNullable<unknown> | null>()\n  return ((arg: string) => {\n    const cached = cache.get(arg)\n    if (cached !== undefined) return cached\n    const result = fn(arg)\n    cache.set(arg, result)\n    return result\n  }) as T\n}\n\nexport function startsWithLower(str: string) {\n  const code = str.charCodeAt(0)\n  return code >= 97 && code <= 122 // 'a' to 'z'\n}\n\nexport function ucFirst(str: string) {\n  return str.charAt(0).toUpperCase() + str.slice(1)\n}\n\nexport function lcFirst(str: string) {\n  return str.charAt(0).toLowerCase() + str.slice(1)\n}\n\nexport function toPascalCase(str: string): string {\n  return extractWords(str).map(toLowerCase).map(ucFirst).join('')\n}\n\nexport function toCamelCase(str: string): string {\n  return lcFirst(toPascalCase(str))\n}\n\nexport function toConstantCase(str: string): string {\n  return extractWords(str).map(toUpperCase).join('_')\n}\n\nexport function toLowerCase(str: string): string {\n  return str.toLowerCase()\n}\n\nexport function toUpperCase(str: string): string {\n  return str.toUpperCase()\n}\n\nfunction extractWords(str: string): string[] {\n  const processedStr = str\n    .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // split camelCase\n    .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // split ALLCAPSWords\n    .replace(/([0-9])([A-Za-z])/g, '$1 $2') // split number followed by letter\n    .replace(/[^a-zA-Z0-9]+/g, ' ') // replace non-alphanumeric with space\n    .trim() // trim leading/trailing spaces\n\n  return processedStr\n    ? processedStr.split(/\\s+/) // split by spaces\n    : [] // Avoid returning [''] for empty strings\n}\n\nexport function asRelativePath(from: string, to: string) {\n  const relPath = relative(from, to)\n  return relPath.startsWith('./') || relPath.startsWith('../')\n    ? relPath\n    : `./${relPath}`\n}\n\nexport function startsWithDigit(str: string) {\n  const code = str.charCodeAt(0)\n  return code >= 48 && code <= 57 // '0' to '9'\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-builder/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-cbor/CHANGELOG.md",
    "content": "# @atproto/lex-cbor\n\n## 0.0.15\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-data@0.0.14\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-data@0.0.13\n\n## 0.0.13\n\n### Patch Changes\n\n- [#4667](https://github.com/bluesky-social/atproto/pull/4667) [`66b7295`](https://github.com/bluesky-social/atproto/commit/66b72950e8bcb39cac3382116bd282b3bb692f16) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use import `with` instead of deprecated import `assert` (fixing build warning)\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n\n## 0.0.11\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-data@0.0.11\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Simplify encoding logic of `number`\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `cborg` dependency, fixing encoding of strings with invalid surrogate pairs, and ignoring `undefined` object properties.\n\n- [#4572](https://github.com/bluesky-social/atproto/pull/4572) [`ecf5921`](https://github.com/bluesky-social/atproto/commit/ecf59214d59d9d2530c197c0679d26e76c6a60ef) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Perf improvements: update `cborg` dependency to version 4.5.8\n\n- Updated dependencies [[`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/lex-data@0.0.9\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove direct depdency on `multiformats`\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - **breaking**: Removed CID utilities (they can be imported from `@atproto/lex-data` instead)\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Throw an error when attempting to encode a float\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4411](https://github.com/bluesky-social/atproto/pull/4411) [`7e1d458`](https://github.com/bluesky-social/atproto/commit/7e1d45877bca0f615e7b1313cfcc66823b3de758) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update name of dist files\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lex-data@0.0.3\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4)]:\n  - @atproto/lex-data@0.0.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-data@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-cbor/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-cbor\",\n  \"version\": \"0.0.15\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon encoding utilities for AT Lexicon data in CBOR format\",\n  \"keywords\": [\n    \"atproto\",\n    \"lex\",\n    \"data\",\n    \"cbor\",\n    \"encoding\",\n    \"utilities\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-cbor\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.mjs\",\n      \"import\": \"./dist/index.mjs\",\n      \"default\": \"./dist/index.cjs\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-json\": \"workspace:^\",\n    \"cborg\": \"^4.5.8\",\n    \"vite\": \"^6.2.0\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"dev\": \"vite build --watch\",\n    \"prebuild\": \"vite build --emptyOutDir\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-cbor/src/encoding.ts",
    "content": "import {\n  DecodeOptions,\n  EncodeOptions,\n  TagDecoder,\n  Token,\n  Type,\n  decode as cborgDecode,\n  decodeFirst as cborgDecodeFirst,\n  encode as cborgEncode,\n} from 'cborg'\nimport { OptionalTypeEncoder } from 'cborg/lib/encode'\nimport { Cid, LexValue, decodeCid, ifCid } from '@atproto/lex-data'\n\n// @NOTE \"cborg\" version 4 is required to support multi-decoding via the\n// \"decodeFirst\" function. However, that version only exposes ES modules.\n// Because this package is using \"commonjs\", \"cborg\" will be bundled instead of\n// depending on it directly.\n\nconst CID_CBOR_TAG = 42\n\n/**\n * Configuration options for CBOR encoding that enforces AT Protocol data model\n * constraints.\n *\n * This configuration ensures:\n * - CIDs are encoded using CBOR tag 42 with a leading 0x00 byte prefix\n * - Map keys must be strings (no numeric or other key types allowed)\n * - `undefined` values are not permitted (undefined object properties will be stripped)\n * - Only safe integer numbers are allowed (no floats or non-integer values)\n */\nexport const encodeOptions = Object.freeze<EncodeOptions>({\n  float64: true,\n  ignoreUndefinedProperties: true,\n  typeEncoders: Object.freeze<{ [typeName: string]: OptionalTypeEncoder }>({\n    Map: (map: Map<unknown, unknown>): null => {\n      for (const key of map.keys()) {\n        if (typeof key !== 'string') {\n          throw new Error(\n            'Only string keys are allowed in CBOR \"map\" by the AT Data Model',\n          )\n        }\n      }\n\n      // @NOTE Maps will be encoded as CBOR \"map\", which will be decoded as object.\n      return null\n    },\n    Object: (obj: object): Token[] | null => {\n      const cid = ifCid(obj)\n      if (cid) {\n        const bytes = new Uint8Array(cid.bytes.byteLength + 1)\n        bytes.set(cid.bytes, 1) // prefix is 0x00, for historical reasons\n        return [new Token(Type.tag, CID_CBOR_TAG), new Token(Type.bytes, bytes)]\n      }\n\n      // Fallback to default object encoder\n      return null\n    },\n    undefined: (): null => {\n      throw new Error('`undefined` is not supported by the AT Data Model')\n    },\n    number: (num: number): null => {\n      if (Number.isSafeInteger(num)) return null\n\n      throw new Error(\n        `Non-integer numbers (${num}) are not supported by the AT Data Model`,\n      )\n    },\n  }),\n})\n\n/**\n * Configuration options for CBOR decoding that enforces AT Protocol data model\n * constraints.\n */\nexport const decodeOptions = /*#__PURE__*/ Object.freeze<DecodeOptions>({\n  allowIndefinite: false,\n  coerceUndefinedToNull: true,\n  allowNaN: false,\n  allowInfinity: false,\n  allowBigInt: true,\n  strict: true,\n  useMaps: false,\n  rejectDuplicateMapKeys: true,\n  tags: /*#__PURE__*/ Object.freeze<TagDecoder[]>(\n    /*#__PURE__*/ Object.assign([], {\n      [CID_CBOR_TAG]: (bytes: Uint8Array): Cid => {\n        if (bytes[0] !== 0) {\n          throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')\n        }\n        const cibBytes = bytes.subarray(1) // ignore leading 0x00\n        return decodeCid(cibBytes)\n      },\n    }),\n  ) as TagDecoder[],\n})\n\n/**\n * Encodes a LexValue to CBOR bytes using the AT Protocol data model (DRISL format).\n *\n * @param data - The LexValue to encode\n * @returns The CBOR-encoded bytes\n * @throws {Error} If the data contains non-string map keys, undefined values, or non-integer numbers\n *\n * @example\n * ```typescript\n * import { encode } from '@atproto/lex-cbor'\n *\n * // Encode a simple object\n * const bytes = encode({ text: 'Hello', count: 42 })\n *\n * // Encode an AT Protocol record\n * const recordBytes = encode({\n *   $type: 'app.bsky.feed.post',\n *   text: 'Hello from AT Protocol!',\n *   createdAt: new Date().toISOString(),\n * })\n * ```\n */\nexport function encode<T extends LexValue = LexValue>(data: T): Uint8Array {\n  return cborgEncode(data, encodeOptions)\n}\n\n/**\n * Decodes CBOR bytes to a LexValue using the AT Protocol data model (DRISL format).\n *\n * @typeParam T - Allows casting the decoded values to a specific LexValue subtype\n * @param bytes - The CBOR bytes to decode\n * @returns The decoded LexValue\n * @throws {Error} If the bytes are not valid CBOR or violate AT Protocol constraints\n *\n * @example\n * ```typescript\n * import { encode, decode } from '@atproto/lex-cbor'\n * import type { LexValue } from '@atproto/lex'\n *\n * // Round-trip encoding and decoding\n * const original = { text: 'Hello', count: 42 }\n * const bytes = encode(original)\n * const decoded: LexValue = decode(bytes)\n *\n * // Decode with a specific type\n * interface Post {\n *   $type: string\n *   text: string\n *   createdAt: string\n * }\n * const post = decode<Post>(recordBytes)\n * ```\n */\nexport function decode<T extends LexValue = LexValue>(bytes: Uint8Array): T {\n  return cborgDecode(bytes, decodeOptions)\n}\n\n/**\n * Generator that yields multiple decoded LexValues from a buffer containing\n * concatenated CBOR-encoded values.\n *\n * This is useful for processing streams or files containing multiple\n * CBOR-encoded records back-to-back (e.g., CAR file blocks or event streams).\n *\n * @typeParam T - Allows casting the decoded values to a specific LexValue subtype\n * @param data - The buffer containing one or more CBOR-encoded values\n * @yields Decoded LexValues one at a time\n * @throws {Error} If any value in the buffer is not valid CBOR or violates AT Protocol constraints\n *\n * @example\n * ```typescript\n * import { encode, decodeAll } from '@atproto/lex-cbor'\n *\n * // Concatenate multiple encoded values\n * const bytes1 = encode({ id: 1, text: 'First' })\n * const bytes2 = encode({ id: 2, text: 'Second' })\n * const combined = new Uint8Array([...bytes1, ...bytes2])\n *\n * // Decode all values from the combined buffer\n * for (const value of decodeAll(combined)) {\n *   console.log(value)\n * }\n * // Output:\n * // { id: 1, text: 'First' }\n * // { id: 2, text: 'Second' }\n * ```\n */\nexport function* decodeAll<T extends LexValue = LexValue>(\n  data: Uint8Array,\n): Generator<T, void, unknown> {\n  do {\n    const [result, remainingBytes] = cborgDecodeFirst(data, decodeOptions)\n    yield result\n    data = remainingBytes\n  } while (data.byteLength > 0)\n}\n"
  },
  {
    "path": "packages/lex/lex-cbor/src/index.ts",
    "content": "import { CborCid, LexValue, cidForCbor } from '@atproto/lex-data'\nimport { encode } from './encoding.js'\n\nexport type { Cid } from '@atproto/lex-data'\nexport type { CborCid, LexValue }\n\nexport { decode, decodeAll, encode } from './encoding.js'\n\nexport {\n  decode as cborDecode,\n  decodeAll as cborDecodeAll,\n  decodeOptions,\n  encode as cborEncode,\n  encodeOptions,\n} from './encoding.js'\n\n/**\n * Computes a CID (Content Identifier) for a given LexValue.\n *\n * This function first encodes the value to CBOR bytes using the AT Protocol\n * data model constraints, then computes the CID hash of those bytes. The\n * resulting CID can be used to uniquely identify and reference the content.\n *\n * @param value - The LexValue to compute a CID for\n * @returns A promise that resolves to the CID for the CBOR-encoded value\n *\n * @example\n * ```typescript\n * import { cidForLex } from '@atproto/lex-cbor'\n *\n * const record = {\n *   $type: 'app.bsky.feed.post',\n *   text: 'Hello, AT Protocol!',\n *   createdAt: new Date().toISOString(),\n * }\n *\n * const cid = await cidForLex(record)\n * console.log(cid.toString()) // e.g., 'bafyreih...'\n * ```\n */\nexport async function cidForLex(value: LexValue): Promise<CborCid> {\n  return cidForCbor(encode(value))\n}\n"
  },
  {
    "path": "packages/lex/lex-cbor/tests/codec.test.ts",
    "content": "import { assert, describe, expect, it } from 'vitest'\nimport { LexValue, isLexMap, parseCid } from '@atproto/lex-data'\nimport { decode, decodeAll, encode } from '../src/index.js'\n\ndescribe('encode', () => {\n  it('encodes data to CBOR format', () => {\n    expect(encode({ hello: 'world' })).toEqual(\n      Uint8Array.from([\n        0xa1, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c,\n        0x64,\n      ]),\n    )\n  })\n\n  it('throws when encoding floats', () => {\n    expect(() => encode({ value: 3.14 })).toThrow()\n  })\n\n  it('Supports encoding \"undefined\" values', () => {\n    expect(encode({ value: undefined })).toStrictEqual(encode({}))\n    expect(encode({ a: 1, value: undefined })).toStrictEqual(encode({ a: 1 }))\n    expect(encode({ foo: { bar: undefined } })).toStrictEqual(\n      encode({ foo: {} }),\n    )\n  })\n\n  it('throws when encoding Maps with non-string keys', () => {\n    expect(() =>\n      // @ts-expect-error\n      encode({\n        foo: new Map<any, any>([\n          [42, 'value'],\n          ['key', 'value2'],\n        ]),\n      }),\n    ).toThrow()\n  })\n})\n\ndescribe('decode', () => {\n  it('decodes CBOR data to original format', () => {\n    const bytes = Uint8Array.from([\n      0xa1, 0x65, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x65, 0x77, 0x6f, 0x72, 0x6c,\n      0x64,\n    ])\n    expect(decode(bytes)).toEqual({ hello: 'world' })\n  })\n})\n\ndescribe('identity', () => {\n  for (const vector of [\n    null,\n    parseCid('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),\n    [\n      parseCid('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),\n      parseCid('bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q'),\n      parseCid('bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke'),\n      parseCid('bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy'),\n    ],\n    new Uint8Array(Buffer.from('hello world')),\n    true,\n    false,\n    0,\n    42,\n    -1,\n    '',\n    'hello world',\n    [],\n    [1, 2, 3],\n    {},\n    { a: 1, b: 'two', c: true },\n    {\n      nested: {\n        array: { value: [1, 2, 3] },\n        object: { key: 'value' },\n        cid: parseCid(\n          'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        ),\n        bytes: new Uint8Array(Buffer.from('byte array')),\n      },\n    },\n  ] as LexValue[]) {\n    it(JSON.stringify(vector), () => {\n      const cbor = encode(vector)\n      const decoded = decode(cbor)\n      expect(decoded).toEqual(vector)\n      expect(encode(decoded)).toEqual(cbor)\n    })\n  }\n})\n\ndescribe('ipld decode multi', () => {\n  it('decodes concatenated dag-cbor messages', async () => {\n    const one = {\n      a: 123,\n      b: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n    const two = {\n      c: new Uint8Array([1, 2, 3]),\n      d: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n    const encoded = Buffer.concat([encode(one), encode(two)])\n    const decoded = Array.from(decodeAll(encoded))\n    expect(decoded.length).toBe(2)\n    expect(decoded[0]).toEqual(one)\n    expect(decoded[1]).toEqual(two)\n  })\n\n  it('parses safe ints as number', async () => {\n    const one = {\n      test: Number.MAX_SAFE_INTEGER,\n    }\n    const encoded = encode(one)\n    const { length, 0: first } = Array.from(decodeAll(encoded))\n    expect(length).toBe(1)\n    assert(isLexMap(first))\n    expect(Number.isInteger(first.test)).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-cbor/tests/dag-cbor.test.ts",
    "content": "import { decodeFirst } from 'cborg'\nimport { encodedLength } from 'cborg/length'\nimport { assert, describe, test } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport * as dagcbor from '../src/index.js'\n\nconst same = assert.deepStrictEqual\nconst { encode, decode } = dagcbor\n\nconst CID = { parse: parseCid }\nconst bytes = {\n  isBinary: (data: unknown): data is Uint8Array | ArrayBuffer => {\n    return data instanceof Uint8Array\n  },\n  toHex: (data: Uint8Array | ArrayBuffer): string => {\n    return Buffer.from(data).toString('hex')\n  },\n  fromHex: (hex: string): Uint8Array => {\n    return Buffer.from(hex, 'hex')\n  },\n}\n\nconst garbage = (length: number): Uint8Array => {\n  const arr = new Uint8Array(length)\n  for (let i = 0; i < length; i++) {\n    arr[i] = Math.floor(Math.random() * 256)\n  }\n  return arr\n}\n\n// The code bellow was copied from\n// https://github.com/ipld/js-dag-cbor/blob/fdcaa70e2b5830ee3ca410d63e7c9c57fafd3b61/test/test-basics.spec.js\n// which is licensed under the same license at this package (MIT and Apache-2.0 dual license)\n\ndescribe('dag-cbor', () => {\n  const obj = {\n    someKey: 'someValue',\n    link: CID.parse('QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL'),\n    links: [\n      CID.parse('QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL'),\n      CID.parse('QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL'),\n    ],\n    nested: {\n      hello: 'world',\n      link: CID.parse('QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL'),\n    },\n    bytes: new TextEncoder().encode('asdf'),\n  }\n  const serializedObj = encode(obj)\n\n  test('.serialize and .deserialize', () => {\n    same(bytes.isBinary(serializedObj), true)\n\n    // Check for the tag 42\n    // d8 = tag, 2a = 42\n    same(bytes.toHex(serializedObj).match(/d82a/g)?.length, 4)\n\n    const deserializedObj = decode(serializedObj)\n    same(deserializedObj, obj)\n  })\n\n  test('.serialize and .deserialize with ArrayBuffer', () => {\n    same(bytes.isBinary(serializedObj), true)\n\n    // Check for the tag 42\n    // d8 = tag, 2a = 42\n    same(bytes.toHex(serializedObj).match(/d82a/g)?.length, 4)\n\n    const deserializedObj = decode(\n      new Uint8Array(\n        serializedObj.buffer.slice(\n          serializedObj.byteOffset,\n          serializedObj.byteOffset + serializedObj.byteLength,\n        ),\n      ),\n    )\n    same(deserializedObj, obj)\n  })\n\n  test('.serialize and .deserialize large objects', () => {\n    // larger than the default borc heap size, should auto-grow the heap\n    const dataSize = 128 * 1024\n    const largeObj = { someKey: [].slice.call(new Uint8Array(dataSize)) }\n\n    const serialized = encode(largeObj)\n    same(bytes.isBinary(serialized), true)\n\n    const deserialized = decode(serialized)\n    same(largeObj, deserialized)\n  })\n\n  test('.serialize and .deserialize object with slash as property', () => {\n    const slashObject = { '/': true }\n    const serialized = encode(slashObject)\n    const deserialized = decode(serialized)\n    same(deserialized, slashObject)\n  })\n\n  test('CIDs have clean for deep comparison', () => {\n    const deserializedObj = decode(serializedObj)\n    // backing buffer must be pristine as some comparison libraries go that deep\n\n    // @ts-expect-error\n    const actual = deserializedObj.link.bytes.join(',')\n    const expected = obj.link.bytes.join(',')\n    same(actual, expected)\n  })\n\n  test('error on circular references', () => {\n    type Circular = { a?: Circular | Circular[] }\n    const circularObj: Circular = {}\n    circularObj.a = circularObj\n    assert.throws(\n      () => encode(circularObj),\n      /object contains circular references/,\n    )\n    const circularArr = [circularObj]\n    circularObj.a = circularArr\n    assert.throws(\n      () => encode(circularArr),\n      /object contains circular references/,\n    )\n  })\n\n  test('error on encoding undefined', () => {\n    // @ts-expect-error\n    assert.throws(() => encode(undefined), /\\Wundefined\\W.*not supported/)\n  })\n\n  test('Ignores undefined object properties', () => {\n    const objWithUndefined = { a: 'a', b: undefined }\n    const serialized = encode(objWithUndefined)\n    const deserialized = decode(serialized)\n    same(deserialized, { a: 'a' })\n  })\n\n  test('error on encoding IEEE 754 specials', () => {\n    for (const special of [NaN, Infinity, -Infinity]) {\n      assert.throws(\n        () => encode(special),\n        new RegExp(`\\\\W${String(special)}\\\\W.*not supported`),\n      )\n      const objWithSpecial = { a: 'a', b: special }\n      assert.throws(\n        () => encode(objWithSpecial),\n        new RegExp(`\\\\W${String(special)}\\\\W.*not supported`),\n      )\n      const arrWithSpecial = [\n        1,\n        -1,\n        Number.MAX_SAFE_INTEGER,\n        special,\n        Number.MIN_SAFE_INTEGER,\n      ]\n      assert.throws(\n        () => encode(arrWithSpecial),\n        new RegExp(`\\\\W${String(special)}\\\\W.*not supported`),\n      )\n    }\n  })\n\n  test('error on decoding IEEE 754 specials', () => {\n    // encoded forms of each of the previous encode() tests\n    const cases = [\n      ['NaN', 'f97e00'],\n      ['NaN', 'f97ff8'],\n      ['NaN', 'fa7ff80000'],\n      ['NaN', 'fb7ff8000000000000'],\n      ['NaN', 'a2616161616162fb7ff8000000000000'],\n      [\n        'NaN',\n        '8701fb3ff199999999999a20fbbff199999999999a1b001ffffffffffffffb7ff80000000000003b001ffffffffffffe',\n      ],\n      ['Infinity', 'f97c00'],\n      ['Infinity', 'fb7ff0000000000000'],\n      ['Infinity', 'a2616161616162fb7ff0000000000000'],\n      [\n        'Infinity',\n        '8701fb3ff199999999999a20fbbff199999999999a1b001ffffffffffffffb7ff00000000000003b001ffffffffffffe',\n      ],\n      ['-Infinity', 'f9fc00'],\n      ['-Infinity', 'fbfff0000000000000'],\n      ['-Infinity', 'a2616161616162fbfff0000000000000'],\n      [\n        '-Infinity',\n        '8701fb3ff199999999999a20fbbff199999999999a1b001ffffffffffffffbfff00000000000003b001ffffffffffffe',\n      ],\n    ]\n    for (const [typ, hex] of cases) {\n      const byts = bytes.fromHex(hex)\n      assert.throws(\n        () => decode(byts),\n        new RegExp(`\\\\W${typ.replace(/^-/, '')}\\\\W.*not supported`),\n      )\n    }\n  })\n\n  test('fuzz serialize and deserialize with garbage', function () {\n    for (let ii = 0; ii < 1000; ii++) {\n      const original = garbage(100)\n      const encoded = encode(original)\n      const decoded = decode(encoded)\n      same(decoded, original)\n    }\n  })\n\n  test('CIDv1', () => {\n    const link = CID.parse('zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS')\n    const encoded = encode({ link })\n    const decoded = decode(encoded)\n    same(decoded, { link })\n  })\n\n  test('encode and decode consistency with Uint8Array and Buffer fields', () => {\n    const buffer = Buffer.from('some data')\n    const bytes = Uint8Array.from(buffer)\n\n    const s1 = encode({ data: buffer })\n    const s2 = encode({ data: bytes })\n\n    same(s1, s2)\n\n    const verify = (s: { data: Uint8Array }) => {\n      same(typeof s, 'object')\n      same(Object.keys(s), ['data'])\n      assert(s.data instanceof Uint8Array)\n      same(s.data.buffer, bytes.buffer)\n    }\n    verify(decode(s1))\n    verify(decode(s2))\n  })\n\n  test('reject extraneous, but valid CBOR data after initial top-level object', () => {\n    assert.throws(() => {\n      // two top-level CBOR objects, the original and a single uint=0, valid if using\n      // CBOR in streaming mode, not valid here\n      const big = new Uint8Array(serializedObj.length + 1)\n      big.set(serializedObj, 0)\n      decode(big)\n    }, /too many terminals/)\n  })\n\n  test('reject bad CID lead-in', () => {\n    // this is the same data as the CIDv1 produces but has the lead-in to the\n    // CID replaced with 0x01 .......................  ↓↓ here\n    const encoded = bytes.fromHex(\n      'a1646c696e6bd82a582501017012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5',\n    )\n    assert.throws(\n      () => decode(encoded),\n      /Invalid CID for CBOR tag 42; expected leading 0x00/,\n    )\n  })\n\n  test('sloppy decode: coerce undefined', () => {\n    // See https://github.com/ipld/js-dag-cbor/issues/44 for context on this\n    let encoded = bytes.fromHex('f7')\n    let decoded = decode(encoded)\n    same(null, decoded)\n\n    encoded = bytes.fromHex('a26362617af763666f6f63626172')\n    decoded = decode(encoded)\n    same({ foo: 'bar', baz: null }, decoded)\n  })\n\n  test('reject duplicate map keys', () => {\n    const encoded = bytes.fromHex('a3636261720363666f6f0163666f6f02')\n    assert.throws(\n      () => decode(encoded),\n      /CBOR decode error: found repeat map key \"foo\"/,\n    )\n  })\n\n  test('determine encoded length of obj', () => {\n    const { encodeOptions } = dagcbor\n\n    const length = encodedLength(obj, encodeOptions)\n    same(length, serializedObj.length)\n  })\n\n  test('.deserialize the first of concatenated serialized objects', () => {\n    const { decodeOptions } = dagcbor\n\n    const concatSerializedObjs = new Uint8Array(serializedObj.length * 2)\n    concatSerializedObjs.set(serializedObj)\n    concatSerializedObjs.set(serializedObj, serializedObj.length)\n\n    const [deserializedObj, remainder] = decodeFirst(\n      concatSerializedObjs,\n      decodeOptions,\n    )\n    same(deserializedObj, obj)\n    same(remainder, serializedObj)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-cbor/tests/data-model-fixtures.json",
    "content": "[\n  {\n    \"json\": {\n      \"string\": \"abc\",\n      \"unicode\": \"a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧\",\n      \"integer\": 123,\n      \"bool\": true,\n      \"null\": null,\n      \"array\": [\"abc\", \"def\", \"ghi\"],\n      \"object\": {\n        \"string\": \"abc\",\n        \"number\": 123,\n        \"bool\": true,\n        \"arr\": [\"abc\", \"def\", \"ghi\"]\n      }\n    },\n    \"cbor_base64\": \"p2Rib29s9WRudWxs9mVhcnJheYNjYWJjY2RlZmNnaGlmb2JqZWN0pGNhcnKDY2FiY2NkZWZjZ2hpZGJvb2z1Zm51bWJlchh7ZnN0cmluZ2NhYmNmc3RyaW5nY2FiY2dpbnRlZ2VyGHtndW5pY29kZXgvYX7DtsOxwqnivZjimI7wk4uT8J+YgPCfkajigI3wn5Gp4oCN8J+Rp+KAjfCfkac\",\n    \"cid\": \"bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq\"\n  },\n  {\n    \"json\": {\n      \"a\": {\n        \"$link\": \"bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a\"\n      },\n      \"b\": {\n        \"$bytes\": \"nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0\"\n      },\n      \"c\": {\n        \"$type\": \"blob\",\n        \"ref\": {\n          \"$link\": \"bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity\"\n        },\n        \"mimeType\": \"image/jpeg\",\n        \"size\": 10000\n      }\n    },\n    \"cbor_base64\": \"o2Fh2CpYJQABcRIgZQYqWloA/BbXPGlEI3zLwVscSnI0SJM2iR0JF0GiOdBhYlggnFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI1hY6RjcmVm2CpYJQABVRIgQljP/3j2E2l2l1Y/kmyR5dNXTS6iWuftktbr/COjiJ5kc2l6ZRknEGUkdHlwZWRibG9iaG1pbWVUeXBlamltYWdlL2pwZWc\",\n    \"cid\": \"bafyreihldkhcwijkde7gx4rpkkuw7pl6lbyu5gieunyc7ihactn5bkd2nm\"\n  },\n  {\n    \"json\": {\n      \"a\": {\n        \"b\": [\n          {\n            \"d\": [\n              {\n                \"$link\": \"bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a\"\n              },\n              {\n                \"$link\": \"bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a\"\n              }\n            ],\n            \"e\": [\n              { \"$bytes\": \"nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0\" },\n              { \"$bytes\": \"iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas\" }\n            ]\n          }\n        ]\n      }\n    },\n    \"cbor_base64\": \"oWFhoWFigaJhZILYKlglAAFxEiBlBipaWgD8Ftc8aUQjfMvBWxxKcjRIkzaJHQkXQaI50NgqWCUAAXESIGUGKlpaAPwW1zxpRCN8y8FbHEpyNEiTNokdCRdBojnQYWWCWCCcURGO8suLD2qbjkmuof1BPPILYu7Vdvid7r6wGsLMjVggiE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas\",\n    \"cid\": \"bafyreid3imdulnhgeytpf6uk7zahjvrsqlofkmm5b5ub2maw4kqus6jp4i\"\n  }\n]\n"
  },
  {
    "path": "packages/lex/lex-cbor/tests/fixtures.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { ui8Equals } from '@atproto/lex-data'\nimport { jsonToLex, lexToJson } from '@atproto/lex-json'\nimport { cidForLex, decodeAll, encode } from '../src/index.js'\nimport fixtures from './data-model-fixtures.json' with { type: 'json' }\n\ndescribe('fixtures', () => {\n  for (const fixture of fixtures) {\n    it(fixture.cid, async () => {\n      const lex = jsonToLex(fixture.json, { strict: true })\n      const cid = await cidForLex(lex)\n      expect(cid.toString()).toBe(fixture.cid)\n      const encoded = encode(lex)\n      expect(encoded).toBeInstanceOf(Uint8Array)\n      expect(\n        ui8Equals(encoded, Buffer.from(fixture.cbor_base64, 'base64')),\n      ).toBe(true)\n      const [decoded, ...rest] = decodeAll(encoded)\n      expect(rest.length).toBe(0)\n      expect(lexToJson(decoded)).toEqual(fixture.json)\n    })\n  }\n})\n"
  },
  {
    "path": "packages/lex/lex-cbor/tests/vectors.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { lexEquals } from '@atproto/lex-data'\nimport { jsonToLex, lexToJson } from '@atproto/lex-json'\nimport { cidForLex, decode, encode } from '../src/index.js'\nimport { vectors } from './vectors.js'\n\ndescribe('lex', () => {\n  for (const vector of vectors) {\n    it(`passes test vector: ${vector.name}`, async () => {\n      const lex = jsonToLex(vector.json)\n      const json = lexToJson(lex)\n      const cbor = encode(lex)\n      const ipldAgain = decode(cbor)\n      const jsonAgain = lexToJson(ipldAgain)\n      const cid = await cidForLex(lex)\n      expect(json).toEqual(vector.json)\n      expect(jsonAgain).toEqual(vector.json)\n      expect(lexEquals(lex, vector.lex)).toBeTruthy()\n      expect(lexEquals(ipldAgain, vector.lex)).toBeTruthy()\n      expect(Buffer.compare(cbor, vector.cbor)).toBe(0)\n      expect(cid.toString()).toEqual(vector.cid)\n    })\n  }\n})\n"
  },
  {
    "path": "packages/lex/lex-cbor/tests/vectors.ts",
    "content": "import { parseCid } from '@atproto/lex-data'\n\nexport const vectors = [\n  {\n    name: 'basic',\n    json: {\n      string: 'abc',\n      unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',\n      integer: 123,\n      bool: true,\n      null: null,\n      array: ['abc', 'def', 'ghi'],\n      object: {\n        string: 'abc',\n        number: 123,\n        bool: true,\n        arr: ['abc', 'def', 'ghi'],\n      },\n    },\n    lex: {\n      string: 'abc',\n      unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',\n      integer: 123,\n      bool: true,\n      null: null,\n      array: ['abc', 'def', 'ghi'],\n      object: {\n        string: 'abc',\n        number: 123,\n        bool: true,\n        arr: ['abc', 'def', 'ghi'],\n      },\n    },\n    cbor: new Uint8Array([\n      167, 100, 98, 111, 111, 108, 245, 100, 110, 117, 108, 108, 246, 101, 97,\n      114, 114, 97, 121, 131, 99, 97, 98, 99, 99, 100, 101, 102, 99, 103, 104,\n      105, 102, 111, 98, 106, 101, 99, 116, 164, 99, 97, 114, 114, 131, 99, 97,\n      98, 99, 99, 100, 101, 102, 99, 103, 104, 105, 100, 98, 111, 111, 108, 245,\n      102, 110, 117, 109, 98, 101, 114, 24, 123, 102, 115, 116, 114, 105, 110,\n      103, 99, 97, 98, 99, 102, 115, 116, 114, 105, 110, 103, 99, 97, 98, 99,\n      103, 105, 110, 116, 101, 103, 101, 114, 24, 123, 103, 117, 110, 105, 99,\n      111, 100, 101, 120, 47, 97, 126, 195, 182, 195, 177, 194, 169, 226, 189,\n      152, 226, 152, 142, 240, 147, 139, 147, 240, 159, 152, 128, 240, 159, 145,\n      168, 226, 128, 141, 240, 159, 145, 169, 226, 128, 141, 240, 159, 145, 167,\n      226, 128, 141, 240, 159, 145, 167,\n    ]),\n    cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',\n  },\n  {\n    name: 'ipld',\n    json: {\n      a: {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      b: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      },\n      c: {\n        $type: 'blob',\n        ref: {\n          $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n        },\n        mimeType: 'image/jpeg',\n        size: 10000,\n      },\n    },\n    lex: {\n      a: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n      b: new Uint8Array([\n        156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,\n        65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,\n        204, 141,\n      ]),\n      c: {\n        $type: 'blob',\n        ref: parseCid(\n          'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n        ),\n        mimeType: 'image/jpeg',\n        size: 10000,\n      },\n    },\n    cbor: new Uint8Array([\n      163, 97, 97, 216, 42, 88, 37, 0, 1, 113, 18, 32, 101, 6, 42, 90, 90, 0,\n      252, 22, 215, 60, 105, 68, 35, 124, 203, 193, 91, 28, 74, 114, 52, 72,\n      147, 54, 137, 29, 9, 23, 65, 162, 57, 208, 97, 98, 88, 32, 156, 81, 17,\n      142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65, 60, 242, 11,\n      98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204, 141, 97, 99,\n      164, 99, 114, 101, 102, 216, 42, 88, 37, 0, 1, 85, 18, 32, 66, 88, 207,\n      255, 120, 246, 19, 105, 118, 151, 86, 63, 146, 108, 145, 229, 211, 87, 77,\n      46, 162, 90, 231, 237, 146, 214, 235, 252, 35, 163, 136, 158, 100, 115,\n      105, 122, 101, 25, 39, 16, 101, 36, 116, 121, 112, 101, 100, 98, 108, 111,\n      98, 104, 109, 105, 109, 101, 84, 121, 112, 101, 106, 105, 109, 97, 103,\n      101, 47, 106, 112, 101, 103,\n    ]),\n    cid: 'bafyreihldkhcwijkde7gx4rpkkuw7pl6lbyu5gieunyc7ihactn5bkd2nm',\n  },\n  {\n    name: 'ipldArray',\n    json: [\n      {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      {\n        $link: 'bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke',\n      },\n      {\n        $link: 'bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy',\n      },\n    ],\n    lex: [\n      parseCid('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),\n      parseCid('bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q'),\n      parseCid('bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke'),\n      parseCid('bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy'),\n    ],\n    cbor: new Uint8Array([\n      132, 216, 42, 88, 37, 0, 1, 113, 18, 32, 101, 6, 42, 90, 90, 0, 252, 22,\n      215, 60, 105, 68, 35, 124, 203, 193, 91, 28, 74, 114, 52, 72, 147, 54,\n      137, 29, 9, 23, 65, 162, 57, 208, 216, 42, 88, 37, 0, 1, 113, 18, 32, 206,\n      188, 253, 200, 24, 248, 158, 85, 31, 33, 95, 133, 103, 145, 125, 120, 196,\n      209, 14, 220, 33, 139, 148, 27, 165, 214, 150, 172, 255, 213, 142, 244,\n      216, 42, 88, 37, 0, 1, 113, 18, 32, 8, 206, 26, 37, 182, 8, 114, 225, 240,\n      224, 175, 74, 102, 104, 101, 188, 145, 99, 35, 55, 249, 209, 206, 95, 5,\n      220, 164, 199, 101, 33, 191, 81, 216, 42, 88, 37, 0, 1, 113, 18, 32, 163,\n      229, 185, 49, 71, 179, 93, 47, 159, 186, 73, 155, 92, 82, 150, 179, 147,\n      12, 56, 8, 177, 42, 60, 74, 164, 100, 227, 215, 175, 174, 41, 126,\n    ]),\n    cid: 'bafyreiaj3udmqlqrcbjxjayzuxwp64gt64olcbjfrkldzoqponpru6gq4m',\n  },\n  {\n    name: 'ipldNested',\n    json: {\n      a: {\n        b: [\n          {\n            d: [\n              {\n                $link:\n                  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              },\n              {\n                $link:\n                  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              },\n            ],\n            e: [\n              {\n                $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n              },\n              {\n                $bytes: 'iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas',\n              },\n            ],\n          },\n        ],\n      },\n    },\n    lex: {\n      a: {\n        b: [\n          {\n            d: [\n              parseCid(\n                'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              ),\n              parseCid(\n                'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              ),\n            ],\n            e: [\n              new Uint8Array([\n                156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174,\n                161, 253, 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238,\n                190, 176, 26, 194, 204, 141,\n              ]),\n              new Uint8Array([\n                136, 79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237,\n                244, 244, 178, 194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14,\n                221, 125, 181, 171,\n              ]),\n            ],\n          },\n        ],\n      },\n    },\n    cbor: new Uint8Array([\n      161, 97, 97, 161, 97, 98, 129, 162, 97, 100, 130, 216, 42, 88, 37, 0, 1,\n      113, 18, 32, 101, 6, 42, 90, 90, 0, 252, 22, 215, 60, 105, 68, 35, 124,\n      203, 193, 91, 28, 74, 114, 52, 72, 147, 54, 137, 29, 9, 23, 65, 162, 57,\n      208, 216, 42, 88, 37, 0, 1, 113, 18, 32, 101, 6, 42, 90, 90, 0, 252, 22,\n      215, 60, 105, 68, 35, 124, 203, 193, 91, 28, 74, 114, 52, 72, 147, 54,\n      137, 29, 9, 23, 65, 162, 57, 208, 97, 101, 130, 88, 32, 156, 81, 17, 142,\n      242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65, 60, 242, 11, 98,\n      238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204, 141, 88, 32, 136,\n      79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237, 244, 244, 178,\n      194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14, 221, 125, 181, 171,\n    ]),\n    cid: 'bafyreid3imdulnhgeytpf6uk7zahjvrsqlofkmm5b5ub2maw4kqus6jp4i',\n  },\n  {\n    name: 'poorlyFormatted',\n    json: {\n      a: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      b: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      c: {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n        another: 'bad value',\n      },\n      d: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        another: 'bad value',\n      },\n      e: {\n        '/': 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      f: {\n        '/': {\n          bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        },\n      },\n    },\n    lex: {\n      a: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      b: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      c: {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n        another: 'bad value',\n      },\n      d: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        another: 'bad value',\n      },\n      e: {\n        '/': 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      f: {\n        '/': {\n          bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n        },\n      },\n    },\n    cbor: new Uint8Array([\n      166, 97, 97, 120, 59, 98, 97, 102, 121, 114, 101, 105, 100, 102, 97, 121,\n      118, 102, 117, 119, 113, 97, 55, 113, 108, 110, 111, 112, 100, 106, 105,\n      113, 114, 120, 122, 115, 54, 98, 108, 109, 111, 101, 117, 52, 114, 117,\n      106, 99, 106, 116, 110, 99, 105, 53, 98, 101, 108, 117, 100, 105, 114,\n      122, 50, 97, 97, 98, 120, 43, 110, 70, 69, 82, 106, 118, 76, 76, 105, 119,\n      57, 113, 109, 52, 53, 74, 114, 113, 72, 57, 81, 84, 122, 121, 67, 50, 76,\n      117, 49, 88, 98, 52, 110, 101, 54, 43, 115, 66, 114, 67, 122, 73, 48, 97,\n      99, 162, 101, 36, 108, 105, 110, 107, 120, 59, 98, 97, 102, 121, 114, 101,\n      105, 103, 111, 120, 116, 54, 52, 113, 103, 104, 121, 116, 122, 107, 114,\n      54, 105, 107, 55, 113, 118, 116, 122, 99, 55, 108, 121, 121, 116, 105,\n      113, 53, 120, 98, 98, 114, 111, 107, 98, 120, 106, 111, 119, 115, 50, 119,\n      112, 55, 118, 109, 111, 54, 113, 103, 97, 110, 111, 116, 104, 101, 114,\n      105, 98, 97, 100, 32, 118, 97, 108, 117, 101, 97, 100, 162, 102, 36, 98,\n      121, 116, 101, 115, 120, 43, 110, 70, 69, 82, 106, 118, 76, 76, 105, 119,\n      57, 113, 109, 52, 53, 74, 114, 113, 72, 57, 81, 84, 122, 121, 67, 50, 76,\n      117, 49, 88, 98, 52, 110, 101, 54, 43, 115, 66, 114, 67, 122, 73, 48, 103,\n      97, 110, 111, 116, 104, 101, 114, 105, 98, 97, 100, 32, 118, 97, 108, 117,\n      101, 97, 101, 161, 97, 47, 120, 59, 98, 97, 102, 121, 114, 101, 105, 103,\n      111, 120, 116, 54, 52, 113, 103, 104, 121, 116, 122, 107, 114, 54, 105,\n      107, 55, 113, 118, 116, 122, 99, 55, 108, 121, 121, 116, 105, 113, 53,\n      120, 98, 98, 114, 111, 107, 98, 120, 106, 111, 119, 115, 50, 119, 112, 55,\n      118, 109, 111, 54, 113, 97, 102, 161, 97, 47, 161, 101, 98, 121, 116, 101,\n      115, 120, 43, 110, 70, 69, 82, 106, 118, 76, 76, 105, 119, 57, 113, 109,\n      52, 53, 74, 114, 113, 72, 57, 81, 84, 122, 121, 67, 50, 76, 117, 49, 88,\n      98, 52, 110, 101, 54, 43, 115, 66, 114, 67, 122, 73, 48,\n    ]),\n    cid: 'bafyreico7wgbbfe6dpfsuednrtrlh6t2yjl6xq5rf32gl3pgwhwxk77cn4',\n  },\n]\n"
  },
  {
    "path": "packages/lex/lex-cbor/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-cbor/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-cbor/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-cbor/vite.config.mts",
    "content": "import path from 'node:path'\nimport { defineConfig } from 'vite'\nimport pkg from './package.json' with { type: 'json' }\n\n// We rely on a bundler to handle bundling of the \"cborg\" package. This is\n// required because \"cborg\" is an ESM-only package that uses \"exports\" fields\n// in its package.json, which causes issues when trying to import it directly\n// in a CJS context without bundling.\n\n// Whenever this package is converted to ESM only, we can likely remove this\n// bundling step.\n\nexport default defineConfig({\n  build: {\n    minify: false,\n    lib: {\n      entry: path.resolve(__dirname, 'src/index.ts'),\n      formats: ['es', 'cjs'],\n      fileName: (format) => {\n        switch (format) {\n          case 'es':\n            return 'index.mjs'\n          case 'cjs':\n            return 'index.cjs'\n          default:\n            return `index.${format}.js`\n        }\n      },\n    },\n    rollupOptions: {\n      // Only devpendencies should be bundled\n      external: Object.keys(pkg.dependencies),\n    },\n  },\n})\n"
  },
  {
    "path": "packages/lex/lex-cbor/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-client/.gitignore",
    "content": "src/lexicons\ntests/lexicons\n"
  },
  {
    "path": "packages/lex/lex-client/CHANGELOG.md",
    "content": "# @atproto/lex-client\n\n## 0.0.17\n\n### Patch Changes\n\n- [#4761](https://github.com/bluesky-social/atproto/pull/4761) [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `XrpcResponse.payload` to be typed as the schema's parse \"Output\"\n\n- [#4761](https://github.com/bluesky-social/atproto/pull/4761) [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow defining request and response processing defaults on the `Client` instance\n\n- [#4773](https://github.com/bluesky-social/atproto/pull/4773) [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `XrpcParamsOptions` as `XrpcRequestParamsOptions`\n\n- [#4761](https://github.com/bluesky-social/atproto/pull/4761) [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ability to process invalid Lexicon Data responses (instead of rejecting them as invalid)\n\n- [#4761](https://github.com/bluesky-social/atproto/pull/4761) [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow using a dynamic non-strict validation mode\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-data@0.0.14\n  - @atproto/lex-json@0.0.14\n\n## 0.0.16\n\n### Patch Changes\n\n- [#4714](https://github.com/bluesky-social/atproto/pull/4714) [`5a2f884`](https://github.com/bluesky-social/atproto/commit/5a2f8847efd91252971fa243d21bd52ada7aa8f4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Create a specific error class for wrapping unexpected error returned by the `fetchHandler`. This allows to better distinguish unexpected internal server errors (due to implementation encountering an unexpected case) from potentially transient errors that occur when performing network HTTP requests.\n\n- Updated dependencies [[`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-schema@0.0.15\n\n## 0.0.15\n\n### Patch Changes\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update error management to be more aligned with the way errors work in `@atproto/xrpc` and `@atproto/xrpc-server`\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add dedicated `XrpcInvalidResponseError` error for responses that don't match the schema\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `matchesSchema` to `matchesSchemaErrors` to make it more explicit what is being matched when called on an XRPC \"result\" (that could either represent a success or failure)\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-json@0.0.13\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4672](https://github.com/bluesky-social/atproto/pull/4672) [`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow calling `xrpc` and `xrpcSafe` functions with a service URL\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-data@0.0.12\n  - @atproto/lex-json@0.0.12\n\n## 0.0.12\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing exports\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc to `Client` class properties and methods\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-json@0.0.11\n  - @atproto/lex-data@0.0.11\n\n## 0.0.11\n\n### Patch Changes\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ability to set default `headers` when creating XRPC agent\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing of error responses\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow omitting `rkey` for record definition usin `any`as record key\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-json@0.0.10\n  - @atproto/lex-schema@0.0.11\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `LexRpcFailure` to `XrpcFailure`, `LexRpcOptions` to `XrpcOptions`, `LexRpcResponse` to `XrpcResponse`, `LexRpcResponseBody` to `XrpcResponseBody`\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on `URLSearchParams.size` when building XRPC request query string\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `Payload` to `XrpcPayload`\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/lex-json@0.0.9\n  - @atproto/lex-data@0.0.9\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/lex-json@0.0.8\n  - @atproto/lex-schema@0.0.9\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-json@0.0.7\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - **breaking:** Use a record type (`com.example.record.Main`) instead of a record schema type (`typeof com.example.record.$defs.main`) as the generic parameter for `ListRecord`.\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n  - @atproto/lex-schema@0.0.7\n  - @atproto/lex-json@0.0.6\n\n## 0.0.6\n\n### Patch Changes\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework `XrpcError` to extend `LexError` from `@atproto/data`\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `asXrpcFailure` utility\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `buildAgent` can now be given an `Agent` as input (and will act as identity in that case)\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n  - @atproto/lex-data@0.0.5\n  - @atproto/lex-json@0.0.5\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Complete rework of the error mechanics\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `uploadBlob` and `getBlob` methods\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Validate records when `validateRequest` is set when calling `create` and `put` methods.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export `FetchHandler` as a distinct type\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - No longer validate records when `validate` option is set in `create()` method. Use `validateRequest` option for that purpose.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `XrpcFailure` error name field to `error` for consistency with XRPC error payloads\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow specifying request content-type through the `encoding` option\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve handling of binary payloads\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `createRecordsSafe`, `deleteRecordsSafe`, `getRecordsSafe` and `putRecordsSafe` methods\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n  - @atproto/lex-schema@0.0.5\n  - @atproto/lex-json@0.0.4\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/lex-json@0.0.3\n  - @atproto/lex-schema@0.0.4\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework object validation logic to work without `options` argument\n\n- [#4405](https://github.com/bluesky-social/atproto/pull/4405) [`2d13d05`](https://github.com/bluesky-social/atproto/commit/2d13d05ab06576703742b1b638d2f243b6b2915f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `.call()` argument \"`params`\" to be `undefined` when all params are optional\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use string formats from `@atproto/syntax`\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename schema methods `validate`, `check` and `maybe` to `safeParse`, `matches` and `ifMatches` respectively.\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba)]:\n  - @atproto/lex-schema@0.0.3\n  - @atproto/lex-data@0.0.2\n  - @atproto/lex-json@0.0.2\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [[`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4)]:\n  - @atproto/lex-schema@0.0.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-data@0.0.1\n  - @atproto/lex-json@0.0.1\n  - @atproto/lex-schema@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-client/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-client\",\n  \"version\": \"0.0.17\",\n  \"license\": \"MIT\",\n  \"description\": \"HTTP client for interacting with Lexicon based APIs\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"xrpc\",\n    \"client\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-client\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-json\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"@atproto/lex-builder\": \"workspace:^\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"prebuild\": \"node ./scripts/lex-build.mjs\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-client/scripts/lex-build.mjs",
    "content": "/* eslint-env node  */\n\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { build } from '@atproto/lex-builder'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nPromise.all([\n  // For src\n  build({\n    lexicons: join(__dirname, '..', '..', '..', '..', 'lexicons'),\n    out: join(__dirname, '..', 'src', 'lexicons'),\n    clear: true,\n    include: [\n      'com.atproto.repo.createRecord',\n      'com.atproto.repo.deleteRecord',\n      'com.atproto.repo.getRecord',\n      'com.atproto.repo.putRecord',\n      'com.atproto.repo.listRecords',\n      'com.atproto.repo.uploadBlob',\n      'com.atproto.sync.getBlob',\n    ],\n    lib: '@atproto/lex-schema',\n    pretty: true,\n    pureAnnotations: true,\n    indexFile: true,\n  }),\n\n  // For tests\n  build({\n    lexicons: join(__dirname, '..', '..', '..', '..', 'lexicons'),\n    out: join(__dirname, '..', 'tests', 'lexicons'),\n    clear: true,\n    include: [\n      'app.bsky.*',\n      'com.atproto.repo.createRecord',\n      'com.atproto.repo.getRecord',\n      'com.atproto.repo.uploadBlob',\n      'com.atproto.sync.getBlob',\n    ],\n    lib: '@atproto/lex-schema',\n    pretty: true,\n    indexFile: true,\n  }),\n]).catch((err) => {\n  console.error('Error building lexicon schemas:', err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/lex/lex-client/src/agent.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest'\nimport {\n  type Agent,\n  type AgentConfig,\n  type FetchHandler,\n  buildAgent,\n  isAgent,\n} from './agent.js'\n\n// ============================================================================\n// isAgent\n// ============================================================================\n\ndescribe(isAgent, () => {\n  it('returns true for a valid agent with did', () => {\n    const agent: Agent = {\n      did: 'did:plc:example',\n      fetchHandler: async () => new Response(),\n    }\n    expect(isAgent(agent)).toBe(true)\n  })\n\n  it('returns true for an agent without did', () => {\n    const agent = { fetchHandler: async () => new Response() }\n    expect(isAgent(agent)).toBe(true)\n  })\n\n  it('returns true when did is undefined', () => {\n    const agent = { did: undefined, fetchHandler: async () => new Response() }\n    expect(isAgent(agent)).toBe(true)\n  })\n\n  it('returns false for null', () => {\n    expect(isAgent(null)).toBe(false)\n  })\n\n  it('returns false for non-objects', () => {\n    expect(isAgent('string')).toBe(false)\n    expect(isAgent(42)).toBe(false)\n    expect(isAgent(undefined)).toBe(false)\n  })\n\n  it('returns false when fetchHandler is not a function', () => {\n    expect(isAgent({ fetchHandler: 'not-a-function' })).toBe(false)\n  })\n\n  it('returns false when did is not a string', () => {\n    expect(isAgent({ did: 42, fetchHandler: async () => new Response() })).toBe(\n      false,\n    )\n  })\n})\n\n// ============================================================================\n// buildAgent\n// ============================================================================\n\ndescribe(buildAgent, () => {\n  describe('from Agent', () => {\n    it('returns the same agent instance', () => {\n      const agent: Agent = {\n        did: 'did:plc:example',\n        fetchHandler: async () => new Response(),\n      }\n      expect(buildAgent(agent)).toBe(agent)\n    })\n  })\n\n  describe('from FetchHandler', () => {\n    it('wraps a function as an agent', () => {\n      const handler: FetchHandler = async () => new Response()\n      const agent = buildAgent(handler)\n\n      expect(agent.did).toBeUndefined()\n      expect(typeof agent.fetchHandler).toBe('function')\n    })\n  })\n\n  describe('from string URL', () => {\n    it('creates an agent that prepends the service URL', async () => {\n      const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>\n        Response.json({ ok: true }),\n      )\n      const agent = buildAgent({\n        service: 'https://example.com',\n        fetch: fetchFn,\n      })\n\n      await agent.fetchHandler('/xrpc/io.example.test', { method: 'GET' })\n\n      expect(fetchFn).toHaveBeenCalledOnce()\n      const [url, init] = fetchFn.mock.calls[0]\n      expect(url).toEqual(\n        new URL('/xrpc/io.example.test', 'https://example.com'),\n      )\n      expect(init?.method).toBe('GET')\n    })\n\n    it('has undefined did', () => {\n      const agent = buildAgent('https://example.com')\n      expect(agent.did).toBeUndefined()\n    })\n  })\n\n  describe('from URL instance', () => {\n    it('creates an agent with the URL as service', async () => {\n      const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>\n        Response.json({ ok: true }),\n      )\n      const agent = buildAgent({\n        service: new URL('https://example.com'),\n        fetch: fetchFn,\n      })\n\n      await agent.fetchHandler('/xrpc/io.example.test', { method: 'GET' })\n\n      expect(fetchFn).toHaveBeenCalledOnce()\n      const [url] = fetchFn.mock.calls[0]\n      expect(url).toEqual(\n        new URL('/xrpc/io.example.test', 'https://example.com'),\n      )\n    })\n  })\n\n  describe('from AgentConfig', () => {\n    it('exposes did from config', () => {\n      const agent = buildAgent({\n        did: 'did:plc:test123',\n        service: 'https://example.com',\n      })\n      expect(agent.did).toBe('did:plc:test123')\n    })\n\n    it('reflects did changes on the config object', () => {\n      const config: AgentConfig = {\n        did: 'did:plc:original',\n        service: 'https://example.com',\n      }\n      const agent = buildAgent(config)\n      expect(agent.did).toBe('did:plc:original')\n\n      config.did = 'did:plc:updated'\n      expect(agent.did).toBe('did:plc:updated')\n    })\n\n    it('throws TypeError when fetch is not available', () => {\n      const originalFetch = globalThis.fetch\n      try {\n        // @ts-expect-error removing fetch to simulate missing environment\n        globalThis.fetch = undefined\n        expect(() => buildAgent({ service: 'https://example.com' })).toThrow(\n          TypeError,\n        )\n      } finally {\n        globalThis.fetch = originalFetch\n      }\n    })\n  })\n\n  describe('headers', () => {\n    it('sends config headers when no request headers', async () => {\n      const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>\n        Response.json({}),\n      )\n      const agent = buildAgent({\n        service: 'https://example.com',\n        headers: { Authorization: 'Bearer token123' },\n        fetch: fetchFn,\n      })\n\n      await agent.fetchHandler('/xrpc/test', { method: 'GET' })\n\n      const [, init] = fetchFn.mock.calls[0]\n      expect(init?.headers).toEqual({ Authorization: 'Bearer token123' })\n    })\n\n    it('sends request headers when no config headers', async () => {\n      const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>\n        Response.json({}),\n      )\n      const agent = buildAgent({\n        service: 'https://example.com',\n        fetch: fetchFn,\n      })\n\n      await agent.fetchHandler('/xrpc/test', {\n        method: 'GET',\n        headers: { 'X-Custom': 'value' },\n      })\n\n      const [, init] = fetchFn.mock.calls[0]\n      expect(init?.headers).toEqual({ 'X-Custom': 'value' })\n    })\n\n    it('merges config and request headers, with request taking priority', async () => {\n      const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>\n        Response.json({}),\n      )\n      const agent = buildAgent({\n        service: 'https://example.com',\n        headers: { Authorization: 'Bearer default', 'X-Default': 'yes' },\n        fetch: fetchFn,\n      })\n\n      await agent.fetchHandler('/xrpc/test', {\n        method: 'GET',\n        headers: { Authorization: 'Bearer override' },\n      })\n\n      const [, init] = fetchFn.mock.calls[0]\n      const headers = new Headers(init?.headers)\n      expect(headers.get('Authorization')).toBe('Bearer override')\n      expect(headers.get('X-Default')).toBe('yes')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-client/src/agent.ts",
    "content": "import { DidString } from '@atproto/lex-schema'\n\n/**\n * A function that performs HTTP requests towards a service endpoint.\n *\n * The handler is responsible for adding the origin (protocol, hostname, and\n * port) to the provided path, typically based on authentication or service\n * configuration. The handler can also be responsible for adding any necessary\n * headers, such as authorization tokens.\n *\n * @param path - The URL path (pathname + query parameters) without the origin\n * @param init - Standard fetch RequestInit options\n * @returns A Promise resolving to the HTTP Response\n */\nexport type FetchHandler = (\n  /**\n   * The URL (pathname + query parameters) to make the request to, without the\n   * origin. The origin (protocol, hostname, and port) must be added by this\n   * {@link FetchHandler}, typically based on authentication or other factors.\n   */\n  path: `/${string}`,\n  init: RequestInit,\n) => Promise<Response>\n\n/**\n * Core interface for making XRPC requests towards a specific service.\n *\n * An {@link Agent} encapsulates an identity and request handling for AT\n * Protocol operations. Agents will typically handle authentication, service URL\n * resolution, and other request configuration, allowing client code to make\n * requests without needing to manage these details directly. The key component\n * of an Agent is the {@link FetchHandler}, which is responsible for\n * constructing the full request URL and adding any necessary headers or\n * authentication tokens. The Agent's `did` property represents the\n * authenticated user's DID, if available, and can be used for operations that\n * require knowledge of the user's identity (such as creating AT Protocol\n * records).\n *\n * @see {@link buildAgent} for creating (simple) Agent instances.\n *\n * @example\n * ```typescript\n * const agent: Agent = {\n *   did: 'did:plc:example123',\n *   fetchHandler: async (path, init) => {\n *     const url = new URL(path, 'https://bsky.social')\n *     return fetch(url, init)\n *   }\n * }\n * ```\n */\nexport interface Agent {\n  /** The DID of the authenticated user, or `undefined` if unauthenticated. */\n  readonly did?: DidString\n  /** The fetch handler used to make HTTP requests. */\n  fetchHandler: FetchHandler\n}\n\nexport function isAgent(value: unknown): value is Agent {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    'fetchHandler' in value &&\n    typeof value.fetchHandler === 'function' &&\n    (!('did' in value) ||\n      value.did === undefined ||\n      typeof value.did === 'string')\n  )\n}\n\nexport type AgentConfig = {\n  /**\n   * The identifier (DID) of the user represented by this agent.\n   */\n  did?: DidString\n\n  /**\n   * The service URL to make requests to. This can be a string, URL, or a\n   * function that returns a string or URL. This is useful for dynamic URLs,\n   * such as a service URL that changes based on authentication.\n   */\n  service: string | URL\n\n  /**\n   * Optional headers to include with every request made by this agent, unless\n   * overridden by the request-specific headers provided to the fetch handler.\n   */\n  headers?: HeadersInit\n\n  /**\n   * Bring your own fetch implementation. Typically useful for testing, logging,\n   * mocking, or adding retries, session management, signatures, proof of\n   * possession (DPoP), SSRF protection, etc. Defaults to the global `fetch`\n   * function.\n   */\n  fetch?: typeof globalThis.fetch\n}\n\n/**\n * Options for creating an Agent.\n *\n * Can be a full {@link AgentConfig} object, or a simple service URL string/{@link URL}.\n */\nexport type AgentOptions = AgentConfig | FetchHandler | string | URL\n\n/**\n * Creates an {@link Agent} from various input types.\n *\n * This factory function accepts an existing Agent (returned as-is), a service\n * URL, or a full configuration object. It handles the common case of creating\n * an unauthenticated agent from just a service URL.\n *\n * @param options - Agent instance, configuration object, or service URL\n * @returns A configured Agent ready for making requests\n * @throws {TypeError} If fetch() is not available in the environment\n *\n * @example // From URL string\n * ```typescript\n * const agent = buildAgent('https://public.api.bsky.app')\n * ```\n *\n * @example // From configuration\n * ```typescript\n * const agent = buildAgent({\n *   did: 'did:plc:example',\n *   service: 'https://bsky.social',\n *   headers: { 'Authorization': 'Bearer ...' }\n * })\n * ```\n */\nexport function buildAgent<O extends Agent | AgentOptions>(\n  options: O,\n): O extends Agent ? O : Agent\nexport function buildAgent(options: Agent | AgentOptions): Agent {\n  const config: Agent | AgentConfig =\n    typeof options === 'function'\n      ? { did: undefined, fetchHandler: options }\n      : typeof options === 'string' || options instanceof URL\n        ? { did: undefined, service: options }\n        : options\n\n  if (isAgent(config)) return config\n\n  const { service, fetch = globalThis.fetch } = config\n\n  if (typeof fetch !== 'function') {\n    throw new TypeError('fetch() is not available in this environment')\n  }\n\n  return {\n    get did() {\n      return config.did\n    },\n\n    async fetchHandler(path, init) {\n      const headers =\n        config.headers != null && init.headers != null\n          ? mergeHeaders(config.headers, init.headers)\n          : config.headers || init.headers\n\n      return fetch(\n        new URL(path, service),\n        headers !== init.headers ? { ...init, headers } : init,\n      )\n    },\n  }\n}\n\nfunction mergeHeaders(\n  defaultHeaders: HeadersInit,\n  requestHeaders: HeadersInit,\n): Headers {\n  // We don't want to alter the original Headers objects, so we create a new one\n  const result = new Headers(defaultHeaders)\n\n  const overrides =\n    requestHeaders instanceof Headers\n      ? requestHeaders\n      : new Headers(requestHeaders)\n\n  for (const [key, value] of overrides.entries()) {\n    result.set(key, value)\n  }\n\n  return result\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/client.ts",
    "content": "import { LexMap, LexValue, TypedLexMap } from '@atproto/lex-data'\nimport {\n  AtIdentifierString,\n  CidString,\n  DidString,\n  Infer,\n  InferMethodInputBody,\n  InferMethodOutputBody,\n  InferRecordKey,\n  LexiconRecordKey,\n  Main,\n  NsidString,\n  Params,\n  Procedure,\n  Query,\n  RecordSchema,\n  Restricted,\n  getMain,\n} from '@atproto/lex-schema'\nimport { Agent, AgentOptions, buildAgent } from './agent.js'\nimport { XrpcFailure } from './errors.js'\nimport { com } from './lexicons/index.js'\nimport {\n  XrpcResponse,\n  XrpcResponseBody,\n  XrpcResponseOptions,\n} from './response.js'\nimport { BinaryBodyInit, Service } from './types.js'\nimport {\n  XrpcRequestHeadersOptions,\n  applyDefaults,\n  buildXrpcRequestHeaders,\n} from './util.js'\nimport {\n  XrpcOptions,\n  XrpcRequestParams,\n  XrpcRequestProcessingOptions,\n  xrpc,\n  xrpcSafe,\n} from './xrpc.js'\n\nexport type {\n  AtIdentifierString,\n  CidString,\n  DidString,\n  Infer,\n  InferMethodInputBody,\n  InferMethodOutputBody,\n  InferRecordKey,\n  LexMap,\n  LexValue,\n  LexiconRecordKey,\n  Main,\n  NsidString,\n  Params,\n  Procedure,\n  Query,\n  RecordSchema,\n  Restricted,\n  TypedLexMap,\n}\n\n/**\n * Configuration options for creating a {@link Client}.\n *\n * @example\n * ```typescript\n * const options: ClientOptions = {\n *   labelers: ['did:plc:labeler1'],\n *   service: 'did:web:api.bsky.app#bsky_appview',\n *   headers: { 'X-Custom-Header': 'value' }\n * }\n * ```\n */\nexport type ClientOptions = XrpcRequestHeadersOptions &\n  Pick<XrpcRequestProcessingOptions, 'validateRequest'> &\n  XrpcResponseOptions\n\nexport type ActionOptions = {\n  /** AbortSignal to cancel the request. */\n  signal?: AbortSignal\n}\n\n/**\n * A composable action that can be invoked via {@link Client.call}.\n *\n * Actions provide a way to define custom operations that integrate with the\n * Client's call interface, enabling type-safe, reusable business logic.\n *\n * @typeParam I - The input type for the action\n * @typeParam O - The output type for the action\n *\n * @example\n * ```typescript\n * const myAction: Action<{ userId: string }, { profile: Profile }> = async (client, input, options) => {\n *   const response = await client.xrpc(someMethod, { params: { actor: input.userId }, ...options })\n *   return { profile: response.body }\n * }\n * ```\n */\nexport type Action<I = any, O = any> = (\n  client: Client,\n  input: I,\n  options: ActionOptions,\n) => O | Promise<O>\n\n/**\n * Extracts the input type from an {@link Action}.\n * @typeParam A - The Action type to extract from\n */\nexport type InferActionInput<A extends Action> =\n  A extends Action<infer I, any> ? I : never\n\n/**\n * Extracts the output type from an {@link Action}.\n * @typeParam A - The Action type to extract from\n */\nexport type InferActionOutput<A extends Action> =\n  A extends Action<any, infer O> ? O : never\n\n/**\n * Options for creating a record in an AT Protocol repository.\n *\n * @see {@link Client.createRecord}\n */\nexport type CreateRecordOptions = Omit<\n  XrpcOptions<typeof com.atproto.repo.createRecord.main>,\n  'body'\n> & {\n  /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */\n  repo?: AtIdentifierString\n  /** Compare-and-swap on the repo commit. If specified, must match current commit. */\n  swapCommit?: string\n  /** Whether to validate the record against its lexicon schema. */\n  validate?: boolean\n}\n\n/**\n * Options for deleting a record from an AT Protocol repository.\n *\n * @see {@link Client.deleteRecord}\n */\nexport type DeleteRecordOptions = Omit<\n  XrpcOptions<typeof com.atproto.repo.deleteRecord.main>,\n  'params'\n> & {\n  /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */\n  repo?: AtIdentifierString\n  /** Compare-and-swap on the repo commit. If specified, must match current commit. */\n  swapCommit?: string\n  /** Compare-and-swap on the record CID. If specified, must match current record. */\n  swapRecord?: string\n}\n\n/**\n * Options for retrieving a record from an AT Protocol repository.\n *\n * @see {@link Client.getRecord}\n */\nexport type GetRecordOptions = Omit<\n  XrpcOptions<typeof com.atproto.repo.getRecord.main>,\n  'params'\n> & {\n  /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */\n  repo?: AtIdentifierString\n}\n\n/**\n * Options for creating or updating a record in an AT Protocol repository.\n *\n * @see {@link Client.putRecord}\n */\nexport type PutRecordOptions = Omit<\n  XrpcOptions<typeof com.atproto.repo.putRecord.main>,\n  'body'\n> & {\n  /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */\n  repo?: AtIdentifierString\n  /** Compare-and-swap on the repo commit. If specified, must match current commit. */\n  swapCommit?: string\n  /** Compare-and-swap on the record CID. If specified, must match current record. */\n  swapRecord?: string\n  /** Whether to validate the record against its lexicon schema. */\n  validate?: boolean\n}\n\n/**\n * Options for listing records in an AT Protocol repository collection.\n *\n * @see {@link Client.listRecords}\n */\nexport type ListRecordsOptions = Omit<\n  XrpcOptions<typeof com.atproto.repo.listRecords.main>,\n  'params'\n> & {\n  /** Repository identifier (DID or handle). Defaults to authenticated user's DID. */\n  repo?: AtIdentifierString\n  /** Maximum number of records to return. */\n  limit?: number\n  /** Pagination cursor from a previous response. */\n  cursor?: string\n  /** If true, returns records in reverse chronological order. */\n  reverse?: boolean\n}\n\nexport type UploadBlobOptions = Omit<\n  XrpcOptions<typeof com.atproto.repo.uploadBlob.main>,\n  'body'\n>\n\nexport type GetBlobOptions = Omit<\n  XrpcOptions<typeof com.atproto.sync.getBlob.main>,\n  'params'\n>\n\nexport type RecordKeyOptions<\n  T extends RecordSchema,\n  AlsoOptionalWhenRecordKeyIs extends LexiconRecordKey = never,\n> = T['key'] extends `literal:${string}` | AlsoOptionalWhenRecordKeyIs\n  ? { rkey?: InferRecordKey<T> }\n  : { rkey: InferRecordKey<T> }\n\n/**\n * Type-safe options for {@link Client.create}, combining record options with key requirements.\n * @typeParam T - The record schema type\n */\nexport type CreateOptions<T extends RecordSchema> = CreateRecordOptions &\n  RecordKeyOptions<T, 'tid' | 'any'>\n\n/**\n * Output type for record creation operations.\n * Contains the URI and CID of the newly created record.\n */\nexport type CreateOutput = InferMethodOutputBody<\n  typeof com.atproto.repo.createRecord.main,\n  Uint8Array\n>\n\n/**\n * Type-safe options for {@link Client.delete}, combining delete options with key requirements.\n * @typeParam T - The record schema type\n */\nexport type DeleteOptions<T extends RecordSchema> = DeleteRecordOptions &\n  RecordKeyOptions<T>\n\n/**\n * Output type for record deletion operations.\n */\nexport type DeleteOutput = InferMethodOutputBody<\n  typeof com.atproto.repo.deleteRecord.main,\n  Uint8Array\n>\n\n/**\n * Type-safe options for {@link Client.get}, combining get options with key requirements.\n * @typeParam T - The record schema type\n */\nexport type GetOptions<T extends RecordSchema> = GetRecordOptions &\n  RecordKeyOptions<T>\n\n/**\n * Output type for record retrieval operations.\n * Contains the record value validated against the schema type.\n * @typeParam T - The record schema type\n */\nexport type GetOutput<T extends RecordSchema> = Omit<\n  InferMethodOutputBody<typeof com.atproto.repo.getRecord.main, Uint8Array>,\n  'value'\n> & { value: Infer<T> }\n\n/**\n * Type-safe options for {@link Client.put}, combining put options with key requirements.\n * @typeParam T - The record schema type\n */\nexport type PutOptions<T extends RecordSchema> = PutRecordOptions &\n  RecordKeyOptions<T>\n\n/**\n * Output type for record put (create/update) operations.\n * Contains the URI and CID of the record.\n */\nexport type PutOutput = InferMethodOutputBody<\n  typeof com.atproto.repo.putRecord.main,\n  Uint8Array\n>\n\n/**\n * Options for {@link Client.list} operations.\n */\nexport type ListOptions = ListRecordsOptions\n\n/**\n * Output type for record listing operations.\n * Contains validated records and any invalid records that failed schema validation.\n * @typeParam T - The record schema type\n */\nexport type ListOutput<T extends RecordSchema> = InferMethodOutputBody<\n  typeof com.atproto.repo.listRecords.main,\n  Uint8Array\n> & {\n  /** Records that successfully validated against the schema. */\n  records: ListRecord<Infer<T>>[]\n  // @NOTE Because the schema uses \"type\": \"unknown\" instead of an open union,\n  // we have to use LexMap instead of Unknown$TypedObject here, which is\n  // unfortunate.\n  /** Records that failed schema validation. */\n  invalid: LexMap[]\n}\n\n/**\n * A record from a list operation with its value typed to the schema.\n * @typeParam Value - The validated record value type\n */\nexport type ListRecord<Value extends LexMap> =\n  com.atproto.repo.listRecords.Record & {\n    value: Value\n  }\n\n/**\n * The Client class is the primary interface for interacting with AT Protocol\n * services. It provides type-safe methods for XRPC calls, record operations,\n * and blob handling.\n *\n * @example // Basic usage\n * ```typescript\n * import { Client } from '@atproto/lex'\n *\n * const client = new Client(oauthSession)\n *\n * const response = await client.xrpc(app.bsky.feed.getTimeline.main, {\n *   params: { limit: 50 }\n * })\n * ```\n */\nexport class Client implements Agent {\n  static appLabelers: readonly DidString[] = []\n\n  /**\n   * Configures the Client (or its sub classes) globally.\n   */\n  static configure(opts: { appLabelers?: Iterable<DidString> }) {\n    if (opts.appLabelers) this.appLabelers = [...opts.appLabelers]\n  }\n\n  /** The underlying agent used for making requests. */\n  public readonly agent: Agent\n\n  /** Custom headers included in all requests. */\n  public readonly headers: Headers\n\n  /** Optional service identifier for routing requests. */\n  public readonly service?: Service\n\n  /** Set of labeler DIDs specific to this client instance. */\n  public readonly labelers: Set<DidString>\n\n  public readonly xrpcDefaults: {\n    readonly validateRequest: boolean\n    readonly validateResponse: boolean\n    readonly strictResponseProcessing: boolean\n  }\n\n  constructor(agent: Agent | AgentOptions, options: ClientOptions = {}) {\n    this.agent = buildAgent(agent)\n    this.service = options.service\n    this.labelers = new Set(options.labelers)\n    this.headers = new Headers(options.headers)\n    this.xrpcDefaults = Object.freeze({\n      validateRequest: options.validateRequest ?? false,\n      validateResponse: options.validateResponse ?? true,\n      strictResponseProcessing: options.strictResponseProcessing ?? true,\n    })\n  }\n\n  /**\n   * The DID of the authenticated user, or `undefined` if not authenticated.\n   */\n  get did(): DidString | undefined {\n    return this.agent.did\n  }\n\n  /**\n   * The DID of the authenticated user.\n   * @throws {Error} if not authenticated\n   */\n  get assertDid(): DidString {\n    this.assertAuthenticated()\n    return this.did\n  }\n\n  /**\n   * Asserts that the client is authenticated.\n   * Use as a type guard when you need to ensure authentication.\n   *\n   * @throws {Error} if not authenticated\n   *\n   * @example\n   * ```typescript\n   * client.assertAuthenticated()\n   * // TypeScript now knows client.did is defined\n   * console.log(client.did)\n   * ```\n   */\n  public assertAuthenticated(): asserts this is { did: DidString } {\n    if (!this.did) throw new Error('Client is not authenticated')\n  }\n\n  /**\n   * Replaces all labelers with the given set.\n   * @param labelers - Iterable of labeler DIDs\n   */\n  public setLabelers(labelers: Iterable<DidString> = []) {\n    this.clearLabelers()\n    this.addLabelers(labelers)\n  }\n\n  /**\n   * Adds labelers to the current set.\n   * @param labelers - Iterable of labeler DIDs to add\n   */\n  public addLabelers(labelers: Iterable<DidString>) {\n    for (const labeler of labelers) this.labelers.add(labeler)\n  }\n\n  /**\n   * Removes all labelers from this client instance.\n   */\n  public clearLabelers() {\n    this.labelers.clear()\n  }\n\n  /**\n   * {@link Agent}'s {@link Agent.fetchHandler} implementation, which adds\n   * labelers and service proxying headers. This method allow a {@link Client}\n   * instance to be used directly as an {@link Agent} for another\n   * {@link Client}, enabling composition of headers (labelers, proxying, etc.).\n   *\n   * @param path - The request path\n   * @param init - Request initialization options\n   */\n  public fetchHandler(\n    path: `/${string}`,\n    init: RequestInit,\n  ): Promise<Response> {\n    const headers = buildXrpcRequestHeaders({\n      headers: init.headers,\n      service: this.service,\n      labelers: [\n        ...(this.constructor as typeof Client).appLabelers.map(\n          (l) => `${l};redact` as const,\n        ),\n        ...this.labelers,\n      ],\n    })\n\n    // Incoming headers take precedence\n    for (const [key, value] of this.headers) {\n      if (!headers.has(key)) headers.set(key, value)\n    }\n\n    // @NOTE The agent here could be another Client instance.\n    return this.agent.fetchHandler(path, { ...init, headers })\n  }\n\n  /**\n   * Makes an XRPC request. Throws on failure.\n   *\n   * @param ns - The lexicon method definition (e.g., `app.bsky.feed.getTimeline`)\n   * @param options - Request options including params and body\n   * @returns The successful XRPC response\n   * @throws {XrpcFailure} when the request fails or returns an error\n   *\n   * @example Query with parameters\n   * ```typescript\n   * const response = await client.xrpc(app.bsky.feed.getTimeline, {\n   *   params: { limit: 50, cursor: 'abc123' }\n   * })\n   * console.log(response.body.feed)\n   * ```\n   *\n   * @example Procedure with body\n   * ```typescript\n   * const response = await client.xrpc(com.atproto.repo.createRecord, {\n   *   body: {\n   *     repo: client.assertDid,\n   *     collection: 'app.bsky.feed.post',\n   *     record: { text: 'Hello!', createdAt: new Date().toISOString() }\n   *   }\n   * })\n   * ```\n   *\n   * @see {@link xrpcSafe} for a non-throwing variant\n   */\n  async xrpc<const M extends Query | Procedure>(\n    ns: NonNullable<unknown> extends XrpcOptions<M>\n      ? Main<M>\n      : Restricted<'This XRPC method requires an \"options\" argument'>,\n  ): Promise<XrpcResponse<M>>\n  async xrpc<const M extends Query | Procedure>(\n    ns: Main<M>,\n    options: XrpcOptions<M>,\n  ): Promise<XrpcResponse<M>>\n  async xrpc<const M extends Query | Procedure>(\n    ns: Main<M>,\n    options: XrpcOptions<M> = {} as XrpcOptions<M>,\n  ): Promise<XrpcResponse<M>> {\n    return xrpc(this, ns, applyDefaults(options, this.xrpcDefaults))\n  }\n\n  /**\n   * Makes an XRPC request without throwing on failure.\n   * Returns either a successful response or a failure object.\n   *\n   * @param ns - The lexicon method definition\n   * @param options - Request options\n   * @returns Either an XrpcResponse on success or XrpcFailure on failure\n   *\n   * @example\n   * ```typescript\n   * const result = await client.xrpcSafe(app.bsky.actor.getProfile.main, {\n   *   params: { actor: 'alice.bsky.social' }\n   * })\n   *\n   * if (result.success) {\n   *   console.log(result.body.displayName)\n   * } else {\n   *   console.error('Failed:', result.error)\n   * }\n   * ```\n   *\n   * @see {@link xrpc} for a throwing variant\n   */\n  async xrpcSafe<const M extends Query | Procedure>(\n    ns: NonNullable<unknown> extends XrpcOptions<M>\n      ? Main<M>\n      : Restricted<'This XRPC method requires an \"options\" argument'>,\n  ): Promise<XrpcResponse<M> | XrpcFailure<M>>\n  async xrpcSafe<const M extends Query | Procedure>(\n    ns: Main<M>,\n    options: XrpcOptions<M>,\n  ): Promise<XrpcResponse<M> | XrpcFailure<M>>\n  async xrpcSafe<const M extends Query | Procedure>(\n    ns: Main<M>,\n    options: XrpcOptions<M> = {} as XrpcOptions<M>,\n  ): Promise<XrpcResponse<M> | XrpcFailure<M>> {\n    return xrpcSafe(this, ns, applyDefaults(options, this.xrpcDefaults))\n  }\n\n  /**\n   * Creates a new record in an AT Protocol repository.\n   *\n   * @param record - The record to create, must include an {@link NsidString} `$type`\n   * @param rkey - Optional record key; if omitted, server generates a TID\n   * @param options - Create options including repo, swapCommit, validate\n   * @returns The XRPC response containing the created record's URI and CID\n   *\n   * @example\n   * ```typescript\n   * const response = await client.createRecord(\n   *   { $type: 'app.bsky.feed.post', text: 'Hello!', createdAt: new Date().toISOString() },\n   *   undefined, // Let server generate rkey\n   *   { validate: true }\n   * )\n   * console.log(response.body.uri)\n   * ```\n   *\n   * @see {@link create} for a higher-level typed alternative\n   */\n  public async createRecord(\n    record: TypedLexMap<NsidString>,\n    rkey?: string,\n    options?: CreateRecordOptions,\n  ) {\n    return this.xrpc(com.atproto.repo.createRecord.main, {\n      ...options,\n      body: {\n        repo: options?.repo ?? this.assertDid,\n        collection: record.$type,\n        record,\n        rkey,\n        validate: options?.validate,\n        swapCommit: options?.swapCommit,\n      },\n    })\n  }\n\n  /**\n   * Deletes a record from an AT Protocol repository.\n   *\n   * @param collection - The collection NSID\n   * @param rkey - The record key\n   * @param options - Delete options including repo, swapCommit, swapRecord\n   *\n   * @see {@link delete} for a higher-level typed alternative\n   */\n  async deleteRecord(\n    collection: NsidString,\n    rkey: string,\n    options?: DeleteRecordOptions,\n  ) {\n    return this.xrpc(com.atproto.repo.deleteRecord.main, {\n      ...options,\n      body: {\n        repo: options?.repo ?? this.assertDid,\n        collection,\n        rkey,\n        swapCommit: options?.swapCommit,\n        swapRecord: options?.swapRecord,\n      },\n    })\n  }\n\n  /**\n   * Retrieves a record from an AT Protocol repository.\n   *\n   * @param collection - The collection NSID\n   * @param rkey - The record key\n   * @param options - Get options including repo\n   *\n   * @see {@link get} for a higher-level typed alternative\n   */\n  public async getRecord(\n    collection: NsidString,\n    rkey: string,\n    options?: GetRecordOptions,\n  ) {\n    return this.xrpc(com.atproto.repo.getRecord.main, {\n      ...options,\n      params: {\n        repo: options?.repo ?? this.assertDid,\n        collection,\n        rkey,\n      },\n    })\n  }\n\n  /**\n   * Creates or updates a record in a repository.\n   *\n   * @param record - The record to put, must include an {@link NsidString} `$type`\n   * @param rkey - The record key\n   * @param options - Put options including repo, swapCommit, swapRecord, validate\n   *\n   * @see {@link put} for a higher-level typed alternative\n   */\n  async putRecord(\n    record: TypedLexMap<NsidString>,\n    rkey: string,\n    options?: PutRecordOptions,\n  ) {\n    return this.xrpc(com.atproto.repo.putRecord.main, {\n      ...options,\n      body: {\n        repo: options?.repo ?? this.assertDid,\n        collection: record.$type,\n        rkey,\n        record,\n        validate: options?.validate,\n        swapCommit: options?.swapCommit,\n        swapRecord: options?.swapRecord,\n      },\n    })\n  }\n\n  /**\n   * Lists records in a collection.\n   *\n   * @param nsid - The collection NSID\n   * @param options - List options including repo, limit, cursor, reverse\n   *\n   * @see {@link list} for a higher-level typed alternative\n   */\n  async listRecords(nsid: NsidString, options?: ListRecordsOptions) {\n    return this.xrpc(com.atproto.repo.listRecords.main, {\n      ...options,\n      params: {\n        repo: options?.repo ?? this.assertDid,\n        collection: nsid,\n        cursor: options?.cursor,\n        limit: options?.limit,\n        reverse: options?.reverse,\n      },\n    })\n  }\n\n  /**\n   * Uploads a blob to an AT Protocol repository.\n   *\n   * @param body - The blob data (Uint8Array, ReadableStream, Blob, etc.)\n   * @param options - Upload options including encoding hint\n   * @returns Response containing the blob reference\n   *\n   * @example\n   * ```typescript\n   * const imageData = await fetch('image.png').then(r => r.arrayBuffer())\n   * const response = await client.uploadBlob(new Uint8Array(imageData), {\n   *   encoding: 'image/png'\n   * })\n   * console.log(response.body.blob) // Use this ref in records\n   * ```\n   */\n  async uploadBlob(body: BinaryBodyInit, options?: UploadBlobOptions) {\n    return this.xrpc(com.atproto.repo.uploadBlob.main, { ...options, body })\n  }\n\n  /**\n   * Retrieves a blob by DID and CID.\n   *\n   * @param did - The DID of the repository containing the blob\n   * @param cid - The CID of the blob\n   * @param options - Call options\n   */\n  async getBlob(did: DidString, cid: CidString, options?: GetBlobOptions) {\n    return this.xrpc(com.atproto.sync.getBlob.main, {\n      ...options,\n      params: { did, cid },\n    })\n  }\n\n  /**\n   * Universal call method for queries, procedures, and custom actions.\n   * Automatically determines the call type based on the lexicon definition.\n   *\n   * @param ns - The lexicon method or action definition\n   * @param arg - The input argument (params for queries, body for procedures, input for actions)\n   * @param options - Call options\n   * @returns The method response body or action output\n   * @see {@link xrpc} if you need access to the full response object\n   *\n   * @example Query\n   * ```typescript\n   * const profile = await client.call(app.bsky.actor.getProfile.main, {\n   *   actor: 'alice.bsky.social'\n   * })\n   * ```\n   *\n   * @example Procedure\n   * ```typescript\n   * const result = await client.call(com.atproto.repo.createRecord.main, {\n   *   repo: did,\n   *   collection: 'app.bsky.feed.post',\n   *   record: { text: 'Hello!' }\n   * })\n   * ```\n   */\n  public async call<const T extends Query>(\n    ns: NonNullable<unknown> extends XrpcRequestParams<T>\n      ? Main<T>\n      : Restricted<'This query type requires a \"params\" argument'>,\n  ): Promise<XrpcResponseBody<T>>\n  public async call<const T extends Procedure>(\n    ns: undefined extends InferMethodInputBody<T, Uint8Array>\n      ? Main<T>\n      : Restricted<'This procedure type requires an \"input\" argument'>,\n  ): Promise<XrpcResponseBody<T>>\n  public async call<const T extends Action>(\n    ns: void extends InferActionInput<T>\n      ? Main<T>\n      : Restricted<'This action type requires an \"input\" argument'>,\n  ): Promise<InferActionOutput<T>>\n  public async call<const T extends Action | Procedure | Query>(\n    ns: Main<T>,\n    arg: T extends Action\n      ? InferActionInput<T>\n      : T extends Procedure\n        ? InferMethodInputBody<T, Uint8Array>\n        : T extends Query\n          ? XrpcRequestParams<T>\n          : never,\n    options?: T extends Action\n      ? ActionOptions\n      : T extends Procedure\n        ? Omit<XrpcOptions<T>, 'body'>\n        : T extends Query\n          ? Omit<XrpcOptions<T>, 'params'>\n          : never,\n  ): Promise<\n    T extends Action\n      ? InferActionOutput<T>\n      : T extends Procedure\n        ? XrpcResponseBody<T>\n        : T extends Query\n          ? XrpcResponseBody<T>\n          : never\n  >\n  public async call(\n    ns: Main<Action> | Main<Procedure> | Main<Query>,\n    arg?: LexValue | Params,\n    options: ActionOptions = {},\n  ): Promise<unknown> {\n    const method = getMain(ns)\n\n    if (typeof method === 'function') {\n      return method(this, arg, options)\n    }\n\n    if (method instanceof Procedure) {\n      const result = await this.xrpc(method, { ...options, body: arg as any })\n      return result.body\n    } else if (method instanceof Query) {\n      const result = await this.xrpc(method, { ...options, params: arg as any })\n      return result.body\n    } else {\n      throw new TypeError('Invalid lexicon')\n    }\n  }\n\n  /**\n   * Creates a new record with full type safety based on the schema.\n   *\n   * @param ns - The record schema definition\n   * @param input - The record data (without `$type`, which is added automatically)\n   * @param options - Create options including rkey (required for some record types)\n   * @returns The create output including URI and CID\n   *\n   * @example Creating a post\n   * ```typescript\n   * const result = await client.create(app.bsky.feed.post.main, {\n   *   text: 'Hello, world!',\n   *   createdAt: new Date().toISOString()\n   * })\n   * console.log(result.uri)\n   * ```\n   *\n   * @example Creating a record with explicit rkey\n   * ```typescript\n   * const result = await client.create(app.bsky.actor.profile.main, {\n   *   displayName: 'Alice'\n   * }, { rkey: 'self' })\n   * ```\n   */\n  public async create<const T extends RecordSchema>(\n    ns: NonNullable<unknown> extends CreateOptions<T>\n      ? Main<T>\n      : Restricted<'This record type requires an \"options\" argument'>,\n    input: Omit<Infer<T>, '$type'>,\n  ): Promise<CreateOutput>\n  public async create<const T extends RecordSchema>(\n    ns: Main<T>,\n    input: Omit<Infer<T>, '$type'>,\n    options: CreateOptions<T>,\n  ): Promise<CreateOutput>\n  public async create<const T extends RecordSchema>(\n    ns: Main<T>,\n    input: Omit<Infer<T>, '$type'>,\n    options: CreateOptions<T> = {} as CreateOptions<T>,\n  ): Promise<CreateOutput> {\n    const schema: T = getMain(ns)\n    const record = schema.build(input) as TypedLexMap<NsidString>\n    const rkey = options.rkey ?? getDefaultRecordKey(schema)\n    if (rkey !== undefined) schema.keySchema.assert(rkey)\n    const response = await this.createRecord(record, rkey, options)\n    return response.body\n  }\n\n  /**\n   * Deletes a record with type-safe options.\n   *\n   * @param ns - The record schema definition\n   * @param options - Delete options (rkey required for non-literal keys)\n   * @returns The delete output\n   */\n  public async delete<const T extends RecordSchema>(\n    ns: NonNullable<unknown> extends DeleteOptions<T>\n      ? Main<T>\n      : Restricted<'This record type requires an \"options\" argument'>,\n  ): Promise<DeleteOutput>\n  public async delete<const T extends RecordSchema>(\n    ns: Main<T>,\n    options?: DeleteOptions<T>,\n  ): Promise<DeleteOutput>\n  public async delete<const T extends RecordSchema>(\n    ns: Main<T>,\n    options: DeleteOptions<T> = {} as DeleteOptions<T>,\n  ): Promise<DeleteOutput> {\n    const schema = getMain(ns)\n    const rkey = schema.keySchema.parse(\n      options.rkey ?? getLiteralRecordKey(schema),\n    )\n    const response = await this.deleteRecord(schema.$type, rkey, options)\n    return response.body\n  }\n\n  /**\n   * Retrieves a record with type-safe validation.\n   *\n   * @param ns - The record schema definition\n   * @param options - Get options (rkey required for non-literal keys)\n   * @returns The record data validated against the schema\n   *\n   * @example\n   * ```typescript\n   * const profile = await client.get(app.bsky.actor.profile.main)\n   * // profile.value is typed as app.bsky.actor.profile.Record\n   * console.log(profile.value.displayName)\n   * ```\n   */\n  public async get<const T extends RecordSchema>(\n    ns: T['key'] extends `literal:${string}`\n      ? Main<T>\n      : Restricted<'This record type requires an \"options\" argument'>,\n  ): Promise<GetOutput<T>>\n  public async get<const T extends RecordSchema>(\n    ns: Main<T>,\n    options?: GetOptions<T>,\n  ): Promise<GetOutput<T>>\n  public async get<const T extends RecordSchema>(\n    ns: Main<T>,\n    options: GetOptions<T> = {} as GetOptions<T>,\n  ): Promise<GetOutput<T>> {\n    const schema = getMain(ns)\n    const rkey = schema.keySchema.parse(\n      options.rkey ?? getLiteralRecordKey(schema),\n    )\n    const response = await this.getRecord(schema.$type, rkey, options)\n    const value = schema.validate(response.body.value)\n    return { ...response.body, value }\n  }\n\n  /**\n   * Creates or updates a record with full type safety.\n   *\n   * @param ns - The record schema definition\n   * @param input - The record data\n   * @param options - Put options (rkey required for non-literal keys)\n   * @returns The put output including URI and CID\n   */\n  public async put<const T extends RecordSchema>(\n    ns: NonNullable<unknown> extends PutOptions<T>\n      ? Main<T>\n      : Restricted<'This record type requires an \"options\" argument'>,\n    input: Omit<Infer<T>, '$type'>,\n  ): Promise<PutOutput>\n  public async put<const T extends RecordSchema>(\n    ns: Main<T>,\n    input: Omit<Infer<T>, '$type'>,\n    options: PutOptions<T>,\n  ): Promise<PutOutput>\n  public async put<const T extends RecordSchema>(\n    ns: Main<T>,\n    input: Omit<Infer<T>, '$type'>,\n    options: PutOptions<T> = {} as PutOptions<T>,\n  ): Promise<PutOutput> {\n    const schema: T = getMain(ns)\n    const record = schema.build(input) as TypedLexMap<NsidString>\n    const rkey = options.rkey ?? getLiteralRecordKey(schema)\n    const response = await this.putRecord(record, rkey, options)\n    return response.body\n  }\n\n  /**\n   * Lists records with type-safe validation and separation of valid/invalid records.\n   *\n   * @param ns - The record schema definition\n   * @param options - List options\n   * @returns Records split into valid (matching schema) and invalid arrays\n   *\n   * @example\n   * ```typescript\n   * const result = await client.list(app.bsky.feed.post.main, { limit: 100 })\n   * console.log(`Found ${result.records.length} valid posts`)\n   * console.log(`Found ${result.invalid.length} invalid records`)\n   * ```\n   */\n  async list<const T extends RecordSchema>(\n    ns: Main<T>,\n    options?: ListOptions,\n  ): Promise<ListOutput<T>> {\n    const schema = getMain(ns)\n    const { body } = await this.listRecords(schema.$type, options)\n\n    const records: ListRecord<Infer<T>>[] = []\n    const invalid: LexMap[] = []\n\n    for (const record of body.records) {\n      const parsed = schema.safeValidate(record.value)\n      if (parsed.success) {\n        records.push({ ...record, value: parsed.value })\n      } else {\n        invalid.push(record.value)\n      }\n    }\n\n    return { ...body, records, invalid }\n  }\n}\n\nfunction getDefaultRecordKey<const T extends RecordSchema>(\n  schema: T,\n): undefined | InferRecordKey<T> {\n  // Let the server generate the TID\n  if (schema.key === 'tid') return undefined\n  if (schema.key === 'any') return undefined\n\n  return getLiteralRecordKey(schema)\n}\n\nfunction getLiteralRecordKey<const T extends RecordSchema>(\n  schema: T,\n): InferRecordKey<T> {\n  if (schema.key.startsWith('literal:')) {\n    return schema.key.slice(8) as InferRecordKey<T>\n  }\n\n  throw new TypeError(\n    `An \"rkey\" must be provided for record key type \"${schema.key}\" (${schema.$type})`,\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/errors.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'\nimport {\n  XrpcAuthenticationError,\n  XrpcFetchError,\n  XrpcInternalError,\n  XrpcInvalidResponseError,\n  XrpcResponseError,\n  XrpcUpstreamError,\n  asXrpcFailure,\n} from './errors.js'\n\n// Minimal method fixture\nconst testQuery = l.query(\n  'io.example.test',\n  l.params(),\n  l.jsonPayload({ value: l.string() }),\n  ['TestError', 'AnotherError'],\n)\n\nconst testQueryNoErrors = l.query(\n  'io.example.noErrors',\n  l.params(),\n  l.jsonPayload({ value: l.string() }),\n)\n\n// ============================================================================\n// XrpcResponseError\n// ============================================================================\n\ndescribe(XrpcResponseError, () => {\n  function createResponseError(\n    status: number,\n    errorCode: string,\n    message?: string,\n  ) {\n    const response = new Response(null, { status })\n    return new XrpcResponseError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: errorCode, message },\n    })\n  }\n\n  it('exposes status from the response', () => {\n    const err = createResponseError(404, 'NotFound')\n    expect(err.reason).toBe(err)\n    expect(err.status).toBe(404)\n  })\n\n  it('exposes headers from the response', () => {\n    const response = new Response(null, {\n      status: 400,\n      headers: { 'X-Test': 'value' },\n    })\n    const err = new XrpcResponseError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'TestError' },\n    })\n    expect(err.reason).toBe(err)\n    expect(err.headers.get('X-Test')).toBe('value')\n  })\n\n  it('exposes body from the payload', () => {\n    const err = createResponseError(400, 'TestError', 'details')\n    expect(err.body).toEqual({ error: 'TestError', message: 'details' })\n  })\n\n  describe('toDownstreamError', () => {\n    it('returns 502 for upstream 500 errors', () => {\n      const err = createResponseError(\n        500,\n        'InternalServerError',\n        'Upstream crashed',\n      )\n      const downstream = err.toDownstreamError()\n\n      expect(downstream.status).toBe(502)\n      expect(downstream.body).toEqual({\n        error: 'InternalServerError',\n        message: 'Upstream crashed',\n      })\n    })\n\n    it('preserves original status for non-500 5xx errors', () => {\n      const err = createResponseError(503, 'ServiceUnavailable', 'Try later')\n      const downstream = err.toDownstreamError()\n\n      expect(downstream.status).toBe(503)\n      expect(downstream.body).toEqual({\n        error: 'ServiceUnavailable',\n        message: 'Try later',\n      })\n    })\n\n    it('preserves original status for 4xx errors', () => {\n      const err = createResponseError(404, 'NotFound', 'Record not found')\n      const downstream = err.toDownstreamError()\n\n      expect(downstream.status).toBe(404)\n      expect(downstream.body).toEqual({\n        error: 'NotFound',\n        message: 'Record not found',\n      })\n    })\n  })\n\n  describe('toJSON', () => {\n    it('returns the payload body', () => {\n      const err = createResponseError(400, 'TestError', 'message')\n      expect(err.toJSON()).toEqual({ error: 'TestError', message: 'message' })\n    })\n  })\n\n  describe('matchesSchemaErrors', () => {\n    it('returns true when error matches method declared errors', () => {\n      const err = createResponseError(400, 'TestError')\n      expect(err.matchesSchemaErrors()).toBe(true)\n    })\n\n    it('returns false for undeclared error codes', () => {\n      const err = createResponseError(400, 'UnknownError')\n      expect(err.matchesSchemaErrors()).toBe(false)\n    })\n\n    it('returns false when method has no declared errors', () => {\n      const response = new Response(null, { status: 400 })\n      const err = new XrpcResponseError(testQueryNoErrors, response, {\n        encoding: 'application/json',\n        body: { error: 'SomeError' },\n      })\n      expect(err.matchesSchemaErrors()).toBe(false)\n    })\n  })\n\n  describe('shouldRetry', () => {\n    it('returns true for retryable status codes', () => {\n      expect(createResponseError(429, 'RateLimit').shouldRetry()).toBe(true)\n      expect(createResponseError(500, 'Internal').shouldRetry()).toBe(true)\n      expect(createResponseError(502, 'BadGateway').shouldRetry()).toBe(true)\n      expect(createResponseError(503, 'Unavailable').shouldRetry()).toBe(true)\n    })\n\n    it('returns false for non-retryable status codes', () => {\n      expect(createResponseError(400, 'BadRequest').shouldRetry()).toBe(false)\n      expect(createResponseError(401, 'Unauthorized').shouldRetry()).toBe(false)\n      expect(createResponseError(404, 'NotFound').shouldRetry()).toBe(false)\n    })\n  })\n})\n\n// ============================================================================\n// XrpcAuthenticationError\n// ============================================================================\n\ndescribe(XrpcAuthenticationError, () => {\n  it('is never retryable', () => {\n    const response = new Response(null, { status: 401 })\n    const err = new XrpcAuthenticationError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'AuthenticationRequired' },\n    })\n    expect(err.shouldRetry()).toBe(false)\n  })\n\n  it('parses WWW-Authenticate header', () => {\n    const response = new Response(null, {\n      status: 401,\n      headers: {\n        'WWW-Authenticate': 'Bearer realm=\"api\", error=\"InvalidToken\"',\n      },\n    })\n    const err = new XrpcAuthenticationError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'AuthenticationRequired' },\n    })\n    expect(err.reason).toBe(err)\n    expect(err.wwwAuthenticate).toHaveProperty('Bearer')\n  })\n\n  it('returns empty object when no WWW-Authenticate header', () => {\n    const response = new Response(null, { status: 401 })\n    const err = new XrpcAuthenticationError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'AuthenticationRequired' },\n    })\n    expect(err.wwwAuthenticate).toEqual({})\n  })\n\n  it('toDownstreamError always returns 401', () => {\n    const response = new Response(null, { status: 401 })\n    const err = new XrpcAuthenticationError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'AuthenticationRequired', message: 'No token' },\n    })\n    const downstream = err.toDownstreamError()\n\n    expect(downstream.status).toBe(401)\n    expect(downstream.body).toEqual({\n      error: 'AuthenticationRequired',\n      message: 'No token',\n    })\n  })\n})\n\n// ============================================================================\n// XrpcUpstreamError\n// ============================================================================\n\ndescribe(XrpcUpstreamError, () => {\n  it('has error code UpstreamFailure', () => {\n    const response = new Response(null, { status: 200 })\n    const err = new XrpcUpstreamError(testQuery, response)\n    expect(err.reason).toBe(err)\n    expect(err.error).toBe('UpstreamFailure')\n  })\n\n  it('toDownstreamError returns 502', () => {\n    const response = new Response(null, { status: 200 })\n    const err = new XrpcUpstreamError(testQuery, response)\n    const downstream = err.toDownstreamError()\n    expect(downstream.status).toBe(502)\n  })\n\n  it('shouldRetry is true for retryable status codes', () => {\n    const response = new Response(null, { status: 502 })\n    const err = new XrpcUpstreamError(testQuery, response)\n    expect(err.shouldRetry()).toBe(true)\n  })\n\n  it('shouldRetry is false for non-retryable status codes', () => {\n    const response = new Response(null, { status: 200 })\n    const err = new XrpcUpstreamError(testQuery, response)\n    expect(err.shouldRetry()).toBe(false)\n  })\n})\n\n// ============================================================================\n// XrpcInvalidResponseError\n// ============================================================================\n\ndescribe(XrpcInvalidResponseError, () => {\n  it('extends XrpcUpstreamError', () => {\n    const response = new Response(null, { status: 200 })\n    const validationError = new LexValidationError([\n      new IssueInvalidType([], 42, ['string']),\n    ])\n    const err = new XrpcInvalidResponseError(\n      testQuery,\n      response,\n      { encoding: 'application/json', body: { value: 42 } },\n      validationError,\n    )\n\n    expect(err).toBeInstanceOf(XrpcUpstreamError)\n    expect(err.reason).toBe(err)\n    expect(err.error).toBe('UpstreamFailure')\n    expect(err.cause).toBe(validationError)\n  })\n\n  it('includes validation error message', () => {\n    const validationError = new LexValidationError([\n      new IssueInvalidType([], 42, ['string']),\n    ])\n    const err = new XrpcInvalidResponseError(\n      testQuery,\n      new Response(null, { status: 200 }),\n      { encoding: 'application/json', body: { value: 42 } },\n      validationError,\n    )\n\n    expect(err.message).toContain('Invalid response:')\n    expect(err.message).toContain(validationError.message)\n  })\n\n  it('toDownstreamError returns 502', () => {\n    const validationError = new LexValidationError([\n      new IssueInvalidType([], 42, ['string']),\n    ])\n    const err = new XrpcInvalidResponseError(\n      testQuery,\n      new Response(null, { status: 200 }),\n      { encoding: 'application/json', body: { value: 42 } },\n      validationError,\n    )\n    const downstream = err.toDownstreamError()\n    expect(downstream.status).toBe(502)\n  })\n})\n\n// ============================================================================\n// XrpcInternalError\n// ============================================================================\n\ndescribe(XrpcInternalError, () => {\n  it('has error code InternalServerError', () => {\n    const err = new XrpcInternalError(testQuery)\n    expect(err.reason).toBe(err)\n    expect(err.error).toBe('InternalServerError')\n  })\n\n  it('toJSON does not expose internal details', () => {\n    const err = new XrpcInternalError(\n      testQuery,\n      'Secret database connection string leaked',\n    )\n    const json = err.toJSON()\n\n    expect(json.error).toBe('InternalServerError')\n    expect(json.message).toBe('Internal Server Error')\n    expect(json.message).not.toContain('Secret')\n  })\n\n  it('toDownstreamError returns 500', () => {\n    const err = new XrpcInternalError(testQuery, 'internal details')\n    const downstream = err.toDownstreamError()\n\n    expect(downstream.status).toBe(500)\n    expect(downstream.body.error).toBe('InternalServerError')\n    expect(downstream.body.message).toBe('Internal Server Error')\n  })\n\n  it('is not retryable', () => {\n    const err = new XrpcInternalError(testQuery, 'something broke')\n    expect(err.shouldRetry()).toBe(false)\n  })\n})\n\n// ============================================================================\n// XrpcFetchError\n// ============================================================================\n\ndescribe(XrpcFetchError, () => {\n  it('extends XrpcInternalError', () => {\n    const err = new XrpcFetchError(testQuery, new TypeError('fetch failed'))\n    expect(err).toBeInstanceOf(XrpcInternalError)\n    expect(err.error).toBe('InternalServerError')\n  })\n\n  it('uses cause message when cause is an Error', () => {\n    const cause = new TypeError('Failed to fetch')\n    const err = new XrpcFetchError(testQuery, cause)\n    expect(err.message).toBe('Unexpected fetchHandler() error: Failed to fetch')\n    expect(err.cause).toBe(cause)\n  })\n\n  it('uses fallback message when cause is not an Error', () => {\n    const err = new XrpcFetchError(testQuery, 'string cause')\n    expect(err.message).toBe('Unexpected fetchHandler() error: string cause')\n    expect(err.cause).toBe('string cause')\n  })\n\n  it('is retryable', () => {\n    const err = new XrpcFetchError(testQuery, new Error('network timeout'))\n    expect(err.shouldRetry()).toBe(true)\n  })\n\n  it('toJSON does not expose internal details', () => {\n    const err = new XrpcFetchError(\n      testQuery,\n      new Error('ECONNREFUSED 10.0.0.1:443'),\n    )\n    const json = err.toJSON()\n\n    expect(json.error).toBe('InternalServerError')\n    expect(json.message).toBe('Failed to perform upstream request')\n    expect(json.message).not.toContain('ECONNREFUSED')\n  })\n\n  it('toDownstreamError returns 502', () => {\n    const err = new XrpcFetchError(testQuery, new Error('DNS lookup failed'))\n    const downstream = err.toDownstreamError()\n\n    expect(downstream.status).toBe(502)\n    expect(downstream.body.error).toBe('InternalServerError')\n    expect(downstream.body.message).toBe('Failed to perform upstream request')\n  })\n})\n\n// ============================================================================\n// asXrpcFailure\n// ============================================================================\n\ndescribe('asXrpcFailure', () => {\n  it('returns existing XrpcResponseError for the same method', () => {\n    const response = new Response(null, { status: 400 })\n    const err = new XrpcResponseError(testQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'TestError' },\n    })\n    expect(asXrpcFailure(testQuery, err)).toBe(err)\n  })\n\n  it('wraps unknown errors in XrpcInternalError', () => {\n    const err = new TypeError('fetch failed')\n    const failure = asXrpcFailure(testQuery, err)\n\n    expect(failure).toBeInstanceOf(XrpcInternalError)\n    expect(failure.cause).toBe(err)\n  })\n\n  it('wraps XrpcError for a different method in XrpcInternalError', () => {\n    const otherQuery = l.query(\n      'io.example.other',\n      l.params(),\n      l.payload('application/json', l.object({ value: l.string() })),\n    )\n    const response = new Response(null, { status: 400 })\n    const err = new XrpcResponseError(otherQuery, response, {\n      encoding: 'application/json',\n      body: { error: 'TestError' },\n    })\n    const failure = asXrpcFailure(testQuery, err)\n    expect(failure).toBeInstanceOf(XrpcInternalError)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-client/src/errors.ts",
    "content": "import { LexError, LexErrorCode, LexErrorData } from '@atproto/lex-data'\nimport {\n  InferMethodError,\n  LexValidationError,\n  Procedure,\n  Query,\n  ResultFailure,\n  lexErrorDataSchema,\n} from '@atproto/lex-schema'\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport { Agent } from './agent.js'\nimport { XrpcUnknownResponsePayload } from './types.js'\nimport {\n  WWWAuthenticate,\n  parseWWWAuthenticateHeader,\n} from './www-authenticate.js'\n\nexport type { XrpcUnknownResponsePayload }\n\nexport type DownstreamError<N extends LexErrorCode = LexErrorCode> = {\n  status: number\n  headers?: Headers\n  encoding?: 'application/json'\n  body: LexErrorData<N>\n}\n\n/**\n * HTTP status codes that indicate a transient error that may succeed on retry.\n *\n * Includes:\n * - 408 Request Timeout\n * - 425 Too Early\n * - 429 Too Many Requests (rate limited)\n * - 500 Internal Server Error\n * - 502 Bad Gateway\n * - 503 Service Unavailable\n * - 504 Gateway Timeout\n * - 522 Connection Timed Out (Cloudflare)\n * - 524 A Timeout Occurred (Cloudflare)\n */\nexport const RETRYABLE_HTTP_STATUS_CODES: ReadonlySet<number> = new Set([\n  408, 425, 429, 500, 502, 503, 504, 522, 524,\n])\n\nexport { LexError }\nexport type { LexErrorCode, LexErrorData }\n\n/**\n * The payload structure for XRPC error responses.\n *\n * All XRPC errors return JSON with an `error` code and optional `message`.\n *\n * @typeParam N - The specific error code type\n */\nexport type XrpcErrorPayload<N extends LexErrorCode = LexErrorCode> = {\n  body: LexErrorData<N>\n  encoding: 'application/json'\n}\n\n/**\n * All unsuccessful responses should follow a standard error response\n * schema. The Content-Type should be application/json, and the payload\n * should be a JSON object with the following fields:\n *\n * - `error` (string, required): type name of the error (generic ASCII\n *   constant, no whitespace)\n * - `message` (string, optional): description of the error, appropriate for\n *   display to humans\n *\n * This function checks whether a given payload matches this schema.\n */\nexport function isXrpcErrorPayload(\n  payload: XrpcUnknownResponsePayload | null | undefined,\n): payload is XrpcErrorPayload {\n  return (\n    payload != null &&\n    payload.encoding === 'application/json' &&\n    lexErrorDataSchema.matches(payload.body)\n  )\n}\n\n/**\n * Abstract base class for all XRPC errors.\n *\n * Extends {@link LexError} and implements {@link ResultFailure} for use with\n * safe/result-based error handling patterns.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n * @typeParam N - The error code type\n * @typeParam TReason - The reason type for ResultFailure\n *\n * @see {@link XrpcResponseError} - For valid XRPC error responses\n * @see {@link XrpcUpstreamError} - For invalid/unexpected responses\n * @see {@link XrpcInternalError} - For network/internal errors\n */\nexport abstract class XrpcError<\n    M extends Procedure | Query = Procedure | Query,\n    N extends LexErrorCode = LexErrorCode,\n    TReason = unknown,\n  >\n  extends LexError<N>\n  implements ResultFailure<TReason>\n{\n  name = 'XrpcError'\n\n  constructor(\n    readonly method: M,\n    error: N,\n    message: string = `${error} Lexicon RPC error`,\n    options?: ErrorOptions,\n  ) {\n    super(error, message, options)\n  }\n\n  /**\n   * @see {@link ResultFailure.success}\n   */\n  readonly success = false as const\n\n  /**\n   * @see {@link ResultFailure.reason}\n   */\n  abstract readonly reason: TReason\n\n  /**\n   * Indicates whether the error is transient and can be retried.\n   */\n  abstract shouldRetry(): boolean\n\n  abstract toDownstreamError(): DownstreamError\n\n  matchesSchemaErrors(): this is XrpcError<M, InferMethodError<M>> {\n    return this.method.errors?.includes(this.error) ?? false\n  }\n}\n\n/**\n * Error class for valid XRPC error responses from the server.\n *\n * This represents a properly formatted XRPC error where the server returned\n * a non-2xx status with a valid JSON error payload containing `error` and\n * optional `message` fields.\n *\n * Use {@link matchesSchemaErrors} to check if the error matches the method's declared\n * error types for type-safe error handling.\n *\n * @typeParam M - The XRPC method type\n * @typeParam N - The error code type (inferred from method or generic)\n *\n * @example Handling specific errors\n * ```typescript\n * try {\n *   await client.xrpc(someMethod, options)\n * } catch (err) {\n *   if (err instanceof XrpcResponseError && err.error === 'RecordNotFound') {\n *     // Handle not found case\n *   }\n * }\n * ```\n */\nexport class XrpcResponseError<\n  M extends Procedure | Query = Procedure | Query,\n  N extends LexErrorCode = InferMethodError<M> | LexErrorCode,\n> extends XrpcError<M, N, XrpcResponseError<M, N>> {\n  name = 'XrpcResponseError'\n\n  constructor(\n    method: M,\n    readonly response: Response,\n    readonly payload: XrpcErrorPayload<N>,\n    options?: ErrorOptions,\n  ) {\n    const { error, message } = payload.body\n    super(method, error, message, options)\n  }\n\n  override get reason(): this {\n    return this\n  }\n\n  override shouldRetry(): boolean {\n    return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)\n  }\n\n  override toJSON(): LexErrorData<N> {\n    return this.payload.body\n  }\n\n  override toDownstreamError(): DownstreamError {\n    // If the upstream server returned a 5xx error, we want to return a 502 Bad\n    // Gateway to downstream clients, as the issue is with the upstream server,\n    // not us. We still return the original error code and message in the body\n    // for transparency, but we do not want to expose internal server errors\n    // from the upstream server as-is to downstream clients.\n    return {\n      status: this.response.status === 500 ? 502 : this.status,\n      headers: stripHopByHopHeaders(this.headers),\n      body: this.toJSON(),\n    }\n  }\n\n  get status(): number {\n    return this.response.status\n  }\n\n  get headers(): Headers {\n    return this.response.headers\n  }\n\n  get body(): LexErrorData<N> {\n    return this.payload.body\n  }\n}\n\nexport type { WWWAuthenticate }\n\n/**\n * Error class for 401 Unauthorized XRPC responses.\n *\n * Extends {@link XrpcResponseError} with access to parsed WWW-Authenticate header\n * information, useful for implementing authentication flows.\n *\n * Authentication errors are never retryable as they require user intervention\n * (e.g., re-authentication, token refresh).\n *\n * @typeParam M - The XRPC method type\n * @typeParam N - The error code type\n *\n * @example Handling authentication errors\n * ```typescript\n * try {\n *   await client.xrpc(someMethod, options)\n * } catch (err) {\n *   if (err instanceof XrpcAuthenticationError) {\n *     const { DPoP } = err.wwwAuthenticate\n *     if (DPoP?.error === 'use_dpop_nonce') {\n *       // Handle DPoP nonce requirement\n *     }\n *   }\n * }\n * ```\n */\nexport class XrpcAuthenticationError<\n  M extends Procedure | Query = Procedure | Query,\n  N extends LexErrorCode = LexErrorCode,\n> extends XrpcResponseError<M, N> {\n  name = 'XrpcAuthenticationError'\n\n  override shouldRetry(): boolean {\n    return false\n  }\n\n  #wwwAuthenticateCached?: WWWAuthenticate\n  /**\n   * Parsed WWW-Authenticate header from the response.\n   * Contains authentication scheme parameters (e.g., Bearer realm, DPoP nonce).\n   */\n  get wwwAuthenticate(): WWWAuthenticate {\n    return (this.#wwwAuthenticateCached ??=\n      parseWWWAuthenticateHeader(\n        this.response.headers.get('www-authenticate'),\n      ) ?? {})\n  }\n\n  override toDownstreamError(): DownstreamError {\n    return {\n      status: 401,\n      headers: stripHopByHopHeaders(this.headers),\n      body: this.toJSON(),\n    }\n  }\n}\n\n/**\n * Error class for invalid or unprocessable XRPC responses from upstream servers.\n *\n * This occurs when the server returns a response that doesn't conform to the\n * XRPC protocol, such as:\n * - Missing or invalid Content-Type header\n * - Response body that doesn't match the method's output schema\n * - Non-JSON error responses\n * - Responses from non-XRPC endpoints\n *\n * The error code is always 'UpstreamFailure' and maps to HTTP 502 Bad Gateway\n * when converted to a response.\n *\n * @typeParam M - The XRPC method type\n */\nexport class XrpcUpstreamError<\n  M extends Procedure | Query = Procedure | Query,\n> extends XrpcError<M, 'UpstreamFailure', XrpcUpstreamError<M>> {\n  name = 'XrpcUpstreamError'\n\n  constructor(\n    method: M,\n    readonly response: Response,\n    readonly payload: XrpcUnknownResponsePayload | null = null,\n    message: string = `Unexpected upstream XRPC response`,\n    options?: ErrorOptions,\n  ) {\n    super(method, 'UpstreamFailure', message, options)\n  }\n\n  override get reason(): this {\n    return this\n  }\n\n  override shouldRetry(): boolean {\n    return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)\n  }\n\n  override toDownstreamError(): DownstreamError {\n    return { status: 502, body: this.toJSON() }\n  }\n}\n\n/**\n * Error class for invalid XRPC responses that fail schema validation.\n *\n * This is a specific type of {@link XrpcUpstreamError} that indicates the\n * upstream server returned a response that was structurally valid but did not\n * conform to the expected schema for the method. This likely indicates a\n * mismatch between client and server versions or an issue with the server's\n * XRPC implementation.\n *\n * @typeParam M - The XRPC method type\n */\nexport class XrpcInvalidResponseError<\n  M extends Procedure | Query = Procedure | Query,\n> extends XrpcUpstreamError<M> {\n  name = 'XrpcInvalidResponseError'\n\n  constructor(\n    method: M,\n    response: Response,\n    payload: XrpcUnknownResponsePayload,\n    readonly cause: LexValidationError,\n  ) {\n    super(method, response, payload, `Invalid response: ${cause.message}`, {\n      cause,\n    })\n  }\n\n  override toDownstreamError(): DownstreamError {\n    // @NOTE This could be reflected as both a 500 (\"we\" are at fault) and 502\n    // (\"they\" are at fault). We are using 502 here to allow downstream clients\n    // to determine that the issue lies at the interface between us and the\n    // upstream server, rather than an issue with our internal processing.\n    return { status: 502, body: this.toJSON() }\n  }\n}\n\n/**\n * Error class for unexpected internal/client-side errors during XRPC requests.\n *\n * The error code is always 'InternalServerError' and these errors not\n * considered retryable as they stem from unforeseen issues in the\n * implementation.\n *\n * @typeParam M - The XRPC method type\n */\nexport class XrpcInternalError<\n  M extends Procedure | Query = Procedure | Query,\n> extends XrpcError<M, 'InternalServerError', XrpcInternalError<M>> {\n  name = 'XrpcInternalError'\n\n  constructor(method: M, message?: string, options?: ErrorOptions) {\n    super(\n      method,\n      'InternalServerError',\n      message ?? 'Unable to fulfill XRPC request',\n      options,\n    )\n  }\n\n  override get reason(): this {\n    return this\n  }\n\n  override shouldRetry(): boolean {\n    return false\n  }\n\n  override toJSON(): LexErrorData {\n    // @NOTE Do not expose internal error details to downstream clients\n    return { error: this.error, message: 'Internal Server Error' }\n  }\n\n  override toDownstreamError(): DownstreamError {\n    return { status: 500, body: this.toJSON() }\n  }\n}\n\n/**\n * Special case of XrpcInternalError that specifically represents errors thrown\n * by {@link Agent.fetchHandler} during the XRPC request. This includes:\n * - Network errors (connection refused, DNS failure)\n * - Request timeouts\n * - Request aborted via AbortSignal\n *\n * These errors are optimistically considered retryable, as many fetch errors\n * are transient and may succeed on retry.\n */\nexport class XrpcFetchError<\n  M extends Procedure | Query = Procedure | Query,\n> extends XrpcInternalError<M> {\n  name = 'XrpcFetchError'\n\n  constructor(method: M, cause: unknown) {\n    const message = cause instanceof Error ? cause.message : String(cause)\n    super(method, `Unexpected fetchHandler() error: ${message}`, { cause })\n  }\n\n  override shouldRetry(): boolean {\n    // Ideally, we would inspect the reason to determine if it's retryable (by\n    // detecting network errors, timeouts, etc.). Since these cases are highly\n    // platform-dependent, we optimistically assume all fetch errors are\n    // transient and retryable.\n    return true\n  }\n\n  override toJSON(): LexErrorData {\n    // @NOTE Do not expose internal error details to downstream clients\n    return { error: this.error, message: 'Failed to perform upstream request' }\n  }\n\n  override toDownstreamError(): DownstreamError {\n    // While it might technically be a 500 error, we use 502 Bad Gateway here to\n    // indicate that the error occurred while communicating with the upstream\n    // server, allowing downstream clients to distinguish between errors in our\n    // internal processing (500) and errors in the upstream server or network\n    // (502).\n    return { status: 502, body: this.toJSON() }\n  }\n}\n\n/**\n * Union type of all possible XRPC failure types.\n *\n * Used as the return type for safe/non-throwing XRPC methods. Check the\n * `success` property to distinguish between success and failure:\n *\n * @typeParam M - The XRPC method type\n *\n * @example\n * ```typescript\n * const result = await client.xrpcSafe(someMethod, options)\n * if (result.success) {\n *   console.log(result.body) // XrpcResponse\n * } else {\n *   // result is XrpcFailure (XrpcResponseError | XrpcUpstreamError | XrpcInternalError)\n *   console.error(result.error, result.message)\n * }\n * ```\n */\nexport type XrpcFailure<M extends Procedure | Query = Procedure | Query> =\n  // The server returned a valid XRPC error response\n  | XrpcResponseError<M>\n  // The response was not a valid XRPC response, or it does not match the schema\n  | XrpcUpstreamError<M>\n  // Something went wrong (network error, etc.)\n  | XrpcInternalError<M>\n\n/**\n * Converts an unknown error into an appropriate {@link XrpcFailure} type.\n *\n * If the error is already an XrpcFailure for the given method, returns it as-is.\n * Otherwise, wraps it in an {@link XrpcInternalError}.\n *\n * @param method - The XRPC method that was called\n * @param cause - The error to convert\n * @returns An XrpcFailure instance\n *\n * @example\n * ```typescript\n * try {\n *   const response = await fetch(...)\n *   // ... process response\n * } catch (err) {\n *   return asXrpcFailure(method, err)\n * }\n * ```\n */\nexport function asXrpcFailure<M extends Procedure | Query>(\n  method: M,\n  cause: unknown,\n): XrpcFailure<M> {\n  if (\n    cause instanceof XrpcResponseError ||\n    cause instanceof XrpcUpstreamError ||\n    cause instanceof XrpcInternalError\n  ) {\n    if (cause.method === method) return cause\n  }\n\n  return new XrpcInternalError(method, undefined, { cause })\n}\n\nconst HOP_BY_HOP_HEADERS = new Set([\n  'connection',\n  'keep-alive',\n  'proxy-authenticate',\n  'proxy-authorization',\n  'te',\n  'trailer',\n  'transfer-encoding',\n  'upgrade',\n])\n\nfunction stripHopByHopHeaders(headers: Headers): Headers {\n  const result = new Headers(headers)\n\n  // Remove statically known hop-by-hop headers\n  for (const name of HOP_BY_HOP_HEADERS) {\n    result.delete(name)\n  }\n\n  // Remove headers listed in the \"Connection\" header\n  const connection = headers.get('connection')\n  if (connection) {\n    for (const name of connection.split(',')) {\n      result.delete(name.trim())\n    }\n  }\n\n  // These are not actually hop-by-hop headers, but we remove them because the\n  // upstream payload gets parsed and re-serialized, so content length and\n  // encoding may no longer be accurate.\n  result.delete('content-length')\n  result.delete('content-encoding')\n\n  return result\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/index.ts",
    "content": "export * from './agent.js'\nexport * from './client.js'\nexport * from './errors.js'\nexport * from './response.js'\nexport * from './types.js'\nexport * from './xrpc.js'\n"
  },
  {
    "path": "packages/lex/lex-client/src/response.ts",
    "content": "import { LexParseOptions, lexParse } from '@atproto/lex-json'\nimport {\n  InferMethodOutputEncoding,\n  InferOutput,\n  LexValue,\n  Payload,\n  Procedure,\n  Query,\n  ResultSuccess,\n  Validator,\n} from '@atproto/lex-schema'\nimport {\n  XrpcAuthenticationError,\n  XrpcInvalidResponseError,\n  XrpcResponseError,\n  XrpcUpstreamError,\n  isXrpcErrorPayload,\n} from './errors.js'\nimport {\n  EncodingString,\n  XrpcUnknownResponsePayload,\n  isEncodingString,\n} from './types.js'\n\nconst CONTENT_TYPE_BINARY = 'application/octet-stream'\nconst CONTENT_TYPE_JSON = 'application/json'\n\n// @NOTE the output schema is used in \"parse\" mode (safeParse), which means that\n// defaults will be applied and coercions will be performed, so we need to use\n// InferOutput here to get the final parsed type, not Infer/InferInput. For this\n// reason, we cannot use InferMethodOutputBody and InferMethodOutput from\n// lex-schema here.\n\ntype InferEncodingType<TEncoding extends string> = TEncoding extends '*/*'\n  ? EncodingString\n  : TEncoding extends `${infer T extends string}/*`\n    ? `${T}/${string}`\n    : TEncoding\n\ntype InferBodyType<\n  TEncoding extends string,\n  TSchema,\n> = TSchema extends Validator\n  ? InferOutput<TSchema>\n  : TEncoding extends `application/json`\n    ? LexValue\n    : Uint8Array\n\n/**\n * The body type of an XRPC response, inferred from the method's output schema.\n *\n * For JSON responses, this is the parsed LexValue. For binary responses,\n * this is a Uint8Array.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n */\nexport type XrpcResponseBody<M extends Procedure | Query> =\n  M['output'] extends Payload<infer TEncoding, infer TSchema>\n    ? TEncoding extends string\n      ? InferBodyType<TEncoding, TSchema>\n      : undefined\n    : never\n\n/**\n * The full payload type of an XRPC response, including body and encoding.\n *\n * Returns `null` for methods that have no output.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n */\nexport type XrpcResponsePayload<M extends Procedure | Query> =\n  M['output'] extends Payload<infer TEncoding, infer TSchema>\n    ? TEncoding extends string\n      ? {\n          encoding: InferEncodingType<TEncoding>\n          body: InferBodyType<TEncoding, TSchema>\n        }\n      : undefined\n    : never\n\nexport type XrpcResponseOptions = {\n  /**\n   * Whether to validate the response against the method's output schema.\n   * Disabling this can improve performance but may lead to runtime errors if\n   * the response does not conform to the expected schema. Only set this to\n   * `false` if you are certain that the upstream service will always return\n   * valid responses.\n   *\n   * @default true\n   */\n  validateResponse?: boolean\n\n  /**\n   * Whether to strictly process response payloads according to Lex encoding\n   * rules. By default, the client will reject responses with invalid Lex data\n   * (floats and invalid $bytes / $link objects).\n   *\n   * Setting this option to `false` will allow the client to accept such\n   * responses in a non-strict mode, where invalid Lex data will be returned\n   * as-is (e.g., floats will not be rejected, and invalid $bytes / $link\n   * objects will not be converted to Uint8Array / Cid). When in non-strict\n   * mode, the validation will also be relaxed when validating the response\n   * against the method's output schema, allowing values that do not strictly\n   * conform to the schema (e.g. datetime strings that are not valid RFC3339\n   * format, blobs that are not of the right size/mime-type, etc.) to be\n   * accepted as long as their basic structure is correct.\n   *\n   * When validation is enabled (the default), the values defined through the\n   * method schema will be enforced, ensuring that the client can still process\n   * the response even if the server returns invalid Lex data.\n   *\n   * @default true\n   * @see {@link LexParseOptions.strict}\n   */\n  strictResponseProcessing?: boolean\n}\n\n/**\n * Small container for XRPC response data.\n *\n * @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.\n */\nexport class XrpcResponse<M extends Procedure | Query>\n  implements ResultSuccess<XrpcResponse<M>>\n{\n  /** @see {@link ResultSuccess.success} */\n  readonly success = true as const\n\n  /** @see {@link ResultSuccess.value} */\n  get value(): this {\n    return this\n  }\n\n  constructor(\n    readonly method: M,\n    readonly status: number,\n    readonly headers: Headers,\n    readonly payload: XrpcResponsePayload<M>,\n  ) {}\n\n  /**\n   * Whether the response payload was parsed as {@link LexValue} (`true`) or is\n   * in binary form {@link Uint8Array} (`false`).\n   */\n  get isParsed() {\n    return this.method.output.encoding === CONTENT_TYPE_JSON\n  }\n\n  /**\n   * The Content-Type encoding of the response (e.g., 'application/json').\n   * Returns `undefined` if the response has no body.\n   */\n  get encoding() {\n    return this.payload?.encoding as InferMethodOutputEncoding<M>\n  }\n\n  /**\n   * The parsed response body.\n   *\n   * For 'application/json' responses, this is the parsed and validated LexValue.\n   * For binary responses, this is a Uint8Array.\n   * Returns `undefined` if the response has no body.\n   */\n  get body() {\n    return this.payload?.body as XrpcResponseBody<M>\n  }\n\n  /**\n   * @throws {XrpcResponseError} in case of (valid) XRPC error responses. Use\n   * {@link XrpcResponseError.matchesSchemaErrors} to narrow the error type based on\n   * the method's declared error schema. This can be narrowed further as a\n   * {@link XrpcAuthenticationError} if the error is an authentication error.\n   * @throws {XrpcUpstreamError} when the response is not a valid XRPC\n   * response, or if the response does not conform to the method's schema.\n   */\n  static async fromFetchResponse<const M extends Procedure | Query>(\n    method: M,\n    response: Response,\n    options?: XrpcResponseOptions,\n  ): Promise<XrpcResponse<M>> {\n    // @NOTE The body MUST either be read or canceled to avoid resource leaks.\n    // Since nothing should cause an exception before \"readPayload\" is\n    // called, we can safely not use a try/finally here.\n\n    // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here\n    if (response.status < 200 || response.status >= 300) {\n      // Always parse json for error responses\n      const payload = await readPayload(method, response, {\n        parse: { strict: options?.strictResponseProcessing ?? true },\n      })\n\n      // Properly formatted XRPC error response ?\n      if (response.status >= 400 && isXrpcErrorPayload(payload)) {\n        throw response.status === 401\n          ? new XrpcAuthenticationError<M>(method, response, payload)\n          : new XrpcResponseError<M>(method, response, payload)\n      }\n\n      // Invalid XRPC response (we probably did not hit an XRPC implementation)\n      throw new XrpcUpstreamError(\n        method,\n        response,\n        payload,\n        response.status >= 500\n          ? 'Upstream server encountered an error'\n          : response.status >= 400\n            ? 'Invalid response payload'\n            : 'Invalid response status code',\n      )\n    }\n\n    const payload = await readPayload(method, response, {\n      // Only parse json if the schema expects it\n      parse: method.output.encoding === CONTENT_TYPE_JSON && {\n        strict: options?.strictResponseProcessing ?? true,\n      },\n    })\n\n    // Response is successful (2xx). Validate payload (data and encoding) against schema.\n    if (method.output.encoding == null) {\n      // Schema expects no payload\n      if (payload) {\n        throw new XrpcUpstreamError(\n          method,\n          response,\n          payload,\n          `Expected response with no body, got ${payload.encoding}`,\n        )\n      }\n    } else {\n      // Schema expects a payload\n      if (!payload || !method.output.matchesEncoding(payload.encoding)) {\n        throw new XrpcUpstreamError(\n          method,\n          response,\n          payload,\n          payload\n            ? `Expected ${method.output.encoding} response, got ${payload.encoding}`\n            : `Expected non-empty response with content-type ${method.output.encoding}`,\n        )\n      }\n\n      // Assert valid response body.\n      if (method.output.schema && options?.validateResponse !== false) {\n        const result = method.output.schema.safeParse(payload.body, {\n          strict: options?.strictResponseProcessing ?? true,\n        })\n\n        if (!result.success) {\n          throw new XrpcInvalidResponseError(\n            method,\n            response,\n            payload,\n            result.reason,\n          )\n        }\n\n        const parsedPayload = {\n          body: result.value,\n          encoding: payload.encoding,\n        } as XrpcResponsePayload<M>\n\n        return new XrpcResponse<M>(\n          method,\n          response.status,\n          response.headers,\n          parsedPayload,\n        )\n      }\n    }\n\n    return new XrpcResponse<M>(\n      method,\n      response.status,\n      response.headers,\n      payload as XrpcResponsePayload<M>,\n    )\n  }\n}\n\ntype ReadPayloadOptions = {\n  /**\n   * Whether to parse the response body as JSON and convert it to LexValue.\n   *\n   * @default false\n   */\n  parse?: false | LexParseOptions\n}\n\n/**\n * @note this function always consumes the response body\n */\nasync function readPayload(\n  method: Query | Procedure,\n  response: Response,\n  options?: ReadPayloadOptions,\n): Promise<undefined | XrpcUnknownResponsePayload> {\n  try {\n    // @TODO Should we limit the maximum response size here (this could also be\n    // done by the FetchHandler)?\n\n    const encoding = response.headers\n      .get('content-type')\n      ?.split(';')[0]\n      .trim()\n      .toLowerCase()\n\n    // Response content-type is undefined\n    if (!encoding) {\n      // If the body is empty, return undefined (= no payload)\n      const arrayBuffer = await response.arrayBuffer()\n      if (arrayBuffer.byteLength === 0) return undefined\n\n      // If we got data despite no content-type, treat it as binary\n      return {\n        encoding: CONTENT_TYPE_BINARY,\n        body: new Uint8Array(arrayBuffer),\n      }\n    }\n\n    if (!isEncodingString(encoding)) {\n      throw new TypeError(`Invalid content-type \"${encoding}\" in response`)\n    }\n\n    if (options?.parse && encoding === CONTENT_TYPE_JSON) {\n      // @NOTE It might be worth returning the raw bytes here (Uint8Array) and\n      // perform the lex parsing using cborg/json, allowing to do\n      // bytes->LexValue in one step instead of bytes->text->JSON->LexValue.\n      // This would require adding encode/decode utilities to lex-json (similar\n      // to @ipld/dag-json)\n      const text = await response.text()\n\n      // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as\n      // using a reviver function during JSON.parse should be faster than\n      // parsing to JSON then converting to Lex (?)\n\n      // @TODO verify statement above\n      return { encoding, body: lexParse(text, options.parse) }\n    }\n\n    const arrayBuffer = await response.arrayBuffer()\n    return { encoding, body: new Uint8Array(arrayBuffer) }\n  } catch (cause) {\n    const message = 'Unable to parse response payload'\n    const messageDetail = cause instanceof TypeError ? cause.message : undefined\n    throw new XrpcUpstreamError(\n      method,\n      response,\n      null,\n      messageDetail ? `${message}: ${messageDetail}` : message,\n      { cause },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/types.ts",
    "content": "import { DidString, LexValue, UnknownString } from '@atproto/lex-schema'\n\nexport type { DidString, LexValue, UnknownString }\n\n/**\n * Service identifier fragment for DID service endpoints.\n *\n * Common values include 'atproto_labeler' for labeling services,\n * or custom service identifiers.\n */\nexport type DidServiceIdentifier = 'atproto_labeler' | UnknownString\n\n/**\n * A full service proxy identifier combining a DID with a service fragment.\n *\n * Used to route requests through a specific service endpoint.\n *\n * @example\n * ```typescript\n * const service: Service = 'did:web:api.bsky.app#bsky_appview'\n * ```\n */\nexport type Service = `${DidString}#${DidServiceIdentifier}`\n\n/**\n * Valid input types for binary request bodies.\n *\n * These types can be used as the body for procedures that expect\n * non-JSON content (e.g., blob uploads, binary data).\n *\n * @example Uploading a blob\n * ```typescript\n * const imageData: BinaryBodyInit = new Uint8Array(buffer)\n * await client.uploadBlob(imageData, { encoding: 'image/png' })\n * ```\n *\n * @example Streaming upload\n * ```typescript\n * const stream: BinaryBodyInit = someReadableStream\n * await client.xrpc(uploadMethod, { body: stream })\n * ```\n *\n * @example File upload in browser\n * ```typescript\n * const fileInput = document.querySelector('input[type=\"file\"]') as HTMLInputElement\n * const file: BinaryBodyInit = fileInput.files[0]\n * await client.xrpc(uploadMethod, { body: file })\n * ```\n */\nexport type BinaryBodyInit =\n  | Uint8Array\n  | ArrayBuffer\n  | Blob\n  | ReadableStream<Uint8Array>\n  | AsyncIterable<Uint8Array>\n  | string\n\nexport type EncodingString = `${string}/${string}`\n\nexport function isEncodingString(\n  contentType: string,\n): contentType is EncodingString {\n  return contentType.includes('/')\n}\n\nexport type XrpcUnknownResponsePayload<\n  TBinary extends BinaryBodyInit = Uint8Array,\n> = {\n  encoding: EncodingString\n  body: LexValue | TBinary\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/util.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport {\n  buildXrpcRequestHeaders,\n  isAsyncIterable,\n  isBlobLike,\n  toReadableStream,\n  toReadableStreamPonyfill,\n} from './util.js'\n\n// ============================================================================\n// isBlobLike\n// ============================================================================\n\ndescribe(isBlobLike, () => {\n  it('returns true for native Blob', () => {\n    expect(isBlobLike(new Blob(['hello']))).toBe(true)\n  })\n\n  it('returns true for native File', () => {\n    expect(isBlobLike(new File(['hello'], 'test.txt'))).toBe(true)\n  })\n\n  it('returns false for null', () => {\n    expect(isBlobLike(null)).toBe(false)\n  })\n\n  it('returns false for undefined', () => {\n    expect(isBlobLike(undefined)).toBe(false)\n  })\n\n  it('returns false for primitives', () => {\n    expect(isBlobLike(42)).toBe(false)\n    expect(isBlobLike('string')).toBe(false)\n    expect(isBlobLike(true)).toBe(false)\n  })\n\n  it('returns false for plain objects', () => {\n    expect(isBlobLike({})).toBe(false)\n    expect(isBlobLike({ stream: () => {} })).toBe(false)\n  })\n\n  it('returns true for Blob-like objects with [Symbol.toStringTag] = \"Blob\"', () => {\n    const blobLike = {\n      [Symbol.toStringTag]: 'Blob',\n      stream: () => new ReadableStream(),\n    }\n    expect(isBlobLike(blobLike)).toBe(true)\n  })\n\n  it('returns true for File-like objects with [Symbol.toStringTag] = \"File\"', () => {\n    const fileLike = {\n      [Symbol.toStringTag]: 'File',\n      stream: () => new ReadableStream(),\n    }\n    expect(isBlobLike(fileLike)).toBe(true)\n  })\n\n  it('returns false for objects with Blob tag but no stream method', () => {\n    const notBlob = {\n      [Symbol.toStringTag]: 'Blob',\n    }\n    expect(isBlobLike(notBlob)).toBe(false)\n  })\n\n  it('returns false for objects with Blob tag but non-function stream', () => {\n    const notBlob = {\n      [Symbol.toStringTag]: 'Blob',\n      stream: 'not a function',\n    }\n    expect(isBlobLike(notBlob)).toBe(false)\n  })\n})\n\n// ============================================================================\n// isAsyncIterable\n// ============================================================================\n\ndescribe(isAsyncIterable, () => {\n  it('returns true for async generators', () => {\n    async function* gen() {\n      yield 1\n    }\n    expect(isAsyncIterable(gen())).toBe(true)\n  })\n\n  it('returns true for objects with Symbol.asyncIterator', () => {\n    const iterable = {\n      [Symbol.asyncIterator]() {\n        return { next: async () => ({ done: true, value: undefined }) }\n      },\n    }\n    expect(isAsyncIterable(iterable)).toBe(true)\n  })\n\n  it('returns false for null', () => {\n    expect(isAsyncIterable(null)).toBe(false)\n  })\n\n  it('returns false for undefined', () => {\n    expect(isAsyncIterable(undefined)).toBe(false)\n  })\n\n  it('returns false for plain objects', () => {\n    expect(isAsyncIterable({})).toBe(false)\n  })\n\n  it('returns false for sync iterables', () => {\n    expect(isAsyncIterable([1, 2, 3])).toBe(false)\n    expect(isAsyncIterable('string')).toBe(false)\n  })\n})\n\n// ============================================================================\n// buildXrpcRequestHeaders\n// ============================================================================\n\ndescribe(buildXrpcRequestHeaders, () => {\n  it('returns empty headers when no options are set', () => {\n    const headers = buildXrpcRequestHeaders({})\n    expect([...headers.entries()]).toEqual([])\n  })\n\n  it('sets atproto-proxy header from service option', () => {\n    const headers = buildXrpcRequestHeaders({\n      service: 'did:plc:1234#atproto_labeler',\n    })\n    expect(headers.get('atproto-proxy')).toBe('did:plc:1234#atproto_labeler')\n  })\n\n  it('does not override existing atproto-proxy header', () => {\n    const headers = buildXrpcRequestHeaders({\n      headers: { 'atproto-proxy': 'did:plc:existing#service' },\n      service: 'did:plc:new#service',\n    })\n    expect(headers.get('atproto-proxy')).toBe('did:plc:existing#service')\n  })\n\n  it('sets atproto-accept-labelers from labelers option', () => {\n    const headers = buildXrpcRequestHeaders({\n      labelers: ['did:plc:labeler1', 'did:plc:labeler2'] as const,\n    })\n    expect(headers.get('atproto-accept-labelers')).toBe(\n      'did:plc:labeler1, did:plc:labeler2',\n    )\n  })\n\n  it('appends to existing atproto-accept-labelers header', () => {\n    const headers = buildXrpcRequestHeaders({\n      headers: { 'atproto-accept-labelers': 'did:plc:existing' },\n      labelers: ['did:plc:new'] as const,\n    })\n    expect(headers.get('atproto-accept-labelers')).toBe(\n      'did:plc:new, did:plc:existing',\n    )\n  })\n\n  it('passes through base headers', () => {\n    const headers = buildXrpcRequestHeaders({\n      headers: { Authorization: 'Bearer token123' },\n    })\n    expect(headers.get('Authorization')).toBe('Bearer token123')\n  })\n\n  it('accepts Headers instance as base headers', () => {\n    const base = new Headers({ 'X-Custom': 'value' })\n    const headers = buildXrpcRequestHeaders({ headers: base })\n    expect(headers.get('X-Custom')).toBe('value')\n  })\n\n  it('sets empty header for empty labelers iterable', () => {\n    const headers = buildXrpcRequestHeaders({ labelers: [] })\n    // An empty array still sets the header (to empty string), distinguishing\n    // \"no labelers requested\" from \"labelers option not provided\"\n    expect(headers.has('atproto-accept-labelers')).toBe(true)\n    expect(headers.get('atproto-accept-labelers')).toBe('')\n  })\n})\n\n// ============================================================================\n// toReadableStream\n// ============================================================================\n\ndescribe(toReadableStream, () => {\n  it('converts async iterable to ReadableStream', async () => {\n    async function* gen() {\n      yield new Uint8Array([1, 2])\n      yield new Uint8Array([3, 4])\n    }\n\n    const stream = toReadableStream(gen())\n    const reader = stream.getReader()\n\n    const chunk1 = await reader.read()\n    expect(chunk1.done).toBe(false)\n    expect(chunk1.value).toEqual(new Uint8Array([1, 2]))\n\n    const chunk2 = await reader.read()\n    expect(chunk2.done).toBe(false)\n    expect(chunk2.value).toEqual(new Uint8Array([3, 4]))\n\n    const end = await reader.read()\n    expect(end.done).toBe(true)\n  })\n\n  it('handles empty async iterable', async () => {\n    async function* gen() {\n      // yields nothing\n    }\n\n    const stream = toReadableStream(gen())\n    const reader = stream.getReader()\n\n    const result = await reader.read()\n    expect(result.done).toBe(true)\n  })\n\n  it('can be consumed with Response API', async () => {\n    async function* gen() {\n      yield new TextEncoder().encode('hello ')\n      yield new TextEncoder().encode('world')\n    }\n\n    const stream = toReadableStream(gen())\n    const response = new Response(stream)\n    const text = await response.text()\n    expect(text).toBe('hello world')\n  })\n\n  it('propagates errors from the async iterable', async () => {\n    async function* gen() {\n      yield new Uint8Array([1])\n      throw new Error('stream error')\n    }\n\n    const stream = toReadableStream(gen())\n    const reader = stream.getReader()\n\n    // First chunk succeeds\n    await reader.read()\n\n    // Second read should reject\n    await expect(reader.read()).rejects.toThrow('stream error')\n  })\n})\n\n// ============================================================================\n// toReadableStreamPonyfill\n// ============================================================================\n\ndescribe(toReadableStreamPonyfill, () => {\n  it('converts async iterable to ReadableStream', async () => {\n    async function* gen() {\n      yield new Uint8Array([1, 2])\n      yield new Uint8Array([3, 4])\n    }\n\n    const stream = toReadableStreamPonyfill(gen())\n    const reader = stream.getReader()\n\n    const chunk1 = await reader.read()\n    expect(chunk1.done).toBe(false)\n    expect(chunk1.value).toEqual(new Uint8Array([1, 2]))\n\n    const chunk2 = await reader.read()\n    expect(chunk2.done).toBe(false)\n    expect(chunk2.value).toEqual(new Uint8Array([3, 4]))\n\n    const end = await reader.read()\n    expect(end.done).toBe(true)\n  })\n\n  it('handles empty async iterable', async () => {\n    async function* gen() {\n      // yields nothing\n    }\n\n    const stream = toReadableStreamPonyfill(gen())\n    const reader = stream.getReader()\n\n    const result = await reader.read()\n    expect(result.done).toBe(true)\n  })\n\n  it('can be consumed with Response API', async () => {\n    async function* gen() {\n      yield new TextEncoder().encode('hello ')\n      yield new TextEncoder().encode('world')\n    }\n\n    const stream = toReadableStreamPonyfill(gen())\n    const response = new Response(stream)\n    const text = await response.text()\n    expect(text).toBe('hello world')\n  })\n\n  it('propagates errors from the async iterable', async () => {\n    async function* gen() {\n      yield new Uint8Array([1])\n      throw new Error('stream error')\n    }\n\n    const stream = toReadableStreamPonyfill(gen())\n    const reader = stream.getReader()\n\n    await reader.read()\n    await expect(reader.read()).rejects.toThrow('stream error')\n  })\n\n  it('calls iterator.return() on cancel', async () => {\n    let returned = false\n    const iterable: AsyncIterable<Uint8Array> = {\n      [Symbol.asyncIterator]() {\n        return {\n          async next() {\n            return { done: false, value: new Uint8Array([1]) }\n          },\n          async return() {\n            returned = true\n            return { done: true, value: undefined }\n          },\n        }\n      },\n    }\n\n    const stream = toReadableStreamPonyfill(iterable)\n    const reader = stream.getReader()\n\n    await reader.read()\n    await reader.cancel()\n\n    expect(returned).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-client/src/util.ts",
    "content": "import type { DidString, Service } from './types.js'\n\nexport function applyDefaults<\n  TDefaults extends Record<string, unknown>,\n  TOptions extends {\n    [K in keyof TDefaults]?: TDefaults[K]\n  },\n>(options: TOptions, defaults: TDefaults): TOptions & TDefaults {\n  const combined: Partial<TDefaults> = { ...options }\n\n  // @NOTE We make sure that options with an explicit `undefined` value get the\n  // default, since spreading doesn't override with `undefined`.\n  for (const key of Object.keys(defaults) as (keyof typeof defaults)[]) {\n    if (options[key] === undefined) {\n      combined[key] = defaults[key]\n    }\n  }\n\n  return combined as TOptions & TDefaults\n}\n\n/**\n * Type guard to check if a value is {@link Blob}-like.\n *\n * Handles both native Blobs and polyfilled Blob implementations\n * (e.g., fetch-blob from node-fetch).\n *\n * @param value - The value to check\n * @returns `true` if the value is a Blob or Blob-like object\n */\nexport function isBlobLike(value: unknown): value is Blob {\n  if (value == null) return false\n  if (typeof value !== 'object') return false\n  if (typeof Blob === 'function' && value instanceof Blob) return true\n\n  // Support for Blobs provided by libraries that don't use the native Blob\n  // (e.g. fetch-blob from node-fetch).\n  // https://github.com/node-fetch/fetch-blob/blob/a1a182e5978811407bef4ea1632b517567dda01f/index.js#L233-L244\n\n  const tag = (value as any)[Symbol.toStringTag]\n  if (tag === 'Blob' || tag === 'File') {\n    return 'stream' in value && typeof value.stream === 'function'\n  }\n\n  return false\n}\n\nexport function isAsyncIterable<T>(\n  value: T,\n): value is unknown extends T\n  ? T & AsyncIterable<unknown>\n  : Extract<T, AsyncIterable<any>> {\n  return (\n    value != null && typeof (value as any)[Symbol.asyncIterator] === 'function'\n  )\n}\n\nexport type XrpcRequestHeadersOptions = {\n  /** Additional HTTP headers to include in the request. */\n  headers?: HeadersInit\n\n  /** Labeler DIDs to request labels from for content moderation. */\n  labelers?: Iterable<DidString>\n\n  /** Service proxy identifier for routing requests through a specific service. */\n  service?: Service\n}\n\n/**\n * Builds HTTP headers for AT Protocol requests.\n *\n * Adds the following headers when applicable:\n * - `atproto-proxy`: Service routing header (if service is specified)\n * - `atproto-accept-labelers`: Comma-separated list of labeler DIDs\n *\n * @see {@link XrpcRequestHeadersOptions}\n * @returns A new Headers object with AT Protocol headers added\n */\nexport function buildXrpcRequestHeaders(\n  options: XrpcRequestHeadersOptions,\n): Headers {\n  const headers = new Headers(options?.headers)\n\n  if (options.service && !headers.has('atproto-proxy')) {\n    headers.set('atproto-proxy', options.service)\n  }\n\n  if (options.labelers) {\n    headers.set(\n      'atproto-accept-labelers',\n      [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]\n        .filter(Boolean)\n        .join(', '),\n    )\n  }\n\n  return headers\n}\n\nexport function toReadableStream(\n  data: AsyncIterable<Uint8Array>,\n): ReadableStream<Uint8Array> {\n  // Use the native ReadableStream.from() if available.\n\n  /* v8 ignore next -- @preserve */\n  if ('from' in ReadableStream && typeof ReadableStream.from === 'function') {\n    return ReadableStream.from(data)\n  }\n\n  /* v8 ignore next -- @preserve */\n  return toReadableStreamPonyfill(data)\n}\n\nexport function toReadableStreamPonyfill(\n  data: AsyncIterable<Uint8Array>,\n): ReadableStream<Uint8Array> {\n  let iterator: AsyncIterator<Uint8Array> | undefined\n  return new ReadableStream({\n    async pull(controller) {\n      try {\n        iterator ??= data[Symbol.asyncIterator]()\n        const result = await iterator!.next()\n        if (result.done) controller.close()\n        else controller.enqueue(result.value)\n      } catch (err) {\n        controller.error(err)\n        iterator = undefined\n      }\n    },\n    async cancel() {\n      await iterator?.return?.()\n      iterator = undefined\n    },\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/www-authenticate.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { parseWWWAuthenticateHeader } from './www-authenticate.js'\n\ndescribe(parseWWWAuthenticateHeader, () => {\n  describe('auth-params', () => {\n    it('parses single unquoted auth param', () => {\n      expect(parseWWWAuthenticateHeader('Bearer realm=example.com')).toEqual({\n        Bearer: { realm: 'example.com' },\n      })\n    })\n\n    it('parses single quoted auth param', () => {\n      expect(parseWWWAuthenticateHeader('Bearer realm=\"example.com\"')).toEqual({\n        Bearer: { realm: 'example.com' },\n      })\n    })\n\n    it('parses quoted values with spaces', () => {\n      expect(parseWWWAuthenticateHeader('Bearer realm=\"my realm\"')).toEqual({\n        Bearer: { realm: 'my realm' },\n      })\n    })\n\n    it('parses quoted values with escaped double quotes', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer realm=\"example\\\\\"quoted\\\\\"\"'),\n      ).toEqual({\n        Bearer: { realm: 'example\"quoted\"' },\n      })\n    })\n\n    it('parses quoted values with escaped backslash', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer realm=\"path\\\\\\\\to\\\\\\\\file\"'),\n      ).toEqual({\n        Bearer: { realm: 'path\\\\to\\\\file' },\n      })\n    })\n\n    it('parses param names with hyphens', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer error-uri=\"https://example.com\"'),\n      ).toEqual({\n        Bearer: { 'error-uri': 'https://example.com' },\n      })\n    })\n\n    it('parses param names with underscores', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer error_description=\"test\"'),\n      ).toEqual({\n        Bearer: { error_description: 'test' },\n      })\n    })\n\n    it('parses param with numeric value', () => {\n      expect(parseWWWAuthenticateHeader('Bearer max-age=3600')).toEqual({\n        Bearer: { 'max-age': '3600' },\n      })\n    })\n\n    it('parses empty quoted value', () => {\n      expect(parseWWWAuthenticateHeader('Bearer realm=\"\"')).toEqual({\n        Bearer: { realm: '' },\n      })\n    })\n\n    it('parses Basic auth challenge', () => {\n      expect(\n        parseWWWAuthenticateHeader('Basic realm=\"Access to staging site\"'),\n      ).toEqual({\n        Basic: { realm: 'Access to staging site' },\n      })\n    })\n\n    it('parses Bearer with realm', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer realm=\"https://auth.example.com\"'),\n      ).toEqual({\n        Bearer: { realm: 'https://auth.example.com' },\n      })\n    })\n\n    it('parses DPoP with algs param', () => {\n      expect(parseWWWAuthenticateHeader('DPoP algs=\"ES256 RS256\"')).toEqual({\n        DPoP: { algs: 'ES256 RS256' },\n      })\n    })\n\n    it('parses Digest auth challenge', () => {\n      const result = parseWWWAuthenticateHeader(\n        'Digest realm=\"digest-realm\", nonce=\"abc123\"',\n      )\n      expect(result).toEqual({\n        Digest: { realm: 'digest-realm', nonce: 'abc123' },\n      })\n    })\n\n    it('handle empty unquoted params', () => {\n      const result = parseWWWAuthenticateHeader('Bearer realm=')\n      expect(result).toEqual({ Bearer: { realm: '' } })\n    })\n\n    it('handle empty params', () => {\n      const result = parseWWWAuthenticateHeader('Bearer realm=\"\"')\n      expect(result).toEqual({ Bearer: { realm: '' } })\n    })\n\n    it('treats scheme-only header as scheme with itself as token68', () => {\n      const result = parseWWWAuthenticateHeader('Basic')\n      expect(result).toEqual({ Basic: {} })\n    })\n\n    it('parses multiple challenges with commas and escaped quotes', () => {\n      const result = parseWWWAuthenticateHeader(\n        `Newauth realm=\"apps\", type=1,\\n\\t    title=\"Login to \\\\\"apps\\\\\"\", Basic realm=\"simple\"`,\n      )\n      expect(result).toEqual({\n        Newauth: {\n          realm: 'apps',\n          type: '1',\n          title: 'Login to \"apps\"',\n        },\n        Basic: { realm: 'simple' },\n      })\n    })\n\n    it('parses first challenge before comma', () => {\n      const result = parseWWWAuthenticateHeader(\n        'Basic realm=\"foo\", Bearer realm=\"bar\"',\n      )\n      expect(result).toEqual({\n        Basic: { realm: 'foo' },\n        Bearer: { realm: 'bar' },\n      })\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles empty string', () => {\n      expect(parseWWWAuthenticateHeader('')).toEqual({})\n    })\n\n    it('handles whitespace-only string', () => {\n      expect(parseWWWAuthenticateHeader('   ')).toEqual({})\n    })\n\n    it('trims whitespace from header', () => {\n      expect(parseWWWAuthenticateHeader('  Bearer realm=\"test\"  ')).toEqual({\n        Bearer: { realm: 'test' },\n      })\n    })\n\n    it('handles commas as quoted param value', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer realm=\"example, with, commas\"'),\n      ).toEqual({ Bearer: { realm: 'example, with, commas' } })\n    })\n\n    it('handles multiple challenges with varying whitespace', () => {\n      expect(\n        parseWWWAuthenticateHeader(\n          '  Bearer realm=\"test\"  ,    Basic    rr=               ',\n        ),\n      ).toEqual({\n        Bearer: { realm: 'test' },\n        Basic: { rr: '' },\n      })\n    })\n  })\n\n  describe('invalid challenges', () => {\n    it('parses single challenge with no comma correctly', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer realm=\"oauth\" error=\"invalid\"'),\n      ).toEqual(undefined)\n    })\n\n    it('ignores invalid challenges', () => {\n      expect(parseWWWAuthenticateHeader('Bearer realm=\"unclosed')).toEqual(\n        undefined,\n      )\n    })\n\n    it('handles random text without equals sign as token68', () => {\n      expect(parseWWWAuthenticateHeader('Bearer sometoken')).toEqual({\n        Bearer: 'sometoken',\n      })\n    })\n\n    it('ignores trailing whitespace after scheme', () => {\n      expect(parseWWWAuthenticateHeader('Bearer   ')).toEqual({ Bearer: {} })\n    })\n\n    it('handles duplicate params (last wins)', () => {\n      expect(\n        parseWWWAuthenticateHeader('Bearer realm=\"first\", realm=\"second\"'),\n      ).toEqual({ Bearer: { realm: 'second' } })\n    })\n\n    it('extracts valid param after invalid characters', () => {\n      expect(parseWWWAuthenticateHeader('Bearer realm@foo=\"bar\"')).toEqual({\n        Bearer: { 'realm@foo': 'bar' },\n      })\n    })\n\n    it('ignores param with empty name', () => {\n      expect(parseWWWAuthenticateHeader('Bearer =\"value\"')).toEqual(undefined)\n    })\n\n    it('handles completely malformed input gracefully', () => {\n      expect(parseWWWAuthenticateHeader('!@#$%')).toEqual({ '!@#$%': {} })\n    })\n\n    it('handles duplicate schemes as invalid', () => {\n      expect(\n        parseWWWAuthenticateHeader('Basic realm=\"first\", Basic realm=\"second\"'),\n      ).toEqual(undefined)\n    })\n\n    it('handles params without scheme as invalid', () => {\n      expect(parseWWWAuthenticateHeader('Bearer, realm=\"foo\"')).toEqual(\n        undefined,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-client/src/www-authenticate.ts",
    "content": "type WWWAuthenticateParams = { [authParam in string]: string }\n\n/**\n * Parsed representation of a WWW-Authenticate HTTP header.\n *\n * Maps authentication scheme names to either:\n * - A token68 string (compact authentication data)\n * - A params object with key-value pairs\n *\n * @example Bearer with realm\n * ```typescript\n * // WWW-Authenticate: Bearer realm=\"example\"\n * const parsed: WWWAuthenticate = {\n *   Bearer: { realm: 'example' }\n * }\n * ```\n *\n * @example DPoP with error\n * ```typescript\n * // WWW-Authenticate: DPoP error=\"use_dpop_nonce\", error_description=\"...\"\n * const parsed: WWWAuthenticate = {\n *   DPoP: { error: 'use_dpop_nonce', error_description: '...' }\n * }\n * ```\n */\nexport type WWWAuthenticate = {\n  [authScheme in string]:\n    | string // token68\n    | WWWAuthenticateParams\n}\n\n/**\n * Returns `undefined` if the header is malformed.\n */\nexport function parseWWWAuthenticateHeader(\n  header?: unknown,\n): undefined | WWWAuthenticate {\n  if (typeof header !== 'string') return undefined\n\n  const wwwAuthenticate: WWWAuthenticate = {}\n\n  // Split over commas, not within quoted strings\n  const trimmedHeader = header.trim()\n  if (!trimmedHeader) return wwwAuthenticate\n\n  const parts = trimmedHeader.split(/,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/)\n\n  let currentParams: WWWAuthenticateParams | null = null\n\n  for (let part of parts) {\n    // Check if the part starts with an auth scheme\n    const schemeMatch = part.trim().match(/^([^\"=\\s]+)(\\s+.*)?$/)\n    if (schemeMatch) {\n      const scheme = schemeMatch[1]\n\n      // Duplicate scheme\n      if (Object.hasOwn(wwwAuthenticate, scheme)) return undefined\n\n      const rest = schemeMatch[2]?.trim()\n      if (!rest) {\n        // Scheme only (no params or token68)\n        currentParams = null\n        wwwAuthenticate[scheme] = Object.create(null)\n        continue\n      }\n\n      if (!rest.includes('=')) {\n        // Scheme with token68\n        currentParams = null\n        wwwAuthenticate[scheme] = rest\n        continue\n      }\n\n      // Scheme with params\n\n      currentParams = Object.create(null) as WWWAuthenticateParams\n      wwwAuthenticate[scheme] = currentParams\n\n      // Fall through to parse params\n      part = rest\n    }\n\n    // Invalid header\n    if (!currentParams) return undefined\n\n    const param = part.match(\n      /^\\s*([^\"\\s=]+)=(?:(\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\")|([^\\s,\"]*))\\s*$/,\n    )\n\n    // invalid param\n    if (!param) return undefined\n\n    const paramName = param[1]\n    const paramValue =\n      param[3] ?? param[2]!.slice(1, -1).replaceAll(/\\\\(.)/g, '$1')\n\n    currentParams[paramName] = paramValue\n  }\n\n  return wwwAuthenticate\n}\n"
  },
  {
    "path": "packages/lex/lex-client/src/xrpc.test.ts",
    "content": "import { assert, describe, expect, expectTypeOf, it, vi } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport { lexToJson } from '@atproto/lex-json'\nimport { l } from '@atproto/lex-schema'\nimport { FetchHandler } from './agent.js'\nimport {\n  XrpcAuthenticationError,\n  XrpcFetchError,\n  XrpcInternalError,\n  XrpcInvalidResponseError,\n  XrpcResponseError,\n  XrpcUpstreamError,\n} from './errors.js'\nimport { XrpcResponse } from './response.js'\nimport { xrpc, xrpcSafe } from './xrpc.js'\n\n// Fixtures\n\nconst rawCid = parseCid(\n  'bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4',\n  { flavor: 'raw' },\n)\n\nconst cborCid = parseCid(\n  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n  { flavor: 'cbor' },\n)\n\nconst testQuery = l.query(\n  'io.example.testQuery',\n  l.params({ limit: l.optional(l.integer()) }),\n  l.jsonPayload({ value: l.string() }),\n  ['TestError'],\n)\n\nconst testProcedure = l.procedure(\n  'io.example.testProcedure',\n  l.params(),\n  l.jsonPayload({ text: l.string() }),\n  l.jsonPayload({ id: l.string() }),\n  ['ProcedureError'],\n)\n\nconst testBinaryQuery = l.query(\n  'io.example.testBinaryQuery',\n  l.params(),\n  l.payload('application/octet-stream', undefined),\n)\n\nconst testBinaryProcedure = l.procedure(\n  'io.example.testBinaryProcedure',\n  l.params(),\n  l.payload('image/*', undefined),\n  l.payload('application/octet-stream', undefined),\n)\n\nconst testNoOutputQuery = l.query(\n  'io.example.testNoOutputQuery',\n  l.params(),\n  l.payload(),\n)\n\nconst testQueryWithDefaults = l.query(\n  'io.example.testQueryWithDefaults',\n  l.params({ foo: l.optional(l.withDefault(l.string(), 'foo-default')) }),\n  l.jsonPayload({\n    foo: l.string(),\n    bar: l.optional(l.withDefault(l.string(), 'bar-default')),\n  }),\n)\n\nconst testQueryGetBlobRef = l.query(\n  'io.example.testQueryGetBlobRef',\n  l.params(),\n  l.jsonPayload({\n    blobRef: l.blob({\n      allowLegacy: false,\n      accept: ['image/png'],\n      maxSize: 10,\n    }),\n  }),\n)\n\ndescribe(xrpc, () => {\n  describe('success paths', () => {\n    it('returns parsed JSON body for a query', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'hello' })\n      })\n\n      const response = await xrpc(fetchHandler, testQuery, {\n        params: { limit: 10 },\n      })\n\n      expect(response).toBeInstanceOf(XrpcResponse)\n      expect(response.success).toBe(true)\n      expect(response.status).toBe(200)\n      expect(response.body).toEqual({ value: 'hello' })\n      expect(response.encoding).toBe('application/json')\n      expect(response.isParsed).toBe(true)\n      expect(response.value).toBe(response)\n    })\n\n    it('returns parsed JSON body for a procedure', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'abc123' })\n      })\n\n      const response = await xrpc(fetchHandler, testProcedure, {\n        body: { text: 'hello world' },\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.status).toBe(200)\n      expect(response.body).toEqual({ id: 'abc123' })\n      expect(response.encoding).toBe('application/json')\n    })\n\n    it('returns binary body for a binary query', async () => {\n      const bytes = new Uint8Array([1, 2, 3, 4])\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(bytes, {\n          headers: { 'content-type': 'application/octet-stream' },\n        })\n      })\n\n      const response = await xrpc(fetchHandler, testBinaryQuery)\n\n      expect(response.success).toBe(true)\n      expect(response.body).toBeInstanceOf(Uint8Array)\n      expect(response.body).toEqual(bytes)\n      expect(response.encoding).toBe('application/octet-stream')\n      expect(response.isParsed).toBe(false)\n    })\n\n    it('returns binary body for a binary procedure', async () => {\n      const bytes = new Uint8Array([10, 20, 30])\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(bytes, {\n          headers: { 'content-type': 'application/octet-stream' },\n        })\n      })\n\n      const response = await xrpc(fetchHandler, testBinaryProcedure, {\n        body: new Uint8Array([99]),\n        encoding: 'image/png',\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toBeInstanceOf(Uint8Array)\n      expect(response.body).toEqual(bytes)\n    })\n\n    it('returns no body for a no-output query', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(null, { status: 200 })\n      })\n\n      const response = await xrpc(fetchHandler, testNoOutputQuery)\n\n      expect(response.success).toBe(true)\n      expect(response.status).toBe(200)\n      expect(response.body).toBeUndefined()\n      expect(response.encoding).toBeUndefined()\n    })\n\n    it('passes query params as URL search params', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'ok' })\n      })\n\n      await xrpc(fetchHandler, testQuery, { params: { limit: 25 } })\n\n      expect(fetchHandler).toHaveBeenCalledOnce()\n      const [path] = fetchHandler.mock.calls[0]\n      expect(path).toContain('/xrpc/io.example.testQuery')\n      expect(path).toContain('limit=25')\n    })\n\n    it('sends POST with JSON body for procedures', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'new-id' })\n      })\n\n      await xrpc(fetchHandler, testProcedure, {\n        body: { text: 'test content' },\n      })\n\n      expect(fetchHandler).toHaveBeenCalledOnce()\n      const [, init] = fetchHandler.mock.calls[0]\n      expect(init.method).toBe('POST')\n      expect(new Headers(init.headers).get('content-type')).toBe(\n        'application/json',\n      )\n    })\n\n    it('forwards custom headers', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'ok' })\n      })\n\n      await xrpc(fetchHandler, testQuery, {\n        params: { limit: 1 },\n        headers: { authorization: 'Bearer token123' },\n      })\n\n      expect(fetchHandler).toHaveBeenCalledOnce()\n      const [, init] = fetchHandler.mock.calls[0]\n      expect(new Headers(init.headers).get('authorization')).toBe(\n        'Bearer token123',\n      )\n    })\n\n    it('accepts optional params as omitted', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'ok' })\n      })\n\n      const response = await xrpc(fetchHandler, testQuery)\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({ value: 'ok' })\n    })\n  })\n\n  describe('error handling', () => {\n    describe('fetch errors', () => {\n      it('throws XrpcFetchError when fetchHandler throws', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          throw new TypeError('fetch failed')\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcFetchError)\n          expect(err).toBeInstanceOf(XrpcInternalError)\n          expect(err.cause).toBeInstanceOf(TypeError)\n          expect(err.message).toContain('fetch failed')\n          return true\n        })\n      })\n\n      it('throws XrpcFetchError when fetchHandler rejects', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          throw new Error('network timeout')\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcFetchError)\n          expect(err.message).toContain('network timeout')\n          expect(err.shouldRetry()).toBe(true)\n          return true\n        })\n      })\n    })\n\n    describe('response errors', () => {\n      it('throws XrpcResponseError for 400 with valid error payload', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return Response.json(\n            { error: 'TestError', message: 'bad request' },\n            { status: 400 },\n          )\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcResponseError)\n          expect(err.status).toBe(400)\n          expect(err.body).toEqual({\n            error: 'TestError',\n            message: 'bad request',\n          })\n          return true\n        })\n      })\n\n      it('throws XrpcAuthenticationError for 401', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return Response.json(\n            { error: 'AuthenticationRequired', message: 'Token expired' },\n            { status: 401 },\n          )\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcAuthenticationError)\n          expect(err.status).toBe(401)\n          expect(err.message).toBe('Token expired')\n          return true\n        })\n      })\n\n      it('throws XrpcUpstreamError for non-XRPC error response', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response('Not Found', {\n            status: 404,\n            headers: { 'content-type': 'text/plain' },\n          })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toBe('Invalid response payload')\n          return true\n        })\n      })\n\n      it('throws XrpcUpstreamError for 500 without valid error payload', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response('Internal Server Error', {\n            status: 500,\n            headers: { 'content-type': 'text/html' },\n          })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toBe('Upstream server encountered an error')\n          return true\n        })\n      })\n\n      it('Reflects upstream 5xx errors with valid XRPC payload', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return Response.json(\n            { error: 'ServerError', message: 'Something went wrong' },\n            { status: 502 },\n          )\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcResponseError)\n          expect(err.status).toBe(502)\n          expect(err.body).toEqual({\n            error: 'ServerError',\n            message: 'Something went wrong',\n          })\n          return true\n        })\n      })\n    })\n\n    describe('invalid response errors', () => {\n      it('throws XrpcInvalidResponseError when response body fails validation', async () => {\n        // Schema expects { value: string } but we return { value: 123 }\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return Response.json({ value: 123 })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcInvalidResponseError)\n          expect(err).toBeInstanceOf(XrpcUpstreamError)\n          expect(err.cause).toBeInstanceOf(Error)\n          return true\n        })\n      })\n\n      it('throws XrpcUpstreamError when response has wrong content-type', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response('binary data', {\n            status: 200,\n            headers: { 'content-type': 'text/plain' },\n          })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toContain('application/json')\n          return true\n        })\n      })\n    })\n\n    describe('content-type header errors', () => {\n      it('throws XrpcInternalError when content-type header is set', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return Response.json({ value: 'ok' })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, {\n            params: { limit: 10 },\n            headers: { 'content-type': 'application/json' },\n          }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcInternalError)\n          expect(err.cause).toBeInstanceOf(TypeError)\n          return true\n        })\n      })\n    })\n\n    describe('response payload parsing', () => {\n      it('throws XrpcUpstreamError when error response body cannot be parsed', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response('not valid json', {\n            status: 400,\n            headers: { 'content-type': 'application/json' },\n          })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toMatch('Unable to parse response payload')\n          assert(err.cause instanceof Error)\n          expect(err.cause.message).toContain('Unexpected token')\n          return true\n        })\n      })\n\n      it('throws XrpcUpstreamError when success response body cannot be parsed', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response('not valid json', {\n            status: 200,\n            headers: { 'content-type': 'application/json' },\n          })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toMatch('Unable to parse response payload')\n          assert(err.cause instanceof Error)\n          expect(err.cause.message).toContain('Unexpected token')\n          return true\n        })\n      })\n\n      it('throws XrpcUpstreamError when schema expects no payload but got one', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return Response.json({ unexpected: 'data' })\n        })\n\n        await expect(xrpc(fetchHandler, testNoOutputQuery)).rejects.toSatisfy(\n          (err) => {\n            assert(err instanceof XrpcUpstreamError)\n            expect(err.message).toContain('no body')\n            return true\n          },\n        )\n      })\n\n      it('throws XrpcUpstreamError when schema expects payload but response is empty', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response(null, { status: 200 })\n        })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toContain('non-empty response')\n          return true\n        })\n      })\n    })\n\n    describe('content-type handling', () => {\n      it('parses content-type with charset parameter', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          return new Response(JSON.stringify({ value: 'hello' }), {\n            status: 200,\n            headers: { 'content-type': 'application/json; charset=utf-8' },\n          })\n        })\n\n        const response = await xrpc(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        expect(response.success).toBe(true)\n        expect(response.body).toEqual({ value: 'hello' })\n      })\n\n      it('handles response with no content-type and empty body as no payload', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          new Response(new ArrayBuffer(0), { status: 200 })\n\n        const response = await xrpc(fetchHandler, testNoOutputQuery)\n\n        expect(response.success).toBe(true)\n        expect(response.body).toBeUndefined()\n      })\n\n      it('treats response with no content-type but non-empty body as binary', async () => {\n        const bytes = new Uint8Array([1, 2, 3])\n        const fetchHandler: FetchHandler = async () =>\n          new Response(bytes, { status: 200 })\n\n        const response = await xrpc(fetchHandler, testBinaryQuery)\n\n        expect(response.success).toBe(true)\n        expect(response.body).toBeInstanceOf(Uint8Array)\n        expect(response.body).toEqual(bytes)\n      })\n    })\n\n    describe('non-2xx non-4xx/5xx responses', () => {\n      it('throws XrpcUpstreamError for 3xx status codes', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          Response.json({ value: 'redirect' }, { status: 302 })\n\n        await expect(\n          xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n        ).rejects.toSatisfy((err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toBe('Invalid response status code')\n          return true\n        })\n      })\n    })\n  })\n\n  describe('validateRequest', () => {\n    it('rejects invalid query params when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'ok' })\n      })\n\n      await expect(\n        xrpc(fetchHandler, testQuery, {\n          // @ts-expect-error intentionally passing invalid params\n          params: { limit: 'not-a-number' },\n          validateRequest: true,\n        }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcInternalError)\n        expect(err).not.toBeInstanceOf(XrpcFetchError)\n        return true\n      })\n    })\n\n    it('rejects invalid procedure body when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'abc' })\n      })\n\n      await expect(\n        xrpc(fetchHandler, testProcedure, {\n          // @ts-expect-error intentionally passing invalid body\n          body: { text: 123 },\n          validateRequest: true,\n        }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcInternalError)\n        expect(err).not.toBeInstanceOf(XrpcFetchError)\n        return true\n      })\n    })\n\n    it('skips body validation by default (invalid body sent as-is)', async () => {\n      const fetchHandler: FetchHandler = async () => Response.json({ id: 'ok' })\n\n      // Invalid body ({ text: 123 }) is not validated client-side\n      const response = await xrpc(fetchHandler, testProcedure, {\n        // @ts-expect-error intentionally passing invalid body\n        body: { text: 123 },\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({ id: 'ok' })\n    })\n\n    it('succeeds with valid body when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'valid' })\n      })\n\n      const response = await xrpc(fetchHandler, testProcedure, {\n        body: { text: 'hello' },\n        validateRequest: true,\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({ id: 'valid' })\n    })\n  })\n\n  describe('validateResponse', () => {\n    it('rejects invalid response body by default', async () => {\n      // Schema expects { value: string } but server returns { value: 123 }\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 123 })\n      })\n\n      await expect(\n        xrpc(fetchHandler, testQuery, { params: { limit: 10 } }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcInvalidResponseError)\n        expect(err).toBeInstanceOf(XrpcUpstreamError)\n        return true\n      })\n    })\n\n    it('accepts invalid response body when disabled', async () => {\n      // Schema expects { value: string } but server returns { value: 123 }\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 123 })\n      })\n\n      const response = await xrpc(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        validateResponse: false,\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({ value: 123 })\n    })\n\n    it('succeeds with valid response body when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'hello' })\n      })\n\n      const response = await xrpc(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        validateResponse: true,\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({ value: 'hello' })\n    })\n  })\n\n  describe('strictResponseProcessing', () => {\n    // Helper: returns a JSON response containing a float (invalid lex data)\n    const validWithFloatHandler: FetchHandler = async () => {\n      return Response.json({ value: 'hello', extra: 1.5 }, { status: 200 })\n    }\n\n    // Helper: returns a JSON error response containing a float\n    const errorWithFloatHandler: FetchHandler = async () => {\n      return Response.json(\n        { error: 'TestError', message: 'test-error-description', extra: 1.5 },\n        { status: 400 },\n      )\n    }\n\n    it('rejects response with invalid lex data by default (strict parsing)', async () => {\n      await expect(\n        xrpc(validWithFloatHandler, testQuery, { params: { limit: 10 } }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcUpstreamError)\n        expect(err.message).toMatch('Unable to parse response payload')\n        expect(err.cause).toBeInstanceOf(TypeError)\n        return true\n      })\n    })\n\n    it('accepts response with invalid lex data when strict processing is disabled', async () => {\n      const response = await xrpc(validWithFloatHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: false,\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({ value: 'hello', extra: 1.5 })\n    })\n\n    it('rejects response with invalid lex data when strict processing is explicitly enabled', async () => {\n      await expect(\n        xrpc(validWithFloatHandler, testQuery, {\n          strictResponseProcessing: true,\n        }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcUpstreamError)\n        expect(err.message).toMatch('Unable to parse response payload')\n        expect(err.cause).toBeInstanceOf(TypeError)\n        return true\n      })\n    })\n\n    it('rejects error response with invalid lex data by default', async () => {\n      await expect(xrpc(errorWithFloatHandler, testQuery)).rejects.toSatisfy(\n        (err) => {\n          assert(err instanceof XrpcUpstreamError)\n          expect(err.message).toMatch('Unable to parse response payload')\n          return true\n        },\n      )\n    })\n\n    it('parses error response with invalid lex data when strict processing is disabled', async () => {\n      await expect(\n        xrpc(errorWithFloatHandler, testQuery, {\n          params: { limit: 10 },\n          strictResponseProcessing: false,\n        }),\n      ).rejects.toSatisfy((err) => {\n        // Error response is still an error, but it should be parsed successfully\n        assert(err instanceof XrpcResponseError)\n        expect(err.status).toBe(400)\n        expect(err.payload.body).toEqual({\n          error: 'TestError',\n          message: 'test-error-description',\n          extra: 1.5,\n        })\n        return true\n      })\n    })\n\n    it('with strictResponseProcessing: false and validateResponse: true, schema validation still runs', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 3, unknownValue: 1.5 }, { status: 200 })\n      })\n\n      await expect(\n        xrpc(fetchHandler, testQuery, {\n          params: { limit: 10 },\n          strictResponseProcessing: false,\n          validateResponse: true,\n        }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcInvalidResponseError)\n        expect(err).toBeInstanceOf(XrpcUpstreamError)\n        return true\n      })\n    })\n\n    it('with strictResponseProcessing: false and validateResponse: false, schema validation is skipped', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json(\n          {\n            // Invalid value\n            value: 3,\n            // Non-strict Lex Data values:\n            unknownValue: 1.2,\n            foo: { $bytes: 3 },\n          },\n          { status: 200 },\n        )\n      })\n\n      const response = await xrpc(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: false,\n        validateResponse: false,\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual({\n        value: 3,\n        unknownValue: 1.2,\n        foo: { $bytes: 3 },\n      })\n\n      // @NOTE \"validateResponse: false\" basically acts as type casting\n      expectTypeOf(response.body).toMatchObjectType<{\n        value: string\n      }>()\n    })\n\n    it('with strictResponseProcessing: true and validateResponse: false, strict parsing still applies', async () => {\n      await expect(\n        xrpc(validWithFloatHandler, testQuery, {\n          params: { limit: 10 },\n          strictResponseProcessing: true,\n          validateResponse: false,\n        }),\n      ).rejects.toSatisfy((err) => {\n        assert(err instanceof XrpcUpstreamError)\n        expect(err.message).toMatch('Unable to parse response payload')\n        return true\n      })\n    })\n\n    it('does not affect binary responses', async () => {\n      const bytes = new Uint8Array([1, 2, 3])\n\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(bytes, {\n          headers: { 'content-type': 'application/octet-stream' },\n        })\n      })\n\n      const response = await xrpc(fetchHandler, testBinaryQuery, {\n        strictResponseProcessing: false,\n      })\n\n      expect(response.success).toBe(true)\n      expect(response.body).toEqual(bytes)\n    })\n  })\n})\n\ndescribe(xrpcSafe, () => {\n  describe('success paths', () => {\n    it('returns successful result for a JSON query', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'hello' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 5 },\n      })\n\n      assert(result.success)\n      expect(result).toBeInstanceOf(XrpcResponse)\n      expect(result.body).toEqual({ value: 'hello' })\n      expect(result.encoding).toBe('application/json')\n      expect(result.value).toBe(result)\n    })\n\n    it('returns successful result for a JSON procedure', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'new-id' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testProcedure, {\n        body: { text: 'hello' },\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ id: 'new-id' })\n    })\n\n    it('returns successful result for a binary query', async () => {\n      const bytes = new Uint8Array([5, 6, 7])\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(bytes, {\n          headers: { 'content-type': 'application/octet-stream' },\n        })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testBinaryQuery)\n\n      assert(result.success)\n      expect(result.body).toBeInstanceOf(Uint8Array)\n      expect(result.body).toEqual(bytes)\n      expect(result.isParsed).toBe(false)\n    })\n\n    it('returns successful result for a binary procedure', async () => {\n      const bytes = new Uint8Array([42])\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(bytes, {\n          headers: { 'content-type': 'application/octet-stream' },\n        })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testBinaryProcedure, {\n        body: new Uint8Array([1, 2]),\n        encoding: 'image/jpeg',\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual(bytes)\n    })\n\n    it('returns successful result for a no-output query', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return new Response(null, { status: 200 })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testNoOutputQuery)\n\n      assert(result.success)\n      expect(result.body).toBeUndefined()\n      expect(result.encoding).toBeUndefined()\n    })\n  })\n\n  describe('error handling', () => {\n    describe('fetch errors', () => {\n      it('returns XrpcFetchError when fetchHandler throws', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          throw new TypeError('fetch failed')\n        })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcFetchError)\n        expect(result).toBeInstanceOf(XrpcInternalError)\n      })\n\n      it('returns XrpcFetchError when fetchHandler rejects', async () => {\n        const fetchHandler = vi.fn<FetchHandler>(async () => {\n          throw new Error('network timeout')\n        })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcFetchError)\n        expect(result.message).toContain('network timeout')\n      })\n    })\n\n    describe('response errors', () => {\n      it('returns XrpcResponseError for 400 with valid error payload', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          Response.json(\n            { error: 'TestError', message: 'bad request' },\n            { status: 400 },\n          )\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        assert(result instanceof XrpcResponseError)\n        expect(result.status).toBe(400)\n        expect(result.body).toEqual({\n          error: 'TestError',\n          message: 'bad request',\n        })\n      })\n\n      it('returns XrpcAuthenticationError for 401', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          Response.json(\n            { error: 'AuthenticationRequired', message: 'Token expired' },\n            { status: 401 },\n          )\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        assert(result instanceof XrpcResponseError)\n        expect(result).toBeInstanceOf(XrpcAuthenticationError)\n        expect(result.status).toBe(401)\n      })\n\n      it('returns XrpcUpstreamError for non-XRPC error response', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          new Response('Not Found', {\n            status: 404,\n            headers: { 'content-type': 'text/plain' },\n          })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcUpstreamError)\n      })\n\n      it('returns XrpcUpstreamError for 500 without valid error payload', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          new Response('Internal Server Error', {\n            status: 500,\n            headers: { 'content-type': 'text/html' },\n          })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcUpstreamError)\n      })\n    })\n\n    describe('invalid response errors', () => {\n      it('returns XrpcInvalidResponseError when response body fails validation', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          Response.json({ value: 123 })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcInvalidResponseError)\n        expect(result).toBeInstanceOf(XrpcUpstreamError)\n      })\n\n      it('returns XrpcUpstreamError when response has wrong content-type', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          new Response('binary data', {\n            status: 200,\n            headers: { 'content-type': 'text/plain' },\n          })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcUpstreamError)\n      })\n    })\n\n    describe('content-type header errors', () => {\n      it('returns XrpcInternalError when content-type header is set', async () => {\n        const fetchHandler: FetchHandler = async () =>\n          Response.json({ value: 'ok' })\n\n        const result = await xrpcSafe(fetchHandler, testQuery, {\n          params: { limit: 10 },\n          headers: { 'content-type': 'application/json' },\n        })\n\n        assert(!result.success)\n        expect(result).toBeInstanceOf(XrpcInternalError)\n        expect(result.cause).toBeInstanceOf(TypeError)\n      })\n    })\n  })\n\n  describe('validateRequest', () => {\n    it('returns XrpcInternalError for invalid query params when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'ok' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        // @ts-expect-error intentionally passing invalid params\n        params: { limit: 'not-a-number' },\n        validateRequest: true,\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcInternalError)\n      expect(result).not.toBeInstanceOf(XrpcFetchError)\n    })\n\n    it('returns XrpcInternalError for invalid body when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'abc' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testProcedure, {\n        // @ts-expect-error intentionally passing invalid body\n        body: { text: 123 },\n        validateRequest: true,\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcInternalError)\n      expect(result).not.toBeInstanceOf(XrpcFetchError)\n    })\n\n    it('skips body validation by default (invalid body sent as-is)', async () => {\n      const fetchHandler: FetchHandler = async () => Response.json({ id: 'ok' })\n\n      const result = await xrpcSafe(fetchHandler, testProcedure, {\n        // @ts-expect-error intentionally passing invalid body\n        body: { text: 123 },\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ id: 'ok' })\n    })\n\n    it('succeeds with valid body when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ id: 'valid' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testProcedure, {\n        body: { text: 'hello' },\n        validateRequest: true,\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ id: 'valid' })\n    })\n  })\n\n  describe('validateResponse', () => {\n    it('returns XrpcInvalidResponseError for invalid body by default', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 123 })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcInvalidResponseError)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n    })\n\n    it('accepts invalid response body when disabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 123 })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        validateResponse: false,\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ value: 123 })\n    })\n\n    it('succeeds with valid response body when enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 'hello' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        validateResponse: true,\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ value: 'hello' })\n    })\n\n    it('applies defaults', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (path) => {\n        const url = new URL(path, 'http://localhost')\n        const foo = url.searchParams.get('foo')\n\n        // default applied while building the request\n        expect(foo).toBe('foo-default')\n\n        return Response.json({ foo: 'foo-value' })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryWithDefaults, {\n        params: {},\n        validateResponse: true,\n      })\n\n      expect(fetchHandler).toHaveBeenCalled()\n      assert(result.success)\n      expect(result.body).toEqual({\n        bar: 'bar-default', // default applied while parsing the response\n        foo: 'foo-value',\n      })\n    })\n  })\n\n  describe('blob constraints', () => {\n    it('rejects invalid blob refs', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        // missing properties jere\n        return Response.json({ blobRef: { $type: 'blob' } })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toMatch('Unable to parse response payload')\n      assert(result.cause instanceof TypeError)\n      expect(result.cause.message).toBe('Invalid blob object')\n    })\n\n    it('rejects blob-refs with cbor data CIDs', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json(\n          lexToJson({\n            blobRef: {\n              $type: 'blob',\n              // ref should be a \"raw\" CID to be strictly valid\n              ref: cborCid,\n              mimeType: 'image/png',\n              size: 1,\n            },\n          }),\n        )\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toMatch('Unable to parse response payload')\n      assert(result.cause instanceof TypeError)\n      expect(result.cause.message).toBe('Invalid blob object')\n    })\n\n    it('enforces blob mime-type constraint by default', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json(\n          lexToJson({\n            blobRef: {\n              $type: 'blob',\n              ref: rawCid,\n              mimeType: 'invalid/mime',\n              size: 10,\n            },\n          }),\n        )\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toBe(\n        'Invalid response: Expected \"image/png\" (got \"invalid/mime\") at $.blobRef.mimeType',\n      )\n    })\n\n    it('enforces blob size constraint by default', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json(\n          lexToJson({\n            blobRef: {\n              $type: 'blob',\n              ref: rawCid,\n              mimeType: 'image/png',\n              size: 100,\n            },\n          }),\n        )\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef)\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toBe(\n        'Invalid response: blob too big (maximum 10, got 100) at $.blobRef',\n      )\n    })\n\n    it('ignores blob constraints in non-strict mode', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json(\n          lexToJson({\n            blobRef: {\n              $type: 'blob',\n              ref: rawCid,\n              mimeType: 'invalid/mime',\n              size: 100,\n            },\n          }),\n        )\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {\n        strictResponseProcessing: false,\n      })\n\n      assert(result.success)\n      expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()\n      expect(result.body).toEqual({\n        blobRef: {\n          $type: 'blob',\n          ref: rawCid,\n          mimeType: 'invalid/mime',\n          size: 100,\n        },\n      })\n    })\n\n    it('transforms legacy blobs in non-strict mode', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({\n          blobRef: {\n            cid: rawCid.toString(),\n            mimeType: 'invalid/mime',\n          },\n        })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {\n        strictResponseProcessing: false,\n      })\n\n      assert(result.success)\n      expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()\n      expect(result.body).toEqual({\n        blobRef: {\n          $type: 'blob',\n          ref: rawCid,\n          mimeType: 'invalid/mime',\n          size: -1,\n        },\n      })\n    })\n\n    it('allows blob-refs with negative size in non-strict mode', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({\n          blobRef: {\n            $type: 'blob',\n            ref: { $link: rawCid.toString() },\n            mimeType: 'invalid/mime',\n            size: -1,\n          },\n        })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQueryGetBlobRef, {\n        strictResponseProcessing: false,\n      })\n\n      assert(result.success)\n      expectTypeOf(result.body).toMatchObjectType<{ blobRef: l.BlobRef }>()\n      expect(result.body).toEqual({\n        blobRef: {\n          $type: 'blob',\n          ref: rawCid,\n          mimeType: 'invalid/mime',\n          size: -1,\n        },\n      })\n    })\n  })\n\n  describe('strictResponseProcessing', () => {\n    const jsonResponseWithFloat: FetchHandler = async () => {\n      return Response.json({ value: 'hello', extra: 1.5 }, { status: 200 })\n    }\n\n    const jsonErrorResponseWithFloat: FetchHandler = async () => {\n      return Response.json(\n        { error: 'TestError', message: 'test-error-description', extra: 1.5 },\n        { status: 400 },\n      )\n    }\n\n    it('returns error for invalid lex data by default (strict parsing)', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toMatch('Unable to parse response payload')\n      assert(result.cause instanceof TypeError)\n      expect(result.cause.message).toBe('Invalid non-integer number: 1.5')\n    })\n\n    it('accepts response with invalid lex data when strict processing is disabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: false,\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ value: 'hello', extra: 1.5 })\n    })\n\n    it('returns error for invalid lex data when strict processing is explicitly enabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: true,\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toMatch('Unable to parse response payload')\n      assert(result.cause instanceof TypeError)\n      expect(result.cause.message).toBe('Invalid non-integer number: 1.5')\n    })\n\n    it('returns error for error response with invalid lex data by default', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(jsonErrorResponseWithFloat)\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toMatch('Unable to parse response payload')\n      assert(result.cause instanceof TypeError)\n      expect(result.cause.message).toBe('Invalid non-integer number: 1.5')\n    })\n\n    it('parses error response with invalid lex data when strict processing is disabled', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(jsonErrorResponseWithFloat)\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: false,\n      })\n\n      assert(!result.success)\n      assert(result instanceof XrpcResponseError)\n      expect(result.status).toBe(400)\n      expect(result.message).toBe('test-error-description')\n    })\n\n    it('with strictResponseProcessing: false and validateResponse: true, schema validation still runs', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 1.5 }, { status: 200 })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: false,\n        validateResponse: true,\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcInvalidResponseError)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n    })\n\n    it('with strictResponseProcessing: false and validateResponse: false, schema validation is skipped', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json({ value: 1.5 }, { status: 200 })\n      })\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: false,\n        validateResponse: false,\n      })\n\n      assert(result.success)\n      expect(result.body).toEqual({ value: 1.5 })\n    })\n\n    it('with strictResponseProcessing: true and validateResponse: false, strict parsing still applies', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(jsonResponseWithFloat)\n\n      const result = await xrpcSafe(fetchHandler, testQuery, {\n        params: { limit: 10 },\n        strictResponseProcessing: true,\n        validateResponse: false,\n      })\n\n      assert(!result.success)\n      expect(result).toBeInstanceOf(XrpcUpstreamError)\n      expect(result.message).toMatch('Unable to parse response payload')\n      assert(result.cause instanceof TypeError)\n      expect(result.cause.message).toBe('Invalid non-integer number: 1.5')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-client/src/xrpc.ts",
    "content": "import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'\nimport { lexStringify } from '@atproto/lex-json'\nimport {\n  InferInput,\n  InferPayload,\n  Main,\n  NsidString,\n  Params,\n  Payload,\n  Procedure,\n  Query,\n  Restricted,\n  Subscription,\n  getMain,\n} from '@atproto/lex-schema'\nimport { Agent, AgentOptions, buildAgent } from './agent.js'\nimport { XrpcFailure, XrpcFetchError, asXrpcFailure } from './errors.js'\nimport { XrpcResponse, XrpcResponseOptions } from './response.js'\nimport { BinaryBodyInit } from './types.js'\nimport {\n  XrpcRequestHeadersOptions,\n  buildXrpcRequestHeaders,\n  isAsyncIterable,\n  isBlobLike,\n  toReadableStream,\n} from './util.js'\n\n/**\n * The query/path parameters type for an XRPC method, inferred from its schema.\n *\n * @typeParam M - The XRPC method type (Procedure, Query, or Subscription)\n */\nexport type XrpcRequestParams<M extends Procedure | Query | Subscription> =\n  InferInput<M['parameters']>\n\n// If all params are optional, allow omitting the params object\ntype XrpcRequestParamsOptions<P extends Params> =\n  NonNullable<unknown> extends P ? { params?: P } : { params: P }\n\ntype XrpcRequestPayload<M extends Procedure | Query> = M extends Procedure\n  ? InferPayload<M['input'], BinaryBodyInit>\n  : undefined\n\ntype XrpcRequestPayloadOptions<TPayload> = TPayload extends {\n  body: infer B\n  encoding: infer E\n}\n  ? {\n      body: B\n\n      /**\n       * mime type hint for binary bodies\n       *\n       * Only needed for endpoints that accept binary input (e.g. file uploads)\n       * when the body is a Blob-like object without a type (e.g. fetch-blob's\n       * Blob). If the body is a Blob-like object with a type, that type will be\n       * used as the content-type header instead of this option.\n       *\n       * @default \"application/octet-stream\"\n       */\n      encoding?: E\n    }\n  : { body?: undefined; encoding?: undefined }\n\n/**\n * Options for making an XRPC request, based on the method schema.\n *\n * Combines {@link XrpcRequestOptions} and {@link XrpcResponseOptions} with\n * method-specific params and body requirements. The type system ensures\n * required params/body are provided based on the method schema.\n *\n * @typeParam M - The XRPC method type (Procedure or Query)\n *\n * @example Query with params\n * ```typescript\n * const options: XrpcOptions<typeof app.bsky.feed.getTimeline.main> = {\n *   params: { limit: 50 }\n * }\n * ```\n *\n * @example Procedure with body\n * ```typescript\n * const options: XrpcOptions<typeof com.atproto.repo.createRecord.main> = {\n *   body: { repo: did, collection: 'app.bsky.feed.post', record: { ... } }\n * }\n * ```\n */\nexport type XrpcOptions<M extends Procedure | Query = Procedure | Query> =\n  XrpcRequestOptions<M> & XrpcResponseOptions\n\nexport type XrpcRequestOptions<\n  M extends Procedure | Query = Procedure | Query,\n> = XrpcRequestProcessingOptions &\n  XrpcRequestHeadersOptions &\n  XrpcRequestPayloadOptions<XrpcRequestPayload<M>> &\n  XrpcRequestParamsOptions<XrpcRequestParams<M>>\n\nexport type XrpcRequestProcessingOptions = {\n  /**\n   * AbortSignal to cancel the request.\n   */\n  signal?: AbortSignal\n\n  /**\n   * Whether to validate the request against the method's input schema. Enabling\n   * this can help catch errors early but may have a performance cost. This\n   * would typically only be set to `true` in development or debugging\n   * scenarios.\n   *\n   * @default false\n   */\n  validateRequest?: boolean\n}\n\n/**\n * Makes an XRPC request and throws on failure.\n *\n * This is the low-level function for making XRPC calls.\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns The successful {@link XrpcResponse}\n * @throws {XrpcFailure} When the request fails\n *\n * @example\n * ```typescript\n * const response = await xrpc('https://bsky.network', com.atproto.identity.resolveHandle, {\n *   params: { handle: \"atproto.com\" }\n * })\n * ```\n *\n * @example\n * ```typescript\n * const response = await xrpc(agent, app.bsky.feed.getTimeline.main, {\n *   params: { limit: 50 }\n * })\n * ```\n */\nexport async function xrpc<const M extends Query | Procedure>(\n  agentOpts: Agent | AgentOptions,\n  ns: NonNullable<unknown> extends XrpcOptions<M>\n    ? Main<M>\n    : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n  agentOpts: Agent | AgentOptions,\n  ns: Main<M>,\n  options: XrpcOptions<M>,\n): Promise<XrpcResponse<M>>\nexport async function xrpc<const M extends Query | Procedure>(\n  agentOpts: Agent | AgentOptions,\n  ns: Main<M>,\n  options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResponse<M>> {\n  const response = await xrpcSafe<M>(agentOpts, ns, options)\n  if (response.success) return response\n  else throw response\n}\n\n/**\n * Union type representing either a successful response or a failure.\n *\n * Both {@link XrpcResponse} and {@link XrpcFailure} have a `success` property\n * that can be used to discriminate between them.\n *\n * @typeParam M - The XRPC method type\n */\nexport type XrpcResult<M extends Procedure | Query> =\n  | XrpcResponse<M>\n  | XrpcFailure<M>\n\n/**\n * Makes an XRPC request without throwing on failure.\n *\n * Returns a discriminated union that can be checked via the `success` property.\n * This is useful for handling errors without try/catch blocks. This also allow\n * failure results to be typed with the method schema, which can provide better\n * type safety when handling errors (e.g. checking for specific error codes).\n *\n * @param agent - The {@link Agent} to use for making the request\n * @param ns - The lexicon method definition\n * @param options - Request {@link XrpcOptions options} (params, body, headers, etc.)\n * @returns Either a successful {@link XrpcResponse} or an {@link XrpcFailure}\n *\n * @example\n * ```typescript\n * const result = await xrpcSafe('https://example.com', app.bsky.actor.getProfile, {\n *   params: { actor: 'alice.bsky.social' }\n * })\n *\n * if (result.success) {\n *   console.log(result.body.displayName)\n * } else {\n *   console.error('Request failed:', result.error)\n * }\n * ```\n */\nexport async function xrpcSafe<const M extends Query | Procedure>(\n  agentOpts: Agent | AgentOptions,\n  ns: NonNullable<unknown> extends XrpcOptions<M>\n    ? Main<M>\n    : Restricted<'This XRPC method requires an \"options\" argument'>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n  agentOpts: Agent | AgentOptions,\n  ns: Main<M>,\n  options: XrpcOptions<M>,\n): Promise<XrpcResult<M>>\nexport async function xrpcSafe<const M extends Query | Procedure>(\n  agentOpts: Agent | AgentOptions,\n  ns: Main<M>,\n  options: XrpcOptions<M> = {} as XrpcOptions<M>,\n): Promise<XrpcResult<M>> {\n  options.signal?.throwIfAborted()\n  const method: M = getMain(ns)\n  try {\n    const agent = buildAgent(agentOpts)\n    const url = xrpcRequestUrl(method, options)\n    const request = xrpcRequestInit(method, options)\n    const response = await agent.fetchHandler(url, request).catch((cause) => {\n      throw new XrpcFetchError(method, cause)\n    })\n    return await XrpcResponse.fromFetchResponse<M>(method, response, options)\n  } catch (cause) {\n    return asXrpcFailure(method, cause)\n  }\n}\n\nfunction xrpcRequestUrl<M extends Procedure | Query | Subscription>(\n  method: M,\n  options: { params?: Params },\n): `/xrpc/${NsidString}${'' | `?${string}`}` {\n  const path = `/xrpc/${method.nsid}` as const\n\n  // @NOTE param.toURLSearchParams() will always validate the params in order to\n  // apply default values, so we can't disable it with options.validateRequest\n\n  const queryString = method.parameters\n    ?.toURLSearchParams(options.params ?? {})\n    .toString()\n\n  return queryString ? (`${path}?${queryString}` as const) : path\n}\n\nfunction xrpcRequestInit<T extends Procedure | Query>(\n  schema: T,\n  options: XrpcRequestProcessingOptions &\n    XrpcRequestHeadersOptions &\n    XrpcProcedureInputOptions & {\n      encoding?: string\n    },\n): RequestInit & { duplex?: 'half' } {\n  const headers = buildXrpcRequestHeaders(options)\n\n  // Tell the server what type of response we're expecting\n  if (schema.output.encoding) {\n    headers.set('accept', schema.output.encoding)\n  }\n\n  // Caller should not set content-type header\n  if (headers.has('content-type')) {\n    const contentType = headers.get('content-type')\n    throw new TypeError(`Unexpected content-type header (${contentType})`)\n  }\n\n  // Requests with body\n  if ('input' in schema) {\n    const encodingHint = options.encoding\n    const input = xrpcProcedureInput(schema, options, encodingHint)\n\n    if (input) {\n      headers.set('content-type', input.encoding)\n    } else if (encodingHint != null) {\n      throw new TypeError(`Unexpected encoding hint (${encodingHint})`)\n    }\n\n    return {\n      duplex: 'half',\n      redirect: 'follow',\n      referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n      mode: 'cors', // (default)\n      signal: options.signal,\n      method: 'POST',\n      headers,\n      body: input?.body,\n    }\n  }\n\n  // Requests without body\n  return {\n    duplex: 'half',\n    redirect: 'follow',\n    referrerPolicy: 'strict-origin-when-cross-origin', // (default)\n    mode: 'cors', // (default)\n    signal: options.signal,\n    method: 'GET',\n    headers,\n  }\n}\n\ntype XrpcProcedureInputOptions = {\n  body?: LexValue | BinaryBodyInit\n  validateRequest?: boolean\n}\n\nfunction xrpcProcedureInput(\n  method: Procedure,\n  options: XrpcProcedureInputOptions,\n  encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n  const { input } = method\n  const { body } = options\n\n  if (options.validateRequest) {\n    input.schema?.check(body)\n  }\n\n  // Special handling for endpoints expecting application/json input\n  if (input.encoding === 'application/json') {\n    // @NOTE **NOT** using isLexValue here to avoid deep checks in order to\n    // distinguish between LexValue and BinaryBodyInit.\n    if (!isLexScalar(body) && !isPlainObject(body) && !Array.isArray(body)) {\n      throw new TypeError(`Expected LexValue body, got ${typeof body}`)\n    }\n\n    return buildPayload(input, lexStringify(body), encodingHint)\n  }\n\n  // Other encodings will be sent unaltered (ie. as binary data)\n  switch (typeof body) {\n    case 'undefined':\n    case 'string':\n      return buildPayload(input, body, encodingHint)\n    case 'object': {\n      if (body === null) break\n      if (\n        ArrayBuffer.isView(body) ||\n        body instanceof ArrayBuffer ||\n        body instanceof ReadableStream\n      ) {\n        return buildPayload(input, body, encodingHint)\n      } else if (isAsyncIterable(body)) {\n        return buildPayload(input, toReadableStream(body), encodingHint)\n      } else if (isBlobLike(body)) {\n        return buildPayload(input, body, encodingHint || body.type)\n      }\n    }\n  }\n\n  throw new TypeError(\n    `Invalid ${typeof body} body for ${input.encoding} encoding`,\n  )\n}\n\nfunction buildPayload(\n  schema: Payload,\n  body: undefined | BodyInit,\n  encodingHint?: string,\n): null | { body: BodyInit; encoding: string } {\n  if (schema.encoding === undefined) {\n    if (body !== undefined) {\n      throw new TypeError(\n        `Cannot send a ${typeof body} body with undefined encoding`,\n      )\n    }\n\n    return null\n  }\n\n  if (body === undefined) {\n    // This error would be returned by the server, but we can catch it earlier\n    // to avoid un-necessary requests. Note that a content-length of 0 does not\n    // necessary mean that the body is \"empty\" (e.g. an empty txt file).\n    throw new TypeError(`A request body is expected but none was provided`)\n  }\n\n  const encoding = buildEncoding(schema, encodingHint)\n  return { encoding, body }\n}\n\nfunction buildEncoding(schema: Payload, encodingHint?: string): string {\n  // Should never happen (required for type safety)\n  if (!schema.encoding) {\n    throw new TypeError('Unexpected payload')\n  }\n\n  if (encodingHint?.length) {\n    if (!schema.matchesEncoding(encodingHint)) {\n      throw new TypeError(\n        `Cannot send a body with content-type \"${encodingHint}\" for \"${schema.encoding}\" encoding`,\n      )\n    }\n    return encodingHint\n  }\n\n  // Fallback\n\n  if (schema.encoding === '*/*') {\n    return 'application/octet-stream'\n  }\n\n  if (schema.encoding.startsWith('text/')) {\n    return schema.encoding.includes('*')\n      ? 'text/plain; charset=utf-8'\n      : `${schema.encoding}; charset=utf-8`\n  }\n\n  if (!schema.encoding.includes('*')) {\n    return schema.encoding\n  }\n\n  throw new TypeError(\n    `Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`,\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-client/tests/client.test.ts",
    "content": "import { assert, describe, expect, it, vi } from 'vitest'\nimport { LexValue, cidForLex } from '@atproto/lex-cbor'\nimport { cidForRawBytes } from '@atproto/lex-data'\nimport { lexParse, lexToJson } from '@atproto/lex-json'\nimport { toDatetimeString } from '@atproto/lex-schema'\nimport {\n  Action,\n  Client,\n  FetchHandler,\n  XrpcAuthenticationError,\n} from '../src/index.js'\nimport { app, com } from './lexicons/index.js'\n\ntype Preference = app.bsky.actor.defs.Preferences[number]\n\ndescribe('utils', () => {\n  describe('TypedObjectSchema', () => {\n    it('overrides $type when building an object', () => {\n      const _r = app.bsky.actor.defs.adultContentPref.build({\n        // @ts-expect-error\n        $type: 'foo',\n        enabled: true,\n      })\n      expect(_r.$type).toBe('app.bsky.actor.defs#adultContentPref')\n    })\n  })\n})\n\ndescribe('Client', () => {\n  describe('actions', () => {\n    it('updatePreferences', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        if (url === '/xrpc/app.bsky.actor.getPreferences') {\n          expect(init?.method).toBe('GET')\n          expect(init?.body).toBeUndefined()\n          return Response.json({ preferences: storedPreferences })\n        } else if (url === '/xrpc/app.bsky.actor.putPreferences') {\n          expect(init?.method).toBe('POST')\n          expect(typeof init?.body).toBe('string')\n          const result = app.bsky.actor.putPreferences.$input.schema.safeParse(\n            lexParse(init?.body as string),\n          )\n          if (result.success) {\n            storedPreferences = result.value.preferences\n            return new Response(null, { status: 204 })\n          } else {\n            return Response.json(\n              { error: 'InvalidRequest', message: result.reason.message },\n              { status: 400 },\n            )\n          }\n        } else {\n          return new Response('Not Found', { status: 404 })\n        }\n      })\n\n      const client = new Client({ fetchHandler })\n\n      const updatePreferences: Action<\n        (pref: Preference[]) => false | Preference[],\n        Preference[]\n      > = async function (client, updatePreferences, options) {\n        const data = await client.call(\n          app.bsky.actor.getPreferences,\n          {},\n          options,\n        )\n\n        const preferences = updatePreferences(data.preferences)\n        if (preferences === false) return data.preferences\n\n        options?.signal?.throwIfAborted()\n\n        await client.call(\n          app.bsky.actor.putPreferences,\n          { preferences },\n          options,\n        )\n\n        return preferences\n      }\n\n      const upsertPreference: Action<Preference, Preference[]> =\n        async function (client, pref, options) {\n          return updatePreferences(\n            client,\n            (prefs) => [...prefs.filter((p) => p.$type !== pref.$type), pref],\n            options,\n          )\n        }\n\n      let storedPreferences: Preference[] = [\n        app.bsky.actor.defs.adultContentPref.build({\n          enabled: false,\n        }),\n        app.bsky.actor.defs.contentLabelPref.build({\n          label: 'my-label',\n          visibility: 'warn',\n        }),\n      ]\n\n      expect(fetchHandler).toHaveBeenCalledTimes(0)\n      expect(storedPreferences).toEqual([\n        {\n          $type: 'app.bsky.actor.defs#adultContentPref',\n          enabled: false,\n        },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'my-label',\n          visibility: 'warn',\n        },\n      ])\n\n      // Upsert adult content preference\n      await client.call(\n        upsertPreference,\n        app.bsky.actor.defs.adultContentPref.build({\n          enabled: true,\n        }),\n      )\n\n      expect(fetchHandler).toHaveBeenCalledTimes(2)\n      expect(storedPreferences).toEqual([\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'my-label',\n          visibility: 'warn',\n        },\n        {\n          $type: 'app.bsky.actor.defs#adultContentPref',\n          enabled: true,\n        },\n      ])\n\n      // @ts-expect-error invalid preference value\n      await client.call(upsertPreference, {\n        $type: 'app.bsky.actor.defs#adultContentPref',\n        // enabled: true,\n      })\n\n      expect(fetchHandler).toHaveBeenCalledTimes(4)\n      expect(storedPreferences).toEqual([\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'my-label',\n          visibility: 'warn',\n        },\n        {\n          $type: 'app.bsky.actor.defs#adultContentPref',\n          enabled: false, // \"false\" default will be enforced when parsing the body\n        },\n      ])\n\n      await expect(async () => {\n        await client.call(upsertPreference, {\n          $type: 'app.bsky.actor.defs#adultContentPref',\n          // @ts-expect-error invalid preference value\n          enabled: 'not-a-boolean',\n        })\n      }).rejects.toThrow('Expected boolean value')\n    })\n  })\n\n  describe('query', () => {\n    it('allows perfoming a GET request and parsing the response', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')\n        expect(init?.method).toBe('GET')\n        expect(init?.body).toBeUndefined()\n\n        return Response.json({\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#adultContentPref',\n              enabled: false,\n            },\n            {\n              $type: 'app.bsky.actor.defs#someOtherPref',\n              otherField: 'some value',\n            },\n          ],\n        })\n      })\n\n      const client = new Client({ fetchHandler })\n\n      const { preferences } = await client.call(app.bsky.actor.getPreferences)\n\n      expect(preferences).toEqual([\n        {\n          $type: 'app.bsky.actor.defs#adultContentPref',\n          enabled: false,\n        },\n        {\n          $type: 'app.bsky.actor.defs#someOtherPref',\n          otherField: 'some value',\n        },\n      ])\n\n      expect(fetchHandler).toHaveBeenCalledTimes(1)\n\n      const adultContentPref = preferences.find((p) =>\n        app.bsky.actor.defs.adultContentPref.isTypeOf(p),\n      )\n\n      expect(adultContentPref).toEqual({\n        $type: 'app.bsky.actor.defs#adultContentPref',\n        enabled: false,\n      })\n    })\n  })\n\n  describe('errors', () => {\n    it('handles invalid XRPC error payloads', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')\n        expect(init?.method).toBe('GET')\n\n        return Response.json(\n          { invalidField: 'this is not a valid xrpc error payload' },\n          { status: 400 },\n        )\n      })\n\n      const client = new Client({ fetchHandler })\n\n      await expect(\n        client.call(app.bsky.actor.getPreferences),\n      ).rejects.toMatchObject({\n        error: 'UpstreamFailure',\n        message: 'Invalid response payload',\n      })\n    })\n\n    it('handles XRPC errors with invalid body data', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')\n        expect(init?.method).toBe('GET')\n\n        return new Response('Not a JSON body', {\n          status: 400,\n          headers: { 'Content-Type': 'text/plain' },\n        })\n      })\n\n      const client = new Client({ fetchHandler })\n\n      await expect(\n        client.call(app.bsky.actor.getPreferences),\n      ).rejects.toMatchObject({\n        error: 'UpstreamFailure',\n        message: 'Invalid response payload',\n      })\n    })\n\n    it('handles XRPC errors with invalid status code', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')\n        expect(init?.method).toBe('GET')\n\n        return new Response(null, {\n          status: 302,\n        })\n      })\n\n      const client = new Client({ fetchHandler })\n\n      await expect(\n        client.call(app.bsky.actor.getPreferences),\n      ).rejects.toMatchObject({\n        error: 'UpstreamFailure',\n        message: 'Invalid response status code',\n      })\n    })\n\n    it('handles XRPC server errors', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')\n        expect(init?.method).toBe('GET')\n\n        return new Response('<p>Server error</p>', {\n          status: 500,\n          headers: { 'Content-Type': 'text/html' },\n        })\n      })\n\n      const client = new Client({ fetchHandler })\n\n      await expect(\n        client.call(app.bsky.actor.getPreferences),\n      ).rejects.toMatchObject({\n        error: 'UpstreamFailure',\n        message: 'Upstream server encountered an error',\n      })\n    })\n\n    it('propatages server error messages', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/app.bsky.actor.getPreferences')\n        expect(init?.method).toBe('GET')\n\n        return Response.json(\n          {\n            error: 'CustomError',\n            message: 'This is a custom error message from the server',\n          },\n          {\n            status: 400,\n          },\n        )\n      })\n\n      const client = new Client({ fetchHandler })\n\n      await expect(\n        client.call(app.bsky.actor.getPreferences),\n      ).rejects.toMatchObject({\n        name: 'XrpcResponseError',\n        message: 'This is a custom error message from the server',\n        payload: {\n          encoding: 'application/json',\n          body: {\n            error: 'CustomError',\n            message: 'This is a custom error message from the server',\n          },\n        },\n      })\n    })\n\n    it('turns 401 responses into XrpcAuthenticationError', async () => {\n      const fetchHandler = vi.fn<FetchHandler>(async () => {\n        return Response.json(\n          {\n            error: 'Unauthorized',\n            message: 'Unauthorized access',\n          },\n          {\n            status: 401,\n            headers: {\n              'www-authenticate':\n                'Basic realm=\"example\", charset=\"UTF-8\", error=\"invalid_token\", error_description=\"The access token is invalid\", Bearer realm=\"oauth\"',\n            },\n          },\n        )\n      })\n\n      const client = new Client({ fetchHandler })\n\n      const response = await client.xrpcSafe(app.bsky.actor.getPreferences)\n\n      assert(response instanceof XrpcAuthenticationError)\n      expect(response.error).toBe('Unauthorized')\n      expect(response.message).toBe('Unauthorized access')\n      expect(response.name).toBe('XrpcAuthenticationError')\n      expect(response.wwwAuthenticate).toEqual({\n        Basic: {\n          realm: 'example',\n          charset: 'UTF-8',\n          error: 'invalid_token',\n          error_description: 'The access token is invalid',\n        },\n        Bearer: {\n          realm: 'oauth',\n        },\n      })\n    })\n  })\n\n  describe('records', () => {\n    it('allows creating records', async () => {\n      let currentTid = 0\n      // Only works 8 times\n      const nextTid = vi.fn(() => `2222222222${2 + currentTid++}22`)\n\n      const did = 'did:plc:alice'\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(url).toBe('/xrpc/com.atproto.repo.createRecord')\n        expect(init?.method).toBe('POST')\n        expect(typeof init?.body).toBe('string')\n        const payload = com.atproto.repo.createRecord.main.input.schema.parse(\n          lexParse(init?.body as string),\n        )\n\n        expect(payload).toMatchObject({\n          repo: did,\n          collection: expect.any(String),\n          record: expect.any(Object),\n        })\n\n        const rkey = payload.rkey || nextTid()\n        const cid = await cidForLex(payload.record as LexValue)\n\n        const responseBody: com.atproto.repo.createRecord.$OutputBody = {\n          cid: cid.toString(),\n          uri: `at://${payload.repo}/${payload.collection}/${rkey}`,\n        }\n\n        return Response.json(responseBody)\n      })\n\n      const client = new Client({ fetchHandler, did })\n\n      await expect(async () => {\n        return client.create(\n          app.bsky.feed.generator,\n          {\n            // @ts-expect-error invalid DID\n            did: 'not-a-did',\n            displayName: 'Alice Generator',\n            createdAt: '2024-01-01T00:00:00Z',\n          },\n          {\n            rkey: 'alice-generator',\n            validateRequest: true,\n          },\n        )\n      }).rejects.toThrow('Invalid DID (got \"not-a-did\") at $.did')\n\n      // validate performs schema validation before making the request\n      expect(fetchHandler).toHaveBeenCalledTimes(0)\n\n      const newGenerator = await client.create(\n        app.bsky.feed.generator,\n        {\n          did,\n          displayName: 'Alice Generator',\n          createdAt: '2024-01-01T00:00:00Z',\n        },\n        {\n          rkey: 'alice-generator',\n          validate: true,\n        },\n      )\n\n      expect(fetchHandler).toHaveBeenCalledTimes(1)\n      expect(newGenerator.cid).toBe(\n        'bafyreihx5eurnmsnj6ulfby3icl4ebh6pliwuqaze25z4ejitnt23b4vw4',\n      )\n\n      const aliceGenerator = await client.create(app.bsky.feed.generator, {\n        did,\n        displayName: 'Alice Generator',\n        createdAt: toDatetimeString(new Date()),\n      })\n\n      expect(nextTid).toHaveBeenCalledTimes(1)\n      expect(aliceGenerator.uri).toBe(\n        `at://${did}/app.bsky.feed.generator/${'2'.repeat(13)}`,\n      )\n\n      const newProfile = await client.create(app.bsky.actor.profile, {\n        displayName: 'Alice',\n      })\n\n      expect(nextTid).toHaveBeenCalledTimes(1)\n      expect(fetchHandler).toHaveBeenCalledTimes(3)\n      expect(newProfile.uri).toBe(\n        'at://did:plc:alice/app.bsky.actor.profile/self',\n      )\n\n      const newPost = await client.create(app.bsky.feed.post, {\n        text: 'Hello, world!',\n        createdAt: toDatetimeString(new Date()),\n      })\n\n      expect(nextTid).toHaveBeenCalledTimes(2)\n      expect(fetchHandler).toHaveBeenCalledTimes(4)\n      expect(newPost.uri).toBe(`at://${did}/app.bsky.feed.post/2222222222322`)\n    })\n\n    it('allows fetching records', async () => {\n      const did = 'did:plc:alice'\n      const fetchHandler = vi.fn<FetchHandler>(async (url, init) => {\n        expect(init?.method).toBe('GET')\n        const urlObj = new URL(url, 'https://example.com')\n        expect(urlObj.pathname).toBe('/xrpc/com.atproto.repo.getRecord')\n\n        const repo = urlObj.searchParams.get('repo')\n        const collection = urlObj.searchParams.get('collection')\n        const rkey = urlObj.searchParams.get('rkey')\n\n        expect(repo).toBe(did)\n        expect(collection).toBe(app.bsky.feed.post.$type)\n        expect(rkey).toBe('2222222222222')\n\n        const record = app.bsky.feed.post.$build({\n          text: 'This is an old post',\n          createdAt: '2024-01-01T00:00:00Z',\n        })\n\n        const cid = await cidForLex(record)\n\n        const responseBody: com.atproto.repo.getRecord.$OutputBody = {\n          cid: cid.toString(),\n          uri: `at://${repo!}/${collection!}/${rkey!}` as any,\n          value: record,\n        }\n\n        return Response.json(responseBody)\n      })\n\n      const client = new Client({ fetchHandler, did })\n\n      const { value: post } = await client.get(app.bsky.feed.post, {\n        rkey: '2222222222222',\n      })\n\n      expect(fetchHandler).toHaveBeenCalledTimes(1)\n      expect(post).toMatchObject({\n        $type: 'app.bsky.feed.post',\n        text: 'This is an old post',\n        createdAt: '2024-01-01T00:00:00Z',\n      })\n\n      // @TODO: using getRecord method (to check we got the cid)\n    })\n  })\n\n  describe('blobs', () => {\n    const fetchHandler = vi.fn(\n      async (url: string, init?: RequestInit): Promise<Response> => {\n        expect(url).toBe('/xrpc/com.atproto.repo.uploadBlob')\n        expect(init?.method).toBe('POST')\n        const headers = new Headers(init?.headers)\n        const type = headers.get('content-type')!\n        expect(type).toBeDefined()\n        const blob =\n          init?.body instanceof Blob\n            ? init.body\n            : ArrayBuffer.isView(init?.body) ||\n                init?.body instanceof ArrayBuffer\n              ? new Blob([init.body], { type })\n              : (() => {\n                  throw new Error('Invalid body type')\n                })()\n\n        const bytes = new Uint8Array(await blob.arrayBuffer())\n\n        const responseBody: com.atproto.repo.uploadBlob.$OutputBody = {\n          blob: {\n            $type: 'blob',\n            ref: await cidForRawBytes(bytes),\n            mimeType: blob.type,\n            size: blob.size,\n          },\n        }\n\n        return Response.json(lexToJson(responseBody))\n      },\n    )\n\n    it('allows uploading blobs', async () => {\n      const client = new Client({ fetchHandler })\n      const blob = new Blob(['hello world'], { type: 'text/plain' })\n\n      const { body } = await client.uploadBlob(blob)\n\n      expect(fetchHandler).toHaveBeenCalledTimes(1)\n      expect(body.blob.$type).toBe('blob')\n      expect(body.blob.mimeType).toBe('text/plain')\n      expect(body.blob.size).toBe(11)\n      expect(body.blob.ref).toEqual(\n        await cidForRawBytes(new TextEncoder().encode('hello world')),\n      )\n    })\n\n    it('allows uploading blobs from Uint8Array', async () => {\n      const client = new Client({ fetchHandler })\n      const data = new TextEncoder().encode('hello world')\n\n      const { body } = await client.uploadBlob(data)\n\n      expect(fetchHandler).toHaveBeenCalledTimes(2)\n      expect(body.blob.$type).toBe('blob')\n      expect(body.blob.mimeType).toBe('application/octet-stream')\n      expect(body.blob.size).toBe(11)\n      expect(body.blob.ref).toEqual(\n        await cidForRawBytes(new TextEncoder().encode('hello world')),\n      )\n    })\n\n    it('allows uploading blobs from ArrayBuffer', async () => {\n      const client = new Client({ fetchHandler })\n      const data = new TextEncoder().encode('hello world').buffer\n\n      const { body } = await client.uploadBlob(data)\n\n      expect(fetchHandler).toHaveBeenCalledTimes(3)\n      expect(body.blob.$type).toBe('blob')\n      expect(body.blob.mimeType).toBe('application/octet-stream')\n      expect(body.blob.size).toBe(11)\n      expect(body.blob.ref).toEqual(\n        await cidForRawBytes(new TextEncoder().encode('hello world')),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-client/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-client/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-client/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-client/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-data/CHANGELOG.md",
    "content": "# @atproto/lex-data\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4761](https://github.com/bluesky-social/atproto/pull/4761) [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `-1` BlobRef size in non-strict mode\n\n## 0.0.13\n\n### Patch Changes\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove concept of \"downstream error response\" (`toResponse()` method) from the `LexError` class\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve messaging of various errors by making them more precisely describe what happened.\n\n## 0.0.12\n\n### Patch Changes\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor code simplification\n\n## 0.0.11\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid exposing empty string as `message` in error payloads\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `ifUint8Array` utility function\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `isLanguageString` to `validateLanguage`\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve code coverage of tests\n\n## 0.0.8\n\n### Patch Changes\n\n- [#4528](https://github.com/bluesky-social/atproto/pull/4528) [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `asMultiformatsCID` utility for improved compatibility between `Cid` interface and legacy use of multiformat's `CID`\n\n- [#4528](https://github.com/bluesky-social/atproto/pull/4528) [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make the `Cid` interface stricter to allow for better futur proofing.\n\n- [#4528](https://github.com/bluesky-social/atproto/pull/4528) [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `isCid`, `ifCid` and `asCid` now always return the input value instead a potentially different instance.\n\n- [#4528](https://github.com/bluesky-social/atproto/pull/4528) [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `createCid` utility\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - **breaking**: replace `strict` options with `flavor` checking when parsing/decoding/validating `Cid` values\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose CID creation and validation utilities\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - **breaking**: `asCid` now throws if the input cannot be cast into a `Cid`\n\n## 0.0.6\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `enumBlobRefs` utility function\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Perform strict `BlobRef` validation by default\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `base64ToUtf8` and `utf8ToBase64` utilities\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `LexErrorData` type, and an `LexError` class to represent structured error information\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `ui8Concat` utility for concatenating `Uint8Array`s\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use stack based approach in `isLexValue` allowing to be called with very deep structures;\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not throw an error from `isLexScalar` (return `false` instead)\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Forbid use of unsafe integers\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `isPlainProto` utility to check if an object is a plain object.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `parseLanguage` and `isLanguage` exports to `parseLanguageString` and `isLanguageString` respectively\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4422](https://github.com/bluesky-social/atproto/pull/4422) [`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for base64url encoding/decoding\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `Cid` interface instead of `CID` class to represent parsed CID data.\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- Updated dependencies [[`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69)]:\n  - @atproto/syntax@0.4.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n"
  },
  {
    "path": "packages/lex/lex-data/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-data\",\n  \"version\": \"0.0.14\",\n  \"license\": \"MIT\",\n  \"description\": \"Core utilities for AT Lexicons\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"utilities\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-data\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"multiformats\": \"^9.9.0\",\n    \"tslib\": \"^2.8.1\",\n    \"uint8arrays\": \"3.0.0\",\n    \"unicode-segmenter\": \"^0.14.0\"\n  },\n  \"devDependencies\": {\n    \"core-js\": \"^3\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/blob.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport {\n  BlobRef,\n  LegacyBlobRef,\n  enumBlobRefs,\n  isBlobRef,\n  isLegacyBlobRef,\n} from './blob.js'\nimport { RawCid, parseCid } from './cid.js'\nimport { LexArray, LexMap, LexValue } from './lex.js'\n\n// await cidForRawBytes(Buffer.from('Hello, World!'))\nconst validBlobCid = parseCid(\n  'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n  { flavor: 'raw' },\n)\n\n// await cidForLex(Buffer.from('Hello, World!'))\nconst invalidBlobCid = parseCid(\n  'bafyreic52vzks7wdklat4evp3vimohl55i2unzqpshz2ytka5omzr7exdy',\n  { flavor: 'cbor' },\n)\n\ndescribe(isBlobRef, () => {\n  it('tests valid blobCid and lexCid', () => {\n    expect(validBlobCid.code).toBe(0x55) // raw\n    expect(validBlobCid.multihash.code).toBe(0x12) // sha2-256\n    expect(invalidBlobCid.code).toBe(0x71) // dag-cbor\n    expect(invalidBlobCid.multihash.code).toBe(0x12) // sha2-256\n  })\n\n  it('parses valid blob', () => {\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        ref: validBlobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      }),\n    ).toBe(true)\n\n    expect(\n      isBlobRef(\n        {\n          $type: 'blob',\n          ref: invalidBlobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        // In non-strict mode, any CID should be accepted\n        { strict: false },\n      ),\n    ).toBe(true)\n  })\n\n  it('performs strict validation by default', () => {\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        ref: invalidBlobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      }),\n    ).toBe(false)\n  })\n\n  it('rejects invalid inputs', () => {\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        ref: { $link: validBlobCid.toString() },\n        mimeType: 'image/jpeg',\n        size: '10000',\n      }),\n    ).toBe(false)\n\n    expect(\n      isBlobRef({\n        // $type: 'blob',\n        ref: validBlobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      }),\n    ).toBe(false)\n\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        ref: validBlobCid,\n        mimeType: { toString: () => 'image/jpeg' },\n        size: 10000,\n      }),\n    ).toBe(false)\n\n    expect(\n      isBlobRef(\n        {\n          $type: 'blob',\n          ref: { $link: validBlobCid.toString() },\n          mimeType: 'image/jpeg',\n          size: '10000',\n        },\n        { strict: true },\n      ),\n    ).toBe(false)\n\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        mimeType: 'image/jpeg',\n        size: 10000,\n      }),\n    ).toBe(false)\n\n    expect(\n      isBlobRef(\n        {\n          $type: 'blob',\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        { strict: true },\n      ),\n    ).toBe(false)\n\n    expect(isBlobRef('not an object')).toBe(false)\n    expect(isBlobRef([])).toBe(false)\n    expect(isBlobRef(new Date())).toBe(false)\n    expect(isBlobRef(new Map())).toBe(false)\n  })\n\n  it('rejects non-integer size', () => {\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        ref: validBlobCid,\n        mimeType: 'image/jpeg',\n        size: 10000.5,\n      }),\n    ).toBe(false)\n  })\n\n  it('rejects invalid CID/multihash code', () => {\n    expect(\n      isBlobRef(\n        {\n          $type: 'blob',\n          ref: validBlobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        { strict: true },\n      ),\n    ).toBe(true)\n\n    expect(\n      isBlobRef(\n        {\n          $type: 'blob',\n          ref: invalidBlobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        { strict: true },\n      ),\n    ).toBe(false)\n  })\n\n  it('rejects extra keys', () => {\n    expect(\n      isBlobRef({\n        $type: 'blob',\n        ref: validBlobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n        extra: 'not allowed',\n      }),\n    ).toBe(false)\n\n    expect(\n      isBlobRef(\n        {\n          $type: 'blob',\n          ref: validBlobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n          extra: 'not allowed',\n        },\n        { strict: true },\n      ),\n    ).toBe(false)\n  })\n\n  describe('strict mode', () => {\n    it('rejects invalid CID version', () => {\n      const cidV0 = parseCid(\n        'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG', // CID v0\n      )\n      expect(\n        isBlobRef(\n          {\n            $type: 'blob',\n            ref: cidV0,\n            mimeType: 'image/jpeg',\n            size: 10000,\n          },\n          { strict: true },\n        ),\n      ).toBe(false)\n    })\n  })\n})\n\ndescribe(isLegacyBlobRef, () => {\n  it('parses valid legacy blob', () => {\n    expect(\n      isLegacyBlobRef({\n        cid: validBlobCid.toString(),\n        mimeType: 'image/jpeg',\n      }),\n    ).toBe(true)\n\n    expect(\n      isLegacyBlobRef({\n        cid: invalidBlobCid.toString(),\n        mimeType: 'image/jpeg',\n      }),\n    ).toBe(true)\n  })\n\n  it('rejects invalid inputs', () => {\n    expect(\n      isLegacyBlobRef({\n        cid: 'babbaaa',\n        mimeType: 'image/jpeg',\n      }),\n    ).toBe(false)\n\n    expect(\n      isLegacyBlobRef({\n        cid: 12345,\n        mimeType: 'image/jpeg',\n      }),\n    ).toBe(false)\n\n    expect(\n      isLegacyBlobRef({\n        mimeType: 'image/jpeg',\n      }),\n    ).toBe(false)\n\n    expect(\n      isLegacyBlobRef({\n        cid: invalidBlobCid.toString(),\n        mimeType: { toString: () => 'image/jpeg' },\n      }),\n    ).toBe(false)\n\n    expect(\n      isLegacyBlobRef({\n        cid: invalidBlobCid.toString(),\n        mimeType: 3,\n      }),\n    ).toBe(false)\n\n    expect(\n      isLegacyBlobRef({\n        cid: invalidBlobCid.toString(),\n        mimeType: '',\n      }),\n    ).toBe(false)\n\n    expect(isLegacyBlobRef([])).toBe(false)\n    expect(isLegacyBlobRef('not an object')).toBe(false)\n    expect(isLegacyBlobRef(new Date())).toBe(false)\n    expect(isLegacyBlobRef(new Map())).toBe(false)\n  })\n\n  it('rejects extra keys', () => {\n    expect(\n      isLegacyBlobRef({\n        cid: validBlobCid.toString(),\n        mimeType: 'image/jpeg',\n        extra: 'not allowed',\n      }),\n    ).toBe(false)\n  })\n})\n\ndescribe(enumBlobRefs, () => {\n  const valid1: BlobRef<RawCid> = {\n    $type: 'blob',\n    ref: validBlobCid,\n    mimeType: 'image/png',\n    size: 2048,\n  }\n\n  const valid2: BlobRef<RawCid> = {\n    $type: 'blob',\n    ref: validBlobCid,\n    mimeType: 'image/jpeg',\n    size: 1024,\n  }\n\n  const invalid: BlobRef = {\n    $type: 'blob',\n    ref: invalidBlobCid,\n    mimeType: 'image/jpeg',\n    size: 1024,\n  }\n\n  const legacy: LegacyBlobRef = {\n    cid: validBlobCid.toString(),\n    mimeType: 'image/gif',\n  }\n\n  const data: LexValue = {\n    name: 'example',\n    file: { deeply: { nested: { in: { object: { valid1 } } } } },\n    attachments: [valid2, invalid, legacy, { description: 'not a blob' }],\n  }\n\n  it('enumerates valid BlobRefs by default', () => {\n    const refs = Array.from(enumBlobRefs(data))\n    expect(refs).toHaveLength(2)\n    expect(refs.includes(valid1)).toBe(true)\n    expect(refs.includes(valid2)).toBe(true)\n  })\n\n  describe('strict support', () => {\n    it('enumerates valid BlobRefs in strict mode', () => {\n      const refs = Array.from(enumBlobRefs(data, { strict: true }))\n      expect(refs).toHaveLength(2)\n      expect(refs.includes(valid1)).toBe(true)\n      expect(refs.includes(valid2)).toBe(true)\n    })\n\n    it('enumerates all BlobRefs in non-strict mode', () => {\n      const refs = Array.from(enumBlobRefs(data, { strict: false }))\n      expect(refs).toHaveLength(3)\n      expect(refs.includes(valid1)).toBe(true)\n      expect(refs.includes(valid2)).toBe(true)\n      expect(refs.includes(invalid)).toBe(true)\n    })\n  })\n\n  describe('legacy support', () => {\n    it('returns LegacyBlobRefs when legacy option is enabled', () => {\n      const refs = Array.from(enumBlobRefs(data, { allowLegacy: true }))\n      expect(refs).toHaveLength(3)\n      expect(refs.includes(valid1)).toBe(true)\n      expect(refs.includes(valid2)).toBe(true)\n      expect(refs.includes(legacy)).toBe(true)\n    })\n  })\n\n  describe('safety', () => {\n    it('handles cyclic structures without infinite loops', () => {\n      const cyclicArray: LexArray = [valid2]\n      const cyclicObject: LexMap = {\n        name: 'cyclic',\n        blob: valid1,\n      }\n\n      // Creating a cycle\n      cyclicArray.push(cyclicArray)\n      cyclicObject.self = cyclicObject\n\n      const refs = Array.from(\n        enumBlobRefs({\n          cyclicObject,\n          cyclicArray,\n        }),\n      )\n      expect(refs).toHaveLength(2)\n      expect(refs.includes(valid1)).toBe(true)\n      expect(refs.includes(valid2)).toBe(true)\n    })\n\n    it('handles deep structures without exceeding call stack', () => {\n      // Creating a deep nested structure\n      let deepData: LexMap = { blob: valid1 }\n      for (let i = 0; i < 100_000; i++) {\n        deepData = { nested: deepData }\n      }\n\n      const refs = Array.from(enumBlobRefs(deepData))\n      expect(refs).toHaveLength(1)\n      expect(refs[0]).toBe(valid1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/blob.ts",
    "content": "import { Cid, RawCid, ifCid, validateCidString } from './cid.js'\nimport { LexValue } from './lex.js'\nimport { isPlainObject, isPlainProto } from './object.js'\n\n// Number.isSafeInteger is actually safe to use with non-number values, so we\n// can use it as a type guard.\nconst isSafeInteger = Number.isSafeInteger as (v: unknown) => v is number\n\n/**\n * Reference to binary data (like images, videos, etc.) in the AT Protocol data model.\n *\n * A BlobRef is a {@link LexMap} with a specific structure that identifies binary\n * content by its content hash (CID), along with metadata about the content type\n * and size.\n *\n * @typeParam Ref - The type of CID reference, defaults to any {@link Cid}\n *\n * @example\n * ```typescript\n * import type { BlobRef } from '@atproto/lex-data'\n *\n * const imageRef: BlobRef = {\n *   $type: 'blob',\n *   mimeType: 'image/jpeg',\n *   ref: cid,  // CID of the blob content\n *   size: 12345\n * }\n * ```\n *\n * @see {@link isBlobRef} to check if a value is a valid BlobRef\n * @see {@link LegacyBlobRef} for the older blob reference format\n */\nexport type BlobRef<Ref extends Cid = Cid> = {\n  $type: 'blob'\n  mimeType: string\n  ref: Ref\n  size: number\n}\n\n/**\n * Options for validating a {@link BlobRef}.\n */\nexport type BlobRefCheckOptions = {\n  /**\n   * If `false`, skips strict CID validation of {@link BlobRef.ref}, allowing\n   * any valid CID. Otherwise, validates that the CID is v1, uses the raw\n   * multicodec, and has a sha256 multihash.\n   *\n   * @default true\n   */\n  strict?: boolean\n}\n\n/**\n * Infers the BlobRef type based on the check options.\n *\n * @typeParam TOptions - The options used for checking\n */\nexport type InferCheckedBlobRef<TOptions extends BlobRefCheckOptions> =\n  TOptions extends { strict: false }\n    ? BlobRef\n    : { strict: boolean } extends TOptions\n      ? BlobRef\n      : BlobRef<RawCid>\n\n/**\n * Type guard to check if a value is a valid {@link BlobRef}.\n *\n * Validates the structure of the input including:\n * - `$type` must be `'blob'`\n * - `mimeType` must be a valid MIME type string (containing '/')\n * - `size` must be a non-negative safe integer\n * - `ref` must be a valid CID (strict validation by default)\n *\n * @param input - The value to check\n * @param options - Optional validation options\n * @returns `true` if the input is a valid BlobRef\n *\n * @example\n * ```typescript\n * import { isBlobRef } from '@atproto/lex-data'\n *\n * if (isBlobRef(data)) {\n *   console.log(data.mimeType)  // e.g., 'image/jpeg'\n *   console.log(data.size)      // e.g., 12345\n * }\n *\n * // Allow any valid CID (not just raw CIDs)\n * if (isBlobRef(data, { strict: false })) {\n *   // ...\n * }\n * ```\n */\nexport function isBlobRef(input: unknown): input is BlobRef<RawCid>\nexport function isBlobRef<TOptions extends BlobRefCheckOptions>(\n  input: unknown,\n  options: TOptions,\n): input is InferCheckedBlobRef<TOptions>\nexport function isBlobRef(\n  input: unknown,\n  options?: BlobRefCheckOptions,\n): input is BlobRef\nexport function isBlobRef(\n  input: unknown,\n  options?: BlobRefCheckOptions,\n): input is BlobRef {\n  if (!isPlainObject(input)) {\n    return false\n  }\n\n  if (input?.$type !== 'blob') {\n    return false\n  }\n\n  const { mimeType, size, ref } = input\n  // @NOTE Very basic mime validation\n  if (typeof mimeType !== 'string' || !mimeType.includes('/')) {\n    return false\n  }\n\n  if (size === -1 && options?.strict === false) {\n    // In non-strict mode, allow size to be -1 to accommodate legacy blob refs\n    // that don't include size information.\n  } else if (!isSafeInteger(size) || size < 0) {\n    return false\n  }\n\n  if (typeof ref !== 'object' || ref === null) {\n    return false\n  }\n\n  for (const key in input) {\n    if (\n      key !== '$type' &&\n      key !== 'mimeType' &&\n      key !== 'ref' &&\n      key !== 'size'\n    ) {\n      return false\n    }\n  }\n\n  const cid = ifCid(\n    ref,\n    // Strict unless explicitly disabled\n    options?.strict === false ? undefined : { flavor: 'raw' },\n  )\n  if (!cid) {\n    return false\n  }\n\n  return true\n}\n\n/**\n * Legacy format for blob references used in older AT Protocol data.\n *\n * This is the older format that stores the CID as a string rather than\n * as a structured CID object. New code should use {@link BlobRef} instead.\n *\n * @example\n * ```typescript\n * import type { LegacyBlobRef } from '@atproto/lex-data'\n *\n * const legacyRef: LegacyBlobRef = {\n *   cid: 'bafyreib...',\n *   mimeType: 'image/jpeg'\n * }\n * ```\n *\n * @see {@link isLegacyBlobRef} to check if a value is a LegacyBlobRef\n * @see {@link BlobRef} for the current blob reference format\n * @deprecated Use {@link BlobRef} for new code\n */\nexport type LegacyBlobRef = {\n  cid: string\n  mimeType: string\n}\n\n/**\n * Type guard to check if a value is a valid {@link LegacyBlobRef}.\n *\n * Validates the structure of the input:\n * - `cid` must be a valid CID string\n * - `mimeType` must be a non-empty string\n * - No additional properties allowed\n *\n * @param input - The value to check\n * @returns `true` if the input is a valid LegacyBlobRef\n *\n * @example\n * ```typescript\n * import { isLegacyBlobRef } from '@atproto/lex-data'\n *\n * if (isLegacyBlobRef(data)) {\n *   console.log(data.cid)       // CID as string\n *   console.log(data.mimeType)  // e.g., 'image/jpeg'\n * }\n * ```\n *\n * @see {@link isBlobRef} for checking the current blob reference format\n */\nexport function isLegacyBlobRef(input: unknown): input is LegacyBlobRef {\n  if (!isPlainObject(input)) {\n    return false\n  }\n\n  const { cid, mimeType } = input\n  if (typeof cid !== 'string') {\n    return false\n  }\n\n  if (typeof mimeType !== 'string' || mimeType.length === 0) {\n    return false\n  }\n\n  for (const key in input) {\n    if (key !== 'cid' && key !== 'mimeType') {\n      return false\n    }\n  }\n\n  if (!validateCidString(cid)) {\n    return false\n  }\n\n  return true\n}\n\n/**\n * Options for enumerating blob references within a {@link LexValue}.\n */\nexport type EnumBlobRefsOptions = BlobRefCheckOptions & {\n  /**\n   * If `true`, also yields {@link LegacyBlobRef} objects in addition to\n   * {@link BlobRef} objects.\n   *\n   * @default false\n   */\n  allowLegacy?: boolean\n}\n\n/**\n * Infers the yielded type of {@link enumBlobRefs} based on options.\n *\n * @typeParam TOptions - The options used for enumeration\n */\nexport type InferEnumBlobRefs<TOptions extends EnumBlobRefsOptions> =\n  TOptions extends { allowLegacy: true }\n    ? InferCheckedBlobRef<TOptions> | LegacyBlobRef\n    : { allowLegacy: boolean } extends TOptions\n      ? InferCheckedBlobRef<TOptions> | LegacyBlobRef\n      : InferCheckedBlobRef<TOptions>\n\n/**\n * Generator that enumerates all {@link BlobRef}s (and, optionally,\n * {@link LegacyBlobRef}s) found within a {@link LexValue}.\n *\n * Performs a deep traversal of the input value, yielding any blob references\n * found. This is useful for extracting all media references from a record.\n *\n * @param input - The LexValue to search for blob references\n * @param options - Optional configuration for the enumeration\n * @yields Each blob reference found in the input\n *\n * @example\n * ```typescript\n * import { enumBlobRefs } from '@atproto/lex-data'\n *\n * const record = {\n *   text: 'Hello',\n *   images: [\n *     { $type: 'blob', mimeType: 'image/jpeg', ref: cid1, size: 1000 },\n *     { $type: 'blob', mimeType: 'image/png', ref: cid2, size: 2000 }\n *   ]\n * }\n *\n * for (const blobRef of enumBlobRefs(record)) {\n *   console.log(blobRef.mimeType, blobRef.size)\n * }\n *\n * // Include legacy blob references\n * for (const ref of enumBlobRefs(record, { allowLegacy: true })) {\n *   // ref may be BlobRef or LegacyBlobRef\n * }\n * ```\n */\nexport function enumBlobRefs(\n  input: LexValue,\n): Generator<BlobRef<RawCid>, void, unknown>\nexport function enumBlobRefs<TOptions extends EnumBlobRefsOptions>(\n  input: LexValue,\n  options: TOptions,\n): Generator<InferEnumBlobRefs<TOptions>, void, unknown>\nexport function enumBlobRefs(\n  input: LexValue,\n  options?: EnumBlobRefsOptions,\n): Generator<BlobRef | LegacyBlobRef, void, unknown>\nexport function* enumBlobRefs(\n  input: LexValue,\n  options?: EnumBlobRefsOptions,\n): Generator<BlobRef | LegacyBlobRef, void, unknown> {\n  // LegacyBlobRef not included by default\n  const includeLegacy = options?.allowLegacy === true\n\n  // Using a stack to avoid recursion depth issues.\n  const stack: LexValue[] = [input]\n\n  // Since we are using a stack, we could end-up in an infinite loop with cyclic\n  // structures. Cyclic structures are not valid LexValues and should, thus,\n  // never occur, but let's be safe.\n  const visited = new Set<object>()\n\n  do {\n    const value = stack.pop()!\n\n    if (value != null && typeof value === 'object') {\n      if (Array.isArray(value)) {\n        if (visited.has(value)) continue\n        visited.add(value)\n        stack.push(...value)\n      } else if (isPlainProto(value)) {\n        if (visited.has(value)) continue\n        visited.add(value)\n        if (isBlobRef(value, options)) {\n          yield value\n        } else if (includeLegacy && isLegacyBlobRef(value)) {\n          yield value\n        } else {\n          for (const v of Object.values(value)) {\n            if (v != null) stack.push(v)\n          }\n        }\n      }\n    }\n  } while (stack.length > 0)\n\n  // Optimization: ease GC's work\n  visited.clear()\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/cid-implementation.test.ts",
    "content": "import { base32 } from 'multiformats/bases/base32'\nimport { CID } from 'multiformats/cid'\nimport { create as createDigest } from 'multiformats/hashes/digest'\nimport { assert, describe, expect, it } from 'vitest'\nimport { Cid } from './cid.js'\nimport { ui8Equals } from './uint8array.js'\n\nexport class BytesCid implements Cid {\n  constructor(readonly bytes: Uint8Array) {\n    if (this.bytes.length < 4) {\n      throw new Error('CID bytes are too short')\n    }\n    if (this.bytes[0] > 1) {\n      throw new Error('Unsupported CID version')\n    }\n    if (this.bytes.length !== 4 + this.bytes[3]) {\n      throw new Error('CID bytes length mismatch')\n    }\n  }\n\n  get version() {\n    return this.bytes[0] as 0 | 1\n  }\n\n  get code() {\n    return this.bytes[1]\n  }\n\n  get multihash() {\n    const code = this.bytes[2]\n    const digest = this.bytes.subarray(4)\n    return { code, digest }\n  }\n\n  equals(other: Cid): boolean {\n    return ui8Equals(this.bytes, other.bytes)\n  }\n\n  toString(): string {\n    return base32.encode(this.bytes)\n  }\n}\n\ndescribe(BytesCid, () => {\n  it('creates a BytesCid from valid bytes', () => {\n    const bytes = new Uint8Array([1, 0x55, 0x12, 3, 1, 2, 3])\n    const cid = new BytesCid(bytes)\n\n    assert(cid.version === 1)\n    assert(cid.code === 0x55)\n    assert(cid.multihash.code === 0x12)\n    assert(ui8Equals(cid.multihash.digest, new Uint8Array([1, 2, 3])))\n    assert(ui8Equals(cid.bytes, bytes))\n    assert(typeof cid.toString === 'function')\n    assert(typeof cid.equals === 'function')\n  })\n\n  it('throws an error for invalid CID bytes', () => {\n    expect(\n      () => new BytesCid(new Uint8Array([2, 0x55, 0x12, 3, 1, 2, 3])),\n    ).toThrowError('Unsupported CID version')\n    expect(() => new BytesCid(new Uint8Array([1, 0x55, 0x12]))).toThrowError(\n      'CID bytes are too short',\n    )\n    expect(\n      () => new BytesCid(new Uint8Array([1, 0x55, 0x12, 4, 1, 2, 3])),\n    ).toThrowError('CID bytes length mismatch')\n  })\n})\n\n/**\n * A minimal custom implementation of the `Cid` interface for testing purposes.\n */\nexport function createCustomCid<\n  TVersion extends 0 | 1,\n  TCodec extends number,\n  THashCode extends number,\n>(\n  version: TVersion,\n  code: TCodec,\n  hashCode: THashCode,\n  digest: Uint8Array,\n): Cid<TVersion, TCodec, THashCode> {\n  return {\n    version,\n    code,\n    multihash: { code: hashCode, digest },\n    bytes: new Uint8Array([version, code, hashCode, digest.length, ...digest]),\n    toString,\n    equals,\n  }\n}\n\nfunction equals(this: Cid, other: Cid): boolean {\n  return (\n    this.version === other.version &&\n    this.code === other.code &&\n    this.multihash.code === other.multihash.code &&\n    ui8Equals(this.multihash.digest, other.multihash.digest)\n  )\n}\n\nfunction toString(this: Cid): string {\n  return CID.create(\n    this.version,\n    this.code,\n    createDigest(this.multihash.code, this.multihash.digest),\n  ).toString()\n}\n\ndescribe(createCustomCid, () => {\n  it('creates a CID with the specified properties', () => {\n    const digest = new Uint8Array([1, 2, 3, 4, 5])\n    const customCid = createCustomCid(1, 0x55, 0x12, digest)\n\n    assert(customCid.version === 1)\n    assert(customCid.code === 0x55)\n    assert(customCid.multihash.code === 0x12)\n    assert(ui8Equals(customCid.multihash.digest, digest))\n    assert(\n      ui8Equals(\n        customCid.bytes,\n        new Uint8Array([1, 0x55, 0x12, 5, 1, 2, 3, 4, 5]),\n      ),\n    )\n    assert(typeof customCid.toString === 'function')\n    assert(typeof customCid.equals === 'function')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/cid.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { sha256, sha512 } from 'multiformats/hashes/sha2'\nimport { describe, expect, it } from 'vitest'\nimport { BytesCid, createCustomCid } from './cid-implementation.test.js'\nimport {\n  CBOR_DATA_CODEC,\n  Cid,\n  RAW_DATA_CODEC,\n  SHA256_HASH_CODE,\n  asMultiformatsCID,\n  cidForRawHash,\n  decodeCid,\n  ensureValidCidString,\n  isCid,\n  isCidForBytes,\n  parseCid,\n  parseCidSafe,\n} from './cid.js'\nimport { ui8Equals } from './uint8array.js'\n\nconst invalidCidStr = 'invalidcidstring'\n\nconst cborCidStr = 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'\nconst cborCid = parseCid(cborCidStr, { flavor: 'cbor' })\n\nconst rawCidStr = 'bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4'\nconst rawCid = parseCid(rawCidStr, { flavor: 'raw' })\n\nconst rawCidLike: Cid = createCustomCid(\n  1,\n  RAW_DATA_CODEC,\n  SHA256_HASH_CODE,\n  rawCid.multihash.digest,\n)\nconst rawBytesCid = new BytesCid(rawCid.bytes)\n\ndescribe(isCid, () => {\n  describe('non-strict mode', () => {\n    it('returns true for parsed CIDs', () => {\n      expect(isCid(cborCid)).toBe(true)\n      expect(isCid(rawCid)).toBe(true)\n    })\n\n    it('returns true for custom compatible CID implementations', () => {\n      expect(isCid(rawCidLike)).toBe(true)\n      expect(isCid(rawBytesCid)).toBe(true)\n    })\n\n    it('returns true for CID v0 and v1', async () => {\n      const digest = await sha256.digest(Buffer.from('hello world'))\n      const cidV0 = CID.createV0(digest)\n      const cidV1 = CID.createV1(RAW_DATA_CODEC, digest)\n      expect(isCid(cidV0)).toBe(true)\n      expect(isCid(cidV1)).toBe(true)\n    })\n\n    it('returns false for invalid CIDs', () => {\n      expect(isCid(new Date())).toBe(false)\n      expect(isCid({})).toBe(false)\n      expect(isCid('not a cid')).toBe(false)\n    })\n  })\n\n  describe('flavors', () => {\n    describe('raw', () => {\n      it('validated \"raw\" cids', async () => {\n        const digest = await sha256.digest(Buffer.from('hello world'))\n        const cid = CID.createV1(RAW_DATA_CODEC, digest)\n        expect(isCid(cid, { flavor: 'raw' })).toBe(true)\n      })\n\n      it('allows other hash algorithms', async () => {\n        const digest = await sha512.digest(Buffer.from('hello world'))\n        const cid = CID.createV1(RAW_DATA_CODEC, digest)\n        expect(isCid(cid, { flavor: 'raw' })).toBe(true)\n      })\n\n      it('rejects CID v0 when strict option is set', async () => {\n        const digest = await sha256.digest(Buffer.from('hello world'))\n        const cid = CID.createV0(digest)\n        expect(isCid(cid, { flavor: 'raw' })).toBe(false)\n      })\n\n      it('rejects CIDs with invalid code', async () => {\n        const digest = await sha256.digest(Buffer.from('hello world'))\n        const cid = CID.createV1(3333, digest)\n        expect(isCid(cid, { flavor: 'raw' })).toBe(false)\n      })\n    })\n\n    describe('cbor', () => {\n      it('validated \"cbor\" cids', async () => {\n        const digest = await sha256.digest(Buffer.from('hello world'))\n        const cid = CID.createV1(CBOR_DATA_CODEC, digest)\n        expect(isCid(cid, { flavor: 'cbor' })).toBe(true)\n      })\n\n      it('rejects CIDs with invalid hash algorithm', async () => {\n        const digest = await sha512.digest(Buffer.from('hello world'))\n        const cid = CID.createV1(RAW_DATA_CODEC, digest)\n        expect(isCid(cid, { flavor: 'cbor' })).toBe(false)\n      })\n\n      it('rejects CID v0 when strict option is set', async () => {\n        const digest = await sha256.digest(Buffer.from('hello world'))\n        const cid = CID.createV0(digest)\n        expect(isCid(cid, { flavor: 'cbor' })).toBe(false)\n      })\n\n      it('rejects CIDs with invalid code', async () => {\n        const digest = await sha256.digest(Buffer.from('hello world'))\n        const cid = CID.createV1(3333, digest)\n        expect(isCid(cid, { flavor: 'cbor' })).toBe(false)\n      })\n    })\n  })\n\n  describe('alternative cid implementations', () => {\n    it('accepts compatible CID implementations', () => {\n      expect(isCid(rawCidLike)).toBe(true)\n    })\n\n    it('rejects non-matching version', () => {\n      expect(isCid({ ...rawCidLike, version: 0 })).toBe(false)\n    })\n\n    it('rejects non-matching code', () => {\n      expect(isCid({ ...rawCidLike, code: -1 })).toBe(false)\n      expect(isCid({ ...rawCidLike, code: 0 })).toBe(false)\n      expect(isCid({ ...rawCidLike, code: 256 })).toBe(false)\n    })\n\n    it('rejects invalid bytes property', () => {\n      expect(isCid({ ...rawCidLike, bytes: undefined })).toBe(false)\n      expect(isCid({ ...rawCidLike, bytes: 12 })).toBe(false)\n      expect(isCid({ ...rawCidLike, bytes: {} })).toBe(false)\n      expect(isCid({ ...rawCidLike, bytes: [] })).toBe(false)\n\n      expect(\n        isCid({\n          ...rawCidLike,\n          bytes: rawCidLike.bytes.subarray(0, rawCidLike.bytes.length - 1),\n        }),\n      ).toBe(false)\n\n      const bytes = new Uint8Array(rawCidLike.bytes.length)\n\n      bytes.set(rawCidLike.bytes)\n      expect(isCid({ ...rawCidLike, bytes })).toBe(true)\n\n      bytes[0] = bytes[0] ^ 0xff\n      expect(isCid({ ...rawCidLike, bytes })).toBe(false)\n      bytes.set(rawCidLike.bytes)\n\n      bytes[3] = bytes[3] ^ 0xff\n      expect(isCid({ ...rawCidLike, bytes })).toBe(false)\n      bytes.set(rawCidLike.bytes)\n\n      bytes[6] = bytes[6] ^ 0xff\n      expect(isCid({ ...rawCidLike, bytes })).toBe(false)\n      bytes.set(rawCidLike.bytes)\n    })\n\n    describe('multihash property', () => {\n      it('rejects non-matching object', () => {\n        expect(isCid({ ...rawCidLike, multihash: undefined })).toBe(false)\n        expect(isCid({ ...rawCidLike, multihash: 12 })).toBe(false)\n        expect(isCid({ ...rawCidLike, multihash: {} })).toBe(false)\n        expect(isCid({ ...rawCidLike, multihash: [] })).toBe(false)\n      })\n\n      it('rejects non-matching code', () => {\n        expect(\n          isCid({\n            ...rawCidLike,\n            multihash: { ...rawCidLike.multihash, code: -1 },\n          }),\n        ).toBe(false)\n        expect(\n          isCid({\n            ...rawCidLike,\n            multihash: { ...rawCidLike.multihash, code: 0 },\n          }),\n        ).toBe(false)\n        expect(\n          isCid({\n            ...rawCidLike,\n            multihash: { ...rawCidLike.multihash, code: 256 },\n          }),\n        ).toBe(false)\n      })\n\n      it('rejects non Uint8Array digest', () => {\n        expect(\n          isCid({\n            ...rawCidLike,\n            multihash: { ...rawCidLike.multihash, digest: new Array(32) },\n          }),\n        ).toBe(false)\n      })\n\n      it('rejects non Uint8Array digest', () => {\n        expect(\n          isCid({\n            ...rawCidLike,\n            multihash: { ...rawCidLike.multihash, digest: new Array(32) },\n          }),\n        ).toBe(false)\n      })\n\n      it('rejects non-matching digest', () => {\n        const differentDigest = new Uint8Array(32)\n        differentDigest[0] = 1\n        expect(\n          isCid({\n            ...rawCidLike,\n            multihash: { ...rawCidLike.multihash, digest: differentDigest },\n          }),\n        ).toBe(false)\n      })\n    })\n\n    describe('equals() method', () => {\n      it('rejects objects without equals method', () => {\n        expect(isCid({ ...rawCidLike, equals: undefined })).toBe(false)\n        expect(isCid({ ...rawCidLike, equals: () => false })).toBe(false)\n      })\n\n      it('rejects object with throwing equals method', () => {\n        expect(\n          isCid({\n            ...rawCidLike,\n            equals: () => {\n              throw new Error('fail')\n            },\n          }),\n        ).toBe(false)\n      })\n    })\n  })\n})\n\ndescribe(decodeCid, () => {\n  it('decodes CID from bytes', () => {\n    const cid = parseCid(cborCidStr)\n    const bytes = cid.bytes\n    const decodedCid = decodeCid(bytes)\n    expect(decodedCid.toString()).toBe(cborCidStr)\n  })\n})\n\ndescribe(parseCid, () => {\n  it('parses valid CIDs', () => {\n    expect(parseCid(cborCidStr).toString()).toBe(cborCidStr)\n    expect(parseCid(rawCidStr).toString()).toBe(rawCidStr)\n  })\n\n  it('throws for invalid CIDs', () => {\n    expect(() => parseCid(invalidCidStr)).toThrow()\n  })\n})\n\ndescribe(isCidForBytes, () => {\n  describe('raw', () => {\n    it('returns true for valid raw CID bytes', async () => {\n      for (const hasher of [sha256, sha512]) {\n        const data = new TextEncoder().encode('hello world')\n        const digest = await hasher.digest(data)\n        const cid = CID.createV1(RAW_DATA_CODEC, digest)\n        expect(await isCidForBytes(cid, data)).toBe(true)\n\n        data[0] = data[0] ^ 0xff\n        expect(await isCidForBytes(cid, data)).toBe(false)\n      }\n    })\n  })\n\n  describe('cbor', () => {\n    it('returns true for valid cbor CID bytes', async () => {\n      for (const hasher of [sha256, sha512]) {\n        // @NOTE this is not valid CBOR, but sufficient for testing the hash\n        const data = new TextEncoder().encode('hello world')\n        const digest = await hasher.digest(data)\n        const cid = CID.createV1(CBOR_DATA_CODEC, digest)\n        expect(await isCidForBytes(cid, data)).toBe(true)\n\n        data[0] = data[0] ^ 0xff\n        expect(await isCidForBytes(cid, data)).toBe(false)\n      }\n    })\n  })\n})\n\ndescribe(parseCidSafe, () => {\n  it('parses valid CIDs', () => {\n    expect(parseCidSafe(cborCidStr)?.toString()).toBe(cborCidStr)\n    expect(parseCidSafe(rawCidStr)?.toString()).toBe(rawCidStr)\n  })\n\n  it('returns undefined for invalid CIDs', () => {\n    expect(parseCidSafe(invalidCidStr)).toBeNull()\n  })\n})\n\ndescribe(ensureValidCidString, () => {\n  it('does not throw for valid CIDs', () => {\n    expect(() => ensureValidCidString(cborCidStr)).not.toThrow()\n  })\n\n  it('throws for invalid CIDs', () => {\n    expect(() => ensureValidCidString(invalidCidStr)).toThrow(\n      'Invalid CID string',\n    )\n  })\n})\n\ndescribe(cidForRawHash, () => {\n  it('creates a RawCid from a SHA-256 hash', () => {\n    const hash = new Uint8Array(32)\n    const cid = cidForRawHash(hash)\n    expect(cid.code).toBe(RAW_DATA_CODEC)\n    expect(cid.multihash.code).toBe(SHA256_HASH_CODE)\n    expect(ui8Equals(cid.multihash.digest, hash)).toBe(true)\n  })\n\n  it('rejects hashes on invalid lengths', () => {\n    expect(() => cidForRawHash(new Uint8Array(31))).toThrow(\n      'Invalid SHA-256 hash length',\n    )\n    expect(() => cidForRawHash(new Uint8Array(33))).toThrow(\n      'Invalid SHA-256 hash length',\n    )\n  })\n})\n\ndescribe(asMultiformatsCID, () => {\n  it('converts compatible CID to multiformats CID', () => {\n    for (const cid of [cborCid, rawCid, rawCidLike, rawBytesCid]) {\n      expect(asMultiformatsCID(cid)).toBeInstanceOf(CID)\n      expect(asMultiformatsCID(cid)).toMatchObject({\n        version: cid.version,\n        code: cid.code,\n        multihash: {\n          code: cid.multihash.code,\n          digest: cid.multihash.digest,\n        },\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/cid.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { create as createDigest } from 'multiformats/hashes/digest'\nimport { sha256, sha512 } from 'multiformats/hashes/sha2'\nimport { isUint8, toHexString } from './lib/util.js'\nimport { isObject } from './object.js'\nimport { ui8Equals } from './uint8array.js'\n\n/**\n * Codec code that indicates the CID references a CBOR-encoded data structure.\n *\n * Used when encoding structured data in AT Protocol repositories.\n *\n * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}\n */\nexport const CBOR_DATA_CODEC = 0x71\nexport type CBOR_DATA_CODEC = typeof CBOR_DATA_CODEC\n\n/**\n * Codec code that indicates the CID references raw binary data (like media blobs).\n *\n * Used in DASL CIDs for binary blobs like images and media.\n *\n * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}\n */\nexport const RAW_DATA_CODEC = 0x55\nexport type RAW_DATA_CODEC = typeof RAW_DATA_CODEC\n\n/**\n * Hash code that indicates that a CID uses SHA-256.\n */\nexport const SHA256_HASH_CODE = sha256.code\nexport type SHA256_HASH_CODE = typeof SHA256_HASH_CODE\n\n/**\n * Hash code that indicates that a CID uses SHA-512.\n */\nexport const SHA512_HASH_CODE = sha512.code\nexport type SHA512_HASH_CODE = typeof SHA512_HASH_CODE\n\n/**\n * Represent the hash part of a CID, which includes the hash algorithm code and\n * the raw digest bytes.\n *\n * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}\n */\nexport interface Multihash<THashCode extends number = number> {\n  /**\n   * Code of the hash algorithm (e.g., SHA256_HASH_CODE).\n   */\n  code: THashCode\n\n  /**\n   * Raw digest bytes.\n   */\n  digest: Uint8Array\n}\n\n/**\n * Compares two {@link Multihash} for equality.\n *\n * @param a - First {@link Multihash}\n * @param b - Second {@link Multihash}\n * @returns `true` if both multihashes have the same code and digest\n */\nexport function multihashEquals(a: Multihash, b: Multihash): boolean {\n  if (a === b) return true\n  return a.code === b.code && ui8Equals(a.digest, b.digest)\n}\n\ndeclare module 'multiformats/cid' {\n  /**\n   * @deprecated use the {@link Cid} interface from `@atproto/lex-data`, and\n   * related helpers ({@link isCid}, {@link ifCid}, {@link asCid},\n   * {@link parseCid}, {@link decodeCid}), instead.\n   *\n   * This is marked as deprecated because we want to discourage direct usage of\n   * `multiformats/cid` in dependent packages, and instead have them rely on the\n   * {@link Cid} interface from `@atproto/lex-data`. The {@link CID} class from\n   * `multiformats` version <10 has compatibility issues with certain TypeScript\n   * configuration, which can lead to type errors in dependent packages.\n   *\n   * We are stuck with version 9 because `@atproto` packages did not drop\n   * CommonJS support yet, and multiformats version 10 only supports ES modules.\n   *\n   * In order to avoid compatibility issues, while preparing for future breaking\n   * changes (CID in multiformats v10+ has a slightly different interface), as\n   * we update or swap out `multiformats`, `@atproto/lex-data` provides its own\n   * stable {@link Cid} interface.\n   */\n  interface CID {}\n}\n\n// multiformats' CID class is not very portable because:\n//\n// - In dependent packages that use \"moduleResolution\" set to \"node16\",\n//   \"nodenext\" or \"bundler\", TypeScript fails to properly resolve the\n//   multiformats package when importing CID from @atproto/lex-data. This causes\n//   type errors in those packages. This is caused by the fact that the\n//   multiformats version <10 (which is the last version that supports CommonJS)\n//   uses \"exports\" field in package.json, which do not contain \"types\"\n//   entrypoints.\n//   https://www.npmjs.com/package/multiformats/v/9.9.0?activeTab=code\n// - By defining our own interface and helper functions, we can have more\n//   control over the public API exposed by this package.\n// - It allow us to have a stable interface in case we need to swap out, or\n//   eventually update multiformats (should we choose to drop CommonJS support)\n//   in the future.\n\n// @NOTE Even though it is not portable, we still re-export CID here so that\n// dependent packages where it can be used, have access to it (instead of\n// importing directly from \"multiformats\" or \"multiformats/cid\").\nexport { /** @deprecated */ CID }\n\n/**\n * Converts a {@link Cid} to a multiformats {@link CID} instance.\n *\n * @deprecated Packages depending on `@atproto/lex-data` should use the\n * {@link Cid} interface instead of relying on `multiformats`'s {@link CID}\n * implementation directly. This is to avoid compatibility issues, and in order\n * to allow better portability, compatibility and future updates.\n */\nexport function asMultiformatsCID<\n  TVersion extends 0 | 1 = 0 | 1,\n  TCodec extends number = number,\n  THashCode extends number = number,\n>(input: Cid<TVersion, TCodec, THashCode>) {\n  const cid =\n    // Already a multiformats CID instance\n    CID.asCID(input) ??\n    // Create a new multiformats CID instance\n    CID.create(\n      input.version,\n      input.code,\n      createDigest(input.multihash.code, input.multihash.digest),\n    )\n\n  // @NOTE: the \"satisfies\" operator is used here to ensure that the Cid\n  // interface is indeed compatible with multiformats' CID implementation, which\n  // allows us to safely rely on multiformats' CID implementation where Cid are\n  // needed.\n  return cid satisfies Cid as CID & Cid<TVersion, TCodec, THashCode>\n}\n\n/**\n * Content Identifier (CID) for addressing content by its hash.\n *\n * CIDs are self-describing content addresses used throughout AT Protocol for\n * linking to data by its cryptographic hash. This interface provides a\n * stable API that is compatible with the `multiformats` library but avoids\n * direct dependency issues.\n *\n * @typeParam TVersion - CID version (0 or 1)\n * @typeParam TCodec - Multicodec content type code\n * @typeParam THashCode - Multihash algorithm code\n *\n * @example\n * ```typescript\n * import type { Cid } from '@atproto/lex-data'\n * import { parseCid, isCid } from '@atproto/lex-data'\n *\n * // Parse a CID from a string\n * const cid: Cid = parseCid('bafyreib...')\n *\n * // Check if a value is a CID\n * if (isCid(value)) {\n *   console.log(cid.toString())\n * }\n * ```\n *\n * @see {@link isCid} to check if a value is a valid CID\n * @see {@link parseCid} to parse a CID from a string\n * @see {@link decodeCid} to decode a CID from bytes\n * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}\n */\nexport interface Cid<\n  TVersion extends 0 | 1 = 0 | 1,\n  TCodec extends number = number,\n  THashCode extends number = number,\n> {\n  // @NOTE This interface is compatible with multiformats' CID implementation\n  // which we are using under the hood.\n\n  /** CID version (0 or 1). AT Protocol uses CIDv1. */\n  readonly version: TVersion\n  /** Coded (e.g., {@link CBOR_DATA_CODEC}, {@link RAW_DATA_CODEC}). */\n  readonly code: TCodec\n  /** The multihash containing the hash algorithm and digest. */\n  readonly multihash: Multihash<THashCode>\n\n  /**\n   * Binary representation of the whole CID.\n   */\n  readonly bytes: Uint8Array\n\n  /**\n   * Compares this CID with another for equality.\n   *\n   * @param other - The CID to compare with\n   * @returns `true` if the CIDs are equal\n   */\n  equals(other: Cid): boolean\n\n  /**\n   * Returns the string representation of this CID (base32 for v1, base58btc for v0).\n   */\n  toString(): string\n}\n\n/**\n * Represents the CID of raw binary data (like media blobs).\n *\n * The use of {@link SHA256_HASH_CODE} is recommended but not required for raw CIDs.\n *\n * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats AT Protocol Data Model - Link and CID Formats}\n */\nexport type RawCid = Cid<1, RAW_DATA_CODEC>\n\n/**\n * Type guard to check if a CID is a raw binary CID.\n *\n * @param cid - The CID to check\n * @returns `true` if the CID is a version 1 CID with raw multicodec\n */\nexport function isRawCid(cid: Cid): cid is RawCid {\n  return cid.version === 1 && cid.code === RAW_DATA_CODEC\n}\n\n/**\n * Represents a DASL compliant CID.\n *\n * DASL CIDs are version 1 CIDs using either raw or DAG-CBOR multicodec\n * with SHA-256 multihash.\n *\n * @see {@link https://dasl.ing/cid.html Content IDs (DASL)}\n */\nexport type DaslCid = Cid<1, RAW_DATA_CODEC | CBOR_DATA_CODEC, SHA256_HASH_CODE>\n\n/**\n * Type guard to check if a CID is DASL compliant.\n *\n * @param cid - The CID to check\n * @returns `true` if the CID is DASL compliant (v1, raw/dag-cbor, sha256)\n */\nexport function isDaslCid(cid: Cid): cid is DaslCid {\n  return (\n    cid.version === 1 &&\n    (cid.code === RAW_DATA_CODEC || cid.code === CBOR_DATA_CODEC) &&\n    cid.multihash.code === SHA256_HASH_CODE &&\n    cid.multihash.digest.byteLength === 0x20 // Should always be 32 bytes (256 bits) for SHA-256, but double-checking anyways\n  )\n}\n\n/**\n * Represents the CID of AT Protocol DAG-CBOR data (like repository MST nodes).\n *\n * CBOR CIDs are version 1 CIDs using DAG-CBOR multicodec with SHA-256 multihash.\n *\n * @see {@link https://atproto.com/specs/data-model#link-and-cid-formats AT Protocol Data Model - Link and CID Formats}\n */\nexport type CborCid = Cid<1, CBOR_DATA_CODEC, SHA256_HASH_CODE>\n\n/**\n * Type guard to check if a CID is a DAG-CBOR CID.\n *\n * @param cid - The CID to check\n * @returns `true` if the CID is a DAG-CBOR CID (v1, dag-cbor, sha256)\n */\nexport function isCborCid(cid: Cid): cid is CborCid {\n  return cid.code === CBOR_DATA_CODEC && isDaslCid(cid)\n}\n\n/**\n * Options for checking CID flavor constraints.\n */\nexport type CheckCidOptions = {\n  /**\n   * The CID flavor to check for.\n   * - `'raw'` - Raw binary CID ({@link RawCid})\n   * - `'cbor'` - DAG-CBOR CID ({@link CborCid})\n   * - `'dasl'` - DASL compliant CID ({@link DaslCid})\n   */\n  flavor?: 'raw' | 'cbor' | 'dasl'\n}\n\n/**\n * Infers the CID type based on check options.\n *\n * @typeParam TOptions - The options used for checking\n */\nexport type InferCheckedCid<TOptions extends CheckCidOptions> =\n  TOptions extends { flavor: 'raw' }\n    ? RawCid\n    : TOptions extends { flavor: 'cbor' }\n      ? CborCid\n      : Cid\n\n/**\n * Type guard to check whether a {@link Cid} instance meets specific flavor\n * constraints.\n */\nexport function checkCid<TOptions extends CheckCidOptions>(\n  cid: Cid,\n  options: TOptions,\n): cid is InferCheckedCid<TOptions>\nexport function checkCid(cid: Cid, options?: CheckCidOptions): boolean\nexport function checkCid(cid: Cid, options?: CheckCidOptions): boolean {\n  switch (options?.flavor) {\n    case undefined:\n      return true\n    case 'cbor':\n      return isCborCid(cid)\n    case 'dasl':\n      return isDaslCid(cid)\n    case 'raw':\n      return isRawCid(cid)\n    default:\n      throw new TypeError(`Unknown CID flavor: ${options?.flavor}`)\n  }\n}\n\n/**\n * Type guard to check whether a value is a valid {@link Cid} instance,\n * optionally checking for specific flavor constraints.\n */\nexport function isCid<TOptions extends CheckCidOptions>(\n  value: unknown,\n  options: TOptions,\n): value is InferCheckedCid<TOptions>\nexport function isCid(value: unknown, options?: CheckCidOptions): value is Cid\nexport function isCid(value: unknown, options?: CheckCidOptions): value is Cid {\n  return isCidImplementation(value) && checkCid(value, options)\n}\n\n/**\n * Returns the input value as a {@link Cid} if it is valid, or `null` otherwise.\n */\nexport function ifCid<TValue, TOptions extends CheckCidOptions>(\n  value: unknown,\n  options: TOptions,\n): (TValue & InferCheckedCid<TOptions>) | null\nexport function ifCid<TValue>(\n  value: TValue,\n  options?: CheckCidOptions,\n): (TValue & Cid) | null\nexport function ifCid(value: unknown, options?: CheckCidOptions): Cid | null {\n  if (isCid(value, options)) return value\n  return null\n}\n\n/**\n * Returns the input value as a {@link Cid} if it is valid.\n *\n * @throws if the input is not a valid {@link Cid}.\n */\nexport function asCid<TValue, TOptions extends CheckCidOptions>(\n  value: TValue,\n  options: TOptions,\n): TValue & InferCheckedCid<TOptions>\nexport function asCid<TValue>(\n  value: TValue,\n  options?: CheckCidOptions,\n): TValue & Cid\nexport function asCid(value: unknown, options?: CheckCidOptions): Cid {\n  if (isCid(value, options)) return value\n  throw new Error(\n    `Invalid ${options?.flavor ? `${options.flavor} CID` : 'CID'} \"${value}\"`,\n  )\n}\n\n/**\n * Decodes a CID from its binary representation.\n *\n * @see {@link https://dasl.ing/cid.html DASL-CIDs}\n * @throws if the input do not represent a valid DASL {@link Cid}\n */\nexport function decodeCid<TOptions extends CheckCidOptions>(\n  cidBytes: Uint8Array,\n  options: TOptions,\n): InferCheckedCid<TOptions>\nexport function decodeCid(cidBytes: Uint8Array, options?: CheckCidOptions): Cid\nexport function decodeCid(\n  cidBytes: Uint8Array,\n  options?: CheckCidOptions,\n): Cid {\n  const cid = CID.decode(cidBytes)\n  return asCid(cid, options)\n}\n\n/**\n * Parses a CID string into a Cid object.\n *\n * @throws if the input is not a valid CID string.\n */\nexport function parseCid<TOptions extends CheckCidOptions>(\n  input: string,\n  options: TOptions,\n): InferCheckedCid<TOptions>\nexport function parseCid(input: string, options?: CheckCidOptions): Cid\nexport function parseCid(input: string, options?: CheckCidOptions): Cid {\n  const cid = CID.parse(input)\n  return asCid(cid, options)\n}\n\n/**\n * Validates that a string is a valid CID representation.\n *\n * Unlike {@link parseCid}, this function returns a boolean instead of throwing.\n * It also verifies that the string is the canonical representation of the CID.\n *\n * @param input - The string to validate\n * @param options - Optional flavor constraints\n * @returns `true` if the string is a valid CID\n */\nexport function validateCidString(\n  input: string,\n  options?: CheckCidOptions,\n): boolean {\n  return parseCidSafe(input, options)?.toString() === input\n}\n\n/**\n * Safely parses a CID string, returning `null` on failure instead of throwing.\n *\n * @param input - The string to parse\n * @param options - Optional flavor constraints\n * @returns The parsed CID, or `null` if parsing fails\n *\n * @example\n * ```typescript\n * import { parseCidSafe } from '@atproto/lex-data'\n *\n * const cid = parseCidSafe('bafyreib...')\n * if (cid) {\n *   console.log(cid.toString())\n * }\n * ```\n */\nexport function parseCidSafe<TOptions extends CheckCidOptions>(\n  input: string,\n  options: TOptions,\n): InferCheckedCid<TOptions> | null\nexport function parseCidSafe(\n  input: string,\n  options?: CheckCidOptions,\n): Cid | null\nexport function parseCidSafe(\n  input: string,\n  options?: CheckCidOptions,\n): Cid | null {\n  try {\n    return parseCid(input, options)\n  } catch {\n    return null\n  }\n}\n\n/**\n * Ensures that a string is a valid CID representation.\n *\n * @param input - The string to validate\n * @param options - Optional flavor constraints\n * @throws If the string is not a valid CID\n */\nexport function ensureValidCidString(\n  input: string,\n  options?: CheckCidOptions,\n): void {\n  if (!validateCidString(input, options)) {\n    throw new Error(`Invalid CID string \"${input}\"`)\n  }\n}\n\n/**\n * Verifies whether the multihash of a given {@link cid} matches the hash of the provided {@link bytes}.\n * @params cid The CID to match against the bytes.\n * @params bytes The bytes to verify.\n * @returns true if the CID matches the bytes, false otherwise.\n */\nexport async function isCidForBytes(\n  cid: Cid,\n  bytes: Uint8Array,\n): Promise<boolean> {\n  if (cid.multihash.code === sha256.code) {\n    const multihash = await sha256.digest(bytes)\n    return multihashEquals(multihash, cid.multihash)\n  }\n\n  if (cid.multihash.code === sha512.code) {\n    const multihash = await sha512.digest(bytes)\n    return multihashEquals(multihash, cid.multihash)\n  }\n\n  // Don't know how to verify other multihash codes\n  throw new Error(\n    `Unsupported CID multihash code: ${toHexString(cid.multihash.code)}`,\n  )\n}\n\n/**\n * Creates a CID from a multicodec, multihash code, and digest.\n *\n * @param code - The multicodec content type code\n * @param multihashCode - The multihash algorithm code\n * @param digest - The raw hash digest bytes\n * @returns A new CIDv1 instance\n *\n * @example\n * ```typescript\n * import { createCid, RAW_DATA_CODEC, SHA256_HASH_CODE } from '@atproto/lex-data'\n *\n * const cid = createCid(RAW_DATA_CODEC, SHA256_HASH_CODE, hashDigest)\n * ```\n */\nexport function createCid<TCodec extends number, THashCode extends number>(\n  code: TCodec,\n  multihashCode: THashCode,\n  digest: Uint8Array,\n) {\n  const cid: Cid = CID.createV1(code, createDigest(multihashCode, digest))\n  return cid as Cid<1, TCodec, THashCode>\n}\n\n/**\n * Creates a DAG-CBOR CID for the given CBOR bytes.\n *\n * Computes the SHA-256 hash of the bytes and creates a CIDv1 with DAG-CBOR multicodec.\n *\n * @param bytes - The CBOR-encoded bytes to hash\n * @returns A promise that resolves to the CborCid\n */\nexport async function cidForCbor(bytes: Uint8Array): Promise<CborCid> {\n  const multihash = await sha256.digest(bytes)\n  return CID.createV1(CBOR_DATA_CODEC, multihash) as CborCid\n}\n\n/**\n * Creates a raw CID for the given binary bytes.\n *\n * Computes the SHA-256 hash of the bytes and creates a CIDv1 with raw multicodec.\n *\n * @param bytes - The raw binary bytes to hash\n * @returns A promise that resolves to the RawCid\n */\nexport async function cidForRawBytes(bytes: Uint8Array): Promise<RawCid> {\n  const multihash = await sha256.digest(bytes)\n  return CID.createV1(RAW_DATA_CODEC, multihash) as RawCid\n}\n\n/**\n * Creates a raw CID from an existing SHA-256 hash digest.\n *\n * @param digest - The SHA-256 hash digest (must be 32 bytes)\n * @returns A RawCid with the given digest\n * @throws If the digest is not a valid SHA-256 hash (not 32 bytes)\n */\nexport function cidForRawHash(digest: Uint8Array): RawCid {\n  // Fool-proofing\n  if (digest.length !== 0x20) {\n    throw new Error(\n      `Invalid SHA-256 hash length: ${toHexString(digest.length)}`,\n    )\n  }\n  return createCid(RAW_DATA_CODEC, sha256.code, digest)\n}\n\nfunction isCidImplementation(value: unknown): value is Cid {\n  if (CID.asCID(value)) {\n    // CIDs created using older multiformats versions did not have a \"bytes\"\n    // property.\n    return (value as { bytes?: Uint8Array }).bytes != null\n  } else {\n    // Unknown implementation, do a structural check\n    try {\n      if (!isObject(value)) return false\n\n      const val = value as Record<string, unknown>\n      if (val.version !== 0 && val.version !== 1) return false\n      if (!isUint8(val.code)) return false\n\n      if (!isObject(val.multihash)) return false\n      const mh = val.multihash as Record<string, unknown>\n      if (!isUint8(mh.code)) return false\n      if (!(mh.digest instanceof Uint8Array)) return false\n\n      // Ensure that the bytes array is consistent with other properties\n      if (!(val.bytes instanceof Uint8Array)) return false\n      if (val.bytes[0] !== val.version) return false\n      if (val.bytes[1] !== val.code) return false\n      if (val.bytes[2] !== mh.code) return false\n      if (val.bytes[3] !== mh.digest.length) return false\n      if (val.bytes.length !== 4 + mh.digest.length) return false\n      if (!ui8Equals(val.bytes.subarray(4), mh.digest)) return false\n\n      if (typeof val.equals !== 'function') return false\n      if (val.equals(val) !== true) return false\n\n      return true\n    } catch {\n      return false\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/index.ts",
    "content": "export * from './blob.js'\nexport * from './cid.js'\nexport * from './lex-equals.js'\nexport * from './lex-error.js'\nexport * from './lex.js'\nexport * from './object.js'\nexport * from './uint8array.js'\nexport * from './utf8.js'\n"
  },
  {
    "path": "packages/lex/lex-data/src/lex-equals.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { parseCid } from './cid.js'\nimport { lexEquals } from './lex-equals.js'\nimport { LexValue } from './lex.js'\n\nfunction expectLexEqual(a: LexValue, b: LexValue, expected: boolean) {\n  expect(lexEquals(a, b)).toBe(expected)\n  expect(lexEquals(b, a)).toBe(expected)\n}\n\ndescribe('lexEquals', () => {\n  it('compares primitive values', () => {\n    expectLexEqual(null, null, true)\n    expectLexEqual(true, true, true)\n    expectLexEqual(false, false, true)\n    expectLexEqual(42, 42, true)\n    expectLexEqual('hello', 'hello', true)\n\n    expectLexEqual(null, false, false)\n    expectLexEqual(false, null, false)\n    expectLexEqual(true, false, false)\n    expectLexEqual(false, true, false)\n    expectLexEqual(42, 43, false)\n    expectLexEqual('hello', 'world', false)\n  })\n\n  it('compares NaN and Infinity correctly', () => {\n    expectLexEqual(NaN, NaN, true)\n    expectLexEqual(Infinity, Infinity, true)\n    expectLexEqual(-Infinity, -Infinity, true)\n\n    expectLexEqual(NaN, 0, false)\n    expectLexEqual(NaN, null, false)\n    expectLexEqual(Infinity, -Infinity, false)\n  })\n\n  it('compares arrays', () => {\n    expectLexEqual([1, 2, 3], [1, 2, 3], true)\n    expectLexEqual([1, 2, 3], [1, 2, 4], false)\n    expectLexEqual([1, 2, 3], [1, 2], false)\n    expectLexEqual([1, 2, 3], 'not an array', false)\n    expectLexEqual([1, 2, 3], { 0: 1, 1: 2, 2: 3 }, false)\n  })\n\n  it('compares Uint8Arrays', () => {\n    expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]), true)\n    expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]), false)\n    expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2]), false)\n    expectLexEqual(new Uint8Array([1, 2, 3]), 'not a Uint8Array', false)\n  })\n\n  it('compares CIDs', () => {\n    const cid1 = parseCid(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    )\n    const cid2 = parseCid(cid1.toString())\n    const cid3 = parseCid(cid1.toString())\n\n    expectLexEqual(cid1, cid2, true)\n    expectLexEqual(cid1, cid3, true)\n    expectLexEqual(cid2, cid3, true)\n\n    expectLexEqual(cid1, cid1.toString(), false)\n    expectLexEqual(cid1, { not: 'a cid' }, false)\n    expectLexEqual(cid1, [], false)\n    expectLexEqual(cid1, cid1.bytes, false)\n  })\n\n  it('compares objects', () => {\n    expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 2 }, true)\n    expectLexEqual(\n      { a: 1, b: 2, c: undefined },\n      { a: 1, b: 2, c: undefined },\n      true,\n    )\n    expectLexEqual(\n      { a: 1, b: 2, c: { e: 1, d: undefined } },\n      { a: 1, b: 2, c: { d: undefined, e: 1 } },\n      true,\n    )\n    expectLexEqual(\n      { a: 1, b: { unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧' } },\n      { a: 1, b: { unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧' } },\n      true,\n    )\n\n    expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 3 }, false)\n    expectLexEqual({ a: 1, b: 2 }, { a: 1 }, false)\n    expectLexEqual({ a: 1, b: 2 }, 'not an object', false)\n    expectLexEqual({ a: 1, b: 2 }, null, false)\n  })\n\n  it('accounts for undefined (but present) properties in objects', () => {\n    expectLexEqual({ a: 1, b: undefined }, { a: 1 }, false)\n    expectLexEqual(\n      { a: 1, b: { c: undefined, d: 2 } },\n      { a: 1, b: { d: 2 } },\n      false,\n    )\n\n    expectLexEqual(\n      { a: 1, b: { c: undefined, d: 2 } },\n      { a: 1, b: { c: 3, d: 2 } },\n      false,\n    )\n  })\n\n  it('compares nested structures', () => {\n    const lex1 = {\n      foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }],\n      baz: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n    const lex2 = {\n      foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }],\n      baz: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n    const lex3 = {\n      foo: [1, 2, { bar: new Uint8Array([3, 4, 5 + 1]) }],\n      baz: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n\n    expectLexEqual(lex1, lex2, true)\n    expectLexEqual(lex1, lex3, false)\n    expectLexEqual(lex2, lex3, false)\n  })\n\n  it('allows comparing invalid numbers (floats, NaN, Infinity)', () => {\n    expectLexEqual(3.14, 2.71, false)\n    expectLexEqual(NaN, 0, false)\n    expectLexEqual(Infinity, -Infinity, false)\n  })\n\n  describe('reference equality', () => {\n    for (const value of [3.14, NaN, Infinity, -Infinity]) {\n      it(`returns true for identical references of ${String(value)}`, () => {\n        expectLexEqual(value, value, true)\n        expectLexEqual([value], [value], true)\n        expectLexEqual({ foo: value }, { foo: value }, true)\n        expectLexEqual([{ foo: value }], [{ foo: value }], true)\n      })\n    }\n  })\n\n  it('returns true for identical references', () => {\n    const arr = [1, 2, 3]\n    expectLexEqual(arr, arr, true)\n\n    const obj = { a: 1, b: 2 }\n    expectLexEqual(obj, obj, true)\n\n    const u8 = new Uint8Array([1, 2, 3])\n    expectLexEqual(u8, u8, true)\n\n    const cid = parseCid(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    )\n    expectLexEqual(cid, cid, true)\n  })\n\n  it('throws when comparing plain object with non-allowed class instance', () => {\n    // @ts-expect-error\n    expect(() => lexEquals({}, new Map())).toThrow()\n    // @ts-expect-error\n    expect(() => lexEquals(new Map(), {})).toThrow()\n    // @ts-expect-error\n    expect(() => lexEquals({ foo: {} }, { foo: new Map() })).toThrow()\n    // @ts-expect-error\n    expect(() => lexEquals({ foo: new Map() }, { foo: {} })).toThrow()\n\n    expect(() => lexEquals({ foo: {} }, { foo: new (class {})() })).toThrow()\n    expect(() => lexEquals({ foo: new (class {})() }, { foo: {} })).toThrow()\n\n    expect(() =>\n      lexEquals({ foo: {} }, { foo: new (class Object {})() }),\n    ).toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/lex-equals.ts",
    "content": "import { ifCid, isCid } from './cid.js'\nimport { LexValue } from './lex.js'\nimport { isPlainObject } from './object.js'\nimport { ui8Equals } from './uint8array.js'\n\n/**\n * Performs deep equality comparison between two {@link LexValue}s.\n *\n * This function correctly handles all Lexicon data types including:\n * - Primitives (string, number, boolean, null)\n * - Arrays (recursive element comparison)\n * - Objects/LexMaps (recursive key-value comparison)\n * - Uint8Arrays (byte-by-byte comparison)\n * - CIDs (using CID equality)\n *\n * @param a - First LexValue to compare\n * @param b - Second LexValue to compare\n * @returns `true` if the values are deeply equal\n * @throws {TypeError} If either value is not a valid LexValue (e.g., contains unsupported types)\n *\n * @example\n * ```typescript\n * import { lexEquals } from '@atproto/lex-data'\n *\n * // Primitives\n * lexEquals('hello', 'hello')  // true\n * lexEquals(42, 42)            // true\n *\n * // Arrays\n * lexEquals([1, 2, 3], [1, 2, 3])  // true\n * lexEquals([1, 2], [1, 2, 3])     // false\n *\n * // Objects\n * lexEquals({ a: 1, b: 2 }, { a: 1, b: 2 })  // true\n * lexEquals({ a: 1 }, { a: 1, b: 2 })        // false\n *\n * // CIDs\n * lexEquals(cid1, cid2)  // true if CIDs are equal\n *\n * // Uint8Arrays\n * lexEquals(new Uint8Array([1, 2]), new Uint8Array([1, 2]))  // true\n * ```\n */\nexport function lexEquals(a: LexValue, b: LexValue): boolean {\n  if (Object.is(a, b)) {\n    return true\n  }\n\n  if (\n    a == null ||\n    b == null ||\n    typeof a !== 'object' ||\n    typeof b !== 'object'\n  ) {\n    return false\n  }\n\n  if (Array.isArray(a)) {\n    if (!Array.isArray(b)) {\n      return false\n    }\n    if (a.length !== b.length) {\n      return false\n    }\n    for (let i = 0; i < a.length; i++) {\n      if (!lexEquals(a[i], b[i])) {\n        return false\n      }\n    }\n    return true\n  } else if (Array.isArray(b)) {\n    return false\n  }\n\n  if (ArrayBuffer.isView(a)) {\n    if (!ArrayBuffer.isView(b)) return false\n    return ui8Equals(a, b)\n  } else if (ArrayBuffer.isView(b)) {\n    return false\n  }\n\n  if (isCid(a)) {\n    // @NOTE CID.equals returns its argument when it is falsy (e.g. null or\n    // undefined) so we need to explicitly check that the output is \"true\".\n    return ifCid(b)?.equals(a) === true\n  } else if (isCid(b)) {\n    return false\n  }\n\n  if (!isPlainObject(a) || !isPlainObject(b)) {\n    // Foolproof (should never happen)\n    throw new TypeError(\n      'Invalid LexValue (expected CID, Uint8Array, or LexMap)',\n    )\n  }\n\n  const aKeys = Object.keys(a)\n  const bKeys = Object.keys(b)\n\n  if (aKeys.length !== bKeys.length) {\n    return false\n  }\n\n  for (const key of aKeys) {\n    const aVal = a[key]\n    const bVal = b[key]\n\n    // Needed because of the optional index signature in the Lex object type\n    // though, in practice, aVal should never be undefined here.\n    if (aVal === undefined) {\n      if (bVal === undefined && bKeys.includes(key)) continue\n      return false\n    } else if (bVal === undefined) {\n      return false\n    }\n\n    if (!lexEquals(aVal, bVal)) {\n      return false\n    }\n  }\n\n  return true\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/lex-error.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { LexError } from './lex-error.js'\n\ndescribe(LexError, () => {\n  it('stores error code and message', () => {\n    const err = new LexError('TestError', 'This is a test error')\n    expect(err.error).toBe('TestError')\n    expect(err.message).toBe('This is a test error')\n  })\n\n  it('strips empty message in toJSON output', () => {\n    const err = new LexError('TestError')\n    expect(err.toJSON()).toEqual({ error: 'TestError' })\n  })\n\n  it('includes message in toJSON when present', () => {\n    const err = new LexError('TestError', 'details here')\n    expect(err.toJSON()).toEqual({\n      error: 'TestError',\n      message: 'details here',\n    })\n  })\n\n  it('formats string output correctly', () => {\n    const err = new LexError('TestError', 'This is a test error')\n    expect(err.toString()).toBe('LexError: [TestError] This is a test error')\n  })\n\n  it('uses constructor name for the name property', () => {\n    const err = new LexError('TestError')\n    expect(err.name).toBe('LexError')\n  })\n\n  it('subclasses use their own constructor name', () => {\n    class MyCustomError extends LexError {\n      name = 'MyCustomError'\n    }\n    const err = new MyCustomError('CustomCode', 'custom message')\n    expect(err.name).toBe('MyCustomError')\n    expect(err.toString()).toBe('MyCustomError: [CustomCode] custom message')\n  })\n\n  it('preserves cause option', () => {\n    const cause = new Error('original')\n    const err = new LexError('TestError', 'wrapped', { cause })\n    expect(err.cause).toBe(cause)\n  })\n\n  it('is an instance of Error', () => {\n    const err = new LexError('TestError')\n    expect(err).toBeInstanceOf(Error)\n    expect(err).toBeInstanceOf(LexError)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/lex-error.ts",
    "content": "/**\n * Error code type for Lexicon errors.\n *\n * Error codes identify the type of error that occurred (e.g., 'InvalidRequest').\n *\n * @example\n * ```typescript\n * import type { LexErrorCode } from '@atproto/lex-data'\n *\n * const errorCode: LexErrorCode = 'InvalidRequest'\n * ```\n */\nexport type LexErrorCode = string & NonNullable<unknown>\n\n/**\n * JSON-serializable error data structure.\n *\n * This is the standard format for error responses in the AT Protocol XRPC protocol.\n *\n * @typeParam N - The specific error code type\n *\n * @example\n * ```typescript\n * import type { LexErrorData } from '@atproto/lex-data'\n *\n * const errorData: LexErrorData = {\n *   error: 'InvalidRequest',\n *   message: 'Missing required field: handle'\n * }\n * ```\n */\nexport type LexErrorData<N extends LexErrorCode = LexErrorCode> = {\n  /** The error code identifying the type of error. */\n  error: N\n  /** Optional human-readable error message. */\n  message?: string\n}\n\n/**\n * Error class for Lexicon-related errors.\n *\n * LexError extends the standard JavaScript {@link Error} with AT\n * Protocol-specific functionality including an `error` code property and\n * methods for representation as (XRPC) error responses payloads.\n *\n * @typeParam N - The specific error code type\n */\nexport class LexError<N extends LexErrorCode = LexErrorCode> extends Error {\n  name = 'LexError'\n\n  /**\n   * @param error - The error code identifying the type of error, typically used in XRPC error payloads\n   * @param message - Optional human-readable error message\n   * @param options - Standard Error options (e.g., cause)\n   */\n  constructor(\n    readonly error: N,\n    message?: string, // Defaults to empty string in Error constructor\n    options?: ErrorOptions,\n  ) {\n    super(message, options)\n  }\n\n  /**\n   * Returns a string representation of this error.\n   *\n   * @returns A formatted string: \"LexErrorClass: [MyErrorCode] My message\"\n   */\n  toString(): string {\n    return `${this.name}: [${this.error}] ${this.message}`\n  }\n\n  /**\n   * Converts this error to a JSON-serializable object.\n   *\n   * @returns The error data suitable for JSON serialization\n   * @note The `error` generic is *not* constrained to {@link N} to allow subclasses to override the error code type.\n   */\n  toJSON(): LexErrorData<LexErrorCode> {\n    const { error, message } = this\n    return { error, message: message || undefined }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/lex.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { parseCid } from './cid.js'\nimport { isLexArray, isLexScalar, isLexValue, isTypedLexMap } from './lex.js'\n\ndescribe(isLexScalar, () => {\n  for (const { note, value, expected } of [\n    { note: 'string', value: 'hello', expected: true },\n    { note: 'boolean', value: true, expected: true },\n    { note: 'null', value: null, expected: true },\n    { note: 'Uint8Array', value: new Uint8Array([1, 2, 3]), expected: true },\n    {\n      note: 'Cid',\n      value: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n      expected: true,\n    },\n    { note: 'number (integer)', value: 42, expected: true },\n    { note: 'number (float)', value: 3.14, expected: false },\n    { note: 'object', value: { a: 1 }, expected: false },\n    { note: 'array', value: [1, 2, 3], expected: false },\n    { note: 'undefined', value: undefined, expected: false },\n    { note: 'function', value: () => {}, expected: false },\n  ]) {\n    it(note, () => {\n      const result = isLexScalar(value)\n      expect(result).toBe(expected)\n    })\n  }\n})\n\ndescribe(isLexArray, () => {\n  it('returns true for valid LexArray', () => {\n    const list = [123, 'blah', true, null, new Uint8Array([1, 2, 3]), { a: 1 }]\n    expect(isLexArray(list)).toBe(true)\n  })\n\n  it('returns false for non-arrays', () => {\n    const values = [\n      123,\n      'blah',\n      true,\n      null,\n      new Uint8Array([1, 2, 3]),\n      { a: 1 },\n    ]\n    for (const value of values) {\n      expect(isLexArray(value)).toBe(false)\n    }\n  })\n\n  it('returns false for arrays with non-Lex values', () => {\n    expect(isLexArray([123, 'blah', () => {}])).toBe(false)\n    expect(isLexArray([123, 'blah', undefined])).toBe(false)\n  })\n})\n\ndescribe(isLexValue, () => {\n  describe('valid values', () => {\n    for (const { note, value } of [\n      { note: 'string', value: 'hello' },\n      { note: 'boolean', value: true },\n      { note: 'null', value: null },\n      { note: 'Uint8Array', value: new Uint8Array([1, 2, 3]) },\n      {\n        note: 'Cid',\n        value: parseCid(\n          'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        ),\n      },\n      {\n        note: 'record with Lex values',\n        value: {\n          a: 123,\n          b: 'blah',\n          c: true,\n          d: null,\n          e: new Uint8Array([1, 2, 3]),\n          f: {\n            nested: 'value',\n          },\n          g: [1, 2, 3],\n        },\n      },\n      {\n        note: 'list with Lex values',\n        value: [\n          123,\n          'blah',\n          true,\n          null,\n          new Uint8Array([1, 2, 3]),\n          {\n            nested: 'value',\n          },\n          [1, 2, 3],\n        ],\n      },\n    ]) {\n      it(note, () => {\n        expect(isLexValue(value)).toBe(true)\n      })\n    }\n  })\n\n  describe('invalid values', () => {\n    for (const { note, value } of [\n      { note: 'float', value: 123.456 },\n      { note: 'undefined', value: undefined },\n      { note: 'function', value: () => {} },\n      { note: 'obj with fn', value: { a: 123, b: () => {} } },\n      { note: 'list with non-Lex value', value: [123, 'blah', () => {}] },\n      { note: 'Date object', value: new Date() },\n      { note: 'Map object', value: new Map() },\n      { note: 'Set object', value: new Set() },\n      { note: 'class instance', value: new (class A {})() },\n    ]) {\n      it(note, () => {\n        expect(isLexValue(value)).toBe(false)\n      })\n    }\n  })\n\n  it('handles cyclic structures', () => {\n    const record: any = {\n      a: 123,\n      b: 'blah',\n    }\n    record.c = record\n\n    expect(isLexValue(record)).toBe(false)\n\n    const list: any[] = [123, 'blah']\n    list.push(list)\n\n    expect(isLexValue(list)).toBe(false)\n\n    const complex: any = {\n      a: {\n        b: [1, 2, 3],\n      },\n    }\n    complex.a.b.push(complex)\n\n    expect(isLexValue(complex)).toBe(false)\n  })\n\n  it('handles deeply nested structures', () => {\n    type Value = null | { nested: Value }\n    let value: Value = null\n    for (let i = 0; i < 1_000_000; i++) {\n      value = { nested: value }\n    }\n    expect(isLexValue(value)).toBe(true)\n  })\n})\n\ndescribe('isLexMap', () => {\n  it('returns true for valid LexMap', () => {\n    const record = {\n      a: 123,\n      b: 'blah',\n      c: true,\n      d: null,\n      e: new Uint8Array([1, 2, 3]),\n      f: {\n        nested: 'value',\n      },\n      g: [1, 2, 3],\n    }\n    expect(isTypedLexMap(record)).toBe(false)\n  })\n\n  it('returns false for non-records', () => {\n    const values = [\n      123,\n      'blah',\n      true,\n      null,\n      new Uint8Array([1, 2, 3]),\n      [1, 2, 3],\n    ]\n    for (const value of values) {\n      expect(isTypedLexMap(value)).toBe(false)\n    }\n  })\n\n  it('returns false for records with non-Lex values', () => {\n    expect(\n      // @ts-expect-error\n      isTypedLexMap({\n        a: 123,\n        b: () => {},\n      }),\n    ).toBe(false)\n    expect(\n      isTypedLexMap({\n        a: 123,\n        b: undefined,\n      }),\n    ).toBe(false)\n  })\n})\n\ndescribe('isTypedLexMap', () => {\n  describe('valid records', () => {\n    for (const { note, json } of [\n      {\n        note: 'trivial record',\n        json: {\n          $type: 'com.example.blah',\n          a: 123,\n          b: 'blah',\n        },\n      },\n      {\n        note: 'float, but integer-like',\n        json: {\n          $type: 'com.example.blah',\n          a: 123.0,\n          b: 'blah',\n        },\n      },\n      {\n        note: 'empty list and object',\n        json: {\n          $type: 'com.example.blah',\n          a: [],\n          b: {},\n        },\n      },\n    ]) {\n      it(note, () => {\n        expect(isTypedLexMap(json)).toBe(true)\n      })\n    }\n  })\n\n  describe('invalid records', () => {\n    for (const { note, json } of [\n      {\n        note: 'float',\n        json: {\n          $type: 'com.example.blah',\n          a: 123.456,\n          b: 'blah',\n        },\n      },\n      {\n        note: 'record with $type null',\n        json: {\n          $type: null,\n          a: 123,\n          b: 'blah',\n        },\n      },\n      {\n        note: 'record with $type wrong type',\n        json: {\n          $type: 123,\n          a: 123,\n          b: 'blah',\n        },\n      },\n      {\n        note: 'record with empty $type string',\n        json: {\n          $type: '',\n          a: 123,\n          b: 'blah',\n        },\n      },\n    ]) {\n      it(note, () => {\n        expect(isTypedLexMap(json)).toBe(false)\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/lex.ts",
    "content": "import { Cid, isCid } from './cid.js'\nimport { isPlainObject } from './object.js'\n\n/**\n * Primitive values in the Lexicon data model.\n *\n * Represents the basic scalar types that can appear in AT Protocol data:\n * - `number` - Integer values only (no floats)\n * - `string` - UTF-8 text\n * - `boolean` - true or false\n * - `null` - Explicit null value\n * - `Cid` - Content Identifier (link by hash)\n * - `Uint8Array` - Binary data (bytes)\n *\n * @see {@link LexValue} for the complete recursive value type\n */\nexport type LexScalar = number | string | boolean | null | Cid | Uint8Array\n\n/**\n * Any valid Lexicon value (recursive type).\n *\n * This is the union of all types that can appear in AT Protocol Lexicon data:\n * - {@link LexScalar} - Primitive values (number, string, boolean, null, Cid, Uint8Array)\n * - `LexValue[]` - Arrays of LexValues\n * - `{ [key: string]?: LexValue }` - Objects with string keys and LexValue values\n *\n * @example\n * ```typescript\n * import type { LexValue } from '@atproto/lex'\n *\n * const scalar: LexValue = 'hello'\n * const array: LexValue = [1, 2, 3]\n * const object: LexValue = { name: 'Alice', age: 30 }\n * ```\n *\n * @see {@link LexScalar} for primitive value types\n * @see {@link LexMap} for object types\n * @see {@link LexArray} for array types\n */\nexport type LexValue = LexScalar | LexValue[] | { [_ in string]?: LexValue }\n\n/**\n * Object with string keys and LexValue values.\n *\n * Represents a plain object in the Lexicon data model where all values\n * must be valid {@link LexValue} types.\n *\n * @example\n * ```typescript\n * import type { LexMap } from '@atproto/lex'\n *\n * const user: LexMap = {\n *   name: 'Alice',\n *   age: 30,\n *   tags: ['admin', 'user']\n * }\n * ```\n *\n * @see {@link TypedLexMap} for objects with a required `$type` property\n */\nexport type LexMap = { [_ in string]?: LexValue }\n\n/**\n * Array of {@link LexValue} elements.\n *\n * @example\n * ```typescript\n * import type { LexArray } from '@atproto/lex'\n *\n * const items: LexArray = [1, 'two', { three: 3 }]\n * ```\n */\nexport type LexArray = LexValue[]\n\n/**\n * Type guard to check if a value is a valid {@link LexMap}.\n *\n * Returns true if the value is a plain object where all values are valid\n * {@link LexValue} types.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid LexMap\n *\n * @example\n * ```typescript\n * import { isLexMap } from '@atproto/lex'\n *\n * if (isLexMap(data)) {\n *   // data is narrowed to LexMap\n *   console.log(Object.keys(data))\n * }\n * ```\n */\nexport function isLexMap(value: unknown): value is LexMap {\n  return isPlainObject(value) && Object.values(value).every(isLexValue)\n}\n\n/**\n * Type guard to check if a value is a valid {@link LexArray}.\n *\n * Returns true if the value is an array where all elements are valid\n * {@link LexValue} types.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid LexArray\n *\n * @example\n * ```typescript\n * import { isLexArray } from '@atproto/lex'\n *\n * if (isLexArray(data)) {\n *   // data is narrowed to LexArray\n *   data.forEach(item => console.log(item))\n * }\n * ```\n */\nexport function isLexArray(value: unknown): value is LexArray {\n  return Array.isArray(value) && value.every(isLexValue)\n}\n\n/**\n * Type guard to check if a value is a valid {@link LexScalar}.\n *\n * Returns true if the value is one of the primitive Lexicon types:\n * number (integer only), string, boolean, null, Cid, or Uint8Array.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid LexScalar\n *\n * @example\n * ```typescript\n * import { isLexScalar } from '@atproto/lex'\n *\n * isLexScalar('hello')     // true\n * isLexScalar(42)          // true\n * isLexScalar(3.14)        // false (floats not allowed)\n * isLexScalar([1, 2])      // false (arrays are not scalars)\n * ```\n */\nexport function isLexScalar(value: unknown): value is LexScalar {\n  switch (typeof value) {\n    case 'object':\n      return value === null || value instanceof Uint8Array || isCid(value)\n    case 'string':\n    case 'boolean':\n      return true\n    case 'number':\n      if (Number.isInteger(value)) return true\n    // fallthrough\n    default:\n      return false\n  }\n}\n\n/**\n * Type guard to check if a value is a valid {@link LexValue}.\n *\n * Performs a deep check to validate that the value (and all nested values)\n * conform to the Lexicon data model. This includes checking for:\n * - Valid scalar types (number, string, boolean, null, Cid, Uint8Array)\n * - Arrays containing only valid LexValues\n * - Plain objects with string keys and valid LexValue values\n * - No cyclic references (which cannot be serialized to JSON or CBOR)\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid LexValue\n *\n * @example\n * ```typescript\n * import { isLexValue } from '@atproto/lex'\n *\n * isLexValue({ name: 'Alice', tags: ['admin'] })  // true\n * isLexValue(new Date())                           // false (not a plain object)\n * isLexValue({ fn: () => {} })                     // false (functions not allowed)\n * ```\n */\nexport function isLexValue(value: unknown): value is LexValue {\n  // Using a stack to avoid recursion depth issues.\n  const stack: unknown[] = [value]\n  // Cyclic structures are not valid LexValues as they cannot be serialized to\n  // JSON or CBOR. This also allows us to avoid infinite loops when traversing\n  // the structure.\n  const visited = new Set<object>()\n\n  do {\n    const value = stack.pop()!\n\n    if (isPlainObject(value)) {\n      if (visited.has(value)) return false\n      visited.add(value)\n      stack.push(...Object.values(value))\n    } else if (Array.isArray(value)) {\n      if (visited.has(value)) return false\n      visited.add(value)\n      stack.push(...value)\n    } else {\n      if (!isLexScalar(value)) return false\n    }\n  } while (stack.length > 0)\n\n  // Optimization: ease GC's work\n  visited.clear()\n\n  return true\n}\n\n/**\n * A {@link LexMap} with a required `$type` property.\n *\n * Used to represent typed objects in the Lexicon data model, where the\n * `$type` property identifies the Lexicon schema that defines the object's\n * structure.\n *\n * @example\n * ```typescript\n * import type { TypedLexMap } from '@atproto/lex'\n *\n * const post: TypedLexMap = {\n *   $type: 'app.bsky.feed.post',\n *   text: 'Hello world!',\n *   createdAt: '2024-01-01T00:00:00Z'\n * }\n * ```\n *\n * @see {@link isTypedLexMap} to check if a value is a TypedLexMap\n */\nexport type TypedLexMap<T extends string = string> = LexMap & { $type: T }\n\n/**\n * Type guard to check if a value is a {@link TypedLexMap}.\n *\n * Returns true if the value is a valid {@link LexMap} with a non-empty\n * `$type` string property.\n *\n * @param value - The LexValue to check\n * @returns `true` if the value is a TypedLexMap\n *\n * @example\n * ```typescript\n * import { isTypedLexMap } from '@atproto/lex'\n *\n * const data = { $type: 'app.bsky.feed.post', text: 'Hello' }\n *\n * if (isTypedLexMap(data)) {\n *   console.log(data.$type)  // 'app.bsky.feed.post'\n * }\n * ```\n */\nexport function isTypedLexMap(value: LexValue): value is TypedLexMap {\n  return (\n    isLexMap(value) && typeof value.$type === 'string' && value.$type.length > 0\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/lib/nodejs-buffer.ts",
    "content": "type Encoding = 'utf8' | 'base64' | 'base64url'\n\ninterface NodeJSBuffer<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>\n  extends Uint8Array<TArrayBuffer> {\n  byteLength: number\n  toString(encoding?: Encoding): string\n}\n\ninterface NodeJSBufferConstructor {\n  new (input: string, encoding?: Encoding): NodeJSBuffer\n  from(\n    input: Uint8Array | ArrayBuffer | ArrayBufferView,\n  ): NodeJSBuffer<ArrayBuffer>\n  from(input: string, encoding?: Encoding): NodeJSBuffer<ArrayBuffer>\n  concat(list: readonly Uint8Array[], totalLength?: number): NodeJSBuffer\n  byteLength(input: string, encoding?: Encoding): number\n  prototype: NodeJSBuffer\n}\n\n// Avoids a direct reference to Node.js Buffer, which might not exist in some\n// environments (e.g. browsers, Deno, Bun) to prevent bundlers from trying to\n// include polyfills.\nconst BUFFER = /*#__PURE__*/ (() => 'Bu' + 'f'.repeat(2) + 'er')() as 'Buffer'\nexport const NodeJSBuffer: NodeJSBufferConstructor | null =\n  (globalThis as any)?.[BUFFER]?.prototype instanceof Uint8Array &&\n  'byteLength' in (globalThis as any)[BUFFER]\n    ? ((globalThis as any)[BUFFER] as NodeJSBufferConstructor)\n    : /* v8 ignore next -- @preserve */ null\n"
  },
  {
    "path": "packages/lex/lex-data/src/lib/util.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { isUint8, toHexString } from './util.js'\n\ndescribe(toHexString, () => {\n  it('converts 0 to 0x00', () => {\n    expect(toHexString(0)).toBe('0x00')\n  })\n\n  it('converts single-digit hex values with padding', () => {\n    expect(toHexString(1)).toBe('0x01')\n    expect(toHexString(15)).toBe('0x0f')\n  })\n\n  it('converts two-digit hex values without extra padding', () => {\n    expect(toHexString(16)).toBe('0x10')\n    expect(toHexString(255)).toBe('0xff')\n  })\n\n  it('converts larger numbers', () => {\n    expect(toHexString(256)).toBe('0x100')\n    expect(toHexString(4096)).toBe('0x1000')\n  })\n})\n\ndescribe(isUint8, () => {\n  it('returns true for valid uint8 values', () => {\n    expect(isUint8(0)).toBe(true)\n    expect(isUint8(1)).toBe(true)\n    expect(isUint8(127)).toBe(true)\n    expect(isUint8(255)).toBe(true)\n  })\n\n  it('returns false for values outside uint8 range', () => {\n    expect(isUint8(-1)).toBe(false)\n    expect(isUint8(256)).toBe(false)\n  })\n\n  it('returns false for non-integer numbers', () => {\n    expect(isUint8(1.5)).toBe(false)\n    expect(isUint8(0.1)).toBe(false)\n  })\n\n  it('returns false for non-number types', () => {\n    expect(isUint8('0')).toBe(false)\n    expect(isUint8(null)).toBe(false)\n    expect(isUint8(undefined)).toBe(false)\n    expect(isUint8(true)).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/lib/util.ts",
    "content": "export function toHexString(number: number): string {\n  return `0x${number.toString(16).padStart(2, '0')}`\n}\n\nexport function isUint8(val: unknown): val is number {\n  return Number.isInteger(val) && (val as number) >= 0 && (val as number) < 256\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/object.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { parseCid } from './cid.js'\nimport { isObject, isPlainObject } from './object.js'\n\ndescribe('isObject', () => {\n  it('returns true for plain objects', () => {\n    expect(isObject({})).toBe(true)\n    expect(isObject({ a: 1 })).toBe(true)\n  })\n\n  it('returns true for CIDs', () => {\n    const cid = parseCid(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    )\n    expect(isObject(cid)).toBe(true)\n  })\n\n  it('returns true for class instances', () => {\n    class MyClass {}\n    expect(isObject(new MyClass())).toBe(true)\n  })\n\n  it('returns true for arrays', () => {\n    expect(isObject([])).toBe(true)\n    expect(isObject([1, 2, 3])).toBe(true)\n  })\n\n  it('returns false for null', () => {\n    expect(isObject(null)).toBe(false)\n  })\n\n  it('returns false for non-objects', () => {\n    expect(isObject(42)).toBe(false)\n    expect(isObject('string')).toBe(false)\n    expect(isObject(undefined)).toBe(false)\n    expect(isObject(true)).toBe(false)\n  })\n})\n\ndescribe('isPlainObject', () => {\n  it('returns true for plain objects', () => {\n    expect(isPlainObject({})).toBe(true)\n    expect(isPlainObject({ a: 1 })).toBe(true)\n  })\n\n  it('returns true for objects with null prototype', () => {\n    const obj = Object.create(null)\n    obj.a = 1\n    expect(isPlainObject(obj)).toBe(true)\n    expect(isPlainObject({ __proto__: null, foo: 'bar' })).toBe(true)\n  })\n\n  it('returns false for class instances', () => {\n    class MyClass {}\n    expect(isPlainObject(new MyClass())).toBe(false)\n  })\n\n  it('returns false for CIDs', () => {\n    const cid = parseCid(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    )\n    expect(isPlainObject(cid)).toBe(false)\n  })\n\n  it('returns false for arrays', () => {\n    expect(isPlainObject([])).toBe(false)\n    expect(isPlainObject([1, 2, 3])).toBe(false)\n  })\n\n  it('returns false for null', () => {\n    expect(isPlainObject(null)).toBe(false)\n  })\n\n  it('returns false for non-objects', () => {\n    expect(isPlainObject(42)).toBe(false)\n    expect(isPlainObject('string')).toBe(false)\n    expect(isPlainObject(undefined)).toBe(false)\n    expect(isPlainObject(true)).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/object.ts",
    "content": "/**\n * Checks whether the input is an object (not null).\n *\n * Returns true for any non-null value with typeof 'object', including\n * arrays, plain objects, class instances, etc.\n *\n * @param input - The value to check\n * @returns `true` if the input is an object (not null)\n *\n * @example\n * ```typescript\n * import { isObject } from '@atproto/lex-data'\n *\n * isObject({})           // true\n * isObject([1, 2, 3])    // true\n * isObject(new Date())   // true\n * isObject(null)         // false\n * isObject('string')     // false\n * ```\n */\nexport function isObject(input: unknown): input is object {\n  return input != null && typeof input === 'object'\n}\n\nconst ObjectProto = Object.prototype\nconst ObjectToString = Object.prototype.toString\n\n/**\n * Checks whether the input is a plain object.\n *\n * A plain object is an object (not null) whose prototype is either null\n * or `Object.prototype`. This excludes arrays, class instances, and other\n * special objects.\n *\n * @param input - The value to check\n * @returns `true` if the input is a plain object\n *\n * @example\n * ```typescript\n * import { isPlainObject } from '@atproto/lex-data'\n *\n * isPlainObject({})                    // true\n * isPlainObject({ a: 1 })              // true\n * isPlainObject(Object.create(null))   // true\n * isPlainObject([1, 2, 3])             // false\n * isPlainObject(new Date())            // false\n * isPlainObject(null)                  // false\n * ```\n */\nexport function isPlainObject(input: unknown) {\n  return isObject(input) && isPlainProto(input)\n}\n\n/**\n * Checks whether the prototype of an object is plain (null or Object.prototype).\n *\n * This is useful for checking if an object is a plain object without\n * checking that it's non-null first (the null check is already done).\n *\n * @param input - The object to check (must be non-null)\n * @returns `true` if the object's prototype is plain\n *\n * @example\n * ```typescript\n * import { isPlainProto } from '@atproto/lex-data'\n *\n * isPlainProto({})                    // true\n * isPlainProto(Object.create(null))   // true\n * isPlainProto([1, 2, 3])             // false (Array.prototype)\n * isPlainProto(new Date())            // false (Date.prototype)\n * ```\n */\nexport function isPlainProto(input: object): input is Record<string, unknown> {\n  const proto = Object.getPrototypeOf(input)\n  if (proto === null) return true\n  return (\n    (proto === ObjectProto ||\n      // Needed to support NodeJS's `runInNewContext` which produces objects\n      // with a different prototype\n      Object.getPrototypeOf(proto) === null) &&\n    ObjectToString.call(input) === '[object Object]'\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-base64.ts",
    "content": "/** @default 'base64' */\nexport type Base64Alphabet = 'base64' | 'base64url'\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-concat.test.ts",
    "content": "import { assert, describe, expect, it } from 'vitest'\nimport { ui8ConcatNode, ui8ConcatPonyfill } from './uint8array-concat.js'\nimport { ui8Equals } from './uint8array.js'\n\nfor (const ui8Concat of [ui8ConcatNode, ui8ConcatPonyfill] as const) {\n  // Tests should run in NodeJS where implementations are available.\n  assert(ui8Concat, 'ui8Concat implementation should not be undefined')\n\n  describe(ui8Concat.name, () => {\n    describe('empty array', () => {\n      it('returns empty Uint8Array when given empty array', () => {\n        const result = ui8Concat([])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(result.length).toBe(0)\n      })\n    })\n\n    describe('single array', () => {\n      it('returns copy of single Uint8Array', () => {\n        const input = new Uint8Array([1, 2, 3])\n        const result = ui8Concat([input])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, input)).toBe(true)\n      })\n\n      it('returns copy of single empty Uint8Array', () => {\n        const input = new Uint8Array(0)\n        const result = ui8Concat([input])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(result.length).toBe(0)\n      })\n    })\n\n    describe('multiple arrays', () => {\n      it('concatenates two arrays', () => {\n        const a = new Uint8Array([1, 2, 3])\n        const b = new Uint8Array([4, 5, 6])\n        const result = ui8Concat([a, b])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([1, 2, 3, 4, 5, 6]))).toBe(true)\n      })\n\n      it('concatenates three arrays', () => {\n        const a = new Uint8Array([1, 2])\n        const b = new Uint8Array([3, 4])\n        const c = new Uint8Array([5, 6])\n        const result = ui8Concat([a, b, c])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([1, 2, 3, 4, 5, 6]))).toBe(true)\n      })\n\n      it('concatenates many arrays', () => {\n        const arrays = [\n          new Uint8Array([0x00]),\n          new Uint8Array([0x01, 0x02]),\n          new Uint8Array([0x03, 0x04, 0x05]),\n          new Uint8Array([0x06, 0x07, 0x08, 0x09]),\n          new Uint8Array([0x0a]),\n        ]\n        const result = ui8Concat(arrays)\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(\n          ui8Equals(\n            result,\n            new Uint8Array([\n              0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,\n            ]),\n          ),\n        ).toBe(true)\n      })\n    })\n\n    describe('arrays with different lengths', () => {\n      it('concatenates arrays of varying lengths', () => {\n        const a = new Uint8Array([1])\n        const b = new Uint8Array([2, 3, 4, 5, 6])\n        const c = new Uint8Array([7, 8])\n        const result = ui8Concat([a, b, c])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(\n          ui8Equals(result, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])),\n        ).toBe(true)\n      })\n\n      it('handles large arrays', () => {\n        const size = 10000\n        const a = new Uint8Array(size).fill(0xaa)\n        const b = new Uint8Array(size).fill(0xbb)\n        const result = ui8Concat([a, b])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(result.length).toBe(size * 2)\n        // Check first array portion\n        for (let i = 0; i < size; i++) {\n          expect(result[i]).toBe(0xaa)\n        }\n        // Check second array portion\n        for (let i = size; i < size * 2; i++) {\n          expect(result[i]).toBe(0xbb)\n        }\n      })\n    })\n\n    describe('empty Uint8Arrays in input', () => {\n      it('handles empty array at the beginning', () => {\n        const a = new Uint8Array(0)\n        const b = new Uint8Array([1, 2, 3])\n        const result = ui8Concat([a, b])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([1, 2, 3]))).toBe(true)\n      })\n\n      it('handles empty array in the middle', () => {\n        const a = new Uint8Array([1, 2])\n        const b = new Uint8Array(0)\n        const c = new Uint8Array([3, 4])\n        const result = ui8Concat([a, b, c])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([1, 2, 3, 4]))).toBe(true)\n      })\n\n      it('handles empty array at the end', () => {\n        const a = new Uint8Array([1, 2, 3])\n        const b = new Uint8Array(0)\n        const result = ui8Concat([a, b])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([1, 2, 3]))).toBe(true)\n      })\n\n      it('handles multiple empty arrays', () => {\n        const a = new Uint8Array(0)\n        const b = new Uint8Array([1])\n        const c = new Uint8Array(0)\n        const d = new Uint8Array([2])\n        const e = new Uint8Array(0)\n        const result = ui8Concat([a, b, c, d, e])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([1, 2]))).toBe(true)\n      })\n\n      it('handles all empty arrays', () => {\n        const a = new Uint8Array(0)\n        const b = new Uint8Array(0)\n        const c = new Uint8Array(0)\n        const result = ui8Concat([a, b, c])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(result.length).toBe(0)\n      })\n    })\n\n    describe('byte value preservation', () => {\n      it('preserves all byte values from 0x00 to 0xff', () => {\n        const allBytes = new Uint8Array(256)\n        for (let i = 0; i < 256; i++) {\n          allBytes[i] = i\n        }\n        const result = ui8Concat([allBytes])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, allBytes)).toBe(true)\n      })\n\n      it('preserves boundary byte values in concatenation', () => {\n        const a = new Uint8Array([0x00, 0x01, 0x7f])\n        const b = new Uint8Array([0x80, 0xfe, 0xff])\n        const result = ui8Concat([a, b])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(\n          ui8Equals(\n            result,\n            new Uint8Array([0x00, 0x01, 0x7f, 0x80, 0xfe, 0xff]),\n          ),\n        ).toBe(true)\n      })\n    })\n\n    describe('subarray handling', () => {\n      it('correctly concatenates subarrays', () => {\n        const fullArray = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])\n        const sub1 = fullArray.subarray(0, 3) // [0, 1, 2]\n        const sub2 = fullArray.subarray(5, 8) // [5, 6, 7]\n        const result = ui8Concat([sub1, sub2])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([0, 1, 2, 5, 6, 7]))).toBe(true)\n      })\n\n      it('correctly concatenates subarray with regular array', () => {\n        const fullArray = new Uint8Array([10, 20, 30, 40, 50])\n        const sub = fullArray.subarray(1, 4) // [20, 30, 40]\n        const regular = new Uint8Array([100, 200])\n        const result = ui8Concat([sub, regular])\n        expect(result).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(result, new Uint8Array([20, 30, 40, 100, 200]))).toBe(\n          true,\n        )\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-concat.ts",
    "content": "import { NodeJSBuffer } from './lib/nodejs-buffer.js'\n\nconst Buffer = NodeJSBuffer\n\nexport const ui8ConcatNode = Buffer\n  ? function ui8ConcatNode(array: readonly Uint8Array[]): Uint8Array {\n      return Buffer.concat(array)\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nexport function ui8ConcatPonyfill(array: readonly Uint8Array[]): Uint8Array {\n  let totalLength = 0\n  for (const arr of array) totalLength += arr.length\n  const result = new Uint8Array(totalLength)\n  let offset = 0\n  for (const arr of array) {\n    result.set(arr, offset)\n    offset += arr.length\n  }\n  return result\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-from-base64.test.ts",
    "content": "import 'core-js/modules/es.uint8-array.from-base64.js'\nimport 'core-js/modules/es.uint8-array.to-base64.js'\n\nimport { assert, describe, expect, it } from 'vitest'\nimport {\n  fromBase64Native,\n  fromBase64Node,\n  fromBase64Ponyfill,\n} from './uint8array-from-base64.js'\nimport { ui8Equals } from './uint8array.js'\n\n// @NOTE This test suite relies on the NodeJS Buffer implementation to generate\n// valid base64 strings for testing.\n\n// @NOTE b64 needs a test suite because fromBase64 implementations differ in\n// their behavior when encountering invalid base64 strings. This is not the case\n// for toBase64, which is straightforward and has no edge cases.\n\nfor (const fromBase64 of [\n  fromBase64Native,\n  fromBase64Node,\n  fromBase64Ponyfill,\n] as const) {\n  // Tests should run in NodeJS where implementations are either available or\n  // polyfilled (see core-js imports above).\n  assert(fromBase64 !== null, 'fromBase64 implementation should not be null')\n\n  describe(fromBase64.name, () => {\n    describe('valid base64 strings', () => {\n      it('decodes empty string', () => {\n        const decoded = fromBase64('')\n        expect(decoded).toBeInstanceOf(Uint8Array)\n        expect(decoded.length).toBe(0)\n      })\n\n      it('decodes 10MB', () => {\n        const bytes = Buffer.allocUnsafe(10_000_000).fill('🐩')\n        const encoded = bytes.toString('base64')\n        const decoded = fromBase64(encoded)\n        expect(decoded).toBeInstanceOf(Uint8Array)\n        expect(ui8Equals(decoded, bytes)).toBe(true)\n      })\n\n      for (const buffer of [\n        Buffer.from(''),\n        Buffer.from('\\0\\0'),\n        Buffer.from('\\0\\0\\0'),\n        Buffer.from('\\0\\0\\0\\0'),\n        Buffer.from('__'),\n        Buffer.from('é'),\n        Buffer.from('àç'),\n        Buffer.from('\\0éàç'),\n        Buffer.from('```\u001b'),\n        Buffer.from('aaa'),\n        Buffer.from('Hello, World!'),\n        Buffer.from('😀😃😄😁😆😅😂🤣😊😇'),\n        Buffer.from('👩‍💻👨‍💻👩‍🔬👨‍🔬👩‍🚀👨‍🚀'),\n        Buffer.from('🌍🌎🌏🌐🪐🌟✨⚡🔥💧'),\n        Buffer.from(new Uint8Array([0xfb, 0xff, 0xbf])),\n        Buffer.from(new Uint8Array([0xfb, 0xff, 0xbf])),\n        Buffer.from(new Uint8Array([0x4d])),\n        Buffer.from(new Uint8Array([0x4d, 0x61])),\n        Buffer.from(new Uint8Array([0x4d, 0x61, 0x6e])),\n        Buffer.from(new Uint8Array([0x4d])),\n        Buffer.from(new Uint8Array([0x4d, 0x61])),\n        Buffer.from(new Uint8Array([0x00, 0x4d, 0x61, 0x6e, 0x00])),\n      ]) {\n        const base64 = buffer.toString('base64')\n        const base64Unpadded = base64.replace(/=+$/, '')\n        const base64url = buffer.toString('base64url') // No padding in base64url\n\n        it(`decodes ${JSON.stringify(base64)}`, () => {\n          const decoded = fromBase64(base64)\n          expect(decoded).toBeInstanceOf(Uint8Array)\n          expect(ui8Equals(decoded, buffer)).toBe(true)\n        })\n\n        it(`decodes ${JSON.stringify(base64url)} (base64url)`, () => {\n          const decoded = fromBase64(base64url, 'base64url')\n          expect(decoded).toBeInstanceOf(Uint8Array)\n          expect(ui8Equals(decoded, buffer)).toBe(true)\n        })\n\n        if (base64 !== base64Unpadded) {\n          it(`decodes ${JSON.stringify(base64Unpadded)} (unpadded)`, () => {\n            const decoded = fromBase64(base64Unpadded)\n            expect(decoded).toBeInstanceOf(Uint8Array)\n            expect(ui8Equals(decoded, buffer)).toBe(true)\n          })\n        }\n      }\n    })\n\n    describe('invalid base64 strings', () => {\n      for (const invalidB64 of [\n        'çç',\n        'é',\n        'YWJjZGU$$$',\n        '@@@@',\n        'abcd!',\n        'ab=cd',\n        // \"YWFh\" is \"aaa\" in base64\n        'YWFh' + 'é',\n        'YWFh' + 'éé',\n        'YWFh' + 'ééé',\n        'YWFh' + 'éééé',\n        // Invalid padding\n        'YWFh' + '=',\n        'YWFh' + '==',\n        'YWFh' + '===',\n        'YWFh' + '====',\n        'YWFh' + '=====',\n        'YWFh' + '======',\n        'TWEé',\n        'TWE👍',\n        // More invalid padding\n        // 'TWE=', // 'Ma'\n        'TWE=' + '=',\n        'TWE=' + '==',\n        // 'TQ==', // 'M'\n        'TQ==' + '=',\n        'TQ==' + '==',\n      ] as const) {\n        it(`throws on invalid base64 string \"${invalidB64}\"`, () => {\n          expect(() => fromBase64(invalidB64)).toThrow()\n        })\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-from-base64.ts",
    "content": "import { fromString } from 'uint8arrays/from-string'\nimport { NodeJSBuffer } from './lib/nodejs-buffer.js'\nimport { Base64Alphabet } from './uint8array-base64.js'\n\nconst Buffer = NodeJSBuffer\n\ndeclare global {\n  interface Uint8ArrayConstructor {\n    /**\n     * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64 Uint8Array.fromBase64()}\n     */\n    fromBase64?: (\n      b64: string,\n      options?: {\n        /** @default 'base64' */\n        alphabet?: 'base64' | 'base64url'\n        lastChunkHandling?: 'loose' | 'strict' | 'stop-before-partial'\n      },\n    ) => Uint8Array\n  }\n}\n\nexport const fromBase64Native =\n  typeof Uint8Array.fromBase64 === 'function'\n    ? function fromBase64Native(\n        b64: string,\n        alphabet: Base64Alphabet = 'base64',\n      ): Uint8Array {\n        return Uint8Array.fromBase64!(b64, {\n          alphabet,\n          lastChunkHandling: 'loose',\n        })\n      }\n    : /* v8 ignore next -- @preserve */ null\n\nexport const fromBase64Node = Buffer\n  ? function fromBase64Node(\n      b64: string,\n      alphabet: Base64Alphabet = 'base64',\n    ): Uint8Array {\n      const bytes = Buffer.from(b64, alphabet)\n      verifyBase64ForBytes(b64, bytes)\n      // Convert to Uint8Array because even though Buffer is a sub class of\n      // Uint8Array, it serializes differently to Uint8Array (e.g. in JSON) and\n      // results in unexpected behavior downstream (e.g. in tests)\n      return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nexport function fromBase64Ponyfill(\n  b64: string,\n  alphabet: Base64Alphabet = 'base64',\n): Uint8Array {\n  const bytes = fromString(b64, b64.endsWith('=') ? `${alphabet}pad` : alphabet)\n  verifyBase64ForBytes(b64, bytes)\n  return bytes\n}\n\n// @NOTE NodeJS will silently stop decoding at the first invalid character,\n// while \"uint8arrays/from-string\" will not validate that the padding is\n// correct. The following function performs basic validation to ensure that the\n// input was a valid base64 string. The availability of the \"bytes\" allows\n// to perform checks with O[1] complexity.\nfunction verifyBase64ForBytes(b64: string, bytes: Uint8Array): void {\n  const paddingCount = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0\n  const trimmedLength = b64.length - paddingCount\n  const expectedByteLength = Math.floor((trimmedLength * 3) / 4)\n  if (bytes.length !== expectedByteLength) {\n    throw new Error('Invalid base64 string')\n  }\n\n  const expectedB64Length = (bytes.length / 3) * 4\n  const expectedPaddingCount =\n    expectedB64Length % 4 === 0 ? 0 : 4 - (expectedB64Length % 4)\n  const expectedFullB64Length = expectedB64Length + expectedPaddingCount\n  if (b64.length > expectedFullB64Length) {\n    throw new Error('Invalid base64 string')\n  }\n\n  // The previous might still allow false positive if only the last few\n  // chars are invalid.\n  for (\n    let i = Math.ceil(expectedB64Length);\n    i < b64.length - paddingCount;\n    i++\n  ) {\n    const code = b64.charCodeAt(i)\n    if (\n      !(code >= 65 && code <= 90) && // A-Z\n      !(code >= 97 && code <= 122) && // a-z\n      !(code >= 48 && code <= 57) && // 0-9\n      code !== 43 && // +\n      code !== 47 // /\n    ) {\n      throw new Error('Invalid base64 string')\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-to-base64.test.ts",
    "content": "import 'core-js/modules/es.uint8-array.from-base64.js'\nimport 'core-js/modules/es.uint8-array.to-base64.js'\nimport { assert, describe, expect, it } from 'vitest'\nimport {\n  toBase64Native,\n  toBase64Node,\n  toBase64Ponyfill,\n} from './uint8array-to-base64.js'\n\nfor (const toBase64 of [\n  toBase64Native,\n  toBase64Node,\n  toBase64Ponyfill,\n] as const) {\n  // Tests should run in NodeJS where implementations are either available or\n  // polyfilled (see core-js imports above).\n  assert(toBase64, 'toBase64 implementation should not be null')\n\n  describe(toBase64, () => {\n    describe('basic encoding', () => {\n      it('encodes empty Uint8Array', () => {\n        const encoded = toBase64(new Uint8Array(0))\n        expect(typeof encoded).toBe('string')\n        expect(encoded).toBe('')\n      })\n\n      it('encodes 10MB', () => {\n        const bytes = Buffer.allocUnsafe(10_000_000).fill('🐩')\n        const encoded = toBase64(bytes)\n        expect(typeof encoded).toBe('string')\n        // Verify by decoding back\n        const decoded = Buffer.from(encoded, 'base64')\n        expect(decoded.equals(bytes)).toBe(true)\n      })\n    })\n\n    describe('base64 alphabet (default)', () => {\n      for (const string of [\n        '',\n        '\\0\\0',\n        '\\0\\0\\0',\n        '\\0\\0\\0\\0',\n        '__',\n        'é',\n        'àç',\n        '\\0éàç',\n        '```\\x1b',\n        'aaa',\n        'Hello, World!',\n        '😀😃😄😁😆😅😂🤣😊😇',\n        '👩‍💻👨‍💻👩‍🔬👨‍🔬👩‍🚀👨‍🚀',\n        '🌍🌎🌏🌐🪐🌟✨⚡🔥💧',\n      ] as const) {\n        const buffer = Buffer.from(string, 'utf8')\n        const expected = buffer.toString('base64').replace(/=+$/, '')\n\n        it(`encodes ${JSON.stringify(string)} as ${JSON.stringify(expected)}`, () => {\n          const encoded = toBase64(buffer)\n          expect(encoded).toBe(expected)\n        })\n      }\n    })\n\n    describe('base64url alphabet', () => {\n      for (const string of [\n        '',\n        '\\0\\0',\n        '\\0\\0\\0',\n        '\\0\\0\\0\\0',\n        '__',\n        'é',\n        'àç',\n        '\\0éàç',\n        '```\\x1b',\n        'aaa',\n        'Hello, World!',\n        '😀😃😄😁😆😅😂🤣😊😇',\n        '👩‍💻👨‍💻👩‍🔬👨‍🔬👩‍🚀👨‍🚀',\n        '🌍🌎🌏🌐🪐🌟✨⚡🔥💧',\n      ] as const) {\n        const buffer = Buffer.from(string, 'utf8')\n        const expected = buffer.toString('base64url')\n\n        it(`encodes ${JSON.stringify(string)} as ${JSON.stringify(expected)}`, () => {\n          const encoded = toBase64(buffer, 'base64url')\n          expect(encoded).toBe(expected)\n        })\n      }\n    })\n\n    describe('base64 vs base64url character differences', () => {\n      // Test data that produces + and / in standard base64\n      // These should become - and _ in base64url\n      it('uses + and / for base64 alphabet', () => {\n        // 0xfb, 0xff, 0xbf produces \"+/+/\" in base64\n        const bytes = new Uint8Array([0xfb, 0xff, 0xbf])\n        const encoded = toBase64(bytes)\n        expect(encoded).toContain('+')\n        expect(encoded).toContain('/')\n        expect(encoded).not.toContain('-')\n        expect(encoded).not.toContain('_')\n      })\n\n      it('uses - and _ for base64url alphabet', () => {\n        // Same bytes should use - and _ in base64url\n        const bytes = new Uint8Array([0xfb, 0xff, 0xbf])\n        const encoded = toBase64(bytes, 'base64url')\n        expect(encoded).toContain('-')\n        expect(encoded).toContain('_')\n        expect(encoded).not.toContain('+')\n        expect(encoded).not.toContain('/')\n      })\n    })\n\n    describe('padding behavior', () => {\n      it('omits padding by default for 1-byte input', () => {\n        // 1 byte -> 2 base64 chars + 2 padding\n        const bytes = new Uint8Array([0x4d]) // 'M' -> 'TQ=='\n        const encoded = toBase64(bytes)\n        expect(encoded).toBe('TQ')\n        expect(encoded).not.toContain('=')\n      })\n\n      it('omits padding by default for 2-byte input', () => {\n        // 2 bytes -> 3 base64 chars + 1 padding\n        const bytes = new Uint8Array([0x4d, 0x61]) // 'Ma' -> 'TWE='\n        const encoded = toBase64(bytes)\n        expect(encoded).toBe('TWE')\n        expect(encoded).not.toContain('=')\n      })\n\n      it('no padding needed for 3-byte input', () => {\n        // 3 bytes -> 4 base64 chars, no padding needed\n        const bytes = new Uint8Array([0x4d, 0x61, 0x6e]) // 'Man' -> 'TWFu'\n        const encoded = toBase64(bytes)\n        expect(encoded).toBe('TWFu')\n      })\n\n      it('includes double padding when omitPadding: false for 1-byte input', () => {\n        const bytes = new Uint8Array([0x4d]) // 'M' -> 'TQ=='\n        const encoded = toBase64(bytes)\n        expect(encoded).toBe('TQ')\n      })\n\n      it('includes single padding when omitPadding: false for 2-byte input', () => {\n        const bytes = new Uint8Array([0x4d, 0x61]) // 'Ma' -> 'TWE='\n        const encoded = toBase64(bytes)\n        expect(encoded).toBe('TWE')\n      })\n    })\n\n    describe('Uint8Array subarray handling', () => {\n      it('correctly encodes a subarray', () => {\n        const fullArray = new Uint8Array([0x00, 0x4d, 0x61, 0x6e, 0x00])\n        const subarray = fullArray.subarray(1, 4) // 'Man'\n        const encoded = toBase64(subarray)\n        expect(encoded).toBe('TWFu')\n      })\n\n      it('correctly encodes a subarray with offset', () => {\n        const buffer = new ArrayBuffer(10)\n        const fullView = new Uint8Array(buffer)\n        fullView.set([0x00, 0x00, 0x4d, 0x61, 0x6e, 0x00, 0x00])\n        const subarray = new Uint8Array(buffer, 2, 3) // 'Man' at offset 2\n        const encoded = toBase64(subarray)\n        expect(encoded).toBe('TWFu')\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array-to-base64.ts",
    "content": "import { toString } from 'uint8arrays/to-string'\nimport { NodeJSBuffer } from './lib/nodejs-buffer.js'\nimport { Base64Alphabet } from './uint8array-base64.js'\n\nconst Buffer = NodeJSBuffer\n\ndeclare global {\n  interface Uint8Array {\n    /**\n     * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64 Uint8Array.prototype.toBase64()}\n     */\n    toBase64?: (options?: {\n      /** @default 'base64' */\n      alphabet?: 'base64' | 'base64url'\n      omitPadding?: boolean\n    }) => string\n  }\n}\n\nexport const toBase64Native =\n  typeof Uint8Array.prototype.toBase64 === 'function'\n    ? function toBase64Native(\n        bytes: Uint8Array,\n        alphabet: Base64Alphabet = 'base64',\n      ): string {\n        return bytes.toBase64!({ alphabet, omitPadding: true })\n      }\n    : /* v8 ignore next -- @preserve */ null\n\nexport const toBase64Node = Buffer\n  ? function toBase64Node(\n      bytes: Uint8Array,\n      alphabet: Base64Alphabet = 'base64',\n    ): string {\n      const buffer = bytes instanceof Buffer ? bytes : Buffer.from(bytes)\n      const b64 = buffer.toString(alphabet)\n\n      // @NOTE We strip padding for strict compatibility with\n      // uint8arrays.toString behavior. Tests failing because of the presence of\n      // padding are not really synonymous with an actual error and we might\n      // (should?) actually want to keep the padding at some point.\n      return b64.charCodeAt(b64.length - 1) === /* '=' */ 0x3d\n        ? b64.charCodeAt(b64.length - 2) === /* '=' */ 0x3d\n          ? b64.slice(0, -2) // '=='\n          : b64.slice(0, -1) // '='\n        : b64\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nexport function toBase64Ponyfill(\n  bytes: Uint8Array,\n  alphabet: Base64Alphabet = 'base64',\n): string {\n  return toString(bytes, alphabet)\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array.test.ts",
    "content": "import 'core-js/modules/es.uint8-array.from-base64.js'\nimport 'core-js/modules/es.uint8-array.to-base64.js'\n\nimport { describe, expect, it } from 'vitest'\nimport {\n  asUint8Array,\n  fromBase64,\n  ifUint8Array,\n  toBase64,\n  ui8Concat,\n  ui8Equals,\n} from './uint8array.js'\n\ndescribe(toBase64, () => {\n  it('encodes empty Uint8Array', () => {\n    const encoded = toBase64(new Uint8Array(0))\n    expect(typeof encoded).toBe('string')\n    expect(encoded).toBe('')\n  })\n\n  it('encodes single byte', () => {\n    const encoded = toBase64(new Uint8Array([0x4d]))\n    expect(encoded).toBe('TQ')\n  })\n\n  it('encodes multiple bytes', () => {\n    const encoded = toBase64(new Uint8Array([0x4d, 0x61, 0x6e]))\n    expect(encoded).toBe('TWFu')\n  })\n\n  it('encodes with default alphabet (base64)', () => {\n    const bytes = new Uint8Array([0xfb, 0xff, 0xbf])\n    const encoded = toBase64(bytes)\n    expect(encoded).toContain('+')\n    expect(encoded).toContain('/')\n  })\n\n  it('encodes with base64url alphabet', () => {\n    const bytes = new Uint8Array([0xfb, 0xff, 0xbf])\n    const encoded = toBase64(bytes, 'base64url')\n    expect(encoded).toContain('-')\n    expect(encoded).toContain('_')\n  })\n\n  it('handles large data', () => {\n    const bytes = new Uint8Array(10000).fill(0xaa)\n    const encoded = toBase64(bytes)\n    expect(typeof encoded).toBe('string')\n    expect(encoded.length).toBeGreaterThan(0)\n  })\n})\n\ndescribe(fromBase64, () => {\n  it('decodes empty string', () => {\n    const decoded = fromBase64('')\n    expect(decoded).toBeInstanceOf(Uint8Array)\n    expect(decoded.length).toBe(0)\n  })\n\n  it('decodes single character', () => {\n    const decoded = fromBase64('TQ')\n    expect(decoded).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(decoded, new Uint8Array([0x4d]))).toBe(true)\n  })\n\n  it('decodes multiple characters', () => {\n    const decoded = fromBase64('TWFu')\n    expect(decoded).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(decoded, new Uint8Array([0x4d, 0x61, 0x6e]))).toBe(true)\n  })\n\n  it('decodes base64url alphabet', () => {\n    const decoded = fromBase64('-_-_', 'base64url')\n    expect(decoded).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(decoded, new Uint8Array([0xfb, 0xff, 0xbf]))).toBe(true)\n  })\n\n  it('decodes padded base64', () => {\n    const decoded = fromBase64('TQ==')\n    expect(decoded).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(decoded, new Uint8Array([0x4d]))).toBe(true)\n  })\n\n  it('decodes unpadded base64', () => {\n    const decoded = fromBase64('TQ')\n    expect(decoded).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(decoded, new Uint8Array([0x4d]))).toBe(true)\n  })\n\n  it('throws on invalid base64 string', () => {\n    expect(() => fromBase64('@@@@')).toThrow()\n  })\n\n  it('handles large data', () => {\n    const bytes = new Uint8Array(10000).fill(0xbb)\n    const encoded = toBase64(bytes)\n    const decoded = fromBase64(encoded)\n    expect(ui8Equals(decoded, bytes)).toBe(true)\n  })\n})\n\ndescribe('roundtrip toBase64 <-> fromBase64', () => {\n  it('roundtrips empty array', () => {\n    const original = new Uint8Array(0)\n    const encoded = toBase64(original)\n    const decoded = fromBase64(encoded)\n    expect(ui8Equals(decoded, original)).toBe(true)\n  })\n\n  it('roundtrips all byte values', () => {\n    const allBytes = new Uint8Array(256)\n    for (let i = 0; i < 256; i++) {\n      allBytes[i] = i\n    }\n    const encoded = toBase64(allBytes)\n    const decoded = fromBase64(encoded)\n    expect(ui8Equals(decoded, allBytes)).toBe(true)\n  })\n\n  it('roundtrips with base64url alphabet', () => {\n    const original = new Uint8Array([0xfb, 0xff, 0xbf, 0x00, 0xff])\n    const encoded = toBase64(original, 'base64url')\n    const decoded = fromBase64(encoded, 'base64url')\n    expect(ui8Equals(decoded, original)).toBe(true)\n  })\n\n  it('roundtrips random-like data', () => {\n    const data = new Uint8Array([\n      0x00, 0x01, 0x7f, 0x80, 0xfe, 0xff, 0x10, 0x20, 0x30, 0x40,\n    ])\n    const encoded = toBase64(data)\n    const decoded = fromBase64(encoded)\n    expect(ui8Equals(decoded, data)).toBe(true)\n  })\n})\n\ndescribe(asUint8Array, () => {\n  describe('Uint8Array input', () => {\n    it('returns same Uint8Array instance', () => {\n      const input = new Uint8Array([1, 2, 3])\n      const result = asUint8Array(input)\n      expect(result).toBe(input)\n    })\n\n    it('returns same empty Uint8Array instance', () => {\n      const input = new Uint8Array(0)\n      const result = asUint8Array(input)\n      expect(result).toBe(input)\n    })\n  })\n\n  describe('ArrayBuffer input', () => {\n    it('converts ArrayBuffer to Uint8Array', () => {\n      const buffer = new ArrayBuffer(4)\n      const view = new Uint8Array(buffer)\n      view.set([1, 2, 3, 4])\n      const result = asUint8Array(buffer)\n      expect(result).toBeInstanceOf(Uint8Array)\n      expect(ui8Equals(result!, new Uint8Array([1, 2, 3, 4]))).toBe(true)\n    })\n\n    it('converts empty ArrayBuffer to empty Uint8Array', () => {\n      const buffer = new ArrayBuffer(0)\n      const result = asUint8Array(buffer)\n      expect(result).toBeInstanceOf(Uint8Array)\n      expect(result!.length).toBe(0)\n    })\n  })\n\n  describe('TypedArray (ArrayBufferView) input', () => {\n    it('converts Int8Array to Uint8Array', () => {\n      const input = new Int8Array([1, 2, 3, 4])\n      const result = asUint8Array(input)\n      expect(result).toBeInstanceOf(Uint8Array)\n      expect(result!.length).toBe(4)\n    })\n\n    it('converts Int16Array to Uint8Array', () => {\n      const input = new Int16Array([1, 2])\n      const result = asUint8Array(input)\n      expect(result).toBeInstanceOf(Uint8Array)\n      // Int16Array has 2 bytes per element, so 2 elements = 4 bytes\n      expect(result!.length).toBe(4)\n    })\n\n    it('converts Int32Array to Uint8Array', () => {\n      const input = new Int32Array([1])\n      const result = asUint8Array(input)\n      expect(result).toBeInstanceOf(Uint8Array)\n      // Int32Array has 4 bytes per element, so 1 element = 4 bytes\n      expect(result!.length).toBe(4)\n    })\n\n    it('converts Float32Array to Uint8Array', () => {\n      const input = new Float32Array([1.5])\n      const result = asUint8Array(input)\n      expect(result).toBeInstanceOf(Uint8Array)\n      // Float32Array has 4 bytes per element\n      expect(result!.length).toBe(4)\n    })\n\n    it('converts Float64Array to Uint8Array', () => {\n      const input = new Float64Array([1.5])\n      const result = asUint8Array(input)\n      expect(result).toBeInstanceOf(Uint8Array)\n      // Float64Array has 8 bytes per element\n      expect(result!.length).toBe(8)\n    })\n\n    it('converts DataView to Uint8Array', () => {\n      const buffer = new ArrayBuffer(4)\n      const view = new DataView(buffer)\n      view.setUint8(0, 1)\n      view.setUint8(1, 2)\n      view.setUint8(2, 3)\n      view.setUint8(3, 4)\n      const result = asUint8Array(view)\n      expect(result).toBeInstanceOf(Uint8Array)\n      expect(ui8Equals(result!, new Uint8Array([1, 2, 3, 4]))).toBe(true)\n    })\n\n    it('handles TypedArray with byteOffset', () => {\n      const buffer = new ArrayBuffer(8)\n      const fullView = new Uint8Array(buffer)\n      fullView.set([0, 0, 1, 2, 3, 4, 0, 0])\n      // Create a view with offset\n      const offsetView = new Uint8Array(buffer, 2, 4)\n      const result = asUint8Array(offsetView)\n      expect(result).toBeInstanceOf(Uint8Array)\n      expect(result).toBe(offsetView) // Uint8Array returns same instance\n    })\n\n    it('handles Int16Array with byteOffset correctly', () => {\n      const buffer = new ArrayBuffer(8)\n      const fullView = new Uint8Array(buffer)\n      fullView.set([0, 0, 1, 0, 2, 0, 0, 0])\n      // Create Int16Array starting at byte 2, with 2 elements\n      const int16View = new Int16Array(buffer, 2, 2)\n      const result = asUint8Array(int16View)\n      expect(result).toBeInstanceOf(Uint8Array)\n      expect(result!.length).toBe(4) // 2 Int16 elements = 4 bytes\n    })\n  })\n\n  describe('invalid inputs', () => {\n    it('returns undefined for null', () => {\n      const result = asUint8Array(null)\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for undefined', () => {\n      const result = asUint8Array(undefined)\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for string', () => {\n      const result = asUint8Array('hello')\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for number', () => {\n      const result = asUint8Array(42)\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for boolean', () => {\n      const result = asUint8Array(true)\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for plain object', () => {\n      const result = asUint8Array({ foo: 'bar' })\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for array', () => {\n      const result = asUint8Array([1, 2, 3])\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for function', () => {\n      const result = asUint8Array(() => {})\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for symbol', () => {\n      const result = asUint8Array(Symbol('test'))\n      expect(result).toBeUndefined()\n    })\n\n    it('returns undefined for BigInt', () => {\n      const result = asUint8Array(BigInt(42))\n      expect(result).toBeUndefined()\n    })\n  })\n})\n\ndescribe(ui8Equals, () => {\n  describe('equal arrays', () => {\n    it('returns true for identical arrays', () => {\n      const a = new Uint8Array([1, 2, 3])\n      const b = new Uint8Array([1, 2, 3])\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n\n    it('returns true for same instance', () => {\n      const a = new Uint8Array([1, 2, 3])\n      expect(ui8Equals(a, a)).toBe(true)\n    })\n\n    it('returns true for empty arrays', () => {\n      const a = new Uint8Array(0)\n      const b = new Uint8Array(0)\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n\n    it('returns true for single element arrays', () => {\n      const a = new Uint8Array([255])\n      const b = new Uint8Array([255])\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n\n    it('returns true for arrays with all zeros', () => {\n      const a = new Uint8Array([0, 0, 0])\n      const b = new Uint8Array([0, 0, 0])\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n\n    it('returns true for arrays with all 255s', () => {\n      const a = new Uint8Array([255, 255, 255])\n      const b = new Uint8Array([255, 255, 255])\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n  })\n\n  describe('unequal arrays - different lengths', () => {\n    it('returns false when first is longer', () => {\n      const a = new Uint8Array([1, 2, 3, 4])\n      const b = new Uint8Array([1, 2, 3])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false when second is longer', () => {\n      const a = new Uint8Array([1, 2, 3])\n      const b = new Uint8Array([1, 2, 3, 4])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false when one is empty', () => {\n      const a = new Uint8Array([1])\n      const b = new Uint8Array(0)\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false when comparing empty to non-empty', () => {\n      const a = new Uint8Array(0)\n      const b = new Uint8Array([1])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n  })\n\n  describe('unequal arrays - different content', () => {\n    it('returns false when first byte differs', () => {\n      const a = new Uint8Array([1, 2, 3])\n      const b = new Uint8Array([0, 2, 3])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false when middle byte differs', () => {\n      const a = new Uint8Array([1, 2, 3])\n      const b = new Uint8Array([1, 0, 3])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false when last byte differs', () => {\n      const a = new Uint8Array([1, 2, 3])\n      const b = new Uint8Array([1, 2, 0])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false for completely different arrays', () => {\n      const a = new Uint8Array([0, 0, 0])\n      const b = new Uint8Array([255, 255, 255])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('returns false when single values differ', () => {\n      const a = new Uint8Array([0])\n      const b = new Uint8Array([1])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles boundary byte values', () => {\n      const a = new Uint8Array([0x00, 0x7f, 0x80, 0xff])\n      const b = new Uint8Array([0x00, 0x7f, 0x80, 0xff])\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n\n    it('detects difference at boundary values', () => {\n      const a = new Uint8Array([0x7f])\n      const b = new Uint8Array([0x80])\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('handles large arrays efficiently', () => {\n      const size = 100000\n      const a = new Uint8Array(size).fill(0xaa)\n      const b = new Uint8Array(size).fill(0xaa)\n      expect(ui8Equals(a, b)).toBe(true)\n    })\n\n    it('detects single byte difference in large arrays', () => {\n      const size = 100000\n      const a = new Uint8Array(size).fill(0xaa)\n      const b = new Uint8Array(size).fill(0xaa)\n      b[size - 1] = 0xbb // Difference at the end\n      expect(ui8Equals(a, b)).toBe(false)\n    })\n\n    it('compares subarrays correctly', () => {\n      const full = new Uint8Array([0, 1, 2, 3, 4, 5])\n      const sub1 = full.subarray(1, 4) // [1, 2, 3]\n      const sub2 = new Uint8Array([1, 2, 3])\n      expect(ui8Equals(sub1, sub2)).toBe(true)\n    })\n\n    it('detects difference in subarrays', () => {\n      const full = new Uint8Array([0, 1, 2, 3, 4, 5])\n      const sub1 = full.subarray(1, 4) // [1, 2, 3]\n      const sub2 = new Uint8Array([1, 2, 4]) // Different last byte\n      expect(ui8Equals(sub1, sub2)).toBe(false)\n    })\n  })\n})\n\ndescribe(ifUint8Array, () => {\n  it('returns the input if it is a Uint8Array', () => {\n    const input = new Uint8Array([1, 2, 3])\n    const result = ifUint8Array(input)\n    expect(result).toBe(input)\n  })\n\n  it('returns undefined for non-Uint8Array inputs', () => {\n    expect(ifUint8Array(null)).toBeUndefined()\n    expect(ifUint8Array(undefined)).toBeUndefined()\n    expect(ifUint8Array({})).toBeUndefined()\n    expect(ifUint8Array([])).toBeUndefined()\n    expect(ifUint8Array('string')).toBeUndefined()\n    expect(ifUint8Array(123)).toBeUndefined()\n    expect(ifUint8Array(true)).toBeUndefined()\n  })\n})\n\ndescribe(ui8Concat, () => {\n  it('concatenates empty array', () => {\n    const result = ui8Concat([])\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(result.length).toBe(0)\n  })\n\n  it('concatenates single array', () => {\n    const input = new Uint8Array([1, 2, 3])\n    const result = ui8Concat([input])\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(result, input)).toBe(true)\n  })\n\n  it('concatenates two arrays', () => {\n    const a = new Uint8Array([1, 2])\n    const b = new Uint8Array([3, 4])\n    const result = ui8Concat([a, b])\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(result, new Uint8Array([1, 2, 3, 4]))).toBe(true)\n  })\n\n  it('concatenates multiple arrays', () => {\n    const arrays = [\n      new Uint8Array([1]),\n      new Uint8Array([2, 3]),\n      new Uint8Array([4, 5, 6]),\n    ]\n    const result = ui8Concat(arrays)\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(result, new Uint8Array([1, 2, 3, 4, 5, 6]))).toBe(true)\n  })\n\n  it('handles empty arrays in input', () => {\n    const a = new Uint8Array(0)\n    const b = new Uint8Array([1, 2])\n    const c = new Uint8Array(0)\n    const result = ui8Concat([a, b, c])\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(ui8Equals(result, new Uint8Array([1, 2]))).toBe(true)\n  })\n\n  it('handles all empty arrays', () => {\n    const result = ui8Concat([new Uint8Array(0), new Uint8Array(0)])\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(result.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/uint8array.ts",
    "content": "import { Base64Alphabet } from './uint8array-base64.js'\nimport { ui8ConcatNode, ui8ConcatPonyfill } from './uint8array-concat.js'\nimport {\n  fromBase64Native,\n  fromBase64Node,\n  fromBase64Ponyfill,\n} from './uint8array-from-base64.js'\nimport {\n  toBase64Native,\n  toBase64Node,\n  toBase64Ponyfill,\n} from './uint8array-to-base64.js'\n\nexport type { Base64Alphabet }\n\n// @TODO drop dependency on uint8arrays package once Uint8Array.fromBase64 /\n// Uint8Array.prototype.toBase64 is widely supported, and mark fromBase64 /\n// toBase64 as deprecated. We can also drop NodeJS specific implementations\n// once NodeJS <24 is no longer supported.\n\n/**\n * Encodes a Uint8Array into a base64 string.\n *\n * Uses native Uint8Array.prototype.toBase64 when available (Node.js 24+, modern browsers),\n * falling back to Node.js Buffer or a ponyfill implementation.\n *\n * @param bytes - The binary data to encode\n * @param alphabet - The base64 alphabet to use ('base64' or 'base64url'), defaults to 'base64'\n * @returns The base64 encoded string\n *\n * @example\n * ```typescript\n * import { toBase64 } from '@atproto/lex-data'\n *\n * const bytes = new Uint8Array([72, 101, 108, 108, 111])\n * toBase64(bytes)           // 'SGVsbG8='\n * toBase64(bytes, 'base64url')  // 'SGVsbG8' (URL-safe, no padding)\n * ```\n */\nexport const toBase64: (\n  bytes: Uint8Array,\n  alphabet?: Base64Alphabet,\n) => string =\n  /* v8 ignore next -- @preserve */ toBase64Native ??\n  toBase64Node ??\n  toBase64Ponyfill\n\n/**\n * Decodes a base64 string into a Uint8Array.\n *\n * Supports both padded and unpadded base64 strings. Uses native\n * Uint8Array.fromBase64 when available, falling back to Node.js Buffer\n * or a ponyfill implementation.\n *\n * @param b64 - The base64 string to decode\n * @param alphabet - The base64 alphabet to use ('base64' or 'base64url'), defaults to 'base64'\n * @returns The decoded binary data\n * @throws If the input is not a valid base64 string\n *\n * @example\n * ```typescript\n * import { fromBase64 } from '@atproto/lex-data'\n *\n * fromBase64('SGVsbG8=')       // Uint8Array([72, 101, 108, 108, 111])\n * fromBase64('SGVsbG8', 'base64url')  // Same, URL-safe alphabet\n * ```\n */\nexport const fromBase64: (\n  b64: string,\n  alphabet?: Base64Alphabet,\n) => Uint8Array =\n  /* v8 ignore next -- @preserve */ fromBase64Native ??\n  fromBase64Node ??\n  fromBase64Ponyfill\n\n/* v8 ignore next -- @preserve */\nif (toBase64 === toBase64Ponyfill || fromBase64 === fromBase64Ponyfill) {\n  /*#__PURE__*/\n  console.warn(\n    '[@atproto/lex-data]: Uint8Array.fromBase64 / Uint8Array.prototype.toBase64 not available in this environment. Falling back to ponyfill implementation.',\n  )\n}\n\n/**\n * Returns the input if it is a Uint8Array, otherwise returns undefined.\n *\n * @param input - The value to check\n * @returns The input if it's a Uint8Array, otherwise undefined\n *\n * @example\n * ```typescript\n * import { ifUint8Array } from '@atproto/lex-data'\n *\n * ifUint8Array(new Uint8Array([1, 2]))  // Uint8Array([1, 2])\n * ifUint8Array('not binary')            // undefined\n * ifUint8Array(new ArrayBuffer(4))      // undefined\n * ```\n */\nexport function ifUint8Array(input: unknown): Uint8Array | undefined {\n  if (input instanceof Uint8Array) {\n    return input\n  }\n\n  return undefined\n}\n\n/**\n * Coerces various binary data representations into a Uint8Array.\n *\n * Handles the following input types:\n * - `Uint8Array` - Returned as-is\n * - `ArrayBufferView` (e.g., DataView, other TypedArrays) - Converted to Uint8Array\n * - `ArrayBuffer` - Wrapped in a Uint8Array\n *\n * @param input - The value to convert\n * @returns A Uint8Array, or `undefined` if the input could not be converted\n *\n * @example\n * ```typescript\n * import { asUint8Array } from '@atproto/lex-data'\n *\n * asUint8Array(new Uint8Array([1, 2]))     // Uint8Array([1, 2])\n * asUint8Array(new ArrayBuffer(4))         // Uint8Array of length 4\n * asUint8Array(new Int16Array([1, 2]))     // Uint8Array view of the buffer\n * asUint8Array('string')                   // undefined\n * ```\n */\nexport function asUint8Array(input: unknown): Uint8Array | undefined {\n  if (input instanceof Uint8Array) {\n    return input\n  }\n\n  if (ArrayBuffer.isView(input)) {\n    return new Uint8Array(\n      input.buffer,\n      input.byteOffset,\n      input.byteLength / Uint8Array.BYTES_PER_ELEMENT,\n    )\n  }\n\n  if (input instanceof ArrayBuffer) {\n    return new Uint8Array(input)\n  }\n\n  return undefined\n}\n\n/**\n * Compares two Uint8Arrays for byte-by-byte equality.\n *\n * @param a - First Uint8Array to compare\n * @param b - Second Uint8Array to compare\n * @returns `true` if both arrays have the same length and identical bytes\n *\n * @example\n * ```typescript\n * import { ui8Equals } from '@atproto/lex-data'\n *\n * ui8Equals(new Uint8Array([1, 2]), new Uint8Array([1, 2]))  // true\n * ui8Equals(new Uint8Array([1, 2]), new Uint8Array([1, 3]))  // false\n * ui8Equals(new Uint8Array([1]), new Uint8Array([1, 2]))     // false\n * ```\n */\nexport function ui8Equals(a: Uint8Array, b: Uint8Array): boolean {\n  if (a.byteLength !== b.byteLength) {\n    return false\n  }\n\n  for (let i = 0; i < a.byteLength; i++) {\n    if (a[i] !== b[i]) {\n      return false\n    }\n  }\n\n  return true\n}\n\n/**\n * Concatenates multiple Uint8Arrays into a single Uint8Array.\n *\n * Uses Node.js Buffer.concat when available for performance,\n * falling back to a ponyfill implementation.\n *\n * @param arrays - The Uint8Arrays to concatenate\n * @returns A new Uint8Array containing all input bytes in order\n *\n * @example\n * ```typescript\n * import { ui8Concat } from '@atproto/lex-data'\n *\n * const a = new Uint8Array([1, 2])\n * const b = new Uint8Array([3, 4])\n * ui8Concat([a, b])  // Uint8Array([1, 2, 3, 4])\n * ```\n */\nexport const ui8Concat =\n  /* v8 ignore next -- @preserve */ ui8ConcatNode ?? ui8ConcatPonyfill\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-from-base64.test.ts",
    "content": "import { assert, describe, expect, it } from 'vitest'\nimport {\n  utf8FromBase64Node,\n  utf8FromBase64Ponyfill,\n} from './utf8-from-base64.js'\n\nconst strings = [\n  'Hello, World!',\n  '¡Hola, Mundo!',\n  'こんにちは世界',\n  '😀👩‍💻🌍',\n  '',\n  '𓀀𓁐𓂀𓃰𓄿𓅱𓆑𓇋𓈖𓉔𓊃𓋴𓌳𓍿𓎛𓏏',\n]\n\nfor (const utf8FromBase64 of [\n  utf8FromBase64Node,\n  utf8FromBase64Ponyfill,\n] as const) {\n  assert(utf8FromBase64, 'implementation should not be null')\n\n  describe(utf8FromBase64, () => {\n    it('decodes base64 to utf8 string', () => {\n      for (const text of strings) {\n        const b64 = Buffer.from(text, 'utf8').toString('base64')\n        const decoded = utf8FromBase64(b64, 'base64')\n        expect(decoded).toBe(text)\n      }\n    })\n\n    it('decodes base64url to utf8 string', () => {\n      for (const text of strings) {\n        const b64u = Buffer.from(text, 'utf8').toString('base64url')\n        const decoded = utf8FromBase64(b64u, 'base64url')\n        expect(decoded).toBe(text)\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-from-base64.ts",
    "content": "import { fromString } from 'uint8arrays/from-string'\nimport { NodeJSBuffer } from './lib/nodejs-buffer.js'\nimport { Base64Alphabet } from './uint8array-base64.js'\n\nconst Buffer = NodeJSBuffer\n\nexport const utf8FromBase64Node = Buffer\n  ? function utf8FromBase64Node(\n      b64: string,\n      alphabet: Base64Alphabet = 'base64',\n    ): string {\n      return Buffer.from(b64, alphabet).toString('utf8')\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nconst textDecoder = /*#__PURE__*/ new TextDecoder()\nexport function utf8FromBase64Ponyfill(\n  b64: string,\n  alphabet?: Base64Alphabet,\n): string {\n  const bytes = fromString(b64, alphabet)\n  return textDecoder.decode(bytes)\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-grapheme-len.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { graphemeLenNative, graphemeLenPonyfill } from './utf8-grapheme-len.js'\n\ndescribe(graphemeLenNative!, () => {\n  it('computes grapheme length', () => {\n    expect(graphemeLenNative!('a')).toBe(1)\n    expect(graphemeLenNative!('~')).toBe(1)\n    expect(graphemeLenNative!('ö')).toBe(1)\n    expect(graphemeLenNative!('ñ')).toBe(1)\n    expect(graphemeLenNative!('©')).toBe(1)\n    expect(graphemeLenNative!('⽘')).toBe(1)\n    expect(graphemeLenNative!('☎')).toBe(1)\n    expect(graphemeLenNative!('𓋓')).toBe(1)\n    expect(graphemeLenNative!('😀')).toBe(1)\n    expect(graphemeLenNative!('👨‍👩‍👧‍👧')).toBe(1)\n    expect(graphemeLenNative!('a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧')).toBe(10)\n    // https://github.com/bluesky-social/atproto/issues/4321\n    expect(graphemeLenNative!('नमस्ते')).toBe(3)\n  })\n})\n\ndescribe(graphemeLenPonyfill, () => {\n  it('computes grapheme length', () => {\n    expect(graphemeLenPonyfill('a')).toBe(1)\n    expect(graphemeLenPonyfill('~')).toBe(1)\n    expect(graphemeLenPonyfill('ö')).toBe(1)\n    expect(graphemeLenPonyfill('ñ')).toBe(1)\n    expect(graphemeLenPonyfill('©')).toBe(1)\n    expect(graphemeLenPonyfill('⽘')).toBe(1)\n    expect(graphemeLenPonyfill('☎')).toBe(1)\n    expect(graphemeLenPonyfill('𓋓')).toBe(1)\n    expect(graphemeLenPonyfill('😀')).toBe(1)\n    expect(graphemeLenPonyfill('👨‍👩‍👧‍👧')).toBe(1)\n    expect(graphemeLenPonyfill('a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧')).toBe(10)\n    // https://github.com/bluesky-social/atproto/issues/4321\n    expect(graphemeLenPonyfill('नमस्ते')).toBe(3)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-grapheme-len.ts",
    "content": "import { countGraphemes } from 'unicode-segmenter/grapheme'\n\n// @TODO: Drop usage of \"unicode-segmenter\" package when Intl.Segmenter is\n// widely supported.\n// https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter\nconst segmenter =\n  'Segmenter' in Intl && typeof Intl.Segmenter === 'function'\n    ? /*#__PURE__*/ new Intl.Segmenter()\n    : /* v8 ignore next -- @preserve */ null\n\nexport const graphemeLenNative = segmenter\n  ? function graphemeLenNative(str: string): number {\n      let length = 0\n      for (const _ of segmenter.segment(str)) length++\n      return length\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nexport function graphemeLenPonyfill(str: string): number {\n  return countGraphemes(str)\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-len.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { utf8LenCompute, utf8LenNode } from './utf8-len.js'\n\nfor (const utf8Len of [utf8LenNode!, utf8LenCompute!] as const) {\n  describe(utf8Len, () => {\n    it('computes utf8 string length', () => {\n      expect(utf8Len('a')).toBe(1)\n      expect(utf8Len('~')).toBe(1)\n      expect(utf8Len('ö')).toBe(2)\n      expect(utf8Len('ñ')).toBe(2)\n      expect(utf8Len('©')).toBe(2)\n      expect(utf8Len('⽘')).toBe(3)\n      expect(utf8Len('☎')).toBe(3)\n      expect(utf8Len('𓋓')).toBe(4)\n      expect(utf8Len('😀')).toBe(4)\n      expect(utf8Len('👨‍👩‍👧‍👧')).toBe(25)\n      // high surrogate with no low surrogate\n      expect(utf8Len('\\uD83D')).toBe(3)\n      // low surrogate with no high surrogate\n      expect(utf8Len('\\uDC00')).toBe(3)\n    })\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-len.ts",
    "content": "import { NodeJSBuffer } from './lib/nodejs-buffer.js'\n\n// @NOTE This file is not meant to be exported directly. Instead, we re-export\n// public functions from ./utf8.ts. The reason for this separation is that this\n// file allows to test both the NodeJS-optimized and ponyfill implementations.\n\nexport const utf8LenNode = NodeJSBuffer\n  ? function utf8LenNode(string: string): number {\n      return NodeJSBuffer!.byteLength(string, 'utf8')\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nexport function utf8LenCompute(string: string): number {\n  // The code below is similar to TextEncoder's implementation of UTF-8\n  // encoding. However, using TextEncoder to get the byte length is slower\n  // as it requires allocating a new Uint8Array and copying data:\n\n  // return new TextEncoder().encode(string).byteLength\n\n  // The base length is the string length (all ASCII)\n  let len = string.length\n  let code: number\n\n  // The loop calculates the number of additional bytes needed for\n  // non-ASCII characters\n  for (let i = 0; i < string.length; i += 1) {\n    code = string.charCodeAt(i)\n\n    if (code <= 0x7f) {\n      // ASCII, 1 byte\n    } else if (code <= 0x7ff) {\n      // 2 bytes char\n      len += 1\n    } else {\n      // 3 bytes char\n      len += 2\n      // If the current char is a high surrogate, and the next char is a low\n      // surrogate, skip the next char as the total is a 4 bytes char\n      // (represented as a surrogate pair in UTF-16) and was already accounted\n      // for.\n      if (code >= 0xd800 && code <= 0xdbff) {\n        code = string.charCodeAt(i + 1)\n        if (code >= 0xdc00 && code <= 0xdfff) {\n          i++\n        }\n      }\n    }\n  }\n\n  return len\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-to-base64.test.ts",
    "content": "import { assert, describe, expect, it } from 'vitest'\nimport { utf8ToBase64Node, utf8ToBase64Ponyfill } from './utf8-to-base64.js'\n\nconst strings = [\n  'Hello, World!',\n  '¡Hola, Mundo!',\n  'こんにちは世界',\n  '😀👩‍💻🌍',\n  '',\n  '𓀀𓁐𓂀𓃰𓄿𓅱𓆑𓇋𓈖𓉔𓊃𓋴𓌳𓍿𓎛𓏏',\n]\n\nfor (const utf8ToBase64 of [utf8ToBase64Node, utf8ToBase64Ponyfill] as const) {\n  assert(utf8ToBase64, 'implementation should not be null')\n\n  describe(utf8ToBase64, () => {\n    it('encodes utf8 string to base64', () => {\n      for (const text of strings) {\n        const b64 = Buffer.from(text, 'utf8')\n          .toString('base64')\n          .replaceAll('=', '') // utf8ToBase64 omits padding\n        const encoded = utf8ToBase64(text, 'base64')\n        expect(encoded).toBe(b64)\n      }\n    })\n\n    it('encodes utf8 string to base64url', () => {\n      for (const text of strings) {\n        const b64u = Buffer.from(text, 'utf8').toString('base64url')\n        const encoded = utf8ToBase64(text, 'base64url')\n        expect(encoded).toBe(b64u)\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8-to-base64.ts",
    "content": "import { toString } from 'uint8arrays/to-string'\nimport { NodeJSBuffer } from './lib/nodejs-buffer.js'\nimport { Base64Alphabet } from './uint8array-base64.js'\nimport { toBase64Node } from './uint8array-to-base64.js'\n\nconst Buffer = NodeJSBuffer\n\nexport const utf8ToBase64Node = Buffer\n  ? function utf8ToBase64Node(text: string, alphabet?: Base64Alphabet): string {\n      const buffer = Buffer.from(text, 'utf8')\n      return toBase64Node!(buffer, alphabet)\n    }\n  : /* v8 ignore next -- @preserve */ null\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\nexport function utf8ToBase64Ponyfill(\n  text: string,\n  alphabet?: Base64Alphabet,\n): string {\n  const bytes = textEncoder.encode(text)\n  return toString(bytes, alphabet)\n}\n"
  },
  {
    "path": "packages/lex/lex-data/src/utf8.ts",
    "content": "import { Base64Alphabet } from './uint8array.js'\nimport {\n  utf8FromBase64Node,\n  utf8FromBase64Ponyfill,\n} from './utf8-from-base64.js'\nimport { graphemeLenNative, graphemeLenPonyfill } from './utf8-grapheme-len.js'\nimport { utf8LenCompute, utf8LenNode } from './utf8-len.js'\nimport { utf8ToBase64Node, utf8ToBase64Ponyfill } from './utf8-to-base64.js'\n\n/**\n * Counts the number of grapheme clusters (user-perceived characters) in a string.\n *\n * Grapheme clusters represent what users typically think of as \"characters\",\n * handling complex cases like:\n * - Emoji with skin tones and ZWJ sequences (e.g., family emoji)\n * - Combined characters (e.g., 'e' + combining accent)\n * - Regional indicator pairs (flag emoji)\n *\n * Uses native {@link Intl.Segmenter} when available, falling back to a ponyfill.\n *\n * @param str - The string to measure\n * @returns The number of grapheme clusters\n *\n * @example\n * ```typescript\n * import { graphemeLen } from '@atproto/lex-data'\n *\n * graphemeLen('hello')        // 5\n * graphemeLen('cafe\\u0301')   // 4 (cafe with combining accent)\n * graphemeLen('\\u{1F468}\\u{200D}\\u{1F469}\\u{200D}\\u{1F467}\\u{200D}\\u{1F466}')  // 1 (family emoji)\n * ```\n */\nexport const graphemeLen: (str: string) => number =\n  /* v8 ignore next -- @preserve */ graphemeLenNative ?? graphemeLenPonyfill\n\n/* v8 ignore next -- @preserve */\nif (graphemeLen === graphemeLenPonyfill) {\n  /*#__PURE__*/\n  console.warn(\n    '[@atproto/lex-data]: Intl.Segmenter is not available in this environment. Falling back to ponyfill implementation.',\n  )\n}\n\n/**\n * Calculates the UTF-8 byte length of a string.\n *\n * Returns the number of bytes the string would occupy when encoded as UTF-8.\n * This is important for Lexicon validation where schemas specify byte limits.\n *\n * Uses Node.js Buffer.byteLength when available for performance,\n * falling back to a computed implementation.\n *\n * @param str - The string to measure\n * @returns The UTF-8 byte length\n *\n * @example\n * ```typescript\n * import { utf8Len } from '@atproto/lex-data'\n *\n * utf8Len('hello')      // 5 (ASCII: 1 byte per char)\n * utf8Len('\\u00e9')     // 2 (e with accent: 2 bytes)\n * utf8Len('\\u{1F600}')  // 4 (emoji: 4 bytes)\n * utf8Len('\\u{1F468}\\u{200D}\\u{1F469}\\u{200D}\\u{1F467}\\u{200D}\\u{1F466}')  // 25 (family emoji)\n * ```\n */\nexport const utf8Len: (string: string) => number =\n  /* v8 ignore next -- @preserve */ utf8LenNode ?? utf8LenCompute\n\n/**\n * Encodes a UTF-8 string to base64.\n *\n * First encodes the string as UTF-8 bytes, then encodes those bytes as base64.\n *\n * @param str - The string to encode\n * @param alphabet - The base64 alphabet to use ('base64' or 'base64url')\n * @returns The base64-encoded string\n *\n * @example\n * ```typescript\n * import { utf8ToBase64 } from '@atproto/lex-data'\n *\n * utf8ToBase64('Hello')  // 'SGVsbG8='\n * ```\n */\nexport const utf8ToBase64: (str: string, alphabet?: Base64Alphabet) => string =\n  /* v8 ignore next -- @preserve */ utf8ToBase64Node ?? utf8ToBase64Ponyfill\n\n/**\n * Decodes a base64 string to UTF-8.\n *\n * Decodes the base64 to bytes, then interprets those bytes as UTF-8 text.\n *\n * @param b64 - The base64 string to decode\n * @param alphabet - The base64 alphabet to use ('base64' or 'base64url')\n * @returns The decoded UTF-8 string\n *\n * @example\n * ```typescript\n * import { utf8FromBase64 } from '@atproto/lex-data'\n *\n * utf8FromBase64('SGVsbG8=')  // 'Hello'\n * ```\n */\nexport const utf8FromBase64: (\n  b64: string,\n  alphabet?: Base64Alphabet,\n) => string =\n  /* v8 ignore next -- @preserve */ utf8FromBase64Node ?? utf8FromBase64Ponyfill\n"
  },
  {
    "path": "packages/lex/lex-data/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-data/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-data/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-data/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-document/CHANGELOG.md",
    "content": "# @atproto/lex-document\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-schema@0.0.16\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-schema@0.0.15\n\n## 0.0.15\n\n### Patch Changes\n\n- [#4691](https://github.com/bluesky-social/atproto/pull/4691) [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `LexiconSchemaBuilder` async disposable\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-schema@0.0.14\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve validation of `encoding` payload fields in lexicon documents\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n\n## 0.0.13\n\n### Patch Changes\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `LexiconSchemaBuilder` returns and more accurately typed `Schema`\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix validation of `unknown` schema defs (used to allow any value, now requires `LexMap`)\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-schema@0.0.11\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-schema@0.0.9\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-schema@0.0.8\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-schema@0.0.7\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure that default values match constraints\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-schema@0.0.5\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4416](https://github.com/bluesky-social/atproto/pull/4416) [`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Align lexicon document validation with the spec\n\n- Updated dependencies []:\n  - @atproto/lex-schema@0.0.4\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework object validation logic to work without `options` argument\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performance of lexicon document schema validation by reusing singleton schema instances for commonly used types such as string, number, boolean, null, any, and empty object. This reduces memory usage and speeds up initialization.\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `key` field in `record` definitions is now non optional (as per spec) and properly validated\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4400](https://github.com/bluesky-social/atproto/pull/4400) [`03a2a4b`](https://github.com/bluesky-social/atproto/commit/03a2a4bb3814ced7ad1d4fe6c94b5348a3bbc097) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add test cases\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use string formats from `@atproto/syntax`\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename schema methods `validate`, `check` and `maybe` to `safeParse`, `matches` and `ifMatches` respectively.\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` option to `const` and `enum` schemas\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba)]:\n  - @atproto/lex-schema@0.0.3\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4)]:\n  - @atproto/lex-schema@0.0.2\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4374](https://github.com/bluesky-social/atproto/pull/4374) [`5ffd612`](https://github.com/bluesky-social/atproto/commit/5ffd6129909071e979c30f31266119865ab582b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly await cleanup of iterator when encountering multiple lexicon documents with the same id.\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-schema@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-document/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-document\",\n  \"version\": \"0.0.17\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon document validation tools for AT\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"document\",\n    \"lex\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-document\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"core-js\": \"^3\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-document/src/index.ts",
    "content": "import 'core-js/modules/esnext.symbol.async-dispose'\nimport 'core-js/modules/esnext.symbol.dispose'\n\nexport * from './lexicon-document.js'\nexport * from './lexicon-indexer.js'\nexport * from './lexicon-schema-builder.js'\nexport * from './lexicon-iterable-indexer.js'\n"
  },
  {
    "path": "packages/lex/lex-document/src/lexicon-document.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { lexiconDocumentSchema } from './lexicon-document.js'\n\ndescribe('lexiconDocumentSchema', () => {\n  it('allows unknown fields to be present', () => {\n    const value = {\n      lexicon: 1,\n      id: 'com.example.unknownFields',\n      defs: {\n        test: {\n          type: 'object',\n          properties: {},\n          foo: 3,\n        },\n      },\n    }\n\n    expect(lexiconDocumentSchema.safeParse(value)).toStrictEqual({\n      success: true,\n      value,\n    })\n  })\n\n  it('validates a minimal lexicons document', () => {\n    expect(\n      lexiconDocumentSchema.safeParse({\n        lexicon: 1,\n        id: 'com.example.lexicon',\n        defs: {\n          demo: {\n            type: 'integer',\n          },\n        },\n      }),\n    ).toMatchObject({\n      success: true,\n    })\n  })\n\n  it('rejects lexicons with invalid lexicon field', () => {\n    expect(\n      lexiconDocumentSchema.safeParse({\n        lexicon: 'one',\n        id: 'com.example.lexicon',\n        defs: {\n          demo: {\n            type: 'integer',\n          },\n        },\n      }),\n    ).toMatchObject({\n      success: false,\n      reason: { issues: [{ code: 'invalid_value', values: [1] }] },\n    })\n  })\n\n  it('rejects lexicons with invalid NSID in id field', () => {\n    expect(\n      lexiconDocumentSchema.safeParse({\n        lexicon: 1,\n        id: 'not-an-nsid',\n        defs: {\n          demo: {\n            type: 'integer',\n          },\n        },\n      }),\n    ).toMatchObject({\n      success: false,\n      reason: { issues: [{ code: 'invalid_format', format: 'nsid' }] },\n    })\n  })\n\n  it('rejects lexicons with numeric id field', () => {\n    expect(\n      lexiconDocumentSchema.safeParse({\n        lexicon: 1,\n        id: 2,\n        defs: {\n          demo: {\n            type: 'integer',\n          },\n        },\n      }),\n    ).toMatchObject({\n      success: false,\n      reason: { issues: [{ code: 'invalid_type', expected: ['string'] }] },\n    })\n  })\n\n  it('rejects object defs with invalid required fields', () => {\n    expect(\n      lexiconDocumentSchema.safeParse({\n        lexicon: 1,\n        id: 'com.example.lexicon',\n        defs: {\n          demo: {\n            type: 'object',\n            properties: {\n              foo: { type: 'string' },\n            },\n            required: ['bar'],\n          },\n        },\n      }),\n    ).toMatchObject({\n      success: false,\n      reason: {\n        issues: [{ code: 'custom', path: ['defs', 'demo', 'required'] }],\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-document/src/lexicon-document.ts",
    "content": "import { l } from '@atproto/lex-schema'\n\n// https://atproto.com/specs/lexicon\n\n// \"Concrete\" Types\n\n/**\n * Schema for validating Lexicon boolean type definitions.\n *\n * Validates boolean field definitions that may include a default value,\n * a constant value, and an optional description.\n */\nexport const lexiconBooleanSchema = l.object({\n  type: l.literal('boolean'),\n  default: l.optional(l.boolean()),\n  const: l.optional(l.boolean()),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon boolean definition.\n *\n * Represents the structure of a boolean field in a Lexicon document,\n * including optional default, const, and description properties.\n *\n * @see {@link lexiconBooleanSchema} for the schema definition\n */\nexport type LexiconBoolean = l.Infer<typeof lexiconBooleanSchema>\n\n/**\n * Schema for validating Lexicon integer type definitions.\n *\n * Validates integer field definitions with support for default values,\n * minimum/maximum constraints, enumerated values, and constant values.\n */\nexport const lexiconIntegerSchema = l.object({\n  type: l.literal('integer'),\n  default: l.optional(l.integer()),\n  minimum: l.optional(l.integer()),\n  maximum: l.optional(l.integer()),\n  enum: l.optional(l.array(l.integer())),\n  const: l.optional(l.integer()),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon integer definition.\n *\n * Represents the structure of an integer field in a Lexicon document,\n * including optional constraints like minimum, maximum, enum, and const.\n *\n * @see {@link lexiconIntegerSchema} for the schema definition\n */\nexport type LexiconInteger = l.Infer<typeof lexiconIntegerSchema>\n\n/**\n * Schema for validating Lexicon string type definitions.\n *\n * Validates string field definitions with support for format validation,\n * length constraints (both character and grapheme-based), enumerated values,\n * known values, and constant values.\n */\nexport const lexiconStringSchema = l.object({\n  type: l.literal('string'),\n  format: l.optional(l.enum<l.StringFormat>(l.STRING_FORMATS)),\n  default: l.optional(l.string()),\n  minLength: l.optional(l.integer()),\n  maxLength: l.optional(l.integer()),\n  minGraphemes: l.optional(l.integer()),\n  maxGraphemes: l.optional(l.integer()),\n  enum: l.optional(l.array(l.string())),\n  const: l.optional(l.string()),\n  knownValues: l.optional(l.array(l.string())),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon string definition.\n *\n * Represents the structure of a string field in a Lexicon document,\n * including optional format, length constraints, enum, const, and knownValues.\n *\n * @see {@link lexiconStringSchema} for the schema definition\n */\nexport type LexiconString = l.Infer<typeof lexiconStringSchema>\n\n/**\n * Schema for validating Lexicon bytes type definitions.\n *\n * Validates binary data field definitions with optional length constraints.\n * Used for raw byte arrays in DAG-CBOR encoding.\n */\nexport const lexiconBytesSchema = l.object({\n  type: l.literal('bytes'),\n  maxLength: l.optional(l.integer()),\n  minLength: l.optional(l.integer()),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon bytes definition.\n *\n * Represents the structure of a binary data field in a Lexicon document,\n * including optional minLength and maxLength constraints.\n *\n * @see {@link lexiconBytesSchema} for the schema definition\n */\nexport type LexiconBytes = l.Infer<typeof lexiconBytesSchema>\n\n/**\n * Schema for validating Lexicon CID link type definitions.\n *\n * Validates Content Identifier (CID) link field definitions.\n * CIDs are used to reference content-addressed data in IPFS/IPLD.\n */\nexport const lexiconCidLinkSchema = l.object({\n  type: l.literal('cid-link'),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon CID link definition.\n *\n * Represents the structure of a CID link field in a Lexicon document.\n *\n * @see {@link lexiconCidLinkSchema} for the schema definition\n */\nexport type LexiconCid = l.Infer<typeof lexiconCidLinkSchema>\n\n/**\n * Schema for validating Lexicon blob type definitions.\n *\n * Validates blob field definitions with optional MIME type acceptance list\n * and maximum size constraints. Blobs represent uploaded file references.\n */\nexport const lexiconBlobSchema = l.object({\n  type: l.literal('blob'),\n  accept: l.optional(l.array(l.string())),\n  maxSize: l.optional(l.integer()),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon blob definition.\n *\n * Represents the structure of a blob field in a Lexicon document,\n * including optional accept list and maxSize constraints.\n *\n * @see {@link lexiconBlobSchema} for the schema definition\n */\nexport type LexiconBlob = l.Infer<typeof lexiconBlobSchema>\n\n/**\n * Array of all concrete (primitive) Lexicon type schemas.\n * Includes boolean, integer, string, bytes, cid-link, and blob types.\n */\nconst CONCRETE_TYPES = [\n  lexiconBooleanSchema,\n  lexiconIntegerSchema,\n  lexiconStringSchema,\n  // Lexicon (DAG-CBOR)\n  lexiconBytesSchema,\n  lexiconCidLinkSchema,\n  // Lexicon Specific\n  lexiconBlobSchema,\n] as const\n\n// Meta types\n\n/**\n * Schema for validating Lexicon unknown type definitions.\n *\n * Validates unknown field definitions which accept any valid data.\n * Used when the schema cannot determine the type ahead of time.\n */\nexport const lexiconUnknownSchema = l.object({\n  type: l.literal('unknown'),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon unknown definition.\n *\n * Represents the structure of an unknown field in a Lexicon document.\n *\n * @see {@link lexiconUnknownSchema} for the schema definition\n */\nexport type LexiconUnknown = l.Infer<typeof lexiconUnknownSchema>\n\n/**\n * Schema for validating Lexicon token type definitions.\n *\n * Validates token definitions which represent symbolic constants.\n * Tokens are used to define enumeration-like values that can be\n * referenced across different lexicons.\n */\nexport const lexiconTokenSchema = l.object({\n  type: l.literal('token'),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon token definition.\n *\n * Represents the structure of a token in a Lexicon document.\n *\n * @see {@link lexiconTokenSchema} for the schema definition\n */\nexport type LexiconToken = l.Infer<typeof lexiconTokenSchema>\n\n/**\n * Schema for validating Lexicon reference type definitions.\n *\n * Validates reference definitions which point to other type definitions\n * within the same or different Lexicon documents. References use the\n * format \"nsid#defName\" for cross-document refs or \"#defName\" for local refs.\n */\nexport const lexiconRefSchema = l.object({\n  type: l.literal('ref'),\n  ref: l.string(),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon reference definition.\n *\n * Represents the structure of a reference field in a Lexicon document,\n * including the ref string pointing to another definition.\n *\n * @see {@link lexiconRefSchema} for the schema definition\n */\nexport type LexiconRef = l.Infer<typeof lexiconRefSchema>\n\n/**\n * Schema for validating Lexicon union reference type definitions.\n *\n * Validates union definitions which can reference multiple possible types.\n * The union can be closed (only listed types allowed) or open (allows\n * additional unlisted types).\n */\nexport const lexiconRefUnionSchema = l.object({\n  type: l.literal('union'),\n  refs: l.array(l.string()),\n  closed: l.optional(l.boolean()),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon union reference definition.\n *\n * Represents the structure of a union field in a Lexicon document,\n * including an array of reference strings and optional closed flag.\n *\n * @see {@link lexiconRefUnionSchema} for the schema definition\n */\nexport type LexiconRefUnion = l.Infer<typeof lexiconRefUnionSchema>\n\n// Complex Types\n\nconst ARRAY_ITEMS_SCHEMAS = [\n  ...CONCRETE_TYPES,\n  // Meta\n  lexiconUnknownSchema,\n  lexiconRefSchema,\n  lexiconRefUnionSchema,\n] as const\n\n/**\n * TypeScript type representing valid item types for Lexicon arrays.\n *\n * Union of all types that can appear as items within a Lexicon array definition.\n */\nexport type LexiconArrayItems = l.Infer<(typeof ARRAY_ITEMS_SCHEMAS)[number]>\n\n/**\n * Schema for validating Lexicon array type definitions.\n *\n * Validates array field definitions with specified item type and\n * optional length constraints.\n */\nexport const lexiconArraySchema = l.object({\n  type: l.literal('array'),\n  items: l.discriminatedUnion('type', ARRAY_ITEMS_SCHEMAS),\n  minLength: l.optional(l.integer()),\n  maxLength: l.optional(l.integer()),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon array definition.\n *\n * Represents the structure of an array field in a Lexicon document,\n * including the items schema and optional length constraints.\n *\n * @see {@link lexiconArraySchema} for the schema definition\n */\nexport type LexiconArray = l.Infer<typeof lexiconArraySchema>\n\nconst requirePropertiesRefinement: l.RefinementCheck<{\n  required?: string[]\n  properties: Record<string, unknown>\n}> = {\n  check: (v) => !v.required || v.required.every((k) => k in v.properties),\n  message: 'All required parameters must be defined in properties',\n  path: 'required',\n}\n\n/**\n * Schema for validating Lexicon object type definitions.\n *\n * Validates object definitions with named properties, required field lists,\n * and nullable field lists. Includes refinement to ensure all required\n * properties are defined in the properties map.\n */\nexport const lexiconObjectSchema = l.refine(\n  l.object({\n    type: l.literal('object'),\n    properties: l.dict(\n      l.string(),\n      l.discriminatedUnion('type', [\n        ...ARRAY_ITEMS_SCHEMAS,\n        lexiconArraySchema,\n      ]),\n    ),\n    required: l.optional(l.array(l.string())),\n    nullable: l.optional(l.array(l.string())),\n    description: l.optional(l.string()),\n  }),\n  requirePropertiesRefinement,\n)\n\n/**\n * TypeScript type for a Lexicon object definition.\n *\n * Represents the structure of an object type in a Lexicon document,\n * including properties map, required array, and nullable array.\n *\n * @see {@link lexiconObjectSchema} for the schema definition\n */\nexport type LexiconObject = l.Infer<typeof lexiconObjectSchema>\n\n// Records\n\n/**\n * Schema for validating Lexicon record key definitions.\n *\n * Validates record key type specifications. Valid values are:\n * - \"any\": Any valid record key\n * - \"nsid\": Namespaced identifier\n * - \"tid\": Timestamp identifier\n * - \"literal:<string>\": A specific literal string value\n */\nexport const lexiconRecordKeySchema = l.custom(\n  l.isLexiconRecordKey,\n  'Invalid record key definition (must be \"any\", \"nsid\", \"tid\", or \"literal:<string>\")',\n)\n\n/**\n * TypeScript type for valid Lexicon record key values.\n *\n * Can be \"any\", \"nsid\", \"tid\", or \"literal:<string>\".\n *\n * @see {@link lexiconRecordKeySchema} for the schema definition\n */\nexport type LexiconRecordKey = l.LexiconRecordKey\n\n/**\n * Schema for validating Lexicon record type definitions.\n *\n * Validates record definitions which define the structure of data\n * stored in AT Protocol repositories. Records have a key type\n * and an object schema defining the record's data structure.\n */\nexport const lexiconRecordSchema = l.object({\n  type: l.literal('record'),\n  record: lexiconObjectSchema,\n  description: l.optional(l.string()),\n  key: lexiconRecordKeySchema,\n})\n\n/**\n * TypeScript type for a Lexicon record definition.\n *\n * Represents the structure of a record type in a Lexicon document,\n * including the key type and the object schema for the record data.\n *\n * @see {@link lexiconRecordSchema} for the schema definition\n */\nexport type LexiconRecord = l.Infer<typeof lexiconRecordSchema>\n\n// XRPC Methods\n\n/**\n * Schema for validating Lexicon XRPC method parameters.\n *\n * Validates the parameters definition for query and procedure methods.\n * Parameters can only be primitive types (boolean, integer, string)\n * or arrays of primitives.\n */\nexport const lexiconParameters = l.refine(\n  l.object({\n    type: l.literal('params'),\n    properties: l.dict(\n      l.string(),\n      l.discriminatedUnion('type', [\n        lexiconBooleanSchema,\n        lexiconIntegerSchema,\n        lexiconStringSchema,\n        l.object({\n          type: l.literal('array'),\n          items: l.discriminatedUnion('type', [\n            lexiconBooleanSchema,\n            lexiconIntegerSchema,\n            lexiconStringSchema,\n          ]),\n          minLength: l.optional(l.integer()),\n          maxLength: l.optional(l.integer()),\n          description: l.optional(l.string()),\n        }),\n      ]),\n    ),\n    required: l.optional(l.array(l.string())),\n    description: l.optional(l.string()),\n  }),\n  requirePropertiesRefinement,\n)\n\n/**\n * TypeScript type for Lexicon XRPC method parameters.\n *\n * Represents the structure of parameters for query and procedure methods.\n *\n * @see {@link lexiconParameters} for the schema definition\n */\nexport type LexiconParameters = l.Infer<typeof lexiconParameters>\n\n/**\n * Schema for validating Lexicon XRPC method payloads.\n *\n * Validates input/output payload definitions for procedures and queries.\n * Payloads specify the encoding (MIME type) and optional schema for\n * the request or response body.\n */\nexport const lexiconPayload = l.object({\n  encoding: l.refine(l.string(), {\n    check: (v) => !v.includes(',') && !v.includes(';') && !v.includes(' '),\n    message:\n      'Invalid encoding string (must be a single MIME type without parameters)',\n  }),\n  schema: l.optional(\n    l.discriminatedUnion('type', [\n      lexiconRefSchema,\n      lexiconRefUnionSchema,\n      lexiconObjectSchema,\n    ]),\n  ),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon XRPC payload definition.\n *\n * Represents the structure of an input or output payload,\n * including encoding type and optional schema.\n *\n * @see {@link lexiconPayload} for the schema definition\n */\nexport type LexiconPayload = l.Infer<typeof lexiconPayload>\n\n/**\n * Schema for validating Lexicon XRPC error definitions.\n *\n * Validates error definitions that can be returned by XRPC methods.\n * Each error has a name and optional description.\n */\nexport const lexiconError = l.object({\n  name: l.string({ minLength: 1 }),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon XRPC error definition.\n *\n * Represents an error that can be returned by an XRPC method.\n *\n * @see {@link lexiconError} for the schema definition\n */\nexport type LexiconError = l.Infer<typeof lexiconError>\n\n/**\n * Schema for validating Lexicon query (GET) method definitions.\n *\n * Validates query method definitions which represent read-only HTTP GET\n * operations. Queries can have parameters, an output payload, and\n * defined error types.\n */\nexport const lexiconQuerySchema = l.object({\n  type: l.literal('query'),\n  parameters: l.optional(lexiconParameters),\n  output: l.optional(lexiconPayload),\n  errors: l.optional(l.array(lexiconError)),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon query method definition.\n *\n * Represents the structure of an XRPC query (GET) method.\n *\n * @see {@link lexiconQuerySchema} for the schema definition\n */\nexport type LexiconQuery = l.Infer<typeof lexiconQuerySchema>\n\n/**\n * Schema for validating Lexicon procedure (POST) method definitions.\n *\n * Validates procedure method definitions which represent HTTP POST\n * operations that may modify state. Procedures can have parameters,\n * input payload, output payload, and defined error types.\n */\nexport const lexiconProcedureSchema = l.object({\n  type: l.literal('procedure'),\n  parameters: l.optional(lexiconParameters),\n  input: l.optional(lexiconPayload),\n  output: l.optional(lexiconPayload),\n  errors: l.optional(l.array(lexiconError)),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon procedure method definition.\n *\n * Represents the structure of an XRPC procedure (POST) method.\n *\n * @see {@link lexiconProcedureSchema} for the schema definition\n */\nexport type LexiconProcedure = l.Infer<typeof lexiconProcedureSchema>\n\n/**\n * Schema for validating Lexicon subscription (WebSocket) method definitions.\n *\n * Validates subscription method definitions which represent real-time\n * streaming connections over WebSocket. Subscriptions have parameters,\n * a message schema defining the streamed data format, and error types.\n */\nexport const lexiconSubscriptionSchema = l.object({\n  type: l.literal('subscription'),\n  description: l.optional(l.string()),\n  parameters: l.optional(lexiconParameters),\n  message: l.object({\n    description: l.optional(l.string()),\n    schema: lexiconRefUnionSchema,\n  }),\n  errors: l.optional(l.array(lexiconError)),\n})\n\n/**\n * TypeScript type for a Lexicon subscription method definition.\n *\n * Represents the structure of an XRPC subscription (WebSocket) method.\n *\n * @see {@link lexiconSubscriptionSchema} for the schema definition\n */\nexport type LexiconSubscription = l.Infer<typeof lexiconSubscriptionSchema>\n\n// Permissions\n\n/**\n * Schema for validating language codes in Lexicon permission definitions.\n */\nexport const lexiconLanguageSchema = l.string({ format: 'language' })\n\n/**\n * TypeScript type for a BCP 47 language code string.\n *\n * @see {@link lexiconLanguageSchema} for the schema definition\n */\nexport type LexiconLanguage = l.Infer<typeof lexiconLanguageSchema>\n\n/**\n * Schema for validating language-keyed string dictionaries.\n * Used for localized text in permission definitions.\n */\nexport const lexiconLanguageDict = l.dict(lexiconLanguageSchema, l.string())\n\n/**\n * TypeScript type for a language-keyed dictionary of localized strings.\n *\n * @see {@link lexiconLanguageDict} for the schema definition\n */\nexport type LexiconLanguageDict = l.Infer<typeof lexiconLanguageDict>\n\n/**\n * Schema for validating individual Lexicon permission definitions.\n */\nexport const lexiconPermissionSchema = l.intersection(\n  l.object({\n    type: l.literal('permission'),\n    resource: l.string({ minLength: 1 }),\n  }),\n  l.dict(l.string(), l.paramSchema),\n)\n\n/**\n * TypeScript type for a Lexicon permission definition.\n *\n * Represents a single permission with a resource identifier\n * and optional additional parameters.\n *\n * @see {@link lexiconPermissionSchema} for the schema definition\n */\nexport type LexiconPermission = l.Infer<typeof lexiconPermissionSchema>\n\n/**\n * Schema for validating Lexicon permission set definitions.\n */\nexport const lexiconPermissionSetSchema = l.object({\n  type: l.literal('permission-set'),\n  permissions: l.array(lexiconPermissionSchema),\n  title: l.optional(l.string()),\n  'title:lang': l.optional(lexiconLanguageDict),\n  detail: l.optional(l.string()),\n  'detail:lang': l.optional(lexiconLanguageDict),\n  description: l.optional(l.string()),\n})\n\n/**\n * TypeScript type for a Lexicon permission set definition.\n *\n * Represents a collection of permissions with optional\n * localized title and detail text.\n *\n * @see {@link lexiconPermissionSetSchema} for the schema definition\n */\nexport type LexiconPermissionSet = l.Infer<typeof lexiconPermissionSetSchema>\n\nconst NAMED_LEXICON_SCHEMAS = [\n  ...CONCRETE_TYPES,\n  lexiconArraySchema,\n  lexiconObjectSchema,\n  lexiconTokenSchema,\n] as const\n\n/**\n * TypeScript type for any named Lexicon definition.\n *\n * Union of all definition types that can appear in the defs section\n * of a Lexicon document (excluding main-only types).\n */\nexport type NamedLexiconDefinition = l.Infer<\n  (typeof NAMED_LEXICON_SCHEMAS)[number]\n>\n\nconst MAIN_LEXICON_SCHEMAS = [\n  lexiconPermissionSetSchema,\n  lexiconProcedureSchema,\n  lexiconQuerySchema,\n  lexiconRecordSchema,\n  lexiconSubscriptionSchema,\n  ...NAMED_LEXICON_SCHEMAS,\n] as const\n\n/**\n * TypeScript type for main Lexicon definitions.\n *\n * Union of all definition types that can appear as the \"main\" entry\n * in a Lexicon document's defs section.\n */\nexport type MainLexiconDefinition = l.Infer<\n  (typeof MAIN_LEXICON_SCHEMAS)[number]\n>\n\n/**\n * Schema for validating Lexicon document identifiers (NSIDs).\n *\n * Validates that the identifier follows the Namespaced Identifier format\n * (e.g., \"com.atproto.repo.createRecord\").\n */\nexport const lexiconIdentifierSchema = l.string({ format: 'nsid' })\n\n/**\n * TypeScript type for a Lexicon document identifier.\n *\n * A Namespaced Identifier (NSID) string in reverse-domain format.\n *\n * @see {@link lexiconIdentifierSchema} for the schema definition\n */\nexport type LexiconIdentifier = l.Infer<typeof lexiconIdentifierSchema>\n\n/**\n * Schema for validating complete Lexicon document structures.\n *\n * Validates the top-level structure of a Lexicon document, including:\n * - `lexicon`: Must be 1 (the current Lexicon version)\n * - `id`: The document's NSID\n * - `revision`: Optional version number\n * - `description`: Optional document description\n * - `defs`: Map of definition names to their schemas\n *\n * The \"main\" definition (if present) can be any main-only type,\n * while other definitions are limited to named types.\n *\n * @example\n * ```ts\n * const result = lexiconDocumentSchema.parse({\n *   lexicon: 1,\n *   id: 'com.example.getProfile',\n *   defs: {\n *     main: {\n *       type: 'query',\n *       output: {\n *         encoding: 'application/json',\n *         schema: { type: 'ref', ref: '#profile' }\n *       }\n *     },\n *     profile: {\n *       type: 'object',\n *       properties: {\n *         name: { type: 'string' }\n *       }\n *     }\n *   }\n * })\n * ```\n */\nexport const lexiconDocumentSchema = l.object({\n  lexicon: l.literal(1),\n  id: lexiconIdentifierSchema,\n  revision: l.optional(l.integer()),\n  description: l.optional(l.string()),\n  defs: l.intersection(\n    l.object({\n      main: l.optional(l.discriminatedUnion('type', MAIN_LEXICON_SCHEMAS)),\n    }),\n    l.dict(\n      l.string({ minLength: 1 }),\n      l.discriminatedUnion('type', NAMED_LEXICON_SCHEMAS),\n    ),\n  ),\n})\n\n/**\n * TypeScript type for a complete Lexicon document.\n *\n * Represents the full structure of a Lexicon JSON document,\n * including the version, identifier, and all type definitions.\n *\n * @see {@link lexiconDocumentSchema} for the schema definition\n */\nexport type LexiconDocument = l.Infer<typeof lexiconDocumentSchema>\n"
  },
  {
    "path": "packages/lex/lex-document/src/lexicon-indexer.ts",
    "content": "import { LexiconDocument } from './lexicon-document.js'\n\n/**\n * Interface for indexing and retrieving Lexicon documents by their NSID.\n *\n * @example\n * ```ts\n * // Using a custom indexer implementation\n * const networkIndexer: LexiconIndexer = {\n *   async get(nsid: string) {\n *     const doc = await resolveLexicon(nsid)\n *     return doc\n *   }\n * }\n *\n * const validator = await LexiconSchemaBuilder.build(networkIndexer, 'com.example.post#main')\n * ```\n */\nexport interface LexiconIndexer {\n  /**\n   * Retrieves a Lexicon document by its NSID.\n   *\n   * @param nsid - The Namespaced Identifier of the Lexicon document to retrieve\n   * @returns A promise that resolves to the Lexicon document\n   * @throws When the document with the given NSID cannot be found\n   *\n   * @example\n   * ```ts\n   * const doc = await indexer.get('com.atproto.repo.createRecord')\n   * console.log(doc.defs.main?.type) // 'procedure'\n   * ```\n   */\n  get(nsid: string): Promise<LexiconDocument>\n\n  /**\n   * Optional async disposal method for cleanup.\n   *\n   * When implemented, allows the indexer to be used with `await using`\n   * syntax for automatic resource cleanup.\n   *\n   * @returns A promise that resolves when disposal is complete\n   */\n  [Symbol.asyncDispose]?: () => Promise<void>\n\n  /**\n   * Optional async iterator for iterating over all available Lexicon documents.\n   *\n   * @returns An async iterator yielding Lexicon documents\n   *\n   * @example\n   * ```ts\n   * if (Symbol.asyncIterator in indexer) {\n   *   for await (const doc of indexer) {\n   *     console.log(doc.id)\n   *   }\n   * }\n   * ```\n   */\n  [Symbol.asyncIterator]?: () => AsyncIterator<LexiconDocument, void, unknown>\n}\n"
  },
  {
    "path": "packages/lex/lex-document/src/lexicon-iterable-indexer.ts",
    "content": "import { LexiconDocument } from './lexicon-document.js'\nimport { LexiconIndexer } from './lexicon-indexer.js'\n\n/**\n * Lazily indexes Lexicon documents from an iterable source.\n *\n * This class implements `LexiconIndexer` by consuming documents from an\n * iterable (sync or async) and caching them for efficient retrieval.\n * Documents are indexed on-demand as they are requested or iterated over.\n *\n * @example\n * ```ts\n * // From an array of documents\n * const docs = [lexiconDoc1, lexiconDoc2, lexiconDoc3]\n * const indexer = new LexiconIterableIndexer(docs)\n *\n * // Documents are indexed lazily as requested\n * const doc = await indexer.get('com.example.post')\n * ```\n *\n * @example\n * ```ts\n * // From an async generator (e.g., reading from files)\n * async function* loadLexicons() {\n *   for (const file of lexiconFiles) {\n *     yield JSON.parse(await fs.readFile(file, 'utf8'))\n *   }\n * }\n *\n * await using indexer = new LexiconIterableIndexer(loadLexicons())\n * const schemas = await LexiconSchemaBuilder.buildAll(indexer)\n * ```\n */\nexport class LexiconIterableIndexer implements LexiconIndexer, AsyncDisposable {\n  readonly #lexicons: Map<string, LexiconDocument> = new Map()\n  readonly #iterator:\n    | AsyncIterator<LexiconDocument, void, unknown>\n    | Iterator<LexiconDocument, void, unknown>\n\n  /**\n   * Creates a new {@link LexiconIterableIndexer} from an iterable source.\n   *\n   * @param source - An iterable or async iterable of Lexicon documents.\n   *   The iterator is consumed lazily as documents are requested.\n   *\n   * @example\n   * ```ts\n   * // Sync iterable (array, Set, Map.values(), etc.)\n   * const indexer = new LexiconIterableIndexer(lexiconDocuments)\n   *\n   * // Async iterable (async generator, ReadableStream, etc.)\n   * const indexer = new LexiconIterableIndexer(asyncLexiconStream)\n   * ```\n   */\n  constructor(\n    readonly source: AsyncIterable<LexiconDocument> | Iterable<LexiconDocument>,\n  ) {\n    this.#iterator =\n      Symbol.asyncIterator in source\n        ? source[Symbol.asyncIterator]()\n        : source[Symbol.iterator]()\n  }\n\n  /**\n   * Retrieves a Lexicon document by its NSID.\n   *\n   * If the document has already been indexed, it is returned from cache.\n   * Otherwise, the source iterator is consumed until the document is found.\n   *\n   * @see {@link LexiconIndexer.get}\n   */\n  async get(id: string): Promise<LexiconDocument> {\n    const cached = this.#lexicons.get(id)\n    if (cached) return cached\n\n    for await (const doc of this) {\n      if (doc.id === id) return doc\n    }\n\n    throw Object.assign(new Error(`Lexicon ${id} not found`), {\n      code: 'ENOENT',\n    })\n  }\n\n  async *[Symbol.asyncIterator](): AsyncIterator<\n    LexiconDocument,\n    void,\n    undefined\n  > {\n    const returned = new Set<string>()\n\n    for (const doc of this.#lexicons.values()) {\n      returned.add(doc.id)\n      yield doc\n    }\n\n    do {\n      const { value, done } = await this.#iterator.next()\n\n      if (done) break\n\n      if (returned.has(value.id)) {\n        const err = new Error(`Duplicate lexicon document id: ${value.id}`)\n        await this.#iterator.throw?.(err)\n        throw err // In case iterator.throw does not exist or does not throw\n      }\n\n      this.#lexicons.set(value.id, value)\n      returned.add(value.id)\n      yield value\n    } while (true)\n\n    // At this point, the underlying iterator is done. However, there may have\n    // been requests (.get()) for documents that caused the iterator to yield\n    // those documents during concurrent execution of this loop. If that was the\n    // case, new documents may have been added to `#lexicons` that have not yet\n    // been yielded. We need to yield those as well. Since we yield control back\n    // to the caller, we need to repeat this process until no new documents\n    // appear sunce we don't know what happens.\n\n    for (const doc of this.#lexicons.values()) {\n      if (!returned.has(doc.id)) {\n        returned.add(doc.id)\n        yield doc\n      }\n    }\n  }\n\n  async [Symbol.asyncDispose](): Promise<void> {\n    await this.#iterator.return?.()\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-document/src/lexicon-schema-builder.test.ts",
    "content": "import { beforeAll, describe, expect, it } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport { l } from '@atproto/lex-schema'\nimport { LexiconDocument, lexiconDocumentSchema } from './lexicon-document.js'\nimport { LexiconIterableIndexer } from './lexicon-iterable-indexer.js'\nimport { LexiconSchemaBuilder } from './lexicon-schema-builder.js'\n\ndescribe('LexiconSchemaBuilder', () => {\n  let schemas: Map<\n    string,\n    | l.Validator<unknown>\n    | l.Query\n    | l.Subscription\n    | l.Procedure\n    | l.PermissionSet\n  >\n\n  const getSchema = <T extends abstract new (...args: any) => any>(\n    ref: string,\n    type: T,\n  ) => {\n    const schema = schemas.get(ref)\n    expect(schema).toBeDefined()\n    expect(schema).toBeInstanceOf(type)\n    return schema as InstanceType<T>\n  }\n\n  beforeAll(async () => {\n    const indexer = new LexiconIterableIndexer([\n      lexiconDocumentSchema.parse({\n        lexicon: 1,\n        id: 'com.example.kitchenSink',\n        defs: {\n          main: {\n            type: 'record',\n            description: 'A record',\n            key: 'tid',\n            record: {\n              type: 'object',\n              required: [\n                'object',\n                'array',\n                'boolean',\n                'integer',\n                'string',\n                'bytes',\n                'cidLink',\n              ],\n              properties: {\n                object: { type: 'ref', ref: '#object' },\n                array: { type: 'array', items: { type: 'string' } },\n                boolean: { type: 'boolean' },\n                integer: { type: 'integer' },\n                string: { type: 'string' },\n                bytes: { type: 'bytes' },\n                cidLink: { type: 'cid-link' },\n              },\n            },\n          },\n          object: {\n            type: 'object',\n            required: ['object', 'array', 'boolean', 'integer', 'string'],\n            properties: {\n              object: { type: 'ref', ref: '#subObject' },\n              array: { type: 'array', items: { type: 'string' } },\n              boolean: { type: 'boolean' },\n              integer: { type: 'integer' },\n              string: { type: 'string' },\n              refToEnumWithDefault: { type: 'ref', ref: '#enumWithDefault' },\n            },\n          },\n          subObject: {\n            type: 'object',\n            required: ['boolean'],\n            properties: {\n              boolean: { type: 'boolean' },\n            },\n          },\n          enumWithDefault: {\n            type: 'string',\n            default: 'option3',\n            enum: ['option1', 'option2', 'option3'],\n          },\n        },\n      }),\n    ])\n    schemas = await LexiconSchemaBuilder.buildAll(indexer)\n  })\n\n  it('Validates records correctly', () => {\n    const schema = getSchema('com.example.kitchenSink#main', l.RecordSchema)\n\n    const value = {\n      $type: 'com.example.kitchenSink',\n      object: {\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        refToEnumWithDefault: 'option3',\n      },\n      array: ['one', 'two'],\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      datetime: new Date().toISOString(),\n      atUri: 'at://did:web:example.com/com.example.test/self',\n      did: 'did:web:example.com',\n      cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      bytes: new Uint8Array([0, 1, 2, 3]),\n      cidLink: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n    }\n\n    expect(schema.safeParse(value)).toStrictEqual({ success: true, value })\n  })\n\n  it('Validates objects correctly', () => {\n    const schema = getSchema(\n      'com.example.kitchenSink#object',\n      l.TypedObjectSchema,\n    )\n\n    const value = {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      integer: 123,\n      string: 'string',\n    }\n\n    expect(schema.safeParse(value)).toStrictEqual({\n      success: true,\n      value: {\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        refToEnumWithDefault: 'option3',\n      },\n    })\n  })\n\n  it('rejects invalid enum values', () => {\n    const schema = getSchema(\n      'com.example.kitchenSink#object',\n      l.TypedObjectSchema,\n    )\n\n    const value = {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      refToEnumWithDefault: 'invalidOption',\n    }\n\n    expect(schema.safeParse(value)).toMatchObject({\n      success: false,\n      reason: {\n        issues: [\n          {\n            code: 'invalid_value',\n            input: 'invalidOption',\n            values: ['option1', 'option2', 'option3'],\n          },\n        ],\n      },\n    })\n  })\n\n  it('does not apply defaults when validating', () => {\n    const schema = getSchema(\n      'com.example.kitchenSink#object',\n      l.TypedObjectSchema,\n    )\n\n    const value = {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      integer: 123,\n      string: 'string',\n    }\n\n    expect(schema.safeValidate(value)).toStrictEqual({\n      success: true,\n      value: {\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n      },\n    })\n  })\n\n  it('allows missing optional record fields', () => {\n    const schema = getSchema(\n      'com.example.kitchenSink#object',\n      l.TypedObjectSchema,\n    )\n\n    expect(\n      schema.matches({\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n      }),\n    ).toBe(true)\n  })\n\n  it('Rejects missing required record fields', () => {\n    const schema = getSchema(\n      'com.example.kitchenSink#object',\n      l.TypedObjectSchema,\n    )\n\n    const value = {\n      object: { boolean: true },\n      // array: ['one', 'two'],\n      boolean: true,\n      integer: 123,\n      string: 'string',\n    }\n\n    expect(schema.safeParse(value)).toMatchObject({\n      success: false,\n      reason: { issues: [{ code: 'required_key', key: 'array' }] },\n    })\n  })\n\n  it('fails validation when ref uri has multiple hash segments', async () => {\n    const schema: LexiconDocument = {\n      lexicon: 1,\n      id: 'com.example.invalid',\n      defs: {\n        main: {\n          type: 'object',\n          properties: {\n            test: { type: 'ref', ref: 'com.example.invalid#test#test' },\n          },\n        },\n      },\n    }\n\n    await expect(async () => {\n      await LexiconSchemaBuilder.buildAll(new LexiconIterableIndexer([schema]))\n    }).rejects.toThrow('Uri can only have one hash segment')\n  })\n\n  it('fails lexicon parsing when uri is invalid', async () => {\n    const schema: LexiconDocument = {\n      lexicon: 1,\n      id: 'com.example.invalid',\n      defs: {\n        main: {\n          type: 'object',\n          properties: {\n            test: { type: 'ref', ref: 'com.example.missing#main' },\n          },\n        },\n      },\n    }\n\n    await expect(async () => {\n      await LexiconSchemaBuilder.buildAll(new LexiconIterableIndexer([schema]))\n    }).rejects.toThrow('Lexicon com.example.missing not found')\n  })\n\n  it('fails lexicon parsing when uri is invalid', async () => {\n    const schema: LexiconDocument = {\n      lexicon: 1,\n      id: 'com.example.invalid',\n      defs: {\n        main: {\n          type: 'object',\n          properties: {\n            test: { type: 'ref', ref: 'com.example.invalid#nonexistent' },\n          },\n        },\n      },\n    }\n\n    await expect(async () => {\n      await LexiconSchemaBuilder.buildAll(new LexiconIterableIndexer([schema]))\n    }).rejects.toThrow(\n      'No definition found for hash \"\"nonexistent\"\" in com.example.invalid',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-document/src/lexicon-schema-builder.ts",
    "content": "import { LexValue } from '@atproto/lex-data'\nimport { l } from '@atproto/lex-schema'\nimport {\n  LexiconArray,\n  LexiconArrayItems,\n  LexiconDocument,\n  LexiconError,\n  LexiconObject,\n  LexiconParameters,\n  LexiconPayload,\n  LexiconRef,\n  LexiconRefUnion,\n} from './lexicon-document.js'\nimport { LexiconIndexer } from './lexicon-indexer.js'\n\n/**\n * Builds validators for Lexicon documents.\n *\n * This class converts Lexicon type definitions into runtime validators\n * that can validate data against the schema. It handles reference resolution,\n * supporting both local (`#defName`) and cross-document (`nsid#defName`) refs.\n *\n * @example\n * ```ts\n * import { LexiconSchemaBuilder, LexiconIterableIndexer } from '@atproto/lex-document'\n *\n * // Build a single validator\n * const indexer = new LexiconIterableIndexer(lexiconDocs)\n * const validator = await LexiconSchemaBuilder.build(indexer, 'com.example.post#main')\n *\n * // Validate data\n * const result = validator.safeParse(myPostData)\n * if (result.success) {\n *   console.log('Valid:', result.value)\n * } else {\n *   console.log('Invalid:', result.error)\n * }\n * ```\n *\n * @example\n * ```ts\n * // Build all validators from an iterable indexer\n * const indexer = new LexiconIterableIndexer(lexiconDocs)\n * const allSchemas = await LexiconSchemaBuilder.buildAll(indexer)\n *\n * for (const [ref, schema] of allSchemas) {\n *   console.log(`Built validator for ${ref}`)\n * }\n * ```\n */\nexport class LexiconSchemaBuilder implements AsyncDisposable {\n  /**\n   * Builds a validator for a single Lexicon definition reference.\n   *\n   * @param indexer - The Lexicon indexer to resolve documents from\n   * @param fullRef - The full reference to build, in format \"nsid#defName\"\n   * @returns A promise resolving to a validator for the referenced definition\n   * @throws Error if the reference does not point to a schema type\n   *\n   * @example\n   * ```ts\n   * const validator = await LexiconSchemaBuilder.build(\n   *   indexer,\n   *   'app.bsky.feed.post#main'\n   * )\n   *\n   * validator.parse(postRecord) // Throws if invalid\n   * ```\n   */\n  static async build(\n    indexer: LexiconIndexer,\n    fullRef: string,\n  ): Promise<l.Schema<LexValue>> {\n    const ctx = new LexiconSchemaBuilder(indexer)\n    try {\n      const result = await ctx.buildFullRef(fullRef)\n      if (!(result instanceof l.Schema)) {\n        throw new Error(`Ref ${fullRef} is not a schema type`)\n      }\n      return result\n    } finally {\n      await ctx.done()\n    }\n  }\n\n  /**\n   * Builds validators for all definitions in all documents from an iterable indexer.\n   *\n   * This method iterates over all Lexicon documents available in the indexer\n   * and builds validators for every definition in each document.\n   *\n   * @param indexer - An iterable Lexicon indexer (must implement `Symbol.asyncIterator`)\n   * @returns A promise resolving to a Map of full references to their validators.\n   *   The map values can be validators, Query, Subscription, Procedure, or PermissionSet.\n   * @throws Error if the indexer does not support iteration\n   *\n   * @example\n   * ```ts\n   * const indexer = new LexiconIterableIndexer(allLexiconDocs)\n   * const schemas = await LexiconSchemaBuilder.buildAll(indexer)\n   *\n   * // Access a specific schema\n   * const postSchema = schemas.get('app.bsky.feed.post#main')\n   *\n   * // Iterate all schemas\n   * for (const [ref, schema] of schemas) {\n   *   console.log(ref, schema)\n   * }\n   * ```\n   */\n  static async buildAll(indexer: LexiconIndexer) {\n    const builder = new LexiconSchemaBuilder(indexer)\n    const schemas = new Map<\n      string,\n      | l.Schema<LexValue>\n      | l.Query\n      | l.Subscription\n      | l.Procedure\n      | l.PermissionSet\n    >()\n    if (!isAsyncIterableObject(indexer)) {\n      throw new Error('An iterable indexer is required to build all schemas')\n    }\n    try {\n      for await (const doc of indexer) {\n        for (const hash of Object.keys(doc.defs)) {\n          const fullRef = `${doc.id}#${hash}`\n          const schema = await builder.buildFullRef(fullRef)\n          schemas.set(fullRef, schema)\n        }\n      }\n      return schemas\n    } finally {\n      await builder.done()\n    }\n  }\n\n  #asyncTasks = new AsyncTasks()\n\n  /**\n   * Creates a new LexiconSchemaBuilder instance.\n   *\n   * Note: For most use cases, prefer using the static `build()` or `buildAll()`\n   * methods instead of instantiating directly.\n   *\n   * @param indexer - The Lexicon indexer to resolve documents from\n   */\n  constructor(protected indexer: LexiconIndexer) {}\n\n  /**\n   * Waits for all pending reference resolution tasks to complete.\n   *\n   * When building schemas with cross-references, the builder schedules\n   * async tasks to resolve those references. This method must be called\n   * to ensure all references are fully resolved before using the validators.\n   *\n   * @returns A promise that resolves when all pending tasks are complete\n   * @throws Rethrows any errors from failed reference resolution\n   */\n  async done(): Promise<void> {\n    await this.#asyncTasks.done()\n  }\n\n  async [Symbol.asyncDispose]() {\n    await this.done()\n  }\n\n  /**\n   * Builds a validator for a full reference (memoized).\n   *\n   * Results are cached, so calling with the same reference returns\n   * the same promise/result.\n   *\n   * @param fullRef - The full reference in format \"nsid#defName\"\n   * @returns A promise resolving to the built schema or method definition\n   */\n  buildFullRef = memoize(async (fullRef: string) => {\n    const { nsid, hash } = parseRef(fullRef)\n\n    const doc = await this.indexer.get(nsid)\n\n    return this.compileDef(doc, hash)\n  })\n\n  protected buildRefGetter(fullRef: string): () => l.Schema<LexValue> {\n    let schema: l.Schema<LexValue>\n\n    this.#asyncTasks.add(\n      this.buildFullRef(fullRef).then((v) => {\n        if (!(v instanceof l.Schema)) {\n          throw new Error(`Only refs to schema types are allowed`)\n        }\n        schema = v\n      }),\n    )\n\n    return () => {\n      if (schema) return schema\n      throw new Error('Validator not yet built. Did you await done()?')\n    }\n  }\n\n  protected buildTypedRefGetter(\n    fullRef: string,\n  ): () => l.TypedObjectSchema | l.RecordSchema {\n    let validator: l.TypedObjectSchema | l.RecordSchema\n\n    this.#asyncTasks.add(\n      this.buildFullRef(fullRef).then((v) => {\n        if (v instanceof l.TypedObjectSchema || v instanceof l.RecordSchema) {\n          validator = v\n        } else {\n          throw new Error(\n            'Only refs to records and object definitions are allowed',\n          )\n        }\n      }),\n    )\n\n    return () => {\n      if (validator) return validator\n      throw new Error('Validator not yet built. Did you await done()?')\n    }\n  }\n\n  protected compileDef(doc: LexiconDocument, hash: string) {\n    const def = Object.hasOwn(doc.defs, hash) ? doc.defs[hash] : null\n    if (!def) {\n      throw new Error(\n        `No definition found for hash \"${JSON.stringify(hash)}\" in ${doc.id}`,\n      )\n    }\n    switch (def.type) {\n      case 'permission-set':\n        return l.permissionSet(\n          doc.id,\n          def.permissions.map(({ resource, type, ...p }) =>\n            l.permission(resource, p),\n          ),\n          def,\n        )\n      case 'procedure':\n        return l.procedure(\n          doc.id,\n          this.compileParams(doc, def.parameters),\n          this.compilePayload(doc, def.input),\n          this.compilePayload(doc, def.output),\n          this.compileErrors(doc, def.errors),\n        )\n      case 'query':\n        return l.query(\n          doc.id,\n          this.compileParams(doc, def.parameters),\n          this.compilePayload(doc, def.output),\n          this.compileErrors(doc, def.errors),\n        )\n      case 'subscription':\n        return l.subscription(\n          doc.id,\n          this.compileParams(doc, def.parameters),\n          this.compilePayloadSchema(doc, def.message.schema),\n          this.compileErrors(doc, def.errors),\n        )\n      case 'token':\n        return l.token(doc.id, hash)\n      case 'record':\n        return l.record(def.key, doc.id, this.compileObject(doc, def.record))\n      case 'object':\n        return l.typedObject(doc.id, hash, this.compileObject(doc, def))\n      default:\n        return this.compileLeaf(doc, def)\n    }\n  }\n\n  protected compileLeaf(\n    doc: LexiconDocument,\n    def: LexiconArray | LexiconArrayItems,\n  ): l.Schema<LexValue> {\n    if (\n      'const' in def &&\n      'enum' in def &&\n      def.enum != null &&\n      def.const !== undefined &&\n      !(def.enum as readonly unknown[]).includes(def.const)\n    ) {\n      return l.never()\n    }\n\n    switch (def.type) {\n      case 'string': {\n        const schema = l.string(def)\n        if (def.default != null) schema.check(def.default)\n        if (def.const != null) schema.check(def.const)\n        if (def.enum != null) for (const v of def.enum) schema.check(v)\n\n        const result =\n          def.const != null\n            ? l.literal(def.const)\n            : def.enum != null\n              ? l.enum(def.enum)\n              : schema\n\n        return def.default != null ? l.withDefault(result, def.default) : result\n      }\n      case 'integer': {\n        const schema = l.integer(def)\n        if (def.default != null) schema.check(def.default)\n        if (def.const != null) schema.check(def.const)\n        if (def.enum != null) for (const v of def.enum) schema.check(v)\n\n        const result =\n          def.const != null\n            ? l.literal(def.const)\n            : def.enum != null\n              ? l.enum(def.enum)\n              : schema\n\n        return def.default != null ? l.withDefault(result, def.default) : result\n      }\n      case 'boolean': {\n        const result = def.const != null ? l.literal(def.const) : l.boolean()\n\n        return def.default != null ? l.withDefault(result, def.default) : result\n      }\n      case 'blob':\n        return l.blob(def)\n      case 'cid-link':\n        return l.cid()\n      case 'bytes':\n        return l.bytes(def)\n      case 'unknown':\n        return l.lexMap()\n      case 'array':\n        return l.array(this.compileLeaf(doc, def.items), def)\n      default:\n        return this.compileRef(doc, def)\n    }\n  }\n\n  protected compileRef(\n    doc: LexiconDocument,\n    def: LexiconRef | LexiconRefUnion,\n  ): l.Schema<LexValue> {\n    switch (def.type) {\n      case 'ref':\n        return l.ref(this.buildRefGetter(buildFullRef(doc, def.ref)))\n      case 'union':\n        return l.typedUnion(\n          def.refs.map((r) =>\n            l.typedRef(this.buildTypedRefGetter(buildFullRef(doc, r))),\n          ),\n          def.closed ?? false,\n        )\n      default:\n        // @ts-expect-error\n        throw new Error(`Unknown lexicon type: ${def.type}`)\n    }\n  }\n\n  protected compileObject(doc: LexiconDocument, def: LexiconObject) {\n    const props: Record<string, l.Schema<undefined | LexValue>> = {}\n    for (const [key, propDef] of Object.entries(def.properties)) {\n      if (propDef === undefined) continue\n\n      const isNullable = def.nullable?.includes(key)\n      const isRequired = def.required?.includes(key)\n\n      let schema: l.Schema<undefined | LexValue> = this.compileLeaf(\n        doc,\n        propDef,\n      )\n\n      if (isNullable) {\n        schema = l.nullable(schema)\n      }\n\n      if (!isRequired) {\n        schema = l.optional(schema)\n      }\n\n      props[key] = schema\n    }\n    return l.object(props)\n  }\n\n  protected compilePayload(\n    doc: LexiconDocument,\n    def: LexiconPayload | undefined,\n  ) {\n    return l.payload(\n      def?.encoding,\n      def?.schema ? this.compilePayloadSchema(doc, def.schema) : undefined,\n    )\n  }\n\n  protected compileErrors(\n    _doc: LexiconDocument,\n    errors?: readonly LexiconError[],\n  ): undefined | string[] {\n    return errors?.map((e) => e.name)\n  }\n\n  protected compilePayloadSchema(\n    doc: LexiconDocument,\n    def: LexiconObject | LexiconRef | LexiconRefUnion,\n  ): l.Schema<LexValue, LexValue> {\n    switch (def.type) {\n      case 'object':\n        return this.compileObject(doc, def)\n      default:\n        return this.compileRef(doc, def)\n    }\n  }\n\n  protected compileParams(doc: LexiconDocument, def?: LexiconParameters) {\n    if (!def) return l.params()\n\n    const shape: l.ParamsShape = {}\n    for (const [paramName, paramDef] of Object.entries(def.properties)) {\n      if (paramDef === undefined) continue\n\n      const isRequired = def.required?.includes(paramName)\n\n      const propSchema = this.compileLeaf(doc, paramDef) as\n        | l.StringSchema\n        | l.BooleanSchema\n        | l.IntegerSchema\n        | l.ArraySchema<l.StringSchema>\n        | l.ArraySchema<l.BooleanSchema>\n        | l.ArraySchema<l.IntegerSchema>\n\n      shape[paramName] = isRequired ? propSchema : l.optional(propSchema)\n    }\n\n    return l.params(shape)\n  }\n}\n\nclass AsyncTasks {\n  /**\n   * A set that, eventually, contains only rejected promises.\n   */\n  #promises = new Set<Promise<void>>()\n\n  async done(): Promise<void> {\n    do {\n      // @NOTE this is going to throw on the first rejected promise (which is\n      // what we want)\n      for (const p of this.#promises) await p\n      // At this point, all settled promises should have been removed. If\n      // this.#promises is not empty, it means new promises were added during\n      // the awaiting process, so we loop again.\n    } while (this.#promises.size > 0)\n  }\n\n  add(p: Promise<void>) {\n    const promise = Promise.resolve(p).then(() => {\n      // No need to keep the promise any longer\n      this.#promises.delete(promise)\n    })\n\n    void promise.catch((_err) => {\n      // ignore errors here, they should be caught though done()\n    })\n\n    this.#promises.add(promise)\n  }\n}\n\nfunction parseRef(fullRef: string) {\n  const { length, 0: nsid, 1: hash } = fullRef.split('#')\n  if (length !== 2) throw new Error('Uri can only have one hash segment')\n  if (!nsid || !hash) throw new Error('Invalid ref, missing hash')\n  return { nsid, hash }\n}\n\nfunction buildFullRef(from: LexiconDocument, ref: string) {\n  if (ref.startsWith('#')) return `${from.id}${ref}`\n  return ref\n}\n\nexport function memoize<Fn extends (arg: string) => unknown>(fn: Fn): Fn {\n  const cache = new Map<string, ReturnType<Fn>>()\n  return ((arg: string) => {\n    if (cache.has(arg)) return cache.get(arg)!\n    const result = fn(arg) as ReturnType<Fn>\n    cache.set(arg, result)\n    return result\n  }) as Fn\n}\n\nfunction isAsyncIterableObject<T>(\n  obj: T,\n): obj is T & object & AsyncIterable<unknown> {\n  return (\n    obj != null &&\n    typeof obj === 'object' &&\n    Symbol.asyncIterator in obj &&\n    typeof obj[Symbol.asyncIterator] === 'function'\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-document/tests/fixtures.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { lexiconDocumentSchema } from '../src/index.js'\nimport invalidLexicons from './lexicon-invalid.json' with { type: 'json' }\nimport validLexicons from './lexicon-valid.json' with { type: 'json' }\n\ndescribe('fixtures', () => {\n  describe('valid lexicons', () => {\n    for (const { name, lexicon } of validLexicons) {\n      it(name, () => {\n        expect(lexiconDocumentSchema.parse(lexicon)).toBe(lexicon)\n      })\n    }\n  })\n\n  describe('invalid lexicons', () => {\n    for (const { name, lexicon } of invalidLexicons) {\n      it(name, () => {\n        expect(lexiconDocumentSchema.safeParse(lexicon)).toMatchObject({\n          success: false,\n        })\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-document/tests/lexicon-invalid.json",
    "content": "[\n  {\n    \"name\": \"invalid lexicon field\",\n    \"lexicon\": {\n      \"lexicon\": \"one\",\n      \"id\": \"com.example.lexicon\",\n      \"defs\": {\n        \"demo\": {\n          \"type\": \"integer\"\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"invalid id field\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": 2,\n      \"defs\": {\n        \"demo\": {\n          \"type\": \"integer\"\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"defined unknown\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon\",\n      \"defs\": {\n        \"demo\": {\n          \"type\": \"unknown\"\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"defined ref\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon\",\n      \"defs\": {\n        \"demo\": {\n          \"type\": \"ref\",\n          \"ref\": \"com.atproto.repo.strongRef\"\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"non-main primary\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon\",\n      \"defs\": {\n        \"demo\": {\n          \"type\": \"record\",\n          \"key\": \"any\",\n          \"record\": {\n            \"type\": \"object\"\n          }\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"record missing type object\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon\",\n      \"defs\": {\n        \"main\": {\n          \"type\": \"record\",\n          \"key\": \"any\",\n          \"record\": {\n            \"properties\": {\n              \"b\": {\n                \"type\": \"boolean\"\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "packages/lex/lex-document/tests/lexicon-valid.json",
    "content": "[\n  {\n    \"name\": \"minimal\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon\",\n      \"defs\": {\n        \"demo\": {\n          \"type\": \"integer\"\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"minimal record\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon.record\",\n      \"defs\": {\n        \"main\": {\n          \"type\": \"record\",\n          \"key\": \"any\",\n          \"record\": {\n            \"type\": \"object\",\n            \"properties\": {}\n          }\n        }\n      }\n    }\n  },\n  {\n    \"name\": \"basic permission-set\",\n    \"lexicon\": {\n      \"lexicon\": 1,\n      \"id\": \"com.example.lexicon.perms\",\n      \"defs\": {\n        \"main\": {\n          \"type\": \"permission-set\",\n          \"title\": \"test case\",\n          \"permissions\": [\n            {\n              \"type\": \"permission\",\n              \"resource\": \"repo\",\n              \"collection\": [\"com.example.calendar.event\"],\n              \"action\": [\"delete\", \"create\"]\n            },\n            {\n              \"type\": \"permission\",\n              \"resource\": \"repo\",\n              \"collection\": [\"com.example.calendar.rsvp\"]\n            },\n            {\n              \"type\": \"permission\",\n              \"resource\": \"rpc\",\n              \"lxm\": [\"com.example.lexicon.endpoint\"],\n              \"aud\": \"*\"\n            },\n            {\n              \"type\": \"permission\",\n              \"resource\": \"rpc\",\n              \"lxm\": [\"com.example.lexicon.endpointTwo\"],\n              \"inheritAud\": true\n            }\n          ]\n        }\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "packages/lex/lex-document/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-document/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-document/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-document/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-installer/CHANGELOG.md",
    "content": "# @atproto/lex-installer\n\n## 0.0.22\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-data@0.0.14\n  - @atproto/lex-resolver@0.0.19\n  - @atproto/lex-builder@0.0.19\n  - @atproto/lex-document@0.0.17\n  - @atproto/lex-cbor@0.0.15\n\n## 0.0.21\n\n### Patch Changes\n\n- Updated dependencies [[`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-schema@0.0.15\n  - @atproto/syntax@0.5.1\n  - @atproto/lex-resolver@0.0.18\n  - @atproto/lex-builder@0.0.18\n  - @atproto/lex-document@0.0.16\n\n## 0.0.20\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-document@0.0.15\n  - @atproto/lex-resolver@0.0.17\n  - @atproto/lex-builder@0.0.17\n  - @atproto/lex-cbor@0.0.14\n\n## 0.0.19\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-resolver@0.0.16\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies [[`66b7295`](https://github.com/bluesky-social/atproto/commit/66b72950e8bcb39cac3382116bd282b3bb692f16)]:\n  - @atproto/lex-cbor@0.0.13\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`cd9deb6`](https://github.com/bluesky-social/atproto/commit/cd9deb6f210b91661595398cb2ef70bc40eccabe), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-document@0.0.14\n  - @atproto/lex-builder@0.0.16\n  - @atproto/lex-data@0.0.12\n  - @atproto/lex-resolver@0.0.15\n  - @atproto/lex-cbor@0.0.12\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`619068f`](https://github.com/bluesky-social/atproto/commit/619068fb81203b3b43b632892bdcb0a5067f7fe4)]:\n  - @atproto/lex-builder@0.0.15\n  - @atproto/lex-resolver@0.0.14\n\n## 0.0.15\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-document@0.0.13\n  - @atproto/lex-resolver@0.0.14\n  - @atproto/lex-cbor@0.0.11\n  - @atproto/lex-data@0.0.11\n  - @atproto/lex-builder@0.0.14\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-resolver@0.0.13\n  - @atproto/lex-cbor@0.0.10\n  - @atproto/lex-document@0.0.12\n  - @atproto/lex-schema@0.0.11\n  - @atproto/lex-builder@0.0.13\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-resolver@0.0.12\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`ecf5921`](https://github.com/bluesky-social/atproto/commit/ecf59214d59d9d2530c197c0679d26e76c6a60ef), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/syntax@0.4.3\n  - @atproto/lex-cbor@0.0.9\n  - @atproto/lex-data@0.0.9\n  - @atproto/lex-builder@0.0.12\n  - @atproto/lex-document@0.0.11\n  - @atproto/lex-resolver@0.0.11\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/lex-cbor@0.0.8\n  - @atproto/lex-document@0.0.10\n  - @atproto/lex-resolver@0.0.10\n  - @atproto/lex-schema@0.0.9\n  - @atproto/lex-builder@0.0.11\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-cbor@0.0.7\n  - @atproto/lex-builder@0.0.10\n  - @atproto/lex-document@0.0.9\n  - @atproto/lex-resolver@0.0.9\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-builder@0.0.9\n  - @atproto/lex-data@0.0.6\n  - @atproto/lex-schema@0.0.7\n  - @atproto/lex-resolver@0.0.8\n  - @atproto/lex-cbor@0.0.6\n  - @atproto/lex-document@0.0.8\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n  - @atproto/lex-builder@0.0.8\n  - @atproto/lex-data@0.0.5\n  - @atproto/lex-document@0.0.7\n  - @atproto/lex-resolver@0.0.7\n  - @atproto/lex-cbor@0.0.5\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n  - @atproto/lex-schema@0.0.5\n  - @atproto/lex-builder@0.0.7\n  - @atproto/lex-cbor@0.0.4\n  - @atproto/lex-resolver@0.0.6\n  - @atproto/lex-document@0.0.6\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`e39ca11`](https://github.com/bluesky-social/atproto/commit/e39ca114accac65070dcdd424a181821aad6d99d), [`7e1d458`](https://github.com/bluesky-social/atproto/commit/7e1d45877bca0f615e7b1313cfcc66823b3de758)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/lex-document@0.0.5\n  - @atproto/lex-builder@0.0.6\n  - @atproto/lex-cbor@0.0.3\n  - @atproto/lex-resolver@0.0.5\n  - @atproto/lex-schema@0.0.4\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework object validation logic to work without `options` argument\n\n- [#4384](https://github.com/bluesky-social/atproto/pull/4384) [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use the record CID in the lexicon manifest instead of the CID of the lexicon definition\n\n- [#4385](https://github.com/bluesky-social/atproto/pull/4385) [`0f2fc65`](https://github.com/bluesky-social/atproto/commit/0f2fc6592f0c89d26ac7a2ef70b12cbd15a18d05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly check for lexicon manifest changes by sorting lexicons before comparison\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use string formats from `@atproto/syntax`\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`03a2a4b`](https://github.com/bluesky-social/atproto/commit/03a2a4bb3814ced7ad1d4fe6c94b5348a3bbc097), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`a17d2e8`](https://github.com/bluesky-social/atproto/commit/a17d2e8a59ceb00fa8197642e0767fcc776d5b70), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba)]:\n  - @atproto/lex-schema@0.0.3\n  - @atproto/lex-document@0.0.4\n  - @atproto/lex-builder@0.0.5\n  - @atproto/lex-resolver@0.0.4\n  - @atproto/lex-data@0.0.2\n  - @atproto/lex-cbor@0.0.2\n  - @atproto/syntax@0.4.2\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4380](https://github.com/bluesky-social/atproto/pull/4380) [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deterministically order lexicon manifest items\n\n- Updated dependencies [[`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4), [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4)]:\n  - @atproto/lex-schema@0.0.2\n  - @atproto/lex-builder@0.0.4\n  - @atproto/lex-document@0.0.3\n  - @atproto/lex-resolver@0.0.3\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`5ffd612`](https://github.com/bluesky-social/atproto/commit/5ffd6129909071e979c30f31266119865ab582b6)]:\n  - @atproto/lex-document@0.0.2\n  - @atproto/lex-builder@0.0.3\n  - @atproto/lex-resolver@0.0.2\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [[`7456f53`](https://github.com/bluesky-social/atproto/commit/7456f53e45fb3eef2f3bbdf2513da2d8ab078d80)]:\n  - @atproto/lex-builder@0.0.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-builder@0.0.1\n  - @atproto/lex-cbor@0.0.1\n  - @atproto/lex-data@0.0.1\n  - @atproto/lex-document@0.0.1\n  - @atproto/lex-resolver@0.0.1\n  - @atproto/lex-schema@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-installer/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-installer\",\n  \"version\": \"0.0.22\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon document packet manager for AT Lexicons\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"install\",\n    \"lex\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-installer\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-builder\": \"workspace:^\",\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-document\": \"workspace:^\",\n    \"@atproto/lex-resolver\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/src/fs.ts",
    "content": "import { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\n/**\n * Reads and parses a JSON file from the filesystem.\n *\n * @param path - Absolute or relative path to the JSON file\n * @returns The parsed JSON content\n * @throws {Error} When the file cannot be read (e.g., ENOENT, EACCES)\n * @throws {SyntaxError} When the file contains invalid JSON\n *\n * @example\n * ```typescript\n * import { readJsonFile } from '@atproto/lex-installer'\n *\n * const manifest = await readJsonFile('./lexicons.manifest.json')\n * ```\n *\n * @example\n * Handle missing file:\n * ```typescript\n * import { readJsonFile, isEnoentError } from '@atproto/lex-installer'\n *\n * try {\n *   const data = await readJsonFile('./config.json')\n * } catch (err) {\n *   if (isEnoentError(err)) {\n *     console.log('File does not exist, using defaults')\n *   } else {\n *     throw err\n *   }\n * }\n * ```\n */\nexport async function readJsonFile(path: string): Promise<unknown> {\n  const contents = await readFile(path, 'utf8')\n  return JSON.parse(contents)\n}\n\n/**\n * Writes data as formatted JSON to a file.\n *\n * The function:\n * - Creates parent directories if they don't exist\n * - Formats JSON with 2-space indentation\n * - Overwrites existing files\n * - Sets file permissions to 0o644 (rw-r--r--)\n *\n * @param path - Absolute or relative path for the output file\n * @param data - Data to serialize as JSON\n * @throws {Error} When the file cannot be written\n *\n * @example\n * ```typescript\n * import { writeJsonFile } from '@atproto/lex-installer'\n *\n * await writeJsonFile('./output/data.json', {\n *   name: 'example',\n *   values: [1, 2, 3],\n * })\n * ```\n *\n * @example\n * Write a lexicon document:\n * ```typescript\n * import { writeJsonFile } from '@atproto/lex-installer'\n *\n * await writeJsonFile('./lexicons/app/bsky/feed/post.json', lexiconDocument)\n * ```\n */\nexport async function writeJsonFile(\n  path: string,\n  data: unknown,\n): Promise<void> {\n  await mkdir(dirname(path), { recursive: true })\n  const contents = JSON.stringify(data, null, 2)\n  await writeFile(path, contents, {\n    encoding: 'utf8',\n    mode: 0o644,\n    flag: 'w', // override\n  })\n}\n\n/**\n * Checks if an error is an ENOENT (file not found) error.\n *\n * Useful for handling cases where a file may or may not exist,\n * such as reading an optional configuration file.\n *\n * @param err - The error to check\n * @returns `true` if the error is an ENOENT error, `false` otherwise\n *\n * @example\n * ```typescript\n * import { readFile } from 'node:fs/promises'\n * import { isEnoentError } from '@atproto/lex-installer'\n *\n * const config = await readFile('./config.json').catch((err) => {\n *   if (isEnoentError(err)) {\n *     return { defaults: true }\n *   }\n *   throw err\n * })\n * ```\n *\n * @example\n * In try/catch:\n * ```typescript\n * try {\n *   const manifest = await readFile('./lexicons.manifest.json', 'utf8')\n * } catch (err) {\n *   if (isEnoentError(err)) {\n *     // File doesn't exist, create a new manifest\n *     return { version: 1, lexicons: [], resolutions: {} }\n *   }\n *   throw err\n * }\n * ```\n */\nexport function isEnoentError(err: unknown): boolean {\n  return err instanceof Error && 'code' in err && err.code === 'ENOENT'\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/src/index.ts",
    "content": "import { isEnoentError, readJsonFile } from './fs.js'\nimport { LexInstaller, LexInstallerOptions } from './lex-installer.js'\nimport {\n  LexiconsManifest,\n  lexiconsManifestSchema,\n} from './lexicons-manifest.js'\n\n/**\n * Options for the {@link install} function.\n *\n * Extends {@link LexInstallerOptions} with additional options for controlling\n * the installation behavior.\n *\n * @example\n * ```typescript\n * const options: LexInstallOptions = {\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n *   add: ['com.example.myLexicon', 'at://did:plc:xyz/com.example.otherLexicon'],\n *   save: true,\n *   ci: false,\n * }\n * ```\n */\nexport type LexInstallOptions = LexInstallerOptions & {\n  /**\n   * Array of lexicons to add to the installation. Can be NSID strings\n   * (e.g., 'com.example.myLexicon') or AT URIs\n   * (e.g., 'at://did:plc:xyz/com.example.myLexicon').\n   */\n  add?: string[]\n\n  /**\n   * Whether to save the updated manifest after installation.\n   * When `true`, the manifest file will be written with any new lexicons.\n   * @default false\n   */\n  save?: boolean\n\n  /**\n   * Enable CI mode for strict manifest verification.\n   * When `true`, throws an error if the manifest is out of date,\n   * useful for continuous integration pipelines.\n   * @default false\n   */\n  ci?: boolean\n}\n\n/**\n * Installs lexicons from the network based on the provided options.\n *\n * This is the main entry point for programmatic lexicon installation.\n * It reads an existing manifest (if present), installs any new lexicons,\n * and optionally saves the updated manifest.\n *\n * @param options - Configuration options for the installation\n * @throws {Error} When the manifest file cannot be read (unless it doesn't exist)\n * @throws {Error} When in CI mode and the manifest is out of date\n *\n * @example\n * Install lexicons and save the manifest:\n * ```typescript\n * import { install } from '@atproto/lex-installer'\n *\n * await install({\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n *   add: ['app.bsky.feed.post', 'app.bsky.actor.profile'],\n *   save: true,\n * })\n * ```\n *\n * @example\n * Verify manifest in CI pipeline:\n * ```typescript\n * import { install } from '@atproto/lex-installer'\n *\n * // Throws if manifest is out of date\n * await install({\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n *   ci: true,\n * })\n * ```\n *\n * @example\n * Install from specific AT URIs:\n * ```typescript\n * import { install } from '@atproto/lex-installer'\n *\n * await install({\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n *   add: ['at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post'],\n *   save: true,\n * })\n * ```\n */\nexport async function install(options: LexInstallOptions) {\n  const manifest: LexiconsManifest | undefined = await readJsonFile(\n    options.manifest,\n  ).then(\n    (json) => lexiconsManifestSchema.parse(json),\n    (cause: unknown) => {\n      if (isEnoentError(cause)) return undefined\n      throw new Error('Failed to read lexicons manifest', { cause })\n    },\n  )\n\n  const additions = new Set(options.add)\n\n  // Perform the installation using the existing manifest as \"hint\"\n  await using installer = new LexInstaller(options)\n\n  await installer.install({ additions, manifest })\n\n  // Verify lockfile\n  if (options.ci && (!manifest || !installer.equals(manifest))) {\n    throw new Error('Lexicons manifest is out of date')\n  }\n\n  // Save changes if requested\n  if (options.save) {\n    await installer.save()\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/src/lex-installer.ts",
    "content": "import { join } from 'node:path'\nimport { LexiconDirectoryIndexer } from '@atproto/lex-builder'\nimport { cidForLex } from '@atproto/lex-cbor'\nimport { Cid, lexEquals } from '@atproto/lex-data'\nimport {\n  LexiconDocument,\n  LexiconParameters,\n  LexiconPermission,\n  LexiconRef,\n  LexiconRefUnion,\n  LexiconUnknown,\n  MainLexiconDefinition,\n  NamedLexiconDefinition,\n} from '@atproto/lex-document'\nimport { LexResolver, LexResolverOptions } from '@atproto/lex-resolver'\nimport { AtUriString, NsidString } from '@atproto/lex-schema'\nimport { AtUri, NSID } from '@atproto/syntax'\nimport { isEnoentError, writeJsonFile } from './fs.js'\nimport {\n  LexiconsManifest,\n  normalizeLexiconsManifest,\n} from './lexicons-manifest.js'\nimport { NsidMap } from './nsid-map.js'\nimport { NsidSet } from './nsid-set.js'\n\n/**\n * Configuration options for the {@link LexInstaller} class.\n *\n * Extends {@link LexResolverOptions} with paths for lexicon storage\n * and manifest management.\n *\n * @example\n * ```typescript\n * const options: LexInstallerOptions = {\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n *   update: false,\n * }\n * ```\n */\nexport type LexInstallerOptions = LexResolverOptions & {\n  /**\n   * Path to the directory where lexicon JSON files will be stored.\n   * The directory structure mirrors the NSID hierarchy\n   * (e.g., 'app.bsky.feed.post' becomes 'app/bsky/feed/post.json').\n   */\n  lexicons: string\n\n  /**\n   * Path to the manifest file that tracks installed lexicons and their resolutions.\n   */\n  manifest: string\n\n  /**\n   * When `true`, forces re-fetching of lexicons from the network even if they\n   * already exist locally. Useful for updating to newer versions.\n   * @default false\n   */\n  update?: boolean\n}\n\n/**\n * Manages the installation of Lexicon schemas from the AT Protocol network.\n *\n * The `LexInstaller` class handles fetching, caching, and organizing lexicon\n * documents. It tracks dependencies between lexicons and ensures all referenced\n * schemas are installed. The class implements `AsyncDisposable` for proper\n * resource cleanup.\n *\n * @example\n * Basic usage with async disposal:\n * ```typescript\n * import { LexInstaller } from '@atproto/lex-installer'\n *\n * await using installer = new LexInstaller({\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n * })\n *\n * await installer.install({\n *   additions: ['app.bsky.feed.post'],\n * })\n *\n * await installer.save()\n * // Resources automatically cleaned up when block exits\n * ```\n *\n * @example\n * Manual disposal:\n * ```typescript\n * const installer = new LexInstaller({\n *   lexicons: './lexicons',\n *   manifest: './lexicons.manifest.json',\n * })\n *\n * try {\n *   await installer.install({ additions: ['app.bsky.actor.profile'] })\n *   await installer.save()\n * } finally {\n *   await installer[Symbol.asyncDispose]()\n * }\n * ```\n */\nexport class LexInstaller implements AsyncDisposable {\n  protected readonly lexiconResolver: LexResolver\n  protected readonly indexer: LexiconDirectoryIndexer\n  protected readonly documents = new NsidMap<LexiconDocument>()\n  protected readonly manifest: LexiconsManifest = {\n    version: 1,\n    lexicons: [],\n    resolutions: {},\n  }\n\n  constructor(protected readonly options: LexInstallerOptions) {\n    this.lexiconResolver = new LexResolver(options)\n    this.indexer = new LexiconDirectoryIndexer({\n      lexicons: options.lexicons,\n    })\n  }\n\n  async [Symbol.asyncDispose](): Promise<void> {\n    await this.indexer[Symbol.asyncDispose]()\n  }\n\n  /**\n   * Compares the current manifest state with another manifest for equality.\n   *\n   * Both manifests are normalized before comparison to ensure consistent\n   * ordering of entries. Useful for detecting changes during CI verification.\n   *\n   * @param manifest - The manifest to compare against\n   * @returns `true` if the manifests are equivalent, `false` otherwise\n   */\n  equals(manifest: LexiconsManifest): boolean {\n    return lexEquals(\n      normalizeLexiconsManifest(manifest),\n      normalizeLexiconsManifest(this.manifest),\n    )\n  }\n\n  /**\n   * Installs lexicons and their dependencies.\n   *\n   * This method processes explicit additions and restores entries from an\n   * existing manifest. It recursively resolves and installs all referenced\n   * lexicons to ensure complete dependency trees.\n   *\n   * @param options - Installation options\n   * @param options.additions - Iterable of lexicon identifiers to add.\n   *   Can be NSID strings or AT URIs.\n   * @param options.manifest - Existing manifest to use as a baseline.\n   *   Previously resolved URIs are preserved unless explicitly overridden.\n   *\n   * @example\n   * Install new lexicons:\n   * ```typescript\n   * await installer.install({\n   *   additions: ['app.bsky.feed.post', 'app.bsky.actor.profile'],\n   * })\n   * ```\n   *\n   * @example\n   * Install with existing manifest as hint:\n   * ```typescript\n   * const existingManifest = await readJsonFile('./lexicons.manifest.json')\n   * await installer.install({\n   *   additions: ['com.example.newLexicon'],\n   *   manifest: existingManifest,\n   * })\n   * ```\n   *\n   * @example\n   * Install from specific AT URIs:\n   * ```typescript\n   * await installer.install({\n   *   additions: [\n   *     'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post',\n   *   ],\n   * })\n   * ```\n   */\n  async install({\n    additions,\n    manifest,\n  }: {\n    additions?: Iterable<string>\n    manifest?: LexiconsManifest\n  } = {}): Promise<void> {\n    const roots = new NsidMap<AtUri | null>()\n\n    // First, process explicit additions\n    for (const lexicon of new Set(additions)) {\n      const [nsid, uri]: [NSID, AtUri | null] = lexicon.startsWith('at://')\n        ? ((uri) => [NSID.from(uri.rkey), uri])(new AtUri(lexicon))\n        : [NSID.from(lexicon), null]\n\n      if (roots.has(nsid)) {\n        throw new Error(\n          `Duplicate lexicon addition: ${nsid} (${roots.get(nsid) ?? lexicon})`,\n        )\n      }\n\n      roots.set(nsid, uri)\n      console.debug(`Adding new lexicon: ${nsid} (${uri ?? 'from NSID'})`)\n    }\n\n    // Next, restore previously existing manifest entries\n    if (manifest) {\n      for (const lexicon of manifest.lexicons) {\n        const nsid = NSID.from(lexicon)\n\n        // Skip entries already added explicitly\n        if (!roots.has(nsid)) {\n          const uri = manifest.resolutions[lexicon]\n            ? new AtUri(manifest.resolutions[lexicon].uri)\n            : null\n\n          roots.set(nsid, uri)\n\n          console.debug(\n            `Adding lexicon from manifest: ${nsid} (${uri ?? 'from NSID'})`,\n          )\n        }\n      }\n    }\n\n    // Install all root lexicons (and store them in the manifest)\n    await Promise.all(\n      Array.from(roots, async ([nsid, sourceUri]) => {\n        console.debug(`Installing lexicon: ${nsid}`)\n\n        const { lexicon: document } = sourceUri\n          ? await this.installFromUri(sourceUri)\n          : await this.installFromNsid(nsid)\n\n        // Store the direct reference in the new manifest\n        this.manifest.lexicons.push(document.id)\n      }),\n    )\n\n    // Then recursively install all referenced lexicons\n    let results: unknown[]\n    do {\n      results = await Promise.all(\n        Array.from(this.getMissingIds(), async (nsid) => {\n          console.debug(`Resolving dependency lexicon: ${nsid}`)\n\n          const nsidStr = nsid.toString() as NsidString\n          const resolvedUri = manifest?.resolutions[nsidStr]?.uri\n            ? new AtUri(manifest.resolutions[nsidStr].uri)\n            : null\n          if (resolvedUri) {\n            await this.installFromUri(resolvedUri)\n          } else {\n            await this.installFromNsid(nsid)\n          }\n        }),\n      )\n    } while (results.length > 0)\n  }\n\n  protected getMissingIds(): NsidSet {\n    const missing = new NsidSet()\n\n    for (const document of this.documents.values()) {\n      for (const nsid of listDocumentNsidRefs(document)) {\n        if (!this.documents.has(nsid)) {\n          missing.add(nsid)\n        }\n      }\n    }\n\n    return missing\n  }\n\n  protected async installFromNsid(nsid: NSID) {\n    const uri = await this.lexiconResolver.resolve(nsid)\n    return this.installFromUri(uri)\n  }\n\n  protected async installFromUri(uri: AtUri): Promise<{\n    lexicon: LexiconDocument\n    uri: AtUri\n  }> {\n    const { lexicon, cid } = this.options.update\n      ? await this.fetch(uri)\n      : await this.indexer.get(uri.rkey).then(\n          async (lexicon) => {\n            console.debug(`Re-using existing lexicon ${uri.rkey} from indexer`)\n            const cid = await cidForLex(lexicon)\n            return { cid, lexicon }\n          },\n          (err) => {\n            if (isEnoentError(err)) return this.fetch(uri)\n            throw err\n          },\n        )\n\n    this.documents.set(NSID.from(lexicon.id), lexicon)\n    this.manifest.resolutions[lexicon.id] = {\n      cid: cid.toString(),\n      uri: uri.toString() as AtUriString,\n    }\n\n    return { lexicon, uri }\n  }\n\n  /**\n   * Fetches a lexicon document from the network and saves it locally.\n   *\n   * The lexicon is retrieved from the specified AT URI, written to the\n   * local lexicons directory, and its metadata is recorded for the manifest.\n   *\n   * @param uri - The AT URI pointing to the lexicon document\n   * @returns An object containing the fetched lexicon document and its CID\n   */\n  async fetch(uri: AtUri): Promise<{ lexicon: LexiconDocument; cid: Cid }> {\n    console.debug(`Fetching lexicon from ${uri}...`)\n\n    const { lexicon, cid } = await this.lexiconResolver.fetch(uri, {\n      noCache: this.options.update,\n    })\n\n    const basePath = join(this.options.lexicons, ...lexicon.id.split('.'))\n    await writeJsonFile(`${basePath}.json`, lexicon)\n\n    return { lexicon, cid }\n  }\n\n  /**\n   * Saves the current manifest to disk.\n   *\n   * The manifest is normalized before saving to ensure consistent ordering\n   * of entries, making it suitable for version control.\n   */\n  async save(): Promise<void> {\n    await writeJsonFile(\n      this.options.manifest,\n      normalizeLexiconsManifest(this.manifest),\n    )\n  }\n}\n\nfunction* listDocumentNsidRefs(doc: LexiconDocument): Iterable<NSID> {\n  try {\n    for (const def of Object.values(doc.defs)) {\n      if (def) {\n        for (const ref of defRefs(def)) {\n          const [nsid] = ref.split('#', 1)\n          if (nsid) yield NSID.from(nsid)\n        }\n      }\n    }\n  } catch (cause) {\n    throw new Error(`Failed to extract refs from lexicon ${doc.id}`, { cause })\n  }\n}\n\nfunction* defRefs(\n  def:\n    | MainLexiconDefinition\n    | NamedLexiconDefinition\n    | LexiconPermission\n    | LexiconUnknown\n    | LexiconParameters\n    | LexiconRef\n    | LexiconRefUnion,\n): Iterable<string> {\n  switch (def.type) {\n    case 'string':\n      if (def.knownValues) {\n        for (const val of def.knownValues) {\n          // Tokens ?\n          const { length, 0: nsid, 1: hash } = val.split('#')\n          if (length === 2 && hash) {\n            try {\n              NSID.from(nsid)\n              yield val\n            } catch {\n              // ignore invalid nsid\n            }\n          }\n        }\n      }\n      return\n    case 'array':\n      return yield* defRefs(def.items)\n    case 'params':\n    case 'object':\n      for (const prop of Object.values(def.properties)) {\n        yield* defRefs(prop)\n      }\n      return\n    case 'union':\n      yield* def.refs\n      return\n    case 'ref': {\n      yield def.ref\n      return\n    }\n    case 'record':\n      yield* defRefs(def.record)\n      return\n    case 'procedure':\n      if (def.input?.schema) {\n        yield* defRefs(def.input.schema)\n      }\n    // fallthrough\n    case 'query':\n      if (def.output?.schema) {\n        yield* defRefs(def.output.schema)\n      }\n    // fallthrough\n    case 'subscription':\n      if (def.parameters) {\n        yield* defRefs(def.parameters)\n      }\n      if ('message' in def && def.message?.schema) {\n        yield* defRefs(def.message.schema)\n      }\n      return\n    case 'permission-set':\n      for (const permission of def.permissions) {\n        yield* defRefs(permission)\n      }\n      return\n    case 'permission':\n      if (def.resource === 'rpc') {\n        if (Array.isArray(def.lxm)) {\n          for (const lxm of def.lxm) {\n            if (typeof lxm === 'string') {\n              yield lxm\n            }\n          }\n        }\n      } else if (def.resource === 'repo') {\n        if (Array.isArray(def.collection)) {\n          for (const lxm of def.collection) {\n            if (typeof lxm === 'string') {\n              yield lxm\n            }\n          }\n        }\n      }\n      return\n    case 'boolean':\n    case 'cid-link':\n    case 'token':\n    case 'bytes':\n    case 'blob':\n    case 'integer':\n    case 'unknown':\n      // @NOTE We explicitly list all types here to ensure exhaustiveness\n      // causing TS to error if a new type is added without updating this switch\n      return\n    default:\n      // @ts-expect-error\n      throw new Error(`Unknown lexicon def type: ${def.type}`)\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/src/lexicons-manifest.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { lexiconsManifestSchema } from './lexicons-manifest.js'\n\ndescribe('lexiconsManifestSchema', () => {\n  it('parses a valid manifest', () => {\n    expect(\n      lexiconsManifestSchema.parse({\n        version: 1,\n        lexicons: ['com.example.lexicon'],\n        resolutions: {\n          'com.example.lexicon': {\n            uri: 'at://did:plc:foobar/com.atproto.lexicon.schema/com.example.lexicon',\n            cid: 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku',\n          },\n        },\n      }),\n    ).toEqual({\n      version: 1,\n      lexicons: ['com.example.lexicon'],\n      resolutions: {\n        'com.example.lexicon': {\n          uri: 'at://did:plc:foobar/com.atproto.lexicon.schema/com.example.lexicon',\n          cid: 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku',\n        },\n      },\n    })\n  })\n\n  it('rejects an invalid manifest', () => {\n    expect(() =>\n      lexiconsManifestSchema.parse({\n        version: 1,\n        lexicons: ['com.example.lexicon'],\n        resolutions: {\n          'com.example.lexicon': {\n            uri: 'invalid-uri',\n            cid: 'not-a-cid',\n          },\n        },\n      }),\n    ).toThrow()\n\n    expect(() =>\n      lexiconsManifestSchema.parse({\n        version: 2,\n        lexicons: ['com.example.lexicon'],\n        resolutions: {},\n      }),\n    ).toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-installer/src/lexicons-manifest.ts",
    "content": "import { l } from '@atproto/lex-schema'\n\n/**\n * Schema for validating and parsing lexicons manifest files.\n *\n * The manifest tracks which lexicons are installed and how they were resolved.\n * This schema ensures the manifest file conforms to the expected structure.\n */\nexport const lexiconsManifestSchema = l.object({\n  /** Schema version, currently always 1 */\n  version: l.literal(1),\n  /** Array of NSID strings for directly requested lexicons */\n  lexicons: l.array(l.string({ format: 'nsid' })),\n  /** Map of NSID to resolution info (AT URI and CID) for all installed lexicons */\n  resolutions: l.dict(\n    l.string({ format: 'nsid' }),\n    l.object({\n      /** AT URI where the lexicon was fetched from */\n      uri: l.string({ format: 'at-uri' }),\n      /** Content identifier (CID) of the lexicon document */\n      cid: l.string({ format: 'cid' }),\n    }),\n  ),\n})\n\n/**\n * Type representing a parsed lexicons manifest.\n */\nexport type LexiconsManifest = l.Infer<typeof lexiconsManifestSchema>\n\n/**\n * Normalizes a lexicons manifest for consistent storage and comparison.\n *\n * This function:\n * - Sorts the `lexicons` array alphabetically\n * - Sorts the `resolutions` object entries by key\n * - Validates the result against the schema\n *\n * Normalization ensures that manifests with the same content produce identical\n * JSON output, making them suitable for version control and comparison.\n *\n * @param manifest - The manifest to normalize\n * @returns A new normalized manifest object\n */\nexport function normalizeLexiconsManifest(\n  manifest: LexiconsManifest,\n): LexiconsManifest {\n  const normalized: LexiconsManifest = {\n    version: manifest.version,\n    lexicons: [...manifest.lexicons].sort(),\n    resolutions: Object.fromEntries(\n      Object.entries(manifest.resolutions)\n        .sort(compareObjectEntriesFn)\n        .map(([k, { uri, cid }]) => [k, { uri, cid }]),\n    ),\n  }\n  // For good measure:\n  return lexiconsManifestSchema.parse(normalized)\n}\n\nfunction compareObjectEntriesFn(\n  a: [string, unknown],\n  b: [string, unknown],\n): number {\n  return a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/src/nsid-map.ts",
    "content": "import { NSID } from '@atproto/syntax'\n\n/**\n * A Map implementation that maps keys of type K to an internal representation I.\n *\n * Key identity is determined by the {@link Object.is} comparison of the\n * encoded keys. This is useful for complex key types that can be serialized\n * to a unique primitive representation (typically strings).\n *\n * @typeParam K - The external key type\n * @typeParam V - The value type stored in the map\n * @typeParam I - The internal encoded key type used for identity comparison\n */\nclass MappedMap<K, V, I = any> implements Map<K, V> {\n  private map = new Map<I, V>()\n\n  /**\n   * Creates a new MappedMap with custom key encoding/decoding functions.\n   *\n   * @param encodeKey - Function to convert external keys to internal representation\n   * @param decodeKey - Function to convert internal representation back to external keys\n   */\n  constructor(\n    private readonly encodeKey: (key: K) => I,\n    private readonly decodeKey: (enc: I) => K,\n  ) {}\n\n  get size(): number {\n    return this.map.size\n  }\n\n  clear(): void {\n    this.map.clear()\n  }\n\n  set(key: K, value: V): this {\n    this.map.set(this.encodeKey(key), value)\n    return this\n  }\n\n  get(key: K): V | undefined {\n    return this.map.get(this.encodeKey(key))\n  }\n\n  has(key: K): boolean {\n    return this.map.has(this.encodeKey(key))\n  }\n\n  delete(key: K): boolean {\n    return this.map.delete(this.encodeKey(key))\n  }\n\n  values(): IterableIterator<V> {\n    return this.map.values()\n  }\n\n  *keys(): IterableIterator<K> {\n    for (const key of this.map.keys()) {\n      yield this.decodeKey(key)\n    }\n  }\n\n  *entries(): IterableIterator<[K, V]> {\n    for (const [key, value] of this.map.entries()) {\n      yield [this.decodeKey(key), value]\n    }\n  }\n\n  forEach(\n    callbackfn: (value: V, key: K, map: MappedMap<K, V>) => void,\n    thisArg?: any,\n  ): void {\n    for (const [key, value] of this) {\n      callbackfn.call(thisArg, value, key, this)\n    }\n  }\n\n  [Symbol.iterator](): IterableIterator<[K, V]> {\n    return this.entries()\n  }\n\n  [Symbol.toStringTag]: string = 'MappedMap'\n}\n\n/**\n * A Map specialized for using NSID (Namespaced Identifier) values as keys.\n *\n * NSIDs are compared by their string representation, allowing different\n * NSID object instances with the same value to be treated as the same key.\n *\n * @typeParam T - The value type stored in the map\n *\n * @example\n * ```typescript\n * import { NsidMap } from '@atproto/lex-installer'\n * import { NSID } from '@atproto/syntax'\n * import { LexiconDocument } from '@atproto/lex-document'\n *\n * const lexicons = new NsidMap<LexiconDocument>()\n *\n * // Store lexicon documents by NSID\n * lexicons.set(NSID.from('app.bsky.feed.post'), postLexicon)\n * lexicons.set(NSID.from('app.bsky.actor.profile'), profileLexicon)\n *\n * // Retrieve by NSID (different object instance works)\n * const doc = lexicons.get(NSID.from('app.bsky.feed.post'))\n *\n * // Iterate over entries\n * for (const [nsid, lexicon] of lexicons) {\n *   console.log(`${nsid}: ${lexicon.description}`)\n * }\n * ```\n */\nexport class NsidMap<T> extends MappedMap<NSID, T, string> {\n  /**\n   * Creates a new empty NsidMap.\n   */\n  constructor() {\n    super(\n      (key) => key.toString(),\n      (enc) => NSID.from(enc),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/src/nsid-set.ts",
    "content": "import { NSID } from '@atproto/syntax'\n\n/**\n * A Set implementation that maps values of type K to an internal representation I.\n *\n * Value identity is determined by the {@link Object.is} comparison of the\n * encoded values. This is useful for complex types that can be serialized\n * to a unique primitive representation (typically strings).\n *\n * @typeParam K - The external value type stored in the set\n * @typeParam I - The internal encoded type used for identity comparison\n */\nexport class MappedSet<K, I = any> implements Set<K> {\n  private set = new Set<I>()\n\n  /**\n   * Creates a new MappedSet with custom encoding/decoding functions.\n   *\n   * @param encodeValue - Function to convert external values to internal representation\n   * @param decodeValue - Function to convert internal representation back to external values\n   */\n  constructor(\n    private readonly encodeValue: (val: K) => I,\n    private readonly decodeValue: (enc: I) => K,\n  ) {}\n\n  get size(): number {\n    return this.set.size\n  }\n\n  clear(): void {\n    this.set.clear()\n  }\n\n  add(val: K): this {\n    this.set.add(this.encodeValue(val))\n    return this\n  }\n\n  has(val: K): boolean {\n    return this.set.has(this.encodeValue(val))\n  }\n\n  delete(val: K): boolean {\n    return this.set.delete(this.encodeValue(val))\n  }\n\n  *values(): IterableIterator<K> {\n    for (const val of this.set.values()) {\n      yield this.decodeValue(val)\n    }\n  }\n\n  keys(): SetIterator<K> {\n    return this.values()\n  }\n\n  *entries(): IterableIterator<[K, K]> {\n    for (const val of this) yield [val, val]\n  }\n\n  forEach(\n    callbackfn: (value: K, value2: K, set: Set<K>) => void,\n    thisArg?: any,\n  ): void {\n    for (const val of this) {\n      callbackfn.call(thisArg, val, val, this)\n    }\n  }\n\n  [Symbol.iterator](): IterableIterator<K> {\n    return this.values()\n  }\n\n  [Symbol.toStringTag]: string = 'MappedSet'\n}\n\n/**\n * A Set specialized for storing NSID (Namespaced Identifier) values.\n *\n * NSIDs are compared by their string representation, allowing different\n * NSID object instances with the same value to be treated as equal.\n *\n * @example\n * ```typescript\n * import { NsidSet } from '@atproto/lex-installer'\n * import { NSID } from '@atproto/syntax'\n *\n * const nsidSet = new NsidSet()\n *\n * nsidSet.add(NSID.from('app.bsky.feed.post'))\n * nsidSet.add(NSID.from('app.bsky.actor.profile'))\n *\n * // Check membership\n * nsidSet.has(NSID.from('app.bsky.feed.post')) // true\n *\n * // Iterate over NSIDs\n * for (const nsid of nsidSet) {\n *   console.log(nsid.toString())\n * }\n * ```\n */\nexport class NsidSet extends MappedSet<NSID, string> {\n  /**\n   * Creates a new empty NsidSet.\n   */\n  constructor() {\n    super(\n      (val) => val.toString(),\n      (enc) => NSID.from(enc),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-installer/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-json/CHANGELOG.md",
    "content": "# @atproto/lex-json\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-data@0.0.14\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-data@0.0.13\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n\n## 0.0.11\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent `jsonToLex` from throwing in non-strict mode\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-data@0.0.11\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4532](https://github.com/bluesky-social/atproto/pull/4532) [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor change to validation of integers in lex data\n\n- Updated dependencies [[`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/lex-data@0.0.9\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Forbid use of unsafe integers\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lex-data@0.0.3\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4)]:\n  - @atproto/lex-data@0.0.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-data@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-json/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-json\",\n  \"version\": \"0.0.14\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon encoding utilities for AT Lexicon data in JSON format\",\n  \"keywords\": [\n    \"atproto\",\n    \"lex\",\n    \"data\",\n    \"json\",\n    \"encoding\",\n    \"utilities\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-json\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-json/src/blob.ts",
    "content": "import {\n  BlobRef,\n  BlobRefCheckOptions,\n  LexMap,\n  isBlobRef,\n} from '@atproto/lex-data'\nimport { parseLexLink } from './link.js'\n\n/**\n * Parses a blob reference from a JSON object.\n *\n * In the AT Protocol, blobs are referenced using a specific object structure\n * with `$type: 'blob'`, a `ref` property containing a CID link, and metadata\n * like `mimeType` and `size`. This function validates and parses such objects\n * into `BlobRef` instances.\n *\n * The function handles both cases where the `ref` property is:\n * - A `{$link: string}` object (when parsing from JSON)\n * - Already a `Cid` instance (when the parent object has been partially converted)\n *\n * @param input - A Lex map potentially representing a blob reference\n * @param options - Optional blob reference validation options\n * @returns The parsed `BlobRef` if the input is a valid blob reference,\n *          or `undefined` if the input is not a valid blob representation\n *\n * @example\n * ```typescript\n * // Parse a blob reference from JSON\n * const blobRef = parseBlobRef({\n *   $type: 'blob',\n *   ref: { $link: 'bafyreib2rxk3rybloqtqwbo' },\n *   mimeType: 'image/png',\n *   size: 12345\n * })\n *\n * // blobRef.ref is a Cid instance\n *\n * // Returns undefined for non-blob objects\n * const result = parseBlobRef({ foo: 'bar' })\n * // result is undefined\n * ```\n */\nexport function parseBlobRef(\n  input: LexMap,\n  options?: BlobRefCheckOptions,\n): BlobRef | undefined {\n  if (input.$type !== 'blob') return undefined\n\n  const ref = input?.ref\n  if (!ref || typeof ref !== 'object') return undefined\n\n  // @NOTE Because json to lex conversion can be performed both in a depth-first\n  // manner (e.g. via lexParse) or in a breadth-first manner (e.g. via\n  // jsonToLex), the `ref` property may either be a LexMap with a $link\n  // property, or it may already be a CID instance.\n\n  if ('$link' in ref) {\n    const cid = parseLexLink(ref)\n    if (!cid) return undefined\n\n    const blob = { ...input, ref: cid }\n    if (isBlobRef(blob, options)) return blob\n  }\n\n  if (isBlobRef(input)) {\n    return input\n  }\n\n  return undefined\n}\n"
  },
  {
    "path": "packages/lex/lex-json/src/bytes.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { encodeLexBytes, parseLexBytes } from './bytes.js'\n\ndescribe(parseLexBytes, () => {\n  it('parses valid $bytes object', () => {\n    const bytes = Buffer.from('Hello, world!')\n    const input = { $bytes: bytes.toString('base64') }\n    const result = parseLexBytes(input)\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(new TextDecoder().decode(result!)).toBe('Hello, world!')\n  })\n\n  it('parses valid $bytes object (without padding)', () => {\n    const bytes = Buffer.from('Hello, world!')\n    const input = { $bytes: bytes.toString('base64').replace(/=*$/, '') }\n    const result = parseLexBytes(input)\n    expect(result).toBeInstanceOf(Uint8Array)\n    expect(new TextDecoder().decode(result!)).toBe('Hello, world!')\n  })\n\n  it('returns undefined for non-$bytes object', () => {\n    const input = { foo: 'bar' }\n    const result = parseLexBytes(input)\n    expect(result).toBeUndefined()\n  })\n\n  it('returns undefined for $bytes with non-string value', () => {\n    const input = { $bytes: 12345 }\n    const result = parseLexBytes(input)\n    expect(result).toBeUndefined()\n  })\n\n  it('returns undefined for $bytes with extra properties', () => {\n    const bytes = Buffer.from('Hello, world!')\n    const input = { $bytes: bytes.toString('base64'), extra: true }\n    const result = parseLexBytes(input)\n    expect(result).toBeUndefined()\n  })\n\n  it('returns undefined for invalid base64 string', () => {\n    const input = { $bytes: '!!!invalid-base64!!!' }\n    const result = parseLexBytes(input)\n    expect(result).toBeUndefined()\n  })\n})\n\ndescribe(encodeLexBytes, () => {\n  it('encodes Uint8Array to $bytes object', () => {\n    const bytes = Buffer.from('Hello, world!')\n    const result = encodeLexBytes(bytes)\n    expect(result).toEqual({\n      $bytes: bytes.toString('base64').replace(/=*$/, ''),\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-json/src/bytes.ts",
    "content": "import { fromBase64, toBase64 } from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\n\n/**\n * Parses a `{$bytes: string}` JSON object into a `Uint8Array`.\n *\n * In the AT Protocol data model, binary data is represented in JSON as an object\n * with a single `$bytes` property containing a base64-encoded string. This function\n * decodes that representation back into raw bytes.\n *\n * @param input - An object potentially containing a `$bytes` property\n * @returns The decoded `Uint8Array` if the input is a valid `$bytes` object,\n *          or `undefined` if the input is not a valid `$bytes` representation\n *\n * @example\n * ```typescript\n * // Parse a $bytes object to Uint8Array\n * const bytes = parseLexBytes({ $bytes: 'SGVsbG8sIHdvcmxkIQ==' })\n * // bytes is Uint8Array containing \"Hello, world!\"\n *\n * // Returns undefined for non-$bytes objects\n * const result = parseLexBytes({ foo: 'bar' })\n * // result is undefined\n *\n * // Returns undefined for objects with extra properties\n * const invalid = parseLexBytes({ $bytes: 'SGVsbG8=', extra: true })\n * // invalid is undefined\n * ```\n */\nexport function parseLexBytes(\n  input?: Record<string, unknown>,\n): Uint8Array | undefined {\n  if (!input || !('$bytes' in input)) {\n    return undefined\n  }\n\n  for (const key in input) {\n    if (key !== '$bytes') {\n      return undefined\n    }\n  }\n\n  if (typeof input.$bytes !== 'string') {\n    return undefined\n  }\n\n  try {\n    return fromBase64(input.$bytes)\n  } catch {\n    return undefined\n  }\n}\n\n/**\n * Encodes a `Uint8Array` into a `{$bytes: string}` JSON representation.\n *\n * In the AT Protocol data model, binary data is represented in JSON as an object\n * with a single `$bytes` property containing a base64-encoded string. This function\n * performs that encoding.\n *\n * @param bytes - The binary data to encode\n * @returns An object with a `$bytes` property containing the base64-encoded data\n *\n * @example\n * ```typescript\n * const bytes = new TextEncoder().encode('Hello, world!')\n * const encoded = encodeLexBytes(bytes)\n * // encoded is { $bytes: 'SGVsbG8sIHdvcmxkIQ==' }\n * ```\n */\nexport function encodeLexBytes(bytes: Uint8Array): JsonValue {\n  return { $bytes: toBase64(bytes) }\n}\n"
  },
  {
    "path": "packages/lex/lex-json/src/index.ts",
    "content": "export * from './bytes.js'\nexport * from './json.js'\nexport * from './lex-json.js'\nexport * from './link.js'\n"
  },
  {
    "path": "packages/lex/lex-json/src/json.ts",
    "content": "/**\n * Primitive JSON values: string, number, boolean, or null.\n *\n * These are the scalar (non-composite) types that can appear in JSON data.\n * In the context of the AT Protocol:\n * - `string` - Text values, including special encoded types like `$link` and `$bytes`\n * - `number` - Numeric values (note: Lex only supports safe integers)\n * - `boolean` - True/false values\n * - `null` - Explicit null values\n *\n * @see {@link JsonValue} for the complete JSON type including arrays and objects\n */\nexport type JsonScalar = number | string | boolean | null\n\n/**\n * Any valid JSON value.\n *\n * This is a recursive type that represents the full JSON data model:\n * - Scalars: string, number, boolean, null\n * - Arrays: ordered lists of JSON values\n * - Objects: string-keyed maps of JSON values\n *\n * @example\n * ```typescript\n * const scalar: JsonValue = \"hello\"\n * const array: JsonValue = [1, 2, 3]\n * const object: JsonValue = { name: \"Alice\", age: 30 }\n * const nested: JsonValue = { users: [{ name: \"Bob\" }] }\n * ```\n */\nexport type JsonValue = JsonScalar | JsonValue[] | { [_ in string]?: JsonValue }\n\n/**\n * A JSON object with string keys and JSON values.\n *\n * This type represents a plain JavaScript object that is valid JSON,\n * where all keys are strings and all values are valid JSON values.\n *\n * @example\n * ```typescript\n * const obj: JsonObject = {\n *   name: \"Alice\",\n *   tags: [\"admin\", \"user\"],\n *   metadata: { created: \"2024-01-01\" }\n * }\n * ```\n */\nexport type JsonObject = { [_ in string]?: JsonValue }\n"
  },
  {
    "path": "packages/lex/lex-json/src/lex-json.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { LexValue, lexEquals, parseCid } from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\nimport { jsonToLex, lexParse, lexStringify, lexToJson } from './lex-json.js'\n\nexport const validVectors: Array<{\n  name: string\n  json: JsonValue\n  lex: LexValue\n}> = [\n  {\n    name: 'pure json',\n    json: {\n      string: 'abc',\n      unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',\n      integer: 123,\n      bool: true,\n      null: null,\n      array: ['abc', 'def', 'ghi'],\n      object: {\n        string: 'abc',\n        number: 123,\n        bool: true,\n        arr: ['abc', 'def', 'ghi'],\n      },\n    },\n    lex: {\n      string: 'abc',\n      unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',\n      integer: 123,\n      bool: true,\n      null: null,\n      array: ['abc', 'def', 'ghi'],\n      object: {\n        string: 'abc',\n        number: 123,\n        bool: true,\n        arr: ['abc', 'def', 'ghi'],\n      },\n    },\n  },\n  {\n    name: 'lex data',\n    json: {\n      a: {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      b: {\n        $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      },\n      c: {\n        $type: 'blob',\n        ref: {\n          $link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n        },\n        mimeType: 'image/jpeg',\n        size: 10000,\n      },\n    },\n    lex: {\n      a: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n      b: new Uint8Array([\n        156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,\n        65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,\n        204, 141,\n      ]),\n      c: {\n        $type: 'blob',\n        ref: parseCid(\n          'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n        ),\n        mimeType: 'image/jpeg',\n        size: 10000,\n      },\n    },\n  },\n  {\n    name: 'lexArray',\n    json: [\n      {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      {\n        $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',\n      },\n      {\n        $link: 'bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke',\n      },\n      {\n        $link: 'bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy',\n      },\n    ],\n    lex: [\n      parseCid('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),\n      parseCid('bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q'),\n      parseCid('bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke'),\n      parseCid('bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy'),\n    ],\n  },\n  {\n    name: 'root cid',\n    json: {\n      $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    },\n    lex: parseCid(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    ),\n  },\n  {\n    name: 'root bytes',\n    json: {\n      $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n    },\n    lex: new Uint8Array([\n      156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65,\n      60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204,\n      141,\n    ]),\n  },\n  {\n    name: 'lexNested',\n    json: {\n      a: {\n        b: [\n          {\n            d: [\n              {\n                $link:\n                  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              },\n              {\n                $link:\n                  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              },\n            ],\n            e: [\n              {\n                $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n              },\n              {\n                $bytes: 'iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas',\n              },\n            ],\n          },\n        ],\n      },\n    },\n    lex: {\n      a: {\n        b: [\n          {\n            d: [\n              parseCid(\n                'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              ),\n              parseCid(\n                'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n              ),\n            ],\n            e: [\n              new Uint8Array([\n                156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174,\n                161, 253, 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238,\n                190, 176, 26, 194, 204, 141,\n              ]),\n              new Uint8Array([\n                136, 79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237,\n                244, 244, 178, 194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14,\n                221, 125, 181, 171,\n              ]),\n            ],\n          },\n        ],\n      },\n    },\n  },\n  {\n    name: 'empty structures',\n    json: {\n      emptyObject: {},\n      emptyArray: [],\n      emtyBytes: { $bytes: '' },\n    },\n    lex: {\n      emptyObject: {},\n      emptyArray: [],\n      emtyBytes: new Uint8Array([]),\n    },\n  },\n  {\n    name: 'mixed types in array',\n    json: {\n      arr: [\n        'string',\n        123,\n        true,\n        null,\n        {\n          $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        },\n        { $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0' },\n        { nested: 'object' },\n        ['nested', 'array'],\n      ],\n    },\n    lex: {\n      arr: [\n        'string',\n        123,\n        true,\n        null,\n        parseCid('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),\n        new Uint8Array([\n          156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,\n          65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,\n          204, 141,\n        ]),\n        { nested: 'object' },\n        ['nested', 'array'],\n      ],\n    },\n  },\n  {\n    name: \"mismatched order in object doesn't affect equality\",\n    json: {\n      a: 'valueA',\n      b: 'valueB',\n      c: {\n        $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      },\n      d: {\n        a: 'valueA',\n        b: 'valueB',\n      },\n    },\n    lex: {\n      c: parseCid(\n        'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      ),\n      d: {\n        b: 'valueB',\n        a: 'valueA',\n      },\n      a: 'valueA',\n      b: 'valueB',\n    },\n  },\n]\n\nexport const acceptableVectors: Array<{\n  note: string\n  json: JsonValue\n}> = [\n  {\n    note: 'non string $type',\n    json: {\n      $type: 3124,\n      foo: 'bar',\n    },\n  },\n  {\n    note: 'object with float values',\n    json: {\n      a: 1.5,\n    },\n  },\n  {\n    note: 'blob with wrong field type',\n    json: {\n      $type: 'blob',\n      ref: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n      mimeType: 'image/jpeg',\n      size: 10000,\n    },\n  },\n  {\n    note: 'blob with missing key',\n    json: {\n      $type: 'blob',\n      mimeType: 'image/jpeg',\n      size: 10000,\n    },\n  },\n  {\n    note: 'blob with extra fields',\n    json: {\n      $type: 'blob',\n      ref: {\n        $link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n      },\n      mimeType: 'image/jpeg',\n      size: 10000,\n      other: 'blah',\n    },\n  },\n  {\n    note: 'bytes with extra fields',\n    json: {\n      $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      other: 'blah',\n    },\n  },\n  {\n    note: 'link with extra fields',\n    json: {\n      $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n      other: 'blah',\n    },\n  },\n  {\n    note: '$bytes and $link',\n    json: {\n      $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n      $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n    },\n  },\n  {\n    note: '$bytes and $type',\n    json: {\n      $type: 'bytes',\n      $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',\n    },\n  },\n  {\n    note: '$link and $type',\n    json: {\n      $type: 'blob',\n      $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',\n    },\n  },\n]\n\nexport const invalidVectors: Array<{\n  note: string\n  json: JsonValue\n}> = [\n  {\n    note: 'bytes with wrong field type',\n    json: {\n      $bytes: [1, 2, 3],\n    },\n  },\n  {\n    note: 'invalid base64 in $bytes',\n    json: {\n      $bytes: '🐻',\n    },\n  },\n  {\n    note: 'link with wrong field type',\n    json: {\n      $link: 1234,\n    },\n  },\n  {\n    note: 'link with bogus CID',\n    json: {\n      $link: '.',\n    },\n  },\n]\n\ndescribe(lexParse, () => {\n  describe('valid vectors', () => {\n    for (const { name, json, lex } of validVectors) {\n      describe(name, () => {\n        it('parses lex from string', () => {\n          expect(\n            lexEquals(lex, lexParse(JSON.stringify(json), { strict: false })),\n          ).toBe(true)\n          expect(\n            lexEquals(lex, lexParse(JSON.stringify(json), { strict: true })),\n          ).toBe(true)\n        })\n      })\n    }\n  })\n\n  describe('acceptable vectors', () => {\n    for (const { note, json } of acceptableVectors) {\n      describe(note, () => {\n        it('parses lex from string in non-strict mode', () => {\n          expect(() =>\n            lexParse(JSON.stringify(json), { strict: false }),\n          ).not.toThrow()\n        })\n\n        it('parses lex from string in strict mode', () => {\n          expect(() =>\n            lexParse(JSON.stringify(json), { strict: true }),\n          ).toThrow()\n        })\n      })\n    }\n  })\n\n  describe('invalid vectors', () => {\n    describe('strict mode', () => {\n      for (const { note, json } of invalidVectors) {\n        describe(note, () => {\n          it('throws when parsing malformed JSON', () => {\n            expect(() =>\n              lexParse(JSON.stringify(json), { strict: true }),\n            ).toThrow()\n          })\n        })\n      }\n    })\n    describe('non-strict mode', () => {\n      for (const { note, json } of invalidVectors) {\n        describe(note, () => {\n          it('does not throws when parsing malformed JSON', () => {\n            expect(() =>\n              lexParse(JSON.stringify(json), { strict: false }),\n            ).not.toThrow()\n          })\n        })\n      }\n    })\n  })\n})\n\ndescribe(lexStringify, () => {\n  describe('valid vectors', () => {\n    for (const { name, json, lex } of validVectors) {\n      describe(name, () => {\n        it('stringifies lex to string', () => {\n          expect(JSON.parse(lexStringify(lex))).toEqual(json)\n        })\n      })\n    }\n  })\n})\n\ndescribe(jsonToLex, () => {\n  describe('valid vectors', () => {\n    for (const { name, json, lex } of validVectors) {\n      describe(name, () => {\n        it('converts json to lex (in strict mode)', () => {\n          expect(lexEquals(jsonToLex(json, { strict: true }), lex)).toBe(true)\n          expect(lexEquals(lex, jsonToLex(json, { strict: true }))).toBe(true)\n        })\n\n        it('converts json to lex (in non-strict mode)', () => {\n          expect(lexEquals(jsonToLex(json, { strict: false }), lex)).toBe(true)\n          expect(lexEquals(lex, jsonToLex(json, { strict: false }))).toBe(true)\n        })\n      })\n    }\n  })\n\n  describe('acceptable vectors', () => {\n    for (const { note, json } of acceptableVectors) {\n      describe(note, () => {\n        it('parses lex from json in strict mode', () => {\n          expect(() => jsonToLex(json, { strict: true })).toThrow()\n        })\n\n        it('parses lex from json in non-strict mode', () => {\n          expect(() => jsonToLex(json, { strict: false })).not.toThrow()\n        })\n      })\n    }\n  })\n\n  describe('invalid vectors', () => {\n    for (const { note, json } of invalidVectors) {\n      describe(note, () => {\n        it(`throws for malformed object`, () => {\n          expect(() => jsonToLex(json, { strict: true })).toThrow()\n        })\n\n        it('throws for nested malformed object', () => {\n          expect(() => jsonToLex({ nested: json }, { strict: true })).toThrow()\n          expect(() => jsonToLex([json], { strict: true })).toThrow()\n        })\n\n        it('does not throw in non-strict mode', () => {\n          expect(() => jsonToLex(json, { strict: false })).not.toThrow()\n        })\n      })\n    }\n  })\n})\n\ndescribe(lexToJson, () => {\n  describe('valid vectors', () => {\n    for (const { name, json, lex } of validVectors) {\n      describe(name, () => {\n        it('converts lex to json', () => {\n          expect(lexToJson(lex)).toEqual(json)\n          expect(lexToJson(lex)).toEqual(json)\n        })\n      })\n    }\n  })\n})\n\ndescribe('json > lex > json', () => {\n  describe('valid vectors', () => {\n    for (const { name, json } of validVectors) {\n      describe(name, () => {\n        it('converts json to lex', () => {\n          expect(lexToJson(jsonToLex(json))).toEqual(json)\n        })\n      })\n    }\n  })\n})\n\ndescribe('lex > json > lex', () => {\n  describe('valid vectors', () => {\n    for (const { name, lex } of validVectors) {\n      describe(name, () => {\n        it('converts lex to json', () => {\n          ;('')\n          expect(lexEquals(jsonToLex(lexToJson(lex)), lex)).toBe(true)\n          expect(lexEquals(lex, jsonToLex(lexToJson(lex)))).toBe(true)\n        })\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-json/src/lex-json.ts",
    "content": "import {\n  BlobRef,\n  Cid,\n  LexArray,\n  LexMap,\n  LexValue,\n  isCid,\n} from '@atproto/lex-data'\nimport { parseBlobRef } from './blob.js'\nimport { encodeLexBytes, parseLexBytes } from './bytes.js'\nimport { JsonObject, JsonValue } from './json.js'\nimport { encodeLexLink, parseLexLink } from './link.js'\n\n/**\n * Serialize a Lex value to a JSON string.\n *\n * This function serializes AT Protocol data model values to JSON, automatically\n * encoding special types:\n * - `Cid` instances are encoded as `{$link: string}`\n * - `Uint8Array` instances are encoded as `{$bytes: string}` (base64)\n *\n * @param input - The Lex value to stringify\n * @returns A JSON string representation of the value\n *\n * @example\n * ```typescript\n * import { lexStringify } from '@atproto/lex'\n *\n * // Stringify with CID and bytes encoding\n * const json = lexStringify({\n *   ref: someCid,\n *   data: new Uint8Array([72, 101, 108, 108, 111])\n * })\n * // json is '{\"ref\":{\"$link\":\"bafyrei...\"},\"data\":{\"$bytes\":\"SGVsbG8=\"}}'\n * ```\n */\nexport function lexStringify(input: LexValue): string {\n  // @NOTE Because of the way the \"replacer\" works in JSON.stringify, it's\n  // simpler to convert Lex to JSON first rather than trying to do it\n  // on-the-fly.\n  return JSON.stringify(lexToJson(input))\n}\n\n/**\n * Options for parsing JSON to Lex values.\n */\nexport type LexParseOptions = {\n  /**\n   * When enabled, forbids the presence of invalid Lex values such as:\n   * - Non-integer numbers (only safe integers are valid in the Lex data model)\n   * - Malformed `$link` objects\n   * - Malformed `$bytes` objects\n   * - Objects with invalid or empty `$type` properties\n   * - Invalid {@link BlobRef} (`$type: 'blob'`) objects\n   *\n   * When disabled (default), invalid special objects are left as plain objects.\n   *\n   * @default false\n   */\n  strict?: boolean\n}\n\n/**\n * Parses a JSON string into Lex values.\n *\n * This function parses JSON and automatically decodes AT Protocol special types:\n * - `{$link: string}` objects are decoded to `Cid` instances\n * - `{$bytes: string}` objects are decoded to `Uint8Array` instances\n * - `{$type: 'blob'}` objects are validated\n *\n * @typeParam T - Type cast for the resulting Lex value. Use when you want to specify the expected structure of the parsed data.\n * @param input - The JSON string to parse\n * @param options - Parsing options (e.g., strict mode)\n * @returns The parsed Lex value\n * @throws {SyntaxError} If the input is not valid JSON\n * @throws {TypeError} If strict mode is enabled and invalid Lex values are found\n *\n * @example\n * ```typescript\n * import { lexParse } from '@atproto/lex'\n *\n * // Parse JSON with $link and $bytes decoding\n * const parsed = lexParse<{\n *   ref: Cid\n *   data: Uint8Array\n * }>(`{\n *   \"ref\": { \"$link\": \"bafyrei...\" },\n *   \"data\": { \"$bytes\": \"SGVsbG8sIHdvcmxkIQ==\" }\n * }`)\n *\n * // Parse a single CID\n * const someCid = lexParse<Cid>('{\"$link\": \"bafyrei...\"}')\n *\n * // Parse binary data\n * const someBytes = lexParse<Uint8Array>('{\"$bytes\": \"SGVsbG8sIHdvcmxkIQ==\"}')\n * ```\n */\nexport function lexParse<T extends LexValue = LexValue>(\n  input: string,\n  options: LexParseOptions = { strict: false },\n): T {\n  return JSON.parse(input, function (key: string, value: JsonValue): LexValue {\n    switch (typeof value) {\n      case 'object':\n        if (value === null) return null\n        if (Array.isArray(value)) return value\n        return parseSpecialJsonObject(value, options) ?? value\n      case 'number':\n        if (Number.isSafeInteger(value)) return value\n        if (options.strict) {\n          throw new TypeError(`Invalid non-integer number: ${value}`)\n        }\n      // fallthrough\n      default:\n        return value\n    }\n  })\n}\n\n/**\n * Converts a parsed JSON representation of Lexicon value to a {@link LexValue}.\n *\n * This function transforms already-parsed JSON objects into Lex values by\n * decoding AT Protocol special types:\n * - `{$link: string}` objects are converted to `Cid` instances\n * - `{$bytes: string}` objects are converted to `Uint8Array` instances\n *\n * Use this when you have a JavaScript object (e.g., from `JSON.parse()`) and\n * need to convert it to the Lex data model. For parsing JSON strings directly,\n * use {@link lexParse} instead.\n *\n * @param value - The JSON value to convert\n * @param options - Parsing options (e.g., strict mode)\n * @returns The converted Lex value\n * @throws {TypeError} If strict mode is enabled and invalid Lex values are found\n * @throws {TypeError} If the value contains unsupported types (e.g., undefined at top level)\n *\n * @example\n * ```typescript\n * import { jsonToLex } from '@atproto/lex'\n *\n * // Convert parsed JSON to Lex values\n * const lex = jsonToLex({\n *   ref: { $link: 'bafyrei...' },  // Converted to Cid\n *   data: { $bytes: 'SGVsbG8sIHdvcmxkIQ==' }  // Converted to Uint8Array\n * })\n * ```\n */\nexport function jsonToLex(\n  value: JsonValue,\n  options: LexParseOptions = { strict: false },\n): LexValue {\n  switch (typeof value) {\n    case 'object': {\n      if (value === null) return null\n      if (Array.isArray(value)) return jsonArrayToLex(value, options)\n      return (\n        parseSpecialJsonObject(value, options) ??\n        jsonObjectToLexMap(value, options)\n      )\n    }\n    case 'number':\n      if (Number.isSafeInteger(value)) return value\n      if (options.strict) {\n        throw new TypeError(`Invalid non-integer number: ${value}`)\n      }\n    // fallthrough\n    case 'boolean':\n    case 'string':\n      return value\n    default:\n      throw new TypeError(`Invalid JSON value: ${typeof value}`)\n  }\n}\n\nfunction jsonArrayToLex(\n  input: JsonValue[],\n  options: LexParseOptions,\n): LexValue[] {\n  // Lazily copy value\n  let copy: LexValue[] | undefined\n  for (let i = 0; i < input.length; i++) {\n    const inputItem = input[i]\n    const item = jsonToLex(inputItem, options)\n    if (item !== inputItem) {\n      copy ??= Array.from(input)\n      copy[i] = item\n    }\n  }\n  return copy ?? input\n}\n\nfunction jsonObjectToLexMap(\n  input: JsonObject,\n  options: LexParseOptions,\n): LexMap {\n  // Lazily copy value\n  let copy: LexMap | undefined = undefined\n  for (const [key, jsonValue] of Object.entries(input)) {\n    // Prevent prototype pollution\n    if (key === '__proto__') {\n      throw new TypeError('Invalid key: __proto__')\n    }\n\n    // Ignore (strip) undefined values\n    if (jsonValue === undefined) {\n      copy ??= { ...input }\n      delete copy[key]\n      continue\n    }\n\n    const value = jsonToLex(jsonValue!, options)\n    if (value !== jsonValue) {\n      copy ??= { ...input }\n      copy[key] = value\n    }\n  }\n  return copy ?? input\n}\n\n/**\n * Converts a Lex value to a JSON-compatible value.\n *\n * This function transforms Lex data model values into plain JavaScript objects\n * suitable for JSON serialization:\n * - `Cid` instances are converted to `{$link: string}` objects\n * - `Uint8Array` instances are converted to `{$bytes: string}` objects (base64)\n *\n * Use this when you need to convert Lex values to plain objects (e.g., for\n * custom serialization or inspection). For direct JSON string output, use\n * {@link lexStringify} instead.\n *\n * @param value - The Lex value to convert\n * @returns The JSON-compatible value\n * @throws {TypeError} If the value contains unsupported types\n *\n * @example\n * ```typescript\n * import { lexToJson } from '@atproto/lex'\n *\n * // Convert Lex values to JSON-compatible objects\n * const obj = lexToJson({\n *   ref: someCid,      // Converted to { $link: string }\n *   data: someBytes    // Converted to { $bytes: string }\n * })\n * ```\n */\nexport function lexToJson(value: LexValue): JsonValue {\n  switch (typeof value) {\n    case 'object':\n      if (value === null) {\n        return value\n      } else if (Array.isArray(value)) {\n        return lexArrayToJson(value)\n      } else if (isCid(value)) {\n        return encodeLexLink(value)\n      } else if (ArrayBuffer.isView(value)) {\n        return encodeLexBytes(value)\n      } else {\n        return encodeLexMap(value)\n      }\n    case 'boolean':\n    case 'string':\n    case 'number':\n      return value\n    default:\n      throw new TypeError(`Invalid Lex value: ${typeof value}`)\n  }\n}\n\nfunction lexArrayToJson(input: LexArray): JsonValue[] {\n  // Lazily copy value\n  let copy: JsonValue[] | undefined\n  for (let i = 0; i < input.length; i++) {\n    const inputItem = input[i]\n    const item = lexToJson(inputItem)\n    if (item !== inputItem) {\n      copy ??= Array.from(input) as JsonValue[]\n      copy[i] = item\n    }\n  }\n  return copy ?? (input as JsonValue[])\n}\n\nfunction encodeLexMap(input: LexMap): JsonObject {\n  // Lazily copy value\n  let copy: JsonObject | undefined = undefined\n  for (const [key, lexValue] of Object.entries(input)) {\n    // Prevent prototype pollution\n    if (key === '__proto__') {\n      throw new TypeError('Invalid key: __proto__')\n    }\n\n    // Ignore (strip) undefined values\n    if (lexValue === undefined) {\n      copy ??= { ...input } as JsonObject\n      delete copy[key]\n      continue\n    }\n\n    const jsonValue = lexToJson(lexValue!)\n    if (jsonValue !== lexValue) {\n      copy ??= { ...input } as JsonObject\n      copy[key] = jsonValue\n    }\n  }\n  return copy ?? (input as JsonObject)\n}\n\nfunction parseSpecialJsonObject(\n  input: LexMap,\n  options: LexParseOptions,\n): Cid | Uint8Array | BlobRef | undefined {\n  // Hot path: use hints to avoid parsing when possible\n\n  if (input.$link !== undefined) {\n    const cid = parseLexLink(input)\n    if (cid) return cid\n    if (options.strict) throw new TypeError(`Invalid $link object`)\n  } else if (input.$bytes !== undefined) {\n    const bytes = parseLexBytes(input)\n    if (bytes) return bytes\n    if (options.strict) throw new TypeError(`Invalid $bytes object`)\n  } else if (input.$type !== undefined) {\n    // @NOTE Since blobs are \"just\" regular lex objects with a special shape,\n    // and because an object that does not conform to the blob shape would still\n    // result in undefined being returned, we only attempt to parse blobs when\n    // the strict option is enabled.\n    if (options.strict) {\n      if (input.$type === 'blob') {\n        const blob = parseBlobRef(input, options)\n        if (blob) return blob\n        throw new TypeError(`Invalid blob object`)\n      } else if (typeof input.$type !== 'string') {\n        throw new TypeError(`Invalid $type property (${typeof input.$type})`)\n      } else if (input.$type.length === 0) {\n        throw new TypeError(`Empty $type property`)\n      }\n    }\n  }\n\n  // @NOTE We ignore legacy blob representation here. They can be handled at the\n  // application level if needed.\n\n  return undefined\n}\n"
  },
  {
    "path": "packages/lex/lex-json/src/link.ts",
    "content": "import {\n  CheckCidOptions,\n  Cid,\n  InferCheckedCid,\n  parseCid,\n} from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\n\n/**\n * Parses a `{$link: string}` JSON object into a {@link Cid} instance.\n *\n * In the AT Protocol data model, CID references are represented in JSON as an\n * object with a single `$link` property containing a base32-encoded CID string,\n * prefixed with \"b\". This function decodes that representation into a `Cid`\n * object.\n *\n * @param input - An object potentially containing a `{$link: string}` property\n * @param options - Optional CID validation options\n * @returns The parsed {@link Cid} if the input is a valid `$link` object,\n *          or `undefined` if the input is not a valid `$link` representation\n * @throws {TypeError} If `$link` is present but is not a valid CID string\n *\n * @example\n * ```typescript\n * // Parse a $link object to Cid\n * const cid = parseLexLink({ $link: 'bafyreib2rxk3rybloqtqwbo' })\n * // cid is a Cid instance\n *\n * // Returns undefined for non-$link objects\n * const result = parseLexLink({ foo: 'bar' })\n * // result is undefined\n *\n * // Returns undefined for objects with extra properties\n * const invalid = parseLexLink({ $link: 'bafyrei...', extra: true })\n * // invalid is undefined\n * ```\n */\nexport function parseLexLink<TOptions extends CheckCidOptions>(\n  input: undefined | Record<string, unknown>,\n  options: TOptions,\n): InferCheckedCid<TOptions> | undefined\nexport function parseLexLink(\n  input?: Record<string, unknown>,\n  options?: CheckCidOptions,\n): Cid | undefined\nexport function parseLexLink(\n  input?: Record<string, unknown>,\n  options?: CheckCidOptions,\n): Cid | undefined {\n  if (!input || !('$link' in input)) {\n    return undefined\n  }\n\n  for (const key in input) {\n    if (key !== '$link') {\n      return undefined\n    }\n  }\n\n  const { $link } = input\n\n  if (typeof $link !== 'string') {\n    return undefined\n  }\n\n  if ($link.length === 0) {\n    return undefined\n  }\n\n  // Arbitrary limit to prevent DoS via extremely long CIDs\n  if ($link.length > 2048) {\n    return undefined\n  }\n\n  try {\n    return parseCid($link, options)\n  } catch (cause) {\n    return undefined\n  }\n}\n\n/**\n * Encodes a {@link Cid} instance into a `{$link: string}` JSON representation.\n *\n * In the AT Protocol data model, CID references are represented in JSON as an\n * object with a single `$link` property containing a base32-encoded CID string,\n * prefixed with \"b\". This function performs that encoding.\n *\n * @param cid - The CID to encode\n * @returns An object with a `$link` property containing the string representation of the CID\n *\n * @example\n * ```typescript\n * const cid = CID.parse('bafyreib2rxk3rybloqtqwbo')\n * const encoded = encodeLexLink(cid)\n * // encoded is { $link: 'bafyreib2rxk3rybloqtqwbo' }\n * ```\n */\nexport function encodeLexLink(cid: Cid): JsonValue {\n  return { $link: cid.toString() }\n}\n"
  },
  {
    "path": "packages/lex/lex-json/tests/data-model-invalid.json",
    "content": "[\n  {\n    \"note\": \"top-level not an object\",\n    \"json\": \"blah\"\n  },\n  {\n    \"note\": \"float\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": \"com.example.blah\",\n        \"a\": 123.456,\n        \"b\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"record with $type null\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": null,\n        \"a\": 123,\n        \"b\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"record with $type wrong type\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": 123,\n        \"a\": 123,\n        \"b\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"record with empty $type string\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": \"\",\n        \"a\": 123,\n        \"b\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"blob with string size\",\n    \"json\": {\n      \"blb\": {\n        \"$type\": \"blob\",\n        \"ref\": {\n          \"$link\": \"bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity\"\n        },\n        \"mimeType\": \"image/jpeg\",\n        \"size\": \"10000\"\n      }\n    }\n  },\n  {\n    \"note\": \"blob with missing key\",\n    \"json\": {\n      \"blb\": {\n        \"$type\": \"blob\",\n        \"mimeType\": \"image/jpeg\",\n        \"size\": 10000\n      }\n    }\n  },\n  {\n    \"note\": \"bytes with wrong field type\",\n    \"json\": {\n      \"lnk\": {\n        \"$bytes\": [1, 2, 3]\n      }\n    }\n  },\n  {\n    \"note\": \"bytes with extra fields\",\n    \"json\": {\n      \"lnk\": {\n        \"$bytes\": \"nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0\",\n        \"other\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"link with wrong field type\",\n    \"json\": {\n      \"lnk\": {\n        \"$link\": 1234\n      }\n    }\n  },\n  {\n    \"note\": \"link with bogus CID\",\n    \"json\": {\n      \"lnk\": {\n        \"$link\": \".\"\n      }\n    }\n  },\n  {\n    \"note\": \"link with extra fields\",\n    \"json\": {\n      \"lnk\": {\n        \"$link\": \"bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity\",\n        \"other\": \"blah\"\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "packages/lex/lex-json/tests/data-model-valid.json",
    "content": "[\n  {\n    \"note\": \"trivial record\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": \"com.example.blah\",\n        \"a\": 123,\n        \"b\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"float, but integer-like\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": \"com.example.blah\",\n        \"a\": 123.0,\n        \"b\": \"blah\"\n      }\n    }\n  },\n  {\n    \"note\": \"empty list and object\",\n    \"json\": {\n      \"rcrd\": {\n        \"$type\": \"com.example.blah\",\n        \"a\": [],\n        \"b\": {}\n      }\n    }\n  },\n  {\n    \"note\": \"list of nullable\",\n    \"json\": {\n      \"arr\": [1, 2, null]\n    }\n  },\n  {\n    \"note\": \"list of lists\",\n    \"json\": {\n      \"arr\": [\n        [1, 2, 3],\n        [4, 5, 6]\n      ],\n      \"arr2\": [null, null, null]\n    }\n  }\n]\n"
  },
  {
    "path": "packages/lex/lex-json/tests/fixtures.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { isPlainObject } from '@atproto/lex-data'\nimport { JsonValue, jsonToLex } from '../src/index.js'\nimport invalidFixtures from './data-model-invalid.json' with { type: 'json' }\nimport validFixtures from './data-model-valid.json' with { type: 'json' }\n\nfunction parseLexFixture(input: JsonValue) {\n  const lex = jsonToLex(input, { strict: true })\n  if (!isPlainObject(lex)) {\n    throw new Error('Expected a plain object')\n  }\n}\n\ndescribe('invalidFixtures', () => {\n  for (const fixture of invalidFixtures) {\n    it(fixture.note, async () => {\n      expect(() => parseLexFixture(fixture.json)).toThrow()\n    })\n  }\n})\n\ndescribe('validFixtures', () => {\n  for (const fixture of validFixtures) {\n    it(fixture.note, () => {\n      expect(() => parseLexFixture(fixture.json)).not.toThrow()\n    })\n  }\n})\n"
  },
  {
    "path": "packages/lex/lex-json/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-json/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-json/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-json/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-password-session/.gitignore",
    "content": "src/lexicons\n"
  },
  {
    "path": "packages/lex/lex-password-session/CHANGELOG.md",
    "content": "# @atproto/lex-password-session\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-client@0.0.17\n  - @atproto/lex-schema@0.0.16\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`5a2f884`](https://github.com/bluesky-social/atproto/commit/5a2f8847efd91252971fa243d21bd52ada7aa8f4), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-client@0.0.16\n  - @atproto/lex-schema@0.0.15\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-client@0.0.15\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4672](https://github.com/bluesky-social/atproto/pull/4672) [`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `PasswordSession` async disposable for better use with hard coded app passwords\n\n- [#4672](https://github.com/bluesky-social/atproto/pull/4672) [`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add easy login for app password based bots\n\n- Updated dependencies [[`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2)]:\n  - @atproto/lex-client@0.0.14\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-client@0.0.13\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-client@0.0.12\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `PasswordSession.createAccount` static method\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `PasswordSession.create` to `PasswordSession.login`\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make all `PasswordSessionOptions` optional\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-client@0.0.11\n  - @atproto/lex-schema@0.0.11\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/lex-client@0.0.10\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-client@0.0.9\n  - @atproto/lex-schema@0.0.9\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4519](https://github.com/bluesky-social/atproto/pull/4519) [`5b19d39`](https://github.com/bluesky-social/atproto/commit/5b19d390b3c2f8701ac99395bc21d888d4a6866a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Password based session manager for `@atproto/lex-client`\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-client@0.0.8\n"
  },
  {
    "path": "packages/lex/lex-password-session/README.md",
    "content": "# @atproto/lex-password-session\n\nPassword-based session authentication for AT Protocol Lexicons. See the [Changelog](./CHANGELOG.md) for version history.\n\n```bash\nnpm install @atproto/lex-password-session\n```\n\n- Session management with automatic token refresh\n- Hooks for persisting and monitoring session state\n- PDS endpoint discovery from DID documents\n- Two-factor authentication support\n\n> [!IMPORTANT]\n>\n> This package is currently in **preview**. The API and features are subject to change before the stable release.\n\n**What is this?**\n\n`@atproto/lex-password-session` provides a `PasswordSession` class that implements the `Agent` interface from `@atproto/lex-client`. It handles password-based authentication with AT Protocol services, including:\n\n1. Creating sessions with username/password credentials\n2. Automatic token refresh when access tokens expire\n3. Session persistence through lifecycle hooks\n4. Graceful logout with server-side session cleanup\n\n```typescript\nimport { Client } from '@atproto/lex-client'\nimport { PasswordSession } from '@atproto/lex-password-session'\nimport * as app from './lexicons/app.js'\n\n// Login with credentials\nconst session = await PasswordSession.login({\n  service: 'https://bsky.social',\n  identifier: 'alice.bsky.social',\n  password: 'app-password',\n  onUpdated: (data) => saveToStorage(data),\n  onDeleted: (data) => clearStorage(data.did),\n})\n\nconst client = new Client(session)\n\n// Make authenticated requests\nconst profile = await client.call(app.bsky.actor.getProfile, {\n  actor: session.did,\n})\n```\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [Quick Start](#quick-start)\n- [PasswordSession](#passwordsession)\n  - [Login](#login)\n  - [Two-Factor Authentication](#two-factor-authentication)\n  - [Resume Session](#resume-session)\n  - [Logout](#logout)\n  - [Static Delete](#static-delete)\n  - [Create Account](#create-account)\n- [Session Hooks](#session-hooks)\n  - [onUpdated](#onupdated)\n  - [onUpdateFailure](#onupdatefailure)\n  - [onDeleted](#ondeleted)\n  - [onDeleteFailure](#ondeletefailure)\n- [Session Data](#session-data)\n- [Error Handling](#error-handling)\n- [Using with Client](#using-with-client)\n- [License](#license)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## Quick Start\n\n**1. Install the package**\n\n```bash\nnpm install @atproto/lex-password-session @atproto/lex-client\n```\n\n**2. Login and make requests**\n\n```typescript\nimport { Client } from '@atproto/lex-client'\nimport { PasswordSession } from '@atproto/lex-password-session'\n\nconst session = await PasswordSession.login({\n  service: 'https://bsky.social',\n  identifier: 'your-handle.bsky.social',\n  password: 'your-app-password',\n})\n\nconst client = new Client(session)\n\n// Make authenticated API calls\nconsole.log('Logged in as:', session.did)\n```\n\n## PasswordSession\n\nThe `PasswordSession` class manages password-based authentication sessions.\n\n### Login\n\nCreate a new session with username and password:\n\n```typescript\nimport { PasswordSession } from '@atproto/lex-password-session'\n\nconst session = await PasswordSession.login({\n  service: 'https://bsky.social',\n  identifier: 'alice.bsky.social', // handle or email\n  password: 'app-password',\n  onUpdated: (data) => {\n    // Persist session for later restoration\n    localStorage.setItem('session', JSON.stringify(data))\n  },\n  onDeleted: () => {\n    localStorage.removeItem('session')\n  },\n})\n\nconsole.log('Logged in as:', session.did)\n```\n\nThe `login()` method throws on failure. For expected errors like invalid credentials, an `XrpcResponseError` is thrown. For 2FA requirements, a `LexAuthFactorError` is thrown.\n\n### Two-Factor Authentication\n\n> [!CAUTION]\n>\n> Two-factor authentication only applies when using **main account credentials**, which is **strongly discouraged**. Password authentication should be used with [app passwords](https://bsky.app/settings/app-passwords) only because they are designed for programmatic access (bots, scripts, CLI tools). For user-facing applications, use OAuth via [@atproto/oauth-client](../../../oauth/oauth-client) which provides better security and user control.\n\nIf the account has 2FA enabled, login will throw a `LexAuthFactorError`:\n\n```typescript\nimport {\n  PasswordSession,\n  LexAuthFactorError,\n} from '@atproto/lex-password-session'\n\nasync function loginWith2FA(\n  identifier: string,\n  password: string,\n  authFactorToken?: string,\n): Promise<PasswordSession> {\n  try {\n    return await PasswordSession.login({\n      service: 'https://bsky.social',\n      identifier,\n      password,\n      authFactorToken,\n      onUpdated: (data) => saveToStorage(data),\n      onDeleted: (data) => removeFromStorage(data.did),\n    })\n  } catch (err) {\n    if (err instanceof LexAuthFactorError && !authFactorToken) {\n      // 2FA required - prompt user for code\n      const token = await promptUserFor2FACode(err.message)\n      return loginWith2FA(identifier, password, token)\n    }\n    throw err\n  }\n}\n```\n\n### Resume Session\n\nRestore a previously saved session:\n\n```typescript\nimport { PasswordSession, SessionData } from '@atproto/lex-password-session'\n\n// Load session from storage\nconst savedSession: SessionData = JSON.parse(localStorage.getItem('session')!)\n\n// Resume the session (automatically refreshes tokens)\nconst session = await PasswordSession.resume(savedSession, {\n  onUpdated: (data) => {\n    localStorage.setItem('session', JSON.stringify(data))\n  },\n  onDeleted: () => {\n    localStorage.removeItem('session')\n  },\n})\n\nconsole.log('Session resumed for:', session.did)\n\n// Access session properties\nconsole.log(session.did) // User's DID\nconsole.log(session.handle) // User's handle\nconsole.log(session.destroyed) // false (session is active)\n```\n\n> [!NOTE]\n>\n> `resume()` automatically calls `refresh()` to ensure the session is valid and tokens are current.\n\n### Logout\n\nEnd the session and notify the server:\n\n```typescript\nawait session.logout()\n```\n\nAfter logout:\n\n- The `onDeleted` hook is called\n- The session is marked as destroyed (`session.destroyed === true`)\n- Further requests will throw `'Logged out'`\n\n### Static Delete\n\nDelete a session without creating a session instance:\n\n```typescript\nimport { PasswordSession, SessionData } from '@atproto/lex-password-session'\n\nconst data: SessionData = JSON.parse(localStorage.getItem('session')!)\n\n// Delete the session on the server\nawait PasswordSession.delete(data)\n```\n\nThis is useful for cleanup scenarios where you don't need to make additional requests.\n\n### Create Account\n\nCreate a new account and get an authenticated session:\n\n```typescript\nimport { PasswordSession } from '@atproto/lex-password-session'\n\nconst session = await PasswordSession.createAccount(\n  {\n    handle: 'alice.bsky.social',\n    email: 'alice@example.com',\n    password: 'secure-password',\n  },\n  {\n    service: 'https://bsky.social',\n    onUpdated: (data) => saveToStorage(data),\n    onDeleted: (data) => removeFromStorage(data.did),\n  },\n)\n\nconsole.log('Account created:', session.did)\n```\n\n## Session Hooks\n\nHooks provide callbacks for session lifecycle events. All hooks receive the session instance as `this` context.\n\n### onUpdated\n\nCalled when the session is successfully created or refreshed:\n\n```typescript\nconst session = await PasswordSession.login({\n  service: 'https://bsky.social',\n  identifier: 'alice.bsky.social',\n  password: 'app-password',\n  onUpdated(data) {\n    // `this` is the PasswordSession instance\n    console.log('Session updated for:', this.did)\n\n    // Persist the updated session\n    saveSession(data)\n  },\n})\n```\n\n> [!IMPORTANT]\n>\n> Requests are blocked while `onUpdated` is running. Keep this callback fast to avoid delays.\n\n### onUpdateFailure\n\nCalled when token refresh fails due to transient errors (network issues, server unavailability):\n\n```typescript\n{\n  onUpdateFailure(data, error) {\n    console.warn('Token refresh failed:', error.message)\n    // Session may still be valid - consider retry logic\n  }\n}\n```\n\n### onDeleted\n\nCalled when the session is terminated (logout or server-side invalidation):\n\n```typescript\n{\n  onDeleted(data) {\n    console.log('Session ended for:', data.did)\n    clearPersistedSession(data.did)\n    redirectToLogin()\n  }\n}\n```\n\n### onDeleteFailure\n\nCalled when logout fails due to transient errors:\n\n```typescript\n{\n  onDeleteFailure(data, error) {\n    console.error('Logout failed:', error.message)\n    // Consider queuing for retry to avoid orphaned sessions\n    queueLogoutRetry(data)\n  }\n}\n```\n\n> [!WARNING]\n>\n> Ignoring delete failures can leave sessions active on the server. Implement retry logic for security-sensitive applications.\n\n## Session Data\n\nThe `SessionData` type contains all data needed to authenticate and restore sessions:\n\n```typescript\ntype SessionData = {\n  // Session credentials and user info from createSession response\n  accessJwt: string\n  refreshJwt: string\n  did: string\n  handle: string\n  email?: string\n  emailConfirmed?: boolean\n  didDoc?: object\n  // ... other fields from createSession\n\n  // Original service URL used for login\n  service: string\n}\n```\n\n## Error Handling\n\nThe `PasswordSession` class uses exception-based error handling:\n\n```typescript\nimport {\n  PasswordSession,\n  LexAuthFactorError,\n} from '@atproto/lex-password-session'\nimport { XrpcResponseError } from '@atproto/lex-client'\n\ntry {\n  const session = await PasswordSession.login({\n    service: 'https://bsky.social',\n    identifier: 'alice.bsky.social',\n    password: 'wrong-password',\n  })\n} catch (err) {\n  if (err instanceof LexAuthFactorError) {\n    console.error('2FA required')\n  } else if (err instanceof XrpcResponseError) {\n    switch (err.error) {\n      case 'AuthenticationRequired':\n        console.error('Invalid credentials')\n        break\n      case 'AccountTakedown':\n        console.error('Account has been suspended')\n        break\n      default:\n        console.error('Login failed:', err.message)\n    }\n  } else {\n    throw err\n  }\n}\n```\n\nCommon error codes:\n\n| Error Code                | Description                    |\n| ------------------------- | ------------------------------ |\n| `AuthenticationRequired`  | Invalid username or password   |\n| `AuthFactorTokenRequired` | 2FA code needed                |\n| `AccountTakedown`         | Account suspended              |\n| `ExpiredToken`            | Token has expired (on refresh) |\n| `InvalidToken`            | Token is invalid               |\n\n## Using with Client\n\nThe `PasswordSession` implements the `Agent` interface and can be used directly with `Client`:\n\n```typescript\nimport { Client } from '@atproto/lex-client'\nimport { PasswordSession } from '@atproto/lex-password-session'\nimport * as app from './lexicons/app.js'\n\nconst session = await PasswordSession.login({\n  service: 'https://bsky.social',\n  identifier: 'alice.bsky.social',\n  password: 'app-password',\n})\n\nconst client = new Client(session)\n\n// The client automatically uses the session for authentication\nconst profile = await client.call(app.bsky.actor.getProfile, {\n  actor: client.assertDid,\n})\n\n// Tokens are automatically refreshed when expired\nconst timeline = await client.call(app.bsky.feed.getTimeline, {\n  limit: 50,\n})\n\n// Create records\nawait client.create(app.bsky.feed.post, {\n  text: 'Hello from lex-password-session!',\n  createdAt: new Date().toISOString(),\n})\n```\n\nThe session handles:\n\n- Adding `Authorization` headers to requests\n- Detecting expired tokens (401 responses or `ExpiredToken` errors)\n- Automatically refreshing tokens and retrying failed requests\n- Routing requests to the correct PDS based on DID document\n\n## License\n\nMIT or Apache2\n"
  },
  {
    "path": "packages/lex/lex-password-session/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-password-session\",\n  \"version\": \"0.0.10\",\n  \"license\": \"MIT\",\n  \"description\": \"Password based client authentication for AT Lexicons\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"utilities\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-password-session\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/lex-client\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-builder\": \"workspace:^\",\n    \"@atproto/lex-server\": \"workspace:^\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"prebuild\": \"node ./scripts/lex-build.mjs\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-password-session/scripts/lex-build.mjs",
    "content": "/* eslint-env node  */\n\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { build } from '@atproto/lex-builder'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nbuild({\n  lexicons: join(__dirname, '..', '..', '..', '..', 'lexicons'),\n  out: join(__dirname, '..', 'src', 'lexicons'),\n  clear: true,\n  include: [\n    'com.atproto.server.getSession',\n    'com.atproto.server.createAccount',\n    'com.atproto.server.createSession',\n    'com.atproto.server.deleteSession',\n    'com.atproto.server.refreshSession',\n  ],\n  lib: '@atproto/lex-schema',\n  pretty: true,\n  pureAnnotations: true,\n  indexFile: true,\n}).catch((err) => {\n  console.error('Error building lexicon schemas:', err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/lex/lex-password-session/src/error.ts",
    "content": "import { LexError, XrpcFailure } from '@atproto/lex-client'\n\n/**\n * Error thrown when two-factor authentication (2FA) is required.\n *\n * This error is thrown by {@link PasswordSession.login} when the server\n * requires an additional authentication factor (e.g., email code). Catch this\n * error to prompt the user for their 2FA code and retry the login with the\n * `authFactorToken` parameter.\n *\n * @example Handling 2FA requirement\n * ```ts\n * import { PasswordSession, LexAuthFactorError } from '@atproto/lex-password-session'\n *\n * try {\n *   const session = await PasswordSession.login({\n *     service: 'https://bsky.social',\n *     identifier: 'alice.bsky.social',\n *     password: 'xxxx-xxxx-xxxx-xxxx',\n *   })\n * } catch (err) {\n *   if (err instanceof LexAuthFactorError) {\n *     // Prompt user for 2FA code\n *     const token = await promptUser('Enter 2FA code from email:')\n *\n *     // Retry with the 2FA token\n *     const session = await PasswordSession.login({\n *       service: 'https://bsky.social',\n *       identifier: 'alice.bsky.social',\n *       password: 'xxxx-xxxx-xxxx-xxxx',\n *       authFactorToken: token,\n *     })\n *   }\n * }\n * ```\n *\n * @extends LexError\n */\nexport class LexAuthFactorError extends LexError {\n  name = 'LexAuthFactorError'\n\n  /**\n   * Creates a new LexAuthFactorError.\n   *\n   * @param cause - The underlying XRPC failure response from the server\n   */\n  constructor(readonly cause: XrpcFailure) {\n    super(cause.error, cause.message ?? 'Auth factor token required', { cause })\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-password-session/src/index.ts",
    "content": "export * from './error.js'\nexport * from './password-session.js'\n"
  },
  {
    "path": "packages/lex/lex-password-session/src/password-session-utils.test.ts",
    "content": "/* eslint-disable @typescript-eslint/no-namespace */\n\nimport { describe, expect, it } from 'vitest'\nimport { DidString, HandleString } from '@atproto/lex-schema'\nimport { LexServerAuthError } from '@atproto/lex-server'\n\nconst randomString = () =>\n  Math.random().toString(36).substring(2, 10) +\n  Math.random().toString(36).substring(2, 10)\n\nexport class Session {\n  active = true\n  accessJwt = randomString()\n  refreshJwt = randomString()\n  constructor(readonly identifier: string) {}\n  get did(): DidString {\n    return `did:example:${this.identifier}`\n  }\n  get handle(): HandleString {\n    return `${this.identifier}.example`\n  }\n  get email(): string {\n    return `${this.identifier}@example.com`\n  }\n  rotate() {\n    this.accessJwt = randomString()\n    this.refreshJwt = randomString()\n    return this\n  }\n  destroy() {\n    this.active = false\n  }\n}\n\ndescribe('Session', () => {\n  it('generates DID and handle from identifier', async () => {\n    const session = new Session('alice')\n    expect(session.did).toBe('did:example:alice')\n    expect(session.handle).toBe('alice.example')\n  })\n\n  it('rotates tokens', async () => {\n    const session = new Session('alice')\n    const oldAccess = session.accessJwt\n    const oldRefresh = session.refreshJwt\n    session.rotate()\n    expect(session.accessJwt).not.toBe(oldAccess)\n    expect(session.refreshJwt).not.toBe(oldRefresh)\n  })\n\n  it('destroys session', async () => {\n    const session = new Session('alice')\n    expect(session.active).toBe(true)\n    session.destroy()\n    expect(session.active).toBe(false)\n  })\n})\n\nexport class AuthVerifier {\n  sessions: Session[] = []\n\n  async create(credentials: {\n    identifier: string\n    password: string\n    authFactorToken?: string\n  }) {\n    if (!credentials.identifier || credentials.password !== 'password123') {\n      throw new LexServerAuthError(\n        'AuthenticationRequired',\n        'Invalid identifier',\n      )\n    }\n    if (credentials.authFactorToken !== '2fa-token') {\n      throw new LexServerAuthError(\n        'AuthFactorTokenRequired',\n        '2FA token is required',\n      )\n    }\n    const session = new Session(credentials.identifier)\n    this.sessions.push(session)\n    return session\n  }\n\n  async findBy(predicate: (s: Session) => boolean) {\n    return this.sessions.find((s) => s.active && predicate(s))\n  }\n\n  accessStrategy = async ({ request }: { request: Request }) => {\n    const auth = request.headers.get('authorization')\n    const token = auth?.startsWith('Bearer ') && auth.slice(7)\n    const session = await this.findBy((s) => s.accessJwt === token)\n    if (!session) {\n      throw new LexServerAuthError('AuthenticationRequired', 'Invalid token', {\n        Bearer: { realm: 'access token' },\n      })\n    }\n    return { session }\n  }\n\n  refreshStrategy = async ({ request }: { request: Request }) => {\n    const auth = request.headers.get('authorization')\n    const token = auth?.startsWith('Bearer ') && auth.slice(7)\n    const session = await this.findBy((s) => s.refreshJwt === token)\n    if (!session) {\n      throw new LexServerAuthError('ExpiredToken', 'Invalid token', {\n        Bearer: { realm: 'refresh token' },\n      })\n    }\n    return { session }\n  }\n}\n\ndescribe('AuthVerifier', () => {\n  it('creates session with valid credentials', async () => {\n    const verifier = new AuthVerifier()\n    const session = await verifier.create({\n      identifier: 'alice',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n    })\n    expect(session.identifier).toBe('alice')\n  })\n\n  it('rejects invalid credentials', async () => {\n    const verifier = new AuthVerifier()\n    await expect(\n      verifier.create({\n        identifier: 'alice',\n        password: 'wrong-password',\n      }),\n    ).rejects.toMatchObject({\n      error: 'AuthenticationRequired',\n    })\n  })\n\n  it('rejects missing 2fa token', async () => {\n    const verifier = new AuthVerifier()\n    await expect(\n      verifier.create({\n        identifier: 'alice',\n        password: 'password123',\n      }),\n    ).rejects.toMatchObject({\n      error: 'AuthFactorTokenRequired',\n    })\n  })\n\n  it('finds session by access token', async () => {\n    const verifier = new AuthVerifier()\n    const session = await verifier.create({\n      identifier: 'alice',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n    })\n    const found = await verifier.accessStrategy({\n      request: new Request('http://example.com', {\n        headers: { authorization: `Bearer ${session.accessJwt}` },\n      }),\n    })\n    expect(found.session).toBe(session)\n  })\n\n  it('finds session by refresh token', async () => {\n    const verifier = new AuthVerifier()\n    const session = await verifier.create({\n      identifier: 'alice',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n    })\n    const found = await verifier.refreshStrategy({\n      request: new Request('http://example.com', {\n        headers: { authorization: `Bearer ${session.refreshJwt}` },\n      }),\n    })\n    expect(found.session).toBe(session)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-password-session/src/password-session.test.ts",
    "content": "/* eslint-disable @typescript-eslint/no-namespace */\n\nimport { afterAll, assert, beforeAll, describe, expect, it, vi } from 'vitest'\nimport { Client, XrpcAuthenticationError } from '@atproto/lex-client'\nimport { l } from '@atproto/lex-schema'\nimport { LexRouter, LexServerAuthError } from '@atproto/lex-server'\nimport { Server, serve } from '@atproto/lex-server/nodejs'\nimport { LexAuthFactorError } from './error.js'\nimport { com } from './lexicons/index.js'\nimport { AuthVerifier } from './password-session-utils.test.js'\nimport {\n  PasswordSession,\n  PasswordSessionOptions,\n  SessionData,\n} from './password-session.js'\n\nconst defaultOptions: Partial<PasswordSessionOptions> = {\n  onUpdateFailure: async (session, cause) => {\n    throw new Error('Should not fail to refresh session', { cause })\n  },\n  onDeleteFailure: async (session, cause) => {\n    throw new Error('Should not fail to delete session', { cause })\n  },\n}\n\n// Example app lexicon\nnamespace app {\n  export namespace example {\n    export namespace customMethod {\n      export const main = l.procedure(\n        'app.example.customMethod',\n        l.params(),\n        l.jsonPayload({ message: l.string() }),\n        l.jsonPayload({\n          message: l.string(),\n          did: l.string({ format: 'did' }),\n        }),\n      )\n    }\n\n    export namespace expiredToken {\n      export const main = l.query(\n        'app.example.expiredToken',\n        l.params(),\n        l.payload(),\n      )\n    }\n  }\n}\n\ndescribe(PasswordSession, () => {\n  let entrywayServer: Server\n  let entrywayOrigin: string\n\n  let pdsServer: Server\n  let pdsOrigin: string\n\n  beforeAll(async () => {\n    const authVerifier = new AuthVerifier()\n\n    const entrywayRouter = new LexRouter()\n      .add(com.atproto.server.createSession, async ({ input }) => {\n        const session = await authVerifier.create(input.body)\n\n        const body: com.atproto.server.createSession.$OutputBody = {\n          accessJwt: session.accessJwt,\n          refreshJwt: session.refreshJwt,\n\n          did: session.did,\n          didDoc: {\n            '@context': 'https://w3.org/ns/did/v1',\n            id: session.did,\n            service: [\n              {\n                id: `${session.did}#atproto_pds`,\n                type: 'AtprotoPersonalDataServer',\n                serviceEndpoint: pdsUrl,\n              },\n            ],\n          },\n          handle: session.handle,\n        }\n\n        return { body }\n      })\n      .add(com.atproto.server.getSession, {\n        auth: authVerifier.accessStrategy,\n        handler: async ({ credentials: { session } }) => {\n          const body: com.atproto.server.getSession.$OutputBody = {\n            did: session.did,\n            didDoc: {\n              '@context': 'https://w3.org/ns/did/v1',\n              id: session.did,\n              service: [\n                {\n                  id: `${session.did}#atproto_pds`,\n                  type: 'AtprotoPersonalDataServer',\n                  serviceEndpoint: pdsOrigin,\n                },\n              ],\n            },\n            handle: session.handle,\n            email: session.email,\n            emailConfirmed: true,\n            emailAuthFactor: false,\n            active: true,\n            status: 'active',\n          }\n\n          return { body }\n        },\n      })\n      .add(com.atproto.server.refreshSession, {\n        auth: authVerifier.refreshStrategy,\n        handler: async ({ credentials: { session } }) => {\n          await session.rotate()\n\n          // Note, we omit email and didDoc here to test that they are properly\n          // fetched via getSession in the agent\n          const body: com.atproto.server.refreshSession.$OutputBody = {\n            accessJwt: session.accessJwt,\n            refreshJwt: session.refreshJwt,\n\n            did: session.did,\n            didDoc: undefined,\n            handle: session.handle,\n\n            email: undefined,\n            emailConfirmed: undefined,\n          }\n\n          return { body }\n        },\n      })\n      .add(com.atproto.server.deleteSession, {\n        auth: authVerifier.refreshStrategy,\n        handler: async ({ credentials: { session } }) => {\n          await session.destroy()\n          return {}\n        },\n      })\n\n    entrywayServer = await serve(entrywayRouter)\n    const { port } = entrywayServer.address() as { port: number }\n    entrywayOrigin = `http://localhost:${port}`\n\n    const pdsRouter = new LexRouter()\n      .add(app.example.customMethod, {\n        auth: authVerifier.accessStrategy,\n        handler: async ({ input, credentials: { session } }) => {\n          return { body: { message: input.body.message, did: session.did } }\n        },\n      })\n      .add(app.example.expiredToken, async () => {\n        throw new LexServerAuthError('ExpiredToken', 'Token expired')\n      })\n\n    pdsServer = await serve(pdsRouter)\n    const { port: pdsPort } = pdsServer.address() as { port: number }\n    pdsOrigin = `http://localhost:${pdsPort}`\n    const pdsUrl = pdsOrigin\n  })\n\n  afterAll(async () => {\n    entrywayServer.close()\n    pdsServer.close()\n  })\n\n  it('fails with invalid credentials', async () => {\n    const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()\n    const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()\n\n    await expect(\n      PasswordSession.login({\n        ...defaultOptions,\n        service: entrywayOrigin,\n        identifier: 'alice',\n        password: 'wrong-password',\n        onDeleted,\n        onUpdated,\n      }),\n    ).rejects.toMatchObject({\n      success: false,\n      error: 'AuthenticationRequired',\n    })\n\n    expect(onDeleted).not.toHaveBeenCalled()\n    expect(onUpdated).not.toHaveBeenCalled()\n  })\n\n  it('requires 2fa', async () => {\n    const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()\n    const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()\n\n    const result = await PasswordSession.login({\n      ...defaultOptions,\n      service: entrywayOrigin,\n      identifier: 'alice',\n      password: 'password123',\n      onDeleted,\n      onUpdated,\n    }).then(\n      () => {\n        throw new Error('Expected to fail')\n      },\n      (err: unknown) => err,\n    )\n\n    assert(result instanceof LexAuthFactorError)\n    expect(result.error).toBe('AuthFactorTokenRequired')\n    expect(onDeleted).not.toHaveBeenCalled()\n    expect(onUpdated).not.toHaveBeenCalled()\n  })\n\n  it('logs in', async () => {\n    const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()\n    const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()\n\n    const session = await PasswordSession.login({\n      ...defaultOptions,\n      service: entrywayOrigin,\n      identifier: 'alice',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n      onDeleted,\n      onUpdated,\n    })\n\n    expect(onUpdated).toHaveBeenCalledTimes(1)\n\n    const client = new Client(session)\n\n    await expect(\n      client.call(app.example.customMethod, { message: 'hello' }),\n    ).resolves.toMatchObject({\n      message: 'hello',\n      did: 'did:example:alice',\n    })\n\n    await expect(\n      client.call(app.example.customMethod, { message: 'world' }),\n    ).resolves.toMatchObject({\n      message: 'world',\n      did: 'did:example:alice',\n    })\n\n    expect(onDeleted).not.toHaveBeenCalled()\n\n    await session.logout()\n\n    expect(onDeleted).toHaveBeenCalled()\n\n    await expect(\n      client.call(app.example.customMethod, { message: 'hello' }),\n    ).rejects.toMatchObject({\n      message: 'Unable to fulfill XRPC request',\n      cause: expect.objectContaining({\n        message: 'Logged out',\n      }),\n    })\n  })\n\n  it('fails to perform unauthenticated call', async () => {\n    const client = new Client(pdsOrigin)\n    const result = await client.xrpcSafe(app.example.customMethod, {\n      body: { message: 'hello' },\n    })\n\n    assert(result.success === false)\n    assert(result instanceof XrpcAuthenticationError)\n    expect(result).toMatchObject({\n      success: false,\n      error: 'AuthenticationRequired',\n    })\n    expect(result.wwwAuthenticate).toEqual({\n      Bearer: { realm: 'access token' },\n    })\n  })\n\n  it('refreshes expired token', async () => {\n    const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()\n    const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()\n\n    const session = await PasswordSession.login({\n      ...defaultOptions,\n      service: entrywayOrigin,\n      identifier: 'bob',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n      onUpdated,\n      onDeleted,\n    })\n\n    const client = new Client(session)\n\n    await expect(\n      client.call(app.example.customMethod, { message: 'before' }),\n    ).resolves.toMatchObject({\n      message: 'before',\n      did: 'did:example:bob',\n    })\n\n    expect(onUpdated).toHaveBeenCalledTimes(1)\n\n    await expect(client.call(app.example.expiredToken)).rejects.toThrow(\n      'Token expired',\n    )\n\n    expect(onUpdated).toHaveBeenCalledTimes(2)\n    expect(onUpdated).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        service: entrywayOrigin,\n\n        accessJwt: expect.any(String),\n        refreshJwt: expect.any(String),\n\n        email: expect.stringContaining('@'),\n        emailConfirmed: true,\n        emailAuthFactor: false,\n        handle: 'bob.example',\n        did: 'did:example:bob',\n        didDoc: expect.objectContaining({ id: 'did:example:bob' }),\n      }),\n    )\n\n    await expect(\n      client.call(app.example.customMethod, { message: 'after' }),\n    ).resolves.toMatchObject({\n      message: 'after',\n      did: 'did:example:bob',\n    })\n  })\n\n  it('restores session from storage', async () => {\n    const onDeleted: PasswordSessionOptions['onDeleted'] = vi.fn()\n    const onUpdated: PasswordSessionOptions['onUpdated'] = vi.fn()\n\n    const initialAgent = await PasswordSession.login({\n      ...defaultOptions,\n      service: entrywayOrigin,\n      identifier: 'carla',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n      onUpdated,\n      onDeleted,\n    })\n\n    expect(initialAgent.did).toEqual('did:example:carla')\n    expect(onDeleted).toHaveBeenCalledTimes(0)\n    expect(onUpdated).toHaveBeenCalledTimes(1)\n    expect(onUpdated).toHaveBeenCalledWith(\n      expect.objectContaining({\n        accessJwt: expect.any(String),\n        refreshJwt: expect.any(String),\n      }),\n    )\n\n    const sessionData = initialAgent.session\n\n    const resumedAgent = await PasswordSession.resume(sessionData, {\n      ...defaultOptions,\n      onUpdated,\n      onDeleted,\n    })\n\n    expect(resumedAgent.did).toEqual('did:example:carla')\n    expect(onDeleted).toHaveBeenCalledTimes(0)\n    expect(onUpdated).toHaveBeenCalledTimes(2)\n\n    // The initial session was refreshed. The data it contains is now invalid.\n    await expect(initialAgent.refresh()).rejects.toMatchObject({\n      success: false,\n      error: 'ExpiredToken',\n    })\n\n    expect(onDeleted).toHaveBeenCalledTimes(1)\n\n    const client = new Client(resumedAgent)\n    await expect(\n      client.call(app.example.customMethod, { message: 'resume' }),\n    ).resolves.toMatchObject({\n      message: 'resume',\n      did: 'did:example:carla',\n    })\n\n    expect(onDeleted).toHaveBeenCalledTimes(1)\n    expect(onUpdated).toHaveBeenCalledTimes(2)\n\n    await resumedAgent.logout()\n\n    expect(onDeleted).toHaveBeenCalledTimes(2)\n    expect(onUpdated).toHaveBeenCalledTimes(2)\n  })\n\n  it('silently ignores expected logout errors', async () => {\n    let sessionData: SessionData | null = null\n\n    const session = await PasswordSession.login({\n      ...defaultOptions,\n      service: entrywayOrigin,\n      identifier: 'dave',\n      password: 'password123',\n      authFactorToken: '2fa-token',\n      onUpdated: (data) => {\n        sessionData = structuredClone(data)\n      },\n    })\n\n    assert(sessionData)\n\n    await session.logout()\n    await session.logout()\n\n    await PasswordSession.delete(sessionData)\n    await PasswordSession.delete(sessionData)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-password-session/src/password-session.ts",
    "content": "import {\n  Agent,\n  XrpcFailure,\n  buildAgent,\n  xrpc,\n  xrpcSafe,\n} from '@atproto/lex-client'\nimport { LexAuthFactorError } from './error.js'\nimport { com } from './lexicons/index.js'\nimport { extractPdsUrl, extractXrpcErrorCode } from './util.js'\n\n/**\n * Represents a failure response when refreshing a session.\n *\n * This type captures the possible error responses from\n * `com.atproto.server.refreshSession`, including both expected errors\n * (e.g., invalid/expired refresh token) and unexpected errors (e.g., network issues).\n */\nexport type RefreshFailure = XrpcFailure<\n  typeof com.atproto.server.refreshSession.main\n>\n\n/**\n * Represents a failure response when deleting a session.\n *\n * This type captures the possible error responses from\n * `com.atproto.server.deleteSession`, including both expected errors\n * and unexpected errors (e.g., network issues, server unavailability).\n */\nexport type DeleteFailure = XrpcFailure<\n  typeof com.atproto.server.deleteSession.main\n>\n\n/**\n * Persisted session data containing authentication credentials and service information.\n *\n * This type extends the response from `com.atproto.server.createSession` with the\n * service URL used for authentication. Store this data securely to resume sessions\n * later without re-authenticating.\n */\nexport type SessionData = com.atproto.server.createSession.$OutputBody & {\n  service: string\n}\n\nexport type LoginOptions = PasswordSessionOptions & {\n  service: string | URL\n  identifier: string\n  password: string\n  allowTakendown?: boolean\n  authFactorToken?: string\n}\n\nexport type PasswordSessionOptions = {\n  /**\n   * Custom fetch implementation to use for network requests\n   */\n  fetch?: typeof globalThis.fetch\n\n  /**\n   * Called whenever the session is successfully created/refreshed, and new\n   * credentials have been obtained. Use this hook to persist the updated\n   * session information.\n   *\n   * If this callback returns a promise, this function will never be called\n   * again (on the same process) until the promise resolves.\n   *\n   * @note this function **must** not throw\n   */\n  onUpdated?: (this: PasswordSession, data: SessionData) => void | Promise<void>\n\n  /**\n   * Called whenever the session update fails due to an expected error, such as\n   * a network issue or server unavailability. This function can be used to log\n   * the error or notify the user, but should not assume that the session is\n   * invalid.\n   *\n   * @note this function **must** not throw\n   */\n  onUpdateFailure?: (\n    this: PasswordSession,\n    data: SessionData,\n    err: RefreshFailure,\n  ) => void | Promise<void>\n\n  /**\n   * Called whenever the session is deleted, either due to an explicit logout or\n   * because the refresh operation indicated that the session is no longer\n   * valid. Use this hook to clean up any persisted session information and\n   * update the application state accordingly.\n   *\n   * @note this function **must** not throw\n   */\n  onDeleted?: (this: PasswordSession, data: SessionData) => void | Promise<void>\n\n  /**\n   * Called whenever a session deletion fails due to an unexpected error, such\n   * as a network issue or server unavailability. This function can be used to\n   * log the error or notify the user. When this function is called, the session\n   * might still be valid on the server. It is up to the implementation to\n   * decide whether to retry the deletion or keep the session active. Ignoring\n   * these errors is not recommended as it can lead to orphaned sessions on the\n   * server, or security issues if the user believes they have logged out when a\n   * bad actor is still using the session. The implementation should consider\n   * keeping track of failed deletions and retrying them later, until they\n   * succeed.\n   *\n   * @note this function **must** not throw\n   */\n  onDeleteFailure?: (\n    this: PasswordSession,\n    data: SessionData,\n    err: DeleteFailure,\n  ) => void | Promise<void>\n}\n\n/**\n * Password-based authentication session for AT Protocol services.\n *\n * This class provides session management for CLI tools, scripts, and bots that\n * need to authenticate with AT Protocol services using password credentials.\n * It implements the {@link Agent} interface, allowing it to be used directly\n * with AT Protocol clients.\n *\n * **Security Warning:** It is strongly recommended to use app passwords instead\n * of main account credentials. App passwords provide limited access and can be\n * revoked independently without compromising your main account. For browser-based\n * applications, use OAuth-based authentication instead.\n *\n * @example Basic usage with app password\n * ```ts\n * const session = await PasswordSession.login({\n *   service: 'https://bsky.social',\n *   identifier: 'alice.bsky.social',\n *   password: 'xxxx-xxxx-xxxx-xxxx', // App password\n *   onUpdated: (data) => saveToStorage(data),\n *   onDeleted: (data) => clearStorage(data.did),\n * })\n *\n * const client = new Client(session)\n * // Use client to make authenticated requests\n * ```\n *\n * @example Resuming a persisted session\n * ```ts\n * const savedData = JSON.parse(fs.readFileSync('session.json', 'utf8'))\n * const session = await PasswordSession.resume(savedData, {\n *   onUpdated: (data) => saveToStorage(data),\n *   onDeleted: (data) => clearStorage(data.did),\n * })\n * ```\n *\n * @implements {Agent}\n */\nexport class PasswordSession implements Agent, AsyncDisposable {\n  /**\n   * Internal {@link Agent} used for session management towards the\n   * authentication service only.\n   */\n  #serviceAgent: Agent\n\n  #sessionData: null | SessionData\n  #sessionPromise: Promise<SessionData>\n\n  constructor(\n    sessionData: SessionData,\n    protected readonly options: PasswordSessionOptions = {},\n  ) {\n    this.#serviceAgent = buildAgent({\n      service: sessionData.service,\n      fetch: options.fetch,\n    })\n\n    this.#sessionData = sessionData\n    this.#sessionPromise = Promise.resolve(this.#sessionData)\n  }\n\n  /**\n   * The DID (Decentralized Identifier) of the authenticated account.\n   *\n   * @throws {Error} If the session has been destroyed (logged out).\n   */\n  get did() {\n    return this.session.did\n  }\n\n  /**\n   * The handle (username) of the authenticated account.\n   *\n   * @throws {Error} If the session has been destroyed (logged out).\n   */\n  get handle() {\n    return this.session.handle\n  }\n\n  /**\n   * The current session data containing authentication credentials.\n   *\n   * @throws {Error} If the session has been destroyed (logged out).\n   */\n  get session() {\n    if (this.#sessionData) return this.#sessionData\n    throw new Error('Logged out')\n  }\n\n  /**\n   * Whether this session has been destroyed (logged out).\n   *\n   * Once destroyed, this session instance can no longer be used for\n   * authenticated requests. Create a new session via {@link PasswordSession.login}\n   * or {@link PasswordSession.resume}.\n   */\n  get destroyed(): boolean {\n    return this.#sessionData === null\n  }\n\n  /**\n   * Handles authenticated fetch requests to the user's PDS.\n   *\n   * This method implements the {@link Agent} interface and is called by\n   * AT Protocol clients to make authenticated requests. It automatically:\n   * - Adds the access token to request headers\n   * - Detects expired tokens and triggers refresh\n   * - Retries requests after successful token refresh\n   *\n   * @param path - The request path (will be resolved against the PDS URL)\n   * @param init - Standard fetch RequestInit options (headers, body, etc.)\n   * @returns The fetch Response from the PDS\n   * @throws {TypeError} If an 'authorization' header is already set in init\n   */\n  async fetchHandler(path: string, init: RequestInit): Promise<Response> {\n    const headers = new Headers(init.headers)\n    if (headers.has('authorization')) {\n      throw new TypeError(\"Unexpected 'authorization' header set\")\n    }\n\n    const sessionPromise = this.#sessionPromise\n    const sessionData = await sessionPromise\n\n    const fetch = this.options.fetch ?? globalThis.fetch\n\n    headers.set('authorization', `Bearer ${sessionData.accessJwt}`)\n    const initialRes = await fetch(fetchUrl(sessionData, path), {\n      ...init,\n      headers,\n    })\n\n    const refreshNeeded =\n      initialRes.status === 401 ||\n      (initialRes.status === 400 &&\n        (await extractXrpcErrorCode(initialRes)) === 'ExpiredToken')\n\n    if (!refreshNeeded) {\n      return initialRes\n    }\n\n    // Refresh session (unless it was already refreshed in the meantime)\n    const newSessionPromise =\n      this.#sessionPromise === sessionPromise\n        ? this.refresh()\n        : this.#sessionPromise\n\n    // Error should have been propagated through hooks\n    const newSessionData = await newSessionPromise.catch((_err) => null)\n    if (!newSessionData) {\n      return initialRes\n    }\n\n    // refresh silently failed, no point in retrying.\n    if (newSessionData.accessJwt === sessionData.accessJwt) {\n      return initialRes\n    }\n\n    if (init?.signal?.aborted) {\n      return initialRes\n    }\n\n    // The stream was already consumed. We cannot retry the request. A solution\n    // would be to tee() the input stream but that would bufferize the entire\n    // stream in memory which can lead to memory starvation. Instead, we will\n    // return the original response and let the calling code handle retries.\n    if (ReadableStream && init?.body instanceof ReadableStream) {\n      return initialRes\n    }\n\n    // Make sure the initial request is cancelled to avoid leaking resources\n    // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection\n    if (!initialRes.bodyUsed) {\n      await initialRes.body?.cancel()\n    }\n\n    // Finally, retry the request with the new access token\n    headers.set('authorization', `Bearer ${newSessionData.accessJwt}`)\n    return fetch(fetchUrl(newSessionData, path), { ...init, headers })\n  }\n\n  /**\n   * Refreshes the session by obtaining new access and refresh tokens.\n   *\n   * This method is automatically called by {@link fetchHandler} when the access\n   * token expires. You can also call it manually to proactively refresh tokens.\n   *\n   * On success, the {@link PasswordSessionOptions.onUpdated} callback is invoked\n   * with the new session data. On expected failures (invalid session), the\n   * {@link PasswordSessionOptions.onDeleted} callback is invoked. On unexpected\n   * failures (network issues), the {@link PasswordSessionOptions.onUpdateFailure}\n   * callback is invoked and the existing session data is preserved.\n   *\n   * @returns The refreshed session data\n   * @throws {RefreshFailure} If the session is no longer valid (triggers onDeleted)\n   */\n  async refresh(): Promise<SessionData> {\n    this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {\n      const response = await xrpcSafe(\n        this.#serviceAgent,\n        com.atproto.server.refreshSession.main,\n        { headers: { Authorization: `Bearer ${sessionData.refreshJwt}` } },\n      )\n\n      if (!response.success && response.matchesSchemaErrors()) {\n        // Expected errors that indicate the session is no longer valid\n        await this.options.onDeleted?.call(this, sessionData)\n\n        // Update the session promise to a rejected state\n        this.#sessionData = null\n        throw response\n      }\n\n      if (!response.success) {\n        // We failed to refresh the token, assume the session might still be\n        // valid by returning the existing session.\n        await this.options.onUpdateFailure?.call(this, sessionData, response)\n\n        return sessionData\n      }\n\n      const data = response.body\n\n      // Historically, refreshSession did not return all the fields from\n      // getSession. In particular, emailConfirmed and didDoc were missing.\n      // Similarly, some servers might not return the didDoc in refreshSession.\n      // We fetch them via getSession if missing, allowing to ensure that we are\n      // always talking with the right PDS.\n      if (data.emailConfirmed == null || data.didDoc == null) {\n        const extraData = await xrpcSafe(\n          this.#serviceAgent,\n          com.atproto.server.getSession.main,\n          { headers: { Authorization: `Bearer ${data.accessJwt}` } },\n        )\n        if (extraData.success && extraData.body.did === data.did) {\n          Object.assign(data, extraData.body)\n        }\n      }\n\n      const newSession: SessionData = {\n        ...data,\n        service: sessionData.service,\n      }\n\n      await this.options.onUpdated?.call(this, newSession)\n\n      return (this.#sessionData = newSession)\n    })\n\n    return this.#sessionPromise\n  }\n\n  /**\n   * Logs out by deleting the session on the server.\n   *\n   * This method invalidates both the access and refresh tokens on the server,\n   * preventing any further use of this session. After successful logout, the\n   * session is marked as destroyed and the {@link PasswordSessionOptions.onDeleted}\n   * callback is invoked.\n   *\n   * If the logout request fails due to network issues or server unavailability,\n   * the {@link PasswordSessionOptions.onDeleteFailure} callback is invoked and\n   * the session remains active locally. In this case, you should retry the\n   * logout later to ensure the session is properly invalidated on the server.\n   *\n   * @throws {DeleteFailure} If the logout request fails due to unexpected errors\n   */\n  async logout(): Promise<void> {\n    let reason: DeleteFailure | null = null\n\n    this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {\n      const result = await xrpcSafe(\n        this.#serviceAgent,\n        com.atproto.server.deleteSession.main,\n        { headers: { Authorization: `Bearer ${sessionData.refreshJwt}` } },\n      )\n\n      if (result.success || result.matchesSchemaErrors()) {\n        await this.options.onDeleted?.call(this, sessionData)\n\n        // Update the session promise to a rejected state\n        this.#sessionData = null\n        throw new Error('Logged out')\n      } else {\n        // Capture the reason for the failure to re-throw in the outer promise\n        reason = result\n\n        // An unknown/unexpected error occurred (network, server down, etc)\n        await this.options.onDeleteFailure?.call(this, sessionData, result)\n\n        // Keep the session in an active state\n        return sessionData\n      }\n    })\n\n    return this.#sessionPromise.then(\n      (_session) => {\n        // If the promise above resolved, then logout failed. Re-throw the\n        // reason captured earlier.\n        throw reason!\n      },\n      (_err) => {\n        // Successful logout\n      },\n    )\n  }\n\n  async [Symbol.asyncDispose]() {\n    await this.logout()\n  }\n\n  /**\n   * Creates a new account and returns an authenticated session.\n   *\n   * This static method registers a new account on the specified service and\n   * automatically creates an authenticated session for it.\n   *\n   * @param body - Account creation parameters (handle, email, password, etc.)\n   * @param options - Session options including the service URL\n   * @returns A new PasswordSession for the created account\n   * @throws If account creation fails (e.g., handle taken, invalid invite code)\n   *\n   * @example\n   * ```ts\n   * const session = await PasswordSession.createAccount(\n   *   {\n   *     handle: 'alice.bsky.social',\n   *     email: 'alice@example.com',\n   *     password: 'secure-password',\n   *   },\n   *   {\n   *     service: 'https://bsky.social',\n   *     onUpdated: (data) => saveToStorage(data),\n   *   }\n   * )\n   * ```\n   */\n  static async createAccount(\n    body: com.atproto.server.createAccount.$InputBody,\n    {\n      service,\n      headers,\n      ...options\n    }: PasswordSessionOptions & {\n      headers?: HeadersInit\n      service: string | URL\n    },\n  ): Promise<PasswordSession> {\n    const response = await xrpc(\n      buildAgent({ service, headers, fetch: options.fetch }),\n      com.atproto.server.createAccount.main,\n      { body },\n    )\n\n    const data: SessionData = {\n      ...response.body,\n      service: String(service),\n    }\n\n    const agent = new PasswordSession(data, options)\n    await options.onUpdated?.call(agent, data)\n    return agent\n  }\n\n  /**\n   * Creates a new authenticated session using password credentials.\n   *\n   * This static method authenticates with the specified service and returns\n   * a new PasswordSession instance that can be used for authenticated requests.\n   *\n   * **Security Warning:** It is strongly recommended to use app passwords instead\n   * of main account credentials. App passwords can be created in your account\n   * settings and provide limited access that can be revoked independently. For\n   * browser-based applications, use OAuth-based authentication instead.\n   *\n   * @param options - Login options including service URL, identifier, and password\n   * @param options.service - The AT Protocol service URL (e.g., 'https://bsky.social')\n   * @param options.identifier - The user's handle or DID\n   * @param options.password - The user's password or app password\n   * @param options.allowTakendown - If true, allow login to takendown accounts\n   * @param options.authFactorToken - 2FA token if required by the server\n   * @returns A new authenticated PasswordSession\n   * @throws {LexAuthFactorError} If the server requires a 2FA token\n   * @throws If authentication fails (invalid credentials, etc.)\n   *\n   * **Basic login with app password in script**\n   * @example\n   * ```ts\n   * // .env\n   * // APP_PASSWORD_CREDENTIALS=\"https://<handle>:<app-password>@<pds-hosting-provider>\"\n   *\n   * // Make sure to dispose (or logout) the session when done to avoid leaking\n   * // resources and leaving orphaned sessions on the server\n   * await using session = await PasswordSession.login(process.env.APP_PASSWORD_CREDENTIALS)\n   *\n   * // Use session to make authenticated requests\n   * ```\n   *\n   * **Basic login with user password (not recommended!!!)**\n   * @example\n   * ```ts\n   * const session = await PasswordSession.login({\n   *   service: 'https://bsky.social',\n   *   identifier: 'alice.bsky.social',\n   *   password: 'xxxx',\n   *   onUpdated: (data) => saveToStorage(data),\n   *   onDeleted: (data) => clearStorage(data.did),\n   * })\n   *\n   * // Next time, use resume with the persisted session data to avoid storing\n   * // user credentials.\n   * ```\n   *\n   * **Handling 2FA requirement**\n   * @example\n   * ```ts\n   * try {\n   *   const session = await PasswordSession.login({\n   *     service: 'https://bsky.social',\n   *     identifier: 'alice.bsky.social',\n   *     password: 'xxxx',\n   *   })\n   * } catch (err) {\n   *   if (err instanceof LexAuthFactorError) {\n   *     const token = await promptUser('Enter 2FA code:')\n   *     const session = await PasswordSession.login({\n   *       service: 'https://bsky.social',\n   *       identifier: 'alice.bsky.social',\n   *       password: 'xxxx',\n   *       authFactorToken: token,\n   *     })\n   *   }\n   * }\n   * ```\n   */\n  static async login(\n    input: string | URL | LoginOptions,\n  ): Promise<PasswordSession> {\n    const {\n      service,\n      identifier,\n      password,\n      allowTakendown,\n      authFactorToken,\n      ...options\n    } =\n      typeof input === 'string' || input instanceof URL\n        ? parseLoginUrl(input)\n        : input\n\n    const xrpcAgent = buildAgent({\n      service,\n      fetch: options.fetch,\n    })\n\n    const response = await xrpcSafe(\n      xrpcAgent,\n      com.atproto.server.createSession.main,\n      { body: { identifier, password, allowTakendown, authFactorToken } },\n    )\n\n    if (!response.success) {\n      if (response.error === 'AuthFactorTokenRequired') {\n        throw new LexAuthFactorError(response)\n      }\n      throw response.reason\n    }\n\n    const data: SessionData = {\n      ...response.body,\n      service: String(service),\n    }\n\n    const agent = new PasswordSession(data, options)\n    await options.onUpdated?.call(agent, data)\n    return agent\n  }\n\n  /**\n   * Resume an existing session, ensuring it is still valid by refreshing it.\n   * Any error thrown here indicates that the session is definitely no longer\n   * valid. Network errors will be propagated through the\n   * {@link PasswordSessionOptions.onUpdateFailure} hook, and not re-thrown\n   * here. This means that a resolved promise does not necessarily indicate a\n   * valid session, only that it's refresh did not definitively fail.\n   *\n   * This is the same as calling {@link PasswordSession.refresh} after\n   * constructing the {@link PasswordSession} manually.\n   *\n   * @throws If, and only if, the session is definitely no longer valid.\n   */\n  static async resume(\n    data: SessionData,\n    options: PasswordSessionOptions,\n  ): Promise<PasswordSession> {\n    const agent = new PasswordSession(data, options)\n    await agent.refresh()\n    return agent\n  }\n\n  /**\n   * Delete a session without having to {@link resume resume()} it first, or\n   * provide hooks.\n   *\n   * @throws In case of unexpected error (network issue, server down, etc)\n   * meaning that the session may still be valid.\n   */\n  static async delete(\n    data: SessionData,\n    options?: PasswordSessionOptions,\n  ): Promise<void> {\n    const agent = new PasswordSession(data, options)\n    await agent.logout()\n  }\n}\n\nfunction fetchUrl(sessionData: SessionData, path: string): URL {\n  const pdsUrl = extractPdsUrl(sessionData.didDoc)\n  return new URL(path, pdsUrl ?? sessionData.service)\n}\n\nfunction parseLoginUrl(input: string | URL): LoginOptions {\n  const url = typeof input === 'string' ? new URL(input) : input\n  if (url.pathname !== '/') {\n    throw new TypeError('Invalid login URL: unexpected pathname')\n  }\n  if (url.hash) {\n    throw new TypeError('Invalid login URL: unexpected hash')\n  }\n  if (url.search) {\n    throw new TypeError('Invalid login URL: unexpected search parameters')\n  }\n  if (!url.username || !url.password) {\n    throw new TypeError('Invalid login URL: missing identifier or password')\n  }\n  return {\n    service: url.origin,\n    identifier: url.username,\n    password: url.password,\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-password-session/src/util.ts",
    "content": "import { LexMap, LexValue } from '@atproto/lex-client'\nimport { lexErrorDataSchema } from '@atproto/lex-schema'\n\nexport async function extractXrpcErrorCode(\n  response: Response,\n): Promise<string | null> {\n  const json = await peekJson(response, 10 * 1024) // Avoid reading large bodies\n  if (json === undefined) return null\n  if (!lexErrorDataSchema.matches(json)) return null\n  return json.error\n}\n\nasync function peekJson(\n  response: Response,\n  maxSize = Infinity,\n): Promise<undefined | LexValue> {\n  const type = extractType(response)\n  if (type !== 'application/json') return undefined\n  const length = extractLength(response)\n  if (length != null && length > maxSize) return undefined\n\n  try {\n    return (await response.clone().json()) as Promise<LexValue>\n  } catch {\n    return undefined\n  }\n}\n\nfunction extractLength({ headers }: Response) {\n  return headers.get('Content-Length')\n    ? Number(headers.get('Content-Length'))\n    : undefined\n}\n\nfunction extractType({ headers }: Response) {\n  return headers.get('Content-Type')?.split(';')[0]?.trim().toLowerCase()\n}\n\nexport function extractPdsUrl(didDoc?: LexMap): string | null {\n  const pdsService = ifArray(didDoc?.service)?.find((service) =>\n    ifString((service as any)?.id)?.endsWith('#atproto_pds'),\n  )\n  const pdsEndpoint = ifString((pdsService as any)?.serviceEndpoint)\n  return pdsEndpoint && URL.canParse(pdsEndpoint) ? pdsEndpoint : null\n}\n\nconst ifString = <T>(v: T) =>\n  (typeof v === 'string' ? v : undefined) as unknown extends T\n    ? undefined | string\n    : T extends string\n      ? string\n      : undefined\n\nconst ifArray = <T>(v: T) =>\n  (Array.isArray(v) ? v : undefined) as unknown extends T\n    ? undefined | unknown[]\n    : T extends unknown[]\n      ? Extract<T, unknown[]>\n      : undefined\n"
  },
  {
    "path": "packages/lex/lex-password-session/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-password-session/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-password-session/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-password-session/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-resolver/.gitignore",
    "content": "src/lexicons\n\n"
  },
  {
    "path": "packages/lex/lex-resolver/CHANGELOG.md",
    "content": "# @atproto/lex-resolver\n\n## 0.0.19\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-client@0.0.17\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-data@0.0.14\n  - @atproto/lex-document@0.0.17\n\n## 0.0.18\n\n### Patch Changes\n\n- Updated dependencies [[`5a2f884`](https://github.com/bluesky-social/atproto/commit/5a2f8847efd91252971fa243d21bd52ada7aa8f4), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`192685f`](https://github.com/bluesky-social/atproto/commit/192685fca75a68c9c50a94817d3f27da7fc02f56)]:\n  - @atproto/lex-client@0.0.16\n  - @atproto/lex-schema@0.0.15\n  - @atproto/syntax@0.5.1\n  - @atproto/repo@0.8.13\n  - @atproto/lex-document@0.0.16\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-client@0.0.15\n  - @atproto/lex-document@0.0.15\n\n## 0.0.16\n\n### Patch Changes\n\n- Updated dependencies [[`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2)]:\n  - @atproto/lex-client@0.0.14\n\n## 0.0.15\n\n### Patch Changes\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-document@0.0.14\n  - @atproto/lex-data@0.0.12\n  - @atproto/lex-client@0.0.13\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-document@0.0.13\n  - @atproto/lex-client@0.0.12\n  - @atproto/lex-data@0.0.11\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-client@0.0.11\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-document@0.0.12\n  - @atproto/lex-schema@0.0.11\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.2.6\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/lex-client@0.0.10\n  - @atproto/syntax@0.4.3\n  - @atproto/lex-data@0.0.9\n  - @atproto/lex-document@0.0.11\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/lex-client@0.0.9\n  - @atproto/lex-document@0.0.10\n  - @atproto/lex-schema@0.0.9\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-client@0.0.8\n  - @atproto/lex-document@0.0.9\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-client@0.0.7\n  - @atproto/lex-data@0.0.6\n  - @atproto/lex-schema@0.0.7\n  - @atproto/lex-document@0.0.8\n  - @atproto-labs/did-resolver@0.2.5\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n  - @atproto/lex-data@0.0.5\n  - @atproto/lex-client@0.0.6\n  - @atproto/lex-document@0.0.7\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-client@0.0.5\n  - @atproto/lex-data@0.0.4\n  - @atproto/lex-schema@0.0.5\n  - @atproto/lex-document@0.0.6\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/lex-document@0.0.5\n  - @atproto/lex-client@0.0.4\n  - @atproto/lex-schema@0.0.4\n  - @atproto/repo@0.8.12\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4384](https://github.com/bluesky-social/atproto/pull/4384) [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Verify the signature when fetching lexicon document from a repo\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add hooks\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `LexResolverError.from(string)` utility\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `didAuthority` option (Replaced by `hooks.onResolveAuthority`)\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename schema methods `validate`, `check` and `maybe` to `safeParse`, `matches` and `ifMatches` respectively.\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`2d13d05`](https://github.com/bluesky-social/atproto/commit/2d13d05ab06576703742b1b638d2f243b6b2915f), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`03a2a4b`](https://github.com/bluesky-social/atproto/commit/03a2a4bb3814ced7ad1d4fe6c94b5348a3bbc097), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba)]:\n  - @atproto/lex-schema@0.0.3\n  - @atproto/lex-document@0.0.4\n  - @atproto/lex-client@0.0.3\n  - @atproto/lex-data@0.0.2\n  - @atproto/syntax@0.4.2\n  - @atproto/crypto@0.4.5\n  - @atproto-labs/did-resolver@0.2.4\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-client@0.0.2\n  - @atproto/lex-document@0.0.3\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [[`5ffd612`](https://github.com/bluesky-social/atproto/commit/5ffd6129909071e979c30f31266119865ab582b6)]:\n  - @atproto/lex-document@0.0.2\n  - @atproto/lex-client@0.0.1\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-client@0.0.1\n  - @atproto/lex-document@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-resolver/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-resolver\",\n  \"version\": \"0.0.19\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon document resolver utility for AT Lexicons\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"resolver\",\n    \"utility\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-resolver\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/did-resolver\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/lex-client\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-document\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"@atproto/repo\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-builder\": \"workspace:^\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"prebuild\": \"node ./scripts/lex-build.mjs\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-resolver/scripts/lex-build.mjs",
    "content": "/* eslint-env node  */\n\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { build } from '@atproto/lex-builder'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nbuild({\n  lexicons: join(__dirname, '..', '..', '..', '..', 'lexicons'),\n  out: join(__dirname, '..', 'src', 'lexicons'),\n  clear: true,\n  include: ['com.atproto.sync.getRecord'],\n  lib: '@atproto/lex-schema',\n  pretty: true,\n  pureAnnotations: true,\n  indexFile: true,\n}).catch((err) => {\n  console.error('Error building lexicon schemas:', err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/lex/lex-resolver/src/index.ts",
    "content": "export * from './lex-resolver.js'\nexport * from './lex-resolver-error.js'\n"
  },
  {
    "path": "packages/lex/lex-resolver/src/lex-resolver-error.ts",
    "content": "import { LexError } from '@atproto/lex-data'\nimport { NSID } from '@atproto/syntax'\n\n/**\n * Error class for lexicon resolution failures.\n *\n * This error is thrown when the {@link LexResolver} encounters issues during\n * the resolution process, such as DNS lookup failures, DID resolution errors,\n * invalid lexicon documents, or network failures.\n *\n * @example Catching resolution errors\n * ```typescript\n * import { LexResolver, LexResolverError } from '@atproto/lex-resolver'\n *\n * const resolver = new LexResolver({})\n *\n * try {\n *   const result = await resolver.get('com.example.myLexicon')\n * } catch (error) {\n *   if (error instanceof LexResolverError) {\n *     console.error(`Failed to resolve ${error.nsid}: ${error.description}`)\n *     // Access the original cause if available\n *     if (error.cause) {\n *       console.error('Caused by:', error.cause)\n *     }\n *   }\n * }\n * ```\n *\n * @example Creating errors with the factory method\n * ```typescript\n * import { LexResolverError } from '@atproto/lex-resolver'\n *\n * // Create from string NSID\n * const error = LexResolverError.from(\n *   'com.example.myLexicon',\n *   'Custom error description'\n * )\n * ```\n */\nexport class LexResolverError extends LexError {\n  name = 'LexResolverError'\n\n  /**\n   * Creates a new LexResolverError instance.\n   *\n   * @param nsid - The NSID that failed to resolve\n   * @param description - Human-readable description of the error. Defaults to\n   *   a generic message if not provided.\n   * @param options - Standard error options including `cause` for error chaining\n   *\n   * @example\n   * ```typescript\n   * import { NSID } from '@atproto/syntax'\n   * import { LexResolverError } from '@atproto/lex-resolver'\n   *\n   * const nsid = NSID.from('com.example.myLexicon')\n   * const error = new LexResolverError(\n   *   nsid,\n   *   'DNS lookup failed',\n   *   { cause: originalError }\n   * )\n   * ```\n   */\n  constructor(\n    /**\n     * The NSID that failed to resolve.\n     */\n    public readonly nsid: NSID,\n    /**\n     * Human-readable description of what went wrong during resolution.\n     */\n    public readonly description = `Could not resolve Lexicon for NSID`,\n    options?: ErrorOptions,\n  ) {\n    super('LexiconResolutionFailure', `${description} (${nsid})`, options)\n  }\n\n  /**\n   * Factory method to create a LexResolverError from a string or NSID.\n   *\n   * This is a convenience method that handles the conversion of string NSIDs\n   * to NSID objects automatically.\n   *\n   * @param nsid - The NSID as a string or NSID object\n   * @param description - Optional human-readable description of the error\n   * @returns A new LexResolverError instance\n   *\n   * @example\n   * ```typescript\n   * import { LexResolverError } from '@atproto/lex-resolver'\n   *\n   * // Create from string\n   * const error1 = LexResolverError.from('com.example.myLexicon')\n   *\n   * // Create with description\n   * const error2 = LexResolverError.from(\n   *   'com.example.myLexicon',\n   *   'Authority not found in DNS'\n   * )\n   * ```\n   */\n  static from(nsid: NSID | string, description?: string) {\n    return new LexResolverError(\n      typeof nsid === 'string' ? NSID.from(nsid) : nsid,\n      description,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-resolver/src/lex-resolver.ts",
    "content": "import { resolveTxt } from 'node:dns/promises'\nimport * as crypto from '@atproto/crypto'\nimport { buildAgent, xrpc } from '@atproto/lex-client'\nimport { Cid } from '@atproto/lex-data'\nimport { LexiconDocument, lexiconDocumentSchema } from '@atproto/lex-document'\nimport {\n  MST,\n  MemoryBlockstore,\n  def as repoDef,\n  readCarWithRoot,\n  verifyCommitSig,\n} from '@atproto/repo'\nimport { AtUri, NSID, NsidString } from '@atproto/syntax'\nimport {\n  AtprotoVerificationMethod,\n  CreateDidResolverOptions,\n  Did,\n  DidResolver,\n  ResolveDidOptions,\n  assertDid,\n  createDidResolver,\n  extractAtprotoData,\n} from '@atproto-labs/did-resolver'\nimport { LexResolverError } from './lex-resolver-error.js'\nimport { com } from './lexicons/index.js'\n\n/**\n * Result returned when successfully resolving a lexicon document.\n *\n * Contains the full AT URI where the lexicon was found, the content-addressed\n * identifier (CID) for integrity verification, and the parsed lexicon document.\n */\nexport type LexResolverResult = {\n  /** The AT URI where the lexicon document was found */\n  uri: AtUri\n  /** Content identifier (CID) of the lexicon record for integrity verification */\n  cid: Cid\n  /** The parsed and validated lexicon document */\n  lexicon: LexiconDocument\n}\n\n/**\n * Result returned when fetching a lexicon document from a specific URI.\n *\n * This is a subset of {@link LexResolverResult} used internally and by hooks,\n * containing only the CID and lexicon document (without the URI, which is\n * already known from the fetch request).\n */\nexport type LexResolverFetchResult = {\n  /** Content identifier (CID) of the lexicon record */\n  cid: Cid\n  /** The parsed and validated lexicon document */\n  lexicon: LexiconDocument\n}\n\nexport type Awaitable<T> = T | Promise<T>\n\n/**\n * Callback hooks for customizing the lexicon resolution process.\n *\n * Hooks allow you to intercept, cache, or override the default resolution\n * behavior at various stages. Each hook can be synchronous or asynchronous.\n *\n * @example Implementing a cache with hooks\n * ```typescript\n * import { LexResolver, LexResolverHooks, LexResolverFetchResult } from '@atproto/lex-resolver'\n * import { AtUri } from '@atproto/syntax'\n *\n * const cache = new Map<string, LexResolverFetchResult>()\n *\n * const hooks: LexResolverHooks = {\n *   // Return cached result if available, bypassing network fetch\n *   onFetch({ uri }) {\n *     return cache.get(uri.toString())\n *   },\n *   // Cache successful fetches\n *   onFetchResult({ uri, cid, lexicon }) {\n *     cache.set(uri.toString(), { cid, lexicon })\n *   },\n *   // Log errors for monitoring\n *   onFetchError({ uri, err }) {\n *     console.error(`Failed to fetch ${uri}:`, err)\n *   }\n * }\n *\n * const resolver = new LexResolver({ hooks })\n * ```\n *\n * @example Overriding authority resolution for testing\n * ```typescript\n * const hooks: LexResolverHooks = {\n *   // Always resolve to a test DID\n *   onResolveAuthority({ nsid }) {\n *     if (nsid.authority === 'test.example') {\n *       return 'did:plc:test123'\n *     }\n *     // Return undefined to use default resolution\n *   }\n * }\n * ```\n */\nexport type LexResolverHooks = {\n  /**\n   * Hook called before resolving a lexicon authority DID. If a DID is returned,\n   * it will be used instead of performing the default resolution. In that case,\n   * the `onResolveAuthorityResult` and `onResolveAuthorityError` hooks will\n   * not be called.\n   *\n   * @param data - Object containing the NSID being resolved\n   * @returns A DID to use instead of default resolution, or void/undefined to proceed normally\n   */\n  onResolveAuthority?(data: { nsid: NSID }): Awaitable<void | Did>\n\n  /**\n   * Hook called after successfully resolving a lexicon authority DID.\n   *\n   * @param data - Object containing the NSID and resolved DID\n   */\n  onResolveAuthorityResult?(data: { nsid: NSID; did: Did }): Awaitable<void>\n\n  /**\n   * Hook called when authority resolution fails.\n   *\n   * @param data - Object containing the NSID and error that occurred\n   */\n  onResolveAuthorityError?(data: { nsid: NSID; err: unknown }): Awaitable<void>\n\n  /**\n   * Hook called before fetching a lexicon URI. If a result is returned, it will\n   * be used instead of performing the default fetch. In that case, the\n   * `onFetchResult` and `onFetchError` hooks will not be called.\n   *\n   * @param data - Object containing the URI being fetched\n   * @returns A fetch result to use instead of default fetch, or void/undefined to proceed normally\n   */\n  onFetch?(data: { uri: AtUri }): Awaitable<void | LexResolverFetchResult>\n\n  /**\n   * Hook called after successfully fetching a lexicon document.\n   *\n   * @param data - Object containing the URI, CID, and parsed lexicon document\n   */\n  onFetchResult?(data: {\n    uri: AtUri\n    cid: Cid\n    lexicon: LexiconDocument\n  }): Awaitable<void>\n\n  /**\n   * Hook called when fetching fails.\n   *\n   * @param data - Object containing the URI and error that occurred\n   */\n  onFetchError?(data: { uri: AtUri; err: unknown }): Awaitable<void>\n}\n\n/**\n * Configuration options for the {@link LexResolver}.\n *\n * Extends DID resolver options with lexicon-specific hooks for customizing\n * the resolution process.\n *\n * @see {@link CreateDidResolverOptions} for DID resolver configuration\n */\nexport type LexResolverOptions = CreateDidResolverOptions & {\n  /**\n   * Optional hooks for customizing the resolution process.\n   * See {@link LexResolverHooks} for available callbacks.\n   */\n  hooks?: LexResolverHooks\n}\n\nexport { AtUri, type Cid, NSID }\nexport type { LexiconDocument, ResolveDidOptions }\n\n/**\n * Resolves Lexicon documents from the AT Protocol network.\n *\n * The {@link LexResolver} handles the complete process of resolving a lexicon\n * by NSID:\n * 1. **Authority Resolution**: Looks up the `_lexicon.<authority>` DNS TXT record\n *    to find the DID that controls lexicons for that namespace\n * 2. **DID Resolution**: Resolves the DID document to find the PDS endpoint and\n *    signing key\n * 3. **Record Fetch**: Fetches the lexicon record from the PDS with cryptographic\n *    proof verification\n * 4. **Validation**: Validates the lexicon document structure\n *\n * @example Basic usage - resolve a lexicon by NSID\n * ```typescript\n * import { LexResolver } from '@atproto/lex-resolver'\n *\n * const resolver = new LexResolver({})\n *\n * // Get a lexicon document by its NSID\n * const result = await resolver.get('app.bsky.feed.post')\n * console.log(result.lexicon) // The parsed lexicon document\n * console.log(result.uri)     // AT URI where it was found\n * console.log(result.cid)     // Content identifier for verification\n * ```\n *\n * @example Two-step resolution for more control\n * ```typescript\n * import { LexResolver } from '@atproto/lex-resolver'\n *\n * const resolver = new LexResolver({})\n *\n * // Step 1: Resolve the authority to get the AT URI\n * const uri = await resolver.resolve('app.bsky.feed.post')\n * console.log(uri.toString()) // 'at://did:plc:xxx/com.atproto.lexicon.schema/app.bsky.feed.post'\n *\n * // Step 2: Fetch the lexicon from the URI\n * const result = await resolver.fetch(uri)\n * console.log(result.lexicon)\n * ```\n *\n * @example Using hooks for caching\n * ```typescript\n * import { LexResolver, LexResolverFetchResult } from '@atproto/lex-resolver'\n *\n * const cache = new Map<string, LexResolverFetchResult>()\n *\n * const resolver = new LexResolver({\n *   hooks: {\n *     onFetch({ uri }) {\n *       return cache.get(uri.toString())\n *     },\n *     onFetchResult({ uri, cid, lexicon }) {\n *       cache.set(uri.toString(), { cid, lexicon })\n *     }\n *   }\n * })\n * ```\n *\n * @example Error handling\n * ```typescript\n * import { LexResolver, LexResolverError } from '@atproto/lex-resolver'\n *\n * const resolver = new LexResolver({})\n *\n * try {\n *   const result = await resolver.get('com.example.unknown')\n * } catch (error) {\n *   if (error instanceof LexResolverError) {\n *     console.error(`Failed to resolve ${error.nsid}: ${error.description}`)\n *   }\n * }\n * ```\n */\nexport class LexResolver {\n  protected readonly didResolver: DidResolver<'plc' | 'web'>\n\n  constructor(protected readonly options: LexResolverOptions) {\n    this.didResolver = createDidResolver(options)\n  }\n\n  /**\n   * Gets a lexicon document by its NSID.\n   *\n   * This is the primary method for resolving lexicons. It combines\n   * {@link resolve} and {@link fetch} into a single operation, handling\n   * authority resolution, DID lookup, and record fetching.\n   *\n   * @param nsidStr - The NSID to resolve, either as a string or NSID object\n   * @param options - Optional DID resolution options (e.g., signal for cancellation)\n   * @returns The resolved lexicon result containing URI, CID, and lexicon document\n   * @throws {LexResolverError} If resolution fails at any stage\n   *\n   * @example\n   * ```typescript\n   * // Resolve using string NSID\n   * const result = await resolver.get('app.bsky.feed.post')\n   *\n   * // Resolve using NSID object\n   * import { NSID } from '@atproto/syntax'\n   * const nsid = NSID.from('app.bsky.feed.post')\n   * const result = await resolver.get(nsid)\n   *\n   * // With abort signal for cancellation\n   * const controller = new AbortController()\n   * const result = await resolver.get('app.bsky.feed.post', {\n   *   signal: controller.signal\n   * })\n   * ```\n   */\n  async get(\n    nsidStr: NSID | string,\n    options?: ResolveDidOptions,\n  ): Promise<LexResolverResult> {\n    const uri = await this.resolve(nsidStr)\n    return this.fetch(uri, options)\n  }\n\n  /**\n   * Resolves the authority for an NSID and returns the AT URI for the lexicon.\n   *\n   * This method performs the first stage of lexicon resolution:\n   * 1. Parses the NSID to extract the authority domain\n   * 2. Looks up the `_lexicon.<authority>` DNS TXT record\n   * 3. Extracts the DID from the TXT record (format: `did=<did>`)\n   * 4. Constructs the AT URI for the lexicon record\n   *\n   * Use this when you need the URI without fetching the actual document,\n   * or when you want to implement custom fetching logic.\n   *\n   * @param nsidStr - The NSID to resolve, either as a string or NSID object\n   * @returns The AT URI pointing to the lexicon record\n   * @throws {LexResolverError} If authority resolution fails (e.g., DNS lookup fails)\n   *\n   * @example\n   * ```typescript\n   * // Resolve to get the AT URI\n   * const uri = await resolver.resolve('app.bsky.feed.post')\n   * console.log(uri.toString())\n   * // Output: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/com.atproto.lexicon.schema/app.bsky.feed.post'\n   *\n   * // The URI can then be used with fetch() or stored for later use\n   * const result = await resolver.fetch(uri)\n   * ```\n   */\n  async resolve(nsidStr: NSID | string): Promise<AtUri> {\n    const nsid = NSID.from(nsidStr)\n\n    const did =\n      (await this.options.hooks?.onResolveAuthority?.({ nsid })) ??\n      (await this.resolveLexiconAuthority(nsid).then(\n        async (did) => {\n          await this.options.hooks?.onResolveAuthorityResult?.({ nsid, did })\n          return did\n        },\n        async (err) => {\n          await this.options.hooks?.onResolveAuthorityError?.({ nsid, err })\n          throw err\n        },\n      ))\n\n    return AtUri.make(did, 'com.atproto.lexicon.schema', nsid.toString())\n  }\n\n  // @TODO This class could be made compatible with browsers by making the\n  // following method abstract and/or by allowing the caller to inject a DNS\n  // resolver implementation (based on DNS-over-HTTPS or similar), instead of\n  // using the Node.js built-in resolver.\n  protected async resolveLexiconAuthority(nsid: NSID): Promise<Did> {\n    try {\n      return await getDomainTxtDid(`_lexicon.${nsid.authority}`)\n    } catch (cause) {\n      throw new LexResolverError(\n        nsid,\n        `Failed to resolve lexicon DID authority for ${nsid}`,\n        { cause },\n      )\n    }\n  }\n\n  /**\n   * Fetches a lexicon document from a specific AT URI.\n   *\n   * This method performs the second stage of lexicon resolution:\n   * 1. Resolves the DID from the URI to find the PDS endpoint\n   * 2. Fetches the record from the PDS using `com.atproto.sync.getRecord`\n   * 3. Verifies the cryptographic proof (commit signature)\n   * 4. Validates the lexicon document structure\n   * 5. Ensures the document ID matches the URI rkey\n   *\n   * Use this when you already have an AT URI (e.g., from {@link resolve})\n   * and want to fetch the lexicon document.\n   *\n   * @param uriStr - The AT URI to fetch, either as a string or AtUri object\n   * @param options - Optional DID resolution options (e.g., signal for cancellation, noCache)\n   * @returns The resolved lexicon result containing URI, CID, and lexicon document\n   * @throws {LexResolverError} If fetching or validation fails\n   *\n   * @example\n   * ```typescript\n   * // Fetch from a known URI\n   * const result = await resolver.fetch(\n   *   'at://did:plc:xyz/com.atproto.lexicon.schema/app.bsky.feed.post'\n   * )\n   *\n   * // Fetch with no-cache to bypass any upstream caching\n   * const result = await resolver.fetch(uri, { noCache: true })\n   *\n   * // Fetch with abort signal\n   * const controller = new AbortController()\n   * const result = await resolver.fetch(uri, { signal: controller.signal })\n   * ```\n   */\n  async fetch(\n    uriStr: AtUri | string,\n    options?: ResolveDidOptions,\n  ): Promise<LexResolverResult> {\n    const uri = typeof uriStr === 'string' ? new AtUri(uriStr) : uriStr\n\n    const { lexicon, cid } =\n      (await this.options.hooks?.onFetch?.({ uri })) ??\n      (await this.fetchLexiconUri(uri, options).then(\n        async (res) => {\n          await this.options.hooks?.onFetchResult?.({ uri, ...res })\n          return res\n        },\n        async (err) => {\n          await this.options.hooks?.onFetchError?.({ uri, err })\n          throw err\n        },\n      ))\n\n    return { uri, cid, lexicon }\n  }\n\n  protected async fetchLexiconUri(\n    uri: AtUri,\n    options?: ResolveDidOptions,\n  ): Promise<LexResolverFetchResult> {\n    const { did, nsid } = parseLexiconUri(uri)\n\n    const { pds, key } = await this.didResolver\n      .resolve(did, options)\n      .then(extractAtprotoData)\n      .catch((cause) => {\n        throw new LexResolverError(\n          nsid,\n          `Failed to resolve DID document for ${did}`,\n          { cause },\n        )\n      })\n\n    if (!key || !pds || !URL.canParse(pds.serviceEndpoint)) {\n      throw new LexResolverError(\n        nsid,\n        `No atproto PDS service endpoint or signing key found in ${did} DID document`,\n      )\n    }\n\n    const agent = buildAgent({\n      service: pds.serviceEndpoint,\n      fetch: this.options.fetch,\n    })\n\n    const collection = 'com.atproto.lexicon.schema'\n    const rkey = nsid.toString()\n\n    const { cid, record } = await xrpc(agent, com.atproto.sync.getRecord, {\n      signal: options?.signal,\n      headers: options?.noCache ? { 'Cache-Control': 'no-cache' } : undefined,\n      params: { did, collection, rkey },\n    }).then(\n      ({ body }) => {\n        return verifyRecordProof(body, did, key, collection, rkey).catch(\n          (cause) => {\n            throw new LexResolverError(\n              nsid,\n              `Failed to verify Lexicon record proof at ${uri}`,\n              { cause },\n            )\n          },\n        )\n      },\n      (cause) => {\n        throw new LexResolverError(nsid, `Failed to fetch Record ${uri}`, {\n          cause,\n        })\n      },\n    )\n\n    const validationResult = lexiconDocumentSchema.safeParse(record)\n    if (!validationResult.success) {\n      throw new LexResolverError(nsid, `Invalid Lexicon document at ${uri}`, {\n        cause: validationResult.reason,\n      })\n    }\n\n    const lexicon = validationResult.value\n    if (lexicon.id !== uri.rkey) {\n      throw new LexResolverError(\n        nsid,\n        `Invalid document id \"${lexicon.id}\" at ${uri}`,\n      )\n    }\n\n    return { lexicon, cid }\n  }\n}\n\nfunction parseLexiconUri(uri: AtUri): {\n  did: Did\n  nsid: NSID\n} {\n  // Validate input URI\n  const nsid = NSID.from(uri.rkey)\n  try {\n    const did = uri.host\n    assertDid(did)\n    return { did, nsid }\n  } catch (cause) {\n    throw new LexResolverError(nsid, `URI host is not a DID ${uri}`, { cause })\n  }\n}\n\nasync function getDomainTxtDid(domain: string): Promise<Did> {\n  const didLines = (await resolveTxt(domain))\n    .map((chunks) => chunks.join(''))\n    .filter((i) => i.startsWith('did='))\n\n  if (didLines.length === 1) {\n    const did = didLines[0].slice(4)\n    assertDid(did)\n    return did\n  }\n\n  throw didLines.length > 1\n    ? new Error('Multiple DIDs found in DNS TXT records')\n    : new Error('No DID found in DNS TXT records')\n}\n\nasync function verifyRecordProof(\n  car: Uint8Array,\n  did: Did,\n  key: AtprotoVerificationMethod,\n  collection: NsidString,\n  rkey: string,\n) {\n  const { root, blocks } = await readCarWithRoot(car)\n  const blockstore = new MemoryBlockstore(blocks)\n\n  const commit = await blockstore.readObj(root, repoDef.commit)\n  if (commit.did !== did) {\n    throw new Error(`Invalid repo did: ${commit.did}`)\n  }\n\n  const signingKey = getDidKeyFromMultibase(key)\n  const validSig = await verifyCommitSig(commit, signingKey)\n  if (!validSig) {\n    throw new Error(`Invalid signature on commit: ${root.toString()}`)\n  }\n\n  const mst = MST.load(blockstore, commit.data)\n\n  const cid = await mst.get(`${collection}/${rkey}`)\n  if (!cid) throw new Error('Record not found in proof')\n\n  const record = await blockstore.readRecord(cid)\n  if (record?.$type !== collection) {\n    throw new Error(\n      `Invalid record type: expected ${collection}, got ${record?.$type}`,\n    )\n  }\n\n  return { cid, record }\n}\n\nfunction getDidKeyFromMultibase(key: AtprotoVerificationMethod) {\n  switch (key.type) {\n    case 'EcdsaSecp256r1VerificationKey2019': {\n      const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n      return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n    }\n    case 'EcdsaSecp256k1VerificationKey2019': {\n      const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n      return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n    }\n    case 'Multikey': {\n      const { jwtAlg, keyBytes } = crypto.parseMultikey(key.publicKeyMultibase)\n      return crypto.formatDidKey(jwtAlg, keyBytes)\n    }\n    default: {\n      // Should never happen\n      throw new Error(`Unsupported verification method type: ${key.type}`)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-resolver/tests/index.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\n\n// @TODO Port tests from lexicon-resolver\n\ndescribe('noop', () => {\n  it('does nothing', () => {\n    expect(true).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-resolver/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/node.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-resolver/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-resolver/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-resolver/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/CHANGELOG.md",
    "content": "# @atproto/lex-schema\n\n## 0.0.16\n\n### Patch Changes\n\n- [#4761](https://github.com/bluesky-social/atproto/pull/4761) [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow using a dynamic non-strict validation mode\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-data@0.0.14\n\n## 0.0.15\n\n### Patch Changes\n\n- [#4734](https://github.com/bluesky-social/atproto/pull/4734) [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `IssueInvalidFormat`'s `message` property to `detail`\n\n- [#4734](https://github.com/bluesky-social/atproto/pull/4734) [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add a read-only `message` property to the `Issue` class\n\n- [#4734](https://github.com/bluesky-social/atproto/pull/4734) [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `LexValidationError` class now implements `ResultFailure` allowing it to be used as validation return value directly (without the need to be wrapped)\n\n- [#4734](https://github.com/bluesky-social/atproto/pull/4734) [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update stringification of issues (`toString()`) to consistently display the error path at the end.\n\n- [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export more string format type assertion utilities\n\n- [#4734](https://github.com/bluesky-social/atproto/pull/4734) [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `PropertyKey` is no longer exported. Use the global value instead.\n\n- [#4734](https://github.com/bluesky-social/atproto/pull/4734) [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add [Standard Schema](https://standardschema.dev/) compatibility\n\n- Updated dependencies [[`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f)]:\n  - @atproto/syntax@0.5.1\n\n## 0.0.14\n\n### Patch Changes\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `WWWAuthenticate` to have multiple challenges for the same scheme\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export more `DatetimeString` utilities from `@atproto/syntax`\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `$` utility function bound to the instance, allowing to use them without the schema as `this` context\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ignore empty strings from `params` parsing\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Renaming `ValidationError` to `LexValidationError` for consistency with `LexError` & sub-classes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lex-data@0.0.13\n\n## 0.0.13\n\n### Patch Changes\n\n- [#4654](https://github.com/bluesky-social/atproto/pull/4654) [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Params cannot be arrays of mixed string, number and booleans (items can only be of one type)\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typeing of `string()` schemas\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix import statement\n\n- [#4654](https://github.com/bluesky-social/atproto/pull/4654) [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Schemas now define a `type` property that allows to better discriminate them\n\n- [#4654](https://github.com/bluesky-social/atproto/pull/4654) [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `knownValues` in string schemas\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `MaybeTypedObject` type utility (retrun type of `TypedObjectSchema.isTypeOf` method)\n\n- [#4654](https://github.com/bluesky-social/atproto/pull/4654) [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly support \"enum\" and \"const\" schemas in params\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow using `InferMethod*` type helpers without any argument\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor optimization when checking payload encoding\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error messages when encountering an unexpected legacy blob\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add new `issueUnexpectedType` method on `ValidationContext` to better allow creation issues with multiple allowed types\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update validation message for $typed object to match working of `@atproto/lexicon`\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `UnknownObject` with `LexMap` to improve consistency with underlying data structure being validated\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `TypedRecord` type utility (result of `RecordSchema.isTypeOf` methid\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n\n## 0.0.12\n\n### Patch Changes\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Constrain XRPC `Payload` to be `LexValue` instead of `unknown` (better reflecting reality)\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly infer type of generic `Payload` body\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly coerce params into arrays when defined as such\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Constrain subscription `message` schema to validate `LexValue` only\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-data@0.0.11\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `asX` and `assertX` string format assertion utilities\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Memoize array schemas (without options)\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `default` option from `string`, `integer`, `boolean`, `enum` and `literal` types. These are replace with a new `withDefault()` type wrapper.\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `TypedObject` and `Record`'s `build()` method now performs parsing of the input data (ensuring that defaults are applied).\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `TypedObject` to `Unknown$TypedObject`\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Distinguish \"parse\" and \"validation\" modes when checking against a schema. Validation (`validate()` and `safeValidate()`) only ensures that a value matches the input schema, while parsing (`parse()` and `safeParse()`) will also apply defaults and coerce input values into the expected output type.\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `cidLink()` to `cid()`\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Memoize empty `params` schemas\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performance of string format checking\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix inability to assign (object containing) open union results to `LexMap` type\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add new `Unknown$Type` type to represent records an object's unknown `$type` property (typically from open unions).\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `UnknownObjectOutput` to `UnknownObject`\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fail early when validating nested structures\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/syntax@0.4.3\n  - @atproto/lex-data@0.0.9\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n\n## 0.0.8\n\n### Patch Changes\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add stricter validation rules for `CidSchema`\n\n- [#4512](https://github.com/bluesky-social/atproto/pull/4512) [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `tokenSchema` `value` as public property\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export new `$TypedMaybe` type util\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n\n## 0.0.6\n\n### Patch Changes\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `LexMap` exported type\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `jsonPayload` schema builder utilities\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `lexErrorData` validation schema\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `Main<T>` and `getMain()` helpers to work with namespaced schemas\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Simplify definition of `TypedObject`\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `ValidationError` now extend `LexError` (from `@atproto/data`)\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Memoize most popular schemas\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n\n## 0.0.5\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `cast()` method to Schema classes. This acts as a type cast that does not alter the value or throws if the value does not match the schema.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Forbid use of unsafe integers\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `InferParamsSchema` with `InferMethodParams`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `matchesEncoding` method on the `PayloadSchema` class.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `InferProcedureParameters` with `InferMethodParams`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export format checking utilities for string (`isDidString`, `isCidString`, etc.)\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `$Typed` and `Un$Typed` utilities\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `ResultFailure`'s error field to `reason`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Type the `encoding` field of `Payload` more accurately. Methods with an encoding of `*/*` are now correctly represented as `${string}/${string}` instead of the `*/*` literal type.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `matchesMime` utility method on `BlobSchema` class\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce size and accept options when validating blobs\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `InferProcedureInputBody` with `InferMethodInputBody`\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `check()` method to all Schema classes. That method is an alias for the `assert()` method that allows to avoid `ts(2775)` errors.\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename format assertion and checking utilities to all contain the `String` prefix (like in `asAtUriString`, `assertAtUriString`, etc.)\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `InferProcedureOutputBody` with `InferMethodOutputBody`\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lex-data@0.0.3\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` option to `literal` and `enum` schemas\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework object validation logic to work without `options` argument\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `literal` schemas no longer use the value as \"default\". The \"default\" must now be explicitly provided.\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `l.nullable` for nullable object properties and `l.optional` for optional object properties in lex schemas.\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `l.refine` utility to add custom refinements to existing schemas.\n\n- [#4401](https://github.com/bluesky-social/atproto/pull/4401) [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add test suite for all schema types\n\n- [#4401](https://github.com/bluesky-social/atproto/pull/4401) [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `array` schema's generic to be both a Validator type or an item value type\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Validation issues are now child classed to the `Issue` abstract class instead of simple objects with interfaces. This allows for better extensibility and custom behavior on issues (such as custom error messages).\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace use of `CID` with `Cid`\n\n- [#4401](https://github.com/bluesky-social/atproto/pull/4401) [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Drop `lexiconType` property\n\n- [#4401](https://github.com/bluesky-social/atproto/pull/4401) [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix type of `typedObject` and `record` schemas\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove options (`required`, `nullable`) from `object` schemas. Those are replaced by `l.optional` and `l.nullable` wrappers.\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Discriminated unions now only accept discriminators to be declared a `literal` or `enum` schemas and will throw at initialization if discriminators values aren't strictly disjoined.\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use string formats from `@atproto/syntax`\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `l.regexp` schema builder\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Require `l.discriminatedUnion` discriminator field to be a `literal` or `enum` schema\n\n- [#4401](https://github.com/bluesky-social/atproto/pull/4401) [`a487ab8`](https://github.com/bluesky-social/atproto/commit/a487ab8afe8f18d00662e666049be8d28de2b57e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Simplify `ParamsSchema` interface\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove options (`required`) from `params` schemas. Those are replaced by `l.optional` wrappers.\n\n- [#4387](https://github.com/bluesky-social/atproto/pull/4387) [`9f87ff3`](https://github.com/bluesky-social/atproto/commit/9f87ff3aa60090c8c38b6ce400cba6ceff5cd2e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Simplify `dict` type definition\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename schema methods `validate`, `check` and `maybe` to `safeParse`, `matches` and `ifMatches` respectively.\n\n- [#4397](https://github.com/bluesky-social/atproto/pull/4397) [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `CHANGELOG.md` to npm package\n\n- [#4390](https://github.com/bluesky-social/atproto/pull/4390) [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` option to `const` and `enum` schemas\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4)]:\n  - @atproto/lex-data@0.0.2\n  - @atproto/syntax@0.4.2\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4380](https://github.com/bluesky-social/atproto/pull/4380) [`23c271f`](https://github.com/bluesky-social/atproto/commit/23c271fcac27f090727e2f835697d4733784bdb4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `literal` value as default\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4371](https://github.com/bluesky-social/atproto/pull/4371) [`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Release\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-data@0.0.1\n"
  },
  {
    "path": "packages/lex/lex-schema/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-schema\",\n  \"version\": \"0.0.16\",\n  \"license\": \"MIT\",\n  \"description\": \"Lexicon schema system for AT Lexicons\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"lex\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-schema\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@standard-schema/spec\": \"^1.1.0\",\n    \"iso-datestring-validator\": \"^2.2.2\",\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/$type.test.ts",
    "content": "import { describe, it } from 'vitest'\nimport { LexMap } from '@atproto/lex-data'\nimport { Unknown$TypedObject } from './$type.js'\n\ndescribe('Unknown$TypedObject', () => {\n  it('allows assigning Unknown$TypedObject to LexMap', () => {\n    function expectLexMap(_value: LexMap) {}\n\n    const someObject = {\n      $type: 'some-type',\n    } as Unknown$TypedObject\n\n    expectLexMap(someObject)\n\n    expectLexMap({\n      arr: [someObject],\n      val: someObject,\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/$type.ts",
    "content": "import { NsidString } from './string-format.js'\nimport { OmitKey, Simplify } from './types.js'\n\n/**\n * Constructs the `$type` string type for a given NSID and hash.\n *\n * The `$type` value identifies a schema definition within a lexicon:\n * - For \"main\" definitions: just the NSID (e.g., `'app.bsky.feed.post'`)\n * - For named definitions: NSID + hash + name (e.g., `'app.bsky.feed.defs#postView'`)\n *\n * @typeParam N - The NSID string type\n * @typeParam H - The hash/definition name (use `'main'` for the main definition)\n *\n * @example\n * ```typescript\n * type MainType = $Type<'app.bsky.feed.post', 'main'>\n * // Result: 'app.bsky.feed.post'\n *\n * type DefType = $Type<'app.bsky.feed.defs', 'postView'>\n * // Result: 'app.bsky.feed.defs#postView'\n * ```\n */\nexport type $Type<\n  N extends NsidString = NsidString,\n  H extends string = string,\n> = N extends NsidString\n  ? string extends H\n    ? N | `${N}#${string}`\n    : H extends 'main'\n      ? N\n      : `${N}#${H}`\n  : never\n\n/**\n * Extracts the `$type` string type from an object type.\n *\n * @typeParam O - An object type with an optional `$type` property\n *\n * @example\n * ```typescript\n * type Post = { $type: 'app.bsky.feed.post'; text: string }\n * type PostType = $TypeOf<Post>\n * // Result: 'app.bsky.feed.post'\n * ```\n */\nexport type $TypeOf<O extends { $type?: string }> = NonNullable<O['$type']>\n\n/**\n * Constructs a `$type` string value from an NSID and definition name.\n *\n * For the \"main\" definition, returns just the NSID. For named definitions,\n * returns the NSID followed by `#` and the definition name.\n *\n * @typeParam N - The NSID string type\n * @typeParam H - The definition name type\n * @param nsid - The NSID of the lexicon\n * @param hash - The definition name within the lexicon (use `'main'` for the main definition)\n * @returns The constructed `$type` string\n *\n * @example\n * ```typescript\n * $type('app.bsky.feed.post', 'main')\n * // Returns: 'app.bsky.feed.post'\n *\n * $type('app.bsky.feed.defs', 'postView')\n * // Returns: 'app.bsky.feed.defs#postView'\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function $type<N extends NsidString, H extends string>(\n  nsid: N,\n  hash: H,\n): $Type<N, H> {\n  return (hash === 'main' ? nsid : `${nsid}#${hash}`) as $Type<N, H>\n}\n\n/**\n * Represents an object with a required `$type` property.\n *\n * This type adds a `$type` property to an existing object type, useful for\n * representing typed AT Protocol objects.\n *\n * @typeParam V - The base object type\n * @typeParam T - The `$type` string literal type\n *\n * @example\n * ```typescript\n * type Post = $Typed<{ text: string; createdAt: string }, 'app.bsky.feed.post'>\n * // Result: { $type: 'app.bsky.feed.post'; text: string; createdAt: string }\n * ```\n */\nexport type $Typed<V, T extends string = string> = Simplify<\n  V & {\n    $type: T\n  }\n>\n\n/**\n * Ensures an object has the specified `$type` property.\n *\n * If the object already has the correct `$type`, returns it unchanged.\n * Otherwise, creates a new object with the `$type` property added.\n *\n * @typeParam V - The object type (may already have `$type`)\n * @typeParam T - The expected `$type` string\n * @param value - The object to add `$type` to\n * @param $type - The `$type` value to ensure\n * @returns The object with the `$type` property\n *\n * @example\n * ```typescript\n * const post = $typed({ text: 'hello' }, 'app.bsky.feed.post')\n * // Result: { $type: 'app.bsky.feed.post', text: 'hello' }\n *\n * // If already typed, returns same object\n * const typed = { $type: 'app.bsky.feed.post', text: 'hello' }\n * const same = $typed(typed, 'app.bsky.feed.post')\n * console.log(typed === same) // true\n * ```\n */\nexport function $typed<V extends { $type?: unknown }, T extends string>(\n  value: V,\n  $type: T,\n): $Typed<V, T> {\n  return value.$type === $type ? (value as $Typed<V, T>) : { ...value, $type }\n}\n\n/**\n * Represents an object with an optional `$type` property.\n *\n * This is used for objects that may or may not have type information,\n * such as input parameters that accept both typed and untyped values.\n *\n * @typeParam V - The base object type\n * @typeParam T - The optional `$type` string literal type\n */\nexport type $TypedMaybe<V, T extends string = string> = Simplify<\n  V & {\n    $type?: T\n  }\n>\n\n/**\n * Removes the `$type` property from an object type.\n *\n * Useful for extracting the \"content\" of a typed object without the type marker.\n *\n * @typeParam V - An object type with an optional `$type` property\n *\n * @example\n * ```typescript\n * type Post = { $type: 'app.bsky.feed.post'; text: string }\n * type PostContent = Un$Typed<Post>\n * // Result: { text: string }\n * ```\n */\nexport type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>\n\n/**\n * Unique symbol for branding unknown `$type` strings.\n * @internal\n */\ndeclare const unknown$TypeSymbol: unique symbol\n\n/**\n * Represents an unknown or unrecognized `$type` string.\n *\n * This branded type is used in union types to distinguish between\n * known typed objects and unknown typed objects (from open unions).\n * The branding prevents accidentally matching known `$type` values.\n */\nexport type Unknown$Type = string & { [unknown$TypeSymbol]: true }\n\n/**\n * Represents an object with an unknown `$type` value.\n *\n * This type is used in open union schemas to represent typed objects that\n * don't match any of the known types. The {@link Unknown$Type} branding ensures\n * that invalid instances of known types don't accidentally match this type.\n *\n * For example, in an open union like:\n * ```typescript\n * type MyOpenUnion = { $type: 'A'; a: number } | Unknown$TypedObject\n * ```\n *\n * A value `{ $type: 'A' }` (missing the required `a` property) will NOT match\n * `Unknown$TypedObject` because `'A'` is not assignable to `Unknown$Type`.\n * This ensures that malformed instances of known types are properly rejected.\n *\n * @example\n * ```typescript\n * // This represents any typed object we don't recognize\n * const unknownTyped: Unknown$TypedObject = {\n *   $type: 'some.unknown.type' as Unknown$Type,\n *   // ... arbitrary properties\n * }\n * ```\n */\nexport type Unknown$TypedObject = { $type: Unknown$Type }\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/record-key.ts",
    "content": "import { isValidRecordKey } from '@atproto/syntax'\n\n/**\n * The valid record key constraint types in a lexicon definition.\n *\n * - `'any'` - Accepts any valid record key\n * - `'nsid'` - Record key must be a valid NSID\n * - `'tid'` - Record key must be a valid TID\n * - `'literal:...'` - Record key must be the exact specified value\n *\n * @example\n * ```typescript\n * const constraint: LexiconRecordKey = 'tid'\n * const literalConstraint: LexiconRecordKey = 'literal:self'\n * ```\n */\nexport type LexiconRecordKey = 'any' | 'nsid' | 'tid' | `literal:${string}`\n\n/**\n * Type guard that checks if a value is a valid lexicon record key constraint.\n *\n * @typeParam T - The input type\n * @param key - The value to check\n * @returns `true` if the value is a valid record key constraint\n *\n * @example\n * ```typescript\n * if (isLexiconRecordKey(value)) {\n *   // value is typed as LexiconRecordKey\n *   console.log('Valid constraint:', value)\n * }\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function isLexiconRecordKey<T>(key: T): key is T & LexiconRecordKey {\n  return (\n    key === 'any' ||\n    key === 'nsid' ||\n    key === 'tid' ||\n    (typeof key === 'string' &&\n      key.startsWith('literal:') &&\n      key.length > 8 &&\n      isValidRecordKey(key.slice(8)))\n  )\n}\n\n/**\n * Validates and returns a value as a lexicon record key constraint, throwing if invalid.\n *\n * @param key - The value to validate\n * @returns The value typed as {@link LexiconRecordKey}\n * @throws {Error} If the value is not a valid record key constraint\n *\n * @example\n * ```typescript\n * const constraint = asLexiconRecordKey('tid')\n * // constraint is typed as LexiconRecordKey\n *\n * asLexiconRecordKey('invalid') // throws Error\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function asLexiconRecordKey(key: unknown): LexiconRecordKey {\n  if (isLexiconRecordKey(key)) return key\n  throw new Error(`Invalid record key: ${String(key)}`)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/result.ts",
    "content": "export type ResultSuccess<V = any> = { success: true; value: V }\n\n/**\n * Represents a failed result containing an error reason.\n *\n * @typeParam E - The type of the error reason\n */\nexport type ResultFailure<E = Error> = { success: false; reason: E }\n\n/**\n * A discriminated union type representing either a success or failure outcome.\n *\n * Check the `success` property to determine the outcome and access the\n * appropriate property (`value` for success, `reason` for failure).\n *\n * @typeParam V - The type of the success value\n * @typeParam E - The type of the error reason\n *\n * @example\n * ```typescript\n * function parseJson(text: string): Result<unknown, SyntaxError> {\n *   try {\n *     return success(JSON.parse(text))\n *   } catch (e) {\n *     return failure(e as SyntaxError)\n *   }\n * }\n * ```\n */\nexport type Result<V = any, E = Error> = ResultSuccess<V> | ResultFailure<E>\n\n/**\n * Creates a successful result wrapping the given value.\n *\n * @typeParam V - The type of the value\n * @param value - The success value to wrap\n * @returns {ResultSuccess} A success result containing the value\n *\n * @example\n * ```typescript\n * const result = success(42)\n * console.log(result.success) // true\n * console.log(result.value)   // 42\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function success<V>(value: V): ResultSuccess<V> {\n  return { success: true, value }\n}\n\n/**\n * Creates a failed result wrapping the given error reason.\n *\n * @typeParam E - The type of the error reason\n * @param reason - The error reason to wrap\n * @returns {ResultFailure} A failure result containing the error\n *\n * @example\n * ```typescript\n * const result = failure(new Error('Something went wrong'))\n * console.log(result.success) // false\n * console.log(result.reason.message) // \"Something went wrong\"\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function failure<E>(reason: E): ResultFailure<E> {\n  return { success: false, reason }\n}\n\n/**\n * Extracts the error reason from a failure result.\n *\n * @typeParam T - The type of the error reason\n * @param result - A failure result\n * @returns {T} The error reason\n *\n * @example\n * ```typescript\n * const result = failure(new Error('oops'))\n * const error = failureReason(result)\n * console.log(error.message) // \"oops\"\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function failureReason<T>(result: ResultFailure<T>): T {\n  return result.reason\n}\n\n/**\n * Extracts the value from a success result.\n *\n * @typeParam T - The type of the success value\n * @param result - A success result\n * @returns {T} The success value\n *\n * @example\n * ```typescript\n * const result = success(42)\n * const value = successValue(result)\n * console.log(value) // 42\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function successValue<T>(result: ResultSuccess<T>): T {\n  return result.value\n}\n\n/**\n * Catches any error and wraps it in a {@link ResultFailure<Error>}.\n *\n * @param err - The error to catch.\n * @returns {ResultFailure} A failure result containing the error.\n * @example\n *\n * ```ts\n * declare function someFunction(): Promise<string>\n *\n * const result = await someFunction().then(success, catchall)\n * if (result.success) {\n *   console.log(result.value) // string\n * } else {\n *   console.error(result.reason instanceof Error) // true\n *   console.error(result.reason.message) // string\n * }\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function catchall(err: unknown): ResultFailure<Error> {\n  if (err instanceof Error) return failure(err)\n  return failure(new Error('Unknown error', { cause: err }))\n}\n\n/**\n * Creates a catcher function for the given constructor that wraps caught errors\n * in a {@link ResultFailure}.\n *\n * @example\n *\n * ```ts\n * class FooError extends Error {}\n * class BarError extends Error {}\n *\n * declare function someFunction(): Promise<string>\n *\n * const result = await someFunction()\n *   .then(success)\n *   .catch(createCatcher(FooError))\n *   .catch(createCatcher(BarError))\n *\n * if (result.success) {\n *   console.log(result.value) // string\n * } else {\n *   console.error(result.reason) // FooError | BarError\n * }\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function createCatcher<T>(Ctor: new (...args: any[]) => T) {\n  return (err: unknown): ResultFailure<T> => {\n    if (err instanceof Ctor) return failure(err)\n    throw err\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/schema.ts",
    "content": "import { StandardSchemaV1 } from '@standard-schema/spec'\nimport { lazyProperty } from '../util/lazy-property.js'\nimport { StandardSchemaAdapter } from './standard-schema.js'\nimport {\n  InferInput,\n  InferOutput,\n  ValidationContext,\n  ValidationOptions,\n  ValidationResult,\n  Validator,\n} from './validator.js'\n\n/**\n * Options for parsing operations.\n * Excludes the `mode` option as it is implicitly set to `\"parse\"`.\n */\nexport type ParseOptions = Omit<ValidationOptions, 'mode'>\n\n/**\n * Options for validation operations.\n * Excludes the `mode` option as it is implicitly set to `\"validate\"`.\n */\nexport type ValidateOptions = Omit<ValidationOptions, 'mode'>\n\n/**\n * Internal type structure for schema type inference.\n *\n * This interface defines the phantom types used for compile-time type inference\n * without affecting runtime behavior. The `input` and `output` properties\n * represent the expected input type during validation and the resulting output\n * type after parsing, respectively.\n *\n * @typeParam TInput - The type accepted as input during validation\n * @typeParam TOutput - The type returned after parsing (may differ from input due to coercion)\n */\nexport interface SchemaInternals<out TInput = unknown, out TOutput = TInput> {\n  input: TInput\n  output: TOutput\n}\n\n/**\n * Abstract base class for all schema validators in the lexicon system.\n *\n * This class provides the standard validation interface that all schema types\n * implement. It offers multiple methods for validating and parsing data:\n *\n * - **Assertion methods**: `assert()`, `check()` - throw on invalid input\n * - **Type guard methods**: `matches()`, `ifMatches()` - return boolean or optional value\n * - **Parse methods**: `parse()`, `safeParse()` - allow value transformation/coercion\n * - **Validate methods**: `validate()`, `safeValidate()` - validation without coercion\n *\n * All methods are also available with a `$` prefix (e.g., `$parse()`, `$validate()`)\n * for consistent access in generated lexicon namespaces.\n *\n * @typeParam TInput - The type accepted as valid input during validation\n * @typeParam TOutput - The type returned after parsing (may include transformations)\n *\n * @example\n * ```typescript\n * class MySchema extends Schema<string> {\n *   validateInContext(input: unknown, ctx: ValidationContext): ValidationResult {\n *     if (typeof input !== 'string') {\n *       return ctx.issueUnexpectedType(input, 'string')\n *     }\n *     return ctx.success(input)\n *   }\n * }\n *\n * const schema = new MySchema()\n * schema.assert('hello')     // OK\n * schema.assert(123)         // Throws LexValidationError\n * schema.matches('hello')    // true\n * schema.matches(123)        // false\n * ```\n */\nexport abstract class Schema<out TInput = unknown, out TOutput = TInput>\n  implements Validator<TInput, TOutput>, StandardSchemaV1<TInput, TOutput>\n{\n  /**\n   * Internal phantom property for type inference.\n   * This property does not exist at runtime.\n   *\n   * @internal\n   */\n  declare readonly ['__lex']: SchemaInternals<TInput, TOutput>\n\n  get '~standard'(): StandardSchemaV1.Props<TInput, TOutput> {\n    // Lazily create, and cache, the Standard Schema adapter for this schema\n    // instance.\n    return lazyProperty(this, '~standard', new StandardSchemaAdapter(this))\n  }\n\n  // Needed to discriminate multiple schema types when used in unions. Without\n  // this, Typescript could allow an EnumSchema<\"foo\" | \"bar\"> to be used where\n  // a StringSchema is expected, since they would both be structurally\n  // compatible.\n  abstract readonly type: string\n\n  /**\n   * Performs validation of the input value within a validation context.\n   *\n   * This method must be implemented by subclasses to define the actual\n   * validation logic. It should not be called directly; use\n   * {@link ValidationContext.validate} instead to ensure proper mode enforcement.\n   *\n   * @param input - The value to validate\n   * @param ctx - The validation context providing path tracking and issue reporting\n   * @returns A validation result indicating success with the validated value or failure with issues\n   *\n   * @internal\n   */\n  abstract validateInContext(\n    input: unknown,\n    ctx: ValidationContext,\n  ): ValidationResult\n\n  /**\n   * @note use {@link check}() instead of {@link assert}() if you encounter a\n   * `ts(2775)` error and you are not able to fully type the validator. This\n   * will typically arise in generic contexts, where the narrowed type is not\n   * needed.\n   */\n  assert(input: unknown): asserts input is InferInput<this> {\n    const result = ValidationContext.validate(input, this)\n    if (!result.success) throw result.reason\n  }\n\n  /**\n   * Alias for {@link assert}(). Most useful in generic contexts where the\n   * validator is not exactly typed, allowing to avoid \"_Assertions require\n   * every name in the call target to be declared with an explicit type\n   * annotation. ts(2775)_\" errors.\n   */\n  check(input: unknown): void {\n    this.assert(input)\n  }\n\n  /**\n   * Casts the input (by validating it) to the output type if it matches the\n   * schema, otherwise throws. This is the same as calling {@link parse}() with\n   * `mode: \"validate\"`.\n   */\n  cast<I>(input: I): I & InferInput<this> {\n    const result = ValidationContext.validate(input, this)\n    if (result.success) return result.value\n    throw result.reason\n  }\n\n  /**\n   * Type guard that checks if the input matches this schema.\n   *\n   * @param input - The value to check\n   * @returns `true` if the input is valid according to this schema\n   *\n   * @example\n   * ```typescript\n   * if (schema.matches(data)) {\n   *   // data is narrowed to the schema's input type\n   *   console.log(data)\n   * }\n   * ```\n   */\n  matches<I>(input: I): input is I & InferInput<this> {\n    const result = ValidationContext.validate(input, this)\n    return result.success\n  }\n\n  /**\n   * Returns the input if it matches this schema, otherwise returns `undefined`.\n   *\n   * This is useful for optional filtering operations where you want to\n   * conditionally extract values that match a schema.\n   *\n   * @param input - The value to check\n   * @returns The input value with narrowed type if valid, otherwise `undefined`\n   *\n   * @example\n   * ```typescript\n   * const validData = schema.ifMatches(data)\n   * if (validData !== undefined) {\n   *   // validData is the schema's input type\n   *   console.log(validData)\n   * }\n   * ```\n   */\n  ifMatches<I>(input: I): (I & InferInput<this>) | undefined {\n    return this.matches(input) ? input : undefined\n  }\n\n  /**\n   * Parses the input, allowing value transformations and coercion.\n   *\n   * Unlike {@link validate}, this method allows the schema to transform\n   * the input value (e.g., applying default values, type coercion).\n   * Throws a {@link LexValidationError} if the input is invalid.\n   *\n   * @param input - The value to parse\n   * @param options - Optional parsing configuration\n   * @returns The parsed and potentially transformed value\n   * @throws {LexValidationError} If the input fails validation\n   *\n   * @example\n   * ```typescript\n   * const result = schema.parse(rawData)\n   * // result has defaults applied and is fully typed\n   * ```\n   */\n  parse(input: unknown, options?: ParseOptions): InferOutput<this> {\n    const result = this.safeParse(input, options)\n    if (result.success) return result.value\n    throw result.reason\n  }\n\n  /**\n   * Safely parses the input without throwing, returning a result object.\n   *\n   * This method allows value transformations like {@link parse}, but\n   * returns a discriminated union result instead of throwing on error.\n   *\n   * @param input - The value to parse\n   * @param options - Optional parsing configuration\n   * @returns A {@link ValidationResult} with either the parsed value or validation errors\n   *\n   * @example\n   * ```typescript\n   * const result = schema.safeParse(data)\n   * if (result.success) {\n   *   console.log(result.value)\n   * } else {\n   *   console.error(result.reason.issues)\n   * }\n   * ```\n   */\n  safeParse(\n    input: unknown,\n    options?: ParseOptions,\n  ): ValidationResult<InferOutput<this>> {\n    return ValidationContext.validate(input, this, {\n      ...options,\n      mode: 'parse',\n    })\n  }\n\n  /**\n   * Validates the input strictly without allowing transformations.\n   *\n   * Unlike {@link parse}, this method requires the input to exactly match\n   * the schema without any transformations (no defaults applied, no coercion).\n   * Throws a {@link LexValidationError} if the input is invalid or would require transformation.\n   *\n   * @typeParam I - The input type (preserved in the return type)\n   * @param input - The value to validate\n   * @param options - Optional validation configuration\n   * @returns The validated input with narrowed type\n   * @throws {LexValidationError} If the input fails validation or requires transformation\n   *\n   * @example\n   * ```typescript\n   * const validated = schema.validate(data)\n   * // validated is typed as the intersection of input type and schema type\n   * ```\n   */\n  validate<I>(input: I, options?: ValidateOptions): I & InferInput<this> {\n    const result = this.safeValidate(input, options)\n    if (result.success) return result.value\n    throw result.reason\n  }\n\n  /**\n   * Safely validates the input without throwing, returning a result object.\n   *\n   * This method performs strict validation like {@link validate}, but\n   * returns a discriminated union result instead of throwing on error.\n   *\n   * @typeParam I - The input type (preserved in the result value type)\n   * @param input - The value to validate\n   * @param options - Optional validation configuration\n   * @returns A {@link ValidationResult} with either the validated value or validation errors\n   *\n   * @example\n   * ```typescript\n   * const result = schema.safeValidate(data)\n   * if (result.success) {\n   *   console.log(result.value)\n   * } else {\n   *   console.error(result.reason.issues)\n   * }\n   * ```\n   */\n  safeValidate<I>(\n    input: I,\n    options?: ValidateOptions,\n  ): ValidationResult<I & InferInput<this>> {\n    return ValidationContext.validate(input, this, {\n      ...options,\n      mode: 'validate',\n    })\n  }\n\n  // @NOTE Dollar-prefixed aliases\n  //\n  // The `lex-builder` lib generates namespaced utility functions that allow\n  // accessing the schema's methods without the need to specify the \".main.\"\n  // part of the namespace. This allows utilities for a particular record type\n  // to be called like \"app.bsky.feed.post.<utility>()\" instead of\n  // \"app.bsky.feed.post.main.<utility>()\".\n  //\n  // Because those utilities could conflict with other schemas (e.g. if there is\n  // a lexicon definition with the same name as the \"<utility>\"), those exported\n  // utilities will be prefixed with \"$\".\n  //\n  // Similarly, since those utilities are defined as simple \"const\", they are\n  // also bound (using JS's .bind) to the schema instance, so that they can be\n  // used without worrying about the context (e.g. \"app.bsky.feed.post.$parse()\"\n  // will work regardless of how it is imported or called).\n  //\n  // In order to provide the same functionalities for non-main definitions, we\n  // also define those aliases directly on the schema instance, so that they can\n  // be used in the same way as the utilities generated by \"lex-builder\". For\n  // example, if there is a non-main definition \"app.bsky.feed.defs.postView\",\n  // it will also be possible to call \"app.bsky.feed.defs.postView.$parse()\".\n  //\n  // These methods are also \"bound\" to the instance so that they can be used\n  // exactly like the utilities generated by \"lex-builder\", without worrying\n  // about the context.\n  //\n  // There are two ways we could \"bind\" those methods to the instance:\n  // 1. Define them as getters that return the bound method (e.g. get $parse() {\n  //    return this.parse.bind(this) })\n  // 2. Define them as properties that are initialized in the constructor (e.g.\n  //    this.$parse = this.parse.bind(this))\n  //\n  // Since a **lot** of those methods would end-up being created in systems that\n  // contains many schemas (e.g. the appview), we choose the first approach\n  // (getters) in order to avoid the overhead of creating all those bound\n  // functions upfront when instantiating the schemas.\n\n  /**\n   * Bound alias for {@link assert} for compatibility with generated utilities.\n   * @see {@link assert}\n   */\n  get $assert(): typeof this.assert {\n    return lazyProperty(this, '$assert', this.assert.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link check} for compatibility with generated utilities.\n   * @see {@link check}\n   */\n  get $check(): typeof this.check {\n    return lazyProperty(this, '$check', this.check.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link cast} for compatibility with generated utilities.\n   * @see {@link cast}\n   */\n  get $cast(): typeof this.cast {\n    return lazyProperty(this, '$cast', this.cast.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link matches} for compatibility with generated utilities.\n   * @see {@link matches}\n   */\n  get $matches(): typeof this.matches {\n    return lazyProperty(this, '$matches', this.matches.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link ifMatches} for compatibility with generated utilities.\n   * @see {@link ifMatches}\n   */\n  get $ifMatches(): typeof this.ifMatches {\n    return lazyProperty(this, '$ifMatches', this.ifMatches.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link parse} for compatibility with generated utilities.\n   * @see {@link parse}\n   */\n  get $parse(): typeof this.parse {\n    return lazyProperty(this, '$parse', this.parse.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link safeParse} for compatibility with generated utilities.\n   * @see {@link safeParse}\n   */\n  get $safeParse(): typeof this.safeParse {\n    return lazyProperty(this, '$safeParse', this.safeParse.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link validate} for compatibility with generated utilities.\n   * @see {@link validate}\n   */\n  get $validate(): typeof this.validate {\n    return lazyProperty(this, '$validate', this.validate.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link safeValidate} for compatibility with generated utilities.\n   * @see {@link safeValidate}\n   */\n  get $safeValidate(): typeof this.safeValidate {\n    return lazyProperty(this, '$safeValidate', this.safeValidate.bind(this))\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/standard-schema.test.ts",
    "content": "import { assert, describe, expect, it } from 'vitest'\nimport { array } from '../schema/array.js'\nimport { integer } from '../schema/integer.js'\nimport { object } from '../schema/object.js'\nimport { optional } from '../schema/optional.js'\nimport { string } from '../schema/string.js'\nimport { withDefault } from '../schema/with-default.js'\nimport { LexValidationError } from './validation-error.js'\n\ndescribe('StandardSchemaAdapter', () => {\n  describe('metadata', () => {\n    const schema = integer()\n\n    it('has version 1', () => {\n      expect(schema['~standard'].version).toBe(1)\n    })\n\n    it('has vendor @atproto/lex-schema', () => {\n      expect(schema['~standard'].vendor).toBe('@atproto/lex-schema')\n    })\n  })\n\n  describe('lazy caching', () => {\n    it('returns the same adapter instance on repeated accesses', () => {\n      const schema = integer()\n      const first = schema['~standard']\n      const second = schema['~standard']\n      expect(first).toBe(second)\n    })\n  })\n\n  describe('validate() result shape on success', () => {\n    it('returns a value property for a valid integer', () => {\n      const result = integer()['~standard'].validate(42)\n      expect(result).toMatchObject({ value: 42 })\n    })\n\n    it('returns a value property for a valid string', () => {\n      const result = string()['~standard'].validate('hello')\n      expect(result).toMatchObject({ value: 'hello' })\n    })\n\n    it('does not include an issues property on success', () => {\n      const result = integer()['~standard'].validate(1)\n      expect(result).not.toHaveProperty('issues')\n    })\n  })\n\n  describe('validate() result shape on failure', () => {\n    it('returns a LexValidationError with issues for an invalid value', () => {\n      const result = integer()['~standard'].validate('not-a-number')\n      assert(result instanceof LexValidationError)\n      expect(Array.isArray(result.issues)).toBe(true)\n      expect(result.issues.length).toBeGreaterThan(0)\n    })\n\n    it('does not include a value property on failure', () => {\n      const result = integer()['~standard'].validate('not-a-number')\n      expect(result).not.toHaveProperty('value')\n    })\n\n    describe('issues[].message', () => {\n      it('is a non-empty string', () => {\n        const result = integer()['~standard'].validate('not-a-number')\n        assert(result instanceof LexValidationError)\n        for (const issue of result.issues) {\n          expect(typeof issue.message).toBe('string')\n          expect(issue.message.length).toBeGreaterThan(0)\n        }\n      })\n\n      it('describes the type mismatch', () => {\n        const result = integer()['~standard'].validate('not-a-number')\n        assert(result instanceof LexValidationError)\n        expect(result.issues[0].message).toContain('integer')\n      })\n    })\n\n    describe('issues[].path', () => {\n      it('is an empty array for a root-level failure', () => {\n        const result = integer()['~standard'].validate('not-a-number')\n        assert(result instanceof LexValidationError)\n        expect(result.issues[0].path).toEqual([])\n      })\n\n      it('reflects the property key for a nested object failure', () => {\n        const schema = object({ age: integer() })\n        const result = schema['~standard'].validate({ age: 'not-a-number' })\n        assert(result instanceof LexValidationError)\n        expect(result.issues[0].path).toContain('age')\n      })\n\n      it('reflects the index for an array element failure', () => {\n        const schema = array(integer())\n        const result = schema['~standard'].validate([1, 'bad', 3])\n        assert(result instanceof LexValidationError)\n        expect(result.issues[0].path).toContain(1)\n      })\n    })\n  })\n\n  describe('parse mode (default value application)', () => {\n    it('applies default values when input is undefined', () => {\n      const schema = withDefault(integer(), 10)\n      const result = schema['~standard'].validate(undefined)\n      expect(result).toMatchObject({ value: 10 })\n    })\n\n    it('uses the provided value instead of default when input is present', () => {\n      const schema = withDefault(integer(), 10)\n      const result = schema['~standard'].validate(42)\n      expect(result).toMatchObject({ value: 42 })\n    })\n\n    it('applies defaults for optional object properties in parse mode', () => {\n      const schema = object({\n        name: string(),\n        count: optional(withDefault(integer(), 0)),\n      })\n      const result = schema['~standard'].validate({ name: 'Alice' })\n      expect(result).toMatchObject({ value: { name: 'Alice', count: 0 } })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/standard-schema.ts",
    "content": "import { StandardSchemaV1 } from '@standard-schema/spec'\nimport { ValidationContext, Validator } from './validator.js'\n\n/**\n * The Standard Schema adapter for {@link Validator} instances.\n */\nexport class StandardSchemaAdapter<TInput, TOutput>\n  implements StandardSchemaV1.Props<TInput, TOutput>\n{\n  readonly version = 1\n\n  readonly vendor = '@atproto/lex-schema'\n\n  declare readonly types: StandardSchemaV1.Types<TInput, TOutput>\n\n  constructor(private readonly validator: Validator<TInput, TOutput>) {}\n\n  validate(\n    value: unknown,\n    options?: StandardSchemaV1.Options,\n  ): StandardSchemaV1.Result<TOutput> {\n    // Perform validation in \"parse\" mode to ensure transformations (defaults,\n    // coercions, etc.) are applied. Also ensures that the output type is\n    // returned. Note that ValidationResult is compatible with\n    // StandardSchemaV1.Result :-)\n    return ValidationContext.validate(value, this.validator, {\n      ...options?.libraryOptions,\n      mode: 'parse',\n    })\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/string-format.ts",
    "content": "import { isValidISODateString } from 'iso-datestring-validator'\nimport { validateCidString } from '@atproto/lex-data'\nimport {\n  AtIdentifierString,\n  AtUriString,\n  DatetimeString,\n  DidString,\n  HandleString,\n  NsidString,\n  RecordKeyString,\n  TidString,\n  UriString,\n  isAtIdentifierString,\n  isDatetimeString,\n  isValidAtUri,\n  isValidDid,\n  isValidHandle,\n  isValidLanguage,\n  isValidNsid,\n  isValidRecordKey,\n  isValidTid,\n  isValidUri,\n} from '@atproto/syntax'\nimport { CheckFn } from '../util/assertion-util.js'\n\n// -----------------------------------------------------------------------------\n// Individual string format types and type guards\n// -----------------------------------------------------------------------------\n\n// Re-exporting from @atproto/syntax without modification to preserve types and\n// documentation for types and utilities that are already well-defined there.\n// @TODO rework other string formats in @atproto/syntax to follow this pattern\n// and re-export here, e.g. language tags, NSIDs, record keys, etc.\n\nexport {\n  type AtIdentifierString,\n  asAtIdentifierString,\n  ifAtIdentifierString,\n  isAtIdentifierString,\n} from '@atproto/syntax'\n\n// AtIdentifierString utilities\nexport { isDidIdentifier, isHandleIdentifier } from '@atproto/syntax'\n\nexport {\n  type DatetimeString,\n  asDatetimeString,\n  ifDatetimeString,\n  isDatetimeString,\n} from '@atproto/syntax'\n\n/**\n * Matches any ISO-ish datetime string. This is a more lenient check than\n * the strict {@link isDatetimeString} guard, which only allows datetimes that\n * fully conform to the AT Protocol specification (e.g. must include timezone).\n */\nexport function isDatetimeStringLoose<I>(\n  input: I,\n): input is I & DatetimeString {\n  // @NOTE the returned type assertion is inaccurate wrt. the DatetimeString\n  // type definition. A more accurate solution would be to use a branded type\n  // instead of a template literal for the \"datetime\" format\n  if (typeof input !== 'string') return false\n  try {\n    return isValidISODateString(input)\n  } catch {\n    // @NOTE isValidISODateString throws on some inputs\n    return false\n  }\n}\n\n// DatetimeString utilities\nexport { currentDatetimeString, toDatetimeString } from '@atproto/syntax'\n\n/**\n * Type guard that checks if a value is a valid AT URI.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid AT URI\n */\nexport const isAtUriString: CheckFn<AtUriString> = isValidAtUri\nexport type {\n  /**\n   * An AT URI string pointing to a resource in the AT Protocol network.\n   *\n   * @example `\"at://did:plc:1234.../app.bsky.feed.post/3k2...\"`\n   */\n  AtUriString,\n}\n\n/**\n * Type guard that checks if a value is a valid CID string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid CID string\n */\nexport const isCidString = ((v) => validateCidString(v)) as CheckFn<CidString>\n/**\n * A Content Identifier (CID) string.\n *\n * CIDs are self-describing content addresses used to identify data by its hash.\n *\n * @example `\"bafyreig...\"`\n */\nexport type CidString = string\n\n/**\n * Type guard that checks if a value is a valid DID string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid DID string\n */\nexport const isDidString: CheckFn<DidString> = isValidDid\nexport type {\n  /**\n   * A Decentralized Identifier (DID) string.\n   *\n   * DIDs are globally unique identifiers that don't require a central authority.\n   *\n   * @example `\"did:plc:1234abcd...\"` or `\"did:web:example.com\"`\n   */\n  DidString,\n}\n\n/**\n * Type guard that checks if a value is a valid handle string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid handle string\n */\nexport const isHandleString: CheckFn<HandleString> = isValidHandle\nexport type {\n  /**\n   * A handle string - a human-readable identifier for users.\n   *\n   * @example `\"alice.bsky.social\"` or `\"bob.example.com\"`\n   */\n  HandleString,\n}\n\n/**\n * Type guard that checks if a value is a valid BCP-47 language tag.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid language string\n */\nexport const isLanguageString = isValidLanguage as CheckFn<LanguageString>\n/**\n * A BCP-47 language tag string.\n *\n * @example `\"en\"`, `\"en-US\"`, `\"zh-Hans\"`\n */\nexport type LanguageString = string\n\n/**\n * Type guard that checks if a value is a valid NSID string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid NSID string\n */\nexport const isNsidString: CheckFn<NsidString> = isValidNsid\nexport type {\n  /**\n   * A Namespaced Identifier (NSID) string identifying a lexicon.\n   *\n   * NSIDs use reverse-domain notation to identify schemas.\n   *\n   * @example `\"app.bsky.feed.post\"`, `\"com.atproto.repo.createRecord\"`\n   */\n  NsidString,\n}\n\n/**\n * Type guard that checks if a value is a valid record key string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid record key string\n */\nexport const isRecordKeyString: CheckFn<RecordKeyString> = isValidRecordKey\nexport type {\n  /**\n   * A record key string identifying a record within a collection.\n   *\n   * @example `\"3k2...\"` (TID format) or `\"self\"` (literal key)\n   */\n  RecordKeyString,\n}\n\n/**\n * Type guard that checks if a value is a valid TID string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid TID string\n */\nexport const isTidString: CheckFn<TidString> = isValidTid\nexport type {\n  /**\n   * A Timestamp Identifier (TID) string.\n   *\n   * TIDs are time-based identifiers used for record keys.\n   *\n   * @example `\"3k2...\"`\n   */\n  TidString,\n}\n\n/**\n * Type guard that checks if a value is a valid URI string.\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid URI string\n */\nexport const isUriString: CheckFn<UriString> = isValidUri\nexport type {\n  /**\n   * A standard URI string.\n   *\n   * @example `\"https://example.com/path\"`\n   */\n  UriString,\n}\n\n// -----------------------------------------------------------------------------\n// String format registry\n// -----------------------------------------------------------------------------\n\ntype StringFormats = {\n  'at-identifier': AtIdentifierString\n  'at-uri': AtUriString\n  cid: CidString\n  datetime: DatetimeString\n  did: DidString\n  handle: HandleString\n  language: LanguageString\n  nsid: NsidString\n  'record-key': RecordKeyString\n  tid: TidString\n  uri: UriString\n}\n\n/**\n * Union type of all valid string format names.\n */\nexport type StringFormat = Extract<keyof StringFormats, string>\n\nconst stringFormatVerifiers: {\n  readonly [K in StringFormat]: readonly [\n    strict: CheckFn<StringFormats[K]>,\n    loose?: CheckFn<StringFormats[K]>,\n  ]\n} = /*#__PURE__*/ Object.freeze({\n  __proto__: null,\n\n  'at-identifier': [isAtIdentifierString],\n  'at-uri': [isAtUriString],\n  cid: [isCidString],\n  datetime: [isDatetimeString, isDatetimeStringLoose],\n  did: [isDidString],\n  handle: [isHandleString],\n  language: [isLanguageString],\n  nsid: [isNsidString],\n  'record-key': [isRecordKeyString],\n  tid: [isTidString],\n  uri: [isUriString],\n})\n\nexport type StringFormatValidationOptions = {\n  /**\n   * Allows to be more lenient in validation by using a \"loose\" verification\n   * function, if available. The behavior of the loose verifier depends on the\n   * specific format, but generally it may allow for a wider range of valid\n   * inputs, including values that are not compliant with the AT Protocol\n   * specification.\n   *\n   * @default true\n   */\n  strict?: boolean\n}\n\n/**\n * Infers the string type for a given format name.\n *\n * @typeParam F - The format name\n *\n * @example\n * ```typescript\n * type Did = InferStringFormat<'did'>\n * // Result: DidString\n * ```\n */\nexport type InferStringFormat<F extends StringFormat> = F extends StringFormat\n  ? StringFormats[F]\n  : never\n\n/**\n * Type guard that checks if a string matches a specific format.\n *\n * @typeParam I - The input string type\n * @typeParam F - The format to check\n * @param input - The string to validate\n * @param format - The format name to validate against\n * @returns `true` if the string matches the format\n *\n * @example\n * ```typescript\n * const value: string = 'did:plc:1234...'\n * if (isStringFormat(value, 'did')) {\n *   // value is typed as DidString\n *   console.log('Valid DID:', value)\n * }\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function isStringFormat<I extends string, F extends StringFormat>(\n  input: I,\n  format: F,\n  options?: StringFormatValidationOptions,\n): input is I & StringFormats[F] {\n  const formatVerifier = stringFormatVerifiers[format]\n  // Fool-proof\n  if (!formatVerifier) throw new TypeError(`Unknown string format: ${format}`)\n\n  const check: CheckFn<StringFormats[F]> =\n    options?.strict === false\n      ? formatVerifier[1] ?? formatVerifier[0]\n      : formatVerifier[0]\n\n  return check(input)\n}\n\n/**\n * Asserts that a string matches a specific format, throwing if invalid.\n *\n * @typeParam I - The input string type\n * @typeParam F - The format to check\n * @param input - The string to validate\n * @param format - The format name to validate against\n * @throws {TypeError} If the string doesn't match the format\n *\n * @example\n * ```typescript\n * assertStringFormat(value, 'handle')\n * // value is now typed as HandleString\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function assertStringFormat<I extends string, F extends StringFormat>(\n  input: I,\n  format: F,\n  options?: StringFormatValidationOptions,\n): asserts input is I & StringFormats[F] {\n  if (!isStringFormat(input, format, options)) {\n    throw new TypeError(`Invalid string format (${format}): ${input}`)\n  }\n}\n\n/**\n * Validates and returns a string as the specified format type, throwing if invalid.\n *\n * This is useful when you need to convert a string to a format type in an expression.\n *\n * @typeParam I - The input string type\n * @typeParam F - The format to validate against\n * @param input - The string to validate\n * @param format - The format name to validate against\n * @returns The input typed as the format type\n * @throws {TypeError} If the string doesn't match the format\n *\n * @example\n * ```typescript\n * const did = asStringFormat(userInput, 'did')\n * // did is typed as DidString\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function asStringFormat<I extends string, F extends StringFormat>(\n  input: I,\n  format: F,\n  options?: StringFormatValidationOptions,\n): I & StringFormats[F] {\n  assertStringFormat(input, format, options)\n  return input\n}\n\n/**\n * Returns the string as the format type if valid, otherwise returns `undefined`.\n *\n * This is useful for optional validation where you want to handle invalid values\n * without throwing.\n *\n * @typeParam I - The input string type\n * @typeParam F - The format to validate against\n * @param input - The string to validate\n * @param format - The format name to validate against\n * @returns The typed string if valid, otherwise `undefined`\n *\n * @example\n * ```typescript\n * const did = ifStringFormat(maybeInvalid, 'did')\n * if (did) {\n *   // did is typed as DidString\n * }\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function ifStringFormat<I extends string, F extends StringFormat>(\n  input: I,\n  format: F,\n  options?: StringFormatValidationOptions,\n): undefined | (I & StringFormats[F]) {\n  return isStringFormat(input, format, options) ? input : undefined\n}\n\n/**\n * Array of all valid string format names.\n *\n * @example\n * ```typescript\n * for (const format of STRING_FORMATS) {\n *   console.log(format) // 'at-identifier', 'at-uri', 'cid', ...\n * }\n * ```\n */\nexport const STRING_FORMATS = /*#__PURE__*/ Object.freeze(\n  /*#__PURE__*/ Object.keys(stringFormatVerifiers),\n) as readonly StringFormat[]\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/types.ts",
    "content": "/**\n * Same as `string` but prevents TypeScript from allowing union types to\n * be widened to `string` in IDEs.\n *\n * This is useful when you want autocompletion for known string values\n * while still allowing arbitrary strings.\n *\n * @example\n * ```typescript\n * // With plain string, union is widened:\n * type Status1 = 'active' | 'inactive' | string // just becomes \"string\"\n *\n * // With UnknownString, union is preserved:\n * type Status2 = 'active' | 'inactive' | UnknownString\n * // Autocomplete will suggest 'active' and 'inactive'\n * ```\n */\nexport type UnknownString = string & NonNullable<unknown>\n\n/**\n * Simplifies a type by expanding intersections and mapped types.\n *\n * This improves IDE tooltips by showing the actual shape of a type\n * rather than complex intersections.\n *\n * @typeParam T - The type to simplify\n *\n * @example\n * ```typescript\n * type Complex = { a: string } & { b: number }\n * type Simple = Simplify<Complex>\n * // Hover shows: { a: string; b: number }\n * ```\n */\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n\n/**\n * Internal symbol for branding restricted types.\n * @internal\n */\ndeclare const __restricted: unique symbol\n\n/**\n * A type that represents a value that cannot be used, with a custom\n * message explaining the restriction.\n *\n * This is useful for creating \"never use this\" types that provide\n * helpful error messages when someone tries to use them.\n *\n * @typeParam Message - A string literal type containing the error message\n *\n * @example\n * ```typescript\n * type DeprecatedField = Restricted<'This field has been deprecated. Use newField instead.'>\n *\n * interface MyType {\n *   oldField?: DeprecatedField\n *   newField: string\n * }\n *\n * const obj: MyType = {\n *   oldField: 'value', // Error: Type 'string' is not assignable to type 'Restricted<...>'\n *   newField: 'value'\n * }\n * ```\n */\nexport type Restricted<Message extends string> = typeof __restricted & {\n  [__restricted]: Message\n}\n\n/**\n * Converts all properties of `P` that may be `undefined` into actual\n * optional properties on the resulting type.\n *\n * This is useful when working with types where some properties are typed as\n * `T | undefined` but should really be optional (`T?`).\n *\n * @typeParam P - The object type to transform\n *\n * @example\n * ```typescript\n * type Input = {\n *   required: string\n *   optional: string | undefined\n * }\n *\n * type Output = WithOptionalProperties<Input>\n * // Result: {\n * //   required: string\n * //   optional?: string | undefined\n * // }\n * ```\n */\nexport type WithOptionalProperties<P> = Simplify<\n  {\n    -readonly [K in keyof P as undefined extends P[K] ? never : K]-?: P[K]\n  } & {\n    -readonly [K in keyof P as undefined extends P[K] ? K : never]?: P[K]\n  }\n>\n\n/**\n * Creates a type by omitting a specific key from an object type.\n *\n * Similar to TypeScript's built-in `Omit`, but preserves the type structure\n * more accurately in some edge cases.\n *\n * @typeParam T - The object type to transform\n * @typeParam K - The key to omit (must be a key of T)\n *\n * @example\n * ```typescript\n * type Person = { name: string; age: number; email: string }\n * type PersonWithoutEmail = OmitKey<Person, 'email'>\n * // Result: { name: string; age: number }\n * ```\n */\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/validation-error.ts",
    "content": "import { LexError } from '@atproto/lex-data'\nimport { arrayAgg } from '../util/array-agg.js'\nimport { ResultFailure, failureReason } from './result.js'\nimport {\n  Issue,\n  IssueInvalidType,\n  IssueInvalidValue,\n} from './validation-issue.js'\n\n/**\n * Error thrown when validation fails.\n *\n * Contains detailed information about all validation issues encountered,\n * including the path to each invalid value and descriptions of what was\n * expected vs what was received.\n *\n * Extends {@link LexError} with the error name \"InvalidRequest\" for\n * consistency with the AT Protocol error handling conventions.\n *\n * @example\n * ```typescript\n * const error = new LexValidationError([\n *   new IssueInvalidType(['user', 'age'], 'hello', ['number'])\n * ])\n * console.log(error.message)\n * // \"Expected number value type at $.user.age (got string)\"\n *\n * console.log(error.issues.length) // 1\n * console.log(error.toJSON())\n * // { error: 'InvalidRequest', message: '...', issues: [...] }\n * ```\n *\n * @note this class implements {@link ResultFailure} to allow it to be used\n * directly as a failure reason in validation results, avoiding the need for\n * wrapping it in an additional object.\n */\nexport class LexValidationError\n  extends LexError<'InvalidRequest'>\n  implements ResultFailure<LexValidationError>\n{\n  name = 'LexValidationError'\n\n  /**\n   * The list of validation issues that caused this error.\n   *\n   * Issues are aggregated when possible (e.g., multiple invalid type issues\n   * at the same path are combined into a single issue listing all expected types).\n   */\n  readonly issues: Issue[]\n\n  /**\n   * Creates a new validation error from a list of issues.\n   *\n   * Issues are automatically aggregated to combine related issues at the same\n   * path (e.g., multiple type expectations from a union schema).\n   *\n   * @param issues - The validation issues that caused this error\n   * @param options - Standard Error options (e.g., `cause`)\n   */\n  constructor(issues: Issue[], options?: ErrorOptions) {\n    const issuesAgg = aggregateIssues(issues)\n    super('InvalidRequest', issuesAgg.join(', '), options)\n    this.issues = issuesAgg\n  }\n\n  /** @see {ResultFailure.success} */\n  readonly success = false as const\n\n  /** @see {ResultFailure.reason} */\n  get reason() {\n    return this\n  }\n\n  /**\n   * Converts the error to a JSON-serializable object.\n   *\n   * @returns An object containing the error details and issues details\n   */\n  override toJSON() {\n    return {\n      ...super.toJSON(),\n      issues: this.issues.map((issue) => issue.toJSON()),\n    }\n  }\n\n  /**\n   * Creates a validation error by combining multiple validation failures.\n   *\n   * This is useful when validating against multiple possible schemas (e.g., unions)\n   * and all branches fail. The resulting error contains issues from all failures.\n   *\n   * @param failures - The validation failures to combine\n   * @returns A single validation error containing all issues from the failures\n   *\n   * @example\n   * ```typescript\n   * const failures = schemas.map(s => s.safeValidate(data)).filter(r => !r.success)\n   * if (failures.length === schemas.length) {\n   *   throw LexValidationError.fromFailures(failures)\n   * }\n   * ```\n   */\n  static fromFailures(\n    failures: readonly ResultFailure<LexValidationError>[],\n  ): LexValidationError {\n    if (failures.length === 1) return failureReason(failures[0])\n    const issues = failures.flatMap(extractFailureIssues)\n    return new LexValidationError(issues, {\n      // Keep the original errors as the cause chain\n      cause: failures.map(failureReason),\n    })\n  }\n}\n\nfunction extractFailureIssues(result: ResultFailure<LexValidationError>) {\n  return result.reason.issues\n}\n\nfunction aggregateIssues(issues: Issue[]): Issue[] {\n  // Quick path for common cases\n  if (issues.length <= 1) return issues\n  if (issues.length === 2 && issues[0].code !== issues[1].code) return issues\n\n  return [\n    // Aggregate invalid_type with identical paths\n    ...arrayAgg(\n      issues.filter((issue) => issue instanceof IssueInvalidType),\n      (a, b) => comparePropertyPaths(a.path, b.path),\n      (issues) =>\n        new IssueInvalidType(\n          issues[0].path,\n          issues[0].input,\n          Array.from(new Set(issues.flatMap((iss) => iss.expected))),\n        ),\n    ),\n    // Aggregate invalid_value with identical paths\n    ...arrayAgg(\n      issues.filter((issue) => issue instanceof IssueInvalidValue),\n      (a, b) => comparePropertyPaths(a.path, b.path),\n      (issues) =>\n        new IssueInvalidValue(\n          issues[0].path,\n          issues[0].input,\n          Array.from(new Set(issues.flatMap((iss) => iss.values))),\n        ),\n    ),\n    // Pass through other issues\n    ...issues.filter(\n      (issue) =>\n        !(issue instanceof IssueInvalidType) &&\n        !(issue instanceof IssueInvalidValue),\n    ),\n  ]\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nfunction comparePropertyPaths(\n  a: readonly PropertyKey[],\n  b: readonly PropertyKey[],\n) {\n  if (a.length !== b.length) return false\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) return false\n  }\n  return true\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/validation-issue.ts",
    "content": "import { ifCid, isLegacyBlobRef, isPlainObject } from '@atproto/lex-data'\n\n/**\n * Abstract base class for all validation issues.\n *\n * An issue represents a single validation failure, containing:\n * - A code identifying the type of issue\n * - The path to the invalid value in the data structure\n * - The actual input value that failed validation\n *\n * Subclasses add specific properties relevant to each issue type and implement\n * the {@link message} property for human-readable error messages (that don't\n * contain the error path)\n */\nexport abstract class Issue {\n  abstract readonly message: string\n\n  constructor(\n    readonly code: string,\n    readonly path: readonly PropertyKey[],\n    readonly input: unknown,\n  ) {}\n\n  /**\n   * Returns a human-readable description of the validation issue.\n   */\n  toString() {\n    return `${this.message}${stringifyPath(this.path)}`\n  }\n\n  /**\n   * Converts the issue to a JSON-serializable object.\n   *\n   * @returns An object containing the issue code, path, and message\n   */\n  toJSON() {\n    return {\n      code: this.code,\n      path: this.path,\n      message: this.message,\n    }\n  }\n}\n\n/**\n * A custom validation issue with a user-defined message.\n *\n * Use this for validation rules that don't fit into the standard issue categories.\n */\nexport class IssueCustom extends Issue {\n  constructor(\n    readonly path: readonly PropertyKey[],\n    readonly input: unknown,\n    readonly message: string,\n  ) {\n    super('custom', path, input)\n  }\n}\n\n/**\n * Issue for string values that don't match an expected format.\n *\n * Used for AT Protocol specific formats like DID, handle, NSID, AT-URI, etc.\n */\nexport class IssueInvalidFormat extends Issue {\n  constructor(\n    path: readonly PropertyKey[],\n    input: unknown,\n    readonly format: string,\n    readonly detail?: string,\n  ) {\n    super('invalid_format', path, input)\n  }\n\n  override get message(): string {\n    return `Invalid ${this.formatDescription}${this.detail ? ` (${this.detail}, ` : ' ('}got ${stringifyValue(this.input)})`\n  }\n\n  /** Returns a human-readable description of the expected format. */\n  get formatDescription(): string {\n    switch (this.format) {\n      case 'at-identifier':\n        return `AT identifier`\n      case 'did':\n        return `DID`\n      case 'nsid':\n        return `NSID`\n      case 'cid':\n        return `CID string`\n      case 'tid':\n        return `TID string`\n      case 'record-key':\n        return `record key`\n      default:\n        return this.format\n    }\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      format: this.format,\n    }\n  }\n}\n\n/**\n * Issue for values that have an unexpected type.\n *\n * This is one of the most common validation issues, occurring when the\n * runtime type of a value doesn't match the expected schema type.\n */\nexport class IssueInvalidType extends Issue {\n  constructor(\n    path: readonly PropertyKey[],\n    input: unknown,\n    readonly expected: readonly string[],\n  ) {\n    super('invalid_type', path, input)\n  }\n\n  override get message(): string {\n    return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type (got ${stringifyType(this.input)})`\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      expected: this.expected,\n    }\n  }\n}\n\n/**\n * Issue for values that don't match any of the expected literal values.\n *\n * Used when a value must be one of a specific set of allowed values\n * (e.g., enum-like constraints).\n */\nexport class IssueInvalidValue extends Issue {\n  constructor(\n    path: readonly PropertyKey[],\n    input: unknown,\n    readonly values: readonly unknown[],\n  ) {\n    super('invalid_value', path, input)\n  }\n\n  override get message(): string {\n    return `Expected ${oneOf(this.values.map(stringifyValue))} (got ${stringifyValue(this.input)})`\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      values: this.values,\n    }\n  }\n}\n\n/**\n * Issue for missing required object properties.\n */\nexport class IssueRequiredKey extends Issue {\n  constructor(\n    path: readonly PropertyKey[],\n    input: unknown,\n    readonly key: PropertyKey,\n  ) {\n    super('required_key', path, input)\n  }\n\n  override get message(): string {\n    return `Missing required key \"${String(this.key)}\"`\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      key: this.key,\n    }\n  }\n}\n\n/**\n * The type of measurement for size constraint issues.\n *\n * - `'array'` - Array length\n * - `'string'` - String length in characters\n * - `'integer'` - Numeric value\n * - `'grapheme'` - String length in grapheme clusters\n * - `'bytes'` - Byte length\n * - `'blob'` - Blob size\n */\nexport type MeasurableType =\n  | 'array'\n  | 'string'\n  | 'integer'\n  | 'grapheme'\n  | 'bytes'\n  | 'blob'\n\n/**\n * Issue for values that exceed a maximum constraint.\n */\nexport class IssueTooBig extends Issue {\n  constructor(\n    path: readonly PropertyKey[],\n    input: unknown,\n    readonly maximum: number,\n    readonly type: MeasurableType,\n    readonly actual: number,\n  ) {\n    super('too_big', path, input)\n  }\n\n  override get message(): string {\n    return `${this.type} too big (maximum ${this.maximum}, got ${this.actual})`\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      type: this.type,\n      maximum: this.maximum,\n    }\n  }\n}\n\n/**\n * Issue for values that are below a minimum constraint.\n */\nexport class IssueTooSmall extends Issue {\n  constructor(\n    path: readonly PropertyKey[],\n    input: unknown,\n    readonly minimum: number,\n    readonly type: MeasurableType,\n    readonly actual: number,\n  ) {\n    super('too_small', path, input)\n  }\n\n  override get message(): string {\n    return `${this.type} too small (minimum ${this.minimum}, got ${this.actual})`\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      type: this.type,\n      minimum: this.minimum,\n    }\n  }\n}\n\n// -----------------------------------------------------------------------------\n// Helper functions for formatting error messages\n// -----------------------------------------------------------------------------\n\nfunction stringifyExpectedType(expected: string): string {\n  if (expected === '$typed') {\n    return 'an object which includes the \"$type\" property'\n  }\n  return expected\n}\n\nfunction stringifyPath(path: readonly PropertyKey[]) {\n  return ` at ${buildJsonPath(path)}`\n}\n\nfunction buildJsonPath(path: readonly PropertyKey[]): string {\n  return `$${path.map(toJsonPathSegment).join('')}`\n}\n\nfunction toJsonPathSegment(segment: PropertyKey): string {\n  if (typeof segment === 'number' || typeof segment === 'symbol') {\n    return `[${String(segment)}]`\n  } else if (/^[a-zA-Z_$][a-zA-Z0-9_]*$/.test(segment)) {\n    return `.${segment}`\n  } else {\n    return `[${JSON.stringify(segment)}]`\n  }\n}\n\nfunction oneOf(arr: readonly string[]): string {\n  if (arr.length === 0) return ''\n  if (arr.length === 1) return arr[0]\n  return `one of ${arr.slice(0, -1).join(', ')} or ${arr.at(-1)}`\n}\n\nfunction stringifyType(value: unknown): string {\n  switch (typeof value) {\n    case 'object':\n      if (value === null) return 'null'\n      if (Array.isArray(value)) return 'array'\n      if (ifCid(value)) return 'cid'\n      if (isLegacyBlobRef(value)) return 'legacy-blob'\n      if (value instanceof Date) return 'date'\n      if (value instanceof RegExp) return 'regexp'\n      if (value instanceof Map) return 'map'\n      if (value instanceof Set) return 'set'\n      return 'object'\n    case 'number':\n      if (Number.isInteger(value) && Number.isSafeInteger(value)) {\n        return 'integer'\n      }\n      if (Number.isNaN(value)) {\n        return 'NaN'\n      }\n      if (value === Infinity) {\n        return 'Infinity'\n      }\n      if (value === -Infinity) {\n        return '-Infinity'\n      }\n      return 'float'\n    default:\n      return typeof value\n  }\n}\n\nfunction stringifyValue(value: unknown): string {\n  switch (typeof value) {\n    case 'bigint':\n      return `${value}n`\n    case 'number':\n    case 'string':\n    case 'boolean':\n      return JSON.stringify(value)\n    case 'object':\n      if (Array.isArray(value)) {\n        return `[${stringifyArray(value, stringifyValue)}]`\n      }\n      if (isPlainObject(value)) {\n        return `{${stringifyArray(Object.entries(value), stringifyObjectEntry)}}`\n      }\n    // fallthrough\n    default:\n      return stringifyType(value)\n  }\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nfunction stringifyObjectEntry([key, _value]: [PropertyKey, unknown]): string {\n  return `${JSON.stringify(key)}: ...`\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nfunction stringifyArray<T>(\n  arr: readonly T[],\n  fn: (item: T) => string,\n  n = 2,\n): string {\n  return arr.slice(0, n).map(fn).join(', ') + (arr.length > n ? ', ...' : '')\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core/validator.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport { ResultFailure, ResultSuccess, success } from './result.js'\nimport { LexValidationError } from './validation-error.js'\nimport {\n  Issue,\n  IssueInvalidFormat,\n  IssueInvalidType,\n  IssueInvalidValue,\n  IssueRequiredKey,\n  IssueTooBig,\n  IssueTooSmall,\n  MeasurableType,\n} from './validation-issue.js'\n\n/**\n * Represents a successful validation result.\n *\n * @typeParam Value - The type of the validated value\n */\nexport type ValidationSuccess<Value = unknown> = ResultSuccess<Value>\n\n/**\n * Represents a failed validation result containing a {@link LexValidationError}.\n *\n * @extends ResultFailure<LexValidationError>\n * @see {@link ResultFailure}\n * @see {@link LexValidationError}\n */\nexport type ValidationFailure = LexValidationError\n\n/**\n * Discriminated union representing the outcome of a validation operation.\n *\n * Check the `success` property to determine if validation passed or failed:\n * - If `success` is `true`, the `value` property contains the validated data\n * - If `success` is `false`, the `reason` property contains the {@link LexValidationError}\n *\n * @typeParam Value - The type of the validated value on success\n *\n * @example\n * ```typescript\n * const result: ValidationResult<string> = schema.safeParse(data)\n * if (result.success) {\n *   // result.value is string\n * } else {\n *   // result.reason is LexValidationError\n * }\n * ```\n */\nexport type ValidationResult<Value = unknown> =\n  | ValidationSuccess<Value>\n  | ValidationFailure\n\n/**\n * Extracts the input type that a validator accepts.\n *\n * Use this utility type to infer what type a schema will accept during validation.\n *\n * @typeParam V - A validator type\n *\n * @example\n * ```typescript\n * const userSchema = new ObjectSchema({ name: stringSchema, age: numberSchema })\n * type UserInput = InferInput<typeof userSchema>\n * // { name: string; age: number }\n * ```\n */\nexport type InferInput<V extends Validator> = V['__lex']['input']\n\n/**\n * Extracts the output type that a validator produces after parsing.\n *\n * The output type may differ from the input type when the schema applies\n * transformations such as default values or type coercion during parsing.\n *\n * @typeParam V - A validator type\n *\n * @example\n * ```typescript\n * const schema = new StringSchema().default('hello')\n * type Input = InferInput<typeof schema>   // string | undefined\n * type Output = InferOutput<typeof schema> // string\n * ```\n */\nexport type InferOutput<V extends Validator> = V['__lex']['output']\n\n/**\n * Alias for {@link InferInput} for convenient type inference.\n *\n * @typeParam V - A validator type\n */\nexport type { InferInput as Infer }\n\nexport interface Validator<TInput = unknown, TOutput = TInput> {\n  /**\n   * This property is used for type inference purposes and does not actually\n   * exist at runtime.\n   *\n   * @internal\n   * @deprecated **INTERNAL API, DO NOT USE**.\n   */\n  readonly ['__lex']: {\n    input: TInput\n    output: TOutput\n  }\n\n  /**\n   * @internal **INTERNAL API**: use {@link ValidationContext.validate} instead\n   *\n   * This method is implemented by subclasses to perform transformation and\n   * validation of the input value. Do not call this method directly; as the\n   * {@link ValidationContext.options.mode} option will **not** be enforced. See\n   * {@link ValidationContext.validate} for details. When delegating validation\n   * from one validator sub-class implementation to another schema,\n   * {@link ValidationContext.validate} must be used instead of calling\n   * {@link Validator.validateInContext}. This will allow to stop the validation\n   * process if the value was transformed (by the other schema) but\n   * transformations are not allowed.\n   *\n   * By convention, the {@link ValidationResult} must return the original input\n   * value if validation was successful and no transformation was applied (i.e.\n   * the input already conformed to the schema). If a default value, or any\n   * other transformation was applied, the returned value can be different from\n   * the input.\n   *\n   * This convention allows the {@link Validator.check check} and\n   * {@link Validator.assert assert} methods to check whether the input value\n   * exactly matches the schema (without defaults or transformations), by\n   * checking if the returned value is strictly equal to the input.\n   *\n   * @see {@link ValidationContext.validate}\n   */\n  validateInContext(input: unknown, ctx: ValidationContext): ValidationResult\n}\n\n/**\n * Configuration options for validation and parsing operations.\n *\n * @example\n * ```typescript\n * // Validate mode (strict, no transformations)\n * ValidationContext.validate(data, schema, { mode: 'validate' })\n *\n * // Parse mode (allows transformations like defaults)\n * ValidationContext.validate(data, schema, { mode: 'parse' })\n *\n * // With initial path for nested validation\n * ValidationContext.validate(data, schema, { path: ['user', 'profile'] })\n * ```\n */\nexport type ValidationOptions = {\n  /**\n   * The validation mode determining how transformations are handled.\n   *\n   * - `\"validate\"`: Strict validation where the result must be\n   *   strictly equal to the input value. No transformations such as applying\n   *   default values are allowed.\n   * - `\"parse\"`: Allows the schema to transform the input value, such as\n   *   applying default values or performing type coercion.\n   *\n   * @default \"validate\"\n   */\n  mode?: 'validate' | 'parse'\n\n  /**\n   * The initial path to the value being validated.\n   *\n   * This is used to provide context in validation issues when validating\n   * nested structures. The path is prepended to all issue paths.\n   *\n   * @example\n   * ```typescript\n   * // Issues will be reported at paths like \"user.name\" instead of just \"name\"\n   * ValidationContext.validate(data, schema, { path: ['user'] })\n   * ```\n   */\n  path?: readonly PropertyKey[]\n\n  /**\n   * Whether to enforce strict validation rules (e.g., MIME type matching, size\n   * limits, datetime format).\n   *\n   * This is typically useful to allow more lax validation when parsing server\n   * responses, while enforcing strict validation for user input.\n   *\n   * @default true\n   */\n  strict?: boolean\n}\n\n/**\n * Manages the state and context for validation operations.\n *\n * The `ValidationContext` class is responsible for:\n * - Tracking the current path in nested structures for error reporting\n * - Collecting validation issues during traversal\n * - Enforcing validation mode (validate vs parse)\n * - Providing factory methods for creating validation results\n *\n * Use the static {@link ValidationContext.validate} method as the primary entry point\n * for validation. This ensures proper mode enforcement and issue aggregation.\n *\n * @example\n * ```typescript\n * // Primary usage via static method\n * const result = ValidationContext.validate(data, schema, { mode: 'parse' })\n *\n * // Within a custom validator implementation\n * class MyValidator implements Validator {\n *   validateInContext(input: unknown, ctx: ValidationContext): ValidationResult {\n *     if (typeof input !== 'string') {\n *       return ctx.issueUnexpectedType(input, 'string')\n *     }\n *     return ctx.success(input)\n *   }\n * }\n * ```\n */\nexport class ValidationContext {\n  /**\n   * Validates input against a validator in parse mode.\n   *\n   * In parse mode, the schema may transform the input (e.g., apply defaults).\n   *\n   * @param input - The value to validate\n   * @param validator - The validator to use\n   * @param options - Validation options with mode set to 'parse'\n   * @returns A validation result with the parsed output type\n   */\n  static validate<V extends Validator>(\n    input: unknown,\n    validator: V,\n    options: ValidationOptions & {\n      mode: 'parse'\n    },\n  ): ValidationResult<InferOutput<V>>\n\n  /**\n   * Validates input against a validator in validate mode (default).\n   *\n   * In validate mode, the result must be strictly equal to the input.\n   * No transformations are allowed.\n   *\n   * @typeParam V - The validator type\n   * @typeParam I - The input type\n   * @param input - The value to validate\n   * @param validator - The validator to use\n   * @param options - Optional validation options (defaults to validate mode)\n   * @returns A validation result preserving the input type intersected with the schema type\n   */\n  static validate<V extends Validator, I = unknown>(\n    input: I,\n    validator: V,\n    options?: ValidationOptions & {\n      mode?: 'validate'\n    },\n  ): ValidationResult<I & InferInput<V>>\n\n  /**\n   * Validates input against a validator with configurable options.\n   *\n   * @param input - The value to validate\n   * @param validator - The validator to use\n   * @param options - Optional validation options\n   * @returns A validation result with either the input or output type\n   */\n  static validate<V extends Validator>(\n    input: unknown,\n    validator: V,\n    options?: ValidationOptions,\n  ): ValidationResult<InferOutput<V> | InferInput<V>>\n  static validate<V extends Validator>(\n    input: unknown,\n    validator: V,\n    options?: ValidationOptions,\n  ): ValidationResult<InferOutput<V> | InferInput<V>> {\n    const context = new ValidationContext({\n      path: options?.path ?? [],\n      mode: options?.mode ?? 'validate',\n      strict: options?.strict ?? true,\n    })\n    return context.validate(input, validator)\n  }\n\n  /**\n   * The current path being validated, used for error reporting.\n   */\n  protected readonly currentPath: PropertyKey[]\n\n  /**\n   * Accumulated validation issues collected during traversal.\n   */\n  protected readonly issues: Issue[] = []\n\n  /**\n   * Creates a new validation context with the specified options.\n   *\n   * @param options - The validation options (path and mode are required)\n   */\n  constructor(readonly options: Required<ValidationOptions>) {\n    // Create a copy because we will be mutating the array during validation.\n    this.currentPath = Array.from(options.path)\n  }\n\n  /**\n   * Returns a copy of the current validation path.\n   *\n   * The path represents the location in the data structure being validated,\n   * used for constructing meaningful error messages.\n   */\n  get path() {\n    return Array.from(this.currentPath)\n  }\n\n  /**\n   * Creates a new path by appending segments to the current path.\n   *\n   * @param path - Optional path segment(s) to append\n   * @returns A new path array with the segment(s) appended\n   */\n  concatPath(path?: PropertyKey | readonly PropertyKey[]) {\n    if (path == null) return this.path\n    return this.currentPath.concat(path)\n  }\n\n  /**\n   * Validates input against a validator within this context.\n   *\n   * This is the primary entry point for validation within a context. Always use\n   * this method instead of calling {@link Validator.validateInContext} directly,\n   * as this method enforces validation mode rules and handles transformation detection.\n   *\n   * @typeParam V - The validator type\n   * @param input - The value to validate\n   * @param validator - The validator to use\n   * @returns A validation result with the validated value or error\n   */\n  validate<V extends Validator>(\n    input: unknown,\n    validator: V,\n  ): ValidationResult<InferInput<V>> {\n    // This is the only place where validateInContext should be called.\n    const result = validator.validateInContext(input, this)\n\n    if (result.success) {\n      if (this.issues.length > 0) {\n        // Validator returned a success but issues were added via the context.\n        // This means the overall validation failed.\n        return new LexValidationError(Array.from(this.issues))\n      }\n\n      if (this.options.mode !== 'parse' && !Object.is(result.value, input)) {\n        // If the value changed, it means that a default (or some other\n        // transformation) was applied, meaning that the original value did\n        // *not* match the (output) schema. When not in \"parse\" mode, we\n        // consider this a failure.\n\n        // This check is the reason why Validator.validateInContext should not\n        // be used directly, and ValidatorContext.validate should be used\n        // instead, even when delegating validation from one validator to\n        // another.\n\n        // This if block comes before the next one because 'this.issues' will\n        // end-up being appended to the returned LexValidationError (see the\n        // \"failure\" method below), resulting in a more complete error report.\n        return this.issueInvalidValue(input, [result.value])\n      }\n    }\n\n    return result as ValidationResult<InferInput<V>>\n  }\n\n  /**\n   * Validates a child property of an object within this context.\n   *\n   * This method automatically manages the path stack, pushing the property key\n   * before validation and popping it afterward. Use this for validating object\n   * properties to ensure proper path tracking in error messages.\n   *\n   * @typeParam I - The input object type\n   * @typeParam K - The property key type\n   * @typeParam V - The validator type\n   * @param input - The parent object containing the property\n   * @param key - The property key to validate\n   * @param validator - The validator to use for the property value\n   * @returns A validation result for the property value\n   *\n   * @example\n   * ```typescript\n   * // In a custom object validator\n   * const result = ctx.validateChild(input, 'name', stringSchema)\n   * // If validation fails, error path will include 'name'\n   * ```\n   */\n  validateChild<\n    I extends object,\n    K extends PropertyKey & keyof I,\n    V extends Validator,\n  >(input: I, key: K, validator: V): ValidationResult<InferInput<V>> {\n    // @NOTE we could add support for recursive schemas by keeping track of\n    // \"parent\" objects in the context and checking for circular references\n    // here. This would allow us to validate recursive structures without\n    // hitting maximum call stack errors, and would also allow us to provide\n    // better error messages for circular reference issues. However, this is not\n    // a priority at the moment as recursive structures are not supported in\n    // the context of AT Protocol lexicons, and we can always add this in the\n    // future if needed.\n\n    // Instead of creating a new context, we just push/pop the path segment.\n    this.currentPath.push(key)\n    try {\n      return this.validate(input[key], validator)\n    } finally {\n      this.currentPath.length--\n    }\n  }\n\n  /**\n   * Adds a validation issue to the context without immediately failing.\n   *\n   * Use this method to collect multiple issues during validation before\n   * determining the final result. Issues added this way will be included\n   * in the final error if validation fails.\n   *\n   * @param issue - The validation issue to add\n   */\n  addIssue(issue: Issue): void {\n    this.issues.push(issue)\n  }\n\n  /**\n   * Creates a successful validation result with the given value.\n   *\n   * @typeParam V - The value type\n   * @param value - The validated value\n   * @returns A successful validation result\n   */\n  success<V>(value: V): ValidationResult<V> {\n    return success(value)\n  }\n\n  /**\n   * Creates a failed validation result with the given error.\n   *\n   * @param reason - The validation error\n   * @returns A failed validation result\n   */\n  failure(reason: LexValidationError): ValidationFailure {\n    return reason\n  }\n\n  /**\n   * Creates a failed validation result from a single issue.\n   *\n   * Any previously accumulated issues in the context are included in the error.\n   *\n   * @param issue - The validation issue that caused the failure\n   * @returns A failed validation result\n   */\n  issue(issue: Issue) {\n    return this.failure(new LexValidationError([...this.issues, issue]))\n  }\n\n  /**\n   * Creates a failure for an invalid value that doesn't match expected values.\n   *\n   * @param input - The actual value that was received\n   * @param values - The expected valid values\n   * @returns A failed validation result with an invalid value issue\n   */\n  issueInvalidValue(input: unknown, values: readonly unknown[]) {\n    return this.issue(new IssueInvalidValue(this.path, input, values))\n  }\n\n  /**\n   * Creates a failure for an invalid type.\n   *\n   * @param input - The actual value that was received\n   * @param expected - An array of expected type names\n   * @returns A failed validation result with an invalid type issue\n   */\n  issueInvalidType(input: unknown, expected: readonly string[]) {\n    return this.issue(new IssueInvalidType(this.path, input, expected))\n  }\n\n  /**\n   * Creates a failure for an invalid type.\n   *\n   * @param input - The actual value that was received\n   * @param expected - The expected type name\n   * @returns A failed validation result with an invalid type issue\n   */\n  issueUnexpectedType(input: unknown, expected: string) {\n    return this.issueInvalidType(input, [expected])\n  }\n\n  /**\n   * Creates a failure for a missing required key in an object.\n   *\n   * @param input - The object missing the required key\n   * @param key - The name of the required key\n   * @returns A failed validation result with a required key issue\n   */\n  issueRequiredKey(input: object, key: PropertyKey) {\n    return this.issue(new IssueRequiredKey(this.path, input, key))\n  }\n\n  /**\n   * Creates a failure for an invalid string format.\n   *\n   * @param input - The actual value that was received\n   * @param format - The expected format name (e.g., 'did', 'handle', 'uri')\n   * @param msg - Optional additional message describing the format error\n   * @returns A failed validation result with an invalid format issue\n   */\n  issueInvalidFormat(input: unknown, format: string, msg?: string) {\n    return this.issue(new IssueInvalidFormat(this.path, input, format, msg))\n  }\n\n  /**\n   * Creates a failure for a value that exceeds a maximum constraint.\n   *\n   * @param input - The actual value that was received\n   * @param type - The type of measurement (e.g., 'string', 'array', 'bytes')\n   * @param max - The maximum allowed value\n   * @param actual - The actual measured value\n   * @returns A failed validation result with a too big issue\n   */\n  issueTooBig(\n    input: unknown,\n    type: MeasurableType,\n    max: number,\n    actual: number,\n  ) {\n    return this.issue(new IssueTooBig(this.path, input, max, type, actual))\n  }\n\n  /**\n   * Creates a failure for a value that is below a minimum constraint.\n   *\n   * @param input - The actual value that was received\n   * @param type - The type of measurement (e.g., 'string', 'array', 'bytes')\n   * @param min - The minimum required value\n   * @param actual - The actual measured value\n   * @returns A failed validation result with a too small issue\n   */\n  issueTooSmall(\n    input: unknown,\n    type: MeasurableType,\n    min: number,\n    actual: number,\n  ) {\n    return this.issue(new IssueTooSmall(this.path, input, min, type, actual))\n  }\n\n  /**\n   * Creates a failure for an invalid property value within an object.\n   *\n   * This is a convenience method that automatically extracts the property value\n   * and constructs the appropriate path.\n   *\n   * @typeParam I - The input object type\n   * @param input - The object containing the invalid property\n   * @param property - The property key with the invalid value\n   * @param values - The expected valid values\n   * @returns A failed validation result with an invalid value issue at the property path\n   */\n  issueInvalidPropertyValue<I>(\n    input: I,\n    property: keyof I & PropertyKey,\n    values: readonly unknown[],\n  ) {\n    const value = input[property]\n    const path = this.concatPath(property)\n    return this.issue(new IssueInvalidValue(path, value, values))\n  }\n\n  /**\n   * Creates a failure for an invalid property type within an object.\n   *\n   * This is a convenience method that automatically extracts the property value\n   * and constructs the appropriate path.\n   *\n   * @typeParam I - The input object type\n   * @param input - The object containing the invalid property\n   * @param property - The property key with the invalid type\n   * @param expected - The expected type name\n   * @returns A failed validation result with an invalid type issue at the property path\n   */\n  issueInvalidPropertyType<I>(\n    input: I,\n    property: keyof I & PropertyKey,\n    expected: string,\n  ) {\n    const value = input[property]\n    const path = this.concatPath(property)\n    return this.issue(new IssueInvalidType(path, value, [expected]))\n  }\n}\n\n/**\n * Recursively unwraps a wrapped validator to its innermost validator type.\n *\n * Some validators wrap other validators (e.g., optional, nullable). This type\n * utility recursively unwraps such wrappers to reveal the core validator.\n *\n * @typeParam T - A validator type, possibly wrapped\n *\n * @example\n * ```typescript\n * type Inner = UnwrapValidator<OptionalValidator<NullableValidator<StringSchema>>>\n * // Result: StringSchema\n * ```\n */\nexport type UnwrapValidator<T extends Validator> = T extends {\n  unwrap(): infer U extends Validator\n}\n  ? UnwrapValidator<U>\n  : T\n\n/**\n * Interface for validators that wrap another validator.\n *\n * Implement this interface when creating validators that wrap or modify\n * the behavior of another validator (e.g., optional, nullable, transform).\n *\n * @typeParam Validator - The type of the wrapped validator\n *\n * @example\n * ```typescript\n * class OptionalSchema<V extends Validator> implements WrappedValidator<V> {\n *   constructor(private inner: V) {}\n *\n *   unwrap(): V {\n *     return this.inner\n *   }\n * }\n * ```\n */\nexport interface WrappedValidator<out Validator> {\n  /**\n   * Returns the inner wrapped validator.\n   *\n   * @returns The wrapped validator\n   */\n  unwrap(): Validator\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/core.ts",
    "content": "export * from './core/$type.js'\nexport * from './core/record-key.js'\nexport * from './core/result.js'\nexport * from './core/schema.js'\nexport * from './core/string-format.js'\nexport * from './core/types.js'\nexport * from './core/validation-error.js'\nexport * from './core/validation-issue.js'\nexport * from './core/validator.js'\n"
  },
  {
    "path": "packages/lex/lex-schema/src/external.ts",
    "content": "export * from './core.js'\nexport * from './helpers.js'\nexport * from './schema.js'\n"
  },
  {
    "path": "packages/lex/lex-schema/src/helpers.test.ts",
    "content": "import { describe, test } from 'vitest'\nimport * as l from './external.js'\n\nclass BinaryValue {\n  declare readonly ['__binaryValue']: BinaryValue\n}\n\nconst expectType = <T>(_: T) => {}\nconst binaryValue = new BinaryValue()\n\ndescribe('InferMethodParams', () => {\n  describe('query', () => {\n    test('infers parameter types', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params({\n          feed: l.string({ format: 'at-uri' }),\n          limit: l.optional(l.integer()),\n        }),\n        l.payload('application/json', undefined),\n      )\n\n      type Params = l.InferMethodParams<typeof query>\n\n      expectType<Params>({\n        feed: 'at://did:plc:abc123/app.bsky.feed.post/xyz',\n        limit: 50,\n      })\n      // @ts-expect-error\n      expectType<Params>({ feed: 123, limit: 50 })\n      // @ts-expect-error\n      expectType<Params>({ limit: 50 })\n    })\n  })\n\n  describe('procedure', () => {\n    test('infers parameter types', () => {\n      const procedure = l.procedure(\n        'com.example.list',\n        l.params({ limit: l.string() }),\n        l.payload(),\n        l.payload(),\n      )\n\n      type Params = l.InferMethodParams<typeof procedure>\n\n      expectType<Params>({ limit: '10' })\n      // @ts-expect-error\n      expectType<Params>({ limit: 10 })\n      // @ts-expect-error\n      expectType<Params>({ limit: binaryValue })\n      // @ts-expect-error\n      expectType<Params>(binaryValue)\n    })\n  })\n\n  describe('subscription', () => {\n    test('infers parameter types', () => {\n      const subscription = l.subscription(\n        'com.example.subscribe',\n        l.params({\n          cursor: l.optional(l.integer()),\n        }),\n        l.lexMap(),\n      )\n\n      type Params = l.InferMethodParams<typeof subscription>\n\n      expectType<Params>({ cursor: 100 })\n      // @ts-expect-error\n      expectType<Params>({ cursor: '100' })\n      // @ts-expect-error\n      expectType<Params>({ cursor: binaryValue })\n    })\n  })\n})\n\ndescribe('InferMethodInput', () => {\n  describe('procedure', () => {\n    test('with payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.create',\n        l.params(),\n        l.jsonPayload({ text: l.string() }),\n        l.payload(),\n      )\n\n      type Input = l.InferMethodInput<typeof procedure, BinaryValue>\n\n      expectType<Input>({ encoding: 'application/json', body: { text: 'hi' } })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'application/json', body: { text: 123 } })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'text/plain', body: { text: 'hi' } })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'text/plain', body: 'hello' })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'text/plain', body: new Uint8Array() })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'text/plain', body: binaryValue })\n    })\n\n    test('without payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.create',\n        l.params(),\n        l.payload('*/*', undefined),\n        l.payload(),\n      )\n\n      type Input = l.InferMethodInput<typeof procedure, BinaryValue>\n\n      expectType<Input>({ encoding: 'image/png', body: binaryValue })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'image/png', body: new Uint8Array() })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'image/png', body: [] })\n      // @ts-expect-error\n      expectType<Input>({ encoding: 'image/png', body: 'foo' })\n    })\n  })\n})\n\ndescribe('InferMethodInputBody', () => {\n  describe('procedure', () => {\n    test('with payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.create',\n        l.params(),\n        l.jsonPayload({ text: l.string() }),\n        l.payload(),\n      )\n\n      type InputBody = l.InferMethodInputBody<typeof procedure, BinaryValue>\n\n      expectType<InputBody>({ text: 'hello' })\n      // @ts-expect-error\n      expectType<InputBody>({ text: 123 })\n      // @ts-expect-error\n      expectType<InputBody>(binaryValue)\n    })\n\n    test('without payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.upload',\n        l.params(),\n        l.payload('*/*', undefined),\n        l.payload(),\n      )\n\n      type InputBody = l.InferMethodInputBody<typeof procedure, BinaryValue>\n\n      expectType<InputBody>(binaryValue)\n      // @ts-expect-error\n      expectType<InputBody>(new Uint8Array())\n      // @ts-expect-error\n      expectType<InputBody>({ text: 'hello' })\n    })\n  })\n})\n\ndescribe('InferMethodInputEncoding', () => {\n  describe('procedure', () => {\n    test('with payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.create',\n        l.params(),\n        l.jsonPayload({ text: l.string() }),\n        l.payload(),\n      )\n\n      type InputEncoding = l.InferMethodInputEncoding<typeof procedure>\n\n      expectType<InputEncoding>('application/json')\n      // @ts-expect-error\n      expectType<InputEncoding>('text/plain')\n      // @ts-expect-error\n      expectType<InputEncoding>(123)\n    })\n\n    test('without payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.upload',\n        l.params(),\n        l.payload('*/*', undefined),\n        l.payload(),\n      )\n\n      type InputEncoding = l.InferMethodInputEncoding<typeof procedure>\n\n      expectType<InputEncoding>('image/png')\n      expectType<InputEncoding>('application/octet-stream')\n      // @ts-expect-error\n      expectType<InputEncoding>(123)\n    })\n  })\n})\n\ndescribe('InferMethodOutput', () => {\n  describe('query', () => {\n    test('generic output', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.payload(),\n      ) as unknown as l.Query\n      const unknownValue = {} as unknown\n      const lexValue = {} as l.LexValue\n\n      type Output = l.InferMethodOutput<typeof query, BinaryValue>\n\n      expectType<Output>(undefined)\n      expectType<Output>({ body: binaryValue, encoding: 'text/plain' })\n      expectType<Output>({ body: lexValue, encoding: 'application/json' })\n\n      expectType<Output>({\n        // @ts-expect-error\n        body: unknownValue,\n        encoding: 'application/octet-stream',\n      })\n\n      class Foo {\n        constructor(readonly field: number = 3) {}\n      }\n      expectType<Output>({\n        // @ts-expect-error\n        body: new Foo(),\n        encoding: 'application/octet-stream',\n      })\n    })\n\n    test('with payload schema', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.jsonPayload({ items: l.array(l.string()) }),\n      )\n\n      type Output = l.InferMethodOutput<typeof query, BinaryValue>\n\n      expectType<Output>({ encoding: 'application/json', body: { items: [] } })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'application/json', body: { items: [1] } })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'text/plain', body: { items: [] } })\n    })\n\n    test('without payload schema', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.payload('*/*', undefined),\n      )\n\n      type Output = l.InferMethodOutput<typeof query, BinaryValue>\n\n      expectType<Output>({ encoding: 'image/png', body: binaryValue })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'image/png', body: new Uint8Array() })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'image/png', body: 'string' })\n    })\n  })\n\n  describe('procedure', () => {\n    test('generic output', () => {\n      const procedure = l.procedure(\n        'com.example.procedure',\n        l.params(),\n        l.payload(),\n        l.payload(),\n      ) as unknown as l.Procedure\n      const unknownValue = {} as unknown\n      const lexValue = {} as l.LexValue\n\n      type Output = l.InferMethodOutput<typeof procedure, BinaryValue>\n\n      expectType<Output>(undefined)\n      expectType<Output>({ body: binaryValue, encoding: 'text/plain' })\n      expectType<Output>({ body: lexValue, encoding: 'application/json' })\n      expectType<Output>({ body: { foo: 'bar' }, encoding: 'application/json' })\n\n      expectType<Output>({\n        // @ts-expect-error\n        body: unknownValue,\n        encoding: 'application/octet-stream',\n      })\n\n      class Foo {\n        constructor(readonly field: number = 3) {}\n      }\n      expectType<Output>({\n        // @ts-expect-error\n        body: new Foo(),\n        encoding: 'application/octet-stream',\n      })\n    })\n\n    test('with payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.create',\n        l.params(),\n        l.payload(),\n        l.payload(\n          'application/json',\n          l.object({ uri: l.string({ format: 'at-uri' }) }),\n        ),\n        undefined,\n      )\n\n      type Output = l.InferMethodOutput<typeof procedure, BinaryValue>\n\n      expectType<Output>({\n        encoding: 'application/json',\n        body: { uri: 'at://did:plc:abc/post/123' },\n      })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'application/json', body: { uri: 123 } })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'text/plain', body: { uri: 'at://...' } })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'text/plain', body: 'Hello' })\n    })\n\n    test('without payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.export',\n        l.params(),\n        l.payload(),\n        l.payload('*/*', undefined),\n        undefined,\n      )\n\n      type Output = l.InferMethodOutput<typeof procedure, BinaryValue>\n\n      expectType<Output>({\n        encoding: 'application/zip',\n        body: binaryValue,\n      })\n      expectType<Output>({\n        encoding: 'application/zip',\n        // @ts-expect-error\n        body: new Uint8Array(),\n      })\n      // @ts-expect-error\n      expectType<Output>({ encoding: 'application/zip', body: 'string' })\n    })\n  })\n})\n\ndescribe('InferMethodOutputBody', () => {\n  describe('query', () => {\n    test('generic output', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.payload(),\n      ) as unknown as l.Query\n      const lexValue = {} as l.LexValue\n\n      type OutputBody = l.InferMethodOutputBody<typeof query, BinaryValue>\n\n      expectType<OutputBody>(undefined)\n      expectType<OutputBody>(binaryValue)\n      expectType<OutputBody>(lexValue)\n\n      class Foo {\n        constructor(readonly field: number = 3) {}\n      }\n      // @ts-expect-error\n      expectType<OutputBody>(new Foo())\n    })\n\n    test('with payload schema', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.payload(\n          'application/json',\n          l.object({\n            cursor: l.optional(l.string()),\n            feed: l.string({ format: 'at-uri' }),\n          }),\n        ),\n      )\n\n      type OutputBody = l.InferMethodOutputBody<typeof query, BinaryValue>\n\n      expectType<OutputBody>({\n        cursor: 'abc123',\n        feed: 'at://did:plc:abc123/app.bsky.feed.post/xyz',\n      })\n      // @ts-expect-error\n      expectType<OutputBody>({ cursor: 123, feed: 'at://...' })\n      // @ts-expect-error\n      expectType<OutputBody>({ feed: 123 })\n    })\n\n    test('without payload schema', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.payload('*/*', undefined),\n      )\n\n      type OutputBody = l.InferMethodOutputBody<typeof query, BinaryValue>\n\n      expectType<OutputBody>(binaryValue)\n      // @ts-expect-error\n      expectType<OutputBody>(new Uint8Array())\n      // @ts-expect-error\n      expectType<OutputBody>({ data: 'foo' })\n    })\n  })\n\n  describe('procedure', () => {\n    test('with payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.get',\n        l.params(),\n        l.payload(),\n        l.payload(\n          'application/json',\n          l.object({ uri: l.string({ format: 'at-uri' }) }),\n        ),\n        undefined,\n      )\n\n      type OutputBody = l.InferMethodOutputBody<typeof procedure, BinaryValue>\n\n      expectType<OutputBody>({ uri: 'at://did:plc:abc/post/123' })\n      // @ts-expect-error\n      expectType<OutputBody>({ uri: 123 })\n      // @ts-expect-error\n      expectType<OutputBody>(binaryValue)\n    })\n\n    test('without payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.export',\n        l.params(),\n        l.payload(),\n        l.payload('*/*', undefined),\n        undefined,\n      )\n\n      type OutputBody = l.InferMethodOutputBody<typeof procedure, BinaryValue>\n\n      expectType<OutputBody>(binaryValue)\n      // @ts-expect-error\n      expectType<OutputBody>(new Uint8Array())\n      // @ts-expect-error\n      expectType<OutputBody>({ data: 'foo' })\n    })\n  })\n})\n\ndescribe('InferMethodOutputEncoding', () => {\n  describe('query', () => {\n    test('with payload schema', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.jsonPayload({ data: l.string() }),\n      )\n\n      type OutputEncoding = l.InferMethodOutputEncoding<typeof query>\n\n      expectType<OutputEncoding>('application/json')\n      // @ts-expect-error\n      expectType<OutputEncoding>('text/plain')\n      // @ts-expect-error\n      expectType<OutputEncoding>(123)\n    })\n\n    test('without payload schema', () => {\n      const query = l.query(\n        'com.example.query',\n        l.params(),\n        l.payload('*/*', undefined),\n      )\n\n      type OutputEncoding = l.InferMethodOutputEncoding<typeof query>\n\n      expectType<OutputEncoding>('image/png')\n      expectType<OutputEncoding>('application/octet-stream')\n      // @ts-expect-error\n      expectType<OutputEncoding>(123)\n    })\n  })\n\n  describe('procedure', () => {\n    test('with payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.create',\n        l.params(),\n        l.payload(),\n        l.jsonPayload({ id: l.string() }),\n        undefined,\n      )\n\n      type OutputEncoding = l.InferMethodOutputEncoding<typeof procedure>\n\n      expectType<OutputEncoding>('application/json')\n      // @ts-expect-error\n      expectType<OutputEncoding>('text/plain')\n      // @ts-expect-error\n      expectType<OutputEncoding>(123)\n    })\n\n    test('without payload schema', () => {\n      const procedure = l.procedure(\n        'com.example.export',\n        l.params(),\n        l.payload(),\n        l.payload('*/*', undefined),\n        undefined,\n      )\n\n      type OutputEncoding = l.InferMethodOutputEncoding<typeof procedure>\n\n      expectType<OutputEncoding>('application/zip')\n      expectType<OutputEncoding>('application/octet-stream')\n      // @ts-expect-error\n      expectType<OutputEncoding>(123)\n    })\n  })\n})\n\ndescribe('InferMethodMessage', () => {\n  describe('subscription', () => {\n    test('with message schema', () => {\n      const subscription = l.subscription(\n        'com.example.subscribe',\n        l.params(),\n        l.object({\n          seq: l.integer(),\n          event: l.string(),\n        }),\n      )\n\n      type Message = l.InferMethodMessage<typeof subscription>\n\n      expectType<Message>({ seq: 1, event: 'create' })\n      // @ts-expect-error\n      expectType<Message>({ seq: '1', event: 'create' })\n      // @ts-expect-error\n      expectType<Message>({ seq: 1 })\n      // @ts-expect-error\n      expectType<Message>(binaryValue)\n    })\n\n    test('without message schema', () => {\n      const subscription = l.subscription(\n        'com.example.subscribe',\n        l.params(),\n        l.integer(),\n      )\n\n      type Message = l.InferMethodMessage<typeof subscription>\n\n      // @ts-expect-error \"unknown\" is turned into LexValue\n      expectType<Message>(undefined)\n      // @ts-expect-error Not a LexValue\n      expectType<Message>({ any: 'value' })\n      expectType<Message>(123)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/helpers.ts",
    "content": "import { LexErrorData } from '@atproto/lex-data'\nimport { InferOutput, Restricted, Schema } from './core.js'\nimport {\n  InferPayload,\n  InferPayloadBody,\n  InferPayloadEncoding,\n  Procedure,\n  Query,\n  Subscription,\n  object,\n  optional,\n  string,\n} from './schema.js'\n\nexport type Main<T> = T | { main: T }\n\nexport function getMain<T extends object>(ns: Main<T>): T {\n  return 'main' in ns ? ns.main : ns\n}\n\n/**\n * Every XRPC implementation should translate `application/json` and `text/*`\n * payloads into their native equivalent ({@link LexValue} or string). Binary\n * data payloads, however, can be represented differently depending on the\n * environment and use case (e.g. `Uint8Array`, `Blob`, `Buffer`,\n * `ReadableStream`, etc.). This type is a placeholder to represent binary data\n * when not explicitly provided.\n */\nexport type BinaryData = Restricted<'Binary data'>\n\nexport type InferMethodParams<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n  M extends Procedure<any, infer TParams, any, any, any>\n    ? InferOutput<TParams>\n    : M extends Query<any, infer TParams, any, any>\n      ? InferOutput<TParams>\n      : M extends Subscription<any, infer TParams, any, any>\n        ? InferOutput<TParams>\n        : never\n\nexport type InferMethodInput<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n  B = BinaryData,\n> =\n  M extends Procedure<any, any, infer TInput, any, any>\n    ? InferPayload<TInput, B>\n    : undefined\n\nexport type InferMethodInputBody<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n  B = BinaryData,\n> =\n  M extends Procedure<any, any, infer TInput, any, any>\n    ? InferPayloadBody<TInput, B>\n    : undefined\n\nexport type InferMethodInputEncoding<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n  M extends Procedure<any, any, infer TInput, any, any>\n    ? InferPayloadEncoding<TInput>\n    : undefined\n\nexport type InferMethodOutput<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n  B = BinaryData,\n> =\n  M extends Procedure<any, any, any, infer TOutput, any>\n    ? InferPayload<TOutput, B>\n    : M extends Query<any, any, infer TOutput, any>\n      ? InferPayload<TOutput, B>\n      : undefined\n\nexport type InferMethodOutputBody<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n  B = BinaryData,\n> =\n  M extends Procedure<any, any, any, infer TOutput, any>\n    ? InferPayloadBody<TOutput, B>\n    : M extends Query<any, any, infer TOutput, any>\n      ? InferPayloadBody<TOutput, B>\n      : undefined\n\nexport type InferMethodOutputEncoding<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n  M extends Procedure<any, any, any, infer TOutput, any>\n    ? InferPayloadEncoding<TOutput>\n    : M extends Query<any, any, infer TOutput, any>\n      ? InferPayloadEncoding<TOutput>\n      : undefined\n\nexport type InferMethodMessage<\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n  M extends Subscription<any, any, infer TMessage, any>\n    ? InferOutput<TMessage>\n    : undefined\n\nexport type InferMethodError<\n  //\n  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> = M extends { errors: readonly (infer E extends string)[] } ? E : never\n\nexport const lexErrorDataSchema = object({\n  error: string({ minLength: 1 }),\n  message: optional(string()),\n}) satisfies Schema<LexErrorData>\n"
  },
  {
    "path": "packages/lex/lex-schema/src/index.ts",
    "content": "import * as l from './external.js'\nexport * from './external.js'\nexport { l }\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/array.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { array } from './array.js'\nimport { integer } from './integer.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\n\ndescribe('ArraySchema', () => {\n  describe('validation', () => {\n    it('validates arrays with string items', () => {\n      const schema = array(string())\n      const result = schema.safeValidate(['hello', 'world'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates arrays with integer items', () => {\n      const schema = array(integer())\n      const result = schema.safeValidate([1, 2, 3])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates arrays with object items', () => {\n      const schema = array(\n        object({\n          name: string(),\n          age: integer(),\n        }),\n      )\n      const result = schema.safeValidate([\n        { name: 'Alice', age: 30 },\n        { name: 'Bob', age: 25 },\n      ])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates empty arrays', () => {\n      const schema = array(string())\n      const result = schema.safeValidate([])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects non-array values', () => {\n      const schema = array(string())\n      const result = schema.safeValidate('not an array')\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects null values', () => {\n      const schema = array(string())\n      const result = schema.safeValidate(null)\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects undefined values', () => {\n      const schema = array(string())\n      const result = schema.safeValidate(undefined)\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects objects that look like arrays', () => {\n      const schema = array(string())\n      const result = schema.safeValidate({ 0: 'a', 1: 'b', length: 2 })\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects arrays with invalid items', () => {\n      const schema = array(integer())\n      const result = schema.safeValidate([1, 2, 'three'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects arrays with some invalid items', () => {\n      const schema = array(string())\n      const result = schema.safeValidate(['valid', null, 'also valid'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects single values', () => {\n      const schema = array(string())\n      const result = schema.safeValidate(3)\n      expect(result).toMatchObject({\n        success: false,\n        reason: expect.objectContaining({\n          message: expect.stringContaining(\n            'Expected array value type (got integer) at $',\n          ),\n        }),\n      })\n    })\n  })\n\n  describe('minLength constraint', () => {\n    it('validates arrays meeting minLength', () => {\n      const schema = array(string(), { minLength: 2 })\n      const result = schema.safeParse(['a', 'b'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates arrays exceeding minLength', () => {\n      const schema = array(string(), { minLength: 2 })\n      const result = schema.safeParse(['a', 'b', 'c'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects arrays below minLength', () => {\n      const schema = array(string(), { minLength: 3 })\n      const result = schema.safeParse(['a', 'b'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects empty arrays when minLength is set', () => {\n      const schema = array(string(), { minLength: 1 })\n      const result = schema.safeParse([])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('validates empty arrays when minLength is 0', () => {\n      const schema = array(string(), { minLength: 0 })\n      const result = schema.safeParse([])\n      expect(result).toMatchObject({ success: true })\n    })\n  })\n\n  describe('maxLength constraint', () => {\n    it('validates arrays meeting maxLength', () => {\n      const schema = array(string(), { maxLength: 3 })\n      const result = schema.safeParse(['a', 'b', 'c'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates arrays below maxLength', () => {\n      const schema = array(string(), { maxLength: 3 })\n      const result = schema.safeParse(['a', 'b'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects arrays exceeding maxLength', () => {\n      const schema = array(string(), { maxLength: 2 })\n      const result = schema.safeParse(['a', 'b', 'c'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('validates empty arrays with maxLength', () => {\n      const schema = array(string(), { maxLength: 5 })\n      const result = schema.safeParse([])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects empty arrays when maxLength is 0', () => {\n      const schema = array(string(), { maxLength: 0 })\n      const result = schema.safeParse(['a'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('validates empty arrays when maxLength is 0', () => {\n      const schema = array(string(), { maxLength: 0 })\n      const result = schema.safeParse([])\n      expect(result).toMatchObject({ success: true })\n    })\n  })\n\n  describe('minLength and maxLength together', () => {\n    it('validates arrays within range', () => {\n      const schema = array(string(), {\n        minLength: 2,\n        maxLength: 4,\n      })\n      const result = schema.safeParse(['a', 'b', 'c'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates arrays at min boundary', () => {\n      const schema = array(string(), {\n        minLength: 2,\n        maxLength: 4,\n      })\n      const result = schema.safeParse(['a', 'b'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('validates arrays at max boundary', () => {\n      const schema = array(string(), {\n        minLength: 2,\n        maxLength: 4,\n      })\n      const result = schema.safeParse(['a', 'b', 'c', 'd'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects arrays below minLength', () => {\n      const schema = array(string(), {\n        minLength: 2,\n        maxLength: 4,\n      })\n      const result = schema.safeParse(['a'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('rejects arrays above maxLength', () => {\n      const schema = array(string(), {\n        minLength: 2,\n        maxLength: 4,\n      })\n      const result = schema.safeParse(['a', 'b', 'c', 'd', 'e'])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('validates single-length range', () => {\n      const schema = array(string(), {\n        minLength: 3,\n        maxLength: 3,\n      })\n      const result = schema.safeParse(['a', 'b', 'c'])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects arrays not matching exact length', () => {\n      const schema = array(string(), {\n        minLength: 3,\n        maxLength: 3,\n      })\n      const result = schema.safeParse(['a', 'b'])\n      expect(result).toMatchObject({ success: false })\n    })\n  })\n\n  describe('nested arrays', () => {\n    it('validates arrays of arrays', () => {\n      const schema = array(array(string()))\n      const result = schema.safeParse([\n        ['a', 'b'],\n        ['c', 'd'],\n      ])\n      expect(result).toMatchObject({ success: true })\n    })\n\n    it('rejects invalid nested arrays', () => {\n      const schema = array(array(integer()))\n      const result = schema.safeParse([\n        [1, 2],\n        [3, 'four'],\n      ])\n      expect(result).toMatchObject({ success: false })\n    })\n\n    it('validates deeply nested arrays', () => {\n      const schema = array(array(array(integer())))\n      const result = schema.safeParse([[[1, 2], [3]], [[4, 5, 6]]])\n      expect(result).toMatchObject({ success: true })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/array.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n} from '../core.js'\nimport { memoizedTransformer } from '../util/memoize.js'\n\n/**\n * Configuration options for array schema validation.\n *\n * @property minLength - Minimum number of items in the array\n * @property maxLength - Maximum number of items in the array\n */\nexport type ArraySchemaOptions = {\n  minLength?: number\n  maxLength?: number\n}\n\n/**\n * Schema for validating arrays where all items match a given schema.\n *\n * Validates that the input is an array, checks length constraints, and\n * validates each item against the provided item schema.\n *\n * @template TItem - The validator type for array items\n *\n * @example\n * ```ts\n * const schema = new ArraySchema(l.string(), { maxLength: 10 })\n * const result = schema.validate(['a', 'b', 'c'])\n * ```\n */\nexport class ArraySchema<const TItem extends Validator> extends Schema<\n  Array<InferInput<TItem>>,\n  Array<InferOutput<TItem>>\n> {\n  readonly type = 'array' as const\n\n  constructor(\n    readonly validator: TItem,\n    readonly options: ArraySchemaOptions = {},\n  ) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!Array.isArray(input)) {\n      return ctx.issueUnexpectedType(input, 'array')\n    }\n\n    const { minLength, maxLength } = this.options\n\n    if (minLength != null && input.length < minLength) {\n      return ctx.issueTooSmall(input, 'array', minLength, input.length)\n    }\n\n    if (maxLength != null && input.length > maxLength) {\n      return ctx.issueTooBig(input, 'array', maxLength, input.length)\n    }\n\n    let copy: undefined | unknown[]\n\n    for (let i = 0; i < input.length; i++) {\n      const result = ctx.validateChild(input, i, this.validator)\n      if (!result.success) return result\n\n      if (result.value !== input[i]) {\n        if (ctx.options.mode === 'validate') {\n          // In \"validate\" mode, we can't modify the input, so we issue an error\n          return ctx.issueInvalidPropertyValue(input, i, [result.value])\n        }\n\n        // Copy on write (but only if we did not already make a copy)\n        copy ??= Array.from(input)\n        copy[i] = result.value\n      }\n    }\n\n    return ctx.success(copy ?? input)\n  }\n}\n\n/**\n * Creates an array schema that validates each item against the provided schema.\n *\n * @param items - Schema to validate each array item against\n * @param options - Optional length constraints\n * @returns A new {@link ArraySchema} instance\n *\n * @example\n * ```ts\n * // Array of strings\n * const tagsSchema = l.array(l.string())\n *\n * // Array with length constraints\n * const limitedSchema = l.array(l.integer(), { maxLength: 100 })\n *\n * // Array of objects\n * const usersSchema = l.array(l.object({\n *   name: l.string(),\n *   age: l.integer(),\n * }))\n *\n * // Non-empty array\n * const nonEmptySchema = l.array(l.string(), { minLength: 1 })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nfunction arraySchema<const TValidator extends Validator>(\n  items: TValidator,\n  options?: ArraySchemaOptions,\n): ArraySchema<TValidator>\nfunction arraySchema<\n  const TValue,\n  const TValidator extends Validator<TValue> = Validator<TValue>,\n>(items: TValidator, options?: ArraySchemaOptions): ArraySchema<TValidator>\nfunction arraySchema<const TValidator extends Validator>(\n  items: TValidator,\n  options?: ArraySchemaOptions,\n) {\n  return new ArraySchema<TValidator>(items, options)\n}\n\nexport const array = /*#__PURE__*/ memoizedTransformer(arraySchema)\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/blob.test.ts",
    "content": "import { assert, describe, expect, it } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport { blob } from './blob.js'\n\n// await cidForRawBytes(Buffer.from('Hello, World!'))\nconst blobCid = parseCid(\n  'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n)\n// await cidForLex(Buffer.from('Hello, World!'))\nconst lexCid = parseCid(\n  'bafyreic52vzks7wdklat4evp3vimohl55i2unzqpshz2ytka5omzr7exdy',\n)\n\ndescribe('BlobSchema', () => {\n  describe('basic validation', () => {\n    const schema = blob({})\n\n    it('validates valid blob references', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('blob')\n        expect(result.value.mimeType).toBe('image/jpeg')\n        expect(result.value.size).toBe(10000)\n      }\n    })\n\n    it('validates blob with different mime types', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/png',\n        size: 5000,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates blob with size 0', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'text/plain',\n        size: 0,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-objects', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('BlobRef validation', () => {\n    const schema = blob({})\n\n    it('rejects blob without $type', () => {\n      const result = schema.safeParse({\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with wrong $type', () => {\n      const result = schema.safeParse({\n        $type: 'notblob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob without ref', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob without mimeType', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob without size', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with invalid ref type', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: 'not a cid',\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with invalid mimeType type', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 123,\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with invalid size type', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: '10000',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with negative size', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: -1,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with decimal size', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000.5,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with extra properties', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n        extra: 'not allowed',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with $link format for ref', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: { $link: blobCid.toString() },\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with unknown properties', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n        unknownProp: 42,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('strict validation', () => {\n    const schema = blob()\n\n    it('accepts valid raw CID in strict mode', () => {\n      const result = schema.safeParse(\n        {\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        { strict: true },\n      )\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-raw CID in strict mode', () => {\n      const result = schema.safeParse(\n        {\n          $type: 'blob',\n          ref: lexCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        { strict: true },\n      )\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts non-raw CID in non-strict mode', () => {\n      const result = schema.safeParse(\n        {\n          $type: 'blob',\n          ref: lexCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        },\n        { strict: false },\n      )\n      expect(result.success).toBe(true)\n    })\n\n    it('coerces legacy blob format in non-strict parse mode', () => {\n      const result = schema.safeParse(\n        {\n          cid: lexCid.toString(),\n          mimeType: 'image/jpeg',\n        },\n        { strict: false },\n      )\n      assert(result.success)\n      expect(result.value).toEqual({\n        $type: 'blob',\n        ref: lexCid,\n        mimeType: 'image/jpeg',\n        size: -1,\n      })\n    })\n  })\n\n  describe('legacy blob format', () => {\n    it('rejects legacy format by default', () => {\n      const schema = blob({})\n      const result = schema.safeParse({\n        cid: blobCid.toString(),\n        mimeType: 'image/jpeg',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts legacy format when allowLegacy is true', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        cid: blobCid.toString(),\n        mimeType: 'image/jpeg',\n      })\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect('cid' in result.value && result.value.cid).toBe(\n          blobCid.toString(),\n        )\n        expect(result.value.mimeType).toBe('image/jpeg')\n      }\n    })\n\n    it('accepts legacy format with lexCid when allowLegacy is true', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        cid: lexCid.toString(),\n        mimeType: 'image/png',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects legacy format without cid', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        mimeType: 'image/jpeg',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects legacy format without mimeType', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        cid: blobCid.toString(),\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects legacy format with invalid cid', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        cid: 'invalid-cid',\n        mimeType: 'image/jpeg',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects legacy format with numeric cid', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        cid: 123,\n        mimeType: 'image/jpeg',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects legacy format with extra properties', () => {\n      const schema = blob({ allowLegacy: true })\n      const result = schema.safeParse({\n        cid: blobCid.toString(),\n        mimeType: 'image/jpeg',\n        extra: 'not allowed',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts both BlobRef and LegacyBlobRef formats when allowLegacy is true', () => {\n      const schema = blob({ allowLegacy: true })\n\n      const blobRefResult = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(blobRefResult.success).toBe(true)\n\n      const legacyResult = schema.safeParse({\n        cid: blobCid.toString(),\n        mimeType: 'image/jpeg',\n      })\n      expect(legacyResult.success).toBe(true)\n    })\n  })\n\n  describe('accept and maxSize options', () => {\n    it('accepts blob with accept option (not enforced)', () => {\n      const schema = blob({ accept: ['image/jpeg', 'image/png'] })\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/gif',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts blob with maxSize option (not enforced)', () => {\n      const schema = blob({ maxSize: 1000 })\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts blob matching accept constraint', () => {\n      const schema = blob({ accept: ['image/jpeg', 'image/png'] })\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts blob matching maxSize constraint', () => {\n      const schema = blob({ maxSize: 20000 })\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = blob({})\n\n    it('validates blob with large size', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'video/mp4',\n        size: Number.MAX_SAFE_INTEGER,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects empty object', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object with only $type', () => {\n      const result = schema.safeParse({ $type: 'blob' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with empty mimeType', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: '',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with null ref', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: null,\n        mimeType: 'image/jpeg',\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with null mimeType', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: null,\n        size: 10000,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects blob with null size', () => {\n      const result = schema.safeParse({\n        $type: 'blob',\n        ref: blobCid,\n        mimeType: 'image/jpeg',\n        size: null,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('legacy blob format with strict mode combinations', () => {\n    describe('allowLegacy: false (default)', () => {\n      const schema = blob()\n\n      describe('strict: true (default)', () => {\n        it('rejects legacy blob format', () => {\n          const result = schema.safeParse({\n            cid: blobCid.toString(),\n            mimeType: 'image/jpeg',\n          })\n          expect(result.success).toBe(false)\n        })\n\n        it('accepts standard BlobRef', () => {\n          const result = schema.safeParse({\n            $type: 'blob',\n            ref: blobCid,\n            mimeType: 'image/jpeg',\n            size: 10000,\n          })\n          expect(result.success).toBe(true)\n        })\n      })\n\n      describe('strict: false', () => {\n        it('coerces legacy blob format into BlobRef', () => {\n          const result = schema.safeParse(\n            {\n              cid: blobCid.toString(),\n              mimeType: 'image/jpeg',\n            },\n            { strict: false },\n          )\n          assert(result.success)\n          expect(result.value).toEqual({\n            $type: 'blob',\n            ref: blobCid,\n            mimeType: 'image/jpeg',\n            size: -1,\n          })\n        })\n\n        it('coerces legacy blob format with lexCid', () => {\n          const result = schema.safeParse(\n            {\n              cid: lexCid.toString(),\n              mimeType: 'image/png',\n            },\n            { strict: false },\n          )\n          assert(result.success)\n          expect(result.value).toEqual({\n            $type: 'blob',\n            ref: lexCid,\n            mimeType: 'image/png',\n            size: -1,\n          })\n        })\n\n        it('rejects legacy blob format with invalid cid', () => {\n          const result = schema.safeParse(\n            {\n              cid: 'invalid-cid',\n              mimeType: 'image/jpeg',\n            },\n            { strict: false },\n          )\n          expect(result.success).toBe(false)\n        })\n\n        it('accepts standard BlobRef with non-raw CID', () => {\n          const result = schema.safeParse(\n            {\n              $type: 'blob',\n              ref: lexCid,\n              mimeType: 'image/jpeg',\n              size: 10000,\n            },\n            { strict: false },\n          )\n          expect(result.success).toBe(true)\n        })\n      })\n    })\n\n    describe('allowLegacy: true', () => {\n      const schema = blob({ allowLegacy: true })\n\n      describe('strict: true (default)', () => {\n        it('accepts legacy blob format as LegacyBlobRef', () => {\n          const result = schema.safeParse({\n            cid: blobCid.toString(),\n            mimeType: 'image/jpeg',\n          })\n          assert(result.success)\n          expect('cid' in result.value && result.value.cid).toBe(\n            blobCid.toString(),\n          )\n        })\n\n        it('accepts standard BlobRef', () => {\n          const result = schema.safeParse({\n            $type: 'blob',\n            ref: blobCid,\n            mimeType: 'image/jpeg',\n            size: 10000,\n          })\n          expect(result.success).toBe(true)\n        })\n\n        it('rejects non-raw CID in BlobRef format (strict)', () => {\n          const result = schema.safeParse({\n            $type: 'blob',\n            ref: lexCid,\n            mimeType: 'image/jpeg',\n            size: 10000,\n          })\n          expect(result.success).toBe(false)\n        })\n      })\n\n      describe('strict: false', () => {\n        it('accepts legacy blob format as LegacyBlobRef', () => {\n          const result = schema.safeParse(\n            {\n              cid: blobCid.toString(),\n              mimeType: 'image/jpeg',\n            },\n            { strict: false },\n          )\n          assert(result.success)\n          expect('cid' in result.value && result.value.cid).toBe(\n            blobCid.toString(),\n          )\n        })\n\n        it('accepts standard BlobRef with non-raw CID (non-strict)', () => {\n          const result = schema.safeParse(\n            {\n              $type: 'blob',\n              ref: lexCid,\n              mimeType: 'image/jpeg',\n              size: 10000,\n            },\n            { strict: false },\n          )\n          expect(result.success).toBe(true)\n        })\n\n        it('accepts standard BlobRef with raw CID', () => {\n          const result = schema.safeParse(\n            {\n              $type: 'blob',\n              ref: blobCid,\n              mimeType: 'image/jpeg',\n              size: 10000,\n            },\n            { strict: false },\n          )\n          expect(result.success).toBe(true)\n        })\n      })\n    })\n  })\n\n  describe('mime and size checks depend on strict mode', () => {\n    describe('accept constraint', () => {\n      const schema = blob({ accept: ['image/jpeg', 'image/png'] })\n\n      it('rejects non-matching mime type in strict mode (default)', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/gif',\n          size: 10000,\n        })\n        expect(result.success).toBe(false)\n      })\n\n      it('accepts non-matching mime type in non-strict mode', () => {\n        const result = schema.safeParse(\n          {\n            $type: 'blob',\n            ref: blobCid,\n            mimeType: 'image/gif',\n            size: 10000,\n          },\n          { strict: false },\n        )\n        expect(result.success).toBe(true)\n      })\n\n      it('accepts matching mime type in strict mode', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        })\n        expect(result.success).toBe(true)\n      })\n    })\n\n    describe('maxSize constraint', () => {\n      const schema = blob({ maxSize: 1000 })\n\n      it('rejects oversized blob in strict mode (default)', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 5000,\n        })\n        expect(result.success).toBe(false)\n      })\n\n      it('accepts oversized blob in non-strict mode', () => {\n        const result = schema.safeParse(\n          {\n            $type: 'blob',\n            ref: blobCid,\n            mimeType: 'image/jpeg',\n            size: 5000,\n          },\n          { strict: false },\n        )\n        expect(result.success).toBe(true)\n      })\n\n      it('accepts correctly sized blob in strict mode', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 500,\n        })\n        expect(result.success).toBe(true)\n      })\n    })\n\n    describe('combined accept and maxSize constraints', () => {\n      const schema = blob({\n        accept: ['image/jpeg'],\n        maxSize: 20000,\n      })\n\n      it('accepts valid blob in strict mode', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        })\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects wrong mime in strict mode', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/png',\n          size: 10000,\n        })\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects oversized in strict mode', () => {\n        const result = schema.safeParse({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 30000,\n        })\n        expect(result.success).toBe(false)\n      })\n\n      it('accepts wrong mime and oversized in non-strict mode', () => {\n        const result = schema.safeParse(\n          {\n            $type: 'blob',\n            ref: blobCid,\n            mimeType: 'video/mp4',\n            size: 99999,\n          },\n          { strict: false },\n        )\n        expect(result.success).toBe(true)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/blob.ts",
    "content": "import {\n  BlobRef,\n  LegacyBlobRef,\n  isBlobRef,\n  isLegacyBlobRef,\n  parseCidSafe,\n} from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Configuration options for blob schema validation.\n */\nexport type BlobSchemaOptions = {\n  /**\n   * Whether to allow legacy blob references format\n   *\n   * @default false\n   * @see {@link LegacyBlobRef}\n   */\n  allowLegacy?: boolean\n\n  /**\n   * List of accepted MIME types (supports wildcards like 'image/*' or '*\\/*')\n   *\n   * @default undefined // accepts all MIME types\n   */\n  accept?: string[]\n\n  /**\n   * Maximum blob size in bytes\n   *\n   * @default undefined // no size limit\n   */\n  maxSize?: number\n}\n\nexport type { BlobRef, LegacyBlobRef }\nexport { isBlobRef, isLegacyBlobRef }\n\n/**\n * Schema for validating blob references in AT Protocol.\n *\n * Validates BlobRef objects which contain a CID reference to binary data,\n * along with metadata like MIME type and size. Can optionally accept\n * legacy blob reference format.\n *\n * @template TOptions - The configuration options type\n *\n * @example\n * ```ts\n * const schema = new BlobSchema({ accept: ['image/*'], maxSize: 1000000 })\n * const result = schema.validate(blobRef)\n * ```\n */\nexport class BlobSchema<\n  const TOptions extends BlobSchemaOptions = NonNullable<unknown>,\n> extends Schema<\n  TOptions extends { allowLegacy: true } ? BlobRef | LegacyBlobRef : BlobRef\n> {\n  readonly type = 'blob' as const\n\n  constructor(readonly options?: TOptions) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    const blob = parseValue.call(ctx, input, this.options)\n\n    if (!blob) {\n      return ctx.issueUnexpectedType(input, 'blob')\n    }\n\n    // In non-strict mode, we allow blob refs to pass through without MIME\n    // type or size checks.\n    if (ctx.options.strict) {\n      const accept = this.options?.accept\n      if (accept && !matchesMime(blob.mimeType, accept)) {\n        return ctx.issueInvalidPropertyValue(blob, 'mimeType', accept)\n      }\n\n      const maxSize = this.options?.maxSize\n      if (maxSize != null && 'size' in blob && blob.size > maxSize) {\n        return ctx.issueTooBig(blob, 'blob', maxSize, blob.size)\n      }\n    }\n\n    return ctx.success(blob)\n  }\n\n  matchesMime(mime: string): boolean {\n    const accept = this.options?.accept\n    if (!accept) return true\n    return matchesMime(mime, accept)\n  }\n}\n\nfunction parseValue(\n  this: ValidationContext,\n  input: unknown,\n  options?: BlobSchemaOptions,\n): BlobRef | LegacyBlobRef | null {\n  // If there is a $type property, we treat if as a potential BlobRef and\n  // validate accordingly.\n  if ((input as any)?.$type !== undefined) {\n    // Use the context's option for the \"strict\" check\n    return isBlobRef(input, this.options) ? input : null\n  }\n\n  // If there is no $type property, we may be dealing with a legacy blob ref. If\n  // legacy refs are allowed, validate against the legacy format. If not\n  // allowed, but we are in non-strict \"parse\" mode, coerce legacy refs into\n  // standard BlobRef format for backward compatibility. Otherwise, reject the\n  // value.\n  if (options?.allowLegacy) {\n    if (isLegacyBlobRef(input)) {\n      return input\n    }\n  } else if (!this.options.strict && this.options.mode === 'parse') {\n    if (isLegacyBlobRef(input)) {\n      const { cid, mimeType } = input\n      const ref = parseCidSafe(cid)\n      if (ref) return { $type: 'blob', ref, mimeType, size: -1 }\n    }\n  }\n\n  return null\n}\n\nfunction matchesMime(mime: string, accepted: string[]): boolean {\n  if (accepted.includes('*/*')) return true\n  if (accepted.includes(mime)) return true\n  for (const value of accepted) {\n    if (value.endsWith('/*') && mime.startsWith(value.slice(0, -1))) {\n      return true\n    }\n  }\n  return false\n}\n\n/**\n * Creates a blob schema for validating blob references with optional constraints.\n *\n * Blob references are used in AT Protocol to reference binary data stored\n * separately from records. They contain a CID, MIME type, and size information.\n *\n * @param options - Optional configuration for MIME type filtering and size limits\n * @returns A new {@link BlobSchema} instance\n *\n * @example\n * ```ts\n * // Basic blob reference\n * const fileSchema = l.blob()\n *\n * // Image files only\n * const imageSchema = l.blob({ accept: ['image/png', 'image/jpeg', 'image/gif'] })\n *\n * // Any image type with size limit\n * const avatarSchema = l.blob({ accept: ['image/*'], maxSize: 1000000 })\n *\n * // Allow legacy format\n * const legacySchema = l.blob({ allowLegacy: true })\n * ```\n */\nexport const blob = /*#__PURE__*/ memoizedOptions(function <\n  O extends BlobSchemaOptions = { allowLegacy?: false },\n>(options?: O) {\n  return new BlobSchema(options)\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/boolean.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { boolean } from './boolean.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('BooleanSchema', () => {\n  describe('basic validation', () => {\n    const schema = boolean()\n\n    it('validates true', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('validates false', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(false)\n      }\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('true')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(1)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with default value', () => {\n    it('uses default value of true when input is undefined', () => {\n      const schema = withDefault(boolean(), true)\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('uses default value of false when input is undefined', () => {\n      const schema = withDefault(boolean(), false)\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(false)\n      }\n    })\n\n    it('overrides default value with explicit true', () => {\n      const schema = withDefault(boolean(), false)\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('overrides default value with explicit false', () => {\n      const schema = withDefault(boolean(), true)\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(false)\n      }\n    })\n\n    it('rejects invalid types even with default', () => {\n      const schema = withDefault(boolean(), true)\n      const result = schema.safeParse('not a boolean')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = boolean()\n\n    it('rejects Boolean object', () => {\n      const result = schema.safeParse(new Boolean(true))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects truthy values', () => {\n      const result = schema.safeParse('truthy')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects falsy values', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/boolean.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Schema for validating boolean values.\n *\n * Only accepts JavaScript `true` or `false` values. Does not perform\n * any coercion from strings or numbers.\n *\n * @example\n * ```ts\n * const schema = new BooleanSchema()\n * schema.validate(true)  // success\n * schema.validate(false) // success\n * schema.validate('true') // fails - no string coercion\n * ```\n */\nexport class BooleanSchema extends Schema<boolean> {\n  readonly type = 'boolean' as const\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (typeof input === 'boolean') {\n      return ctx.success(input)\n    }\n\n    return ctx.issueUnexpectedType(input, 'boolean')\n  }\n}\n\n/**\n * Creates a boolean schema that validates true/false values.\n *\n * @returns A new {@link BooleanSchema} instance\n *\n * @example\n * ```ts\n * const enabledSchema = l.boolean()\n *\n * enabledSchema.parse(true)   // true\n * enabledSchema.parse(false)  // false\n * enabledSchema.parse('true') // throws - strings not accepted\n * ```\n */\nexport const boolean = /*#__PURE__*/ memoizedOptions(function () {\n  return new BooleanSchema()\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/bytes.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { bytes } from './bytes.js'\n\ndescribe('BytesSchema', () => {\n  describe('basic validation', () => {\n    const schema = bytes({})\n\n    it('validates Uint8Array', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2, 3]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates empty Uint8Array', () => {\n      const result = schema.safeParse(new Uint8Array([]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates ArrayBuffer', () => {\n      const buffer = new ArrayBuffer(4)\n      const result = schema.safeParse(buffer)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates TypedArray views', () => {\n      const int8 = new Int8Array([1, 2, 3])\n      const result = schema.safeParse(int8)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates Uint16Array', () => {\n      const uint16 = new Uint16Array([1, 2, 3])\n      const result = schema.safeParse(uint16)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates DataView', () => {\n      const buffer = new ArrayBuffer(4)\n      const dataView = new DataView(buffer)\n      const result = schema.safeParse(dataView)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('not bytes')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({ data: [1, 2, 3] })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([1, 2, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('minLength constraint', () => {\n    const schema = bytes({ minLength: 3 })\n\n    it('validates bytes at minimum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates bytes above minimum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2, 3, 4]))\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects bytes below minimum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1]))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty bytes when minLength is set', () => {\n      const result = schema.safeParse(new Uint8Array([]))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('maxLength constraint', () => {\n    const schema = bytes({ maxLength: 5 })\n\n    it('validates bytes at maximum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2, 3, 4]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates bytes below maximum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates empty bytes when only maxLength is set', () => {\n      const result = schema.safeParse(new Uint8Array([]))\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects bytes above maximum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2, 3, 4, 5]))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('minLength and maxLength constraints', () => {\n    const schema = bytes({ minLength: 2, maxLength: 5 })\n\n    it('validates bytes within range', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates bytes at minimum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates bytes at maximum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2, 3, 4]))\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects bytes below minimum length', () => {\n      const result = schema.safeParse(new Uint8Array([0]))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects bytes above maximum length', () => {\n      const result = schema.safeParse(new Uint8Array([0, 1, 2, 3, 4, 5]))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('validates with minLength of 0', () => {\n      const schema = bytes({ minLength: 0 })\n      const result = schema.safeParse(new Uint8Array([]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with maxLength of 0', () => {\n      const schema = bytes({ maxLength: 0 })\n      const result = schema.safeParse(new Uint8Array([]))\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-empty bytes with maxLength of 0', () => {\n      const schema = bytes({ maxLength: 0 })\n      const result = schema.safeParse(new Uint8Array([0]))\n      expect(result.success).toBe(false)\n    })\n\n    it('validates bytes with all zeros', () => {\n      const schema = bytes({})\n      const result = schema.safeParse(new Uint8Array([0, 0, 0, 0]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates bytes with all 255s', () => {\n      const schema = bytes({})\n      const result = schema.safeParse(new Uint8Array([255, 255, 255, 255]))\n      expect(result.success).toBe(true)\n    })\n\n    it('validates large byte arrays', () => {\n      const schema = bytes({})\n      const largeArray = new Uint8Array(10000)\n      const result = schema.safeParse(largeArray)\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('TypedArray coercion', () => {\n    const schema = bytes({})\n\n    it('coerces Int8Array to Uint8Array', () => {\n      const int8 = new Int8Array([1, 2, 3])\n      const result = schema.safeParse(int8)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBeInstanceOf(Uint8Array)\n      }\n    })\n\n    it('coerces Uint16Array to Uint8Array', () => {\n      const uint16 = new Uint16Array([256, 512])\n      const result = schema.safeParse(uint16)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBeInstanceOf(Uint8Array)\n      }\n    })\n\n    it('coerces Float32Array to Uint8Array', () => {\n      const float32 = new Float32Array([1.5, 2.5])\n      const result = schema.safeParse(float32)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBeInstanceOf(Uint8Array)\n      }\n    })\n\n    it('validates coerced TypedArray with length constraints', () => {\n      const schema = bytes({ minLength: 2, maxLength: 10 })\n      const int16 = new Int16Array([1, 2, 3]) // 6 bytes\n      const result = schema.safeParse(int16)\n      expect(result.success).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/bytes.ts",
    "content": "import { asUint8Array, ifUint8Array } from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Configuration options for bytes schema validation.\n *\n * @property minLength - Minimum length in bytes\n * @property maxLength - Maximum length in bytes\n */\nexport type BytesSchemaOptions = {\n  minLength?: number\n  maxLength?: number\n}\n\n/**\n * Schema for validating binary data as Uint8Array with optional length constraints.\n *\n * In \"parse\" mode, coerces various binary formats (Buffer, ArrayBuffer, etc.)\n * into Uint8Array. In \"validate\" mode, only accepts Uint8Array directly.\n *\n * @example\n * ```ts\n * const schema = new BytesSchema({ maxLength: 1024 })\n * const result = schema.validate(new Uint8Array([1, 2, 3]))\n * ```\n */\nexport class BytesSchema extends Schema<Uint8Array> {\n  readonly type = 'bytes' as const\n\n  constructor(readonly options: BytesSchemaOptions = {}) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    // In \"parse\" mode, coerce different binary formats into Uint8Array\n    const bytes =\n      ctx.options.mode === 'parse' ? asUint8Array(input) : ifUint8Array(input)\n    if (!bytes) {\n      return ctx.issueUnexpectedType(input, 'bytes')\n    }\n\n    const { minLength } = this.options\n    if (minLength != null && bytes.length < minLength) {\n      return ctx.issueTooSmall(bytes, 'bytes', minLength, bytes.length)\n    }\n\n    const { maxLength } = this.options\n    if (maxLength != null && bytes.length > maxLength) {\n      return ctx.issueTooBig(bytes, 'bytes', maxLength, bytes.length)\n    }\n\n    return ctx.success(bytes)\n  }\n}\n\n/**\n * Creates a bytes schema for validating binary data with optional length constraints.\n *\n * Validates Uint8Array values and can coerce other binary formats in parse mode.\n *\n * @param options - Optional configuration for minimum and maximum byte length\n * @returns A new {@link BytesSchema} instance\n *\n * @example\n * ```ts\n * // Basic bytes schema\n * const dataSchema = l.bytes()\n *\n * // With size constraints\n * const avatarSchema = l.bytes({ maxLength: 1000000 }) // 1MB max\n *\n * // With minimum size\n * const hashSchema = l.bytes({ minLength: 32, maxLength: 32 }) // Exactly 32 bytes\n * ```\n */\nexport const bytes = /*#__PURE__*/ memoizedOptions(function (\n  options?: BytesSchemaOptions,\n) {\n  return new BytesSchema(options)\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/cid.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport { cid } from './cid.js'\n\nconst cborCid = parseCid(\n  'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n  { flavor: 'cbor' },\n)\n\nconst rawCid = parseCid(\n  'bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4',\n  { flavor: 'raw' },\n)\n\nconst v0Cid = parseCid('QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB')\n\n// Using git-raw codec (0x78) instead of DAG-CBOR or raw binary\nconst gitRawCid = parseCid(\n  'bafybeigvgzoolc3drupxhlevdp2ugqcrbcsqfmcek2zxiw5wctk3xjpjwy',\n)\n\n// Using SHA-512 (0x13) instead of SHA-256\nconst sha512Cid = parseCid(\n  'bafybgqfcn3rz4mdzywp2jb6mjvpdq24rxjvbmdcmizrjdgx2ujjpvj4kxf4d62ywrzm6njk44cxhha4pj3bkvqz2esfgrm7mdkdcqcxjibf7c',\n)\n\ndescribe('CidSchema', () => {\n  describe('default mode (non-strict)', () => {\n    const schema = cid({})\n\n    it('validates CID v1 with DAG-CBOR codec and SHA-256', () => {\n      const result = schema.safeParse(cborCid)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates CID v1 with raw binary codec', () => {\n      const result = schema.safeParse(rawCid)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates CID v0', () => {\n      const result = schema.safeParse(v0Cid)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-CID objects', () => {\n      const result = schema.safeParse({ not: 'a cid' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse(cborCid.toString())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('strict mode', () => {\n    const schema = cid({ flavor: 'dasl' })\n\n    it('validates CID v1 with DAG-CBOR codec and SHA-256', () => {\n      const result = schema.safeParse(cborCid)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates CID v1 with raw binary codec and SHA-256', () => {\n      const result = schema.safeParse(rawCid)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects CID v0', () => {\n      const result = schema.safeParse(v0Cid)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects CID v1 with non-standard codec', () => {\n      const result = schema.safeParse(gitRawCid)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects CID v1 with non-SHA-256 hash', () => {\n      const result = schema.safeParse(sha512Cid)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-CID objects', () => {\n      const result = schema.safeParse({ not: 'a cid' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse(cborCid.toString())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/cid.ts",
    "content": "import { CheckCidOptions, Cid, InferCheckedCid, isCid } from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\nexport type { Cid }\n\n/**\n * Configuration options for CID schema validation.\n *\n * @see CheckCidOptions from @atproto/lex-data for available options\n */\nexport type CidSchemaOptions = CheckCidOptions\n\n/**\n * Schema for validating Content Identifiers (CIDs).\n *\n * CIDs are self-describing content-addressed identifiers used in AT Protocol\n * to reference data by its cryptographic hash. This schema validates that\n * the input is a valid CID object.\n *\n * @template TOptions - The configuration options type\n *\n * @example\n * ```ts\n * const schema = new CidSchema()\n * const result = schema.validate(someCid)\n * ```\n */\nexport class CidSchema<\n  const TOptions extends CidSchemaOptions = { flavor: undefined },\n> extends Schema<InferCheckedCid<TOptions>> {\n  readonly type = 'cid' as const\n\n  constructor(readonly options?: TOptions) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isCid(input, this.options)) {\n      return ctx.issueUnexpectedType(input, 'cid')\n    }\n\n    return ctx.success(input)\n  }\n}\n\n/**\n * Creates a CID schema for validating Content Identifiers.\n *\n * CIDs are used throughout AT Protocol to reference content by its hash.\n * This is commonly used for referencing blobs, commits, and other data.\n *\n * @param options - Optional configuration for CID validation\n * @returns A new {@link CidSchema} instance\n *\n * @example\n * ```ts\n * // Basic CID validation\n * const cidSchema = l.cid()\n *\n * // Validate a CID from a blob reference\n * const result = cidSchema.validate(blobRef.ref)\n * ```\n */\nexport const cid = /*#__PURE__*/ memoizedOptions(function <\n  O extends CidSchemaOptions = NonNullable<unknown>,\n>(options?: O) {\n  return new CidSchema(options)\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/custom.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest'\nimport { IssueCustom } from '../core.js'\nimport { custom } from './custom.js'\n\ndescribe('CustomSchema', () => {\n  describe('basic validation', () => {\n    it('validates input that passes custom assertion', () => {\n      const schema = custom(\n        (input): input is string => typeof input === 'string',\n        'Must be a string',\n      )\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('rejects input that fails custom assertion', () => {\n      const schema = custom(\n        (input): input is string => typeof input === 'string',\n        'Must be a string',\n      )\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('includes custom message in error', () => {\n      const schema = custom(\n        (input): input is string => typeof input === 'string',\n        'Custom error message',\n      )\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n      if (!result.success) {\n        expect(result.reason.message).toContain('Custom error message')\n      }\n    })\n  })\n\n  describe('complex type guards', () => {\n    it('validates objects with specific properties', () => {\n      interface User {\n        name: string\n        age: number\n      }\n\n      const schema = custom((input): input is User => {\n        return (\n          typeof input === 'object' &&\n          input !== null &&\n          'name' in input &&\n          'age' in input &&\n          typeof (input as any).name === 'string' &&\n          typeof (input as any).age === 'number'\n        )\n      }, 'Must be a valid User object')\n\n      expect(schema.matches({ name: 'Alice', age: 30 })).toBe(true)\n    })\n\n    it('rejects objects missing required properties', () => {\n      interface User {\n        name: string\n        age: number\n      }\n\n      const schema = custom((input): input is User => {\n        return (\n          typeof input === 'object' &&\n          input !== null &&\n          'name' in input &&\n          'age' in input &&\n          typeof (input as any).name === 'string' &&\n          typeof (input as any).age === 'number'\n        )\n      }, 'Must be a valid User object')\n\n      expect(schema.matches({ name: 'Alice' })).toBe(false)\n    })\n\n    it('validates arrays with specific element types', () => {\n      const schema = custom((input): input is number[] => {\n        return (\n          Array.isArray(input) &&\n          input.every((item) => typeof item === 'number')\n        )\n      }, 'Must be an array of numbers')\n\n      const result = schema.safeParse([1, 2, 3, 4])\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects arrays with mixed types', () => {\n      const schema = custom((input): input is number[] => {\n        return (\n          Array.isArray(input) &&\n          input.every((item) => typeof item === 'number')\n        )\n      }, 'Must be an array of numbers')\n\n      const result = schema.safeParse([1, 'two', 3])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('custom context usage', () => {\n    it('can add custom issues through context', () => {\n      const schema = custom((input, ctx): input is string => {\n        if (typeof input !== 'string') {\n          ctx.addIssue({\n            code: 'invalid_type',\n            path: ctx.path,\n            input,\n            expected: ['string'],\n          } as any)\n          return false\n        }\n        return true\n      }, 'Must be a string')\n\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('accesses path from context', () => {\n      let capturedPath: any[] = []\n      const schema = custom((input, ctx): input is string => {\n        capturedPath = [...ctx.path]\n        return typeof input === 'string'\n      }, 'Must be a string')\n\n      schema.safeParse('test')\n      expect(capturedPath).toEqual([])\n    })\n\n    it('validates with custom path', () => {\n      const schema = custom(\n        (input): input is string => typeof input === 'string',\n        'Must be a string',\n        'customField',\n      )\n\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n      if (!result.success) {\n        expect(result.reason.message).toContain('customField')\n      }\n    })\n\n    it('validates with array of paths', () => {\n      const schema = custom(\n        (input): input is string => typeof input === 'string',\n        'Must be a string',\n        ['nested', 'field'],\n      )\n\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n      if (!result.success) {\n        expect(result.reason.message).toContain('nested')\n        expect(result.reason.message).toContain('field')\n      }\n    })\n  })\n\n  describe('business logic validation', () => {\n    it('validates email format', () => {\n      const schema = custom((input): input is string => {\n        return (\n          typeof input === 'string' && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(input)\n        )\n      }, 'Must be a valid email address')\n\n      const validResult = schema.safeParse('user@example.com')\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = schema.safeParse('not-an-email')\n      expect(invalidResult.success).toBe(false)\n    })\n\n    it('validates password strength', () => {\n      const schema = custom((input): input is string => {\n        if (typeof input !== 'string') return false\n        return (\n          input.length >= 8 &&\n          /[A-Z]/.test(input) &&\n          /[a-z]/.test(input) &&\n          /[0-9]/.test(input)\n        )\n      }, 'Password must be at least 8 characters with uppercase, lowercase, and numbers')\n\n      const validResult = schema.safeParse('MyPass123')\n      expect(validResult.success).toBe(true)\n\n      const weakResult = schema.safeParse('weak')\n      expect(weakResult.success).toBe(false)\n    })\n\n    it('validates age range', () => {\n      const schema = custom((input): input is number => {\n        return typeof input === 'number' && input >= 18 && input <= 120\n      }, 'Age must be between 18 and 120')\n\n      const validResult = schema.safeParse(25)\n      expect(validResult.success).toBe(true)\n\n      const tooYoungResult = schema.safeParse(15)\n      expect(tooYoungResult.success).toBe(false)\n\n      const tooOldResult = schema.safeParse(150)\n      expect(tooOldResult.success).toBe(false)\n    })\n\n    it('validates positive numbers', () => {\n      const schema = custom((input): input is number => {\n        return typeof input === 'number' && input > 0\n      }, 'Must be a positive number')\n\n      const validResult = schema.safeParse(5)\n      expect(validResult.success).toBe(true)\n\n      const zeroResult = schema.safeParse(0)\n      expect(zeroResult.success).toBe(false)\n\n      const negativeResult = schema.safeParse(-5)\n      expect(negativeResult.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles null input', () => {\n      const schema = custom(\n        (input): input is null => input === null,\n        'Must be null',\n      )\n\n      const validResult = schema.safeParse(null)\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = schema.safeParse(undefined)\n      expect(invalidResult.success).toBe(false)\n    })\n\n    it('handles undefined input', () => {\n      const schema = custom(\n        (input): input is undefined => input === undefined,\n        'Must be undefined',\n      )\n\n      const validResult = schema.safeParse(undefined)\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = schema.safeParse(null)\n      expect(invalidResult.success).toBe(false)\n    })\n\n    it('handles empty string', () => {\n      const schema = custom(\n        (input): input is string =>\n          typeof input === 'string' && input.length > 0,\n        'Must be a non-empty string',\n      )\n\n      const validResult = schema.safeParse('hello')\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = schema.safeParse('')\n      expect(invalidResult.success).toBe(false)\n    })\n\n    it('handles empty array', () => {\n      const schema = custom(\n        (input): input is any[] => Array.isArray(input) && input.length > 0,\n        'Must be a non-empty array',\n      )\n\n      const validResult = schema.safeParse([1, 2, 3])\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = schema.safeParse([])\n      expect(invalidResult.success).toBe(false)\n    })\n\n    it('handles complex nested structures', () => {\n      interface ComplexType {\n        users: Array<{ name: string; email: string }>\n        metadata: { count: number }\n      }\n\n      const schema = custom((input): input is ComplexType => {\n        if (typeof input !== 'object' || input === null) return false\n        const obj = input as any\n        return (\n          Array.isArray(obj.users) &&\n          obj.users.every(\n            (u: any) =>\n              typeof u === 'object' &&\n              typeof u.name === 'string' &&\n              typeof u.email === 'string',\n          ) &&\n          typeof obj.metadata === 'object' &&\n          typeof obj.metadata.count === 'number'\n        )\n      }, 'Must be a valid complex structure')\n\n      const validResult = schema.safeParse({\n        users: [\n          { name: 'Alice', email: 'alice@example.com' },\n          { name: 'Bob', email: 'bob@example.com' },\n        ],\n        metadata: { count: 2 },\n      })\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = schema.safeParse({\n        users: [{ name: 'Alice' }], // missing email\n        metadata: { count: 1 },\n      })\n      expect(invalidResult.success).toBe(false)\n    })\n  })\n\n  describe('type narrowing', () => {\n    it('correctly narrows union types', () => {\n      type StringOrNumber = string | number\n\n      const schema = custom(\n        (input): input is string => typeof input === 'string',\n        'Must be a string',\n      )\n\n      const input: StringOrNumber = 'hello'\n\n      const result = schema.safeParse(input)\n      expect(result.success).toBe(true)\n\n      if (result.success) {\n        // Type should be narrowed to string\n        const value: string = result.value\n        expect(typeof value).toBe('string')\n      }\n    })\n\n    it('validates discriminated unions', () => {\n      type Shape =\n        | { type: 'circle'; radius: number }\n        | { type: 'rectangle'; width: number; height: number }\n\n      const circleSchema = custom((input): input is Shape => {\n        return (\n          typeof input === 'object' &&\n          input !== null &&\n          'type' in input &&\n          (input as any).type === 'circle' &&\n          'radius' in input &&\n          typeof (input as any).radius === 'number'\n        )\n      }, 'Must be a valid circle')\n\n      const validResult = circleSchema.safeParse({ type: 'circle', radius: 5 })\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = circleSchema.safeParse({\n        type: 'rectangle',\n        width: 10,\n        height: 20,\n      })\n      expect(invalidResult.success).toBe(false)\n    })\n  })\n\n  describe('assertion context behavior', () => {\n    it('calls assertion with null as this', () => {\n      const assertion = vi.fn(function (\n        this: unknown,\n        input: unknown,\n      ): input is string {\n        expect(this).toBeNull()\n        return typeof input === 'string'\n      })\n\n      custom(assertion as any, 'Must be a string').safeParse('test')\n\n      expect(assertion).toHaveBeenCalledTimes(1)\n    })\n\n    it('provides addIssue method in context', () => {\n      const schema = custom((input, ctx): input is string => {\n        ctx.addIssue(new IssueCustom(ctx.path, input, 'This is a custom issue'))\n        return false\n      }, 'Must be a string')\n\n      expect(schema.safeParse('test')).toMatchObject({\n        success: false,\n        reason: {\n          issues: [\n            { message: 'This is a custom issue' },\n            { message: 'Must be a string' },\n          ],\n        },\n      })\n    })\n\n    it('provides path array in context', () => {\n      const schema = custom((input, ctx): input is string => {\n        expect(Array.isArray(ctx.path)).toBe(true)\n        return typeof input === 'string'\n      }, 'Must be a string')\n\n      schema.safeParse('test')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/custom.ts",
    "content": "import { Issue, IssueCustom, Schema, ValidationContext } from '../core.js'\n\n/**\n * Context object provided to custom assertion functions.\n *\n * @property path - Current validation path as an array of property keys\n * @property addIssue - Function to add additional validation issues\n */\nexport type CustomAssertionContext = {\n  path: PropertyKey[]\n  addIssue(issue: Issue): void\n}\n\n/**\n * Type guard function for custom schema validation.\n *\n * @template TValue - The type to validate/narrow to\n */\nexport type CustomAssertion<TValue> = (\n  this: null,\n  input: unknown,\n  ctx: CustomAssertionContext,\n) => input is TValue\n\n/**\n * Schema with a custom validation function.\n *\n * Allows defining completely custom validation logic using a type guard\n * assertion function. The function receives the input and validation context,\n * and must return whether the input is valid.\n *\n * @template TValue - The validated output type\n *\n * @example\n * ```ts\n * const schema = new CustomSchema(\n *   (input): input is Date => input instanceof Date,\n *   'Expected a Date instance'\n * )\n * ```\n */\nexport class CustomSchema<out TValue = unknown> extends Schema<TValue> {\n  readonly type = 'custom' as const\n\n  constructor(\n    private readonly assertion: CustomAssertion<TValue>,\n    private readonly message: string,\n    private readonly path?: PropertyKey | readonly PropertyKey[],\n  ) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!this.assertion.call(null, input, ctx)) {\n      const path = ctx.concatPath(this.path)\n      return ctx.issue(new IssueCustom(path, input, this.message))\n    }\n    return ctx.success(input as TValue)\n  }\n}\n\n/**\n * Creates a custom schema with a user-defined validation function.\n *\n * Use this when the built-in schemas don't cover your validation needs.\n * The assertion function must be a type guard that narrows the input type.\n *\n * @param assertion - Type guard function that validates the input\n * @param message - Error message when validation fails\n * @param path - Optional path to associate with validation errors\n * @returns A new {@link CustomSchema} instance\n *\n * @example\n * ```ts\n * // Validate Date instances\n * const dateSchema = l.custom(\n *   (input): input is Date => input instanceof Date && !isNaN(input.getTime()),\n *   'Expected a valid Date'\n * )\n *\n * // Validate specific object shape\n * const pointSchema = l.custom(\n *   (input): input is { x: number; y: number } =>\n *     typeof input === 'object' &&\n *     input !== null &&\n *     typeof (input as any).x === 'number' &&\n *     typeof (input as any).y === 'number',\n *   'Expected a point with x and y coordinates'\n * )\n *\n * // With custom path\n * const validConfig = l.custom(\n *   (input): input is Config => validateConfig(input),\n *   'Invalid configuration',\n *   ['config']\n * )\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function custom<TValue>(\n  assertion: CustomAssertion<TValue>,\n  message: string,\n  path?: PropertyKey | readonly PropertyKey[],\n) {\n  return new CustomSchema<TValue>(assertion, message, path)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/dict.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { boolean } from './boolean.js'\nimport { dict } from './dict.js'\nimport { enumSchema } from './enum.js'\nimport { integer } from './integer.js'\nimport { string } from './string.js'\n\ndescribe('DictSchema', () => {\n  const schema = dict(string(), integer())\n\n  it('validates plain objects with valid keys and values', () => {\n    const result = schema.safeParse({\n      count: 42,\n      total: 100,\n      score: 85,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates empty objects', () => {\n    const result = schema.safeParse({})\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects non-objects', () => {\n    const result = schema.safeParse('not an object')\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects arrays', () => {\n    const result = schema.safeParse([1, 2, 3])\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects null', () => {\n    const result = schema.safeParse(null)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects undefined', () => {\n    const result = schema.safeParse(undefined)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects invalid value types', () => {\n    const result = schema.safeParse({\n      count: 'not a number',\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects when one value is invalid', () => {\n    const result = schema.safeParse({\n      count: 42,\n      invalid: 'string',\n      total: 100,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates with enum key schema', () => {\n    const enumKeySchema = dict(enumSchema(['tag1', 'tag2', 'tag3']), boolean())\n\n    const result = enumKeySchema.safeParse({\n      tag1: true,\n      tag2: false,\n      tag3: true,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects invalid keys with enum key schema', () => {\n    const enumKeySchema = dict(enumSchema(['tag1', 'tag2']), boolean())\n\n    const result = enumKeySchema.safeParse({\n      tag1: true,\n      invalidTag: false,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates nested dict schemas', () => {\n    const nestedSchema = dict(string(), dict(string(), integer()))\n\n    const result = nestedSchema.safeParse({\n      group1: { count: 10, total: 20 },\n      group2: { score: 85 },\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates with string key schema constraints', () => {\n    const constrainedKeySchema = dict(\n      string({ minLength: 3, maxLength: 10 }),\n      integer(),\n    )\n\n    const result = constrainedKeySchema.safeParse({\n      abc: 1,\n      defghij: 2,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects keys that do not meet key schema constraints', () => {\n    const constrainedKeySchema = dict(string({ minLength: 3 }), integer())\n\n    const result = constrainedKeySchema.safeParse({\n      ab: 1, // too short\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates with value schema constraints', () => {\n    const constrainedValueSchema = dict(\n      string(),\n      integer({ minimum: 0, maximum: 100 }),\n    )\n\n    const result = constrainedValueSchema.safeParse({\n      score: 50,\n      total: 100,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects values that do not meet value schema constraints', () => {\n    const constrainedValueSchema = dict(\n      string(),\n      integer({ minimum: 0, maximum: 100 }),\n    )\n\n    const result = constrainedValueSchema.safeParse({\n      score: 150, // too high\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates dict with string values', () => {\n    const stringValueSchema = dict(string(), string())\n\n    const result = stringValueSchema.safeParse({\n      name: 'Alice',\n      city: 'New York',\n      country: 'USA',\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates dict with boolean values', () => {\n    const booleanValueSchema = dict(string(), boolean())\n\n    const result = booleanValueSchema.safeParse({\n      enabled: true,\n      visible: false,\n      active: true,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('handles objects with numeric string keys', () => {\n    const result = schema.safeParse({\n      '0': 1,\n      '1': 2,\n      '2': 3,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('preserves the original object when no transformations occur', () => {\n    const input = { count: 42, total: 100 }\n    const result = schema.safeParse(input)\n\n    if (result.success) {\n      // The implementation returns the same object if no changes are needed\n      expect(result.value).toBe(input)\n    } else {\n      throw new Error('Expected validation to succeed')\n    }\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/dict.ts",
    "content": "import { isPlainObject } from '@atproto/lex-data'\nimport {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n} from '../core.js'\n\n/**\n * Schema for validating dictionary/map-like objects with dynamic keys.\n *\n * Unlike `ObjectSchema` which validates a fixed set of properties, `DictSchema`\n * validates objects where any string key is allowed, with both keys and values\n * validated against their respective schemas.\n *\n * @note There is no dictionary in Lexicon schemas. This is a custom extension\n * to allow map-like objects when using the lex library programmatically (i.e.\n * not code generated from a lexicon schema).\n *\n * @template TKey - The validator type for dictionary keys (must validate strings)\n * @template TValue - The validator type for dictionary values\n *\n * @example\n * ```ts\n * const schema = new DictSchema(l.string(), l.integer())\n * const result = schema.validate({ a: 1, b: 2, c: 3 })\n * ```\n */\nexport class DictSchema<\n  const TKey extends Validator<string> = any,\n  const TValue extends Validator = any,\n> extends Schema<\n  Record<InferInput<TKey>, InferInput<TValue>>,\n  Record<InferInput<TKey>, InferOutput<TValue>>\n> {\n  readonly type = 'dict' as const\n\n  constructor(\n    readonly keySchema: TKey,\n    readonly valueSchema: TValue,\n  ) {\n    super()\n  }\n\n  validateInContext(\n    input: unknown,\n    ctx: ValidationContext,\n    options?: { ignoredKeys?: { has(k: string): boolean } },\n  ) {\n    if (!isPlainObject(input)) {\n      return ctx.issueUnexpectedType(input, 'dict')\n    }\n\n    let copy: undefined | Record<string, unknown>\n\n    for (const key in input) {\n      if (options?.ignoredKeys?.has(key)) continue\n\n      const keyResult = ctx.validate(key, this.keySchema)\n      if (!keyResult.success) return keyResult\n      if (keyResult.value !== key) {\n        // We can't safely \"move\" the key to a different name in the output\n        // object (because there may already be something there), so we issue a\n        // \"required key\" error if the key validation changes the key\n        return ctx.issueRequiredKey(input, key)\n      }\n\n      const valueResult = ctx.validateChild(input, key, this.valueSchema)\n      if (!valueResult.success) return valueResult\n\n      if (!Object.is(valueResult.value, input[key])) {\n        if (ctx.options.mode === 'validate') {\n          // In \"validate\" mode, we can't modify the input, so we issue an error\n          return ctx.issueInvalidPropertyValue(input, key, [valueResult.value])\n        }\n\n        copy ??= { ...input }\n        copy[key] = valueResult.value\n      }\n    }\n\n    return ctx.success(copy ?? input)\n  }\n}\n\n/**\n * Creates a dictionary schema for validating map-like objects.\n *\n * Validates objects where all keys match the key schema and all values\n * match the value schema. Useful for dynamic key-value mappings.\n *\n * @param key - Schema to validate each key (must be a string validator)\n * @param value - Schema to validate each value\n * @returns A new {@link DictSchema} instance\n *\n * @example\n * ```ts\n * // String to number mapping\n * const scoresSchema = l.dict(l.string(), l.integer())\n * scoresSchema.parse({ alice: 100, bob: 85 })\n *\n * // Constrained keys\n * const langSchema = l.dict(\n *   l.string({ minLength: 2, maxLength: 5 }), // Language codes\n *   l.string() // Translations\n * )\n *\n * // Complex values\n * const usersById = l.dict(\n *   l.string({ format: 'did' }),\n *   l.object({ name: l.string(), age: l.integer() })\n * )\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function dict<\n  const TKey extends Validator<string>,\n  const TValue extends Validator,\n>(key: TKey, value: TValue) {\n  return new DictSchema<TKey, TValue>(key, value)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/discriminated-union.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { discriminatedUnion } from './discriminated-union.js'\nimport { enumSchema } from './enum.js'\nimport { integer } from './integer.js'\nimport { literal } from './literal.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\n\ndescribe('DiscriminatedUnionSchema', () => {\n  describe('with literal discriminators', () => {\n    const schema = discriminatedUnion('type', [\n      object({\n        type: literal('cat'),\n        meow: string(),\n      }),\n      object({\n        type: literal('dog'),\n        bark: string(),\n      }),\n    ])\n\n    it('validates first variant', () => {\n      const result = schema.safeParse({\n        type: 'cat',\n        meow: 'meow',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second variant', () => {\n      const result = schema.safeParse({\n        type: 'dog',\n        bark: 'woof',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-objects', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([{ type: 'cat', meow: 'meow' }])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects missing discriminator', () => {\n      const result = schema.safeParse({\n        meow: 'meow',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with invalid discriminator value', () => {\n      const result = schema.safeParse({\n        type: 'bird',\n        chirp: 'tweet',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with valid discriminator but invalid properties', () => {\n      const result = schema.safeParse({\n        type: 'cat',\n        meow: 123,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with valid discriminator but missing required properties', () => {\n      const result = schema.safeParse({\n        type: 'cat',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('allows extra properties', () => {\n      const result = schema.safeParse({\n        type: 'cat',\n        meow: 'meow',\n        extra: 'property',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('with enum discriminators', () => {\n    const schema = discriminatedUnion('status', [\n      object({\n        status: enumSchema(['pending', 'processing']),\n        progress: integer(),\n      }),\n      object({\n        status: enumSchema(['complete', 'failed']),\n        result: string(),\n      }),\n    ])\n\n    it('validates first variant with first enum value', () => {\n      const result = schema.safeParse({\n        status: 'pending',\n        progress: 0,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates first variant with second enum value', () => {\n      const result = schema.safeParse({\n        status: 'processing',\n        progress: 50,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second variant with first enum value', () => {\n      const result = schema.safeParse({\n        status: 'complete',\n        result: 'success',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second variant with second enum value', () => {\n      const result = schema.safeParse({\n        status: 'failed',\n        result: 'error',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid discriminator value', () => {\n      const result = schema.safeParse({\n        status: 'unknown',\n        progress: 0,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object missing discriminator', () => {\n      const result = schema.safeParse({\n        progress: 0,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object with valid discriminator but wrong properties', () => {\n      const result = schema.safeParse({\n        status: 'pending',\n        result: 'success',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with mixed literal and enum discriminators', () => {\n    const schema = discriminatedUnion('kind', [\n      object({\n        kind: literal('simple'),\n        value: string(),\n      }),\n      object({\n        kind: enumSchema(['complex', 'advanced']),\n        value: integer(),\n      }),\n    ])\n\n    it('validates literal discriminator variant', () => {\n      const result = schema.safeParse({\n        kind: 'simple',\n        value: 'text',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates enum discriminator variant with first value', () => {\n      const result = schema.safeParse({\n        kind: 'complex',\n        value: 42,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates enum discriminator variant with second value', () => {\n      const result = schema.safeParse({\n        kind: 'advanced',\n        value: 100,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid discriminator value', () => {\n      const result = schema.safeParse({\n        kind: 'unknown',\n        value: 'test',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with single variant', () => {\n    const schema = discriminatedUnion('type', [\n      object({\n        type: literal('only'),\n        value: string(),\n      }),\n    ])\n\n    it('validates the single variant', () => {\n      const result = schema.safeParse({\n        type: 'only',\n        value: 'test',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects other discriminator values', () => {\n      const result = schema.safeParse({\n        type: 'other',\n        value: 'test',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with three variants', () => {\n    const schema = discriminatedUnion('shape', [\n      object({\n        shape: literal('circle'),\n        radius: integer(),\n      }),\n      object({\n        shape: literal('square'),\n        side: integer(),\n      }),\n      object({\n        shape: literal('rectangle'),\n        width: integer(),\n        height: integer(),\n      }),\n    ])\n\n    it('validates first variant', () => {\n      const result = schema.safeParse({\n        shape: 'circle',\n        radius: 10,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second variant', () => {\n      const result = schema.safeParse({\n        shape: 'square',\n        side: 5,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates third variant', () => {\n      const result = schema.safeParse({\n        shape: 'rectangle',\n        width: 10,\n        height: 20,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid discriminator', () => {\n      const result = schema.safeParse({\n        shape: 'triangle',\n        sides: 3,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with number discriminators', () => {\n    const schema = discriminatedUnion('version', [\n      object({\n        version: literal(1),\n        oldFormat: string(),\n      }),\n      object({\n        version: literal(2),\n        newFormat: string(),\n      }),\n    ])\n\n    it('validates first version', () => {\n      const result = schema.safeParse({\n        version: 1,\n        oldFormat: 'data',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second version', () => {\n      const result = schema.safeParse({\n        version: 2,\n        newFormat: 'data',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid version number', () => {\n      const result = schema.safeParse({\n        version: 3,\n        format: 'data',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string version', () => {\n      const result = schema.safeParse({\n        version: '1',\n        oldFormat: 'data',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with boolean discriminators', () => {\n    const schema = discriminatedUnion('enabled', [\n      object({\n        enabled: literal(true),\n        config: string(),\n      }),\n      object({\n        enabled: literal(false),\n        reason: string(),\n      }),\n    ])\n\n    it('validates true variant', () => {\n      const result = schema.safeParse({\n        enabled: true,\n        config: 'settings',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates false variant', () => {\n      const result = schema.safeParse({\n        enabled: false,\n        reason: 'disabled',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects string boolean', () => {\n      const result = schema.safeParse({\n        enabled: 'true',\n        config: 'settings',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with null discriminator', () => {\n    const schema = discriminatedUnion('value', [\n      object({\n        value: literal(null),\n        empty: string(),\n      }),\n      object({\n        value: literal('present'),\n        data: string(),\n      }),\n    ])\n\n    it('validates null discriminator variant', () => {\n      const result = schema.safeParse({\n        value: null,\n        empty: 'nothing',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates non-null discriminator variant', () => {\n      const result = schema.safeParse({\n        value: 'present',\n        data: 'something',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects undefined discriminator', () => {\n      const result = schema.safeParse({\n        value: undefined,\n        empty: 'nothing',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('constructor validation', () => {\n    it('throws on overlapping literal discriminator values', () => {\n      expect(() => {\n        discriminatedUnion('type', [\n          object({\n            type: literal('duplicate'),\n            a: string(),\n          }),\n          object({\n            type: literal('duplicate'),\n            b: string(),\n          }),\n        ])\n      }).toThrow('Overlapping discriminator value: duplicate')\n    })\n\n    it('throws on overlapping enum discriminator values', () => {\n      expect(() => {\n        discriminatedUnion('status', [\n          object({\n            status: enumSchema(['active', 'pending']),\n            a: string(),\n          }),\n          object({\n            status: enumSchema(['pending', 'complete']),\n            b: string(),\n          }),\n        ])\n      }).toThrow('Overlapping discriminator value: pending')\n    })\n\n    it('throws on overlapping literal and enum discriminator values', () => {\n      expect(() => {\n        discriminatedUnion('kind', [\n          object({\n            kind: literal('test'),\n            a: string(),\n          }),\n          object({\n            kind: enumSchema(['test', 'other']),\n            b: string(),\n          }),\n        ])\n      }).toThrow('Overlapping discriminator value: test')\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = discriminatedUnion('type', [\n      object({\n        type: literal('a'),\n        value: string(),\n      }),\n      object({\n        type: literal('b'),\n        value: integer(),\n      }),\n    ])\n\n    it('rejects empty object', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object with null discriminator', () => {\n      const result = schema.safeParse({\n        type: null,\n        value: 'test',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object with undefined discriminator', () => {\n      const result = schema.safeParse({\n        type: undefined,\n        value: 'test',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects primitive values', () => {\n      expect(schema.safeParse(42).success).toBe(false)\n      expect(schema.safeParse('string').success).toBe(false)\n      expect(schema.safeParse(true).success).toBe(false)\n    })\n\n    it('handles empty string discriminator', () => {\n      const emptySchema = discriminatedUnion('key', [\n        object({\n          key: literal(''),\n          value: string(),\n        }),\n      ])\n\n      const result = emptySchema.safeParse({\n        key: '',\n        value: 'test',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('handles zero discriminator', () => {\n      const zeroSchema = discriminatedUnion('count', [\n        object({\n          count: literal(0),\n          value: string(),\n        }),\n      ])\n\n      const result = zeroSchema.safeParse({\n        count: 0,\n        value: 'test',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('handles false discriminator', () => {\n      const falseSchema = discriminatedUnion('flag', [\n        object({\n          flag: literal(false),\n          value: string(),\n        }),\n      ])\n\n      const result = falseSchema.safeParse({\n        flag: false,\n        value: 'test',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects class instances', () => {\n      class CustomClass {\n        type = 'a'\n        value = 'test'\n      }\n      const result = schema.safeParse(new CustomClass())\n      expect(result.success).toBe(false)\n    })\n\n    it('handles discriminator with special characters', () => {\n      const specialSchema = discriminatedUnion('$type', [\n        object({\n          $type: literal('test'),\n          value: string(),\n        }),\n      ])\n\n      const result = specialSchema.safeParse({\n        $type: 'test',\n        value: 'data',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('handles objects with prototype properties', () => {\n      const obj = Object.create({ type: 'a' })\n      obj.value = 'test'\n      // Should fail because discriminator is not an own property\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates object with discriminator as own property', () => {\n      const obj = Object.create(null)\n      obj.type = 'a'\n      obj.value = 'test'\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('complex nested structures', () => {\n    const schema = discriminatedUnion('type', [\n      object({\n        type: literal('user'),\n        name: string(),\n        age: integer(),\n      }),\n      object({\n        type: literal('post'),\n        title: string(),\n        content: string(),\n      }),\n    ])\n\n    it('validates complex user object', () => {\n      const result = schema.safeParse({\n        type: 'user',\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates complex post object', () => {\n      const result = schema.safeParse({\n        type: 'post',\n        title: 'Hello World',\n        content: 'This is a test post',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects user with missing age', () => {\n      const result = schema.safeParse({\n        type: 'user',\n        name: 'Alice',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects post with invalid content type', () => {\n      const result = schema.safeParse({\n        type: 'post',\n        title: 'Hello World',\n        content: 123,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects mixed properties from different variants', () => {\n      const result = schema.safeParse({\n        type: 'user',\n        name: 'Alice',\n        title: 'Hello World',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('discriminator key variations', () => {\n    it('works with different discriminator key names', () => {\n      const kindSchema = discriminatedUnion('kind', [\n        object({\n          kind: literal('a'),\n          value: string(),\n        }),\n      ])\n\n      const tagSchema = discriminatedUnion('tag', [\n        object({\n          tag: literal('a'),\n          value: string(),\n        }),\n      ])\n\n      expect(kindSchema.safeParse({ kind: 'a', value: 'test' }).success).toBe(\n        true,\n      )\n      expect(tagSchema.safeParse({ tag: 'a', value: 'test' }).success).toBe(\n        true,\n      )\n    })\n\n    it('rejects when discriminator key does not match schema', () => {\n      const schema = discriminatedUnion('type', [\n        object({\n          type: literal('a'),\n          value: string(),\n        }),\n      ])\n\n      const result = schema.safeParse({\n        kind: 'a',\n        value: 'test',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/discriminated-union.ts",
    "content": "import { isPlainObject } from '@atproto/lex-data'\nimport {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  ValidationResult,\n  Validator,\n} from '../core.js'\nimport { EnumSchema } from './enum.js'\nimport { LiteralSchema } from './literal.js'\nimport { ObjectSchema } from './object.js'\n\n/**\n * Type representing a single variant in a discriminated union.\n *\n * Must be an ObjectSchema with the discriminator property using either\n * a LiteralSchema or EnumSchema.\n *\n * @template Discriminator - The discriminator property name\n */\nexport type DiscriminatedUnionVariant<Discriminator extends string = string> =\n  ObjectSchema<Record<Discriminator, EnumSchema<any> | LiteralSchema<any>>>\n\n/**\n * Type representing a non-empty tuple of discriminated union variants.\n *\n * @template TDiscriminator - The discriminator property name\n */\nexport type DiscriminatedUnionVariants<TDiscriminator extends string> =\n  readonly [\n    DiscriminatedUnionVariant<TDiscriminator>,\n    ...DiscriminatedUnionVariant<TDiscriminator>[],\n  ]\n\ntype DiscriminatedUnionSchemaInput<TVariants extends readonly Validator[]> =\n  TVariants extends readonly [\n    infer TValidator extends Validator,\n    ...infer TRest extends readonly Validator[],\n  ]\n    ? InferInput<TValidator> | DiscriminatedUnionSchemaInput<TRest>\n    : never\n\ntype DiscriminatedUnionSchemaOutput<TVariants extends readonly Validator[]> =\n  TVariants extends readonly [\n    infer TValidator extends Validator,\n    ...infer TRest extends readonly Validator[],\n  ]\n    ? InferOutput<TValidator> | DiscriminatedUnionSchemaOutput<TRest>\n    : never\n\n/**\n * Schema for validating discriminated unions of objects.\n *\n * More efficient than regular union schemas when discriminating on a known\n * property. Looks up the correct variant schema directly based on the\n * discriminator value instead of trying each variant in sequence.\n *\n * @note There is no discriminated union in Lexicon schemas. This is a custom\n * extension to allow optimized validation of union of objects when using the\n * lex library programmatically (i.e. not code generated from a lexicon schema).\n *\n * @template TDiscriminator - The discriminator property name\n * @template TVariants - Tuple type of the variant schemas\n *\n * @example\n * ```ts\n * const schema = new DiscriminatedUnionSchema('type', [\n *   l.object({ type: l.literal('text'), content: l.string() }),\n *   l.object({ type: l.literal('image'), url: l.string() }),\n * ])\n * ```\n */\nexport class DiscriminatedUnionSchema<\n  const TDiscriminator extends string,\n  const TVariants extends DiscriminatedUnionVariants<TDiscriminator>,\n> extends Schema<\n  DiscriminatedUnionSchemaInput<TVariants>,\n  DiscriminatedUnionSchemaOutput<TVariants>\n> {\n  readonly type = 'discriminatedUnion' as const\n\n  readonly variantsMap: Map<unknown, DiscriminatedUnionVariant<TDiscriminator>>\n\n  constructor(\n    readonly discriminator: TDiscriminator,\n    readonly variants: TVariants,\n  ) {\n    super()\n\n    // Although we usually try to avoid initialization work in constructors,\n    // here it is necessary to ensure that invalid discriminated throw from the\n    // place of construction, rather than later during validation.\n    this.variantsMap = buildVariantsMap(discriminator, variants)\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isPlainObject(input)) {\n      return ctx.issueUnexpectedType(input, 'object')\n    }\n\n    const { discriminator } = this\n\n    if (!Object.hasOwn(input, discriminator)) {\n      return ctx.issueRequiredKey(input, discriminator)\n    }\n\n    const discriminatorValue = input[discriminator]\n\n    const variant = this.variantsMap.get(discriminatorValue)\n    if (variant) {\n      return ctx.validate(input, variant) as ValidationResult<\n        DiscriminatedUnionSchemaInput<TVariants>\n      >\n    }\n\n    return ctx.issueInvalidPropertyValue(input, discriminator, [\n      ...this.variantsMap.keys(),\n    ])\n  }\n}\n\nfunction buildVariantsMap<Discriminator extends string>(\n  discriminator: Discriminator,\n  variants: DiscriminatedUnionVariants<Discriminator>,\n) {\n  const variantsMap = new Map<\n    unknown,\n    DiscriminatedUnionVariant<Discriminator>\n  >()\n\n  for (const variant of variants) {\n    const schema = variant.shape[discriminator]\n    if (schema instanceof LiteralSchema) {\n      if (variantsMap.has(schema.value)) {\n        throw new TypeError(`Overlapping discriminator value: ${schema.value}`)\n      }\n      variantsMap.set(schema.value, variant)\n    } else if (schema instanceof EnumSchema) {\n      for (const val of schema.values) {\n        if (variantsMap.has(val)) {\n          throw new TypeError(`Overlapping discriminator value: ${val}`)\n        }\n        variantsMap.set(val, variant)\n      }\n    } else {\n      // Only enumerable discriminator schemas are supported\n\n      // Should never happen if types are used correctly\n      throw new TypeError(\n        `Discriminator schema must be a LiteralSchema or EnumSchema`,\n      )\n    }\n  }\n\n  return variantsMap\n}\n\n/**\n * Creates a discriminated union schema for efficient object type switching.\n *\n * Unlike regular `union()`, this schema uses a discriminator property to\n * directly look up the correct variant, providing O(1) validation instead\n * of trying each variant sequentially.\n *\n * @param discriminator - Property name to discriminate on\n * @param variants - Non-empty array of object schemas with the discriminator property\n * @returns A new {@link DiscriminatedUnionSchema} instance\n *\n * @example\n * ```ts\n * // Message types discriminated by 'kind'\n * const messageSchema = l.discriminatedUnion('kind', [\n *   l.object({ kind: l.literal('text'), text: l.string() }),\n *   l.object({ kind: l.literal('image'), url: l.string(), alt: l.optional(l.string()) }),\n *   l.object({ kind: l.literal('video'), url: l.string(), duration: l.integer() }),\n * ])\n *\n * // Using enums for multiple values mapping to same variant\n * const statusSchema = l.discriminatedUnion('status', [\n *   l.object({ status: l.enum(['pending', 'processing']), startedAt: l.string() }),\n *   l.object({ status: l.literal('completed'), completedAt: l.string() }),\n *   l.object({ status: l.literal('failed'), error: l.string() }),\n * ])\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function discriminatedUnion<\n  const Discriminator extends string,\n  const Options extends DiscriminatedUnionVariants<Discriminator>,\n>(discriminator: Discriminator, variants: Options) {\n  return new DiscriminatedUnionSchema<Discriminator, Options>(\n    discriminator,\n    variants,\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/enum.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { enumSchema } from './enum.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('EnumSchema', () => {\n  describe('with string values', () => {\n    const schema = enumSchema(['male', 'female', 'other'])\n\n    it('validates matching string values', () => {\n      const result = schema.safeParse('male')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates all enum values', () => {\n      expect(schema.safeParse('male').success).toBe(true)\n      expect(schema.safeParse('female').success).toBe(true)\n      expect(schema.safeParse('other').success).toBe(true)\n    })\n\n    it('rejects non-matching string values', () => {\n      const result = schema.safeParse('unknown')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({ value: 'male' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['male'])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string when not in enum', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with number values', () => {\n    const schema = enumSchema([1, 2, 3])\n\n    it('validates matching number values', () => {\n      const result = schema.safeParse(1)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates all enum values', () => {\n      expect(schema.safeParse(1).success).toBe(true)\n      expect(schema.safeParse(2).success).toBe(true)\n      expect(schema.safeParse(3).success).toBe(true)\n    })\n\n    it('rejects non-matching number values', () => {\n      const result = schema.safeParse(4)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string numbers', () => {\n      const result = schema.safeParse('1')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero when not in enum', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects negative numbers when not in enum', () => {\n      const result = schema.safeParse(-1)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with boolean values', () => {\n    const schema = enumSchema([true, false])\n\n    it('validates true', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates false', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects string booleans', () => {\n      expect(schema.safeParse('true').success).toBe(false)\n      expect(schema.safeParse('false').success).toBe(false)\n    })\n\n    it('rejects number booleans', () => {\n      expect(schema.safeParse(1).success).toBe(false)\n      expect(schema.safeParse(0).success).toBe(false)\n    })\n  })\n\n  describe('with single boolean value', () => {\n    const schema = enumSchema([true])\n\n    it('validates true', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects false when not in enum', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with null value', () => {\n    const schema = enumSchema([null, 'value'])\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates other enum values', () => {\n      const result = schema.safeParse('value')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with mixed type values', () => {\n    const schema = enumSchema(['string', 123, true, null])\n\n    it('validates string value', () => {\n      const result = schema.safeParse('string')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates number value', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates boolean value', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates null value', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-matching values', () => {\n      expect(schema.safeParse('other').success).toBe(false)\n      expect(schema.safeParse(456).success).toBe(false)\n      expect(schema.safeParse(false).success).toBe(false)\n      expect(schema.safeParse(undefined).success).toBe(false)\n    })\n  })\n\n  describe('with default option', () => {\n    const schema = withDefault(enumSchema(['red', 'green', 'blue']), 'red')\n\n    it('validates matching values', () => {\n      const result = schema.safeParse('green')\n      expect(result.success).toBe(true)\n    })\n\n    it('uses default when input is undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('red')\n      }\n    })\n\n    it('uses default when no argument is passed', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('red')\n      }\n    })\n\n    it('does not use default for null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('does not use default for invalid values', () => {\n      const result = schema.safeParse('yellow')\n      expect(result.success).toBe(false)\n    })\n\n    it('does not use default for empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with default option as number', () => {\n    const schema = withDefault(enumSchema([1, 2, 3]), 1)\n\n    it('uses default when input is undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(1)\n      }\n    })\n\n    it('does not use default for zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with default option as boolean', () => {\n    const schema = withDefault(enumSchema([true, false]), false)\n\n    it('uses default when input is undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(false)\n      }\n    })\n\n    it('validates true even when default is false', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n  })\n\n  describe('with default option as null', () => {\n    const schema = withDefault(enumSchema([null, 'value']), null)\n\n    it('uses default when input is undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('validates explicit null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n  })\n\n  describe('with single value', () => {\n    const schema = enumSchema(['only'])\n\n    it('validates the single value', () => {\n      const result = schema.safeParse('only')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects any other value', () => {\n      expect(schema.safeParse('other').success).toBe(false)\n      expect(schema.safeParse('').success).toBe(false)\n      expect(schema.safeParse(null).success).toBe(false)\n      expect(schema.safeParse(undefined).success).toBe(false)\n    })\n  })\n\n  describe('with empty string value', () => {\n    const schema = enumSchema(['', 'value'])\n\n    it('validates empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates other values', () => {\n      const result = schema.safeParse('value')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-matching values', () => {\n      const result = schema.safeParse('other')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with zero value', () => {\n    const schema = enumSchema([0, 1, 2])\n\n    it('validates zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates other values', () => {\n      expect(schema.safeParse(1).success).toBe(true)\n      expect(schema.safeParse(2).success).toBe(true)\n    })\n\n    it('rejects false even though zero is in enum', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('case sensitivity', () => {\n    const schema = enumSchema(['Value', 'VALUE', 'value'])\n\n    it('validates exact case matches', () => {\n      expect(schema.safeParse('Value').success).toBe(true)\n      expect(schema.safeParse('VALUE').success).toBe(true)\n      expect(schema.safeParse('value').success).toBe(true)\n    })\n\n    it('rejects case mismatches', () => {\n      expect(schema.safeParse('vaLue').success).toBe(false)\n      expect(schema.safeParse('VaLuE').success).toBe(false)\n    })\n  })\n\n  describe('with special string values', () => {\n    const schema = enumSchema([\n      'with space',\n      'with\\ttab',\n      'with\\nnewline',\n      '123',\n      'true',\n      'null',\n      'undefined',\n    ])\n\n    it('validates strings with spaces', () => {\n      const result = schema.safeParse('with space')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings with tabs', () => {\n      const result = schema.safeParse('with\\ttab')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings with newlines', () => {\n      const result = schema.safeParse('with\\nnewline')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates number-like strings', () => {\n      const result = schema.safeParse('123')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects actual numbers for number-like strings', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates keyword strings', () => {\n      expect(schema.safeParse('true').success).toBe(true)\n      expect(schema.safeParse('null').success).toBe(true)\n      expect(schema.safeParse('undefined').success).toBe(true)\n    })\n\n    it('rejects actual boolean/null/undefined for keyword strings', () => {\n      expect(schema.safeParse(true).success).toBe(false)\n      expect(schema.safeParse(null).success).toBe(false)\n      expect(schema.safeParse(undefined).success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/enum.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\n\n/**\n * Schema that accepts one of several specific literal values.\n *\n * Validates that the input matches one of the allowed values using strict\n * equality. Similar to TypeScript union of literals.\n *\n * @template TValue - The union of literal types\n *\n * @example\n * ```ts\n * const schema = new EnumSchema(['pending', 'active', 'completed'])\n * schema.validate('active')  // success\n * schema.validate('invalid') // fails\n * ```\n */\nexport class EnumSchema<\n  const TValue extends null | string | number | boolean,\n> extends Schema<TValue> {\n  readonly type = 'enum' as const\n\n  constructor(readonly values: readonly TValue[]) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!(this.values as readonly unknown[]).includes(input)) {\n      return ctx.issueInvalidValue(input, this.values)\n    }\n\n    return ctx.success(input as TValue)\n  }\n}\n\n/**\n * Creates an enum schema that accepts one of the specified values.\n *\n * Similar to TypeScript's union of string literals. Use `l.enum()` for\n * the namespace-friendly alias.\n *\n * @param value - Array of allowed values\n * @returns A new {@link EnumSchema} instance\n *\n * @example\n * ```ts\n * // String enum\n * const statusSchema = l.enum(['pending', 'active', 'completed', 'failed'])\n *\n * // Number enum\n * const prioritySchema = l.enum([1, 2, 3, 4, 5])\n *\n * // Mixed types\n * const mixedSchema = l.enum(['auto', 0, 1, true])\n *\n * // Use in objects\n * const taskSchema = l.object({\n *   title: l.string(),\n *   status: l.enum(['todo', 'in-progress', 'done']),\n * })\n *\n * // In discriminated unions\n * const resultSchema = l.discriminatedUnion('status', [\n *   l.object({ status: l.enum(['pending', 'processing']), progress: l.integer() }),\n *   l.object({ status: l.literal('completed'), result: l.unknown() }),\n * ])\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function enumSchema<const V extends null | string | number | boolean>(\n  value: readonly V[],\n) {\n  return new EnumSchema<V>(value)\n}\n\n// @NOTE \"enum\" is a reserved keyword in JS/TS\nexport { enumSchema as enum }\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/integer.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('IntegerSchema', () => {\n  describe('basic validation', () => {\n    const schema = integer()\n\n    it('validates integers', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates negative integers', () => {\n      const result = schema.safeParse(-42)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates large integers', () => {\n      const result = schema.safeParse(Number.MAX_SAFE_INTEGER)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates small integers', () => {\n      const result = schema.safeParse(Number.MIN_SAFE_INTEGER)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects floats', () => {\n      const result = schema.safeParse(3.14)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('42')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([42])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects NaN', () => {\n      const result = schema.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Infinity', () => {\n      const result = schema.safeParse(Infinity)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects -Infinity', () => {\n      const result = schema.safeParse(-Infinity)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('default value', () => {\n    const schema = withDefault(integer(), 10)\n\n    it('uses default when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result).toMatchObject({\n        success: true,\n        value: 10,\n      })\n    })\n\n    it('does not use default when explicit value is provided', () => {\n      const result = schema.safeParse(20)\n      expect(result).toMatchObject({\n        success: true,\n        value: 20,\n      })\n    })\n\n    it('does not use default when zero is provided', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(0)\n      }\n    })\n  })\n\n  describe('minimum constraint', () => {\n    const schema = integer({ minimum: 10 })\n\n    it('accepts values equal to minimum', () => {\n      const result = schema.safeParse(10)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts values greater than minimum', () => {\n      const result = schema.safeParse(20)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values less than minimum', () => {\n      const result = schema.safeParse(5)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero when minimum is positive', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects negative values when minimum is positive', () => {\n      const result = schema.safeParse(-10)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('maximum constraint', () => {\n    const schema = integer({ maximum: 100 })\n\n    it('accepts values equal to maximum', () => {\n      const result = schema.safeParse(100)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts values less than maximum', () => {\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values greater than maximum', () => {\n      const result = schema.safeParse(150)\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts zero when maximum is positive', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts negative values when maximum is positive', () => {\n      const result = schema.safeParse(-10)\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('minimum and maximum constraints', () => {\n    const schema = integer({ minimum: 10, maximum: 100 })\n\n    it('accepts values within range', () => {\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts minimum value', () => {\n      const result = schema.safeParse(10)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts maximum value', () => {\n      const result = schema.safeParse(100)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values below minimum', () => {\n      const result = schema.safeParse(5)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values above maximum', () => {\n      const result = schema.safeParse(150)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('negative range constraints', () => {\n    const schema = integer({ minimum: -100, maximum: -10 })\n\n    it('accepts negative values within range', () => {\n      const result = schema.safeParse(-50)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts minimum negative value', () => {\n      const result = schema.safeParse(-100)\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts maximum negative value', () => {\n      const result = schema.safeParse(-10)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values below minimum', () => {\n      const result = schema.safeParse(-150)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values above maximum', () => {\n      const result = schema.safeParse(-5)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects positive values', () => {\n      const result = schema.safeParse(10)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('zero constraints', () => {\n    const schema = integer({ minimum: 0, maximum: 0 })\n\n    it('accepts zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects positive values', () => {\n      const result = schema.safeParse(1)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects negative values', () => {\n      const result = schema.safeParse(-1)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('combined with default value', () => {\n    const schema = withDefault(integer({ minimum: 10, maximum: 100 }), 50)\n\n    it('uses default when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(50)\n      }\n    })\n\n    it('validates explicit values with constraints', () => {\n      const result = schema.safeParse(75)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects explicit values outside constraints', () => {\n      const result = schema.safeParse(5)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles minimum of 0', () => {\n      const schema = integer({ minimum: 0 })\n      expect(schema.safeParse(0).success).toBe(true)\n      expect(schema.safeParse(-1).success).toBe(false)\n      expect(schema.safeParse(1).success).toBe(true)\n    })\n\n    it('handles maximum of 0', () => {\n      const schema = integer({ maximum: 0 })\n      expect(schema.safeParse(0).success).toBe(true)\n      expect(schema.safeParse(1).success).toBe(false)\n      expect(schema.safeParse(-1).success).toBe(true)\n    })\n\n    it('handles very large ranges', () => {\n      const schema = integer({\n        minimum: Number.MIN_SAFE_INTEGER,\n        maximum: Number.MAX_SAFE_INTEGER,\n      })\n      expect(schema.safeParse(Number.MIN_SAFE_INTEGER).success).toBe(true)\n      expect(schema.safeParse(Number.MAX_SAFE_INTEGER).success).toBe(true)\n      expect(schema.safeParse(0).success).toBe(true)\n    })\n\n    it('allows unconstrained schema', () => {\n      const schema = integer()\n      expect(schema.safeParse(Number.MIN_SAFE_INTEGER).success).toBe(true)\n      expect(schema.safeParse(Number.MAX_SAFE_INTEGER).success).toBe(true)\n      expect(schema.safeParse(0).success).toBe(true)\n      expect(schema.safeParse(-999999).success).toBe(true)\n      expect(schema.safeParse(999999).success).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/integer.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Configuration options for integer schema validation.\n *\n * @property minimum - Minimum allowed value (inclusive)\n * @property maximum - Maximum allowed value (inclusive)\n */\nexport type IntegerSchemaOptions = {\n  minimum?: number\n  maximum?: number\n}\n\n/**\n * Schema for validating integer values with optional range constraints.\n *\n * Only accepts safe integers (values that can be exactly represented in JavaScript).\n * Use {@link IntegerSchemaOptions} to constrain the allowed range.\n *\n * @example\n * ```ts\n * const schema = new IntegerSchema({ minimum: 0, maximum: 100 })\n * const result = schema.validate(42)\n * ```\n */\nexport class IntegerSchema extends Schema<number> {\n  readonly type = 'integer' as const\n\n  constructor(readonly options?: IntegerSchemaOptions) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isInteger(input)) {\n      return ctx.issueUnexpectedType(input, 'integer')\n    }\n\n    if (this.options?.minimum != null && input < this.options.minimum) {\n      return ctx.issueTooSmall(input, 'integer', this.options.minimum, input)\n    }\n\n    if (this.options?.maximum != null && input > this.options.maximum) {\n      return ctx.issueTooBig(input, 'integer', this.options.maximum, input)\n    }\n\n    return ctx.success(input)\n  }\n}\n\n/**\n * Simple wrapper around {@link Number.isSafeInteger} that acts as a type guard.\n */\nfunction isInteger(input: unknown): input is number {\n  return Number.isSafeInteger(input)\n}\n\n/**\n * Creates an integer schema with optional minimum and maximum constraints.\n *\n * Validates that the input is a safe integer (can be exactly represented in JavaScript)\n * and optionally falls within a specified range.\n *\n * @param options - Optional configuration for minimum and maximum values\n * @returns A new {@link IntegerSchema} instance\n *\n * @example\n * ```ts\n * // Basic integer\n * const countSchema = l.integer()\n *\n * // With minimum value\n * const positiveSchema = l.integer({ minimum: 1 })\n *\n * // With range constraints\n * const percentSchema = l.integer({ minimum: 0, maximum: 100 })\n *\n * // Age validation\n * const ageSchema = l.integer({ minimum: 0, maximum: 150 })\n * ```\n */\nexport const integer = /*#__PURE__*/ memoizedOptions(function (\n  options?: IntegerSchemaOptions,\n) {\n  return new IntegerSchema(options)\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/intersection.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { boolean } from './boolean.js'\nimport { dict } from './dict.js'\nimport { enumSchema } from './enum.js'\nimport { intersection } from './intersection.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\n\ndescribe('IntersectionSchema', () => {\n  const schema = intersection(\n    object({\n      title: string(),\n    }),\n    dict(enumSchema(['tag1', 'tag2']), boolean()),\n  )\n\n  it('validates extra properties with the provided validator', () => {\n    const result = schema.safeParse({\n      title: 'My Post',\n      tag1: true,\n      tag2: false,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects extra properties that fail the provided validator', () => {\n    const result = schema.safeParse({\n      title: 'My Post',\n      tag1: 'not a boolean',\n    })\n    expect(result.success).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/intersection.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  Schema,\n  Simplify,\n  ValidationContext,\n} from '../core.js'\nimport { DictSchema } from './dict.js'\nimport { ObjectSchema } from './object.js'\n\n/**\n * Type utility for computing the intersection of two object types.\n *\n * Allows to more accurately represent the intersection of two object types\n * where both types may share some keys, and one of them uses an index\n * signature.\n *\n * @template A - First object type (typically from ObjectSchema)\n * @template B - Second object type (typically from DictSchema)\n *\n * @see {@link https://www.typescriptlang.org/play/?#code/C4TwDgpgBAglC8UDeUBmB7dAuKByARgIYBOuUAvlAGTJQDaA+lAJYB2UAzsMWwOYC6OVgFcAtvgjEKAKGkATCAGMANiWiL0rLlEI4YsjVuBQA1hBA4uPVrwRQARBnT2Dm7QDdCy4dESE6ZiD8UAD0IVAi4pJQABQcABbowspyUBIORMT2AJSyEAAeYOjExqCQUACSrMCSHErAzJoAPNJQsFAFNaxyHFAASkrFck1WfAA0UMKsJqzoAO6sAHxjrVAAQh35XT39g8TDozYTUzPzSyuLdqtwVKttMYHoqO00j88bnRDdvawQ7pJ3NpQAD860BbRwSHBQLadAA0ix2G91oJ1vDggAfWABcxPF5QOH8aFtci5aRlaAwVDMfIQVKIKo1Yh1RQNZq0Jw4AgkMjkCYoRiIzjcPioyISKTkRayBQqNRQQzaQgAMRpdL01NpclcRignm8EFVWrsKrVchxQVC4XF0SxmSAA Playground link}\n */\nexport type Intersect<A, B> = B[keyof B] extends never\n  ? A\n  : keyof A & keyof B extends never\n    ? // If A and B don't overlap, just return A & B\n      A & B\n    : // Otherwise, properly represent the fact that accessing using an\n      // index signature could return a value from either A or B\n      A & { [K in keyof B]: B[K] | A[keyof A & K] }\n\n/**\n * Schema for combining an object schema with a dictionary schema.\n *\n * Validates that the input matches both the fixed object shape and allows\n * additional properties that match the dictionary schema. Properties defined\n * in the object schema are validated by the object, and remaining properties\n * are validated by the dictionary.\n *\n * @template Left - The ObjectSchema type for fixed properties\n * @template Right - The DictSchema type for additional properties\n *\n * @example\n * ```ts\n * const schema = new IntersectionSchema(\n *   l.object({ name: l.string() }),\n *   l.dict(l.string(), l.integer())\n * )\n * // Validates: { name: 'test', score: 100, count: 5 }\n * ```\n */\nexport class IntersectionSchema<\n  const Left extends ObjectSchema = any,\n  const Right extends DictSchema = any,\n> extends Schema<\n  Simplify<Intersect<InferInput<Left>, InferInput<Right>>>,\n  Simplify<Intersect<InferOutput<Left>, InferOutput<Right>>>\n> {\n  readonly type = 'intersection' as const\n\n  constructor(\n    protected readonly left: Left,\n    protected readonly right: Right,\n  ) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    const leftResult = ctx.validate(input, this.left)\n    if (!leftResult.success) return leftResult\n\n    return this.right.validateInContext(leftResult.value, ctx, {\n      ignoredKeys: this.left.validatorsMap,\n    })\n  }\n}\n\n/**\n * Creates an intersection schema combining fixed object properties with dynamic dictionary properties.\n *\n * Useful for objects that have a known set of properties plus additional\n * arbitrary properties that follow a pattern.\n *\n * @param left - Object schema defining the fixed, known properties\n * @param right - Dictionary schema for validating additional properties\n * @returns A new {@link IntersectionSchema} instance\n *\n * @example\n * ```ts\n * // Object with fixed and dynamic properties\n * const configSchema = l.intersection(\n *   l.object({\n *     version: l.integer(),\n *     name: l.string(),\n *   }),\n *   l.dict(l.string(), l.string()) // Additional string properties\n * )\n *\n * configSchema.parse({\n *   version: 1,\n *   name: 'my-config',\n *   customField: 'value',\n *   anotherField: 'another',\n * })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function intersection<\n  const Left extends ObjectSchema,\n  const Right extends DictSchema,\n>(left: Left, right: Right) {\n  return new IntersectionSchema<Left, Right>(left, right)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/lex-map.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { lexMap } from './lex-map.js'\n\ndescribe(lexMap, () => {\n  describe('basic validation', () => {\n    const schema = lexMap()\n\n    it('accepts empty plain objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual({})\n      }\n    })\n\n    it('accepts plain objects with string values', () => {\n      const obj = { key: 'value', name: 'test' }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts plain objects with number values', () => {\n      const obj = { count: 42, total: 100 }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts plain objects with boolean values', () => {\n      const obj = { enabled: true, visible: false }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts plain objects with null values', () => {\n      const obj = { value: null, optional: null }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts nested plain objects', () => {\n      const obj = {\n        nested: {\n          deep: {\n            value: 'test',\n          },\n        },\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts plain objects with array values', () => {\n      const obj = {\n        items: [1, 2, 3],\n        names: ['alice', 'bob'],\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts plain objects with mixed value types', () => {\n      const obj = {\n        string: 'value',\n        number: 42,\n        boolean: true,\n        null: null,\n        array: [1, 'two', 3],\n        nested: { key: 'value' },\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts plain objects with Uint8Array values', () => {\n      const obj = {\n        bytes: new Uint8Array([1, 2, 3, 4]),\n        data: new Uint8Array([255, 0, 128]),\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n  })\n\n  describe('rejects non-plain-objects', () => {\n    const schema = lexMap()\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([1, 2, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Date objects', () => {\n      const result = schema.safeParse(new Date())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects RegExp objects', () => {\n      const result = schema.safeParse(/test/gi)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Map objects', () => {\n      const result = schema.safeParse(new Map([['key', 'value']]))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Set objects', () => {\n      const result = schema.safeParse(new Set([1, 2, 3]))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects WeakMap objects', () => {\n      const result = schema.safeParse(new WeakMap())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects WeakSet objects', () => {\n      const result = schema.safeParse(new WeakSet())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Error objects', () => {\n      const result = schema.safeParse(new Error('test'))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Promise objects', () => {\n      const result = schema.safeParse(Promise.resolve(42))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects functions', () => {\n      const result = schema.safeParse(() => 'test')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Symbol', () => {\n      const result = schema.safeParse(Symbol('test'))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects BigInt', () => {\n      const result = schema.safeParse(BigInt(123))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects class instances', () => {\n      class TestClass {\n        constructor(public value: string) {}\n      }\n      const result = schema.safeParse(new TestClass('test'))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('rejects invalid value types', () => {\n    const schema = lexMap()\n\n    it('rejects objects with floating point numbers', () => {\n      const result = schema.safeParse({ value: 3.14 })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with NaN values', () => {\n      const result = schema.safeParse({ value: NaN })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Infinity values', () => {\n      const result = schema.safeParse({ value: Infinity })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with -Infinity values', () => {\n      const result = schema.safeParse({ value: -Infinity })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with undefined values', () => {\n      const result = schema.safeParse({ value: undefined })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with function values', () => {\n      const result = schema.safeParse({ fn: () => 'test' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Symbol values', () => {\n      const result = schema.safeParse({ sym: Symbol('test') })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with BigInt values', () => {\n      const result = schema.safeParse({ big: BigInt(123) })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Date values', () => {\n      const result = schema.safeParse({ date: new Date() })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with RegExp values', () => {\n      const result = schema.safeParse({ pattern: /test/i })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Map values', () => {\n      const result = schema.safeParse({ map: new Map() })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Set values', () => {\n      const result = schema.safeParse({ set: new Set() })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Error values', () => {\n      const result = schema.safeParse({ error: new Error('test') })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with Promise values', () => {\n      const result = schema.safeParse({ promise: Promise.resolve(1) })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with class instance values', () => {\n      class TestClass {}\n      const result = schema.safeParse({ instance: new TestClass() })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('rejects invalid nested values', () => {\n    const schema = lexMap()\n\n    it('rejects deeply nested invalid values', () => {\n      const result = schema.safeParse({\n        nested: {\n          deep: {\n            invalid: 3.14,\n          },\n        },\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with invalid values', () => {\n      const result = schema.safeParse({\n        items: [1, 2, 3.14],\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with undefined values', () => {\n      const result = schema.safeParse({\n        items: [1, undefined, 3],\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects nested arrays with invalid values', () => {\n      const result = schema.safeParse({\n        matrix: [\n          [1, 2],\n          [3, 4.5],\n        ],\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects mixed valid and invalid keys', () => {\n      const result = schema.safeParse({\n        valid: 'string',\n        invalid: Infinity,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = lexMap()\n\n    it('accepts objects with numeric string keys', () => {\n      const obj = { '0': 'zero', '1': 'one', '2': 'two' }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with special string keys', () => {\n      const obj = {\n        'with spaces': 'value',\n        'with-dashes': 'value',\n        'with.dots': 'value',\n        with_underscores: 'value',\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with empty string keys', () => {\n      const obj = { '': 'empty key value' }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with zero values', () => {\n      const obj = { count: 0, index: 0 }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with negative integer values', () => {\n      const obj = { temperature: -10, balance: -500 }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with empty string values', () => {\n      const obj = { name: '', description: '' }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with empty array values', () => {\n      const obj = { items: [], tags: [] }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with empty nested objects', () => {\n      const obj = { config: {}, metadata: {} }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with empty Uint8Array values', () => {\n      const obj = { data: new Uint8Array([]) }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts deeply nested structures', () => {\n      const obj = {\n        level1: {\n          level2: {\n            level3: {\n              level4: {\n                level5: {\n                  value: 'deep',\n                  array: [1, 2, [3, 4, [5]]],\n                },\n              },\n            },\n          },\n        },\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts complex nested arrays and objects', () => {\n      const obj = {\n        matrix: [\n          [1, 2],\n          [3, 4],\n          [5, 6],\n        ],\n        nested: {\n          arrays: [[['deep']]],\n          mixed: [{ a: 1 }, { b: 2 }],\n        },\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with null prototype', () => {\n      const obj = Object.create(null)\n      obj.key = 'value'\n      obj.count = 42\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(obj)\n      }\n    })\n\n    it('accepts objects with $type property', () => {\n      const obj = { $type: 'app.bsky.feed.post', text: 'Hello' }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with various special characters in keys', () => {\n      const obj = {\n        '@mention': 'user',\n        '#hashtag': 'tag',\n        '!important': 'flag',\n        'key:value': 'pair',\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n  })\n\n  describe('large objects', () => {\n    const schema = lexMap()\n\n    it('accepts objects with many keys', () => {\n      const obj: Record<string, number> = {}\n      for (let i = 0; i < 100; i++) {\n        obj[`key${i}`] = i\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with large arrays', () => {\n      const obj = {\n        numbers: Array.from({ length: 1000 }, (_, i) => i),\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts objects with large Uint8Array values', () => {\n      const obj = {\n        data: new Uint8Array(1000).fill(0),\n      }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n  })\n\n  describe('preservation of input', () => {\n    const schema = lexMap()\n\n    it('preserves the original object reference', () => {\n      const input = { key: 'value', count: 42 }\n      const result = schema.safeParse(input)\n\n      if (result.success) {\n        expect(result.value).toBe(input)\n      } else {\n        throw new Error('Expected validation to succeed')\n      }\n    })\n\n    it('preserves nested object references', () => {\n      const nested = { inner: 'value' }\n      const input = { outer: nested }\n      const result = schema.safeParse(input)\n\n      if (result.success) {\n        expect(result.value).toBe(input)\n        expect(result.value.outer).toBe(nested)\n      } else {\n        throw new Error('Expected validation to succeed')\n      }\n    })\n\n    it('preserves array references in object values', () => {\n      const arr = [1, 2, 3]\n      const input = { items: arr }\n      const result = schema.safeParse(input)\n\n      if (result.success) {\n        expect(result.value).toBe(input)\n        expect(result.value.items).toBe(arr)\n      } else {\n        throw new Error('Expected validation to succeed')\n      }\n    })\n\n    it('preserves Uint8Array references in object values', () => {\n      const bytes = new Uint8Array([1, 2, 3])\n      const input = { data: bytes }\n      const result = schema.safeParse(input)\n\n      if (result.success) {\n        expect(result.value).toBe(input)\n        expect(result.value.data).toBe(bytes)\n      } else {\n        throw new Error('Expected validation to succeed')\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/lex-map.ts",
    "content": "import { LexMap, isPlainObject } from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\nimport { lexValue } from './lex-value.js'\n\nconst propertyValueSchema = /*#__PURE__*/ lexValue()\n\nexport type { LexMap }\n\n/**\n * AT Protocol lexicon schema definitions with \"type\": \"unknown\" are represented\n * as plain objects with string keys and values that are valid AT Protocol data\n * types (string, integer, boolean, null, bytes, cid, array, or object). This\n * type alias corresponds to the expected structure of such \"unknown\" schema\n * values.\n */\nexport class LexMapSchema extends Schema<LexMap> {\n  readonly type = 'lexMap' as const\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isPlainObject(input)) {\n      return ctx.issueUnexpectedType(input, 'object')\n    }\n\n    for (const key of Object.keys(input)) {\n      // @NOTE We use a lexValue() schema here to recursively validate all\n      // nested values, which ensures that the error reporting includes the\n      // correct path and type information for any invalid nested values. This\n      // allows for more informative error descriptions than a simple \"isLexMap\"\n      // check.\n      const r = ctx.validateChild(input, key, propertyValueSchema) // recursively validate all properties\n      if (!r.success) return r\n    }\n\n    return ctx.success(input)\n  }\n}\n\n/**\n * Creates a schema that accepts any plain object with string keys and values\n * that are valid AT Protocol data types (string, integer, boolean, null, bytes,\n * cid, array, or object).\n *\n * @see {@link LexMap} from `@atproto/lex-data` for the type definition of valid AT Protocol data types\n * @returns A new {@link LexMapSchema} instance\n *\n * @example\n * ```ts\n * // Accept any object shape\n * const schema = l.lexMap()\n *\n * schema.validate({ any: 'props' })    // success\n * schema.validate([1, 2, 3])           // fails - only plain objects are accepted\n * schema.validate({ foo: new Date() }) // fails - Date is not a valid LexValue\n * schema.validate({ foo: 1.2 })        // fails - 1.2 is not a valid LexValue (not an integer)\n * ```\n */\nexport const lexMap = /*#__PURE__*/ memoizedOptions(function () {\n  return new LexMapSchema()\n})\n\n/** @deprecated Use {@link lexMap} instead */\nexport const unknownObject = lexMap\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/lex-value.test.ts",
    "content": "import { describe, expect, test } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport { lexValue } from './lex-value.js'\n\nconst schema = lexValue()\n\ndescribe(lexValue, () => {\n  describe('valid values', () => {\n    for (const { note, value } of [\n      { note: 'string', value: 'hello' },\n      { note: 'boolean true', value: true },\n      { note: 'boolean false', value: false },\n      { note: 'null', value: null },\n      { note: 'integer', value: 42 },\n      { note: 'negative integer', value: -1 },\n      { note: 'zero', value: 0 },\n      { note: 'Uint8Array', value: new Uint8Array([1, 2, 3]) },\n      {\n        note: 'Cid',\n        value: parseCid(\n          'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        ),\n      },\n      { note: 'empty plain object', value: {} },\n      {\n        note: 'object with Lex values',\n        value: {\n          a: 123,\n          b: 'blah',\n          c: true,\n          d: null,\n          e: new Uint8Array([1, 2, 3]),\n          f: { nested: 'value' },\n          g: [1, 2, 3],\n        },\n      },\n      { note: 'empty array', value: [] },\n      {\n        note: 'array with Lex values',\n        value: [\n          123,\n          'blah',\n          true,\n          null,\n          new Uint8Array([1, 2, 3]),\n          { nested: 'value' },\n          [1, 2, 3],\n        ],\n      },\n    ]) {\n      test(note, () => {\n        const result = schema.safeParse(value)\n        expect(result.success).toBe(true)\n      })\n    }\n  })\n\n  describe('invalid values', () => {\n    for (const { note, value } of [\n      { note: 'float', value: 42.5 },\n      { note: 'undefined', value: undefined },\n      { note: 'function', value: () => {} },\n      { note: 'Date object', value: new Date() },\n      { note: 'Map object', value: new Map() },\n      { note: 'Set object', value: new Set() },\n      { note: 'class instance', value: new (class A {})() },\n      { note: 'object with function value', value: { a: 123, b: () => {} } },\n      {\n        note: 'object with undefined value',\n        value: { a: 123, b: undefined },\n      },\n      { note: 'array with function', value: [123, 'blah', () => {}] },\n      { note: 'array with undefined', value: [123, 'blah', undefined] },\n    ]) {\n      test(note, () => {\n        const result = schema.safeParse(value)\n        expect(result.success).toBe(false)\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/lex-value.ts",
    "content": "import { LexValue, isLexScalar, isPlainObject } from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\nexport type { LexValue }\n\nconst EXPECTED_TYPES = Object.freeze([\n  // Scalar types\n  'null',\n  'boolean',\n  'integer',\n  'string',\n  'cid',\n  'bytes',\n  // Recursive types\n  'array',\n  'object',\n] as const)\n\n/**\n * AT Protocol lexicon values are any valid AT Protocol data types: string,\n * integer, boolean, null, bytes, cid, array, or object.\n */\nexport class LexValueSchema extends Schema<LexValue> {\n  readonly type = 'lexValue' as const\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    // @NOTE We are *not* using \"isLexValue\" here to allow for more specific\n    // error messages about the path and type of the invalid value. The\n    // \"isLexValue\" check is effectively performed by the recursive validation\n    // of child properties below.\n\n    // @NOTE There are two limitations to the fact that we are not using\n    // \"isLexValue\" here:\n    // 1. We cannot detect circular references in objects or arrays, which would\n    //    cause infinite recursion. However, circular references are not valid\n    //    AT Protocol data types, so this is not a concern for valid input. This\n    //    could easily be addressed in the \"validateChild\" method by keeping\n    //    track of \"parent\" objects.\n    // 2. We are limited in the recursion depth we can validate due to potential\n    //    recursion depth limits in JavaScript. However, this is also not a\n    //    concern for most valid input, as extremely deep nesting is unlikely in\n    //    typical use cases.\n    if (isPlainObject(input)) {\n      for (const key of Object.keys(input)) {\n        const r = ctx.validateChild(input, key, this) // recursively validate all properties\n        if (!r.success) return r\n      }\n    } else if (Array.isArray(input)) {\n      for (let i = 0; i < input.length; i++) {\n        const r = ctx.validateChild(input, i, this) // recursively validate all array items\n        if (!r.success) return r\n      }\n    } else if (!isLexScalar(input)) {\n      return ctx.issueInvalidType(input, EXPECTED_TYPES)\n    }\n\n    return ctx.success(input)\n  }\n}\n\n/**\n * Creates a schema that accepts any valid AT Protocol data type: string,\n * integer, boolean, null, bytes, cid, array, or plain object. Arrays and\n * objects are recursively validated to ensure all nested values are also valid\n * AT Protocol data types.\n *\n * @see {@link LexValue} from `@atproto/lex-data` for the type definition of valid AT Protocol data types\n * @returns A new {@link LexValueSchema} instance\n *\n * @example\n * ```ts\n * const schema = l.lexValue()\n *\n * schema.validate('hello')              // success\n * schema.validate(42)                   // success\n * schema.validate(null)                 // success\n * schema.validate([1, 'two', null])     // success\n * schema.validate({ any: 'props' })     // success\n * schema.validate(new Date())           // fails - Date is not a valid LexValue\n * schema.validate({ foo: 1.2 })         // fails - 1.2 is not a valid LexValue (not an integer)\n * ```\n */\nexport const lexValue = /*#__PURE__*/ memoizedOptions(function () {\n  return new LexValueSchema()\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/literal.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { literal } from './literal.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('LiteralSchema', () => {\n  describe('string literals', () => {\n    const schema = literal('hello')\n\n    it('validates exact string match', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('rejects different strings', () => {\n      const result = schema.safeParse('world')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects similar strings with different case', () => {\n      const result = schema.safeParse('Hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string when literal is non-empty', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['hello'])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('empty string literal', () => {\n    const schema = literal('')\n\n    it('validates empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('')\n      }\n    })\n\n    it('rejects non-empty strings', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('number literals', () => {\n    const schema = literal(42)\n\n    it('validates exact number match', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(42)\n      }\n    })\n\n    it('rejects different numbers', () => {\n      const result = schema.safeParse(43)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string representation of number', () => {\n      const result = schema.safeParse('42')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('zero literal', () => {\n    const schema = literal(0)\n\n    it('validates zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(0)\n      }\n    })\n\n    it('rejects false', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects other numbers', () => {\n      const result = schema.safeParse(1)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('negative number literals', () => {\n    const schema = literal(-42)\n\n    it('validates exact negative number match', () => {\n      const result = schema.safeParse(-42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(-42)\n      }\n    })\n\n    it('rejects positive equivalent', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects different negative numbers', () => {\n      const result = schema.safeParse(-43)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('decimal number literals', () => {\n    const schema = literal(3.14)\n\n    it('validates exact decimal match', () => {\n      const result = schema.safeParse(3.14)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(3.14)\n      }\n    })\n\n    it('rejects different decimals', () => {\n      const result = schema.safeParse(3.15)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects integer equivalent', () => {\n      const result = schema.safeParse(3)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('boolean literals', () => {\n    describe('true literal', () => {\n      const schema = literal(true)\n\n      it('validates true', () => {\n        const result = schema.safeParse(true)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(true)\n        }\n      })\n\n      it('rejects false', () => {\n        const result = schema.safeParse(false)\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects truthy values', () => {\n        const result = schema.safeParse(1)\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects string representation', () => {\n        const result = schema.safeParse('true')\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects null', () => {\n        const result = schema.safeParse(null)\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects undefined', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(false)\n      })\n    })\n\n    describe('false literal', () => {\n      const schema = literal(false)\n\n      it('validates false', () => {\n        const result = schema.safeParse(false)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(false)\n        }\n      })\n\n      it('rejects true', () => {\n        const result = schema.safeParse(true)\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects falsy values', () => {\n        const result = schema.safeParse(0)\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects empty string', () => {\n        const result = schema.safeParse('')\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects null', () => {\n        const result = schema.safeParse(null)\n        expect(result.success).toBe(false)\n      })\n    })\n  })\n\n  describe('null literal', () => {\n    const schema = literal(null)\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects false', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string \"null\"', () => {\n      const result = schema.safeParse('null')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('default values', () => {\n    describe('string literal with default', () => {\n      const schema = withDefault(literal('hello'), 'hello')\n\n      it('uses default value when undefined is provided', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe('hello')\n        }\n      })\n\n      it('uses explicit value over default', () => {\n        const result = schema.safeParse('hello')\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe('hello')\n        }\n      })\n\n      it('rejects non-matching value even with default', () => {\n        const result = schema.safeParse('world')\n        expect(result.success).toBe(false)\n      })\n    })\n\n    describe('number literal with default', () => {\n      const schema = withDefault(literal(42), 42)\n\n      it('uses default value when undefined is provided', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(42)\n        }\n      })\n\n      it('uses explicit value over default', () => {\n        const result = schema.safeParse(42)\n        expect(result.success).toBe(true)\n      })\n    })\n\n    describe('boolean literal with default', () => {\n      const schema = withDefault(literal(true), true)\n\n      it('uses default value when undefined is provided', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(true)\n        }\n      })\n\n      it('uses explicit value over default', () => {\n        const result = schema.safeParse(true)\n        expect(result.success).toBe(true)\n      })\n    })\n\n    describe('null literal with default', () => {\n      const schema = withDefault(literal(null), null)\n\n      it('uses default value when undefined is provided', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(null)\n        }\n      })\n\n      it('uses explicit value over default', () => {\n        const result = schema.safeParse(null)\n        expect(result.success).toBe(true)\n      })\n    })\n\n    describe('false literal with default', () => {\n      const schema = withDefault(literal(false), false)\n\n      it('uses default value when undefined is provided', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(false)\n        }\n      })\n\n      it('does not confuse explicit false with undefined', () => {\n        const result = schema.safeParse(false)\n        expect(result.success).toBe(true)\n      })\n    })\n\n    describe('zero literal with default', () => {\n      const schema = withDefault(literal(0), 0)\n\n      it('uses default value when undefined is provided', () => {\n        const result = schema.safeParse(undefined)\n        expect(result.success).toBe(true)\n        if (result.success) {\n          expect(result.value).toBe(0)\n        }\n      })\n\n      it('does not confuse explicit zero with undefined', () => {\n        const result = schema.safeParse(0)\n        expect(result.success).toBe(true)\n      })\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles special string characters', () => {\n      const schema = literal('hello\\nworld')\n      expect(schema.safeParse('hello\\nworld').success).toBe(true)\n      expect(schema.safeParse('hello world').success).toBe(false)\n    })\n\n    it('handles unicode characters in strings', () => {\n      const schema = literal('Hello 世界 🌍')\n      expect(schema.safeParse('Hello 世界 🌍').success).toBe(true)\n      expect(schema.safeParse('Hello world').success).toBe(false)\n    })\n\n    it('handles emoji literals', () => {\n      const schema = literal('🚀')\n      expect(schema.safeParse('🚀').success).toBe(true)\n      expect(schema.safeParse('🌟').success).toBe(false)\n    })\n\n    it('handles very long string literals', () => {\n      const longString = 'a'.repeat(1000)\n      const schema = literal(longString)\n      expect(schema.safeParse(longString).success).toBe(true)\n      expect(schema.safeParse(longString + 'b').success).toBe(false)\n    })\n\n    it('handles string with whitespace', () => {\n      const schema = literal('  hello  ')\n      expect(schema.safeParse('  hello  ').success).toBe(true)\n      expect(schema.safeParse('hello').success).toBe(false)\n      expect(schema.safeParse('  hello').success).toBe(false)\n    })\n\n    it('handles Number.MAX_SAFE_INTEGER', () => {\n      const schema = literal(Number.MAX_SAFE_INTEGER)\n      expect(schema.safeParse(Number.MAX_SAFE_INTEGER).success).toBe(true)\n      expect(schema.safeParse(Number.MAX_SAFE_INTEGER - 1).success).toBe(false)\n    })\n\n    it('handles Number.MIN_SAFE_INTEGER', () => {\n      const schema = literal(Number.MIN_SAFE_INTEGER)\n      expect(schema.safeParse(Number.MIN_SAFE_INTEGER).success).toBe(true)\n      expect(schema.safeParse(Number.MIN_SAFE_INTEGER + 1).success).toBe(false)\n    })\n\n    it('rejects NaN', () => {\n      const schema = literal(42)\n      expect(schema.safeParse(NaN).success).toBe(false)\n    })\n\n    it('rejects Infinity', () => {\n      const schema = literal(42)\n      expect(schema.safeParse(Infinity).success).toBe(false)\n    })\n\n    it('rejects -Infinity', () => {\n      const schema = literal(42)\n      expect(schema.safeParse(-Infinity).success).toBe(false)\n    })\n\n    it('rejects Boolean objects', () => {\n      const schema = literal(true)\n      expect(schema.safeParse(new Boolean(true)).success).toBe(false)\n    })\n\n    it('rejects String objects', () => {\n      const schema = literal('hello')\n      expect(schema.safeParse(new String('hello')).success).toBe(false)\n    })\n\n    it('rejects Number objects', () => {\n      const schema = literal(42)\n      expect(schema.safeParse(new Number(42)).success).toBe(false)\n    })\n\n    it('distinguishes between -0 and +0', () => {\n      const schemaPositive = literal(0)\n      const schemaNegative = literal(-0)\n      // In JavaScript, 0 === -0, so both should validate for both schemas\n      expect(schemaPositive.safeParse(0).success).toBe(true)\n      expect(schemaPositive.safeParse(-0).success).toBe(true)\n      expect(schemaNegative.safeParse(0).success).toBe(true)\n      expect(schemaNegative.safeParse(-0).success).toBe(true)\n    })\n\n    it('handles very small decimal differences', () => {\n      const schema = literal(0.1 + 0.2)\n      // Note: 0.1 + 0.2 !== 0.3 in JavaScript due to floating point precision\n      expect(schema.safeParse(0.1 + 0.2).success).toBe(true)\n      expect(schema.safeParse(0.3).success).toBe(false)\n    })\n  })\n\n  describe('type safety', () => {\n    it('accepts exact literal types in TypeScript', () => {\n      const schema = literal('specific' as const)\n      const result = schema.safeParse('specific')\n      expect(result.success).toBe(true)\n    })\n\n    it('preserves literal type in success result', () => {\n      const schema = literal(42)\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        // TypeScript should infer result.value as the literal type 42\n        expect(result.value).toBe(42)\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/literal.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\n\n/**\n * Schema that only accepts a specific literal value.\n *\n * Validates that the input is exactly equal to the specified value using\n * strict equality (===).\n *\n * @template TValue - The literal type (null, string, number, or boolean)\n *\n * @example\n * ```ts\n * const schema = new LiteralSchema('admin')\n * schema.validate('admin') // success\n * schema.validate('user')  // fails\n * ```\n */\nexport class LiteralSchema<\n  const TValue extends null | string | number | boolean,\n> extends Schema<TValue> {\n  readonly type = 'literal' as const\n\n  constructor(readonly value: TValue) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (input !== this.value) {\n      return ctx.issueInvalidValue(input, [this.value])\n    }\n\n    return ctx.success(this.value)\n  }\n}\n\n/**\n * Creates a literal schema that only accepts the exact specified value.\n *\n * Useful for discriminator fields in unions, constant values, or type narrowing.\n *\n * @param value - The exact value that must be matched\n * @returns A new {@link LiteralSchema} instance\n *\n * @example\n * ```ts\n * // String literal\n * const roleSchema = l.literal('admin')\n *\n * // Number literal\n * const versionSchema = l.literal(1)\n *\n * // Boolean literal\n * const enabledSchema = l.literal(true)\n *\n * // Null literal\n * const nullSchema = l.literal(null)\n *\n * // In discriminated unions\n * const actionSchema = l.discriminatedUnion('type', [\n *   l.object({ type: l.literal('create'), data: l.unknown() }),\n *   l.object({ type: l.literal('delete'), id: l.string() }),\n * ])\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function literal<const V extends null | string | number | boolean>(\n  value: V,\n) {\n  return new LiteralSchema<V>(value)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/never.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { never } from './never.js'\n\ndescribe('NeverSchema', () => {\n  describe('basic validation', () => {\n    const schema = never()\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('string')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects false', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = never()\n\n    it('rejects BigInt', () => {\n      const result = schema.safeParse(BigInt(123))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Symbol', () => {\n      const result = schema.safeParse(Symbol('test'))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects functions', () => {\n      const result = schema.safeParse(() => {})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Date objects', () => {\n      const result = schema.safeParse(new Date())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects RegExp objects', () => {\n      const result = schema.safeParse(/test/)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects nested objects', () => {\n      const result = schema.safeParse({ nested: { value: 'test' } })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects nested arrays', () => {\n      const result = schema.safeParse([\n        [1, 2],\n        [3, 4],\n      ])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Map objects', () => {\n      const result = schema.safeParse(new Map())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Set objects', () => {\n      const result = schema.safeParse(new Set())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Error objects', () => {\n      const result = schema.safeParse(new Error('test'))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('complex data types', () => {\n    const schema = never()\n\n    it('rejects class instances', () => {\n      class TestClass {\n        value = 'test'\n      }\n      const result = schema.safeParse(new TestClass())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with values', () => {\n      const result = schema.safeParse([1, 2, 3, 'four', true])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with properties', () => {\n      const result = schema.safeParse({\n        name: 'test',\n        age: 30,\n        active: true,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Promise objects', () => {\n      const result = schema.safeParse(Promise.resolve(42))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Buffer objects', () => {\n      const result = schema.safeParse(Buffer.from('test'))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('special number values', () => {\n    const schema = never()\n\n    it('rejects NaN', () => {\n      const result = schema.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Infinity', () => {\n      const result = schema.safeParse(Infinity)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects negative Infinity', () => {\n      const result = schema.safeParse(-Infinity)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects negative zero', () => {\n      const result = schema.safeParse(-0)\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/never.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Schema that always fails validation.\n *\n * Represents an impossible type - no value can satisfy this schema.\n * Useful for exhaustiveness checking or marking impossible branches.\n *\n * @example\n * ```ts\n * const schema = new NeverSchema()\n * schema.validate(anything) // always fails\n * ```\n */\nexport class NeverSchema extends Schema<never> {\n  readonly type = 'never' as const\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    return ctx.issueUnexpectedType(input, 'never')\n  }\n}\n\n/**\n * Creates a never schema that always fails validation.\n *\n * Useful for exhaustiveness checking in TypeScript or marking impossible\n * code paths.\n *\n * @returns A new {@link NeverSchema} instance\n *\n * @example\n * ```ts\n * // Exhaustiveness checking\n * type Status = 'active' | 'inactive'\n *\n * function handleStatus(status: Status) {\n *   switch (status) {\n *     case 'active': return 'Active'\n *     case 'inactive': return 'Inactive'\n *     default:\n *       // TypeScript will error if we miss a case\n *       l.never().parse(status)\n *   }\n * }\n *\n * // In impossible union branches\n * const schema = l.object({\n *   type: l.literal('fixed'),\n *   dynamic: l.never(), // This property can never exist\n * })\n * ```\n */\nexport const never = /*#__PURE__*/ memoizedOptions(function () {\n  return new NeverSchema()\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/null.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { nullSchema } from './null.js'\n\ndescribe('NullSchema', () => {\n  describe('basic validation', () => {\n    const schema = nullSchema()\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('null')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = nullSchema()\n\n    it('rejects falsy values', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string \"null\"', () => {\n      const result = schema.safeParse('null')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects NaN', () => {\n      const result = schema.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects nested null in object', () => {\n      const result = schema.safeParse({ value: null })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects nested null in array', () => {\n      const result = schema.safeParse([null])\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/null.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Schema for validating null values.\n *\n * Only accepts the JavaScript `null` value. Rejects `undefined` and all\n * other values.\n *\n * @example\n * ```ts\n * const schema = new NullSchema()\n * schema.validate(null)      // success\n * schema.validate(undefined) // fails\n * ```\n */\nexport class NullSchema extends Schema<null> {\n  readonly type = 'null' as const\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (input !== null) {\n      return ctx.issueUnexpectedType(input, 'null')\n    }\n\n    return ctx.success(null)\n  }\n}\n\n/**\n * Creates a null schema that only accepts the null value.\n *\n * Useful for explicitly representing null in union types or optional fields.\n *\n * @returns A new {@link NullSchema} instance\n *\n * @example\n * ```ts\n * // Explicit null\n * const nullOnlySchema = l.null()\n *\n * // Nullable string (string or null)\n * const nullableStringSchema = l.union([l.string(), l.null()])\n * ```\n */\nexport const nullSchema = /*#__PURE__*/ memoizedOptions(function () {\n  return new NullSchema()\n})\n\nexport { nullSchema as null }\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/nullable.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { enumSchema } from './enum.js'\nimport { integer } from './integer.js'\nimport { nullable } from './nullable.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('NullableSchema', () => {\n  describe('with StringSchema', () => {\n    const schema = nullable(string())\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('validates valid string values', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('validates empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({ value: 'test' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['hello'])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with IntegerSchema', () => {\n    const schema = nullable(integer())\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('validates valid integer values', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(42)\n      }\n    })\n\n    it('validates zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates negative integers', () => {\n      const result = schema.safeParse(-42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects floats', () => {\n      const result = schema.safeParse(3.14)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('42')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with EnumSchema', () => {\n    const schema = nullable(enumSchema(['red', 'green', 'blue']))\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('validates valid enum values', () => {\n      expect(schema.safeParse('red').success).toBe(true)\n      expect(schema.safeParse('green').success).toBe(true)\n      expect(schema.safeParse('blue').success).toBe(true)\n    })\n\n    it('rejects invalid enum values', () => {\n      const result = schema.safeParse('yellow')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string when not in enum', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with constrained StringSchema', () => {\n    const schema = nullable(string({ minLength: 3, maxLength: 10 }))\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings within constraints', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings below minimum length', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings above maximum length', () => {\n      const result = schema.safeParse('hello world!')\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts strings at minimum boundary', () => {\n      const result = schema.safeParse('abc')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings at maximum boundary', () => {\n      const result = schema.safeParse('1234567890')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('with constrained IntegerSchema', () => {\n    const schema = nullable(integer({ minimum: 0, maximum: 100 }))\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates integers within constraints', () => {\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates minimum value', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates maximum value', () => {\n      const result = schema.safeParse(100)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values below minimum', () => {\n      const result = schema.safeParse(-1)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values above maximum', () => {\n      const result = schema.safeParse(101)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with StringSchema having default value', () => {\n    const schema = nullable(withDefault(string(), 'default'))\n\n    it('validates null explicitly', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('uses default value when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('default')\n      }\n    })\n\n    it('validates explicit string values', () => {\n      const result = schema.safeParse('custom')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('custom')\n      }\n    })\n  })\n\n  describe('with ObjectSchema', () => {\n    const schema = nullable(\n      object({\n        name: string(),\n        age: integer(),\n      }),\n    )\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('validates valid objects', () => {\n      const result = schema.safeParse({ name: 'Alice', age: 30 })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid objects with missing properties', () => {\n      const result = schema.safeParse({ name: 'Alice' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid objects with wrong types', () => {\n      const result = schema.safeParse({ name: 'Alice', age: 'thirty' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects primitive values', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('nested nullable schemas', () => {\n    const schema = nullable(nullable(string()))\n\n    it('validates null at outer level', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('validates valid string values', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid types', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with StringSchema format constraints', () => {\n    const schema = nullable(string({ format: 'uri' }))\n\n    it('validates null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates valid URIs', () => {\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid URIs', () => {\n      const result = schema.safeParse('not a uri')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid format even with valid string', () => {\n      const result = schema.safeParse('just a string')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    const stringSchema = nullable(string())\n\n    it('handles null correctly without coercion', () => {\n      const result = stringSchema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n        expect(result.value).not.toBe(undefined)\n        expect(result.value).not.toBe('')\n        expect(result.value).not.toBe(0)\n        expect(result.value).not.toBe(false)\n      }\n    })\n\n    it('distinguishes null from falsy values', () => {\n      expect(stringSchema.safeParse(null).success).toBe(true)\n      expect(stringSchema.safeParse(undefined).success).toBe(false)\n      expect(stringSchema.safeParse('').success).toBe(true)\n      expect(stringSchema.safeParse(0).success).toBe(false)\n      expect(stringSchema.safeParse(false).success).toBe(false)\n    })\n\n    it('handles NaN correctly', () => {\n      const result = stringSchema.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('handles Symbol correctly', () => {\n      const result = stringSchema.safeParse(Symbol('test'))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('type preservation', () => {\n    it('preserves string type for valid strings', () => {\n      const schema = nullable(string())\n      const result = schema.safeParse('test')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(typeof result.value).toBe('string')\n        expect(result.value).toBe('test')\n      }\n    })\n\n    it('preserves number type for valid integers', () => {\n      const schema = nullable(integer())\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(typeof result.value).toBe('number')\n        expect(result.value).toBe(42)\n      }\n    })\n\n    it('preserves object type for valid objects', () => {\n      const schema = nullable(object({ key: string() }))\n      const input = { key: 'value' }\n      const result = schema.safeParse(input)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(typeof result.value).toBe('object')\n        expect(result.value).toEqual({ key: 'value' })\n      }\n    })\n\n    it('preserves null type exactly', () => {\n      const schema = nullable(string())\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n        expect(result.value === null).toBe(true)\n        expect(typeof result.value).toBe('object')\n      }\n    })\n  })\n\n  describe('with complex wrapped schemas', () => {\n    it('validates nullable enum with default', () => {\n      const schema = nullable(\n        withDefault(enumSchema(['option1', 'option2']), 'option1'),\n      )\n\n      expect(schema.safeParse(null).success).toBe(true)\n      expect(schema.safeParse('option1').success).toBe(true)\n      expect(schema.safeParse('option2').success).toBe(true)\n      expect(schema.safeParse(undefined).success).toBe(true)\n      expect(schema.safeParse('invalid').success).toBe(false)\n    })\n\n    it('handles nullable schema with grapheme constraints', () => {\n      const schema = nullable(string({ minGraphemes: 2, maxGraphemes: 5 }))\n\n      expect(schema.safeParse(null).success).toBe(true)\n      expect(schema.safeParse('ab').success).toBe(true)\n      expect(schema.safeParse('hello').success).toBe(true)\n      expect(schema.safeParse('a').success).toBe(false)\n      expect(schema.safeParse('hello!').success).toBe(false)\n    })\n\n    it('handles nullable integer with negative range', () => {\n      const schema = nullable(integer({ minimum: -100, maximum: -10 }))\n\n      expect(schema.safeParse(null).success).toBe(true)\n      expect(schema.safeParse(-50).success).toBe(true)\n      expect(schema.safeParse(-100).success).toBe(true)\n      expect(schema.safeParse(-10).success).toBe(true)\n      expect(schema.safeParse(0).success).toBe(false)\n      expect(schema.safeParse(-5).success).toBe(false)\n      expect(schema.safeParse(-150).success).toBe(false)\n    })\n  })\n\n  describe('validation error behavior', () => {\n    it('returns failure for wrapped schema validation errors', () => {\n      const schema = nullable(integer({ minimum: 10 }))\n      const result = schema.safeParse(5)\n      expect(result.success).toBe(false)\n    })\n\n    it('returns failure for type mismatches', () => {\n      const schema = nullable(string())\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('returns success for null regardless of wrapped constraints', () => {\n      const schema = nullable(string({ minLength: 100, format: 'uri' }))\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n    })\n\n    it('wrapped schema validation applies when value is not null', () => {\n      const schema = nullable(string({ minLength: 5 }))\n      expect(schema.safeParse(null).success).toBe(true)\n      expect(schema.safeParse('hello').success).toBe(true)\n      expect(schema.safeParse('hi').success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/nullable.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n} from '../core.js'\nimport { memoizedTransformer } from '../util/memoize.js'\n\n/**\n * Schema wrapper that allows null values in addition to the wrapped schema.\n *\n * When the input is `null`, validation succeeds immediately. Otherwise,\n * the input is validated against the wrapped schema.\n *\n * @template TValidator - The wrapped validator type\n *\n * @example\n * ```ts\n * const schema = new NullableSchema(l.string())\n * schema.validate(null)    // success\n * schema.validate('hello') // success\n * ```\n */\nexport class NullableSchema<const TValidator extends Validator> extends Schema<\n  InferInput<TValidator> | null,\n  InferOutput<TValidator> | null\n> {\n  readonly type = 'nullable' as const\n\n  constructor(readonly validator: TValidator) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (input === null) {\n      return ctx.success(null)\n    }\n\n    return ctx.validate(input, this.validator)\n  }\n}\n\n/**\n * Creates a nullable schema that accepts null in addition to the wrapped type.\n *\n * Wraps another schema to allow null values. Different from `optional()` which\n * allows undefined.\n *\n * @param validator - The validator to make nullable\n * @returns A new {@link NullableSchema} instance\n *\n * @example\n * ```ts\n * // Nullable string\n * const nullableString = l.nullable(l.string())\n * nullableString.parse(null)    // null\n * nullableString.parse('hello') // 'hello'\n *\n * // In an object\n * const userSchema = l.object({\n *   name: l.string(),\n *   deletedAt: l.nullable(l.string({ format: 'datetime' })),\n * })\n *\n * // Combine with optional for null or undefined\n * const maybeString = l.optional(l.nullable(l.string()))\n * ```\n */\nexport const nullable = /*#__PURE__*/ memoizedTransformer(function <\n  const TValidator extends Validator,\n>(validator: TValidator) {\n  return new NullableSchema<TValidator>(validator)\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/object.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { enumSchema } from './enum.js'\nimport { integer } from './integer.js'\nimport { nullable } from './nullable.js'\nimport { object } from './object.js'\nimport { optional } from './optional.js'\nimport { string } from './string.js'\n\ndescribe('ObjectSchema', () => {\n  const schema = object({\n    name: string(),\n    age: optional(integer()),\n    gender: optional(nullable(enumSchema(['male', 'female']))),\n  })\n\n  it('validates plain objects', () => {\n    const result = schema.safeParse({\n      name: 'Alice',\n      age: 30,\n      gender: 'female',\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects non-objects', () => {\n    const result = schema.safeParse('not an object')\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects missing properties', () => {\n    const result = schema.safeParse({\n      age: 30,\n      gender: 'female',\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates optional properties', () => {\n    const result = schema.safeParse({\n      name: 'Alice',\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates nullable properties', () => {\n    const result = schema.safeParse({\n      name: 'Alice',\n      gender: null,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects invalid property types', () => {\n    const result = schema.safeParse({\n      name: 'Alice',\n      age: 'thirty',\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('ignores extra properties', () => {\n    const result = schema.safeParse({\n      name: 'Alice',\n      age: 30,\n      extra: 'value',\n    })\n    expect(result.success).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/object.ts",
    "content": "import { isPlainObject } from '@atproto/lex-data'\nimport {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n  WithOptionalProperties,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\n\n/**\n * Type representing the shape of an object schema.\n *\n * Maps property names to their corresponding validators.\n */\nexport type ObjectSchemaShape = Record<string, Validator>\n\n/**\n * Schema for validating objects with a defined shape.\n *\n * Each property in the shape is validated against its corresponding schema.\n * Properties wrapped in `optional()` are not required.\n *\n * @template TShape - The object shape type mapping property names to validators\n *\n * @example\n * ```ts\n * const schema = new ObjectSchema({\n *   name: l.string(),\n *   age: l.optional(l.integer()),\n * })\n * const result = schema.validate({ name: 'Alice' })\n * ```\n */\nexport class ObjectSchema<\n  const TShape extends ObjectSchemaShape = any,\n> extends Schema<\n  WithOptionalProperties<{\n    [K in keyof TShape]: InferInput<TShape[K]>\n  }>,\n  WithOptionalProperties<{\n    [K in keyof TShape]: InferOutput<TShape[K]>\n  }>\n> {\n  readonly type = 'object' as const\n\n  constructor(readonly shape: TShape) {\n    super()\n  }\n\n  get validatorsMap(): Map<string, Validator> {\n    const map = new Map(Object.entries(this.shape))\n\n    return lazyProperty(this, 'validatorsMap', map)\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isPlainObject(input)) {\n      return ctx.issueUnexpectedType(input, 'object')\n    }\n\n    // Lazily copy value\n    let copy: undefined | Record<string, unknown>\n\n    for (const [key, propDef] of this.validatorsMap) {\n      const result = ctx.validateChild(input, key, propDef)\n      if (!result.success) {\n        if (!(key in input)) {\n          // Transform into \"required key\" issue\n          return ctx.issueRequiredKey(input, key)\n        }\n\n        return result\n      }\n\n      // Skip copying if key is not present in input (and value is undefined)\n      if (result.value === undefined && !(key in input)) {\n        continue\n      }\n\n      if (!Object.is(result.value, input[key])) {\n        if (ctx.options.mode === 'validate') {\n          // In \"validate\" mode, we can't modify the input, so we issue an error\n          return ctx.issueInvalidPropertyValue(input, key, [result.value])\n        }\n\n        copy ??= { ...input }\n        copy[key] = result.value\n      }\n    }\n\n    return ctx.success(copy ?? input)\n  }\n}\n\n/**\n * Creates an object schema with the specified property validators.\n *\n * Validates that the input is a plain object and each property matches\n * its corresponding schema. Properties wrapped in `optional()` are not required.\n *\n * @param properties - Object mapping property names to their validators\n * @returns A new {@link ObjectSchema} instance\n *\n * @example\n * ```ts\n * // Basic object\n * const userSchema = l.object({\n *   name: l.string(),\n *   email: l.string({ format: 'uri' }),\n * })\n *\n * // With optional properties\n * const profileSchema = l.object({\n *   displayName: l.string(),\n *   bio: l.optional(l.string({ maxLength: 256 })),\n *   avatar: l.optional(l.blob({ accept: ['image/*'] })),\n * })\n *\n * // Nested objects\n * const postSchema = l.object({\n *   text: l.string(),\n *   author: l.object({\n *     did: l.string({ format: 'did' }),\n *     handle: l.string({ format: 'handle' }),\n *   }),\n * })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function object<const TShape extends ObjectSchemaShape>(\n  properties: TShape,\n) {\n  return new ObjectSchema<TShape>(properties)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/optional.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { boolean } from './boolean.js'\nimport { integer } from './integer.js'\nimport { optional } from './optional.js'\nimport { string } from './string.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('OptionalSchema', () => {\n  describe('basic validation with string schema', () => {\n    const schema = optional(string())\n\n    it('validates defined string values', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('validates empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('')\n      }\n    })\n\n    it('validates undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(undefined)\n      }\n    })\n\n    it('rejects invalid types for the inner schema', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({ value: 'hello' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['hello'])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('basic validation with integer schema', () => {\n    const schema = optional(integer())\n\n    it('validates defined integer values', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(42)\n      }\n    })\n\n    it('validates zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(0)\n      }\n    })\n\n    it('validates negative integers', () => {\n      const result = schema.safeParse(-42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(-42)\n      }\n    })\n\n    it('validates undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(undefined)\n      }\n    })\n\n    it('rejects invalid types for the inner schema', () => {\n      const result = schema.safeParse('not a number')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects floats', () => {\n      const result = schema.safeParse(3.14)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('basic validation with boolean schema', () => {\n    const schema = optional(boolean())\n\n    it('validates true', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('validates false', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(false)\n      }\n    })\n\n    it('validates undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(undefined)\n      }\n    })\n\n    it('rejects strings', () => {\n      const result = schema.safeParse('true')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(1)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('inner schema with constraints', () => {\n    const schema = optional(string({ minLength: 5, maxLength: 10 }))\n\n    it('validates values meeting inner schema constraints', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates values at minimum boundary', () => {\n      const result = schema.safeParse('abcde')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates values at maximum boundary', () => {\n      const result = schema.safeParse('1234567890')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values violating inner schema minimum constraint', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values violating inner schema maximum constraint', () => {\n      const result = schema.safeParse('this is too long')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings when inner schema has minLength', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('inner schema with default value', () => {\n    const schema = optional(withDefault(string(), 'default'))\n\n    it('applies default value when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('default')\n      }\n    })\n\n    it('does not apply default when explicit value is provided', () => {\n      const result = schema.safeParse('explicit')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('explicit')\n      }\n    })\n\n    it('does not apply default when empty string is provided', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('')\n      }\n    })\n  })\n\n  describe('inner schema with default value and constraints', () => {\n    const schema = optional(withDefault(string({ minLength: 5 }), 'default'))\n\n    it('applies default value when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('default')\n      }\n    })\n\n    it('validates explicit values against constraints', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects explicit values violating constraints', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('inner schema with invalid default value', () => {\n    const schema = optional(string({ default: 'bad', minLength: 5 }))\n\n    it('returns undefined when default value violates constraints', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(undefined)\n      }\n    })\n\n    it('still validates conforming explicit values', () => {\n      const result = schema.safeParse('valid')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('inner schema with integer default', () => {\n    const schema = optional(withDefault(integer(), 42))\n\n    it('applies default value when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(42)\n      }\n    })\n\n    it('does not apply default when explicit value is provided', () => {\n      const result = schema.safeParse(100)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(100)\n      }\n    })\n\n    it('does not apply default when zero is provided', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(0)\n      }\n    })\n  })\n\n  describe('inner schema with boolean default', () => {\n    const schema = optional(withDefault(boolean(), true))\n\n    it('applies default value when undefined is provided', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('does not apply default when explicit true is provided', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('does not apply default when explicit false is provided', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(false)\n      }\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = optional(string())\n\n    it('handles very long strings', () => {\n      const longString = 'a'.repeat(10000)\n      const result = schema.safeParse(longString)\n      expect(result.success).toBe(true)\n    })\n\n    it('handles strings with special characters', () => {\n      const result = schema.safeParse('hello\\nworld\\ttab')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles strings with unicode characters', () => {\n      const result = schema.safeParse('Hello 世界 🌍')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles empty string distinctly from undefined', () => {\n      const emptyResult = schema.safeParse('')\n      expect(emptyResult.success).toBe(true)\n      if (emptyResult.success) {\n        expect(emptyResult.value).toBe('')\n      }\n\n      const undefinedResult = schema.safeParse(undefined)\n      expect(undefinedResult.success).toBe(true)\n      if (undefinedResult.success) {\n        expect(undefinedResult.value).toBe(undefined)\n      }\n    })\n  })\n\n  describe('type distinctions', () => {\n    it('distinguishes between zero and undefined for integers', () => {\n      const schema = optional(integer())\n\n      const zeroResult = schema.safeParse(0)\n      expect(zeroResult.success).toBe(true)\n      if (zeroResult.success) {\n        expect(zeroResult.value).toBe(0)\n      }\n\n      const undefinedResult = schema.safeParse(undefined)\n      expect(undefinedResult.success).toBe(true)\n      if (undefinedResult.success) {\n        expect(undefinedResult.value).toBe(undefined)\n      }\n    })\n\n    it('distinguishes between false and undefined for booleans', () => {\n      const schema = optional(boolean())\n\n      const falseResult = schema.safeParse(false)\n      expect(falseResult.success).toBe(true)\n      if (falseResult.success) {\n        expect(falseResult.value).toBe(false)\n      }\n\n      const undefinedResult = schema.safeParse(undefined)\n      expect(undefinedResult.success).toBe(true)\n      if (undefinedResult.success) {\n        expect(undefinedResult.value).toBe(undefined)\n      }\n    })\n\n    it('distinguishes between empty string and undefined for strings', () => {\n      const schema = optional(string())\n\n      const emptyResult = schema.safeParse('')\n      expect(emptyResult.success).toBe(true)\n      if (emptyResult.success) {\n        expect(emptyResult.value).toBe('')\n      }\n\n      const undefinedResult = schema.safeParse(undefined)\n      expect(undefinedResult.success).toBe(true)\n      if (undefinedResult.success) {\n        expect(undefinedResult.value).toBe(undefined)\n      }\n    })\n  })\n\n  describe('nested optional schemas', () => {\n    const schema = optional(optional(string()))\n\n    it('validates defined values through nested optionals', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('validates undefined through nested optionals', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(undefined)\n      }\n    })\n\n    it('rejects invalid types through nested optionals', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('inner schema format constraints', () => {\n    const schema = optional(string({ format: 'uri' }))\n\n    it('validates values meeting format constraint', () => {\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values violating format constraint', () => {\n      const result = schema.safeParse('not a uri')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('integer constraint validation', () => {\n    const schema = optional(integer({ minimum: 0, maximum: 100 }))\n\n    it('validates values within range', () => {\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates values at minimum boundary', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates values at maximum boundary', () => {\n      const result = schema.safeParse(100)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values below minimum', () => {\n      const result = schema.safeParse(-1)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values above maximum', () => {\n      const result = schema.safeParse(101)\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/optional.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  Schema,\n  UnwrapValidator,\n  ValidationContext,\n  Validator,\n} from '../core.js'\nimport { memoizedTransformer } from '../util/memoize.js'\nimport { WithDefaultSchema } from './with-default.js'\n\n/**\n * Schema wrapper that makes a value optional (allows undefined).\n *\n * When the input is `undefined`, validation succeeds without running the\n * inner validator. If the inner validator has a default value (via `withDefault`),\n * that default will be applied in parse mode.\n *\n * @template TValidator - The wrapped validator type\n *\n * @example\n * ```ts\n * const schema = new OptionalSchema(l.string())\n * schema.validate(undefined) // success\n * schema.validate('hello')   // success\n * ```\n */\nexport class OptionalSchema<TValidator extends Validator> extends Schema<\n  InferInput<TValidator> | undefined,\n  UnwrapValidator<TValidator> extends WithDefaultSchema<infer TValidator>\n    ? InferOutput<TValidator>\n    : InferOutput<TValidator> | undefined\n> {\n  readonly type = 'optional' as const\n\n  constructor(readonly validator: TValidator) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    // Optimization: No need to apply child schema defaults in validation mode\n    if (input === undefined && ctx.options.mode === 'validate') {\n      return ctx.success(input)\n    }\n\n    // @NOTE The inner schema might apply a default value so we need to run it\n    // even if input is undefined.\n    const result = ctx.validate(input, this.validator)\n\n    if (result.success) {\n      return result\n    }\n\n    if (input === undefined) {\n      return ctx.success(input)\n    }\n\n    return result\n  }\n}\n\n/**\n * Creates an optional schema that allows undefined values.\n *\n * Wraps another schema to make it optional. When used in an object schema,\n * properties with optional schemas are not required.\n *\n * @param validator - The validator to make optional\n * @returns A new {@link OptionalSchema} instance\n *\n * @example\n * ```ts\n * // Optional string\n * const optionalBio = l.optional(l.string())\n *\n * // In an object - property is not required\n * const userSchema = l.object({\n *   name: l.string(),\n *   bio: l.optional(l.string()),\n * })\n * userSchema.parse({ name: 'Alice' }) // Valid, bio is undefined\n *\n * // With default value\n * const countSchema = l.optional(l.withDefault(l.integer(), 0))\n * countSchema.parse(undefined) // Returns 0\n * ```\n */\nexport const optional = /*#__PURE__*/ memoizedTransformer(function <\n  const TValidator extends Validator,\n>(validator: TValidator) {\n  return new OptionalSchema<TValidator>(validator)\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/params.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { array } from './array.js'\nimport { boolean } from './boolean.js'\nimport { enumSchema } from './enum.js'\nimport { integer } from './integer.js'\nimport { literal } from './literal.js'\nimport { optional } from './optional.js'\nimport { paramSchema, params, paramsSchema } from './params.js'\nimport { string } from './string.js'\n\ndescribe('ParamsSchema', () => {\n  describe('basic validation', () => {\n    const schema = params({\n      name: string(),\n      age: integer(),\n    })\n\n    it('validates plain objects with required params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-objects', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null values', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined values', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['Alice', 30])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects missing required properties', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid property types', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 'thirty',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('optional parameters', () => {\n    const schema = params({\n      name: string(),\n      age: optional(integer()),\n      active: optional(boolean()),\n    })\n\n    it('validates with all parameters present', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 30,\n        active: true,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with only required parameters', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with some optional parameters', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when required parameter is missing', () => {\n      const result = schema.safeParse({\n        age: 30,\n        active: true,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('additional unspecified parameters', () => {\n    const schema = params({\n      name: string(),\n    })\n\n    it('accepts string values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        extra: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts boolean values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        flag: true,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts integer values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        count: 42,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts array values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        tags: ['tag1', 'tag2'],\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts multiple additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        extra1: 'value',\n        extra2: 42,\n        extra3: true,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects null values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        extra: null,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        extra: { nested: 'object' },\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined values in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        extra: undefined,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('parameter types', () => {\n    describe('string parameters', () => {\n      const schema = params({\n        text: string(),\n      })\n\n      it('validates string values', () => {\n        const result = schema.safeParse({ text: 'hello' })\n        expect(result.success).toBe(true)\n      })\n\n      it('validates empty strings', () => {\n        const result = schema.safeParse({ text: '' })\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects non-string values', () => {\n        const result = schema.safeParse({ text: 123 })\n        expect(result.success).toBe(false)\n      })\n    })\n\n    describe('integer parameters', () => {\n      const schema = params({\n        count: integer(),\n      })\n\n      it('validates integer values', () => {\n        const result = schema.safeParse({ count: 42 })\n        expect(result.success).toBe(true)\n      })\n\n      it('validates zero', () => {\n        const result = schema.safeParse({ count: 0 })\n        expect(result.success).toBe(true)\n      })\n\n      it('validates negative integers', () => {\n        const result = schema.safeParse({ count: -10 })\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects non-integer values', () => {\n        const result = schema.safeParse({ count: 'not a number' })\n        expect(result.success).toBe(false)\n      })\n    })\n\n    describe('boolean parameters', () => {\n      const schema = params({\n        flag: boolean(),\n      })\n\n      it('validates true', () => {\n        const result = schema.safeParse({ flag: true })\n        expect(result.success).toBe(true)\n      })\n\n      it('validates false', () => {\n        const result = schema.safeParse({ flag: false })\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects non-boolean values', () => {\n        const result = schema.safeParse({ flag: 'true' })\n        expect(result.success).toBe(false)\n      })\n    })\n\n    describe('array parameters', () => {\n      const schema = params({\n        tags: array(string()),\n      })\n\n      it('validates string arrays', () => {\n        const result = schema.safeParse({ tags: ['tag1', 'tag2'] })\n        expect(result.success).toBe(true)\n      })\n\n      it('validates empty arrays', () => {\n        const result = schema.safeParse({ tags: [] })\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects arrays with invalid items', () => {\n        const result = schema.safeParse({ tags: ['tag1', 123] })\n        expect(result.success).toBe(false)\n      })\n    })\n  })\n\n  describe('coercion', () => {\n    it('throws for invalid enum values', () => {\n      const schema = params({\n        status: enumSchema(['active', 'inactive']),\n      })\n      expect(() => schema.fromURLSearchParams('status=unknown')).toThrow(\n        'Expected one of \"active\" or \"inactive\"',\n      )\n    })\n\n    it('throws for invalid const values', () => {\n      const schema = params({\n        version: literal(42),\n      })\n      expect(() => schema.fromURLSearchParams('version=99')).toThrow(\n        'Expected 42',\n      )\n    })\n\n    it('handles negative integer enum values', () => {\n      const schema = params({\n        offset: enumSchema([-10, 0, 10]),\n      })\n      const result = schema.fromURLSearchParams('offset=-10')\n      expect(result).toEqual({ offset: -10 })\n    })\n\n    it('handles boolean const false', () => {\n      const schema = params({\n        disabled: literal(false),\n      })\n      const result = schema.fromURLSearchParams('disabled=false')\n      expect(result).toEqual({ disabled: false })\n    })\n  })\n\n  describe('fromURLSearchParams', () => {\n    const schema = params({\n      name: string(),\n      age: optional(integer()),\n      active: optional(boolean()),\n      tags: optional(array(string())),\n      ids: optional(array(integer())),\n      bools: optional(array(boolean())),\n    })\n\n    it('parses string parameters', () => {\n      const result = schema.fromURLSearchParams('name=Alice')\n      expect(result).toEqual({ name: 'Alice' })\n    })\n\n    it('parses and coerces boolean true', () => {\n      const result = schema.fromURLSearchParams('name=Alice&active=true')\n      expect(result).toEqual({ name: 'Alice', active: true })\n    })\n\n    it('parses and coerces boolean false', () => {\n      const result = schema.fromURLSearchParams('name=Alice&active=false')\n      expect(result).toEqual({ name: 'Alice', active: false })\n    })\n\n    it('parses and coerces integer values', () => {\n      const result = schema.fromURLSearchParams('name=Alice&age=30')\n      expect(result).toEqual({ name: 'Alice', age: 30 })\n    })\n\n    it('parses and coerces negative integers', () => {\n      const result = schema.fromURLSearchParams('name=Alice&age=-5')\n      expect(result).toEqual({ name: 'Alice', age: -5 })\n    })\n\n    it('does not coerce non-integer numbers', () => {\n      const result = schema.fromURLSearchParams('name=Alice&extra=3.14')\n      expect(result).toEqual({ name: 'Alice', extra: '3.14' })\n    })\n\n    it('keeps string values for string schema even if they look like numbers', () => {\n      const result = schema.fromURLSearchParams('name=123')\n      expect(result).toEqual({ name: '123' })\n    })\n\n    it('parses multiple values as array', () => {\n      const result = schema.fromURLSearchParams('name=Alice&tags=one&tags=two')\n      expect(result).toEqual({ name: 'Alice', tags: ['one', 'two'] })\n    })\n\n    it('does not coerce numeric values of unknown params', () => {\n      expect(\n        schema.fromURLSearchParams('name=Alice&num=1&num=2&num=3&foo=3'),\n      ).toEqual({ name: 'Alice', num: ['1', '2', '3'], foo: '3' })\n\n      expect(\n        schema.fromURLSearchParams('name=Alice&val=true&val=123&val=text'),\n      ).toEqual({ name: 'Alice', val: ['true', '123', 'text'] })\n    })\n\n    it('handles empty URLSearchParams', () => {\n      expect(() => schema.fromURLSearchParams(new URLSearchParams())).toThrow()\n      expect(() => schema.fromURLSearchParams('')).toThrow()\n    })\n\n    it('handles multiple parameters', () => {\n      const urlParams = new URLSearchParams(\n        'name=Alice&age=30&active=true&extra=value',\n      )\n      const result = schema.fromURLSearchParams(urlParams)\n      expect(result).toEqual({\n        name: 'Alice',\n        age: 30,\n        active: true,\n        extra: 'value',\n      })\n    })\n\n    it('coerces single values into arrays in parse mode', () => {\n      expect(\n        schema.fromURLSearchParams([\n          ['name', 'Alice'],\n          ['tags', 'tag1'],\n        ]),\n      ).toEqual({ name: 'Alice', tags: ['tag1'] })\n\n      expect(\n        schema.fromURLSearchParams([\n          ['name', 'Alice'],\n          ['tags', 'true'],\n        ]),\n      ).toEqual({ name: 'Alice', tags: ['true'] })\n\n      expect(\n        schema.fromURLSearchParams([\n          ['name', 'Alice'],\n          ['tags', '1'],\n        ]),\n      ).toEqual({ name: 'Alice', tags: ['1'] })\n    })\n\n    it('coerces single boolean values into arrays in parse mode', () => {\n      expect(\n        schema.fromURLSearchParams([\n          ['name', 'Alice'],\n          ['bools', 'true'],\n        ]),\n      ).toEqual({ name: 'Alice', bools: [true] })\n\n      expect(\n        schema.fromURLSearchParams([\n          ['name', 'Alice'],\n          ['bools', 'false'],\n        ]),\n      ).toEqual({ name: 'Alice', bools: [false] })\n\n      expect(() =>\n        schema.fromURLSearchParams([\n          ['name', 'Alice'],\n          ['bools', 'notabool'],\n        ]),\n      ).toThrow('Expected boolean value type (got string) at $.bools')\n\n      expect(() =>\n        schema.fromURLSearchParams(\n          [\n            ['name', 'Alice'],\n            ['bools', '2'],\n          ],\n          {\n            path: ['foo', 'bar'],\n          },\n        ),\n      ).toThrow('Expected boolean value type (got string) at $.foo.bar.bools')\n    })\n\n    it('ignores empty string values', () => {\n      const result = schema.fromURLSearchParams([\n        ['name', 'Alice'],\n        ['extra', ''],\n      ])\n      expect(result).toEqual({ name: 'Alice' })\n    })\n\n    it('ignores empty string values for known parameters', () => {\n      const result = schema.fromURLSearchParams([\n        ['name', 'Alice'],\n        ['age', ''],\n      ])\n      expect(result).toEqual({ name: 'Alice' })\n    })\n  })\n\n  describe('toURLSearchParams', () => {\n    const schema = params({\n      name: string(),\n      age: optional(integer()),\n      active: optional(boolean()),\n    })\n\n    it('converts string parameters', () => {\n      const result = schema.toURLSearchParams({ name: 'Alice' })\n      expect(result.toString()).toBe('name=Alice')\n    })\n\n    it('converts integer parameters', () => {\n      const result = schema.toURLSearchParams({ name: 'Alice', age: 30 })\n      expect(result.toString()).toBe('name=Alice&age=30')\n    })\n\n    it('converts boolean parameters', () => {\n      const result = schema.toURLSearchParams({ name: 'Alice', active: true })\n      expect(result.toString()).toBe('name=Alice&active=true')\n    })\n\n    it('converts multiple parameters', () => {\n      const result = schema.toURLSearchParams({\n        name: 'Alice',\n        age: 30,\n        active: true,\n      })\n      expect(result.toString()).toBe('name=Alice&age=30&active=true')\n    })\n\n    it('handles array values', () => {\n      const result = schema.toURLSearchParams({\n        name: 'Alice',\n        // @ts-expect-error\n        tags: ['tag1', 'tag2'],\n      })\n      expect(result.toString()).toBe('name=Alice&tags=tag1&tags=tag2')\n    })\n\n    it('skips undefined values', () => {\n      const result = schema.toURLSearchParams({\n        name: 'Alice',\n        age: undefined,\n      })\n      expect(result.toString()).toBe('name=Alice')\n    })\n\n    it('handles empty arrays', () => {\n      const result = schema.toURLSearchParams({\n        name: 'Alice',\n        // @ts-expect-error\n        tags: [],\n      })\n      expect(result.toString()).toBe('name=Alice')\n    })\n\n    it('rejects arrays with multiple types', () => {\n      expect(() => {\n        schema.toURLSearchParams({\n          name: 'Alice',\n          // @ts-expect-error\n          values: [1, true, 'text'],\n        })\n      }).toThrow()\n    })\n\n    it('handles arrays with multiple types', () => {\n      const result = schema.toURLSearchParams({\n        name: 'Alice',\n        // @ts-expect-error\n        values: ['foo', 'bar'],\n      })\n      expect(result.toString()).toBe('name=Alice&values=foo&values=bar')\n    })\n\n    it('handles undefined input', () => {\n      // @ts-expect-error\n      expect(() => schema.toURLSearchParams(undefined)).toThrow()\n    })\n\n    it('converts negative integers', () => {\n      const result = schema.toURLSearchParams({ name: 'Alice', age: -5 })\n      expect(result.toString()).toBe('name=Alice&age=-5')\n    })\n\n    it('converts zero', () => {\n      const result = schema.toURLSearchParams({ name: 'Alice', age: 0 })\n      expect(result.toString()).toBe('name=Alice&age=0')\n    })\n\n    it('converts false boolean', () => {\n      const result = schema.toURLSearchParams({ name: 'Alice', active: false })\n      expect(result.toString()).toBe('name=Alice&active=false')\n    })\n  })\n\n  describe('roundtrip conversion', () => {\n    const schema = params({\n      name: string(),\n      age: optional(integer()),\n      active: optional(boolean()),\n    })\n\n    it('roundtrips simple params correctly', () => {\n      const original = { name: 'Alice', age: 30, active: true }\n      const urlParams = schema.toURLSearchParams(original)\n      const result = schema.fromURLSearchParams(urlParams)\n      expect(result).toEqual(original)\n    })\n\n    it('roundtrips params with arrays', () => {\n      const original = { name: 'Alice', tags: ['tag1', 'tag2'] }\n      const urlParams = schema.toURLSearchParams(original)\n      const result = schema.fromURLSearchParams(urlParams)\n      expect(result).toEqual(original)\n    })\n\n    it('roundtrips params with boolean false', () => {\n      const original = { name: 'Alice', active: false }\n      const urlParams = schema.toURLSearchParams(original)\n      const result = schema.fromURLSearchParams(urlParams)\n      expect(result).toEqual(original)\n    })\n\n    it('roundtrips params with zero', () => {\n      const original = { name: 'Alice', age: 0 }\n      const urlParams = schema.toURLSearchParams(original)\n      const result = schema.fromURLSearchParams(urlParams)\n      expect(result).toEqual(original)\n    })\n  })\n\n  describe('empty schema', () => {\n    const schema = params()\n\n    it('validates empty object', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts additional params', () => {\n      const result = schema.safeParse({ extra: 'value' })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-objects', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('complex scenarios', () => {\n    const schema = params({\n      query: string({ minLength: 1 }),\n      limit: optional(integer({ minimum: 1, maximum: 100 })),\n      offset: optional(integer({ minimum: 0 })),\n      filters: optional(array(string())),\n    })\n\n    it('validates typical query parameters', () => {\n      const result = schema.safeParse({\n        query: 'search term',\n        limit: 10,\n        offset: 0,\n        filters: ['filter1', 'filter2'],\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates minimal query', () => {\n      const result = schema.safeParse({\n        query: 'search',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects empty query string', () => {\n      const result = schema.safeParse({\n        query: '',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects limit out of range', () => {\n      const result = schema.safeParse({\n        query: 'search',\n        limit: 200,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects negative offset', () => {\n      const result = schema.safeParse({\n        query: 'search',\n        offset: -1,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts additional unspecified params', () => {\n      const result = schema.safeParse({\n        query: 'search',\n        extra: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = params({\n      name: string(),\n    })\n\n    it('rejects objects with custom prototypes', () => {\n      const obj = Object.create({ inherited: 'value' })\n      obj.name = 'Alice'\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates with numeric string keys', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        '123': 'numeric key',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('handles empty string keys in additional params', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        '': 'empty key',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('memoized params schema', () => {\n    it('returns the same instance when no shape is provided', () => {\n      expect(params()).toBe(params())\n    })\n\n    it('returns different instances when (identical) shapes are provided', () => {\n      const schemaA = params({ a: string() })\n      const schemaB = params({ a: string() })\n      expect(schemaA).not.toBe(schemaB)\n    })\n  })\n})\n\ndescribe('paramSchema', () => {\n  describe('scalar values', () => {\n    it('validates boolean values', () => {\n      const result = paramSchema.safeParse(true)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates integer values', () => {\n      const result = paramSchema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates string values', () => {\n      const result = paramSchema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates empty strings', () => {\n      const result = paramSchema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates zero', () => {\n      const result = paramSchema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates negative integers', () => {\n      const result = paramSchema.safeParse(-50)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates false boolean', () => {\n      const result = paramSchema.safeParse(false)\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('array values', () => {\n    it('validates arrays of booleans', () => {\n      const result = paramSchema.safeParse([true, false, true])\n      expect(result.success).toBe(true)\n    })\n\n    it('validates arrays of integers', () => {\n      const result = paramSchema.safeParse([1, 2, 3, 4])\n      expect(result.success).toBe(true)\n    })\n\n    it('validates arrays of strings', () => {\n      const result = paramSchema.safeParse(['foo', 'bar', 'baz'])\n      expect(result.success).toBe(true)\n    })\n\n    it('validates empty arrays', () => {\n      const result = paramSchema.safeParse([])\n      expect(result.success).toBe(true)\n    })\n\n    it('validates arrays with single element', () => {\n      const result = paramSchema.safeParse(['single'])\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects arrays with mixed scalar types', () => {\n      const result = paramSchema.safeParse([true, 42, 'text'])\n      expect(result.success).toBe(false)\n    })\n\n    it('validates arrays with negative integers', () => {\n      const result = paramSchema.safeParse([-1, -2, -3])\n      expect(result.success).toBe(true)\n    })\n\n    it('validates arrays with empty strings', () => {\n      const result = paramSchema.safeParse(['', 'non-empty', ''])\n      expect(result.success).toBe(true)\n    })\n\n    it('validates arrays with zero', () => {\n      const result = paramSchema.safeParse([0, 1, 2])\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects nested arrays', () => {\n      const result = paramSchema.safeParse([\n        [1, 2],\n        [3, 4],\n      ])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with null values', () => {\n      const result = paramSchema.safeParse([1, null, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with undefined values', () => {\n      const result = paramSchema.safeParse([1, undefined, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with object values', () => {\n      const result = paramSchema.safeParse([1, { key: 'value' }, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with floating point numbers', () => {\n      const result = paramSchema.safeParse([1, 2.5, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with NaN', () => {\n      const result = paramSchema.safeParse([1, NaN, 3])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays with Infinity', () => {\n      const result = paramSchema.safeParse([1, Infinity, 3])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('invalid values', () => {\n    it('rejects null values', () => {\n      const result = paramSchema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined values', () => {\n      const result = paramSchema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object values', () => {\n      const result = paramSchema.safeParse({ key: 'value' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects floating point numbers', () => {\n      const result = paramSchema.safeParse(3.14)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects NaN', () => {\n      const result = paramSchema.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Infinity', () => {\n      const result = paramSchema.safeParse(Infinity)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects -Infinity', () => {\n      const result = paramSchema.safeParse(-Infinity)\n      expect(result.success).toBe(false)\n    })\n  })\n})\n\ndescribe('paramsSchema', () => {\n  it('validates empty object', () => {\n    const result = paramsSchema.safeParse({})\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with boolean parameters', () => {\n    const result = paramsSchema.safeParse({\n      enabled: true,\n      disabled: false,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with integer parameters', () => {\n    const result = paramsSchema.safeParse({\n      limit: 10,\n      offset: 0,\n      count: 100,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with string parameters', () => {\n    const result = paramsSchema.safeParse({\n      name: 'Alice',\n      query: 'search term',\n      cursor: 'abc123',\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with array parameters', () => {\n    const result = paramsSchema.safeParse({\n      tags: ['tag1', 'tag2', 'tag3'],\n      ids: [1, 2, 3, 4],\n      flags: [true, false, true],\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with mixed parameter types', () => {\n    const result = paramsSchema.safeParse({\n      name: 'Alice',\n      age: 30,\n      active: true,\n      tags: ['user', 'admin'],\n      limit: 50,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with empty string parameters', () => {\n    const result = paramsSchema.safeParse({\n      query: '',\n      cursor: '',\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with negative integer parameters', () => {\n    const result = paramsSchema.safeParse({\n      offset: -10,\n      delta: -5,\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('validates object with empty array parameters', () => {\n    const result = paramsSchema.safeParse({\n      tags: [],\n      ids: [],\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects object with arrays of mixed scalar types', () => {\n    const result = paramsSchema.safeParse({\n      values: [true, 42, 'text'],\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates object with numeric string keys', () => {\n    const result = paramsSchema.safeParse({\n      '0': 'value0',\n      '1': 'value1',\n      '2': 'value2',\n    })\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects non-object values', () => {\n    const result = paramsSchema.safeParse('not an object')\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects null values', () => {\n    const result = paramsSchema.safeParse(null)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects undefined values', () => {\n    const result = paramsSchema.safeParse(undefined)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects arrays', () => {\n    const result = paramsSchema.safeParse([1, 2, 3])\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with null parameter values', () => {\n    const result = paramsSchema.safeParse({\n      name: 'Alice',\n      invalid: null,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with floating point parameter values', () => {\n    const result = paramsSchema.safeParse({\n      value: 3.14,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with NaN parameter values', () => {\n    const result = paramsSchema.safeParse({\n      value: NaN,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with Infinity parameter values', () => {\n    const result = paramsSchema.safeParse({\n      value: Infinity,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with object parameter values', () => {\n    const result = paramsSchema.safeParse({\n      nested: { key: 'value' },\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with nested array parameter values', () => {\n    const result = paramsSchema.safeParse({\n      nested: [\n        [1, 2],\n        [3, 4],\n      ],\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with arrays containing invalid values', () => {\n    const result = paramsSchema.safeParse({\n      tags: ['valid', null, 'also valid'],\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with arrays containing objects', () => {\n    const result = paramsSchema.safeParse({\n      items: [1, { key: 'value' }, 3],\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object with arrays containing floating point numbers', () => {\n    const result = paramsSchema.safeParse({\n      values: [1, 2.5, 3],\n    })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects when one parameter is invalid', () => {\n    const result = paramsSchema.safeParse({\n      valid1: 'string',\n      valid2: 42,\n      invalid: null,\n      valid3: true,\n    })\n    expect(result.success).toBe(false)\n  })\n\n  describe('edge cases', () => {\n    it('validates single parameter', () => {\n      const result = paramsSchema.safeParse({\n        single: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates many parameters', () => {\n      const manyParams: Record<string, string> = {}\n      for (let i = 0; i < 100; i++) {\n        manyParams[`param${i}`] = `value${i}`\n      }\n      const result = paramsSchema.safeParse(manyParams)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates parameters with long string values', () => {\n      const result = paramsSchema.safeParse({\n        longString: 'a'.repeat(1000),\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates parameters with large integer values', () => {\n      const result = paramsSchema.safeParse({\n        largeInt: 2147483647,\n        negativeInt: -2147483648,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates parameters with long arrays', () => {\n      const result = paramsSchema.safeParse({\n        longArray: Array.from({ length: 100 }, (_, i) => i),\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates parameters with special characters in keys', () => {\n      const result = paramsSchema.safeParse({\n        'key-with-dashes': 'value',\n        key_with_underscores: 'value',\n        'key.with.dots': 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('preserves original object when no transformations occur', () => {\n      const input = {\n        name: 'Alice',\n        age: 30,\n        tags: ['user'],\n      }\n      const result = paramsSchema.safeParse(input)\n\n      if (result.success) {\n        expect(result.value).toBe(input)\n      } else {\n        throw new Error('Expected validation to succeed')\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/params.ts",
    "content": "import { isPlainObject } from '@atproto/lex-data'\nimport {\n  Infer,\n  InferInput,\n  InferOutput,\n  Issue,\n  IssueInvalidType,\n  IssueInvalidValue,\n  LexValidationError,\n  ParseOptions,\n  Schema,\n  ValidationContext,\n  Validator,\n  WithOptionalProperties,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\nimport { memoizedOptions } from '../util/memoize.js'\nimport { ArraySchema, array } from './array.js'\nimport { BooleanSchema, boolean } from './boolean.js'\nimport { dict } from './dict.js'\nimport { EnumSchema } from './enum.js'\nimport { IntegerSchema, integer } from './integer.js'\nimport { LiteralSchema } from './literal.js'\nimport { OptionalSchema, optional } from './optional.js'\nimport { StringSchema, string } from './string.js'\nimport { union } from './union.js'\nimport { WithDefaultSchema } from './with-default.js'\n\n/**\n * Scalar types allowed in URL parameters: boolean, integer, or string.\n */\nexport type ParamScalar = Infer<typeof paramScalarSchema>\nconst paramScalarSchema = union([boolean(), integer(), string()])\n\n/**\n * A single parameter value: scalar or array of scalars.\n */\nexport type Param = Infer<typeof paramSchema>\n\n/**\n * Schema for validating individual parameter values.\n */\nexport const paramSchema = union([\n  paramScalarSchema,\n  array(boolean()),\n  array(integer()),\n  array(string()),\n])\n\n/**\n * Type for a params object with string keys and optional param values.\n */\nexport type Params = Infer<typeof paramsSchema>\n\n/**\n * Schema for validating arbitrary params objects.\n */\nexport const paramsSchema = dict(string(), optional(paramSchema))\n\nexport type ParamScalarValidator =\n  // @NOTE In order to properly coerce URLSearchParams, we need to distinguish\n  // between scalar and array validators, requiring to be able to detect which\n  // schema types are being used, restricting the allowed param validators here.\n  | LiteralSchema<string>\n  | LiteralSchema<number>\n  | LiteralSchema<boolean>\n  | EnumSchema<string>\n  | EnumSchema<number>\n  // | EnumSchema<boolean> // Boolean lexicon definitions don't allow \"enum\"\n  | StringSchema<any>\n  | BooleanSchema\n  | IntegerSchema\n\ntype AsArrayParamSchema<TSchema extends Validator> =\n  // This allows to \"distribute\" any union of scalar validators into a union of\n  // arrays of those validators, instead of an array of union. If TSchema is\n  // BooleanSchema | IntegerSchema, we want the result to be\n  // ArraySchema<BooleanSchema> | ArraySchema<IntegerSchema>, not\n  // ArraySchema<BooleanSchema | IntegerSchema>, since the latter would allow\n  // arrays with mixed types (e.g. [true, 42]), which we don't want.\n  TSchema extends any ? ArraySchema<TSchema> : never\n\nexport type ParamValueValidator =\n  | ParamScalarValidator\n  | AsArrayParamSchema<ParamScalarValidator>\n\nexport type ParamValidator =\n  | ParamValueValidator\n  | OptionalSchema<ParamValueValidator>\n  | OptionalSchema<WithDefaultSchema<ParamValueValidator>>\n  | WithDefaultSchema<ParamValueValidator>\n\n/**\n * Type representing the shape of a params schema definition.\n *\n * Maps parameter names to their validators (must be Param or undefined).\n */\nexport type ParamsShape = {\n  [x: string]: ParamValidator\n}\n\n/**\n * Schema for validating URL query parameters in Lexicon endpoints.\n *\n * Params are the query string parameters passed to queries, procedures,\n * and subscriptions. Values must be scalars (boolean, integer, string)\n * or arrays of scalars, as they need to be serializable to URL format.\n *\n * Provides methods for converting to/from URLSearchParams.\n *\n * @template TShape - The params shape type mapping names to validators\n *\n * @example\n * ```ts\n * const schema = new ParamsSchema({\n *   limit: l.optional(l.integer({ minimum: 1, maximum: 100 })),\n *   cursor: l.optional(l.string()),\n * })\n * ```\n */\nexport class ParamsSchema<\n  const TShape extends ParamsShape = ParamsShape,\n> extends Schema<\n  WithOptionalProperties<{\n    [K in keyof TShape]: InferInput<TShape[K]>\n  }>,\n  WithOptionalProperties<{\n    [K in keyof TShape]: InferOutput<TShape[K]>\n  }>\n> {\n  readonly type = 'params' as const\n\n  constructor(readonly shape: TShape) {\n    super()\n  }\n\n  get shapeValidators(): Map<string, ParamValidator> {\n    const map = new Map(Object.entries(this.shape))\n\n    return lazyProperty(this, 'shapeValidators', map)\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isPlainObject(input)) {\n      return ctx.issueUnexpectedType(input, 'object')\n    }\n\n    // Lazily copy value\n    let copy: undefined | Record<string, unknown>\n\n    // Ensure that non-specified params conform to param schema\n    for (const key in input) {\n      if (this.shapeValidators.has(key)) continue\n\n      const result = ctx.validateChild(input, key, paramSchema)\n      if (!result.success) return result\n\n      if (result.value !== input[key]) {\n        if (ctx.options.mode === 'validate') {\n          // In \"validate\" mode, we can't modify the input, so we issue an error\n          return ctx.issueInvalidPropertyValue(input, key, [result.value])\n        }\n\n        copy ??= { ...input }\n        copy[key] = result.value\n      }\n    }\n\n    for (const [key, propDef] of this.shapeValidators) {\n      const result = ctx.validateChild(input, key, propDef)\n      if (!result.success) {\n        if (!(key in input)) {\n          // Transform into \"required key\" issue\n          return ctx.issueRequiredKey(input, key)\n        }\n\n        return result\n      }\n\n      // Skip copying if key is not present in input (and value is undefined)\n      if (result.value === undefined && !(key in input)) {\n        continue\n      }\n\n      if (!Object.is(result.value, input[key])) {\n        if (ctx.options.mode === 'validate') {\n          // In \"validate\" mode, we can't modify the input, so we issue an error\n          return ctx.issueInvalidPropertyValue(input, key, [result.value])\n        }\n\n        // Copy on write\n        copy ??= { ...input }\n        copy[key] = result.value\n      }\n    }\n\n    return ctx.success(copy ?? input)\n  }\n\n  fromURLSearchParams(\n    input: string | Iterable<[string, string]>,\n    options?: ParseOptions,\n  ): InferOutput<this> {\n    const params: Record<string, unknown> = {}\n\n    const iterable =\n      typeof input === 'string' ? new URLSearchParams(input) : input\n    const entries =\n      iterable instanceof URLSearchParams ? iterable.entries() : iterable\n\n    for (const [name, value] of entries) {\n      // Ignore empty strings\n      if (!value) continue\n\n      const validator = this.shapeValidators.get(name)\n      const innerValidator = validator ? unwrapSchema(validator) : undefined\n      const expectsArray = innerValidator instanceof ArraySchema\n      const scalarValidator = expectsArray\n        ? unwrapSchema(innerValidator.validator)\n        : innerValidator\n\n      const coerced = coerceParam(name, value, scalarValidator, options)\n\n      const currentParam = params[name]\n      if (currentParam === undefined) {\n        params[name] = expectsArray ? [coerced] : coerced\n      } else if (Array.isArray(currentParam)) {\n        currentParam.push(coerced)\n      } else {\n        params[name] = [currentParam, coerced]\n      }\n    }\n\n    return this.parse(params, options)\n  }\n\n  toURLSearchParams(input: InferInput<this>): URLSearchParams {\n    const urlSearchParams = new URLSearchParams()\n\n    // @NOTE We apply defaults here to ensure that server with different\n    // defaults still receive all expected parameters.\n    const params = this.parse(input)\n\n    for (const [key, value] of Object.entries(params)) {\n      if (Array.isArray(value)) {\n        for (const v of value) {\n          urlSearchParams.append(key, String(v))\n        }\n      } else if (value !== undefined) {\n        urlSearchParams.append(key, String(value))\n      }\n    }\n\n    return urlSearchParams\n  }\n}\n\nfunction coerceParam(\n  name: string,\n  param: string,\n  schema?: ParamScalarValidator,\n  options?: ParseOptions,\n): ParamScalar {\n  let issue: Issue\n\n  if (!schema) {\n    // The param is unknown (not defined in schema), so we don't apply any\n    // coercion and just return the string value.\n    return param\n  } else if (schema instanceof StringSchema) {\n    return param\n  } else if (schema instanceof IntegerSchema) {\n    if (/^-?\\d+$/.test(param)) return Number(param)\n    issue = new IssueInvalidType(paramPath(name, options), param, ['integer'])\n  } else if (schema instanceof BooleanSchema) {\n    if (param === 'true') return true\n    if (param === 'false') return false\n    issue = new IssueInvalidType(paramPath(name, options), param, ['boolean'])\n  } else if (schema instanceof LiteralSchema) {\n    const { value } = schema\n    if (String(value) === param) return value\n    issue = new IssueInvalidValue(paramPath(name, options), param, [value])\n  } else if (schema instanceof EnumSchema) {\n    const { values } = schema\n    for (const value of values) {\n      if (String(value) === param) return value\n    }\n    issue = new IssueInvalidValue(paramPath(name, options), param, values)\n  } else {\n    // This should never happen. If it *does*, it means that the user of\n    // lex-schema is mixing different versions of the lib, which is not\n    // supported. Throwing an error here is better than silently accepting\n    // invalid params and causing unexpected behavior down the line (ie. error\n    // message returning the string value instead of the expected\n    // boolean/number/string value).\n    throw new Error(`Unsupported schema type for param coercion: ${schema}`)\n  }\n\n  // We were not able to coerce the param to the expected type. There is no\n  // point in returning the original string value since it doesn't conform to\n  // the expected schema, so we throw a validation error instead. We could\n  // return the \"param\" here, which would cause the validation to fail later on\n  // (see fromURLSearchParams()'s return statement). The main benefit of\n  // returning the original \"param\" value is that the error path would include\n  // the index of the param in case of array params (e.g. \"tags[1]\"), which\n  // could be helpful for debugging. The cost overhead is not worth it though\n  // (IMO).\n  throw new LexValidationError([issue])\n}\n\nfunction paramPath(key: string, options?: ParseOptions) {\n  return options?.path ? [...options.path, key] : [key]\n}\n\n/**\n * Creates a params schema for URL query parameters.\n *\n * Params schemas validate query string parameters for Lexicon endpoints.\n * Values must be boolean, integer, string, or arrays of those types.\n *\n * @param properties - Object mapping parameter names to their validators\n * @returns A new {@link ParamsSchema} instance\n *\n * @example\n * ```ts\n * // Simple pagination params\n * const paginationParams = l.params({\n *   limit: l.optional(l.withDefault(l.integer({ minimum: 1, maximum: 100 }), 50)),\n *   cursor: l.optional(l.string()),\n * })\n *\n * // Required parameter\n * const actorParams = l.params({\n *   actor: l.string({ format: 'at-identifier' }),\n * })\n *\n * // Array parameter (multiple values)\n * const filterParams = l.params({\n *   tags: l.optional(l.array(l.string())),\n * })\n *\n * // Convert from URL\n * const urlParams = new URLSearchParams('limit=25&cursor=abc')\n * const validated = paginationParams.fromURLSearchParams(urlParams)\n *\n * // Convert to URL\n * const searchParams = paginationParams.toURLSearchParams({ limit: 25 })\n * ```\n */\nexport const params = /*#__PURE__*/ memoizedOptions(function params<\n  const TShape extends ParamsShape = NonNullable<unknown>,\n>(properties: TShape = {} as TShape) {\n  return new ParamsSchema<TShape>(properties)\n})\n\ntype UnwrapSchema<S extends Validator> =\n  S extends OptionalSchema<infer U>\n    ? UnwrapSchema<U>\n    : S extends WithDefaultSchema<infer U>\n      ? UnwrapSchema<U>\n      : S\n\nfunction unwrapSchema<S extends Validator>(schema: S): UnwrapSchema<S> {\n  while (\n    schema instanceof OptionalSchema ||\n    schema instanceof WithDefaultSchema\n  ) {\n    return unwrapSchema(schema.validator)\n  }\n  return schema as UnwrapSchema<S>\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/payload.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { lexMap } from './lex-map.js'\nimport { object } from './object.js'\nimport { payload } from './payload.js'\nimport { string } from './string.js'\n\ndescribe('Payload', () => {\n  describe('basic construction', () => {\n    it('creates payload with encoding and no schema', () => {\n      const def = payload('application/json', undefined)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload with encoding and schema', () => {\n      const schema = object({\n        name: string(),\n      })\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n\n    it('creates payload with undefined encoding and undefined schema', () => {\n      const def = payload(undefined, undefined)\n      expect(def.encoding).toBeUndefined()\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload with text encoding', () => {\n      const def = payload('text/plain', undefined)\n      expect(def.encoding).toBe('text/plain')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload with text/html encoding', () => {\n      const def = payload('text/html', undefined)\n      expect(def.encoding).toBe('text/html')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload with application/octet-stream encoding', () => {\n      const def = payload('application/octet-stream', undefined)\n      expect(def.encoding).toBe('application/octet-stream')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload with image encoding', () => {\n      const def = payload('image/png', undefined)\n      expect(def.encoding).toBe('image/png')\n      expect(def.schema).toBeUndefined()\n    })\n  })\n\n  describe('encoding types', () => {\n    it('handles application/json encoding', () => {\n      const def = payload('application/json', undefined)\n      expect(def.encoding).toBe('application/json')\n    })\n\n    it('handles text/* encodings', () => {\n      const textPlain = payload('text/plain', undefined)\n      expect(textPlain.encoding).toBe('text/plain')\n\n      const textHtml = payload('text/html', undefined)\n      expect(textHtml.encoding).toBe('text/html')\n\n      const textCss = payload('text/css', undefined)\n      expect(textCss.encoding).toBe('text/css')\n    })\n\n    it('handles binary encodings', () => {\n      const octetStream = payload('application/octet-stream', undefined)\n      expect(octetStream.encoding).toBe('application/octet-stream')\n\n      const imagePng = payload('image/png', undefined)\n      expect(imagePng.encoding).toBe('image/png')\n\n      const imageJpeg = payload('image/jpeg', undefined)\n      expect(imageJpeg.encoding).toBe('image/jpeg')\n    })\n\n    it('handles custom mime types', () => {\n      const def = payload('application/vnd.custom+json', undefined)\n      expect(def.encoding).toBe('application/vnd.custom+json')\n    })\n  })\n\n  describe('with schemas', () => {\n    it('creates payload with object schema', () => {\n      const schema = object({\n        id: integer(),\n        name: string(),\n      })\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n\n    it('creates payload with string schema', () => {\n      const schema = string()\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n\n    it('creates payload with integer schema', () => {\n      const schema = integer()\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n\n    it('creates payload with unknown schema', () => {\n      const schema = object({})\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n\n    it('creates payload with complex nested schema', () => {\n      const schema = object({\n        user: object({\n          id: integer(),\n          name: string(),\n          email: string({ format: 'uri' }),\n        }),\n        metadata: object({\n          createdAt: string({ format: 'datetime' }),\n          updatedAt: string({ format: 'datetime' }),\n        }),\n      })\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n  })\n\n  describe('validation constraints', () => {\n    it('throws error when schema is defined but encoding is undefined', () => {\n      const schema = string()\n      expect(() => {\n        // @ts-expect-error\n        payload(undefined, schema)\n      }).toThrow(TypeError)\n      expect(() => {\n        // @ts-expect-error\n        payload(undefined, schema)\n      }).toThrow('schema cannot be defined when encoding is undefined')\n    })\n\n    it('throws error when object schema is defined but encoding is undefined', () => {\n      const schema = object({\n        name: string(),\n      })\n      expect(() => {\n        // @ts-expect-error\n        payload(undefined, schema)\n      }).toThrow(TypeError)\n      expect(() => {\n        // @ts-expect-error\n        payload(undefined, schema)\n      }).toThrow('schema cannot be defined when encoding is undefined')\n    })\n\n    it('throws error when integer schema is defined but encoding is undefined', () => {\n      const schema = integer()\n      expect(() => {\n        // @ts-expect-error\n        payload(undefined, schema)\n      }).toThrow(TypeError)\n    })\n\n    it('allows undefined encoding with undefined schema', () => {\n      expect(() => {\n        payload(undefined, undefined)\n      }).not.toThrow()\n    })\n\n    it('allows defined encoding with undefined schema', () => {\n      expect(() => {\n        payload('application/json', undefined)\n      }).not.toThrow()\n    })\n\n    it('allows defined encoding with defined schema', () => {\n      const schema = string()\n      expect(() => {\n        payload('application/json', schema)\n      }).not.toThrow()\n    })\n  })\n\n  describe('property access', () => {\n    it('has accessible encoding property', () => {\n      const def = payload('application/json', undefined)\n      expect(def.encoding).toBe('application/json')\n    })\n\n    it('has accessible schema property', () => {\n      const schema = string()\n      const def = payload('application/json', schema)\n      expect(def.schema).toBe(schema)\n    })\n\n    it('encoding property is immutable in TypeScript', () => {\n      const def = payload('application/json', undefined)\n      // TypeScript enforces readonly at compile time\n      expect(def.encoding).toBe('application/json')\n    })\n\n    it('schema property is immutable in TypeScript', () => {\n      const schema = string()\n      const def = payload('application/json', schema)\n      // TypeScript enforces readonly at compile time\n      expect(def.schema).toBe(schema)\n    })\n  })\n\n  describe('usage scenarios', () => {\n    it('creates payload for JSON API response', () => {\n      const def = payload(\n        'application/json',\n        object({\n          success: string(),\n          data: lexMap(),\n        }),\n      )\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBeDefined()\n    })\n\n    it('creates payload for plain text response', () => {\n      const def = payload('text/plain', undefined)\n      expect(def.encoding).toBe('text/plain')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload for binary data', () => {\n      const def = payload('application/octet-stream', undefined)\n      expect(def.encoding).toBe('application/octet-stream')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload for image upload', () => {\n      const def = payload('image/jpeg', undefined)\n      expect(def.encoding).toBe('image/jpeg')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload for multipart form data', () => {\n      const def = payload('multipart/form-data', undefined)\n      expect(def.encoding).toBe('multipart/form-data')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates payload for URL encoded form', () => {\n      const def = payload('application/x-www-form-urlencoded', undefined)\n      expect(def.encoding).toBe('application/x-www-form-urlencoded')\n      expect(def.schema).toBeUndefined()\n    })\n\n    it('creates empty payload (no encoding, no schema)', () => {\n      const def = payload(undefined, undefined)\n      expect(def.encoding).toBeUndefined()\n      expect(def.schema).toBeUndefined()\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles encoding with charset parameter', () => {\n      const def = payload('application/json; charset=utf-8', undefined)\n      expect(def.encoding).toBe('application/json; charset=utf-8')\n    })\n\n    it('handles encoding with multiple parameters', () => {\n      const def = payload('multipart/form-data; boundary=something', undefined)\n      expect(def.encoding).toBe('multipart/form-data; boundary=something')\n    })\n\n    it('handles empty string encoding', () => {\n      const def = payload('', undefined)\n      expect(def.encoding).toBe('')\n    })\n\n    it('creates multiple payloads with same schema reference', () => {\n      const sharedSchema = object({\n        id: integer(),\n      })\n      const def1 = payload('application/json', sharedSchema)\n      const def2 = payload('application/json', sharedSchema)\n\n      expect(def1.schema).toBe(def2.schema)\n      expect(def1.schema).toBe(sharedSchema)\n    })\n\n    it('creates multiple payloads with different schemas', () => {\n      const schema1 = string()\n      const schema2 = integer()\n      const def1 = payload('application/json', schema1)\n      const def2 = payload('application/json', schema2)\n\n      expect(def1.schema).not.toBe(def2.schema)\n      expect(def1.schema).toBe(schema1)\n      expect(def2.schema).toBe(schema2)\n    })\n  })\n\n  describe('type inference scenarios', () => {\n    it('works with application/json and object schema', () => {\n      const schema = object({\n        message: string(),\n      })\n      const def = payload('application/json', schema)\n      expect(def.encoding).toBe('application/json')\n      expect(def.schema).toBe(schema)\n    })\n\n    it('works with text/* encodings expecting string bodies', () => {\n      const def1 = payload('text/plain', undefined)\n      const def2 = payload('text/html', undefined)\n      const def3 = payload('text/csv', undefined)\n\n      expect(def1.encoding).toBe('text/plain')\n      expect(def2.encoding).toBe('text/html')\n      expect(def3.encoding).toBe('text/csv')\n    })\n\n    it('works with binary encodings expecting Uint8Array bodies', () => {\n      const def1 = payload('image/png', undefined)\n      const def2 = payload('application/octet-stream', undefined)\n      const def3 = payload('video/mp4', undefined)\n\n      expect(def1.encoding).toBe('image/png')\n      expect(def2.encoding).toBe('application/octet-stream')\n      expect(def3.encoding).toBe('video/mp4')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/payload.ts",
    "content": "import { LexValue } from '@atproto/lex-data'\nimport { InferInput, Schema, Validator } from '../core.js'\nimport { ObjectSchema, object } from './object.js'\n\nexport type { LexValue }\n\ntype ToBodyMime<TEncoding extends string> = TEncoding extends '*/*'\n  ? `${string}/${string}`\n  : TEncoding extends `${infer T extends string}/*`\n    ? `${T}/${string}`\n    : TEncoding\n\ntype ToBodyType<\n  TEncoding extends string,\n  TSchema,\n  TBinary,\n> = TSchema extends Schema\n  ? InferInput<TSchema>\n  : TEncoding extends `application/json`\n    ? LexValue\n    : TBinary\n\n/**\n * Infers the type of a Payload's encoding and body.\n *\n * @template TPayload - The Payload type\n * @template TBody - Fallback body type for non-JSON encodings\n */\nexport type InferPayload<TPayload extends Payload, TBinary> =\n  TPayload extends Payload<infer TEncoding, infer TSchema>\n    ? TEncoding extends string\n      ? {\n          encoding: ToBodyMime<TEncoding>\n          body: ToBodyType<TEncoding, TSchema, TBinary>\n        }\n      : undefined\n    : never\n\n/**\n * Converts schema encoding patterns to data encoding types.\n *\n * Handles wildcards like '*\\/*' and 'image/*' in MIME types.\n *\n * @template TPayload - The Payload type\n */\nexport type InferPayloadEncoding<TPayload extends Payload> =\n  TPayload extends Payload<infer TEncoding, any>\n    ? TEncoding extends string\n      ? ToBodyMime<TEncoding>\n      : undefined\n    : never\n\n/**\n * Infers the body type from a Payload and fallback type.\n *\n * @template TPayload - The Payload type\n * @template TBody - Fallback body type for non-JSON encodings without schema\n */\nexport type InferPayloadBody<TPayload extends Payload, TBinary> =\n  TPayload extends Payload<infer TEncoding, infer TSchema>\n    ? TEncoding extends string\n      ? ToBodyType<TEncoding, TSchema, TBinary>\n      : undefined\n    : never\n\n/**\n * Determines valid schema type based on encoding presence.\n *\n * @template E - The encoding string type, or undefined\n */\nexport type PayloadSchema<E extends string | undefined> = E extends undefined\n  ? undefined\n  : Schema<LexValue> | undefined\n\n/**\n * Represents a payload definition for Lexicon endpoints.\n *\n * Payloads define the body format for HTTP requests and responses.\n * They consist of an encoding (MIME type) and an optional schema\n * for validating the body content.\n *\n * @template TEncoding - The MIME type string, or undefined for no body\n * @template TPayload - The schema type for body validation\n *\n * @example\n * ```ts\n * const jsonPayload = new Payload('application/json', l.object({ data: l.string() }))\n * const binaryPayload = new Payload('image/*', undefined)\n * const noPayload = new Payload(undefined, undefined)\n * ```\n */\nexport class Payload<\n  const TEncoding extends string | undefined = string | undefined,\n  const TSchema extends PayloadSchema<TEncoding> = PayloadSchema<TEncoding>,\n> {\n  constructor(\n    readonly encoding: TEncoding,\n    readonly schema: TSchema,\n  ) {\n    if (encoding === undefined && schema !== undefined) {\n      throw new TypeError('schema cannot be defined when encoding is undefined')\n    }\n  }\n\n  /**\n   * Checks whether the given content-type matches the expected payload schema's\n   * encoding.\n   */\n  matchesEncoding(contentType: string | undefined): boolean {\n    const { encoding } = this\n\n    // Handle undefined cases\n    if (encoding === undefined) {\n      // Expecting no body\n      return contentType == null\n    } else if (contentType == null) {\n      // Expecting a body, but got no content-type\n      return false\n    }\n\n    if (encoding === '*/*') {\n      return true\n    }\n\n    const mime = contentType?.split(';', 1)[0].trim()\n    if (encoding.endsWith('/*')) {\n      return mime.startsWith(encoding.slice(0, -1))\n    }\n\n    // Invalid: Lexicon can only specify \"*/*\" or \"type/*\" wildcards\n    if (encoding.includes('*')) {\n      return false\n    }\n\n    return encoding === mime\n  }\n}\n\n/**\n * Creates a payload definition for Lexicon endpoint bodies.\n *\n * Defines the expected MIME type and optional validation schema for\n * request or response bodies.\n *\n * @param encoding - MIME type string (e.g., 'application/json', 'image/*'), or undefined for no body\n * @param validator - Optional schema for validating the body content. Must be undefined if encoding is undefined.\n * @returns A new {@link Payload} instance\n *\n * @example\n * ```ts\n * // JSON payload with schema\n * const output = l.payload('application/json', l.object({\n *   posts: l.array(postSchema),\n *   cursor: l.optional(l.string()),\n * }))\n *\n * // Binary payload (no schema validation)\n * const blobInput = l.payload('*\\/*', undefined)\n *\n * // Image payload with wildcard\n * const imageInput = l.payload('image/*', undefined)\n *\n * // No payload (for endpoints without body)\n * const noBody = l.payload()\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function payload<\n  const E extends string | undefined = undefined,\n  const S extends PayloadSchema<E> = undefined,\n>(encoding: E = undefined as E, validator: S = undefined as S) {\n  return new Payload<E, S>(encoding, validator)\n}\n\n/**\n * Creates a JSON payload with an object schema.\n *\n * Convenience function for the common case of JSON request/response bodies.\n * Equivalent to `l.payload('application/json', l.object(properties))`.\n *\n * @param properties - Object mapping property names to validators\n * @returns A new {@link Payload} instance with 'application/json' encoding\n *\n * @example\n * ```ts\n * // Query output\n * const profileOutput = l.jsonPayload({\n *   did: l.string({ format: 'did' }),\n *   handle: l.string({ format: 'handle' }),\n *   displayName: l.optional(l.string()),\n * })\n *\n * // Procedure input\n * const createPostInput = l.jsonPayload({\n *   text: l.string({ maxGraphemes: 300 }),\n *   createdAt: l.string({ format: 'datetime' }),\n * })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function jsonPayload<\n  P extends Record<string, Validator<undefined | LexValue>>,\n>(properties: P): Payload<'application/json', ObjectSchema<P>> {\n  return payload('application/json', object(properties))\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/permission-set.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { PermissionSet, permissionSet } from './permission-set.js'\nimport { Permission, permission } from './permission.js'\n\ndescribe('PermissionSet', () => {\n  describe('constructor', () => {\n    it('creates a PermissionSet instance with all parameters', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.post:write', {}),\n      ] as const\n      const options = {\n        title: 'Post Management',\n        detail: 'Allows reading and writing posts',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms).toBeInstanceOf(PermissionSet)\n      expect(perms.nsid).toBe(nsid)\n      expect(perms.permissions).toBe(permissions)\n      expect(perms.options).toBe(options)\n    })\n\n    it('creates a PermissionSet instance with minimal options', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms).toBeInstanceOf(PermissionSet)\n      expect(perms.nsid).toBe(nsid)\n      expect(perms.permissions).toBe(permissions)\n      expect(perms.options).toEqual({})\n    })\n\n    it('creates a PermissionSet instance with empty permissions array', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [] as const\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms).toBeInstanceOf(PermissionSet)\n      expect(perms.permissions).toEqual([])\n    })\n\n    it('creates a PermissionSet instance with title only', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.like:read', {})] as const\n      const options = {\n        title: 'Like Management',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe('Like Management')\n      expect(perms.options.detail).toBeUndefined()\n    })\n\n    it('creates a PermissionSet instance with detail only', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.like:read', {})] as const\n      const options = {\n        detail: 'Allows reading likes on posts',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBeUndefined()\n      expect(perms.options.detail).toBe('Allows reading likes on posts')\n    })\n\n    it('creates a PermissionSet instance with localized titles', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: 'Post Management',\n        'title:lang': {\n          es: 'Gestión de Publicaciones',\n          fr: 'Gestion des Publications',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe('Post Management')\n      expect(perms.options['title:lang']).toEqual({\n        es: 'Gestión de Publicaciones',\n        fr: 'Gestion des Publications',\n      })\n    })\n\n    it('creates a PermissionSet instance with localized details', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        detail: 'Allows reading posts',\n        'detail:lang': {\n          es: 'Permite leer publicaciones',\n          fr: 'Permet de lire les publications',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.detail).toBe('Allows reading posts')\n      expect(perms.options['detail:lang']).toEqual({\n        es: 'Permite leer publicaciones',\n        fr: 'Permet de lire les publications',\n      })\n    })\n\n    it('creates a PermissionSet instance with all options including localization', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.post:write', {}),\n      ] as const\n      const options = {\n        title: 'Post Management',\n        'title:lang': {\n          es: 'Gestión de Publicaciones',\n          fr: 'Gestion des Publications',\n          de: 'Beitragsverwaltung',\n        },\n        detail: 'Allows reading and writing posts',\n        'detail:lang': {\n          es: 'Permite leer y escribir publicaciones',\n          fr: 'Permet de lire et écrire les publications',\n          de: 'Ermöglicht das Lesen und Schreiben von Beiträgen',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe('Post Management')\n      expect(perms.options['title:lang']).toEqual({\n        es: 'Gestión de Publicaciones',\n        fr: 'Gestion des Publications',\n        de: 'Beitragsverwaltung',\n      })\n      expect(perms.options.detail).toBe('Allows reading and writing posts')\n      expect(perms.options['detail:lang']).toEqual({\n        es: 'Permite leer y escribir publicaciones',\n        fr: 'Permet de lire et écrire les publications',\n        de: 'Ermöglicht das Lesen und Schreiben von Beiträgen',\n      })\n    })\n  })\n\n  describe('property immutability', () => {\n    it('options object itself is mutable', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = { title: 'Test' }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      // The reference is readonly, but the object itself can be mutated\n      options.title = 'Updated Title'\n      expect(perms.options.title).toBe('Updated Title')\n    })\n  })\n\n  describe('with multiple permissions', () => {\n    it('creates a PermissionSet with multiple read permissions', () => {\n      const nsid = 'app.bsky.oauth.read'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.like:read', {}),\n        permission('app.bsky.feed.repost:read', {}),\n        permission('app.bsky.graph.follow:read', {}),\n      ] as const\n      const options = {\n        title: 'Read Access',\n        detail: 'Allows reading various resources',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions).toHaveLength(4)\n      expect(perms.permissions[0].resource).toBe('app.bsky.feed.post:read')\n      expect(perms.permissions[1].resource).toBe('app.bsky.feed.like:read')\n      expect(perms.permissions[2].resource).toBe('app.bsky.feed.repost:read')\n      expect(perms.permissions[3].resource).toBe('app.bsky.graph.follow:read')\n    })\n\n    it('creates a PermissionSet with mixed read/write permissions', () => {\n      const nsid = 'app.bsky.oauth.full'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.post:write', {}),\n        permission('app.bsky.feed.like:read', {}),\n        permission('app.bsky.feed.like:write', {}),\n      ] as const\n      const options = {\n        title: 'Full Access',\n        detail: 'Allows reading and writing posts and likes',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions).toHaveLength(4)\n    })\n\n    it('creates a PermissionSet with a single permission', () => {\n      const nsid = 'app.bsky.oauth.limited'\n      const permissions = [\n        permission('app.bsky.actor.profile:read', {}),\n      ] as const\n      const options = {\n        title: 'Profile Read',\n        detail: 'Allows reading user profiles only',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions).toHaveLength(1)\n      expect(perms.permissions[0].resource).toBe('app.bsky.actor.profile:read')\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles very long NSID', () => {\n      const nsid =\n        'com.example.very.long.namespace.identifier.oauth.permissions'\n      const permissions = [permission('resource:action', {})] as const\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.nsid).toBe(nsid)\n    })\n\n    it('handles long title strings', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const longTitle = 'A'.repeat(500)\n      const options = {\n        title: longTitle,\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe(longTitle)\n      expect(perms.options.title?.length).toBe(500)\n    })\n\n    it('handles long detail strings', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const longDetail = 'B'.repeat(1000)\n      const options = {\n        detail: longDetail,\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.detail).toBe(longDetail)\n      expect(perms.options.detail?.length).toBe(1000)\n    })\n\n    it('handles multiple language codes in title:lang', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: 'Post Management',\n        'title:lang': {\n          es: 'Gestión de Publicaciones',\n          fr: 'Gestion des Publications',\n          de: 'Beitragsverwaltung',\n          it: 'Gestione dei Post',\n          pt: 'Gerenciamento de Postagens',\n          ja: '投稿管理',\n          ko: '게시물 관리',\n          'zh-CN': '帖子管理',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(Object.keys(perms.options['title:lang'] || {})).toHaveLength(8)\n    })\n\n    it('handles undefined values in title:lang', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: 'Post Management',\n        'title:lang': {\n          es: 'Gestión de Publicaciones',\n          fr: undefined,\n          de: 'Beitragsverwaltung',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options['title:lang']?.es).toBe('Gestión de Publicaciones')\n      expect(perms.options['title:lang']?.fr).toBeUndefined()\n      expect(perms.options['title:lang']?.de).toBe('Beitragsverwaltung')\n    })\n\n    it('handles undefined values in detail:lang', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        detail: 'Allows reading posts',\n        'detail:lang': {\n          es: undefined,\n          fr: 'Permet de lire les publications',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options['detail:lang']?.es).toBeUndefined()\n      expect(perms.options['detail:lang']?.fr).toBe(\n        'Permet de lire les publications',\n      )\n    })\n\n    it('handles special characters in title', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: 'Post Management: Read & Write (Full Access)',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe(\n        'Post Management: Read & Write (Full Access)',\n      )\n    })\n\n    it('handles special characters in detail', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        detail:\n          'Allows reading posts, likes & reposts (includes all sub-resources)',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.detail).toBe(\n        'Allows reading posts, likes & reposts (includes all sub-resources)',\n      )\n    })\n\n    it('handles unicode characters in title', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: '投稿管理 📝',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe('投稿管理 📝')\n    })\n\n    it('handles empty strings in title', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: '',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe('')\n    })\n\n    it('handles empty strings in detail', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        detail: '',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.detail).toBe('')\n    })\n\n    it('handles large number of permissions', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = Array.from({ length: 100 }, (_, i) =>\n        permission(`resource${i}:action`, {}),\n      ) as any\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions).toHaveLength(100)\n    })\n  })\n\n  describe('real-world permission set examples', () => {\n    it('creates a feed management permission set', () => {\n      const nsid = 'app.bsky.oauth.feed'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.post:write', {}),\n        permission('app.bsky.feed.like:read', {}),\n        permission('app.bsky.feed.like:write', {}),\n        permission('app.bsky.feed.repost:read', {}),\n        permission('app.bsky.feed.repost:write', {}),\n      ] as const\n      const options = {\n        title: 'Feed Management',\n        'title:lang': {\n          es: 'Gestión de Feed',\n          fr: 'Gestion du Feed',\n        },\n        detail: 'Full access to manage posts, likes, and reposts in your feed',\n        'detail:lang': {\n          es: 'Acceso completo para gestionar publicaciones, me gusta y reposts en tu feed',\n          fr: 'Accès complet pour gérer les publications, les likes et les reposts dans votre fil',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.nsid).toBe('app.bsky.oauth.feed')\n      expect(perms.permissions).toHaveLength(6)\n      expect(perms.options.title).toBe('Feed Management')\n    })\n\n    it('creates a read-only permission set', () => {\n      const nsid = 'app.bsky.oauth.readonly'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.like:read', {}),\n        permission('app.bsky.actor.profile:read', {}),\n        permission('app.bsky.graph.follow:read', {}),\n      ] as const\n      const options = {\n        title: 'Read-Only Access',\n        detail: 'View posts, likes, profiles, and follows without modification',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.nsid).toBe('app.bsky.oauth.readonly')\n      expect(perms.permissions.every((p) => p.resource.endsWith(':read'))).toBe(\n        true,\n      )\n    })\n\n    it('creates a profile management permission set', () => {\n      const nsid = 'app.bsky.oauth.profile'\n      const permissions = [\n        permission('app.bsky.actor.profile:read', {}),\n        permission('app.bsky.actor.profile:write', {}),\n      ] as const\n      const options = {\n        title: 'Profile Management',\n        'title:lang': {\n          es: 'Gestión de Perfil',\n          fr: 'Gestion du Profil',\n          de: 'Profilverwaltung',\n        },\n        detail: 'Read and update your profile information',\n        'detail:lang': {\n          es: 'Leer y actualizar la información de tu perfil',\n          fr: 'Lire et mettre à jour les informations de votre profil',\n          de: 'Profilinformationen lesen und aktualisieren',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.nsid).toBe('app.bsky.oauth.profile')\n      expect(perms.permissions).toHaveLength(2)\n    })\n\n    it('creates a minimal permission set', () => {\n      const nsid = 'app.bsky.oauth.minimal'\n      const permissions = [\n        permission('app.bsky.actor.profile:read', {}),\n      ] as const\n      const options = {\n        title: 'Basic Access',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.nsid).toBe('app.bsky.oauth.minimal')\n      expect(perms.permissions).toHaveLength(1)\n      expect(perms.options.detail).toBeUndefined()\n    })\n  })\n\n  describe('permission validation', () => {\n    it('validates that permissions are Permission instances', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permission1 = permission('app.bsky.feed.post:read', {})\n      const permission2 = permission('app.bsky.feed.post:write', {})\n      const permissions = [permission1, permission2] as const\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions[0]).toBeInstanceOf(Permission)\n      expect(perms.permissions[1]).toBeInstanceOf(Permission)\n    })\n\n    it('preserves permission resource strings', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [\n        permission('app.bsky.feed.post:read', {}),\n        permission('app.bsky.feed.like:write', {}),\n        permission('app.bsky.graph.follow:read', {}),\n      ] as const\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions[0].resource).toBe('app.bsky.feed.post:read')\n      expect(perms.permissions[1].resource).toBe('app.bsky.feed.like:write')\n      expect(perms.permissions[2].resource).toBe('app.bsky.graph.follow:read')\n    })\n\n    it('preserves permission options', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissionOptions = { custom: 'value' }\n      const permissions = [\n        permission('app.bsky.feed.post:read', permissionOptions),\n      ] as const\n      const options = {}\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.permissions[0].options).toBe(permissionOptions)\n    })\n  })\n\n  describe('option variations', () => {\n    it('accepts title without detail', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        title: 'Post Reading',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBe('Post Reading')\n      expect(perms.options.detail).toBeUndefined()\n      expect(perms.options['title:lang']).toBeUndefined()\n      expect(perms.options['detail:lang']).toBeUndefined()\n    })\n\n    it('accepts detail without title', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        detail: 'Allows reading posts from the feed',\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBeUndefined()\n      expect(perms.options.detail).toBe('Allows reading posts from the feed')\n      expect(perms.options['title:lang']).toBeUndefined()\n      expect(perms.options['detail:lang']).toBeUndefined()\n    })\n\n    it('accepts title:lang without title', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        'title:lang': {\n          es: 'Gestión de Publicaciones',\n          fr: 'Gestion des Publications',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.title).toBeUndefined()\n      expect(perms.options['title:lang']).toEqual({\n        es: 'Gestión de Publicaciones',\n        fr: 'Gestion des Publications',\n      })\n    })\n\n    it('accepts detail:lang without detail', () => {\n      const nsid = 'app.bsky.oauth.permissions'\n      const permissions = [permission('app.bsky.feed.post:read', {})] as const\n      const options = {\n        'detail:lang': {\n          es: 'Permite leer publicaciones',\n          fr: 'Permet de lire les publications',\n        },\n      }\n\n      const perms = permissionSet(nsid, permissions, options)\n\n      expect(perms.options.detail).toBeUndefined()\n      expect(perms.options['detail:lang']).toEqual({\n        es: 'Permite leer publicaciones',\n        fr: 'Permet de lire les publications',\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/permission-set.ts",
    "content": "import { NsidString } from '../core.js'\nimport { Permission } from './permission.js'\n\n/**\n * Configuration options for a permission set.\n *\n * @property title - Human-readable title for the permission set\n * @property title:lang - Localized titles by language code\n * @property detail - Detailed description of the permission set\n * @property detail:lang - Localized descriptions by language code\n */\nexport type PermissionSetOptions = {\n  title?: string\n  'title:lang'?: Record<string, undefined | string>\n  detail?: string\n  'detail:lang'?: Record<string, undefined | string>\n}\n\n/**\n * Represents a collection of related permissions in AT Protocol.\n *\n * Permission sets group permissions together with metadata for OAuth\n * authorization flows. They are identified by an NSID.\n *\n * @template TNsid - The NSID identifying this permission set\n * @template TPermissions - Tuple type of the included permissions\n *\n * @example\n * ```ts\n * const feedAccess = new PermissionSet(\n *   'app.bsky.feed.access',\n *   [readPermission, writePermission],\n *   { title: 'Feed Access', detail: 'Read and write to your feed' }\n * )\n * ```\n */\nexport class PermissionSet<\n  const TNsid extends NsidString = any,\n  const TPermissions extends readonly Permission[] = any,\n> {\n  constructor(\n    readonly nsid: TNsid,\n    readonly permissions: TPermissions,\n    readonly options: PermissionSetOptions = {},\n  ) {}\n}\n\n/**\n * Creates a permission set grouping related permissions.\n *\n * Permission sets define OAuth scopes that applications can request.\n * They include human-readable metadata for authorization UIs.\n *\n * @param nsid - The NSID identifying this permission set\n * @param permissions - Array of permissions included in this set\n * @param options - Optional metadata (title, detail, localization)\n * @returns A new {@link PermissionSet} instance\n *\n * @example\n * ```ts\n * // Define individual permissions\n * const readPosts = l.permission('read', { collection: 'app.bsky.feed.post' })\n * const writePosts = l.permission('write', { collection: 'app.bsky.feed.post' })\n *\n * // Group into a permission set\n * const postManagement = l.permissionSet(\n *   'app.bsky.feed.postManagement',\n *   [readPosts, writePosts],\n *   {\n *     title: 'Post Management',\n *     detail: 'View and create posts on your behalf',\n *     'title:lang': {\n *       'es': 'Gestion de publicaciones',\n *       'fr': 'Gestion des publications',\n *     },\n *   }\n * )\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function permissionSet<\n  const N extends NsidString,\n  const P extends readonly Permission[],\n>(nsid: N, permissions: P, options?: PermissionSetOptions) {\n  return new PermissionSet<N, P>(nsid, permissions, options)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/permission.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { permission } from './permission.js'\n\ndescribe('Permission', () => {\n  describe('basic construction', () => {\n    it('creates a perm with resource and empty options', () => {\n      const perm = permission('read', {})\n      expect(perm.resource).toBe('read')\n      expect(perm.options).toEqual({})\n    })\n\n    it('creates a perm with resource and options', () => {\n      const options = { limit: 100 }\n      const perm = permission('read', options)\n      expect(perm.resource).toBe('read')\n      expect(perm.options).toEqual({ limit: 100 })\n    })\n\n    it('preserves the options object reference', () => {\n      const options = { limit: 100 }\n      const perm = permission('read', options)\n      expect(perm.options).toBe(options)\n    })\n\n    it('preserves resource as const literal type', () => {\n      const perm = permission('read' as const, {})\n      expect(perm.resource).toBe('read')\n    })\n  })\n\n  describe('resource strings', () => {\n    it('handles simple resource names', () => {\n      const perm = permission('read', {})\n      expect(perm.resource).toBe('read')\n    })\n\n    it('handles namespaced resource names', () => {\n      const perm = permission('com.example.read', {})\n      expect(perm.resource).toBe('com.example.read')\n    })\n\n    it('handles resource names with dashes', () => {\n      const perm = permission('read-posts', {})\n      expect(perm.resource).toBe('read-posts')\n    })\n\n    it('handles resource names with underscores', () => {\n      const perm = permission('read_posts', {})\n      expect(perm.resource).toBe('read_posts')\n    })\n\n    it('handles resource names with colons', () => {\n      const perm = permission('posts:read', {})\n      expect(perm.resource).toBe('posts:read')\n    })\n\n    it('handles resource names with slashes', () => {\n      const perm = permission('posts/read', {})\n      expect(perm.resource).toBe('posts/read')\n    })\n\n    it('handles resource names with wildcards', () => {\n      const perm = permission('posts:*', {})\n      expect(perm.resource).toBe('posts:*')\n    })\n\n    it('handles empty resource string', () => {\n      const perm = permission('', {})\n      expect(perm.resource).toBe('')\n    })\n\n    it('handles very long resource strings', () => {\n      const longResource = 'com.example.service.'.repeat(50) + 'read'\n      const perm = permission(longResource, {})\n      expect(perm.resource).toBe(longResource)\n    })\n\n    it('handles resource strings with unicode characters', () => {\n      const perm = permission('リソース', {})\n      expect(perm.resource).toBe('リソース')\n    })\n\n    it('handles resource strings with special characters', () => {\n      const perm = permission('resource@#$%', {})\n      expect(perm.resource).toBe('resource@#$%')\n    })\n  })\n\n  describe('options with string parameters', () => {\n    it('accepts empty options object', () => {\n      const perm = permission('read', {})\n      expect(perm.options).toEqual({})\n    })\n\n    it('accepts options with string value', () => {\n      const perm = permission('read', { format: 'json' })\n      expect(perm.options).toEqual({ format: 'json' })\n    })\n\n    it('accepts options with multiple string values', () => {\n      const perm = permission('read', {\n        format: 'json',\n        encoding: 'utf-8',\n      })\n      expect(perm.options).toEqual({ format: 'json', encoding: 'utf-8' })\n    })\n\n    it('accepts options with empty string value', () => {\n      const perm = permission('read', { filter: '' })\n      expect(perm.options).toEqual({ filter: '' })\n    })\n\n    it('accepts options with very long string values', () => {\n      const longString = 'value'.repeat(1000)\n      const perm = permission('read', { data: longString })\n      expect(perm.options.data).toBe(longString)\n    })\n  })\n\n  describe('options with integer parameters', () => {\n    it('accepts options with positive integer', () => {\n      const perm = permission('read', { limit: 100 })\n      expect(perm.options).toEqual({ limit: 100 })\n    })\n\n    it('accepts options with zero', () => {\n      const perm = permission('read', { offset: 0 })\n      expect(perm.options).toEqual({ offset: 0 })\n    })\n\n    it('accepts options with negative integer', () => {\n      const perm = permission('read', { delta: -50 })\n      expect(perm.options).toEqual({ delta: -50 })\n    })\n\n    it('accepts options with multiple integers', () => {\n      const perm = permission('read', {\n        limit: 100,\n        offset: 20,\n        maxRetries: 3,\n      })\n      expect(perm.options).toEqual({\n        limit: 100,\n        offset: 20,\n        maxRetries: 3,\n      })\n    })\n\n    it('accepts options with large integers', () => {\n      const perm = permission('read', { maxSize: 2147483647 })\n      expect(perm.options).toEqual({ maxSize: 2147483647 })\n    })\n  })\n\n  describe('options with boolean parameters', () => {\n    it('accepts options with true boolean', () => {\n      const perm = permission('read', { includeDeleted: true })\n      expect(perm.options).toEqual({ includeDeleted: true })\n    })\n\n    it('accepts options with false boolean', () => {\n      const perm = permission('read', { includeDeleted: false })\n      expect(perm.options).toEqual({ includeDeleted: false })\n    })\n\n    it('accepts options with multiple booleans', () => {\n      const perm = permission('read', {\n        includeDeleted: true,\n        includeDrafts: false,\n        includeArchived: true,\n      })\n      expect(perm.options).toEqual({\n        includeDeleted: true,\n        includeDrafts: false,\n        includeArchived: true,\n      })\n    })\n  })\n\n  describe('options with array parameters', () => {\n    it('accepts options with string array', () => {\n      const perm = permission('read', { fields: ['id', 'name'] })\n      expect(perm.options).toEqual({ fields: ['id', 'name'] })\n    })\n\n    it('accepts options with integer array', () => {\n      const perm = permission('read', { ids: [1, 2, 3] })\n      expect(perm.options).toEqual({ ids: [1, 2, 3] })\n    })\n\n    it('accepts options with boolean array', () => {\n      const perm = permission('read', { flags: [true, false, true] })\n      expect(perm.options).toEqual({ flags: [true, false, true] })\n    })\n\n    it('accepts options with empty array', () => {\n      const perm = permission('read', { fields: [] })\n      expect(perm.options).toEqual({ fields: [] })\n    })\n\n    it('preserves array reference in options', () => {\n      const fields = ['id', 'name']\n      const perm = permission('read', { fields })\n      expect(perm.options.fields).toBe(fields)\n    })\n  })\n\n  describe('options with mixed parameter types', () => {\n    it('accepts options with string, integer, and boolean', () => {\n      const perm = permission('read', {\n        format: 'json',\n        limit: 100,\n        includeDeleted: true,\n      })\n      expect(perm.options).toEqual({\n        format: 'json',\n        limit: 100,\n        includeDeleted: true,\n      })\n    })\n\n    it('accepts options with all parameter types', () => {\n      const perm = permission('read', {\n        format: 'json',\n        limit: 100,\n        includeDeleted: true,\n        fields: ['id', 'name'],\n      })\n      expect(perm.options).toEqual({\n        format: 'json',\n        limit: 100,\n        includeDeleted: true,\n        fields: ['id', 'name'],\n      })\n    })\n\n    it('accepts options with many parameters', () => {\n      const perm = permission('read', {\n        param1: 'value1',\n        param2: 'value2',\n        param3: 'value3',\n        param4: 123,\n        param5: 456,\n        param6: true,\n        param7: false,\n        param8: ['a', 'b'],\n      })\n      expect(perm.options).toEqual({\n        param1: 'value1',\n        param2: 'value2',\n        param3: 'value3',\n        param4: 123,\n        param5: 456,\n        param6: true,\n        param7: false,\n        param8: ['a', 'b'],\n      })\n    })\n  })\n\n  describe('options with undefined values', () => {\n    it('accepts options with undefined values', () => {\n      const perm = permission('read', {\n        optionalParam: undefined,\n      })\n      expect(perm.options).toEqual({ optionalParam: undefined })\n    })\n\n    it('accepts options with mix of defined and undefined values', () => {\n      const perm = permission('read', {\n        required: 'value',\n        optional: undefined,\n      })\n      expect(perm.options).toEqual({\n        required: 'value',\n        optional: undefined,\n      })\n    })\n\n    it('preserves undefined in options object', () => {\n      const options = { param: undefined }\n      const perm = permission('read', options)\n      expect('param' in perm.options).toBe(true)\n      expect(perm.options.param).toBeUndefined()\n    })\n  })\n\n  describe('multiple perm instances', () => {\n    it('creates independent instances', () => {\n      const perm1 = permission('read', { limit: 100 })\n      const perm2 = permission('write', { format: 'json' })\n\n      expect(perm1.resource).toBe('read')\n      expect(perm2.resource).toBe('write')\n      expect(perm1.options).toEqual({ limit: 100 })\n      expect(perm2.options).toEqual({ format: 'json' })\n    })\n\n    it('instances with same values are not equal', () => {\n      const perm1 = permission('read', { limit: 100 })\n      const perm2 = permission('read', { limit: 100 })\n\n      expect(perm1).not.toBe(perm2)\n      expect(perm1.resource).toBe(perm2.resource)\n      expect(perm1.options).not.toBe(perm2.options)\n      expect(perm1.options).toEqual(perm2.options)\n    })\n\n    it('instances sharing options object reference', () => {\n      const options = { limit: 100 }\n      const perm1 = permission('read', options)\n      const perm2 = permission('write', options)\n\n      expect(perm1.options).toBe(perm2.options)\n      expect(perm1.options).toBe(options)\n      expect(perm2.options).toBe(options)\n    })\n  })\n\n  describe('common perm patterns', () => {\n    it('creates read perm', () => {\n      const perm = permission('read', {})\n      expect(perm.resource).toBe('read')\n    })\n\n    it('creates write perm', () => {\n      const perm = permission('write', {})\n      expect(perm.resource).toBe('write')\n    })\n\n    it('creates delete perm', () => {\n      const perm = permission('delete', {})\n      expect(perm.resource).toBe('delete')\n    })\n\n    it('creates admin perm', () => {\n      const perm = permission('admin', {})\n      expect(perm.resource).toBe('admin')\n    })\n\n    it('creates scoped resource perm', () => {\n      const perm = permission('posts:read', { limit: 50 })\n      expect(perm.resource).toBe('posts:read')\n      expect(perm.options).toEqual({ limit: 50 })\n    })\n\n    it('creates namespaced perm', () => {\n      const perm = permission('com.example.posts.read', {\n        includeDeleted: false,\n      })\n      expect(perm.resource).toBe('com.example.posts.read')\n      expect(perm.options).toEqual({ includeDeleted: false })\n    })\n\n    it('creates CRUD perms', () => {\n      const create = permission('create', {})\n      const read = permission('read', {})\n      const update = permission('update', {})\n      const deleteP = permission('delete', {})\n\n      expect(create.resource).toBe('create')\n      expect(read.resource).toBe('read')\n      expect(update.resource).toBe('update')\n      expect(deleteP.resource).toBe('delete')\n    })\n\n    it('creates perm with scope and filters', () => {\n      const perm = permission('posts:read', {\n        scope: 'public',\n        limit: 100,\n        includeDeleted: false,\n      })\n      expect(perm.resource).toBe('posts:read')\n      expect(perm.options).toEqual({\n        scope: 'public',\n        limit: 100,\n        includeDeleted: false,\n      })\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles perm with all parameter types in options', () => {\n      const perm = permission('complex', {\n        stringParam: 'value',\n        intParam: 42,\n        boolParam: true,\n        arrayParam: [1, 2, 3],\n        undefinedParam: undefined,\n      })\n\n      expect(perm.options.stringParam).toBe('value')\n      expect(perm.options.intParam).toBe(42)\n      expect(perm.options.boolParam).toBe(true)\n      expect(perm.options.arrayParam).toEqual([1, 2, 3])\n      expect(perm.options.undefinedParam).toBeUndefined()\n    })\n\n    it('handles resource with whitespace', () => {\n      const perm = permission('read posts', {})\n      expect(perm.resource).toBe('read posts')\n    })\n\n    it('handles resource with leading/trailing whitespace', () => {\n      const perm = permission('  read  ', {})\n      expect(perm.resource).toBe('  read  ')\n    })\n\n    it('handles options with numeric string keys', () => {\n      const perm = permission('read', { '123': 'value' })\n      expect(perm.options['123']).toBe('value')\n    })\n\n    it('handles options with special character keys', () => {\n      const perm = permission('read', { 'key-name': 'value' })\n      expect(perm.options['key-name']).toBe('value')\n    })\n  })\n\n  describe('type safety', () => {\n    it('preserves resource type as literal', () => {\n      const perm = permission('read' as const, {})\n      // At compile time, TypeScript should infer the type as 'read'\n      expect(perm.resource).toBe('read')\n    })\n\n    it('preserves options type', () => {\n      const options = { limit: 100 } as const\n      const perm = permission('read', options)\n      // At compile time, TypeScript should infer the exact type\n      expect(perm.options.limit).toBe(100)\n    })\n\n    it('handles generic string resource type', () => {\n      const resource: string = 'dynamic'\n      const perm = permission(resource, {})\n      expect(perm.resource).toBe('dynamic')\n    })\n\n    it('handles union resource types', () => {\n      type ResourceType = 'read' | 'write' | 'delete'\n      const resource: ResourceType = 'read'\n      const perm = permission(resource, {})\n      expect(perm.resource).toBe('read')\n    })\n  })\n\n  describe('constructor behavior', () => {\n    it('requires both resource and options arguments', () => {\n      // TypeScript enforces this at compile time\n      const perm = permission('read', {})\n      expect(perm.resource).toBeDefined()\n      expect(perm.options).toBeDefined()\n    })\n\n    it('does not modify input options object', () => {\n      const options = { limit: 100 }\n      const originalOptions = { ...options }\n      permission('read', options)\n      expect(options).toEqual(originalOptions)\n    })\n\n    it('accepts options as object literal', () => {\n      const perm = permission('read', { limit: 100 })\n      expect(perm.options).toEqual({ limit: 100 })\n    })\n\n    it('accepts options as variable', () => {\n      const options = { limit: 100 }\n      const perm = permission('read', options)\n      expect(perm.options).toEqual({ limit: 100 })\n    })\n\n    it('accepts resource as string literal', () => {\n      const perm = permission('read', {})\n      expect(perm.resource).toBe('read')\n    })\n\n    it('accepts resource as variable', () => {\n      const resource = 'read'\n      const perm = permission(resource, {})\n      expect(perm.resource).toBe('read')\n    })\n  })\n\n  describe('object enumeration', () => {\n    it('enumerates all properties', () => {\n      const perm = permission('read', { limit: 100 })\n      const keys = Object.keys(perm)\n      expect(keys).toContain('resource')\n      expect(keys).toContain('options')\n    })\n\n    it('can be spread into object', () => {\n      const perm = permission('read', { limit: 100 })\n      const spread = { ...perm }\n      expect(spread.resource).toBe('read')\n      expect(spread.options).toEqual({ limit: 100 })\n    })\n  })\n\n  describe('JSON serialization', () => {\n    it('can be JSON stringified', () => {\n      const perm = permission('read', { limit: 100 })\n      const json = JSON.stringify(perm)\n      const parsed = JSON.parse(json)\n      expect(parsed.resource).toBe('read')\n      expect(parsed.options).toEqual({ limit: 100 })\n    })\n\n    it('handles complex options in JSON', () => {\n      const perm = permission('read', {\n        fields: ['id', 'name'],\n        limit: 100,\n        includeDeleted: false,\n      })\n      const json = JSON.stringify(perm)\n      const parsed = JSON.parse(json)\n      expect(parsed.options).toEqual({\n        fields: ['id', 'name'],\n        limit: 100,\n        includeDeleted: false,\n      })\n    })\n\n    it('preserves undefined in JSON serialization', () => {\n      const perm = permission('read', {\n        defined: 'value',\n        undefined: undefined,\n      })\n      // JSON.stringify removes undefined values by default\n      const json = JSON.stringify(perm)\n      const parsed = JSON.parse(json)\n      expect('undefined' in parsed.options).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/permission.ts",
    "content": "import { Params } from './params.js'\n\n/**\n * Type alias for permission options (same as Params).\n */\nexport type PermissionOptions = Params\n\n/**\n * Represents a single permission in an AT Protocol permission set.\n *\n * Permissions define access rights to specific resources with optional\n * parameters for fine-grained control.\n *\n * @template TResource - The resource identifier string type\n * @template TOptions - The options type (must be valid Params)\n *\n * @example\n * ```ts\n * const readPermission = new Permission('read', { collection: 'app.bsky.feed.post' })\n * ```\n */\nexport class Permission<\n  const TResource extends string = any,\n  const TOptions extends PermissionOptions = any,\n> {\n  constructor(\n    readonly resource: TResource,\n    readonly options: TOptions,\n  ) {}\n}\n\n/**\n * Creates a permission definition for AT Protocol authorization.\n *\n * Permissions specify what resources an application can access.\n * Used in permission sets to define OAuth scopes.\n *\n * @param resource - The resource identifier (e.g., 'read', 'write', 'admin')\n * @param options - Optional parameters for the permission\n * @returns A new {@link Permission} instance\n *\n * @example\n * ```ts\n * // Simple permission\n * const readPermission = l.permission('read')\n *\n * // Permission with options\n * const writePostsPermission = l.permission('write', {\n *   collection: 'app.bsky.feed.post',\n * })\n *\n * // Multiple permissions with different scopes\n * const readProfile = l.permission('read', { collection: 'app.bsky.actor.profile' })\n * const readFeed = l.permission('read', { collection: 'app.bsky.feed.*' })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function permission<\n  const R extends string,\n  const O extends PermissionOptions,\n>(resource: R, options: PermissionOptions & O = {} as O) {\n  return new Permission<R, O>(resource, options)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/procedure.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { object } from './object.js'\nimport { params } from './params.js'\nimport { payload } from './payload.js'\nimport { procedure } from './procedure.js'\nimport { string } from './string.js'\n\ndescribe('Procedure', () => {\n  describe('basic construction', () => {\n    it('creates a procedure with all parameters', () => {\n      const errors = ['InvalidRequest', 'Unauthorized'] as const\n\n      const createPost = procedure(\n        'com.example.createPost',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        errors,\n      )\n\n      expect(createPost.nsid).toBe('com.example.createPost')\n      expect(createPost.parameters).toEqual(params())\n      expect(createPost.input).toEqual(payload('application/json', undefined))\n      expect(createPost.output).toEqual(payload('application/json', undefined))\n      expect(createPost.errors).toBe(errors)\n    })\n\n    it('creates a procedure without errors', () => {\n      const doSomething = procedure(\n        'com.example.doSomething',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(doSomething.nsid).toBe('com.example.doSomething')\n      expect(doSomething.parameters).toEqual(params())\n      expect(doSomething.input).toEqual(payload('application/json', undefined))\n      expect(doSomething.output).toEqual(payload('application/json', undefined))\n      expect(doSomething.errors).toBeUndefined()\n    })\n\n    it('creates a procedure with empty errors array', () => {\n      const performAction = procedure(\n        'com.example.performAction',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        [] as const,\n      )\n\n      expect(performAction.errors).toEqual([])\n    })\n  })\n\n  describe('with parameters schema', () => {\n    it('creates a procedure with query parameters', () => {\n      const listPosts = procedure(\n        'com.example.listPosts',\n        params({\n          limit: string(),\n          cursor: string(),\n        }),\n        payload(undefined, undefined),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(listPosts.parameters.shape).toHaveProperty('limit')\n      expect(listPosts.parameters.shape).toHaveProperty('cursor')\n    })\n\n    it('creates a procedure with empty parameters', () => {\n      const myProcedure = procedure(\n        'com.example.action',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(Object.keys(myProcedure.parameters.shape)).toHaveLength(0)\n    })\n  })\n\n  describe('with input payload', () => {\n    it('creates a procedure with JSON input', () => {\n      const inputSchema = object({\n        text: string(),\n      })\n\n      const myProcedure = procedure(\n        'com.example.createPost',\n        params(),\n        payload('application/json', inputSchema),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(myProcedure.input.encoding).toBe('application/json')\n      expect(myProcedure.input.schema).toBe(inputSchema)\n    })\n\n    it('creates a procedure with text input', () => {\n      const myProcedure = procedure(\n        'com.example.uploadText',\n        params(),\n        payload('text/plain', undefined),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(myProcedure.input.encoding).toBe('text/plain')\n      expect(myProcedure.input.schema).toBeUndefined()\n    })\n\n    it('creates a procedure with binary input', () => {\n      const myProcedure = procedure(\n        'com.example.uploadBlob',\n        params(),\n        payload('application/octet-stream', undefined),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(myProcedure.input.encoding).toBe('application/octet-stream')\n    })\n\n    it('creates a procedure with no input', () => {\n      const myProcedure = procedure(\n        'com.example.action',\n        params(),\n        payload(undefined, undefined),\n        payload('application/json', undefined),\n        undefined,\n      )\n\n      expect(myProcedure.input.encoding).toBeUndefined()\n      expect(myProcedure.input.schema).toBeUndefined()\n    })\n  })\n\n  describe('with output payload', () => {\n    it('creates a procedure with JSON output', () => {\n      const outputSchema = object({\n        uri: string(),\n        cid: string(),\n      })\n\n      const myProcedure = procedure(\n        'com.example.getPost',\n        params(),\n        payload(undefined, undefined),\n        payload('application/json', outputSchema),\n        undefined,\n      )\n\n      expect(myProcedure.output.encoding).toBe('application/json')\n      expect(myProcedure.output.schema).toBe(outputSchema)\n    })\n\n    it('creates a procedure with text output', () => {\n      const myProcedure = procedure(\n        'com.example.export',\n        params(),\n        payload(undefined, undefined),\n        payload('text/plain', undefined),\n        undefined,\n      )\n\n      expect(myProcedure.output.encoding).toBe('text/plain')\n    })\n\n    it('creates a procedure with binary output', () => {\n      const myProcedure = procedure(\n        'com.example.download',\n        params(),\n        payload(undefined, undefined),\n        payload('application/octet-stream', undefined),\n        undefined,\n      )\n\n      expect(myProcedure.output.encoding).toBe('application/octet-stream')\n    })\n\n    it('creates a procedure with no output', () => {\n      const myProcedure = procedure(\n        'com.example.deletePost',\n        params(),\n        payload('application/json', undefined),\n        payload(undefined, undefined),\n        undefined,\n      )\n\n      expect(myProcedure.output.encoding).toBeUndefined()\n      expect(myProcedure.output.schema).toBeUndefined()\n    })\n  })\n\n  describe('with error definitions', () => {\n    it('creates a procedure with single error', () => {\n      const myProcedure = procedure(\n        'com.example.action',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        ['InvalidRequest'] as const,\n      )\n\n      expect(myProcedure.errors).toEqual(['InvalidRequest'])\n    })\n\n    it('creates a procedure with multiple errors', () => {\n      const myProcedure = procedure(\n        'com.example.createPost',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        ['InvalidRequest', 'Unauthorized', 'RateLimitExceeded'] as const,\n      )\n\n      expect(myProcedure.errors).toHaveLength(3)\n      expect(myProcedure.errors).toContain('InvalidRequest')\n      expect(myProcedure.errors).toContain('Unauthorized')\n      expect(myProcedure.errors).toContain('RateLimitExceeded')\n    })\n  })\n\n  describe('property access', () => {\n    it('provides access to all properties', () => {\n      const errors = ['Error1', 'Error2'] as const\n\n      const myProcedure = procedure(\n        'com.example.test',\n        params(),\n        payload('application/json', undefined),\n        payload('application/json', undefined),\n        errors,\n      )\n\n      expect(myProcedure.nsid).toBe('com.example.test')\n      expect(myProcedure.parameters).toEqual(params())\n      expect(myProcedure.input).toEqual(payload('application/json', undefined))\n      expect(myProcedure.output).toEqual(payload('application/json', undefined))\n      expect(myProcedure.errors).toBe(errors)\n    })\n\n    it('maintains reference equality for complex properties', () => {\n      const parameters = params({ test: string() })\n      const inputSchema = object({ field: string() })\n      const outputSchema = object({ result: string() })\n      const input = payload('application/json', inputSchema)\n      const output = payload('application/json', outputSchema)\n\n      const myProcedure = procedure(\n        'com.example.test',\n        parameters,\n        input,\n        output,\n        undefined,\n      )\n\n      // Verify references are maintained\n      expect(myProcedure.parameters).toBe(parameters)\n      expect(myProcedure.input).toBe(input)\n      expect(myProcedure.output).toBe(output)\n      expect(myProcedure.input.schema).toBe(inputSchema)\n      expect(myProcedure.output.schema).toBe(outputSchema)\n    })\n  })\n\n  describe('complex scenarios', () => {\n    it('creates a fully-featured procedure', () => {\n      const inputSchema = object({\n        text: string(),\n        mentions: string(),\n      })\n      const outputSchema = object({\n        messageId: string(),\n        timestamp: string(),\n      })\n      const errors = [\n        'ConversationNotFound',\n        'MessageTooLong',\n        'RateLimitExceeded',\n      ] as const\n\n      const myProcedure = procedure(\n        'com.example.chat.sendMessage',\n        params({\n          conversationId: string(),\n        }),\n        payload('application/json', inputSchema),\n        payload('application/json', outputSchema),\n        errors,\n      )\n\n      expect(myProcedure.nsid).toBe('com.example.chat.sendMessage')\n      expect(myProcedure.parameters.shape).toHaveProperty('conversationId')\n      expect(myProcedure.input.encoding).toBe('application/json')\n      expect(myProcedure.input.schema).toBe(inputSchema)\n      expect(myProcedure.output.encoding).toBe('application/json')\n      expect(myProcedure.output.schema).toBe(outputSchema)\n      expect(myProcedure.errors).toEqual(errors)\n    })\n\n    it('creates a minimal procedure', () => {\n      const myProcedure = procedure(\n        'com.example.ping',\n        params(),\n        payload(undefined, undefined),\n        payload(undefined, undefined),\n        undefined,\n      )\n\n      expect(myProcedure.nsid).toBe('com.example.ping')\n      expect(Object.keys(myProcedure.parameters.shape)).toHaveLength(0)\n      expect(myProcedure.input.encoding).toBeUndefined()\n      expect(myProcedure.output.encoding).toBeUndefined()\n      expect(myProcedure.errors).toBeUndefined()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/procedure.ts",
    "content": "import { NsidString } from '../core.js'\nimport { ParamsSchema } from './params.js'\nimport { Payload } from './payload.js'\n\n/**\n * Represents a Lexicon procedure (HTTP POST) endpoint definition.\n *\n * Procedures are operations that may modify state on the server.\n * They have parameters, an input payload (request body), an output\n * payload (response body), and optional error types.\n *\n * @template TNsid - The NSID identifying this procedure\n * @template TParameters - The parameters schema type\n * @template TInputPayload - The request body payload type\n * @template TOutputPayload - The response body payload type\n * @template TErrors - Array of error type strings, or undefined\n *\n * @example\n * ```ts\n * const createPost = new Procedure(\n *   'app.bsky.feed.post',\n *   l.params({}),\n *   l.jsonPayload({ text: l.string() }),\n *   l.jsonPayload({ uri: l.string(), cid: l.string() }),\n *   ['InvalidRecord']\n * )\n * ```\n */\nexport class Procedure<\n  const TNsid extends NsidString = NsidString,\n  const TParameters extends ParamsSchema = ParamsSchema,\n  const TInputPayload extends Payload = Payload,\n  const TOutputPayload extends Payload = Payload,\n  const TErrors extends undefined | readonly string[] =\n    | undefined\n    | readonly string[],\n> {\n  readonly type = 'procedure' as const\n\n  constructor(\n    readonly nsid: TNsid,\n    readonly parameters: TParameters,\n    readonly input: TInputPayload,\n    readonly output: TOutputPayload,\n    readonly errors: TErrors,\n  ) {}\n}\n\n/**\n * Creates a procedure definition for a Lexicon POST endpoint.\n *\n * Procedures can modify server state. They accept both URL parameters\n * and a request body (input payload).\n *\n * @param nsid - The NSID identifying this procedure endpoint\n * @param parameters - Schema for URL query parameters\n * @param input - Schema for request body payload\n * @param output - Schema for response body payload\n * @param errors - Optional array of error type strings\n * @returns A new {@link Procedure} instance\n *\n * @example\n * ```ts\n * // Create record procedure\n * const createRecord = l.procedure(\n *   'com.atproto.repo.createRecord',\n *   l.params({}),\n *   l.jsonPayload({\n *     repo: l.string({ format: 'at-identifier' }),\n *     collection: l.string({ format: 'nsid' }),\n *     record: l.unknown(),\n *   }),\n *   l.jsonPayload({\n *     uri: l.string({ format: 'at-uri' }),\n *     cid: l.string({ format: 'cid' }),\n *   }),\n *   ['InvalidRecord', 'RepoNotFound'],\n * )\n *\n * // Procedure with binary input\n * const uploadBlob = l.procedure(\n *   'com.atproto.repo.uploadBlob',\n *   l.params({}),\n *   l.payload('*\\/*', undefined), // Accept any content type\n *   l.jsonPayload({ blob: l.blob() }),\n * )\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function procedure<\n  const N extends NsidString,\n  const P extends ParamsSchema,\n  const I extends Payload,\n  const O extends Payload,\n  const E extends undefined | readonly string[] = undefined,\n>(nsid: N, parameters: P, input: I, output: O, errors: E = undefined as E) {\n  return new Procedure<N, P, I, O, E>(nsid, parameters, input, output, errors)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/query.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { ObjectSchema, object } from './object.js'\nimport { optional } from './optional.js'\nimport { ParamsSchema, params } from './params.js'\nimport { Payload, payload } from './payload.js'\nimport { Query, query } from './query.js'\nimport { string } from './string.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('Query', () => {\n  describe('constructor', () => {\n    it('creates a Query instance with all parameters', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params({\n        feed: string({ format: 'at-uri' }),\n        limit: optional(integer({ minimum: 1, maximum: 100 })),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          feed: string({ format: 'at-uri' }),\n        }),\n      )\n      const errors = ['NotFound', 'RateLimitExceeded'] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.nsid).toBe(nsid)\n      expect(myQuery.parameters).toBe(parameters)\n      expect(myQuery.output).toBe(output)\n      expect(myQuery.errors).toBe(errors)\n    })\n\n    it('creates a Query instance without errors', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params({\n        feed: string({ format: 'at-uri' }),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          feed: string({ format: 'at-uri' }),\n        }),\n      )\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.nsid).toBe(nsid)\n      expect(myQuery.parameters).toBe(parameters)\n      expect(myQuery.output).toBe(output)\n      expect(myQuery.errors).toBeUndefined()\n    })\n\n    it('creates a Query instance with empty parameters', () => {\n      const nsid = 'app.bsky.actor.getProfile'\n      const parameters = params()\n      const output = payload(\n        'application/json',\n        object({\n          did: string({ format: 'did' }),\n        }),\n      )\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.parameters).toBe(parameters)\n    })\n  })\n\n  describe('properties', () => {\n    it('has nsid property', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.nsid).toBe(nsid)\n      expect(myQuery.nsid).toBe('app.bsky.feed.getFeedSkeleton')\n    })\n\n    it('has parameters property', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params({\n        feed: string({ format: 'at-uri' }),\n      })\n      const output = payload('application/json', undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.parameters).toBe(parameters)\n      expect(myQuery.parameters).toBeInstanceOf(ParamsSchema)\n    })\n\n    it('has output property', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.output).toBe(output)\n      expect(myQuery.output).toBeInstanceOf(Payload)\n    })\n\n    it('has errors property', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n      const errors = ['NotFound', 'RateLimitExceeded'] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery.errors).toBe(errors)\n      expect(myQuery.errors).toEqual(['NotFound', 'RateLimitExceeded'])\n    })\n  })\n\n  describe('with complex parameters', () => {\n    it('creates a Query with multiple parameter types', () => {\n      const nsid = 'app.bsky.feed.searchPosts'\n      const parameters = params({\n        q: string({ minLength: 1 }),\n        limit: optional(integer({ minimum: 1, maximum: 100 })),\n        cursor: optional(string()),\n        author: optional(string({ format: 'did' })),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          cursor: optional(string()),\n          posts: object({}),\n        }),\n      )\n      const errors = ['BadRequest', 'RateLimitExceeded'] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.parameters).toBe(parameters)\n      expect(myQuery.errors).toEqual(['BadRequest', 'RateLimitExceeded'])\n    })\n  })\n\n  describe('with various output payloads', () => {\n    it('creates a Query with undefined output payload', () => {\n      const nsid = 'app.bsky.actor.getProfile'\n      const parameters = params({\n        actor: string({ format: 'at-identifier' }),\n      })\n      const output = payload(undefined, undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.output.encoding).toBeUndefined()\n      expect(myQuery.output.schema).toBeUndefined()\n    })\n\n    it('creates a Query with JSON payload', () => {\n      const nsid = 'app.bsky.actor.getProfile'\n      const parameters = params({\n        actor: string({ format: 'at-identifier' }),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          did: string({ format: 'did' }),\n          handle: string({ format: 'handle' }),\n        }),\n      )\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.output.encoding).toBe('application/json')\n      expect(myQuery.output.schema).toBeInstanceOf(ObjectSchema)\n    })\n\n    it('creates a Query with text payload', () => {\n      const nsid = 'app.bsky.feed.getPost'\n      const parameters = params({\n        uri: string({ format: 'at-uri' }),\n      })\n      const output = payload('text/plain', undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery).toBeInstanceOf(Query)\n      expect(myQuery.output.encoding).toBe('text/plain')\n    })\n  })\n\n  describe('with different error configurations', () => {\n    it('creates a Query with a single error', () => {\n      const nsid = 'app.bsky.feed.getPost'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n      const errors = ['NotFound'] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery.errors).toEqual(['NotFound'])\n    })\n\n    it('creates a Query with multiple errors', () => {\n      const nsid = 'app.bsky.feed.getPost'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n      const errors = ['NotFound', 'Unauthorized', 'RateLimitExceeded'] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery.errors).toEqual([\n        'NotFound',\n        'Unauthorized',\n        'RateLimitExceeded',\n      ])\n    })\n\n    it('creates a Query with empty errors array', () => {\n      const nsid = 'app.bsky.feed.getPost'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n      const errors = [] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery.errors).toEqual([])\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles very long NSID', () => {\n      const nsid = 'com.example.very.long.namespace.identifier.method.name'\n      const parameters = params()\n      const output = payload('application/json', undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.nsid).toBe(nsid)\n    })\n\n    it('handles myQuery with all optional parameters', () => {\n      const nsid = 'app.bsky.feed.search'\n      const parameters = params({\n        q: optional(string()),\n        limit: optional(integer()),\n        cursor: optional(string()),\n      })\n      const output = payload('application/json', undefined)\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.parameters).toBe(parameters)\n    })\n\n    it('handles myQuery with complex nested output schema', () => {\n      const nsid = 'app.bsky.feed.getTimeline'\n      const parameters = params()\n      const output = payload(\n        'application/json',\n        object({\n          cursor: optional(string()),\n          feed: object({\n            post: object({\n              uri: string({ format: 'at-uri' }),\n              cid: string({ format: 'cid' }),\n            }),\n          }),\n        }),\n      )\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.output.schema).toBeInstanceOf(ObjectSchema)\n    })\n  })\n\n  describe('real-world myQuery examples', () => {\n    it('creates a getFeedSkeleton myQuery', () => {\n      const nsid = 'app.bsky.feed.getFeedSkeleton'\n      const parameters = params({\n        feed: string({ format: 'at-uri' }),\n        limit: optional(withDefault(integer({ minimum: 1, maximum: 100 }), 50)),\n        cursor: optional(string()),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          cursor: optional(string()),\n          feed: object({}),\n        }),\n      )\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.nsid).toBe('app.bsky.feed.getFeedSkeleton')\n    })\n\n    it('creates a searchPosts myQuery', () => {\n      const nsid = 'app.bsky.feed.searchPosts'\n      const parameters = params({\n        q: string({ minLength: 1, maxLength: 300 }),\n        limit: optional(withDefault(integer({ minimum: 1, maximum: 100 }), 25)),\n        cursor: optional(string()),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          cursor: optional(string()),\n          hitsTotal: optional(integer()),\n          posts: object({}),\n        }),\n      )\n      const errors = ['BadRequest'] as const\n\n      const myQuery = query(nsid, parameters, output, errors)\n\n      expect(myQuery.nsid).toBe('app.bsky.feed.searchPosts')\n      expect(myQuery.errors).toEqual(['BadRequest'])\n    })\n\n    it('creates a getProfile myQuery', () => {\n      const nsid = 'app.bsky.actor.getProfile'\n      const parameters = params({\n        actor: string({ format: 'at-identifier' }),\n      })\n      const output = payload(\n        'application/json',\n        object({\n          did: string({ format: 'did' }),\n          handle: string({ format: 'handle' }),\n          displayName: optional(string({ maxGraphemes: 64 })),\n          description: optional(string({ maxGraphemes: 256 })),\n        }),\n      )\n\n      const myQuery = query(nsid, parameters, output, undefined)\n\n      expect(myQuery.nsid).toBe('app.bsky.actor.getProfile')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/query.ts",
    "content": "import { NsidString } from '../core.js'\nimport { ParamsSchema } from './params.js'\nimport { Payload } from './payload.js'\n\n/**\n * Represents a Lexicon query (HTTP GET) endpoint definition.\n *\n * Queries are read-only operations that retrieve data from a server.\n * They have parameters (passed as URL query parameters), an output\n * payload, and optional error types.\n *\n * @template TNsid - The NSID identifying this query\n * @template TParameters - The parameters schema type\n * @template TOutputPayload - The output payload type\n * @template TErrors - Array of error type strings, or undefined\n *\n * @example\n * ```ts\n * const getPostQuery = new Query(\n *   'app.bsky.feed.getPost',\n *   l.params({ uri: l.string({ format: 'at-uri' }) }),\n *   l.payload('application/json', postSchema),\n *   ['NotFound']\n * )\n * ```\n */\nexport class Query<\n  const TNsid extends NsidString = NsidString,\n  const TParameters extends ParamsSchema = ParamsSchema,\n  const TOutputPayload extends Payload = Payload,\n  const TErrors extends undefined | readonly string[] =\n    | undefined\n    | readonly string[],\n> {\n  readonly type = 'query' as const\n\n  constructor(\n    readonly nsid: TNsid,\n    readonly parameters: TParameters,\n    readonly output: TOutputPayload,\n    readonly errors: TErrors,\n  ) {}\n}\n\n/**\n * Creates a query definition for a Lexicon GET endpoint.\n *\n * Queries retrieve data without side effects. Parameters are sent as\n * URL query string parameters.\n *\n * @param nsid - The NSID identifying this query endpoint\n * @param parameters - Schema for URL query parameters\n * @param output - Expected response payload schema\n * @param errors - Optional array of error type strings\n * @returns A new {@link Query} instance\n *\n * @example\n * ```ts\n * // Simple query with JSON output\n * const getProfile = l.query(\n *   'app.bsky.actor.getProfile',\n *   l.params({ actor: l.string({ format: 'at-identifier' }) }),\n *   l.jsonPayload({ displayName: l.string(), handle: l.string() }),\n * )\n *\n * // Query with pagination and errors\n * const getTimeline = l.query(\n *   'app.bsky.feed.getTimeline',\n *   l.params({\n *     limit: l.optional(l.integer({ minimum: 1, maximum: 100 })),\n *     cursor: l.optional(l.string()),\n *   }),\n *   l.jsonPayload({ feed: l.array(feedItemSchema), cursor: l.optional(l.string()) }),\n *   ['BlockedActor', 'BlockedByActor'],\n * )\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function query<\n  const N extends NsidString,\n  const P extends ParamsSchema,\n  const O extends Payload,\n  const E extends undefined | readonly string[] = undefined,\n>(nsid: N, parameters: P, output: O, errors: E = undefined as E) {\n  return new Query<N, P, O, E>(nsid, parameters, output, errors)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/record.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { Infer, Unknown$Type, Unknown$TypedObject } from '../core.js'\nimport { object } from './object.js'\nimport { record } from './record.js'\nimport { string } from './string.js'\n\ndescribe('RecordSchema', () => {\n  describe('basic validation', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('validates record with correct $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('app.bsky.feed.post')\n        expect(result.value.text).toBe('Hello world')\n      }\n    })\n\n    it('rejects record with incorrect $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.like',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects record missing $type', () => {\n      const result = schema.safeParse({\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-objects', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse([{ $type: 'app.bsky.feed.post' }])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('isTypeOf method', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        text: string(),\n      }),\n    )\n    type Schema = Infer<typeof schema>\n\n    it('returns true for matching $type', () => {\n      const result = schema.isTypeOf({ $type: 'app.bsky.feed.post' })\n      expect(result).toBe(true)\n    })\n\n    it('returns false for non-matching $type', () => {\n      const result = schema.isTypeOf({ $type: 'app.bsky.feed.like' })\n      expect(result).toBe(false)\n    })\n\n    it('returns false for missing $type', () => {\n      const result = schema.isTypeOf({})\n      expect(result).toBe(false)\n    })\n\n    it('returns false for undefined $type', () => {\n      const result = schema.isTypeOf({ $type: undefined })\n      expect(result).toBe(false)\n    })\n\n    it('returns false for null $type', () => {\n      const result = schema.isTypeOf({ $type: null })\n      expect(result).toBe(false)\n    })\n\n    it('properly discriminates Unknown$TypeObject', () => {\n      function foo(value: Unknown$TypedObject | Schema) {\n        if (schema.isTypeOf(value)) {\n          value.text\n        } else {\n          // @ts-expect-error\n          value.text\n        }\n      }\n\n      foo({\n        $type: 'app.bsky.feed.post',\n        text: 'aze',\n        // @ts-expect-error\n        unknownProperty: 'should not be allowed !',\n      })\n\n      foo({\n        $type: 'blah' as Unknown$Type,\n        // @ts-expect-error\n        unknownProperty: 'should not be allowed !',\n      })\n    })\n  })\n\n  describe('build method', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('adds correct $type to input', () => {\n      const result = schema.build({ text: 'Hello world' })\n      expect(result.$type).toBe('app.bsky.feed.post')\n      expect(result.text).toBe('Hello world')\n    })\n\n    it('preserves existing properties', () => {\n      const result = schema.build({\n        text: 'Hello world',\n        // @ts-expect-error\n        extra: 'value',\n      })\n      expect(result.$type).toBe('app.bsky.feed.post')\n      expect(result.text).toBe('Hello world')\n      // @ts-expect-error\n      expect(result.extra).toBe('value')\n    })\n\n    it('overwrites existing $type', () => {\n      const result = schema.build({\n        // @ts-expect-error\n        $type: 'wrong.type',\n        text: 'Hello world',\n      })\n      expect(result.$type).toBe('app.bsky.feed.post')\n    })\n  })\n\n  describe('key type: any', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('validates record keys', () => {\n      const result = schema.keySchema.safeParse('anyStringKey')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates alphanumeric keys', () => {\n      const result = schema.keySchema.safeParse('key123')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates keys with special characters', () => {\n      const result = schema.keySchema.safeParse('key-with-dashes')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.keySchema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-strings', () => {\n      const result = schema.keySchema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('key type: tid', () => {\n    const schema = record(\n      'tid',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('validates valid TID', () => {\n      const result = schema.keySchema.safeParse('3jzfcijpj2z2a')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid TID format', () => {\n      const result = schema.keySchema.safeParse('not-a-tid')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects TID with invalid characters', () => {\n      const result = schema.keySchema.safeParse('3jzfcijpj2z2!')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.keySchema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects regular strings', () => {\n      const result = schema.keySchema.safeParse('regularString')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('key type: nsid', () => {\n    const schema = record(\n      'nsid',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('validates valid NSID', () => {\n      const result = schema.keySchema.safeParse('app.bsky.feed.post')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates NSID with multiple segments', () => {\n      const result = schema.keySchema.safeParse(\n        'com.example.app.feature.action',\n      )\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid NSID format', () => {\n      const result = schema.keySchema.safeParse('not-an-nsid')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects NSID with invalid characters', () => {\n      const result = schema.keySchema.safeParse('app.bsky.feed!')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.keySchema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('key type: literal', () => {\n    describe('literal:self', () => {\n      const schema = record(\n        'literal:self',\n        'app.bsky.feed.post',\n        object({\n          $type: string(),\n          text: string(),\n        }),\n      )\n\n      it('validates exact literal \"self\"', () => {\n        const result = schema.keySchema.safeParse('self')\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects non-matching strings', () => {\n        const result = schema.keySchema.safeParse('other')\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects case variations', () => {\n        const result = schema.keySchema.safeParse('Self')\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects empty strings', () => {\n        const result = schema.keySchema.safeParse('')\n        expect(result.success).toBe(false)\n      })\n    })\n\n    describe('literal:customKey', () => {\n      const schema = record(\n        'literal:customKey',\n        'app.bsky.feed.post',\n        object({\n          $type: string(),\n          text: string(),\n        }),\n      )\n\n      it('validates exact literal match', () => {\n        const result = schema.keySchema.safeParse('customKey')\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects non-matching strings', () => {\n        const result = schema.keySchema.safeParse('otherKey')\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects partial matches', () => {\n        const result = schema.keySchema.safeParse('custom')\n        expect(result.success).toBe(false)\n      })\n    })\n  })\n\n  describe('$type with hash fragment', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post#main',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('validates record with correct $type including hash', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post#main',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects record with $type without hash', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects record with different hash fragment', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post#other',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('complex nested schema', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string({ maxLength: 300 }),\n        createdAt: string({ format: 'datetime' }),\n      }),\n    )\n\n    it('validates complex record with all constraints', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n        createdAt: '2023-12-25T12:00:00Z',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when nested field violates constraints', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'a'.repeat(301),\n        createdAt: '2023-12-25T12:00:00Z',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when datetime format is invalid', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n        createdAt: 'not-a-date',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('handles $type as number', () => {\n      const result = schema.safeParse({\n        $type: 123,\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('handles $type as boolean', () => {\n      const result = schema.safeParse({\n        $type: true,\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('handles $type as object', () => {\n      const result = schema.safeParse({\n        $type: { value: 'app.bsky.feed.post' },\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('handles $type as array', () => {\n      const result = schema.safeParse({\n        $type: ['app.bsky.feed.post'],\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('preserves extra properties not in schema', () => {\n      const input = {\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n        extra: 'value',\n        another: 123,\n      }\n\n      const result = schema.safeParse(input)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        // @ts-expect-error\n        expect(result.value.extra).toBe('value')\n        // @ts-expect-error\n        expect(result.value.another).toBe(123)\n      }\n    })\n\n    it('handles empty object', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(false)\n    })\n\n    it('handles deeply nested structures', () => {\n      const complexSchema = record(\n        'any',\n        'app.bsky.complex',\n        object({\n          $type: string(),\n          nested: object({\n            deep: object({\n              value: string(),\n            }),\n          }),\n        }),\n      )\n\n      const result = complexSchema.safeParse({\n        $type: 'app.bsky.complex',\n        nested: {\n          deep: {\n            value: 'test',\n          },\n        },\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('$isTypeOf method', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('returns true for matching $type', () => {\n      const result = schema.$isTypeOf({ $type: 'app.bsky.feed.post' })\n      expect(result).toBe(true)\n    })\n\n    it('returns false for non-matching $type', () => {\n      const result = schema.$isTypeOf({ $type: 'app.bsky.feed.like' })\n      expect(result).toBe(false)\n    })\n  })\n\n  describe('$build method', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('adds correct $type to input', () => {\n      const result = schema.$build({ text: 'Hello world' })\n      expect(result.$type).toBe('app.bsky.feed.post')\n      expect(result.text).toBe('Hello world')\n    })\n  })\n\n  describe('validation with missing required fields', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n        author: string(),\n      }),\n    )\n\n    it('rejects when required field is missing', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('validates when all required fields are present', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n        author: 'did:plc:123',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('different record key types', () => {\n    it('constructs with key type \"any\"', () => {\n      const schema = record('any', 'app.bsky.test', object({ $type: string() }))\n      expect(schema.key).toBe('any')\n      expect(schema.keySchema).toBeDefined()\n    })\n\n    it('constructs with key type \"tid\"', () => {\n      const schema = record('tid', 'app.bsky.test', object({ $type: string() }))\n      expect(schema.key).toBe('tid')\n      expect(schema.keySchema).toBeDefined()\n    })\n\n    it('constructs with key type \"nsid\"', () => {\n      const schema = record(\n        'nsid',\n        'app.bsky.test',\n        object({ $type: string() }),\n      )\n      expect(schema.key).toBe('nsid')\n      expect(schema.keySchema).toBeDefined()\n    })\n\n    it('constructs with literal key type', () => {\n      const schema = record(\n        'literal:custom',\n        'app.bsky.test',\n        object({ $type: string() }),\n      )\n      expect(schema.key).toBe('literal:custom')\n      expect(schema.keySchema).toBeDefined()\n    })\n  })\n\n  describe('validation with undefined vs missing fields', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('rejects when required field is explicitly undefined', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: undefined,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when $type is explicitly undefined', () => {\n      const result = schema.safeParse({\n        $type: undefined,\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when $type is null', () => {\n      const result = schema.safeParse({\n        $type: null,\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('record with empty $type string', () => {\n    it('rejects empty $type string', () => {\n      const schema = record(\n        'any',\n        'app.bsky.feed.post',\n        object({\n          $type: string(),\n          text: string(),\n        }),\n      )\n\n      const result = schema.safeParse({\n        $type: '',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('special characters in $type', () => {\n    it('validates $type with dots', () => {\n      const schema = record(\n        'any',\n        'app.bsky.feed.post',\n        object({\n          $type: string(),\n          text: string(),\n        }),\n      )\n\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates $type with hash and alphanumeric fragment', () => {\n      const schema = record(\n        'any',\n        'app.bsky.feed.post#reply123',\n        object({\n          $type: string(),\n          text: string(),\n        }),\n      )\n\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post#reply123',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('case sensitivity', () => {\n    const schema = record(\n      'any',\n      'app.bsky.feed.post',\n      object({\n        $type: string(),\n        text: string(),\n      }),\n    )\n\n    it('rejects $type with different case', () => {\n      const result = schema.safeParse({\n        $type: 'App.Bsky.Feed.Post',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects $type with uppercase', () => {\n      const result = schema.safeParse({\n        $type: 'APP.BSKY.FEED.POST',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/record.ts",
    "content": "import {\n  $Typed,\n  $typed,\n  InferInput,\n  InferOutput,\n  LexiconRecordKey,\n  NsidString,\n  Schema,\n  TidString,\n  Unknown$TypedObject,\n  ValidationContext,\n  Validator,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\nimport { literal } from './literal.js'\nimport { string } from './string.js'\n\n/**\n * Infers the record key type from a RecordSchema.\n *\n * @template R - The RecordSchema type\n */\nexport type InferRecordKey<R extends RecordSchema> =\n  R extends RecordSchema<infer TKey> ? RecordKeySchemaOutput<TKey> : never\n\nexport type TypedRecord<\n  TType extends NsidString,\n  TValue extends { $type?: unknown } = { $type?: unknown },\n> = TValue extends { $type: TType }\n  ? TValue\n  : $Typed<Exclude<TValue, Unknown$TypedObject>, TType>\n\n/**\n * Schema for AT Protocol records with a type identifier and key constraints.\n *\n * Records are the primary data unit in AT Protocol. Each record has a `$type`\n * field identifying its Lexicon schema, and is stored at a specific key\n * (TID, NSID, or other format) in a repository.\n *\n * @template TKey - The record key type ('tid', 'nsid', 'any', or 'literal:...')\n * @template TType - The NSID string identifying this record type\n * @template TShape - The validator type for the record's data shape\n *\n * @example\n * ```ts\n * const postSchema = new RecordSchema(\n *   'tid',\n *   'app.bsky.feed.post',\n *   l.object({ text: l.string(), createdAt: l.string() })\n * )\n * ```\n */\nexport class RecordSchema<\n  const TKey extends LexiconRecordKey = any,\n  const TType extends NsidString = any,\n  const TShape extends Validator<{ [k: string]: unknown }> = any,\n> extends Schema<\n  $Typed<InferInput<TShape>, TType>,\n  $Typed<InferOutput<TShape>, TType>\n> {\n  readonly type = 'record' as const\n\n  keySchema: RecordKeySchema<TKey>\n\n  constructor(\n    readonly key: TKey,\n    readonly $type: TType,\n    readonly schema: TShape,\n  ) {\n    super()\n    this.keySchema = recordKey(key)\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    const result = ctx.validate(input, this.schema)\n\n    if (!result.success) {\n      return result\n    }\n\n    if (result.value.$type !== this.$type) {\n      return ctx.issueInvalidPropertyValue(result.value, '$type', [this.$type])\n    }\n\n    return result\n  }\n\n  build(\n    input: Omit<InferInput<this>, '$type'>,\n  ): $Typed<InferOutput<this>, TType> {\n    return this.parse($typed(input, this.$type))\n  }\n\n  isTypeOf<TValue extends { $type?: unknown }>(\n    value: TValue,\n  ): value is TypedRecord<TType, TValue> {\n    return value.$type === this.$type\n  }\n\n  /**\n   * Bound alias for {@link build} for compatibility with generated utilities.\n   * @see {@link build}\n   */\n  get $build(): typeof this.build {\n    return lazyProperty(this, '$build', this.build.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link isTypeOf} for compatibility with generated utilities.\n   * @see {@link isTypeOf}\n   */\n  get $isTypeOf(): typeof this.isTypeOf {\n    return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this))\n  }\n}\n\nexport type RecordKeySchemaOutput<Key extends LexiconRecordKey> =\n  Key extends 'any'\n    ? string\n    : Key extends 'tid'\n      ? TidString\n      : Key extends 'nsid'\n        ? NsidString\n        : Key extends `literal:${infer L extends string}`\n          ? L\n          : never\n\nexport type RecordKeySchema<Key extends LexiconRecordKey> = Schema<\n  RecordKeySchemaOutput<Key>\n>\n\nconst keySchema = string({ minLength: 1 })\nconst tidSchema = string({ format: 'tid' })\nconst nsidSchema = string({ format: 'nsid' })\nconst selfLiteralSchema = literal('self')\n\nfunction recordKey<Key extends LexiconRecordKey>(\n  key: Key,\n): RecordKeySchema<Key> {\n  // @NOTE Use cached instances for common schemas\n  if (key === 'any') return keySchema as any\n  if (key === 'tid') return tidSchema as any\n  if (key === 'nsid') return nsidSchema as any\n  if (key.startsWith('literal:')) {\n    const value = key.slice(8) as RecordKeySchemaOutput<Key>\n    if (value === 'self') return selfLiteralSchema as any\n    return literal(value)\n  }\n\n  throw new Error(`Unsupported record key type: ${key}`)\n}\n\n/**\n * Ensures that a `$type` used in a record is a valid NSID (i.e. no fragment).\n */\ntype AsNsid<T> = T extends `${string}#${string}` ? never : T\n\n/**\n * Creates a record schema for AT Protocol records.\n *\n * Records are the primary data unit in AT Protocol repositories. They have\n * a `$type` field identifying their Lexicon schema, and are stored at keys\n * following a specific format (TID, NSID, etc.).\n *\n * This function offers two overloads:\n * - One that infers the output type from the provided arguments (does not\n *   support circular references)\n * - One with an explicitly defined interface for use with codegen and\n *   circular references\n *\n * @param key - The record key type: 'tid', 'nsid', 'any', or 'literal:value'\n * @param type - The NSID identifying this record type (e.g., 'app.bsky.feed.post')\n * @param validator - Schema validator for the record's properties\n * @returns A new {@link RecordSchema} instance\n *\n * @example\n * ```ts\n * // Post record with TID key\n * const postSchema = l.record('tid', 'app.bsky.feed.post', l.object({\n *   text: l.string({ maxGraphemes: 300 }),\n *   createdAt: l.string({ format: 'datetime' }),\n *   reply: l.optional(l.object({\n *     root: l.ref(() => strongRefSchema),\n *     parent: l.ref(() => strongRefSchema),\n *   })),\n * }))\n *\n * // Profile record with literal 'self' key\n * const profileSchema = l.record('literal:self', 'app.bsky.actor.profile', l.object({\n *   displayName: l.optional(l.string({ maxGraphemes: 64 })),\n *   description: l.optional(l.string({ maxGraphemes: 256 })),\n *   avatar: l.optional(l.blob({ accept: ['image/*'] })),\n * }))\n *\n * // Build a record with automatic $type injection\n * const post = postSchema.build({ text: 'Hello!', createdAt: new Date().toISOString() })\n * ```\n */\nexport function record<\n  const K extends LexiconRecordKey,\n  const T extends NsidString,\n  const S extends Validator<{ [k: string]: unknown }>,\n>(key: K, type: AsNsid<T>, validator: S): RecordSchema<K, T, S>\nexport function record<\n  const K extends LexiconRecordKey,\n  const V extends { $type: NsidString },\n>(\n  key: K,\n  type: AsNsid<V['$type']>,\n  validator: Validator<Omit<V, '$type'>>,\n): RecordSchema<K, V['$type'], Validator<Omit<V, '$type'>>>\n/*@__NO_SIDE_EFFECTS__*/\nexport function record<\n  const K extends LexiconRecordKey,\n  const T extends NsidString,\n  const S extends Validator<{ [k: string]: unknown }>,\n>(key: K, type: T, validator: S) {\n  return new RecordSchema<K, T, S>(key, type, validator)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/ref.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { Schema, Validator } from '../core.js'\nimport { integer } from './integer.js'\nimport { object } from './object.js'\nimport { optional } from './optional.js'\nimport { ref } from './ref.js'\nimport { string } from './string.js'\n\ndescribe('RefSchema', () => {\n  describe('basic validation', () => {\n    it('validates through a simple string reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates through an integer reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer()\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid input through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates null rejection through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates undefined rejection through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer()\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('lazy schema resolution', () => {\n    it('does not call getter until first validation', () => {\n      let getterCalled = false\n      const schema = ref(() => {\n        getterCalled = true\n        return string()\n      })\n      expect(getterCalled).toBe(false)\n\n      schema.safeParse('test')\n      expect(getterCalled).toBe(true)\n    })\n\n    it('throws error if getter is called multiple times', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n\n      // Access schema property to resolve it\n      schema.validator\n\n      // Try to access the original getter again (which should throw)\n      // This is internal behavior, but we're testing the protection mechanism\n      expect(() => {\n        // Force access to the cached schema property\n        const schemaValue = schema.validator\n        // This should work fine as it's now cached\n        expect(schemaValue).toBeDefined()\n      }).not.toThrow()\n    })\n  })\n\n  describe('with object schemas', () => {\n    it('validates objects through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = object({\n        name: string(),\n        age: integer(),\n      })\n\n      expect(\n        schema.safeValidate({\n          name: 'Alice',\n          age: 30,\n        }),\n      ).toMatchObject({\n        success: true,\n      })\n    })\n\n    it('rejects invalid objects through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = object({\n        name: string(),\n        age: integer(),\n      })\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 'thirty',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with missing properties through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = object({\n        name: string(),\n        age: integer(),\n      })\n      const result = schema.safeParse({\n        name: 'Alice',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with constrained schemas', () => {\n    it('validates string with minLength constraint through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string({ minLength: 5 })\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects string violating minLength through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string({ minLength: 5 })\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('validates integer with range constraints through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer({ minimum: 0, maximum: 100 })\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects integer violating constraints through reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer({ minimum: 0, maximum: 100 })\n      const result = schema.safeParse(150)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('circular references', () => {\n    it('supports indirect circular references', () => {\n      // Create two schemas that reference each other\n      // This demonstrates forward references are possible\n\n      type A = { value: string; ref?: B }\n      type B = { value: number; ref?: A }\n\n      const schemaA: Schema<A> = object({\n        value: string(),\n        ref: optional(ref<Validator<B>>((() => schemaB) as any)),\n      })\n\n      const schemaB: Schema<B> = object({\n        value: integer(),\n        ref: optional(ref<Validator<A>>((() => schemaA) as any)),\n      })\n\n      expect(\n        schemaB.matches({\n          value: 42,\n          ref: {\n            value: 'hello',\n            ref: {\n              value: 3,\n            },\n          },\n        }),\n      ).toBe(true)\n\n      expect(\n        schemaA.matches({\n          value: 'hello',\n          ref: {\n            value: 3,\n            ref: {\n              value: 'world',\n            },\n          },\n        }),\n      ).toBe(true)\n    })\n  })\n\n  describe('multiple validations', () => {\n    it('validates multiple inputs correctly', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string({ minLength: 3 })\n\n      const result1 = schema.safeParse('hello')\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse('hi')\n      expect(result2.success).toBe(false)\n\n      const result3 = schema.safeParse('world')\n      expect(result3.success).toBe(true)\n\n      const result4 = schema.safeParse('no')\n      expect(result4.success).toBe(false)\n    })\n\n    it('handles different types of validation failures', () => {\n      const schema = ref(() =>\n        object({\n          name: string({ minLength: 2 }),\n          age: integer({ minimum: 0 }),\n        }),\n      )\n\n      const result1 = schema.safeParse({ name: 'A', age: 25 })\n      expect(result1.success).toBe(false)\n\n      const result2 = schema.safeParse({ name: 'Alice', age: -5 })\n      expect(result2.success).toBe(false)\n\n      const result3 = schema.safeParse({ name: 'Alice', age: 25 })\n      expect(result3.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles empty string validation', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles zero validation', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer()\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects NaN through integer reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer()\n      const result = schema.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Infinity through integer reference', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = integer()\n      const result = schema.safeParse(Infinity)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays when expecting string', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse(['array'])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects when expecting string', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse({ key: 'value' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans when expecting string', () => {\n      const schema = ref(() => innerSchema)\n      const innerSchema = string()\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('nested references', () => {\n    it('validates through nested RefSchema', () => {\n      const innerRef = ref(() => innerSchema)\n      const innerSchema = string({ minLength: 3 })\n      const outerRef = ref(() => innerRef)\n\n      const result = outerRef.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid input through nested RefSchema', () => {\n      const innerRef = ref(() => innerSchema)\n      const innerSchema = string({ minLength: 3 })\n      const outerRef = ref(() => innerRef)\n\n      const result = outerRef.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('validates with deeply nested references', () => {\n      const level3 = ref(() => innerSchema)\n      const innerSchema = integer({ minimum: 0 })\n      const level2 = ref(() => level3)\n      const level1 = ref(() => level2)\n\n      const result = level1.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('schema property access', () => {\n    it('allows direct access to resolved schema', () => {\n      const innerSchema = string({ minLength: 5 })\n      const refSchema = ref(() => innerSchema)\n\n      const resolved = refSchema.validator\n      expect(resolved).toBe(innerSchema)\n    })\n\n    it('returns same instance on multiple schema property accesses', () => {\n      const innerSchema = string()\n      const refSchema = ref(() => innerSchema)\n\n      const first = refSchema.validator\n      const second = refSchema.validator\n      const third = refSchema.validator\n\n      expect(first).toBe(second)\n      expect(second).toBe(third)\n    })\n\n    it('resolves schema before validation', () => {\n      let resolved = false\n      const refSchema = ref(() => {\n        resolved = true\n        return string()\n      })\n\n      expect(resolved).toBe(false)\n\n      const schemaValue = refSchema.validator\n      expect(resolved).toBe(true)\n      expect(schemaValue).toBeDefined()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/ref.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n  WrappedValidator,\n} from '../core.js'\n\n/**\n * Function type that returns a validator, used for lazy schema resolution.\n *\n * @template TValidator - The validator type that will be returned\n */\nexport type RefSchemaGetter<out TValidator extends Validator> = () => TValidator\n\n/**\n * Schema for creating references to other schemas with lazy resolution.\n *\n * Useful for handling circular references or breaking module dependency cycles.\n * The referenced schema is resolved lazily when first needed for validation.\n *\n * @template TValidator - The referenced validator type\n *\n * @example\n * ```ts\n * // Self-referential schema for tree structure\n * const nodeSchema = l.object({\n *   value: l.string(),\n *   children: l.array(l.ref(() => nodeSchema)),\n * })\n * ```\n */\nexport class RefSchema<const TValidator extends Validator>\n  extends Schema<InferInput<TValidator>, InferOutput<TValidator>>\n  implements WrappedValidator<TValidator>\n{\n  readonly type = 'ref' as const\n\n  #getter: RefSchemaGetter<TValidator>\n\n  constructor(getter: RefSchemaGetter<TValidator>) {\n    // @NOTE In order to avoid circular dependency issues, we don't resolve\n    // the schema here. Instead, we resolve it lazily when first accessed.\n\n    super()\n\n    this.#getter = getter\n  }\n\n  get validator(): TValidator {\n    return this.#getter.call(null)\n  }\n\n  unwrap(): TValidator {\n    return this.validator\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    return ctx.validate(input, this.validator)\n  }\n}\n\n/**\n * Creates a reference schema with lazy resolution.\n *\n * Allows referencing schemas that may not be defined yet, enabling\n * circular references and breaking dependency cycles. The getter function\n * is called lazily when validation is first performed.\n *\n * @param get - Function that returns the referenced validator\n * @returns A new {@link RefSchema} instance\n *\n * @example\n * ```ts\n * // Circular reference - tree node that contains children of the same type\n * const treeNodeSchema = l.object({\n *   name: l.string(),\n *   children: l.optional(l.array(l.ref(() => treeNodeSchema))),\n * })\n *\n * // Cross-module reference\n * const commentSchema = l.object({\n *   text: l.string(),\n *   author: l.ref(() => userSchema), // userSchema defined elsewhere\n * })\n *\n * // Explicitly typed reference\n * const itemSchema = l.ref<Item>(() => complexItemSchema)\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function ref<const TValidator extends Validator>(\n  get: RefSchemaGetter<TValidator>,\n): RefSchema<TValidator>\nexport function ref<TInput, TOutput extends TInput = TInput>(\n  get: RefSchemaGetter<Validator<TInput, TOutput>>,\n): RefSchema<Validator<TInput, TOutput>>\nexport function ref<const TValidator extends Validator>(\n  get: RefSchemaGetter<TValidator>,\n) {\n  return new RefSchema<TValidator>(get)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/refine.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { object } from './object.js'\nimport { refine } from './refine.js'\nimport { string } from './string.js'\n\ndescribe('refine', () => {\n  describe('basic refinement checks', () => {\n    const schema = refine(integer(), {\n      check: (value) => value > 0,\n      message: 'Value must be positive',\n    })\n\n    it('validates values that pass the refinement check', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values that fail the refinement check', () => {\n      const result = schema.safeParse(-5)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero when check requires positive', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n\n    it('still validates base schema constraints', () => {\n      const result = schema.safeParse('not a number')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement with type assertions', () => {\n    const schema = refine(integer(), {\n      check: (value): value is 42 => value === 42,\n      message: 'Value must be 42',\n    })\n\n    it('validates values that pass the type assertion', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values that fail the type assertion', () => {\n      const result = schema.safeParse(43)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement with string schema', () => {\n    const schema = refine(string(), {\n      check: (value) => value.includes('@'),\n      message: 'String must contain @ symbol',\n    })\n\n    it('validates strings that pass the refinement check', () => {\n      const result = schema.safeParse('user@example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings that fail the refinement check', () => {\n      const result = schema.safeParse('userexample.com')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-strings before refinement check', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement with base schema constraints', () => {\n    const schema = refine(integer({ minimum: 0, maximum: 100 }), {\n      check: (value) => value % 2 === 0,\n      message: 'Value must be even',\n    })\n\n    it('validates values that pass both base constraints and refinement', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values that fail base constraints', () => {\n      const result = schema.safeParse(150)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values that pass base constraints but fail refinement', () => {\n      const result = schema.safeParse(43)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values that fail both base constraints and refinement', () => {\n      const result = schema.safeParse(-5)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('multiple refinements chained', () => {\n    const schema = refine(\n      refine(integer(), {\n        check: (value) => value > 0,\n        message: 'Value must be positive',\n      }),\n      {\n        check: (value) => value < 100,\n        message: 'Value must be less than 100',\n      },\n    )\n\n    it('validates values that pass all refinements', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values that fail first refinement', () => {\n      const result = schema.safeParse(-5)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects values that fail second refinement', () => {\n      const result = schema.safeParse(150)\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts values at the boundary of both refinements', () => {\n      const result1 = schema.safeParse(1)\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse(99)\n      expect(result2.success).toBe(true)\n    })\n  })\n\n  describe('refinement with custom path', () => {\n    const schema = refine(integer(), {\n      check: (value) => value > 0,\n      message: 'Value must be positive',\n      path: 'customField',\n    })\n\n    it('validates values that pass the refinement', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values that fail the refinement', () => {\n      const result = schema.safeParse(-5)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement with array path', () => {\n    const schema = refine(integer(), {\n      check: (value) => value > 0,\n      message: 'Value must be positive',\n      path: ['nested', 'field'],\n    })\n\n    it('validates values that pass the refinement', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values that fail the refinement', () => {\n      const result = schema.safeParse(-5)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement on object properties', () => {\n    const schema = object({\n      age: refine(integer(), {\n        check: (value) => value >= 18,\n        message: 'Age must be at least 18',\n      }),\n      email: refine(string(), {\n        check: (value) => value.includes('@'),\n        message: 'Email must contain @ symbol',\n      }),\n    })\n\n    it('validates objects with properties that pass refinements', () => {\n      const result = schema.safeParse({\n        age: 25,\n        email: 'user@example.com',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects objects with age below minimum', () => {\n      const result = schema.safeParse({\n        age: 16,\n        email: 'user@example.com',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with invalid email', () => {\n      const result = schema.safeParse({\n        age: 25,\n        email: 'userexample.com',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects with both properties failing refinements', () => {\n      const result = schema.safeParse({\n        age: 16,\n        email: 'userexample.com',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('complex refinement logic', () => {\n    const schema = refine(string(), {\n      check: (value) => {\n        const hasLowerCase = /[a-z]/.test(value)\n        const hasUpperCase = /[A-Z]/.test(value)\n        const hasNumber = /[0-9]/.test(value)\n        return hasLowerCase && hasUpperCase && hasNumber\n      },\n      message: 'Password must contain lowercase, uppercase, and numbers',\n    })\n\n    it('validates strings that meet all password requirements', () => {\n      const result = schema.safeParse('Password123')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings without lowercase', () => {\n      const result = schema.safeParse('PASSWORD123')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings without uppercase', () => {\n      const result = schema.safeParse('password123')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings without numbers', () => {\n      const result = schema.safeParse('Password')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement with comparison logic', () => {\n    const schema = refine(integer(), {\n      check: (value) => {\n        // Check if value is a prime number\n        if (value <= 1) return false\n        if (value <= 3) return true\n        if (value % 2 === 0 || value % 3 === 0) return false\n        for (let i = 5; i * i <= value; i += 6) {\n          if (value % i === 0 || value % (i + 2) === 0) return false\n        }\n        return true\n      },\n      message: 'Value must be a prime number',\n    })\n\n    it('validates prime numbers', () => {\n      const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]\n      primes.forEach((prime) => {\n        const result = schema.safeParse(prime)\n        expect(result.success).toBe(true)\n      })\n    })\n\n    it('rejects non-prime numbers', () => {\n      const nonPrimes = [0, 1, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20]\n      nonPrimes.forEach((nonPrime) => {\n        const result = schema.safeParse(nonPrime)\n        expect(result.success).toBe(false)\n      })\n    })\n  })\n\n  describe('refinement with string length logic', () => {\n    const schema = refine(string({ minLength: 1, maxLength: 50 }), {\n      check: (value) => value.trim().length > 0,\n      message: 'String must not be only whitespace',\n    })\n\n    it('validates non-empty trimmed strings', () => {\n      const result = schema.safeParse('hello world')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings with leading/trailing whitespace', () => {\n      const result = schema.safeParse('  hello  ')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings with only whitespace', () => {\n      const result = schema.safeParse('     ')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects single space', () => {\n      const result = schema.safeParse(' ')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects tabs and newlines only', () => {\n      const result = schema.safeParse('\\t\\n  ')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement preserves original schema', () => {\n    const originalSchema = integer({ minimum: 0 })\n    const refinedSchema = refine(originalSchema, {\n      check: (value) => value % 2 === 0,\n      message: 'Value must be even',\n    })\n\n    it('original schema still works independently', () => {\n      const result = originalSchema.safeParse(5)\n      expect(result.success).toBe(true)\n    })\n\n    it('refined schema has additional constraint', () => {\n      const result = refinedSchema.safeParse(5)\n      expect(result.success).toBe(false)\n    })\n\n    it('refined schema inherits base constraints', () => {\n      const result1 = refinedSchema.safeParse(-2)\n      expect(result1.success).toBe(false)\n\n      const result2 = refinedSchema.safeParse(4)\n      expect(result2.success).toBe(true)\n    })\n  })\n\n  describe('refinement with boundary conditions', () => {\n    const schema = refine(integer({ minimum: 0, maximum: 100 }), {\n      check: (value) => value !== 50,\n      message: 'Value must not be 50',\n    })\n\n    it('validates values at lower boundary', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates values at upper boundary', () => {\n      const result = schema.safeParse(100)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects the specific excluded value', () => {\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates values around the excluded value', () => {\n      const result1 = schema.safeParse(49)\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse(51)\n      expect(result2.success).toBe(true)\n    })\n  })\n\n  describe('refinement with regex patterns', () => {\n    const schema = refine(string(), {\n      check: (value) => /^[A-Z][a-zA-Z0-9]*$/.test(value),\n      message: 'Must start with uppercase letter',\n    })\n\n    it('validates strings starting with uppercase', () => {\n      const result = schema.safeParse('Hello123')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings starting with lowercase', () => {\n      const result = schema.safeParse('hello123')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings starting with number', () => {\n      const result = schema.safeParse('123Hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings starting with special character', () => {\n      const result = schema.safeParse('_Hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('validates single uppercase letter', () => {\n      const result = schema.safeParse('A')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('refinement with custom error messages', () => {\n    const schema = refine(integer(), {\n      check: (value) => value >= 1 && value <= 10,\n      message: 'Value must be between 1 and 10 (inclusive)',\n    })\n\n    it('validates values within range', () => {\n      const result = schema.safeParse(5)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects values outside range', () => {\n      const result = schema.safeParse(11)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles refinement that always returns true', () => {\n      const schema = refine(integer(), {\n        check: () => true,\n        message: 'This should never fail',\n      })\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n    })\n\n    it('handles refinement that always returns false', () => {\n      const schema = refine(integer(), {\n        check: () => false,\n        message: 'This always fails',\n      })\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(false)\n    })\n\n    it('handles empty string refinement', () => {\n      const schema = refine(string(), {\n        check: (value) => value === '',\n        message: 'Value must be empty string',\n      })\n      const result1 = schema.safeParse('')\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse('hello')\n      expect(result2.success).toBe(false)\n    })\n\n    it('handles zero value refinement', () => {\n      const schema = refine(integer(), {\n        check: (value) => value === 0,\n        message: 'Value must be zero',\n      })\n      const result1 = schema.safeParse(0)\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse(1)\n      expect(result2.success).toBe(false)\n    })\n\n    it('handles negative value refinement', () => {\n      const schema = refine(integer(), {\n        check: (value) => value < 0,\n        message: 'Value must be negative',\n      })\n      const result1 = schema.safeParse(-5)\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse(5)\n      expect(result2.success).toBe(false)\n    })\n  })\n\n  describe('refinement with combined string constraints', () => {\n    const schema = refine(string({ minLength: 8, maxLength: 20 }), {\n      check: (value) => {\n        const hasSpecialChar = /[!@#$%^&*(),.?\":{}|<>]/.test(value)\n        const hasLetter = /[a-zA-Z]/.test(value)\n        return hasSpecialChar && hasLetter\n      },\n      message: 'Must contain letters and special characters',\n    })\n\n    it('validates strings meeting all requirements', () => {\n      const result = schema.safeParse('Hello@World!')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings too short', () => {\n      const result = schema.safeParse('Hi@!')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings without special characters', () => {\n      const result = schema.safeParse('HelloWorld')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings without letters', () => {\n      const result = schema.safeParse('12345!@#$%')\n      expect(result.success).toBe(false)\n    })\n\n    it('validates strings at minimum length with requirements', () => {\n      const result = schema.safeParse('Hello@12')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('refinement inheritance', () => {\n    const baseSchema = integer({ minimum: 0, maximum: 1000 })\n    const refinedOnce = refine(baseSchema, {\n      check: (value) => value % 10 === 0,\n      message: 'Must be divisible by 10',\n    })\n    const refinedTwice = refine(refinedOnce, {\n      check: (value) => value % 100 === 0,\n      message: 'Must be divisible by 100',\n    })\n\n    it('validates with all inherited constraints', () => {\n      const result = refinedTwice.safeParse(500)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when failing base constraint', () => {\n      const result = refinedTwice.safeParse(1500)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when failing first refinement', () => {\n      const result = refinedTwice.safeParse(505)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when failing second refinement', () => {\n      const result = refinedTwice.safeParse(50)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('refinement with string format validation', () => {\n    const schema = refine(string({ format: 'uri' }), {\n      check: (value) => value.startsWith('https://'),\n      message: 'Must be HTTPS URI',\n    })\n\n    it('validates HTTPS URIs', () => {\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects HTTP URIs', () => {\n      const result = schema.safeParse('http://example.com')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects other valid URIs', () => {\n      const result = schema.safeParse('ftp://example.com')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid URIs', () => {\n      const result = schema.safeParse('not a uri')\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/refine.ts",
    "content": "import {\n  InferInput,\n  IssueCustom,\n  ValidationContext,\n  ValidationResult,\n  Validator,\n} from '../core.js'\nimport { CustomAssertionContext } from './custom.js'\n\n/**\n * Configuration for a refinement check that validates a condition.\n *\n * @template T - The type being validated\n * @property check - Function that returns true if the value passes the check\n * @property message - Error message when the check fails\n * @property path - Optional path to associate with the error\n */\nexport type RefinementCheck<T> = {\n  check: (value: T, ctx: CustomAssertionContext) => boolean\n  message: string\n  path?: PropertyKey | readonly PropertyKey[]\n}\n\n/**\n * Configuration for a refinement assertion that narrows the type.\n *\n * @template T - The input type being validated\n * @template Out - The narrowed output type\n * @property check - Type guard function that narrows the type\n * @property message - Error message when the assertion fails\n * @property path - Optional path to associate with the error\n */\nexport type RefinementAssertion<T, Out extends T> = {\n  check: (this: null, value: T, ctx: CustomAssertionContext) => value is Out\n  message: string\n  path?: PropertyKey | readonly PropertyKey[]\n}\n\n/**\n * Infers the input type from a refinement configuration.\n *\n * @template R - The refinement type\n */\nexport type InferRefinement<R> =\n  R extends RefinementCheck<infer T>\n    ? T\n    : R extends RefinementAssertion<infer T, any>\n      ? T\n      : never\n\n/**\n * Union type of refinement check or assertion.\n *\n * @template T - The input type being validated\n * @template Out - The output type (same as T for checks, narrowed for assertions)\n */\nexport type Refinement<T = any, Out extends T = T> =\n  | RefinementCheck<T>\n  | RefinementAssertion<T, Out>\n\n/**\n * Creates a refined schema by adding additional validation constraints.\n *\n * Wraps an existing schema with an additional check function. The base schema\n * is validated first, then the refinement check is applied to the result.\n *\n * @param schema - The base schema to refine\n * @param refinement - The refinement check or assertion to apply\n * @returns A new schema that includes the refinement\n *\n * @example\n * ```ts\n * // Simple check refinement\n * const positiveInt = l.refine(l.integer(), {\n *   check: (value) => value > 0,\n *   message: 'Value must be positive',\n * })\n *\n * positiveInt.parse(5)  // 5\n * positiveInt.parse(-1) // throws\n *\n * // Type-narrowing assertion\n * const nonEmptyString = l.refine(l.string(), {\n *   check: (value): value is string & { length: number } => value.length > 0,\n *   message: 'String must not be empty',\n * })\n *\n * // With custom path for nested errors\n * const validDateRange = l.refine(\n *   l.object({ start: l.string(), end: l.string() }),\n *   {\n *     check: (v) => new Date(v.start) < new Date(v.end),\n *     message: 'Start date must be before end date',\n *     path: ['end'],\n *   }\n * )\n * ```\n */\nexport function refine<\n  const TValidator extends Validator,\n  TInput extends InferInput<TValidator>,\n>(\n  schema: TValidator,\n  refinement: RefinementAssertion<InferInput<TValidator>, TInput>,\n): TValidator & Validator<TInput>\nexport function refine<const TValidator extends Validator>(\n  schema: TValidator,\n  refinement: RefinementCheck<InferInput<TValidator>>,\n): TValidator\nexport function refine<\n  TRefinement extends Refinement,\n  const TValidator extends Validator<InferRefinement<TRefinement>>,\n>(schema: TValidator, refinement: TRefinement): TValidator\n/*@__NO_SIDE_EFFECTS__*/\nexport function refine<const TValidator extends Validator>(\n  schema: TValidator,\n  refinement: Refinement<unknown>,\n): TValidator {\n  // This is basically the same as monkey patching the \"validateInContext\"\n  // method to the schema, but done in a way that does not mutate the original\n  // schema. This is safe to do because Validators don't update their internal\n  // state over their lifetime.\n  return Object.create(schema, {\n    validateInContext: {\n      // We do not use an arrow function to avoid creating a closure\n      value: validateInContextUnbound.bind({ schema, refinement }),\n      enumerable: false,\n      writable: false,\n      configurable: true,\n    },\n  })\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nfunction validateInContextUnbound<S extends Validator>(\n  this: {\n    schema: S\n    refinement: Refinement<InferInput<S>>\n  },\n  input: unknown,\n  ctx: ValidationContext,\n): ValidationResult<InferInput<S>> {\n  const result = ctx.validate(input, this.schema)\n  if (!result.success) return result\n\n  const checkResult = this.refinement.check.call(null, result.value, ctx)\n  if (!checkResult) {\n    const path = ctx.concatPath(this.refinement.path)\n    return ctx.issue(new IssueCustom(path, input, this.refinement.message))\n  }\n\n  return result\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/regexp.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { regexp } from './regexp.js'\n\ndescribe('RegexpSchema', () => {\n  describe('basic validation', () => {\n    const schema = regexp(/^[a-z]+$/)\n\n    it('validates strings matching the pattern', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings with different matching values', () => {\n      const result = schema.safeParse('world')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings not matching the pattern', () => {\n      const result = schema.safeParse('Hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings with numbers when pattern requires letters', () => {\n      const result = schema.safeParse('hello123')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-strings', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['hello'])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects plain objects', () => {\n      const result = schema.safeParse({ value: 'hello' })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('numeric patterns', () => {\n    const schema = regexp(/^\\d+$/)\n\n    it('validates numeric strings', () => {\n      const result = schema.safeParse('12345')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates single digit strings', () => {\n      const result = schema.safeParse('0')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings with letters', () => {\n      const result = schema.safeParse('123abc')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('email pattern', () => {\n    const schema = regexp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/)\n\n    it('validates simple email addresses', () => {\n      const result = schema.safeParse('user@example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates email with subdomain', () => {\n      const result = schema.safeParse('user@mail.example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates email with numbers and special characters', () => {\n      const result = schema.safeParse('user.name+tag@example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects email without @', () => {\n      const result = schema.safeParse('userexample.com')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects email without domain', () => {\n      const result = schema.safeParse('user@')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects email without TLD', () => {\n      const result = schema.safeParse('user@example')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('URL pattern', () => {\n    const schema = regexp(/^https?:\\/\\/[^\\s/$.?#].[^\\s]*$/)\n\n    it('validates HTTP URLs', () => {\n      const result = schema.safeParse('http://example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates HTTPS URLs', () => {\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates URLs with paths', () => {\n      const result = schema.safeParse('https://example.com/path/to/resource')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates URLs with query parameters', () => {\n      const result = schema.safeParse('https://example.com?param=value')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects URLs without protocol', () => {\n      const result = schema.safeParse('example.com')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects URLs with spaces', () => {\n      const result = schema.safeParse('https://example.com/path with spaces')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('phone number pattern', () => {\n    const schema = regexp(/^\\+?[1-9]\\d{1,14}$/)\n\n    it('validates simple phone numbers', () => {\n      const result = schema.safeParse('1234567890')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates phone numbers with plus prefix', () => {\n      const result = schema.safeParse('+1234567890')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates international phone numbers', () => {\n      const result = schema.safeParse('+441234567890')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects phone numbers starting with zero', () => {\n      const result = schema.safeParse('0123456789')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects phone numbers with letters', () => {\n      const result = schema.safeParse('123-456-ABCD')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects phone numbers with special characters', () => {\n      const result = schema.safeParse('123-456-7890')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('hex color pattern', () => {\n    const schema = regexp(/^#[0-9A-Fa-f]{6}$/)\n\n    it('validates 6-digit hex colors', () => {\n      const result = schema.safeParse('#FF5733')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates lowercase hex colors', () => {\n      const result = schema.safeParse('#ff5733')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates mixed case hex colors', () => {\n      const result = schema.safeParse('#Ff5733')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects hex colors without hash', () => {\n      const result = schema.safeParse('FF5733')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects 3-digit hex colors', () => {\n      const result = schema.safeParse('#FFF')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects hex colors with invalid characters', () => {\n      const result = schema.safeParse('#GG5733')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('alphanumeric pattern', () => {\n    const schema = regexp(/^[a-zA-Z0-9]+$/)\n\n    it('validates alphanumeric strings', () => {\n      const result = schema.safeParse('abc123')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates only letters', () => {\n      const result = schema.safeParse('abcdef')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates only numbers', () => {\n      const result = schema.safeParse('123456')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings with spaces', () => {\n      const result = schema.safeParse('abc 123')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings with special characters', () => {\n      const result = schema.safeParse('abc_123')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('case-insensitive pattern', () => {\n    const schema = regexp(/^hello$/i)\n\n    it('validates lowercase match', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates uppercase match', () => {\n      const result = schema.safeParse('HELLO')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates mixed case match', () => {\n      const result = schema.safeParse('HeLLo')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-matching strings', () => {\n      const result = schema.safeParse('world')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('multiline pattern', () => {\n    const schema = regexp(/^line\\d+$/m)\n\n    it('validates single line matching pattern', () => {\n      const result = schema.safeParse('line1')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates multiline string with matching line', () => {\n      const result = schema.safeParse('line1\\nline2')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings without matching lines', () => {\n      const result = schema.safeParse('hello\\nworld')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('optional character pattern', () => {\n    const schema = regexp(/^colou?r$/)\n\n    it('validates with optional character present', () => {\n      const result = schema.safeParse('colour')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with optional character absent', () => {\n      const result = schema.safeParse('color')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings not matching pattern', () => {\n      const result = schema.safeParse('colouur')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('character range pattern', () => {\n    const schema = regexp(/^[0-5]+$/)\n\n    it('validates strings within range', () => {\n      const result = schema.safeParse('012345')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates single character in range', () => {\n      const result = schema.safeParse('3')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings with characters outside range', () => {\n      const result = schema.safeParse('0123456')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('quantifier pattern', () => {\n    const schema = regexp(/^a{3}$/)\n\n    it('validates exact repetition count', () => {\n      const result = schema.safeParse('aaa')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects fewer repetitions', () => {\n      const result = schema.safeParse('aa')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects more repetitions', () => {\n      const result = schema.safeParse('aaaa')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('range quantifier pattern', () => {\n    const schema = regexp(/^a{2,4}$/)\n\n    it('validates minimum repetition count', () => {\n      const result = schema.safeParse('aa')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates middle repetition count', () => {\n      const result = schema.safeParse('aaa')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates maximum repetition count', () => {\n      const result = schema.safeParse('aaaa')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects fewer than minimum repetitions', () => {\n      const result = schema.safeParse('a')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects more than maximum repetitions', () => {\n      const result = schema.safeParse('aaaaa')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('alternation pattern', () => {\n    const schema = regexp(/^(cat|dog|bird)$/)\n\n    it('validates first alternative', () => {\n      const result = schema.safeParse('cat')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second alternative', () => {\n      const result = schema.safeParse('dog')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates third alternative', () => {\n      const result = schema.safeParse('bird')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings not matching any alternative', () => {\n      const result = schema.safeParse('fish')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('word boundary pattern', () => {\n    const schema = regexp(/\\bword\\b/)\n\n    it('validates word with boundaries', () => {\n      const result = schema.safeParse('word')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates word within sentence', () => {\n      const result = schema.safeParse('this is a word')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects word as part of larger word', () => {\n      const result = schema.safeParse('wording')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('lookahead pattern', () => {\n    const schema = regexp(/^(?=.*[A-Z])(?=.*[0-9]).{8,}$/)\n\n    it('validates string meeting all lookahead conditions', () => {\n      const result = schema.safeParse('Password1')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates string with multiple uppercase and numbers', () => {\n      const result = schema.safeParse('ABC12345')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects string without uppercase', () => {\n      const result = schema.safeParse('password1')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string without number', () => {\n      const result = schema.safeParse('Password')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects string too short', () => {\n      const result = schema.safeParse('Pass1')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('unicode pattern', () => {\n    const schema = regexp(/^[\\u4e00-\\u9fa5]+$/)\n\n    it('validates Chinese characters', () => {\n      const result = schema.safeParse('你好')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates multiple Chinese characters', () => {\n      const result = schema.safeParse('世界')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-Chinese characters', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects mixed Chinese and English', () => {\n      const result = schema.safeParse('你好hello')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('empty string pattern', () => {\n    const schema = regexp(/^$/)\n\n    it('validates empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-empty strings', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects whitespace strings', () => {\n      const result = schema.safeParse(' ')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('wildcard pattern', () => {\n    const schema = regexp(/^.*$/)\n\n    it('validates empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates any string', () => {\n      const result = schema.safeParse('anything goes here 123 !@#')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings with special characters', () => {\n      const result = schema.safeParse('!@#$%^&*()')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates strings with unicode', () => {\n      const result = schema.safeParse('Hello 世界 🌍')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles pattern with escape sequences', () => {\n      const schema = regexp(/^\\d{3}\\.\\d{3}\\.\\d{3}\\.\\d{3}$/)\n      const result = schema.safeParse('192.168.001.001')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles pattern with special regex characters', () => {\n      const schema = regexp(/^\\$\\d+\\.\\d{2}$/)\n      const result = schema.safeParse('$99.99')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles very long strings', () => {\n      const schema = regexp(/^[a-z]+$/)\n      const longString = 'a'.repeat(10000)\n      const result = schema.safeParse(longString)\n      expect(result.success).toBe(true)\n    })\n\n    it('handles strings with newlines', () => {\n      const schema = regexp(/^hello\\nworld$/)\n      const result = schema.safeParse('hello\\nworld')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles strings with tabs', () => {\n      const schema = regexp(/^hello\\tworld$/)\n      const result = schema.safeParse('hello\\tworld')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles emoji patterns', () => {\n      const schema = regexp(/^[\\u{1F600}-\\u{1F64F}]+$/u)\n      const result = schema.safeParse('😀😃😄')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles global flag in pattern', () => {\n      const schema = regexp(/test/g)\n      const result = schema.safeParse('test')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles pattern matching anywhere in string', () => {\n      const schema = regexp(/test/)\n      const result = schema.safeParse('this is a test string')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles complex nested groups', () => {\n      const schema = regexp(/^((https?|ftp):\\/\\/)?([a-z0-9]+\\.)+[a-z]{2,}$/)\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/regexp.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\n\n/**\n * Schema for validating strings against a regular expression pattern.\n *\n * Validates that the input is a string and matches the provided pattern.\n * The pattern is tested using RegExp.test().\n *\n * @template TValue - The string type (can be narrowed with branded types)\n *\n * @example\n * ```ts\n * const schema = new RegexpSchema(/^[a-z]+$/)\n * schema.validate('hello') // success\n * schema.validate('Hello') // fails - uppercase not allowed\n * ```\n */\nexport class RegexpSchema<\n  TValue extends string = string,\n> extends Schema<TValue> {\n  readonly type = 'regexp' as const\n\n  constructor(public readonly pattern: RegExp) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (typeof input !== 'string') {\n      return ctx.issueUnexpectedType(input, 'string')\n    }\n\n    if (!this.pattern.test(input)) {\n      return ctx.issueInvalidFormat(input, this.pattern.toString())\n    }\n\n    return ctx.success(input as TValue)\n  }\n}\n\n/**\n * Creates a regexp schema that validates strings against a pattern.\n *\n * Useful for custom string formats not covered by the built-in format\n * validators.\n *\n * @param pattern - Regular expression pattern to match against\n * @returns A new {@link RegexpSchema} instance\n *\n * @example\n * ```ts\n * // Simple pattern\n * const slugSchema = l.regexp(/^[a-z0-9-]+$/)\n *\n * // With anchors for exact match\n * const uuidSchema = l.regexp(\n *   /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n * )\n *\n * // Semantic versioning\n * const semverSchema = l.regexp(/^\\d+\\.\\d+\\.\\d+(-[\\w.]+)?(\\+[\\w.]+)?$/)\n *\n * // Use in object\n * const configSchema = l.object({\n *   name: l.regexp(/^[a-z][a-z0-9-]*$/), // kebab-case identifier\n *   version: semverSchema,\n * })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function regexp<TInput extends string = string>(pattern: RegExp) {\n  return new RegexpSchema<TInput>(pattern)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/string.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from 'vitest'\nimport { Infer, UnknownString } from '../core.js'\nimport { StringSchemaOptions, string } from './string.js'\nimport { token } from './token.js'\nimport { withDefault } from './with-default.js'\n\ndescribe('StringSchema', () => {\n  describe('basic validation', () => {\n    const schema = string()\n\n    it('validates plain strings', () => {\n      const result = schema.safeParse('hello world')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects non-strings', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['hello'])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects plain objects', () => {\n      const result = schema.safeParse({ value: 'hello' })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('default values', () => {\n    it('uses default value when no input provided', () => {\n      const schema = withDefault(string(), 'default value')\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('default value')\n      }\n    })\n\n    it('validates default value against constraints', () => {\n      const schema = string({ default: 'hi', minLength: 5 })\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('minLength constraint', () => {\n    const schema = string({ minLength: 5 })\n\n    it('accepts strings meeting minimum length', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings exceeding minimum length', () => {\n      const result = schema.safeParse('hello world')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings below minimum length', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty strings when minLength is set', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('maxLength constraint', () => {\n    const schema = string({ maxLength: 10 })\n\n    it('accepts strings meeting maximum length', () => {\n      const result = schema.safeParse('1234567890')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings below maximum length', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings exceeding maximum length', () => {\n      const result = schema.safeParse('hello world!')\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts empty strings', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('correctly handles UTF-8 multi-byte characters', () => {\n      // Emoji takes 4 bytes in UTF-8\n      const schema = string({ maxLength: 4 })\n      const result = schema.safeParse('😀')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when multi-byte characters exceed maxLength', () => {\n      const schema = string({ maxLength: 3 })\n      const result = schema.safeParse('😀')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('combined min and max length', () => {\n    const schema = string({ minLength: 3, maxLength: 10 })\n\n    it('accepts strings within range', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings at minimum boundary', () => {\n      const result = schema.safeParse('abc')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings at maximum boundary', () => {\n      const result = schema.safeParse('1234567890')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings below minimum', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings above maximum', () => {\n      const result = schema.safeParse('hello world!')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('minGraphemes constraint', () => {\n    const schema = string({ minGraphemes: 3 })\n\n    it('accepts strings meeting minimum graphemes', () => {\n      const result = schema.safeParse('abc')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings exceeding minimum graphemes', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings below minimum graphemes', () => {\n      const result = schema.safeParse('ab')\n      expect(result.success).toBe(false)\n    })\n\n    it('counts emoji as single graphemes', () => {\n      const result = schema.safeParse('😀😀😀')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when emoji count is below minimum', () => {\n      const result = schema.safeParse('😀😀')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('maxGraphemes constraint', () => {\n    const schema = string({ maxGraphemes: 5 })\n\n    it('accepts strings meeting maximum graphemes', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings below maximum graphemes', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings exceeding maximum graphemes', () => {\n      const result = schema.safeParse('hello world')\n      expect(result.success).toBe(false)\n    })\n\n    it('counts emoji as single graphemes', () => {\n      const result = schema.safeParse('😀😀😀😀😀')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when emoji count exceeds maximum', () => {\n      const result = schema.safeParse('😀😀😀😀😀😀')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('combined grapheme constraints', () => {\n    const schema = string({ minGraphemes: 2, maxGraphemes: 5 })\n\n    it('accepts strings within grapheme range', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings at minimum boundary', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts strings at maximum boundary', () => {\n      const result = schema.safeParse('world')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects strings below minimum graphemes', () => {\n      const result = schema.safeParse('a')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects strings above maximum graphemes', () => {\n      const result = schema.safeParse('hello!')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: datetime', () => {\n    const schema = string({ format: 'datetime' })\n\n    it('accepts valid ISO datetime strings', () => {\n      const result = schema.safeParse('2023-12-25T12:00:00Z')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts datetime with milliseconds', () => {\n      const result = schema.safeParse('2023-12-25T12:00:00.123Z')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid datetime strings', () => {\n      const result = schema.safeParse('not a date')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid date format', () => {\n      const result = schema.safeParse('12/25/2023')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects datetime without timezone', () => {\n      const result = schema.safeParse('2023-12-25T12:00:00')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects date-only strings', () => {\n      // Date-only is not a valid datetime in either strict or loose mode\n      const result = schema.safeParse('2023-12-25')\n      expect(result.success).toBe(false)\n    })\n\n    describe('loose validation', () => {\n      it('accepts datetime without timezone', () => {\n        const result = schema.safeParse('2023-12-25T12:00:00', {\n          strict: false,\n        })\n        expect(result.success).toBe(true)\n      })\n\n      it('accepts datetime without separator', () => {\n        const result = schema.safeParse('20231225T120000', { strict: false })\n        expect(result.success).toBe(true)\n      })\n\n      it('rejects datetime with \"-00:00\" timezone offset', () => {\n        const result = schema.safeParse('2023-12-25T12:00:00-00:00', {\n          strict: false,\n        })\n        expect(result.success).toBe(false)\n      })\n\n      it('rejects date-only strings', () => {\n        const result = schema.safeParse('2023-12-25', { strict: false })\n        expect(result.success).toBe(false)\n      })\n\n      it('accepts datetime with timezone offset', () => {\n        const result = schema.safeParse('2023-12-25T12:00:00+05:30', {\n          strict: false,\n        })\n        expect(result.success).toBe(true)\n      })\n\n      it('still rejects completely invalid strings', () => {\n        expect(schema.safeParse('not a date', { strict: false }).success).toBe(\n          false,\n        )\n        expect(schema.safeParse('12/25/2023', { strict: false }).success).toBe(\n          false,\n        )\n        expect(schema.safeParse('', { strict: false }).success).toBe(false)\n      })\n\n      it('uses strict mode by default', () => {\n        // Datetime without timezone is not AT Protocol compliant\n        expect(schema.safeParse('2023-12-25T12:00:00').success).toBe(false)\n        // Date-only is not AT Protocol compliant\n        expect(schema.safeParse('2023-12-25').success).toBe(false)\n        // With timezone offset (AT Protocol compliant)\n        expect(schema.safeParse('2023-12-25T12:00:00+05:30').success).toBe(true)\n      })\n    })\n  })\n\n  describe('format: uri', () => {\n    const schema = string({ format: 'uri' })\n\n    it('accepts valid HTTP URIs', () => {\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts valid URIs with paths', () => {\n      const result = schema.safeParse('https://example.com/path/to/resource')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts URIs with different schemes', () => {\n      const result = schema.safeParse('ftp://files.example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid URIs', () => {\n      const result = schema.safeParse('not a uri')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects URIs without scheme', () => {\n      const result = schema.safeParse('example.com')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: at-uri', () => {\n    const schema = string({ format: 'at-uri' })\n\n    it('accepts valid AT URI', () => {\n      const result = schema.safeParse(\n        'at://did:plc:abc123/app.bsky.feed.post/xyz',\n      )\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid AT URI', () => {\n      const result = schema.safeParse('https://example.com')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects plain strings', () => {\n      const result = schema.safeParse('not an at-uri')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: did', () => {\n    const schema = string({ format: 'did' })\n\n    it('accepts valid DID with plc method', () => {\n      const result = schema.safeParse('did:plc:abc123')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts valid DID with web method', () => {\n      const result = schema.safeParse('did:web:example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid DID format', () => {\n      const result = schema.safeParse('not-a-did')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects DID without method', () => {\n      const result = schema.safeParse('did:')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: handle', () => {\n    const schema = string({ format: 'handle' })\n\n    it('accepts valid handle', () => {\n      const result = schema.safeParse('user.bsky.social')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts handle with subdomain', () => {\n      const result = schema.safeParse('alice.test.example.com')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid handle format', () => {\n      const result = schema.safeParse('invalid handle!')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects handle with spaces', () => {\n      const result = schema.safeParse('user name.bsky.social')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: at-identifier', () => {\n    const schema = string({ format: 'at-identifier' })\n\n    it('accepts valid DID as at-identifier', () => {\n      const result = schema.safeParse('did:plc:abc123')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts valid handle as at-identifier', () => {\n      const result = schema.safeParse('user.bsky.social')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid at-identifier', () => {\n      const result = schema.safeParse('invalid!')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: nsid', () => {\n    const schema = string({ format: 'nsid' })\n\n    it('accepts valid NSID', () => {\n      const result = schema.safeParse('app.bsky.feed.post')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts NSID with multiple segments', () => {\n      const result = schema.safeParse('com.example.app.feature.action')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid NSID format', () => {\n      const result = schema.safeParse('not-an-nsid')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects NSID with invalid characters', () => {\n      const result = schema.safeParse('app.bsky.feed!')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: cid', () => {\n    const schema = string({ format: 'cid' })\n\n    it('accepts valid CID v1', () => {\n      const result = schema.safeParse(\n        'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',\n      )\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid CID format', () => {\n      const result = schema.safeParse('not-a-cid')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects plain strings', () => {\n      const result = schema.safeParse('abc123')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: language', () => {\n    const schema = string({ format: 'language' })\n\n    it('accepts valid BCP 47 language code', () => {\n      const result = schema.safeParse('en')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts language code with region', () => {\n      const result = schema.safeParse('en-US')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts language code with script and region', () => {\n      const result = schema.safeParse('zh-Hans-CN')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid language code', () => {\n      const result = schema.safeParse('not valid')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: tid', () => {\n    const schema = string({ format: 'tid' })\n\n    it('accepts valid TID', () => {\n      const result = schema.safeParse('3jzfcijpj2z2a')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid TID format', () => {\n      const result = schema.safeParse('not-a-tid')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects TID with invalid characters', () => {\n      const result = schema.safeParse('3jzfcijpj2z2!')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('format: record-key', () => {\n    const schema = string({ format: 'record-key' })\n\n    it('accepts valid record key', () => {\n      const result = schema.safeParse('3jzfcijpj2z2a')\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts alphanumeric record key', () => {\n      const result = schema.safeParse('myRecordKey123')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects record key with invalid characters', () => {\n      const result = schema.safeParse('invalid/key')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects record key with spaces', () => {\n      const result = schema.safeParse('invalid key')\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('type coercion', () => {\n    const schema = string()\n\n    it('coerces Date objects to ISO strings', () => {\n      const date = new Date('2023-12-25T12:00:00Z')\n      const result = schema.safeParse(date)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('2023-12-25T12:00:00.000Z')\n      }\n    })\n\n    it('rejects invalid Date objects', () => {\n      const invalidDate = new Date('invalid')\n      const result = schema.safeParse(invalidDate)\n      expect(result.success).toBe(false)\n    })\n\n    it('coerces URL objects to strings', () => {\n      const url = new URL('https://example.com/path')\n      const result = schema.safeParse(url)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('https://example.com/path')\n      }\n    })\n\n    it('coerces String objects to primitive strings', () => {\n      const stringObj = new String('hello')\n      const result = schema.safeParse(stringObj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('coerces TokenSchema instances to strings', () => {\n      const result = schema.safeParse(token('my.to.ken', 'main'))\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('my.to.ken')\n      }\n    })\n  })\n\n  describe('combined constraints and format', () => {\n    const schema = string({\n      format: 'handle',\n      minLength: 5,\n      maxLength: 50,\n    })\n\n    it('validates both format and length constraints', () => {\n      const result = schema.safeParse('user.bsky.social')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when length is valid but format is invalid', () => {\n      const result = schema.safeParse('invalid handle!')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when format is valid but length is too short', () => {\n      const result = schema.safeParse('a.bc')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when format is valid but length is too long', () => {\n      const longHandle =\n        'very.long.subdomain.name.that.exceeds.maximum.length.example.com'\n      const result = schema.safeParse(longHandle)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles strings with special characters', () => {\n      const schema = string()\n      const result = schema.safeParse('hello\\nworld\\ttab')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles strings with unicode characters', () => {\n      const schema = string()\n      const result = schema.safeParse('Hello 世界 🌍')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles very long strings', () => {\n      const schema = string({ maxLength: 10000 })\n      const longString = 'a'.repeat(10000)\n      const result = schema.safeParse(longString)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects very long strings exceeding maxLength', () => {\n      const schema = string({ maxLength: 100 })\n      const longString = 'a'.repeat(101)\n      const result = schema.safeParse(longString)\n      expect(result.success).toBe(false)\n    })\n\n    it('handles zero as minLength', () => {\n      const schema = string({ minLength: 0 })\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('handles complex emoji sequences', () => {\n      const schema = string({ maxGraphemes: 5 })\n      // Family emoji is a single grapheme cluster\n      const result = schema.safeParse('👨‍👩‍👧‍👦')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('knownValues option', () => {\n    it('allows omitting knownValues at runtime', () => {\n      string<{ knownValues: ['active', 'inactive'] }>()\n\n      // @ts-expect-error format requires options to be set\n      string<{ knownValues: ['active', 'inactive']; format: 'did' }>()\n\n      // @ts-expect-error any options, besides knownValues, must be provided\n      string<{ knownValues: ['active', 'inactive']; minLength: 5 }>()\n\n      string<{\n        knownValues: ['john.doe', 'someone.else']\n        format: 'handle'\n      }>({\n        format: 'handle',\n      })\n\n      string<{\n        knownValues: ['john.doe', 'someone.else']\n      }>({\n        // Being *more* precise than the generic if fine\n        format: 'handle',\n      })\n\n      string<{\n        knownValues: ['did', 'inactive']\n        format: 'did'\n      }>({\n        // @ts-expect-error does not match format form generic constraint\n        format: 'handle',\n      })\n\n      string<{\n        knownValues: ['active', 'inactive']\n        minLength: 10\n      }>({\n        minLength: 10,\n      })\n\n      string<{\n        knownValues: ['active', 'inactive']\n        minLength: 5\n      }>({\n        // @ts-expect-error mismatch\n        minLength: 10,\n      })\n    })\n  })\n\n  it('properly types knownValues in parameters', () => {\n    const schema = string({\n      knownValues: ['active', 'inactive'],\n    })\n    type SchemaType = Infer<typeof schema>\n    expectTypeOf<{\n      foo: SchemaType\n    }>().toMatchObjectType<{\n      foo: 'active' | 'inactive' | UnknownString\n    }>()\n    expectTypeOf<{\n      foo: SchemaType\n    }>().not.toMatchObjectType<{\n      foo: string\n    }>()\n    expectTypeOf<{\n      foo: SchemaType\n    }>().not.toMatchObjectType<{\n      foo: 'active' | 'inactive'\n    }>()\n    expectTypeOf<{\n      foo: SchemaType\n    }>().not.toMatchObjectType<{\n      foo: UnknownString\n    }>()\n  })\n\n  it('type string<any>() as string', () => {\n    const schema = string<any>()\n    type SchemaType = Infer<typeof schema>\n    expectTypeOf<{\n      foo: SchemaType\n    }>().toMatchObjectType<{\n      foo: string\n    }>()\n  })\n\n  it('type string<StringSchemaOptions>({}) as string', () => {\n    const schema = string<StringSchemaOptions>({})\n    type SchemaType = Infer<typeof schema>\n    expectTypeOf<{\n      foo: SchemaType\n    }>().toMatchObjectType<{\n      foo: string\n    }>()\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/string.ts",
    "content": "import { graphemeLen, ifCid, utf8Len } from '@atproto/lex-data'\nimport {\n  InferStringFormat,\n  Restricted,\n  Schema,\n  StringFormat,\n  UnknownString,\n  ValidationContext,\n  isStringFormat,\n} from '../core.js'\nimport { IfAny } from '../util/if-any.js'\nimport { memoizedOptions } from '../util/memoize.js'\nimport { TokenSchema } from './token.js'\n\n/**\n * Configuration options for string schema validation.\n *\n * @property format - Expected string format (e.g., 'datetime', 'uri', 'at-uri', 'did', 'handle', 'nsid', 'cid', 'tid', 'record-key', 'at-identifier', 'language')\n * @property knownValues - Known string literal values for type narrowing\n * @property minLength - Minimum length in UTF-8 bytes\n * @property maxLength - Maximum length in UTF-8 bytes\n * @property minGraphemes - Minimum number of grapheme clusters\n * @property maxGraphemes - Maximum number of grapheme clusters\n */\nexport type StringSchemaOptions = {\n  format?: StringFormat\n  knownValues?: readonly string[]\n  minLength?: number\n  maxLength?: number\n  minGraphemes?: number\n  maxGraphemes?: number\n}\n\n/**\n * Schema for validating string values with optional format and length constraints.\n *\n * Supports various string formats defined in the Lexicon specification, as well as\n * length constraints measured in UTF-8 bytes or grapheme clusters.\n *\n * @template TOptions - The configuration options type\n *\n * @example\n * ```ts\n * const schema = new StringSchema({ format: 'datetime', maxLength: 64 })\n * const result = schema.validate('2024-01-15T10:30:00Z')\n * ```\n */\nexport class StringSchema<\n  const TOptions extends StringSchemaOptions = StringSchemaOptions,\n> extends Schema<\n  IfAny<\n    TOptions,\n    string,\n    TOptions extends { format: infer F extends StringFormat }\n      ? InferStringFormat<F>\n      : TOptions extends { knownValues: readonly (infer V extends string)[] }\n        ? V | UnknownString\n        : string\n  >\n> {\n  readonly type = 'string' as const\n\n  // @NOTE since the _string utility allows omitting knownValues when TOptions\n  // *does* include it (since it's only used for typing), we cannot type options\n  // as TOptions directly since it may not actually include knownValues at\n  // runtime, making schema.options.knownValues potentially undefined even when\n  // TOptions includes it.\n  readonly options: StringSchemaOptions\n\n  constructor(options: TOptions) {\n    super()\n    this.options = options\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    const str = coerceToString(input)\n    if (str == null) {\n      return ctx.issueUnexpectedType(input, 'string')\n    }\n\n    let lazyUtf8Len: number\n\n    const minLength = this.options.minLength\n    if (minLength != null) {\n      if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {\n        return ctx.issueTooSmall(str, 'string', minLength, lazyUtf8Len)\n      }\n    }\n\n    const maxLength = this.options.maxLength\n    if (maxLength != null) {\n      // Optimization: we can avoid computing the UTF-8 length if the maximum\n      // possible length, in bytes, of the input JS string is smaller than the\n      // maxLength (in UTF-8 string bytes).\n      if (str.length * 3 <= maxLength) {\n        // Input string so small it can't possibly exceed maxLength\n      } else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) {\n        return ctx.issueTooBig(str, 'string', maxLength, lazyUtf8Len)\n      }\n    }\n\n    let lazyGraphLen: number\n\n    const minGraphemes = this.options.minGraphemes\n    if (minGraphemes != null) {\n      // Optimization: avoid counting graphemes if the length check already fails\n      if (str.length < minGraphemes) {\n        return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)\n      } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {\n        return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)\n      }\n    }\n\n    const maxGraphemes = this.options.maxGraphemes\n    if (maxGraphemes != null) {\n      if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {\n        return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)\n      }\n    }\n\n    const format = this.options.format\n    if (format != null && !isStringFormat(str, format, ctx.options)) {\n      return ctx.issueInvalidFormat(str, format)\n    }\n\n    return ctx.success(str)\n  }\n}\n\nexport function coerceToString(input: unknown): string | null {\n  switch (typeof input) {\n    // @NOTE We do *not* coerce numbers/booleans to strings because that can\n    // lead to them being accepted as string instead of being coerced to\n    // number/boolean when the input is a string and the expected result is\n    // number/boolean (e.g. in params).\n    case 'string':\n      return input\n    case 'object': {\n      if (input == null) return null\n\n      // @NOTE Allow using TokenSchema instances in places expecting strings,\n      // converting them to their string value.\n      if (input instanceof TokenSchema) {\n        return input.toString()\n      }\n\n      if (input instanceof Date) {\n        if (Number.isNaN(input.getTime())) return null\n        return input.toISOString()\n      }\n\n      if (input instanceof URL) {\n        return input.toString()\n      }\n\n      const cid = ifCid(input)\n      if (cid) return cid.toString()\n\n      if (input instanceof String) {\n        return input.valueOf()\n      }\n    }\n\n    // falls through\n    default:\n      return null\n  }\n}\n\nfunction _string(): StringSchema<NonNullable<unknown>>\nfunction _string<\n  // Allow calling `string<{ knownValues: [...] }>()` without passing an options\n  // object, since knownValues is only used for typing and has no runtime\n  // effect, so it can be safely omitted at runtime.\n  const TOptions extends {\n    knownValues: StringSchemaOptions['knownValues']\n  } & {\n    [K in Exclude<\n      keyof StringSchemaOptions,\n      'knownValues'\n    >]?: Restricted<`An options argument is required when using the \"${K}\" option`>\n  },\n>(): StringSchema<\n  IfAny<TOptions, any, { knownValues: TOptions['knownValues'] }>\n>\nfunction _string<const TOptions extends StringSchemaOptions>(\n  // If TOptions is explicitly provided (e.g. `string<{ ... }>({ ... })`), we\n  // allow the actual options argument to omit the \"knownValues\" property since\n  // it's only used for inferring the type and has no runtime effect.\n  options: TOptions | Omit<TOptions, 'knownValues'>,\n): StringSchema<TOptions>\nfunction _string(options: StringSchemaOptions = {}) {\n  return new StringSchema(options)\n}\n\n/**\n * Creates a string schema with optional format and length constraints.\n *\n * Strings can be validated against various formats (datetime, uri, did, handle, etc.)\n * and constrained by length in UTF-8 bytes or grapheme clusters.\n *\n * @param options - Optional configuration for format and length constraints\n * @returns A new {@link StringSchema} instance\n *\n * @example\n * ```ts\n * // Basic string\n * const nameSchema = l.string()\n *\n * // With format validation\n * const dateSchema = l.string({ format: 'datetime' })\n *\n * // With length constraints (UTF-8 bytes)\n * const bioSchema = l.string({ maxLength: 256 })\n *\n * // With grapheme constraints (user-perceived characters)\n * const displayNameSchema = l.string({ maxGraphemes: 64 })\n *\n * // Combining constraints\n * const handleSchema = l.string({ format: 'handle', minLength: 3, maxLength: 253 })\n * ```\n */\nexport const string = /*#__PURE__*/ memoizedOptions(_string)\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/subscription.test.ts",
    "content": "import { describe, expect, expectTypeOf, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { ObjectSchema, object } from './object.js'\nimport { optional } from './optional.js'\nimport { ParamsSchema, params } from './params.js'\nimport { RefSchema, ref } from './ref.js'\nimport { string } from './string.js'\nimport {\n  InferSubscriptionMessage,\n  InferSubscriptionParameters,\n  Subscription,\n  subscription,\n} from './subscription.js'\n\ndescribe('Subscription', () => {\n  describe('constructor', () => {\n    it('creates a Subscription instance with all parameters', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params({\n        cursor: optional(integer()),\n      })\n      const message = object({\n        seq: integer(),\n        data: string(),\n      })\n      const errors = ['ConsumerTooSlow', 'FutureCursor'] as const\n\n      const mySub = subscription(nsid, parameters, message, errors)\n\n      expect(mySub).toBeInstanceOf(Subscription)\n      expect(mySub.nsid).toBe(nsid)\n      expect(mySub.parameters).toBe(parameters)\n      expect(mySub.message).toBe(message)\n      expect(mySub.errors).toBe(errors)\n    })\n\n    it('creates a Subscription instance without errors', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params({\n        cursor: optional(integer()),\n      })\n      const message = object({\n        seq: integer(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub).toBeInstanceOf(Subscription)\n      expect(mySub.nsid).toBe(nsid)\n      expect(mySub.parameters).toBe(parameters)\n      expect(mySub.message).toBe(message)\n      expect(mySub.errors).toBeUndefined()\n    })\n\n    it('creates a Subscription instance with empty parameters', () => {\n      const nsid = 'app.bsky.notification.subscribe'\n      const parameters = params()\n      const message = object({\n        type: string(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub).toBeInstanceOf(Subscription)\n      expect(mySub.parameters).toBe(parameters)\n    })\n  })\n\n  describe('type property', () => {\n    it('has type set to \"subscription\"', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.type).toBe('subscription')\n    })\n\n    it('type is a constant value', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.type).toBe('subscription')\n      // TypeScript enforces readonly at compile time\n      expect(typeof mySub.type).toBe('string')\n    })\n  })\n\n  describe('properties', () => {\n    it('has nsid property', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.nsid).toBe(nsid)\n      expect(typeof mySub.nsid).toBe('string')\n    })\n\n    it('has parameters property', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.parameters).toBe(parameters)\n      expect(mySub.parameters).toBeInstanceOf(ParamsSchema)\n    })\n\n    it('has message property', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.message).toBe(message)\n      expect(mySub.message).toBeInstanceOf(ObjectSchema)\n    })\n\n    it('has errors property', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n      })\n      const errors = ['ConsumerTooSlow'] as const\n\n      const mySub = subscription(nsid, parameters, message, errors)\n\n      expect(mySub.errors).toBe(errors)\n      expect(Array.isArray(mySub.errors)).toBe(true)\n    })\n  })\n\n  describe('with complex parameters', () => {\n    it('creates a Subscription with multiple parameter types', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params({\n        cursor: optional(integer({ minimum: 0 })),\n        includeDeletes: optional(integer({ minimum: 0, maximum: 1 })),\n      })\n      const message = object({\n        seq: integer(),\n        data: string(),\n      })\n      const errors = ['ConsumerTooSlow', 'FutureCursor'] as const\n\n      const mySub = subscription(nsid, parameters, message, errors)\n\n      expect(mySub).toBeInstanceOf(Subscription)\n      expect(mySub.parameters).toBe(parameters)\n      expect(mySub.errors).toEqual(['ConsumerTooSlow', 'FutureCursor'])\n    })\n  })\n\n  describe('with various message types', () => {\n    it('creates a Subscription with ObjectSchema message', () => {\n      const mySub = subscription(\n        'com.atproto.sync.subscribeRepos',\n        params(),\n        object({\n          seq: integer(),\n          data: string(),\n        }),\n        undefined,\n      )\n\n      expect(mySub).toBeInstanceOf(Subscription)\n      expect(mySub.message).toBeInstanceOf(ObjectSchema)\n    })\n\n    it('creates a Subscription with RefSchema message', () => {\n      const message = string()\n      const mySub = subscription(\n        'app.bsky.feed.subscribe',\n        params(),\n        ref(() => message),\n        undefined,\n      )\n\n      expect(mySub).toBeInstanceOf(Subscription)\n      expect(mySub.message).toBeInstanceOf(RefSchema)\n    })\n  })\n\n  describe('with different error configurations', () => {\n    it('creates a Subscription with a single error', () => {\n      const mySub = subscription(\n        'com.atproto.sync.subscribeRepos',\n        params(),\n        object({}),\n        ['ConsumerTooSlow'],\n      )\n\n      expect(mySub.errors).toEqual(['ConsumerTooSlow'])\n    })\n\n    it('creates a Subscription with multiple errors', () => {\n      const mySub = subscription(\n        'com.atproto.sync.subscribeRepos',\n        params(),\n        object({}),\n        ['ConsumerTooSlow', 'FutureCursor', 'InvalidCursor'],\n      )\n\n      expectTypeOf<\n        readonly ['ConsumerTooSlow', 'FutureCursor', 'InvalidCursor']\n      >(mySub.errors)\n\n      expect(mySub.errors).toEqual([\n        'ConsumerTooSlow',\n        'FutureCursor',\n        'InvalidCursor',\n      ])\n    })\n\n    it('creates a Subscription with empty errors array', () => {\n      const mySub = subscription(\n        'com.atproto.sync.subscribeRepos',\n        params(),\n        object({}),\n        [],\n      )\n\n      expect(mySub.errors).toEqual([])\n    })\n  })\n\n  describe('type inference', () => {\n    it('InferSubscriptionParameters correctly infers parameter types', () => {\n      const mySub = subscription(\n        'com.atproto.sync.subscribeRepos',\n        params({\n          cursor: optional(integer()),\n        }),\n        object({\n          seq: integer(),\n        }),\n      )\n\n      type Params = InferSubscriptionParameters<typeof mySub>\n\n      expectTypeOf<Params>({\n        cursor: 12345,\n      })\n    })\n\n    it('InferSubscriptionMessage correctly infers message type', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n        data: string(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      type Message = InferSubscriptionMessage<typeof mySub>\n\n      expectTypeOf<Message>({\n        seq: 12345,\n        data: 'test data',\n      })\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles very long NSID', () => {\n      const nsid =\n        'com.example.very.long.namespace.identifier.subscription.name'\n      const parameters = params()\n      const message = object({})\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.nsid).toBe(nsid)\n    })\n\n    it('handles subscription with all optional parameters', () => {\n      const nsid = 'app.bsky.feed.subscribe'\n      const parameters = params({\n        cursor: optional(integer()),\n        includeDeletes: optional(integer()),\n        includeEdits: optional(integer()),\n      })\n      const message = object({})\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.parameters).toBe(parameters)\n    })\n\n    it('handles subscription with complex nested message schema', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer(),\n        blocks: object({\n          cid: string({ format: 'cid' }),\n          data: object({\n            uri: string({ format: 'at-uri' }),\n            content: string(),\n          }),\n        }),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.message).toBeInstanceOf(ObjectSchema)\n    })\n  })\n\n  describe('real-world subscription examples', () => {\n    it('creates a subscribeLabels subscription', () => {\n      const nsid = 'com.atproto.label.subscribeLabels'\n      const parameters = params({\n        cursor: optional(integer({ minimum: 0 })),\n      })\n      const message = object({\n        seq: integer(),\n        labels: object({\n          uri: string({ format: 'uri' }),\n          val: string(),\n        }),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.type).toBe('subscription')\n      expect(mySub.nsid).toBe('com.atproto.label.subscribeLabels')\n      expect(mySub.parameters.matches({ cursor: 10 })).toBe(true)\n      expect(mySub.parameters.matches({ cursor: -10 })).toBe(false)\n      expect(\n        mySub.message.matches({\n          seq: 1,\n          labels: { uri: 'http://foo.com', val: 'test' },\n        }),\n      ).toBe(true)\n      expect(\n        mySub.message.matches({\n          seq: 1,\n          labels: { uri: 'http://foo.com', val: 3 },\n        }),\n      ).toBe(false)\n    })\n\n    it('creates a notification subscription', () => {\n      const nsid = 'app.bsky.notification.subscribe'\n      const parameters = params({\n        cursor: optional(string()),\n      })\n      const message = object({\n        type: string(),\n        uri: string({ format: 'at-uri' }),\n        cid: string({ format: 'cid' }),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.type).toBe('subscription')\n      expect(mySub.nsid).toBe('app.bsky.notification.subscribe')\n    })\n  })\n\n  describe('with mixed parameter and message types', () => {\n    it('handles required and optional parameters with complex message', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params({\n        cursor: optional(integer({ minimum: 0 })),\n        includeDeletes: optional(integer({ minimum: 0, maximum: 1 })),\n      })\n      const message = object({\n        seq: integer(),\n        rebase: optional(integer({ minimum: 0, maximum: 1 })),\n        tooBig: optional(integer({ minimum: 0, maximum: 1 })),\n        repo: string({ format: 'did' }),\n        commit: string({ format: 'cid' }),\n        blocks: string(),\n        ops: object({}),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.type).toBe('subscription')\n      expect(mySub.parameters).toBe(parameters)\n      expect(mySub.message).toBe(message)\n    })\n  })\n\n  describe('validation through nested schemas', () => {\n    it('parameters can validate input through ParamsSchema', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params({\n        cursor: integer({ minimum: 0 }),\n      })\n      const message = object({})\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      // Test that the parameters schema can validate\n      const validResult = mySub.parameters.safeParse({ cursor: 100 })\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = mySub.parameters.safeParse({ cursor: -1 })\n      expect(invalidResult.success).toBe(false)\n    })\n\n    it('message can validate input through ObjectSchema', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({\n        seq: integer({ minimum: 0 }),\n        data: string({ minLength: 1 }),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      // Test that the message schema can validate\n      const validResult = mySub.message.safeParse({\n        seq: 100,\n        data: 'test',\n      })\n      expect(validResult.success).toBe(true)\n\n      const invalidResult = mySub.message.safeParse({\n        seq: -1,\n        data: '',\n      })\n      expect(invalidResult.success).toBe(false)\n    })\n  })\n\n  describe('property access', () => {\n    it('can access nsid after construction', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({})\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.nsid).toBe(nsid)\n      expect(mySub.nsid).toBe('com.atproto.sync.subscribeRepos')\n    })\n\n    it('can access type after construction', () => {\n      const nsid = 'com.atproto.sync.subscribeRepos'\n      const parameters = params()\n      const message = object({})\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.type).toBe('subscription')\n      expect(typeof mySub.type).toBe('string')\n    })\n  })\n\n  describe('different message schema types', () => {\n    it('constructs with ObjectSchema message', () => {\n      const nsid = 'app.bsky.test'\n      const parameters = params()\n      const message = object({\n        field: string(),\n      })\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.message).toBeInstanceOf(ObjectSchema)\n      expect(mySub.message).toBe(message)\n    })\n\n    it('constructs with RefSchema message', () => {\n      const nsid = 'app.bsky.test'\n      const parameters = params()\n      const message = ref(() => object({}))\n\n      const mySub = subscription(nsid, parameters, message, undefined)\n\n      expect(mySub.message).toBeInstanceOf(RefSchema)\n      expect(mySub.message).toBe(message)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/subscription.ts",
    "content": "import { LexValue } from '@atproto/lex-data'\nimport { Infer, NsidString, Schema } from '../core.js'\nimport { ParamsSchema } from './params.js'\n\n/**\n * Infers the parameters type from a Subscription definition.\n *\n * @template S - The Subscription type\n */\nexport type InferSubscriptionParameters<S extends Subscription> = Infer<\n  S['parameters']\n>\n\n/**\n * Infers the message type from a Subscription definition.\n *\n * @template S - The Subscription type\n */\nexport type InferSubscriptionMessage<S extends Subscription> = Infer<\n  S['message']\n>\n\n/**\n * Represents a Lexicon subscription (WebSocket) endpoint definition.\n *\n * Subscriptions are real-time event streams delivered over WebSocket.\n * They have parameters for initializing the connection and a message\n * schema for validating incoming events.\n *\n * @template TNsid - The NSID identifying this subscription\n * @template TParameters - The connection parameters schema type\n * @template TMessage - The message schema type\n * @template TErrors - Array of error type strings, or undefined\n *\n * @example\n * ```ts\n * const firehose = new Subscription(\n *   'com.atproto.sync.subscribeRepos',\n *   l.params({ cursor: l.optional(l.integer()) }),\n *   repoEventSchema,\n *   ['FutureCursor']\n * )\n * ```\n */\nexport class Subscription<\n  const TNsid extends NsidString = NsidString,\n  const TParameters extends ParamsSchema = ParamsSchema,\n  const TMessage extends Schema<LexValue> = Schema<LexValue>,\n  const TErrors extends undefined | readonly string[] =\n    | undefined\n    | readonly string[],\n> {\n  readonly type = 'subscription' as const\n\n  constructor(\n    readonly nsid: TNsid,\n    readonly parameters: TParameters,\n    readonly message: TMessage,\n    readonly errors: TErrors,\n  ) {}\n}\n\n/**\n * Creates a subscription definition for a Lexicon WebSocket endpoint.\n *\n * Subscriptions enable real-time event streaming. The connection is\n * initialized with parameters, and the server sends messages matching\n * the message schema.\n *\n * @param nsid - The NSID identifying this subscription endpoint\n * @param parameters - Schema for connection parameters\n * @param message - Schema for validating incoming messages\n * @param errors - Optional array of error type strings\n * @returns A new {@link Subscription} instance\n *\n * @example\n * ```ts\n * // Repository event stream\n * const subscribeRepos = l.subscription(\n *   'com.atproto.sync.subscribeRepos',\n *   l.params({\n *     cursor: l.optional(l.integer()),\n *   }),\n *   l.typedUnion([\n *     l.typedRef(() => commitEventSchema),\n *     l.typedRef(() => handleEventSchema),\n *     l.typedRef(() => identityEventSchema),\n *   ], false),\n *   ['FutureCursor', 'ConsumerTooSlow'],\n * )\n *\n * // Label stream\n * const subscribeLabels = l.subscription(\n *   'com.atproto.label.subscribeLabels',\n *   l.params({ cursor: l.optional(l.integer()) }),\n *   labelEventSchema,\n * )\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function subscription<\n  const N extends NsidString,\n  const P extends ParamsSchema,\n  const M extends Schema<LexValue>,\n  const E extends undefined | readonly string[] = undefined,\n>(nsid: N, parameters: P, message: M, errors: E = undefined as E) {\n  return new Subscription<N, P, M, E>(nsid, parameters, message, errors)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/token.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { token } from './token.js'\n\ndescribe('TokenSchema', () => {\n  describe('basic validation', () => {\n    const schema = token('my.to.ken')\n\n    it('validates exact token match', () => {\n      expect(schema.safeParse('my.to.ken')).toMatchObject({\n        success: true,\n        value: 'my.to.ken',\n      })\n    })\n\n    it('rejects different strings', () => {\n      const result = schema.safeParse('other.to.ken')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects similar strings with different case', () => {\n      const result = schema.safeParse('My.To.Ken')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects partial matches', () => {\n      const result = schema.safeParse('my.to')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects symbols', () => {\n      const result = schema.safeParse(Symbol.for(schema.toString()))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects functions', () => {\n      const result = schema.safeParse(() => schema)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects dates', () => {\n      const result = schema.safeParse(new Date())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects regular expressions', () => {\n      const result = schema.safeParse(/mytoken/)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('TokenSchema instance validation', () => {\n    const schema = token('my.to.ken')\n\n    it('accepts TokenSchema instance with same value', () => {\n      const otherInstance = token('my.to.ken')\n      const result = schema.safeParse(otherInstance)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('my.to.ken')\n      }\n    })\n\n    it('rejects TokenSchema instance with different value', () => {\n      const otherInstance = token('other.to.ken')\n      const result = schema.safeParse(otherInstance)\n      expect(result.success).toBe(false)\n    })\n\n    it('accepts itself as input', () => {\n      expect(schema.safeParse(schema)).toMatchObject({\n        success: true,\n        value: 'my.to.ken',\n      })\n    })\n  })\n\n  describe('type validation', () => {\n    const schema = token('my.to.ken')\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects objects', () => {\n      const result = schema.safeParse({ token: 'mytoken' })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['mytoken'])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects String objects', () => {\n      const result = schema.safeParse(new String('mytoken'))\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('serialization methods', () => {\n    const schema = token('my.to.ken', 'foo')\n\n    it('toJSON returns the token value', () => {\n      expect(schema.toJSON()).toBe('my.to.ken#foo')\n    })\n\n    it('toString returns the token value', () => {\n      expect(schema.toString()).toBe('my.to.ken#foo')\n    })\n\n    it('toJSON and toString return the same value', () => {\n      expect(schema.toJSON()).toBe(schema.toString())\n    })\n\n    it('serializes to primitive string in JSON', () => {\n      expect(JSON.stringify({ token: schema })).toBe(\n        '{\"token\":\"my.to.ken#foo\"}',\n      )\n    })\n  })\n\n  describe('value property', () => {\n    const schema = token('my.to.ken')\n\n    it('exposes the token value through serialization', () => {\n      // value is protected, so we verify it through toString/toJSON\n      expect(schema.toString()).toBe('my.to.ken')\n      expect(schema.toJSON()).toBe('my.to.ken')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/token.ts",
    "content": "import { $type, NsidString, Schema, ValidationContext } from '../core.js'\n\n/**\n * Schema for Lexicon token values.\n *\n * Tokens are named constants in Lexicon, identified by their NSID and hash.\n * They validate to their string value (e.g., 'app.bsky.feed.defs#requestLess').\n * TokenSchema instances can also be used as values themselves.\n *\n * @template TValue - The token string literal type\n *\n * @example\n * ```ts\n * const schema = new TokenSchema('app.bsky.feed.defs#requestLess')\n * schema.validate('app.bsky.feed.defs#requestLess') // success\n * ```\n */\nexport class TokenSchema<\n  const TValue extends string = string,\n> extends Schema<TValue> {\n  readonly type = 'token' as const\n\n  constructor(readonly value: TValue) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (input === this.value) {\n      return ctx.success(this.value)\n    }\n\n    // @NOTE: allow using the token instance itself (but convert to the actual\n    // token value)\n    if (input instanceof TokenSchema && input.value === this.value) {\n      return ctx.success(this.value)\n    }\n\n    if (typeof input !== 'string') {\n      return ctx.issueUnexpectedType(input, 'token')\n    }\n\n    return ctx.issueInvalidValue(input, [this.value])\n  }\n\n  // When using the TokenSchema instance as data, let's serialize it to the\n  // token value\n\n  toJSON(): string {\n    return this.value\n  }\n\n  toString(): string {\n    return this.value\n  }\n}\n\n/**\n * Creates a token schema for Lexicon named constants.\n *\n * Tokens are used in Lexicon as named constants or enum-like values.\n * The token instance can be used both as a schema validator and as\n * the token value itself (it serializes to its string value).\n *\n * @param nsid - The NSID part of the token\n * @param hash - The hash part of the token (defaults to 'main')\n * @returns A new {@link TokenSchema} instance\n *\n * @example\n * ```ts\n * // Define tokens\n * const requestLess = l.token('app.bsky.feed.defs', 'requestLess')\n * const requestMore = l.token('app.bsky.feed.defs', 'requestMore')\n *\n * // Use as a value\n * console.log(requestLess.toString()) // 'app.bsky.feed.defs#requestLess'\n *\n * // Use in union for validation\n * const feedbackSchema = l.union([requestLess, requestMore])\n *\n * // Validate\n * feedbackSchema.parse('app.bsky.feed.defs#requestLess') // success\n *\n * // Token instances can be used as values in other schemas\n * const feedbackRequest = l.object({\n *   feedback: requestLess, // Accepts the token value\n * })\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function token<\n  const N extends NsidString,\n  const H extends string = 'main',\n>(nsid: N, hash: H = 'main' as H) {\n  return new TokenSchema($type(nsid, hash))\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/typed-object.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { Infer, Unknown$Type, Unknown$TypedObject } from '../core.js'\nimport { enumSchema } from './enum.js'\nimport { integer } from './integer.js'\nimport { nullable } from './nullable.js'\nimport { object } from './object.js'\nimport { optional } from './optional.js'\nimport { string } from './string.js'\nimport { typedObject } from './typed-object.js'\n\ndescribe('TypedObjectSchema', () => {\n  const schema = typedObject(\n    'app.bsky.feed.post',\n    'main',\n    object({\n      text: string(),\n      likes: optional(integer()),\n    }),\n  )\n  type Schema = Infer<typeof schema>\n\n  describe('basic validation', () => {\n    it('validates plain objects without $type', () => {\n      const result = schema.safeParse({\n        text: 'Hello world',\n        likes: 5,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates plain objects with matching $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n        likes: 5,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects objects with non-matching $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.like',\n        text: 'Hello world',\n        likes: 5,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-objects', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects arrays', () => {\n      const result = schema.safeParse(['text', 5])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numbers', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('property validation', () => {\n    it('rejects missing required properties', () => {\n      const result = schema.safeParse({\n        likes: 5,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('validates optional properties', () => {\n      const result = schema.safeParse({\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid property types', () => {\n      const result = schema.safeParse({\n        text: 'Hello world',\n        likes: 'five',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid required property types', () => {\n      const result = schema.safeParse({\n        text: 123,\n        likes: 5,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('ignores extra properties', () => {\n      const result = schema.safeParse({\n        text: 'Hello world',\n        likes: 5,\n        extra: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('$type validation', () => {\n    it('treats undefined $type as valid', () => {\n      const result = schema.safeParse({\n        $type: undefined,\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects empty string $type', () => {\n      const result = schema.safeParse({\n        $type: '',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects numeric $type', () => {\n      const result = schema.safeParse({\n        $type: 123,\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object $type', () => {\n      const result = schema.safeParse({\n        $type: { type: 'app.bsky.feed.post' },\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-normalized $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post#main',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects $type with extra characters', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post#main-extra',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects case-mismatched $type', () => {\n      const result = schema.safeParse({\n        $type: 'APP.BSKY.FEED.POST#MAIN',\n        text: 'Hello world',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('isTypeOf method', () => {\n    it('returns true for objects without $type', () => {\n      const obj = { text: 'Hello' }\n      expect(schema.isTypeOf(obj)).toBe(true)\n    })\n\n    it('returns true for objects with undefined $type', () => {\n      const obj = { $type: undefined, text: 'Hello' }\n      expect(schema.isTypeOf(obj)).toBe(true)\n    })\n\n    it('returns true for objects with matching $type', () => {\n      const obj = { $type: 'app.bsky.feed.post', text: 'Hello' }\n      expect(schema.isTypeOf(obj)).toBe(true)\n    })\n\n    it('returns false for objects with non-matching $type', () => {\n      const obj = { $type: 'app.bsky.feed.like', text: 'Hello' }\n      expect(schema.isTypeOf(obj)).toBe(false)\n    })\n\n    it('returns false for objects with empty $type', () => {\n      const obj = { $type: '', text: 'Hello' }\n      expect(schema.isTypeOf(obj)).toBe(false)\n    })\n\n    it('returns false for objects with numeric $type', () => {\n      const obj = { $type: 123, text: 'Hello' }\n      expect(schema.isTypeOf(obj)).toBe(false)\n    })\n\n    it('properly discriminates Unknown$TypeObject', () => {\n      function foo(value: Unknown$TypedObject | Schema) {\n        if (schema.isTypeOf(value)) {\n          value.text\n        } else {\n          // @ts-expect-error\n          value.text\n        }\n      }\n\n      foo({\n        $type: 'app.bsky.feed.post',\n        text: 'aze',\n        // @ts-expect-error\n        unknownProperty: 'should not be allowed !',\n      })\n\n      foo({\n        $type: 'blah' as Unknown$Type,\n        // @ts-expect-error\n        unknownProperty: 'should not be allowed !',\n      })\n    })\n  })\n\n  describe('$isTypeOf method', () => {\n    it('returns true for objects without $type', () => {\n      const obj = { text: 'Hello' }\n      expect(schema.$isTypeOf(obj)).toBe(true)\n    })\n\n    it('returns true for objects with matching $type', () => {\n      const obj = { $type: 'app.bsky.feed.post', text: 'Hello' }\n      expect(schema.$isTypeOf(obj)).toBe(true)\n    })\n\n    it('returns false for objects with non-matching $type', () => {\n      const obj = { $type: 'app.bsky.feed.like', text: 'Hello' }\n      expect(schema.$isTypeOf(obj)).toBe(false)\n    })\n\n    it('behaves identically to isTypeOf', () => {\n      const obj1 = { text: 'Hello' }\n      const obj2 = { $type: 'app.bsky.feed.post', text: 'Hello' }\n      const obj3 = { $type: 'app.bsky.feed.like', text: 'Hello' }\n\n      expect(schema.$isTypeOf(obj1)).toBe(schema.isTypeOf(obj1))\n      expect(schema.$isTypeOf(obj2)).toBe(schema.isTypeOf(obj2))\n      expect(schema.$isTypeOf(obj3)).toBe(schema.isTypeOf(obj3))\n    })\n  })\n\n  describe('build method', () => {\n    it('adds $type to object without $type', () => {\n      const input = { text: 'Hello world', likes: 5 }\n      const result = schema.build(input)\n      expect(result).toEqual({\n        text: 'Hello world',\n        likes: 5,\n        $type: 'app.bsky.feed.post',\n      })\n    })\n\n    it('adds $type to object with only required properties', () => {\n      const input = { text: 'Hello world' }\n      const result = schema.build(input)\n      expect(result).toEqual({\n        text: 'Hello world',\n        $type: 'app.bsky.feed.post',\n      })\n    })\n\n    it('preserves existing properties', () => {\n      const input = { text: 'Hello', likes: 10, extra: 'value' } as any\n      const result = schema.build(input)\n      expect(result).toEqual({\n        text: 'Hello',\n        likes: 10,\n        extra: 'value',\n        $type: 'app.bsky.feed.post',\n      })\n    })\n\n    it('does not mutate the input object', () => {\n      const input = { text: 'Hello world', likes: 5 }\n      const inputCopy = { ...input }\n      schema.build(input)\n      expect(input).toEqual(inputCopy)\n    })\n\n    it('adds $type to empty object', () => {\n      const emptySchema = typedObject('app.bsky.test', 'main', object({}))\n      const input = {}\n      const result = emptySchema.build(input)\n      expect(result).toEqual({ $type: 'app.bsky.test' })\n    })\n  })\n\n  describe('$build method', () => {\n    it('adds $type to object without $type', () => {\n      const input = { text: 'Hello world', likes: 5 }\n      const result = schema.$build(input)\n      expect(result).toEqual({\n        text: 'Hello world',\n        likes: 5,\n        $type: 'app.bsky.feed.post',\n      })\n    })\n\n    it('behaves identically to build', () => {\n      const input1 = { text: 'Hello world', likes: 5 }\n      const input2 = { text: 'Another post' }\n\n      expect(schema.$build(input1)).toEqual(schema.build(input1))\n      expect(schema.$build(input2)).toEqual(schema.build(input2))\n    })\n\n    it('does not mutate the input object', () => {\n      const input = { text: 'Hello world' }\n      const inputCopy = { ...input }\n      schema.$build(input)\n      expect(input).toEqual(inputCopy)\n    })\n  })\n\n  describe('bound $ methods', () => {\n    it('$build can be used as a detached function', () => {\n      const { $build } = schema\n      const result = $build({ text: 'Hello' })\n      expect(result).toEqual({\n        text: 'Hello',\n        $type: 'app.bsky.feed.post',\n      })\n    })\n\n    it('$isTypeOf can be used as a detached function', () => {\n      const { $isTypeOf } = schema\n      expect($isTypeOf({ text: 'Hello' })).toBe(true)\n      expect($isTypeOf({ $type: 'app.bsky.feed.post', text: 'Hello' })).toBe(\n        true,\n      )\n      expect($isTypeOf({ $type: 'other.type', text: 'Hello' })).toBe(false)\n    })\n\n    it('$parse can be used as a detached function', () => {\n      const { $parse } = schema\n      const result = $parse({ text: 'Hello' })\n      expect(result).toEqual({ text: 'Hello' })\n    })\n\n    it('$matches can be used as a detached function', () => {\n      const { $matches } = schema\n      expect($matches({ text: 'Hello' })).toBe(true)\n      expect($matches(42)).toBe(false)\n    })\n\n    it('lazy property returns the same function on repeated access', () => {\n      const fn1 = schema.$build\n      const fn2 = schema.$build\n      expect(fn1).toBe(fn2)\n    })\n  })\n\n  describe('with complex nested schemas', () => {\n    const complexSchema = typedObject(\n      'app.bsky.actor.profile',\n      'main',\n      object({\n        displayName: string(),\n        bio: optional(string({ maxLength: 256 })),\n        followerCount: optional(integer({ minimum: 0 })),\n        verified: optional(nullable(enumSchema([true, false]))),\n      }),\n    )\n\n    it('validates complex nested structure', () => {\n      const result = complexSchema.safeParse({\n        displayName: 'John Doe',\n        bio: 'Software developer',\n        followerCount: 1000,\n        verified: true,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with nullable property set to null', () => {\n      const result = complexSchema.safeParse({\n        displayName: 'John Doe',\n        verified: null,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when nested constraint is violated', () => {\n      const result = complexSchema.safeParse({\n        displayName: 'John Doe',\n        followerCount: -1,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when string exceeds maxLength', () => {\n      const result = complexSchema.safeParse({\n        displayName: 'John Doe',\n        bio: 'x'.repeat(257),\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('validates with matching $type', () => {\n      const result = complexSchema.safeParse({\n        $type: 'app.bsky.actor.profile',\n        displayName: 'John Doe',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects with non-matching $type', () => {\n      const result = complexSchema.safeParse({\n        $type: 'app.bsky.feed.post',\n        displayName: 'John Doe',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with different $type formats', () => {\n    it('validates with main type', () => {\n      const mainSchema = typedObject(\n        'app.bsky.feed.post',\n        'main',\n        object({ text: string() }),\n      )\n      const result = mainSchema.safeParse({ text: 'Hello' })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with custom fragment', () => {\n      const fragmentSchema = typedObject(\n        'app.bsky.feed.post',\n        'reply',\n        object({ text: string() }),\n      )\n      const result = fragmentSchema.safeParse({\n        $type: 'app.bsky.feed.post#reply',\n        text: 'Hello',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('distinguishes between different fragments', () => {\n      const replySchema = typedObject(\n        'app.bsky.feed.post',\n        'reply',\n        object({ text: string() }),\n      )\n      const result = replySchema.safeParse({\n        $type: 'app.bsky.feed.post#quote',\n        text: 'Hello',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('validates with long NSID', () => {\n      const longSchema = typedObject(\n        'com.example.app.feature.action.detail',\n        'variant',\n        object({ value: string() }),\n      )\n      const result = longSchema.safeParse({\n        $type: 'com.example.app.feature.action.detail#variant',\n        value: 'test',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('validates object with only extra properties', () => {\n      const minimalSchema = typedObject('app.bsky.test', 'main', object({}))\n      const result = minimalSchema.safeParse({\n        extra1: 'value1',\n        extra2: 'value2',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates empty object with no required properties', () => {\n      const minimalSchema = typedObject('app.bsky.test', 'main', object({}))\n      const result = minimalSchema.safeParse({})\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with $type as only property', () => {\n      const minimalSchema = typedObject('app.bsky.test', 'main', object({}))\n      const result = minimalSchema.safeParse({\n        $type: 'app.bsky.test',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects object with prototype properties', () => {\n      const obj = Object.create({ inherited: 'value' })\n      obj.text = 'Hello world'\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Date objects', () => {\n      const result = schema.safeParse(new Date())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects RegExp objects', () => {\n      const result = schema.safeParse(/pattern/)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Error objects', () => {\n      const result = schema.safeParse(new Error('test'))\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Map objects', () => {\n      const result = schema.safeParse(new Map())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Set objects', () => {\n      const result = schema.safeParse(new Set())\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects class instances', () => {\n      class CustomClass {\n        text = 'Hello'\n      }\n      const result = schema.safeParse(new CustomClass())\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('integration with all property types', () => {\n    const fullSchema = typedObject(\n      'app.bsky.test',\n      'full',\n      object({\n        required: string(),\n        optional: optional(string()),\n        nullable: nullable(string()),\n        optionalNullable: optional(nullable(string())),\n      }),\n    )\n\n    it('validates with all properties present', () => {\n      const result = fullSchema.safeParse({\n        required: 'value',\n        optional: 'value',\n        nullable: 'value',\n        optionalNullable: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with only required property and nullable', () => {\n      const result = fullSchema.safeParse({\n        required: 'value',\n        nullable: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with nullable property set to null', () => {\n      const result = fullSchema.safeParse({\n        required: 'value',\n        nullable: null,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with required nullable and optional nullable set to null', () => {\n      const result = fullSchema.safeParse({\n        required: 'value',\n        nullable: 'value',\n        optionalNullable: null,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when required property is missing', () => {\n      const result = fullSchema.safeParse({\n        optional: 'value',\n        nullable: 'value',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when required property is null', () => {\n      const result = fullSchema.safeParse({\n        required: null,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects when required property is undefined', () => {\n      const result = fullSchema.safeParse({\n        required: undefined,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('validates with $type and all properties', () => {\n      const result = fullSchema.safeParse({\n        $type: 'app.bsky.test#full',\n        required: 'value',\n        optional: 'value',\n        nullable: null,\n        optionalNullable: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('comparison with plain ObjectSchema', () => {\n    const plainSchema = object({\n      text: string(),\n      likes: optional(integer()),\n    })\n\n    it('typed schema accepts same input as plain schema', () => {\n      const input = { text: 'Hello', likes: 5 }\n      const typedResult = schema.safeParse(input)\n      const plainResult = plainSchema.safeParse(input)\n      expect(typedResult.success).toBe(plainResult.success)\n    })\n\n    it('typed schema adds $type enforcement', () => {\n      const input = { $type: 'wrong.type', text: 'Hello' }\n      const typedResult = schema.safeParse(input)\n      const plainResult = plainSchema.safeParse(input)\n      expect(typedResult.success).toBe(false)\n      expect(plainResult.success).toBe(true)\n    })\n\n    it('both schemas reject invalid types', () => {\n      const input = { text: 123 }\n      const typedResult = schema.safeParse(input)\n      const plainResult = plainSchema.safeParse(input)\n      expect(typedResult.success).toBe(false)\n      expect(plainResult.success).toBe(false)\n    })\n\n    it('typed schema accepts matching $type', () => {\n      const input = { $type: 'app.bsky.feed.post', text: 'Hello' }\n      const typedResult = schema.safeParse(input)\n      expect(typedResult.success).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/typed-object.ts",
    "content": "import { isPlainObject } from '@atproto/lex-data'\nimport {\n  $Type,\n  $TypeOf,\n  $Typed,\n  $TypedMaybe,\n  $type,\n  $typed,\n  InferInput,\n  InferOutput,\n  NsidString,\n  Schema,\n  Unknown$TypedObject,\n  ValidationContext,\n  Validator,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\n\nexport type MaybeTypedObject<\n  TType extends $Type,\n  TValue extends { $type?: unknown } = { $type?: unknown },\n> = TValue extends { $type?: TType }\n  ? TValue\n  : $TypedMaybe<Exclude<TValue, Unknown$TypedObject>, TType>\n\n/**\n * Schema for typed objects in Lexicon unions.\n *\n * Typed objects have a `$type` field that identifies which variant they are\n * in a union. The `$type` can be omitted in input (it's implicit), but if\n * present, it must match the expected value.\n *\n * @template TType - The $type string literal type\n * @template TShape - The validator type for the object's shape\n *\n * @example\n * ```ts\n * const schema = new TypedObjectSchema(\n *   'app.bsky.embed.images#view',\n *   l.object({ images: l.array(imageSchema) })\n * )\n * ```\n */\nexport class TypedObjectSchema<\n  const TType extends $Type = $Type,\n  const TShape extends Validator<{ [k: string]: unknown }> = any,\n> extends Schema<\n  $TypedMaybe<InferInput<TShape>, TType>,\n  $TypedMaybe<InferOutput<TShape>, TType>\n> {\n  readonly type = 'typedObject' as const\n\n  constructor(\n    readonly $type: TType,\n    readonly schema: TShape,\n  ) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isPlainObject(input)) {\n      return ctx.issueUnexpectedType(input, 'object')\n    }\n\n    if (\n      '$type' in input &&\n      input.$type !== undefined &&\n      input.$type !== this.$type\n    ) {\n      return ctx.issueInvalidPropertyValue(input, '$type', [this.$type])\n    }\n\n    return ctx.validate(input, this.schema)\n  }\n\n  build(\n    input: Omit<InferInput<this>, '$type'>,\n  ): $Typed<InferOutput<this>, TType> {\n    return this.parse($typed(input, this.$type)) as $Typed<\n      InferOutput<this>,\n      TType\n    >\n  }\n\n  isTypeOf<TValue extends Record<string, unknown>>(\n    value: TValue,\n  ): value is MaybeTypedObject<TType, TValue> {\n    return value.$type === undefined || value.$type === this.$type\n  }\n\n  /**\n   * Bound alias for {@link build} for compatibility with generated utilities.\n   * @see {@link build}\n   */\n  get $build(): typeof this.build {\n    return lazyProperty(this, '$build', this.build.bind(this))\n  }\n\n  /**\n   * Bound alias for {@link isTypeOf} for compatibility with generated utilities.\n   * @see {@link isTypeOf}\n   */\n  get $isTypeOf(): typeof this.isTypeOf {\n    return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this))\n  }\n}\n\n/**\n * Creates a typed object schema for use in Lexicon unions.\n *\n * Typed objects are identified by their `$type` field, which combines an NSID\n * and a hash (e.g., 'app.bsky.embed.images#view'). Used for union variants.\n *\n * This function offers two overloads:\n * - One that infers the type from arguments (no circular reference support)\n * - One with explicit interface for codegen with circular references\n *\n * @param nsid - The NSID part of the type (e.g., 'app.bsky.embed.images')\n * @param hash - The hash part of the type (e.g., 'view'), defaults to 'main'\n * @param validator - Schema for validating the object properties\n * @returns A new {@link TypedObjectSchema} instance\n *\n * @example\n * ```ts\n * // Image embed view\n * const imageViewSchema = l.typedObject(\n *   'app.bsky.embed.images',\n *   'view',\n *   l.object({\n *     images: l.array(l.object({\n *       thumb: l.string(),\n *       fullsize: l.string(),\n *       alt: l.string(),\n *     })),\n *   })\n * )\n *\n * // Main type (hash defaults to 'main')\n * const postViewSchema = l.typedObject(\n *   'app.bsky.feed.defs',\n *   'postView',\n *   l.object({ uri: l.string(), cid: l.string(), author: authorSchema })\n * )\n *\n * // Use $isTypeOf to narrow union types\n * if (imageViewSchema.$isTypeOf(embed)) {\n *   // embed is narrowed to image view type\n * }\n *\n * // Use $build to construct typed objects\n * const view = imageViewSchema.$build({ images: [...] })\n * // view.$type === 'app.bsky.embed.images#view'\n * ```\n */\nexport function typedObject<\n  const N extends NsidString,\n  const H extends string,\n  const S extends Validator<{ [k: string]: unknown }>,\n>(nsid: N, hash: H, validator: S): TypedObjectSchema<$Type<N, H>, S>\nexport function typedObject<V extends { $type?: $Type }>(\n  nsid: V extends { $type?: infer T extends string }\n    ? T extends `${infer N}#${string}`\n      ? N\n      : T // (T is a \"main\" type, so already an NSID)\n    : never,\n  hash: V extends { $type?: infer T extends string }\n    ? T extends `${string}#${infer H}`\n      ? H\n      : 'main'\n    : never,\n  validator: Validator<Omit<V, '$type'>>,\n): TypedObjectSchema<$TypeOf<V>, Validator<V>>\n/*@__NO_SIDE_EFFECTS__*/\nexport function typedObject<\n  const N extends NsidString,\n  const H extends string,\n  const S extends Validator<{ [k: string]: unknown }>,\n>(nsid: N, hash: H, validator: S) {\n  return new TypedObjectSchema<$Type<N, H>, S>($type(nsid, hash), validator)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/typed-ref.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\nimport { typedObject } from './typed-object.js'\nimport { typedRef } from './typed-ref.js'\n\ndescribe('TypedRefSchema', () => {\n  describe('basic validation', () => {\n    it('validates through a typed object reference with explicit $type', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n          age: integer(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        age: 30,\n      })\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.user')\n      }\n    })\n\n    it('validates through a typed object with explicit $type', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n          age: integer(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        age: 30,\n      })\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.user')\n      }\n    })\n\n    it('rejects input with wrong $type', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.wrong',\n        name: 'Alice',\n      })\n\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid input through reference', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n          age: integer(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        age: 'thirty',\n      })\n\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-objects through reference', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.value',\n        'main',\n        object({\n          value: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null through reference', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined through reference', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('$type property', () => {\n    it('exposes the $type from the referenced schema', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.post',\n        'main',\n        object({\n          text: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      expect(schema.$type).toBe('com.example.post')\n    })\n\n    it('validates that output has correct $type', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.like',\n        'main',\n        object({\n          subject: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.like',\n        subject: 'at://did:plc:abc/app.bsky.feed.post/123',\n      })\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.like')\n      }\n    })\n\n    it('ensures $type matches expected value', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.follow',\n        'main',\n        object({\n          subject: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      // Try to pass wrong $type\n      const result = schema.safeParse({\n        $type: 'com.example.block',\n        subject: 'did:plc:abc',\n      })\n\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('lazy schema resolution', () => {\n    it('does not call getter until first validation', () => {\n      let getterCalled = false\n\n      const schema = typedRef(() => {\n        getterCalled = true\n        return typedObject(\n          'com.example.test',\n          'main',\n          object({\n            value: string(),\n          }),\n        )\n      })\n\n      expect(getterCalled).toBe(false)\n\n      schema.safeParse({ value: 'test' })\n      expect(getterCalled).toBe(true)\n    })\n\n    it('does not call getter until $type is accessed', () => {\n      let getterCalled = false\n\n      const schema = typedRef(() => {\n        getterCalled = true\n        return typedObject(\n          'com.example.test',\n          'main',\n          object({\n            value: string(),\n          }),\n        )\n      })\n\n      expect(getterCalled).toBe(false)\n\n      // Access $type should trigger getter\n      const type = schema.$type\n      expect(getterCalled).toBe(true)\n      expect(type).toBe('com.example.test')\n    })\n\n    it('throws error if getter is called recursively', () => {\n      // @ts-expect-error\n      const schema = typedRef(() => {\n        // This would cause infinite recursion if not protected\n        return schema.validator\n      })\n\n      expect(() => {\n        schema.safeParse({ value: 'test' })\n      }).toThrow()\n    })\n  })\n\n  describe('with constrained schemas', () => {\n    it('validates typed object with string constraints', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.post',\n        'main',\n        object({\n          text: string({ minLength: 1, maxLength: 300 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.post',\n        text: 'This is a valid post',\n      })\n\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects typed object violating string constraints', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.post',\n        'main',\n        object({\n          text: string({ minLength: 1, maxLength: 300 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.post',\n        text: '',\n      })\n\n      expect(result.success).toBe(false)\n    })\n\n    it('validates typed object with integer constraints', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.rating',\n        'main',\n        object({\n          score: integer({ minimum: 1, maximum: 5 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.rating',\n        score: 4,\n      })\n\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects typed object violating integer constraints', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.rating',\n        'main',\n        object({\n          score: integer({ minimum: 1, maximum: 5 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.rating',\n        score: 10,\n      })\n\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('multiple validations', () => {\n    it('validates multiple inputs correctly', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string({ minLength: 2 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result1 = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n      })\n      expect(result1.success).toBe(true)\n\n      const result2 = schema.safeParse({ $type: 'com.example.user', name: 'A' })\n      expect(result2.success).toBe(false)\n\n      const result3 = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Bob',\n      })\n      expect(result3.success).toBe(true)\n\n      const result4 = schema.safeParse({ $type: 'com.example.user', name: '' })\n      expect(result4.success).toBe(false)\n    })\n\n    it('handles different types of validation failures', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string({ minLength: 2 }),\n          age: integer({ minimum: 0, maximum: 150 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result1 = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'A',\n        age: 25,\n      })\n      expect(result1.success).toBe(false)\n\n      const result2 = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        age: 200,\n      })\n      expect(result2.success).toBe(false)\n\n      const result3 = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        age: 25,\n      })\n      expect(result3.success).toBe(true)\n\n      const result4 = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n      })\n      expect(result4.success).toBe(false)\n    })\n\n    it('validates same input multiple times consistently', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.post',\n        'main',\n        object({\n          text: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const input = { $type: 'com.example.post', text: 'Hello world' }\n\n      const result1 = schema.safeParse(input)\n      const result2 = schema.safeParse(input)\n      const result3 = schema.safeParse(input)\n\n      expect(result1.success).toBe(true)\n      expect(result2.success).toBe(true)\n      expect(result3.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('handles empty object validation', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.empty',\n        'main',\n        object({}),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({ $type: 'com.example.empty' })\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.empty')\n      }\n    })\n\n    it('rejects arrays', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.test',\n        'main',\n        object({\n          value: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse([{ value: 'test' }])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects primitive values', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.test',\n        'main',\n        object({\n          value: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result1 = schema.safeParse('string')\n      expect(result1.success).toBe(false)\n\n      const result2 = schema.safeParse(123)\n      expect(result2.success).toBe(false)\n\n      const result3 = schema.safeParse(true)\n      expect(result3.success).toBe(false)\n    })\n\n    it('handles objects with extra properties', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        extra: 'property',\n        another: 'value',\n      })\n\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with zero values', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.counter',\n        'main',\n        object({\n          count: integer(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.counter',\n        count: 0,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates with empty strings', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.text',\n        'main',\n        object({\n          content: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.text',\n        content: '',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects NaN in integer fields', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.number',\n        'main',\n        object({\n          value: integer(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.number',\n        value: NaN,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Infinity in integer fields', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.number',\n        'main',\n        object({\n          value: integer(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.number',\n        value: Infinity,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('nested references', () => {\n    it('validates through nested TypedRefSchema', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string({ minLength: 2 }),\n        }),\n      )\n\n      const innerRef = typedRef(() => typedObjectSchema)\n      const outerRef = typedRef(() => innerRef.validator)\n\n      const result = outerRef.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n      })\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.user')\n      }\n    })\n\n    it('validates with objects containing TypedRef fields', () => {\n      const innerTyped = typedObject(\n        'com.example.profile',\n        'main',\n        object({\n          bio: string(),\n        }),\n      )\n\n      const outerTyped = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n          profile: typedRef(() => innerTyped),\n        }),\n      )\n\n      const schema = typedRef(() => outerTyped)\n\n      const result = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        profile: {\n          $type: 'com.example.profile',\n          bio: 'Software developer',\n        },\n      })\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.user')\n        expect(result.value.profile.$type).toBe('com.example.profile')\n      }\n    })\n\n    it('rejects nested objects with wrong $type', () => {\n      const innerTyped = typedObject(\n        'com.example.profile',\n        'main',\n        object({\n          bio: string(),\n        }),\n      )\n\n      const outerTyped = typedObject(\n        'com.example.user',\n        'main',\n        object({\n          name: string(),\n          profile: typedRef(() => innerTyped),\n        }),\n      )\n\n      const schema = typedRef(() => outerTyped)\n\n      const result = schema.safeParse({\n        $type: 'com.example.user',\n        name: 'Alice',\n        profile: {\n          $type: 'com.example.wrongtype',\n          bio: 'Software developer',\n        },\n      })\n\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('schema property access', () => {\n    it('allows direct access to resolved schema', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.test',\n        'main',\n        object({\n          value: string(),\n        }),\n      )\n\n      const refSchema = typedRef(() => typedObjectSchema)\n\n      const resolved = refSchema.validator\n      expect(resolved).toBe(typedObjectSchema)\n      expect(resolved.$type).toBe('com.example.test')\n    })\n\n    it('returns same instance on multiple schema property accesses', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.test',\n        'main',\n        object({\n          value: string(),\n        }),\n      )\n\n      const refSchema = typedRef(() => typedObjectSchema)\n\n      const first = refSchema.validator\n      const second = refSchema.validator\n      const third = refSchema.validator\n\n      expect(first).toBe(second)\n      expect(second).toBe(third)\n      expect(first.$type).toBe('com.example.test')\n    })\n  })\n\n  describe('complex object structures', () => {\n    it('validates complex nested structure', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.post',\n        'main',\n        object({\n          text: string({ minLength: 1, maxLength: 300 }),\n          createdAt: string({ format: 'datetime' }),\n          likeCount: integer({ minimum: 0 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.post',\n        text: 'Hello world!',\n        createdAt: '2023-01-01T00:00:00Z',\n        likeCount: 42,\n      })\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value.$type).toBe('com.example.post')\n        expect(result.value.text).toBe('Hello world!')\n        expect(result.value.likeCount).toBe(42)\n      }\n    })\n\n    it('validates structure with multiple property types', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.record',\n        'main',\n        object({\n          id: string({ format: 'nsid' }),\n          count: integer({ minimum: 0 }),\n          flag: string(),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      const result = schema.safeParse({\n        $type: 'com.example.record',\n        id: 'com.example.feed.post',\n        count: 100,\n        flag: 'active',\n      })\n\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects structure with any invalid property', () => {\n      const typedObjectSchema = typedObject(\n        'com.example.record',\n        'main',\n        object({\n          name: string({ minLength: 1 }),\n          count: integer({ minimum: 0 }),\n        }),\n      )\n\n      const schema = typedRef(() => typedObjectSchema)\n\n      // Valid name, invalid count\n      const result1 = schema.safeParse({\n        $type: 'com.example.record',\n        name: 'test',\n        count: -5,\n      })\n      expect(result1.success).toBe(false)\n\n      // Invalid name, valid count\n      const result2 = schema.safeParse({\n        $type: 'com.example.record',\n        name: '',\n        count: 5,\n      })\n      expect(result2.success).toBe(false)\n\n      // Both invalid\n      const result3 = schema.safeParse({\n        $type: 'com.example.record',\n        name: '',\n        count: -5,\n      })\n      expect(result3.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/typed-ref.ts",
    "content": "import {\n  $Typed,\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n} from '../core.js'\n\n/**\n * Interface for validators that have a $type property.\n *\n * Used by typed objects and records to identify their type in unions.\n *\n * @template TInput - The input type (with optional $type)\n * @template TOutput - The output type (with non-optional $type)\n */\nexport interface TypedObjectValidator<\n  TInput extends { $type?: string } = { $type?: string },\n  TOutput extends TInput = TInput,\n> extends Validator<TInput, TOutput> {\n  $type: NonNullable<TOutput['$type']>\n}\n\n/**\n * Function type that returns a typed object validator, used for lazy resolution.\n *\n * @template TValidator - The typed object validator type\n */\nexport type TypedRefGetter<out TValidator extends TypedObjectValidator> =\n  () => TValidator\n\n/**\n * Schema for referencing typed objects with lazy resolution.\n *\n * Used in typed unions to reference typed object schemas. Requires the\n * `$type` field to be present and match the referenced schema's type.\n * The referenced schema is resolved lazily to support circular references.\n *\n * @template TValidator - The referenced typed object validator type\n *\n * @example\n * ```ts\n * const ref = new TypedRefSchema(() => imageViewSchema)\n * // ref.$type === 'app.bsky.embed.images#view'\n * ```\n */\nexport class TypedRefSchema<\n  const TValidator extends TypedObjectValidator = TypedObjectValidator,\n> extends Schema<\n  $Typed<InferInput<TValidator>>,\n  $Typed<InferOutput<TValidator>>\n> {\n  readonly type = 'typedRef' as const\n\n  #getter: TypedRefGetter<TValidator>\n\n  constructor(getter: TypedRefGetter<TValidator>) {\n    // @NOTE In order to avoid circular dependency issues, we don't resolve\n    // the schema here. Instead, we resolve it lazily when first accessed.\n\n    super()\n\n    this.#getter = getter\n  }\n\n  get validator(): TValidator {\n    return this.#getter.call(null)\n  }\n\n  get $type(): TValidator['$type'] {\n    return this.validator.$type\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    const result = ctx.validate(input, this.validator)\n    if (!result.success) return result\n\n    if (result.value.$type !== this.$type) {\n      return ctx.issueInvalidPropertyValue(result.value, '$type', [this.$type])\n    }\n\n    return result\n  }\n}\n\n/**\n * Creates a reference to a typed object schema for use in typed unions.\n *\n * Unlike regular `ref()`, this requires the referenced schema to have a\n * `$type` property, and validates that the input's `$type` matches.\n *\n * @param get - Function that returns the typed object validator\n * @returns A new {@link TypedRefSchema} instance\n *\n * @example\n * ```ts\n * // Reference to image embed view\n * const imageRef = l.typedRef(() => imageViewSchema)\n *\n * // Use in a typed union\n * const embedUnion = l.typedUnion([\n *   l.typedRef(() => imageViewSchema),\n *   l.typedRef(() => videoViewSchema),\n *   l.typedRef(() => externalViewSchema),\n * ], true) // closed union\n *\n * // The $type is accessible on the ref\n * console.log(imageRef.$type) // 'app.bsky.embed.images#view'\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function typedRef<const TValidator extends TypedObjectValidator>(\n  get: TypedRefGetter<TValidator>,\n): TypedRefSchema<TValidator>\nexport function typedRef<\n  TInput extends { $type?: string },\n  TOutput extends TInput = TInput,\n>(\n  get: TypedRefGetter<TypedObjectValidator<TInput, TOutput>>,\n): TypedRefSchema<TypedObjectValidator<TInput, TOutput>>\nexport function typedRef<const TValidator extends TypedObjectValidator>(\n  get: TypedRefGetter<TValidator>,\n): TypedRefSchema<TValidator> {\n  return new TypedRefSchema<TValidator>(get)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/typed-union.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { integer } from './integer.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\nimport { typedObject } from './typed-object.js'\nimport { typedRef } from './typed-ref.js'\nimport { typedUnion } from './typed-union.js'\n\ndescribe('TypedUnionSchema', () => {\n  const personSchema = typedObject(\n    'app.bsky.actor.person',\n    'main',\n    object({\n      name: string(),\n      age: integer(),\n    }),\n  )\n\n  const postSchema = typedObject(\n    'app.bsky.feed.post',\n    'main',\n    object({\n      text: string(),\n      createdAt: string(),\n    }),\n  )\n\n  const commentSchema = typedObject(\n    'app.bsky.feed.comment',\n    'main',\n    object({\n      text: string(),\n      parentUri: string(),\n    }),\n  )\n\n  describe('closed union', () => {\n    const schema = typedUnion(\n      [typedRef(() => personSchema), typedRef(() => postSchema)],\n      true,\n    )\n\n    it('validates first type in union', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second type in union', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello world',\n        createdAt: '2023-01-01T00:00:00Z',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects unknown $type in closed union', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.like',\n        subject: 'some-uri',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object without $type', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object with invalid property for the $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 'thirty',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object missing required properties', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-object input', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects array', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects number', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects boolean', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('open union', () => {\n    const schema = typedUnion(\n      [typedRef(() => personSchema), typedRef(() => postSchema)],\n      false,\n    )\n\n    it('validates known type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates unknown $type with valid structure', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.like',\n        subject: 'some-uri',\n        createdAt: '2023-01-01T00:00:00Z',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('accepts any properties for unknown $type', () => {\n      const result = schema.safeParse({\n        $type: 'unknown.nsid.type',\n        anyProperty: 'any value',\n        anotherProperty: 123,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects unknown $type with non-string $type', () => {\n      const result = schema.safeParse({\n        $type: 123,\n        someProperty: 'value',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects unknown $type with null $type', () => {\n      const result = schema.safeParse({\n        $type: null,\n        someProperty: 'value',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object without $type', () => {\n      const result = schema.safeParse({\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects non-object input', () => {\n      const result = schema.safeParse('not an object')\n      expect(result.success).toBe(false)\n    })\n\n    it('validates known type with extra properties', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 30,\n        extraProperty: 'extra',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects known type with invalid property types', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 123,\n        age: 30,\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with three types', () => {\n    const schema = typedUnion(\n      [\n        typedRef(() => personSchema),\n        typedRef(() => postSchema),\n        typedRef(() => commentSchema),\n      ],\n      true,\n    )\n\n    it('validates first type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello',\n        createdAt: '2023-01-01T00:00:00Z',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates third type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.comment',\n        text: 'Nice post!',\n        parentUri: 'at://did:plc:xyz/app.bsky.feed.post/123',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects unknown type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.like',\n        subject: 'some-uri',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with single type', () => {\n    const schema = typedUnion([typedRef(() => personSchema)], true)\n\n    it('validates the single type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects different type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.feed.post',\n        text: 'Hello',\n        createdAt: '2023-01-01T00:00:00Z',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('$types getter', () => {\n    const schema = typedUnion(\n      [typedRef(() => personSchema), typedRef(() => postSchema)],\n      true,\n    )\n\n    it('returns array of valid $type values', () => {\n      const types = schema.$types\n      expect(types).toContain('app.bsky.actor.person')\n      expect(types).toContain('app.bsky.feed.post')\n      expect(types.length).toBe(2)\n    })\n  })\n\n  describe('refsMap getter', () => {\n    const schema = typedUnion(\n      [typedRef(() => personSchema), typedRef(() => postSchema)],\n      true,\n    )\n\n    it('returns map of $type to ref schema', () => {\n      const refsMap = schema.validatorsMap\n      expect(refsMap.size).toBe(2)\n      expect(refsMap.has('app.bsky.actor.person')).toBe(true)\n      expect(refsMap.has('app.bsky.feed.post')).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = typedUnion(\n      [typedRef(() => personSchema), typedRef(() => postSchema)],\n      true,\n    )\n\n    it('rejects object with $type as empty string in closed union', () => {\n      const result = schema.safeParse({\n        $type: '',\n        name: 'Alice',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('validates object with $type as empty string in open union', () => {\n      const openSchema = typedUnion([typedRef(() => personSchema)], false)\n      const result = openSchema.safeParse({\n        $type: '',\n        someProperty: 'value',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects plain object with only $type', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('handles object with $type and undefined properties', () => {\n      const result = schema.safeParse({\n        $type: 'app.bsky.actor.person',\n        name: 'Alice',\n        age: 30,\n        extra: undefined,\n      })\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('closed property', () => {\n    it('exposes closed property as true', () => {\n      const schema = typedUnion([typedRef(() => personSchema)], true)\n      expect(schema.closed).toBe(true)\n    })\n\n    it('exposes closed property as false', () => {\n      const schema = typedUnion([typedRef(() => personSchema)], false)\n      expect(schema.closed).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/typed-union.ts",
    "content": "import { isPlainObject } from '@atproto/lex-data'\nimport {\n  InferInput,\n  InferOutput,\n  Schema,\n  Unknown$TypedObject,\n  ValidationContext,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\nimport { TypedObjectSchema } from './typed-object.js'\nimport { TypedRefSchema } from './typed-ref.js'\n\n/**\n * Schema for Lexicon typed unions (unions discriminated by $type).\n *\n * Typed unions are collections of typed objects identified by their `$type`\n * field. Can be \"open\" (accept unknown types) or \"closed\" (only accept\n * known types).\n *\n * @template TValidators - Tuple of {@link TypedRefSchema} or {@link TypedObjectSchema} instances\n * @template TClosed - Whether the union is closed (rejects unknown $types)\n *\n * @example\n * ```ts\n * const embedUnion = new TypedUnionSchema([\n *   l.typedRef(() => imageSchema),\n *   l.typedRef(() => videoSchema),\n * ], true) // closed - only accepts images and videos\n * ```\n */\nexport class TypedUnionSchema<\n  const TValidators extends readonly (\n    | TypedRefSchema\n    | TypedObjectSchema\n  )[] = [],\n  const TClosed extends boolean = boolean,\n> extends Schema<\n  TClosed extends true\n    ? InferInput<TValidators[number]>\n    : InferInput<TValidators[number]> | Unknown$TypedObject,\n  TClosed extends true\n    ? InferOutput<TValidators[number]>\n    : InferOutput<TValidators[number]> | Unknown$TypedObject\n> {\n  readonly type = 'typedUnion' as const\n\n  constructor(\n    protected readonly validators: TValidators,\n    public readonly closed: TClosed,\n  ) {\n    // @NOTE In order to avoid circular dependency issues, we don't access the\n    // refs's schema (or $type) here. Instead, we access them lazily when first\n    // needed. The biggest issue with this strategy is that we can't throw\n    // early if the refs contain multiple refs with the same $type.\n\n    super()\n  }\n\n  get validatorsMap(): Map<unknown, TValidators[number]> {\n    const map = new Map<unknown, TValidators[number]>()\n    for (const ref of this.validators) map.set(ref.$type, ref)\n\n    return lazyProperty(this, 'validatorsMap', map)\n  }\n\n  get $types() {\n    return Array.from(this.validatorsMap.keys())\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    if (!isPlainObject(input) || !('$type' in input)) {\n      return ctx.issueUnexpectedType(input, '$typed')\n    }\n\n    const { $type } = input\n\n    const validator = this.validatorsMap.get($type)\n    if (validator) {\n      return ctx.validate(input, validator)\n    }\n\n    if (this.closed) {\n      return ctx.issueInvalidPropertyValue(input, '$type', this.$types)\n    }\n\n    if (typeof $type !== 'string') {\n      return ctx.issueInvalidPropertyType(input, '$type', 'string')\n    }\n\n    return ctx.success(input)\n  }\n}\n\n/**\n * Creates a typed union schema for Lexicon unions.\n *\n * Typed unions discriminate variants by their `$type` field. Can be open\n * (accepts unknown types, useful for extensibility) or closed (strict).\n *\n * @param refs - Array of typed refs for the union variants\n * @param closed - Whether to reject unknown $type values\n * @returns A new {@link TypedUnionSchema} instance\n *\n * @example\n * ```ts\n * // Closed union - only accepts known types\n * const embedSchema = l.typedUnion([\n *   l.typedRef(() => imageViewSchema),\n *   l.typedRef(() => videoViewSchema),\n *   l.typedRef(() => externalViewSchema),\n * ], true)\n *\n * // Open union - accepts unknown types for forward compatibility\n * const feedItemSchema = l.typedUnion([\n *   l.typedRef(() => postSchema),\n *   l.typedRef(() => repostSchema),\n * ], false) // unknown types pass through\n *\n * // Get all known $types\n * console.log(embedSchema.$types)\n * // ['app.bsky.embed.images#view', 'app.bsky.embed.video#view', ...]\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function typedUnion<\n  const R extends readonly TypedRefSchema[],\n  const C extends boolean,\n>(refs: R, closed: C) {\n  return new TypedUnionSchema<R, C>(refs, closed)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/union.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { boolean } from './boolean.js'\nimport { integer } from './integer.js'\nimport { object } from './object.js'\nimport { string } from './string.js'\nimport { union } from './union.js'\n\ndescribe('UnionSchema', () => {\n  const stringOrNumber = union([string(), integer()])\n\n  it('validates string input', () => {\n    const result = stringOrNumber.safeParse('hello')\n    expect(result.success).toBe(true)\n  })\n\n  it('validates number input', () => {\n    const result = stringOrNumber.safeParse(42)\n    expect(result.success).toBe(true)\n  })\n\n  it('rejects boolean input', () => {\n    const result = stringOrNumber.safeParse(true)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects null input', () => {\n    const result = stringOrNumber.safeParse(null)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects undefined input', () => {\n    const result = stringOrNumber.safeParse(undefined)\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects object input when not in union', () => {\n    const result = stringOrNumber.safeParse({ key: 'value' })\n    expect(result.success).toBe(false)\n  })\n\n  it('rejects array input when not in union', () => {\n    const result = stringOrNumber.safeParse([1, 2, 3])\n    expect(result.success).toBe(false)\n  })\n\n  describe('with object types', () => {\n    const schema = union([\n      object({\n        type: string(),\n        name: string(),\n      }),\n      object({\n        type: string(),\n        age: integer(),\n      }),\n    ])\n\n    it('validates first object variant', () => {\n      const result = schema.safeParse({\n        type: 'person',\n        name: 'Alice',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates second object variant', () => {\n      const result = schema.safeParse({\n        type: 'record',\n        age: 30,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects object missing required properties', () => {\n      const result = schema.safeParse({\n        type: 'person',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects object with invalid property types', () => {\n      const result = schema.safeParse({\n        type: 'record',\n        age: 'thirty',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with three types', () => {\n    const schema = union([string(), integer(), boolean()])\n\n    it('validates string input', () => {\n      const result = schema.safeParse('text')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates number input', () => {\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates boolean input', () => {\n      const result = schema.safeParse(false)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects null input', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects array input', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('with constrained types', () => {\n    const schema = union([string({ minLength: 5 }), integer({ minimum: 100 })])\n\n    it('validates string meeting constraint', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates number meeting constraint', () => {\n      const result = schema.safeParse(150)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects string not meeting constraint', () => {\n      const result = schema.safeParse('hi')\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects number not meeting constraint', () => {\n      const result = schema.safeParse(50)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates first matching type even if later types could match', () => {\n      const result = schema.safeParse('valid')\n      expect(result.success).toBe(true)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('validates with single type in union', () => {\n      const schema = union([string()])\n      const result = schema.safeParse('test')\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects when single type in union does not match', () => {\n      const schema = union([string()])\n      const result = schema.safeParse(123)\n      expect(result.success).toBe(false)\n    })\n\n    it('validates empty string', () => {\n      const result = stringOrNumber.safeParse('')\n      expect(result.success).toBe(true)\n    })\n\n    it('validates zero', () => {\n      const result = stringOrNumber.safeParse(0)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates negative numbers', () => {\n      const result = stringOrNumber.safeParse(-42)\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects NaN', () => {\n      const result = stringOrNumber.safeParse(NaN)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects Infinity', () => {\n      const result = stringOrNumber.safeParse(Infinity)\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects -Infinity', () => {\n      const result = stringOrNumber.safeParse(-Infinity)\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/union.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  LexValidationError,\n  Schema,\n  ValidationContext,\n  ValidationFailure,\n  Validator,\n} from '../core.js'\n\n/**\n * Type representing a non-empty tuple of validators for union schemas.\n *\n * Requires at least one validator in the tuple.\n */\nexport type UnionSchemaValidators = readonly [Validator, ...Validator[]]\n\n/**\n * Schema for validating values that match one of several possible schemas.\n *\n * Tries each validator in order until one succeeds. If all validators fail,\n * returns a combined error from all attempts.\n *\n * @template TValidators - Tuple type of the validators in the union\n *\n * @example\n * ```ts\n * const schema = new UnionSchema([l.string(), l.integer()])\n * schema.validate('hello') // success\n * schema.validate(42)      // success\n * schema.validate(true)    // fails\n * ```\n */\nexport class UnionSchema<\n  const TValidators extends UnionSchemaValidators = any,\n> extends Schema<\n  InferInput<TValidators[number]>,\n  InferOutput<TValidators[number]>\n> {\n  readonly type = 'union' as const\n\n  constructor(protected readonly validators: TValidators) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    const failures: ValidationFailure[] = []\n\n    for (const validator of this.validators) {\n      const result = ctx.validate(input, validator)\n      if (result.success) return result\n\n      failures.push(result)\n    }\n\n    return ctx.failure(LexValidationError.fromFailures(failures))\n  }\n}\n\n/**\n * Creates a union schema that accepts values matching any of the provided schemas.\n *\n * Validators are tried in order. Use `discriminatedUnion()` for better\n * performance when discriminating on a known property.\n *\n * @param validators - Non-empty array of validators to try\n * @returns A new {@link UnionSchema} instance\n *\n * @example\n * ```ts\n * // String or number\n * const stringOrNumber = l.union([l.string(), l.integer()])\n *\n * // Nullable value\n * const nullableString = l.union([l.string(), l.null()])\n *\n * // Multiple object types\n * const mediaSchema = l.union([\n *   l.object({ type: l.literal('image'), url: l.string() }),\n *   l.object({ type: l.literal('video'), url: l.string(), duration: l.integer() }),\n * ])\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function union<const TValidators extends UnionSchemaValidators>(\n  validators: TValidators,\n) {\n  return new UnionSchema<TValidators>(validators)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/unknown.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { unknown } from './unknown.js'\n\ndescribe('UnknownSchema', () => {\n  describe('basic validation', () => {\n    const schema = unknown()\n\n    it('accepts strings', () => {\n      const result = schema.safeParse('hello')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('hello')\n      }\n    })\n\n    it('accepts numbers', () => {\n      const result = schema.safeParse(42)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(42)\n      }\n    })\n\n    it('accepts booleans', () => {\n      const result = schema.safeParse(true)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(true)\n      }\n    })\n\n    it('accepts null', () => {\n      const result = schema.safeParse(null)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(null)\n      }\n    })\n\n    it('accepts undefined', () => {\n      const result = schema.safeParse(undefined)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(undefined)\n      }\n    })\n\n    it('accepts plain objects', () => {\n      const obj = { key: 'value', nested: { prop: 123 } }\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(obj)\n      }\n    })\n\n    it('accepts arrays', () => {\n      const arr = [1, 2, 3, 'four', { five: 5 }]\n      const result = schema.safeParse(arr)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(arr)\n      }\n    })\n\n    it('accepts empty arrays', () => {\n      const result = schema.safeParse([])\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual([])\n      }\n    })\n\n    it('accepts empty objects', () => {\n      const result = schema.safeParse({})\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual({})\n      }\n    })\n  })\n\n  describe('edge cases', () => {\n    const schema = unknown()\n\n    it('accepts zero', () => {\n      const result = schema.safeParse(0)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(0)\n      }\n    })\n\n    it('accepts empty string', () => {\n      const result = schema.safeParse('')\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe('')\n      }\n    })\n\n    it('accepts negative numbers', () => {\n      const result = schema.safeParse(-123)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(-123)\n      }\n    })\n\n    it('accepts floating point numbers', () => {\n      const result = schema.safeParse(3.14159)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(3.14159)\n      }\n    })\n\n    it('accepts NaN', () => {\n      const result = schema.safeParse(NaN)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBeNaN()\n      }\n    })\n\n    it('accepts Infinity', () => {\n      const result = schema.safeParse(Infinity)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(Infinity)\n      }\n    })\n\n    it('accepts -Infinity', () => {\n      const result = schema.safeParse(-Infinity)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(-Infinity)\n      }\n    })\n\n    it('accepts BigInt', () => {\n      const bigIntValue = BigInt(9007199254740991)\n      const result = schema.safeParse(bigIntValue)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(bigIntValue)\n      }\n    })\n\n    it('accepts functions', () => {\n      const fn = () => 'test'\n      const result = schema.safeParse(fn)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(fn)\n      }\n    })\n\n    it('accepts Date objects', () => {\n      const date = new Date('2023-01-01')\n      const result = schema.safeParse(date)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(date)\n      }\n    })\n\n    it('accepts RegExp objects', () => {\n      const regex = /test/gi\n      const result = schema.safeParse(regex)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(regex)\n      }\n    })\n\n    it('accepts Symbol', () => {\n      const sym = Symbol('test')\n      const result = schema.safeParse(sym)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(sym)\n      }\n    })\n\n    it('accepts Map objects', () => {\n      const map = new Map([['key', 'value']])\n      const result = schema.safeParse(map)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(map)\n      }\n    })\n\n    it('accepts Set objects', () => {\n      const set = new Set([1, 2, 3])\n      const result = schema.safeParse(set)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(set)\n      }\n    })\n\n    it('accepts WeakMap objects', () => {\n      const weakMap = new WeakMap()\n      const result = schema.safeParse(weakMap)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(weakMap)\n      }\n    })\n\n    it('accepts WeakSet objects', () => {\n      const weakSet = new WeakSet()\n      const result = schema.safeParse(weakSet)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(weakSet)\n      }\n    })\n\n    it('accepts class instances', () => {\n      class TestClass {\n        constructor(public value: string) {}\n      }\n      const instance = new TestClass('test')\n      const result = schema.safeParse(instance)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(instance)\n      }\n    })\n\n    it('accepts nested complex structures', () => {\n      const complex = {\n        array: [1, 'two', { three: 3 }],\n        nested: {\n          deep: {\n            value: [null, undefined, true],\n          },\n        },\n        fn: () => 'test',\n        date: new Date(),\n      }\n      const result = schema.safeParse(complex)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toEqual(complex)\n      }\n    })\n  })\n\n  describe('special JavaScript values', () => {\n    const schema = unknown()\n\n    it('accepts objects with null prototype', () => {\n      const obj = Object.create(null)\n      obj.key = 'value'\n      const result = schema.safeParse(obj)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(obj)\n      }\n    })\n\n    it('accepts Proxy objects', () => {\n      const target = { value: 42 }\n      const proxy = new Proxy(target, {})\n      const result = schema.safeParse(proxy)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(proxy)\n      }\n    })\n\n    it('accepts typed arrays', () => {\n      const uint8Array = new Uint8Array([1, 2, 3])\n      const result = schema.safeParse(uint8Array)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(uint8Array)\n      }\n    })\n\n    it('accepts ArrayBuffer', () => {\n      const buffer = new ArrayBuffer(8)\n      const result = schema.safeParse(buffer)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(buffer)\n      }\n    })\n\n    it('accepts Promise objects', () => {\n      const promise = Promise.resolve(42)\n      const result = schema.safeParse(promise)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(promise)\n      }\n    })\n\n    it('accepts Error objects', () => {\n      const error = new Error('test error')\n      const result = schema.safeParse(error)\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.value).toBe(error)\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/unknown.ts",
    "content": "import { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Schema that accepts any value without validation.\n *\n * Passes through any input unchanged. Use sparingly as it bypasses\n * type safety. Useful for dynamic data or when the schema is not\n * known at compile time.\n *\n * @example\n * ```ts\n * const schema = new UnknownSchema()\n * schema.validate(anything) // always succeeds\n * ```\n */\nexport class UnknownSchema extends Schema<unknown> {\n  readonly type = 'unknown' as const\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    return ctx.success(input)\n  }\n}\n\n/**\n * Creates an unknown schema that accepts any value.\n *\n * The value passes through without any validation or transformation.\n * Use this when you need to accept arbitrary data.\n *\n * @returns A new {@link UnknownSchema} instance\n *\n * @example\n * ```ts\n * // Accept any value\n * const anyDataSchema = l.unknown()\n *\n * // In an object with a dynamic field\n * const flexibleSchema = l.object({\n *   type: l.string(),\n *   data: l.unknown(),\n * })\n * ```\n */\nexport const unknown = /*#__PURE__*/ memoizedOptions(function () {\n  return new UnknownSchema()\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema/with-default.ts",
    "content": "import {\n  InferInput,\n  InferOutput,\n  Schema,\n  ValidationContext,\n  Validator,\n} from '../core.js'\n\n/**\n * Schema wrapper that provides a default value when the input is undefined.\n *\n * In parse mode, when the input is `undefined`, the default value is used\n * instead. In validate mode, undefined values pass through unchanged (the\n * default is not applied).\n *\n * @template TValidator - The wrapped validator type\n *\n * @example\n * ```ts\n * const schema = new WithDefaultSchema(l.integer(), 0)\n * schema.parse(undefined) // 0\n * schema.parse(42)        // 42\n * ```\n */\nexport class WithDefaultSchema<\n  const TValidator extends Validator,\n> extends Schema<InferInput<TValidator>, InferOutput<TValidator>> {\n  readonly type = 'withDefault' as const\n\n  constructor(\n    readonly validator: TValidator,\n    readonly defaultValue: InferInput<TValidator>,\n  ) {\n    super()\n  }\n\n  validateInContext(input: unknown, ctx: ValidationContext) {\n    // When in a validation context, the output should not be altered,\n    // so we don't apply the default.\n    if (input === undefined && ctx.options.mode !== 'validate') {\n      return ctx.validate(this.defaultValue, this.validator)\n    }\n\n    return ctx.validate(input, this.validator)\n  }\n}\n\n/**\n * Creates a schema that applies a default value when the input is undefined.\n *\n * Commonly used with `optional()` to provide fallback values for missing\n * properties. The default value is validated against the schema.\n *\n * @param validator - The validator for the value\n * @param defaultValue - The default value to use when input is undefined\n * @returns A new {@link WithDefaultSchema} instance\n *\n * @example\n * ```ts\n * // Integer with default\n * const countSchema = l.withDefault(l.integer(), 0)\n * countSchema.parse(undefined) // 0\n * countSchema.parse(5)         // 5\n *\n * // Commonly combined with optional in objects\n * const settingsSchema = l.object({\n *   theme: l.optional(l.withDefault(l.string(), 'light')),\n *   pageSize: l.optional(l.withDefault(l.integer(), 25)),\n * })\n * settingsSchema.parse({}) // { theme: 'light', pageSize: 25 }\n *\n * // Boolean with default\n * const enabledSchema = l.withDefault(l.boolean(), false)\n * ```\n */\nexport function withDefault<const TValidator extends Validator>(\n  validator: TValidator,\n  defaultValue: InferInput<TValidator>,\n) {\n  return new WithDefaultSchema<TValidator>(validator, defaultValue)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/schema.ts",
    "content": "// Concrete Types\nexport * from './schema/array.js'\nexport * from './schema/blob.js'\nexport * from './schema/boolean.js'\nexport * from './schema/bytes.js'\nexport * from './schema/cid.js'\nexport * from './schema/dict.js'\nexport * from './schema/enum.js'\nexport * from './schema/integer.js'\nexport * from './schema/lex-map.js'\nexport * from './schema/lex-value.js'\nexport * from './schema/literal.js'\nexport * from './schema/never.js'\nexport * from './schema/null.js'\nexport * from './schema/object.js'\nexport * from './schema/regexp.js'\nexport * from './schema/string.js'\nexport * from './schema/unknown.js'\n\n// Composite Types\nexport * from './schema/custom.js'\nexport * from './schema/discriminated-union.js'\nexport * from './schema/intersection.js'\nexport * from './schema/nullable.js'\nexport * from './schema/optional.js'\nexport * from './schema/ref.js'\nexport * from './schema/refine.js'\nexport * from './schema/union.js'\nexport * from './schema/with-default.js'\n\n// Lexicon specific Types\nexport * from './schema/params.js'\nexport * from './schema/payload.js'\nexport * from './schema/permission-set.js'\nexport * from './schema/permission.js'\nexport * from './schema/procedure.js'\nexport * from './schema/query.js'\nexport * from './schema/record.js'\nexport * from './schema/subscription.js'\nexport * from './schema/token.js'\nexport * from './schema/typed-object.js'\nexport * from './schema/typed-ref.js'\nexport * from './schema/typed-union.js'\n"
  },
  {
    "path": "packages/lex/lex-schema/src/util/array-agg.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { arrayAgg } from './array-agg.js'\n\ndescribe('arrayAgg', () => {\n  it('aggregates items based on comparison and aggregation functions', () => {\n    const input = [1, 1, 2, 2, 3, 3, 3]\n    const result = arrayAgg(\n      input,\n      (a, b) => a === b,\n      (items) => ({ value: items[0], count: items.length }),\n    )\n    expect(result).toEqual([\n      { value: 1, count: 2 },\n      { value: 2, count: 2 },\n      { value: 3, count: 3 },\n    ])\n  })\n\n  it('returns an empty array when input is empty', () => {\n    const input: number[] = []\n    const result = arrayAgg(\n      input,\n      (a, b) => a === b,\n      (items) => ({ value: items[0], count: items.length }),\n    )\n    expect(result).toEqual([])\n  })\n\n  it('handles non-consecutive grouping', () => {\n    const input = [1, 2, 1, 2, 3, 1]\n    const result = arrayAgg(\n      input,\n      (a, b) => a === b,\n      (items) => ({ value: items[0], count: items.length }),\n    )\n    expect(result).toEqual([\n      { value: 1, count: 3 },\n      { value: 2, count: 2 },\n      { value: 3, count: 1 },\n    ])\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/src/util/array-agg.ts",
    "content": "/**\n * Aggregates items in an array based on a comparison function and an aggregation function.\n *\n * @param arr - The input array to aggregate.\n * @param cmp - A comparison function that determines if two items belong to the same group.\n * @param agg - An aggregation function that combines items in a group into a single item.\n * @returns An array of aggregated items.\n * @example\n * ```ts\n * const input = [1, 1, 2, 2, 3, 3, 3]\n * const result = arrayAgg(\n *   input,\n *   (a, b) => a === b,\n *   (items) => { value: items[0], sum: items.reduce((sum, item) => sum + item, 0) },\n * )\n * // result is [{ value: 1, sum: 2 }, { value: 2, sum: 4 }, { value: 3, sum: 6 }]\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function arrayAgg<T, O>(\n  arr: readonly T[],\n  cmp: (a: T, b: T) => boolean,\n  agg: (items: [T, ...T[]]) => O,\n): O[] {\n  if (arr.length === 0) return []\n\n  const groups: [T, ...T[]][] = [[arr[0]]]\n  const skipped = Array<undefined | boolean>(arr.length)\n\n  outer: for (let i = 1; i < arr.length; i++) {\n    if (skipped[i]) continue\n    const item = arr[i]\n    for (let j = 0; j < groups.length; j++) {\n      if (cmp(item, groups[j][0])) {\n        groups[j].push(item)\n        skipped[i] = true\n        continue outer\n      }\n    }\n    groups.push([item])\n  }\n\n  return groups.map(agg)\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/util/assertion-util.ts",
    "content": "export type CheckFn<T> = <I extends string>(input: I) => input is I & T\n"
  },
  {
    "path": "packages/lex/lex-schema/src/util/if-any.ts",
    "content": "export type IfAny<T, TrueValue, FalseValue> = 0 extends 1 & T\n  ? TrueValue\n  : FalseValue\n"
  },
  {
    "path": "packages/lex/lex-schema/src/util/lazy-property.ts",
    "content": "/*@__NO_SIDE_EFFECTS__*/\nexport function lazyProperty<\n  O extends object,\n  const K extends keyof O,\n  const V extends O[K],\n>(obj: O, key: K, value: V): V {\n  Object.defineProperty(obj, key, {\n    value,\n    writable: false,\n    enumerable: false,\n    configurable: true,\n  })\n  return value\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/src/util/memoize.ts",
    "content": "/*@__NO_SIDE_EFFECTS__*/\nexport function memoizedOptions<F extends (...args: any[]) => any>(fn: F): F {\n  let cache: null | { value: ReturnType<F> } = null\n\n  return function cached(...args: any[]): ReturnType<F> {\n    // Not using the cache if there are args\n    if (args.length > 0) {\n      return fn(...args)\n    }\n\n    if (cache != null) {\n      return cache.value\n    }\n\n    const value = fn(...args)\n    cache = { value }\n    return value\n  } as F\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nexport function memoizedTransformer<\n  F extends (key: any, ...args: any[]) => unknown,\n>(fn: F): F {\n  let cache: WeakMap<object, ReturnType<F>>\n\n  return function cached(key: any, ...args: any[]): any {\n    if (args.length > 0) return fn(key, ...args)\n\n    cache ??= new WeakMap()\n    const cached = cache.get(key)\n    if (cached) return cached\n    const result = fn(key, ...args) as ReturnType<F>\n    cache.set(key, result)\n    return result\n  } as F\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/tests/l.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { parseCid } from '@atproto/lex-data'\nimport { l } from '../src/index.js'\n\n// await cidForRawBytes(Buffer.from('Hello, World!'))\nconst blobCid = parseCid(\n  'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',\n)\n\ndescribe('simple schemas', () => {\n  describe('l.integer', () => {\n    const schema = l.integer()\n\n    it('validates integers', () => {\n      expect(schema.matches(42)).toBe(true)\n    })\n\n    it('rejects floats', () => {\n      expect(schema.matches(3.14)).toBe(false)\n    })\n\n    it('rejects strings', () => {\n      expect(schema.matches('42')).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.integer()).toBe(schema)\n    })\n\n    it('does not memoize with options', () => {\n      // @ts-expect-error\n      expect(l.integer({ unknownOption: 43 })).not.toBe(schema)\n      expect(l.integer({ minimum: 0 })).not.toBe(schema)\n      expect(l.integer({ minimum: 0 })).not.toBe(l.integer({ minimum: 0 }))\n      expect(l.integer({ maximum: 100 })).not.toBe(l.integer({ maximum: 100 }))\n    })\n  })\n\n  describe('l.string', () => {\n    const schema = l.string()\n\n    it('validates strings', () => {\n      expect(schema.matches('hello')).toBe(true)\n    })\n\n    it('rejects numbers', () => {\n      expect(schema.matches(123)).toBe(false)\n    })\n\n    it('rejects null', () => {\n      expect(schema.matches(null)).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.string()).toBe(schema)\n    })\n  })\n\n  describe('l.boolean', () => {\n    const schema = l.boolean()\n\n    it('validates true', () => {\n      expect(schema.matches(true)).toBe(true)\n    })\n\n    it('validates false', () => {\n      expect(schema.matches(false)).toBe(true)\n    })\n\n    it('rejects strings', () => {\n      expect(schema.matches('true')).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.boolean()).toBe(schema)\n\n      expect(l.optional(l.boolean())).toBe(l.optional(l.boolean()))\n      expect(l.nullable(l.boolean())).toBe(l.nullable(l.boolean()))\n    })\n  })\n\n  describe('l.blob', () => {\n    const schema = l.blob()\n\n    it('validates valid blob references', () => {\n      expect(\n        schema.matches({\n          $type: 'blob',\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        }),\n      ).toBe(true)\n    })\n\n    it('rejects blob without $type', () => {\n      expect(\n        schema.matches({\n          ref: blobCid,\n          mimeType: 'image/jpeg',\n          size: 10000,\n        }),\n      ).toBe(false)\n    })\n\n    it('rejects non-objects', () => {\n      expect(schema.matches('not a blob')).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.blob()).toBe(schema)\n    })\n  })\n\n  describe('l.null', () => {\n    const schema = l.null()\n\n    it('validates null', () => {\n      expect(schema.matches(null)).toBe(true)\n    })\n\n    it('rejects undefined', () => {\n      expect(schema.matches(undefined)).toBe(false)\n    })\n\n    it('rejects strings', () => {\n      expect(schema.matches('null')).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.null()).toBe(schema)\n    })\n  })\n\n  describe('l.literal', () => {\n    const schema = l.literal('active')\n\n    it('validates matching literal', () => {\n      expect(schema.matches('active')).toBe(true)\n    })\n\n    it('rejects non-matching value', () => {\n      expect(schema.matches('inactive')).toBe(false)\n    })\n  })\n\n  describe('l.enum', () => {\n    const schema = l.enum(['red', 'green', 'blue'])\n\n    it('validates enum values', () => {\n      expect(schema.matches('red')).toBe(true)\n      expect(schema.matches('green')).toBe(true)\n      expect(schema.matches('blue')).toBe(true)\n    })\n\n    it('rejects non-enum values', () => {\n      expect(schema.matches('yellow')).toBe(false)\n    })\n  })\n\n  describe('l.array', () => {\n    const schema = l.array(l.string())\n\n    it('validates arrays of strings', () => {\n      expect(schema.matches(['hello', 'world'])).toBe(true)\n    })\n\n    it('validates empty arrays', () => {\n      expect(schema.matches([])).toBe(true)\n    })\n\n    it('rejects arrays with invalid items', () => {\n      expect(schema.matches(['hello', 123])).toBe(false)\n    })\n\n    it('rejects non-arrays', () => {\n      expect(schema.matches('not an array')).toBe(false)\n    })\n  })\n\n  describe('l.object', () => {\n    const schema = l.object({\n      name: l.string(),\n      age: l.integer(),\n    })\n\n    it('validates valid objects', () => {\n      expect(schema.matches({ name: 'Alice', age: 30 })).toBe(true)\n    })\n\n    it('rejects objects with missing properties', () => {\n      expect(schema.matches({ name: 'Alice' })).toBe(false)\n    })\n\n    it('rejects objects with invalid property types', () => {\n      expect(schema.matches({ name: 'Alice', age: 'thirty' })).toBe(false)\n    })\n  })\n\n  describe('l.nullable', () => {\n    const schema = l.nullable(l.string())\n\n    it('validates null', () => {\n      expect(schema.matches(null)).toBe(true)\n    })\n\n    it('validates wrapped type', () => {\n      expect(schema.matches('hello')).toBe(true)\n    })\n\n    it('rejects invalid types', () => {\n      expect(schema.matches(123)).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.nullable(l.string())).toBe(schema)\n    })\n  })\n\n  describe('l.optional', () => {\n    const schema = l.optional(l.string())\n\n    it('validates undefined', () => {\n      expect(schema.matches(undefined)).toBe(true)\n    })\n\n    it('validates wrapped type', () => {\n      expect(schema.matches('hello')).toBe(true)\n    })\n\n    it('rejects null', () => {\n      expect(schema.matches(null)).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.optional(l.string())).toBe(schema)\n    })\n  })\n\n  describe('l.unknown', () => {\n    const schema = l.unknown()\n\n    it('validates any value', () => {\n      expect(schema.matches('string')).toBe(true)\n      expect(schema.matches(123)).toBe(true)\n      expect(schema.matches(null)).toBe(true)\n      expect(schema.matches({ key: 'value' })).toBe(true)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.unknown()).toBe(schema)\n    })\n  })\n\n  describe('l.never', () => {\n    const schema = l.never()\n\n    it('rejects all values', () => {\n      expect(schema.matches('string')).toBe(false)\n      expect(schema.matches(123)).toBe(false)\n      expect(schema.matches(null)).toBe(false)\n      expect(schema.matches(undefined)).toBe(false)\n    })\n\n    it('memoizes instances', () => {\n      expect(l.never()).toBe(schema)\n      expect(l.never()).toBe(schema)\n      expect(l.never()).toBe(schema)\n    })\n  })\n})\n\ndescribe('complex schemas', () => {\n  const addressSchema = l.object({\n    street: l.string(),\n    city: l.string(),\n    zipCode: l.integer(),\n  })\n\n  const mobilityPreferenceSchema = l.discriminatedUnion('type', [\n    l.object({\n      type: l.literal('car'),\n      carModel: l.string(),\n    }),\n    l.object({\n      type: l.literal('bike'),\n      bikeType: l.string(),\n    }),\n    l.object({\n      type: l.literal('public_transport'),\n      preferredLines: l.array(l.string()),\n    }),\n  ])\n\n  const userSchema = l.object({\n    id: l.integer(),\n    name: l.string(),\n    gender: l.optional(l.nullable(l.enum(['male', 'female']))),\n    address: addressSchema,\n    mobilityPreferences: l.optional(l.array(mobilityPreferenceSchema)),\n    parent: l.optional(l.ref((() => userSchema) as any)),\n  })\n\n  describe('addressSchema', () => {\n    it('validates valid address', () => {\n      const result = addressSchema.safeParse({\n        street: '123 Main St',\n        city: 'Springfield',\n        zipCode: 12345,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects missing required field', () => {\n      const result = addressSchema.safeParse({\n        street: '123 Main St',\n        city: 'Springfield',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects invalid field type', () => {\n      const result = addressSchema.safeParse({\n        street: '123 Main St',\n        city: 'Springfield',\n        zipCode: '12345',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('mobilityPreferenceSchema (discriminatedUnion)', () => {\n    it('validates car preference', () => {\n      const result = mobilityPreferenceSchema.safeParse({\n        type: 'car',\n        carModel: 'Tesla Model 3',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates bike preference', () => {\n      const result = mobilityPreferenceSchema.safeParse({\n        type: 'bike',\n        bikeType: 'mountain',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates public transport preference', () => {\n      const result = mobilityPreferenceSchema.safeParse({\n        type: 'public_transport',\n        preferredLines: ['Line 1', 'Line 2'],\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects invalid discriminator value', () => {\n      const result = mobilityPreferenceSchema.safeParse({\n        type: 'helicopter',\n        model: 'Apache',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects missing discriminator', () => {\n      const result = mobilityPreferenceSchema.safeParse({\n        carModel: 'Tesla Model 3',\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n\n  describe('userSchema', () => {\n    const validUser = {\n      id: 1,\n      name: 'Alice',\n      address: {\n        street: '123 Main St',\n        city: 'Springfield',\n        zipCode: 12345,\n      },\n    }\n\n    it('validates minimal valid user', () => {\n      const result = userSchema.safeParse(validUser)\n      expect(result.success).toBe(true)\n    })\n\n    it('validates user with optional gender', () => {\n      const result = userSchema.safeParse({\n        ...validUser,\n        gender: 'female',\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates user with null gender', () => {\n      const result = userSchema.safeParse({\n        ...validUser,\n        gender: null,\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates user with mobility preferences', () => {\n      const result = userSchema.safeParse({\n        ...validUser,\n        mobilityPreferences: [\n          { type: 'car', carModel: 'Tesla' },\n          { type: 'bike', bikeType: 'road' },\n        ],\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('validates user with parent reference', () => {\n      const result = userSchema.safeParse({\n        ...validUser,\n        parent: {\n          id: 2,\n          name: 'Bob',\n          address: {\n            street: '456 Oak Ave',\n            city: 'Springfield',\n            zipCode: 12346,\n          },\n        },\n      })\n      expect(result.success).toBe(true)\n    })\n\n    it('rejects user with invalid gender', () => {\n      const result = userSchema.safeParse({\n        ...validUser,\n        gender: 'other',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects user with missing required field', () => {\n      const result = userSchema.safeParse({\n        id: 1,\n        name: 'Alice',\n      })\n      expect(result.success).toBe(false)\n    })\n\n    it('rejects user with invalid mobility preference', () => {\n      const result = userSchema.safeParse({\n        ...validUser,\n        mobilityPreferences: [{ type: 'invalid', data: 'test' }],\n      })\n      expect(result.success).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-schema/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-schema/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex/lex-server/.npmignore",
    "content": "*.test.ts\n"
  },
  {
    "path": "packages/lex/lex-server/CHANGELOG.md",
    "content": "# @atproto/lex-server\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-client@0.0.17\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-data@0.0.14\n  - @atproto/lex-cbor@0.0.15\n  - @atproto/lex-json@0.0.14\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies [[`5a2f884`](https://github.com/bluesky-social/atproto/commit/5a2f8847efd91252971fa243d21bd52ada7aa8f4), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-client@0.0.16\n  - @atproto/lex-schema@0.0.15\n\n## 0.0.12\n\n### Patch Changes\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `/xrpc/_health` requests\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update error management to be more aligned with the way errors work in `@atproto/xrpc` and `@atproto/xrpc-server`\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `LexServerError` error class to encode errors destined to be returned as XRPC responses by an XRPC server\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow defining a custom `fallback` handler for non XRPC HTTP calls\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-schema@0.0.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-client@0.0.15\n  - @atproto/lex-cbor@0.0.14\n  - @atproto/lex-json@0.0.13\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`66b7295`](https://github.com/bluesky-social/atproto/commit/66b72950e8bcb39cac3382116bd282b3bb692f16)]:\n  - @atproto/lex-cbor@0.0.13\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `LexValue` schema validation\n\n- [#4660](https://github.com/bluesky-social/atproto/pull/4660) [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing accuracy of `ReadableStream` values\n\n- Updated dependencies [[`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`39dea03`](https://github.com/bluesky-social/atproto/commit/39dea03c417a1da069962560505427a7aa25ad7a), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df), [`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-schema@0.0.13\n  - @atproto/lex-data@0.0.12\n  - @atproto/lex-cbor@0.0.12\n  - @atproto/lex-json@0.0.12\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4603](https://github.com/bluesky-social/atproto/pull/4603) [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix flaky test\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix `exports` field in package.json\n\n- [#4601](https://github.com/bluesky-social/atproto/pull/4601) [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add JSDoc\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-schema@0.0.12\n  - @atproto/lex-json@0.0.11\n  - @atproto/lex-cbor@0.0.11\n  - @atproto/lex-data@0.0.11\n\n## 0.0.8\n\n### Patch Changes\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `LexRouter.handlers` property public\n\n- [#4589](https://github.com/bluesky-social/atproto/pull/4589) [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `sendResponse` for writing a `Response` object to a NodeJS `ServerResponse`\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-cbor@0.0.10\n  - @atproto/lex-json@0.0.10\n  - @atproto/lex-schema@0.0.11\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n  - @atproto-labs/did-resolver@0.2.6\n\n## 0.0.6\n\n### Patch Changes\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `handle()` method to `fetch()`\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`ecf5921`](https://github.com/bluesky-social/atproto/commit/ecf59214d59d9d2530c197c0679d26e76c6a60ef), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/lex-schema@0.0.10\n  - @atproto/lex-cbor@0.0.9\n  - @atproto/lex-json@0.0.9\n  - @atproto/lex-data@0.0.9\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/lex-cbor@0.0.8\n  - @atproto/lex-json@0.0.8\n  - @atproto/lex-schema@0.0.9\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-schema@0.0.8\n  - @atproto/lex-cbor@0.0.7\n  - @atproto/lex-json@0.0.7\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4501](https://github.com/bluesky-social/atproto/pull/4501) [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add service auth authentication method\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n  - @atproto/did@0.2.4\n  - @atproto/lex-schema@0.0.7\n  - @atproto/lex-cbor@0.0.6\n  - @atproto/lex-json@0.0.6\n  - @atproto-labs/did-resolver@0.2.5\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4498](https://github.com/bluesky-social/atproto/pull/4498) [`e9f065f`](https://github.com/bluesky-social/atproto/commit/e9f065fce85cd7940617eb611050e6ef65c54c04) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add AbortSignal to context\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4443](https://github.com/bluesky-social/atproto/pull/4443) [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add new XRPC server library\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-schema@0.0.6\n  - @atproto/lex-data@0.0.5\n  - @atproto/lex-cbor@0.0.5\n  - @atproto/lex-json@0.0.5\n"
  },
  {
    "path": "packages/lex/lex-server/README.md",
    "content": "# @atproto/lex-server\n\nRequest router for Atproto Lexicon protocols and schemas. See the [Changelog](./CHANGELOG.md) for version history.\n\n```bash\nnpm install @atproto/lex-server\n```\n\n- Type-safe request routing based on Lexicon schemas\n- Support for queries, procedures, and WebSocket subscriptions\n- Built on Web standard `Request`/`Response` APIs (portable across runtimes)\n- Custom authentication with credential passing\n- Graceful shutdown with `AsyncDisposable` pattern\n\n> [!IMPORTANT]\n>\n> This package is currently in **preview**. The API and features are subject to change before the stable release.\n\n**What is this?**\n\nBuilding AT Protocol servers requires handling XRPC requests, validating inputs against Lexicon schemas, managing authentication, and supporting real-time subscriptions. `@atproto/lex-server` automates this by:\n\n1. Routing requests to type-safe handlers based on Lexicon schemas\n2. Automatically validating request parameters and bodies\n3. Providing a flexible authentication system with custom strategies\n4. Supporting WebSocket subscriptions with backpressure handling\n\n```typescript\nimport { LexRouter } from '@atproto/lex-server'\nimport { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'\nimport * as app from './lexicons/app.js'\n\nconst router = new LexRouter({ upgradeWebSocket })\n  .add(app.bsky.actor.getProfile, async ({ params }) => {\n    const profile = await db.getProfile(params.actor)\n    return { body: profile }\n  })\n  .add(app.bsky.feed.post.create, {\n    auth: requireAuth,\n    handler: async ({ credentials, input }) => {\n      const result = await db.createPost(credentials.did, input.body)\n      return { body: result }\n    },\n  })\n\nawait serve(router, { port: 3000 })\n```\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n- [Quick Start](#quick-start)\n- [LexRouter](#lexrouter)\n  - [Creating a Router](#creating-a-router)\n  - [Adding Routes](#adding-routes)\n  - [Handler Context](#handler-context)\n  - [Handler Output](#handler-output)\n- [Queries and Procedures](#queries-and-procedures)\n  - [Query Handler](#query-handler)\n  - [Procedure Handler](#procedure-handler)\n  - [Binary Payloads](#binary-payloads)\n- [Subscriptions](#subscriptions)\n- [Authentication](#authentication)\n  - [Custom Authentication](#custom-authentication)\n  - [WWW-Authenticate Headers](#www-authenticate-headers)\n- [Error Handling](#error-handling)\n  - [LexServerError](#lexservererror)\n  - [LexServerAuthError](#lexserverautherror)\n  - [Error Handler Callback](#error-handler-callback)\n- [Node.js Server](#nodejs-server)\n  - [serve()](#serve)\n  - [createServer()](#createserver)\n  - [toRequestListener()](#torequestlistener)\n  - [upgradeWebSocket()](#upgradewebsocket)\n- [Advanced Usage](#advanced-usage)\n  - [Custom Response Objects](#custom-response-objects)\n  - [Response Headers](#response-headers)\n  - [Connection Info](#connection-info)\n- [License](#license)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n## Quick Start\n\n**1. Install the package**\n\n```bash\nnpm install @atproto/lex-server\n```\n\n**2. Generate Lexicon schemas**\n\nUse `@atproto/lex` to generate TypeScript schemas from your Lexicon definitions:\n\n```bash\nlex install app.bsky.actor.getProfile\nlex build\n```\n\n**3. Create a router and add handlers**\n\n```typescript\nimport { LexRouter, LexServerError } from '@atproto/lex-server'\nimport { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'\nimport * as app from './lexicons/app.js'\n\nconst router = new LexRouter({ upgradeWebSocket })\n\n// Add a query handler\nrouter.add(app.bsky.actor.getProfile, async ({ params }) => {\n  const profile = await db.getProfile(params.actor)\n  if (!profile) {\n    throw new LexServerError(404, {\n      error: 'NotFound',\n      message: 'Profile not found',\n    })\n  }\n  return { body: profile }\n})\n\n// Start the server\nconst server = await serve(router, { port: 3000 })\nconsole.log('Server listening on port 3000')\n```\n\n## LexRouter\n\nThe `LexRouter` class is the core of `@atproto/lex-server`. It routes XRPC requests to type-safe handlers based on Lexicon schemas.\n\n### Creating a Router\n\n```typescript\nimport { LexRouter } from '@atproto/lex-server'\nimport { upgradeWebSocket } from '@atproto/lex-server/nodejs'\n\nconst router = new LexRouter({\n  // Required for WebSocket subscriptions (Node.js)\n  upgradeWebSocket,\n\n  // Optional: Add logging or error reporting for unexpected errors in handlers\n  onHandlerError: ({ error, request, method }) => {\n    console.error(`Error in ${method.nsid}:`, error)\n  },\n\n  // Optional: WebSocket backpressure settings\n  highWaterMark: 250_000, // bytes (default: 250KB)\n  lowWaterMark: 50_000, // bytes (default: 50KB)\n})\n```\n\n### Adding Routes\n\nRoutes are added using the `.add()` method, which accepts a Lexicon schema and a handler:\n\n```typescript\n// Simple handler (no authentication)\nrouter.add(schema, async ({ params, input, request }) => {\n  return { body: result }\n})\n\n// Handler with authentication\nrouter.add(schema, {\n  auth: async ({ request, params }) => credentials,\n  handler: async ({ params, input, credentials, request }) => {\n    return { body: result }\n  },\n})\n```\n\nThe router supports method chaining:\n\n```typescript\nconst router = new LexRouter()\n  .add(app.bsky.actor.getProfile, profileHandler)\n  .add(app.bsky.feed.getTimeline, timelineHandler)\n  .add(app.bsky.feed.post.create, postHandler)\n```\n\n### Handler Context\n\nHandlers receive a context object with the following properties:\n\n```typescript\ntype LexRouterHandlerContext<Method, Credentials> = {\n  credentials: Credentials // Result of auth function (undefined if no auth)\n  input: InferMethodInput<Method> // Parsed request body (procedures only)\n  params: InferMethodParams<Method> // Parsed URL query parameters\n  request: Request // Original Web Request object\n  connection?: ConnectionInfo // Network connection info\n}\n```\n\n### Handler Output\n\nHandlers can return various output formats:\n\n```typescript\n// JSON response (encoding inferred from schema)\nreturn { body: { key: 'value' } }\n\n// With custom encoding\nreturn { encoding: 'text/plain', body: 'Hello, world!' }\n\n// With response headers\nreturn { body: data, headers: { 'x-custom': 'value' } }\n\n// Empty response (200 OK with no body)\nreturn {}\n\n// Custom Response object (full control)\nreturn new Response(body, { status: 201, headers })\n\n// Proxy Response\nreturn fetch('https://example.com/data')\n```\n\n## Queries and Procedures\n\n### Query Handler\n\nQueries handle `GET` requests and receive parameters from the URL query string:\n\n```typescript\nimport * as app from './lexicons/app.js'\n\nrouter.add(app.bsky.actor.getProfile, async ({ params }) => {\n  // params.actor is typed and validated\n  const profile = await db.getProfile(params.actor)\n  return { body: profile }\n})\n```\n\n### Procedure Handler\n\nProcedures handle `POST` requests and receive a request body:\n\n```typescript\nrouter.add(app.bsky.feed.post.create, async ({ input }) => {\n  // input.body contains the parsed and validated request body\n  const post = await db.createPost(input.body)\n  return { body: { uri: post.uri, cid: post.cid } }\n})\n```\n\n### Binary Payloads\n\nFor endpoints that accept or return binary data:\n\n```typescript\n// Binary input\nrouter.add(app.example.uploadBlob, async ({ input }) => {\n  // input.body is a Request object for streaming\n  // input.encoding contains the content-type\n  const blob = await input.body.arrayBuffer()\n  return { body: { cid: await store(blob) } }\n})\n\n// Binary output\nrouter.add(app.example.getBlob, async ({ params }) => {\n  const stream = await getBlob(params.cid)\n  return {\n    encoding: 'application/octet-stream',\n    body: stream,\n  }\n})\n```\n\n## Subscriptions\n\nSubscriptions provide real-time data over WebSocket connections. Handlers are async generators that yield messages:\n\n```typescript\nimport { LexRouter, LexError } from '@atproto/lex-server'\nimport { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'\nimport { scheduler } from 'node:timers/promises'\n\nconst router = new LexRouter({\n  upgradeWebSocket, // Required for WebSocket support in nodejs\n})\n\nrouter.add(com.example.stream, async function* ({ params, request }) {\n  const { cursor = 0, limit = 10 } = params\n  const { signal } = request\n\n  for (let i = 0; i < limit; i++) {\n    // Yield messages to the client\n    yield com.example.stream.message.$build({\n      data: `Message ${cursor + i}`,\n      cursor: cursor + i,\n    })\n\n    // Wait between messages (respects abort signal)\n    await scheduler.wait(1000, { signal })\n  }\n\n  // Throwing a LexError closes the connection with an error frame\n  throw new LexError('LimitReached', `Limit of ${limit} messages reached`)\n})\n```\n\nMessages are CBOR-encoded and sent as WebSocket binary frames. The router handles:\n\n- WebSocket upgrade negotiation\n- Backpressure management\n- Graceful connection cleanup\n- Error frame encoding\n\n## Authentication\n\n### Custom Authentication\n\nAuthentication is implemented through the `auth` function in handler configs:\n\n```typescript\nimport { LexError, LexServerAuthError } from '@atproto/lex-server'\n\ntype Credentials = { did: string; scope: string[] }\n\nconst requireAuth = async ({\n  request,\n}: {\n  request: Request\n}): Promise<Credentials> => {\n  const header = request.headers.get('authorization')\n  if (!header?.startsWith('Bearer ')) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Bearer token required',\n      {\n        Bearer: { realm: 'api' },\n      },\n    )\n  }\n\n  const token = header.slice(7)\n  const session = await verifyToken(token)\n  if (!session) {\n    throw new LexServerAuthError(\n      'InvalidToken',\n      'Token is invalid or expired',\n      {\n        Bearer: { realm: 'api', error: 'invalid_token' },\n      },\n    )\n  }\n\n  return { did: session.did, scope: session.scope }\n}\n\n// Use with handlers\nrouter.add(app.bsky.feed.post.create, {\n  auth: requireAuth,\n  handler: async ({ credentials, input }) => {\n    // credentials.did is available here\n    const post = await db.createPost(credentials.did, input.body)\n    return { body: post }\n  },\n})\n```\n\nThe auth function:\n\n1. Is called **before** parsing the request body\n2. Receives `params`, `request`, and `connection` info\n3. Should throw `LexServerError` (or `LexServerAuthError`) on failure\n4. Returns credentials that are passed to the handler\n\n### WWW-Authenticate Headers\n\nUse `LexServerAuthError` to include `WWW-Authenticate` headers in error responses:\n\n```typescript\nimport { LexServerAuthError } from '@atproto/lex-server'\n\n// Simple Bearer challenge\nthrow new LexServerAuthError('AuthenticationRequired', 'Login required', {\n  Bearer: { realm: 'api' },\n})\n// WWW-Authenticate: Bearer realm=\"api\"\n\n// Multiple schemes\nthrow new LexServerAuthError('AuthenticationRequired', 'Auth required', {\n  Bearer: { realm: 'api', scope: 'read write' },\n  Basic: { realm: 'api' },\n})\n// WWW-Authenticate: Bearer realm=\"api\", scope=\"read write\", Basic realm=\"api\"\n\n// Token68 format\nthrow new LexServerAuthError('AuthenticationRequired', 'Auth required', {\n  Bearer: 'token68value',\n})\n// WWW-Authenticate: Bearer token68value\n```\n\n## Error Handling\n\n### LexServerError\n\nThrow `LexServerError` to return structured XRPC error responses:\n\n```typescript\nimport { LexServerError } from '@atproto/lex-server'\n\nrouter.add(app.bsky.actor.getProfile, async ({ params }) => {\n  try {\n    const profile = await db.getProfile(params.actor)\n    return { body: profile }\n  } catch (cause) {\n    throw new LexServerError(\n      404,\n      {\n        error: 'NotFound',\n        message: 'Profile not found',\n      },\n      {\n        'cache-control': 'max-age=600',\n      },\n      { cause },\n    )\n  }\n})\n```\n\nError responses follow the XRPC format:\n\n```json\n{\n  \"error\": \"NotFound\",\n  \"message\": \"Profile not found\"\n}\n```\n\n### LexServerAuthError\n\n`LexServerAuthError` extends `LexServerError` with `WWW-Authenticate` header support:\n\n```typescript\nimport { LexServerAuthError } from '@atproto/lex-server'\n\nthrow new LexServerAuthError('AuthenticationRequired', 'Invalid credentials', {\n  Bearer: { realm: 'api' },\n})\n```\n\nThis returns a 401 response with the `WWW-Authenticate` header.\n\n### Error Handler Callback\n\nUse `onHandlerError` to log or report unexpected errors:\n\n```typescript\nconst router = new LexRouter({\n  onHandlerError: async ({ error, request, method }) => {\n    // Log errors (excluding expected abort signals)\n    console.error(`Error in ${method.nsid}:`, error)\n    await reportToSentry(error)\n  },\n})\n```\n\n> [!NOTE]\n>\n> The callback is only invoked for unexpected errors, not for `LexError` instances or request aborts.\n\n## Node.js Server\n\nThe `@atproto/lex-server/nodejs` subpath provides Node.js-specific utilities.\n\n### serve()\n\nStart a server and begin listening:\n\n```typescript\nimport { serve } from '@atproto/lex-server/nodejs'\n\nconst server = await serve(router, { port: 3000 })\nconsole.log('Server listening on port 3000')\n\n// Graceful shutdown\nawait server.terminate()\n```\n\nThe server supports `AsyncDisposable`:\n\n```typescript\nawait using server = await serve(router, { port: 3000 })\n// Server is automatically terminated when scope exits\n```\n\nOptions:\n\n```typescript\ntype StartServerOptions = {\n  port?: number\n  host?: string\n  gracefulTerminationTimeout?: number // ms to wait for connections to close\n}\n```\n\n### createServer()\n\nCreate a server without starting it:\n\n```typescript\nimport { createServer } from '@atproto/lex-server/nodejs'\n\nconst server = createServer(router, {\n  gracefulTerminationTimeout: 5000,\n})\n\nserver.listen(3000, () => {\n  console.log('Server listening')\n})\n```\n\n### toRequestListener()\n\nConvert a handler to an Express/Connect-compatible middleware:\n\n```typescript\nimport express from 'express'\nimport { toRequestListener } from '@atproto/lex-server/nodejs'\n\nconst app = express()\n\n// Mount the XRPC router\napp.use('/xrpc', toRequestListener(router.fetch))\n\napp.listen(3000)\n```\n\n### upgradeWebSocket()\n\nRequired for WebSocket subscription support in Node.js:\n\n```typescript\nimport { LexRouter } from '@atproto/lex-server'\nimport { upgradeWebSocket } from '@atproto/lex-server/nodejs'\n\nconst router = new LexRouter({ upgradeWebSocket })\n```\n\n## Advanced Usage\n\n### Custom Response Objects\n\nReturn a `Response` object for full control over the response (disables any kind\nof validation of the body):\n\n```typescript\nrouter.add(schema, async ({ params }) => {\n  if (params.redirect) {\n    return Response.redirect('https://example.com', 302)\n  }\n\n  return Response.json(\n    { custom: true },\n    {\n      status: 201,\n      headers: {\n        'X-Custom-Header': 'value',\n      },\n    },\n  )\n})\n```\n\n### Response Headers\n\nAdd headers to responses:\n\n```typescript\nrouter.add(schema, async ({ params }) => {\n  return {\n    body: { data: 'value' },\n    headers: {\n      'Cache-Control': 'public, max-age=3600',\n      'X-Request-Id': crypto.randomUUID(),\n    },\n  }\n})\n```\n\n### Connection Info\n\nAccess network connection information:\n\n```typescript\nrouter.add(schema, async ({ connection }) => {\n  console.log('Remote address:', connection?.remoteAddr?.hostname)\n  console.log('Local address:', connection?.localAddr?.hostname)\n  return { body: { status: 'ok' } }\n})\n```\n\nConnection info structure:\n\n```typescript\ntype ConnectionInfo = {\n  localAddr?: {\n    hostname: string\n    port: number\n    transport: 'tcp' | 'udp'\n  }\n  remoteAddr?: {\n    hostname: string\n    port: number\n    transport: 'tcp' | 'udp'\n  }\n}\n```\n\n## License\n\nMIT or Apache2\n"
  },
  {
    "path": "packages/lex/lex-server/examples/subscription.mjs",
    "content": "#! /usr/bin/env node\n\n/* eslint-env node */\n/* eslint-disable @typescript-eslint/no-namespace */\n/* eslint-disable n/no-extraneous-import */\n\nimport { scheduler } from 'node:timers/promises'\nimport { l } from '@atproto/lex'\nimport { LexError, LexRouter } from '@atproto/lex-server'\nimport { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'\n\n// This code would typically be generated by @atproto/lex\nconst nsid = 'com.example.echo'\nconst message = l.typedObject(\n  nsid,\n  'message',\n  l.object({\n    message: l.string(),\n    cursor: l.integer({ minimum: 0 }),\n  }),\n)\nconst main = l.subscription(\n  nsid,\n  l.params({\n    message: l.string({ minLength: 1 }),\n    cursor: l.optional(l.withDefault(l.integer({ minimum: 0 }), 0)),\n    limit: l.optional(\n      l.withDefault(l.integer({ minimum: 1, maximum: 100 }), 10),\n    ),\n  }),\n  l.typedUnion([l.typedRef(() => message)], false),\n  ['LimitReached'],\n)\nconst com = { example: { echo: { main, message } } }\n\nconst router = new LexRouter({\n  upgradeWebSocket,\n  fallback: indexHtml,\n  onHandlerError: ({ error, method }) => {\n    console.error(`Handler error in method ${method.nsid}:`, error)\n  },\n})\n  //\n  .add(com.example.echo, async function* ({ signal, params }) {\n    const { message, cursor, limit } = params\n\n    for (let i = 0; i < limit; i++) {\n      yield com.example.echo.message.$build({\n        message: message,\n        cursor: cursor + i,\n      })\n\n      // Wait 1 second between messages (stop waiting if the request is aborted)\n      await scheduler.wait(1_000, { signal })\n    }\n\n    throw new LexError('LimitReached', `Limit of ${limit} messages reached`)\n  })\n\nserve(router.fetch, { port: 8080 })\n\nasync function indexHtml() {\n  return new Response(\n    html`\n      <h1>Open dev tools and look at the console</h1>\n      <script type=\"module\">\n        import { decodeMultiple } from 'https://cdn.jsdelivr.net/npm/cbor-x@1.6.0/+esm'\n\n        const host = window.location.host\n        const nsid = 'com.example.echo'\n        const params = new URLSearchParams(window.location.search)\n        if (!params.has('message')) {\n          params.set('message', 'Hello, world!')\n        }\n\n        const url = 'ws://' + host + '/xrpc/' + nsid + '?' + params.toString()\n\n        const ws = new WebSocket(url)\n        ws.binaryType = 'arraybuffer'\n\n        ws.addEventListener('message', async (event) => {\n          const bytes = new Uint8Array(event.data)\n          let { length, 0: header, 1: data } = await decodeMultiple(bytes)\n          if (length !== 2) {\n            console.warn('Invalid message format', bytes)\n          } else if (header.op === 1) {\n            if (\n              data &&\n              typeof data === 'object' &&\n              typeof header.t === 'string' &&\n              !('$type' in data)\n            ) {\n              data.$type = header.t.startsWith('#') ? nsid + header.t : header.t\n            }\n\n            console.log('Message frame', data)\n          } else if (header.op === -1) {\n            console.warn('Error frame', data)\n          } else {\n            console.warn('Unknown message', header, data)\n          }\n        })\n\n        ws.addEventListener('close', (event) => {\n          console.info('Closed', {\n            code: event.code,\n            reason: event.reason,\n            wasClean: event.wasClean,\n          })\n        })\n\n        setTimeout(() => {\n          ws.close()\n        }, 20_000)\n\n        // Expose for debugging\n        window.ws = ws\n      </script>\n    `,\n    {\n      status: 200,\n      headers: { 'content-type': 'text/html' },\n    },\n  )\n}\n\n/**\n * Simple HTML template tag function to enable syntax highlighting.\n * @param {TemplateStringsArray} parts\n * @param {...never} args\n * @returns {string}\n */\nfunction html(parts, ...args) {\n  if (args.length) throw new Error('No substitutions allowed in HTML template')\n  return parts[0]\n}\n"
  },
  {
    "path": "packages/lex/lex-server/nodejs.d.ts",
    "content": "export * from './dist/nodejs.js'\n"
  },
  {
    "path": "packages/lex/lex-server/nodejs.js",
    "content": "/* eslint-env node, commonjs */\n\n'use strict'\n\nmodule.exports = require('./dist/nodejs.js')\n"
  },
  {
    "path": "packages/lex/lex-server/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-server\",\n  \"version\": \"0.0.14\",\n  \"license\": \"MIT\",\n  \"description\": \"Request router for Atproto Lexicon protocols and schemas\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\",\n    \"router\",\n    \"typescript\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex/lex-server\"\n  },\n  \"files\": [\n    \"./src\",\n    \"./tsconfig.build.json\",\n    \"./tsconfig.tests.json\",\n    \"./tsconfig.json\",\n    \"./dist\",\n    \"./nodejs.js\",\n    \"./CHANGELOG.md\"\n  ],\n  \"sideEffects\": false,\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./nodejs\": {\n      \"types\": \"./dist/nodejs.d.ts\",\n      \"import\": \"./dist/nodejs.js\",\n      \"require\": \"./dist/nodejs.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/did-resolver\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"@atproto/lex-client\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-json\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"http-terminator\": \"^3.2.0\",\n    \"tslib\": \"^2.8.1\",\n    \"ws\": \"^8.18.3\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex\": \"workspace:^\",\n    \"@atproto/lex-client\": \"workspace:^\",\n    \"@types/ws\": \"^8.18.1\",\n    \"@vitest/coverage-v8\": \"4.0.16\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/errors.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { XrpcInternalError, XrpcResponseError } from '@atproto/lex-client'\nimport { LexError } from '@atproto/lex-data'\nimport { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'\nimport { LexServerAuthError, LexServerError } from './errors.js'\n\n// Minimal method fixtures for XrpcError subclasses\nconst testQuery = l.query(\n  'io.example.test',\n  l.params(),\n  l.payload('application/json', l.object({ value: l.string() })),\n)\n\ndescribe(LexServerError, () => {\n  it('stores status, body, and headers', () => {\n    const error = new LexServerError(\n      400,\n      { error: 'InvalidRequest', message: 'Bad input' },\n      { 'X-Custom': 'header' },\n    )\n\n    expect(error.status).toBe(400)\n    expect(error.body).toEqual({\n      error: 'InvalidRequest',\n      message: 'Bad input',\n    })\n    expect(error.headers?.get('x-custom')).toBe('header')\n    expect(error.error).toBe('InvalidRequest')\n    expect(error.message).toBe('Bad input')\n  })\n\n  it('has undefined headers when none provided', () => {\n    const error = new LexServerError(500, { error: 'InternalError' })\n    expect(error.headers).toBeUndefined()\n  })\n\n  it('toJSON returns the body', () => {\n    const body = { error: 'TestError' as const, message: 'test' }\n    const error = new LexServerError(400, body)\n    expect(error.toJSON()).toEqual(body)\n  })\n\n  it('toResponse creates a Response with correct status and body', async () => {\n    const error = new LexServerError(\n      422,\n      { error: 'ValidationError', message: 'Invalid data' },\n      { 'X-Test': 'yes' },\n    )\n    const response = error.toResponse()\n\n    expect(response.status).toBe(422)\n    expect(response.headers.get('X-Test')).toBe('yes')\n    expect(await response.json()).toEqual({\n      error: 'ValidationError',\n      message: 'Invalid data',\n    })\n  })\n\n  describe('from()', () => {\n    it('returns existing LexServerError as-is', () => {\n      const original = new LexServerError(400, { error: 'Test' })\n      expect(LexServerError.from(original)).toBe(original)\n    })\n\n    it('returns a LexServerAuthError as-is (since it extends LexServerError)', () => {\n      const original = new LexServerAuthError('AuthenticationRequired', 'test')\n      expect(LexServerError.from(original)).toBe(original)\n    })\n\n    it('converts XrpcError to downstream LexServerError', () => {\n      const xrpcError = new XrpcInternalError(testQuery, 'Something broke')\n      const serverError = LexServerError.from(xrpcError)\n\n      expect(serverError).toBeInstanceOf(LexServerError)\n      expect(serverError.status).toBe(500)\n      expect(serverError.body.error).toBe('InternalServerError')\n      expect(serverError.cause).toBe(xrpcError)\n    })\n\n    it('converts XrpcResponseError with 500 to 502', () => {\n      const response = new Response(null, { status: 500 })\n      const xrpcError = new XrpcResponseError(testQuery, response, {\n        encoding: 'application/json',\n        body: { error: 'Boom', message: 'Try again later' },\n      })\n      const serverError = LexServerError.from(xrpcError)\n\n      expect(serverError.status).toBe(502)\n      expect(serverError.body.error).toBe('Boom')\n    })\n\n    it('preserves the status of non-500 5xx errors', () => {\n      const response = new Response(null, { status: 502 })\n      const xrpcError = new XrpcResponseError(testQuery, response, {\n        encoding: 'application/json',\n        body: { error: 'FooBar', message: 'Try again later' },\n      })\n      const serverError = LexServerError.from(xrpcError)\n\n      expect(serverError.status).toBe(502)\n      expect(serverError.body.error).toBe('FooBar')\n    })\n\n    it('converts XrpcResponseError with 4xx preserving status', () => {\n      const response = new Response(null, { status: 404 })\n      const xrpcError = new XrpcResponseError(testQuery, response, {\n        encoding: 'application/json',\n        body: { error: 'NotFound', message: 'Record not found' },\n      })\n      const serverError = LexServerError.from(xrpcError)\n\n      expect(serverError.status).toBe(404)\n      expect(serverError.body.error).toBe('NotFound')\n    })\n\n    it('converts LexValidationError to 400', () => {\n      const validationError = new LexValidationError([\n        new IssueInvalidType([], 'hello', ['number']),\n      ])\n      const serverError = LexServerError.from(validationError)\n\n      expect(serverError.status).toBe(400)\n      expect(serverError.body.error).toBe('InvalidRequest')\n      expect(serverError.cause).toBe(validationError)\n    })\n\n    it('converts plain LexError to 500', () => {\n      const lexError = new LexError('CustomError', 'Something happened')\n      const serverError = LexServerError.from(lexError)\n\n      expect(serverError.status).toBe(500)\n      expect(serverError.body.error).toBe('CustomError')\n      expect(serverError.cause).toBe(lexError)\n    })\n\n    it('converts unknown errors to 500 InternalServerError', () => {\n      const serverError = LexServerError.from(new TypeError('oops'))\n\n      expect(serverError.status).toBe(500)\n      expect(serverError.body.error).toBe('InternalServerError')\n      expect(serverError.body.message).toBe('An internal error occurred')\n    })\n\n    it('converts non-Error values to 500 InternalServerError', () => {\n      const serverError = LexServerError.from('string error')\n\n      expect(serverError.status).toBe(500)\n      expect(serverError.body.error).toBe('InternalServerError')\n    })\n  })\n})\n\ndescribe(LexServerAuthError, () => {\n  it('always has status 401', () => {\n    const error = new LexServerAuthError(\n      'AuthenticationRequired',\n      'Token expired',\n    )\n    expect(error.status).toBe(401)\n  })\n\n  it('sets WWW-Authenticate header', () => {\n    const error = new LexServerAuthError(\n      'AuthenticationRequired',\n      'Token required',\n      { Bearer: { realm: 'api.example.com', error: 'InvalidToken' } },\n    )\n    const header = error.headers?.get('WWW-Authenticate')\n    expect(header).toContain('Bearer')\n    expect(header).toContain('realm=\"api.example.com\"')\n    expect(header).toContain('error=\"InvalidToken\"')\n  })\n\n  it('sets Access-Control-Expose-Headers for CORS', () => {\n    const error = new LexServerAuthError(\n      'AuthenticationRequired',\n      'Token required',\n      {\n        Bearer: { realm: 'api.example.com', error: 'InvalidToken' },\n      },\n    )\n    expect(error.headers?.get('Access-Control-Expose-Headers')).toBe(\n      'WWW-Authenticate',\n    )\n    expect(error.headers?.get('WWW-Authenticate')).toBe(\n      'Bearer realm=\"api.example.com\", error=\"InvalidToken\"',\n    )\n  })\n\n  it('does not set WWW-Authenticate header if wwwAuthenticate is empty', () => {\n    const error = new LexServerAuthError('AuthenticationRequired', 'No token')\n    expect(error.headers).toBeUndefined()\n  })\n\n  it('toResponse returns 401 with proper headers', async () => {\n    const error = new LexServerAuthError(\n      'AuthenticationRequired',\n      'Missing token',\n      { Bearer: { error: 'MissingToken' } },\n    )\n    const response = error.toResponse()\n\n    expect(response.status).toBe(401)\n    expect(response.headers.get('WWW-Authenticate')).toBe(\n      'Bearer error=\"MissingToken\"',\n    )\n    const body = await response.json()\n    expect(body.error).toBe('AuthenticationRequired')\n    expect(body.message).toBe('Missing token')\n  })\n\n  describe('from()', () => {\n    it('returns existing LexServerAuthError as-is', () => {\n      const original = new LexServerAuthError('AuthenticationRequired', 'test')\n      expect(LexServerAuthError.from(original)).toBe(original)\n    })\n\n    it('wraps a LexServerError using its error code and message', () => {\n      const serverError = new LexServerError(403, {\n        error: 'Forbidden',\n        message: 'Access denied',\n      })\n      const authError = LexServerAuthError.from(serverError, {\n        Bearer: { error: 'InsufficientScope' },\n      })\n\n      expect(authError).toBeInstanceOf(LexServerAuthError)\n      expect(authError.error).toBe('Forbidden')\n      expect(authError.message).toBe('Access denied')\n      expect(authError.cause).toBe(serverError)\n      expect(authError.headers?.get('WWW-Authenticate')).toBe(\n        'Bearer error=\"InsufficientScope\"',\n      )\n    })\n\n    it('wraps a LexError preserving error code and message', () => {\n      const lexError = new LexError('ExpiredToken', 'Token has expired')\n      const authError = LexServerAuthError.from(lexError, {\n        Bearer: { error: 'ExpiredToken' },\n      })\n\n      expect(authError).toBeInstanceOf(LexServerAuthError)\n      expect(authError.error).toBe('ExpiredToken')\n      expect(authError.message).toBe('Token has expired')\n      expect(authError.cause).toBe(lexError)\n    })\n\n    it('wraps unknown errors with default error code', () => {\n      const authError = LexServerAuthError.from(new Error('something'))\n\n      expect(authError.error).toBe('AuthenticationRequired')\n      expect(authError.message).toBe('Authentication failed')\n    })\n\n    it('wraps non-Error values with default error code', () => {\n      const authError = LexServerAuthError.from(null)\n\n      expect(authError.error).toBe('AuthenticationRequired')\n      expect(authError.message).toBe('Authentication failed')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-server/src/errors.ts",
    "content": "import { XrpcError } from '@atproto/lex-client'\nimport { LexError, LexErrorCode, LexErrorData } from '@atproto/lex-data'\nimport { LexValidationError } from '@atproto/lex-schema'\nimport {\n  WWWAuthenticate,\n  formatWWWAuthenticateHeader,\n} from './lib/www-authenticate.js'\n\nexport { LexError }\nexport type { LexErrorCode, LexErrorData, WWWAuthenticate }\n\n/**\n * Base error class for representing errors that should be converted to XRPC\n * error responses.\n */\nexport class LexServerError<\n  N extends LexErrorCode = LexErrorCode,\n> extends LexError<N> {\n  name = 'LexServerError'\n\n  readonly headers?: Headers\n\n  constructor(\n    readonly status: number,\n    readonly body: LexErrorData<N>,\n    headers?: HeadersInit,\n    options?: ErrorOptions,\n  ) {\n    super(body.error, body.message, options)\n    this.headers = headers ? new Headers(headers) : undefined\n  }\n\n  override toJSON(): LexErrorData<N> {\n    return this.body\n  }\n\n  public toResponse(): Response {\n    const { status, headers } = this\n    // @NOTE using this.toJSON() instead of this.body to allow overrides in subclasses\n    return Response.json(this.toJSON(), { status, headers })\n  }\n\n  static from(cause: unknown): LexServerError {\n    if (cause instanceof LexServerError) {\n      return cause\n    }\n\n    // Convert @atproto/lex-client errors to downstream LexServerError\n    if (cause instanceof XrpcError) {\n      const { status, body, headers } = cause.toDownstreamError()\n      return new LexServerError(status, body, headers, { cause })\n    }\n\n    // Convert @atproto/lex-schema validation errors to 400 Bad Request\n    if (cause instanceof LexValidationError) {\n      return new LexServerError(400, cause.toJSON(), undefined, {\n        cause,\n      })\n    }\n\n    // Any other error is treated as a generic 500 Internal Server Error\n    if (cause instanceof LexError) {\n      return new LexServerError(500, cause.toJSON(), undefined, {\n        cause,\n      })\n    }\n\n    return new LexServerError(\n      500,\n      { error: 'InternalServerError', message: 'An internal error occurred' },\n      undefined,\n      { cause },\n    )\n  }\n}\n\n/**\n * Error class for authentication failures in XRPC server handlers.\n *\n * Extends {@link LexError} to include WWW-Authenticate header support,\n * which is required by HTTP authentication standards (RFC 7235).\n * The error automatically generates the appropriate 401 response with\n * the WWW-Authenticate header when converted to a Response.\n *\n * @typeParam N - The Lexicon error code type\n *\n * @example Throwing an auth error\n * ```typescript\n * import { LexServerAuthError } from '@atproto/lex-server'\n *\n * throw new LexServerAuthError(\n *   'AuthenticationRequired',\n *   'Invalid or expired token',\n *   { Bearer: { error: 'InvalidToken', realm: 'api.example.com' } }\n * )\n * ```\n */\nexport class LexServerAuthError<\n  N extends LexErrorCode = LexErrorCode,\n> extends LexServerError<N> {\n  name = 'LexServerAuthError'\n\n  /**\n   * Creates a new authentication error.\n   *\n   * @param error - The Lexicon error code (e.g., 'AuthenticationRequired')\n   * @param message - Human-readable error message\n   * @param wwwAuthenticate - WWW-Authenticate header parameters\n   * @param options - Standard Error options including `cause`\n   */\n  constructor(\n    error: N,\n    message: string,\n    readonly wwwAuthenticate: WWWAuthenticate = {},\n    options?: ErrorOptions,\n  ) {\n    const headers = Object.keys(wwwAuthenticate).length\n      ? new Headers({\n          'WWW-Authenticate': formatWWWAuthenticateHeader(wwwAuthenticate),\n          'Access-Control-Expose-Headers': 'WWW-Authenticate', // CORS\n        })\n      : undefined\n    super(401, { error, message }, headers, options)\n  }\n\n  /**\n   * Creates a LexServerAuthError from an existing LexError.\n   *\n   * If the input is already a LexServerAuthError, returns it unchanged.\n   * Otherwise, wraps the error with the provided WWW-Authenticate parameters.\n   *\n   * @param cause - The original LexError to wrap\n   * @param wwwAuthenticate - WWW-Authenticate header parameters\n   * @returns A LexServerAuthError instance\n   *\n   * @example\n   * ```typescript\n   * function authenticate(token: string): Promise<User> {\n   *   try {\n   *     return await validateToken(token)\n   *   } catch (cause) {\n   *     throw LexServerAuthError.from(cause, {\n   *       Bearer: { error: 'InvalidToken' }\n   *     })\n   *   }\n   * }\n   * ```\n   */\n  static from(\n    cause: unknown,\n    wwwAuthenticate?: WWWAuthenticate,\n  ): LexServerAuthError {\n    if (cause instanceof LexServerAuthError) {\n      return cause\n    }\n\n    if (cause instanceof LexError) {\n      return new LexServerAuthError(\n        cause.error,\n        cause.message,\n        wwwAuthenticate,\n        { cause },\n      )\n    }\n\n    return new LexServerAuthError(\n      'AuthenticationRequired',\n      'Authentication failed',\n      wwwAuthenticate,\n      { cause },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/index.ts",
    "content": "export * from './errors.js'\nexport * from './lex-router.js'\nexport * from './service-auth.js'\n"
  },
  {
    "path": "packages/lex/lex-server/src/lex-router.test.ts",
    "content": "import { AddressInfo } from 'node:net'\nimport { scheduler } from 'node:timers/promises'\nimport { describe, expect, it, vi } from 'vitest'\nimport { WebSocket } from 'ws'\nimport { decodeAll } from '@atproto/lex-cbor'\nimport { buildAgent, xrpc } from '@atproto/lex-client'\nimport { parseCid } from '@atproto/lex-data'\nimport { l } from '@atproto/lex-schema'\nimport { LexError, LexServerAuthError, LexServerError } from './errors.js'\nimport {\n  ConnectionInfo,\n  HandlerErrorHook,\n  HealthCheckHandler,\n  LexRouter,\n  LexRouterAuth,\n  LexRouterMethodHandler,\n  SocketErrorHook,\n} from './lex-router.js'\nimport { serve, upgradeWebSocket } from './nodejs.js'\n\n// ============================================================================\n// Schema Definitions\n// ============================================================================\n\nconst io = {\n  example: {\n    echo: l.procedure(\n      'io.example.echo',\n      l.params(),\n      l.payload('*/*'),\n      l.payload('*/*'),\n    ),\n    status: l.query(\n      'io.example.status',\n      l.params(),\n      l.payload('application/json', l.object({ status: l.string() })),\n    ),\n    ipld: l.procedure(\n      'io.example.ipld',\n      l.params(),\n      l.payload(\n        'application/json',\n        l.object({\n          cid: l.cid(),\n          bytes: l.bytes(),\n        }),\n      ),\n      l.payload(\n        'application/json',\n        l.object({\n          cid: l.cid(),\n          bytes: l.bytes(),\n        }),\n      ),\n    ),\n    paramsToBody: l.query(\n      'io.example.paramsToBody',\n      l.params({\n        name: l.string(),\n        pronouns: l.array(l.string()),\n      }),\n      l.payload(\n        'application/json',\n        l.object({\n          params: l.object({\n            name: l.string(),\n            pronouns: l.array(l.string()),\n          }),\n        }),\n      ),\n    ),\n  },\n}\n\nconst handlers: {\n  [K in keyof typeof io.example]: LexRouterMethodHandler<(typeof io.example)[K]>\n} = {\n  echo: async ({ input }) => ({\n    encoding: input.encoding,\n    body: input.body.body!,\n  }),\n  status: async () => ({ body: { status: 'ok' } }),\n  ipld: async ({ input }) => ({ body: input.body! }),\n  paramsToBody: async ({ params }) => ({ body: { params } }),\n}\n\n// ============================================================================\n// Basic LexRouter Tests\n// ============================================================================\n\ndescribe(LexRouter, () => {\n  it('returns MethodNotImplemented when the route is not found', async () => {\n    const router = new LexRouter()\n    const request = new Request(`https://example.com/xrpc/foo.bar.baz`)\n    const response = await router.fetch(request)\n    expect(response.status).toBe(501)\n    expect(await response.json()).toMatchObject({\n      error: 'MethodNotImplemented',\n    })\n  })\n\n  it('streams payloads', async () => {\n    const router = new LexRouter().add(io.example.echo, handlers.echo)\n    const request = new Request('https://example.com/xrpc/io.example.echo', {\n      method: 'POST',\n      headers: { 'content-type': 'text/plain' },\n      // @ts-expect-error\n      duplex: 'half',\n      body: new ReadableStream({\n        start(controller) {\n          setTimeout(() => {\n            controller.enqueue(new TextEncoder().encode('aaa'))\n            setTimeout(() => {\n              controller.enqueue(new TextEncoder().encode('bbb'))\n              setTimeout(() => {\n                controller.error(new Error('Stream closed'))\n              }, 50)\n            }, 50)\n          }, 50)\n        },\n      }),\n    })\n    const response = await router.fetch(request)\n\n    const reader = response.body!.getReader()\n    const chunks: string[] = []\n    try {\n      // eslint-disable-next-line no-constant-condition\n      while (true) {\n        const { done, value } = await reader.read()\n        if (done) break\n        chunks.push(new TextDecoder().decode(value))\n      }\n    } catch (err) {\n      expect((err as Error).message).toBe('Stream closed')\n    }\n    expect(chunks).toEqual(['aaa', 'bbb'])\n  })\n\n  it('maps params to body', async () => {\n    const router = new LexRouter().add(\n      io.example.paramsToBody,\n      handlers.paramsToBody,\n    )\n\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramsToBody?name=Alice&pronouns=she%2Fher&pronouns=they%2Fthem',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(await response.json()).toEqual({\n      params: {\n        name: 'Alice',\n        pronouns: ['she/her', 'they/them'],\n      },\n    })\n  })\n})\n\ndescribe('lex-client integration', () => {\n  const router = new LexRouter()\n    .add(io.example.echo, handlers.echo)\n    .add(io.example.status, handlers.status)\n\n  it('echoes text', async () => {\n    const agent = buildAgent({\n      fetch: async (input, init) => {\n        const request = new Request(input, init)\n        return router.fetch(request)\n      },\n      service: 'https://example.com',\n    })\n    const message = 'Hello, LexRouter!'\n    const response = await xrpc(agent, io.example.echo, {\n      body: message,\n      encoding: 'text/plain',\n    })\n    const responseText = new TextDecoder().decode(response.body)\n    expect(responseText).toBe(message)\n    expect(response.encoding).toBe('text/plain')\n  })\n\n  it('streams text', async () => {\n    const agent = buildAgent({\n      fetch: async (input, init) => {\n        const request = new Request(input, init)\n        return router.fetch(request)\n      },\n      service: 'https://example.com',\n    })\n    const message = 'Hello, LexRouter Stream!'\n    const response = await xrpc(agent, io.example.echo, {\n      body: new ReadableStream({\n        start(controller) {\n          controller.enqueue(new TextEncoder().encode(message))\n          controller.close()\n        },\n      }),\n      encoding: 'text/plain',\n    })\n    const responseText = new TextDecoder().decode(response.body)\n    expect(responseText).toBe(message)\n    expect(response.encoding).toBe('text/plain')\n  })\n\n  it('performs simple query', async () => {\n    const agent = buildAgent({\n      fetch: async (input, init) => {\n        const request = new Request(input, init)\n        return router.fetch(request)\n      },\n      service: 'https://example.com',\n    })\n    const response = await xrpc(agent, io.example.status)\n    expect(response.success).toBe(true)\n    expect(response.status).toBe(200)\n    expect(response.encoding).toBe('application/json')\n    expect(response.body.status).toBe('ok')\n  })\n})\n\ndescribe('IPLD values', () => {\n  it('can send and receive ipld vals', async () => {\n    const ipldHandler: LexRouterMethodHandler<typeof io.example.ipld> = vi.fn(\n      async ({ input }) => {\n        return { body: input.body! }\n      },\n    )\n\n    const router = new LexRouter().add(io.example.ipld, ipldHandler)\n\n    const agent = buildAgent({\n      fetch: async (input, init) => {\n        const request = new Request(input, init)\n        return router.fetch(request)\n      },\n      service: 'https://example.com',\n    })\n\n    const cid = parseCid(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    )\n\n    const bytes = new Uint8Array([0, 1, 2, 3])\n\n    const response = await xrpc(agent, io.example.ipld, {\n      body: { cid, bytes },\n    })\n\n    expect(ipldHandler).toHaveBeenCalledTimes(1)\n    expect(response.success).toBe(true)\n    expect(response.encoding).toBe('application/json')\n    expect(response.body.cid.equals(cid)).toBe(true)\n    expect(response.body.bytes).toEqual(bytes)\n  })\n})\n\n// ============================================================================\n// Authentication Tests (ported from xrpc-server/tests/auth.test.ts)\n// ============================================================================\n\ndescribe('Authentication', () => {\n  // Basic auth schema\n  const io = {\n    example: {\n      authTest: l.procedure(\n        'io.example.authTest',\n        l.params(),\n        l.payload(\n          'application/json',\n          l.object({\n            present: l.literal(true),\n          }),\n        ),\n        l.payload(\n          'application/json',\n          l.object({\n            username: l.string(),\n            original: l.string(),\n          }),\n        ),\n      ),\n    },\n  }\n\n  type BasicAuthCredentials = {\n    username: string\n    original: string\n  }\n\n  function createBasicAuth(allowed: {\n    username: string\n    password: string\n  }): LexRouterAuth<BasicAuthCredentials> {\n    return async ({ request }) => {\n      const header = request.headers.get('authorization') ?? ''\n      if (!header.startsWith('Basic ')) {\n        throw new LexServerAuthError(\n          'AuthenticationRequired',\n          'Authentication required',\n        )\n      }\n      const original = header.slice(6)\n      const decoded = Buffer.from(original, 'base64').toString()\n      // @NOTE not using .split(':') to allow colons in password\n      const colonIndex = decoded.indexOf(':')\n      const [username, password] =\n        colonIndex === -1\n          ? [decoded, '']\n          : [decoded.slice(0, colonIndex), decoded.slice(colonIndex + 1)]\n      if (username !== allowed.username || password !== allowed.password) {\n        throw new LexServerAuthError(\n          'AuthenticationRequired',\n          'Invalid credentials',\n        )\n      }\n      return { username, original }\n    }\n  }\n\n  function basicAuth(creds: { username: string; password: string }) {\n    return `Basic ${Buffer.from(`${creds.username}:${creds.password}`).toString('base64')}`\n  }\n\n  const authTestHandler: LexRouterMethodHandler<\n    typeof io.example.authTest,\n    BasicAuthCredentials\n  > = async ({ credentials }) => ({\n    body: {\n      username: credentials.username,\n      original: credentials.original,\n    },\n  })\n\n  it('fails on bad auth before invalid request payload', async () => {\n    const router = new LexRouter().add(io.example.authTest, {\n      auth: createBasicAuth({ username: 'admin', password: 'password' }),\n      handler: authTestHandler,\n    })\n\n    const request = new Request(\n      'https://example.com/xrpc/io.example.authTest',\n      {\n        method: 'POST',\n        headers: {\n          'content-type': 'application/json',\n          authorization: basicAuth({ username: 'admin', password: 'wrong' }),\n        },\n        body: JSON.stringify({ present: false }),\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(401)\n    const data = await response.json()\n    expect(data.error).toBe('AuthenticationRequired')\n  })\n\n  it('fails on invalid request payload after good auth', async () => {\n    const router = new LexRouter().add(io.example.authTest, {\n      auth: createBasicAuth({ username: 'admin', password: 'password' }),\n      handler: authTestHandler,\n    })\n\n    const request = new Request(\n      'https://example.com/xrpc/io.example.authTest',\n      {\n        method: 'POST',\n        headers: {\n          'content-type': 'application/json',\n          authorization: basicAuth({ username: 'admin', password: 'password' }),\n        },\n        body: JSON.stringify({ present: false }),\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.error).toBe('InvalidRequest')\n  })\n\n  it('succeeds on good auth and payload', async () => {\n    const router = new LexRouter().add(io.example.authTest, {\n      auth: createBasicAuth({ username: 'admin', password: 'password' }),\n      handler: authTestHandler,\n    })\n\n    const request = new Request(\n      'https://example.com/xrpc/io.example.authTest',\n      {\n        method: 'POST',\n        headers: {\n          'content-type': 'application/json',\n          authorization: basicAuth({ username: 'admin', password: 'password' }),\n        },\n        body: JSON.stringify({ present: true }),\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    const data = await response.json()\n    expect(data.username).toBe('admin')\n    expect(data.original).toBe('YWRtaW46cGFzc3dvcmQ=')\n  })\n\n  it('handles missing auth header', async () => {\n    const router = new LexRouter().add(io.example.authTest, {\n      auth: createBasicAuth({ username: 'admin', password: 'password' }),\n      handler: authTestHandler,\n    })\n\n    const request = new Request(\n      'https://example.com/xrpc/io.example.authTest',\n      {\n        method: 'POST',\n        headers: { 'content-type': 'application/json' },\n        body: JSON.stringify({ present: true }),\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(401)\n    const data = await response.json()\n    expect(data.error).toBe('AuthenticationRequired')\n  })\n})\n\n// ============================================================================\n// Error Handling Tests (ported from xrpc-server/tests/errors.test.ts)\n// ============================================================================\n\ndescribe('Error Handling', () => {\n  const io = {\n    example: {\n      error: l.query(\n        'io.example.error',\n        l.params({\n          which: l.optional(l.string()),\n        }),\n        l.payload(),\n      ),\n      throwFalsyValue: l.query(\n        'io.example.throwFalsyValue',\n        l.params(),\n        l.payload(),\n      ),\n      invalidResponse: l.query(\n        'io.example.invalidResponse',\n        l.params(),\n        l.payload(\n          'application/json',\n          l.object({\n            expectedValue: l.string(),\n          }),\n        ),\n      ),\n    },\n  }\n\n  describe('Custom Errors', () => {\n    it('throws custom error using LexError', async () => {\n      const handler: LexRouterMethodHandler<typeof io.example.error> = async ({\n        params,\n      }) => {\n        if (params.which === 'foo') {\n          throw new LexServerError(400, {\n            error: 'Foo',\n            message: 'It was this one!',\n          })\n        }\n        return {}\n      }\n\n      const router = new LexRouter().add(io.example.error, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.error?which=foo',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('Foo')\n      expect(data.message).toBe('It was this one!')\n    })\n\n    it('returns custom error via Response object', async () => {\n      const handler: LexRouterMethodHandler<typeof io.example.error> = async ({\n        params,\n      }) => {\n        if (params.which === 'bar') {\n          return Response.json(\n            { error: 'Bar', message: 'It was that one!' },\n            { status: 400 },\n          )\n        }\n        return {}\n      }\n\n      const router = new LexRouter().add(io.example.error, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.error?which=bar',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('Bar')\n      expect(data.message).toBe('It was that one!')\n    })\n\n    it('handles falsy values thrown as InternalServerError', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.throwFalsyValue\n      > = async () => {\n        throw ''\n      }\n\n      const router = new LexRouter().add(io.example.throwFalsyValue, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.throwFalsyValue',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(500)\n      const data = await response.json()\n      expect(data.error).toBe('InternalServerError')\n    })\n  })\n\n  describe('HTTP Method Mismatches', () => {\n    it('rejects POST for query endpoints', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.error\n      > = async () => ({})\n\n      const router = new LexRouter().add(io.example.error, handler)\n\n      const request = new Request('https://example.com/xrpc/io.example.error', {\n        method: 'POST',\n      })\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(405)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n      expect(data.message).toBe('Method not allowed')\n    })\n\n    it('rejects GET for procedure endpoints', async () => {\n      const procedure = l.procedure(\n        'io.example.procedure',\n        l.params(),\n        l.payload('application/json', l.object({ data: l.string() })),\n        l.payload(),\n      )\n\n      const handler: LexRouterMethodHandler<typeof procedure> = async () => ({})\n\n      const router = new LexRouter().add(procedure, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.procedure',\n        { method: 'GET' },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(405)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n      expect(data.message).toBe('Method not allowed')\n    })\n  })\n\n  describe('Method Not Found', () => {\n    it('returns MethodNotImplemented for non-existent methods', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.doesNotExist',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(501)\n      expect(await response.json()).toMatchObject({\n        error: 'MethodNotImplemented',\n      })\n    })\n  })\n\n  describe('Custom Error Handlers', () => {\n    it('allows custom onHandlerError handler', async () => {\n      const onHandlerError = vi.fn<HandlerErrorHook>()\n      const customRouter = new LexRouter({\n        onHandlerError,\n      })\n\n      const handler: LexRouterMethodHandler<\n        typeof io.example.error\n      > = async () => {\n        throw new Error('Test error')\n      }\n\n      customRouter.add(io.example.error, handler)\n\n      const request = new Request('https://example.com/xrpc/io.example.error')\n      const response = await customRouter.fetch(request)\n\n      expect(onHandlerError).toHaveBeenCalled()\n      expect(response.status).toBe(500)\n    })\n  })\n})\n\n// ============================================================================\n// Routing Tests\n// ============================================================================\n\ndescribe('Routing', () => {\n  describe('non-/xrpc/ paths', () => {\n    it('returns 404 for non-xrpc paths without fallback', async () => {\n      const router = new LexRouter()\n      const request = new Request('https://example.com/health')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(404)\n      expect(await response.text()).toBe('Not Found')\n    })\n\n    it('delegates to fallback handler for non-xrpc paths', async () => {\n      const fallback = vi.fn(async () => new Response('OK from fallback'))\n      const router = new LexRouter({ fallback })\n\n      const request = new Request('https://example.com/health')\n      const connection: ConnectionInfo = {\n        completed: Promise.resolve(),\n        remoteAddr: { hostname: '127.0.0.1', port: 3000, transport: 'tcp' },\n      }\n      const response = await router.fetch(request, connection)\n\n      expect(fallback).toHaveBeenCalledWith(request, connection)\n      expect(response.status).toBe(200)\n      expect(await response.text()).toBe('OK from fallback')\n    })\n  })\n\n  describe('/xrpc/_health endpoint', () => {\n    it('returns default health check response', async () => {\n      const router = new LexRouter()\n      const request = new Request('https://example.com/xrpc/_health')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      expect(await response.json()).toEqual({ status: 'ok' })\n    })\n\n    it('calls custom healthCheck handler', async () => {\n      const healthCheck = vi.fn<HealthCheckHandler>(async () => ({\n        status: 'ok',\n        version: '1.0.0',\n      }))\n      const router = new LexRouter({ healthCheck })\n\n      const request = new Request('https://example.com/xrpc/_health')\n      const response = await router.fetch(request)\n\n      expect(healthCheck).toHaveBeenCalledWith(request)\n      expect(response.status).toBe(200)\n      expect(await response.json()).toEqual({ status: 'ok', version: '1.0.0' })\n    })\n\n    it('returns 405 for non-GET requests', async () => {\n      const router = new LexRouter()\n      const request = new Request('https://example.com/xrpc/_health', {\n        method: 'POST',\n      })\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(405)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n      expect(data.message).toBe('Method not allowed')\n    })\n\n    it('returns 400 when atproto-proxy header is set', async () => {\n      const router = new LexRouter()\n      const request = new Request('https://example.com/xrpc/_health', {\n        headers: { 'atproto-proxy': 'did:plc:example#atproto_labeler' },\n      })\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n      expect(data.message).toContain('atproto-proxy')\n    })\n\n    it('does not call healthCheck when atproto-proxy is set', async () => {\n      const healthCheck = vi.fn<HealthCheckHandler>(async () => ({\n        status: 'ok',\n      }))\n      const router = new LexRouter({ healthCheck })\n      const request = new Request('https://example.com/xrpc/_health', {\n        headers: { 'atproto-proxy': 'did:plc:example#atproto_labeler' },\n      })\n      const response = await router.fetch(request)\n\n      expect(healthCheck).not.toHaveBeenCalled()\n      expect(response.status).toBe(400)\n    })\n  })\n\n  describe('invalid NSID', () => {\n    it('returns 400 for invalid NSID format', async () => {\n      const router = new LexRouter()\n      const request = new Request('https://example.com/xrpc/not-an-nsid!!')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n      expect(data.message).toContain('Invalid NSID')\n    })\n\n    it('returns 400 for empty NSID', async () => {\n      const router = new LexRouter()\n      const request = new Request('https://example.com/xrpc/')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n    })\n  })\n\n  describe('atproto-proxy header', () => {\n    it('bypasses local handler when atproto-proxy header is set', async () => {\n      const router = new LexRouter().add(io.example.status, handlers.status)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'did:plc:example#atproto_labeler' } },\n      )\n      const response = await router.fetch(request)\n\n      // The handler should NOT be called - currently returns MethodNotImplemented\n      // because proxy is not yet implemented\n      expect(response.status).toBe(501)\n    })\n\n    it('returns 400 for invalid atproto-proxy header format', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'not-a-valid-proxy' } },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n      expect(data.message).toContain('atproto-proxy')\n    })\n\n    it('returns 400 for atproto-proxy without fragment', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'did:plc:example' } },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n    })\n\n    it('returns 400 for atproto-proxy with empty fragment', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'did:plc:example#' } },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n    })\n\n    it('returns 400 for atproto-proxy with spaces', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'did:plc:example #service' } },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n    })\n\n    it('returns 400 for atproto-proxy with multiple fragments', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'did:plc:example#svc#extra' } },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n    })\n\n    it('returns 400 for atproto-proxy with space in fragment', async () => {\n      const router = new LexRouter()\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { headers: { 'atproto-proxy': 'did:plc:example#service id' } },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n    })\n  })\n\n  describe('NSID normalization', () => {\n    it('matches handler when URL has uppercase domain segments', async () => {\n      const router = new LexRouter().add(io.example.status, handlers.status)\n\n      const request = new Request('https://example.com/xrpc/IO.Example.status')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      expect(await response.json()).toEqual({ status: 'ok' })\n    })\n\n    it('matches handler when URL has mixed-case domain segments', async () => {\n      const router = new LexRouter().add(io.example.status, handlers.status)\n\n      const request = new Request('https://example.com/xrpc/IO.EXAMPLE.status')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      expect(await response.json()).toEqual({ status: 'ok' })\n    })\n\n    it('preserves case sensitivity of method name (last segment)', async () => {\n      const router = new LexRouter().add(io.example.status, handlers.status)\n\n      // \"Status\" (uppercase S) should not match \"status\"\n      const request = new Request('https://example.com/xrpc/io.example.Status')\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(501)\n      expect(await response.json()).toMatchObject({\n        error: 'MethodNotImplemented',\n      })\n    })\n\n    it('prevents duplicate registration with different domain casing', async () => {\n      const router = new LexRouter().add(io.example.status, handlers.status)\n\n      expect(() => {\n        // Same NSID with different domain casing should be detected as duplicate\n        const statusUpperCase = l.query(\n          'IO.Example.status' as 'io.example.status',\n          l.params(),\n          l.payload('application/json', l.object({ status: l.string() })),\n        )\n        router.add(statusUpperCase, handlers.status)\n      }).toThrow(/already registered/)\n    })\n  })\n\n  describe('error handling', () => {\n    it('onHandlerError receives LexServerError', async () => {\n      const onHandlerError = vi.fn<HandlerErrorHook>()\n      const router = new LexRouter({ onHandlerError })\n\n      router.add(io.example.status, async () => {\n        throw new Error('Unexpected error')\n      })\n\n      const request = new Request('https://example.com/xrpc/io.example.status')\n      await router.fetch(request)\n\n      expect(onHandlerError).toHaveBeenCalledTimes(1)\n      const ctx = onHandlerError.mock.calls[0][0]\n      expect(ctx.error).toBeInstanceOf(LexServerError)\n      expect(ctx.error.status).toBe(500)\n      expect(ctx.method).toBeDefined()\n      expect(ctx.request).toBe(request)\n    })\n\n    it('does not call onHandlerError for aborted requests', async () => {\n      const onHandlerError = vi.fn<HandlerErrorHook>()\n      const router = new LexRouter({ onHandlerError })\n\n      router.add(io.example.status, async (_ctx) => {\n        const reason = new Error('aborted')\n        throw new Error('handler error', { cause: reason })\n      })\n\n      const controller = new AbortController()\n      const reason = new Error('aborted')\n      controller.abort(reason)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { signal: controller.signal },\n      )\n\n      // Need to create a handler that actually throws with the abort reason\n      const router2 = new LexRouter({ onHandlerError })\n      router2.add(io.example.status, async ({ signal }) => {\n        throw new Error('handler error', { cause: signal.reason })\n      })\n\n      const response = await router2.fetch(request)\n\n      expect(response.status).toBe(499)\n      expect(onHandlerError).not.toHaveBeenCalled()\n    })\n\n    it('returns 499 for aborted requests', async () => {\n      const controller = new AbortController()\n      const reason = new Error('Client disconnected')\n      controller.abort(reason)\n\n      const router = new LexRouter()\n      router.add(io.example.status, async () => {\n        throw new Error('after abort', { cause: reason })\n      })\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.status',\n        { signal: controller.signal },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(499)\n      const data = await response.json()\n      expect(data.error).toBe('RequestAborted')\n    })\n  })\n})\n\n// ============================================================================\n// Parameter Tests (ported from xrpc-server/tests/parameters.test.ts)\n// ============================================================================\n\ndescribe('Parameters', () => {\n  const io = {\n    example: {\n      paramTest: l.query(\n        'io.example.paramTest',\n        l.params({\n          str: l.string({ minLength: 2, maxLength: 10 }),\n          int: l.integer({ minimum: 2, maximum: 10 }),\n          bool: l.boolean(),\n          arr: l.array(l.integer(), { maxLength: 2 }),\n          def: l.optional(l.withDefault(l.integer(), 0)),\n        }),\n        l.payload(\n          'application/json',\n          l.object({\n            str: l.string(),\n            int: l.integer(),\n            bool: l.boolean(),\n            arr: l.array(l.integer()),\n            def: l.optional(l.integer()),\n          }),\n        ),\n      ),\n    },\n  }\n\n  const handler: LexRouterMethodHandler<typeof io.example.paramTest> = async ({\n    params,\n  }) => ({\n    body: {\n      str: params.str,\n      int: params.int,\n      bool: params.bool,\n      arr: params.arr,\n      def: params.def,\n    },\n  })\n\n  const router = new LexRouter().add(io.example.paramTest, handler)\n\n  it('validates query params - valid input', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=5&bool=true&arr=1&arr=2&def=5',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    const data = await response.json()\n    expect(data.str).toBe('valid')\n    expect(data.int).toBe(5)\n    expect(data.bool).toBe(true)\n    expect(data.arr).toEqual([1, 2])\n    expect(data.def).toBe(5)\n  })\n\n  it('applies default values', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=5&bool=true&arr=3&arr=4',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    const data = await response.json()\n    // def should be undefined or 0 (default) when not provided\n    expect(data.def).toBe(0)\n  })\n\n  it('coerces types from query strings', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=10&int=5&bool=true&arr=3&arr=4',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    const data = await response.json()\n    expect(data.str).toBe('10')\n    expect(data.int).toBe(5)\n    expect(data.bool).toBe(true)\n    expect(data.arr).toEqual([3, 4])\n  })\n\n  it('rejects string too short', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=n&int=5&bool=true&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('str')\n  })\n\n  it('rejects string too long', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=loooooooooooooong&int=5&bool=true&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('str')\n  })\n\n  it('rejects missing required parameter str', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?int=5&bool=true&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('str')\n  })\n\n  it('rejects missing required parameter int', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&bool=true&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('int')\n  })\n\n  it('rejects missing required parameter bool', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=5&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('bool')\n  })\n\n  it('rejects integer too small', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=-1&bool=true&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('int')\n  })\n\n  it('rejects integer too large', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=11&bool=true&arr=1',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('int')\n  })\n\n  it('rejects missing required array parameter', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=5&bool=true',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.message).toContain('arr')\n  })\n\n  it('rejects array too large', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.paramTest?str=valid&int=5&bool=true&arr=1&arr=2&arr=3',\n    )\n    const response = await router.fetch(request)\n    const data = await response.json()\n\n    expect(response.status).toBe(400)\n    expect(data.message).toContain('arr')\n  })\n})\n\n// ============================================================================\n// Procedure Tests (ported from xrpc-server/tests/procedures.test.ts)\n// ============================================================================\n\ndescribe('Procedures', () => {\n  const io = {\n    example: {\n      pingOne: l.procedure(\n        'io.example.pingOne',\n        l.params({\n          message: l.string(),\n        }),\n        l.payload(),\n        l.payload('text/plain'),\n      ),\n      pingTwo: l.procedure(\n        'io.example.pingTwo',\n        l.params(),\n        l.payload('text/plain'),\n        l.payload('text/plain'),\n      ),\n      pingThree: l.procedure(\n        'io.example.pingThree',\n        l.params(),\n        l.payload('application/octet-stream'),\n        l.payload('application/octet-stream'),\n      ),\n      pingFour: l.procedure(\n        'io.example.pingFour',\n        l.params(),\n        l.payload(\n          'application/json',\n          l.object({\n            message: l.string(),\n          }),\n        ),\n        l.payload(\n          'application/json',\n          l.object({\n            message: l.string(),\n          }),\n        ),\n      ),\n    },\n  }\n\n  const handlers = {\n    pingOne: (async ({ params }) => ({\n      encoding: 'text/plain',\n      body: params.message,\n    })) as LexRouterMethodHandler<typeof io.example.pingOne>,\n    pingTwo: (async ({ input }) => ({\n      encoding: 'text/plain',\n      body: input.body.body!,\n    })) as LexRouterMethodHandler<typeof io.example.pingTwo>,\n    pingThree: (async ({ input }) => ({\n      encoding: 'application/octet-stream',\n      body: input.body.body!,\n    })) as LexRouterMethodHandler<typeof io.example.pingThree>,\n    pingFour: (async ({ input }) => ({\n      body: { message: input.body.message },\n    })) as LexRouterMethodHandler<typeof io.example.pingFour>,\n  }\n\n  const router = new LexRouter()\n    .add(io.example.pingOne, handlers.pingOne)\n    .add(io.example.pingTwo, handlers.pingTwo)\n    .add(io.example.pingThree, handlers.pingThree)\n    .add(io.example.pingFour, handlers.pingFour)\n\n  it('serves procedure with params returning text', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingOne?message=hello%20world',\n      { method: 'POST' },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe('text/plain')\n    expect(await response.text()).toBe('hello world')\n  })\n\n  it('serves procedure with text input/output', async () => {\n    const request = new Request('https://example.com/xrpc/io.example.pingTwo', {\n      method: 'POST',\n      headers: { 'content-type': 'text/plain' },\n      body: 'hello world',\n    })\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe('text/plain')\n    expect(await response.text()).toBe('hello world')\n  })\n\n  it('serves procedure with binary input/output', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingThree',\n      {\n        method: 'POST',\n        headers: { 'content-type': 'application/octet-stream' },\n        body: new TextEncoder().encode('hello world'),\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe(\n      'application/octet-stream',\n    )\n    const responseBytes = new Uint8Array(await response.arrayBuffer())\n    expect(new TextDecoder().decode(responseBytes)).toBe('hello world')\n  })\n\n  it('serves procedure with JSON input/output', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingFour',\n      {\n        method: 'POST',\n        headers: { 'content-type': 'application/json' },\n        body: JSON.stringify({ message: 'hello world' }),\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe('application/json')\n    const data = await response.json()\n    expect(data.message).toBe('hello world')\n  })\n})\n\n// ============================================================================\n// Query Tests (ported from xrpc-server/tests/queries.test.ts)\n// ============================================================================\n\ndescribe('Queries', () => {\n  const io = {\n    example: {\n      pingOne: l.query(\n        'io.example.pingOne',\n        l.params({\n          message: l.string(),\n        }),\n        l.payload('text/plain'),\n      ),\n      pingTwo: l.query(\n        'io.example.pingTwo',\n        l.params({\n          message: l.string(),\n        }),\n        l.payload('application/octet-stream'),\n      ),\n      pingThree: l.query(\n        'io.example.pingThree',\n        l.params({\n          message: l.string(),\n        }),\n        l.payload('application/json', l.object({ message: l.string() })),\n      ),\n    },\n  }\n\n  const handlers = {\n    pingOne: (async ({ params }) => ({\n      encoding: 'text/plain',\n      body: params.message,\n    })) satisfies LexRouterMethodHandler<typeof io.example.pingOne>,\n    pingTwo: (async ({ params }) => ({\n      encoding: 'application/octet-stream',\n      body: new TextEncoder().encode(params.message),\n    })) satisfies LexRouterMethodHandler<typeof io.example.pingTwo>,\n    pingThree: (async ({ params }) => ({\n      body: { message: params.message },\n      headers: { 'x-test-header-name': 'test-value' },\n    })) satisfies LexRouterMethodHandler<typeof io.example.pingThree>,\n  }\n\n  const router = new LexRouter()\n    .add(io.example.pingOne, handlers.pingOne)\n    .add(io.example.pingTwo, handlers.pingTwo)\n    .add(io.example.pingThree, handlers.pingThree)\n\n  it('serves query with text response', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingOne?message=hello%20world',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe('text/plain')\n    expect(await response.text()).toBe('hello world')\n  })\n\n  it('serves query with binary response', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingTwo?message=hello%20world',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe(\n      'application/octet-stream',\n    )\n    const bytes = new Uint8Array(await response.arrayBuffer())\n    expect(new TextDecoder().decode(bytes)).toBe('hello world')\n  })\n\n  it('serves query with JSON response and custom headers', async () => {\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingThree?message=hello%20world',\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(200)\n    expect(response.headers.get('content-type')).toBe('application/json')\n    expect(response.headers.get('x-test-header-name')).toBe('test-value')\n    const data = await response.json()\n    expect(data.message).toBe('hello world')\n  })\n\n  it('rejects query with content-type header', async () => {\n    // GET requests can't have a body, but they can have content-type headers\n    // The server should reject queries that have content-type/content-length headers\n    const request = new Request(\n      'https://example.com/xrpc/io.example.pingOne?message=hello',\n      {\n        method: 'GET',\n        headers: { 'content-type': 'application/json' },\n      },\n    )\n    const response = await router.fetch(request)\n\n    expect(response.status).toBe(400)\n    const data = await response.json()\n    expect(data.error).toBe('InvalidRequest')\n  })\n})\n\n// ============================================================================\n// Response Handling Tests (ported from xrpc-server/tests/responses.test.ts)\n// ============================================================================\n\ndescribe('Responses', () => {\n  describe('Streaming Responses', () => {\n    const io = {\n      example: {\n        readableStream: l.query(\n          'io.example.readableStream',\n          l.params({\n            shouldErr: l.optional(l.boolean()),\n          }),\n          l.payload('application/vnd.ipld.car'),\n        ),\n      },\n    }\n\n    it('returns readable streams of bytes', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.readableStream\n      > = async () => {\n        const stream = new ReadableStream({\n          start(controller) {\n            for (let i = 0; i < 5; i++) {\n              controller.enqueue(new Uint8Array([i]))\n            }\n            controller.close()\n          },\n        })\n\n        return {\n          encoding: 'application/vnd.ipld.car',\n          body: stream,\n        }\n      }\n\n      const router = new LexRouter().add(io.example.readableStream, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.readableStream',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      expect(response.headers.get('content-type')).toBe(\n        'application/vnd.ipld.car',\n      )\n\n      const reader = response.body!.getReader()\n      const chunks: number[] = []\n      // eslint-disable-next-line no-constant-condition\n      while (true) {\n        const { done, value } = await reader.read()\n        if (done) break\n        chunks.push(...value)\n      }\n      expect(chunks).toEqual([0, 1, 2, 3, 4])\n    })\n\n    it('handles errors on readable streams of bytes', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.readableStream\n      > = async ({ params }) => {\n        const stream = new ReadableStream({\n          start(controller) {\n            for (let i = 0; i < 5; i++) {\n              controller.enqueue(new Uint8Array([i]))\n            }\n            if (params.shouldErr) {\n              controller.error(new Error('Stream error'))\n            } else {\n              controller.close()\n            }\n          },\n        })\n\n        return {\n          encoding: 'application/vnd.ipld.car',\n          body: stream,\n        }\n      }\n\n      const router = new LexRouter().add(io.example.readableStream, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.readableStream?shouldErr=true',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n\n      const reader = response.body!.getReader()\n      await expect(async () => {\n        // eslint-disable-next-line no-constant-condition\n        while (true) {\n          const { done } = await reader.read()\n          if (done) break\n        }\n      }).rejects.toThrow('Stream error')\n    })\n  })\n\n  describe('Empty Responses', () => {\n    const io = {\n      example: {\n        emptyResponse: l.query(\n          'io.example.emptyResponse',\n          l.params(),\n          l.payload(),\n        ),\n      },\n    }\n\n    it('handles responses with no body', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.emptyResponse\n      > = async () => ({})\n\n      const router = new LexRouter().add(io.example.emptyResponse, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.emptyResponse',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      expect(response.body).toBeNull()\n    })\n\n    it('handles responses with headers but no body', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.emptyResponse\n      > = async () => ({\n        headers: { 'x-custom-header': 'value' },\n      })\n\n      const router = new LexRouter().add(io.example.emptyResponse, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.emptyResponse',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      expect(response.headers.get('x-custom-header')).toBe('value')\n      expect(response.body).toBeNull()\n    })\n  })\n\n  describe('Custom Response Objects', () => {\n    const io = {\n      example: {\n        customResponse: l.query(\n          'io.example.customResponse',\n          l.params({\n            status: l.integer(),\n          }),\n          l.payload(),\n        ),\n      },\n    }\n\n    it('allows returning custom Response objects', async () => {\n      const handler: LexRouterMethodHandler<\n        typeof io.example.customResponse\n      > = async ({ params }) => {\n        return new Response(JSON.stringify({ code: params.status }), {\n          status: params.status,\n          headers: { 'content-type': 'application/json' },\n        })\n      }\n\n      const router = new LexRouter().add(io.example.customResponse, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.customResponse?status=201',\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(201)\n      const data = await response.json()\n      expect(data.code).toBe(201)\n    })\n  })\n})\n\n// ============================================================================\n// Body Handling Tests (ported from xrpc-server/tests/bodies.test.ts)\n// ============================================================================\n\ndescribe('Body Handling', () => {\n  describe('Input Validation', () => {\n    const io = {\n      example: {\n        validationTest: l.procedure(\n          'io.example.validationTest',\n          l.params(),\n          l.payload(\n            'application/json',\n            l.object({\n              foo: l.string(),\n              bar: l.optional(l.integer()),\n            }),\n          ),\n          l.payload(\n            'application/json',\n            l.object({\n              foo: l.string(),\n              bar: l.optional(l.integer()),\n            }),\n          ),\n        ),\n      },\n    }\n\n    const handler: LexRouterMethodHandler<\n      typeof io.example.validationTest\n    > = async ({ input }) => ({\n      body: input.body!,\n    })\n\n    const router = new LexRouter().add(io.example.validationTest, handler)\n\n    it('validates input and output bodies', async () => {\n      const request = new Request(\n        'https://example.com/xrpc/io.example.validationTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'application/json' },\n          body: JSON.stringify({ foo: 'hello', bar: 123 }),\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      const data = await response.json()\n      expect(data.foo).toBe('hello')\n      expect(data.bar).toBe(123)\n    })\n\n    it('rejects missing required fields', async () => {\n      const request = new Request(\n        'https://example.com/xrpc/io.example.validationTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'application/json' },\n          body: JSON.stringify({}),\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.message).toContain('foo')\n    })\n\n    it('rejects wrong types', async () => {\n      const request = new Request(\n        'https://example.com/xrpc/io.example.validationTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'application/json' },\n          body: JSON.stringify({ foo: 123 }),\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.message).toContain('foo')\n    })\n\n    it('rejects wrong content-type', async () => {\n      const request = new Request(\n        'https://example.com/xrpc/io.example.validationTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'image/jpeg' },\n          body: new Uint8Array([1, 2, 3]),\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n    })\n  })\n\n  describe('Binary Data Support', () => {\n    const io = {\n      example: {\n        blobTest: l.procedure(\n          'io.example.blobTest',\n          l.params(),\n          l.payload('*/*'),\n          l.payload('application/octet-stream'),\n        ),\n      },\n    }\n\n    const handler: LexRouterMethodHandler<typeof io.example.blobTest> = async ({\n      input,\n    }) => {\n      return {\n        encoding: 'application/octet-stream',\n        body: new Uint8Array(await input.body.arrayBuffer()),\n      }\n    }\n\n    const router = new LexRouter().add(io.example.blobTest, handler)\n\n    it('supports ArrayBuffers', async () => {\n      const bytes = new Uint8Array([1, 2, 3, 4, 5])\n      const request = new Request(\n        'https://example.com/xrpc/io.example.blobTest',\n        {\n          method: 'POST',\n          // @NOTE content-type will default to application/octet-stream\n          body: bytes,\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      const responseBytes = new Uint8Array(await response.arrayBuffer())\n      expect(responseBytes).toEqual(bytes)\n      expect(response.headers.get('content-type')).toBe(\n        'application/octet-stream',\n      )\n    })\n\n    it('supports empty payload', async () => {\n      const bytes = new Uint8Array(0)\n      const request = new Request(\n        'https://example.com/xrpc/io.example.blobTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'application/octet-stream' },\n          body: bytes,\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      const responseBytes = new Uint8Array(await response.arrayBuffer())\n      expect(responseBytes).toEqual(bytes)\n    })\n\n    it('supports ReadableStream', async () => {\n      const message = 'hello world'\n      const stream = new ReadableStream({\n        start(controller) {\n          controller.enqueue(new TextEncoder().encode(message))\n          controller.close()\n        },\n      })\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.blobTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'application/octet-stream' },\n          // @ts-expect-error\n          duplex: 'half',\n          body: stream,\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      const responseBytes = new Uint8Array(await response.arrayBuffer())\n      expect(new TextDecoder().decode(responseBytes)).toBe(message)\n    })\n\n    it('requires any parsable Content-Type for blob uploads', async () => {\n      const bytes = new Uint8Array([1, 2, 3])\n      const request = new Request(\n        'https://example.com/xrpc/io.example.blobTest',\n        {\n          method: 'POST',\n          headers: { 'content-type': 'some/thing' },\n          body: bytes,\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n    })\n  })\n\n  describe('Edge Cases', () => {\n    it('errors on missing Content-Type for JSON payload', async () => {\n      const io = {\n        example: {\n          emptyContentType: l.procedure(\n            'io.example.emptyContentType',\n            l.params(),\n            l.payload('application/json', l.object({ data: l.string() })),\n            l.payload('application/json', l.object({ data: l.string() })),\n          ),\n        },\n      }\n\n      const handler: LexRouterMethodHandler<\n        typeof io.example.emptyContentType\n      > = async ({ input }) => ({\n        body: { data: input.body!.data },\n      })\n\n      const router = new LexRouter().add(io.example.emptyContentType, handler)\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.emptyContentType',\n        {\n          method: 'POST',\n          body: JSON.stringify({ data: 'test' }),\n        },\n      )\n\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(400)\n      const data = await response.json()\n      expect(data.error).toBe('InvalidRequest')\n    })\n\n    it('defaults to application/octet-stream for empty Content-Type', async () => {\n      const io = {\n        example: {\n          emptyContentTypeBlob: l.procedure(\n            'io.example.emptyContentTypeBlob',\n            l.params(),\n            l.payload('*/*'),\n            l.payload('application/json', l.object({ encoding: l.string() })),\n          ),\n        },\n      }\n\n      const handler: LexRouterMethodHandler<\n        typeof io.example.emptyContentTypeBlob\n      > = async ({ input }) => ({\n        body: { encoding: input.encoding },\n      })\n\n      const router = new LexRouter().add(\n        io.example.emptyContentTypeBlob,\n        handler,\n      )\n\n      const request = new Request(\n        'https://example.com/xrpc/io.example.emptyContentTypeBlob',\n        {\n          method: 'POST',\n          body: new Uint8Array([1, 2, 3]),\n        },\n      )\n      const response = await router.fetch(request)\n\n      expect(response.status).toBe(200)\n      const data = await response.json()\n      expect(response.headers.get('content-type')).toBe('application/json')\n      expect(data.encoding).toBe('application/octet-stream')\n    })\n  })\n})\n\ndescribe('Subscription', () => {\n  const io = {\n    example: {\n      subscribe: l.subscription(\n        'io.example.subscribe',\n        l.params({\n          message: l.withDefault(l.string(), 'hello'),\n        }),\n        l.object({\n          message: l.string(),\n          count: l.integer(),\n        }),\n      ),\n    },\n  }\n\n  it('handles subscriptions with cleanup', async () => {\n    let sentCount = 0\n    const maxMessages = 10\n\n    const { resolve, promise: finallyPromise } = timeoutDeferred(5000)\n\n    const router = new LexRouter({ upgradeWebSocket }).add(\n      io.example.subscribe,\n      async function* ({ params: { message }, signal }) {\n        try {\n          for (; sentCount < maxMessages; ) {\n            await scheduler.wait(5, { signal })\n            yield { message, count: ++sentCount }\n          }\n        } finally {\n          resolve()\n        }\n      },\n    )\n\n    await using server = await serve(router)\n\n    const { port } = server.address() as AddressInfo\n    const ws = new WebSocket(\n      `ws://localhost:${port}/xrpc/io.example.subscribe?message=ping`,\n    )\n    ws.binaryType = 'arraybuffer'\n\n    const messages: unknown[] = []\n    ws.addEventListener('message', (event) => {\n      try {\n        const bytes = new Uint8Array(event.data as ArrayBuffer)\n        const data = [...decodeAll(bytes)]\n        messages.push(data)\n      } catch (err) {\n        messages.push(err)\n      }\n      if (messages.length >= 3) {\n        ws.close()\n      }\n    })\n\n    // Ensures that \"finally\" block is indeed called\n    await finallyPromise\n\n    expect(messages).toStrictEqual([\n      [{ op: 1 }, { message: 'ping', count: 1 }],\n      [{ op: 1 }, { message: 'ping', count: 2 }],\n      [{ op: 1 }, { message: 'ping', count: 3 }],\n    ])\n\n    expect(sentCount).toBeGreaterThanOrEqual(3)\n    expect(sentCount).toBeLessThan(maxMessages)\n  })\n\n  it('returns 405 for non-GET request', async () => {\n    const router = new LexRouter({ upgradeWebSocket }).add(\n      io.example.subscribe,\n      async function* () {},\n    )\n\n    await using server = await serve(router)\n    const { port } = server.address() as AddressInfo\n\n    const response = await fetch(\n      `http://localhost:${port}/xrpc/io.example.subscribe?message=ping`,\n      { method: 'POST' },\n    )\n\n    expect(response.status).toBe(405)\n    const data = await response.json()\n    expect(data.error).toBe('InvalidRequest')\n    expect(data.message).toBe('Method not allowed')\n  })\n\n  it('returns 426 for non-WebSocket request', async () => {\n    const router = new LexRouter({ upgradeWebSocket }).add(\n      io.example.subscribe,\n      async function* () {},\n    )\n\n    await using server = await serve(router)\n    const { port } = server.address() as AddressInfo\n\n    const response = await fetch(\n      `http://localhost:${port}/xrpc/io.example.subscribe?message=ping`,\n      { method: 'GET' },\n    )\n\n    expect(response.status).toBe(426)\n    expect(response.headers.get('upgrade')).toBe('websocket')\n    expect(response.headers.get('connection')).toBe('Upgrade')\n    const data = await response.json()\n    expect(data.error).toBe('InvalidRequest')\n    expect(data.message).toBe(\n      'XRPC subscriptions are only available over WebSocket',\n    )\n  })\n\n  it('closes with 1003 when client sends a message to the subscription', async () => {\n    const router = new LexRouter({ upgradeWebSocket }).add(\n      io.example.subscribe,\n      async function* ({ signal }) {\n        while (true) {\n          await scheduler.wait(50, { signal })\n          yield { message: 'ping', count: 1 }\n        }\n      },\n    )\n\n    await using server = await serve(router)\n    const { port } = server.address() as AddressInfo\n\n    const { resolve, reject, promise } = timeoutDeferred<{ code: number }>(5000)\n\n    const ws = new WebSocket(\n      `ws://localhost:${port}/xrpc/io.example.subscribe?message=ping`,\n    )\n    ws.addEventListener('open', () => {\n      ws.send('unexpected message from client')\n    })\n    ws.addEventListener('error', reject)\n    ws.addEventListener('close', resolve)\n\n    const { code } = await promise\n\n    expect(code).toBe(1003)\n  })\n\n  describe('error close codes', () => {\n    const subscribeWithErrors = l.subscription(\n      'io.example.subscribeWithErrors',\n      l.params(),\n      l.object({ message: l.string() }),\n      ['FutureCursor', 'ConsumerTooSlow'],\n    )\n\n    it('closes with 1008 and sends error frame for known LexError', async () => {\n      const router = new LexRouter({ upgradeWebSocket }).add(\n        subscribeWithErrors,\n        async function* () {\n          yield await Promise.reject(\n            new LexError('FutureCursor', 'Too far in the future'),\n          )\n        },\n      )\n\n      await using server = await serve(router)\n      const { port } = server.address() as AddressInfo\n\n      const { resolve, reject, promise } = timeoutDeferred<{ code: number }>(\n        5000,\n      )\n      const receivedFrames: unknown[][] = []\n\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.subscribeWithErrors`,\n      )\n      ws.binaryType = 'arraybuffer'\n      ws.addEventListener('message', (event) => {\n        const bytes = new Uint8Array(event.data as ArrayBuffer)\n        receivedFrames.push([...decodeAll(bytes)])\n      })\n      ws.addEventListener('close', resolve)\n      ws.addEventListener('error', reject)\n\n      const { code } = await promise\n\n      expect(code).toBe(1008)\n      expect(receivedFrames).toHaveLength(1)\n      const [header, body] = receivedFrames[0]\n      expect(header).toEqual({ op: -1 })\n      expect(body).toMatchObject({ error: 'FutureCursor' })\n    })\n\n    it('closes with 1011 and sends InternalServerError frame for unknown error', async () => {\n      const router = new LexRouter({ upgradeWebSocket }).add(\n        subscribeWithErrors,\n        async function* () {\n          yield await Promise.reject(new Error('unexpected failure'))\n        },\n      )\n\n      await using server = await serve(router)\n      const { port } = server.address() as AddressInfo\n\n      const { resolve, reject, promise } = timeoutDeferred<{ code: number }>(\n        5000,\n      )\n      const receivedFrames: unknown[][] = []\n\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.subscribeWithErrors`,\n      )\n      ws.binaryType = 'arraybuffer'\n      ws.addEventListener('message', (event) => {\n        const bytes = new Uint8Array(event.data as ArrayBuffer)\n        receivedFrames.push([...decodeAll(bytes)])\n      })\n      ws.addEventListener('close', resolve)\n      ws.addEventListener('error', reject)\n\n      const { code } = await promise\n\n      expect(code).toBe(1011)\n      expect(receivedFrames).toHaveLength(1)\n      const [header, body] = receivedFrames[0]\n      expect(header).toEqual({ op: -1 })\n      expect(body).toMatchObject({ error: 'InternalServerError' })\n    })\n\n    it('closes with 1011 for a LexError not listed in method.errors', async () => {\n      const router = new LexRouter({ upgradeWebSocket }).add(\n        subscribeWithErrors,\n        async function* () {\n          yield await Promise.reject(\n            new LexError('SomeOtherError', 'Not a declared error'),\n          )\n        },\n      )\n\n      await using server = await serve(router)\n      const { port } = server.address() as AddressInfo\n\n      const { resolve, reject, promise } = timeoutDeferred<{ code: number }>(\n        5000,\n      )\n\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.subscribeWithErrors`,\n      )\n      ws.addEventListener('close', resolve)\n      ws.addEventListener('error', reject)\n\n      const { code } = await promise\n\n      expect(code).toBe(1011)\n    })\n  })\n\n  describe('onSocketError hook', () => {\n    it('calls onSocketError when the generator throws a non-abort error', async () => {\n      const onSocketError = vi.fn<SocketErrorHook>()\n      const router = new LexRouter({ upgradeWebSocket, onSocketError }).add(\n        io.example.subscribe,\n        async function* () {\n          yield await Promise.reject(new Error('generator failure'))\n        },\n      )\n\n      await using server = await serve(router)\n      const { port } = server.address() as AddressInfo\n\n      const { resolve, reject, promise } = timeoutDeferred<{ code: number }>(\n        5000,\n      )\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.subscribe?message=ping`,\n      )\n      ws.addEventListener('close', resolve)\n      ws.addEventListener('error', reject)\n\n      await promise\n\n      expect(onSocketError).toHaveBeenCalledTimes(1)\n      const ctx = onSocketError.mock.calls[0][0]\n      expect(ctx.error).toBeInstanceOf(Error)\n      expect(ctx.method).toBeDefined()\n      expect(ctx.request).toBeDefined()\n    })\n\n    it('does not call onSocketError when the error matches the abort reason', async () => {\n      const onSocketError = vi.fn<SocketErrorHook>()\n      const router = new LexRouter({ upgradeWebSocket, onSocketError }).add(\n        io.example.subscribe,\n        async function* ({ signal }) {\n          // Wait for abort, then throw with the abort reason as cause\n          await new Promise<void>((_, reject) => {\n            signal.addEventListener('abort', () => {\n              reject(new Error('aborted', { cause: signal.reason }))\n            })\n          })\n          yield { message: 'never', count: 0 }\n        },\n      )\n\n      await using server = await serve(router)\n      const { port } = server.address() as AddressInfo\n\n      const { resolve, reject, promise } = timeoutDeferred<{ code: number }>(\n        5000,\n      )\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.subscribe?message=ping`,\n      )\n      // Close from the client side to trigger the abort\n      ws.addEventListener('open', () => ws.close())\n      ws.addEventListener('close', resolve)\n      ws.addEventListener('error', reject)\n\n      await promise\n\n      expect(onSocketError).not.toHaveBeenCalled()\n    })\n  })\n})\n\nfunction defer<T = void>() {\n  let res: (value: T | PromiseLike<T>) => void\n  let rej: (err: unknown) => void\n  const promise = new Promise<T>((resolve, reject) => {\n    res = resolve\n    rej = reject\n  })\n  return { resolve: res!, reject: rej!, promise }\n}\n\nfunction timeoutDeferred<T = void>(ms: number) {\n  const { resolve, reject, promise } = defer<T>()\n  const to = setTimeout(() => reject(new Error('Timed out')), ms).unref()\n  return {\n    resolve,\n    reject,\n    promise: promise.finally(() => clearTimeout(to)),\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/lex-router.ts",
    "content": "import { encode } from '@atproto/lex-cbor'\nimport {\n  LexError,\n  LexErrorData,\n  LexValue,\n  isPlainObject,\n  ui8Concat,\n} from '@atproto/lex-data'\nimport { lexParse, lexToJson } from '@atproto/lex-json'\nimport {\n  DidString,\n  InferMethodInput,\n  InferMethodMessage,\n  InferMethodOutput,\n  InferMethodOutputBody,\n  InferMethodOutputEncoding,\n  InferMethodParams,\n  Main,\n  NsidString,\n  Procedure,\n  Query,\n  Subscription,\n  getMain,\n  isDidString,\n  isNsidString,\n} from '@atproto/lex-schema'\nimport { LexServerError } from './errors.js'\nimport { drainWebsocket } from './lib/drain-websocket.js'\n\nconst XRPC_PATH_PREFIX = '/xrpc/'\nconst XRPC_HEALTH_CHECK_PATH = '/xrpc/_health'\n\ntype Awaitable<T> = T | Promise<T>\n\n/**\n * Union type representing the supported Lexicon method types.\n *\n * - `Query`: Read-only methods invoked via HTTP GET\n * - `Procedure`: Methods that may modify state, invoked via HTTP POST\n * - `Subscription`: Real-time streaming methods over WebSocket\n */\nexport type LexMethod = Query | Procedure | Subscription\n\n/**\n * Network address for TCP or UDP connections.\n *\n * @example\n * ```typescript\n * const addr: NetAddr = {\n *   hostname: '127.0.0.1',\n *   port: 3000,\n *   transport: 'tcp'\n * }\n * ```\n */\nexport type NetAddr = {\n  /** The hostname or IP address of the connection. */\n  hostname: string\n  /** The port number of the connection. */\n  port: number\n  /** The transport protocol used. */\n  transport: 'tcp' | 'udp'\n}\n\n/**\n * Unix domain socket address.\n *\n * @example\n * ```typescript\n * const addr: UnixAddr = {\n *   path: '/var/run/app.sock',\n *   transport: 'unix'\n * }\n * ```\n */\nexport type UnixAddr = {\n  /** The filesystem path to the Unix socket. */\n  path: string\n  /** The transport protocol used. */\n  transport: 'unix' | 'unixpacket'\n}\n\n/**\n * Union type for all supported address types.\n *\n * Can be a network address ({@link NetAddr}), Unix socket address ({@link UnixAddr}),\n * or `undefined` when the address is not available.\n */\nexport type Addr = NetAddr | UnixAddr | undefined\n\n/**\n * Metadata about the client connection for an incoming request.\n *\n * @typeParam A - The address type, defaults to {@link Addr}\n *\n * @example\n * ```typescript\n * const info: ConnectionInfo<NetAddr> = {\n *   remoteAddr: { hostname: '192.168.1.1', port: 54321, transport: 'tcp' },\n *   completed: new Promise((resolve) => socket.on('close', resolve))\n * }\n * ```\n */\nexport type ConnectionInfo<A extends Addr = Addr> = {\n  /** The remote address of the client, if available. */\n  remoteAddr: A\n  /** Promise that resolves when the connection is fully closed. */\n  completed: Promise<void>\n}\n\n/**\n * Function signature for handling HTTP requests in the XRPC router.\n *\n * This is the standard fetch-style handler that processes incoming requests\n * and returns responses. It is used both internally by the router and can\n * be used to integrate with other HTTP frameworks.\n *\n * @param request - The incoming HTTP request\n * @param connection - Optional connection metadata including remote address\n * @returns A promise resolving to the HTTP response\n *\n * @example\n * ```typescript\n * const handler: FetchHandler = async (request, connection) => {\n *   console.log('Request from:', connection?.remoteAddr)\n *   return new Response('Hello, World!')\n * }\n * ```\n */\nexport type FetchHandler = (\n  request: Request,\n  connection?: ConnectionInfo,\n) => Promise<Response>\n\n/**\n * Context object passed to XRPC method handlers.\n *\n * Contains all the information needed to process a request, including\n * parsed parameters, authentication credentials, and the raw request object.\n *\n * @typeParam Method - The Lexicon method type (Query, Procedure, or Subscription)\n * @typeParam Credentials - The type of authentication credentials, determined by the auth handler\n *\n * @example\n * ```typescript\n * const handler: LexRouterMethodHandler<MyMethod, UserCredentials> = async (ctx) => {\n *   const { credentials, params, input, signal } = ctx\n *   // credentials.userId is available if auth handler returns UserCredentials\n *   // params contains validated query parameters\n *   // input contains the request body (for procedures)\n *   // signal can be used to abort long-running operations\n *   return { body: { result: 'success' } }\n * }\n * ```\n */\nexport type LexRouterHandlerContext<Method extends LexMethod, Credentials> = {\n  /** Authentication credentials returned by the auth handler. */\n  credentials: Credentials\n  /** Parsed and validated request input (body for procedures, undefined for queries). */\n  input: InferMethodInput<Method, Body>\n  /** Parsed and validated URL query parameters. */\n  params: InferMethodParams<Method>\n  /** The original HTTP request object. */\n  request: Request\n  /** Abort signal that is triggered when the request is cancelled. */\n  signal: AbortSignal\n  /** Connection metadata including remote address. */\n  connection?: ConnectionInfo\n}\n\ntype AsOptionalPayloadOptions<T> = T extends undefined | void\n  ? { encoding?: undefined; body?: undefined }\n  : T\n\n/**\n * Return type for XRPC method handlers (queries and procedures).\n *\n * Handlers can return either:\n * - A raw {@link Response} object for full control over the HTTP response\n * - An object with `body`, optional `encoding`, and optional `headers`\n *\n * For JSON methods, the body is automatically serialized. For other encodings,\n * the body must be a valid {@link BodyInit} type.\n *\n * @typeParam Method - The Lexicon method type (Query or Procedure)\n *\n * @example\n * ```typescript\n * // Return JSON body (most common)\n * return { body: { users: [...] } }\n *\n * // Return with custom headers\n * return {\n *   body: { data: 'value' },\n *   headers: { 'Cache-Control': 'max-age=3600' }\n * }\n *\n * // Return raw Response for full control\n * return new Response(binaryData, {\n *   headers: { 'Content-Type': 'application/octet-stream' }\n * })\n * ```\n */\nexport type LexRouterHandlerOutput<Method extends Query | Procedure> =\n  | Response\n  | ({\n      headers?: HeadersInit\n    } & (InferMethodOutputEncoding<Method> extends 'application/json'\n      ? {\n          // Allow omitting body when output is JSON\n          encoding?: 'application/json'\n          body: InferMethodOutputBody<Method>\n        }\n      : AsOptionalPayloadOptions<InferMethodOutput<Method, BodyInit>>))\n\n/**\n * Handler function for XRPC query and procedure methods.\n *\n * Receives a context object with request details and credentials,\n * and returns either a Response or a structured output object.\n *\n * @typeParam Method - The Lexicon method type (Query or Procedure)\n * @typeParam Credentials - The type of authentication credentials\n *\n * @example\n * ```typescript\n * const getProfile: LexRouterMethodHandler<GetProfileMethod, UserCredentials> = async (ctx) => {\n *   const profile = await db.getProfile(ctx.params.actor)\n *   return { body: profile }\n * }\n * ```\n */\nexport type LexRouterMethodHandler<\n  Method extends Query | Procedure = Query | Procedure,\n  Credentials = unknown,\n> = (\n  ctx: LexRouterHandlerContext<Method, Credentials>,\n) => Awaitable<LexRouterHandlerOutput<Method>>\n\n/**\n * Configuration object for registering an XRPC method with authentication.\n *\n * Used when you need to specify both a handler and an auth function.\n *\n * @typeParam Method - The Lexicon method type (Query or Procedure)\n * @typeParam Credentials - The type of authentication credentials\n *\n * @example\n * ```typescript\n * const config: LexRouterMethodConfig<GetProfileMethod, UserCredentials> = {\n *   handler: async (ctx) => {\n *     return { body: await getProfile(ctx.params.actor) }\n *   },\n *   auth: async ({ request }) => {\n *     return verifyToken(request.headers.get('authorization'))\n *   }\n * }\n * ```\n */\nexport type LexRouterMethodConfig<\n  Method extends Query | Procedure = Query | Procedure,\n  Credentials = unknown,\n> = {\n  /** The handler function that processes the request. */\n  handler: LexRouterMethodHandler<Method, Credentials>\n  /** Authentication function that validates credentials before the handler runs. */\n  auth: LexRouterAuth<Credentials, Method>\n}\n\n/**\n * Handler function for XRPC subscription methods (WebSocket streams).\n *\n * Returns an async iterable that yields messages to be sent over the WebSocket.\n * The connection remains open until the iterable completes or an error occurs.\n *\n * @typeParam Method - The Lexicon subscription method type\n * @typeParam Credentials - The type of authentication credentials\n *\n * @example\n * ```typescript\n * const subscribeRepos: LexRouterSubscriptionHandler<SubscribeReposMethod> = async function* (ctx) {\n *   const cursor = ctx.params.cursor ?? 0\n *   for await (const event of eventStream.since(cursor)) {\n *     if (ctx.signal.aborted) break\n *     yield { $type: 'com.atproto.sync.subscribeRepos#commit', ...event }\n *   }\n * }\n * ```\n */\nexport type LexRouterSubscriptionHandler<\n  Method extends Subscription = Subscription,\n  Credentials = unknown,\n> = (\n  ctx: LexRouterHandlerContext<Method, Credentials>,\n) => AsyncIterable<InferMethodMessage<Method>>\n\n/**\n * Configuration object for registering an XRPC subscription with authentication.\n *\n * Used when you need to specify both a handler and an auth function for subscriptions.\n *\n * @typeParam Method - The Lexicon subscription method type\n * @typeParam Credentials - The type of authentication credentials\n *\n * @example\n * ```typescript\n * const config: LexRouterSubscriptionConfig<SubscribeReposMethod, ServiceCredentials> = {\n *   handler: async function* (ctx) {\n *     for await (const event of eventStream) {\n *       yield event\n *     }\n *   },\n *   auth: async ({ request }) => {\n *     return verifyServiceAuth(request)\n *   }\n * }\n * ```\n */\nexport type LexRouterSubscriptionConfig<\n  Method extends Subscription = Subscription,\n  Credentials = unknown,\n> = {\n  /** The handler function that yields subscription messages. */\n  handler: LexRouterSubscriptionHandler<Method, Credentials>\n  /** Authentication function that validates credentials before the handler runs. */\n  auth: LexRouterAuth<Credentials, Method>\n}\n\n/**\n * Context object passed to authentication handlers.\n *\n * Contains the information needed to authenticate a request before\n * the main handler is invoked.\n *\n * @typeParam Method - The Lexicon method type\n *\n * @example\n * ```typescript\n * const authHandler: LexRouterAuth<UserCredentials> = async (ctx) => {\n *   const token = ctx.request.headers.get('authorization')\n *   if (!token) throw new LexServerAuthError('AuthenticationRequired', 'Missing token')\n *   return { userId: await verifyToken(token) }\n * }\n * ```\n */\nexport type LexRouterAuthContext<Method extends LexMethod = LexMethod> = {\n  /** The Lexicon method definition being called. */\n  method: Method\n  /** Parsed and validated URL query parameters. */\n  params: InferMethodParams<Method>\n  /** The original HTTP request object. */\n  request: Request\n  /** Connection metadata including remote address. */\n  connection?: ConnectionInfo\n}\n\n/**\n * Authentication handler function for XRPC methods.\n *\n * Called before the main handler to validate authentication credentials.\n * Should return the validated credentials or throw an error if authentication fails.\n *\n * @typeParam Credentials - The type of credentials to return on success\n * @typeParam Method - The Lexicon method type\n *\n * @example\n * ```typescript\n * // Simple token-based auth\n * const tokenAuth: LexRouterAuth<{ userId: string }> = async ({ request }) => {\n *   const token = request.headers.get('authorization')?.replace('Bearer ', '')\n *   if (!token) throw new LexServerAuthError('AuthenticationRequired', 'Token required')\n *   const userId = await verifyToken(token)\n *   if (!userId) throw new LexServerAuthError('AuthenticationRequired', 'Invalid token')\n *   return { userId }\n * }\n *\n * // Using with serviceAuth for AT Protocol service authentication\n * import { serviceAuth } from '@atproto/lex-server'\n * const auth = serviceAuth({ audience: 'did:web:example.com', unique: checkNonce })\n * ```\n */\nexport type LexRouterAuth<\n  Credentials = unknown,\n  Method extends LexMethod = LexMethod,\n> = (ctx: LexRouterAuthContext<Method>) => Credentials | Promise<Credentials>\n\n/**\n * Context object passed to error handler callbacks.\n *\n * Used for logging and monitoring errors that occur during request handling.\n */\nexport type HandlerErrorContext = {\n  request: Request\n  method: LexMethod\n  error: LexServerError\n}\n\nexport type HandlerErrorHook = (\n  ctx: HandlerErrorContext,\n) => void | Promise<void>\n\nexport type SocketErrorContext = {\n  request: Request\n  method: Subscription\n  error: unknown\n}\n\nexport type SocketErrorHook = (ctx: SocketErrorContext) => void | Promise<void>\n\n/**\n * Function that upgrades an HTTP request to a WebSocket connection.\n *\n * This is platform-specific: Deno provides this natively, while Node.js\n * requires the `upgradeWebSocket` function from this package.\n *\n * @param request - The HTTP request to upgrade\n * @returns An object containing the WebSocket and the upgrade response\n *\n * @example\n * ```typescript\n * // In Node.js, use the provided upgradeWebSocket function\n * import { upgradeWebSocket } from '@atproto/lex-server/nodejs'\n *\n * const router = new LexRouter({ upgradeWebSocket })\n * ```\n */\nexport type UpgradeWebSocket = (request: Request) => {\n  /** The WebSocket instance for bidirectional communication. */\n  socket: WebSocket\n  /** The HTTP response to return (101 Switching Protocols). */\n  response: Response\n}\n\nexport type HealthCheckHandler = (\n  request: Request,\n) => Awaitable<{ [x: string]: unknown; status: 'ok' }>\n\n/**\n * Configuration options for the {@link LexRouter}.\n *\n * @example\n * ```typescript\n * const options: LexRouterOptions = {\n *   upgradeWebSocket,\n *   onHandlerError: async ({ error, request, method }) => {\n *     console.error(`Error in ${method.nsid}:`, error)\n *     await reportToSentry(error)\n *   },\n *   highWaterMark: 64 * 1024,  // 64KB\n *   lowWaterMark: 16 * 1024    // 16KB\n * }\n * ```\n */\nexport type LexRouterOptions = {\n  /**\n   * Function to upgrade HTTP requests to WebSocket connections. Required for\n   * subscription methods. Defaults to Deno's built-in\n   * {@link globalThis.upgradeWebSocket} if available. For NodeJS, use the\n   * homonymous export from `@atproto/lex-server/nodejs`.\n   */\n  upgradeWebSocket?: UpgradeWebSocket\n  /**\n   * Callback invoked when an error occurs during request handling. Useful for\n   * logging and error reporting. Not called for client-induced errors (e.g.,\n   * request abortion).\n   */\n  onHandlerError?: HandlerErrorHook\n  /**\n   * Optional hook for handling errors during generation of WebSocket messages.\n   */\n  onSocketError?: SocketErrorHook\n  /**\n   * Optional health check handler. If provided, this function will be called\n   * for requests to the /xrpc/_health endpoint, allowing for custom health\n   * check logic and responses.\n   *\n   * If not provided, the server will respond to /xrpc/_health requests with a\n   * default JSON response of `{ status: 'ok' }`.\n   */\n  healthCheck?: HealthCheckHandler\n  /**\n   * Optional fallback handler for requests that are not /xrpc/ paths. Can be\n   * used to serve static files or other routes. If not provided, non-/xrpc/\n   * requests will return 404 responses.\n   */\n  fallback?: FetchHandler\n  /**\n   * High water mark for WebSocket backpressure (in bytes). When buffered data\n   * exceeds this, the handler will wait before sending more.\n   */\n  highWaterMark?: number\n  /**\n   * Low water mark for WebSocket backpressure (in bytes). The handler resumes\n   * sending when buffered data drops below this.\n   */\n  lowWaterMark?: number\n}\n\n/**\n * XRPC router for handling AT Protocol Lexicon methods.\n *\n * The router handles HTTP routing, parameter parsing, input validation,\n * authentication, and response serialization for XRPC methods. It supports\n * queries (GET), procedures (POST), and subscriptions (WebSocket).\n *\n * @example Setting up a basic XRPC server\n * ```typescript\n * import { LexRouter } from '@atproto/lex-server'\n * import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'\n * import { getProfile, createPost, subscribeRepos } from './lexicons'\n *\n * const router = new LexRouter({ upgradeWebSocket })\n *\n * // Register a query handler (GET request)\n * router.add(getProfile, async (ctx) => {\n *   const profile = await db.getProfile(ctx.params.actor)\n *   return { body: profile }\n * })\n *\n * // Register a procedure handler with authentication (POST request)\n * router.add(createPost, {\n *   handler: async (ctx) => {\n *     const post = await db.createPost(ctx.credentials.did, ctx.input.body)\n *     return { body: { uri: post.uri, cid: post.cid } }\n *   },\n *   auth: async ({ request }) => {\n *     return verifyAccessToken(request)\n *   }\n * })\n *\n * // Register a subscription handler (WebSocket)\n * router.add(subscribeRepos, async function* (ctx) {\n *   for await (const event of eventStream.since(ctx.params.cursor)) {\n *     if (ctx.signal.aborted) break\n *     yield event\n *   }\n * })\n *\n * // Start the server\n * const server = await serve(router, { port: 3000 })\n * console.log('XRPC server listening on port 3000')\n * ```\n *\n * @example Using with service authentication\n * ```typescript\n * import { LexRouter, serviceAuth } from '@atproto/lex-server'\n *\n * const router = new LexRouter()\n *\n * const auth = serviceAuth({\n *   audience: 'did:web:api.example.com',\n *   unique: async (nonce) => {\n *     // Check and record nonce uniqueness\n *     return await nonceStore.checkAndAdd(nonce)\n *   }\n * })\n *\n * router.add(protectedMethod, {\n *   handler: async (ctx) => {\n *     // ctx.credentials contains { did, didDocument, jwt }\n *     return { body: { callerDid: ctx.credentials.did } }\n *   },\n *   auth\n * })\n * ```\n */\nexport class LexRouter {\n  /** Map of NSID strings to their fetch handlers. */\n  readonly handlers: Map<NsidString, FetchHandler> = new Map()\n\n  /**\n   * Creates a new XRPC router.\n   *\n   * @param options - Router configuration options\n   */\n  constructor(readonly options: LexRouterOptions = {}) {}\n\n  /**\n   * Registers a subscription handler without authentication.\n   *\n   * @param ns - The Lexicon namespace definition for the subscription\n   * @param handler - Async generator function that yields subscription messages\n   * @returns This router instance for chaining\n   */\n  add<M extends Subscription>(\n    ns: Main<M>,\n    handler: LexRouterSubscriptionHandler<M, void>,\n  ): this\n  /**\n   * Registers a subscription handler with authentication.\n   *\n   * @param ns - The Lexicon namespace definition for the subscription\n   * @param config - Configuration object with handler and auth function\n   * @returns This router instance for chaining\n   */\n  add<M extends Subscription, Credentials>(\n    ns: Main<M>,\n    config: LexRouterSubscriptionConfig<M, Credentials>,\n  ): this\n  /**\n   * Registers a query or procedure handler without authentication.\n   *\n   * @param ns - The Lexicon namespace definition for the method\n   * @param handler - Handler function that processes requests\n   * @returns This router instance for chaining\n   */\n  add<M extends Query | Procedure>(\n    ns: Main<M>,\n    handler: LexRouterMethodHandler<M, void>,\n  ): this\n  /**\n   * Registers a query or procedure handler with authentication.\n   *\n   * @param ns - The Lexicon namespace definition for the method\n   * @param config - Configuration object with handler and auth function\n   * @returns This router instance for chaining\n   */\n  add<M extends Query | Procedure, Credentials>(\n    ns: Main<M>,\n    config: LexRouterMethodConfig<M, Credentials>,\n  ): this\n  /**\n   * Registers a Lexicon method handler.\n   *\n   * This is the unified overload that accepts any method type with optional authentication.\n   *\n   * @param ns - The Lexicon namespace definition\n   * @param config - Handler function or configuration object\n   * @returns This router instance for chaining\n   *\n   * @throws {TypeError} If a method with the same NSID is already registered\n   *\n   * @example\n   * ```typescript\n   * // Register without auth (credentials will be void)\n   * router.add(myQuery, async (ctx) => {\n   *   return { body: { data: 'value' } }\n   * })\n   *\n   * // Register with auth\n   * router.add(myProcedure, {\n   *   handler: async (ctx) => {\n   *     console.log('Caller:', ctx.credentials.userId)\n   *     return { body: { success: true } }\n   *   },\n   *   auth: async ({ request }) => ({ userId: await verifyToken(request) })\n   * })\n   * ```\n   */\n  add<M extends LexMethod, Credentials = unknown>(\n    ns: Main<M>,\n    config: M extends Subscription\n      ?\n          | LexRouterSubscriptionHandler<M, Credentials>\n          | LexRouterSubscriptionConfig<M, Credentials>\n      : M extends Query | Procedure\n        ?\n            | LexRouterMethodHandler<M, Credentials>\n            | LexRouterMethodConfig<M, Credentials>\n        : never,\n  ): this\n  add<M extends LexMethod>(\n    ns: Main<M>,\n    config:\n      | LexRouterSubscriptionHandler<any, any>\n      | LexRouterSubscriptionConfig<any, any>\n      | LexRouterMethodHandler<any, any>\n      | LexRouterMethodConfig<any, any>,\n  ) {\n    const method = getMain(ns)\n    const nsid = normalizeNsid(method.nsid)\n\n    if (this.handlers.has(nsid)) {\n      throw new TypeError(`Method ${method.nsid} already registered`)\n    }\n\n    const methodConfig =\n      typeof config === 'function'\n        ? { handler: config, auth: undefined }\n        : config\n\n    const handler: FetchHandler =\n      method.type === 'subscription'\n        ? this.buildSubscriptionHandler(\n            method,\n            methodConfig.handler as LexRouterSubscriptionHandler<any, any>,\n            methodConfig.auth,\n          )\n        : this.buildMethodHandler(\n            method,\n            methodConfig.handler as LexRouterMethodHandler<any, any>,\n            methodConfig.auth,\n          )\n\n    this.handlers.set(nsid, handler)\n\n    return this\n  }\n\n  private buildMethodHandler<Method extends Query | Procedure, Credentials>(\n    method: Method,\n    methodHandler: LexRouterMethodHandler<Method, Credentials>,\n    auth?: LexRouterAuth<Credentials, Method>,\n  ): FetchHandler {\n    const getInput = (\n      method.type === 'procedure'\n        ? getProcedureInput.bind(method)\n        : getQueryInput.bind(method)\n    ) as (request: Request) => Promise<InferMethodInput<Method, Body>>\n\n    return async (request, connection) => {\n      // @NOTE CORS requests should be handled by a middleware before reaching\n      // this point.\n      if (\n        (method.type === 'procedure' && request.method !== 'POST') ||\n        (method.type === 'query' &&\n          request.method !== 'GET' &&\n          request.method !== 'HEAD')\n      ) {\n        return invalidRequestResponse('Method not allowed', 405)\n      }\n\n      try {\n        const url = new URL(request.url)\n        const params = method.parameters.fromURLSearchParams(\n          url.searchParams,\n        ) as InferMethodParams<Method>\n\n        const credentials = auth\n          ? await auth({ method, params, request, connection })\n          : (undefined as Credentials)\n\n        const input = await getInput(request)\n\n        const output = await methodHandler({\n          credentials,\n          params,\n          input,\n          request,\n          connection,\n          signal: request.signal,\n        })\n\n        if (output instanceof Response) {\n          return output\n        }\n\n        // @TODO add validation of output based on method.output.schema?\n\n        if (output.body === undefined && output.encoding === undefined) {\n          return new Response(null, { status: 200, headers: output.headers })\n        }\n\n        if (method.output?.encoding === 'application/json') {\n          return Response.json(lexToJson(output.body as LexValue), {\n            status: 200,\n            headers: output.headers,\n          })\n        }\n\n        const headers = new Headers(output.headers)\n        headers.set('content-type', output.encoding!)\n        return new Response(output.body as BodyInit | null | undefined, {\n          status: 200,\n          headers,\n        })\n      } catch (error) {\n        return this.handlerError(request, method, error)\n      }\n    }\n  }\n\n  private buildSubscriptionHandler<Method extends Subscription, Credentials>(\n    method: Method,\n    methodHandler: LexRouterSubscriptionHandler<Method, Credentials>,\n    auth?: LexRouterAuth<Credentials, Method>,\n  ): FetchHandler {\n    const {\n      onSocketError,\n      upgradeWebSocket = (globalThis as any).Deno?.upgradeWebSocket as\n        | UpgradeWebSocket\n        | undefined,\n    } = this.options\n    if (!upgradeWebSocket) {\n      throw new TypeError(\n        'WebSocket upgrade not supported in this environment. Please provide an upgradeWebSocket option when creating the LexRouter.',\n      )\n    }\n\n    return async (request, connection) => {\n      if (request.method !== 'GET') {\n        return invalidRequestResponse('Method not allowed', 405)\n      }\n\n      if (\n        request.headers.get('connection')?.toLowerCase() !== 'upgrade' ||\n        request.headers.get('upgrade')?.toLowerCase() !== 'websocket'\n      ) {\n        return invalidRequestResponse(\n          'XRPC subscriptions are only available over WebSocket',\n          426,\n          {\n            Connection: 'Upgrade',\n            Upgrade: 'websocket',\n          },\n        )\n      }\n\n      if (request.signal.aborted) {\n        return invalidRequestResponse('Request aborted', 499)\n      }\n\n      try {\n        const { response, socket } = upgradeWebSocket(request)\n\n        // @NOTE We are using a distinct signal than request.signal because that\n        // signal may get aborted before the WebSocket is closed (this is the\n        // case with Deno).\n        const abortController = new AbortController()\n        const { signal } = abortController\n        const abort = () => abortController.abort()\n\n        const onOpen = async () => {\n          try {\n            const url = new URL(request.url)\n            const params = method.parameters.fromURLSearchParams(\n              url.searchParams,\n            ) as InferMethodParams<Method>\n\n            const credentials: Credentials = auth\n              ? await auth({ method, params, request, connection })\n              : (undefined as Credentials)\n\n            signal.throwIfAborted()\n\n            const iterable = methodHandler({\n              credentials,\n              params,\n              input: undefined as InferMethodInput<Method, Body>,\n              request,\n              connection,\n              signal,\n            })\n\n            const iterator = iterable[Symbol.asyncIterator]()\n\n            if (iterator.return) {\n              signal.addEventListener(\n                'abort',\n                () => {\n                  // @NOTE if iterator.return() throws, and no onSocketError is\n                  // provided, or if onSocketError itself throws, the error will\n                  // be unhandled, causing the process to crash. This is\n                  // intentional, as it surfaces critical errors that occur\n                  // during cleanup of the subscription.\n\n                  void new Promise((resolve) => {\n                    // Wrapping in new Promise to catch any potential sync errors thrown by iterator.return()\n                    resolve(iterator.return!())\n                  }).catch(\n                    onSocketError\n                      ? (error) => onSocketError({ request, method, error })\n                      : null,\n                  )\n                },\n                {\n                  once: true,\n                },\n              )\n            }\n\n            while (!signal.aborted && socket.readyState === 1) {\n              const result = await iterator.next()\n              if (result.done) break\n\n              // @TODO add validation of output based on method.output.schema?\n\n              const data = encodeMessageFrame(method, result.value)\n\n              socket.send(data)\n\n              // Apply backpressure by waiting for the buffered data to drain\n              // before generating the next message\n              await drainWebsocket(socket, signal, this.options)\n            }\n\n            if (socket.readyState === 1) {\n              socket.close(1000)\n            }\n          } catch (error) {\n            // If the socket is still open, send an error frame before closing\n            if (socket.readyState === 1) {\n              const isLexError = error instanceof LexError\n\n              // https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1\n              const code =\n                isLexError && method.errors?.includes(error.error)\n                  ? 1008 // Policy Violation for known LexErrors\n                  : 1011 // Internal Error for unexpected errors\n\n              if (isLexError) {\n                socket.send(encodeErrorFrame(error.toJSON()))\n                socket.close(code, error.error)\n              } else {\n                const error = 'InternalServerError'\n                const message = 'An internal error occurred'\n                socket.send(encodeErrorFrame({ error, message }))\n                socket.close(code, error)\n              }\n            }\n\n            if (onSocketError && !isAbortReason(signal, error)) {\n              await onSocketError({ request, method, error })\n            }\n          } finally {\n            abortController.abort()\n          }\n        }\n\n        socket.addEventListener('error', abort)\n        socket.addEventListener('close', abort)\n        socket.addEventListener('open', onOpen)\n        socket.addEventListener('message', onMessage)\n\n        return response\n      } catch (error) {\n        return this.handlerError(request, method, error)\n      }\n    }\n  }\n\n  private async handlerError(\n    request: Request,\n    method: LexMethod,\n    cause: unknown,\n  ) {\n    // Only report unexpected processing errors\n    if (isAbortReason(request.signal, cause)) {\n      return Response.json({ error: 'RequestAborted' }, { status: 499 })\n    }\n\n    const error = LexServerError.from(cause)\n\n    const { onHandlerError } = this.options\n    if (onHandlerError) await onHandlerError({ error, request, method })\n\n    return error.toResponse()\n  }\n\n  /**\n   * The main fetch handler for processing XRPC requests.\n   *\n   * Routes incoming requests to the appropriate method handler based on the\n   * NSID in the URL path. Returns appropriate error responses for invalid\n   * paths or unimplemented methods.\n   *\n   * This handler can be used directly with HTTP servers that support the\n   * fetch API pattern, or converted to a Node.js request listener using\n   * `toRequestListener()`.\n   *\n   * @param request - The incoming HTTP request\n   * @param connection - Optional connection metadata\n   * @returns A promise resolving to the HTTP response\n   *\n   * @example\n   * ```typescript\n   * // Use with Deno\n   * Deno.serve(router.fetch)\n   *\n   * // Use with Bun\n   * Bun.serve({ fetch: router.fetch })\n   *\n   * // Use with Node.js\n   * import { toRequestListener } from '@atproto/lex-server/nodejs'\n   * const listener = toRequestListener(router.fetch)\n   * http.createServer(listener).listen(3000)\n   * ```\n   */\n  fetch: FetchHandler = async (\n    request: Request,\n    connection?: ConnectionInfo,\n  ): Promise<Response> => {\n    const { pathname } = new URL(request.url)\n    const atprotoProxy = request.headers.get('atproto-proxy')\n\n    if (!pathname.startsWith(XRPC_PATH_PREFIX)) {\n      // Handle non XRPC paths\n      const { fallback } = this.options\n      if (fallback) return fallback(request, connection)\n      return new Response('Not Found', { status: 404 })\n    }\n\n    if (pathname === XRPC_HEALTH_CHECK_PATH) {\n      if (request.method !== 'GET') {\n        return invalidRequestResponse('Method not allowed', 405)\n      }\n      if (atprotoProxy != null) {\n        return invalidRequestResponse(\n          'atproto-proxy header is not allowed on health check endpoint',\n        )\n      }\n\n      const { healthCheck } = this.options\n      const data = healthCheck ? await healthCheck(request) : { status: 'ok' }\n\n      return Response.json(data)\n    }\n\n    const subPath = pathname.slice(XRPC_PATH_PREFIX.length)\n\n    if (!isNsidString(subPath)) {\n      return invalidRequestResponse('Invalid NSID in URL path')\n    }\n\n    const nsid = normalizeNsid(subPath)\n    if (atprotoProxy == null) {\n      const handler = this.handlers.get(nsid)\n      if (handler) return handler(request, connection)\n    } else {\n      // Handle service proxying logic.\n\n      const proxyInfo = parseAtprotoProxyHeader(atprotoProxy)\n      if (!proxyInfo) {\n        return invalidRequestResponse(\n          `Invalid atproto-proxy header value: ${atprotoProxy}`,\n        )\n      }\n\n      // @TODO actually implement service proxying logic here. The reason it was\n      // not done already is because we want to perform all the heavy lifting\n      // here, while still allowing the possibility to override the endpoint\n      // resolution, etc.\n\n      // @NOTE see ./service-auth.ts for potential common code (did resolver, etc.)\n    }\n\n    return Response.json(\n      {\n        error: 'MethodNotImplemented',\n        message: `XRPC method \"${nsid}\" not implemented on this server`,\n      },\n      { status: 501 },\n    )\n  }\n}\n\nasync function getProcedureInput<M extends Procedure>(\n  this: M,\n  request: Request,\n): Promise<InferMethodInput<M, Body>> {\n  const encodingRaw = request.headers\n    .get('content-type')\n    ?.split(';')[0]\n    .trim()\n    .toLowerCase()\n\n  const encoding =\n    encodingRaw ||\n    // If the caller did not provide a content-type, but the method\n    // expects an input, assume binary\n    (request.body != null && this.input.encoding != null\n      ? 'application/octet-stream'\n      : undefined)\n\n  if (!this.input.matchesEncoding(encoding)) {\n    throw new LexServerError(400, {\n      error: 'InvalidRequest',\n      message: `Invalid content-type: ${encoding}`,\n    })\n  }\n\n  if (this.input.encoding === 'application/json') {\n    // @TODO limit size?\n    const data = lexParse(await request.text())\n    const body = this.input.schema ? this.input.schema.parse(data) : data\n    return { encoding, body } as InferMethodInput<M, Body>\n  } else if (this.input.encoding) {\n    const body: Body = request\n    return { encoding, body } as InferMethodInput<M, Body>\n  } else {\n    return undefined as InferMethodInput<M, Body>\n  }\n}\n\nasync function getQueryInput<M extends Query>(\n  this: M,\n  request: Request,\n): Promise<InferMethodInput<M, Body>> {\n  if (\n    request.body ||\n    request.headers.has('content-type') ||\n    request.headers.has('content-length')\n  ) {\n    throw new LexServerError(400, {\n      error: 'InvalidRequest',\n      message: 'GET requests must not have a body',\n    })\n  }\n\n  return undefined as InferMethodInput<M, Body>\n}\n\nfunction onMessage(this: WebSocket, _event: unknown) {\n  const error = 'InvalidRequest'\n  const message = 'XRPC subscriptions do not accept messages'\n  this.send(encodeErrorFrame({ error, message }))\n  // 1003 indicates that an endpoint is terminating the connection\n  // because it has received a type of data it cannot accept (e.g., an\n  // endpoint that understands only text data MAY send this if it\n  // receives a binary message).\n  this.close(1003, error)\n}\n\n// Pre-encoded frame header for error frames\nconst ERROR_FRAME_HEADER = /*#__PURE__*/ encode({ op: -1 })\n\nfunction encodeErrorFrame(errorData: LexErrorData): Uint8Array {\n  return ui8Concat([ERROR_FRAME_HEADER, encode(errorData)])\n}\n\n// Pre-encoded frame header for message frames with unknown type\nconst UNKNOWN_MESSAGE_FRAME_HEADER = /*#__PURE__*/ encode({ op: 1 })\n\nfunction encodeMessageFrame(method: Subscription, value: LexValue): Uint8Array {\n  if (isPlainObject(value) && typeof value.$type === 'string') {\n    const { $type, ...rest } = value\n    return ui8Concat([\n      encode({\n        op: 1,\n        t:\n          // If $type starts with `nsid#`, strip the NSID prefix\n          $type.charCodeAt(0) !== 0x23 && // '#'\n          $type.charCodeAt(method.nsid.length) === 0x23 && // '#'\n          $type.startsWith(method.nsid)\n            ? $type.slice(method.nsid.length)\n            : $type,\n      }),\n      encode(rest),\n    ])\n  }\n\n  return ui8Concat([UNKNOWN_MESSAGE_FRAME_HEADER, encode(value)])\n}\n\nfunction isAbortReason(signal: AbortSignal, error: unknown): boolean {\n  return (\n    signal.aborted &&\n    signal.reason != null &&\n    error instanceof Error &&\n    (error === signal.reason || error.cause === signal.reason)\n  )\n}\n\nexport type ServiceProxyInfo = {\n  did: DidString\n  serviceId: string\n}\n\nfunction parseAtprotoProxyHeader(value: string): ServiceProxyInfo | null {\n  // /!\\ Hot path\n\n  // (fast) sanity check to avoid unnecessary parsing for non-DID values\n  if (!value.startsWith('did:')) return null\n\n  // The format is expected to be `did:example:service#serviceId`\n  const hashIndex = value.indexOf('#')\n  if (hashIndex === -1) return null\n\n  const fragmentIndex = hashIndex + 1\n  // Basic validation if the fragment\n  if (fragmentIndex === value.length) return null\n  if (value.includes('#', fragmentIndex)) return null\n  if (value.includes(' ', fragmentIndex)) return null\n\n  const did = value.slice(0, hashIndex)\n  if (!isDidString(did)) return null\n\n  const serviceId = value.slice(fragmentIndex)\n  return { did, serviceId }\n}\n\nfunction normalizeNsid(nsid: NsidString): NsidString {\n  const lastDotIdx = nsid.lastIndexOf('.')\n\n  // The domain name part of the NSID is case-insensitive, but the last part is\n  // case-sensitive. Normalize the domain part to lowercase.\n  if (lastDotIdx !== -1 && hasUpperCase(nsid, 0, lastDotIdx)) {\n    return `${nsid.slice(0, lastDotIdx).toLowerCase()}.${nsid.slice(lastDotIdx + 1)}` as NsidString\n  }\n\n  return nsid\n}\n\nfunction hasUpperCase(str: string, start = 0, end = str.length): boolean {\n  for (let i = start; i < end; i++) {\n    const code = str.charCodeAt(i)\n    if (code >= 0x41 && code <= 0x5a) {\n      return true\n    }\n  }\n  return false\n}\n\nfunction invalidRequestResponse(\n  message: string,\n  status = 400,\n  headers?: HeadersInit,\n): Response {\n  return Response.json(\n    {\n      error: 'InvalidRequest',\n      message,\n    },\n    { status, headers },\n  )\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/lib/drain-websocket.ts",
    "content": "import { abortableSleep } from './sleep.js'\n\n/**\n * Performs polling based backpressure management for a WebSocket connection. If\n * the amount of buffered data exceeds the specified high water mark, this\n * function will wait until the buffered amount drops below the low water mark\n * before resolving. This is useful for preventing memory issues when sending\n * large amounts of data over a WebSocket connection.\n */\nexport async function drainWebsocket(\n  socket: WebSocket,\n  signal: AbortSignal,\n  {\n    highWaterMark = 250_000, // 250 KB\n    lowWaterMark = 50_000, // 50 KB\n  }: {\n    highWaterMark?: number\n    lowWaterMark?: number\n  } = {},\n): Promise<void> {\n  if (socket.bufferedAmount > highWaterMark) {\n    // Once we exceed the high water mark, we wait until the buffered amount\n    // drops below the low water mark before allowing more data to be sent. This\n    // creates a hysteresis effect that prevents rapid toggling around the\n    // threshold.\n    while (\n      socket.readyState === 1 &&\n      socket.bufferedAmount !== 0 &&\n      socket.bufferedAmount > lowWaterMark\n    ) {\n      await abortableSleep(10, signal)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/lib/sleep.ts",
    "content": "export async function abortableSleep(\n  ms: number,\n  signal: AbortSignal,\n): Promise<void> {\n  signal.throwIfAborted()\n\n  return new Promise((resolve, reject) => {\n    const cleanup = () => {\n      signal.removeEventListener('abort', onAbort)\n      clearTimeout(timeoutHandle)\n    }\n\n    const timeoutHandle = setTimeout(() => {\n      cleanup()\n      resolve()\n    }, ms)\n\n    const onAbort = () => {\n      cleanup()\n      reject(signal.reason)\n    }\n\n    signal.addEventListener('abort', onAbort)\n  })\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/lib/www-authenticate.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { formatWWWAuthenticateHeader } from './www-authenticate.js'\n\ndescribe(formatWWWAuthenticateHeader, () => {\n  describe('single scheme with params object', () => {\n    it('formats a Bearer challenge with params', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: { realm: 'api.example.com', error: 'InvalidToken' },\n      })\n      expect(result).toBe(\n        'Bearer realm=\"api.example.com\", error=\"InvalidToken\"',\n      )\n    })\n\n    it('omits undefined param values', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: { realm: 'api', error: undefined },\n      })\n      expect(result).toBe('Bearer realm=\"api\"')\n    })\n\n    it('omits null param values', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: { realm: 'api', error: null as any },\n      })\n      expect(result).toBe('Bearer realm=\"api\"')\n    })\n\n    it('outputs only the scheme when all params are undefined', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: {},\n      })\n      expect(result).toBe('Bearer')\n    })\n  })\n\n  describe('single scheme with token68 string', () => {\n    it('formats a token68 value', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: 'base64encodedvalue==',\n      })\n      expect(result).toBe('Bearer base64encodedvalue==')\n    })\n  })\n\n  describe('multiple schemes', () => {\n    it('joins multiple different schemes with a comma', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: { realm: 'api' },\n        Basic: { realm: 'api' },\n      })\n      expect(result).toBe('Bearer realm=\"api\", Basic realm=\"api\"')\n    })\n  })\n\n  describe('array of challenges for the same scheme (new feature)', () => {\n    it('emits one challenge per array element', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: [\n          { realm: 'first', error: 'TokenExpired' },\n          { realm: 'second', error: 'TokenRevoked' },\n        ],\n      })\n      expect(result).toBe(\n        'Bearer realm=\"first\", error=\"TokenExpired\", Bearer realm=\"second\", error=\"TokenRevoked\"',\n      )\n    })\n\n    it('handles an array mixing token68 strings and param objects', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: ['token68value', { realm: 'api', error: 'BadToken' }],\n      })\n      expect(result).toBe(\n        'Bearer token68value, Bearer realm=\"api\", error=\"BadToken\"',\n      )\n    })\n\n    it('handles an array of token68 strings', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: ['firstToken', 'secondToken'],\n      })\n      expect(result).toBe('Bearer firstToken, Bearer secondToken')\n    })\n\n    it('handles an array with a single element', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: [{ realm: 'api' }],\n      })\n      expect(result).toBe('Bearer realm=\"api\"')\n    })\n\n    it('handles array challenges alongside other schemes', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: [\n          { realm: 'r1', error: 'Err1' },\n          { realm: 'r2', error: 'Err2' },\n        ],\n        Basic: { realm: 'fallback' },\n      })\n      expect(result).toBe(\n        'Bearer realm=\"r1\", error=\"Err1\", Bearer realm=\"r2\", error=\"Err2\", Basic realm=\"fallback\"',\n      )\n    })\n  })\n\n  describe('null / undefined scheme values', () => {\n    it('skips schemes with null value', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: null as any,\n        Basic: { realm: 'api' },\n      })\n      expect(result).toBe('Basic realm=\"api\"')\n    })\n\n    it('skips schemes with undefined value', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: undefined,\n        Basic: { realm: 'api' },\n      })\n      expect(result).toBe('Basic realm=\"api\"')\n    })\n\n    it('returns an empty string when all schemes are null/undefined', () => {\n      const result = formatWWWAuthenticateHeader({\n        Bearer: undefined,\n      })\n      expect(result).toBe('')\n    })\n  })\n\n  it('returns an empty string for an empty object', () => {\n    expect(formatWWWAuthenticateHeader({})).toBe('')\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-server/src/lib/www-authenticate.ts",
    "content": "/**\n * Type representing the value of a WWW-Authenticate HTTP header.\n *\n * Supports multiple authentication schemes, each with optional parameters.\n * Parameters can be provided as a token68 string (for schemes like Bearer)\n * or as key-value pairs.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 | RFC 7235 Section 4.1}\n *\n * @example Bearer scheme with parameters\n * ```typescript\n * const auth: WWWAuthenticate = {\n *   Bearer: {\n *     realm: 'api.example.com',\n *     error: 'InvalidToken',\n *     error_description: 'The token has expired'\n *   }\n * }\n * // Formats to: Bearer realm=\"api.example.com\", error=\"InvalidToken\", error_description=\"The token has expired\"\n * ```\n *\n * @example Multiple schemes\n * ```typescript\n * const auth: WWWAuthenticate = {\n *   Bearer: { realm: 'api' },\n *   Basic: { realm: 'api' }\n * }\n * // Formats to: Bearer realm=\"api\", Basic realm=\"api\"\n * ```\n *\n * @example Token68 value (no parameters)\n * ```typescript\n * const auth: WWWAuthenticate = {\n *   Bearer: 'base64encodedvalue=='\n * }\n * // Formats to: Bearer base64encodedvalue==\n * ```\n */\nexport type WWWAuthenticate = {\n  [authScheme in string]?:\n    | string // token68\n    | WWWAuthenticateParams\n    | (string | WWWAuthenticateParams)[]\n}\n\nexport type WWWAuthenticateParams = { [authParam in string]?: string }\n\n/**\n * Formats a WWWAuthenticate object into an HTTP header string.\n *\n * Converts the structured authentication scheme and parameter data into\n * the proper WWW-Authenticate header format per RFC 7235.\n *\n * @param wwwAuthenticate - The authentication schemes and parameters\n * @returns Formatted header string ready for use in HTTP responses\n *\n * @example\n * ```typescript\n * const header = formatWWWAuthenticateHeader({\n *   Bearer: {\n *     realm: 'api.example.com',\n *     error: 'MissingToken'\n *   }\n * })\n * // Returns: 'Bearer realm=\"api.example.com\", error=\"MissingToken\"'\n * ```\n *\n * @example Empty or undefined values\n * ```typescript\n * const header = formatWWWAuthenticateHeader({\n *   Bearer: { realm: 'api', error: undefined }\n * })\n * // Returns: 'Bearer realm=\"api\"' (undefined values are omitted)\n * ```\n */\nexport function formatWWWAuthenticateHeader(\n  wwwAuthenticate: WWWAuthenticate,\n): string {\n  const challenges: string[] = []\n  for (const [scheme, params] of Object.entries(wwwAuthenticate)) {\n    if (params == null) continue\n\n    if (typeof params === 'string') {\n      challenges.push(formatWWWAuthenticateChallenge(scheme, params))\n    } else if (Array.isArray(params)) {\n      for (const p of params) {\n        challenges.push(formatWWWAuthenticateChallenge(scheme, p))\n      }\n    } else {\n      challenges.push(formatWWWAuthenticateChallenge(scheme, params))\n    }\n  }\n  return challenges.join(', ')\n}\n\nfunction formatWWWAuthenticateChallenge(\n  scheme: string,\n  params: string | WWWAuthenticateParams,\n): string {\n  const paramsStr =\n    typeof params === 'string' ? params : formatWWWAuthenticateParams(params)\n  return paramsStr?.length ? `${scheme} ${paramsStr}` : scheme\n}\n\nfunction formatWWWAuthenticateParams(params: WWWAuthenticateParams): string {\n  const parts: string[] = []\n  for (const [name, val] of Object.entries(params)) {\n    if (val != null) parts.push(`${name}=${JSON.stringify(val)}`)\n  }\n  return parts.join(', ')\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/nodejs.test.ts",
    "content": "import { AddressInfo } from 'node:net'\nimport { scheduler } from 'node:timers/promises'\nimport { afterAll, beforeAll, describe, expect, it } from 'vitest'\nimport { Server, serve } from './nodejs.js'\n\ndescribe('Node.js RequestListener', () => {\n  let server: Server\n  let address: string\n\n  beforeAll(async () => {\n    server = await serve(async (request) => {\n      const { pathname } = new URL(request.url)\n      if (pathname === '/hello') {\n        return new Response('Hello, world!', {\n          status: 200,\n          headers: { 'Content-Type': 'text/plain' },\n        })\n      } else if (pathname === '/throw') {\n        throw new Error('Test error')\n      } else if (pathname === '/echo') {\n        return new Response(request.body, {\n          status: 200,\n          headers: { 'Content-Type': 'application/octet-stream' },\n        })\n      }\n      return new Response('Not Found', { status: 404 })\n    })\n    const { port } = server.address() as AddressInfo\n    address = `http://localhost:${port}`\n  })\n\n  afterAll(async () => {\n    await server.terminate()\n  })\n\n  it('should respond with Hello, world! on /hello', async () => {\n    const res = await fetch(new URL(`/hello`, address))\n    const text = await res.text()\n    expect(res.status).toBe(200)\n    expect(text).toBe('Hello, world!')\n  })\n\n  it('should respond with Not Found on unknown path', async () => {\n    const res = await fetch(new URL(`/unknown`, address))\n    const text = await res.text()\n    expect(res.status).toBe(404)\n    expect(text).toBe('Not Found')\n  })\n\n  it('should handle thrown errors and respond with 500', async () => {\n    const res = await fetch(new URL(`/throw`, address))\n    const text = await res.text()\n    expect(res.status).toBe(500)\n    expect(text).toBe('Internal Server Error')\n  })\n\n  it('should handle streaming bodies', async () => {\n    const totalSize = 1024 * 1024\n    const consumerSize = 42 * 1024\n\n    let sentBytes = 0\n    let receivedBytes = 0\n\n    const res = await fetch(new URL(`/echo`, address), {\n      method: 'POST',\n      // @ts-expect-error\n      duplex: 'half',\n      body: new ReadableStream({\n        async pull(controller) {\n          const chunkSize = Math.min(1024, totalSize - sentBytes)\n          controller.enqueue('A'.repeat(chunkSize))\n          sentBytes += chunkSize\n          await scheduler.wait(0) // Yield to event loop\n          if (sentBytes === totalSize) controller.close()\n        },\n      }),\n    })\n\n    const reader = res.body!.getReader()\n\n    // eslint-disable-next-line no-constant-condition\n    while (true) {\n      const result = await reader.read()\n      if (result.done) break\n      receivedBytes += Buffer.byteLength(result.value)\n      if (receivedBytes >= consumerSize) {\n        await reader.cancel()\n        break\n      }\n    }\n\n    expect(receivedBytes).toBeGreaterThanOrEqual(consumerSize)\n    expect(sentBytes).toBeGreaterThanOrEqual(consumerSize)\n    expect(sentBytes).toBeLessThan(totalSize)\n  })\n\n  it('should echo back request body on /echo', async () => {\n    const body = `Echo this back`\n    const res = await fetch(new URL(`/echo`, address), {\n      method: 'POST',\n      body,\n    })\n    const text = await res.text()\n    expect(res.status).toBe(200)\n    expect(text).toBe(body)\n  })\n})\n"
  },
  {
    "path": "packages/lex/lex-server/src/nodejs.ts",
    "content": "import { once } from 'node:events'\nimport {\n  IncomingHttpHeaders,\n  IncomingMessage,\n  RequestListener,\n  Server as HttpServer,\n  ServerOptions,\n  ServerResponse,\n  createServer as createHttpServer,\n} from 'node:http'\nimport { ListenOptions } from 'node:net'\nimport { Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport type { ReadableStream as NodeReadableStream } from 'node:stream/web'\nimport { createHttpTerminator } from 'http-terminator'\nimport { WebSocket as WebSocketPonyfill, WebSocketServer } from 'ws'\nimport type { FetchHandler } from './lex-router.js'\n\n// @ts-expect-error\nSymbol.asyncDispose ??= Symbol.for('Symbol.asyncDispose')\n\nconst kResponseWs = Symbol.for('@atproto/lex-server:WebSocket')\n\nfunction isUpgradeRequest(request: Request, upgrade: string): boolean {\n  return (\n    request.method === 'GET' &&\n    request.headers.get('connection')?.toLowerCase() === 'upgrade' &&\n    request.headers.get('upgrade')?.toLowerCase() === upgrade\n  )\n}\n\n/**\n * Upgrades an HTTP request to a WebSocket connection for Node.js.\n *\n * This function must be passed to the {@link LexRouter} constructor to enable\n * subscription (WebSocket) support on Node.js. It creates a WebSocket instance\n * and a placeholder response that signals the need for protocol upgrade.\n *\n * The actual upgrade is handled internally when the response is sent through\n * {@link sendResponse}.\n *\n * @param request - The incoming HTTP request to upgrade\n * @returns An object containing the WebSocket and upgrade response\n * @throws {TypeError} If the request is not a valid WebSocket upgrade request\n *\n * @example\n * ```typescript\n * import { LexRouter } from '@atproto/lex-server'\n * import { upgradeWebSocket } from '@atproto/lex-server/nodejs'\n *\n * // Pass to router for subscription support\n * const router = new LexRouter({ upgradeWebSocket })\n *\n * // Now you can add subscription handlers\n * router.add(subscribeRepos, async function* (ctx) {\n *   for await (const event of eventStream) {\n *     yield event\n *   }\n * })\n * ```\n */\nexport function upgradeWebSocket(request: Request): {\n  response: Response\n  socket: WebSocket\n} {\n  if (!isUpgradeRequest(request, 'websocket')) {\n    throw new TypeError('upgradeWebSocket() expects a WebSocket upgrade')\n  }\n\n  // Placeholder response for WebSocket upgrade. The actual handling will happen\n  // through the handleWebSocketUpgrade function. Headers set on the response\n  // will be applied during the upgrade.\n  const response = new Response(null, { status: 200 })\n\n  // The Response constructor does not allow setting status 101, so we\n  // define it directly. The purpose of this response is just to signal\n  // that an upgrade is needed, and to carry any headers.\n  Object.defineProperty(response, 'status', {\n    value: 101,\n    enumerable: false,\n    configurable: false,\n    writable: false,\n  })\n\n  // @ts-expect-error\n  const socket: WebSocket = new WebSocketPonyfill(null, undefined, {\n    autoPong: true,\n  })\n\n  // Attach the WebSocket to the response for later retrieval\n  Object.defineProperty(response, kResponseWs, {\n    value: socket,\n    enumerable: false,\n    configurable: false,\n    writable: false,\n  })\n\n  return { response, socket }\n}\n\nconst kUpgradeEvent = Symbol.for('@atproto/lex-server:upgrade')\n\nfunction handleWebSocketUpgrade(\n  req: IncomingMessage,\n  response: Response,\n): void {\n  const ws = (response as { [kResponseWs]?: WebSocketPonyfill })[kResponseWs]\n  if (!ws) throw new TypeError('Response not created by upgradeWebSocket()')\n\n  // Create a one time use WebSocketServer to handle the upgrade\n  const wss = new WebSocketServer({\n    autoPong: true,\n    noServer: true,\n    clientTracking: false,\n    perMessageDeflate: true,\n    // @ts-expect-error\n    WebSocket: function () {\n      // Return the websocket that was created earlier instead of a new instance\n      return ws\n    },\n  })\n\n  // Apply headers that might have been set on the response object during\n  // handling. This will be called during wss.handleUpgrade().\n  wss.on('headers', (headers) => {\n    for (const [name, value] of response.headers) {\n      headers.push(`${name}: ${value}`)\n    }\n  })\n\n  wss.handleUpgrade(req, req.socket, Buffer.alloc(0), (_socket) => {\n    // @TODO find a way to properly \"close\" the _socket when the server is\n    // shutting down (might require replacing http-terminator with a local\n    // implementation)\n\n    req.emit(kUpgradeEvent, ws)\n  })\n}\n\n/**\n * Sends a fetch API Response through a Node.js ServerResponse.\n *\n * Handles both regular HTTP responses and WebSocket upgrades. For WebSocket\n * upgrades (status 101), delegates to the WebSocket upgrade handler.\n *\n * This function is used internally by {@link toRequestListener} and\n * {@link createServer}, but can be used directly for custom integrations.\n *\n * @param req - The Node.js IncomingMessage\n * @param res - The Node.js ServerResponse to write to\n * @param response - The fetch API Response to send\n * @throws {TypeError} If headers have already been sent\n *\n * @example\n * ```typescript\n * import http from 'node:http'\n * import { sendResponse } from '@atproto/lex-server/nodejs'\n *\n * const server = http.createServer(async (req, res) => {\n *   const response = new Response('Hello, World!', {\n *     headers: { 'Content-Type': 'text/plain' }\n *   })\n *   await sendResponse(req, res, response)\n * })\n * ```\n */\nexport async function sendResponse(\n  req: IncomingMessage,\n  res: ServerResponse,\n  response: Response,\n): Promise<void> {\n  // Invalid usage\n  if (res.headersSent) {\n    throw new TypeError('Response has already been sent')\n  }\n\n  if (response.status === 101) {\n    return handleWebSocketUpgrade(req, response)\n  }\n\n  res.statusCode = response.status\n  res.statusMessage = response.statusText\n\n  for (const [key, value] of response.headers) {\n    res.setHeader(key, value)\n  }\n\n  if (response.body != null && req.method !== 'HEAD') {\n    const stream = Readable.fromWeb(response.body as NodeReadableStream)\n    await pipeline(stream, res)\n  } else {\n    await response.body?.cancel()\n    res.end()\n  }\n}\n\nfunction toRequest(req: IncomingMessage): Request {\n  const host = req.headers.host ?? req.socket.localAddress ?? 'localhost'\n  const isEncrypted = (req.socket as any).encrypted === true\n  const protocol = isEncrypted ? 'https' : 'http'\n  const url = new URL(req.url ?? '/', `${protocol}://${host}`)\n  const headers = toHeaders(req.headers)\n  const body = toBody(req)\n  const signal = requestSignal(req)\n\n  return new Request(url, {\n    signal,\n    method: req.method,\n    headers,\n    body,\n    referrer: headers.get('referrer') ?? headers.get('referer') ?? undefined,\n    redirect: 'manual',\n    // @ts-expect-error\n    duplex: body ? 'half' : undefined,\n  })\n}\n\nfunction requestSignal(req: IncomingMessage): AbortSignal {\n  if (req.destroyed) return AbortSignal.abort()\n\n  const abortController = new AbortController()\n\n  const abort = (err?: Error | WebSocket) => {\n    abortController.abort(err instanceof Error ? err : undefined)\n\n    req.off('close', abort)\n    req.off('error', abort)\n    req.off('end', abort)\n    req.off(kUpgradeEvent, abort)\n  }\n\n  req.on('close', abort)\n  req.on('error', abort)\n  req.on('end', abort)\n  req.on(kUpgradeEvent, abort)\n\n  return abortController.signal\n}\n\nfunction requestCompletion(req: IncomingMessage): Promise<void> {\n  if (req.destroyed) return Promise.resolve()\n\n  // Unlike the abort signal, we complete the promise only when the request\n  // is fully done, accounting for websocket upgrade.\n  return new Promise((resolve) => {\n    const cleanup = () => {\n      req.off('close', done)\n      req.off('error', done)\n      req.off('end', done)\n      req.off(kUpgradeEvent, onUpgrade)\n    }\n\n    const onUpgrade = (ws: WebSocket) => {\n      cleanup()\n      ws.addEventListener('close', () => resolve())\n    }\n\n    const done = () => {\n      resolve()\n      cleanup()\n    }\n\n    req.on('close', done)\n    req.on('error', done)\n    req.on('end', done)\n    req.on(kUpgradeEvent, onUpgrade)\n  })\n}\n\nfunction toHeaders(headers: IncomingHttpHeaders): Headers {\n  const result = new Headers()\n  for (const [key, value] of Object.entries(headers)) {\n    if (value === undefined) continue\n    if (Array.isArray(value)) {\n      for (const v of value) result.append(key, v)\n    } else {\n      result.set(key, value)\n    }\n  }\n  return result\n}\n\nfunction toBody(req: IncomingMessage): null | ReadableStream {\n  if (\n    req.method === 'GET' ||\n    req.method === 'HEAD' ||\n    req.method === 'OPTIONS'\n  ) {\n    return null\n  }\n\n  if (\n    req.headers['content-type'] == null &&\n    req.headers['transfer-encoding'] == null &&\n    req.headers['content-length'] == null\n  ) {\n    return null\n  }\n\n  return Readable.toWeb(req) as ReadableStream\n}\n\n/**\n * Network address type for Node.js TCP connections.\n *\n * @example\n * ```typescript\n * const addr: NetAddr = {\n *   transport: 'tcp',\n *   hostname: '192.168.1.100',\n *   port: 54321\n * }\n * ```\n */\nexport type NetAddr = {\n  /** Always 'tcp' for Node.js HTTP connections. */\n  transport: 'tcp'\n  /** The IP address of the remote client. */\n  hostname: string\n  /** The port number of the remote client. */\n  port: number\n}\n\n/**\n * Connection metadata for Node.js HTTP requests.\n *\n * Provides information about the client connection, including the remote\n * address and a promise that resolves when the connection is fully closed\n * (including WebSocket connections).\n */\nexport type NodeConnectionInfo = {\n  /** Promise that resolves when the connection is fully closed. */\n  completed: Promise<void>\n  /** The remote address of the client, if available. */\n  remoteAddr: NetAddr | undefined\n}\n\n/**\n * Interface for objects that can handle fetch-style requests.\n *\n * Used by {@link createServer} and {@link serve} to accept either\n * a fetch handler function or an object with a `fetch` method\n * (like {@link LexRouter}).\n */\nexport interface HandlerObject {\n  /** The fetch handler method. */\n  fetch: FetchHandler\n}\n\nasync function handleRequest(\n  req: IncomingMessage,\n  res: ServerResponse,\n  fetchHandler: FetchHandler,\n) {\n  const request = toRequest(req)\n  const info = toConnectionInfo(req)\n  const response = await fetchHandler(request, info)\n  await sendResponse(req, res, response)\n}\n\nfunction toConnectionInfo(req: IncomingMessage): NodeConnectionInfo {\n  const { socket } = req\n\n  return {\n    completed: requestCompletion(req),\n    remoteAddr:\n      socket.remoteAddress != null\n        ? {\n            transport: 'tcp',\n            hostname: socket.remoteAddress,\n            port: socket.remotePort!,\n          }\n        : undefined,\n  }\n}\n\n/**\n * Converts a fetch-style handler to a Node.js request listener.\n *\n * The returned listener can be used with Node.js HTTP servers directly,\n * or as middleware in frameworks like Express (supports the `next` callback).\n *\n * @typeParam Request - The request class type (default: IncomingMessage)\n * @typeParam Response - The response class type (default: ServerResponse)\n * @param fetchHandler - The fetch-style handler function\n * @returns A Node.js RequestListener compatible with http.createServer\n *\n * @example Using as Express middleware\n * ```typescript\n * import express from 'express'\n * import { toRequestListener } from '@atproto/lex-server/nodejs'\n * import { LexRouter } from '@atproto/lex-server'\n *\n * const router = new LexRouter()\n * // Register handlers...\n *\n * const app = express()\n *\n * // Mount the XRPC router\n * app.use('/xrpc', toRequestListener(router.fetch))\n * ```\n */\nexport function toRequestListener<\n  Request extends typeof IncomingMessage = typeof IncomingMessage,\n  Response extends typeof ServerResponse<\n    InstanceType<Request>\n  > = typeof ServerResponse,\n>(fetchHandler: FetchHandler) {\n  return ((\n    req: InstanceType<Request>,\n    res: InstanceType<Response> & { req: InstanceType<Request> },\n    next?: (err?: unknown) => void,\n  ): void => {\n    handleRequest(req, res, fetchHandler).catch((err) => {\n      if (next) next(err)\n      else {\n        if (!res.headersSent) {\n          res.statusCode = 500\n          res.setHeader('content-type', 'text/plain; charset=utf-8')\n          res.end('Internal Server Error')\n        } else if (!res.writableEnded) {\n          res.destroy()\n        }\n      }\n    })\n  }) satisfies RequestListener<Request, Response>\n}\n\n/**\n * Options for creating an XRPC server.\n *\n * Extends Node.js {@link ServerOptions} with additional options for graceful shutdown.\n */\nexport type CreateServerOptions<\n  Request extends typeof IncomingMessage = typeof IncomingMessage,\n  Response extends typeof ServerResponse<\n    InstanceType<Request>\n  > = typeof ServerResponse,\n> = ServerOptions<Request, Response> & {\n  /**\n   * Timeout in milliseconds for graceful termination.\n   *\n   * When `terminate()` is called, the server will wait up to this duration\n   * for active connections to complete before forcibly closing them.\n   */\n  gracefulTerminationTimeout?: number\n}\n\n/**\n * Extended HTTP server with graceful shutdown support.\n *\n * Extends the standard Node.js HttpServer with a `terminate()` method\n * for graceful shutdown and implements `AsyncDisposable` for use with\n * `await using`.\n *\n * @typeParam Request - The request class type\n * @typeParam Response - The response class type\n *\n * @example Graceful shutdown\n * ```typescript\n * const server = createServer(router)\n * server.listen(3000)\n *\n * process.on('SIGTERM', async () => {\n *   console.log('Shutting down...')\n *   await server.terminate()\n *   console.log('Server stopped')\n * })\n * ```\n *\n * @example Using with await using\n * ```typescript\n * await using server = await serve(router, { port: 3000 })\n * // Server will be automatically terminated when scope exits\n * ```\n */\nexport interface Server<\n  Request extends typeof IncomingMessage = typeof IncomingMessage,\n  Response extends typeof ServerResponse<\n    InstanceType<Request>\n  > = typeof ServerResponse,\n> extends HttpServer<Request, Response>,\n    AsyncDisposable {\n  /**\n   * Gracefully terminates the server.\n   *\n   * Stops accepting new connections and waits for active connections\n   * to complete (up to `gracefulTerminationTimeout`).\n   *\n   * @returns Promise that resolves when the server is fully stopped\n   */\n  terminate(): Promise<void>\n  [Symbol.asyncDispose](): Promise<void>\n}\n\n/**\n * Creates an HTTP server configured for XRPC request handling.\n *\n * The server includes graceful shutdown support and can be used with\n * either a fetch handler function or an object with a `fetch` method\n * (like {@link LexRouter}).\n *\n * Note: This creates the server but does not start listening. Call\n * `server.listen()` to start the server, or use {@link serve} for\n * a combined create-and-listen operation.\n *\n * @typeParam Request - The request class type\n * @typeParam Response - The response class type\n * @param handler - A fetch handler or object with fetch method\n * @param options - Server configuration options\n * @returns An HTTP server with graceful shutdown support\n *\n * @example Basic usage\n * ```typescript\n * import { LexRouter } from '@atproto/lex-server'\n * import { createServer, upgradeWebSocket } from '@atproto/lex-server/nodejs'\n *\n * const router = new LexRouter({ upgradeWebSocket })\n * router.add(myMethod, myHandler)\n *\n * const server = createServer(router)\n * server.listen(3000, () => {\n *   console.log('Server listening on port 3000')\n * })\n * ```\n *\n * @example With graceful termination timeout\n * ```typescript\n * const server = createServer(router, {\n *   gracefulTerminationTimeout: 10000 // 10 seconds\n * })\n * ```\n */\nexport function createServer<\n  Request extends typeof IncomingMessage = typeof IncomingMessage,\n  Response extends typeof ServerResponse<\n    InstanceType<Request>\n  > = typeof ServerResponse,\n>(\n  handler: FetchHandler | HandlerObject,\n  options: CreateServerOptions<Request, Response> = {},\n): Server<Request, Response> {\n  const fetchHandler =\n    typeof handler === 'function' ? handler : handler.fetch.bind(handler)\n\n  const listener = toRequestListener(fetchHandler)\n  const server = createHttpServer(options, listener)\n\n  const terminator = createHttpTerminator({\n    server: server as HttpServer,\n    gracefulTerminationTimeout: options?.gracefulTerminationTimeout,\n  })\n\n  const terminate = async function terminate(this: Server<Request, Response>) {\n    if (this !== server) {\n      throw new TypeError('Server.terminate called with incorrect context')\n    }\n    // @TODO properly close all active WebSocket connections\n    return terminator.terminate()\n  }\n\n  Object.defineProperty(server, 'terminate', {\n    value: terminate,\n    enumerable: false,\n    configurable: false,\n    writable: false,\n  })\n\n  Object.defineProperty(server, Symbol.asyncDispose, {\n    value: terminate,\n    enumerable: false,\n    configurable: false,\n    writable: false,\n  })\n\n  return server as Server<Request, Response>\n}\n\n/**\n * Combined options for creating and starting an XRPC server.\n *\n * Includes both server creation options and network listen options.\n *\n * @typeParam Request - The request class type\n * @typeParam Response - The response class type\n *\n * @example\n * ```typescript\n * const options: StartServerOptions = {\n *   port: 3000,\n *   host: '0.0.0.0',\n *   gracefulTerminationTimeout: 10000\n * }\n * ```\n */\nexport type StartServerOptions<\n  Request extends typeof IncomingMessage = typeof IncomingMessage,\n  Response extends typeof ServerResponse<\n    InstanceType<Request>\n  > = typeof ServerResponse,\n> = ListenOptions & CreateServerOptions<Request, Response>\n\n/**\n * Creates and starts an HTTP server, returning when it's ready to accept connections.\n *\n * This is a convenience function that combines {@link createServer} and `server.listen()`\n * into a single async operation. The returned promise resolves once the server\n * is actively listening.\n *\n * @typeParam Request - The request class type\n * @typeParam Response - The response class type\n * @param handler - A fetch handler or object with fetch method (like {@link LexRouter})\n * @param options - Combined server and listen options\n * @returns Promise resolving to the running server\n *\n * @example Basic usage\n * ```typescript\n * import { LexRouter } from '@atproto/lex-server'\n * import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'\n *\n * const router = new LexRouter({ upgradeWebSocket })\n *\n * // Register handlers\n * router.add(getProfile, async (ctx) => {\n *   return { body: await db.getProfile(ctx.params.actor) }\n * })\n *\n * // Start server on port 3000\n * const server = await serve(router, { port: 3000 })\n * console.log('Server listening on port 3000')\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => server.terminate())\n * process.on('SIGINT', () => server.terminate())\n * ```\n *\n * @example With all options\n * ```typescript\n * const server = await serve(router, {\n *   port: 3000,\n *   host: '0.0.0.0',\n *   gracefulTerminationTimeout: 15000,\n * })\n * ```\n *\n * @example Using with await using (auto-cleanup)\n * ```typescript\n * async function main() {\n *   await using server = await serve(router, { port: 3000 })\n *\n *   // Server is running...\n *   console.log('Server listening on port 3000')\n *\n *   // Wait for termination signal\n *   await Promise.race([\n *     once(process, 'SIGINT'),\n *     once(process, 'SIGTERM'),\n *   ])\n *\n *   // Server will be automatically terminated when scope exits\n * }\n * ```\n */\nexport async function serve<\n  Request extends typeof IncomingMessage = typeof IncomingMessage,\n  Response extends typeof ServerResponse<\n    InstanceType<Request>\n  > = typeof ServerResponse,\n>(\n  handler: FetchHandler | HandlerObject,\n  options?: StartServerOptions<Request, Response>,\n): Promise<Server<Request, Response>> {\n  const server = createServer(handler, options)\n  server.listen(options)\n  await once(server, 'listening')\n  return server\n}\n"
  },
  {
    "path": "packages/lex/lex-server/src/service-auth.ts",
    "content": "import * as crypto from '@atproto/crypto'\nimport {\n  AtprotoDid,\n  AtprotoDidDocument,\n  Did,\n  matchesIdentifier,\n} from '@atproto/did'\nimport { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport {\n  CreateDidResolverOptions,\n  createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport { LexServerAuthError } from './errors.js'\nimport type { LexRouterAuth } from './lex-router.js'\n\nconst BEARER_PREFIX = 'Bearer '\n\n/**\n * Callback function to check and record nonce uniqueness.\n *\n * Used to prevent replay attacks by ensuring each nonce is only used once.\n * The implementation must track nonces for at least the `maxAge` duration\n * (default 5 minutes before and after the current time).\n *\n * @param nonce - The nonce string from the JWT token\n * @returns Promise resolving to `true` if the nonce is unique (first time seen),\n *          `false` if it has been seen before\n *\n * @example\n * ```typescript\n * // Using Redis for nonce tracking\n * const checkNonce: UniqueNonceChecker = async (nonce) => {\n *   const key = `nonce:${nonce}`\n *   const result = await redis.setnx(key, '1')\n *   if (result === 1) {\n *     await redis.expire(key, 600) // 10 minutes TTL\n *     return true\n *   }\n *   return false\n * }\n * ```\n */\nexport type UniqueNonceChecker = (nonce: string) => Promise<boolean>\n\n/**\n * Configuration options for AT Protocol service authentication.\n *\n * Service auth is used for server-to-server communication in the AT Protocol,\n * where one service authenticates to another using signed JWT tokens tied to\n * the caller's DID.\n *\n * @example\n * ```typescript\n * const options: ServiceAuthOptions = {\n *   audience: 'did:web:api.example.com',\n *   unique: async (nonce) => nonceStore.checkAndAdd(nonce),\n *   maxAge: 300, // 5 minutes\n *   // Optional DID resolver options\n *   plcDirectoryUrl: 'https://plc.directory'\n * }\n * ```\n */\nexport type ServiceAuthOptions = CreateDidResolverOptions & {\n  /**\n   * Expected audience (\"aud\") claim in the JWT token.\n   *\n   * This should be the DID of your service. The token must include this\n   * value in its `aud` claim to be accepted. Set to `null` to skip\n   * audience verification (not recommended for production).\n   */\n  audience: null | DidString\n  /**\n   * Function to check and record nonce uniqueness.\n   *\n   * This is critical for preventing replay attacks. The value checked here\n   * must be unique within `maxAge` seconds before and after the current time.\n   *\n   * @param nonce - The nonce to check\n   * @returns Promise resolving to `true` if unique, `false` if seen before\n   */\n  unique: UniqueNonceChecker\n  /**\n   * Maximum age of the JWT token in seconds.\n   *\n   * Tokens with `iat` (issued at) or `exp` (expiry) timestamps outside\n   * this window from the current time will be rejected.\n   *\n   * @default 300 (5 minutes)\n   */\n  maxAge?: number\n}\n\n/**\n * Credentials returned after successful service authentication.\n *\n * Contains the verified DID, resolved DID document, and parsed JWT token.\n * These are available in handler context as `ctx.credentials`.\n *\n * @example\n * ```typescript\n * router.add(protectedMethod, {\n *   handler: async (ctx) => {\n *     const { did, didDocument, jwt } = ctx.credentials\n *     console.log('Request from:', did)\n *     console.log('Token expires:', new Date(jwt.payload.exp * 1000))\n *     return { body: { callerDid: did } }\n *   },\n *   auth: serviceAuth({ audience: myDid, unique: checkNonce })\n * })\n * ```\n */\nexport type ServiceAuthCredentials = {\n  /** The verified AT Protocol DID of the caller. */\n  did: AtprotoDid\n  /** The resolved DID document of the caller. */\n  didDocument: AtprotoDidDocument\n  /** The parsed and validated JWT token. */\n  jwt: ParsedJwt\n}\n\n/**\n * Creates an authentication handler for verifying AT Protocol service auth JWTs.\n *\n * Service auth is the standard authentication mechanism for server-to-server\n * communication in the AT Protocol. It uses JWT bearer tokens signed by the\n * caller's DID signing key, with the signature verified against the public\n * key in the caller's DID document.\n *\n * The handler performs the following validations:\n * - Extracts and parses the Bearer token from the Authorization header\n * - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)\n * - Resolves the issuer's DID document\n * - Verifies the JWT signature against the `#atproto` verification method\n * - Checks nonce uniqueness to prevent replay attacks\n *\n * @param options - Configuration options for service auth\n * @returns An auth handler function for use with {@link LexRouter.add}\n *\n * @example Basic usage\n * ```typescript\n * import { LexRouter, serviceAuth } from '@atproto/lex-server'\n *\n * const router = new LexRouter()\n *\n * const auth = serviceAuth({\n *   audience: 'did:web:api.example.com',\n *   unique: async (nonce) => {\n *     // Check if nonce has been seen, return true if unique\n *     const isNew = await redis.setnx(`nonce:${nonce}`, '1')\n *     if (isNew) await redis.expire(`nonce:${nonce}`, 600)\n *     return isNew\n *   }\n * })\n *\n * router.add(myMethod, {\n *   handler: async (ctx) => {\n *     console.log('Authenticated as:', ctx.credentials.did)\n *     return { body: { success: true } }\n *   },\n *   auth\n * })\n * ```\n */\nexport function serviceAuth({\n  audience,\n  maxAge = 5 * 60,\n  unique,\n  ...options\n}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {\n  const didResolver = createDidResolver(options)\n\n  return async ({ request, method }) => {\n    const { signal } = request\n    const jwt = await parseJwtBearer(request, {\n      lxm: method.nsid,\n      maxAge,\n      audience,\n      unique,\n    })\n\n    let didDocument: AtprotoDidDocument = await didResolver\n      .resolve(jwt.payload.iss, { signal })\n      .catch((cause) => {\n        throw new LexServerAuthError(\n          'AuthenticationRequired',\n          'Could not resolve DID document',\n          { Bearer: { error: 'DidResolutionFailed' } },\n          { cause },\n        )\n      })\n\n    const key = getAtprotoSigningKey(didDocument)\n\n    if (!key || !(await verifyJwt(jwt, key))) {\n      signal.throwIfAborted()\n\n      // Try refreshing the DID document in case it was updated\n      didDocument = await didResolver\n        .resolve(jwt.payload.iss, { signal, noCache: true })\n        .catch((cause) => {\n          throw new LexServerAuthError(\n            'AuthenticationRequired',\n            'Could not resolve DID document',\n            { Bearer: { error: 'DidResolutionFailed' } },\n            { cause },\n          )\n        })\n\n      // Verify again with the fresh key (if it changed)\n      const keyFresh = getAtprotoSigningKey(didDocument)\n      if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {\n        throw new LexServerAuthError(\n          'AuthenticationRequired',\n          'Invalid JWT signature',\n          { Bearer: { error: 'BadJwtSignature' } },\n        )\n      }\n    }\n\n    return {\n      did: didDocument.id,\n      didDocument,\n      jwt,\n    }\n  }\n}\n\nasync function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {\n  try {\n    return await crypto.verifySignature(key, jwt.message, jwt.signature, {\n      jwtAlg: jwt.header.alg,\n      allowMalleableSig: true,\n    })\n  } catch (cause) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Could not verify JWT signature',\n      { Bearer: { error: 'BadJwtSignature' } },\n      { cause },\n    )\n  }\n}\n\nfunction getAtprotoSigningKey(\n  didDocument: AtprotoDidDocument,\n): null | Did<'key'> {\n  try {\n    const key = didDocument.verificationMethod?.find(\n      isAtprotoVerificationMethod,\n      didDocument,\n    )\n\n    if (key?.publicKeyMultibase) {\n      if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n        const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n        return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n      } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n        const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n        return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n      } else if (key.type === 'Multikey') {\n        const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n        return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n      }\n    }\n  } catch {\n    // Invalid key, ignore\n  }\n\n  return null\n}\n\nfunction isAtprotoVerificationMethod<\n  V extends string | { id: string; type: string; publicKeyMultibase?: string },\n>(\n  this: AtprotoDidDocument,\n  vm: V,\n): vm is Exclude<V, string> & {\n  id: `${string}#atproto`\n} {\n  return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)\n}\n\nasync function parseJwtBearer(\n  request: Request,\n  options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n  const authorization = request.headers.get('authorization')\n  if (!authorization?.startsWith(BEARER_PREFIX)) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Bearer token required',\n      { Bearer: { error: 'MissingBearer' } },\n    )\n  }\n\n  const token = authorization.slice(BEARER_PREFIX.length).trim()\n\n  return parseJwt(token, options)\n}\n\n/**\n * Options for parsing and validating a JWT token.\n */\nexport type ParseJwtOptions = {\n  /** Maximum age in seconds for token validity window. */\n  maxAge: number\n  /** Expected audience claim, or null to skip audience verification. */\n  audience: null | DidString\n  /** Function to check nonce uniqueness. */\n  unique: UniqueNonceChecker\n  /** Expected lexicon method NSID for the `lxm` claim. */\n  lxm: string\n}\n\n/**\n * A parsed and partially validated JWT token.\n *\n * Contains the decoded header and payload, along with the raw bytes\n * needed for signature verification.\n *\n * @example\n * ```typescript\n * const jwt: ParsedJwt = {\n *   header: { alg: 'ES256K', typ: 'JWT' },\n *   payload: {\n *     iss: 'did:plc:abc123',\n *     aud: 'did:web:api.example.com',\n *     exp: 1704067200,\n *     iat: 1704066900,\n *     lxm: 'com.atproto.sync.getBlob'\n *   },\n *   message: new Uint8Array([...]),\n *   signature: new Uint8Array([...])\n * }\n * ```\n */\nexport type ParsedJwt = {\n  /** The decoded JWT header containing algorithm and type. */\n  header: HeaderObject\n  /** The decoded JWT payload containing claims. */\n  payload: PayloadObject\n  /** The raw header.payload bytes for signature verification. */\n  message: Uint8Array\n  /** The decoded signature bytes. */\n  signature: Uint8Array\n}\n\nasync function parseJwt(\n  token: string,\n  options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n  const {\n    length,\n    0: headerB64,\n    1: payloadB64,\n    2: signatureB64,\n  } = token.split('.')\n  if (length !== 3) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Invalid JWT token',\n      { Bearer: { error: 'BadJwt' } },\n    )\n  }\n\n  let header: HeaderObject\n  try {\n    header = jsonFromBase64(headerB64, isHeaderObject)\n  } catch (cause) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Invalid JWT token',\n      { Bearer: { error: 'BadJwt' } },\n      { cause },\n    )\n  }\n\n  if (\n    header.alg === 'none' ||\n    // service tokens are not OAuth 2.0 access tokens\n    // https://datatracker.ietf.org/doc/html/rfc9068\n    header.typ === 'at+jwt' ||\n    // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n    header.typ === 'refresh+jwt' ||\n    // \"DPoP\" proofs are not meant to be used as service tokens\n    // https://datatracker.ietf.org/doc/html/rfc9449\n    header.typ === 'dpop+jwt'\n  ) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Invalid JWT token',\n      { Bearer: { error: 'BadJwt' } },\n    )\n  }\n\n  let payload: PayloadObject\n  try {\n    payload = jsonFromBase64(payloadB64, isPayloadObject)\n  } catch (cause) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Invalid JWT token',\n      { Bearer: { error: 'BadJwt' } },\n      { cause },\n    )\n  }\n\n  if (options.audience !== null && options.audience !== payload.aud) {\n    throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {\n      Bearer: { error: 'InvalidAudience' },\n    })\n  }\n\n  const now = Math.floor(Date.now() / 1000)\n\n  if (payload.nbf != null && now < payload.nbf) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'JWT token not yet valid',\n      { Bearer: { error: 'JwtNotYetValid' } },\n    )\n  }\n\n  if (now > payload.exp) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'JWT token expired',\n      { Bearer: { error: 'JwtExpired' } },\n    )\n  }\n\n  // Prevent issuer from generating very long-lived tokens\n  if (\n    timeDiff(now, payload.exp) > options.maxAge ||\n    timeDiff(now, payload.iat) > options.maxAge\n  ) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'JWT token too old',\n      { Bearer: { error: 'JwtTooOld' } },\n    )\n  }\n\n  if (payload.lxm != null && typeof payload.lxm !== options.lxm) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Invalid JWT lexicon method (\"lxm\")',\n      { Bearer: { error: 'BadJwtLexiconMethod' } },\n    )\n  }\n\n  if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {\n    throw new LexServerAuthError(\n      'AuthenticationRequired',\n      'Replay attack detected: nonce is not unique',\n      { Bearer: { error: 'NonceNotUnique' } },\n    )\n  }\n\n  return {\n    header,\n    payload,\n    message: textEncoder.encode(`${headerB64}.${payloadB64}`),\n    signature: fromBase64(signatureB64, 'base64url'),\n  }\n}\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\n\ntype HeaderObject = { alg: string; typ?: string }\nfunction isHeaderObject(obj: unknown): obj is HeaderObject {\n  return (\n    isPlainObject(obj) &&\n    typeof obj.alg === 'string' &&\n    (obj.typ === undefined || typeof obj.typ === 'string')\n  )\n}\n\ntype PayloadObject = {\n  iss: DidString\n  aud: DidString\n  exp: number\n  iat?: number\n  nbf?: number\n  lxm?: string\n  nonce?: string\n}\nexport function isPayloadObject(obj: unknown): obj is PayloadObject {\n  return (\n    isPlainObject(obj) &&\n    typeof obj.iss === 'string' &&\n    typeof obj.aud === 'string' &&\n    (obj.lxm === undefined || typeof obj.lxm === 'string') &&\n    (obj.nonce === undefined || typeof obj.nonce === 'string') &&\n    (obj.iat === undefined || isPositiveInt(obj.iat)) &&\n    (obj.nbf === undefined || isPositiveInt(obj.nbf)) &&\n    isPositiveInt(obj.exp) &&\n    isDidString(obj.iss) &&\n    isDidString(obj.aud)\n  )\n}\n\nfunction timeDiff(t1: number, t2?: number): number {\n  if (t2 === undefined) return 0\n  return Math.abs(t1 - t2)\n}\n\nfunction isPositiveInt(value: unknown): value is number {\n  return typeof value === 'number' && Number.isInteger(value) && value > 0\n}\n\nfunction jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {\n  const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))\n  if (isType(obj)) return obj\n  throw new Error('Invalid type')\n}\n"
  },
  {
    "path": "packages/lex/lex-server/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/tsconfig.examples.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"include\": [\n    \"./examples/**/*.ts\",\n    \"./examples/**/*.js\",\n    \"./examples/**/*.mjs\",\n    \"./examples/**/*.cjs\"\n  ],\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"noEmit\": true,\n    \"target\": \"ES2023\",\n    \"module\": \"nodenext\",\n    \"moduleResolution\": \"nodenext\",\n    \"rewriteRelativeImportExtensions\": true,\n    \"erasableSyntaxOnly\": true,\n    \"verbatimModuleSyntax\": true,\n    \"rootDir\": \".\",\n    \"baseUrl\": \".\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.examples.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lex/lex-server/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/lex/lex-server/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/lex-cli/CHANGELOG.md",
    "content": "# @atproto/lex-cli\n\n## 0.9.9\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lexicon@0.6.2\n\n## 0.9.8\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lexicon@0.6.0\n\n## 0.9.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.5.2\n\n## 0.9.6\n\n### Patch Changes\n\n- [#4294](https://github.com/bluesky-social/atproto/pull/4294) [`ac1d29ec0`](https://github.com/bluesky-social/atproto/commit/ac1d29ec0f105b4579491f563e2abd73f36c1df3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export record types using the `Main` identifier\n\n## 0.9.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.5.1\n\n## 0.9.4\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n\n## 0.9.3\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n\n## 0.9.2\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/lexicon@0.4.13\n\n## 0.9.1\n\n### Patch Changes\n\n- [`8d56743a1`](https://github.com/bluesky-social/atproto/commit/8d56743a18413a73e6bbca358336c770553d861f) Thanks [@devinivy](https://github.com/devinivy)! - Fix codegen for nsids containing dashes.\n\n## 0.9.0\n\n### Minor Changes\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `QueryParams` is now always generated as a `type` instead of an `interface`\n\n### Patch Changes\n\n- Updated dependencies [[`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/lexicon@0.4.12\n\n## 0.8.3\n\n### Patch Changes\n\n- [#3995](https://github.com/bluesky-social/atproto/pull/3995) [`e3e31b2b9`](https://github.com/bluesky-social/atproto/commit/e3e31b2b9bf8c4de6b2d7fa992c3b3795686ea72) Thanks [@mozzius](https://github.com/mozzius)! - Add put method to record utility classes\n\n## 0.8.2\n\n### Patch Changes\n\n- [#3906](https://github.com/bluesky-social/atproto/pull/3906) [`d880665e6`](https://github.com/bluesky-social/atproto/commit/d880665e63100e9e06b0ef2d287e5c3e0272e7ca) Thanks [@mozzius](https://github.com/mozzius)! - Fix type generation for arrays of strings with known values\n\n## 0.8.1\n\n### Patch Changes\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/lexicon@0.4.11\n\n## 0.8.0\n\n### Minor Changes\n\n- [#3715](https://github.com/bluesky-social/atproto/pull/3715) [`23462184d`](https://github.com/bluesky-social/atproto/commit/23462184dc941ba2fc3b4d054985a53715585020) Thanks [@knotbin](https://github.com/knotbin)! - Fix conflicts between lexicon in gen-api, record types in index file now named ComExampleNameRecord instead of NameRecord\n\n## 0.7.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.10\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n  - @atproto/lexicon@0.4.9\n\n## 0.7.0\n\n### Minor Changes\n\n- [#3282](https://github.com/bluesky-social/atproto/pull/3282) [`c501715b0`](https://github.com/bluesky-social/atproto/commit/c501715b0dbe6daad962e1df80986521d00f65aa) Thanks [@tcyrus](https://github.com/tcyrus)! - Fix type-only imports in codegen\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/syntax@0.3.4\n  - @atproto/lexicon@0.4.8\n\n## 0.6.1\n\n### Patch Changes\n\n- [#3534](https://github.com/bluesky-social/atproto/pull/3534) [`175f89f8f`](https://github.com/bluesky-social/atproto/commit/175f89f8fe0e570518e32b48e49f5bbf63395b67) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve compatibility with Windows runtimes.\n\n## 0.6.0\n\n### Minor Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update the code generation to better reflect the data typings. In particular this change will cause generated code to explicit the `$type` property that can be present in the data.\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `[string]: unknown` index signature from custom user objects, input and output schemas.\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `.js` file extension to `import` statements in generated code.\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Type the generated `ids` object (that contains all the lexicon namespace ids) as `const`.\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensures that empty `schemas` arrays are typed as `LexiconDoc[]`.\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n\n## 0.5.7\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/lexicon@0.4.6\n  - @atproto/syntax@0.3.2\n\n## 0.5.6\n\n### Patch Changes\n\n- [#3420](https://github.com/bluesky-social/atproto/pull/3420) [`6241f6b00`](https://github.com/bluesky-social/atproto/commit/6241f6b00b8728cd5f5e879591b0ca98308edab0) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Expose resetRouteRateLimits to the handler context\n\n## 0.5.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.5\n\n## 0.5.4\n\n### Patch Changes\n\n- [#3274](https://github.com/bluesky-social/atproto/pull/3274) [`672243a9e`](https://github.com/bluesky-social/atproto/commit/672243a9ea0a2cbd0cfaa4e255c58de658ed22e2) Thanks [@dholms](https://github.com/dholms)! - Allow ratelimit calcKey to return null\n\n## 0.5.3\n\n### Patch Changes\n\n- Updated dependencies [[`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95)]:\n  - @atproto/lexicon@0.4.4\n\n## 0.5.2\n\n### Patch Changes\n\n- [#2911](https://github.com/bluesky-social/atproto/pull/2911) [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Retain type of `schemas` using definition type instead of obscuring into a `LexiconDoc[]`\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/syntax@0.3.1\n  - @atproto/lexicon@0.4.3\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies [[`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94)]:\n  - @atproto/lexicon@0.4.2\n\n## 0.5.0\n\n### Minor Changes\n\n- [#2707](https://github.com/bluesky-social/atproto/pull/2707) [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The generated client implementation uses the new `XrpcClient` class from `@atproto/xrpc`, instead of the deprecated `Client` and `ServiceClient` class.\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e)]:\n  - @atproto/lexicon@0.4.1\n\n## 0.4.1\n\n### Patch Changes\n\n- [#2691](https://github.com/bluesky-social/atproto/pull/2691) [`08ef309c9`](https://github.com/bluesky-social/atproto/commit/08ef309c9c1f35f0e7093cb845321e876133d23e) Thanks [@dholms](https://github.com/dholms)! - Fix use of prettier.format for codegen\n\n## 0.4.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/lexicon@0.4.0\n  - @atproto/syntax@0.3.0\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.3.3\n  - @atproto/syntax@0.2.1\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/lexicon@0.3.2\n\n## 0.3.0\n\n### Minor Changes\n\n- [#2039](https://github.com/bluesky-social/atproto/pull/2039) [`bf8d718c`](https://github.com/bluesky-social/atproto/commit/bf8d718cf918ac8d8a2cb1f57fde80535284642d) Thanks [@dholms](https://github.com/dholms)! - Namespace lexicon codegen\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60)]:\n  - @atproto/syntax@0.1.5\n  - @atproto/lexicon@0.3.1\n\n## 0.2.4\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/syntax@0.1.4\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.2.3\n  - @atproto/syntax@0.1.3\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.2.2\n  - @atproto/syntax@0.1.2\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n  - @atproto/lexicon@0.2.1\n"
  },
  {
    "path": "packages/lex-cli/README.md",
    "content": "# Lexicon CLI Tool\n\nCommand-line tool to generate Lexicon schemas and APIs.\n\n## Usage\n\n```\nUsage: lex [options] [command]\n\nLexicon CLI\n\nOptions:\n  -V, --version                     output the version number\n  -h, --help                        display help for command\n\nCommands:\n  new [options] <nsid> [outfile]    Create a new schema json file\n  gen-md <schemas...>               Generate markdown documentation\n  gen-ts-obj <schemas...>           Generate a TS file that exports an array of schemas\n  gen-api <outdir> <schemas...>     Generate a TS client API\n  gen-server <outdir> <schemas...>  Generate a TS server API\n  help [command]                    display help for command\n```\n\n**Example 1:** Generate an api\n\n```\n$ lex gen-api ./api/src ./schemas/com/service/*.json ./schemas/com/another/*.json\n```\n\n**Example 2:** Generate a server\n\n```\n$ lex gen-server ./server/src/xrpc ./schemas/com/service/*.json ./schemas/com/another/*.json\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/lex-cli/package.json",
    "content": "{\n  \"name\": \"@atproto/lex-cli\",\n  \"version\": \"0.9.9\",\n  \"license\": \"MIT\",\n  \"description\": \"TypeScript codegen tool for atproto Lexicon schemas\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lex-cli\"\n  },\n  \"bin\": {\n    \"lex\": \"dist/index.js\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"chalk\": \"^4.1.2\",\n    \"commander\": \"^9.4.0\",\n    \"prettier\": \"^3.2.5\",\n    \"ts-morph\": \"^24.0.0\",\n    \"yesno\": \"^0.4.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/lex-cli/src/codegen/client.ts",
    "content": "import {\n  IndentationText,\n  Project,\n  SourceFile,\n  VariableDeclarationKind,\n} from 'ts-morph'\nimport { type LexRecord, type LexiconDoc, Lexicons } from '@atproto/lexicon'\nimport { NSID } from '@atproto/syntax'\nimport { type GeneratedAPI } from '../types'\nimport { gen, lexiconsTs, utilTs } from './common'\nimport {\n  genCommonImports,\n  genImports,\n  genRecord,\n  genUserType,\n  genXrpcInput,\n  genXrpcOutput,\n  genXrpcParams,\n} from './lex-gen'\nimport {\n  type DefTreeNode,\n  lexiconsToDefTree,\n  schemasToNsidTokens,\n  toCamelCase,\n  toScreamingSnakeCase,\n  toTitleCase,\n} from './util'\n\nconst ATP_METHODS = {\n  list: 'com.atproto.repo.listRecords',\n  get: 'com.atproto.repo.getRecord',\n  create: 'com.atproto.repo.createRecord',\n  put: 'com.atproto.repo.putRecord',\n  delete: 'com.atproto.repo.deleteRecord',\n}\n\nexport async function genClientApi(\n  lexiconDocs: LexiconDoc[],\n): Promise<GeneratedAPI> {\n  const project = new Project({\n    useInMemoryFileSystem: true,\n    manipulationSettings: { indentationText: IndentationText.TwoSpaces },\n  })\n  const api: GeneratedAPI = { files: [] }\n  const lexicons = new Lexicons(lexiconDocs)\n  const nsidTree = lexiconsToDefTree(lexiconDocs)\n  const nsidTokens = schemasToNsidTokens(lexiconDocs)\n  for (const lexiconDoc of lexiconDocs) {\n    api.files.push(await lexiconTs(project, lexicons, lexiconDoc))\n  }\n  api.files.push(await utilTs(project))\n  api.files.push(await lexiconsTs(project, lexiconDocs))\n  api.files.push(await indexTs(project, lexiconDocs, nsidTree, nsidTokens))\n  return api\n}\n\nconst indexTs = (\n  project: Project,\n  lexiconDocs: LexiconDoc[],\n  nsidTree: DefTreeNode[],\n  nsidTokens: Record<string, string[]>,\n) =>\n  gen(project, '/index.ts', async (file) => {\n    //= import { XrpcClient, type FetchHandler, type FetchHandlerOptions } from '@atproto/xrpc'\n    const xrpcImport = file.addImportDeclaration({\n      moduleSpecifier: '@atproto/xrpc',\n    })\n    xrpcImport.addNamedImports([\n      { name: 'XrpcClient' },\n      { name: 'FetchHandler', isTypeOnly: true },\n      { name: 'FetchHandlerOptions', isTypeOnly: true },\n    ])\n    //= import {schemas} from './lexicons.js'\n    file\n      .addImportDeclaration({ moduleSpecifier: './lexicons.js' })\n      .addNamedImports([{ name: 'schemas' }])\n    //= import {CID} from 'multiformats/cid'\n    file\n      .addImportDeclaration({\n        moduleSpecifier: 'multiformats/cid',\n      })\n      .addNamedImports([{ name: 'CID' }])\n\n    //= import { type OmitKey, type Un$Typed } from './util.js'\n    file\n      .addImportDeclaration({ moduleSpecifier: `./util.js` })\n      .addNamedImports([\n        { name: 'OmitKey', isTypeOnly: true },\n        { name: 'Un$Typed', isTypeOnly: true },\n      ])\n\n    // generate type imports and re-exports\n    for (const lexicon of lexiconDocs) {\n      const moduleSpecifier = `./types/${lexicon.id.split('.').join('/')}.js`\n      file\n        .addImportDeclaration({ moduleSpecifier })\n        .setNamespaceImport(toTitleCase(lexicon.id))\n      file\n        .addExportDeclaration({ moduleSpecifier })\n        .setNamespaceExport(toTitleCase(lexicon.id))\n    }\n\n    // generate token enums\n    for (const nsidAuthority in nsidTokens) {\n      // export const {THE_AUTHORITY} = {\n      //  {Name}: \"{authority.the.name}\"\n      // }\n      file.addVariableStatement({\n        isExported: true,\n        declarationKind: VariableDeclarationKind.Const,\n        declarations: [\n          {\n            name: toScreamingSnakeCase(nsidAuthority),\n            initializer: [\n              '{',\n              ...nsidTokens[nsidAuthority].map(\n                (nsidName) =>\n                  `${toTitleCase(nsidName)}: \"${nsidAuthority}.${nsidName}\",`,\n              ),\n              '}',\n            ].join('\\n'),\n          },\n        ],\n      })\n    }\n\n    //= export class AtpBaseClient {...}\n    const clientCls = file.addClass({\n      name: 'AtpBaseClient',\n      isExported: true,\n      extends: 'XrpcClient',\n    })\n\n    for (const ns of nsidTree) {\n      //= ns: NS\n      clientCls.addProperty({\n        name: ns.propName,\n        type: ns.className,\n      })\n    }\n\n    //= constructor (options: FetchHandler | FetchHandlerOptions) {\n    //=   super(options, schemas)\n    //=   {namespace declarations}\n    //= }\n    clientCls.addConstructor({\n      parameters: [\n        { name: 'options', type: 'FetchHandler | FetchHandlerOptions' },\n      ],\n      statements: [\n        'super(options, schemas)',\n        ...nsidTree.map(\n          (ns) => `this.${ns.propName} = new ${ns.className}(this)`,\n        ),\n      ],\n    })\n\n    //= /** @deprecated use `this` instead */\n    //= get xrpc(): XrpcClient {\n    //=   return this\n    //= }\n    clientCls\n      .addGetAccessor({\n        name: 'xrpc',\n        returnType: 'XrpcClient',\n        statements: ['return this'],\n      })\n      .addJsDoc('@deprecated use `this` instead')\n\n    // generate classes for the schemas\n    for (const ns of nsidTree) {\n      genNamespaceCls(file, ns)\n    }\n  })\n\nfunction genNamespaceCls(file: SourceFile, ns: DefTreeNode) {\n  //= export class {ns}NS {...}\n  const cls = file.addClass({\n    name: ns.className,\n    isExported: true,\n  })\n  //= _client: XrpcClient\n  cls.addProperty({\n    name: '_client',\n    type: 'XrpcClient',\n  })\n\n  for (const userType of ns.userTypes) {\n    if (userType.def.type !== 'record') {\n      continue\n    }\n    //= type: TypeRecord\n    const name = NSID.parse(userType.nsid).name || ''\n    cls.addProperty({\n      name: toCamelCase(name),\n      type: `${toTitleCase(userType.nsid)}Record`,\n    })\n  }\n\n  for (const child of ns.children) {\n    //= child: ChildNS\n    cls.addProperty({\n      name: child.propName,\n      type: child.className,\n    })\n\n    // recurse\n    genNamespaceCls(file, child)\n  }\n\n  //= constructor(public client: XrpcClient) {\n  //=  this._client = client\n  //=  {child namespace prop declarations}\n  //=  {record prop declarations}\n  //= }\n  cls.addConstructor({\n    parameters: [\n      {\n        name: 'client',\n        type: 'XrpcClient',\n      },\n    ],\n    statements: [\n      `this._client = client`,\n      ...ns.children.map(\n        (ns) => `this.${ns.propName} = new ${ns.className}(client)`,\n      ),\n      ...ns.userTypes\n        .filter((ut) => ut.def.type === 'record')\n        .map((ut) => {\n          const name = NSID.parse(ut.nsid).name || ''\n          return `this.${toCamelCase(name)} = new ${toTitleCase(\n            ut.nsid,\n          )}Record(client)`\n        }),\n    ],\n  })\n\n  // methods\n  for (const userType of ns.userTypes) {\n    if (userType.def.type !== 'query' && userType.def.type !== 'procedure') {\n      continue\n    }\n    const isGetReq = userType.def.type === 'query'\n    const moduleName = toTitleCase(userType.nsid)\n    const name = toCamelCase(NSID.parse(userType.nsid).name || '')\n    const method = cls.addMethod({\n      name,\n      returnType: `Promise<${moduleName}.Response>`,\n    })\n    if (isGetReq) {\n      method.addParameter({\n        name: 'params?',\n        type: `${moduleName}.QueryParams`,\n      })\n    } else if (userType.def.type === 'procedure') {\n      method.addParameter({\n        name: 'data?',\n        type: `${moduleName}.InputSchema`,\n      })\n    }\n    method.addParameter({\n      name: 'opts?',\n      type: `${moduleName}.CallOptions`,\n    })\n    method.setBodyText(\n      [\n        `return this._client`,\n        isGetReq\n          ? `.call('${userType.nsid}', params, undefined, opts)`\n          : `.call('${userType.nsid}', opts?.qp, data, opts)`,\n        userType.def.errors?.length\n          ? // Only add a catch block if there are custom errors\n            `  .catch((e) => { throw ${moduleName}.toKnownErr(e) })`\n          : '',\n      ].join('\\n'),\n    )\n  }\n\n  // record api classes\n  for (const userType of ns.userTypes) {\n    if (userType.def.type !== 'record') {\n      continue\n    }\n    genRecordCls(file, userType.nsid, userType.def)\n  }\n}\n\nfunction genRecordCls(file: SourceFile, nsid: string, lexRecord: LexRecord) {\n  //= export class {type}Record {...}\n  const cls = file.addClass({\n    name: `${toTitleCase(nsid)}Record`,\n    isExported: true,\n  })\n  //= _client: XrpcClient\n  cls.addProperty({\n    name: '_client',\n    type: 'XrpcClient',\n  })\n\n  //= constructor(client: XrpcClient) {\n  //=  this._client = client\n  //= }\n  const cons = cls.addConstructor()\n  cons.addParameter({\n    name: 'client',\n    type: 'XrpcClient',\n  })\n  cons.setBodyText(`this._client = client`)\n\n  // methods\n  const typeModule = toTitleCase(nsid)\n  {\n    //= list()\n    const method = cls.addMethod({\n      isAsync: true,\n      name: 'list',\n      returnType: `Promise<{cursor?: string, records: ({uri: string, value: ${typeModule}.Record})[]}>`,\n    })\n    method.addParameter({\n      name: 'params',\n      type: `OmitKey<${toTitleCase(ATP_METHODS.list)}.QueryParams, \"collection\">`,\n    })\n    method.setBodyText(\n      [\n        `const res = await this._client.call('${ATP_METHODS.list}', { collection: '${nsid}', ...params })`,\n        `return res.data`,\n      ].join('\\n'),\n    )\n  }\n  {\n    //= get()\n    const method = cls.addMethod({\n      isAsync: true,\n      name: 'get',\n      returnType: `Promise<{uri: string, cid: string, value: ${typeModule}.Record}>`,\n    })\n    method.addParameter({\n      name: 'params',\n      type: `OmitKey<${toTitleCase(ATP_METHODS.get)}.QueryParams, \"collection\">`,\n    })\n    method.setBodyText(\n      [\n        `const res = await this._client.call('${ATP_METHODS.get}', { collection: '${nsid}', ...params })`,\n        `return res.data`,\n      ].join('\\n'),\n    )\n  }\n  {\n    //= create()\n    const method = cls.addMethod({\n      isAsync: true,\n      name: 'create',\n      returnType: 'Promise<{uri: string, cid: string}>',\n    })\n    method.addParameter({\n      name: 'params',\n      type: `OmitKey<${toTitleCase(\n        ATP_METHODS.create,\n      )}.InputSchema, \"collection\" | \"record\">`,\n    })\n    method.addParameter({\n      name: 'record',\n      type: `Un$Typed<${typeModule}.Record>`,\n    })\n    method.addParameter({\n      name: 'headers?',\n      type: `Record<string, string>`,\n    })\n    const maybeRkeyPart = lexRecord.key?.startsWith('literal:')\n      ? `rkey: '${lexRecord.key.replace('literal:', '')}', `\n      : ''\n    method.setBodyText(\n      [\n        `const collection = '${nsid}'`,\n        `const res = await this._client.call('${ATP_METHODS.create}', undefined, { collection, ${maybeRkeyPart}...params, record: { ...record, $type: collection } }, { encoding: 'application/json', headers })`,\n        `return res.data`,\n      ].join('\\n'),\n    )\n  }\n  {\n    //= put()\n    const method = cls.addMethod({\n      isAsync: true,\n      name: 'put',\n      returnType: 'Promise<{uri: string, cid: string}>',\n    })\n    method.addParameter({\n      name: 'params',\n      type: `OmitKey<${toTitleCase(ATP_METHODS.put)}.InputSchema, \"collection\" | \"record\">`,\n    })\n    method.addParameter({\n      name: 'record',\n      type: `Un$Typed<${typeModule}.Record>`,\n    })\n    method.addParameter({\n      name: 'headers?',\n      type: `Record<string, string>`,\n    })\n    method.setBodyText(\n      [\n        `const collection = '${nsid}'`,\n        `const res = await this._client.call('${ATP_METHODS.put}', undefined, { collection, ...params, record: { ...record, $type: collection } }, { encoding: 'application/json', headers })`,\n        `return res.data`,\n      ].join('\\n'),\n    )\n  }\n  {\n    //= delete()\n    const method = cls.addMethod({\n      isAsync: true,\n      name: 'delete',\n      returnType: 'Promise<void>',\n    })\n    method.addParameter({\n      name: 'params',\n      type: `OmitKey<${toTitleCase(\n        ATP_METHODS.delete,\n      )}.InputSchema, \"collection\">`,\n    })\n    method.addParameter({\n      name: 'headers?',\n      type: `Record<string, string>`,\n    })\n\n    method.setBodyText(\n      [\n        `await this._client.call('${ATP_METHODS.delete}', undefined, { collection: '${nsid}', ...params }, { headers })`,\n      ].join('\\n'),\n    )\n  }\n}\n\nconst lexiconTs = (project, lexicons: Lexicons, lexiconDoc: LexiconDoc) =>\n  gen(\n    project,\n    `/types/${lexiconDoc.id.split('.').join('/')}.ts`,\n    async (file) => {\n      const main = lexiconDoc.defs.main\n      if (\n        main?.type === 'query' ||\n        main?.type === 'subscription' ||\n        main?.type === 'procedure'\n      ) {\n        //= import {HeadersMap, XRPCError} from '@atproto/xrpc'\n        const xrpcImport = file.addImportDeclaration({\n          moduleSpecifier: '@atproto/xrpc',\n        })\n        xrpcImport.addNamedImports([\n          { name: 'HeadersMap' },\n          { name: 'XRPCError' },\n        ])\n      }\n\n      genCommonImports(file, lexiconDoc.id)\n\n      const imports: Set<string> = new Set()\n      for (const defId in lexiconDoc.defs) {\n        const def = lexiconDoc.defs[defId]\n        const lexUri = `${lexiconDoc.id}#${defId}`\n        if (defId === 'main') {\n          if (def.type === 'query' || def.type === 'procedure') {\n            genXrpcParams(file, lexicons, lexUri, false)\n            genXrpcInput(file, imports, lexicons, lexUri, false)\n            genXrpcOutput(file, imports, lexicons, lexUri)\n            genClientXrpcCommon(file, lexicons, lexUri)\n          } else if (def.type === 'subscription') {\n            continue\n          } else if (def.type === 'record') {\n            genRecord(file, imports, lexicons, lexUri)\n          } else {\n            genUserType(file, imports, lexicons, lexUri)\n          }\n        } else {\n          genUserType(file, imports, lexicons, lexUri)\n        }\n      }\n      genImports(file, imports, lexiconDoc.id)\n    },\n  )\n\nfunction genClientXrpcCommon(\n  file: SourceFile,\n  lexicons: Lexicons,\n  lexUri: string,\n) {\n  const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure'])\n\n  //= export interface CallOptions {...}\n  const opts = file.addInterface({\n    name: 'CallOptions',\n    isExported: true,\n  })\n  opts.addProperty({ name: 'signal?', type: 'AbortSignal' })\n  opts.addProperty({ name: 'headers?', type: 'HeadersMap' })\n  if (def.type === 'procedure') {\n    opts.addProperty({ name: 'qp?', type: 'QueryParams' })\n  }\n  if (def.type === 'procedure' && def.input) {\n    let encodingType = 'string'\n    if (def.input.encoding !== '*/*') {\n      encodingType = def.input.encoding\n        .split(',')\n        .map((v) => `'${v.trim()}'`)\n        .join(' | ')\n    }\n    opts.addProperty({\n      name: 'encoding?',\n      type: encodingType,\n    })\n  }\n\n  // export interface Response {...}\n  const res = file.addInterface({\n    name: 'Response',\n    isExported: true,\n  })\n  res.addProperty({ name: 'success', type: 'boolean' })\n  res.addProperty({ name: 'headers', type: 'HeadersMap' })\n  if (def.output?.schema) {\n    if (def.output.encoding?.includes(',')) {\n      res.addProperty({ name: 'data', type: 'OutputSchema | Uint8Array' })\n    } else {\n      res.addProperty({ name: 'data', type: 'OutputSchema' })\n    }\n  } else if (def.output?.encoding) {\n    res.addProperty({ name: 'data', type: 'Uint8Array' })\n  }\n\n  // export class {errcode}Error {...}\n  const customErrors: { name: string; cls: string }[] = []\n  for (const error of def.errors || []) {\n    let name = toTitleCase(error.name)\n    if (!name.endsWith('Error')) name += 'Error'\n    const errCls = file.addClass({\n      name,\n      extends: 'XRPCError',\n      isExported: true,\n    })\n    errCls.addConstructor({\n      parameters: [{ name: 'src', type: 'XRPCError' }],\n      statements: [\n        'super(src.status, src.error, src.message, src.headers, { cause: src })',\n      ],\n    })\n\n    customErrors.push({ name: error.name, cls: name })\n  }\n\n  // export function toKnownErr(err: any) {...}\n  file.addFunction({\n    name: 'toKnownErr',\n    isExported: true,\n    parameters: [{ name: 'e', type: 'any' }],\n    statements: customErrors.length\n      ? [\n          'if (e instanceof XRPCError) {',\n          ...customErrors.map(\n            (err) => `if (e.error === '${err.name}') return new ${err.cls}(e)`,\n          ),\n          '}',\n          'return e',\n        ]\n      : ['return e'],\n  })\n}\n"
  },
  {
    "path": "packages/lex-cli/src/codegen/common.ts",
    "content": "import { Options as PrettierOptions, format } from 'prettier'\nimport { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'\nimport { type LexiconDoc } from '@atproto/lexicon'\nimport { type GeneratedFile } from '../types'\nimport { toTitleCase } from './util'\n\nconst PRETTIER_OPTS: PrettierOptions = {\n  parser: 'typescript',\n  tabWidth: 2,\n  semi: false,\n  singleQuote: true,\n  trailingComma: 'all',\n}\n\nexport const utilTs = (project) =>\n  gen(project, '/util.ts', async (file) => {\n    file.replaceWithText(`\nimport { type ValidationResult } from '@atproto/lexicon'\n\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n\nexport type $Typed<V, T extends string = string> = V & { $type: T }\nexport type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>\n\nexport type $Type<Id extends string, Hash extends string> = Hash extends 'main'\n  ? Id\n  : \\`\\${Id}#\\${Hash}\\`\n\nfunction isObject<V>(v: V): v is V & object {\n  return v != null && typeof v === 'object'\n}\n\nfunction is$type<Id extends string, Hash extends string>(\n  $type: unknown,\n  id: Id,\n  hash: Hash,\n): $type is $Type<Id, Hash> {\n  return hash === 'main'\n    ? $type === id\n    : // $type === \\`\\${id}#\\${hash}\\`\n      typeof $type === 'string' &&\n        $type.length === id.length + 1 + hash.length &&\n        $type.charCodeAt(id.length) === 35 /* '#' */ &&\n        $type.startsWith(id) &&\n        $type.endsWith(hash)\n}\n${\n  /**\n   * The construct below allows to properly distinguish open unions. Consider\n   * the following example:\n   *\n   * ```ts\n   * type Foo = { $type?: $Type<'foo', 'main'>; foo: string }\n   * type Bar = { $type?: $Type<'bar', 'main'>; bar: string }\n   * type OpenFooBarUnion = $Typed<Foo> | $Typed<Bar> | { $type: string }\n   * ```\n   *\n   * In the context of lexicons, when there is a open union as shown above, the\n   * if `$type` if either `foo` or `bar`, then the object IS of type `Foo` or\n   * `Bar`.\n   *\n   * ```ts\n   * declare const obj1: OpenFooBarUnion\n   * if (is$typed(obj1, 'foo', 'main')) {\n   *   obj1.$type // $Type<'foo', 'main'>\n   *   obj1.foo // string\n   * }\n   * ```\n   *\n   * Similarly, if an object is of type `unknown`, then the `is$typed` function\n   * should only return assurance about the `$type` property, which is what it\n   * actually checks:\n   *\n   * ```ts\n   * declare const obj2: unknown\n   * if (is$typed(obj2, 'foo', 'main')) {\n   *  obj2.$type // $Type<'foo', 'main'>\n   *  // @ts-expect-error\n   *  obj2.foo\n   * }\n   * ```\n   *\n   * The construct bellow is what makes these two scenarios possible.\n   */\n  ''\n}\nexport type $TypedObject<V, Id extends string, Hash extends string> = V extends {\n  $type: $Type<Id, Hash>\n}\n  ? V\n  : V extends { $type?: string }\n    ? V extends { $type?: infer T extends $Type<Id, Hash> }\n      ? V & { $type: T }\n      : never\n    : V & { $type: $Type<Id, Hash> }\n\nexport function is$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is $TypedObject<V, Id, Hash> {\n  return isObject(v) && '$type' in v && is$type(v.$type, id, hash)\n}\n\nexport function maybe$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is V & object & { $type?: $Type<Id, Hash> } {\n  return (\n    isObject(v) &&\n    ('$type' in v\n      ? v.$type === undefined || is$type(v.$type, id, hash)\n      : true)\n  )\n}\n\nexport type Validator<R = unknown> = (v: unknown) => ValidationResult<R>\nexport type ValidatorParam<V extends Validator> =\n  V extends Validator<infer R> ? R : never\n\n/**\n * Utility function that allows to convert a \"validate*\" utility function into a\n * type predicate.\n */\nexport function asPredicate<V extends Validator>(validate: V) {\n  return function <T>(v: T): v is T & ValidatorParam<V> {\n    return validate(v).success\n  }\n}\n`)\n  })\n\nexport const lexiconsTs = (project, lexicons: LexiconDoc[]) =>\n  gen(project, '/lexicons.ts', async (file) => {\n    //= import { type LexiconDoc, Lexicons } from '@atproto/lexicon'\n    file\n      .addImportDeclaration({\n        moduleSpecifier: '@atproto/lexicon',\n      })\n      .addNamedImports([\n        { name: 'LexiconDoc', isTypeOnly: true },\n        { name: 'Lexicons' },\n        { name: 'ValidationError' },\n        { name: 'ValidationResult', isTypeOnly: true },\n      ])\n\n    //= import { is$typed, maybe$typed, type $Typed } from './util'\n    file\n      .addImportDeclaration({\n        moduleSpecifier: './util.js',\n      })\n      .addNamedImports([\n        { name: '$Typed', isTypeOnly: true },\n        { name: 'is$typed' },\n        { name: 'maybe$typed' },\n      ])\n\n    //= export const schemaDict = {...} as const satisfies Record<string, LexiconDoc>\n    file.addVariableStatement({\n      isExported: true,\n      declarationKind: VariableDeclarationKind.Const,\n      declarations: [\n        {\n          name: 'schemaDict',\n          initializer:\n            JSON.stringify(\n              lexicons.reduce(\n                (acc, cur) => ({\n                  ...acc,\n                  [toTitleCase(cur.id)]: cur,\n                }),\n                {},\n              ),\n              null,\n              2,\n            ) + ' as const satisfies Record<string, LexiconDoc>',\n        },\n      ],\n    })\n\n    //= export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]\n    file.addVariableStatement({\n      isExported: true,\n      declarationKind: VariableDeclarationKind.Const,\n      declarations: [\n        {\n          name: 'schemas',\n          initializer: 'Object.values(schemaDict) satisfies LexiconDoc[]',\n        },\n      ],\n    })\n\n    //= export const lexicons: Lexicons = new Lexicons(schemas)\n    file.addVariableStatement({\n      isExported: true,\n      declarationKind: VariableDeclarationKind.Const,\n      declarations: [\n        {\n          name: 'lexicons',\n          type: 'Lexicons',\n          initializer: 'new Lexicons(schemas)',\n        },\n      ],\n    })\n\n    file.addFunction({\n      isExported: true,\n      name: 'validate',\n      overloads: [\n        {\n          typeParameters: ['T extends { $type: string }'],\n          parameters: [\n            { name: 'v', type: 'unknown' },\n            { name: 'id', type: 'string' },\n            { name: 'hash', type: 'string' },\n            { name: 'requiredType', type: 'true' },\n          ],\n          returnType: 'ValidationResult<T>',\n        },\n        {\n          typeParameters: ['T extends { $type?: string }'],\n          parameters: [\n            { name: 'v', type: 'unknown' },\n            { name: 'id', type: 'string' },\n            { name: 'hash', type: 'string' },\n            { name: 'requiredType', type: 'false', hasQuestionToken: true },\n          ],\n          returnType: 'ValidationResult<T>',\n        },\n      ],\n      parameters: [\n        { name: 'v', type: 'unknown' },\n        { name: 'id', type: 'string' },\n        { name: 'hash', type: 'string' },\n        { name: 'requiredType', type: 'boolean', hasQuestionToken: true },\n      ],\n      statements: [\n        // If $type is present, make sure it is valid before validating the rest of the object\n        'return (requiredType ? is$typed : maybe$typed)(v, id, hash) ? lexicons.validate(`${id}#${hash}`, v) : { success: false, error: new ValidationError(`Must be an object with \"${hash === \\'main\\' ? id : `${id}#${hash}`}\" $type property`) }',\n      ],\n      returnType: 'ValidationResult',\n    })\n\n    //= export const ids = {...}\n    file.addVariableStatement({\n      isExported: true,\n      declarationKind: VariableDeclarationKind.Const,\n      declarations: [\n        {\n          name: 'ids',\n          initializer: `{${lexicons\n            .map(\n              (lex) => `\\n  ${toTitleCase(lex.id)}: ${JSON.stringify(lex.id)},`,\n            )\n            .join('')}\\n} as const`,\n        },\n      ],\n    })\n  })\n\nexport async function gen(\n  project: Project,\n  path: string,\n  gen: (file: SourceFile) => Promise<void>,\n): Promise<GeneratedFile> {\n  const file = project.createSourceFile(path)\n  await gen(file)\n  await file.save() // Save in the \"in memory\" file system\n  const src = `${banner()}${file.getFullText()}`\n  const content = await format(src, PRETTIER_OPTS)\n\n  return { path, content }\n}\n\nfunction banner() {\n  return `/**\n * GENERATED CODE - DO NOT MODIFY\n */\n`\n}\n"
  },
  {
    "path": "packages/lex-cli/src/codegen/lex-gen.ts",
    "content": "import { relative as getRelativePath } from 'node:path/posix'\nimport { JSDoc, SourceFile, VariableDeclarationKind } from 'ts-morph'\nimport {\n  type LexArray,\n  type LexBlob,\n  type LexBytes,\n  type LexCidLink,\n  type LexIpldType,\n  type LexObject,\n  type LexPrimitive,\n  type LexToken,\n  Lexicons,\n} from '@atproto/lexicon'\nimport { toCamelCase, toScreamingSnakeCase, toTitleCase } from './util'\n\ninterface Commentable {\n  addJsDoc: ({ description }: { description: string }) => JSDoc\n}\nexport function genComment<T extends Commentable>(\n  commentable: T,\n  def: { description?: string },\n): T {\n  if (def.description) {\n    commentable.addJsDoc({ description: def.description })\n  }\n  return commentable\n}\n\nexport function genCommonImports(file: SourceFile, baseNsid: string) {\n  //= import {ValidationResult, BlobRef} from '@atproto/lexicon'\n  file\n    .addImportDeclaration({\n      moduleSpecifier: '@atproto/lexicon',\n    })\n    .addNamedImports([\n      { name: 'ValidationResult', isTypeOnly: true },\n      { name: 'BlobRef' },\n    ])\n\n  //= import {CID} from 'multiformats/cid'\n  file\n    .addImportDeclaration({\n      moduleSpecifier: 'multiformats/cid',\n    })\n    .addNamedImports([{ name: 'CID' }])\n\n  //= import { validate as _validate } from '../../lexicons.ts'\n  file\n    .addImportDeclaration({\n      moduleSpecifier: `${baseNsid\n        .split('.')\n        .map((_str) => '..')\n        .join('/')}/lexicons`,\n    })\n    .addNamedImports([{ name: 'validate', alias: '_validate' }])\n\n  //= import { type $Typed, is$typed as _is$typed, type OmitKey } from '../[...]/util.ts'\n  file\n    .addImportDeclaration({\n      moduleSpecifier: `${baseNsid\n        .split('.')\n        .map((_str) => '..')\n        .join('/')}/util`,\n    })\n    .addNamedImports([\n      { name: '$Typed', isTypeOnly: true },\n      { name: 'is$typed', alias: '_is$typed' },\n      { name: 'OmitKey', isTypeOnly: true },\n    ])\n\n  // tsc adds protection against circular imports, which hurts bundle size.\n  // Since we know that lexicon.ts and util.ts do not depend on the file being\n  // generated, we can safely bypass this protection.\n  // Note that we are not using `import * as util from '../../util'` because\n  // typescript will emit is own helpers for the import, which we want to avoid.\n  file.addVariableStatement({\n    isExported: false,\n    declarationKind: VariableDeclarationKind.Const,\n    declarations: [\n      { name: 'is$typed', initializer: '_is$typed' },\n      { name: 'validate', initializer: '_validate' },\n    ],\n  })\n\n  //= const id = \"{baseNsid}\"\n  file.addVariableStatement({\n    isExported: false, // Do not export to allow tree-shaking\n    declarationKind: VariableDeclarationKind.Const,\n    declarations: [{ name: 'id', initializer: JSON.stringify(baseNsid) }],\n  })\n}\n\nexport function genImports(\n  file: SourceFile,\n  imports: Set<string>,\n  baseNsid: string,\n) {\n  const startPath = '/' + baseNsid.split('.').slice(0, -1).join('/')\n\n  for (const nsid of imports) {\n    const targetPath = '/' + nsid.split('.').join('/') + '.js'\n    let resolvedPath = getRelativePath(startPath, targetPath)\n    if (!resolvedPath.startsWith('.')) {\n      resolvedPath = `./${resolvedPath}`\n    }\n    file.addImportDeclaration({\n      isTypeOnly: true,\n      moduleSpecifier: resolvedPath,\n      namespaceImport: toTitleCase(nsid),\n    })\n  }\n}\n\nexport function genUserType(\n  file: SourceFile,\n  imports: Set<string>,\n  lexicons: Lexicons,\n  lexUri: string,\n) {\n  const def = lexicons.getDefOrThrow(lexUri)\n  switch (def.type) {\n    case 'array':\n      genArray(file, imports, lexUri, def)\n      break\n    case 'token':\n      genToken(file, lexUri, def)\n      break\n    case 'object': {\n      const ifaceName: string = toTitleCase(getHash(lexUri))\n      genObject(file, imports, lexUri, def, ifaceName, {\n        typeProperty: true,\n      })\n      genObjHelpers(file, lexUri, ifaceName, {\n        requireTypeProperty: false,\n      })\n      break\n    }\n\n    case 'blob':\n    case 'bytes':\n    case 'cid-link':\n    case 'boolean':\n    case 'integer':\n    case 'string':\n    case 'unknown':\n      genPrimitiveOrBlob(file, lexUri, def)\n      break\n\n    default:\n      throw new Error(\n        `genLexUserType() called with wrong definition type (${def.type}) in ${lexUri}`,\n      )\n  }\n}\n\nfunction genObject(\n  file: SourceFile,\n  imports: Set<string>,\n  lexUri: string,\n  def: LexObject,\n  ifaceName: string,\n  {\n    defaultsArePresent = true,\n    allowUnknownProperties = false,\n    typeProperty = false,\n  }: {\n    defaultsArePresent?: boolean\n    allowUnknownProperties?: boolean\n    typeProperty?: boolean | 'required'\n  } = {},\n) {\n  const iface = file.addInterface({\n    name: ifaceName,\n    isExported: true,\n  })\n  genComment(iface, def)\n\n  if (typeProperty) {\n    const hash = getHash(lexUri)\n    const baseNsid = stripScheme(stripHash(lexUri))\n\n    //= $type?: <uri>\n    iface.addProperty({\n      name: typeProperty === 'required' ? `$type` : `$type?`,\n      type:\n        // Not using $Type here because it is less readable than a plain string\n        // `$Type<${JSON.stringify(baseNsid)}, ${JSON.stringify(hash)}>`\n        hash === 'main'\n          ? JSON.stringify(`${baseNsid}`)\n          : JSON.stringify(`${baseNsid}#${hash}`),\n    })\n  }\n\n  const nullableProps = new Set(def.nullable)\n  if (def.properties) {\n    for (const propKey in def.properties) {\n      const propDef = def.properties[propKey]\n      const propNullable = nullableProps.has(propKey)\n      const req =\n        def.required?.includes(propKey) ||\n        (defaultsArePresent &&\n          'default' in propDef &&\n          propDef.default !== undefined)\n      if (propDef.type === 'ref' || propDef.type === 'union') {\n        //= propName: External|External\n        const types =\n          propDef.type === 'union'\n            ? propDef.refs.map((ref) => refToUnionType(ref, lexUri, imports))\n            : [refToType(propDef.ref, stripScheme(stripHash(lexUri)), imports)]\n        if (propDef.type === 'union' && !propDef.closed) {\n          types.push('{ $type: string }')\n        }\n        iface.addProperty({\n          name: `${propKey}${req ? '' : '?'}`,\n          type: makeType(types, { nullable: propNullable }),\n        })\n        continue\n      } else {\n        if (propDef.type === 'array') {\n          //= propName: type[]\n          let propAst\n          if (propDef.items.type === 'ref') {\n            propAst = iface.addProperty({\n              name: `${propKey}${req ? '' : '?'}`,\n              type: makeType(\n                refToType(\n                  propDef.items.ref,\n                  stripScheme(stripHash(lexUri)),\n                  imports,\n                ),\n                {\n                  nullable: propNullable,\n                  array: true,\n                },\n              ),\n            })\n          } else if (propDef.items.type === 'union') {\n            const types = propDef.items.refs.map((ref) =>\n              refToUnionType(ref, lexUri, imports),\n            )\n            if (!propDef.items.closed) {\n              types.push('{ $type: string }')\n            }\n            propAst = iface.addProperty({\n              name: `${propKey}${req ? '' : '?'}`,\n              type: makeType(types, {\n                nullable: propNullable,\n                array: true,\n              }),\n            })\n          } else {\n            propAst = iface.addProperty({\n              name: `${propKey}${req ? '' : '?'}`,\n              type: makeType(primitiveOrBlobToType(propDef.items), {\n                nullable: propNullable,\n                array: true,\n              }),\n            })\n          }\n          genComment(propAst, propDef)\n        } else {\n          //= propName: type\n          genComment(\n            iface.addProperty({\n              name: `${propKey}${req ? '' : '?'}`,\n              type: makeType(primitiveOrBlobToType(propDef), {\n                nullable: propNullable,\n              }),\n            }),\n            propDef,\n          )\n        }\n      }\n    }\n\n    if (allowUnknownProperties) {\n      //= [k: string]: unknown\n      iface.addIndexSignature({\n        keyName: 'k',\n        keyType: 'string',\n        returnType: 'unknown',\n      })\n    }\n  }\n}\n\nexport function genToken(file: SourceFile, lexUri: string, def: LexToken) {\n  //= /** <comment> */\n  //= export const <TOKEN> = `${id}#<token>`\n  genComment(\n    file.addVariableStatement({\n      isExported: true,\n      declarationKind: VariableDeclarationKind.Const,\n      declarations: [\n        {\n          name: toScreamingSnakeCase(getHash(lexUri)),\n          initializer: `\\`\\${id}#${getHash(lexUri)}\\``,\n        },\n      ],\n    }),\n    def,\n  )\n}\n\nexport function genArray(\n  file: SourceFile,\n  imports: Set<string>,\n  lexUri: string,\n  def: LexArray,\n) {\n  if (def.items.type === 'ref') {\n    file.addTypeAlias({\n      name: toTitleCase(getHash(lexUri)),\n      type: `${refToType(\n        def.items.ref,\n        stripScheme(stripHash(lexUri)),\n        imports,\n      )}[]`,\n      isExported: true,\n    })\n  } else if (def.items.type === 'union') {\n    const types = def.items.refs.map((ref) =>\n      refToUnionType(ref, lexUri, imports),\n    )\n    if (!def.items.closed) {\n      types.push('{ $type: string }')\n    }\n    file.addTypeAlias({\n      name: toTitleCase(getHash(lexUri)),\n      type: `(${types.join('|')})[]`,\n      isExported: true,\n    })\n  } else {\n    genComment(\n      file.addTypeAlias({\n        name: toTitleCase(getHash(lexUri)),\n        type: `${primitiveOrBlobToType(def.items)}[]`,\n        isExported: true,\n      }),\n      def,\n    )\n  }\n}\n\nexport function genPrimitiveOrBlob(\n  file: SourceFile,\n  lexUri: string,\n  def: LexPrimitive | LexBlob | LexIpldType,\n) {\n  genComment(\n    file.addTypeAlias({\n      name: toTitleCase(getHash(lexUri)),\n      type: primitiveOrBlobToType(def),\n      isExported: true,\n    }),\n    def,\n  )\n}\n\nexport function genXrpcParams(\n  file: SourceFile,\n  lexicons: Lexicons,\n  lexUri: string,\n  defaultsArePresent = true,\n) {\n  const def = lexicons.getDefOrThrow(lexUri, [\n    'query',\n    'subscription',\n    'procedure',\n  ])\n\n  // @NOTE We need to use a `type` here instead of  an `interface` because we\n  // need the generated type to be used as generic type parameter like this:\n  //\n  // type QueryParams = {} // Generated by this function\n  //\n  // type MyUtil<P extends xrpcServer.QueryParam> = (...)\n  // type NsType = MyUtil<NS.QueryParams> // ERROR if `NS.QueryParams` is an `interface`\n  //\n  // Second line will fail if `NS.QueryParams` is an `interface` that does\n  // not explicitly extend `xrpcServer.QueryParam`, or have a string index\n  // signature that encompasses `xrpcServer.QueryParam`.\n\n  //= export type QueryParams = {...}\n  if (def.parameters) {\n    genComment(\n      file.addTypeAlias({\n        name: 'QueryParams',\n        isExported: true,\n        type: `{\n          ${Object.entries(def.parameters.properties)\n            .map(([paramKey, paramDef]) => {\n              const req =\n                def.parameters!.required?.includes(paramKey) ||\n                (defaultsArePresent &&\n                  'default' in paramDef &&\n                  paramDef.default !== undefined)\n              const jsDoc = paramDef.description\n                ? `/** ${paramDef.description} */\\n`\n                : ''\n              return `${jsDoc}${paramKey}${req ? '' : '?'}: ${\n                paramDef.type === 'array'\n                  ? primitiveToType(paramDef.items) + '[]'\n                  : primitiveToType(paramDef)\n              }`\n            })\n            .join('\\n')}\n        }`,\n      }),\n      def.parameters,\n    )\n  } else {\n    file.addTypeAlias({\n      name: 'QueryParams',\n      isExported: true,\n      type: '{}',\n    })\n  }\n}\n\nexport function genXrpcInput(\n  file: SourceFile,\n  imports: Set<string>,\n  lexicons: Lexicons,\n  lexUri: string,\n  defaultsArePresent = true,\n) {\n  const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure'])\n\n  if (def.type === 'procedure' && def.input?.schema) {\n    if (def.input.schema.type === 'ref' || def.input.schema.type === 'union') {\n      //= export type InputSchema = ...\n\n      const types =\n        def.input.schema.type === 'union'\n          ? def.input.schema.refs.map((ref) =>\n              refToUnionType(ref, lexUri, imports),\n            )\n          : [\n              refToType(\n                def.input.schema.ref,\n                stripScheme(stripHash(lexUri)),\n                imports,\n              ),\n            ]\n\n      if (def.input.schema.type === 'union' && !def.input.schema.closed) {\n        types.push('{ $type: string }')\n      }\n      file.addTypeAlias({\n        name: 'InputSchema',\n        type: types.join('|'),\n        isExported: true,\n      })\n    } else {\n      //= export interface InputSchema {...}\n      genObject(file, imports, lexUri, def.input.schema, `InputSchema`, {\n        defaultsArePresent,\n      })\n    }\n  } else if (def.type === 'procedure' && def.input?.encoding) {\n    //= export type InputSchema = string | Uint8Array | Blob\n    file.addTypeAlias({\n      isExported: true,\n      name: 'InputSchema',\n      type: 'string | Uint8Array | Blob',\n    })\n  } else {\n    //= export type InputSchema = undefined\n    file.addTypeAlias({\n      isExported: true,\n      name: 'InputSchema',\n      type: 'undefined',\n    })\n  }\n}\n\nexport function genXrpcOutput(\n  file: SourceFile,\n  imports: Set<string>,\n  lexicons: Lexicons,\n  lexUri: string,\n  defaultsArePresent = true,\n) {\n  const def = lexicons.getDefOrThrow(lexUri, [\n    'query',\n    'subscription',\n    'procedure',\n  ])\n\n  const schema =\n    def.type === 'subscription' ? def.message?.schema : def.output?.schema\n  if (schema) {\n    if (schema.type === 'ref' || schema.type === 'union') {\n      //= export type OutputSchema = ...\n      const types =\n        schema.type === 'union'\n          ? schema.refs.map((ref) => refToUnionType(ref, lexUri, imports))\n          : [refToType(schema.ref, stripScheme(stripHash(lexUri)), imports)]\n      if (schema.type === 'union' && !schema.closed) {\n        types.push('{ $type: string }')\n      }\n      file.addTypeAlias({\n        name: 'OutputSchema',\n        type: types.join('|'),\n        isExported: true,\n      })\n    } else {\n      //= export interface OutputSchema {...}\n      genObject(file, imports, lexUri, schema, `OutputSchema`, {\n        defaultsArePresent,\n      })\n    }\n  }\n}\n\nexport function genRecord(\n  file: SourceFile,\n  imports: Set<string>,\n  lexicons: Lexicons,\n  lexUri: string,\n) {\n  const hash = getHash(lexUri)\n  const ifaceName: string = toTitleCase(hash)\n  const def = lexicons.getDefOrThrow(lexUri, ['record'])\n\n  //= export interface {X} {...}\n  genObject(file, imports, lexUri, def.record, ifaceName, {\n    defaultsArePresent: true,\n    allowUnknownProperties: true,\n    typeProperty: 'required',\n  })\n\n  //= export function is{X}(v: unknown): v is {X} {...}\n  genObjHelpers(file, lexUri, ifaceName, {\n    requireTypeProperty: true,\n  })\n\n  // For convenience, we re-export the type and the type guard under the generic\n  // names \"Record\", \"isRecord\" and \"validateRecord\".\n  // @NOTE This does not account for potential name clashes with a potential\n  // \"#record\" def.\n\n  //= export { {X} as Record, is{X} as isRecord }\n  file.addExportDeclaration({\n    namedExports: [\n      {\n        isTypeOnly: true,\n        name: ifaceName,\n        alias: 'Record',\n      },\n      {\n        name: `is${ifaceName}`,\n        alias: 'isRecord',\n      },\n      {\n        name: `validate${ifaceName}`,\n        alias: 'validateRecord',\n      },\n    ],\n  })\n}\n\nfunction genObjHelpers(\n  file: SourceFile,\n  lexUri: string,\n  ifaceName: string,\n  {\n    requireTypeProperty,\n  }: {\n    requireTypeProperty: boolean\n  },\n) {\n  const hash = getHash(lexUri)\n\n  const hashVar = `hash${ifaceName}`\n\n  file.addVariableStatement({\n    isExported: false,\n    declarationKind: VariableDeclarationKind.Const,\n    declarations: [{ name: hashVar, initializer: JSON.stringify(hash) }],\n  })\n\n  const isX = toCamelCase(`is-${ifaceName}`)\n\n  //= export function is{X}<V>(v: V) {...}\n  file\n    .addFunction({\n      name: isX,\n      typeParameters: [{ name: `V` }],\n      parameters: [{ name: `v`, type: `V` }],\n      isExported: true,\n    })\n    .setBodyText(`return is$typed(v, id, ${hashVar})`)\n\n  const validateX = toCamelCase(`validate-${ifaceName}`)\n\n  //= export function validate{X}(v: unknown) {...}\n  file\n    .addFunction({\n      name: validateX,\n      typeParameters: [{ name: `V` }],\n      parameters: [{ name: `v`, type: `V` }],\n      isExported: true,\n    })\n    .setBodyText(\n      `return validate<${ifaceName} & V>(v, id, ${hashVar}${requireTypeProperty ? ', true' : ''})`,\n    )\n}\n\nexport function stripScheme(uri: string): string {\n  if (uri.startsWith('lex:')) return uri.slice(4)\n  return uri\n}\n\nexport function stripHash(uri: string): string {\n  return uri.split('#')[0] || ''\n}\n\nexport function getHash(uri: string): string {\n  return uri.split('#').pop() || ''\n}\n\nexport function ipldToType(def: LexCidLink | LexBytes) {\n  if (def.type === 'bytes') {\n    return 'Uint8Array'\n  }\n  return 'CID'\n}\n\nfunction refToUnionType(\n  ref: string,\n  lexUri: string,\n  imports: Set<string>,\n): string {\n  const baseNsid = stripScheme(stripHash(lexUri))\n  return `$Typed<${refToType(ref, baseNsid, imports)}>`\n}\n\nfunction refToType(\n  ref: string,\n  baseNsid: string,\n  imports: Set<string>,\n): string {\n  // TODO: import external types!\n  let [refBase, refHash] = ref.split('#')\n  refBase = stripScheme(refBase)\n  if (!refHash) refHash = 'main'\n\n  // internal\n  if (!refBase || baseNsid === refBase) {\n    return toTitleCase(refHash)\n  }\n\n  // external\n  imports.add(refBase)\n  return `${toTitleCase(refBase)}.${toTitleCase(refHash)}`\n}\n\nexport function primitiveOrBlobToType(\n  def: LexBlob | LexPrimitive | LexIpldType,\n): string {\n  switch (def.type) {\n    case 'blob':\n      return 'BlobRef'\n    case 'bytes':\n      return 'Uint8Array'\n    case 'cid-link':\n      return 'CID'\n    default:\n      return primitiveToType(def)\n  }\n}\n\nexport function primitiveToType(def: LexPrimitive): string {\n  switch (def.type) {\n    case 'string':\n      if (def.knownValues?.length) {\n        return `${def.knownValues\n          .map((v) => JSON.stringify(v))\n          .join(' | ')} | (string & {})`\n      } else if (def.enum) {\n        return def.enum.map((v) => JSON.stringify(v)).join(' | ')\n      } else if (def.const) {\n        return JSON.stringify(def.const)\n      }\n      return 'string'\n    case 'integer':\n      if (def.enum) {\n        return def.enum.map((v) => JSON.stringify(v)).join(' | ')\n      } else if (def.const) {\n        return JSON.stringify(def.const)\n      }\n      return 'number'\n    case 'boolean':\n      if (def.const) {\n        return JSON.stringify(def.const)\n      }\n      return 'boolean'\n    case 'unknown':\n      // @TODO Should we use \"object\" here ?\n      // the \"Record\" identifier from typescript get overwritten by the Record\n      // interface created by lex-cli.\n      return '{ [_ in string]: unknown }' // Record<string, unknown>\n    default:\n      throw new Error(`Unexpected primitive type: ${JSON.stringify(def)}`)\n  }\n}\n\nfunction makeType(\n  _types: string | string[],\n  opts?: { array?: boolean; nullable?: boolean },\n) {\n  const types = ([] as string[]).concat(_types)\n  if (opts?.nullable) types.push('null')\n  const arr = opts?.array ? '[]' : ''\n  if (types.length === 1) return `(${types[0]})${arr}`\n  if (arr) return `(${types.join(' | ')})${arr}`\n  return types.join(' | ')\n}\n"
  },
  {
    "path": "packages/lex-cli/src/codegen/server.ts",
    "content": "import {\n  IndentationText,\n  Project,\n  SourceFile,\n  VariableDeclarationKind,\n} from 'ts-morph'\nimport { type LexiconDoc, Lexicons } from '@atproto/lexicon'\nimport { NSID } from '@atproto/syntax'\nimport { type GeneratedAPI } from '../types'\nimport { gen, lexiconsTs, utilTs } from './common'\nimport {\n  genCommonImports,\n  genImports,\n  genRecord,\n  genUserType,\n  genXrpcInput,\n  genXrpcOutput,\n  genXrpcParams,\n} from './lex-gen'\nimport {\n  type DefTreeNode,\n  lexiconsToDefTree,\n  schemasToNsidTokens,\n  toCamelCase,\n  toScreamingSnakeCase,\n  toTitleCase,\n} from './util'\n\nexport async function genServerApi(\n  lexiconDocs: LexiconDoc[],\n): Promise<GeneratedAPI> {\n  const project = new Project({\n    useInMemoryFileSystem: true,\n    manipulationSettings: { indentationText: IndentationText.TwoSpaces },\n  })\n  const api: GeneratedAPI = { files: [] }\n  const lexicons = new Lexicons(lexiconDocs)\n  const nsidTree = lexiconsToDefTree(lexiconDocs)\n  const nsidTokens = schemasToNsidTokens(lexiconDocs)\n  for (const lexiconDoc of lexiconDocs) {\n    api.files.push(await lexiconTs(project, lexicons, lexiconDoc))\n  }\n  api.files.push(await utilTs(project))\n  api.files.push(await lexiconsTs(project, lexiconDocs))\n  api.files.push(await indexTs(project, lexiconDocs, nsidTree, nsidTokens))\n  return api\n}\n\nconst indexTs = (\n  project: Project,\n  lexiconDocs: LexiconDoc[],\n  nsidTree: DefTreeNode[],\n  nsidTokens: Record<string, string[]>,\n) =>\n  gen(project, '/index.ts', async (file) => {\n    //= import {createServer as createXrpcServer, Server as XrpcServer} from '@atproto/xrpc-server'\n    file.addImportDeclaration({\n      moduleSpecifier: '@atproto/xrpc-server',\n      namedImports: [\n        { name: 'Auth', isTypeOnly: true },\n        { name: 'Options', alias: 'XrpcOptions', isTypeOnly: true },\n        { name: 'Server', alias: 'XrpcServer' },\n        { name: 'StreamConfigOrHandler', isTypeOnly: true },\n        { name: 'MethodConfigOrHandler', isTypeOnly: true },\n        { name: 'createServer', alias: 'createXrpcServer' },\n      ],\n    })\n    //= import {schemas} from './lexicons.js'\n    file\n      .addImportDeclaration({\n        moduleSpecifier: './lexicons.js',\n      })\n      .addNamedImport({\n        name: 'schemas',\n      })\n\n    // generate type imports\n    for (const lexiconDoc of lexiconDocs) {\n      if (\n        lexiconDoc.defs.main?.type !== 'query' &&\n        lexiconDoc.defs.main?.type !== 'subscription' &&\n        lexiconDoc.defs.main?.type !== 'procedure'\n      ) {\n        continue\n      }\n      file\n        .addImportDeclaration({\n          moduleSpecifier: `./types/${lexiconDoc.id.split('.').join('/')}.js`,\n        })\n        .setNamespaceImport(toTitleCase(lexiconDoc.id))\n    }\n\n    // generate token enums\n    for (const nsidAuthority in nsidTokens) {\n      // export const {THE_AUTHORITY} = {\n      //  {Name}: \"{authority.the.name}\"\n      // }\n      file.addVariableStatement({\n        isExported: true,\n        declarationKind: VariableDeclarationKind.Const,\n        declarations: [\n          {\n            name: toScreamingSnakeCase(nsidAuthority),\n            initializer: [\n              '{',\n              ...nsidTokens[nsidAuthority].map(\n                (nsidName) =>\n                  `${toTitleCase(nsidName)}: \"${nsidAuthority}.${nsidName}\",`,\n              ),\n              '}',\n            ].join('\\n'),\n          },\n        ],\n      })\n    }\n\n    //= export function createServer(options?: XrpcOptions) { ... }\n    const createServerFn = file.addFunction({\n      name: 'createServer',\n      returnType: 'Server',\n      parameters: [\n        { name: 'options', type: 'XrpcOptions', hasQuestionToken: true },\n      ],\n      isExported: true,\n    })\n    createServerFn.setBodyText(`return new Server(options)`)\n\n    //= export class Server {...}\n    const serverCls = file.addClass({\n      name: 'Server',\n      isExported: true,\n    })\n    //= xrpc: XrpcServer = createXrpcServer(methodSchemas)\n    serverCls.addProperty({\n      name: 'xrpc',\n      type: 'XrpcServer',\n    })\n\n    // generate classes for the schemas\n    for (const ns of nsidTree) {\n      //= ns: NS\n      serverCls.addProperty({\n        name: ns.propName,\n        type: ns.className,\n      })\n\n      // class...\n      genNamespaceCls(file, ns)\n    }\n\n    //= constructor (options?: XrpcOptions) {\n    //=  this.xrpc = createXrpcServer(schemas, options)\n    //=  {namespace declarations}\n    //= }\n    serverCls\n      .addConstructor({\n        parameters: [\n          { name: 'options', type: 'XrpcOptions', hasQuestionToken: true },\n        ],\n      })\n      .setBodyText(\n        [\n          'this.xrpc = createXrpcServer(schemas, options)',\n          ...nsidTree.map(\n            (ns) => `this.${ns.propName} = new ${ns.className}(this)`,\n          ),\n        ].join('\\n'),\n      )\n  })\n\nfunction genNamespaceCls(file: SourceFile, ns: DefTreeNode) {\n  //= export class {ns}NS {...}\n  const cls = file.addClass({\n    name: ns.className,\n    isExported: true,\n  })\n  //= _server: Server\n  cls.addProperty({\n    name: '_server',\n    type: 'Server',\n  })\n\n  for (const child of ns.children) {\n    //= child: ChildNS\n    cls.addProperty({\n      name: child.propName,\n      type: child.className,\n    })\n\n    // recurse\n    genNamespaceCls(file, child)\n  }\n\n  //= constructor(server: Server) {\n  //=  this._server = server\n  //=  {child namespace declarations}\n  //= }\n  const cons = cls.addConstructor()\n  cons.addParameter({\n    name: 'server',\n    type: 'Server',\n  })\n  cons.setBodyText(\n    [\n      `this._server = server`,\n      ...ns.children.map(\n        (ns) => `this.${ns.propName} = new ${ns.className}(server)`,\n      ),\n    ].join('\\n'),\n  )\n\n  // methods\n  for (const userType of ns.userTypes) {\n    if (\n      userType.def.type !== 'query' &&\n      userType.def.type !== 'subscription' &&\n      userType.def.type !== 'procedure'\n    ) {\n      continue\n    }\n    const moduleName = toTitleCase(userType.nsid)\n    const name = toCamelCase(NSID.parse(userType.nsid).name || '')\n    const isSubscription = userType.def.type === 'subscription'\n    const method = cls.addMethod({\n      name,\n      typeParameters: [\n        {\n          name: 'A',\n          constraint: 'Auth',\n          default: 'void',\n        },\n      ],\n    })\n    method.addParameter({\n      name: 'cfg',\n      type: isSubscription\n        ? `StreamConfigOrHandler<\n          A,\n          ${moduleName}.QueryParams,\n          ${moduleName}.HandlerOutput,\n        >`\n        : `MethodConfigOrHandler<\n          A,\n          ${moduleName}.QueryParams,\n          ${moduleName}.HandlerInput,\n          ${moduleName}.HandlerOutput,\n        >`,\n    })\n    const methodType = isSubscription ? 'streamMethod' : 'method'\n    method.setBodyText(\n      [\n        // Placing schema on separate line, since the following one was being formatted\n        // into multiple lines and causing the ts-ignore to ignore the wrong line.\n        `const nsid = '${userType.nsid}' // @ts-ignore`,\n        `return this._server.xrpc.${methodType}(nsid, cfg)`,\n      ].join('\\n'),\n    )\n  }\n}\n\nconst lexiconTs = (project, lexicons: Lexicons, lexiconDoc: LexiconDoc) =>\n  gen(\n    project,\n    `/types/${lexiconDoc.id.split('.').join('/')}.ts`,\n    async (file) => {\n      const main = lexiconDoc.defs.main\n      if (main?.type === 'query' || main?.type === 'procedure') {\n        const streamingInput =\n          main?.type === 'procedure' &&\n          main.input?.encoding &&\n          !main.input.schema\n        const streamingOutput = main.output?.encoding && !main.output.schema\n        if (streamingInput || streamingOutput) {\n          //= import stream from 'node:stream'\n          file.addImportDeclaration({\n            moduleSpecifier: 'node:stream',\n            defaultImport: 'stream',\n          })\n        }\n      }\n\n      genCommonImports(file, lexiconDoc.id)\n\n      const imports: Set<string> = new Set()\n      for (const defId in lexiconDoc.defs) {\n        const def = lexiconDoc.defs[defId]\n        const lexUri = `${lexiconDoc.id}#${defId}`\n        if (defId === 'main') {\n          if (def.type === 'query' || def.type === 'procedure') {\n            genXrpcParams(file, lexicons, lexUri)\n            genXrpcInput(file, imports, lexicons, lexUri)\n            genXrpcOutput(file, imports, lexicons, lexUri, false)\n            genServerXrpcMethod(file, lexicons, lexUri)\n          } else if (def.type === 'subscription') {\n            genXrpcParams(file, lexicons, lexUri)\n            genXrpcOutput(file, imports, lexicons, lexUri, false)\n            genServerXrpcStreaming(file, lexicons, lexUri)\n          } else if (def.type === 'record') {\n            genRecord(file, imports, lexicons, lexUri)\n          } else {\n            genUserType(file, imports, lexicons, lexUri)\n          }\n        } else {\n          genUserType(file, imports, lexicons, lexUri)\n        }\n      }\n      genImports(file, imports, lexiconDoc.id)\n    },\n  )\n\nfunction genServerXrpcMethod(\n  file: SourceFile,\n  lexicons: Lexicons,\n  lexUri: string,\n) {\n  const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure'])\n\n  //= export interface HandlerInput {...}\n  if (def.type === 'procedure' && def.input?.encoding) {\n    const handlerInput = file.addInterface({\n      name: 'HandlerInput',\n      isExported: true,\n    })\n\n    handlerInput.addProperty({\n      name: 'encoding',\n      type: def.input.encoding\n        .split(',')\n        .map((v) => `'${v.trim()}'`)\n        .join(' | '),\n    })\n    handlerInput.addProperty({\n      name: 'body',\n      type: def.input.schema\n        ? def.input.encoding.includes(',')\n          ? 'InputSchema | stream.Readable'\n          : 'InputSchema'\n        : 'stream.Readable',\n    })\n  } else {\n    file.addTypeAlias({\n      isExported: true,\n      name: 'HandlerInput',\n      type: 'void',\n    })\n  }\n\n  // export interface HandlerSuccess {...}\n  let hasHandlerSuccess = false\n  if (def.output?.schema || def.output?.encoding) {\n    hasHandlerSuccess = true\n    const handlerSuccess = file.addInterface({\n      name: 'HandlerSuccess',\n      isExported: true,\n    })\n\n    if (def.output.encoding) {\n      handlerSuccess.addProperty({\n        name: 'encoding',\n        type: def.output.encoding\n          .split(',')\n          .map((v) => `'${v.trim()}'`)\n          .join(' | '),\n      })\n    }\n    if (def.output?.schema) {\n      if (def.output.encoding.includes(',')) {\n        handlerSuccess.addProperty({\n          name: 'body',\n          type: 'OutputSchema | Uint8Array | stream.Readable',\n        })\n      } else {\n        handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema' })\n      }\n    } else if (def.output?.encoding) {\n      handlerSuccess.addProperty({\n        name: 'body',\n        type: 'Uint8Array | stream.Readable',\n      })\n    }\n    handlerSuccess.addProperty({\n      name: 'headers?',\n      type: '{ [key: string]: string }',\n    })\n  }\n\n  // export interface HandlerError {...}\n  const handlerError = file.addInterface({\n    name: 'HandlerError',\n    isExported: true,\n  })\n  handlerError.addProperties([\n    { name: 'status', type: 'number' },\n    { name: 'message?', type: 'string' },\n  ])\n  if (def.errors?.length) {\n    handlerError.addProperty({\n      name: 'error?',\n      type: def.errors.map((err) => `'${err.name}'`).join(' | '),\n    })\n  }\n\n  // export type HandlerOutput = ...\n  file.addTypeAlias({\n    isExported: true,\n    name: 'HandlerOutput',\n    type: `HandlerError | ${hasHandlerSuccess ? 'HandlerSuccess' : 'void'}`,\n  })\n}\n\nfunction genServerXrpcStreaming(\n  file: SourceFile,\n  lexicons: Lexicons,\n  lexUri: string,\n) {\n  const def = lexicons.getDefOrThrow(lexUri, ['subscription'])\n\n  file.addImportDeclaration({\n    moduleSpecifier: '@atproto/xrpc-server',\n    namedImports: [{ name: 'ErrorFrame' }],\n  })\n\n  file.addImportDeclaration({\n    moduleSpecifier: 'node:http',\n    namedImports: [{ name: 'IncomingMessage' }],\n  })\n\n  // export type HandlerError = ...\n  file.addTypeAlias({\n    name: 'HandlerError',\n    isExported: true,\n    type: `ErrorFrame<${arrayToUnion(def.errors?.map((e) => e.name))}>`,\n  })\n\n  // export type HandlerOutput = ...\n  file.addTypeAlias({\n    isExported: true,\n    name: 'HandlerOutput',\n    type: `HandlerError | ${def.message?.schema ? 'OutputSchema' : 'void'}`,\n  })\n}\n\nfunction arrayToUnion(arr?: string[]) {\n  if (!arr?.length) {\n    return 'never'\n  }\n  return arr.map((item) => `'${item}'`).join(' | ')\n}\n"
  },
  {
    "path": "packages/lex-cli/src/codegen/util.ts",
    "content": "import { type LexUserType, type LexiconDoc } from '@atproto/lexicon'\nimport { NSID } from '@atproto/syntax'\n\nexport interface DefTreeNodeUserType {\n  nsid: string\n  def: LexUserType\n}\n\nexport interface DefTreeNode {\n  name: string\n  className: string\n  propName: string\n  children: DefTreeNode[]\n  userTypes: DefTreeNodeUserType[]\n}\n\nexport function lexiconsToDefTree(lexicons: LexiconDoc[]): DefTreeNode[] {\n  const tree: DefTreeNode[] = []\n  for (const lexicon of lexicons) {\n    if (!lexicon.defs.main) {\n      continue\n    }\n    const node = getOrCreateNode(tree, lexicon.id.split('.').slice(0, -1))\n    node.userTypes.push({ nsid: lexicon.id, def: lexicon.defs.main })\n  }\n  return tree\n}\n\nfunction getOrCreateNode(tree: DefTreeNode[], path: string[]): DefTreeNode {\n  let node: DefTreeNode | undefined\n  for (let i = 0; i < path.length; i++) {\n    const segment = path[i]\n    node = tree.find((v) => v.name === segment)\n    if (!node) {\n      node = {\n        name: segment,\n        className: `${toTitleCase(path.slice(0, i + 1).join('-'))}NS`,\n        propName: toCamelCase(segment),\n        children: [],\n        userTypes: [],\n      } as DefTreeNode\n      tree.push(node)\n    }\n    tree = node.children\n  }\n  if (!node) throw new Error(`Invalid schema path: ${path.join('.')}`)\n  return node\n}\n\nexport function schemasToNsidTokens(\n  lexiconDocs: LexiconDoc[],\n): Record<string, string[]> {\n  const nsidTokens: Record<string, string[]> = {}\n  for (const lexiconDoc of lexiconDocs) {\n    const nsidp = NSID.parse(lexiconDoc.id)\n    if (!nsidp.name) continue\n    for (const defId in lexiconDoc.defs) {\n      const def = lexiconDoc.defs[defId]\n      if (def.type !== 'token') continue\n      const authority = nsidp.segments.slice(0, -1).join('.')\n      nsidTokens[authority] ??= []\n      nsidTokens[authority].push(\n        nsidp.name + (defId === 'main' ? '' : `#${defId}`),\n      )\n    }\n  }\n  return nsidTokens\n}\n\nexport function toTitleCase(v: string): string {\n  v = v.replace(/^([a-z])/gi, (_, g) => g.toUpperCase()) // upper-case first letter\n  v = v.replace(/[.#-]([a-z])/gi, (_, g) => g.toUpperCase()) // uppercase any dash, dot, or hash segments\n  return v.replace(/[.-]/g, '') // remove lefover dashes or dots\n}\n\nexport function toCamelCase(v: string): string {\n  v = v.replace(/[.#-]([a-z])/gi, (_, g) => g.toUpperCase()) // uppercase any dash, dot, or hash segments\n  return v.replace(/[.-]/g, '') // remove lefover dashes or dots\n}\n\nexport function toScreamingSnakeCase(v: string): string {\n  v = v.replace(/[.#-]+/gi, '_') // convert dashes, dots, and hashes into underscores\n  return v.toUpperCase() // and scream!\n}\n"
  },
  {
    "path": "packages/lex-cli/src/index.ts",
    "content": "#!/usr/bin/env node\n\nimport path from 'node:path'\nimport { Command } from 'commander'\nimport yesno from 'yesno'\nimport { genClientApi } from './codegen/client'\nimport { genServerApi } from './codegen/server'\nimport * as mdGen from './mdgen'\nimport {\n  applyFileDiff,\n  genFileDiff,\n  genTsObj,\n  printFileDiff,\n  readAllLexicons,\n} from './util'\n\nconst program = new Command()\nprogram.name('lex').description('Lexicon CLI').version('0.0.0')\n\nprogram\n  .command('gen-md')\n  .description('Generate markdown documentation')\n  .option('--yes', 'skip confirmation')\n  .argument('<outfile>', 'path of the file to write to', toPath)\n  .argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)\n  .action(\n    async (outFile: string, lexiconPaths: string[], o: { yes?: true }) => {\n      if (!outFile.endsWith('.md')) {\n        console.error(\n          'Must supply the path to a .md file as the first parameter',\n        )\n        process.exit(1)\n      }\n      if (!o?.yes) await confirmOrExit()\n      console.log('Writing', outFile)\n      const lexicons = readAllLexicons(lexiconPaths)\n      await mdGen.process(outFile, lexicons)\n    },\n  )\n\nprogram\n  .command('gen-ts-obj')\n  .description('Generate a TS file that exports an array of lexicons')\n  .argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)\n  .action((lexiconPaths: string[]) => {\n    const lexicons = readAllLexicons(lexiconPaths)\n    console.log(genTsObj(lexicons))\n  })\n\nprogram\n  .command('gen-api')\n  .description('Generate a TS client API')\n  .option('--yes', 'skip confirmation')\n  .argument('<outdir>', 'path of the directory to write to', toPath)\n  .argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)\n  .action(async (outDir: string, lexiconPaths: string[], o: { yes?: true }) => {\n    const lexicons = readAllLexicons(lexiconPaths)\n    const api = await genClientApi(lexicons)\n    const diff = genFileDiff(outDir, api)\n    console.log('This will write the following files:')\n    printFileDiff(diff)\n    if (!o?.yes) await confirmOrExit()\n    applyFileDiff(diff)\n    console.log('API generated.')\n  })\n\nprogram\n  .command('gen-server')\n  .description('Generate a TS server API')\n  .option('--yes', 'skip confirmation')\n  .argument('<outdir>', 'path of the directory to write to', toPath)\n  .argument('<lexicons...>', 'paths of the lexicon files to include', toPaths)\n  .action(async (outDir: string, lexiconPaths: string[], o: { yes?: true }) => {\n    const lexicons = readAllLexicons(lexiconPaths)\n    const api = await genServerApi(lexicons)\n    const diff = genFileDiff(outDir, api)\n    console.log('This will write the following files:')\n    printFileDiff(diff)\n    if (!o?.yes) await confirmOrExit()\n    applyFileDiff(diff)\n    console.log('API generated.')\n  })\n\nprogram.parse()\n\nfunction toPath(v: string) {\n  return v ? path.resolve(v) : undefined\n}\n\nfunction toPaths(v: string, acc: string[]) {\n  acc = acc || []\n  acc.push(path.resolve(v))\n  return acc\n}\n\nasync function confirmOrExit() {\n  const ok = await yesno({\n    question: 'Are you sure you want to continue? [y/N]',\n    defaultValue: false,\n  })\n  if (!ok) {\n    console.log('Aborted.')\n    process.exit(0)\n  }\n}\n"
  },
  {
    "path": "packages/lex-cli/src/mdgen/index.ts",
    "content": "import fs from 'node:fs'\nimport { type LexiconDoc } from '@atproto/lexicon'\n\nconst INSERT_START = [\n  '<!-- START lex generated content. Please keep comment here to allow auto update -->',\n  \"<!-- DON'T EDIT THIS SECTION! INSTEAD RE-RUN lex TO UPDATE -->\",\n]\nconst INSERT_END = [\n  '<!-- END lex generated TOC please keep comment here to allow auto update -->',\n]\n\nexport async function process(outFilePath: string, lexicons: LexiconDoc[]) {\n  let existingContent = ''\n  try {\n    existingContent = fs.readFileSync(outFilePath, 'utf8')\n  } catch (e) {\n    // ignore - no existing content\n  }\n  const fileLines: StringTree = existingContent.split('\\n')\n\n  // find previously generated content\n  let startIndex = fileLines.findIndex((line) => matchesStart(line))\n  let endIndex = fileLines.findIndex((line) => matchesEnd(line))\n  if (startIndex === -1) {\n    startIndex = fileLines.length\n  }\n  if (endIndex === -1) {\n    endIndex = fileLines.length\n  }\n\n  // generate & insert content\n  fileLines.splice(startIndex, endIndex - startIndex + 1, [\n    INSERT_START,\n    await genMdLines(lexicons),\n    INSERT_END,\n  ])\n\n  fs.writeFileSync(outFilePath, merge(fileLines), 'utf8')\n}\n\nasync function genMdLines(lexicons: LexiconDoc[]): Promise<StringTree> {\n  const doc: StringTree = []\n  for (const lexicon of lexicons) {\n    console.log(lexicon.id)\n    const desc: StringTree = []\n    if (lexicon.description) {\n      desc.push(lexicon.description, ``)\n    }\n    doc.push([\n      `---`,\n      ``,\n      `## ${lexicon.id}`,\n      '',\n      desc,\n      '```json',\n      JSON.stringify(lexicon, null, 2),\n      '```',\n    ])\n  }\n  return doc\n}\n\ntype StringTree = (StringTree | string | undefined)[]\nfunction merge(arr: StringTree): string {\n  return arr\n    .flat(10)\n    .filter((v) => typeof v === 'string')\n    .join('\\n')\n}\n\nfunction matchesStart(line) {\n  return /<!-- START lex /.test(line)\n}\n\nfunction matchesEnd(line) {\n  return /<!-- END lex /.test(line)\n}\n"
  },
  {
    "path": "packages/lex-cli/src/types.ts",
    "content": "export interface GeneratedFile {\n  path: string\n  content: string\n}\n\nexport interface GeneratedAPI {\n  files: GeneratedFile[]\n}\n\nexport interface FileDiff {\n  act: 'add' | 'mod' | 'del'\n  path: string\n  content?: string\n}\n"
  },
  {
    "path": "packages/lex-cli/src/util.ts",
    "content": "import fs from 'node:fs'\nimport { join } from 'node:path'\nimport chalk from 'chalk'\nimport { ZodError, type ZodFormattedError } from 'zod'\nimport { type LexiconDoc, parseLexiconDoc } from '@atproto/lexicon'\nimport { type FileDiff, type GeneratedAPI } from './types'\n\nexport function readAllLexicons(paths: string[]): LexiconDoc[] {\n  paths = [...paths].sort() // incoming path order may have come from locale-dependent shell globs\n  const docs: LexiconDoc[] = []\n  for (const path of paths) {\n    if (!path.endsWith('.json') || !fs.statSync(path).isFile()) {\n      continue\n    }\n    try {\n      docs.push(readLexicon(path))\n    } catch (e) {\n      // skip\n    }\n  }\n  return docs\n}\n\nexport function readLexicon(path: string): LexiconDoc {\n  let str: string\n  let obj: unknown\n  try {\n    str = fs.readFileSync(path, 'utf8')\n  } catch (e) {\n    console.error(`Failed to read file`, path)\n    throw e\n  }\n  try {\n    obj = JSON.parse(str)\n  } catch (e) {\n    console.error(`Failed to parse JSON in file`, path)\n    throw e\n  }\n  if (\n    obj &&\n    typeof obj === 'object' &&\n    typeof (obj as LexiconDoc).lexicon === 'number'\n  ) {\n    try {\n      return parseLexiconDoc(obj)\n    } catch (e) {\n      console.error(`Invalid lexicon`, path)\n      if (e instanceof ZodError) {\n        printZodError(e.format())\n      }\n      throw e\n    }\n  } else {\n    console.error(`Not lexicon schema`, path)\n    throw new Error(`Not lexicon schema`)\n  }\n}\n\nexport function genTsObj(lexicons: LexiconDoc[]): string {\n  return `export const lexicons = ${JSON.stringify(lexicons, null, 2)}`\n}\n\nexport function genFileDiff(outDir: string, api: GeneratedAPI) {\n  const diffs: FileDiff[] = []\n  const existingFiles = readdirRecursiveSync(outDir)\n\n  for (const file of api.files) {\n    file.path = join(outDir, file.path)\n    if (existingFiles.includes(file.path)) {\n      diffs.push({ act: 'mod', path: file.path, content: file.content })\n    } else {\n      diffs.push({ act: 'add', path: file.path, content: file.content })\n    }\n  }\n  for (const filepath of existingFiles) {\n    if (api.files.find((f) => f.path === filepath)) {\n      // do nothing\n    } else {\n      diffs.push({ act: 'del', path: filepath })\n    }\n  }\n\n  return diffs\n}\n\nexport function printFileDiff(diff: FileDiff[]) {\n  for (const d of diff) {\n    switch (d.act) {\n      case 'add':\n        console.log(`${chalk.greenBright('[+ add]')} ${d.path}`)\n        break\n      case 'mod':\n        console.log(`${chalk.yellowBright('[* mod]')} ${d.path}`)\n        break\n      case 'del':\n        console.log(`${chalk.redBright('[- del]')} ${d.path}`)\n        break\n    }\n  }\n}\n\nexport function applyFileDiff(diff: FileDiff[]) {\n  for (const d of diff) {\n    switch (d.act) {\n      case 'add':\n      case 'mod':\n        fs.mkdirSync(join(d.path, '..'), { recursive: true }) // lazy way to make sure the parent dir exists\n        fs.writeFileSync(d.path, d.content || '', 'utf8')\n        break\n      case 'del':\n        fs.unlinkSync(d.path)\n        break\n    }\n  }\n}\n\nfunction printZodError(node: ZodFormattedError<any>, path = ''): boolean {\n  if (node._errors?.length) {\n    console.log(chalk.red(`Issues at ${path}:`))\n    for (const err of dedup(node._errors)) {\n      console.log(chalk.red(` - ${err}`))\n    }\n    return true\n  } else {\n    for (const k in node) {\n      if (k === '_errors') {\n        continue\n      }\n      printZodError(node[k], `${path}/${k}`)\n    }\n  }\n  return false\n}\n\nfunction readdirRecursiveSync(root: string, files: string[] = [], prefix = '') {\n  const dir = join(root, prefix)\n  if (!fs.existsSync(dir)) return files\n  if (fs.statSync(dir).isDirectory())\n    fs.readdirSync(dir).forEach(function (name) {\n      readdirRecursiveSync(root, files, join(prefix, name))\n    })\n  else if (prefix.endsWith('.ts')) {\n    files.push(join(root, prefix))\n  }\n\n  return files\n}\n\nfunction dedup(arr: string[]): string[] {\n  return Array.from(new Set(arr))\n}\n"
  },
  {
    "path": "packages/lex-cli/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/lex-cli/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/lexicon/CHANGELOG.md",
    "content": "# @atproto/lexicon\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common-web@0.4.18\n\n## 0.6.1\n\n### Patch Changes\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Refactor uri validation to use `@atproto/syntax`\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/syntax@0.4.3\n  - @atproto/common-web@0.4.13\n\n## 0.6.0\n\n### Minor Changes\n\n- [#4416](https://github.com/bluesky-social/atproto/pull/4416) [`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Align lexicon document validation with the spec\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/common-web@0.4.7\n\n## 0.5.2\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/common-web@0.4.3\n\n## 0.5.0\n\n### Minor Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `permission-set` in lexicon documents\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/syntax@0.4.1\n\n## 0.4.14\n\n### Patch Changes\n\n- [#4122](https://github.com/bluesky-social/atproto/pull/4122) [`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - Allow unknown fields in lexicon documents\n\n## 0.4.13\n\n### Patch Changes\n\n- [#4069](https://github.com/bluesky-social/atproto/pull/4069) [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12) Thanks [@devinivy](https://github.com/devinivy)! - Lexicon document validation compatible with published lexicons.\n\n## 0.4.12\n\n### Patch Changes\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve return type of `assertValidXrpcParams`\n\n## 0.4.11\n\n### Patch Changes\n\n- [#3798](https://github.com/bluesky-social/atproto/pull/3798) [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing of `BlobRef.toJSON()`\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/common-web@0.4.2\n\n## 0.4.10\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common-web@0.4.1\n\n## 0.4.9\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n\n## 0.4.8\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/syntax@0.3.4\n\n## 0.4.7\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Various performance improvements\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fully type `ValidationResult`'s `value` property, allowing `NS.validateMyType` helper functions to return a typed value in case of success.\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/syntax@0.3.3\n\n## 0.4.6\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39)]:\n  - @atproto/common-web@0.4.0\n  - @atproto/syntax@0.3.2\n\n## 0.4.5\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/common-web@0.3.2\n\n## 0.4.4\n\n### Patch Changes\n\n- [#3223](https://github.com/bluesky-social/atproto/pull/3223) [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95) Thanks [@gaearon](https://github.com/gaearon)! - Add fast paths that skip UTF8 encoding\n\n## 0.4.3\n\n### Patch Changes\n\n- [#2911](https://github.com/bluesky-social/atproto/pull/2911) [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve validation performances by using discriminated unions where possible\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/syntax@0.3.1\n\n## 0.4.2\n\n### Patch Changes\n\n- [#2817](https://github.com/bluesky-social/atproto/pull/2817) [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94) Thanks [@gaearon](https://github.com/gaearon)! - Add fast path skipping grapheme counting\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]:\n  - @atproto/common-web@0.3.1\n\n## 0.4.1\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add the ability to instantiate a Lexicon from an iterable, and to use a Lexicon as iterable.\n\n- [#2707](https://github.com/bluesky-social/atproto/pull/2707) [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove internal circular dependency.\n\n## 0.4.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common-web@0.3.0\n  - @atproto/syntax@0.3.0\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n  - @atproto/syntax@0.2.1\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4)]:\n  - @atproto/syntax@0.2.0\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60)]:\n  - @atproto/syntax@0.1.5\n\n## 0.3.0\n\n### Minor Changes\n\n- [#1801](https://github.com/bluesky-social/atproto/pull/1801) [`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89) Thanks [@gaearon](https://github.com/gaearon)! - Methods that accepts lexicons now take `LexiconDoc` type instead of `unknown`\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/common-web@0.2.3\n  - @atproto/syntax@0.1.4\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies [[`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/common-web@0.2.2\n  - @atproto/syntax@0.1.3\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/common-web@0.2.1\n  - @atproto/syntax@0.1.2\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n"
  },
  {
    "path": "packages/lexicon/README.md",
    "content": "# @atproto/lexicon: schema validation library\n\nTypeScript implementation of the Lexicon data and API schema description language, which is part of [atproto](https://atproto.com).\n\n[![NPM](https://img.shields.io/npm/v/@atproto/lexicon)](https://www.npmjs.com/package/@atproto/lexicon)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Usage\n\n```typescript\nimport { Lexicons } from '@atproto/lexicon'\n\n// create your lexicons collection\nconst lex = new Lexicons()\n\n// add lexicon documents\nlex.add({\n  lex: 1,\n  id: 'com.example.post',\n  defs: {\n    // ...\n  }\n})\n\n// validate\nlex.assertValidRecord('com.example.record', {$type: 'com.example.record', ...})\nlex.assertValidXrpcParams('com.example.query', {...})\nlex.assertValidXrpcInput('com.example.procedure', {...})\nlex.assertValidXrpcOutput('com.example.query', {...})\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/lexicon/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Lexicon',\n  transform: { '^.+\\\\.ts$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/lexicon/package.json",
    "content": "{\n  \"name\": \"@atproto/lexicon\",\n  \"version\": \"0.6.2\",\n  \"license\": \"MIT\",\n  \"description\": \"atproto Lexicon schema language library\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lexicon\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"iso-datestring-validator\": \"^2.2.2\",\n    \"multiformats\": \"^9.9.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/lexicon/src/blob-refs.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { z } from 'zod'\nimport { check, ipldToJson, schema } from '@atproto/common-web'\n\nexport const typedJsonBlobRef = z\n  .object({\n    $type: z.literal('blob'),\n    ref: schema.cid,\n    mimeType: z.string(),\n    size: z.number(),\n  })\n  .strict()\nexport type TypedJsonBlobRef = z.infer<typeof typedJsonBlobRef>\n\nexport const untypedJsonBlobRef = z\n  .object({\n    cid: z.string(),\n    mimeType: z.string(),\n  })\n  .strict()\nexport type UntypedJsonBlobRef = z.infer<typeof untypedJsonBlobRef>\n\nexport const jsonBlobRef = z.union([typedJsonBlobRef, untypedJsonBlobRef])\nexport type JsonBlobRef = z.infer<typeof jsonBlobRef>\n\nexport class BlobRef {\n  public original: JsonBlobRef\n\n  constructor(\n    public ref: CID,\n    public mimeType: string,\n    public size: number,\n    original?: JsonBlobRef,\n  ) {\n    this.original = original ?? {\n      $type: 'blob',\n      ref,\n      mimeType,\n      size,\n    }\n  }\n\n  static asBlobRef(obj: unknown): BlobRef | null {\n    if (check.is(obj, jsonBlobRef)) {\n      return BlobRef.fromJsonRef(obj)\n    }\n    return null\n  }\n\n  static fromJsonRef(json: JsonBlobRef): BlobRef {\n    if (check.is(json, typedJsonBlobRef)) {\n      return new BlobRef(json.ref, json.mimeType, json.size)\n    } else {\n      return new BlobRef(CID.parse(json.cid), json.mimeType, -1, json)\n    }\n  }\n\n  ipld(): JsonBlobRef {\n    return this.original\n  }\n\n  toJSON() {\n    return ipldToJson(this.ipld())\n  }\n}\n"
  },
  {
    "path": "packages/lexicon/src/index.ts",
    "content": "export * from './types'\nexport * from './lexicons'\nexport * from './blob-refs'\nexport * from './serialize'\n"
  },
  {
    "path": "packages/lexicon/src/lexicons.ts",
    "content": "import {\n  InvalidLexiconError,\n  LexRecord,\n  LexUserType,\n  LexiconDefNotFoundError,\n  LexiconDoc,\n  ValidationError,\n  ValidationResult,\n  isObj,\n} from './types'\nimport { toLexUri } from './util'\nimport {\n  assertValidRecord,\n  assertValidXrpcInput,\n  assertValidXrpcMessage,\n  assertValidXrpcOutput,\n  assertValidXrpcParams,\n} from './validation'\nimport { object as validateObject } from './validators/complex'\n\n/**\n * A collection of compiled lexicons.\n */\nexport class Lexicons implements Iterable<LexiconDoc> {\n  docs: Map<string, LexiconDoc> = new Map()\n  defs: Map<string, LexUserType> = new Map()\n\n  constructor(docs?: Iterable<LexiconDoc>) {\n    if (docs) {\n      for (const doc of docs) {\n        this.add(doc)\n      }\n    }\n  }\n\n  /**\n   * @example clone a lexicon:\n   * ```ts\n   * const clone = new Lexicons(originalLexicon)\n   * ```\n   *\n   * @example get docs array:\n   * ```ts\n   * const docs = Array.from(lexicons)\n   * ```\n   */\n  [Symbol.iterator](): Iterator<LexiconDoc> {\n    return this.docs.values()\n  }\n\n  /**\n   * Add a lexicon doc.\n   */\n  add(doc: LexiconDoc): void {\n    const uri = toLexUri(doc.id)\n    if (this.docs.has(uri)) {\n      throw new Error(`${uri} has already been registered`)\n    }\n\n    // WARNING\n    // mutates the object\n    // -prf\n    resolveRefUris(doc, uri)\n\n    this.docs.set(uri, doc)\n    for (const [defUri, def] of iterDefs(doc)) {\n      this.defs.set(defUri, def)\n    }\n  }\n\n  /**\n   * Remove a lexicon doc.\n   */\n  remove(uri: string) {\n    uri = toLexUri(uri)\n    const doc = this.docs.get(uri)\n    if (!doc) {\n      throw new Error(`Unable to remove \"${uri}\": does not exist`)\n    }\n    for (const [defUri, _def] of iterDefs(doc)) {\n      this.defs.delete(defUri)\n    }\n    this.docs.delete(uri)\n  }\n\n  /**\n   * Get a lexicon doc.\n   */\n  get(uri: string): LexiconDoc | undefined {\n    uri = toLexUri(uri)\n    return this.docs.get(uri)\n  }\n\n  /**\n   * Get a definition.\n   */\n  getDef(uri: string): LexUserType | undefined {\n    uri = toLexUri(uri)\n    return this.defs.get(uri)\n  }\n\n  /**\n   * Get a def, throw if not found. Throws on not found.\n   */\n  getDefOrThrow<T extends LexUserType['type'] = LexUserType['type']>(\n    uri: string,\n    types?: readonly T[],\n  ): Extract<LexUserType, { type: T }>\n  getDefOrThrow(\n    uri: string,\n    types?: readonly LexUserType['type'][],\n  ): LexUserType {\n    const def = this.getDef(uri)\n    if (!def) {\n      throw new LexiconDefNotFoundError(`Lexicon not found: ${uri}`)\n    }\n    if (types && !types.includes(def.type)) {\n      throw new InvalidLexiconError(\n        `Not a ${types.join(' or ')} lexicon: ${uri}`,\n      )\n    }\n    return def\n  }\n\n  /**\n   * Validate a record or object.\n   */\n  validate(lexUri: string, value: unknown): ValidationResult {\n    if (!isObj(value)) {\n      throw new ValidationError(`Value must be an object`)\n    }\n\n    const lexUriNormalized = toLexUri(lexUri)\n    const def = this.getDefOrThrow(lexUriNormalized, ['record', 'object'])\n\n    if (def.type === 'record') {\n      return validateObject(this, 'Record', def.record, value)\n    } else if (def.type === 'object') {\n      return validateObject(this, 'Object', def, value)\n    } else {\n      // shouldn't happen\n      throw new InvalidLexiconError('Definition must be a record or object')\n    }\n  }\n\n  /**\n   * Validate a record and throw on any error.\n   */\n  assertValidRecord(lexUri: string, value: unknown) {\n    if (!isObj(value)) {\n      throw new ValidationError(`Record must be an object`)\n    }\n    if (!('$type' in value)) {\n      throw new ValidationError(`Record/$type must be a string`)\n    }\n    const { $type } = value\n    if (typeof $type !== 'string') {\n      throw new ValidationError(`Record/$type must be a string`)\n    }\n\n    const lexUriNormalized = toLexUri(lexUri)\n    if (toLexUri($type) !== lexUriNormalized) {\n      throw new ValidationError(\n        `Invalid $type: must be ${lexUriNormalized}, got ${$type}`,\n      )\n    }\n\n    const def = this.getDefOrThrow(lexUriNormalized, ['record'])\n    return assertValidRecord(this, def as LexRecord, value)\n  }\n\n  /**\n   * Validate xrpc query params and throw on any error.\n   */\n  assertValidXrpcParams(lexUri: string, value: unknown) {\n    lexUri = toLexUri(lexUri)\n    const def = this.getDefOrThrow(lexUri, [\n      'query',\n      'procedure',\n      'subscription',\n    ])\n    return assertValidXrpcParams(this, def, value)\n  }\n\n  /**\n   * Validate xrpc input body and throw on any error.\n   */\n  assertValidXrpcInput(lexUri: string, value: unknown) {\n    lexUri = toLexUri(lexUri)\n    const def = this.getDefOrThrow(lexUri, ['procedure'])\n    return assertValidXrpcInput(this, def, value)\n  }\n\n  /**\n   * Validate xrpc output body and throw on any error.\n   */\n  assertValidXrpcOutput(lexUri: string, value: unknown) {\n    lexUri = toLexUri(lexUri)\n    const def = this.getDefOrThrow(lexUri, ['query', 'procedure'])\n    return assertValidXrpcOutput(this, def, value)\n  }\n\n  /**\n   * Validate xrpc subscription message and throw on any error.\n   */\n  assertValidXrpcMessage<T = unknown>(lexUri: string, value: unknown): T {\n    lexUri = toLexUri(lexUri)\n    const def = this.getDefOrThrow(lexUri, ['subscription'])\n    return assertValidXrpcMessage(this, def, value) as T\n  }\n\n  /**\n   * Resolve a lex uri given a ref\n   */\n  resolveLexUri(lexUri: string, ref: string) {\n    lexUri = toLexUri(lexUri)\n    return toLexUri(ref, lexUri)\n  }\n}\n\nfunction* iterDefs(doc: LexiconDoc): Generator<[string, LexUserType]> {\n  for (const defId in doc.defs) {\n    yield [`lex:${doc.id}#${defId}`, doc.defs[defId]]\n    if (defId === 'main') {\n      yield [`lex:${doc.id}`, doc.defs[defId]]\n    }\n  }\n}\n\n// WARNING\n// this method mutates objects\n// -prf\nfunction resolveRefUris(obj: any, baseUri: string): any {\n  for (const k in obj) {\n    if (obj.type === 'ref') {\n      obj.ref = toLexUri(obj.ref, baseUri)\n    } else if (obj.type === 'union') {\n      obj.refs = obj.refs.map((ref) => toLexUri(ref, baseUri))\n    } else if (Array.isArray(obj[k])) {\n      obj[k] = obj[k].map((item: any) => {\n        if (typeof item === 'string') {\n          return item.startsWith('#') ? toLexUri(item, baseUri) : item\n        } else if (item && typeof item === 'object') {\n          return resolveRefUris(item, baseUri)\n        }\n        return item\n      })\n    } else if (obj[k] && typeof obj[k] === 'object') {\n      obj[k] = resolveRefUris(obj[k], baseUri)\n    }\n  }\n  return obj\n}\n"
  },
  {
    "path": "packages/lexicon/src/serialize.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport {\n  IpldValue,\n  JsonValue,\n  check,\n  ipldToJson,\n  jsonToIpld,\n} from '@atproto/common-web'\nimport { BlobRef, jsonBlobRef } from './blob-refs'\n\n/**\n * @note this is equivalent to `unknown` because of {@link IpldValue} being `unknown`.\n * @deprecated Use {@link Lex} from `@atproto/lex-data` instead.\n */\nexport type LexValue = unknown\n\n/**\n * @deprecated Use {@link TypedLexMap} from `@atproto/lex-data` instead.\n */\nexport type RepoRecord = Record<string, LexValue>\n\n// @NOTE avoiding use of check.is() here only because it makes\n// these implementations slow, and they often live in hot paths.\n\n/**\n * @deprecated Use `LexValue` from `@atproto/lex-data` instead (which doesn't need conversion to IPLD).\n */\nexport const lexToIpld = (val: LexValue): IpldValue => {\n  // walk arrays\n  if (Array.isArray(val)) {\n    return val.map((item) => lexToIpld(item))\n  }\n  // objects\n  if (val && typeof val === 'object') {\n    // convert blobs, leaving the original encoding so that we don't change CIDs on re-encode\n    if (val instanceof BlobRef) {\n      return val.original\n    }\n    // retain cids & bytes\n    if (CID.asCID(val) || val instanceof Uint8Array) {\n      return val\n    }\n    // walk plain objects\n    const toReturn = {}\n    for (const key of Object.keys(val)) {\n      toReturn[key] = lexToIpld(val[key])\n    }\n    return toReturn\n  }\n  // pass through\n  return val\n}\n\n/**\n * @deprecated Use `LexValue` from `@atproto/lex-data` instead instead (which doesn't need conversion to IPLD).\n */\nexport const ipldToLex = (val: IpldValue): LexValue => {\n  // map arrays\n  if (Array.isArray(val)) {\n    return val.map((item) => ipldToLex(item))\n  }\n  // objects\n  if (val && typeof val === 'object') {\n    // convert blobs, using hints to avoid expensive is() check\n    if (\n      (val['$type'] === 'blob' ||\n        (typeof val['cid'] === 'string' &&\n          typeof val['mimeType'] === 'string')) &&\n      check.is(val, jsonBlobRef)\n    ) {\n      return BlobRef.fromJsonRef(val)\n    }\n    // retain cids, bytes\n    if (CID.asCID(val) || val instanceof Uint8Array) {\n      return val\n    }\n    // map plain objects\n    const toReturn = {}\n    for (const key of Object.keys(val)) {\n      toReturn[key] = ipldToLex(val[key])\n    }\n    return toReturn\n  }\n  // pass through\n  return val\n}\n\nexport const lexToJson = (val: LexValue): JsonValue => {\n  return ipldToJson(lexToIpld(val))\n}\n\nexport const stringifyLex = (val: LexValue): string => {\n  return JSON.stringify(lexToJson(val))\n}\n\nexport const jsonToLex = (val: JsonValue): LexValue => {\n  return ipldToLex(jsonToIpld(val))\n}\n\nexport const jsonStringToLex = (val: string): LexValue => {\n  return jsonToLex(JSON.parse(val))\n}\n"
  },
  {
    "path": "packages/lexicon/src/types.ts",
    "content": "import { z } from 'zod'\nimport { validateLanguage } from '@atproto/common-web'\nimport { isValidNsid } from '@atproto/syntax'\nimport { requiredPropertiesRefinement } from './util'\n\nexport const languageSchema = z\n  .string()\n  .refine(validateLanguage, 'Invalid BCP47 language tag')\n\nexport const lexLang = z.record(languageSchema, z.string().optional())\n\nexport type LexLang = z.infer<typeof lexLang>\n\n// primitives\n// =\n\nexport const lexBoolean = z.object({\n  type: z.literal('boolean'),\n  description: z.string().optional(),\n  default: z.boolean().optional(),\n  const: z.boolean().optional(),\n})\nexport type LexBoolean = z.infer<typeof lexBoolean>\n\nexport const lexInteger = z.object({\n  type: z.literal('integer'),\n  description: z.string().optional(),\n  default: z.number().int().optional(),\n  minimum: z.number().int().optional(),\n  maximum: z.number().int().optional(),\n  enum: z.number().int().array().optional(),\n  const: z.number().int().optional(),\n})\nexport type LexInteger = z.infer<typeof lexInteger>\n\nexport const lexStringFormat = z.enum([\n  'datetime',\n  'uri',\n  'at-uri',\n  'did',\n  'handle',\n  'at-identifier',\n  'nsid',\n  'cid',\n  'language',\n  'tid',\n  'record-key',\n])\nexport type LexStringFormat = z.infer<typeof lexStringFormat>\n\nexport const lexString = z.object({\n  type: z.literal('string'),\n  format: lexStringFormat.optional(),\n  description: z.string().optional(),\n  default: z.string().optional(),\n  minLength: z.number().int().optional(),\n  maxLength: z.number().int().optional(),\n  minGraphemes: z.number().int().optional(),\n  maxGraphemes: z.number().int().optional(),\n  enum: z.string().array().optional(),\n  const: z.string().optional(),\n  knownValues: z.string().array().optional(),\n})\nexport type LexString = z.infer<typeof lexString>\n\nexport const lexUnknown = z.object({\n  type: z.literal('unknown'),\n  description: z.string().optional(),\n})\nexport type LexUnknown = z.infer<typeof lexUnknown>\n\nexport const lexPrimitive = z.discriminatedUnion('type', [\n  lexBoolean,\n  lexInteger,\n  lexString,\n  lexUnknown,\n])\nexport type LexPrimitive = z.infer<typeof lexPrimitive>\n\n// ipld types\n// =\n\nexport const lexBytes = z.object({\n  type: z.literal('bytes'),\n  description: z.string().optional(),\n  maxLength: z.number().optional(),\n  minLength: z.number().optional(),\n})\nexport type LexBytes = z.infer<typeof lexBytes>\n\nexport const lexCidLink = z.object({\n  type: z.literal('cid-link'),\n  description: z.string().optional(),\n})\nexport type LexCidLink = z.infer<typeof lexCidLink>\n\nexport const lexIpldType = z.discriminatedUnion('type', [lexBytes, lexCidLink])\nexport type LexIpldType = z.infer<typeof lexIpldType>\n\n// references\n// =\n\nexport const lexRef = z.object({\n  type: z.literal('ref'),\n  description: z.string().optional(),\n  ref: z.string(),\n})\nexport type LexRef = z.infer<typeof lexRef>\n\nexport const lexRefUnion = z.object({\n  type: z.literal('union'),\n  description: z.string().optional(),\n  refs: z.string().array(),\n  closed: z.boolean().optional(),\n})\nexport type LexRefUnion = z.infer<typeof lexRefUnion>\n\nexport const lexRefVariant = z.discriminatedUnion('type', [lexRef, lexRefUnion])\nexport type LexRefVariant = z.infer<typeof lexRefVariant>\n\n// blobs\n// =\n\nexport const lexBlob = z.object({\n  type: z.literal('blob'),\n  description: z.string().optional(),\n  accept: z.string().array().optional(),\n  maxSize: z.number().optional(),\n})\nexport type LexBlob = z.infer<typeof lexBlob>\n\n// complex types\n// =\n\nexport const lexArray = z.object({\n  type: z.literal('array'),\n  description: z.string().optional(),\n  items: z.discriminatedUnion('type', [\n    // lexPrimitive\n    lexBoolean,\n    lexInteger,\n    lexString,\n    lexUnknown,\n    // lexIpldType\n    lexBytes,\n    lexCidLink,\n    // lexRefVariant\n    lexRef,\n    lexRefUnion,\n    // other\n    lexBlob,\n  ]),\n  minLength: z.number().int().optional(),\n  maxLength: z.number().int().optional(),\n})\nexport type LexArray = z.infer<typeof lexArray>\n\nexport const lexPrimitiveArray = lexArray.merge(\n  z.object({\n    items: lexPrimitive,\n  }),\n)\nexport type LexPrimitiveArray = z.infer<typeof lexPrimitiveArray>\n\nexport const lexToken = z.object({\n  type: z.literal('token'),\n  description: z.string().optional(),\n})\nexport type LexToken = z.infer<typeof lexToken>\n\nexport const lexObject = z\n  .object({\n    type: z.literal('object'),\n    description: z.string().optional(),\n    required: z.string().array().optional(),\n    nullable: z.string().array().optional(),\n    properties: z.record(\n      z.string(),\n      z.discriminatedUnion('type', [\n        lexArray,\n\n        // lexPrimitive\n        lexBoolean,\n        lexInteger,\n        lexString,\n        lexUnknown,\n        // lexIpldType\n        lexBytes,\n        lexCidLink,\n        // lexRefVariant\n        lexRef,\n        lexRefUnion,\n        // other\n        lexBlob,\n      ]),\n    ),\n  })\n  .superRefine(requiredPropertiesRefinement)\nexport type LexObject = z.infer<typeof lexObject>\n\n// permissions\n// =\n\nconst lexPermission = z.intersection(\n  z.object({\n    type: z.literal('permission'),\n    resource: z.string().nonempty(),\n  }),\n  z.record(\n    z.string(),\n    z\n      .union([\n        z.array(z.union([z.string(), z.number().int(), z.boolean()])),\n\n        z.boolean(),\n        z.number().int(),\n        z.string(),\n      ])\n      .optional(),\n  ),\n)\n\nexport type LexPermission = z.infer<typeof lexPermission>\n\nexport const lexPermissionSet = z.object({\n  type: z.literal('permission-set'),\n  description: z.string().optional(),\n  title: z.string().optional(),\n  'title:lang': lexLang.optional(),\n  detail: z.string().optional(),\n  'detail:lang': lexLang.optional(),\n  permissions: z.array(lexPermission),\n})\n\nexport type LexPermissionSet = z.infer<typeof lexPermissionSet>\n\n// xrpc\n// =\n\nexport const lexXrpcParameters = z\n  .object({\n    type: z.literal('params'),\n    description: z.string().optional(),\n    required: z.string().array().optional(),\n    properties: z.record(\n      z.string(),\n      z.discriminatedUnion('type', [\n        lexPrimitiveArray,\n\n        // lexPrimitive\n        lexBoolean,\n        lexInteger,\n        lexString,\n        lexUnknown,\n      ]),\n    ),\n  })\n  .superRefine(requiredPropertiesRefinement)\nexport type LexXrpcParameters = z.infer<typeof lexXrpcParameters>\n\nexport const lexXrpcBody = z.object({\n  description: z.string().optional(),\n  encoding: z.string(),\n  // @NOTE using discriminatedUnion with a refined schema requires zod >= 4\n  schema: z.union([lexRefVariant, lexObject]).optional(),\n})\nexport type LexXrpcBody = z.infer<typeof lexXrpcBody>\n\nexport const lexXrpcError = z.object({\n  name: z.string(),\n  description: z.string().optional(),\n})\nexport type LexXrpcError = z.infer<typeof lexXrpcError>\n\nexport const lexXrpcQuery = z.object({\n  type: z.literal('query'),\n  description: z.string().optional(),\n  parameters: lexXrpcParameters.optional(),\n  output: lexXrpcBody.optional(),\n  errors: lexXrpcError.array().optional(),\n})\nexport type LexXrpcQuery = z.infer<typeof lexXrpcQuery>\n\nexport const lexXrpcProcedure = z.object({\n  type: z.literal('procedure'),\n  description: z.string().optional(),\n  parameters: lexXrpcParameters.optional(),\n  input: lexXrpcBody.optional(),\n  output: lexXrpcBody.optional(),\n  errors: lexXrpcError.array().optional(),\n})\nexport type LexXrpcProcedure = z.infer<typeof lexXrpcProcedure>\n\nexport const lexXrpcSubscription = z.object({\n  type: z.literal('subscription'),\n  description: z.string().optional(),\n  parameters: lexXrpcParameters.optional(),\n  message: z.object({\n    description: z.string().optional(),\n    schema: lexRefUnion,\n  }),\n  errors: lexXrpcError.array().optional(),\n})\nexport type LexXrpcSubscription = z.infer<typeof lexXrpcSubscription>\n\n// database\n// =\n\nexport const lexRecord = z.object({\n  type: z.literal('record'),\n  description: z.string().optional(),\n  key: z.string().optional(),\n  record: lexObject,\n})\nexport type LexRecord = z.infer<typeof lexRecord>\n\n// core\n// =\n\n// We need to use `z.custom` here because\n// lexXrpcProperty and lexObject are refined\n// `z.union` would work, but it's too slow\n// see #915 for details\nexport const lexUserType = z.custom<\n  | LexRecord\n  | LexPermissionSet\n  | LexXrpcQuery\n  | LexXrpcProcedure\n  | LexXrpcSubscription\n  | LexBlob\n  | LexArray\n  | LexToken\n  | LexObject\n  | LexBoolean\n  | LexInteger\n  | LexString\n  | LexBytes\n  | LexCidLink\n  | LexUnknown\n>(\n  (val) => {\n    if (!val || typeof val !== 'object') {\n      return\n    }\n\n    if (val['type'] === undefined) {\n      return\n    }\n\n    switch (val['type']) {\n      case 'record':\n        return lexRecord.parse(val)\n\n      case 'permission-set':\n        return lexPermissionSet.parse(val)\n\n      case 'query':\n        return lexXrpcQuery.parse(val)\n      case 'procedure':\n        return lexXrpcProcedure.parse(val)\n      case 'subscription':\n        return lexXrpcSubscription.parse(val)\n\n      case 'blob':\n        return lexBlob.parse(val)\n\n      case 'array':\n        return lexArray.parse(val)\n      case 'token':\n        return lexToken.parse(val)\n      case 'object':\n        return lexObject.parse(val)\n\n      case 'boolean':\n        return lexBoolean.parse(val)\n      case 'integer':\n        return lexInteger.parse(val)\n      case 'string':\n        return lexString.parse(val)\n      case 'bytes':\n        return lexBytes.parse(val)\n      case 'cid-link':\n        return lexCidLink.parse(val)\n      case 'unknown':\n        return lexUnknown.parse(val)\n    }\n  },\n  (val) => {\n    if (!val || typeof val !== 'object') {\n      return {\n        message: 'Must be an object',\n        fatal: true,\n      }\n    }\n\n    if (val['type'] === undefined) {\n      return {\n        message: 'Must have a type',\n        fatal: true,\n      }\n    }\n\n    if (typeof val['type'] !== 'string') {\n      return {\n        message: 'Type property must be a string',\n        fatal: true,\n      }\n    }\n\n    return {\n      message: `Invalid type: ${val['type']} must be one of: record, query, procedure, subscription, blob, array, token, object, boolean, integer, string, bytes, cid-link, unknown`,\n      fatal: true,\n    }\n  },\n)\nexport type LexUserType = z.infer<typeof lexUserType>\n\nexport const lexiconDoc = z\n  .object({\n    lexicon: z.literal(1),\n    id: z.string().refine(isValidNsid, {\n      message: 'Must be a valid NSID',\n    }),\n    revision: z.number().optional(),\n    description: z.string().optional(),\n    defs: z.record(z.string(), lexUserType),\n  })\n  .refine(\n    (doc) => {\n      for (const [defId, def] of Object.entries(doc.defs)) {\n        if (\n          defId !== 'main' &&\n          (def.type === 'record' ||\n            def.type === 'permission-set' ||\n            def.type === 'procedure' ||\n            def.type === 'query' ||\n            def.type === 'subscription')\n        ) {\n          return false\n        }\n      }\n      return true\n    },\n    {\n      message: `Records, permission sets, procedures, queries, and subscriptions must be the main definition.`,\n    },\n  )\nexport type LexiconDoc = z.infer<typeof lexiconDoc>\n\n// helpers\n// =\n\nexport function isValidLexiconDoc(v: unknown): v is LexiconDoc {\n  return lexiconDoc.safeParse(v).success\n}\n\nexport function isObj<V>(v: V): v is V & object {\n  return v != null && typeof v === 'object'\n}\n\nexport type DiscriminatedObject = { $type: string }\nexport function isDiscriminatedObject(v: unknown): v is DiscriminatedObject {\n  return isObj(v) && '$type' in v && typeof v.$type === 'string'\n}\n\nexport function parseLexiconDoc(v: unknown): LexiconDoc {\n  lexiconDoc.parse(v)\n  return v as LexiconDoc\n}\n\nexport type ValidationResult<V = unknown> =\n  | {\n      success: true\n      value: V\n    }\n  | {\n      success: false\n      error: ValidationError\n    }\n\nexport class ValidationError extends Error {}\nexport class InvalidLexiconError extends Error {}\nexport class LexiconDefNotFoundError extends Error {}\n"
  },
  {
    "path": "packages/lexicon/src/util.ts",
    "content": "import { z } from 'zod'\n\nexport function toLexUri(str: string, baseUri?: string): string {\n  if (str.split('#').length > 2) {\n    throw new Error('Uri can only have one hash segment')\n  }\n\n  if (str.startsWith('lex:')) {\n    return str\n  }\n  if (str.startsWith('#')) {\n    if (!baseUri) {\n      throw new Error(`Unable to resolve uri without anchor: ${str}`)\n    }\n    return `${baseUri}${str}`\n  }\n  return `lex:${str}`\n}\n\nexport function requiredPropertiesRefinement<\n  ObjectType extends {\n    required?: string[]\n    properties?: Record<string, unknown>\n  },\n>(object: ObjectType, ctx: z.RefinementCtx) {\n  // Required fields check\n  if (object.required === undefined) {\n    return\n  }\n\n  if (!Array.isArray(object.required)) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.invalid_type,\n      received: typeof object.required,\n      expected: 'array',\n    })\n    return\n  }\n\n  if (object.properties === undefined) {\n    if (object.required.length > 0) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: `Required fields defined but no properties defined`,\n      })\n    }\n    return\n  }\n\n  for (const field of object.required) {\n    if (object.properties[field] === undefined) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: `Required field \"${field}\" not defined`,\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/lexicon/src/validation.ts",
    "content": "import { Lexicons } from './lexicons'\nimport {\n  LexRecord,\n  LexRefVariant,\n  LexUserType,\n  LexXrpcProcedure,\n  LexXrpcQuery,\n  LexXrpcSubscription,\n} from './types'\nimport { object, validateOneOf } from './validators/complex'\nimport { params } from './validators/xrpc'\n\nexport function assertValidRecord(\n  lexicons: Lexicons,\n  def: LexRecord,\n  value: unknown,\n) {\n  const res = object(lexicons, 'Record', def.record, value)\n  if (!res.success) throw res.error\n  return res.value\n}\n\nexport function assertValidXrpcParams(\n  lexicons: Lexicons,\n  def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription,\n  value: unknown,\n) {\n  if (def.parameters) {\n    const res = params(lexicons, 'Params', def.parameters, value)\n    if (!res.success) throw res.error\n    return res.value\n  }\n}\n\nexport function assertValidXrpcInput(\n  lexicons: Lexicons,\n  def: LexXrpcProcedure,\n  value: unknown,\n) {\n  if (def.input?.schema) {\n    // loop: all input schema definitions\n    return assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)\n  }\n}\n\nexport function assertValidXrpcOutput(\n  lexicons: Lexicons,\n  def: LexXrpcProcedure | LexXrpcQuery,\n  value: unknown,\n) {\n  if (def.output?.schema) {\n    // loop: all output schema definitions\n    return assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)\n  }\n}\n\nexport function assertValidXrpcMessage(\n  lexicons: Lexicons,\n  def: LexXrpcSubscription,\n  value: unknown,\n) {\n  if (def.message?.schema) {\n    // loop: all output schema definitions\n    return assertValidOneOf(\n      lexicons,\n      'Message',\n      def.message.schema,\n      value,\n      true,\n    )\n  }\n}\n\nfunction assertValidOneOf(\n  lexicons: Lexicons,\n  path: string,\n  def: LexRefVariant | LexUserType,\n  value: unknown,\n  mustBeObj = false,\n) {\n  const res = validateOneOf(lexicons, path, def, value, mustBeObj)\n  if (!res.success) throw res.error\n  return res.value\n}\n"
  },
  {
    "path": "packages/lexicon/src/validators/blob.ts",
    "content": "import { BlobRef } from '../blob-refs'\nimport { Lexicons } from '../lexicons'\nimport { LexUserType, ValidationError, ValidationResult } from '../types'\n\nexport function blob(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  // check\n  if (!value || !(value instanceof BlobRef)) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} should be a blob ref`),\n    }\n  }\n  return { success: true, value }\n}\n"
  },
  {
    "path": "packages/lexicon/src/validators/complex.ts",
    "content": "import { Lexicons } from '../lexicons'\nimport {\n  LexArray,\n  LexRefVariant,\n  LexUserType,\n  ValidationError,\n  ValidationResult,\n  isDiscriminatedObject,\n  isObj,\n} from '../types'\nimport { toLexUri } from '../util'\nimport { blob } from './blob'\nimport { validate as validatePrimitive } from './primitives'\n\nexport function validate(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  switch (def.type) {\n    case 'object':\n      return object(lexicons, path, def, value)\n    case 'array':\n      return array(lexicons, path, def, value)\n    case 'blob':\n      return blob(lexicons, path, def, value)\n    default:\n      return validatePrimitive(lexicons, path, def, value)\n  }\n}\n\nexport function array(\n  lexicons: Lexicons,\n  path: string,\n  def: LexArray,\n  value: unknown,\n): ValidationResult {\n  // type\n  if (!Array.isArray(value)) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be an array`),\n    }\n  }\n\n  // maxLength\n  if (typeof def.maxLength === 'number') {\n    if ((value as Array<unknown>).length > def.maxLength) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must not have more than ${def.maxLength} elements`,\n        ),\n      }\n    }\n  }\n\n  // minLength\n  if (typeof def.minLength === 'number') {\n    if ((value as Array<unknown>).length < def.minLength) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must not have fewer than ${def.minLength} elements`,\n        ),\n      }\n    }\n  }\n\n  // items\n  const itemsDef = def.items\n  for (let i = 0; i < (value as Array<unknown>).length; i++) {\n    const itemValue = value[i]\n    const itemPath = `${path}/${i}`\n    const res = validateOneOf(lexicons, itemPath, itemsDef, itemValue)\n    if (!res.success) {\n      return res\n    }\n  }\n\n  return { success: true, value }\n}\n\nexport function object(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  // type\n  if (!isObj(value)) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be an object`),\n    }\n  }\n\n  // properties\n  let resultValue = value\n  if ('properties' in def && def.properties != null) {\n    for (const key in def.properties) {\n      const keyValue = value[key]\n      if (keyValue === null && def.nullable?.includes(key)) {\n        continue\n      }\n      const propDef = def.properties[key]\n      if (keyValue === undefined && !def.required?.includes(key)) {\n        // Fast path for non-required undefined props.\n        if (\n          propDef.type === 'integer' ||\n          propDef.type === 'boolean' ||\n          propDef.type === 'string'\n        ) {\n          if (propDef.default === undefined) {\n            continue\n          }\n        } else {\n          // Other types have no defaults.\n          continue\n        }\n      }\n      const propPath = `${path}/${key}`\n      const validated = validateOneOf(lexicons, propPath, propDef, keyValue)\n      const propValue = validated.success ? validated.value : keyValue\n\n      // Return error for bad validation, giving required rule precedence\n      if (propValue === undefined) {\n        if (def.required?.includes(key)) {\n          return {\n            success: false,\n            error: new ValidationError(\n              `${path} must have the property \"${key}\"`,\n            ),\n          }\n        }\n      } else {\n        if (!validated.success) {\n          return validated\n        }\n      }\n\n      // Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value\n      if (propValue !== keyValue) {\n        if (resultValue === value) {\n          // Lazy shallow clone\n          resultValue = { ...value }\n        }\n        resultValue[key] = propValue\n      }\n    }\n  }\n\n  return { success: true, value: resultValue }\n}\n\nexport function validateOneOf(\n  lexicons: Lexicons,\n  path: string,\n  def: LexRefVariant | LexUserType,\n  value: unknown,\n  mustBeObj = false, // this is the only type constraint we need currently (used by xrpc body schema validators)\n): ValidationResult {\n  let concreteDef: LexUserType\n\n  if (def.type === 'union') {\n    if (!isDiscriminatedObject(value)) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must be an object which includes the \"$type\" property`,\n        ),\n      }\n    }\n    if (!refsContainType(def.refs, value.$type)) {\n      if (def.closed) {\n        return {\n          success: false,\n          error: new ValidationError(\n            `${path} $type must be one of ${def.refs.join(', ')}`,\n          ),\n        }\n      }\n      return { success: true, value }\n    } else {\n      concreteDef = lexicons.getDefOrThrow(value.$type)\n    }\n  } else if (def.type === 'ref') {\n    concreteDef = lexicons.getDefOrThrow(def.ref)\n  } else {\n    concreteDef = def\n  }\n\n  return mustBeObj\n    ? object(lexicons, path, concreteDef, value)\n    : validate(lexicons, path, concreteDef, value)\n}\n\n// to avoid bugs like #0189 this needs to handle both\n// explicit and implicit #main\nconst refsContainType = (refs: string[], type: string) => {\n  const lexUri = toLexUri(type)\n  if (refs.includes(lexUri)) {\n    return true\n  }\n\n  if (lexUri.endsWith('#main')) {\n    return refs.includes(lexUri.slice(0, -5))\n  } else {\n    return !lexUri.includes('#') && refs.includes(`${lexUri}#main`)\n  }\n}\n"
  },
  {
    "path": "packages/lexicon/src/validators/formats.ts",
    "content": "import { isValidISODateString } from 'iso-datestring-validator'\nimport { CID } from 'multiformats/cid'\nimport { validateLanguage } from '@atproto/common-web'\nimport {\n  ensureValidAtUri,\n  ensureValidDid,\n  ensureValidHandle,\n  ensureValidRecordKey,\n  isValidNsid,\n  isValidTid,\n  isValidUri,\n} from '@atproto/syntax'\nimport { ValidationError, ValidationResult } from '../types'\n\nexport function datetime(path: string, value: string): ValidationResult {\n  try {\n    if (!isValidISODateString(value)) {\n      throw new Error()\n    }\n  } catch {\n    return {\n      success: false,\n      error: new ValidationError(\n        `${path} must be an valid atproto datetime (both RFC-3339 and ISO-8601)`,\n      ),\n    }\n  }\n  return { success: true, value }\n}\n\nexport function uri(path: string, value: string): ValidationResult {\n  if (!isValidUri(value)) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a uri`),\n    }\n  }\n  return { success: true, value }\n}\n\nexport function atUri(path: string, value: string): ValidationResult {\n  try {\n    ensureValidAtUri(value)\n  } catch {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a valid at-uri`),\n    }\n  }\n\n  return { success: true, value }\n}\n\nexport function did(path: string, value: string): ValidationResult {\n  try {\n    ensureValidDid(value)\n  } catch {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a valid did`),\n    }\n  }\n\n  return { success: true, value }\n}\n\nexport function handle(path: string, value: string): ValidationResult {\n  try {\n    ensureValidHandle(value)\n  } catch {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a valid handle`),\n    }\n  }\n\n  return { success: true, value }\n}\n\nexport function atIdentifier(path: string, value: string): ValidationResult {\n  // We can discriminate based on the \"did:\" prefix\n  if (value.startsWith('did:')) {\n    const didResult = did(path, value)\n    if (didResult.success) return didResult\n  } else {\n    const handleResult = handle(path, value)\n    if (handleResult.success) return handleResult\n  }\n\n  return {\n    success: false,\n    error: new ValidationError(`${path} must be a valid did or a handle`),\n  }\n}\n\nexport function nsid(path: string, value: string): ValidationResult {\n  if (isValidNsid(value)) {\n    return {\n      success: true,\n      value,\n    }\n  } else {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a valid nsid`),\n    }\n  }\n}\n\nexport function cid(path: string, value: string): ValidationResult {\n  try {\n    CID.parse(value)\n  } catch {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a cid string`),\n    }\n  }\n  return { success: true, value }\n}\n\n// The language format validates well-formed BCP 47 language tags: https://www.rfc-editor.org/info/bcp47\nexport function language(path: string, value: string): ValidationResult {\n  if (validateLanguage(value)) {\n    return { success: true, value }\n  }\n  return {\n    success: false,\n    error: new ValidationError(\n      `${path} must be a well-formed BCP 47 language tag`,\n    ),\n  }\n}\n\nexport function tid(path: string, value: string): ValidationResult {\n  if (isValidTid(value)) {\n    return { success: true, value }\n  }\n\n  return {\n    success: false,\n    error: new ValidationError(`${path} must be a valid TID`),\n  }\n}\n\nexport function recordKey(path: string, value: string): ValidationResult {\n  try {\n    ensureValidRecordKey(value)\n  } catch {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a valid Record Key`),\n    }\n  }\n  return { success: true, value }\n}\n"
  },
  {
    "path": "packages/lexicon/src/validators/primitives.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { graphemeLen, utf8Len } from '@atproto/common-web'\nimport { Lexicons } from '../lexicons'\nimport {\n  LexBoolean,\n  LexBytes,\n  LexInteger,\n  LexString,\n  LexUserType,\n  ValidationError,\n  ValidationResult,\n} from '../types'\nimport * as formats from './formats'\n\nexport function validate(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  switch (def.type) {\n    case 'boolean':\n      return boolean(lexicons, path, def, value)\n    case 'integer':\n      return integer(lexicons, path, def, value)\n    case 'string':\n      return string(lexicons, path, def, value)\n    case 'bytes':\n      return bytes(lexicons, path, def, value)\n    case 'cid-link':\n      return cidLink(lexicons, path, def, value)\n    case 'unknown':\n      return unknown(lexicons, path, def, value)\n    default:\n      return {\n        success: false,\n        error: new ValidationError(`Unexpected lexicon type: ${def.type}`),\n      }\n  }\n}\n\nfunction boolean(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  def = def as LexBoolean\n\n  // type\n  const type = typeof value\n  if (type === 'undefined') {\n    if (typeof def.default === 'boolean') {\n      return { success: true, value: def.default }\n    }\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a boolean`),\n    }\n  } else if (type !== 'boolean') {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a boolean`),\n    }\n  }\n\n  // const\n  if (typeof def.const === 'boolean') {\n    if (value !== def.const) {\n      return {\n        success: false,\n        error: new ValidationError(`${path} must be ${def.const}`),\n      }\n    }\n  }\n\n  return { success: true, value }\n}\n\nfunction integer(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  def = def as LexInteger\n\n  // type\n  const type = typeof value\n  if (type === 'undefined') {\n    if (typeof def.default === 'number') {\n      return { success: true, value: def.default }\n    }\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be an integer`),\n    }\n  } else if (!Number.isInteger(value)) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be an integer`),\n    }\n  }\n\n  // const\n  if (typeof def.const === 'number') {\n    if (value !== def.const) {\n      return {\n        success: false,\n        error: new ValidationError(`${path} must be ${def.const}`),\n      }\n    }\n  }\n\n  // enum\n  if (Array.isArray(def.enum)) {\n    if (!def.enum.includes(value as number)) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must be one of (${def.enum.join('|')})`,\n        ),\n      }\n    }\n  }\n\n  // maximum\n  if (typeof def.maximum === 'number') {\n    if ((value as number) > def.maximum) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} can not be greater than ${def.maximum}`,\n        ),\n      }\n    }\n  }\n\n  // minimum\n  if (typeof def.minimum === 'number') {\n    if ((value as number) < def.minimum) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} can not be less than ${def.minimum}`,\n        ),\n      }\n    }\n  }\n\n  return { success: true, value }\n}\n\nfunction string(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  def = def as LexString\n\n  // type\n  if (typeof value === 'undefined') {\n    if (typeof def.default === 'string') {\n      return { success: true, value: def.default }\n    }\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a string`),\n    }\n  } else if (typeof value !== 'string') {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a string`),\n    }\n  }\n\n  // const\n  if (typeof def.const === 'string') {\n    if (value !== def.const) {\n      return {\n        success: false,\n        error: new ValidationError(`${path} must be ${def.const}`),\n      }\n    }\n  }\n\n  // enum\n  if (Array.isArray(def.enum)) {\n    if (!def.enum.includes(value as string)) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must be one of (${def.enum.join('|')})`,\n        ),\n      }\n    }\n  }\n\n  // maxLength and minLength\n  if (typeof def.minLength === 'number' || typeof def.maxLength === 'number') {\n    // If the JavaScript string length * 3 is below the maximum limit,\n    // its UTF8 length (which <= .length * 3) will also be below.\n    if (typeof def.minLength === 'number' && value.length * 3 < def.minLength) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must not be shorter than ${def.minLength} characters`,\n        ),\n      }\n    }\n\n    // If the JavaScript string length * 3 is within the maximum limit,\n    // its UTF8 length (which <= .length * 3) will also be within.\n    // When there's no minimal length, this lets us skip the UTF8 length check.\n    let canSkipUtf8LenChecks = false\n    if (\n      typeof def.minLength === 'undefined' &&\n      typeof def.maxLength === 'number' &&\n      value.length * 3 <= def.maxLength\n    ) {\n      canSkipUtf8LenChecks = true\n    }\n\n    if (!canSkipUtf8LenChecks) {\n      const len = utf8Len(value)\n\n      if (typeof def.maxLength === 'number') {\n        if (len > def.maxLength) {\n          return {\n            success: false,\n            error: new ValidationError(\n              `${path} must not be longer than ${def.maxLength} characters`,\n            ),\n          }\n        }\n      }\n\n      if (typeof def.minLength === 'number') {\n        if (len < def.minLength) {\n          return {\n            success: false,\n            error: new ValidationError(\n              `${path} must not be shorter than ${def.minLength} characters`,\n            ),\n          }\n        }\n      }\n    }\n  }\n\n  // maxGraphemes and minGraphemes\n  if (\n    typeof def.maxGraphemes === 'number' ||\n    typeof def.minGraphemes === 'number'\n  ) {\n    let needsMaxGraphemesCheck = false\n    let needsMinGraphemesCheck = false\n\n    if (typeof def.maxGraphemes === 'number') {\n      if (value.length <= def.maxGraphemes) {\n        // If the JavaScript string length (UTF-16) is within the maximum limit,\n        // its grapheme length (which <= .length) will also be within.\n        needsMaxGraphemesCheck = false\n      } else {\n        needsMaxGraphemesCheck = true\n      }\n    }\n\n    if (typeof def.minGraphemes === 'number') {\n      if (value.length < def.minGraphemes) {\n        // If the JavaScript string length (UTF-16) is below the minimal limit,\n        // its grapheme length (which <= .length) will also be below.\n        // Fail early.\n        return {\n          success: false,\n          error: new ValidationError(\n            `${path} must not be shorter than ${def.minGraphemes} graphemes`,\n          ),\n        }\n      } else {\n        needsMinGraphemesCheck = true\n      }\n    }\n\n    if (needsMaxGraphemesCheck || needsMinGraphemesCheck) {\n      const len = graphemeLen(value)\n\n      if (typeof def.maxGraphemes === 'number') {\n        if (len > def.maxGraphemes) {\n          return {\n            success: false,\n            error: new ValidationError(\n              `${path} must not be longer than ${def.maxGraphemes} graphemes`,\n            ),\n          }\n        }\n      }\n\n      if (typeof def.minGraphemes === 'number') {\n        if (len < def.minGraphemes) {\n          return {\n            success: false,\n            error: new ValidationError(\n              `${path} must not be shorter than ${def.minGraphemes} graphemes`,\n            ),\n          }\n        }\n      }\n    }\n  }\n\n  if (typeof def.format === 'string') {\n    switch (def.format) {\n      case 'datetime':\n        return formats.datetime(path, value)\n      case 'uri':\n        return formats.uri(path, value)\n      case 'at-uri':\n        return formats.atUri(path, value)\n      case 'did':\n        return formats.did(path, value)\n      case 'handle':\n        return formats.handle(path, value)\n      case 'at-identifier':\n        return formats.atIdentifier(path, value)\n      case 'nsid':\n        return formats.nsid(path, value)\n      case 'cid':\n        return formats.cid(path, value)\n      case 'language':\n        return formats.language(path, value)\n      case 'tid':\n        return formats.tid(path, value)\n      case 'record-key':\n        return formats.recordKey(path, value)\n    }\n  }\n\n  return { success: true, value }\n}\n\nfunction bytes(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  def = def as LexBytes\n\n  if (!value || !(value instanceof Uint8Array)) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a byte array`),\n    }\n  }\n\n  // maxLength\n  if (typeof def.maxLength === 'number') {\n    if (value.byteLength > def.maxLength) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must not be larger than ${def.maxLength} bytes`,\n        ),\n      }\n    }\n  }\n\n  // minLength\n  if (typeof def.minLength === 'number') {\n    if (value.byteLength < def.minLength) {\n      return {\n        success: false,\n        error: new ValidationError(\n          `${path} must not be smaller than ${def.minLength} bytes`,\n        ),\n      }\n    }\n  }\n\n  return { success: true, value }\n}\n\nfunction cidLink(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  if (CID.asCID(value) === null) {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be a CID`),\n    }\n  }\n\n  return { success: true, value }\n}\n\nfunction unknown(\n  lexicons: Lexicons,\n  path: string,\n  def: LexUserType,\n  value: unknown,\n): ValidationResult {\n  // type\n  if (!value || typeof value !== 'object') {\n    return {\n      success: false,\n      error: new ValidationError(`${path} must be an object`),\n    }\n  }\n\n  return { success: true, value }\n}\n"
  },
  {
    "path": "packages/lexicon/src/validators/xrpc.ts",
    "content": "import { Lexicons } from '../lexicons'\nimport { LexXrpcParameters, ValidationError, ValidationResult } from '../types'\nimport { array } from './complex'\nimport * as PrimitiveValidators from './primitives'\n\nexport function params(\n  lexicons: Lexicons,\n  path: string,\n  def: LexXrpcParameters,\n  val: unknown,\n): ValidationResult<Record<string, unknown>> {\n  // type\n  const value = val && typeof val === 'object' ? val : {}\n\n  const requiredProps = new Set(def.required ?? [])\n\n  // properties\n  let resultValue = value as Record<string, unknown>\n  if (typeof def.properties === 'object') {\n    for (const key in def.properties) {\n      const propDef = def.properties[key]\n      const validated =\n        propDef.type === 'array'\n          ? array(lexicons, key, propDef, value[key])\n          : PrimitiveValidators.validate(lexicons, key, propDef, value[key])\n      const propValue = validated.success ? validated.value : value[key]\n      const propIsUndefined = typeof propValue === 'undefined'\n      // Return error for bad validation, giving required rule precedence\n      if (propIsUndefined && requiredProps.has(key)) {\n        return {\n          success: false,\n          error: new ValidationError(`${path} must have the property \"${key}\"`),\n        }\n      } else if (!propIsUndefined && !validated.success) {\n        return validated\n      }\n      // Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value\n      if (propValue !== value[key]) {\n        if (resultValue === value) {\n          // Lazy shallow clone\n          resultValue = { ...value }\n        }\n        resultValue[key] = propValue\n      }\n    }\n  }\n\n  return { success: true, value: resultValue }\n}\n"
  },
  {
    "path": "packages/lexicon/tests/_scaffolds/lexicons.ts",
    "content": "import { LexiconDoc } from '../../src/index'\n\nconst lexicons: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'com.example.kitchenSink',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: [\n            'object',\n            'array',\n            'boolean',\n            'integer',\n            'string',\n            'bytes',\n            'cidLink',\n          ],\n          properties: {\n            object: { type: 'ref', ref: '#object' },\n            array: { type: 'array', items: { type: 'string' } },\n            boolean: { type: 'boolean' },\n            integer: { type: 'integer' },\n            string: { type: 'string' },\n            bytes: { type: 'bytes' },\n            cidLink: { type: 'cid-link' },\n          },\n        },\n      },\n      object: {\n        type: 'object',\n        required: ['object', 'array', 'boolean', 'integer', 'string'],\n        properties: {\n          object: { type: 'ref', ref: '#subobject' },\n          array: { type: 'array', items: { type: 'string' } },\n          boolean: { type: 'boolean' },\n          integer: { type: 'integer' },\n          string: { type: 'string' },\n        },\n      },\n      subobject: {\n        type: 'object',\n        required: ['boolean'],\n        properties: {\n          boolean: { type: 'boolean' },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.query',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'A query',\n        parameters: {\n          type: 'params',\n          required: ['boolean', 'integer'],\n          properties: {\n            boolean: { type: 'boolean' },\n            integer: { type: 'integer' },\n            string: { type: 'string' },\n            array: { type: 'array', items: { type: 'string' } },\n            def: { type: 'integer', default: 0 },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: { type: 'ref', ref: 'com.example.kitchenSink#object' },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.procedure',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'A procedure',\n        parameters: {\n          type: 'params',\n          required: ['boolean', 'integer'],\n          properties: {\n            boolean: { type: 'boolean' },\n            integer: { type: 'integer' },\n            string: { type: 'string' },\n            array: { type: 'array', items: { type: 'string' } },\n          },\n        },\n        input: {\n          encoding: 'application/json',\n          schema: { type: 'ref', ref: 'com.example.kitchenSink#object' },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: { type: 'ref', ref: 'com.example.kitchenSink#object' },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.optional',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            object: { type: 'ref', ref: 'com.example.kitchenSink#object' },\n            array: { type: 'array', items: { type: 'string' } },\n            boolean: { type: 'boolean' },\n            integer: { type: 'integer' },\n            string: { type: 'string' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.default',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          required: ['boolean'],\n          properties: {\n            boolean: { type: 'boolean', default: false },\n            integer: { type: 'integer', default: 0 },\n            string: { type: 'string', default: '' },\n            object: { type: 'ref', ref: '#object' },\n          },\n        },\n      },\n      object: {\n        type: 'object',\n        properties: {\n          boolean: { type: 'boolean', default: true },\n          integer: { type: 'integer', default: 1 },\n          string: { type: 'string', default: 'x' },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.union',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['unionOpen', 'unionClosed'],\n          properties: {\n            unionOpen: {\n              type: 'union',\n              refs: [\n                'com.example.kitchenSink#object',\n                'com.example.kitchenSink#subobject',\n              ],\n            },\n            unionClosed: {\n              type: 'union',\n              closed: true,\n              refs: [\n                'com.example.kitchenSink#object',\n                'com.example.kitchenSink#subobject',\n              ],\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.unknown',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['unknown'],\n          properties: {\n            unknown: { type: 'unknown' },\n            optUnknown: { type: 'unknown' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.arrayLength',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            array: {\n              type: 'array',\n              minLength: 2,\n              maxLength: 4,\n              items: { type: 'integer' },\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.boolConst',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            boolean: {\n              type: 'boolean',\n              const: false,\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.integerRange',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            integer: {\n              type: 'integer',\n              minimum: 2,\n              maximum: 4,\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.integerEnum',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            integer: {\n              type: 'integer',\n              enum: [1, 2],\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.integerConst',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            integer: {\n              type: 'integer',\n              const: 0,\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.stringLength',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            string: {\n              type: 'string',\n              minLength: 2,\n              maxLength: 4,\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.stringLengthNoMinLength',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            string: {\n              type: 'string',\n              maxLength: 4,\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.stringLengthGrapheme',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            string: {\n              type: 'string',\n              minGraphemes: 2,\n              maxGraphemes: 4,\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.stringEnum',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            string: {\n              type: 'string',\n              enum: ['a', 'b'],\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.stringConst',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            string: {\n              type: 'string',\n              const: 'a',\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.datetime',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            datetime: { type: 'string', format: 'datetime' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.uri',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            uri: { type: 'string', format: 'uri' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.atUri',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            atUri: { type: 'string', format: 'at-uri' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.did',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            did: { type: 'string', format: 'did' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.handle',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            handle: { type: 'string', format: 'handle' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.atIdentifier',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            atIdentifier: { type: 'string', format: 'at-identifier' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.nsid',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            nsid: { type: 'string', format: 'nsid' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.cid',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            cid: { type: 'string', format: 'cid' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.language',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            language: { type: 'string', format: 'language' },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.byteLength',\n    defs: {\n      main: {\n        type: 'record',\n        record: {\n          type: 'object',\n          properties: {\n            bytes: {\n              type: 'bytes',\n              minLength: 2,\n              maxLength: 4,\n            },\n          },\n        },\n      },\n    },\n  },\n]\n\nexport default lexicons\n"
  },
  {
    "path": "packages/lexicon/tests/general.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { LexiconDoc, Lexicons, parseLexiconDoc } from '../src/index'\nimport LexiconDocs from './_scaffolds/lexicons'\n\ndescribe('Lexicons collection', () => {\n  const lex = new Lexicons(LexiconDocs)\n\n  it('Adds schemas', () => {\n    expect(() => lex.add(LexiconDocs[0])).toThrow()\n  })\n\n  it('Correctly references all definitions', () => {\n    expect(lex.getDef('com.example.kitchenSink')).toEqual(\n      LexiconDocs[0].defs.main,\n    )\n    expect(lex.getDef('lex:com.example.kitchenSink')).toEqual(\n      LexiconDocs[0].defs.main,\n    )\n    expect(lex.getDef('com.example.kitchenSink#main')).toEqual(\n      LexiconDocs[0].defs.main,\n    )\n    expect(lex.getDef('lex:com.example.kitchenSink#main')).toEqual(\n      LexiconDocs[0].defs.main,\n    )\n    expect(lex.getDef('com.example.kitchenSink#object')).toEqual(\n      LexiconDocs[0].defs.object,\n    )\n    expect(lex.getDef('lex:com.example.kitchenSink#object')).toEqual(\n      LexiconDocs[0].defs.object,\n    )\n  })\n})\n\ndescribe('General validation', () => {\n  const lex = new Lexicons(LexiconDocs)\n  it('Validates records correctly', () => {\n    {\n      const res = lex.validate('com.example.kitchenSink', {\n        $type: 'com.example.kitchenSink',\n        object: {\n          object: { boolean: true },\n          array: ['one', 'two'],\n          boolean: true,\n          integer: 123,\n          string: 'string',\n        },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        datetime: new Date().toISOString(),\n        atUri: 'at://did:web:example.com/com.example.test/self',\n        did: 'did:web:example.com',\n        cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        bytes: new Uint8Array([0, 1, 2, 3]),\n        cidLink: CID.parse(\n          'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        ),\n      })\n      expect(res.success).toBe(true)\n    }\n    {\n      const res = lex.validate('com.example.kitchenSink', {})\n      expect(res.success).toBe(false)\n      if (res.success) throw new Error('Asserted')\n      expect(res.error?.message).toBe('Record must have the property \"object\"')\n    }\n  })\n  it('Validates objects correctly', () => {\n    {\n      const res = lex.validate('com.example.kitchenSink#object', {\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n      })\n      expect(res.success).toBe(true)\n    }\n    {\n      const res = lex.validate('com.example.kitchenSink#object', {})\n      expect(res.success).toBe(false)\n      if (res.success) throw new Error('Asserted')\n      expect(res.error?.message).toBe('Object must have the property \"object\"')\n    }\n  })\n  it('fails when a required property is missing', () => {\n    const schema = {\n      lexicon: 1,\n      id: 'com.example.kitchenSink',\n      defs: {\n        test: {\n          type: 'object',\n          required: ['foo'],\n          properties: {},\n        },\n      },\n    }\n    expect(() => {\n      parseLexiconDoc(schema)\n    }).toThrow('Required field \\\\\"foo\\\\\" not defined')\n  })\n  it('allows unknown fields to be present', () => {\n    const schema = {\n      lexicon: 1,\n      id: 'com.example.unknownFields',\n      defs: {\n        test: {\n          type: 'object',\n          properties: {},\n          foo: 3,\n        },\n      },\n    }\n\n    expect(() => {\n      parseLexiconDoc(schema)\n    }).not.toThrow()\n  })\n  it('fails lexicon parsing when uri is invalid', () => {\n    const schema: LexiconDoc = {\n      lexicon: 1,\n      id: 'com.example.invalidUri',\n      defs: {\n        main: {\n          type: 'object',\n          properties: {\n            test: { type: 'ref', ref: 'com.example.invalid#test#test' },\n          },\n        },\n      },\n    }\n\n    expect(() => {\n      new Lexicons([schema])\n    }).toThrow('Uri can only have one hash segment')\n  })\n  it('fails validation when ref uri has multiple hash segments', () => {\n    const schema: LexiconDoc = {\n      lexicon: 1,\n      id: 'com.example.invalidUri',\n      defs: {\n        main: {\n          type: 'object',\n          properties: {\n            test: { type: 'integer' },\n          },\n        },\n        object: {\n          type: 'object',\n          required: ['test'],\n          properties: {\n            test: {\n              type: 'union',\n              refs: ['com.example.invalidUri'],\n            },\n          },\n        },\n      },\n    }\n    const lexicons = new Lexicons([schema])\n    expect(() => {\n      lexicons.validate('com.example.invalidUri#object', {\n        test: {\n          $type: 'com.example.invalidUri#main#main',\n          test: 123,\n        },\n      })\n    }).toThrow('Uri can only have one hash segment')\n  })\n  it('union handles both implicit and explicit #main', () => {\n    const schemas: LexiconDoc[] = [\n      {\n        lexicon: 1,\n        id: 'com.example.implicitMain',\n        defs: {\n          main: {\n            type: 'object',\n            required: ['test'],\n            properties: {\n              test: { type: 'string' },\n            },\n          },\n        },\n      },\n      {\n        lexicon: 1,\n        id: 'com.example.testImplicitMain',\n        defs: {\n          main: {\n            type: 'object',\n            required: ['union'],\n            properties: {\n              union: {\n                type: 'union',\n                refs: ['com.example.implicitMain'],\n              },\n            },\n          },\n        },\n      },\n      {\n        lexicon: 1,\n        id: 'com.example.testExplicitMain',\n        defs: {\n          main: {\n            type: 'object',\n            required: ['union'],\n            properties: {\n              union: {\n                type: 'union',\n                refs: ['com.example.implicitMain#main'],\n              },\n            },\n          },\n        },\n      },\n    ]\n\n    const lexicon = new Lexicons(schemas)\n\n    let result = lexicon.validate('com.example.testImplicitMain', {\n      union: {\n        $type: 'com.example.implicitMain',\n        test: 123,\n      },\n    })\n    expect(result.success).toBeFalsy()\n    expect(result['error']?.message).toBe('Object/union/test must be a string')\n\n    result = lexicon.validate('com.example.testImplicitMain', {\n      union: {\n        $type: 'com.example.implicitMain#main',\n        test: 123,\n      },\n    })\n    expect(result.success).toBeFalsy()\n    expect(result['error']?.message).toBe('Object/union/test must be a string')\n\n    result = lexicon.validate('com.example.testExplicitMain', {\n      union: {\n        $type: 'com.example.implicitMain',\n        test: 123,\n      },\n    })\n    expect(result.success).toBeFalsy()\n    expect(result['error']?.message).toBe('Object/union/test must be a string')\n\n    result = lexicon.validate('com.example.testExplicitMain', {\n      union: {\n        $type: 'com.example.implicitMain#main',\n        test: 123,\n      },\n    })\n    expect(result.success).toBeFalsy()\n    expect(result['error']?.message).toBe('Object/union/test must be a string')\n  })\n})\n\ndescribe('Record validation', () => {\n  const lex = new Lexicons(LexiconDocs)\n\n  const passingSink = {\n    $type: 'com.example.kitchenSink',\n    object: {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      integer: 123,\n      string: 'string',\n    },\n    array: ['one', 'two'],\n    boolean: true,\n    integer: 123,\n    string: 'string',\n    bytes: new Uint8Array([0, 1, 2, 3]),\n    cidLink: CID.parse(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    ),\n  }\n\n  it('Passes valid schemas', () => {\n    lex.assertValidRecord('com.example.kitchenSink', passingSink)\n  })\n\n  it('Fails invalid input types', () => {\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', undefined),\n    ).toThrow('Record must be an object')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', 1234),\n    ).toThrow('Record must be an object')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', 'string'),\n    ).toThrow('Record must be an object')\n  })\n\n  it('Fails incorrect $type', () => {\n    expect(() => lex.assertValidRecord('com.example.kitchenSink', {})).toThrow(\n      'Record/$type must be a string',\n    )\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', { $type: 'foo' }),\n    ).toThrow('Invalid $type: must be lex:com.example.kitchenSink, got foo')\n  })\n\n  it('Fails missing required', () => {\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        $type: 'com.example.kitchenSink',\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n        datetime: new Date().toISOString(),\n        atUri: 'at://did:web:example.com/com.example.test/self',\n        did: 'did:web:example.com',\n        cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        bytes: new Uint8Array([0, 1, 2, 3]),\n        cidLink: CID.parse(\n          'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n        ),\n      }),\n    ).toThrow('Record must have the property \"object\"')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        object: undefined,\n      }),\n    ).toThrow('Record must have the property \"object\"')\n  })\n\n  it('Fails incorrect types', () => {\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        object: {\n          ...passingSink.object,\n          object: { boolean: '1234' },\n        },\n      }),\n    ).toThrow('Record/object/object/boolean must be a boolean')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        object: true,\n      }),\n    ).toThrow('Record/object must be an object')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        array: 1234,\n      }),\n    ).toThrow('Record/array must be an array')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        integer: true,\n      }),\n    ).toThrow('Record/integer must be an integer')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        string: {},\n      }),\n    ).toThrow('Record/string must be a string')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        bytes: 1234,\n      }),\n    ).toThrow('Record/bytes must be a byte array')\n    expect(() =>\n      lex.assertValidRecord('com.example.kitchenSink', {\n        ...passingSink,\n        cidLink: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n      }),\n    ).toThrow('Record/cidLink must be a CID')\n  })\n\n  it('Handles optional properties correctly', () => {\n    lex.assertValidRecord('com.example.optional', {\n      $type: 'com.example.optional',\n    })\n  })\n\n  it('Handles default properties correctly', () => {\n    const result = lex.assertValidRecord('com.example.default', {\n      $type: 'com.example.default',\n      object: {},\n    })\n    expect(result).toEqual({\n      $type: 'com.example.default',\n      boolean: false,\n      integer: 0,\n      string: '',\n      object: {\n        boolean: true,\n        integer: 1,\n        string: 'x',\n      },\n    })\n    expect(result).not.toHaveProperty('datetime')\n  })\n\n  it('Handles unions correctly', () => {\n    lex.assertValidRecord('com.example.union', {\n      $type: 'com.example.union',\n      unionOpen: {\n        $type: 'com.example.kitchenSink#object',\n        object: { boolean: true },\n        array: ['one', 'two'],\n        boolean: true,\n        integer: 123,\n        string: 'string',\n      },\n      unionClosed: {\n        $type: 'com.example.kitchenSink#subobject',\n        boolean: true,\n      },\n    })\n    lex.assertValidRecord('com.example.union', {\n      $type: 'com.example.union',\n      unionOpen: {\n        $type: 'com.example.other',\n      },\n      unionClosed: {\n        $type: 'com.example.kitchenSink#subobject',\n        boolean: true,\n      },\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.union', {\n        $type: 'com.example.union',\n        unionOpen: {},\n        unionClosed: {},\n      }),\n    ).toThrow(\n      'Record/unionOpen must be an object which includes the \"$type\" property',\n    )\n    expect(() =>\n      lex.assertValidRecord('com.example.union', {\n        $type: 'com.example.union',\n        unionOpen: {\n          $type: 'com.example.other',\n        },\n        unionClosed: {\n          $type: 'com.example.other',\n          boolean: true,\n        },\n      }),\n    ).toThrow(\n      'Record/unionClosed $type must be one of lex:com.example.kitchenSink#object, lex:com.example.kitchenSink#subobject',\n    )\n  })\n\n  it('Handles unknowns correctly', () => {\n    lex.assertValidRecord('com.example.unknown', {\n      $type: 'com.example.unknown',\n      unknown: { foo: 'bar' },\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.unknown', {\n        $type: 'com.example.unknown',\n      }),\n    ).toThrow('Record must have the property \"unknown\"')\n  })\n\n  it('Applies array length constraints', () => {\n    lex.assertValidRecord('com.example.arrayLength', {\n      $type: 'com.example.arrayLength',\n      array: [1, 2, 3],\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.arrayLength', {\n        $type: 'com.example.arrayLength',\n        array: [1],\n      }),\n    ).toThrow('Record/array must not have fewer than 2 elements')\n    expect(() =>\n      lex.assertValidRecord('com.example.arrayLength', {\n        $type: 'com.example.arrayLength',\n        array: [1, 2, 3, 4, 5],\n      }),\n    ).toThrow('Record/array must not have more than 4 elements')\n  })\n\n  it('Applies array item constraints', () => {\n    expect(() =>\n      lex.assertValidRecord('com.example.arrayLength', {\n        $type: 'com.example.arrayLength',\n        array: [1, '2', 3],\n      }),\n    ).toThrow('Record/array/1 must be an integer')\n    expect(() =>\n      lex.assertValidRecord('com.example.arrayLength', {\n        $type: 'com.example.arrayLength',\n        array: [1, undefined, 3],\n      }),\n    ).toThrow('Record/array/1 must be an integer')\n  })\n\n  it('Applies boolean const constraint', () => {\n    lex.assertValidRecord('com.example.boolConst', {\n      $type: 'com.example.boolConst',\n      boolean: false,\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.boolConst', {\n        $type: 'com.example.boolConst',\n        boolean: true,\n      }),\n    ).toThrow('Record/boolean must be false')\n  })\n\n  it('Applies integer range constraint', () => {\n    lex.assertValidRecord('com.example.integerRange', {\n      $type: 'com.example.integerRange',\n      integer: 2,\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.integerRange', {\n        $type: 'com.example.integerRange',\n        integer: 1,\n      }),\n    ).toThrow('Record/integer can not be less than 2')\n    expect(() =>\n      lex.assertValidRecord('com.example.integerRange', {\n        $type: 'com.example.integerRange',\n        integer: 5,\n      }),\n    ).toThrow('Record/integer can not be greater than 4')\n  })\n\n  it('Applies integer enum constraint', () => {\n    lex.assertValidRecord('com.example.integerEnum', {\n      $type: 'com.example.integerEnum',\n      integer: 2,\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.integerEnum', {\n        $type: 'com.example.integerEnum',\n        integer: 0,\n      }),\n    ).toThrow('Record/integer must be one of (1|2)')\n  })\n\n  it('Applies integer const constraint', () => {\n    lex.assertValidRecord('com.example.integerConst', {\n      $type: 'com.example.integerConst',\n      integer: 0,\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.integerConst', {\n        $type: 'com.example.integerConst',\n        integer: 1,\n      }),\n    ).toThrow('Record/integer must be 0')\n  })\n\n  it('Applies integer whole-number constraint', () => {\n    expect(() =>\n      lex.assertValidRecord('com.example.integerRange', {\n        $type: 'com.example.integerRange',\n        integer: 2.5,\n      }),\n    ).toThrow('Record/integer must be an integer')\n  })\n\n  it('Applies string length constraint', () => {\n    // Shorter than two UTF8 characters\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: '',\n      }),\n    ).toThrow('Record/string must not be shorter than 2 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: 'a',\n      }),\n    ).toThrow('Record/string must not be shorter than 2 characters')\n\n    // Two to four UTF8 characters\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'ab',\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: '\\u0301', // Combining acute accent (2 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'a\\u0301', // 'a' + combining acute accent (1 + 2 bytes = 3 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'aé', // 'a' (1 byte) + 'é' (2 bytes) = 3 bytes\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'abc',\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: '一', // CJK character (3 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: '\\uD83D', // Unpaired high surrogate (3 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'abcd',\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'éé', // 'é' + 'é' (2 + 2 bytes = 4 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: 'aaé', // 1 + 1 + 2 = 4 bytes\n    })\n    lex.assertValidRecord('com.example.stringLength', {\n      $type: 'com.example.stringLength',\n      string: '👋', // 4 bytes\n    })\n\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: 'abcde',\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: 'a\\u0301\\u0301', // 1 + (2 * 2) = 5 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: '\\uD83D\\uD83D', // Two unpaired high surrogates (3 * 2 = 6 bytes)\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: 'ééé', // 2 + 2 + 2 bytes = 6 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: '👋a', // 4 + 1 bytes = 5 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: '👨👨', // 4 + 4 = 8 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLength', {\n        $type: 'com.example.stringLength',\n        string: '👨‍👩‍👧‍👧', // 4 emojis × 4 bytes + 3 ZWJs × 3 bytes = 25 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n  })\n\n  it('Applies string length constraint (no minLength)', () => {\n    // Shorter than two UTF8 characters\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: '',\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'a',\n    })\n\n    // Two to four UTF8 characters\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'ab',\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: '\\u0301', // Combining acute accent (2 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'a\\u0301', // 'a' + combining acute accent (1 + 2 bytes = 3 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'aé', // 'a' (1 byte) + 'é' (2 bytes) = 3 bytes\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'abc',\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: '一', // CJK character (3 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: '\\uD83D', // Unpaired high surrogate (3 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'abcd',\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'éé', // 'é' + 'é' (2 + 2 bytes = 4 bytes)\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: 'aaé', // 1 + 1 + 2 = 4 bytes\n    })\n    lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n      $type: 'com.example.stringLengthNoMinLength',\n      string: '👋', // 4 bytes\n    })\n\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: 'abcde',\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: 'a\\u0301\\u0301', // 1 + (2 * 2) = 5 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: '\\uD83D\\uD83D', // Two unpaired high surrogates (3 * 2 = 6 bytes)\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: 'ééé', // 2 + 2 + 2 bytes = 6 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: '👋a', // 4 + 1 bytes = 5 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: '👨👨', // 4 + 4 = 8 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthNoMinLength', {\n        $type: 'com.example.stringLengthNoMinLength',\n        string: '👨‍👩‍👧‍👧', // 4 emojis × 4 bytes + 3 ZWJs × 3 bytes = 25 bytes\n      }),\n    ).toThrow('Record/string must not be longer than 4 characters')\n  })\n\n  it('Applies grapheme string length constraint', () => {\n    // Shorter than two graphemes\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: '',\n      }),\n    ).toThrow('Record/string must not be shorter than 2 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: '\\u0301\\u0301\\u0301', // Three combining acute accents\n      }),\n    ).toThrow('Record/string must not be shorter than 2 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: 'a',\n      }),\n    ).toThrow('Record/string must not be shorter than 2 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: 'a\\u0301\\u0301\\u0301\\u0301', // 'á́́́' ('a' with four combining acute accents)\n      }),\n    ).toThrow('Record/string must not be shorter than 2 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: '5\\uFE0F', // '5️' with emoji presentation\n      }),\n    ).toThrow('Record/string must not be shorter than 2 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: '👨‍👩‍👧‍👧',\n      }),\n    ).toThrow('Record/string must not be shorter than 2 graphemes')\n\n    // Two to four graphemes\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: 'ab',\n    })\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: 'a\\u0301b', // 'áb' with combining accent\n    })\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: 'a\\u0301b\\u0301', // 'áb́'\n    })\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: '😀😀',\n    })\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: '12👨‍👩‍👧‍👧',\n    })\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: 'abcd',\n    })\n    lex.assertValidRecord('com.example.stringLengthGrapheme', {\n      $type: 'com.example.stringLengthGrapheme',\n      string: 'a\\u0301b\\u0301c\\u0301d\\u0301', // 'áb́ćd́'\n    })\n\n    // Longer than four graphemes\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: 'abcde',\n      }),\n    ).toThrow('Record/string must not be longer than 4 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: 'a\\u0301b\\u0301c\\u0301d\\u0301e\\u0301', // 'áb́ćd́é'\n      }),\n    ).toThrow('Record/string must not be longer than 4 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: '😀😀😀😀😀',\n      }),\n    ).toThrow('Record/string must not be longer than 4 graphemes')\n    expect(() =>\n      lex.assertValidRecord('com.example.stringLengthGrapheme', {\n        $type: 'com.example.stringLengthGrapheme',\n        string: 'ab😀de',\n      }),\n    ).toThrow('Record/string must not be longer than 4 graphemes')\n  })\n\n  it('Applies string enum constraint', () => {\n    lex.assertValidRecord('com.example.stringEnum', {\n      $type: 'com.example.stringEnum',\n      string: 'a',\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.stringEnum', {\n        $type: 'com.example.stringEnum',\n        string: 'c',\n      }),\n    ).toThrow('Record/string must be one of (a|b)')\n  })\n\n  it('Applies string const constraint', () => {\n    lex.assertValidRecord('com.example.stringConst', {\n      $type: 'com.example.stringConst',\n      string: 'a',\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.stringConst', {\n        $type: 'com.example.stringConst',\n        string: 'b',\n      }),\n    ).toThrow('Record/string must be a')\n  })\n\n  it('Applies datetime formatting constraint', () => {\n    for (const datetime of [\n      '2022-12-12T00:50:36.809Z',\n      '2022-12-12T00:50:36Z',\n      '2022-12-12T00:50:36.8Z',\n      '2022-12-12T00:50:36.80Z',\n      '2022-12-12T00:50:36+00:00',\n      '2022-12-12T00:50:36.8+00:00',\n      '2022-12-11T19:50:36-05:00',\n      '2022-12-11T19:50:36.8-05:00',\n      '2022-12-11T19:50:36.80-05:00',\n      '2022-12-11T19:50:36.809-05:00',\n    ]) {\n      lex.assertValidRecord('com.example.datetime', {\n        $type: 'com.example.datetime',\n        datetime,\n      })\n    }\n    expect(() =>\n      lex.assertValidRecord('com.example.datetime', {\n        $type: 'com.example.datetime',\n        datetime: 'bad date',\n      }),\n    ).toThrow(\n      'Record/datetime must be an valid atproto datetime (both RFC-3339 and ISO-8601)',\n    )\n  })\n\n  it('Applies uri formatting constraint', () => {\n    for (const uri of [\n      'https://example.com',\n      'https://example.com/with/path',\n      'https://example.com/with/path?and=query',\n      'at://bsky.social',\n      'did:example:test',\n    ]) {\n      lex.assertValidRecord('com.example.uri', {\n        $type: 'com.example.uri',\n        uri,\n      })\n    }\n    expect(() =>\n      lex.assertValidRecord('com.example.uri', {\n        $type: 'com.example.uri',\n        uri: 'not a uri',\n      }),\n    ).toThrow('Record/uri must be a uri')\n  })\n\n  it('Applies at-uri formatting constraint', () => {\n    lex.assertValidRecord('com.example.atUri', {\n      $type: 'com.example.atUri',\n      atUri: 'at://did:web:example.com/com.example.test/self',\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.atUri', {\n        $type: 'com.example.atUri',\n        atUri: 'http://not-atproto.com',\n      }),\n    ).toThrow('Record/atUri must be a valid at-uri')\n  })\n\n  it('Applies did formatting constraint', () => {\n    lex.assertValidRecord('com.example.did', {\n      $type: 'com.example.did',\n      did: 'did:web:example.com',\n    })\n    lex.assertValidRecord('com.example.did', {\n      $type: 'com.example.did',\n      did: 'did:plc:12345678abcdefghijklmnop',\n    })\n\n    expect(() =>\n      lex.assertValidRecord('com.example.did', {\n        $type: 'com.example.did',\n        did: 'bad did',\n      }),\n    ).toThrow('Record/did must be a valid did')\n    expect(() =>\n      lex.assertValidRecord('com.example.did', {\n        $type: 'com.example.did',\n        did: 'did:short',\n      }),\n    ).toThrow('Record/did must be a valid did')\n  })\n\n  it('Applies handle formatting constraint', () => {\n    lex.assertValidRecord('com.example.handle', {\n      $type: 'com.example.handle',\n      handle: 'test.bsky.social',\n    })\n    lex.assertValidRecord('com.example.handle', {\n      $type: 'com.example.handle',\n      handle: 'bsky.test',\n    })\n\n    expect(() =>\n      lex.assertValidRecord('com.example.handle', {\n        $type: 'com.example.handle',\n        handle: 'bad handle',\n      }),\n    ).toThrow('Record/handle must be a valid handle')\n    expect(() =>\n      lex.assertValidRecord('com.example.handle', {\n        $type: 'com.example.handle',\n        handle: '-bad-.test',\n      }),\n    ).toThrow('Record/handle must be a valid handle')\n  })\n\n  it('Applies at-identifier formatting constraint', () => {\n    lex.assertValidRecord('com.example.atIdentifier', {\n      $type: 'com.example.atIdentifier',\n      atIdentifier: 'bsky.test',\n    })\n    lex.assertValidRecord('com.example.atIdentifier', {\n      $type: 'com.example.atIdentifier',\n      atIdentifier: 'did:plc:12345678abcdefghijklmnop',\n    })\n\n    expect(() =>\n      lex.assertValidRecord('com.example.atIdentifier', {\n        $type: 'com.example.atIdentifier',\n        atIdentifier: 'bad id',\n      }),\n    ).toThrow('Record/atIdentifier must be a valid did or a handle')\n    expect(() =>\n      lex.assertValidRecord('com.example.atIdentifier', {\n        $type: 'com.example.atIdentifier',\n        atIdentifier: '-bad-.test',\n      }),\n    ).toThrow('Record/atIdentifier must be a valid did or a handle')\n  })\n\n  it('Applies nsid formatting constraint', () => {\n    lex.assertValidRecord('com.example.nsid', {\n      $type: 'com.example.nsid',\n      nsid: 'com.atproto.test',\n    })\n    lex.assertValidRecord('com.example.nsid', {\n      $type: 'com.example.nsid',\n      nsid: 'app.bsky.nested.test',\n    })\n\n    expect(() =>\n      lex.assertValidRecord('com.example.nsid', {\n        $type: 'com.example.nsid',\n        nsid: 'bad nsid',\n      }),\n    ).toThrow('Record/nsid must be a valid nsid')\n    expect(() =>\n      lex.assertValidRecord('com.example.nsid', {\n        $type: 'com.example.nsid',\n        nsid: 'com.bad-.foo',\n      }),\n    ).toThrow('Record/nsid must be a valid nsid')\n  })\n\n  it('Applies cid formatting constraint', () => {\n    lex.assertValidRecord('com.example.cid', {\n      $type: 'com.example.cid',\n      cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.cid', {\n        $type: 'com.example.cid',\n        cid: 'abapsdofiuwrpoiasdfuaspdfoiu',\n      }),\n    ).toThrow('Record/cid must be a cid string')\n  })\n\n  it('Applies language formatting constraint', () => {\n    lex.assertValidRecord('com.example.language', {\n      $type: 'com.example.language',\n      language: 'en-US-boont',\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.language', {\n        $type: 'com.example.language',\n        language: 'not-a-language-',\n      }),\n    ).toThrow('Record/language must be a well-formed BCP 47 language tag')\n  })\n\n  it('Applies bytes length constraints', () => {\n    lex.assertValidRecord('com.example.byteLength', {\n      $type: 'com.example.byteLength',\n      bytes: new Uint8Array([1, 2, 3]),\n    })\n    expect(() =>\n      lex.assertValidRecord('com.example.byteLength', {\n        $type: 'com.example.byteLength',\n        bytes: new Uint8Array([1]),\n      }),\n    ).toThrow('Record/bytes must not be smaller than 2 bytes')\n    expect(() =>\n      lex.assertValidRecord('com.example.byteLength', {\n        $type: 'com.example.byteLength',\n        bytes: new Uint8Array([1, 2, 3, 4, 5]),\n      }),\n    ).toThrow('Record/bytes must not be larger than 4 bytes')\n  })\n})\n\ndescribe('XRPC parameter validation', () => {\n  const lex = new Lexicons(LexiconDocs)\n\n  it('Passes valid parameters', () => {\n    const queryResult = lex.assertValidXrpcParams('com.example.query', {\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      array: ['x', 'y'],\n    })\n    expect(queryResult).toEqual({\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      array: ['x', 'y'],\n      def: 0,\n    })\n    const paramResult = lex.assertValidXrpcParams('com.example.procedure', {\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      array: ['x', 'y'],\n      def: 1,\n    })\n    expect(paramResult).toEqual({\n      boolean: true,\n      integer: 123,\n      string: 'string',\n      array: ['x', 'y'],\n      def: 1,\n    })\n  })\n\n  it('Handles required correctly', () => {\n    lex.assertValidXrpcParams('com.example.query', {\n      boolean: true,\n      integer: 123,\n    })\n    expect(() =>\n      lex.assertValidXrpcParams('com.example.query', {\n        boolean: true,\n      }),\n    ).toThrow('Params must have the property \"integer\"')\n    expect(() =>\n      lex.assertValidXrpcParams('com.example.query', {\n        boolean: true,\n        integer: undefined,\n      }),\n    ).toThrow('Params must have the property \"integer\"')\n  })\n\n  it('Validates parameter types', () => {\n    expect(() =>\n      lex.assertValidXrpcParams('com.example.query', {\n        boolean: 'string',\n        integer: 123,\n        string: 'string',\n      }),\n    ).toThrow('boolean must be a boolean')\n    expect(() =>\n      lex.assertValidXrpcParams('com.example.query', {\n        boolean: true,\n        float: 123.45,\n        integer: 123,\n        string: 'string',\n        array: 'x',\n      }),\n    ).toThrow('array must be an array')\n  })\n})\n\ndescribe('XRPC input validation', () => {\n  const lex = new Lexicons(LexiconDocs)\n\n  it('Passes valid inputs', () => {\n    lex.assertValidXrpcInput('com.example.procedure', {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      float: 123.45,\n      integer: 123,\n      string: 'string',\n    })\n  })\n\n  it('Validates the input', () => {\n    // dont need to check this extensively since it's the same logic as tested in record validation\n    expect(() =>\n      lex.assertValidXrpcInput('com.example.procedure', {\n        object: { boolean: 'string' },\n        array: ['one', 'two'],\n        boolean: true,\n        float: 123.45,\n        integer: 123,\n        string: 'string',\n      }),\n    ).toThrow('Input/object/boolean must be a boolean')\n    expect(() => lex.assertValidXrpcInput('com.example.procedure', {})).toThrow(\n      'Input must have the property \"object\"',\n    )\n  })\n})\n\ndescribe('XRPC output validation', () => {\n  const lex = new Lexicons(LexiconDocs)\n\n  it('Passes valid outputs', () => {\n    lex.assertValidXrpcOutput('com.example.query', {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      float: 123.45,\n      integer: 123,\n      string: 'string',\n    })\n    lex.assertValidXrpcOutput('com.example.procedure', {\n      object: { boolean: true },\n      array: ['one', 'two'],\n      boolean: true,\n      float: 123.45,\n      integer: 123,\n      string: 'string',\n    })\n  })\n\n  it('Validates the output', () => {\n    // dont need to check this extensively since it's the same logic as tested in record validation\n    expect(() =>\n      lex.assertValidXrpcOutput('com.example.query', {\n        object: { boolean: 'string' },\n        array: ['one', 'two'],\n        boolean: true,\n        float: 123.45,\n        integer: 123,\n        string: 'string',\n      }),\n    ).toThrow('Output/object/boolean must be a boolean')\n    expect(() =>\n      lex.assertValidXrpcOutput('com.example.procedure', {}),\n    ).toThrow('Output must have the property \"object\"')\n  })\n})\n"
  },
  {
    "path": "packages/lexicon/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/lexicon/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lexicon/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/CHANGELOG.md",
    "content": "# @atproto/lexicon-resolver\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex@0.0.22\n  - @atproto/lex-document@0.0.17\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f), [`192685f`](https://github.com/bluesky-social/atproto/commit/192685fca75a68c9c50a94817d3f27da7fc02f56)]:\n  - @atproto/syntax@0.5.1\n  - @atproto/repo@0.8.13\n  - @atproto/lex@0.0.21\n  - @atproto/lex-document@0.0.16\n\n## 0.3.0\n\n### Minor Changes\n\n- [#4697](https://github.com/bluesky-social/atproto/pull/4697) [`1c473ab`](https://github.com/bluesky-social/atproto/commit/1c473ab555d734fa1bb68d3cdb2e17a94929afd3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `@atproto/lex-cli` based codegen with `@atproto/lex`\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lex@0.0.20\n  - @atproto/lex-document@0.0.15\n\n## 0.2.6\n\n### Patch Changes\n\n- [#4577](https://github.com/bluesky-social/atproto/pull/4577) [`78e8ec2`](https://github.com/bluesky-social/atproto/commit/78e8ec25df860f6d383f3a0e38a2c3a5670bfe01) Thanks [@devinivy](https://github.com/devinivy)! - Do not require at:// handle for lexicon resolution\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/repo@0.8.12\n  - @atproto/xrpc@0.7.7\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/identity@0.4.10\n  - @atproto/lexicon@0.5.2\n  - @atproto/repo@0.8.11\n  - @atproto/xrpc@0.7.6\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto-labs/fetch-node@0.2.0\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`8dd77bad2`](https://github.com/bluesky-social/atproto/commit/8dd77bad2fdee20e39d3787198d960c19d8df3d0)]:\n  - @atproto/repo@0.8.10\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/repo@0.8.9\n  - @atproto/identity@0.4.9\n  - @atproto/lexicon@0.5.1\n  - @atproto/xrpc@0.7.5\n\n## 0.2.0\n\n### Minor Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `nsid` property to `LexiconResolutionError`\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `LexiconResolutionError.from` method to create an error from a string `nsid`\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export all types used in public interfaces\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto-labs/fetch-node@0.1.10\n  - @atproto/xrpc@0.7.4\n  - @atproto/repo@0.8.8\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/repo@0.8.7\n  - @atproto/xrpc@0.7.3\n\n## 0.1.0\n\n### Minor Changes\n\n- [#4069](https://github.com/bluesky-social/atproto/pull/4069) [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12) Thanks [@devinivy](https://github.com/devinivy)! - Support for Lexicon resolution and DID authority lookups.\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/repo@0.8.6\n  - @atproto/lexicon@0.4.13\n  - @atproto/xrpc@0.7.2\n"
  },
  {
    "path": "packages/lexicon-resolver/README.md",
    "content": "# @atproto/lexicon-resolver\n\nATProto Lexicon resolution\n\n[![NPM](https://img.shields.io/npm/v/@atproto/lexicon-resolver)](https://www.npmjs.com/package/@atproto/lexicon-resolver)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Usage\n\nThis package may be used to determine the DID authority for a Lexicon based on its NSID, and to resolve a Lexicon from its NSID based on [Lexicon Resolution](https://atproto.com/specs/lexicon#lexicon-publication-and-resolution) from the network. Resolutions always verify the inclusion proof for the Lexicon schema document published to the ATProto network.\n\n```ts\nimport {\n  resolveLexicon,\n  resolveLexiconDidAuthority,\n} from '@atproto/lexicon-resolver'\n\n// Which DID is the authority over this Lexicon?\nconst didAuthority = await resolveLexiconDidAuthority('app.bsky.feed.post')\n// Resolve the Lexicon document with resolution details\nconst resolved = await resolveLexicon('app.bsky.feed.post')\n/**\n * {\n *   commit: {\n *     did: 'did:plc:4v4y5r3lwsbtmsxhile2ljac',\n *     rev: '3lnlpukgipj2c',\n *     sig: Uint8Array(64),\n *     ...\n *   },\n *   uri: AtUri(at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.post),\n *   cid: CID(bafyreidgbehqwweghrrddfu6jgj7lyr6fwhzgazhirnszdb5lvr7iynkiy),\n *   nsid: NSID('app.bsky.feed.post'),\n *   lexicon: {\n *     '$type': 'com.atproto.lexicon.schema',\n *     lexicon: 1\n *     id: 'app.bsky.feed.post',\n *     defs: { main: [Object], ... },\n *   }\n * }\n */\n```\n\n### With identity caching\n\nIdentity data is used in order to fetch and verify record contents. The @atproto/identity package can be used to offer more control over caching and other behaviors of identity lookups.\n\n```ts\nimport { IdResolver, MemoryCache } from '@atproto/identity'\nimport { buildLexiconResolver } from '@atproto/lexicon-resolver'\n\nconst resolveLexicon = buildLexiconResolver({\n  idResolver: new IdResolver({\n    didCache: new MemoryCache(),\n  }),\n})\n\nconst resolved = await resolveLexicon('app.bsky.feed.post')\n```\n\n### With DID authority override\n\nYou may specify a specific DID authority you'd like to use to perform a Lexicon resolution, overriding ATProto's DNS-based authority over Lexicons. This is described in some more detail in [Authority and Control](https://atproto.com/specs/lexicon#authority-and-control).\n\n```ts\nimport { resolveLexicon } from '@atproto/lexicon-resolver'\n\nconst resolved = await resolveLexicon('app.bsky.feed.post', {\n  didAuthority: 'did:plc:...',\n})\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/lexicon-resolver/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Lexicon Resolver',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/package.json",
    "content": "{\n  \"name\": \"@atproto/lexicon-resolver\",\n  \"version\": \"0.3.2\",\n  \"type\": \"commonjs\",\n  \"license\": \"MIT\",\n  \"description\": \"ATProto Lexicon resolution\",\n  \"keywords\": [\n    \"atproto\",\n    \"lexicon\"\n  ],\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"codegen\": \"lex build --indexFile --lexicons ../../lexicons --clear --include com.atproto.sync.getRecord --include com.atproto.lexicon.schema\"\n  },\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/lexicon-resolver\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch-node\": \"workspace:^\",\n    \"@atproto/identity\": \"workspace:^\",\n    \"@atproto/lex\": \"workspace:^\",\n    \"@atproto/lex-document\": \"workspace:^\",\n    \"@atproto/repo\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/src/index.ts",
    "content": "export * from './record.js'\nexport * from './lexicon.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicon.ts",
    "content": "import dns from 'node:dns/promises'\nimport { Cid, l } from '@atproto/lex'\nimport { LexiconDocument, lexiconDocumentSchema } from '@atproto/lex-document'\nimport { Commit } from '@atproto/repo'\nimport { AtUri, DidString, NSID, NsidString } from '@atproto/syntax'\nimport * as lexiconsSchema from './lexicons/com/atproto/lexicon/schema.js'\nimport {\n  BuildRecordResolverOptions,\n  ResolveRecordOptions,\n  buildRecordResolver,\n} from './record.js'\n\nconst DNS_SUBDOMAIN = '_lexicon'\nconst DNS_PREFIX = 'did='\n\nexport type LexiconDocumentRecord = lexiconsSchema.Main & LexiconDocument\nexport const LEXICON_SCHEMA_NSID = lexiconsSchema.$nsid\n\n/**\n * Resolve Lexicon from an NSID\n */\nexport type LexiconResolver = (\n  nsid: NSID | NsidString,\n) => Promise<LexiconResolution>\n\n/**\n * Resolve Lexicon from an NSID using Lexicon DID authority and record resolution\n */\nexport type AtprotoLexiconResolver = (\n  nsid: NSID | NsidString,\n  options?: ResolveLexiconOptions,\n) => Promise<LexiconResolution>\n\nexport type BuildLexiconResolverOptions = BuildRecordResolverOptions\n\nexport type ResolveLexiconOptions = ResolveRecordOptions & {\n  didAuthority?: DidString\n}\n\nexport type LexiconResolution = {\n  commit: Commit\n  uri: AtUri\n  cid: Cid\n  nsid: NSID\n  lexicon: LexiconDocumentRecord\n}\n\nexport { AtUri, NSID }\nexport type { Cid, Commit, DidString, LexiconDocument, NsidString }\n\n/**\n * Build a Lexicon resolver function.\n */\nexport function buildLexiconResolver(\n  options: BuildLexiconResolverOptions = {},\n): AtprotoLexiconResolver {\n  const resolveRecord = buildRecordResolver(options)\n  return async function (\n    input: NSID | NsidString,\n    opts: ResolveLexiconOptions = {},\n  ): Promise<LexiconResolution> {\n    const nsid = NSID.from(input)\n    const didAuthority = await getDidAuthority(nsid, opts)\n    const verified = await resolveRecord(\n      AtUri.make(didAuthority, lexiconsSchema.$nsid, nsid.toString()),\n      { forceRefresh: opts.forceRefresh },\n    ).catch((err) => {\n      throw new LexiconResolutionError(\n        nsid,\n        'Could not resolve Lexicon schema record',\n        { cause: err },\n      )\n    })\n\n    if (!lexiconsSchema.$matches(verified.record)) {\n      throw new LexiconResolutionError(nsid, 'Invalid Lexicon schema record')\n    }\n\n    const validationResult = lexiconDocumentSchema.safeValidate(verified.record)\n    if (!validationResult.success) {\n      throw new LexiconResolutionError(nsid, 'Invalid Lexicon document', {\n        cause: validationResult.reason,\n      })\n    }\n\n    const lexicon = validationResult.value\n    if (lexicon.id !== nsid.toString()) {\n      throw new LexiconResolutionError(\n        nsid,\n        `Lexicon schema record id (${lexicon.id}) does not match NSID`,\n      )\n    }\n    const { uri, cid, commit } = verified\n    return { commit, uri, cid, nsid, lexicon }\n  } satisfies LexiconResolver\n}\n\nexport const resolveLexicon = buildLexiconResolver()\n\n/**\n * Resolve the DID authority for a Lexicon from the network using DNS, based on its NSID.\n * @param input NSID or string representing one for which to lookup its Lexicon DID authority.\n */\nexport async function resolveLexiconDidAuthority(\n  input: NSID | NsidString,\n): Promise<DidString | undefined> {\n  const nsid = NSID.from(input)\n  const did = await resolveDns(nsid.authority)\n  if (did == null || !l.isDidString(did)) return\n  return did\n}\n\nexport class LexiconResolutionError extends Error {\n  constructor(\n    public readonly nsid: NSID,\n    public readonly description = `Could not resolve Lexicon for NSID`,\n    options?: ErrorOptions,\n  ) {\n    super(`${description} (${nsid})`, options)\n    this.name = 'LexiconResolutionError'\n  }\n\n  static from(\n    input: NSID | string,\n    description?: string,\n    options?: ErrorOptions,\n  ): LexiconResolutionError {\n    const nsid = NSID.from(input)\n    return new LexiconResolutionError(nsid, description, options)\n  }\n}\n\nasync function getDidAuthority(nsid: NSID, options: ResolveLexiconOptions) {\n  if (options.didAuthority) {\n    return options.didAuthority\n  }\n  const did = await resolveLexiconDidAuthority(nsid)\n  if (!did) {\n    throw new LexiconResolutionError(\n      nsid,\n      `Could not resolve a DID authority for NSID`,\n    )\n  }\n  return did\n}\n\nasync function resolveDns(authority: string): Promise<string | undefined> {\n  let chunkedResults: string[][]\n  try {\n    chunkedResults = await dns.resolveTxt(`${DNS_SUBDOMAIN}.${authority}`)\n  } catch (err) {\n    return undefined\n  }\n  return parseDnsResult(chunkedResults)\n}\n\nfunction parseDnsResult(chunkedResults: string[][]): string | undefined {\n  const results = chunkedResults.map((chunks) => chunks.join(''))\n  const found = results.filter((i) => i.startsWith(DNS_PREFIX))\n  if (found.length !== 1) {\n    return undefined\n  }\n  return found[0].slice(DNS_PREFIX.length)\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto/lexicon/schema.defs.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nimport { l } from '@atproto/lex'\n\nconst $nsid = 'com.atproto.lexicon.schema'\n\nexport { $nsid }\n\n/** Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc). */\ntype Main = {\n  $type: 'com.atproto.lexicon.schema'\n\n  /**\n   * Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.\n   */\n  lexicon: number\n}\n\nexport type { Main }\n\n/** Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc). */\nconst main = l.record<'nsid', Main>(\n  'nsid',\n  $nsid,\n  l.object({ lexicon: l.integer() }),\n)\n\nexport { main }\n\nexport const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main),\n  $build = /*#__PURE__*/ main.build.bind(main),\n  $type = /*#__PURE__*/ main.$type\nexport const $assert = /*#__PURE__*/ main.assert.bind(main),\n  $check = /*#__PURE__*/ main.check.bind(main),\n  $cast = /*#__PURE__*/ main.cast.bind(main),\n  $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main),\n  $matches = /*#__PURE__*/ main.matches.bind(main),\n  $parse = /*#__PURE__*/ main.parse.bind(main),\n  $safeParse = /*#__PURE__*/ main.safeParse.bind(main),\n  $validate = /*#__PURE__*/ main.validate.bind(main),\n  $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main)\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto/lexicon/schema.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * from './schema.defs.js'\nexport * as $defs from './schema.defs.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto/lexicon.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * as schema from './lexicon/schema.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto/sync/getRecord.defs.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nimport { l } from '@atproto/lex'\n\nconst $nsid = 'com.atproto.sync.getRecord'\n\nexport { $nsid }\n\n/** Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth. */\nconst main = l.query(\n  $nsid,\n  l.params({\n    did: l.string({ format: 'did' }),\n    collection: l.string({ format: 'nsid' }),\n    rkey: l.string({ format: 'record-key' }),\n  }),\n  l.payload('application/vnd.ipld.car'),\n  [\n    'RecordNotFound',\n    'RepoNotFound',\n    'RepoTakendown',\n    'RepoSuspended',\n    'RepoDeactivated',\n  ],\n)\nexport { main }\n\nexport type $Params = l.InferMethodParams<typeof main>\nexport type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>\nexport type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<\n  typeof main,\n  B\n>\n\nexport const $lxm = main.nsid,\n  $params = main.parameters,\n  $output = main.output\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto/sync/getRecord.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * from './getRecord.defs.js'\nexport * as $defs from './getRecord.defs.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto/sync.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * as getRecord from './sync/getRecord.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com/atproto.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * as lexicon from './atproto/lexicon.js'\nexport * as sync from './atproto/sync.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/com.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * as atproto from './com/atproto.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/lexicons/index.ts",
    "content": "/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nexport * as com from './com.js'\n"
  },
  {
    "path": "packages/lexicon-resolver/src/record.ts",
    "content": "import { IdResolver, parseToAtprotoDocument } from '@atproto/identity'\nimport {\n  AgentConfig,\n  Cid,\n  Client,\n  DidString,\n  FetchHandler,\n  LexMap,\n  l,\n} from '@atproto/lex'\nimport {\n  Commit,\n  MST,\n  MemoryBlockstore,\n  def as repoDef,\n  readCarWithRoot,\n  verifyCommitSig,\n} from '@atproto/repo'\nimport { AtUri, AtUriString } from '@atproto/syntax'\nimport { safeFetchWrap } from '@atproto-labs/fetch-node'\nimport { com } from './lexicons/index.js'\n\nexport { AtUri, IdResolver }\nexport type {\n  AgentConfig,\n  AtUriString,\n  Cid,\n  Commit,\n  DidString,\n  FetchHandler,\n  LexMap,\n}\n\n/**\n * Resolve a record from the network.\n */\nexport type RecordResolver = (\n  uri: AtUri | AtUriString,\n) => Promise<RecordResolution>\n\n/**\n * Resolve a record from the network, verifying its authenticity.\n */\nexport type AtprotoRecordResolver = (\n  uri: AtUri | AtUriString,\n  options?: ResolveRecordOptions,\n) => Promise<RecordResolution>\n\nexport type BuildRecordResolverOptions = {\n  idResolver?: IdResolver\n  rpc?: Partial<AgentConfig> | FetchHandler\n}\n\nexport type ResolveRecordOptions = {\n  forceRefresh?: boolean\n}\n\nexport type RecordResolution = {\n  commit: Commit\n  uri: AtUri\n  cid: Cid\n  record: LexMap\n}\n\n/**\n * Build a record resolver function.\n */\nexport function buildRecordResolver(\n  options: BuildRecordResolverOptions = {},\n): AtprotoRecordResolver {\n  const { idResolver = new IdResolver(), rpc } = options\n  return async function resolveRecord(\n    uriStr: AtUri | AtUriString,\n    opts: ResolveRecordOptions = {},\n  ): Promise<RecordResolution> {\n    const uri = typeof uriStr === 'string' ? new AtUri(uriStr) : uriStr\n    const did = await getDidFromUri(uri, { idResolver })\n    const identityDoc = await idResolver.did\n      .ensureResolve(did, opts.forceRefresh)\n      .catch((err) => {\n        throw new RecordResolutionError('Could not resolve DID identity data', {\n          cause: err,\n        })\n      })\n    const { pds, signingKey } = parseToAtprotoDocument(identityDoc)\n    if (!pds) {\n      throw new RecordResolutionError(\n        'Incomplete DID identity data: missing pds',\n      )\n    }\n    if (!signingKey) {\n      throw new RecordResolutionError(\n        'Incomplete DID identity data: missing signing key',\n      )\n    }\n    const client = new Client(\n      typeof rpc === 'function'\n        ? { fetchHandler: rpc }\n        : {\n            ...rpc,\n            service: rpc?.service ?? pds,\n            fetch: rpc?.fetch ?? safeFetch,\n          },\n    )\n    const proofBytes = await client\n      .call(com.atproto.sync.getRecord, {\n        did,\n        collection: uri.collection as l.NsidString,\n        rkey: uri.rkey as l.RecordKeyString,\n      })\n      .catch((err) => {\n        throw new RecordResolutionError('Could not fetch record proof', {\n          cause: err,\n        })\n      })\n    const verified = await verifyRecordProof(proofBytes, {\n      uri: AtUri.make(did, uri.collection, uri.rkey),\n      signingKey,\n    })\n    return verified\n  }\n}\n\nexport const resolveRecord = buildRecordResolver()\n\nexport const safeFetch = safeFetchWrap({\n  allowIpHost: false,\n  allowImplicitRedirect: true,\n  responseMaxSize: (1024 + 10) * 1024, // 1MB + 10kB, just a bit larger than max record size\n})\n\nexport class RecordResolutionError extends Error {\n  constructor(message?: string, options?: ErrorOptions) {\n    super(message, options)\n    this.name = 'RecordResolutionError'\n  }\n}\n\nasync function getDidFromUri(\n  uri: AtUri,\n  { idResolver }: { idResolver: IdResolver },\n): Promise<DidString> {\n  if (l.isDidString(uri.host)) {\n    return uri.host\n  }\n\n  const resolved = await idResolver.handle.resolve(uri.host)\n  if (!resolved || !l.isDidString(resolved)) {\n    throw new RecordResolutionError('Could not resolve handle found in AT-URI')\n  }\n\n  return resolved\n}\n\nasync function verifyRecordProof(\n  proofBytes: Uint8Array,\n  { uri, signingKey }: { uri: AtUri; signingKey: string },\n): Promise<RecordResolution> {\n  const { root, blocks } = await readCarWithRoot(proofBytes).catch((err) => {\n    throw new RecordResolutionError('Malformed record proof', { cause: err })\n  })\n  const blockstore = new MemoryBlockstore(blocks)\n  const commit = await blockstore.readObj(root, repoDef.commit).catch((err) => {\n    throw new RecordResolutionError('Invalid commit in record proof', {\n      cause: err,\n    })\n  })\n  if (commit.did !== uri.host) {\n    throw new RecordResolutionError(`Invalid repo did: ${commit.did}`)\n  }\n  const validSig = await verifyCommitSig(commit, signingKey)\n  if (!validSig) {\n    throw new RecordResolutionError(\n      `Invalid signature on commit: ${root.toString()}`,\n    )\n  }\n  const mst = MST.load(blockstore, commit.data)\n  const cid = await mst.get(`${uri.collection}/${uri.rkey}`)\n  if (!cid) {\n    throw new RecordResolutionError('Record not found in proof')\n  }\n  const record = (await blockstore.readRecord(cid)) as LexMap\n  return { commit, uri, cid, record }\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/src/util.ts",
    "content": "import { ensureValidDid } from '@atproto/syntax'\n\nexport function isValidDid(did: string) {\n  try {\n    ensureValidDid(did)\n    return true\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/tests/lexicon.test.ts",
    "content": "import { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env'\nimport { DidString, NSID } from '@atproto/syntax'\nimport {\n  AtprotoLexiconResolver,\n  buildLexiconResolver,\n  resolveLexiconDidAuthority,\n} from '../src/index.js'\n\nconst dnsEntries: [entry: string, ...result: string[][]][] = []\n\njest.mock('node:dns/promises', () => {\n  return {\n    resolveTxt: (entry: string) => {\n      const found = dnsEntries.find(([e]) => e === entry)\n      if (found) return found.slice(1)\n      return []\n    },\n  }\n})\n\ndescribe('Lexicon resolution', () => {\n  let network: TestNetworkNoAppView\n  let sc: SeedClient\n  let resolveLexicon: AtprotoLexiconResolver\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'lex_lexicon_resolution',\n    })\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    dnsEntries.push(['_lexicon.alice.example', [`did=${sc.dids.alice}`]])\n    resolveLexicon = buildLexiconResolver({\n      rpc: { fetch },\n      idResolver: network.pds.ctx.idResolver,\n    })\n  })\n\n  afterAll(async () => {\n    jest.unmock('node:dns/promises')\n    await network.close()\n  })\n\n  it('resolves Lexicon.', async () => {\n    const client = network.pds.getClient()\n    const lex = await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.alice, rkey: 'example.alice.name1' },\n      { id: 'example.alice.name1', lexicon: 1, defs: {} },\n      sc.getHeaders(sc.dids.alice),\n    )\n    const result = await resolveLexicon('example.alice.name1', {\n      forceRefresh: true,\n    })\n    expect(result.commit.did).toEqual(sc.dids.alice)\n    expect(result.cid.toString()).toEqual(lex.cid)\n    expect(result.uri.toString()).toEqual(lex.uri)\n    expect(result.nsid.toString()).toEqual('example.alice.name1')\n    expect(result.lexicon).toEqual({\n      $type: 'com.atproto.lexicon.schema',\n      id: 'example.alice.name1',\n      lexicon: 1,\n      defs: {},\n    })\n  })\n\n  it('fails on mismatched id.', async () => {\n    const client = network.pds.getClient()\n    await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.alice, rkey: 'example.alice.mismatch' },\n      { id: 'example.test1.mismatch.bad', lexicon: 1, defs: {} },\n      sc.getHeaders(sc.dids.alice),\n    )\n    await expect(\n      resolveLexicon('example.alice.mismatch', {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow(\n      'Lexicon schema record id (example.test1.mismatch.bad) does not match NSID (example.alice.mismatch)',\n    )\n  })\n\n  it('fails on missing DNS entry.', async () => {\n    const client = network.pds.getClient()\n    await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.bob, rkey: 'example.bob.name' },\n      { id: 'example.bob.name', lexicon: 1, defs: {} },\n      sc.getHeaders(sc.dids.bob),\n    )\n    await expect(\n      resolveLexicon('example.bob.name', {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow(\n      'Could not resolve a DID authority for NSID (example.bob.name)',\n    )\n  })\n\n  it('fails on missing record.', async () => {\n    await expect(\n      resolveLexicon('example.alice.missing', {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow('Could not resolve Lexicon schema record')\n  })\n\n  it('fails on bad verification.', async () => {\n    const client = network.pds.getClient()\n    const alicekey = await network.pds.ctx.actorStore.keypair(sc.dids.alice)\n    const bobkey = await network.pds.ctx.actorStore.keypair(sc.dids.bob)\n    await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.alice, rkey: 'example.alice.badsig' },\n      { id: 'example.alice.badsig', lexicon: 1, defs: {} },\n      sc.getHeaders(sc.dids.alice),\n    )\n    // switch alice's key away from the one used by her pds\n    await network.pds.ctx.plcClient.updateAtprotoKey(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      bobkey.did(),\n    )\n    await expect(\n      resolveLexicon('example.alice.badsig', {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow(\n      expect.objectContaining({\n        name: 'LexiconResolutionError',\n        message:\n          'Could not resolve Lexicon schema record (example.alice.badsig)',\n        cause: expect.objectContaining({\n          name: 'RecordResolutionError',\n          message: expect.stringContaining('Invalid signature on commit'),\n        }),\n      }),\n    )\n    // reset alice's key\n    await network.pds.ctx.plcClient.updateAtprotoKey(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      alicekey.did(),\n    )\n  })\n\n  it('fails on invalid Lexicon document.', async () => {\n    const client = network.pds.getClient()\n    await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.alice, rkey: 'example.alice.baddoc' },\n      { id: 'example.alice.baddoc', lexicon: 999, defs: {} },\n      sc.getHeaders(sc.dids.alice),\n    )\n    await expect(\n      resolveLexicon('example.alice.baddoc', {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow(\n      expect.objectContaining({\n        name: 'LexiconResolutionError',\n        message: 'Invalid Lexicon document (example.alice.baddoc)',\n        cause: expect.objectContaining({\n          name: 'LexValidationError',\n        }),\n      }),\n    )\n  })\n\n  it('resolves Lexicon based on override authority.', async () => {\n    const client = network.pds.getClient()\n    await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.alice, rkey: 'example.alice.override' },\n      {\n        id: 'example.alice.override',\n        lexicon: 1,\n        defs: { alice: { type: 'string' } },\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    const carolLex = await client.com.atproto.lexicon.schema.create(\n      { repo: sc.dids.carol, rkey: 'example.alice.override' },\n      {\n        id: 'example.alice.override',\n        lexicon: 1,\n        defs: { carol: { type: 'string' } },\n      },\n      sc.getHeaders(sc.dids.carol),\n    )\n    const result = await resolveLexicon('example.alice.override', {\n      didAuthority: sc.dids.carol as DidString,\n      forceRefresh: true,\n    })\n    expect(result.commit.did).toEqual(sc.dids.carol)\n    expect(result.cid.toString()).toEqual(carolLex.cid)\n    expect(result.uri.toString()).toEqual(carolLex.uri)\n    expect(result.nsid.toString()).toEqual('example.alice.override')\n    expect(result.lexicon).toEqual({\n      $type: 'com.atproto.lexicon.schema',\n      id: 'example.alice.override',\n      lexicon: 1,\n      defs: { carol: { type: 'string' } },\n    })\n  })\n\n  describe('DID authority', () => {\n    it('handles a simple DNS resolution', async () => {\n      dnsEntries.push(['_lexicon.simple.test', ['did=did:example:simpleDid']])\n      const did = await resolveLexiconDidAuthority('test.simple.name')\n      expect(did).toBe('did:example:simpleDid')\n    })\n\n    it('handles a noisy DNS resolution', async () => {\n      dnsEntries.push([\n        '_lexicon.noisy.test',\n        ['blah blah blah'],\n        ['did:example:fakeDid'],\n        ['atproto=did:example:fakeDid'],\n        ['did=did:example:noisyDid'],\n        [\n          'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',\n          'apsodfiuweproiasudfpoasidfu',\n        ],\n      ])\n      const did = await resolveLexiconDidAuthority('test.noisy.name')\n      expect(did).toBe('did:example:noisyDid')\n    })\n\n    it('handles a bad DNS resolution', async () => {\n      dnsEntries.push([\n        '_lexicon.bad.test',\n        ['blah blah blah'],\n        ['did:example:fakeDid'],\n        ['atproto=did:example:fakeDid'],\n        [\n          'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',\n          'apsodfiuweproiasudfpoasidfu',\n        ],\n      ])\n      const did = await resolveLexiconDidAuthority('test.bad.name')\n      expect(did).toBeUndefined()\n    })\n\n    it('throws on multiple dids under same domain', async () => {\n      dnsEntries.push([\n        '_lexicon.bad.test',\n        ['did=did:example:firstDid'],\n        ['did=did:example:secondDid'],\n      ])\n      const did = await resolveLexiconDidAuthority('test.multi.name')\n      expect(did).toBeUndefined()\n    })\n\n    it('fails on invalid NSID', async () => {\n      // @ts-expect-error testing invalid input\n      await expect(resolveLexiconDidAuthority('not an nsid')).rejects.toThrow(\n        'Disallowed characters in NSID',\n      )\n    })\n\n    it('fails on invalid DID result', async () => {\n      dnsEntries.push(['_lexicon.invalid.test', ['did=not:a:did']])\n      const did = await resolveLexiconDidAuthority('test.invalid.name')\n      expect(did).toBeUndefined()\n    })\n\n    it('accepts NSID object', async () => {\n      const did = await resolveLexiconDidAuthority(\n        NSID.parse('test.simple.name'),\n      )\n      expect(did).toBe('did:example:simpleDid')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lexicon-resolver/tests/record.test.ts",
    "content": "import assert from 'node:assert'\nimport { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env'\nimport { AtUriString, l } from '@atproto/lex'\nimport { encode } from '@atproto/lex-cbor'\nimport { AtprotoRecordResolver, buildRecordResolver } from '../src/index.js'\n\ndescribe('Record resolution', () => {\n  let network: TestNetworkNoAppView\n  let sc: SeedClient\n  let resolveRecord: AtprotoRecordResolver\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'lex_record_resolution',\n    })\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    resolveRecord = buildRecordResolver({\n      rpc: { fetch },\n      idResolver: network.pds.ctx.idResolver,\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('resolves record by AT-URI object.', async () => {\n    const post = await sc.post(sc.dids.alice, 'post1')\n    const result = await resolveRecord(post.ref.uri, {\n      forceRefresh: true,\n    })\n    expect(result.commit.did).toEqual(sc.dids.alice)\n    expect(result.cid.toString()).toEqual(post.ref.cidStr)\n    expect(result.uri.toString()).toEqual(post.ref.uriStr)\n    expect(result.record.text).toEqual('post1')\n  })\n\n  it('resolves record by AT-URI string.', async () => {\n    const post = await sc.post(sc.dids.alice, 'post2')\n    assert(l.isAtUriString(post.ref.uriStr))\n    const result = await resolveRecord(post.ref.uriStr, {\n      forceRefresh: true,\n    })\n    expect(result.commit.did).toEqual(sc.dids.alice)\n    expect(result.cid.toString()).toEqual(post.ref.cidStr)\n    expect(result.uri.toString()).toEqual(post.ref.uriStr)\n    expect(result.record.text).toEqual('post2')\n  })\n\n  it(\"does not resolve record that doesn't exist.\", async () => {\n    await expect(\n      resolveRecord(`at://${sc.dids.alice}/app.bsky.feed.post/2222222222222`, {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow('Record not found')\n  })\n\n  it('does not resolve record with bad commit signature.', async () => {\n    const alicekey = await network.pds.ctx.actorStore.keypair(sc.dids.alice)\n    const bobkey = await network.pds.ctx.actorStore.keypair(sc.dids.bob)\n    const post = await sc.post(sc.dids.alice, 'post3')\n    // switch alice's key away from the one used by her pds\n    await network.pds.ctx.plcClient.updateAtprotoKey(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      bobkey.did(),\n    )\n    await expect(\n      resolveRecord(post.ref.uri, {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow('Invalid signature on commit')\n    // reset alice's key\n    await network.pds.ctx.plcClient.updateAtprotoKey(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      alicekey.did(),\n    )\n  })\n\n  it('does not resolve record with corrupted CAR block.', async () => {\n    const post = await sc.post(sc.dids.alice, 'post4')\n    const badCbor = encode({})\n    await network.pds.ctx.actorStore.transact(sc.dids.alice, (txn) =>\n      txn.repo.db.db\n        .updateTable('repo_block')\n        .set({\n          content: badCbor,\n          size: badCbor.byteLength,\n        })\n        .where('cid', '=', post.ref.cidStr)\n        .execute(),\n    )\n    await expect(\n      resolveRecord(post.ref.uri, {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow('Malformed record proof')\n  })\n\n  it('does not resolve record with missing signing key.', async () => {\n    const post = await sc.post(sc.dids.alice, 'post5')\n    await network.pds.ctx.plcClient.updateData(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      (doc) => {\n        doc.verificationMethods = {\n          not_atproto: doc.verificationMethods.atproto,\n        }\n        return doc\n      },\n    )\n    await expect(\n      resolveRecord(post.ref.uri, {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow('Incomplete DID identity data: missing signing key')\n    // reset alice's key\n    await network.pds.ctx.plcClient.updateData(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      (doc) => {\n        doc.verificationMethods = {\n          atproto: doc.verificationMethods.not_atproto,\n        }\n        return doc\n      },\n    )\n  })\n\n  it('does not resolve record with missing pds.', async () => {\n    const post = await sc.post(sc.dids.alice, 'post6')\n    await network.pds.ctx.plcClient.updateData(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      (doc) => {\n        doc.services = {\n          not_atproto_pds: doc.services.atproto_pds,\n        }\n        return doc\n      },\n    )\n    await expect(\n      resolveRecord(post.ref.uri, {\n        forceRefresh: true,\n      }),\n    ).rejects.toThrow('Incomplete DID identity data: missing pds')\n    // reset alice's pds\n    await network.pds.ctx.plcClient.updateData(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      (doc) => {\n        doc.services = {\n          atproto_pds: doc.services.not_atproto_pds,\n        }\n        return doc\n      },\n    )\n  })\n\n  it('resolves record despite missing at:// handle.', async () => {\n    const post = await sc.post(sc.dids.alice, 'post7')\n    await network.pds.ctx.plcClient.updateData(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      (doc) => {\n        doc.alsoKnownAs = doc.alsoKnownAs.map((aka) =>\n          aka.replace('at://', 'notat://'),\n        )\n        return doc\n      },\n    )\n    const result = await resolveRecord(post.ref.uriStr as AtUriString, {\n      forceRefresh: true,\n    })\n    expect(result.commit.did).toEqual(sc.dids.alice)\n    expect(result.cid.toString()).toEqual(post.ref.cidStr)\n    expect(result.uri.toString()).toEqual(post.ref.uriStr)\n    expect(result.record.text).toEqual('post7')\n    // reset alice's handle\n    await network.pds.ctx.plcClient.updateData(\n      sc.dids.alice,\n      network.pds.ctx.plcRotationKey,\n      (doc) => {\n        doc.alsoKnownAs = doc.alsoKnownAs.map((aka) =>\n          aka.replace('notat://', 'at://'),\n        )\n        return doc\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "packages/lexicon-resolver/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../tsconfig/node.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/lexicon-resolver/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/oauth/jwk/CHANGELOG.md",
    "content": "# @atproto/jwk\n\n## 0.6.0\n\n### Minor Changes\n\n- [#4103](https://github.com/bluesky-social/atproto/pull/4103) [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update key matching algorithm to support `key_ops`\n\n- [#4103](https://github.com/bluesky-social/atproto/pull/4103) [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Only allow `\"use\"` claims in public jwk\n\n### Patch Changes\n\n- [#4103](https://github.com/bluesky-social/atproto/pull/4103) [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Silently ignore invalid JWKs from JSON Web Key Set (as per spec)\n\n- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make the `jwk` property of `Key` instances public\n\n- [#4103](https://github.com/bluesky-social/atproto/pull/4103) [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid using `revoked` and inactive keys from JWK sets\n\n## 0.5.0\n\n### Minor Changes\n\n- [#4101](https://github.com/bluesky-social/atproto/pull/4101) [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent inconsistent use of `use` and `key_ops` in JWK\n\n### Patch Changes\n\n- [#4101](https://github.com/bluesky-social/atproto/pull/4101) [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove requirement for JWK to define either `use` or `key_ops`.\n\n## 0.4.0\n\n### Minor Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove export of internal utilities\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return object instead of array as result of `findPrivateKey`\n\n### Patch Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `jwkPrivateSchema` to ensure a key is private\n\n## 0.3.0\n\n### Minor Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `findKey` to `findPrivateKey` to better reflect the method's behavior\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3879](https://github.com/bluesky-social/atproto/pull/3879) [`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly validate JWK `htu` claim by enforcing URL without query or fragment\n\n## 0.1.5\n\n### Patch Changes\n\n- [#3747](https://github.com/bluesky-social/atproto/pull/3747) [`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc) Thanks [@devinivy](https://github.com/devinivy)! - Fix typo in Keyset.findKey error message\n\n## 0.1.4\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly support locales with 3 chars (Asturian)\n\n## 0.1.3\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2879](https://github.com/bluesky-social/atproto/pull/2879) [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Mark jwk key fields as readonly\n\n- [#2879](https://github.com/bluesky-social/atproto/pull/2879) [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unsafe type casting during JWT verification\n\n- [#2879](https://github.com/bluesky-social/atproto/pull/2879) [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow (passthrough) unknown properties in JWT payload & headers\n\n- [#2879](https://github.com/bluesky-social/atproto/pull/2879) [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose ValidationError to allow for proper error handling\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow build from Parcel\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n"
  },
  {
    "path": "packages/oauth/jwk/package.json",
    "content": "{\n  \"name\": \"@atproto/jwk\",\n  \"version\": \"0.6.0\",\n  \"license\": \"MIT\",\n  \"description\": \"A library for working with JSON Web Keys (JWKs) in TypeScript. This is meant to be extended by environment-specific libraries like @atproto/jwk-jose.\",\n  \"keywords\": [\n    \"atproto\",\n    \"jwk\",\n    \"jwks\",\n    \"jwt\",\n    \"json web key\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/jwk\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"multiformats\": \"^9.9.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/alg.ts",
    "content": "import { JwkError } from './errors.js'\nimport { JwkBase, isEncKeyUsage, isSigKeyUsage } from './jwk.js'\n\n// Copy variable to prevent bundlers from automatically polyfilling \"process\" (e.g. parcel)\nconst { process } = globalThis\nconst IS_NODE_RUNTIME =\n  typeof process !== 'undefined' && typeof process?.versions?.node === 'string'\n\nexport function* jwkAlgorithms(jwk: JwkBase): Generator<string, void, unknown> {\n  // Ed25519, Ed448, and secp256k1 always have \"alg\"\n\n  if (typeof jwk.alg === 'string') {\n    yield jwk.alg\n    return\n  }\n\n  switch (jwk.kty) {\n    case 'EC': {\n      if (jwkSupportsEnc(jwk)) {\n        yield 'ECDH-ES'\n        yield 'ECDH-ES+A128KW'\n        yield 'ECDH-ES+A192KW'\n        yield 'ECDH-ES+A256KW'\n      }\n\n      if (jwkSupportsSig(jwk)) {\n        const crv = 'crv' in jwk ? jwk.crv : undefined\n        switch (crv) {\n          case 'P-256':\n          case 'P-384':\n            yield `ES${crv.slice(-3)}`\n            break\n          case 'P-521':\n            yield 'ES512'\n            break\n          case 'secp256k1':\n            if (IS_NODE_RUNTIME) yield 'ES256K'\n            break\n          default:\n            throw new JwkError(`Unsupported crv \"${crv}\"`)\n        }\n      }\n\n      return\n    }\n\n    case 'OKP': {\n      if (!jwk.use) throw new JwkError('Missing \"use\" Parameter value')\n      yield 'ECDH-ES'\n      yield 'ECDH-ES+A128KW'\n      yield 'ECDH-ES+A192KW'\n      yield 'ECDH-ES+A256KW'\n      return\n    }\n\n    case 'RSA': {\n      if (jwkSupportsEnc(jwk)) {\n        yield 'RSA-OAEP'\n        yield 'RSA-OAEP-256'\n        yield 'RSA-OAEP-384'\n        yield 'RSA-OAEP-512'\n        if (IS_NODE_RUNTIME) yield 'RSA1_5'\n      }\n\n      if (jwkSupportsSig(jwk)) {\n        yield 'PS256'\n        yield 'PS384'\n        yield 'PS512'\n        yield 'RS256'\n        yield 'RS384'\n        yield 'RS512'\n      }\n\n      return\n    }\n\n    case 'oct': {\n      if (jwkSupportsEnc(jwk)) {\n        yield 'A128GCMKW'\n        yield 'A192GCMKW'\n        yield 'A256GCMKW'\n        yield 'A128KW'\n        yield 'A192KW'\n        yield 'A256KW'\n      }\n\n      if (jwkSupportsSig(jwk)) {\n        yield 'HS256'\n        yield 'HS384'\n        yield 'HS512'\n      }\n\n      return\n    }\n\n    default:\n      throw new JwkError(`Unsupported kty \"${jwk.kty}\"`)\n  }\n}\n\nfunction jwkSupportsEnc(jwk: JwkBase): boolean {\n  return (\n    jwk.key_ops?.some(isEncKeyUsage) ?? (jwk.use == null || jwk.use === 'enc')\n  )\n}\n\nfunction jwkSupportsSig(jwk: JwkBase): boolean {\n  return (\n    jwk.key_ops?.some(isSigKeyUsage) ?? (jwk.use == null || jwk.use === 'sig')\n  )\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/errors.ts",
    "content": "export type ErrorOptions = { cause?: unknown }\n\nexport const ERR_JWKS_NO_MATCHING_KEY = 'ERR_JWKS_NO_MATCHING_KEY'\nexport const ERR_JWK_INVALID = 'ERR_JWK_INVALID'\nexport const ERR_JWK_NOT_FOUND = 'ERR_JWK_NOT_FOUND'\nexport const ERR_JWT_INVALID = 'ERR_JWT_INVALID'\nexport const ERR_JWT_CREATE = 'ERR_JWT_CREATE'\nexport const ERR_JWT_VERIFY = 'ERR_JWT_VERIFY'\n\nexport class JwkError extends TypeError {\n  constructor(\n    message = 'JWK error',\n    public readonly code = ERR_JWK_INVALID,\n    options?: ErrorOptions,\n  ) {\n    super(message, options)\n  }\n}\n\nexport class JwtCreateError extends Error {\n  constructor(\n    message = 'Unable to create JWT',\n    public readonly code = ERR_JWT_CREATE,\n    options?: ErrorOptions,\n  ) {\n    super(message, options)\n  }\n\n  static from(cause: unknown, code?: string, message?: string): JwtCreateError {\n    if (cause instanceof JwtCreateError) return cause\n    if (cause instanceof JwkError) {\n      return new JwtCreateError(message, cause.code, { cause })\n    }\n\n    return new JwtCreateError(message, code, { cause })\n  }\n}\n\nexport class JwtVerifyError extends Error {\n  constructor(\n    message = 'Invalid JWT',\n    public readonly code = ERR_JWT_VERIFY,\n    options?: ErrorOptions,\n  ) {\n    super(message, options)\n  }\n\n  static from(cause: unknown, code?: string, message?: string): JwtVerifyError {\n    if (cause instanceof JwtVerifyError) return cause\n    if (cause instanceof JwkError) {\n      return new JwtVerifyError(message, cause.code, { cause })\n    }\n\n    return new JwtVerifyError(message, code, { cause })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/index.ts",
    "content": "// Since we expose zod schemas, let's expose ZodError (under a generic name) so\n// that dependents can catch schema parsing errors without requiring an explicit\n// dependency on zod, or risking a conflict in case of mismatching zob versions.\nexport { ZodError as ValidationError } from 'zod'\n\nexport * from './alg.js'\nexport * from './errors.js'\nexport * from './jwk.js'\nexport * from './jwks.js'\nexport * from './jwt-decode.js'\nexport * from './jwt-verify.js'\nexport * from './jwt.js'\nexport * from './key.js'\nexport * from './keyset.js'\n"
  },
  {
    "path": "packages/oauth/jwk/src/jwk.ts",
    "content": "import { z } from 'zod'\nimport { isLastOccurrence } from './util'\n\nexport const PUBLIC_KEY_USAGE = ['verify', 'encrypt', 'wrapKey'] as const\nexport const publicKeyUsageSchema = z.enum(PUBLIC_KEY_USAGE)\nexport type PublicKeyUsage = (typeof PUBLIC_KEY_USAGE)[number]\nexport function isPublicKeyUsage(usage: unknown): usage is PublicKeyUsage {\n  return (PUBLIC_KEY_USAGE as readonly unknown[]).includes(usage)\n}\n\n/**\n * Determines if the given key usage is consistent for \"sig\" (signature) public\n * key use.\n */\nexport function isSigKeyUsage(v: KeyUsage) {\n  return v === 'verify'\n}\n\n/**\n * Determines if the given key usage is consistent for \"enc\" (encryption) public\n * key use.\n *\n * > When a key is used to wrap another key and a public key use\n * > designation for the first key is desired, the \"enc\" (encryption)\n * > key use value is used, since key wrapping is a kind of encryption.\n * > The \"enc\" value is also to be used for public keys used for key\n * > agreement operations.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.2}\n */\nexport function isEncKeyUsage(v: KeyUsage) {\n  return v === 'encrypt' || v === 'wrapKey'\n}\n\nexport const PRIVATE_KEY_USAGE = [\n  'sign',\n  'decrypt',\n  'unwrapKey',\n  'deriveKey',\n  'deriveBits',\n] as const\nexport const privateKeyUsageSchema = z.enum(PRIVATE_KEY_USAGE)\nexport type PrivateKeyUsage = (typeof PRIVATE_KEY_USAGE)[number]\nexport function isPrivateKeyUsage(usage: unknown): usage is PrivateKeyUsage {\n  return (PRIVATE_KEY_USAGE as readonly unknown[]).includes(usage)\n}\n\nexport const KEY_USAGE = [...PRIVATE_KEY_USAGE, ...PUBLIC_KEY_USAGE] as const\nexport const keyUsageSchema = z.enum(KEY_USAGE)\nexport type KeyUsage = (typeof KEY_USAGE)[number]\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4 JSON Web Key (JWK) Format}\n * @see {@link https://www.iana.org/assignments/jose/jose.xhtml#web-key-parameters IANA \"JSON Web Key Parameters\" registry}\n */\nconst jwkBaseSchema = z.object({\n  kty: z.string().min(1),\n  alg: z.string().min(1).optional(),\n  kid: z.string().min(1).optional(),\n  use: z.enum(['sig', 'enc']).optional(),\n  key_ops: z\n    .array(keyUsageSchema)\n    .min(1, { message: 'At least one key usage must be specified' })\n    // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3\n    // > Duplicate key operation values MUST NOT be present in the array.\n    .refine((ops) => ops.every(isLastOccurrence), {\n      message: 'key_ops must not contain duplicates',\n    })\n    .optional(),\n\n  x5c: z.array(z.string()).optional(), // X.509 Certificate Chain\n  x5t: z.string().min(1).optional(), // X.509 Certificate SHA-1 Thumbprint\n  'x5t#S256': z.string().min(1).optional(), // X.509 Certificate SHA-256 Thumbprint\n  x5u: z.string().url().optional(), // X.509 URL\n\n  // https://www.w3.org/TR/webcrypto/\n  ext: z.boolean().optional(), // Extractable\n\n  // Federation Historical Keys Response\n  // https://openid.net/specs/openid-federation-1_0.html#name-federation-historical-keys-res\n  iat: z.number().int().optional(), // Issued At (timestamp)\n  exp: z.number().int().optional(), // Expiration Time (timestamp)\n  nbf: z.number().int().optional(), // Not Before (timestamp)\n  revoked: z //  properties of the revocation\n    .object({\n      revoked_at: z.number().int(),\n      reason: z.string().optional(),\n    })\n    .optional(),\n})\n\nexport type JwkBase = z.infer<typeof jwkBaseSchema>\n\nconst jwkRsaKeySchema = jwkBaseSchema.extend({\n  kty: z.literal('RSA'),\n  alg: z\n    .enum(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])\n    .optional(),\n\n  n: z.string().min(1), // Modulus\n  e: z.string().min(1), // Exponent\n\n  d: z.string().min(1).optional(), // Private Exponent\n  p: z.string().min(1).optional(), // First Prime Factor\n  q: z.string().min(1).optional(), // Second Prime Factor\n  dp: z.string().min(1).optional(), // First Factor CRT Exponent\n  dq: z.string().min(1).optional(), // Second Factor CRT Exponent\n  qi: z.string().min(1).optional(), // First CRT Coefficient\n  oth: z\n    .array(\n      z.object({\n        r: z.string().optional(),\n        d: z.string().optional(),\n        t: z.string().optional(),\n      }),\n    )\n    .min(1)\n    .optional(), // Other Primes Info\n})\n\nconst jwkEcKeySchema = jwkBaseSchema.extend({\n  kty: z.literal('EC'),\n  alg: z.enum(['ES256', 'ES384', 'ES512']).optional(),\n  crv: z.enum(['P-256', 'P-384', 'P-521']),\n\n  x: z.string().min(1),\n  y: z.string().min(1),\n\n  d: z.string().min(1).optional(), // ECC Private Key\n})\n\nconst jwkEcSecp256k1KeySchema = jwkBaseSchema.extend({\n  kty: z.literal('EC'),\n  alg: z.enum(['ES256K']).optional(),\n  crv: z.enum(['secp256k1']),\n\n  x: z.string().min(1),\n  y: z.string().min(1),\n\n  d: z.string().min(1).optional(), // ECC Private Key\n})\n\nconst jwkOkpKeySchema = jwkBaseSchema.extend({\n  kty: z.literal('OKP'),\n  alg: z.enum(['EdDSA']).optional(),\n  crv: z.enum(['Ed25519', 'Ed448']),\n\n  x: z.string().min(1),\n  d: z.string().min(1).optional(), // ECC Private Key\n})\n\nconst jwkSymKeySchema = jwkBaseSchema.extend({\n  kty: z.literal('oct'), // Octet Sequence (used to represent symmetric keys)\n  alg: z.enum(['HS256', 'HS384', 'HS512']).optional(),\n\n  k: z.string(), // Key Value (base64url encoded)\n})\n\n/**\n * Zod parser for known JWK types\n */\nexport const jwkSchema = z\n  .union([\n    jwkRsaKeySchema,\n    jwkEcKeySchema,\n    jwkEcSecp256k1KeySchema,\n    jwkOkpKeySchema,\n    jwkSymKeySchema,\n  ])\n  // @TODO These rules should be applied to jwkBaseSchema, but Zod 3 doesn't\n  // support extending refined schemas. Move these to the base schema when we\n  // upgrade to Zod 4.\n  .refine(\n    // https://datatracker.ietf.org/doc/html/rfc7517#section-4.2\n    // > The \"use\" (public key use) parameter identifies the intended use of the\n    // > public key\n    (k): boolean => k.use == null || isPublicJwk(k),\n    {\n      message: '\"use\" can only be used with public keys',\n      path: ['use'],\n    },\n  )\n  .refine(\n    (k): boolean => !k.key_ops?.some(isPrivateKeyUsage) || isPrivateJwk(k),\n    {\n      message: 'private key usage not allowed for public keys',\n      path: ['key_ops'],\n    },\n  )\n  .refine(\n    // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3\n    // > The \"use\" and \"key_ops\" JWK members SHOULD NOT be used together;\n    // > however, if both are used, the information they convey MUST be\n    // > consistent.\n    (k): boolean =>\n      k.use == null ||\n      k.key_ops == null ||\n      (k.use === 'sig' && k.key_ops.every(isSigKeyUsage)) ||\n      (k.use === 'enc' && k.key_ops.every(isEncKeyUsage)),\n    {\n      message: '\"key_ops\" must be consistent with \"use\"',\n      path: ['key_ops'],\n    },\n  )\n\nexport type Jwk = z.output<typeof jwkSchema>\n\n/** @deprecated use {@link jwkSchema} instead */\nexport const jwkValidator = jwkSchema\n\nexport const jwkPubSchema = jwkSchema\n  .refine(hasKid, {\n    message: '\"kid\" is required',\n    path: ['kid'],\n  })\n  // @NOTE for legacy reasons, we don't impose the presence of either \"use\" or \"key_ops\"\n  .refine(isPublicJwk, {\n    message: 'private key not allowed',\n  })\n  .refine((k): boolean => !k.key_ops || k.key_ops.every(isPublicKeyUsage), {\n    message: '\"key_ops\" must not contain private key usage for public keys',\n    path: ['key_ops'],\n  })\n\nexport type PublicJwk = z.output<typeof jwkPubSchema>\n\nexport const jwkPrivateSchema = jwkSchema\n  // @NOTE we don't impose the presence of \"kid\"\n  .refine(isPrivateJwk, {\n    message: 'private key required',\n  })\n\nexport type PrivateJwk = z.output<typeof jwkPrivateSchema>\n\nexport function hasKid<J extends object>(\n  jwk: J,\n): jwk is J & { kid: NonNullable<unknown> } {\n  return 'kid' in jwk && jwk.kid != null\n}\n\nexport function hasSharedSecretJwk<J extends object>(\n  jwk: J,\n): jwk is J & { k: NonNullable<unknown> } {\n  return 'k' in jwk && jwk.k != null\n}\n\nexport function hasPrivateSecretJwk<J extends object>(\n  jwk: J,\n): jwk is J & { d: NonNullable<unknown> } {\n  return 'd' in jwk && jwk.d != null\n}\n\nexport function isPrivateJwk<J extends object>(jwk: J) {\n  return hasPrivateSecretJwk(jwk) || hasSharedSecretJwk(jwk)\n}\n\nexport function isPublicJwk<J extends object>(\n  jwk: J,\n): jwk is Extract<\n  Exclude<J, { k: NonNullable<unknown> }>,\n  { d?: NonNullable<unknown> }\n> & { d?: never } {\n  return !hasPrivateSecretJwk(jwk) && !hasSharedSecretJwk(jwk)\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/jwks.ts",
    "content": "import { z } from 'zod'\nimport { jwkPubSchema, jwkSchema } from './jwk.js'\n\n/**\n * JSON Web Key Set schema. The keys set, in this context, represents a\n * collection of JSON Web Keys (JWKs), that can be both public and private.\n */\nexport const jwksSchema = z.object({\n  keys: z.array(z.unknown()).transform((input) => {\n    // > Implementations SHOULD ignore JWKs within a JWK Set that use \"kty\"\n    // > (key type) values that are not understood by them, that are missing\n    // > required members, or for which values are out of the supported\n    // > ranges.\n    return input\n      .map((item) => jwkSchema.safeParse(item))\n      .filter((res) => res.success)\n      .map((res) => res.data)\n  }),\n})\n\nexport type Jwks = z.output<typeof jwksSchema>\n\n/**\n * Public JSON Web Key Set schema.\n */\nexport const jwksPubSchema = z.object({\n  keys: z.array(z.unknown()).transform((input) => {\n    // > Implementations SHOULD ignore JWKs within a JWK Set that use \"kty\"\n    // > (key type) values that are not understood by them, that are missing\n    // > required members, or for which values are out of the supported\n    // > ranges.\n    return input\n      .map((item) => jwkPubSchema.safeParse(item))\n      .filter((res) => res.success)\n      .map((res) => res.data)\n  }),\n})\n\nexport type JwksPub = z.output<typeof jwksPubSchema>\n"
  },
  {
    "path": "packages/oauth/jwk/src/jwt-decode.ts",
    "content": "import { ERR_JWT_INVALID, JwtVerifyError } from './errors.js'\nimport {\n  JwtHeader,\n  JwtPayload,\n  jwtHeaderSchema,\n  jwtPayloadSchema,\n} from './jwt.js'\nimport { parseB64uJson } from './util.js'\n\nexport function unsafeDecodeJwt(jwt: string): {\n  header: JwtHeader\n  payload: JwtPayload\n} {\n  const { 0: headerEnc, 1: payloadEnc, length } = jwt.split('.')\n  if (length > 3 || length < 2) {\n    throw new JwtVerifyError(undefined, ERR_JWT_INVALID)\n  }\n\n  const header = jwtHeaderSchema.parse(parseB64uJson(headerEnc!))\n  if (length === 2 && header?.alg !== 'none') {\n    throw new JwtVerifyError(undefined, ERR_JWT_INVALID)\n  }\n\n  const payload = jwtPayloadSchema.parse(parseB64uJson(payloadEnc!))\n\n  return { header, payload }\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/jwt-verify.ts",
    "content": "import { JwtHeader, JwtPayload } from './jwt.js'\nimport { RequiredKey } from './util.js'\n\nexport type VerifyOptions<C extends string = never> = {\n  audience?: string | readonly string[]\n  /** in seconds */\n  clockTolerance?: number\n  issuer?: string | readonly string[]\n  /** in seconds */\n  maxTokenAge?: number\n  subject?: string\n  typ?: string\n  currentDate?: Date\n  requiredClaims?: readonly C[]\n}\n\nexport type VerifyResult<C extends string = never> = {\n  payload: RequiredKey<JwtPayload, C>\n  protectedHeader: JwtHeader\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/jwt.ts",
    "content": "import { z } from 'zod'\nimport { jwkPubSchema } from './jwk.js'\nimport { jwtCharsRefinement, segmentedStringRefinementFactory } from './util.js'\n\nexport const signedJwtSchema = z\n  .string()\n  .superRefine(jwtCharsRefinement)\n  .superRefine(segmentedStringRefinementFactory(3))\n\nexport type SignedJwt = z.infer<typeof signedJwtSchema>\nexport const isSignedJwt = (data: unknown): data is SignedJwt =>\n  signedJwtSchema.safeParse(data).success\n\nexport const unsignedJwtSchema = z\n  .string()\n  .superRefine(jwtCharsRefinement)\n  .superRefine(segmentedStringRefinementFactory(2))\n\nexport type UnsignedJwt = z.infer<typeof unsignedJwtSchema>\nexport const isUnsignedJwt = (data: unknown): data is UnsignedJwt =>\n  unsignedJwtSchema.safeParse(data).success\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4}\n */\nexport const jwtHeaderSchema = z\n  .object({\n    /** \"alg\" (Algorithm) Header Parameter */\n    alg: z.string(),\n    /** \"jku\" (JWK Set URL) Header Parameter */\n    jku: z.string().url().optional(),\n    /** \"jwk\" (JSON Web Key) Header Parameter */\n    jwk: z\n      .object({\n        kty: z.string(),\n        crv: z.string().optional(),\n        x: z.string().optional(),\n        y: z.string().optional(),\n        e: z.string().optional(),\n        n: z.string().optional(),\n      })\n      .optional(),\n    /** \"kid\" (Key ID) Header Parameter */\n    kid: z.string().optional(),\n    /** \"x5u\" (X.509 URL) Header Parameter */\n    x5u: z.string().optional(),\n    /** \"x5c\" (X.509 Certificate Chain) Header Parameter */\n    x5c: z.array(z.string()).optional(),\n    /** \"x5t\" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */\n    x5t: z.string().optional(),\n    /** \"x5t#S256\" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */\n    'x5t#S256': z.string().optional(),\n    /** \"typ\" (Type) Header Parameter */\n    typ: z.string().optional(),\n    /** \"cty\" (Content Type) Header Parameter */\n    cty: z.string().optional(),\n    /** \"crit\" (Critical) Header Parameter */\n    crit: z.array(z.string()).optional(),\n  })\n  .passthrough()\n\nexport type JwtHeader = z.infer<typeof jwtHeaderSchema>\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc9449.html#section-4.2-4.6}\n * @see {@link https://www.rfc-editor.org/rfc/rfc9110#section-7.1}\n */\nexport const htuSchema = z.string().superRefine((value, ctx) => {\n  try {\n    const url = new URL(value)\n    if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Only http: and https: protocols are allowed',\n      })\n    }\n\n    if (url.username || url.password) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Credentials not allowed',\n      })\n    }\n\n    if (url.search) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Query string not allowed',\n      })\n    }\n\n    if (url.hash) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Fragment not allowed',\n      })\n    }\n  } catch (err) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.invalid_string,\n      validation: 'url',\n    })\n  }\n\n  return value\n})\n\n// https://www.iana.org/assignments/jwt/jwt.xhtml\nexport const jwtPayloadSchema = z\n  .object({\n    iss: z.string().optional(),\n    aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(),\n    sub: z.string().optional(),\n    exp: z.number().int().optional(),\n    nbf: z.number().int().optional(),\n    iat: z.number().int().optional(),\n    jti: z.string().optional(),\n    htm: z.string().optional(),\n    htu: htuSchema.optional(),\n    ath: z.string().optional(),\n    acr: z.string().optional(),\n    azp: z.string().optional(),\n    amr: z.array(z.string()).optional(),\n    // https://datatracker.ietf.org/doc/html/rfc7800\n    cnf: z\n      .object({\n        kid: z.string().optional(), // Key ID\n        jwk: jwkPubSchema.optional(), // JWK\n        jwe: z.string().optional(), // Encrypted key\n        jku: z.string().url().optional(), // JWK Set URI (\"kid\" should also be provided)\n\n        // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1\n        jkt: z.string().optional(),\n\n        // https://datatracker.ietf.org/doc/html/rfc8705\n        'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint\n\n        // https://datatracker.ietf.org/doc/html/rfc9203\n        osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation\n      })\n      .optional(),\n\n    client_id: z.string().optional(),\n\n    scope: z.string().optional(),\n    nonce: z.string().optional(),\n\n    at_hash: z.string().optional(),\n    c_hash: z.string().optional(),\n    s_hash: z.string().optional(),\n    auth_time: z.number().int().optional(),\n\n    // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\n\n    // OpenID: \"profile\" scope\n    name: z.string().optional(),\n    family_name: z.string().optional(),\n    given_name: z.string().optional(),\n    middle_name: z.string().optional(),\n    nickname: z.string().optional(),\n    preferred_username: z.string().optional(),\n    gender: z.string().optional(), // OpenID only defines \"male\" and \"female\" without forbidding other values\n    picture: z.string().url().optional(),\n    profile: z.string().url().optional(),\n    website: z.string().url().optional(),\n    birthdate: z\n      .string()\n      .regex(/\\d{4}-\\d{2}-\\d{2}/) // YYYY-MM-DD\n      .optional(),\n    zoneinfo: z\n      .string()\n      .regex(/^[A-Za-z0-9_/]+$/)\n      .optional(),\n    locale: z\n      .string()\n      .regex(/^[a-z]{2,3}(-[A-Z]{2})?$/)\n      .optional(),\n    updated_at: z.number().int().optional(),\n\n    // OpenID: \"email\" scope\n    email: z.string().optional(),\n    email_verified: z.boolean().optional(),\n\n    // OpenID: \"phone\" scope\n    phone_number: z.string().optional(),\n    phone_number_verified: z.boolean().optional(),\n\n    // OpenID: \"address\" scope\n    // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim\n    address: z\n      .object({\n        formatted: z.string().optional(),\n        street_address: z.string().optional(),\n        locality: z.string().optional(),\n        region: z.string().optional(),\n        postal_code: z.string().optional(),\n        country: z.string().optional(),\n      })\n      .optional(),\n\n    // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2\n    authorization_details: z\n      .array(\n        z\n          .object({\n            type: z.string(),\n            // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2\n            locations: z.array(z.string()).optional(),\n            actions: z.array(z.string()).optional(),\n            datatypes: z.array(z.string()).optional(),\n            identifier: z.string().optional(),\n            privileges: z.array(z.string()).optional(),\n          })\n          .passthrough(),\n      )\n      .optional(),\n  })\n  .passthrough()\n\nexport type JwtPayload = z.infer<typeof jwtPayloadSchema>\n"
  },
  {
    "path": "packages/oauth/jwk/src/key.ts",
    "content": "import { jwkAlgorithms } from './alg.js'\nimport {\n  Jwk,\n  KeyUsage,\n  PUBLIC_KEY_USAGE,\n  PrivateJwk,\n  PublicJwk,\n  PublicKeyUsage,\n  hasSharedSecretJwk,\n  isEncKeyUsage,\n  isPrivateJwk,\n  isPublicKeyUsage,\n  isSigKeyUsage,\n  jwkPubSchema,\n  jwkSchema,\n} from './jwk.js'\nimport { VerifyOptions, VerifyResult } from './jwt-verify.js'\nimport { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'\nimport { cachedGetter } from './util.js'\n\nexport type KeyMatchOptions = {\n  usage?: KeyUsage\n  kid?: string | string[]\n  alg?: string | string[]\n}\n\nexport type ActivityCheckOptions = {\n  allowRevoked?: boolean\n  clockTolerance?: number\n  currentDate?: Date\n}\n\nexport abstract class Key<J extends Jwk = Jwk> {\n  constructor(readonly jwk: Readonly<J>) {}\n\n  @cachedGetter\n  get isPrivate(): boolean {\n    return isPrivateJwk(this.jwk)\n  }\n\n  @cachedGetter\n  get isSymetric(): boolean {\n    return hasSharedSecretJwk(this.jwk)\n  }\n\n  get privateJwk(): Readonly<PrivateJwk> | undefined {\n    if (!this.isPrivate) return undefined\n\n    return this.jwk as Readonly<PrivateJwk>\n  }\n\n  @cachedGetter\n  get publicJwk(): Readonly<PublicJwk> | undefined {\n    if (this.isSymetric) return undefined\n    if (!this.isPrivate) return this.jwk as Readonly<PublicJwk>\n\n    const validated = jwkPubSchema.safeParse({\n      ...this.jwk,\n      d: undefined,\n      k: undefined,\n      use: undefined,\n      key_ops: buildPublicKeyOps(this.keyOps) ?? PUBLIC_KEY_USAGE,\n    })\n\n    // One reason why the parsing might fail is if key_ops is empty. This check\n    // also allows to future proof the code (e.g if another type of private key\n    // is added that uses a different property than \"d\" or \"k\" to store its\n    // private value).\n    if (!validated.success) return undefined\n\n    return Object.freeze(validated.data)\n  }\n\n  @cachedGetter\n  get bareJwk(): Readonly<Jwk> | undefined {\n    if (this.isSymetric) return undefined\n    const { kty, crv, e, n, x, y } = this.jwk as any\n    return Object.freeze(jwkSchema.parse({ crv, e, kty, n, x, y }))\n  }\n\n  /**\n   * @note Only defined on public keys\n   */\n  get use(): 'sig' | 'enc' | undefined {\n    return this.jwk.use\n  }\n\n  get keyOps(): readonly KeyUsage[] | undefined {\n    return this.jwk.key_ops\n  }\n\n  /**\n   * The (forced) algorithm to use. If not provided, the key will be usable with\n   * any of the algorithms in {@link algorithms}.\n   *\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 | \"alg\" (Algorithm) Header Parameter Values for JWS}\n   */\n  get alg() {\n    return this.jwk.alg\n  }\n\n  get kid() {\n    return this.jwk.kid\n  }\n\n  get crv() {\n    return (this.jwk as { crv: undefined } | Extract<J, { crv: unknown }>).crv\n  }\n\n  /**\n   * All the algorithms that this key can be used with. If `alg` is provided,\n   * this set will only contain that algorithm.\n   */\n  @cachedGetter\n  get algorithms(): readonly string[] {\n    return Object.freeze(Array.from(jwkAlgorithms(this.jwk)))\n  }\n\n  get isRevoked() {\n    return this.jwk.revoked != null\n  }\n\n  isActive(options?: ActivityCheckOptions) {\n    if (!options?.allowRevoked && this.isRevoked) return false\n\n    const tolerance = options?.clockTolerance ?? 0\n    if (tolerance !== Infinity) {\n      const now = options?.currentDate?.getTime() ?? Date.now()\n      const { exp, nbf } = this.jwk\n\n      if (nbf != null && !(now >= nbf * 1e3 - tolerance)) return false\n      if (exp != null && !(now < exp * 1e3 + tolerance)) return false\n    }\n\n    return true\n  }\n\n  matches(opts: KeyMatchOptions): boolean {\n    if (opts.kid != null) {\n      const matchesKid = Array.isArray(opts.kid)\n        ? this.kid != null && opts.kid.includes(this.kid)\n        : this.kid === opts.kid\n      if (!matchesKid) return false\n    }\n\n    if (opts.alg != null) {\n      const matchesAlg = Array.isArray(opts.alg)\n        ? opts.alg.some((a) => this.algorithms.includes(a))\n        : this.algorithms.includes(opts.alg)\n      if (!matchesAlg) return false\n    }\n\n    if (opts.usage != null) {\n      const matchesOps =\n        this.keyOps == null ||\n        this.keyOps.includes(opts.usage) ||\n        // @NOTE Because this.jwk represents the private key (typically used for\n        // private operations), the public counterpart operations are allowed.\n        (opts.usage === 'verify' && this.keyOps.includes('sign')) ||\n        (opts.usage === 'encrypt' && this.keyOps.includes('decrypt')) ||\n        (opts.usage === 'wrapKey' && this.keyOps.includes('unwrapKey'))\n      if (!matchesOps) return false\n\n      const matchesUse =\n        this.use == null ||\n        (this.use === 'sig' && isSigKeyUsage(opts.usage)) ||\n        (this.use === 'enc' && isEncKeyUsage(opts.usage))\n      if (!matchesUse) return false\n\n      // @NOTE This is only relevant when \"key_ops\" and \"use\" are undefined.\n      // This line also ensures that when \"opts.usage\" is a private key usage\n      // (e.g. \"sign\"), the key is indeed a private key.\n      const matchesKeyType = this.isPrivate || isPublicKeyUsage(opts.usage)\n      if (!matchesKeyType) return false\n    }\n\n    return true\n  }\n\n  /**\n   * Create a signed JWT\n   */\n  abstract createJwt(header: JwtHeader, payload: JwtPayload): Promise<SignedJwt>\n\n  /**\n   * Verify the signature, headers and payload of a JWT\n   *\n   * @throws {JwtVerifyError} if the JWT is invalid\n   */\n  abstract verifyJwt<C extends string = never>(\n    token: SignedJwt,\n    options?: VerifyOptions<C>,\n  ): Promise<VerifyResult<C>>\n}\n\nfunction buildPublicKeyOps(\n  keyUsages?: readonly KeyUsage[],\n): PublicKeyUsage[] | undefined {\n  if (keyUsages == null) return undefined\n\n  // https://datatracker.ietf.org/doc/html/rfc7517#section-4.3\n  // > Duplicate key operation values MUST NOT be present in the array.\n  const publicOps = new Set(keyUsages.filter(isPublicKeyUsage))\n\n  // @NOTE Translating private key usage into public key usage\n  if (keyUsages.includes('sign')) publicOps.add('verify')\n  if (keyUsages.includes('decrypt')) publicOps.add('encrypt')\n  if (keyUsages.includes('unwrapKey')) publicOps.add('wrapKey')\n\n  return Array.from(publicOps)\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/keyset.ts",
    "content": "import {\n  ERR_JWKS_NO_MATCHING_KEY,\n  ERR_JWK_NOT_FOUND,\n  ERR_JWT_INVALID,\n  JwkError,\n  JwtCreateError,\n  JwtVerifyError,\n} from './errors.js'\nimport { PrivateKeyUsage } from './jwk.js'\nimport { JwksPub } from './jwks.js'\nimport { unsafeDecodeJwt } from './jwt-decode.js'\nimport { VerifyOptions, VerifyResult } from './jwt-verify.js'\nimport { JwtHeader, JwtPayload, SignedJwt } from './jwt.js'\nimport { ActivityCheckOptions, Key, KeyMatchOptions } from './key.js'\nimport {\n  Override,\n  cachedGetter,\n  isDefined,\n  matchesAny,\n  preferredOrderCmp,\n} from './util.js'\n\nexport type { ActivityCheckOptions, KeyMatchOptions }\nexport type FindKeyOptions = KeyMatchOptions & ActivityCheckOptions\n\nexport type JwtSignHeader = Override<\n  JwtHeader,\n  Pick<FindKeyOptions, 'alg' | 'kid'>\n>\n\nexport type JwtPayloadGetter<P = JwtPayload> = (\n  header: JwtHeader,\n  key: Key,\n) => P | PromiseLike<P>\n\nconst extractPrivateJwk = (key: Key) => key.privateJwk\nconst extractPublicJwk = (key: Key) => key.publicJwk\n\nexport class Keyset<K extends Key = Key> implements Iterable<K> {\n  private readonly keys: readonly K[]\n\n  constructor(\n    iterable: Iterable<K | null | undefined | false>,\n    /**\n     * The preferred algorithms to use when signing a JWT using this keyset.\n     *\n     * @see {@link https://datatracker.ietf.org/doc/html/rfc7518#section-3.1}\n     */\n    public readonly preferredSigningAlgorithms: readonly string[] = iterable instanceof\n    Keyset\n      ? [...iterable.preferredSigningAlgorithms]\n      : [\n          // Prefer elliptic curve algorithms\n          'EdDSA',\n          'ES256K',\n          'ES256',\n          // https://datatracker.ietf.org/doc/html/rfc7518#section-3.5\n          'PS256',\n          'PS384',\n          'PS512',\n          'HS256',\n          'HS384',\n          'HS512',\n        ],\n  ) {\n    const keys: K[] = []\n\n    const keyIds = new Set<string>()\n    for (const key of iterable) {\n      if (!key) continue\n\n      keys.push(key)\n\n      if (key.kid) {\n        if (keyIds.has(key.kid)) throw new JwkError(`Duplicate key: ${key.kid}`)\n        else keyIds.add(key.kid)\n      }\n    }\n\n    this.keys = Object.freeze(keys)\n  }\n\n  get size(): number {\n    return this.keys.length\n  }\n\n  @cachedGetter\n  get signAlgorithms(): readonly string[] {\n    const algorithms = new Set<string>()\n    for (const key of this) {\n      if (key.use !== 'sig') continue\n      for (const alg of key.algorithms) {\n        algorithms.add(alg)\n      }\n    }\n    return Object.freeze(\n      [...algorithms].sort(preferredOrderCmp(this.preferredSigningAlgorithms)),\n    )\n  }\n\n  @cachedGetter\n  get publicJwks() {\n    return Object.freeze({\n      keys: Object.freeze(Array.from(this, extractPublicJwk).filter(isDefined)),\n    })\n  }\n\n  @cachedGetter\n  get privateJwks() {\n    return Object.freeze({\n      keys: Object.freeze(\n        Array.from(this, extractPrivateJwk).filter(isDefined),\n      ),\n    })\n  }\n\n  has(kid: string): boolean {\n    return this.keys.some((key) => key.kid === kid)\n  }\n\n  get(options: FindKeyOptions): K {\n    const key = this.find(options)\n    if (key) return key\n\n    throw new JwkError(\n      `Key not found ${options.kid ?? options.alg ?? options.usage ?? '<unknown>'}`,\n      ERR_JWK_NOT_FOUND,\n    )\n  }\n\n  find(options: FindKeyOptions): K | undefined {\n    for (const key of this.list(options)) {\n      return key\n    }\n\n    return undefined\n  }\n\n  *list<O extends FindKeyOptions>(options: O) {\n    for (const key of this) {\n      if (key.isActive(options) && key.matches(options)) {\n        yield key\n      }\n    }\n  }\n\n  findPrivateKey({\n    kid,\n    alg,\n    usage,\n    ...options\n  }: FindKeyOptions & { usage: PrivateKeyUsage }): {\n    key: Key\n    alg: string\n  } {\n    const matchingKeys: Key[] = []\n\n    // Allow the loop bellow to return early when a single \"alg\" is provided\n    if (Array.isArray(alg) && alg.length === 1) alg = alg[0]\n\n    for (const key of this.list({ ...options, kid, alg, usage })) {\n      // Skip negotiation if a single \"alg\" was provided\n      if (typeof alg === 'string') return { key, alg }\n\n      matchingKeys.push(key)\n    }\n\n    const isAllowedAlg = matchesAny(alg)\n    const candidates = matchingKeys.map(\n      (key) => [key, key.algorithms.filter(isAllowedAlg)] as const,\n    )\n\n    // Return the first candidates that matches the preferred algorithms\n    for (const prefAlg of this.preferredSigningAlgorithms) {\n      for (const [matchingKey, matchingAlgs] of candidates) {\n        if (matchingAlgs.includes(prefAlg)) {\n          return { key: matchingKey, alg: prefAlg }\n        }\n      }\n    }\n\n    // Return any candidate\n    for (const [matchingKey, matchingAlgs] of candidates) {\n      for (const alg of matchingAlgs) {\n        return { key: matchingKey, alg }\n      }\n    }\n\n    throw new JwkError(\n      `No private key found for ${kid || alg || usage}`,\n      ERR_JWK_NOT_FOUND,\n    )\n  }\n\n  [Symbol.iterator](): IterableIterator<K> {\n    return this.keys.values()\n  }\n\n  async createJwt(\n    { alg: sAlg, kid: sKid, ...header }: JwtSignHeader,\n    payload: JwtPayload | JwtPayloadGetter,\n  ): Promise<SignedJwt> {\n    try {\n      const { key, alg } = this.findPrivateKey({\n        alg: sAlg,\n        kid: sKid,\n        usage: 'sign',\n        allowRevoked: false, // For explicitness (default value is false)\n      })\n      const protectedHeader = { ...header, alg, kid: key.kid }\n\n      if (typeof payload === 'function') {\n        payload = await payload(protectedHeader, key)\n      }\n\n      return await key.createJwt(protectedHeader, payload)\n    } catch (err) {\n      throw JwtCreateError.from(err)\n    }\n  }\n\n  async verifyJwt<C extends string = never>(\n    token: SignedJwt,\n    options?: ActivityCheckOptions & VerifyOptions<C>,\n  ): Promise<VerifyResult<C> & { key: K }> {\n    const { header } = unsafeDecodeJwt(token)\n    const { kid, alg } = header\n\n    const errors: unknown[] = []\n\n    for (const key of this.list({ ...options, kid, alg, usage: 'verify' })) {\n      try {\n        const result = await key.verifyJwt<C>(token, options)\n        return { ...result, key }\n      } catch (err) {\n        errors.push(err)\n      }\n    }\n\n    switch (errors.length) {\n      case 0:\n        throw new JwtVerifyError('No key matched', ERR_JWKS_NO_MATCHING_KEY)\n      case 1:\n        throw JwtVerifyError.from(errors[0], ERR_JWT_INVALID)\n      default:\n        throw JwtVerifyError.from(errors, ERR_JWT_INVALID)\n    }\n  }\n\n  toJSON() {\n    // Make a copy to allow mutation of the result\n    return structuredClone(this.publicJwks) as JwksPub\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk/src/util.ts",
    "content": "import { base64url } from 'multiformats/bases/base64'\nimport { RefinementCtx, ZodIssueCode } from 'zod'\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\nexport type Override<T, V> = Simplify<V & Omit<T, keyof V>>\n\nexport type RequiredKey<T, K extends keyof T = never> = Simplify<\n  T & {\n    [L in K]-?: unknown extends T[L]\n      ? NonNullable<unknown> | null\n      : Exclude<T[L], undefined>\n  }\n>\n\nexport const isDefined = <T>(i: T | undefined): i is T => i !== undefined\n\nexport const preferredOrderCmp =\n  <T>(order: readonly T[]) =>\n  (a: T, b: T) => {\n    const aIdx = order.indexOf(a)\n    const bIdx = order.indexOf(b)\n    if (aIdx === bIdx) return 0\n    if (aIdx === -1) return 1\n    if (bIdx === -1) return -1\n    return aIdx - bIdx\n  }\n\nexport function matchesAny<T extends string | number | symbol | boolean>(\n  value: null | undefined | T | readonly T[],\n): (v: unknown) => v is T {\n  return value == null\n    ? (v): v is T => true\n    : Array.isArray(value)\n      ? (v): v is T => value.includes(v)\n      : (v): v is T => v === value\n}\n\n/**\n * Decorator to cache the result of a getter on a class instance.\n */\nexport const cachedGetter = <T extends object, V>(\n  target: (this: T) => V,\n  _context: ClassGetterDecoratorContext<T, V>,\n) => {\n  return function (this: T) {\n    const value = target.call(this)\n    Object.defineProperty(this, target.name, {\n      get: () => value,\n      enumerable: true,\n      configurable: true,\n    })\n    return value\n  }\n}\n\nconst decoder = new TextDecoder()\nexport function parseB64uJson(input: string): unknown {\n  const inputBytes = base64url.baseDecode(input)\n  const json = decoder.decode(inputBytes)\n  return JSON.parse(json)\n}\n\n/**\n * @example\n * ```ts\n * // jwtSchema will only allow base64url chars & \".\" (dot)\n * const jwtSchema = z.string().superRefine(jwtCharsRefinement)\n * ```\n */\nexport const jwtCharsRefinement = (data: string, ctx: RefinementCtx): void => {\n  // Note: this is a hot path, let's avoid using a RegExp\n  let char\n\n  for (let i = 0; i < data.length; i++) {\n    char = data.charCodeAt(i)\n\n    if (\n      // Base64 URL encoding (most frequent)\n      (65 <= char && char <= 90) || // A-Z\n      (97 <= char && char <= 122) || // a-z\n      (48 <= char && char <= 57) || // 0-9\n      char === 45 || // -\n      char === 95 || // _\n      // Boundary (least frequent, check last)\n      char === 46 // .\n    ) {\n      // continue\n    } else {\n      // Invalid char might be a surrogate pair\n      const invalidChar = String.fromCodePoint(data.codePointAt(i)!)\n      return ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: `Invalid character \"${invalidChar}\" in JWT at position ${i}`,\n      })\n    }\n  }\n}\n\n/**\n * @example\n * ```ts\n * type SegmentedString3 = SegmentedString<3> // `${string}.${string}.${string}`\n * type SegmentedString4 = SegmentedString<4> // `${string}.${string}.${string}.${string}`\n * ```\n *\n * @note\n * This utility only provides one way type safety (A SegmentedString<4> can be\n * assigned to SegmentedString<3> but not vice versa). The purpose of this\n * utility is to improve DX by avoiding as many potential errors as build time.\n * DO NOT rely on this to enforce security or data integrity.\n */\ntype SegmentedString<\n  C extends number,\n  Acc extends string[] = [string],\n> = Acc['length'] extends C\n  ? `${Acc[0]}`\n  : `${Acc[0]}.${SegmentedString<C, [string, ...Acc]>}`\n\n/**\n * @example\n * ```ts\n * const jwtSchema = z.string().superRefine(segmentedStringRefinementFactory(3))\n * type Jwt = z.infer<typeof jwtSchema> // `${string}.${string}.${string}`\n * ```\n */\nexport const segmentedStringRefinementFactory = <C extends number>(\n  count: C,\n  minPartLength = 2,\n) => {\n  if (!Number.isFinite(count) || count < 1 || (count | 0) !== count) {\n    throw new TypeError(`Count must be a natural number (got ${count})`)\n  }\n\n  const minTotalLength = count * minPartLength + (count - 1)\n  const errorPrefix = `Invalid JWT format`\n\n  return (data: string, ctx: RefinementCtx): data is SegmentedString<C> => {\n    if (data.length < minTotalLength) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: `${errorPrefix}: too short`,\n      })\n      return false\n    }\n    let currentStart = 0\n    for (let i = 0; i < count - 1; i++) {\n      const nextDot = data.indexOf('.', currentStart)\n      if (nextDot === -1) {\n        ctx.addIssue({\n          code: ZodIssueCode.custom,\n          message: `${errorPrefix}: expected ${count} segments, got ${i + 1}`,\n        })\n        return false\n      }\n      if (nextDot - currentStart < minPartLength) {\n        ctx.addIssue({\n          code: ZodIssueCode.custom,\n          message: `${errorPrefix}: segment ${i + 1} is too short`,\n        })\n        return false\n      }\n      currentStart = nextDot + 1\n    }\n    if (data.indexOf('.', currentStart) !== -1) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: `${errorPrefix}: too many segments`,\n      })\n      return false\n    }\n    if (data.length - currentStart < minPartLength) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: `${errorPrefix}: last segment is too short`,\n      })\n      return false\n    }\n    return true\n  }\n}\n\nexport function isLastOccurrence<\n  T extends number | boolean | string | null | undefined | symbol | bigint,\n>(v: T, i: number, arr: readonly T[]): boolean {\n  return arr.indexOf(v, i + 1) === -1\n}\n"
  },
  {
    "path": "packages/oauth/jwk/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/oauth/jwk/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/jwk-jose/CHANGELOG.md",
    "content": "# @atproto/jwk-jose\n\n## 0.1.11\n\n### Patch Changes\n\n- Updated dependencies [[`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815)]:\n  - @atproto/jwk@0.6.0\n\n## 0.1.10\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto/jwk@0.4.0\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/jwk@0.3.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05)]:\n  - @atproto/jwk@0.2.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/jwk@0.1.5\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/jwk@0.1.4\n\n## 0.1.4\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/jwk@0.1.3\n\n## 0.1.3\n\n### Patch Changes\n\n- [#2879](https://github.com/bluesky-social/atproto/pull/2879) [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve compatibility with runtimes relying on webcrypto (by explicit JOSE's importJWK() \"alg\" argument).\n\n- [#2879](https://github.com/bluesky-social/atproto/pull/2879) [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unsafe type casting during JWT verification\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0)]:\n  - @atproto/jwk@0.1.2\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Misc fixes for confidential client usage\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow importing JoseKey without specifying a kid\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow build from Parcel\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/jwk@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto/jwk@0.1.0\n"
  },
  {
    "path": "packages/oauth/jwk-jose/package.json",
    "content": "{\n  \"name\": \"@atproto/jwk-jose\",\n  \"version\": \"0.1.11\",\n  \"license\": \"MIT\",\n  \"description\": \"`jose` based implementation of @atproto/jwk Key's\",\n  \"keywords\": [\n    \"atproto\",\n    \"jwk\",\n    \"jose\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/jwk-jose\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/jwk\": \"workspace:^\",\n    \"jose\": \"^5.2.0\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk-jose/src/index.ts",
    "content": "export * from './jose-key.js'\n"
  },
  {
    "path": "packages/oauth/jwk-jose/src/jose-key.ts",
    "content": "import {\n  type GenerateKeyPairOptions,\n  type GenerateKeyPairResult,\n  type JWK,\n  type JWTVerifyOptions,\n  type KeyLike,\n  SignJWT,\n  errors,\n  exportJWK,\n  generateKeyPair,\n  importJWK,\n  importPKCS8,\n  jwtVerify,\n} from 'jose'\nimport {\n  Jwk,\n  JwkError,\n  JwtCreateError,\n  JwtHeader,\n  JwtPayload,\n  JwtVerifyError,\n  Key,\n  SignedJwt,\n  VerifyOptions,\n  VerifyResult,\n  isPrivateJwk,\n  jwkSchema,\n  jwtHeaderSchema,\n  jwtPayloadSchema,\n} from '@atproto/jwk'\nimport { RequiredKey, either } from './util.js'\n\nconst { JOSEError } = errors\n\nexport {\n  type GenerateKeyPairOptions,\n  type GenerateKeyPairResult,\n  type Jwk,\n  type JwtHeader,\n  type JwtPayload,\n  type KeyLike,\n  type SignedJwt,\n  type VerifyOptions,\n}\n\nexport class JoseKey<J extends Jwk = Jwk> extends Key<J> {\n  /**\n   * Some runtimes (e.g. Bun) require an `alg` second argument to be set when\n   * invoking `importJWK`. In order to be compatible with these runtimes, we\n   * provide the following method to ensure the `alg` is always set. We also\n   * take the opportunity to ensure that the `alg` is compatible with this key.\n   */\n  protected async getKeyObj(alg: string) {\n    if (!this.algorithms.includes(alg)) {\n      throw new JwkError(`Key cannot be used with algorithm \"${alg}\"`)\n    }\n    try {\n      return await importJWK(this.jwk as JWK, alg)\n    } catch (cause) {\n      throw new JwkError('Failed to import JWK', undefined, { cause })\n    }\n  }\n\n  async createJwt(header: JwtHeader, payload: JwtPayload): Promise<SignedJwt> {\n    try {\n      const { kid } = header\n      if (kid && kid !== this.kid) {\n        throw new JwtCreateError(\n          `Invalid \"kid\" (${kid}) used to sign with key \"${this.kid}\"`,\n        )\n      }\n\n      const { alg } = header\n      if (!alg) {\n        throw new JwtCreateError('Missing \"alg\" in JWT header')\n      }\n\n      const keyObj = await this.getKeyObj(alg)\n      const jwtBuilder = new SignJWT(payload).setProtectedHeader({\n        ...header,\n        alg,\n        kid: this.kid,\n      })\n\n      const signedJwt = await jwtBuilder.sign(keyObj)\n\n      return signedJwt as SignedJwt\n    } catch (cause) {\n      if (cause instanceof JOSEError) {\n        throw new JwtCreateError(cause.message, cause.code, { cause })\n      } else {\n        throw JwtCreateError.from(cause)\n      }\n    }\n  }\n\n  async verifyJwt<C extends string = never>(\n    token: SignedJwt,\n    options?: VerifyOptions<C>,\n  ): Promise<VerifyResult<C>> {\n    try {\n      const result = await jwtVerify(\n        token,\n        async ({ alg }) => this.getKeyObj(alg),\n        { ...options, algorithms: this.algorithms } as JWTVerifyOptions,\n      )\n\n      // @NOTE if all tokens are signed exclusively through createJwt(), then\n      // there should be no need to parse the payload and headers here. But\n      // since the JWT could have been signed with the same key from somewhere\n      // else, let's parse it to ensure the integrity (and type safety) of the\n      // data.\n      const headerParsed = jwtHeaderSchema.safeParse(result.protectedHeader)\n      if (!headerParsed.success) {\n        throw new JwtVerifyError('Invalid JWT header', undefined, {\n          cause: headerParsed.error,\n        })\n      }\n\n      const payloadParsed = jwtPayloadSchema.safeParse(result.payload)\n      if (!payloadParsed.success) {\n        throw new JwtVerifyError('Invalid JWT payload', undefined, {\n          cause: payloadParsed.error,\n        })\n      }\n\n      return {\n        protectedHeader: headerParsed.data,\n        // \"requiredClaims\" enforced by jwtVerify()\n        payload: payloadParsed.data as RequiredKey<JwtPayload, C>,\n      }\n    } catch (cause) {\n      if (cause instanceof JOSEError) {\n        throw new JwtVerifyError(cause.message, cause.code, { cause })\n      } else {\n        throw JwtVerifyError.from(cause)\n      }\n    }\n  }\n\n  static async generateKeyPair(\n    allowedAlgos: readonly string[] = ['ES256'],\n    options?: GenerateKeyPairOptions,\n  ) {\n    if (!allowedAlgos.length) {\n      throw new JwkError('No algorithms provided for key generation')\n    }\n\n    const errors: unknown[] = []\n    for (const alg of allowedAlgos) {\n      try {\n        return await generateKeyPair(alg, options)\n      } catch (err) {\n        errors.push(err)\n      }\n    }\n\n    throw new JwkError('Failed to generate key pair', undefined, {\n      cause: new AggregateError(errors, 'None of the algorithms worked'),\n    })\n  }\n\n  static async generate(\n    allowedAlgos: string[] = ['ES256'],\n    kid?: string,\n    options?: Omit<GenerateKeyPairOptions, 'extractable'>,\n  ): Promise<JoseKey> {\n    const kp = await this.generateKeyPair(allowedAlgos, {\n      ...options,\n      extractable: true,\n    })\n    return this.fromKeyLike(kp.privateKey, kid)\n  }\n\n  static async fromImportable(\n    input: string | KeyLike | Jwk,\n    kid?: string,\n  ): Promise<JoseKey> {\n    if (typeof input === 'string') {\n      // PKCS8\n      if (input.startsWith('-----')) {\n        // The \"alg\" is only needed in WebCrypto (NodeJS will be fine)\n        return this.fromPKCS8(input, '', kid)\n      }\n\n      // Jwk (string)\n      if (input.startsWith('{')) {\n        return this.fromJWK(input, kid)\n      }\n\n      throw new JwkError('Invalid input')\n    }\n\n    if (typeof input === 'object') {\n      // Jwk\n      if ('kty' in input || 'alg' in input) {\n        return this.fromJWK(input, kid)\n      }\n\n      // KeyLike\n      return this.fromKeyLike(input, kid)\n    }\n\n    throw new JwkError('Invalid input')\n  }\n\n  /**\n   * @see {@link exportJWK}\n   */\n  static async fromKeyLike(\n    keyLike: KeyLike | Uint8Array,\n    kid?: string,\n    alg?: string,\n  ): Promise<JoseKey> {\n    const jwk = await exportJWK(keyLike)\n    if (alg) {\n      if (!jwk.alg) jwk.alg = alg\n      else if (jwk.alg !== alg) throw new JwkError('Invalid \"alg\" in JWK')\n    }\n    return this.fromJWK(jwk, kid)\n  }\n\n  /**\n   * @see {@link importPKCS8}\n   */\n  static async fromPKCS8(\n    pem: string,\n    alg: string,\n    kid?: string,\n  ): Promise<JoseKey> {\n    const keyLike = await importPKCS8(pem, alg, { extractable: true })\n    return this.fromKeyLike(keyLike, kid)\n  }\n\n  static async fromJWK(\n    input: string | Record<string, unknown>,\n    inputKid?: string,\n  ): Promise<JoseKey> {\n    const jwk = typeof input === 'string' ? JSON.parse(input) : input\n    if (!jwk || typeof jwk !== 'object') throw new JwkError('Invalid JWK')\n\n    const kid = either(jwk.kid, inputKid)\n\n    // Backwards compatibility with old behavior\n    if (jwk.use != null && isPrivateJwk(jwk)) {\n      console.warn(\n        'Deprecation warning: Private JWK with a \"use\" property will be rejected in the future. Please remove replace \"use\" with (valid) \"key_ops\".',\n      )\n      jwk.key_ops ??= jwk.use === 'sig' ? ['sign'] : ['encrypt']\n      delete jwk.use\n    }\n\n    return new JoseKey<Jwk>(jwkSchema.parse({ ...jwk, kid }))\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk-jose/src/util.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-types\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\n\nexport type RequiredKey<T, K extends keyof T = never> = Simplify<\n  T & {\n    [L in K]-?: unknown extends T[L]\n      ? NonNullable<unknown> | null\n      : Exclude<T[L], undefined>\n  }\n>\n\nexport function either<T extends string | number | boolean>(\n  a?: T,\n  b?: T,\n): T | undefined {\n  if (a != null && b != null && a !== b) {\n    throw new TypeError(`Expected \"${b}\", got \"${a}\"`)\n  }\n  return a ?? b ?? undefined\n}\n"
  },
  {
    "path": "packages/oauth/jwk-jose/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/nodenext.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/oauth/jwk-jose/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/CHANGELOG.md",
    "content": "# @atproto/jwk-webcrypto\n\n## 0.2.0\n\n### Minor Changes\n\n- [#4103](https://github.com/bluesky-social/atproto/pull/4103) [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Only allow `\"use\"` claims in public jwk\n\n### Patch Changes\n\n- Updated dependencies [[`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815)]:\n  - @atproto/jwk@0.6.0\n  - @atproto/jwk-jose@0.1.11\n\n## 0.1.10\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n  - @atproto/jwk-jose@0.1.10\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto/jwk@0.4.0\n  - @atproto/jwk-jose@0.1.9\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/jwk@0.3.0\n  - @atproto/jwk-jose@0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05)]:\n  - @atproto/jwk@0.2.0\n  - @atproto/jwk-jose@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/jwk@0.1.5\n  - @atproto/jwk-jose@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/jwk@0.1.4\n  - @atproto/jwk-jose@0.1.5\n\n## 0.1.4\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/jwk-jose@0.1.4\n  - @atproto/jwk@0.1.3\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0)]:\n  - @atproto/jwk@0.1.2\n  - @atproto/jwk-jose@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/jwk-jose@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/jwk-jose@0.1.1\n  - @atproto/jwk@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto/jwk-jose@0.1.0\n  - @atproto/jwk@0.1.0\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/package.json",
    "content": "{\n  \"name\": \"@atproto/jwk-webcrypto\",\n  \"version\": \"0.2.0\",\n  \"license\": \"MIT\",\n  \"description\": \"Webcrypto based implementation of @atproto/jwk Key's\",\n  \"keywords\": [\n    \"atproto\",\n    \"jwk\",\n    \"webcrypto\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/jwk-webcrypto\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/jwk\": \"workspace:^\",\n    \"@atproto/jwk-jose\": \"workspace:^\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/src/index.ts",
    "content": "export * from './webcrypto-key.js'\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/src/util.ts",
    "content": "export type JWSAlgorithm =\n  // HMAC\n  | 'HS256'\n  | 'HS384'\n  | 'HS512'\n  // RSA\n  | 'PS256'\n  | 'PS384'\n  | 'PS512'\n  | 'RS256'\n  | 'RS384'\n  | 'RS512'\n  // EC\n  | 'ES256'\n  | 'ES256K'\n  | 'ES384'\n  | 'ES512'\n  // OKP\n  | 'EdDSA'\n\nexport type SubtleAlgorithm = RsaHashedKeyGenParams | EcKeyGenParams\n\nexport function toSubtleAlgorithm(\n  alg: string,\n  crv?: string,\n  options?: { modulusLength?: number },\n): SubtleAlgorithm {\n  switch (alg) {\n    case 'PS256':\n    case 'PS384':\n    case 'PS512':\n      return {\n        name: 'RSA-PSS',\n        hash: `SHA-${alg.slice(-3) as '256' | '384' | '512'}`,\n        modulusLength: options?.modulusLength ?? 2048,\n        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\n      }\n    case 'RS256':\n    case 'RS384':\n    case 'RS512':\n      return {\n        name: 'RSASSA-PKCS1-v1_5',\n        hash: `SHA-${alg.slice(-3) as '256' | '384' | '512'}`,\n        modulusLength: options?.modulusLength ?? 2048,\n        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\n      }\n    case 'ES256':\n    case 'ES384':\n      return {\n        name: 'ECDSA',\n        namedCurve: `P-${alg.slice(-3) as '256' | '384'}`,\n      }\n    case 'ES512':\n      return {\n        name: 'ECDSA',\n        namedCurve: 'P-521',\n      }\n    default:\n      // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773\n\n      throw new TypeError(`Unsupported alg \"${alg}\"`)\n  }\n}\n\nexport function fromSubtleAlgorithm(algorithm: KeyAlgorithm): JWSAlgorithm {\n  switch (algorithm.name) {\n    case 'RSA-PSS':\n    case 'RSASSA-PKCS1-v1_5': {\n      const hash = (<RsaHashedKeyAlgorithm>algorithm).hash.name\n      switch (hash) {\n        case 'SHA-256':\n        case 'SHA-384':\n        case 'SHA-512': {\n          const prefix = algorithm.name === 'RSA-PSS' ? 'PS' : 'RS'\n          return `${prefix}${hash.slice(-3) as '256' | '384' | '512'}`\n        }\n        default:\n          throw new TypeError('unsupported RsaHashedKeyAlgorithm hash')\n      }\n    }\n    case 'ECDSA': {\n      const namedCurve = (<EcKeyAlgorithm>algorithm).namedCurve\n      switch (namedCurve) {\n        case 'P-256':\n        case 'P-384':\n        case 'P-512':\n          return `ES${namedCurve.slice(-3) as '256' | '384' | '512'}`\n        case 'P-521':\n          return 'ES512'\n        default:\n          throw new TypeError('unsupported EcKeyAlgorithm namedCurve')\n      }\n    }\n    case 'Ed448':\n    case 'Ed25519':\n      return 'EdDSA'\n    default:\n      // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773\n\n      throw new TypeError(`Unexpected algorithm \"${algorithm.name}\"`)\n  }\n}\n\nexport function isCryptoKeyPair(\n  v: unknown,\n  extractable?: boolean,\n): v is CryptoKeyPair {\n  return (\n    typeof v === 'object' &&\n    v !== null &&\n    'privateKey' in v &&\n    v.privateKey instanceof CryptoKey &&\n    v.privateKey.type === 'private' &&\n    (extractable == null || v.privateKey.extractable === extractable) &&\n    v.privateKey.usages.includes('sign') &&\n    'publicKey' in v &&\n    v.publicKey instanceof CryptoKey &&\n    v.publicKey.type === 'public' &&\n    v.publicKey.extractable === true &&\n    v.publicKey.usages.includes('verify')\n  )\n}\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/src/webcrypto-key.ts",
    "content": "import { Jwk, JwkError, jwkSchema } from '@atproto/jwk'\nimport { GenerateKeyPairOptions, JoseKey } from '@atproto/jwk-jose'\nimport { fromSubtleAlgorithm, isCryptoKeyPair } from './util.js'\n\nexport class WebcryptoKey<J extends Jwk = Jwk> extends JoseKey<J> {\n  // We need to override the static method generate from JoseKey because\n  // the browser needs both the private and public keys\n  static override async generate(\n    allowedAlgos: string[] = ['ES256'],\n    kid: string = crypto.randomUUID(),\n    options?: GenerateKeyPairOptions,\n  ): Promise<WebcryptoKey> {\n    const keyPair = await this.generateKeyPair(allowedAlgos, options)\n\n    // Type safety only: in the browser, 'jose' always generates a CryptoKeyPair\n    if (!isCryptoKeyPair(keyPair)) {\n      throw new TypeError('Invalid CryptoKeyPair')\n    }\n\n    return this.fromKeypair(keyPair, kid)\n  }\n\n  static async fromKeypair(\n    cryptoKeyPair: CryptoKeyPair,\n    kid?: string,\n  ): Promise<WebcryptoKey> {\n    const {\n      alg = fromSubtleAlgorithm(cryptoKeyPair.privateKey.algorithm),\n      ...jwk\n    } = await crypto.subtle.exportKey(\n      'jwk',\n      cryptoKeyPair.privateKey.extractable\n        ? cryptoKeyPair.privateKey\n        : cryptoKeyPair.publicKey,\n    )\n\n    return new WebcryptoKey<Jwk>(\n      jwkSchema.parse({ ...jwk, kid, alg }),\n      cryptoKeyPair,\n    )\n  }\n\n  constructor(\n    jwk: Readonly<J>,\n    readonly cryptoKeyPair: CryptoKeyPair,\n  ) {\n    // Webcrypto keys are bound to a single algorithm\n    if (!jwk.alg) throw new JwkError('JWK \"alg\" is required for Webcrypto keys')\n\n    super(jwk)\n  }\n\n  get isPrivate() {\n    return true\n  }\n\n  protected override async getKeyObj(alg: string) {\n    if (this.jwk.alg !== alg) {\n      throw new JwkError(`Key cannot be used with algorithm \"${alg}\"`)\n    }\n    return this.cryptoKeyPair.privateKey\n  }\n}\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/jwk-webcrypto/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/CHANGELOG.md",
    "content": "# @atproto/oauth-client\n\n## 0.6.0\n\n### Minor Changes\n\n- [#4642](https://github.com/bluesky-social/atproto/pull/4642) [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove support for legacy session data that does not contain `authMethod`.\n\n- [#4642](https://github.com/bluesky-social/atproto/pull/4642) [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on `EventTarget` (missing in some environments)\n\n### Patch Changes\n\n- [#4642](https://github.com/bluesky-social/atproto/pull/4642) [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Delete any pre-existing OAuth session when a new one is created (for a given `sub`)\n\n- [#4642](https://github.com/bluesky-social/atproto/pull/4642) [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid throwing errors when trying to revoke a missing or invalid session\n\n## 0.5.14\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n  - @atproto-labs/did-resolver@0.2.6\n  - @atproto-labs/handle-resolver@0.3.6\n  - @atproto/oauth-types@0.6.2\n  - @atproto-labs/identity-resolver@0.3.6\n\n## 0.5.13\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto-labs/identity-resolver@0.3.5\n  - @atproto/did@0.2.4\n  - @atproto-labs/did-resolver@0.2.5\n  - @atproto-labs/handle-resolver@0.3.5\n  - @atproto/oauth-types@0.6.1\n\n## 0.5.12\n\n### Patch Changes\n\n- Updated dependencies [[`95ef3c2`](https://github.com/bluesky-social/atproto/commit/95ef3c24e8072e9d49412950b033cb8607764ee0), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41)]:\n  - @atproto/oauth-types@0.6.0\n\n## 0.5.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.7\n\n## 0.5.10\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n  - @atproto-labs/did-resolver@0.2.4\n  - @atproto-labs/handle-resolver@0.3.4\n  - @atproto/oauth-types@0.5.2\n  - @atproto-labs/identity-resolver@0.3.4\n\n## 0.5.9\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto-labs/identity-resolver@0.3.3\n  - @atproto-labs/handle-resolver@0.3.3\n  - @atproto-labs/did-resolver@0.2.3\n  - @atproto/did@0.2.2\n  - @atproto/oauth-types@0.5.1\n  - @atproto/xrpc@0.7.6\n\n## 0.5.8\n\n### Patch Changes\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto/oauth-types@0.5.0\n\n## 0.5.7\n\n### Patch Changes\n\n- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `core-js` to polyfill `Symbol.dispose`\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `AbortSignal.timeout` to generate timeout based signals\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/oauth-types@0.4.2\n  - @atproto/jwk@0.6.0\n  - @atproto/did@0.2.1\n  - @atproto-labs/did-resolver@0.2.2\n  - @atproto-labs/handle-resolver@0.3.2\n  - @atproto-labs/identity-resolver@0.3.2\n\n## 0.5.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.5\n\n## 0.5.5\n\n### Patch Changes\n\n- [#4150](https://github.com/bluesky-social/atproto/pull/4150) [`86c4699da`](https://github.com/bluesky-social/atproto/commit/86c4699da8cf184c251e58c0a3a2612dd676f0ea) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `redirect_uri` validation on the client because it does not properly match loopback redirect uris\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/did@0.2.0\n  - @atproto-labs/simple-store@0.3.0\n  - @atproto/xrpc@0.7.4\n  - @atproto-labs/did-resolver@0.2.1\n  - @atproto-labs/handle-resolver@0.3.1\n  - @atproto-labs/simple-store-memory@0.1.4\n  - @atproto-labs/identity-resolver@0.3.1\n\n## 0.5.4\n\n### Patch Changes\n\n- [#4139](https://github.com/bluesky-social/atproto/pull/4139) [`6231c8730`](https://github.com/bluesky-social/atproto/commit/6231c8730adb3a4c17dec417e5332b2be61070e5) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Fix support for multiple redirect URIs in `@atproto/oauth-client`\n\n  Previously the callback method assumed a singular `redirect_uris` value, and enforced only performing the callback with the first registered redirect URI. This change allows passing the actual redirect URI to the `callback` method, much like the `authorize` method supports.\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.3\n\n## 0.5.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.2\n\n## 0.5.2\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n  - @atproto/oauth-types@0.4.1\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.7.1\n\n## 0.5.0\n\n### Minor Changes\n\n- [#3982](https://github.com/bluesky-social/atproto/pull/3982) [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `didResolver` and `handleResolver` no longer exposed on `OAuthClient` class\n\n### Patch Changes\n\n- [#3982](https://github.com/bluesky-social/atproto/pull/3982) [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow providing custom `identityProvider` implementation as `OAuthClient` constructor option\n\n- Updated dependencies [[`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a)]:\n  - @atproto-labs/identity-resolver@0.3.0\n  - @atproto/oauth-types@0.4.0\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [[`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47)]:\n  - @atproto-labs/handle-resolver@0.3.0\n  - @atproto-labs/identity-resolver@0.2.0\n\n## 0.4.1\n\n### Patch Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Re-export all types & utilities needed to instantiate an OAuth client\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `OAuthClient` to be instantiated with custom `didResolver` instance\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto-labs/handle-resolver@0.2.0\n  - @atproto-labs/did-resolver@0.2.0\n  - @atproto/jwk@0.4.0\n  - @atproto-labs/identity-resolver@0.1.19\n  - @atproto/oauth-types@0.3.1\n\n## 0.4.0\n\n### Minor Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bind the OAuth session to the kid that was used to authenticate the client (private_key_jwt)\n\n### Patch Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing `exp` claim in client attestation JWT\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/oauth-types@0.3.0\n  - @atproto/jwk@0.3.0\n\n## 0.3.22\n\n### Patch Changes\n\n- [#3933](https://github.com/bluesky-social/atproto/pull/3933) [`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use resolved handle or did instead of raw input as \"login_hint\"\n\n- [#3926](https://github.com/bluesky-social/atproto/pull/3926) [`4e96e2c7b`](https://github.com/bluesky-social/atproto/commit/4e96e2c7b7cc0231607d3065c95704069c4ca2a2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `iss` claim from DPoP proofs\n\n- Updated dependencies [[`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee)]:\n  - @atproto-labs/identity-resolver@0.1.18\n\n## 0.3.21\n\n### Patch Changes\n\n- [#3935](https://github.com/bluesky-social/atproto/pull/3935) [`cd4bed3c9`](https://github.com/bluesky-social/atproto/commit/cd4bed3c9e68878c3f79620fe19f6994ebcb932e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Cache new DPoP nonces from successful retries\n\n## 0.3.20\n\n### Patch Changes\n\n- [#3919](https://github.com/bluesky-social/atproto/pull/3919) [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `application/x-www-form-urlencoded` content instead of JSON for OAuth requests\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c)]:\n  - @atproto/jwk@0.2.0\n  - @atproto/oauth-types@0.2.8\n\n## 0.3.19\n\n### Patch Changes\n\n- [#3877](https://github.com/bluesky-social/atproto/pull/3877) [`a03f0b906`](https://github.com/bluesky-social/atproto/commit/a03f0b906b108f8c766a5700f0d68b55748f23bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove un-necessary validation of `alg` on every dpop token creation\n\n## 0.3.18\n\n### Patch Changes\n\n- [`36d0d370c`](https://github.com/bluesky-social/atproto/commit/36d0d370c24498f74c243ebfb01564e5050c672d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove query & fragment from DPoP proof `htu` claim\n\n## 0.3.17\n\n### Patch Changes\n\n- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4)]:\n  - @atproto-labs/fetch@0.2.3\n  - @atproto-labs/did-resolver@0.1.13\n  - @atproto-labs/identity-resolver@0.1.17\n\n## 0.3.16\n\n### Patch Changes\n\n- Updated dependencies [[`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b)]:\n  - @atproto/oauth-types@0.2.7\n  - @atproto/xrpc@0.7.0\n\n## 0.3.15\n\n### Patch Changes\n\n- Updated dependencies [[`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4)]:\n  - @atproto-labs/simple-store@0.2.0\n  - @atproto/oauth-types@0.2.6\n  - @atproto-labs/did-resolver@0.1.12\n  - @atproto-labs/handle-resolver@0.1.8\n  - @atproto-labs/simple-store-memory@0.1.3\n  - @atproto-labs/identity-resolver@0.1.16\n\n## 0.3.14\n\n### Patch Changes\n\n- Updated dependencies [[`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/oauth-types@0.2.5\n  - @atproto/jwk@0.1.5\n\n## 0.3.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.6.12\n\n## 0.3.12\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/identity-resolver@0.1.15\n  - @atproto/xrpc@0.6.11\n\n## 0.3.11\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor code optimizations\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto-labs/fetch@0.2.2\n  - @atproto/oauth-types@0.2.4\n  - @atproto/jwk@0.1.4\n  - @atproto-labs/did-resolver@0.1.11\n  - @atproto-labs/identity-resolver@0.1.14\n  - @atproto/xrpc@0.6.10\n\n## 0.3.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/identity-resolver@0.1.13\n  - @atproto/xrpc@0.6.9\n\n## 0.3.9\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/simple-store-memory@0.1.2\n  - @atproto-labs/identity-resolver@0.1.12\n  - @atproto-labs/handle-resolver@0.1.7\n  - @atproto-labs/did-resolver@0.1.10\n  - @atproto-labs/simple-store@0.1.2\n  - @atproto/oauth-types@0.2.3\n  - @atproto-labs/fetch@0.2.1\n  - @atproto/jwk@0.1.3\n  - @atproto/xrpc@0.6.8\n  - @atproto/did@0.1.5\n\n## 0.3.8\n\n### Patch Changes\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87), [`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87), [`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9)]:\n  - @atproto-labs/did-resolver@0.1.9\n  - @atproto/did@0.1.4\n  - @atproto/xrpc@0.6.7\n  - @atproto-labs/identity-resolver@0.1.11\n  - @atproto-labs/handle-resolver@0.1.6\n\n## 0.3.7\n\n### Patch Changes\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2)]:\n  - @atproto/jwk@0.1.2\n  - @atproto-labs/fetch@0.2.0\n  - @atproto/oauth-types@0.2.2\n  - @atproto-labs/did-resolver@0.1.8\n  - @atproto-labs/identity-resolver@0.1.10\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto-labs/did-resolver@0.1.7\n  - @atproto-labs/identity-resolver@0.1.9\n  - @atproto/xrpc@0.6.6\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`a200e5095`](https://github.com/bluesky-social/atproto/commit/a200e50951d297c3f9670e96027262196bc29b0b)]:\n  - @atproto-labs/handle-resolver@0.1.5\n  - @atproto-labs/identity-resolver@0.1.8\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/xrpc@0.6.5\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [[`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f)]:\n  - @atproto-labs/fetch@0.1.2\n  - @atproto-labs/did-resolver@0.1.6\n  - @atproto-labs/identity-resolver@0.1.7\n\n## 0.3.2\n\n### Patch Changes\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Verify authorization_endpoint URL protocol\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure that client-id is a web url\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve message of OAuthResolverError in case of metadata validation error\n\n- Updated dependencies [[`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3)]:\n  - @atproto/oauth-types@0.2.1\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/identity-resolver@0.1.6\n  - @atproto/xrpc@0.6.4\n\n## 0.3.0\n\n### Minor Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `\"auto\"` instead of `undefined` to descibe the refresh mechanism to use in various methods.\n\n### Patch Changes\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `allowHttp` OAuthClient construction option to allow working with \"http:\" oauth providers (for development & testing purposes).\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Perform issuer validation _before_ refreshing tokens.\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure token response is properly typed according to the atproto OAuth spec\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use fetch()'s \"cache\" option instead of headers to force caching behavior\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not use cache when checking sub authority\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow all oauth request parameters to be used as authorize() options\n\n- Updated dependencies [[`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf)]:\n  - @atproto/oauth-types@0.2.0\n  - @atproto-labs/did-resolver@0.1.5\n  - @atproto-labs/handle-resolver@0.1.4\n  - @atproto/did@0.1.3\n  - @atproto-labs/identity-resolver@0.1.5\n\n## 0.2.2\n\n### Patch Changes\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve client side validation of client metadata\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use scope from client metadata as default value\n\n- Updated dependencies [[`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/oauth-types@0.1.5\n  - @atproto/xrpc@0.6.3\n  - @atproto-labs/fetch@0.1.1\n  - @atproto-labs/did-resolver@0.1.4\n  - @atproto-labs/identity-resolver@0.1.4\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/did@0.1.2\n  - @atproto/xrpc@0.6.2\n  - @atproto-labs/did-resolver@0.1.3\n  - @atproto-labs/handle-resolver@0.1.3\n  - @atproto-labs/identity-resolver@0.1.3\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The `OAuthClient` (and runtime specific sub-classes) no longer return @atproto/api `Agent` instances. Instead, they return `OAuthSession` instances that can be used to instantiate the `Agent` class.\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"nonce\" from authorization request\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Mandate the use of \"atproto\" scope\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"openid\" compatibility. The reason is that although we were technically \"openid\" compatible, ATProto identifiers are distributed identifiers. When a client relies on OpenID to authenticate users, it will use the auth provider in combination with the identifier to uniquely identify the user. Since ATProto identifiers are meant to be able to move from one provider to the other, OpenID compatibility could break authentication after a user was migrated to a different provider.\n\n  The way OpenID compliant clients would adapt to this particularity would typically be to remove the provider + identifier combination and use the identifier alone. While this is indeed the right way to handle ATProto identifiers, it requires more work to avoid impersonation. In particular, when obtaining a user identifier, the client **must** verify that the issuer of the identity token is indeed the server responsible for that user. This mechanism being not enforced by the OpenID standard, OpenID compatibility could lead to security issues. For this reason, we decided to remove OpenID compatibility from the OAuth provider.\n\n  Note that a trusted central authority could still offer OpenID compatibility by relying on ATProto's regular OAuth flow under the hood. This capability is out of the scope of this library.\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename OAuthAgent into OAuthSession\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `OAuthSession`'s `request` method to `fetchHandler`. The goal of this change is to allow `OAuthSession` to be used in order to instantiate `XrpcClient` by implementing the `FetchHandlerObject` interface.\n\n### Patch Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `getTokenInfo()` method to `OAuthSession`.\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not remove scopes not advertised in the AS's \"scopes_supported\" when building the authorization request.\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `getTokenSet()` method public in `OAuthSession`.\n\n- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/xrpc@0.6.1\n  - @atproto/oauth-types@0.1.4\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]:\n  - @atproto/api@0.13.3\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]:\n  - @atproto/api@0.13.2\n\n## 0.1.5\n\n### Patch Changes\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The non-standard `introspection_endpoint_auth_method`, and `introspection_endpoint_auth_signing_alg` client metadata properties were removed. The client's `token_endpoint_auth_method`, and `token_endpoint_auth_signing_alg` properties are now used as the only indication of how a client must authenticate at the introspection endpoint.\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The non-standard `revocation_endpoint_auth_method`, and `revocation_endpoint_auth_signing_alg` client metadata properties were removed. The client's `token_endpoint_auth_method`, and `token_endpoint_auth_signing_alg` properties are now used as the only indication of how a client must authenticate at the revocation endpoint.\n\n- [#2727](https://github.com/bluesky-social/atproto/pull/2727) [`3ebcd4e61`](https://github.com/bluesky-social/atproto/commit/3ebcd4e6161291d3649d7f8a9c5ee4ac26d590a2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"exp\" from dpop proof\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The non-standard `pushed_authorization_request_endpoint_auth_method`, and `pushed_authorization_request_endpoint_auth_signing_alg` client metadata properties were removed. The client's `token_endpoint_auth_method`, and `token_endpoint_auth_signing_alg` properties are now used as the only indication of how a client must authenticate at the introspection endpoint.\n\n- Updated dependencies [[`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb)]:\n  - @atproto/oauth-types@0.1.3\n\n## 0.1.4\n\n### Patch Changes\n\n- [#2710](https://github.com/bluesky-social/atproto/pull/2710) [`04112783d`](https://github.com/bluesky-social/atproto/commit/04112783db17f865c9e2b673190f77dd0b7461e3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add CustomEvent ponyfill for enviroments that don't provide it\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`22af354a5`](https://github.com/bluesky-social/atproto/commit/22af354a5db595d7cbc0e65f02601de3565337e1)]:\n  - @atproto/api@0.13.1\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Misc fixes for confidential client usage\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Better implement aptroto OAuth spec\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/oauth-types@0.1.2\n  - @atproto-labs/handle-resolver@0.1.2\n  - @atproto/did@0.1.1\n  - @atproto/xrpc@0.6.0\n  - @atproto/api@0.13.0\n  - @atproto-labs/identity-resolver@0.1.2\n  - @atproto-labs/did-resolver@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add event emitting capability to OAuthClient\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/oauth-types@0.1.1\n  - @atproto/jwk@0.1.1\n  - @atproto-labs/identity-resolver@0.1.1\n  - @atproto-labs/handle-resolver@0.1.1\n  - @atproto-labs/did-resolver@0.1.1\n  - @atproto-labs/simple-store@0.1.1\n  - @atproto-labs/simple-store-memory@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/simple-store-memory@0.1.0\n  - @atproto-labs/identity-resolver@0.1.0\n  - @atproto-labs/handle-resolver@0.1.0\n  - @atproto-labs/did-resolver@0.1.0\n  - @atproto-labs/simple-store@0.1.0\n  - @atproto/oauth-types@0.1.0\n  - @atproto-labs/fetch@0.1.0\n  - @atproto/jwk@0.1.0\n  - @atproto/did@0.1.0\n"
  },
  {
    "path": "packages/oauth/oauth-client/README.md",
    "content": "# @atproto/oauth-client: atproto flavoured OAuth client\n\nCore library for implementing [atproto][ATPROTO] OAuth clients.\n\nFor a browser specific implementation, see [@atproto/oauth-client-browser](https://www.npmjs.com/package/@atproto/oauth-client-browser).\nFor a node specific implementation, see\n[@atproto/oauth-client-node](https://www.npmjs.com/package/@atproto/oauth-client-node).\n\n## Usage\n\n### Configuration\n\n```ts\nimport { OAuthClient, Key, Session } from '@atproto/oauth-client'\nimport { JoseKey } from '@atproto/jwk-jose' // NodeJS/Browser only\n\nconst client = new OAuthClient({\n  handleResolver: 'https://my-backend.example', // backend instances should use a DNS based resolver\n  responseMode: 'query', // or \"fragment\" (frontend only) or \"form_post\" (backend only)\n\n  // These must be the same metadata as the one exposed on the\n  // \"client_id\" endpoint (except when using a loopback client)\n  clientMetadata: {\n    client_id: 'https://my-app.example/atproto-oauth-client.json',\n    jwks_uri: 'https://my-app.example/jwks.json',\n  },\n\n  runtimeImplementation: {\n    // A runtime specific implementation of the crypto operations needed by the\n    // OAuth client. See \"@atproto/oauth-client-browser\" for a browser specific\n    // implementation. The following example is suitable for use in NodeJS.\n\n    createKey(algs: string[]): Promise<Key> {\n      // algs is an ordered array of preferred algorithms (e.g. ['RS256', 'ES256'])\n\n      // Note, in browser environments, it is better to use non extractable keys\n      // to prevent the private key from being stolen. This can be done using\n      // the WebcryptoKey class from the \"@atproto/jwk-webcrypto\" package. The\n      // inconvenient of these keys (which is also what makes them stronger) is\n      // that the only way to persist them across browser reloads is to save\n      // them in the indexed DB.\n      return JoseKey.generate(algs)\n    },\n\n    getRandomValues(length: number): Uint8Array | PromiseLike<Uint8Array> {\n      return crypto.getRandomValues(new Uint8Array(length))\n    },\n\n    digest(\n      bytes: Uint8Array,\n      algorithm: { name: string },\n    ): Uint8Array | PromiseLike<Uint8Array> {\n      // sha256 is required. Unsupported algorithms should throw an error.\n\n      if (algorithm.name.startsWith('sha')) {\n        const subtleAlgo = `SHA-${algorithm.name.slice(3)}`\n        const buffer = await crypto.subtle.digest(subtleAlgo, bytes)\n        return new Uint8Array(buffer)\n      }\n\n      throw new TypeError(`Unsupported algorithm: ${algorithm.name}`)\n    },\n\n    requestLock: <T>(\n      name: string,\n      fn: () => T | PromiseLike<T>,\n    ): Promise<T> => {\n      // This function is used to prevent concurrent refreshes of the same\n      // credentials. It is important to ensure that only one refresh is done at\n      // a time to prevent the sessions from being revoked.\n\n      // The following example shows a simple in-memory lock. In a real\n      // application, you should use a more robust solution (e.g. a system wide\n      // lock manager). Note that not providing a lock will result in an\n      // in-memory lock to be used (DO NOT copy-paste the following code).\n\n      declare const locks: Map<string, Promise<void>>\n\n      const current = locks.get(name) || Promise.resolve()\n      const next = current\n        .then(fn)\n        .catch(() => {})\n        .finally(() => {\n          if (locks.get(name) === next) locks.delete(name)\n        })\n\n      locks.set(name, next)\n      return next\n    },\n  },\n\n  stateStore: {\n    // A store for saving state data while the user is being redirected to the\n    // authorization server.\n\n    set(key: string, internalState: InternalStateData): Promise<void> {\n      throw new Error('Not implemented')\n    },\n    get(key: string): Promise<InternalStateData | undefined> {\n      throw new Error('Not implemented')\n    },\n    del(key: string): Promise<void> {\n      throw new Error('Not implemented')\n    },\n  },\n\n  sessionStore: {\n    // A store for saving session data.\n\n    set(sub: string, session: Session): Promise<void> {\n      throw new Error('Not implemented')\n    },\n    get(sub: string): Promise<Session | undefined> {\n      throw new Error('Not implemented')\n    },\n    del(sub: string): Promise<void> {\n      throw new Error('Not implemented')\n    },\n  },\n\n  keyset: [\n    // For backend clients only, a list of private keys to use for signing\n    // credentials. These keys MUST correspond to the public keys exposed on the\n    // \"jwks_uri\" of the client metadata. Note that the jwks JSON corresponding\n    // to the following keys can be obtained using the `client.jwks` getter.\n    await JoseKey.fromImportable(process.env.PRIVATE_KEY_1),\n    await JoseKey.fromImportable(process.env.PRIVATE_KEY_2),\n    await JoseKey.fromImportable(process.env.PRIVATE_KEY_3),\n  ],\n})\n```\n\n### Authentication\n\n```ts\nconst url = await client.authorize('foo.bsky.team', {\n  state: '434321',\n  prompt: 'consent',\n  scope: 'email',\n  ui_locales: 'fr',\n})\n```\n\nMake user visit `url`. Then, once it was redirected to the callback URI, perform the following:\n\n```ts\n// Parse the query params from the callback URI\nconst params = new URLSearchParams('code=...&state=...')\n\n// Process the callback using the OAuth client\nconst result = await client.callback(params)\n\n// Verify the state (e.g. to link to an internal user)\nresult.state === '434321' // true\n\nconst oauthSession = result.session\n```\n\nThe sign-in process results in an `OAuthSession` instance that can be used to make\nauthenticated requests to the resource server. This instance will automatically\nrefresh the credentials when needed.\n\n### Making authenticated requests\n\nThe `OAuthSession` instance obtained after signing in can be used to make\nauthenticated requests to the user's PDS. There are two main use-cases:\n\n1. Making authenticated request to Bluesky's AppView in order to fetch and\n   manipulate data from the `app.bsky` lexicon.\n\n2. Making authenticated request to your own AppView, in order to fetch and\n   manipulate data from your own lexicon.\n\n#### Making authenticated requests to Bluesky's AppView\n\nThe `@atproto/oauth-client` package provides a `OAuthSession` class that can be\nused to make authenticated requests to Bluesky's AppView. This can be achieved\nby constructing an `Agent` (from `@atproto/api`) instance using the\n`OAuthSession` instance.\n\n```ts\nimport { Agent } from '@atproto/api'\n\nconst agent = new Agent(oauthSession)\n\n// Make an authenticated request to the server. New credentials will be\n// automatically fetched if needed (causing sessionStore.set() to be called).\nawait agent.post({\n  text: 'Hello, world!',\n})\n\n// revoke credentials on the server (causing sessionStore.del() to be called)\nawait agent.signOut()\n```\n\n#### Making authenticated requests to your own AppView\n\nThe `OAuthSession` instance obtained after signing in can be used to instantiate\nthe `XrpcClient` class from the `@atproto/xrpc` package.\n\n```ts\nimport { Lexicons } from '@atproto/lexicon'\nimport { OAuthClient } from '@atproto/oauth-client' // or \"@atproto/oauth-client-browser\" or \"@atproto/oauth-client-node\"\nimport { XrpcClient } from '@atproto/xrpc'\n\n// Define your lexicons\nconst myLexicon = new Lexicons([\n  {\n    lexicon: 1,\n    id: 'com.example.query',\n    defs: {\n      main: {\n        // ...\n      },\n    },\n  },\n])\n\n// Describe your app's oauth client\nconst oauthClient = new OAuthClient({\n  // ...\n})\n\n// Authenticate the user\nconst oauthSession = await oauthClient.restore('did:plc:123')\n\n// Instantiate a client using the `oauthSession` as fetch handler object\nconst client = new XrpcClient(oauthSession, myLexicon)\n\n// Make authenticated calls\nconst response = await client.call('com.example.query')\n```\n\nNote that the user's PDS might not know about your lexicon, or what to do with\nthose calls (PDS' are only mandated to implement the `com.atproto` lexicon). In\norder to process your calls, you need to have a backend that will process those\ncalls. You can then instruct your PDS to forward those calls to your backend.\n\n```ts\nconst response = await client.call(\n  'com.example.query',\n  {\n    // Params\n  },\n  {\n    headers: {\n      // The PDS will proxy calls to the specified service in did:plc:xyz's did document.\n      // These calls will be authenticated using \"service auth\", a single use JWT Bearer token, signed with the logged-in user's private key.\n      'atproto-proxy': 'did:plc:xyz#serviceId',\n    },\n  },\n)\n```\n\nYou can also instantiate the `XrpcClient` class with a custom `fetch` function\nthat will provide the `atproto-proxy` header on all calls:\n\n```ts\nconst boundClient = new XrpcClient((url, init) => {\n  const headers = new Headers(init?.headers)\n\n  // Add the atproto-proxy header if it is not already present\n  if (!headers.has('atproto-proxy')) {\n    headers.set('atproto-proxy', 'did:plc:xyz#serviceId')\n  }\n\n  return oauthSession.fetchHandler(url, { ...init, headers })\n}, myLexicon)\n\n// No need to specify the atproto-proxy header anymore\nconst response = await boundClient.call('com.example.query')\n```\n\n> [!NOTE]\n>\n> Proxying every call through the PDS is not recommended for performance\n> reasons, as it will increase the latency of readonly calls to your lexicon.\n> Doing so will also prevent your backend from being able to anticipate writes\n> on the network. Indeed, write calls will be sent to the PDS, which will then\n> propagate them on the network through a relay (a.k.a. \"firehose\"). This will\n> introduce a delay between the time the write is made and the time it is\n> processed by your backend.\n>\n> In order to avoid those issues, it is recommended that you implement your\n> backend using a backend-for-frontend pattern. This backend will be responsible\n> for processing the calls made by the client, and will be able to anticipate\n> writes on the network.\n>\n> Read more about the backend-for-frontend pattern in the [atproto][ATPROTO]\n> documentation website.\n\n## Advances use-cases\n\n### Listening for session updates and deletion\n\nThe `OAuthClient` will emit events whenever a session is updated or deleted.\n\n```ts\nimport {\n  Session,\n  TokenRefreshError,\n  TokenRevokedError,\n} from '@atproto/oauth-client'\n\nclient.addEventListener('updated', (event: CustomEvent<Session>) => {\n  console.log('Refreshed tokens were saved in the store:', event.detail)\n})\n\nclient.addEventListener(\n  'deleted',\n  (\n    event: CustomEvent<{\n      sub: string\n      cause: TokenRefreshError | TokenRevokedError | unknown\n    }>,\n  ) => {\n    console.log('Session was deleted from the session store:', event.detail)\n\n    const { cause } = event.detail\n\n    if (cause instanceof TokenRefreshError) {\n      // - refresh_token unavailable or expired\n      // - oauth response error (`cause.cause instanceof OAuthResponseError`)\n      // - session data does not match expected values returned by the OAuth server\n    } else if (cause instanceof TokenRevokedError) {\n      // Session was revoked through:\n      // - agent.signOut()\n      // - client.revoke(sub)\n    } else {\n      // An unexpected error occurred, causing the session to be deleted\n    }\n  },\n)\n```\n\n### Force user to re-authenticate\n\n```ts\nconst url = await client.authorize(handle, {\n  prompt: 'login',\n  state,\n})\n```\n\nor\n\n```ts\nconst url = await client.authorize(handle, {\n  state,\n})\n```\n\n### Silent Sign-In\n\nUsing silent sign-in requires to handle retries on the callback endpoint.\n\n```ts\nasync function createLoginUrl(handle: string, state?: string): string {\n  return client.authorize(handle, {\n    state,\n    // Use \"prompt=none\" to attempt silent sign-in\n    prompt: 'none',\n  })\n}\n\nasync function handleCallback(params: URLSearchParams) {\n  try {\n    return await client.callback(params)\n  } catch (err) {\n    // Silent sign-in failed, retry without prompt=none\n    if (\n      err instanceof OAuthCallbackError &&\n      ['login_required', 'consent_required'].includes(err.params.get('error'))\n    ) {\n      // Do *not* use prompt=none when retrying (to avoid infinite redirects)\n      const url = await client.authorize(handle, { state: err.state })\n\n      // Allow calling code to catch the error and redirect the user to the new URL\n      return new MyLoginRequiredError(url)\n    }\n\n    throw err\n  }\n}\n```\n\n[ATPROTO]: https://atproto.com/ 'AT Protocol'\n"
  },
  {
    "path": "packages/oauth/oauth-client/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-client\",\n  \"version\": \"0.6.0\",\n  \"license\": \"MIT\",\n  \"description\": \"OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"client\",\n    \"isomorphic\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-client\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/did-resolver\": \"workspace:^\",\n    \"@atproto-labs/fetch\": \"workspace:^\",\n    \"@atproto-labs/handle-resolver\": \"workspace:^\",\n    \"@atproto-labs/identity-resolver\": \"workspace:^\",\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto-labs/simple-store-memory\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/jwk\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\",\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"core-js\": \"^3\",\n    \"multiformats\": \"^9.9.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/constants.ts",
    "content": "/**\n * Per ATProto spec (OpenID uses RS256)\n */\nexport const FALLBACK_ALG = 'ES256'\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/errors/auth-method-unsatisfiable-error.ts",
    "content": "export class AuthMethodUnsatisfiableError extends Error {}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/errors/token-invalid-error.ts",
    "content": "export class TokenInvalidError extends Error {\n  constructor(\n    public readonly sub: string,\n    message = `The session for \"${sub}\" is invalid`,\n    options?: { cause?: unknown },\n  ) {\n    super(message, options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/errors/token-refresh-error.ts",
    "content": "export class TokenRefreshError extends Error {\n  constructor(\n    public readonly sub: string,\n    message: string,\n    options?: { cause?: unknown },\n  ) {\n    super(message, options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/errors/token-revoked-error.ts",
    "content": "export class TokenRevokedError extends Error {\n  constructor(\n    public readonly sub: string,\n    message = `The session for \"${sub}\" was successfully revoked`,\n    options?: { cause?: unknown },\n  ) {\n    super(message, options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/fetch-dpop.ts",
    "content": "import { base64url } from 'multiformats/bases/base64'\nimport { Key } from '@atproto/jwk'\nimport { Fetch, FetchContext, cancelBody, peekJson } from '@atproto-labs/fetch'\nimport { SimpleStore } from '@atproto-labs/simple-store'\n\n// \"undefined\" in non https environments or environments without crypto\nconst subtle = globalThis.crypto?.subtle as SubtleCrypto | undefined\n\nconst ReadableStream = globalThis.ReadableStream as\n  | typeof globalThis.ReadableStream\n  | undefined\n\nexport type DpopFetchWrapperOptions<C = FetchContext> = {\n  key: Key\n  nonces: SimpleStore<string, string>\n  supportedAlgs?: string[]\n  sha256?: (input: string) => Promise<string>\n\n  /**\n   * Is the intended server an authorization server (true) or a resource server\n   * (false)? Setting this may allow to avoid parsing the response body to\n   * determine the dpop-nonce.\n   *\n   * @default undefined\n   */\n  isAuthServer?: boolean\n  fetch?: Fetch<C>\n}\n\nexport function dpopFetchWrapper<C = FetchContext>({\n  key,\n  // @TODO we should provide a default based on specs\n  supportedAlgs,\n  nonces,\n  sha256 = typeof subtle !== 'undefined' ? subtleSha256 : undefined,\n  isAuthServer,\n  fetch = globalThis.fetch,\n}: DpopFetchWrapperOptions<C>): Fetch<C> {\n  if (!sha256) {\n    throw new TypeError(\n      `crypto.subtle is not available in this environment. Please provide a sha256 function.`,\n    )\n  }\n\n  // Throws if negotiation fails\n  const alg = negotiateAlg(key, supportedAlgs)\n\n  return async function (this: C, input, init) {\n    const request: Request =\n      init == null && input instanceof Request\n        ? input\n        : new Request(input, init)\n\n    const authorizationHeader = request.headers.get('Authorization')\n    const ath = authorizationHeader?.startsWith('DPoP ')\n      ? await sha256(authorizationHeader.slice(5))\n      : undefined\n\n    const { origin } = new URL(request.url)\n\n    const htm = request.method\n    const htu = buildHtu(request.url)\n\n    let initNonce: string | undefined\n    try {\n      initNonce = await nonces.get(origin)\n    } catch {\n      // Ignore get errors, we will just not send a nonce\n    }\n\n    const initProof = await buildProof(key, alg, htm, htu, initNonce, ath)\n    request.headers.set('DPoP', initProof)\n\n    const initResponse = await fetch.call(this, request)\n\n    // Make sure the response body is consumed. Either by the caller (when the\n    // response is returned), of if an error is thrown (catch block).\n\n    const nextNonce = initResponse.headers.get('DPoP-Nonce')\n    if (!nextNonce || nextNonce === initNonce) {\n      // No nonce was returned or it is the same as the one we sent. No need to\n      // update the nonce store, or retry the request.\n      return initResponse\n    }\n\n    // Store the fresh nonce for future requests\n    try {\n      await nonces.set(origin, nextNonce)\n    } catch {\n      // Ignore set errors\n    }\n\n    const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer)\n    if (!shouldRetry) {\n      // Not a \"use_dpop_nonce\" error, so there is no need to retry\n      return initResponse\n    }\n\n    // If the input stream was already consumed, we cannot retry the request. A\n    // solution would be to clone() the request but that would bufferize the\n    // entire stream in memory which can lead to memory starvation. Instead, we\n    // will return the original response and let the calling code handle retries.\n\n    if (input === request) {\n      // The input request body was consumed. We cannot retry the request.\n      return initResponse\n    }\n\n    if (ReadableStream && init?.body instanceof ReadableStream) {\n      // The init body was consumed. We cannot retry the request.\n      return initResponse\n    }\n\n    // We will now retry the request with the fresh nonce.\n\n    // The initial response body must be consumed (see cancelBody's doc).\n    await cancelBody(initResponse, 'log')\n\n    const nextProof = await buildProof(key, alg, htm, htu, nextNonce, ath)\n    const nextRequest = new Request(input, init)\n    nextRequest.headers.set('DPoP', nextProof)\n\n    const retryRequest = await fetch.call(this, nextRequest)\n    const retryNonce = retryRequest.headers.get('DPoP-Nonce')\n    if (!retryNonce || retryNonce === initNonce) {\n      // No nonce was returned or it is the same as the one we sent. No need to\n      // update the nonce store, or retry the request.\n      return retryRequest\n    }\n\n    // Store the fresh nonce for future requests\n    try {\n      await nonces.set(origin, retryNonce)\n    } catch {\n      // Ignore set errors\n    }\n\n    return retryRequest\n  }\n}\n\n/**\n * Strip query and fragment\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9449.html#section-4.2-4.6}\n */\nfunction buildHtu(url: string): string {\n  const fragmentIndex = url.indexOf('#')\n  const queryIndex = url.indexOf('?')\n\n  const end =\n    fragmentIndex === -1\n      ? queryIndex\n      : queryIndex === -1\n        ? fragmentIndex\n        : Math.min(fragmentIndex, queryIndex)\n\n  return end === -1 ? url : url.slice(0, end)\n}\n\nasync function buildProof(\n  key: Key,\n  alg: string,\n  htm: string,\n  htu: string,\n  nonce?: string,\n  ath?: string,\n) {\n  const jwk = key.bareJwk\n  if (!jwk) {\n    throw new Error('Only asymmetric keys can be used as DPoP proofs')\n  }\n\n  const now = Math.floor(Date.now() / 1e3)\n\n  return key.createJwt(\n    // https://datatracker.ietf.org/doc/html/rfc9449#section-4.2\n    {\n      alg,\n      typ: 'dpop+jwt',\n      jwk,\n    },\n    {\n      iat: now,\n      // Any collision will cause the request to be rejected by the server. no biggie.\n      jti: Math.random().toString(36).slice(2),\n      htm,\n      htu,\n      nonce,\n      ath,\n    },\n  )\n}\n\nasync function isUseDpopNonceError(\n  response: Response,\n  isAuthServer?: boolean,\n): Promise<boolean> {\n  // https://datatracker.ietf.org/doc/html/rfc6750#section-3\n  // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no\n  if (isAuthServer === undefined || isAuthServer === false) {\n    if (response.status === 401) {\n      const wwwAuth = response.headers.get('WWW-Authenticate')\n      if (wwwAuth?.startsWith('DPoP')) {\n        return wwwAuth.includes('error=\"use_dpop_nonce\"')\n      }\n    }\n  }\n\n  // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid\n  if (isAuthServer === undefined || isAuthServer === true) {\n    if (response.status === 400) {\n      try {\n        const json = await peekJson(response, 10 * 1024)\n        return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce'\n      } catch {\n        // Response too big (to be \"use_dpop_nonce\" error) or invalid JSON\n        return false\n      }\n    }\n  }\n\n  return false\n}\n\nfunction negotiateAlg(key: Key, supportedAlgs: string[] | undefined): string {\n  if (supportedAlgs) {\n    // Use order of supportedAlgs as preference\n    const alg = supportedAlgs.find((a) => key.algorithms.includes(a))\n    if (alg) return alg\n  } else {\n    const [alg] = key.algorithms\n    if (alg) return alg\n  }\n\n  throw new Error('Key does not match any alg supported by the server')\n}\n\nasync function subtleSha256(input: string): Promise<string> {\n  if (subtle == null) {\n    throw new Error(\n      `crypto.subtle is not available in this environment. Please provide a sha256 function.`,\n    )\n  }\n\n  const bytes = new TextEncoder().encode(input)\n  const digest = await subtle.digest('SHA-256', bytes)\n  const digestBytes = new Uint8Array(digest)\n  return base64url.baseEncode(digestBytes)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/identity-resolver.ts",
    "content": "export {\n  type CreateIdentityResolverOptions,\n  createIdentityResolver,\n} from '@atproto-labs/identity-resolver'\n\n// @TODO Currently, the `OAuthClient`'s `IdentityResolver` is an instance of\n// `AtprotoIdentityResolver`, which implements the ATProto Identity resolution\n// protocol (did resolution + dns resolution). In the future, we may want to\n// allow using a different `IdentityResolver` implementation, such as one based\n// on XRPC's \"com.atproto.identity.resolveIdentity\" method. This would be\n// particularly useful for browser based clients, since DNS lookups are not\n// available in browser environments (and require an alternative implementation,\n// such as one based on the \"com.atproto.identity.resolveHandle\" XRPC method, or\n// using DNS-over-HTTPS). Once we decide to support such a behavior, the\n// `identityResolver` option below should be made mandatory, and the code bellow\n// should be removed from the @atproto/oauth-client package (and moved to the\n// environment specific package, such as @atproto/oauth-client-browser and\n// @atproto/oauth-client-node), allowing the dependency graph to be optimized\n// for the specific environment. When that is done, the\n// `AtprotoIdentityResolver` class should also be moved to its own package.\n\n// @TODO Once we move to a distinct implementation, we should also introduce a\n// caching layer for the `IdentityResolver` to avoid redundant resolution\n// requests. Once this is done, the caching layers for the did and handle\n// resolvers should be removed as they will be redundant.\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/index.ts",
    "content": "import 'core-js/modules/es.symbol.dispose'\n\nexport * from '@atproto-labs/did-resolver'\nexport {\n  FetchError,\n  FetchRequestError,\n  FetchResponseError,\n} from '@atproto-labs/fetch'\nexport * from '@atproto-labs/handle-resolver'\n\nexport * from '@atproto/did'\nexport * from '@atproto/jwk'\nexport * from '@atproto/oauth-types'\n\nexport * from './lock.js'\nexport * from './oauth-authorization-server-metadata-resolver.js'\nexport * from './oauth-callback-error.js'\nexport * from './oauth-client.js'\nexport * from './oauth-protected-resource-metadata-resolver.js'\nexport * from './oauth-resolver-error.js'\nexport * from './oauth-response-error.js'\nexport * from './oauth-server-agent.js'\nexport * from './oauth-server-factory.js'\nexport * from './oauth-session.js'\nexport * from './runtime-implementation.js'\nexport * from './session-getter.js'\nexport * from './state-store.js'\nexport * from './types.js'\n\nexport * from './errors/token-invalid-error.js'\nexport * from './errors/token-refresh-error.js'\nexport * from './errors/token-revoked-error.js'\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/lock.ts",
    "content": "import { RuntimeLock } from './runtime-implementation.js'\n\nconst locks = new Map<unknown, Promise<void>>()\n\nfunction acquireLocalLock(name: unknown): Promise<() => void> {\n  return new Promise((resolveAcquire) => {\n    const prev = locks.get(name) ?? Promise.resolve()\n    const next = prev.then(() => {\n      return new Promise<void>((resolveRelease) => {\n        const release = () => {\n          // Only delete the lock if it is still the current one\n          if (locks.get(name) === next) locks.delete(name)\n\n          resolveRelease()\n        }\n\n        resolveAcquire(release)\n      })\n    })\n\n    locks.set(name, next)\n  })\n}\n\nexport const requestLocalLock: RuntimeLock = (name, fn) => {\n  return acquireLocalLock(name).then(async (release) => {\n    try {\n      return await fn()\n    } finally {\n      release()\n    }\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-authorization-server-metadata-resolver.ts",
    "content": "import {\n  OAuthAuthorizationServerMetadata,\n  oauthAuthorizationServerMetadataValidator,\n  oauthIssuerIdentifierSchema,\n} from '@atproto/oauth-types'\nimport {\n  Fetch,\n  FetchResponseError,\n  bindFetch,\n  cancelBody,\n} from '@atproto-labs/fetch'\nimport {\n  CachedGetter,\n  GetCachedOptions,\n  SimpleStore,\n} from '@atproto-labs/simple-store'\nimport { contentMime } from './util.js'\n\nexport type { GetCachedOptions, OAuthAuthorizationServerMetadata }\n\nexport type AuthorizationServerMetadataCache = SimpleStore<\n  string,\n  OAuthAuthorizationServerMetadata\n>\n\nexport type OAuthAuthorizationServerMetadataResolverConfig = {\n  allowHttpIssuer?: boolean\n}\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}\n */\nexport class OAuthAuthorizationServerMetadataResolver extends CachedGetter<\n  string,\n  OAuthAuthorizationServerMetadata\n> {\n  private readonly fetch: Fetch<unknown>\n  private readonly allowHttpIssuer: boolean\n\n  constructor(\n    cache: AuthorizationServerMetadataCache,\n    fetch?: Fetch,\n    config?: OAuthAuthorizationServerMetadataResolverConfig,\n  ) {\n    super(async (issuer, options) => this.fetchMetadata(issuer, options), cache)\n\n    this.fetch = bindFetch(fetch)\n    this.allowHttpIssuer = config?.allowHttpIssuer === true\n  }\n\n  async get(\n    input: string,\n    options?: GetCachedOptions,\n  ): Promise<OAuthAuthorizationServerMetadata> {\n    const issuer = oauthIssuerIdentifierSchema.parse(input)\n    if (!this.allowHttpIssuer && issuer.startsWith('http:')) {\n      throw new TypeError(\n        'Unsecure issuer URL protocol only allowed in development and test environments',\n      )\n    }\n    return super.get(issuer, options)\n  }\n\n  private async fetchMetadata(\n    issuer: string,\n    options?: GetCachedOptions,\n  ): Promise<OAuthAuthorizationServerMetadata> {\n    const url = new URL(`/.well-known/oauth-authorization-server`, issuer)\n    const request = new Request(url, {\n      headers: { accept: 'application/json' },\n      cache: options?.noCache ? 'no-cache' : undefined,\n      signal: options?.signal,\n      redirect: 'manual', // response must be 200 OK\n    })\n\n    const response = await this.fetch(request)\n\n    // https://datatracker.ietf.org/doc/html/rfc8414#section-3.2\n    if (response.status !== 200) {\n      await cancelBody(response, 'log')\n      throw await FetchResponseError.from(\n        response,\n        `Unexpected status code ${response.status} for \"${url}\"`,\n        undefined,\n        { cause: request },\n      )\n    }\n\n    if (contentMime(response.headers) !== 'application/json') {\n      await cancelBody(response, 'log')\n      throw await FetchResponseError.from(\n        response,\n        `Unexpected content type for \"${url}\"`,\n        undefined,\n        { cause: request },\n      )\n    }\n\n    const metadata = oauthAuthorizationServerMetadataValidator.parse(\n      await response.json(),\n    )\n\n    // Validate the issuer (MIX-UP attacks)\n    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-mix-up-attacks\n    // https://datatracker.ietf.org/doc/html/rfc8414#section-2\n    if (metadata.issuer !== issuer) {\n      throw new TypeError(`Invalid issuer ${metadata.issuer}`)\n    }\n\n    // ATPROTO requires client_id_metadata_document\n    // https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/\n    if (metadata.client_id_metadata_document_supported !== true) {\n      throw new TypeError(\n        `Authorization server \"${issuer}\" does not support client_id_metadata_document`,\n      )\n    }\n\n    return metadata\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-callback-error.ts",
    "content": "export class OAuthCallbackError extends Error {\n  static from(err: unknown, params: URLSearchParams, state?: string) {\n    if (err instanceof OAuthCallbackError) return err\n    const message = err instanceof Error ? err.message : undefined\n    return new OAuthCallbackError(params, message, state, err)\n  }\n\n  constructor(\n    public readonly params: URLSearchParams,\n    message = params.get('error_description') || 'OAuth callback error',\n    public readonly state?: string,\n    cause?: unknown,\n  ) {\n    super(message, { cause })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-client-auth.ts",
    "content": "import { Keyset } from '@atproto/jwk'\nimport {\n  CLIENT_ASSERTION_TYPE_JWT_BEARER,\n  OAuthAuthorizationServerMetadata,\n  OAuthClientCredentials,\n} from '@atproto/oauth-types'\nimport { FALLBACK_ALG } from './constants.js'\nimport { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'\nimport { Runtime } from './runtime.js'\nimport { ClientMetadata } from './types.js'\nimport { Awaitable } from './util.js'\n\nexport type ClientAuthMethod =\n  | { method: 'none' }\n  | { method: 'private_key_jwt'; kid: string }\n\nexport function negotiateClientAuthMethod(\n  serverMetadata: OAuthAuthorizationServerMetadata,\n  clientMetadata: ClientMetadata,\n  keyset?: Keyset,\n): ClientAuthMethod {\n  const method = clientMetadata.token_endpoint_auth_method\n\n  // @NOTE ATproto spec requires that AS support both \"none\" and\n  // \"private_key_jwt\", and that clients use one of the other. The following\n  // check ensures that the AS is indeed compliant with this client's\n  // configuration.\n  const methods = supportedMethods(serverMetadata)\n  if (!methods.includes(method)) {\n    throw new Error(\n      `The server does not support \"${method}\" authentication. Supported methods are: ${methods.join(\n        ', ',\n      )}.`,\n    )\n  }\n\n  if (method === 'private_key_jwt') {\n    // Invalid client configuration. This should not happen as\n    // \"validateClientMetadata\" already check this.\n    if (!keyset) throw new Error('A keyset is required for private_key_jwt')\n\n    const alg = supportedAlgs(serverMetadata)\n\n    // @NOTE we can't use `keyset.findPrivateKey` here because we can't enforce\n    // that the returned key contains a \"kid\". The following implementation is\n    // more robust against keysets containing keys without a \"kid\" property.\n    for (const key of keyset.list({ alg, usage: 'sign' })) {\n      // Return the first key from the key set that matches the server's\n      // supported algorithms.\n      if (key.kid) return { method: 'private_key_jwt', kid: key.kid }\n    }\n\n    throw new Error(\n      alg.includes(FALLBACK_ALG)\n        ? `Client authentication method \"${method}\" requires at least one \"${FALLBACK_ALG}\" signing key with a \"kid\" property`\n        : // AS is not compliant with the ATproto OAuth spec.\n          `Authorization server requires \"${method}\" authentication method, but does not support \"${FALLBACK_ALG}\" algorithm.`,\n    )\n  }\n\n  if (method === 'none') {\n    return { method: 'none' }\n  }\n\n  throw new Error(\n    `The ATProto OAuth spec requires that client use either \"none\" or \"private_key_jwt\" authentication method.` +\n      (method === 'client_secret_basic'\n        ? ' You might want to explicitly set \"token_endpoint_auth_method\" to one of those values in the client metadata document.'\n        : ` You set \"${method}\" which is not allowed.`),\n  )\n}\n\nexport type ClientCredentialsFactory = () => Awaitable<{\n  headers?: Record<string, string>\n  payload?: OAuthClientCredentials\n}>\n\n/**\n * @throws {AuthMethodUnsatisfiableError} if the authentication method is no\n * long usable (either because the AS changed, of because the key is no longer\n * available in the keyset).\n */\nexport function createClientCredentialsFactory(\n  authMethod: ClientAuthMethod,\n  serverMetadata: OAuthAuthorizationServerMetadata,\n  clientMetadata: ClientMetadata,\n  runtime: Runtime,\n  keyset?: Keyset,\n): ClientCredentialsFactory {\n  // Ensure the AS still supports the auth method.\n  if (!supportedMethods(serverMetadata).includes(authMethod.method)) {\n    throw new AuthMethodUnsatisfiableError(\n      `Client authentication method \"${authMethod.method}\" no longer supported`,\n    )\n  }\n\n  if (authMethod.method === 'none') {\n    return () => ({\n      payload: {\n        client_id: clientMetadata.client_id,\n      },\n    })\n  }\n\n  if (authMethod.method === 'private_key_jwt') {\n    try {\n      // The client used to be a confidential client but no longer has a keyset.\n      if (!keyset) throw new Error('A keyset is required for private_key_jwt')\n\n      // @NOTE throws if no matching key can be found\n      const { key, alg } = keyset.findPrivateKey({\n        usage: 'sign',\n        kid: authMethod.kid,\n        alg: supportedAlgs(serverMetadata),\n      })\n\n      // https://www.rfc-editor.org/rfc/rfc7523.html#section-3\n      return async () => ({\n        payload: {\n          client_id: clientMetadata.client_id,\n          client_assertion_type: CLIENT_ASSERTION_TYPE_JWT_BEARER,\n          client_assertion: await key.createJwt(\n            { alg },\n            {\n              // > The JWT MUST contain an \"iss\" (issuer) claim that contains a\n              // > unique identifier for the entity that issued the JWT.\n              iss: clientMetadata.client_id,\n              // > For client authentication, the subject MUST be the\n              // > \"client_id\" of the OAuth client.\n              sub: clientMetadata.client_id,\n              // > The JWT MUST contain an \"aud\" (audience) claim containing a value\n              // > that identifies the authorization server as an intended audience.\n              // > The token endpoint URL of the authorization server MAY be used as a\n              // > value for an \"aud\" element to identify the authorization server as an\n              // > intended audience of the JWT.\n              aud: serverMetadata.issuer,\n              // > The JWT MAY contain a \"jti\" (JWT ID) claim that provides a\n              // > unique identifier for the token.\n              jti: await runtime.generateNonce(),\n              // > The JWT MAY contain an \"iat\" (issued at) claim that\n              // > identifies the time at which the JWT was issued.\n              iat: Math.floor(Date.now() / 1000),\n              // > The JWT MUST contain an \"exp\" (expiration time) claim that\n              // > limits the time window during which the JWT can be used.\n              exp: Math.floor(Date.now() / 1000) + 60, // 1 minute\n            },\n          ),\n        },\n      })\n    } catch (cause) {\n      throw new AuthMethodUnsatisfiableError('Failed to load private key', {\n        cause,\n      })\n    }\n  }\n\n  throw new AuthMethodUnsatisfiableError(\n    // @ts-expect-error\n    `Unsupported auth method ${authMethod.method}`,\n  )\n}\n\nfunction supportedMethods(serverMetadata: OAuthAuthorizationServerMetadata) {\n  return serverMetadata['token_endpoint_auth_methods_supported']\n}\n\nfunction supportedAlgs(serverMetadata: OAuthAuthorizationServerMetadata) {\n  return (\n    serverMetadata['token_endpoint_auth_signing_alg_values_supported'] ?? [\n      // @NOTE If not specified, assume that the server supports the ES256\n      // algorithm, as prescribed by the spec:\n      //\n      // > Clients and Authorization Servers currently must support the ES256\n      // > cryptographic system [for client authentication].\n      //\n      // https://atproto.com/specs/oauth#confidential-client-authentication\n      FALLBACK_ALG,\n    ]\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-client.ts",
    "content": "import { Key, Keyset } from '@atproto/jwk'\nimport {\n  OAuthAuthorizationRequestParameters,\n  OAuthClientIdDiscoverable,\n  OAuthClientMetadata,\n  OAuthClientMetadataInput,\n  OAuthResponseMode,\n  oauthClientMetadataSchema,\n} from '@atproto/oauth-types'\nimport {\n  AtprotoDid,\n  DidCache,\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  type DidResolverCommonOptions,\n  assertAtprotoDid,\n} from '@atproto-labs/did-resolver'\nimport { Fetch } from '@atproto-labs/fetch'\nimport { HandleCache, HandleResolver } from '@atproto-labs/handle-resolver'\nimport { HANDLE_INVALID } from '@atproto-labs/identity-resolver'\nimport { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'\nimport { FALLBACK_ALG } from './constants.js'\nimport { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'\nimport { TokenRevokedError } from './errors/token-revoked-error.js'\nimport {\n  CreateIdentityResolverOptions,\n  createIdentityResolver,\n} from './identity-resolver.js'\nimport {\n  AuthorizationServerMetadataCache,\n  OAuthAuthorizationServerMetadataResolver,\n} from './oauth-authorization-server-metadata-resolver.js'\nimport { OAuthCallbackError } from './oauth-callback-error.js'\nimport { negotiateClientAuthMethod } from './oauth-client-auth.js'\nimport {\n  OAuthProtectedResourceMetadataResolver,\n  ProtectedResourceMetadataCache,\n} from './oauth-protected-resource-metadata-resolver.js'\nimport { OAuthResolver } from './oauth-resolver.js'\nimport { DpopNonceCache, OAuthServerAgent } from './oauth-server-agent.js'\nimport { OAuthServerFactory } from './oauth-server-factory.js'\nimport { OAuthSession } from './oauth-session.js'\nimport { RuntimeImplementation } from './runtime-implementation.js'\nimport { Runtime } from './runtime.js'\nimport {\n  SessionGetter,\n  SessionHooks,\n  SessionStore,\n  isExpectedSessionError,\n} from './session-getter.js'\nimport { InternalStateData, StateStore } from './state-store.js'\nimport { AuthorizeOptions, CallbackOptions, ClientMetadata } from './types.js'\nimport { validateClientMetadata } from './validate-client-metadata.js'\n\n// Export all types needed to construct OAuthClientOptions\nexport type {\n  AuthorizationServerMetadataCache,\n  CreateIdentityResolverOptions,\n  DidCache,\n  DpopNonceCache,\n  Fetch,\n  HandleCache,\n  HandleResolver,\n  InternalStateData,\n  OAuthClientMetadata,\n  OAuthClientMetadataInput,\n  OAuthResponseMode,\n  ProtectedResourceMetadataCache,\n  RuntimeImplementation,\n  SessionHooks,\n  SessionStore,\n  StateStore,\n}\n\nexport { Key, Keyset }\n\nexport type OAuthClientOptions = {\n  // Config\n  responseMode: OAuthResponseMode\n  clientMetadata: Readonly<OAuthClientMetadataInput>\n  keyset?: Keyset | Iterable<Key | undefined | null | false>\n  /**\n   * Determines if the client will allow communicating with the OAuth Servers\n   * (Authorization & Resource), or to retrieve \"did:web\" documents, over\n   * unsafe HTTP connections. It is recommended to set this to `true` only for\n   * development purposes.\n   *\n   * @note This does not affect the identity resolution mechanism, which will\n   * allow HTTP connections to the PLC Directory (if the provided directory url\n   * is \"http:\" based).\n   * @default false\n   * @see {@link OAuthProtectedResourceMetadataResolver.allowHttpResource}\n   * @see {@link OAuthAuthorizationServerMetadataResolver.allowHttpIssuer}\n   * @see {@link DidResolverCommonOptions.allowHttp}\n   */\n  allowHttp?: boolean\n\n  // Stores\n  stateStore: StateStore\n  sessionStore: SessionStore\n  authorizationServerMetadataCache?: AuthorizationServerMetadataCache\n  protectedResourceMetadataCache?: ProtectedResourceMetadataCache\n  dpopNonceCache?: DpopNonceCache\n\n  // Services\n  runtimeImplementation: RuntimeImplementation\n  fetch?: Fetch\n} & CreateIdentityResolverOptions &\n  SessionHooks\n\nexport type OAuthClientFetchMetadataOptions = {\n  clientId: OAuthClientIdDiscoverable\n  fetch?: Fetch\n  signal?: AbortSignal\n}\n\nexport class OAuthClient {\n  static async fetchMetadata({\n    clientId,\n    fetch = globalThis.fetch,\n    signal,\n  }: OAuthClientFetchMetadataOptions) {\n    signal?.throwIfAborted()\n\n    const request = new Request(clientId, {\n      redirect: 'error',\n      signal: signal,\n    })\n    const response = await fetch(request)\n\n    if (response.status !== 200) {\n      response.body?.cancel?.()\n      throw new TypeError(`Failed to fetch client metadata: ${response.status}`)\n    }\n\n    // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html#section-4.1\n    const mime = response.headers.get('content-type')?.split(';')[0].trim()\n    if (mime !== 'application/json') {\n      response.body?.cancel?.()\n      throw new TypeError(`Invalid client metadata content type: ${mime}`)\n    }\n\n    const json: unknown = await response.json()\n\n    signal?.throwIfAborted()\n\n    return oauthClientMetadataSchema.parse(json)\n  }\n\n  // Config\n  readonly clientMetadata: ClientMetadata\n  readonly responseMode: OAuthResponseMode\n  readonly keyset?: Keyset\n\n  // Services\n  readonly runtime: Runtime\n  readonly fetch: Fetch\n  readonly oauthResolver: OAuthResolver\n  readonly serverFactory: OAuthServerFactory\n\n  // Stores\n  protected readonly sessionGetter: SessionGetter\n  protected readonly stateStore: StateStore\n\n  constructor(options: OAuthClientOptions) {\n    const {\n      stateStore,\n      sessionStore,\n\n      dpopNonceCache = new SimpleStoreMemory({ ttl: 60e3, max: 100 }),\n      authorizationServerMetadataCache = new SimpleStoreMemory({\n        ttl: 60e3,\n        max: 100,\n      }),\n      protectedResourceMetadataCache = new SimpleStoreMemory({\n        ttl: 60e3,\n        max: 100,\n      }),\n\n      responseMode,\n      clientMetadata,\n      runtimeImplementation,\n      keyset,\n    } = options\n\n    this.keyset = keyset\n      ? keyset instanceof Keyset\n        ? keyset\n        : new Keyset(keyset)\n      : undefined\n    this.clientMetadata = validateClientMetadata(clientMetadata, this.keyset)\n    this.responseMode = responseMode\n\n    this.runtime = new Runtime(runtimeImplementation)\n    this.fetch = options.fetch ?? globalThis.fetch\n    this.oauthResolver = new OAuthResolver(\n      createIdentityResolver(options),\n      new OAuthProtectedResourceMetadataResolver(\n        protectedResourceMetadataCache,\n        this.fetch,\n        { allowHttpResource: options.allowHttp },\n      ),\n      new OAuthAuthorizationServerMetadataResolver(\n        authorizationServerMetadataCache,\n        this.fetch,\n        { allowHttpIssuer: options.allowHttp },\n      ),\n    )\n    this.serverFactory = new OAuthServerFactory(\n      this.clientMetadata,\n      this.runtime,\n      this.oauthResolver,\n      this.fetch,\n      this.keyset,\n      dpopNonceCache,\n    )\n\n    this.stateStore = stateStore\n    this.sessionGetter = new SessionGetter(\n      sessionStore,\n      this.serverFactory,\n      this.runtime,\n      options,\n    )\n  }\n\n  // Exposed as public API for convenience\n  get identityResolver() {\n    return this.oauthResolver.identityResolver\n  }\n\n  get jwks() {\n    return this.keyset?.publicJwks ?? ({ keys: [] as const } as const)\n  }\n\n  async authorize(\n    input: string,\n    { signal, ...options }: AuthorizeOptions = {},\n  ): Promise<URL> {\n    const redirectUri =\n      options?.redirect_uri ?? this.clientMetadata.redirect_uris[0]\n    if (!this.clientMetadata.redirect_uris.includes(redirectUri)) {\n      // The server will enforce this, but let's catch it early\n      throw new TypeError('Invalid redirect_uri')\n    }\n\n    const { identityInfo, metadata } = await this.oauthResolver.resolve(input, {\n      signal,\n    })\n\n    const pkce = await this.runtime.generatePKCE()\n    const dpopKey = await this.runtime.generateKey(\n      metadata.dpop_signing_alg_values_supported || [FALLBACK_ALG],\n    )\n\n    const authMethod = negotiateClientAuthMethod(\n      metadata,\n      this.clientMetadata,\n      this.keyset,\n    )\n    const state = await this.runtime.generateNonce()\n\n    await this.stateStore.set(state, {\n      iss: metadata.issuer,\n      dpopKey,\n      authMethod,\n      verifier: pkce.verifier,\n      appState: options?.state,\n    })\n\n    const parameters: OAuthAuthorizationRequestParameters = {\n      ...options,\n\n      client_id: this.clientMetadata.client_id,\n      redirect_uri: redirectUri,\n      code_challenge: pkce.challenge,\n      code_challenge_method: pkce.method,\n      state,\n      login_hint: identityInfo\n        ? identityInfo.handle !== HANDLE_INVALID\n          ? identityInfo.handle\n          : identityInfo.did\n        : undefined,\n      response_mode: this.responseMode,\n      response_type: 'code' as const,\n      scope: options?.scope ?? this.clientMetadata.scope,\n    }\n\n    const authorizationUrl = new URL(metadata.authorization_endpoint)\n\n    // Since the user will be redirected to the authorization_endpoint url using\n    // a browser, we need to make sure that the url is valid.\n    if (\n      authorizationUrl.protocol !== 'https:' &&\n      authorizationUrl.protocol !== 'http:'\n    ) {\n      throw new TypeError(\n        `Invalid authorization endpoint protocol: ${authorizationUrl.protocol}`,\n      )\n    }\n\n    if (metadata.pushed_authorization_request_endpoint) {\n      const server = await this.serverFactory.fromMetadata(\n        metadata,\n        authMethod,\n        dpopKey,\n      )\n      const parResponse = await server.request(\n        'pushed_authorization_request',\n        parameters,\n      )\n\n      authorizationUrl.searchParams.set(\n        'client_id',\n        this.clientMetadata.client_id,\n      )\n      authorizationUrl.searchParams.set('request_uri', parResponse.request_uri)\n      return authorizationUrl\n    } else if (metadata.require_pushed_authorization_requests) {\n      throw new Error(\n        'Server requires pushed authorization requests (PAR) but no PAR endpoint is available',\n      )\n    } else {\n      for (const [key, value] of Object.entries(parameters)) {\n        if (value) authorizationUrl.searchParams.set(key, String(value))\n      }\n\n      // Length of the URL that will be sent to the server\n      const urlLength =\n        authorizationUrl.pathname.length + authorizationUrl.search.length\n      if (urlLength < 2048) {\n        return authorizationUrl\n      } else if (!metadata.pushed_authorization_request_endpoint) {\n        throw new Error('Login URL too long')\n      }\n    }\n\n    throw new Error(\n      'Server does not support pushed authorization requests (PAR)',\n    )\n  }\n\n  /**\n   * This method allows the client to proactively revoke the request_uri it\n   * created through PAR.\n   */\n  async abortRequest(authorizeUrl: URL) {\n    const requestUri = authorizeUrl.searchParams.get('request_uri')\n    if (!requestUri) return\n\n    // @NOTE This is not implemented here because, 1) the request server should\n    // invalidate the request_uri after some delay anyways, and 2) I am not sure\n    // that the revocation endpoint is even supposed to support this (and I\n    // don't want to spend the time checking now).\n\n    // @TODO investigate actual necessity & feasibility of this feature\n  }\n\n  async callback(\n    params: URLSearchParams,\n    options: CallbackOptions = {},\n  ): Promise<{\n    session: OAuthSession\n    state: string | null\n  }> {\n    const responseJwt = params.get('response')\n    if (responseJwt != null) {\n      // https://openid.net/specs/oauth-v2-jarm.html\n      throw new OAuthCallbackError(params, 'JARM not supported')\n    }\n\n    const issuerParam = params.get('iss')\n    const stateParam = params.get('state')\n    const errorParam = params.get('error')\n    const codeParam = params.get('code')\n\n    if (!stateParam) {\n      throw new OAuthCallbackError(params, 'Missing \"state\" parameter')\n    }\n    const stateData = await this.stateStore.get(stateParam)\n    if (stateData) {\n      // Prevent any kind of replay\n      await this.stateStore.del(stateParam)\n    } else {\n      throw new OAuthCallbackError(\n        params,\n        `Unknown authorization session \"${stateParam}\"`,\n      )\n    }\n\n    try {\n      if (errorParam != null) {\n        throw new OAuthCallbackError(params, undefined, stateData.appState)\n      }\n\n      if (!codeParam) {\n        throw new OAuthCallbackError(\n          params,\n          'Missing \"code\" query param',\n          stateData.appState,\n        )\n      }\n\n      const server = await this.serverFactory.fromIssuer(\n        stateData.iss,\n        stateData.authMethod,\n        stateData.dpopKey,\n      )\n\n      if (issuerParam != null) {\n        if (!server.issuer) {\n          throw new OAuthCallbackError(\n            params,\n            'Issuer not found in metadata',\n            stateData.appState,\n          )\n        }\n        if (server.issuer !== issuerParam) {\n          throw new OAuthCallbackError(\n            params,\n            'Issuer mismatch',\n            stateData.appState,\n          )\n        }\n      } else if (\n        server.serverMetadata.authorization_response_iss_parameter_supported\n      ) {\n        throw new OAuthCallbackError(\n          params,\n          'iss missing from the response',\n          stateData.appState,\n        )\n      }\n\n      const tokenSet = await server.exchangeCode(\n        codeParam,\n        stateData.verifier,\n        options?.redirect_uri ?? server.clientMetadata.redirect_uris[0],\n      )\n\n      // We revoke any existing session first to avoid leaving orphaned sessions\n      // on the AS.\n      try {\n        await this.revoke(tokenSet.sub)\n      } catch {\n        // No existing session, or failed to get it. This is fine.\n      }\n\n      try {\n        await this.sessionGetter.setStored(tokenSet.sub, {\n          dpopKey: stateData.dpopKey,\n          authMethod: server.authMethod,\n          tokenSet,\n        })\n\n        const session = this.createSession(server, tokenSet.sub)\n\n        return { session, state: stateData.appState ?? null }\n      } catch (err) {\n        await server.revoke(tokenSet.refresh_token || tokenSet.access_token)\n\n        throw err\n      }\n    } catch (err) {\n      // Make sure, whatever the underlying error, that the appState is\n      // available in the calling code\n      throw OAuthCallbackError.from(err, params, stateData.appState)\n    }\n  }\n\n  /**\n   * Load a stored session. This will refresh the token only if needed (about to\n   * expire) by default.\n   *\n   * @see {@link SessionGetter.restore}\n   */\n  async restore(\n    sub: string,\n    refresh: boolean | 'auto' = 'auto',\n  ): Promise<OAuthSession> {\n    // sub arg is lightly typed for convenience of library user\n    assertAtprotoDid(sub)\n\n    const { dpopKey, authMethod, tokenSet } =\n      await this.sessionGetter.getSession(sub, refresh)\n\n    try {\n      const server = await this.serverFactory.fromIssuer(\n        tokenSet.iss,\n        authMethod,\n        dpopKey,\n        {\n          noCache: refresh === true,\n          allowStale: refresh === false,\n        },\n      )\n\n      return this.createSession(server, sub)\n    } catch (err) {\n      if (err instanceof AuthMethodUnsatisfiableError) {\n        await this.sessionGetter.delStored(sub, err)\n      }\n\n      throw err\n    }\n  }\n\n  async revoke(sub: string) {\n    // sub arg is lightly typed for convenience of library user\n    assertAtprotoDid(sub)\n\n    const res = await this.sessionGetter.getSession(sub, false).catch((err) => {\n      if (isExpectedSessionError(err)) return null\n      throw err\n    })\n\n    if (!res) return\n\n    const { dpopKey, authMethod, tokenSet } = res\n\n    // NOT using `;(await this.restore(sub, false)).signOut()` because we want\n    // the tokens to be deleted even if it was not possible to fetch the issuer\n    // data.\n    try {\n      const server = await this.serverFactory.fromIssuer(\n        tokenSet.iss,\n        authMethod,\n        dpopKey,\n      )\n      await server.revoke(tokenSet.access_token)\n    } finally {\n      await this.sessionGetter.delStored(sub, new TokenRevokedError(sub))\n    }\n  }\n\n  protected createSession(\n    server: OAuthServerAgent,\n    sub: AtprotoDid,\n  ): OAuthSession {\n    return new OAuthSession(server, sub, this.sessionGetter, this.fetch)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-protected-resource-metadata-resolver.ts",
    "content": "import {\n  OAuthProtectedResourceMetadata,\n  oauthProtectedResourceMetadataSchema,\n} from '@atproto/oauth-types'\nimport {\n  Fetch,\n  FetchResponseError,\n  bindFetch,\n  cancelBody,\n} from '@atproto-labs/fetch'\nimport {\n  CachedGetter,\n  GetCachedOptions,\n  SimpleStore,\n} from '@atproto-labs/simple-store'\nimport { contentMime } from './util.js'\n\nexport type { GetCachedOptions, OAuthProtectedResourceMetadata }\n\nexport type ProtectedResourceMetadataCache = SimpleStore<\n  string,\n  OAuthProtectedResourceMetadata\n>\n\nexport type OAuthProtectedResourceMetadataResolverConfig = {\n  allowHttpResource?: boolean\n}\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html}\n */\nexport class OAuthProtectedResourceMetadataResolver extends CachedGetter<\n  string,\n  OAuthProtectedResourceMetadata\n> {\n  private readonly fetch: Fetch<unknown>\n  private readonly allowHttpResource: boolean\n\n  constructor(\n    cache: ProtectedResourceMetadataCache,\n    fetch: Fetch = globalThis.fetch,\n    config?: OAuthProtectedResourceMetadataResolverConfig,\n  ) {\n    super(async (origin, options) => this.fetchMetadata(origin, options), cache)\n\n    this.fetch = bindFetch(fetch)\n    this.allowHttpResource = config?.allowHttpResource === true\n  }\n\n  async get(\n    resource: string | URL,\n    options?: GetCachedOptions,\n  ): Promise<OAuthProtectedResourceMetadata> {\n    const { protocol, origin } = new URL(resource)\n\n    if (protocol !== 'https:' && protocol !== 'http:') {\n      throw new TypeError(\n        `Invalid protected resource metadata URL protocol: ${protocol}`,\n      )\n    }\n\n    if (protocol === 'http:' && !this.allowHttpResource) {\n      throw new TypeError(\n        `Unsecure resource metadata URL (${protocol}) only allowed in development and test environments`,\n      )\n    }\n\n    return super.get(origin, options)\n  }\n\n  private async fetchMetadata(\n    origin: string,\n    options?: GetCachedOptions,\n  ): Promise<OAuthProtectedResourceMetadata> {\n    const url = new URL(`/.well-known/oauth-protected-resource`, origin)\n    const request = new Request(url, {\n      signal: options?.signal,\n      headers: { accept: 'application/json' },\n      cache: options?.noCache ? 'no-cache' : undefined,\n      redirect: 'manual', // response must be 200 OK\n    })\n\n    const response = await this.fetch(request)\n\n    // https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2\n    if (response.status !== 200) {\n      await cancelBody(response, 'log')\n      throw await FetchResponseError.from(\n        response,\n        `Unexpected status code ${response.status} for \"${url}\"`,\n        undefined,\n        { cause: request },\n      )\n    }\n\n    if (contentMime(response.headers) !== 'application/json') {\n      await cancelBody(response, 'log')\n      throw await FetchResponseError.from(\n        response,\n        `Unexpected content type for \"${url}\"`,\n        undefined,\n        { cause: request },\n      )\n    }\n\n    const metadata = oauthProtectedResourceMetadataSchema.parse(\n      await response.json(),\n    )\n\n    // https://www.rfc-editor.org/rfc/rfc9728.html#section-3.3\n    if (metadata.resource !== origin) {\n      throw new TypeError(`Invalid issuer ${metadata.resource}`)\n    }\n\n    return metadata\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-resolver-error.ts",
    "content": "import { ZodError } from 'zod'\n\nexport class OAuthResolverError extends Error {\n  constructor(message: string, options?: { cause?: unknown }) {\n    super(message, options)\n  }\n\n  static from(cause: unknown, message?: string): OAuthResolverError {\n    if (cause instanceof OAuthResolverError) return cause\n    const validationReason =\n      cause instanceof ZodError\n        ? `${cause.errors[0].path} ${cause.errors[0].message}`\n        : null\n    const fullMessage =\n      (message ?? `Unable to resolve identity`) +\n      (validationReason ? ` (${validationReason})` : '')\n    return new OAuthResolverError(fullMessage, {\n      cause,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-resolver.ts",
    "content": "import { extractPdsUrl } from '@atproto/did'\nimport {\n  OAuthAuthorizationServerMetadata,\n  oauthIssuerIdentifierSchema,\n} from '@atproto/oauth-types'\nimport {\n  IdentityInfo,\n  IdentityResolver,\n  ResolveIdentityOptions,\n} from '@atproto-labs/identity-resolver'\nimport {\n  GetCachedOptions,\n  OAuthAuthorizationServerMetadataResolver,\n} from './oauth-authorization-server-metadata-resolver.js'\nimport { OAuthProtectedResourceMetadataResolver } from './oauth-protected-resource-metadata-resolver.js'\nimport { OAuthResolverError } from './oauth-resolver-error.js'\n\nexport type { GetCachedOptions }\nexport type ResolveOAuthOptions = GetCachedOptions & ResolveIdentityOptions\n\nexport class OAuthResolver {\n  constructor(\n    readonly identityResolver: IdentityResolver,\n    readonly protectedResourceMetadataResolver: OAuthProtectedResourceMetadataResolver,\n    readonly authorizationServerMetadataResolver: OAuthAuthorizationServerMetadataResolver,\n  ) {}\n\n  /**\n   * @param input - A handle, DID, PDS URL or Entryway URL\n   */\n  public async resolve(\n    input: string,\n    options?: ResolveOAuthOptions,\n  ): Promise<{\n    identityInfo?: IdentityInfo\n    metadata: OAuthAuthorizationServerMetadata\n  }> {\n    // Allow using an entryway, or PDS url, directly as login input (e.g.\n    // when the user forgot their handle, or when the handle does not\n    // resolve to a DID)\n    return /^https?:\\/\\//.test(input)\n      ? this.resolveFromService(input, options)\n      : this.resolveFromIdentity(input, options)\n  }\n\n  /**\n   * @note this method can be used to verify if a particular uri supports OAuth\n   * based sign-in (for compatibility with legacy implementation).\n   */\n  public async resolveFromService(\n    input: string,\n    options?: ResolveOAuthOptions,\n  ): Promise<{\n    metadata: OAuthAuthorizationServerMetadata\n  }> {\n    try {\n      // Assume first that input is a PDS URL (as required by ATPROTO)\n      const metadata = await this.getResourceServerMetadata(input, options)\n      return { metadata }\n    } catch (err) {\n      if (!options?.signal?.aborted && err instanceof OAuthResolverError) {\n        try {\n          // Fallback to trying to fetch as an issuer (Entryway)\n          const result = oauthIssuerIdentifierSchema.safeParse(input)\n          if (result.success) {\n            const metadata = await this.getAuthorizationServerMetadata(\n              result.data,\n              options,\n            )\n            return { metadata }\n          }\n        } catch {\n          // Fallback failed, throw original error\n        }\n      }\n\n      throw err\n    }\n  }\n\n  public async resolveFromIdentity(\n    input: string,\n    options?: ResolveOAuthOptions,\n  ): Promise<{\n    identityInfo: IdentityInfo\n    metadata: OAuthAuthorizationServerMetadata\n    pds: URL\n  }> {\n    const identityInfo = await this.resolveIdentity(input, options)\n\n    options?.signal?.throwIfAborted()\n\n    const pds = extractPdsUrl(identityInfo.didDoc)\n\n    const metadata = await this.getResourceServerMetadata(pds, options)\n\n    return { identityInfo, metadata, pds }\n  }\n\n  public async resolveIdentity(\n    input: string,\n    options?: ResolveIdentityOptions,\n  ): Promise<IdentityInfo> {\n    try {\n      return await this.identityResolver.resolve(input, options)\n    } catch (cause) {\n      throw OAuthResolverError.from(\n        cause,\n        `Failed to resolve identity: ${input}`,\n      )\n    }\n  }\n\n  public async getAuthorizationServerMetadata(\n    issuer: string,\n    options?: GetCachedOptions,\n  ): Promise<OAuthAuthorizationServerMetadata> {\n    try {\n      return await this.authorizationServerMetadataResolver.get(issuer, options)\n    } catch (cause) {\n      throw OAuthResolverError.from(\n        cause,\n        `Failed to resolve OAuth server metadata for issuer: ${issuer}`,\n      )\n    }\n  }\n\n  public async getResourceServerMetadata(\n    pdsUrl: string | URL,\n    options?: GetCachedOptions,\n  ) {\n    try {\n      const rsMetadata = await this.protectedResourceMetadataResolver.get(\n        pdsUrl,\n        options,\n      )\n\n      // ATPROTO requires one, and only one, authorization server entry\n      if (rsMetadata.authorization_servers?.length !== 1) {\n        throw new OAuthResolverError(\n          rsMetadata.authorization_servers?.length\n            ? `Unable to determine authorization server for PDS: ${pdsUrl}`\n            : `No authorization servers found for PDS: ${pdsUrl}`,\n        )\n      }\n\n      const issuer = rsMetadata.authorization_servers![0]!\n\n      options?.signal?.throwIfAborted()\n\n      const asMetadata = await this.getAuthorizationServerMetadata(\n        issuer,\n        options,\n      )\n\n      // https://www.rfc-editor.org/rfc/rfc9728.html#section-4\n      if (asMetadata.protected_resources) {\n        if (!asMetadata.protected_resources.includes(rsMetadata.resource)) {\n          throw new OAuthResolverError(\n            `PDS \"${pdsUrl}\" not protected by issuer \"${issuer}\"`,\n          )\n        }\n      }\n\n      return asMetadata\n    } catch (cause) {\n      throw OAuthResolverError.from(\n        cause,\n        `Failed to resolve OAuth server metadata for resource: ${pdsUrl}`,\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-response-error.ts",
    "content": "import { Json } from '@atproto-labs/fetch'\nimport { ifString } from './util.js'\n\nexport class OAuthResponseError extends Error {\n  readonly error?: string\n  readonly errorDescription?: string\n\n  constructor(\n    public readonly response: Response,\n    public readonly payload: Json,\n  ) {\n    const objPayload = typeof payload === 'object' ? payload : undefined\n    const error = ifString(objPayload?.['error'])\n    const errorDescription = ifString(objPayload?.['error_description'])\n\n    const messageError = error ? `\"${error}\"` : 'unknown'\n    const messageDesc = errorDescription ? `: ${errorDescription}` : ''\n    const message = `OAuth ${messageError} error${messageDesc}`\n\n    super(message)\n\n    this.error = error\n    this.errorDescription = errorDescription\n  }\n\n  get status() {\n    return this.response.status\n  }\n\n  get headers() {\n    return this.response.headers\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-server-agent.ts",
    "content": "import { AtprotoDid } from '@atproto/did'\nimport { Key, Keyset } from '@atproto/jwk'\nimport {\n  AtprotoOAuthScope,\n  AtprotoOAuthTokenResponse,\n  OAuthAuthorizationRequestPar,\n  OAuthAuthorizationServerMetadata,\n  OAuthEndpointName,\n  OAuthParResponse,\n  OAuthRedirectUri,\n  OAuthTokenRequest,\n  atprotoOAuthTokenResponseSchema,\n  oauthParResponseSchema,\n} from '@atproto/oauth-types'\nimport { Fetch, Json, bindFetch, fetchJsonProcessor } from '@atproto-labs/fetch'\nimport { SimpleStore } from '@atproto-labs/simple-store'\nimport { TokenRefreshError } from './errors/token-refresh-error.js'\nimport { dpopFetchWrapper } from './fetch-dpop.js'\nimport {\n  ClientAuthMethod,\n  ClientCredentialsFactory,\n  createClientCredentialsFactory,\n} from './oauth-client-auth.js'\nimport { OAuthResolver } from './oauth-resolver.js'\nimport { OAuthResponseError } from './oauth-response-error.js'\nimport { Runtime } from './runtime.js'\nimport { ClientMetadata } from './types.js'\n\nexport type { AtprotoOAuthScope, AtprotoOAuthTokenResponse }\n\nexport type TokenSet = {\n  iss: string\n  sub: AtprotoDid\n  aud: string\n  scope: AtprotoOAuthScope\n\n  refresh_token?: string\n  access_token: string\n  token_type: 'DPoP'\n  /** ISO Date */\n  expires_at?: string\n}\n\nexport type DpopNonceCache = SimpleStore<string, string>\n\nexport class OAuthServerAgent {\n  protected dpopFetch: Fetch<unknown>\n  protected clientCredentialsFactory: ClientCredentialsFactory\n\n  /**\n   * @throws see {@link createClientCredentialsFactory}\n   */\n  constructor(\n    readonly authMethod: ClientAuthMethod,\n    readonly dpopKey: Key,\n    readonly serverMetadata: OAuthAuthorizationServerMetadata,\n    readonly clientMetadata: ClientMetadata,\n    readonly dpopNonces: DpopNonceCache,\n    readonly oauthResolver: OAuthResolver,\n    readonly runtime: Runtime,\n    readonly keyset?: Keyset,\n    fetch?: Fetch,\n  ) {\n    this.clientCredentialsFactory = createClientCredentialsFactory(\n      authMethod,\n      serverMetadata,\n      clientMetadata,\n      runtime,\n      keyset,\n    )\n\n    this.dpopFetch = dpopFetchWrapper<void>({\n      fetch: bindFetch(fetch),\n      key: dpopKey,\n      supportedAlgs: serverMetadata.dpop_signing_alg_values_supported,\n      sha256: async (v) => runtime.sha256(v),\n      nonces: dpopNonces,\n      isAuthServer: true,\n    })\n  }\n\n  get issuer() {\n    return this.serverMetadata.issuer\n  }\n\n  async revoke(token: string) {\n    try {\n      await this.request('revocation', { token })\n    } catch {\n      // Don't care\n    }\n  }\n\n  async exchangeCode(\n    code: string,\n    codeVerifier?: string,\n    redirectUri?: OAuthRedirectUri,\n  ): Promise<TokenSet> {\n    const now = Date.now()\n\n    const tokenResponse = await this.request('token', {\n      grant_type: 'authorization_code',\n      // redirectUri should always be passed by the calling code, but if it is\n      // not, default to the first redirect_uri registered for the client:\n      redirect_uri: redirectUri ?? this.clientMetadata.redirect_uris[0],\n      code,\n      code_verifier: codeVerifier,\n    })\n\n    try {\n      // /!\\ IMPORTANT /!\\\n      //\n      // The tokenResponse MUST always be valid before the \"sub\" it contains\n      // can be trusted (see Atproto's OAuth spec for details).\n      const aud = await this.verifyIssuer(tokenResponse.sub)\n\n      return {\n        aud,\n        sub: tokenResponse.sub,\n        iss: this.issuer,\n\n        scope: tokenResponse.scope,\n        refresh_token: tokenResponse.refresh_token,\n        access_token: tokenResponse.access_token,\n        token_type: tokenResponse.token_type,\n\n        expires_at:\n          typeof tokenResponse.expires_in === 'number'\n            ? new Date(now + tokenResponse.expires_in * 1000).toISOString()\n            : undefined,\n      }\n    } catch (err) {\n      await this.revoke(tokenResponse.access_token)\n\n      throw err\n    }\n  }\n\n  async refresh(tokenSet: TokenSet): Promise<TokenSet> {\n    if (!tokenSet.refresh_token) {\n      throw new TokenRefreshError(tokenSet.sub, 'No refresh token available')\n    }\n\n    // /!\\ IMPORTANT /!\\\n    //\n    // The \"sub\" MUST be a DID, whose issuer authority is indeed the server we\n    // are trying to obtain credentials from. Note that we are doing this\n    // *before* we actually try to refresh the token:\n    // 1) To avoid unnecessary refresh\n    // 2) So that the refresh is the last async operation, ensuring as few\n    //    async operations happen before the result gets a chance to be stored.\n    const aud = await this.verifyIssuer(tokenSet.sub)\n\n    const now = Date.now()\n\n    const tokenResponse = await this.request('token', {\n      grant_type: 'refresh_token',\n      refresh_token: tokenSet.refresh_token,\n    })\n\n    return {\n      aud,\n      sub: tokenSet.sub,\n      iss: this.issuer,\n\n      scope: tokenResponse.scope,\n      refresh_token: tokenResponse.refresh_token,\n      access_token: tokenResponse.access_token,\n      token_type: tokenResponse.token_type,\n\n      expires_at:\n        typeof tokenResponse.expires_in === 'number'\n          ? new Date(now + tokenResponse.expires_in * 1000).toISOString()\n          : undefined,\n    }\n  }\n\n  /**\n   * VERY IMPORTANT ! Always call this to process token responses.\n   *\n   * Whenever an OAuth token response is received, we **MUST** verify that the\n   * \"sub\" is a DID, whose issuer authority is indeed the server we just\n   * obtained credentials from. This check is a critical step to actually be\n   * able to use the \"sub\" (DID) as being the actual user's identifier.\n   *\n   * @returns The user's PDS URL (the resource server for the user)\n   */\n  protected async verifyIssuer(sub: AtprotoDid): Promise<string> {\n    const resolved = await this.oauthResolver.resolveFromIdentity(sub, {\n      noCache: true,\n      allowStale: false,\n      signal: AbortSignal.timeout(10e3),\n    })\n\n    if (this.issuer !== resolved.metadata.issuer) {\n      // Best case scenario; the user switched PDS. Worst case scenario; a bad\n      // actor is trying to impersonate a user. In any case, we must not allow\n      // this token to be used.\n      throw new TypeError('Issuer mismatch')\n    }\n\n    return resolved.pds.href\n  }\n\n  async request<Endpoint extends OAuthEndpointName>(\n    endpoint: Endpoint,\n    payload: Endpoint extends 'token'\n      ? OAuthTokenRequest\n      : Endpoint extends 'pushed_authorization_request'\n        ? OAuthAuthorizationRequestPar\n        : Record<string, unknown>,\n  ): Promise<\n    Endpoint extends 'token'\n      ? AtprotoOAuthTokenResponse\n      : Endpoint extends 'pushed_authorization_request'\n        ? OAuthParResponse\n        : Json\n  >\n  async request(\n    endpoint: OAuthEndpointName,\n    payload: Record<string, unknown>,\n  ): Promise<unknown> {\n    const url = this.serverMetadata[`${endpoint}_endpoint`]\n    if (!url) throw new Error(`No ${endpoint} endpoint available`)\n\n    const auth = await this.clientCredentialsFactory()\n\n    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-3.2.2\n    // https://datatracker.ietf.org/doc/html/rfc7009#section-2.1\n    // https://datatracker.ietf.org/doc/html/rfc7662#section-2.1\n    // https://datatracker.ietf.org/doc/html/rfc9126#section-2\n    const { response, json } = await this.dpopFetch(url, {\n      method: 'POST',\n      headers: {\n        ...auth.headers,\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: wwwFormUrlEncode({ ...payload, ...auth.payload }),\n    }).then(fetchJsonProcessor())\n\n    if (response.ok) {\n      switch (endpoint) {\n        case 'token':\n          return atprotoOAuthTokenResponseSchema.parse(json)\n        case 'pushed_authorization_request':\n          return oauthParResponseSchema.parse(json)\n        default:\n          return json\n      }\n    } else {\n      throw new OAuthResponseError(response, json)\n    }\n  }\n}\n\nfunction wwwFormUrlEncode(payload: Record<string, undefined | unknown>) {\n  return new URLSearchParams(\n    Object.entries(payload)\n      .filter(entryHasDefinedValue)\n      .map(stringifyEntryValue),\n  ).toString()\n}\n\nfunction entryHasDefinedValue(\n  entry: [string, unknown],\n): entry is [string, null | NonNullable<unknown>] {\n  return entry[1] !== undefined\n}\n\nfunction stringifyEntryValue(entry: [string, unknown]): [string, string] {\n  const name = entry[0]\n  const value = entry[1]\n\n  switch (typeof value) {\n    case 'string':\n      return [name, value]\n    case 'number':\n    case 'boolean':\n      return [name, String(value)]\n    default: {\n      const enc = JSON.stringify(value)\n      if (enc === undefined) {\n        throw new Error(`Unsupported value type for ${name}: ${String(value)}`)\n      }\n      return [name, enc]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-server-factory.ts",
    "content": "import { Key, Keyset } from '@atproto/jwk'\nimport { OAuthAuthorizationServerMetadata } from '@atproto/oauth-types'\nimport { Fetch } from '@atproto-labs/fetch'\nimport { GetCachedOptions } from './oauth-authorization-server-metadata-resolver.js'\nimport { ClientAuthMethod } from './oauth-client-auth.js'\nimport { OAuthResolver } from './oauth-resolver.js'\nimport { DpopNonceCache, OAuthServerAgent } from './oauth-server-agent.js'\nimport { Runtime } from './runtime.js'\nimport { ClientMetadata } from './types.js'\n\nexport class OAuthServerFactory {\n  constructor(\n    readonly clientMetadata: ClientMetadata,\n    readonly runtime: Runtime,\n    readonly resolver: OAuthResolver,\n    readonly fetch: Fetch,\n    readonly keyset: Keyset | undefined,\n    readonly dpopNonceCache: DpopNonceCache,\n  ) {}\n\n  /**\n   * @param authMethod `undefined` means that we are restoring a session that\n   * was created before we started storing the `authMethod` in the session. In\n   * that case, we will use the first key from the keyset.\n   *\n   * Support for this might be removed in the future.\n   *\n   * @throws see {@link OAuthServerFactory.fromMetadata}\n   */\n  async fromIssuer(\n    issuer: string,\n    authMethod: ClientAuthMethod,\n    dpopKey: Key,\n    options?: GetCachedOptions,\n  ) {\n    const serverMetadata = await this.resolver.getAuthorizationServerMetadata(\n      issuer,\n      options,\n    )\n\n    return this.fromMetadata(serverMetadata, authMethod, dpopKey)\n  }\n\n  /**\n   * @throws see {@link OAuthServerAgent}\n   */\n  async fromMetadata(\n    serverMetadata: OAuthAuthorizationServerMetadata,\n    authMethod: ClientAuthMethod,\n    dpopKey: Key,\n  ) {\n    return new OAuthServerAgent(\n      authMethod,\n      dpopKey,\n      serverMetadata,\n      this.clientMetadata,\n      this.dpopNonceCache,\n      this.resolver,\n      this.runtime,\n      this.keyset,\n      this.fetch,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/oauth-session.ts",
    "content": "import { AtprotoDid } from '@atproto/did'\nimport {\n  AtprotoOAuthScope,\n  OAuthAuthorizationServerMetadata,\n} from '@atproto/oauth-types'\nimport { Fetch, bindFetch } from '@atproto-labs/fetch'\nimport { TokenInvalidError } from './errors/token-invalid-error.js'\nimport { TokenRevokedError } from './errors/token-revoked-error.js'\nimport { dpopFetchWrapper } from './fetch-dpop.js'\nimport { OAuthServerAgent, TokenSet } from './oauth-server-agent.js'\nimport { SessionGetter } from './session-getter.js'\n\nconst ReadableStream = globalThis.ReadableStream as\n  | typeof globalThis.ReadableStream\n  | undefined\n\nexport type { AtprotoDid, AtprotoOAuthScope }\nexport type TokenInfo = {\n  expiresAt?: Date\n  expired?: boolean\n  scope: AtprotoOAuthScope\n  iss: string\n  aud: string\n  sub: AtprotoDid\n}\n\nexport class OAuthSession {\n  protected dpopFetch: Fetch<unknown>\n\n  constructor(\n    public readonly server: OAuthServerAgent,\n    public readonly sub: AtprotoDid,\n    private readonly sessionGetter: SessionGetter,\n    fetch: Fetch = globalThis.fetch,\n  ) {\n    this.dpopFetch = dpopFetchWrapper<void>({\n      fetch: bindFetch(fetch),\n      key: server.dpopKey,\n      supportedAlgs: server.serverMetadata.dpop_signing_alg_values_supported,\n      sha256: async (v) => server.runtime.sha256(v),\n      nonces: server.dpopNonces,\n      isAuthServer: false,\n    })\n  }\n\n  get did(): AtprotoDid {\n    return this.sub\n  }\n\n  get serverMetadata(): Readonly<OAuthAuthorizationServerMetadata> {\n    return this.server.serverMetadata\n  }\n\n  /**\n   * @param refresh When `true`, the credentials will be refreshed even if they\n   * are not expired. When `false`, the credentials will not be refreshed even\n   * if they are expired. When `undefined`, the credentials will be refreshed\n   * if, and only if, they are (about to be) expired. Defaults to `undefined`.\n   */\n  protected async getTokenSet(refresh: boolean | 'auto'): Promise<TokenSet> {\n    const { tokenSet } = await this.sessionGetter.getSession(this.sub, refresh)\n\n    return tokenSet\n  }\n\n  async getTokenInfo(refresh: boolean | 'auto' = 'auto'): Promise<TokenInfo> {\n    const tokenSet = await this.getTokenSet(refresh)\n    const expiresAt =\n      tokenSet.expires_at == null ? undefined : new Date(tokenSet.expires_at)\n\n    return {\n      expiresAt,\n      get expired() {\n        return expiresAt == null\n          ? undefined\n          : expiresAt.getTime() < Date.now() - 5e3\n      },\n      scope: tokenSet.scope,\n      iss: tokenSet.iss,\n      aud: tokenSet.aud,\n      sub: tokenSet.sub,\n    }\n  }\n\n  async signOut(): Promise<void> {\n    try {\n      const tokenSet = await this.getTokenSet(false)\n      await this.server.revoke(tokenSet.access_token)\n    } finally {\n      await this.sessionGetter.delStored(\n        this.sub,\n        new TokenRevokedError(this.sub),\n      )\n    }\n  }\n\n  async fetchHandler(pathname: string, init?: RequestInit): Promise<Response> {\n    // This will try and refresh the token if it is known to be expired\n    const tokenSet = await this.getTokenSet('auto')\n\n    const initialUrl = new URL(pathname, tokenSet.aud satisfies string)\n    const initialAuth = `${tokenSet.token_type} ${tokenSet.access_token}`\n\n    const headers = new Headers(init?.headers)\n    headers.set('Authorization', initialAuth)\n\n    const initialResponse = await this.dpopFetch(initialUrl, {\n      ...init,\n      headers,\n    })\n\n    // If the token is not expired, we don't need to refresh it\n    if (!isInvalidTokenResponse(initialResponse)) {\n      return initialResponse\n    }\n\n    let tokenSetFresh: TokenSet\n    try {\n      // Force a refresh\n      tokenSetFresh = await this.getTokenSet(true)\n    } catch (err) {\n      return initialResponse\n    }\n\n    // The stream was already consumed. We cannot retry the request. A solution\n    // would be to tee() the input stream but that would bufferize the entire\n    // stream in memory which can lead to memory starvation. Instead, we will\n    // return the original response and let the calling code handle retries.\n    if (ReadableStream && init?.body instanceof ReadableStream) {\n      return initialResponse\n    }\n\n    const finalAuth = `${tokenSetFresh.token_type} ${tokenSetFresh.access_token}`\n    const finalUrl = new URL(pathname, tokenSetFresh.aud)\n\n    headers.set('Authorization', finalAuth)\n\n    const finalResponse = await this.dpopFetch(finalUrl, { ...init, headers })\n\n    // The token was successfully refreshed, but is still not accepted by the\n    // resource server. This might be due to the resource server not accepting\n    // credentials from the authorization server (e.g. because some migration\n    // occurred). Any ways, there is no point in keeping the session.\n    if (isInvalidTokenResponse(finalResponse)) {\n      // @TODO Is there a \"softer\" way to handle this, e.g. by marking the\n      // session as \"expired\" in the session store, allowing the user to trigger\n      // a new login (using login_hint)?\n      await this.sessionGetter.delStored(\n        this.sub,\n        new TokenInvalidError(this.sub),\n      )\n    }\n\n    return finalResponse\n  }\n}\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no}\n */\nfunction isInvalidTokenResponse(response: Response) {\n  if (response.status !== 401) return false\n  const wwwAuth = response.headers.get('WWW-Authenticate')\n  return (\n    wwwAuth != null &&\n    (wwwAuth.startsWith('Bearer ') || wwwAuth.startsWith('DPoP ')) &&\n    wwwAuth.includes('error=\"invalid_token\"')\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/runtime-implementation.ts",
    "content": "import { Key } from '@atproto/jwk'\nimport { Awaitable } from './util.js'\n\nexport type { Key }\nexport type RuntimeKeyFactory = (algs: string[]) => Key | PromiseLike<Key>\n\nexport type RuntimeRandomValues = (length: number) => Awaitable<Uint8Array>\n\nexport type DigestAlgorithm = { name: 'sha256' | 'sha384' | 'sha512' }\nexport type RuntimeDigest = (\n  data: Uint8Array,\n  alg: DigestAlgorithm,\n) => Awaitable<Uint8Array>\n\nexport type RuntimeLock = <T>(\n  name: string,\n  fn: () => Awaitable<T>,\n) => Awaitable<T>\n\nexport interface RuntimeImplementation {\n  createKey: RuntimeKeyFactory\n  getRandomValues: RuntimeRandomValues\n  digest: RuntimeDigest\n  requestLock?: RuntimeLock\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/runtime.ts",
    "content": "import { base64url } from 'multiformats/bases/base64'\nimport { Key } from '@atproto/jwk'\nimport { requestLocalLock } from './lock.js'\nimport { RuntimeImplementation, RuntimeLock } from './runtime-implementation.js'\n\nexport class Runtime {\n  readonly hasImplementationLock: boolean\n  readonly usingLock: RuntimeLock\n\n  constructor(protected implementation: RuntimeImplementation) {\n    const { requestLock } = implementation\n\n    this.hasImplementationLock = requestLock != null\n    this.usingLock =\n      requestLock?.bind(implementation) ||\n      // Falling back to a local lock\n      requestLocalLock\n  }\n\n  public async generateKey(algs: string[]): Promise<Key> {\n    const algsSorted = Array.from(algs).sort(compareAlgos)\n    return this.implementation.createKey(algsSorted)\n  }\n\n  public async sha256(text: string): Promise<string> {\n    const bytes = new TextEncoder().encode(text)\n    const digest = await this.implementation.digest(bytes, { name: 'sha256' })\n    return base64url.baseEncode(digest)\n  }\n\n  public async generateNonce(length = 16): Promise<string> {\n    const bytes = await this.implementation.getRandomValues(length)\n    return base64url.baseEncode(bytes)\n  }\n\n  public async generatePKCE(byteLength?: number) {\n    const verifier = await this.generateVerifier(byteLength)\n    return {\n      verifier,\n      challenge: await this.sha256(verifier),\n      method: 'S256' as const,\n    }\n  }\n\n  public async calculateJwkThumbprint(jwk) {\n    const components = extractJktComponents(jwk)\n    const data = JSON.stringify(components)\n    return this.sha256(data)\n  }\n\n  /**\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1}\n   * @note It is RECOMMENDED that the output of a suitable random number generator\n   * be used to create a 32-octet sequence. The octet sequence is then\n   * base64url-encoded to produce a 43-octet URL safe string to use as the code\n   * verifier.\n   */\n  protected async generateVerifier(byteLength = 32) {\n    if (byteLength < 32 || byteLength > 96) {\n      throw new TypeError('Invalid code_verifier length')\n    }\n    const bytes = await this.implementation.getRandomValues(byteLength)\n    return base64url.baseEncode(bytes)\n  }\n}\n\nfunction extractJktComponents(jwk) {\n  const get = (field) => {\n    const value = jwk[field]\n    if (typeof value !== 'string' || !value) {\n      throw new TypeError(`\"${field}\" Parameter missing or invalid`)\n    }\n    return value\n  }\n\n  switch (jwk.kty) {\n    case 'EC':\n      return { crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y') }\n    case 'OKP':\n      return { crv: get('crv'), kty: get('kty'), x: get('x') }\n    case 'RSA':\n      return { e: get('e'), kty: get('kty'), n: get('n') }\n    case 'oct':\n      return { k: get('k'), kty: get('kty') }\n    default:\n      throw new TypeError('\"kty\" (Key Type) Parameter missing or unsupported')\n  }\n}\n\n/**\n * 256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other (in original order)\n */\nfunction compareAlgos(a: string, b: string): number {\n  if (a === 'ES256K') return -1\n  if (b === 'ES256K') return 1\n\n  for (const prefix of ['ES', 'PS', 'RS']) {\n    if (a.startsWith(prefix)) {\n      if (b.startsWith(prefix)) {\n        const aLen = parseInt(a.slice(2, 5))\n        const bLen = parseInt(b.slice(2, 5))\n\n        // Prefer shorter key lengths\n        return aLen - bLen\n      }\n      return -1\n    } else if (b.startsWith(prefix)) {\n      return 1\n    }\n  }\n\n  // Don't know how to compare, keep original order\n  return 0\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/session-getter.ts",
    "content": "import { AtprotoDid } from '@atproto/did'\nimport { Key } from '@atproto/jwk'\nimport {\n  CachedGetter,\n  GetCachedOptions,\n  GetOptions,\n  SimpleStore,\n} from '@atproto-labs/simple-store'\nimport { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'\nimport { TokenInvalidError } from './errors/token-invalid-error.js'\nimport { TokenRefreshError } from './errors/token-refresh-error.js'\nimport { TokenRevokedError } from './errors/token-revoked-error.js'\nimport { ClientAuthMethod } from './oauth-client-auth.js'\nimport { OAuthResponseError } from './oauth-response-error.js'\nimport { TokenSet } from './oauth-server-agent.js'\nimport { OAuthServerFactory } from './oauth-server-factory.js'\nimport { Runtime } from './runtime.js'\nimport { combineSignals } from './util.js'\n\nexport type Session = {\n  dpopKey: Key\n  authMethod: ClientAuthMethod\n  tokenSet: TokenSet\n}\n\nexport type SessionStore = SimpleStore<string, Session>\n\nexport type SessionHooks = {\n  onUpdate?: (sub: AtprotoDid, session: Session) => void\n  onDelete?: (\n    sub: AtprotoDid,\n    cause: TokenRefreshError | TokenRevokedError | TokenInvalidError | unknown,\n  ) => void\n}\n\nexport function isExpectedSessionError(err: unknown) {\n  return (\n    err instanceof TokenRefreshError ||\n    err instanceof TokenRevokedError ||\n    err instanceof TokenInvalidError ||\n    err instanceof AuthMethodUnsatisfiableError ||\n    // The stored session is invalid (e.g. missing properties) and cannot\n    // be used properly\n    err instanceof TypeError\n  )\n}\n\n/**\n * There are several advantages to wrapping the sessionStore in a (single)\n * CachedGetter, the main of which is that the cached getter will ensure that at\n * most one fresh call is ever being made. Another advantage, is that it\n * contains the logic for reading from the cache which, if the cache is based on\n * localStorage/indexedDB, will sync across multiple tabs (for a given sub).\n */\nexport class SessionGetter extends CachedGetter<AtprotoDid, Session> {\n  constructor(\n    sessionStore: SessionStore,\n    serverFactory: OAuthServerFactory,\n    private readonly runtime: Runtime,\n    private readonly hooks: SessionHooks = {},\n  ) {\n    super(\n      async (sub, { signal }, storedSession) => {\n        // There needs to be a previous session to be able to refresh. If\n        // storedSession is undefined, it means that the store does not contain\n        // a session for the given sub.\n        if (storedSession === undefined) {\n          // Because the session is not in the store, this.delStored() method\n          // will not be called by the CachedGetter class (because there is\n          // nothing to delete). This would typically happen if there is no\n          // synchronization mechanism between instances of this class. Let's\n          // make sure an event is dispatched here if this occurs.\n          const msg = 'The session was deleted by another process'\n          const cause = new TokenRefreshError(sub, msg)\n          await hooks.onDelete?.call(null, sub, cause)\n          throw cause\n        }\n\n        // @NOTE Throwing a TokenRefreshError (or any other error class defined\n        // in the deleteOnError options) will result in this.delStored() being\n        // called.\n\n        const { dpopKey, authMethod, tokenSet } = storedSession\n\n        if (sub !== tokenSet.sub) {\n          // Fool-proofing (e.g. against invalid session storage)\n          throw new TokenRefreshError(sub, 'Stored session sub mismatch')\n        }\n\n        if (!tokenSet.refresh_token) {\n          throw new TokenRefreshError(sub, 'No refresh token available')\n        }\n\n        const server = await serverFactory.fromIssuer(\n          tokenSet.iss,\n          authMethod,\n          dpopKey,\n        )\n\n        // Because refresh tokens can only be used once, we must not use the\n        // \"signal\" to abort the refresh, or throw any abort error beyond this\n        // point. Any thrown error beyond this point will prevent the\n        // TokenGetter from obtaining, and storing, the new token set,\n        // effectively rendering the currently saved session unusable.\n        signal?.throwIfAborted()\n\n        try {\n          const newTokenSet = await server.refresh(tokenSet)\n\n          if (sub !== newTokenSet.sub) {\n            // The server returned another sub. Was the tokenSet manipulated?\n            throw new TokenRefreshError(sub, 'Token set sub mismatch')\n          }\n\n          return {\n            dpopKey,\n            tokenSet: newTokenSet,\n            authMethod: server.authMethod,\n          }\n        } catch (cause) {\n          // Since refresh tokens can only be used once, we might run into\n          // concurrency issues if multiple instances (e.g. browser tabs) are\n          // trying to refresh the same token simultaneously. The chances of\n          // this happening when multiple instances are started simultaneously\n          // is reduced by randomizing the expiry time (see isStale() below).\n          // The best solution is to use a mutex/lock to ensure that only one\n          // instance is refreshing the token at a time (runtime.usingLock) but\n          // that is not always possible. Let's try to recover from concurrency\n          // issues, or force the session to be deleted by throwing a\n          // TokenRefreshError.\n          if (\n            cause instanceof OAuthResponseError &&\n            cause.status === 400 &&\n            cause.error === 'invalid_grant'\n          ) {\n            // In case there is no lock implementation in the runtime, we will\n            // wait for a short time to give the other concurrent instances a\n            // chance to finish their refreshing of the token. If a concurrent\n            // refresh did occur, we will pretend that this one succeeded.\n            if (!runtime.hasImplementationLock) {\n              await new Promise((r) => setTimeout(r, 1000))\n\n              const stored = await this.getStored(sub)\n              if (stored === undefined) {\n                // A concurrent refresh occurred and caused the session to be\n                // deleted (for a reason we can't know at this point).\n\n                // Using a distinct error message mainly for debugging\n                // purposes. Also, throwing a TokenRefreshError to trigger\n                // deletion through the deleteOnError callback.\n                const msg = 'The session was deleted by another process'\n                throw new TokenRefreshError(sub, msg, { cause })\n              } else if (\n                stored.tokenSet.access_token !== tokenSet.access_token ||\n                stored.tokenSet.refresh_token !== tokenSet.refresh_token\n              ) {\n                // A concurrent refresh occurred. Pretend this one succeeded.\n                return stored\n              } else {\n                // There were no concurrent refresh. The token is (likely)\n                // simply no longer valid.\n              }\n            }\n\n            // Make sure the session gets deleted from the store\n            const msg = cause.errorDescription ?? 'The session was revoked'\n            throw new TokenRefreshError(sub, msg, { cause })\n          }\n\n          throw cause\n        }\n      },\n      sessionStore,\n      {\n        isStale: (sub, { tokenSet }) => {\n          return (\n            tokenSet.expires_at != null &&\n            new Date(tokenSet.expires_at).getTime() <\n              Date.now() +\n                // Add some lee way to ensure the token is not expired when it\n                // reaches the server.\n                10e3 +\n                // Add some randomness to reduce the chances of multiple\n                // instances trying to refresh the token at the same.\n                30e3 * Math.random()\n          )\n        },\n        onStoreError: async (err, sub, { tokenSet, dpopKey, authMethod }) => {\n          // If the token data cannot be stored, let's revoke it\n          try {\n            const server = await serverFactory.fromIssuer(\n              tokenSet.iss,\n              authMethod,\n              dpopKey,\n            )\n            await server.revoke(tokenSet.refresh_token ?? tokenSet.access_token)\n          } catch {\n            // At least we tried...\n          }\n\n          // Attempt to delete the session from the store. Note that this might\n          // fail if the store is not available, which is fine.\n          try {\n            await this.delStored(sub, err)\n          } catch {\n            // Ignore (better to propagate the original storage error)\n          }\n\n          throw err\n        },\n        deleteOnError: isExpectedSessionError,\n      },\n    )\n  }\n\n  override async getStored(\n    sub: AtprotoDid,\n    options?: GetOptions,\n  ): Promise<Session | undefined> {\n    return super.getStored(sub, options)\n  }\n\n  override async setStored(sub: AtprotoDid, session: Session) {\n    // Prevent tampering with the stored value\n    if (sub !== session.tokenSet.sub) {\n      throw new TypeError('Token set does not match the expected sub')\n    }\n    await super.setStored(sub, session)\n    await this.hooks.onUpdate?.call(null, sub, session)\n  }\n\n  override async delStored(sub: AtprotoDid, cause?: unknown): Promise<void> {\n    await super.delStored(sub, cause)\n    await this.hooks.onDelete?.call(null, sub, cause)\n  }\n\n  /**\n   * @deprecated Use {@link getSession} instead\n   * @internal (not really deprecated)\n   */\n  override async get(\n    sub: AtprotoDid,\n    options?: GetCachedOptions,\n  ): Promise<Session> {\n    const session = await this.runtime.usingLock(\n      `@atproto-oauth-client-${sub}`,\n      async () => {\n        // Make sure, even if there is no signal in the options, that the\n        // request will be cancelled after at most 30 seconds.\n        const signal = AbortSignal.timeout(30e3)\n\n        using abortController = combineSignals([options?.signal, signal])\n\n        return await super.get(sub, {\n          ...options,\n          signal: abortController.signal,\n        })\n      },\n    )\n\n    if (sub !== session.tokenSet.sub) {\n      // Fool-proofing (e.g. against invalid session storage)\n      throw new Error('Token set does not match the expected sub')\n    }\n\n    return session\n  }\n\n  /**\n   * @param refresh When `true`, the credentials will be refreshed even if they\n   * are not expired. When `false`, the credentials will not be refreshed even\n   * if they are expired. When `undefined`, the credentials will be refreshed\n   * if, and only if, they are (about to be) expired. Defaults to `undefined`.\n   */\n  async getSession(sub: AtprotoDid, refresh: boolean | 'auto' = 'auto') {\n    return this.get(sub, {\n      noCache: refresh === true,\n      allowStale: refresh === false,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/state-store.ts",
    "content": "import { Key } from '@atproto/jwk'\nimport { SimpleStore } from '@atproto-labs/simple-store'\nimport { ClientAuthMethod } from './oauth-client-auth.js'\n\nexport type InternalStateData = {\n  iss: string\n  dpopKey: Key\n  authMethod: ClientAuthMethod\n  verifier: string\n  appState?: string\n}\n\n/**\n * A store pending oauth authorization flows. The key is the \"state\" parameter\n * used in the authorization request, and the value is an object containing the\n * necessary information to complete the flow once the user is redirected back\n * to the client.\n *\n * @note The data stored in this store is typically short-lived. It should be\n * automatically cleared after a certain period of time (e.g. 1 hour) to prevent\n * the store from growing indefinitely. It is up to the implementation to\n * implement this cleanup mechanism.\n */\nexport type StateStore = SimpleStore<string, InternalStateData>\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/types.ts",
    "content": "import { TypeOf, z } from 'zod'\nimport {\n  OAuthAuthorizationRequestParameters,\n  oauthClientIdDiscoverableSchema,\n  oauthClientIdLoopbackSchema,\n  oauthClientMetadataSchema,\n} from '@atproto/oauth-types'\nimport { Simplify } from './util.js'\n\n// Note: These types are not prefixed with `OAuth` because they are not specific\n// to OAuth. They are specific to this packages. OAuth specific types are in\n// `@atproto/oauth-types`.\n\nexport type AuthorizeOptions = Simplify<\n  Omit<\n    OAuthAuthorizationRequestParameters,\n    | 'client_id'\n    | 'response_mode'\n    | 'response_type'\n    | 'login_hint'\n    | 'code_challenge'\n    | 'code_challenge_method'\n  > & {\n    signal?: AbortSignal\n  }\n>\n\nexport type CallbackOptions = Simplify<\n  Partial<Pick<OAuthAuthorizationRequestParameters, 'redirect_uri'>>\n>\n\nexport const clientMetadataSchema = oauthClientMetadataSchema.extend({\n  client_id: z.union([\n    oauthClientIdDiscoverableSchema,\n    oauthClientIdLoopbackSchema,\n  ]),\n})\n\nexport type ClientMetadata = TypeOf<typeof clientMetadataSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/util.ts",
    "content": "export type Awaitable<T> = T | PromiseLike<T>\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n\nexport const ifString = <V>(v: V) => (typeof v === 'string' ? v : undefined)\n\nexport function contentMime(headers: Headers): string | undefined {\n  return headers.get('content-type')?.split(';')[0]!.trim()\n}\n\nexport function combineSignals(\n  signals: readonly (AbortSignal | undefined)[],\n): AbortController & Disposable {\n  const controller = new DisposableAbortController()\n\n  const onAbort = function (this: AbortSignal, _event: Event) {\n    const reason = new Error('This operation was aborted', {\n      cause: this.reason,\n    })\n\n    controller.abort(reason)\n  }\n\n  try {\n    for (const sig of signals) {\n      if (sig) {\n        sig.throwIfAborted()\n        sig.addEventListener('abort', onAbort, { signal: controller.signal })\n      }\n    }\n\n    return controller\n  } catch (err) {\n    controller.abort(err)\n    throw err\n  }\n}\n\n/**\n * Allows using {@link AbortController} with the `using` keyword, in order to\n * automatically abort them once the execution block ends.\n */\nclass DisposableAbortController extends AbortController implements Disposable {\n  [Symbol.dispose]() {\n    this.abort(new Error('AbortController was disposed'))\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/src/validate-client-metadata.ts",
    "content": "import { Keyset } from '@atproto/jwk'\nimport {\n  OAuthClientMetadataInput,\n  assertOAuthDiscoverableClientId,\n  assertOAuthLoopbackClientId,\n} from '@atproto/oauth-types'\nimport { FALLBACK_ALG } from './constants.js'\nimport { ClientMetadata, clientMetadataSchema } from './types.js'\n\nexport function validateClientMetadata(\n  input: OAuthClientMetadataInput,\n  keyset?: Keyset,\n): ClientMetadata {\n  // Allow to pass a keyset and omit the jwks/jwks_uri properties\n  if (!input.jwks && !input.jwks_uri && keyset?.size) {\n    input = { ...input, jwks: keyset.toJSON() }\n  }\n\n  const metadata = clientMetadataSchema.parse(input)\n\n  // Validate client ID\n  if (metadata.client_id.startsWith('http:')) {\n    assertOAuthLoopbackClientId(metadata.client_id)\n  } else {\n    assertOAuthDiscoverableClientId(metadata.client_id)\n  }\n\n  const scopes = metadata.scope?.split(' ')\n  if (!scopes?.includes('atproto')) {\n    throw new TypeError(`Client metadata must include the \"atproto\" scope`)\n  }\n\n  if (!metadata.response_types.includes('code')) {\n    throw new TypeError(`\"response_types\" must include \"code\"`)\n  }\n\n  if (!metadata.grant_types.includes('authorization_code')) {\n    throw new TypeError(`\"grant_types\" must include \"authorization_code\"`)\n  }\n\n  const method = metadata.token_endpoint_auth_method\n  const methodAlg = metadata.token_endpoint_auth_signing_alg\n  switch (method) {\n    case 'none':\n      if (methodAlg) {\n        throw new TypeError(\n          `\"token_endpoint_auth_signing_alg\" must not be provided when \"token_endpoint_auth_method\" is \"${method}\"`,\n        )\n      }\n      break\n\n    case 'private_key_jwt': {\n      if (!methodAlg) {\n        throw new TypeError(\n          `\"token_endpoint_auth_signing_alg\" must be provided when \"token_endpoint_auth_method\" is \"${method}\"`,\n        )\n      }\n\n      if (!keyset) {\n        throw new TypeError(\n          `Client authentication method \"${method}\" requires a keyset`,\n        )\n      }\n\n      // @NOTE This reproduces the logic from `negotiateClientAuthMethod` at\n      // initialization time to ensure that every key that might end-up being\n      // used is indeed valid & advertised in the metadata.\n      const signingKeys = Array.from(keyset.list({ usage: 'sign' })).filter(\n        (key) => key.kid,\n      )\n\n      if (!signingKeys.length) {\n        throw new TypeError(\n          `Client authentication method \"${method}\" requires at least one active signing key with a \"kid\" property`,\n        )\n      }\n\n      if (!signingKeys.some((key) => key.algorithms.includes(FALLBACK_ALG))) {\n        throw new TypeError(\n          `Client authentication method \"${method}\" requires at least one active \"${FALLBACK_ALG}\" signing key`,\n        )\n      }\n\n      if (metadata.jwks) {\n        // Ensure that all the signing keys that could end-up being used are\n        // advertised in the JWKS.\n        for (const key of signingKeys) {\n          if (\n            !metadata.jwks.keys.some((k) => k.kid === key.kid && !k.revoked)\n          ) {\n            throw new TypeError(\n              `Missing or inactive key \"${key.kid}\" in jwks. Make sure that every signing key of the Keyset is declared as an active key in the Metadata's JWKS.`,\n            )\n          }\n        }\n      } else if (metadata.jwks_uri) {\n        // @NOTE we only ensure that all the signing keys are referenced in JWKS\n        // when it is available (see previous \"if\") as we don't want to download\n        // that file here (for efficiency reasons).\n      } else {\n        throw new TypeError(\n          `Client authentication method \"${method}\" requires a JWKS`,\n        )\n      }\n\n      break\n    }\n\n    default:\n      throw new TypeError(\n        `Unsupported \"token_endpoint_auth_method\" value: ${method}`,\n      )\n  }\n\n  return metadata\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/CHANGELOG.md",
    "content": "# @atproto/oauth-client-browser\n\n## 0.3.41\n\n### Patch Changes\n\n- [#4642](https://github.com/bluesky-social/atproto/pull/4642) [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly await disposal of underlying indexed db when disposing a `BrowserOAuthClient`\n\n- Updated dependencies [[`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df)]:\n  - @atproto/oauth-client@0.6.0\n\n## 0.3.40\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n  - @atproto-labs/did-resolver@0.2.6\n  - @atproto-labs/handle-resolver@0.3.6\n  - @atproto/oauth-client@0.5.14\n  - @atproto/oauth-types@0.6.2\n\n## 0.3.39\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n  - @atproto/oauth-client@0.5.13\n  - @atproto-labs/did-resolver@0.2.5\n  - @atproto-labs/handle-resolver@0.3.5\n  - @atproto/oauth-types@0.6.1\n\n## 0.3.38\n\n### Patch Changes\n\n- Updated dependencies [[`95ef3c2`](https://github.com/bluesky-social/atproto/commit/95ef3c24e8072e9d49412950b033cb8607764ee0), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41)]:\n  - @atproto/oauth-types@0.6.0\n  - @atproto/oauth-client@0.5.12\n\n## 0.3.37\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.11\n\n## 0.3.36\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n  - @atproto-labs/did-resolver@0.2.4\n  - @atproto-labs/handle-resolver@0.3.4\n  - @atproto/oauth-client@0.5.10\n  - @atproto/oauth-types@0.5.2\n\n## 0.3.35\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto-labs/handle-resolver@0.3.3\n  - @atproto-labs/did-resolver@0.2.3\n  - @atproto/did@0.2.2\n  - @atproto/oauth-client@0.5.9\n  - @atproto/oauth-types@0.5.1\n\n## 0.3.34\n\n### Patch Changes\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto/oauth-types@0.5.0\n  - @atproto/oauth-client@0.5.8\n\n## 0.3.33\n\n### Patch Changes\n\n- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typings of `init` and `signIn` methods\n\n- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `core-js` to polyfill `Symbol.dispose`\n\n- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid module loading error in SSR environments (in which `navigator` is not present)\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/oauth-types@0.4.2\n  - @atproto/oauth-client@0.5.7\n  - @atproto/jwk@0.6.0\n  - @atproto/jwk-webcrypto@0.2.0\n  - @atproto/did@0.2.1\n  - @atproto-labs/did-resolver@0.2.2\n  - @atproto-labs/handle-resolver@0.3.2\n\n## 0.3.32\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.6\n\n## 0.3.31\n\n### Patch Changes\n\n- [#4150](https://github.com/bluesky-social/atproto/pull/4150) [`86c4699da`](https://github.com/bluesky-social/atproto/commit/86c4699da8cf184c251e58c0a3a2612dd676f0ea) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow forcing OAuth callback instead of automated init.\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`86c4699da`](https://github.com/bluesky-social/atproto/commit/86c4699da8cf184c251e58c0a3a2612dd676f0ea), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/did@0.2.0\n  - @atproto-labs/simple-store@0.3.0\n  - @atproto/oauth-client@0.5.5\n  - @atproto-labs/did-resolver@0.2.1\n  - @atproto-labs/handle-resolver@0.3.1\n\n## 0.3.30\n\n### Patch Changes\n\n- Updated dependencies [[`6231c8730`](https://github.com/bluesky-social/atproto/commit/6231c8730adb3a4c17dec417e5332b2be61070e5)]:\n  - @atproto/oauth-client@0.5.4\n\n## 0.3.29\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.3\n\n## 0.3.28\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n  - @atproto/jwk-webcrypto@0.1.10\n  - @atproto/oauth-client@0.5.2\n  - @atproto/oauth-types@0.4.1\n\n## 0.3.27\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.1\n\n## 0.3.26\n\n### Patch Changes\n\n- Updated dependencies [[`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a)]:\n  - @atproto/oauth-client@0.5.0\n  - @atproto/oauth-types@0.4.0\n\n## 0.3.25\n\n### Patch Changes\n\n- Updated dependencies [[`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47), [`9dac8b0c6`](https://github.com/bluesky-social/atproto/commit/9dac8b0c600520ecb0066ac104787b27668dea47)]:\n  - @atproto-labs/handle-resolver@0.3.0\n  - @atproto/oauth-client@0.4.2\n\n## 0.3.24\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto-labs/handle-resolver@0.2.0\n  - @atproto/oauth-client@0.4.1\n  - @atproto-labs/did-resolver@0.2.0\n  - @atproto/jwk@0.4.0\n  - @atproto/jwk-webcrypto@0.1.9\n  - @atproto/oauth-types@0.3.1\n\n## 0.3.23\n\n### Patch Changes\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/oauth-types@0.3.0\n  - @atproto/oauth-client@0.4.0\n  - @atproto/jwk@0.3.0\n  - @atproto/jwk-webcrypto@0.1.8\n\n## 0.3.22\n\n### Patch Changes\n\n- Updated dependencies [[`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee), [`4e96e2c7b`](https://github.com/bluesky-social/atproto/commit/4e96e2c7b7cc0231607d3065c95704069c4ca2a2)]:\n  - @atproto/oauth-client@0.3.22\n\n## 0.3.21\n\n### Patch Changes\n\n- Updated dependencies [[`cd4bed3c9`](https://github.com/bluesky-social/atproto/commit/cd4bed3c9e68878c3f79620fe19f6994ebcb932e)]:\n  - @atproto/oauth-client@0.3.21\n\n## 0.3.20\n\n### Patch Changes\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c)]:\n  - @atproto/jwk@0.2.0\n  - @atproto/oauth-types@0.2.8\n  - @atproto/oauth-client@0.3.20\n  - @atproto/jwk-webcrypto@0.1.7\n\n## 0.3.19\n\n### Patch Changes\n\n- Updated dependencies [[`a03f0b906`](https://github.com/bluesky-social/atproto/commit/a03f0b906b108f8c766a5700f0d68b55748f23bd)]:\n  - @atproto/oauth-client@0.3.19\n\n## 0.3.18\n\n### Patch Changes\n\n- Updated dependencies [[`36d0d370c`](https://github.com/bluesky-social/atproto/commit/36d0d370c24498f74c243ebfb01564e5050c672d)]:\n  - @atproto/oauth-client@0.3.18\n\n## 0.3.17\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.13\n  - @atproto/oauth-client@0.3.17\n\n## 0.3.16\n\n### Patch Changes\n\n- Updated dependencies [[`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8)]:\n  - @atproto/oauth-types@0.2.7\n  - @atproto/oauth-client@0.3.16\n\n## 0.3.15\n\n### Patch Changes\n\n- Updated dependencies [[`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4)]:\n  - @atproto-labs/simple-store@0.2.0\n  - @atproto/oauth-types@0.2.6\n  - @atproto-labs/did-resolver@0.1.12\n  - @atproto-labs/handle-resolver@0.1.8\n  - @atproto/oauth-client@0.3.15\n\n## 0.3.14\n\n### Patch Changes\n\n- Updated dependencies [[`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/oauth-types@0.2.5\n  - @atproto/jwk@0.1.5\n  - @atproto/oauth-client@0.3.14\n  - @atproto/jwk-webcrypto@0.1.6\n\n## 0.3.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.13\n\n## 0.3.12\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.12\n\n## 0.3.11\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/oauth-types@0.2.4\n  - @atproto/jwk@0.1.4\n  - @atproto/oauth-client@0.3.11\n  - @atproto-labs/did-resolver@0.1.11\n  - @atproto/jwk-webcrypto@0.1.5\n\n## 0.3.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.10\n\n## 0.3.9\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/handle-resolver@0.1.7\n  - @atproto-labs/did-resolver@0.1.10\n  - @atproto-labs/simple-store@0.1.2\n  - @atproto/jwk-webcrypto@0.1.4\n  - @atproto/oauth-client@0.3.9\n  - @atproto/oauth-types@0.2.3\n  - @atproto/jwk@0.1.3\n  - @atproto/did@0.1.5\n\n## 0.3.8\n\n### Patch Changes\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87), [`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87)]:\n  - @atproto-labs/did-resolver@0.1.9\n  - @atproto/did@0.1.4\n  - @atproto/oauth-client@0.3.8\n  - @atproto-labs/handle-resolver@0.1.6\n\n## 0.3.7\n\n### Patch Changes\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0)]:\n  - @atproto/jwk@0.1.2\n  - @atproto/jwk-webcrypto@0.1.3\n  - @atproto/oauth-client@0.3.7\n  - @atproto/oauth-types@0.2.2\n  - @atproto-labs/did-resolver@0.1.8\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto-labs/did-resolver@0.1.7\n  - @atproto/oauth-client@0.3.6\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`a200e5095`](https://github.com/bluesky-social/atproto/commit/a200e50951d297c3f9670e96027262196bc29b0b)]:\n  - @atproto-labs/handle-resolver@0.1.5\n  - @atproto/oauth-client@0.3.5\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.4\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.6\n  - @atproto/oauth-client@0.3.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3)]:\n  - @atproto/oauth-types@0.2.1\n  - @atproto/oauth-client@0.3.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.1\n\n## 0.3.0\n\n### Minor Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `\"auto\"` instead of `undefined` to descibe the refresh mechanism to use in various methods.\n\n### Patch Changes\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `allowHttp` OAuthClient construction option to allow working with \"http:\" oauth providers (for development & testing purposes).\n\n- Updated dependencies [[`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf)]:\n  - @atproto/oauth-client@0.3.0\n  - @atproto/oauth-types@0.2.0\n  - @atproto-labs/did-resolver@0.1.5\n  - @atproto-labs/handle-resolver@0.1.4\n  - @atproto/did@0.1.3\n\n## 0.2.2\n\n### Patch Changes\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve invalid client_id error messages from BrowserOAuthClient.from()\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not strip query string from URL after oauth redirect in fragment mode\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Relax type restriction on clientId option\n\n- Updated dependencies [[`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8)]:\n  - @atproto/oauth-types@0.1.5\n  - @atproto/oauth-client@0.2.2\n  - @atproto-labs/did-resolver@0.1.4\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]:\n  - @atproto/did@0.1.2\n  - @atproto-labs/did-resolver@0.1.3\n  - @atproto-labs/handle-resolver@0.1.3\n  - @atproto/oauth-client@0.2.1\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The `OAuthClient` (and runtime specific sub-classes) no longer return @atproto/api `Agent` instances. Instead, they return `OAuthSession` instances that can be used to instantiate the `Agent` class.\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"openid\" compatibility. The reason is that although we were technically \"openid\" compatible, ATProto identifiers are distributed identifiers. When a client relies on OpenID to authenticate users, it will use the auth provider in combination with the identifier to uniquely identify the user. Since ATProto identifiers are meant to be able to move from one provider to the other, OpenID compatibility could break authentication after a user was migrated to a different provider.\n\n  The way OpenID compliant clients would adapt to this particularity would typically be to remove the provider + identifier combination and use the identifier alone. While this is indeed the right way to handle ATProto identifiers, it requires more work to avoid impersonation. In particular, when obtaining a user identifier, the client **must** verify that the issuer of the identity token is indeed the server responsible for that user. This mechanism being not enforced by the OpenID standard, OpenID compatibility could lead to security issues. For this reason, we decided to remove OpenID compatibility from the OAuth provider.\n\n  Note that a trusted central authority could still offer OpenID compatibility by relying on ATProto's regular OAuth flow under the hood. This capability is out of the scope of this library.\n\n### Patch Changes\n\n- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/oauth-client@0.2.0\n  - @atproto/oauth-types@0.1.4\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.1.7\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`3ebcd4e61`](https://github.com/bluesky-social/atproto/commit/3ebcd4e6161291d3649d7f8a9c5ee4ac26d590a2), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb)]:\n  - @atproto/oauth-client@0.1.5\n  - @atproto/oauth-types@0.1.3\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`04112783d`](https://github.com/bluesky-social/atproto/commit/04112783db17f865c9e2b673190f77dd0b7461e3)]:\n  - @atproto/oauth-client@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/oauth-client@0.1.2\n  - @atproto/oauth-types@0.1.2\n  - @atproto-labs/handle-resolver@0.1.2\n  - @atproto/did@0.1.1\n  - @atproto/jwk-webcrypto@0.1.2\n  - @atproto-labs/did-resolver@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add event emitting capability to OAuthClient\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/oauth-types@0.1.1\n  - @atproto/jwk@0.1.1\n  - @atproto-labs/handle-resolver@0.1.1\n  - @atproto-labs/did-resolver@0.1.1\n  - @atproto-labs/simple-store@0.1.1\n  - @atproto/oauth-client@0.1.1\n  - @atproto/jwk-webcrypto@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/handle-resolver@0.1.0\n  - @atproto-labs/did-resolver@0.1.0\n  - @atproto-labs/simple-store@0.1.0\n  - @atproto/jwk-webcrypto@0.1.0\n  - @atproto/oauth-client@0.1.0\n  - @atproto/oauth-types@0.1.0\n  - @atproto/jwk@0.1.0\n  - @atproto/did@0.1.0\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/README.md",
    "content": "# atproto OAuth Client for the Browser\n\nThis package provides a browser specific OAuth client implementation for\natproto. It implements all the OAuth features required by [ATPROTO] (PKCE, DPoP,\netc.).\n\n`@atproto/oauth-client-browser` is designed for front-end applications that do\nnot have a backend server to manage OAuth sessions, a.k.a \"Single Page\nApplications\" (SPA).\n\n> [!IMPORTANT]\n>\n> When a backend server is available, it is recommended to use\n> [`@atproto/oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node)\n> to manage OAuth sessions from the server side and use a session cookie to map\n> the OAuth session to the front-end. Because this mechanism allows the backend\n> to invalidate OAuth credentials at scale, this method is more secure than\n> managing OAuth sessions from the front-end directly. Thanks to the added\n> security, the OAuth server will provide longer lived tokens when issued to a\n> BFF (Backend-for-frontend).\n\n## Setup\n\n### Client ID\n\nThe `client_id` is what identifies your application to the OAuth server. It is\nused to fetch the client metadata and to initiate the OAuth flow. The\n`client_id` must be a URL that points to the [client\nmetadata](#client-metadata).\n\n### Client Metadata\n\nYour OAuth client metadata should be hosted at a URL that corresponds to the\n`client_id` of your application. This URL should return a JSON object with the\nclient metadata. The client metadata should be configured according to the\nneeds of your application and must respect the [ATPROTO] spec.\n\n```json\n{\n  // Must be the same URL as the one used to obtain this JSON object\n  \"client_id\": \"https://my-app.com/client-metadata.json\",\n  \"client_name\": \"My App\",\n  \"client_uri\": \"https://my-app.com\",\n  \"logo_uri\": \"https://my-app.com/logo.png\",\n  \"tos_uri\": \"https://my-app.com/tos\",\n  \"policy_uri\": \"https://my-app.com/policy\",\n  \"redirect_uris\": [\"https://my-app.com/callback\"],\n  \"scope\": \"atproto\",\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"response_types\": [\"code\"],\n  \"token_endpoint_auth_method\": \"none\",\n  \"application_type\": \"web\",\n  \"dpop_bound_access_tokens\": true\n}\n```\n\nThe client metadata is used to instantiate an OAuth client. There are two ways\nof doing this:\n\n1. Either you \"burn\" the metadata into your application:\n\n   ```typescript\n   import { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\n   const client = new BrowserOAuthClient({\n     clientMetadata: {\n       // Exact same JSON object as the one returned by the client_id URL\n     },\n     // ...\n   })\n   ```\n\n2. Or you load it asynchronously from the URL:\n\n   ```typescript\n   import { OAuthClient } from '@atproto/oauth-client-browser'\n\n   const client = await BrowserOAuthClient.load({\n     clientId: 'https://my-app.com/client-metadata.json',\n     // ...\n   })\n   ```\n\nIf performances are important to you, it is recommended to burn the metadata\ninto the script. Server side rendering techniques can also be used to inject the\nmetadata into the script at runtime.\n\n### Handle Resolver\n\nWhenever your application initiates an OAuth flow, it will start to resolve\nthe (user provider) APTROTO handle of the user. This is typically done though a\nDNS request. However, because DNS resolution is not available in the browser, a\nbackend service must be provided.\n\n> [!CAUTION]\n>\n> Using Bluesky-hosted services for handle resolution (eg, the `bsky.social`\n> endpoint) will leak both user IP addresses and handle identifiers to Bluesky,\n> a third party. While Bluesky has a declared privacy policy, both developers\n> and users of applications need to be informed and aware of the privacy\n> implications of this arrangement. Application developers are encouraged to\n> improve user privacy by operating their own handle resolution service when\n> possible. If you are a PDS self-hoster, you can use your PDS's URL for\n> `handleResolver`.\n\nIf a `string` or `URL` object is used as `handleResolver`, the library will\nexpect this value to be the URL of a service running the\n`com.atproto.identity.resolveHandle` XRPC Lexicon method.\n\n> [!TIP]\n>\n> If you host your own PDS, you can use its URL as a handle resolver.\n\n```typescript\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: 'https://my-pds.example.com',\n  // ...\n})\n```\n\nAlternatively, if a \"DNS over HTTPS\" (DoH) service is available, it can be used\nto resolve the handle. In this case, the `handleResolver` should be initialized\nwith a `AtprotoDohHandleResolver` instance:\n\n```typescript\nimport {\n  BrowserOAuthClient,\n  AtprotoDohHandleResolver,\n} from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: new AtprotoDohHandleResolver('https://my-doh.example.com'),\n  // ...\n})\n```\n\n### Other configuration options\n\nIn addition to [Client Metadata](#client-metadata) and [Handle\nResolver](#handle-resolver), the `BrowserOAuthClient` constructor accepts the\nfollowing optional configuration options:\n\n- `fetch`: A custom wrapper around the `fetch` function. This can be useful to\n  add custom headers, logging, or to use a different fetch implementation.\n  Defaults to `window.fetch`.\n\n- `responseMode`: `query` or `fragment`. Determines how the authorization\n  response is returned to the client. Defaults to `fragment`.\n\n- `plcDirectoryUrl`: The URL of the PLC directory. This will typically not be\n  needed unless you run an entire atproto stack locally. Defaults to\n  `https://plc.directory`.\n\n## Usage\n\nOnce the `client` is set up, it can be used to initiate & manage OAuth sessions.\n\n### Initializing the client\n\nThe client will manage the sessions for you. In order to do so, it must first\ninitialize itself. Note that this operation must be performed once (and **only\nonce**) whenever the web app is loaded.\n\n```typescript\nconst result: undefined | { session: OAuthSession; state?: string } =\n  await client.init()\n\nif (result) {\n  const { session, state } = result\n  if (state != null) {\n    console.log(\n      `${session.sub} was successfully authenticated (state: ${state})`,\n    )\n  } else {\n    console.log(`${session.sub} was restored (last active session)`)\n  }\n}\n```\n\nThe return value can be used to determine if the client was able to restore the\nlast used session (`session` is defined) or if the current navigation is the\nresult of an authorization redirect (both `session` and `state` are defined).\n\n### Initiating an OAuth flow\n\nIn order to initiate an OAuth flow, we must first determine which PDS the\nauthentication flow will be initiated from. This means that the user must\nprovide one of the following information:\n\n- The user's handle\n- The user's DID\n- A PDS/Entryway URL\n\nUsing that information, the OAuthClient will resolve all the needed information\nto initiate the OAuth flow, and redirect the user to the OAuth server.\n\n```typescript\ntry {\n  await client.signIn('my.handle.com', {\n    state: 'some value needed later',\n    prompt: 'none', // Attempt to sign in without user interaction (SSO)\n    ui_locales: 'fr-CA fr en', // Only supported by some OAuth servers (requires OpenID Connect support + i18n support)\n    signal: new AbortController().signal, // Optional, allows to cancel the sign in (and destroy the pending authorization, for better security)\n  })\n\n  console.log('Never executed')\n} catch (err) {\n  console.log('The user aborted the authorization process by navigating \"back\"')\n}\n```\n\nThe returned promise will never resolve (because the user will be redirected to\nthe OAuth server). The promise will reject if the user cancels the sign in\n(using an `AbortSignal`), or if the user navigates back from the OAuth server\n(because of browser's back-forward cache).\n\n### Handling the OAuth response\n\nWhen the user is redirected back to the application, the OAuth response will be\navailable in the URL. The `BrowserOAuthClient` will automatically detect the\nresponse and handle it when `client.init()` is called. Alternatively, the\napplication can manually handle the response using the\n`client.callback(urlQueryParams)` method.\n\n### Restoring a session\n\nThe client keeps track of all the sessions that it manages through an internal\nstore. Regardless of the session that was returned from the `client.init()`\ncall, any other session can be loaded using the `client.restore()` method. This\nmethod will throw an error if the session is no longer available or if it has\nbecome expired.\n\n```ts\nconst aliceSession = await client.restore('did:plc:alice')\nconst bobSession = await client.restore('did:plc:bob')\n```\n\nIn its current form, the client does not expose methods to list all sessions\nin its store. The app will have to keep track of those itself.\n\n### Watching for session invalidation\n\nThe client will emit events whenever a session becomes unavailable, allowing to\ntrigger global behaviors (e.g. show the login page).\n\n```ts\nclient.addEventListener(\n  'deleted',\n  (\n    event: CustomEvent<{\n      sub: string\n      cause: TokenRefreshError | TokenRevokedError | TokenInvalidError\n    }>,\n  ) => {\n    const { sub, cause } = event.detail\n    console.error(`Session for ${sub} is no longer available (cause: ${cause})`)\n  },\n)\n```\n\n## Usage with `@atproto/api`\n\nThe `@atproto/api` package provides a way to interact with multiple Bluesky\nspecific XRPC lexicons (`com.atproto`, `app.bsky`, `chat.bsky`, `tools.ozone`)\nthrough the `Agent` interface. The `oauthSession` returned by the\n`BrowserOAuthClient` can be used to instantiate an `Agent` instance.\n\n```typescript\nimport { Agent } from '@atproto/api'\n\nconst session = await client.restore('did:plc:alice')\n\nconst agent = new Agent(session)\n\nawait agent.getProfile({ actor: agent.accountDid })\n```\n\nAny refresh of the credentials will happen under the hood, and the new tokens\nwill be saved in the session store (in the browser's indexed DB).\n\n## Advances use-cases\n\n### Using in development (localhost)\n\nThe OAuth server must be able to fetch the `client_metadata` object. The best\nway to do this if you didn't already deployed your app is to use a tunneling\nservice like [ngrok](https://ngrok.com/).\n\nThe `client_id` will then be something like\n`https://<your-ngrok-id>.ngrok.io/<path_to_your_client_metadata>`.\n\nThere is however a special case for loopback clients. A loopback client is a\nclient that runs on `localhost`. In this case, the OAuth server will not be able\nto fetch the `client_metadata` object because `localhost` is not accessible from\nthe outside. To work around this, atproto OAuth servers are required to support\nthis case by providing an hard coded `client_metadata` object for the client.\n\nThis has several restrictions:\n\n1. There is no way of configuring the client metadata (name, logo, etc.)\n2. The validity of the refresh tokens (if any) will be very limited (typically 1\n   day)\n3. Silent-sign-in will not be allowed\n4. Only `http://127.0.0.1:<any_port>` and `http://[::1]:<any_port>` can be used\n   as origin for your app, and **not** `http://localhost:<any_port>`. This\n   library will automatically redirect the user to an IP based origin\n   (`http://127.0.0.1:<port>`) when visiting an origin with `localhost`.\n\nUsing a loopback client is only recommended for development purposes. A loopback\nclient can be instantiated like this:\n\n```typescript\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: 'https://bsky.social',\n  // Only works if the current origin is a loopback address:\n  clientMetadata: undefined,\n})\n```\n\nIf you need to use a special `redirect_uris`, you can configure them like this:\n\n```typescript\nimport { BrowserOAuthClient } from '@atproto/oauth-client-browser'\n\nconst client = new BrowserOAuthClient({\n  handleResolver: 'https://bsky.social',\n  // Note that the origin of the \"client_id\" URL must be \"http://localhost\" when\n  // using this configuration, regardless of the actual hostname (\"127.0.0.1\" or\n  // \"[::1]\"), port or pathname. Only the `redirect_uris` must contain the\n  // actual url that will be used to redirect the user back to the application.\n  clientMetadata: `http://localhost?redirect_uri=${encodeURIComponent('http://127.0.0.1:8080/callback')}`,\n})\n```\n\n[ATPROTO]: https://atproto.com/ 'AT Protocol'\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-client-browser\",\n  \"version\": \"0.3.41\",\n  \"license\": \"MIT\",\n  \"description\": \"ATPROTO OAuth client for the browser (relies on WebCrypto & Indexed DB)\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"client\",\n    \"browser\",\n    \"webcrypto\",\n    \"indexed\",\n    \"db\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-client-browser\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"dependencies\": {\n    \"@atproto-labs/did-resolver\": \"workspace:^\",\n    \"@atproto-labs/handle-resolver\": \"workspace:^\",\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/jwk\": \"workspace:^\",\n    \"@atproto/jwk-webcrypto\": \"workspace:^\",\n    \"@atproto/oauth-client\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\",\n    \"core-js\": \"^3\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/browser-oauth-client.ts",
    "content": "import {\n  AuthorizeOptions,\n  ClientMetadata,\n  Fetch,\n  OAuthCallbackError,\n  OAuthClient,\n  OAuthClientOptions,\n  OAuthSession,\n  SessionHooks,\n} from '@atproto/oauth-client'\nimport {\n  OAuthClientMetadataInput,\n  OAuthResponseMode,\n  assertOAuthDiscoverableClientId,\n  atprotoLoopbackClientMetadata,\n  isOAuthClientIdLoopback,\n} from '@atproto/oauth-types'\nimport { BrowserOAuthDatabase } from './browser-oauth-database.js'\nimport { BrowserRuntimeImplementation } from './browser-runtime-implementation.js'\nimport { LoginContinuedInParentWindowError } from './errors.js'\nimport { Simplify, buildLoopbackClientId } from './util.js'\n\nexport type BrowserOAuthClientOptions = Simplify<\n  {\n    clientMetadata?: Readonly<OAuthClientMetadataInput>\n    responseMode?: Exclude<OAuthResponseMode, 'form_post'>\n    fetch?: Fetch\n  } & Omit<\n    OAuthClientOptions,\n    // Overridden by this lib\n    | 'clientMetadata'\n    | 'responseMode'\n    | 'keyset'\n    | 'fetch'\n    // Provided by this lib\n    | 'runtimeImplementation'\n    | 'sessionStore'\n    | 'stateStore'\n    | 'didCache'\n    | 'handleCache'\n    | 'dpopNonceCache'\n    | 'authorizationServerMetadataCache'\n    | 'protectedResourceMetadataCache'\n  >\n>\n\nconst NAMESPACE = `@@atproto/oauth-client-browser`\n\n//- Popup channel\n\nconst POPUP_CHANNEL_NAME = `${NAMESPACE}(popup-channel)`\nconst POPUP_STATE_PREFIX = `${NAMESPACE}(popup-state):`\n\ntype PopupChannelResultData = {\n  key: string\n  result: PromiseRejectedResult | PromiseFulfilledResult<string>\n}\n\ntype PopupChannelAckData = {\n  key: string\n  ack: true\n}\n\ntype PopupChannelData = PopupChannelResultData | PopupChannelAckData\n\n//- State synchronization channel\n\ntype SyncChannelMessage = {\n  [K in keyof SessionHooks & string]: {\n    name: K\n    args: Parameters<NonNullable<SessionHooks[K]>>\n  }\n}[keyof SessionHooks]\n\nconst syncChannel = new BroadcastChannel(\n  `${NAMESPACE}(synchronization-channel:2)`,\n)\n\nexport type BrowserOAuthClientLoadOptions = Simplify<\n  {\n    clientId: string\n    signal?: AbortSignal\n  } & Omit<BrowserOAuthClientOptions, 'clientMetadata'>\n>\n\nconst runtimeImplementation = new BrowserRuntimeImplementation()\n\nexport class BrowserOAuthClient extends OAuthClient implements AsyncDisposable {\n  static async load({ clientId, ...options }: BrowserOAuthClientLoadOptions) {\n    if (clientId.startsWith('http:')) {\n      const clientMetadata = atprotoLoopbackClientMetadata(clientId)\n      return new BrowserOAuthClient({ clientMetadata, ...options })\n    } else if (clientId.startsWith('https:')) {\n      assertOAuthDiscoverableClientId(clientId)\n      const clientMetadata = await OAuthClient.fetchMetadata({\n        clientId,\n        ...options,\n      })\n      return new BrowserOAuthClient({ ...options, clientMetadata })\n    } else {\n      throw new TypeError(`Invalid client id: ${clientId}`)\n    }\n  }\n\n  private readonly ac = new AbortController()\n\n  private readonly database: BrowserOAuthDatabase\n\n  constructor({\n    clientMetadata = atprotoLoopbackClientMetadata(\n      buildLoopbackClientId(window.location),\n    ),\n    // \"fragment\" is a safer default as the query params will not be sent to the server\n    responseMode = 'fragment',\n    ...options\n  }: BrowserOAuthClientOptions) {\n    if (!globalThis.crypto?.subtle) {\n      throw new Error('WebCrypto API is required')\n    }\n\n    if (!['query', 'fragment'].includes(responseMode)) {\n      // Make sure \"form_post\" is not used as it is not supported in the browser\n      throw new TypeError(`Invalid response mode: ${responseMode}`)\n    }\n\n    const database = new BrowserOAuthDatabase()\n\n    super({\n      ...options,\n\n      clientMetadata,\n      responseMode,\n      keyset: undefined,\n\n      runtimeImplementation,\n\n      sessionStore: database.getSessionStore(),\n      stateStore: database.getStateStore(),\n\n      didCache: database.getDidCache(),\n      handleCache: database.getHandleCache(),\n      dpopNonceCache: database.getDpopNonceCache(),\n      authorizationServerMetadataCache:\n        database.getAuthorizationServerMetadataCache(),\n      protectedResourceMetadataCache:\n        database.getProtectedResourceMetadataCache(),\n\n      onDelete: async (sub, cause) => {\n        if (localStorage.getItem(`${NAMESPACE}(sub)`) === sub) {\n          localStorage.removeItem(`${NAMESPACE}(sub)`)\n        }\n\n        syncChannel.postMessage({\n          name: 'onDelete',\n          args: [sub, cause],\n        } satisfies SyncChannelMessage)\n\n        return options.onDelete?.call(null, sub, cause)\n      },\n\n      onUpdate: async (sub, session) => {\n        syncChannel.postMessage({\n          name: 'onUpdate',\n          args: [sub, session],\n        } satisfies SyncChannelMessage)\n\n        return options.onUpdate?.call(null, sub, session)\n      },\n    })\n\n    this.database = database\n\n    const { signal } = this.ac\n\n    // Trigger hooks when an event is emitted in another tab\n    syncChannel.addEventListener(\n      'message',\n      (event) => {\n        if (event.source === window) return\n\n        const { name, args } = event.data as SyncChannelMessage\n\n        const hook = options[name]\n\n        // @ts-expect-error TS has a hard time matching the args with the hook\n        void hook?.(...args)\n      },\n      // Remove the listener when the client is disposed\n      { signal },\n    )\n  }\n\n  /**\n   * This method will automatically restore any existing session, or attempt to\n   * process login callback if the URL contains oauth parameters.\n   *\n   * Use {@link BrowserOAuthClient.initCallback} instead of this method if you\n   * want to force a login callback. This can be esp. useful if you are using\n   * this lib from a framework that has some kind of URL manipulation (like a\n   * client side router).\n   *\n   * Use {@link BrowserOAuthClient.initRestore} instead of this method if you\n   * want to only restore existing sessions, and bypass the automatic processing\n   * of login callbacks.\n   */\n  async init(refresh?: boolean): Promise<\n    // Session restored\n    | { session: OAuthSession; state?: never }\n    // Login callback processed\n    | { session: OAuthSession; state: string | null }\n    // No session or callback\n    | undefined\n  > {\n    // If the URL currently contains oauth query parameters (\"state\" + \"code\" or\n    // \"state\" + \"error\"), let's automatically process them.\n    const params = this.readCallbackParams()\n    if (params) {\n      const redirectUri = this.findRedirectUrl()\n      if (redirectUri) return this.initCallback(params, redirectUri)\n    }\n\n    return this.initRestore(refresh)\n  }\n\n  async initRestore(refresh?: boolean) {\n    // @NOTE Fixing the location should not be needed from callback endpoints\n    // since callback endpoint are required to use IP based URLs (for localhost)\n    await fixLocation(this.clientMetadata)\n\n    const sub = localStorage.getItem(`${NAMESPACE}(sub)`)\n    if (sub) {\n      try {\n        const session = await this.restore(sub, refresh)\n        return { session }\n      } catch (err) {\n        localStorage.removeItem(`${NAMESPACE}(sub)`)\n        throw err\n      }\n    }\n  }\n\n  async restore(sub: string, refresh?: boolean): Promise<OAuthSession> {\n    const session = await super.restore(sub, refresh)\n    localStorage.setItem(`${NAMESPACE}(sub)`, session.sub)\n    return session\n  }\n\n  async revoke(sub: string) {\n    localStorage.removeItem(`${NAMESPACE}(sub)`)\n    return super.revoke(sub)\n  }\n\n  async signIn(\n    input: string,\n    options?: AuthorizeOptions,\n  ): Promise<OAuthSession> {\n    if (options?.display === 'popup') {\n      return this.signInPopup(input, options)\n    } else {\n      return this.signInRedirect(input, options)\n    }\n  }\n\n  async signInRedirect(\n    input: string,\n    options?: AuthorizeOptions,\n  ): Promise<never> {\n    const url = await this.authorize(input, options)\n\n    window.location.href = url.href\n\n    // back-forward cache\n    return new Promise<never>((resolve, reject) => {\n      setTimeout(\n        (err: Error) => {\n          // Take the opportunity to proactively cancel the pending request\n          this.abortRequest(url).then(\n            () => reject(err),\n            (reason) => reject(new AggregateError([err, reason])),\n          )\n        },\n        5e3,\n        new Error('User navigated back'),\n      )\n    })\n  }\n\n  async signInPopup(\n    input: string,\n    options?: Omit<AuthorizeOptions, 'state'>,\n  ): Promise<OAuthSession> {\n    // Open new window asap to prevent popup busting by browsers\n    const popupFeatures = 'width=600,height=600,menubar=no,toolbar=no'\n    let popup: Window | null = window.open(\n      'about:blank',\n      '_blank',\n      popupFeatures,\n    )\n\n    const stateKey = `${Math.random().toString(36).slice(2)}`\n\n    const url = await this.authorize(input, {\n      ...options,\n      state: `${POPUP_STATE_PREFIX}${stateKey}`,\n      display: options?.display ?? 'popup',\n    })\n\n    options?.signal?.throwIfAborted()\n\n    if (popup) {\n      popup.window.location.href = url.href\n    } else {\n      popup = window.open(url.href, '_blank', popupFeatures)\n    }\n\n    popup?.focus()\n\n    return new Promise<OAuthSession>((resolve, reject) => {\n      const popupChannel = new BroadcastChannel(POPUP_CHANNEL_NAME)\n\n      const cleanup = () => {\n        clearTimeout(timeout)\n        popupChannel.removeEventListener('message', onMessage)\n        popupChannel.close()\n        options?.signal?.removeEventListener('abort', cancel)\n        popup?.close()\n      }\n\n      const cancel = () => {\n        // @TODO Store fact that the request was cancelled, allowing any\n        // callback (e.g. in the popup) to revoke the session or credentials.\n\n        reject(new Error(options?.signal?.aborted ? 'Aborted' : 'Timeout'))\n        cleanup()\n      }\n\n      options?.signal?.addEventListener('abort', cancel)\n\n      const timeout = setTimeout(cancel, 5 * 60e3)\n\n      const onMessage = async ({ data }: MessageEvent<PopupChannelData>) => {\n        if (data.key !== stateKey) return\n        if (!('result' in data)) return\n\n        // Send acknowledgment to popup window\n        popupChannel.postMessage({ key: stateKey, ack: true })\n\n        cleanup()\n\n        const { result } = data\n        if (result.status === 'fulfilled') {\n          const sub = result.value\n          try {\n            options?.signal?.throwIfAborted()\n            resolve(await this.restore(sub, false))\n          } catch (err) {\n            reject(err)\n            void this.revoke(sub)\n          }\n        } else {\n          const { message, params } = result.reason\n          reject(new OAuthCallbackError(new URLSearchParams(params), message))\n        }\n      }\n\n      popupChannel.addEventListener('message', onMessage)\n    })\n  }\n\n  public findRedirectUrl() {\n    for (const uri of this.clientMetadata.redirect_uris) {\n      const url = new URL(uri)\n      if (\n        location.origin === url.origin &&\n        location.pathname === url.pathname\n      ) {\n        return uri\n      }\n    }\n\n    return undefined\n  }\n\n  public readCallbackParams(): URLSearchParams | null {\n    const params =\n      this.responseMode === 'fragment'\n        ? new URLSearchParams(location.hash.slice(1))\n        : new URLSearchParams(location.search)\n\n    // Only if the current URL contains a valid oauth response params\n    if (!params.has('state') || !(params.has('code') || params.has('error'))) {\n      return null\n    }\n\n    return params\n  }\n\n  public async initCallback(\n    params = this.readCallbackParams(),\n    redirectUri = this.findRedirectUrl(),\n  ): Promise<{\n    session: OAuthSession\n    state: string | null\n  }> {\n    if (!params) {\n      throw new TypeError('No OAuth callback parameters found in the URL')\n    }\n\n    // Replace the current history entry without the params (this will prevent\n    // the following code to run again if the user refreshes the page)\n    if (this.responseMode === 'fragment') {\n      history.replaceState(null, '', location.pathname + location.search)\n    } else if (this.responseMode === 'query') {\n      history.replaceState(null, '', location.pathname)\n    }\n\n    // Utility function to send the result of the popup to the parent window\n    const sendPopupResult = (message: PopupChannelResultData) => {\n      const popupChannel = new BroadcastChannel(POPUP_CHANNEL_NAME)\n\n      return new Promise<boolean>((resolve) => {\n        const cleanup = (result: boolean) => {\n          clearTimeout(timer)\n          popupChannel.removeEventListener('message', onMessage)\n          popupChannel.close()\n          resolve(result)\n        }\n\n        const onMessage = ({ data }: MessageEvent<PopupChannelData>) => {\n          if ('ack' in data && message.key === data.key) cleanup(true)\n        }\n\n        popupChannel.addEventListener('message', onMessage)\n        popupChannel.postMessage(message)\n        // Receiving of \"ack\" should be very fast, giving it 500 ms anyway\n        const timer = setTimeout(cleanup, 500, false)\n      })\n    }\n\n    return this.callback(params, { redirect_uri: redirectUri })\n      .then(async (result) => {\n        if (result.state?.startsWith(POPUP_STATE_PREFIX)) {\n          const receivedByParent = await sendPopupResult({\n            key: result.state.slice(POPUP_STATE_PREFIX.length),\n            result: {\n              status: 'fulfilled',\n              value: result.session.sub,\n            },\n          })\n\n          // Revoke the credentials if the parent window was closed\n          if (!receivedByParent) await result.session.signOut()\n\n          throw new LoginContinuedInParentWindowError() // signInPopup\n        }\n\n        localStorage.setItem(`${NAMESPACE}(sub)`, result.session.sub)\n\n        return result\n      })\n      .catch(async (err) => {\n        if (\n          err instanceof OAuthCallbackError &&\n          err.state?.startsWith(POPUP_STATE_PREFIX)\n        ) {\n          await sendPopupResult({\n            key: err.state.slice(POPUP_STATE_PREFIX.length),\n            result: {\n              status: 'rejected',\n              reason: {\n                message: err.message,\n                params: Array.from(err.params.entries()),\n              },\n            },\n          })\n\n          throw new LoginContinuedInParentWindowError() // signInPopup\n        }\n\n        // Most probable cause at this point is that the \"state\" parameter is\n        // invalid.\n        throw err\n      })\n      .catch((err) => {\n        if (err instanceof LoginContinuedInParentWindowError) {\n          // parent will also try to close the popup\n          window.close()\n        }\n\n        throw err\n      })\n  }\n\n  async [Symbol.asyncDispose]() {\n    try {\n      this.ac.abort()\n    } finally {\n      await this.database[Symbol.asyncDispose]()\n    }\n  }\n\n  dispose() {\n    this[Symbol.dispose]()\n  }\n}\n\n/**\n * Since \"localhost\" is often used either in IP mode or in hostname mode,\n * and because the redirect uris must use the IP mode, we need to make sure\n * that the current location url is not using \"localhost\".\n *\n * This is required for the IndexedDB to work properly. Indeed, the IndexedDB\n * is shared by origin, so we must ensure to be on the same origin as the\n * redirect uris.\n */\nfunction fixLocation(clientMetadata: ClientMetadata) {\n  if (!isOAuthClientIdLoopback(clientMetadata.client_id)) return\n  if (window.location.hostname !== 'localhost') return\n\n  const locationUrl = new URL(window.location.href)\n\n  for (const uri of clientMetadata.redirect_uris) {\n    const url = new URL(uri)\n    if (\n      (url.hostname === '127.0.0.1' || url.hostname === '[::1]') &&\n      (!url.port || url.port === locationUrl.port) &&\n      url.protocol === locationUrl.protocol &&\n      url.pathname === locationUrl.pathname\n    ) {\n      url.port = locationUrl.port\n      window.location.href = url.href\n\n      // Prevent init() on the wrong origin\n      throw new Error('Redirecting to loopback IP...')\n    }\n  }\n\n  throw new Error(\n    `Please use the loopback IP address instead of ${locationUrl}`,\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/browser-oauth-database.ts",
    "content": "import { DidDocument } from '@atproto/did'\nimport { Key } from '@atproto/jwk'\nimport { WebcryptoKey } from '@atproto/jwk-webcrypto'\nimport { InternalStateData, Session } from '@atproto/oauth-client'\nimport {\n  OAuthAuthorizationServerMetadata,\n  OAuthProtectedResourceMetadata,\n} from '@atproto/oauth-types'\nimport { ResolvedHandle } from '@atproto-labs/handle-resolver'\nimport { SimpleStore, Value } from '@atproto-labs/simple-store'\nimport { DB, DBObjectStore } from './indexed-db/index.js'\nimport { TupleUnion } from './util.js'\n\ntype Item<V> = {\n  value: V\n  expiresAt?: string // ISO Date\n}\n\ntype EncodedKey = {\n  keyId: string\n  keyPair: CryptoKeyPair\n}\n\nfunction encodeKey(key: Key): EncodedKey {\n  if (!(key instanceof WebcryptoKey) || !key.kid) {\n    throw new Error('Invalid key object')\n  }\n  return {\n    keyId: key.kid,\n    keyPair: key.cryptoKeyPair,\n  }\n}\n\nasync function decodeKey(encoded: EncodedKey): Promise<Key> {\n  return WebcryptoKey.fromKeypair(encoded.keyPair, encoded.keyId)\n}\n\nexport type Schema = {\n  state: Item<Omit<InternalStateData, 'dpopKey'> & { dpopKey: EncodedKey }>\n  session: Item<Omit<Session, 'dpopKey'> & { dpopKey: EncodedKey }>\n  didCache: Item<DidDocument>\n  dpopNonceCache: Item<string>\n  handleCache: Item<ResolvedHandle>\n  authorizationServerMetadataCache: Item<OAuthAuthorizationServerMetadata>\n  protectedResourceMetadataCache: Item<OAuthProtectedResourceMetadata>\n}\n\nexport type DatabaseStore<V extends Value> = SimpleStore<string, V>\n\nconst STORES: TupleUnion<keyof Schema> = [\n  'state',\n  'session',\n\n  'didCache',\n  'dpopNonceCache',\n  'handleCache',\n  'authorizationServerMetadataCache',\n  'protectedResourceMetadataCache',\n]\n\nexport type BrowserOAuthDatabaseOptions = {\n  name?: string\n  durability?: 'strict' | 'relaxed'\n  cleanupInterval?: number\n}\n\nexport class BrowserOAuthDatabase {\n  #dbPromise: Promise<DB<Schema>>\n  #cleanupInterval?: ReturnType<typeof setInterval>\n\n  constructor(options?: BrowserOAuthDatabaseOptions) {\n    this.#dbPromise = DB.open<Schema>(\n      options?.name ?? '@atproto-oauth-client',\n      [\n        (db) => {\n          for (const name of STORES) {\n            const store = db.createObjectStore(name, { autoIncrement: true })\n            store.createIndex('expiresAt', 'expiresAt', { unique: false })\n          }\n        },\n      ],\n      { durability: options?.durability ?? 'strict' },\n    )\n\n    this.#cleanupInterval = setInterval(() => {\n      void this.cleanup()\n    }, options?.cleanupInterval ?? 30e3)\n  }\n\n  protected async run<N extends keyof Schema, R>(\n    storeName: N,\n    mode: 'readonly' | 'readwrite',\n    fn: (s: DBObjectStore<Schema[N]>) => R | Promise<R>,\n  ): Promise<R> {\n    const db = await this.#dbPromise\n    return await db.transaction([storeName], mode, (tx) =>\n      fn(tx.objectStore(storeName)),\n    )\n  }\n\n  protected createStore<N extends keyof Schema, V extends Value>(\n    name: N,\n    {\n      encode,\n      decode,\n      expiresAt,\n    }: {\n      encode: (value: V) => Schema[N]['value'] | PromiseLike<Schema[N]['value']>\n      decode: (encoded: Schema[N]['value']) => V | PromiseLike<V>\n      expiresAt: (value: V) => null | Date\n    },\n  ): DatabaseStore<V> {\n    return {\n      get: async (key) => {\n        // Find item in store\n        const item = await this.run(name, 'readonly', (store) => store.get(key))\n\n        // Not found\n        if (item === undefined) return undefined\n\n        // Too old (delete)\n        if (item.expiresAt != null && new Date(item.expiresAt) < new Date()) {\n          await this.run(name, 'readwrite', (store) => store.delete(key))\n          return undefined\n        }\n\n        // Item found and valid. Decode\n        return decode(item.value)\n      },\n\n      set: async (key, value) => {\n        // Create encoded item record\n        const item = {\n          value: await encode(value),\n          expiresAt: expiresAt(value)?.toISOString(),\n        } as Schema[N]\n\n        // Store item record\n        await this.run(name, 'readwrite', (store) => store.put(item, key))\n      },\n\n      del: async (key) => {\n        // Delete\n        await this.run(name, 'readwrite', (store) => store.delete(key))\n      },\n    }\n  }\n\n  getSessionStore(): DatabaseStore<Session> {\n    return this.createStore('session', {\n      expiresAt: ({ tokenSet }) =>\n        tokenSet.refresh_token || tokenSet.expires_at == null\n          ? null\n          : new Date(tokenSet.expires_at),\n      encode: ({ dpopKey, ...session }) => ({\n        ...session,\n        dpopKey: encodeKey(dpopKey),\n      }),\n      decode: async ({ dpopKey, ...encoded }) => ({\n        ...encoded,\n        dpopKey: await decodeKey(dpopKey),\n      }),\n    })\n  }\n\n  getStateStore(): DatabaseStore<InternalStateData> {\n    return this.createStore('state', {\n      expiresAt: (_value) => new Date(Date.now() + 10 * 60e3),\n      encode: ({ dpopKey, ...session }) => ({\n        ...session,\n        dpopKey: encodeKey(dpopKey),\n      }),\n      decode: async ({ dpopKey, ...encoded }) => ({\n        ...encoded,\n        dpopKey: await decodeKey(dpopKey),\n      }),\n    })\n  }\n\n  getDpopNonceCache(): DatabaseStore<string> {\n    return this.createStore('dpopNonceCache', {\n      expiresAt: (_value) => new Date(Date.now() + 600e3),\n      encode: (value) => value,\n      decode: (encoded) => encoded,\n    })\n  }\n\n  getDidCache(): DatabaseStore<DidDocument> {\n    return this.createStore('didCache', {\n      expiresAt: (_value) => new Date(Date.now() + 60e3),\n      encode: (value) => value,\n      decode: (encoded) => encoded,\n    })\n  }\n\n  getHandleCache(): DatabaseStore<ResolvedHandle> {\n    return this.createStore('handleCache', {\n      expiresAt: (_value) => new Date(Date.now() + 60e3),\n      encode: (value) => value,\n      decode: (encoded) => encoded,\n    })\n  }\n\n  getAuthorizationServerMetadataCache():\n    | undefined\n    | DatabaseStore<OAuthAuthorizationServerMetadata> {\n    return this.createStore('authorizationServerMetadataCache', {\n      expiresAt: (_value) => new Date(Date.now() + 60e3),\n      encode: (value) => value,\n      decode: (encoded) => encoded,\n    })\n  }\n\n  getProtectedResourceMetadataCache():\n    | undefined\n    | DatabaseStore<OAuthProtectedResourceMetadata> {\n    return this.createStore('protectedResourceMetadataCache', {\n      expiresAt: (_value) => new Date(Date.now() + 60e3),\n      encode: (value) => value,\n      decode: (encoded) => encoded,\n    })\n  }\n\n  async cleanup() {\n    const db = await this.#dbPromise\n\n    for (const name of STORES) {\n      await db.transaction([name], 'readwrite', (tx) =>\n        tx\n          .objectStore(name)\n          .index('expiresAt')\n          .deleteAll(IDBKeyRange.upperBound(Date.now())),\n      )\n    }\n  }\n\n  async [Symbol.asyncDispose]() {\n    clearInterval(this.#cleanupInterval)\n    this.#cleanupInterval = undefined\n\n    const dbPromise = this.#dbPromise\n    this.#dbPromise = Promise.reject(new Error('Database has been disposed'))\n\n    // Avoid \"unhandled promise rejection\"\n    this.#dbPromise.catch(() => null)\n\n    // Spec recommends not to throw errors in dispose\n    const db = await dbPromise.catch(() => null)\n    if (db) await (db[Symbol.asyncDispose] || db[Symbol.dispose]).call(db)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/browser-runtime-implementation.ts",
    "content": "import { WebcryptoKey } from '@atproto/jwk-webcrypto'\nimport {\n  DigestAlgorithm,\n  Key,\n  RuntimeImplementation,\n  RuntimeLock,\n} from '@atproto/oauth-client'\n\n/**\n * @see {@link // https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request}\n */\nconst nativeRequestLock: undefined | RuntimeLock =\n  typeof navigator !== 'undefined' && navigator.locks?.request\n    ? <T>(name: string, fn: () => T | PromiseLike<T>): Promise<T> =>\n        navigator.locks.request(name, { mode: 'exclusive' }, async () => fn())\n    : undefined\n\nexport class BrowserRuntimeImplementation implements RuntimeImplementation {\n  requestLock = nativeRequestLock\n\n  constructor() {\n    if (typeof crypto !== 'object' || !crypto?.subtle) {\n      throw new Error(\n        'Crypto with CryptoSubtle is required. If running in a browser, make sure the current page is loaded over HTTPS.',\n      )\n    }\n\n    if (!this.requestLock) {\n      // There is no real need to polyfill this on older browsers. Indeed, the\n      // oauth-client library will try and recover from concurrency issues when\n      // refreshing tokens.\n      console.warn(\n        'Locks API not available. You should consider using a more recent browser.',\n      )\n    }\n  }\n\n  async createKey(algs: string[]): Promise<Key> {\n    return WebcryptoKey.generate(algs)\n  }\n\n  getRandomValues(byteLength: number): Uint8Array {\n    return crypto.getRandomValues(new Uint8Array(byteLength))\n  }\n\n  async digest(\n    data: Uint8Array,\n    { name }: DigestAlgorithm,\n  ): Promise<Uint8Array> {\n    switch (name) {\n      case 'sha256':\n      case 'sha384':\n      case 'sha512': {\n        const buf = await crypto.subtle.digest(`SHA-${name.slice(3)}`, data)\n        return new Uint8Array(buf)\n      }\n      default:\n        throw new Error(`Unsupported digest algorithm: ${name}`)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/errors.ts",
    "content": "/**\n * Special error class destined to be thrown when the login process was\n * performed in a popup and should be continued in the parent/initiating window.\n */\nexport class LoginContinuedInParentWindowError extends Error {\n  code = 'LOGIN_CONTINUED_IN_PARENT_WINDOW'\n  constructor() {\n    super('Login complete, please close the popup window.')\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/index.ts",
    "content": "import 'core-js/modules/esnext.symbol.async-dispose'\nimport 'core-js/modules/esnext.symbol.dispose'\n\nexport * from '@atproto/jwk-webcrypto'\nexport * from '@atproto/oauth-client'\n\nexport * from './browser-oauth-client.js'\nexport * from './errors.js'\nexport { buildLoopbackClientId } from './util.js'\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/README.md",
    "content": "# IndexedDB utilities\n\nThis is a small wrapper around the IndexedDB API that provides a simple way to\nstore and retrieve data from an IndexedDB database.\n\nThis _could_ be used as a standalone library, but the Bluesky dev team does not\nwant to maintain it as such. As it is currently only used by the\n`@atproto/oauth-client-browser` package, it is included here.\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/db-index.ts",
    "content": "import { ObjectStoreSchema } from './schema.js'\nimport { promisify } from './util.js'\n\nexport class DBIndex<Schema extends ObjectStoreSchema> {\n  constructor(private idbIndex: IDBIndex) {}\n\n  count(query?: IDBValidKey | IDBKeyRange) {\n    return promisify(this.idbIndex.count(query))\n  }\n\n  get(query: IDBValidKey | IDBKeyRange) {\n    return promisify<Schema>(this.idbIndex.get(query))\n  }\n\n  getKey(query: IDBValidKey | IDBKeyRange) {\n    return promisify(this.idbIndex.getKey(query))\n  }\n\n  getAll(query?: IDBValidKey | IDBKeyRange | null, count?: number) {\n    return promisify<Schema[]>(this.idbIndex.getAll(query, count))\n  }\n\n  getAllKeys(query?: IDBValidKey | IDBKeyRange | null, count?: number) {\n    return promisify(this.idbIndex.getAllKeys(query, count))\n  }\n\n  deleteAll(query?: IDBValidKey | IDBKeyRange | null): Promise<void> {\n    return new Promise((resolve, reject) => {\n      const result = this.idbIndex.openCursor(query)\n      result.onsuccess = function (event) {\n        const cursor = (event as any).target.result as IDBCursorWithValue\n        if (cursor) {\n          cursor.delete()\n          cursor.continue()\n        } else {\n          resolve()\n        }\n      }\n      result.onerror = function (event) {\n        reject((event.target as any)?.error || new Error('Unexpected error'))\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/db-object-store.ts",
    "content": "import { DBIndex } from './db-index.js'\nimport { ObjectStoreSchema } from './schema.js'\nimport { promisify } from './util.js'\n\nexport class DBObjectStore<Schema extends ObjectStoreSchema> {\n  constructor(private idbObjStore: IDBObjectStore) {}\n\n  get name() {\n    return this.idbObjStore.name\n  }\n\n  index(name: string) {\n    return new DBIndex<Schema>(this.idbObjStore.index(name))\n  }\n\n  get(key: IDBValidKey | IDBKeyRange) {\n    return promisify<Schema>(this.idbObjStore.get(key))\n  }\n\n  getKey(query: IDBValidKey | IDBKeyRange) {\n    return promisify<IDBValidKey | undefined>(this.idbObjStore.getKey(query))\n  }\n\n  getAll(query?: IDBValidKey | IDBKeyRange | null, count?: number) {\n    return promisify<Schema[]>(this.idbObjStore.getAll(query, count))\n  }\n\n  getAllKeys(query?: IDBValidKey | IDBKeyRange | null, count?: number) {\n    return promisify<IDBValidKey[]>(this.idbObjStore.getAllKeys(query, count))\n  }\n\n  add(value: Schema, key?: IDBValidKey) {\n    return promisify(this.idbObjStore.add(value, key))\n  }\n\n  put(value: Schema, key?: IDBValidKey) {\n    return promisify(this.idbObjStore.put(value, key))\n  }\n\n  delete(key: IDBValidKey | IDBKeyRange) {\n    return promisify(this.idbObjStore.delete(key))\n  }\n\n  clear() {\n    return promisify(this.idbObjStore.clear())\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/db-transaction.ts",
    "content": "import 'core-js/modules/esnext.symbol.dispose'\n\nimport { DBObjectStore } from './db-object-store.js'\nimport { DatabaseSchema } from './schema.js'\n\nexport class DBTransaction<Schema extends DatabaseSchema>\n  implements Disposable\n{\n  #tx: IDBTransaction | null\n\n  constructor(tx: IDBTransaction) {\n    this.#tx = tx\n\n    const onAbort = () => {\n      cleanup()\n    }\n    const onComplete = () => {\n      cleanup()\n    }\n    const cleanup = () => {\n      this.#tx = null\n      tx.removeEventListener('abort', onAbort)\n      tx.removeEventListener('complete', onComplete)\n    }\n    tx.addEventListener('abort', onAbort)\n    tx.addEventListener('complete', onComplete)\n  }\n\n  protected get tx(): IDBTransaction {\n    if (!this.#tx) throw new Error('Transaction already ended')\n    return this.#tx\n  }\n\n  async abort() {\n    const { tx } = this\n    this.#tx = null\n    tx.abort()\n  }\n\n  async commit() {\n    const { tx } = this\n    this.#tx = null\n    tx.commit?.()\n  }\n\n  objectStore<T extends keyof Schema & string>(name: T) {\n    const store = this.tx.objectStore(name)\n    return new DBObjectStore<Schema[T]>(store)\n  }\n\n  [Symbol.dispose](): void {\n    if (this.#tx) this.commit()\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/db.ts",
    "content": "import 'core-js/modules/esnext.symbol.dispose'\n\nimport { DBTransaction } from './db-transaction.js'\nimport { DatabaseSchema } from './schema.js'\n\nexport class DB<Schema extends DatabaseSchema> implements Disposable {\n  static async open<Schema extends DatabaseSchema = DatabaseSchema>(\n    dbName: string,\n    migrations: ReadonlyArray<(db: IDBDatabase) => void>,\n    txOptions?: IDBTransactionOptions,\n  ) {\n    const db = await new Promise<IDBDatabase>((resolve, reject) => {\n      const request = indexedDB.open(dbName, migrations.length)\n\n      request.onerror = () => reject(request.error)\n      request.onsuccess = () => resolve(request.result)\n      request.onupgradeneeded = ({ oldVersion, newVersion }) => {\n        const db = request.result\n        try {\n          for (\n            let version = oldVersion;\n            version < (newVersion ?? migrations.length);\n            ++version\n          ) {\n            const migration = migrations[version]\n            if (migration) migration(db)\n            else throw new Error(`Missing migration for version ${version}`)\n          }\n        } catch (err) {\n          db.close()\n          reject(err)\n        }\n      }\n    })\n\n    return new DB<Schema>(db, txOptions)\n  }\n\n  #db: null | IDBDatabase\n\n  constructor(\n    db: IDBDatabase,\n    protected readonly txOptions?: IDBTransactionOptions,\n  ) {\n    this.#db = db\n\n    const cleanup = () => {\n      this.#db = null\n      db.removeEventListener('versionchange', cleanup)\n      db.removeEventListener('close', cleanup)\n      db.close() // Can we call close on a \"closed\" database?\n    }\n\n    db.addEventListener('versionchange', cleanup)\n    db.addEventListener('close', cleanup)\n  }\n\n  protected get db(): IDBDatabase {\n    if (!this.#db) throw new Error('Database closed')\n    return this.#db\n  }\n\n  get name() {\n    return this.db.name\n  }\n\n  get objectStoreNames() {\n    return this.db.objectStoreNames\n  }\n\n  get version() {\n    return this.db.version\n  }\n\n  async transaction<T extends readonly (keyof Schema & string)[], R>(\n    storeNames: T,\n    mode: IDBTransactionMode,\n    run: (tx: DBTransaction<Pick<Schema, T[number]>>) => R | PromiseLike<R>,\n  ): Promise<R> {\n    // eslint-disable-next-line no-async-promise-executor\n    return new Promise<R>(async (resolve, reject) => {\n      try {\n        const tx = this.db.transaction(storeNames, mode, this.txOptions)\n        let result: { done: false } | { done: true; value: R } = { done: false }\n\n        tx.oncomplete = () => {\n          if (result.done) resolve(result.value)\n          else reject(new Error('Transaction completed without result'))\n        }\n        tx.onerror = () => reject(tx.error)\n        tx.onabort = () => reject(tx.error || new Error('Transaction aborted'))\n\n        try {\n          const value = await run(new DBTransaction(tx))\n          result = { done: true, value }\n          tx.commit()\n        } catch (err) {\n          tx.abort()\n          throw err\n        }\n      } catch (err) {\n        reject(err)\n      }\n    })\n  }\n\n  close() {\n    const { db } = this\n    this.#db = null\n    db.close()\n  }\n\n  [Symbol.dispose]() {\n    if (this.#db) return this.close()\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/index.ts",
    "content": "export * from './db.js'\nexport * from './db-index.js'\nexport * from './db-object-store.js'\nexport * from './db-transaction.js'\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/schema.ts",
    "content": "export type ObjectStoreSchema = NonNullable<unknown>\nexport type DatabaseSchema = Record<string, ObjectStoreSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db/util.ts",
    "content": "export function handleRequest<T>(\n  request: IDBRequest<T>,\n  onSuccess: (result: T) => void,\n  onError: (error: Error) => void,\n) {\n  const cleanup = () => {\n    request.removeEventListener('success', success)\n    request.removeEventListener('error', error)\n  }\n  const success = () => {\n    onSuccess(request.result)\n    cleanup()\n  }\n  const error = () => {\n    onError(request.error || new Error('Unknown error'))\n    cleanup()\n  }\n  request.addEventListener('success', success)\n  request.addEventListener('error', error)\n}\n\nexport function promisify<T>(request: IDBRequest<T>) {\n  return new Promise<T>((resolve, reject) => {\n    handleRequest(request, resolve, reject)\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/indexed-db-store.ts",
    "content": "import { Key, SimpleStore, Value } from '@atproto-labs/simple-store'\nimport { DB, DBObjectStore } from './indexed-db/index.js'\n\nconst storeName = 'store'\ntype Item<V> = {\n  value: V\n  createdAt: Date\n}\n\nexport class IndexedDBStore<\n  K extends Extract<IDBValidKey, Key>,\n  V extends Value,\n> implements SimpleStore<K, V>\n{\n  constructor(\n    private dbName: string,\n    protected maxAge = 600e3,\n  ) {}\n\n  protected async run<R>(\n    mode: 'readonly' | 'readwrite',\n    fn: (s: DBObjectStore<Item<V>>) => R | Promise<R>,\n  ): Promise<R> {\n    const db = await DB.open<{ store: Item<V> }>(\n      this.dbName,\n      [\n        (db) => {\n          const store = db.createObjectStore(storeName)\n          store.createIndex('createdAt', 'createdAt', { unique: false })\n        },\n      ],\n      { durability: 'strict' },\n    )\n    try {\n      return await db.transaction([storeName], mode, (tx) =>\n        fn(tx.objectStore(storeName)),\n      )\n    } finally {\n      await db[Symbol.dispose]()\n    }\n  }\n\n  async get(key: K): Promise<V | undefined> {\n    const item = await this.run('readonly', (store) => store.get(key))\n\n    if (!item) return undefined\n\n    const age = Date.now() - item.createdAt.getTime()\n    if (age > this.maxAge) {\n      await this.del(key)\n      return undefined\n    }\n\n    return item?.value\n  }\n\n  async set(key: K, value: V): Promise<void> {\n    await this.run('readwrite', (store) => {\n      store.put({ value, createdAt: new Date() }, key)\n    })\n  }\n\n  async del(key: K): Promise<void> {\n    await this.run('readwrite', (store) => {\n      store.delete(key)\n    })\n  }\n\n  async deleteOutdated() {\n    const upperBound = new Date(Date.now() - this.maxAge)\n    const query = IDBKeyRange.upperBound(upperBound)\n\n    await this.run('readwrite', async (store) => {\n      const index = store.index('createdAt')\n      const keys = await index.getAllKeys(query)\n      for (const key of keys) store.delete(key)\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/src/util.ts",
    "content": "import { isLoopbackHost } from '@atproto/oauth-types'\n\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\nexport type TupleUnion<U extends string, R extends any[] = []> = {\n  [S in U]: Exclude<U, S> extends never\n    ? [...R, S]\n    : TupleUnion<Exclude<U, S>, [...R, S]>\n}[U]\n\n/**\n * @example\n * ```ts\n * const clientId = buildLoopbackClientId(window.location)\n * ```\n */\nexport function buildLoopbackClientId(\n  location: {\n    hostname: string\n    pathname: string\n    port: string\n  },\n  localhost = '127.0.0.1',\n): string {\n  if (!isLoopbackHost(location.hostname)) {\n    throw new TypeError(`Expected a loopback host, got ${location.hostname}`)\n  }\n\n  const redirectUri = `http://${location.hostname === 'localhost' ? localhost : location.hostname}${location.port && !location.port.startsWith(':') ? `:${location.port}` : location.port}${location.pathname}`\n\n  return `http://localhost${\n    location.pathname === '/' ? '' : location.pathname\n  }?redirect_uri=${encodeURIComponent(redirectUri)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/.gitignore",
    "content": "src/lexicons\ndist\n.swc\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/CHANGELOG.md",
    "content": "# @atproto/oauth-client-browser-example\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4586](https://github.com/bluesky-social/atproto/pull/4586) [`b619ae8`](https://github.com/bluesky-social/atproto/commit/b619ae87a8aff8f0db7785b26b0a3602d6ef6149) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid using the app with an invalid scope that prevents the labelers to be set\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose bskyClient instance and lexicons on window object\n\n- [#4461](https://github.com/bluesky-social/atproto/pull/4461) [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Add support for signup (prompt=create)\n\n- [#4467](https://github.com/bluesky-social/atproto/pull/4467) [`a78380c`](https://github.com/bluesky-social/atproto/commit/a78380c89cb47c348e77eadb0738e7cb1c867a69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor ui tweak\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rewrite `getSession` and `getRecord` using generic hooks\n\n- [#4457](https://github.com/bluesky-social/atproto/pull/4457) [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add bin script, allowing to run through npx\n\n## 0.0.8\n\n### Patch Changes\n\n- [#4319](https://github.com/bluesky-social/atproto/pull/4319) [`3202dce91`](https://github.com/bluesky-social/atproto/commit/3202dce91b31daa065769b6387c6b199f515671a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update example app\n\n## 0.0.7\n\n### Patch Changes\n\n- [#4311](https://github.com/bluesky-social/atproto/pull/4311) [`d764c54fe`](https://github.com/bluesky-social/atproto/commit/d764c54fe4ffaccb9ed9580c153373fc6e12f803) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor improvements\n\n## 0.0.6\n\n### Patch Changes\n\n- [`f4cb3e4d0`](https://github.com/bluesky-social/atproto/commit/f4cb3e4d0ac45e567fa14f79b99a84621fa89a56) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update default scopes\n\n## 0.0.5\n\n### Patch Changes\n\n- [#3952](https://github.com/bluesky-social/atproto/pull/3952) [`09d90ae48`](https://github.com/bluesky-social/atproto/commit/09d90ae486c451512e72c098228de3f3dd058101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve UI\n\n## 0.0.4\n\n### Patch Changes\n\n- [#3820](https://github.com/bluesky-social/atproto/pull/3820) [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `com.atproto.server.getSession` query.\n\n- [#3820](https://github.com/bluesky-social/atproto/pull/3820) [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow configuration of loopback client id scope through query param\n\n## 0.0.3\n\n### Patch Changes\n\n- [#3797](https://github.com/bluesky-social/atproto/pull/3797) [`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `pdsAgent` as global constant\n\n## 0.0.2\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update react to version 19\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build using SWC\n\n## 0.0.1\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>OAuth Client Example</title>\n    <link href=\"./src/index.css\" rel=\"stylesheet\" />\n  </head>\n  <body class=\"min-h-screen bg-slate-100 dark:bg-slate-800\">\n    <div id=\"root\"></div>\n    <script src=\"./src/index.tsx\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-client-browser-example\",\n  \"version\": \"0.0.10\",\n  \"license\": \"MIT\",\n  \"description\": \"Example single page application app using ATPROTO OAuth\",\n  \"keywords\": [\n    \"example\",\n    \"spa\",\n    \"atproto\",\n    \"oauth\",\n    \"browser\",\n    \"client\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-client-browser\"\n  },\n  \"bin\": \"./server.js\",\n  \"type\": \"commonjs\",\n  \"main\": \"./dist/files.json\",\n  \"exports\": {\n    \".\": \"./dist/files.json\",\n    \"./server\": {\n      \"import\": \"./server.js\"\n    }\n  },\n  \"files\": [\n    \"server.js\",\n    \"dist\"\n  ],\n  \"devDependencies\": {\n    \"@atproto-labs/rollup-plugin-bundle-manifest\": \"workspace:^\",\n    \"@atproto/lex\": \"workspace:^\",\n    \"@atproto/oauth-client-browser\": \"workspace:^\",\n    \"@tailwindcss/vite\": \"^4.1.3\",\n    \"@tanstack/react-query\": \"^5.71.10\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"@vitejs/plugin-react-swc\": \"^3.8.0\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-json-view\": \"^1.21.3\",\n    \"tailwindcss\": \"^4.1.3\",\n    \"vite\": \"^6.2.0\"\n  },\n  \"scripts\": {\n    \"prebuild\": \"lex build --clear --lexicons ../../../lexicons --out ./src/lexicons\",\n    \"build\": \"vite build --emptyOutDir -- ignore additional npm args\",\n    \"dev\": \"vite --port 8080 --host 127.0.0.1\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/server.js",
    "content": "/* eslint-env node, commonjs */\n\n'use strict'\n\nconst { once } = require('node:events')\nconst { createServer } = require('node:http')\nconst files = require('./dist/files.json')\n\nexports.middleware = middleware\nfunction middleware(\n  req,\n  res,\n  next = (err) => {\n    if (err) console.error(err)\n\n    const { statusCode, statusMessage } = err\n      ? { statusCode: 404, statusMessage: 'Not Found' }\n      : { statusCode: 500, statusMessage: 'Internal Server Error' }\n\n    res\n      .writeHead(statusCode, statusMessage, { 'content-type': 'text/plain' })\n      .end(statusMessage)\n  },\n) {\n  const path = req.url?.split('?')[0].slice(1) || 'index.html'\n  const file = Object.hasOwn(files, path) ? files[path] : null\n\n  if (file) {\n    res\n      .writeHead(200, 'OK', { 'content-type': file.mime })\n      .end(Buffer.from(file.data, 'base64'))\n  } else {\n    next()\n  }\n}\n\nexports.start = start\nasync function start(port = 0) {\n  const server = createServer(middleware)\n  server.listen(port)\n  await once(server, 'listening')\n  return server\n}\n\nif (require.main === module) {\n  const port = Number(process.argv[2] || process.env.PORT || 0)\n  start(port).then((server) => {\n    const address = server.address()\n    const port = typeof address === 'string' ? address : address && address.port\n    console.log(`Listening on http://127.0.0.1:${port}/`)\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/App.tsx",
    "content": "import { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { ReactNode, useEffect } from 'react'\nimport { Home } from './Home.tsx'\nimport * as lexicons from './lexicons.ts'\nimport { AuthenticationProvider } from './providers/AuthenticationProvider.tsx'\nimport {\n  BskyClientProvider,\n  useBskyClient,\n} from './providers/BskyClientProvider.tsx'\n\nconst queryClient = new QueryClient()\n\nexport function App() {\n  return (\n    <AuthenticationProvider>\n      <BskyClientProvider>\n        <QueryClientProvider client={queryClient}>\n          <DevTools>\n            <Home />\n          </DevTools>\n        </QueryClientProvider>\n      </BskyClientProvider>\n    </AuthenticationProvider>\n  )\n}\n\nexport function DevTools({ children }: { children?: ReactNode }) {\n  const client = useBskyClient()\n\n  useEffect(() => {\n    const global = window as { bskyClient?: typeof client } & Partial<\n      typeof lexicons\n    >\n    global.bskyClient = client\n    Object.assign(window, lexicons)\n    return () => {\n      delete global.bskyClient\n    }\n  }, [client])\n\n  return <>{children}</>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/Home.tsx",
    "content": "import { Layout } from './components/Layout.tsx'\nimport { ProfileInfo } from './components/ProfileInfo.tsx'\nimport { SessionInfo } from './components/SessionInfo.tsx'\nimport { TokenInfo } from './components/TokenInfo.tsx'\nimport { UserMenu } from './components/UserMenu.tsx'\n\nexport function Home() {\n  return (\n    <Layout nav={<UserMenu />}>\n      <ProfileInfo className=\"rounded-md bg-white shadow-md\">\n        <div className=\"p-4\">\n          <TokenInfo />\n          <SessionInfo />\n        </div>\n      </ProfileInfo>\n    </Layout>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/AtmosphereSignInDialog.tsx",
    "content": "import { JSX } from 'react'\nimport { Button } from '../components/Button.tsx'\nimport { AtmosphereSignInForm } from './AtmosphereSignInForm.tsx'\n\nexport type AtmosphereSignInDialogProps = JSX.IntrinsicElements['div'] & {\n  signIn: (input: string) => Promise<void>\n  signUp: (input: string) => Promise<void>\n  disabled?: boolean\n  loading?: boolean\n  signUpUrl?: string\n}\n\nexport function AtmosphereSignInDialog({\n  signIn,\n  signUp,\n  loading,\n  disabled,\n  signUpUrl,\n\n  // div\n  className = '',\n  role = 'dialog',\n  ...props\n}: AtmosphereSignInDialogProps) {\n  return (\n    <div\n      role={role}\n      className={`flex w-[450px] max-w-full flex-col items-stretch space-y-4 rounded-md bg-white p-4 shadow-md ${className}`}\n      {...props}\n    >\n      <h2 className=\"text-center text-2xl font-medium\">\n        Login with the Atmosphere\n      </h2>\n      <p>Enter your handle to continue</p>\n\n      <AtmosphereSignInForm\n        signIn={signIn}\n        loading={loading}\n        disabled={disabled}\n        placeholder=\"@alice.example.com\"\n      />\n\n      {signUpUrl && (\n        <>\n          <Button\n            key=\"signup\"\n            type=\"button\"\n            loading={loading}\n            disabled={disabled}\n            size=\"large\"\n            action={() => signUp(signUpUrl)}\n            name=\"signup-button\"\n          >\n            Sign up with {new URL(signUpUrl).host}\n          </Button>\n          <Button\n            key=\"login\"\n            type=\"button\"\n            loading={loading}\n            disabled={disabled}\n            transparent\n            size=\"large\"\n            action={() => signIn(signUpUrl)}\n            name=\"login-button\"\n          >\n            Login with {new URL(signUpUrl).host}\n          </Button>\n        </>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/AtmosphereSignInForm.tsx",
    "content": "import { FormEvent, JSX, useEffect, useRef, useState } from 'react'\nimport { Button } from './Button.tsx'\n\nexport type AtmosphereSignInFormProps = JSX.IntrinsicElements['form'] & {\n  placeholder?: string\n  autoFocus?: boolean\n  disabled?: boolean\n  loading?: boolean\n  signIn: (input: string) => Promise<void>\n}\n\n/**\n * @returns Nice tailwind css form asking to enter either a handle or the host\n *   to use to login.\n */\nexport function AtmosphereSignInForm({\n  signIn,\n  autoFocus = true,\n  placeholder,\n  loading: forceLoading,\n\n  // form\n  className,\n  onSubmit,\n  ...props\n}: AtmosphereSignInFormProps) {\n  const [value, setValue] = useState('')\n  const [error, setError] = useState<Error | undefined>(undefined)\n  const [loading, setLoading] = useState(false)\n  const formRef = useRef<HTMLFormElement>(null)\n\n  const disabled = props.disabled || forceLoading || loading\n\n  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n    if (disabled) return\n\n    onSubmit?.(event)\n    if (!event.defaultPrevented) {\n      event.preventDefault()\n\n      const invalid = !formRef.current?.reportValidity()\n      if (invalid) return\n\n      try {\n        setLoading(true)\n\n        await signIn(value.replace('@', '').toLowerCase())\n      } catch (err) {\n        console.warn('Error during sign-in:', err)\n        setError(err as Error)\n      } finally {\n        setLoading(false)\n      }\n    }\n  }\n\n  useEffect(() => {\n    setError(undefined)\n  }, [value])\n\n  return (\n    <form\n      {...props}\n      ref={formRef}\n      className={`${className || ''} w-full`}\n      onSubmit={handleSubmit}\n    >\n      <fieldset className=\"rounded-md border border-solid border-slate-200 text-neutral-700 dark:border-slate-700 dark:text-neutral-100\">\n        <div className=\"relative flex flex-wrap items-center justify-stretch space-x-2 p-1\">\n          <input\n            name=\"identifier\"\n            type=\"text\"\n            className=\"relative mx-1 block w-[1px] min-w-0 flex-auto bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100\"\n            placeholder={placeholder}\n            aria-label={placeholder}\n            disabled={disabled}\n            required\n            min={3}\n            max={2048}\n            autoCapitalize=\"off\"\n            autoComplete=\"username\"\n            autoCorrect=\"off\"\n            spellCheck=\"false\"\n            autoFocus={autoFocus}\n            pattern={\n              value.startsWith('http:') || value.startsWith('https:')\n                ? '^https?:\\\\/\\\\/([a-z0-9\\\\-]+\\\\.)*[a-z]{2,}(:\\\\d{1,5})?$'\n                : value.startsWith('did:')\n                  ? '^(did:plc:[a-z2-7]{24}|did:web:[a-z0-9._\\\\-]+)$'\n                  : '^@?[a-zA-Z0-9\\\\-]+(\\\\.[a-zA-Z0-9_\\\\-]+)+$'\n            }\n            title={error ? String(error) : undefined}\n            value={value}\n            onChange={(e) => setValue(e.target.value)}\n          />\n          <Button type=\"submit\" loading={loading || forceLoading} transparent>\n            Login\n          </Button>\n        </div>\n      </fieldset>\n\n      {error ? <div>{String(error)}</div> : null}\n    </form>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/Button.tsx",
    "content": "import { JSX, useState } from 'react'\nimport { Spinner } from './Spinner.tsx'\n\nexport type ButtonProps = {\n  action?: () => unknown | Promise<unknown>\n  loading?: boolean\n  size?: 'small' | 'medium' | 'large'\n  transparent?: boolean\n} & JSX.IntrinsicElements['button']\nexport function Button({\n  action,\n  loading = false,\n  size = 'medium',\n  transparent = false,\n\n  // button\n  children,\n  onClick,\n  disabled,\n  className = '',\n  ...props\n}: ButtonProps) {\n  const actionable =\n    props.type === 'submit' || onClick != null || action != null\n\n  const [pendingActions, setPendingActions] = useState(0)\n\n  const doAction = action\n    ? async () => {\n        setPendingActions((p) => p + 1)\n        try {\n          await action()\n        } catch (error) {\n          console.error('Error in button action:', error)\n        } finally {\n          setPendingActions((p) => p - 1)\n        }\n      }\n    : null\n\n  const isLoading = loading || pendingActions > 0\n  const sizeClass =\n    size === 'small'\n      ? 'px-2 py-[2px] text-sm'\n      : size === 'large'\n        ? 'px-3 py-2 text-lg'\n        : 'px-2 py-1'\n\n  return (\n    <button\n      {...props}\n      onClick={(event) => {\n        onClick?.(event)\n        if (doAction != null && !event.defaultPrevented) {\n          event.preventDefault()\n          void doAction()\n        }\n      }}\n      tabIndex={props?.tabIndex ?? (actionable ? 0 : -1)}\n      disabled={disabled || isLoading}\n      className={[\n        'relative overflow-hidden',\n        'inline-block rounded-md',\n        'focus:outline-none focus:ring-2 focus:ring-offset-2',\n        'transition duration-300 ease-in-out',\n        transparent\n          ? 'bg-transparent text-purple-600 hover:bg-purple-100 focus:ring-purple-500'\n          : 'bg-purple-600 text-white hover:bg-purple-700 focus:ring-purple-800',\n        sizeClass,\n        className,\n      ].join(' ')}\n    >\n      {isLoading && (\n        <span className=\"absolute inset-0 z-10 flex items-center justify-center\">\n          <Spinner size={size} />\n        </span>\n      )}\n\n      <span className={isLoading ? 'invisible' : 'visible'}>{children}</span>\n    </button>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/ButtonDropdown.tsx",
    "content": "import {\n  JSX,\n  MouseEventHandler,\n  ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from 'react'\nimport { useClickOutside } from '../lib/use-click-outside.ts'\nimport { useEscapeKey } from '../lib/use-escape-key.ts'\nimport { useRandomString } from '../lib/use-random-string.ts'\nimport { Button, ButtonProps } from './Button.tsx'\n\nexport type Item = {\n  label?: ReactNode\n  onClick?: MouseEventHandler<HTMLButtonElement>\n  items?: readonly Item[]\n}\n\nexport type DropdownProps = ButtonProps & {\n  menu: readonly Item[]\n}\nexport function ButtonDropdown({\n  menu,\n  children,\n  className = '',\n  ...buttonProps\n}: DropdownProps) {\n  const buttonId = useRandomString({ prefix: 'dropdown-button-' })\n  const dropdownId = useRandomString({ prefix: 'dropdown-menu-' })\n  const rootRef = useRef<HTMLDivElement>(null)\n  const [open, setOpen] = useState(false)\n  const close = useCallback(() => setOpen(false), [])\n\n  useEscapeKey(close)\n  useClickOutside(rootRef, close)\n\n  const id = buttonProps.id || buttonId\n\n  return (\n    <div ref={rootRef} className=\"relative inline-block\">\n      <Button\n        {...buttonProps}\n        id={id}\n        key=\"button\"\n        className={['relative z-10', className].join(' ')}\n        onClick={() => setOpen((prev) => !prev)}\n        aria-haspopup=\"true\"\n        aria-expanded={open}\n        aria-controls={dropdownId}\n      >\n        {children}\n      </Button>\n\n      {open && (\n        <div\n          key=\"menu\"\n          id={dropdownId}\n          className=\"absolute right-0 z-50 mt-2 min-w-36 origin-top-right overflow-hidden rounded-md bg-white py-1 shadow-lg ring-1 ring-gray-300 focus:outline-none\"\n          onClick={(event) => {\n            if (!event.defaultPrevented) setOpen(false)\n          }}\n          role=\"menu\"\n          aria-labelledby={id}\n        >\n          {menu.map((item, index) => (\n            <Item\n              key={`item-${index}`}\n              item={item}\n              className={\n                index > 0 ? 'mt-1 border-t border-gray-200 pt-1' : undefined\n              }\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport type ItemProps = {\n  item: Item\n} & JSX.IntrinsicElements['div']\n\nfunction Item({ item: { label, onClick, items }, ...props }: ItemProps) {\n  return (\n    <div {...props}>\n      {label && (\n        <button\n          key=\"label\"\n          type=\"button\"\n          role=\"menuitem\"\n          className={[\n            'flex items-center gap-2',\n            'block w-full px-4 py-2 text-left text-sm text-gray-700 focus:outline-none',\n            onClick ? 'hover:bg-gray-100 focus:bg-gray-100' : 'cursor-default',\n          ].join(' ')}\n          onClick={onClick}\n        >\n          {label}\n        </button>\n      )}\n\n      {items?.map((item, index) => <Item key={index} item={item} />)}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/Icons.tsx",
    "content": "import { type FunctionComponent, type JSX, forwardRef } from 'react'\n\nexport type IconProps = Omit<\n  JSX.IntrinsicElements['svg'],\n  'viewBox' | 'children' | 'xmlns' | 'title'\n> & {\n  /**\n   * The title of the icon, used for accessibility.\n   */\n  title?: string\n}\n\nconst makeSvgComponent = (path: string, displayName: string) => {\n  const SvgComponent: FunctionComponent<IconProps> = forwardRef(\n    ({ title, ...props }, ref) => (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        {...props}\n        ref={ref}\n        aria-hidden={!title}\n      >\n        {title && <title>{title}</title>}\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d={path}\n        ></path>\n      </svg>\n    ),\n  )\n  SvgComponent.displayName = displayName\n  return SvgComponent\n}\n\nexport const AccountIcon = makeSvgComponent(\n  'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z',\n  'AccountIcon',\n)\n\nexport const ClipboardIcon = makeSvgComponent(\n  'M8.17 4A3.001 3.001 0 0 1 11 2h2c1.306 0 2.418.835 2.83 2H17a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h1.17ZM8 6H7a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V6Zm6 0V5a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v1h4Z',\n  'ClipboardIcon',\n)\nexport const SquareArrowTopRightIcon = makeSvgComponent(\n  'M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-7.293 7.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM3 6a1 1 0 0 1 1-1h5a1 1 0 0 1 0 2H5v12h12v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6Z',\n  'SquareArrowTopRightIcon',\n)\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/JsonQueryResult.tsx",
    "content": "import { UseQueryResult } from '@tanstack/react-query'\nimport ReactJson from 'react-json-view'\n\nexport function JsonQueryResult<T>({\n  result,\n  transform,\n}: {\n  result: UseQueryResult<T>\n  transform?: (data: T) => object\n}) {\n  return (\n    <div className=\"overflow-auto\">\n      {result.data !== undefined ? (\n        result.data === null ? (\n          'null'\n        ) : (\n          <ReactJson\n            src={transform ? transform(result.data) : result.data}\n            indentWidth={2}\n            displayDataTypes={false}\n            name={false}\n            quotesOnKeys={false}\n            displayObjectSize={false}\n            enableClipboard={false}\n            collapsed\n          />\n        )\n      ) : result.isLoading ? (\n        <p>Loading...</p>\n      ) : result.isError ? (\n        <p>Error: {String(result.error)}</p>\n      ) : (\n        <p>Error: no-data</p>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/Layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nexport function Layout({\n  children,\n  nav,\n}: {\n  children: ReactNode\n  nav?: ReactNode\n}) {\n  return (\n    <div className=\"container mx-auto flex min-h-screen max-w-3xl flex-col p-4\">\n      <nav className=\"mb-8 flex items-center\">\n        <div className=\"flex-1\" />\n        {nav}\n      </nav>\n      <main className=\"flex flex-1 flex-col items-stretch space-y-4\">\n        {children}\n      </main>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/ProfileInfo.tsx",
    "content": "import { JSX, useMemo } from 'react'\nimport { l } from '@atproto/lex'\nimport { app, com } from '../lexicons.ts'\nimport { useBlobUrl } from '../lib/use-blob-url.ts'\nimport { useBskyClient } from '../providers/BskyClientProvider.tsx'\nimport { useLexQuery } from '../queries/use-lex-query.ts'\nimport { useLexRecord } from '../queries/use-lex-record.ts'\n\nexport type ProfileInfoProps = JSX.IntrinsicElements['div']\n\nexport function ProfileInfo({\n  className = '',\n  children,\n  ...props\n}: ProfileInfoProps) {\n  // @NOTE for more detailed profile info, we should be using the\n  // app.bsky.actor.getProfile query. This example uses the record for\n  // demonstration purposes.\n  const profileQuery = useLexRecord(app.bsky.actor.profile)\n\n  const avatarUrl = useBlobRefUrl(profileQuery.data?.value?.avatar)\n  const bannerUrl = useBlobRefUrl(profileQuery.data?.value?.banner)\n  const displayName = profileQuery.data?.value?.displayName\n  const description = profileQuery.data?.value?.description\n  const pronouns = profileQuery.data?.value?.pronouns\n\n  return (\n    <div className={`overflow-hidden ${className}`} {...props}>\n      {bannerUrl && (\n        <div className=\"h-32 w-full overflow-hidden\">\n          <img\n            src={bannerUrl}\n            alt=\"Banner\"\n            className=\"h-full w-full object-cover\"\n          />\n        </div>\n      )}\n      {(avatarUrl || displayName || description) && (\n        <div className=\"relative p-4\">\n          {avatarUrl && (\n            <img\n              src={avatarUrl}\n              alt={displayName || 'Avatar'}\n              className=\"absolute -top-12 left-4 h-24 w-24 rounded-full border-4 border-white bg-white object-cover\"\n            />\n          )}\n          <div className=\"ml-28\">\n            <h2 className=\"text-2xl font-bold\">{displayName}</h2>\n            {pronouns && <p className=\"text-sm text-gray-500\">{pronouns}</p>}\n            {description && <p className=\"mt-2\">{description}</p>}\n          </div>\n        </div>\n      )}\n\n      {children}\n    </div>\n  )\n}\n\nfunction useBlobRefUrl(ref: l.BlobRef | null | undefined) {\n  const { did } = useBskyClient()\n  const blobQuery = useLexQuery(\n    com.atproto.sync.getBlob,\n    did && ref ? { did, cid: ref.ref.toString() } : false,\n  )\n  const blob = useMemo(() => {\n    return blobQuery.data\n      ? new Blob([blobQuery.data.body], { type: blobQuery.data.encoding })\n      : null\n  }, [blobQuery.data])\n  const url = useBlobUrl(blob)\n  return url\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/SessionInfo.tsx",
    "content": "import { com } from '../lexicons.ts'\nimport { useLexQuery } from '../queries/use-lex-query.ts'\nimport { Button } from './Button.tsx'\nimport { JsonQueryResult } from './JsonQueryResult.tsx'\n\nexport function SessionInfo() {\n  const result = useLexQuery(com.atproto.server.getSession)\n\n  return (\n    <div>\n      <h2>\n        getSession\n        <Button\n          action={async () => result.refetch({ throwOnError: false })}\n          className=\"ml-1\"\n          size=\"small\"\n          transparent\n        >\n          refresh\n        </Button>\n      </h2>\n      <JsonQueryResult result={result} transform={(data) => data.body} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/Spinner.tsx",
    "content": "import { JSX } from 'react'\n\ntype SpinnerProps = {\n  size?: 'small' | 'medium' | 'large'\n} & JSX.IntrinsicElements['div']\n\nexport function Spinner({\n  size = 'medium',\n  className = '',\n  ...props\n}: SpinnerProps) {\n  const sizeClass =\n    size === 'small' ? 'h-4 w-4' : size === 'large' ? 'h-6 w-6' : 'h-5 w-5'\n  return (\n    <div {...props} className={`flex items-center justify-center ${className}`}>\n      <div className={`relative inline-block ${sizeClass}`} role=\"status\">\n        <div\n          className={`animate-spin rounded-full border-2 border-solid border-current border-t-transparent ${sizeClass}`}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/TokenInfo.tsx",
    "content": "import { useGetTokenInfoQuery } from '../queries/use-get-token-info-query.ts'\nimport { Button } from './Button.tsx'\nimport { JsonQueryResult } from './JsonQueryResult.tsx'\n\nexport function TokenInfo() {\n  const result = useGetTokenInfoQuery()\n\n  return (\n    <div>\n      <h2>\n        Token info\n        <Button\n          action={async () => result.refetch({ throwOnError: false })}\n          className=\"ml-1\"\n          size=\"small\"\n          transparent\n        >\n          refresh\n        </Button>\n      </h2>\n      <JsonQueryResult result={result} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/components/UserMenu.tsx",
    "content": "import { app, com } from '../lexicons.ts'\nimport {\n  useOAuthContext,\n  useOAuthSession,\n} from '../providers/OAuthProvider.tsx'\nimport { useLexQuery } from '../queries/use-lex-query.ts'\nimport { useLexRecord } from '../queries/use-lex-record.ts'\nimport { ButtonDropdown } from './ButtonDropdown.tsx'\nimport { ClipboardIcon, SquareArrowTopRightIcon } from './Icons.tsx'\n\nexport function UserMenu() {\n  const { signOut } = useOAuthContext()\n  const session = useOAuthSession()\n\n  const profileQuery = useLexRecord(app.bsky.actor.profile)\n  const sessionQuery = useLexQuery(com.atproto.server.getSession)\n\n  const displayName = profileQuery.data?.value?.displayName\n  const handle = sessionQuery.data?.body.handle\n\n  return (\n    <ButtonDropdown\n      transparent\n      aria-label=\"User menu\"\n      menu={[\n        {\n          label: handle && <b className=\"flex-1\">{handle}</b>,\n          onClick: () => {\n            if (handle) navigator.clipboard.writeText(handle)\n          },\n          items: [\n            {\n              label: (\n                <>\n                  <span className=\"flex-1\">{session.did}</span>\n                  <ClipboardIcon className=\"w-4\" />\n                </>\n              ),\n              onClick: () => {\n                navigator.clipboard.writeText(session.did)\n              },\n            },\n            {\n              label: (\n                <>\n                  <span className=\"flex-1\">Profile</span>\n                  <SquareArrowTopRightIcon className=\"w-4\" />\n                </>\n              ),\n              onClick: () => {\n                window.open(`https://bsky.app/profile/${session.did}`, '_blank')\n              },\n            },\n          ],\n        },\n        {\n          label: 'Sign out',\n          onClick: signOut,\n        },\n      ]}\n    >\n      {displayName ||\n        (profileQuery.isLoading\n          ? null\n          : handle || (sessionQuery.isLoading ? null : handle || session.did))}\n    </ButtonDropdown>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/constants.ts",
    "content": "const { searchParams } = new URL(window.location.href)\nconst getParam = <T extends string | undefined>(\n  name: string,\n  defaultValue: T,\n): string | T => searchParams.get(name) ?? defaultValue\n\n// Inserted during build\ndeclare const process: { env: { NODE_ENV: string } }\n\nexport const ENV_DEFAULT = process.env.NODE_ENV\nexport const ENV = getParam('env', ENV_DEFAULT)\n\nexport const PLC_DIRECTORY_URL_DEFAULT =\n  ENV === 'development' ? 'http://localhost:2582' : undefined\nexport const HANDLE_RESOLVER_URL_DEFAULT =\n  ENV === 'development' ? 'http://localhost:2584' : 'https://bsky.social'\nexport const SIGN_UP_URL_DEFAULT =\n  ENV === 'development' ? 'http://localhost:2583' : 'https://bsky.social'\nexport const BSKY_API_URL_DEFAULT =\n  ENV === 'development' ? 'http://localhost:2584' : 'https://api.bsky.app'\nexport const BSKY_API_DID_DEFAULT =\n  ENV === 'development' ? 'did:example:invalid' : 'did:web:api.bsky.app'\n\nexport const BSKY_API_URL = getParam('bsky_api_url', BSKY_API_URL_DEFAULT)\nexport const BSKY_API_DID = getParam('bsky_api_did', BSKY_API_DID_DEFAULT)\n\nexport const PLC_DIRECTORY_URL = getParam(\n  'plc_directory_url',\n  PLC_DIRECTORY_URL_DEFAULT,\n)\nexport const HANDLE_RESOLVER_URL = getParam(\n  'handle_resolver',\n  HANDLE_RESOLVER_URL_DEFAULT,\n)\nexport const SIGN_UP_URL = getParam('sign_up_url', SIGN_UP_URL_DEFAULT)\n\nexport const OAUTH_SCOPE_DEFAULT: string =\n  ENV === 'development'\n    ? [\n        'account:email',\n        'account:status',\n        'identity:*',\n        'blob:*/*',\n        'repo:*',\n        `rpc:*?aud=${BSKY_API_DID}#bsky_appview`,\n        `include:com.example.calendar.basePermissions?aud=${BSKY_API_DID}#calendar_service`,\n      ].join(' ')\n    : ENV === 'production'\n      ? [\n          'account:email',\n          'account:status',\n          'blob:*/*',\n          'repo:*',\n          `rpc:*?aud=${BSKY_API_DID}#bsky_appview`,\n          `include:directory.lexicon.calendar.basePermissions?aud=${BSKY_API_DID}#calendar_service`,\n        ].join(' ')\n      : [\n          'account:email',\n          'account:status',\n          'blob:*/*',\n          'repo:*',\n          `rpc:*?aud=${BSKY_API_DID}#bsky_appview`,\n        ].join(' ')\nexport const OAUTH_SCOPE: string =\n  searchParams.get('scope') ?? OAUTH_SCOPE_DEFAULT\n\n// This app is dynamically configured via query parameters. The canonical URL is\n// always 127.0.0.1 with the relevant params set.\nexport const LOOPBACK_CANONICAL_LOCATION = Object.assign(\n  new URL(window.location.origin),\n  {\n    protocol: 'http:',\n    hostname: '127.0.0.1',\n    search: new URLSearchParams({\n      ...(ENV !== ENV_DEFAULT && { env: ENV }),\n      ...(PLC_DIRECTORY_URL !== PLC_DIRECTORY_URL_DEFAULT && {\n        plc_directory_url: PLC_DIRECTORY_URL,\n      }),\n      ...(HANDLE_RESOLVER_URL !== HANDLE_RESOLVER_URL_DEFAULT && {\n        handle_resolver: HANDLE_RESOLVER_URL,\n      }),\n      ...(SIGN_UP_URL !== SIGN_UP_URL_DEFAULT && { sign_up_url: SIGN_UP_URL }),\n      ...(OAUTH_SCOPE !== OAUTH_SCOPE_DEFAULT && { scope: OAUTH_SCOPE }),\n    }).toString(),\n  },\n).href as `http://127.0.0.1/${string}`\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/index.css",
    "content": "@import 'tailwindcss';\n\n/* Styles applies on be <body> tag by the oauth provider */\n@source inline(\"min-h-screen\");\n@source inline(\"bg-slate-100\");\n@source inline(\"dark:bg-slate-800\");\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/index.tsx",
    "content": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { App } from './App.tsx'\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n)\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/lexicons.ts",
    "content": "export * as com from './lexicons/com.js'\nexport * as app from './lexicons/app.js'\nexport * as chat from './lexicons/chat.js'\nexport * as tools from './lexicons/tools.js'\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/lib/use-abortable-effect.ts",
    "content": "import { DependencyList, useEffect } from 'react'\n\nexport function useAbortableEffect(\n  effect: (signal: AbortSignal) => void | (() => void),\n  deps: DependencyList = [],\n): void {\n  useEffect(() => {\n    const abortController = new AbortController()\n    const cleanup = effect(abortController.signal)\n    return () => {\n      abortController.abort()\n      cleanup?.()\n    }\n  }, deps)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/lib/use-blob-url.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport function useBlobUrl(data: Blob | null): string | null {\n  const [url, setUrl] = useState<string | null>(null)\n\n  useEffect(() => {\n    if (!data) {\n      setUrl(null)\n      return\n    }\n\n    const blobUrl = URL.createObjectURL(data)\n    setUrl(blobUrl)\n\n    return () => {\n      // Clear the URL after some time to prevent flickering if the blob is\n      // recreated quickly (e.g., avatar updates), or during animations.\n      setTimeout(() => {\n        URL.revokeObjectURL(blobUrl)\n      }, 1000)\n    }\n  }, [data])\n\n  return url\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/lib/use-click-outside.ts",
    "content": "import { RefObject, useEffect } from 'react'\n\nexport function useClickOutside(\n  ref: RefObject<HTMLElement | null>,\n  handler: (event: MouseEvent) => void,\n) {\n  useEffect(() => {\n    const listener = (event: MouseEvent) => {\n      if (\n        !event.defaultPrevented &&\n        ref.current &&\n        !ref.current.contains(event.target as Node)\n      ) {\n        handler(event)\n      }\n    }\n\n    document.addEventListener('mousedown', listener)\n    return () => {\n      document.removeEventListener('mousedown', listener)\n    }\n  }, [ref, handler])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/lib/use-escape-key.ts",
    "content": "import { useEffect } from 'react'\n\nexport function useEscapeKey(callback: () => void) {\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (event.key === 'Escape') {\n      event.preventDefault()\n      callback()\n    }\n  }\n\n  useEffect(() => {\n    window.addEventListener('keydown', handleKeyDown)\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [callback])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/lib/use-random-string.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nexport const LOWER = UPPER.toLowerCase() as Lowercase<typeof UPPER>\nexport const DIGITS = '0123456789'\n\nexport const ALPHANUMERIC = `${UPPER}${LOWER}${DIGITS}` as const\n\nexport type UseRandomStringOptions = BuildRandomStringOptions & {\n  prefix?: string\n  suffix?: string\n}\n\nexport function useRandomString(options?: UseRandomStringOptions) {\n  const [state, setState] = useState(() => buildRandomString(options))\n  useEffect(() => {\n    setState(buildRandomString(options))\n  }, [options?.length, options?.alphabet])\n\n  return `${options?.prefix ?? ''}${state}${options?.suffix ?? ''}`\n}\n\ntype BuildRandomStringOptions = {\n  length?: number\n  alphabet?: string\n}\n\nfunction buildRandomString({\n  length = 16,\n  alphabet = ALPHANUMERIC,\n}: BuildRandomStringOptions = {}) {\n  return Array.from({ length }, () => getRandomCharFrom(alphabet)).join('')\n}\n\nfunction getRandomCharFrom(alphabet: string) {\n  return alphabet.charAt((Math.random() * alphabet.length) | 0)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/oauthClient.ts",
    "content": "import {\n  BrowserOAuthClient,\n  OAuthSession,\n  buildAtprotoLoopbackClientMetadata,\n} from '@atproto/oauth-client-browser'\nimport {\n  ENV,\n  HANDLE_RESOLVER_URL,\n  LOOPBACK_CANONICAL_LOCATION,\n  OAUTH_SCOPE,\n  PLC_DIRECTORY_URL,\n} from './constants.ts'\n\nexport const clientMetadata = buildAtprotoLoopbackClientMetadata({\n  scope: Array.from(\n    // Strip duplicate values from env\n    new Set([\n      // Always required\n      'atproto',\n      // Required by this app to setup labelers\n      'rpc:app.bsky.actor.getPreferences?aud=*',\n      // Additional scopes from env\n      ...OAUTH_SCOPE.split(' ').filter(Boolean),\n    ]),\n  ).join(' '),\n  redirect_uris: [LOOPBACK_CANONICAL_LOCATION],\n})\n\nexport const oauthEvents = new EventTarget() as EventTarget & {\n  addEventListener(\n    type: 'deleted',\n    listener: (event: CustomEvent<{ sub: string; cause: string }>) => void,\n    options?: boolean | AddEventListenerOptions,\n  ): void\n  addEventListener(\n    type: 'updated',\n    listener: (\n      event: CustomEvent<{ sub: string; session: OAuthSession }>,\n    ) => void,\n    options?: boolean | AddEventListenerOptions,\n  ): void\n}\n\nexport const oauthClient = new BrowserOAuthClient({\n  allowHttp: ENV === 'development' || ENV === 'test',\n  handleResolver: HANDLE_RESOLVER_URL,\n  plcDirectoryUrl: PLC_DIRECTORY_URL,\n  clientMetadata,\n  // Since the client is static, let's forward the hooks using a shared event\n  // target (oauthEvents) so that they can be consumed by other parts of the\n  // app.\n  onDelete: (sub, cause) => {\n    console.debug('OAuth session deleted:', sub, cause)\n    oauthEvents.dispatchEvent(\n      new CustomEvent('deleted', { detail: { sub, cause } }),\n    )\n  },\n  onUpdate: (sub, session) => {\n    console.debug('OAuth session refreshed:', sub)\n    oauthEvents.dispatchEvent(\n      new CustomEvent('updated', { detail: { sub, session } }),\n    )\n  },\n})\n\n// We use \"false\" as refresh parameter to the oauthClient's init method in order\n// restore the previously loaded session without making a network request to\n// refresh tokens if the session requires a refresh. This allows the app to work\n// off-line, and also makes the initial loading faster (by optimistically\n// restoring the session in the initPromise).\nexport const initPromise = oauthClient.init(false).then(\n  async (result) => {\n    // Only trigger a (background) token refresh if we are not back from an\n    // authorization flow (state is undefined).\n    if (result && result.state === undefined) {\n      void result.session.getTokenInfo(true)\n    }\n    return result\n  },\n  (err) => {\n    console.warn('Failed to initialize OAuth client:', err)\n  },\n)\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/providers/AuthenticationProvider.tsx",
    "content": "import {\n  ReactNode,\n  createContext,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from 'react'\nimport { Client, DidString } from '@atproto/lex'\nimport { AtmosphereSignInDialog } from '../components/AtmosphereSignInDialog.tsx'\nimport { Layout } from '../components/Layout.tsx'\nimport { Spinner } from '../components/Spinner.tsx'\nimport { SIGN_UP_URL } from '../constants.ts'\nimport * as app from '../lexicons/app.ts'\nimport { useAbortableEffect } from '../lib/use-abortable-effect.ts'\nimport { OAuthProvider, useOAuthContext } from './OAuthProvider.tsx'\n\nexport type AuthenticatedClient = Client & { did: DidString }\nexport type AuthenticationContextType = {\n  client: AuthenticatedClient\n}\n\nexport const AuthenticationContext =\n  createContext<AuthenticationContextType | null>(null)\nAuthenticationContext.displayName = 'AuthenticationContext'\n\nexport function AuthenticationProvider({ children }: { children?: ReactNode }) {\n  return (\n    <OAuthProvider>\n      <AuthenticationProviderInternal>\n        {children}\n      </AuthenticationProviderInternal>\n    </OAuthProvider>\n  )\n}\n\nfunction AuthenticationProviderInternal({\n  children,\n}: {\n  children?: ReactNode\n}) {\n  const { isLoading, session, signIn, signUp } = useOAuthContext()\n  const [initialized, setInitialized] = useState(false)\n  const [configuredClient, setConfiguredClient] =\n    useState<AuthenticatedClient | null>(null)\n\n  // As soon as initial loading/configuration is done, we are \"initialized\"\n  const isConfiguring = session != null && configuredClient == null\n  useEffect(() => {\n    if (!isLoading && !isConfiguring) setInitialized(true)\n  }, [isLoading, isConfiguring])\n\n  const client = useMemo(\n    () => (session ? new Client(session) : null),\n    [session],\n  )\n\n  useAbortableEffect(\n    (signal) => {\n      if (client) {\n        void configureClient(client, signal).then(\n          (client) => {\n            if (!signal.aborted) setConfiguredClient(client)\n          },\n          () => {\n            // Most likely aborted, ignore\n          },\n        )\n      } else {\n        setConfiguredClient(null)\n      }\n    },\n    [client],\n  )\n\n  const valueClient =\n    session && client && configuredClient === client ? configuredClient : null\n  const value = useMemo<AuthenticationContextType | null>(() => {\n    if (valueClient) return { client: valueClient }\n    return null\n  }, [valueClient])\n\n  if (value) {\n    return (\n      <AuthenticationContext.Provider value={value}>\n        {children}\n      </AuthenticationContext.Provider>\n    )\n  }\n\n  return (\n    <Layout>\n      <div className=\"flex flex-grow flex-col items-center justify-center\">\n        {initialized ? (\n          <AtmosphereSignInDialog\n            signUpUrl={SIGN_UP_URL}\n            loading={isLoading || isConfiguring}\n            signIn={signIn}\n            signUp={signUp}\n          />\n        ) : (\n          <Spinner />\n        )}\n      </div>\n    </Layout>\n  )\n}\n\nexport function useAuthenticationContext(\n  debugName = 'useAuthenticationContext',\n) {\n  const context = useContext(AuthenticationContext)\n  if (context) return context\n\n  throw new Error(\n    `${debugName} must be used within a ${AuthenticationContext.displayName}`,\n  )\n}\n\nasync function configureClient(\n  client: Client,\n  signal: AbortSignal,\n): Promise<AuthenticatedClient> {\n  const { preferences } = await getPreferences(client, signal)\n\n  const labelers = preferences\n    .findLast((v) => app.bsky.actor.defs.labelersPref.matches(v))\n    ?.labelers.map((l) => l.did)\n\n  client.setLabelers(labelers)\n  client.assertAuthenticated()\n\n  console.info('Configured client with labelers:', labelers)\n\n  return client\n}\n\nasync function getPreferences(client: Client, signal: AbortSignal) {\n  for (let attempt = 0; ; attempt++) {\n    try {\n      return await client.call(app.bsky.actor.getPreferences, {}, { signal })\n    } catch (err) {\n      // TODO handle 403 ?\n      console.warn('Failed to get preferences, retrying...', err)\n      signal.throwIfAborted()\n      await new Promise((resolve) =>\n        setTimeout(resolve, Math.min(200 * 1.5 ** attempt, 5000)),\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/providers/BskyClientProvider.tsx",
    "content": "import { createContext, useContext, useMemo } from 'react'\nimport { Client } from '@atproto/lex'\nimport { asDid } from '@atproto/oauth-client-browser'\nimport { BSKY_API_DID, BSKY_API_URL } from '../constants.ts'\nimport { useAuthenticationContext } from './AuthenticationProvider.tsx'\n\nconst BSKY_APPVIEW_DID_SERVICE = `${asDid(BSKY_API_DID)}#bsky_appview` as const\n\nconst unauthenticatedClient = new Client(BSKY_API_URL)\n\nconst BskyClientContext = createContext<Client>(unauthenticatedClient)\nBskyClientContext.displayName = 'BskyClientContext'\n\nexport function BskyClientProvider({\n  children,\n}: {\n  children?: React.ReactNode\n}) {\n  // @NOTE We prefer using an AuthenticationContext \"client\" instead of the\n  // OAuthProvider \"session\" as agent to ensure that any configuration (e.g.\n  // labelers, etc.) on the client is preserved and applied to the BskyClient\n  // context value as well.\n  const agent = useAuthenticationContext().client\n\n  const value = useMemo(() => {\n    return agent\n      ? new Client(agent, { service: BSKY_APPVIEW_DID_SERVICE })\n      : unauthenticatedClient\n  }, [agent])\n\n  return (\n    <BskyClientContext.Provider value={value}>\n      {children}\n    </BskyClientContext.Provider>\n  )\n}\n\nexport function useBskyClient() {\n  return useContext(BskyClientContext)\n}\n\nexport function useUnauthenticatedBskyClient() {\n  return unauthenticatedClient\n}\n\n/**\n * Can only be used from within an authenticated context\n * ({@link AuthenticationContext} or {@link OAuthContext}).\n */\nexport function useAuthenticatedBskyClient() {\n  const client: Client = useBskyClient()\n  client.assertAuthenticated()\n  return client\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/providers/OAuthProvider.tsx",
    "content": "import {\n  PropsWithChildren,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useState,\n} from 'react'\nimport { type OAuthSession } from '@atproto/oauth-client-browser'\nimport { useAbortableEffect } from '../lib/use-abortable-effect'\nimport { initPromise, oauthClient, oauthEvents } from '../oauthClient'\n\nexport type SignInFunction = (\n  input: string,\n  options?: { display?: 'popup' },\n) => Promise<void>\nexport type SignUpFunction = (\n  input: string,\n  options?: { display?: 'popup' },\n) => Promise<void>\nexport type SignOutFunction = () => Promise<void>\n\nexport const OAuthContext = createContext<null | {\n  session: null | OAuthSession\n  isLoading: boolean\n  isSignedIn: boolean\n  signIn: SignInFunction\n  signUp: SignUpFunction\n  signOut: SignOutFunction\n}>(null)\n\nexport function OAuthProvider({ children }: PropsWithChildren) {\n  const [initialized, setInitialized] = useState(false)\n  const [loading, setLoading] = useState(false)\n  const [session, setSession] = useState<null | OAuthSession>(null)\n\n  useAbortableEffect(\n    (signal) => {\n      setInitialized(false)\n      setSession(null)\n\n      void initPromise\n        .then(async (result) => {\n          if (signal.aborted) return\n\n          if (result) setSession(result.session)\n        })\n        .finally(() => {\n          if (signal.aborted) return\n\n          setInitialized(true)\n        })\n    },\n    [initPromise],\n  )\n\n  // Keep tabs in sync by listening to the oauth client's events and updating\n  // the session state accordingly. The deletion part is needed because the\n  // oauth client internal data is shared across tabs, so if a session is\n  // deleted in one tab, the other tabs should reflect that change as well. The\n  // update part is optional.\n  useAbortableEffect(\n    (signal) => {\n      // If the session is removed from another tab, we should update the state\n      // in this tab as well.\n      if (session) {\n        oauthEvents.addEventListener(\n          'deleted',\n          (evt) => {\n            if (evt.detail.sub === session.sub) setSession(null)\n          },\n          { signal },\n        )\n      } else {\n        // If we don't have a session, and one is refreshed in another tab,\n        // let's load it in the current tab as well.\n        oauthEvents.addEventListener(\n          'updated',\n          (evt) => {\n            void oauthClient.restore(evt.detail.sub, false).then((session) => {\n              if (!signal.aborted) setSession(session)\n            })\n          },\n          { signal },\n        )\n      }\n    },\n    [oauthEvents, session],\n  )\n\n  // When initializing the AuthProvider, we used \"false\" as restore's refresh\n  // argument so that the app can work off-line. The following effect will\n  // ensure that the session is pro actively refreshed whenever the app gets\n  // back online.\n  useEffect(() => {\n    if (!session) return\n\n    // @NOTE If the refresh token was revoked, the \"deleted\" event will be\n    // triggered on the client, causing the previous effect to clear the session\n    const check = () => {\n      void session.getTokenInfo(true).catch((err) => {\n        console.warn('Failed to refresh OAuth session token info:', err)\n      })\n    }\n\n    const interval = setInterval(check, 10 * 60e3)\n    return () => clearInterval(interval)\n  }, [session])\n\n  const signIn = useCallback<SignInFunction>(\n    async (input, options) => {\n      setLoading(true)\n\n      try {\n        const session = await oauthClient\n          .restore(input, true)\n          .catch(async (_err) => oauthClient.signIn(input, options))\n\n        setSession(session)\n      } finally {\n        setLoading(false)\n      }\n    },\n    [oauthClient],\n  )\n\n  const signOut = useCallback<SignOutFunction>(async () => {\n    if (session) {\n      setSession(null)\n      setLoading(true)\n      try {\n        await session.signOut()\n      } finally {\n        setLoading(false)\n      }\n    }\n  }, [session])\n\n  const signUp = useCallback<SignUpFunction>(\n    async (input, options) => {\n      setLoading(true)\n      try {\n        const session = await oauthClient.signIn(input, {\n          ...options,\n          prompt: 'create',\n        })\n\n        setSession(session)\n      } finally {\n        setLoading(false)\n      }\n    },\n    [oauthClient],\n  )\n\n  return (\n    <OAuthContext.Provider\n      value={{\n        session,\n\n        isLoading: !initialized || loading,\n        isSignedIn: !!session,\n\n        signIn,\n        signUp,\n        signOut,\n      }}\n    >\n      {children}\n    </OAuthContext.Provider>\n  )\n}\n\nexport function useOAuthContext() {\n  const value = useContext(OAuthContext)\n  if (!value) throw new Error('useOAuth must be used within an OAuthProvider')\n  return value\n}\n\nexport function useOAuthSession(): OAuthSession {\n  const { session } = useOAuthContext()\n  if (!session) throw new Error('User is not logged in')\n  return session\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/queries/use-get-token-info-query.ts",
    "content": "import { Query, useQuery } from '@tanstack/react-query'\nimport { TokenInfo } from '@atproto/oauth-client-browser'\nimport { useOAuthSession } from '../providers/OAuthProvider.tsx'\n\nexport function useGetTokenInfoQuery() {\n  const session = useOAuthSession()\n  return useQuery({\n    queryKey: ['tokenInfo', session.did] as const,\n    staleTime(query: Query<TokenInfo>) {\n      const exp = query.state.data?.expiresAt\n      if (!exp) return 0\n      return exp.getTime() - Date.now()\n    },\n    refetchOnWindowFocus: true,\n    queryFn: async (context): Promise<TokenInfo> => {\n      const query = context.client\n        .getQueryCache()\n        .find<TokenInfo>({ queryKey: context.queryKey, exact: true })\n      // The OAuthProvider will force a refresh of the token in the background\n      // when initialized, so there is no point in forcing a refresh here during\n      // initial load (ie. if there is no cache data yet)\n      const forceRefresh = query?.state.data == null ? 'auto' : true\n      return session.getTokenInfo(forceRefresh)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/queries/use-lex-query.ts",
    "content": "import { UseQueryResult, useQuery } from '@tanstack/react-query'\nimport {\n  Query,\n  Restricted,\n  XrpcFailure,\n  XrpcRequestParams,\n  XrpcResponse,\n  getMain,\n} from '@atproto/lex'\nimport { useBskyClient } from '../providers/BskyClientProvider.tsx'\n\nexport function useLexQuery<S extends Query>(\n  ns: NonNullable<unknown> extends XrpcRequestParams<S>\n    ? S | { main: S }\n    : Restricted<'This XRPC method requires a \"params\" argument'>,\n): UseQueryResult<XrpcResponse<S>, XrpcFailure<S>>\nexport function useLexQuery<\n  S extends Query,\n  P extends false | XrpcRequestParams<S>,\n>(\n  ns: S | { main: S },\n  params: P,\n): UseQueryResult<P extends false ? null : XrpcResponse<S>, XrpcFailure<S>>\nexport function useLexQuery<S extends Query>(\n  ns: S | { main: S },\n  params: false | XrpcRequestParams<S> = {} as XrpcRequestParams<S>,\n): UseQueryResult<null | XrpcResponse<S>, XrpcFailure<S>> {\n  const schema = getMain(ns)\n  const client = useBskyClient()\n\n  const queryString =\n    params === false\n      ? params\n      : schema.parameters.toURLSearchParams(params).toString()\n\n  return useQuery({\n    queryKey: [client.did, schema.nsid, queryString],\n    queryFn: async ({ signal }) => {\n      if (params === false) return null\n      const result = await client.xrpcSafe(schema, { signal, params } as any)\n      if (result.success) return result.value\n      throw result.reason\n    },\n    retry: (failureCount, error) => {\n      if (failureCount > 10) return false\n      return error.shouldRetry()\n    },\n    retryDelay: (attemptIndex) => {\n      return Math.min(1000 * 2 ** attemptIndex, 30000)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/src/queries/use-lex-record.ts",
    "content": "import { UseQueryResult, useQuery } from '@tanstack/react-query'\nimport { GetOptions, GetOutput, XrpcError, l } from '@atproto/lex'\nimport { useBskyClient } from '../providers/BskyClientProvider.tsx'\n\nexport function useLexRecord<S extends l.RecordSchema>(\n  ns: NonNullable<unknown> extends GetOptions<S>\n    ? S | { main: S }\n    : l.Restricted<'This record schema requires a \"rkey\" argument'>,\n): UseQueryResult<GetOutput<S>>\nexport function useLexRecord<S extends l.RecordSchema>(\n  ns: S | { main: S },\n  options: GetOptions<S>,\n): UseQueryResult<GetOutput<S>>\nexport function useLexRecord<S extends l.RecordSchema>(\n  ns: S | { main: S },\n  options: GetOptions<S> = {} as GetOptions<S>,\n): UseQueryResult<GetOutput<S>> {\n  const schema = 'main' in ns ? ns.main : ns\n  const client = useBskyClient()\n\n  return useQuery({\n    queryKey: [\n      options?.repo ?? client.did ?? null,\n      schema.$type,\n      options.rkey ?? null,\n    ],\n    queryFn: async ({ signal }) => {\n      return client.get(schema, { ...options, signal })\n    },\n    retry: (failureCount, error) => {\n      if (failureCount > 10) return false\n      return error instanceof XrpcError && error.shouldRetry()\n    },\n    retryDelay: (attemptIndex) => {\n      return Math.min(1000 * 2 ** attemptIndex, 30000)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/tsconfig.build.json",
    "content": "{\n  \"extends\": [\n    \"../../../tsconfig/browser.json\",\n    \"../../../tsconfig/bundler.json\"\n  ],\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\",\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src/**/*.ts\", \"./src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tools.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/tsconfig.tools.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"moduleResolution\": \"node16\",\n    \"module\": \"node18\",\n    \"noEmit\": true\n  },\n  \"include\": [\"./*.js\", \"./*.mjs\", \"./*.cjs\", \"./*.ts\"],\n  \"exclude\": []\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-browser-example/vite.config.mjs",
    "content": "import tailwindcss from '@tailwindcss/vite'\nimport react from '@vitejs/plugin-react-swc'\nimport { defineConfig } from 'vite'\nimport { bundleManifest } from '@atproto-labs/rollup-plugin-bundle-manifest'\n\nexport default defineConfig({\n  plugins: [\n    //\n    react({ plugins: [['@lingui/swc-plugin', {}]] }),\n    tailwindcss(),\n  ],\n  build: {\n    emptyOutDir: true,\n    outDir: './dist',\n    sourcemap: true,\n    commonjsOptions: {\n      include: [\n        /did/,\n        /fetch/,\n        /handle-resolver/,\n        /identity-resolver/,\n        /jwk/,\n        /lex-client/,\n        /lex-data/,\n        /lex-json/,\n        /lex-schema/,\n        /lex/,\n        /node_modules/,\n        /oauth-client-browser/,\n        /oauth-client/,\n        /oauth-scopes/,\n        /oauth-types/,\n        /pipe/,\n        /simple-store/,\n        /syntax/,\n      ],\n    },\n    rollupOptions: {\n      plugins: [bundleManifest({ name: 'files.json', data: true })],\n    },\n  },\n  // Needed because this is a monorepo (and packages are CommonJS)\n  optimizeDeps: {\n    include: [\n      // @NOTEs Only explicit dependencies of this package should be included\n      // here\n      '@atproto/lex',\n      '@atproto/oauth-client-browser',\n    ],\n  },\n})\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/.gitignore",
    "content": "/android/build\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/CHANGELOG.md",
    "content": "# @atproto/oauth-client-expo\n\n## 0.0.10\n\n### Patch Changes\n\n- [#4642](https://github.com/bluesky-social/atproto/pull/4642) [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on `event-target-polyfill`\n\n- Updated dependencies [[`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df)]:\n  - @atproto/oauth-client@0.6.0\n  - @atproto/oauth-client-browser@0.3.41\n\n## 0.0.9\n\n### Patch Changes\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add iOS specific options on authentication web-view\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dev logs\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.14\n  - @atproto/oauth-client-browser@0.3.40\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.13\n  - @atproto/oauth-client-browser@0.3.39\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.12\n  - @atproto/oauth-client-browser@0.3.38\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.11\n  - @atproto/oauth-client-browser@0.3.37\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.10\n  - @atproto/oauth-client-browser@0.3.36\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.9\n  - @atproto/oauth-client-browser@0.3.35\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The package now re-exports everything from `@atproto/oauth-client`, avoiding the need to explicitly depend on it.\n\n- [#4300](https://github.com/bluesky-social/atproto/pull/4300) [`1a7bd8c0d`](https://github.com/bluesky-social/atproto/commit/1a7bd8c0d2a5dad5cd035a82f54655470172203d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `abortcontroller-polyfill` that was causing \"Property 'DOMException' doesn't exist\" errors\n\n- [#4300](https://github.com/bluesky-social/atproto/pull/4300) [`1a7bd8c0d`](https://github.com/bluesky-social/atproto/commit/1a7bd8c0d2a5dad5cd035a82f54655470172203d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Polyfill `core-js/proposals/explicit-resource-management` for web\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.8\n  - @atproto/oauth-client-browser@0.3.34\n\n## 0.0.1\n\n### Patch Changes\n\n- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - OAuthClient SDK for Expo\n\n- Updated dependencies [[`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/oauth-client-browser@0.3.33\n  - @atproto/oauth-client@0.5.7\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/README.md",
    "content": "# Expo Atproto OAuth\n\nThis is an Expo client library for Atproto OAuth. It implements the required\nnative crypto functions for supporting JWTs in React Native and uses the base\n`OAuthClient` interface found in [the Atproto repository](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client).\n\n### In bare React Native projects\n\nFor bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/)\nbefore continuing.\n\n## Installation\n\nOnce you have satisfied the prerequisites, you can simply install the library with `npm install --save @atproto/oauth-client-expo`.\n\n## Usage\n\n### Serve your `oauth-client-metadata.json`\n\nYou will need to server an `oauth-client-metadata.json` from your application's website. An example of this metadata\nwould look like this:\n\n```json\n// assets/oauth-client-metadata.json\n{\n  \"client_id\": \"https://example.com/oauth-client-metadata.json\",\n  \"client_name\": \"React Native OAuth Client Demo\",\n  \"client_uri\": \"https://example.com\",\n  \"redirect_uris\": [\"com.example:/auth/callback\"],\n  \"scope\": \"atproto repo:* rpc:*?aud=did:web:api.bsky.app#bsky_appview\",\n  \"token_endpoint_auth_method\": \"none\",\n  \"response_types\": [\"code\"],\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"application_type\": \"native\",\n  \"dpop_bound_access_tokens\": true\n}\n```\n\n- The `client_id` should be the same URL as where you are serving your\n  `oauth-client-metadata.json` from\n- The `client_uri` can be the home page of where you are serving your metadata\n  from\n- Your `redirect_uris` should contain a native redirect URI (for ios/android),\n  as well as a web redirect URI (for web).\n- native redirect URI must have a custom scheme, which is formatted as the\n  _reverse_ of the domain you are serving the metadata from. Since I am serving\n  mine from `example.com`, I use `com.example` as the scheme. If my domain were\n  `atproto.expo.dev`, I would use `dev.expo.atproto`. Additionally, the scheme\n  _must_ contain _only one trailing slash_ after the `:`. `com.example://` is\n  invalid.\n- The `application_type` must be `native`\n\nFor a real-world example, see [Skylight's client metadata](https://skylight.expo.app/oauth/client-metadata.json).\n\nFor more information about client metadata, see [the Atproto documentation](https://atproto.com/specs/oauth#client-id-metadata-document).\n\n### Create a client\n\nNext, you want to create an `ExpoOAuthClient`. You will need to pass in the same client metadata to the client as you are serving in your `oauth-client-metadata.json`.\n\n```ts\n// utils/oauth-client.ts\nconst clientMetadata = require('../assets/oauth-client-metadata.json')\n\nconst client = new ExpoOAuthClient({\n  handleResolver: 'https://bsky.social',\n  clientMetadata,\n})\n```\n\n### Sign a user in\n\nWhenever you are ready, you can initiate a sign in attempt for the user using the client using `client.signIn(input)`\n\n`input` must be one of the following:\n\n- A valid Atproto user handle, e.g. `hailey.bsky.team` or `example.com`\n- A valid DID, e.g. `did:web:example.com` or `did:plc:oisofpd7lj26yvgiivf3lxsi`\n- A valid PDS host, e.g. `https://cocoon.example.com` or `https://bsky.social`\n\n> [!NOTE] If you wish to allow a user to _create_ an account instead of signing\n> in, simply use a valid PDS hostname rather than a handle. They will be\n> presented the option to either Sign In with an existing account, or create a\n> new one, on the PDS's sign in page.\n\nThe response of `signIn` will be a promise resolving to the following:\n\n```ts\n    | { status: WebBrowserResultType } // See Expo Web Browser documentation\n    | { status: 'error'; error: unknown }\n    | { status: 'success'; session: OAuthSession }\n```\n\nFor example:\n\n```ts\ntry {\n  const session = await client.signIn(input ?? '')\n  setSession(session)\n  const agent = new Agent(session)\n  setAgent(agent)\n} catch (err) {\n  Alert.alert('Error', String(err))\n}\n```\n\n### Create an `Agent`\n\nTo interface with the various Atproto APIs, you will need to create an `Agent`. You will pass your `OAuthSession` to the `Agent` or `XrpcClient` constructor.\n\n```ts\nconst agent = new Agent(session)\n// or\nconst xrpc = new XrpcClient(session)\n```\n\nSession refreshes will be handled for you for the lifetime of the agent.\n\n### Restoring a session\n\nAfter, for example, closing the application, you will probably need to restore the user's session. You can do this by using the user's DID on the `ExpoOAuthClient`.\n\n```ts\nconst session = await client.restore('did:plc:oisofpd7lj26yvgiivf3lxsi')\nconst agent = new Agent(session)\n```\n\nIf the session needs to be refreshed, `.restore()` will automatically do this for you before returning a session (based on the token's expiration date). In order to force a refresh, you can pass in `true` as the second argument to `restore`.\n\n```ts\nconst session = await client.restore(\n  'did:plc:oisofpd7lj26yvgiivf3lxsi',\n  true, // force a refresh, ensuring tokens were not revoked\n)\n```\n\n## Additional Reading\n\n- [Atproto OAuth Spec](https://atproto.com/specs/oauth)\n- [Atproto Web OAuth Example](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example)\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/.editorconfig",
    "content": "[*.{kt,kts}]\nindent_size=2\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\ngroup = 'expo.modules.atprotooauthclient'\nversion = '0.1.0'\n\ndef expoModulesCorePlugin = new File(project(\":expo-modules-core\").projectDir.absolutePath, \"ExpoModulesCorePlugin.gradle\")\napply from: expoModulesCorePlugin\napplyKotlinExpoModulesCorePlugin()\nuseCoreDependencies()\nuseExpoPublishing()\n\n// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.\n// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.\n// Most of the time, you may like to manage the Android SDK versions yourself.\ndef useManagedAndroidSdkVersions = false\nif (useManagedAndroidSdkVersions) {\n  useDefaultAndroidSdkVersions()\n} else {\n  buildscript {\n    // Simple helper that allows the root project to override versions declared by this library.\n    ext.safeExtGet = { prop, fallback ->\n      rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback\n    }\n  }\n  project.android {\n    compileSdkVersion safeExtGet(\"compileSdkVersion\", 34)\n    defaultConfig {\n      minSdkVersion safeExtGet(\"minSdkVersion\", 21)\n      targetSdkVersion safeExtGet(\"targetSdkVersion\", 34)\n    }\n  }\n}\n\nandroid {\n  namespace \"expo.modules.atprotooauthclient\"\n  defaultConfig {\n    versionCode 1\n    versionName \"0.1.0\"\n  }\n  lintOptions {\n    abortOnError false\n  }\n}\n\ndependencies {\n  implementation \"com.nimbusds:nimbus-jose-jwt:10.3.1\"\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/src/main/AndroidManifest.xml",
    "content": "<manifest>\n</manifest>\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/src/main/java/expo/modules/atprotooauthclient/Crypto.kt",
    "content": "package expo.modules.atprotooauthclient\n\nimport com.nimbusds.jose.Algorithm\nimport com.nimbusds.jose.jwk.Curve\nimport com.nimbusds.jose.jwk.ECKey\nimport com.nimbusds.jose.jwk.KeyUse\nimport com.nimbusds.jose.util.Base64URL\nimport expo.modules.atprotooauthclient.EncodedJWK\nimport java.security.KeyPairGenerator\nimport java.security.MessageDigest\nimport java.security.interfaces.ECPrivateKey\nimport java.security.interfaces.ECPublicKey\nimport java.util.UUID\n\nclass Crypto {\n  fun digest(data: ByteArray): ByteArray {\n    val instance = MessageDigest.getInstance(\"sha256\")\n    return instance.digest(data)\n  }\n\n  fun getRandomValues(byteLength: Int): ByteArray {\n    val random = ByteArray(byteLength)\n    java.security.SecureRandom().nextBytes(random)\n    return random\n  }\n\n  fun generateJwk(): EncodedJWK {\n    val keyIdString = UUID.randomUUID().toString()\n\n    val keyPairGen = KeyPairGenerator.getInstance(\"EC\")\n    keyPairGen.initialize(Curve.P_256.toECParameterSpec())\n    val keyPair = keyPairGen.generateKeyPair()\n\n    val publicKey = keyPair.public as ECPublicKey\n    val privateKey = keyPair.private as ECPrivateKey\n\n    val privateJwk =\n      ECKey\n        .Builder(Curve.P_256, publicKey)\n        .privateKey(privateKey)\n        .keyUse(KeyUse.SIGNATURE)\n        .keyID(keyIdString)\n        .algorithm(Algorithm.parse(\"ES256\"))\n        .build()\n\n    return EncodedJWK().apply {\n      kty = privateJwk.keyType.value\n      crv = privateJwk.curve.toString()\n      kid = keyIdString\n      x = privateJwk.x.toString()\n      y = privateJwk.y.toString()\n      d = privateJwk.d.toString()\n      alg = privateJwk.algorithm.name\n    }\n  }\n\n  fun decodeJwk(encodedJwk: EncodedJWK): ECKey {\n    val xb64url = Base64URL.from(encodedJwk.x)\n    val yb64url = Base64URL.from(encodedJwk.y)\n    val db64url = Base64URL.from(encodedJwk.d)\n    return ECKey\n      .Builder(Curve.P_256, xb64url, yb64url)\n      .d(db64url)\n      .keyUse(KeyUse.SIGNATURE)\n      .keyID(encodedJwk.kid)\n      .algorithm(Algorithm.parse(encodedJwk.alg))\n      .build()\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/src/main/java/expo/modules/atprotooauthclient/ExpoAtprotoOAuthClientModule.kt",
    "content": "package expo.modules.atprotooauthclient\n\nimport expo.modules.atprotooauthclient.Crypto\nimport expo.modules.atprotooauthclient.Jose\nimport expo.modules.kotlin.modules.Module\nimport expo.modules.kotlin.modules.ModuleDefinition\n\nclass ExpoAtprotoOAuthClientModule : Module() {\n  override fun definition() =\n    ModuleDefinition {\n      Name(\"ExpoAtprotoOAuthClient\")\n\n      AsyncFunction(\"digest\") { data: ByteArray, algo: String ->\n        if (algo != \"sha256\") {\n          throw IllegalArgumentException(\"Unsupported algorithm: $algo\")\n        }\n        return@AsyncFunction Crypto().digest(data)\n      }\n\n      AsyncFunction(\"getRandomValues\") { byteLength: Int ->\n        return@AsyncFunction Crypto().getRandomValues(byteLength)\n      }\n\n      AsyncFunction(\"generatePrivateJwk\") { algo: String ->\n        if (algo != \"ES256\") {\n          throw IllegalArgumentException(\"Unsupported algorithm: $algo\")\n        }\n        return@AsyncFunction Crypto().generateJwk()\n      }\n\n      AsyncFunction(\"createJwt\") { header: String, payload: String, encodedJwk: EncodedJWK ->\n        val jwk = Crypto().decodeJwk(encodedJwk)\n        return@AsyncFunction Jose().createJwt(header, payload, jwk)\n      }\n\n      AsyncFunction(\"verifyJwt\") { token: String, encodedJwk: EncodedJWK, options: VerifyOptions ->\n        val jwk = Crypto().decodeJwk(encodedJwk)\n        return@AsyncFunction Jose().verifyJwt(token, jwk, options)\n      }\n    }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/src/main/java/expo/modules/atprotooauthclient/Jose.kt",
    "content": "package expo.modules.atprotooauthclient\n\nimport com.nimbusds.jose.JWSHeader\nimport com.nimbusds.jose.crypto.ECDSASigner\nimport com.nimbusds.jose.crypto.ECDSAVerifier\nimport com.nimbusds.jose.jwk.ECKey\nimport com.nimbusds.jwt.JWTClaimsSet\nimport com.nimbusds.jwt.SignedJWT\nimport expo.modules.atprotooauthclient.VerifyOptions\nimport expo.modules.atprotooauthclient.VerifyResult\n\nclass InvalidPayloadException(\n  message: String,\n) : Exception(message)\n\nclass Jose {\n  fun createJwt(\n    header: String,\n    payload: String,\n    jwk: ECKey,\n  ): String {\n    val parsedHeader = JWSHeader.parse(header)\n    val parsedPayload = JWTClaimsSet.parse(payload)\n\n    val signer = ECDSASigner(jwk)\n    val jwt = SignedJWT(parsedHeader, parsedPayload)\n    jwt.sign(signer)\n\n    return jwt.serialize()\n  }\n\n  fun verifyJwt(\n    token: String,\n    jwk: ECKey,\n    options: VerifyOptions,\n  ): VerifyResult {\n    val jwt = SignedJWT.parse(token)\n    val verifier = ECDSAVerifier(jwk)\n\n    if (!jwt.verify(verifier)) {\n      throw InvalidPayloadException(\"invalid JWT signature\")\n    }\n\n    val protectedHeader = emptyMap<String, Any>().toMutableMap()\n    protectedHeader[\"alg\"] = jwt.header.algorithm\n\n    jwt.header.getCustomParam(\"jku\")?.let {\n      protectedHeader[\"jku\"] = it.toString()\n    }\n    jwt.header.keyID?.let {\n      protectedHeader[\"kid\"] = it\n    }\n    jwt.header.type?.let {\n      protectedHeader[\"typ\"] = it.toString()\n    }\n    jwt.header.contentType?.let {\n      protectedHeader[\"cty\"] = it\n    }\n    jwt.header.criticalParams?.let {\n      protectedHeader[\"crit\"] = it.toList()\n    }\n\n    options.typ?.let {\n      if (jwt.header.type.toString() != it) {\n        throw InvalidPayloadException(\"typ mismatch\")\n      }\n    }\n\n    val claims = jwt.jwtClaimsSet\n\n    options.requiredClaims?.let { requiredClaims ->\n      requiredClaims.forEach { claim ->\n        if (!claims.claims.containsKey(claim)) {\n          throw InvalidPayloadException(\"required claim '$claim' missing\")\n        }\n      }\n    }\n\n    options.audience?.let {\n      if (!claims.audience.contains(it)) {\n        throw InvalidPayloadException(\"audience mismatch\")\n      }\n    }\n\n    options.subject?.let {\n      if (claims.subject != it) {\n        throw InvalidPayloadException(\"subject mismatch\")\n      }\n    }\n\n    options.checkTolerance?.let {\n      val currentTime = options.currentDate ?: (System.currentTimeMillis() / 1000.0)\n      if (claims.issueTime.time / 1000.0 + it < currentTime) {\n        throw InvalidPayloadException(\"token expired\")\n      }\n    }\n\n    options.maxTokenAge?.let {\n      val currentTime = options.currentDate ?: (System.currentTimeMillis() / 1000.0)\n      if (claims.issueTime.time / 1000.0 + it < currentTime) {\n        throw InvalidPayloadException(\"token expired\")\n      }\n    }\n\n    options.issuer?.let {\n      if (claims.issuer != it) {\n        throw InvalidPayloadException(\"issuer mismatch\")\n      }\n    }\n\n    return VerifyResult().apply {\n      payload = jwt.payload.toString()\n      this.protectedHeader = protectedHeader\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/android/src/main/java/expo/modules/atprotooauthclient/Records.kt",
    "content": "package expo.modules.atprotooauthclient\n\nimport expo.modules.kotlin.records.Field\nimport expo.modules.kotlin.records.Record\n\nclass EncodedJWK : Record {\n  @Field\n  var kty: String = \"\"\n\n  @Field\n  var crv: String = \"\"\n\n  @Field\n  var kid: String = \"\"\n\n  @Field\n  var x: String = \"\"\n\n  @Field\n  var y: String = \"\"\n\n  @Field\n  var d: String = \"\"\n\n  @Field\n  var alg: String = \"\"\n}\n\nclass VerifyOptions : Record {\n  @Field\n  var audience: String? = null\n\n  @Field\n  var checkTolerance: Double? = null\n\n  @Field\n  var issuer: String? = null\n\n  @Field\n  var maxTokenAge: Double? = null\n\n  @Field\n  var subject: String? = null\n\n  @Field\n  var typ: String? = null\n\n  @Field\n  var currentDate: Double? = null\n\n  @Field\n  var requiredClaims: Array<String>? = null\n}\n\nclass VerifyResult : Record {\n  @Field\n  var payload: String = \"\"\n\n  @Field\n  var protectedHeader: Map<String, Any> = emptyMap()\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/expo-module.config.json",
    "content": "{\n  \"platforms\": [\"apple\", \"android\", \"web\"],\n  \"apple\": {\n    \"modules\": [\"ExpoAtprotoOAuthClientModule\"]\n  },\n  \"android\": {\n    \"modules\": [\"expo.modules.atprotooauthclient.ExpoAtprotoOAuthClientModule\"]\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/ios/Crypto.swift",
    "content": "import Foundation\nimport CryptoKit\nimport JOSESwift\n\nclass CryptoUtil: NSObject {\n  static func digest(data: Data) -> Data {\n    let hash = SHA256.hash(data: data)\n    return Data(hash)\n  }\n\n  public static func getRandomValues(byteLength: Int) -> Data {\n    let bytes = (0..<byteLength).map { _ in UInt8.random(in: UInt8.min...UInt8.max) }\n    return Data(bytes)\n  }\n\n  static func generateJwk() -> EncodedJWK {\n    let kid = UUID().uuidString\n\n    let privKey = P256.Signing.PrivateKey()\n    let pubKey = privKey.publicKey\n\n    let x = pubKey.x963Representation[1..<33].base64URLEncodedString()\n    let y = pubKey.x963Representation[33...].base64URLEncodedString()\n    let d = privKey.rawRepresentation.base64URLEncodedString()\n\n    let jwk = EncodedJWK()\n    jwk.kty = \"EC\"\n    jwk.crv = \"P-256\"\n    jwk.kid = kid\n    jwk.x = x\n    jwk.y = y\n    jwk.d = d\n    jwk.alg = \"ES256\"\n\n    return jwk\n  }\n\n  static func decodeJwk(x: String, y: String, d: String) throws -> SecKey {\n    func base64UrlDecode(_ string: String) -> Data? {\n      var base64 = string\n        .replacingOccurrences(of: \"-\", with: \"+\")\n        .replacingOccurrences(of: \"_\", with: \"/\")\n\n      let remainder = base64.count % 4\n      if remainder > 0 {\n        base64 += String(repeating: \"=\", count: 4 - remainder)\n      }\n\n      return Data(base64Encoded: base64)\n    }\n\n    guard let xData = base64UrlDecode(x),\n          let yData = base64UrlDecode(y),\n          let dData = base64UrlDecode(d) else {\n      throw ExpoAtprotoOAuthClientError.invalidJwk\n    }\n\n    var keyData = Data()\n    keyData.append(0x04)\n    keyData.append(xData)\n    keyData.append(yData)\n    keyData.append(dData)\n\n    let attributes: [String: Any] = [\n      kSecAttrKeyType as String: kSecAttrKeyTypeEC,\n      kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,\n      kSecAttrKeySizeInBits as String: 256\n    ]\n\n    var error: Unmanaged<CFError>?\n\n    let key = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)\n    if error != nil {\n      throw error!.takeUnretainedValue()\n    }\n\n    guard let key = key else {\n      throw ExpoAtprotoOAuthClientError.invalidJwk\n    }\n\n    return key\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/ios/ExpoAtprotoOAuthClient.podspec",
    "content": "require 'json'\n\npackage = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))\n\nPod::Spec.new do |s|\n  s.name           = 'ExpoAtprotoOAuthClient'\n  s.version        = package['version']\n  s.summary        = package['description']\n  s.description    = package['description']\n  s.license        = package['license']\n  s.author         = package['author']\n  s.authors        = package['authors']\n  s.homepage       = package['homepage']\n  s.platforms      = {\n    :ios => '15.1',\n    :tvos => '15.1'\n  }\n  s.swift_version  = '5.4'\n  s.source         = { git: 'https://github.com/bluesky-social/atproto' }\n  s.static_framework = true\n\n  s.dependency 'ExpoModulesCore'\n  s.dependency 'JOSESwift', '~> 2.3'\n\n  # Swift/Objective-C compatibility\n  s.pod_target_xcconfig = {\n    'DEFINES_MODULE' => 'YES',\n  }\n\n  s.source_files = \"**/*.{h,m,mm,swift,hpp,cpp}\"\nend\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/ios/ExpoAtprotoOAuthClientModule.swift",
    "content": "import ExpoModulesCore\n\nenum ExpoAtprotoOAuthClientError: Error {\n  case unsupportedAlgorithm(String)\n  case invalidJwk\n  case invalidHeader(String)\n  case invalidPayload(String)\n  case nullSigner\n}\n\npublic class ExpoAtprotoOAuthClientModule: Module {\n  public func definition() -> ModuleDefinition {\n    Name(\"ExpoAtprotoOAuthClient\")\n\n    AsyncFunction(\"digest\") { (data: Data, algo: String) throws -> Data in\n      if algo != \"sha256\" {\n        throw ExpoAtprotoOAuthClientError.unsupportedAlgorithm(algo)\n      }\n      return CryptoUtil.digest(data: data)\n    }\n\n    AsyncFunction(\"getRandomValues\") { (byteLength: Int) -> Data in\n      return CryptoUtil.getRandomValues(byteLength: byteLength)\n    }\n\n    AsyncFunction(\"generatePrivateJwk\") { (algo: String) throws -> EncodedJWK in\n      if algo != \"ES256\" {\n        throw ExpoAtprotoOAuthClientError.unsupportedAlgorithm(algo)\n      }\n      return CryptoUtil.generateJwk()\n    }\n\n    AsyncFunction(\"createJwt\") { (header: String, payload: String, jwk: EncodedJWK) throws -> String in\n      let jwk = try CryptoUtil.decodeJwk(x: jwk.x, y: jwk.y, d: jwk.d)\n      let jwt = try JoseUtil.createJwt(header: header, payload: payload, jwk: jwk)\n      return jwt\n    }\n\n    AsyncFunction(\"verifyJwt\") { (token: String, jwk: EncodedJWK, options: VerifyOptions) throws -> VerifyResult in\n      let jwk = try CryptoUtil.decodeJwk(x: jwk.x, y: jwk.y, d: jwk.d)\n      let res = try JoseUtil.verifyJwt(token: token, jwk: jwk, options: options)\n      return res\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/ios/Jose.swift",
    "content": "import JOSESwift\n\nclass JoseUtil: NSObject {\n  private static func headerStringToHeader(_ headerString: String) -> JWSHeader? {\n    guard let headerData = headerString.data(using: .utf8) else {\n      return nil\n    }\n    return JWSHeader(headerData)\n  }\n\n  private static func payloadStringToPayload(_ payloadString: String) -> Payload? {\n    guard let payloadData = payloadString.data(using: .utf8) else {\n      return nil\n    }\n    return Payload(payloadData)\n  }\n\n  static func createJwt(header: String, payload: String, jwk: SecKey) throws -> String {\n    guard let header = headerStringToHeader(header) else {\n      throw ExpoAtprotoOAuthClientError.invalidHeader(\"could not parse header string\")\n    }\n\n    guard let payload = payloadStringToPayload(payload) else {\n      throw ExpoAtprotoOAuthClientError.invalidPayload(\"could not parse payload string\")\n    }\n\n    let signer = Signer(signingAlgorithm: .ES256, key: jwk)\n\n    guard let signer = signer else {\n      throw ExpoAtprotoOAuthClientError.nullSigner\n    }\n\n    let jws = try JWS(header: header, payload: payload, signer: signer)\n\n    return jws.compactSerializedString\n  }\n\n  static func verifyJwt(token: String, jwk: SecKey, options: VerifyOptions) throws -> VerifyResult {\n    guard let jws = try? JWS(compactSerialization: token),\n          let verifier = Verifier(verifyingAlgorithm: .ES256, key: jwk),\n          let validation = try? jws.validate(using: verifier)\n    else {\n      throw ExpoAtprotoOAuthClientError.invalidJwk\n    }\n\n    let header = validation.header\n    let payload = String(data: validation.payload.data(), encoding: .utf8)\n    guard let payload = payload else {\n      throw ExpoAtprotoOAuthClientError.invalidPayload(\"unable to parse payload\")\n    }\n\n    var protectedHeader: [String: Any] = [:]\n    protectedHeader[\"alg\"] = \"ES256\"\n    if header.jku != nil {\n      protectedHeader[\"jku\"] = header.jku?.absoluteString\n    }\n    if header.kid != nil {\n      protectedHeader[\"kid\"] = header.kid\n    }\n    if header.typ != nil {\n      protectedHeader[\"typ\"] = header.typ\n    }\n    if header.cty != nil {\n      protectedHeader[\"cty\"] = header.cty\n    }\n    if header.crit != nil {\n      protectedHeader[\"crit\"] = header.crit\n    }\n\n    if let typ = options.typ {\n      if header.typ != typ {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"typ mismatch\")\n      }\n    }\n\n    let claims = try JSONSerialization.jsonObject(with: validation.payload.data(), options: []) as? [String: Any]\n\n    if let requiredClaims = options.requiredClaims {\n      try requiredClaims.forEach { c in\n        if claims?[c] == nil {\n          throw ExpoAtprotoOAuthClientError.invalidPayload(\"required claim \\(c) missing\")\n        }\n      }\n    }\n\n    if let audience = options.audience {\n      if claims?[\"aud\"] as? String != audience {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"audience mismatch\")\n      }\n    }\n\n    if let subject = options.subject {\n      if claims?[\"sub\"] as? String != subject {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"subject mismatch\")\n      }\n    }\n\n    if let checkTolerance = options.clockTolerance {\n      let now = Date()\n      let expiryDate: Date\n      if let expiryString = claims?[\"exp\"] as? String {\n        let formatter = ISO8601DateFormatter()\n        expiryDate = formatter.date(from: expiryString)!\n      } else {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"expiry missing\")\n      }\n      if expiryDate < now - checkTolerance {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"token expired\")\n      }\n    }\n\n    if let maxTokenAge = options.maxTokenAge {\n      let now = Date()\n      if let expiryString = claims?[\"exp\"] as? String {\n        let formatter = ISO8601DateFormatter()\n        let expiryDate = formatter.date(from: expiryString)!\n        if expiryDate < now - maxTokenAge {\n          throw ExpoAtprotoOAuthClientError.invalidPayload(\"token expired\")\n        }\n      } else {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"expiry missing\")\n      }\n    }\n\n    if let issuer = options.issuer {\n      if claims?[\"iss\"] as? String != issuer {\n        throw ExpoAtprotoOAuthClientError.invalidPayload(\"issuer mismatch\")\n      }\n    }\n\n    let res = VerifyResult()\n    res.payload = payload\n    res.protectedHeader = protectedHeader\n\n    return res\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/ios/Records.swift",
    "content": "import ExpoModulesCore\n\nstruct EncodedJWK: Record {\n  @Field\n  var kty: String\n\n  @Field\n  var crv: String\n\n  @Field\n  var kid: String\n\n  @Field\n  var x: String\n\n  @Field\n  var y: String\n\n  @Field\n  var d: String\n\n  @Field\n  var alg: String\n}\n\nstruct VerifyOptions: Record {\n  @Field\n  var audience: String?\n\n  @Field\n  var clockTolerance: Double?\n\n  @Field\n  var issuer: String?\n\n  @Field\n  var maxTokenAge: Double?\n\n  @Field\n  var subject: String?\n\n  @Field\n  var typ: String?\n\n  @Field\n  var currentDate: Date?\n\n  @Field\n  var requiredClaims: [String]?\n}\n\nstruct VerifyResult: Record {\n  @Field\n  var payload: String\n\n  @Field\n  var protectedHeader: [String: Any]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-client-expo\",\n  \"version\": \"0.0.10\",\n  \"license\": \"MIT\",\n  \"description\": \"ATPROTO OAuth client for Expo applications\",\n  \"authors\": [\n    \"Hailey <me@haileyok.com> (https://github.com/haileyok)\",\n    \"Matthieu Sieben <me@matthieusieben.com> (https://github.com/matthieusieben)\"\n  ],\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"client\",\n    \"react-native\",\n    \"expo\"\n  ],\n  \"bugs\": {\n    \"url\": \"https://github.com/bluesky-social/atproto/issues\"\n  },\n  \"homepage\": \"https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-expo#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-client-expo\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto/oauth-client\": \"workspace:^\",\n    \"@atproto/oauth-client-browser\": \"workspace:^\",\n    \"core-js\": \"^3\",\n    \"expo-web-browser\": \"^15.0.8\",\n    \"react-native-mmkv\": \"^3.3.3\",\n    \"react-native-url-polyfill\": \"^3.0.0\"\n  },\n  \"peerDependencies\": {\n    \"expo\": \"*\",\n    \"react-native\": \"*\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/ExpoAtprotoOAuthClientModule.ts",
    "content": "import { NativeModule, requireNativeModule } from 'expo'\nimport { SignedJwt, VerifyOptions, VerifyResult } from '@atproto/oauth-client'\nimport { ExpoAtprotoOAuthClientModuleEvents } from './ExpoAtprotoOAuthClientModule.types'\n\nexport type NativeJwk = {\n  kty: 'EC'\n  crv: 'P-256'\n  kid: string\n  x: string\n  y: string\n  d: string\n  alg: 'ES256'\n}\n\ndeclare class ExpoAtprotoOAuthClientModule extends NativeModule<ExpoAtprotoOAuthClientModuleEvents> {\n  digest(data: Uint8Array, algo: string): Promise<Uint8Array>\n\n  getRandomValues(byteLength: number): Promise<Uint8Array>\n\n  generatePrivateJwk(algorithm: string): Promise<NativeJwk>\n\n  createJwt(header: string, payload: string, jwk: NativeJwk): Promise<SignedJwt>\n\n  verifyJwt<C extends string = never>(\n    token: SignedJwt,\n    jwk: NativeJwk,\n    options: VerifyOptions<C>,\n  ): Promise<VerifyResult<C>>\n}\n\nexport default requireNativeModule<ExpoAtprotoOAuthClientModule>(\n  'ExpoAtprotoOAuthClient',\n)\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/ExpoAtprotoOAuthClientModule.types.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-types\nexport type ExpoAtprotoOAuthClientModuleEvents = {}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/expo-oauth-client-interface.ts",
    "content": "import type {\n  AuthorizeOptions,\n  OAuthClient,\n  OAuthSession,\n} from '@atproto/oauth-client'\n\nexport interface ExpoOAuthClientInterface extends OAuthClient, AsyncDisposable {\n  signIn(input: string, options?: AuthorizeOptions): Promise<OAuthSession>\n  handleCallback(): Promise<null | OAuthSession>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/expo-oauth-client-options.ts",
    "content": "import type {\n  OAuthClientMetadataInput,\n  OAuthClientOptions,\n  OAuthResponseMode,\n} from '@atproto/oauth-client'\n\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n\nexport type ExpoOAuthClientOptions = Simplify<\n  {\n    clientMetadata: Readonly<OAuthClientMetadataInput>\n    responseMode?: Exclude<OAuthResponseMode, 'form_post'>\n  } & Omit<\n    OAuthClientOptions,\n    | 'clientMetadata'\n    | 'responseMode'\n    | 'keyset'\n    | 'runtimeImplementation'\n    | 'sessionStore'\n    | 'stateStore'\n    | 'didCache'\n    | 'handleCache'\n    | 'dpopNonceCache'\n    | 'authorizationServerMetadataCache'\n    | 'protectedResourceMetadataCache'\n  >\n>\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/expo-oauth-client.d.ts",
    "content": "import { ExpoOAuthClientInterface } from './expo-oauth-client-interface'\nimport { ExpoOAuthClientOptions } from './expo-oauth-client-options'\n\nexport declare class ExpoOAuthClient implements ExpoOAuthClientInterface {\n  constructor(options: ExpoOAuthClientOptions)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/expo-oauth-client.native.ts",
    "content": "import { openAuthSessionAsync } from 'expo-web-browser'\nimport {\n  AuthorizeOptions,\n  OAuthClient,\n  OAuthSession,\n  RuntimeImplementation,\n} from '@atproto/oauth-client'\nimport { default as NativeModule } from './ExpoAtprotoOAuthClientModule'\nimport { ExpoOAuthClientInterface } from './expo-oauth-client-interface'\nimport { ExpoOAuthClientOptions } from './expo-oauth-client-options'\nimport { ExpoKey } from './utils/expo-key'\nimport {\n  AuthorizationServerMetadataCache,\n  DidCache,\n  DpopNonceCache,\n  HandleCache,\n  ProtectedResourceMetadataCache,\n  SessionStore,\n  StateStore,\n} from './utils/stores'\n\nexport const CUSTOM_URI_SCHEME_REGEX = /^(?:[^.]+(?:\\.[^.]+)+):\\/(?:[^/].*)?$/\nconst isCustomUriScheme = (uri: string) => CUSTOM_URI_SCHEME_REGEX.test(uri)\n\nconst runtimeImplementation: RuntimeImplementation = {\n  createKey: async (algs) => ExpoKey.generate(algs),\n  digest: async (bytes, { name }) => NativeModule.digest(bytes, name),\n  getRandomValues: async (length) => NativeModule.getRandomValues(length),\n}\n\nexport class ExpoOAuthClient\n  extends OAuthClient\n  implements ExpoOAuthClientInterface\n{\n  readonly #disposables: DisposableStack\n\n  constructor(options: ExpoOAuthClientOptions) {\n    using stack = new DisposableStack()\n\n    super({\n      ...options,\n      responseMode: options.responseMode ?? 'query',\n      keyset: undefined,\n      runtimeImplementation,\n      sessionStore: stack.use(new SessionStore()),\n      stateStore: stack.use(new StateStore()),\n      didCache: stack.use(new DidCache()),\n      handleCache: stack.use(new HandleCache()),\n      dpopNonceCache: stack.use(new DpopNonceCache()),\n      authorizationServerMetadataCache: stack.use(\n        new AuthorizationServerMetadataCache(),\n      ),\n      protectedResourceMetadataCache: stack.use(\n        new ProtectedResourceMetadataCache(),\n      ),\n    })\n\n    this.#disposables = stack.move()\n  }\n\n  async handleCallback(): Promise<null | OAuthSession> {\n    return null\n  }\n\n  async signIn(\n    input: string,\n    options?: AuthorizeOptions,\n  ): Promise<OAuthSession> {\n    const redirectUri =\n      options?.redirect_uri ??\n      this.clientMetadata.redirect_uris.find(isCustomUriScheme)\n\n    if (!redirectUri) {\n      throw new TypeError(\n        'A redirect URI with a custom scheme is required for Expo OAuth.',\n      )\n    }\n\n    const url = await this.authorize(input, {\n      ...options,\n      redirect_uri: redirectUri,\n      display: options?.display ?? 'touch',\n    })\n\n    const result = await openAuthSessionAsync(url.toString(), redirectUri, {\n      dismissButtonStyle: 'cancel', // iOS only\n      preferEphemeralSession: false, // iOS only\n    })\n\n    if (result.type === 'success') {\n      const callbackUrl = new URL(result.url)\n      const params =\n        this.responseMode === 'fragment'\n          ? new URLSearchParams(callbackUrl.hash.slice(1))\n          : callbackUrl.searchParams\n\n      const { session } = await this.callback(params, {\n        redirect_uri: redirectUri,\n      })\n      return session\n    } else {\n      throw new Error(`Authentication cancelled: ${result.type}`)\n    }\n  }\n\n  async [Symbol.asyncDispose]() {\n    this.#disposables.dispose()\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/expo-oauth-client.web.ts",
    "content": "import {\n  AuthorizeOptions,\n  BrowserOAuthClient,\n  OAuthSession,\n} from '@atproto/oauth-client-browser'\nimport { ExpoOAuthClientInterface } from './expo-oauth-client-interface'\nimport { ExpoOAuthClientOptions } from './expo-oauth-client-options'\n\nexport class ExpoOAuthClient\n  extends BrowserOAuthClient\n  implements ExpoOAuthClientInterface\n{\n  constructor({\n    clientMetadata,\n    responseMode = 'fragment',\n    ...options\n  }: ExpoOAuthClientOptions) {\n    super({ ...options, clientMetadata, responseMode })\n  }\n\n  override async signIn(\n    input: string,\n    options?: AuthorizeOptions,\n  ): Promise<OAuthSession> {\n    // Force popup mode\n    return this.signInPopup(input, {\n      ...options,\n      display: options?.display ?? 'touch',\n    })\n  }\n\n  async handleCallback(): Promise<null | OAuthSession> {\n    const params = this.readCallbackParams()\n    if (!params) return null\n\n    const url = this.findRedirectUrl()\n    if (!url) return null\n\n    const { session } = await this.initCallback(params, url)\n    return session\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/index.ts",
    "content": "import './polyfill'\n\nexport * from '@atproto/oauth-client'\n\nexport type { ExpoOAuthClientInterface } from './expo-oauth-client-interface'\nexport type { ExpoOAuthClientOptions } from './expo-oauth-client-options'\n\nexport { ExpoOAuthClient } from './expo-oauth-client'\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/polyfill.d.ts",
    "content": "export {}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/polyfill.native.ts",
    "content": "import 'core-js/proposals/explicit-resource-management'\nimport 'react-native-url-polyfill/auto'\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/polyfill.web.ts",
    "content": "import 'core-js/proposals/explicit-resource-management'\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/utils/expo-key.ts",
    "content": "import {\n  type Jwk,\n  type JwtHeader,\n  type JwtPayload,\n  Key,\n  type SignedJwt,\n  type VerifyOptions,\n  type VerifyResult,\n} from '@atproto/oauth-client'\nimport type { NativeJwk } from '../ExpoAtprotoOAuthClientModule'\nimport { default as NativeModule } from '../ExpoAtprotoOAuthClientModule'\n\nexport type ExpoJwk = Jwk & NativeJwk & { key_ops: ['sign'] }\nexport class ExpoKey extends Key<ExpoJwk> {\n  async createJwt(header: JwtHeader, payload: JwtPayload): Promise<SignedJwt> {\n    return NativeModule.createJwt(\n      JSON.stringify(header),\n      JSON.stringify(payload),\n      toNativeJwk(this.jwk),\n    )\n  }\n\n  async verifyJwt<C extends string = never>(\n    token: SignedJwt,\n    options: VerifyOptions<C> = {},\n  ): Promise<VerifyResult<C>> {\n    return NativeModule.verifyJwt(token, toNativeJwk(this.jwk), options)\n  }\n\n  static async generate(algs: string[]): Promise<ExpoKey> {\n    if (algs.includes('ES256')) {\n      const jwk = await NativeModule.generatePrivateJwk('ES256')\n      return new ExpoKey({ ...jwk, key_ops: ['sign'] })\n    }\n\n    throw TypeError(`No supported algorithm found in: ${algs.join(', ')}`)\n  }\n}\n\nfunction toNativeJwk(jwk: ExpoJwk): NativeJwk {\n  return {\n    kty: jwk.kty,\n    crv: jwk.crv,\n    kid: jwk.kid,\n    x: jwk.x,\n    y: jwk.y,\n    d: jwk.d,\n    alg: jwk.alg,\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/utils/mmkv-simple-store-ttl.ts",
    "content": "import { Configuration, MMKV } from 'react-native-mmkv'\nimport type { SimpleStore, Value } from '@atproto-labs/simple-store'\nimport { MMKVSimpleStore, MMKVSimpleStoreOptions } from './mmkv-simple-store'\n\nexport type MMKVSimpleStoreTTLOptions<V extends Value> =\n  MMKVSimpleStoreOptions<V> & {\n    clearInterval?: null | false | number\n    expiresAt: (value: V) => null | number\n  }\n\n/**\n * A {@link SimpleStore} implementation based on {@link MMKVSimpleStore} that\n * supports expiring entries after a certain time.\n */\nexport class MMKVSimpleStoreTTL<V extends Value>\n  extends MMKVSimpleStore<V>\n  implements Disposable, SimpleStore<string, V>\n{\n  readonly #store: MMKV\n  readonly #expiresAt: (value: V) => null | number\n  readonly #clearTimer?: ReturnType<typeof setInterval>\n\n  constructor({\n    clearInterval = 60 * 1e3,\n    expiresAt,\n    encode,\n    decode,\n\n    ...config\n  }: MMKVSimpleStoreTTLOptions<V> & Configuration) {\n    super({ ...config, encode, decode })\n\n    this.#store = new MMKV({ ...config, id: `${config.id}.exp` })\n    this.#expiresAt = expiresAt\n    if (clearInterval) {\n      this.#clearTimer = setInterval(() => this.clearExpired(), clearInterval)\n    }\n\n    this.clearExpired()\n  }\n\n  [Symbol.dispose]() {\n    clearInterval(this.#clearTimer)\n    this.clearExpired()\n  }\n\n  override set(key: string, value: V): void {\n    super.set(key, value)\n\n    const expirationDate = this.#expiresAt.call(null, value)\n    if (expirationDate == null) this.#store.delete(key)\n    else this.#store.set(key, expirationDate)\n  }\n\n  override get(key: string): V | undefined {\n    if (this.isExpired(key)) {\n      this.del(key)\n      return undefined\n    }\n\n    return super.get(key)\n  }\n\n  override del(key: string): void {\n    super.del(key)\n    this.#store.delete(key)\n  }\n\n  override clear(): void {\n    super.clear()\n    this.#store.clearAll()\n  }\n\n  getExpirationTime(key: string): number | undefined {\n    return this.#store.getNumber(key) ?? undefined\n  }\n\n  isExpired(key: string): boolean {\n    const expirationTime = this.getExpirationTime(key)\n    return expirationTime != null && expirationTime < Date.now()\n  }\n\n  clearExpired() {\n    for (const key of this.#store.getAllKeys() ?? []) {\n      if (this.isExpired(key)) {\n        this.del(key)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/utils/mmkv-simple-store.ts",
    "content": "import { Configuration, MMKV } from 'react-native-mmkv'\nimport type { SimpleStore, Value } from '@atproto-labs/simple-store'\n\nexport type MMKVSimpleStoreOptions<V extends Value> = {\n  decode: (value: string) => V\n  encode: (value: V) => string\n}\n\n/**\n * A {@link SimpleStore} implementation using {@link MMKV} for storage.\n */\nexport class MMKVSimpleStore<V extends Value>\n  implements SimpleStore<string, V>\n{\n  readonly #store: MMKV\n  readonly #encode: (value: V) => string\n  readonly #decode: (value: string) => V\n\n  constructor({\n    decode,\n    encode,\n    ...config\n  }: MMKVSimpleStoreOptions<V> & Configuration) {\n    this.#store = new MMKV(config)\n    this.#decode = decode\n    this.#encode = encode\n  }\n\n  set(key: string, value: V): void {\n    const encoded = this.#encode.call(null, value)\n    this.#store.set(key, encoded)\n  }\n\n  get(key: string): V | undefined {\n    const value = this.#store.getString(key)\n    if (value === undefined) return undefined\n\n    return this.#decode.call(null, value)\n  }\n\n  del(key: string): void {\n    this.#store.delete(key)\n  }\n\n  clear() {\n    this.#store.clearAll()\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/src/utils/stores.ts",
    "content": "import type {\n  DidDocument,\n  InternalStateData,\n  OAuthAuthorizationServerMetadata,\n  OAuthProtectedResourceMetadata,\n  ResolvedHandle,\n  Session,\n} from '@atproto/oauth-client'\nimport { ExpoKey } from './expo-key'\nimport { MMKVSimpleStoreTTL } from './mmkv-simple-store-ttl'\n\nconst MMKV_ID = 'expo-atproto-oauth-client'\n\nexport class AuthorizationServerMetadataCache extends MMKVSimpleStoreTTL<OAuthAuthorizationServerMetadata> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.authorizationServerMetadata`,\n      expiresAt: oneMinuteFromNow,\n      decode: JSON.parse,\n      encode: JSON.stringify,\n    })\n  }\n}\n\nexport class ProtectedResourceMetadataCache extends MMKVSimpleStoreTTL<OAuthProtectedResourceMetadata> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.protectedResourceMetadata`,\n      expiresAt: oneMinuteFromNow,\n      decode: JSON.parse,\n      encode: JSON.stringify,\n    })\n  }\n}\n\nexport class DpopNonceCache extends MMKVSimpleStoreTTL<string> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.dpopNonce`,\n      expiresAt: tenMinutesFromNow,\n      decode: identity,\n      encode: identity,\n    })\n  }\n}\n\nexport class DidCache extends MMKVSimpleStoreTTL<DidDocument> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.did`,\n      expiresAt: oneMinuteFromNow,\n      decode: JSON.parse,\n      encode: JSON.stringify,\n    })\n  }\n}\n\nexport class HandleCache extends MMKVSimpleStoreTTL<ResolvedHandle> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.handle`,\n      expiresAt: oneMinuteFromNow,\n      decode: JSON.parse,\n      encode: JSON.stringify,\n    })\n  }\n}\n\nexport class StateStore extends MMKVSimpleStoreTTL<InternalStateData> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.state`,\n      expiresAt: tenMinutesFromNow,\n      decode: (value) => {\n        const parsed = JSON.parse(value)\n        return { ...parsed, dpopKey: new ExpoKey(parsed.dpopKey) }\n      },\n      encode: (value) => {\n        return JSON.stringify({ ...value, dpopKey: value.dpopKey.jwk })\n      },\n    })\n  }\n}\n\nexport class SessionStore extends MMKVSimpleStoreTTL<Session> {\n  constructor() {\n    super({\n      id: `${MMKV_ID}.session`,\n      expiresAt: ({ tokenSet }) => {\n        if (tokenSet.refresh_token) return null\n        if (tokenSet.expires_at) return new Date(tokenSet.expires_at).valueOf()\n        return null\n      },\n      decode: (value) => {\n        const parsed = JSON.parse(value)\n        return { ...parsed, dpopKey: new ExpoKey(parsed.dpopKey) }\n      },\n      encode: (value) => {\n        return JSON.stringify({ ...value, dpopKey: value.dpopKey.jwk })\n      },\n    })\n  }\n}\n\nfunction identity<T>(x: T): T {\n  return x\n}\n\nfunction tenMinutesFromNow() {\n  return Date.now() + 10 * 60e3\n}\n\nfunction oneMinuteFromNow() {\n  return Date.now() + 60e3\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/expo.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-expo/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/CHANGELOG.md",
    "content": "# @atproto/oauth-client-node\n\n## 0.3.17\n\n### Patch Changes\n\n- Updated dependencies [[`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df), [`a23d132`](https://github.com/bluesky-social/atproto/commit/a23d13268ccfd51a54d21256469b8cb43f7b07df)]:\n  - @atproto/oauth-client@0.6.0\n\n## 0.3.16\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n  - @atproto-labs/did-resolver@0.2.6\n  - @atproto-labs/handle-resolver-node@0.1.25\n  - @atproto/oauth-client@0.5.14\n  - @atproto/oauth-types@0.6.2\n\n## 0.3.15\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n  - @atproto/oauth-client@0.5.13\n  - @atproto-labs/did-resolver@0.2.5\n  - @atproto-labs/handle-resolver-node@0.1.24\n  - @atproto/oauth-types@0.6.1\n\n## 0.3.14\n\n### Patch Changes\n\n- Updated dependencies [[`95ef3c2`](https://github.com/bluesky-social/atproto/commit/95ef3c24e8072e9d49412950b033cb8607764ee0), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41)]:\n  - @atproto/oauth-types@0.6.0\n  - @atproto/oauth-client@0.5.12\n\n## 0.3.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.11\n\n## 0.3.12\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n  - @atproto-labs/did-resolver@0.2.4\n  - @atproto-labs/handle-resolver-node@0.1.23\n  - @atproto/oauth-client@0.5.10\n  - @atproto/oauth-types@0.5.2\n\n## 0.3.11\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto-labs/did-resolver@0.2.3\n  - @atproto/did@0.2.2\n  - @atproto/oauth-client@0.5.9\n  - @atproto-labs/handle-resolver-node@0.1.22\n  - @atproto/oauth-types@0.5.1\n\n## 0.3.10\n\n### Patch Changes\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto/oauth-types@0.5.0\n  - @atproto/oauth-client@0.5.8\n  - @atproto-labs/handle-resolver-node@0.1.21\n\n## 0.3.9\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/oauth-types@0.4.2\n  - @atproto/oauth-client@0.5.7\n  - @atproto/jwk@0.6.0\n  - @atproto/jwk-webcrypto@0.2.0\n  - @atproto/did@0.2.1\n  - @atproto/jwk-jose@0.1.11\n  - @atproto-labs/did-resolver@0.2.2\n  - @atproto-labs/handle-resolver-node@0.1.20\n\n## 0.3.8\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.6\n\n## 0.3.7\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`86c4699da`](https://github.com/bluesky-social/atproto/commit/86c4699da8cf184c251e58c0a3a2612dd676f0ea), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/did@0.2.0\n  - @atproto-labs/simple-store@0.3.0\n  - @atproto/oauth-client@0.5.5\n  - @atproto-labs/did-resolver@0.2.1\n  - @atproto-labs/handle-resolver-node@0.1.19\n\n## 0.3.6\n\n### Patch Changes\n\n- [#4139](https://github.com/bluesky-social/atproto/pull/4139) [`6231c8730`](https://github.com/bluesky-social/atproto/commit/6231c8730adb3a4c17dec417e5332b2be61070e5) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Fix support for multiple redirect URIs in `@atproto/oauth-client`\n\n  Previously the callback method assumed a singular `redirect_uris` value, and enforced only performing the callback with the first registered redirect URI. This change allows passing the actual redirect URI to the `callback` method, much like the `authorize` method supports.\n\n- Updated dependencies [[`6231c8730`](https://github.com/bluesky-social/atproto/commit/6231c8730adb3a4c17dec417e5332b2be61070e5)]:\n  - @atproto/oauth-client@0.5.4\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.3\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n  - @atproto/jwk-jose@0.1.10\n  - @atproto/jwk-webcrypto@0.1.10\n  - @atproto/oauth-client@0.5.2\n  - @atproto/oauth-types@0.4.1\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.5.1\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`4c2d49917`](https://github.com/bluesky-social/atproto/commit/4c2d499178c61eb8a9d7f658e89fe68fa07f81e7), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a)]:\n  - @atproto/oauth-client@0.5.0\n  - @atproto/oauth-types@0.4.0\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver-node@0.1.18\n  - @atproto/oauth-client@0.4.2\n\n## 0.3.0\n\n### Minor Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `NodeOAuthClient.fromClientId`\n\n### Patch Changes\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Re-export all types & utilities needed to instantiate an OAuth client\n\n- [#3976](https://github.com/bluesky-social/atproto/pull/3976) [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow passing custom `handleResolver` option\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto/oauth-client@0.4.1\n  - @atproto-labs/did-resolver@0.2.0\n  - @atproto/jwk@0.4.0\n  - @atproto-labs/handle-resolver-node@0.1.17\n  - @atproto/jwk-jose@0.1.9\n  - @atproto/jwk-webcrypto@0.1.9\n  - @atproto/oauth-types@0.3.1\n\n## 0.2.24\n\n### Patch Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor typing change\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/oauth-types@0.3.0\n  - @atproto/oauth-client@0.4.0\n  - @atproto/jwk@0.3.0\n  - @atproto/jwk-jose@0.1.8\n  - @atproto/jwk-webcrypto@0.1.8\n\n## 0.2.23\n\n### Patch Changes\n\n- Updated dependencies [[`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee), [`4e96e2c7b`](https://github.com/bluesky-social/atproto/commit/4e96e2c7b7cc0231607d3065c95704069c4ca2a2)]:\n  - @atproto/oauth-client@0.3.22\n\n## 0.2.22\n\n### Patch Changes\n\n- Updated dependencies [[`cd4bed3c9`](https://github.com/bluesky-social/atproto/commit/cd4bed3c9e68878c3f79620fe19f6994ebcb932e)]:\n  - @atproto/oauth-client@0.3.21\n\n## 0.2.21\n\n### Patch Changes\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c)]:\n  - @atproto/jwk@0.2.0\n  - @atproto/oauth-types@0.2.8\n  - @atproto/oauth-client@0.3.20\n  - @atproto/jwk-jose@0.1.7\n  - @atproto/jwk-webcrypto@0.1.7\n\n## 0.2.20\n\n### Patch Changes\n\n- Updated dependencies [[`a03f0b906`](https://github.com/bluesky-social/atproto/commit/a03f0b906b108f8c766a5700f0d68b55748f23bd)]:\n  - @atproto/oauth-client@0.3.19\n\n## 0.2.19\n\n### Patch Changes\n\n- Updated dependencies [[`36d0d370c`](https://github.com/bluesky-social/atproto/commit/36d0d370c24498f74c243ebfb01564e5050c672d)]:\n  - @atproto/oauth-client@0.3.18\n\n## 0.2.18\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver-node@0.1.16\n  - @atproto-labs/did-resolver@0.1.13\n  - @atproto/oauth-client@0.3.17\n\n## 0.2.17\n\n### Patch Changes\n\n- Updated dependencies [[`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8)]:\n  - @atproto/oauth-types@0.2.7\n  - @atproto/oauth-client@0.3.16\n\n## 0.2.16\n\n### Patch Changes\n\n- Updated dependencies [[`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4)]:\n  - @atproto-labs/simple-store@0.2.0\n  - @atproto/oauth-types@0.2.6\n  - @atproto-labs/did-resolver@0.1.12\n  - @atproto/oauth-client@0.3.15\n  - @atproto-labs/handle-resolver-node@0.1.15\n\n## 0.2.15\n\n### Patch Changes\n\n- Updated dependencies [[`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/oauth-types@0.2.5\n  - @atproto/jwk@0.1.5\n  - @atproto/oauth-client@0.3.14\n  - @atproto/jwk-jose@0.1.6\n  - @atproto/jwk-webcrypto@0.1.6\n\n## 0.2.14\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.13\n\n## 0.2.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.12\n\n## 0.2.12\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/oauth-types@0.2.4\n  - @atproto/jwk@0.1.4\n  - @atproto/oauth-client@0.3.11\n  - @atproto-labs/did-resolver@0.1.11\n  - @atproto/jwk-jose@0.1.5\n  - @atproto/jwk-webcrypto@0.1.5\n  - @atproto-labs/handle-resolver-node@0.1.14\n\n## 0.2.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.10\n\n## 0.2.10\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/handle-resolver-node@0.1.13\n  - @atproto-labs/did-resolver@0.1.10\n  - @atproto-labs/simple-store@0.1.2\n  - @atproto/jwk-webcrypto@0.1.4\n  - @atproto/oauth-client@0.3.9\n  - @atproto/oauth-types@0.2.3\n  - @atproto/jwk-jose@0.1.4\n  - @atproto/jwk@0.1.3\n  - @atproto/did@0.1.5\n\n## 0.2.9\n\n### Patch Changes\n\n- Updated dependencies [[`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87), [`cc2a1222b`](https://github.com/bluesky-social/atproto/commit/cc2a1222bd2b8ddd70d70dad174c1c63246a2d87)]:\n  - @atproto-labs/did-resolver@0.1.9\n  - @atproto/did@0.1.4\n  - @atproto/oauth-client@0.3.8\n  - @atproto-labs/handle-resolver-node@0.1.12\n\n## 0.2.8\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver-node@0.1.11\n\n## 0.2.7\n\n### Patch Changes\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0)]:\n  - @atproto/jwk@0.1.2\n  - @atproto/jwk-jose@0.1.3\n  - @atproto/jwk-webcrypto@0.1.3\n  - @atproto/oauth-client@0.3.7\n  - @atproto/oauth-types@0.2.2\n  - @atproto-labs/did-resolver@0.1.8\n  - @atproto-labs/handle-resolver-node@0.1.10\n\n## 0.2.6\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto-labs/did-resolver@0.1.7\n  - @atproto/oauth-client@0.3.6\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver-node@0.1.9\n  - @atproto/oauth-client@0.3.5\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.4\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/did-resolver@0.1.6\n  - @atproto/oauth-client@0.3.3\n  - @atproto-labs/handle-resolver-node@0.1.8\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3)]:\n  - @atproto/oauth-types@0.2.1\n  - @atproto/oauth-client@0.3.2\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.3.1\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use `\"auto\"` instead of `undefined` to descibe the refresh mechanism to use in various methods.\n\n### Patch Changes\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bugfix: Prevent accidental override of `NodeOAuthClient` constructor options\n\n- Updated dependencies [[`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf)]:\n  - @atproto/oauth-client@0.3.0\n  - @atproto/oauth-types@0.2.0\n  - @atproto-labs/did-resolver@0.1.5\n  - @atproto/did@0.1.3\n  - @atproto-labs/handle-resolver-node@0.1.7\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver-node@0.1.6\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto-labs/handle-resolver-node@0.1.5\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8)]:\n  - @atproto/oauth-types@0.1.5\n  - @atproto/oauth-client@0.2.2\n  - @atproto-labs/handle-resolver-node@0.1.4\n  - @atproto-labs/did-resolver@0.1.4\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c), [`cb4abbb67`](https://github.com/bluesky-social/atproto/commit/cb4abbb673c69a8a89b49dca5c038f3da2153c6c)]:\n  - @atproto/did@0.1.2\n  - @atproto-labs/did-resolver@0.1.3\n  - @atproto-labs/handle-resolver-node@0.1.3\n  - @atproto/oauth-client@0.2.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The `OAuthClient` (and runtime specific sub-classes) no longer return @atproto/api `Agent` instances. Instead, they return `OAuthSession` instances that can be used to instantiate the `Agent` class.\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"openid\" compatibility. The reason is that although we were technically \"openid\" compatible, ATProto identifiers are distributed identifiers. When a client relies on OpenID to authenticate users, it will use the auth provider in combination with the identifier to uniquely identify the user. Since ATProto identifiers are meant to be able to move from one provider to the other, OpenID compatibility could break authentication after a user was migrated to a different provider.\n\n  The way OpenID compliant clients would adapt to this particularity would typically be to remove the provider + identifier combination and use the identifier alone. While this is indeed the right way to handle ATProto identifiers, it requires more work to avoid impersonation. In particular, when obtaining a user identifier, the client **must** verify that the issuer of the identity token is indeed the server responsible for that user. This mechanism being not enforced by the OpenID standard, OpenID compatibility could lead to security issues. For this reason, we decided to remove OpenID compatibility from the OAuth provider.\n\n  Note that a trusted central authority could still offer OpenID compatibility by relying on ATProto's regular OAuth flow under the hood. This capability is out of the scope of this library.\n\n### Patch Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove un-necessary dev dependency\n\n- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/oauth-client@0.2.0\n  - @atproto/oauth-types@0.1.4\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.1.7\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.1.6\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`3ebcd4e61`](https://github.com/bluesky-social/atproto/commit/3ebcd4e6161291d3649d7f8a9c5ee4ac26d590a2), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb)]:\n  - @atproto/oauth-client@0.1.5\n  - @atproto/oauth-types@0.1.3\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`04112783d`](https://github.com/bluesky-social/atproto/commit/04112783db17f865c9e2b673190f77dd0b7461e3)]:\n  - @atproto/oauth-client@0.1.4\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-client@0.1.3\n\n## 0.0.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Better implement aptroto OAuth spec\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/oauth-client@0.1.2\n  - @atproto/jwk-jose@0.1.2\n  - @atproto/oauth-types@0.1.2\n  - @atproto/did@0.1.1\n  - @atproto/jwk-webcrypto@0.1.2\n  - @atproto-labs/handle-resolver-node@0.1.2\n  - @atproto-labs/did-resolver@0.1.2\n\n## 0.0.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Create NodeJS OAuth SDK\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add event emitting capability to OAuthClient\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/oauth-types@0.1.1\n  - @atproto/jwk-jose@0.1.1\n  - @atproto/jwk@0.1.1\n  - @atproto-labs/handle-resolver-node@0.1.1\n  - @atproto-labs/did-resolver@0.1.1\n  - @atproto-labs/simple-store@0.1.1\n  - @atproto/oauth-client@0.1.1\n  - @atproto/jwk-webcrypto@0.1.1\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/README.md",
    "content": "# atproto OAuth Client for NodeJS\n\nThis package implements all the OAuth features required by [ATPROTO] (PKCE,\netc.) to run in a NodeJS based environment such as desktop apps built with\nElectron or traditional web app backends built with frameworks like Express.\n\n## Setup\n\n### Client configuration\n\nThe `client_id` is what identifies your application to the OAuth server. It is\nused to fetch the client metadata, and to initiate the OAuth flow. The\n`client_id` must be a URL that points to the client metadata.\n\nYour OAuth client metadata should be hosted at a URL that corresponds to the\n`client_id` of your application. This URL should return a JSON object with the\nclient metadata. The client metadata should be configured according to the\nneeds of your application, and must respect the [ATPROTO].\n\n#### From a backend service\n\nThe `client_metadata` object will typically be built by the backend at startup.\n\n```ts\nimport { NodeOAuthClient, Session } from '@atproto/oauth-client-node'\nimport { JoseKey } from '@atproto/jwk-jose'\n\nconst client = new NodeOAuthClient({\n  // This object will be used to build the payload of the /client-metadata.json\n  // endpoint metadata, exposing the client metadata to the OAuth server.\n  clientMetadata: {\n    // Must be a URL that will be exposing this metadata\n    client_id: 'https://my-app.com/client-metadata.json',\n    client_name: 'My App',\n    client_uri: 'https://my-app.com',\n    logo_uri: 'https://my-app.com/logo.png',\n    tos_uri: 'https://my-app.com/tos',\n    policy_uri: 'https://my-app.com/policy',\n    redirect_uris: ['https://my-app.com/callback'],\n    grant_types: ['authorization_code', 'refresh_token'],\n    scope: 'atproto transition:generic',\n    response_types: ['code'],\n    application_type: 'web',\n    token_endpoint_auth_method: 'private_key_jwt',\n    token_endpoint_auth_signing_alg: 'RS256',\n    dpop_bound_access_tokens: true,\n    jwks_uri: 'https://my-app.com/jwks.json',\n  },\n\n  // Used to authenticate the client to the token endpoint. Will be used to\n  // build the jwks object to be exposed on the \"jwks_uri\" endpoint.\n  keyset: await Promise.all([\n    JoseKey.fromImportable(process.env.PRIVATE_KEY_1, 'key1'),\n    JoseKey.fromImportable(process.env.PRIVATE_KEY_2, 'key2'),\n    JoseKey.fromImportable(process.env.PRIVATE_KEY_3, 'key3'),\n  ]),\n\n  // Interface to store authorization state data (during authorization flows)\n  stateStore: {\n    async set(key: string, internalState: NodeSavedState): Promise<void> {},\n    async get(key: string): Promise<NodeSavedState | undefined> {},\n    async del(key: string): Promise<void> {},\n  },\n\n  // Interface to store authenticated session data\n  sessionStore: {\n    async set(sub: string, session: Session): Promise<void> {},\n    async get(sub: string): Promise<Session | undefined> {},\n    async del(sub: string): Promise<void> {},\n  },\n\n  // A lock to prevent concurrent access to the session store. Optional if only one instance is running.\n  requestLock,\n})\n\nconst app = express()\n\n// Expose the metadata and jwks\napp.get('client-metadata.json', (req, res) => res.json(client.clientMetadata))\napp.get('jwks.json', (req, res) => res.json(client.jwks))\n\n// Create an endpoint to initiate the OAuth flow\napp.get('/login', async (req, res, next) => {\n  try {\n    const handle = 'some-handle.bsky.social' // eg. from query string\n    const state = '434321'\n\n    // Revoke any pending authentication requests if the connection is closed (optional)\n    const ac = new AbortController()\n    req.on('close', () => ac.abort())\n\n    const url = await client.authorize(handle, {\n      signal: ac.signal,\n      state,\n      // Only supported if OAuth server is openid-compliant\n      ui_locales: 'fr-CA fr en',\n    })\n\n    res.redirect(url)\n  } catch (err) {\n    next(err)\n  }\n})\n\n// Create an endpoint to handle the OAuth callback\napp.get('/atproto-oauth-callback', async (req, res, next) => {\n  try {\n    const params = new URLSearchParams(req.url.split('?')[1])\n\n    const { session, state } = await client.callback(params)\n\n    // Process successful authentication here\n    console.log('authorize() was called with state:', state)\n\n    console.log('User authenticated as:', session.did)\n\n    const agent = new Agent(session)\n\n    // Make Authenticated API calls\n    const profile = await agent.getProfile({ actor: agent.did })\n    console.log('Bsky profile:', profile.data)\n\n    res.json({ ok: true })\n  } catch (err) {\n    next(err)\n  }\n})\n\n// Whenever needed, restore a user's session\nasync function worker() {\n  const userDid = 'did:plc:123'\n\n  const oauthSession = await client.restore(userDid)\n\n  // Note: If the current access_token is expired, the session will automatically\n  // (and transparently) refresh it. The new token set will be saved though\n  // the client's session store.\n\n  const agent = new Agent(oauthSession)\n\n  // Make Authenticated API calls\n  const profile = await agent.getProfile({ actor: agent.did })\n  console.log('Bsky profile:', profile.data)\n}\n```\n\n#### From a native application\n\nThis applies to mobile apps, desktop apps, etc. based on NodeJS (e.g. Electron).\n\nThe client metadata must be hosted on an internet-accessible URL owned by you.\nThe client metadata will typically contain:\n\n```json\n{\n  \"client_id\": \"https://my-app.com/client-metadata.json\",\n  \"client_name\": \"My App\",\n  \"client_uri\": \"https://my-app.com\",\n  \"logo_uri\": \"https://my-app.com/logo.png\",\n  \"tos_uri\": \"https://my-app.com/tos\",\n  \"policy_uri\": \"https://my-app.com/policy\",\n  \"redirect_uris\": [\"https://my-app.com/atproto-oauth-callback\"],\n  \"scope\": \"atproto\",\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"response_types\": [\"code\"],\n  \"application_type\": \"native\",\n  \"token_endpoint_auth_method\": \"none\",\n  \"dpop_bound_access_tokens\": true\n}\n```\n\nInstead of hard-coding the client metadata in your app, you can fetch it when\nthe app starts:\n\n```ts\nimport { NodeOAuthClient } from '@atproto/oauth-client-node'\n\nconst client = await NodeOAuthClient.fromClientId({\n  clientId: 'https://my-app.com/client-metadata.json',\n\n  stateStore: {\n    async set(key: string, internalState: NodeSavedState): Promise<void> {},\n    async get(key: string): Promise<NodeSavedState | undefined> {},\n    async del(key: string): Promise<void> {},\n  },\n\n  sessionStore: {\n    async set(sub: string, session: Session): Promise<void> {},\n    async get(sub: string): Promise<Session | undefined> {},\n    async del(sub: string): Promise<void> {},\n  },\n\n  // A lock to prevent concurrent access to the session store. Optional if only one instance is running.\n  requestLock,\n})\n```\n\n> [!NOTE]\n>\n> There is no `keyset` in this instance. This is due to the fact that app\n> clients cannot safely store a private key. The `token_endpoint_auth_method` is\n> set to `none` in the client metadata, which means that the client will not be\n> authenticating itself to the token endpoint. This will cause sessions to have\n> a shorter lifetime. You can circumvent this by providing a \"BFF\" (Backend for\n> Frontend) that will perform an authenticated OAuth flow and use a session id\n> based mechanism to authenticate the client.\n\n### Common configuration options\n\nThe `OAuthClient` and `OAuthAgent` classes will manage and refresh OAuth tokens\ntransparently. They are also responsible to properly format the HTTP requests\npayload, using DPoP, and transparently retrying requests when the access token\nexpires.\n\nFor this to work, the client must be configured with the following options:\n\n#### `sessionStore`\n\nA simple key-value store to save the OAuth session data. This is used to save\nthe access token, refresh token, and other session data.\n\n```ts\nconst sessionStore: NodeSavedSessionStore = {\n  async set(sub: string, sessionData: NodeSavedSession) {\n    // Insert or update the session data in your database\n    await saveSessionDataToDb(sub, sessionData)\n  },\n\n  async get(sub: string) {\n    // Retrieve the session data from your database\n    const sessionData = await getSessionDataFromDb(sub)\n    if (!sessionData) return undefined\n\n    return sessionData\n  },\n\n  async del(sub: string) {\n    // Delete the session data from your database\n    await deleteSessionDataFromDb(sub)\n  },\n}\n```\n\n#### `stateStore`\n\nA simple key-value store to save the state of the OAuth\nauthorization flow. This is used to prevent CSRF attacks.\n\nThe implementation of the `StateStore` is similar to the\n[`sessionStore`](#sessionstore).\n\n```ts\ninterface NodeSavedStateStore {\n  set: (key: string, internalState: NodeSavedState) => Promise<void>\n  get: (key: string) => Promise<NodeSavedState | undefined>\n  del: (key: string) => Promise<void>\n}\n```\n\nOne notable exception is that state store items can (and should) be deleted\nafter a short period of time (one hour should be more than enough).\n\n#### `requestLock`\n\nWhen multiple instances of the client are running, this lock will prevent\nconcurrent refreshes of the same session.\n\nHere is an example implementation based on [`redlock`](https://www.npmjs.com/package/redlock):\n\n```ts\nimport { RuntimeLock } from '@atproto/oauth-client-node'\nimport Redis from 'ioredis'\nimport Redlock from 'redlock'\n\nconst redisClients = new Redis()\nconst redlock = new Redlock(redisClients)\n\nconst requestLock: RuntimeLock = async (key, fn) => {\n  // 30 seconds should be enough. Since we will be using one lock per user id\n  // we can be quite liberal with the lock duration here.\n  const lock = await redlock.lock(key, 45e3)\n  try {\n    return await fn()\n  } finally {\n    await redlock.unlock(lock)\n  }\n}\n```\n\n## Usage with `@atproto/api`\n\n`@atproto/oauth-client-*` packages all return an `ApiClient` instance upon\nsuccessful authentication. This instance can be used to make authenticated\nrequests using all the `ApiClient` methods defined in [[API]] (non exhaustive\nlist of examples below). Any refresh of the credentials will happen under the\nhood, and the new tokens will be saved in the session store.\n\n```ts\nconst session = await client.restore('did:plc:123')\nconst agent = new Agent(session)\n\n// Feeds and content\nawait agent.getTimeline(params, opts)\nawait agent.getAuthorFeed(params, opts)\nawait agent.getPostThread(params, opts)\nawait agent.getPost(params)\nawait agent.getPosts(params, opts)\nawait agent.getLikes(params, opts)\nawait agent.getRepostedBy(params, opts)\nawait agent.post(record)\nawait agent.deletePost(postUri)\nawait agent.like(uri, cid)\nawait agent.deleteLike(likeUri)\nawait agent.repost(uri, cid)\nawait agent.deleteRepost(repostUri)\nawait agent.uploadBlob(data, opts)\n\n// Social graph\nawait agent.getFollows(params, opts)\nawait agent.getFollowers(params, opts)\nawait agent.follow(did)\nawait agent.deleteFollow(followUri)\n\n// Actors\nawait agent.getProfile(params, opts)\nawait agent.upsertProfile(updateFn)\nawait agent.getProfiles(params, opts)\nawait agent.getSuggestions(params, opts)\nawait agent.searchActors(params, opts)\nawait agent.searchActorsTypeahead(params, opts)\nawait agent.mute(did)\nawait agent.unmute(did)\nawait agent.muteModList(listUri)\nawait agent.unmuteModList(listUri)\nawait agent.blockModList(listUri)\nawait agent.unblockModList(listUri)\n\n// Notifications\nawait agent.listNotifications(params, opts)\nawait agent.countUnreadNotifications(params, opts)\nawait agent.updateSeenNotifications()\n\n// Identity\nawait agent.resolveHandle(params, opts)\nawait agent.updateHandle(params, opts)\n\n// etc.\n\n// Always remember to revoke the credentials when you are done\nawait session.signOut()\n```\n\n## Advances use-cases\n\n### Listening for session updates and deletion\n\nThe `OAuthClient` will emit events whenever a session is updated or deleted.\n\n```ts\nimport {\n  Session,\n  TokenRefreshError,\n  TokenRevokedError,\n} from '@atproto/oauth-client-node'\n\nclient.addEventListener('updated', (event: CustomEvent<Session>) => {\n  console.log('Refreshed tokens were saved in the store:', event.detail)\n})\n\nclient.addEventListener(\n  'deleted',\n  (\n    event: CustomEvent<{\n      sub: string\n      cause: TokenRefreshError | TokenRevokedError | unknown\n    }>,\n  ) => {\n    console.log('Session was deleted from the session store:', event.detail)\n\n    const { cause } = event.detail\n\n    if (cause instanceof TokenRefreshError) {\n      // - refresh_token unavailable or expired\n      // - oauth response error (`cause.cause instanceof OAuthResponseError`)\n      // - session data does not match expected values returned by the OAuth server\n    } else if (cause instanceof TokenRevokedError) {\n      // Session was revoked through:\n      // - session.signOut()\n      // - client.revoke(sub)\n    } else {\n      // An unexpected error occurred, causing the session to be deleted\n    }\n  },\n)\n```\n\n### Silent Sign-In\n\nUsing silent sign-in requires to handle retries on the callback endpoint.\n\n```ts\napp.get('/login', async (req, res) => {\n  const handle = 'some-handle.bsky.social' // eg. from query string\n  const user = req.user.id\n\n  const url = await client.authorize(handle, {\n    // Use \"prompt=none\" to attempt silent sign-in\n    prompt: 'none',\n\n    // Build an internal state to map the login request to the user, and allow retries\n    state: JSON.stringify({\n      user,\n      handle,\n    }),\n  })\n\n  res.redirect(url)\n})\n\napp.get('/atproto-oauth-callback', async (req, res) => {\n  const params = new URLSearchParams(req.url.split('?')[1])\n  try {\n    try {\n      const { session, state } = await client.callback(params)\n\n      // Process successful authentication here. For example:\n\n      const agent = new Agent(session)\n\n      const profile = await agent.getProfile({ actor: agent.did })\n\n      console.log('Bsky profile:', profile.data)\n    } catch (err) {\n      // Silent sign-in failed, retry without prompt=none\n      if (\n        err instanceof OAuthCallbackError &&\n        ['login_required', 'consent_required'].includes(err.params.get('error'))\n      ) {\n        // Parse previous state\n        const { user, handle } = JSON.parse(err.state)\n\n        const url = await client.authorize(handle, {\n          // Note that we omit the prompt parameter here. Setting \"prompt=none\"\n          // here would result in an infinite redirect loop.\n\n          // Build a new state (or re-use the previous one)\n          state: JSON.stringify({\n            user,\n            handle,\n          }),\n        })\n\n        // redirect to new URL\n        res.redirect(url)\n\n        return\n      }\n\n      throw err\n    }\n  } catch (err) {\n    next(err)\n  }\n})\n```\n\n[ATPROTO]: https://atproto.com/ 'AT Protocol'\n[API]: ../../api/README.md\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-client-node\",\n  \"version\": \"0.3.17\",\n  \"license\": \"MIT\",\n  \"description\": \"ATPROTO OAuth client for the NodeJS\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"client\",\n    \"node\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-client-node\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"dependencies\": {\n    \"@atproto-labs/did-resolver\": \"workspace:^\",\n    \"@atproto-labs/handle-resolver-node\": \"workspace:^\",\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/jwk\": \"workspace:^\",\n    \"@atproto/jwk-jose\": \"workspace:^\",\n    \"@atproto/jwk-webcrypto\": \"workspace:^\",\n    \"@atproto/oauth-client\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/src/index.ts",
    "content": "export * from '@atproto-labs/handle-resolver-node'\nexport * from '@atproto/jwk-webcrypto'\nexport * from '@atproto/oauth-client'\nexport * from '@atproto/jwk-jose'\n\nexport * from './node-oauth-client.js'\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/src/node-dpop-store.ts",
    "content": "import { Jwk, Key } from '@atproto/jwk'\nimport { JoseKey } from '@atproto/jwk-jose'\nimport { InternalStateData, Session } from '@atproto/oauth-client'\nimport { SimpleStore } from '@atproto-labs/simple-store'\n\ntype ToDpopJwkValue<V extends { dpopKey: Key }> = Omit<V, 'dpopKey'> & {\n  dpopJwk: Jwk\n}\n\n/**\n * Utility function that allows to simplify the store interface by exposing a\n * JWK (JSON) instead of a Key instance.\n */\nexport function toDpopKeyStore<\n  K extends string,\n  V extends { dpopKey: Key; dpopJwk?: never },\n>(store: SimpleStore<K, ToDpopJwkValue<V>>): SimpleStore<K, V> {\n  return {\n    async set(sub: K, { dpopKey, ...data }: V) {\n      const dpopJwk = dpopKey.privateJwk\n      if (!dpopJwk) throw new Error('Private DPoP JWK is missing.')\n\n      await store.set(sub, { ...data, dpopJwk })\n    },\n\n    async get(sub: K) {\n      const result = await store.get(sub)\n      if (!result) return undefined\n\n      const { dpopJwk, ...data } = result\n      const dpopKey = await JoseKey.fromJWK(dpopJwk)\n      return { ...data, dpopKey } as unknown as V\n    },\n\n    del: store.del.bind(store),\n    clear: store.clear?.bind(store),\n  }\n}\n\nexport type NodeSavedState = ToDpopJwkValue<InternalStateData>\nexport type NodeSavedStateStore = SimpleStore<string, NodeSavedState>\n\nexport type NodeSavedSession = ToDpopJwkValue<Session>\nexport type NodeSavedSessionStore = SimpleStore<string, NodeSavedSession>\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/src/node-oauth-client.ts",
    "content": "import { createHash, randomBytes } from 'node:crypto'\nimport { JoseKey } from '@atproto/jwk-jose'\nimport {\n  HandleResolver,\n  OAuthClient,\n  OAuthClientFetchMetadataOptions,\n  OAuthClientOptions,\n  RuntimeImplementation,\n  RuntimeLock,\n} from '@atproto/oauth-client'\nimport { OAuthResponseMode } from '@atproto/oauth-types'\nimport {\n  AtprotoHandleResolverNode,\n  AtprotoHandleResolverNodeOptions,\n} from '@atproto-labs/handle-resolver-node'\nimport {\n  NodeSavedSessionStore,\n  NodeSavedStateStore,\n  toDpopKeyStore,\n} from './node-dpop-store.js'\nimport { Override } from './util.js'\n\nexport type * from './node-dpop-store.js'\nexport type { OAuthClientOptions, OAuthResponseMode, RuntimeLock }\n\nexport type NodeOAuthClientOptions = Override<\n  OAuthClientOptions,\n  {\n    responseMode?: Exclude<OAuthResponseMode, 'fragment'>\n\n    stateStore: NodeSavedStateStore\n    sessionStore: NodeSavedSessionStore\n\n    /**\n     * Used to build a {@link NodeOAuthClientOptions.handleResolver} if none is\n     * provided.\n     */\n    fallbackNameservers?: AtprotoHandleResolverNodeOptions['fallbackNameservers']\n\n    handleResolver?: HandleResolver | string | URL\n\n    /**\n     * Used to build a {@link NodeOAuthClientOptions.runtimeImplementation} if\n     * none is provided. Pass in `requestLocalLock` from `@atproto/oauth-client`\n     * to mute warning.\n     */\n    requestLock?: RuntimeLock\n\n    runtimeImplementation?: RuntimeImplementation\n  }\n>\n\nexport type NodeOAuthClientFromMetadataOptions =\n  OAuthClientFetchMetadataOptions &\n    Omit<NodeOAuthClientOptions, 'clientMetadata'>\n\nexport class NodeOAuthClient extends OAuthClient {\n  constructor({\n    requestLock = undefined,\n    fallbackNameservers = undefined,\n\n    fetch,\n    responseMode = 'query',\n\n    stateStore,\n    sessionStore,\n\n    handleResolver = new AtprotoHandleResolverNode({\n      fetch,\n      fallbackNameservers,\n    }),\n\n    runtimeImplementation = {\n      requestLock,\n      createKey: (algs) => JoseKey.generate(algs),\n      getRandomValues: randomBytes,\n      digest: (bytes, algorithm) =>\n        createHash(algorithm.name).update(bytes).digest(),\n    },\n\n    ...options\n  }: NodeOAuthClientOptions) {\n    if (!runtimeImplementation.requestLock) {\n      // Ok if only one instance of the client is running at a time.\n      console.warn('No lock mechanism provided. Credentials might get revoked.')\n    }\n\n    super({\n      ...options,\n\n      fetch,\n      responseMode,\n      handleResolver,\n      runtimeImplementation,\n\n      stateStore: toDpopKeyStore(stateStore),\n      sessionStore: toDpopKeyStore(sessionStore),\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/src/util.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-types\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\nexport type Override<T, V> = Simplify<V & Omit<T, keyof V>>\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-client-node/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/CHANGELOG.md",
    "content": "# @atproto/oauth-provider\n\n## 0.15.14\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-resolver@0.0.19\n  - @atproto/lex-document@0.0.17\n  - @atproto/common@0.5.15\n\n## 0.15.13\n\n### Patch Changes\n\n- Updated dependencies [[`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f)]:\n  - @atproto/syntax@0.5.1\n  - @atproto/lex-resolver@0.0.18\n  - @atproto/lex-document@0.0.16\n\n## 0.15.12\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common@0.5.14\n  - @atproto/lex-document@0.0.15\n  - @atproto/lex-resolver@0.0.17\n  - @atproto/oauth-provider-frontend@0.2.9\n  - @atproto/oauth-scopes@0.3.2\n\n## 0.15.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex-resolver@0.0.16\n\n## 0.15.10\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-document@0.0.14\n  - @atproto/lex-resolver@0.0.15\n  - @atproto/common@0.5.12\n\n## 0.15.9\n\n### Patch Changes\n\n- [#4620](https://github.com/bluesky-social/atproto/pull/4620) [`fdbbff8`](https://github.com/bluesky-social/atproto/commit/fdbbff854363ed3518a4039ca43cd279e69600e0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure that the `client_id` query param is correct when cancelling an authentication request\n\n- Updated dependencies [[`fdbbff8`](https://github.com/bluesky-social/atproto/commit/fdbbff854363ed3518a4039ca43cd279e69600e0), [`fdbbff8`](https://github.com/bluesky-social/atproto/commit/fdbbff854363ed3518a4039ca43cd279e69600e0)]:\n  - @atproto/oauth-types@0.6.3\n\n## 0.15.8\n\n### Patch Changes\n\n- [#4619](https://github.com/bluesky-social/atproto/pull/4619) [`a2e4e95`](https://github.com/bluesky-social/atproto/commit/a2e4e9584730c1742aca7c1fcc59533a7c159740) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix depencies version\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix development in Safari\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `Permissions-Policy` header from Form redirect page\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Drop migrational code supporting old cookie style.\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Throw more detailed error upon CSRF login issue\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid potential conflict with `Object.prototype` when parsing cookies.\n\n- Updated dependencies [[`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`a2e4e95`](https://github.com/bluesky-social/atproto/commit/a2e4e9584730c1742aca7c1fcc59533a7c159740), [`7b9a98a`](https://github.com/bluesky-social/atproto/commit/7b9a98a763636c5f66a06da11fe6013f29dd9157), [`19ecf5f`](https://github.com/bluesky-social/atproto/commit/19ecf5f76ae0d88c1963211a76920e00eecdd965), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-document@0.0.13\n  - @atproto/oauth-provider-frontend@0.2.9\n  - @atproto/oauth-provider-ui@0.4.3\n  - @atproto/lex-resolver@0.0.14\n  - @atproto/common@0.5.11\n\n## 0.15.7\n\n### Patch Changes\n\n- Updated dependencies [[`49b3806`](https://github.com/bluesky-social/atproto/commit/49b38069ed4b5bd1ef71e967c78e5123b1c1f6f1)]:\n  - @atproto/common@0.5.10\n  - @atproto/lex-resolver@0.0.13\n  - @atproto/lex-document@0.0.12\n\n## 0.15.6\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n  - @atproto/oauth-scopes@0.3.1\n  - @atproto/oauth-types@0.6.2\n  - @atproto/lex-resolver@0.0.12\n  - @atproto/oauth-provider-ui@0.4.2\n  - @atproto/oauth-provider-api@0.3.7\n  - @atproto/oauth-provider-frontend@0.2.8\n\n## 0.15.5\n\n### Patch Changes\n\n- [#4569](https://github.com/bluesky-social/atproto/pull/4569) [`fa4ef5e`](https://github.com/bluesky-social/atproto/commit/fa4ef5e8150b6ae7fabdc90b847370481e1a6b33) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix oauth response when using `prompt=select_account` and no session are available\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/syntax@0.4.3\n  - @atproto/lex-document@0.0.11\n  - @atproto/lex-resolver@0.0.11\n  - @atproto/oauth-provider-frontend@0.2.7\n  - @atproto/common@0.5.9\n\n## 0.15.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.5.8\n  - @atproto/lex-document@0.0.10\n  - @atproto/lex-resolver@0.0.10\n\n## 0.15.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.5.7\n  - @atproto/lex-document@0.0.9\n  - @atproto/lex-resolver@0.0.9\n\n## 0.15.2\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n  - @atproto/lex-resolver@0.0.8\n  - @atproto/common@0.5.6\n  - @atproto/lex-document@0.0.8\n  - @atproto/oauth-types@0.6.1\n  - @atproto/oauth-provider-api@0.3.6\n  - @atproto/oauth-provider-frontend@0.2.7\n  - @atproto/oauth-provider-ui@0.4.1\n\n## 0.15.1\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-document@0.0.7\n  - @atproto/lex-resolver@0.0.7\n  - @atproto/common@0.5.5\n\n## 0.15.0\n\n### Minor Changes\n\n- [#4461](https://github.com/bluesky-social/atproto/pull/4461) [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Expose prompt_values_supported in Authorization Server Metadata\n\n- [#4461](https://github.com/bluesky-social/atproto/pull/4461) [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Support initiating user registration via prompt=create\n\n### Patch Changes\n\n- Updated dependencies [[`95ef3c2`](https://github.com/bluesky-social/atproto/commit/95ef3c24e8072e9d49412950b033cb8607764ee0), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41)]:\n  - @atproto/oauth-types@0.6.0\n  - @atproto/oauth-provider-ui@0.4.0\n  - @atproto/lex-resolver@0.0.6\n  - @atproto/common@0.5.4\n  - @atproto/lex-document@0.0.6\n  - @atproto/oauth-provider-api@0.3.5\n  - @atproto/oauth-provider-frontend@0.2.6\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lex-document@0.0.5\n  - @atproto/common@0.5.3\n  - @atproto/lex-resolver@0.0.5\n\n## 0.14.0\n\n### Minor Changes\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `@atproto/lexicon` with `@atproto/lex-document`\n\n- [#4353](https://github.com/bluesky-social/atproto/pull/4353) [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use arrays to represent \"account\" permission's `action` attribute, allowing multiple actions to be specified for that resource.\n\n### Patch Changes\n\n- [#4382](https://github.com/bluesky-social/atproto/pull/4382) [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `toScopes()` utility on `IncludeScope`\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `@atproto/lexicon-resolver` with `@atproto/lex-resolver`\n\n- Updated dependencies [[`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`03a2a4b`](https://github.com/bluesky-social/atproto/commit/03a2a4bb3814ced7ad1d4fe6c94b5348a3bbc097), [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db)]:\n  - @atproto/oauth-provider-ui@0.3.6\n  - @atproto/oauth-scopes@0.3.0\n  - @atproto/lex-document@0.0.4\n  - @atproto/lex-resolver@0.0.4\n  - @atproto/did@0.2.3\n  - @atproto/syntax@0.4.2\n  - @atproto/oauth-types@0.5.2\n  - @atproto/common@0.5.2\n  - @atproto/oauth-provider-frontend@0.2.5\n  - @atproto/oauth-provider-api@0.3.4\n\n## 0.13.5\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common@0.5.0\n  - @atproto/did@0.2.2\n  - @atproto/lexicon@0.5.2\n  - @atproto/lexicon-resolver@0.2.4\n  - @atproto/oauth-scopes@0.2.2\n  - @atproto/oauth-types@0.5.1\n  - @atproto/oauth-provider-ui@0.3.5\n  - @atproto/oauth-provider-api@0.3.3\n  - @atproto/oauth-provider-frontend@0.2.4\n\n## 0.13.4\n\n### Patch Changes\n\n- [#4301](https://github.com/bluesky-social/atproto/pull/4301) [`f496fa2c4`](https://github.com/bluesky-social/atproto/commit/f496fa2c4d9316229523454c691c75c269aba21e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Set dark background on authorization page's `<body>` in dark mode\n\n- Updated dependencies [[`f496fa2c4`](https://github.com/bluesky-social/atproto/commit/f496fa2c4d9316229523454c691c75c269aba21e)]:\n  - @atproto/oauth-provider-ui@0.3.4\n\n## 0.13.3\n\n### Patch Changes\n\n- [#4293](https://github.com/bluesky-social/atproto/pull/4293) [`8c03d75b6`](https://github.com/bluesky-social/atproto/commit/8c03d75b6c11bed15b58bfa7ff4bf68199fc6511) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove un-implemented `introspect` endpoint from OAuth Authorization Server metadata\n\n- [#4265](https://github.com/bluesky-social/atproto/pull/4265) [`1e702ea67`](https://github.com/bluesky-social/atproto/commit/1e702ea675e3697e050be1f28e54bb1298b56436) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `onResetPasswordRequested` and `onResetPasswordConfirmed` hooks to be called after the respective actions are completed.\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto/oauth-types@0.5.0\n  - @atproto-labs/fetch-node@0.2.0\n  - @atproto/oauth-provider-api@0.3.2\n  - @atproto/oauth-provider-frontend@0.2.3\n  - @atproto/oauth-provider-ui@0.3.3\n  - @atproto/lexicon-resolver@0.2.3\n\n## 0.13.2\n\n### Patch Changes\n\n- [#4256](https://github.com/bluesky-social/atproto/pull/4256) [`e71d265dd`](https://github.com/bluesky-social/atproto/commit/e71d265dd4ef35dcd5bb7606b528f417d6af2b70) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error in case of invalid loopback client metadata\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/oauth-types@0.4.2\n  - @atproto/jwk@0.6.0\n  - @atproto/did@0.2.1\n  - @atproto/oauth-provider-api@0.3.1\n  - @atproto/oauth-provider-frontend@0.2.2\n  - @atproto/oauth-provider-ui@0.3.2\n  - @atproto/jwk-jose@0.1.11\n  - @atproto/oauth-scopes@0.2.1\n\n## 0.13.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon-resolver@0.2.2\n\n## 0.13.0\n\n### Minor Changes\n\n- [#4217](https://github.com/bluesky-social/atproto/pull/4217) [`7351589a3`](https://github.com/bluesky-social/atproto/commit/7351589a317ff438c6010154e642a297adb76aa8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `ResetPasswordConfirmData` exported type to `ResetPasswordConfirmInput`.\n\n- [#4217](https://github.com/bluesky-social/atproto/pull/4217) [`7351589a3`](https://github.com/bluesky-social/atproto/commit/7351589a317ff438c6010154e642a297adb76aa8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `ResetPasswordRequestData` exported type to `ResetPasswordRequestInput`.\n\n### Patch Changes\n\n- [#4217](https://github.com/bluesky-social/atproto/pull/4217) [`7351589a3`](https://github.com/bluesky-social/atproto/commit/7351589a317ff438c6010154e642a297adb76aa8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `onResetPasswordRequest` and `onResetPasswordConfirm` hooks\n\n## 0.12.1\n\n### Patch Changes\n\n- [#4191](https://github.com/bluesky-social/atproto/pull/4191) [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `DpopProof` readonly\n\n- [#4191](https://github.com/bluesky-social/atproto/pull/4191) [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing initialization of `onDecodeToken` hook\n\n- [#4191](https://github.com/bluesky-social/atproto/pull/4191) [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve token verification error details\n\n## 0.12.0\n\n### Minor Changes\n\n- [#4149](https://github.com/bluesky-social/atproto/pull/4149) [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `AccessTokenMode` enum values to be more meaningful\n\n- [#4149](https://github.com/bluesky-social/atproto/pull/4149) [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `SignedTokenPayload` to `AccessTokenPayload`\n\n### Patch Changes\n\n- [#4149](https://github.com/bluesky-social/atproto/pull/4149) [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `onCreateToken` and `onDecodeToken` hooks to intercept access token JWT claims encoding/decoding\n\n- Updated dependencies [[`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`d570db43d`](https://github.com/bluesky-social/atproto/commit/d570db43d6df2044dbaa5813cac469b3e73ba219)]:\n  - @atproto/oauth-scopes@0.2.0\n  - @atproto/oauth-provider-frontend@0.2.1\n  - @atproto/oauth-provider-ui@0.3.1\n  - @atproto/lexicon-resolver@0.2.1\n  - @atproto/common@0.4.12\n  - @atproto/lexicon@0.5.1\n\n## 0.11.2\n\n### Patch Changes\n\n- [#4155](https://github.com/bluesky-social/atproto/pull/4155) [`d54d278ab`](https://github.com/bluesky-social/atproto/commit/d54d278abd679fbb44ff795d02b53b7caab31301) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow unexpected error to go through when fetching permission sets\n\n## 0.11.1\n\n### Patch Changes\n\n- Updated dependencies [[`f4cb3e4d0`](https://github.com/bluesky-social/atproto/commit/f4cb3e4d0ac45e567fa14f79b99a84621fa89a56)]:\n  - @atproto/oauth-provider-api@0.3.0\n  - @atproto/oauth-provider-frontend@0.2.0\n  - @atproto/oauth-provider-ui@0.3.0\n\n## 0.11.0\n\n### Minor Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `include:<nsid>` scopes\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/oauth-scopes@0.1.0\n  - @atproto/lexicon-resolver@0.2.0\n  - @atproto/did@0.2.0\n  - @atproto-labs/simple-store@0.3.0\n  - @atproto-labs/fetch-node@0.1.10\n  - @atproto/oauth-provider-frontend@0.1.12\n  - @atproto/oauth-provider-ui@0.2.1\n  - @atproto-labs/simple-store-memory@0.1.4\n\n## 0.10.2\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n  - @atproto/jwk-jose@0.1.10\n  - @atproto/oauth-provider-api@0.2.1\n  - @atproto/oauth-types@0.4.1\n  - @atproto/oauth-provider-frontend@0.1.12\n  - @atproto/oauth-provider-ui@0.2.1\n\n## 0.10.1\n\n### Patch Changes\n\n- [#4100](https://github.com/bluesky-social/atproto/pull/4100) [`832866c33`](https://github.com/bluesky-social/atproto/commit/832866c33b442443c52bc5891840f7527d81f926) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce stronger validation of jwks loaded through their own uri\n\n- Updated dependencies [[`c274bd1b3`](https://github.com/bluesky-social/atproto/commit/c274bd1b38813abd5b287f1c94dca1fd62854918)]:\n  - @atproto/oauth-scopes@0.0.2\n  - @atproto/oauth-provider-ui@0.2.0\n\n## 0.10.0\n\n### Minor Changes\n\n- [#4095](https://github.com/bluesky-social/atproto/pull/4095) [`1dbc7750d`](https://github.com/bluesky-social/atproto/commit/1dbc7750d2bede009a776a44a170a19120b52fc8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for ATProto OAuth scopes\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `onScopeDetails` hook\n\n### Patch Changes\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `onAuthorizationRequest` hook, triggered right before an authorization request is created.\n\n- [#4092](https://github.com/bluesky-social/atproto/pull/4092) [`43fbeda63`](https://github.com/bluesky-social/atproto/commit/43fbeda63e12134e8ebac73b4c2005b0918fc888) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `cookie` dependency\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The scopes allowed in the client metadata document is no longer constrained by the `scopes_supported` in the server metadata.\n\n- Updated dependencies [[`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add), [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add), [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add)]:\n  - @atproto/oauth-scopes@0.0.1\n  - @atproto/oauth-provider-api@0.2.0\n  - @atproto/oauth-provider-ui@0.2.0\n  - @atproto/oauth-provider-frontend@0.1.11\n\n## 0.9.3\n\n### Patch Changes\n\n- [#3973](https://github.com/bluesky-social/atproto/pull/3973) [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve propagation of authorization error codes\n\n- [#3973](https://github.com/bluesky-social/atproto/pull/3973) [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error reporting in case of failed PAR\n\n- [#3953](https://github.com/bluesky-social/atproto/pull/3953) [`f792b9193`](https://github.com/bluesky-social/atproto/commit/f792b919386341d0dc4dcf873506f088af61ae16) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error reporting\n\n- Updated dependencies [[`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a)]:\n  - @atproto/oauth-types@0.4.0\n  - @atproto/oauth-provider-api@0.1.6\n  - @atproto/oauth-provider-frontend@0.1.10\n  - @atproto/oauth-provider-ui@0.1.11\n\n## 0.9.2\n\n### Patch Changes\n\n- [#3967](https://github.com/bluesky-social/atproto/pull/3967) [`68c43a94b`](https://github.com/bluesky-social/atproto/commit/68c43a94bd76dc8040cdff9406cabaf1a484d999) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return `invalid_grant` instead of `invalid_client` when incorrectly authenticated with token endpoint\n\n- [#3967](https://github.com/bluesky-social/atproto/pull/3967) [`68c43a94b`](https://github.com/bluesky-social/atproto/commit/68c43a94bd76dc8040cdff9406cabaf1a484d999) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return `invalid_client` instead of `invalid_grant` when incorrectly authenticated with PAR endpoint\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto/jwk@0.4.0\n  - @atproto/jwk-jose@0.1.9\n  - @atproto/oauth-provider-api@0.1.5\n  - @atproto/oauth-types@0.3.1\n  - @atproto/oauth-provider-frontend@0.1.9\n  - @atproto/oauth-provider-ui@0.1.10\n\n## 0.9.1\n\n### Patch Changes\n\n- [#3811](https://github.com/bluesky-social/atproto/pull/3811) [`7d9808ca8`](https://github.com/bluesky-social/atproto/commit/7d9808ca81dc13efbb9ced56ee2edd1e03966e10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow HTTPS `redirect_uris` from any origin\n\n- [#3883](https://github.com/bluesky-social/atproto/pull/3883) [`e27d90845`](https://github.com/bluesky-social/atproto/commit/e27d90845496e46e2b0e8b362d43881900d7a9e3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Increase oauth session & refresh token lifetimes\n\n## 0.9.0\n\n### Minor Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error reporting in case an invalid `token_endpoint_auth_method` is used in the client metadata document.\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - OAuthProvider `requestStore` option is now required\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure that the credentials used during a refresh correspond to those used to create the OAuth tokens.\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Represent missing client auth with a `null` instead of \"none\" when storing request data.\n\n### Patch Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix a flawed logic preventing the proper error from being propagated upon failed code grant\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Verify the \"aud\" claim of JAR requests\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Verify the presence of a \"kid\" in signed JAR headers\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly authenticate revoke requests by requiring a DPoP proof to better comply with OAuth 2.0 Token Revocation (RFC7009)\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow missing DPoP header during PAR request if `dpop_jkt` is provided\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Protect against concurrent use of request code\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Validate presence of DPoP proofs sooner when processing token requests\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use client attestation's `exp` claim to determine the life time of JWT's `jti` nonce.\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Silently ignore \"Refresh token replayed\" errors when revoking a refresh token that has already been revoked.\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/oauth-types@0.3.0\n  - @atproto/jwk@0.3.0\n  - @atproto/oauth-provider-api@0.1.4\n  - @atproto/oauth-provider-frontend@0.1.8\n  - @atproto/oauth-provider-ui@0.1.9\n  - @atproto/jwk-jose@0.1.8\n\n## 0.8.1\n\n### Patch Changes\n\n- [#3933](https://github.com/bluesky-social/atproto/pull/3933) [`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Normalize and validate `login_hint` in oauth request properties\n\n- Updated dependencies [[`30f851dee`](https://github.com/bluesky-social/atproto/commit/30f851dee8495b5034743fda1c095509f1fd95bf)]:\n  - @atproto/oauth-provider-frontend@0.1.7\n\n## 0.8.0\n\n### Minor Changes\n\n- [#3879](https://github.com/bluesky-social/atproto/pull/3879) [`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve validation of DPoP proofs\n\n### Patch Changes\n\n- [#3879](https://github.com/bluesky-social/atproto/pull/3879) [`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return DPoP validation result from `authenticateRequest`\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c)]:\n  - @atproto/jwk@0.2.0\n  - @atproto/oauth-types@0.2.8\n  - @atproto/jwk-jose@0.1.7\n  - @atproto/oauth-provider-api@0.1.3\n  - @atproto/oauth-provider-frontend@0.1.6\n  - @atproto/oauth-provider-ui@0.1.8\n\n## 0.7.10\n\n### Patch Changes\n\n- Updated dependencies [[`71b9dcda9`](https://github.com/bluesky-social/atproto/commit/71b9dcda9611ab3662ccb2c4e175579396f16b3a)]:\n  - @atproto/oauth-provider-ui@0.1.7\n\n## 0.7.9\n\n### Patch Changes\n\n- [#3900](https://github.com/bluesky-social/atproto/pull/3900) [`06bf684a4`](https://github.com/bluesky-social/atproto/commit/06bf684a4a3fd2b8c73d2729e4951cedca8cba5e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add max length limit to passwords\n\n## 0.7.8\n\n### Patch Changes\n\n- [`d1e3e68dd`](https://github.com/bluesky-social/atproto/commit/d1e3e68dd9eb7bed13d9023bc0e4ce3c448eabf5) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `/.well-known/change-password` endpoint\n\n- Updated dependencies [[`d1e3e68dd`](https://github.com/bluesky-social/atproto/commit/d1e3e68dd9eb7bed13d9023bc0e4ce3c448eabf5)]:\n  - @atproto/oauth-provider-frontend@0.1.5\n  - @atproto/oauth-provider-ui@0.1.6\n\n## 0.7.7\n\n### Patch Changes\n\n- [#3818](https://github.com/bluesky-social/atproto/pull/3818) [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on the Public Suffix List\n\n- [#3820](https://github.com/bluesky-social/atproto/pull/3820) [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `transition:email` oauth scope\n\n- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`36dbd4155`](https://github.com/bluesky-social/atproto/commit/36dbd41551f74052a3f584719a1a7edd86eca201), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4)]:\n  - @atproto-labs/fetch-node@0.1.9\n  - @atproto-labs/pipe@0.1.1\n  - @atproto/oauth-provider-ui@0.1.5\n  - @atproto-labs/fetch@0.2.3\n  - @atproto/oauth-provider-frontend@0.1.4\n\n## 0.7.6\n\n### Patch Changes\n\n- Updated dependencies [[`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8), [`e1bda27e5`](https://github.com/bluesky-social/atproto/commit/e1bda27e550d3ba9dab1fab1f27726c185d8bf9f), [`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8)]:\n  - @atproto/oauth-types@0.2.7\n  - @atproto/oauth-provider-ui@0.1.4\n  - @atproto/oauth-provider-api@0.1.2\n  - @atproto/oauth-provider-frontend@0.1.4\n  - @atproto/common@0.4.11\n\n## 0.7.5\n\n### Patch Changes\n\n- [#3783](https://github.com/bluesky-social/atproto/pull/3783) [`d794b0676`](https://github.com/bluesky-social/atproto/commit/d794b06763050b4b32484e90116461deae45cbe3) Thanks [@devinivy](https://github.com/devinivy)! - Revert \"Use more secure COEP header when hCaptcha is enabled\"\n\n## 0.7.4\n\n### Patch Changes\n\n- Updated dependencies [[`81524fcb0`](https://github.com/bluesky-social/atproto/commit/81524fcb007f12161fd6928badbf176b1568b4b3), [`a70dad5ae`](https://github.com/bluesky-social/atproto/commit/a70dad5aea32ce26d2cca170a06d184935b4865d)]:\n  - @atproto/oauth-provider-ui@0.1.3\n\n## 0.7.3\n\n### Patch Changes\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow customizing contrast and hue colors\n\n- Updated dependencies [[`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`0d77d1b55`](https://github.com/bluesky-social/atproto/commit/0d77d1b550a58117aee8f7f1e2be24d255ade9e4), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a)]:\n  - @atproto/oauth-provider-frontend@0.1.3\n  - @atproto-labs/simple-store@0.2.0\n  - @atproto/oauth-types@0.2.6\n  - @atproto-labs/simple-store-memory@0.1.3\n  - @atproto/oauth-provider-api@0.1.1\n  - @atproto/oauth-provider-ui@0.1.2\n\n## 0.7.2\n\n### Patch Changes\n\n- [#3755](https://github.com/bluesky-social/atproto/pull/3755) [`96de2acb3`](https://github.com/bluesky-social/atproto/commit/96de2acb301683effe4313cb93d7747f87a73b5e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use more secure COEP header when hCaptcha is enabled\n\n- Updated dependencies [[`0f3899dd5`](https://github.com/bluesky-social/atproto/commit/0f3899dd52d0094c29222c65e2636217f9a8ece4)]:\n  - @atproto/oauth-provider-frontend@0.1.2\n\n## 0.7.1\n\n### Patch Changes\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"dependency\" on `rollup-plugin-bundle-manifest`\n\n- Updated dependencies [[`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f), [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f)]:\n  - @atproto/oauth-provider-frontend@0.1.1\n  - @atproto/oauth-provider-ui@0.1.1\n\n## 0.7.0\n\n### Minor Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - OAuthProvider will now always generate JWT access tokens. This will prevent \"leaked\" `tokenId` values from being used as access tokens directly. This change also introduces an `AccessTokenMode` that allows generating \"stateless\" tokens (when the AS and RS are different servers), or shorter \"light\" tokens (that only act as wrapper around `tokenId` values).\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unused `getAuthorizationDetails` hook\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Change name of `onSignupAttempt` hook to `onSignUpAttempt`\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Store & verify new authorization requests against previously approved scopes for the same client\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Split oauth endpoints & authorization page routes from `OAuthProvider`\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix bug allowing to authenticate using previous account even if the \"remember me\" checkbox was left unchecked\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Change \"brand\" color to \"primary\"\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove instrospection endpoint\n\n### Patch Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Security fix: Properly validate JWT `exp` claim when it is zero.\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Always log to console in dev mode\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not return invalid authorization response errors\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply time mitigation strategy on the sensitive part of the operation only.\n\n- Updated dependencies [[`8b98fec88`](https://github.com/bluesky-social/atproto/commit/8b98fec8857aacddeed9efb5c755474951e6d9d4), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e)]:\n  - @atproto/oauth-provider-ui@0.1.0\n  - @atproto/oauth-types@0.2.5\n  - @atproto-labs/rollup-plugin-bundle-manifest@0.2.0\n  - @atproto/oauth-provider-api@0.1.0\n  - @atproto/jwk@0.1.5\n  - @atproto/oauth-provider-frontend@0.1.0\n  - @atproto/jwk-jose@0.1.6\n\n## 0.6.6\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common@0.4.10\n\n## 0.6.5\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/common@0.4.9\n\n## 0.6.4\n\n### Patch Changes\n\n- [#3690](https://github.com/bluesky-social/atproto/pull/3690) [`9b28184cb`](https://github.com/bluesky-social/atproto/commit/9b28184cb9c417173f46cfb5824dc197dec3e069) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose hcaptcha tokens in hook and errors\n\n- [#3690](https://github.com/bluesky-social/atproto/pull/3690) [`9b28184cb`](https://github.com/bluesky-social/atproto/commit/9b28184cb9c417173f46cfb5824dc197dec3e069) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove hcaptcha hostname check\n\n## 0.6.3\n\n### Patch Changes\n\n- [#3688](https://github.com/bluesky-social/atproto/pull/3688) [`98d8a677c`](https://github.com/bluesky-social/atproto/commit/98d8a677ca4671137727d14567c8354c48c9e850) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add debugging info to HCaptcha validation errors\n\n- [#3688](https://github.com/bluesky-social/atproto/pull/3688) [`98d8a677c`](https://github.com/bluesky-social/atproto/commit/98d8a677ca4671137727d14567c8354c48c9e850) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add hook with hcaptcha result\n\n## 0.6.2\n\n### Patch Changes\n\n- [#3681](https://github.com/bluesky-social/atproto/pull/3681) [`a5a760c1f`](https://github.com/bluesky-social/atproto/commit/a5a760c1f0efd7246c9eebbc0f482d2f505de0a1) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow null hostname in hcaptcha result\n\n## 0.6.1\n\n### Patch Changes\n\n- [#3656](https://github.com/bluesky-social/atproto/pull/3656) [`42807cad5`](https://github.com/bluesky-social/atproto/commit/42807cad56786e402d601ef9ed97379d5641a2c6) Thanks [@Johannes-Andersen](https://github.com/Johannes-Andersen)! - hCaptcha error codes should be optional\n\n## 0.6.0\n\n### Minor Changes\n\n- [#3645](https://github.com/bluesky-social/atproto/pull/3645) [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove onSignupHcaptchaResult hook\n\n- [#3645](https://github.com/bluesky-social/atproto/pull/3645) [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `onSignedUp` hook to access hcaptcha result data.\n\n### Patch Changes\n\n- [#3645](https://github.com/bluesky-social/atproto/pull/3645) [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix hcaptcha verification based on score\n\n- [#3627](https://github.com/bluesky-social/atproto/pull/3627) [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix CSP directives for assets loaded through an `src`.\n\n- [#3627](https://github.com/bluesky-social/atproto/pull/3627) [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make CSP header shorter (by combining <script> tags in the backend, when possible)\n\n- [#3627](https://github.com/bluesky-social/atproto/pull/3627) [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disable un-necessary pre-loading of assets\n\n- [#3640](https://github.com/bluesky-social/atproto/pull/3640) [`cc4122652`](https://github.com/bluesky-social/atproto/commit/cc4122652ed42ba55826c019d0ec57bf25df1ecd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Split OAuth Provider's ui into its own package\n\n- [#3627](https://github.com/bluesky-social/atproto/pull/3627) [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fixes issue in internal HTML generation class\n\n- [#3627](https://github.com/bluesky-social/atproto/pull/3627) [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Set `Cross-Origin-Embedder-Policy` to `unsafe-none` when HCaptcha is enabled\n\n- [#3645](https://github.com/bluesky-social/atproto/pull/3645) [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve HCaptcha error reporting\n\n- Updated dependencies [[`cc4122652`](https://github.com/bluesky-social/atproto/commit/cc4122652ed42ba55826c019d0ec57bf25df1ecd), [`cc4122652`](https://github.com/bluesky-social/atproto/commit/cc4122652ed42ba55826c019d0ec57bf25df1ecd), [`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/oauth-provider-ui@0.0.2\n  - @atproto/oauth-provider-api@0.0.1\n  - @atproto/syntax@0.4.0\n\n## 0.5.2\n\n### Patch Changes\n\n- [#3622](https://github.com/bluesky-social/atproto/pull/3622) [`9e3eace8f`](https://github.com/bluesky-social/atproto/commit/9e3eace8f9c22141e6da80b7696cd3b3e7c38779) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly validate handle syntax during sign-up\n\n- [#3621](https://github.com/bluesky-social/atproto/pull/3621) [`5ada66ceb`](https://github.com/bluesky-social/atproto/commit/5ada66ceb9d5b2c64f112bb62da0edc421c765bf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow invite codes in any format\n\n## 0.5.1\n\n### Patch Changes\n\n- [#3611](https://github.com/bluesky-social/atproto/pull/3611) [`c01d7f5d1`](https://github.com/bluesky-social/atproto/commit/c01d7f5d155445d7741c09f91c84af64b31bdbed) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make branding colors optional\n\n- [#3614](https://github.com/bluesky-social/atproto/pull/3614) [`8827ff433`](https://github.com/bluesky-social/atproto/commit/8827ff433a211d2db80840cfc4ee146a7fb44849) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve branding parsing\n\n## 0.5.0\n\n### Minor Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for account sign-up\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for password reset\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly support locales with 3 chars (Asturian)\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for multiple locales\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto-labs/fetch@0.2.2\n  - @atproto/oauth-types@0.2.4\n  - @atproto/jwk@0.1.4\n  - @atproto-labs/fetch-node@0.1.8\n  - @atproto/jwk-jose@0.1.5\n\n## 0.4.0\n\n### Minor Changes\n\n- [#3557](https://github.com/bluesky-social/atproto/pull/3557) [`82d5a2d36`](https://github.com/bluesky-social/atproto/commit/82d5a2d3617c40caab7a18e46c709c4b3c48e7f8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - DeviceManager options can now be passed as argument to the OAuthProvider constructor\n\n- [#3557](https://github.com/bluesky-social/atproto/pull/3557) [`82d5a2d36`](https://github.com/bluesky-social/atproto/commit/82d5a2d3617c40caab7a18e46c709c4b3c48e7f8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update \"trustProxy\" options to allow function\n\n## 0.3.1\n\n### Patch Changes\n\n- [#3538](https://github.com/bluesky-social/atproto/pull/3538) [`bde6f71c4`](https://github.com/bluesky-social/atproto/commit/bde6f71c4cd33022d29da0ff23463a5838c4de24) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Mark \"userAgent\" as optional in `RequestMetadata`\n\n## 0.3.0\n\n### Minor Changes\n\n- [#3525](https://github.com/bluesky-social/atproto/pull/3525) [`6ea9c961a`](https://github.com/bluesky-social/atproto/commit/6ea9c961af964cd9b0d00b5073c695c5e0b3345a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `onClientInfo` and `onAuthorizationDetails` hooks to `getClientInfo` and `getAuthorizationDetails` respectively.\n\n### Patch Changes\n\n- [#3525](https://github.com/bluesky-social/atproto/pull/3525) [`6ea9c961a`](https://github.com/bluesky-social/atproto/commit/6ea9c961af964cd9b0d00b5073c695c5e0b3345a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add the following hooks in the `OAuthProvider`:\n\n  - `onAuthorized` which is triggered when the user \"authorized\" a client (a `code` is issued)\n  - `onTokenCreated` which is triggered when the code is exchanged for a token\n  - `onTokenRefreshed` which is triggered when a refresh token is exchanged for a new access token\n\n- [#3514](https://github.com/bluesky-social/atproto/pull/3514) [`e69e89a03`](https://github.com/bluesky-social/atproto/commit/e69e89a037829bd4f6656d6aa42b77b97b4934e5) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly compute sleep time in contantTime util\n\n## 0.2.17\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto-labs/simple-store-memory@0.1.2\n  - @atproto-labs/simple-store@0.1.2\n  - @atproto-labs/fetch-node@0.1.7\n  - @atproto/oauth-types@0.2.3\n  - @atproto-labs/fetch@0.2.1\n  - @atproto/jwk-jose@0.1.4\n  - @atproto/jwk@0.1.3\n  - @atproto/common@0.4.8\n\n## 0.2.16\n\n### Patch Changes\n\n- Updated dependencies [[`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/common@0.4.7\n\n## 0.2.15\n\n### Patch Changes\n\n- [#3432](https://github.com/bluesky-social/atproto/pull/3432) [`b04943191`](https://github.com/bluesky-social/atproto/commit/b04943191b9f89a5263a77358d47d1362a6454a6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add user friendly description for transition:\\* scopes\n\n## 0.2.14\n\n### Patch Changes\n\n- [#3415](https://github.com/bluesky-social/atproto/pull/3415) [`c5a4cdb0a`](https://github.com/bluesky-social/atproto/commit/c5a4cdb0a52f4583ffe783a0b259e80263f24a8c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error description in case invalid DPoP nonce is used\n\n## 0.2.13\n\n### Patch Changes\n\n- Updated dependencies [[`9c0128193`](https://github.com/bluesky-social/atproto/commit/9c01281931a371304bcfa465005d7363c003bc5f)]:\n  - @atproto-labs/fetch-node@0.1.6\n\n## 0.2.12\n\n### Patch Changes\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`5ece8c6ae`](https://github.com/bluesky-social/atproto/commit/5ece8c6aeab9c5c3f51295d93ed6e27c3c6095c2)]:\n  - @atproto/jwk@0.1.2\n  - @atproto/jwk-jose@0.1.3\n  - @atproto-labs/fetch@0.2.0\n  - @atproto/oauth-types@0.2.2\n  - @atproto-labs/fetch-node@0.1.5\n\n## 0.2.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.6\n\n## 0.2.10\n\n### Patch Changes\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4)]:\n  - @atproto/common@0.4.5\n\n## 0.2.9\n\n### Patch Changes\n\n- [#3135](https://github.com/bluesky-social/atproto/pull/3135) [`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve \"invalid_client_metadata\" error description\n\n- Updated dependencies [[`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f)]:\n  - @atproto-labs/fetch@0.1.2\n  - @atproto-labs/fetch-node@0.1.4\n\n## 0.2.8\n\n### Patch Changes\n\n- Updated dependencies [[`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3), [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3)]:\n  - @atproto/oauth-types@0.2.1\n\n## 0.2.7\n\n### Patch Changes\n\n- [#2852](https://github.com/bluesky-social/atproto/pull/2852) [`709ba3015`](https://github.com/bluesky-social/atproto/commit/709ba301578c1956b8eb0d89bad717615a4fd7ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove response content-encoding logic\n\n## 0.2.6\n\n### Patch Changes\n\n- [#2902](https://github.com/bluesky-social/atproto/pull/2902) [`8f2b80a0d`](https://github.com/bluesky-social/atproto/commit/8f2b80a0dcf118652452ea09764a947b09991e0f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Better report invalid content-encoding errors\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow using different ioredis version\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use fetch()'s \"cache\" option instead of headers to force caching behavior\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error message when invalid client id used during code exchange\n\n- Updated dependencies [[`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf)]:\n  - @atproto/oauth-types@0.2.0\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`80450cbf2`](https://github.com/bluesky-social/atproto/commit/80450cbf2ca27967ee9fe1a5f4bc590b26f1e6b2)]:\n  - @atproto-labs/fetch-node@0.1.3\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies [[`8943c1008`](https://github.com/bluesky-social/atproto/commit/8943c10082702bbc0fc150237c6cc421251afd51)]:\n  - @atproto-labs/fetch-node@0.1.2\n\n## 0.2.3\n\n### Patch Changes\n\n- [#2847](https://github.com/bluesky-social/atproto/pull/2847) [`1226ed268`](https://github.com/bluesky-social/atproto/commit/1226ed2682970a58ae433b9deb11290333988ddd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not display the client_name of untrusted clients\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]:\n  - @atproto/common@0.4.4\n\n## 0.2.2\n\n### Patch Changes\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disable request params scopes defaulting to client metadata scopes. Requires that client always provide a \"scope\" parameter when initiating an oauth flow.\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"plain\" from code_challenge_methods_supported\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Require definition of \"scope\" in client metadata document\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve reporting of metadata validation error\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly validate request_uri request parameter\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce code_challenge_method=S256 request parameter\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Explicitely forbid MTLS client auth method\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return \"invalid_client\" on invalid client credentials\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent use of empty string in unsupported oidc request parameters\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow fetching of source maps files from browser debugger\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow native clients to use https: redirect uris\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow client metadata to contain other values than \"code\"\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve code re-use\n\n- Updated dependencies [[`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/oauth-types@0.1.5\n  - @atproto-labs/fetch-node@0.1.1\n  - @atproto/common@0.4.3\n  - @atproto-labs/fetch@0.1.1\n\n## 0.2.1\n\n### Patch Changes\n\n- [#2749](https://github.com/bluesky-social/atproto/pull/2749) [`c180cf4d8`](https://github.com/bluesky-social/atproto/commit/c180cf4d86d174c24dd640d3c95b8a461c0cd41d) Thanks [@devinivy](https://github.com/devinivy)! - Fix client-side crash on authorize page\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"nonce\" from authorization request\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Mandate the use of \"atproto\" scope\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove \"openid\" compatibility. The reason is that although we were technically \"openid\" compatible, ATProto identifiers are distributed identifiers. When a client relies on OpenID to authenticate users, it will use the auth provider in combination with the identifier to uniquely identify the user. Since ATProto identifiers are meant to be able to move from one provider to the other, OpenID compatibility could break authentication after a user was migrated to a different provider.\n\n  The way OpenID compliant clients would adapt to this particularity would typically be to remove the provider + identifier combination and use the identifier alone. While this is indeed the right way to handle ATProto identifiers, it requires more work to avoid impersonation. In particular, when obtaining a user identifier, the client **must** verify that the issuer of the identity token is indeed the server responsible for that user. This mechanism being not enforced by the OpenID standard, OpenID compatibility could lead to security issues. For this reason, we decided to remove OpenID compatibility from the OAuth provider.\n\n  Note that a trusted central authority could still offer OpenID compatibility by relying on ATProto's regular OAuth flow under the hood. This capability is out of the scope of this library.\n\n### Patch Changes\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Display requested scopes during the auth flow\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Generate proper invalid_authorization_details\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Stronger CORS protections\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not require user consent during oauth flow for first party apps.\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve reporting of validation errors\n\n- Updated dependencies [[`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7)]:\n  - @atproto/oauth-types@0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The non-standard `introspection_endpoint_auth_method`, and `introspection_endpoint_auth_signing_alg` client metadata properties were removed. The client's `token_endpoint_auth_method`, and `token_endpoint_auth_signing_alg` properties are now used as the only indication of how a client must authenticate at the introspection endpoint.\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The non-standard `revocation_endpoint_auth_method`, and `revocation_endpoint_auth_signing_alg` client metadata properties were removed. The client's `token_endpoint_auth_method`, and `token_endpoint_auth_signing_alg` properties are now used as the only indication of how a client must authenticate at the revocation endpoint.\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - The non-standard `pushed_authorization_request_endpoint_auth_method`, and `pushed_authorization_request_endpoint_auth_signing_alg` client metadata properties were removed. The client's `token_endpoint_auth_method`, and `token_endpoint_auth_signing_alg` properties are now used as the only indication of how a client must authenticate at the introspection endpoint.\n\n- [#2728](https://github.com/bluesky-social/atproto/pull/2728) [`5131b027f`](https://github.com/bluesky-social/atproto/commit/5131b027f019cf9f8ec47605648063ae1857f1e3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow charset in content-type header of incoming requests\n\n- [#2727](https://github.com/bluesky-social/atproto/pull/2727) [`3ebcd4e61`](https://github.com/bluesky-social/atproto/commit/3ebcd4e6161291d3649d7f8a9c5ee4ac26d590a2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Do not require \"exp\" claim in dpop proof\n\n- Updated dependencies [[`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb)]:\n  - @atproto/oauth-types@0.1.3\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unused file\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/jwk-jose@0.1.2\n  - @atproto/oauth-types@0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add 2FA support\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/oauth-types@0.1.1\n  - @atproto/jwk-jose@0.1.1\n  - @atproto/jwk@0.1.1\n  - @atproto-labs/simple-store@0.1.1\n  - @atproto-labs/simple-store-memory@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto-labs/simple-store-memory@0.1.0\n  - @atproto-labs/simple-store@0.1.0\n  - @atproto-labs/fetch-node@0.1.0\n  - @atproto/oauth-types@0.1.0\n  - @atproto-labs/fetch@0.1.0\n  - @atproto/jwk-jose@0.1.0\n  - @atproto-labs/pipe@0.1.0\n  - @atproto/jwk@0.1.0\n"
  },
  {
    "path": "packages/oauth/oauth-provider/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-provider\",\n  \"version\": \"0.15.14\",\n  \"license\": \"MIT\",\n  \"description\": \"Generic OAuth2 and OpenID Connect provider for Node.js. Currently only supports features needed for Atproto.\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"oauth2\",\n    \"open id connect\",\n    \"oidc\",\n    \"provider\",\n    \"oidc provider\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-provider\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch\": \"workspace:^\",\n    \"@atproto-labs/fetch-node\": \"workspace:^\",\n    \"@atproto-labs/pipe\": \"workspace:^\",\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto-labs/simple-store-memory\": \"workspace:^\",\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/jwk\": \"workspace:^\",\n    \"@atproto/jwk-jose\": \"workspace:^\",\n    \"@atproto/lex-document\": \"workspace:^\",\n    \"@atproto/lex-resolver\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\",\n    \"@atproto/oauth-provider-api\": \"workspace:*\",\n    \"@atproto/oauth-provider-frontend\": \"workspace:*\",\n    \"@atproto/oauth-provider-ui\": \"workspace:*\",\n    \"@atproto/oauth-scopes\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@hapi/accept\": \"^6.0.3\",\n    \"@hapi/address\": \"^5.1.1\",\n    \"@hapi/bourne\": \"^3.0.0\",\n    \"@hapi/content\": \"^6.0.0\",\n    \"cookie\": \"^0.7.0\",\n    \"disposable-email-domains-js\": \"^1.5.0\",\n    \"forwarded\": \"^0.2.0\",\n    \"http-errors\": \"^2.0.0\",\n    \"ioredis\": \"^5.3.2\",\n    \"jose\": \"^5.2.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@atproto-labs/rollup-plugin-bundle-manifest\": \"workspace:^\",\n    \"@types/cookie\": \"^0.6.0\",\n    \"@types/forwarded\": \"0.1.3\",\n    \"@types/http-errors\": \"^2.0.4\",\n    \"@types/send\": \"^0.17.4\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/access-token/access-token-mode.ts",
    "content": "export enum AccessTokenMode {\n  stateless = 'stateless',\n  stateful = 'stateful',\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/account/account-manager.ts",
    "content": "import {\n  OAuthIssuerIdentifier,\n  isOAuthClientIdLoopback,\n} from '@atproto/oauth-types'\nimport { Client } from '../client/client.js'\nimport { DeviceId } from '../device/device-id.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { HCaptchaClient, HcaptchaVerifyResult } from '../lib/hcaptcha.js'\nimport { constantTime } from '../lib/util/time.js'\nimport { OAuthHooks, RequestMetadata } from '../oauth-hooks.js'\nimport { Customization } from '../oauth-provider.js'\nimport { Sub } from '../oidc/sub.js'\nimport {\n  Account,\n  AccountStore,\n  AuthorizedClientData,\n  DeviceAccount,\n  ResetPasswordConfirmInput,\n  ResetPasswordRequestInput,\n  SignUpData,\n} from './account-store.js'\nimport { SignInData } from './sign-in-data.js'\nimport { SignUpInput } from './sign-up-input.js'\n\nconst TIMING_ATTACK_MITIGATION_DELAY = 400\nconst BRUTE_FORCE_MITIGATION_DELAY = 300\n\nexport class AccountManager {\n  protected readonly inviteCodeRequired: boolean\n  protected readonly hcaptchaClient?: HCaptchaClient\n\n  constructor(\n    issuer: OAuthIssuerIdentifier,\n    protected readonly store: AccountStore,\n    protected readonly hooks: OAuthHooks,\n    customization: Customization,\n  ) {\n    this.inviteCodeRequired = customization.inviteCodeRequired !== false\n    this.hcaptchaClient = customization.hcaptcha\n      ? new HCaptchaClient(new URL(issuer).hostname, customization.hcaptcha)\n      : undefined\n  }\n\n  protected async processHcaptchaToken(\n    input: SignUpInput,\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n  ): Promise<HcaptchaVerifyResult | undefined> {\n    if (!this.hcaptchaClient) {\n      return undefined\n    }\n\n    if (!input.hcaptchaToken) {\n      throw new InvalidRequestError('hCaptcha token is required')\n    }\n\n    const tokens = this.hcaptchaClient.buildClientTokens(\n      deviceMetadata.ipAddress,\n      input.handle,\n      deviceMetadata.userAgent,\n    )\n\n    const result = await this.hcaptchaClient\n      .verify('signup', input.hcaptchaToken, deviceMetadata.ipAddress, tokens)\n      .catch((err) => {\n        throw InvalidRequestError.from(err, 'hCaptcha verification failed')\n      })\n\n    await this.hooks.onHcaptchaResult?.call(null, {\n      input,\n      deviceId,\n      deviceMetadata,\n      tokens,\n      result,\n    })\n\n    try {\n      this.hcaptchaClient.checkVerifyResult(result, tokens)\n    } catch (err) {\n      throw InvalidRequestError.from(err, 'hCaptcha verification failed')\n    }\n\n    return result\n  }\n\n  protected async enforceInviteCode(\n    input: SignUpInput,\n    _deviceId: DeviceId,\n    _deviceMetadata: RequestMetadata,\n  ): Promise<string | undefined> {\n    if (!this.inviteCodeRequired) {\n      return undefined\n    }\n\n    if (!input.inviteCode) {\n      throw new InvalidRequestError('Invite code is required')\n    }\n\n    return input.inviteCode\n  }\n\n  protected async buildSignupData(\n    input: SignUpInput,\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n  ): Promise<SignUpData> {\n    const [hcaptchaResult, inviteCode] = await Promise.all([\n      this.processHcaptchaToken(input, deviceId, deviceMetadata),\n      this.enforceInviteCode(input, deviceId, deviceMetadata),\n    ])\n\n    return { ...input, hcaptchaResult, inviteCode }\n  }\n\n  public async createAccount(\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n    input: SignUpInput,\n  ): Promise<Account> {\n    await this.hooks.onSignUpAttempt?.call(null, {\n      input,\n      deviceId,\n      deviceMetadata,\n    })\n\n    const data = await this.buildSignupData(input, deviceId, deviceMetadata)\n\n    // Mitigation against brute forcing email of users.\n    // @TODO Add rate limit to all the OAuth routes.\n    const account = await constantTime(\n      BRUTE_FORCE_MITIGATION_DELAY,\n      async () => {\n        return this.store.createAccount(data)\n      },\n    ).catch((err) => {\n      throw InvalidRequestError.from(err, 'Account creation failed')\n    })\n\n    try {\n      await this.hooks.onSignedUp?.call(null, {\n        data,\n        account,\n        deviceId,\n        deviceMetadata,\n      })\n\n      return account\n    } catch (err) {\n      await this.removeDeviceAccount(deviceId, account.sub)\n\n      throw InvalidRequestError.from(\n        err,\n        'The account was successfully created but something went wrong, try signing-in.',\n      )\n    }\n  }\n\n  public async authenticateAccount(\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n    data: SignInData,\n  ): Promise<Account> {\n    try {\n      await this.hooks.onSignInAttempt?.call(null, {\n        data,\n        deviceId,\n        deviceMetadata,\n      })\n\n      const account = await constantTime(\n        TIMING_ATTACK_MITIGATION_DELAY,\n        async () => {\n          return this.store.authenticateAccount(data)\n        },\n      )\n\n      await this.hooks.onSignedIn?.call(null, {\n        data,\n        account,\n        deviceId,\n        deviceMetadata,\n      })\n\n      return account\n    } catch (err) {\n      throw InvalidRequestError.from(\n        err,\n        'Unable to sign-in due to an unexpected server error',\n      )\n    }\n  }\n\n  public async upsertDeviceAccount(\n    deviceId: DeviceId,\n    sub: Sub,\n  ): Promise<void> {\n    await this.store.upsertDeviceAccount(deviceId, sub)\n  }\n\n  public async getDeviceAccount(\n    deviceId: DeviceId,\n    sub: Sub,\n  ): Promise<DeviceAccount> {\n    const deviceAccount = await this.store.getDeviceAccount(deviceId, sub)\n    if (!deviceAccount) throw new InvalidRequestError(`Account not found`)\n\n    return deviceAccount\n  }\n\n  public async setAuthorizedClient(\n    account: Account,\n    client: Client,\n    data: AuthorizedClientData,\n  ): Promise<void> {\n    // \"Loopback\" clients are not distinguishable from one another.\n    if (isOAuthClientIdLoopback(client.id)) return\n\n    await this.store.setAuthorizedClient(account.sub, client.id, data)\n  }\n\n  public async getAccount(sub: Sub) {\n    return this.store.getAccount(sub)\n  }\n\n  public async removeDeviceAccount(deviceId: DeviceId, sub: Sub) {\n    return this.store.removeDeviceAccount(deviceId, sub)\n  }\n\n  public async listDeviceAccounts(\n    deviceId: DeviceId,\n  ): Promise<DeviceAccount[]> {\n    const deviceAccounts = await this.store.listDeviceAccounts({\n      deviceId,\n    })\n\n    return deviceAccounts // Fool proof\n      .filter((deviceAccount) => deviceAccount.deviceId === deviceId)\n  }\n\n  public async listAccountDevices(sub: Sub): Promise<DeviceAccount[]> {\n    const deviceAccounts = await this.store.listDeviceAccounts({\n      sub,\n    })\n\n    return deviceAccounts // Fool proof\n      .filter((deviceAccount) => deviceAccount.account.sub === sub)\n  }\n\n  public async resetPasswordRequest(\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n    input: ResetPasswordRequestInput,\n  ) {\n    await this.hooks.onResetPasswordRequest?.call(null, {\n      input,\n      deviceId,\n      deviceMetadata,\n    })\n\n    return constantTime(TIMING_ATTACK_MITIGATION_DELAY, async () => {\n      const account = await this.store.resetPasswordRequest(input)\n\n      if (!account) {\n        return // Silently ignore to prevent user enumeration\n      }\n\n      await this.hooks.onResetPasswordRequested?.call(null, {\n        input,\n        deviceId,\n        deviceMetadata,\n        account,\n      })\n    })\n  }\n\n  public async resetPasswordConfirm(\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n    input: ResetPasswordConfirmInput,\n  ) {\n    await this.hooks.onResetPasswordConfirm?.call(null, {\n      input,\n      deviceId,\n      deviceMetadata,\n    })\n\n    return constantTime(TIMING_ATTACK_MITIGATION_DELAY, async () => {\n      const account = await this.store.resetPasswordConfirm(input)\n\n      if (!account) {\n        throw new InvalidRequestError('Invalid token')\n      }\n\n      await this.hooks.onResetPasswordConfirmed?.call(null, {\n        input,\n        deviceId,\n        deviceMetadata,\n        account,\n      })\n    })\n  }\n\n  public async verifyHandleAvailability(handle: string): Promise<void> {\n    return constantTime(TIMING_ATTACK_MITIGATION_DELAY, async () => {\n      return this.store.verifyHandleAvailability(handle)\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/account/account-store.ts",
    "content": "import {\n  Account,\n  ConfirmResetPasswordInput,\n  InitiatePasswordResetInput,\n} from '@atproto/oauth-provider-api'\nimport { OAuthScope } from '@atproto/oauth-types'\nimport { ClientId } from '../client/client-id.js'\nimport { DeviceId } from '../device/device-id.js'\nimport { DeviceData } from '../device/device-store.js'\nimport { HcaptchaVerifyResult } from '../lib/hcaptcha.js'\nimport { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport {\n  HandleUnavailableError,\n  InvalidRequestError,\n  SecondAuthenticationFactorRequiredError,\n} from '../oauth-errors.js'\nimport { Sub } from '../oidc/sub.js'\nimport { InviteCode } from '../types/invite-code.js'\nimport { SignUpInput } from './sign-up-input.js'\n\n// Export all types needed to implement the AccountStore interface\n\nexport * from '../client/client-id.js'\nexport * from '../device/device-data.js'\nexport * from '../device/device-id.js'\nexport * from '../oidc/sub.js'\nexport * from '../request/request-id.js'\n\nexport type {\n  Account,\n  HcaptchaVerifyResult,\n  InviteCode,\n  OAuthScope,\n  SignUpInput,\n}\n\nexport {\n  HandleUnavailableError,\n  InvalidRequestError,\n  SecondAuthenticationFactorRequiredError,\n}\n\nexport type ResetPasswordRequestInput = InitiatePasswordResetInput\nexport type ResetPasswordConfirmInput = ConfirmResetPasswordInput\n\nexport type CreateAccountData = {\n  locale: string\n  email: string\n  password: string\n  handle: string\n  inviteCode?: string | undefined\n}\n\nexport type AuthenticateAccountData = {\n  locale: string\n  password: string\n  username: string\n  emailOtp?: string | undefined\n}\n\nexport type AuthorizedClientData = { authorizedScopes: readonly string[] }\nexport type AuthorizedClients = Map<ClientId, AuthorizedClientData>\n\nexport type DeviceAccount = {\n  deviceId: DeviceId\n\n  /**\n   * The data associated with the device, created through the\n   * {@link DeviceStore}. This data is used to identify devices on which a user\n   * has logged in.\n   */\n  deviceData: DeviceData\n\n  /**\n   * The account associated with the device account.\n   */\n  account: Account\n\n  /**\n   * The list of clients that are authorized by the account, as created through\n   * the {@link AccountStore.setAuthorizedClient} method.\n   */\n  authorizedClients: AuthorizedClients\n\n  /**\n   * The date at which the device account was created. This value is currently\n   * not used.\n   */\n  createdAt: Date\n\n  /**\n   * The date at which the device account was last updated. This value is used\n   * to determine the date at which the user last authenticated on a device\n   */\n  updatedAt: Date\n}\n\nexport type SignUpData = SignUpInput & {\n  hcaptchaResult?: HcaptchaVerifyResult\n  inviteCode?: InviteCode\n}\n\nexport interface AccountStore {\n  /**\n   * @throws {HandleUnavailableError} - To indicate that the handle is already taken\n   * @throws {InvalidRequestError} - To indicate that some data is invalid\n   */\n  createAccount(data: CreateAccountData): Awaitable<Account>\n\n  /**\n   * @throws {InvalidRequestError} - When the credentials are not valid\n   * @throws {SecondAuthenticationFactorRequiredError} - To indicate that an {@link SecondAuthenticationFactorRequiredError.type} is required in the credentials\n   */\n  authenticateAccount(data: AuthenticateAccountData): Awaitable<Account>\n\n  /**\n   * Add a client & scopes to the list of authorized clients for the given account.\n   */\n  setAuthorizedClient(\n    sub: Sub,\n    clientId: ClientId,\n    data: AuthorizedClientData,\n  ): Awaitable<void>\n\n  /**\n   * @throws {InvalidRequestError} - When the credentials are not valid\n   */\n  getAccount(sub: Sub): Awaitable<{\n    account: Account\n    authorizedClients: AuthorizedClients\n  }>\n\n  /**\n   * @param data.requestId - If provided, the inserted account must be bound to\n   * that particular requestId.\n   *\n   * @note Whenever a particular device account is created, all **unbound**\n   * device accounts for the same `deviceId` & `sub` should be deleted.\n   *\n   * @note When a particular request is deleted (through\n   * {@link RequestStore.deleteRequest}), all accounts bound to that request\n   * should be deleted as well.\n   */\n  upsertDeviceAccount(deviceId: DeviceId, sub: Sub): Awaitable<void>\n\n  /**\n   * @param requestId - If provided, the result must either have the same\n   * requestId, or not be bound to a particular requestId. If `null`, the\n   * result must not be bound to a particular requestId.\n   * @throws {InvalidRequestError} - Instead of returning `null` in order to\n   * provide a custom error message\n   */\n  getDeviceAccount(\n    deviceId: DeviceId,\n    sub: Sub,\n  ): Awaitable<DeviceAccount | null>\n\n  /**\n   * Removes *all* the unbound device-accounts associated with the given device\n   * & account.\n   *\n   * @note Noop if the device-account is not found.\n   */\n  removeDeviceAccount(deviceId: DeviceId, sub: Sub): Awaitable<void>\n\n  /**\n   * @returns **all** the device accounts that match the {@link requestId}\n   * criteria and given {@link filter}.\n   */\n  listDeviceAccounts(\n    filter: { sub: Sub } | { deviceId: DeviceId },\n  ): Awaitable<DeviceAccount[]>\n\n  resetPasswordRequest(\n    data: ResetPasswordRequestInput,\n  ): Awaitable<null | Account>\n\n  resetPasswordConfirm(\n    data: ResetPasswordConfirmInput,\n  ): Awaitable<null | Account>\n\n  /**\n   * @throws {HandleUnavailableError} - To indicate that the handle is already taken\n   */\n  verifyHandleAvailability(handle: string): Awaitable<void>\n}\n\nexport const isAccountStore = buildInterfaceChecker<AccountStore>([\n  'createAccount',\n  'authenticateAccount',\n  'setAuthorizedClient',\n  'getAccount',\n  'upsertDeviceAccount',\n  'getDeviceAccount',\n  'removeDeviceAccount',\n  'listDeviceAccounts',\n  'resetPasswordRequest',\n  'resetPasswordConfirm',\n  'verifyHandleAvailability',\n])\n\nexport function asAccountStore<V>(implementation: V): V & AccountStore {\n  if (!implementation || !isAccountStore(implementation)) {\n    throw new Error('Invalid AccountStore implementation')\n  }\n  return implementation\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/account/sign-in-data.ts",
    "content": "import { z } from 'zod'\nimport { localeSchema } from '../lib/util/locale.js'\nimport { emailOtpSchema } from '../types/email-otp.js'\nimport { newPasswordSchema, oldPasswordSchema } from '../types/password.js'\n\nexport const signInDataSchema = z\n  .object({\n    locale: localeSchema,\n    username: z.string(),\n    password: z.union([oldPasswordSchema, newPasswordSchema]),\n    emailOtp: emailOtpSchema.optional(),\n  })\n  .strict()\n\nexport type SignInData = z.output<typeof signInDataSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/account/sign-up-input.ts",
    "content": "import { z } from 'zod'\nimport { hcaptchaTokenSchema } from '../lib/hcaptcha.js'\nimport { localeSchema } from '../lib/util/locale.js'\nimport { emailSchema } from '../types/email.js'\nimport { handleSchema } from '../types/handle.js'\nimport { inviteCodeSchema } from '../types/invite-code.js'\nimport { newPasswordSchema } from '../types/password.js'\n\nexport const signUpInputSchema = z\n  .object({\n    locale: localeSchema,\n    handle: handleSchema,\n    email: emailSchema,\n    password: newPasswordSchema,\n    inviteCode: inviteCodeSchema.optional(),\n    hcaptchaToken: hcaptchaTokenSchema.optional(),\n  })\n  .strict()\n\nexport type SignUpInput = z.output<typeof signUpInputSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-auth.ts",
    "content": "import { CLIENT_ASSERTION_TYPE_JWT_BEARER } from '@atproto/oauth-types'\n\nexport type ClientAuth =\n  | { method: 'none' }\n  | {\n      method: 'private_key_jwt'\n\n      /**\n       * Algorithm used for client authentication.\n       *\n       * @note We could allow clients to use a different algorithm over time\n       * (e.g. because new safer algorithms become available). For now, we\n       * require that the algorithm remains the same, as it is a bad practice to\n       * use the same key for different purposes.\n       */\n      alg: string\n\n      /**\n       * ID of the key that was used for client authentication.\n       *\n       * @note The most important thing to validate is that the actual key didn't change (which is )\n       */\n      kid: string\n\n      /**\n       * Thumbprint of the key used for client authentication. This value must\n       * be the same during token refreshes as the thumbprint of the key used\n       * during initial token issuance.\n       *\n       * @note This value is computed by the AS to ensure that the key used for\n       * client auth does not change\n       */\n      jkt: string\n\n      /**\n       * Nonce used to prevent replay attacks. This value is generated by the\n       * client when generating it's assertion JWT and must be unique for each\n       * request.\n       *\n       * @see {@link https://www.rfc-editor.org/rfc/rfc7523.html#section-3}\n       */\n      jti: string\n\n      /**\n       * \"exp\" (expiration time) claim that limits the time window during which\n       * the JWT can be used.\n       *\n       * @note This field is optional for legacy reasons.\n       */\n      exp?: number\n    }\n\n/**\n * @note In its previous version, the code was storing the\n * \"client_assertion_type\" instead of the authentication method, which was\n * confusing and prevented proper comparison with the client's\n * \"token_endpoint_auth_method\" metadata.\n */\nexport type ClientAuthLegacy = {\n  method: typeof CLIENT_ASSERTION_TYPE_JWT_BEARER\n  alg: string\n  kid: string\n  jkt: string\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-data.ts",
    "content": "import { Jwks } from '@atproto/jwk'\nimport { OAuthClientMetadata } from '@atproto/oauth-types'\n\nexport type { OAuthClientMetadata }\n\nexport type ClientData = {\n  metadata: OAuthClientMetadata\n  jwks?: Jwks\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-id.ts",
    "content": "import { OAuthClientId, oauthClientIdSchema } from '@atproto/oauth-types'\n\nexport type ClientId = OAuthClientId\nexport const clientIdSchema = oauthClientIdSchema\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-info.ts",
    "content": "export type ClientInfo = {\n  /**\n   * Defaults to `false`\n   */\n  isFirstParty: boolean\n\n  /**\n   * Defaults to `true` if the client is isFirstParty, or if the client was\n   * loaded from the store. (i.e. false in case of \"loopback\" & \"discoverable\"\n   * clients)\n   */\n  isTrusted: boolean\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-manager.ts",
    "content": "import { Jwks, Keyset, jwksPubSchema } from '@atproto/jwk'\nimport {\n  OAuthAuthorizationServerMetadata,\n  OAuthClientIdDiscoverable,\n  OAuthClientIdLoopback,\n  OAuthClientMetadata,\n  OAuthClientMetadataInput,\n  isLocalHostname,\n  isOAuthClientIdDiscoverable,\n  isOAuthClientIdLoopback,\n  oauthClientMetadataSchema,\n} from '@atproto/oauth-types'\nimport {\n  Fetch,\n  bindFetch,\n  fetchJsonProcessor,\n  fetchJsonZodProcessor,\n  fetchOkProcessor,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\nimport {\n  CachedGetter,\n  GetCachedOptions,\n  SimpleStore,\n} from '@atproto-labs/simple-store'\nimport { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'\nimport { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'\nimport { callAsync } from '../lib/util/function.js'\nimport { Awaitable } from '../lib/util/type.js'\nimport { OAuthHooks } from '../oauth-hooks.js'\nimport { ClientId } from './client-id.js'\nimport { ClientStore } from './client-store.js'\nimport { parseDiscoverableClientId, parseRedirectUri } from './client-utils.js'\nimport { Client } from './client.js'\n\nconst fetchMetadataHandler = pipe(\n  fetchOkProcessor(),\n  // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html#section-4.1\n  fetchJsonProcessor('application/json', true),\n  fetchJsonZodProcessor(oauthClientMetadataSchema),\n)\n\nconst fetchJwksHandler = pipe(\n  fetchOkProcessor(),\n  fetchJsonProcessor('application/json', false),\n  fetchJsonZodProcessor(jwksPubSchema),\n)\n\nexport type LoopbackMetadataGetter = (\n  url: string,\n) => Awaitable<OAuthClientMetadataInput>\n\nexport class ClientManager {\n  protected readonly jwks: CachedGetter<string, Jwks>\n  protected readonly metadataGetter: CachedGetter<string, OAuthClientMetadata>\n\n  constructor(\n    protected readonly serverMetadata: OAuthAuthorizationServerMetadata,\n    protected readonly keyset: Keyset,\n    protected readonly hooks: OAuthHooks,\n    protected readonly store: ClientStore | null,\n    protected readonly loopbackMetadata: LoopbackMetadataGetter | null = null,\n    safeFetch: Fetch,\n    clientJwksCache: SimpleStore<string, Jwks>,\n    clientMetadataCache: SimpleStore<string, OAuthClientMetadata>,\n  ) {\n    const fetch = bindFetch(safeFetch)\n\n    this.jwks = new CachedGetter(async (uri, options) => {\n      const jwks = await fetch(buildJsonGetRequest(uri, options)).then(\n        fetchJwksHandler,\n      )\n\n      return jwks\n    }, clientJwksCache)\n\n    this.metadataGetter = new CachedGetter(async (uri, options) => {\n      const metadata = await fetch(buildJsonGetRequest(uri, options)).then(\n        fetchMetadataHandler,\n      )\n\n      // Validate within the getter to avoid caching invalid metadata\n      return this.validateClientMetadata(uri, metadata)\n    }, clientMetadataCache)\n  }\n\n  /**\n   *\n   * @see {@link https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2 OIDC Client Registration}\n   */\n  public async getClient(clientId: ClientId) {\n    const metadata = await this.getClientMetadata(clientId).catch((err) => {\n      throw InvalidClientMetadataError.from(\n        err,\n        `Unable to obtain client metadata for \"${clientId}\"`,\n      )\n    })\n\n    const jwks = metadata.jwks_uri\n      ? await this.jwks.get(metadata.jwks_uri).catch((err) => {\n          throw InvalidClientMetadataError.from(\n            err,\n            `Unable to obtain jwks from \"${metadata.jwks_uri}\" for \"${clientId}\"`,\n          )\n        })\n      : undefined\n\n    const partialInfo = await callAsync(this.hooks.getClientInfo, clientId, {\n      metadata,\n      jwks,\n    }).catch((err) => {\n      throw InvalidClientMetadataError.from(\n        err,\n        `Rejected client information for \"${clientId}\"`,\n      )\n    })\n\n    const isFirstParty = partialInfo?.isFirstParty ?? false\n    const isTrusted = partialInfo?.isTrusted ?? isFirstParty\n\n    return new Client(clientId, metadata, jwks, { isFirstParty, isTrusted })\n  }\n\n  public async loadClients(\n    clientIds: Iterable<ClientId>,\n    {\n      onError = (err) => {\n        throw err\n      },\n    }: {\n      onError?: (\n        err: unknown,\n        clientId: ClientId,\n      ) => Awaitable<Client | null | undefined>\n    } = {},\n  ): Promise<Map<ClientId, Client>> {\n    // Make sure we don't load the same client multiple times\n    const uniqueClientIds =\n      clientIds instanceof Set ? clientIds : new Set(clientIds)\n\n    // Load all (unique) clients in parallel\n    const clients = await Promise.all(\n      Array.from(uniqueClientIds, async (clientId) =>\n        this.getClient(clientId).catch((err) => onError(err, clientId)),\n      ),\n    )\n\n    // Return a map for easy lookups\n    return new Map(\n      clients\n        .filter((c) => c != null && c instanceof Client)\n        .map((c) => [c.id, c]),\n    )\n  }\n\n  protected async getClientMetadata(\n    clientId: ClientId,\n  ): Promise<OAuthClientMetadata> {\n    if (isOAuthClientIdLoopback(clientId)) {\n      return this.getLoopbackClientMetadata(clientId)\n    } else if (isOAuthClientIdDiscoverable(clientId)) {\n      return this.getDiscoverableClientMetadata(clientId)\n    } else if (this.store) {\n      return this.getStoredClientMetadata(clientId)\n    }\n\n    throw new InvalidClientMetadataError(`Invalid client ID \"${clientId}\"`)\n  }\n\n  protected async getLoopbackClientMetadata(\n    clientId: OAuthClientIdLoopback,\n  ): Promise<OAuthClientMetadata> {\n    const { loopbackMetadata } = this\n    if (!loopbackMetadata) {\n      throw new InvalidClientMetadataError('Loopback clients are not allowed')\n    }\n\n    const metadataRaw = await callAsync(loopbackMetadata, clientId).catch(\n      (err) => {\n        throw InvalidClientMetadataError.from(\n          err,\n          `Invalid loopback client id \"${clientId}\"`,\n        )\n      },\n    )\n\n    const metadata = await oauthClientMetadataSchema\n      .parseAsync(metadataRaw)\n      .catch((err) => {\n        throw InvalidClientMetadataError.from(\n          err,\n          `Invalid loopback client metadata for \"${clientId}\"`,\n        )\n      })\n\n    return this.validateClientMetadata(clientId, metadata)\n  }\n\n  protected async getDiscoverableClientMetadata(\n    clientId: OAuthClientIdDiscoverable,\n  ): Promise<OAuthClientMetadata> {\n    const metadataUrl = parseDiscoverableClientId(clientId)\n\n    const metadata = await this.metadataGetter.get(metadataUrl.href)\n\n    // Note: we do *not* re-validate the metadata here, as the metadata is\n    // validated within the getter. This is to avoid double validation.\n    //\n    // return this.validateClientMetadata(metadataUrl.href, metadata)\n    return metadata\n  }\n\n  protected async getStoredClientMetadata(\n    clientId: ClientId,\n  ): Promise<OAuthClientMetadata> {\n    if (this.store) {\n      const metadata = await this.store.findClient(clientId)\n      return this.validateClientMetadata(clientId, metadata)\n    }\n\n    throw new InvalidClientMetadataError(`Invalid client ID \"${clientId}\"`)\n  }\n\n  /**\n   * This method will ensure that the client metadata is valid w.r.t. the OAuth\n   * and OIDC specifications. It will also ensure that the metadata is\n   * compatible with the implementation of this library, and ATPROTO's\n   * requirements.\n   */\n  protected validateClientMetadata(\n    clientId: ClientId,\n    metadata: OAuthClientMetadata,\n  ): OAuthClientMetadata {\n    // @TODO This method should only check for rules that are specific to this\n    // implementation or the ATPROTO specification. All generic validation rules\n    // should be moved to the @atproto/oauth-types package.\n\n    if (metadata.jwks && metadata.jwks_uri) {\n      throw new InvalidClientMetadataError(\n        'jwks_uri and jwks are mutually exclusive',\n      )\n    }\n\n    // Known OIDC specific parameters\n    for (const k of [\n      'default_max_age',\n      'userinfo_signed_response_alg',\n      'id_token_signed_response_alg',\n      'userinfo_encrypted_response_alg',\n    ] as const) {\n      if (metadata[k] != null) {\n        throw new InvalidClientMetadataError(`Unsupported \"${k}\" parameter`)\n      }\n    }\n\n    const clientUriUrl = metadata.client_uri\n      ? new URL(metadata.client_uri)\n      : null\n\n    if (clientUriUrl && isLocalHostname(clientUriUrl.hostname)) {\n      throw new InvalidClientMetadataError('client_uri hostname is invalid')\n    }\n\n    const scopes = metadata.scope?.split(' ')\n\n    if (!scopes) {\n      throw new InvalidClientMetadataError('Missing scope property')\n    }\n\n    if (!scopes.includes('atproto')) {\n      throw new InvalidClientMetadataError('Missing \"atproto\" scope')\n    }\n\n    const dupScope = scopes?.find(isDuplicate)\n    if (dupScope) {\n      throw new InvalidClientMetadataError(`Duplicate scope \"${dupScope}\"`)\n    }\n\n    const dupGrantType = metadata.grant_types.find(isDuplicate)\n    if (dupGrantType) {\n      throw new InvalidClientMetadataError(\n        `Duplicate grant type \"${dupGrantType}\"`,\n      )\n    }\n\n    for (const grantType of metadata.grant_types) {\n      switch (grantType) {\n        case 'implicit':\n          // Never allowed (unsafe)\n          throw new InvalidClientMetadataError(\n            `Grant type \"${grantType}\" is not allowed`,\n          )\n\n        // @TODO Add support (e.g. for first party client)\n        // case 'client_credentials':\n        // case 'password':\n        case 'authorization_code':\n        case 'refresh_token':\n          if (!this.serverMetadata.grant_types_supported?.includes(grantType)) {\n            throw new InvalidClientMetadataError(\n              `Unsupported grant type \"${grantType}\"`,\n            )\n          }\n          break\n\n        default:\n          throw new InvalidClientMetadataError(\n            `Grant type \"${grantType}\" is not supported`,\n          )\n      }\n    }\n\n    if (metadata.client_id && metadata.client_id !== clientId) {\n      throw new InvalidClientMetadataError('client_id does not match')\n    }\n\n    if (metadata.subject_type && metadata.subject_type !== 'public') {\n      throw new InvalidClientMetadataError(\n        'Only \"public\" subject_type is supported',\n      )\n    }\n\n    switch (metadata.token_endpoint_auth_method) {\n      case 'none':\n        if (metadata.token_endpoint_auth_signing_alg) {\n          throw new InvalidClientMetadataError(\n            `token_endpoint_auth_method \"none\" must not have token_endpoint_auth_signing_alg`,\n          )\n        }\n        break\n\n      case 'private_key_jwt':\n        if (!metadata.jwks && !metadata.jwks_uri) {\n          throw new InvalidClientMetadataError(\n            `private_key_jwt auth method requires jwks or jwks_uri`,\n          )\n        }\n        if (metadata.jwks?.keys.length === 0) {\n          throw new InvalidClientMetadataError(\n            `private_key_jwt auth method requires at least one key in jwks`,\n          )\n        }\n        if (!metadata.token_endpoint_auth_signing_alg) {\n          throw new InvalidClientMetadataError(\n            `Missing token_endpoint_auth_signing_alg client metadata`,\n          )\n        }\n        break\n\n      default:\n        throw new InvalidClientMetadataError(\n          `Unsupported client authentication method \"${metadata.token_endpoint_auth_method}\". Make sure \"token_endpoint_auth_method\" is set to one of: \"${Client.AUTH_METHODS_SUPPORTED.join('\", \"')}\"`,\n        )\n    }\n\n    if (metadata.authorization_encrypted_response_enc) {\n      throw new InvalidClientMetadataError(\n        'Encrypted authorization response is not supported',\n      )\n    }\n\n    if (metadata.tls_client_certificate_bound_access_tokens) {\n      throw new InvalidClientMetadataError(\n        'Mutual-TLS bound access tokens are not supported',\n      )\n    }\n\n    if (\n      metadata.authorization_encrypted_response_enc &&\n      !metadata.authorization_encrypted_response_alg\n    ) {\n      throw new InvalidClientMetadataError(\n        'authorization_encrypted_response_enc requires authorization_encrypted_response_alg',\n      )\n    }\n\n    // ATPROTO spec requires the use of DPoP (OAuth spec defaults to false)\n    if (metadata.dpop_bound_access_tokens !== true) {\n      throw new InvalidClientMetadataError(\n        '\"dpop_bound_access_tokens\" must be true',\n      )\n    }\n\n    // ATPROTO spec requires the use of PKCE, does not support OIDC\n    if (!metadata.response_types.includes('code')) {\n      throw new InvalidClientMetadataError('response_types must include \"code\"')\n    } else if (!metadata.grant_types.includes('authorization_code')) {\n      // Consistency check\n      throw new InvalidClientMetadataError(\n        `The \"code\" response type requires that \"grant_types\" contains \"authorization_code\"`,\n      )\n    }\n\n    if (metadata.authorization_details_types?.length) {\n      const dupAuthDetailsType =\n        metadata.authorization_details_types.find(isDuplicate)\n      if (dupAuthDetailsType) {\n        throw new InvalidClientMetadataError(\n          `Duplicate authorization_details_type \"${dupAuthDetailsType}\"`,\n        )\n      }\n\n      const authorizationDetailsTypesSupported =\n        this.serverMetadata.authorization_details_types_supported\n      if (!authorizationDetailsTypesSupported) {\n        throw new InvalidClientMetadataError(\n          'authorization_details_types are not supported',\n        )\n      }\n      for (const type of metadata.authorization_details_types) {\n        if (!authorizationDetailsTypesSupported.includes(type)) {\n          throw new InvalidClientMetadataError(\n            `Unsupported authorization_details_type \"${type}\"`,\n          )\n        }\n      }\n    }\n\n    if (!metadata.redirect_uris?.length) {\n      // ATPROTO spec requires that at least one redirect URI is provided\n\n      throw new InvalidClientMetadataError(\n        'At least one redirect_uri is required',\n      )\n    }\n\n    if (\n      metadata.application_type === 'native' &&\n      metadata.token_endpoint_auth_method !== 'none'\n    ) {\n      // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4\n      //\n      // > Except when using a mechanism like Dynamic Client Registration\n      // > [RFC7591] to provision per-instance secrets, native apps are\n      // > classified as public clients, as defined by Section 2.1 of OAuth 2.0\n      // > [RFC6749]; they MUST be registered with the authorization server as\n      // > such. Authorization servers MUST record the client type in the client\n      // > registration details in order to identify and process requests\n      // > accordingly.\n\n      // @NOTE We may want to remove this restriction in the future, for example\n      // if https://github.com/bluesky-social/proposals/tree/main/0010-client-assertion-backend\n      // gets adopted\n\n      throw new InvalidClientMetadataError(\n        'Native clients must authenticate using \"none\" method',\n      )\n    }\n\n    if (\n      metadata.application_type === 'web' &&\n      metadata.grant_types.includes('implicit')\n    ) {\n      // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2\n      //\n      // > Web Clients [as defined by \"application_type\"] using the OAuth\n      // > Implicit Grant Type MUST only register URLs using the https\n      // > scheme as redirect_uris; they MUST NOT use localhost as the\n      // > hostname.\n\n      for (const redirectUri of metadata.redirect_uris) {\n        const url = parseRedirectUri(redirectUri)\n        if (url.protocol !== 'https:') {\n          throw new InvalidRedirectUriError(\n            `Web clients must use HTTPS redirect URIs`,\n          )\n        }\n\n        if (url.hostname === 'localhost') {\n          throw new InvalidRedirectUriError(\n            `Web clients must not use localhost as the hostname`,\n          )\n        }\n      }\n    }\n\n    for (const redirectUri of metadata.redirect_uris) {\n      const url = parseRedirectUri(redirectUri)\n\n      if (url.username || url.password) {\n        // Is this a valid concern? Should we allow credentials in the URI?\n        throw new InvalidRedirectUriError(\n          `Redirect URI ${url} must not contain credentials`,\n        )\n      }\n\n      switch (true) {\n        // FIRST: Loopback redirect URI exception (only for native apps)\n\n        case url.hostname === 'localhost': {\n          // https://datatracker.ietf.org/doc/html/rfc8252#section-8.3\n          //\n          // > While redirect URIs using localhost (i.e.,\n          // > \"http://localhost:{port}/{path}\") function similarly to loopback IP\n          // > redirects described in Section 7.3, the use of localhost is NOT\n          // > RECOMMENDED. Specifying a redirect URI with the loopback IP literal\n          // > rather than localhost avoids inadvertently listening on network\n          // > interfaces other than the loopback interface. It is also less\n          // > susceptible to client-side firewalls and misconfigured host name\n          // > resolution on the user's device.\n          throw new InvalidRedirectUriError(\n            `Loopback redirect URI ${url} is not allowed (use explicit IPs instead)`,\n          )\n        }\n\n        case url.hostname === '127.0.0.1':\n        case url.hostname === '[::1]': {\n          // Only allowed for native apps\n          if (metadata.application_type !== 'native') {\n            throw new InvalidRedirectUriError(\n              `Loopback redirect URIs are only allowed for native apps`,\n            )\n          }\n\n          if (url.port) {\n            // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3\n            //\n            // > The authorization server MUST allow any port to be specified at\n            // > the time of the request for loopback IP redirect URIs, to\n            // > accommodate clients that obtain an available ephemeral port\n            // > from the operating system at the time of the request.\n            //\n            // Note: although validation of the redirect_uri will ignore the\n            // port we still allow it to be specified, as the spec does not\n            // forbid it. If a port number is specified, ports will need to\n            // match when validating authorization requests. See\n            // \"compareRedirectUri()\".\n          }\n\n          if (url.protocol !== 'http:') {\n            // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3\n            //\n            // > Loopback redirect URIs use the \"http\" scheme and are constructed\n            // > with the loopback IP literal and whatever port the client is\n            // > listening on. That is, \"http://127.0.0.1:{port}/{path}\" for IPv4,\n            // > and \"http://[::1]:{port}/{path}\" for IPv6.\n            throw new InvalidRedirectUriError(\n              `Loopback redirect URI ${url} must use HTTP`,\n            )\n          }\n\n          break\n        }\n\n        // SECOND: Protocol-based URI Redirection\n\n        case url.protocol === 'http:': {\n          // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2\n          //\n          // > request_uri [...] URLs MUST use the https scheme unless the\n          // > target Request Object is signed in a way that is verifiable by\n          // > the OP.\n          //\n          // OIDC/Request Object are not supported. ATproto spec should not\n          // allow HTTP redirect URIs either.\n\n          // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2\n          //\n          // > Authorization Servers MAY reject Redirection URI values using\n          // > the http scheme, other than the loopback case for Native\n          // > Clients.\n          throw new InvalidRedirectUriError(\n            'Only loopback redirect URIs are allowed to use the \"http\" scheme',\n          )\n        }\n\n        case url.protocol === 'https:': {\n          if (isLocalHostname(url.hostname)) {\n            throw new InvalidRedirectUriError(\n              `Redirect URI \"${url}\"'s domain name must not be a local hostname`,\n            )\n          }\n\n          // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4\n          //\n          // > In addition to the collision-resistant properties, requiring a\n          // > URI scheme based on a domain name that is under the control of\n          // > the app can help to prove ownership in the event of a dispute\n          // > where two apps claim the same private-use URI scheme (where one\n          // > app is acting maliciously).\n          //\n          // We can't enforce this here (in generic client validation) because\n          // we don't have a concept of generic proven ownership.\n          //\n          // Discoverable clients, however, will have this check covered in the\n          // `validateDiscoverableClientMetadata`, by using the client_id's\n          // domain as \"proven ownership\".\n\n          // The following restriction from OIDC is *not* enforced for clients\n          // as it prevents \"App Links\" / \"Apple Universal Links\" from being\n          // used as redirect URIs.\n          //\n          // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2\n          //\n          // > Native Clients [as defined by \"application_type\"] MUST only\n          // > register redirect_uris using custom URI schemes or loopback URLs\n          // > using the http scheme; loopback URLs use localhost or the IP\n          // > loopback literals 127.0.0.1 or [::1] as the hostname.\n          //\n          // if (metadata.application_type === 'native') {\n          //   throw new InvalidRedirectUriError(\n          //     `Native clients must use custom URI schemes or loopback URLs`,\n          //   )\n          // }\n\n          break\n        }\n\n        case isPrivateUseUriScheme(url): {\n          if (metadata.application_type !== 'native') {\n            throw new InvalidRedirectUriError(\n              `Private-Use URI Scheme redirect URI are only allowed for native apps`,\n            )\n          }\n\n          break\n        }\n\n        default:\n          // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4\n          //\n          // > At a minimum, any private-use URI scheme that doesn't contain a\n          // > period character (\".\") SHOULD be rejected.\n          throw new InvalidRedirectUriError(\n            `Invalid redirect URI scheme \"${url.protocol}\"`,\n          )\n      }\n    }\n\n    if (isOAuthClientIdLoopback(clientId)) {\n      return this.validateLoopbackClientMetadata(clientId, metadata)\n    } else if (isOAuthClientIdDiscoverable(clientId)) {\n      return this.validateDiscoverableClientMetadata(clientId, metadata)\n    } else {\n      return metadata\n    }\n  }\n\n  validateLoopbackClientMetadata(\n    clientId: OAuthClientIdLoopback,\n    metadata: OAuthClientMetadata,\n  ): OAuthClientMetadata {\n    if (metadata.client_uri) {\n      throw new InvalidClientMetadataError(\n        'client_uri is not allowed for loopback clients',\n      )\n    }\n\n    if (metadata.application_type !== 'native') {\n      throw new InvalidClientMetadataError(\n        'Loopback clients must have application_type \"native\"',\n      )\n    }\n\n    const method = metadata.token_endpoint_auth_method\n    if (method !== 'none') {\n      throw new InvalidClientMetadataError(\n        `Loopback clients are not allowed to use \"token_endpoint_auth_method\" ${method}`,\n      )\n    }\n\n    return metadata\n  }\n\n  validateDiscoverableClientMetadata(\n    clientId: OAuthClientIdDiscoverable,\n    metadata: OAuthClientMetadata,\n  ): OAuthClientMetadata {\n    if (!metadata.client_id) {\n      // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html\n      throw new InvalidClientMetadataError(\n        `client_id is required for discoverable clients`,\n      )\n    }\n\n    const clientIdUrl = parseDiscoverableClientId(clientId)\n\n    if (metadata.client_uri) {\n      // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html\n      //\n      // The client_uri must be a parent of the client_id URL. This might be\n      // relaxed in the future.\n\n      const clientUriUrl = new URL(metadata.client_uri)\n\n      if (clientUriUrl.origin !== clientIdUrl.origin) {\n        throw new InvalidClientMetadataError(\n          `client_uri must have the same origin as the client_id`,\n        )\n      }\n\n      if (clientIdUrl.pathname !== clientUriUrl.pathname) {\n        if (\n          !clientIdUrl.pathname.startsWith(\n            clientUriUrl.pathname.endsWith('/')\n              ? clientUriUrl.pathname\n              : `${clientUriUrl.pathname}/`,\n          )\n        ) {\n          throw new InvalidClientMetadataError(\n            `client_uri must be a parent URL of the client_id`,\n          )\n        }\n      }\n    }\n\n    for (const redirectUri of metadata.redirect_uris) {\n      // @NOTE at this point, all redirect URIs have already been validated by\n      // oauthRedirectUriSchema\n\n      const url = parseRedirectUri(redirectUri)\n\n      if (isPrivateUseUriScheme(url)) {\n        // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1\n        //\n        // > When choosing a URI scheme to associate with the app, apps MUST use\n        // > a URI scheme based on a domain name under their control, expressed\n        // > in reverse order, as recommended by Section 3.8 of [RFC7595] for\n        // > private-use URI schemes.\n\n        // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4\n        //\n        // > In addition to the collision-resistant properties, requiring a\n        // > URI scheme based on a domain name that is under the control of\n        // > the app can help to prove ownership in the event of a dispute\n        // > where two apps claim the same private-use URI scheme (where one\n        // > app is acting maliciously).\n\n        // https://atproto.com/specs/oauth\n        //\n        // > Any custom scheme must match the client_id hostname in\n        // > reverse-domain order. The URI scheme must be followed by a single\n        // > colon (:) then a single forward slash (/) and then a URI path\n        // > component. For example, an app with client_id\n        // > https://app.example.com/client-metadata.json could have a\n        // > redirect_uri of com.example.app:/callback.\n        const protocol = `${reverseDomain(clientIdUrl.hostname)}:`\n        if (url.protocol !== protocol) {\n          throw new InvalidRedirectUriError(\n            `Private-Use URI Scheme redirect URI, for discoverable client metadata, must be the fully qualified domain name (FQDN) of the client_id, in reverse order (${protocol})`,\n          )\n        }\n      }\n    }\n\n    return metadata\n  }\n}\n\nfunction isDuplicate<\n  T extends string | number | boolean | null | undefined | symbol,\n>(value: T, index: number, array: T[]) {\n  return array.includes(value, index + 1)\n}\n\nfunction reverseDomain(domain: string) {\n  return domain.split('.').reverse().join('.')\n}\n\nfunction isPrivateUseUriScheme(uri: URL) {\n  return uri.protocol.includes('.')\n}\n\nfunction buildJsonGetRequest(uri: string, options?: GetCachedOptions) {\n  return new Request(uri, {\n    headers: { accept: 'application/json' },\n    // @ts-expect-error invalid types in \"undici-types\"\n    cache: options?.noCache ? 'no-cache' : undefined,\n    signal: options?.signal,\n    redirect: 'error',\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-store.ts",
    "content": "import { OAuthClientMetadata } from '@atproto/oauth-types'\nimport { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { ClientId } from './client-id.js'\n\n// Export all types needed to implement the ClientStore interface\nexport * from './client-data.js'\nexport * from './client-id.js'\nexport type { Awaitable, OAuthClientMetadata }\n\nexport interface ClientStore {\n  findClient(clientId: ClientId): Awaitable<OAuthClientMetadata>\n}\n\nexport const isClientStore = buildInterfaceChecker<ClientStore>([\n  'findClient', //\n])\n\nexport function ifClientStore<V extends Partial<ClientStore>>(\n  implementation?: V,\n): (V & ClientStore) | undefined {\n  if (implementation && isClientStore(implementation)) {\n    return implementation\n  }\n\n  return undefined\n}\n\nexport function asClientStore<V extends Partial<ClientStore>>(\n  implementation?: V,\n): V & ClientStore {\n  const store = ifClientStore(implementation)\n  if (store) return store\n\n  throw new Error('Invalid ClientStore implementation')\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client-utils.ts",
    "content": "import {\n  OAuthClientIdDiscoverable,\n  isLocalHostname,\n  parseOAuthDiscoverableClientId,\n} from '@atproto/oauth-types'\nimport { InvalidClientIdError } from '../errors/invalid-client-id-error.js'\nimport { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'\n\nexport function parseRedirectUri(redirectUri: string): URL {\n  try {\n    return new URL(redirectUri)\n  } catch (err) {\n    throw InvalidRedirectUriError.from(err)\n  }\n}\n\nexport function parseDiscoverableClientId(\n  clientId: OAuthClientIdDiscoverable,\n): URL {\n  try {\n    const url = parseOAuthDiscoverableClientId(clientId)\n\n    // Extra validation, prevent usage of invalid internet domain names.\n    if (isLocalHostname(url.hostname)) {\n      throw new InvalidClientIdError(\n        \"The client_id's TLD must not be a local hostname\",\n      )\n    }\n\n    return url\n  } catch (err) {\n    throw InvalidClientIdError.from(\n      err,\n      'Invalid discoverable client identifier',\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/client/client.ts",
    "content": "import {\n  JWTClaimVerificationOptions,\n  type JWTHeaderParameters,\n  type JWTPayload,\n  type JWTVerifyOptions,\n  type JWTVerifyResult,\n  type KeyLike,\n  type ResolvedKey,\n  UnsecuredJWT,\n  type UnsecuredResult,\n  calculateJwkThumbprint,\n  createLocalJWKSet,\n  createRemoteJWKSet,\n  errors,\n  exportJWK,\n  jwtVerify,\n} from 'jose'\nimport { Jwks, SignedJwt, UnsignedJwt } from '@atproto/jwk'\nimport {\n  CLIENT_ASSERTION_TYPE_JWT_BEARER,\n  OAuthAuthorizationRequestParameters,\n  OAuthClientCredentials,\n  OAuthClientMetadata,\n  OAuthRedirectUri,\n} from '@atproto/oauth-types'\nimport { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'\nimport { AuthorizationError } from '../errors/authorization-error.js'\nimport { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'\nimport { InvalidClientError } from '../errors/invalid-client-error.js'\nimport { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { InvalidScopeError } from '../errors/invalid-scope-error.js'\nimport { asArray } from '../lib/util/cast.js'\nimport { compareRedirectUri } from '../lib/util/redirect-uri.js'\nimport { Awaitable } from '../lib/util/type.js'\nimport { ClientAuth } from './client-auth.js'\nimport { ClientId } from './client-id.js'\nimport { ClientInfo } from './client-info.js'\n\nconst { JOSEError } = errors\n\nexport class Client {\n  /**\n   * @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}\n   */\n  static readonly AUTH_METHODS_SUPPORTED = ['none', 'private_key_jwt'] as const\n\n  private readonly keyGetter: (\n    protectedHeader: JWTHeaderParameters,\n  ) => Awaitable<KeyLike | Uint8Array>\n\n  constructor(\n    public readonly id: ClientId,\n    public readonly metadata: OAuthClientMetadata,\n    public readonly jwks: undefined | Jwks = metadata.jwks,\n    public readonly info: ClientInfo,\n  ) {\n    // If the remote JWKS content is provided, we don't need to fetch it again.\n    this.keyGetter =\n      jwks || !metadata.jwks_uri\n        ? createLocalJWKSet(jwks || { keys: [] })\n        : createRemoteJWKSet(new URL(metadata.jwks_uri), {})\n  }\n\n  /**\n   * @see {@link https://www.rfc-editor.org/rfc/rfc9101.html#name-request-object-2}\n   */\n  public async decodeRequestObject(\n    jar: SignedJwt | UnsignedJwt,\n    audience: string,\n  ) {\n    // https://www.rfc-editor.org/rfc/rfc9101.html#name-request-object-2\n    // > If signed, the Authorization Request Object SHOULD contain the Claims\n    // > iss (issuer) and aud (audience) as members with their semantics being\n    // > the same as defined in the JWT [RFC7519] specification. The value of\n    // > aud should be the value of the authorization server (AS) issuer, as\n    // > defined in RFC 8414 [RFC8414].\n    try {\n      // We need to special case the \"none\" algorithm, as the validation method\n      // is different for signed and unsigned JWTs.\n      if (this.metadata.request_object_signing_alg === 'none') {\n        return await this.jwtVerifyUnsecured(jar, {\n          audience,\n          maxTokenAge: JAR_MAX_AGE / 1e3,\n          allowMissingAudience: true,\n          allowMissingIssuer: true,\n        })\n      }\n\n      return await this.jwtVerify(jar, {\n        audience,\n        maxTokenAge: JAR_MAX_AGE / 1e3,\n        algorithms: this.metadata.request_object_signing_alg\n          ? [this.metadata.request_object_signing_alg]\n          : // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2\n            //\n            // > The default, if omitted, is that any algorithm supported by the OP\n            // > and the RP MAY be used.\n            undefined,\n      })\n    } catch (err) {\n      const message =\n        err instanceof JOSEError\n          ? `Invalid \"request\" object: ${err.message}`\n          : `Invalid \"request\" object`\n\n      throw new InvalidRequestError(message, err)\n    }\n  }\n\n  protected async jwtVerifyUnsecured<PayloadType = JWTPayload>(\n    token: string,\n    {\n      audience,\n      allowMissingAudience = false,\n      allowMissingIssuer = false,\n      ...options\n    }: Omit<JWTClaimVerificationOptions, 'issuer'> & {\n      allowMissingIssuer?: boolean\n      allowMissingAudience?: boolean\n    } = {},\n  ): Promise<UnsecuredResult<PayloadType>> {\n    // jose does not support `allowMissingAudience` and `allowMissingIssuer`\n    // options, so we need to handle audience and issuer checks manually (see\n    // bellow).\n\n    const result = UnsecuredJWT.decode<PayloadType>(token, options)\n\n    if (!allowMissingIssuer || result.payload.iss != null) {\n      if (result.payload.iss !== this.id) {\n        throw new JOSEError(`Invalid \"iss\" claim \"${result.payload.iss}\"`)\n      }\n    }\n\n    if (!allowMissingAudience || result.payload.aud != null) {\n      if (audience != null) {\n        const payloadAud = asArray(result.payload.aud)\n        if (!asArray(audience).some((aud) => payloadAud.includes(aud))) {\n          throw new JOSEError(`Invalid \"aud\" claim \"${result.payload.aud}\"`)\n        }\n      }\n    }\n\n    return result\n  }\n\n  protected async jwtVerify<PayloadType = JWTPayload>(\n    token: string,\n    options?: Omit<JWTVerifyOptions, 'issuer'>,\n  ): Promise<JWTVerifyResult<PayloadType> & ResolvedKey<KeyLike>> {\n    return jwtVerify<PayloadType>(token, this.keyGetter, {\n      ...options,\n      issuer: this.id,\n    })\n  }\n\n  /**\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}\n   * @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}\n   */\n  public async authenticate(\n    input: OAuthClientCredentials,\n    checks: {\n      authorizationServerIdentifier: string\n    },\n  ): Promise<ClientAuth> {\n    const method = this.metadata.token_endpoint_auth_method\n\n    if (method === 'none') {\n      return { method: 'none' }\n    }\n\n    if (method === 'private_key_jwt') {\n      if (!('client_assertion' in input)) {\n        throw new InvalidRequestError(\n          `client authentication method \"${method}\" required a \"client_assertion\"`,\n        )\n      }\n\n      if (input.client_assertion_type === CLIENT_ASSERTION_TYPE_JWT_BEARER) {\n        // https://www.rfc-editor.org/rfc/rfc7523.html#section-3\n\n        const result = await this.jwtVerify<{\n          jti: string\n          exp?: number\n        }>(input.client_assertion, {\n          // > 1. The JWT MUST contain an \"iss\" (issuer) claim that contains a\n          // >    unique identifier for the entity that issued the JWT.\n          //\n          // The \"issuer\" is already checked by jwtVerify()\n\n          // > 2. The JWT MUST contain a \"sub\" (subject) claim identifying the\n          // >    principal that is the subject of the JWT. Two cases need to be\n          // >    differentiated: [...] For client authentication, the subject\n          // >    MUST be the \"client_id\" of the OAuth client.\n          subject: this.id,\n\n          // > 3. The JWT MUST contain an \"aud\" (audience) claim containing a\n          // >    value that identifies the authorization server as an intended\n          // >    audience. The token endpoint URL of the authorization server\n          // >    MAY be used as a value for an \"aud\" element to identify the\n          // >    authorization server as an intended audience of the JWT.\n          audience: checks.authorizationServerIdentifier,\n\n          requiredClaims: [\n            // > 4. The JWT MUST contain an \"exp\" (expiration time) claim that\n            // >    limits the time window during which the JWT can be used.\n            //\n            // @TODO The presence of \"exp\" didn't use to be enforced by this\n            // implementation (or provided by the oauth-client). This is mostly\n            // fine because \"iat\" *is* required, but this makes this\n            // implementation non compliant with RFC7523. We can't just make it\n            // required as it might break existing clients.\n\n            // 'exp',\n\n            // > 7. The JWT MAY contain a \"jti\" (JWT ID) claim that provides a\n            // >    unique identifier for the token. The authorization server\n            // >    MAY ensure that JWTs are not replayed by maintaining the set\n            // >    of used \"jti\" values for the length of time for which the\n            // >    JWT would be considered valid based on the applicable \"exp\"\n            // >    instant.\n            'jti',\n          ],\n\n          // > 5. The JWT MAY contain an \"nbf\" (not before) claim that\n          // >    identifies the time before which the token MUST NOT be\n          // >    accepted for processing.\n          //\n          // This is already enforced by jose\n\n          // > 6. The JWT MAY contain an \"iat\" (issued at) claim that identifies\n          // >    the time at which the JWT was issued.  Note that the\n          // >    authorization server may reject JWTs with an \"iat\" claim value\n          // >    that is unreasonably far in the past.\n          maxTokenAge: CLIENT_ASSERTION_MAX_AGE / 1000,\n        }).catch((err) => {\n          const msg =\n            err instanceof JOSEError\n              ? `Validation of \"client_assertion\" failed: ${err.message}`\n              : `Unable to verify \"client_assertion\" JWT`\n\n          throw new InvalidClientError(msg, err)\n        })\n\n        if (!result.protectedHeader.kid) {\n          throw new InvalidClientError(`\"kid\" required in client_assertion`)\n        }\n\n        return {\n          method: 'private_key_jwt',\n          jti: result.payload.jti,\n          exp: result.payload.exp,\n          jkt: await authJwkThumbprint(result.key),\n          alg: result.protectedHeader.alg,\n          kid: result.protectedHeader.kid,\n        }\n      }\n\n      throw new InvalidClientError(\n        `Unsupported client_assertion_type \"${input.client_assertion_type}\"`,\n      )\n    }\n\n    // @ts-expect-error Ensure to keep Client.AUTH_METHODS_SUPPORTED in sync\n    // with the implementation of this function.\n    if (Client.AUTH_METHODS_SUPPORTED.includes(method)) {\n      throw new Error(\n        `verifyCredentials() should implement all of ${[\n          Client.AUTH_METHODS_SUPPORTED,\n        ]}`,\n      )\n    }\n\n    throw new InvalidClientMetadataError(\n      `Unsupported token_endpoint_auth_method \"${method}\"`,\n    )\n  }\n\n  /**\n   * Validates the request parameters against the client metadata.\n   */\n  public validateRequest(\n    parameters: Readonly<OAuthAuthorizationRequestParameters>,\n  ): Readonly<OAuthAuthorizationRequestParameters> {\n    if (parameters.client_id !== this.id) {\n      throw new AuthorizationError(\n        parameters,\n        'The \"client_id\" parameter field does not match the value used to authenticate the client',\n      )\n    }\n\n    if (parameters.scope !== undefined) {\n      // Any scope requested by the client must be registered in the client\n      // metadata.\n      const declaredScopes = this.metadata.scope?.split(' ')\n\n      if (!declaredScopes) {\n        throw new InvalidScopeError(\n          parameters,\n          'Client has no declared scopes in its metadata',\n        )\n      }\n\n      for (const scope of parameters.scope.split(' ')) {\n        if (!declaredScopes.includes(scope)) {\n          throw new InvalidScopeError(\n            parameters,\n            `Scope \"${scope}\" is not declared in the client metadata`,\n          )\n        }\n      }\n    }\n\n    if (!this.metadata.response_types.includes(parameters.response_type)) {\n      throw new AuthorizationError(\n        parameters,\n        `Invalid response_type \"${parameters.response_type}\" requested by the client`,\n      )\n    }\n\n    if (parameters.response_type.includes('code')) {\n      if (!this.metadata.grant_types.includes('authorization_code')) {\n        throw new AuthorizationError(\n          parameters,\n          `This client is not allowed to use the \"authorization_code\" grant type`,\n        )\n      }\n    }\n\n    const { redirect_uri } = parameters\n    if (redirect_uri) {\n      if (\n        !this.metadata.redirect_uris.some((uri) =>\n          compareRedirectUri(uri, redirect_uri),\n        )\n      ) {\n        throw new AuthorizationError(\n          parameters,\n          `Invalid redirect_uri ${redirect_uri}`,\n        )\n      }\n    } else {\n      const { defaultRedirectUri } = this\n      if (defaultRedirectUri) {\n        parameters = { ...parameters, redirect_uri: defaultRedirectUri }\n      } else {\n        // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#authorization-request\n        //\n        // > \"redirect_uri\": OPTIONAL if only one redirect URI is registered for\n        // > this client. REQUIRED if multiple redirect URIs are registered for this\n        // > client.\n        throw new AuthorizationError(parameters, 'redirect_uri is required')\n      }\n    }\n\n    if (parameters.authorization_details) {\n      const { authorization_details_types } = this.metadata\n      if (!authorization_details_types) {\n        throw new InvalidAuthorizationDetailsError(\n          parameters,\n          'Client Metadata does not declare any \"authorization_details\"',\n        )\n      }\n\n      for (const detail of parameters.authorization_details) {\n        if (!authorization_details_types?.includes(detail.type)) {\n          throw new InvalidAuthorizationDetailsError(\n            parameters,\n            `Client Metadata does not declare any \"authorization_details\" of type \"${detail.type}\"`,\n          )\n        }\n      }\n    }\n\n    return parameters\n  }\n\n  get defaultRedirectUri(): OAuthRedirectUri | undefined {\n    const { redirect_uris } = this.metadata\n    return redirect_uris.length === 1 ? redirect_uris[0] : undefined\n  }\n}\n\nexport async function authJwkThumbprint(\n  key: Uint8Array | KeyLike,\n): Promise<string> {\n  try {\n    return await calculateJwkThumbprint(await exportJWK(key), 'sha512')\n  } catch (err) {\n    throw new InvalidClientError('Unable to compute JWK thumbprint', err)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/constants.ts",
    "content": "// The purpose of the prefix is to provide type safety\n\nexport const DEVICE_ID_PREFIX = 'dev-'\nexport const DEVICE_ID_BYTES_LENGTH = 16 // 128 bits\n\nexport const SESSION_ID_PREFIX = 'ses-'\nexport const SESSION_ID_BYTES_LENGTH = 16 // 128 bits - only valid if device id is valid\n\nexport const REFRESH_TOKEN_PREFIX = 'ref-'\nexport const REFRESH_TOKEN_BYTES_LENGTH = 32 // 256 bits\n\nexport const TOKEN_ID_PREFIX = 'tok-'\nexport const TOKEN_ID_BYTES_LENGTH = 16 // 128 bits - used as `jti` in JWTs (cannot be forged)\n\nexport const REQUEST_ID_PREFIX = 'req-'\nexport const REQUEST_ID_BYTES_LENGTH = 16 // 128 bits\n\nexport const CODE_PREFIX = 'cod-'\nexport const CODE_BYTES_LENGTH = 32\n\nconst SECOND = 1e3\nconst MINUTE = 60 * SECOND\nconst HOUR = 60 * MINUTE\nconst DAY = 24 * HOUR\nconst WEEK = 7 * DAY\nconst YEAR = 365.25 * DAY\nconst MONTH = YEAR / 12\n\n/** 7 days */\nexport const AUTHENTICATION_MAX_AGE = 7 * DAY\n\n/** 15 minutes */\nexport const EPHEMERAL_SESSION_MAX_AGE = 15 * MINUTE\n\n/** 60 minutes */\nexport const TOKEN_MAX_AGE = 60 * MINUTE\n\n/** 5 minutes */\nexport const AUTHORIZATION_INACTIVITY_TIMEOUT = 5 * MINUTE\n\n/** 2 week */\nexport const PUBLIC_CLIENT_SESSION_LIFETIME = 2 * WEEK\n\n/** @see {@link PUBLIC_CLIENT_SESSION_LIFETIME} */\nexport const PUBLIC_CLIENT_REFRESH_LIFETIME = PUBLIC_CLIENT_SESSION_LIFETIME\n\n/** 2 years */\nexport const CONFIDENTIAL_CLIENT_SESSION_LIFETIME = 2 * YEAR\n\n/** 3 months */\nexport const CONFIDENTIAL_CLIENT_REFRESH_LIFETIME = 3 * MONTH\n\n/** 5 minutes */\nexport const PAR_EXPIRES_IN = 5 * MINUTE\n\n/**\n * 59 seconds (should be less than a minute)\n *\n * > \"A general guidance for the validity time would be less than a minute.\"\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9101#section-10.2 | JWT-Secured Authorization Request (JAR) - Section 10.2 (d)}\n */\nexport const JAR_MAX_AGE = 59 * SECOND\n\n/** 1 minute */\nexport const CLIENT_ASSERTION_MAX_AGE = 1 * MINUTE\n\n/** 3 minutes */\nexport const DPOP_NONCE_MAX_AGE = 3 * MINUTE\n\n/** 5 seconds */\nexport const SESSION_FIXATION_MAX_AGE = 5 * SECOND\n\n/** 1 day */\nexport const CODE_CHALLENGE_REPLAY_TIMEFRAME = 1 * DAY\n\n/** 5 minutes */\nexport const LEXICON_REFRESH_FREQUENCY = 5 * MINUTE\n\nexport const NODE_ENV = process.env.NODE_ENV || 'production'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/customization/branding.ts",
    "content": "import { z } from 'zod'\nimport { colorsSchema } from './colors.js'\nimport { linksSchema } from './links.js'\n\nexport const brandingSchema = z.object({\n  name: z.string().optional(),\n  logo: z.string().url().optional(),\n  colors: colorsSchema.optional(),\n  links: z.array(linksSchema).optional(),\n})\nexport type BrandingInput = z.input<typeof brandingSchema>\nexport type Branding = z.infer<typeof brandingSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/customization/build-customization-css.ts",
    "content": "import { extractHue, pickContrastColor } from '../lib/util/color.js'\nimport { Branding } from './branding.js'\nimport { COLOR_NAMES } from './colors.js'\nimport { Customization } from './customization.js'\n\nexport function buildCustomizationCss({\n  branding,\n}: Customization): undefined | string {\n  const vars = Array.from(buildCustomizationVars(branding))\n  if (vars.length) return `:root { ${vars.join(' ')} }`\n}\n\nfunction* buildCustomizationVars(branding?: Branding): Generator<string> {\n  if (branding?.colors) {\n    const contrastLight = branding.colors.light ?? { r: 255, g: 255, b: 255 }\n    const contrastDark = branding.colors.dark ?? { r: 0, g: 0, b: 0 }\n\n    for (const name of COLOR_NAMES) {\n      const value = branding.colors[name]\n      if (!value) continue // Skip missing colors\n\n      const contrast =\n        branding.colors[`${name}Contrast`] ??\n        pickContrastColor(value, contrastLight, contrastDark)\n\n      const hue = branding.colors[`${name}Hue`] ?? extractHue(value)\n\n      yield `--branding-color-${name}: ${value.r} ${value.g} ${value.b};`\n      yield `--branding-color-${name}-contrast: ${contrast.r} ${contrast.g} ${contrast.b};`\n      yield `--branding-color-${name}-hue: ${hue};`\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/customization/build-customization-data.ts",
    "content": "import { CustomizationData } from '@atproto/oauth-provider-api'\nimport { Customization } from './customization.js'\n\nexport function buildCustomizationData({\n  branding,\n  availableUserDomains,\n  inviteCodeRequired,\n  hcaptcha,\n}: Customization): CustomizationData {\n  // @NOTE the front end does not need colors here as they will be injected as\n  // CSS variables.\n  // @NOTE We only copy the values explicitly needed to avoid leaking sensitive\n  // data (in case the caller passed more than what we expect).\n  return {\n    availableUserDomains,\n    inviteCodeRequired,\n    hcaptchaSiteKey: hcaptcha?.siteKey,\n    name: branding?.name,\n    logo: branding?.logo,\n    links: branding?.links,\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/customization/colors.ts",
    "content": "import { z } from 'zod'\nimport { colorHueSchema } from '../types/color-hue.js'\nimport { rgbColorSchema } from '../types/rgb-color.js'\n\nexport const COLOR_NAMES = ['primary', 'error', 'warning', 'success'] as const\nexport type ColorName = (typeof COLOR_NAMES)[number]\n\nexport const colorsSchema = z\n  .object({\n    light: rgbColorSchema.optional(),\n    dark: rgbColorSchema.optional(),\n  })\n  .extend(\n    Object.fromEntries(\n      COLOR_NAMES.map((name) => [name, rgbColorSchema.optional()]),\n    ) as Record<ColorName, z.ZodOptional<typeof rgbColorSchema>>,\n  )\n  .extend(\n    Object.fromEntries(\n      COLOR_NAMES.map((name) => [`${name}Contrast`, rgbColorSchema.optional()]),\n    ) as Record<`${ColorName}Contrast`, z.ZodOptional<typeof rgbColorSchema>>,\n  )\n  .extend(\n    Object.fromEntries(\n      COLOR_NAMES.map((name) => [`${name}Hue`, colorHueSchema.optional()]),\n    ) as Record<`${ColorName}Hue`, z.ZodOptional<typeof colorHueSchema>>,\n  )\n\nexport type Colors = z.infer<typeof colorsSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/customization/customization.ts",
    "content": "import { z } from 'zod'\nimport { hcaptchaConfigSchema } from '../lib/hcaptcha.js'\nimport { brandingSchema } from './branding.js'\n\nexport const customizationSchema = z.object({\n  /**\n   * Available user domains that can be used to sign up. A non-empty array\n   * is required to enable the sign-up feature.\n   */\n  availableUserDomains: z.array(z.string()).optional(),\n  /**\n   * UI customizations\n   */\n  branding: brandingSchema.optional(),\n  /**\n   * Is an invite code required to sign up?\n   */\n  inviteCodeRequired: z.boolean().optional(),\n  /**\n   * Enables hCaptcha during sign-up.\n   */\n  hcaptcha: hcaptchaConfigSchema.optional(),\n})\nexport type CustomizationInput = z.input<typeof customizationSchema>\nexport type Customization = z.infer<typeof customizationSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/customization/links.ts",
    "content": "import { z } from 'zod'\nimport { isLinkRel } from '../lib/html/build-document.js'\nimport { multiLangStringSchema } from '../lib/util/locale.js'\n\nexport const linksSchema = z.object({\n  title: z.union([z.string(), multiLangStringSchema]),\n  href: z.string().url(),\n  rel: z.string().refine(isLinkRel, 'Invalid link rel').optional(),\n})\nexport type Links = z.infer<typeof linksSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/device/device-data.ts",
    "content": "import { z } from 'zod'\nimport { sessionIdSchema } from './session-id.js'\n\nexport const deviceDataSchema = z.object({\n  sessionId: sessionIdSchema,\n  lastSeenAt: z.date(),\n  userAgent: z.string().nullable(),\n  ipAddress: z.string(),\n})\n\nexport type DeviceData = z.infer<typeof deviceDataSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/device/device-id.ts",
    "content": "import { z } from 'zod'\nimport { DEVICE_ID_BYTES_LENGTH, DEVICE_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const DEVICE_ID_LENGTH =\n  DEVICE_ID_PREFIX.length + DEVICE_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const deviceIdSchema = z\n  .string()\n  .length(DEVICE_ID_LENGTH)\n  .refine(\n    (v): v is `${typeof DEVICE_ID_PREFIX}${string}` =>\n      v.startsWith(DEVICE_ID_PREFIX),\n    {\n      message: `Invalid device ID format`,\n    },\n  )\n\nexport type DeviceId = z.infer<typeof deviceIdSchema>\n\nexport function isDeviceId(value: unknown): value is DeviceId {\n  return deviceIdSchema.safeParse(value).success\n}\n\nexport const generateDeviceId = async (): Promise<DeviceId> => {\n  return `${DEVICE_ID_PREFIX}${await randomHexId(DEVICE_ID_BYTES_LENGTH)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/device/device-manager.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { z } from 'zod'\nimport { SESSION_FIXATION_MAX_AGE } from '../constants.js'\nimport { parseHttpCookies } from '../lib/http/index.js'\nimport {\n  RequestMetadata,\n  extractRequestMetadata,\n  setCookie,\n} from '../lib/http/request.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId, deviceIdSchema, generateDeviceId } from './device-id.js'\nimport { DeviceStore } from './device-store.js'\nimport { generateSessionId, sessionIdSchema } from './session-id.js'\n\n/**\n * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}\n */\nexport const keygripSchema = z.object({\n  sign: z.function().args(z.any()).returns(z.string()),\n  verify: z.function().args(z.any(), z.string()).returns(z.boolean()),\n  index: z.function().args(z.any(), z.string()).returns(z.number()),\n})\n\nexport const deviceManagerOptionsSchema = z.object({\n  /**\n   * Controls whether the IP address is read from the `X-Forwarded-For` header\n   * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).\n   */\n  trustProxy: z\n    .function()\n    .args<[addr: z.ZodString, i: z.ZodNumber]>(z.string(), z.number())\n    .returns(z.boolean())\n    .optional(),\n\n  /**\n   * Amount of time (in ms) after which session IDs will be rotated\n   *\n   * @default 300e3 // (5 minutes)\n   */\n  rotationRate: z.number().default(300e3),\n  /**\n   * Cookie options\n   */\n  cookie: z\n    .object({\n      keys: keygripSchema.optional(),\n      /**\n       * Amount of time (in ms) after which the session cookie will expire.\n       * If set to `null`, the cookie will be a session cookie (deleted when the\n       * browser is closed).\n       *\n       * @default 10 years\n       */\n      age: z\n        .number()\n        .nullable()\n        .default(10 * 365.2 * 24 * 60 * 60e3),\n      /**\n       * Controls whether the cookie is only sent over HTTPS (if `true`), or also\n       * over HTTP (if `false`). This should **NOT** be set to `false` in\n       * production.\n       */\n      secure: z.boolean().default(true),\n      /**\n       * Controls whether the cookie is sent along with cross-site requests.\n       *\n       * @default 'lax'\n       */\n      sameSite: z.enum(['lax', 'strict']).default('lax'),\n    })\n    .default({}),\n})\n\nexport type DeviceManagerOptions = z.input<typeof deviceManagerOptionsSchema>\n\ntype CookieValue = {\n  deviceId: DeviceId\n  sessionId: string\n}\n\nexport type DeviceInfo = {\n  deviceId: DeviceId\n  deviceMetadata: RequestMetadata\n}\n\n/**\n * This class provides an abstraction for keeping track of DEVICE sessions. It\n * relies on a {@link DeviceStore} to persist session data and a cookie to\n * identify the session.\n */\nexport class DeviceManager {\n  private readonly options: z.output<typeof deviceManagerOptionsSchema>\n\n  constructor(\n    private readonly store: DeviceStore,\n    options: DeviceManagerOptions = {},\n  ) {\n    this.options = deviceManagerOptionsSchema.parse(options)\n  }\n\n  public async hasSession(req: IncomingMessage): Promise<boolean> {\n    const cookies = await this.getCookies(req)\n    return cookies !== null\n  }\n\n  public async load(\n    req: IncomingMessage,\n    res: ServerResponse,\n    forceRotate = false,\n  ): Promise<DeviceInfo> {\n    const cookie = await this.getCookies(req)\n    if (cookie) {\n      return this.refresh(\n        req,\n        res,\n        cookie.value,\n        forceRotate || cookie.mustRotate,\n      )\n    } else {\n      return this.create(req, res)\n    }\n  }\n\n  private async create(\n    req: IncomingMessage,\n    res: ServerResponse,\n  ): Promise<DeviceInfo> {\n    const deviceMetadata = this.getRequestMetadata(req)\n\n    const [deviceId, sessionId] = await Promise.all([\n      generateDeviceId(),\n      generateSessionId(),\n    ] as const)\n\n    await this.store.createDevice(deviceId, {\n      sessionId,\n      lastSeenAt: new Date(),\n      userAgent: deviceMetadata.userAgent ?? null,\n      ipAddress: deviceMetadata.ipAddress,\n    })\n\n    await this.setCookies(req, res, { deviceId, sessionId })\n\n    return { deviceId, deviceMetadata }\n  }\n\n  private async refresh(\n    req: IncomingMessage,\n    res: ServerResponse,\n    { deviceId, sessionId }: CookieValue,\n    forceRotate = false,\n  ): Promise<DeviceInfo> {\n    const data = await this.store.readDevice(deviceId)\n    if (!data) return this.create(req, res)\n\n    const lastSeenAt = new Date(data.lastSeenAt)\n    const age = Date.now() - lastSeenAt.getTime()\n\n    if (sessionId !== data.sessionId) {\n      if (age <= SESSION_FIXATION_MAX_AGE) {\n        // The cookie was probably rotated by a concurrent request. Let's\n        // update the cookie with the new sessionId.\n        forceRotate = true\n      } else {\n        // Something's wrong. Let's create a new session.\n        await this.store.deleteDevice(deviceId)\n        return this.create(req, res)\n      }\n    }\n\n    const deviceMetadata = this.getRequestMetadata(req)\n\n    const shouldRotate =\n      forceRotate ||\n      deviceMetadata.ipAddress !== data.ipAddress ||\n      deviceMetadata.userAgent !== data.userAgent ||\n      age > this.options.rotationRate\n\n    if (shouldRotate) {\n      await this.rotate(req, res, deviceId, {\n        ipAddress: deviceMetadata.ipAddress,\n        userAgent: deviceMetadata.userAgent || data.userAgent,\n      })\n    }\n\n    return { deviceId, deviceMetadata }\n  }\n\n  private async rotate(\n    req: IncomingMessage,\n    res: ServerResponse,\n    deviceId: DeviceId,\n    data?: Partial<Omit<DeviceData, 'sessionId' | 'lastSeenAt'>>,\n  ): Promise<void> {\n    const sessionId = await generateSessionId()\n\n    await this.store.updateDevice(deviceId, {\n      ...data,\n      sessionId,\n      lastSeenAt: new Date(),\n    })\n\n    await this.setCookies(req, res, { deviceId, sessionId })\n  }\n\n  private async getCookies(\n    req: IncomingMessage,\n  ): Promise<{ value: CookieValue; mustRotate: boolean } | null> {\n    const cookies = parseHttpCookies(req)\n\n    const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema)\n    const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema)\n\n    const deviceId = device?.value\n    const sessionId = session?.value\n\n    // Silently ignore invalid cookies\n    if (!deviceId || !sessionId) {\n      // If the device cookie is still present, let's cleanup the DB\n      if (deviceId) await this.store.deleteDevice(deviceId)\n\n      return null\n    }\n\n    return {\n      value: { deviceId, sessionId },\n      mustRotate: device.mustRotate || session.mustRotate,\n    }\n  }\n\n  private parseCookie<T>(\n    cookies: Record<string, string | undefined>,\n    name: string,\n    schema: z.ZodType<T> | z.ZodEffects<z.ZodTypeAny, T, string>,\n  ): null | { value: T; mustRotate: boolean } {\n    const rawValue = Object.hasOwn(cookies, name) ? cookies[name] : null\n    if (!rawValue) return null\n\n    const result = schema.safeParse(rawValue)\n    if (!result.success) return null\n\n    const value = result.data\n\n    if (this.options.cookie.keys) {\n      const hashName = `${name}:hash`\n\n      const hash = Object.hasOwn(cookies, hashName) ? cookies[hashName] : null\n      if (!hash) return null\n\n      const idx = this.options.cookie.keys.index(rawValue, hash)\n      if (idx < 0) return null\n\n      return { value, mustRotate: idx !== 0 }\n    }\n\n    return { value, mustRotate: false }\n  }\n\n  private async setCookies(\n    req: IncomingMessage,\n    res: ServerResponse,\n    { deviceId, sessionId }: CookieValue,\n  ) {\n    this.writeCookie(res, `dev-id`, deviceId)\n    this.writeCookie(res, `ses-id`, sessionId)\n  }\n\n  private writeCookie(res: ServerResponse, name: string, value?: string) {\n    const cookieOptions = {\n      maxAge: value\n        ? this.options.cookie.age == null\n          ? undefined\n          : this.options.cookie.age / 1000\n        : 0,\n      httpOnly: true,\n      path: '/',\n      secure: this.options.cookie.secure !== false,\n      sameSite: this.options.cookie.sameSite,\n    } as const\n\n    setCookie(res, name, value || '', cookieOptions)\n\n    if (this.options.cookie.keys) {\n      const hash = value ? this.options.cookie.keys.sign(value) : ''\n      setCookie(res, `${name}:hash`, hash, cookieOptions)\n    }\n  }\n\n  public getRequestMetadata(req: IncomingMessage) {\n    return extractRequestMetadata(req, this.options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/device/device-store.ts",
    "content": "import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId } from './device-id.js'\n\n// Export all types needed to implement the DeviceStore interface\nexport * from './device-data.js'\nexport * from './device-id.js'\nexport * from './session-id.js'\n\nexport type { Awaitable }\n\nexport interface DeviceStore {\n  createDevice(deviceId: DeviceId, data: DeviceData): Awaitable<void>\n  readDevice(deviceId: DeviceId): Awaitable<DeviceData | null>\n  updateDevice(deviceId: DeviceId, data: Partial<DeviceData>): Awaitable<void>\n  deleteDevice(deviceId: DeviceId): Awaitable<void>\n}\n\nexport const isDeviceStore = buildInterfaceChecker<DeviceStore>([\n  'createDevice',\n  'readDevice',\n  'updateDevice',\n  'deleteDevice',\n])\n\nexport function asDeviceStore<V>(implementation: V): V & DeviceStore {\n  if (!implementation || !isDeviceStore(implementation)) {\n    throw new Error('Invalid DeviceStore implementation')\n  }\n  return implementation\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/device/session-id.ts",
    "content": "import { z } from 'zod'\nimport { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const SESSION_ID_LENGTH =\n  SESSION_ID_PREFIX.length + SESSION_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const sessionIdSchema = z\n  .string()\n  .length(SESSION_ID_LENGTH)\n  .refine(\n    (v): v is `${typeof SESSION_ID_PREFIX}${string}` =>\n      v.startsWith(SESSION_ID_PREFIX),\n    {\n      message: `Invalid session ID format`,\n    },\n  )\nexport type SessionId = z.infer<typeof sessionIdSchema>\nexport const generateSessionId = async (): Promise<SessionId> => {\n  return `${SESSION_ID_PREFIX}${await randomHexId(SESSION_ID_BYTES_LENGTH)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/dpop/dpop-manager.ts",
    "content": "import { createHash } from 'node:crypto'\nimport { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'\nimport { z } from 'zod'\nimport { ValidationError } from '@atproto/jwk'\nimport { DPOP_NONCE_MAX_AGE } from '../constants.js'\nimport { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'\nimport { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'\nimport { ifURL } from '../lib/util/cast.js'\nimport {\n  DpopNonce,\n  DpopSecret,\n  dpopSecretSchema,\n  rotationIntervalSchema,\n} from './dpop-nonce.js'\nimport { DpopProof } from './dpop-proof.js'\n\nconst { JOSEError } = errors\n\nexport { DpopNonce, type DpopSecret }\n\nexport const dpopManagerOptionsSchema = z.object({\n  /**\n   * Set this to `false` to disable the use of nonces in DPoP proofs. Set this\n   * to a secret Uint8Array or hex encoded string to use a predictable seed for\n   * all nonces (typically useful when multiple instances are running). Leave\n   * undefined to generate a random seed at startup.\n   */\n  dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),\n  dpopRotationInterval: rotationIntervalSchema.optional(),\n})\nexport type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>\n\nexport class DpopManager {\n  protected readonly dpopNonce?: DpopNonce\n\n  constructor(options: DpopManagerOptions = {}) {\n    const { dpopSecret, dpopRotationInterval } =\n      dpopManagerOptionsSchema.parse(options)\n    this.dpopNonce =\n      dpopSecret === false\n        ? undefined\n        : new DpopNonce(dpopSecret, dpopRotationInterval)\n  }\n\n  nextNonce(): string | undefined {\n    return this.dpopNonce?.next()\n  }\n\n  /**\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n   */\n  async checkProof(\n    httpMethod: string,\n    httpUrl: Readonly<URL>,\n    httpHeaders: Record<string, undefined | string | string[]>,\n    accessToken?: string,\n  ): Promise<null | DpopProof> {\n    // Fool proofing against use of empty string\n    if (!httpMethod) {\n      throw new TypeError('HTTP method is required')\n    }\n\n    const proof = extractProof(httpHeaders)\n    if (!proof) return null\n\n    const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {\n      typ: 'dpop+jwt',\n      maxTokenAge: 10, // Will ensure presence & validity of \"iat\" claim\n      clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,\n    }).catch((err) => {\n      throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof')\n    })\n\n    // @NOTE For legacy & backwards compatibility reason, we cannot use\n    // `jwtPayloadSchema` here as it will reject DPoP proofs containing a query\n    // or fragment component in the \"htu\" claim.\n\n    // const { ath, htm, htu, jti, nonce } = await jwtPayloadSchema\n    //   .parseAsync(payload)\n    //   .catch((err) => {\n    //     throw buildInvalidDpopProofError('Invalid DPoP proof', err)\n    //   })\n\n    // @TODO Uncomment previous lines (and remove redundant checks bellow) once\n    // we decide to drop legacy support.\n    const { ath, htm, htu, jti, nonce } = payload\n\n    if (nonce !== undefined && typeof nonce !== 'string') {\n      throw new InvalidDpopProofError('Invalid DPoP \"nonce\" type')\n    }\n\n    if (!jti || typeof jti !== 'string') {\n      throw new InvalidDpopProofError('DPoP \"jti\" missing')\n    }\n\n    // Note rfc9110#section-9.1 states that the method name is case-sensitive\n    if (!htm || htm !== httpMethod) {\n      throw new InvalidDpopProofError('DPoP \"htm\" mismatch')\n    }\n\n    if (!htu || typeof htu !== 'string') {\n      throw new InvalidDpopProofError('Invalid DPoP \"htu\" type')\n    }\n\n    // > To reduce the likelihood of false negatives, servers SHOULD employ\n    // > syntax-based normalization (Section 6.2.2 of [RFC3986]) and\n    // > scheme-based normalization (Section 6.2.3 of [RFC3986]) before\n    // > comparing the htu claim.\n    //\n    // RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3\n    if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {\n      throw new InvalidDpopProofError('DPoP \"htu\" mismatch')\n    }\n\n    if (!nonce && this.dpopNonce) {\n      throw new UseDpopNonceError()\n    }\n\n    if (nonce && !this.dpopNonce?.check(nonce)) {\n      throw new UseDpopNonceError('DPoP \"nonce\" mismatch')\n    }\n\n    if (accessToken) {\n      const accessTokenHash = createHash('sha256').update(accessToken).digest()\n      if (ath !== accessTokenHash.toString('base64url')) {\n        throw new InvalidDpopProofError('DPoP \"ath\" mismatch')\n      }\n    } else if (ath !== undefined) {\n      throw new InvalidDpopProofError('DPoP \"ath\" claim not allowed')\n    }\n\n    // @NOTE we can assert there is a jwk because the jwtVerify used the\n    // EmbeddedJWK key getter mechanism.\n    const jwk = protectedHeader.jwk!\n    const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {\n      throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt')\n    })\n\n    // @NOTE We freeze the proof to prevent accidental modification (esp. from\n    // hooks).\n    return Object.freeze({ jti, jkt, htm, htu })\n  }\n}\n\nfunction extractProof(\n  httpHeaders: Record<string, undefined | string | string[]>,\n): string | null {\n  const dpopHeader = httpHeaders['dpop']\n  switch (typeof dpopHeader) {\n    case 'string':\n      if (dpopHeader) return dpopHeader\n      throw new InvalidDpopProofError('DPoP header cannot be empty')\n    case 'object':\n      // @NOTE the \"0\" case should never happen a node.js HTTP server will only\n      // return an array if the header is set multiple times.\n      if (dpopHeader.length === 1 && dpopHeader[0]) return dpopHeader[0]!\n      throw new InvalidDpopProofError('DPoP header must contain a single proof')\n    default:\n      return null\n  }\n}\n\n/**\n * Constructs the HTTP URI (htu) claim as defined in RFC9449.\n *\n * The htu claim is the normalized URL of the HTTP request, excluding the query\n * string and fragment. This function ensures that the URL is normalized by\n * removing the search and hash components, as well as by using an URL object to\n * simplify the pathname (e.g. removing dot segments).\n *\n * @returns The normalized URL as a string.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\nfunction normalizeHtuUrl(url: Readonly<URL>): string {\n  // NodeJS's `URL` normalizes the pathname, so we can just use that.\n  return url.origin + url.pathname\n}\n\nfunction parseHtu(htu: string): string {\n  const url = ifURL(htu)\n  if (!url) {\n    throw new InvalidDpopProofError('DPoP \"htu\" is not a valid URL')\n  }\n\n  // @NOTE the checks bellow can be removed once once jwtPayloadSchema is used\n  // to validate the DPoP proof payload as it already performs these checks\n  // (though the htuSchema).\n\n  if (url.password || url.username) {\n    throw new InvalidDpopProofError('DPoP \"htu\" must not contain credentials')\n  }\n\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new InvalidDpopProofError('DPoP \"htu\" must be http or https')\n  }\n\n  // @NOTE For legacy & backwards compatibility reason, we allow a query and\n  // fragment in the DPoP proof's htu. This is not a standard behavior as the\n  // htu is not supposed to contain query or fragment.\n\n  // NodeJS's `URL` normalizes the pathname.\n  return normalizeHtuUrl(url)\n}\n\nfunction wrapInvalidDpopProofError(\n  err: unknown,\n  title: string,\n): InvalidDpopProofError {\n  const msg =\n    err instanceof JOSEError || err instanceof ValidationError\n      ? `${title}: ${err.message}`\n      : title\n  return new InvalidDpopProofError(msg, err)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/dpop/dpop-nonce.ts",
    "content": "import { createHmac, randomBytes } from 'node:crypto'\nimport { z } from 'zod'\nimport { DPOP_NONCE_MAX_AGE } from '../constants.js'\n\nconst MAX_ROTATION_INTERVAL = DPOP_NONCE_MAX_AGE / 3\nconst MIN_ROTATION_INTERVAL = Math.min(1000, MAX_ROTATION_INTERVAL)\n\nexport const rotationIntervalSchema = z\n  .number()\n  .int()\n  .min(MIN_ROTATION_INTERVAL)\n  .max(MAX_ROTATION_INTERVAL)\n\nconst SECRET_BYTE_LENGTH = 32\n\nexport const secretBytesSchema = z\n  .instanceof(Uint8Array)\n  .refine((secret) => secret.length === SECRET_BYTE_LENGTH, {\n    message: `Secret must be exactly ${SECRET_BYTE_LENGTH} bytes long`,\n  })\n\nexport const secretHexSchema = z\n  .string()\n  .regex(\n    /^[0-9a-f]+$/i,\n    `Secret must be a ${SECRET_BYTE_LENGTH * 2} chars hex string`,\n  )\n  .length(SECRET_BYTE_LENGTH * 2)\n  .transform((hex): Uint8Array => Buffer.from(hex, 'hex'))\n\nexport const dpopSecretSchema = z.union([secretBytesSchema, secretHexSchema])\nexport type DpopSecret = z.input<typeof dpopSecretSchema>\n\nexport class DpopNonce {\n  readonly #rotationInterval: number\n  readonly #secret: Uint8Array\n\n  // Nonce state\n  #counter: number\n  #prev: string\n  #now: string\n  #next: string\n\n  constructor(\n    secret: DpopSecret = randomBytes(SECRET_BYTE_LENGTH),\n    rotationInterval = MAX_ROTATION_INTERVAL,\n  ) {\n    this.#rotationInterval = rotationIntervalSchema.parse(rotationInterval)\n    this.#secret = Uint8Array.from(dpopSecretSchema.parse(secret))\n\n    this.#counter = this.currentCounter\n    this.#prev = this.compute(this.#counter - 1)\n    this.#now = this.compute(this.#counter)\n    this.#next = this.compute(this.#counter + 1)\n  }\n\n  /**\n   * Returns the number of full rotations since the epoch\n   */\n  protected get currentCounter() {\n    return (Date.now() / this.#rotationInterval) | 0\n  }\n\n  protected rotate() {\n    const counter = this.currentCounter\n    switch (counter - this.#counter) {\n      case 0:\n        // counter === this.#counter => nothing to do\n        return\n      case 1:\n        // Optimization: avoid recomputing #prev & #now\n        this.#prev = this.#now\n        this.#now = this.#next\n        this.#next = this.compute(counter + 1)\n        break\n      case 2:\n        // Optimization: avoid recomputing #prev\n        this.#prev = this.#next\n        this.#now = this.compute(counter)\n        this.#next = this.compute(counter + 1)\n        break\n      default:\n        // All nonces are outdated, so we recompute all of them\n        this.#prev = this.compute(counter - 1)\n        this.#now = this.compute(counter)\n        this.#next = this.compute(counter + 1)\n        break\n    }\n    this.#counter = counter\n  }\n\n  protected compute(counter: number) {\n    return createHmac('sha256', this.#secret)\n      .update(numTo64bits(counter))\n      .digest()\n      .toString('base64url')\n  }\n\n  public next() {\n    this.rotate()\n    return this.#next\n  }\n\n  public check(nonce: string) {\n    return this.#next === nonce || this.#now === nonce || this.#prev === nonce\n  }\n}\n\nfunction numTo64bits(num: number) {\n  const arr = new Uint8Array(8)\n  // @NOTE Assigning to an uint8 will only keep the last 8 int bits\n  arr[7] = num |= 0\n  arr[6] = num >>= 8\n  arr[5] = num >>= 8\n  arr[4] = num >>= 8\n  arr[3] = num >>= 8\n  arr[2] = num >>= 8\n  arr[1] = num >>= 8\n  arr[0] = num >>= 8\n  return arr\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/dpop/dpop-proof.ts",
    "content": "export type DpopProof = Readonly<{\n  jti: string\n  jkt: string\n  htm: string\n  htu: string\n}>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/access-denied-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationError } from './authorization-error.js'\n\nexport class AccessDeniedError extends AuthorizationError {\n  constructor(\n    parameters: OAuthAuthorizationRequestParameters,\n    error_description = 'Access denied',\n    cause?: unknown,\n  ) {\n    super(parameters, error_description, 'access_denied', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/account-selection-required-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationError } from './authorization-error.js'\n\nexport class AccountSelectionRequiredError extends AuthorizationError {\n  constructor(\n    parameters: OAuthAuthorizationRequestParameters,\n    error_description = 'Account selection required',\n    cause?: unknown,\n  ) {\n    super(parameters, error_description, 'account_selection_required', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/authorization-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport {\n  AuthorizationResponseError,\n  isAuthorizationResponseError,\n} from '../types/authorization-response-error.js'\nimport { buildErrorPayload } from './error-parser.js'\nimport { OAuthError } from './oauth-error.js'\n\nexport type { AuthorizationResponseError, OAuthAuthorizationRequestParameters }\n\nexport class AuthorizationError extends OAuthError {\n  constructor(\n    public readonly parameters: OAuthAuthorizationRequestParameters,\n    error_description: string,\n    error: AuthorizationResponseError = 'invalid_request',\n    cause?: unknown,\n  ) {\n    super(error, error_description, 400, cause)\n  }\n\n  static from(\n    parameters: OAuthAuthorizationRequestParameters,\n    cause: unknown,\n  ): AuthorizationError {\n    if (cause instanceof AuthorizationError) return cause\n    const payload = buildErrorPayload(cause)\n    return new AuthorizationError(\n      parameters,\n      payload.error_description,\n      isAuthorizationResponseError(payload.error)\n        ? payload.error // Propagate \"error\" derived from the cause\n        : rootCause(cause) instanceof OAuthError\n          ? 'invalid_request'\n          : 'server_error',\n      cause,\n    )\n  }\n}\n\nfunction rootCause(err: unknown): unknown {\n  while (err instanceof Error && err.cause != null) {\n    err = err.cause\n  }\n  return err\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/consent-required-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationError } from './authorization-error.js'\n\nexport class ConsentRequiredError extends AuthorizationError {\n  constructor(\n    parameters: OAuthAuthorizationRequestParameters,\n    error_description = 'User consent required',\n    cause?: unknown,\n  ) {\n    super(parameters, error_description, 'consent_required', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/error-parser.ts",
    "content": "import { errors } from 'jose'\nimport { ZodError } from 'zod'\nimport { JwtVerifyError } from '@atproto/jwk'\nimport { formatZodError } from '../lib/util/zod-error.js'\nimport { OAuthError } from './oauth-error.js'\n\nconst { JOSEError } = errors\n\nconst INVALID_REQUEST = 'invalid_request'\nconst SERVER_ERROR = 'server_error'\n\nexport function buildErrorStatus(error: unknown): number {\n  if (error instanceof OAuthError) {\n    return error.statusCode\n  }\n\n  if (error instanceof JwtVerifyError) {\n    return 400\n  }\n\n  if (error instanceof ZodError) {\n    return 400\n  }\n\n  if (error instanceof JOSEError) {\n    return 400\n  }\n\n  if (error instanceof TypeError) {\n    return 400\n  }\n\n  if (isBoom(error)) {\n    return error.output.statusCode\n  }\n\n  if (isXrpcError(error)) {\n    return error.type\n  }\n\n  const status = (error as any)?.status\n  if (\n    typeof status === 'number' &&\n    status === (status | 0) &&\n    status >= 400 &&\n    status < 600\n  ) {\n    return status\n  }\n\n  return 500\n}\n\nexport type ErrorPayload = {\n  error: string\n  error_description: string\n}\n\nexport function buildErrorPayload(error: unknown): ErrorPayload {\n  if (error instanceof OAuthError) {\n    return error.toJSON()\n  }\n\n  if (error instanceof ZodError) {\n    return {\n      error: INVALID_REQUEST,\n      error_description: formatZodError(error, 'Validation error'),\n    }\n  }\n\n  if (error instanceof JOSEError) {\n    return {\n      error: INVALID_REQUEST,\n      error_description: error.message,\n    }\n  }\n\n  if (error instanceof TypeError) {\n    return {\n      error: INVALID_REQUEST,\n      error_description: error.message,\n    }\n  }\n\n  if (isBoom(error)) {\n    return {\n      error: error.output.statusCode <= 500 ? INVALID_REQUEST : SERVER_ERROR,\n      error_description:\n        error.output.statusCode <= 500\n          ? isPayloadLike(error.output?.payload)\n            ? error.output.payload.message\n            : error.message\n          : 'Server error',\n    }\n  }\n\n  if (isXrpcError(error)) {\n    return {\n      error: error.type <= 500 ? INVALID_REQUEST : SERVER_ERROR,\n      error_description: error.payload.message,\n    }\n  }\n\n  const status = buildErrorStatus(error)\n  return {\n    error: status < 500 ? INVALID_REQUEST : SERVER_ERROR,\n    error_description:\n      error instanceof Error && (error as any)?.expose === true\n        ? error.message\n        : 'Server error',\n  }\n}\n\nfunction isBoom(v: unknown): v is Error & {\n  isBoom: true\n  output: { statusCode: number; payload: unknown }\n} {\n  return (\n    v instanceof Error &&\n    (v as any).isBoom === true &&\n    isHttpErrorCode(v['output']?.['statusCode'])\n  )\n}\n\nfunction isXrpcError(v: unknown): v is Error & {\n  type: number\n  payload: { error: string; message: string }\n} {\n  return (\n    v instanceof Error &&\n    isHttpErrorCode(v['type']) &&\n    isPayloadLike(v['payload'])\n  )\n}\n\nfunction isHttpErrorCode(v: unknown): v is number {\n  return typeof v === 'number' && v >= 400 && v < 600 && v === (v | 0)\n}\n\nfunction isPayloadLike(v: unknown): v is { error: string; message: string } {\n  return (\n    v != null &&\n    typeof v === 'object' &&\n    typeof v['error'] === 'string' &&\n    typeof v['message'] === 'string'\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/handle-unavailable-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\nexport class HandleUnavailableError extends OAuthError {\n  constructor(\n    readonly reason: 'syntax' | 'domain' | 'slur' | 'taken',\n    details: string = 'That handle is not available',\n    cause?: unknown,\n  ) {\n    super('handle_unavailable', details, 400, cause)\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      reason: this.reason,\n    } as const\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-authorization-details-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationError } from './authorization-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc9396#section-14.6 | RFC 9396 - OAuth Dynamic Client Registration Metadata Registration Error}\n *\n * The AS MUST refuse to process any unknown authorization details type or\n * authorization details not conforming to the respective type definition. The\n * AS MUST abort processing and respond with an error\n * invalid_authorization_details to the client if any of the following are true\n * of the objects in the authorization_details structure:\n *  - contains an unknown authorization details type value,\n *  - is an object of known type but containing unknown fields,\n *  - contains fields of the wrong type for the authorization details type,\n *  - contains fields with invalid values for the authorization details type, or\n *  - is missing required fields for the authorization details type.\n */\nexport class InvalidAuthorizationDetailsError extends AuthorizationError {\n  constructor(\n    parameters: OAuthAuthorizationRequestParameters,\n    error_description: string,\n    cause?: unknown,\n  ) {\n    super(parameters, error_description, 'invalid_authorization_details', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-client-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }\n *\n * Client authentication failed (e.g., unknown client, no client authentication\n * included, or unsupported authentication method). The authorization server MAY\n * return an HTTP 401 (Unauthorized) status code to indicate which HTTP\n * authentication schemes are supported.  If the client attempted to\n * authenticate via the \"Authorization\" request header field, the authorization\n * server MUST respond with an HTTP 401 (Unauthorized) status code and include\n * the \"WWW-Authenticate\" response header field matching the authentication\n * scheme used by the client.\n */\nexport class InvalidClientError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('invalid_client', error_description, 400, cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-client-id-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591 - Client Registration Error Response}\n *\n * The value of one of the client metadata fields is invalid and the server has\n * rejected this request.  Note that an authorization server MAY choose to\n * substitute a valid value for any requested parameter of a client's metadata.\n */\nexport class InvalidClientIdError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('invalid_client_id', error_description, 400, cause)\n  }\n\n  static from(\n    cause: unknown,\n    fallbackMessage = 'Invalid client identifier',\n  ): InvalidClientIdError {\n    if (cause instanceof InvalidClientIdError) {\n      return cause\n    }\n    if (cause instanceof TypeError) {\n      // This method is meant to be used in the context of parsing & validating\n      // a client client metadata. In that context, a TypeError would more\n      // likely represent a problem with the data (e.g. invalid URL constructor\n      // arg) and not a programming error.\n      return new InvalidClientIdError(cause.message, cause)\n    }\n    return new InvalidClientIdError(fallbackMessage, cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-client-metadata-error.ts",
    "content": "import { ZodError } from 'zod'\nimport { FetchError } from '@atproto-labs/fetch'\nimport { OAuthError } from './oauth-error.js'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591 - Client Registration Error Response}\n *\n * The value of one of the client metadata fields is invalid and the server has\n * rejected this request.  Note that an authorization server MAY choose to\n * substitute a valid value for any requested parameter of a client's metadata.\n */\nexport class InvalidClientMetadataError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('invalid_client_metadata', error_description, 400, cause)\n  }\n\n  static from(cause: unknown, message = 'Invalid client metadata'): OAuthError {\n    if (cause instanceof OAuthError) {\n      return cause\n    }\n\n    if (cause instanceof FetchError) {\n      throw new InvalidClientMetadataError(\n        cause.expose ? `${message}: ${cause.message}` : message,\n        cause,\n      )\n    }\n\n    if (cause instanceof ZodError) {\n      const causeMessage =\n        cause.issues\n          .map(\n            ({ path, message }) =>\n              `Validation${path.length ? ` of \"${path.join('.')}\"` : ''} failed with error: ${message}`,\n          )\n          .join(' ') || cause.message\n\n      throw new InvalidClientMetadataError(\n        causeMessage ? `${message}: ${causeMessage}` : message,\n        cause,\n      )\n    }\n\n    if (\n      cause instanceof Error &&\n      'code' in cause &&\n      cause.code === 'DEPTH_ZERO_SELF_SIGNED_CERT'\n    ) {\n      throw new InvalidClientMetadataError(\n        `${message}: Self-signed certificate`,\n        cause,\n      )\n    }\n\n    if (cause instanceof TypeError) {\n      // This method is meant to be used in the context of parsing & validating\n      // a client client metadata. In that context, a TypeError would more\n      // likely represent a problem with the data (e.g. invalid URL constructor\n      // arg) and not a programming error.\n      return new InvalidClientMetadataError(\n        `${message}: ${cause.message}`,\n        cause,\n      )\n    }\n\n    return new InvalidClientMetadataError(message, cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-dpop-key-binding-error.ts",
    "content": "import { WWWAuthenticateError } from './www-authenticate-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field}\n *\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc9449#name-the-dpop-authentication-sch | RFC9449 - The DPoP Authentication Scheme}\n */\nexport class InvalidDpopKeyBindingError extends WWWAuthenticateError {\n  constructor(cause?: unknown) {\n    const error = 'invalid_token'\n    const error_description = 'Invalid DPoP key binding'\n    super(\n      error,\n      error_description,\n      { DPoP: { error, error_description } },\n      cause,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-dpop-proof-error.ts",
    "content": "import { WWWAuthenticateError } from './www-authenticate-error.js'\n\nexport class InvalidDpopProofError extends WWWAuthenticateError {\n  constructor(error_description: string, cause?: unknown) {\n    const error = 'invalid_dpop_proof'\n    super(\n      error,\n      error_description,\n      { DPoP: { error, error_description } },\n      cause,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-grant-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }\n *\n * The provided authorization grant (e.g., authorization code, resource owner\n * credentials) or refresh token is invalid, expired, revoked, does not match\n * the redirection URI used in the authorization request, or was issued to\n * another client.\n */\nexport class InvalidGrantError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('invalid_grant', error_description, 400, cause)\n  }\n\n  static from(err: unknown, error_description: string): InvalidGrantError {\n    if (err instanceof InvalidGrantError) return err\n    return new InvalidGrantError(error_description, err)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-invite-code-error.ts",
    "content": "import { InvalidRequestError } from './invalid-request-error'\n\nexport class InvalidInviteCodeError extends InvalidRequestError {\n  constructor(details?: string, cause?: unknown) {\n    super(\n      'This invite code is invalid.' + (details ? ` ${details}` : ''),\n      cause,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-redirect-uri-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591}\n *\n * The value of one or more redirection URIs is invalid.\n */\nexport class InvalidRedirectUriError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('invalid_redirect_uri', error_description, 400, cause)\n  }\n\n  static from(cause?: unknown): InvalidRedirectUriError {\n    if (cause instanceof InvalidRedirectUriError) return cause\n    return new InvalidRedirectUriError('Invalid redirect URI', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-request-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token}\n * : The request is missing a required parameter, includes an unsupported\n * parameter value (other than grant type), repeats a parameter, includes\n * multiple credentials, utilizes more than one mechanism for authenticating the\n * client, or is otherwise malformed.\n *\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 | RFC6749 - Authorization Code Grant, Authorization Request}\n * : The request is missing a required parameter, includes an invalid parameter\n * value, includes a parameter more than once, or is otherwise malformed.\n *\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field}\n * : The request is missing a required parameter, includes an unsupported\n * parameter or parameter value, repeats the same parameter, uses more than one\n * method for including an access token, or is otherwise malformed. The resource\n * server SHOULD respond with the HTTP 400 (Bad Request) status code.\n */\nexport class InvalidRequestError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('invalid_request', error_description, 400, cause)\n  }\n\n  static from(err: unknown, message = 'Invalid request data'): OAuthError {\n    if (err instanceof OAuthError) return err\n    return new InvalidRequestError(message, err)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-scope-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationError } from './authorization-error.js'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1}\n */\nexport class InvalidScopeError extends AuthorizationError {\n  constructor(\n    parameters: OAuthAuthorizationRequestParameters,\n    error_description: string,\n    cause?: unknown,\n  ) {\n    super(parameters, error_description, 'invalid_scope', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/invalid-token-error.ts",
    "content": "import { errors } from 'jose'\nimport { ZodError } from 'zod'\nimport { JwtVerifyError } from '@atproto/jwk'\nimport { OAuthError } from './oauth-error.js'\nimport { WWWAuthenticateError } from './www-authenticate-error.js'\n\nconst { JOSEError } = errors\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field }\n *\n * The access token provided is expired, revoked, malformed, or invalid for\n * other reasons.  The resource SHOULD respond with the HTTP 401 (Unauthorized)\n * status code.  The client MAY request a new access token and retry the\n * protected resource request.\n */\nexport class InvalidTokenError extends WWWAuthenticateError {\n  static from(\n    err: unknown,\n    tokenType: string,\n    fallbackMessage = 'Invalid token',\n  ): InvalidTokenError {\n    if (err instanceof InvalidTokenError) {\n      return err\n    }\n\n    if (err instanceof OAuthError) {\n      return new InvalidTokenError(tokenType, err.error_description, err)\n    }\n\n    if (err instanceof JOSEError) {\n      return new InvalidTokenError(tokenType, err.message, err)\n    }\n\n    if (err instanceof JwtVerifyError) {\n      return new InvalidTokenError(tokenType, err.message, err)\n    }\n\n    if (err instanceof ZodError) {\n      return new InvalidTokenError(tokenType, err.message, err)\n    }\n\n    return new InvalidTokenError(tokenType, fallbackMessage, err)\n  }\n\n  constructor(\n    readonly tokenType: string,\n    error_description: string,\n    cause?: unknown,\n  ) {\n    const error = 'invalid_token'\n    super(\n      error,\n      error_description,\n      { [tokenType]: { error, error_description } },\n      cause,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/login-required-error.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationError } from './authorization-error.js'\n\nexport class LoginRequiredError extends AuthorizationError {\n  constructor(\n    parameters: OAuthAuthorizationRequestParameters,\n    error_description = 'Login is required',\n    cause?: unknown,\n  ) {\n    super(parameters, error_description, 'login_required', cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/oauth-error.ts",
    "content": "export class OAuthError extends Error {\n  public expose: boolean\n\n  constructor(\n    public readonly error: string,\n    public readonly error_description: string,\n    public readonly status = 400,\n    cause?: unknown,\n  ) {\n    super(error_description, { cause })\n\n    Error.captureStackTrace?.(this, this.constructor)\n\n    this.name = this.constructor.name\n    this.expose = status < 500\n  }\n\n  get statusCode() {\n    return this.status\n  }\n\n  toJSON() {\n    return {\n      error: this.error,\n      error_description: this.error_description,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/second-authentication-factor-required-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\nexport class SecondAuthenticationFactorRequiredError extends OAuthError {\n  constructor(\n    public type: 'emailOtp',\n    public hint: string,\n    cause?: unknown,\n  ) {\n    const error = 'second_authentication_factor_required'\n    super(\n      error,\n      `${type} authentication factor required (hint: ${hint})`,\n      401,\n      cause,\n    )\n  }\n\n  toJSON() {\n    return {\n      ...super.toJSON(),\n      type: this.type,\n      hint: this.hint,\n    } as const\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/unauthorized-client-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }\n *\n * The authenticated client is not authorized to use this authorization grant\n * type.\n *\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 | RFC6749 - Authorization Code Grant, Authorization Request}\n *\n * The client is not authorized to request an authorization code using this\n * method.\n */\nexport class UnauthorizedClientError extends OAuthError {\n  constructor(error_description: string, cause?: unknown) {\n    super('unauthorized_client', error_description, 400, cause)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/use-dpop-nonce-error.ts",
    "content": "import { OAuthError } from './oauth-error.js'\nimport { WWWAuthenticateError } from './www-authenticate-error.js'\n\n/**\n * @see\n * {@link https://datatracker.ietf.org/doc/html/rfc9449#section-8 | RFC9449 - Section 8. Authorization Server-Provided Nonce}\n */\nexport class UseDpopNonceError extends OAuthError {\n  constructor(\n    error_description = 'Authorization server requires nonce in DPoP proof',\n    cause?: unknown,\n  ) {\n    super('use_dpop_nonce', error_description, 400, cause)\n  }\n\n  /**\n   * Convert this error into an error meant to be used as \"Resource\n   * Server-Provided Nonce\" error.\n   *\n   * @see\n   * {@link https://datatracker.ietf.org/doc/html/rfc9449#section-9 | RFC9449 - Section 9. Resource Server-Provided Nonce}\n   */\n  toWwwAuthenticateError(): WWWAuthenticateError {\n    const { error, error_description } = this\n    return new WWWAuthenticateError(\n      error,\n      error_description,\n      { DPoP: { error, error_description } },\n      this,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/errors/www-authenticate-error.ts",
    "content": "import { VERIFY_ALGOS } from '../lib/util/crypto.js'\nimport { OAuthError } from './oauth-error.js'\n\nexport type WWWAuthenticateParams = Record<string, string | undefined>\nexport type WWWAuthenticate = Record<string, undefined | WWWAuthenticateParams>\n\nexport class WWWAuthenticateError extends OAuthError {\n  public readonly wwwAuthenticate: WWWAuthenticate\n\n  constructor(\n    error: string,\n    error_description: string,\n    wwwAuthenticate: WWWAuthenticate,\n    cause?: unknown,\n  ) {\n    super(error, error_description, 401, cause)\n\n    this.wwwAuthenticate =\n      wwwAuthenticate['DPoP'] != null\n        ? {\n            ...wwwAuthenticate,\n            DPoP: { algs: VERIFY_ALGOS.join(' '), ...wwwAuthenticate['DPoP'] },\n          }\n        : wwwAuthenticate\n  }\n\n  get wwwAuthenticateHeader() {\n    return formatWWWAuthenticateHeader(this.wwwAuthenticate)\n  }\n}\n\nfunction formatWWWAuthenticateHeader(wwwAuthenticate: WWWAuthenticate): string {\n  return Object.entries(wwwAuthenticate)\n    .filter(isWWWAuthenticateEntry)\n    .map(wwwAuthenticateEntryToString)\n    .join(', ')\n}\n\ntype WWWAuthenticateEntry = [type: string, params: WWWAuthenticateParams]\nfunction isWWWAuthenticateEntry(\n  entry: [string, unknown],\n): entry is WWWAuthenticateEntry {\n  const [, value] = entry\n  return value != null && typeof value === 'object'\n}\n\nfunction wwwAuthenticateEntryToString([type, params]: WWWAuthenticateEntry) {\n  const paramsEnc = Object.entries(params)\n    .filter(isParamEntry)\n    .map(paramEntryToString)\n\n  return paramsEnc.length ? `${type} ${paramsEnc.join(', ')}` : type\n}\n\ntype ParamEntry = [name: string, value: string]\n\nfunction isParamEntry(entry: [string, unknown]): entry is ParamEntry {\n  const [, value] = entry\n  return typeof value === 'string' && value !== '' && !value.includes('\"')\n}\n\nfunction paramEntryToString([name, value]: ParamEntry): string {\n  return `${name}=\"${value}\"`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/index.ts",
    "content": "// Avoid having to explicitly depend sub dependencies\nexport * from '@atproto-labs/fetch'\nexport * from '@atproto-labs/fetch-node'\nexport * from '@atproto/jwk'\nexport * from '@atproto/jwk-jose'\nexport * from '@atproto/oauth-types'\n\nexport * from './constants.js'\nexport * from './oauth-client.js'\nexport * from './oauth-dpop.js'\nexport * from './oauth-errors.js'\nexport * from './oauth-hooks.js'\nexport * from './oauth-middleware.js'\nexport * from './oauth-provider.js'\nexport * from './oauth-store.js'\nexport * from './oauth-verifier.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lexicon/lexicon-data.ts",
    "content": "import { LexiconDocument } from '@atproto/lex-document'\n\nexport type { LexiconDocument }\n\nexport type LexiconData = {\n  createdAt: Date\n  updatedAt: Date\n  lastSucceededAt: null | Date\n  uri: null | string\n  lexicon: null | LexiconDocument\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lexicon/lexicon-getter.ts",
    "content": "import { LexResolver, LexResolverError } from '@atproto/lex-resolver'\nimport { Nsid } from '@atproto/oauth-scopes'\nimport { CachedGetter } from '@atproto-labs/simple-store'\nimport { LEXICON_REFRESH_FREQUENCY } from '../constants.js'\nimport { LexiconData, LexiconStore } from './lexicon-store.js'\n\n/**\n * This utility class handles the retrieval and caching of lexicon\n * data. In particular, it handles failed retrieval attempts by returning cached\n * data.\n *\n * @private\n */\nexport class LexiconGetter extends CachedGetter<Nsid, LexiconData> {\n  constructor(store: LexiconStore, lexResolver: LexResolver) {\n    super(\n      async (input, options, storedData) => {\n        const now = new Date()\n        const result = await lexResolver.get(input, options).catch((err) => {\n          // We swallow LexiconResolutionError errors, returning potentially\n          // \"null\" values here to avoid hammering the resolver with requests\n          // for the same lexicon that is known to be unavailable. The getter\n          // should be called again based on the isStale() function below.\n          if (err instanceof LexResolverError) return undefined\n\n          // Unexpected error are propagated\n          throw err\n        })\n\n        return {\n          // Keep original createdAt, if available\n          createdAt: storedData?.createdAt ?? now,\n          // Always update updatedAt\n          updatedAt: now,\n          // Update the data with fresh data, if available, or keep cached\n          // values (if any) otherwise.\n          lastSucceededAt: result ? now : storedData?.lastSucceededAt ?? null,\n          uri: result ? result.uri.toString() : storedData?.uri ?? null,\n          lexicon: result ? result.lexicon : storedData?.lexicon ?? null,\n        }\n      },\n      {\n        set: async (nsid, data) => store.storeLexicon(nsid, data),\n        get: async (nsid) => (await store.findLexicon(nsid)) ?? undefined,\n        del: async (nsid) => store.deleteLexicon(nsid),\n      },\n      {\n        isStale: (nsid, data) => {\n          const timeSinceLastUpdate = Date.now() - data.updatedAt.getTime()\n          return timeSinceLastUpdate >= LEXICON_REFRESH_FREQUENCY\n        },\n      },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lexicon/lexicon-manager.ts",
    "content": "import { LexiconPermissionSet } from '@atproto/lex-document'\nimport { LexResolver, LexResolverError } from '@atproto/lex-resolver'\nimport { IncludeScope, Nsid } from '@atproto/oauth-scopes'\nimport { LexiconGetter } from './lexicon-getter.js'\nimport { LexiconStore } from './lexicon-store.js'\n\nexport * from './lexicon-store.js'\n\nexport class LexiconManager {\n  protected readonly lexiconGetter: LexiconGetter\n\n  constructor(store: LexiconStore, lexResolver: LexResolver) {\n    this.lexiconGetter = new LexiconGetter(store, lexResolver)\n  }\n\n  public async getPermissionSetsFromScope(scope?: string) {\n    const { includeScopes } = parseScope(scope)\n    return this.extractPermissionSets(includeScopes)\n  }\n\n  /**\n   * Transforms a scope string from an authorization request into a scope\n   * composed solely of granular permission scopes, transforming any NSID\n   * into its corresponding permission scopes.\n   */\n  public async buildTokenScope(scope: string): Promise<string> {\n    const { includeScopes, otherScopes } = parseScope(scope)\n\n    // If the scope does not contain any \"include:<nsid>\" scopes, return it as-is.\n    if (!includeScopes.length) return scope\n\n    const permissionSets = await this.extractPermissionSets(includeScopes)\n\n    return Array.from(includeScopes)\n      .flatMap(nsidToPermissionScopes, permissionSets)\n      .concat(otherScopes)\n      .join(' ')\n  }\n\n  /**\n   * Given a list of scope values, extract those that are NSIDs and return their\n   * corresponding permission sets.\n   */\n  protected async extractPermissionSets(includeScopes: IncludeScope[]) {\n    const nsids = extractNsids(includeScopes)\n    return this.getPermissionSets(nsids)\n  }\n\n  protected async getPermissionSets(nsids: Set<Nsid>) {\n    return new Map<string, LexiconPermissionSet>(\n      await Promise.all(Array.from(nsids, this.getPermissionSetEntry, this)),\n    )\n  }\n\n  protected async getPermissionSetEntry(\n    nsid: Nsid,\n  ): Promise<[nsid: Nsid, permissionSet: LexiconPermissionSet]> {\n    const permissionSet = await this.getPermissionSet(nsid)\n    return [nsid, permissionSet]\n  }\n\n  protected async getPermissionSet(nsid: Nsid): Promise<LexiconPermissionSet> {\n    const { lexicon } = await this.lexiconGetter.get(nsid)\n\n    if (!lexicon) {\n      throw LexResolverError.from(nsid)\n    }\n\n    if (lexicon.defs.main?.type !== 'permission-set') {\n      const description = 'Lexicon document is not a permission set'\n      throw LexResolverError.from(nsid, description)\n    }\n\n    return lexicon.defs.main\n  }\n}\n\nfunction parseScope(scope?: string) {\n  const includeScopes: IncludeScope[] = []\n  const otherScopes: string[] = []\n\n  if (scope) {\n    for (const scopeValue of scope.split(' ')) {\n      const parsed = IncludeScope.fromString(scopeValue)\n      if (parsed) {\n        includeScopes.push(parsed)\n      } else {\n        otherScopes.push(scopeValue)\n      }\n    }\n  }\n\n  return {\n    includeScopes,\n    otherScopes,\n  }\n}\n\nfunction extractNsids(includeScopes: IncludeScope[]): Set<Nsid> {\n  return new Set(Array.from(includeScopes, extractNsid))\n}\n\nfunction extractNsid(nsidScope: IncludeScope): Nsid {\n  return nsidScope.nsid\n}\n\nexport function nsidToPermissionScopes(\n  this: Map<string, LexiconPermissionSet>,\n  includeScope: IncludeScope,\n): string[] {\n  const permissionSet = this.get(includeScope.nsid)\n  if (permissionSet) return includeScope.toScopes(permissionSet)\n\n  // Should never happen (mostly there for type safety & future proofing)\n  throw new Error(`Missing permission set for NSID: ${includeScope.nsid}`)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lexicon/lexicon-store.ts",
    "content": "import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { LexiconData, LexiconDocument } from './lexicon-data.js'\n\nexport type { Awaitable, LexiconData, LexiconDocument }\n\nexport interface LexiconStore {\n  findLexicon(nsid: string): Awaitable<LexiconData | null>\n  storeLexicon(nsid: string, data: LexiconData): Awaitable<void>\n  deleteLexicon(nsid: string): Awaitable<void>\n}\n\nexport const isLexiconStore = buildInterfaceChecker<LexiconStore>([\n  'findLexicon',\n  'storeLexicon',\n  'deleteLexicon',\n])\n\nexport function ifLexiconStore<V extends Partial<LexiconStore>>(\n  implementation?: V,\n): (V & LexiconStore) | undefined {\n  if (implementation && isLexiconStore(implementation)) {\n    return implementation\n  }\n\n  return undefined\n}\n\nexport function asLexiconStore<V extends Partial<LexiconStore>>(\n  implementation?: V,\n): V & LexiconStore {\n  const store = ifLexiconStore(implementation)\n  if (store) return store\n\n  throw new Error('Invalid LexiconStore implementation')\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/csp/index.ts",
    "content": "import { CombinedTuple, Simplify } from '../util/type.js'\n\nexport type CspValue =\n  | `data:`\n  | `http:${string}`\n  | `https:${string}`\n  | `'none'`\n  | `'self'`\n  | `'sha256-${string}'`\n  | `'nonce-${string}'`\n  | `'unsafe-inline'`\n  | `'unsafe-eval'`\n  | `'strict-dynamic'`\n  | `'report-sample'`\n  | `'unsafe-hashes'`\n\nconst STRING_DIRECTIVES = ['base-uri'] as const\nconst BOOLEAN_DIRECTIVES = [\n  'upgrade-insecure-requests',\n  'block-all-mixed-content',\n] as const\nconst ARRAY_DIRECTIVES = [\n  'connect-src',\n  'default-src',\n  'form-action',\n  'frame-ancestors',\n  'frame-src',\n  'img-src',\n  'script-src',\n  'style-src',\n] as const\n\nexport type CspConfig = Simplify<\n  {\n    [K in (typeof BOOLEAN_DIRECTIVES)[number]]?: boolean\n  } & {\n    [K in (typeof STRING_DIRECTIVES)[number]]?: CspValue\n  } & {\n    [K in (typeof ARRAY_DIRECTIVES)[number]]?: Iterable<CspValue>\n  }\n>\n\nconst NONE = \"'none'\"\n\nexport function buildCsp(config: CspConfig): string {\n  const values: string[] = []\n\n  for (const name of BOOLEAN_DIRECTIVES) {\n    if (config[name] === true) values.push(name)\n  }\n\n  for (const name of STRING_DIRECTIVES) {\n    if (config[name]) values.push(`${name} ${config[name]}`)\n  }\n\n  for (const name of ARRAY_DIRECTIVES) {\n    // Remove duplicate values by using a Set\n    const val = config[name] ? new Set(config[name]) : undefined\n    if (val?.size) values.push(`${name} ${Array.from(val).join(' ')}`)\n  }\n\n  return values.join('; ')\n}\n\nexport function mergeCsp<C extends (CspConfig | null | undefined)[]>(\n  ...configs: C\n) {\n  return configs.filter((v) => v != null).reduce(combineCsp) as CombinedTuple<C>\n}\n\nexport function combineCsp(a: CspConfig, b: CspConfig): CspConfig {\n  const result: CspConfig = {}\n\n  for (const name of BOOLEAN_DIRECTIVES) {\n    // @NOTE b (if defined) takes precedence\n    const value = b[name] ?? a[name]\n    if (value != null) result[name] = value\n  }\n\n  for (const name of STRING_DIRECTIVES) {\n    if (a[name] || b[name]) {\n      const aNotNone = a[name] === NONE ? undefined : a[name]\n      const bNotNone = b[name] === NONE ? undefined : b[name]\n      // @NOTE b takes precedence\n      result[name] = bNotNone || aNotNone || NONE\n    }\n  }\n\n  for (const name of ARRAY_DIRECTIVES) {\n    if (a[name] && b[name]) {\n      const set = new Set(a[name])\n      if (b[name]) for (const value of b[name]) set.add(value)\n      if (set.size > 1 && set.has(NONE)) set.delete(NONE)\n      result[name] = [...set]\n    } else if (a[name] || b[name]) {\n      result[name] = a[name] || b[name]\n    }\n  }\n\n  return result\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/hcaptcha.ts",
    "content": "import { createHash } from 'node:crypto'\nimport { z } from 'zod'\nimport {\n  Fetch,\n  FetchBound,\n  bindFetch,\n  fetchJsonProcessor,\n  fetchJsonZodProcessor,\n  fetchOkProcessor,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\n\nexport const hcaptchaTokenSchema = z.string().min(1)\nexport type HcaptchaToken = z.infer<typeof hcaptchaTokenSchema>\n\nexport const hcaptchaConfigSchema = z.object({\n  /**\n   * The hCaptcha site key to use for the sign-up form.\n   */\n  siteKey: z.string().min(1),\n  /**\n   * The hCaptcha secret key to use for the sign-up form.\n   */\n  secretKey: z.string().min(1),\n  /**\n   * A salt to use when hashing client tokens.\n   */\n  tokenSalt: z.string().min(1),\n  /**\n   * The risk score above which the user is considered a threat and will be\n   * denied access. This will be ignored if the enterprise features are not\n   * available.\n   *\n   * Note: Score values ranges from 0.0 (no risk) to 1.0 (confirmed threat).\n   */\n  scoreThreshold: z.number().optional(),\n})\nexport type HcaptchaConfig = z.infer<typeof hcaptchaConfigSchema>\n\n/**\n * @see {@link https://docs.hcaptcha.com/#verify-the-user-response-server-side hCaptcha API}\n */\nexport const hcaptchaVerifyResultSchema = z.object({\n  /**\n   * is the passcode valid, and does it meet security criteria you specified, e.g. sitekey?\n   */\n  success: z.boolean(),\n  /**\n   * timestamp of the challenge (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)\n   */\n  challenge_ts: z.string(),\n  /**\n   * the hostname of the site where the challenge was passed\n   */\n  hostname: z.string().nullable(),\n  /**\n   * optional: any error codes returned by the hCaptcha API.\n   * @see {@link https://docs.hcaptcha.com/#siteverify-error-codes-table}\n   */\n  'error-codes': z.array(z.string()).optional(),\n  /**\n   * ENTERPRISE feature: a score denoting malicious activity. Value ranges from\n   * 0.0 (no risk) to 1.0 (confirmed threat).\n   */\n  score: z.number().optional(),\n  /**\n   * ENTERPRISE feature: reason(s) for score.\n   */\n  score_reason: z.array(z.string()).optional(),\n  /**\n   * sitekey of the request\n   */\n  sitekey: z.string().optional(),\n  /**\n   * obj of form: {'ip_device': 1, .. etc}\n   */\n  behavior_counts: z.record(z.unknown()).optional(),\n  /**\n   * how similar is this? (0.0 - 1.0, -1 on err)\n   */\n  similarity: z.number().optional(),\n  /**\n   * count of similar_tokens not processed\n   */\n  similarity_failures: z.number().optional(),\n  /**\n   * array of strings for any similarity errors\n   */\n  similarity_error_details: z.array(z.string()).optional(),\n  /**\n   * encoded clientID\n   */\n  scoped_uid_0: z.string().optional(),\n  /**\n   * encoded IP\n   */\n  scoped_uid_1: z.string().optional(),\n  /**\n   * encoded IP (APT)\n   */\n  scoped_uid_2: z.string().optional(),\n  /**\n   * Risk Insights (APT + RI)\n   */\n  risk_insights: z.record(z.unknown()).optional(),\n  /**\n   * Advanced Threat Signatures (APT)\n   */\n  sigs: z.record(z.unknown()).optional(),\n  /**\n   * tags added via Rules\n   */\n  tags: z.array(z.string()).optional(),\n})\n\nexport type HcaptchaVerifyResult = z.infer<typeof hcaptchaVerifyResultSchema>\n\nexport type HcaptchaClientTokens = {\n  hashedIp: string\n  hashedHandle: string\n  hashedUserAgent?: string\n}\n\nconst fetchSuccessHandler = pipe(\n  fetchOkProcessor(),\n  fetchJsonProcessor(),\n  fetchJsonZodProcessor(hcaptchaVerifyResultSchema),\n)\n\nexport class HCaptchaClient {\n  protected readonly fetch: FetchBound\n  constructor(\n    readonly hostname: string,\n    readonly config: HcaptchaConfig,\n    fetch: Fetch = globalThis.fetch,\n  ) {\n    this.fetch = bindFetch(fetch)\n  }\n\n  public async verify(\n    behaviorType: 'login' | 'signup',\n    response: string,\n    remoteip: string,\n    clientTokens: HcaptchaClientTokens,\n  ) {\n    return this.fetch('https://api.hcaptcha.com/siteverify', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: new URLSearchParams({\n        secret: this.config.secretKey,\n        sitekey: this.config.siteKey,\n        behavior_type: behaviorType,\n        response,\n        remoteip,\n        client_tokens: JSON.stringify(clientTokens),\n      }).toString(),\n    }).then(fetchSuccessHandler)\n  }\n\n  public checkVerifyResult(\n    result: HcaptchaVerifyResult,\n    tokens: HcaptchaClientTokens,\n  ): void {\n    const { success, score } = result\n\n    if (success !== true) {\n      throw new HCaptchaVerifyError(\n        result,\n        tokens,\n        'Expected success to be true',\n      )\n    }\n\n    // https://docs.hcaptcha.com/#verify-the-user-response-server-side\n\n    // Please [...] note that the hostname field is derived from the user's\n    // browser, and should not be used for authentication of any kind; it is\n    // primarily useful as a statistical metric. Additionally, in the event that\n    // your site experiences unusually high challenge traffic, the hostname\n    // field may be returned as \"not-provided\" rather than the usual value; all\n    // other fields will return their normal values.\n\n    if (\n      // Ignore if enterprise feature is not enabled\n      score != null &&\n      // Ignore if disabled through config\n      this.config.scoreThreshold != null &&\n      score >= this.config.scoreThreshold\n    ) {\n      throw new HCaptchaVerifyError(\n        result,\n        tokens,\n        `Score ${score} is above the threshold ${this.config.scoreThreshold}`,\n      )\n    }\n  }\n\n  public buildClientTokens(\n    remoteip: string,\n    handle: string,\n    userAgent?: string,\n  ): HcaptchaClientTokens {\n    return {\n      hashedIp: this.hashToken(remoteip),\n      hashedHandle: this.hashToken(handle),\n      hashedUserAgent: userAgent ? this.hashToken(userAgent) : undefined,\n    }\n  }\n\n  protected hashToken(value: string) {\n    const hash = createHash('sha256')\n    hash.update(this.config.tokenSalt)\n    hash.update(value)\n    return hash.digest().toString('base64')\n  }\n}\n\nexport class HCaptchaVerifyError extends Error {\n  constructor(\n    readonly result: HcaptchaVerifyResult,\n    readonly tokens: HcaptchaClientTokens,\n    message?: string,\n  ) {\n    super(message)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/README.md",
    "content": "# Safe HTML generation and concatenation utility\n\nThis library provides a safe way to generate and concatenate HTML strings.\n\nThis code _could_ be used as a standalone library, but the Bluesky dev team does\nnot want to maintain it as such. As it is currently only used by the\n`@atproto/oauth-provider` package, it is included here. Future development\nshould aim to keep this library independent of the rest of the\n`@atproto/oauth-provider` package, so that it can be extracted and published.\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/build-document.ts",
    "content": "import { HtmlValue } from './escapers.js'\nimport { Html } from './html.js'\nimport { html } from './tags.js'\n\nexport type AssetRef = {\n  url: string\n}\n\nexport type Attrs = Record<string, boolean | string | undefined>\n\n/**\n * @see {@link https://developer.mozilla.org/fr/docs/Web/HTML/Attributes/rel}\n */\nconst ALLOWED_LINK_REL_VALUES = Object.freeze([\n  'alternate',\n  'author',\n  'canonical',\n  'dns-prefetch',\n  'external',\n  'expect',\n  'help',\n  'icon',\n  'license',\n  'manifest',\n  'me',\n  'modulepreload',\n  'next',\n  'pingback',\n  'preconnect',\n  'prefetch',\n  'preload',\n  'prerender',\n  'prev',\n  'privacy-policy',\n  'search',\n  'stylesheet',\n  'terms-of-service',\n] as const)\nexport type LinkRel = (typeof ALLOWED_LINK_REL_VALUES)[number]\nexport const isLinkRel = (rel: unknown): rel is LinkRel =>\n  (ALLOWED_LINK_REL_VALUES as readonly unknown[]).includes(rel)\n\nexport type LinkAttrs = Attrs & {\n  href: string\n  rel: LinkRel\n}\nexport type MetaAttrs =\n  | { name: string; content: string }\n  | { 'http-equiv': string; content: string }\n\nconst defaultViewport = html`<meta\n  name=\"viewport\"\n  content=\"width=device-width, initial-scale=1.0\"\n/>`\n\nexport type BuildDocumentOptions = {\n  htmlAttrs?: Attrs\n  base?: URL\n  meta?: readonly MetaAttrs[]\n  links?: readonly LinkAttrs[]\n  preloads?: readonly AssetRef[]\n  head?: HtmlValue\n  title?: HtmlValue\n  scripts?: readonly (Html | AssetRef | undefined)[]\n  styles?: readonly (Html | AssetRef | undefined)[]\n  body?: HtmlValue\n  bodyAttrs?: Attrs\n}\n\nexport const buildDocument = ({\n  htmlAttrs,\n  head,\n  title,\n  body,\n  bodyAttrs,\n  base,\n  meta,\n  links,\n  preloads,\n  scripts,\n  styles,\n}: BuildDocumentOptions) => html`<!doctype html>\n<html${attrsToHtml(htmlAttrs)}>\n  <head>\n    <meta charset=\"UTF-8\" />\n    ${title && html`<title>${title}</title>`}\n    ${base && html`<base href=\"${base.href}\" />`}\n    ${meta?.some(isViewportMeta) ? null : defaultViewport}\n    ${meta?.map(metaToHtml)}\n    ${preloads?.map(linkPreload)}\n    ${links?.map(linkToHtml)}\n    ${head}\n    ${styles?.map(styleToHtml)}\n  </head>\n  <body${attrsToHtml(bodyAttrs)}>${body}${scripts?.map(scriptToHtml)}</body>\n</html>`\n\nfunction isViewportMeta<T extends MetaAttrs>(\n  attrs: T,\n): attrs is T & { name: 'viewport' } {\n  return 'name' in attrs && attrs.name === 'viewport'\n}\n\nfunction linkToHtml(attrs: LinkAttrs) {\n  return html`<link${attrsToHtml(attrs)} />`\n}\n\nfunction metaToHtml(attrs: MetaAttrs) {\n  return html`<meta${attrsToHtml(attrs)} />`\n}\n\nfunction* attrsToHtml(attrs?: Attrs) {\n  if (attrs) {\n    for (const [name, value] of Object.entries(attrs)) {\n      if (value == null) continue\n      else if (value === false) continue\n      else if (value === true) yield html` ${name}`\n      else yield html` ${name}=\"${value}\"`\n    }\n  }\n}\n\nfunction linkPreload(asset: AssetRef) {\n  const [path] = asset.url.split('?', 2)\n\n  if (path.endsWith('.js')) {\n    return html`<link rel=\"modulepreload\" href=\"${asset.url}\" />`\n  }\n\n  if (path.endsWith('.css')) {\n    return html`<link rel=\"preload\" href=\"${asset.url}\" as=\"style\" />`\n  }\n\n  return undefined\n}\n\nfunction scriptToHtml(script?: Html | AssetRef): Html | undefined {\n  if (script == null) return undefined\n  return script instanceof Html\n    ? // prettier-ignore\n      html`<script>${script}</script>` // hash validity requires no space around the content\n    : html`<script type=\"module\" src=\"${script.url}\"></script>`\n}\n\nfunction styleToHtml(style?: Html | AssetRef): Html | undefined {\n  if (style == null) return undefined\n  return style instanceof Html\n    ? // prettier-ignore\n      html`<style>${style}</style>` // hash validity requires no space around the content\n    : html`<link rel=\"stylesheet\" href=\"${style.url}\" />`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/escapers.ts",
    "content": "import { Html } from './html.js'\nimport { NestedIterable, stringReplacer } from './util.js'\n\nexport function* javascriptEscaper(code: string) {\n  // \"</script>\" can only appear in javascript strings, so we can safely escape\n  // the \"<\" without breaking the javascript.\n  yield* stringReplacer(code, '</script>', '\\\\u003c/script>')\n}\n\nexport function* jsonEscaper(value: unknown) {\n  // https://redux.js.org/usage/server-rendering#security-considerations\n  const json = JSON.stringify(value)\n  if (json === undefined) throw new TypeError('Cannot serialize to JSON')\n  // \"<\" can only appear in JSON strings, so we can safely escape it without\n  // breaking the JSON.\n  yield* stringReplacer(json, '<', '\\\\u003c')\n}\n\nexport function* cssEscaper(css: string) {\n  yield* stringReplacer(css, '</style>', '\\\\u003c/style>')\n}\n\nexport type HtmlVariable = Html | string | number | null | undefined\nexport type HtmlValue = NestedIterable<HtmlVariable>\n\nexport function* htmlEscaper(\n  htmlFragments: TemplateStringsArray,\n  values: readonly HtmlValue[],\n): Generator<string | Html, void, undefined> {\n  for (let i = 0; i < htmlFragments.length; i++) {\n    yield htmlFragments[i]!\n\n    const value = values[i]\n    if (value != null) yield* htmlVariableToFragments(value)\n  }\n}\n\nfunction* htmlVariableToFragments(\n  value: HtmlValue,\n): Generator<string | Html, void, undefined> {\n  if (value == null) {\n    return\n  } else if (typeof value === 'number') {\n    yield String(value)\n  } else if (typeof value === 'string') {\n    yield encode(value)\n  } else if (value instanceof Html) {\n    yield value\n  } else {\n    // Will throw if the value is not an iterable\n    for (const v of value) yield* htmlVariableToFragments(v)\n  }\n}\n\nconst specialCharRegExp = /[<>\"'&]/g\nconst specialCharMap = new Map([\n  ['<', '&lt;'],\n  ['>', '&gt;'],\n  ['\"', '&quot;'],\n  [\"'\", '&apos;'],\n  ['&', '&amp;'],\n])\nconst specialCharMapGet = (c: string) => specialCharMap.get(c)!\nfunction encode(value: string): string {\n  return value.replace(specialCharRegExp, specialCharMapGet)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/html.ts",
    "content": "const symbol = Symbol('Html.dangerouslyCreate')\n\n/**\n * This class represents trusted HTML that can be safely embedded in a web page,\n * or used as fragments to build a larger HTML document.\n */\nexport class Html implements Iterable<string> {\n  readonly #fragments: readonly (Html | string)[]\n\n  private constructor(fragments: Iterable<Html | string>, guard: symbol) {\n    if (guard !== symbol) {\n      // Forces developers to use `Html.dangerouslyCreate` to create an Html\n      // instance, to make it clear that the content needs to be trusted.\n      throw new TypeError(\n        'Use Html.dangerouslyCreate() to create an Html instance',\n      )\n    }\n\n    // Transform into an array in case iterable can be consumed only once\n    // (e.g. a generator function).\n    this.#fragments = Array.from(fragments)\n  }\n\n  toString(): string {\n    // More efficient than `return this.#fragments.join('')` because it avoids\n    // creating intermediate strings when items of this.#fragments are Html\n    // instances (as all their toString() would end-up being called, creating\n    // lots of intermediary strings). The approach here allows to do a full scan\n    // of all the child nodes and concatenate them in a single pass.\n    return Array.from(this).join('')\n  }\n\n  [Symbol.toPrimitive](hint): string {\n    switch (hint) {\n      case 'string':\n      case 'default':\n        return this.toString()\n      default:\n        throw new TypeError(`Cannot convert Html to a ${hint}`)\n    }\n  }\n\n  *[Symbol.iterator](): IterableIterator<string> {\n    for (const fragment of this.#fragments) {\n      if (typeof fragment === 'string') {\n        yield fragment\n      } else {\n        yield* fragment\n      }\n    }\n  }\n\n  static dangerouslyCreate(fragments: Iterable<Html | string>): Html {\n    return new Html(fragments, symbol)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/hydration-data.ts",
    "content": "import { Html, js } from './index.js'\n\nexport function declareHydrationData<T extends Record<string, unknown>>(\n  values: T,\n): Html {\n  return Html.dangerouslyCreate(hydrationDataGenerator(values))\n}\n\nexport function* hydrationDataGenerator(\n  values: Record<string, unknown>,\n): Generator<Html> {\n  for (const [key, val] of Object.entries(values)) {\n    yield js`window[${key}]=JSON.parse(${JSON.stringify(val)});`\n  }\n  // The script tag is removed after the data is assigned to the global\n  // variables to prevent other scripts from reading the values. The \"app\"\n  // script will read the global variable and then unset it.\n  yield js`document.currentScript.remove();`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/index.ts",
    "content": "export * from './html.js'\nexport * from './tags.js'\n\n// Extra util\nexport * from './build-document.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/tags.ts",
    "content": "import {\n  HtmlValue,\n  cssEscaper,\n  htmlEscaper,\n  javascriptEscaper,\n  jsonEscaper,\n} from './escapers.js'\nimport { Html } from './html.js'\n\nexport { type HtmlValue }\nexport const html = (\n  tpl: TemplateStringsArray,\n  ...val: readonly HtmlValue[]\n) =>\n  tpl.length === 1 && val.length === 0\n    ? // Optimization for static HTML, avoid creating an iterable\n      Html.dangerouslyCreate(tpl)\n    : Html.dangerouslyCreate(htmlEscaper(tpl, val))\n\n/**\n * Escapes code to use as a JavaScript string inside a `<script>` tag.\n */\nexport const javascriptCode = (code: string) =>\n  Html.dangerouslyCreate(javascriptEscaper(code))\n\n/**\n * Creates an HTML safe JavaScript code block, with JSON serialization of the\n * injected variables.\n *\n * @example\n * ```js\n * const dataOnTheServer = { foo: 'bar' };\n * const clientScript = js`\n *   const data = ${dataOnTheServer};\n *   console.log(data);\n * `\n * console.log(clientScript.toString()); // Output: 'const data = {\"foo\":\"bar\"};console.log(data);'\n * ```\n */\nexport const js = (tpl: TemplateStringsArray, ...val: readonly unknown[]) =>\n  tpl.length === 1 && val.length === 0\n    ? // Optimization for static JavaScript, avoid un-necessary serialization\n      javascriptCode(tpl[0])\n    : javascriptCode(String.raw({ raw: tpl }, ...val.map(jsonCode)))\n\n/**\n * Escapes a value to be used as a JSON string inside a `<script>` tag.\n *\n * @see {@link https://redux.js.org/usage/server-rendering#security-considerations}\n */\nexport const jsonCode = (value: unknown) =>\n  Html.dangerouslyCreate(jsonEscaper(value))\n\n/**\n * Creates an HTML safe CSS code block.\n */\nexport const css = (tpl: TemplateStringsArray, ...val: readonly number[]) =>\n  tpl.length === 1 && val.length === 0\n    ? // Optimization for static CSS, avoid creating an iterable\n      cssCode(tpl[0])\n    : cssCode(String.raw({ raw: tpl }, ...val.map(jsonEscaper)))\n\n/**\n * Escapes a value to be uses as CSS styles inside a `<style>` tag.\n */\nexport const cssCode = (code?: string) =>\n  code ? Html.dangerouslyCreate(cssEscaper(code)) : undefined\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/html/util.ts",
    "content": "export type NestedIterable<V> = V | Iterable<NestedIterable<V>>\n\nexport function* stringReplacer(\n  source: string,\n  searchValue: string,\n  replaceValue: string,\n): Generator<string, void, undefined> {\n  let previousIndex = 0\n  let index = source.indexOf(searchValue)\n  while (index !== -1) {\n    yield source.slice(previousIndex, index)\n    yield replaceValue\n    previousIndex = index + searchValue.length\n    index = source.indexOf(searchValue, previousIndex)\n  }\n  yield source.slice(previousIndex)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/README.md",
    "content": "# utilities for generating middlewares to work with Node's http module or Express / Connect frameworks\n\nThis library uses a functional programming style to generate middleware\nfunctions that can be used with Node's http module or Express / Connect\nframeworks.\n\nThis code _could_ be used as a standalone library, but the Bluesky dev team does\nnot want to maintain it as such. As it is currently only used by the\n`@atproto/oauth-provider` package, it is included here. Future development\nshould aim to keep this library independent of the rest of the\n`@atproto/oauth-provider` package, so that it can be extracted and published.\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/accept.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { mediaType } from '@hapi/accept'\nimport { SubCtx, subCtx } from './context.js'\nimport { Middleware, NextFunction } from './types.js'\n\ntype View<\n  T extends object | void,\n  D,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n> = (\n  this: SubCtx<T, { data: D }>,\n  req: Req,\n  res: Res,\n  next: NextFunction,\n) => void | PromiseLike<void>\n\n/**\n * @example\n * ```ts\n *  app.use(\n *    acceptMiddleware(\n *      async function (req, res) {\n *        return { hello: 'world' }\n *      },\n *      {\n *        '': 'application/json', // Fallback to JSON\n *        'text/plain': function (req, res) {\n *           res.writeHead(200).end(this.data.hello)\n *         },\n *        'application/json': function (req, res) {\n *          res.writeHead(200).end(JSON.stringify(this.data))\n *        }\n *      }\n *    )\n *  )\n * ```\n */\nexport function acceptMiddleware<\n  D,\n  T extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  controller: (this: T, req: Req, res: Res) => D | PromiseLike<D>,\n  views: Record<string, string | View<T, D, Req, Res>>,\n  fallback: Middleware<T> = (req, res, _next) => void res.writeHead(406).end(),\n): (this: T, req: Req, res: Res, next: NextFunction) => Promise<void> {\n  const viewsMap = new Map(Object.entries(views))\n  const preferences = Array.from(viewsMap.keys()).filter(Boolean)\n\n  // Make sure that every view is either a function or a string that points to a\n  // function.\n  for (const type of viewsMap.keys()) {\n    const view = viewsMap.get(type)\n    if (typeof view === 'string' && typeof viewsMap.get(view) !== 'function') {\n      throw new Error(`Invalid view \"${view}\" for media type \"${type}\"`)\n    }\n  }\n\n  return async function (req, res, next) {\n    try {\n      const type = req.headers['accept']\n        ? mediaType(req.headers['accept'], preferences) || undefined\n        : '' // indicate that the client accepts anything\n\n      let view = type != null ? viewsMap.get(type) : undefined\n\n      if (typeof view === 'string') view = viewsMap.get(view)\n      if (typeof view === 'string') throw new Error('Invalid view') // should not happen\n\n      if (view) {\n        const data = await controller.call(this, req, res)\n        const ctx = subCtx(this, { data })\n        if (type) res.setHeader('Content-Type', type)\n\n        await view.call(ctx, req, res, next)\n      } else {\n        // media negotiation failed\n        await fallback.call(this, req, res, next)\n      }\n    } catch (err) {\n      if (!res.headersSent) res.removeHeader('Content-Type')\n      next(err)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/context.ts",
    "content": "export type SubCtx<Parent extends object | void, Child extends object> = Child &\n  Omit<Parent, keyof Child>\n\nexport function subCtx<Parent extends object | void, Child extends object>(\n  parent: Parent,\n  child: Child,\n): SubCtx<Parent, Child> {\n  const proto = typeof parent === 'object' ? parent : null\n  const entries = Object.entries(child)\n\n  // Optimization for small objects\n  switch (entries.length) {\n    case 0:\n      return Object.create(proto)\n    case 1: {\n      const e0 = entries[0]\n      return Object.create(proto, {\n        [e0[0]]: valueDescriptor(e0[1]),\n      })\n    }\n    case 2: {\n      const e0 = entries[0]\n      const e1 = entries[1]\n      return Object.create(proto, {\n        [e0[0]]: valueDescriptor(e0[1]),\n        [e1[0]]: valueDescriptor(e1[1]),\n      })\n    }\n  }\n\n  return Object.create(proto, Object.fromEntries(entries.map(entryToEntryDesc)))\n}\n\nfunction entryToEntryDesc(\n  entry: [string, unknown],\n): [string, PropertyDescriptor] {\n  return [entry[0], valueDescriptor(entry[1])]\n}\n\nfunction valueDescriptor(value: unknown): PropertyDescriptor {\n  return { value, enumerable: true, writable: false }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/headers.ts",
    "content": "import type { ServerResponse } from 'node:http'\n\nexport function appendHeader(\n  res: ServerResponse,\n  header: string,\n  value: string | readonly string[],\n): void {\n  const existing = res.getHeader(header)\n  if (existing == null) {\n    res.setHeader(header, value)\n  } else {\n    const arr = Array.isArray(existing) ? existing : [String(existing)]\n    res.setHeader(header, arr.concat(value))\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/index.ts",
    "content": "export * from './accept.js'\nexport * from './context.js'\nexport * from './headers.js'\nexport * from './middleware.js'\nexport * from './parser.js'\nexport * from './request.js'\nexport * from './response.js'\nexport * from './router.js'\nexport * from './stream.js'\nexport * from './types.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/method.ts",
    "content": "import type { IncomingMessage } from 'node:http'\n\nexport type MethodMatcherInput = string | Iterable<string> | MethodMatcher\nexport type MethodMatcher = (req: IncomingMessage) => boolean\n\nexport function createMethodMatcher(method: MethodMatcherInput): MethodMatcher {\n  if (method === '*') return () => true\n  if (typeof method === 'function') return method\n\n  if (typeof method === 'string') {\n    method = method.toUpperCase()\n    return (req) => req.method === method\n  }\n\n  const set = new Set(Array.from(method, (m) => m.toUpperCase()))\n  if (set.size === 0) return () => false\n  return (req) => req.method != null && set.has(req.method)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/middleware.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { invokeOnce } from '../util/function.js'\nimport { writeJson } from './response.js'\nimport { Handler, Middleware, NextFunction } from './types.js'\n\nconst isNonNullable = <X>(x: X): x is NonNullable<X> => x != null\n\nexport function combineMiddlewares<M extends Middleware<any, any, any>>(\n  middlewares: Iterable<null | undefined | M>,\n  options?: { skipKeyword?: string },\n): M\n\n/**\n * Combine express/connect like middlewares (that can be async) into a single\n * middleware.\n */\nexport function combineMiddlewares(\n  middlewares: Iterable<null | undefined | Middleware<unknown>>,\n  { skipKeyword }: { skipKeyword?: string } = {},\n): Middleware<unknown> {\n  const middlewaresArray = Array.from(middlewares).filter(isNonNullable)\n\n  // Optimization: if there are no middlewares, return a noop middleware.\n  if (middlewaresArray.length === 0) return (req, res, next) => void next()\n  if (middlewaresArray.length === 1) return middlewaresArray[0]\n\n  return function (req, res, next) {\n    let i = 0\n    const nextMiddleware = (err?: unknown) => {\n      if (err) {\n        if (skipKeyword && err === skipKeyword) next()\n        else next(err)\n      } else if (i >= middlewaresArray.length) {\n        next()\n      } else {\n        const currentMiddleware = middlewaresArray[i++]!\n        const currentNext = invokeOnce(nextMiddleware)\n        currentMiddleware.call(this, req, res, currentNext)\n      }\n    }\n    nextMiddleware()\n  }\n}\n\nexport type AsHandler<M extends Middleware<any, any, any>> =\n  M extends Middleware<infer T, infer Req, infer Res>\n    ? Handler<T, Req, Res>\n    : never\n\n/**\n * Convert a middleware in a function that can be used as both a middleware and\n * and handler.\n */\nexport function asHandler<M extends Middleware<any, any, any>>(\n  middleware: M,\n  options?: FinalHandlerOptions,\n) {\n  return function (\n    this,\n    req,\n    res,\n    next = invokeOnce(createFinalHandler(req, res, options)),\n  ) {\n    return middleware.call(this, req, res, next)\n  } as AsHandler<M>\n}\n\nexport const DEV_MODE = process.env['NODE_ENV'] === 'development'\n\nexport type FinalHandlerOptions = {\n  debug?: boolean\n}\n\nexport function createFinalHandler(\n  req: IncomingMessage,\n  res: ServerResponse,\n  options?: FinalHandlerOptions,\n): NextFunction {\n  return (err) => {\n    if (err != null && (options?.debug ?? DEV_MODE)) {\n      console.error(err)\n    }\n\n    if (res.headersSent) {\n      // If an error occurred, and headers were sent, we can't know that the\n      // whole response body was sent. So we can't safely reuse the socket.\n      if (err) req.socket.destroy()\n\n      return\n    }\n\n    const { status, ...payload } = buildFallbackPayload(req, err)\n\n    res.setHeader('Content-Security-Policy', \"default-src 'none'\")\n    res.setHeader('X-Content-Type-Options', 'nosniff')\n\n    writeJson(res, payload, { status })\n  }\n}\n\nfunction buildFallbackPayload(\n  req: IncomingMessage,\n  err: unknown,\n): {\n  status: number\n  error: string\n  error_description: string\n  stack?: undefined | string\n} {\n  const status = err ? getErrorStatusCode(err) : 404\n  const expose = getProp(err, 'expose', 'boolean') ?? status < 500\n\n  return {\n    status,\n    error: err\n      ? expose\n        ? getProp(err, 'code', 'string') ??\n          getProp(err, 'error', 'string') ??\n          'unknown_error'\n        : 'system_error'\n      : 'not_found',\n    error_description:\n      err instanceof Error\n        ? expose\n          ? getProp(err, 'error_description', 'string') ||\n            String(err.message) ||\n            'Unknown error'\n          : 'System error'\n        : `Cannot ${req.method} ${req.url}`,\n    stack: DEV_MODE && err instanceof Error ? err.stack : undefined,\n  }\n}\n\nfunction getErrorStatusCode(err: NonNullable<unknown>): number {\n  const status =\n    getProp(err, 'status', 'number') ?? getProp(err, 'statusCode', 'number')\n  return status != null && status >= 400 && status < 600 ? status : 500\n}\n\n// eslint-disable-next-line\nfunction getProp(obj: unknown, key: string, t: 'function'): Function | undefined\nfunction getProp(obj: unknown, key: string, t: 'string'): string | undefined\nfunction getProp(obj: unknown, key: string, t: 'number'): number | undefined\nfunction getProp(obj: unknown, key: string, t: 'boolean'): boolean | undefined\nfunction getProp(obj: unknown, key: string, t: 'object'): object | undefined\nfunction getProp(obj: unknown, key: string, t: 'symbol'): symbol | undefined\nfunction getProp(obj: unknown, key: string, t: 'bigint'): bigint | undefined\nfunction getProp(obj: unknown, key: string, t: 'undefined'): undefined\nfunction getProp(\n  obj: unknown,\n  key: string,\n  type:\n    | 'string'\n    | 'number'\n    | 'boolean'\n    | 'object'\n    | 'function'\n    | 'symbol'\n    | 'bigint'\n    | 'undefined',\n): unknown\n\nfunction getProp(obj: unknown, key: string, type: string): unknown {\n  if (obj != null && typeof obj === 'object' && key in obj) {\n    const value = (obj as Record<string, unknown>)[key]\n    if (typeof value === type) return value\n  }\n  return undefined\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/parser.ts",
    "content": "import { parse as parseJson } from '@hapi/bourne'\nimport { type as hapiContentType } from '@hapi/content'\nimport createHttpError from 'http-errors'\n\nexport type JsonScalar = string | number | boolean | null\nexport type Json = JsonScalar | Json[] | { [_ in string]?: Json }\n\n/**\n * Parse a content-type string into its components.\n *\n * @throws {TypeError} If the content-type is invalid.\n */\nexport function parseContentType(type: unknown): ContentType {\n  if (typeof type !== 'string') {\n    throw createHttpError(\n      415,\n      `Invalid content-type: ${type == null ? String(type) : typeof type}`,\n    )\n  }\n\n  try {\n    return hapiContentType(type)\n  } catch (err) {\n    // De-boomify the error\n    throw createHttpError(\n      415,\n      err instanceof Error ? err.message : 'Invalid content-type',\n    )\n  }\n}\n\nexport type ContentType = {\n  mime: string\n  charset?: string\n  boundary?: string\n}\n\nexport type Parser<T extends string = string, R = unknown> = {\n  readonly name: string\n  readonly test: (mime: string) => mime is T\n  readonly parse: (buffer: Buffer, type: ContentType) => R\n}\n\nexport type ParserName<P extends Parser> = P extends { readonly name: infer N }\n  ? N\n  : never\nexport type ParserType<P extends Parser> = P extends Parser<infer T> ? T : never\nexport type ParserResult<P extends Parser> = ReturnType<P['parse']>\n\nexport type ParserForType<P extends Parser, T> =\n  P extends Parser<infer U> ? (U extends T ? P : never) : never\n\nexport const parsers = [\n  {\n    name: 'json',\n    test: (mime): mime is `application/json` | `application/${string}+json` => {\n      return /^application\\/(?:.+\\+)?json$/.test(mime)\n    },\n    parse: (buffer, { charset }): Json => {\n      if (charset != null && !/^utf-?8$/i.test(charset)) {\n        throw createHttpError(415, 'Unsupported charset')\n      }\n      try {\n        return parseJson(buffer.toString())\n      } catch (err) {\n        throw createHttpError(400, 'Invalid JSON', { cause: err })\n      }\n    },\n  },\n  {\n    name: 'urlencoded',\n    test: (mime): mime is 'application/x-www-form-urlencoded' => {\n      return mime === 'application/x-www-form-urlencoded'\n    },\n    parse: (buffer, { charset }): { [_ in string]?: string } => {\n      if (charset != null && !/^utf-?8$/i.test(charset)) {\n        throw createHttpError(415, 'Unsupported charset')\n      }\n      try {\n        if (!buffer.length) return {}\n        const params = new URLSearchParams(buffer.toString())\n        if (params.has('__proto__')) throw new TypeError('Invalid key')\n        return Object.fromEntries(params)\n      } catch (err) {\n        throw createHttpError(400, 'Invalid URL-encoded data', { cause: err })\n      }\n    },\n  },\n  {\n    name: 'bytes',\n    test: (mime): mime is 'application/octet-stream' => {\n      return mime === 'application/octet-stream'\n    },\n    parse: (buffer): Buffer => buffer,\n  },\n] as const satisfies Parser[]\n\nexport type KnownParser = (typeof parsers)[number]\n\nexport type KnownNames = KnownParser['name']\nexport type KnownTypes = ParserType<KnownParser>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/path.ts",
    "content": "export type PathMatcher<P extends Params> = (pathname: string) => P | undefined\n\ntype StringPath<P extends Params> = string extends keyof P\n  ? `/${string}`\n  : keyof P extends never\n    ? `/${string}` | ``\n    : {\n        [K in keyof P]: K extends string\n          ?\n              | `${`/:${K}` | `/${string}/:${K}`}${StringPath<Omit<P, K>>}`\n              | `${StringPath<Omit<P, K>>}${`/:${K}` | `/:${K}/${string}`}`\n          : never\n      }[keyof P]\n\nexport type Path<P extends Params> =\n  | string\n  | StringPath<P>\n  | RegExp\n  | PathMatcher<P>\nexport type Params = Record<string, undefined | string>\n\nexport function createPathMatcher<P extends Params = Params>(\n  refPath: Path<P>,\n): PathMatcher<P> {\n  if (typeof refPath === 'string') {\n    // Create a path matcher for a path with parameters (like /foo/:fooId/bar/:barId).\n    if (refPath.includes('/:')) {\n      const refParts = refPath\n        .slice(1)\n        .split('/')\n        .map((part, i) => [part, i] as const)\n      const refPartsLength = refParts.length\n\n      const staticParts = refParts.filter(([p]) => !p.startsWith(':'))\n      const paramParts = refParts\n        // Extract parameters, ignoring those with no name (like /foo/:/bar).\n        .filter(([p]) => p.startsWith(':') && p.length > 1)\n        .map(([p, i]) => [p.slice(1), i] as const)\n\n      return (reqPath: string) => {\n        const reqParts = reqPath.slice(1).split('/')\n\n        if (reqParts.length !== refPartsLength) return undefined\n\n        // Make sure all static parts match.\n        for (let i = 0; i < staticParts.length; i++) {\n          const value = staticParts[i]![0]\n          const idx = staticParts[i]![1]\n\n          if (value !== reqParts[idx]) return undefined\n        }\n\n        // Then extract the parameters.\n        const params: Record<string, string> = {}\n        for (let i = 0; i < paramParts.length; i++) {\n          const name = paramParts[i]![0]\n          const idx = paramParts[i]![1]\n\n          const value = reqParts[idx]\n\n          // Empty parameter values are not allowed.\n          if (!value) return undefined\n\n          params[name] = value\n        }\n\n        return params as P\n      }\n    }\n\n    return (reqPath: string) => (reqPath === refPath ? ({} as P) : undefined)\n  }\n\n  if (refPath instanceof RegExp) {\n    return (reqPath: string) => {\n      const match = reqPath.match(refPath)\n      return match ? ((match.groups || {}) as P) : undefined\n    }\n  }\n\n  return refPath\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/request.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { languages, mediaType } from '@hapi/accept'\nimport {\n  CookieSerializeOptions,\n  parse as parseCookie,\n  serialize as serializeCookie,\n} from 'cookie'\nimport forwarded from 'forwarded'\nimport createHttpError from 'http-errors'\nimport { appendHeader } from './headers.js'\nimport { UrlReference, urlMatch } from './url.js'\n\nexport function validateHeaderValue(\n  req: IncomingMessage,\n  name: keyof IncomingMessage['headers'],\n  allowedValues: readonly (string | null)[],\n) {\n  const value = req.headers[name] ?? null\n\n  if (Array.isArray(value)) {\n    throw createHttpError(400, `Invalid ${name} header`)\n  }\n\n  if (!allowedValues.includes(value)) {\n    throw createHttpError(\n      400,\n      value\n        ? `Forbidden ${name} header \"${value}\" (expected ${allowedValues})`\n        : `Missing ${name} header`,\n    )\n  }\n}\n\nexport function validateFetchMode(\n  req: IncomingMessage,\n  expectedMode: readonly (\n    | null\n    | 'navigate'\n    | 'same-origin'\n    | 'no-cors'\n    | 'cors'\n  )[],\n) {\n  validateHeaderValue(req, 'sec-fetch-mode', expectedMode)\n}\n\nexport function validateFetchDest(\n  req: IncomingMessage,\n  expectedDest: readonly (\n    | null\n    | 'document'\n    | 'embed'\n    | 'font'\n    | 'image'\n    | 'manifest'\n    | 'media'\n    | 'object'\n    | 'report'\n    | 'script'\n    | 'serviceworker'\n    | 'sharedworker'\n    | 'style'\n    | 'worker'\n    | 'xslt'\n  )[],\n) {\n  validateHeaderValue(req, 'sec-fetch-dest', expectedDest)\n}\n\nexport function validateFetchSite(\n  req: IncomingMessage,\n  expectedSite: readonly (\n    | null\n    | 'same-origin'\n    | 'same-site'\n    | 'cross-site'\n    | 'none'\n  )[],\n) {\n  validateHeaderValue(req, 'sec-fetch-site', expectedSite)\n}\n\nexport function validateReferrer(\n  req: IncomingMessage,\n  reference: UrlReference,\n  allowNull: true,\n): URL | null\nexport function validateReferrer(\n  req: IncomingMessage,\n  reference: UrlReference,\n  allowNull?: false,\n): URL\nexport function validateReferrer(\n  req: IncomingMessage,\n  reference: UrlReference,\n  allowNull = false,\n) {\n  // @NOTE The header name \"referer\" is actually a misspelling of the word\n  // \"referrer\". https://en.wikipedia.org/wiki/HTTP_referer\n  const referrer = req.headers['referer']\n  const referrerUrl = referrer ? new URL(referrer) : null\n  if (referrerUrl ? !urlMatch(referrerUrl, reference) : !allowNull) {\n    throw createHttpError(400, `Invalid referrer ${referrer}`)\n  }\n  return referrerUrl\n}\n\nexport function validateOrigin(\n  req: IncomingMessage,\n  expectedOrigin: string,\n  optional = true,\n) {\n  const reqOrigin = req.headers['origin']\n  if (reqOrigin ? reqOrigin !== expectedOrigin : !optional) {\n    throw createHttpError(400, `Invalid origin ${reqOrigin}`)\n  }\n}\n\nexport type { CookieSerializeOptions }\n\nexport function setCookie(\n  res: ServerResponse,\n  cookieName: string,\n  value: string,\n  options?: CookieSerializeOptions,\n) {\n  appendHeader(res, 'Set-Cookie', serializeCookie(cookieName, value, options))\n}\n\nexport function getCookie(\n  req: IncomingMessage,\n  cookieName: string,\n): string | undefined {\n  const cookies = parseHttpCookies(req)\n  return Object.hasOwn(cookies, cookieName) ? cookies[cookieName] : undefined\n}\n\nexport function clearCookie(\n  res: ServerResponse,\n  cookieName: string,\n  options?: Omit<CookieSerializeOptions, 'maxAge' | 'expires'>,\n) {\n  setCookie(res, cookieName, '', { ...options, maxAge: 0 })\n}\n\nexport function parseHttpCookies(\n  req: IncomingMessage & { cookies?: any },\n): Record<string, undefined | string> {\n  req.cookies ??= req.headers['cookie']\n    ? { __proto__: null, ...parseCookie(req.headers['cookie']) }\n    : { __proto__: null }\n  return req.cookies\n}\n\nexport type ExtractRequestMetadataOptions = {\n  /**\n   * A function that determines whether a given IP address is trusted. The\n   * function is called with the IP addresses and its index in the list of\n   * forwarded addresses (starting from 0, 0 corresponding to the ip of the\n   * incoming HTTP connection, and the last item being the first proxied IP\n   * address in the proxy chain, deduced from the `X-Forwarded-For` header). The\n   * function should return `true` if the IP address is trusted, and `false`\n   * otherwise.\n   *\n   * @see {@link https://www.npmjs.com/package/proxy-addr} for a utility that\n   * allows you to create a trust function.\n   */\n  trustProxy?: (addr: string, i: number) => boolean\n}\n\nexport type RequestMetadata = {\n  userAgent?: string\n  ipAddress: string\n  port: number\n}\n\nexport function extractRequestMetadata(\n  req: IncomingMessage,\n  options?: ExtractRequestMetadataOptions,\n): RequestMetadata {\n  const ip = extractIp(req, options)\n  return {\n    userAgent: req.headers['user-agent'],\n    ipAddress: ip,\n    port: extractPort(req, ip),\n  }\n}\n\nfunction extractIp(\n  req: IncomingMessage,\n  options?: ExtractRequestMetadataOptions,\n): string {\n  const trust = options?.trustProxy\n  if (trust) {\n    const ips = forwarded(req)\n    for (let i = 0; i < ips.length; i++) {\n      const isTrusted = trust(ips[i], i)\n      if (!isTrusted) return ips[i]\n    }\n    // Let's return the last (\"furthest\") IP address in the chain if all of them\n    // are trusted. Note that this may indicate an issue with either the trust\n    // function (too permissive), or the proxy configuration (one of them not\n    // setting the X-Forwarded-For header).\n    const ip = ips[ips.length - 1]\n    if (ip) return ip\n  }\n\n  // Express app compatibility (see \"trust proxy\" setting)\n  if ('ip' in req) {\n    const ip = req.ip\n    if (typeof ip === 'string') return ip\n  }\n\n  const ip = req.socket.remoteAddress\n  if (ip) return ip\n\n  throw new Error('Could not determine IP address')\n}\n\nfunction extractPort(req: IncomingMessage, ip: string): number {\n  if (ip !== req.socket.remoteAddress) {\n    // Trust the X-Forwarded-Port header only if the IP address was a trusted\n    // proxied IP.\n    const forwardedPort = req.headers['x-forwarded-port']\n    if (typeof forwardedPort === 'string') {\n      const port = Number(forwardedPort.trim())\n      if (!Number.isInteger(port) || port < 0 || port > 65535) {\n        throw new Error('Invalid forwarded port')\n      }\n      return port\n    }\n  }\n\n  const port = req.socket.remotePort\n  if (port != null) return port\n\n  throw new Error('Could not determine port')\n}\n\nexport function extractLocales(req: IncomingMessage) {\n  const acceptLanguage = req.headers['accept-language']\n  return acceptLanguage ? languages(acceptLanguage) : []\n}\n\nexport function negotiateResponseContent<T extends string>(\n  req: IncomingMessage,\n  types: readonly T[],\n): T | undefined {\n  const type = mediaType(req.headers['accept'], types)\n  if (type) return type as T\n\n  return undefined\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/response.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { type Readable, pipeline } from 'node:stream'\nimport createHttpError from 'http-errors'\nimport { Awaitable } from '../util/type.js'\nimport { negotiateResponseContent } from './request.js'\nimport type { Handler, Middleware } from './types.js'\n\nexport function writeRedirect(\n  res: ServerResponse,\n  url: string,\n  status = 302,\n): void {\n  res.writeHead(status, { Location: url }).end()\n}\n\nexport type WriteResponseOptions = {\n  status?: number\n  contentType?: string\n}\n\nexport function writeStream(\n  res: ServerResponse,\n  stream: Readable,\n  {\n    status = 200,\n    contentType = 'application/octet-stream',\n  }: WriteResponseOptions = {},\n): void {\n  res.statusCode = status\n  res.setHeader('content-type', contentType)\n\n  if (res.req.method === 'HEAD') {\n    res.end()\n    stream.destroy()\n  } else {\n    pipeline([stream, res], (_err: Error | null) => {\n      // The error will be propagated through the streams\n    })\n  }\n}\n\nexport function writeBuffer(\n  res: ServerResponse,\n  chunk: string | Buffer,\n  opts: WriteResponseOptions,\n): void {\n  if (opts?.status != null) res.statusCode = opts.status\n  res.setHeader('content-type', opts?.contentType || 'application/octet-stream')\n  res.end(chunk)\n}\n\nexport function toJsonBuffer(value: unknown): Buffer {\n  try {\n    return Buffer.from(JSON.stringify(value))\n  } catch (cause) {\n    throw new Error(`Failed to serialize as JSON`, { cause })\n  }\n}\n\nexport function writeJson(\n  res: ServerResponse,\n  payload: unknown,\n  { contentType = 'application/json', ...options }: WriteResponseOptions = {},\n): void {\n  const buffer = toJsonBuffer(payload)\n  writeBuffer(res, buffer, { ...options, contentType })\n}\n\nexport function staticJsonMiddleware(\n  value: unknown,\n  { contentType = 'application/json', ...options }: WriteResponseOptions = {},\n): Handler<unknown> {\n  const buffer = toJsonBuffer(value)\n  const staticOptions: WriteResponseOptions = { ...options, contentType }\n  return function (req, res) {\n    writeBuffer(res, buffer, staticOptions)\n  }\n}\n\nexport function cacheControlMiddleware(maxAge: number): Middleware<void> {\n  const header = `max-age=${maxAge}`\n  return function (req, res, next) {\n    res.setHeader('Cache-Control', header)\n    next()\n  }\n}\n\nexport type JsonResponse<P = unknown> = WriteResponseOptions & {\n  json: P\n}\n\nexport function jsonHandler<\n  T,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  buildJson: (this: T, req: Req, res: Res) => Awaitable<JsonResponse>,\n): Middleware<T, Req, Res> {\n  return function (req, res, next) {\n    // Ensure we can agree on a content encoding & type before starting to\n    // build the JSON response.\n    if (negotiateResponseContent(req, ['application/json'])) {\n      // A middleware should not be async, so we wrap the async operation in a\n      // promise and return it.\n      void (async () => {\n        try {\n          const jsonResponse = await buildJson.call(this, req, res)\n          const { json, status = 200, ...options } = jsonResponse\n          writeJson(res, json, { ...options, status })\n        } catch (err) {\n          next(asError(err, 'Failed to build JSON response'))\n        }\n      })()\n    } else {\n      next(createHttpError(406, 'Unsupported media type'))\n    }\n  }\n}\n\nfunction asError(cause: unknown, message: string): Error {\n  return cause instanceof Error ? cause : new Error(message, { cause })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/route.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { SubCtx, subCtx } from './context.js'\nimport { MethodMatcherInput, createMethodMatcher } from './method.js'\nimport { combineMiddlewares } from './middleware.js'\nimport { Params, Path, createPathMatcher } from './path.js'\nimport { Middleware } from './types.js'\n\nexport type RouteCtx<\n  T extends object | void,\n  P extends Params = Params,\n> = SubCtx<T, { params: Readonly<P> }>\nexport type RouteMiddleware<\n  T extends object | void,\n  P extends Params,\n  Req = IncomingMessage,\n  Res = ServerResponse,\n> = Middleware<RouteCtx<T, P>, Req, Res>\n\n/**\n * @example\n * ```ts\n * createRoute<{ foo: string }>('GET', '/foo/:foo', function (req, res) {\n *   console.log(this.params.foo) // OK\n *   console.log(this.params.bar) // Error\n * })\n *\n * createRoute<{ foo: string }>(['POST', 'PUT'], '/foo/:foo', function (req, res) {\n *   console.log(this.params.foo) // OK\n *   console.log(this.params.bar) // Error\n * })\n * ```\n */\nexport function createRoute<\n  P extends Params = Params,\n  T extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  method: MethodMatcherInput,\n  path: Path<P>,\n  ...mw: RouteMiddleware<T, P, Req, Res>[]\n): Middleware<T, Req, Res> {\n  const paramsMatcher = createPathMatcher<P>(path)\n  const methodMatcher = createMethodMatcher(method)\n\n  const middleware = combineMiddlewares(mw, { skipKeyword: 'route' })\n\n  return function (req, res, next) {\n    if (methodMatcher(req)) {\n      const pathname = req.url?.split('?')[0] ?? '/'\n      const params = paramsMatcher(pathname)\n      if (params) {\n        const context = subCtx(this, { params })\n        return middleware.call(context, req, res, next)\n      }\n    }\n\n    return next()\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/router.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { SubCtx, subCtx } from './context.js'\nimport { MethodMatcherInput } from './method.js'\nimport { combineMiddlewares } from './middleware.js'\nimport { Params, Path } from './path.js'\nimport { RouteMiddleware, createRoute } from './route.js'\nimport { Middleware } from './types.js'\n\nexport type RouterCtx<T extends object | void = void> = SubCtx<\n  T,\n  { url: Readonly<URL> }\n>\n\nexport type RouterMiddleware<\n  T extends object | void = void,\n  Req = IncomingMessage,\n  Res = ServerResponse,\n> = Middleware<RouterCtx<T>, Req, Res>\n\nexport type RouterConfig = {\n  /** Used to build the origin of the {@link RouterCtx['url']} context property */\n  protocol?: string\n  /** Used to build the origin of the {@link RouterCtx['url']} context property */\n  host?: string\n}\n\nexport class Router<\n  T extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n> {\n  private readonly middlewares: RouterMiddleware<T, Req, Res>[] = []\n\n  constructor(private readonly config?: RouterConfig) {}\n\n  use(...middlewares: RouterMiddleware<T, Req, Res>[]) {\n    this.middlewares.push(...middlewares)\n    return this\n  }\n\n  all<P extends Params = Params>(\n    path: Path<P>,\n    ...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]\n  ) {\n    return this.addRoute<P>('*', path, ...mw)\n  }\n\n  get<P extends Params = Params>(\n    path: Path<P>,\n    ...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]\n  ) {\n    return this.addRoute<P>('GET', path, ...mw)\n  }\n\n  post<P extends Params = Params>(\n    path: Path<P>,\n    ...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]\n  ) {\n    return this.addRoute<P>('POST', path, ...mw)\n  }\n\n  options<P extends Params = Params>(\n    path: Path<P>,\n    ...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]\n  ) {\n    return this.addRoute<P>('OPTIONS', path, ...mw)\n  }\n\n  addRoute<P extends Params>(\n    method: MethodMatcherInput,\n    path: Path<P>,\n    ...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]\n  ) {\n    return this.use(createRoute(method, path, ...mw))\n  }\n\n  /**\n   * @returns router middleware which dispatches a route matching the request.\n   */\n  buildMiddleware(): Middleware<T, Req, Res> {\n    const { config } = this\n\n    // Calling next('router') from a middleware will skip all the remaining\n    // middlewares in the stack.\n    const middleware = combineMiddlewares(this.middlewares, {\n      skipKeyword: 'router',\n    })\n\n    return function (this, req, res, next) {\n      // Parse the URL using node's URL parser.\n      const url = extractUrl(req, config)\n      if (url instanceof Error) return next(url)\n\n      // Any error thrown here will be uncaught/unhandled (a middleware should\n      // never throw)\n      const context = subCtx(this, { url })\n      middleware.call(context, req, res, next)\n    }\n  }\n}\n\nfunction extractUrl(req: IncomingMessage, config?: RouterConfig): URL | Error {\n  try {\n    const protocol = config?.protocol || 'https:'\n    const host = config?.host || req.headers.host || 'localhost'\n    const pathname = req.url || '/'\n    return new URL(pathname, `${protocol}//${host}`)\n  } catch (cause) {\n    const error =\n      cause instanceof Error ? cause : new Error('Invalid URL', { cause })\n    return Object.assign(error, { status: 400, statusCode: 400 })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/security-headers.ts",
    "content": "import type { ServerResponse } from 'node:http'\nimport { type CspConfig, buildCsp } from '../csp/index.js'\n\n/**\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy COEP on MDN}\n */\nexport enum CrossOriginEmbedderPolicy {\n  unsafeNone = 'unsafe-none',\n  requireCorp = 'require-corp',\n  credentialless = 'credentialless',\n}\n\n/**\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy CORP on MDN}\n */\nexport enum CrossOriginResourcePolicy {\n  sameSite = 'same-site',\n  sameOrigin = 'same-origin',\n  crossOrigin = 'cross-origin',\n}\n\n/**\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy COOP on MDN}\n */\nexport enum CrossOriginOpenerPolicy {\n  unsafeNone = 'unsafe-none',\n  sameOriginAllowPopups = 'same-origin-allow-popups',\n  sameOrigin = 'same-origin',\n  noopenerAllowPopups = 'noopener-allow-popups',\n}\n\nexport type HTTPStrictTransportSecurityConfig = {\n  maxAge: number\n  includeSubDomains?: boolean\n  preload?: boolean\n}\n\nexport type SecurityHeadersOptions = {\n  /**\n   * Defaults to `default-src: 'none'`. Use an empty object to disable CSP.\n   * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy CSP on MDN}\n   */\n  csp?: CspConfig\n  coep?: CrossOriginEmbedderPolicy\n  corp?: CrossOriginResourcePolicy\n  coop?: CrossOriginOpenerPolicy\n  /**\n   * Defaults to 2 years. Use `false` to disable HSTS.\n   */\n  hsts?: HTTPStrictTransportSecurityConfig | false\n}\n\nexport function* buildSecurityHeaders({\n  csp = { 'default-src': [\"'none'\"] },\n  coep = CrossOriginEmbedderPolicy.requireCorp,\n  corp = CrossOriginResourcePolicy.sameOrigin,\n  coop = CrossOriginOpenerPolicy.sameOrigin,\n  hsts = { maxAge: 63072000 },\n}: SecurityHeadersOptions): Generator<[string, string], void, unknown> {\n  // @NOTE Never set CSP through http-equiv meta as not all directives will\n  // be honored. Always set it through the Content-Security-Policy header.\n  const cspString = buildCsp(csp)\n  if (cspString) {\n    yield ['Content-Security-Policy', cspString]\n  }\n\n  yield ['Cross-Origin-Embedder-Policy', coep]\n  yield ['Cross-Origin-Resource-Policy', corp]\n  yield ['Cross-Origin-Opener-Policy', coop]\n\n  if (hsts) {\n    yield ['Strict-Transport-Security', buildHstsValue(hsts)]\n  }\n\n  // @TODO make these headers configurable (?)\n  yield ['Permissions-Policy', 'otp-credentials=*, document-domain=()']\n  yield ['Referrer-Policy', 'same-origin']\n  yield ['X-Frame-Options', 'DENY']\n  yield ['X-Content-Type-Options', 'nosniff']\n  yield ['X-XSS-Protection', '0']\n}\n\nexport function setSecurityHeaders(\n  res: ServerResponse,\n  options: SecurityHeadersOptions,\n): void {\n  for (const [header, value] of buildSecurityHeaders(options)) {\n    // Only set the header if it is not already set\n    if (!res.hasHeader(header)) {\n      res.setHeader(header, value)\n    }\n  }\n}\n\nfunction buildHstsValue(config: HTTPStrictTransportSecurityConfig): string {\n  let value = `max-age=${config.maxAge}`\n  if (config.includeSubDomains) value += '; includeSubDomains'\n  if (config.preload) value += '; preload'\n  return value\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/stream.ts",
    "content": "import type { IncomingMessage } from 'node:http'\nimport { Readable } from 'node:stream'\nimport createHttpError from 'http-errors'\nimport { decodeStream, streamToNodeBuffer } from '@atproto/common'\nimport {\n  KnownNames,\n  KnownParser,\n  ParserResult,\n  parseContentType,\n  parsers,\n} from './parser.js'\n\nexport function decodeHttpRequest(req: IncomingMessage): Readable {\n  try {\n    return decodeStream(req, req.headers['content-encoding'])\n  } catch (cause) {\n    const message =\n      cause instanceof TypeError ? cause.message : `Invalid content-encoding`\n    throw createHttpError(415, message, { cause })\n  }\n}\n\n/**\n * Generic method that parses a stream of unknown nature (HTTP request/response,\n * socket, file, etc.), but of known mime type, into a parsed object.\n *\n * @throws {TypeError} If the content-type is not valid or supported.\n */\n\nexport async function parseHttpRequest<A extends readonly KnownNames[]>(\n  req: IncomingMessage,\n  allow: A,\n) {\n  const type = parseContentType(\n    req.headers['content-type'] ?? 'application/octet-stream',\n  )\n\n  const parser = parsers.find(\n    (parser) => allow.includes(parser.name) && parser.test(type.mime),\n  )\n\n  if (!parser) {\n    throw createHttpError(415, `Unsupported content-type: ${type.mime}`)\n  }\n\n  const stream = decodeHttpRequest(req)\n  const buffer = await streamToNodeBuffer(stream)\n  return parser.parse(buffer, type) as ParserResult<\n    Extract<KnownParser, { name: A[number] }>\n  >\n}\n\nexport async function flushStream(stream: AsyncIterable<any>): Promise<void> {\n  for await (const _ of stream) {\n    // Consume the stream to completion\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/types.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport type NextFunction = (err?: unknown) => void\n\nexport type Middleware<\n  T = void,\n  Req = IncomingMessage,\n  Res = ServerResponse,\n> = (this: T, req: Req, res: Res, next: NextFunction) => void\n\nexport type Handler<T = void, Req = IncomingMessage, Res = ServerResponse> = (\n  this: T,\n  req: Req,\n  res: Res,\n  next?: NextFunction,\n) => void\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/http/url.ts",
    "content": "export type UrlReference = {\n  origin?: string\n  pathname?: string\n  searchParams?: Iterable<readonly [string, string]> // compatible with URLSearchParams\n}\n\nexport function urlMatch(url: URL, reference: UrlReference) {\n  if (reference.origin !== undefined) {\n    if (url.origin !== reference.origin) return false\n  }\n\n  if (reference.pathname !== undefined) {\n    if (url.pathname !== reference.pathname) return false\n  }\n\n  if (reference.searchParams !== undefined) {\n    for (const [key, value] of reference.searchParams) {\n      if (url.searchParams.get(key) !== value) return false\n    }\n  }\n\n  return true\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/nsid.ts",
    "content": "import { NSID } from '@atproto/syntax'\nexport { NSID }\n\nexport function parseNSID(value: string): NSID | null {\n  try {\n    return NSID.parse(value)\n  } catch {\n    return null\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/redis.ts",
    "content": "import { Redis, type RedisOptions } from 'ioredis'\n\nexport type { Redis, RedisOptions }\n\nexport type CreateRedisOptions = Redis | RedisOptions | string\n\nexport function createRedis(options: CreateRedisOptions): Redis {\n  if (typeof options === 'string') {\n    const url = new URL(\n      options.startsWith('redis://') ? options : `redis://${options}`,\n    )\n\n    return new Redis({\n      host: url.hostname,\n      port: parseInt(url.port, 10),\n      password: url.password,\n    })\n  } else if ('on' in options && 'call' in options && 'acl' in options) {\n    // Not using \"instanceof\" here in case the options is an instance of another\n    // version of ioredis (Redis is both a class and an interface).\n    return options\n  } else {\n    return new Redis(options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/authorization-header.ts",
    "content": "import { z } from 'zod'\nimport {\n  oauthAccessTokenSchema,\n  oauthTokenTypeSchema,\n} from '@atproto/oauth-types'\nimport { InvalidRequestError } from '../../errors/invalid-request-error.js'\nimport { WWWAuthenticateError } from '../../errors/www-authenticate-error.js'\n\nexport const authorizationHeaderSchema = z.tuple([\n  oauthTokenTypeSchema,\n  oauthAccessTokenSchema,\n])\n\nexport const parseAuthorizationHeader = (header: unknown) => {\n  if (typeof header !== 'string') {\n    throw new WWWAuthenticateError(\n      'invalid_request',\n      'Authorization header required',\n      { Bearer: {}, DPoP: {} },\n    )\n  }\n\n  const parsed = authorizationHeaderSchema.safeParse(header.split(' '))\n  if (!parsed.success) {\n    throw new InvalidRequestError('Invalid authorization header')\n  }\n  return parsed.data\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/cast.ts",
    "content": "export function asArray<T>(value: T | T[]): T[] {\n  if (value == null) return []\n  return Array.isArray(value) ? value : [value]\n}\n\nexport function asURL(value: string | { toString: () => string }): URL {\n  return new URL(value)\n}\n\nexport function ifURL(\n  value: string | { toString: () => string },\n): URL | undefined {\n  try {\n    return asURL(value)\n  } catch {\n    return undefined\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/color.ts",
    "content": "import { parseUi8Dec, parseUi8Hex } from './ui8.js'\n\nexport type RgbColor = { r: number; g: number; b: number }\nexport type HslColor = { h: number; s: number; l: number }\nexport type RgbaColor = { r: number; g: number; b: number; a: number }\nexport type HslaColor = { h: number; s: number; l: number; a: number }\n\nexport function parseColor(color: string): RgbColor | RgbaColor {\n  if (color.startsWith('#')) {\n    return parseHexColor(color)\n  }\n\n  if (color.startsWith('rgba(')) {\n    return parseRgbaColor(color)\n  }\n\n  if (color.startsWith('rgb(')) {\n    return parseRgbColor(color)\n  }\n\n  // Should never happen (as long as the input is a validated WebColor)\n  throw new TypeError(`Invalid color value: ${color}`)\n}\n\nexport function parseHexColor(v: string): RgbColor | RgbaColor {\n  // parseInt('az', 16) does not return NaN so we need to check the format\n  if (!/^#[0-9a-f]+$/i.test(v)) {\n    throw new TypeError(`Invalid hex color value: ${v}`)\n  }\n\n  if (v.length === 4 || v.length === 5) {\n    const r = parseUi8Hex(v[1].repeat(2))\n    const g = parseUi8Hex(v[2].repeat(2))\n    const b = parseUi8Hex(v[3].repeat(2))\n    const a = v.length > 4 ? parseUi8Hex(v[4].repeat(2)) : undefined\n    return a == null ? { r, g, b } : { r, g, b, a }\n  }\n\n  if (v.length === 7 || v.length === 9) {\n    const r = parseUi8Hex(v.slice(1, 3))\n    const g = parseUi8Hex(v.slice(3, 5))\n    const b = parseUi8Hex(v.slice(5, 7))\n    const a = v.length > 8 ? parseUi8Hex(v.slice(7, 9)) : undefined\n    return a == null ? { r, g, b } : { r, g, b, a }\n  }\n\n  throw new TypeError(`Invalid hex color value: ${v}`)\n}\n\nexport function parseRgbColor(v: string): RgbColor {\n  const matches = v.match(/^\\s*rgb\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)\\s*$/)\n  if (!matches) throw new TypeError(`Invalid rgb color value: ${v}`)\n\n  const r = parseUi8Dec(matches[1])\n  const g = parseUi8Dec(matches[2])\n  const b = parseUi8Dec(matches[3])\n  return { r, g, b }\n}\n\nexport function parseRgbaColor(v: string): RgbaColor {\n  const matches = v.match(\n    /^\\s*rgba\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)\\s*$/,\n  )\n  if (!matches) throw new TypeError(`Invalid rgba color value: ${v}`)\n\n  const r = parseUi8Dec(matches[1])\n  const g = parseUi8Dec(matches[2])\n  const b = parseUi8Dec(matches[3])\n  const a = parseUi8Dec(matches[4])\n  return { r, g, b, a }\n}\n\n/**\n * Return the color that has the best contrast with the reference color.\n */\nexport function pickContrastColor(ref: RgbColor, a: RgbColor, b: RgbColor) {\n  return computeContrastRatio(ref, a) > computeContrastRatio(ref, b) ? a : b\n}\n\n/**\n * @see {@link https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef}\n */\nfunction relativeLuminance({ r, g, b }: RgbColor) {\n  return rgbLum(r) * 0.2126 + rgbLum(g) * 0.7152 + rgbLum(b) * 0.0722\n}\n\nfunction rgbLum(value) {\n  const rgb = value / 255\n  return rgb < 0.03928 ? rgb / 12.92 : Math.pow((rgb + 0.055) / 1.055, 2.4)\n}\n\n/**\n * @see {@link https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef}\n */\nfunction computeContrastRatio(a: RgbColor, b: RgbColor) {\n  const aLum = relativeLuminance(a)\n  const bLum = relativeLuminance(b)\n  const [lighter, darker] = aLum > bLum ? [aLum, bLum] : [bLum, aLum]\n  return (lighter + 0.05) / (darker + 0.05)\n}\n\nexport function extractHue(input: RgbColor): number {\n  const r = input.r / 255\n  const g = input.g / 255\n  const b = input.b / 255\n\n  const max = Math.max(r, g, b)\n  const min = Math.min(r, g, b)\n\n  const chroma = max - min\n\n  switch (max) {\n    case min:\n      return 0 // Achromatic\n    case r: {\n      const segment = (g - b) / chroma\n      const shift = segment < 0 ? 360 / 60 : 0 / 60\n      return 60 * (segment + shift)\n    }\n    case g: {\n      const segment = (b - r) / chroma\n      const shift = 120 / 60\n      return 60 * (segment + shift)\n    }\n    // \"default\" needed for type safety. In practice, should be same as \"case b:\"\n    default: {\n      const segment = (r - g) / chroma\n      const shift = 240 / 60\n      return 60 * (segment + shift)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/crypto.ts",
    "content": "import { randomBytes } from 'node:crypto'\n\nexport async function randomBuffer(bytesLength = 16) {\n  return new Promise<Buffer>((resolve, reject) => {\n    randomBytes(bytesLength, (err, buf) => {\n      if (err) return reject(err)\n      resolve(buf)\n    })\n  })\n}\n\nexport async function randomHexId(bytesLength = 16) {\n  const buffer = await randomBuffer(bytesLength)\n  return buffer.toString('hex')\n}\n\n// Basically all algorithms supported by \"jose\"'s jwtVerify().\n// @TODO Is there a way to get this list from the runtime instead of hardcoding it?\nexport const VERIFY_ALGOS = [\n  'RS256',\n  'RS384',\n  'RS512',\n\n  'PS256',\n  'PS384',\n  'PS512',\n\n  'ES256',\n  'ES256K',\n  'ES384',\n  'ES512',\n] as const\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/date.ts",
    "content": "export function dateToEpoch(date: Date = new Date()) {\n  return Math.floor(date.getTime() / 1000)\n}\n\nexport function dateToRelativeSeconds(date: Date) {\n  return Math.floor((date.getTime() - Date.now()) / 1000)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/error.ts",
    "content": "import { ZodError } from 'zod'\nimport { formatZodError } from './zod-error.js'\n\nexport function formatError(err: unknown, prefix: string): string {\n  if (err instanceof ZodError) return formatZodError(err, prefix)\n  return prefix\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/function.ts",
    "content": "/**\n * This function serves two purposes:\n * - It ensures that the return value is a Promise, even if the function returns\n *   a \"thenable\" (i.e. a Promise-like object).\n * - It allows to avoid assigning a `this` context to the function, which is\n *   particularly useful when the function is a member of a \"private\" object.\n */\nexport async function callAsync<F extends (...args: any[]) => unknown>(\n  fn: F,\n  ...args: Parameters<F>\n): Promise<Awaited<ReturnType<F>>>\nexport async function callAsync<F extends (...args: any[]) => unknown>(\n  fn?: F,\n  ...args: Parameters<F>\n): Promise<Awaited<ReturnType<F>> | undefined>\nexport async function callAsync<F extends (...args: any[]) => unknown>(\n  fn?: F,\n  ...args: Parameters<F>\n): Promise<Awaited<ReturnType<F>> | undefined> {\n  return (await fn?.(...args)) as Awaited<ReturnType<F>> | undefined\n}\n\nexport function invokeOnce<T extends (this: any, ...a: any[]) => any>(\n  fn: T,\n): T {\n  let fnNullable: T | null = fn\n  return function (...args) {\n    if (fnNullable) {\n      const fn = fnNullable\n      fnNullable = null\n      return fn.call(this, ...args)\n    }\n    throw new Error('Function called multiple times')\n  } as T\n}\n\nexport function includedIn<T>(this: readonly T[], value: T): boolean {\n  return this.includes(value)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/locale.ts",
    "content": "import { z } from 'zod'\n\nexport const localeSchema = z\n  .string()\n  .regex(/^[a-z]{2,3}(-[A-Z]{2})?$/, 'Invalid locale')\nexport type Locale = z.infer<typeof localeSchema>\n\nexport const multiLangStringSchema = z.record(\n  localeSchema,\n  z.string().optional(),\n)\nexport type MultiLangString = z.infer<typeof multiLangStringSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/object.ts",
    "content": "export function mergeDefaults<T extends object>(\n  defaults: T,\n  ...overrides: (T | undefined)[]\n): T {\n  // @NOTE Not using the spread operator here because TS allows \"undefined\"\n  // values to be spread, which can lead to defaults being overwritten with\n  // \"undefined\". This function ensures that only defined values in \"options\"\n  // will overwrite the corresponding values in \"defaults\".\n  if (!overrides.length) return defaults\n  if (!overrides.some(Boolean)) return defaults\n  const result: T = { ...defaults } as T\n  for (const options of overrides) {\n    if (options) {\n      for (const key in options) {\n        const value = options[key]\n        if (value !== undefined) {\n          result[key] = value\n        }\n      }\n    }\n  }\n  return result\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/redirect-uri.ts",
    "content": "import { isLoopbackHost } from '@atproto/oauth-types'\n\n/**\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-8.4}\n * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-8.4.2}\n */\nexport function compareRedirectUri(\n  allowed_uri: string,\n  request_uri: string,\n): boolean {\n  // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4\n  //\n  // > Authorization servers MUST require clients to register their complete\n  // > redirect URI (including the path component) and reject authorization\n  // > requests that specify a redirect URI that doesn't exactly match the\n  // > one that was registered; the exception is loopback redirects, where\n  // > an exact match is required except for the port URI component.\n  if (allowed_uri === request_uri) return true\n\n  // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3\n  const allowedUri = new URL(allowed_uri)\n  if (isLoopbackHost(allowedUri.hostname)) {\n    const requestUri = new URL(request_uri)\n\n    return (\n      // > The authorization server MUST allow any port to be specified at the\n      // > time of the request for loopback IP redirect URIs, to accommodate\n      // > clients that obtain an available ephemeral port from the operating\n      // > system at the time of the request\n      //\n      // Note: We only apply this rule if the allowed URI does not have a port\n      // specified.\n      (!allowedUri.port || allowedUri.port === requestUri.port) &&\n      allowedUri.hostname === requestUri.hostname &&\n      allowedUri.pathname === requestUri.pathname &&\n      allowedUri.protocol === requestUri.protocol &&\n      allowedUri.search === requestUri.search &&\n      allowedUri.hash === requestUri.hash &&\n      allowedUri.username === requestUri.username &&\n      allowedUri.password === requestUri.password\n    )\n  }\n\n  return false\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/time.ts",
    "content": "import { setTimeout as sleep } from 'node:timers/promises'\nimport { Awaitable } from './type.js'\n\nexport function onOvertimeDefault(options: {\n  start: number\n  end: number\n  elapsed: number\n  time: number\n}): void {\n  console.warn(\n    `constantTime: execution time was ${options.elapsed}ms (which is greater than ${options.time}ms). You should increase the \"time\" to properly defend against timing attacks.`,\n  )\n}\n\n/**\n * Utility function to protect against timing attacks.\n */\nexport async function constantTime<R>(\n  time: number,\n  fn: () => Awaitable<R>,\n  onOvertime = onOvertimeDefault,\n): Promise<R> {\n  if (!Number.isFinite(time) || time <= 0) {\n    throw new TypeError(`\"time\" must be a positive number`)\n  }\n\n  const start = Date.now()\n  try {\n    return await fn()\n  } finally {\n    const end = Date.now()\n    const elapsed = end - start\n\n    const remaining = time - elapsed\n    if (remaining >= 0) {\n      // Happy path, execution time was smaller than \"time\"\n      await sleep(remaining)\n    } else {\n      // The function execution took longer than \"time\"\n      onOvertime({ start, end, elapsed, time })\n\n      // Sleep until the next multiple of \"time\" to mitigate any attack\n      const multiplier = Math.ceil(elapsed / time)\n      const remaining = multiplier * time - elapsed\n\n      await sleep(remaining)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/type.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-types\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\nexport type Override<T, V> = Simplify<{\n  [K in keyof (V & T)]: K extends keyof V\n    ? V[K]\n    : K extends keyof T\n      ? T[K]\n      : never\n}>\nexport type Awaitable<T> = T | Promise<T>\nexport type NonNullableKeys<T, K extends keyof T> = Simplify<\n  OmitKey<T, K> & {\n    [P in K]-?: NonNullable<T[P]>\n  }\n>\n/**\n * When a type has an `[x: string]: unknown` index signature, in addition to\n * some known properties, using {@link Omit} will result in a type that only has\n * the index signature, and no known properties.\n *\n * ```ts\n * Omit<{ a: 3; b: 4; [x: string]: unknown }, 'a'> // { [x: string]: unknown }\n * ```\n *\n * In order to properly omit specific known properties from a type with an index\n * signature, we need to use another utility type that will behave correctly.\n *\n * ```ts\n * OmitKey<{ a: 3; b: 4; [x: string]: unknown }, 'a'> // { b: 4; [x: string]: unknown }\n * ```\n */\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n\nexport type RequiredKey<T, K extends keyof T = never> = Simplify<\n  T & {\n    [L in K]-?: unknown extends T[L]\n      ? NonNullable<unknown> | null\n      : Exclude<T[L], undefined>\n  }\n>\n\n/**\n * Converts a tuple to the equivalent type of combining every item into a single\n * one. If any of the item in the tuple is non nullish, the result will be non\n * nullish.\n */\nexport type CombinedTuple<T extends readonly unknown[]> = T extends []\n  ? undefined\n  : Exclude<\n      T[number],\n      // If any item in the tuple is never `null` (resp. `undefined`), exclude\n      // `null` (resp. `undefined`) from `T[number]`\n      {\n        [K in keyof T]-?:\n          | (null extends T[K] ? never : null)\n          | (undefined extends T[K] ? never : undefined)\n      }[keyof T]\n    >\n\n/**\n * Similar to {@link Required} but also ensures that all values are defined.\n */\nexport type RequiredDefined<T> = { [K in keyof T]-?: Exclude<T[K], undefined> }\n\n// <hardcore-mode> (don't touch this)\n\n/**\n * @example\n * ```ts\n * type F = UnionToFnUnion<'a' | 'b'> // (() => 'a') | (() => 'b')\n * ```\n */\ntype UnionToFnUnion<T> = T extends any ? () => T : never\n\n/**\n * @example\n * ```ts\n * type A = UnionToIntersection<(() => 'a') | (() => 'b')> // (() => 'a') & (() => 'b')\n *\n * UnionToIntersection<{ foo: string | number } | { foo: number; bar: 4 }> // { foo: number; bar: 4 }\n * ```\n */\ntype UnionToIntersection<T> = (T extends any ? (x: T) => void : never) extends (\n  x: infer U,\n) => void\n  ? U\n  : never\n\n/**\n * @example\n * ```ts\n * type B = ExtractUnionItem<'a' | 'b'> // 'b'\n * ```\n */\ntype ExtractUnionItem<T> =\n  // There exists a quirk in the way TypeScript works when inferring return\n  // types of an (disjoined) intersection of functions:\n  //\n  // type AnB = (() => 'a') & (() => 'b')\n  // type B = AnB extends () => infer R ? R : never // 'b'\n  //\n  // By turning the input union T (e.g. 'a' | 'b') into a union of function\n  // (() => 'a') | (() => 'b') and then into an intersection of those functions\n  // (() => 'a') & (() => 'b'), we can exploit the special TypeScript behavior\n  // to infer only the last return type from the functions, which is effectively\n  // equal to the last item of the input union T.\n  UnionToIntersection<UnionToFnUnion<T>> extends () => infer R ? R : never\n\n/**\n * Utility that turn a union of types (`'a' | 'b'`) into a tuple with matching\n * types (`['a', 'b']`).\n *\n * @note this only work with unions of \"const\" types. Using this with globals\n * types (`string`, etc.) will yield unexpected results.\n *\n * @example\n * ```ts\n * type T = UnionToTuple<'a' | 'b'> // ['a', 'b']\n * type T = UnionToTuple<'a' | 'b' | 'c'> // ['a', 'b', 'c']\n * ```\n */\ntype UnionToTuple<T> = UnionToTupleInternal<T>\n\ntype UnionToTupleInternal<\n  T,\n  // Accumulator for terminal recursivity (initialized to empty tuple)\n  Acc extends readonly any[] = [],\n  // Get the next item from the union (if any)\n  Next = ExtractUnionItem<T>,\n> =\n  // If there were no more items to extract from the union T, then we are done\n  [Next] extends [never]\n    ? // Return result of previous recursive calls\n      Acc\n    : // Recursively call UnionToTupleInternal by Exclude'ing the Next item from\n      // the union (T) and adding it to the accumulator.\n      UnionToTupleInternal<Exclude<T, Next>, readonly [Next, ...Acc]>\n\n/**\n * This utility allows to create an assertion function that checks if a\n * particular interface is fully implemented by some value.\n *\n * The use of the (rather complex) {@link UnionToTuple} allows to ensure that,\n * at runtime, all the required interface keys are indeed checked, and that\n * future additions to the interface do not result in a false sense of type\n * safety.\n *\n * @note This function should not be made public, as it relies on a quirk of\n * TypeScript to work properly.\n *\n * @example Valid use\n *\n * ```ts\n * const isFoo = buildInterfaceChecker<{ foo: string }>(['foo'])\n * const isFooBar = buildInterfaceChecker<{ foo: string; bar: boolean }>([\n *   'foo',\n *   'bar',\n * ])\n *\n * declare const val: { foo?: string }\n *\n * if (isFoo(val)) {\n *   val // { foo: string }\n * }\n * ```\n *\n * @example Use cases where the runtime keys do not match the interface keys\n *\n * ```ts\n * buildInterfaceChecker<{ foo: string }>([])\n * buildInterfaceChecker<{ foo: string }>(['fee'])\n * buildInterfaceChecker<{ foo: string; bar: string }>(['foo'])\n * buildInterfaceChecker<{ foo: string; bar: string }>(['foo', 'baz'])\n * ```\n */\nexport const buildInterfaceChecker =\n  <I extends object>(keys: readonly string[] & UnionToTuple<keyof I>) =>\n  <V extends Partial<I>>(value: V): value is V & RequiredDefined<I> =>\n    keys.every((name) => value[name] !== undefined)\n\n// </hardcore-mode>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/ui8.ts",
    "content": "export function parseUi8Hex(v: string) {\n  return asUi8(parseInt(v, 16))\n}\n\nexport function parseUi8Dec(v: string) {\n  return asUi8(parseInt(v, 10))\n}\n\nexport function asUi8(v: number) {\n  if (v >= 0 && v <= 255 && Number.isInteger(v)) return v\n  throw new TypeError(\n    `Invalid value \"${v}\" (expected an integer between 0 and 255)`,\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/well-known.ts",
    "content": "export function buildWellknownUrl(url: URL, name: string): URL {\n  const path =\n    url.pathname === '/'\n      ? `/.well-known/${name}`\n      : `${url.pathname.replace(/\\/+$/, '')}/${name}`\n\n  return new URL(path, url)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/util/zod-error.ts",
    "content": "import { ZodError, ZodIssue, ZodIssueCode } from 'zod'\n\nexport function formatZodError(err: ZodError, prefix?: string): string {\n  const message = err.issues.length\n    ? err.issues.map(formatZodIssue).join('; ')\n    : err.message // Should never happen (issues should never be empty)\n  return prefix ? `${prefix}: ${message}` : message\n}\n\nexport function formatZodIssue(issue: ZodIssue): string {\n  if (issue.code === ZodIssueCode.invalid_union) {\n    return issue.unionErrors\n      .map((err) => err.issues.map(formatZodIssue).join('; '))\n      .join(', or ')\n  }\n\n  if (issue.path.length === 1 && typeof issue.path[0] === 'number') {\n    return `${issue.message} at index ${issue.path[0]}`\n  }\n\n  if (issue.path.length) {\n    return `${issue.message} at ${issue.path.join('.')}`\n  }\n\n  return issue.message\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/write-form-redirect.ts",
    "content": "import type { ServerResponse } from 'node:http'\nimport { html, js } from './html/index.js'\nimport { setCookie } from './http/request.js'\nimport { SecurityHeadersOptions } from './http/security-headers.js'\nimport { writeHtml } from './write-html.js'\n\nexport type WriteFormRedirectOptions = SecurityHeadersOptions\n\n// We prevent the user from coming \"back\" to this page and resubmitting the form\n// repeatedly by disabling the submit button after the first submission.\nconst SCRIPT = js`\nconst form = document.forms[0];\n\nlet canSubmit = true;\n\nform.addEventListener('submit', (event) => {\n  if (!canSubmit) {\n    event.preventDefault();\n  } else {\n    canSubmit = false;\n  }\n});\n\nsetTimeout(() => {\n  form.submit();\n}, 1);\n`\n\n// @NOTE If translations and design are needed, consider replacing this with a\n// web app page.\n\nexport function writeFormRedirect(\n  res: ServerResponse,\n  method: 'post' | 'get',\n  uri: string,\n  params: Iterable<[string, string]>,\n  options?: WriteFormRedirectOptions,\n): void {\n  res.setHeader('Cache-Control', 'no-store')\n\n  // Prevent the Chrome from caching this page\n  // see: https://latesthackingnews.com/2023/12/12/google-updates-chrome-bfcache-for-faster-page-viewing/\n  setCookie(res, 'bfCacheBypass', 'foo', { maxAge: 1, sameSite: 'lax' })\n\n  return writeHtml(res, {\n    ...options,\n    htmlAttrs: { lang: 'en' },\n    scripts: [SCRIPT],\n    body: html`\n      <form method=\"${method}\" action=\"${uri}\">\n        ${Array.from(params, ([key, value]) => [\n          html`<input type=\"hidden\" name=\"${key}\" value=\"${value}\" />`,\n        ])}\n        <input type=\"submit\" value=\"Continue\" />\n      </form>\n    `,\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/lib/write-html.ts",
    "content": "import { createHash } from 'node:crypto'\nimport type { ServerResponse } from 'node:http'\nimport { CspValue, mergeCsp } from './csp/index.js'\nimport {\n  AssetRef,\n  BuildDocumentOptions,\n  Html,\n  buildDocument,\n} from './html/index.js'\nimport { WriteResponseOptions, writeBuffer } from './http/response.js'\nimport {\n  SecurityHeadersOptions,\n  setSecurityHeaders,\n} from './http/security-headers.js'\n\nexport type WriteHtmlOptions = BuildDocumentOptions &\n  WriteResponseOptions &\n  SecurityHeadersOptions\n\nexport function writeHtml(\n  res: ServerResponse,\n  options: WriteHtmlOptions,\n): void {\n  // @NOTE the csp string might be quite long. In that case it might be tempting\n  // to set it through the http-equiv <meta> in the HTML. However, some\n  // directives cannot be enforced by browsers when set through the meta tag\n  // (e.g. 'frame-ancestors'). Therefore, it's better to set the CSP through the\n  // HTTP header.\n  const csp = mergeCsp(\n    {\n      // Keep \"upgrade-insecure-requests\" in sync with HSTS setting. HSTS is\n      // typically set to false for localhost endpoints. Chrome and FF will\n      // ignore \"upgrade-insecure-requests\" from localhost, but Safari will\n      // enforce it, requiring to be explicitly disable it for localhost.\n      'upgrade-insecure-requests': options.hsts !== false,\n      'default-src': [\"'none'\"],\n      'base-uri': options.base?.origin as undefined | `https://${string}`,\n      'script-src': options.scripts?.map(assetToCsp).filter((v) => v != null),\n      'style-src': options.styles?.map(assetToCsp).filter((v) => v != null),\n    },\n    options.csp,\n  )\n\n  const html = buildDocument(options).toString()\n\n  // HTML pages should always be served with safety protection headers\n  setSecurityHeaders(res, { ...options, csp })\n  writeBuffer(res, html, {\n    ...options,\n    contentType: options?.contentType ?? 'text/html; charset=utf-8',\n  })\n}\n\nfunction assetToCsp(asset?: Html | AssetRef): undefined | CspValue {\n  if (asset == null) return undefined\n  if (asset instanceof Html) {\n    // Inline assets are \"allowed\" by their hash\n    const hash = createHash('sha256')\n    for (const fragment of asset) hash.update(fragment)\n    return `'sha256-${hash.digest('base64')}'`\n  } else {\n    // External assets are referenced by their origin\n    if (asset.url.startsWith('https:') || asset.url.startsWith('http:')) {\n      return new URL(asset.url).origin as `https:${string}` | `http:${string}`\n    }\n\n    // Internal assets are served from the same origin\n    return `'self'`\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/metadata/build-metadata.ts",
    "content": "import { Keyset } from '@atproto/jwk'\nimport {\n  OAuthAuthorizationServerMetadata,\n  OAuthIssuerIdentifier,\n  oauthAuthorizationServerMetadataValidator,\n} from '@atproto/oauth-types'\nimport { Client } from '../client/client.js'\nimport { VERIFY_ALGOS } from '../lib/util/crypto.js'\n\nexport type CustomMetadata = {\n  authorization_details_types_supported?: string[]\n  protected_resources?: string[]\n}\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8414#section-2}\n * @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata}\n * @see {@link https://openid.net/specs/openid-connect-prompt-create-1_0.html}\n */\nexport function buildMetadata(\n  issuer: OAuthIssuerIdentifier,\n  keyset: Keyset,\n  customMetadata?: CustomMetadata,\n): OAuthAuthorizationServerMetadata {\n  return oauthAuthorizationServerMetadataValidator.parse({\n    issuer,\n\n    scopes_supported: [\n      'atproto',\n\n      // These serve as hint that this server supports the transitional scopes.\n      // This is not a specced behavior.\n      'transition:email',\n      'transition:generic',\n      'transition:chat.bsky',\n\n      // Other atproto scopes can't be enumerated as they are dynamic.\n    ],\n    subject_types_supported: [\n      //\n      'public', // The same \"sub\" is returned for all clients\n      // 'pairwise', // A different \"sub\" is returned for each client\n    ],\n    response_types_supported: [\n      // OAuth\n      'code',\n      // 'token',\n\n      // OpenID\n      // 'none',\n      // 'code id_token token',\n      // 'code id_token',\n      // 'code token',\n      // 'id_token token',\n      // 'id_token',\n    ],\n    response_modes_supported: [\n      // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes\n      'query',\n      'fragment',\n      // https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode\n      'form_post',\n    ],\n    grant_types_supported: [\n      //\n      'authorization_code',\n      'refresh_token',\n    ],\n    code_challenge_methods_supported: [\n      // https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#pkce-code-challenge-method\n      'S256',\n\n      // atproto does not allow \"plain\"\n      // 'plain',\n    ],\n    ui_locales_supported: [\n      //\n      'en-US',\n    ],\n    display_values_supported: [\n      //\n      'page',\n      'popup',\n      'touch',\n      // 'wap', LoL\n    ],\n\n    // https://openid.net/specs/openid-connect-prompt-create-1_0.html\n    prompt_values_supported: [\n      'none',\n      'login',\n      'consent',\n      'select_account',\n      'create',\n    ],\n\n    // https://datatracker.ietf.org/doc/html/rfc9207\n    authorization_response_iss_parameter_supported: true,\n\n    // https://datatracker.ietf.org/doc/html/rfc9101#section-4\n    request_object_signing_alg_values_supported: [...VERIFY_ALGOS, 'none'],\n    request_object_encryption_alg_values_supported: [], // None\n    request_object_encryption_enc_values_supported: [], // None\n\n    request_parameter_supported: true,\n    request_uri_parameter_supported: true,\n    require_request_uri_registration: true,\n\n    jwks_uri: new URL('/oauth/jwks', issuer).href,\n\n    authorization_endpoint: new URL('/oauth/authorize', issuer).href,\n\n    token_endpoint: new URL('/oauth/token', issuer).href,\n    token_endpoint_auth_methods_supported: [...Client.AUTH_METHODS_SUPPORTED],\n    token_endpoint_auth_signing_alg_values_supported: [...VERIFY_ALGOS],\n\n    revocation_endpoint: new URL('/oauth/revoke', issuer).href,\n\n    // @TODO Should we implement these endpoints?\n    // introspection_endpoint: new URL('/oauth/introspect', issuer).href,\n    // end_session_endpoint: new URL('/oauth/logout', issuer).href,\n\n    // https://datatracker.ietf.org/doc/html/rfc9126#section-5\n    pushed_authorization_request_endpoint: new URL('/oauth/par', issuer).href,\n\n    require_pushed_authorization_requests: true,\n\n    // https://datatracker.ietf.org/doc/html/rfc9449#section-5.1\n    dpop_signing_alg_values_supported: [...VERIFY_ALGOS],\n\n    // https://datatracker.ietf.org/doc/html/rfc9396#section-14.4\n    authorization_details_types_supported:\n      customMetadata?.authorization_details_types_supported,\n\n    // https://www.rfc-editor.org/rfc/rfc9728.html#section-4\n    protected_resources: customMetadata?.protected_resources,\n\n    // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html\n    client_id_metadata_document_supported: true,\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-client.ts",
    "content": "export * from '@atproto/oauth-types'\nexport type * from './client/client.js'\nexport * from './client/client-utils.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-dpop.ts",
    "content": "export * from './dpop/dpop-nonce.js'\nexport * from './dpop/dpop-manager.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-errors.ts",
    "content": "// Root Error class\nexport { OAuthError } from './errors/oauth-error.js'\n\nexport * from './errors/access-denied-error.js'\nexport * from './errors/account-selection-required-error.js'\nexport * from './errors/authorization-error.js'\nexport * from './errors/consent-required-error.js'\nexport * from './errors/handle-unavailable-error.js'\nexport * from './errors/invalid-authorization-details-error.js'\nexport * from './errors/invalid-client-error.js'\nexport * from './errors/invalid-client-id-error.js'\nexport * from './errors/invalid-client-metadata-error.js'\nexport * from './errors/invalid-dpop-key-binding-error.js'\nexport * from './errors/invalid-dpop-proof-error.js'\nexport * from './errors/invalid-grant-error.js'\nexport * from './errors/invalid-invite-code-error.js'\nexport * from './errors/invalid-redirect-uri-error.js'\nexport * from './errors/invalid-request-error.js'\nexport * from './errors/invalid-scope-error.js'\nexport * from './errors/invalid-token-error.js'\nexport * from './errors/login-required-error.js'\nexport * from './errors/second-authentication-factor-required-error.js'\nexport * from './errors/unauthorized-client-error.js'\nexport * from './errors/use-dpop-nonce-error.js'\nexport * from './errors/www-authenticate-error.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-hooks.ts",
    "content": "import { Jwks } from '@atproto/jwk'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport {\n  OAuthAccessToken,\n  OAuthAuthorizationDetails,\n  OAuthAuthorizationRequestParameters,\n  OAuthClientMetadata,\n  OAuthTokenResponse,\n  OAuthTokenType,\n} from '@atproto/oauth-types'\nimport {\n  ResetPasswordConfirmInput,\n  ResetPasswordRequestInput,\n  SignUpData,\n} from './account/account-store.js'\nimport { SignInData } from './account/sign-in-data.js'\nimport { SignUpInput } from './account/sign-up-input.js'\nimport { ClientAuth } from './client/client-auth.js'\nimport { ClientId } from './client/client-id.js'\nimport { ClientInfo } from './client/client-info.js'\nimport { Client } from './client/client.js'\nimport { DeviceId } from './device/device-id.js'\nimport { DpopProof } from './dpop/dpop-proof.js'\nimport { AccessDeniedError } from './errors/access-denied-error.js'\nimport { AuthorizationError } from './errors/authorization-error.js'\nimport { InvalidRequestError } from './errors/invalid-request-error.js'\nimport { OAuthError } from './errors/oauth-error.js'\nimport {\n  HcaptchaClientTokens,\n  HcaptchaConfig,\n  HcaptchaVerifyResult,\n} from './lib/hcaptcha.js'\nimport { RequestMetadata } from './lib/http/request.js'\nimport { Awaitable, OmitKey } from './lib/util/type.js'\nimport { RequestId } from './request/request-id.js'\nimport { AccessTokenPayload } from './signer/access-token-payload.js'\nimport { TokenClaims } from './token/token-claims.js'\n\n// Make sure all types needed to implement the OAuthHooks are exported\nexport {\n  AccessDeniedError,\n  type AccessTokenPayload,\n  type Account,\n  AuthorizationError,\n  type Awaitable,\n  Client,\n  type ClientAuth,\n  type ClientId,\n  type ClientInfo,\n  type DeviceId,\n  type DpopProof,\n  type HcaptchaClientTokens,\n  type HcaptchaConfig,\n  type HcaptchaVerifyResult,\n  InvalidRequestError,\n  type Jwks,\n  type OAuthAccessToken,\n  type OAuthAuthorizationDetails,\n  type OAuthAuthorizationRequestParameters,\n  type OAuthClientMetadata,\n  OAuthError,\n  type OAuthTokenResponse,\n  type OAuthTokenType,\n  type RequestMetadata,\n  type ResetPasswordConfirmInput,\n  type ResetPasswordRequestInput,\n  type SignInData,\n  type SignUpData,\n  type SignUpInput,\n  type TokenClaims,\n}\n\nexport type OAuthHooks = {\n  /**\n   * Use this to alter, override or validate the client metadata & jwks returned\n   * by the client store.\n   *\n   * @throws {InvalidClientMetadataError} if the metadata is invalid\n   * @see {@link InvalidClientMetadataError}\n   */\n  getClientInfo?: (\n    clientId: ClientId,\n    data: { metadata: OAuthClientMetadata; jwks?: Jwks },\n  ) => Awaitable<undefined | Partial<ClientInfo>>\n\n  /**\n   * This hook is called when a user attempts to sign up, after every validation\n   * has passed (including hcaptcha).\n   */\n  onSignUpAttempt?: (data: {\n    input: SignUpInput\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a user attempts to sign up, after the hcaptcha\n   * `/siteverify` request has been made (and before the result is validated).\n   */\n  onHcaptchaResult?: (data: {\n    input: SignUpInput\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n    tokens: HcaptchaClientTokens\n    result: HcaptchaVerifyResult\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a user requests a password reset, before the\n   * reset password request is triggered on the account store.\n   */\n  onResetPasswordRequest?: (data: {\n    input: ResetPasswordRequestInput\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a user requests a password reset, before the\n   * reset password request is triggered on the account store.\n   */\n  onResetPasswordRequested?: (data: {\n    input: ResetPasswordRequestInput\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n    account: Account\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a user confirms a password reset, before the\n   * password is actually reset on the account store.\n   */\n  onResetPasswordConfirm?: (data: {\n    input: ResetPasswordConfirmInput\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called after a user confirms a password reset, and the\n   * password was successfully reset on the account store.\n   */\n  onResetPasswordConfirmed?: (data: {\n    input: ResetPasswordConfirmInput\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n    account: Account\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a user successfully signs up.\n   *\n   * @throws {AccessDeniedError} to deny the sign-up\n   */\n  onSignedUp?: (data: {\n    data: SignUpData\n    account: Account\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n  }) => Awaitable<void>\n\n  onSignInAttempt?: (data: {\n    data: SignInData\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a user successfully signs in.\n   *\n   * @throws {InvalidRequestError} when the sing-in should be denied\n   */\n  onSignedIn?: (data: {\n    data: SignInData\n    account: Account\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n  }) => Awaitable<void>\n\n  /**\n   * Allows validating an authorization request (typically the requested scopes)\n   * before it is created. Note that the validity against the client metadata is\n   * already enforced by the OAuth provider.\n   *\n   * @throws {AuthorizationError}\n   */\n  onAuthorizationRequest?: (data: {\n    client: Client\n    clientAuth: null | ClientAuth\n    parameters: Readonly<OAuthAuthorizationRequestParameters>\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when a client is authorized.\n   *\n   * @throws {AuthorizationError} to deny the authorization request and redirect\n   * the user to the client with an OAuth error (other errors will result in an\n   * internal server error being displayed to the user)\n   *\n   * @note We use `deviceMetadata` instead of `clientMetadata` to make it clear\n   * that this metadata is from the user device, which might be different from\n   * the client metadata (because the OAuth client could live in a backend).\n   */\n  onAuthorized?: (data: {\n    client: Client\n    account: Account\n    parameters: OAuthAuthorizationRequestParameters\n    deviceId: DeviceId\n    deviceMetadata: RequestMetadata\n    requestId: RequestId\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called whenever a token is about to be created. You can use\n   * it to modify the token claims or perform additional validation.\n   *\n   * This hook should never throw an error.\n   */\n  onCreateToken?: (data: {\n    client: Client\n    account: Account\n    parameters: OAuthAuthorizationRequestParameters\n    claims: TokenClaims\n  }) => Awaitable<void | OmitKey<AccessTokenPayload, 'iss'>>\n\n  /**\n   * This hook is called whenever a token was just decoded, and basic validation\n   * was performed (signature, expiration, not-before).\n   *\n   * It can be used to modify the payload (e.g., to add custom claims), or to\n   * perform additional validation.\n   *\n   * This hook is called when authenticating requests through the\n   * `authenticateRequest()` method in `OAuthVerifier` and `OAuthProvider`.\n   *\n   * Any error thrown here will be propagated.\n   */\n  onDecodeToken?: (data: {\n    tokenType: OAuthTokenType\n    token: OAuthAccessToken\n    payload: AccessTokenPayload\n    dpopProof: null | DpopProof\n  }) => Promise<AccessTokenPayload | void>\n\n  /**\n   * This hook is called when an authorized client exchanges an authorization\n   * code for an access token.\n   *\n   * @throws {OAuthError} to cancel the token creation and revoke the session\n   */\n  onTokenCreated?: (data: {\n    client: Client\n    clientAuth: ClientAuth\n    clientMetadata: RequestMetadata\n    account: Account\n    parameters: OAuthAuthorizationRequestParameters\n  }) => Awaitable<void>\n\n  /**\n   * This hook is called when an authorized client refreshes an access token.\n   *\n   * @throws {OAuthError} to cancel the token refresh and revoke the session\n   */\n  onTokenRefreshed?: (data: {\n    client: Client\n    clientAuth: ClientAuth\n    clientMetadata: RequestMetadata\n    account: Account\n    parameters: OAuthAuthorizationRequestParameters\n  }) => Awaitable<void>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-middleware.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { asHandler, combineMiddlewares } from './lib/http/middleware.js'\nimport { Handler } from './lib/http/types.js'\nimport { OAuthProvider } from './oauth-provider.js'\nimport { assetsMiddleware } from './router/assets/assets.js'\nimport { createAccountPageMiddleware } from './router/create-account-page-middleware.js'\nimport { createApiMiddleware } from './router/create-api-middleware.js'\nimport { createAuthorizationPageMiddleware } from './router/create-authorization-page-middleware.js'\nimport { createOAuthMiddleware } from './router/create-oauth-middleware.js'\nimport { ErrorHandler } from './router/error-handler.js'\nimport { MiddlewareOptions } from './router/middleware-options.js'\n\n// Export all the types exposed\nexport type {\n  ErrorHandler,\n  Handler,\n  IncomingMessage,\n  MiddlewareOptions,\n  ServerResponse,\n}\n\n/**\n * @returns An http request handler that can be used with node's http server\n * or as a middleware with express / connect.\n */\nexport function oauthMiddleware<\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  server: OAuthProvider,\n  { ...options }: MiddlewareOptions<Req, Res> = {},\n): Handler<void, Req, Res> {\n  const { onError } = options\n\n  // options is shallow cloned so it's fine to mutate it\n  options.onError =\n    process.env['NODE_ENV'] === 'development'\n      ? (req, res, err, msg) => {\n          console.error(`OAuthProvider error (${msg}):`, err)\n          return onError?.(req, res, err, msg)\n        }\n      : onError\n\n  return asHandler(\n    combineMiddlewares([\n      assetsMiddleware,\n      createOAuthMiddleware(server, options),\n      createApiMiddleware(server, options),\n      createAuthorizationPageMiddleware(server, options),\n      createAccountPageMiddleware(server, options),\n    ]),\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-provider.ts",
    "content": "import { createHash } from 'node:crypto'\nimport type { Redis, RedisOptions } from 'ioredis'\nimport { Jwks, Keyset } from '@atproto/jwk'\nimport { LexResolver } from '@atproto/lex-resolver'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport {\n  CLIENT_ASSERTION_TYPE_JWT_BEARER,\n  OAuthAccessToken,\n  OAuthAuthorizationCodeGrantTokenRequest,\n  OAuthAuthorizationRequestJar,\n  OAuthAuthorizationRequestPar,\n  OAuthAuthorizationRequestParameters,\n  OAuthAuthorizationRequestQuery,\n  OAuthAuthorizationServerMetadata,\n  OAuthClientCredentials,\n  OAuthClientMetadata,\n  OAuthParResponse,\n  OAuthRefreshTokenGrantTokenRequest,\n  OAuthTokenIdentification,\n  OAuthTokenRequest,\n  OAuthTokenResponse,\n  OAuthTokenType,\n  atprotoLoopbackClientMetadata,\n  oauthAuthorizationRequestParametersSchema,\n} from '@atproto/oauth-types'\nimport { safeFetchWrap } from '@atproto-labs/fetch-node'\nimport { SimpleStore } from '@atproto-labs/simple-store'\nimport { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'\nimport { AccessTokenMode } from './access-token/access-token-mode.js'\nimport { AccountManager } from './account/account-manager.js'\nimport {\n  AccountStore,\n  AuthorizedClientData,\n  DeviceAccount,\n  asAccountStore,\n} from './account/account-store.js'\nimport { ClientAuth, ClientAuthLegacy } from './client/client-auth.js'\nimport { ClientId } from './client/client-id.js'\nimport {\n  ClientManager,\n  LoopbackMetadataGetter,\n} from './client/client-manager.js'\nimport { ClientStore, ifClientStore } from './client/client-store.js'\nimport { Client } from './client/client.js'\nimport {\n  AUTHENTICATION_MAX_AGE,\n  CONFIDENTIAL_CLIENT_REFRESH_LIFETIME,\n  CONFIDENTIAL_CLIENT_SESSION_LIFETIME,\n  PUBLIC_CLIENT_REFRESH_LIFETIME,\n  PUBLIC_CLIENT_SESSION_LIFETIME,\n  TOKEN_MAX_AGE,\n} from './constants.js'\nimport { Branding, BrandingInput } from './customization/branding.js'\nimport {\n  Customization,\n  CustomizationInput,\n  customizationSchema,\n} from './customization/customization.js'\nimport { DeviceId } from './device/device-id.js'\nimport {\n  DeviceInfo,\n  DeviceManager,\n  DeviceManagerOptions,\n} from './device/device-manager.js'\nimport { DeviceStore, asDeviceStore } from './device/device-store.js'\nimport { AccountSelectionRequiredError } from './errors/account-selection-required-error.js'\nimport { AuthorizationError } from './errors/authorization-error.js'\nimport { ConsentRequiredError } from './errors/consent-required-error.js'\nimport { InvalidDpopKeyBindingError } from './errors/invalid-dpop-key-binding-error.js'\nimport { InvalidDpopProofError } from './errors/invalid-dpop-proof-error.js'\nimport { InvalidGrantError } from './errors/invalid-grant-error.js'\nimport { InvalidRequestError } from './errors/invalid-request-error.js'\nimport { LoginRequiredError } from './errors/login-required-error.js'\nimport { LexiconManager } from './lexicon/lexicon-manager.js'\nimport { LexiconStore, asLexiconStore } from './lexicon/lexicon-store.js'\nimport { HcaptchaConfig } from './lib/hcaptcha.js'\nimport { RequestMetadata } from './lib/http/request.js'\nimport { dateToRelativeSeconds } from './lib/util/date.js'\nimport { formatError } from './lib/util/error.js'\nimport { MultiLangString } from './lib/util/locale.js'\nimport { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'\nimport { OAuthHooks } from './oauth-hooks.js'\nimport {\n  DpopProof,\n  OAuthVerifier,\n  OAuthVerifierOptions,\n  VerifyTokenPayloadOptions,\n} from './oauth-verifier.js'\nimport { ReplayStore, ifReplayStore } from './replay/replay-store.js'\nimport { codeSchema } from './request/code.js'\nimport { RequestManager } from './request/request-manager.js'\nimport { RequestStore, asRequestStore } from './request/request-store.js'\nimport { parseRequestUri } from './request/request-uri.js'\nimport { AuthorizationRedirectParameters } from './result/authorization-redirect-parameters.js'\nimport { AuthorizationResultAuthorizePage } from './result/authorization-result-authorize-page.js'\nimport { AuthorizationResultRedirect } from './result/authorization-result-redirect.js'\nimport { ErrorHandler } from './router/error-handler.js'\nimport { AccessTokenPayload } from './signer/access-token-payload.js'\nimport { TokenData } from './token/token-data.js'\nimport { TokenManager } from './token/token-manager.js'\nimport {\n  TokenStore,\n  asTokenStore,\n  refreshTokenSchema,\n} from './token/token-store.js'\nimport { isPARResponseError } from './types/par-response-error.js'\n\nexport { AccessTokenMode, Keyset, LexResolver }\nexport type {\n  AccessTokenPayload,\n  AuthorizationRedirectParameters,\n  AuthorizationResultAuthorizePage as AuthorizationResultAuthorize,\n  AuthorizationResultRedirect,\n  Branding,\n  BrandingInput,\n  CustomMetadata,\n  Customization,\n  CustomizationInput,\n  ErrorHandler,\n  HcaptchaConfig,\n  MultiLangString,\n  OAuthAuthorizationServerMetadata,\n  VerifyTokenPayloadOptions,\n}\n\ntype OAuthProviderConfig = {\n  /**\n   * Maximum age a device/account session can be before requiring\n   * re-authentication.\n   */\n  authenticationMaxAge?: number\n\n  /**\n   * Maximum age access & id tokens can be before requiring a refresh.\n   */\n  tokenMaxAge?: number\n\n  /**\n   * If set to {@link AccessTokenMode.stateless}, the generated access tokens\n   * will contain all the necessary information to validate the token without\n   * needing to query the database. This is useful for cases where the Resource\n   * Server is on a different host/server than the Authorization Server.\n   *\n   * When set to {@link AccessTokenMode.light}, the access tokens will contain\n   * only the necessary information to validate the token, but the token id\n   * will need to be queried from the database to retrieve the full token\n   * information (scope, audience, etc.)\n   *\n   * @see {@link AccessTokenMode}\n   * @default {AccessTokenMode.stateless}\n   */\n  accessTokenMode?: AccessTokenMode\n\n  /**\n   * Additional metadata to be included in the discovery document.\n   */\n  metadata?: CustomMetadata\n\n  /**\n   * A Lexicon resolver instance to use for fetching lexicon schemas.\n   */\n  lexResolver?: LexResolver\n\n  /**\n   * A custom fetch function that can be used to fetch the client metadata from\n   * the internet. By default, the fetch function is a safeFetchWrap() function\n   * that protects against SSRF attacks, large responses & known bad domains. If\n   * you want to disable all protections, you can provide `globalThis.fetch` as\n   * fetch function.\n   */\n  safeFetch?: typeof globalThis.fetch\n\n  /**\n   * A redis instance to use for replay protection. If not provided, replay\n   * protection will use memory storage.\n   */\n  redis?: Redis | RedisOptions | string\n\n  /**\n   * This will be used as the default store for all the stores. If a store is\n   * not provided, this store will be used instead. If the `store` does not\n   * implement a specific store, a runtime error will be thrown. Make sure that\n   * this store implements all the interfaces not provided in the other\n   * `<name>Store` options.\n   */\n  store?: Partial<\n    AccountStore &\n      ClientStore &\n      DeviceStore &\n      LexiconStore &\n      ReplayStore &\n      RequestStore &\n      TokenStore\n  >\n\n  accountStore?: AccountStore\n  clientStore?: ClientStore\n  deviceStore?: DeviceStore\n  lexiconStore?: LexiconStore\n  replayStore?: ReplayStore\n  requestStore?: RequestStore\n  tokenStore?: TokenStore\n\n  /**\n   * In order to speed up the client fetching process, you can provide a cache\n   * to store HTTP responses.\n   *\n   * @note the cached entries should automatically expire after a certain time (typically 10 minutes)\n   */\n  clientJwksCache?: SimpleStore<string, Jwks>\n\n  /**\n   * In order to speed up the client fetching process, you can provide a cache\n   * to store HTTP responses.\n   *\n   * @note the cached entries should automatically expire after a certain time (typically 10 minutes)\n   */\n  clientMetadataCache?: SimpleStore<string, OAuthClientMetadata>\n\n  /**\n   * In order to enable loopback clients, you can provide a function that\n   * returns the client metadata for a given loopback URL. This is useful for\n   * development and testing purposes. This function is not called for internet\n   * clients.\n   *\n   * @default is as specified by ATPROTO\n   */\n  loopbackMetadata?: null | false | LoopbackMetadataGetter\n}\n\nexport type OAuthProviderOptions = OAuthProviderConfig &\n  OAuthVerifierOptions &\n  OAuthHooks &\n  DeviceManagerOptions &\n  CustomizationInput\n\nexport class OAuthProvider extends OAuthVerifier {\n  protected readonly accessTokenMode: AccessTokenMode\n  protected readonly hooks: OAuthHooks\n\n  public readonly metadata: OAuthAuthorizationServerMetadata\n  public readonly customization: Customization\n\n  public readonly authenticationMaxAge: number\n\n  public readonly accountManager: AccountManager\n  public readonly deviceManager: DeviceManager\n  public readonly clientManager: ClientManager\n  public readonly lexiconManager: LexiconManager\n  public readonly requestManager: RequestManager\n  public readonly tokenManager: TokenManager\n\n  public constructor({\n    // OAuthProviderConfig\n    authenticationMaxAge = AUTHENTICATION_MAX_AGE,\n    tokenMaxAge = TOKEN_MAX_AGE,\n    accessTokenMode = AccessTokenMode.stateless,\n\n    metadata,\n\n    safeFetch = safeFetchWrap(),\n    store, // compound store implementation\n    lexResolver = new LexResolver({ fetch: safeFetch }),\n\n    // Required stores\n    accountStore = asAccountStore(store),\n    deviceStore = asDeviceStore(store),\n    lexiconStore = asLexiconStore(store),\n    tokenStore = asTokenStore(store),\n    requestStore = asRequestStore(store),\n\n    // Optional stores\n    clientStore = ifClientStore(store),\n    replayStore = ifReplayStore(store),\n\n    clientJwksCache = new SimpleStoreMemory({\n      maxSize: 50_000_000,\n      ttl: 600e3,\n    }),\n    clientMetadataCache = new SimpleStoreMemory({\n      maxSize: 50_000_000,\n      ttl: 600e3,\n    }),\n\n    loopbackMetadata = atprotoLoopbackClientMetadata,\n\n    // OAuthHooks &\n    // OAuthVerifierOptions &\n    // DeviceManagerOptions &\n    // Customization\n    ...rest\n  }: OAuthProviderOptions) {\n    super({ replayStore, ...rest })\n\n    // @NOTE: hooks don't really need a type parser, as all zod can actually\n    // check at runtime is the fact that the values are functions. The only way\n    // we would benefit from zod here would be to wrap the functions with a\n    // validator for the provided function's return types, which we don't\n    // really need if types are respected.\n    this.hooks = rest\n\n    this.accessTokenMode = accessTokenMode\n    this.authenticationMaxAge = authenticationMaxAge\n    this.metadata = buildMetadata(this.issuer, this.keyset, metadata)\n    this.customization = customizationSchema.parse(rest)\n\n    this.deviceManager = new DeviceManager(deviceStore, {\n      ...rest,\n      cookie: {\n        ...rest.cookie,\n        // \"secure\" defaults to \"true\" in DeviceManager. For the oauth routes to\n        // work from localhost on Safari, we need to explicitly set secure to\n        // false for localhost usage. This is not really an issue with Chrome\n        // and Firefox, but Safari enforces it strictly.\n        secure: !this.issuer.startsWith('http:'),\n      },\n    })\n    this.accountManager = new AccountManager(\n      this.issuer,\n      accountStore,\n      this.hooks,\n      this.customization,\n    )\n    this.clientManager = new ClientManager(\n      this.metadata,\n      this.keyset,\n      this.hooks,\n      clientStore || null,\n      loopbackMetadata || null,\n      safeFetch,\n      clientJwksCache,\n      clientMetadataCache,\n    )\n    this.lexiconManager = new LexiconManager(lexiconStore, lexResolver)\n    this.requestManager = new RequestManager(\n      requestStore,\n      this.lexiconManager,\n      this.signer,\n      this.metadata,\n      this.hooks,\n    )\n    this.tokenManager = new TokenManager(\n      tokenStore,\n      this.lexiconManager,\n      this.signer,\n      this.hooks,\n      this.accessTokenMode,\n      tokenMaxAge,\n    )\n  }\n\n  get jwks() {\n    return this.keyset.publicJwks\n  }\n\n  /**\n   * @returns true if the user's consent is required for the requested scopes\n   */\n  public checkConsentRequired(\n    parameters: OAuthAuthorizationRequestParameters,\n    clientData?: AuthorizedClientData,\n  ) {\n    // Client was never authorized before\n    if (!clientData) return true\n\n    // Client explicitly asked for consent\n    if (parameters.prompt === 'consent') return true\n\n    // No scope requested, and client is known by user, no consent required\n    const requestedScopes = parameters.scope?.split(' ')\n    if (requestedScopes == null) return false\n\n    // Ensure that all requested scopes were previously authorized by the user\n    const { authorizedScopes } = clientData\n    return !requestedScopes.every((scope) => authorizedScopes.includes(scope))\n  }\n\n  public checkLoginRequired(deviceAccount: DeviceAccount) {\n    const authAge = Date.now() - deviceAccount.updatedAt.getTime()\n    return authAge > this.authenticationMaxAge\n  }\n\n  protected async authenticateClient(\n    clientCredentials: OAuthClientCredentials,\n    dpopProof: null | DpopProof,\n    options?: {\n      allowMissingDpopProof?: boolean\n    },\n  ): Promise<{\n    client: Client\n    clientAuth: ClientAuth\n  }> {\n    const client = await this.clientManager.getClient(\n      clientCredentials.client_id,\n    )\n\n    if (\n      client.metadata.dpop_bound_access_tokens &&\n      !dpopProof &&\n      !options?.allowMissingDpopProof\n    ) {\n      throw new InvalidDpopProofError('DPoP proof required')\n    }\n\n    if (dpopProof && !client.metadata.dpop_bound_access_tokens) {\n      throw new InvalidDpopProofError('DPoP proof not allowed for this client')\n    }\n\n    const clientAuth = await client.authenticate(clientCredentials, {\n      authorizationServerIdentifier: this.issuer,\n    })\n\n    if (clientAuth.method === 'private_key_jwt') {\n      // Clients MUST NOT use their client assertion key to sign DPoP proofs\n      if (dpopProof && clientAuth.jkt === dpopProof.jkt) {\n        throw new InvalidRequestError(\n          'The DPoP proof must be signed with a different key than the client assertion',\n        )\n      }\n\n      // https://www.rfc-editor.org/rfc/rfc7523.html#section-3\n      // > 7.  [...] The authorization server MAY ensure that JWTs are not\n      // >     replayed by maintaining the set of used \"jti\" values for the\n      // >     length of time for which the JWT would be considered valid based\n      // >     on the applicable \"exp\" instant.\n\n      const unique = await this.replayManager.uniqueAuth(\n        clientAuth.jti,\n        client.id,\n        clientAuth.exp,\n      )\n      if (!unique) {\n        throw new InvalidGrantError(`${clientAuth.method} jti reused`)\n      }\n    }\n\n    return { client, clientAuth }\n  }\n\n  async decodeJAR(\n    client: Client,\n    input: OAuthAuthorizationRequestJar,\n  ): Promise<OAuthAuthorizationRequestParameters> {\n    const { payload } = await client.decodeRequestObject(\n      input.request,\n      this.issuer,\n    )\n\n    const { jti } = payload\n    if (!jti) {\n      throw new InvalidRequestError(\n        'Request object payload must contain a \"jti\" claim',\n      )\n    }\n    if (!(await this.replayManager.uniqueJar(jti, client.id))) {\n      throw new InvalidRequestError('Request object was replayed')\n    }\n\n    const parameters = await oauthAuthorizationRequestParametersSchema\n      .parseAsync(payload)\n      .catch((err) => {\n        const msg = formatError(err, 'Invalid parameters in JAR')\n        throw new InvalidRequestError(msg, err)\n      })\n\n    return parameters\n  }\n\n  /**\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc9126}\n   */\n  public async pushedAuthorizationRequest(\n    credentials: OAuthClientCredentials,\n    authorizationRequest: OAuthAuthorizationRequestPar,\n    dpopProof: null | DpopProof,\n  ): Promise<OAuthParResponse> {\n    try {\n      const { client, clientAuth } = await this.authenticateClient(\n        credentials,\n        dpopProof,\n        // Allow missing DPoP header for PAR requests as rfc9449 allows it\n        // (though the dpop_jkt parameter must be present in that case, see\n        // check bellow).\n        { allowMissingDpopProof: true },\n      )\n\n      const parameters =\n        'request' in authorizationRequest // Handle JAR\n          ? await this.decodeJAR(client, authorizationRequest)\n          : authorizationRequest\n\n      if (!parameters.dpop_jkt) {\n        if (client.metadata.dpop_bound_access_tokens) {\n          if (dpopProof) parameters.dpop_jkt = dpopProof.jkt\n          else {\n            // @NOTE When both PAR and DPoP are used, either the DPoP header, or\n            // the dpop_jkt parameter must be present. We do not enforce this\n            // for legacy reasons.\n            // https://datatracker.ietf.org/doc/html/rfc9449#section-10.1\n          }\n        }\n      } else {\n        if (!client.metadata.dpop_bound_access_tokens) {\n          throw new InvalidRequestError(\n            'DPoP bound access tokens are not enabled for this client',\n          )\n        }\n\n        // Proof is optional if the dpop_jkt is provided, but if it is provided,\n        // it must match the DPoP proof JKT.\n        if (dpopProof && dpopProof.jkt !== parameters.dpop_jkt) {\n          throw new InvalidDpopKeyBindingError()\n        }\n      }\n\n      const { requestUri, expiresAt } =\n        await this.requestManager.createAuthorizationRequest(\n          client,\n          clientAuth,\n          parameters,\n          null,\n        )\n\n      return {\n        request_uri: requestUri,\n        expires_in: dateToRelativeSeconds(expiresAt),\n      }\n    } catch (err) {\n      // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1\n      // > Since initial processing of the pushed authorization request does not\n      // > involve resource owner interaction, error codes related to user\n      // > interaction, such as \"access_denied\", are never returned.\n      if (err instanceof AuthorizationError && !isPARResponseError(err.error)) {\n        throw new InvalidRequestError(err.error_description, err)\n      }\n      throw err\n    }\n  }\n\n  private async processAuthorizationRequest(\n    client: Client,\n    deviceId: DeviceId,\n    query: OAuthAuthorizationRequestQuery,\n  ) {\n    // PAR\n    if ('request_uri' in query) {\n      const requestUri = parseRequestUri(query.request_uri, {\n        path: ['query', 'request_uri'],\n      })\n      return this.requestManager.get(requestUri, deviceId, client.id)\n    }\n\n    // JAR\n    if ('request' in query) {\n      // @NOTE Since JAR are signed with the client's private key, a JAR *could*\n      // technically be used to authenticate the client when requests are\n      // created without PAR (i.e. created on the fly by the authorize\n      // endpoint). This implementation actually used to support this\n      // (un-spec'd) behavior. That support was removed:\n      // - Because it was not actually used\n      // - Because it was not part of any standard\n      // - Because it makes extending the client authentication mechanism more\n      //   complex since any extension would not only need to affect the\n      //   \"private_key_jwt\" auth method but also the JAR \"request\" object.\n      const parameters = await this.decodeJAR(client, query)\n\n      return this.requestManager.createAuthorizationRequest(\n        client,\n        null,\n        parameters,\n        deviceId,\n      )\n    }\n\n    // \"Regular\" authorization request (created on the fly by directing the user\n    // to the authorization endpoint with all the parameters in the url).\n    return this.requestManager.createAuthorizationRequest(\n      client,\n      null,\n      query,\n      deviceId,\n    )\n  }\n\n  /**\n   * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}\n   */\n  public async authorize(\n    query: OAuthAuthorizationRequestQuery,\n    { deviceId, deviceMetadata }: DeviceInfo,\n  ): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorizePage> {\n    const { issuer } = this\n\n    // If there is a chance to redirect the user to the client, let's do\n    // it by wrapping the error in an AuthorizationError.\n    const throwAuthorizationError =\n      'redirect_uri' in query\n        ? (err: unknown): never => {\n            // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1\n            throw AuthorizationError.from(query, err)\n          }\n        : null\n\n    const client = await this.clientManager\n      .getClient(query.client_id)\n      .catch(throwAuthorizationError)\n\n    const { parameters, requestUri } = await this.processAuthorizationRequest(\n      client,\n      deviceId,\n      query,\n    ).catch(throwAuthorizationError)\n\n    try {\n      const sessions = (\n        await this.accountManager.listDeviceAccounts(deviceId)\n      ).map((deviceAccount) => ({\n        account: deviceAccount.account,\n\n        // @TODO Return the session expiration date instead of a boolean to\n        // avoid having to rely on a leeway when \"accepting\" the request.\n        loginRequired:\n          parameters.prompt === 'login' ||\n          this.checkLoginRequired(deviceAccount),\n        consentRequired: this.checkConsentRequired(\n          parameters,\n          deviceAccount.authorizedClients.get(client.id),\n        ),\n      }))\n\n      // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest\n\n      // prompt=select_account\n      //\n      // > The Authorization Server SHOULD prompt the End-User to select a user\n      // > account. This enables an End-User who has multiple accounts at the\n      // > Authorization Server to select amongst the multiple accounts that\n      // > they might have current sessions for. If it cannot obtain an account\n      // > selection choice made by the End-User, it MUST return an error,\n      // > typically account_selection_required.\n      if (parameters.prompt === 'select_account' && !sessions.length) {\n        throw new AccountSelectionRequiredError(parameters)\n      }\n\n      // prompt=none\n      //\n      // > The Authorization Server MUST NOT display any authentication or\n      // > consent user interface pages. An error is returned if an End-User is\n      // > not already authenticated or the Client does not have pre-configured\n      // > consent for the requested Claims or does not fulfill other conditions\n      // > for processing the request. The error code will typically be\n      // > login_required, interaction_required, or another code defined in\n      // > Section 3.1.2.6. This can be used as a method to check for existing\n      // > authentication and/or consent.\n      if (parameters.prompt === 'none') {\n        const ssoSessions = sessions.filter(matchesHint, parameters)\n        if (ssoSessions.length > 1) {\n          throw new AccountSelectionRequiredError(parameters)\n        }\n        if (ssoSessions.length < 1) {\n          throw new LoginRequiredError(parameters)\n        }\n\n        const ssoSession = ssoSessions[0]!\n        if (ssoSession.loginRequired) {\n          throw new LoginRequiredError(parameters)\n        }\n        if (ssoSession.consentRequired) {\n          throw new ConsentRequiredError(parameters)\n        }\n\n        const code = await this.requestManager.setAuthorized(\n          requestUri,\n          client,\n          ssoSession.account,\n          deviceId,\n          deviceMetadata,\n        )\n\n        return { issuer, parameters, redirect: { code } }\n      }\n\n      // Automatic SSO when a hint was provided that matches a single session\n      if (parameters.prompt == null && parameters.login_hint != null) {\n        const ssoSessions = sessions.filter(matchesHint, parameters)\n        if (ssoSessions.length === 1) {\n          const ssoSession = ssoSessions[0]!\n          if (!ssoSession.loginRequired && !ssoSession.consentRequired) {\n            const code = await this.requestManager.setAuthorized(\n              requestUri,\n              client,\n              ssoSession.account,\n              deviceId,\n              deviceMetadata,\n            )\n\n            return { issuer, parameters, redirect: { code } }\n          }\n        }\n      }\n\n      return {\n        issuer,\n        client,\n        parameters,\n        requestUri,\n        sessions: sessions.map((session) => ({\n          // Map to avoid leaking other data that might be present in the session\n          account: session.account,\n          loginRequired: session.loginRequired,\n          consentRequired: session.consentRequired,\n\n          selected:\n            parameters.prompt == null ||\n            parameters.prompt === 'login' ||\n            parameters.prompt === 'consent'\n              ? matchesHint.call(parameters, session)\n              : false,\n        })),\n        permissionSets: await this.lexiconManager\n          .getPermissionSetsFromScope(parameters.scope)\n          .catch((cause) => {\n            throw new AuthorizationError(\n              parameters,\n              'Unable to retrieve permission sets',\n              'invalid_scope',\n              cause,\n            )\n          }),\n      }\n    } catch (err) {\n      try {\n        await this.requestManager.delete(requestUri)\n      } catch {\n        // There are two error here. Better keep the outer one.\n        //\n        // @TODO Maybe move this entire code to the /authorize endpoint\n        // (allowing to log this error)\n      }\n\n      throw AuthorizationError.from(parameters, err)\n    }\n  }\n\n  public async token(\n    clientCredentials: OAuthClientCredentials,\n    clientMetadata: RequestMetadata,\n    request: OAuthTokenRequest,\n    dpopProof: null | DpopProof,\n  ): Promise<OAuthTokenResponse> {\n    const { client, clientAuth } = await this.authenticateClient(\n      clientCredentials,\n      dpopProof,\n    )\n\n    if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {\n      throw new InvalidGrantError(\n        `Grant type \"${request.grant_type}\" is not supported by the server`,\n      )\n    }\n\n    if (!client.metadata.grant_types.includes(request.grant_type)) {\n      throw new InvalidGrantError(\n        `\"${request.grant_type}\" grant type is not allowed for this client`,\n      )\n    }\n\n    if (request.grant_type === 'authorization_code') {\n      return this.authorizationCodeGrant(\n        client,\n        clientAuth,\n        clientMetadata,\n        request,\n        dpopProof,\n      )\n    }\n\n    if (request.grant_type === 'refresh_token') {\n      return this.refreshTokenGrant(\n        client,\n        clientAuth,\n        clientMetadata,\n        request,\n        dpopProof,\n      )\n    }\n\n    throw new InvalidGrantError(\n      `Grant type \"${request.grant_type}\" not supported`,\n    )\n  }\n\n  protected async compareClientAuth(\n    client: Client,\n    clientAuth: ClientAuth,\n    dpopProof: null | DpopProof,\n    initial: {\n      parameters: OAuthAuthorizationRequestParameters\n      clientId: ClientId\n      clientAuth: null | ClientAuth | ClientAuthLegacy\n    },\n  ): Promise<void> {\n    // Fool proofing, ensure that the client is authenticating using the right method\n    if (clientAuth.method !== client.metadata.token_endpoint_auth_method) {\n      throw new InvalidGrantError(\n        `Client authentication method mismatch (expected ${client.metadata.token_endpoint_auth_method}, got ${clientAuth.method})`,\n      )\n    }\n\n    if (initial.clientId !== client.id) {\n      throw new InvalidGrantError(`Token was not issued to this client`)\n    }\n\n    const { parameters } = initial\n    if (parameters.dpop_jkt) {\n      if (!dpopProof) {\n        throw new InvalidGrantError(`DPoP proof is required for this request`)\n      } else if (parameters.dpop_jkt !== dpopProof.jkt) {\n        throw new InvalidGrantError(\n          `DPoP proof does not match the expected JKT`,\n        )\n      }\n    }\n\n    if (!initial.clientAuth) {\n      // If the client did not use PAR, it was not authenticated when the request\n      // was initially created (see authorize() method in OAuthProvider). Since\n      // PAR is not mandatory, and since the token exchange currently taking place\n      // *is* authenticated (`clientAuth`), we allow \"upgrading\" the\n      // authentication method (the token created will be bound to the current\n      // clientAuth).\n      return\n    }\n\n    switch (initial.clientAuth.method) {\n      case CLIENT_ASSERTION_TYPE_JWT_BEARER: // LEGACY\n      case 'private_key_jwt':\n        if (clientAuth.method !== 'private_key_jwt') {\n          throw new InvalidGrantError(\n            `Client authentication method mismatch (expected ${initial.clientAuth.method})`,\n          )\n        }\n        if (\n          clientAuth.kid !== initial.clientAuth.kid ||\n          clientAuth.alg !== initial.clientAuth.alg ||\n          clientAuth.jkt !== initial.clientAuth.jkt\n        ) {\n          throw new InvalidGrantError(\n            `The session was initiated with a different key than the client assertion currently used`,\n          )\n        }\n        break\n      case 'none':\n        // @NOTE We allow the client to \"upgrade\" to a confidential client if\n        // the session was initially created without client authentication.\n        break\n      default:\n        throw new InvalidGrantError(\n          // @ts-expect-error (future proof, backwards compatibility)\n          `Invalid method \"${initial.clientAuth.method}\"`,\n        )\n    }\n  }\n\n  protected async authorizationCodeGrant(\n    client: Client,\n    clientAuth: ClientAuth,\n    clientMetadata: RequestMetadata,\n    input: OAuthAuthorizationCodeGrantTokenRequest,\n    dpopProof: null | DpopProof,\n  ): Promise<OAuthTokenResponse> {\n    const code = await codeSchema\n      .parseAsync(input.code, { path: ['code'] })\n      .catch((err) => {\n        const msg = formatError(err, 'Invalid code')\n        throw new InvalidGrantError(msg, err)\n      })\n\n    const data = await this.requestManager\n      .consumeCode(code)\n      .catch(async (err) => {\n        // Code not found in request manager: check for replays\n        const tokenInfo = await this.tokenManager.findByCode(code)\n        if (tokenInfo) {\n          // try/finally to ensure that both code path get executed (sequentially)\n          try {\n            // \"code\" was replayed, delete existing session\n            await this.tokenManager.deleteToken(tokenInfo.id)\n          } finally {\n            // As an additional security measure, we also sign the device out,\n            // so that the device cannot be used to access the account anymore\n            // without a new authentication.\n            const { deviceId, sub } = tokenInfo.data\n            if (deviceId) {\n              await this.accountManager.removeDeviceAccount(deviceId, sub)\n            }\n          }\n        }\n\n        throw InvalidGrantError.from(err, `Invalid code`)\n      })\n\n    // @NOTE at this point, the request data was removed from the store and only\n    // exists in memory here (in the \"data\" variable). Because of this, any\n    // error thrown after this point will permanently cause the request data to\n    // be lost.\n\n    await this.compareClientAuth(client, clientAuth, dpopProof, data)\n\n    // If the DPoP proof was not provided earlier (PAR / authorize), let's add\n    // it now.\n    const parameters =\n      dpopProof &&\n      client.metadata.dpop_bound_access_tokens &&\n      !data.parameters.dpop_jkt\n        ? { ...data.parameters, dpop_jkt: dpopProof.jkt }\n        : data.parameters\n\n    await this.validateCodeGrant(parameters, input)\n\n    const { account } = await this.accountManager.getAccount(data.sub)\n\n    return this.tokenManager.createToken(\n      client,\n      clientAuth,\n      clientMetadata,\n      account,\n      data.deviceId,\n      parameters,\n      code,\n    )\n  }\n\n  protected async validateCodeGrant(\n    parameters: OAuthAuthorizationRequestParameters,\n    input: OAuthAuthorizationCodeGrantTokenRequest,\n  ): Promise<void> {\n    if (parameters.redirect_uri !== input.redirect_uri) {\n      throw new InvalidGrantError(\n        'The redirect_uri parameter must match the one used in the authorization request',\n      )\n    }\n\n    if (parameters.code_challenge) {\n      if (!input.code_verifier) {\n        throw new InvalidGrantError('code_verifier is required')\n      }\n      if (input.code_verifier.length < 43) {\n        throw new InvalidGrantError('code_verifier too short')\n      }\n      switch (parameters.code_challenge_method) {\n        case undefined: // default is \"plain\"\n        case 'plain':\n          if (parameters.code_challenge !== input.code_verifier) {\n            throw new InvalidGrantError('Invalid code_verifier')\n          }\n          break\n\n        case 'S256': {\n          const inputChallenge = Buffer.from(\n            parameters.code_challenge,\n            'base64',\n          )\n          const computedChallenge = createHash('sha256')\n            .update(input.code_verifier)\n            .digest()\n          if (inputChallenge.compare(computedChallenge) !== 0) {\n            throw new InvalidGrantError('Invalid code_verifier')\n          }\n          break\n        }\n\n        default:\n          // Should never happen (because request validation should catch this)\n          throw new Error(`Unsupported code_challenge_method`)\n      }\n      const unique = await this.replayManager.uniqueCodeChallenge(\n        parameters.code_challenge,\n      )\n      if (!unique) {\n        throw new InvalidGrantError('Code challenge already used')\n      }\n    } else if (input.code_verifier !== undefined) {\n      throw new InvalidRequestError(\"code_challenge parameter wasn't provided\")\n    }\n  }\n\n  protected async refreshTokenGrant(\n    client: Client,\n    clientAuth: ClientAuth,\n    clientMetadata: RequestMetadata,\n    input: OAuthRefreshTokenGrantTokenRequest,\n    dpopProof: null | DpopProof,\n  ): Promise<OAuthTokenResponse> {\n    const refreshToken = await refreshTokenSchema\n      .parseAsync(input.refresh_token, { path: ['refresh_token'] })\n      .catch((err) => {\n        const msg = formatError(err, 'Invalid refresh token')\n        throw new InvalidGrantError(msg, err)\n      })\n\n    const tokenInfo = await this.tokenManager.consumeRefreshToken(refreshToken)\n\n    try {\n      const { data } = tokenInfo\n      await this.compareClientAuth(client, clientAuth, dpopProof, data)\n      await this.validateRefreshGrant(client, clientAuth, data)\n\n      return await this.tokenManager.rotateToken(\n        client,\n        clientAuth,\n        clientMetadata,\n        tokenInfo,\n      )\n    } catch (err) {\n      await this.tokenManager.deleteToken(tokenInfo.id)\n\n      throw err\n    }\n  }\n\n  protected async validateRefreshGrant(\n    client: Client,\n    clientAuth: ClientAuth,\n    data: TokenData,\n  ): Promise<void> {\n    const [sessionLifetime, refreshLifetime] =\n      clientAuth.method !== 'none' || client.info.isFirstParty\n        ? [\n            CONFIDENTIAL_CLIENT_SESSION_LIFETIME,\n            CONFIDENTIAL_CLIENT_REFRESH_LIFETIME,\n          ]\n        : [PUBLIC_CLIENT_SESSION_LIFETIME, PUBLIC_CLIENT_REFRESH_LIFETIME]\n\n    const sessionAge = Date.now() - data.createdAt.getTime()\n    if (sessionAge > sessionLifetime) {\n      throw new InvalidGrantError(`Session expired`)\n    }\n\n    const refreshAge = Date.now() - data.updatedAt.getTime()\n    if (refreshAge > refreshLifetime) {\n      throw new InvalidGrantError(`Refresh token expired`)\n    }\n  }\n\n  /**\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 rfc7009}\n   */\n  public async revoke(\n    clientCredentials: OAuthClientCredentials,\n    { token }: OAuthTokenIdentification,\n    dpopProof: null | DpopProof,\n  ) {\n    // > The authorization server first validates the client credentials (in\n    // > case of a confidential client)\n    const { client, clientAuth } = await this.authenticateClient(\n      clientCredentials,\n      dpopProof,\n    )\n\n    const tokenInfo = await this.tokenManager.findToken(token)\n    if (tokenInfo) {\n      // > [...] and then verifies whether the token was issued to the client\n      // > making the revocation request.\n      const { data } = tokenInfo\n      await this.compareClientAuth(client, clientAuth, dpopProof, data)\n\n      // > In the next step, the authorization server invalidates the token. The\n      // > invalidation takes place immediately, and the token cannot be used\n      // > again after the revocation.\n      await this.tokenManager.deleteToken(tokenInfo.id)\n    }\n  }\n\n  protected override async decodeToken(\n    tokenType: OAuthTokenType,\n    token: OAuthAccessToken,\n    dpopProof: null | DpopProof,\n  ): Promise<AccessTokenPayload> {\n    const tokenPayload = await super.decodeToken(tokenType, token, dpopProof)\n\n    if (this.accessTokenMode !== AccessTokenMode.stateless) {\n      // @NOTE in non stateless mode, some claims can be omitted (most notably\n      // \"scope\"). We load the token claims here (allowing to ensure that the\n      // token is still valid, and to retrieve a (potentially updated) set of\n      // claims).\n\n      const tokenClaims = await this.tokenManager.loadTokenClaims(\n        tokenType,\n        tokenPayload,\n      )\n\n      Object.assign(tokenPayload, tokenClaims)\n    }\n\n    return tokenPayload\n  }\n}\n\nfunction matchesHint(\n  this: OAuthAuthorizationRequestParameters,\n  { account }: { account: Account },\n): boolean {\n  const hint = this.login_hint\n  if (!hint) return false\n\n  return account.sub === hint || account.preferred_username === hint\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-store.ts",
    "content": "/**\n * Every store file exports all the types needed to implement that store. This\n * files re-exports all the types from the x-store files.\n */\n\nexport * from './account/account-store.js'\nexport * from './client/client-store.js'\nexport * from './device/device-store.js'\nexport * from './lexicon/lexicon-store.js'\nexport * from './replay/replay-store.js'\nexport * from './request/request-store.js'\nexport * from './token/token-store.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oauth-verifier.ts",
    "content": "import type { Redis, RedisOptions } from 'ioredis'\nimport { Key, Keyset, isSignedJwt } from '@atproto/jwk'\nimport {\n  OAuthAccessToken,\n  OAuthIssuerIdentifier,\n  OAuthTokenType,\n  oauthIssuerIdentifierSchema,\n} from '@atproto/oauth-types'\nimport { DpopManager, DpopManagerOptions } from './dpop/dpop-manager.js'\nimport { DpopNonce } from './dpop/dpop-nonce.js'\nimport { DpopProof } from './dpop/dpop-proof.js'\nimport { InvalidDpopKeyBindingError } from './errors/invalid-dpop-key-binding-error.js'\nimport { InvalidDpopProofError } from './errors/invalid-dpop-proof-error.js'\nimport { InvalidTokenError } from './errors/invalid-token-error.js'\nimport { UseDpopNonceError } from './errors/use-dpop-nonce-error.js'\nimport { WWWAuthenticateError } from './errors/www-authenticate-error.js'\nimport { parseAuthorizationHeader } from './lib/util/authorization-header.js'\nimport { includedIn } from './lib/util/function.js'\nimport { OAuthHooks } from './oauth-hooks.js'\nimport { ReplayManager } from './replay/replay-manager.js'\nimport { ReplayStoreMemory } from './replay/replay-store-memory.js'\nimport { ReplayStoreRedis } from './replay/replay-store-redis.js'\nimport { ReplayStore } from './replay/replay-store.js'\nimport { AccessTokenPayload } from './signer/access-token-payload.js'\nimport { Signer } from './signer/signer.js'\n\nexport type DecodeTokenHook = OAuthHooks['onDecodeToken']\n\nexport type OAuthVerifierOptions = DpopManagerOptions & {\n  /**\n   * The \"issuer\" identifier of the OAuth provider, this is the base URL of the\n   * OAuth provider.\n   */\n  issuer: URL | string\n\n  /**\n   * The keyset used to sign access tokens.\n   */\n  keyset: Keyset | Iterable<Key | undefined | null | false>\n\n  /**\n   * A redis instance to use for replay protection. If not provided, replay\n   * protection will use memory storage.\n   */\n  redis?: Redis | RedisOptions | string\n\n  replayStore?: ReplayStore\n\n  onDecodeToken?: DecodeTokenHook\n}\n\nexport type VerifyTokenPayloadOptions = {\n  /** One of these audience must be included in the token audience(s) */\n  audience?: [string, ...string[]]\n  /** One of these scope must be included in the token scope(s) */\n  scope?: [string, ...string[]]\n}\n\nexport { DpopNonce, Key, Keyset }\nexport type {\n  AccessTokenPayload,\n  DpopProof,\n  OAuthTokenType,\n  RedisOptions,\n  ReplayStore,\n}\n\nexport class OAuthVerifier {\n  private readonly onDecodeToken?: DecodeTokenHook\n\n  public readonly issuer: OAuthIssuerIdentifier\n  public readonly keyset: Keyset\n\n  public readonly dpopManager: DpopManager\n  public readonly replayManager: ReplayManager\n  public readonly signer: Signer\n\n  constructor({\n    redis,\n    issuer,\n    keyset,\n    replayStore = redis != null\n      ? new ReplayStoreRedis({ redis })\n      : new ReplayStoreMemory(),\n    onDecodeToken,\n\n    ...rest\n  }: OAuthVerifierOptions) {\n    const dpopMgrOptions: DpopManagerOptions = rest\n\n    const issuerParsed = oauthIssuerIdentifierSchema.parse(issuer)\n    const issuerUrl = new URL(issuerParsed)\n\n    // @TODO (?) support issuer with path\n    if (issuerUrl.pathname !== '/') {\n      throw new TypeError(\n        `\"issuer\" must be an URL with no path, search or hash (${issuerUrl})`,\n      )\n    }\n\n    this.issuer = issuerParsed\n    this.keyset = keyset instanceof Keyset ? keyset : new Keyset(keyset)\n\n    this.dpopManager = new DpopManager(dpopMgrOptions)\n    this.replayManager = new ReplayManager(replayStore)\n    this.signer = new Signer(this.issuer, this.keyset)\n\n    this.onDecodeToken = onDecodeToken\n  }\n\n  public nextDpopNonce() {\n    return this.dpopManager.nextNonce()\n  }\n\n  public async checkDpopProof(\n    httpMethod: string,\n    httpUrl: Readonly<URL>,\n    httpHeaders: Record<string, undefined | string | string[]>,\n    accessToken?: string,\n  ): Promise<null | DpopProof> {\n    const dpopProof = await this.dpopManager.checkProof(\n      httpMethod,\n      httpUrl,\n      httpHeaders,\n      accessToken,\n    )\n\n    if (dpopProof) {\n      const unique = await this.replayManager.uniqueDpop(dpopProof.jti)\n      if (!unique) throw new InvalidDpopProofError('DPoP proof replayed')\n    }\n\n    return dpopProof\n  }\n\n  protected async decodeToken(\n    tokenType: OAuthTokenType,\n    token: OAuthAccessToken,\n    dpopProof: null | DpopProof,\n  ): Promise<AccessTokenPayload> {\n    if (!isSignedJwt(token)) {\n      throw new InvalidTokenError(tokenType, `Malformed token`)\n    }\n\n    const { payload } = await this.signer\n      .verifyAccessToken(token)\n      .catch((err) => {\n        throw InvalidTokenError.from(err, tokenType)\n      })\n\n    if (payload.cnf?.jkt) {\n      // An access token with a cnf.jkt claim must be a DPoP token\n      if (tokenType !== 'DPoP') {\n        throw new InvalidTokenError(\n          'DPoP',\n          `Access token is bound to a DPoP proof, but token type is ${tokenType}`,\n        )\n      }\n\n      // DPoP token type must be used with a DPoP proof\n      if (!dpopProof) {\n        throw new InvalidDpopProofError(`DPoP proof required`)\n      }\n\n      // DPoP proof must be signed with the key that matches the \"cnf\" claim\n      if (payload.cnf.jkt !== dpopProof.jkt) {\n        throw new InvalidDpopKeyBindingError()\n      }\n    } else {\n      // An access token without a cnf.jkt claim must be a Bearer token\n      if (tokenType !== 'Bearer') {\n        throw new InvalidTokenError(\n          'Bearer',\n          `Bearer token type must be used without a DPoP proof`,\n        )\n      }\n\n      // @NOTE We ignore (but allow) DPoP proofs for Bearer tokens\n    }\n\n    const payloadOverride = await this.onDecodeToken?.call(null, {\n      tokenType,\n      token,\n      payload,\n      dpopProof,\n    })\n\n    return payloadOverride ?? payload\n  }\n\n  /**\n   * @throws {WWWAuthenticateError}\n   * @throws {InvalidTokenError}\n   */\n  public async authenticateRequest(\n    httpMethod: string,\n    httpUrl: Readonly<URL>,\n    httpHeaders: Record<string, undefined | string | string[]>,\n    verifyOptions?: VerifyTokenPayloadOptions,\n  ): Promise<AccessTokenPayload> {\n    const [tokenType, token] = parseAuthorizationHeader(\n      httpHeaders['authorization'],\n    )\n    try {\n      const dpopProof = await this.checkDpopProof(\n        httpMethod,\n        httpUrl,\n        httpHeaders,\n        token,\n      )\n\n      const tokenPayload = await this.decodeToken(tokenType, token, dpopProof)\n\n      this.verifyTokenPayload(tokenType, tokenPayload, verifyOptions)\n\n      return tokenPayload\n    } catch (err) {\n      if (err instanceof UseDpopNonceError) throw err.toWwwAuthenticateError()\n      if (err instanceof WWWAuthenticateError) throw err\n\n      throw InvalidTokenError.from(err, tokenType)\n    }\n  }\n\n  protected verifyTokenPayload(\n    tokenType: OAuthTokenType,\n    tokenPayload: AccessTokenPayload,\n    options?: VerifyTokenPayloadOptions,\n  ): void {\n    if (options?.audience) {\n      const { aud } = tokenPayload\n      const hasMatch =\n        aud != null &&\n        (Array.isArray(aud)\n          ? options.audience.some(includedIn, aud)\n          : options.audience.includes(aud))\n      if (!hasMatch) {\n        const details = `(got: ${aud}, expected one of: ${options.audience})`\n        throw new InvalidTokenError(tokenType, `Invalid audience ${details}`)\n      }\n    }\n\n    if (options?.scope) {\n      const { scope } = tokenPayload\n      const scopes = scope?.split(' ')\n      if (!scopes || !options.scope.some(includedIn, scopes)) {\n        const details = `(got: ${scope}, expected one of: ${options.scope})`\n        throw new InvalidTokenError(tokenType, `Invalid scope ${details}`)\n      }\n    }\n\n    if (tokenPayload.exp != null && tokenPayload.exp * 1000 <= Date.now()) {\n      const expirationDate = new Date(tokenPayload.exp * 1000).toISOString()\n      throw new InvalidTokenError(\n        tokenType,\n        `Token expired at ${expirationDate}`,\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/oidc/sub.ts",
    "content": "import { z } from 'zod'\n\nexport const subSchema = z.string().min(1)\nexport type Sub = z.infer<typeof subSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/replay/replay-manager.ts",
    "content": "import { ClientId } from '../client/client-id.js'\nimport {\n  CLIENT_ASSERTION_MAX_AGE,\n  CODE_CHALLENGE_REPLAY_TIMEFRAME,\n  DPOP_NONCE_MAX_AGE,\n  JAR_MAX_AGE,\n} from '../constants.js'\nimport { ReplayStore } from './replay-store.js'\n\nconst SECURITY_RATIO = 1.1 // 10% extra time for security\nconst asTimeFrame = (timeFrame: number) => Math.ceil(timeFrame * SECURITY_RATIO)\n\nexport class ReplayManager {\n  constructor(protected readonly replayStore: ReplayStore) {}\n\n  async uniqueAuth(\n    jti: string,\n    clientId: ClientId,\n    exp?: number,\n  ): Promise<boolean> {\n    const timeFrame =\n      exp == null\n        ? asTimeFrame(CLIENT_ASSERTION_MAX_AGE)\n        : exp * 1000 - Date.now()\n    return this.replayStore.unique(`Auth@${clientId}`, jti, timeFrame)\n  }\n\n  async uniqueJar(jti: string, clientId: ClientId): Promise<boolean> {\n    return this.replayStore.unique(\n      `JAR@${clientId}`,\n      jti,\n      asTimeFrame(JAR_MAX_AGE),\n    )\n  }\n\n  async uniqueDpop(jti: string, clientId?: ClientId): Promise<boolean> {\n    return this.replayStore.unique(\n      clientId ? `DPoP@${clientId}` : `DPoP`,\n      jti,\n      asTimeFrame(DPOP_NONCE_MAX_AGE),\n    )\n  }\n\n  async uniqueCodeChallenge(challenge: string): Promise<boolean> {\n    return this.replayStore.unique(\n      'CodeChallenge',\n      challenge,\n      asTimeFrame(CODE_CHALLENGE_REPLAY_TIMEFRAME),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/replay/replay-store-memory.ts",
    "content": "import type { ReplayStore } from './replay-store.js'\n\nexport class ReplayStoreMemory implements ReplayStore {\n  private lastCleanup = Date.now()\n  private nonces = new Map<string, number>()\n\n  /**\n   * Returns true if the nonce is unique within the given time frame.\n   */\n  async unique(\n    namespace: string,\n    nonce: string,\n    timeFrame: number,\n  ): Promise<boolean> {\n    this.cleanup()\n    const key = `${namespace}:${nonce}`\n\n    const now = Date.now()\n\n    const exp = this.nonces.get(key)\n    this.nonces.set(key, now + timeFrame)\n\n    return exp == null || exp < now\n  }\n\n  private cleanup() {\n    const now = Date.now()\n\n    if (this.lastCleanup < now - 60_000) {\n      for (const [key, expires] of this.nonces) {\n        if (expires < now) this.nonces.delete(key)\n      }\n      this.lastCleanup = now\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/replay/replay-store-redis.ts",
    "content": "import type { Redis } from 'ioredis'\nimport { CreateRedisOptions, createRedis } from '../lib/redis.js'\nimport type { ReplayStore } from './replay-store.js'\n\nexport type { CreateRedisOptions, Redis }\n\nexport type ReplayStoreRedisOptions = {\n  redis: CreateRedisOptions\n}\n\nexport class ReplayStoreRedis implements ReplayStore {\n  private readonly redis: Redis\n\n  constructor(options: ReplayStoreRedisOptions) {\n    this.redis = createRedis(options.redis)\n  }\n\n  /**\n   * Returns true if the nonce is unique within the given time frame.\n   */\n  async unique(\n    namespace: string,\n    nonce: string,\n    timeFrame: number,\n  ): Promise<boolean> {\n    const key = `nonces:${namespace}:${nonce}`\n    const prev = await this.redis.set(key, '1', 'PX', timeFrame, 'GET')\n    return prev == null\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/replay/replay-store.ts",
    "content": "import { Awaitable } from '../lib/util/type.js'\n\n// Export all types needed to implement the ReplayStore interface\nexport type { Awaitable }\n\nexport interface ReplayStore {\n  /**\n   * Returns true if the nonce is unique within the given time frame. While not\n   * strictly necessary for security purposes, the namespace should be used to\n   * mitigate denial of service attacks from one client to the other.\n   *\n   * @param timeFrame expressed in milliseconds.\n   */\n  unique(\n    namespace: string,\n    nonce: string,\n    timeFrame: number,\n  ): Awaitable<boolean>\n}\n\nexport function isReplayStore(\n  implementation: Record<string, unknown> & Partial<ReplayStore>,\n): implementation is Record<string, unknown> & ReplayStore {\n  return typeof implementation.unique === 'function'\n}\n\nexport function ifReplayStore(\n  implementation?: Record<string, unknown> & Partial<ReplayStore>,\n): ReplayStore | undefined {\n  if (implementation && isReplayStore(implementation)) {\n    return implementation\n  }\n\n  return undefined\n}\n\nexport function asReplayStore(\n  implementation?: Record<string, unknown> & Partial<ReplayStore>,\n): ReplayStore {\n  const store = ifReplayStore(implementation)\n  if (store) return store\n\n  throw new Error('Invalid ReplayStore implementation')\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/request/code.ts",
    "content": "import { z } from 'zod'\nimport { CODE_BYTES_LENGTH, CODE_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const CODE_LENGTH = CODE_PREFIX.length + CODE_BYTES_LENGTH * 2 // hex encoding\n\nexport const codeSchema = z\n  .string()\n  .length(CODE_LENGTH) // hex encoding\n  .refine(\n    (v): v is `${typeof CODE_PREFIX}${string}` => v.startsWith(CODE_PREFIX),\n    {\n      message: `Invalid code format`,\n    },\n  )\n\nexport const isCode = (data: unknown): data is Code =>\n  codeSchema.safeParse(data).success\n\nexport type Code = z.infer<typeof codeSchema>\nexport const generateCode = async (): Promise<Code> => {\n  return `${CODE_PREFIX}${await randomHexId(CODE_BYTES_LENGTH)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/request/request-data.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'\nimport { ClientId } from '../client/client-id.js'\nimport { DeviceId } from '../device/device-id.js'\nimport { NonNullableKeys } from '../lib/util/type.js'\nimport { Sub } from '../oidc/sub.js'\nimport { Code } from './code.js'\n\nexport type {\n  ClientAuth,\n  ClientAuthLegacy,\n  ClientId,\n  Code,\n  DeviceId,\n  OAuthAuthorizationRequestParameters,\n  Sub,\n}\n\nexport type RequestData = {\n  clientId: ClientId\n  clientAuth: null | ClientAuth | ClientAuthLegacy\n  parameters: Readonly<OAuthAuthorizationRequestParameters>\n  expiresAt: Date\n  deviceId: DeviceId | null\n  sub: Sub | null\n  code: Code | null\n}\n\nexport type RequestDataAuthorized = NonNullableKeys<\n  RequestData,\n  'sub' | 'deviceId'\n>\n\nexport const isRequestDataAuthorized = (\n  data: RequestData,\n): data is RequestDataAuthorized => data.sub !== null && data.deviceId !== null\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/request/request-id.ts",
    "content": "import { z } from 'zod'\nimport { REQUEST_ID_BYTES_LENGTH, REQUEST_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const REQUEST_ID_LENGTH =\n  REQUEST_ID_PREFIX.length + REQUEST_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const requestIdSchema = z\n  .string()\n  .length(REQUEST_ID_LENGTH)\n  .refine(\n    (v): v is `${typeof REQUEST_ID_PREFIX}${string}` =>\n      v.startsWith(REQUEST_ID_PREFIX),\n    {\n      message: `Invalid request ID format`,\n    },\n  )\n\nexport type RequestId = z.infer<typeof requestIdSchema>\nexport const generateRequestId = async (): Promise<RequestId> => {\n  return `${REQUEST_ID_PREFIX}${await randomHexId(REQUEST_ID_BYTES_LENGTH)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/request/request-manager.ts",
    "content": "import { isAtprotoDid } from '@atproto/did'\nimport { LexResolverError } from '@atproto/lex-resolver'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport { isAtprotoOauthScope } from '@atproto/oauth-scopes'\nimport {\n  OAuthAuthorizationRequestParameters,\n  OAuthAuthorizationServerMetadata,\n} from '@atproto/oauth-types'\nimport { isValidHandle } from '@atproto/syntax'\nimport { ClientAuth } from '../client/client-auth.js'\nimport { ClientId } from '../client/client-id.js'\nimport { Client } from '../client/client.js'\nimport {\n  AUTHORIZATION_INACTIVITY_TIMEOUT,\n  NODE_ENV,\n  PAR_EXPIRES_IN,\n  TOKEN_MAX_AGE,\n} from '../constants.js'\nimport { DeviceId } from '../device/device-id.js'\nimport { AccessDeniedError } from '../errors/access-denied-error.js'\nimport { AuthorizationError } from '../errors/authorization-error.js'\nimport { ConsentRequiredError } from '../errors/consent-required-error.js'\nimport { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'\nimport { InvalidGrantError } from '../errors/invalid-grant-error.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { InvalidScopeError } from '../errors/invalid-scope-error.js'\nimport { LexiconManager } from '../lexicon/lexicon-manager.js'\nimport { RequestMetadata } from '../lib/http/request.js'\nimport { OAuthHooks } from '../oauth-hooks.js'\nimport { Signer } from '../signer/signer.js'\nimport { Code, generateCode } from './code.js'\nimport {\n  RequestDataAuthorized,\n  isRequestDataAuthorized,\n} from './request-data.js'\nimport { generateRequestId } from './request-id.js'\nimport { RequestStore, UpdateRequestData } from './request-store.js'\nimport {\n  RequestUri,\n  decodeRequestUri,\n  encodeRequestUri,\n} from './request-uri.js'\n\nexport class RequestManager {\n  constructor(\n    protected readonly store: RequestStore,\n    protected readonly lexiconManager: LexiconManager,\n    protected readonly signer: Signer,\n    protected readonly metadata: OAuthAuthorizationServerMetadata,\n    protected readonly hooks: OAuthHooks,\n    protected readonly tokenMaxAge = TOKEN_MAX_AGE,\n  ) {}\n\n  protected createTokenExpiry() {\n    return new Date(Date.now() + this.tokenMaxAge)\n  }\n\n  async createAuthorizationRequest(\n    client: Client,\n    clientAuth: null | ClientAuth,\n    input: Readonly<OAuthAuthorizationRequestParameters>,\n    deviceId: null | DeviceId,\n  ) {\n    const parameters = await this.validate(client, clientAuth, input)\n\n    await this.hooks.onAuthorizationRequest?.call(null, {\n      client,\n      clientAuth,\n      parameters,\n    })\n\n    const expiresAt = new Date(Date.now() + PAR_EXPIRES_IN)\n    const requestId = await generateRequestId()\n\n    await this.store.createRequest(requestId, {\n      clientId: client.id,\n      clientAuth,\n      parameters,\n      expiresAt,\n      deviceId,\n      sub: null,\n      code: null,\n    })\n\n    const requestUri = encodeRequestUri(requestId)\n    return { requestUri, expiresAt, parameters }\n  }\n\n  protected async validate(\n    client: Client,\n    clientAuth: null | ClientAuth,\n    parameters: Readonly<OAuthAuthorizationRequestParameters>,\n  ): Promise<Readonly<OAuthAuthorizationRequestParameters>> {\n    // -------------------------------\n    // Validate unsupported parameters\n    // -------------------------------\n\n    for (const k of [\n      // Known unsupported OIDC parameters\n      'claims',\n      'id_token_hint',\n      'nonce', // note that OIDC \"nonce\" is redundant with PKCE\n    ] as const) {\n      if (parameters[k] !== undefined) {\n        throw new AuthorizationError(parameters, `Unsupported \"${k}\" parameter`)\n      }\n    }\n\n    // -----------------------\n    // Validate against server\n    // -----------------------\n\n    if (\n      !this.metadata.response_types_supported?.includes(\n        parameters.response_type,\n      )\n    ) {\n      throw new AuthorizationError(\n        parameters,\n        `Unsupported response_type \"${parameters.response_type}\"`,\n        'unsupported_response_type',\n      )\n    }\n\n    if (\n      parameters.response_type === 'code' &&\n      !this.metadata.grant_types_supported?.includes('authorization_code')\n    ) {\n      throw new AuthorizationError(\n        parameters,\n        `Unsupported grant_type \"authorization_code\"`,\n        'invalid_request',\n      )\n    }\n\n    if (parameters.authorization_details) {\n      for (const detail of parameters.authorization_details) {\n        if (\n          !this.metadata.authorization_details_types_supported?.includes(\n            detail.type,\n          )\n        ) {\n          throw new InvalidAuthorizationDetailsError(\n            parameters,\n            `Unsupported \"authorization_details\" type \"${detail.type}\"`,\n          )\n        }\n      }\n    }\n\n    // -----------------------\n    // Validate against client\n    // -----------------------\n\n    parameters = client.validateRequest(parameters)\n\n    // -------------------\n    // Validate parameters\n    // -------------------\n\n    if (!parameters.redirect_uri) {\n      // Should already be ensured by client.validateRequest(). Adding here for\n      // clarity & extra safety.\n      throw new AuthorizationError(parameters, 'Missing \"redirect_uri\"')\n    }\n\n    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1\n    // > The authorization server MAY fully or partially ignore the scope\n    // > requested by the client, based on the authorization server policy or\n    // > the resource owner's instructions. If the issued access token scope is\n    // > different from the one requested by the client, the authorization\n    // > server MUST include the scope response parameter in the token response\n    // > (Section 3.2.3) to inform the client of the actual scope granted.\n\n    // Let's make sure the scopes are unique (to reduce the token & storage\n    // size).\n    const scopes = new Set(parameters.scope?.split(' '))\n\n    // @NOTE An app requesting a not yet supported list of scopes will need to\n    // re-authenticate the user once the scopes are supported. This is due to\n    // the fact that the AS does not know how to properly display those scopes\n    // to the user, so it cannot properly ask for consent.\n    const scope =\n      Array.from(scopes).filter(isAtprotoOauthScope).join(' ') || undefined\n    parameters = { ...parameters, scope }\n\n    if (parameters.code_challenge) {\n      switch (parameters.code_challenge_method) {\n        case undefined:\n          // https://datatracker.ietf.org/doc/html/rfc7636#section-4.3\n          parameters = { ...parameters, code_challenge_method: 'plain' }\n        // falls through\n        case 'plain':\n        case 'S256':\n          break\n        default: {\n          throw new AuthorizationError(\n            parameters,\n            `Unsupported code_challenge_method \"${parameters.code_challenge_method}\"`,\n          )\n        }\n      }\n    } else {\n      if (parameters.code_challenge_method) {\n        // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1\n        throw new AuthorizationError(\n          parameters,\n          'code_challenge is required when code_challenge_method is provided',\n        )\n      }\n\n      // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1\n      //\n      // > An AS MUST reject requests without a code_challenge from public\n      // > clients, and MUST reject such requests from other clients unless\n      // > there is reasonable assurance that the client mitigates\n      // > authorization code injection in other ways. See Section 7.5.1 for\n      // > details.\n      //\n      // > [...] In the specific deployment and the specific request, there is\n      // > reasonable assurance by the authorization server that the client\n      // > implements the OpenID Connect nonce mechanism properly.\n      //\n      // atproto does not implement the OpenID Connect nonce mechanism, so we\n      // require the use of PKCE for all clients.\n\n      throw new AuthorizationError(parameters, 'Use of PKCE is required')\n    }\n\n    // -----------------\n    // atproto extension\n    // -----------------\n\n    if (parameters.response_type !== 'code') {\n      throw new AuthorizationError(\n        parameters,\n        'atproto only supports the \"code\" response_type',\n      )\n    }\n\n    if (!scopes.has('atproto')) {\n      throw new InvalidScopeError(parameters, 'The \"atproto\" scope is required')\n    } else if (scopes.has('openid')) {\n      throw new InvalidScopeError(\n        parameters,\n        'OpenID Connect is not compatible with atproto',\n      )\n    }\n\n    if (parameters.code_challenge_method !== 'S256') {\n      throw new AuthorizationError(\n        parameters,\n        'atproto requires use of \"S256\" code_challenge_method',\n      )\n    }\n\n    // atproto extension: if the client is not trusted, and not authenticated,\n    // force users to consent to authorization requests. We do this to avoid\n    // unauthenticated clients from being able to silently re-authenticate\n    // users.\n    if (\n      !client.info.isTrusted &&\n      !client.info.isFirstParty &&\n      client.metadata.token_endpoint_auth_method === 'none'\n    ) {\n      if (parameters.prompt === 'none') {\n        throw new ConsentRequiredError(\n          parameters,\n          'Public clients are not allowed to use silent-sign-on',\n        )\n      }\n\n      // force \"consent\" for unauthenticated third party clients, unless they\n      // are trying to create accounts:\n      if (parameters.prompt !== 'create') {\n        parameters = { ...parameters, prompt: 'consent' }\n      }\n    }\n\n    // atproto extension: ensure that the login_hint is a valid handle or DID\n    // @NOTE we to allow invalid case here, which is not spec'd anywhere.\n    const hint = parameters.login_hint?.toLowerCase()\n    if (hint) {\n      if (!isAtprotoDid(hint) && !isValidHandle(hint)) {\n        throw new AuthorizationError(parameters, `Invalid login_hint \"${hint}\"`)\n      }\n\n      // @TODO: ensure that the account actually exists on this server (there is\n      // no point in showing the UI to the user if the account does not exist).\n\n      // Update the parameters to ensure the right case is used\n      parameters = { ...parameters, login_hint: hint }\n    }\n\n    // Make sure that every nsid in the scope resolves to a valid permission set\n    // lexicon\n    if (parameters.scope) {\n      try {\n        await this.lexiconManager.getPermissionSetsFromScope(parameters.scope)\n      } catch (err) {\n        // Parse expected errors\n        if (err instanceof LexResolverError) {\n          throw new AuthorizationError(\n            parameters,\n            err.message,\n            'invalid_scope',\n            err,\n          )\n        }\n\n        // Unexpected error\n        throw err\n      }\n    }\n\n    return parameters\n  }\n\n  async get(requestUri: RequestUri, deviceId?: DeviceId, clientId?: ClientId) {\n    const requestId = decodeRequestUri(requestUri)\n\n    const data = await this.store.readRequest(requestId)\n    if (!data) throw new InvalidRequestError('Unknown request_uri')\n\n    const updates: UpdateRequestData = {}\n\n    try {\n      if (data.sub || data.code) {\n        // If an account was linked to the request, the next step is to exchange\n        // the code for a token.\n        throw new AccessDeniedError(\n          data.parameters,\n          'This request was already authorized',\n        )\n      }\n\n      if (data.expiresAt < new Date()) {\n        throw new AccessDeniedError(data.parameters, 'This request has expired')\n      } else {\n        updates.expiresAt = new Date(\n          Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT,\n        )\n      }\n\n      if (clientId != null && data.clientId !== clientId) {\n        throw new AccessDeniedError(\n          data.parameters,\n          'This request was initiated for another client',\n        )\n      }\n\n      if (deviceId != null) {\n        if (!data.deviceId) {\n          updates.deviceId = deviceId\n        } else if (data.deviceId !== deviceId) {\n          throw new AccessDeniedError(\n            data.parameters,\n            'This request was initiated from another device',\n          )\n        }\n      }\n    } catch (err) {\n      await this.store.deleteRequest(requestId)\n      throw err\n    }\n\n    if (Object.keys(updates).length > 0) {\n      await this.store.updateRequest(requestId, updates)\n    }\n\n    return {\n      requestUri,\n      expiresAt: updates.expiresAt || data.expiresAt,\n      parameters: data.parameters,\n      clientId: data.clientId,\n    }\n  }\n\n  async setAuthorized(\n    requestUri: RequestUri,\n    client: Client,\n    account: Account,\n    deviceId: DeviceId,\n    deviceMetadata: RequestMetadata,\n    scopeOverride?: string,\n  ): Promise<Code> {\n    const requestId = decodeRequestUri(requestUri)\n\n    const data = await this.store.readRequest(requestId)\n    if (!data) throw new InvalidRequestError('Unknown request_uri')\n\n    let { parameters } = data\n\n    try {\n      if (data.expiresAt < new Date()) {\n        throw new AccessDeniedError(parameters, 'This request has expired')\n      }\n      if (!data.deviceId) {\n        throw new AccessDeniedError(\n          parameters,\n          'This request was not initiated',\n        )\n      }\n      if (data.deviceId !== deviceId) {\n        throw new AccessDeniedError(\n          parameters,\n          'This request was initiated from another device',\n        )\n      }\n      if (data.sub || data.code) {\n        throw new AccessDeniedError(\n          parameters,\n          'This request was already authorized',\n        )\n      }\n\n      // If a new scope value is provided, update the parameters by ensuring\n      // that every existing scope in the parameters is also present in the\n      // override value. This allows the user to remove scopes from the request,\n      // but not to add new ones.\n      if (scopeOverride != null) {\n        const allowedScopes = new Set(scopeOverride.split(' '))\n        const existingScopes = parameters.scope?.split(' ')\n\n        // Compute the intersection of the existing scopes and the overrides.\n        const newScopes = existingScopes?.filter((s) => allowedScopes.has(s))\n\n        // Validate: make sure the new scopes are valid\n        if (!newScopes?.includes('atproto')) {\n          throw new AccessDeniedError(\n            parameters,\n            'The \"atproto\" scope is required',\n          )\n        }\n\n        parameters = { ...parameters, scope: newScopes.join(' ') }\n      }\n\n      // Only response_type=code is supported\n      const code = await generateCode()\n\n      // Bind the request to the account, preventing it from being used again.\n      await this.store.updateRequest(requestId, {\n        sub: account.sub,\n        code,\n        // Allow the client to exchange the code for a token within the next 60 seconds.\n        expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),\n        parameters,\n      })\n\n      await this.hooks.onAuthorized?.call(null, {\n        client,\n        account,\n        parameters,\n        deviceId,\n        deviceMetadata,\n        requestId,\n      })\n\n      return code\n    } catch (err) {\n      await this.store.deleteRequest(requestId)\n      throw err\n    }\n  }\n\n  /**\n   * @note If this method throws an error, any token previously generated from\n   * the same `code` **must** me revoked.\n   */\n  public async consumeCode(code: Code): Promise<RequestDataAuthorized> {\n    const result = await this.store.consumeRequestCode(code)\n    if (!result) throw new InvalidGrantError('Invalid code')\n\n    const { requestId, data } = result\n\n    // Fool-proofing the store implementation against code replay attacks (in\n    // case consumeRequestCode() does not delete the request).\n    if (NODE_ENV !== 'production') {\n      const result = await this.store.readRequest(requestId)\n      if (result) {\n        throw new Error('Invalid store implementation: request not deleted')\n      }\n    }\n\n    if (!isRequestDataAuthorized(data) || data.code !== code) {\n      // Should never happen: maybe the store implementation is faulty ?\n      throw new Error('Unexpected request state')\n    }\n\n    if (data.expiresAt < new Date()) {\n      throw new InvalidGrantError('This code has expired')\n    }\n\n    return data\n  }\n\n  async delete(requestUri: RequestUri): Promise<void> {\n    const requestId = decodeRequestUri(requestUri)\n    await this.store.deleteRequest(requestId)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/request/request-store.ts",
    "content": "import { InvalidGrantError } from '../errors/invalid-grant-error.js'\nimport { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { Code } from './code.js'\nimport { RequestData } from './request-data.js'\nimport { RequestId } from './request-id.js'\n\n// Export all types needed to implement the RequestStore interface\nexport * from './code.js'\nexport * from './request-data.js'\nexport * from './request-id.js'\nexport type { Awaitable }\n\nexport type UpdateRequestData = Pick<\n  Partial<RequestData>,\n  'sub' | 'code' | 'deviceId' | 'expiresAt' | 'parameters'\n>\n\nexport type FoundRequestResult = {\n  requestId: RequestId\n  data: RequestData\n}\n\nexport { InvalidGrantError }\n\nexport interface RequestStore {\n  createRequest(requestId: RequestId, data: RequestData): Awaitable<void>\n  /**\n   * Note that expired requests **can** be returned to yield a different error\n   * message than if the request was not found.\n   */\n  readRequest(requestId: RequestId): Awaitable<RequestData | null>\n  updateRequest(requestId: RequestId, data: UpdateRequestData): Awaitable<void>\n  deleteRequest(requestId: RequestId): void | Awaitable<void>\n  /**\n   * @note it is **IMPORTANT** that this method prevents concurrent retrieval of\n   * the same code. If two requests are made with the same code, only one of\n   * them should succeed and return the request data.\n   *\n   * @throws {InvalidGrantError} - When the request is not found or has expired\n   * (allows to provide an error message instead of returning `null`).\n   */\n  consumeRequestCode(code: Code): Awaitable<FoundRequestResult | null>\n}\n\nexport const isRequestStore = buildInterfaceChecker<RequestStore>([\n  'createRequest',\n  'readRequest',\n  'updateRequest',\n  'deleteRequest',\n  'consumeRequestCode',\n])\n\nexport function asRequestStore<V extends Partial<RequestStore>>(\n  implementation?: V,\n): V & RequestStore {\n  if (!implementation || !isRequestStore(implementation)) {\n    throw new Error('Invalid RequestStore implementation')\n  }\n  return implementation\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/request/request-uri.ts",
    "content": "import { z } from 'zod'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { formatError } from '../lib/util/error.js'\nimport { RequestId, requestIdSchema } from './request-id.js'\n\nexport const REQUEST_URI_PREFIX = 'urn:ietf:params:oauth:request_uri:'\n\nexport const requestUriSchema = z\n  .string()\n  .refinement(\n    (data): data is `${typeof REQUEST_URI_PREFIX}${RequestId}` =>\n      data.startsWith(REQUEST_URI_PREFIX) &&\n      requestIdSchema.safeParse(decodeRequestUri(data as any)).success,\n    {\n      code: z.ZodIssueCode.custom,\n      message: 'Invalid request_uri format',\n    },\n  )\n\nexport type RequestUri = z.infer<typeof requestUriSchema>\n\nexport function encodeRequestUri(requestId: RequestId): RequestUri {\n  return `${REQUEST_URI_PREFIX}${encodeURIComponent(requestId) as RequestId}`\n}\n\nexport function decodeRequestUri(requestUri: RequestUri): RequestId {\n  const requestIdEnc = requestUri.slice(REQUEST_URI_PREFIX.length)\n  return decodeURIComponent(requestIdEnc) as RequestId\n}\n\nexport function parseRequestUri(\n  requestUri: string,\n  parseParams?: { path?: (string | number)[] },\n): RequestUri {\n  const parseResult = requestUriSchema.safeParse(requestUri, parseParams)\n  if (!parseResult.success) {\n    const err = parseResult.error\n    const msg = formatError(err, 'Invalid \"request_uri\" query parameter')\n    throw new InvalidRequestError(msg, err)\n  }\n  return parseResult.data\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/result/authorization-redirect-parameters.ts",
    "content": "import { OAuthTokenType } from '@atproto/oauth-types'\nimport { Code } from '../request/code.js'\n\n/**\n * @note `iss` and `state` will be added from the\n * {@link AuthorizationResultRedirect} object.\n */\nexport type AuthorizationRedirectParameters =\n  | {\n      // iss: string\n      // state?: string\n      code: Code\n      id_token?: string\n      access_token?: string\n      token_type?: OAuthTokenType\n      expires_in?: string\n    }\n  | {\n      // iss: string\n      // state?: string\n      error: string\n      error_description?: string\n      error_uri?: string\n    }\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/result/authorization-result-authorize-page.ts",
    "content": "import type { LexiconPermissionSet } from '@atproto/lex-document'\nimport type { Session } from '@atproto/oauth-provider-api'\nimport type { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport type { Client } from '../client/client.js'\nimport type { RequestUri } from '../request/request-uri.js'\n\nexport type AuthorizationResultAuthorizePage = {\n  issuer: string\n  client: Client\n  parameters: OAuthAuthorizationRequestParameters\n  permissionSets: Map<string, LexiconPermissionSet>\n\n  requestUri: RequestUri\n  sessions: readonly Session[]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/result/authorization-result-redirect.ts",
    "content": "import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'\nimport { AuthorizationRedirectParameters } from './authorization-redirect-parameters.js'\n\nexport type AuthorizationResultRedirect = {\n  issuer: string\n  parameters: OAuthAuthorizationRequestParameters\n  redirect: AuthorizationRedirectParameters\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/assets-manifest.ts",
    "content": "import { createReadStream } from 'node:fs'\nimport { join } from 'node:path'\nimport { Readable } from 'node:stream'\nimport type { Manifest } from '@atproto-labs/rollup-plugin-bundle-manifest'\nimport { AssetRef } from '../../lib/html/build-document.js'\nimport {\n  Middleware,\n  validateFetchDest,\n  validateFetchSite,\n  writeStream,\n} from '../../lib/http/index.js'\n\ntype Asset =\n  | {\n      type: 'asset'\n      mime?: string\n      sha256: string\n      stream: () => Readable\n    }\n  | {\n      type: 'chunk'\n      mime: string\n      sha256: string\n      dynamicImports: string[]\n      isDynamicEntry: boolean\n      isEntry: boolean\n      isImplicitEntry: boolean\n      name: string\n      stream: () => Readable\n    }\n\nconst ASSETS_URL_PREFIX = '/@atproto/oauth-provider/~assets/'\n\nexport function parseAssetsManifest(manifestPath: string) {\n  // Using `require` instead of `JSON.parse(readFileSync())` so that node's\n  // watch mode can pick up changes to the manifest file.\n\n  // eslint-disable-next-line\n  const manifest = require(manifestPath) as Manifest\n\n  const assets = new Map<string, Asset>(\n    Object.entries(manifest).map(([filename, { data, ...item }]) => {\n      const buffer = data ? Buffer.from(data, 'base64') : null\n      const filepath = join(manifestPath, '..', filename)\n      const stream = buffer\n        ? () => Readable.from(buffer)\n        : () => createReadStream(filepath)\n      return [filename, { ...item, stream }]\n    }),\n  )\n\n  const assetsMiddleware: Middleware = (req, res, next) => {\n    if (req.method !== 'GET' && req.method !== 'HEAD') return next()\n    if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()\n\n    const filename = decodeURIComponent(req.url.slice(ASSETS_URL_PREFIX.length))\n    if (!filename) return next()\n\n    const asset = assets.get(filename)\n    if (!asset) return next()\n\n    try {\n      // Allow \"null\" (ie. no header) to allow loading assets outside of a\n      // fetch context (not from a web page).\n      validateFetchSite(req, [null, 'none', 'cross-site', 'same-origin'])\n      validateFetchDest(req, [null, 'document', 'style', 'script'])\n    } catch (err) {\n      return next(err)\n    }\n\n    if (req.headers['if-none-match'] === asset.sha256) {\n      return void res.writeHead(304).end()\n    }\n\n    res.setHeader('ETag', asset.sha256)\n    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')\n\n    writeStream(res, asset.stream(), { contentType: asset.mime })\n  }\n\n  return {\n    getAssets,\n    assetsMiddleware,\n  }\n\n  function getAssets(entryName: string) {\n    const scripts = getScripts(entryName)\n    if (!scripts.length) return null\n    const styles = getStyles(entryName)\n    return { scripts, styles }\n  }\n\n  function getScripts(entryName: string) {\n    return Array.from(assets)\n      .filter(\n        ([, asset]) =>\n          asset.type === 'chunk' && asset.isEntry && asset.name === entryName,\n      )\n      .map(assetEntryUrl)\n  }\n\n  function getStyles(_entryName: string) {\n    return Array.from(assets)\n      .filter(([, asset]) => asset.mime === 'text/css')\n      .map(assetEntryUrl)\n  }\n}\n\nfunction assetEntryUrl([filename]: [string, Asset]): AssetRef {\n  return { url: assetUrl(filename) }\n}\n\nfunction assetUrl(filename: string) {\n  return `${ASSETS_URL_PREFIX}${encodeURIComponent(filename)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/assets.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { HydrationData as FeHydrationData } from '@atproto/oauth-provider-frontend/hydration-data'\nimport type { HydrationData as UiHydrationData } from '@atproto/oauth-provider-ui/hydration-data'\nimport { buildCustomizationCss } from '../../customization/build-customization-css.js'\nimport { buildCustomizationData } from '../../customization/build-customization-data.js'\nimport { Customization } from '../../customization/customization.js'\nimport { CspConfig, mergeCsp } from '../../lib/csp/index.js'\nimport { declareHydrationData } from '../../lib/html/hydration-data.js'\nimport { cssCode, html } from '../../lib/html/index.js'\nimport { combineMiddlewares } from '../../lib/http/middleware.js'\nimport { WriteResponseOptions } from '../../lib/http/response.js'\nimport {\n  CrossOriginEmbedderPolicy,\n  SecurityHeadersOptions,\n} from '../../lib/http/security-headers.js'\nimport { mergeDefaults } from '../../lib/util/object.js'\nimport { Simplify } from '../../lib/util/type.js'\nimport { WriteHtmlOptions, writeHtml } from '../../lib/write-html.js'\nimport { parseAssetsManifest } from './assets-manifest.js'\nimport { setupCsrfToken } from './csrf.js'\n\n// If the \"ui\" and \"frontend\" packages are ever unified, this can be replaced\n// with a single expression:\n//\n// const { getAssets, assetsMiddleware } = parseAssetsManifest(\n//   require.resolve('@atproto/oauth-provider-ui/bundle-manifest.json'),\n// )\n\nconst ui = parseAssetsManifest(\n  require.resolve('@atproto/oauth-provider-ui/bundle-manifest.json'),\n)\nconst fe = parseAssetsManifest(\n  require.resolve('@atproto/oauth-provider-frontend/bundle-manifest.json'),\n)\n\ntype HydrationData = Simplify<UiHydrationData & FeHydrationData>\n\nfunction getAssets(entryName: keyof HydrationData) {\n  const assetRef = ui.getAssets(entryName) || fe.getAssets(entryName)\n  if (assetRef) return assetRef\n\n  // Fool-proof. Should never happen.\n  throw new Error(`Entry \"${entryName}\" not found in assets`)\n}\n\nexport const assetsMiddleware = combineMiddlewares([\n  ui.assetsMiddleware,\n  fe.assetsMiddleware,\n])\n\nconst SPA_CSP: CspConfig = {\n  // API calls are made to the same origin\n  'connect-src': [\"'self'\"],\n  // Allow loading of PDS logo & User avatars\n  'img-src': ['data:', 'https:'],\n  // Prevent embedding in iframes\n  'frame-ancestors': [\"'none'\"],\n}\n\n/**\n * @see {@link https://docs.hcaptcha.com/#content-security-policy-settings}\n */\nconst HCAPTCHA_CSP: CspConfig = {\n  'script-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],\n  'frame-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],\n  'style-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],\n  'connect-src': ['https://hcaptcha.com', 'https://*.hcaptcha.com'],\n}\n\nexport type SendWebAppOptions = SecurityHeadersOptions & WriteResponseOptions\n\nexport function sendWebAppFactory<P extends keyof HydrationData>(\n  page: P,\n  customization: Customization,\n  defaults: SendWebAppOptions = {},\n) {\n  // Pre-computed options:\n  const customizationData = buildCustomizationData(customization)\n  const customizationCss = cssCode(buildCustomizationCss(customization))\n  const { scripts, styles } = getAssets(page)\n\n  const csp = mergeCsp(\n    SPA_CSP,\n    customization?.hcaptcha ? HCAPTCHA_CSP : undefined,\n  )\n\n  return async function sendWebApp(\n    req: IncomingMessage,\n    res: ServerResponse,\n    options: SendWebAppOptions & {\n      data: Omit<HydrationData[P], '__customizationData'>\n    },\n  ): Promise<void> {\n    await setupCsrfToken(req, res)\n\n    const script = declareHydrationData({\n      ...options.data,\n      __customizationData: customizationData,\n    })\n\n    return writeHtml(\n      res,\n      mergeDefaults<WriteHtmlOptions>(defaults, options, {\n        bodyAttrs: {\n          class:\n            'bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100',\n        },\n        csp: options?.csp ? mergeCsp(csp, options.csp) : csp,\n        coep: options?.coep ?? CrossOriginEmbedderPolicy.credentialless,\n        meta: [{ name: 'robots', content: 'noindex' }],\n        body: html`<div id=\"root\"></div>`,\n        scripts: [script, ...scripts],\n        styles: [...styles, customizationCss],\n      }),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/csrf.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport createHttpError from 'http-errors'\nimport { CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from '@atproto/oauth-provider-api'\nimport {\n  CookieSerializeOptions,\n  getCookie,\n  setCookie,\n} from '../../lib/http/index.js'\nimport { randomHexId } from '../../lib/util/crypto.js'\n\nconst TOKEN_BYTE_LENGTH = 12\nconst TOKEN_LENGTH = TOKEN_BYTE_LENGTH * 2 // 2 hex chars per byte\n\n// @NOTE Cookie based CSRF protection is redundant with session cookies using\n// `SameSite` and could probably be removed in the future.\nconst CSRF_COOKIE_OPTIONS: Readonly<CookieSerializeOptions> = {\n  expires: undefined, // \"session\" cookie\n  secure: true,\n  httpOnly: false, // Need to be accessible from JavaScript\n  sameSite: 'lax',\n  path: `/`,\n}\n\nasync function generateCsrfToken() {\n  return randomHexId(TOKEN_BYTE_LENGTH)\n}\n\nexport async function setupCsrfToken(\n  req: IncomingMessage,\n  res: ServerResponse,\n): Promise<void> {\n  const token = getCookieCsrf(req) || (await generateCsrfToken())\n\n  // Refresh cookie (See Chrome's \"Lax+POST\" behavior)\n  setCookie(res, CSRF_COOKIE_NAME, token, CSRF_COOKIE_OPTIONS)\n}\n\nexport async function validateCsrfToken(\n  req: IncomingMessage,\n  res: ServerResponse,\n) {\n  const cookieValue = getCookieCsrf(req)\n  const headerValue = getHeadersCsrf(req)\n\n  // Refresh cookie (See Chrome's \"Lax+POST\" behavior), or set a new one,\n  // allowing clients to retry with the new token.\n  setCookie(\n    res,\n    CSRF_COOKIE_NAME,\n    cookieValue || (await generateCsrfToken()),\n    CSRF_COOKIE_OPTIONS,\n  )\n\n  if (!headerValue) {\n    throw createHttpError(400, `Missing CSRF header`)\n  }\n  if (!cookieValue) {\n    throw createHttpError(400, `Missing CSRF cookie`)\n  }\n  if (cookieValue !== headerValue) {\n    throw createHttpError(400, `CSRF mismatch`)\n  }\n}\n\nexport function getCookieCsrf(req: IncomingMessage) {\n  const cookieValue = getCookie(req, CSRF_COOKIE_NAME)\n  if (cookieValue?.length === TOKEN_LENGTH) {\n    return cookieValue\n  }\n  return undefined\n}\n\nexport function getHeadersCsrf(req: IncomingMessage) {\n  const headerValue = req.headers[CSRF_HEADER_NAME]\n  if (typeof headerValue === 'string' && headerValue.length === TOKEN_LENGTH) {\n    return headerValue\n  }\n  return undefined\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/send-account-page.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { ActiveDeviceSession } from '@atproto/oauth-provider-api'\nimport { Customization } from '../../customization/customization.js'\nimport { SendWebAppOptions, sendWebAppFactory } from './assets.js'\n\nexport function sendAccountPageFactory(\n  customization: Customization,\n  options?: SendWebAppOptions,\n) {\n  const sendApp = sendWebAppFactory('account-page', customization, options)\n\n  return async function sendAccountPage(\n    req: IncomingMessage,\n    res: ServerResponse,\n    data: {\n      deviceSessions: readonly ActiveDeviceSession[]\n    },\n  ): Promise<void> {\n    return sendApp(req, res, {\n      data: { __deviceSessions: data.deviceSessions },\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/send-authorization-page.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { Customization } from '../../customization/customization.js'\nimport { AuthorizationResultAuthorizePage } from '../../result/authorization-result-authorize-page.js'\nimport { SendWebAppOptions, sendWebAppFactory } from './assets.js'\n\nexport function sendAuthorizePageFactory(\n  customization: Customization,\n  options?: SendWebAppOptions,\n) {\n  const sendApp = sendWebAppFactory(\n    'authorization-page',\n    customization,\n    options,\n  )\n\n  return async function sendAuthorizePage(\n    req: IncomingMessage,\n    res: ServerResponse,\n    data: AuthorizationResultAuthorizePage,\n  ): Promise<void> {\n    return sendApp(req, res, {\n      data: {\n        __authorizeData: {\n          requestUri: data.requestUri,\n\n          clientId: data.client.id,\n          clientMetadata: data.client.metadata,\n          clientTrusted: data.client.info.isTrusted,\n          clientFirstParty: data.client.info.isFirstParty,\n\n          scope: data.parameters.scope,\n          uiLocales: data.parameters.ui_locales,\n          loginHint: data.parameters.login_hint,\n          promptMode: data.parameters.prompt,\n          permissionSets: Object.fromEntries(data.permissionSets),\n        },\n        __sessions: data.sessions,\n      },\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/send-cookie-error-page.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { Customization } from '../../customization/customization.js'\nimport { SendWebAppOptions, sendWebAppFactory } from './assets.js'\n\nexport function sendCookieErrorPageFactory(\n  customization: Customization,\n  options?: SendWebAppOptions,\n) {\n  const sendApp = sendWebAppFactory('cookie-error-page', customization, options)\n\n  return async function sendCookieErrorPage(\n    req: IncomingMessage,\n    res: ServerResponse,\n    data: { continueUrl: URL },\n  ) {\n    return sendApp(req, res, {\n      status: 400,\n      data: { __continueUrl: data.continueUrl.toString() },\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/send-error-page.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { Customization } from '../../customization/customization.js'\nimport {\n  buildErrorPayload,\n  buildErrorStatus,\n} from '../../errors/error-parser.js'\nimport { SendWebAppOptions, sendWebAppFactory } from './assets.js'\n\nexport function sendErrorPageFactory(\n  customization: Customization,\n  options?: SendWebAppOptions,\n) {\n  const sendApp = sendWebAppFactory('error-page', customization, options)\n\n  return async function sendErrorPage(\n    req: IncomingMessage,\n    res: ServerResponse,\n    err: unknown,\n  ): Promise<void> {\n    return sendApp(req, res, {\n      status: buildErrorStatus(err),\n      data: { __errorData: buildErrorPayload(err) },\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/assets/send-redirect.ts",
    "content": "import type { ServerResponse } from 'node:http'\nimport {\n  OAuthAuthorizationRequestParameters,\n  OAuthResponseMode,\n} from '@atproto/oauth-types'\nimport { AuthorizationError } from '../../errors/authorization-error.js'\nimport {\n  WriteFormRedirectOptions,\n  writeFormRedirect,\n} from '../../lib/write-form-redirect.js'\nimport { AuthorizationRedirectParameters } from '../../result/authorization-redirect-parameters.js'\nimport { AuthorizationResultRedirect } from '../../result/authorization-result-redirect.js'\n\n// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-7.5.4\nconst REDIRECT_STATUS_CODE = 303\n\nexport const SUCCESS_REDIRECT_KEYS = [\n  'code',\n  'id_token',\n  'access_token',\n  'expires_in',\n  'token_type',\n] as const\n\nexport const ERROR_REDIRECT_KEYS = [\n  'error',\n  'error_description',\n  'error_uri',\n] as const\n\nexport type OAuthRedirectQueryParameter =\n  | 'iss'\n  | 'state'\n  | (typeof SUCCESS_REDIRECT_KEYS)[number]\n  | (typeof ERROR_REDIRECT_KEYS)[number]\n\nexport function buildRedirectUri(\n  parameters: OAuthAuthorizationRequestParameters,\n): string {\n  const uri = parameters.redirect_uri\n  if (uri) return uri\n\n  throw new AuthorizationError(parameters, 'No redirect_uri', 'invalid_request')\n}\n\nexport function buildRedirectMode(\n  parameters: OAuthAuthorizationRequestParameters,\n): OAuthResponseMode {\n  const mode = parameters.response_mode || 'query' // @TODO default should depend on response_type\n  return mode\n}\n\nexport function buildRedirectParams(\n  issuer: string,\n  parameters: OAuthAuthorizationRequestParameters,\n  redirect: AuthorizationRedirectParameters,\n): [OAuthRedirectQueryParameter, string][] {\n  const params: [OAuthRedirectQueryParameter, string][] = [\n    ['iss', issuer], // rfc9207\n  ]\n\n  if (parameters.state != null) {\n    params.push(['state', parameters.state])\n  }\n\n  const keys = 'code' in redirect ? SUCCESS_REDIRECT_KEYS : ERROR_REDIRECT_KEYS\n  for (const key of keys) {\n    const value = redirect[key]\n    if (value != null) params.push([key, value])\n  }\n\n  return params\n}\n\nexport function sendAuthorizationResultRedirect(\n  res: ServerResponse,\n  result: AuthorizationResultRedirect,\n  options?: WriteFormRedirectOptions,\n) {\n  const { issuer, parameters, redirect } = result\n\n  return sendRedirect(\n    res,\n    {\n      redirectUri: buildRedirectUri(parameters),\n      mode: buildRedirectMode(parameters),\n      params: buildRedirectParams(issuer, parameters, redirect),\n    },\n    options,\n  )\n}\n\nexport type OAuthRedirectOptions = {\n  mode: OAuthResponseMode\n  redirectUri: string\n  params: Iterable<[string, string]>\n}\n\nexport function sendRedirect(\n  res: ServerResponse,\n  redirect: OAuthRedirectOptions,\n  options?: WriteFormRedirectOptions,\n): void {\n  res.setHeader('Cache-Control', 'no-store')\n\n  const { mode, redirectUri: uri, params } = redirect\n  switch (mode) {\n    case 'query':\n      return writeQuery(res, uri, params)\n    case 'fragment':\n      return writeFragment(res, uri, params)\n    case 'form_post':\n      return writeFormRedirect(res, 'post', uri, params, options)\n  }\n\n  // @ts-expect-error fool proof\n  throw new Error(`Unsupported mode: ${mode}`)\n}\n\nfunction writeQuery(\n  res: ServerResponse,\n  uri: string,\n  params: Iterable<[string, string]>,\n): void {\n  const url = new URL(uri)\n  for (const [key, value] of params) url.searchParams.set(key, value)\n  res.writeHead(REDIRECT_STATUS_CODE, { Location: url.href }).end()\n}\n\nfunction writeFragment(\n  res: ServerResponse,\n  uri: string,\n  params: Iterable<[string, string]>,\n): void {\n  const url = new URL(uri)\n  const searchParams = new URLSearchParams()\n  for (const [key, value] of params) searchParams.set(key, value)\n  url.hash = searchParams.toString()\n  res.writeHead(REDIRECT_STATUS_CODE, { Location: url.href }).end()\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/create-account-page-middleware.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { ActiveDeviceSession } from '@atproto/oauth-provider-api'\nimport {\n  Middleware,\n  Router,\n  validateFetchDest,\n  validateFetchMode,\n  validateOrigin,\n  writeRedirect,\n} from '../lib/http/index.js'\nimport { SecurityHeadersOptions } from '../lib/http/security-headers.js'\nimport type { OAuthProvider } from '../oauth-provider.js'\nimport { sendAccountPageFactory } from './assets/send-account-page.js'\nimport { sendErrorPageFactory } from './assets/send-error-page.js'\nimport type { MiddlewareOptions } from './middleware-options.js'\n\nexport function createAccountPageMiddleware<\n  Ctx extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  server: OAuthProvider,\n  { onError }: MiddlewareOptions<Req, Res>,\n): Middleware<Ctx, Req, Res> {\n  const issuerUrl = new URL(server.issuer)\n  const issuerOrigin = issuerUrl.origin\n\n  const securityOptions: SecurityHeadersOptions = {\n    hsts: issuerUrl.protocol === 'http:' ? false : undefined,\n  }\n\n  const sendAccountPage = sendAccountPageFactory(\n    server.customization,\n    securityOptions,\n  )\n  const sendErrorPage = sendErrorPageFactory(\n    server.customization,\n    securityOptions,\n  )\n\n  const router = new Router<Ctx, Req, Res>(issuerUrl)\n\n  // Create password reset discovery endpoint\n  // https://w3c.github.io/webappsec-change-password-url/\n  router.get('/.well-known/change-password', (_req, res) => {\n    writeRedirect(res, new URL('/account/reset-password', issuerUrl).toString())\n  })\n\n  // Create frontend account pages\n  router.get<never>(/^\\/account(?:\\/.*)?$/, async function (req, res) {\n    try {\n      res.setHeader('Referrer-Policy', 'same-origin')\n\n      res.setHeader('Cache-Control', 'no-store')\n      res.setHeader('Pragma', 'no-cache')\n\n      validateFetchMode(req, ['navigate'])\n      validateFetchDest(req, ['document'])\n      validateOrigin(req, issuerOrigin)\n\n      const { deviceId } = await server.deviceManager.load(req, res)\n      const deviceAccounts =\n        await server.accountManager.listDeviceAccounts(deviceId)\n\n      sendAccountPage(req, res, {\n        deviceSessions: deviceAccounts.map(\n          (deviceAccount): ActiveDeviceSession => ({\n            account: deviceAccount.account,\n            loginRequired: server.checkLoginRequired(deviceAccount),\n          }),\n        ),\n      })\n    } catch (err) {\n      onError?.(\n        req,\n        res,\n        err,\n        `Failed to handle navigation request to \"${req.url}\"`,\n      )\n\n      if (!res.headersSent) {\n        return sendErrorPage(req, res, err)\n      }\n    }\n  })\n\n  return router.buildMiddleware()\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/create-api-middleware.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport createHttpError from 'http-errors'\nimport { z } from 'zod'\nimport { signedJwtSchema } from '@atproto/jwk'\nimport {\n  API_ENDPOINT_PREFIX,\n  ActiveAccountSession,\n  ActiveDeviceSession,\n  ActiveOAuthSession,\n  ApiEndpoints,\n  ISODateString,\n} from '@atproto/oauth-provider-api'\nimport {\n  OAuthAuthorizationRequestParameters,\n  OAuthRedirectUri,\n  OAuthResponseMode,\n  oauthRedirectUriSchema,\n  oauthResponseModeSchema,\n} from '@atproto/oauth-types'\nimport { signInDataSchema } from '../account/sign-in-data.js'\nimport { signUpInputSchema } from '../account/sign-up-input.js'\nimport { DeviceId, deviceIdSchema } from '../device/device-id.js'\nimport { AuthorizationError } from '../errors/authorization-error.js'\nimport {\n  ErrorPayload,\n  buildErrorPayload,\n  buildErrorStatus,\n} from '../errors/error-parser.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { WWWAuthenticateError } from '../errors/www-authenticate-error.js'\nimport {\n  JsonResponse,\n  Middleware,\n  RequestMetadata,\n  Router,\n  RouterCtx,\n  SubCtx,\n  flushStream,\n  jsonHandler,\n  parseHttpRequest,\n  subCtx,\n  validateFetchMode,\n  validateFetchSite,\n  validateOrigin,\n  validateReferrer,\n} from '../lib/http/index.js'\nimport { RouteCtx, createRoute } from '../lib/http/route.js'\nimport { asArray } from '../lib/util/cast.js'\nimport { localeSchema } from '../lib/util/locale.js'\nimport type { Awaitable } from '../lib/util/type.js'\nimport type { OAuthProvider } from '../oauth-provider.js'\nimport { Sub, subSchema } from '../oidc/sub.js'\nimport { RequestUri, requestUriSchema } from '../request/request-uri.js'\nimport { AuthorizationRedirectParameters } from '../result/authorization-redirect-parameters.js'\nimport { tokenIdSchema } from '../token/token-id.js'\nimport { emailOtpSchema } from '../types/email-otp.js'\nimport { emailSchema } from '../types/email.js'\nimport { handleSchema } from '../types/handle.js'\nimport { newPasswordSchema } from '../types/password.js'\nimport { validateCsrfToken } from './assets/csrf.js'\nimport {\n  ERROR_REDIRECT_KEYS,\n  OAuthRedirectOptions,\n  OAuthRedirectQueryParameter,\n  SUCCESS_REDIRECT_KEYS,\n  buildRedirectMode,\n  buildRedirectParams,\n  buildRedirectUri,\n} from './assets/send-redirect.js'\nimport type { MiddlewareOptions } from './middleware-options.js'\n\nconst verifyHandleSchema = z.object({ handle: handleSchema }).strict()\n\nexport function createApiMiddleware<\n  Ctx extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  server: OAuthProvider,\n  { onError }: MiddlewareOptions<Req, Res>,\n): Middleware<Ctx, Req, Res> {\n  const issuerUrl = new URL(server.issuer)\n  const issuerOrigin = issuerUrl.origin\n  const router = new Router<Ctx, Req, Res>(issuerUrl)\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/verify-handle-availability',\n      schema: verifyHandleSchema,\n      async handler() {\n        await server.accountManager.verifyHandleAvailability(this.input.handle)\n        return { json: { available: true } }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/sign-up',\n      schema: signUpInputSchema,\n      rotateDeviceCookies: true,\n      async handler() {\n        const { deviceId, deviceMetadata, input, requestUri } = this\n\n        const account = await server.accountManager.createAccount(\n          deviceId,\n          deviceMetadata,\n          input,\n        )\n\n        // Remember when not in the context of a request by default\n        const remember = requestUri == null\n\n        // Only \"remember\" the newly created account if it was not created during an\n        // OAuth flow.\n        if (remember) {\n          await server.accountManager.upsertDeviceAccount(deviceId, account.sub)\n        }\n\n        const ephemeralToken = remember\n          ? undefined\n          : await server.signer.createEphemeralToken({\n              sub: account.sub,\n              deviceId,\n              requestUri: this.requestUri,\n            })\n\n        const json = { account, ephemeralToken }\n        return { json }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/sign-in',\n      schema: signInDataSchema.extend({ remember: z.boolean().optional() }),\n      rotateDeviceCookies: true,\n      async handler() {\n        const { deviceId, deviceMetadata, requestUri } = this\n\n        // Remember when not in the context of a request by default\n        const { remember = requestUri == null, ...input } = this.input\n\n        const account = await server.accountManager.authenticateAccount(\n          deviceId,\n          deviceMetadata,\n          input,\n        )\n\n        if (remember) {\n          await server.accountManager.upsertDeviceAccount(deviceId, account.sub)\n        } else {\n          // In case the user was already signed in, and signed in again, this\n          // time without \"remember me\", let's sign them off of the device.\n          await server.accountManager.removeDeviceAccount(deviceId, account.sub)\n        }\n\n        const ephemeralToken = remember\n          ? undefined\n          : await server.signer.createEphemeralToken({\n              sub: account.sub,\n              deviceId,\n              requestUri,\n            })\n\n        if (requestUri) {\n          // Check if a consent is required for the client, but only if this\n          // call is made within the context of an oauth request.\n\n          const { clientId, parameters } = await server.requestManager.get(\n            requestUri,\n            deviceId,\n          )\n\n          const { authorizedClients } = await server.accountManager.getAccount(\n            account.sub,\n          )\n\n          const json = {\n            account,\n            ephemeralToken,\n            consentRequired: server.checkConsentRequired(\n              parameters,\n              authorizedClients.get(clientId),\n            ),\n          }\n\n          return { json }\n        }\n\n        const json = { account, ephemeralToken }\n        return { json }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/sign-out',\n      schema: z\n        .object({\n          sub: z.union([subSchema, z.array(subSchema)]),\n        })\n        .strict(),\n      rotateDeviceCookies: true,\n      async handler() {\n        const uniqueSubs = new Set(asArray(this.input.sub))\n\n        for (const sub of uniqueSubs) {\n          await server.accountManager.removeDeviceAccount(this.deviceId, sub)\n        }\n\n        return { json: { success: true as const } }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/reset-password-request',\n      schema: z\n        .object({\n          locale: localeSchema,\n          email: emailSchema,\n        })\n        .strict(),\n      async handler() {\n        await server.accountManager.resetPasswordRequest(\n          this.deviceId,\n          this.deviceMetadata,\n          this.input,\n        )\n        return { json: { success: true } }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/reset-password-confirm',\n      schema: z\n        .object({\n          token: emailOtpSchema,\n          password: newPasswordSchema,\n        })\n        .strict(),\n      async handler() {\n        await server.accountManager.resetPasswordConfirm(\n          this.deviceId,\n          this.deviceMetadata,\n          this.input,\n        )\n        return { json: { success: true } }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'GET',\n      endpoint: '/device-sessions',\n      schema: undefined,\n      async handler() {\n        const deviceAccounts = await server.accountManager.listDeviceAccounts(\n          this.deviceId,\n        )\n\n        const json = deviceAccounts.map(\n          (deviceAccount): ActiveDeviceSession => ({\n            account: deviceAccount.account,\n            loginRequired: server.checkLoginRequired(deviceAccount),\n          }),\n        )\n\n        return { json }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'GET',\n      endpoint: '/oauth-sessions',\n      schema: z.object({ sub: subSchema }).strict(),\n      async handler(req, res) {\n        const { account } = await authenticate.call(this, req, res)\n\n        const tokenInfos = await server.tokenManager.listAccountTokens(\n          account.sub,\n        )\n\n        const clientIds = tokenInfos.map((tokenInfo) => tokenInfo.data.clientId)\n\n        const clients = await server.clientManager.loadClients(clientIds, {\n          onError: (err, clientId) => {\n            onError?.(req, res, err, `Failed to load client ${clientId}`)\n            return undefined // metadata won't be available in the UI\n          },\n        })\n\n        // @TODO: We should ideally filter sessions that are expired (or even\n        // expose the expiration date). This requires a change to the way\n        // TokenInfo are stored (see TokenManager#isTokenExpired and\n        // TokenManager#isTokenInactive).\n        const json = tokenInfos.map(({ id, data }): ActiveOAuthSession => {\n          return {\n            tokenId: id,\n\n            createdAt: data.createdAt.toISOString() as ISODateString,\n            updatedAt: data.updatedAt.toISOString() as ISODateString,\n\n            clientId: data.clientId,\n            clientMetadata: clients.get(data.clientId)?.metadata,\n\n            scope: data.parameters.scope,\n          }\n        })\n\n        return { json }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'GET',\n      endpoint: '/account-sessions',\n      schema: z.object({ sub: subSchema }).strict(),\n      async handler(req, res) {\n        const { account } = await authenticate.call(this, req, res)\n\n        const deviceAccounts = await server.accountManager.listAccountDevices(\n          account.sub,\n        )\n\n        const json = deviceAccounts.map(\n          (accountSession): ActiveAccountSession => ({\n            deviceId: accountSession.deviceId,\n            deviceMetadata: {\n              ipAddress: accountSession.deviceData.ipAddress,\n              userAgent: accountSession.deviceData.userAgent,\n              lastSeenAt:\n                accountSession.deviceData.lastSeenAt.toISOString() as ISODateString,\n            },\n\n            isCurrentDevice: accountSession.deviceId === this.deviceId,\n          }),\n        )\n\n        return { json }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/revoke-account-session',\n      schema: z.object({ sub: subSchema, deviceId: deviceIdSchema }).strict(),\n      async handler() {\n        // @NOTE This route is not authenticated. If a user is able to steal\n        // another user's session cookie, we allow them to revoke the device\n        // session.\n\n        await server.accountManager.removeDeviceAccount(\n          this.input.deviceId,\n          this.input.sub,\n        )\n\n        return { json: { success: true } }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/revoke-oauth-session',\n      schema: z.object({ sub: subSchema, tokenId: tokenIdSchema }).strict(),\n      async handler(req, res) {\n        const { account } = await authenticate.call(this, req, res)\n\n        const tokenInfo = await server.tokenManager.getTokenInfo(\n          this.input.tokenId,\n        )\n\n        if (!tokenInfo || tokenInfo.account.sub !== account.sub) {\n          // report this as though the token was not found\n          throw new InvalidRequestError(`Invalid token`)\n        }\n\n        await server.tokenManager.deleteToken(tokenInfo.id)\n\n        return { json: { success: true } }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/consent',\n      schema: z\n        .object({\n          sub: z.union([subSchema, signedJwtSchema]),\n          scope: z.string().optional(),\n        })\n        .strict(),\n      async handler(req, res) {\n        if (!this.requestUri) {\n          throw new InvalidRequestError(\n            'This endpoint can only be used in the context of an OAuth request',\n          )\n        }\n\n        // Any AuthorizationError caught in this block will result in a redirect\n        // to the client's redirect_uri with an error.\n        try {\n          const { clientId, parameters } = await server.requestManager.get(\n            this.requestUri,\n            this.deviceId,\n          )\n\n          // Any error thrown in this block will be transformed into an\n          // AuthorizationError.\n          try {\n            const { account, authorizedClients } = await authenticate.call(\n              this,\n              req,\n              res,\n            )\n\n            const client = await server.clientManager.getClient(clientId)\n\n            const code = await server.requestManager.setAuthorized(\n              this.requestUri,\n              client,\n              account,\n              this.deviceId,\n              this.deviceMetadata,\n              this.input.scope,\n            )\n\n            const clientData = authorizedClients.get(clientId)\n            if (server.checkConsentRequired(parameters, clientData)) {\n              const scopes = new Set(clientData?.authorizedScopes)\n\n              // Add the newly accepted scopes to the authorized scopes\n\n              // @NOTE `oauthScopeSchema` ensures that `scope` contains no\n              // leading/trailing/duplicate spaces.\n              for (const s of parameters.scope?.split(' ') ?? []) scopes.add(s)\n\n              await server.accountManager.setAuthorizedClient(account, client, {\n                ...clientData,\n                authorizedScopes: [...scopes],\n              })\n            }\n\n            const url = buildRedirectUrl(server.issuer, parameters, { code })\n\n            return { json: { url } }\n          } catch (err) {\n            // Since we have access to the parameters, we can re-throw an\n            // AuthorizationError with the redirect_uri parameter.\n            throw AuthorizationError.from(parameters, err)\n          }\n        } catch (err) {\n          onError?.(req, res, err, 'Failed to consent authorization request')\n\n          // If any error happened (unauthenticated, invalid request, etc.),\n          // lets make sure the request can no longer be used.\n          try {\n            await server.requestManager.delete(this.requestUri)\n          } catch (err) {\n            onError?.(req, res, err, 'Failed to delete request')\n          }\n\n          if (err instanceof AuthorizationError) {\n            try {\n              const url = buildRedirectUrl(\n                server.issuer,\n                err.parameters,\n                err.toJSON(),\n              )\n\n              return { json: { url } }\n            } catch {\n              // Unable to build redirect URL, ignore\n            }\n          }\n\n          // @NOTE Not re-throwing the error here, as the error was already\n          // handled by the `onError` callback, and apiRoute (`apiMiddleware`)\n          // would call `onError` again.\n          return buildErrorJsonResponse(err)\n        }\n      },\n    }),\n  )\n\n  router.use(\n    apiRoute({\n      method: 'POST',\n      endpoint: '/reject',\n      schema: z.object({}).strict(),\n      rotateDeviceCookies: true,\n      async handler(req, res) {\n        const { requestUri } = this\n        if (!requestUri) {\n          throw new InvalidRequestError(\n            'This endpoint can only be used in the context of an OAuth request',\n          )\n        }\n\n        // Once this endpoint is called, the request will definitely be\n        // rejected.\n        try {\n          // No need to authenticate the user here as they are not authorizing a\n          // particular account (CSRF protection is enough).\n\n          // @NOTE that the client could *technically* trigger this endpoint while\n          // the user is on the authorize page by forging the request (because the\n          // client knows the RequestURI from PAR and has all the info needed to\n          // forge the request, including CSRF). This cannot be used as DoS attack\n          // as the request ID is not guessable and would only result in a bad UX\n          // for misbehaving clients, only for the users of those clients.\n\n          const { parameters } = await server.requestManager.get(\n            requestUri,\n            this.deviceId,\n          )\n\n          const url = buildRedirectUrl(server.issuer, parameters, {\n            error: 'access_denied',\n            error_description: 'The user rejected the request',\n          })\n\n          return { json: { url } }\n        } catch (err) {\n          onError?.(req, res, err, 'Failed to reject authorization request')\n\n          if (err instanceof AuthorizationError) {\n            try {\n              const url = buildRedirectUrl(\n                server.issuer,\n                err.parameters,\n                err.toJSON(),\n              )\n\n              return { json: { url } }\n            } catch {\n              // Unable to build redirect URL, ignore\n            }\n          }\n\n          return buildErrorJsonResponse(err)\n        } finally {\n          await server.requestManager.delete(requestUri).catch((err) => {\n            onError?.(req, res, err, 'Failed to delete request')\n          })\n        }\n      },\n    }),\n  )\n\n  return router.buildMiddleware()\n\n  async function authenticate(\n    this: ApiContext<void, { sub: Sub }>,\n    req: Req,\n    res: Res,\n  ) {\n    const authorization = req.headers.authorization?.split(' ')\n    if (authorization?.[0].toLowerCase() === 'bearer') {\n      try {\n        // If there is an authorization header, verify that the ephemeral token it\n        // contains is a jwt bound to the right [sub, device, request].\n        const ephemeralToken = signedJwtSchema.parse(authorization[1])\n        const { payload } =\n          await server.signer.verifyEphemeralToken(ephemeralToken)\n\n        if (\n          payload.sub === this.input.sub &&\n          payload.deviceId === this.deviceId &&\n          payload.requestUri === this.requestUri\n        ) {\n          return await server.accountManager.getAccount(payload.sub)\n        }\n      } catch (err) {\n        onError?.(req, res, err, 'Failed to authenticate ephemeral token')\n        // Fall back to session based authentication\n      }\n    }\n\n    try {\n      // Ensures the \"sub\" has an active session on the device\n      const deviceAccount = await server.accountManager.getDeviceAccount(\n        this.deviceId,\n        this.input.sub,\n      )\n\n      // The session exists but was created too long ago\n      if (server.checkLoginRequired(deviceAccount)) {\n        throw new InvalidRequestError('Login required')\n      }\n\n      return deviceAccount\n    } catch (err) {\n      throw new WWWAuthenticateError(\n        'unauthorized',\n        `User ${this.input.sub} not authenticated on this device`,\n        { Bearer: {} },\n        err,\n      )\n    }\n  }\n\n  type ApiContext<T extends object | void, I = void> = SubCtx<\n    T,\n    {\n      deviceId: DeviceId\n      deviceMetadata: RequestMetadata\n\n      /**\n       * The parsed input data (json payload if \"POST\", query params if \"GET\").\n       */\n      input: I\n\n      /**\n       * When defined, the request originated from the authorize page.\n       */\n      requestUri?: RequestUri\n    }\n  >\n\n  type InferValidation<S extends void | z.ZodTypeAny> = S extends z.ZodTypeAny\n    ? z.infer<S>\n    : void\n\n  /**\n   * The main purpose of this function is to ensure that the endpoint\n   * implementation matches its type definition from {@link ApiEndpoints}.\n   * @private\n   */\n  function apiRoute<\n    C extends RouterCtx<Ctx>,\n    M extends 'GET' | 'POST',\n    E extends `/${string}` &\n      // Extract all the endpoint path that match the method (allows for\n      // auto-complete & better error reporting)\n      {\n        [E in keyof ApiEndpoints]: ApiEndpoints[E] extends { method: M }\n          ? E\n          : never\n      }[keyof ApiEndpoints],\n    S extends // A schema that validates the POST input or GET params\n      ApiEndpoints[E] extends { method: 'POST'; input: infer I }\n        ? z.ZodType<I>\n        : ApiEndpoints[E] extends { method: 'GET'; params: infer P }\n          ? z.ZodType<P>\n          : void,\n  >(options: {\n    method: M\n    endpoint: E\n    schema: S\n    rotateDeviceCookies?: boolean\n    handler: (\n      this: ApiContext<RouteCtx<C>, InferValidation<S>>,\n      req: Req,\n      res: Res,\n    ) => Awaitable<JsonResponse<ErrorPayload | ApiEndpoints[E]['output']>>\n  }): Middleware<C, Req, Res> {\n    return createRoute(\n      options.method,\n      `${API_ENDPOINT_PREFIX}${options.endpoint}`,\n      apiMiddleware(options),\n    )\n  }\n\n  function apiMiddleware<C extends RouterCtx, S extends void | z.ZodTypeAny>({\n    method,\n    schema,\n    rotateDeviceCookies,\n    handler,\n  }: {\n    method: 'GET' | 'POST'\n    schema: S\n    rotateDeviceCookies?: boolean\n    handler: (\n      this: ApiContext<C, InferValidation<S>>,\n      req: Req,\n      res: Res,\n    ) => Awaitable<JsonResponse>\n  }): Middleware<C, Req, Res> {\n    const parseInput: (this: C, req: Req) => Promise<InferValidation<S>> =\n      schema == null // No schema means endpoint doesn't accept any input\n        ? async function (req) {\n            await flushStream(req)\n            return undefined\n          }\n        : method === 'POST'\n          ? async function (req) {\n              const body = await parseHttpRequest(req, ['json'])\n              return schema.parseAsync(body, { path: ['body'] })\n            }\n          : async function (req) {\n              await flushStream(req)\n              const query = Object.fromEntries(this.url.searchParams)\n              return schema.parseAsync(query, { path: ['query'] })\n            }\n\n    return jsonHandler<C, Req, Res>(async function (req, res) {\n      try {\n        // Prevent caching of API routes\n        res.setHeader('Cache-Control', 'no-store')\n        res.setHeader('Pragma', 'no-cache')\n\n        // Prevent CORS requests\n        validateFetchMode(req, ['same-origin'])\n        validateFetchSite(req, ['same-origin'])\n        validateOrigin(req, issuerOrigin)\n        const referrer = validateReferrer(req, { origin: issuerOrigin })\n\n        // Ensure we are one the right page\n        if (\n          // trailing slashes are not allowed\n          referrer.pathname !== '/oauth/authorize' &&\n          referrer.pathname !== '/account' &&\n          !referrer.pathname.startsWith(`/account/`)\n        ) {\n          throw createHttpError(400, `Invalid referrer ${referrer}`)\n        }\n\n        // Check if the request originated from the authorize page\n        const requestUri =\n          referrer.pathname === '/oauth/authorize'\n            ? await requestUriSchema.parseAsync(\n                referrer.searchParams.get('request_uri'),\n              )\n            : undefined\n\n        // Validate CSRF token\n        await validateCsrfToken(req, res)\n\n        // Parse and validate the input data\n        const input = await parseInput.call(this, req)\n\n        // Load session data, rotating the session cookie if needed\n        const { deviceId, deviceMetadata } = await server.deviceManager.load(\n          req,\n          res,\n          rotateDeviceCookies,\n        )\n\n        const context: ApiContext<C, InferValidation<S>> = subCtx(this, {\n          input,\n          requestUri,\n          deviceId,\n          deviceMetadata,\n        })\n\n        return await handler.call(context, req, res)\n      } catch (err) {\n        onError?.(req, res, err, `Failed to handle API request`)\n\n        // Make sore to always return a JSON response\n        return buildErrorJsonResponse(err)\n      }\n    })\n  }\n}\n\nfunction buildErrorJsonResponse(err: unknown) {\n  // @TODO Rework the API error responses (relying on codes)\n  const json = buildErrorPayload(err)\n  const status = buildErrorStatus(err)\n\n  return { json, status }\n}\n\nfunction buildRedirectUrl(\n  iss: string,\n  parameters: OAuthAuthorizationRequestParameters,\n  redirect: AuthorizationRedirectParameters,\n): string {\n  const url = new URL('/oauth/authorize/redirect', iss)\n\n  url.searchParams.set('redirect_mode', buildRedirectMode(parameters))\n  url.searchParams.set('redirect_uri', buildRedirectUri(parameters))\n\n  for (const [key, value] of buildRedirectParams(iss, parameters, redirect)) {\n    url.searchParams.set(key, value)\n  }\n\n  return url.href\n}\n\nexport function parseRedirectUrl(url: URL): OAuthRedirectOptions {\n  if (url.pathname !== '/oauth/authorize/redirect') {\n    throw new InvalidRequestError(\n      `Invalid redirect URL: ${url.pathname} is not a valid path`,\n    )\n  }\n\n  const params: [OAuthRedirectQueryParameter, string][] = []\n\n  const state = url.searchParams.get('state')\n  if (state) params.push(['state', state])\n\n  const iss = url.searchParams.get('iss')\n  if (iss) params.push(['iss', iss])\n\n  if (url.searchParams.has('code')) {\n    for (const key of SUCCESS_REDIRECT_KEYS) {\n      const value = url.searchParams.get(key)\n      if (value != null) params.push([key, value])\n    }\n  } else if (url.searchParams.has('error')) {\n    for (const key of ERROR_REDIRECT_KEYS) {\n      const value = url.searchParams.get(key)\n      if (value != null) params.push([key, value])\n    }\n  } else {\n    throw new InvalidRequestError(\n      'Invalid redirect URL: neither code nor error found',\n    )\n  }\n\n  try {\n    const mode: OAuthResponseMode = oauthResponseModeSchema.parse(\n      url.searchParams.get('redirect_mode'),\n    )\n\n    const redirectUri: OAuthRedirectUri = oauthRedirectUriSchema.parse(\n      url.searchParams.get('redirect_uri'),\n    )\n\n    return { mode, redirectUri, params }\n  } catch (err) {\n    throw InvalidRequestError.from(err, 'Invalid redirect URL')\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport {\n  OAuthAuthorizationRequestQuery,\n  oauthAuthorizationRequestQuerySchema,\n} from '@atproto/oauth-types'\nimport { AuthorizationError } from '../errors/authorization-error.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport {\n  Middleware,\n  Router,\n  RouterCtx,\n  getCookie,\n  setCookie,\n  validateFetchDest,\n  validateFetchMode,\n  validateFetchSite,\n  validateOrigin,\n  validateReferrer,\n} from '../lib/http/index.js'\nimport { SecurityHeadersOptions } from '../lib/http/security-headers.js'\nimport { formatError } from '../lib/util/error.js'\nimport type { Awaitable } from '../lib/util/type.js'\nimport { writeFormRedirect } from '../lib/write-form-redirect.js'\nimport type { OAuthProvider } from '../oauth-provider.js'\nimport { parseRequestUri, requestUriSchema } from '../request/request-uri.js'\nimport { sendAuthorizePageFactory } from './assets/send-authorization-page.js'\nimport { sendCookieErrorPageFactory } from './assets/send-cookie-error-page.js'\nimport { sendErrorPageFactory } from './assets/send-error-page.js'\nimport {\n  sendAuthorizationResultRedirect,\n  sendRedirect,\n} from './assets/send-redirect.js'\nimport { parseRedirectUrl } from './create-api-middleware.js'\nimport type { MiddlewareOptions } from './middleware-options.js'\n\nexport function createAuthorizationPageMiddleware<\n  Ctx extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  server: OAuthProvider,\n  { onError }: MiddlewareOptions<Req, Res>,\n): Middleware<Ctx, Req, Res> {\n  const issuerUrl = new URL(server.issuer)\n  const issuerOrigin = issuerUrl.origin\n\n  const securityOptions: SecurityHeadersOptions = {\n    hsts: issuerUrl.protocol === 'http:' ? false : undefined,\n  }\n\n  const sendAuthorizePage = sendAuthorizePageFactory(\n    server.customization,\n    securityOptions,\n  )\n  const sendErrorPage = sendErrorPageFactory(\n    server.customization,\n    securityOptions,\n  )\n  const sendCookieErrorPage = sendCookieErrorPageFactory(\n    server.customization,\n    securityOptions,\n  )\n\n  const router = new Router<Ctx, Req, Res>(issuerUrl)\n\n  router.get(\n    '/oauth/authorize',\n    withErrorHandler(async function (req, res) {\n      res.setHeader('Cache-Control', 'no-store')\n      res.setHeader('Pragma', 'no-cache')\n\n      // \"same-origin\" is required to support the redirect test logic below (as\n      // well as refreshing the authorization page).\n\n      // @TODO Consider removing this altogether to allow hosting PDS and app on\n      // the same site but different origins (different subdomains).\n      validateFetchSite(req, ['same-origin', 'cross-site', 'none'])\n      validateFetchMode(req, ['navigate'])\n      validateFetchDest(req, ['document'])\n      validateOrigin(req, issuerOrigin)\n\n      // Do not perform any of the following logic if the request is invalid\n      const query = parseOAuthAuthorizationRequestQuery(this.url)\n\n      // @NOTE For some reason, even when loaded through a\n      // ASWebAuthenticationSession, iOS will sometimes fail to properly save\n      // cookies set during the rendering of the page. When this happens, the\n      // authorization page logic, which relies on cookies to maintain the session,\n      // will fail. To work around this, we perform an initial redirect to ourselves\n      // using a form GET submit, in an attempt to verify if the browser saves\n      // cookies on redirect or not. If it does, we proceed as normal. If it\n      // doesn't, we redirect the user back to the client with an error message.\n      if (\n        // Only for iOS users\n        req.headers['user-agent']?.includes('iPhone OS') &&\n        // Disabled if the user already passed the test, which means their browser preserves cookies on redirect\n        !(getCookie(req, 'cookie-test') === 'succeeded') &&\n        // Disabled if the user already has a session\n        !(await server.deviceManager.hasSession(req))\n      ) {\n        // @TODO Another possible solution would be to avoid relying on cookies if we\n        // detect that they are not being preserved. This would mean that preserving\n        // sessions (SSO) would not be possible for browsers that don't preserve\n        // cookies on redirect, but at least the authorization request could still be\n        // completed. This was not implemented yet due to the extra complexity\n        // involved in supporting this.\n\n        // 1) When the user first comes here, we will test if their browser\n        // preserves cookies by redirecting back to ourselves\n        if (!this.url.searchParams.has('redirect-test')) {\n          // 2) Set a testing cookie\n          setCookie(res, 'cookie-test', 'testing', {\n            sameSite: 'lax',\n            httpOnly: true,\n          })\n\n          // 3) And send an auto-submit form redirecting back to ourselves\n          return writeFormRedirect(\n            res,\n            'get',\n            this.url.href,\n            // 4) We add an extra query parameter to trigger the test logic after\n            // the redirect occurred.\n            [...this.url.searchParams, ['redirect-test', '1']],\n            securityOptions,\n          )\n        } else {\n          // 5) We just got redirected back to ourselves. Verify that the\n          // browser preserved cookies during the redirect\n          if (getCookie(req, 'cookie-test')) {\n            // 6) Success! The browser preserved cookies. Proceed with the\n            // normal authorization flow.\n\n            // 7) Set a long lasting cookie to skip the test next time\n            setCookie(res, 'cookie-test', 'succeeded', {\n              sameSite: 'lax',\n              maxAge: 31 * 24 * 60 * 60,\n              httpOnly: true,\n            })\n          } else {\n            // The browser did NOT preserve cookies. We have to abort the\n            // authorization request.\n\n            if (this.url.searchParams.get('redirect-test') === '1') {\n              // 8) Show an error page to the user explaining the situation\n\n              // Give the browser another chance to save cookies after the use\n              // pressed \"Continue\"\n              setCookie(res, 'cookie-test', 'testing', {\n                sameSite: 'lax',\n                httpOnly: true,\n              })\n\n              // Make sure next time we reach the other branch and redirect back\n              // to the client\n              const continueUrl = new URL(this.url.href)\n              continueUrl.searchParams.set('redirect-test', '2')\n              return sendCookieErrorPage(req, res, { continueUrl })\n            } else {\n              // 9) Once the use acknowledges the error, redirect them back to\n              // the client with an error message.\n\n              // Allow the client to understand what happened (the `error`\n              // response parameter value is constrained by the OAuth2 spec)\n              const message = 'ERR_COOKIES_UNSUPPORTED'\n\n              // @NOTE AuthorizationError thrown here will be caught by the\n              // error handler middleware defined below, and cause a redirect\n              // back to the client with the error parameters.\n              if ('request_uri' in query) {\n                // Load and delete the authorization request\n                const requestUri = parseRequestUri(query.request_uri, {\n                  path: ['query', 'request_uri'],\n                })\n                const data = await server.requestManager.get(\n                  requestUri,\n                  undefined,\n                  query.client_id,\n                )\n                await server.requestManager.delete(requestUri)\n                throw new AuthorizationError(data.parameters, message)\n              } else if ('request' in query) {\n                const client = await server.clientManager.getClient(\n                  query.client_id,\n                )\n                const parameters = await server.decodeJAR(client, query)\n                throw new AuthorizationError(parameters, message)\n              } else {\n                throw new AuthorizationError(query, message)\n              }\n            }\n          }\n        }\n      }\n\n      // Normal authorization flow\n      const device = await server.deviceManager.load(req, res)\n\n      const result = await server.authorize(query, device)\n\n      if ('redirect' in result) {\n        return sendAuthorizationResultRedirect(res, result, securityOptions)\n      } else {\n        return sendAuthorizePage(req, res, result)\n      }\n    }),\n  )\n\n  // This is a private endpoint that will be called by the user after the\n  // authorization request was either approved or denied. The logic performed\n  // here **could** be performed directly in the frontend. We decided to\n  // implement it here to avoid duplicating the logic.\n  router.get(\n    '/oauth/authorize/redirect',\n    withErrorHandler(async function (req, res) {\n      // Ensure we come from the authorization page\n      validateFetchSite(req, ['same-origin'])\n      validateFetchMode(req, ['navigate'])\n      validateFetchDest(req, ['document'])\n      validateOrigin(req, issuerOrigin)\n\n      const referrer = validateReferrer(req, {\n        origin: issuerOrigin,\n        pathname: '/oauth/authorize',\n      })\n\n      // Ensure we are coming from the authorization page\n      requestUriSchema.parse(referrer.searchParams.get('request_uri'))\n\n      return sendRedirect(res, parseRedirectUrl(this.url), securityOptions)\n    }),\n  )\n\n  return router.buildMiddleware()\n\n  function withErrorHandler<T extends RouterCtx>(\n    handler: (this: T, req: Req, res: Res) => Awaitable<void>,\n  ): Middleware<T, Req, Res> {\n    return async function (req, res) {\n      try {\n        await handler.call(this, req, res)\n      } catch (err) {\n        onError?.(req, res, err, `Authorization Request Error`)\n\n        if (!res.headersSent) {\n          if (err instanceof AuthorizationError) {\n            return sendAuthorizationResultRedirect(\n              res,\n              {\n                issuer: server.issuer,\n                parameters: err.parameters,\n                redirect: err.toJSON(),\n              },\n              securityOptions,\n            )\n          } else {\n            return sendErrorPage(req, res, err)\n          }\n        } else if (!res.destroyed) {\n          res.end()\n        }\n      }\n    }\n  }\n}\n\nfunction parseOAuthAuthorizationRequestQuery(\n  url: URL,\n): OAuthAuthorizationRequestQuery {\n  const query = Object.fromEntries(url.searchParams)\n  const result = oauthAuthorizationRequestQuerySchema.safeParse(query, {\n    path: ['query'],\n  })\n\n  if (!result.success) {\n    const message = 'Invalid request parameters'\n    const err = result.error\n    throw new InvalidRequestError(formatError(err, message), err)\n  }\n\n  return result.data\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/create-oauth-middleware.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport {\n  oauthAuthorizationRequestParSchema,\n  oauthClientCredentialsSchema,\n  oauthTokenIdentificationSchema,\n  oauthTokenRequestSchema,\n} from '@atproto/oauth-types'\nimport { buildErrorPayload, buildErrorStatus } from '../errors/error-parser.js'\nimport { InvalidClientError } from '../errors/invalid-client-error.js'\nimport { InvalidGrantError } from '../errors/invalid-grant-error.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { WWWAuthenticateError } from '../errors/www-authenticate-error.js'\nimport {\n  Middleware,\n  Router,\n  cacheControlMiddleware,\n  combineMiddlewares,\n  jsonHandler,\n  parseHttpRequest,\n  staticJsonMiddleware,\n} from '../lib/http/index.js'\nimport { formatError } from '../lib/util/error.js'\nimport { OAuthError } from '../oauth-errors.js'\nimport type { OAuthProvider } from '../oauth-provider.js'\nimport type { MiddlewareOptions } from './middleware-options.js'\n\n// CORS preflight\nconst corsHeaders: Middleware = function (req, res, next) {\n  res.setHeader('Access-Control-Max-Age', '86400') // 1 day\n\n  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin\n  //\n  // > For requests without credentials, the literal value \"*\" can be\n  // > specified as a wildcard; the value tells browsers to allow\n  // > requesting code from any origin to access the resource.\n  // > Attempting to use the wildcard with credentials results in an\n  // > error.\n  //\n  // A \"*\" is safer to use than reflecting the request origin.\n  res.setHeader('Access-Control-Allow-Origin', '*')\n\n  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods\n  // > The value \"*\" only counts as a special wildcard value for\n  // > requests without credentials (requests without HTTP cookies or\n  // > HTTP authentication information). In requests with credentials,\n  // > it is treated as the literal method name \"*\" without special\n  // > semantics.\n  res.setHeader('Access-Control-Allow-Methods', '*')\n\n  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,DPoP')\n\n  next()\n}\n\nconst corsPreflight: Middleware = combineMiddlewares([\n  corsHeaders,\n  (req, res) => {\n    res.writeHead(200).end()\n  },\n])\n\nexport function createOAuthMiddleware<\n  Ctx extends object | void = void,\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n>(\n  server: OAuthProvider,\n  { onError }: MiddlewareOptions<Req, Res>,\n): Middleware<Ctx, Req, Res> {\n  const router = new Router<Ctx, Req, Res>(new URL(server.issuer))\n\n  //- Public OAuth endpoints\n\n  router.options('/.well-known/oauth-authorization-server', corsPreflight)\n  router.get(\n    '/.well-known/oauth-authorization-server',\n    corsHeaders,\n    cacheControlMiddleware(300),\n    staticJsonMiddleware(server.metadata),\n  )\n\n  router.options('/oauth/jwks', corsPreflight)\n  router.get(\n    '/oauth/jwks',\n    corsHeaders,\n    cacheControlMiddleware(300),\n    staticJsonMiddleware(server.jwks),\n  )\n\n  router.options('/oauth/par', corsPreflight)\n  router.post(\n    '/oauth/par',\n    corsHeaders,\n    oauthHandler(async function (req) {\n      const payload = await parseHttpRequest(req, ['json', 'urlencoded'])\n\n      // https://datatracker.ietf.org/doc/html/rfc9126#name-error-response\n      // https://datatracker.ietf.org/doc/html/rfc6749#autoid-56\n\n      const credentials = await oauthClientCredentialsSchema\n        .parseAsync(payload, { path: ['body'] })\n        .catch((err) => throwInvalidClient(err, 'Client credentials missing'))\n\n      const authorizationRequest = await oauthAuthorizationRequestParSchema\n        .parseAsync(payload, { path: ['body'] })\n        .catch((err) =>\n          throwInvalidRequest(err, 'Invalid authorization request'),\n        )\n\n      const dpopProof = await server.checkDpopProof(\n        req.method!,\n        this.url,\n        req.headers,\n      )\n\n      return server.pushedAuthorizationRequest(\n        credentials,\n        authorizationRequest,\n        dpopProof,\n      )\n    }, 201),\n  )\n  // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3\n  // > If the request did not use the POST method, the authorization server\n  // > responds with an HTTP 405 (Method Not Allowed) status code.\n  router.all('/oauth/par', (req, res) => {\n    res.writeHead(405).end()\n  })\n\n  router.options('/oauth/token', corsPreflight)\n  router.post(\n    '/oauth/token',\n    corsHeaders,\n    oauthHandler(async function (req) {\n      const payload = await parseHttpRequest(req, ['json', 'urlencoded'])\n\n      const clientMetadata = await server.deviceManager.getRequestMetadata(req)\n\n      const clientCredentials = await oauthClientCredentialsSchema\n        .parseAsync(payload, { path: ['body'] })\n        .catch((err) => throwInvalidGrant(err, 'Client credentials missing'))\n\n      const tokenRequest = await oauthTokenRequestSchema\n        .parseAsync(payload, { path: ['body'] })\n        .catch((err) => throwInvalidGrant(err, 'Invalid request payload'))\n\n      const dpopProof = await server.checkDpopProof(\n        req.method!,\n        this.url,\n        req.headers,\n      )\n\n      return server.token(\n        clientCredentials,\n        clientMetadata,\n        tokenRequest,\n        dpopProof,\n      )\n    }),\n  )\n\n  router.options('/oauth/revoke', corsPreflight)\n  router.post(\n    '/oauth/revoke',\n    corsHeaders,\n    oauthHandler(async function (req, res) {\n      const payload = await parseHttpRequest(req, ['json', 'urlencoded'])\n\n      const credentials = await oauthClientCredentialsSchema\n        .parseAsync(payload, { path: ['body'] })\n        .catch((err) => throwInvalidRequest(err, 'Client credentials missing'))\n\n      const tokenIdentification = await oauthTokenIdentificationSchema\n        .parseAsync(payload, { path: ['body'] })\n        .catch((err) => throwInvalidRequest(err, 'Invalid request payload'))\n\n      const dpopProof = await server.checkDpopProof(\n        req.method!,\n        this.url,\n        req.headers,\n      )\n\n      try {\n        await server.revoke(credentials, tokenIdentification, dpopProof)\n      } catch (err) {\n        // > Note: invalid tokens do not cause an error response since the\n        // > client cannot handle such an error in a reasonable way.  Moreover,\n        // > the purpose of the revocation request, invalidating the particular\n        // > token, is already achieved.\n        //\n        // https://datatracker.ietf.org/doc/html/rfc7009#section-2.2\n\n        onError?.(req, res, err, 'Failed to revoke token')\n      }\n\n      return {}\n    }),\n  )\n\n  return router.buildMiddleware()\n\n  function oauthHandler<T>(\n    buildOAuthResponse: (this: T, req: Req, res: Res) => unknown,\n    status?: number,\n  ): Middleware<T, Req, Res> {\n    return jsonHandler<T, Req, Res>(async function (req, res) {\n      try {\n        // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1\n        res.setHeader('Cache-Control', 'no-store')\n        res.setHeader('Pragma', 'no-cache')\n\n        // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2\n        const dpopNonce = server.nextDpopNonce()\n        if (dpopNonce) {\n          const name = 'DPoP-Nonce'\n          res.setHeader(name, dpopNonce)\n          res.appendHeader('Access-Control-Expose-Headers', name)\n        }\n\n        const json = await buildOAuthResponse.call(this, req, res)\n        return { json, status }\n      } catch (err) {\n        onError?.(\n          req,\n          res,\n          err,\n          err instanceof OAuthError\n            ? `OAuth \"${err.error}\" error`\n            : 'Unexpected error',\n        )\n\n        if (!res.headersSent && err instanceof WWWAuthenticateError) {\n          const name = 'WWW-Authenticate'\n          res.setHeader(name, err.wwwAuthenticateHeader)\n          res.appendHeader('Access-Control-Expose-Headers', name)\n        }\n\n        const status = buildErrorStatus(err)\n        const json = buildErrorPayload(err)\n\n        return { json, status }\n      }\n    })\n  }\n}\n\nfunction throwInvalidGrant(err: unknown, prefix: string): never {\n  throw new InvalidGrantError(formatError(err, prefix), err)\n}\n\nfunction throwInvalidClient(err: unknown, prefix: string): never {\n  throw new InvalidClientError(formatError(err, prefix), err)\n}\n\nfunction throwInvalidRequest(err: unknown, prefix: string): never {\n  throw new InvalidRequestError(formatError(err, prefix), err)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/error-handler.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport type ErrorHandler<\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n> = (req: Req, res: Res, err: unknown, message: string) => void\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/router/middleware-options.ts",
    "content": "import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { ErrorHandler } from './error-handler.js'\n\nexport type MiddlewareOptions<\n  Req extends IncomingMessage = IncomingMessage,\n  Res extends ServerResponse = ServerResponse,\n> = {\n  onError?: ErrorHandler<Req, Res>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/signer/access-token-payload.ts",
    "content": "import { z } from 'zod'\nimport { jwtPayloadSchema } from '@atproto/jwk'\nimport { clientIdSchema } from '../client/client-id.js'\nimport { subSchema } from '../oidc/sub.js'\nimport { tokenIdSchema } from '../token/token-id.js'\n\nexport const accessTokenPayloadSchema = jwtPayloadSchema\n  .partial()\n  .extend({\n    // Following are required\n    jti: tokenIdSchema,\n    sub: subSchema,\n    exp: z.number().int(),\n    iat: z.number().int(),\n    iss: z.string().min(1),\n\n    // @NOTE \"aud\", \"scope\", \"client_id\" are not required, as are stored in the\n    // DB in 'light' access token mode.\n\n    // Restrict type of following\n    client_id: clientIdSchema.optional(),\n  })\n  .passthrough()\n\nexport type AccessTokenPayload = z.infer<typeof accessTokenPayloadSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/signer/api-token-payload.ts",
    "content": "import { z } from 'zod'\nimport { jwtPayloadSchema } from '@atproto/jwk'\nimport { deviceIdSchema } from '../oauth-store.js'\nimport { subSchema } from '../oidc/sub.js'\nimport { requestUriSchema } from '../request/request-uri.js'\n\nexport const apiTokenPayloadSchema = jwtPayloadSchema\n  .extend({\n    sub: subSchema,\n\n    deviceId: deviceIdSchema,\n    // If the token is bound to a particular authorization request, it can only\n    // be used in the context of that request.\n    requestUri: requestUriSchema.optional(),\n  })\n  .passthrough()\n\nexport type ApiTokenPayload = z.infer<typeof apiTokenPayloadSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/signer/signer.ts",
    "content": "import {\n  JwtPayload,\n  JwtPayloadGetter,\n  JwtSignHeader,\n  Keyset,\n  SignedJwt,\n  VerifyOptions,\n} from '@atproto/jwk'\nimport { EPHEMERAL_SESSION_MAX_AGE } from '../constants.js'\nimport { dateToEpoch } from '../lib/util/date.js'\nimport { OmitKey, RequiredKey } from '../lib/util/type.js'\nimport {\n  AccessTokenPayload,\n  accessTokenPayloadSchema,\n} from './access-token-payload.js'\nimport { ApiTokenPayload, apiTokenPayloadSchema } from './api-token-payload.js'\n\nexport type SignPayload = JwtPayload & { iss?: never }\n\nexport { Keyset }\nexport type { JwtPayloadGetter, JwtSignHeader, SignedJwt, VerifyOptions }\n\nexport class Signer {\n  constructor(\n    public readonly issuer: string,\n    public readonly keyset: Keyset,\n  ) {}\n\n  async verify<C extends string = never>(\n    token: SignedJwt,\n    options?: Omit<VerifyOptions<C>, 'issuer'>,\n  ) {\n    return this.keyset.verifyJwt<C>(token, {\n      ...options,\n      issuer: [this.issuer],\n    })\n  }\n\n  public async sign(\n    signHeader: JwtSignHeader,\n    payload: SignPayload | JwtPayloadGetter<SignPayload>,\n  ): Promise<SignedJwt> {\n    return this.keyset.createJwt(signHeader, async (protectedHeader, key) => ({\n      ...(typeof payload === 'function'\n        ? await payload(protectedHeader, key)\n        : payload),\n      iss: this.issuer,\n    }))\n  }\n\n  async createAccessToken(\n    payload: OmitKey<AccessTokenPayload, 'iss'>,\n  ): Promise<SignedJwt> {\n    return this.sign(\n      {\n        // https://datatracker.ietf.org/doc/html/rfc9068#section-2.1\n        alg: undefined,\n        typ: 'at+jwt',\n      },\n      payload,\n    )\n  }\n\n  async verifyAccessToken<C extends string = never>(\n    token: SignedJwt,\n    options?: Omit<VerifyOptions<C>, 'issuer' | 'typ'>,\n  ) {\n    const result = await this.verify<C>(token, { ...options, typ: 'at+jwt' })\n    return {\n      protectedHeader: result.protectedHeader,\n      payload: accessTokenPayloadSchema.parse(result.payload) as RequiredKey<\n        AccessTokenPayload,\n        C\n      >,\n    }\n  }\n\n  async createEphemeralToken(\n    payload: OmitKey<ApiTokenPayload, 'iss' | 'aud' | 'iat'>,\n  ) {\n    return this.sign(\n      {\n        alg: undefined,\n        typ: 'at+jwt',\n      },\n      {\n        ...payload,\n        aud: `oauth-provider-api@${this.issuer}`,\n        iat: dateToEpoch(),\n      },\n    )\n  }\n\n  async verifyEphemeralToken<C extends string = never>(\n    token: SignedJwt,\n    options?: Omit<VerifyOptions<C>, 'issuer' | 'audience' | 'typ'>,\n  ) {\n    const result = await this.verify<C>(token, {\n      ...options,\n      maxTokenAge: options?.maxTokenAge ?? EPHEMERAL_SESSION_MAX_AGE / 1e3,\n      audience: `oauth-provider-api@${this.issuer}`,\n      typ: 'at+jwt',\n    })\n    return {\n      protectedHeader: result.protectedHeader,\n      payload: apiTokenPayloadSchema.parse(result.payload) as RequiredKey<\n        ApiTokenPayload,\n        C\n      >,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/token/refresh-token.ts",
    "content": "import { z } from 'zod'\nimport {\n  REFRESH_TOKEN_BYTES_LENGTH,\n  REFRESH_TOKEN_PREFIX,\n} from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const REFRESH_TOKEN_LENGTH =\n  REFRESH_TOKEN_PREFIX.length + REFRESH_TOKEN_BYTES_LENGTH * 2 // hex encoding\n\nexport const refreshTokenSchema = z\n  .string()\n  .length(REFRESH_TOKEN_LENGTH)\n  .refine(\n    (v): v is `${typeof REFRESH_TOKEN_PREFIX}${string}` =>\n      v.startsWith(REFRESH_TOKEN_PREFIX),\n    {\n      message: `Invalid refresh token format`,\n    },\n  )\n\nexport const isRefreshToken = (data: unknown): data is RefreshToken =>\n  refreshTokenSchema.safeParse(data).success\n\nexport type RefreshToken = z.infer<typeof refreshTokenSchema>\nexport const generateRefreshToken = async (): Promise<RefreshToken> => {\n  return `${REFRESH_TOKEN_PREFIX}${await randomHexId(\n    REFRESH_TOKEN_BYTES_LENGTH,\n  )}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/token/token-claims.ts",
    "content": "import { OAuthScope } from '@atproto/oauth-types'\nimport { ClientId } from '../client/client-id.js'\nimport { TokenId } from './token-id.js'\n\n/**\n * The access token claims that will be set by the {@link TokenManager} and that\n * will be passed to the \"onCreateToken\" hook.\n *\n * @note \"iss\" is missing here because it cannot be altered and will always be\n * set to the Authorization Server's identifier.\n */\nexport type TokenClaims = {\n  jti: TokenId\n  sub: string\n  iat: number\n  exp: number\n  aud: string | [string, ...string[]]\n  cnf?: { jkt: string }\n  scope?: OAuthScope\n  client_id: ClientId\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/token/token-data.ts",
    "content": "import {\n  OAuthAuthorizationDetails,\n  OAuthAuthorizationRequestParameters,\n} from '@atproto/oauth-types'\nimport { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'\nimport { ClientId } from '../client/client-id.js'\nimport { DeviceId } from '../device/device-id.js'\nimport { Sub } from '../oidc/sub.js'\nimport { Code } from '../request/code.js'\n\nexport type {\n  ClientAuth,\n  ClientId,\n  Code,\n  DeviceId,\n  OAuthAuthorizationDetails,\n  OAuthAuthorizationRequestParameters,\n  Sub,\n}\n\nexport type TokenData = {\n  createdAt: Date\n  updatedAt: Date\n  expiresAt: Date\n  clientId: ClientId\n  clientAuth: ClientAuth | ClientAuthLegacy\n  deviceId: DeviceId | null\n  sub: Sub\n  parameters: OAuthAuthorizationRequestParameters\n  details?: null // Legacy field, not used\n  code: Code | null\n\n  /**\n   * This will contain the parameter scope, translated into permissions\n   *\n   * @note null because this didn't use to exist. New tokens should always\n   * include a scope.\n   */\n  scope: string | null\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/token/token-id.ts",
    "content": "import { z } from 'zod'\nimport { TOKEN_ID_BYTES_LENGTH, TOKEN_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const TOKEN_ID_LENGTH =\n  TOKEN_ID_PREFIX.length + TOKEN_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const tokenIdSchema = z\n  .string()\n  .length(TOKEN_ID_LENGTH)\n  .refine(\n    (v): v is `${typeof TOKEN_ID_PREFIX}${string}` =>\n      v.startsWith(TOKEN_ID_PREFIX),\n    {\n      message: `Invalid token ID format`,\n    },\n  )\n\nexport type TokenId = z.infer<typeof tokenIdSchema>\nexport const generateTokenId = async (): Promise<TokenId> => {\n  return `${TOKEN_ID_PREFIX}${await randomHexId(TOKEN_ID_BYTES_LENGTH)}`\n}\n\nexport const isTokenId = (data: unknown): data is TokenId =>\n  tokenIdSchema.safeParse(data).success\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/token/token-manager.ts",
    "content": "import { SignedJwt, isSignedJwt } from '@atproto/jwk'\nimport { LexResolverError } from '@atproto/lex-resolver'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport {\n  OAuthAccessToken,\n  OAuthAuthorizationRequestParameters,\n  OAuthScope,\n  OAuthTokenResponse,\n  OAuthTokenType,\n} from '@atproto/oauth-types'\nimport { AccessTokenMode } from '../access-token/access-token-mode.js'\nimport { ClientAuth } from '../client/client-auth.js'\nimport { Client } from '../client/client.js'\nimport { TOKEN_MAX_AGE } from '../constants.js'\nimport { DeviceId } from '../device/device-id.js'\nimport { InvalidGrantError } from '../errors/invalid-grant-error.js'\nimport { InvalidRequestError } from '../errors/invalid-request-error.js'\nimport { InvalidTokenError } from '../errors/invalid-token-error.js'\nimport { LexiconManager } from '../lexicon/lexicon-manager.js'\nimport { RequestMetadata } from '../lib/http/request.js'\nimport { dateToEpoch, dateToRelativeSeconds } from '../lib/util/date.js'\nimport { OAuthHooks } from '../oauth-hooks.js'\nimport { Sub } from '../oidc/sub.js'\nimport { Code, isCode } from '../request/code.js'\nimport { AccessTokenPayload } from '../signer/access-token-payload.js'\nimport { Signer } from '../signer/signer.js'\nimport {\n  RefreshToken,\n  generateRefreshToken,\n  isRefreshToken,\n} from './refresh-token.js'\nimport { TokenClaims } from './token-claims.js'\nimport { TokenId, generateTokenId, isTokenId } from './token-id.js'\nimport { CreateTokenData, TokenInfo, TokenStore } from './token-store.js'\n\nexport { AccessTokenMode, Signer }\nexport type { OAuthHooks, TokenStore }\n\nexport class TokenManager {\n  constructor(\n    protected readonly store: TokenStore,\n    protected readonly lexiconManager: LexiconManager,\n    protected readonly signer: Signer,\n    protected readonly hooks: OAuthHooks,\n    protected readonly accessTokenMode: AccessTokenMode,\n    protected readonly tokenMaxAge = TOKEN_MAX_AGE,\n  ) {}\n\n  protected createTokenExpiry(now = new Date()) {\n    return new Date(now.getTime() + this.tokenMaxAge)\n  }\n\n  protected async createAccessToken(\n    tokenId: TokenId,\n    client: Client,\n    account: Account,\n    parameters: OAuthAuthorizationRequestParameters,\n    issuedAt: Date,\n    expiresAt: Date,\n    scope: OAuthScope,\n  ): Promise<OAuthAccessToken> {\n    const claims: TokenClaims = {\n      jti: tokenId,\n      sub: account.sub,\n      iat: dateToEpoch(issuedAt),\n      exp: dateToEpoch(expiresAt),\n      aud: account.aud,\n\n      ...(parameters.dpop_jkt && {\n        cnf: { jkt: parameters.dpop_jkt },\n      }),\n\n      // Because tokens can end-up being quite big, we only include the scope in\n      // stateless mode.\n      ...(this.accessTokenMode === AccessTokenMode.stateless && {\n        scope,\n      }),\n\n      // https://datatracker.ietf.org/doc/html/rfc8693#section-4.3\n      client_id: client.id,\n    }\n\n    const claimsOverride = await this.hooks.onCreateToken?.call(null, {\n      client,\n      account,\n      parameters,\n      claims,\n    })\n\n    return this.signer.createAccessToken(claimsOverride ?? claims)\n  }\n\n  async createToken(\n    client: Client,\n    clientAuth: ClientAuth,\n    clientMetadata: RequestMetadata,\n    account: Account,\n    deviceId: null | DeviceId,\n    parameters: OAuthAuthorizationRequestParameters,\n    code: Code,\n  ): Promise<OAuthTokenResponse> {\n    await this.validateTokenParams(client, clientAuth, parameters)\n\n    const tokenId = await generateTokenId()\n    const refreshToken = client.metadata.grant_types.includes('refresh_token')\n      ? await generateRefreshToken()\n      : undefined\n\n    const now = new Date()\n    const expiresAt = this.createTokenExpiry(now)\n\n    const scope = await this.lexiconManager\n      .buildTokenScope(parameters.scope!)\n      .catch((err) => {\n        // Parse expected errors\n        if (err instanceof LexResolverError) {\n          throw new InvalidRequestError(err.message, err)\n        }\n\n        // Unexpected error\n        throw err\n      })\n\n    const accessToken = await this.createAccessToken(\n      tokenId,\n      client,\n      account,\n      parameters,\n      now,\n      expiresAt,\n      scope,\n    )\n\n    const response = this.buildTokenResponse(\n      inferTokenType(parameters),\n      accessToken,\n      refreshToken,\n      expiresAt,\n      account.sub,\n      scope,\n    )\n\n    const tokenData: CreateTokenData = {\n      createdAt: now,\n      updatedAt: now,\n      expiresAt,\n      clientId: client.id,\n      clientAuth,\n      deviceId,\n      sub: account.sub,\n      parameters,\n      details: null,\n      scope,\n      code,\n    }\n\n    await this.store.createToken(tokenId, tokenData, refreshToken)\n\n    try {\n      await this.hooks.onTokenCreated?.call(null, {\n        client,\n        clientAuth,\n        clientMetadata,\n        account,\n        parameters,\n      })\n\n      return response\n    } catch (err) {\n      // If the hook fails, we delete the token to avoid leaving a dangling\n      // token in the store.\n      await this.deleteToken(tokenId)\n      throw err\n    }\n  }\n\n  protected async validateTokenParams(\n    client: Client,\n    clientAuth: ClientAuth,\n    parameters: OAuthAuthorizationRequestParameters,\n  ): Promise<void> {\n    if (client.metadata.dpop_bound_access_tokens && !parameters.dpop_jkt) {\n      throw new InvalidGrantError(\n        `DPoP JKT is required for DPoP bound access tokens`,\n      )\n    }\n  }\n\n  protected buildTokenResponse(\n    tokenType: OAuthTokenType,\n    accessToken: OAuthAccessToken,\n    refreshToken: string | undefined,\n    expiresAt: Date,\n    sub: Sub,\n    scope: string,\n  ): OAuthTokenResponse {\n    return {\n      access_token: accessToken,\n      token_type: tokenType,\n      refresh_token: refreshToken,\n      scope,\n\n      // @NOTE using a getter so that the value gets computed when the JSON\n      // response is generated, allowing to value to be as accurate as possible.\n      get expires_in() {\n        return dateToRelativeSeconds(expiresAt)\n      },\n\n      // ATPROTO extension: add the sub claim to the token response to allow\n      // clients to resolve the PDS url (audience) using the did resolution\n      // mechanism.\n      sub,\n    }\n  }\n\n  async rotateToken(\n    client: Client,\n    clientAuth: ClientAuth,\n    clientMetadata: RequestMetadata,\n    tokenInfo: TokenInfo,\n  ): Promise<OAuthTokenResponse> {\n    const { account, data } = tokenInfo\n    const { parameters } = data\n\n    await this.validateTokenParams(client, clientAuth, parameters)\n\n    const nextTokenId = await generateTokenId()\n    const nextRefreshToken = await generateRefreshToken()\n\n    const now = new Date()\n    const expiresAt = this.createTokenExpiry(now)\n\n    // @NOTE since the permission sets are stored in a persistent store,\n    // it's fine to propagate a 500 (server_error) here as the values should\n    // be retrievable from the store.\n    const scope = await this.lexiconManager.buildTokenScope(parameters.scope!)\n\n    await this.store.rotateToken(tokenInfo.id, nextTokenId, nextRefreshToken, {\n      updatedAt: now,\n      expiresAt,\n      // @NOTE Normally, the clientAuth not change over time. There are two\n      // exceptions:\n      // - Upgrade from a legacy representation of client authentication to\n      //   a modern one.\n      // - Allow clients to become \"confidential\" if they were previously\n      //   \"public\"\n      clientAuth,\n      scope,\n    })\n\n    const accessToken = await this.createAccessToken(\n      nextTokenId,\n      client,\n      account,\n      parameters,\n      now,\n      expiresAt,\n      scope,\n    )\n\n    const response = this.buildTokenResponse(\n      inferTokenType(parameters),\n      accessToken,\n      nextRefreshToken,\n      expiresAt,\n      account.sub,\n      scope,\n    )\n\n    await this.hooks.onTokenRefreshed?.call(null, {\n      client,\n      clientAuth,\n      clientMetadata,\n      account,\n      parameters,\n    })\n\n    return response\n  }\n\n  /**\n   * @note The token validity is not guaranteed. The caller must ensure that the\n   * token is valid before using the returned token info.\n   */\n  public async findToken(token: string): Promise<null | TokenInfo> {\n    if (isTokenId(token)) {\n      return this.getTokenInfo(token)\n    } else if (isCode(token)) {\n      return this.findByCode(token)\n    } else if (isRefreshToken(token)) {\n      return this.findByRefreshToken(token)\n    } else if (isSignedJwt(token)) {\n      return this.findByAccessToken(token)\n    } else {\n      throw new InvalidRequestError(`Invalid token`)\n    }\n  }\n\n  public async findByAccessToken(token: SignedJwt): Promise<null | TokenInfo> {\n    const { payload } = await this.signer.verifyAccessToken(token, {\n      clockTolerance: Infinity,\n    })\n\n    const tokenInfo = await this.getTokenInfo(payload.jti)\n    if (!tokenInfo) return null\n\n    // Fool-proof: Invalid store implementation ?\n    if (payload.sub !== tokenInfo.account.sub) {\n      await this.deleteToken(tokenInfo.id)\n      throw new Error(\n        `Account sub (${tokenInfo.account.sub}) does not match token sub (${payload.sub})`,\n      )\n    }\n\n    return tokenInfo\n  }\n\n  protected async findByRefreshToken(\n    token: RefreshToken,\n  ): Promise<null | TokenInfo> {\n    return this.store.findTokenByRefreshToken(token)\n  }\n\n  public async consumeRefreshToken(token: RefreshToken): Promise<TokenInfo> {\n    // @NOTE concurrent refreshes of the same refresh token could theoretically\n    // lead to two new tokens (access & refresh) being created. This is deemed\n    // acceptable for now (as the mechanism can only be used once since only one\n    // of the two refresh token created will be valid, and any future refresh\n    // attempts from outdated tokens will cause the entire session to be\n    // invalidated). Ideally, the store should be able to handle this case by\n    // atomically consuming the refresh token and returning the token info.\n\n    // @TODO Add another store method that atomically consumes the refresh token\n    // with a lock.\n    const tokenInfo = await this.findByRefreshToken(token).catch((err) => {\n      throw InvalidGrantError.from(err, `Invalid refresh token`)\n    })\n\n    if (!tokenInfo) {\n      throw new InvalidGrantError(`Invalid refresh token`)\n    }\n\n    if (tokenInfo.currentRefreshToken !== token) {\n      await this.deleteToken(tokenInfo.id)\n      throw new InvalidGrantError(`Refresh token replayed`)\n    }\n\n    return tokenInfo\n  }\n\n  public async findByCode(code: Code): Promise<null | TokenInfo> {\n    return this.store.findTokenByCode(code)\n  }\n\n  public async deleteToken(tokenId: TokenId): Promise<void> {\n    return this.store.deleteToken(tokenId)\n  }\n\n  async getTokenInfo(tokenId: TokenId): Promise<null | TokenInfo> {\n    return this.store.readToken(tokenId)\n  }\n\n  /**\n   * This method is called to when decoding a token that was encoded in\n   * {@link AccessTokenMode.light} mode, using data from the store to fill the\n   * data that was omitted in the token itself.\n   */\n  async loadTokenClaims(\n    tokenType: OAuthTokenType,\n    tokenPayload: AccessTokenPayload,\n  ): Promise<TokenClaims> {\n    const tokenId = tokenPayload.jti\n    const tokenInfo = await this.getTokenInfo(tokenId).catch((err) => {\n      throw InvalidTokenError.from(err, tokenType)\n    })\n\n    if (!tokenInfo) {\n      throw new InvalidTokenError(tokenType, `Invalid token`)\n    }\n\n    const { account, data } = tokenInfo\n\n    // Fool proof, make sure that the database & token payload are consistent.\n    // These should both be either undefined or a string so it's safe to compare\n    // the values directly.\n    if (tokenPayload.cnf?.jkt !== data.parameters.dpop_jkt) {\n      await this.deleteToken(tokenId)\n      throw new InvalidTokenError(tokenType, `Invalid token`)\n    }\n\n    if (isCurrentTokenExpired(tokenInfo)) {\n      await this.deleteToken(tokenId)\n      throw new InvalidTokenError(tokenType, `Token expired`)\n    }\n\n    return {\n      jti: tokenId,\n      sub: account.sub,\n      iat: dateToEpoch(data.updatedAt),\n      exp: dateToEpoch(data.expiresAt),\n      aud: account.aud,\n      scope: data.scope ?? data.parameters.scope,\n      // https://datatracker.ietf.org/doc/html/rfc8693#section-4.3\n      client_id: data.clientId,\n    }\n  }\n\n  async listAccountTokens(sub: Sub): Promise<TokenInfo[]> {\n    const results = await this.store.listAccountTokens(sub)\n    return results\n      .filter((tokenInfo) => tokenInfo.account.sub === sub) // Fool proof\n      .filter((tokenInfo) => !isCurrentTokenExpired(tokenInfo))\n  }\n}\n\nfunction isCurrentTokenExpired(tokenInfo: TokenInfo): boolean {\n  return tokenInfo.data.expiresAt.getTime() < Date.now()\n}\n\nfunction inferTokenType(\n  parameters: OAuthAuthorizationRequestParameters,\n): OAuthTokenType {\n  if (parameters.dpop_jkt) {\n    return 'DPoP'\n  }\n  return 'Bearer'\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/token/token-store.ts",
    "content": "import type { Account } from '@atproto/oauth-provider-api'\nimport { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { Sub } from '../oidc/sub.js'\nimport { Code } from '../request/code.js'\nimport { RefreshToken } from './refresh-token.js'\nimport { TokenData } from './token-data.js'\nimport { TokenId } from './token-id.js'\n\n// Export all types needed to implement the TokenStore interface\nexport * from './refresh-token.js'\nexport * from './token-data.js'\nexport * from './token-id.js'\nexport type { Account, Awaitable, Sub }\n\nexport type TokenInfo = {\n  id: TokenId\n  data: TokenData\n  account: Account\n  currentRefreshToken: null | RefreshToken\n}\n\nexport type NewTokenData = {\n  clientAuth: TokenData['clientAuth']\n  expiresAt: TokenData['expiresAt']\n  updatedAt: TokenData['updatedAt']\n  scope: NonNullable<TokenData['scope']>\n}\n\nexport type CreateTokenData = TokenData & {\n  scope: NonNullable<TokenData['scope']>\n}\n\n/**\n * @param data historically, {@link TokenData.scope} was not present in\n * {@link TokenData}, causing it to be \"nullable\" when returned from\n * {@link TokenStore.readToken}. We use {@link CreateTokenData} here to allow\n * the store implementation to expect its presence.\n */\nexport interface TokenStore {\n  createToken(\n    tokenId: TokenId,\n    data: CreateTokenData,\n    refreshToken?: RefreshToken,\n  ): Awaitable<void>\n\n  readToken(tokenId: TokenId): Awaitable<null | TokenInfo>\n\n  deleteToken(tokenId: TokenId): Awaitable<void>\n\n  rotateToken(\n    tokenId: TokenId,\n    newTokenId: TokenId,\n    newRefreshToken: RefreshToken,\n    newData: NewTokenData,\n  ): Awaitable<void>\n\n  /**\n   * Find a token by its refresh token. Note that previous refresh tokens\n   * should also return the token. The data model is responsible for storing\n   * old refresh tokens when a new one is issued.\n   */\n  findTokenByRefreshToken(\n    refreshToken: RefreshToken,\n  ): Awaitable<null | TokenInfo>\n\n  findTokenByCode(code: Code): Awaitable<null | TokenInfo>\n\n  listAccountTokens(sub: Sub): Awaitable<TokenInfo[]>\n}\n\nexport const isTokenStore = buildInterfaceChecker<TokenStore>([\n  'createToken',\n  'readToken',\n  'deleteToken',\n  'rotateToken',\n  'findTokenByRefreshToken',\n  'findTokenByCode',\n  'listAccountTokens',\n])\n\nexport function asTokenStore<V extends Partial<TokenStore>>(\n  implementation?: V,\n): V & TokenStore {\n  if (!implementation || !isTokenStore(implementation)) {\n    throw new Error('Invalid TokenStore implementation')\n  }\n  return implementation\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/authorization-response-error.ts",
    "content": "import { z } from 'zod'\nimport {\n  oauthAuthorizationResponseErrorSchema,\n  oidcAuthorizationResponseErrorSchema,\n} from '@atproto/oauth-types'\n\nexport const authorizationResponseErrorSchema = z.union([\n  oauthAuthorizationResponseErrorSchema,\n  // OIDC authentication error response are not part of the ATproto flavoured\n  // OAuth but we allow them because they provide better feedback to the client\n  // (in particular when SSO is used).\n  oidcAuthorizationResponseErrorSchema,\n  // This error is defined by rfc9396 (not part of the OAuth 2.1 or OIDC). But\n  // since, in ATproto flavoured OAuth, client registration is a dynamic part of\n  // the authorization process, we allow it.\n  z.literal('invalid_authorization_details'),\n])\n\nexport type AuthorizationResponseError = z.infer<\n  typeof authorizationResponseErrorSchema\n>\n\nexport function isAuthorizationResponseError<T>(\n  value: T,\n): value is T & AuthorizationResponseError {\n  return authorizationResponseErrorSchema.safeParse(value).success\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/color-hue.ts",
    "content": "import { z } from 'zod'\n\nexport const colorHueSchema = z.number().min(0).max(360)\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/email-otp.ts",
    "content": "import { z } from 'zod'\n\nexport const emailOtpSchema = z.string().min(1)\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/email.ts",
    "content": "import { isEmailValid } from '@hapi/address'\nimport { isDisposableEmail } from 'disposable-email-domains-js'\nimport { z } from 'zod'\n\nexport const emailSchema = z\n  .string()\n  .email()\n  // @NOTE Internally, `zod` uses a regexp for validating emails.. This\n  // validation strategy *could* be less permissive in some (edge) cases than\n  // `@hapi/address` as the latter uses an algorithm based on the spec. Truth\n  // is, it is kinda hard to know if the set of emails allowed by\n  // `@hapi/address` is covered by the set of emails allowed by `zod`.\n  // Additionally, this could change with future changes in either libraries.\n  //\n  // Because of this uncertainty, and because other part of the Bluesky/ATProto\n  // codebases rely solely on `zod`, this code only allows emails that are valid\n  // according to both libraries ensuring that we never encounter a case where\n  // an email allowed here is in a format that would be rejected by other parts\n  // of our systems.\n  .refine(isEmailValid, {\n    message: 'Invalid email address',\n  })\n  .refine((email) => !isDisposableEmail(email), {\n    message: 'Disposable email addresses are not allowed',\n  })\n  .transform((value) => value.toLowerCase())\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/handle.ts",
    "content": "import { z } from 'zod'\nimport { ensureValidHandle, normalizeHandle } from '@atproto/syntax'\n\nexport const handleSchema = z\n  .string()\n  // @NOTE: We only check against validity towards ATProto's syntax. Additional\n  // rules may be imposed by the store implementation.\n  .superRefine((value, ctx) => {\n    try {\n      ensureValidHandle(value)\n    } catch (err) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: err instanceof Error ? err.message : 'Invalid handle',\n      })\n    }\n  })\n  .transform(normalizeHandle)\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/invite-code.ts",
    "content": "import { z } from 'zod'\n\nexport const inviteCodeSchema = z.string().min(1)\nexport type InviteCode = z.infer<typeof inviteCodeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/par-response-error.ts",
    "content": "import { z } from 'zod'\nimport { authorizationResponseErrorSchema } from './authorization-response-error.js'\n\n// https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1\n// > Since initial processing of the pushed authorization request does not\n// > involve resource owner interaction, error codes related to user\n// > interaction, such as \"access_denied\", are never returned.\n\nexport const parResponseErrorSchema = z.intersection(\n  authorizationResponseErrorSchema,\n  z.enum([\n    'invalid_request',\n    'unauthorized_client',\n    'unsupported_response_type',\n    'invalid_scope',\n    'server_error',\n    'temporarily_unavailable',\n  ]),\n)\n\nexport type PARResponseError = z.infer<typeof parResponseErrorSchema>\n\nexport function isPARResponseError<T>(value: T): value is T & PARResponseError {\n  return parResponseErrorSchema.safeParse(value).success\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/password.ts",
    "content": "import { z } from 'zod'\n\nexport const oldPasswordSchema = z.string().min(1).max(512)\nexport const newPasswordSchema = z.string().min(8).max(256)\n"
  },
  {
    "path": "packages/oauth/oauth-provider/src/types/rgb-color.ts",
    "content": "import { z } from 'zod'\nimport { RgbColor, parseColor } from '../lib/util/color.js'\n\nexport const rgbColorSchema = z.string().transform((value, ctx): RgbColor => {\n  try {\n    const parsed = parseColor(value)\n    if ('a' in parsed && parsed.a !== undefined) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Alpha values are not supported',\n      })\n    }\n    return parsed\n  } catch (e) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      message: e instanceof Error ? e.message : 'Invalid color value',\n    })\n    return z.NEVER\n  }\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/nodenext.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/CHANGELOG.md",
    "content": "# @atproto/oauth-provider-api\n\n## 0.3.7\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-types@0.6.2\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-types@0.6.1\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`95ef3c2`](https://github.com/bluesky-social/atproto/commit/95ef3c24e8072e9d49412950b033cb8607764ee0), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41)]:\n  - @atproto/oauth-types@0.6.0\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-types@0.5.2\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-types@0.5.1\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58)]:\n  - @atproto/oauth-types@0.5.0\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/oauth-types@0.4.2\n  - @atproto/jwk@0.6.0\n\n## 0.3.0\n\n### Minor Changes\n\n- [`f4cb3e4d0`](https://github.com/bluesky-social/atproto/commit/f4cb3e4d0ac45e567fa14f79b99a84621fa89a56) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Adapt to UI to support permission set.\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n  - @atproto/oauth-types@0.4.1\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Adapt `AuthorizeData` backend injected data type\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a)]:\n  - @atproto/oauth-types@0.4.0\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto/jwk@0.4.0\n  - @atproto/oauth-types@0.3.1\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/oauth-types@0.3.0\n  - @atproto/jwk@0.3.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05), [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c)]:\n  - @atproto/jwk@0.2.0\n  - @atproto/oauth-types@0.2.8\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8)]:\n  - @atproto/oauth-types@0.2.7\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a), [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a)]:\n  - @atproto/oauth-types@0.2.6\n\n## 0.1.0\n\n### Minor Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Various adaptations\n\n### Patch Changes\n\n- Updated dependencies [[`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/oauth-types@0.2.5\n  - @atproto/jwk@0.1.5\n\n## 0.0.1\n\n### Patch Changes\n\n- [#3640](https://github.com/bluesky-social/atproto/pull/3640) [`cc4122652`](https://github.com/bluesky-social/atproto/commit/cc4122652ed42ba55826c019d0ec57bf25df1ecd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Exctracted types shared between OAuthProvider backand and it's UI into a separate package.\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-provider-api\",\n  \"version\": \"0.3.7\",\n  \"license\": \"MIT\",\n  \"description\": \"Shared data types for the @atproto/oauth-provider and @atproto/oauth-provider-ui packages\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"provider\",\n    \"types\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-provider-api\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/jwk\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/src/api-endpoints.ts",
    "content": "import type { SignedJwt } from '@atproto/jwk'\nimport type { OAuthClientMetadata } from '@atproto/oauth-types'\nimport type { Account, DeviceMetadata, ISODateString } from './types.js'\n\n// These are the endpoints implemented by the OAuth provider, for its UI to\n// call.\n\nexport type ApiEndpoints = {\n  '/verify-handle-availability': {\n    method: 'POST'\n    input: VerifyHandleAvailabilityInput\n    output: { available: true }\n  }\n  '/sign-up': {\n    method: 'POST'\n    input: SignUpInput\n    output: SignUpOutput\n  }\n  '/sign-in': {\n    method: 'POST'\n    input: SignInInput\n    output: SignInOutput\n  }\n  '/reset-password-request': {\n    method: 'POST'\n    input: InitiatePasswordResetInput\n    output: { success: true }\n  }\n  '/reset-password-confirm': {\n    method: 'POST'\n    input: ConfirmResetPasswordInput\n    output: { success: true }\n  }\n  '/sign-out': {\n    method: 'POST'\n    input: SignOutInput\n    output: { success: true }\n  }\n  /**\n   * Lists all the accounts that are currently active, on the current device.\n   */\n  '/device-sessions': {\n    method: 'GET'\n    output: ActiveDeviceSession[]\n  }\n  /**\n   * Lists all the active OAuth sessions (access/refresh tokens) that where\n   * issued to OAuth clients (apps).\n   *\n   * @NOTE can be revoked using the oauth revocation endpoint (json or form\n   * encoded)\n   *\n   * ```http\n   * POST /oauth/revoke\n   * Content-Type: application/x-www-form-urlencoded\n   *\n   * token=<tokenId>\n   * ```\n   */\n  '/oauth-sessions': {\n    method: 'GET'\n    params: { sub: string }\n    output: ActiveOAuthSession[]\n  }\n  '/revoke-oauth-session': {\n    method: 'POST'\n    input: RevokeOAuthSessionInput\n    output: { success: true }\n  }\n  /**\n   * Lists all the sessions that are currently active for a particular user, on\n   * other devices.\n   */\n  '/account-sessions': {\n    method: 'GET'\n    params: { sub: string }\n    output: ActiveAccountSession[]\n  }\n  '/revoke-account-session': {\n    method: 'POST'\n    input: RevokeAccountSessionInput\n    output: { success: true }\n  }\n  '/consent': {\n    method: 'POST'\n    input: ConsentInput\n    output: { url: string }\n  }\n  '/reject': {\n    method: 'POST'\n    input: RejectInput\n    output: { url: string }\n  }\n}\n\n/**\n * When a user signs in without the \"remember me\" option, the server returns an\n * ephemeral token. When used as `Bearer` authorization header, the token will\n * be used in order to authenticate the users in place of using the user's\n * cookie based session (which are only created when \"remember me\" is checked).\n *\n * Only include this token in the `Authorization` header when making requests to\n * the OAuth provider API, **FOR THE ACCOUNT IT WAS GENERATED FOR**.\n */\nexport type EphemeralToken = SignedJwt\n\nexport type SignInInput = {\n  locale: string\n  username: string\n  password: string\n  emailOtp?: string\n  remember?: boolean\n}\n\nexport type SignInOutput = {\n  account: Account\n  ephemeralToken?: EphemeralToken\n  consentRequired?: boolean\n}\n\nexport type SignUpInput = {\n  locale: string\n  handle: string\n  email: string\n  password: string\n  inviteCode?: string\n  hcaptchaToken?: string\n}\n\nexport type SignUpOutput = {\n  account: Account\n  ephemeralToken?: EphemeralToken\n}\n\nexport type SignOutInput = {\n  sub: string | string[]\n}\n\nexport type InitiatePasswordResetInput = {\n  locale: string\n  email: string\n}\n\nexport type ConfirmResetPasswordInput = {\n  token: string\n  password: string\n}\n\nexport type VerifyHandleAvailabilityInput = {\n  handle: string\n}\n\nexport type RevokeAccountSessionInput = {\n  sub: string\n  deviceId: string\n}\n\nexport type RevokeOAuthSessionInput = {\n  sub: string\n  tokenId: string\n}\n\nexport type ConsentInput = {\n  sub: string\n  scope?: string\n}\n\nexport type RejectInput = Record<string, never>\n\n/**\n * Represents an account that is currently signed-in to the Authorization\n * Server. If the session was created too long ago, the user may be required to\n * re-authenticate ({@link ActiveDeviceSession.loginRequired}).\n */\nexport type ActiveDeviceSession = {\n  account: Account\n\n  /**\n   * The session is too old and the user must re-authenticate.\n   */\n  loginRequired: boolean\n}\n\n/**\n * Represents another device on which an account is currently signed-in.\n */\nexport type ActiveAccountSession = {\n  deviceId: string\n  deviceMetadata: DeviceMetadata\n\n  isCurrentDevice: boolean\n}\n\n/**\n * Represents an active OAuth session (access token).\n */\nexport type ActiveOAuthSession = {\n  tokenId: string\n\n  createdAt: ISODateString\n  updatedAt: ISODateString\n\n  clientId: string\n  /** An \"undefined\" value means that the client metadata could not be fetched */\n  clientMetadata?: OAuthClientMetadata\n\n  scope?: string\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/src/contants.ts",
    "content": "export const CSRF_COOKIE_NAME = 'csrf-token'\nexport const CSRF_HEADER_NAME = 'x-csrf-token'\n\nexport const API_ENDPOINT_PREFIX = '/@atproto/oauth-provider/~api'\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/src/customization-data.ts",
    "content": "import type { LinkDefinition } from './types.js'\n\n// These are the types of the variables that are injected into the HTML by the\n// backend. They are used to configure the frontend.\n\nexport type CustomizationData = {\n  // Functional customization\n  hcaptchaSiteKey?: string\n  inviteCodeRequired?: boolean\n  availableUserDomains?: string[]\n\n  // Aesthetic customization\n  name?: string\n  logo?: string\n  links?: LinkDefinition[]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/src/index.ts",
    "content": "export type * from './api-endpoints.js'\nexport type * from './customization-data.js'\nexport type * from './types.js'\n\nexport * from './contants.js'\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/src/types.ts",
    "content": "// @TODO replace with OidcUserinfo\nexport type Account = {\n  sub: string\n  aud: string | [string, ...string[]]\n\n  email?: string\n  email_verified?: boolean\n  name?: string\n  preferred_username?: string\n  picture?: string\n}\n\nexport type Session = {\n  account: Account\n  info?: never // Prevent relying on this in the frontend\n\n  selected: boolean\n  loginRequired: boolean\n  consentRequired: boolean\n}\n\nexport type MultiLangString = Record<string, string | undefined>\n\nexport type LinkDefinition = {\n  title: string | MultiLangString\n  href: string\n  rel?: string\n}\n\nexport type DeviceMetadata = {\n  userAgent: string | null\n  ipAddress: string\n  lastSeenAt: ISODateString\n}\n\nexport type ISODateString = `${string}T${string}Z`\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-api/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/.gitignore",
    "content": "src/locales/*/*.ts\n.swc\ndist\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/.linguirc",
    "content": "{\n  \"format\": \"po\",\n  \"sourceLocale\": \"en\",\n  \"locales\": [\n    \"en\",\n    \"an\",\n    \"ast\",\n    \"ca\",\n    \"da\",\n    \"de\",\n    \"el\",\n    \"en-GB\",\n    \"es\",\n    \"eu\",\n    \"fi\",\n    \"fr\",\n    \"ga\",\n    \"gl\",\n    \"hi\",\n    \"hu\",\n    \"ia\",\n    \"id\",\n    \"it\",\n    \"ja\",\n    \"km\",\n    \"ko\",\n    \"ne\",\n    \"nl\",\n    \"pl\",\n    \"pt-BR\",\n    \"ro\",\n    \"ru\",\n    \"sv\",\n    \"th\",\n    \"tr\",\n    \"uk\",\n    \"vi\",\n    \"zh-CN\",\n    \"zh-HK\",\n    \"zh-TW\"\n  ],\n  \"fallbackLocales\": {\n    \"default\": \"en\"\n  },\n  \"catalogs\": [\n    {\n      \"path\": \"<rootDir>/src/locales/{locale}/messages\",\n      \"include\": [\n        \"<rootDir>/src\"\n      ],\n      \"exclude\": [\n        \"**/dist/**\",\n        \"**/node_modules/**\"\n      ]\n    }\n  ],\n  \"compileNamespace\": \"ts\"\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/CHANGELOG.md",
    "content": "# @atproto/oauth-provider-frontend\n\n## 0.2.9\n\n### Patch Changes\n\n- [#4619](https://github.com/bluesky-social/atproto/pull/4619) [`a2e4e95`](https://github.com/bluesky-social/atproto/commit/a2e4e9584730c1742aca7c1fcc59533a7c159740) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix depencies version\n\n## 0.2.8\n\n## 0.2.7\n\n## 0.2.6\n\n## 0.2.5\n\n## 0.2.4\n\n## 0.2.3\n\n## 0.2.2\n\n## 0.2.1\n\n### Patch Changes\n\n- [#4186](https://github.com/bluesky-social/atproto/pull/4186) [`d570db43d`](https://github.com/bluesky-social/atproto/commit/d570db43d6df2044dbaa5813cac469b3e73ba219) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add Japanese translation on OAuth Provider UI\n\n## 0.2.0\n\n### Minor Changes\n\n- [`f4cb3e4d0`](https://github.com/bluesky-social/atproto/commit/f4cb3e4d0ac45e567fa14f79b99a84621fa89a56) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Adapt to UI to support permission set.\n\n## 0.1.12\n\n## 0.1.11\n\n## 0.1.10\n\n## 0.1.9\n\n## 0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- [#3934](https://github.com/bluesky-social/atproto/pull/3934) [`30f851dee`](https://github.com/bluesky-social/atproto/commit/30f851dee8495b5034743fda1c095509f1fd95bf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix footer links not working in account page\n\n## 0.1.6\n\n## 0.1.5\n\n### Patch Changes\n\n- [`d1e3e68dd`](https://github.com/bluesky-social/atproto/commit/d1e3e68dd9eb7bed13d9023bc0e4ce3c448eabf5) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve auto completion of sing-in & reset password flows\n\n## 0.1.4\n\n## 0.1.3\n\n### Patch Changes\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `:` chars in url path parts\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Always display icon and name of authorized clients\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add paragraph explaining both sections from \"Your account\" page.\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix buttons alignment and labels in \"My Devices\" section\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly display the \"lastSeenAt\" date\n\n## 0.1.2\n\n### Patch Changes\n\n- [#3758](https://github.com/bluesky-social/atproto/pull/3758) [`0f3899dd5`](https://github.com/bluesky-social/atproto/commit/0f3899dd52d0094c29222c65e2636217f9a8ece4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor UI/UX tweaks\n\n## 0.1.1\n\n### Patch Changes\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix dependencies\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Explicit exported package `files`\n\n## 0.1.0\n\n### Minor Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - New build system\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/account.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Mock - OAuth Provider</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script>\n      /*\n       * This file's purpose is to provide a way to develop the UI without\n       * running a full featured OAuth server. It mocks the server responses and\n       * provides configuration data to the UI.\n       *\n       * This file is not part of the production build.\n       *\n       * Start the development server with the following command from the\n       * oauth-provider root:\n       *\n       * ```sh\n       * pnpm run start:ui\n       * ```\n       *\n       * Then open the browser at http://localhost:5173/\n       */\n    </script>\n    <style>\n      :root {\n        --branding-color-primary: 10 122 255;\n        --branding-color-primary-contrast: 255 255 255;\n        --branding-color-primary-hue: 212.57142857142856;\n\n        --branding-color-error: 244 11 66;\n        --branding-color-error-contrast: 255 255 255;\n        --branding-color-error-hue: 345.83690987124464;\n\n        --branding-color-warning: 251 86 7;\n        --branding-color-warning-contrast: 255 255 255;\n        --branding-color-warning-hue: 19.426229508196723;\n\n        --branding-color-success: 2 195 154;\n        --branding-color-success-contrast: 0 0 0;\n        --branding-color-success-hue: 167.2538860103627;\n      }\n    </style>\n    <script type=\"module\">\n      import { API_ENDPOINT_PREFIX } from '@atproto/oauth-provider-api'\n\n      /*\n       * PDS branding configuration\n       */\n\n      history.replaceState(history.state, '', '/account')\n\n      const devices = new Map([\n        [\n          'device1',\n          {\n            userAgent:\n              'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',\n            ipAddress: '192.0.0.1',\n            lastSeenAt: new Date().toISOString(),\n          },\n        ],\n        [\n          'device2',\n          {\n            userAgent:\n              'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',\n            ipAddress: '192.0.0.1',\n            lastSeenAt: '2024-11-26T02:32:15.233Z',\n          },\n        ],\n      ])\n\n      const accounts = new Map(\n        [\n          {\n            sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',\n            email: 'eric@foobar.com',\n            email_verified: true,\n            name: 'Eric',\n            preferred_username: 'esb.lol',\n            picture:\n              'https://cdn.bsky.app/img/avatar/plain/did:plc:3jpt2mvvsumj2r7eqk4gzzjz/bafkreiaexnb3bkzbaxktm5q3l3txyweflh3smcruigesvroqjrqxec4zv4@jpeg',\n          },\n          {\n            sub: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb',\n            email: 'eric@foobar.com',\n            email_verified: true,\n            name: 'Tom Sawyeeeeeeeeeee',\n            preferred_username: 'test.esb.lol',\n            picture:\n              'https://cdn.bsky.app/img/avatar/plain/did:plc:dpajgwmnecpdyjyqzjzm6bnb/bafkreia6dx7fhoi6fxwfpgm7jrxijpqci7ap53wpilkpazojwvqlmgud2m@jpeg',\n          },\n          {\n            sub: 'did:plc:matttmattmattmattmattmat',\n            email: 'matthieu@foobar.com',\n            email_verified: true,\n            name: 'Matthieu',\n            preferred_username: 'matthieu.bsky.test',\n            picture: /** @type {sting|undefined} */ (undefined),\n          },\n        ].map((a) => [a.sub, a]),\n      )\n\n      const clients = new Map(\n        [\n          {\n            client_id: 'https://bsky.app/oauth-client.json',\n            client_name: 'Bluesky',\n            client_uri: 'https://bsky.app',\n            logo_uri: 'https://web-cdn.bsky.app/static/apple-touch-icon.png',\n          },\n        ].map((c) => [c.client_id, c]),\n      )\n\n      // Unable to load metadata for this client:\n      clients.set('https://example.com/oauth-client.json', undefined)\n\n      const accountDeviceSessions = new Map([\n        [\n          'device1',\n          [\n            {\n              sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',\n              remember: true,\n              loginRequired: true,\n            },\n            {\n              sub: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb',\n              remember: false,\n              loginRequired: false,\n            },\n          ],\n        ],\n        [\n          'device2',\n          [\n            {\n              sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',\n              remember: true,\n              loginRequired: false,\n            },\n          ],\n        ],\n      ])\n\n      const accountOAuthSessions = new Map([\n        [\n          'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',\n          [\n            {\n              tokenId: 'token1',\n              createdAt: '2023-10-01T00:00:00.000Z',\n              updatedAt: '2025-10-01T00:00:00.000Z',\n              clientId: 'https://bsky.app/oauth-client.json',\n              scope: 'atproto transition:generic transition:chat.bsky',\n            },\n          ],\n        ],\n        [\n          'did:plc:dpajgwmnecpdyjyqzjzm6bnb',\n          [\n            {\n              tokenId: 'token2',\n              createdAt: '2023-10-01T00:00:00.000Z',\n              updatedAt: '2023-10-01T00:00:00.000Z',\n              clientId: 'https://bsky.app/oauth-client.json',\n              scope:\n                'atproto transition:generic transition:email transition:chat.bsky',\n            },\n            {\n              tokenId: 'token3',\n              createdAt: '2024-08-01T00:00:00.000Z',\n              updatedAt: '2025-10-01T00:00:00.000Z',\n              clientId: 'https://example.com/oauth-client.json',\n              scope: /** @type {string|undefined} */ (undefined),\n            },\n          ],\n        ],\n      ])\n\n      const currentDeviceId = 'device1' // Simulate that this device is \"device1\"\n\n      async function mockFetch(...args) {\n        const [input, init] = args\n\n        const method = init?.method ?? 'GET'\n        const url =\n          typeof input === 'string'\n            ? new URL(input, window.location)\n            : input instanceof URL\n              ? input\n              : undefined\n\n        if (url) {\n          console.log(`Fetching: ${method} ${url.pathname}${url.search}`)\n          switch (`${method} ${url.pathname}`) {\n            case `POST ${API_ENDPOINT_PREFIX}/sign-up`: {\n              const {\n                locale,\n                handle,\n                email,\n                password,\n                inviteCode,\n                hcaptchaToken,\n              } = JSON.parse(init.body)\n\n              return jsonResponse({ error: 'Not implemented' }, 400)\n            }\n\n            case `POST ${API_ENDPOINT_PREFIX}/sign-in`: {\n              const { username, remember } = JSON.parse(init.body)\n              for (const [sub, account] of accounts) {\n                if (\n                  account.email === username ||\n                  account.preferred_username === username ||\n                  username === 'a'\n                ) {\n                  accountDeviceSessions.set(\n                    currentDeviceId,\n                    (\n                      accountDeviceSessions\n                        .get(currentDeviceId)\n                        ?.filter((s) => s.sub !== sub) ?? []\n                    ).concat({ sub, remember, loginRequired: false }),\n                  )\n                  return jsonResponse({ account })\n                }\n              }\n              return jsonResponse({ error: 'Invalid credentials' }, 400)\n            }\n            case `GET ${API_ENDPOINT_PREFIX}/device-sessions`:\n              return jsonResponse(\n                accountDeviceSessions.get(currentDeviceId)?.map((s) => ({\n                  remembered: s.remember,\n                  loginRequired: s.loginRequired,\n                  account: accounts.get(s.sub),\n                })) ?? [],\n              )\n            case `GET ${API_ENDPOINT_PREFIX}/oauth-sessions`: {\n              const sub = url.searchParams.get('sub')\n              return jsonResponse(\n                accountOAuthSessions.get(sub)?.map((oauthSession) => ({\n                  ...oauthSession,\n                  clientMetadata: clients.get(oauthSession.clientId),\n                })) ?? [],\n              )\n            }\n            case `GET ${API_ENDPOINT_PREFIX}/account-sessions`: {\n              const sub = url.searchParams.get('sub')\n              return jsonResponse(\n                Array.from(\n                  accountDeviceSessions.entries(),\n                  ([deviceId, deviceSession]) =>\n                    deviceSession\n                      .filter((s) => s.sub === sub)\n                      .map((s) => ({\n                        deviceId,\n                        deviceMetadata: devices.get(deviceId),\n                        remember: s.remember,\n                        isCurrentDevice: true,\n                      })),\n                ).flat(),\n              )\n            }\n            case `POST ${API_ENDPOINT_PREFIX}/sign-out`: {\n              const { sub } = JSON.parse(init.body)\n              accountDeviceSessions.set(\n                currentDeviceId,\n                accountDeviceSessions\n                  .get(currentDeviceId)\n                  ?.filter((s) => s.sub !== sub) ?? [],\n              )\n              return jsonResponse({ success: true })\n            }\n            case `POST ${API_ENDPOINT_PREFIX}/revoke-account-session`: {\n              const { sub, deviceId } = JSON.parse(init.body)\n              accountDeviceSessions.set(\n                deviceId,\n                accountDeviceSessions\n                  .get(deviceId)\n                  ?.filter((s) => s.sub !== sub) ?? [],\n              )\n              return jsonResponse({ success: true })\n            }\n            case `POST ${API_ENDPOINT_PREFIX}/verify-handle-availability`:\n              return jsonResponse({ available: true })\n            case `POST ${API_ENDPOINT_PREFIX}/reset-password-request`:\n              return jsonResponse({ available: true })\n            case `POST ${API_ENDPOINT_PREFIX}/reset-password-confirm`:\n              return jsonResponse({ available: true })\n          }\n        }\n\n        return origFetch.call(this, ...args)\n      }\n\n      function jsonResponse(payload, status = 200) {\n        console.log('Mock response:', payload)\n        return new Response(JSON.stringify(payload), {\n          status,\n          headers: { 'Content-Type': 'application/json' },\n        })\n      }\n\n      const origFetch = window.fetch\n      Object.defineProperty(window, 'fetch', {\n        writable: true,\n        configurable: true,\n        value: mockFetch,\n      })\n\n      window.__customizationData = {\n        availableUserDomains: ['.bsky.social', '.bsky.team'],\n        inviteCodeRequired: false,\n        hcaptchaSiteKey: undefined,\n        name: 'Bluesky',\n        links: [\n          {\n            title: { en: 'Home', fr: 'Accueil', ja: 'ホーム' },\n            href: 'https://bsky.social/',\n            rel: 'canonical', // prevents the login page from being indexed by search engines\n          },\n          {\n            title: { en: 'Terms of Service', ja: '利用規約' },\n            href: 'https://bsky.social/about/support/tos',\n            rel: 'terms-of-service',\n          },\n          {\n            title: { en: 'Privacy Policy', ja: 'プライバシーポリシー' },\n            href: 'https://bsky.social/about/support/privacy-policy',\n            rel: 'privacy-policy',\n          },\n          {\n            title: { en: 'Support', ja: 'サポート' },\n            href: 'https://blueskyweb.zendesk.com/hc/en-us',\n            rel: 'help',\n          },\n        ],\n        logo: `data:image/svg+xml,${encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 320 286\"><path fill=\"rgb(10,122,255)\" d=\"M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z\" /></svg>')}`,\n      }\n\n      window.__deviceSessions =\n        accountDeviceSessions.get(currentDeviceId)?.map((s) => ({\n          remembered: s.remember,\n          loginRequired: s.loginRequired,\n          account: accounts.get(s.sub),\n        })) ?? []\n    </script>\n    <script src=\"./src/account-page.tsx\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/hydration-data.d.ts",
    "content": "import {\n  ActiveDeviceSession,\n  CustomizationData,\n} from '@atproto/oauth-provider-api'\n\nexport type HydrationData = {\n  'account-page': {\n    /**\n     * needed by `useCustomizationData.ts`\n     */\n    __customizationData: CustomizationData\n    /**\n     * needed by `useDeviceSessionsQuery.ts`\n     */\n    __deviceSessions: readonly ActiveDeviceSession[]\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>OAuth mock pages</title>\n  </head>\n  <body>\n    <a href=\"/account.html\">My Account</a>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-provider-frontend\",\n  \"version\": \"0.2.9\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-provider-frontend\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"type\": \"commonjs\",\n  \"exports\": {\n    \"./bundle-manifest.json\": {\n      \"default\": \"./dist/bundle-manifest.json\"\n    },\n    \"./hydration-data\": {\n      \"types\": \"./hydration-data.d.ts\"\n    }\n  },\n  \"files\": [\n    \"dist\",\n    \"hydration-data.d.ts\"\n  ],\n  \"optionalDependencies\": {\n    \"@atproto/oauth-provider-api\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@atproto-labs/fetch\": \"workspace:^\",\n    \"@atproto-labs/rollup-plugin-bundle-manifest\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@lingui/cli\": \"^5.2.0\",\n    \"@lingui/core\": \"^5.2.0\",\n    \"@lingui/react\": \"^5.2.0\",\n    \"@lingui/swc-plugin\": \"^5.4.0\",\n    \"@lingui/vite-plugin\": \"^5.2.0\",\n    \"@radix-ui/react-dialog\": \"^1.1.6\",\n    \"@radix-ui/react-icons\": \"^1.3.2\",\n    \"@radix-ui/react-popover\": \"^1.1.6\",\n    \"@radix-ui/react-toast\": \"^1.2.6\",\n    \"@tailwindcss/vite\": \"^4.1.3\",\n    \"@tanstack/react-form\": \"^1.3.0\",\n    \"@tanstack/react-query\": \"^5.71.10\",\n    \"@tanstack/react-router\": \"^1.115.0\",\n    \"@tanstack/react-router-devtools\": \"^1.115.0\",\n    \"@tanstack/router-plugin\": \"^1.115.0\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"@vitejs/plugin-react-swc\": \"^3.8.0\",\n    \"clsx\": \"^2.1.1\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"tailwindcss\": \"^4.0.14\",\n    \"ua-parser-js\": \"^2.0.3\",\n    \"vite\": \"^6.2.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"scripts\": {\n    \"i18n:extract\": \"lingui extract --clean\",\n    \"i18n:compile\": \"lingui compile --typescript\",\n    \"i18n\": \"pnpm i18n:extract && pnpm i18n:compile\",\n    \"prebuild\": \"pnpm run i18n:compile\",\n    \"build\": \"vite build --emptyOutDir -- ignore additional npm args\",\n    \"dev:ui\": \"vite --port 5173\",\n    \"dev:src\": \"vite build --watch\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/account-page.tsx",
    "content": "import './style.css'\n\nimport '#/locales/setup'\n\nimport { i18n } from '@lingui/core'\nimport { I18nProvider } from '@lingui/react'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { RouterProvider, createRouter } from '@tanstack/react-router'\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { Provider as ToastProvider } from '#/components/Toast'\nimport { Provider as LocaleProvider } from '#/locales'\nimport { routeTree } from '#/routeTree.gen'\n\nconst qc = new QueryClient()\nconst router = createRouter({\n  routeTree,\n  pathParamsAllowedCharacters: [':'],\n})\n\n// Register the router instance for type safety\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof router\n  }\n}\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <I18nProvider i18n={i18n}>\n      <LocaleProvider>\n        <QueryClientProvider client={qc}>\n          <ToastProvider>\n            <RouterProvider router={router} />\n          </ToastProvider>\n        </QueryClientProvider>\n      </LocaleProvider>\n    </I18nProvider>\n  </StrictMode>,\n)\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/api/api.ts",
    "content": "import {\n  API_ENDPOINT_PREFIX,\n  ApiEndpoints,\n  CSRF_COOKIE_NAME,\n  CSRF_HEADER_NAME,\n} from '@atproto/oauth-provider-api'\nimport { readCookie } from '../util/cookies.ts'\nimport {\n  JsonClient,\n  JsonErrorPayload,\n  JsonErrorResponse,\n} from './json-client.ts'\n\nexport type { Options } from './json-client.ts'\n\nexport class Api extends JsonClient<ApiEndpoints> {\n  constructor() {\n    const baseUrl = new URL(API_ENDPOINT_PREFIX, window.origin).toString()\n    super(baseUrl, () => ({\n      [CSRF_HEADER_NAME]: readCookie(CSRF_COOKIE_NAME),\n    }))\n  }\n\n  // Override the parent's parseError method to handle expected error responses\n  // and transform them into instances of the corresponding error classes.\n  public static override parseError(\n    json: unknown,\n  ): undefined | JsonErrorResponse {\n    // @NOTE Most specific errors first !\n    if (SecondAuthenticationFactorRequiredError.is(json)) {\n      return new SecondAuthenticationFactorRequiredError(json)\n    }\n    if (InvalidCredentialsError.is(json)) {\n      return new InvalidCredentialsError(json)\n    }\n    if (InvalidInviteCodeError.is(json)) {\n      return new InvalidInviteCodeError(json)\n    }\n    if (HandleUnavailableError.is(json)) {\n      return new HandleUnavailableError(json)\n    }\n    if (EmailTakenError.is(json)) {\n      return new EmailTakenError(json)\n    }\n    if (RequestExpiredError.is(json)) {\n      return new RequestExpiredError(json)\n    }\n    if (UnknownRequestUriError.is(json)) {\n      return new UnknownRequestUriError(json)\n    }\n    if (InvalidRequestError.is(json)) {\n      return new InvalidRequestError(json)\n    }\n    if (AccessDeniedError.is(json)) {\n      return new AccessDeniedError(json)\n    }\n    return super.parseError(json)\n  }\n}\n\nexport type AccessDeniedPayload = JsonErrorPayload<'access_denied'>\nexport class AccessDeniedError<\n  P extends AccessDeniedPayload = AccessDeniedPayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description || 'Access denied',\n  ) {\n    super(payload, message)\n  }\n\n  static is(json: unknown): json is AccessDeniedPayload {\n    return super.is(json) && json.error === 'access_denied'\n  }\n}\n\nexport type InvalidRequestPayload = JsonErrorPayload<'invalid_request'>\nexport class InvalidRequestError<\n  P extends InvalidRequestPayload = InvalidRequestPayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description || 'Invalid request',\n  ) {\n    super(payload, message)\n  }\n\n  static is(json: unknown): json is InvalidRequestPayload {\n    return super.is(json) && json.error === 'invalid_request'\n  }\n}\n\nexport type InvalidInviteCodePayload = InvalidRequestPayload & {\n  error_description: `This invite code is invalid.${string}`\n}\nexport class InvalidInviteCodeError<\n  P extends InvalidInviteCodePayload = InvalidInviteCodePayload,\n> extends InvalidRequestError<P> {\n  constructor(payload: P) {\n    super(payload)\n  }\n\n  static is(json: unknown): json is InvalidInviteCodePayload {\n    return (\n      super.is(json) &&\n      json.error_description != null &&\n      json.error_description.startsWith('This invite code is invalid.')\n    )\n  }\n}\n\nexport type RequestExpiredPayload = AccessDeniedPayload & {\n  error_description: 'This request has expired'\n}\nexport class RequestExpiredError<\n  P extends RequestExpiredPayload = RequestExpiredPayload,\n> extends AccessDeniedError<P> {\n  static is(json: unknown): json is RequestExpiredPayload {\n    return (\n      super.is(json) && json.error_description === 'This request has expired'\n    )\n  }\n}\n\nexport type InvalidCredentialsPayload = InvalidRequestPayload & {\n  error_description: 'Invalid identifier or password'\n}\nexport class InvalidCredentialsError<\n  P extends InvalidCredentialsPayload = InvalidCredentialsPayload,\n> extends InvalidRequestError<P> {\n  static is(json: unknown): json is InvalidCredentialsPayload {\n    return (\n      super.is(json) &&\n      json.error_description === 'Invalid identifier or password'\n    )\n  }\n}\n\nexport type UnknownRequestPayload = InvalidRequestPayload & {\n  error_description: 'Unknown request_uri'\n}\nexport class UnknownRequestUriError<\n  P extends UnknownRequestPayload = UnknownRequestPayload,\n> extends InvalidRequestError<P> {\n  static is(json: unknown): json is UnknownRequestPayload {\n    return super.is(json) && json.error_description === 'Unknown request_uri'\n  }\n}\nexport type EmailTakenPayload = InvalidRequestPayload & {\n  error_description: 'Email already taken'\n}\nexport class EmailTakenError<\n  P extends EmailTakenPayload = EmailTakenPayload,\n> extends InvalidRequestError<P> {\n  static is(json: unknown): json is EmailTakenPayload {\n    return super.is(json) && json.error_description === 'Email already taken'\n  }\n}\n\nexport type HandleUnavailablePayload =\n  JsonErrorPayload<'handle_unavailable'> & {\n    reason: 'syntax' | 'domain' | 'slur' | 'taken'\n  }\nexport class HandleUnavailableError<\n  P extends HandleUnavailablePayload = HandleUnavailablePayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description || 'That handle cannot be used',\n  ) {\n    super(payload, message)\n  }\n\n  get reason() {\n    return this.payload.reason\n  }\n\n  static is(json: unknown): json is HandleUnavailablePayload {\n    return (\n      super.is(json) &&\n      json.error === 'handle_unavailable' &&\n      'reason' in json &&\n      (json.reason === 'syntax' ||\n        json.reason === 'domain' ||\n        json.reason === 'slur' ||\n        json.reason === 'taken')\n    )\n  }\n}\n\nexport type SecondAuthenticationFactorRequiredPayload =\n  JsonErrorPayload<'second_authentication_factor_required'> & {\n    type: 'emailOtp'\n    hint: string\n  }\nexport class SecondAuthenticationFactorRequiredError<\n  P extends\n    SecondAuthenticationFactorRequiredPayload = SecondAuthenticationFactorRequiredPayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description ||\n      `${payload.type} authentication factor required (hint: ${payload.hint})`,\n  ) {\n    super(payload, message)\n  }\n\n  get type() {\n    return this.payload.type\n  }\n  get hint() {\n    return this.payload.hint\n  }\n\n  static is(json: unknown): json is SecondAuthenticationFactorRequiredPayload {\n    return (\n      super.is(json) &&\n      json.error === 'second_authentication_factor_required' &&\n      'type' in json &&\n      json.type === 'emailOtp' &&\n      'hint' in json &&\n      typeof json.hint === 'string'\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/api/index.ts",
    "content": "import { useMemo } from 'react'\nimport { Api } from '#/api/api'\n\nexport type * from '@atproto/oauth-provider-api'\nexport * from '#/api/api'\nexport * from '#/api/json-client'\n\nexport function useApi() {\n  return useMemo(() => new Api(), [])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/api/json-client.ts",
    "content": "// Using a type import to avoid bundling this lib\nimport type { Json } from '@atproto-labs/fetch'\n\nexport { type Json }\ntype Awaitable<T> = T | PromiseLike<T>\n\nexport type Options = {\n  signal?: AbortSignal\n}\n\nexport type EndpointPath = `/${string}`\nexport type EndpointDefinition =\n  | {\n      method: 'POST'\n      input: Json\n      output: Json | void\n    }\n  | {\n      method: 'GET'\n      params?: Record<string, string | undefined>\n      output: Json | void\n    }\n\nexport class JsonClient<\n  Endpoints extends { [Path: EndpointPath]: EndpointDefinition },\n> {\n  constructor(\n    protected readonly baseUrl: string,\n    protected readonly getHeaders: () => Awaitable<\n      Record<string, string | undefined>\n    >,\n  ) {}\n\n  public async fetch<Path extends EndpointPath & keyof Endpoints>(\n    method: Endpoints[Path]['method'],\n    path: Path,\n    input: Endpoints[Path] extends { method: 'GET' }\n      ? Endpoints[Path]['params']\n      : Endpoints[Path] extends { method: 'POST' }\n        ? Endpoints[Path]['input']\n        : undefined,\n    options?: Options,\n  ): Promise<Endpoints[Path]['output']> {\n    const url = new URL(`${this.baseUrl}${path}`)\n    if (method === 'GET') {\n      if (input) {\n        for (const [key, value] of Object.entries(input)) {\n          url.searchParams.set(key, value)\n        }\n      }\n    }\n\n    const body = method === 'POST' ? JSON.stringify(input) : undefined\n\n    const headers = Object.entries(await this.getHeaders.call(null))\n      .filter((entry): entry is [string, string] => entry[1] != null)\n      .map(([k, v]) => [k.toLowerCase(), v] as [string, string])\n\n    const response = await fetch(url, {\n      method,\n      headers:\n        body && !headers.some(([k]) => k === 'content-type')\n          ? headers.concat([['content-type', 'application/json']])\n          : headers,\n      mode: 'same-origin',\n      body,\n      signal: options?.signal,\n    })\n\n    if (response.status === 204) {\n      return undefined\n    }\n\n    const responseType = response.headers.get('content-type')\n    if (responseType !== 'application/json') {\n      await response.body?.cancel()\n      throw new Error(`Invalid content type \"${responseType}\"`, {\n        cause: response,\n      })\n    }\n\n    const json = await response.json()\n\n    if (response.ok) return json as Endpoints[Path]['output']\n    else throw this.parseError(response, json)\n  }\n\n  protected parseError(response: Response, json: Json): Error {\n    const Class = this.constructor as typeof JsonClient\n    const error = Class.parseError(json)\n    if (error) return error\n\n    return new Error('Invalid JSON response', { cause: response })\n  }\n\n  public static parseError(json: unknown): undefined | JsonErrorResponse {\n    if (JsonErrorResponse.is(json)) {\n      return new JsonErrorResponse(json)\n    }\n  }\n}\n\nexport type JsonErrorPayload<E extends string = string> = {\n  error: E\n  error_description?: string\n}\n\nexport class JsonErrorResponse<\n  P extends JsonErrorPayload = JsonErrorPayload,\n> extends Error {\n  constructor(\n    public readonly payload: P,\n    message = payload.error_description,\n  ) {\n    super(message || `Error \"${payload.error}\"`)\n  }\n\n  get error(): string {\n    return this.payload.error\n  }\n\n  get description(): string | undefined {\n    return this.payload.error_description\n  }\n\n  static is(json: unknown): json is JsonErrorPayload {\n    return (\n      json != null &&\n      typeof json === 'object' &&\n      typeof json['error'] === 'string' &&\n      (json['error_description'] === undefined ||\n        typeof json['error_description'] === 'string')\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/AccountSelector.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { Trans } from '@lingui/react/macro'\nimport { DotsHorizontalIcon } from '@radix-ui/react-icons'\nimport * as Popover from '@radix-ui/react-popover'\nimport { clsx } from 'clsx'\nimport { Avatar } from '#/components/Avatar'\nimport { Button } from '#/components/Button'\nimport { Link } from '#/components/Link'\nimport { useCurrentSession } from '#/data/useCurrentSession'\nimport { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'\nimport { useSignOutMutation } from '#/data/useSignOutMutation'\nimport { getAccountName } from '#/util/getAccountName'\nimport { sanitizeHandle } from '#/util/sanitizeHandle'\n\nexport function AccountSelector() {\n  const { _ } = useLingui()\n  const { data } = useDeviceSessionsQuery()\n  const { account: currentAccount } = useCurrentSession()\n\n  const { mutate: signOut } = useSignOutMutation()\n\n  return (\n    <Popover.Root>\n      <Popover.Trigger asChild>\n        <button\n          className={clsx([\n            'flex items-center space-x-2 truncate rounded-lg border py-1 pl-1 pr-3',\n            'bg-contrast-0 dark:bg-contrast-25 border-contrast-50 dark:border-contrast-100',\n            'hover:bg-contrast-25 dark:hover:bg-contrast-50 hover:border-contrast-100 dark:hover:border-contrast-200',\n          ])}\n          aria-label={_(msg`Select an account`)}\n          style={{ maxWidth: 220 }}\n        >\n          <div>\n            <Avatar\n              size={36}\n              src={currentAccount.picture}\n              displayName={currentAccount.name}\n            />\n          </div>\n          <div className=\"flex-1 truncate text-left\">\n            <p className=\"text-text-default truncate text-sm font-bold leading-tight\">\n              {getAccountName(currentAccount)}\n            </p>\n            <p className=\"text-text-light truncate text-sm leading-tight\">\n              {sanitizeHandle(currentAccount.preferred_username)}\n            </p>\n          </div>\n          <div className=\"pl-4\">\n            <DotsHorizontalIcon width={20} />\n          </div>\n        </button>\n      </Popover.Trigger>\n      <Popover.Portal>\n        <Popover.Content\n          side=\"top\"\n          align=\"end\"\n          className=\"PopoverContent w-full\"\n          sideOffset={5}\n          style={{ width: 320 }}\n        >\n          <div className=\"bg-contrast-0 dark:bg-contrast-25 border-contrast-25 dark:border-contrast-50 shadow-contrast-900/15 dark:shadow-contrast-0/60 relative rounded-lg border shadow-xl\">\n            <div className=\"flex flex-col overflow-hidden rounded-lg\">\n              {data.map(({ account }, i) => (\n                <Link\n                  key={account.sub}\n                  to=\"/account/$sub\"\n                  params={account}\n                  className={clsx([\n                    'flex items-center space-x-2 py-2 pl-2 pr-4',\n                    'hover:bg-contrast-25 dark:hover:bg-contrast-50 focus:bg-contrast-25 dark:focus:bg-contrast-50',\n                    i !== 0 &&\n                      'border-contrast-25 dark:border-contrast-50 border-t',\n                  ])}\n                >\n                  <Avatar\n                    size={36}\n                    src={account.picture}\n                    displayName={account.name}\n                  />\n                  <div className=\"flex-1 space-x-1 truncate text-left\">\n                    <p className=\"text-text-default truncate text-sm font-bold leading-snug\">\n                      {getAccountName(account)}\n                    </p>\n                    <p className=\"text-text-light truncate text-sm leading-snug\">\n                      {sanitizeHandle(account.preferred_username)}\n                    </p>\n                  </div>\n                  <div className=\"flex-shrink\">\n                    <Button\n                      size=\"sm\"\n                      color=\"secondary\"\n                      onClick={(e) => {\n                        // technically invalid markup to have a button inside a link :/\n                        // prevent click from bubbling up to the Link\n                        e.stopPropagation()\n                        e.preventDefault()\n                        signOut({ sub: account.sub })\n                      }}\n                    >\n                      <Button.Text>\n                        <Trans>Sign out</Trans>\n                      </Button.Text>\n                    </Button>\n                  </div>\n                </Link>\n              ))}\n            </div>\n          </div>\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Admonition.tsx",
    "content": "import {\n  CircleBackslashIcon,\n  ExclamationTriangleIcon,\n  InfoCircledIcon,\n  QuestionMarkCircledIcon,\n} from '@radix-ui/react-icons'\nimport { clsx } from 'clsx'\nimport { ReactNode } from 'react'\n\ntype Variant = 'tip' | 'info' | 'warning' | 'error'\n\nconst icons: Record<Variant, typeof QuestionMarkCircledIcon> = {\n  tip: QuestionMarkCircledIcon,\n  info: InfoCircledIcon,\n  warning: ExclamationTriangleIcon,\n  error: CircleBackslashIcon,\n}\n\nconst borderColors: Record<Variant, string> = {\n  tip: '',\n  info: '',\n  warning: 'border-warning-500 dark:border-warning-700',\n  error: 'border-error-500 dark:border-error-400',\n}\nconst iconColors: Record<Variant, string> = {\n  tip: 'text-success-600 dark:text-success-500',\n  info: 'text-primary-500',\n  warning: 'text-warning-600 dark:text-warning-500',\n  error: 'text-error-500 dark:text-error-400',\n}\n\nexport function Card({\n  children,\n  variant,\n}: {\n  children: ReactNode\n  variant?: Variant\n}) {\n  const borderColor = variant ? borderColors[variant] : ''\n  return (\n    <div\n      className={clsx([\n        'border-contrast-25 dark:border-contrast-50 shadow-contrast-500/20 dark:shadow-contrast-0/50 flex items-start space-x-3 rounded-md border py-3 pl-3 pr-4 shadow-lg',\n        borderColor,\n      ])}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport function Icon({ variant }: { variant?: Variant }) {\n  const Icon = variant ? icons[variant] : icons.info\n  const color = variant ? iconColors[variant] : iconColors.info\n  return (\n    <div className={clsx(['pt-0.5', color])}>\n      <Icon width={20} height={20} />\n    </div>\n  )\n}\n\nexport function Content({ children }: { children: ReactNode }) {\n  return <div className=\"flex-grow space-y-1\">{children}</div>\n}\n\nexport function Title({ children }: { children: ReactNode }) {\n  return <h3 className=\"text-lg font-semibold leading-snug\">{children}</h3>\n}\n\nexport function Text({ children }: { children: ReactNode }) {\n  return <p className=\"text-md text-text-light\">{children}</p>\n}\n\nexport function Default({\n  variant,\n  title,\n  text,\n}: {\n  variant?: Variant\n  title?: string\n  text: string\n}) {\n  return (\n    <Card variant={variant}>\n      <Icon variant={variant} />\n      <Content>\n        {title && <Title>{title}</Title>}\n        <Text>{text}</Text>\n      </Content>\n    </Card>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Avatar.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { PersonIcon } from '@radix-ui/react-icons'\n\nexport function Avatar({\n  src,\n  size = 40,\n  displayName,\n}: {\n  src: string | undefined\n  size?: number\n  displayName: string | undefined\n}) {\n  const { _ } = useLingui()\n  return (\n    <div\n      className=\"relative flex items-center justify-center overflow-hidden rounded-full\"\n      style={{\n        width: size,\n        height: size,\n      }}\n    >\n      {src ? (\n        <img src={src} alt={_(msg`User avatar`)} className=\"absolute inset-0\" />\n      ) : displayName ? (\n        <p\n          className=\"absolute uppercase\"\n          style={{\n            fontSize: size / 2 + 'px',\n            lineHeight: size + 'px',\n          }}\n        >\n          {displayName.replace(/[^A-z0-0]/g, '').slice(0, 1)}\n        </p>\n      ) : (\n        <PersonIcon height={size * (1 / 2)} width={size * (1 / 2)} />\n      )}\n      <div className=\"border-contrast-100 absolute inset-0 rounded-full border-2\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Button.tsx",
    "content": "import { clsx } from 'clsx'\nimport { useMemo } from 'react'\n\ntype ButtonVariantProps = {\n  color?: 'primary' | 'secondary'\n  size?: 'sm' | 'md' | 'lg'\n}\ntype ButtonStateProps = {\n  disabled?: boolean\n}\n\nexport function Button({\n  children,\n  color = 'primary',\n  size = 'md',\n  ...rest\n}: React.ButtonHTMLAttributes<HTMLButtonElement> &\n  ButtonVariantProps &\n  ButtonStateProps & {\n    children: React.ReactNode\n  }) {\n  const cn = useButtonStyles({ color, size, disabled: rest.disabled })\n  return (\n    <button\n      {...rest}\n      aria-disabled={rest.disabled ? 'true' : 'false'}\n      className={clsx(cn, rest.className)}\n    >\n      {children}\n    </button>\n  )\n}\n\nfunction Text({ children }: { children: React.ReactNode }) {\n  return <span>{children}</span>\n}\n\nButton.Text = Text\n\nexport type ButtonStyleProps = ButtonVariantProps & ButtonStateProps\n\nexport function useButtonStyles({ color, size, disabled }: ButtonStyleProps) {\n  return useMemo(() => {\n    return clsx([\n      'flex-1 flex items-center justify-center text-center rounded-md font-medium',\n      size === 'sm' && ['px-3 h-7 space-x-1', 'text-sm'],\n      size === 'md' && ['px-5 h-10 space-x-1', 'text-md'],\n      size === 'lg' && ['px-6 h-12 space-x-2', 'text-md'],\n      color === 'primary' && [\n        disabled\n          ? ['bg-primary-400 text-primary-100', 'cursor-not-allowed']\n          : [\n              'bg-primary text-primary-contrast',\n              'focus:outline-none focus:shadow-sm focus:shadow-primary-700/30',\n            ],\n      ],\n      color === 'secondary' && [\n        disabled\n          ? ['bg-contrast-300 text-white/50', 'cursor-not-allowed']\n          : [\n              'bg-contrast-500 dark:bg-contrast-300 text-white',\n              'hover:bg-contrast-600 focus:bg-contrast-600 dark:hover:bg-contrast-400 dark:focus:bg-contrast-400',\n              'focus:outline-none focus:shadow-sm focus:shadow-contrast-700/30',\n            ],\n      ],\n    ])\n  }, [])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/ContentCard.tsx",
    "content": "import { clsx } from 'clsx'\nimport { ReactNode } from 'react'\n\nexport function ContentCard({\n  children,\n  size = 'narrow',\n}: {\n  children: ReactNode\n  size?: 'full' | 'narrow'\n}) {\n  const maxWidth = size === 'full' ? 600 : 400\n  return (\n    <div\n      className={clsx([\n        'mx-auto rounded-lg border p-5 shadow-xl md:p-7 dark:shadow-2xl',\n        'border-contrast-25 dark:border-contrast-50 shadow-contrast-500/20 dark:shadow-contrast-0/50',\n      ])}\n      style={{\n        maxWidth,\n      }}\n    >\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Dialog.tsx",
    "content": "import * as Dialog from '@radix-ui/react-dialog'\nimport { Cross2Icon } from '@radix-ui/react-icons'\nimport { clsx } from 'clsx'\nimport { AriaRole, ReactNode } from 'react'\n\nexport const Root = Dialog.Root\nexport const Trigger = Dialog.Trigger\nexport const Title = Dialog.Title\nexport const Description = Dialog.Description\n\nexport function Outer({ children }: { children: ReactNode }) {\n  return (\n    <Dialog.Portal>\n      <Dialog.Overlay className=\"DialogOverlay bg-contrast-900/30 dark:bg-contrast-0/60 fixed inset-0\" />\n      {children}\n    </Dialog.Portal>\n  )\n}\n\nexport function Inner({\n  children,\n  role,\n  className,\n}: {\n  children: ReactNode\n  role?: AriaRole\n  className?: string\n}) {\n  return (\n    <Dialog.Content\n      role={role}\n      className={clsx([\n        'DialogContent',\n        'max-w-[600px] rounded-xl p-5 shadow-xl',\n        'bg-contrast-0 dark:bg-contrast-25 shadow-contrast-975/15 dark:shadow-contrast-0/60',\n        className,\n      ])}\n    >\n      {children}\n    </Dialog.Content>\n  )\n}\n\nexport function Close() {\n  return (\n    <Dialog.Close\n      className={clsx([\n        'absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border transition-colors focus:outline-0',\n        'bg-contrast-0 dark:bg-contrast-25 border-contrast-25 dark:border-contrast-50',\n        'hover:bg-contrast-25 dark:hover:bg-contrast-50 hover:border-contrast-50 dark:hover:border-contrast-100',\n      ])}\n    >\n      <Cross2Icon className=\"text-text-light hover:text-text-default focus:text-text-default\" />\n    </Dialog.Close>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Divider.tsx",
    "content": "export function Divider() {\n  return <div className=\"border-border-default w-full border-t\" />\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/ErrorScreen.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { ErrorComponentProps } from '@tanstack/react-router'\n\nexport function ErrorScreen({\n  title,\n  description,\n}: {\n  title?: string\n  description: string\n}) {\n  return (\n    <main className=\"bg-contrast-25 min-h-screen px-4 pt-16 md:px-6\">\n      <div\n        className=\"mx-auto w-full\"\n        style={{ maxWidth: 600, minHeight: '100vh' }}\n      >\n        <div role=\"alert\">\n          <h1 className=\"text-3xl font-bold\">\n            {title || <Trans>Whoops! An error occurred.</Trans>}\n          </h1>\n          <p>{description}</p>\n        </div>\n      </div>\n    </main>\n  )\n}\n\nexport function RouterErrorComponent({ error }: ErrorComponentProps) {\n  return <ErrorScreen description={error.message} />\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Footer.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { ReactNode } from '@tanstack/react-router'\nimport { useMemo } from 'react'\nimport { LocaleSelector } from '#/components/LocaleSelector'\nimport { useCustomizationData } from '#/data/useCustomizationData'\nimport { useLocale } from '#/locales'\nimport { Locale, locales } from '#/locales/locales'\nimport { getLangString } from '#/util/lang'\nimport { useLangString } from '#/util/lang-string'\nimport type { LinkDefinition } from '@atproto/oauth-provider-api'\n\nexport function Footer() {\n  const { locale, setLocale } = useLocale()\n  const { links } = useCustomizationData()\n\n  const translatedLinks = useMemo(() => {\n    return links\n      ?.map((link) =>\n        typeof link.title === 'string'\n          ? (link as typeof link & { title: string })\n          : {\n              ...link,\n              title: getLangString(\n                link.title,\n                locale,\n                link.title['en'] || Object.values(link.title).find(Boolean),\n              ),\n            },\n      )\n      .filter((link): link is typeof link & { title: string } => !!link.title)\n  }, [links, locale])\n\n  return (\n    <footer className=\"h-15 bg-contrast-25 dark:bg-contrast-50 fixed inset-x-0 bottom-0 flex items-center justify-between px-4 md:px-6\">\n      <div className=\"flex flex-wrap\">\n        {translatedLinks?.map((link) => (\n          <a\n            href={link.href}\n            className=\"text-text-light mr-4 text-sm hover:underline focus:underline focus:outline-none\"\n            key={link.href}\n          >\n            <LinkTitle link={link} />\n          </a>\n        ))}\n      </div>\n\n      <LocaleSelector\n        items={Object.entries(locales).map(([code, l]) => ({\n          label: l.flag + ' ' + l.name,\n          value: code,\n        }))}\n        value={locale}\n        onSelect={(value) => setLocale(value as Locale)}\n      />\n    </footer>\n  )\n}\n\nexport type LinkNameProps = {\n  link: LinkDefinition\n}\n\nexport function LinkTitle({ link }: LinkNameProps): ReactNode {\n  const { t } = useLingui()\n\n  const title = useLangString(link.title)\n  if (title) return title\n\n  // Fallback\n  if (link.rel === 'canonical') return t`Home`\n  if (link.rel === 'privacy-policy') return t`Privacy Policy`\n  if (link.rel === 'terms-of-service') return t`Terms of Service`\n  if (link.rel === 'help') return t`Support`\n\n  // English version\n  return typeof link.title === 'object'\n    ? link.title['en'] || Object.values(link.title).find(Boolean)\n    : link.title\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Layout.tsx",
    "content": "import { clsx } from 'clsx'\n\nexport function Outer({ children }: { children: React.ReactNode }) {\n  return <main className=\"px-4 md:px-6\">{children}</main>\n}\n\nexport function Center({\n  children,\n  className,\n  style = {},\n}: {\n  children: React.ReactNode\n  className?: string\n  style?: React.CSSProperties\n}) {\n  return (\n    <div\n      className={clsx(['mx-auto w-full py-10', className])}\n      style={{ maxWidth: 600, ...style }}\n    >\n      {children}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Link.tsx",
    "content": "import { Link as RouterLink, LinkComponentProps } from '@tanstack/react-router'\nimport { clsx } from 'clsx'\nimport { ButtonStyleProps, useButtonStyles } from '#/components/Button'\n\nexport type LinkProps = LinkComponentProps & {\n  label?: string\n}\n\nexport function Link({ children, label, ...rest }: LinkProps) {\n  return (\n    <RouterLink {...rest} aria-label={label || rest.href || rest.to}>\n      {children}\n    </RouterLink>\n  )\n}\n\nexport function InlineLink({ children, className, ...rest }: LinkProps) {\n  return (\n    <Link\n      {...rest}\n      className={clsx([\n        'text-primary-500',\n        'hover:underline',\n        'focus:underline focus:outline-none',\n        className,\n      ])}\n    >\n      {children}\n    </Link>\n  )\n}\n\nexport function staticClick(\n  onClick: (e: React.MouseEvent) => void,\n): Partial<LinkComponentProps> {\n  return {\n    to: '.',\n    onClick(e) {\n      e.preventDefault()\n      onClick(e)\n    },\n  }\n}\n\nLink.staticClick = staticClick\nInlineLink.staticClick = staticClick\n\nexport function ButtonLink({\n  children,\n  color = 'primary',\n  size = 'md',\n  disabled,\n  ...rest\n}: LinkProps & ButtonStyleProps) {\n  const cn = useButtonStyles({ color, size, disabled })\n  return (\n    <Link {...rest} className={cn}>\n      {children}\n    </Link>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Loader.tsx",
    "content": "const sizes = {\n  sm: 20,\n  md: 28,\n  lg: 36,\n}\n\nexport function Loader({\n  fill = 'var(--color-primary)',\n  size: sizeName = 'md',\n  width,\n}: {\n  fill?: string\n  size?: 'sm' | 'md' | 'lg'\n  width?: number\n}) {\n  const size = sizes[sizeName] || width\n\n  return (\n    <div\n      className=\"align-center relative justify-center\"\n      style={{ width: size, height: size }}\n    >\n      <div className=\"loader-animation\">\n        <svg fill=\"none\" viewBox=\"0 0 24 24\" width={size} height={size}>\n          <path\n            fill={fill || 'var(--color-primary)'}\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z\"\n          />\n        </svg>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/LocaleSelector.tsx",
    "content": "import { ChevronDownIcon } from '@radix-ui/react-icons'\nimport { clsx } from 'clsx'\n\nexport function LocaleSelector({\n  items,\n  value,\n  onSelect,\n}: {\n  items: {\n    label: string\n    value: string\n  }[]\n  value: string\n  onSelect: (value: string) => void\n}) {\n  return (\n    <div className=\"relative\">\n      <select\n        className={clsx([\n          'bg-contrast-25 text-text-default border-contrast-100 cursor-pointer rounded-full border py-1.5 pl-2 pr-8 text-sm font-semibold focus:shadow-sm',\n          'hover:bg-contrast-0 focus:bg-contrast-0 dark:hover:bg-contrast-0',\n          'focus:bg-contrast-0 dark:focus:bg-contrast-0 focus:outline-none',\n        ])}\n        onChange={(e) => onSelect(e.target.value)}\n        value={value}\n      >\n        {items.map((item) => (\n          <option key={item.value} value={item.value}>\n            {item.label}\n          </option>\n        ))}\n      </select>\n\n      <ChevronDownIcon className=\"pointer-events-none absolute bottom-0 right-2 top-0 my-auto h-5 w-5\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Nav.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { AccountSelector } from '#/components/AccountSelector'\nimport { Link } from '#/components/Link'\nimport { useCustomizationData } from '#/data/useCustomizationData'\n\nexport function Nav() {\n  const { t } = useLingui()\n  const { logo, name } = useCustomizationData()\n\n  return (\n    <>\n      <nav className=\"bg-contrast-0 dark:bg-contrast-25 border-contrast-100 h-15 fixed inset-x-0 top-0 flex items-center justify-between border-b px-4 md:px-6\">\n        {logo ? (\n          <Link to=\"/account\">\n            <div style={{ width: 120, height: 30 }}>\n              <img\n                src={logo}\n                alt={name || t`Logo`}\n                className=\"h-full w-full object-contain object-left\"\n              />\n            </div>\n          </Link>\n        ) : (\n          <div />\n        )}\n\n        <AccountSelector />\n      </nav>\n      {/* Spacer */}\n      <div className=\"h-15\" />\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Prompt.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { useState } from 'react'\nimport { Button } from '#/components/Button'\nimport * as Dialog from '#/components/Dialog'\n\nexport function Prompt({\n  children,\n  title,\n  description,\n  confirmCTA,\n  cancelCTA,\n  onConfirm,\n  onCancel,\n}: {\n  children: React.ReactNode\n  title: string\n  description?: string\n  confirmCTA?: string\n  cancelCTA?: string\n  onConfirm?: () => void\n  onCancel?: () => void\n}) {\n  const { _ } = useLingui()\n  const [open, setOpen] = useState(false)\n\n  const handleOnConfirm = () => {\n    setOpen(false)\n    onConfirm?.()\n  }\n  const handleOnCancel = () => {\n    setOpen(false)\n    onCancel?.()\n  }\n\n  return (\n    <Dialog.Root open={open} onOpenChange={setOpen}>\n      <Dialog.Trigger asChild>{children}</Dialog.Trigger>\n\n      <Dialog.Outer>\n        <Dialog.Inner role=\"alertdialog\" className=\"max-w-[400px]!\">\n          <Dialog.Title className=\"text-xl font-semibold leading-snug\">\n            {title}\n          </Dialog.Title>\n          {description && (\n            <Dialog.Description className=\"text-text-light pt-1 leading-snug\">\n              {description}\n            </Dialog.Description>\n          )}\n          <div className=\"flex flex-wrap-reverse items-center gap-2 pt-4\">\n            <Button\n              className=\"w-full min-w-[150px]\"\n              color=\"secondary\"\n              onClick={handleOnCancel}\n            >\n              {cancelCTA || _(msg`Cancel`)}\n            </Button>\n            {confirmCTA && (\n              <Button\n                className=\"w-full min-w-[150px]\"\n                color=\"primary\"\n                onClick={handleOnConfirm}\n              >\n                {confirmCTA}\n              </Button>\n            )}\n          </div>\n          <Dialog.Close />\n        </Dialog.Inner>\n      </Dialog.Outer>\n    </Dialog.Root>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/Toast.tsx",
    "content": "import { Cross2Icon } from '@radix-ui/react-icons'\nimport * as ToastBase from '@radix-ui/react-toast'\nimport { clsx } from 'clsx'\nimport {\n  ReactNode,\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from 'react'\n\ntype Variant = 'success' | 'warning' | 'error'\n\ntype Toast = {\n  id: string\n  variant: Variant\n  title: string\n  duration?: number\n  dissmissable?: boolean\n}\n\ntype Context = {\n  show(toast: Omit<Toast, 'id'>): void\n}\n\nconst Context = createContext<Context>({\n  show: () => {},\n})\n\nconst borderColors: Record<Variant, string> = {\n  success: 'border-success-200 dark:border-success-900',\n  warning: 'border-warning-200 dark:border-warning-900',\n  error: 'border-error-200 dark:border-error-900',\n}\nconst bgColors: Record<Variant, string> = {\n  success: 'bg-success-50 dark:bg-success-975',\n  warning: 'bg-warning-100 dark:bg-warning-975',\n  error: 'bg-error-50 dark:bg-error-975',\n}\nconst titleColors: Record<Variant, string> = {\n  success: 'text-success-900 dark:text-success-400',\n  warning: 'text-warning-800 dark:text-warning-400',\n  error: 'text-error-800 dark:text-error-50',\n}\n\nconst getRandomId = () => {\n  const randomId = Math.random().toString(36).substring(2, 8)\n  return randomId\n}\n\nexport function Provider({ children }: { children: ReactNode }) {\n  const [toasts, setToasts] = useState<Toast[]>([])\n\n  const show = useCallback<Context['show']>(\n    (toast) => {\n      setToasts((prev) => [\n        ...prev,\n        {\n          ...toast,\n          id: getRandomId(),\n        },\n      ])\n    },\n    [setToasts],\n  )\n\n  const ctx = useMemo(\n    () => ({\n      show,\n    }),\n    [show],\n  )\n\n  return (\n    <ToastBase.Provider swipeDirection=\"up\">\n      <Context.Provider value={ctx}>\n        {children}\n\n        {toasts.map((toast) => (\n          <ToastBase.Root\n            key={toast.id}\n            duration={toast.duration}\n            onOpenChange={(open) => {\n              if (!open) {\n                setTimeout(() => {\n                  setToasts((prev) => prev.filter((t) => t.id !== toast.id))\n                }, 1e3)\n              }\n            }}\n            className={clsx(['ToastRoot', 'py-1'])}\n          >\n            <div\n              className={clsx([\n                'relative rounded-full border py-3 pl-6 pr-8 shadow-lg',\n                'shadow-contrast-900/15 dark:shadow-contrast-0/60',\n                borderColors[toast.variant],\n                bgColors[toast.variant],\n              ])}\n            >\n              <ToastBase.Title\n                className={clsx([\n                  'text-sm font-semibold leading-snug',\n                  titleColors[toast.variant],\n                ])}\n              >\n                {toast.title}\n              </ToastBase.Title>\n              {toast.dissmissable && (\n                <ToastBase.Close\n                  className={clsx([\n                    'absolute bottom-0 right-2 top-0 my-auto flex h-6 w-6 items-center justify-center rounded-full border transition-colors focus:outline-0',\n                    'border-contrast-975/30',\n                    'hover:bg-contrast-975/30 hover:border-contrast-975/60',\n                  ])}\n                >\n                  <Cross2Icon className=\"text-text-light hover:text-text-default focus:text-text-default\" />\n                </ToastBase.Close>\n              )}\n            </div>\n          </ToastBase.Root>\n        ))}\n      </Context.Provider>\n\n      <ToastBase.Viewport className=\"fixed left-6 right-6 top-0 mx-auto max-w-[400px] pt-8\" />\n    </ToastBase.Provider>\n  )\n}\n\nexport function useToast() {\n  return useContext(Context)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/Checkbox.tsx",
    "content": "import { clsx } from 'clsx'\n\nexport type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {\n  name: string\n  value: string\n  invalid?: boolean\n  disabled?: boolean\n}\n\nexport function Checkbox({ disabled, invalid, ...rest }: CheckboxProps) {\n  return (\n    <input\n      id={`checkbox-${rest.name}`}\n      type=\"checkbox\"\n      {...rest}\n      className={clsx([\n        'block h-5 w-5 rounded-md border-2 focus:shadow-sm focus:outline-none',\n        'border-contrast-200 focus:border-primary-500 focus:bg-contrast-25 dark:focus:bg-contrast-50 focus:shadow-primary-600/30',\n        invalid &&\n          'border-error-300 text-error-900 placeholder-error-300 focus:border-error-500',\n        disabled && 'bg-contrast-50 text-contrast-500 cursor-not-allowed',\n      ])}\n    />\n  )\n}\n\nfunction Label({\n  children,\n  name,\n}: {\n  children: React.ReactNode\n  name: string\n}) {\n  return (\n    <label htmlFor={`checkbox-${name}`} className=\"text-sm\">\n      {children}\n    </label>\n  )\n}\n\nCheckbox.Label = Label\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/Errors.tsx",
    "content": "import { StandardSchemaV1Issue } from '@tanstack/react-form'\nimport { ReactNode } from 'react'\n\nexport function Errors({\n  errors,\n}: {\n  errors: (StandardSchemaV1Issue | undefined)[]\n}) {\n  if (errors.length === 0) return null\n  return (\n    <ul className=\"space-y-1\">\n      {(errors.filter(Boolean) as StandardSchemaV1Issue[]).map((error, i) => (\n        <Error key={i}>{error.message}</Error>\n      ))}\n    </ul>\n  )\n}\n\nexport function Error({ children }: { children: ReactNode }) {\n  return (\n    <li className=\"text-error-900 dark:text-error-25 bg-error-100 dark:bg-error-800 space-y-1 rounded-md px-2 py-1 text-sm\">\n      {children}\n    </li>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/Fieldset.tsx",
    "content": "import { ReactNode } from 'react'\n\nexport function Fieldset({\n  children,\n  label,\n}: {\n  children: ReactNode\n  label: string\n}) {\n  return (\n    <fieldset className=\"space-y-3\">\n      <legend className=\"hidden\">{label}</legend>\n      {children}\n    </fieldset>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/Item.tsx",
    "content": "import { clsx } from 'clsx'\nimport { ReactNode } from 'react'\n\nexport function Item({\n  children,\n  className,\n}: {\n  children: ReactNode\n  className?: string\n}) {\n  return <div className={clsx('space-y-2', className)}>{children}</div>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/Label.tsx",
    "content": "import { clsx } from 'clsx'\nimport { ReactNode } from 'react'\n\nexport function Label({\n  children,\n  name,\n  hidden,\n}: {\n  children: ReactNode\n  name: string\n  hidden?: boolean\n}) {\n  return (\n    <label\n      htmlFor={`field-${name}`}\n      className={clsx([\n        'text-text-light block text-sm font-medium',\n        hidden && 'sr-only',\n      ])}\n    >\n      {children}\n    </label>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/Text.tsx",
    "content": "import { clsx } from 'clsx'\nimport { InputHTMLAttributes } from 'react'\n\nexport type TextProps = InputHTMLAttributes<HTMLInputElement> & {\n  name: string\n  value: string\n  invalid?: boolean\n  disabled?: boolean\n}\n\nexport function Text({ disabled, invalid, ...rest }: TextProps) {\n  return (\n    <input\n      {...rest}\n      id={`field-${rest.name}`}\n      disabled={disabled}\n      className={clsx([\n        'text-md block w-full rounded-md border-2 px-4 py-2.5 focus:shadow-sm focus:outline-none',\n        'border-contrast-200 focus:border-primary focus:bg-contrast-25 dark:focus:bg-contrast-50 focus:shadow-primary-600/30',\n        invalid &&\n          'border-error focus:border-error text-error placeholder-error focus:text-inherit',\n        disabled && 'bg-contrast-50 text-contrast-500 cursor-not-allowed',\n      ])}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/forms/index.tsx",
    "content": "export * from '#/components/forms/Fieldset'\nexport * from '#/components/forms/Item'\nexport * from '#/components/forms/Label'\nexport * from '#/components/forms/Text'\nexport * from '#/components/forms/Errors'\nexport * from '#/components/forms/Checkbox'\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/components/util/Palette.tsx",
    "content": "export function Palette() {\n  return (\n    <>\n      <div className=\"flex items-center justify-center\">\n        <div className=\"bg-contrast-0 p-10\" />\n        <div className=\"bg-contrast-25 p-10\" />\n        <div className=\"bg-contrast-50 p-10\" />\n        <div className=\"bg-contrast-100 p-10\" />\n        <div className=\"bg-contrast-200 p-10\" />\n        <div className=\"bg-contrast-300 p-10\" />\n        <div className=\"bg-contrast-400 p-10\" />\n        <div className=\"bg-contrast-500 p-10\" />\n        <div className=\"bg-contrast-600 p-10\" />\n        <div className=\"bg-contrast-700 p-10\" />\n        <div className=\"bg-contrast-800 p-10\" />\n        <div className=\"bg-contrast-900 p-10\" />\n        <div className=\"bg-contrast-950 p-10\" />\n        <div className=\"bg-contrast-975 p-10\" />\n        <div className=\"bg-contrast-1000 p-10\" />\n      </div>\n\n      <div className=\"flex items-center justify-center\">\n        <div className=\"bg-primary-25 p-10\" />\n        <div className=\"bg-primary-50 p-10\" />\n        <div className=\"bg-primary-100 p-10\" />\n        <div className=\"bg-primary-200 p-10\" />\n        <div className=\"bg-primary-300 p-10\" />\n        <div className=\"bg-primary-400 p-10\" />\n        <div className=\"bg-primary-500 p-10\" />\n        <div className=\"bg-primary-600 p-10\" />\n        <div className=\"bg-primary-700 p-10\" />\n        <div className=\"bg-primary-800 p-10\" />\n        <div className=\"bg-primary-900 p-10\" />\n        <div className=\"bg-primary-950 p-10\" />\n        <div className=\"bg-primary-975 p-10\" />\n      </div>\n\n      <div className=\"flex items-center justify-center\">\n        <div className=\"bg-error-25 p-10\" />\n        <div className=\"bg-error-50 p-10\" />\n        <div className=\"bg-error-100 p-10\" />\n        <div className=\"bg-error-200 p-10\" />\n        <div className=\"bg-error-300 p-10\" />\n        <div className=\"bg-error-400 p-10\" />\n        <div className=\"bg-error-500 p-10\" />\n        <div className=\"bg-error-600 p-10\" />\n        <div className=\"bg-error-700 p-10\" />\n        <div className=\"bg-error-800 p-10\" />\n        <div className=\"bg-error-900 p-10\" />\n        <div className=\"bg-error-950 p-10\" />\n        <div className=\"bg-error-975 p-10\" />\n      </div>\n\n      <div className=\"flex items-center justify-center\">\n        <div className=\"bg-warning-25 p-10\" />\n        <div className=\"bg-warning-50 p-10\" />\n        <div className=\"bg-warning-100 p-10\" />\n        <div className=\"bg-warning-200 p-10\" />\n        <div className=\"bg-warning-300 p-10\" />\n        <div className=\"bg-warning-400 p-10\" />\n        <div className=\"bg-warning-500 p-10\" />\n        <div className=\"bg-warning-600 p-10\" />\n        <div className=\"bg-warning-700 p-10\" />\n        <div className=\"bg-warning-800 p-10\" />\n        <div className=\"bg-warning-900 p-10\" />\n        <div className=\"bg-warning-950 p-10\" />\n        <div className=\"bg-warning-975 p-10\" />\n      </div>\n\n      <div className=\"flex items-center justify-center\">\n        <div className=\"bg-success-25 p-10\" />\n        <div className=\"bg-success-50 p-10\" />\n        <div className=\"bg-success-100 p-10\" />\n        <div className=\"bg-success-200 p-10\" />\n        <div className=\"bg-success-300 p-10\" />\n        <div className=\"bg-success-400 p-10\" />\n        <div className=\"bg-success-500 p-10\" />\n        <div className=\"bg-success-600 p-10\" />\n        <div className=\"bg-success-700 p-10\" />\n        <div className=\"bg-success-800 p-10\" />\n        <div className=\"bg-success-900 p-10\" />\n        <div className=\"bg-success-950 p-10\" />\n        <div className=\"bg-success-975 p-10\" />\n      </div>\n\n      <div className=\"flex items-center justify-center\">\n        <div className=\"bg-primary text-primary-contrast p-10\">Az</div>\n        <div className=\"bg-error text-error-contrast p-10\">Az</div>\n        <div className=\"bg-warning text-warning-contrast p-10\">Az</div>\n        <div className=\"bg-success text-success-contrast p-10\">Az</div>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useAccountSessionsQuery.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { ActiveAccountSession, useApi } from '#/api'\n\nexport type UseAccountSessionsQueryInput = {\n  sub: string\n}\n\nexport const accountSessionsQueryKey = ({\n  sub,\n}: UseAccountSessionsQueryInput) => ['account-sessions', sub] as const\n\nexport function useAccountSessionsQuery(input: UseAccountSessionsQueryInput) {\n  const api = useApi()\n\n  return useQuery<ActiveAccountSession[]>({\n    refetchOnWindowFocus: 'always',\n    staleTime: 15e3, // 15s\n    queryKey: accountSessionsQueryKey(input),\n    queryFn: async (options) => {\n      return api.fetch('GET', '/account-sessions', input, options)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useClientName.ts",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport {\n  OAuthClientMetadata,\n  isConventionalOAuthClientId,\n  isOAuthClientIdLoopback,\n} from '#/util/oauth-client'\n\nexport function useClientName({\n  clientId,\n  clientMetadata,\n}: {\n  clientId: string\n  clientMetadata?: OAuthClientMetadata\n}): string {\n  const { t } = useLingui()\n\n  if (isOAuthClientIdLoopback(clientId)) {\n    return t`A local app`\n  }\n\n  if (clientMetadata?.client_name) {\n    return clientMetadata.client_name\n  }\n\n  if (isConventionalOAuthClientId(clientId)) {\n    return new URL(clientId).hostname\n  }\n\n  // Should never happen\n  return clientId\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useCurrentSession.ts",
    "content": "import { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'\nimport { Route as AccountRoute } from '#/routes/account/_appLayout/$sub'\n\nexport function useCurrentSession() {\n  const { data: sessions } = useDeviceSessionsQuery()\n  const { sub } = AccountRoute.useParams()\n  const current = sessions?.find(({ account }) => account.sub === sub)\n\n  if (!current) {\n    throw new Error(\n      `No current account available. Are you sure you're using this hook in the right context?`,\n    )\n  }\n\n  return current\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useCustomizationData.ts",
    "content": "import { useHydrationData } from './useHydrationData'\n\nexport function useCustomizationData() {\n  return useHydrationData('__customizationData')\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useDeviceSessionsQuery.ts",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useCallback } from 'react'\nimport { ActiveDeviceSession, useApi } from '#/api'\nimport { upsert } from '#/util/upsert'\nimport { useHydrationData } from './useHydrationData'\n\nexport const useDeviceSessionsQueryKey = ['device-sessions'] as const\nexport type UseAccountsQueryResponse = ActiveDeviceSession[]\n\n/**\n * All accounts logged in on _this device_.\n */\nexport function useDeviceSessionsQuery() {\n  const api = useApi()\n\n  const initialData = useHydrationData('__deviceSessions')\n\n  return useQuery<ActiveDeviceSession[]>({\n    initialData: [...initialData],\n    refetchOnWindowFocus: 'always',\n    staleTime: 15e3, // 15s\n    queryKey: useDeviceSessionsQueryKey,\n    queryFn: async (options) => {\n      return api.fetch('GET', '/device-sessions', undefined, options)\n    },\n  })\n}\n\nexport function useUpsertDeviceAccount() {\n  const qc = useQueryClient()\n\n  return useCallback(\n    (newSession: ActiveDeviceSession) => {\n      return qc.setQueryData<ActiveDeviceSession[]>(\n        useDeviceSessionsQueryKey,\n        (data) =>\n          upsert(\n            data,\n            newSession,\n            (a) => a.account.sub === newSession.account.sub,\n          ),\n      )\n    },\n    [qc, ...useDeviceSessionsQueryKey],\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useFriendlyClientId.ts",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport {\n  isConventionalOAuthClientId,\n  isOAuthClientIdLoopback,\n} from '#/util/oauth-client'\n\nexport function useFriendlyClientId({\n  clientId,\n}: {\n  clientId: string\n}): string {\n  const { t } = useLingui()\n\n  if (isOAuthClientIdLoopback(clientId)) {\n    return t`loopback`\n  }\n\n  if (isConventionalOAuthClientId(clientId)) {\n    return new URL(clientId).hostname\n  }\n\n  // Should never happen\n  return clientId\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useHasAccounts.ts",
    "content": "import { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'\n\nexport function useHasAccounts() {\n  const { data: accounts } = useDeviceSessionsQuery()\n  return accounts?.length > 0\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useHydrationData.ts",
    "content": "import type { HydrationData } from '../hydration-data.d.ts'\n\nconst hydrationData = window as typeof window & HydrationData['account-page']\n\nexport function useHydrationData<T extends keyof HydrationData['account-page']>(\n  key: T,\n): HydrationData['account-page'][T] {\n  return hydrationData[key]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useOAuthSessionsQuery.ts",
    "content": "import { useQuery } from '@tanstack/react-query'\nimport { ActiveOAuthSession, useApi } from '#/api'\n\nexport type OAuthSessionsQueryInput = {\n  sub: string\n}\n\nexport const oauthSessionsQueryKey = ({ sub }: OAuthSessionsQueryInput) =>\n  ['oauth-sessions', sub] as const\n\nexport function useOAuthSessionsQuery(input: OAuthSessionsQueryInput) {\n  const api = useApi()\n  return useQuery<ActiveOAuthSession[]>({\n    refetchOnWindowFocus: 'always',\n    staleTime: 15e3, // 15s\n    queryKey: oauthSessionsQueryKey(input),\n    queryFn: async (options) => {\n      return await api.fetch('GET', '/oauth-sessions', input, options)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/usePasswordConfirmMutation.ts",
    "content": "import { useMutation } from '@tanstack/react-query'\nimport { ConfirmResetPasswordInput, useApi } from '#/api'\n\nexport type PasswordConfirmMutationInput = ConfirmResetPasswordInput\n\nexport function usePasswordConfirmMutation() {\n  const api = useApi()\n\n  return useMutation({\n    async mutationFn(input: ConfirmResetPasswordInput) {\n      await api.fetch('POST', '/reset-password-confirm', input)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/usePasswordResetMutation.ts",
    "content": "import { useMutation } from '@tanstack/react-query'\nimport { InitiatePasswordResetInput, useApi } from '#/api'\nimport { useLocale } from '#/locales'\n\nexport type PasswordResetMutationInput = Omit<\n  InitiatePasswordResetInput,\n  'locale'\n>\n\nexport function usePasswordResetMutation() {\n  const api = useApi()\n  const { locale } = useLocale()\n\n  return useMutation({\n    async mutationFn(input: PasswordResetMutationInput) {\n      await api.fetch('POST', '/reset-password-request', { ...input, locale })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useRevokeAccountSessionMutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { RevokeAccountSessionInput, useApi } from '#/api'\nimport { accountSessionsQueryKey } from '#/data/useAccountSessionsQuery'\nimport { useDeviceSessionsQueryKey } from '#/data/useDeviceSessionsQuery'\n\nexport function useRevokeAccountSessionMutation() {\n  const api = useApi()\n  const qc = useQueryClient()\n\n  return useMutation({\n    async mutationFn(input: RevokeAccountSessionInput) {\n      return api.fetch('POST', '/revoke-account-session', input)\n    },\n    onSuccess(_, input) {\n      qc.invalidateQueries({ queryKey: accountSessionsQueryKey(input) })\n      qc.invalidateQueries({ queryKey: useDeviceSessionsQueryKey })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useRevokeOAuthSessionMutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { RevokeOAuthSessionInput, useApi } from '#/api'\nimport { oauthSessionsQueryKey } from './useOAuthSessionsQuery'\n\nexport function useRevokeOAuthSessionMutation() {\n  const api = useApi()\n  const qc = useQueryClient()\n\n  return useMutation({\n    async mutationFn(input: RevokeOAuthSessionInput) {\n      await api.fetch('POST', '/revoke-oauth-session', input)\n    },\n    onError(error, input) {\n      qc.invalidateQueries({ queryKey: oauthSessionsQueryKey(input) })\n    },\n    onSuccess(_, input) {\n      qc.invalidateQueries({ queryKey: oauthSessionsQueryKey(input) })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useSignInMutation.tsx",
    "content": "import { useMutation } from '@tanstack/react-query'\nimport { SignInInput, useApi } from '#/api'\nimport { useUpsertDeviceAccount } from '#/data/useDeviceSessionsQuery'\nimport { useLocale } from '#/locales'\n\nexport type SignInMutationInput = Omit<SignInInput, 'locale'>\n\nexport function useSignInMutation() {\n  const api = useApi()\n  const { locale } = useLocale()\n\n  const upsertDeviceAccount = useUpsertDeviceAccount()\n\n  return useMutation({\n    async mutationFn(input: SignInMutationInput) {\n      const res = await api.fetch('POST', '/sign-in', { ...input, locale })\n\n      upsertDeviceAccount({\n        account: res.account,\n        loginRequired: false,\n      })\n\n      return res\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/data/useSignOutMutation.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { SignOutInput, useApi } from '#/api'\nimport { accountSessionsQueryKey } from '#/data/useAccountSessionsQuery'\nimport { useDeviceSessionsQueryKey } from '#/data/useDeviceSessionsQuery'\n\nexport function useSignOutMutation() {\n  const api = useApi()\n  const qc = useQueryClient()\n\n  return useMutation({\n    async mutationFn(input: SignOutInput) {\n      return api.fetch('POST', '/sign-out', input)\n    },\n    onSuccess(_, input) {\n      qc.invalidateQueries({ queryKey: useDeviceSessionsQueryKey })\n      const subs = Array.isArray(input.sub) ? input.sub : [input.sub]\n      for (const sub of subs) {\n        qc.invalidateQueries({ queryKey: accountSessionsQueryKey({ sub }) })\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/activateLocale.ts",
    "content": "import { i18n } from '@lingui/core'\nimport * as en from '#/locales/en/messages'\nimport { Locale } from './locales'\n\nexport async function activateLocale(locale: Locale) {\n  const { messages } = await import(`./${locale}/messages.ts`).catch((e) => {\n    console.error('Error loading locale', e)\n    return en\n  })\n\n  i18n.load(locale, messages)\n  i18n.activate(locale)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/an/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: an\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ast/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ast\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ca/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ca\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/da/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: da\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/de/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: de\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/el/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: el\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/en/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: en\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"@handle or email\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"← Back to accounts\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"A local app\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"A new password is required\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"Accounts\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"An error occurred, please try again.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"Are you sure you want to remove this device?\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"Back to sign in\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"Branding\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"Cancel\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"Click here to send a new code to your email.\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"Code\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"Code was resent\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"Connected apps\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"Credentials\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"Don't see the email? <0>Try sending again.</0>\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"Email code is required\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"Email is required\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"Enter a new password\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"Enter your email\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"Enter your email to receive a reset code.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"Failed to load connected apps\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"Failed to load devices\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"Failed to remove device\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"Failed to resend code\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"Failed to sign out\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"Forgot password?\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"Get reset code\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"Home\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"Identifier\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"Invalid email\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"Invalid handle\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"Invalid identifier or password.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"It appears that you haven’t used this account to sign in to any apps yet.\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"Logo\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"Looks like you aren't logged in on any other devices.\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"loopback\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"My devices\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"No connected apps\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"No devices\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"Password\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"Password is required\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"Privacy Policy\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"Remove this device\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"Reset password\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"Revoke access\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"Revoke access to {clientName}\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"Revoke access to this application\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"Select an account\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"Select the account you would like to manage.\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"Sign in\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"Sign in with another account\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"Sign out\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"Something went wrong\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"Success!\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"Successfully removed device\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"Successfully signed out\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"Support\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"Terms of Service\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"This device\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"This is a list of all the applications you have authorized to access your account.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"Unknown user agent\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"User avatar\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"View and manage account for {0}\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"We weren't able to load your accounts. Please refresh the page to try again.\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"Whoops! An error occurred.\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"XXXXX-XXXXX\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"Your account\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"Your password has been reset.\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/en-GB/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: en-GB\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/es/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: es\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/eu/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: eu\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/fi/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: fi\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/fr/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: fr\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"@identifiant ou adresse email\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"← Retour à la liste des comptes\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"Une application anonyme\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"Un nouveau mot de passe est requis\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"Utilisateurs\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"Une erreur s'est produite, veuillez réessayer.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"Êtes-vous sûr de vouloir déconnecter cet appareil ?\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"Êtes-vous sûr de vouloir supprimer l'accès? Cette application ne sera plus en mesure d'accéder à vos informations.\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"Retour vers la page de connexion\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"Personnalisation\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"Annuler\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"Cliquez ici pour envoyer un nouveau code dans votre boîte mail.\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"Code\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"Un nouveau code a été envoyé\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"Applications connectées\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"Identifiants\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"Vous ne voyez pas l'email ? <0>Essayez d'envoyer à nouveau.</0>\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"Veuillez entrer le code de vérification envoyé par email\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"L'adresse email est requise\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"Entrez un nouveau mot de passe\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"Entrez votre adresse email\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"Entrez votre adresse email pour recevoir un code de réinitialisation.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"Échec du chargement des applications connectées\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"Échec du chargement des appareils\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"Échec de la suppression de l'appareil\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"Échec de l'envoi du code par email\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"Échec de la déconnexion\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"Mot de passe oublié ?\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"Obtenir le code de réinitialisation\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"Accueil\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"Identifiant\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"Adresse email invalide\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"Identifiant invalide\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"Identifiant ou mot de passe invalide.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"Il semble que vous n'ayez pas encore utilisé ce compte pour vous connecter à une application.\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"Logo\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"Votre compte n'est utilisé sur aucun autre appareil.\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"loopback\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"Mes appareils\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"Aucune application connectée\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"Aucun appareil\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"Mot de passe\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"Le mot de passe est requis\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"Le mot de passe doit contenir au moins {MIN_PASSWORD_LENGTH} caractères\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"Politique de confidentialité\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"Déconnecter cet appareil\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"Réinitialiser le mot de passe\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"Révoquer l'accès\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"Révoquer l'accès à {clientName}\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"Révoquer l'accès à cette application\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"Sélectionner un compte\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"Sélectionnez le compte que vous souhaitez gérer.\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"Se connecter\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"Se connecter avec un autre compte\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"Déconnecter\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"Une erreur s'est produite\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"Succès !\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"Déconnexion de l'appareil réussie\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"Déconnexion réussie\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"Support\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"Conditions d'utilisation\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"Cet appareil\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"Ceci est une liste de toutes les applications que vous avez autorisées à accéder à votre compte.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"Ceci est une liste de tous les appareils que vous avez utilisés pour vous connecter à votre compte. De nouvelles applications peuvent être autorisées depuis n'importe lequel de ces appareils. Si vous pensez que votre compte a été compromis, nous vous recommandons de révoquer l'accès à tous les appareils.\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"Appareil inconnu\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"Avatar de l'utilisateur\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"Afficher et gérer le compte de {0}\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"Nous n'avons pas pu charger vos comptes. Veuillez réactualiser la page pour réessayer.\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"Oups ! Une erreur s'est produite.\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"XXXXX-XXXXX\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"Votre compte\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"Votre mot de passe a été réinitialisé.\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ga/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ga\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/gl/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: gl\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/hi/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: hi\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/hu/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: hu\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ia/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ia\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/id/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: id\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/index.tsx",
    "content": "import {\n  ReactNode,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { activateLocale } from '#/locales/activateLocale'\nimport { Locale, detectLocale, locales } from './locales'\n\nconst Context = createContext<{\n  locale: Locale\n  setLocale: (locale: Locale) => void\n}>({\n  locale: 'en',\n  setLocale: () => {},\n})\n\nexport function Provider({ children }: { children: ReactNode }) {\n  const prevLocale = useRef<Locale>('en')\n  const [locale, setLocale] = useState<Locale>(detectLocale)\n\n  useEffect(() => {\n    if (prevLocale.current !== locale) {\n      activateLocale(locale)\n        .then(() => {\n          prevLocale.current = locale\n        })\n        .catch((e) => {\n          console.error(e)\n          setLocale(prevLocale.current)\n        })\n    }\n  }, [locale, setLocale])\n\n  const safeSetLocale = useCallback(\n    (locale: Locale) => {\n      if (locale in locales) {\n        setLocale(locale)\n      } else {\n        throw new Error(`Unsupported locale: ${locale}`)\n      }\n    },\n    [setLocale],\n  )\n\n  const ctx = useMemo(\n    () => ({\n      locale,\n      setLocale: safeSetLocale,\n    }),\n    [locale, safeSetLocale],\n  )\n\n  return <Context.Provider value={ctx}>{children}</Context.Provider>\n}\n\nexport function useLocale() {\n  return useContext(Context)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/it/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: it\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ja/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ja\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2025-09-09 06:43+0900\\n\"\n\"Last-Translator: dolciss\\n\"\n\"Language-Team: Japanese\\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"@ハンドルまたはメールアドレス\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"← アカウントに戻る\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"ローカルアプリ\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"新しいパスワードが必要です\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"アカウント\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"エラーが発生しました、もう一度お試しください。\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"本当にこのデバイスを削除しますか？\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"本当にこのデバイスを削除しますか？このアプリケーションは、もうあなたのアカウントにアクセスできなくなります。\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"サインインに戻る\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"ブランド\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"キャンセル\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"ここをクリックして、新しいコードをメールで送信します。\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"コード\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"コード再送済\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"接続済アプリ\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"認証情報\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"メールが見つかりませんか？<0>再送信を試みてください。</0>\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"メールアドレス\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"メールの確認コードが必要\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"メールアドレスが必要\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"新しいパスワードを入力\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"メールアドレスを入力\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"リセットコードを受信するには、メールアドレスを入力してください。\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"接続済アプリの読み込みに失敗しました\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"デバイスの読み込みに失敗しました\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"デバイスの削除に失敗しました\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"確認コードの再送に失敗しました\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"サインアウトに失敗しました\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"パスワードを忘れた？\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"リセットコードを取得\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"ホーム\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"識別子\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"無効なメールアドレス\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"無効なハンドル\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"無効な識別子またはパスワード\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"このアカウントを使用して、まだアプリにサインインしていないようです。\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"ロゴ\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"あなたは他のデバイスにログインしていないようです。\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"ループバック\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"デバイス\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"接続済アプリなし\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"デバイスなし\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"パスワード\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"パスワードが必要\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"パスワードは {MIN_PASSWORD_LENGTH} 文字以上でなければなりません\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"プライバシーポリシー\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"このデバイスを削除\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"パスワードをリセット\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"アクセスを取り消す\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"{clientName} へのアクセスを取り消す\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"このアプリへのアクセスを取り消す\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"アカウントを選択\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"管理したいアカウントを選択してください。\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"サインイン\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"他のアカウントでサインイン\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"サインアウト\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"何らかの問題が発生したようです\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"成功！\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"デバイスの削除に成功しました\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"サインアウトに成功しました\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"サポート\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"利用規約\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"このデバイス\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"このリストは、あなたのアカウントへのアクセスを許可したすべてのアプリケーションの一覧です。\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"このリストは、あなたのアカウントにサインインするために使用したすべてのデバイスの一覧です。これらのデバイスのいずれかから新しいアプリを認証できます。アカウントが侵害されたと思われる場合は、すべてのデバイスへのアクセスを取り消すことをお勧めします。\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"不明なユーザーエージェント\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"ユーザーのアバター\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"{0} のアカウントを表示および管理\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"アカウントを読み込めませんでした。ページを更新してもう一度お試しください。\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"おっと！エラーが発生しました。\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"XXXXX-XXXXX\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"あなたのアカウント\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"あなたのパスワードはリセットされました。\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/km/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: km\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ko/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ko\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/locales.ts",
    "content": "// @TODO Enable locales once they get translated\nexport const locales = {\n  // an: {\n  //   name: 'Aragonés',\n  // },\n  // ast: {\n  //   name: 'Asturianu',\n  // },\n  // ca: {\n  //   name: 'Català',\n  //   flag: '🇦🇩', // Andorra's flag (though Andorra does not cover the entire zone speaking Català)\n  // },\n  // da: {\n  //   name: 'Dansk',\n  //   flag: '🇩🇰',\n  // },\n  // de: {\n  //   name: 'Deutsch',\n  //   flag: '🇩🇪',\n  // },\n  // el: {\n  //   name: 'Ελληνικά',\n  //   flag: '🇬🇷',\n  // },\n  en: {\n    name: 'English',\n    flag: '🇺🇸',\n  },\n  // 'en-GB': {\n  //   name: 'English (UK)',\n  //   flag: '🇬🇧',\n  // },\n  // es: {\n  //   name: 'Español',\n  //   flag: '🇪🇸',\n  // },\n  // eu: {\n  //   name: 'Euskara',\n  // },\n  // fi: {\n  //   name: 'Suomi',\n  //   flag: '🇫🇮',\n  // },\n  fr: {\n    name: 'Français',\n    flag: '🇫🇷',\n  },\n  // ga: {\n  //   name: 'Gaeilge',\n  //   flag: '🇮🇪',\n  // },\n  // gl: {\n  //   name: 'Galego',\n  // },\n  // hi: {\n  //   name: 'हिन्दी',\n  //   flag: '🇮🇳',\n  // },\n  // hu: {\n  //   name: 'Magyar',\n  //   flag: '🇭🇺',\n  // },\n  // ia: {\n  //   name: 'Interlingua',\n  // },\n  // id: {\n  //   name: 'Bahasa Indonesia',\n  //   flag: '🇮🇩',\n  // },\n  // it: {\n  //   name: 'Italiano',\n  //   flag: '🇮🇹',\n  // },\n  ja: {\n    name: '日本語',\n    flag: '🇯🇵',\n  },\n  // km: {\n  //   name: 'ភាសាខ្មែរ',\n  //   flag: '🇰🇭',\n  // },\n  // ko: {\n  //   name: '한국어',\n  //   flag: '🇰🇷',\n  // },\n  // ne: {\n  //   name: 'नेपाली',\n  //   flag: '🇳🇵',\n  // },\n  // nl: {\n  //   name: 'Nederlands',\n  //   flag: '🇳🇱',\n  // },\n  // pl: {\n  //   name: 'Polski',\n  //   flag: '🇵🇱',\n  // },\n  // 'pt-BR': {\n  //   name: 'Português (Brasil)',\n  //   flag: '🇧🇷',\n  // },\n  // ro: {\n  //   name: 'Română',\n  //   flag: '🇷🇴',\n  // },\n  // ru: {\n  //   name: 'Русский',\n  //   flag: '🇷🇺',\n  // },\n  // sv: {\n  //   name: 'Svenska',\n  //   flag: '🇸🇪',\n  // },\n  // th: {\n  //   name: 'ไทย',\n  //   flag: '🇹🇭',\n  // },\n  // tr: {\n  //   name: 'Türkçe',\n  //   flag: '🇹🇷',\n  // },\n  // uk: {\n  //   name: 'Українська',\n  //   flag: '🇺🇦',\n  // },\n  // vi: {\n  //   name: 'Tiếng Việt',\n  //   flag: '🇻🇳',\n  // },\n  // 'zh-CN': {\n  //   name: '中文(简体)',\n  //   flag: '🇨🇳',\n  // },\n  // 'zh-HK': {\n  //   name: '中文(香港)',\n  //   flag: '🇭🇰',\n  // },\n  // 'zh-TW': {\n  //   name: '中文(繁體)',\n  //   flag: '🇹🇼',\n  // },\n} as const satisfies Record<string, { name: string; flag?: string }>\n\nexport type Locale = keyof typeof locales\n\nexport function isLocale(v: unknown): v is Locale {\n  return typeof v === 'string' && Object.hasOwn(locales, v)\n}\n\nexport function asLocale(locale: string): Locale | undefined {\n  if (isLocale(locale)) {\n    return locale\n  }\n\n  // Resolve similar locales (e.g. \"fr-BE\" -> \"fr\")\n  const lang = locale.split('-')[0]\n  if (isLocale(lang)) {\n    return lang\n  }\n\n  // Resolve similar locals (e.g. \"pt-PT\" -> \"pt-BR\")\n  for (const locale in locales) {\n    if (locale.startsWith(`${lang}-`)) {\n      return locale as keyof typeof locales\n    }\n  }\n\n  return undefined\n}\n\nexport function detectLocale(userLocales: readonly string[] = []): Locale {\n  for (const locale of userLocales) {\n    const resolved = asLocale(locale)\n    if (resolved) return resolved\n  }\n\n  for (const locale of navigator.languages) {\n    const resolved = asLocale(locale)\n    if (resolved) return resolved\n  }\n\n  return 'en'\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ne/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ne\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/nl/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: nl\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/pl/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: pl\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/pt-BR/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: pt-BR\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ro/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ro\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/ru/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ru\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/setup.ts",
    "content": "import { i18n } from '@lingui/core'\nimport { messages } from './en/messages'\n\ni18n.load('en', messages)\ni18n.activate('en')\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/sv/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: sv\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/th/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: th\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/tr/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: tr\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/uk/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: uk\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/vi/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: vi\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/zh-CN/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh-CN\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/zh-HK/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh-HK\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/locales/zh-TW/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-03-19 13:49-0500\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh-TW\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:162\nmsgid \"@handle or email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:51\nmsgid \"← Back to accounts\"\nmsgstr \"\"\n\n#: src/data/useClientName.ts:18\nmsgid \"A local app\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:76\nmsgid \"A new password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:26\n#: src/routes/account/_minimalLayout/index.tsx:75\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:127\n#: src/routes/account/_minimalLayout/reset-password.tsx:62\n#: src/routes/account/_minimalLayout/reset-password.tsx:89\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:303\nmsgid \"Are you sure you want to remove this device?\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:201\nmsgid \"Are you sure you want to revoke access? This application won't be able to access your account anymore.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:133\n#: src/routes/account/_minimalLayout/reset-password.tsx:212\nmsgid \"Back to sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/branding.tsx:14\nmsgid \"Branding\"\nmsgstr \"\"\n\n#: src/components/Prompt.tsx:56\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:345\nmsgid \"Click here to send a new code to your email.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:204\n#: src/routes/account/_minimalLayout/reset-password.tsx:239\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:99\nmsgid \"Code was resent\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:63\nmsgid \"Connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:145\nmsgid \"Credentials\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:340\nmsgid \"Don't see the email? <0>Try sending again.</0>\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:160\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:73\nmsgid \"Email code is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:53\nmsgid \"Email is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:290\nmsgid \"Enter a new password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:171\nmsgid \"Enter your email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:143\nmsgid \"Enter your email to receive a reset code.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:78\nmsgid \"Failed to load connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:118\nmsgid \"Failed to load devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:250\nmsgid \"Failed to remove device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:105\nmsgid \"Failed to resend code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:172\nmsgid \"Failed to sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:250\nmsgid \"Forgot password?\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:203\nmsgid \"Get reset code\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:53\n#: src/components/Footer.tsx:70\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:152\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:52\nmsgid \"Invalid email\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:85\nmsgid \"Invalid handle\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:125\nmsgid \"Invalid identifier or password.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:95\nmsgid \"It appears that you haven’t used this account to sign in to any apps yet.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout.tsx:21\n#: src/components/Nav.tsx:18\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:134\nmsgid \"Looks like you aren't logged in on any other devices.\"\nmsgstr \"\"\n\n#: src/data/useFriendlyClientId.ts:15\nmsgid \"loopback\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:101\nmsgid \"My devices\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:93\nmsgid \"No connected apps\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:133\nmsgid \"No devices\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:177\n#: src/routes/account/_minimalLayout/sign-in.tsx:187\n#: src/routes/account/_minimalLayout/reset-password.tsx:278\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:90\nmsgid \"Password is required\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:79\nmsgid \"Password must be at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:71\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:302\nmsgid \"Remove this device\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:29\n#: src/routes/account/_minimalLayout/reset-password.tsx:140\n#: src/routes/account/_minimalLayout/reset-password.tsx:334\nmsgid \"Reset password\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:203\n#: src/routes/account/_appLayout/$sub.tsx:208\nmsgid \"Revoke access\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:197\nmsgid \"Revoke access to {clientName}\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:198\nmsgid \"Revoke access to this application\"\nmsgstr \"\"\n\n#: src/components/AccountSelector.tsx:32\nmsgid \"Select an account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:78\nmsgid \"Select the account you would like to manage.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:32\n#: src/routes/account/_minimalLayout/sign-in.tsx:136\n#: src/routes/account/_minimalLayout/sign-in.tsx:241\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:123\nmsgid \"Sign in with another account\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:304\n#: src/routes/account/_appLayout/$sub.tsx:314\n#: src/components/AccountSelector.tsx:103\nmsgid \"Sign out\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:50\nmsgid \"Something went wrong\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:126\nmsgid \"Success!\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:244\nmsgid \"Successfully removed device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:166\nmsgid \"Successfully signed out\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:73\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/components/Footer.tsx:72\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:298\nmsgid \"This device\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:67\nmsgid \"This is a list of all the applications you have authorized to access your account.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:105\nmsgid \"This is a list of all the devices you have used to sign in to your account. New apps can be authorized from any of these devices. If you believe that your account has been compromised, we recommend that you revoke access to all devices.\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:283\nmsgid \"Unknown user agent\"\nmsgstr \"\"\n\n#: src/components/Avatar.tsx:24\nmsgid \"User avatar\"\nmsgstr \"\"\n\n#. placeholder {0}: getAccountName(account)\n#: src/routes/account/_minimalLayout/index.tsx:98\nmsgid \"View and manage account for {0}\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/index.tsx:52\nmsgid \"We weren't able to load your accounts. Please refresh the page to try again.\"\nmsgstr \"\"\n\n#: src/components/ErrorScreen.tsx:19\nmsgid \"Whoops! An error occurred.\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/sign-in.tsx:213\n#: src/routes/account/_minimalLayout/reset-password.tsx:249\nmsgid \"XXXXX-XXXXX\"\nmsgstr \"\"\n\n#: src/routes/account/_appLayout/$sub.tsx:32\n#: src/routes/account/_appLayout/$sub.tsx:58\nmsgid \"Your account\"\nmsgstr \"\"\n\n#: src/routes/account/_minimalLayout/reset-password.tsx:129\nmsgid \"Your password has been reset.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routeTree.gen.ts",
    "content": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { createFileRoute } from '@tanstack/react-router'\n\n// Import Routes\n\nimport { Route as rootRoute } from './routes/__root'\nimport { Route as AccountMinimalLayoutImport } from './routes/account/_minimalLayout'\nimport { Route as AccountAppLayoutImport } from './routes/account/_appLayout'\nimport { Route as AccountMinimalLayoutIndexImport } from './routes/account/_minimalLayout/index'\nimport { Route as AccountMinimalLayoutSignInImport } from './routes/account/_minimalLayout/sign-in'\nimport { Route as AccountMinimalLayoutResetPasswordImport } from './routes/account/_minimalLayout/reset-password'\nimport { Route as AccountMinimalLayoutBrandingImport } from './routes/account/_minimalLayout/branding'\nimport { Route as AccountAppLayoutSubImport } from './routes/account/_appLayout/$sub'\n\n// Create Virtual Routes\n\nconst AccountImport = createFileRoute('/account')()\n\n// Create/Update Routes\n\nconst AccountRoute = AccountImport.update({\n  id: '/account',\n  path: '/account',\n  getParentRoute: () => rootRoute,\n} as any)\n\nconst AccountMinimalLayoutRoute = AccountMinimalLayoutImport.update({\n  id: '/_minimalLayout',\n  getParentRoute: () => AccountRoute,\n} as any)\n\nconst AccountAppLayoutRoute = AccountAppLayoutImport.update({\n  id: '/_appLayout',\n  getParentRoute: () => AccountRoute,\n} as any)\n\nconst AccountMinimalLayoutIndexRoute = AccountMinimalLayoutIndexImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => AccountMinimalLayoutRoute,\n} as any)\n\nconst AccountMinimalLayoutSignInRoute = AccountMinimalLayoutSignInImport.update(\n  {\n    id: '/sign-in',\n    path: '/sign-in',\n    getParentRoute: () => AccountMinimalLayoutRoute,\n  } as any,\n)\n\nconst AccountMinimalLayoutResetPasswordRoute =\n  AccountMinimalLayoutResetPasswordImport.update({\n    id: '/reset-password',\n    path: '/reset-password',\n    getParentRoute: () => AccountMinimalLayoutRoute,\n  } as any)\n\nconst AccountMinimalLayoutBrandingRoute =\n  AccountMinimalLayoutBrandingImport.update({\n    id: '/branding',\n    path: '/branding',\n    getParentRoute: () => AccountMinimalLayoutRoute,\n  } as any)\n\nconst AccountAppLayoutSubRoute = AccountAppLayoutSubImport.update({\n  id: '/$sub',\n  path: '/$sub',\n  getParentRoute: () => AccountAppLayoutRoute,\n} as any)\n\n// Populate the FileRoutesByPath interface\n\ndeclare module '@tanstack/react-router' {\n  interface FileRoutesByPath {\n    '/account': {\n      id: '/account'\n      path: '/account'\n      fullPath: '/account'\n      preLoaderRoute: typeof AccountImport\n      parentRoute: typeof rootRoute\n    }\n    '/account/_appLayout': {\n      id: '/account/_appLayout'\n      path: '/account'\n      fullPath: '/account'\n      preLoaderRoute: typeof AccountAppLayoutImport\n      parentRoute: typeof AccountRoute\n    }\n    '/account/_minimalLayout': {\n      id: '/account/_minimalLayout'\n      path: ''\n      fullPath: '/account'\n      preLoaderRoute: typeof AccountMinimalLayoutImport\n      parentRoute: typeof AccountImport\n    }\n    '/account/_appLayout/$sub': {\n      id: '/account/_appLayout/$sub'\n      path: '/$sub'\n      fullPath: '/account/$sub'\n      preLoaderRoute: typeof AccountAppLayoutSubImport\n      parentRoute: typeof AccountAppLayoutImport\n    }\n    '/account/_minimalLayout/branding': {\n      id: '/account/_minimalLayout/branding'\n      path: '/branding'\n      fullPath: '/account/branding'\n      preLoaderRoute: typeof AccountMinimalLayoutBrandingImport\n      parentRoute: typeof AccountMinimalLayoutImport\n    }\n    '/account/_minimalLayout/reset-password': {\n      id: '/account/_minimalLayout/reset-password'\n      path: '/reset-password'\n      fullPath: '/account/reset-password'\n      preLoaderRoute: typeof AccountMinimalLayoutResetPasswordImport\n      parentRoute: typeof AccountMinimalLayoutImport\n    }\n    '/account/_minimalLayout/sign-in': {\n      id: '/account/_minimalLayout/sign-in'\n      path: '/sign-in'\n      fullPath: '/account/sign-in'\n      preLoaderRoute: typeof AccountMinimalLayoutSignInImport\n      parentRoute: typeof AccountMinimalLayoutImport\n    }\n    '/account/_minimalLayout/': {\n      id: '/account/_minimalLayout/'\n      path: '/'\n      fullPath: '/account/'\n      preLoaderRoute: typeof AccountMinimalLayoutIndexImport\n      parentRoute: typeof AccountMinimalLayoutImport\n    }\n  }\n}\n\n// Create and export the route tree\n\ninterface AccountAppLayoutRouteChildren {\n  AccountAppLayoutSubRoute: typeof AccountAppLayoutSubRoute\n}\n\nconst AccountAppLayoutRouteChildren: AccountAppLayoutRouteChildren = {\n  AccountAppLayoutSubRoute: AccountAppLayoutSubRoute,\n}\n\nconst AccountAppLayoutRouteWithChildren =\n  AccountAppLayoutRoute._addFileChildren(AccountAppLayoutRouteChildren)\n\ninterface AccountMinimalLayoutRouteChildren {\n  AccountMinimalLayoutBrandingRoute: typeof AccountMinimalLayoutBrandingRoute\n  AccountMinimalLayoutResetPasswordRoute: typeof AccountMinimalLayoutResetPasswordRoute\n  AccountMinimalLayoutSignInRoute: typeof AccountMinimalLayoutSignInRoute\n  AccountMinimalLayoutIndexRoute: typeof AccountMinimalLayoutIndexRoute\n}\n\nconst AccountMinimalLayoutRouteChildren: AccountMinimalLayoutRouteChildren = {\n  AccountMinimalLayoutBrandingRoute: AccountMinimalLayoutBrandingRoute,\n  AccountMinimalLayoutResetPasswordRoute:\n    AccountMinimalLayoutResetPasswordRoute,\n  AccountMinimalLayoutSignInRoute: AccountMinimalLayoutSignInRoute,\n  AccountMinimalLayoutIndexRoute: AccountMinimalLayoutIndexRoute,\n}\n\nconst AccountMinimalLayoutRouteWithChildren =\n  AccountMinimalLayoutRoute._addFileChildren(AccountMinimalLayoutRouteChildren)\n\ninterface AccountRouteChildren {\n  AccountAppLayoutRoute: typeof AccountAppLayoutRouteWithChildren\n  AccountMinimalLayoutRoute: typeof AccountMinimalLayoutRouteWithChildren\n}\n\nconst AccountRouteChildren: AccountRouteChildren = {\n  AccountAppLayoutRoute: AccountAppLayoutRouteWithChildren,\n  AccountMinimalLayoutRoute: AccountMinimalLayoutRouteWithChildren,\n}\n\nconst AccountRouteWithChildren =\n  AccountRoute._addFileChildren(AccountRouteChildren)\n\nexport interface FileRoutesByFullPath {\n  '/account': typeof AccountMinimalLayoutRouteWithChildren\n  '/account/$sub': typeof AccountAppLayoutSubRoute\n  '/account/branding': typeof AccountMinimalLayoutBrandingRoute\n  '/account/reset-password': typeof AccountMinimalLayoutResetPasswordRoute\n  '/account/sign-in': typeof AccountMinimalLayoutSignInRoute\n  '/account/': typeof AccountMinimalLayoutIndexRoute\n}\n\nexport interface FileRoutesByTo {\n  '/account': typeof AccountMinimalLayoutIndexRoute\n  '/account/$sub': typeof AccountAppLayoutSubRoute\n  '/account/branding': typeof AccountMinimalLayoutBrandingRoute\n  '/account/reset-password': typeof AccountMinimalLayoutResetPasswordRoute\n  '/account/sign-in': typeof AccountMinimalLayoutSignInRoute\n}\n\nexport interface FileRoutesById {\n  __root__: typeof rootRoute\n  '/account': typeof AccountRouteWithChildren\n  '/account/_appLayout': typeof AccountAppLayoutRouteWithChildren\n  '/account/_minimalLayout': typeof AccountMinimalLayoutRouteWithChildren\n  '/account/_appLayout/$sub': typeof AccountAppLayoutSubRoute\n  '/account/_minimalLayout/branding': typeof AccountMinimalLayoutBrandingRoute\n  '/account/_minimalLayout/reset-password': typeof AccountMinimalLayoutResetPasswordRoute\n  '/account/_minimalLayout/sign-in': typeof AccountMinimalLayoutSignInRoute\n  '/account/_minimalLayout/': typeof AccountMinimalLayoutIndexRoute\n}\n\nexport interface FileRouteTypes {\n  fileRoutesByFullPath: FileRoutesByFullPath\n  fullPaths:\n    | '/account'\n    | '/account/$sub'\n    | '/account/branding'\n    | '/account/reset-password'\n    | '/account/sign-in'\n    | '/account/'\n  fileRoutesByTo: FileRoutesByTo\n  to:\n    | '/account'\n    | '/account/$sub'\n    | '/account/branding'\n    | '/account/reset-password'\n    | '/account/sign-in'\n  id:\n    | '__root__'\n    | '/account'\n    | '/account/_appLayout'\n    | '/account/_minimalLayout'\n    | '/account/_appLayout/$sub'\n    | '/account/_minimalLayout/branding'\n    | '/account/_minimalLayout/reset-password'\n    | '/account/_minimalLayout/sign-in'\n    | '/account/_minimalLayout/'\n  fileRoutesById: FileRoutesById\n}\n\nexport interface RootRouteChildren {\n  AccountRoute: typeof AccountRouteWithChildren\n}\n\nconst rootRouteChildren: RootRouteChildren = {\n  AccountRoute: AccountRouteWithChildren,\n}\n\nexport const routeTree = rootRoute\n  ._addFileChildren(rootRouteChildren)\n  ._addFileTypes<FileRouteTypes>()\n\n/* ROUTE_MANIFEST_START\n{\n  \"routes\": {\n    \"__root__\": {\n      \"filePath\": \"__root.tsx\",\n      \"children\": [\n        \"/account\"\n      ]\n    },\n    \"/account\": {\n      \"filePath\": \"account\",\n      \"children\": [\n        \"/account/_appLayout\",\n        \"/account/_minimalLayout\"\n      ]\n    },\n    \"/account/_appLayout\": {\n      \"filePath\": \"account/_appLayout.tsx\",\n      \"parent\": \"/account\",\n      \"children\": [\n        \"/account/_appLayout/$sub\"\n      ]\n    },\n    \"/account/_minimalLayout\": {\n      \"filePath\": \"account/_minimalLayout.tsx\",\n      \"parent\": \"/account\",\n      \"children\": [\n        \"/account/_minimalLayout/branding\",\n        \"/account/_minimalLayout/reset-password\",\n        \"/account/_minimalLayout/sign-in\",\n        \"/account/_minimalLayout/\"\n      ]\n    },\n    \"/account/_appLayout/$sub\": {\n      \"filePath\": \"account/_appLayout/$sub.tsx\",\n      \"parent\": \"/account/_appLayout\"\n    },\n    \"/account/_minimalLayout/branding\": {\n      \"filePath\": \"account/_minimalLayout/branding.tsx\",\n      \"parent\": \"/account/_minimalLayout\"\n    },\n    \"/account/_minimalLayout/reset-password\": {\n      \"filePath\": \"account/_minimalLayout/reset-password.tsx\",\n      \"parent\": \"/account/_minimalLayout\"\n    },\n    \"/account/_minimalLayout/sign-in\": {\n      \"filePath\": \"account/_minimalLayout/sign-in.tsx\",\n      \"parent\": \"/account/_minimalLayout\"\n    },\n    \"/account/_minimalLayout/\": {\n      \"filePath\": \"account/_minimalLayout/index.tsx\",\n      \"parent\": \"/account/_minimalLayout\"\n    }\n  }\n}\nROUTE_MANIFEST_END */\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/__root.tsx",
    "content": "import { HeadContent, Outlet, createRootRoute } from '@tanstack/react-router'\nimport { RouterErrorComponent } from '#/components/ErrorScreen'\nimport { Footer } from '#/components/Footer'\nimport * as Layout from '#/components/Layout'\n\nexport const Route = createRootRoute({\n  component: Root,\n  errorComponent: RouterErrorComponent,\n})\n\nfunction Root() {\n  return (\n    <>\n      <HeadContent />\n\n      <Layout.Outer>\n        <Outlet />\n      </Layout.Outer>\n\n      <Footer />\n\n      {/* <TanStackRouterDevtools /> */}\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_appLayout/$sub.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { Trans } from '@lingui/react/macro'\nimport { Cross2Icon, ExitIcon } from '@radix-ui/react-icons'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { useMemo } from 'react'\nimport { UAParser } from 'ua-parser-js'\nimport { ActiveAccountSession, ActiveOAuthSession } from '#/api'\nimport * as Admonition from '#/components/Admonition'\nimport { Avatar } from '#/components/Avatar'\nimport { Button } from '#/components/Button'\nimport { InlineLink } from '#/components/Link'\nimport { Loader } from '#/components/Loader'\nimport { Prompt } from '#/components/Prompt'\nimport { useToast } from '#/components/Toast'\nimport { useAccountSessionsQuery } from '#/data/useAccountSessionsQuery'\nimport { useClientName } from '#/data/useClientName'\nimport { useFriendlyClientId } from '#/data/useFriendlyClientId'\nimport { useOAuthSessionsQuery } from '#/data/useOAuthSessionsQuery'\nimport { useRevokeAccountSessionMutation } from '#/data/useRevokeAccountSessionMutation'\nimport { useRevokeOAuthSessionMutation } from '#/data/useRevokeOAuthSessionMutation'\n\nexport const Route = createFileRoute('/account/_appLayout/$sub')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { _ } = useLingui()\n\n  return (\n    <>\n      <title>{_(msg`Your account`)}</title>\n      <AccountHome />\n    </>\n  )\n}\n\nexport function AccountHome() {\n  const { _ } = useLingui()\n  const { sub } = Route.useParams()\n  const { data: sessions, error, isLoading } = useOAuthSessionsQuery({ sub })\n  const {\n    data: accountSessions,\n    error: accountSessionsError,\n    isLoading: accountSessionsIsLoading,\n  } = useAccountSessionsQuery({ sub })\n\n  return (\n    <>\n      <ul className=\"text-text-light flex items-center space-x-2 text-sm\">\n        <li>\n          <InlineLink to=\"/account\" className=\"text-text-light underline\">\n            <Trans>Home</Trans>\n          </InlineLink>\n        </li>\n        <li className=\"text-custom-primary\">/</li>\n        <li>\n          <Trans>Your account</Trans>\n        </li>\n      </ul>\n\n      <h2 className=\"text-custom-primary text-primary pb-4 pt-8 text-xl font-bold\">\n        <Trans>Connected apps</Trans>\n      </h2>\n\n      <p className=\"text-text-light mb-2\">\n        <Trans>\n          This is a list of all the applications you have authorized to access\n          your account.\n        </Trans>\n      </p>\n\n      {isLoading ? (\n        <Loader size=\"lg\" fill=\"var(--color-contrast-300)\" />\n      ) : error || !sessions ? (\n        <Admonition.Default\n          variant=\"error\"\n          text={_(msg`Failed to load connected apps`)}\n        />\n      ) : sessions.length > 0 ? (\n        <div className=\"space-y-2\">\n          {sessions.map((session) => (\n            <ApplicationSessionCard\n              key={session.tokenId}\n              sub={sub}\n              session={session}\n            />\n          ))}\n        </div>\n      ) : (\n        <Admonition.Default\n          variant=\"info\"\n          title={_(msg`No connected apps`)}\n          text={_(\n            msg`It appears that you haven’t used this account to sign in to any apps yet.`,\n          )}\n        />\n      )}\n\n      <h2 className=\"text-custom-primary pb-4 pt-8 text-xl font-bold\">\n        <Trans>My devices</Trans>\n      </h2>\n\n      <p className=\"text-text-light mb-2\">\n        <Trans>\n          This is a list of all the devices you have used to sign in to your\n          account. New apps can be authorized from any of these devices. If you\n          believe that your account has been compromised, we recommend that you\n          revoke access to all devices.\n        </Trans>\n      </p>\n\n      {accountSessionsIsLoading ? (\n        <Loader size=\"lg\" fill=\"var(--color-contrast-300)\" />\n      ) : accountSessionsError || !accountSessions ? (\n        <Admonition.Default\n          variant=\"error\"\n          text={_(msg`Failed to load devices`)}\n        />\n      ) : accountSessions.length > 0 ? (\n        <div className=\"space-y-3\">\n          {accountSessions.map((session) => (\n            <AccountSessionCard\n              key={`${sub}@${session.deviceId}`}\n              sub={sub}\n              session={session}\n            />\n          ))}\n        </div>\n      ) : (\n        <Admonition.Default\n          variant=\"info\"\n          title={_(msg`No devices`)}\n          text={_(msg`Looks like you aren't logged in on any other devices.`)}\n        />\n      )}\n    </>\n  )\n}\n\nfunction ApplicationSessionCard({\n  session: { clientId, clientMetadata, tokenId },\n  sub,\n}: {\n  session: ActiveOAuthSession\n  sub: string\n}) {\n  const { _ } = useLingui()\n  const { show } = useToast()\n  const { mutateAsync: revokeSessions, isPending } =\n    useRevokeOAuthSessionMutation()\n\n  const friendlyClientId = useFriendlyClientId({\n    clientId,\n  })\n  const clientName = useClientName({\n    clientId,\n    clientMetadata,\n  })\n\n  const revoke = async () => {\n    try {\n      await revokeSessions({ sub, tokenId })\n      show({\n        variant: 'success',\n        title: _(msg`Successfully signed out`),\n        duration: 2e3,\n      })\n    } catch (e) {\n      show({\n        variant: 'error',\n        title: _(msg`Failed to sign out`),\n        duration: 2e3,\n      })\n    }\n  }\n\n  return (\n    <div className=\"bg-contrast-25 dark:bg-contrast-50 border-contrast-50 dark:border-contrast-100 flex items-start justify-between space-x-4 rounded-lg border p-4\">\n      <div className=\"flex flex-1 items-center space-x-2 truncate\">\n        <Avatar\n          size={40}\n          src={clientMetadata?.logo_uri}\n          displayName={clientName}\n        />\n        <div className=\"flex-1 truncate\">\n          <h3 className=\"truncate font-bold leading-snug\">{clientName}</h3>\n          <p className=\"text-text-light truncate text-sm leading-snug\">\n            {friendlyClientId}\n          </p>\n        </div>\n      </div>\n      <div>\n        <Prompt\n          title={\n            clientName !== clientId\n              ? _(msg`Revoke access to ${clientName}`)\n              : _(msg`Revoke access to this application`)\n          }\n          description={_(\n            msg`Are you sure you want to revoke access? This application won't be able to access your account anymore.`,\n          )}\n          confirmCTA={_(msg`Revoke access`)}\n          onConfirm={revoke}\n        >\n          <Button color=\"secondary\" disabled={isPending}>\n            <Button.Text>\n              <Trans>Revoke access</Trans>\n            </Button.Text>\n            <Cross2Icon width={16} />\n          </Button>\n        </Prompt>\n      </div>\n    </div>\n  )\n}\n\nfunction AccountSessionCard({\n  session,\n  sub,\n}: {\n  session: ActiveAccountSession\n  sub: string\n}) {\n  const { show } = useToast()\n  const { _, i18n } = useLingui()\n  const { mutateAsync: revokeSessions, isPending } =\n    useRevokeAccountSessionMutation()\n\n  const { userAgent, lastSeenAt, ipAddress } = session.deviceMetadata\n\n  const ua = useMemo(() => {\n    if (!userAgent) {\n      return null\n    }\n    return UAParser(userAgent)\n  }, [userAgent])\n\n  const remove = async () => {\n    try {\n      await revokeSessions({ sub, deviceId: session.deviceId })\n      show({\n        variant: 'success',\n        title: _(msg`Successfully removed device`),\n        duration: 2e3,\n      })\n    } catch (e) {\n      show({\n        variant: 'error',\n        title: _(msg`Failed to remove device`),\n        duration: 2e3,\n      })\n    }\n  }\n\n  const lastUsed = useMemo(() => {\n    // Fool-proofing\n    if (!lastSeenAt) return undefined\n\n    const date = new Date(lastSeenAt)\n\n    // Fool-proofing\n    if (isNaN(date.getTime())) return lastSeenAt\n\n    return i18n.date(date, {\n      year: 'numeric',\n      month: 'numeric',\n      day: 'numeric',\n    })\n  }, [session])\n\n  return (\n    <div className=\"border-contrast-50 dark:border-contrast-100 flex flex-wrap items-center justify-between space-x-4 border-t px-2 pt-3\">\n      <div className=\"flex min-w-36 flex-1 flex-col space-x-2 truncate\">\n        <p className=\"truncate font-semibold\">\n          {ua ? (\n            ua.device.is('mobile') ? (\n              [ua.os.name].filter(Boolean).join(' • ')\n            ) : (\n              [ua.os.name, ua.browser.name].filter(Boolean).join(' • ')\n            )\n          ) : (\n            <Trans>Unknown user agent</Trans>\n          )}\n        </p>\n        <p className=\"truncate text-sm\">\n          <span className=\"text-text-light\">\n            {lastUsed}\n            {' • '}\n          </span>\n          <span className=\"text-warning-600 truncate font-mono\">\n            {ipAddress}\n          </span>\n        </p>\n      </div>\n      {session.isCurrentDevice && (\n        <div className=\"bg-contrast-25 dark:bg-contrast-50 text-text-light min-w-max shrink-0 grow-0 rounded-full px-2 py-1 text-xs\">\n          <Trans>This device</Trans>\n        </div>\n      )}\n      <Prompt\n        title={_(msg`Remove this device`)}\n        description={_(msg`Are you sure you want to remove this device?`)}\n        confirmCTA={_(msg`Sign out`)}\n        onConfirm={remove}\n      >\n        <Button\n          color=\"secondary\"\n          size=\"sm\"\n          className=\"min-w-max shrink-0 grow-0\"\n          disabled={isPending}\n        >\n          <Button.Text>\n            <Trans>Sign out</Trans>\n          </Button.Text>\n          <ExitIcon width={20} />\n        </Button>\n      </Prompt>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_appLayout.tsx",
    "content": "import { Navigate, Outlet, createFileRoute } from '@tanstack/react-router'\nimport * as Layout from '#/components/Layout'\nimport { Nav } from '#/components/Nav'\nimport { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'\nimport { Route as AccountRoute } from '#/routes/account/_appLayout/$sub'\n\nexport const Route = createFileRoute('/account/_appLayout')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { sub } = AccountRoute.useParams()\n  const { data: sessions } = useDeviceSessionsQuery()\n  const activeSession = sessions.find((session) => session.account.sub === sub)\n  const nextSession = sessions.find((session) => session.account.sub !== sub)\n\n  return activeSession ? (\n    <>\n      <Nav />\n      <Layout.Center>\n        <Outlet />\n      </Layout.Center>\n    </>\n  ) : nextSession ? (\n    // or <Navigate to=\"/account/$sub\" params={{ sub: nextSession.account.sub }} />\n    <Navigate to=\"/account\" />\n  ) : (\n    <Navigate to=\"/account/sign-in\" />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_minimalLayout/branding.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { Palette } from '#/components/util/Palette'\n\nexport const Route = createFileRoute('/account/_minimalLayout/branding')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { t } = useLingui()\n\n  return (\n    <>\n      <title>{t`Branding`}</title>\n      <Palette />\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_minimalLayout/index.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { Trans } from '@lingui/react/macro'\nimport { ChevronRightIcon } from '@radix-ui/react-icons'\nimport { Navigate, createFileRoute } from '@tanstack/react-router'\nimport { clsx } from 'clsx'\nimport { ActiveDeviceSession } from '#/api'\nimport * as Admonition from '#/components/Admonition'\nimport { Avatar } from '#/components/Avatar'\nimport { ContentCard } from '#/components/ContentCard'\nimport { InlineLink, Link } from '#/components/Link'\nimport { Loader } from '#/components/Loader'\nimport { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'\nimport { getAccountName } from '#/util/getAccountName'\nimport { sanitizeHandle } from '#/util/sanitizeHandle'\n\nexport const Route = createFileRoute('/account/_minimalLayout/')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { _ } = useLingui()\n\n  return (\n    <>\n      <title>{_(msg`Accounts`)}</title>\n      <Index />\n    </>\n  )\n}\n\nfunction Index() {\n  const { _ } = useLingui()\n  const { data: sessions, isLoading, error } = useDeviceSessionsQuery()\n\n  // This is the account dashboard home page.\n  //\n  // @TODO When the user is signed in, redirect to their account page.\n  //\n  // @TODO When the user is not signed in, show a nice (anonymous) view that\n  // explains them they can create an account on this PDS.\n\n  return isLoading ? (\n    <div className=\"flex items-center justify-center\">\n      <Loader size=\"lg\" />\n    </div>\n  ) : error || !sessions ? (\n    <Admonition.Default\n      variant=\"error\"\n      title={_(msg`Something went wrong`)}\n      text={_(\n        msg`We weren't able to load your accounts. Please refresh the page to try again.`,\n      )}\n    />\n  ) : sessions.length ? (\n    <SelectorScreen sessions={sessions} />\n  ) : (\n    <Navigate to=\"/account/sign-in\" />\n  )\n}\n\nexport function SelectorScreen({\n  sessions,\n}: {\n  sessions: ActiveDeviceSession[]\n}) {\n  const { _ } = useLingui()\n\n  return (\n    <>\n      <ContentCard>\n        <div className=\"space-y-4\">\n          <div className=\"space-y-1\">\n            <h1 className=\"text-custom-primary text-xl font-bold\">\n              <Trans>Accounts</Trans>\n            </h1>\n            <p className=\"text-text-light\">\n              <Trans>Select the account you would like to manage.</Trans>\n            </p>\n          </div>\n\n          <div className=\"space-y-2\">\n            {sessions\n              // @TODO redirect to sign in with the identifier pre-filled when a\n              // login is required (session is too old).\n              .filter((s) => !s.loginRequired)\n              .map(({ account }) => (\n                <Link\n                  key={account.sub}\n                  to=\"/account/$sub\"\n                  params={account}\n                  className={clsx([\n                    'flex items-center space-x-2 rounded-lg border px-2 py-2',\n                    'bg-contrast-25 dark:bg-contrast-50 border-contrast-50 dark:border-contrast-100',\n                    'hover:bg-contrast-50 dark:hover:bg-contrast-100',\n                  ])}\n                  label={_(\n                    msg`View and manage account for ${getAccountName(account)}`,\n                  )}\n                >\n                  <Avatar\n                    size={40}\n                    src={account.picture}\n                    displayName={account.name}\n                  />\n                  <div className=\"flex-1 space-y-0 truncate\">\n                    <h2 className=\"text-primary truncate font-semibold leading-snug\">\n                      {account.name}\n                    </h2>\n                    <p className=\"text-text-light truncate text-sm\">\n                      {sanitizeHandle(account.preferred_username) ||\n                        account.sub}\n                    </p>\n                  </div>\n                  <ChevronRightIcon width={20} className=\"text-text-light\" />\n                </Link>\n              ))}\n\n            <InlineLink\n              to=\"/account/sign-in\"\n              className=\"text-text-light inline-block w-full pt-2 text-center text-sm\"\n            >\n              <Trans>Sign in with another account</Trans>\n            </InlineLink>\n          </div>\n        </div>\n      </ContentCard>\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_minimalLayout/reset-password.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { Trans } from '@lingui/react/macro'\nimport { useForm } from '@tanstack/react-form'\nimport { createFileRoute } from '@tanstack/react-router'\nimport { clsx } from 'clsx'\nimport { useState } from 'react'\nimport { z } from 'zod'\nimport { Button } from '#/components/Button'\nimport { Divider } from '#/components/Divider'\nimport { ButtonLink, InlineLink } from '#/components/Link'\nimport { useToast } from '#/components/Toast'\nimport * as Form from '#/components/forms'\nimport { usePasswordConfirmMutation } from '#/data/usePasswordConfirmMutation'\nimport { usePasswordResetMutation } from '#/data/usePasswordResetMutation'\nimport { format2FACode } from '#/util/format2FACode'\nimport { MIN_PASSWORD_LENGTH, getPasswordStrength } from '#/util/passwords'\nimport { wait } from '#/util/wait'\n\nexport const Route = createFileRoute('/account/_minimalLayout/reset-password')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { _ } = useLingui()\n\n  return (\n    <>\n      <title>{_(msg`Reset password`)}</title>\n      <ResetPassword />\n    </>\n  )\n}\n\nfunction ResetPassword() {\n  const { _ } = useLingui()\n  const [showConfirmStep, setShowConfirmStep] = useState(false)\n  const [showSuccess, setShowSuccess] = useState(false)\n  const [error, setError] = useState('')\n  const { mutateAsync: resetPassword } = usePasswordResetMutation()\n  const { mutateAsync: confirmPassword } = usePasswordConfirmMutation()\n  const { show } = useToast()\n\n  const form = useForm({\n    defaultValues: {\n      email: '',\n    },\n    validators: {\n      onSubmit: z.object({\n        email: z\n          .string()\n          .email(_(msg`Invalid email`))\n          .nonempty(_(msg`Email is required`)),\n      }),\n    },\n    onSubmit: async ({ value }) => {\n      setError('')\n      try {\n        await wait(500, resetPassword({ email: value.email }))\n        setShowConfirmStep(true)\n      } catch (e) {\n        setError(_(msg`An error occurred, please try again.`))\n      }\n    },\n  })\n  const confirmForm = useForm({\n    defaultValues: {\n      token: '',\n      password: '',\n    },\n    validators: {\n      onSubmit: z.object({\n        token: z.string().nonempty(_(msg`Email code is required`)),\n        password: z\n          .string()\n          .nonempty(_(msg`A new password is required`))\n          .min(\n            MIN_PASSWORD_LENGTH,\n            _(msg`Password must be at least ${MIN_PASSWORD_LENGTH} characters`),\n          ),\n      }),\n    },\n    onSubmit: async ({ value }) => {\n      setError('')\n      try {\n        await wait(500, confirmPassword(value))\n        setShowSuccess(true)\n      } catch (e) {\n        setError(_(msg`An error occurred, please try again.`))\n      }\n    },\n  })\n\n  const resendCode = async () => {\n    try {\n      await resetPassword({ email: form.getFieldValue('email') })\n      show({\n        variant: 'success',\n        title: _(msg`Code was resent`),\n        duration: 2e3,\n      })\n    } catch (e) {\n      show({\n        variant: 'error',\n        title: _(msg`Failed to resend code`),\n        duration: 2e3,\n      })\n    }\n  }\n\n  return (\n    <div\n      className={clsx([\n        'mx-auto rounded-lg border p-5 shadow-xl md:p-7 dark:shadow-2xl',\n        'border-contrast-25 dark:border-contrast-50 shadow-contrast-500/20 dark:shadow-contrast-0/50',\n      ])}\n      style={{\n        maxWidth: 400,\n      }}\n    >\n      <div className=\"w-full space-y-4\">\n        {showSuccess ? (\n          <>\n            <div className=\"space-y-1\">\n              <h1 className=\"text-custom-primary text-xl font-bold\">\n                <Trans>Success!</Trans>\n              </h1>\n              <p className=\"text-text-light\">\n                <Trans>Your password has been reset.</Trans>\n              </p>\n            </div>\n            <ButtonLink size=\"lg\" to=\"/account/sign-in\">\n              <Trans>Back to sign in</Trans>\n            </ButtonLink>\n          </>\n        ) : (\n          <>\n            <div className=\"space-y-1\">\n              <h1 className=\"text-custom-primary text-xl font-bold\">\n                <Trans>Reset password</Trans>\n              </h1>\n              <p className=\"text-text-light\">\n                <Trans>Enter your email to receive a reset code.</Trans>\n              </p>\n            </div>\n            <form\n              onSubmit={(e) => {\n                e.preventDefault()\n                e.stopPropagation()\n                form.handleSubmit()\n              }}\n            >\n              <Form.Fieldset label=\"Test\">\n                <form.Field\n                  name=\"email\"\n                  children={(field) => {\n                    return (\n                      <Form.Item>\n                        <Form.Label name={field.name} hidden>\n                          <Trans>Email</Trans>\n                        </Form.Label>\n                        <Form.Text\n                          name={field.name}\n                          autoCapitalize=\"none\"\n                          autoCorrect=\"off\"\n                          autoComplete=\"email\"\n                          spellCheck=\"false\"\n                          type=\"email\"\n                          enterKeyHint=\"next\"\n                          value={field.state.value}\n                          placeholder={_(msg`Enter your email`)}\n                          onBlur={field.handleBlur}\n                          onChange={(e) => field.handleChange(e.target.value)}\n                          disabled={showConfirmStep}\n                        />\n                        <Form.Errors errors={field.state.meta.errors} />\n                      </Form.Item>\n                    )\n                  }}\n                />\n\n                {!showConfirmStep && (\n                  <>\n                    {error && (\n                      <ul>\n                        <Form.Error>{error}</Form.Error>\n                      </ul>\n                    )}\n\n                    <div className=\"align-center space-y-3 pt-2\">\n                      <form.Subscribe\n                        selector={(state) => [\n                          state.canSubmit,\n                          state.isSubmitting,\n                        ]}\n                        children={([canSubmit, isSubmitting]) => (\n                          <Button\n                            className=\"w-full\"\n                            size=\"lg\"\n                            type=\"submit\"\n                            disabled={!canSubmit || isSubmitting}\n                          >\n                            <Trans>Get reset code</Trans>\n                          </Button>\n                        )}\n                      />\n\n                      <InlineLink\n                        to=\"/account/sign-in\"\n                        className=\"text-text-light inline-block w-full text-center text-sm\"\n                      >\n                        <Trans>Back to sign in</Trans>\n                      </InlineLink>\n                    </div>\n                  </>\n                )}\n              </Form.Fieldset>\n            </form>\n\n            {showConfirmStep && (\n              <form\n                onSubmit={(e) => {\n                  e.preventDefault()\n                  e.stopPropagation()\n                  confirmForm.handleSubmit()\n                }}\n              >\n                <Form.Fieldset label=\"Test\">\n                  <>\n                    <div className=\"pb-2 pt-4\">\n                      <Divider />\n                    </div>\n                    <confirmForm.Field\n                      name=\"token\"\n                      children={(field) => {\n                        return (\n                          <Form.Item>\n                            <Form.Label name={field.name}>\n                              <Trans>Code</Trans>\n                            </Form.Label>\n                            <Form.Text\n                              autoComplete=\"one-time-code\"\n                              autoCapitalize=\"characters\"\n                              autoCorrect=\"off\"\n                              spellCheck=\"false\"\n                              enterKeyHint=\"next\"\n                              name={field.name}\n                              value={field.state.value}\n                              placeholder={_(msg`XXXXX-XXXXX`)}\n                              onBlur={field.handleBlur}\n                              onChange={(e) => {\n                                field.handleChange(\n                                  format2FACode(e.target.value),\n                                )\n                              }}\n                            />\n                            <Form.Errors errors={field.state.meta.errors} />\n                          </Form.Item>\n                        )\n                      }}\n                    />\n                    <confirmForm.Field\n                      name=\"password\"\n                      children={(field) => {\n                        const isMin =\n                          field.state.value.length >= MIN_PASSWORD_LENGTH\n                        const strength = getPasswordStrength(field.state.value)\n                        const defaultBg = 'bg-contrast-300 dark:bg-contrast-300'\n                        const strengthBg =\n                          strength >= 3\n                            ? 'bg-success-500'\n                            : strength === 2\n                              ? 'bg-warning-500'\n                              : 'bg-error-500'\n                        return (\n                          <Form.Item>\n                            <Form.Label name={field.name}>\n                              <Trans>Password</Trans>\n                            </Form.Label>\n                            <Form.Text\n                              type=\"password\"\n                              autoComplete=\"new-password\"\n                              autoCapitalize=\"none\"\n                              autoCorrect=\"off\"\n                              spellCheck=\"false\"\n                              enterKeyHint=\"done\"\n                              minLength={MIN_PASSWORD_LENGTH}\n                              name={field.name}\n                              value={field.state.value}\n                              placeholder={_(msg`Enter a new password`)}\n                              onBlur={field.handleBlur}\n                              onChange={(e) => {\n                                field.handleChange(e.target.value)\n                              }}\n                            />\n                            <div className=\"flex space-x-2\">\n                              {Array.from({ length: 4 }).map((_, i) => (\n                                <div\n                                  className={clsx([\n                                    'h-1 w-full rounded-full',\n                                    strength > i && isMin\n                                      ? strengthBg\n                                      : defaultBg,\n                                  ])}\n                                />\n                              ))}\n                            </div>\n                            <Form.Errors errors={field.state.meta.errors} />\n                          </Form.Item>\n                        )\n                      }}\n                    />\n                  </>\n\n                  {error && (\n                    <ul>\n                      <Form.Error>{error}</Form.Error>\n                    </ul>\n                  )}\n\n                  <div className=\"align-center space-y-3 pt-2\">\n                    <confirmForm.Subscribe\n                      selector={(state) => [\n                        state.canSubmit,\n                        state.isSubmitting,\n                      ]}\n                      children={([canSubmit, isSubmitting]) => (\n                        <Button\n                          className=\"w-full\"\n                          size=\"lg\"\n                          type=\"submit\"\n                          disabled={!canSubmit || isSubmitting}\n                        >\n                          <Trans>Reset password</Trans>\n                        </Button>\n                      )}\n                    />\n\n                    <p className=\"text-text-light inline-block w-full text-center text-sm\">\n                      <Trans>\n                        Don't see the email?{' '}\n                        <InlineLink\n                          className=\"text-sm\"\n                          label={_(\n                            msg`Click here to send a new code to your email.`,\n                          )}\n                          {...InlineLink.staticClick(() => {\n                            resendCode()\n                          })}\n                        >\n                          Try sending again.\n                        </InlineLink>\n                      </Trans>\n                    </p>\n                  </div>\n                </Form.Fieldset>\n              </form>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_minimalLayout/sign-in.tsx",
    "content": "import { msg } from '@lingui/core/macro'\nimport { useLingui } from '@lingui/react'\nimport { Trans } from '@lingui/react/macro'\nimport { useForm } from '@tanstack/react-form'\nimport { createFileRoute, useNavigate } from '@tanstack/react-router'\nimport { clsx } from 'clsx'\nimport { useState } from 'react'\nimport { z } from 'zod'\nimport {\n  InvalidCredentialsError,\n  SecondAuthenticationFactorRequiredError,\n} from '#/api'\nimport { Button } from '#/components/Button'\nimport { InlineLink } from '#/components/Link'\nimport * as Form from '#/components/forms'\nimport { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'\nimport { useSignInMutation } from '#/data/useSignInMutation'\nimport { format2FACode } from '#/util/format2FACode'\nimport { wait } from '#/util/wait'\nimport { normalizeAndEnsureValidHandle } from '@atproto/syntax'\n\nexport const Route = createFileRoute('/account/_minimalLayout/sign-in')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { data: sessions } = useDeviceSessionsQuery()\n  const { _ } = useLingui()\n\n  return (\n    <>\n      <title>{_(msg`Sign in`)}</title>\n      <div\n        className={clsx([\n          'mx-auto rounded-lg border p-5 shadow-xl md:p-7 dark:shadow-2xl',\n          'border-contrast-25 dark:border-contrast-50 shadow-contrast-500/20 dark:shadow-contrast-0/50',\n        ])}\n        style={{\n          maxWidth: 400,\n        }}\n      >\n        <LoginForm />\n      </div>\n\n      {sessions.length > 0 && (\n        <div className=\"flex flex-row justify-center pt-4\">\n          <InlineLink\n            to=\"/account\"\n            className=\"text-text-light inline-block w-full text-center text-sm\"\n          >\n            <Trans>&larr; Back to accounts</Trans>\n          </InlineLink>\n        </div>\n      )}\n    </>\n  )\n}\n\nfunction LoginForm() {\n  const { _ } = useLingui()\n  const [showCode, setShowCode] = useState(false)\n  const [error, setError] = useState('')\n  const { mutateAsync: signIn } = useSignInMutation()\n  const navigate = useNavigate({ from: Route.fullPath })\n\n  const form = useForm({\n    defaultValues: {\n      identifier: '',\n      password: '',\n      code: '',\n    },\n    validators: {\n      onSubmit: z.object({\n        identifier: z.union([\n          z.string().email(),\n          z\n            .string()\n            .transform((v) => (v.startsWith('@') ? v.slice(1) : v))\n            .superRefine((v, ctx) => {\n              try {\n                return normalizeAndEnsureValidHandle(v)\n              } catch (err) {\n                ctx.addIssue({\n                  code: z.ZodIssueCode.custom,\n                  message: _(msg`Invalid handle`),\n                })\n              }\n            }),\n        ]),\n        password: z.string().nonempty(_(msg`Password is required`)),\n        code: z.string(),\n      }),\n    },\n    onSubmit: async ({ value }) => {\n      setError('')\n      try {\n        // throw new SecondAuthenticationFactorRequiredError({\n        //   error: 'second_authentication_factor_required',\n        //   type: 'emailOtp',\n        //   hint: value.identifier,\n        // })\n        // throw new InvalidCredentialsError({\n        //   error: 'invalid_request',\n        //   error_description: 'Invalid identifier or password',\n        // })\n        const res = await wait(\n          500,\n          signIn({\n            // @NOTE For some reason, the validator function output is not taken\n            // into account here so we have to strip the @ again.\n            username: value.identifier.replace(/^@/, ''),\n            password: value.password,\n            emailOtp: showCode ? value.code : undefined,\n          }),\n        )\n        await navigate({\n          to: '/account/$sub',\n          params: res.account,\n        })\n      } catch (e) {\n        if (e instanceof SecondAuthenticationFactorRequiredError) {\n          setShowCode(true)\n        } else if (e instanceof InvalidCredentialsError) {\n          setShowCode(false)\n          setError(_(msg`Invalid identifier or password.`))\n        } else {\n          setError(_(msg`An error occurred, please try again.`))\n        }\n      }\n    },\n  })\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-custom-primary text-xl font-bold\">\n        <Trans>Sign in</Trans>\n      </h1>\n      <form\n        onSubmit={(e) => {\n          e.preventDefault()\n          e.stopPropagation()\n          form.handleSubmit()\n        }}\n      >\n        <Form.Fieldset label={_(msg`Credentials`)}>\n          <form.Field\n            name=\"identifier\"\n            children={(field) => {\n              return (\n                <Form.Item>\n                  <Form.Label name={field.name}>\n                    <Trans>Identifier</Trans>\n                  </Form.Label>\n                  <Form.Text\n                    name={field.name}\n                    autoCapitalize=\"none\"\n                    autoCorrect=\"off\"\n                    autoComplete=\"username\"\n                    spellCheck=\"false\"\n                    type=\"text\"\n                    value={field.state.value}\n                    placeholder={_(msg`@handle or email`)}\n                    onBlur={field.handleBlur}\n                    onChange={(e) => field.handleChange(e.target.value)}\n                  />\n                  <Form.Errors errors={field.state.meta.errors} />\n                </Form.Item>\n              )\n            }}\n          />\n          <form.Field\n            name=\"password\"\n            children={(field) => {\n              return (\n                <Form.Item>\n                  <Form.Label name={field.name}>\n                    <Trans>Password</Trans>\n                  </Form.Label>\n                  <Form.Text\n                    name={field.name}\n                    autoCapitalize=\"none\"\n                    autoCorrect=\"off\"\n                    autoComplete=\"current-password\"\n                    spellCheck=\"false\"\n                    type=\"password\"\n                    value={field.state.value}\n                    placeholder={_(msg`Password`)}\n                    onBlur={field.handleBlur}\n                    onChange={(e) => field.handleChange(e.target.value)}\n                  />\n                  <Form.Errors errors={field.state.meta.errors} />\n                </Form.Item>\n              )\n            }}\n          />\n\n          {showCode && (\n            <form.Field\n              name=\"code\"\n              children={(field) => {\n                return (\n                  <Form.Item>\n                    <Form.Label name={field.name}>\n                      <Trans>Code</Trans>\n                    </Form.Label>\n                    <Form.Text\n                      autoComplete=\"one-time-code\"\n                      autoCapitalize=\"characters\"\n                      autoCorrect=\"off\"\n                      spellCheck=\"false\"\n                      name={field.name}\n                      value={field.state.value}\n                      placeholder={_(msg`XXXXX-XXXXX`)}\n                      onBlur={field.handleBlur}\n                      onChange={(e) => {\n                        field.handleChange(format2FACode(e.target.value))\n                      }}\n                    />\n                  </Form.Item>\n                )\n              }}\n            />\n          )}\n\n          {error && (\n            <ul>\n              <Form.Error>{error}</Form.Error>\n            </ul>\n          )}\n\n          <div className=\"align-center space-y-3 pt-2\">\n            <form.Subscribe\n              selector={(state) => [state.canSubmit, state.isSubmitting]}\n              children={([canSubmit, isSubmitting]) => (\n                <Button\n                  className=\"w-full\"\n                  size=\"lg\"\n                  type=\"submit\"\n                  disabled={!canSubmit || isSubmitting}\n                >\n                  <Trans>Sign in</Trans>\n                </Button>\n              )}\n            />\n\n            <InlineLink\n              to=\"/account/reset-password\"\n              className=\"text-text-light inline-block w-full text-center text-sm\"\n            >\n              <Trans>Forgot password?</Trans>\n            </InlineLink>\n          </div>\n        </Form.Fieldset>\n      </form>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/routes/account/_minimalLayout.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { Outlet, createFileRoute } from '@tanstack/react-router'\nimport * as Layout from '#/components/Layout'\nimport { useCustomizationData } from '#/data/useCustomizationData'\n\nexport const Route = createFileRoute('/account/_minimalLayout')({\n  component: RouteComponent,\n})\n\nfunction RouteComponent() {\n  const { t } = useLingui()\n  const { logo, name } = useCustomizationData()\n\n  return (\n    <Layout.Center className=\"md:pt-[15vh]\">\n      {logo ? (\n        <div className=\"flex justify-center pb-8\" aria-hidden>\n          <div style={{ width: 200, height: 50 }}>\n            <img\n              src={logo}\n              alt={name || t`Logo`}\n              className=\"h-full w-full object-contain\"\n            />\n          </div>\n        </div>\n      ) : null}\n      <Outlet />\n    </Layout.Center>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/style.css",
    "content": "@import 'tailwindcss';\n@import './styles/radix-popover.css';\n@import './styles/radix-dialog.css';\n@import './styles/radix-toast.css';\n\n:root {\n  --branding-color-primary: 131 56 236;\n  --branding-color-primary-contrast: 255 255 255;\n  --branding-color-primary-hue: 265;\n\n  --branding-color-error: 255 0 110;\n  --branding-color-error-contrast: 0 0 0;\n  --branding-color-error-hue: 334.11764705882354;\n\n  --branding-color-warning: 255 171 15;\n  --branding-color-warning-contrast: 0 0 0;\n  --branding-color-warning-hue: 38.99999999999999;\n\n  --branding-color-success: 23 204 136;\n  --branding-color-success-contrast: 0 0 0;\n  --branding-color-success-hue: 157.4585635359116;\n}\n\n:root {\n  --hue-primary: var(--branding-color-primary-hue);\n  --hue-error: var(--branding-color-error-hue);\n  --hue-warning: var(--branding-color-warning-hue);\n  --hue-success: var(--branding-color-success-hue);\n\n  --color-primary: rgb(var(--branding-color-primary));\n  --color-error: rgb(var(--branding-color-error));\n  --color-warning: rgb(var(--branding-color-warning));\n  --color-success: rgb(var(--branding-color-success));\n\n  --color-primary-contrast: rgb(var(--branding-color-primary-contrast));\n  --color-error-contrast: rgb(var(--branding-color-error-contrast));\n  --color-warning-contrast: rgb(var(--branding-color-warning-contrast));\n  --color-success-contrast: rgb(var(--branding-color-success-contrast));\n\n  --color-contrast-0: hsl(var(--hue-primary) 20% 100%);\n  --color-contrast-25: hsl(var(--hue-primary) 20% 95.3%);\n  --color-contrast-50: hsl(var(--hue-primary) 20% 90.6%);\n  --color-contrast-100: hsl(var(--hue-primary) 20% 85.9%);\n  --color-contrast-200: hsl(var(--hue-primary) 20% 81.2%);\n  --color-contrast-300: hsl(var(--hue-primary) 20% 71.8%);\n  --color-contrast-400: hsl(var(--hue-primary) 20% 62.4%);\n  --color-contrast-500: hsl(var(--hue-primary) 20% 53%);\n  --color-contrast-600: hsl(var(--hue-primary) 20% 43.6%);\n  --color-contrast-700: hsl(var(--hue-primary) 20% 34.2%);\n  --color-contrast-800: hsl(var(--hue-primary) 20% 24.8%);\n  --color-contrast-900: hsl(var(--hue-primary) 20% 20.1%);\n  --color-contrast-950: hsl(var(--hue-primary) 20% 15.4%);\n  --color-contrast-975: hsl(var(--hue-primary) 20% 10.7%);\n  --color-contrast-1000: hsl(var(--hue-primary) 20% 6%);\n\n  --color-primary-25: hsl(var(--hue-primary) 100% 97%);\n  --color-primary-50: hsl(var(--hue-primary) 100% 95%);\n  --color-primary-100: hsl(var(--hue-primary) 100% 90%);\n  --color-primary-200: hsl(var(--hue-primary) 100% 80%);\n  --color-primary-300: hsl(var(--hue-primary) 100% 70%);\n  --color-primary-400: hsl(var(--hue-primary) 100% 60%);\n  --color-primary-500: hsl(var(--hue-primary) 100% 53%);\n  --color-primary-600: hsl(var(--hue-primary) 100% 42%);\n  --color-primary-700: hsl(var(--hue-primary) 100% 34%);\n  --color-primary-800: hsl(var(--hue-primary) 100% 26%);\n  --color-primary-900: hsl(var(--hue-primary) 100% 18%);\n  --color-primary-950: hsl(var(--hue-primary) 100% 10%);\n  --color-primary-975: hsl(var(--hue-primary) 100% 7%);\n\n  --color-error-25: hsl(var(--hue-error) 82% 97%);\n  --color-error-50: hsl(var(--hue-error) 82% 95%);\n  --color-error-100: hsl(var(--hue-error) 82% 90%);\n  --color-error-200: hsl(var(--hue-error) 82% 80%);\n  --color-error-300: hsl(var(--hue-error) 82% 70%);\n  --color-error-400: hsl(var(--hue-error) 82% 60%);\n  --color-error-500: hsl(var(--hue-error) 82% 53%);\n  --color-error-600: hsl(var(--hue-error) 82% 42%);\n  --color-error-700: hsl(var(--hue-error) 82% 34%);\n  --color-error-800: hsl(var(--hue-error) 82% 26%);\n  --color-error-900: hsl(var(--hue-error) 82% 18%);\n  --color-error-950: hsl(var(--hue-error) 82% 10%);\n  --color-error-975: hsl(var(--hue-error) 82% 7%);\n\n  --color-warning-25: hsl(var(--hue-warning) 100% 97%);\n  --color-warning-50: hsl(var(--hue-warning) 100% 95%);\n  --color-warning-100: hsl(var(--hue-warning) 100% 90%);\n  --color-warning-200: hsl(var(--hue-warning) 100% 80%);\n  --color-warning-300: hsl(var(--hue-warning) 100% 70%);\n  --color-warning-400: hsl(var(--hue-warning) 100% 60%);\n  --color-warning-500: hsl(var(--hue-warning) 100% 53%);\n  --color-warning-600: hsl(var(--hue-warning) 100% 42%);\n  --color-warning-700: hsl(var(--hue-warning) 100% 34%);\n  --color-warning-800: hsl(var(--hue-warning) 100% 26%);\n  --color-warning-900: hsl(var(--hue-warning) 100% 18%);\n  --color-warning-950: hsl(var(--hue-warning) 100% 10%);\n  --color-warning-975: hsl(var(--hue-warning) 100% 7%);\n\n  --color-success-25: hsl(var(--hue-success) 91% 97%);\n  --color-success-50: hsl(var(--hue-success) 91% 95%);\n  --color-success-100: hsl(var(--hue-success) 91% 90%);\n  --color-success-200: hsl(var(--hue-success) 91% 80%);\n  --color-success-300: hsl(var(--hue-success) 91% 70%);\n  --color-success-400: hsl(var(--hue-success) 91% 60%);\n  --color-success-500: hsl(var(--hue-success) 91% 53%);\n  --color-success-600: hsl(var(--hue-success) 91% 42%);\n  --color-success-700: hsl(var(--hue-success) 91% 34%);\n  --color-success-800: hsl(var(--hue-success) 91% 26%);\n  --color-success-900: hsl(var(--hue-success) 91% 18%);\n  --color-success-950: hsl(var(--hue-success) 91% 10%);\n  --color-success-975: hsl(var(--hue-success) 91% 7%);\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --color-contrast-1000: hsl(var(--hue-primary) 20% 100%);\n    --color-contrast-975: hsl(var(--hue-primary) 20% 95.3%);\n    --color-contrast-950: hsl(var(--hue-primary) 20% 90.6%);\n    --color-contrast-900: hsl(var(--hue-primary) 20% 85.9%);\n    --color-contrast-800: hsl(var(--hue-primary) 20% 81.2%);\n    --color-contrast-700: hsl(var(--hue-primary) 20% 71.8%);\n    --color-contrast-600: hsl(var(--hue-primary) 20% 62.4%);\n    --color-contrast-500: hsl(var(--hue-primary) 20% 53%);\n    --color-contrast-400: hsl(var(--hue-primary) 20% 43.6%);\n    --color-contrast-300: hsl(var(--hue-primary) 20% 34.2%);\n    --color-contrast-200: hsl(var(--hue-primary) 20% 24.8%);\n    --color-contrast-100: hsl(var(--hue-primary) 20% 20.1%);\n    --color-contrast-50: hsl(var(--hue-primary) 20% 15.4%);\n    --color-contrast-25: hsl(var(--hue-primary) 20% 10.7%);\n    --color-contrast-0: hsl(var(--hue-primary) 20% 6%);\n  }\n}\n\n@theme inline {\n  --color-contrast-0: var(--color-contrast-0);\n  --color-contrast-25: var(--color-contrast-25);\n  --color-contrast-50: var(--color-contrast-50);\n  --color-contrast-100: var(--color-contrast-100);\n  --color-contrast-200: var(--color-contrast-200);\n  --color-contrast-300: var(--color-contrast-300);\n  --color-contrast-400: var(--color-contrast-400);\n  --color-contrast-500: var(--color-contrast-500);\n  --color-contrast-600: var(--color-contrast-600);\n  --color-contrast-700: var(--color-contrast-700);\n  --color-contrast-800: var(--color-contrast-800);\n  --color-contrast-900: var(--color-contrast-900);\n  --color-contrast-950: var(--color-contrast-950);\n  --color-contrast-975: var(--color-contrast-975);\n  --color-contrast-1000: var(--color-contrast-1000);\n\n  --color-primary-25: var(--color-primary-25);\n  --color-primary-50: var(--color-primary-50);\n  --color-primary-100: var(--color-primary-100);\n  --color-primary-200: var(--color-primary-200);\n  --color-primary-300: var(--color-primary-300);\n  --color-primary-400: var(--color-primary-400);\n  --color-primary-500: var(--color-primary-500);\n  --color-primary-600: var(--color-primary-600);\n  --color-primary-700: var(--color-primary-700);\n  --color-primary-800: var(--color-primary-800);\n  --color-primary-900: var(--color-primary-900);\n  --color-primary-950: var(--color-primary-950);\n  --color-primary-975: var(--color-primary-975);\n\n  --color-error-25: var(--color-error-25);\n  --color-error-50: var(--color-error-50);\n  --color-error-100: var(--color-error-100);\n  --color-error-200: var(--color-error-200);\n  --color-error-300: var(--color-error-300);\n  --color-error-400: var(--color-error-400);\n  --color-error-500: var(--color-error-500);\n  --color-error-600: var(--color-error-600);\n  --color-error-700: var(--color-error-700);\n  --color-error-800: var(--color-error-800);\n  --color-error-900: var(--color-error-900);\n  --color-error-950: var(--color-error-950);\n  --color-error-975: var(--color-error-975);\n\n  --color-warning-25: var(--color-warning-25);\n  --color-warning-50: var(--color-warning-50);\n  --color-warning-100: var(--color-warning-100);\n  --color-warning-200: var(--color-warning-200);\n  --color-warning-300: var(--color-warning-300);\n  --color-warning-400: var(--color-warning-400);\n  --color-warning-500: var(--color-warning-500);\n  --color-warning-600: var(--color-warning-600);\n  --color-warning-700: var(--color-warning-700);\n  --color-warning-800: var(--color-warning-800);\n  --color-warning-900: var(--color-warning-900);\n  --color-warning-950: var(--color-warning-950);\n  --color-warning-975: var(--color-warning-975);\n\n  --color-success-25: var(--color-success-25);\n  --color-success-50: var(--color-success-50);\n  --color-success-100: var(--color-success-100);\n  --color-success-200: var(--color-success-200);\n  --color-success-300: var(--color-success-300);\n  --color-success-400: var(--color-success-400);\n  --color-success-500: var(--color-success-500);\n  --color-success-600: var(--color-success-600);\n  --color-success-700: var(--color-success-700);\n  --color-success-800: var(--color-success-800);\n  --color-success-900: var(--color-success-900);\n  --color-success-950: var(--color-success-950);\n  --color-success-975: var(--color-success-975);\n\n  --color-primary: var(--color-primary);\n  --color-error: var(--color-error);\n  --color-warning: var(--color-warning);\n  --color-success: var(--color-success);\n\n  --color-primary-contrast: var(--color-primary-contrast);\n  --color-error-contrast: var(--color-error-contrast);\n  --color-warning-contrast: var(--color-warning-contrast);\n  --color-success-contrast: var(--color-success-contrast);\n\n  --color-text-default: var(--color-contrast-900);\n  --color-text-light: var(--color-contrast-700);\n  --color-border-default: var(--color-contrast-200);\n  --color-border-dark: var(--color-contrast-400);\n\n  --space-screen: 100vh;\n}\n\n.debug {\n  border: 1px solid var(--color-error);\n}\n\nhtml,\nbody {\n  color: var(--color-text-default);\n  background-color: var(--color-contrast-0);\n\n  @variant dark {\n    background-color: var(--color-contrast-25);\n  }\n}\nbutton:not(:disabled) {\n  cursor: pointer;\n}\n\ninput:-webkit-autofill,\ninput:-webkit-autofill:hover,\ninput:-webkit-autofill:focus,\ninput:autofill,\ninput:autofill:hover,\ninput:autofill:focus,\ntextarea:-webkit-autofill,\ntextarea:-webkit-autofill:hover,\ntextarea:-webkit-autofill:focus,\nselect:-webkit-autofill,\nselect:-webkit-autofill:hover,\nselect:-webkit-autofill:focus {\n  /* transition: background-color 5000s ease-in-out 0s; */\n  /* font-size: var(--text-md); */\n}\ninput:is(:-webkit-autofill, :autofill),\ninput:is(:-webkit-autofill, :autofill) :is(:focus),\ntextarea:is(:-webkit-autofill, :autofill),\nselect:is(:-webkit-autofill, :autofill) {\n  font-size: var(--text-md);\n  color:;\n  background-color: var(--color-contrast-0);\n  border: 2px solid var(--color-contrast-300);\n  -webkit-box-shadow: 0 0 0px 1000px var(--color-contrast-0) inset;\n  -webkit-text-fill-color: var(--color-text-default);\n\n  @variant dark {\n    -webkit-box-shadow: 0 0 0px 1000px var(--color-contrast-25) inset;\n  }\n}\ninput[type='checkbox'] {\n  appearance: none;\n  position: relative;\n\n  &::after {\n    content: '';\n    display: block;\n    width: 66%;\n    height: 66%;\n    border-radius: var(--radius-xs);\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n  }\n\n  &:checked {\n    background-color: var(--color-primary);\n    border-color: var(--color-primary-400);\n\n    &::after {\n      background-color: white;\n    }\n\n    &:focus::after {\n    }\n  }\n}\nselect {\n  appearance: none;\n}\n\n@keyframes rotate {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n.loader-animation {\n  animation: rotate 500ms linear infinite;\n}\n\n.layout__center {\n  padding-top: 0;\n\n  @variant md {\n    padding-top: 10vh;\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/styles/radix-dialog.css",
    "content": ".DialogOverlay {\n  animation: radixOverlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.DialogContent {\n  position: fixed;\n  top: 30%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: calc(100% - 48px);\n  max-height: 85vh;\n  animation: radixContentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n.DialogContent:focus {\n  outline: none;\n}\n\n@keyframes radixOverlayShow {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes radixContentShow {\n  from {\n    opacity: 0;\n    transform: translate(-50%, -48%) scale(0.96);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, -50%) scale(1);\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/styles/radix-popover.css",
    "content": ".PopoverContent {\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n.PopoverContent:focus {\n}\n.PopoverContent[data-state='open'][data-side='top'] {\n  animation-name: radixSlideDownAndFade;\n}\n.PopoverContent[data-state='open'][data-side='right'] {\n  animation-name: radixSlideLeftAndFade;\n}\n.PopoverContent[data-state='open'][data-side='bottom'] {\n  animation-name: radixSlideUpAndFade;\n}\n.PopoverContent[data-state='open'][data-side='left'] {\n  animation-name: radixSlideRightAndFade;\n}\n\n.PopoverArrow {\n  fill: currentColor;\n}\n\n.PopoverClose {\n}\n.PopoverClose:hover {\n}\n.PopoverClose:focus {\n}\n\n@keyframes radixSlideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes radixSlideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes radixSlideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes radixSlideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/styles/radix-toast.css",
    "content": ".ToastRoot {\n}\n.ToastRoot[data-state='open'] {\n  animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n.ToastRoot[data-state='closed'] {\n  animation: hide 100ms ease-in;\n}\n.ToastRoot[data-state='closed'] ~ .ToastRoot[data-state='open'] {\n  transition: transform 100ms ease-out;\n  transform: translateY(-100%);\n}\n.ToastRoot[data-swipe='move'] {\n  transform: translateY(var(--radix-toast-swipe-move-y));\n}\n.ToastRoot[data-swipe='cancel'] {\n  transform: translateY(0);\n  transition: transform 200ms ease-out;\n}\n.ToastRoot[data-swipe='end'] {\n  animation: swipeOut 100ms ease-out;\n}\n\n@keyframes hide {\n  from {\n    opacity: 1;\n    transform: translateY(0);\n  }\n  to {\n    opacity: 0;\n    transform: translateY(calc(-100% - 40px));\n  }\n}\n\n@keyframes slideIn {\n  from {\n    transform: translateY(calc(-100% - 40px));\n  }\n  to {\n    transform: translateX(0);\n  }\n}\n\n@keyframes swipeOut {\n  from {\n    transform: translateY(var(--radix-toast-swipe-end-y));\n  }\n  to {\n    transform: translateY(calc(-100% - 40px));\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/cookies.ts",
    "content": "export const parseCookieString = (\n  cookie: string = document.cookie,\n): Record<string, string | undefined> =>\n  Object.fromEntries(\n    cookie\n      .split(';')\n      .filter(Boolean)\n      .map((str) => str.split('=', 2).map((s) => decodeURIComponent(s.trim()))),\n  )\n\nexport function readCookie(\n  name: string,\n  cookie: string = document.cookie,\n): string | undefined {\n  const cookies = parseCookieString(cookie)\n  return cookies[name]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/format2FACode.ts",
    "content": "export function format2FACode(value: string) {\n  const normalized = value.toUpperCase().replace(/[^A-Z2-7]/g, '')\n  if (normalized.length <= 5) return normalized\n  return `${normalized.slice(0, 5)}-${normalized.slice(5, 10)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/getAccountName.ts",
    "content": "import { Account } from '#/api'\nimport { sanitizeHandle } from '#/util/sanitizeHandle'\n\nexport function getAccountName(account: Account): string {\n  return (\n    account.name || sanitizeHandle(account.preferred_username) || account.sub\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/lang-string.tsx",
    "content": "import { ReactNode, useMemo } from 'react'\nimport { useLocale } from '#/locales'\nimport type { MultiLangString } from '@atproto/oauth-provider-api'\nimport { getLangString } from './lang'\n\nexport type LangStringProps = {\n  value?: string | MultiLangString\n  fallback?: ReactNode\n}\n\nexport function LangString({ value, fallback }: LangStringProps): ReactNode {\n  const matchingString = useLangString(value)\n  return (\n    matchingString ||\n    fallback ||\n    // If a fallback is not provided, return the english version, if it exists,\n    // or any string otherwise\n    (typeof value === 'object'\n      ? value['en'] || Object.values(value).find(Boolean)\n      : value)\n  )\n}\n\nexport function useLangString(\n  value?: string | MultiLangString,\n  fallback?: string,\n) {\n  const { locale } = useLocale()\n  return useMemo(() => {\n    return getLangString(value, locale, fallback)\n  }, [value, locale, fallback])\n}\n\nexport function LangProp<\n  P extends string,\n  O extends {\n    [K in P]?: string\n  } & {\n    [K in P as `${K}:lang`]?: MultiLangString\n  },\n>({\n  object,\n  property,\n  fallback,\n}: {\n  property: P\n  object?: O\n  fallback?: ReactNode\n}): ReactNode {\n  if (!object) return fallback\n  return (\n    <LangString\n      value={object[`${property}:lang` as keyof O]}\n      fallback={object[property] ?? fallback}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/lang.ts",
    "content": "type LangStringValue =\n  | undefined\n  | null\n  | string\n  | Record<string, string | undefined>\n\n/**\n * Only returns a string if it matches the desired {@link locale}, or return the\n * provided {@link fallback}.\n */\nexport function getLangString(\n  value: LangStringValue,\n  locale: string,\n  fallback: string,\n): string\nexport function getLangString(\n  value: LangStringValue,\n  locale: string,\n  fallback?: string,\n): string | undefined\nexport function getLangString(\n  value: LangStringValue,\n  locale: string,\n  fallback?: string,\n): string | undefined {\n  switch (typeof value) {\n    case 'string':\n      // By convention, string values are in english\n      if (locale === 'en' || locale.startsWith('en-')) return value\n      break\n\n    case 'object': {\n      // Fool-proof\n      if (value === null) break\n\n      // Exact match\n      const localeMatch = value[locale]\n      if (typeof localeMatch === 'string') return localeMatch\n\n      // Fallback to language match  (e.g. \"fr-BE\" -> \"fr\")\n      const lang = locale.split('-')[0]\n      const langMatch = value[lang]\n      if (typeof langMatch === 'string') return langMatch\n\n      // Fallback to any locale from same language (e.g. \"pt-PT\" -> \"pt-BR\")\n      for (const k in value) {\n        if (k.startsWith(`${lang}-`)) {\n          const countryMatch = value[k]\n          if (typeof countryMatch === 'string') return countryMatch\n        }\n      }\n    }\n  }\n\n  return fallback\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/oauth-client.ts",
    "content": "export type { OAuthClientMetadata } from '@atproto/oauth-types'\n\n// @NOTE: not importing these from @atproto/oauth-types here because 1) we don't\n// need to validate here and 2) we prefer not to import un-necessary code to\n// improve bundle size (~100k impact)\nexport const isOAuthClientIdLoopback = (clientId: string) =>\n  clientId.startsWith('http://')\nexport const isConventionalOAuthClientId = (clientId: string) => {\n  try {\n    const url = new URL(clientId)\n    return (\n      url.protocol === 'https:' &&\n      url.pathname === '/oauth-client-metadata.json' &&\n      !url.port &&\n      !url.search\n    )\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/passwords.ts",
    "content": "export const MIN_PASSWORD_LENGTH = 8\n\nconst EMOJI =\n  /(\\ud83c[\\ud000-\\udfff]|\\ud83d[\\ud000-\\udfff]|\\ud83e[\\ud000-\\udfff])/\nconst UPPER = /[A-Z]/\nconst LOWER = /[a-z]/\nconst DEC = /[0-9]/\nconst SPECIAL = /[^a-zA-Z0-9]/\n\nexport enum PasswordStrength {\n  weak = 1,\n  moderate = 2,\n  strong = 3,\n  extra = 4,\n}\n\nexport function getPasswordStrength(pwd: string): PasswordStrength {\n  if (pwd.length < MIN_PASSWORD_LENGTH) {\n    return PasswordStrength.weak\n  }\n\n  // Very long passwords\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 12) {\n    return PasswordStrength.extra\n  }\n\n  // Long passwords\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 8) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.extra\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC], 2)) {\n      return PasswordStrength.extra\n    }\n    return PasswordStrength.strong\n  }\n\n  // Emojis make passwords strong\n  if (pwd.length >= MIN_PASSWORD_LENGTH) {\n    if (matches(pwd, [EMOJI])) {\n      return PasswordStrength.strong\n    }\n  }\n\n  // Pretty long passwords\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 6) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.strong\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC], 2)) {\n      return PasswordStrength.strong\n    }\n    // Only 1 type of alpha-num characters\n    return PasswordStrength.moderate\n  }\n\n  // Longish password\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 4) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.moderate\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC], 2)) {\n      return PasswordStrength.moderate\n    }\n\n    // Only 1 type of alpha-num characters\n    return PasswordStrength.weak\n  }\n\n  // Short password (8-11 characters)\n  if (pwd.length >= MIN_PASSWORD_LENGTH) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.moderate\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC])) {\n      return PasswordStrength.moderate\n    }\n  }\n\n  return PasswordStrength.weak\n}\n\nfunction matches(\n  pwd: string,\n  regexps: RegExp[],\n  regexpsCountToMatch: number = regexps.length,\n): boolean {\n  if (regexpsCountToMatch < 1 || regexpsCountToMatch > regexps.length) {\n    throw new TypeError('Invalid regexpsCountToMatch')\n  }\n  for (const regexp of regexps) {\n    if (regexp.test(pwd)) {\n      regexpsCountToMatch--\n      if (regexpsCountToMatch === 0) return true\n    }\n  }\n  return false\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/sanitizeHandle.ts",
    "content": "export function isInvalidHandle(handle: string): boolean {\n  return handle === 'handle.invalid'\n}\n\nexport function sanitizeHandle(handle?: string): string | undefined {\n  if (!handle) return undefined\n  return isInvalidHandle(handle)\n    ? '⚠Invalid Handle'\n    : `@${handle.replace(/^@/, '')}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/sleep.ts",
    "content": "export async function sleep(delay: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, delay))\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/upsert.ts",
    "content": "export function upsert<T>(\n  arr: undefined | readonly T[],\n  item: T,\n  predicate: (value: T, index: number, obj: readonly T[]) => boolean,\n): T[] {\n  if (!arr) return [item]\n  const idx = arr.findIndex(predicate)\n  return idx === -1\n    ? [...arr, item]\n    : [...arr.slice(0, idx), item, ...arr.slice(idx + 1)]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/src/util/wait.ts",
    "content": "import { sleep } from './sleep'\n\nexport async function wait<T>(delay: number, promise: T) {\n  const [result] = await Promise.all([promise, sleep(delay)])\n  return result\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.src.json\" },\n    { \"path\": \"./tsconfig.tools.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/tsconfig.src.json",
    "content": "{\n  \"extends\": [\n    \"../../../tsconfig/browser.json\",\n    \"../../../tsconfig/bundler.json\"\n  ],\n  \"compilerOptions\": {\n    \"rootDir\": \"src\",\n    \"paths\": {\n      \"#/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/tsconfig.tools.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/nodenext.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"noEmit\": true\n  },\n  \"include\": [\"./*.js\", \"./*.cjs\", \"./*.mjs\", \"./*.ts\", \"./*.cts\", \"./*.mts\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-frontend/vite.config.mts",
    "content": "import { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { lingui } from '@lingui/vite-plugin'\nimport tailwindcss from '@tailwindcss/vite'\nimport { TanStackRouterVite } from '@tanstack/router-plugin/vite'\nimport react from '@vitejs/plugin-react-swc'\nimport { defineConfig } from 'vite'\nimport { bundleManifest } from '@atproto-labs/rollup-plugin-bundle-manifest'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      '#': resolve(__dirname, './src'),\n    },\n  },\n  plugins: [\n    TanStackRouterVite({ target: 'react' }),\n    react({\n      plugins: [['@lingui/swc-plugin', {}]],\n    }),\n    lingui(),\n    tailwindcss(),\n  ],\n  build: {\n    emptyOutDir: false,\n    outDir: './dist',\n    sourcemap: true,\n    rollupOptions: {\n      input: ['./src/account-page.tsx'],\n      output: {\n        manualChunks: undefined,\n        format: 'module',\n        entryFileNames: '[name]-[hash].js',\n        chunkFileNames: '[name]-[hash].js',\n        assetFileNames: '[name]-[hash][extname]',\n      },\n      plugins: [bundleManifest()],\n    },\n    commonjsOptions: {\n      include: [/node_modules/, /oauth-provider-api/, /syntax/],\n    },\n  },\n  optimizeDeps: {\n    include: ['@atproto/oauth-provider-api', '@atproto/syntax'],\n  },\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/.gitignore",
    "content": "src/locales/*/*.ts\n.swc\ndist\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/.linguirc",
    "content": "{\n  \"format\": \"po\",\n  \"sourceLocale\": \"en\",\n  \"locales\": [\n    \"en\",\n    \"an\",\n    \"ast\",\n    \"ca\",\n    \"da\",\n    \"de\",\n    \"el\",\n    \"en-GB\",\n    \"es\",\n    \"eu\",\n    \"fi\",\n    \"fr\",\n    \"ga\",\n    \"gl\",\n    \"hi\",\n    \"hu\",\n    \"ia\",\n    \"id\",\n    \"it\",\n    \"ja\",\n    \"km\",\n    \"ko\",\n    \"ne\",\n    \"nl\",\n    \"pl\",\n    \"pt-BR\",\n    \"ro\",\n    \"ru\",\n    \"sv\",\n    \"th\",\n    \"tr\",\n    \"uk\",\n    \"vi\",\n    \"zh-CN\",\n    \"zh-HK\",\n    \"zh-TW\"\n  ],\n  \"fallbackLocales\": {\n    \"default\": \"en\"\n  },\n  \"catalogs\": [\n    {\n      \"path\": \"<rootDir>/src/locales/{locale}/messages\",\n      \"include\": [\n        \"<rootDir>/src\"\n      ],\n      \"exclude\": [\n        \"**/dist/**\",\n        \"**/node_modules/**\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/CHANGELOG.md",
    "content": "# @atproto/oauth-provider-ui\n\n## 0.4.3\n\n### Patch Changes\n\n- [#4619](https://github.com/bluesky-social/atproto/pull/4619) [`a2e4e95`](https://github.com/bluesky-social/atproto/commit/a2e4e9584730c1742aca7c1fcc59533a7c159740) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix depencies version\n\n- [#4607](https://github.com/bluesky-social/atproto/pull/4607) [`19ecf5f`](https://github.com/bluesky-social/atproto/commit/19ecf5f76ae0d88c1963211a76920e00eecdd965) Thanks [@mozzius](https://github.com/mozzius)! - Fix avatar shape in OAuth UI\n\n- [#4606](https://github.com/bluesky-social/atproto/pull/4606) [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add console error logging upon client-side API request errors\n\n## 0.4.2\n\n## 0.4.1\n\n## 0.4.0\n\n### Minor Changes\n\n- [#4461](https://github.com/bluesky-social/atproto/pull/4461) [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Support selecting view based on prompt parameter\n\n## 0.3.6\n\n### Patch Changes\n\n- [#4382](https://github.com/bluesky-social/atproto/pull/4382) [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `toScopes()` utility on `IncludeScope`\n\n## 0.3.5\n\n## 0.3.4\n\n### Patch Changes\n\n- [#4301](https://github.com/bluesky-social/atproto/pull/4301) [`f496fa2c4`](https://github.com/bluesky-social/atproto/commit/f496fa2c4d9316229523454c691c75c269aba21e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Set dark background on authorization page's `<body>` in dark mode\n\n## 0.3.3\n\n## 0.3.2\n\n## 0.3.1\n\n### Patch Changes\n\n- [#4186](https://github.com/bluesky-social/atproto/pull/4186) [`d570db43d`](https://github.com/bluesky-social/atproto/commit/d570db43d6df2044dbaa5813cac469b3e73ba219) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add Japanese translation on OAuth Provider UI\n\n## 0.3.0\n\n### Minor Changes\n\n- [`f4cb3e4d0`](https://github.com/bluesky-social/atproto/commit/f4cb3e4d0ac45e567fa14f79b99a84621fa89a56) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Adapt to UI to support permission set.\n\n## 0.2.1\n\n## 0.2.0\n\n### Minor Changes\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Display detailed description of requested permissions\n\n## 0.1.11\n\n## 0.1.10\n\n## 0.1.9\n\n## 0.1.8\n\n## 0.1.7\n\n### Patch Changes\n\n- [#3916](https://github.com/bluesky-social/atproto/pull/3916) [`71b9dcda9`](https://github.com/bluesky-social/atproto/commit/71b9dcda9611ab3662ccb2c4e175579396f16b3a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Show sign-in screen instead of welcome screen when user already signed-in\n\n## 0.1.6\n\n### Patch Changes\n\n- [`d1e3e68dd`](https://github.com/bluesky-social/atproto/commit/d1e3e68dd9eb7bed13d9023bc0e4ce3c448eabf5) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve auto completion of sing-in & reset password flows\n\n## 0.1.5\n\n### Patch Changes\n\n- [#3820](https://github.com/bluesky-social/atproto/pull/3820) [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `transition:email` oauth scope\n\n## 0.1.4\n\n### Patch Changes\n\n- [#3810](https://github.com/bluesky-social/atproto/pull/3810) [`e1bda27e5`](https://github.com/bluesky-social/atproto/commit/e1bda27e550d3ba9dab1fab1f27726c185d8bf9f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix CORS issue on `<img>` tags\n\n- [#3797](https://github.com/bluesky-social/atproto/pull/3797) [`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use more consistent UI regardless of profile completion\n\n## 0.1.3\n\n### Patch Changes\n\n- [#3778](https://github.com/bluesky-social/atproto/pull/3778) [`81524fcb0`](https://github.com/bluesky-social/atproto/commit/81524fcb007f12161fd6928badbf176b1568b4b3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor UI fixes\n\n- [#3781](https://github.com/bluesky-social/atproto/pull/3781) [`a70dad5ae`](https://github.com/bluesky-social/atproto/commit/a70dad5aea32ce26d2cca170a06d184935b4865d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove lazy loading of hcaptcha library to resolve chunk loading errors.\n\n## 0.1.2\n\n## 0.1.1\n\n### Patch Changes\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix dependencies\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Explicit exported package `files`\n\n## 0.1.0\n\n### Minor Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - New build system\n\n### Patch Changes\n\n- [#3667](https://github.com/bluesky-social/atproto/pull/3667) [`8b98fec88`](https://github.com/bluesky-social/atproto/commit/8b98fec8857aacddeed9efb5c755474951e6d9d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Hide client id pathname if it is exaclty `/oauth-client-metadata.json`\n\n## 0.0.2\n\n### Patch Changes\n\n- [#3640](https://github.com/bluesky-social/atproto/pull/3640) [`cc4122652`](https://github.com/bluesky-social/atproto/commit/cc4122652ed42ba55826c019d0ec57bf25df1ecd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Split OAuth Provider's ui into its own package\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/CONTRIBUTING.md",
    "content": "In dev, use `pnpm dev:ui` to start a mock ui server. This will serve the ui through Vite at `http://localhost:5173`.\n\nUse the following urls to live-preview the various ui pages:\n\nhttp://localhost:5173/authorization-page.html\nhttp://localhost:5173/error-page.html\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/authorization-page.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Mock - OAuth Provider</title>\n  </head>\n  <body class=\"bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100\">\n    <div id=\"root\"></div>\n    <script>\n      /*\n       * This file's purpose is to provide a way to develop the UI without\n       * running a full featured OAuth server. It mocks the server responses and\n       * provides configuration data to the UI.\n       *\n       * This file is not part of the production build.\n       *\n       * Start the development server with the following command from the\n       * oauth-provider root:\n       *\n       * ```sh\n       * pnpm run start:ui\n       * ```\n       *\n       * Then open the browser at http://localhost:5173/\n       */\n    </script>\n    <style>\n      :root {\n        --branding-color-primary: 10 122 255;\n        --branding-color-primary-contrast: 255 255 255;\n        --branding-color-primary-hue: 212.57142857142856;\n\n        --branding-color-error: 244 11 66;\n        --branding-color-error-contrast: 255 255 255;\n        --branding-color-error-hue: 345.83690987124464;\n\n        --branding-color-warning: 251 86 7;\n        --branding-color-warning-contrast: 255 255 255;\n        --branding-color-warning-hue: 19.426229508196723;\n\n        --branding-color-success: 2 195 154;\n        --branding-color-success-contrast: 0 0 0;\n        --branding-color-success-hue: 167.2538860103627;\n      }\n    </style>\n    <script type=\"module\">\n      import { API_ENDPOINT_PREFIX } from '@atproto/oauth-provider-api'\n\n      /*\n       * PDS branding configuration\n       */\n\n      const name = 'Bluesky'\n      const links = [\n        {\n          title: { en: 'Home', ja: 'ホーム' },\n          href: 'https://bsky.social/',\n          rel: 'canonical', // prevents the login page from being indexed by search engines\n        },\n        {\n          title: { en: 'Terms of Service', ja: '利用規約' },\n          href: 'https://bsky.social/about/support/tos',\n          rel: 'terms-of-service',\n        },\n        {\n          title: { en: 'Privacy Policy', ja: 'プライバシーポリシー' },\n          href: 'https://bsky.social/about/support/privacy-policy',\n          rel: 'privacy-policy',\n        },\n        {\n          title: { en: 'Support', ja: 'サポート' },\n          href: 'https://blueskyweb.zendesk.com/hc/en-us',\n          rel: 'help',\n        },\n      ]\n      const logo = `data:image/svg+xml,${encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 320 286\"><path fill=\"rgb(10,122,255)\" d=\"M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z\" /></svg>')}`\n\n      // Provide a value here to test the \"sing-in only\" flow\n      const loginHint = undefined // 'alice.test'\n\n      // Use empty array to disable the \"sing-up\" flow, use a single value to\n      // disable the domain selector.\n      const availableUserDomains = ['.bsky.social', '.bsky.team']\n\n      // Use non empty string to enable hCaptcha during \"sing-up\" flow\n      const hcaptchaSiteKey = undefined\n\n      /*\n       * Client branding configuration\n       */\n\n      // Use an \"http://\" URL to test the \"an app on your device\" flow\n      const clientId = 'https://example.com/client.json'\n      const clientName = 'My App'\n      const clientPolicyUri = 'https://bsky.app'\n      const clientTosUri = 'https://bsky.app'\n      const clientLogoUri = 'https://picsum.photos/200'\n\n      // Mock data\n\n      const requestUri = 'foo-bar'\n\n      document.cookie = `csrf-${requestUri}=xyz; path=/`\n\n      const lexicons = [\n        {\n          lexicon: 1,\n          id: 'com.atproto.moderation.basePermissions',\n          defs: {\n            main: {\n              type: 'permission-set',\n              title: 'Moderation',\n              'title:lang': { fr: 'Modération' },\n              detail: 'Create moderation reports',\n              'detail:lang': {\n                'fr-FR': 'Créer des rapports de modération',\n              },\n              permissions: [\n                {\n                  type: 'permission',\n                  resource: 'rpc',\n                  aud: '*',\n                  lxm: ['com.atproto.moderation.createReport'],\n                },\n              ],\n            },\n          },\n        },\n        {\n          lexicon: 1,\n          id: 'com.example.calendar.basePermissions',\n          defs: {\n            main: {\n              type: 'permission-set',\n              title: 'Calendar',\n              'title:lang': { fr: 'Calendrier' },\n              detail: 'Manage your events and RSVPs',\n              'detail:lang': {\n                'fr-BE': 'Gérer vos événements et réponses',\n              },\n              permissions: [\n                {\n                  type: 'permission',\n                  resource: 'rpc',\n                  inheritAud: true,\n                  lxm: [\n                    'com.example.calendar.listEvents',\n                    'com.example.calendar.getEventDetails',\n                    'com.example.calendar.getEventRsvps',\n                  ],\n                },\n                {\n                  type: 'permission',\n                  resource: 'repo',\n                  collection: [\n                    'com.example.calendar.event',\n                    'com.example.calendar.rsvp',\n                  ],\n                },\n                {\n                  type: 'permission',\n                  resource: 'blob',\n                  accept: ['image/*', 'video/*'],\n                },\n              ],\n            },\n          },\n        },\n      ]\n\n      window.__customizationData = {\n        availableUserDomains,\n        inviteCodeRequired: false,\n        hcaptchaSiteKey,\n        name,\n        links,\n        logo,\n      }\n\n      window.__authorizeData = {\n        requestUri,\n\n        clientId: clientId,\n        clientMetadata: {\n          client_id: clientId,\n          client_name: clientName,\n          policy_uri: clientPolicyUri,\n          tos_uri: clientTosUri,\n          logo_uri: clientLogoUri,\n        },\n        clientTrusted: false,\n        clientFirstParty: false,\n        permissionSets: Object.fromEntries(\n          lexicons.map((l) => [l.id, l.defs.main]),\n        ),\n\n        loginHint,\n        uiLocales: undefined, // 'en'\n        scope: [\n          'atproto',\n          'account:email',\n          'include:com.atproto.moderation.basePermissions',\n          'include:com.example.calendar.basePermissions?aud=did:web:example.com%23foo',\n          // 'account:status?action=manage',\n          // 'account:repo?action=manage',\n          // 'transition:email',\n          // 'transition:generic',\n          // 'transition:chat.bsky',\n          // 'identity:*',\n          // 'identity:handle',\n          // 'blob:image/*',\n          // 'blob:video/*',\n          // 'repo:app.bsky.feed.post?action=create',\n          // 'repo:app.bsky.feed.like?action=create',\n          // 'repo:app.bsky.graph.follow?action=create',\n          // 'repo:*?action=delete',\n          // 'rpc:*?aud=did:web:bsky.app#bsky_appview',\n          // 'rpc:app.bsky.feed.getFeed?aud=*',\n          // 'rpc:app.bsky.feed.getFeedSkeleton?aud=*',\n          // 'rpc:com.atproto.moderation.createReport?aud=*',\n        ].join(' '),\n      }\n\n      window.__sessions = [\n        {\n          account: {\n            sub: 'did:plc:543',\n            name: 'Bob',\n            email: 'bob@test.com',\n            email_verified: true,\n            preferred_username: 'bob.test',\n            picture: 'https://cat.com/cat.jpg',\n          },\n          selected: false,\n          loginRequired: false,\n          consentRequired: true,\n        },\n      ]\n\n      const origFetch = window.fetch\n\n      async function mockFetch(...args) {\n        const [input, init] = args\n\n        const method = init?.method ?? 'GET'\n        const url =\n          typeof input === 'string'\n            ? new URL(input, window.location)\n            : input instanceof URL\n              ? input\n              : undefined\n        if (url) {\n          switch (`${method} ${url.pathname}`) {\n            case `POST ${API_ENDPOINT_PREFIX}/sign-up`:\n            case `POST ${API_ENDPOINT_PREFIX}/sign-in`:\n              return new Response(\n                JSON.stringify({\n                  consentRequired: false,\n                  account: {\n                    sub: 'did:plc:123',\n                    name: 'Alice',\n                    email: 'alice@test.com',\n                    email_verified: false,\n                    preferred_username: 'alice.test',\n                    picture: 'https://cat.com/cat.jpg',\n                  },\n                }),\n                {\n                  status: 200,\n                  headers: { 'Content-Type': 'application/json' },\n                },\n              )\n            case `POST ${API_ENDPOINT_PREFIX}/accept`:\n              return new Response(\n                JSON.stringify({\n                  error: 'invalid_request',\n                  error_description: 'Noooo',\n                }),\n                {\n                  status: 400,\n                  headers: { 'Content-Type': 'application/json' },\n                },\n              )\n            case `POST ${API_ENDPOINT_PREFIX}/verify-handle-availability`:\n            case `POST ${API_ENDPOINT_PREFIX}/reset-password-request`:\n            case `POST ${API_ENDPOINT_PREFIX}/reset-password-confirm`:\n              return new Response(null, { status: 204 })\n          }\n        }\n\n        return origFetch.call(this, ...args)\n      }\n\n      Object.defineProperty(window, 'fetch', {\n        value: mockFetch,\n        writable: true,\n        configurable: true,\n      })\n    </script>\n    <script src=\"./src/authorization-page.tsx\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/cookie-error-page.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Mock - OAuth Provider</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script>\n      /*\n       * This file's purpose is to provide a way to develop the UI without\n       * running a full featured OAuth server. It mocks the server responses and\n       * provides configuration data to the UI.\n       *\n       * This file is not part of the production build.\n       *\n       * Start the development server with the following command from the\n       * oauth-provider root:\n       *\n       * ```sh\n       * pnpm run start:ui\n       * ```\n       *\n       * Then open the browser at http://localhost:5173/\n       */\n    </script>\n    <style>\n      :root {\n        --branding-color-primary: 10 122 255;\n        --branding-color-primary-contrast: 255 255 255;\n        --branding-color-primary-hue: 212.57142857142856;\n\n        --branding-color-error: 244 11 66;\n        --branding-color-error-contrast: 255 255 255;\n        --branding-color-error-hue: 345.83690987124464;\n\n        --branding-color-warning: 251 86 7;\n        --branding-color-warning-contrast: 255 255 255;\n        --branding-color-warning-hue: 19.426229508196723;\n\n        --branding-color-success: 2 195 154;\n        --branding-color-success-contrast: 0 0 0;\n        --branding-color-success-hue: 167.2538860103627;\n      }\n    </style>\n    <script type=\"module\">\n      /*\n       * PDS branding configuration\n       */\n\n      const name = 'Bluesky'\n      const links = [\n        {\n          title: { en: 'Home', fr: 'Accueil', ja: 'ホーム' },\n          href: 'https://bsky.social/',\n          rel: 'canonical', // prevents the login page from being indexed by search engines\n        },\n        {\n          title: { en: 'Terms of Service', ja: '利用規約' },\n          href: 'https://bsky.social/about/support/tos',\n          rel: 'terms-of-service',\n        },\n        {\n          title: { en: 'Privacy Policy', ja: 'プライバシーポリシー' },\n          href: 'https://bsky.social/about/support/privacy-policy',\n          rel: 'privacy-policy',\n        },\n        {\n          title: { en: 'Support', ja: 'サポート' },\n          href: 'https://blueskyweb.zendesk.com/hc/en-us',\n          rel: 'help',\n        },\n      ]\n      const logo = `data:image/svg+xml,${encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 320 286\"><path fill=\"rgb(10,122,255)\" d=\"M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z\" /></svg>')}`\n\n      // Provide a value here to test the \"sign-in only\" flow\n      const loginHint = undefined // 'alice.test'\n\n      // Use empty array to disable the \"sign-up\" flow, use a single value to\n      // disable the domain selector.\n      const availableUserDomains = ['.bsky.social', '.bsky.team']\n\n      // Use non empty string to enable hCaptcha during \"sign-up\" flow\n      const hcaptchaSiteKey = undefined\n\n      /*\n       * Client branding configuration\n       */\n\n      // Use an \"http://\" URL to test the \"an app on your device\" flow\n      const clientId = 'https://example.com/client.json'\n      const clientName = 'My App'\n      const clientPolicyUri = 'https://bsky.app'\n      const clientTosUri = 'https://bsky.app'\n      const clientLogoUri = 'https://bsky.app'\n\n      // Mock data\n\n      const requestUri = 'foo-bar'\n\n      window.__customizationData = {\n        availableUserDomains,\n        inviteCodeRequired: false,\n        hcaptchaSiteKey,\n        name,\n        links,\n        logo,\n      }\n\n      window.__continueUrl =\n        'https://example.com/callback?request_uri=' + requestUri\n    </script>\n    <script src=\"./src/cookie-error-page.tsx\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/error-page.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Mock - OAuth Provider</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script>\n      /*\n       * This file's purpose is to provide a way to develop the UI without\n       * running a full featured OAuth server. It mocks the server responses and\n       * provides configuration data to the UI.\n       *\n       * This file is not part of the production build.\n       *\n       * Start the development server with the following command from the\n       * oauth-provider root:\n       *\n       * ```sh\n       * pnpm run start:ui\n       * ```\n       *\n       * Then open the browser at http://localhost:5173/\n       */\n    </script>\n    <style>\n      :root {\n        --branding-color-primary: 10 122 255;\n        --branding-color-primary-contrast: 255 255 255;\n        --branding-color-primary-hue: 212.57142857142856;\n\n        --branding-color-error: 244 11 66;\n        --branding-color-error-contrast: 255 255 255;\n        --branding-color-error-hue: 345.83690987124464;\n\n        --branding-color-warning: 251 86 7;\n        --branding-color-warning-contrast: 255 255 255;\n        --branding-color-warning-hue: 19.426229508196723;\n\n        --branding-color-success: 2 195 154;\n        --branding-color-success-contrast: 0 0 0;\n        --branding-color-success-hue: 167.2538860103627;\n      }\n    </style>\n    <script type=\"module\">\n      /*\n       * PDS branding configuration\n       */\n\n      const name = 'Bluesky'\n      const links = [\n        {\n          title: { en: 'Home', fr: 'Accueil', ja: 'ホーム' },\n          href: 'https://bsky.social/',\n          rel: 'canonical', // prevents the login page from being indexed by search engines\n        },\n        {\n          title: { en: 'Terms of Service', ja: '利用規約' },\n          href: 'https://bsky.social/about/support/tos',\n          rel: 'terms-of-service',\n        },\n        {\n          title: { en: 'Privacy Policy', ja: 'プライバシーポリシー' },\n          href: 'https://bsky.social/about/support/privacy-policy',\n          rel: 'privacy-policy',\n        },\n        {\n          title: { en: 'Support', ja: 'サポート' },\n          href: 'https://blueskyweb.zendesk.com/hc/en-us',\n          rel: 'help',\n        },\n      ]\n      const logo = `data:image/svg+xml,${encodeURIComponent('<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 320 286\"><path fill=\"rgb(10,122,255)\" d=\"M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z\" /></svg>')}`\n\n      // Provide a value here to test the \"sing-in only\" flow\n      const loginHint = undefined // 'alice.test'\n\n      // Use empty array to disable the \"sing-up\" flow, use a single value to\n      // disable the domain selector.\n      const availableUserDomains = ['.bsky.social', '.bsky.team']\n\n      // Use non empty string to enable hCaptcha during \"sing-up\" flow\n      const hcaptchaSiteKey = undefined\n\n      /*\n       * Client branding configuration\n       */\n\n      // Use an \"http://\" URL to test the \"an app on your device\" flow\n      const clientId = 'https://example.com/client.json'\n      const clientName = 'My App'\n      const clientPolicyUri = 'https://bsky.app'\n      const clientTosUri = 'https://bsky.app'\n      const clientLogoUri = 'https://bsky.app'\n\n      // Mock data\n\n      const requestUri = 'foo-bar'\n\n      window.__customizationData = {\n        availableUserDomains,\n        inviteCodeRequired: false,\n        hcaptchaSiteKey,\n        name,\n        links,\n        logo,\n      }\n\n      window.__errorData = {\n        error: 'foo',\n        error_description: 'bar',\n      }\n    </script>\n    <script src=\"./src/error-page.tsx\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/hydration-data.d.ts",
    "content": "import type { CustomizationData, Session } from '@atproto/oauth-provider-api'\nimport type { LexiconPermissionSet } from '@atproto/oauth-scopes'\nimport type { OAuthClientMetadata, OAuthPromptMode } from '@atproto/oauth-types'\n\nexport type PermissionSet = LexiconPermissionSet\nexport type PermissionSets = Record<string, undefined | PermissionSet>\n\nexport type AuthorizeData = {\n  requestUri: string\n\n  clientId: string\n  clientMetadata: OAuthClientMetadata\n  clientTrusted: boolean\n  clientFirstParty: boolean\n\n  scope?: string\n  loginHint?: string\n  uiLocales?: string\n  promptMode?: OAuthPromptMode\n  permissionSets: PermissionSets\n}\n\nexport type ErrorData = {\n  error: string\n  error_description: string\n}\n\nexport type HydrationData = {\n  /**\n   * Matches the variables needed by `authorization-page.tsx`\n   */\n  'authorization-page': {\n    __customizationData: CustomizationData\n    __authorizeData: AuthorizeData\n    __sessions: readonly Session[]\n  }\n  /**\n   * Matches the variables needed by `error-page.tsx`\n   */\n  'error-page': {\n    __customizationData: CustomizationData\n    __errorData: ErrorData\n  }\n  /**\n   * Matches the variables needed by `cookie-error-page.tsx`\n   */\n  'cookie-error-page': {\n    __customizationData: CustomizationData\n    __continueUrl: string\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>OAuth mock pages</title>\n  </head>\n  <body>\n    <a href=\"authorization-page.html\">authorization-page</a>\n    <br />\n    <a href=\"error-page.html\">error-page</a>\n    <br />\n    <a href=\"cookie-error-page.html\">cookie-error-page</a>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-provider-ui\",\n  \"version\": \"0.4.3\",\n  \"license\": \"MIT\",\n  \"description\": \"Sign-in & Sign-up UI for the @atproto/oauth-provider\",\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-provider-ui\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"type\": \"commonjs\",\n  \"exports\": {\n    \"./bundle-manifest.json\": {\n      \"default\": \"./dist/bundle-manifest.json\"\n    },\n    \"./hydration-data\": {\n      \"types\": \"./hydration-data.d.ts\"\n    }\n  },\n  \"files\": [\n    \"dist\",\n    \"hydration-data.d.ts\"\n  ],\n  \"optionalDependencies\": {\n    \"@atproto/oauth-provider-api\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@atproto-labs/fetch\": \"workspace:^\",\n    \"@atproto-labs/rollup-plugin-bundle-manifest\": \"workspace:^\",\n    \"@atproto/oauth-scopes\": \"workspace:^\",\n    \"@atproto/oauth-types\": \"workspace:^\",\n    \"@hcaptcha/react-hcaptcha\": \"^1.11.2\",\n    \"@lingui/cli\": \"^5.2.0\",\n    \"@lingui/core\": \"^5.2.0\",\n    \"@lingui/react\": \"^5.2.0\",\n    \"@lingui/swc-plugin\": \"^5.4.0\",\n    \"@lingui/vite-plugin\": \"^5.2.0\",\n    \"@tailwindcss/vite\": \"^4.1.3\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"@vitejs/plugin-react-swc\": \"^3.8.0\",\n    \"clsx\": \"^2.1.1\",\n    \"postcss\": \"^8.4.38\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-error-boundary\": \"^5.0.0\",\n    \"tailwindcss\": \"^4.1.3\",\n    \"typescript\": \"^5.6.3\",\n    \"vite\": \"^6.2.0\"\n  },\n  \"scripts\": {\n    \"i18n:extract\": \"lingui extract --clean\",\n    \"i18n:compile\": \"lingui compile --typescript\",\n    \"i18n\": \"pnpm i18n:extract && pnpm i18n:compile\",\n    \"prebuild\": \"pnpm run i18n:compile\",\n    \"build\": \"vite build --emptyOutDir -- ignore additional npm args\",\n    \"dev:ui\": \"vite --port 5174\",\n    \"dev:src\": \"vite build --watch\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/authorization-page.tsx",
    "content": "import './style.css'\n\nimport type { HydrationData } from '#/hydration-data.d.ts'\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { ErrorBoundary } from 'react-error-boundary'\nimport { LocaleProvider } from './locales/locale-provider.tsx'\nimport { AuthorizeView } from './views/authorize/authorize-view.tsx'\nimport { ErrorView } from './views/error/error-view.tsx'\n\nconst {\n  __authorizeData: authorizeData,\n  __customizationData: customizationData,\n  __sessions: initialSessions,\n} = window as typeof window & HydrationData['authorization-page']\n\n// When the user is logging in, make sure the page URL contains the\n// \"request_uri\" in case the user refreshes the page.\n// @TODO Actually do this on the backend through a redirect.\nconst url = new URL(window.location.href)\nif (\n  url.pathname === '/oauth/authorize' &&\n  !url.searchParams.has('request_uri')\n) {\n  url.search = ''\n  url.searchParams.set('client_id', authorizeData.clientId)\n  url.searchParams.set('request_uri', authorizeData.requestUri)\n  window.history.replaceState(history.state, '', url.pathname + url.search)\n}\n\nconst container = document.getElementById('root')!\n\ncreateRoot(container).render(\n  <StrictMode>\n    <LocaleProvider userLocales={authorizeData.uiLocales?.split(' ')}>\n      <ErrorBoundary\n        fallbackRender={({ error }) => (\n          <ErrorView error={error} customizationData={customizationData} />\n        )}\n      >\n        <AuthorizeView\n          authorizeData={authorizeData}\n          customizationData={customizationData}\n          initialSessions={initialSessions}\n        />\n      </ErrorBoundary>\n    </LocaleProvider>\n  </StrictMode>,\n)\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/button-toggle-visibility.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { Override } from '../../lib/util.ts'\nimport { EyeIcon, EyeSlashIcon } from '../utils/icons.tsx'\nimport { Button, ButtonProps } from './button.tsx'\n\nexport type ButtonToggleVisibilityProps = Override<\n  Omit<ButtonProps, 'aria-label' | 'square'>,\n  {\n    visible: boolean\n    toggleVisible: () => void\n  }\n>\n\n/**\n * Generic button to toggle visibility of an item (e.g. password).\n */\nexport function ButtonToggleVisibility({\n  visible,\n  toggleVisible,\n\n  // button\n  onClick,\n  ...props\n}: ButtonToggleVisibilityProps) {\n  const { t } = useLingui()\n  return (\n    <Button\n      {...props}\n      shape=\"padded\"\n      onClick={(event) => {\n        onClick?.(event)\n        if (!event.defaultPrevented) toggleVisible()\n      }}\n      aria-label={visible ? t`Hide` : t`Make visible`}\n    >\n      {visible ? (\n        <EyeIcon className=\"w-5\" aria-hidden />\n      ) : (\n        <EyeSlashIcon className=\"w-5\" aria-hidden />\n      )}\n    </Button>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/button.tsx",
    "content": "import { clsx } from 'clsx'\nimport { JSX } from 'react'\nimport { Override } from '../../lib/util.ts'\n\nexport type ButtonProps = Override<\n  JSX.IntrinsicElements['button'],\n  {\n    color?: 'primary' | 'grey'\n    loading?: boolean\n    transparent?: boolean\n    shape?: 'padded' | 'rounded' | 'circle'\n  }\n>\n\nexport function Button({\n  color = 'grey',\n  transparent = false,\n  loading = undefined,\n  shape = 'rounded',\n\n  // button\n  children,\n  className,\n  type = 'button',\n  role = 'Button',\n  disabled = false,\n  ...props\n}: ButtonProps) {\n  const actionable = type === 'submit' || props.onClick != null\n\n  return (\n    <button\n      role={role}\n      type={type}\n      disabled={disabled || loading === true}\n      tabIndex={props?.tabIndex ?? (actionable ? 0 : -1)}\n      {...props}\n      className={clsx(\n        'touch-manipulation overflow-hidden',\n        'truncate tracking-wide',\n        actionable ? 'cursor-pointer' : null,\n        shape === 'circle' ? 'rounded-full' : 'rounded-md',\n        shape === 'circle' ? 'size-8' : null,\n        shape === 'rounded'\n          ? 'px-6 py-2'\n          : shape === 'padded'\n            ? 'p-2'\n            : undefined,\n\n        // Transition\n        'transition duration-300 ease-in-out',\n\n        // Outline\n        'outline-none',\n        'focus:ring-primary focus:ring-2 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-offset-black',\n\n        // Background & Text\n        color === 'primary'\n          ? clsx(\n              'accent-slate-100',\n              transparent\n                ? 'text-primary bg-transparent'\n                : 'bg-primary text-primary-contrast',\n            )\n          : color === 'grey'\n            ? clsx(\n                'accent-primary',\n                'text-slate-600 dark:text-slate-300',\n                'hover:bg-gray-200 dark:hover:bg-gray-700',\n                transparent ? 'bg-transparent' : 'bg-gray-100 dark:bg-gray-800',\n              )\n            : undefined,\n        'disabled:opacity-50',\n        className,\n      )}\n    >\n      {children}\n    </button>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/checkbox.tsx",
    "content": "import { clsx } from 'clsx'\nimport { ClassAttributes, InputHTMLAttributes, forwardRef } from 'react'\nimport { Override } from '#/lib/util.ts'\n\nexport type CheckboxProps = Override<\n  InputHTMLAttributes<HTMLInputElement> & ClassAttributes<HTMLInputElement>,\n  {\n    type?: never\n    invalid?: boolean\n  }\n>\n\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(\n  ({ className, invalid, disabled, ...props }, ref) => {\n    return (\n      <input\n        {...props}\n        type=\"checkbox\"\n        ref={ref}\n        disabled={disabled}\n        className={clsx([\n          'block size-4 rounded-md border-2',\n          'accent-primary',\n          'transition duration-300 ease-in-out',\n          'outline-none',\n          'focus:ring-primary focus:ring-2 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-offset-black',\n          invalid && 'accent-error focus:border-error',\n          disabled && 'bg-contrast-50 text-contrast-500 cursor-not-allowed',\n          className,\n        ])}\n      />\n    )\n  },\n)\n\nCheckbox.displayName = 'Checkbox'\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/fieldset.tsx",
    "content": "import { JSX, ReactNode, createContext, useMemo } from 'react'\nimport { useRandomString } from '../../hooks/use-random-string.ts'\nimport { Override } from '../../lib/util.ts'\n\nexport type FieldsetContextValue = {\n  disabled: boolean\n  labelId?: string\n}\n\nexport const FieldsetContext = createContext<FieldsetContextValue>({\n  disabled: false,\n})\nFieldsetContext.displayName = 'FieldsetContext'\n\nexport type FieldsetCardProps = Override<\n  Omit<JSX.IntrinsicElements['fieldset'], 'aria-labelledby'>,\n  {\n    label?: ReactNode\n  }\n>\n\nexport function Fieldset({\n  label,\n  children,\n  disabled,\n  ...props\n}: FieldsetCardProps) {\n  const labelId = useRandomString({ prefix: 'fieldset-' })\n\n  const contextValue = useMemo(\n    () => ({\n      disabled: disabled ?? false,\n      labelId: label ? labelId : undefined,\n    }),\n    [disabled, label, labelId],\n  )\n\n  return (\n    <fieldset {...props} aria-labelledby={labelId} disabled={disabled}>\n      {label && (\n        <legend\n          id={labelId}\n          key=\"title\"\n          className=\"mb-1 text-sm font-medium text-slate-600 dark:text-slate-400\"\n        >\n          {label}\n        </legend>\n      )}\n\n      <div className=\"flex flex-col space-y-4\">\n        <FieldsetContext value={contextValue}>{children}</FieldsetContext>\n      </div>\n    </fieldset>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/form-card-async.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { FormEvent, ReactNode, useCallback } from 'react'\nimport {\n  UseAsyncActionOptions,\n  useAsyncAction,\n} from '../../hooks/use-async-action.ts'\nimport { Override } from '../../lib/util.ts'\nimport { ErrorCard } from '../utils/error-card.tsx'\nimport { Button } from './button.tsx'\nimport { FormCard, FormCardProps } from './form-card.tsx'\n\nexport type { AsyncActionController } from '../../hooks/use-async-action.ts'\n\nexport type ErrorRender = (data: { error: Error }) => ReactNode\nexport const errorRenderDefault: ErrorRender = ({ error }) => (\n  <ErrorCard error={error} />\n)\n\nexport type FormCardAsyncProps = Override<\n  Override<\n    Omit<FormCardProps, 'cancel' | 'actions' | 'prepend'>,\n    Pick<UseAsyncActionOptions, 'ref' | 'onLoading' | 'onError'>\n  >,\n  {\n    invalid?: boolean\n    disabled?: boolean\n\n    onSubmit: (signal: AbortSignal) => void | PromiseLike<void>\n    submitLabel?: ReactNode\n\n    onCancel?: () => void\n    cancelLabel?: ReactNode\n\n    errorRender?: ErrorRender\n  }\n>\n\nexport function FormCardAsync({\n  invalid,\n  disabled,\n\n  onSubmit,\n  submitLabel,\n\n  onCancel = undefined,\n  cancelLabel,\n\n  errorRender = errorRenderDefault,\n\n  // UseAsyncActionOptions\n  ref,\n  onLoading,\n  onError,\n\n  // FormCardProps\n  children,\n  ...props\n}: FormCardAsyncProps) {\n  const { run, loading, error } = useAsyncAction(onSubmit, {\n    ref,\n    onError,\n    onLoading,\n  })\n\n  const doSubmit = useCallback(\n    (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault()\n\n      if (!event.currentTarget.reportValidity()) return\n\n      if (!disabled && !invalid) void run()\n    },\n    [disabled, invalid, run],\n  )\n\n  return (\n    <FormCard\n      {...props}\n      onSubmit={doSubmit}\n      disabled={disabled || loading}\n      prepend={error != null ? errorRender({ error }) : undefined}\n      cancel={\n        onCancel && (\n          <Button onClick={onCancel}>\n            {cancelLabel || <Trans>Cancel</Trans>}\n          </Button>\n        )\n      }\n      actions={\n        <Button\n          color=\"primary\"\n          type=\"submit\"\n          loading={loading}\n          disabled={disabled}\n        >\n          {submitLabel || <Trans>Submit</Trans>}\n        </Button>\n      }\n    >\n      {children}\n    </FormCard>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/form-card.tsx",
    "content": "import { JSX, ReactNode } from 'react'\nimport { Override } from '../../lib/util.ts'\n\nexport type FormCardProps = Override<\n  JSX.IntrinsicElements['form'],\n  {\n    disabled?: boolean\n    append?: ReactNode\n    prepend?: ReactNode\n    cancel?: ReactNode\n    actions?: ReactNode\n  }\n>\n\nexport function FormCard({\n  actions,\n  cancel,\n  append,\n  children,\n  prepend,\n  disabled,\n\n  // form\n  inert = disabled,\n  ...props\n}: FormCardProps) {\n  return (\n    <form {...props} inert={inert} className=\"flex flex-col gap-4\">\n      {prepend && <div key=\"prepend\">{prepend}</div>}\n\n      <div key=\"children\" className=\"space-y-4\">\n        {children}\n      </div>\n\n      {append && <div key=\"append\">{append}</div>}\n\n      {(actions || cancel) && (\n        <div\n          key=\"buttons\"\n          className=\"flex flex-row-reverse flex-wrap items-center justify-end space-x-2 space-x-reverse\"\n        >\n          {actions}\n          <div className=\"flex-auto\" />\n          {cancel}\n        </div>\n      )}\n    </form>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-checkbox.tsx",
    "content": "import { clsx } from 'clsx'\nimport { JSX, ReactNode, useContext, useRef } from 'react'\nimport { useRandomString } from '../../hooks/use-random-string.ts'\nimport { mergeRefs } from '../../lib/ref.ts'\nimport { Override } from '../../lib/util.ts'\nimport { Checkbox } from './checkbox.tsx'\nimport { FieldsetContext } from './fieldset.tsx'\nimport { InputContainer } from './input-container.tsx'\n\nexport type InputCheckboxProps = Override<\n  Omit<JSX.IntrinsicElements['input'], 'className' | 'type' | 'children'>,\n  {\n    className?: string\n    children?: ReactNode\n  }\n>\n\nexport function InputCheckbox({\n  className,\n  children,\n\n  // input\n  id,\n  ref,\n  disabled,\n  title,\n  'aria-label': ariaLabel = title,\n  'aria-labelledby': ariaLabelledBy,\n  ...props\n}: InputCheckboxProps) {\n  const htmlFor = useRandomString('input-checkbox-')\n  const labelRef = useRef<HTMLLabelElement>(null)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const ctx = useContext(FieldsetContext)\n\n  const inputId = id ?? htmlFor\n\n  return (\n    <InputContainer\n      className={clsx('cursor-pointer', className)}\n      icon={\n        <Checkbox\n          {...props}\n          disabled={disabled ?? ctx.disabled}\n          title={title}\n          aria-label={ariaLabel}\n          aria-labelledby={\n            children\n              ? // Prefer the local \"<label>\" element (through \"htmlFor\") over the wrapping \"<fieldset>\" to describe the checkbox.\n                undefined\n              : ariaLabelledBy ?? ctx.labelId\n          }\n          ref={mergeRefs([ref, inputRef])}\n          id={inputId}\n        />\n      }\n      tabIndex={-1}\n      onClick={({ target }) => {\n        // Native behavior of clicking the label should toggle the checkbox.\n        if (target === labelRef.current) return\n        if (target === inputRef.current) return\n\n        inputRef.current?.click()\n        inputRef.current?.focus()\n      }}\n    >\n      {children && (\n        <label\n          ref={labelRef}\n          htmlFor={inputId}\n          className=\"block w-full cursor-pointer select-none leading-[1.6]\"\n        >\n          {children}\n        </label>\n      )}\n    </InputContainer>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-container.tsx",
    "content": "import { clsx } from 'clsx'\nimport { JSX, ReactNode, useState } from 'react'\nimport { Override } from '../../lib/util.ts'\n\nexport type InputContainerProps = Override<\n  JSX.IntrinsicElements['div'],\n  {\n    icon: ReactNode\n    append?: ReactNode\n    bellow?: ReactNode\n    actionable?: boolean\n  }\n>\n\nexport function InputContainer({\n  icon,\n  append,\n  bellow,\n  actionable,\n\n  // div\n  className,\n  children,\n  onFocus,\n  onBlur,\n  ...props\n}: InputContainerProps) {\n  const [hasFocus, setHasFocus] = useState(false)\n\n  actionable ??= props.onClick != null\n\n  return (\n    <div\n      {...props}\n      onFocus={(event) => {\n        onFocus?.(event)\n        if (!event.defaultPrevented) setHasFocus(true)\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!event.defaultPrevented) setHasFocus(false)\n      }}\n      className={clsx(\n        // Layout\n        'min-h-12',\n        'max-w-full',\n        'overflow-hidden',\n        // Border\n        'rounded-lg',\n\n        // Transition\n        'transition duration-300 ease-in-out',\n\n        // Outline\n        'outline-none',\n        'focus:ring-primary focus:ring-2 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-offset-black',\n        'has-focus:ring-primary has-focus:ring-2 has-focus:ring-offset-1 has-focus:ring-offset-white dark:has-focus:ring-offset-black',\n\n        // Background\n        'bg-gray-100 dark:bg-gray-800',\n        actionable\n          ? 'cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700'\n          : undefined,\n\n        className,\n      )}\n    >\n      <div\n        className={clsx(\n          // Layout\n          'px-1',\n          'min-h-12 w-full',\n          'flex items-center justify-stretch',\n          // Border\n          'rounded-lg',\n          bellow ? 'rounded-bl-none rounded-br-none' : undefined,\n\n          // Font\n          'text-slate-600 dark:text-slate-300',\n        )}\n      >\n        {icon && (\n          <div\n            className={clsx(\n              'flex shrink-0 grow-0 items-center justify-center',\n              'mx-2',\n              hasFocus ? 'text-primary' : 'text-slate-500',\n            )}\n          >\n            {icon}\n          </div>\n        )}\n\n        {children}\n\n        <div className=\"ml-1 flex shrink-0 grow-0 items-center\">{append}</div>\n      </div>\n      {bellow && (\n        <div\n          className={clsx(\n            // Layout\n            'space-x-2 px-3 py-2',\n            'flex flex-row items-center gap-1',\n            // Border\n            'rounded-br-2 rounded-bl-2',\n            // Background\n            'bg-gray-200 dark:bg-slate-700',\n            // Font\n            'text-gray-700 dark:text-gray-300',\n            'text-sm italic',\n          )}\n        >\n          {bellow}\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-email-address.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { ChangeEvent, useCallback, useState } from 'react'\nimport { Override } from '../../lib/util.ts'\nimport { AtSymbolIcon } from '../utils/icons.tsx'\nimport { InputText, InputTextProps } from './input-text.tsx'\n\nexport type InputEmailAddressProps = Override<\n  Omit<InputTextProps, 'type'>,\n  {\n    onEmail?: (email: string | undefined) => void\n  }\n>\n\nexport function InputEmailAddress({\n  onEmail,\n\n  // InputTextProps\n  autoCapitalize = 'none',\n  autoComplete = 'email',\n  autoCorrect = 'off',\n  dir = 'auto',\n  icon = <AtSymbolIcon className=\"w-5\" />,\n  onBlur,\n  onChange,\n  pattern = '^[^@]+@[^@]+\\\\.[^@]+$',\n  spellCheck = 'false',\n  value,\n  defaultValue = value,\n  title,\n  ...props\n}: InputEmailAddressProps) {\n  const { t } = useLingui()\n  const [email, setEmail] = useState<string>(\n    typeof defaultValue === 'string' ? defaultValue : '',\n  )\n\n  const doChange = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      const email = event.target.value.toLowerCase()\n\n      setEmail(email)\n      onChange?.(event)\n      onEmail?.(event.target.validity.valid ? email : undefined)\n    },\n    [onChange, onEmail],\n  )\n\n  return (\n    <InputText\n      {...props}\n      title={title ?? t`Email address`}\n      type=\"email\"\n      autoCapitalize={autoCapitalize}\n      autoCorrect={autoCorrect}\n      dir={dir}\n      spellCheck={spellCheck}\n      icon={icon}\n      pattern={pattern}\n      autoComplete={autoComplete}\n      value={email}\n      onChange={doChange}\n      onBlur={onBlur}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-new-password.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { ChangeEvent, useCallback, useState } from 'react'\nimport { MIN_PASSWORD_LENGTH } from '../../lib/password.ts'\nimport { Override } from '../../lib/util.ts'\nimport { PasswordStrengthLabel } from '../utils/password-strength-label.tsx'\nimport { PasswordStrengthMeter } from '../utils/password-strength-meter.tsx'\nimport { InputPassword, InputPasswordProps } from './input-password.tsx'\n\nexport type InputNewPasswordProps = Override<\n  Omit<InputPasswordProps, 'value' | 'defaultValue'>,\n  {\n    password?: string\n    onPassword?: (password: undefined | string) => void\n  }\n>\n\nexport function InputNewPassword({\n  password: passwordInit = '',\n  onPassword,\n\n  // InputPasswordProps\n  onChange,\n  autoComplete = 'new-password',\n  minLength = MIN_PASSWORD_LENGTH,\n  ...props\n}: InputNewPasswordProps) {\n  const { t } = useLingui()\n  const [password, setPassword] = useState<string>(passwordInit)\n\n  const doChange = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      const { value } = event.target\n      onChange?.(event)\n      if (event.defaultPrevented) return\n      setPassword(value)\n      onPassword?.(event.target.validity.valid ? value : undefined)\n    },\n    [onChange, onPassword],\n  )\n\n  return (\n    <InputPassword\n      {...props}\n      placeholder={t`Enter a password`}\n      aria-label={t`Enter your new password`}\n      title={t`Password with at least ${MIN_PASSWORD_LENGTH} characters`}\n      minLength={minLength}\n      onChange={doChange}\n      value={password}\n      autoComplete={autoComplete}\n      bellow={\n        <>\n          <PasswordStrengthMeter password={password} />\n          <PasswordStrengthLabel\n            className=\"grow-1 min-w-max text-xs text-gray-500 dark:text-gray-400\"\n            password={password}\n          />\n        </>\n      }\n    />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-password.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { ChangeEvent, useCallback, useRef, useState } from 'react'\nimport { mergeRefs } from '../../lib/ref.ts'\nimport { Override } from '../../lib/util.ts'\nimport { LockIcon } from '../utils/icons.tsx'\nimport { ButtonToggleVisibility } from './button-toggle-visibility.tsx'\nimport { InputText, InputTextProps } from './input-text.tsx'\n\nexport type InputPasswordProps = Override<\n  Omit<InputTextProps, 'type' | 'children'>,\n  {\n    autoHide?: boolean\n  }\n>\n\nexport function InputPassword({\n  autoHide = true,\n\n  // InputTextProps\n  onBlur,\n  onChange,\n  append,\n  autoComplete = 'current-password',\n  icon = <LockIcon className=\"w-5\" />,\n  value,\n  defaultValue = value,\n  ref,\n  title,\n  dir = 'auto',\n  autoCapitalize = 'none',\n  autoCorrect = 'off',\n  spellCheck = 'false',\n  ...props\n}: InputPasswordProps) {\n  const { t } = useLingui()\n  const inputRef = useRef<HTMLInputElement>(null)\n  const [visible, setVisible] = useState<boolean>(false)\n  const [password, setPassword] = useState<string>(\n    typeof defaultValue === 'string' ? defaultValue : '',\n  )\n\n  const doChange = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      onChange?.(event)\n      setPassword(event.target.value)\n    },\n    [onChange],\n  )\n\n  return (\n    <InputText\n      {...props}\n      title={title ?? t`Password`}\n      ref={mergeRefs([ref, inputRef])}\n      dir={dir}\n      autoCapitalize={autoCapitalize}\n      autoCorrect={autoCorrect}\n      spellCheck={spellCheck}\n      icon={icon}\n      onBlur={\n        autoHide\n          ? (event) => {\n              onBlur?.(event)\n              if (!event.defaultPrevented) setVisible(false)\n            }\n          : onBlur\n      }\n      value={password}\n      onChange={doChange}\n      type={visible ? 'text' : 'password'}\n      autoComplete={autoComplete}\n      append={\n        <>\n          <ButtonToggleVisibility\n            className=\"m-1\"\n            visible={visible}\n            toggleVisible={() => {\n              setVisible((prev) => !prev)\n              inputRef.current?.focus()\n            }}\n          />\n          {append}\n        </>\n      }\n    />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-text.tsx",
    "content": "import { clsx } from 'clsx'\nimport { JSX, ReactNode, useContext, useRef } from 'react'\nimport { mergeRefs } from '../../lib/ref.ts'\nimport { Override } from '../../lib/util.ts'\nimport { FieldsetContext } from './fieldset.tsx'\nimport { InputContainer } from './input-container.tsx'\n\nexport type InputTextProps = Override<\n  Omit<JSX.IntrinsicElements['input'], 'children'>,\n  {\n    icon?: ReactNode\n    append?: ReactNode\n    bellow?: ReactNode\n    className?: string\n  }\n>\n\nexport function InputText({\n  icon,\n  append,\n  bellow,\n  className,\n\n  // input\n  onFocus,\n  onBlur,\n  ref,\n  disabled,\n  title,\n  'aria-label': ariaLabel = title,\n  'aria-labelledby': ariaLabelledBy,\n  placeholder = ariaLabel,\n  ...props\n}: InputTextProps) {\n  const ctx = useContext(FieldsetContext)\n\n  const inputRef = useRef<HTMLInputElement>(null)\n  const focusedRef = useRef(false) // ref instead of state to avoid re-renders\n\n  return (\n    <InputContainer\n      icon={icon}\n      append={append}\n      bellow={bellow}\n      className={clsx('cursor-text', className)}\n      tabIndex={-1}\n      actionable={false}\n      onClick={(event) => {\n        if (inputRef.current !== event.target) {\n          event.preventDefault()\n          event.stopPropagation()\n          inputRef.current?.focus()\n        }\n      }}\n      onMouseDown={(event) => {\n        if (focusedRef.current && event.target !== inputRef.current) {\n          // Prevent \"blur\" event from firing when clicking outside the input\n          event.preventDefault()\n          event.stopPropagation()\n        }\n      }}\n    >\n      <input\n        {...props}\n        disabled={disabled ?? ctx.disabled}\n        title={title}\n        placeholder={placeholder}\n        aria-label={ariaLabel}\n        aria-labelledby={ariaLabelledBy ?? ctx.labelId}\n        ref={mergeRefs([ref, inputRef])}\n        className=\"outline-hidden w-full text-ellipsis bg-transparent bg-clip-padding text-base text-inherit dark:placeholder-gray-500\"\n        onFocus={(event) => {\n          onFocus?.(event)\n          if (!event.defaultPrevented) focusedRef.current = true\n        }}\n        onBlur={(event) => {\n          onBlur?.(event)\n          if (!event.defaultPrevented) focusedRef.current = false\n        }}\n      />\n    </InputContainer>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/input-token.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { ChangeEvent, useState } from 'react'\nimport { Override } from '../../lib/util.ts'\nimport { TokenIcon } from '../utils/icons.tsx'\nimport { InputText, InputTextProps } from './input-text.tsx'\n\nexport type InputTokenProps = Override<\n  Omit<\n    InputTextProps,\n    | 'type'\n    | 'pattern'\n    | 'autoCapitalize'\n    | 'autoCorrect'\n    | 'autoComplete'\n    | 'spellCheck'\n    | 'minLength'\n    | 'maxLength'\n    | 'placeholder'\n    | 'dir'\n  >,\n  {\n    example?: string\n    onToken?: (code: string | null) => void\n  }\n>\n\nexport const OTP_CODE_EXAMPLE = 'XXXXX-XXXXX'\n\nexport function InputToken({\n  example = OTP_CODE_EXAMPLE,\n  onToken,\n\n  // InputTextProps\n  icon = <TokenIcon className=\"w-5\" />,\n  title = example,\n  onChange,\n  value,\n  defaultValue = value,\n  ...props\n}: InputTokenProps) {\n  const { t } = useLingui()\n  const [token, setToken] = useState<string>(\n    typeof defaultValue === 'string' ? defaultValue : '',\n  )\n\n  return (\n    <InputText\n      {...props}\n      type=\"text\"\n      autoCapitalize=\"characters\"\n      autoCorrect=\"off\"\n      autoComplete=\"one-time-code\"\n      spellCheck=\"false\"\n      minLength={11}\n      maxLength={11}\n      dir=\"auto\"\n      icon={icon}\n      pattern=\"^[A-Z2-7]{5}-[A-Z2-7]{5}$\"\n      placeholder={t`Looks like ${example}`}\n      title={title}\n      value={token}\n      onChange={(event: ChangeEvent<HTMLInputElement>) => {\n        const { value, selectionEnd, selectionStart } = event.currentTarget\n\n        const fixedValue = fix(value)\n\n        event.currentTarget.value = fixedValue\n\n        // Move the cursor back where it was relative to the original value\n        const pos = selectionEnd ?? selectionStart\n        if (pos != null) {\n          const fixedSlicedValue = fix(value.slice(0, pos))\n          event.currentTarget.selectionStart =\n            event.currentTarget.selectionEnd = fixedSlicedValue.length\n        }\n\n        setToken(fixedValue)\n        onChange?.(event)\n\n        if (!event.isDefaultPrevented()) {\n          onToken?.(fixedValue.length === 11 ? fixedValue : null)\n        }\n      }}\n    />\n  )\n}\n\nfunction fix(value: string) {\n  const normalized = value.toUpperCase().replaceAll(/[^A-Z2-7]/g, '')\n\n  if (normalized.length <= 5) return normalized\n\n  return `${normalized.slice(0, 5)}-${normalized.slice(5, 10)}`\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/forms/wizard-card.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX, ReactNode, useCallback } from 'react'\nimport { DisabledStep, Step, useStepper } from '../../hooks/use-stepper.ts'\nimport { Override } from '../../lib/util.ts'\n\nexport type DoneFn = (...a: any) => unknown\n\nexport type WizardRenderProps<TDone extends DoneFn> = {\n  /**\n   * Indicates wether the render function being invoked corresponds to the step\n   * currently active. The steps titles could, for example, be rendered in a\n   * list of links, where the current step is highlighted (based on `current`).\n   *\n   * Another use for this is to render the next/previous steps in order to\n   * provide animated transitions between steps. In this case, `current` would\n   * be used to disable any form interaction with the form transitioning in/out.\n   */\n  current: boolean\n  invalid: boolean\n\n  prev?: () => void\n  prevLabel: ReactNode\n\n  // On the last step, the \"next()\" function will actually be the done function\n  next: (() => void) | TDone\n  nextLabel: ReactNode\n}\n\nexport type WizardRenderFn<TDone extends DoneFn> = (\n  data: WizardRenderProps<TDone>,\n) => ReactNode\n\nexport type WizardStep<TDone extends DoneFn> = Step & {\n  titleRender?: WizardRenderFn<TDone>\n  contentRender: WizardRenderFn<TDone>\n}\n\nexport type WizardCardProps<TDone extends DoneFn> = Override<\n  Omit<JSX.IntrinsicElements['div'], 'children'>,\n  {\n    prevLabel?: ReactNode\n    nextLabel?: ReactNode\n\n    onBack?: () => void\n    backLabel?: ReactNode\n\n    onDone: TDone\n    doneLabel?: ReactNode\n\n    steps: readonly (WizardStep<TDone> | DisabledStep)[]\n  }\n>\n\nexport function WizardCard<TDone extends DoneFn>({\n  prevLabel,\n  nextLabel,\n\n  onBack,\n  backLabel,\n\n  onDone,\n  doneLabel,\n\n  steps,\n  className,\n\n  ...props\n}: WizardCardProps<TDone>) {\n  const {\n    atFirst,\n    atLast,\n    count,\n    current,\n    currentPosition,\n    completed,\n    toNext,\n    toPrev,\n    toRequired,\n  } = useStepper(steps)\n\n  // Memoized to avoid re-renders in child (rendered) components\n  const onNext = useCallback(() => {\n    // If already at last step, go to the first incomplete (required) step\n    if (!toNext()) toRequired()\n  }, [toNext, toRequired])\n\n  const data: WizardRenderProps<TDone> = {\n    // The current UI only displays the current title & content.\n    current: true,\n    invalid: current ? current.invalid : false,\n\n    prevLabel: (atFirst && backLabel) || prevLabel || <Trans>Back</Trans>,\n    prev: atFirst ? onBack : toPrev,\n\n    nextLabel: (atLast && doneLabel) || nextLabel || <Trans>Next</Trans>,\n    next: atLast && completed ? onDone : onNext,\n  }\n\n  const stepTitle = current?.titleRender?.(data)\n  const stepContent = current?.contentRender?.(data)\n\n  return (\n    <div className={clsx(className, 'flex flex-col')} {...props}>\n      <p className=\"text-slate-500 dark:text-slate-400\">\n        <Trans>\n          Step {currentPosition} of {count}\n        </Trans>\n      </p>\n\n      {stepTitle && <h2 className=\"mb-4 text-xl font-medium\">{stepTitle}</h2>}\n\n      {stepContent}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/layouts/layout-title-page.tsx",
    "content": "import { clsx } from 'clsx'\nimport { JSX, ReactNode } from 'react'\nimport type { CustomizationData } from '@atproto/oauth-provider-api'\nimport { Override } from '../../lib/util.ts'\nimport { LocaleSelector } from '../../locales/locale-selector.tsx'\n\nexport type LayoutTitlePageProps = Override<\n  JSX.IntrinsicElements['div'],\n  {\n    customizationData?: CustomizationData\n    title?: ReactNode\n    htmlTitle?: string\n    subtitle?: ReactNode\n  }\n>\n\nexport function LayoutTitlePage({\n  customizationData,\n  title,\n  subtitle,\n  htmlTitle = typeof title === 'string' ? title : undefined,\n\n  // div\n  className,\n  children,\n  ...props\n}: LayoutTitlePageProps) {\n  return (\n    <div\n      {...props}\n      className={clsx(\n        className,\n        'flex flex-col items-center',\n        'md:flex md:flex-row md:items-center md:justify-stretch',\n        'min-w-screen min-h-screen',\n      )}\n    >\n      {htmlTitle && <title>{htmlTitle}</title>}\n\n      <div\n        className={clsx(\n          'px-6 pt-4',\n          'w-full',\n          'md:max-w-lg',\n          'flex flex-row items-center',\n          'md:flex-col md:items-end',\n          'md:self-stretch',\n          'md:max-w-fix md:w-1/2 md:p-4',\n          'md:text-right',\n          'md:dark:border-r md:dark:border-slate-700',\n          'md:bg-slate-100 md:dark:bg-slate-800',\n        )}\n      >\n        <div className=\"grid grow content-center md:justify-items-end\">\n          {title && (\n            <h1\n              key=\"title\"\n              className=\"text-primary text-xl font-semibold md:my-4 md:text-2xl lg:text-5xl\"\n            >\n              {title}\n            </h1>\n          )}\n\n          {subtitle && (\n            <p\n              key=\"subtitle\"\n              className=\"hidden max-w-xs text-slate-600 md:block dark:text-slate-400\"\n            >\n              {subtitle}\n            </p>\n          )}\n        </div>\n\n        <LocaleSelector key=\"localeSelector\" className=\"m-1 md:m-2\" />\n      </div>\n\n      <main className=\"w-full p-6 md:max-w-3xl md:px-12\">{children}</main>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/layouts/layout-welcome.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX, ReactNode } from 'react'\nimport type { CustomizationData } from '@atproto/oauth-provider-api'\nimport { Override } from '../../lib/util.ts'\nimport { PageFooter } from '../utils/page-footer.tsx'\n\nexport type LayoutWelcomeProps = Override<\n  JSX.IntrinsicElements['div'],\n  {\n    customizationData?: CustomizationData\n    title?: ReactNode\n    htmlTitle?: string\n  }\n>\n\nexport function LayoutWelcome({\n  customizationData: { logo, name, links } = {},\n  title,\n  htmlTitle = typeof title === 'string' ? title : name,\n\n  // div\n  className,\n  children,\n  ...props\n}: LayoutWelcomeProps) {\n  const { t } = useLingui()\n\n  return (\n    <div\n      {...props}\n      className={clsx(\n        'min-h-screen w-full',\n        'flex flex-col items-center justify-center',\n        className,\n      )}\n    >\n      {htmlTitle && <title>{htmlTitle}</title>}\n\n      <main\n        key=\"main\"\n        className=\"flex w-full grow flex-col items-center justify-center overflow-hidden p-6\"\n      >\n        {logo && (\n          <img\n            key=\"logo\"\n            src={logo}\n            alt={name || t`Logo`}\n            aria-hidden\n            className=\"mb-4 h-16 w-16 md:mb-8 md:h-24 md:w-24\"\n          />\n        )}\n\n        {title && (\n          <h1\n            key=\"title\"\n            className=\"text-primary mx-4 mb-4 text-center text-2xl font-bold md:mb-8 md:text-4xl\"\n          >\n            {title}\n          </h1>\n        )}\n\n        {children}\n      </main>\n\n      <PageFooter links={links} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/account-identifier.tsx",
    "content": "import { JSX } from 'react'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport { Override } from '../../lib/util.ts'\n\nexport type AccountIdentifierProps = Override<\n  Omit<JSX.IntrinsicElements['b'], 'children'>,\n  {\n    account: Account\n  }\n>\n\nexport function AccountIdentifier({\n  account,\n\n  // b\n  ...props\n}: AccountIdentifierProps) {\n  return (\n    <b {...props}>\n      {account.preferred_username || account.email || account.sub}\n    </b>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/account-image.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { AccountIcon } from './icons.tsx'\n\nexport type AccountIconProps = {\n  src?: string\n  alt: string\n}\n\nexport function AccountImage({ src, alt }: AccountIconProps) {\n  const [errored, setErrored] = useState(false)\n\n  useEffect(() => {\n    setErrored(false)\n  }, [src])\n\n  return src && !errored ? (\n    <img\n      aria-hidden\n      src={src}\n      alt={alt}\n      className=\"h-6 w-6 rounded-full\"\n      onError={() => setErrored(true)}\n    />\n  ) : (\n    <div\n      aria-hidden\n      className=\"bg-primary border-primary h-6 w-6 overflow-hidden rounded-full border-2 border-solid text-white\"\n    >\n      <AccountIcon className=\"-mx-1 -mb-1\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/admonition.tsx",
    "content": "import { clsx } from 'clsx'\nimport { JSX, ReactNode, memo } from 'react'\nimport { Override } from '../../lib/util.ts'\nimport { AlertIcon, CircleInfoIcon } from './icons.tsx'\n\nexport type AdmonitionProps = Override<\n  JSX.IntrinsicElements['div'],\n  {\n    prominent?: boolean\n    title?: ReactNode\n    append?: ReactNode\n    type?: 'alert' | 'status'\n  }\n>\n\nexport const Admonition = memo(function Admonition({\n  prominent,\n  title,\n  type = 'status',\n  append,\n\n  // div\n  children,\n  className,\n  ...props\n}: AdmonitionProps) {\n  const Icon = type === 'alert' ? AlertIcon : CircleInfoIcon\n  const titleColor = prominent\n    ? 'text-inherit'\n    : type === 'alert'\n      ? 'text-warning'\n      : 'text-primary'\n  return (\n    <div\n      {...props}\n      role={props.role ?? type}\n      className={clsx(\n        'max-w-full',\n        'flex flex-row',\n        'gap-2',\n        'p-3',\n        'rounded-lg',\n        prominent\n          ? type === 'alert'\n            ? 'bg-warning text-warning-contrast'\n            : 'bg-slate-400 text-slate-900'\n          : 'border border-slate-300 dark:border-slate-700',\n        className,\n      )}\n    >\n      <Icon aria-hidden className={clsx('size-6 fill-current', titleColor)} />\n\n      <div className=\"flex flex-1 flex-col justify-center space-y-1\">\n        {title && <h3 className={`text-md ${titleColor}`}>{title}</h3>}\n        {children && <div className=\"text-sm\">{children}</div>}\n        {append}\n      </div>\n    </div>\n  )\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/client-image.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport type { OAuthClientMetadata } from '@atproto/oauth-types'\nimport { CircleInfoIcon } from './icons'\n\nexport type ClientImageProps = {\n  clientId: string\n  clientMetadata: OAuthClientMetadata\n  clientTrusted: boolean\n}\n\nexport function ClientImage({\n  clientId,\n  clientMetadata,\n  clientTrusted,\n}: ClientImageProps) {\n  const [errored, setErrored] = useState(false)\n\n  const src = clientTrusted ? clientMetadata.logo_uri : undefined\n  const alt = clientMetadata.client_name || clientId\n\n  useEffect(() => {\n    setErrored(false)\n  }, [src])\n\n  return src && !errored ? (\n    <img\n      aria-hidden\n      src={src}\n      alt={alt}\n      className=\"-ml-1 size-8\"\n      onError={() => setErrored(true)}\n    />\n  ) : (\n    <div\n      aria-hidden\n      className=\"bg-primary flex size-8 items-center justify-center overflow-hidden rounded-full text-white\"\n    >\n      <CircleInfoIcon className=\"size-4\" />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/client-name.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { JSX, useMemo } from 'react'\nimport {\n  OAuthClientMetadata,\n  isConventionalOAuthClientId,\n} from '#/lib/oauth-client.ts'\nimport { Override } from '../../lib/util.ts'\nimport { UrlViewer } from './url-viewer.tsx'\n\nexport type ClientNameProps = Override<\n  Omit<JSX.IntrinsicElements['span'], 'children'>,\n  {\n    clientId: string\n    clientMetadata: OAuthClientMetadata\n    clientTrusted: boolean\n  }\n>\n\nexport function ClientName({\n  clientId,\n  clientMetadata,\n  clientTrusted,\n\n  // span\n  ...attrs\n}: ClientNameProps) {\n  const url = useMemo(() => {\n    try {\n      return new URL(clientId)\n    } catch {\n      return null\n    }\n  }, [clientId])\n\n  if (clientTrusted && clientMetadata.client_name) {\n    return <span {...attrs}>{clientMetadata.client_name}</span>\n  }\n\n  // @NOTE: not using isOAuthClientIdLoopback & isOAuthClientIdDiscoverable from\n  // @atproto/oauth-types here because 1) we don't need to validate here and 2)\n  // we prefer not to import un-necessary code to improve bundle size.\n\n  if (url?.protocol === 'http:') {\n    return (\n      <span {...attrs}>\n        <Trans>An application on your device</Trans>\n      </span>\n    )\n  }\n\n  if (url?.protocol === 'https:') {\n    // Only display the url details if the client id does not follow our\n    // convention.\n    const simplifiedView = isConventionalOAuthClientId(clientId)\n\n    return (\n      <UrlViewer\n        {...attrs}\n        url={url}\n        proto={!simplifiedView}\n        host={true}\n        path={!simplifiedView}\n        query={!simplifiedView}\n        hash={false}\n      />\n    )\n  }\n\n  return <span {...attrs}>{clientId}</span>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/description-card.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { HTMLAttributes, ReactNode, useCallback, useRef, useState } from 'react'\nimport { Button } from '#/components/forms/button.tsx'\nimport { useClickOutside } from '#/hooks/use-click-outside'\nimport { useEscapeKey } from '#/hooks/use-escape-key'\nimport { useRandomString } from '#/hooks/use-random-string.ts'\nimport { Override } from '#/lib/util.ts'\nimport { Admonition } from './admonition'\n\nexport type DescriptionCardProps = Override<\n  HTMLAttributes<HTMLDivElement>,\n  {\n    hint?: string\n    image: ReactNode\n    title?: ReactNode\n    description?: ReactNode\n    append?: ReactNode\n  }\n>\n\nexport function DescriptionCard({\n  hint,\n  image,\n  title,\n  description,\n  append,\n\n  // HTMLDivElement\n  className,\n  children,\n  ...attrs\n}: DescriptionCardProps) {\n  const { t } = useLingui()\n  const [open, setOpen] = useState(false)\n  const close = useCallback(() => setOpen(false), [])\n\n  const ref = useRef<HTMLDivElement>(null)\n  useEscapeKey(close)\n  useClickOutside(ref, close)\n\n  const hasChildren = children != null\n  const detailsDivId = useRandomString('details-card-')\n\n  return (\n    <div ref={ref} className={className} {...attrs}>\n      <div className={`flex items-center justify-start gap-2`}>\n        <div\n          className=\"ml-2 flex w-8 flex-grow-0 items-center justify-center\"\n          aria-hidden\n        >\n          {image}\n        </div>\n        <div\n          className={`flex flex-1 flex-col`}\n          aria-describedby={hasChildren ? detailsDivId : undefined}\n        >\n          {title && <h3>{title}</h3>}\n          {description && <p className=\"text-sm\">{description}</p>}\n        </div>\n        {append && (\n          <div className=\"flex shrink-0 grow-0 items-center justify-center\">\n            {append}\n          </div>\n        )}\n        {hasChildren && (\n          <Button\n            onClick={(event) => {\n              if (!event.defaultPrevented) {\n                event.preventDefault()\n                setOpen((prev) => !prev)\n              }\n            }}\n            shape=\"circle\"\n            title={open ? t`Collapse details` : hint ?? t`Expand details`}\n            aria-expanded={open}\n            aria-label={open ? t`Collapse details` : hint ?? t`Expand details`}\n            aria-haspopup=\"true\"\n            aria-controls={detailsDivId}\n          >\n            ?\n          </Button>\n        )}\n      </div>\n      {hasChildren && (\n        <Admonition\n          className=\"mt-4\"\n          hidden={!open}\n          id={detailsDivId}\n          aria-hidden={!open}\n        >\n          {children}\n        </Admonition>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/error-card.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { memo, useEffect, useMemo, useState } from 'react'\nimport { useRandomString } from '../../hooks/use-random-string.ts'\nimport { Api } from '../../lib/api.ts'\nimport { JsonErrorResponse } from '../../lib/json-client.ts'\nimport { Override } from '../../lib/util.ts'\nimport { Admonition, AdmonitionProps } from './admonition.tsx'\nimport { ErrorMessage } from './error-message.tsx'\n\nexport type ErrorCardProps = Override<\n  Omit<AdmonitionProps, 'role'>,\n  {\n    error: unknown\n  }\n>\nexport const ErrorCard = memo(function ErrorCard({\n  error,\n\n  // Admonition\n  children,\n  onClick,\n  onKeyDown,\n  ...props\n}: ErrorCardProps) {\n  const [inputCount, setInputCount] = useState(0)\n  // Every 5th input will toggle showing the details\n  const showDetails = ((inputCount / 5) | 0) % 2 === 1\n\n  const detailsDivId = useRandomString('error-card-')\n\n  const parsedError = useMemo(\n    () =>\n      error instanceof JsonErrorResponse\n        ? // Already parsed:\n          error\n        : // If \"error\" is a json object, try parsing it as a JsonErrorResponse:\n          Api.parseError(error) ?? error,\n    [error],\n  )\n\n  useEffect(() => {\n    // For debugging purposes\n    console.warn('Displayed error details:', parsedError)\n\n    // Reset the input count when the error changes\n    setInputCount(0)\n  }, [parsedError])\n\n  return (\n    <Admonition\n      prominent\n      {...props}\n      aria-controls={detailsDivId}\n      tabIndex={0}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!event.defaultPrevented) {\n          setInputCount((c) => c + 1)\n        }\n      }}\n      onClick={(event) => {\n        onClick?.(event)\n        if (!event.defaultPrevented) {\n          setInputCount((c) => c + 1)\n        }\n      }}\n      type=\"alert\"\n      title={<ErrorMessage error={parsedError} />}\n    >\n      {children && <div className=\"mt-2\">{children}</div>}\n\n      <div hidden={!showDetails} id={detailsDivId} aria-hidden={!showDetails}>\n        {parsedError instanceof JsonErrorResponse ? (\n          <dl className=\"mt-2 grid grid-cols-[auto_1fr] gap-x-2 text-sm\">\n            <dt className=\"font-semibold\">\n              <Trans>Code</Trans>\n            </dt>\n            <dd>\n              <code>{parsedError.error}</code>\n            </dd>\n\n            <dt className=\"font-semibold\">\n              <Trans>Description</Trans>\n            </dt>\n            <dd>{parsedError.description}</dd>\n          </dl>\n        ) : (\n          <pre className=\"text-xs\">{JSON.stringify(parsedError, null, 2)}</pre>\n        )}\n      </div>\n    </Admonition>\n  )\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/error-message.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { ReactNode, memo } from 'react'\nimport {\n  AccessDeniedError,\n  EmailTakenError,\n  HandleUnavailableError,\n  InvalidCredentialsError,\n  InvalidInviteCodeError,\n  InvalidRequestError,\n  RequestExpiredError,\n  SecondAuthenticationFactorRequiredError,\n  UnknownRequestUriError,\n} from '../../lib/api.ts'\nimport { JsonErrorResponse } from '../../lib/json-client.ts'\n\nexport type ApiErrorMessageProps = {\n  error: unknown\n}\n\nexport const ErrorMessage = memo(function ErrorMessage({\n  error,\n}: ApiErrorMessageProps): ReactNode {\n  // Matches the order of the error checks in the API's parseError method (must\n  // be from most specific to least specific to avoid unreachable code paths).\n\n  if (error instanceof SecondAuthenticationFactorRequiredError) {\n    return <Trans>A second authentication factor is required</Trans>\n  }\n\n  if (error instanceof InvalidCredentialsError) {\n    return <Trans>Wrong identifier or password</Trans>\n  }\n\n  if (error instanceof InvalidInviteCodeError) {\n    return <Trans>The invite code is not valid</Trans>\n  }\n\n  if (error instanceof HandleUnavailableError) {\n    switch (error.reason) {\n      case 'syntax':\n        return <Trans>The handle is invalid</Trans>\n      case 'domain':\n        return <Trans>The domain name is not allowed</Trans>\n      case 'slur':\n        return <Trans>The handle contains inappropriate language</Trans>\n      case 'taken':\n        if (error.description === 'Reserved handle') {\n          return <Trans>This handle is reserved</Trans>\n        }\n        return <Trans>The handle is already in use</Trans>\n      default:\n        return <Trans>That handle cannot be used</Trans>\n    }\n  }\n\n  if (error instanceof EmailTakenError) {\n    return <Trans>This email is already used</Trans>\n  }\n\n  if (\n    error instanceof UnknownRequestUriError ||\n    error instanceof RequestExpiredError\n  ) {\n    return <Trans>This sign-in session has expired</Trans>\n  }\n\n  if (error instanceof InvalidRequestError) {\n    return (\n      <Trans>\n        The data you submitted is invalid. Please check the form and try again.\n      </Trans>\n    )\n  }\n\n  if (error instanceof AccessDeniedError) {\n    return (\n      <Trans>\n        This authorization request has been denied. Please try again.\n      </Trans>\n    )\n  }\n\n  if (error instanceof JsonErrorResponse) {\n    return <Trans>Unexpected server response</Trans>\n  }\n\n  return <Trans>An unknown error occurred</Trans>\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/help-card.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX } from 'react'\nimport type { LinkDefinition } from '@atproto/oauth-provider-api'\nimport { Override } from '../../lib/util.ts'\n\nexport type HelpCardProps = Override<\n  Omit<JSX.IntrinsicElements['p'], 'children'>,\n  {\n    links?: readonly LinkDefinition[]\n  }\n>\n\nexport function HelpCard({\n  links,\n\n  className,\n  ...props\n}: HelpCardProps) {\n  const helpLink = links?.find((l) => l.rel === 'help')\n\n  if (!helpLink) return null\n\n  return (\n    <p\n      {...props}\n      className={clsx(\n        'rounded-md bg-slate-100 p-3 text-sm text-slate-800 dark:bg-slate-800 dark:text-slate-400',\n        className,\n      )}\n    >\n      <Trans>\n        Having trouble?{' '}\n        <a\n          role=\"link\"\n          href={helpLink.href}\n          rel={helpLink.rel}\n          target=\"_blank\"\n          className=\"text-primary\"\n        >\n          <Trans>Contact support</Trans>\n        </a>\n      </Trans>\n    </p>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/icons.tsx",
    "content": "import { type FunctionComponent, type JSX, forwardRef } from 'react'\nimport { Override } from '../../lib/util.ts'\n\nexport type IconProps = Override<\n  Omit<JSX.IntrinsicElements['svg'], 'viewBox' | 'children' | 'xmlns'>,\n  {\n    /**\n     * The title of the icon, used for accessibility.\n     */\n    title?: string\n  }\n>\n\nconst makeSvgComponent = (path: string, displayName: string) => {\n  const SvgComponent: FunctionComponent<IconProps> = forwardRef(\n    ({ title, ...props }, ref) => (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        {...props}\n        ref={ref}\n        aria-hidden={!title}\n      >\n        {title && <title>{title}</title>}\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d={path}\n        ></path>\n      </svg>\n    ),\n  )\n  SvgComponent.displayName = displayName\n  return SvgComponent\n}\n\nexport const AtomIcon = makeSvgComponent(\n  'M6.17 5.004c-.553-.029-.814.107-.937.23-.122.122-.258.383-.23.936.03.552.222 1.28.611 2.15.282.628.655 1.304 1.112 2.005a28.26 28.26 0 0 1 1.72-1.88 28.258 28.258 0 0 1 1.88-1.719 14.886 14.886 0 0 0-2.007-1.112c-.868-.39-1.597-.581-2.15-.61Zm5.83.44c-.985-.688-1.953-1.247-2.863-1.655-.998-.447-1.978-.736-2.863-.782-.885-.047-1.791.148-2.455.812-.664.664-.859 1.57-.812 2.455.046.885.335 1.865.782 2.863.408.91.967 1.878 1.655 2.863-.688.985-1.247 1.953-1.655 2.863-.447.998-.736 1.978-.782 2.863-.047.885.148 1.791.812 2.455.664.663 1.57.859 2.455.812.885-.046 1.865-.335 2.863-.782.91-.408 1.878-.967 2.863-1.655.985.688 1.952 1.247 2.863 1.655.998.447 1.978.736 2.863.782.885.047 1.791-.148 2.455-.812.663-.664.859-1.57.812-2.455-.046-.885-.335-1.865-.782-2.863-.408-.91-.967-1.878-1.655-2.863.688-.985 1.247-1.952 1.655-2.863.447-.998.736-1.978.782-2.863.047-.885-.148-1.791-.812-2.455-.664-.664-1.57-.859-2.455-.812-.885.046-1.865.335-2.863.782-.91.408-1.878.967-2.863 1.655Zm0 2.497A25.9 25.9 0 0 0 9.86 9.86 25.899 25.899 0 0 0 7.94 12c.569.711 1.211 1.431 1.92 2.14.709.709 1.429 1.351 2.14 1.92a25.925 25.925 0 0 0 2.14-1.92A25.925 25.925 0 0 0 16.06 12a25.921 25.921 0 0 0-1.92-2.14A25.904 25.904 0 0 0 12 7.94Zm5.274 2.384a28.232 28.232 0 0 0-1.72-1.88 28.27 28.27 0 0 0-1.88-1.719 14.89 14.89 0 0 1 2.007-1.112c.868-.39 1.597-.581 2.15-.61.552-.029.813.107.936.23.123.122.258.383.23.936-.03.552-.222 1.28-.611 2.15a14.883 14.883 0 0 1-1.112 2.005Zm0 3.35a28.24 28.24 0 0 1-1.72 1.88 28.24 28.24 0 0 1-1.88 1.719c.702.457 1.378.83 2.007 1.112.868.39 1.597.581 2.15.61.552.03.813-.106.936-.23.123-.122.258-.383.23-.935-.03-.553-.222-1.282-.611-2.15a14.888 14.888 0 0 0-1.112-2.006Zm-6.949 3.599a28.23 28.23 0 0 1-1.88-1.72 28.27 28.27 0 0 1-1.719-1.88 14.89 14.89 0 0 0-1.112 2.007c-.39.868-.581 1.597-.61 2.15-.029.552.107.813.23.936.122.123.383.258.936.23.552-.03 1.28-.222 2.15-.611a14.884 14.884 0 0 0 2.005-1.112Z',\n  'AtomIcon',\n)\n\nexport const AuthenticateIcon = makeSvgComponent(\n  'M17.71 6.15C17.46 5.38 16.79 5.21 16.45 4.77C16.14 4.31 16.18 3.62 15.53 3.15S14.23 2.92 13.7 2.77 12.81 2 12 2 10.82 2.58 10.3 2.77 9.13 2.67 8.47 3.15 7.86 4.31 7.55 4.77C7.21 5.21 6.55 5.38 6.29 6.15S6.5 7.45 6.5 8 6 9.08 6.29 9.85 7.21 10.79 7.55 11.23C7.86 11.69 7.82 12.38 8.47 12.85S9.77 13.08 10.3 13.23 11.19 14 12 14 13.18 13.42 13.7 13.23 14.87 13.33 15.53 12.85 16.14 11.69 16.45 11.23C16.79 10.79 17.45 10.62 17.71 9.85S17.5 8.55 17.5 8 18 6.92 17.71 6.15M12 12A4 4 0 1 1 16 8A4 4 0 0 1 12 12M14 8A2 2 0 1 1 12 6A2 2 0 0 1 14 8M13.71 15.56L13.08 19.16L12.35 23.29L9.74 20.8L6.44 22.25L7.77 14.75A4 4 0 0 0 9.66 15.17A4.15 4.15 0 0 0 11 15.85A3.32 3.32 0 0 0 12 16A3.5 3.5 0 0 0 13.71 15.56M17.92 18.78L15.34 17.86L15.85 14.92A3.2 3.2 0 0 0 16.7 14.47L16.82 14.37Z',\n  'AuthenticateIcon',\n)\n\nexport const AccountIcon = makeSvgComponent(\n  'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z',\n  'AccountIcon',\n)\n\nexport const AccountOutlinedIcon = makeSvgComponent(\n  'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z',\n  'AccountIcon',\n)\n\nexport const ArrowTop = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Arrow.tsx\n  'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z',\n  'ArrowTop',\n)\n\nexport const AlertIcon = makeSvgComponent(\n  'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z',\n  'AlertIcon',\n)\n\nexport const AtSymbolIcon = makeSvgComponent(\n  'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z',\n  'AtSymbolIcon',\n)\n\nexport const ButterflyIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Logo.tsx#L4\n  'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z',\n  'ButterflyIcon',\n)\n\nexport const ChevronRightIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Chevron.tsx\n  'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',\n  'ChevronRightIcon',\n)\n\nexport const ChatIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Message.tsx\n  'M4 12a8 8 0 1 1 4.445 7.169 1 1 0 0 0-.629-.088l-3.537.662.7-3.415a1 1 0 0 0-.09-.66A7.961 7.961 0 0 1 4 12Zm8-10C6.477 2 2 6.477 2 12c0 1.523.341 2.968.951 4.262l-.93 4.537a1 1 0 0 0 1.163 1.184l4.68-.876A9.968 9.968 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2ZM7.5 13.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Zm4.5 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',\n  'ChatIcon',\n)\n\nexport const CheckMarkIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Check.tsx\n  'M21.59 3.193a1 1 0 0 1 .217 1.397l-11.706 16a1 1 0 0 1-1.429.193l-6.294-5a1 1 0 1 1 1.244-1.566l5.48 4.353 11.09-15.16a1 1 0 0 1 1.398-.217Z',\n  'CheckMarkIcon',\n)\n\nexport const CircleInfoIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/CircleInfo.tsx#L3\n  'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z',\n  'CircleInfoIcon',\n)\n\nexport const EmailIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Envelope.tsx\n  'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z',\n  'EmailIcon',\n)\n\nexport const EyeIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Eye.tsx\n  'M3.135 12C5.413 16.088 8.77 18 12 18s6.587-1.912 8.865-6C18.587 7.912 15.23 6 12 6c-3.228 0-6.587 1.912-8.865 6ZM12 4c4.24 0 8.339 2.611 10.888 7.54a1 1 0 0 1 0 .92C20.338 17.388 16.24 20 12 20c-4.24 0-8.339-2.611-10.888-7.54a1 1 0 0 1 0-.92C3.662 6.612 7.76 4 12 4Zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z',\n  'EyeIcon',\n)\n\nexport const EyeSlashIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/EyeSlash.tsx\n  'M2.293 2.293a1 1 0 0 1 1.414 0L7.335 5.92l.03.03 3.22 3.222 4.243 4.242 3.22 3.22.03.03 3.63 3.629a1 1 0 0 1-1.415 1.414l-3.09-3.09c-2.65 1.478-5.625 1.778-8.421.869-3.039-.987-5.779-3.37-7.67-7.027a1 1 0 0 1 0-.918c1.086-2.1 2.452-3.78 3.996-5.019L2.293 3.707a1 1 0 0 1 0-1.414Zm4.24 5.654 2.021 2.021a4 4 0 0 0 5.478 5.478l1.688 1.688c-2.042.982-4.246 1.124-6.32.45-2.34-.76-4.594-2.586-6.265-5.584.97-1.739 2.135-3.083 3.398-4.053Zm3.535 3.535 2.45 2.45a2 2 0 0 1-2.45-2.45Zm.81-5.405c3.573-.49 7.45 1.369 9.987 5.923a14.797 14.797 0 0 1-1.347 2.02 1 1 0 1 0 1.564 1.247 17.078 17.078 0 0 0 1.806-2.808 1 1 0 0 0 0-.918c-2.833-5.479-7.584-8.088-12.281-7.446a1 1 0 0 0 .271 1.982Z',\n  'EyeSlashIcon',\n)\n\nexport const GlobeIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Globe.tsx\n  'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z',\n  'GlobeIcon',\n)\n\nexport const ImageIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Image.tsx\n  'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',\n  'ImageIcon',\n)\n\nexport const LockIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Lock.tsx\n  'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',\n  'LockIcon',\n)\n\nexport const NewspaperIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/Newspaper.tsx\n  'M1 6.5A2.5 2.5 0 0 1 3.5 4H9a4 4 0 0 1 3 1.354A4 4 0 0 1 15 4h5.5A2.5 2.5 0 0 1 23 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-5.223c-.52 0-1 .125-1.4.372-.421.26-.761.633-.983 1.075a1 1 0 0 1-1.788 0 2.66 2.66 0 0 0-.983-1.075c-.4-.247-.88-.372-1.4-.372H3.5A2.5 2.5 0 0 1 1 17.5v-11ZM11 8a2 2 0 0 0-2-2H3.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h5.223c.776 0 1.564.173 2.277.569V8Zm2 10.569A4.7 4.7 0 0 1 15.277 18H20.5a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5H15a2 2 0 0 0-2 2v10.569Z',\n  'NewspaperIcon',\n)\n\nexport const PaperPlaneIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/PaperPlane.tsx#L4\n  'M3.374 3.22a1 1 0 0 1 1.073-.114l16 8a1 1 0 0 1 0 1.788l-16 8a1 1 0 0 1-1.417-1.136L4.97 12 3.03 4.243a1 1 0 0 1 .344-1.023ZM6.781 13l-1.284 5.133L17.764 12 5.497 5.867 6.781 11H9a1 1 0 1 1 0 2H6.78Z',\n  'PaperPlaneIcon',\n)\n\nexport const RaisingHandIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/RaisingHand.tsx\n  'M12.5 4a.5.5 0 0 0-.5.5V10a1 1 0 1 1-2 0V5.5a.5.5 0 0 0-1 0V11a1 1 0 1 1-2 0V7.5a.5.5 0 0 0-1 0v6a6.5 6.5 0 1 0 13 0V9c-.513 0-.979.192-1.333.509-.41.368-.667.899-.667 1.491v.838c0 .826-.529 1.559-1.312 1.82A2.47 2.47 0 0 0 14 16a1 1 0 1 1-2 0 4.47 4.47 0 0 1 3-4.22V11c0-1.014.379-1.941 1-2.646V5.5a.5.5 0 0 0-1 0V10a1 1 0 1 1-2 0V4.5a.5.5 0 0 0-.5-.5Zm2.112-.838A2.5 2.5 0 0 1 18 5.5v1.626q.481-.124 1-.126a2 2 0 0 1 2 2v4.5a8.5 8.5 0 0 1-17 0v-6a2.5 2.5 0 0 1 3.039-2.442 2.5 2.5 0 0 1 3.349-1.896 2.498 2.498 0 0 1 4.224 0Z',\n  'RaisingHandIcon',\n)\n\nexport const TokenIcon = makeSvgComponent(\n  'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',\n  'TokenIcon',\n)\n\nexport const VideoClipIcon = makeSvgComponent(\n  // https://github.com/bluesky-social/social-app/blob/b32568260f98ea879468fd1bdedacf85d1e6ae8c/src/components/icons/VideoClip.tsx\n  'M3 4a1 1 0 011-1h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2h2V13Zm0 4h-2V19h2ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z',\n  'TokenIcon',\n)\n\nexport const XMarkIcon = makeSvgComponent(\n  'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z',\n  'XMarkIcon',\n)\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/lang-string.tsx",
    "content": "import { ReactNode, useMemo } from 'react'\nimport { getLangString } from '#/lib/lang'\nimport { useCurrentLocale } from '#/locales/locale-provider'\nimport type { MultiLangString } from '@atproto/oauth-provider-api'\n\nexport type LangStringProps = {\n  value?: string | MultiLangString\n  fallback?: ReactNode\n}\n\nexport function LangString({ value, fallback }: LangStringProps): ReactNode {\n  const matchingString = useLangString(value)\n  return (\n    matchingString ||\n    fallback ||\n    // If a fallback is not provided, return the english version, if it exists,\n    // or any string otherwise\n    (typeof value === 'object'\n      ? value['en'] || Object.values(value).find(Boolean)\n      : value)\n  )\n}\n\nexport function useLangString(\n  value?: string | MultiLangString,\n  fallback?: string,\n) {\n  const locale = useCurrentLocale()\n  return useMemo(() => {\n    return getLangString(value, locale, fallback)\n  }, [value, locale, fallback])\n}\n\nexport function LangProp<\n  P extends string,\n  O extends {\n    [K in P]?: string\n  } & {\n    [K in P as `${K}:lang`]?: MultiLangString\n  },\n>({\n  object,\n  property,\n  fallback,\n}: {\n  property: P\n  object?: O\n  fallback?: ReactNode\n}): ReactNode {\n  if (!object) return fallback\n  return (\n    <LangString\n      value={object[`${property}:lang` as keyof O]}\n      fallback={object[property] ?? fallback}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/link-anchor.tsx",
    "content": "import { JSX } from 'react'\nimport type { LinkDefinition } from '@atproto/oauth-provider-api'\nimport { Override } from '../../lib/util.ts'\nimport { LinkTitle } from './link-title.tsx'\n\nexport type LinkAnchorProps = Override<\n  JSX.IntrinsicElements['a'],\n  {\n    link: LinkDefinition\n  }\n>\nexport function LinkAnchor({\n  link,\n\n  // a\n  children = <LinkTitle link={link} />,\n  role = 'link',\n  target = '_blank',\n  href = link.href,\n  rel = link.rel,\n  ...props\n}: LinkAnchorProps) {\n  return (\n    <a {...props} role={role} target={target} href={href} rel={rel}>\n      {children}\n    </a>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/link-title.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { ReactNode } from 'react'\nimport type { LinkDefinition } from '@atproto/oauth-provider-api'\nimport { useLangString } from './lang-string.tsx'\n\nexport type LinkNameProps = {\n  link: LinkDefinition\n}\n\nexport function LinkTitle({ link }: LinkNameProps): ReactNode {\n  const { t } = useLingui()\n\n  const title = useLangString(link.title)\n  if (title) return title\n\n  // Fallback\n  if (link.rel === 'canonical') return t`Home`\n  if (link.rel === 'privacy-policy') return t`Privacy Policy`\n  if (link.rel === 'terms-of-service') return t`Terms of Service`\n  if (link.rel === 'help') return t`Support`\n\n  // English version\n  return typeof link.title === 'object'\n    ? link.title['en'] || Object.values(link.title).find(Boolean)\n    : link.title\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/page-footer.tsx",
    "content": "import { JSX } from 'react'\nimport { Override } from '#/lib/util'\nimport { LocaleSelector } from '#/locales/locale-selector.tsx'\nimport type {\n  CustomizationData,\n  LinkDefinition,\n} from '@atproto/oauth-provider-api'\nimport { LinkAnchor } from './link-anchor.tsx'\n\nexport type { CustomizationData }\nexport type PageFooterProps = Override<\n  JSX.IntrinsicElements['footer'],\n  {\n    links?: LinkDefinition[]\n  }\n>\n\nexport function PageFooter({\n  links,\n\n  // footer\n  className = '',\n  ...props\n}: PageFooterProps) {\n  return (\n    <footer\n      className={`bg-contrast-25 dark:bg-contrast-50 flex w-full flex-wrap items-center justify-between overflow-hidden px-4 md:px-6 ${className}`}\n      {...props}\n    >\n      <nav className=\"flex flex-wrap items-center justify-start\">\n        {links?.map((link, i) => (\n          <LinkAnchor\n            key={i}\n            link={link}\n            className=\"text-text-light m-2 text-xs hover:underline md:m-4 md:text-sm\"\n          />\n        ))}\n      </nav>\n\n      <LocaleSelector className=\"m-1 text-xs md:m-2 md:text-sm\" />\n    </footer>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/password-strength-label.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { JSX } from 'react'\nimport { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'\nimport { Override } from '../../lib/util.ts'\n\nexport type PasswordStrengthLabelProps = Override<\n  Omit<JSX.IntrinsicElements['span'], 'children' | 'aria-label'>,\n  {\n    password: string\n  }\n>\n\nexport function PasswordStrengthLabel({\n  password,\n\n  // span\n  ...props\n}: PasswordStrengthLabelProps) {\n  const { t } = useLingui()\n  const strength = getPasswordStrength(password)\n\n  return (\n    <span {...props} aria-label={t`Password strength`}>\n      {strength === PasswordStrength.extra ? (\n        <Trans>Extra</Trans>\n      ) : strength === PasswordStrength.strong ? (\n        <Trans>Strong</Trans>\n      ) : strength === PasswordStrength.moderate ? (\n        <Trans>Moderate</Trans>\n      ) : password ? (\n        <Trans>Weak</Trans>\n      ) : (\n        <Trans>Missing</Trans>\n      )}\n    </span>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/password-strength-meter.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX } from 'react'\nimport { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'\nimport { Override } from '../../lib/util.ts'\n\nexport type PasswordStrengthMeterProps = Override<\n  Omit<\n    JSX.IntrinsicElements['div'],\n    | 'children'\n    | 'role'\n    | 'aria-label'\n    | 'aria-valuemin'\n    | 'aria-valuemax'\n    | 'aria-valuenow'\n  >,\n  {\n    password: string\n  }\n>\n\nexport function PasswordStrengthMeter({\n  password,\n\n  // div\n  className,\n  ...props\n}: PasswordStrengthMeterProps) {\n  const { t } = useLingui()\n  const strength = password ? getPasswordStrength(password) : 0\n\n  const colorBg = 'bg-gray-300 dark:bg-slate-500'\n  const color =\n    strength === PasswordStrength.extra || strength === PasswordStrength.strong\n      ? 'bg-success'\n      : strength === PasswordStrength.moderate\n        ? 'bg-warning'\n        : 'bg-error'\n\n  return (\n    <div\n      {...props}\n      className={clsx('flex h-1 w-full space-x-2', className)}\n      role=\"meter\"\n      aria-label={t`Password strength indicator`}\n      aria-valuemin={0}\n      aria-valuemax={PasswordStrength.extra}\n      aria-valuenow={strength}\n    >\n      {Array.from({ length: 4 }, (_, i) => (\n        <div\n          key={i}\n          className={`h-1 w-1/4 rounded-sm ${strength > i ? color : colorBg}`}\n        />\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/scope-description.tsx",
    "content": "import type { PermissionSet, PermissionSets } from '#/hydration-data.d.ts'\nimport { Trans, useLingui } from '@lingui/react/macro'\nimport { Fragment, HTMLAttributes, ReactNode, useMemo } from 'react'\nimport { Override } from '#/lib/util'\nimport {\n  AudParam,\n  BlobPermission,\n  CollectionParam,\n  IncludeScope,\n  LxmParam,\n  RepoPermission,\n  RpcPermission,\n  ScopePermissionsTransition,\n} from '@atproto/oauth-scopes'\nimport { Checkbox } from '../forms/checkbox'\nimport { Admonition, AdmonitionProps } from './admonition'\nimport { DescriptionCard } from './description-card'\nimport {\n  AccountOutlinedIcon,\n  AtSymbolIcon,\n  AtomIcon,\n  AuthenticateIcon,\n  ButterflyIcon,\n  ChatIcon,\n  CheckMarkIcon,\n  EmailIcon,\n  NewspaperIcon,\n  RaisingHandIcon,\n} from './icons'\nimport { LangProp } from './lang-string'\n\nexport type ScopeDescriptionProps = Override<\n  HTMLAttributes<HTMLDivElement>,\n  {\n    clientTrusted?: boolean\n    clientFirstParty?: boolean\n    scope?: string\n    permissionSets: PermissionSets\n\n    allowEmail?: boolean\n    onAllowEmail?: (allowed: boolean) => void\n  }\n>\n\nexport function ScopeDescription({\n  scope,\n  permissionSets,\n  clientTrusted = false,\n  clientFirstParty = false,\n  allowEmail,\n  onAllowEmail,\n\n  // div\n  className = '',\n  ...attrs\n}: ScopeDescriptionProps) {\n  const includeScopes = useMemo(() => {\n    return Array.from(\n      new Set(\n        scope\n          ?.split(' ')\n          .map((v) => IncludeScope.fromString(v))\n          .filter((v) => v != null),\n      ),\n    )\n  }, [scope])\n  const permissions = useMemo(() => {\n    return new ScopePermissionsTransition(scope)\n  }, [scope])\n\n  if (permissions.scopes.size === 0) return null\n  if (permissions.scopes.size === 1 && permissions.scopes.has('atproto')) {\n    return null\n  }\n\n  return (\n    <div className={`flex flex-col gap-2 ${className}`} {...attrs} role=\"list\">\n      <EmailPermissions\n        permissions={permissions}\n        allowEmail={allowEmail}\n        onAllowEmail={onAllowEmail}\n      />\n      <IdentityPermissions permissions={permissions} />\n      <AccountPermissions permissions={permissions} />\n\n      {/* Bluesky business logic specific scopes */}\n      <BlueskyAppviewPermissions permissions={permissions} />\n      <BlueskyChatPermissions permissions={permissions} />\n\n      <IncludedPermissions\n        includeScopes={includeScopes}\n        permissionSets={permissionSets}\n      />\n\n      <FineGrainedPermissions permissions={permissions} />\n\n      {(!clientFirstParty || !clientTrusted) && (\n        <IdentityWarning className=\"mt-2\" permissions={permissions} />\n      )}\n    </div>\n  )\n}\n\nfunction IncludedPermissions({\n  includeScopes,\n  permissionSets,\n}: {\n  includeScopes: IncludeScope[]\n  permissionSets: PermissionSets\n}) {\n  if (!includeScopes.length) return null\n\n  return (\n    <>\n      {includeScopes.map((includeScope, i) => (\n        <IncludeScopePermissions\n          key={i}\n          includeScope={includeScope}\n          permissionSet={permissionSets[includeScope.nsid]}\n        />\n      ))}\n    </>\n  )\n}\n\nfunction IncludeScopePermissions({\n  includeScope,\n  permissionSet,\n}: {\n  includeScope: IncludeScope\n  permissionSet?: PermissionSet\n}) {\n  const { nsid } = includeScope\n\n  const permissions = useMemo(() => {\n    if (!permissionSet) return null\n    return new ScopePermissionsTransition(includeScope.toScopes(permissionSet))\n  }, [includeScope, permissionSet])\n\n  return (\n    <DescriptionCard\n      role=\"listitem\"\n      image={\n        isBskyAppNsid(nsid) ? (\n          <ButterflyIcon className=\"size-6\" />\n        ) : isBskyChatNsid(nsid) ? (\n          <ChatIcon className=\"size-6\" />\n        ) : nsid.startsWith('com.atproto.moderation.') ? (\n          <RaisingHandIcon className=\"size-6\" />\n        ) : (\n          <AtomIcon className=\"size-6\" />\n        )\n      }\n      title={\n        <LangProp object={permissionSet} property=\"title\" fallback={nsid} />\n      }\n      description={\n        <LangProp\n          object={permissionSet}\n          property=\"detail\"\n          fallback={\n            // Do not set the \"nsid\" as fallback for the \"detail\" if is was already used when displaying the \"title\"\n            permissionSet?.title ? nsid : null\n          }\n        />\n      }\n    >\n      <p className=\"mt-1\">\n        <Trans>\n          The application requests the permissions necessary to perform the\n          following actions on your behalf:\n        </Trans>\n      </p>\n      {permissions ? (\n        <>\n          <RpcMethodsTable className=\"mt-2\" permissions={permissions} />\n          <RepoTable className=\"mt-2\" permissions={permissions} />\n        </>\n      ) : null}\n    </DescriptionCard>\n  )\n}\n\nfunction IdentityWarning({\n  permissions,\n\n  // Admonition\n  type = 'alert',\n  prominent = true,\n  ...props\n}: {\n  permissions: ScopePermissionsTransition\n} & AdmonitionProps) {\n  const hasFullIdentityAccess = useMemo(() => {\n    return permissions.allowsIdentity({ attr: '*' })\n  }, [permissions])\n\n  if (hasFullIdentityAccess) {\n    return (\n      <Admonition {...props} type={type} prominent={prominent}>\n        <p>\n          <Trans>\n            The application is asking for full control over your network\n            identity, meaning that it could <b>permanently break</b>, or even{' '}\n            <b>steal</b>, your account. Only grant this permission to\n            applications you really trust.\n          </Trans>\n        </p>\n      </Admonition>\n    )\n  }\n\n  return null\n}\n\nfunction EmailPermissions({\n  permissions,\n  allowEmail,\n  onAllowEmail,\n}: {\n  permissions: ScopePermissionsTransition\n  allowEmail?: boolean\n  onAllowEmail?: (allowed: boolean) => void\n}) {\n  const { t } = useLingui()\n\n  const allowedAction = useMemo(() => {\n    return (['manage', 'read'] as const).find((action) =>\n      permissions.allowsAccount({ attr: 'email', action }),\n    )\n  }, [permissions])\n\n  if (allowedAction) {\n    return (\n      <label className={onAllowEmail ? 'cursor-pointer' : undefined}>\n        <DescriptionCard\n          role=\"listitem\"\n          image={<EmailIcon className=\"size-6\" />}\n          title={t`Email`}\n          description={\n            allowedAction === 'manage' ? (\n              <Trans>Read and update your account's email address</Trans>\n            ) : (\n              <Trans>Read your account's email address</Trans>\n            )\n          }\n          append={\n            onAllowEmail && (\n              <Checkbox\n                className=\"m-2\"\n                checked={allowEmail}\n                onChange={(e) => onAllowEmail(e.target.checked)}\n              />\n            )\n          }\n        />\n      </label>\n    )\n  }\n\n  return null\n}\n\nfunction AccountPermissions({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const { t } = useLingui()\n\n  // @NOTE \"account:email\" already covered by EmailPermissions\n  // @NOTE \"account:repo?action=manage\" already covered by RepoPermissions\n\n  if (permissions.allowsAccount({ attr: 'status', action: 'manage' })) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<AccountOutlinedIcon className=\"size-6\" />}\n        title={t`Account`}\n        description={t`Temporarily activate or deactivate your account`}\n      />\n    )\n  }\n\n  return null\n}\n\n/**\n * Will display detailed rep and rpc permissions unless the app only has\n * app.bsky or chat.bsky specific permissions, in which case the\n * <BlueskyAppviewPermissions /> and <BlueskyChatPermissions /> components cover\n * them.\n */\nfunction FineGrainedPermissions({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const hasOnlyBskyAppSpecificPermissions = useMemo(() => {\n    if (permissions.allowsAccount({ attr: 'repo', action: 'manage' })) {\n      return false\n    }\n\n    let foundOne = false\n\n    for (const s of permissions.scopes) {\n      const rpc = RpcPermission.fromString(s)\n      if (rpc) {\n        foundOne = true\n        if (isOfficialBlueskyAppviewServiceId(rpc.aud)) continue\n        if (rpc.lxm.every(isBlueskySpecificNsid)) continue\n        return false\n      }\n\n      const repo = RepoPermission.fromString(s)\n      if (repo) {\n        foundOne = true\n        if (repo.collection.every(isBlueskySpecificNsid)) continue\n        return false\n      }\n    }\n\n    return foundOne\n  }, [permissions])\n\n  if (hasOnlyBskyAppSpecificPermissions) return null\n\n  return (\n    <>\n      <RepoPermissions permissions={permissions} />\n      <RpcMethodsDetails permissions={permissions} />\n    </>\n  )\n}\n\nfunction BlueskyAppviewPermissions({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const hasBskyAppRepo = useMemo(() => {\n    return permissions.scopes.some(scopeEnablesBskyAppRepo)\n  }, [permissions])\n\n  const hasBskyAppRpc = useMemo(() => {\n    return permissions.scopes.some(scopeEnablesPrivateBskyAppMethods)\n  }, [permissions])\n\n  if (hasBskyAppRepo || hasBskyAppRpc) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<ButterflyIcon className=\"size-6\" />}\n        title={'Bluesky'}\n        description={\n          hasBskyAppRepo && hasBskyAppRpc ? (\n            <Trans>\n              Manage your profile, posts, likes and follows as well as read your\n              private preferences\n            </Trans>\n          ) : (\n            <Trans>Manage your profile, posts, likes and follows</Trans>\n          )\n        }\n      />\n    )\n  }\n\n  return null\n}\n\nfunction BlueskyChatPermissions({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const { t } = useLingui()\n\n  const enablesChat = useMemo(() => {\n    return (\n      permissions.hasTransitionChatBsky ||\n      permissions.scopes.some(scopeEnablesChat)\n    )\n  }, [permissions])\n\n  if (enablesChat) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<ChatIcon className=\"size-6\" />}\n        title={t`Chat`}\n        description={t`Read and send messages`}\n      />\n    )\n  }\n\n  return null\n}\n\nfunction IdentityPermissions({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const { t } = useLingui()\n\n  const attr = useMemo(() => {\n    if (permissions.allowsIdentity({ attr: '*' })) {\n      return '*' as const\n    }\n\n    if (permissions.allowsIdentity({ attr: 'handle' })) {\n      return 'handle' as const\n    }\n\n    return null\n  }, [permissions])\n\n  if (attr) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<AtSymbolIcon className=\"h-6\" />}\n        title={t`Identity`}\n        description={\n          attr === '*' ? (\n            <Trans>\n              Manage your <b>full identity</b> including your <b>@handle</b>\n            </Trans>\n          ) : (\n            <Trans>\n              Change your <b>@handle</b>\n            </Trans>\n          )\n        }\n      />\n    )\n  }\n\n  return null\n}\n\nfunction RpcMethodsDetails({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const { t } = useLingui()\n\n  if (permissions.hasTransitionGeneric) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<AuthenticateIcon className=\"size-6\" />}\n        title={t`Authenticate`}\n        description={\n          <Trans>\n            Perform authenticated actions towards <b>any service</b> on your\n            behalf\n          </Trans>\n        }\n      >\n        <p>\n          <RpcDescription />\n        </p>\n      </DescriptionCard>\n    )\n  }\n\n  if (permissions.scopes.some((s) => RpcPermission.fromString(s) != null)) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<AuthenticateIcon className=\"size-6\" />}\n        title={t`Authenticate`}\n        description={t`Perform actions on your behalf`}\n      >\n        <p>\n          <RpcDescription />\n        </p>\n        <p className=\"mt-1\">\n          <Trans>\n            The application requests the permissions necessary to perform the\n            following actions on your behalf:\n          </Trans>\n        </p>\n        <RpcMethodsTable className=\"mt-2\" permissions={permissions} />\n      </DescriptionCard>\n    )\n  }\n\n  return null\n}\n\nfunction RpcDescription() {\n  return (\n    <Trans>\n      The ATProto network uses an authentication mechanism that allows to\n      uniquely identify users when communicating with external services. This is\n      typically used to retrieve or update data linked to your account, such as\n      feed or moderation content.\n    </Trans>\n  )\n}\n\ntype RpcMethodsTableProps = Override<\n  HTMLAttributes<HTMLTableElement>,\n  {\n    permissions: ScopePermissionsTransition\n    children?: never\n  }\n>\nfunction RpcMethodsTable({\n  permissions,\n  className = '',\n  ...attrs\n}: RpcMethodsTableProps) {\n  const audLxmsEntries = useMemo(() => {\n    const map = new Map<AudParam, Set<LxmParam>>()\n\n    for (const s of permissions.scopes) {\n      const parsed = RpcPermission.fromString(s)\n      if (!parsed) continue\n\n      let set = map.get(parsed.aud)\n      if (!set) map.set(parsed.aud, (set = new Set()))\n      for (const lxm of parsed.lxm) set.add(lxm)\n    }\n\n    return Array.from(map.entries())\n      .sort(([a], [b]) => a.localeCompare(b))\n      .map(\n        ([aud, lxms]) =>\n          [\n            aud,\n            lxms.has('*')\n              ? (['*'] as const)\n              : Array.from(lxms).sort((a, b) => a.localeCompare(b)),\n          ] as const,\n      )\n  }, [permissions])\n\n  if (!audLxmsEntries.length) return null\n\n  return (\n    <table className={`w-full table-auto ${className}`} {...attrs}>\n      <thead>\n        <tr className=\"text-sm\">\n          <th className=\"text-left font-normal\">\n            <Trans context=\"RPC lxm\">Call</Trans>\n          </th>\n          <th className=\"text-left font-normal\">\n            <Trans context=\"RPC aud\">Towards</Trans>\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {audLxmsEntries.map(([aud, lxms]) =>\n          lxms.map((lxm, i, array) => (\n            <tr key={lxm} className=\"text-xs\">\n              <td className={i > 0 ? 'pt-1' : undefined}>\n                <Lxm lxm={lxm} />\n              </td>\n              {i === 0 && (\n                <td className=\"align-top\" rowSpan={array.length}>\n                  <Aud aud={aud} />\n                </td>\n              )}\n            </tr>\n          )),\n        )}\n      </tbody>\n    </table>\n  )\n}\n\nfunction RepoPermissions({\n  permissions,\n}: {\n  permissions: ScopePermissionsTransition\n}) {\n  const { t } = useLingui()\n\n  if (\n    permissions.hasTransitionGeneric ||\n    permissions.allowsAccount({ attr: 'repo', action: 'manage' }) ||\n    (permissions.allowsRepo({ collection: '*', action: 'create' }) &&\n      permissions.allowsRepo({ collection: '*', action: 'delete' }) &&\n      permissions.allowsRepo({ collection: '*', action: 'update' }))\n  ) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<NewspaperIcon className=\"size-6\" />}\n        title={t`Repository`}\n        description={t`Create, update, and delete any public record`}\n      >\n        <p>\n          <RepoDescription />\n        </p>\n        <p className=\"mt-1\">\n          <Trans>\n            The application is asking to be able to create, update, and delete{' '}\n            <b>any data</b> from your repository.\n          </Trans>\n        </p>\n      </DescriptionCard>\n    )\n  }\n\n  if (permissions.scopes.some((s) => RepoPermission.fromString(s) != null)) {\n    return (\n      <DescriptionCard\n        role=\"listitem\"\n        image={<NewspaperIcon className=\"size-6\" />}\n        title={t`Repository`}\n        description={t`Publish changes`}\n      >\n        <p>\n          <RepoDescription />\n        </p>\n        <p className=\"mt-1\">\n          <Trans>\n            The application requests the permissions necessary to perform the\n            following actions on your behalf:\n          </Trans>\n        </p>\n        <RepoTable className=\"mt-2\" permissions={permissions} />\n      </DescriptionCard>\n    )\n  }\n\n  return null\n}\n\nfunction RepoDescription() {\n  return (\n    <Trans>\n      Your repository contains all the data publicly available on the ATProto\n      network, such as Bluesky posts, likes, and follows. It also contains data\n      created through other apps you've signed into using this account.\n    </Trans>\n  )\n}\n\ntype RepoTableProps = Override<\n  HTMLAttributes<HTMLTableElement>,\n  {\n    permissions: ScopePermissionsTransition\n    children?: never\n  }\n>\nfunction RepoTable({ permissions, className, ...attrs }: RepoTableProps) {\n  const { t } = useLingui()\n\n  const nsidActions = useMemo(() => {\n    const map = new Map<\n      CollectionParam,\n      {\n        create: boolean\n        update: boolean\n        delete: boolean\n      }\n    >()\n\n    for (const s of permissions.scopes) {\n      const parsed = RepoPermission.fromString(s)\n      if (!parsed) continue\n\n      for (const coll of parsed.collection) {\n        if (map.has(coll)) {\n          const actions = map.get(coll)!\n          for (const action of parsed.action) actions[action] = true\n        } else {\n          map.set(coll, {\n            create: parsed.action.includes('create'),\n            update: parsed.action.includes('update'),\n            delete: parsed.action.includes('delete'),\n          })\n        }\n      }\n    }\n\n    return map\n  }, [permissions])\n\n  const blobScopes = useMemo(() => {\n    if (permissions.hasTransitionGeneric) {\n      return [new BlobPermission(['*/*'])]\n    }\n    return Array.from(\n      permissions.scopes.map((v) => BlobPermission.fromString(v)),\n    ).filter((v) => v != null)\n  }, [permissions])\n\n  if (!nsidActions.size) return null\n\n  const starActions = nsidActions.get('*')\n\n  const nsidActionsEntries = useMemo(() => {\n    return Array.from(nsidActions.entries()).sort(([a], [b]) =>\n      a.localeCompare(b),\n    )\n  }, [nsidActions])\n\n  return (\n    <table className={`w-full table-auto text-left ${className}`} {...attrs}>\n      <thead>\n        <tr className=\"text-sm\">\n          <th className=\"font-normal\">{t`Collection`}</th>\n          <th className=\"text-center font-normal\">{t`Create`}</th>\n          <th className=\"text-center font-normal\">{t`Update`}</th>\n          <th className=\"text-center font-normal\">{t`Delete`}</th>\n        </tr>\n      </thead>\n      <tbody>\n        {nsidActionsEntries.map(([coll, actions], i) => (\n          <tr key={coll} className=\"text-xs\">\n            <td className={i > 0 ? 'pt-1' : undefined}>\n              <Collection coll={coll} />\n            </td>\n            <td className=\"text-center\">\n              {starActions?.create || actions.create ? (\n                <CheckMarkIcon className=\"inline-block size-3\" />\n              ) : null}\n            </td>\n            <td className=\"text-center\">\n              {starActions?.update || actions.update ? (\n                <CheckMarkIcon className=\"inline-block size-3\" />\n              ) : null}\n            </td>\n            <td className=\"text-center\">\n              {starActions?.delete || actions.delete ? (\n                <CheckMarkIcon className=\"inline-block size-3\" />\n              ) : null}\n            </td>\n          </tr>\n        ))}\n        {blobScopes.length > 0 && (\n          <tr>\n            <td className=\"pt-2\">\n              <Trans>Blob storage</Trans>\n            </td>\n            <td colSpan={3} className=\"pt-2 text-center\">\n              <Trans>Upload files</Trans>\n            </td>\n          </tr>\n        )}\n      </tbody>\n    </table>\n  )\n}\n\n// UTILS\n\nfunction isOfficialBlueskyAppviewServiceId(aud: string): boolean {\n  return aud === 'did:web:bsky.app#bsky_appview'\n}\n\nfunction isBskyAppNsid(nsid: string): nsid is `app.bsky.${string}` {\n  return nsid.startsWith('app.bsky.')\n}\nfunction isBskyChatNsid(nsid: string): nsid is `chat.bsky.${string}` {\n  return nsid.startsWith('chat.bsky.')\n}\n\nfunction scopeEnablesChat(scope: string): boolean {\n  if (scope === 'transition:chat.bsky') return true\n  const rpc = RpcPermission.fromString(scope)\n  if (!rpc) return false\n  // Official Bluesky chat is not hosted by the appview service\n  if (isOfficialBlueskyAppviewServiceId(rpc.aud)) return false\n  return rpc.lxm.includes('*') || rpc.lxm.some(isBskyChatNsid)\n}\n\nfunction isBlueskySpecificNsid(nsid: CollectionParam | LxmParam): boolean {\n  return nsid === '*'\n    ? false\n    : nsid === 'com.atproto.moderation.createReport' ||\n        isBskyAppNsid(nsid) ||\n        isBskyChatNsid(nsid)\n}\n\nfunction scopeEnablesBskyAppRepo(scope: string): boolean {\n  if (scope === 'transition:generic') return true\n  const repo = RepoPermission.fromString(scope)\n  if (!repo) return false\n  return (\n    repo.collection.includes('*') || repo.collection.some(isBlueskySpecificNsid)\n  )\n}\n\nfunction scopeEnablesPrivateBskyAppMethods(scope: string): boolean {\n  if (scope === 'transition:generic') return true\n  const rpc = RpcPermission.fromString(scope)\n  if (!rpc) return false\n  return (\n    rpc.lxm.includes('app.bsky.actor.getPreferences') ||\n    rpc.lxm.includes('app.bsky.graph.block') ||\n    rpc.lxm.includes('app.bsky.graph.muteActor') ||\n    rpc.lxm.includes('app.bsky.graph.muteActorList') ||\n    rpc.lxm.includes('app.bsky.graph.muteThread') ||\n    rpc.lxm.includes('app.bsky.graph.unmuteActor') ||\n    rpc.lxm.includes('app.bsky.graph.unmuteActorList') ||\n    rpc.lxm.includes('app.bsky.graph.unmuteThread') ||\n    rpc.lxm.includes('app.bsky.graph.getMutes') ||\n    rpc.lxm.includes('*')\n  )\n}\n\ntype LxmProps = Override<\n  Omit<HTMLAttributes<HTMLDivElement>, 'children'>,\n  { lxm: LxmParam }\n>\nfunction Lxm({ lxm, ...attrs }: LxmProps) {\n  return lxm === '*' ? (\n    <ItemDescription {...attrs}>\n      <Trans>Any method</Trans>\n    </ItemDescription>\n  ) : (\n    <Nsid {...attrs} nsid={lxm} />\n  )\n}\n\ntype AudProps = Override<\n  Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'>,\n  { aud: AudParam }\n>\nfunction Aud({ aud, ...attrs }: AudProps) {\n  if (aud.startsWith('did:web:api.bsky.app#')) {\n    return (\n      <ItemDescription {...attrs} title={aud}>\n        <Trans>Bluesky App services</Trans>\n      </ItemDescription>\n    )\n  }\n  if (aud.startsWith('did:web:api.bsky.chat#')) {\n    return (\n      <ItemDescription {...attrs} title={aud}>\n        <Trans>Bluesky Chat services</Trans>\n      </ItemDescription>\n    )\n  }\n  if (aud.startsWith('did:web:') && aud.includes('#')) {\n    const domain = aud.slice(8, aud.indexOf('#'))\n    return (\n      <ItemDescription {...attrs} title={aud}>\n        <Trans>\n          A service controlled by <b>{domain}</b>\n        </Trans>\n      </ItemDescription>\n    )\n  }\n  if (aud === '*') {\n    return (\n      <ItemDescription {...attrs}>\n        <Trans>Any service</Trans>\n      </ItemDescription>\n    )\n  }\n\n  return (\n    <Identifier {...attrs} title={aud}>\n      {aud}\n    </Identifier>\n  )\n}\n\ntype CollectionProps = Override<\n  HTMLAttributes<HTMLDivElement>,\n  { coll: CollectionParam; children?: never }\n>\nfunction Collection({ coll, ...attrs }: CollectionProps) {\n  return coll === '*' ? (\n    <ItemDescription {...attrs}>\n      <Trans>Any collection</Trans>\n    </ItemDescription>\n  ) : (\n    <Nsid {...attrs} nsid={coll} />\n  )\n}\n\ntype ItemDescriptionProps = HTMLAttributes<HTMLDivElement>\nfunction ItemDescription({\n  children,\n  className = '',\n  ...attrs\n}: ItemDescriptionProps) {\n  return (\n    <em {...attrs} className={`text-slate-500 ${className}`}>\n      {children}\n    </em>\n  )\n}\n\ntype NsidProps = Override<IdentifierProps, { nsid: string; children?: never }>\nfunction Nsid({ nsid, ...attrs }: NsidProps) {\n  return (\n    <Identifier {...attrs}>\n      {nsid.split('.').map((part, i) =>\n        i === 0 ? (\n          part\n        ) : (\n          // line break **after** the dot\n          <Fragment key={i}>\n            {'.'}\n            <wbr />\n            {part}\n          </Fragment>\n        ),\n      )}\n    </Identifier>\n  )\n}\n\ntype IdentifierProps = HTMLAttributes<HTMLDivElement>\nfunction Identifier({\n  children,\n  className = '',\n  ...attrs\n}: IdentifierProps): ReactNode {\n  return (\n    <code {...attrs} className={`text-slate-500 ${className}`}>\n      {children}\n    </code>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/components/utils/url-viewer.tsx",
    "content": "import { JSX, useMemo } from 'react'\nimport { Override } from '../../lib/util.ts'\n\nexport type UrlPartRenderingOptions = {\n  faded?: boolean\n  bold?: boolean\n}\n\nexport type UrlRendererProps = {\n  url: string | URL\n  proto?: boolean | UrlPartRenderingOptions\n  host?: boolean | UrlPartRenderingOptions\n  path?: boolean | UrlPartRenderingOptions\n  query?: boolean | UrlPartRenderingOptions\n  hash?: boolean | UrlPartRenderingOptions\n  as?: string\n}\n\nexport function UrlViewer<As extends keyof JSX.IntrinsicElements = 'span'>({\n  url,\n  proto = false,\n  host = true,\n  path = false,\n  query = false,\n  hash = false,\n  as: As = 'span',\n\n  // Element\n  ...props\n}: Override<JSX.IntrinsicElements[As], UrlRendererProps>) {\n  const urlObj = useMemo(() => (url instanceof URL ? url : new URL(url)), [url])\n\n  return (\n    <As {...props}>\n      {proto && (\n        <UrlPartViewer\n          value={`${urlObj.protocol}//`}\n          {...(proto === true ? null : proto)}\n        />\n      )}\n      {host && (\n        <UrlPartViewer\n          value={urlObj.host}\n          {...(host === true ? { faded: false, bold: true } : host)}\n        />\n      )}\n      {path && (\n        <UrlPartViewer\n          value={urlObj.pathname}\n          {...(path === true ? null : path)}\n        />\n      )}\n      {query && (\n        <UrlPartViewer\n          value={urlObj.search}\n          {...(query === true ? null : query)}\n        />\n      )}\n      {hash && (\n        <UrlPartViewer value={urlObj.hash} {...(hash === true ? null : hash)} />\n      )}\n    </As>\n  )\n}\n\nfunction UrlPartViewer({\n  value,\n  faded = true,\n  bold = false,\n}: { value: string } & UrlPartRenderingOptions) {\n  const Comp = bold ? 'b' : 'span'\n  return <Comp className={faded ? 'opacity-50' : ''}>{value}</Comp>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/cookie-error-page.tsx",
    "content": "import './style.css'\n\nimport type { HydrationData } from '#/hydration-data.d.ts'\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { LocaleProvider } from './locales/locale-provider.tsx'\nimport { CookieErrorView } from './views/error/cookie-error-view.tsx'\n\nconst {\n  //\n  __continueUrl: continueUrl,\n  __customizationData: customizationData,\n} = window as typeof window & HydrationData['cookie-error-page']\n\nconst container = document.getElementById('root')!\n\ncreateRoot(container).render(\n  <StrictMode>\n    <LocaleProvider>\n      <CookieErrorView\n        continueUrl={continueUrl}\n        customizationData={customizationData}\n      />\n    </LocaleProvider>\n  </StrictMode>,\n)\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/error-page.tsx",
    "content": "import './style.css'\n\nimport type { HydrationData } from '#/hydration-data.d.ts'\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport { LocaleProvider } from './locales/locale-provider.tsx'\nimport { ErrorView } from './views/error/error-view.tsx'\n\nconst {\n  //\n  __errorData: errorData,\n  __customizationData: customizationData,\n} = window as typeof window & HydrationData['error-page']\n\nconst container = document.getElementById('root')!\n\ncreateRoot(container).render(\n  <StrictMode>\n    <LocaleProvider>\n      <ErrorView error={errorData} customizationData={customizationData} />\n    </LocaleProvider>\n  </StrictMode>,\n)\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-api.ts",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { useCallback, useState } from 'react'\nimport { useErrorBoundary } from 'react-error-boundary'\nimport type {\n  Account,\n  ConfirmResetPasswordInput,\n  InitiatePasswordResetInput,\n  Session,\n  SignInInput,\n  SignUpInput,\n  VerifyHandleAvailabilityInput,\n} from '@atproto/oauth-provider-api'\nimport { Api, UnknownRequestUriError } from '../lib/api.ts'\nimport { upsert } from '../lib/util.ts'\n\n/**\n * Any function wrapped with this helper will automatically show the error\n * boundary when an `UnknownRequestUriError` is thrown. This typically happens\n * in development, or if the user left its browser session open for a (very)\n * long time.\n *\n * @note Requires an error boundary to be present in the component tree.\n */\nfunction useSafeCallback<F extends (...a: any) => any>(fn: F, deps: unknown[]) {\n  const { showBoundary } = useErrorBoundary<UnknownRequestUriError>()\n\n  return useCallback(\n    async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {\n      try {\n        return await fn(...args)\n      } catch (error) {\n        if (error instanceof UnknownRequestUriError) showBoundary(error)\n        throw error\n      }\n    },\n    deps.concat(showBoundary),\n  )\n}\n\nexport type SessionWithToken = Session & {\n  ephemeralToken?: string\n}\n\nexport function useApi({\n  sessions: sessionsInit = [],\n  onRedirected,\n}: {\n  sessions?: readonly Session[]\n  onRedirected?: () => void\n}) {\n  const [api] = useState(() => new Api())\n  const [sessions, setSessions] =\n    useState<readonly SessionWithToken[]>(sessionsInit)\n\n  const { i18n } = useLingui()\n  const { locale } = i18n\n\n  const selectSub = useCallback(\n    (sub: string | null) => {\n      setSessions((sessions) =>\n        sub === (sessions.find((s) => s.selected)?.account.sub || null)\n          ? sessions\n          : sessions.map((s) => ({ ...s, selected: s.account.sub === sub })),\n      )\n    },\n    [setSessions],\n  )\n\n  const upsertSession = useCallback(\n    ({\n      account,\n      ephemeralToken,\n      // The server will tell us if the user needs to consent to the\n      // authorization. Defaults to true in case of sign-ups\n      consentRequired = true,\n      // When a new session is inserted, assume that the user intends to use\n      // it, and therefore, it is selected by default.\n      selected = true,\n      // When a new session is inserted, it is assumed that the user just\n      // created the session, and therefore, login is not required.\n      loginRequired = false,\n    }: { account: Account } & Partial<SessionWithToken>) => {\n      const session: SessionWithToken = {\n        account,\n        ephemeralToken,\n        selected,\n        loginRequired,\n        consentRequired,\n      }\n\n      setSessions((sessions) =>\n        upsert(sessions, session, (s) => s.account.sub === account.sub).map(\n          // Make sure to de-select any other selected session (if selected is\n          // true)\n          (s) =>\n            !selected || s === session || !s.selected\n              ? s\n              : { ...s, selected: false },\n        ),\n      )\n    },\n    [setSessions],\n  )\n\n  const performRedirect = useCallback(\n    (url: string | URL) => {\n      // @TODO At this point, the request cannot be accepted/rejected anymore.\n      // We should probably change the app's state to something that indicates\n      // that in order to improve UX in case the user comes back to the app.\n      // This is currently ensured by the backend (through back-forward cache\n      // busting) but handling it here would provide a better UX.\n\n      window.location.href = String(url)\n      if (onRedirected) setTimeout(onRedirected)\n    },\n    [onRedirected],\n  )\n\n  const doSignIn = useSafeCallback(\n    async (data: Omit<SignInInput, 'locale'>, signal?: AbortSignal) => {\n      const response = await api.fetch(\n        'POST',\n        '/sign-in',\n        { ...data, locale },\n        { signal },\n      )\n      upsertSession(response)\n    },\n    [api, locale, upsertSession],\n  )\n\n  const doInitiatePasswordReset = useSafeCallback(\n    async (\n      data: Omit<InitiatePasswordResetInput, 'locale'>,\n      signal?: AbortSignal,\n    ) => {\n      await api.fetch(\n        'POST',\n        '/reset-password-request',\n        { ...data, locale },\n        { signal },\n      )\n    },\n    [api, locale],\n  )\n\n  const doConfirmResetPassword = useSafeCallback(\n    async (data: ConfirmResetPasswordInput, signal?: AbortSignal) => {\n      await api.fetch('POST', '/reset-password-confirm', data, { signal })\n    },\n    [api],\n  )\n\n  const doValidateNewHandle = useSafeCallback(\n    async (data: VerifyHandleAvailabilityInput, signal?: AbortSignal) => {\n      await api.fetch('POST', '/verify-handle-availability', data, { signal })\n    },\n    [api],\n  )\n\n  const doSignUp = useSafeCallback(\n    async (data: Omit<SignUpInput, 'locale'>, signal?: AbortSignal) => {\n      const response = await api.fetch(\n        'POST',\n        '/sign-up',\n        { ...data, locale },\n        { signal },\n      )\n      upsertSession(response)\n    },\n    [api, locale, upsertSession],\n  )\n\n  const doConsent = useSafeCallback(\n    async (sub: string, scope?: string) => {\n      // If \"remember me\" was unchecked, we need to use the ephemeral token to\n      // authenticate the request.\n      const bearer = sessions.find((s) => s.account.sub === sub)?.ephemeralToken\n      const { url } = await api.fetch(\n        'POST',\n        '/consent',\n        { sub, scope },\n        { bearer },\n      )\n      performRedirect(url)\n    },\n    [api, sessions, performRedirect],\n  )\n\n  const doReject = useSafeCallback(async () => {\n    const { url } = await api.fetch('POST', '/reject', {})\n    performRedirect(url)\n  }, [api, performRedirect])\n\n  return {\n    sessions,\n    selectSub,\n\n    doSignIn,\n    doInitiatePasswordReset,\n    doConfirmResetPassword,\n    doValidateNewHandle,\n    doSignUp,\n    doConsent,\n    doReject,\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-async-action.ts",
    "content": "import {\n  ForwardedRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from 'react'\n\nexport type AsyncActionController = {\n  reset: () => void\n}\n\nexport type UseAsyncActionOptions = {\n  ref?: ForwardedRef<AsyncActionController>\n  onLoading?: (loading: boolean) => void\n  onError?: (error: Error | undefined) => void\n}\n\nexport function useAsyncAction(\n  fn: (signal: AbortSignal) => void | PromiseLike<void>,\n  { ref, onLoading, onError }: UseAsyncActionOptions = {},\n) {\n  const [loading, setLoading] = useState(false)\n  const [error, setError] = useState<Error | undefined>()\n\n  const doSetError = useCallback(\n    (error: Error | undefined) => {\n      setError(error)\n      onError?.(error)\n    },\n    [onError],\n  )\n\n  const doSetLoading = useCallback(\n    (loading: boolean) => {\n      setLoading(loading)\n      onLoading?.(loading)\n    },\n    [onLoading],\n  )\n\n  const controllerRef = useRef<AbortController>(null)\n\n  const resetRef = useRef<() => void>(null)\n  useEffect(() => {\n    resetRef.current = () => {\n      controllerRef.current?.abort()\n      controllerRef.current = null\n      doSetError(undefined)\n      doSetLoading(false)\n    }\n    return () => {\n      resetRef.current = null\n    }\n  }, [doSetError, doSetLoading])\n\n  useImperativeHandle(\n    ref,\n    (): AsyncActionController => ({\n      reset: () => resetRef.current?.(),\n    }),\n    [],\n  )\n\n  // Cancel pending action when unmounted\n  useEffect(() => {\n    return () => {\n      controllerRef.current?.abort()\n      controllerRef.current = null\n    }\n  }, [])\n\n  const run = useCallback(async (): Promise<void> => {\n    // Cancel previous run\n    controllerRef.current?.abort()\n\n    doSetLoading(true)\n    doSetError(undefined)\n\n    const controller = new AbortController()\n    const { signal } = controller\n\n    controllerRef.current = controller\n\n    try {\n      await fn(signal)\n    } catch (err) {\n      if (controller === controllerRef.current) {\n        doSetError(err instanceof Error ? err : new Error(String(err)))\n      } else {\n        if (!isAbortReason(signal, err)) {\n          console.warn('Async action error after abort', err)\n        }\n      }\n    } finally {\n      if (controller === controllerRef.current) {\n        controllerRef.current = null\n        doSetLoading(false)\n      }\n\n      controller.abort()\n    }\n  }, [fn, doSetLoading, doSetError])\n\n  return {\n    loading,\n    error,\n    run,\n  }\n}\n\nfunction isAbortReason(signal: AbortSignal, err: unknown): boolean {\n  return (\n    signal.aborted &&\n    (signal.reason === err ||\n      signal.reason === err?.['cause'] ||\n      (err instanceof DOMException && err.name === 'AbortError'))\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-bound-dispatch.ts",
    "content": "import { Dispatch, useCallback } from 'react'\n\nexport function useBoundDispatch<A>(dispatch: Dispatch<A>, value: A) {\n  return useCallback(() => dispatch(value), [dispatch, value])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-browser-color-scheme.ts",
    "content": "import { useEffect, useState } from 'react'\n\nconst query =\n  typeof window === 'undefined'\n    ? null\n    : window.matchMedia('(prefers-color-scheme: dark)')\n\nexport function useBrowserColorScheme() {\n  const [theme, setTheme] = useState<'light' | 'dark'>(\n    query?.matches ? 'dark' : 'light',\n  )\n\n  useEffect(() => {\n    if (!query) return\n\n    const listener = () => {\n      setTheme(query.matches ? 'dark' : 'light')\n    }\n\n    query.addEventListener('change', listener)\n\n    return () => {\n      query.removeEventListener('change', listener)\n    }\n\n    // @NOTE \"query\" is a global constant and does not need to be part of the\n    // array bellow:\n  }, [])\n\n  return theme\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-click-outside.ts",
    "content": "import { RefObject, useEffect } from 'react'\n\nexport function useClickOutside(\n  ref: RefObject<HTMLElement | null>,\n  handler: (event: MouseEvent) => void,\n) {\n  useEffect(() => {\n    const listener = (event: MouseEvent) => {\n      if (\n        !event.defaultPrevented &&\n        ref.current &&\n        !ref.current.contains(event.target as Node)\n      ) {\n        handler(event)\n      }\n    }\n\n    document.addEventListener('mousedown', listener)\n    return () => {\n      document.removeEventListener('mousedown', listener)\n    }\n  }, [ref, handler])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-escape-key.ts",
    "content": "import { useEffect } from 'react'\n\nexport function useEscapeKey(callback: () => void) {\n  const handleKeyDown = (event: KeyboardEvent) => {\n    if (event.key === 'Escape') {\n      event.preventDefault()\n      callback()\n    }\n  }\n\n  useEffect(() => {\n    window.addEventListener('keydown', handleKeyDown)\n    return () => {\n      window.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [callback])\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-random-string.ts",
    "content": "import { useEffect, useState } from 'react'\n\nexport const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\nexport const LOWER = UPPER.toLowerCase() as Lowercase<typeof UPPER>\nexport const DIGITS = '0123456789'\n\nexport const ALPHANUMERIC = `${UPPER}${LOWER}${DIGITS}` as const\n\nexport type UseRandomStringOptions = BuildRandomStringOptions & {\n  prefix?: string\n  suffix?: string\n}\n\nexport function useRandomString(options?: UseRandomStringOptions) {\n  const [state, setState] = useState(() => buildRandomString(options))\n  useEffect(() => {\n    setState(buildRandomString(options))\n  }, [options?.length, options?.alphabet])\n\n  return `${options?.prefix ?? ''}${state}${options?.suffix ?? ''}`\n}\n\ntype BuildRandomStringOptions = {\n  length?: number\n  alphabet?: string\n}\n\nfunction buildRandomString({\n  length = 16,\n  alphabet = ALPHANUMERIC,\n}: BuildRandomStringOptions = {}) {\n  return Array.from({ length }, () => getRandomCharFrom(alphabet)).join('')\n}\n\nfunction getRandomCharFrom(alphabet: string) {\n  return alphabet.charAt((Math.random() * alphabet.length) | 0)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/hooks/use-stepper.ts",
    "content": "import { useCallback, useEffect, useState } from 'react'\n\nexport type DisabledStep = false | null | undefined\nexport type Step = {\n  invalid: boolean\n}\n\nconst isEnabled = <S extends Step | DisabledStep>(\n  s: S,\n): s is S extends DisabledStep ? never : S => s != null && s !== false\nconst isRequired = <S extends Step | DisabledStep>(\n  s: S,\n): s is S extends DisabledStep ? never : S & { invalid: true } =>\n  isEnabled(s) && s.invalid === true\nconst isCompleted = <S extends Step | DisabledStep>(\n  s: S,\n): s is S extends DisabledStep ? S : S & { invalid: false } =>\n  !isEnabled(s) || s.invalid === false\n\nexport function useStepper<const S extends Step>(\n  steps: readonly (S | DisabledStep)[],\n) {\n  const firstIdx = steps.findIndex(isEnabled)\n  const lastIdx = steps.findLastIndex(isEnabled)\n  const requiredIdx = steps.findIndex(isRequired)\n\n  const [currentIdx, setCurrentIdx] = useState<number>(firstIdx)\n\n  const to = useCallback(\n    (idx: number) => {\n      if (idx !== -1 && steps[idx]) {\n        setCurrentIdx(idx)\n        return true\n      } else {\n        return false\n      }\n    },\n    [steps.map(isEnabled).join()],\n  )\n\n  const prevIdx = steps.findLastIndex((s, i) => isEnabled(s) && i < currentIdx)\n  const nextIdx = steps.findIndex((s, i) => isEnabled(s) && i > currentIdx)\n\n  const toFirst = useCallback(() => to(firstIdx), [to, firstIdx])\n  const toLast = useCallback(() => to(lastIdx), [to, lastIdx])\n  const toPrev = useCallback(() => to(prevIdx), [to, prevIdx])\n  const toNext = useCallback(() => to(nextIdx), [to, nextIdx])\n  const toRequired = useCallback(() => to(requiredIdx), [to, requiredIdx])\n\n  // Step number in user friendly terms (accounting for disabled steps)\n  const currentPosition =\n    currentIdx +\n    // use \"1 indexed position\" (for user friendliness):\n    1 +\n    // Adjust the position by counting the number of disabled steps before the\n    // current step (if any):\n    steps.reduce(\n      (acc, s, i) => (i >= currentIdx || isEnabled(s) ? acc : acc - 1),\n      0,\n    )\n\n  const count = steps.filter(isEnabled).length\n  const completed = steps.every(isCompleted)\n\n  const current =\n    currentIdx === -1 || !steps[currentIdx] ? undefined : steps[currentIdx]\n\n  // Fool-proof (reset current step in case the current step becomes disabled)\n  const broken = currentIdx === -1\n  useEffect(() => {\n    if (broken) toFirst()\n  }, [broken])\n\n  return {\n    current,\n    currentPosition,\n    count,\n    completed,\n    atFirst: currentPosition === 1,\n    atLast: currentPosition === count,\n    toFirst,\n    toLast,\n    toPrev,\n    toNext,\n    toRequired,\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/api.ts",
    "content": "import {\n  API_ENDPOINT_PREFIX,\n  ApiEndpoints,\n  CSRF_COOKIE_NAME,\n  CSRF_HEADER_NAME,\n} from '@atproto/oauth-provider-api'\nimport { readCookie } from './cookies.ts'\nimport {\n  JsonClient,\n  JsonErrorPayload,\n  JsonErrorResponse,\n} from './json-client.ts'\n\nexport type { Options } from './json-client.ts'\n\nexport class Api extends JsonClient<ApiEndpoints> {\n  constructor() {\n    const baseUrl = new URL(API_ENDPOINT_PREFIX, window.origin).toString()\n    super(baseUrl, () => ({\n      [CSRF_HEADER_NAME]: readCookie(CSRF_COOKIE_NAME),\n    }))\n  }\n\n  // Override the parent's parseError method to handle expected error responses\n  // and transform them into instances of the corresponding error classes.\n  public static override parseError(\n    json: unknown,\n  ): undefined | JsonErrorResponse {\n    // @NOTE Most specific errors first !\n    if (SecondAuthenticationFactorRequiredError.is(json)) {\n      return new SecondAuthenticationFactorRequiredError(json)\n    }\n    if (InvalidCredentialsError.is(json)) {\n      return new InvalidCredentialsError(json)\n    }\n    if (InvalidInviteCodeError.is(json)) {\n      return new InvalidInviteCodeError(json)\n    }\n    if (HandleUnavailableError.is(json)) {\n      return new HandleUnavailableError(json)\n    }\n    if (EmailTakenError.is(json)) {\n      return new EmailTakenError(json)\n    }\n    if (RequestExpiredError.is(json)) {\n      return new RequestExpiredError(json)\n    }\n    if (UnknownRequestUriError.is(json)) {\n      return new UnknownRequestUriError(json)\n    }\n    if (InvalidRequestError.is(json)) {\n      return new InvalidRequestError(json)\n    }\n    if (AccessDeniedError.is(json)) {\n      return new AccessDeniedError(json)\n    }\n    return super.parseError(json)\n  }\n}\n\nexport type AccessDeniedPayload = JsonErrorPayload<'access_denied'>\nexport class AccessDeniedError<\n  P extends AccessDeniedPayload = AccessDeniedPayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description || 'Access denied',\n  ) {\n    super(payload, message)\n  }\n\n  static is(json: unknown): json is AccessDeniedPayload {\n    return super.is(json) && json.error === 'access_denied'\n  }\n}\n\nexport type InvalidRequestPayload = JsonErrorPayload<'invalid_request'>\nexport class InvalidRequestError<\n  P extends InvalidRequestPayload = InvalidRequestPayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description || 'Invalid request',\n  ) {\n    super(payload, message)\n  }\n\n  static is(json: unknown): json is InvalidRequestPayload {\n    return super.is(json) && json.error === 'invalid_request'\n  }\n}\n\nexport type InvalidInviteCodePayload = InvalidRequestPayload & {\n  error_description: `This invite code is invalid.${string}`\n}\nexport class InvalidInviteCodeError<\n  P extends InvalidInviteCodePayload = InvalidInviteCodePayload,\n> extends InvalidRequestError<P> {\n  constructor(payload: P) {\n    super(payload)\n  }\n\n  static is(json: unknown): json is InvalidInviteCodePayload {\n    return (\n      super.is(json) &&\n      json.error_description != null &&\n      json.error_description.startsWith('This invite code is invalid.')\n    )\n  }\n}\n\nexport type RequestExpiredPayload = AccessDeniedPayload & {\n  error_description: 'This request has expired'\n}\nexport class RequestExpiredError<\n  P extends RequestExpiredPayload = RequestExpiredPayload,\n> extends AccessDeniedError<P> {\n  static is(json: unknown): json is RequestExpiredPayload {\n    return (\n      super.is(json) && json.error_description === 'This request has expired'\n    )\n  }\n}\n\nexport type InvalidCredentialsPayload = InvalidRequestPayload & {\n  error_description: 'Invalid identifier or password'\n}\nexport class InvalidCredentialsError<\n  P extends InvalidCredentialsPayload = InvalidCredentialsPayload,\n> extends InvalidRequestError<P> {\n  static is(json: unknown): json is InvalidCredentialsPayload {\n    return (\n      super.is(json) &&\n      json.error_description === 'Invalid identifier or password'\n    )\n  }\n}\n\nexport type UnknownRequestPayload = InvalidRequestPayload & {\n  error_description: 'Unknown request_uri'\n}\nexport class UnknownRequestUriError<\n  P extends UnknownRequestPayload = UnknownRequestPayload,\n> extends InvalidRequestError<P> {\n  static is(json: unknown): json is UnknownRequestPayload {\n    return super.is(json) && json.error_description === 'Unknown request_uri'\n  }\n}\nexport type EmailTakenPayload = InvalidRequestPayload & {\n  error_description: 'Email already taken'\n}\nexport class EmailTakenError<\n  P extends EmailTakenPayload = EmailTakenPayload,\n> extends InvalidRequestError<P> {\n  static is(json: unknown): json is EmailTakenPayload {\n    return super.is(json) && json.error_description === 'Email already taken'\n  }\n}\n\nexport type HandleUnavailablePayload =\n  JsonErrorPayload<'handle_unavailable'> & {\n    reason: 'syntax' | 'domain' | 'slur' | 'taken'\n  }\nexport class HandleUnavailableError<\n  P extends HandleUnavailablePayload = HandleUnavailablePayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description || 'That handle cannot be used',\n  ) {\n    super(payload, message)\n  }\n\n  get reason() {\n    return this.payload.reason\n  }\n\n  static is(json: unknown): json is HandleUnavailablePayload {\n    return (\n      super.is(json) &&\n      json.error === 'handle_unavailable' &&\n      'reason' in json &&\n      (json.reason === 'syntax' ||\n        json.reason === 'domain' ||\n        json.reason === 'slur' ||\n        json.reason === 'taken')\n    )\n  }\n}\n\nexport type SecondAuthenticationFactorRequiredPayload =\n  JsonErrorPayload<'second_authentication_factor_required'> & {\n    type: 'emailOtp'\n    hint: string\n  }\nexport class SecondAuthenticationFactorRequiredError<\n  P extends\n    SecondAuthenticationFactorRequiredPayload = SecondAuthenticationFactorRequiredPayload,\n> extends JsonErrorResponse<P> {\n  constructor(\n    payload: P,\n    message = payload.error_description ||\n      `${payload.type} authentication factor required (hint: ${payload.hint})`,\n  ) {\n    super(payload, message)\n  }\n\n  get type() {\n    return this.payload.type\n  }\n  get hint() {\n    return this.payload.hint\n  }\n\n  static is(json: unknown): json is SecondAuthenticationFactorRequiredPayload {\n    return (\n      super.is(json) &&\n      json.error === 'second_authentication_factor_required' &&\n      'type' in json &&\n      json.type === 'emailOtp' &&\n      'hint' in json &&\n      typeof json.hint === 'string'\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/cookies.ts",
    "content": "export const parseCookieString = (\n  cookie: string = document.cookie,\n): Record<string, string | undefined> =>\n  Object.fromEntries(\n    cookie\n      .split(';')\n      .filter(Boolean)\n      .map((str) => str.split('=', 2).map((s) => decodeURIComponent(s.trim()))),\n  )\n\nexport function readCookie(\n  name: string,\n  cookie: string = document.cookie,\n): string | undefined {\n  const cookies = parseCookieString(cookie)\n  return cookies[name]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/json-client.ts",
    "content": "// Using a type import to avoid bundling this lib\nimport type { Json } from '@atproto-labs/fetch'\n\nexport { type Json }\ntype Awaitable<T> = T | PromiseLike<T>\n\nexport type Options = {\n  signal?: AbortSignal\n  bearer?: string\n}\n\nexport type EndpointPath = `/${string}`\nexport type EndpointDefinition =\n  | {\n      method: 'POST'\n      input: Json\n      output: Json | void\n    }\n  | {\n      method: 'GET'\n      params?: Record<string, string | undefined>\n      output: Json | void\n    }\n\nexport class JsonClient<\n  Endpoints extends { [Path: EndpointPath]: EndpointDefinition },\n> {\n  constructor(\n    protected readonly baseUrl: string,\n    protected readonly getHeaders: () => Awaitable<\n      Record<string, string | undefined>\n    >,\n  ) {}\n\n  public async fetch<Path extends EndpointPath & keyof Endpoints>(\n    method: Endpoints[Path]['method'],\n    path: Path,\n    input: Endpoints[Path] extends { method: 'GET' }\n      ? Endpoints[Path]['params']\n      : Endpoints[Path] extends { method: 'POST' }\n        ? Endpoints[Path]['input']\n        : undefined,\n    options?: Options,\n  ): Promise<Endpoints[Path]['output']> {\n    try {\n      const url = new URL(`${this.baseUrl}${path}`)\n      if (method === 'GET') {\n        if (input) {\n          for (const [key, value] of Object.entries(input)) {\n            url.searchParams.set(key, value)\n          }\n        }\n      }\n\n      const body = method === 'POST' ? JSON.stringify(input) : undefined\n\n      const headers = Object.entries(await this.getHeaders.call(null))\n        .filter((entry): entry is [string, string] => entry[1] != null)\n        .map(([k, v]) => [k.toLowerCase(), v] as [string, string])\n\n      if (options?.bearer) {\n        headers.push(['authorization', `Bearer ${options.bearer}`])\n      }\n\n      const response = await fetch(url, {\n        method,\n        headers:\n          body && !headers.some(([k]) => k === 'content-type')\n            ? headers.concat([['content-type', 'application/json']])\n            : headers,\n        mode: 'same-origin',\n        body,\n        signal: options?.signal,\n      })\n\n      if (response.status === 204) {\n        return undefined\n      }\n\n      const responseType = response.headers.get('content-type')\n      if (responseType !== 'application/json') {\n        await response.body?.cancel()\n        throw new Error(`Invalid content type \"${responseType}\"`, {\n          cause: response,\n        })\n      }\n\n      const json = await response.json()\n\n      if (response.ok) return json as Endpoints[Path]['output']\n      else throw this.parseError(response, json)\n    } catch (err) {\n      console.warn('API request failed', err, { method, path, input, options })\n      throw err\n    }\n  }\n\n  protected parseError(response: Response, json: Json): Error {\n    const Class = this.constructor as typeof JsonClient\n    const error = Class.parseError(json)\n    if (error) return error\n\n    return new Error('Invalid JSON response', { cause: response })\n  }\n\n  public static parseError(json: unknown): undefined | JsonErrorResponse {\n    if (JsonErrorResponse.is(json)) {\n      return new JsonErrorResponse(json)\n    }\n  }\n}\n\nexport type JsonErrorPayload<E extends string = string> = {\n  error: E\n  error_description?: string\n}\n\nexport class JsonErrorResponse<\n  P extends JsonErrorPayload = JsonErrorPayload,\n> extends Error {\n  constructor(\n    public readonly payload: P,\n    message = payload.error_description,\n    options?: ErrorOptions,\n  ) {\n    super(message || `Error \"${payload.error}\"`, options)\n  }\n\n  get error(): string {\n    return this.payload.error\n  }\n\n  get description(): string | undefined {\n    return this.payload.error_description\n  }\n\n  static is(json: unknown): json is JsonErrorPayload {\n    return (\n      json != null &&\n      typeof json === 'object' &&\n      typeof json['error'] === 'string' &&\n      (json['error_description'] === undefined ||\n        typeof json['error_description'] === 'string')\n    )\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/lang.ts",
    "content": "type LangStringValue =\n  | undefined\n  | null\n  | string\n  | Record<string, string | undefined>\n\n/**\n * Only returns a string if it matches the desired {@link locale}, or return the\n * provided {@link fallback}.\n */\nexport function getLangString(\n  value: LangStringValue,\n  locale: string,\n  fallback: string,\n): string\nexport function getLangString(\n  value: LangStringValue,\n  locale: string,\n  fallback?: string,\n): string | undefined\nexport function getLangString(\n  value: LangStringValue,\n  locale: string,\n  fallback?: string,\n): string | undefined {\n  switch (typeof value) {\n    case 'string':\n      // By convention, string values are in english\n      if (locale === 'en' || locale.startsWith('en-')) return value\n      break\n\n    case 'object': {\n      // Fool-proof\n      if (value === null) break\n\n      // Exact match\n      const localeMatch = value[locale]\n      if (typeof localeMatch === 'string') return localeMatch\n\n      // Fallback to language match  (e.g. \"fr-BE\" -> \"fr\")\n      const lang = locale.split('-')[0]\n      const langMatch = value[lang]\n      if (typeof langMatch === 'string') return langMatch\n\n      // Fallback to any locale from same language (e.g. \"pt-PT\" -> \"pt-BR\")\n      for (const k in value) {\n        if (k.startsWith(`${lang}-`)) {\n          const countryMatch = value[k]\n          if (typeof countryMatch === 'string') return countryMatch\n        }\n      }\n    }\n  }\n\n  return fallback\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/oauth-client.ts",
    "content": "export type { OAuthClientMetadata } from '@atproto/oauth-types'\n\n// @NOTE: not importing these from @atproto/oauth-types here because 1) we don't\n// need to validate here and 2) we prefer not to import un-necessary code to\n// improve bundle size (~100k impact)\nexport const isOAuthClientIdLoopback = (clientId: string) =>\n  clientId.startsWith('http://')\nexport const isConventionalOAuthClientId = (clientId: string) => {\n  try {\n    const url = new URL(clientId)\n    return (\n      url.protocol === 'https:' &&\n      url.pathname === '/oauth-client-metadata.json' &&\n      !url.port &&\n      !url.search\n    )\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/password.ts",
    "content": "export const MIN_PASSWORD_LENGTH = 8\n\nconst EMOJI =\n  /(\\ud83c[\\ud000-\\udfff]|\\ud83d[\\ud000-\\udfff]|\\ud83e[\\ud000-\\udfff])/\nconst UPPER = /[A-Z]/\nconst LOWER = /[a-z]/\nconst DEC = /[0-9]/\nconst SPECIAL = /[^a-zA-Z0-9]/\n\nexport enum PasswordStrength {\n  weak = 1,\n  moderate = 2,\n  strong = 3,\n  extra = 4,\n}\n\nexport function getPasswordStrength(pwd: string): PasswordStrength {\n  if (pwd.length < MIN_PASSWORD_LENGTH) {\n    return PasswordStrength.weak\n  }\n\n  // Very long passwords\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 12) {\n    return PasswordStrength.extra\n  }\n\n  // Long passwords\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 8) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.extra\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC], 2)) {\n      return PasswordStrength.extra\n    }\n    return PasswordStrength.strong\n  }\n\n  // Emojis make passwords strong\n  if (pwd.length >= MIN_PASSWORD_LENGTH) {\n    if (matches(pwd, [EMOJI])) {\n      return PasswordStrength.strong\n    }\n  }\n\n  // Pretty long passwords\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 6) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.strong\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC], 2)) {\n      return PasswordStrength.strong\n    }\n    // Only 1 type of alpha-num characters\n    return PasswordStrength.moderate\n  }\n\n  // Longish password\n  if (pwd.length >= MIN_PASSWORD_LENGTH + 4) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.moderate\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC], 2)) {\n      return PasswordStrength.moderate\n    }\n\n    // Only 1 type of alpha-num characters\n    return PasswordStrength.weak\n  }\n\n  // Short password (8-11 characters)\n  if (pwd.length >= MIN_PASSWORD_LENGTH) {\n    if (matches(pwd, [SPECIAL])) {\n      return PasswordStrength.moderate\n    }\n    if (matches(pwd, [UPPER, LOWER, DEC])) {\n      return PasswordStrength.moderate\n    }\n  }\n\n  return PasswordStrength.weak\n}\n\nfunction matches(\n  pwd: string,\n  regexps: RegExp[],\n  regexpsCountToMatch: number = regexps.length,\n): boolean {\n  if (regexpsCountToMatch < 1 || regexpsCountToMatch > regexps.length) {\n    throw new TypeError('Invalid regexpsCountToMatch')\n  }\n  for (const regexp of regexps) {\n    if (regexp.test(pwd)) {\n      regexpsCountToMatch--\n      if (regexpsCountToMatch === 0) return true\n    }\n  }\n  return false\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/ref.ts",
    "content": "import { ForwardedRef } from 'react'\n\nexport function updateRef<T>(ref: ForwardedRef<T>, value: T | null) {\n  if (typeof ref === 'function') {\n    ref(value)\n  } else if (ref) {\n    ref.current = value\n  }\n}\n\nexport function mergeRefs<T>(refs: readonly (ForwardedRef<T> | undefined)[]) {\n  return (value: T | null) => {\n    for (const ref of refs) {\n      if (ref) updateRef(ref, value)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/lib/util.ts",
    "content": "export function upsert<T>(\n  arr: undefined | readonly T[],\n  item: T,\n  predicate: (value: T, index: number, obj: readonly T[]) => boolean,\n): T[] {\n  if (!arr) return [item]\n  const idx = arr.findIndex(predicate)\n  return idx === -1\n    ? [...arr, item]\n    : [...arr.slice(0, idx), item, ...arr.slice(idx + 1)]\n}\n\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\nexport type Override<T, U> = Simplify<Omit<T, keyof U> & U>\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/an/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: an\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ast/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ast\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ca/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ca\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/da/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: da\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/de/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: de\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/el/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: el\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/en/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:03+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: en\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"2FA Confirmation\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"A second authentication factor is required\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"A service controlled by <0>{domain}</0>\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"Account\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"Already have a code?\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"An application on your device\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"An unknown error occurred\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"Another account\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"Any collection\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"Any method\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"Any service\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"Authenticate\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"Authorize\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"Avatar\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"Back\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"Between {minLength} and {maxLength} characters\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"Blob storage\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"Bluesky App services\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"Bluesky Chat services\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"By creating an account you agree to the {0} and the {1} of this service.\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"Call\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"Cancel\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"Change your <0>@handle</0>\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"Chat\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"Check your {0} email for a login code and enter it here.\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"Choose a username\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"Code\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"Collapse details\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"Collection\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"Confirm\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"Confirm your password to continue\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"Confirmation code\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"Continue\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"Cookie Error\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"Create\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"Create a new account\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"Create Account\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"Create, update, and delete any public record\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"Delete\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"Deny access\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"Description\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"Email address\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"Enter a password\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"Enter the code you received to reset your password.\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"Enter your email address\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"Enter your new password\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"Enter your password\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"Enter your username and password\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"Error\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"example-com-xxxxx-xxxxx\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"Expand details\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"Extra\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"Forgot Password\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"Forgot?\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"Grant access to your <0>{0}</0> account\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"Having trouble? <0>Contact support</0>\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"Hide\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"Home\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"Identifier\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"Identity\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"Interface language selector\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"Invalid\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"Invite code\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"Let's get your password reset!\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"Login complete\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"Login to account that is not listed\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"Logo\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"Looks like {example}\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"Make visible\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"Manage your <0>full identity</0> including your <1>@handle</1>\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"Manage your profile, posts, likes and follows\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"Manage your profile, posts, likes and follows as well as read your private preferences\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"Missing\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"Moderate\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"Name\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"New password\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"Next\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"Okay\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"Only letters, numbers, and hyphens\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"Password\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"Password strength\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"Password strength indicator\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"Password Updated\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"Password updated!\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"Perform actions on your behalf\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"Perform authenticated actions towards <0>any service</0> on your behalf\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"Privacy Policy\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"Publish changes\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"Read and send messages\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"Read and update your account's email address\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"Read your account's email address\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"Remember this account on this device\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"Repository\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"Reset code\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"Reset Password\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"Reset your password\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"Select domain\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"Select from an existing account\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"Sign in\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"Sign in\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"Sign in as {0}\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"Sign in as...\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"Sign up\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"Step {currentPosition} of {count}\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"Strong\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"Submit\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"Support\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"Technical details\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"Temporarily activate or deactivate your account\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"Terms of Service\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"That handle cannot be used\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"The application requests the permissions necessary to perform the following actions on your behalf:\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"The data you submitted is invalid. Please check the form and try again.\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"The domain name is not allowed\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"The handle contains inappropriate language\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"The handle is already in use\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"The handle is invalid\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"The invite code is not valid\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"This application is requesting the following list of technical permissions, summarized hereafter:\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"This authorization request has been denied. Please try again.\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"This email is already used\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"This handle is reserved\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"This sign-in session has expired\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"Towards\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"Type your desired username\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"Unexpected server response\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"Update\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"Upload files\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"Username or email address\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"Valid\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"Verify you are human\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"wants to access your <0/> account\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"wants to uniquely identify you through your <0/> account\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"Warning\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"We're so excited to have you join us!\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"Weak\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"Wrong identifier or password\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"You are being redirected...\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"You can change this username to any domain name you control after your account is set up.\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"You can now sign in with your new password.\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"Your account\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"Your full username will be: {0}\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"Your password has been updated!\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/en-GB/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: en-GB\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/es/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: es\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/eu/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: eu\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/fi/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: fi\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/fr/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:03+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: fr\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"Confirmation 2FA\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"Un second facteur d'authentification est requis\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"Un service contrôlé par <0>{domain}</0>\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"Compte utilisateur\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"Vous avez déjà un code ?\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"Une application sur votre appareil\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"Une erreur inconnue s'est produite\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"Un autre compte\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"N'importe quelle collection\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"N'importe quelle action\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"N'importe quel service\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"Authentification\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"Authoriser l'accès\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"Photo de profile\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"Retour\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"Entre {minLength} et {maxLength} caractères\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"Stockage\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"Services de l'application Bluesky\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"Services de discussion de Bluesky\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"En cliquant sur <0>{0}</0>, vous accorderez à cette application l'accès à votre compte conformément à ses <1>conditions d'utilisation</1> et à sa <2>politique de confidentialité</2>.\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"En créant un compte, vous acceptez les {0} et la {1} de ce service.\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"Appeler\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"Annuler\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"Modifier votre <0>@pseudo</0>\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"Discussions\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"Vérifiez vos emails {0} pour un code de connexion et saisissez-le ici.\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"Choisissez un nom d'utilisateur\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"Code\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"Réduire les détails\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"Collection\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"Confirmer\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"Confirmez votre mot de passe pour continuer\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"Code de confirmation\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"Continuer\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"Erreur de cookies\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"Créer\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"Créer un nouveau compte\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"Créer un compte\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"Créer, modifier et supprimer tout contenu public\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"Supprimer\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"Refuser l'accès\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"Description\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"Adresse email\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"Saisissez un mot de passe\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"Saisissez le code que vous avez reçu pour réinitialiser votre mot de passe.\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"Saisissez l'email que vous avez utilisé pour créer votre compte. Nous vous enverrons un \\\"code de réinitialisation\\\" pour que vous puissiez définir un nouveau mot de passe.\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"Saisissez votre adresse email\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"Saisissez votre nouveau mot de passe\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"Saisissez votre mot de passe\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"Saisissez votre nom d'utilisateur et votre mot de passe\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"Erreur\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"exemple-com-xxxxx-xxxxx\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"Développer les détails\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"Extra\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"Mot de passe oublié\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"Oublié ?\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"Accorder l'accès à votre compte <0>{0}</0>\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"Vous rencontrez des difficultés ? <0>Contactez le support</0>\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"Cacher\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"Accueil\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"Identifiant\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"Identité\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"Sélecteur de langue\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"Invalide\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"Code d'invitation\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"Il semble que votre navigateur n'accepte pas les cookies. Appuyez sur \\\"Continuer\\\" pour réessayer. Si l'erreur persiste, veuillez vous assurer que vos paramètres de confidentialité autorisent les cookies pour le site \\\"{0}\\\".\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"Réinitialisons votre mot de passe !\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"Connexion terminée\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"Se connecter à un compte qui n'est pas listé\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"Logo\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"Ressemble à {example}\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"Rendre visible\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"Gérer votre <0>identité complète</0> y compris votre <1>@pseudo</1>\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"Gérer votre profil, vos posts, vos likes et vos abonnements\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"Gérer votre profil, vos posts, vos likes et vos abonnements ainsi que lire vos préférences privées\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"Manquant\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"Modéré\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"Nom\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"Nouveau mot de passe\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"Suivant\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"OK\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"Uniquement des lettres, des chiffres et des traits d'union\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"Mot de passe\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"Complexité du mot de passe\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"Indicateur de complexité du mot de passe\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"Mot de passe mis à jour\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"Mot de passe mis à jour !\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"Mot de passe d'au moins {MIN_PASSWORD_LENGTH} caractères\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"Effectuer des actions en votre nom\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"Effectuer des actions authentifiées vers <0>n'importe quel service</0> en votre nom\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"Veuillez vérifier le nom de domaine du site web avant de saisir votre mot de passe. N'entrez jamais votre mot de passe sur un domaine auquel vous ne faites pas confiance.\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"Politique de Confidentialité\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"Publier des modifications\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"Lire et envoyer des messages\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"Lire et mettre à jour l'adresse e-mail de votre compte\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"Lire l'adresse e-mail de votre compte\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"Se souvenir de ce compte sur cet appareil\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"Données publiques\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"Code de réinitialisation\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"Réinitialiser le mot de passe\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"Réinitialisez votre mot de passe\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"Sélectionner le domaine\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"Sélectionner un compte\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"Se connecter\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"Connexion\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"Se connecter en tant que {0}\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"Se connecter en tant que...\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"S'inscrire\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"Étape {currentPosition} sur {count}\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"Fort\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"Soumettre\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"Support\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"Détails techniques\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"Activer ou désactiver temporairement votre compte\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"Conditions d'Utilisation\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"Ce pseudonyme ne peut pas être utilisé\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"L'application demande un contrôle total sur votre identité, ce qui signifie qu'elle pourrait <0>casser de façon permanente</0>, ou même <1>usurper</1>, votre compte. N'authorisez l'accès qu'aux applications auxquelles vous faites vraiment confiance.\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"L'application demande à pouvoir créer, modifier et supprimer <0>tout contenu public</0> de votre dépôt.\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"L'application demande les autorisations nécessaires pour effectuer les actions suivantes en votre nom :\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"Le réseau ATProto utilise un mécanisme d'authentification qui permet d'identifier de manière unique les utilisateurs lors de la communication avec des services externes. Cela est généralement utilisé pour récupérer ou mettre à jour des données liées à votre compte, telles que le contenu du fil d'actualité ou de modération.\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"Les données que vous avez soumises sont invalides. Veuillez vérifier le formulaire et réessayer.\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"Le nom de domaine n'est pas autorisé\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"Le pseudonyme contient un langage inapproprié\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"Ce pseudonyme est déjà utilisé\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"Le pseudonyme est invalide\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"Le code d'invitation n'est pas valide\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"Cette application demande la liste suivante de permissions techniques, résumées ci-après :\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"Cette demande d'autorisation a été refusée. Veuillez réessayer.\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"Cet email est déjà utilisé\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"Ce pseudonyme est réservé\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"Cette session de connexion a expiré\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"Auprès de\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"Saisissez le nom d'utilisateur souhaité\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"Réponse inattendue du serveur\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"Modifier\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"Envoyer des fichiers\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"Nom d'utilisateur ou adresse email\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"Valide\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"Vérifiez que vous êtes humain\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"veut accéder à votre compte <0/>\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"veut vous identifier de manière unique via votre compte <0/>\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"Avertissement\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"Nous sommes ravis de vous accueillir parmi nous !\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"Faible\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"Identifiant ou mot de passe incorrect\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"Vous êtes redirigé...\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"Vous pourrez changer ce nom d'utilisateur pour n'importe quel nom de domaine sous votre contrôle une fois votre compte créé.\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"Vous aver reçu un email avec un \\\"code de réinitialisation\\\". Saisissez ce code ici puis entrez votre nouveau mot de passe.\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"Votre compte\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"Votre nom d'utilisateur complet sera : {0}\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"Votre mot de passe a été mis à jour !\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"Votre dépôt contient toutes les données publiquement disponibles sur le réseau ATProto, telles que les publications, les likes et les abonnements Bluesky. Il contient également des données créées via d'autres applications auxquelles vous vous êtes connecté avec ce compte.\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ga/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ga\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/gl/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: gl\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/hi/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: hi\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/hu/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: hu\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ia/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ia\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/id/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: id\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/it/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: it\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ja/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ja\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2025-09-09 06:20+0900\\n\"\n\"Last-Translator: dolciss\\n\"\n\"Language-Team: Japanese\\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"２要素認証の確認\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"２番目の認証要素が必要です\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"<0>{domain}</0> によって管理されているサービス\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"アカウント\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"コードをすでに持っていますか？\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"デバイス上のアプリ\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"何らかのエラーが発生しました\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"他のアカウント\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"すべてのコレクション\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"すべてのメソッド\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"すべてのサービス\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"認証\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"承認する\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"アバター\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"戻る\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"{minLength} 文字以上 {maxLength} 文字以下\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"Blobストレージ\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"Blueksyアプリサービス\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"Blueskyチャットサービス\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"<0>承認する</0>をクリックすると、<1>利用規約</1>と<2>プライバシーポリシー</2>に従って、このアプリが次のアクションを実行することを許可します：\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"アカウントを作成することで、このサービスの {0} および {1} に同意したものとみなされます。\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"呼び出し\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"キャンセル\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"<0>@handle</0> を変更\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"チャット\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"確認コードが記載されたメール {0} を確認し、ここに入力してください。\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"ユーザー名を選んでください\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"コード\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"詳細を閉じる\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"コレクション\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"確認\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"続けるにはパスワードを確認してください\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"確認コード\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"作成\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"新規アカウントの作成\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"アカウントを作成\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"すべての公開レコードの作成、更新、削除\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"削除\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"拒否する\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"説明\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"メールアドレス\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"メールアドレス\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"パスワードを入力\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"パスワードをリセットするために受け取ったコードを入力してください。\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"アカウントの作成に使用したメールアドレスを入力します。新しいパスワードを設定できるように、「リセットコード」をお送りします。\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"メールアドレスを入力してください\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"新しいパスワードを入力してください\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"パスワードを入力\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"ユーザー名とパスワードを入力してください\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"エラー\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"example-com-xxxxx-xxxxx\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"詳細を開く\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"最強\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"パスワードを忘れた\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"忘れた？\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"<0>{0}</0> アカウントへのアクセスを許可\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"なにか問題が発生しましたか？ <0>サポートに連絡</0>\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"隠す\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"ホーム\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"識別子\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"識別情報\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"言語選択\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"無効\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"招待コード\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"パスワードをリセットしましょう！\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"ログイン完了\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"リストにないアカウントにログイン\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"ロゴ\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"{example} みたいなもの\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"表示する\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"あなたの <1>@handle</1> を含む <0>すべての識別情報</0> を管理する\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"あなたのプロフィール、投稿、いいね、フォローを管理\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"あなたのプロフィール、投稿、いいね、フォローを管理し、あなたのプライベートな設定を読み取る\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"無し\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"中程度\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"名前\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"新しいパスワード\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"次へ\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"OK\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"英数字とハイフンのみ\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"パスワード\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"パスワード強度\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"パスワード強度表示\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"パスワードが更新されました\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"パスワードが更新されました！\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"パスワードは {MIN_PASSWORD_LENGTH} 文字以上\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"あなたに代わってアクションを実行する\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"あなたに代わって認証されたアクションを <0>任意のサービス</0> が実行する\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"パスワードを入力する前に、ウェブサイトのドメインを確認してください。信用できないドメインには絶対にパスワードを入力しないでください。\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"プライバシーポリシー\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"公開された変更\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"メッセージの読み取りと送信\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"あなたのアカウントのメールアドレスを読み取り、更新する\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"あなたのアカウントのメールアドレスを読み取る\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"このアカウントをこのデバイスに記憶する\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"リポジトリ\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"リセットコード\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"パスワードをリセット\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"パスワードをリセット\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"ドメインを選択\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"既存のアカウントから選択\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"サインイン\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"サインイン\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"{0} でサインイン\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"アカウントの選択\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"サインアップ\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"ステップ {currentPosition} / {count}\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"強い\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"送信\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"サポート\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"技術的な詳細\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"一時的にアカウントを有効化または無効化する\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"利用規約\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"そのハンドルは使用できません\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"アプリは、あなたのネットワーク識別情報を完全に制御することを求めています。つまり、アカウントを <0>永久に壊す</0> ことや、<1>乗っ取る</1> ことさえ可能です。この権限は、本当に信頼できるアプリにのみ付与してください。\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"アプリは、あなたのリポジトリから <0>あらゆるデータ</0> を作成、更新、削除できるように求めています。\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"アプリは、あなたに代わって次のアクションを実行するために必要な権限を要求しています：\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"ATProtoネットワークは、外部サービスと通信する際にユーザーを一意に識別できる認証メカニズムを使用しています。これは通常、フィードやモデレーションコンテンツなど、あなたのアカウントにリンクされたデータを取得または更新するために使用されます。\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"送信データが正しくありません。フォームをチェックしてもう一度お試しください。\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"そのドメイン名は許可されていません\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"ハンドルが不適切な言葉を含んでいます\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"ハンドルはすでに使用されています\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"ハンドルは無効です\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"招待コードが有効ではありません\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"アプリは、以下に要約された技術的な権限のリストを要求しています：\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"この承認リクエストは拒否されました。もう一度お試しください。\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"このメールアドレスはすでに使用されています\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"このハンドルは予約されています\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"このサインインセッションの有効期限が切れました\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"対象\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"希望するユーザー名をタイプしてください\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"予期しないサーバー応答\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"更新\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"ファイルのアップロード\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"ユーザー名またはメールアドレス\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"有効\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"あなたが人間であることを確認\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"あなたの <0/> アカウントにアクセスしようとしています\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"あなたの <0/> アカウントを通じてあなたを識別しようとしています\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"警告\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"私たちはあなたが参加してくれることをとても楽しみにしています！\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"弱い\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"識別子またはパスワードが間違っています\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"リダイレクトしています…\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"アカウントのセットアップ後にユーザー名を自分がコントロールしているドメイン名に変更することができます。\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"新しいパスワードでサインインできるようになりました。\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"「リセットコード」が記載されたメールが届きます。ここにコードを入力し、新しいパスワードを入力します。\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"あなたのアカウント\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"あなたのフルユーザー名： {0}\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"パスワードが変更されました！\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"あなたのリポジトリには、Blueskyの投稿、いいね、フォローなど、ATProtoネットワークで公開されているすべてのデータが含まれています。また、このアカウントを使用してサインインした他のアプリを通じて作成されたデータも含まれています。\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/km/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: km\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ko/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ko\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/load.ts",
    "content": "import { Messages } from '@lingui/core'\n\n// @NOTE run \"pnpm run po:compile\" to compile the messages from the PO files\n\nexport async function loadMessages(locale: string): Promise<Messages> {\n  const { messages } = await import(`./${locale}/messages.ts`)\n  return messages\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/locale-provider.tsx",
    "content": "import { I18n } from '@lingui/core'\nimport { I18nProvider } from '@lingui/react'\nimport {\n  ReactNode,\n  createContext,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from 'react'\n// @NOTE run \"pnpm run po:compile\" to compile the messages from the PO files\nimport { messages as en } from './en/messages.ts'\nimport { loadMessages } from './load.ts'\nimport { Locale, detectLocale, isLocale, locales } from './locales.ts'\n\nexport type LocaleContextValue = {\n  locale: string\n  locales: Partial<Record<Locale, { name: string; flag?: string }>>\n  setLocale: (locale: Locale) => void\n}\n\nconst LocaleContext = createContext<LocaleContextValue | null>(null)\n\nexport function useLocaleContext(): LocaleContextValue {\n  const context = useContext(LocaleContext)\n  if (!context) {\n    throw new Error('useLocaleContext must be used within a LocaleProvider')\n  }\n  return context\n}\n\nexport function useCurrentLocale(): string {\n  return useLocaleContext().locale\n}\n\nexport function LocaleProvider({\n  userLocales = [],\n  children,\n}: {\n  userLocales?: readonly string[]\n  children?: ReactNode\n}) {\n  // Bundle \"en\" messages with the app\n  const i18n = useMemo(() => new I18n({ locale: 'en', messages: { en } }), [])\n\n  const [currentLocale, setCurrentLocale] = useState<string>(() => i18n.locale)\n  const [desiredLocale, setDesiredLocale] = useState<Locale>(() => {\n    return detectLocale(userLocales)\n  })\n\n  // A boolean that is used to avoid flickering of \"en\" content during initial\n  // load.\n  const [initialized, setInitialized] = useState(\n    desiredLocale === currentLocale,\n  )\n\n  // Protect against illegal change of the locale directly through the i18n object\n  useEffect(() => {\n    if (!isLocale(currentLocale)) {\n      setDesiredLocale('en')\n    }\n  }, [locales, currentLocale])\n\n  // Keep currentLocale in sync with i18n's locale prop\n  useEffect(() => {\n    const onChange = () => setCurrentLocale(i18n.locale)\n    i18n.on('change', onChange)\n    return () => i18n.removeListener('change', onChange)\n  }, [i18n])\n\n  // Trigger loading of `desiredLocale`\n  useEffect(() => {\n    if (currentLocale === desiredLocale) {\n      setInitialized(true)\n      return\n    }\n\n    let canceled = false\n    loadMessages(desiredLocale)\n      .then((messages) => {\n        i18n.load(desiredLocale, messages)\n        if (!canceled) i18n.activate(desiredLocale)\n      })\n      .catch((err) => {\n        console.error(`Failed to load locale \"${desiredLocale}\":`, err)\n      })\n      .finally(() => {\n        if (!canceled) setInitialized(true)\n      })\n    return () => {\n      canceled = true\n    }\n  }, [currentLocale, desiredLocale])\n\n  const value = useMemo<LocaleContextValue>(\n    () => ({\n      locale: currentLocale,\n      locales,\n      setLocale: (locale) => {\n        if (isLocale(locale)) setDesiredLocale(locale)\n        else throw new TypeError(`\"${locale}\" is not an available locale`)\n      },\n    }),\n    [locales, currentLocale],\n  )\n\n  return (\n    <LocaleContext value={value}>\n      <I18nProvider i18n={i18n}>{initialized && children}</I18nProvider>\n    </LocaleContext>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/locale-selector.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX } from 'react'\nimport { useLocaleContext } from './locale-provider.tsx'\n\nexport type LocaleSelectorProps = Omit<\n  JSX.IntrinsicElements['select'],\n  'value' | 'defaultValue'\n>\n\nexport function LocaleSelector({\n  className,\n  onChange,\n  ...props\n}: LocaleSelectorProps) {\n  const { locale, locales, setLocale } = useLocaleContext()\n  const { t } = useLingui()\n\n  return (\n    <select\n      {...props}\n      className={clsx(\n        'accent-primary',\n        'cursor-pointer',\n        // Background\n        'bg-gray-100 dark:bg-gray-800',\n        'hover:bg-gray-200 dark:hover:bg-gray-700',\n        // Border\n        'transition duration-300 ease-in-out',\n        'outline-none',\n        'focus:ring-primary focus:ring-2 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-offset-black',\n        // Font\n        'text-slate-600 dark:text-slate-300',\n        // Layout\n        'rounded-lg',\n        'p-2 pr-1',\n        className,\n      )}\n      value={locale}\n      onChange={(e) => {\n        onChange?.(e)\n        if (!e.defaultPrevented) {\n          setLocale(e.target.value as keyof typeof locales)\n        }\n      }}\n      aria-label={t`Interface language selector`}\n    >\n      {Object.entries(locales).map(([key, { name, flag }]) => (\n        <option key={key} value={key}>\n          {flag ? `${flag} ${name}` : name}\n        </option>\n      ))}\n    </select>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/locales.ts",
    "content": "// @TODO Enable locales once they get translated\nexport const locales = {\n  // an: {\n  //   name: 'Aragonés',\n  // },\n  // ast: {\n  //   name: 'Asturianu',\n  // },\n  // ca: {\n  //   name: 'Català',\n  //   flag: '🇦🇩', // Andorra's flag (though Andorra does not cover the entire zone speaking Català)\n  // },\n  // da: {\n  //   name: 'Dansk',\n  //   flag: '🇩🇰',\n  // },\n  // de: {\n  //   name: 'Deutsch',\n  //   flag: '🇩🇪',\n  // },\n  // el: {\n  //   name: 'Ελληνικά',\n  //   flag: '🇬🇷',\n  // },\n  en: {\n    name: 'English',\n    flag: '🇺🇸',\n  },\n  // 'en-GB': {\n  //   name: 'English (UK)',\n  //   flag: '🇬🇧',\n  // },\n  // es: {\n  //   name: 'Español',\n  //   flag: '🇪🇸',\n  // },\n  // eu: {\n  //   name: 'Euskara',\n  // },\n  // fi: {\n  //   name: 'Suomi',\n  //   flag: '🇫🇮',\n  // },\n  fr: {\n    name: 'Français',\n    flag: '🇫🇷',\n  },\n  // ga: {\n  //   name: 'Gaeilge',\n  //   flag: '🇮🇪',\n  // },\n  // gl: {\n  //   name: 'Galego',\n  // },\n  // hi: {\n  //   name: 'हिन्दी',\n  //   flag: '🇮🇳',\n  // },\n  // hu: {\n  //   name: 'Magyar',\n  //   flag: '🇭🇺',\n  // },\n  // ia: {\n  //   name: 'Interlingua',\n  // },\n  // id: {\n  //   name: 'Bahasa Indonesia',\n  //   flag: '🇮🇩',\n  // },\n  // it: {\n  //   name: 'Italiano',\n  //   flag: '🇮🇹',\n  // },\n  ja: {\n    name: '日本語',\n    flag: '🇯🇵',\n  },\n  // km: {\n  //   name: 'ភាសាខ្មែរ',\n  //   flag: '🇰🇭',\n  // },\n  // ko: {\n  //   name: '한국어',\n  //   flag: '🇰🇷',\n  // },\n  // ne: {\n  //   name: 'नेपाली',\n  //   flag: '🇳🇵',\n  // },\n  // nl: {\n  //   name: 'Nederlands',\n  //   flag: '🇳🇱',\n  // },\n  // pl: {\n  //   name: 'Polski',\n  //   flag: '🇵🇱',\n  // },\n  // 'pt-BR': {\n  //   name: 'Português (Brasil)',\n  //   flag: '🇧🇷',\n  // },\n  // ro: {\n  //   name: 'Română',\n  //   flag: '🇷🇴',\n  // },\n  // ru: {\n  //   name: 'Русский',\n  //   flag: '🇷🇺',\n  // },\n  // sv: {\n  //   name: 'Svenska',\n  //   flag: '🇸🇪',\n  // },\n  // th: {\n  //   name: 'ไทย',\n  //   flag: '🇹🇭',\n  // },\n  // tr: {\n  //   name: 'Türkçe',\n  //   flag: '🇹🇷',\n  // },\n  // uk: {\n  //   name: 'Українська',\n  //   flag: '🇺🇦',\n  // },\n  // vi: {\n  //   name: 'Tiếng Việt',\n  //   flag: '🇻🇳',\n  // },\n  // 'zh-CN': {\n  //   name: '中文(简体)',\n  //   flag: '🇨🇳',\n  // },\n  // 'zh-HK': {\n  //   name: '中文(香港)',\n  //   flag: '🇭🇰',\n  // },\n  // 'zh-TW': {\n  //   name: '中文(繁體)',\n  //   flag: '🇹🇼',\n  // },\n} as const satisfies Record<string, { name: string; flag?: string }>\n\nexport type Locale = keyof typeof locales\n\nexport function isLocale(v: unknown): v is Locale {\n  return typeof v === 'string' && Object.hasOwn(locales, v)\n}\n\nexport function asLocale(locale: string): Locale | undefined {\n  if (isLocale(locale)) {\n    return locale\n  }\n\n  // Resolve similar locales (e.g. \"fr-BE\" -> \"fr\")\n  const lang = locale.split('-')[0]\n  if (isLocale(lang)) {\n    return lang\n  }\n\n  // Resolve similar locals (e.g. \"pt-PT\" -> \"pt-BR\")\n  for (const locale in locales) {\n    if (locale.startsWith(`${lang}-`)) {\n      return locale as keyof typeof locales\n    }\n  }\n\n  return undefined\n}\n\nexport function detectLocale(userLocales: readonly string[] = []): Locale {\n  for (const locale of userLocales) {\n    const resolved = asLocale(locale)\n    if (resolved) return resolved\n  }\n\n  for (const locale of navigator.languages) {\n    const resolved = asLocale(locale)\n    if (resolved) return resolved\n  }\n\n  return 'en'\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ne/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ne\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/nl/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: nl\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/pl/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: pl\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/pt-BR/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: pt-BR\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ro/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ro\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/ru/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ru\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/sv/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: sv\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/th/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: th\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/tr/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: tr\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/uk/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: uk\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/vi/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: vi\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/zh-CN/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh-CN\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/zh-HK/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh-HK\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/locales/zh-TW/messages.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2025-02-27 14:42+0100\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh-TW\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:221\nmsgid \"2FA Confirmation\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:27\nmsgid \"A second authentication factor is required\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:848\nmsgid \"A service controlled by <0>{domain}</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:279\nmsgid \"Account\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:71\nmsgid \"Already have a code?\"\nmsgstr \"\"\n\n#: src/components/utils/client-name.tsx:46\nmsgid \"An application on your device\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:87\nmsgid \"An unknown error occurred\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:113\nmsgid \"Another account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:876\nmsgid \"Any collection\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:818\nmsgid \"Any method\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:857\nmsgid \"Any service\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:28\n#: src/components/utils/scope-description.tsx:455\n#: src/components/utils/scope-description.tsx:475\nmsgid \"Authenticate\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-view.tsx:60\n#: src/views/authorize/consent/consent-form.tsx:105\n#: src/views/authorize/consent/consent-form.tsx:174\nmsgid \"Authorize\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:79\nmsgid \"Avatar\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:59\n#: src/views/authorize/sign-in/sign-in-form.tsx:132\n#: src/views/authorize/reset-password/reset-password-view.tsx:65\n#: src/views/authorize/reset-password/reset-password-view.tsx:95\n#: src/views/authorize/consent/consent-form.tsx:99\n#: src/components/forms/wizard-card.tsx:93\nmsgid \"Back\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:152\nmsgid \"Between {minLength} and {maxLength} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:742\nmsgid \"Blob storage\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:833\nmsgid \"Bluesky App services\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:840\nmsgid \"Bluesky Chat services\"\nmsgstr \"\"\n\n#. placeholder {0}: consentLabel || <Trans>Authorize</Trans>\n#: src/views/authorize/consent/consent-form.tsx:173\nmsgid \"By clicking <0>{0}</0>, you will grant this application access to your account in accordance with its <1>terms of service</1> and <2>privacy policy</2>.\"\nmsgstr \"\"\n\n#. placeholder {0}: tosLink ? ( <LinkAnchor className=\"text-primary underline\" link={tosLink}> <Trans>Terms of Service</Trans> </LinkAnchor> ) : ( <Trans>Terms of Service</Trans> )\n#. placeholder {1}: ppLink ? ( <LinkAnchor className=\"text-primary underline\" link={ppLink}> <Trans>Privacy Policy</Trans> </LinkAnchor> ) : ( <Trans>Privacy Policy</Trans> )\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:30\nmsgid \"By creating an account you agree to the {0} and the {1} of this service.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:550\nmsgctxt \"RPC lxm\"\nmsgid \"Call\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:176\n#: src/views/authorize/welcome/welcome-view.tsx:51\n#: src/components/forms/form-card-async.tsx:85\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:431\nmsgid \"Change your <0>@handle</0>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:391\nmsgid \"Chat\"\nmsgstr \"\"\n\n#. placeholder {0}: secondFactor.hint\n#: src/views/authorize/sign-in/sign-in-form.tsx:234\nmsgid \"Check your {0} email for a login code and enter it here.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:99\nmsgid \"Choose a username\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:76\nmsgid \"Code\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Collapse details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:710\nmsgid \"Collection\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:139\nmsgctxt \"verb\"\nmsgid \"Confirm\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:69\nmsgid \"Confirm your password to continue\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:225\nmsgid \"Confirmation code\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:34\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: src/views/error/cookie-error-view.tsx:30\nmsgid \"Cookie Error\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:711\nmsgid \"Create\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:35\nmsgid \"Create a new account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:82\nmsgid \"Create Account\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:596\nmsgid \"Create, update, and delete any public record\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:713\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:109\nmsgid \"Deny access\"\nmsgstr \"\"\n\n#: src/components/utils/error-card.tsx:83\nmsgid \"Description\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:114\n#: src/components/utils/scope-description.tsx:239\nmsgid \"Email\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:55\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:60\n#: src/components/forms/input-email-address.tsx:51\nmsgid \"Email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:44\nmsgid \"Enter a password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:85\nmsgid \"Enter the code you received to reset your password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:70\nmsgid \"Enter the email you used to create your account. We'll send you a \\\"reset code\\\" so you can set a new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-request-form.tsx:58\nmsgid \"Enter your email address\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:45\nmsgid \"Enter your new password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:90\nmsgid \"Enter your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:109\n#: src/views/authorize/sign-in/sign-in-view.tsx:126\nmsgid \"Enter your username and password\"\nmsgstr \"\"\n\n#: src/views/error/error-view.tsx:28\nmsgid \"Error\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:102\nmsgid \"example-com-xxxxx-xxxxx\"\nmsgstr \"\"\n\n#: src/components/utils/description-card.tsx:74\n#: src/components/utils/description-card.tsx:76\nmsgid \"Expand details\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:25\nmsgid \"Extra\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:53\nmsgid \"Forgot Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:189\nmsgid \"Forgot?\"\nmsgstr \"\"\n\n#. placeholder {0}: account.preferred_username || account.email || account.sub\n#: src/views/authorize/consent/consent-view.tsx:45\nmsgid \"Grant access to your <0>{0}</0> account\"\nmsgstr \"\"\n\n#: src/components/utils/help-card.tsx:32\nmsgid \"Having trouble? <0>Contact support</0>\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Hide\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:17\nmsgid \"Home\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:91\n#: src/views/authorize/sign-in/sign-in-form.tsx:146\nmsgid \"Identifier\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:424\nmsgid \"Identity\"\nmsgstr \"\"\n\n#: src/locales/locale-selector.tsx:46\nmsgid \"Interface language selector\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:286\nmsgid \"Invalid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:96\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:101\nmsgid \"Invite code\"\nmsgstr \"\"\n\n#. placeholder {0}: url.hostname\n#: src/views/error/cookie-error-view.tsx:40\nmsgid \"It seems that your browser is not accepting cookies. Press \\\"Continue\\\" to try again. If the error persists, please ensure that your privacy settings allow cookies for the \\\"{0}\\\" website.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:55\nmsgid \"Let's get your password reset!\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:215\nmsgid \"Login complete\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:107\nmsgid \"Login to account that is not listed\"\nmsgstr \"\"\n\n#: src/components/layouts/layout-welcome.tsx:48\nmsgid \"Logo\"\nmsgstr \"\"\n\n#: src/components/forms/input-token.tsx:59\nmsgid \"Looks like {example}\"\nmsgstr \"\"\n\n#: src/components/forms/button-toggle-visibility.tsx:34\nmsgid \"Make visible\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:427\nmsgid \"Manage your <0>full identity</0> including your <1>@handle</1>\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:362\nmsgid \"Manage your profile, posts, likes and follows\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:357\nmsgid \"Manage your profile, posts, likes and follows as well as read your private preferences\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:33\nmsgid \"Missing\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:29\nmsgid \"Moderate\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:84\nmsgid \"Name\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:74\nmsgid \"New password\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:60\n#: src/views/authorize/reset-password/reset-password-view.tsx:90\n#: src/components/forms/wizard-card.tsx:96\nmsgid \"Next\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:119\nmsgid \"Okay\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:157\nmsgid \"Only letters, numbers, and hyphens\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-account-form.tsx:128\n#: src/views/authorize/sign-in/sign-in-form.tsx:170\n#: src/components/forms/input-password.tsx:53\nmsgid \"Password\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:23\nmsgid \"Password strength\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-meter.tsx:45\nmsgid \"Password strength indicator\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:106\nmsgid \"Password Updated\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:113\nmsgid \"Password updated!\"\nmsgstr \"\"\n\n#: src/components/forms/input-new-password.tsx:46\nmsgid \"Password with at least {MIN_PASSWORD_LENGTH} characters\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:476\nmsgid \"Perform actions on your behalf\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:457\nmsgid \"Perform authenticated actions towards <0>any service</0> on your behalf\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:201\nmsgid \"Please verify the domain name of the website before entering your password. Never enter your password on a domain you do not trust.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:42\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:45\n#: src/components/utils/link-title.tsx:18\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:617\nmsgid \"Publish changes\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:392\nmsgid \"Read and send messages\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:242\nmsgid \"Read and update your account's email address\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:244\nmsgid \"Read your account's email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:209\n#: src/views/authorize/sign-in/sign-in-form.tsx:214\nmsgid \"Remember this account on this device\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:595\n#: src/components/utils/scope-description.tsx:616\nmsgid \"Repository\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:59\nmsgid \"Reset code\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:82\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:187\nmsgid \"Reset your password\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:196\nmsgid \"Select domain\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:141\nmsgid \"Select from an existing account\"\nmsgstr \"\"\n\n#: src/views/authorize/welcome/welcome-view.tsx:45\n#: src/views/authorize/sign-in/sign-in-form.tsx:141\nmsgctxt \"verb\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-view.tsx:52\nmsgctxt \"noun\"\nmsgid \"Sign in\"\nmsgstr \"\"\n\n#. placeholder {0}: account.name\n#: src/views/authorize/sign-in/sign-in-picker.tsx:78\nmsgid \"Sign in as {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-picker.tsx:64\nmsgid \"Sign in as...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:88\n#: src/views/authorize/sign-in/sign-in-picker.tsx:53\nmsgid \"Sign up\"\nmsgstr \"\"\n\n#: src/components/forms/wizard-card.tsx:106\nmsgid \"Step {currentPosition} of {count}\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:27\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: src/components/forms/form-card-async.tsx:96\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: src/components/utils/link-title.tsx:20\nmsgid \"Support\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:142\nmsgid \"Technical details\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:280\nmsgid \"Temporarily activate or deactivate your account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:34\n#: src/views/authorize/sign-up/sign-up-disclaimer.tsx:37\n#: src/components/utils/link-title.tsx:19\nmsgid \"Terms of Service\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:52\nmsgid \"That handle cannot be used\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:202\nmsgid \"The application is asking for full control over your network identity, meaning that it could <0>permanently break</0>, or even <1>steal</1>, your account. Only grant this permission to applications you really trust.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:602\nmsgid \"The application is asking to be able to create, update, and delete <0>any data</0> from your repository.\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:169\n#: src/components/utils/scope-description.tsx:482\n#: src/components/utils/scope-description.tsx:623\nmsgid \"The application requests the permissions necessary to perform the following actions on your behalf:\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:497\nmsgid \"The ATProto network uses an authentication mechanism that allows to uniquely identify users when communicating with external services. This is typically used to retrieve or update data linked to your account, such as feed or moderation content.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:69\nmsgid \"The data you submitted is invalid. Please check the form and try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:43\nmsgid \"The domain name is not allowed\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:45\nmsgid \"The handle contains inappropriate language\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:50\nmsgid \"The handle is already in use\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:41\nmsgid \"The handle is invalid\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:35\nmsgid \"The invite code is not valid\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:147\nmsgid \"This application is requesting the following list of technical permissions, summarized hereafter:\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:77\nmsgid \"This authorization request has been denied. Please try again.\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:57\nmsgid \"This email is already used\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:48\nmsgid \"This handle is reserved\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:64\nmsgid \"This sign-in session has expired\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:553\nmsgctxt \"RPC aud\"\nmsgid \"Towards\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:166\nmsgid \"Type your desired username\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:84\nmsgid \"Unexpected server response\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:712\nmsgid \"Update\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:745\nmsgid \"Upload files\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:151\nmsgid \"Username or email address\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:281\nmsgid \"Valid\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:140\nmsgid \"Verify you are human\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:136\nmsgid \"wants to access your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/consent/consent-form.tsx:131\nmsgid \"wants to uniquely identify you through your <0/> account\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-in/sign-in-form.tsx:200\nmsgid \"Warning\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:84\nmsgid \"We're so excited to have you join us!\"\nmsgstr \"\"\n\n#: src/components/utils/password-strength-label.tsx:31\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: src/components/utils/error-message.tsx:31\nmsgid \"Wrong identifier or password\"\nmsgstr \"\"\n\n#: src/views/authorize/authorize-view.tsx:216\nmsgid \"You are being redirected...\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:247\nmsgid \"You can change this username to any domain name you control after your account is set up.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:116\nmsgid \"You can now sign in with your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-confirm-form.tsx:53\nmsgid \"You will receive an email with a \\\"reset code\\\". Enter that code here then enter your new password.\"\nmsgstr \"\"\n\n#: src/views/authorize/sign-up/sign-up-view.tsx:121\nmsgid \"Your account\"\nmsgstr \"\"\n\n#. placeholder {0}: segment.length ? ( <strong className=\"text-gray-800 dark:text-gray-200\"> {preview} </strong> ) : ( <span aria-hidden className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\" /> )\n#: src/views/authorize/sign-up/sign-up-handle-form.tsx:230\nmsgid \"Your full username will be: {0}\"\nmsgstr \"\"\n\n#: src/views/authorize/reset-password/reset-password-view.tsx:108\nmsgid \"Your password has been updated!\"\nmsgstr \"\"\n\n#: src/components/utils/scope-description.tsx:638\nmsgid \"Your repository contains all the data publicly available on the ATProto network, such as Bluesky posts, likes, and follows. It also contains data created through other apps you've signed into using this account.\"\nmsgstr \"\"\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/style.css",
    "content": "@import 'tailwindcss';\n\n/* Styles applies on be <body> tag by the oauth provider */\n@source inline(\"bg-white\");\n@source inline(\"text-slate-900\");\n@source inline(\"dark:bg-slate-900\");\n@source inline(\"dark:text-slate-100\");\n\n:root {\n  --branding-color-primary: 131 56 236;\n  --branding-color-primary-contrast: 255 255 255;\n  --branding-color-primary-hue: 265;\n\n  --branding-color-error: 255 0 110;\n  --branding-color-error-contrast: 0 0 0;\n  --branding-color-error-hue: 334.11764705882354;\n\n  --branding-color-warning: 255 171 15;\n  --branding-color-warning-contrast: 0 0 0;\n  --branding-color-warning-hue: 38.99999999999999;\n\n  --branding-color-success: 23 204 136;\n  --branding-color-success-contrast: 0 0 0;\n  --branding-color-success-hue: 157.4585635359116;\n}\n\n:root {\n  --hue-primary: var(--branding-color-primary-hue);\n  --hue-error: var(--branding-color-error-hue);\n  --hue-warning: var(--branding-color-warning-hue);\n  --hue-success: var(--branding-color-success-hue);\n\n  --color-primary: rgb(var(--branding-color-primary));\n  --color-error: rgb(var(--branding-color-error));\n  --color-warning: rgb(var(--branding-color-warning));\n  --color-success: rgb(var(--branding-color-success));\n\n  --color-primary-contrast: rgb(var(--branding-color-primary-contrast));\n  --color-error-contrast: rgb(var(--branding-color-error-contrast));\n  --color-warning-contrast: rgb(var(--branding-color-warning-contrast));\n  --color-success-contrast: rgb(var(--branding-color-success-contrast));\n\n  --color-contrast-0: hsl(var(--hue-primary) 20% 100%);\n  --color-contrast-25: hsl(var(--hue-primary) 20% 95.3%);\n  --color-contrast-50: hsl(var(--hue-primary) 20% 90.6%);\n  --color-contrast-100: hsl(var(--hue-primary) 20% 85.9%);\n  --color-contrast-200: hsl(var(--hue-primary) 20% 81.2%);\n  --color-contrast-300: hsl(var(--hue-primary) 20% 71.8%);\n  --color-contrast-400: hsl(var(--hue-primary) 20% 62.4%);\n  --color-contrast-500: hsl(var(--hue-primary) 20% 53%);\n  --color-contrast-600: hsl(var(--hue-primary) 20% 43.6%);\n  --color-contrast-700: hsl(var(--hue-primary) 20% 34.2%);\n  --color-contrast-800: hsl(var(--hue-primary) 20% 24.8%);\n  --color-contrast-900: hsl(var(--hue-primary) 20% 20.1%);\n  --color-contrast-950: hsl(var(--hue-primary) 20% 15.4%);\n  --color-contrast-975: hsl(var(--hue-primary) 20% 10.7%);\n  --color-contrast-1000: hsl(var(--hue-primary) 20% 6%);\n\n  --color-primary-25: hsl(var(--hue-primary) 100% 97%);\n  --color-primary-50: hsl(var(--hue-primary) 100% 95%);\n  --color-primary-100: hsl(var(--hue-primary) 100% 90%);\n  --color-primary-200: hsl(var(--hue-primary) 100% 80%);\n  --color-primary-300: hsl(var(--hue-primary) 100% 70%);\n  --color-primary-400: hsl(var(--hue-primary) 100% 60%);\n  --color-primary-500: hsl(var(--hue-primary) 100% 53%);\n  --color-primary-600: hsl(var(--hue-primary) 100% 42%);\n  --color-primary-700: hsl(var(--hue-primary) 100% 34%);\n  --color-primary-800: hsl(var(--hue-primary) 100% 26%);\n  --color-primary-900: hsl(var(--hue-primary) 100% 18%);\n  --color-primary-950: hsl(var(--hue-primary) 100% 10%);\n  --color-primary-975: hsl(var(--hue-primary) 100% 7%);\n\n  --color-error-25: hsl(var(--hue-error) 82% 97%);\n  --color-error-50: hsl(var(--hue-error) 82% 95%);\n  --color-error-100: hsl(var(--hue-error) 82% 90%);\n  --color-error-200: hsl(var(--hue-error) 82% 80%);\n  --color-error-300: hsl(var(--hue-error) 82% 70%);\n  --color-error-400: hsl(var(--hue-error) 82% 60%);\n  --color-error-500: hsl(var(--hue-error) 82% 53%);\n  --color-error-600: hsl(var(--hue-error) 82% 42%);\n  --color-error-700: hsl(var(--hue-error) 82% 34%);\n  --color-error-800: hsl(var(--hue-error) 82% 26%);\n  --color-error-900: hsl(var(--hue-error) 82% 18%);\n  --color-error-950: hsl(var(--hue-error) 82% 10%);\n  --color-error-975: hsl(var(--hue-error) 82% 7%);\n\n  --color-warning-25: hsl(var(--hue-warning) 100% 97%);\n  --color-warning-50: hsl(var(--hue-warning) 100% 95%);\n  --color-warning-100: hsl(var(--hue-warning) 100% 90%);\n  --color-warning-200: hsl(var(--hue-warning) 100% 80%);\n  --color-warning-300: hsl(var(--hue-warning) 100% 70%);\n  --color-warning-400: hsl(var(--hue-warning) 100% 60%);\n  --color-warning-500: hsl(var(--hue-warning) 100% 53%);\n  --color-warning-600: hsl(var(--hue-warning) 100% 42%);\n  --color-warning-700: hsl(var(--hue-warning) 100% 34%);\n  --color-warning-800: hsl(var(--hue-warning) 100% 26%);\n  --color-warning-900: hsl(var(--hue-warning) 100% 18%);\n  --color-warning-950: hsl(var(--hue-warning) 100% 10%);\n  --color-warning-975: hsl(var(--hue-warning) 100% 7%);\n\n  --color-success-25: hsl(var(--hue-success) 91% 97%);\n  --color-success-50: hsl(var(--hue-success) 91% 95%);\n  --color-success-100: hsl(var(--hue-success) 91% 90%);\n  --color-success-200: hsl(var(--hue-success) 91% 80%);\n  --color-success-300: hsl(var(--hue-success) 91% 70%);\n  --color-success-400: hsl(var(--hue-success) 91% 60%);\n  --color-success-500: hsl(var(--hue-success) 91% 53%);\n  --color-success-600: hsl(var(--hue-success) 91% 42%);\n  --color-success-700: hsl(var(--hue-success) 91% 34%);\n  --color-success-800: hsl(var(--hue-success) 91% 26%);\n  --color-success-900: hsl(var(--hue-success) 91% 18%);\n  --color-success-950: hsl(var(--hue-success) 91% 10%);\n  --color-success-975: hsl(var(--hue-success) 91% 7%);\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --color-contrast-1000: hsl(var(--hue-primary) 20% 100%);\n    --color-contrast-975: hsl(var(--hue-primary) 20% 95.3%);\n    --color-contrast-950: hsl(var(--hue-primary) 20% 90.6%);\n    --color-contrast-900: hsl(var(--hue-primary) 20% 85.9%);\n    --color-contrast-800: hsl(var(--hue-primary) 20% 81.2%);\n    --color-contrast-700: hsl(var(--hue-primary) 20% 71.8%);\n    --color-contrast-600: hsl(var(--hue-primary) 20% 62.4%);\n    --color-contrast-500: hsl(var(--hue-primary) 20% 53%);\n    --color-contrast-400: hsl(var(--hue-primary) 20% 43.6%);\n    --color-contrast-300: hsl(var(--hue-primary) 20% 34.2%);\n    --color-contrast-200: hsl(var(--hue-primary) 20% 24.8%);\n    --color-contrast-100: hsl(var(--hue-primary) 20% 20.1%);\n    --color-contrast-50: hsl(var(--hue-primary) 20% 15.4%);\n    --color-contrast-25: hsl(var(--hue-primary) 20% 10.7%);\n    --color-contrast-0: hsl(var(--hue-primary) 20% 6%);\n  }\n}\n\n@theme inline {\n  --color-contrast-0: var(--color-contrast-0);\n  --color-contrast-25: var(--color-contrast-25);\n  --color-contrast-50: var(--color-contrast-50);\n  --color-contrast-100: var(--color-contrast-100);\n  --color-contrast-200: var(--color-contrast-200);\n  --color-contrast-300: var(--color-contrast-300);\n  --color-contrast-400: var(--color-contrast-400);\n  --color-contrast-500: var(--color-contrast-500);\n  --color-contrast-600: var(--color-contrast-600);\n  --color-contrast-700: var(--color-contrast-700);\n  --color-contrast-800: var(--color-contrast-800);\n  --color-contrast-900: var(--color-contrast-900);\n  --color-contrast-950: var(--color-contrast-950);\n  --color-contrast-975: var(--color-contrast-975);\n  --color-contrast-1000: var(--color-contrast-1000);\n\n  --color-primary-25: var(--color-primary-25);\n  --color-primary-50: var(--color-primary-50);\n  --color-primary-100: var(--color-primary-100);\n  --color-primary-200: var(--color-primary-200);\n  --color-primary-300: var(--color-primary-300);\n  --color-primary-400: var(--color-primary-400);\n  --color-primary-500: var(--color-primary-500);\n  --color-primary-600: var(--color-primary-600);\n  --color-primary-700: var(--color-primary-700);\n  --color-primary-800: var(--color-primary-800);\n  --color-primary-900: var(--color-primary-900);\n  --color-primary-950: var(--color-primary-950);\n  --color-primary-975: var(--color-primary-975);\n\n  --color-error-25: var(--color-error-25);\n  --color-error-50: var(--color-error-50);\n  --color-error-100: var(--color-error-100);\n  --color-error-200: var(--color-error-200);\n  --color-error-300: var(--color-error-300);\n  --color-error-400: var(--color-error-400);\n  --color-error-500: var(--color-error-500);\n  --color-error-600: var(--color-error-600);\n  --color-error-700: var(--color-error-700);\n  --color-error-800: var(--color-error-800);\n  --color-error-900: var(--color-error-900);\n  --color-error-950: var(--color-error-950);\n  --color-error-975: var(--color-error-975);\n\n  --color-warning-25: var(--color-warning-25);\n  --color-warning-50: var(--color-warning-50);\n  --color-warning-100: var(--color-warning-100);\n  --color-warning-200: var(--color-warning-200);\n  --color-warning-300: var(--color-warning-300);\n  --color-warning-400: var(--color-warning-400);\n  --color-warning-500: var(--color-warning-500);\n  --color-warning-600: var(--color-warning-600);\n  --color-warning-700: var(--color-warning-700);\n  --color-warning-800: var(--color-warning-800);\n  --color-warning-900: var(--color-warning-900);\n  --color-warning-950: var(--color-warning-950);\n  --color-warning-975: var(--color-warning-975);\n\n  --color-success-25: var(--color-success-25);\n  --color-success-50: var(--color-success-50);\n  --color-success-100: var(--color-success-100);\n  --color-success-200: var(--color-success-200);\n  --color-success-300: var(--color-success-300);\n  --color-success-400: var(--color-success-400);\n  --color-success-500: var(--color-success-500);\n  --color-success-600: var(--color-success-600);\n  --color-success-700: var(--color-success-700);\n  --color-success-800: var(--color-success-800);\n  --color-success-900: var(--color-success-900);\n  --color-success-950: var(--color-success-950);\n  --color-success-975: var(--color-success-975);\n\n  --color-primary: var(--color-primary);\n  --color-error: var(--color-error);\n  --color-warning: var(--color-warning);\n  --color-success: var(--color-success);\n\n  --color-primary-contrast: var(--color-primary-contrast);\n  --color-error-contrast: var(--color-error-contrast);\n  --color-warning-contrast: var(--color-warning-contrast);\n  --color-success-contrast: var(--color-success-contrast);\n\n  --color-text-default: var(--color-contrast-900);\n  --color-text-light: var(--color-contrast-700);\n  --color-border-default: var(--color-contrast-200);\n  --color-border-dark: var(--color-contrast-400);\n\n  --space-screen: 100vh;\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/authorize-view.tsx",
    "content": "import type { AuthorizeData } from '#/hydration-data.d.ts'\nimport { Trans, useLingui } from '@lingui/react/macro'\nimport { useEffect, useState } from 'react'\nimport type { CustomizationData, Session } from '@atproto/oauth-provider-api'\nimport { OAuthPromptMode } from '@atproto/oauth-types'\nimport {\n  LayoutTitlePage,\n  LayoutTitlePageProps,\n} from '../../components/layouts/layout-title-page.tsx'\nimport { useApi } from '../../hooks/use-api.ts'\nimport { useBoundDispatch } from '../../hooks/use-bound-dispatch.ts'\nimport { Override } from '../../lib/util.ts'\nimport { ConsentView } from './consent/consent-view.tsx'\nimport { ResetPasswordView } from './reset-password/reset-password-view.tsx'\nimport { SignInView } from './sign-in/sign-in-view.tsx'\nimport { SignUpView } from './sign-up/sign-up-view.tsx'\nimport { WelcomeView } from './welcome/welcome-view.tsx'\n\nexport type AuthorizeViewProps = Override<\n  LayoutTitlePageProps,\n  {\n    customizationData?: CustomizationData\n    authorizeData: AuthorizeData\n    initialSessions: readonly Session[]\n  }\n>\n\nenum View {\n  Welcome,\n  SignUp,\n  SignIn,\n  ResetPassword,\n  Consent,\n  Done,\n}\n\nfunction getInitialView(\n  promptMode: OAuthPromptMode | undefined,\n  canSignUp: boolean,\n  forceSignIn: boolean,\n  hasInitialSessions: boolean,\n): (typeof View)[keyof typeof View] {\n  if (promptMode === 'create' && canSignUp) {\n    return View.SignUp\n  } else if (forceSignIn) {\n    return View.SignIn\n  } else if (!canSignUp || hasInitialSessions) {\n    return View.SignIn\n  }\n\n  return View.Welcome\n}\n\nexport function AuthorizeView({\n  authorizeData,\n  initialSessions,\n  customizationData,\n\n  // LayoutTitlePage\n  ...props\n}: AuthorizeViewProps) {\n  const { t } = useLingui()\n\n  const forceSignIn = authorizeData.loginHint != null\n\n  const hasAvailableSessions = Boolean(initialSessions.length)\n  const hasAvailableUserDomains = Boolean(\n    customizationData?.availableUserDomains?.length,\n  )\n  const canSignUp = !forceSignIn && hasAvailableUserDomains\n\n  const initialView = getInitialView(\n    authorizeData.promptMode,\n    hasAvailableUserDomains,\n    forceSignIn,\n    hasAvailableSessions,\n  )\n\n  const [view, setView] = useState<View>(initialView)\n\n  const showDone = useBoundDispatch(setView, View.Done)\n  const showSignIn = useBoundDispatch(setView, View.SignIn)\n  const showResetPassword = useBoundDispatch(setView, View.ResetPassword)\n  const showSignUp = useBoundDispatch(setView, View.SignUp)\n  const showConsent = useBoundDispatch(setView, View.Consent)\n\n  const [resetPasswordHint, setResetPasswordHint] = useState<\n    string | undefined\n  >(undefined)\n\n  const {\n    sessions,\n    selectSub,\n    doValidateNewHandle,\n    doSignUp,\n    doSignIn,\n    doInitiatePasswordReset,\n    doConfirmResetPassword,\n    doConsent,\n    doReject,\n  } = useApi({\n    sessions: initialSessions,\n    onRedirected: showDone,\n  })\n\n  const homeView = !canSignUp || sessions.length ? View.SignIn : View.Welcome\n  const showHome = useBoundDispatch(setView, homeView)\n  const showSignUpIfAllowed = canSignUp ? showSignUp : undefined\n\n  // Navigate when the user signs-in (selects a new session)\n  const session = sessions.find((s) => s.selected && !s.loginRequired)\n  useEffect(() => {\n    if (session) {\n      if (session.consentRequired) showConsent()\n      else doConsent(session.account.sub)\n    }\n  }, [session, doConsent, showConsent])\n\n  // Fool-proofing\n  useEffect(() => {\n    if (view === View.SignUp && !canSignUp) setView(homeView)\n  }, [view, homeView, !canSignUp])\n  useEffect(() => {\n    if (view === View.Consent && !session) setView(homeView)\n  }, [view, homeView, !session])\n  useEffect(() => {\n    if (view === View.Welcome && homeView !== View.Welcome) setView(homeView)\n  }, [view, homeView])\n\n  if (view === View.Welcome) {\n    return (\n      <WelcomeView\n        {...props}\n        customizationData={customizationData}\n        onSignIn={showSignIn}\n        onSignUp={showSignUpIfAllowed}\n        onCancel={doReject}\n      />\n    )\n  }\n\n  if (view === View.SignUp) {\n    return (\n      <SignUpView\n        {...props}\n        customizationData={customizationData}\n        onValidateNewHandle={doValidateNewHandle}\n        onBack={showHome}\n        onDone={doSignUp}\n      />\n    )\n  }\n\n  if (view === View.ResetPassword) {\n    return (\n      <ResetPasswordView\n        {...props}\n        emailDefault={resetPasswordHint}\n        onresetPasswordRequest={doInitiatePasswordReset}\n        onResetPasswordConfirm={doConfirmResetPassword}\n        onBack={showHome}\n      />\n    )\n  }\n\n  if (view === View.SignIn) {\n    return (\n      <SignInView\n        {...props}\n        loginHint={authorizeData.loginHint}\n        sessions={sessions}\n        selectSub={selectSub}\n        onSignIn={doSignIn}\n        onSignUp={showSignUpIfAllowed}\n        onBack={homeView === View.SignIn ? doReject : showHome}\n        backLabel={homeView === View.SignIn ? t`Cancel` : undefined}\n        onForgotPassword={(email) => {\n          showResetPassword()\n          setResetPasswordHint(email)\n        }}\n      />\n    )\n  }\n\n  if (view === View.Consent) {\n    // TypeSafety: should never be null here\n    if (!session) return null\n\n    return (\n      <ConsentView\n        {...props}\n        clientId={authorizeData.clientId}\n        clientMetadata={authorizeData.clientMetadata}\n        clientTrusted={authorizeData.clientTrusted}\n        clientFirstParty={authorizeData.clientFirstParty}\n        permissionSets={authorizeData.permissionSets}\n        account={session.account}\n        scope={authorizeData.scope}\n        onConsent={(scope) => doConsent(session.account.sub, scope)}\n        onReject={doReject}\n        onBack={\n          forceSignIn\n            ? undefined\n            : () => {\n                selectSub(null)\n                showHome()\n              }\n        }\n      />\n    )\n  }\n\n  if (view === View.Done) {\n    return (\n      <LayoutTitlePage {...props} title={props.title ?? t`Login complete`}>\n        <Trans>You are being redirected...</Trans>\n      </LayoutTitlePage>\n    )\n  }\n\n  // Fool-proofing\n  throw new Error('Unexpected application state')\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/consent/consent-form.tsx",
    "content": "import type { PermissionSets } from '#/hydration-data.d.ts'\nimport { Trans, useLingui } from '@lingui/react/macro'\nimport { ReactNode, useState } from 'react'\nimport { ClientImage } from '#/components/utils/client-image.tsx'\nimport { DescriptionCard } from '#/components/utils/description-card.tsx'\nimport { ScopeDescription } from '#/components/utils/scope-description.tsx'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport { AccountPermission } from '@atproto/oauth-scopes'\nimport type { OAuthClientMetadata } from '@atproto/oauth-types'\nimport { Button } from '../../../components/forms/button.tsx'\nimport {\n  FormCard,\n  FormCardProps,\n} from '../../../components/forms/form-card.tsx'\nimport { AccountIdentifier } from '../../../components/utils/account-identifier.tsx'\nimport { ClientName } from '../../../components/utils/client-name.tsx'\nimport { Override } from '../../../lib/util.ts'\n\nexport type ConsentFormProps = Override<\n  Omit<FormCardProps, 'onSubmit' | 'cancel' | 'actions' | 'children'>,\n  {\n    clientId: string\n    clientMetadata: OAuthClientMetadata\n    clientTrusted: boolean\n    clientFirstParty: boolean\n    permissionSets: PermissionSets\n\n    account: Account\n    scope?: string\n\n    onConsent: (scope?: string) => void\n    consentLabel?: ReactNode\n\n    onReject: () => void\n    rejectLabel?: ReactNode\n\n    onBack?: () => void\n    backLabel?: ReactNode\n  }\n>\n\nfunction isTransitionScope(scope: string): scope is `transition:${string}` {\n  return scope.startsWith('transition:')\n}\n\nfunction isAccountEmailScope(scope: string): boolean {\n  const parsed = AccountPermission.fromString(scope)\n  if (!parsed) return false\n  return parsed.matches({ attr: 'email', action: 'read' })\n}\n\nfunction stripAccountEmailScope(scope?: string): string | undefined {\n  return scope\n    ?.split(' ')\n    .filter((s) => !isAccountEmailScope(s))\n    .join(' ')\n}\n\nexport function ConsentForm({\n  clientId,\n  clientMetadata,\n  clientTrusted,\n  clientFirstParty,\n  permissionSets,\n\n  account,\n  scope,\n\n  onConsent,\n  consentLabel,\n\n  onReject,\n  rejectLabel,\n\n  onBack,\n  backLabel,\n\n  // FormCardProps\n  ...props\n}: ConsentFormProps) {\n  const { t } = useLingui()\n  const [allowEmail, setAllowEmail] = useState(true)\n\n  // Require the granular scope system to be able to unset the `account:email`\n  // scope.\n  const canUnsetEmail = !scope?.split(' ').some(isTransitionScope)\n\n  return (\n    <FormCard\n      {...props}\n      onSubmit={(event) => {\n        event.preventDefault()\n        const acceptedScope =\n          canUnsetEmail && !allowEmail ? stripAccountEmailScope(scope) : scope\n        onConsent(acceptedScope)\n      }}\n      cancel={\n        onBack && (\n          <Button onClick={onBack}>{backLabel || <Trans>Back</Trans>}</Button>\n        )\n      }\n      actions={\n        <>\n          <Button type=\"submit\" color=\"primary\">\n            {consentLabel || <Trans>Authorize</Trans>}\n          </Button>\n\n          <Button onClick={onReject}>\n            {rejectLabel || <Trans>Deny access</Trans>}\n          </Button>\n        </>\n      }\n    >\n      <DescriptionCard\n        image={\n          <ClientImage\n            clientId={clientId}\n            clientMetadata={clientMetadata}\n            clientTrusted={clientTrusted}\n          />\n        }\n        title={\n          <ClientName\n            clientId={clientId}\n            clientMetadata={clientMetadata}\n            clientTrusted={clientTrusted}\n          />\n        }\n        description={\n          !scope || scope === 'atproto' ? (\n            <Trans>\n              wants to uniquely identify you through your{' '}\n              <AccountIdentifier account={account} /> account\n            </Trans>\n          ) : (\n            <Trans>\n              wants to access your <AccountIdentifier account={account} />{' '}\n              account\n            </Trans>\n          )\n        }\n        hint={t`Technical details`}\n      >\n        {scope ? (\n          <>\n            <p>\n              <Trans>\n                This application is requesting the following list of technical\n                permissions, summarized hereafter:\n              </Trans>\n            </p>\n            <ul className=\"mt-2\">\n              {scope.split(' ').map((scope) => (\n                <li key={scope}>\n                  <code>{scope}</code>\n                </li>\n              ))}\n            </ul>\n          </>\n        ) : null}\n      </DescriptionCard>\n\n      <ScopeDescription\n        scope={scope}\n        permissionSets={permissionSets}\n        clientTrusted={clientTrusted}\n        clientFirstParty={clientFirstParty}\n        allowEmail={canUnsetEmail ? allowEmail : true}\n        onAllowEmail={canUnsetEmail ? setAllowEmail : undefined}\n      />\n\n      <p>\n        <Trans>\n          By clicking <b>{consentLabel || <Trans>Authorize</Trans>}</b>, you\n          will grant this application access to your account in accordance with\n          its{' '}\n          <a\n            role=\"link\"\n            href={clientMetadata.tos_uri}\n            rel=\"nofollow noopener\"\n            target=\"_blank\"\n            className={\n              clientMetadata.tos_uri ? 'text-primary underline' : undefined\n            }\n          >\n            <Trans>terms of service</Trans>\n          </a>\n          {' and '}\n          <a\n            role=\"link\"\n            href={clientMetadata.policy_uri}\n            rel=\"nofollow noopener\"\n            target=\"_blank\"\n            className={\n              clientMetadata.policy_uri ? 'text-primary underline' : undefined\n            }\n          >\n            <Trans>privacy policy</Trans>\n          </a>\n          .\n        </Trans>\n      </p>\n    </FormCard>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/consent/consent-view.tsx",
    "content": "import type { PermissionSets } from '#/hydration-data.d.ts'\nimport { Trans, useLingui } from '@lingui/react/macro'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport type { OAuthClientMetadata } from '@atproto/oauth-types'\nimport {\n  LayoutTitlePage,\n  LayoutTitlePageProps,\n} from '../../../components/layouts/layout-title-page.tsx'\nimport { Override } from '../../../lib/util.ts'\nimport { ConsentForm } from './consent-form.tsx'\n\nexport type ConsentViewProps = Override<\n  LayoutTitlePageProps,\n  {\n    clientId: string\n    clientMetadata: OAuthClientMetadata\n    clientTrusted: boolean\n    clientFirstParty: boolean\n    permissionSets: PermissionSets\n\n    account: Account\n    scope?: string\n\n    onConsent: (scope?: string) => void\n    onReject: () => void\n    onBack?: () => void\n  }\n>\n\nexport function ConsentView({\n  clientId,\n  clientMetadata,\n  clientTrusted,\n  clientFirstParty,\n  permissionSets,\n  account,\n  scope,\n  onConsent,\n  onReject,\n  onBack,\n\n  // LayoutTitlePage\n  title,\n  subtitle = (\n    <Trans>\n      Grant access to your{' '}\n      <b className=\"text-slate-800 dark:text-slate-200\">\n        {account.preferred_username || account.email || account.sub}\n      </b>{' '}\n      account\n    </Trans>\n  ),\n  ...props\n}: ConsentViewProps) {\n  const { t } = useLingui()\n\n  return (\n    <LayoutTitlePage\n      {...props}\n      title={title ?? t`Authorize`}\n      subtitle={subtitle}\n    >\n      <ConsentForm\n        clientId={clientId}\n        clientMetadata={clientMetadata}\n        clientTrusted={clientTrusted}\n        clientFirstParty={clientFirstParty}\n        permissionSets={permissionSets}\n        account={account}\n        scope={scope}\n        onBack={onBack}\n        onConsent={onConsent}\n        onReject={onReject}\n      />\n    </LayoutTitlePage>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/reset-password/reset-password-confirm-form.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { useRef, useState } from 'react'\nimport { Fieldset } from '../../../components/forms/fieldset.tsx'\nimport {\n  FormCardAsync,\n  FormCardAsyncProps,\n} from '../../../components/forms/form-card-async.tsx'\nimport { InputNewPassword } from '../../../components/forms/input-new-password.tsx'\nimport { InputToken } from '../../../components/forms/input-token.tsx'\nimport { Admonition } from '../../../components/utils/admonition.tsx'\nimport { useRandomString } from '../../../hooks/use-random-string.ts'\nimport { Override } from '../../../lib/util.ts'\n\nexport type ResetPasswordConfirmFormProps = Override<\n  FormCardAsyncProps,\n  {\n    onSubmit: (\n      data: {\n        token: string\n        password: string\n      },\n      signal: AbortSignal,\n    ) => void | PromiseLike<void>\n\n    tokenPattern?: string\n    tokenFormat?: string\n    tokenParseValue?: (value: string) => string | false\n  }\n>\n\nexport function ResetPasswordConfirmForm({\n  onSubmit,\n\n  // FormCardAsyncProps\n  invalid,\n  ...props\n}: ResetPasswordConfirmFormProps) {\n  const tokenAriaId = useRandomString({ prefix: 'reset-pwd-email-' })\n  const passwordRef = useRef<HTMLInputElement>(null)\n\n  const [token, setToken] = useState<string | null>(null)\n  const [password, setPassword] = useState<string | undefined>(undefined)\n\n  return (\n    <FormCardAsync\n      {...props}\n      onSubmit={(signal) => {\n        if (token && password) return onSubmit({ token, password }, signal)\n      }}\n      invalid={invalid || !token || !password}\n    >\n      <Admonition id={tokenAriaId} type=\"status\">\n        <Trans>\n          You will receive an email with a \"reset code\". Enter that code here\n          then enter your new password.\n        </Trans>\n      </Admonition>\n\n      <Fieldset label={<Trans>Reset code</Trans>}>\n        <InputToken\n          name=\"code\"\n          aria-labelledby={tokenAriaId}\n          enterKeyHint=\"next\"\n          required\n          autoFocus={true}\n          onToken={(token) => {\n            setToken(token)\n            // Auto-focus next field when token is complete\n            if (token) passwordRef.current?.focus()\n          }}\n        />\n      </Fieldset>\n\n      <Fieldset label={<Trans>New password</Trans>}>\n        <InputNewPassword\n          ref={passwordRef}\n          name=\"password\"\n          enterKeyHint=\"done\"\n          required\n          password={password}\n          onPassword={setPassword}\n        />\n      </Fieldset>\n    </FormCardAsync>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/reset-password/reset-password-request-form.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { useCallback, useRef, useState } from 'react'\nimport { Fieldset } from '../../../components/forms/fieldset.tsx'\nimport {\n  AsyncActionController,\n  FormCardAsync,\n  FormCardAsyncProps,\n} from '../../../components/forms/form-card-async.tsx'\nimport { InputEmailAddress } from '../../../components/forms/input-email-address.tsx'\nimport { Admonition } from '../../../components/utils/admonition.tsx'\nimport { useRandomString } from '../../../hooks/use-random-string.ts'\nimport { mergeRefs } from '../../../lib/ref.ts'\nimport { Override } from '../../../lib/util.ts'\n\nexport type ResetPasswordRequestFormProps = Override<\n  Omit<FormCardAsyncProps, 'children'>,\n  {\n    emailDefault?: string\n    onSubmit: (\n      data: { email: string },\n      signal: AbortSignal,\n    ) => void | PromiseLike<void>\n  }\n>\n\nexport function ResetPasswordRequestForm({\n  emailDefault,\n  onSubmit,\n\n  // FormCardAsyncProps\n  invalid,\n  ref,\n  ...props\n}: ResetPasswordRequestFormProps) {\n  const { t } = useLingui()\n  const emailAriaId = useRandomString({ prefix: 'reset-pwd-email-' })\n  const [email, setEmail] = useState(emailDefault)\n\n  const ctrlRef = useRef<AsyncActionController>(null)\n\n  const doSubmit = useCallback(\n    (signal: AbortSignal) => {\n      if (email) return onSubmit({ email }, signal)\n    },\n    [email, onSubmit],\n  )\n\n  return (\n    <FormCardAsync\n      {...props}\n      ref={mergeRefs([ref, ctrlRef])}\n      invalid={invalid || !email}\n      onSubmit={doSubmit}\n    >\n      <Fieldset label={<Trans>Email address</Trans>}>\n        <InputEmailAddress\n          name=\"email\"\n          placeholder={t`Enter your email address`}\n          aria-labelledby={emailAriaId}\n          title={t`Email address`}\n          required\n          autoFocus={true}\n          value={email}\n          onEmail={(email) => {\n            ctrlRef.current?.reset()\n            setEmail(email)\n          }}\n        />\n        <Admonition type=\"status\" id={emailAriaId}>\n          <Trans>\n            Enter the email you used to create your account. We'll send you a\n            \"reset code\" so you can set a new password.\n          </Trans>\n        </Admonition>\n      </Fieldset>\n    </FormCardAsync>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/reset-password/reset-password-view.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { useState } from 'react'\nimport { Button } from '../../../components/forms/button.tsx'\nimport {\n  LayoutTitlePage,\n  LayoutTitlePageProps,\n} from '../../../components/layouts/layout-title-page.tsx'\nimport { Override } from '../../../lib/util.ts'\nimport { ResetPasswordConfirmForm } from './reset-password-confirm-form.tsx'\nimport { ResetPasswordRequestForm } from './reset-password-request-form.tsx'\n\nexport type ResetPasswordViewProps = Override<\n  LayoutTitlePageProps,\n  {\n    emailDefault?: string\n    onresetPasswordRequest: (\n      data: { email: string },\n      signal: AbortSignal,\n    ) => void | PromiseLike<void>\n    onResetPasswordConfirm: (\n      data: {\n        token: string\n        password: string\n      },\n      signal: AbortSignal,\n    ) => void | PromiseLike<void>\n    onBack: () => void\n  }\n>\n\nenum View {\n  RequestReset,\n  ConfirmReset,\n  PasswordUpdated,\n}\n\nexport function ResetPasswordView({\n  emailDefault,\n  onresetPasswordRequest,\n  onResetPasswordConfirm,\n  onBack,\n\n  // LayoutTitlePage\n  ...props\n}: ResetPasswordViewProps) {\n  const { t } = useLingui()\n  const [view, setView] = useState<View>(View.RequestReset)\n\n  if (view === View.RequestReset) {\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={props.title || t`Forgot Password`}\n        subtitle={\n          props.subtitle || <Trans>Let's get your password reset!</Trans>\n        }\n      >\n        <ResetPasswordRequestForm\n          emailDefault={emailDefault}\n          submitLabel={<Trans>Next</Trans>}\n          onSubmit={async (data, signal) => {\n            await onresetPasswordRequest(data, signal)\n            if (!signal.aborted) setView(View.ConfirmReset)\n          }}\n          cancelLabel={<Trans>Back</Trans>}\n          onCancel={onBack}\n        />\n        <hr className=\"my-5 border-gray-300 dark:border-gray-700\" />\n        <center>\n          <Button transparent onClick={() => setView(View.ConfirmReset)}>\n            <Trans>Already have a code?</Trans>\n          </Button>\n        </center>\n      </LayoutTitlePage>\n    )\n  }\n\n  if (view === View.ConfirmReset) {\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={props.title || t`Reset Password`}\n        subtitle={\n          props.subtitle || (\n            <Trans>Enter the code you received to reset your password.</Trans>\n          )\n        }\n      >\n        <ResetPasswordConfirmForm\n          submitLabel={<Trans>Next</Trans>}\n          onSubmit={async (data, signal) => {\n            await onResetPasswordConfirm(data, signal)\n            if (!signal.aborted) setView(View.PasswordUpdated)\n          }}\n          cancelLabel={<Trans>Back</Trans>}\n          onCancel={onBack}\n        />\n      </LayoutTitlePage>\n    )\n  }\n\n  if (view === View.PasswordUpdated) {\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={props.title || t`Password Updated`}\n        subtitle={\n          props.subtitle || <Trans>Your password has been updated!</Trans>\n        }\n      >\n        <center>\n          <h2 className=\"pb-2 text-xl font-bold\">\n            <Trans>Password updated!</Trans>\n          </h2>\n          <p className=\"pb-4\">\n            <Trans>You can now sign in with your new password.</Trans>\n          </p>\n          <Button color=\"primary\" onClick={onBack}>\n            <Trans>Okay</Trans>\n          </Button>\n        </center>\n      </LayoutTitlePage>\n    )\n  }\n\n  throw new Error(`Invalid view: ${view}`)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-in/sign-in-form.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { ReactNode, useCallback, useRef, useState } from 'react'\nimport { Button } from '../../../components/forms/button.tsx'\nimport { Fieldset } from '../../../components/forms/fieldset.tsx'\nimport {\n  FormCardAsync,\n  FormCardAsyncProps,\n} from '../../../components/forms/form-card-async.tsx'\nimport { InputCheckbox } from '../../../components/forms/input-checkbox.tsx'\nimport { InputPassword } from '../../../components/forms/input-password.tsx'\nimport { InputText } from '../../../components/forms/input-text.tsx'\nimport { InputToken } from '../../../components/forms/input-token.tsx'\nimport { Admonition } from '../../../components/utils/admonition.tsx'\nimport { AtSymbolIcon } from '../../../components/utils/icons.tsx'\nimport { AsyncActionController } from '../../../hooks/use-async-action.ts'\nimport {\n  InvalidCredentialsError,\n  SecondAuthenticationFactorRequiredError,\n} from '../../../lib/api.ts'\nimport { mergeRefs } from '../../../lib/ref.ts'\nimport { Override } from '../../../lib/util.ts'\n\nexport type SignInFormOutput = {\n  username: string\n  password: string\n  remember?: boolean\n}\n\nexport type SignInFormProps = Override<\n  Omit<FormCardAsyncProps, 'append' | 'onCancel'>,\n  {\n    usernameDefault?: string\n    usernameReadonly?: boolean\n    rememberDefault?: boolean\n\n    onBack?: () => void\n    backLabel?: ReactNode\n    onForgotPassword?: (emailHint?: string) => void\n    onSubmit: (\n      credentials: SignInFormOutput,\n      signal: AbortSignal,\n    ) => void | PromiseLike<void>\n  }\n>\n\nexport function SignInForm({\n  usernameDefault = '',\n  usernameReadonly = false,\n  rememberDefault = false,\n\n  onSubmit,\n  onBack,\n  backLabel,\n  onForgotPassword,\n\n  // FormCardAsync\n  ref,\n  invalid,\n  children,\n  ...props\n}: SignInFormProps) {\n  const { t } = useLingui()\n\n  const [username, setUsername] = useState<string>(usernameDefault)\n  const [password, setPassword] = useState<string>('')\n  const [remember, setRemember] = useState<boolean>(rememberDefault)\n  const [otp, setOtp] = useState<string | null>(null)\n\n  const [secondFactor, setSecondFactor] =\n    useState<null | SecondAuthenticationFactorRequiredError>(null)\n\n  const [loading, setLoading] = useState(false)\n\n  const formRef = useRef<AsyncActionController>(null)\n\n  const clearSecondFactor = useCallback(() => {\n    setOtp(null)\n    setSecondFactor(null)\n  }, [setOtp, setSecondFactor])\n\n  const resetState = useCallback(() => {\n    clearSecondFactor()\n    formRef.current?.reset()\n  }, [clearSecondFactor, formRef])\n\n  const doSubmit = useCallback(\n    async (signal: AbortSignal) => {\n      try {\n        await onSubmit(\n          {\n            username,\n            password,\n            remember,\n            ...(secondFactor ? { [secondFactor.type]: otp } : {}),\n          },\n          signal,\n        )\n      } catch (err) {\n        if (signal.aborted) {\n          // If the action was aborted, ignore the error\n          return\n        }\n\n        if (err instanceof SecondAuthenticationFactorRequiredError) {\n          setSecondFactor(err)\n\n          // Do not re-throw 2FA required error to prevent the form from from\n          // displaying it. Instead, we handle the error by showing the second\n          // factor form.\n          return\n        }\n\n        if (err instanceof InvalidCredentialsError) {\n          // If the username/password are not valid, clear the second factor\n          // as valid credentials are a pre-requisite for 2FA.\n          clearSecondFactor()\n        }\n\n        // Any thrown err will be displayed through the form's errorRender\n        throw err\n      }\n    },\n    [username, password, remember, secondFactor, otp, onSubmit],\n  )\n\n  return (\n    <FormCardAsync\n      {...props}\n      ref={mergeRefs([ref, formRef])}\n      onLoading={setLoading}\n      onCancel={onBack}\n      cancelLabel={backLabel ?? t`Back`}\n      append={children}\n      invalid={\n        invalid || !username || !password || (secondFactor != null && !otp)\n      }\n      submitLabel={\n        secondFactor ? (\n          <Trans context=\"verb\">Confirm</Trans>\n        ) : (\n          <Trans context=\"verb\">Sign in</Trans>\n        )\n      }\n      onSubmit={doSubmit}\n    >\n      <Fieldset disabled={loading} label={<Trans>Identifier</Trans>}>\n        <InputText\n          icon={<AtSymbolIcon className=\"w-5\" />}\n          name=\"username\"\n          type=\"text\"\n          title={t`Username or email address`}\n          autoCapitalize=\"none\"\n          autoCorrect=\"off\"\n          autoComplete=\"username\"\n          spellCheck=\"false\"\n          dir=\"auto\"\n          enterKeyHint=\"next\"\n          required\n          readOnly={usernameReadonly}\n          disabled={usernameReadonly}\n          autoFocus={!usernameReadonly}\n          value={username}\n          onChange={(event) => {\n            resetState()\n            setUsername(event.target.value)\n          }}\n        />\n      </Fieldset>\n\n      <Fieldset disabled={loading} label={<Trans>Password</Trans>}>\n        <InputPassword\n          name=\"password\"\n          onChange={(event) => {\n            resetState()\n            setPassword(event.target.value)\n          }}\n          append={\n            onForgotPassword && (\n              <Button\n                className=\"text-sm\"\n                type=\"button\"\n                onClick={() => {\n                  onForgotPassword(\n                    username?.includes('@') ? username : undefined,\n                  )\n                }}\n                aria-label={t`Reset your password`}\n              >\n                <Trans>Forgot?</Trans>\n              </Button>\n            )\n          }\n          enterKeyHint={secondFactor ? 'next' : 'done'}\n          disabled={loading}\n          autoFocus={usernameReadonly}\n          required\n        />\n      </Fieldset>\n\n      <Admonition role=\"alert\" title={<Trans>Warning</Trans>}>\n        <Trans>\n          Please verify the domain name of the website before entering your\n          password. Never enter your password on a domain you do not trust.\n        </Trans>\n      </Admonition>\n\n      <InputCheckbox\n        name=\"remember\"\n        title={t`Remember this account on this device`}\n        enterKeyHint={secondFactor ? 'next' : 'done'}\n        checked={remember}\n        onChange={(event) => setRemember(event.target.checked)}\n      >\n        <Trans>Remember this account on this device</Trans>\n      </InputCheckbox>\n\n      {secondFactor && (\n        <Fieldset\n          key=\"2fa\"\n          disabled={loading}\n          label={<Trans>2FA Confirmation</Trans>}\n        >\n          <div>\n            <InputToken\n              title={t`Confirmation code`}\n              enterKeyHint=\"done\"\n              required\n              autoFocus={true}\n              value={otp ?? ''}\n              onToken={setOtp}\n            />\n\n            <p className=\"text-sm text-slate-600 dark:text-slate-400\">\n              <Trans>\n                Check your {secondFactor.hint} email for a login code and enter\n                it here.\n              </Trans>\n            </p>\n          </div>\n        </Fieldset>\n      )}\n    </FormCardAsync>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-in/sign-in-picker.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { ReactNode } from 'react'\nimport type { Account } from '@atproto/oauth-provider-api'\nimport { Button } from '../../../components/forms/button.tsx'\nimport {\n  FormCard,\n  FormCardProps,\n} from '../../../components/forms/form-card.tsx'\nimport { InputContainer } from '../../../components/forms/input-container.tsx'\nimport { AccountImage } from '../../../components/utils/account-image.tsx'\nimport {\n  AtSymbolIcon,\n  ChevronRightIcon,\n} from '../../../components/utils/icons.tsx'\nimport { Override } from '../../../lib/util.ts'\n\nexport type SignInPickerProps = Override<\n  Omit<FormCardProps, 'cancel' | 'actions' | 'append'>,\n  {\n    accounts: readonly Account[]\n\n    onAccount: (account: Account) => void\n    onOther?: () => void\n    onBack?: () => void\n    onSignUp?: () => void\n\n    backLabel?: ReactNode\n  }\n>\n\nexport function SignInPicker({\n  accounts,\n\n  onAccount,\n  onOther = undefined,\n  onBack,\n  onSignUp,\n\n  backLabel,\n\n  // FormCard\n  children,\n  ...props\n}: SignInPickerProps) {\n  const { t } = useLingui()\n  return (\n    <FormCard\n      {...props}\n      append={children}\n      actions={\n        onSignUp && (\n          <Button onClick={onSignUp} color=\"primary\" transparent>\n            <Trans>Sign up</Trans>\n          </Button>\n        )\n      }\n      cancel={\n        onBack && (\n          <Button onClick={onBack}>{backLabel || <Trans>Back</Trans>}</Button>\n        )\n      }\n    >\n      <p className=\"text-sm font-medium text-slate-600 dark:text-slate-400\">\n        <Trans>Sign in as...</Trans>\n      </p>\n\n      {accounts.map((account) => (\n        <InputContainer\n          tabIndex={0}\n          key={account.sub}\n          onKeyDown={(event) => {\n            if (event.key === 'Enter' || event.key === ' ') {\n              onAccount(account)\n            }\n          }}\n          onClick={() => onAccount(account)}\n          role=\"button\"\n          aria-label={t`Sign in as ${account.name}`}\n          icon={<AccountImage src={account.picture} alt={t`Avatar`} />}\n          append={<ChevronRightIcon aria-hidden className=\"h-4\" />}\n        >\n          <span className=\"flex flex-wrap items-center\">\n            {account.name && (\n              <span className=\"mr-2 truncate font-medium\" arial-label={t`Name`}>\n                {account.name}\n              </span>\n            )}\n\n            <span\n              className=\"truncate text-sm text-neutral-500 dark:text-neutral-400\"\n              arial-label={t`Identifier`}\n            >\n              {account.preferred_username || account.email || account.sub}\n            </span>\n          </span>\n        </InputContainer>\n      ))}\n\n      {onOther && (\n        <InputContainer\n          key=\"other\"\n          tabIndex={0}\n          onKeyDown={(event) => {\n            if (event.key === 'Enter' || event.key === ' ') onOther()\n          }}\n          onClick={onOther}\n          aria-label={t`Login to account that is not listed`}\n          role=\"button\"\n          append={<ChevronRightIcon aria-hidden className=\"h-4\" />}\n          icon={<AtSymbolIcon aria-hidden className=\"h-4 w-6\" />}\n        >\n          <span className=\"truncate text-slate-700 dark:text-slate-400\">\n            <Trans>Another account</Trans>\n          </span>\n        </InputContainer>\n      )}\n    </FormCard>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-in/sign-in-view.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'\nimport type { Session } from '@atproto/oauth-provider-api'\nimport {\n  LayoutTitlePage,\n  LayoutTitlePageProps,\n} from '../../../components/layouts/layout-title-page.tsx'\nimport { Override } from '../../../lib/util.ts'\nimport { SignInForm, SignInFormOutput } from './sign-in-form.tsx'\nimport { SignInPicker } from './sign-in-picker.tsx'\n\nexport type SignInViewProps = Override<\n  LayoutTitlePageProps,\n  {\n    sessions: readonly Session[]\n    selectSub: (sub: string | null) => void\n    loginHint?: string\n\n    onSignIn: (\n      credentials: SignInFormOutput,\n      signal: AbortSignal,\n    ) => void | PromiseLike<void>\n    onSignUp?: () => void\n    onForgotPassword?: (emailHint?: string) => void\n    onBack?: () => void\n    backLabel?: ReactNode\n  }\n>\n\nexport function SignInView({\n  loginHint,\n  sessions,\n  selectSub,\n\n  onSignIn,\n  onSignUp,\n  onForgotPassword,\n  onBack,\n  backLabel,\n\n  // LayoutTitlePage\n  title,\n  subtitle,\n  ...props\n}: SignInViewProps) {\n  const { t } = useLingui()\n  const session = useMemo(() => sessions.find((s) => s.selected), [sessions])\n  const clearSession = useCallback(() => selectSub(null), [selectSub])\n  const accounts = useMemo(() => sessions.map((s) => s.account), [sessions])\n  const [showSignInForm, setShowSignInForm] = useState(sessions.length === 0)\n\n  title ??= t({ message: 'Sign in', context: 'noun' })\n\n  useEffect(() => {\n    // Make sure the \"back\" action shows the account picker instead of the\n    // sign-in form (since the account was added to the list of current\n    // sessions).\n    if (session) setShowSignInForm(false)\n  }, [session])\n\n  if (session) {\n    // All set (parent view will handle the redirect)\n    if (!session.loginRequired) return null\n\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={title}\n        subtitle={subtitle ?? <Trans>Confirm your password to continue</Trans>}\n      >\n        <SignInForm\n          onSubmit={onSignIn}\n          onForgotPassword={onForgotPassword}\n          onBack={clearSession}\n          usernameDefault={\n            session.account.preferred_username || session.account.sub\n          }\n          usernameReadonly={true}\n          rememberDefault={true}\n        />\n      </LayoutTitlePage>\n    )\n  }\n\n  if (loginHint) {\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={title}\n        subtitle={subtitle ?? <Trans>Enter your password</Trans>}\n      >\n        <SignInForm\n          onSubmit={onSignIn}\n          onForgotPassword={onForgotPassword}\n          onBack={onBack}\n          backLabel={backLabel}\n          usernameDefault={loginHint}\n          usernameReadonly={true}\n        />\n      </LayoutTitlePage>\n    )\n  }\n\n  if (sessions.length === 0) {\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={title}\n        subtitle={subtitle ?? <Trans>Enter your username and password</Trans>}\n      >\n        <SignInForm\n          onSubmit={onSignIn}\n          onForgotPassword={onForgotPassword}\n          onBack={onBack}\n          backLabel={backLabel}\n        />\n      </LayoutTitlePage>\n    )\n  }\n\n  if (showSignInForm) {\n    return (\n      <LayoutTitlePage\n        {...props}\n        title={title}\n        subtitle={subtitle ?? <Trans>Enter your username and password</Trans>}\n      >\n        <SignInForm\n          onSubmit={onSignIn}\n          onForgotPassword={onForgotPassword}\n          onBack={() => setShowSignInForm(false)}\n        />\n      </LayoutTitlePage>\n    )\n  }\n\n  return (\n    <LayoutTitlePage\n      {...props}\n      title={title}\n      subtitle={subtitle ?? <Trans>Select from an existing account</Trans>}\n    >\n      <SignInPicker\n        accounts={accounts}\n        onAccount={(a) => selectSub(a.sub)}\n        onOther={() => setShowSignInForm(true)}\n        onBack={onBack}\n        backLabel={backLabel}\n        onSignUp={onSignUp}\n      />\n    </LayoutTitlePage>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-up/sign-up-account-form.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { ReactNode, useEffect, useMemo, useRef, useState } from 'react'\nimport { Fieldset } from '../../../components/forms/fieldset.tsx'\nimport {\n  AsyncActionController,\n  FormCardAsync,\n  FormCardAsyncProps,\n} from '../../../components/forms/form-card-async.tsx'\nimport { InputEmailAddress } from '../../../components/forms/input-email-address.tsx'\nimport { InputNewPassword } from '../../../components/forms/input-new-password.tsx'\nimport { InputText } from '../../../components/forms/input-text.tsx'\nimport { TokenIcon } from '../../../components/utils/icons.tsx'\nimport { mergeRefs } from '../../../lib/ref.ts'\nimport { Override } from '../../../lib/util.ts'\n\nexport type SignUpAccountFormOutput = {\n  email: string\n  password: string\n  inviteCode?: string\n}\n\nexport type SignUpAccountFormProps = Override<\n  Omit<\n    FormCardAsyncProps,\n    'append' | 'onCancel' | 'onSubmit' | 'submitLabel' | 'cancelLabel'\n  >,\n  {\n    inviteCodeRequired?: boolean\n\n    credentials?: SignUpAccountFormOutput\n    onCredentials?: (credentials?: SignUpAccountFormOutput) => void\n\n    onNext: (signal: AbortSignal) => void | PromiseLike<void>\n    nextLabel?: ReactNode\n\n    onPrev?: () => void\n    prevLabel?: ReactNode\n  }\n>\n\nexport function SignUpAccountForm({\n  inviteCodeRequired = true,\n\n  credentials: creds,\n  onCredentials,\n\n  onNext,\n  nextLabel,\n\n  onPrev,\n  prevLabel,\n\n  // FormCardAsyncProps\n  children,\n  ref,\n  invalid,\n  ...props\n}: SignUpAccountFormProps) {\n  const { t } = useLingui()\n\n  const [email, setEmail] = useState(creds?.email)\n  const [password, setPassword] = useState(creds?.password)\n  const [inviteCode, setInviteCode] = useState(creds?.inviteCode)\n\n  const formRef = useRef<AsyncActionController>(null)\n  const resetForm = () => formRef.current?.reset()\n\n  const credentials = useMemo(\n    () =>\n      email && password && (!inviteCodeRequired || inviteCode)\n        ? {\n            email,\n            password,\n            inviteCode: inviteCodeRequired ? inviteCode : undefined,\n          }\n        : undefined,\n    [email, password, inviteCode, inviteCodeRequired],\n  )\n\n  useEffect(() => {\n    onCredentials?.(credentials)\n  }, [credentials, onCredentials])\n\n  return (\n    <FormCardAsync\n      {...props}\n      ref={mergeRefs([ref, formRef])}\n      invalid={invalid || !credentials}\n      onCancel={onPrev}\n      cancelLabel={prevLabel}\n      onSubmit={onNext}\n      submitLabel={nextLabel}\n      append={children}\n    >\n      {inviteCodeRequired && (\n        <Fieldset label={<Trans>Invite code</Trans>}>\n          <InputText\n            icon={<TokenIcon className=\"w-5\" />}\n            autoFocus\n            name=\"inviteCode\"\n            title={t`Invite code`}\n            placeholder={t`example-com-xxxxx-xxxxx`}\n            required\n            value={inviteCode || ''}\n            onChange={(event) => {\n              setInviteCode(event.target.value || undefined)\n              resetForm()\n            }}\n            enterKeyHint=\"next\"\n          />\n        </Fieldset>\n      )}\n\n      <Fieldset label={<Trans>Email</Trans>}>\n        <InputEmailAddress\n          autoFocus={!inviteCodeRequired}\n          name=\"email\"\n          enterKeyHint=\"next\"\n          required\n          defaultValue={email}\n          onEmail={(email) => {\n            setEmail(email)\n            resetForm()\n          }}\n        />\n      </Fieldset>\n\n      <Fieldset label={<Trans>Password</Trans>}>\n        <InputNewPassword\n          name=\"password\"\n          enterKeyHint=\"next\"\n          required\n          password={password}\n          onPassword={(value) => {\n            setPassword(value)\n            resetForm()\n          }}\n        />\n      </Fieldset>\n    </FormCardAsync>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-up/sign-up-disclaimer.tsx",
    "content": "import { Trans } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX } from 'react'\nimport type { LinkDefinition } from '@atproto/oauth-provider-api'\nimport { LinkAnchor } from '../../../components/utils/link-anchor.tsx'\nimport { Override } from '../../../lib/util.ts'\n\nexport type SignUpDisclaimerProps = Override<\n  Omit<JSX.IntrinsicElements['p'], 'children'>,\n  {\n    links?: readonly LinkDefinition[]\n  }\n>\n\nexport function SignUpDisclaimer({\n  links,\n\n  // p\n  className,\n  ...attrs\n}: SignUpDisclaimerProps) {\n  const tosLink = links?.find((l) => l.rel === 'terms-of-service')\n  const ppLink = links?.find((l) => l.rel === 'privacy-policy')\n\n  return (\n    <p\n      className={clsx('text-sm text-slate-500 dark:text-slate-400', className)}\n      {...attrs}\n    >\n      <Trans>\n        By creating an account you agree to the{' '}\n        {tosLink ? (\n          <LinkAnchor className=\"text-primary underline\" link={tosLink}>\n            <Trans>Terms of Service</Trans>\n          </LinkAnchor>\n        ) : (\n          <Trans>Terms of Service</Trans>\n        )}\n        {' and the '}\n        {ppLink ? (\n          <LinkAnchor className=\"text-primary underline\" link={ppLink}>\n            <Trans>Privacy Policy</Trans>\n          </LinkAnchor>\n        ) : (\n          <Trans>Privacy Policy</Trans>\n        )}{' '}\n        of this service.\n      </Trans>\n    </p>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-up/sign-up-handle-form.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { clsx } from 'clsx'\nimport { JSX, ReactNode, useCallback, useEffect, useRef, useState } from 'react'\nimport {\n  AsyncActionController,\n  FormCardAsync,\n  FormCardAsyncProps,\n} from '../../../components/forms/form-card-async.tsx'\nimport { InputText } from '../../../components/forms/input-text.tsx'\nimport { Admonition } from '../../../components/utils/admonition.tsx'\nimport {\n  AtSymbolIcon,\n  CheckMarkIcon,\n  XMarkIcon,\n} from '../../../components/utils/icons.tsx'\nimport { mergeRefs } from '../../../lib/ref.ts'\nimport { Override } from '../../../lib/util.ts'\n\n/**\n * Spec limit is 63, but in practice, we've limited it to 18 in our implementations.\n *\n * @see {@link https://atproto.com/specs/handle | ATProto Handle Spec}\n */\nconst MAX_LENGTH = 18\n\n/**\n * Spec limit is 1, but in practice, we've targeted at least 3 characters in handles.\n *\n * @see {@link https://atproto.com/specs/handle | ATProto Handle Spec}\n */\nconst MIN_LENGTH = 3\n\n/**\n * Spec limit is 253, but in practice, we've targeted 30 characters in handles.\n *\n * @see {@link https://atproto.com/specs/handle | ATProto Handle Spec}\n */\nconst MAX_FULL_LENGTH = 30\n\ntype ValidDomain = `.${string}`\nconst isValidDomain = (domain: string): domain is ValidDomain =>\n  // Ignore domains that are so long that they would make the handle smaller\n  // than MIN_LENGTH characters\n  MIN_LENGTH + domain.length <= MAX_FULL_LENGTH &&\n  // Basic validation here\n  domain.startsWith('.') &&\n  !domain.endsWith('.')\n\nfunction useSegmentValidator(domain: ValidDomain) {\n  const minLen = MIN_LENGTH\n  const maxLen = Math.min(MAX_LENGTH, MAX_FULL_LENGTH - domain.length)\n\n  const validateSegment = useCallback(\n    (segment: string) => {\n      const validLength = segment.length >= minLen && segment.length <= maxLen\n      const validCharset = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/g.test(segment)\n\n      return { validLength, validCharset, valid: validLength && validCharset }\n    },\n    [maxLen, minLen],\n  )\n\n  return {\n    minLength: minLen,\n    maxLength: maxLen,\n    validateSegment,\n  }\n}\n\nexport type SignUpHandleFormProps = Override<\n  Omit<\n    FormCardAsyncProps,\n    'append' | 'onCancel' | 'cancelLabel' | 'onSubmit' | 'submitLabel'\n  >,\n  {\n    domains: string[]\n\n    onNext: (signal: AbortSignal) => void | PromiseLike<void>\n    nextLabel?: ReactNode\n\n    onPrev?: () => void\n    prevLabel?: ReactNode\n\n    handle?: string\n    onHandle?: (handle: string | undefined) => void\n  }\n>\n\nexport function SignUpHandleForm({\n  domains: availableDomains,\n\n  onNext,\n  nextLabel,\n\n  onPrev,\n  prevLabel,\n\n  handle: handleInit,\n  onHandle,\n\n  // FormCardProps\n  invalid,\n  children,\n  ref,\n  ...props\n}: SignUpHandleFormProps) {\n  const { t } = useLingui()\n  const domains = availableDomains.filter(isValidDomain)\n\n  const formRef = useRef<AsyncActionController>(null)\n\n  const [domainIdx, setDomainIdx] = useState(() => {\n    const idx = domains.findIndex((d) => handleInit?.endsWith(d))\n    return idx === -1 ? 0 : idx\n  })\n  const [segment, setSegment] = useState(() => handleInit?.split('.')[0] || '')\n\n  // Automatically update the domain index when the list length changes\n  useEffect(() => {\n    setDomainIdx((v) => Math.min(v, domains.length - 1))\n  }, [domains.length])\n\n  const domain: ValidDomain | null = domains[domainIdx] || domains[0] || null\n\n  const { minLength, maxLength, validateSegment } = useSegmentValidator(domain)\n\n  const validity = validateSegment(segment)\n  const handle = domain && validity.valid ? `${segment}${domain}` : undefined\n  useEffect(() => {\n    // Whenever the user changes the handle, abort any pending form action\n    formRef.current?.reset()\n    onHandle?.(handle)\n  }, [onHandle, handle])\n\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const preview = `@${segment}${domain}`\n\n  return (\n    <FormCardAsync\n      {...props}\n      ref={mergeRefs([ref, formRef])}\n      onCancel={onPrev}\n      cancelLabel={prevLabel}\n      onSubmit={onNext}\n      submitLabel={nextLabel}\n      invalid={invalid || !handle}\n      append={children}\n    >\n      <div>\n        <ValidationMessage hasValue={!!segment} valid={validity.validLength}>\n          <Trans>\n            Between {minLength} and {maxLength} characters\n          </Trans>\n        </ValidationMessage>\n        <ValidationMessage hasValue={!!segment} valid={validity.validCharset}>\n          <Trans>Only letters, numbers, and hyphens</Trans>\n        </ValidationMessage>\n      </div>\n\n      <InputText\n        ref={inputRef}\n        icon={<AtSymbolIcon className=\"w-5\" />}\n        name=\"handle\"\n        type=\"text\"\n        title={t`Type your desired username`}\n        pattern=\"[a-z0-9][a-z0-9\\-]+[a-z0-9]\"\n        minLength={minLength}\n        maxLength={maxLength}\n        autoCapitalize=\"none\"\n        autoCorrect=\"off\"\n        autoComplete=\"off\"\n        dir=\"auto\"\n        enterKeyHint=\"done\"\n        autoFocus\n        required\n        value={segment}\n        onChange={(event) => {\n          const segment = event.target.value.toLowerCase()\n\n          // Ensure the input is always lowercase\n          const selectionStart = event.target.selectionStart\n          const selectionEnd = event.target.selectionEnd\n          event.target.value = segment\n          event.target.setSelectionRange(selectionStart, selectionEnd)\n\n          setSegment(segment)\n        }}\n        append={\n          // @TODO refactor this to a separate component\n          domains.length > 1 && (\n            <select\n              onClick={(event) => event.stopPropagation()}\n              onMouseDown={(event) => event.stopPropagation()}\n              value={domainIdx}\n              aria-label={t`Select domain`}\n              onChange={(event) => {\n                setDomainIdx(Number(event.target.value))\n                inputRef.current?.focus()\n              }}\n              className={clsx(\n                'block w-full',\n                'text-sm',\n\n                'accent-primary',\n                'cursor-pointer',\n                // Background\n                'bg-gray-100 dark:bg-gray-800',\n                'hover:bg-gray-200 dark:hover:bg-gray-700',\n                // Border\n                'transition duration-300 ease-in-out',\n                'outline-none',\n                'focus:ring-primary focus:ring-2 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-offset-black',\n                // Font\n                'text-slate-600 dark:text-slate-300',\n                // Layout\n                'rounded-lg',\n                'p-2 pr-1',\n              )}\n            >\n              {domains.map((domain, idx) => (\n                <option key={domain} value={idx}>\n                  {domain}\n                </option>\n              ))}\n            </select>\n          )\n        }\n        bellow={\n          <Trans>\n            Your full username will be:{' '}\n            {segment.length ? (\n              <strong className=\"text-gray-800 dark:text-gray-200\">\n                {preview}\n              </strong>\n            ) : (\n              <span\n                aria-hidden\n                className=\"w-24 rounded-md bg-gray-300 p-2 dark:bg-slate-600\"\n              />\n            )}\n          </Trans>\n        }\n      />\n\n      <Admonition type=\"status\">\n        <Trans>\n          You can change this username to any domain name you control after your\n          account is set up.\n        </Trans>\n      </Admonition>\n    </FormCardAsync>\n  )\n}\n\ntype ValidationMessageProps = JSX.IntrinsicElements['div'] & {\n  valid: boolean\n  hasValue: boolean\n}\n\nfunction ValidationMessage({\n  valid,\n  hasValue,\n\n  // div\n  children,\n  className,\n  ...props\n}: ValidationMessageProps) {\n  const { t } = useLingui()\n  return (\n    <div\n      {...props}\n      className={clsx('flex flex-row items-center gap-2', className)}\n    >\n      {hasValue ? (\n        <>\n          {valid ? (\n            <CheckMarkIcon\n              className=\"text-success inline-block h-4 w-4\"\n              title={t`Valid`}\n            />\n          ) : (\n            <XMarkIcon\n              className=\"text-error inline-block h-4 w-4\"\n              title={t`Invalid`}\n            />\n          )}\n        </>\n      ) : (\n        <div aria-hidden className=\"flex h-4 w-4 items-center justify-center\">\n          <div className=\"h-2 w-2 rounded-full bg-gray-300 dark:bg-slate-600\" />\n        </div>\n      )}\n      <div className=\"text-sm\">{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx",
    "content": "import HCaptcha from '@hcaptcha/react-hcaptcha'\nimport { ForwardedRef, ReactNode, useCallback, useRef, useState } from 'react'\nimport {\n  FormCardAsync,\n  FormCardAsyncProps,\n} from '../../../components/forms/form-card-async.tsx'\nimport { useBrowserColorScheme } from '../../../hooks/use-browser-color-scheme.ts'\nimport { mergeRefs } from '../../../lib/ref.ts'\nimport { Override } from '../../../lib/util.ts'\n\nexport type SignUpHcaptchaFormProps = Override<\n  Omit<\n    FormCardAsyncProps,\n    'append' | 'onSubmit' | 'submitLabel' | 'onCancel' | 'cancelLabel'\n  >,\n  {\n    siteKey: string\n\n    token?: string\n    onToken: (token: string, ekey: string) => void\n\n    prevLabel?: ReactNode\n    onPrev?: () => void\n\n    nextLabel?: ReactNode\n    onNext: (signal: AbortSignal) => void | PromiseLike<void>\n\n    ref?: ForwardedRef<HCaptcha>\n  }\n>\n\nexport function SignUpHcaptchaForm({\n  siteKey,\n\n  token: tokenInit,\n  onToken,\n\n  prevLabel,\n  onPrev,\n\n  nextLabel,\n  onNext,\n\n  ref,\n\n  // FormCardProps\n  invalid,\n  children,\n  ...props\n}: SignUpHcaptchaFormProps) {\n  const captchaRef = useRef<HCaptcha>(null)\n  const theme = useBrowserColorScheme()\n  const [token, setToken] = useState<string | undefined>(tokenInit)\n\n  const onLoad = useCallback(() => {\n    // this reaches out to the hCaptcha JS API and runs the\n    // execute function on it. you can use other functions as\n    // documented here:\n    // https://docs.hcaptcha.com/configuration#jsapi\n    captchaRef.current?.execute()\n  }, [])\n\n  const onVerify = useCallback(\n    (token: string, ekey: string) => {\n      setToken(token)\n      onToken(token, ekey)\n    },\n    [onToken],\n  )\n\n  const doSubmit = useCallback(\n    (signal: AbortSignal) => {\n      if (token) return onNext(signal)\n      else if (captchaRef.current) captchaRef.current.execute()\n      else throw new Error('Unable to load hCaptcha')\n    },\n    [token, onNext],\n  )\n\n  return (\n    <FormCardAsync\n      {...props}\n      cancelLabel={prevLabel}\n      onCancel={onPrev}\n      submitLabel={nextLabel}\n      onSubmit={doSubmit}\n      append={children}\n      invalid={invalid || !token}\n    >\n      <HCaptcha\n        theme={theme}\n        sitekey={siteKey}\n        onLoad={onLoad}\n        onVerify={onVerify}\n        ref={mergeRefs([ref, captchaRef])}\n      />\n    </FormCardAsync>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/sign-up/sign-up-view.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { ReactNode, useCallback, useState } from 'react'\nimport type { CustomizationData } from '@atproto/oauth-provider-api'\nimport { WizardCard } from '../../../components/forms/wizard-card.tsx'\nimport {\n  LayoutTitlePage,\n  LayoutTitlePageProps,\n} from '../../../components/layouts/layout-title-page.tsx'\nimport { HelpCard } from '../../../components/utils/help-card.tsx'\nimport { Override } from '../../../lib/util.ts'\nimport {\n  SignUpAccountForm,\n  SignUpAccountFormOutput,\n} from './sign-up-account-form.tsx'\nimport { SignUpDisclaimer } from './sign-up-disclaimer.tsx'\nimport { SignUpHandleForm } from './sign-up-handle-form.tsx'\nimport { SignUpHcaptchaForm } from './sign-up-hcaptcha-form.tsx'\n\nexport type SignUpViewProps = Override<\n  LayoutTitlePageProps,\n  {\n    customizationData?: CustomizationData\n\n    onBack?: () => void\n    backLabel?: ReactNode\n    onValidateNewHandle: (\n      data: { handle: string },\n      signal?: AbortSignal,\n    ) => void | PromiseLike<void>\n    onDone: (\n      data: SignUpAccountFormOutput & {\n        handle: string\n        hcaptchaToken?: string\n      },\n      signal?: AbortSignal,\n    ) => void | PromiseLike<void>\n  }\n>\n\nexport function SignUpView({\n  customizationData: {\n    availableUserDomains = [],\n    hcaptchaSiteKey = undefined,\n    inviteCodeRequired = true,\n    links,\n  } = {},\n\n  onValidateNewHandle,\n  onDone,\n  onBack,\n  backLabel,\n\n  // LayoutTitlePage\n  title,\n  subtitle,\n  ...props\n}: SignUpViewProps) {\n  const { t } = useLingui()\n  const [credentials, setCredentials] = useState<\n    undefined | SignUpAccountFormOutput\n  >(undefined)\n  const [handle, setHandle] = useState<undefined | string>(undefined)\n  const [hcaptcha, setHcaptcha] = useState<undefined | string>(undefined)\n\n  /**\n   * \"false\" indicates that the hcaptcha token is invalid (required but not provided)\n   */\n  const hcaptchaToken = hcaptchaSiteKey == null ? undefined : hcaptcha || false\n\n  const doDone = useCallback(\n    (signal: AbortSignal) => {\n      if (credentials && handle && hcaptchaToken !== false) {\n        return onDone({ ...credentials, handle, hcaptchaToken }, signal)\n      }\n    },\n    [credentials, handle, hcaptchaToken, onDone],\n  )\n\n  return (\n    <LayoutTitlePage\n      {...props}\n      title={title ?? t`Create Account`}\n      subtitle={\n        subtitle ?? <Trans>We're so excited to have you join us!</Trans>\n      }\n    >\n      <WizardCard\n        doneLabel={<Trans>Sign up</Trans>}\n        onBack={onBack}\n        backLabel={backLabel}\n        onDone={doDone}\n        steps={[\n          // We use the handle input first since the \"onValidateNewHandle\" check\n          // will make it less likely that the actual signup call will fail, and\n          // will result in a better user experience, especially if there is an\n          // issue with the email address (e.g. already in use).\n          {\n            invalid: !handle,\n            titleRender: () => <Trans>Choose a username</Trans>,\n            contentRender: ({ prev, prevLabel, next, nextLabel, invalid }) => (\n              <SignUpHandleForm\n                className=\"grow\"\n                invalid={invalid}\n                domains={availableUserDomains}\n                handle={handle}\n                onHandle={setHandle}\n                prevLabel={prevLabel}\n                onPrev={prev}\n                nextLabel={nextLabel}\n                onNext={async (signal) => {\n                  if (handle) await onValidateNewHandle({ handle }, signal)\n                  if (!signal.aborted) return next(signal)\n                }}\n              >\n                <SignUpDisclaimer links={links} />\n              </SignUpHandleForm>\n            ),\n          },\n          {\n            invalid: !credentials,\n            titleRender: () => <Trans>Your account</Trans>,\n            contentRender: ({ prev, prevLabel, next, nextLabel, invalid }) => (\n              <SignUpAccountForm\n                className=\"grow\"\n                invalid={invalid}\n                prevLabel={prevLabel}\n                onPrev={prev}\n                nextLabel={nextLabel}\n                onNext={next}\n                inviteCodeRequired={inviteCodeRequired}\n                credentials={credentials}\n                onCredentials={setCredentials}\n              >\n                <SignUpDisclaimer links={links} />\n              </SignUpAccountForm>\n            ),\n          },\n          hcaptchaSiteKey != null && {\n            invalid: hcaptchaToken === false,\n            titleRender: () => <Trans>Verify you are human</Trans>,\n            contentRender: ({ prev, prevLabel, next, nextLabel, invalid }) => (\n              <SignUpHcaptchaForm\n                className=\"grow\"\n                invalid={invalid}\n                siteKey={hcaptchaSiteKey}\n                token={hcaptcha}\n                onToken={setHcaptcha}\n                prevLabel={prevLabel}\n                onPrev={prev}\n                nextLabel={nextLabel}\n                onNext={next}\n              >\n                <SignUpDisclaimer links={links} />\n              </SignUpHcaptchaForm>\n            ),\n          },\n        ]}\n      />\n\n      <HelpCard className=\"mt-4\" links={links} />\n    </LayoutTitlePage>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/authorize/welcome/welcome-view.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { Button } from '../../../components/forms/button.tsx'\nimport {\n  LayoutWelcome,\n  LayoutWelcomeProps,\n} from '../../../components/layouts/layout-welcome.tsx'\nimport { Override } from '../../../lib/util.ts'\n\nexport type WelcomeViewParams = Override<\n  LayoutWelcomeProps,\n  {\n    onSignIn?: () => void\n    onSignUp?: () => void\n    onCancel?: () => void\n  }\n>\n\nexport function WelcomeView({\n  onSignUp,\n  onSignIn,\n  onCancel,\n\n  // LayoutWelcome\n  ...props\n}: WelcomeViewParams) {\n  const { t } = useLingui()\n  return (\n    <LayoutWelcome {...props} title={props.title ?? t`Authenticate`}>\n      {onSignUp && (\n        <Button\n          className={'m-1 w-60 min-w-min max-w-full'}\n          color={onSignIn ? 'primary' : undefined}\n          onClick={onSignUp}\n        >\n          <Trans>Create a new account</Trans>\n        </Button>\n      )}\n\n      {onSignIn && (\n        <Button\n          className={'m-1 w-60 min-w-min max-w-full'}\n          color={onSignUp ? undefined : 'primary'}\n          onClick={onSignIn}\n        >\n          <Trans context=\"verb\">Sign in</Trans>\n        </Button>\n      )}\n\n      {onCancel && (\n        <Button className=\"m-1 w-60 min-w-min max-w-full\" onClick={onCancel}>\n          <Trans>Cancel</Trans>\n        </Button>\n      )}\n    </LayoutWelcome>\n  )\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/error/cookie-error-view.tsx",
    "content": "import { Trans, useLingui } from '@lingui/react/macro'\nimport { memo, useMemo } from 'react'\nimport { Button } from '#/components/forms/button.tsx'\nimport { FormCard } from '#/components/forms/form-card.tsx'\nimport { LayoutTitlePage } from '#/components/layouts/layout-title-page.tsx'\nimport { Admonition } from '#/components/utils/admonition.tsx'\nimport { LayoutWelcomeProps } from '../../components/layouts/layout-welcome.tsx'\nimport { Override } from '../../lib/util.ts'\n\nexport type CookieErrorViewProps = Override<\n  LayoutWelcomeProps,\n  {\n    continueUrl: string\n  }\n>\n\nexport const CookieErrorView = memo(function CookieErrorView({\n  continueUrl,\n\n  // LayoutWelcome\n  title,\n  children,\n  ...props\n}: CookieErrorViewProps) {\n  const { t } = useLingui()\n\n  const url = useMemo(() => new URL(continueUrl), [continueUrl])\n\n  return (\n    <LayoutTitlePage {...props} title={title ?? t`Cookie Error`}>\n      <FormCard\n        action={url.origin}\n        method=\"GET\"\n        actions={<Button type=\"submit\" color=\"primary\">{t`Continue`}</Button>}\n      >\n        {Array.from(url.searchParams).map(([key, value]) => (\n          <input key={key} type=\"hidden\" name={key} value={value} />\n        ))}\n        <Admonition>\n          <Trans>\n            It seems that your browser is not accepting cookies. Press\n            \"Continue\" to try again. If the error persists, please ensure that\n            your privacy settings allow cookies for the \"{url.hostname}\"\n            website.\n          </Trans>\n        </Admonition>\n      </FormCard>\n    </LayoutTitlePage>\n  )\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/src/views/error/error-view.tsx",
    "content": "import { useLingui } from '@lingui/react/macro'\nimport { memo } from 'react'\nimport {\n  LayoutWelcome,\n  LayoutWelcomeProps,\n} from '../../components/layouts/layout-welcome.tsx'\nimport { ErrorCard } from '../../components/utils/error-card.tsx'\nimport { Override } from '../../lib/util.ts'\n\nexport type ErrorViewProps = Override<\n  LayoutWelcomeProps,\n  {\n    error: unknown\n  }\n>\n\nexport const ErrorView = memo(function ErrorView({\n  error,\n\n  // LayoutWelcome\n  title,\n  children,\n  ...props\n}: ErrorViewProps) {\n  const { t } = useLingui()\n\n  return (\n    <LayoutWelcome {...props} title={title ?? t`Error`}>\n      <ErrorCard error={error}>{children}</ErrorCard>\n    </LayoutWelcome>\n  )\n})\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.src.json\" },\n    { \"path\": \"./tsconfig.tools.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/tsconfig.src.json",
    "content": "{\n  \"extends\": [\n    \"../../../tsconfig/browser.json\",\n    \"../../../tsconfig/bundler.json\"\n  ],\n  \"compilerOptions\": {\n    \"rootDir\": \"src\",\n    \"paths\": {\n      \"#/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/tsconfig.tools.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/nodenext.json\"],\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"noEmit\": true\n  },\n  \"include\": [\"./*.js\", \"./*.cjs\", \"./*.mjs\", \"./*.ts\", \"./*.cts\", \"./*.mts\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-provider-ui/vite.config.mjs",
    "content": "import { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { lingui } from '@lingui/vite-plugin'\nimport tailwindcss from '@tailwindcss/vite'\nimport react from '@vitejs/plugin-react-swc'\nimport { defineConfig } from 'vite'\nimport { bundleManifest } from '@atproto-labs/rollup-plugin-bundle-manifest'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      '#': resolve(__dirname, './src'),\n    },\n  },\n  plugins: [\n    react({\n      plugins: [['@lingui/swc-plugin', {}]],\n    }),\n    lingui(),\n    tailwindcss(),\n  ],\n  build: {\n    emptyOutDir: false,\n    outDir: './dist',\n    sourcemap: true,\n    rollupOptions: {\n      input: [\n        './src/authorization-page.tsx',\n        './src/cookie-error-page.tsx',\n        './src/error-page.tsx',\n      ],\n      output: {\n        manualChunks: undefined,\n        format: 'module',\n        entryFileNames: '[name]-[hash].js',\n        chunkFileNames: '[name]-[hash].js',\n        assetFileNames: '[name]-[hash][extname]',\n      },\n      plugins: [bundleManifest()],\n    },\n    commonjsOptions: {\n      include: [\n        /node_modules/,\n        /did/,\n        /oauth-scopes/,\n        /oauth-provider-api/,\n        /syntax/,\n      ],\n    },\n    // this\n    // @NOTE the \"env\" arg (when defineConfig is used with a function) does not\n    // allow to detect watch mode. We do want to set the \"buildDelay\" though to\n    // avoid i18n compilation to trigger too many build (and restart of\n    // dependent services).\n    watch: process.argv.includes('--watch')\n      ? { buildDelay: 500, clearScreen: false }\n      : undefined,\n  },\n  optimizeDeps: {\n    // Needed because this is a monorepo and it exposes CommonJS\n    include: [\n      '@atproto/oauth-provider-api',\n      '@atproto/did',\n      '@atproto/oauth-scopes',\n      '@atproto/syntax',\n    ],\n  },\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/CHANGELOG.md",
    "content": "# @atproto/oauth-scopes\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n\n## 0.3.0\n\n### Minor Changes\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `@atproto/lexicon` with `@atproto/lex-document`\n\n- [#4353](https://github.com/bluesky-social/atproto/pull/4353) [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use arrays to represent \"account\" permission's `action` attribute, allowing multiple actions to be specified for that resource.\n\n### Patch Changes\n\n- [#4382](https://github.com/bluesky-social/atproto/pull/4382) [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `toScopes()` utility on `IncludeScope`\n\n- [#4382](https://github.com/bluesky-social/atproto/pull/4382) [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove ability to define `blob` permission in permission sets\n\n- [#4383](https://github.com/bluesky-social/atproto/pull/4383) [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `@atproto/lexicon-resolver` with `@atproto/lex-resolver`\n\n- [#4353](https://github.com/bluesky-social/atproto/pull/4353) [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow lexicon permission data to be readonly\n\n- [#4382](https://github.com/bluesky-social/atproto/pull/4382) [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disallow `rpc` permissions with specific `aud` in permission-sets\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n  - @atproto/syntax@0.4.2\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/did@0.2.2\n  - @atproto/lexicon@0.5.2\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/did@0.2.1\n\n## 0.2.0\n\n### Minor Changes\n\n- [#4149](https://github.com/bluesky-social/atproto/pull/4149) [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Method `authenticateRequest` now returns `SignedTokenPayload`\n\n### Patch Changes\n\n- [#4149](https://github.com/bluesky-social/atproto/pull/4149) [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add scope normalization utility\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.5.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rework scope parsing utilities to work with Lexicon defined permissions\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use strict NSID validation. repo & rpc scopes with invalid NSIDs will be rejected.\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `PermissionSet` to `ScopePermissions`\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Renaming of \"resource\" concept to better reflect the fact that not all oauth scope values are about resources\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce proper formatting of audience (atproto supported did + fragment part)\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ignore empty string when building a `ScopePermissions`\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export constants and type assertion utilities\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/did@0.2.0\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4097](https://github.com/bluesky-social/atproto/pull/4097) [`c274bd1b3`](https://github.com/bluesky-social/atproto/commit/c274bd1b38813abd5b287f1c94dca1fd62854918) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix permission bug with `transition:email` scope\n\n## 0.0.1\n\n### Patch Changes\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Initial implementation of library used to parse ATProto OAuth scopes\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-scopes\",\n  \"version\": \"0.3.2\",\n  \"license\": \"MIT\",\n  \"description\": \"A library for manipulating and validating ATproto OAuth scopes in TypeScript.\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"scopes\",\n    \"permissions\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/auth-scopes\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"jest\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/atproto-oauth-scope.ts",
    "content": "import { ScopeStringFor, isScopeStringFor } from './lib/syntax.js'\nimport { isNonNullable } from './lib/util.js'\nimport { AccountPermission } from './scopes/account-permission.js'\nimport { BlobPermission } from './scopes/blob-permission.js'\nimport { IdentityPermission } from './scopes/identity-permission.js'\nimport { IncludeScope } from './scopes/include-scope.js'\nimport { RepoPermission } from './scopes/repo-permission.js'\nimport { RpcPermission } from './scopes/rpc-permission.js'\n\nexport { type ScopeStringFor, isScopeStringFor }\n\nexport const STATIC_SCOPE_VALUES = Object.freeze([\n  'atproto',\n  'transition:email',\n  'transition:generic',\n  'transition:chat.bsky',\n] as const)\n\nexport type StaticScopeValue = (typeof STATIC_SCOPE_VALUES)[number]\nexport function isStaticScopeValue(value: string): value is StaticScopeValue {\n  return (STATIC_SCOPE_VALUES as readonly string[]).includes(value)\n}\n\nexport type AtprotoOauthScope =\n  | StaticScopeValue\n  | ScopeStringFor<'account'>\n  | ScopeStringFor<'blob'>\n  | ScopeStringFor<'identity'>\n  | ScopeStringFor<'include'>\n  | ScopeStringFor<'repo'>\n  | ScopeStringFor<'rpc'>\n\n/**\n * @note This function does not only verify the scope string format (with\n * {@link isScopeStringFor}), but also checks if the provided parameters are\n * valid according to the respective scope syntax definition. This allows\n * excluding scopes that cannot be fully interpreted by the current version of\n * the code.\n */\nexport function isAtprotoOauthScope(value: string): value is AtprotoOauthScope {\n  return (\n    isStaticScopeValue(value) ||\n    AccountPermission.fromString(value) != null ||\n    BlobPermission.fromString(value) != null ||\n    IdentityPermission.fromString(value) != null ||\n    IncludeScope.fromString(value) != null ||\n    RepoPermission.fromString(value) != null ||\n    RpcPermission.fromString(value) != null\n  )\n}\n\nexport function normalizeAtprotoOauthScope(scope: string) {\n  return scope\n    .split(' ')\n    .map(normalizeAtprotoOauthScopeValue)\n    .filter(isNonNullable)\n    .sort()\n    .join(' ')\n}\n\nexport function normalizeAtprotoOauthScopeValue(\n  value: string,\n): AtprotoOauthScope | null {\n  if (isStaticScopeValue(value)) return value\n\n  for (const Scope of [\n    AccountPermission,\n    BlobPermission,\n    IdentityPermission,\n    IncludeScope,\n    RepoPermission,\n    RpcPermission,\n  ]) {\n    const parsed = Scope.fromString(value)\n    if (parsed) return parsed.toString()\n  }\n\n  return null\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/index.ts",
    "content": "export * from './atproto-oauth-scope.js'\n\nexport * from './scope-missing-error.js'\nexport * from './scope-permissions-transition.js'\nexport * from './scope-permissions.js'\nexport * from './scopes-set.js'\n\nexport * from './scopes/account-permission.js'\nexport * from './scopes/blob-permission.js'\nexport * from './scopes/identity-permission.js'\nexport * from './scopes/include-scope.js'\nexport * from './scopes/repo-permission.js'\nexport * from './scopes/rpc-permission.js'\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/lexicon.ts",
    "content": "import { ParamValue } from './syntax.js'\n\n// @NOTE Not types from from '@atproto/lex-document' because we want a readonly\n// version here to prevent accidental mutation.\n\nexport type LexiconPermission<P extends string = string> = {\n  readonly type: 'permission'\n  readonly resource: P\n  readonly [x: string]: undefined | ParamValue | readonly ParamValue[]\n}\n\ntype LangMap = { readonly [Lang in string]?: string }\n\nexport type LexiconPermissionSet = {\n  readonly type: 'permission-set'\n  readonly permissions: readonly LexiconPermission<string>[]\n  readonly title?: string\n  readonly 'title:lang'?: LangMap\n  readonly detail?: string\n  readonly 'detail:lang'?: LangMap\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/mime.test.ts",
    "content": "import { isAccept, isMime, matchesAccept, matchesAnyAccept } from './mime.js'\n\ndescribe('isAccept', () => {\n  it('should return true for valid MIME types', () => {\n    expect(isAccept('image/png')).toBe(true)\n    expect(isAccept('application/json')).toBe(true)\n    expect(isAccept('text/html')).toBe(true)\n    expect(isAccept('image/*')).toBe(true)\n    expect(isAccept('*/*')).toBe(true)\n  })\n\n  it('should return false for invalid MIME types', () => {\n    expect(isAccept('image//png')).toBe(false)\n    expect(isAccept('/png')).toBe(false)\n    expect(isAccept('image/')).toBe(false)\n    expect(isAccept('image/**')).toBe(false)\n    expect(isAccept('*/png')).toBe(false)\n    expect(isAccept('*')).toBe(false)\n    expect(isAccept('image/png/extra')).toBe(false)\n  })\n})\n\ndescribe('isMime', () => {\n  it('should return true for valid MIME types', () => {\n    expect(isMime('image/png')).toBe(true)\n    expect(isMime('application/json')).toBe(true)\n  })\n\n  it('should return false for invalid MIME types', () => {\n    expect(isMime('image/*')).toBe(false)\n    expect(isMime('*/*')).toBe(false)\n    expect(isMime('image/png/extra')).toBe(false)\n    expect(isMime('*/mime')).toBe(false)\n    expect(isMime('/png')).toBe(false)\n    expect(isMime('image/')).toBe(false)\n    expect(isMime('image')).toBe(false)\n    expect(isMime('image/ png')).toBe(false)\n    expect(isMime('image//png')).toBe(false)\n  })\n})\n\ndescribe('matchesAccept', () => {\n  it('should match exact MIME type', () => {\n    expect(matchesAccept('image/png', 'image/png')).toBe(true)\n  })\n\n  it('should match wildcard MIME type', () => {\n    expect(matchesAccept('image/*', 'image/jpeg')).toBe(true)\n  })\n\n  it('should match subtype wildcard MIME type', () => {\n    expect(matchesAccept('image/*', 'image/gif')).toBe(true)\n  })\n\n  it('should not match different MIME type', () => {\n    expect(matchesAccept('image/png', 'image/jpeg')).toBe(false)\n  })\n\n  it('should not match different wildcard MIME type', () => {\n    expect(matchesAccept('image/*', 'text/html')).toBe(false)\n  })\n\n  it('should match any MIME type with *', () => {\n    expect(matchesAccept('*/*', 'application/json')).toBe(true)\n  })\n\n  it('should not match invalid MIME type', () => {\n    expect(matchesAccept('image/png', '*/mime')).toBe(false)\n    expect(matchesAccept('image/png', 'image')).toBe(false)\n    expect(matchesAccept('image/*', 'image//png')).toBe(false)\n    expect(matchesAccept('image/*', 'image/ png')).toBe(false)\n    expect(matchesAccept('*/*', 'image/')).toBe(false)\n    expect(matchesAccept('*/*', '/mime')).toBe(false)\n  })\n})\n\ndescribe('matchesAnyAccept', () => {\n  it('should return true if any accept matches', () => {\n    const accepts = ['image/png', 'application/json'] as const\n    expect(matchesAnyAccept(accepts, 'image/png')).toBe(true)\n    expect(matchesAnyAccept(accepts, 'application/json')).toBe(true)\n  })\n\n  it('should return false if no accept matches', () => {\n    const accepts = ['image/png', 'application/json'] as const\n    expect(matchesAnyAccept(accepts, 'text/html')).toBe(false)\n  })\n\n  it('should handle empty accepts array', () => {\n    expect(matchesAnyAccept([], 'image/png')).toBe(false)\n  })\n\n  it('should handle single accept', () => {\n    const accepts = ['image/*'] as const\n    expect(matchesAnyAccept(accepts, 'image/jpeg')).toBe(true)\n    expect(matchesAnyAccept(accepts, 'text/html')).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/mime.ts",
    "content": "// @TODO Refactor in shared location for use with other @atproto packages\n\nfunction isStringSlashString(value: string): value is `${string}/${string}` {\n  const slashIndex = value.indexOf('/')\n\n  if (slashIndex === -1) return false // Missing slash\n  if (slashIndex === 0) return false // No leading part before the slash\n  if (slashIndex === value.length - 1) return false // No trailing part after the slash\n  if (value.includes('/', slashIndex + 1)) return false // More than one slash\n  if (value.includes(' ')) return false // Spaces are not allowed\n\n  return true\n}\n\nexport type Mime = `${string}/${string}`\n\nexport function isMime(value: string): value is Mime {\n  return isStringSlashString(value) && !value.includes('*')\n}\n\nexport type Accept = '*/*' | `${string}/*` | Mime\n\nexport function isAccept(value: unknown): value is Accept {\n  if (typeof value !== 'string') return false\n  if (value === '*/*') return true // Fast path for the most common case\n  if (!isStringSlashString(value)) return false\n  return !value.includes('*') || value.endsWith('/*')\n}\n\n/**\n * @note \"unsafe\" in that it does not check if either {@link accept} or\n * {@link mime} are actually valid values (and could, therefore, lead to false\n * positives if forged values are used).\n */\nfunction matchesAcceptUnsafe(accept: Accept, mime: Mime): boolean {\n  if (accept === '*/*') {\n    return true\n  }\n  if (accept.endsWith('/*')) {\n    return mime.startsWith(accept.slice(0, -1))\n  }\n  return accept === mime\n}\n\nexport function matchesAccept(accept: Accept, mime: string): boolean {\n  return isMime(mime) && matchesAcceptUnsafe(accept, mime)\n}\n\n/**\n * @note \"unsafe\" in that it does not check if either {@link accept} or\n * {@link mime} are actually valid values (and could, therefore, lead to false\n * positives if forged values are used).\n */\nfunction matchesAnyAcceptUnsafe(\n  acceptable: Iterable<Accept>,\n  mime: Mime,\n): boolean {\n  for (const accept of acceptable) {\n    if (matchesAcceptUnsafe(accept, mime)) {\n      return true\n    }\n  }\n  return false\n}\n\nexport function matchesAnyAccept(\n  acceptable: Iterable<Accept>,\n  mime: string,\n): boolean {\n  return isMime(mime) && matchesAnyAcceptUnsafe(acceptable, mime)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/nsid.ts",
    "content": "import { isValidNsid } from '@atproto/syntax'\n\nexport type Nsid = `${string}.${string}.${string}`\nexport const isNsid = (v: unknown): v is Nsid =>\n  typeof v === 'string' && isValidNsid(v)\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/parser.ts",
    "content": "import { ScopeStringSyntax } from './syntax-string.js'\nimport { NeRoArray, ParamValue, ScopeSyntax } from './syntax.js'\n\ntype InferParamPredicate<T extends (value: ParamValue) => boolean> =\n  T extends ((value: ParamValue) => value is infer U extends ParamValue)\n    ? U\n    : ParamValue\n\ntype ParamsSchema = Record<\n  string,\n  | {\n      multiple: false\n      required: boolean\n      default?: ParamValue\n      normalize?: (value: ParamValue) => ParamValue\n      validate: (value: ParamValue) => boolean\n    }\n  | {\n      multiple: true\n      required: boolean\n      default?: NeRoArray<ParamValue>\n      normalize?: (value: NeRoArray<ParamValue>) => NeRoArray<ParamValue>\n      validate: (value: ParamValue) => boolean\n    }\n>\n\ntype InferParams<S extends ParamsSchema> = {\n  [K in keyof S]:\n    | (S[K]['required'] extends true\n        ? never\n        : 'default' extends keyof S[K]\n          ? S[K]['default']\n          : undefined)\n    | (S[K]['multiple'] extends true\n        ? NeRoArray<InferParamPredicate<S[K]['validate']>>\n        : InferParamPredicate<S[K]['validate']>)\n} & NonNullable<unknown>\n\nexport class Parser<P extends string, S extends ParamsSchema> {\n  public readonly schemaKeys: ReadonlySet<keyof S & string>\n\n  constructor(\n    public readonly prefix: P,\n    public readonly schema: S,\n    public readonly positionalName?: keyof S & string,\n  ) {\n    this.schemaKeys = new Set(Object.keys(schema))\n  }\n\n  format(values: InferParams<S>) {\n    const params = new URLSearchParams()\n    let positional: string | undefined = undefined\n\n    for (const key of this.schemaKeys) {\n      const value = values[key]\n      // Ignore undefined values\n      if (value === undefined) continue\n\n      const schema = this.schema[key]\n\n      // Normalize the value if a normalization function is provided\n      const normalized = schema.normalize\n        ? schema.normalize(value as any)\n        : value\n\n      // Ignore values that are equal to the default value\n      if (!schema.required) {\n        if (schema.default === normalized) continue\n        if (\n          schema.multiple &&\n          schema.default &&\n          arrayParamEquals(schema.default, normalized as NeRoArray<string>)\n        ) {\n          continue\n        }\n      }\n\n      if (Array.isArray(normalized)) {\n        if (key === this.positionalName && normalized.length === 1) {\n          positional = String(normalized[0]!)\n        } else {\n          // remove duplicates\n          const unique = new Set(normalized.map(String))\n          for (const v of unique) params.append(key, v)\n        }\n      } else {\n        if (key === this.positionalName) {\n          positional = String(normalized)\n        } else {\n          params.set(key, String(normalized))\n        }\n      }\n    }\n\n    return new ScopeStringSyntax(this.prefix, positional, params).toString()\n  }\n\n  // @NOTE If we needed to ever have more detailed reason as to why parsing\n  // fails, this function could easily be updated to return a\n  // ValidationResult<T> type that explains the reason for failure.\n  parse(syntax: ScopeSyntax<P>) {\n    // @NOTE no need to check prefix, since the typing (P generic) already\n    // ensures it matches\n\n    for (const key of syntax.keys()) {\n      if (!this.schemaKeys.has(key)) return null\n    }\n\n    const result: Record<\n      string,\n      undefined | ParamValue | NeRoArray<ParamValue>\n    > = Object.create(null)\n\n    for (const key of this.schemaKeys) {\n      const definition = this.schema[key]\n\n      const param = definition.multiple\n        ? syntax.getMulti(key)\n        : syntax.getSingle(key)\n\n      if (param === null) {\n        return null // Value is not valid\n      } else if (param !== undefined) {\n        if (key === this.positionalName && syntax.positional !== undefined) {\n          // Positional parameter cannot be used with named parameters\n          return null\n        }\n\n        if (definition.multiple) {\n          // Empty array is not valid\n          if (!(param as ParamValue[]).length) return null\n          if (!(param as ParamValue[]).every(definition.validate)) {\n            return null\n          }\n        } else {\n          if (!definition.validate(param as ParamValue)) {\n            return null\n          }\n        }\n\n        result[key] = param as ParamValue | NeRoArray<ParamValue>\n      } else if (\n        key === this.positionalName &&\n        syntax.positional !== undefined\n      ) {\n        // No named parameters found, but there is a positional parameter\n        const { positional } = syntax\n        if (!definition.validate(positional)) {\n          return null\n        }\n        result[key] = definition.multiple ? [positional] : positional\n      } else if (definition.required) {\n        return null\n      } else {\n        result[key] = definition.default\n      }\n    }\n\n    return result as InferParams<S>\n  }\n}\n\n/**\n * Two param arrays are considered equal if they contain the same values,\n * regardless of the order and duplicates.\n * @param a - The first array to compare.\n * @param b - The second array to compare.\n */\nfunction arrayParamEquals(\n  a: readonly unknown[],\n  b: readonly unknown[],\n): boolean {\n  for (const item of a) if (!b.includes(item)) return false\n  for (const item of b) if (!a.includes(item)) return false\n  return true\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/resource-permission.ts",
    "content": "import { ScopeStringFor } from './syntax.js'\nimport { Matchable } from './util.js'\n\n/**\n * Interface destined to provide consistency across parsed permission scopes for\n * resources (blob, repo, etc.).\n */\nexport interface ResourcePermission<R extends string, T> extends Matchable<T> {\n  toString(): ScopeStringFor<R>\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/syntax-lexicon.ts",
    "content": "import { LexiconPermission } from './lexicon.js'\nimport { ScopeSyntax } from './syntax.js'\n\nconst isArray: (value: unknown) => value is readonly unknown[] = Array.isArray\n\n/**\n * Translates a {@link LexiconPermission} into a {@link ScopeSyntax}.\n */\nexport class LexPermissionSyntax<P extends string = string>\n  implements ScopeSyntax<P>\n{\n  constructor(readonly lexPermission: LexiconPermission<P>) {}\n\n  get prefix() {\n    return this.lexPermission.resource\n  }\n\n  get positional() {\n    return undefined\n  }\n\n  get(key: string) {\n    // Ignore reserved keywords\n    if (key === 'type') return undefined\n    if (key === 'resource') return undefined\n\n    // Ignore inherited properties (toString(), etc.)\n    if (!Object.hasOwn(this.lexPermission, key)) return undefined\n\n    return this.lexPermission[key]\n  }\n\n  *keys() {\n    for (const key of Object.keys(this.lexPermission)) {\n      if (this.get(key) !== undefined) yield key\n    }\n  }\n\n  getSingle(key: string) {\n    const value = this.get(key)\n    if (isArray(value)) return null\n    return value\n  }\n\n  getMulti(key: string) {\n    const value = this.get(key)\n    if (value === undefined) return undefined\n    if (!isArray(value)) return null\n    return value\n  }\n\n  toJSON() {\n    return this.lexPermission\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/syntax-string.test.ts",
    "content": "import { ScopeStringSyntax } from './syntax-string.js'\nimport { ScopeStringFor } from './syntax.js'\n\ndescribe('ScopeStringSyntax', () => {\n  for (const { scope, content } of [\n    {\n      scope: 'my-res',\n      content: { prefix: 'my-res' },\n    },\n    {\n      scope: 'my-res:my-pos',\n      content: { prefix: 'my-res', positional: 'my-pos' },\n    },\n    {\n      scope: 'my-res:',\n      content: { prefix: 'my-res', positional: '' },\n    },\n    {\n      scope: 'my-res:foo?x=value&y=value-y',\n      content: {\n        prefix: 'my-res',\n        positional: 'foo',\n        params: { x: ['value'], y: ['value-y'] },\n      },\n    },\n    {\n      scope: 'my-res?x=value&y=value-y',\n      content: { prefix: 'my-res', params: { x: ['value'], y: ['value-y'] } },\n    },\n    {\n      scope: 'my-res?x=foo&x=bar&x=baz',\n      content: { prefix: 'my-res', params: { x: ['foo', 'bar', 'baz'] } },\n    },\n    {\n      scope: 'rpc:foo.bar?aud=did:foo:bar?lxm=bar.baz',\n      content: {\n        prefix: 'rpc',\n        positional: 'foo.bar',\n        params: { aud: ['did:foo:bar?lxm=bar.baz'] },\n      },\n    },\n  ] satisfies Array<{\n    scope: ScopeStringFor<'my-res' | 'rpc'>\n    content: {\n      prefix: string\n      positional?: string\n      params?: Record<string, string[]>\n    }\n  }>) {\n    const syntax = ScopeStringSyntax.fromString<'my-res' | 'rpc'>(scope)\n\n    describe(scope, () => {\n      it('should match the expected syntax', () => {\n        expect(syntax).toMatchObject({\n          prefix: content.prefix,\n          positional: content.positional,\n        })\n      })\n\n      it(`should match ${scope} prefix`, () => {\n        expect(syntax.prefix).toBe(content.prefix)\n      })\n\n      it(`should return positional parameter`, () => {\n        expect(syntax.positional).toBe(content.positional)\n      })\n\n      it(`should return undefined for nonexistent single-value param`, () => {\n        expect(syntax.getSingle('nonexistent')).toBeUndefined()\n      })\n\n      it(`should return undefined for nonexistent multi-value param`, () => {\n        expect(syntax.getMulti('nonexistent')).toBeUndefined()\n      })\n\n      const { params } = content\n      if (params) {\n        it(`only contain allowed parameters`, () => {\n          const allowedParams = Object.keys(params) as [string, ...string[]]\n          expect(\n            Array.from(syntax.keys()).every((key) =>\n              allowedParams.includes(key),\n            ),\n          ).toBe(true)\n        })\n\n        for (const [key, values] of Object.entries(params)) {\n          it(`should get an array when reading \"${key}\"`, () => {\n            expect(syntax.getMulti(key)).toEqual(values)\n          })\n\n          if (values.length === 1) {\n            it(`should allow retrieving single-value params`, () => {\n              expect(syntax.getSingle(key)).toEqual(values[0])\n            })\n          } else {\n            it(`should return null for multi-value params`, () => {\n              expect(syntax.getSingle(key)).toBeNull()\n            })\n          }\n        }\n      }\n    })\n  }\n\n  describe('invalid positional parameters', () => {\n    it('should return null for positional parameters used together with named parameters', () => {\n      const syntax = ScopeStringSyntax.fromString('my-res:pos?x=value')\n      expect(syntax.getSingle('x')).toBe('value')\n      expect(syntax.getMulti('x')).toEqual(['value'])\n    })\n  })\n\n  describe('url encoding', () => {\n    it('should handle URL encoding in positional parameters', () => {\n      const syntax = ScopeStringSyntax.fromString('my-res:my%20pos')\n      expect(syntax.positional).toBe('my pos')\n    })\n\n    it('should handle URL encoding in named parameters', () => {\n      const syntax = ScopeStringSyntax.fromString('my-res?x=my%20value')\n      expect(syntax.getSingle('x')).toBe('my value')\n    })\n\n    it(`should allow colon (:) in positional parameters`, () => {\n      const syntax = ScopeStringSyntax.fromString('my-res:my:pos')\n      expect(syntax.positional).toBe('my:pos')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/syntax-string.ts",
    "content": "import { ScopeStringFor, ScopeSyntax } from './syntax.js'\nimport { minIdx } from './util.js'\n\n/**\n * Translates a scope string into a {@link ScopeSyntax}.\n */\nexport class ScopeStringSyntax<P extends string> implements ScopeSyntax<P> {\n  constructor(\n    readonly prefix: P,\n    readonly positional?: string,\n    readonly params?: Readonly<URLSearchParams>,\n  ) {}\n\n  *keys() {\n    if (this.params) yield* this.params.keys()\n  }\n\n  getSingle(key: string) {\n    if (!this.params?.has(key)) return undefined\n    const value = this.params.getAll(key)\n    if (value.length > 1) return null\n    return value[0]!\n  }\n\n  getMulti(key: string) {\n    if (!this.params?.has(key)) return undefined\n    return this.params.getAll(key)\n  }\n\n  toString() {\n    let scope: string = this.prefix\n\n    if (this.positional !== undefined) {\n      scope += `:${normalizeURIComponent(encodeURIComponent(this.positional))}`\n    }\n\n    if (this.params?.size) {\n      scope += `?${normalizeURIComponent(this.params.toString())}`\n    }\n\n    return scope as ScopeStringFor<P>\n  }\n\n  static fromString<P extends string>(\n    scopeValue: ScopeStringFor<P>,\n  ): ScopeStringSyntax<P> {\n    const paramIdx = scopeValue.indexOf('?')\n    const colonIdx = scopeValue.indexOf(':')\n    const prefixEnd = minIdx(paramIdx, colonIdx)\n\n    // No param or positional\n    if (prefixEnd === -1) {\n      return new ScopeStringSyntax(scopeValue as P)\n    }\n\n    const prefix = scopeValue.slice(0, prefixEnd) as P\n\n    // Parse the positional parameter if present\n    const positional =\n      colonIdx !== -1\n        ? paramIdx === -1\n          ? decodeURIComponent(scopeValue.slice(colonIdx + 1))\n          : colonIdx < paramIdx\n            ? decodeURIComponent(scopeValue.slice(colonIdx + 1, paramIdx))\n            : undefined\n        : undefined\n\n    // Parse the query string if present and non empty\n    const params =\n      paramIdx !== -1 && paramIdx < scopeValue.length - 1\n        ? new URLSearchParams(scopeValue.slice(paramIdx + 1))\n        : undefined\n\n    return new ScopeStringSyntax(prefix, positional, params)\n  }\n}\n\n/**\n * Set of characters that are allowed in scope components without encoding. This\n * is used to normalize scope components.\n */\nconst ALLOWED_SCOPE_CHARS = new Set(\n  // @NOTE This list must not contain \"?\" or \"&\" as it would interfere with\n  // query string parsing.\n  [':', '/', '+', ',', '@', '%'],\n)\n\nconst NORMALIZABLE_CHARS_MAP = new Map(\n  Array.from(\n    ALLOWED_SCOPE_CHARS,\n    (c) => [encodeURIComponent(c), c] as const,\n  ).filter(\n    ([encoded, c]) =>\n      // Make sure that any char added to ALLOWED_SCOPE_CHARS that is a char\n      // that indeed needs encoding. Also, the normalizeURIComponent only\n      // supports three-character percent-encoded sequences.\n      encoded !== c && encoded.length === 3 && encoded.startsWith('%'),\n  ),\n)\n\n/**\n * Assumes a properly url-encoded string.\n */\nfunction normalizeURIComponent(value: string): string {\n  // No need to read the last two characters since percent encoded characters\n  // are always three characters long.\n  let end = value.length - 2\n\n  for (let i = 0; i < end; i++) {\n    // Check if the character is a percent-encoded character\n    if (value.charCodeAt(i) === 0x25 /* % */) {\n      // Read the next encoded char. Current version only supports\n      // three-character percent-encoded sequences.\n      const encodedChar = value.slice(i, i + 3)\n\n      // Check if the encoded character is in the normalization map\n      const normalizedChar = NORMALIZABLE_CHARS_MAP.get(encodedChar)\n      if (normalizedChar) {\n        // Replace the encoded character with its normalized version\n        value = `${value.slice(0, i)}${normalizedChar}${value.slice(i + encodedChar.length)}`\n\n        // Adjust index to account for the length change\n        i += normalizedChar.length - 1\n\n        // Adjust end index since we replaced encoded char with normalized char\n        end -= encodedChar.length - normalizedChar.length\n      }\n    }\n  }\n\n  return value\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/syntax.test.ts",
    "content": "import { isScopeStringFor } from './syntax.js'\n\ndescribe('isScopeStringFor', () => {\n  describe('exact match', () => {\n    it('should return true for exact match', () => {\n      expect(isScopeStringFor('prefix', 'prefix')).toBe(true)\n    })\n\n    it('should return false for different prefix', () => {\n      expect(isScopeStringFor('prefix', 'differentResource')).toBe(false)\n    })\n  })\n\n  describe('with positional parameter', () => {\n    it('should return true for exact match with positional parameter', () => {\n      expect(isScopeStringFor('prefix:positional', 'prefix')).toBe(true)\n    })\n\n    it('should return false for different prefix with positional parameter', () => {\n      expect(isScopeStringFor('differentResource:positional', 'prefix')).toBe(\n        false,\n      )\n    })\n  })\n\n  describe('with named parameters', () => {\n    it('should return true for exact match with named parameters', () => {\n      expect(isScopeStringFor('prefix?param=value', 'prefix')).toBe(true)\n    })\n\n    it('should return false for different prefix with named parameters', () => {\n      expect(isScopeStringFor('prefix', 'prefi')).toBe(false)\n      expect(isScopeStringFor('prefix:pos', 'prefi')).toBe(false)\n      expect(isScopeStringFor('prefix?param=value', 'prefi')).toBe(false)\n      expect(isScopeStringFor('prefix', 'fix')).toBe(false)\n      expect(isScopeStringFor('prefix:pos', 'fix')).toBe(false)\n      expect(isScopeStringFor('prefix?param=value', 'fix')).toBe(false)\n      expect(isScopeStringFor('differentResource?param=value', 'prefix')).toBe(\n        false,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/syntax.ts",
    "content": "export type ParamValue = string | number | boolean\n\nexport type NeArray<T> = [T, ...T[]]\n\n/**\n * Non-empty readonly array\n */\nexport type NeRoArray<T> = readonly [T, ...T[]]\n\nexport type ScopeStringFor<P extends string> =\n  | P\n  | `${P}:${string}`\n  | `${P}?${string}`\n\n/**\n * Allows to quickly check if a scope is for a specific resource.\n */\nexport function isScopeStringFor<P extends string>(\n  value: string,\n  prefix: P,\n): value is ScopeStringFor<P> {\n  if (value.length > prefix.length) {\n    // First, check the next char is either : or ?\n    const nextChar = value.charCodeAt(prefix.length)\n    if (nextChar !== 0x3a /* : */ && nextChar !== 0x3f /* ? */) {\n      return false\n    }\n\n    // Then check the full prefix\n    return value.startsWith(prefix)\n  } else {\n    // value and prefix must be equal\n    return value === prefix\n  }\n}\n\n/**\n * Abstract interface that allows parsing various syntaxes into permission\n * representations.\n */\nexport interface ScopeSyntax<P extends string> {\n  readonly prefix: P\n  readonly positional?: ParamValue\n  keys(): Iterable<string, void, unknown>\n  getSingle(key: string): ParamValue | null | undefined\n  getMulti(key: string): readonly ParamValue[] | null | undefined\n}\n\nexport function isScopeSyntaxFor<P extends string>(\n  syntax: ScopeSyntax<string>,\n  prefix: P,\n): syntax is ScopeSyntax<P> {\n  return syntax.prefix === prefix\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/lib/util.ts",
    "content": "export interface Matchable<T> {\n  matches(options: T): boolean\n}\n\nexport function minIdx(a: number, b: number): number {\n  if (a === -1) return b\n  if (b === -1) return a\n  return Math.min(a, b)\n}\n\nexport function knownValuesValidator<T>(values: Iterable<T>) {\n  const set = new Set<unknown>(values)\n  return (value: unknown): value is T => set.has(value)\n}\n\nexport function isNonNullable<T>(value: T): value is NonNullable<T> {\n  return value != null\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scope-missing-error.ts",
    "content": "export class ScopeMissingError extends Error {\n  name = 'ScopeMissingError'\n\n  // compatibility layer with http-errors package. The goal if to make\n  // isHttpError(new ScopeMissingError) return true.\n  status = 403\n  expose = true\n  get statusCode() {\n    return this.status\n  }\n\n  constructor(public readonly scope: string) {\n    super(`Missing required scope \"${scope}\"`)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scope-permissions-transition.test.ts",
    "content": "import { ScopePermissionsTransition } from './scope-permissions-transition.js'\n\ndescribe('ScopePermissionsTransition', () => {\n  describe('allowsAccount', () => {\n    it('should allow account:email with transition:email', () => {\n      const set = new ScopePermissionsTransition(\n        'transition:email account:repo',\n      )\n      expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)\n      expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)\n\n      expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(true)\n      expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)\n\n      expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)\n      expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(\n        false,\n      )\n    })\n  })\n\n  describe('allowsBlob', () => {\n    it('should allow blob with transition:generic', () => {\n      const set = new ScopePermissionsTransition('transition:generic')\n      expect(set.allowsBlob({ mime: 'foo/bar' })).toBe(true)\n    })\n  })\n\n  describe('allowsRepo', () => {\n    it('should allow repo with transition:generic', () => {\n      const set = new ScopePermissionsTransition('transition:generic')\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),\n      ).toBe(true)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),\n      ).toBe(true)\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),\n      ).toBe(true)\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),\n      ).toBe(true)\n    })\n  })\n\n  describe('allowsRpc', () => {\n    it('should allow rpc with transition:generic', () => {\n      const set = new ScopePermissionsTransition('transition:generic')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.post',\n        }),\n      ).toBe(true)\n      expect(\n        set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),\n      ).toBe(true)\n      expect(set.allowsRpc({ aud: 'did:web:example.com', lxm: '*' })).toBe(true)\n    })\n\n    it('should allow chat.bsky.* methods with \"transition:chat.bsky\"', () => {\n      const set = new ScopePermissionsTransition('transition:chat.bsky')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'chat.bsky.message.send',\n        }),\n      ).toBe(true)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'chat.bsky.conversation.get',\n        }),\n      ).toBe(true)\n\n      // Control\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.post',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),\n      ).toBe(false)\n      expect(set.allowsRpc({ aud: 'did:web:example.com', lxm: '*' })).toBe(\n        false,\n      )\n    })\n\n    it('should reject chat methods with \"transition:generic\"', () => {\n      const set = new ScopePermissionsTransition('transition:generic')\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'chat.bsky.message.send',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'chat.bsky.conversation.get',\n        }),\n      ).toBe(false)\n\n      // Control\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.post',\n        }),\n      ).toBe(true)\n      expect(\n        set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),\n      ).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scope-permissions-transition.ts",
    "content": "import {\n  AccountPermissionMatch,\n  BlobPermissionMatch,\n  RepoPermissionMatch,\n  RpcPermissionMatch,\n  ScopePermissions,\n} from './scope-permissions.js'\n\n/**\n * Overrides the default permission set to allow transitional scopes to be used\n * in place of the generic scopes.\n */\nexport class ScopePermissionsTransition extends ScopePermissions {\n  get hasTransitionGeneric(): boolean {\n    return this.scopes.has('transition:generic')\n  }\n\n  get hasTransitionEmail(): boolean {\n    return this.scopes.has('transition:email')\n  }\n\n  get hasTransitionChatBsky(): boolean {\n    return this.scopes.has('transition:chat.bsky')\n  }\n\n  override allowsAccount(options: AccountPermissionMatch): boolean {\n    if (\n      options.attr === 'email' &&\n      options.action === 'read' &&\n      this.hasTransitionEmail\n    ) {\n      return true\n    }\n\n    return super.allowsAccount(options)\n  }\n\n  override allowsBlob(options: BlobPermissionMatch): boolean {\n    if (this.hasTransitionGeneric) {\n      return true\n    }\n\n    return super.allowsBlob(options)\n  }\n\n  override allowsRepo(options: RepoPermissionMatch): boolean {\n    if (this.hasTransitionGeneric) {\n      return true\n    }\n\n    return super.allowsRepo(options)\n  }\n\n  override allowsRpc(options: RpcPermissionMatch) {\n    const { lxm } = options\n\n    if (this.hasTransitionGeneric && lxm === '*') {\n      return true\n    }\n\n    if (this.hasTransitionGeneric && !lxm.startsWith('chat.bsky.')) {\n      return true\n    }\n\n    if (this.hasTransitionChatBsky && lxm.startsWith('chat.bsky.')) {\n      return true\n    }\n\n    return super.allowsRpc(options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scope-permissions.test.ts",
    "content": "import { ScopePermissions } from './scope-permissions.js'\n\ndescribe('ScopePermissions', () => {\n  describe('allowsAccount', () => {\n    it('should properly allow \"account:email\"', () => {\n      const set = new ScopePermissions('account:email')\n\n      expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(true)\n      expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)\n\n      expect(set.allowsAccount({ attr: 'repo', action: 'read' })).toBe(false)\n      expect(set.allowsAccount({ attr: 'repo', action: 'manage' })).toBe(false)\n\n      expect(set.allowsAccount({ attr: 'status', action: 'read' })).toBe(false)\n      expect(set.allowsAccount({ attr: 'status', action: 'manage' })).toBe(\n        false,\n      )\n    })\n\n    it('should ignore \"transition:email\"', () => {\n      const set = new ScopePermissions('transition:email')\n\n      expect(set.allowsAccount({ attr: 'email', action: 'read' })).toBe(false)\n      expect(set.allowsAccount({ attr: 'email', action: 'manage' })).toBe(false)\n    })\n  })\n\n  describe('allowsBlob', () => {\n    it('should allow any mime with \"blob:*/*\"', () => {\n      const set = new ScopePermissions('blob:*/*')\n      expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)\n      expect(set.allowsBlob({ mime: 'application/json' })).toBe(true)\n    })\n\n    it('should only allow images with \"blob:image/*\"', () => {\n      const set = new ScopePermissions('blob:image/*')\n      expect(set.allowsBlob({ mime: 'image/png' })).toBe(true)\n      expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)\n    })\n\n    it('should ignore invalid scope \"blob:*\"', () => {\n      const set = new ScopePermissions('blob:*')\n      expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)\n      expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)\n    })\n\n    it('should ignore invalid scope \"blob:/image\"', () => {\n      const set = new ScopePermissions('blob:/image')\n      expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)\n      expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)\n    })\n\n    it('should ignore \"transition:generic\"', () => {\n      const set = new ScopePermissions('transition:generic')\n      expect(set.allowsBlob({ mime: 'image/png' })).toBe(false)\n      expect(set.allowsBlob({ mime: 'application/json' })).toBe(false)\n    })\n  })\n\n  describe('allowsRepo', () => {\n    it('should allow any repo action with \"repo:*\"', () => {\n      const set = new ScopePermissions('repo:*')\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),\n      ).toBe(true)\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),\n      ).toBe(true)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),\n      ).toBe(true)\n    })\n\n    it('should allow specific repo actions', () => {\n      const set = new ScopePermissions('repo:*?action=create')\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),\n      ).toBe(true)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),\n      ).toBe(true)\n\n      // Control\n\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),\n      ).toBe(false)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),\n      ).toBe(false)\n    })\n\n    it('should allow specific repo collection & actions', () => {\n      const set = new ScopePermissions('repo:com.example.foo?action=create')\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),\n      ).toBe(true)\n\n      // Control\n\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),\n      ).toBe(false)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),\n      ).toBe(false)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),\n      ).toBe(false)\n    })\n\n    it('should ignore transition:generic', () => {\n      const set = new ScopePermissions('transition:generic')\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'create' }),\n      ).toBe(false)\n      expect(\n        set.allowsRepo({ collection: 'app.bsky.feed.post', action: 'delete' }),\n      ).toBe(false)\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'create' }),\n      ).toBe(false)\n      expect(\n        set.allowsRepo({ collection: 'com.example.foo', action: 'update' }),\n      ).toBe(false)\n    })\n  })\n\n  describe('allowsRpc', () => {\n    it('should ignore \"rpc:*?lxm=*\"', () => {\n      const set = new ScopePermissions('rpc:*?lxm=*')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'com.example.method',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.getFeed',\n        }),\n      ).toBe(false)\n    })\n\n    it('should allow constraining \"lxm\"', () => {\n      const set = new ScopePermissions('rpc:app.bsky.feed.getFeed?aud=*')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.getFeed',\n        }),\n      ).toBe(true)\n      expect(\n        set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),\n      ).toBe(true)\n\n      // Control\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'com.example.method',\n        }),\n      ).toBe(false)\n    })\n\n    it('should allow constraining \"aud\"', () => {\n      const set = new ScopePermissions('rpc:*?aud=did:web:example.com%23foo')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com#foo',\n          lxm: 'com.example.method',\n        }),\n      ).toBe(true)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com#foo',\n          lxm: 'app.bsky.feed.getFeed',\n        }),\n      ).toBe(true)\n\n      // Control\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:bar.com#foo', // invalid aud (wrong service id)\n          lxm: 'com.example.method',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com', // invalid aud (no service id)\n          lxm: 'com.example.method',\n        }),\n      ).toBe(false)\n    })\n\n    it('should allow constraining \"lxm\" and \"aud\"', () => {\n      const set = new ScopePermissions(\n        'rpc:app.bsky.feed.getFeed?aud=did:web:example.com%23foo',\n      )\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com#foo',\n          lxm: 'app.bsky.feed.getFeed',\n        }),\n      ).toBe(true)\n\n      // Control\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'com.example.method',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({ aud: 'did:plc:blahbla', lxm: 'app.bsky.feed.getFeed' }),\n      ).toBe(false)\n    })\n\n    it('should ignore \"transition:generic\"', () => {\n      const set = new ScopePermissions('transition:generic')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.getFeed',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'com.example.method',\n        }),\n      ).toBe(false)\n    })\n\n    it('should ignore \"transition:chat.bsky\"', () => {\n      const set = new ScopePermissions('transition:chat.bsky')\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'chat.bsky.message.send',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'chat.bsky.conversation.get',\n        }),\n      ).toBe(false)\n\n      // Control\n\n      expect(\n        set.allowsRpc({\n          aud: 'did:web:example.com',\n          lxm: 'app.bsky.feed.post',\n        }),\n      ).toBe(false)\n      expect(\n        set.allowsRpc({ aud: 'did:web:example.com', lxm: 'com.example.foo' }),\n      ).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scope-permissions.ts",
    "content": "import { ScopeMissingError } from './scope-missing-error.js'\nimport {\n  AccountPermission,\n  AccountPermissionMatch,\n} from './scopes/account-permission.js'\nimport {\n  BlobPermission,\n  BlobPermissionMatch,\n} from './scopes/blob-permission.js'\nimport {\n  IdentityPermission,\n  IdentityPermissionMatch,\n} from './scopes/identity-permission.js'\nimport {\n  RepoPermission,\n  RepoPermissionMatch,\n} from './scopes/repo-permission.js'\nimport { RpcPermission, RpcPermissionMatch } from './scopes/rpc-permission.js'\nimport { ScopesSet } from './scopes-set.js'\n\nexport type {\n  AccountPermissionMatch,\n  BlobPermissionMatch,\n  IdentityPermissionMatch,\n  RepoPermissionMatch,\n  RpcPermissionMatch,\n}\n\nexport class ScopePermissions {\n  public readonly scopes: ScopesSet\n\n  constructor(scope?: null | string | Iterable<string>) {\n    this.scopes = new ScopesSet(\n      !scope // \"\" | null | undefined\n        ? undefined\n        : typeof scope === 'string'\n          ? scope.split(' ')\n          : scope,\n    )\n  }\n\n  public allowsAccount(options: AccountPermissionMatch): boolean {\n    return this.scopes.matches('account', options)\n  }\n  public assertAccount(options: AccountPermissionMatch): void {\n    if (!this.allowsAccount(options)) {\n      const scope = AccountPermission.scopeNeededFor(options)\n      throw new ScopeMissingError(scope)\n    }\n  }\n\n  public allowsIdentity(options: IdentityPermissionMatch): boolean {\n    return this.scopes.matches('identity', options)\n  }\n  public assertIdentity(options: IdentityPermissionMatch): void {\n    if (!this.allowsIdentity(options)) {\n      const scope = IdentityPermission.scopeNeededFor(options)\n      throw new ScopeMissingError(scope)\n    }\n  }\n\n  public allowsBlob(options: BlobPermissionMatch): boolean {\n    return this.scopes.matches('blob', options)\n  }\n  public assertBlob(options: BlobPermissionMatch): void {\n    if (!this.allowsBlob(options)) {\n      const scope = BlobPermission.scopeNeededFor(options)\n      throw new ScopeMissingError(scope)\n    }\n  }\n\n  public allowsRepo(options: RepoPermissionMatch): boolean {\n    return this.scopes.matches('repo', options)\n  }\n  public assertRepo(options: RepoPermissionMatch): void {\n    if (!this.allowsRepo(options)) {\n      const scope = RepoPermission.scopeNeededFor(options)\n      throw new ScopeMissingError(scope)\n    }\n  }\n\n  public allowsRpc(options: RpcPermissionMatch): boolean {\n    return this.scopes.matches('rpc', options)\n  }\n  public assertRpc(options: RpcPermissionMatch): void {\n    if (!this.allowsRpc(options)) {\n      const scope = RpcPermission.scopeNeededFor(options)\n      throw new ScopeMissingError(scope)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/account-permission.test.ts",
    "content": "import { AccountPermission } from './account-permission.js'\n\ndescribe('AccountPermission', () => {\n  describe('static', () => {\n    describe('fromString', () => {\n      it('should parse valid scope strings', () => {\n        const scope1 = AccountPermission.fromString('account:email?action=read')\n        expect(scope1).not.toBeNull()\n        expect(scope1!.attr).toBe('email')\n        expect(scope1!.action).toEqual(['read'])\n\n        const scope2 = AccountPermission.fromString(\n          'account:repo?action=manage',\n        )\n        expect(scope2).not.toBeNull()\n        expect(scope2!.attr).toBe('repo')\n        expect(scope2!.action).toEqual(['manage'])\n      })\n\n      it('should parse scope without action (defaults to read)', () => {\n        const scope = AccountPermission.fromString('account:status')\n        expect(scope).not.toBeNull()\n        expect(scope!.attr).toBe('status')\n        expect(scope!.action).toEqual(['read'])\n      })\n\n      it('should reject invalid attribute names', () => {\n        const scope = AccountPermission.fromString('account:invalid')\n        expect(scope).toBeNull()\n      })\n\n      it('should reject invalid action names', () => {\n        const scope = AccountPermission.fromString(\n          'account:email?action=invalid',\n        )\n        expect(scope).toBeNull()\n      })\n\n      it('should reject malformed scope strings', () => {\n        expect(AccountPermission.fromString('invalid:email')).toBeNull()\n        expect(AccountPermission.fromString('account')).toBeNull()\n        expect(AccountPermission.fromString('')).toBeNull()\n        expect(AccountPermission.fromString('account:')).toBeNull()\n      })\n    })\n\n    describe('scopeNeededFor', () => {\n      it('should return correct scope string for read actions', () => {\n        expect(\n          AccountPermission.scopeNeededFor({ attr: 'email', action: 'read' }),\n        ).toBe('account:email')\n        expect(\n          AccountPermission.scopeNeededFor({ attr: 'repo', action: 'read' }),\n        ).toBe('account:repo')\n        expect(\n          AccountPermission.scopeNeededFor({ attr: 'status', action: 'read' }),\n        ).toBe('account:status')\n      })\n\n      it('should return correct scope string for manage actions', () => {\n        expect(\n          AccountPermission.scopeNeededFor({ attr: 'email', action: 'manage' }),\n        ).toBe('account:email?action=manage')\n        expect(\n          AccountPermission.scopeNeededFor({ attr: 'repo', action: 'manage' }),\n        ).toBe('account:repo?action=manage')\n        expect(\n          AccountPermission.scopeNeededFor({\n            attr: 'status',\n            action: 'manage',\n          }),\n        ).toBe('account:status?action=manage')\n      })\n    })\n  })\n\n  describe('instance', () => {\n    describe('matches', () => {\n      it('should match read action', () => {\n        const scope = AccountPermission.fromString('account:email?action=read')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)\n      })\n\n      it('should match manage action', () => {\n        const scope = AccountPermission.fromString('account:repo?action=manage')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'repo', action: 'manage' })).toBe(true)\n      })\n\n      it('should not match unspecified action', () => {\n        const scope = AccountPermission.fromString('account:email?action=read')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)\n      })\n\n      it('should not match different attribute', () => {\n        const scope = AccountPermission.fromString('account:email?action=read')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'repo', action: 'read' })).toBe(false)\n      })\n\n      it('should default to \"read\" action', () => {\n        const scope = AccountPermission.fromString('account:email')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)\n        expect(scope!.matches({ attr: 'email', action: 'manage' })).toBe(false)\n      })\n\n      it('should work with all valid attributes', () => {\n        const emailScope = AccountPermission.fromString(\n          'account:email?action=read',\n        )\n        const repoScope = AccountPermission.fromString(\n          'account:repo?action=manage',\n        )\n        const statusScope = AccountPermission.fromString(\n          'account:status?action=read',\n        )\n\n        expect(emailScope).not.toBeNull()\n        expect(repoScope).not.toBeNull()\n        expect(statusScope).not.toBeNull()\n\n        expect(emailScope!.matches({ attr: 'email', action: 'read' })).toBe(\n          true,\n        )\n        expect(repoScope!.matches({ attr: 'repo', action: 'manage' })).toBe(\n          true,\n        )\n        expect(statusScope!.matches({ attr: 'status', action: 'read' })).toBe(\n          true,\n        )\n      })\n\n      it('should allow read when \"manage\" action is specified', () => {\n        const scope = AccountPermission.fromString(\n          'account:email?action=manage',\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'email', action: 'read' })).toBe(true)\n      })\n    })\n\n    describe('toString', () => {\n      it('should format scope with explicit action', () => {\n        const scope = new AccountPermission('email', ['manage'])\n        expect(scope.toString()).toBe('account:email?action=manage')\n      })\n\n      it('should format scope with default action', () => {\n        const scope = new AccountPermission('repo', ['read'])\n        expect(scope.toString()).toBe('account:repo')\n      })\n\n      it('should format all attributes correctly', () => {\n        expect(new AccountPermission('email', ['read']).toString()).toBe(\n          'account:email',\n        )\n        expect(new AccountPermission('repo', ['read']).toString()).toBe(\n          'account:repo',\n        )\n        expect(new AccountPermission('status', ['read']).toString()).toBe(\n          'account:status',\n        )\n        expect(new AccountPermission('email', ['manage']).toString()).toBe(\n          'account:email?action=manage',\n        )\n      })\n    })\n  })\n\n  it('should maintain consistency between toString and fromString', () => {\n    const testCases = [\n      'account:email',\n      'account:email?action=manage',\n      'account:repo',\n      'account:repo?action=manage',\n      'account:status',\n      'account:status?action=manage',\n    ]\n\n    for (const scope of testCases) {\n      expect(AccountPermission.fromString(scope)?.toString()).toBe(scope)\n    }\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/account-permission.ts",
    "content": "import { Parser } from '../lib/parser.js'\nimport { ResourcePermission } from '../lib/resource-permission.js'\nimport { ScopeStringSyntax } from '../lib/syntax-string.js'\nimport { NeRoArray, ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'\nimport { knownValuesValidator } from '../lib/util.js'\n\nexport const ACCOUNT_ATTRIBUTES = Object.freeze([\n  'email',\n  'repo',\n  'status',\n] as const)\nexport type AccountAttribute = (typeof ACCOUNT_ATTRIBUTES)[number]\n\nexport const ACCOUNT_ACTIONS = Object.freeze(['read', 'manage'] as const)\nexport type AccountAction = (typeof ACCOUNT_ACTIONS)[number]\n\nexport type AccountPermissionMatch = {\n  attr: AccountAttribute\n  action: AccountAction\n}\n\nexport class AccountPermission\n  implements ResourcePermission<'account', AccountPermissionMatch>\n{\n  constructor(\n    public readonly attr: AccountAttribute,\n    public readonly action: NeRoArray<AccountAction>,\n  ) {}\n\n  matches(options: AccountPermissionMatch) {\n    return (\n      this.attr === options.attr &&\n      (this.action.includes('manage') || this.action.includes(options.action))\n    )\n  }\n\n  toString() {\n    return AccountPermission.parser.format(this)\n  }\n\n  protected static readonly parser = new Parser(\n    'account',\n    {\n      attr: {\n        multiple: false,\n        required: true,\n        validate: knownValuesValidator(ACCOUNT_ATTRIBUTES),\n      },\n      action: {\n        multiple: true,\n        required: false,\n        validate: knownValuesValidator(ACCOUNT_ACTIONS),\n        default: ['read' as const],\n      },\n    },\n    'attr',\n  )\n\n  static fromString(scope: string) {\n    if (!isScopeStringFor(scope, 'account')) return null\n    const syntax = ScopeStringSyntax.fromString(scope)\n    return AccountPermission.fromSyntax(syntax)\n  }\n\n  static fromSyntax(syntax: ScopeSyntax<'account'>) {\n    const result = AccountPermission.parser.parse(syntax)\n    if (!result) return null\n\n    return new AccountPermission(result.attr, result.action)\n  }\n\n  static scopeNeededFor(options: AccountPermissionMatch) {\n    return AccountPermission.parser.format({\n      attr: options.attr,\n      action: [options.action],\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/blob-permission.test.ts",
    "content": "import { BlobPermission } from './blob-permission.js'\n\ndescribe('BlobPermission', () => {\n  describe('static', () => {\n    describe('fromString', () => {\n      it('should parse positional scope', () => {\n        const scope = BlobPermission.fromString('blob:image/png')\n        expect(scope).not.toBeNull()\n        expect(scope!.accept).toEqual(['image/png'])\n      })\n\n      it('should parse valid blob scope with multiple accept parameters', () => {\n        const scope = BlobPermission.fromString(\n          'blob?accept=image/png&accept=image/jpeg',\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.accept).toEqual(['image/png', 'image/jpeg'])\n      })\n\n      it('should reject blob scope without accept', () => {\n        const scope = BlobPermission.fromString('blob')\n        expect(scope).toBeNull()\n      })\n\n      for (const invalid of [\n        'invalid',\n        'scope',\n        'blob:invalid',\n        'blob?accept=invalid-mime',\n        'blob?accept=invalid',\n        'blob:*/**',\n        'blob:*/png',\n      ]) {\n        it(`should return null for invalid rpc scope: ${invalid}`, () => {\n          expect(BlobPermission.fromString(invalid)).toBeNull()\n        })\n      }\n    })\n\n    describe('scopeNeededFor', () => {\n      it('should return correct scope string for specific MIME type', () => {\n        const scope = BlobPermission.scopeNeededFor({ mime: 'image/png' })\n        expect(scope).toBe('blob:image/png')\n      })\n\n      it('should return scope that accepts all MIME types', () => {\n        const scope = BlobPermission.scopeNeededFor({\n          mime: 'application/json',\n        })\n        expect(scope).toBe('blob:application/json')\n      })\n    })\n  })\n\n  describe('instance', () => {\n    describe('matches', () => {\n      it('should match exact MIME type', () => {\n        const scope = BlobPermission.fromString('blob:image/png')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ mime: 'image/png' })).toBe(true)\n      })\n\n      it('should match wildcard MIME type', () => {\n        const scope = BlobPermission.fromString('blob:*/*')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ mime: 'image/jpeg' })).toBe(true)\n        expect(scope!.matches({ mime: 'application/json' })).toBe(true)\n      })\n\n      it('should match subtype wildcard MIME type', () => {\n        const scope = BlobPermission.fromString('blob:image/*')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ mime: 'image/gif' })).toBe(true)\n      })\n\n      it('should not match different MIME type', () => {\n        const scope = BlobPermission.fromString('blob:image/png')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ mime: 'image/jpeg' })).toBe(false)\n      })\n\n      it('should match multiple accept values', () => {\n        const scope = BlobPermission.fromString(\n          'blob?accept=image/png&accept=image/jpeg',\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ mime: 'image/png' })).toBe(true)\n        expect(scope!.matches({ mime: 'image/jpeg' })).toBe(true)\n        expect(scope!.matches({ mime: 'image/gif' })).toBe(false)\n      })\n    })\n\n    describe('toString', () => {\n      it('should format scope with accept parameter', () => {\n        const scope = new BlobPermission(['image/png', 'image/jpeg'])\n        expect(scope.toString()).toBe('blob?accept=image/jpeg&accept=image/png')\n      })\n\n      it('should strip redundant accept parameters', () => {\n        expect(new BlobPermission(['*/*', 'image/*']).toString()).toBe(\n          'blob:*/*',\n        )\n        expect(new BlobPermission(['*/*', 'image/png']).toString()).toBe(\n          'blob:*/*',\n        )\n        expect(new BlobPermission(['image/*', 'image/png']).toString()).toBe(\n          'blob:image/*',\n        )\n      })\n\n      it('should use positional format for single accept', () => {\n        expect(new BlobPermission(['image/png']).toString()).toBe(\n          'blob:image/png',\n        )\n        expect(new BlobPermission(['image/*']).toString()).toBe('blob:image/*')\n        expect(new BlobPermission(['*/*']).toString()).toBe('blob:*/*')\n      })\n\n      it('should use query format for multiple accepts', () => {\n        expect(new BlobPermission(['image/png', 'image/jpeg']).toString()).toBe(\n          'blob?accept=image/jpeg&accept=image/png',\n        )\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/blob-permission.ts",
    "content": "import { Accept, isAccept, matchesAnyAccept } from '../lib/mime.js'\nimport { Parser } from '../lib/parser.js'\nimport { ResourcePermission } from '../lib/resource-permission.js'\nimport { ScopeStringSyntax } from '../lib/syntax-string.js'\nimport {\n  NeArray,\n  NeRoArray,\n  ParamValue,\n  ScopeSyntax,\n  isScopeStringFor,\n} from '../lib/syntax.js'\n\nexport { type Accept }\n\nexport const DEFAULT_ACCEPT = Object.freeze(['*/*'] as const)\n\nexport type BlobPermissionMatch = {\n  mime: string\n}\n\nexport class BlobPermission\n  implements ResourcePermission<'blob', BlobPermissionMatch>\n{\n  constructor(public readonly accept: NeRoArray<Accept>) {}\n\n  matches(options: BlobPermissionMatch) {\n    return matchesAnyAccept(this.accept, options.mime)\n  }\n\n  toString() {\n    return BlobPermission.parser.format(this)\n  }\n\n  protected static readonly parser = new Parser(\n    'blob',\n    {\n      accept: {\n        multiple: true,\n        required: true,\n        validate: isAccept,\n        normalize: (value) => {\n          // Returns a more concise representation of the accept values.\n          if (value.includes('*/*')) return DEFAULT_ACCEPT\n\n          return value\n            .map(toLowerCase)\n            .filter(isNonRedundant)\n            .sort() as NeArray<Accept>\n        },\n      },\n    },\n    'accept',\n  )\n\n  static fromString(scope: string) {\n    if (!isScopeStringFor(scope, 'blob')) return null\n    const syntax = ScopeStringSyntax.fromString(scope)\n    return BlobPermission.fromSyntax(syntax)\n  }\n\n  static fromSyntax(syntax: ScopeSyntax<'blob'>) {\n    const result = BlobPermission.parser.parse(syntax)\n    if (!result) return null\n\n    return new BlobPermission(result.accept)\n  }\n\n  static scopeNeededFor(options: BlobPermissionMatch) {\n    return BlobPermission.parser.format({\n      accept: [options.mime as Accept],\n    })\n  }\n}\n\nfunction toLowerCase<T extends ParamValue>(\n  value: T,\n): T extends string ? string : T {\n  return (\n    typeof value === 'string' ? value.toLowerCase() : value\n  ) as T extends string ? string : T\n}\n\nfunction isNonRedundant(\n  value: ParamValue,\n  index: number,\n  arr: readonly ParamValue[],\n): boolean {\n  if (typeof value !== 'string') {\n    return true\n  }\n  if (value.endsWith('/*')) {\n    // assuming the array contains unique element, wildcards cannot be redundant\n    // with one another ('image/*' is not redundant with 'text/*')\n    return true\n  }\n  const base = value.split('/', 1)[0]\n  if (arr.includes(`${base}/*`)) {\n    // If another value in the array is a wildcard for the same base, we can\n    // skip this one as it is redundant. e.g. if the array contains 'image/png'\n    // and 'image/*', we can skip 'image/png' because 'image/*' already covers\n    // it.\n    return false\n  }\n  return true\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/identity-permission.test.ts",
    "content": "import { IdentityPermission } from './identity-permission.js'\n\ndescribe('IdentityPermission', () => {\n  describe('static', () => {\n    describe('fromString', () => {\n      it('should parse positional scope', () => {\n        const scope = IdentityPermission.fromString('identity:handle')\n        expect(scope).not.toBeNull()\n        expect(scope!.attr).toBe('handle')\n      })\n\n      it('should parse valid identity scope with wildcard attribute', () => {\n        const scope = IdentityPermission.fromString('identity:*')\n        expect(scope).not.toBeNull()\n        expect(scope!.attr).toBe('*')\n      })\n\n      it('should return null for invalid identity scope', () => {\n        expect(IdentityPermission.fromString('invalid')).toBeNull()\n        expect(IdentityPermission.fromString('identity:invalid')).toBeNull()\n      })\n\n      for (const invalid of [\n        'identity:*?action=*',\n        'identity:*?action=manage',\n        'identity:*?action=submit',\n        'invalid',\n        'identity:invalid',\n        'identity:handle?action=invalid',\n        'identity?attribute=invalid&action=invalid',\n      ]) {\n        it(`should return null for invalid rpc scope: ${invalid}`, () => {\n          expect(IdentityPermission.fromString(invalid)).toBeNull()\n        })\n      }\n    })\n\n    describe('scopeNeededFor', () => {\n      it('should return correct scope string for specific attribute and action', () => {\n        const scope = IdentityPermission.scopeNeededFor({ attr: 'handle' })\n        expect(scope).toBe('identity:handle')\n      })\n\n      it('should return scope that accepts all attributes with specific action', () => {\n        const scope = IdentityPermission.scopeNeededFor({ attr: '*' })\n        expect(scope).toBe('identity:*')\n      })\n    })\n  })\n\n  describe('instance', () => {\n    describe('matches', () => {\n      it('should match default attribute and action', () => {\n        const scope = IdentityPermission.fromString('identity:handle')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: 'handle' })).toBe(true)\n        expect(scope!.matches({ attr: '*' })).toBe(false)\n      })\n\n      it('should match wildcard attribute with specific action', () => {\n        const scope = IdentityPermission.fromString('identity:*')\n        expect(scope).not.toBeNull()\n        expect(scope!.matches({ attr: '*' })).toBe(true)\n        expect(scope!.matches({ attr: 'handle' })).toBe(true)\n      })\n    })\n\n    describe('toString', () => {\n      it('should format scope with default action', () => {\n        const scope = new IdentityPermission('handle')\n        expect(scope.toString()).toBe('identity:handle')\n      })\n\n      it('should format wildcard attribute with default action', () => {\n        const scope = new IdentityPermission('*')\n        expect(scope.toString()).toBe('identity:*')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/identity-permission.ts",
    "content": "import { Parser } from '../lib/parser.js'\nimport { ResourcePermission } from '../lib/resource-permission.js'\nimport { ScopeStringSyntax } from '../lib/syntax-string.js'\nimport { ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'\nimport { knownValuesValidator } from '../lib/util.js'\n\nexport const IDENTITY_ATTRIBUTES = Object.freeze(['handle', '*'] as const)\nexport type IdentityAttribute = (typeof IDENTITY_ATTRIBUTES)[number]\n\nexport type IdentityPermissionMatch = {\n  attr: IdentityAttribute\n}\n\nexport class IdentityPermission\n  implements ResourcePermission<'identity', IdentityPermissionMatch>\n{\n  constructor(public readonly attr: IdentityAttribute) {}\n\n  matches(options: IdentityPermissionMatch) {\n    return this.attr === '*' || this.attr === options.attr\n  }\n\n  toString() {\n    return IdentityPermission.parser.format(this)\n  }\n\n  protected static readonly parser = new Parser(\n    'identity',\n    {\n      attr: {\n        multiple: false,\n        required: true,\n        validate: knownValuesValidator(IDENTITY_ATTRIBUTES),\n      },\n    },\n    'attr',\n  )\n\n  static fromString(scope: string) {\n    if (!isScopeStringFor(scope, 'identity')) return null\n    const syntax = ScopeStringSyntax.fromString(scope)\n    return IdentityPermission.fromSyntax(syntax)\n  }\n\n  static fromSyntax(syntax: ScopeSyntax<'identity'>) {\n    const result = IdentityPermission.parser.parse(syntax)\n    if (!result) return null\n    return new IdentityPermission(result.attr)\n  }\n\n  static scopeNeededFor(options: IdentityPermissionMatch): string {\n    return IdentityPermission.parser.format(options)\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/include-scope.test.ts",
    "content": "import { ScopeStringFor } from '../lib/syntax'\nimport { LexPermissionSyntax } from '../lib/syntax-lexicon'\nimport { AccountPermission } from './account-permission'\nimport { IdentityPermission } from './identity-permission'\nimport { IncludeScope, LexiconPermissionSet } from './include-scope'\n\ndescribe('IncludeScope', () => {\n  describe('static', () => {\n    describe('fromString', () => {\n      describe('enables', () => {\n        it('parsing of positional nsid', () => {\n          expect(\n            IncludeScope.fromString('include:com.example.bar'),\n          ).toMatchObject({\n            nsid: 'com.example.bar',\n            aud: undefined,\n          })\n        })\n\n        it('parsing of positional nsid and aud param', () => {\n          expect(\n            IncludeScope.fromString(\n              'include:com.example.baz?aud=did:web:example.com%23my_service',\n            ),\n          ).toMatchObject({\n            nsid: 'com.example.baz',\n            aud: 'did:web:example.com#my_service',\n          })\n        })\n\n        it('parsing of # character in query string', () => {\n          expect(\n            IncludeScope.fromString(\n              'include:com.example.baz?aud=did:web:example.com#my_service',\n            ),\n          ).toMatchObject({\n            nsid: 'com.example.baz',\n            aud: 'did:web:example.com#my_service',\n          })\n        })\n\n        it('parsing of named nsid', () => {\n          expect(\n            IncludeScope.fromString('include?nsid=com.example.baz'),\n          ).toMatchObject({\n            nsid: 'com.example.baz',\n            aud: undefined,\n          })\n        })\n\n        it('parsing of named nsid and aud', () => {\n          expect(\n            IncludeScope.fromString(\n              'include?aud=did:web:example.com%23my_service&nsid=com.example.baz',\n            ),\n          ).toMatchObject({\n            nsid: 'com.example.baz',\n            aud: 'did:web:example.com#my_service',\n          })\n        })\n      })\n\n      describe('rejects', () => {\n        for (const invalid of [\n          '',\n          'repo:com.example.baz',\n          'include',\n          'include#',\n\n          // Invalid NSID\n          'include:',\n          'include:#',\n          'include:&',\n          'include:com..example',\n          'include:com',\n          'include:com.example',\n          'include:9com.example.foo',\n          'include:com.example.-bar',\n          'include:invalid^nsid',\n          'include:nsid',\n\n          // Invalid AUD\n          'include:com.example.baz?aud=',\n          'include:com.example.baz?aud=did:web:example.com',\n          'include:com.example.baz?aud=invalid^did',\n          'include:com.example.baz?aud=invalid^did',\n        ]) {\n          it(JSON.stringify(invalid), () => {\n            expect(IncludeScope.fromString(invalid)).toBeNull()\n          })\n        }\n      })\n    })\n  })\n\n  describe('instance', () => {\n    describe('toString', () => {\n      describe('enables', () => {\n        it('formating of scope without aud', () => {\n          expect(new IncludeScope('com.example.foo').toString()).toEqual(\n            'include:com.example.foo',\n          )\n        })\n        it('formating of scope with aud', () => {\n          expect(\n            new IncludeScope(\n              'com.example.foo',\n              'did:web:example.com#my_service',\n            ).toString(),\n          ).toEqual(\n            'include:com.example.foo?aud=did:web:example.com%23my_service',\n          )\n        })\n      })\n    })\n\n    describe('isParentAuthorityOf', () => {\n      const scope = new IncludeScope('com.example.foo.auth')\n\n      describe('enables', () => {\n        it('same authority', () => {\n          expect(scope.isParentAuthorityOf('com.example.foo.identifier')).toBe(\n            true,\n          )\n        })\n\n        it('child authorities', () => {\n          expect(scope.isParentAuthorityOf('com.example.foo.bar.baz')).toBe(\n            true,\n          )\n          expect(scope.isParentAuthorityOf('com.example.foo.bar.baz.quz')).toBe(\n            true,\n          )\n        })\n      })\n\n      describe('rejects', () => {\n        it('invalid nsids', () => {\n          // @ts-expect-error\n          expect(scope.isParentAuthorityOf('com')).toBe(false)\n          // @ts-expect-error\n          expect(scope.isParentAuthorityOf('com.example')).toBe(false)\n        })\n\n        it('siblings of root domain', () => {\n          expect(scope.isParentAuthorityOf('com.example.bar')).toBe(false)\n          expect(scope.isParentAuthorityOf('com.example.bar.foo')).toBe(false)\n          expect(scope.isParentAuthorityOf('com.example.bar.qux')).toBe(false)\n        })\n\n        it('other domains', () => {\n          expect(scope.isParentAuthorityOf('com.atproto.foo')).toBe(false)\n          expect(scope.isParentAuthorityOf('com.atproto.foo.auth')).toBe(false)\n          expect(scope.isParentAuthorityOf('com.atproto.foo.bar')).toBe(false)\n          expect(scope.isParentAuthorityOf('com.atproto.foo.bar')).toBe(false)\n        })\n      })\n    })\n\n    describe('buildPermissions', () => {\n      /**\n       * Utility that transforms a (valid) \"include:<nsid>\" scope and matching\n       * (resolved) permission set into the list of permission scopes.\n       */\n      const compilePermissions = (\n        scope: ScopeStringFor<'include'>,\n        permissionSet: LexiconPermissionSet,\n      ) => IncludeScope.fromString(scope)!.toScopes(permissionSet)\n\n      describe('blob', () => {\n        describe('rejects', () => {\n          it('valid permissions', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'blob',\n                    accept: ['image/*'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('invalid permissions', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'blob',\n                    accept: 'image/*',\n                  },\n                ],\n              }),\n            ).toEqual([])\n\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'blob',\n                    accept: ['image/*'],\n                    extra: 'property',\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n        })\n      })\n\n      describe('rpc', () => {\n        describe('enables', () => {\n          it('allows * aud', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: '*',\n                    lxm: ['com.example.calendar.listEvents'],\n                  },\n                ],\n              }),\n            ).toEqual(['rpc:com.example.calendar.listEvents?aud=*'])\n          })\n\n          it('inherits aud', () => {\n            expect(\n              compilePermissions(\n                'include:com.example.calendar.auth?aud=did:web:example.com#foo',\n                {\n                  type: 'permission-set',\n                  permissions: [\n                    {\n                      type: 'permission',\n                      resource: 'rpc',\n                      inheritAud: true,\n                      lxm: ['com.example.calendar.listEvents'],\n                    },\n                    {\n                      type: 'permission',\n                      resource: 'rpc',\n                      inheritAud: true,\n                      lxm: ['com.example.calendar.getEventDetails'],\n                    },\n                  ],\n                },\n              ),\n            ).toEqual([\n              'rpc:com.example.calendar.listEvents?aud=did:web:example.com%23foo',\n              'rpc:com.example.calendar.getEventDetails?aud=did:web:example.com%23foo',\n            ])\n          })\n        })\n\n        describe('rejects', () => {\n          it('forbids use of specific \"aud\"', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: 'did:web:example.com#foo',\n                    lxm: ['com.example.calendar.listEvents'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('invalid \"lxm\" syntax', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: 'did:web:example.com#foo',\n                    lxm: 'com.example.calendar.listEvents',\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('extra properties', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: 'did:web:example.com#foo',\n                    lxm: ['com.example.calendar.listEvents'],\n                    extra: 'property',\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('missing \"lxm\"', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: 'did:web:example.com#foo',\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('missing \"aud\"', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    lxm: ['com.example.calendar.listEvents'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('missing \"aud\" and \"lxm\"', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('both \"inheritAud\" and \"aud\" specified', () => {\n            expect(\n              compilePermissions(\n                'include:com.example.calendar.auth?aud=did:web:example.com#bar',\n                {\n                  type: 'permission-set',\n                  permissions: [\n                    {\n                      type: 'permission',\n                      resource: 'rpc',\n                      aud: 'did:web:example.com#foo',\n                      inheritAud: true,\n                      lxm: ['com.example.calendar.listEvents'],\n                    },\n                  ],\n                },\n              ),\n            ).toEqual([])\n          })\n\n          it('invalid authority', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: 'did:web:example.com#foo',\n                    lxm: ['com.atproto.moderation.createReport'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('un-specified inherited-aud', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    inheritAud: true,\n                    lxm: ['com.example.calendar.listEvents'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('wildcard-aud', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: '*',\n                    lxm: ['com.example.calendar.listEvents'],\n                  },\n                ],\n              }),\n            ).toEqual(['rpc:com.example.calendar.listEvents?aud=*'])\n          })\n\n          it('wildcard-aud for invalid authority', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'rpc',\n                    aud: '*',\n                    lxm: ['com.atproto.moderation.createReport'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n        })\n      })\n\n      describe('repo', () => {\n        describe('enabled', () => {\n          it('valid permission', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: ['com.example.calendar.event'],\n                    action: ['create', 'update', 'delete'],\n                  },\n                ],\n              }),\n            ).toEqual(['repo:com.example.calendar.event'])\n          })\n\n          it('valid permission with partial actions', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: ['com.example.calendar.event'],\n                    action: ['delete', 'update'],\n                  },\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: [\n                      'com.example.calendar.event',\n                      'com.example.calendar.rsvp',\n                    ],\n                    action: ['delete', 'create'],\n                  },\n                ],\n              }),\n            ).toEqual([\n              'repo:com.example.calendar.event?action=update&action=delete',\n              'repo?collection=com.example.calendar.event&collection=com.example.calendar.rsvp&action=create&action=delete',\n            ])\n          })\n        })\n\n        describe('rejects', () => {\n          it('invalid \"collection\" syntax', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: 'com.example.calendar.event',\n                    action: ['create', 'update', 'delete'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('invalid \"action\" syntax', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: ['com.example.calendar.event'],\n                    action: 'all',\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('invalid \"action\" values', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: ['com.example.calendar.event'],\n                    action: ['create', 'update', 'manage'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('invalid authority', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: ['app.bsky.feed.post'],\n                    action: ['create', 'update', 'delete'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n\n          it('permissions with one valid and one invalid authority', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [\n                  {\n                    type: 'permission',\n                    resource: 'repo',\n                    collection: [\n                      'com.example.calendar.event',\n                      'app.bsky.feed.post',\n                    ],\n                    action: ['create', 'update', 'delete'],\n                  },\n                ],\n              }),\n            ).toEqual([])\n          })\n        })\n      })\n\n      describe('account', () => {\n        const permission = {\n          type: 'permission',\n          resource: 'account',\n          attr: 'email',\n          action: ['read'],\n        } as const\n\n        it('parses valid permission syntax', () => {\n          // Just to make sure that the test bellow doesn't give a false negative\n          const syntax = new LexPermissionSyntax(permission)\n          expect(AccountPermission.fromSyntax(syntax)).toMatchObject({\n            constructor: AccountPermission,\n            attr: 'email',\n            action: ['read'],\n          })\n        })\n\n        describe('rejects', () => {\n          it('account permissions', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [permission],\n              }),\n            ).toEqual([])\n          })\n        })\n      })\n\n      describe('identity', () => {\n        const permission = {\n          type: 'permission',\n          resource: 'identity',\n          attr: 'handle',\n        } as const\n\n        it('parses valid permission syntax', () => {\n          // Just to make sure that the test bellow doesn't give a false negative\n          const syntax = new LexPermissionSyntax(permission)\n          expect(IdentityPermission.fromSyntax(syntax)).toMatchObject({\n            constructor: IdentityPermission,\n            attr: 'handle',\n          })\n        })\n\n        describe('rejects', () => {\n          it('identity permissions', () => {\n            expect(\n              compilePermissions('include:com.example.calendar.auth', {\n                type: 'permission-set',\n                permissions: [permission],\n              }),\n            ).toEqual([])\n          })\n        })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/include-scope.ts",
    "content": "import { AtprotoAudience, isAtprotoAudience } from '@atproto/did'\nimport { LexiconPermission, LexiconPermissionSet } from '../lib/lexicon.js'\nimport { Nsid, isNsid } from '../lib/nsid.js'\nimport { Parser } from '../lib/parser.js'\nimport { LexPermissionSyntax } from '../lib/syntax-lexicon.js'\nimport { ScopeStringSyntax } from '../lib/syntax-string.js'\nimport {\n  ScopeStringFor,\n  ScopeSyntax,\n  isScopeStringFor,\n  isScopeSyntaxFor,\n} from '../lib/syntax.js'\nimport { RepoPermission } from './repo-permission.js'\nimport { RpcPermission } from './rpc-permission.js'\n\nexport { type LexiconPermission, type LexiconPermissionSet, type Nsid, isNsid }\n\n/**\n * This is used to handle \"include:\" oauth scope values, used to include\n * permissions from a lexicon defined permission set. Not being a resource\n * permission, it does not implement `Matchable`.\n */\nexport class IncludeScope {\n  constructor(\n    public readonly nsid: Nsid,\n    public readonly aud: undefined | AtprotoAudience = undefined,\n  ) {}\n\n  toString() {\n    return IncludeScope.parser.format(this)\n  }\n\n  toPermissions(\n    permissionSet: LexiconPermissionSet,\n  ): Array<RepoPermission | RpcPermission> {\n    return Array.from(this.buildPermissions(permissionSet))\n  }\n\n  toScopes(\n    permissionSet: LexiconPermissionSet,\n  ): Array<ScopeStringFor<'repo' | 'rpc'>> {\n    return Array.from(this.buildPermissions(permissionSet), (p) => p.toString())\n  }\n\n  /**\n   * Converts an \"include:\" to the list of permissions it includes, based on the\n   * lexicon defined permission set.\n   */\n  *buildPermissions(\n    permissionSet: LexiconPermissionSet,\n  ): Generator<RepoPermission | RpcPermission, void, unknown> {\n    for (const lexPermission of permissionSet.permissions) {\n      const syntax = this.parseLexPermission(lexPermission)\n      if (!syntax) continue\n\n      const resourcePermission = toResourcePermission(syntax)\n      if (!resourcePermission) continue\n\n      if (this.isAllowedPermission(resourcePermission)) {\n        yield resourcePermission\n      }\n    }\n  }\n\n  protected parseLexPermission(\n    permission: LexiconPermission,\n  ): ScopeSyntax<'repo' | 'rpc'> | null {\n    // This function converts permissions listed in the permission set into\n    // their respective ScopeSyntax representations, handling special cases as\n    // needed.\n\n    if (isLexPermissionForResource(permission, 'repo')) {\n      return new LexPermissionSyntax(permission)\n    }\n\n    if (isLexPermissionForResource(permission, 'rpc')) {\n      // \"rpc\" permissions with a defined audience are not allowed in permission\n      // sets\n      if (permission.aud !== undefined && permission.aud !== '*') {\n        return null\n      }\n\n      // \"rpc\" permissions can \"inherit\" their audience from \"aud\" param defined\n      // in the \"include:<nsid>?aud=<audience>\" scope the permission set was\n      // loaded from.\n      if (\n        permission.inheritAud === true &&\n        permission.aud === undefined &&\n        this.aud !== undefined\n      ) {\n        const { inheritAud, ...rest } = permission\n        return new LexPermissionSyntax({ aud: this.aud, ...rest })\n      }\n\n      return new LexPermissionSyntax(permission)\n    }\n\n    return null\n  }\n\n  /**\n   * Verifies that a permission included through a lexicon permission set is\n   * allowed in the context of the `include:` scope. This basically checks that\n   * the permission is \"under\" the namespace authority of the `include:` scope,\n   * and that it only contains \"repo:\", \"rpc:\", or \"blob:\" permissions.\n   */\n  protected isAllowedPermission(\n    permission: RpcPermission | RepoPermission,\n  ): boolean {\n    if (permission instanceof RpcPermission) {\n      return permission.lxm.every(this.isParentAuthorityOf, this)\n    }\n\n    if (permission instanceof RepoPermission) {\n      return permission.collection.every(this.isParentAuthorityOf, this)\n    }\n\n    throw new TypeError(`Unexpected permission ${permission}`)\n  }\n\n  /**\n   * Verifies that a permission item's nsid is under the same authority as the\n   * nsid of the lexicon itself (which is the same as the nsid of the `include:`\n   * scope).\n   */\n  public isParentAuthorityOf(otherNsid: '*' | Nsid) {\n    if (otherNsid === '*') {\n      return false\n    }\n\n    const lexiconNsid = this.nsid\n\n    const groupPrefixEnd = lexiconNsid.lastIndexOf('.')\n\n    // There should always be a dot, but since this is a security feature, let's\n    // be strict about it.\n    if (groupPrefixEnd === -1) {\n      throw new TypeError('Dot character (\".\") missing from lexicon NSID')\n    }\n\n    // Make sure that otherNsid is at least as long as the \"group prefix\"\n    if (groupPrefixEnd >= otherNsid.length - 1) {\n      return false\n    }\n\n    // Make sure that the \"otherNsid\" starts with the group of the lexiconNsid,\n    // up to the dot itself. We check in reverse order as nsids tend to have\n    // long common prefixes.\n    for (let i = groupPrefixEnd; i >= 0; i--) {\n      if (lexiconNsid.charCodeAt(i) !== otherNsid.charCodeAt(i)) {\n        return false\n      }\n    }\n\n    return true\n  }\n\n  protected static readonly parser = new Parser(\n    'include',\n    {\n      nsid: {\n        multiple: false,\n        required: true,\n        validate: isNsid,\n      },\n      aud: {\n        multiple: false,\n        required: false,\n        validate: isAtprotoAudience,\n      },\n    },\n    'nsid',\n  )\n\n  static fromString(scope: string) {\n    if (!isScopeStringFor(scope, 'include')) return null\n    const syntax = ScopeStringSyntax.fromString(scope)\n    return IncludeScope.fromSyntax(syntax)\n  }\n\n  static fromSyntax(syntax: ScopeSyntax<'include'>) {\n    const result = IncludeScope.parser.parse(syntax)\n    if (!result) return null\n    return new IncludeScope(result.nsid, result.aud)\n  }\n}\n\nfunction toResourcePermission(\n  syntax: ScopeSyntax<'repo' | 'rpc'>,\n): RepoPermission | RpcPermission | null {\n  if (isScopeSyntaxFor(syntax, 'repo')) {\n    return RepoPermission.fromSyntax(syntax)\n  }\n  if (isScopeSyntaxFor(syntax, 'rpc')) {\n    return RpcPermission.fromSyntax(syntax)\n  }\n  return null\n}\n\nfunction isLexPermissionForResource<\n  P extends { resource: unknown },\n  T extends string,\n>(permission: P, type: T): permission is P & { resource: T } {\n  return permission.resource === type\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/repo-permission.test.ts",
    "content": "import { RepoPermission } from './repo-permission.js'\n\ndescribe('RepoPermission', () => {\n  describe('static', () => {\n    describe('fromString', () => {\n      it('should parse positional scope', () => {\n        const scope = RepoPermission.fromString('repo:com.example.foo')\n        expect(scope).not.toBeNull()\n        expect(scope!.collection).toEqual(['com.example.foo'])\n        expect(scope!.action).toEqual(['create', 'update', 'delete'])\n      })\n\n      it('should parse valid repo scope with multiple actions', () => {\n        const scope = RepoPermission.fromString(\n          'repo:com.example.foo?action=create&action=update',\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.collection).toEqual(['com.example.foo'])\n        expect(scope!.action).toEqual(['create', 'update'])\n      })\n\n      it('should parse valid repo scope without actions (defaults to create, update, delete)', () => {\n        const scope = RepoPermission.fromString('repo:com.example.foo')\n        expect(scope).not.toBeNull()\n        expect(scope!.collection).toEqual(['com.example.foo'])\n        expect(scope!.action).toEqual(['create', 'update', 'delete'])\n      })\n\n      it('should allow wildcard collection with specific action', () => {\n        const scope = RepoPermission.fromString('repo:*?action=create')\n        expect(scope).not.toBeNull()\n        expect(scope!.collection).toEqual(['*'])\n        expect(scope!.action).toEqual(['create'])\n        expect(\n          scope!.matches({ action: 'create', collection: 'any.collection' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'update', collection: 'any.collection' }),\n        ).toBe(false)\n      })\n\n      it('should allow wildcard collection without actions', () => {\n        const scope = RepoPermission.fromString('repo:*')\n        expect(scope).not.toBeNull()\n        expect(scope!.collection).toEqual(['*'])\n        expect(scope!.action).toEqual(['create', 'update', 'delete'])\n        expect(\n          scope!.matches({ action: 'create', collection: 'any.collection' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'update', collection: 'any.collection' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'delete', collection: 'any.collection' }),\n        ).toBe(true)\n      })\n\n      it('should ignore scopes with invalid collection names', () => {\n        expect(RepoPermission.fromString('repo:foo bar')).toBeNull()\n        expect(RepoPermission.fromString('repo:.foo')).toBeNull()\n        expect(RepoPermission.fromString('repo:bar.')).toBeNull()\n      })\n\n      it('should reject invalid action names', () => {\n        const scope = RepoPermission.fromString(\n          'repo:com.example.foo?action=invalid',\n        )\n        expect(scope).toBeNull()\n      })\n\n      it('should return null for invalid repo scope', () => {\n        expect(RepoPermission.fromString('invalid')).toBeNull()\n        expect(RepoPermission.fromString('scope')).toBeNull()\n      })\n\n      for (const invalid of [\n        'repo:*?action=*',\n        'invalid',\n        'repo:invalid',\n        'repo:com.example.foo?action=invalid',\n        'repo?collection=invalid&action=invalid',\n      ]) {\n        it(`should return null for invalid rpc scope: ${invalid}`, () => {\n          expect(RepoPermission.fromString(invalid)).toBeNull()\n        })\n      }\n    })\n\n    describe('scopeNeededFor', () => {\n      it('should return correct scope string for specific collection and action', () => {\n        const scope = RepoPermission.scopeNeededFor({\n          collection: 'com.example.foo',\n          action: 'create',\n        })\n        expect(scope).toBe('repo:com.example.foo?action=create')\n      })\n\n      it('should return scope that accepts all collections with specific action', () => {\n        const scope = RepoPermission.scopeNeededFor({\n          collection: '*',\n          action: 'create',\n        })\n        expect(scope).toBe('repo:*?action=create')\n      })\n\n      it('ignores invalid options', () => {\n        // @NOTE the scopeNeededFor assumes valid input, so it does not validate\n        // collection or action.\n\n        expect(\n          RepoPermission.scopeNeededFor({\n            collection: 'invalid',\n            action: 'create',\n          }),\n        ).toBe('repo:invalid?action=create')\n      })\n    })\n  })\n\n  describe('instance', () => {\n    describe('matches', () => {\n      it('should match create action', () => {\n        const scope = RepoPermission.fromString(\n          'repo:com.example.foo?action=create',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({ action: 'create', collection: 'com.example.foo' }),\n        ).toBe(true)\n      })\n\n      it('should not match unspecified action', () => {\n        const scope = RepoPermission.fromString(\n          'repo:com.example.foo?action=create',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({ action: 'update', collection: 'com.example.foo' }),\n        ).toBe(false)\n      })\n\n      it('should match wildcard collection', () => {\n        const scope = RepoPermission.fromString('repo:*?action=create')\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({ action: 'create', collection: 'com.example.bar' }),\n        ).toBe(true)\n      })\n\n      it('should not match different action with wildcard collection', () => {\n        const scope = RepoPermission.fromString('repo:*?action=create')\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({ action: 'delete', collection: 'com.example.bar' }),\n        ).toBe(false)\n      })\n\n      it('should match multiple actions', () => {\n        const scope = RepoPermission.fromString(\n          'repo:com.example.foo?action=create&action=update',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({ action: 'create', collection: 'com.example.foo' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'update', collection: 'com.example.foo' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'delete', collection: 'com.example.foo' }),\n        ).toBe(false)\n      })\n\n      it('should default to \"create\", \"update\", and \"delete\" actions', () => {\n        const scope = RepoPermission.fromString('repo:com.example.foo')\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({ action: 'create', collection: 'com.example.foo' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'update', collection: 'com.example.foo' }),\n        ).toBe(true)\n        expect(\n          scope!.matches({ action: 'delete', collection: 'com.example.foo' }),\n        ).toBe(true)\n      })\n    })\n\n    describe('toString', () => {\n      it('should format repo scope correctly', () => {\n        const scope = new RepoPermission(\n          ['com.example.foo'],\n          ['create', 'update'],\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.toString()).toBe(\n          'repo:com.example.foo?action=create&action=update',\n        )\n      })\n    })\n  })\n\n  describe('consistency', () => {\n    const testCases: { input: string; expected: string }[] = [\n      { input: 'repo:com.example.foo', expected: 'repo:com.example.foo' },\n      {\n        input: 'repo:com.example.foo?action=create',\n        expected: 'repo:com.example.foo?action=create',\n      },\n      {\n        input: 'repo:com.example.foo?action=create&action=update',\n        expected: 'repo:com.example.foo?action=create&action=update',\n      },\n      {\n        input: 'repo:*?action=create&action=update&action=delete',\n        expected: 'repo:*',\n      },\n      {\n        input: 'repo:com.example.foo?action=create&action=update&action=delete',\n        expected: 'repo:com.example.foo',\n      },\n      { input: 'repo:*?action=create', expected: 'repo:*?action=create' },\n      { input: 'repo:*?action=update', expected: 'repo:*?action=update' },\n      {\n        input: 'repo?collection=*&action=update',\n        expected: 'repo:*?action=update',\n      },\n      {\n        input: 'repo?collection=*&collection=com.example.foo&action=update',\n        expected: 'repo:*?action=update',\n      },\n      {\n        input: 'repo?collection=*',\n        expected: 'repo:*',\n      },\n      {\n        input: 'repo?collection=*&action=create&action=update&action=delete',\n        expected: 'repo:*',\n      },\n      {\n        input: 'repo?collection=*&collection=com.example.foo',\n        expected: 'repo:*',\n      },\n      {\n        input: 'repo?action=create&collection=com.example.foo',\n        expected: 'repo:com.example.foo?action=create',\n      },\n      {\n        input:\n          'repo?collection=com.example.foo&action=create&action=update&action=delete',\n        expected: 'repo:com.example.foo',\n      },\n      {\n        input:\n          'repo?action=create&collection=com.example.foo&collection=com.example.bar',\n        expected:\n          'repo?collection=com.example.bar&collection=com.example.foo&action=create',\n      },\n    ]\n\n    for (const { input, expected } of testCases) {\n      it(`should properly re-format ${input}`, () => {\n        expect(RepoPermission.fromString(input)?.toString()).toBe(expected)\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/repo-permission.ts",
    "content": "import { Nsid, isNsid } from '../lib/nsid.js'\nimport { Parser } from '../lib/parser.js'\nimport { ResourcePermission } from '../lib/resource-permission.js'\nimport { ScopeStringSyntax } from '../lib/syntax-string.js'\nimport {\n  NeArray,\n  NeRoArray,\n  ScopeSyntax,\n  isScopeStringFor,\n} from '../lib/syntax.js'\nimport { knownValuesValidator } from '../lib/util.js'\n\nexport { type Nsid, isNsid }\n\nexport const REPO_ACTIONS = Object.freeze([\n  'create',\n  'update',\n  'delete',\n] as const)\nexport type RepoAction = (typeof REPO_ACTIONS)[number]\nexport const isRepoAction = knownValuesValidator(REPO_ACTIONS)\n\nexport type CollectionParam = '*' | Nsid\nexport const isCollectionParam = (value: unknown): value is CollectionParam =>\n  value === '*' || isNsid(value)\n\nexport type RepoPermissionMatch = {\n  collection: string\n  action: RepoAction\n}\n\nexport class RepoPermission\n  implements ResourcePermission<'repo', RepoPermissionMatch>\n{\n  constructor(\n    public readonly collection: NeRoArray<'*' | Nsid>,\n    public readonly action: NeRoArray<RepoAction>,\n  ) {}\n\n  matches({ action, collection }: RepoPermissionMatch) {\n    return (\n      this.action.includes(action) &&\n      (this.collection.includes('*') ||\n        (this.collection as readonly string[]).includes(collection))\n    )\n  }\n\n  toString() {\n    return RepoPermission.parser.format(this)\n  }\n\n  protected static readonly parser = new Parser(\n    'repo',\n    {\n      collection: {\n        multiple: true,\n        required: true,\n        validate: isCollectionParam,\n        normalize: (value) => {\n          if (value.length > 1) {\n            if (value.includes('*')) return ['*'] as const\n            return [...new Set(value)].sort() as NeArray<Nsid>\n          }\n          return value as ['*' | Nsid]\n        },\n      },\n      action: {\n        multiple: true,\n        required: false,\n        validate: isRepoAction,\n        default: REPO_ACTIONS,\n        normalize: (value) => {\n          return value === REPO_ACTIONS\n            ? REPO_ACTIONS // No need to filter if the default was used\n            : (REPO_ACTIONS.filter(includedIn, value) as NeArray<RepoAction>)\n        },\n      },\n    },\n    'collection',\n  )\n\n  static fromString(scope: string): RepoPermission | null {\n    if (!isScopeStringFor(scope, 'repo')) return null\n    const syntax = ScopeStringSyntax.fromString(scope)\n    return RepoPermission.fromSyntax(syntax)\n  }\n\n  static fromSyntax(syntax: ScopeSyntax<'repo'>): RepoPermission | null {\n    const result = RepoPermission.parser.parse(syntax)\n    if (!result) return null\n\n    return new RepoPermission(result.collection, result.action)\n  }\n\n  static scopeNeededFor(options: RepoPermissionMatch): string {\n    return RepoPermission.parser.format({\n      collection: [options.collection as '*' | Nsid],\n      action: [options.action],\n    })\n  }\n}\n\n/**\n * Special utility function to be used as predicate for array methods like\n * `Array.prototype.includes`, etc. When used as predicate, it expects that\n * the array method is called with a `thisArg` that is a readonly array of\n * the same type as the `value` parameter.\n */\nfunction includedIn<T>(this: readonly T[], value: T): boolean {\n  return this.includes(value)\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/rpc-permission.test.ts",
    "content": "import { RpcPermission } from './rpc-permission.js'\n\ndescribe('RpcPermission', () => {\n  describe('static', () => {\n    describe('fromString', () => {\n      it('should parse positional scope', () => {\n        const scope = RpcPermission.fromString(\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.aud).toBe('did:web:example.com#service_id')\n        expect(scope!.lxm).toEqual(['com.example.service'])\n      })\n\n      it('should parse strings correctly', () => {\n        expect(\n          RpcPermission.fromString('rpc?lxm=com.example.method1&aud=*'),\n        ).toEqual({\n          aud: '*',\n          lxm: ['com.example.method1'],\n        })\n        expect(\n          RpcPermission.fromString('rpc:com.example.method1?aud=*'),\n        ).toEqual({\n          aud: '*',\n          lxm: ['com.example.method1'],\n        })\n      })\n\n      it('should render strings correctly', () => {\n        expect(\n          new RpcPermission('did:web:example.com#service_id', [\n            'com.example.service',\n          ]).toString(),\n        ).toBe('rpc:com.example.service?aud=did:web:example.com%23service_id')\n        expect(new RpcPermission('*', ['com.example.method1']).toString()).toBe(\n          'rpc:com.example.method1?aud=*',\n        )\n        expect(\n          new RpcPermission('did:web:example.com#service_id', [\n            'com.example.method1',\n            'com.example.method2',\n          ]).toString(),\n        ).toBe(\n          'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:web:example.com%23service_id',\n        )\n      })\n\n      it('should reject scopes without lxm', () => {\n        expect(\n          RpcPermission.fromString('rpc?aud=did:web:example.com%23service_id'),\n        ).toBeNull()\n        expect(\n          RpcPermission.fromString('rpc:?aud=did:web:example.com%23service_id'),\n        ).toBeNull()\n      })\n\n      it('should reject scopes without aud', () => {\n        expect(\n          RpcPermission.fromString('rpc?lxm=com.example.method1'),\n        ).toBeNull()\n        expect(RpcPermission.fromString('rpc:com.example.method1')).toBeNull()\n      })\n\n      it('should reject scopes with lxm in both positional and query form', () => {\n        expect(\n          RpcPermission.fromString(\n            'rpc:com.example.method1?aud=did:web:example.com&lxm=com.example.method2',\n          ),\n        ).toBeNull()\n      })\n\n      it('should parse valid rpc scope with multiple lxm', () => {\n        const scope = RpcPermission.fromString(\n          'rpc?aud=*&lxm=com.example.method1&lxm=com.example.method2',\n        )\n        expect(scope).not.toBeNull()\n        expect(scope!.aud).toBe('*')\n        expect(scope!.lxm).toEqual([\n          'com.example.method1',\n          'com.example.method2',\n        ])\n      })\n\n      it('should reject rpc scope without lxm', () => {\n        const scope = RpcPermission.fromString('rpc?aud=did:web:example.com')\n        expect(scope).toBeNull()\n      })\n\n      it('should reject rpc scope without aud', () => {\n        const scope = RpcPermission.fromString('rpc?lxm=com.example.method1')\n        expect(scope).toBeNull()\n      })\n\n      it('should reject any aud/any lxm', () => {\n        expect(RpcPermission.fromString('rpc?aud=*&lxm=*')).toBeNull()\n        expect(RpcPermission.fromString('rpc:*?aud=*')).toBeNull()\n      })\n\n      it('should reject missing aud', () => {\n        expect(RpcPermission.fromString('rpc:com.example.service')).toBeNull()\n      })\n\n      it('should reject invalid aud', () => {\n        expect(\n          RpcPermission.fromString('rpc:com.example.service?aud=invalid'),\n        ).toBeNull()\n      })\n\n      it('should reject invalid lxm', () => {\n        expect(RpcPermission.fromString('rpc:invalid')).toBeNull()\n        expect(RpcPermission.fromString('rpc?lxm=invalid')).toBeNull()\n      })\n\n      for (const invalid of [\n        'rpc:*',\n        'invalid',\n        'rpc:invalid',\n        'rpc:invalid?aud=did:web:example.com',\n        'rpc:invalid?aud=did:web:example.com%23service_id',\n        'rpc:foo.bar',\n        'rpc:com.example.service?aud=did:web:example.com%23service_id&invalid=param',\n        'rpc:foo.bar.baz?aud=did:web',\n        'rpc:foo.bar.baz?aud=did:web%23service_id',\n        'rpc:foo.bar.baz?aud=did:plc:111',\n        'rpc:foo.bar.baz?aud=did:plc:111%23service_id',\n        'rpc:foo.bar.baz?aud=did:foo:bar',\n        'rpc:foo.bar.baz?aud=did:foo:bar%23service_id',\n        'rpc:foo.bar.baz?aud=did:web:example.com%23service_id&lxm=foo.bar.baz',\n        'rpc:foo.bar.baz?aud=invalid',\n        'rpc:foo.bar.baz?aud=invalid',\n        'rpc:invalid?aud=did:web:example.com',\n        'rpc:invalid?aud=did:web:example.com%23service_id',\n        'rpc:com.example.service?aud=invalid',\n        'notrpc:com.example.service?aud=did:web:example.com%23service_id',\n        'rpc?lxm=invalid&aud=invalid',\n      ]) {\n        it(`should return null for invalid rpc scope: ${invalid}`, () => {\n          expect(RpcPermission.fromString(invalid)).toBeNull()\n        })\n      }\n    })\n\n    describe('scopeNeededFor', () => {\n      it('should return correct scope string for specific lxm and aud', () => {\n        const scope = RpcPermission.scopeNeededFor({\n          lxm: 'com.example.service',\n          aud: 'did:web:example.com#service_id',\n        })\n        expect(scope).toBe(\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        )\n      })\n\n      it('should return scope that accepts all aud with specific lxm', () => {\n        const scope = RpcPermission.scopeNeededFor({\n          lxm: 'com.example.method1',\n          aud: '*',\n        })\n        expect(scope).toBe('rpc:com.example.method1?aud=*')\n      })\n    })\n  })\n\n  describe('instance', () => {\n    describe('matches', () => {\n      it('should match exact lxm and aud', () => {\n        const scope = RpcPermission.fromString(\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({\n            lxm: 'com.example.service',\n            aud: 'did:web:example.com#service_id',\n          }),\n        ).toBe(true)\n      })\n\n      it('should not match different lxm', () => {\n        const scope = RpcPermission.fromString(\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({\n            lxm: 'com.example.OtherService',\n            aud: 'did:web:example.com#service_id',\n          }),\n        ).toBe(false)\n      })\n\n      it('should not match different aud', () => {\n        const scope = RpcPermission.fromString(\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({\n            lxm: 'com.example.service',\n            aud: 'did:example:456#service_id',\n          }),\n        ).toBe(false)\n      })\n\n      it('should match wildcard aud', () => {\n        const scope = RpcPermission.fromString('rpc:com.example.method1?aud=*')\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({\n            lxm: 'com.example.method1',\n            aud: 'did:web:example.com#service_id',\n          }),\n        ).toBe(true)\n      })\n\n      it('should match wildcard lxm', () => {\n        const scope = RpcPermission.fromString(\n          'rpc:*?aud=did:web:example.com%23service_id',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({\n            lxm: 'com.example.method1',\n            aud: 'did:web:example.com#service_id',\n          }),\n        ).toBe(true)\n      })\n\n      it('should not match different lxm with wildcard aud', () => {\n        const scope = RpcPermission.fromString(\n          'rpc:*?aud=did:web:example.com%23service_id',\n        )\n        expect(scope).not.toBeNull()\n        expect(\n          scope!.matches({\n            lxm: 'com.example.anyMethod',\n            aud: 'did:web:example.com#service_id',\n          }),\n        ).toBe(true)\n      })\n    })\n\n    describe('toString', () => {\n      it('should format scope with lxm and aud', () => {\n        const scope = new RpcPermission('did:web:example.com#service_id', [\n          'com.example.service',\n        ])\n        expect(scope.toString()).toBe(\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        )\n      })\n\n      it('should format scope with wildcard aud', () => {\n        const scope = new RpcPermission('*', ['com.example.method1'])\n        expect(scope.toString()).toBe('rpc:com.example.method1?aud=*')\n      })\n\n      it('should format scope with wildcard lxm', () => {\n        const scope = new RpcPermission('did:web:example.com#service_id', ['*'])\n        expect(scope.toString()).toBe(\n          'rpc:*?aud=did:web:example.com%23service_id',\n        )\n      })\n\n      it('simplifies lxm if one of them is \"*\"', () => {\n        const scope = new RpcPermission('did:web:example.com#service_id', [\n          '*',\n          'com.example.method1',\n        ])\n        expect(scope.toString()).toBe(\n          'rpc:*?aud=did:web:example.com%23service_id',\n        )\n      })\n    })\n  })\n\n  describe('consistency', () => {\n    const testCases: { input: string; expected: string }[] = [\n      {\n        input: 'rpc:com.example.service?aud=did:web:example.com%23service_id',\n        expected:\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n      },\n      {\n        input: 'rpc:com.example.service?aud=did:web:example.com#service_id',\n        expected:\n          'rpc:com.example.service?aud=did:web:example.com%23service_id',\n      },\n      {\n        input: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',\n        expected: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',\n      },\n      {\n        input:\n          'rpc?lxm=com.example.method1&lxm=com.example.method2&lxm=*&aud=did:web:example.com%23service_id',\n        expected: 'rpc:*?aud=did:web:example.com%23service_id',\n      },\n      {\n        input: 'rpc?aud=did:web:example.com%23foo&lxm=com.example.service',\n        expected: 'rpc:com.example.service?aud=did:web:example.com%23foo',\n      },\n      {\n        input: 'rpc?lxm=com.example.method1&aud=did:web:example.com#foo',\n        expected: 'rpc:com.example.method1?aud=did:web:example.com%23foo',\n      },\n      {\n        input: 'rpc?lxm=com.example.method1&aud=did:web:example.com%23bar',\n        expected: 'rpc:com.example.method1?aud=did:web:example.com%23bar',\n      },\n      {\n        input: 'rpc:com.example.method1?&aud=*',\n        expected: 'rpc:com.example.method1?aud=*',\n      },\n    ]\n\n    for (const { input, expected } of testCases) {\n      it(`should properly re-format ${input}`, () => {\n        expect(RpcPermission.fromString(input)?.toString()).toBe(expected)\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes/rpc-permission.ts",
    "content": "import { AtprotoAudience, isAtprotoAudience } from '@atproto/did'\nimport { Nsid, isNsid } from '../lib/nsid.js'\nimport { Parser } from '../lib/parser.js'\nimport { ResourcePermission } from '../lib/resource-permission.js'\nimport { ScopeStringSyntax } from '../lib/syntax-string.js'\nimport { NeRoArray, ScopeSyntax, isScopeStringFor } from '../lib/syntax.js'\n\nexport { type AtprotoAudience, type Nsid, isAtprotoAudience, isNsid }\n\nexport type LxmParam = '*' | Nsid\nexport const isLxmParam = (value: unknown): value is LxmParam =>\n  value === '*' || isNsid(value)\nexport type AudParam = '*' | AtprotoAudience\nexport const isAudParam = (value: unknown): value is AudParam =>\n  value === '*' || isAtprotoAudience(value)\n\nexport type RpcPermissionMatch = {\n  lxm: string\n  aud: string\n}\n\nexport class RpcPermission\n  implements ResourcePermission<'rpc', RpcPermissionMatch>\n{\n  constructor(\n    public readonly aud: '*' | AtprotoAudience,\n    public readonly lxm: NeRoArray<'*' | Nsid>,\n  ) {}\n\n  matches(options: RpcPermissionMatch) {\n    const { aud, lxm } = this\n    return (\n      (aud === '*' || aud === options.aud) &&\n      (lxm.includes('*') || (lxm as readonly string[]).includes(options.lxm))\n    )\n  }\n\n  toString() {\n    return RpcPermission.parser.format(this)\n  }\n\n  protected static readonly parser = new Parser(\n    'rpc',\n    {\n      lxm: {\n        multiple: true,\n        required: true,\n        validate: isLxmParam,\n        normalize: (value) =>\n          value.length > 1 && value.includes('*')\n            ? (['*'] as const)\n            : ([...new Set(value)].sort() as [Nsid, ...Nsid[]]),\n      },\n      aud: {\n        multiple: false,\n        required: true,\n        validate: isAudParam,\n      },\n    },\n    'lxm',\n  )\n\n  static fromString(scope: string): RpcPermission | null {\n    if (!isScopeStringFor(scope, 'rpc')) return null\n    const syntax = ScopeStringSyntax.fromString(scope)\n    return RpcPermission.fromSyntax(syntax)\n  }\n\n  static fromSyntax(syntax: ScopeSyntax<'rpc'>): RpcPermission | null {\n    const result = RpcPermission.parser.parse(syntax)\n    if (!result) return null\n\n    // rpc:*?aud=* is forbidden\n    if (result.aud === '*' && result.lxm.includes('*')) return null\n\n    return new RpcPermission(result.aud, result.lxm)\n  }\n\n  static scopeNeededFor(options: RpcPermissionMatch): string {\n    return RpcPermission.parser.format({\n      aud: options.aud as AtprotoAudience,\n      lxm: [options.lxm as Nsid],\n    })\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes-set.test.ts",
    "content": "import { ScopesSet } from './scopes-set.js'\n\ndescribe('ScopesSet', () => {\n  it('should initialize with an empty set', () => {\n    const set = new ScopesSet()\n    expect(set.size).toBe(0)\n  })\n\n  it('should add scopes correctly', () => {\n    const set = new ScopesSet()\n    set.add('repo:read')\n    expect(set.size).toBe(1)\n    expect(set.has('repo:read')).toBe(true)\n    expect(set.has('repo:write')).toBe(false)\n  })\n\n  it('should remove scopes correctly', () => {\n    const set = new ScopesSet(['repo:read'])\n    set.delete('repo:read')\n    expect(set.size).toBe(0)\n    expect(set.has('repo:read')).toBe(false)\n  })\n\n  it('should match included scopes', () => {\n    const set = new ScopesSet(['repo:com.example.foo'])\n    expect(\n      set.matches('repo', { action: 'create', collection: 'com.example.foo' }),\n    ).toBe(true)\n    expect(\n      set.matches('repo', { action: 'create', collection: 'com.example.bar' }),\n    ).toBe(false)\n  })\n\n  it('should not match missing scopes', () => {\n    const set = new ScopesSet(['repo:com.example.foo?action=create'])\n    expect(\n      set.matches('repo', { action: 'delete', collection: 'com.example.foo' }),\n    ).toBe(false)\n  })\n\n  it('should not match invalid scopes', () => {\n    const set = new ScopesSet(['repo:not-a-valid-nsid'])\n    expect(\n      set.matches('repo', { action: 'create', collection: 'not-a-valid-nsid' }),\n    ).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/src/scopes-set.ts",
    "content": "import { ScopeMissingError } from './scope-missing-error.js'\nimport {\n  AccountPermission,\n  AccountPermissionMatch,\n} from './scopes/account-permission.js'\nimport {\n  BlobPermission,\n  BlobPermissionMatch,\n} from './scopes/blob-permission.js'\nimport {\n  IdentityPermission,\n  IdentityPermissionMatch,\n} from './scopes/identity-permission.js'\nimport {\n  RepoPermission,\n  RepoPermissionMatch,\n} from './scopes/repo-permission.js'\nimport { RpcPermission, RpcPermissionMatch } from './scopes/rpc-permission.js'\n\nexport { ScopeMissingError }\n\nexport type ScopeMatchingOptionsByResource = {\n  account: AccountPermissionMatch\n  identity: IdentityPermissionMatch\n  repo: RepoPermissionMatch\n  rpc: RpcPermissionMatch\n  blob: BlobPermissionMatch\n}\n\n/**\n * Utility class to manage a set of scopes and check if they match specific\n * options for a given resource.\n */\nexport class ScopesSet extends Set<string> {\n  /**\n   * Check if the container has a scope that matches the given options for a\n   * specific resource.\n   */\n  public matches<R extends keyof ScopeMatchingOptionsByResource>(\n    resource: R,\n    options: ScopeMatchingOptionsByResource[R],\n  ): boolean {\n    for (const scope of this) {\n      if (permissionScopeMatches(scope, resource, options)) return true\n    }\n    return false\n  }\n\n  public assert<R extends keyof ScopeMatchingOptionsByResource>(\n    resource: R,\n    options: ScopeMatchingOptionsByResource[R],\n  ) {\n    if (!this.matches(resource, options)) {\n      const scope = scopeNeededFor(resource, options)\n      throw new ScopeMissingError(scope)\n    }\n  }\n\n  public some(fn: (scope: string) => boolean): boolean {\n    for (const scope of this) if (fn(scope)) return true\n    return false\n  }\n\n  public every(fn: (scope: string) => boolean): boolean {\n    for (const scope of this) if (!fn(scope)) return false\n    return true\n  }\n\n  public *filter(fn: (scope: string) => boolean) {\n    for (const scope of this) if (fn(scope)) yield scope\n  }\n\n  public *map<O>(fn: (scope: string) => O) {\n    for (const scope of this) yield fn(scope)\n  }\n\n  static fromString(string?: string): ScopesSet {\n    return new ScopesSet(string?.split(' '))\n  }\n}\n\nfunction scopeNeededFor<R extends keyof ScopeMatchingOptionsByResource>(\n  resource: R,\n  options: ScopeMatchingOptionsByResource[R],\n): string {\n  switch (resource) {\n    case 'account':\n      return AccountPermission.scopeNeededFor(options as AccountPermissionMatch)\n    case 'identity':\n      return IdentityPermission.scopeNeededFor(\n        options as IdentityPermissionMatch,\n      )\n    case 'repo':\n      return RepoPermission.scopeNeededFor(options as RepoPermissionMatch)\n    case 'rpc':\n      return RpcPermission.scopeNeededFor(options as RpcPermissionMatch)\n    case 'blob':\n      return BlobPermission.scopeNeededFor(options as BlobPermissionMatch)\n  }\n  // @ts-expect-error\n  throw new TypeError(`Unknown resource: ${resource}`)\n}\n\nfunction permissionScopeMatches<R extends keyof ScopeMatchingOptionsByResource>(\n  scope: string,\n  resource: R,\n  options: ScopeMatchingOptionsByResource[R],\n): boolean {\n  // @NOTE we might want to cache the parsed scopes though, in practice, a\n  // single scope is unlikely to be parsed multiple times during a single\n  // request.\n  const permission = parsePermissionScope(resource, scope)\n  if (!permission) return false\n\n  // @ts-expect-error\n  return permission.matches(options)\n}\n\nfunction parsePermissionScope(resource: string, scope: string) {\n  switch (resource) {\n    case 'account':\n      return AccountPermission.fromString(scope)\n    case 'identity':\n      return IdentityPermission.fromString(scope)\n    case 'repo':\n      return RepoPermission.fromString(scope)\n    case 'rpc':\n      return RpcPermission.fromString(scope)\n    case 'blob':\n      return BlobPermission.fromString(scope)\n    default:\n      return null\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/tsconfig.build.json",
    "content": "{\n  \"extends\": [\"../../../tsconfig/isomorphic.json\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"**/*.test.ts\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-scopes/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/CHANGELOG.md",
    "content": "# @atproto/oauth-types\n\n## 0.6.3\n\n### Patch Changes\n\n- [#4620](https://github.com/bluesky-social/atproto/pull/4620) [`fdbbff8`](https://github.com/bluesky-social/atproto/commit/fdbbff854363ed3518a4039ca43cd279e69600e0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure that `OAuthAuthorizationRequestQuery` always contains a `client_id`\n\n- [#4620](https://github.com/bluesky-social/atproto/pull/4620) [`fdbbff8`](https://github.com/bluesky-social/atproto/commit/fdbbff854363ed3518a4039ca43cd279e69600e0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `oauthRequestUriSchema` to require min length\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [[`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e), [`d54d707`](https://github.com/bluesky-social/atproto/commit/d54d7077eb32041e1f61c312efa1dd0d768c774e)]:\n  - @atproto/did@0.3.0\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/did@0.2.4\n\n## 0.6.0\n\n### Minor Changes\n\n- [#4461](https://github.com/bluesky-social/atproto/pull/4461) [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Add prompt_values_supported to Authorization Server Metadata\n\n### Patch Changes\n\n- [#4465](https://github.com/bluesky-social/atproto/pull/4465) [`95ef3c2`](https://github.com/bluesky-social/atproto/commit/95ef3c24e8072e9d49412950b033cb8607764ee0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error message in case of invalid redirect uri\n\n## 0.5.2\n\n### Patch Changes\n\n- Updated dependencies [[`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea)]:\n  - @atproto/did@0.2.3\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/did@0.2.2\n\n## 0.5.0\n\n### Minor Changes\n\n- [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove exported `oauthHttpsRedirectURISchema` and `oauthPrivateUseRedirectURISchema` in favor of `oauthRedirectUriSchema` and `oauthLoopbackClientRedirectUriSchema`, which provide semantically meaningful groupings of redirect URIs based on OAuth client types.\n\n  `oauthHttpsRedirectURISchema` can still be accessed using `httpsUriSchema`.\n  `oauthPrivateUseRedirectURISchema` can still be accessed using `privateUseUriSchema`.\n\n- [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unused `isLoopbackUrl` utility\n\n### Patch Changes\n\n- [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce stronger validation of `privateUseUriSchema`\n\n## 0.4.2\n\n### Patch Changes\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose new utilities to build and work with atproto loopback client metadata\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve validation of `AtprotoOAuthScope`\n\n- [#4216](https://github.com/bluesky-social/atproto/pull/4216) [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `AtprotoOAuthTokenResponse` schema\n\n- Updated dependencies [[`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`f560cf226`](https://github.com/bluesky-social/atproto/commit/f560cf2266715666ce5852ab095fcfb3876ae815), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:\n  - @atproto/jwk@0.6.0\n  - @atproto/did@0.2.1\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [[`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6), [`8a88e2c15`](https://github.com/bluesky-social/atproto/commit/8a88e2c15451f5e8239400eeb277ad31d178b8e6)]:\n  - @atproto/jwk@0.5.0\n\n## 0.4.0\n\n### Minor Changes\n\n- [#3973](https://github.com/bluesky-social/atproto/pull/3973) [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `OidcAuthenticationErrorResponse` to `OidcAuthorizationResponseError`\n\n- [#3973](https://github.com/bluesky-social/atproto/pull/3973) [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `OAuthAuthenticationErrorResponse` to `OAuthAuthorizationResponseError`\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee), [`90b4775fc`](https://github.com/bluesky-social/atproto/commit/90b4775fc9c6959171bc12b961ce9421cc14d6ee)]:\n  - @atproto/jwk@0.4.0\n\n## 0.3.0\n\n### Minor Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Change the default client authentication method in the client document metadata to \"client_secret_basic\" (as per spec)\n\n### Patch Changes\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove invalid default for `code_challenge_method` authorization request parameter\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove schema's `.optional()` modifier when a `.default()` is defined\n\n- [#3847](https://github.com/bluesky-social/atproto/pull/3847) [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent using `none` as `token_endpoint_auth_signing_alg_values_supported` in authorization server metadata documents.\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/jwk@0.3.0\n\n## 0.2.8\n\n### Patch Changes\n\n- [#3919](https://github.com/bluesky-social/atproto/pull/3919) [`a3b24ca77`](https://github.com/bluesky-social/atproto/commit/a3b24ca77ca24ac19b17cf9ee2a5ca9612ccf96c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Parse JSON encoded Authorization Request Parameters\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05)]:\n  - @atproto/jwk@0.2.0\n\n## 0.2.7\n\n### Patch Changes\n\n- [#3797](https://github.com/bluesky-social/atproto/pull/3797) [`a48b093f0`](https://github.com/bluesky-social/atproto/commit/a48b093f0ba3cf67b7abc50d309afcb336d8ead8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `OidcUserinfo` type\n\n## 0.2.6\n\n### Patch Changes\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add definition for `ConventionalOAuthClientId`\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove hard coded `client_name` from loopback client metadata\n\n## 0.2.5\n\n### Patch Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `OAuthAuthenticationErrorResponse`\n\n- Updated dependencies [[`26a077716`](https://github.com/bluesky-social/atproto/commit/26a07771673bf1090a61efb7c970235f0b2509fc)]:\n  - @atproto/jwk@0.1.5\n\n## 0.2.4\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly support locales with 3 chars (Asturian)\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/jwk@0.1.4\n\n## 0.2.3\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Support environments not providing URL.canParse\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/jwk@0.1.3\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0), [`2889c7699`](https://github.com/bluesky-social/atproto/commit/2889c76995ce3c569f595ac3c678218e9ce659f0)]:\n  - @atproto/jwk@0.1.2\n\n## 0.2.1\n\n### Patch Changes\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add oauthClientIdLoopbackSchema and oauthClientIdDiscoverableSchema schemas\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce use of http and https url where applicable\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Strong validation or redirect_uri\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow oauthIssuerIdentifier to be an \"http:\" url. Make sure to manually check for \"http:\" issuers if you don't allow them.\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove ALLOW_UNSECURE_ORIGINS constant\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove invalid `issuer` property from OAuthTokenResponse\n\n### Patch Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing \"wap\" display request parameter value\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove invalid `client_id` property from oauthRefreshTokenGrantTokenRequestSchema\n\n- [#2874](https://github.com/bluesky-social/atproto/pull/2874) [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing of oauthIssuerIdentifierSchema\n\n## 0.1.5\n\n### Patch Changes\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly validate client metadata scope\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow ClientID query params to end with a slash \"/\" char\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose OAuthScope\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - add assertion utils for client ids\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow loopback client ids to omit the (empty) path parameter\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce ClientID URL path to be normalized\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename OAuthAuthenticationRequestParameters to OAuthAuthorizationRequestParameters\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Restrict the value used as code_challenge_methods_supported\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing \"expires_in\" property to OAuthParResponse type definition\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow loopback clients to define their scopes through the \"scope\" client_id query parameter.\n\n- [#2755](https://github.com/bluesky-social/atproto/pull/2755) [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error description in case of invalid loopback client_id\n\n## 0.1.4\n\n### Patch Changes\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Validate scopes characters according to OAuth 2.1 spec\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Re-use code definition of oauthResponseTypeSchema\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove non-standard \"sub\" from OAuthTokenResponse\n\n## 0.1.3\n\n### Patch Changes\n\n- [#2729](https://github.com/bluesky-social/atproto/pull/2729) [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Avoid code duplication in the definition of OAuthEndpointName\n\n## 0.1.2\n\n### Patch Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Better implement aptroto OAuth spec\n\n## 0.1.1\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add client_id_metadata_document_supported in metadata\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/jwk@0.1.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n### Patch Changes\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto/jwk@0.1.0\n"
  },
  {
    "path": "packages/oauth/oauth-types/README.md",
    "content": "# @atproto/oauth-types\n\nThis library exposes utilities for typing and validating OAuth related data structures.\n"
  },
  {
    "path": "packages/oauth/oauth-types/package.json",
    "content": "{\n  \"name\": \"@atproto/oauth-types\",\n  \"version\": \"0.6.3\",\n  \"license\": \"MIT\",\n  \"description\": \"OAuth typing & validation library\",\n  \"keywords\": [\n    \"atproto\",\n    \"oauth\",\n    \"types\",\n    \"isomorphic\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/oauth/oauth-types\"\n  },\n  \"type\": \"commonjs\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"@atproto/did\": \"workspace:^\",\n    \"@atproto/jwk\": \"workspace:^\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/atproto-loopback-client-id.ts",
    "content": "import { DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS } from './atproto-loopback-client-redirect-uris.js'\nimport {\n  AtprotoOAuthScope,\n  DEFAULT_ATPROTO_OAUTH_SCOPE,\n  asAtprotoOAuthScope,\n  isAtprotoOAuthScope,\n} from './atproto-oauth-scope.js'\nimport {\n  LOOPBACK_CLIENT_ID_ORIGIN,\n  OAuthClientIdLoopback,\n  parseOAuthLoopbackClientId,\n} from './oauth-client-id-loopback.js'\nimport {\n  OAuthLoopbackRedirectURI,\n  oauthLoopbackClientRedirectUriSchema,\n} from './oauth-redirect-uri.js'\nimport { arrayEquivalent, asArray } from './util.js'\n\nexport type OAuthLoopbackClientIdConfig = {\n  scope?: string\n  redirect_uris?: Iterable<string>\n}\n\nexport function buildAtprotoLoopbackClientId(\n  config?: OAuthLoopbackClientIdConfig,\n): OAuthClientIdLoopback {\n  if (config) {\n    const params = new URLSearchParams()\n\n    const { scope } = config\n    if (scope != null && scope !== DEFAULT_ATPROTO_OAUTH_SCOPE) {\n      params.set('scope', asAtprotoOAuthScope(scope))\n    }\n\n    const redirectUris = asArray(config.redirect_uris)\n    if (\n      redirectUris &&\n      !arrayEquivalent(redirectUris, DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS)\n    ) {\n      if (!redirectUris.length) {\n        throw new TypeError(`Unexpected empty \"redirect_uris\" config`)\n      }\n      for (const uri of redirectUris) {\n        params.append(\n          'redirect_uri',\n          oauthLoopbackClientRedirectUriSchema.parse(uri),\n        )\n      }\n    }\n\n    if (params.size) {\n      return `${LOOPBACK_CLIENT_ID_ORIGIN}?${params.toString()}`\n    }\n  }\n\n  return LOOPBACK_CLIENT_ID_ORIGIN\n}\n\nexport type AtprotoLoopbackClientIdParams = {\n  scope: AtprotoOAuthScope\n  redirect_uris: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]\n}\n\nexport function parseAtprotoLoopbackClientId(\n  clientId: string,\n): AtprotoLoopbackClientIdParams {\n  const { scope = DEFAULT_ATPROTO_OAUTH_SCOPE, redirect_uris } =\n    parseOAuthLoopbackClientId(clientId)\n  if (!isAtprotoOAuthScope(scope)) {\n    throw new TypeError(\n      'ATProto Loopback ClientID must include \"atproto\" scope',\n    )\n  }\n  return {\n    scope,\n    redirect_uris: redirect_uris ?? [...DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS],\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/atproto-loopback-client-metadata.ts",
    "content": "import {\n  AtprotoLoopbackClientIdParams,\n  OAuthLoopbackClientIdConfig,\n  buildAtprotoLoopbackClientId,\n  parseAtprotoLoopbackClientId,\n} from './atproto-loopback-client-id.js'\nimport { AtprotoOAuthScope } from './atproto-oauth-scope.js'\nimport { OAuthClientIdLoopback } from './oauth-client-id-loopback.js'\nimport { OAuthClientMetadataInput } from './oauth-client-metadata.js'\nimport { OAuthLoopbackRedirectURI } from './oauth-redirect-uri.js'\n\nexport type AtprotoLoopbackClientMetadata = OAuthClientMetadataInput & {\n  client_id: OAuthClientIdLoopback\n  scope: AtprotoOAuthScope\n  redirect_uris: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]\n}\n\nexport function atprotoLoopbackClientMetadata(\n  clientId: string,\n): AtprotoLoopbackClientMetadata {\n  const params = parseAtprotoLoopbackClientId(clientId)\n  // Safe to cast because parseAtprotoLoopbackClientId ensures it's a loopback ID\n  return buildMetadataInternal(clientId as OAuthClientIdLoopback, params)\n}\n\nexport function buildAtprotoLoopbackClientMetadata(\n  config: OAuthLoopbackClientIdConfig,\n): AtprotoLoopbackClientMetadata {\n  const clientId = buildAtprotoLoopbackClientId(config)\n  return buildMetadataInternal(clientId, parseAtprotoLoopbackClientId(clientId))\n}\n\nfunction buildMetadataInternal(\n  clientId: OAuthClientIdLoopback,\n  clientParams: AtprotoLoopbackClientIdParams,\n): AtprotoLoopbackClientMetadata {\n  return {\n    client_id: clientId,\n    scope: clientParams.scope,\n    redirect_uris: clientParams.redirect_uris,\n    response_types: ['code'],\n    grant_types: ['authorization_code', 'refresh_token'],\n    token_endpoint_auth_method: 'none',\n    application_type: 'native',\n    dpop_bound_access_tokens: true,\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/atproto-loopback-client-redirect-uris.ts",
    "content": "export const DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS = Object.freeze([\n  `http://127.0.0.1/`,\n  `http://[::1]/`,\n] as const)\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/atproto-oauth-scope.ts",
    "content": "import { z } from 'zod'\nimport { OAuthScope, isOAuthScope } from './oauth-scope.js'\nimport { SpaceSeparatedValue, isSpaceSeparatedValue } from './util.js'\n\nexport const ATPROTO_SCOPE_VALUE = 'atproto'\nexport type AtprotoScopeValue = typeof ATPROTO_SCOPE_VALUE\n\nexport type AtprotoOAuthScope = OAuthScope &\n  SpaceSeparatedValue<AtprotoScopeValue>\n\nexport function isAtprotoOAuthScope(input: string): input is AtprotoOAuthScope {\n  return (\n    isOAuthScope(input) && isSpaceSeparatedValue(ATPROTO_SCOPE_VALUE, input)\n  )\n}\n\nexport function asAtprotoOAuthScope<I extends string>(input: I) {\n  if (isAtprotoOAuthScope(input)) return input\n  throw new TypeError(`Value must contain \"${ATPROTO_SCOPE_VALUE}\" scope value`)\n}\n\nexport function assertAtprotoOAuthScope(\n  input: string,\n): asserts input is AtprotoOAuthScope {\n  void asAtprotoOAuthScope(input)\n}\n\nexport const atprotoOAuthScopeSchema = z.string().refine(isAtprotoOAuthScope, {\n  message: 'Invalid ATProto OAuth scope',\n})\n\n// Default scope is for reading identity (did) only\nexport const DEFAULT_ATPROTO_OAUTH_SCOPE =\n  ATPROTO_SCOPE_VALUE satisfies AtprotoOAuthScope\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/atproto-oauth-token-response.ts",
    "content": "import { TypeOf, z } from 'zod'\nimport { atprotoDidSchema } from '@atproto/did'\nimport { atprotoOAuthScopeSchema } from './atproto-oauth-scope'\nimport { oauthTokenResponseSchema } from './oauth-token-response.js'\n\nexport const atprotoOAuthTokenResponseSchema = oauthTokenResponseSchema.extend({\n  token_type: z.literal('DPoP'),\n  sub: atprotoDidSchema,\n  scope: atprotoOAuthScopeSchema,\n  // OpenID is not compatible with atproto identities\n  id_token: z.never().optional(),\n})\n\nexport type AtprotoOAuthTokenResponse = TypeOf<\n  typeof atprotoOAuthTokenResponseSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/constants.ts",
    "content": "export const CLIENT_ASSERTION_TYPE_JWT_BEARER =\n  'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/index.ts",
    "content": "export * from './constants.js'\nexport * from './uri.js'\nexport * from './util.js'\n\nexport * from './atproto-loopback-client-id.js'\nexport * from './atproto-loopback-client-metadata.js'\nexport * from './atproto-loopback-client-redirect-uris.js'\nexport * from './atproto-oauth-scope.js'\nexport * from './atproto-oauth-token-response.js'\nexport * from './oauth-access-token.js'\nexport * from './oauth-authorization-code-grant-token-request.js'\nexport * from './oauth-authorization-details.js'\nexport * from './oauth-authorization-request-jar.js'\nexport * from './oauth-authorization-request-par.js'\nexport * from './oauth-authorization-request-parameters.js'\nexport * from './oauth-authorization-request-query.js'\nexport * from './oauth-authorization-request-uri.js'\nexport * from './oauth-authorization-response-error.js'\nexport * from './oauth-authorization-server-metadata.js'\nexport * from './oauth-client-credentials-grant-token-request.js'\nexport * from './oauth-client-credentials.js'\nexport * from './oauth-client-id-discoverable.js'\nexport * from './oauth-client-id-loopback.js'\nexport * from './oauth-client-id.js'\nexport * from './oauth-client-metadata.js'\nexport * from './oauth-endpoint-auth-method.js'\nexport * from './oauth-endpoint-name.js'\nexport * from './oauth-grant-type.js'\nexport * from './oauth-introspection-response.js'\nexport * from './oauth-issuer-identifier.js'\nexport * from './oauth-par-response.js'\nexport * from './oauth-password-grant-token-request.js'\nexport * from './oauth-prompt-mode.js'\nexport * from './oauth-protected-resource-metadata.js'\nexport * from './oauth-redirect-uri.js'\nexport * from './oauth-refresh-token-grant-token-request.js'\nexport * from './oauth-refresh-token.js'\nexport * from './oauth-request-uri.js'\nexport * from './oauth-response-mode.js'\nexport * from './oauth-response-type.js'\nexport * from './oauth-scope.js'\nexport * from './oauth-token-identification.js'\nexport * from './oauth-token-request.js'\nexport * from './oauth-token-response.js'\nexport * from './oauth-token-type.js'\nexport * from './oidc-authorization-error-response.js'\nexport * from './oidc-claims-parameter.js'\nexport * from './oidc-claims-properties.js'\nexport * from './oidc-entity-type.js'\nexport * from './oidc-userinfo.js'\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-access-token.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthAccessTokenSchema = z.string().min(1)\nexport type OAuthAccessToken = z.infer<typeof oauthAccessTokenSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-code-grant-token-request.ts",
    "content": "import { z } from 'zod'\nimport { oauthRedirectUriSchema } from './oauth-redirect-uri.js'\n\nexport const oauthAuthorizationCodeGrantTokenRequestSchema = z.object({\n  grant_type: z.literal('authorization_code'),\n  code: z.string().min(1),\n  redirect_uri: oauthRedirectUriSchema,\n  /** @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1} */\n  code_verifier: z\n    .string()\n    .min(43)\n    .max(128)\n    .regex(/^[a-zA-Z0-9-._~]+$/)\n    .optional(),\n})\n\nexport type OAuthAuthorizationCodeGrantTokenRequest = z.infer<\n  typeof oauthAuthorizationCodeGrantTokenRequestSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-details.ts",
    "content": "import { z } from 'zod'\nimport { dangerousUriSchema } from './uri.js'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9396#section-2 | RFC 9396, Section 2}\n */\nexport const oauthAuthorizationDetailSchema = z.object({\n  type: z.string(),\n  /**\n   * An array of strings representing the location of the resource or RS. These\n   * strings are typically URIs identifying the location of the RS.\n   */\n  locations: z.array(dangerousUriSchema).optional(),\n  /**\n   * An array of strings representing the kinds of actions to be taken at the\n   * resource.\n   */\n  actions: z.array(z.string()).optional(),\n  /**\n   * An array of strings representing the kinds of data being requested from the\n   * resource.\n   */\n  datatypes: z.array(z.string()).optional(),\n  /**\n   * A string identifier indicating a specific resource available at the API.\n   */\n  identifier: z.string().optional(),\n  /**\n   * An array of strings representing the types or levels of privilege being\n   * requested at the resource.\n   */\n  privileges: z.array(z.string()).optional(),\n})\n\nexport type OAuthAuthorizationDetail = z.infer<\n  typeof oauthAuthorizationDetailSchema\n>\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9396#section-2 | RFC 9396, Section 2}\n */\nexport const oauthAuthorizationDetailsSchema = z.array(\n  oauthAuthorizationDetailSchema,\n)\n\nexport type OAuthAuthorizationDetails = z.infer<\n  typeof oauthAuthorizationDetailsSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-request-jar.ts",
    "content": "import { z } from 'zod'\nimport { signedJwtSchema, unsignedJwtSchema } from '@atproto/jwk'\n\nexport const oauthAuthorizationRequestJarSchema = z.object({\n  /**\n   * AuthorizationRequest inside a JWT:\n   * - \"iat\" is required and **MUST** be less than one minute\n   *\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc9101}\n   */\n  request: z.union([signedJwtSchema, unsignedJwtSchema]),\n})\n\nexport type OAuthAuthorizationRequestJar = z.infer<\n  typeof oauthAuthorizationRequestJarSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-request-par.ts",
    "content": "import { z } from 'zod'\nimport { oauthAuthorizationRequestJarSchema } from './oauth-authorization-request-jar.js'\nimport { oauthAuthorizationRequestParametersSchema } from './oauth-authorization-request-parameters.js'\n\nexport const oauthAuthorizationRequestParSchema = z.union([\n  oauthAuthorizationRequestParametersSchema,\n  oauthAuthorizationRequestJarSchema,\n])\n\nexport type OAuthAuthorizationRequestPar = z.infer<\n  typeof oauthAuthorizationRequestParSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-request-parameters.ts",
    "content": "import { z } from 'zod'\nimport { signedJwtSchema } from '@atproto/jwk'\nimport { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js'\nimport { oauthClientIdSchema } from './oauth-client-id.js'\nimport { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'\nimport { oauthPromptModeSchema } from './oauth-prompt-mode.js'\nimport { oauthRedirectUriSchema } from './oauth-redirect-uri.js'\nimport { oauthResponseModeSchema } from './oauth-response-mode.js'\nimport { oauthResponseTypeSchema } from './oauth-response-type.js'\nimport { oauthScopeSchema } from './oauth-scope.js'\nimport { oidcClaimsParameterSchema } from './oidc-claims-parameter.js'\nimport { oidcClaimsPropertiesSchema } from './oidc-claims-properties.js'\nimport { oidcEntityTypeSchema } from './oidc-entity-type.js'\nimport { jsonObjectPreprocess, numberPreprocess } from './util.js'\n\n/**\n * @note non string parameters will be converted from their string\n * representation since oauth request parameters are typically sent as URL\n * encoded form data or URL encoded query string.\n * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest | OIDC}\n */\nexport const oauthAuthorizationRequestParametersSchema = z.object({\n  client_id: oauthClientIdSchema,\n  state: z.string().optional(),\n  redirect_uri: oauthRedirectUriSchema.optional(),\n  scope: oauthScopeSchema.optional(),\n  response_type: oauthResponseTypeSchema,\n\n  // PKCE\n\n  // https://datatracker.ietf.org/doc/html/rfc7636#section-4.3\n  code_challenge: z.string().optional(),\n  code_challenge_method: oauthCodeChallengeMethodSchema.optional(),\n\n  // DPOP\n\n  // https://datatracker.ietf.org/doc/html/rfc9449#section-12.3\n  dpop_jkt: z.string().optional(),\n\n  // OIDC\n\n  // Default depend on response_type\n  response_mode: oauthResponseModeSchema.optional(),\n\n  nonce: z.string().optional(),\n\n  // Specifies the allowable elapsed time in seconds since the last time the\n  // End-User was actively authenticated by the OP. If the elapsed time is\n  // greater than this value, the OP MUST attempt to actively re-authenticate\n  // the End-User. (The max_age request parameter corresponds to the OpenID 2.0\n  // PAPE [OpenID.PAPE] max_auth_age request parameter.) When max_age is used,\n  // the ID Token returned MUST include an auth_time Claim Value. Note that\n  // max_age=0 is equivalent to prompt=login.\n  max_age: z.preprocess(numberPreprocess, z.number().int().min(0)).optional(),\n\n  claims: z\n    .preprocess(\n      jsonObjectPreprocess,\n      z.record(\n        oidcEntityTypeSchema,\n        z.record(\n          oidcClaimsParameterSchema,\n          z.union([z.literal(null), oidcClaimsPropertiesSchema]),\n        ),\n      ),\n    )\n    .optional(),\n\n  // https://openid.net/specs/openid-connect-core-1_0.html#RegistrationParameter\n  // Not supported by this library (yet?)\n  // registration: clientMetadataSchema.optional(),\n\n  login_hint: z.string().min(1).optional(),\n\n  ui_locales: z\n    .string()\n    .regex(/^[a-z]{2,3}(-[A-Z]{2})?( [a-z]{2,3}(-[A-Z]{2})?)*$/) // fr-CA fr en\n    .optional(),\n\n  // Previous ID Token, should be provided when prompt=none is used\n  id_token_hint: signedJwtSchema.optional(),\n\n  // Type of UI the AS is displayed on\n  display: z.enum(['page', 'popup', 'touch', 'wap']).optional(),\n\n  // How the AS should prompt the user for authorization:\n  prompt: oauthPromptModeSchema.optional(),\n\n  // https://datatracker.ietf.org/doc/html/rfc9396\n  authorization_details: z\n    .preprocess(jsonObjectPreprocess, oauthAuthorizationDetailsSchema)\n    .optional(),\n})\n\n/**\n * @see {oauthAuthorizationRequestParametersSchema}\n */\nexport type OAuthAuthorizationRequestParameters = z.infer<\n  typeof oauthAuthorizationRequestParametersSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-request-query.ts",
    "content": "import { z } from 'zod'\nimport { oauthAuthorizationRequestJarSchema } from './oauth-authorization-request-jar.js'\nimport { oauthAuthorizationRequestParametersSchema } from './oauth-authorization-request-parameters.js'\nimport { oauthAuthorizationRequestUriSchema } from './oauth-authorization-request-uri.js'\nimport { oauthClientIdSchema } from './oauth-client-id.js'\n\nexport const oauthAuthorizationRequestQuerySchema = z.intersection(\n  z.object({\n    // REQUIRED. OAuth 2.0 [RFC6749] client_id.\n    client_id: oauthClientIdSchema,\n  }),\n  z.union([\n    oauthAuthorizationRequestParametersSchema,\n    oauthAuthorizationRequestJarSchema,\n    oauthAuthorizationRequestUriSchema,\n  ]),\n)\n\nexport type OAuthAuthorizationRequestQuery = z.infer<\n  typeof oauthAuthorizationRequestQuerySchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-request-uri.ts",
    "content": "import { z } from 'zod'\nimport { oauthRequestUriSchema } from './oauth-request-uri.js'\n\nexport const oauthAuthorizationRequestUriSchema = z.object({\n  request_uri: oauthRequestUriSchema,\n})\n\nexport type OAuthAuthorizationRequestUri = z.infer<\n  typeof oauthAuthorizationRequestUriSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-response-error.ts",
    "content": "import { z } from 'zod'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#name-error-response-2}\n */\nexport const oauthAuthorizationResponseErrorSchema = z.enum([\n  // The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.\n  'invalid_request',\n  // The client is not authorized to request an authorization code using this method.\n  'unauthorized_client',\n  // The resource owner or authorization server denied the request.\n  'access_denied',\n  // The authorization server does not support obtaining an authorization code using this method.\n  'unsupported_response_type',\n  // The requested scope is invalid, unknown, or malformed.\n  'invalid_scope',\n  // The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.)\n  'server_error',\n  // The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.)\n  'temporarily_unavailable',\n])\n\nexport type OAuthAuthorizationResponseError = z.infer<\n  typeof oauthAuthorizationResponseErrorSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-authorization-server-metadata.ts",
    "content": "import { z } from 'zod'\nimport { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'\nimport { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'\nimport { oauthPromptModeSchema } from './oauth-prompt-mode.js'\nimport { webUriSchema } from './uri.js'\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}\n * @note we do not enforce https: scheme in URIs to support development\n * environments. Make sure to validate the URIs before using it in a production\n * environment.\n */\nexport const oauthAuthorizationServerMetadataSchema = z.object({\n  issuer: oauthIssuerIdentifierSchema,\n\n  claims_supported: z.array(z.string()).optional(),\n  claims_locales_supported: z.array(z.string()).optional(),\n  claims_parameter_supported: z.boolean().optional(),\n  request_parameter_supported: z.boolean().optional(),\n  request_uri_parameter_supported: z.boolean().optional(),\n  require_request_uri_registration: z.boolean().optional(),\n  scopes_supported: z.array(z.string()).optional(),\n  subject_types_supported: z.array(z.string()).optional(),\n  response_types_supported: z.array(z.string()).optional(),\n  response_modes_supported: z.array(z.string()).optional(),\n  grant_types_supported: z.array(z.string()).optional(),\n  code_challenge_methods_supported: z\n    .array(oauthCodeChallengeMethodSchema)\n    .min(1)\n    .optional(),\n  ui_locales_supported: z.array(z.string()).optional(),\n  id_token_signing_alg_values_supported: z.array(z.string()).optional(),\n  display_values_supported: z.array(z.string()).optional(),\n  request_object_signing_alg_values_supported: z.array(z.string()).optional(),\n  authorization_response_iss_parameter_supported: z.boolean().optional(),\n  authorization_details_types_supported: z.array(z.string()).optional(),\n  request_object_encryption_alg_values_supported: z\n    .array(z.string())\n    .optional(),\n  request_object_encryption_enc_values_supported: z\n    .array(z.string())\n    .optional(),\n\n  jwks_uri: webUriSchema.optional(),\n\n  authorization_endpoint: webUriSchema, // .optional(),\n\n  token_endpoint: webUriSchema, // .optional(),\n  // https://www.rfc-editor.org/rfc/rfc8414.html#section-2\n  token_endpoint_auth_methods_supported: z\n    .array(z.string())\n    // > If omitted, the default is \"client_secret_basic\" [...].\n    .default(['client_secret_basic']),\n  token_endpoint_auth_signing_alg_values_supported: z\n    .array(z.string())\n    .optional(),\n\n  revocation_endpoint: webUriSchema.optional(),\n  introspection_endpoint: webUriSchema.optional(),\n  pushed_authorization_request_endpoint: webUriSchema.optional(),\n\n  require_pushed_authorization_requests: z.boolean().optional(),\n\n  userinfo_endpoint: webUriSchema.optional(),\n  end_session_endpoint: webUriSchema.optional(),\n  registration_endpoint: webUriSchema.optional(),\n\n  // https://datatracker.ietf.org/doc/html/rfc9449#section-5.1\n  dpop_signing_alg_values_supported: z.array(z.string()).optional(),\n\n  // https://www.rfc-editor.org/rfc/rfc9728.html#section-4\n  protected_resources: z.array(webUriSchema).optional(),\n\n  // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html\n  client_id_metadata_document_supported: z.boolean().optional(),\n\n  // https://openid.net/specs/openid-connect-prompt-create-1_0.html#section-4.2\n  prompt_values_supported: z.array(oauthPromptModeSchema).optional(),\n})\n\nexport type OAuthAuthorizationServerMetadata = z.infer<\n  typeof oauthAuthorizationServerMetadataSchema\n>\n\nexport const oauthAuthorizationServerMetadataValidator =\n  oauthAuthorizationServerMetadataSchema\n    .superRefine((data, ctx) => {\n      if (\n        data.require_pushed_authorization_requests &&\n        !data.pushed_authorization_request_endpoint\n      ) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message:\n            '\"pushed_authorization_request_endpoint\" required when \"require_pushed_authorization_requests\" is true',\n        })\n      }\n    })\n    .superRefine((data, ctx) => {\n      if (data.response_types_supported) {\n        if (!data.response_types_supported.includes('code')) {\n          ctx.addIssue({\n            code: z.ZodIssueCode.custom,\n            message: 'Response type \"code\" is required',\n          })\n        }\n      }\n    })\n    .superRefine((data, ctx) => {\n      if (\n        data.token_endpoint_auth_signing_alg_values_supported?.includes('none')\n      ) {\n        // https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3\n        // > The value `none` MUST NOT be used.\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: 'Client authentication method \"none\" is not allowed',\n        })\n      }\n    })\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-client-credentials-grant-token-request.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthClientCredentialsGrantTokenRequestSchema = z.object({\n  grant_type: z.literal('client_credentials'),\n})\n\nexport type OAuthClientCredentialsGrantTokenRequest = z.infer<\n  typeof oauthClientCredentialsGrantTokenRequestSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-client-credentials.ts",
    "content": "import { z } from 'zod'\nimport { signedJwtSchema } from '@atproto/jwk'\nimport { CLIENT_ASSERTION_TYPE_JWT_BEARER } from './constants.js'\nimport { oauthClientIdSchema } from './oauth-client-id.js'\n\nexport const oauthClientCredentialsJwtBearerSchema = z.object({\n  client_id: oauthClientIdSchema,\n  client_assertion_type: z.literal(CLIENT_ASSERTION_TYPE_JWT_BEARER),\n  /**\n   * - \"sub\" the subject MUST be the \"client_id\" of the OAuth client\n   * - \"iat\" is required and MUST be less than one minute\n   * - \"aud\" must containing a value that identifies the authorization server\n   * - The JWT MAY contain a \"jti\" (JWT ID) claim that provides a unique identifier for the token.\n   * - Note that the authorization server may reject JWTs with an \"exp\" claim value that is unreasonably far in the future.\n   *\n   * @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}\n   */\n  client_assertion: signedJwtSchema,\n})\n\nexport type OAuthClientCredentialsJwtBearer = z.infer<\n  typeof oauthClientCredentialsJwtBearerSchema\n>\n\nexport const oauthClientCredentialsSecretPostSchema = z.object({\n  client_id: oauthClientIdSchema,\n  client_secret: z.string(),\n})\n\nexport type OAuthClientCredentialsSecretPost = z.infer<\n  typeof oauthClientCredentialsSecretPostSchema\n>\n\nexport const oauthClientCredentialsNoneSchema = z.object({\n  client_id: oauthClientIdSchema,\n})\n\nexport type OAuthClientCredentialsNone = z.infer<\n  typeof oauthClientCredentialsNoneSchema\n>\n\n//\n\nexport const oauthClientCredentialsSchema = z.union([\n  oauthClientCredentialsJwtBearerSchema,\n  oauthClientCredentialsSecretPostSchema,\n  // Must be last since it is less specific\n  oauthClientCredentialsNoneSchema,\n])\n\nexport type OAuthClientCredentials = z.infer<\n  typeof oauthClientCredentialsSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-client-id-discoverable.ts",
    "content": "import { TypeOf, z } from 'zod'\nimport { oauthClientIdSchema } from './oauth-client-id.js'\nimport { httpsUriSchema } from './uri.js'\nimport { extractUrlPath, isHostnameIP } from './util.js'\n\n/**\n * @see {@link https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html}\n */\nexport const oauthClientIdDiscoverableSchema = z\n  .intersection(oauthClientIdSchema, httpsUriSchema)\n  .superRefine((value, ctx): value is `https://${string}/${string}` => {\n    const url = new URL(value)\n\n    if (url.username || url.password) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'ClientID must not contain credentials',\n      })\n      return false\n    }\n\n    if (url.hash) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'ClientID must not contain a fragment',\n      })\n      return false\n    }\n\n    if (url.pathname === '/') {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message:\n          'ClientID must contain a path component (e.g. \"/client-metadata.json\")',\n      })\n      return false\n    }\n\n    if (url.pathname.endsWith('/')) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'ClientID path must not end with a trailing slash',\n      })\n      return false\n    }\n\n    if (isHostnameIP(url.hostname)) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'ClientID hostname must not be an IP address',\n      })\n      return false\n    }\n\n    // URL constructor normalizes the URL, so we extract the path manually to\n    // avoid normalization, then compare it to the normalized path to ensure\n    // that the URL does not contain path traversal or other unexpected characters\n    if (extractUrlPath(value) !== url.pathname) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: `ClientID must be in canonical form (\"${url.href}\", got \"${value}\")`,\n      })\n      return false\n    }\n\n    return true\n  })\n\nexport type OAuthClientIdDiscoverable = TypeOf<\n  typeof oauthClientIdDiscoverableSchema\n>\n\nexport function isOAuthClientIdDiscoverable(\n  clientId: string,\n): clientId is OAuthClientIdDiscoverable {\n  return oauthClientIdDiscoverableSchema.safeParse(clientId).success\n}\n\nexport const conventionalOAuthClientIdSchema =\n  oauthClientIdDiscoverableSchema.superRefine(\n    (value, ctx): value is `https://${string}/oauth-client-metadata.json` => {\n      const url = new URL(value)\n\n      if (url.port) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: 'ClientID must not contain a port',\n        })\n        return false\n      }\n\n      if (url.search) {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: 'ClientID must not contain a query string',\n        })\n        return false\n      }\n\n      if (url.pathname !== '/oauth-client-metadata.json') {\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: 'ClientID must be \"/oauth-client-metadata.json\"',\n        })\n        return false\n      }\n\n      return true\n    },\n  )\n\nexport type ConventionalOAuthClientId = TypeOf<\n  typeof conventionalOAuthClientIdSchema\n>\n\nexport function isConventionalOAuthClientId(\n  clientId: string,\n): clientId is ConventionalOAuthClientId {\n  return conventionalOAuthClientIdSchema.safeParse(clientId).success\n}\n\nexport function assertOAuthDiscoverableClientId(\n  value: string,\n): asserts value is OAuthClientIdDiscoverable {\n  void oauthClientIdDiscoverableSchema.parse(value)\n}\n\nexport function parseOAuthDiscoverableClientId(clientId: string): URL {\n  return new URL(oauthClientIdDiscoverableSchema.parse(clientId))\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-client-id-loopback.ts",
    "content": "import { oauthClientIdSchema } from './oauth-client-id.js'\nimport {\n  OAuthLoopbackRedirectURI,\n  oauthLoopbackClientRedirectUriSchema,\n} from './oauth-redirect-uri.js'\nimport { OAuthScope, oauthScopeSchema } from './oauth-scope.js'\n\nexport const LOOPBACK_CLIENT_ID_ORIGIN = 'http://localhost'\n\n// @NOTE This is not actually based on a standard, but rather a convention\n// established by Bluesky in the Atproto specs and implementation. As such, and\n// in order to respect the convention from this package, these should be\n// prefixed with \"Atproto\" instead of \"OAuth\". For legacy reasons, we keep the\n// current names, but we should rename them in a future major release, unless\n// loopback client ids have since then been standardized.\n\nexport type OAuthClientIdLoopback =\n  `http://localhost${'' | `/`}${'' | `?${string}`}`\n\nexport type OAuthLoopbackClientIdParams = {\n  scope?: OAuthScope\n  redirect_uris?: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]\n}\n\nexport const oauthClientIdLoopbackSchema = oauthClientIdSchema.superRefine(\n  (input, ctx): input is OAuthClientIdLoopback => {\n    const result = safeParseOAuthLoopbackClientId(input)\n    if (!result.success) {\n      ctx.addIssue({ code: 'custom', message: result.message })\n    }\n    return result.success\n  },\n)\n\nexport function assertOAuthLoopbackClientId(\n  input: string,\n): asserts input is OAuthClientIdLoopback {\n  void parseOAuthLoopbackClientId(input)\n}\n\nexport function isOAuthClientIdLoopback<T extends string>(\n  input: T,\n): input is T & OAuthClientIdLoopback {\n  return safeParseOAuthLoopbackClientId(input).success\n}\n\nexport function asOAuthClientIdLoopback<T extends string>(input: T) {\n  assertOAuthLoopbackClientId(input)\n  return input\n}\n\nexport function parseOAuthLoopbackClientId(\n  input: string,\n): OAuthLoopbackClientIdParams {\n  const result = safeParseOAuthLoopbackClientId(input)\n  if (result.success) return result.value\n\n  throw new TypeError(`Invalid loopback client ID: ${result.message}`)\n}\n\n/**\n * Similar to Zod's {@link SafeParseReturnType} but uses a simple \"message\"\n * string instead of an \"error\" Error object.\n */\ntype LightParseReturnType<T> =\n  | { success: true; value: T }\n  | { success: false; message: string }\n\nexport function safeParseOAuthLoopbackClientId(\n  input: string,\n): LightParseReturnType<OAuthLoopbackClientIdParams> {\n  // @NOTE Not using \"new URL\" to ensure input indeed matches the type\n  // OAuthClientIdLoopback\n\n  if (!input.startsWith(LOOPBACK_CLIENT_ID_ORIGIN)) {\n    return {\n      success: false,\n      message: `Value must start with \"${LOOPBACK_CLIENT_ID_ORIGIN}\"`,\n    }\n  }\n\n  if (input.includes('#', LOOPBACK_CLIENT_ID_ORIGIN.length)) {\n    return {\n      success: false,\n      message: 'Value must not contain a hash component',\n    }\n  }\n\n  // Since we don't allow a path component (except for a single \"/\") the query\n  // string starts after the origin (+ 1 if there is a \"/\")\n  const queryStringIdx =\n    input.length > LOOPBACK_CLIENT_ID_ORIGIN.length &&\n    input.charCodeAt(LOOPBACK_CLIENT_ID_ORIGIN.length) === 0x2f /* '/' */\n      ? LOOPBACK_CLIENT_ID_ORIGIN.length + 1\n      : LOOPBACK_CLIENT_ID_ORIGIN.length\n\n  // Since we determined the position of the query string based on the origin\n  // length (instead of looking for a \"?\"), we need to make sure the query\n  // string position (if any) indeed starts with a \"?\".\n  if (\n    input.length !== queryStringIdx &&\n    input.charCodeAt(queryStringIdx) !== 0x3f /* '?' */\n  ) {\n    return {\n      success: false,\n      message: 'Value must not contain a path component',\n    }\n  }\n\n  const queryString = input.slice(queryStringIdx + 1)\n  return safeParseOAuthLoopbackClientIdQueryString(queryString)\n}\n\nexport function safeParseOAuthLoopbackClientIdQueryString(\n  input: string | Iterable<[key: string, value: string]>,\n): LightParseReturnType<OAuthLoopbackClientIdParams> {\n  // Parse query params\n  const params: OAuthLoopbackClientIdParams = {}\n\n  const it = typeof input === 'string' ? new URLSearchParams(input) : input\n  for (const [key, value] of it) {\n    if (key === 'scope') {\n      if ('scope' in params) {\n        return {\n          success: false,\n          message: 'Duplicate \"scope\" query parameter',\n        }\n      }\n\n      const res = oauthScopeSchema.safeParse(value)\n      if (!res.success) {\n        const reason = res.error.issues.map((i) => i.message).join(', ')\n        return {\n          success: false,\n          message: `Invalid \"scope\" query parameter: ${reason || 'Validation failed'}`,\n        }\n      }\n\n      params.scope = res.data\n    } else if (key === 'redirect_uri') {\n      const res = oauthLoopbackClientRedirectUriSchema.safeParse(value)\n      if (!res.success) {\n        const reason = res.error.issues.map((i) => i.message).join(', ')\n        return {\n          success: false,\n          message: `Invalid \"redirect_uri\" query parameter: ${reason || 'Validation failed'}`,\n        }\n      }\n\n      if (params.redirect_uris == null) params.redirect_uris = [res.data]\n      else params.redirect_uris.push(res.data)\n    } else {\n      return {\n        success: false,\n        message: `Unexpected query parameter \"${key}\"`,\n      }\n    }\n  }\n\n  return {\n    success: true,\n    value: params,\n  }\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-client-id.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthClientIdSchema = z.string().min(1)\nexport type OAuthClientId = z.infer<typeof oauthClientIdSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-client-metadata.ts",
    "content": "import { z } from 'zod'\nimport { jwksPubSchema } from '@atproto/jwk'\nimport { oauthClientIdSchema } from './oauth-client-id.js'\nimport { oauthEndpointAuthMethod } from './oauth-endpoint-auth-method.js'\nimport { oauthGrantTypeSchema } from './oauth-grant-type.js'\nimport { oauthRedirectUriSchema } from './oauth-redirect-uri.js'\nimport { oauthResponseTypeSchema } from './oauth-response-type.js'\nimport { oauthScopeSchema } from './oauth-scope.js'\nimport { webUriSchema } from './uri.js'\n\n/**\n * @see {@link https://openid.net/specs/openid-connect-registration-1_0.html}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7591}\n * @note we do not enforce https: scheme in URIs to support development\n * environments. Make sure to validate the URIs before using it in a production\n * environment.\n */\nexport const oauthClientMetadataSchema = z.object({\n  /**\n   * @note redirect_uris require additional validation\n   */\n  // https://www.rfc-editor.org/rfc/rfc7591.html#section-2\n  redirect_uris: z.array(oauthRedirectUriSchema).nonempty(),\n  response_types: z\n    .array(oauthResponseTypeSchema)\n    .nonempty()\n    // > If omitted, the default is that the client will use only the \"code\"\n    // > response type.\n    .default(['code']),\n  grant_types: z\n    .array(oauthGrantTypeSchema)\n    .nonempty()\n    // > If omitted, the default behavior is that the client will use only the\n    // > \"authorization_code\" Grant Type.\n    .default(['authorization_code']),\n  scope: oauthScopeSchema.optional(),\n  // https://www.rfc-editor.org/rfc/rfc7591.html#section-2\n  token_endpoint_auth_method: oauthEndpointAuthMethod\n    // > If unspecified or omitted, the default is \"client_secret_basic\" [...].\n    .default('client_secret_basic'),\n  token_endpoint_auth_signing_alg: z.string().optional(),\n  userinfo_signed_response_alg: z.string().optional(),\n  userinfo_encrypted_response_alg: z.string().optional(),\n  jwks_uri: webUriSchema.optional(),\n  jwks: jwksPubSchema.optional(),\n  application_type: z.enum(['web', 'native']).default('web'), // default, per spec, is \"web\"\n  subject_type: z.enum(['public', 'pairwise']).default('public'),\n  request_object_signing_alg: z.string().optional(),\n  id_token_signed_response_alg: z.string().optional(),\n  authorization_signed_response_alg: z.string().default('RS256'),\n  authorization_encrypted_response_enc: z.enum(['A128CBC-HS256']).optional(),\n  authorization_encrypted_response_alg: z.string().optional(),\n  client_id: oauthClientIdSchema.optional(),\n  client_name: z.string().optional(),\n  client_uri: webUriSchema.optional(),\n  policy_uri: webUriSchema.optional(),\n  tos_uri: webUriSchema.optional(),\n  logo_uri: webUriSchema.optional(), // @TODO: allow data: uri ?\n\n  /**\n   * Default Maximum Authentication Age. Specifies that the End-User MUST be\n   * actively authenticated if the End-User was authenticated longer ago than\n   * the specified number of seconds. The max_age request parameter overrides\n   * this default value. If omitted, no default Maximum Authentication Age is\n   * specified.\n   */\n  default_max_age: z.number().optional(),\n  require_auth_time: z.boolean().optional(),\n  contacts: z.array(z.string().email()).optional(),\n  tls_client_certificate_bound_access_tokens: z.boolean().optional(),\n\n  // https://datatracker.ietf.org/doc/html/rfc9449#section-5.2\n  dpop_bound_access_tokens: z.boolean().optional(),\n\n  // https://datatracker.ietf.org/doc/html/rfc9396#section-14.5\n  authorization_details_types: z.array(z.string()).optional(),\n})\n\nexport type OAuthClientMetadata = z.infer<typeof oauthClientMetadataSchema>\nexport type OAuthClientMetadataInput = z.input<typeof oauthClientMetadataSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-code-challenge-method.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthCodeChallengeMethodSchema = z.enum(['S256', 'plain'])\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-endpoint-auth-method.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthEndpointAuthMethod = z.enum([\n  'client_secret_basic',\n  'client_secret_jwt',\n  'client_secret_post',\n  'none',\n  'private_key_jwt',\n  'self_signed_tls_client_auth',\n  'tls_client_auth',\n])\n\nexport type OauthEndpointAuthMethod = z.infer<typeof oauthEndpointAuthMethod>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-endpoint-name.ts",
    "content": "export const OAUTH_ENDPOINT_NAMES = [\n  'token',\n  'revocation',\n  'introspection',\n  'pushed_authorization_request',\n] as const\n\nexport type OAuthEndpointName = (typeof OAUTH_ENDPOINT_NAMES)[number]\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-grant-type.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthGrantTypeSchema = z.enum([\n  'authorization_code',\n  'implicit',\n  'refresh_token',\n  'password', // Not part of OAuth 2.1\n  'client_credentials',\n  'urn:ietf:params:oauth:grant-type:jwt-bearer',\n  'urn:ietf:params:oauth:grant-type:saml2-bearer',\n])\n\nexport type OAuthGrantType = z.infer<typeof oauthGrantTypeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-introspection-response.ts",
    "content": "import { OAuthAuthorizationDetails } from './oauth-authorization-details.js'\nimport { OAuthTokenType } from './oauth-token-type.js'\n\n// https://datatracker.ietf.org/doc/html/rfc7662#section-2.2\nexport type OAuthIntrospectionResponse =\n  | { active: false }\n  | {\n      active: true\n\n      scope?: string\n      client_id?: string\n      username?: string\n      token_type?: OAuthTokenType\n      authorization_details?: OAuthAuthorizationDetails\n\n      aud?: string | [string, ...string[]]\n      exp?: number\n      iat?: number\n      iss?: string\n      jti?: string\n      nbf?: number\n      sub?: string\n    }\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-issuer-identifier.ts",
    "content": "import { z } from 'zod'\nimport { webUriSchema } from './uri.js'\n\nexport const oauthIssuerIdentifierSchema = webUriSchema.superRefine(\n  (value, ctx) => {\n    // Validate the issuer (MIX-UP attacks)\n\n    if (value.endsWith('/')) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Issuer URL must not end with a slash',\n      })\n      return false\n    }\n\n    const url = new URL(value)\n\n    if (url.username || url.password) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Issuer URL must not contain a username or password',\n      })\n      return false\n    }\n\n    if (url.hash || url.search) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Issuer URL must not contain a query or fragment',\n      })\n      return false\n    }\n\n    const canonicalValue = url.pathname === '/' ? url.origin : url.href\n    if (value !== canonicalValue) {\n      ctx.addIssue({\n        code: z.ZodIssueCode.custom,\n        message: 'Issuer URL must be in the canonical form',\n      })\n      return false\n    }\n\n    return true\n  },\n)\n\nexport type OAuthIssuerIdentifier = z.infer<typeof oauthIssuerIdentifierSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-par-response.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthParResponseSchema = z.object({\n  request_uri: z.string(),\n  expires_in: z.number().int().positive(),\n})\n\nexport type OAuthParResponse = z.infer<typeof oauthParResponseSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-password-grant-token-request.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthPasswordGrantTokenRequestSchema = z.object({\n  grant_type: z.literal('password'),\n  username: z.string(),\n  password: z.string(),\n})\n\nexport type OAuthPasswordGrantTokenRequest = z.infer<\n  typeof oauthPasswordGrantTokenRequestSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-prompt-mode.ts",
    "content": "import { z } from 'zod'\n\n/**\n * - \"none\" will only be allowed if the user already allowed the client on the same device\n * - \"login\" will force the user to login again, unless he very recently logged in\n * - \"consent\" will force the user to consent again\n * - \"select_account\" will force the user to select an account\n * - \"create\" will force the user registration screen\n */\nexport const oauthPromptModeSchema = z.enum([\n  'none',\n  'login',\n  'consent',\n  'select_account',\n  'create',\n])\n\nexport type OAuthPromptMode = z.infer<typeof oauthPromptModeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-protected-resource-metadata.ts",
    "content": "import { z } from 'zod'\nimport { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'\nimport { webUriSchema } from './uri.js'\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2}\n */\nexport const oauthProtectedResourceMetadataSchema = z.object({\n  /**\n   * REQUIRED. The protected resource's resource identifier, which is a URL that\n   * uses the https scheme and has no query or fragment components. Using these\n   * well-known resources is described in Section 3.\n   *\n   * @note This schema allows non https URLs for testing & development purposes.\n   * Make sure to validate the URL before using it in a production environment.\n   */\n  resource: webUriSchema\n    .refine((url) => !url.includes('?'), {\n      message: 'Resource URL must not contain query parameters',\n    })\n    .refine((url) => !url.includes('#'), {\n      message: 'Resource URL must not contain a fragment',\n    }),\n\n  /**\n   * OPTIONAL. JSON array containing a list of OAuth authorization server issuer\n   * identifiers, as defined in [RFC8414], for authorization servers that can be\n   * used with this protected resource. Protected resources MAY choose not to\n   * advertise some supported authorization servers even when this parameter is\n   * used. In some use cases, the set of authorization servers will not be\n   * enumerable, in which case this metadata parameter would not be used.\n   */\n  authorization_servers: z.array(oauthIssuerIdentifierSchema).optional(),\n\n  /**\n   * OPTIONAL. URL of the protected resource's JWK Set [JWK] document. This\n   * contains public keys belonging to the protected resource, such as signing\n   * key(s) that the resource server uses to sign resource responses. This URL\n   * MUST use the https scheme. When both signing and encryption keys are made\n   * available, a use (public key use) parameter value is REQUIRED for all keys\n   * in the referenced JWK Set to indicate each key's intended usage.\n   */\n  jwks_uri: webUriSchema.optional(),\n\n  /**\n   * RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope\n   * values that are used in authorization requests to request access to this\n   * protected resource. Protected resources MAY choose not to advertise some\n   * scope values supported even when this parameter is used.\n   */\n  scopes_supported: z.array(z.string()).optional(),\n\n  /**\n   * OPTIONAL. JSON array containing a list of the supported methods of sending\n   * an OAuth 2.0 Bearer Token [RFC6750] to the protected resource. Defined\n   * values are [\"header\", \"body\", \"query\"], corresponding to Sections 2.1, 2.2,\n   * and 2.3 of RFC 6750.\n   */\n  bearer_methods_supported: z\n    .array(z.enum(['header', 'body', 'query']))\n    .optional(),\n\n  /**\n   * OPTIONAL. JSON array containing a list of the JWS [JWS] signing algorithms\n   * (alg values) [JWA] supported by the protected resource for signing resource\n   * responses, for instance, as described in [FAPI.MessageSigning]. No default\n   * algorithms are implied if this entry is omitted. The value none MUST NOT be\n   * used.\n   */\n  resource_signing_alg_values_supported: z.array(z.string()).optional(),\n\n  /**\n   * OPTIONAL. URL of a page containing human-readable information that\n   * developers might want or need to know when using the protected resource\n   */\n  resource_documentation: webUriSchema.optional(),\n\n  /**\n   * OPTIONAL. URL that the protected resource provides to read about the\n   * protected resource's requirements on how the client can use the data\n   * provided by the protected resource\n   */\n  resource_policy_uri: webUriSchema.optional(),\n\n  /**\n   * OPTIONAL. URL that the protected resource provides to read about the\n   * protected resource's terms of service\n   */\n  resource_tos_uri: webUriSchema.optional(),\n})\n\nexport type OAuthProtectedResourceMetadata = z.infer<\n  typeof oauthProtectedResourceMetadataSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-redirect-uri.ts",
    "content": "import { TypeOf, ZodIssueCode, z } from 'zod'\nimport {\n  HttpsUri,\n  LoopbackUri,\n  PrivateUseUri,\n  httpsUriSchema,\n  loopbackUriSchema,\n  privateUseUriSchema,\n} from './uri.js'\n\n/**\n * This is a {@link loopbackUriSchema} with the additional restriction that\n * the hostname `localhost` is not allowed.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-8.3 Loopback Redirect Considerations} RFC8252\n *\n * > While redirect URIs using localhost (i.e.,\n * > \"http://localhost:{port}/{path}\") function similarly to loopback IP\n * > redirects described in Section 7.3, the use of localhost is NOT\n * > RECOMMENDED. Specifying a redirect URI with the loopback IP literal rather\n * > than localhost avoids inadvertently listening on network interfaces other\n * > than the loopback interface.  It is also less susceptible to client-side\n * > firewalls and misconfigured host name resolution on the user's device.\n */\nexport const loopbackRedirectURISchema = loopbackUriSchema.superRefine(\n  (value, ctx): value is Exclude<LoopbackUri, `http://localhost${string}`> => {\n    if (value.startsWith('http://localhost')) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message:\n          'Use of \"localhost\" hostname is not allowed (RFC 8252), use a loopback IP such as \"127.0.0.1\" instead',\n      })\n      return false\n    }\n\n    return true\n  },\n)\nexport type LoopbackRedirectURI = TypeOf<typeof loopbackRedirectURISchema>\n\nexport const oauthLoopbackClientRedirectUriSchema = loopbackRedirectURISchema\nexport type OAuthLoopbackRedirectURI = TypeOf<\n  typeof oauthLoopbackClientRedirectUriSchema\n>\n\nexport const oauthRedirectUriSchema = z\n  .string()\n  .superRefine(\n    (value, ctx): value is HttpsUri | LoopbackRedirectURI | PrivateUseUri => {\n      if (value.startsWith('https:')) {\n        const result = httpsUriSchema.safeParse(value)\n        if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)\n        return result.success\n      } else if (value.startsWith('http:')) {\n        const result = loopbackRedirectURISchema.safeParse(value)\n        if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)\n        return result.success\n      } else if (/^[^.:]+(?:\\.[^.:]+)+:/.test(value)) {\n        const result = privateUseUriSchema.safeParse(value)\n        if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)\n        return result.success\n      } else {\n        ctx.addIssue({\n          code: ZodIssueCode.custom,\n          message:\n            'URL must use the \"https:\" or \"http:\" protocol, or a private-use URI scheme (RFC 8252)',\n        })\n        return false\n      }\n    },\n  )\n\nexport type OAuthRedirectUri = TypeOf<typeof oauthRedirectUriSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-refresh-token-grant-token-request.ts",
    "content": "import { z } from 'zod'\nimport { oauthRefreshTokenSchema } from './oauth-refresh-token.js'\n\nexport const oauthRefreshTokenGrantTokenRequestSchema = z.object({\n  grant_type: z.literal('refresh_token'),\n  refresh_token: oauthRefreshTokenSchema,\n})\n\nexport type OAuthRefreshTokenGrantTokenRequest = z.infer<\n  typeof oauthRefreshTokenGrantTokenRequestSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-refresh-token.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthRefreshTokenSchema = z.string().min(1)\nexport type OAuthRefreshToken = z.infer<typeof oauthRefreshTokenSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-request-uri.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthRequestUriSchema = z.string().min(1)\n\nexport type OAuthRequestUri = z.infer<typeof oauthRequestUriSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-response-mode.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthResponseModeSchema = z.enum([\n  'query',\n  'fragment',\n  'form_post',\n])\n\nexport type OAuthResponseMode = z.infer<typeof oauthResponseModeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-response-type.ts",
    "content": "import { z } from 'zod'\n\nexport const oauthResponseTypeSchema = z.enum([\n  // OAuth2 (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-4.1.1)\n  'code', // Authorization Code Grant\n  'token', // Implicit Grant\n\n  // OIDC (https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)\n  'none',\n  'code id_token token',\n  'code id_token',\n  'code token',\n  'id_token token',\n  'id_token',\n])\n\nexport type OAuthResponseType = z.infer<typeof oauthResponseTypeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-scope.ts",
    "content": "import { z } from 'zod'\n\n// scope       = scope-token *( SP scope-token )\n// scope-token = 1*( %x21 / %x23-5B / %x5D-7E )\nexport const OAUTH_SCOPE_REGEXP =\n  /^[\\x21\\x23-\\x5B\\x5D-\\x7E]+(?: [\\x21\\x23-\\x5B\\x5D-\\x7E]+)*$/\n\nexport const isOAuthScope = (input: string): boolean =>\n  OAUTH_SCOPE_REGEXP.test(input)\n\n/**\n * A (single) space separated list of non empty printable ASCII char string\n * (except backslash and double quote).\n *\n * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1}\n */\nexport const oauthScopeSchema = z.string().refine(isOAuthScope, {\n  message: 'Invalid OAuth scope',\n})\n\nexport type OAuthScope = z.infer<typeof oauthScopeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-token-identification.ts",
    "content": "import { z } from 'zod'\nimport { oauthAccessTokenSchema } from './oauth-access-token.js'\nimport { oauthRefreshTokenSchema } from './oauth-refresh-token.js'\n\nexport const oauthTokenIdentificationSchema = z.object({\n  token: z.union([oauthAccessTokenSchema, oauthRefreshTokenSchema]),\n  token_type_hint: z.enum(['access_token', 'refresh_token']).optional(),\n})\n\nexport type OAuthTokenIdentification = z.infer<\n  typeof oauthTokenIdentificationSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-token-request.ts",
    "content": "import { z } from 'zod'\nimport { oauthAuthorizationCodeGrantTokenRequestSchema } from './oauth-authorization-code-grant-token-request.js'\nimport { oauthClientCredentialsGrantTokenRequestSchema } from './oauth-client-credentials-grant-token-request.js'\nimport { oauthPasswordGrantTokenRequestSchema } from './oauth-password-grant-token-request.js'\nimport { oauthRefreshTokenGrantTokenRequestSchema } from './oauth-refresh-token-grant-token-request.js'\n\nexport const oauthTokenRequestSchema = z.discriminatedUnion('grant_type', [\n  oauthAuthorizationCodeGrantTokenRequestSchema,\n  oauthRefreshTokenGrantTokenRequestSchema,\n  oauthPasswordGrantTokenRequestSchema,\n  oauthClientCredentialsGrantTokenRequestSchema,\n])\n\nexport type OAuthTokenRequest = z.infer<typeof oauthTokenRequestSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-token-response.ts",
    "content": "import { z } from 'zod'\nimport { signedJwtSchema } from '@atproto/jwk'\nimport { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js'\nimport { oauthTokenTypeSchema } from './oauth-token-type.js'\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1 | RFC 6749 (OAuth2), Section 5.1}\n */\nexport const oauthTokenResponseSchema = z\n  .object({\n    // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1\n    access_token: z.string(),\n    token_type: oauthTokenTypeSchema,\n    scope: z.string().optional(),\n    refresh_token: z.string().optional(),\n    expires_in: z.number().optional(),\n    // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse\n    id_token: signedJwtSchema.optional(),\n    // https://datatracker.ietf.org/doc/html/rfc9396#name-enriched-authorization-deta\n    authorization_details: oauthAuthorizationDetailsSchema.optional(),\n  })\n  // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1\n  // > The client MUST ignore unrecognized value names in the response.\n  .passthrough()\n\n/**\n * @see {@link oauthTokenResponseSchema}\n */\nexport type OAuthTokenResponse = z.infer<typeof oauthTokenResponseSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oauth-token-type.ts",
    "content": "import { z } from 'zod'\n\n// Case insensitive input, normalized output\nexport const oauthTokenTypeSchema = z.union([\n  z\n    .string()\n    .regex(/^DPoP$/i)\n    .transform(() => 'DPoP' as const),\n  z\n    .string()\n    .regex(/^Bearer$/i)\n    .transform(() => 'Bearer' as const),\n])\n\nexport type OAuthTokenType = z.infer<typeof oauthTokenTypeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oidc-authorization-error-response.ts",
    "content": "import { z } from 'zod'\n\n/**\n * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthError}\n */\nexport const oidcAuthorizationResponseErrorSchema = z.enum([\n  // The Authorization Server requires End-User interaction of some form to proceed. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User interaction.\n  'interaction_required',\n  // The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User authentication.\n  'login_required',\n  // The End-User is REQUIRED to select a session at the Authorization Server. The End-User MAY be authenticated at the Authorization Server with different associated accounts, but the End-User did not select a session. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface to prompt for a session to use.\n  'account_selection_required',\n  // The Authorization Server requires End-User consent. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for End-User consent.\n  'consent_required',\n  // The request_uri in the Authorization Request returns an error or contains invalid data.\n  'invalid_request_uri',\n  // The request parameter contains an invalid Request Object.\n  'invalid_request_object',\n  // The OP does not support use of the request parameter defined in Section 6.\n  'request_not_supported',\n  // The OP does not support use of the request_uri parameter defined in Section 6.\n  'request_uri_not_supported',\n  // The OP does not support use of the registration parameter defined in Section 7.2.1.\n  'registration_not_supported',\n])\n\nexport type OidcAuthorizationResponseError = z.infer<\n  typeof oidcAuthorizationResponseErrorSchema\n>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oidc-claims-parameter.ts",
    "content": "import { z } from 'zod'\n\nexport const oidcClaimsParameterSchema = z.enum([\n  // https://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html#rfc.section.5.2\n  // if client metadata \"require_auth_time\" is true, this *must* be provided\n  'auth_time',\n\n  // OIDC\n  'nonce',\n  'acr',\n\n  // OpenID: \"profile\" scope\n  'name',\n  'family_name',\n  'given_name',\n  'middle_name',\n  'nickname',\n  'preferred_username',\n  'gender',\n  'picture',\n  'profile',\n  'website',\n  'birthdate',\n  'zoneinfo',\n  'locale',\n  'updated_at',\n\n  // OpenID: \"email\" scope\n  'email',\n  'email_verified',\n\n  // OpenID: \"phone\" scope\n  'phone_number',\n  'phone_number_verified',\n\n  // OpenID: \"address\" scope\n  'address',\n])\n\nexport type OidcClaimsParameter = z.infer<typeof oidcClaimsParameterSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oidc-claims-properties.ts",
    "content": "import { z } from 'zod'\n\nconst oidcClaimsValueSchema = z.union([z.string(), z.number(), z.boolean()])\n\nexport const oidcClaimsPropertiesSchema = z.object({\n  essential: z.boolean().optional(),\n  value: oidcClaimsValueSchema.optional(),\n  values: z.array(oidcClaimsValueSchema).optional(),\n})\n\nexport type OidcClaimsProperties = z.infer<typeof oidcClaimsPropertiesSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oidc-entity-type.ts",
    "content": "import { z } from 'zod'\n\nexport const oidcEntityTypeSchema = z.enum(['userinfo', 'id_token'])\n\nexport type OidcEntityType = z.infer<typeof oidcEntityTypeSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/oidc-userinfo.ts",
    "content": "import { z } from 'zod'\n\nexport const oidcUserinfoSchema = z.object({\n  sub: z.string(),\n  iss: z.string().url().optional(),\n  aud: z.union([z.string(), z.array(z.string()).min(1)]).optional(),\n\n  email: z.string().email().optional(),\n  email_verified: z.boolean().optional(),\n  name: z.string().optional(),\n  preferred_username: z.string().optional(),\n  picture: z.string().url().optional(),\n})\n\nexport type OidcUserinfo = z.infer<typeof oidcUserinfoSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/uri.ts",
    "content": "import { TypeOf, ZodIssueCode, z } from 'zod'\nimport {\n  canParseUrl,\n  isHostnameIP,\n  isLocalHostname,\n  isLoopbackHost,\n} from './util.js'\n\n/**\n * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).\n *\n * Any value that matches this schema is safe to parse using `new URL()`.\n */\nexport const dangerousUriSchema = z\n  .string()\n  .refine(\n    (data): data is `${string}:${string}` =>\n      data.includes(':') && canParseUrl(data),\n    {\n      message: 'Invalid URL',\n    },\n  )\n\n/**\n * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).\n */\nexport type DangerousUrl = TypeOf<typeof dangerousUriSchema>\n\nexport const loopbackUriSchema = dangerousUriSchema.superRefine(\n  (\n    value,\n    ctx,\n  ): value is\n    | `http://[::1]${string}`\n    | `http://localhost${'' | `${':' | '/' | '?' | '#'}${string}`}`\n    | `http://127.0.0.1${'' | `${':' | '/' | '?' | '#'}${string}`}` => {\n    // Loopback url must use the \"http:\" protocol\n    if (!value.startsWith('http://')) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: 'URL must use the \"http:\" protocol',\n      })\n      return false\n    }\n\n    const url = new URL(value)\n\n    if (!isLoopbackHost(url.hostname)) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: 'URL must use \"localhost\", \"127.0.0.1\" or \"[::1]\" as hostname',\n      })\n      return false\n    }\n\n    return true\n  },\n)\n\nexport type LoopbackUri = TypeOf<typeof loopbackUriSchema>\n\nexport const httpsUriSchema = dangerousUriSchema.superRefine(\n  (value, ctx): value is `https://${string}` => {\n    if (!value.startsWith('https://')) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: 'URL must use the \"https:\" protocol',\n      })\n      return false\n    }\n\n    const url = new URL(value)\n\n    // Disallow loopback URLs with the `https:` protocol\n    if (isLoopbackHost(url.hostname)) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: 'https: URL must not use a loopback host',\n      })\n      return false\n    }\n\n    if (isHostnameIP(url.hostname)) {\n      // Hostname is an IP address\n    } else {\n      // Hostname is a domain name\n      if (!url.hostname.includes('.')) {\n        // we don't depend on PSL here, so we only check for a dot\n        ctx.addIssue({\n          code: ZodIssueCode.custom,\n          message: 'Domain name must contain at least two segments',\n        })\n        return false\n      }\n\n      if (url.hostname.endsWith('.local')) {\n        ctx.addIssue({\n          code: ZodIssueCode.custom,\n          message: 'Domain name must not end with \".local\"',\n        })\n        return false\n      }\n    }\n\n    return true\n  },\n)\n\nexport type HttpsUri = TypeOf<typeof httpsUriSchema>\n\nexport const webUriSchema = z\n  .string()\n  .superRefine((value, ctx): value is LoopbackUri | HttpsUri => {\n    // discriminated union of `loopbackUriSchema` and `httpsUriSchema`\n    if (value.startsWith('http://')) {\n      const result = loopbackUriSchema.safeParse(value)\n      if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)\n      return result.success\n    }\n\n    if (value.startsWith('https://')) {\n      const result = httpsUriSchema.safeParse(value)\n      if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)\n      return result.success\n    }\n\n    ctx.addIssue({\n      code: ZodIssueCode.custom,\n      message: 'URL must use the \"http:\" or \"https:\" protocol',\n    })\n    return false\n  })\n\nexport type WebUri = TypeOf<typeof webUriSchema>\n\nexport const privateUseUriSchema = dangerousUriSchema.superRefine(\n  (value, ctx): value is `${string}.${string}:/${string}` => {\n    const dotIdx = value.indexOf('.')\n    const colonIdx = value.indexOf(':')\n\n    // Optimization: avoid parsing the URL if the protocol does not contain a \".\"\n    if (dotIdx === -1 || colonIdx === -1 || dotIdx > colonIdx) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message:\n          'Private-use URI scheme requires a \".\" as part of the protocol',\n      })\n      return false\n    }\n\n    const url = new URL(value)\n\n    // Should be covered by the check before, but let's be extra sure\n    if (!url.protocol.includes('.')) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: 'Invalid private-use URI scheme',\n      })\n      return false\n    }\n\n    // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1\n    //\n    // > When choosing a URI scheme to associate with the app, apps MUST use a\n    // > URI scheme based on a domain name under their control, expressed in\n    // > reverse order\n    //\n    // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4\n    //\n    // > In addition to the collision-resistant properties, requiring a URI\n    // > scheme based on a domain name that is under the control of the app can\n    // > help to prove ownership in the event of a dispute where two apps claim\n    // > the same private-use URI scheme (where one app is acting maliciously).\n    //\n    // We can't check for ownership here (as there is no concept of proven\n    // ownership in a generic validation logic), besides excluding local domains\n    // as they can't be controlled/owned by the app.\n    //\n    // https://atproto.com/specs/oauth\n    //\n    // > Any custom scheme must match the `client_id` hostname in reverse-domain\n    // > order.\n    //\n    // This ATPROTO specific requirement cannot be enforced here, (as there is\n    // no concept of `client_id` in this context).\n\n    const uriScheme = url.protocol.slice(0, -1) // remove trailing \":\"\n    const urlDomain = uriScheme.split('.').reverse().join('.')\n\n    if (isLocalHostname(urlDomain)) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message: `Private-use URI Scheme redirect URI must not be a local hostname`,\n      })\n    }\n\n    // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1\n    //\n    // > Following the requirements of Section 3.2 of [RFC3986], as there is no\n    // > naming authority for private-use URI scheme redirects, only a single\n    // > slash (\"/\") appears after the scheme component.\n    if (\n      url.href.startsWith(`${url.protocol}//`) ||\n      url.username ||\n      url.password ||\n      url.hostname ||\n      url.port\n    ) {\n      ctx.addIssue({\n        code: ZodIssueCode.custom,\n        message:\n          'Private-Use URI Scheme must be in the form <scheme>:/{path} (notice the single slash!) as per RFC 8252',\n      })\n      return false\n    }\n\n    return true\n  },\n)\n\nexport type PrivateUseUri = TypeOf<typeof privateUseUriSchema>\n"
  },
  {
    "path": "packages/oauth/oauth-types/src/util.ts",
    "content": "export const canParseUrl =\n  // eslint-disable-next-line n/no-unsupported-features/node-builtins\n  URL.canParse?.bind(URL) ??\n  // URL.canParse is not available in Node.js < 18.7.0\n  ((urlStr: string): boolean => {\n    try {\n      new URL(urlStr)\n      return true\n    } catch {\n      return false\n    }\n  })\n\nexport function isHostnameIP(hostname: string) {\n  // IPv4\n  if (hostname.match(/^\\d+\\.\\d+\\.\\d+\\.\\d+$/)) return true\n\n  // IPv6\n  if (hostname.startsWith('[') && hostname.endsWith(']')) return true\n\n  return false\n}\n\nexport type LoopbackHost = 'localhost' | '127.0.0.1' | '[::1]'\n\nexport function isLoopbackHost(host: unknown): host is LoopbackHost {\n  return host === 'localhost' || host === '127.0.0.1' || host === '[::1]'\n}\n\nexport function isLocalHostname(hostname: string): boolean {\n  const parts = hostname.split('.')\n  if (parts.length < 2) return true\n\n  const tld = parts.at(-1)!.toLowerCase()\n  return (\n    tld === 'test' ||\n    tld === 'local' ||\n    tld === 'localhost' ||\n    tld === 'invalid' ||\n    tld === 'example'\n  )\n}\n\nexport function safeUrl(input: URL | string): URL | null {\n  try {\n    return new URL(input)\n  } catch {\n    return null\n  }\n}\n\nexport function extractUrlPath(url) {\n  // Extracts the path from a URL, without relying on the URL constructor\n  // (because it normalizes the URL)\n  const endOfProtocol = url.startsWith('https://')\n    ? 8\n    : url.startsWith('http://')\n      ? 7\n      : -1\n  if (endOfProtocol === -1) {\n    throw new TypeError('URL must use the \"https:\" or \"http:\" protocol')\n  }\n\n  const hashIdx = url.indexOf('#', endOfProtocol)\n  const questionIdx = url.indexOf('?', endOfProtocol)\n\n  const queryStrIdx =\n    questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx)\n      ? questionIdx\n      : -1\n\n  const pathEnd =\n    hashIdx === -1\n      ? queryStrIdx === -1\n        ? url.length\n        : queryStrIdx\n      : queryStrIdx === -1\n        ? hashIdx\n        : Math.min(hashIdx, queryStrIdx)\n\n  const slashIdx = url.indexOf('/', endOfProtocol)\n\n  const pathStart = slashIdx === -1 || slashIdx > pathEnd ? pathEnd : slashIdx\n\n  if (endOfProtocol === pathStart) {\n    throw new TypeError('URL must contain a host')\n  }\n\n  return url.substring(pathStart, pathEnd)\n}\n\nexport const jsonObjectPreprocess = (val: unknown) => {\n  if (typeof val === 'string' && val.startsWith('{') && val.endsWith('}')) {\n    try {\n      return JSON.parse(val)\n    } catch {\n      return val\n    }\n  }\n\n  return val\n}\n\nexport const numberPreprocess = (val: unknown): unknown => {\n  if (typeof val === 'string') {\n    const number = Number(val)\n    if (!Number.isNaN(number)) return number\n  }\n  return val\n}\n\n/**\n * Returns true if the two arrays contain the same elements, regardless of order\n * or duplicates.\n */\nexport function arrayEquivalent<T>(a: readonly T[], b: readonly T[]) {\n  if (a === b) return true\n  return a.every(includedIn, b) && b.every(includedIn, a)\n}\n\nexport function includedIn<T>(this: readonly T[], item: T) {\n  return this.includes(item)\n}\n\nexport function asArray<T>(\n  value: Iterable<T> | undefined,\n): undefined | readonly T[] {\n  if (value == null) return undefined\n  if (Array.isArray(value)) return value // already a (possibly readonly) array\n  return Array.from(value)\n}\n\nexport type SpaceSeparatedValue<Value extends string> =\n  `${'' | `${string} `}${Value}${'' | ` ${string}`}`\n\nexport const isSpaceSeparatedValue = <Value extends string>(\n  value: Value,\n  input: string,\n): input is SpaceSeparatedValue<Value> => {\n  if (value.length === 0) throw new TypeError('Value cannot be empty')\n  if (value.includes(' ')) throw new TypeError('Value cannot contain spaces')\n\n  // Optimized version of:\n  // return input.split(' ').includes(value)\n\n  const inputLength = input.length\n  const valueLength = value.length\n\n  if (inputLength < valueLength) return false\n\n  let idx = input.indexOf(value)\n  let idxEnd: number\n\n  while (idx !== -1) {\n    idxEnd = idx + valueLength\n\n    if (\n      // at beginning or preceded by space\n      (idx === 0 || input.charCodeAt(idx - 1) === 32) &&\n      // at end or followed by space\n      (idxEnd === inputLength || input.charCodeAt(idxEnd) === 32)\n    ) {\n      return true\n    }\n\n    idx = input.indexOf(value, idxEnd + 1)\n  }\n\n  return false\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/oauth/oauth-types/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/ozone/CHANGELOG.md",
    "content": "# @atproto/ozone\n\n## 0.1.167\n\n### Patch Changes\n\n- [#4709](https://github.com/bluesky-social/atproto/pull/4709) [`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa) Thanks [@foysalit](https://github.com/foysalit)! - Introduce a purge event to remove ozone's data on age assurance\n\n- Updated dependencies [[`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa), [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f)]:\n  - @atproto/api@0.19.4\n  - @atproto/syntax@0.5.1\n  - @atproto/xrpc-server@0.10.16\n\n## 0.1.166\n\n### Patch Changes\n\n- [#4683](https://github.com/bluesky-social/atproto/pull/4683) [`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Introduce recIdStr field\n\n- Updated dependencies [[`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c), [`0e5df95`](https://github.com/bluesky-social/atproto/commit/0e5df95e3a8d81931524848d301cd43d1f12fb78)]:\n  - @atproto/api@0.19.2\n\n## 0.1.165\n\n### Patch Changes\n\n- [#4704](https://github.com/bluesky-social/atproto/pull/4704) [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Add feed to sendInteractions input\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common@0.5.14\n  - @atproto/xrpc-server@0.10.15\n  - @atproto/api@0.19.1\n  - @atproto/lexicon@0.6.2\n\n## 0.1.164\n\n### Patch Changes\n\n- Updated dependencies [[`450f085`](https://github.com/bluesky-social/atproto/commit/450f0856630fa08c20dc60fef8b5d2a07b9a2552)]:\n  - @atproto/api@0.19.0\n\n## 0.1.163\n\n### Patch Changes\n\n- [#4608](https://github.com/bluesky-social/atproto/pull/4608) [`9fdfb8a`](https://github.com/bluesky-social/atproto/commit/9fdfb8a3ad656b158fa54f5d2a79f45cb6d16f4d) Thanks [@ThisIsMissEm](https://github.com/ThisIsMissEm)! - Prevent tools.ozone.verification.grantVerification from verifying handle.invalid\n\n- Updated dependencies [[`00e6dbd`](https://github.com/bluesky-social/atproto/commit/00e6dbdcea295cfa3dff7eb7517420039cc3e821)]:\n  - @atproto/identity@0.4.11\n  - @atproto/common@0.5.11\n  - @atproto/xrpc-server@0.10.12\n\n## 0.1.162\n\n### Patch Changes\n\n- [#4581](https://github.com/bluesky-social/atproto/pull/4581) [`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7) Thanks [@mozzius](https://github.com/mozzius)! - Add `presentation` to video embed as a hint to the client about how to display the video\n\n- Updated dependencies [[`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7)]:\n  - @atproto/api@0.18.18\n\n## 0.1.161\n\n### Patch Changes\n\n- [#4451](https://github.com/bluesky-social/atproto/pull/4451) [`5605e4d`](https://github.com/bluesky-social/atproto/commit/5605e4d619cd16b11e506e0ebc893923c74296ed) Thanks [@foysalit](https://github.com/foysalit)! - Avoid repo_push_event insertion if no rows are created\n\n- Updated dependencies [[`2e5a24c`](https://github.com/bluesky-social/atproto/commit/2e5a24cb875650120365e3f5c23a041e61a5f9c4), [`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957), [`5622bcf`](https://github.com/bluesky-social/atproto/commit/5622bcf02315f9f24940a32aa3a6d9341c646c59)]:\n  - @atproto/api@0.18.8\n  - @atproto/ws-client@0.0.4\n  - @atproto/xrpc-server@0.10.4\n\n## 0.1.160\n\n### Patch Changes\n\n- [#4419](https://github.com/bluesky-social/atproto/pull/4419) [`cfa01ed`](https://github.com/bluesky-social/atproto/commit/cfa01edb9cd769b49327b8875b890d84fa8956d2) Thanks [@foysalit](https://github.com/foysalit)! - Fix email delivery status for ozone send email event\n\n- [#4423](https://github.com/bluesky-social/atproto/pull/4423) [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763) Thanks [@foysalit](https://github.com/foysalit)! - Add min length for required comment fields in ozone events\n\n- [#4427](https://github.com/bluesky-social/atproto/pull/4427) [`7eb99f2`](https://github.com/bluesky-social/atproto/commit/7eb99f2ac7049ddf8aea050e77e0236c1277909a) Thanks [@foysalit](https://github.com/foysalit)! - Store and expose age assurance event's access property\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`380aa3b`](https://github.com/bluesky-social/atproto/commit/380aa3bfe73b5c4e59961c27ae988786b69c129d), [`308f432`](https://github.com/bluesky-social/atproto/commit/308f432f7aef196b4df0a6dc7c5367ab5a8b8964), [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/api@0.18.5\n  - @atproto/common@0.5.3\n  - @atproto/xrpc-server@0.10.3\n  - @atproto/xrpc@0.7.7\n\n## 0.1.159\n\n### Patch Changes\n\n- [#4407](https://github.com/bluesky-social/atproto/pull/4407) [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds ageassurance namespace, methods, and utils for Age Assurance V2\n\n- Updated dependencies [[`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7)]:\n  - @atproto/syntax@0.4.2\n  - @atproto/crypto@0.4.5\n  - @atproto/api@0.18.4\n  - @atproto/common@0.5.2\n  - @atproto/xrpc-server@0.10.2\n\n## 0.1.158\n\n### Patch Changes\n\n- [#4347](https://github.com/bluesky-social/atproto/pull/4347) [`69f53d6`](https://github.com/bluesky-social/atproto/commit/69f53d632d84f255cafa8b10698184048a71b97b) Thanks [@bnewbold](https://github.com/bnewbold)! - lexicon updates to have fully-qualified token refs in knownValue lists\n\n- Updated dependencies [[`69f53d6`](https://github.com/bluesky-social/atproto/commit/69f53d632d84f255cafa8b10698184048a71b97b)]:\n  - @atproto/api@0.18.3\n  - @atproto/common@0.5.1\n  - @atproto/xrpc-server@0.10.1\n\n## 0.1.157\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/xrpc-server@0.10.0\n  - @atproto/common@0.5.0\n  - @atproto/api@0.18.2\n  - @atproto/identity@0.4.10\n  - @atproto/lexicon@0.5.2\n  - @atproto/crypto@0.4.4\n  - @atproto/ws-client@0.0.3\n  - @atproto/xrpc@0.7.6\n\n## 0.1.156\n\n### Patch Changes\n\n- [#4340](https://github.com/bluesky-social/atproto/pull/4340) [`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e) Thanks [@foysalit](https://github.com/foysalit)! - Add optional email data to scheduled action api in ozone\n\n- [#4344](https://github.com/bluesky-social/atproto/pull/4344) [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc) Thanks [@foysalit](https://github.com/foysalit)! - Add targetServices param to takedown events allowing mods to specify which service to apply takedown on\n\n- Updated dependencies [[`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e), [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc), [`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/api@0.18.1\n  - @atproto/xrpc-server@0.9.6\n  - @atproto/ws-client@0.0.2\n\n## 0.1.155\n\n### Patch Changes\n\n- [#4330](https://github.com/bluesky-social/atproto/pull/4330) [`3628cebfb`](https://github.com/bluesky-social/atproto/commit/3628cebfbb04ba49f326bbf411a2d15de2900302) Thanks [@mistydemeo](https://github.com/mistydemeo)! - adjust explicit-slurs regex\n\n- [#4335](https://github.com/bluesky-social/atproto/pull/4335) [`82e75bf6c`](https://github.com/bluesky-social/atproto/commit/82e75bf6c1b31daa834386edce35c8aa4c787229) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove non-existing `reporter_stats` from materliaziled view to refresh\n\n## 0.1.154\n\n### Patch Changes\n\n- Updated dependencies [[`94ddc8219`](https://github.com/bluesky-social/atproto/commit/94ddc8219c144475df622137ab88895255136eda), [`39b5c08e0`](https://github.com/bluesky-social/atproto/commit/39b5c08e0799468eba0c3bf50f4f5a8104c35f34)]:\n  - @atproto/api@0.18.0\n\n## 0.1.153\n\n### Patch Changes\n\n- Updated dependencies [[`15fe80c39`](https://github.com/bluesky-social/atproto/commit/15fe80c39ff428652dfaa6b30c0bdb59a145aac6)]:\n  - @atproto/api@0.17.7\n\n## 0.1.152\n\n### Patch Changes\n\n- Updated dependencies [[`7c1429fe3`](https://github.com/bluesky-social/atproto/commit/7c1429fe36226d0d57e57c037ba4221d2fbd57ee)]:\n  - @atproto/api@0.17.6\n\n## 0.1.151\n\n### Patch Changes\n\n- [#4279](https://github.com/bluesky-social/atproto/pull/4279) [`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031) Thanks [@foysalit](https://github.com/foysalit)! - Add strike system to ozone\n\n- Updated dependencies [[`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031)]:\n  - @atproto/api@0.17.5\n\n## 0.1.150\n\n### Patch Changes\n\n- Updated dependencies [[`a8e307ef4`](https://github.com/bluesky-social/atproto/commit/a8e307ef4851b164ee38bb5149343631e329f143)]:\n  - @atproto/api@0.17.4\n\n## 0.1.149\n\n### Patch Changes\n\n- [#4274](https://github.com/bluesky-social/atproto/pull/4274) [`ca768fe1b`](https://github.com/bluesky-social/atproto/commit/ca768fe1b0ba1662140b6eea550683d8675fa56e) Thanks [@foysalit](https://github.com/foysalit)! - Fix fetching event detail for subject with blob\n\n- Updated dependencies [[`386f583cf`](https://github.com/bluesky-social/atproto/commit/386f583cffa2c596a12be4e98dde498f3b8670f6)]:\n  - @atproto/api@0.17.3\n\n## 0.1.148\n\n### Patch Changes\n\n- [#4262](https://github.com/bluesky-social/atproto/pull/4262) [`1cb5b9b80`](https://github.com/bluesky-social/atproto/commit/1cb5b9b80c20a054f7fbacd89d0d440dc2241d81) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Finalize report reason lexicons, update migration map in Ozone\n\n- Updated dependencies [[`1cb5b9b80`](https://github.com/bluesky-social/atproto/commit/1cb5b9b80c20a054f7fbacd89d0d440dc2241d81)]:\n  - @atproto/api@0.17.2\n\n## 0.1.147\n\n### Patch Changes\n\n- [#4241](https://github.com/bluesky-social/atproto/pull/4241) [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd) Thanks [@foysalit](https://github.com/foysalit)! - Add scheduled action api to ozone\n\n- Updated dependencies [[`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd)]:\n  - @atproto/api@0.17.1\n\n## 0.1.146\n\n### Patch Changes\n\n- Updated dependencies [[`dba2d30e2`](https://github.com/bluesky-social/atproto/commit/dba2d30e2c4ce0eb624f2139b485719d14474940), [`7f38ee03c`](https://github.com/bluesky-social/atproto/commit/7f38ee03c01357686a4ce54cdf8eed4e37074a58)]:\n  - @atproto/api@0.17.0\n\n## 0.1.145\n\n### Patch Changes\n\n- Updated dependencies [[`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33), [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33)]:\n  - @atproto/api@0.16.11\n\n## 0.1.144\n\n### Patch Changes\n\n- Updated dependencies [[`8dc4caf55`](https://github.com/bluesky-social/atproto/commit/8dc4caf55840578c835b4c851d4a599c15627a78)]:\n  - @atproto/api@0.16.10\n\n## 0.1.143\n\n### Patch Changes\n\n- Updated dependencies [[`ff30786af`](https://github.com/bluesky-social/atproto/commit/ff30786af6f72ad6506939bfca01a3f55a096c1c)]:\n  - @atproto/api@0.16.9\n\n## 0.1.142\n\n### Patch Changes\n\n- [#4170](https://github.com/bluesky-social/atproto/pull/4170) [`55cc15cdd`](https://github.com/bluesky-social/atproto/commit/55cc15cdd664865d53f027e63708226012dc39ef) Thanks [@foysalit](https://github.com/foysalit)! - Add ozone proxy for revoke credentials\n\n## 0.1.141\n\n### Patch Changes\n\n- [#3881](https://github.com/bluesky-social/atproto/pull/3881) [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add expanded moderation report reasons as outlined in\n  [RFC-0009](https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity)\n- Updated dependencies [[`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523)]:\n  - @atproto/api@0.16.8\n  - @atproto/common@0.4.12\n  - @atproto/identity@0.4.9\n  - @atproto/lexicon@0.5.1\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.9.5\n  - @atproto/xrpc@0.7.5\n\n## 0.1.140\n\n### Patch Changes\n\n- Updated dependencies [[`09717f29a`](https://github.com/bluesky-social/atproto/commit/09717f29ac7ca742c9c3310980dbe4d112b7597f)]:\n  - @atproto/api@0.16.7\n\n## 0.1.139\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/xrpc@0.7.4\n  - @atproto/api@0.16.6\n  - @atproto/xrpc-server@0.9.4\n\n## 0.1.138\n\n### Patch Changes\n\n- [#4142](https://github.com/bluesky-social/atproto/pull/4142) [`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - add com.atproto.temp.revokeAccountCredentials lexicon schema\n\n- Updated dependencies [[`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6)]:\n  - @atproto/api@0.16.5\n\n## 0.1.137\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/api@0.16.4\n  - @atproto/xrpc@0.7.3\n  - @atproto/xrpc-server@0.9.3\n\n## 0.1.136\n\n### Patch Changes\n\n- [#4109](https://github.com/bluesky-social/atproto/pull/4109) [`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e) Thanks [@foysalit](https://github.com/foysalit)! - Add batchId filter to tools.ozone.moderation.queryEvents endpoint\n\n- Updated dependencies [[`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/api@0.16.3\n  - @atproto/lexicon@0.4.13\n  - @atproto/xrpc@0.7.2\n  - @atproto/xrpc-server@0.9.2\n\n## 0.1.135\n\n### Patch Changes\n\n- Updated dependencies [[`c370d933b`](https://github.com/bluesky-social/atproto/commit/c370d933b76b4e15b83a82b40d1b6a32bd54add6)]:\n  - @atproto/api@0.16.2\n\n## 0.1.134\n\n### Patch Changes\n\n- [#3927](https://github.com/bluesky-social/atproto/pull/3927) [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef) Thanks [@foysalit](https://github.com/foysalit)! - Introduces ozone event timeline lexicons\n\n- Updated dependencies [[`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671)]:\n  - @atproto/xrpc-server@0.9.1\n  - @atproto/api@0.16.1\n\n## 0.1.133\n\n### Patch Changes\n\n- Updated dependencies [[`9751eebd7`](https://github.com/bluesky-social/atproto/commit/9751eebd718066984a91046b63e410caecd64022)]:\n  - @atproto/api@0.16.0\n\n## 0.1.132\n\n### Patch Changes\n\n- Updated dependencies [[`8787fd9de`](https://github.com/bluesky-social/atproto/commit/8787fd9dea769716412c9883e355cd496664bc6e), [`dc84906c8`](https://github.com/bluesky-social/atproto/commit/dc84906c865e8a97939a909dd3f75decde538363)]:\n  - @atproto/api@0.15.27\n\n## 0.1.131\n\n### Patch Changes\n\n- [#4041](https://github.com/bluesky-social/atproto/pull/4041) [`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add `unregisterPush` API\n\n- [#4048](https://github.com/bluesky-social/atproto/pull/4048) [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e) Thanks [@foysalit](https://github.com/foysalit)! - Add externalId to ozone events for deduping events per subject and event type\n\n- [#4044](https://github.com/bluesky-social/atproto/pull/4044) [`5ae998797`](https://github.com/bluesky-social/atproto/commit/5ae9987972b7ab8f9d6740886ed56b552d5664dd) Thanks [@foysalit](https://github.com/foysalit)! - Allow filtering ozone event stream for age assurance events\n\n- Updated dependencies [[`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82), [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e)]:\n  - @atproto/api@0.15.26\n\n## 0.1.130\n\n### Patch Changes\n\n- Updated dependencies [[`88c136427`](https://github.com/bluesky-social/atproto/commit/88c136427451a20d21812a1aa88a70cf21904138)]:\n  - @atproto/api@0.15.25\n\n## 0.1.129\n\n### Patch Changes\n\n- [#4034](https://github.com/bluesky-social/atproto/pull/4034) [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1) Thanks [@foysalit](https://github.com/foysalit)! - Add age assurance event types to ozone lexicons\n\n- Updated dependencies [[`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/xrpc-server@0.9.0\n  - @atproto/api@0.15.24\n  - @atproto/lexicon@0.4.12\n  - @atproto/xrpc@0.7.1\n\n## 0.1.128\n\n### Patch Changes\n\n- [#3991](https://github.com/bluesky-social/atproto/pull/3991) [`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8) Thanks [@foysalit](https://github.com/foysalit)! - Add modTool parameter to ozone events\n\n- Updated dependencies [[`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8)]:\n  - @atproto/api@0.15.23\n\n## 0.1.127\n\n### Patch Changes\n\n- [#3945](https://github.com/bluesky-social/atproto/pull/3945) [`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b) Thanks [@foysalit](https://github.com/foysalit)! - Add safelink module in ozone\n\n- Updated dependencies [[`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b)]:\n  - @atproto/api@0.15.22\n\n## 0.1.126\n\n### Patch Changes\n\n- Updated dependencies [[`d344723a1`](https://github.com/bluesky-social/atproto/commit/d344723a1018b2436b5453526397936bd587a2e2)]:\n  - @atproto/api@0.15.21\n\n## 0.1.125\n\n### Patch Changes\n\n- [#4005](https://github.com/bluesky-social/atproto/pull/4005) [`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7) Thanks [@mozzius](https://github.com/mozzius)! - add `subscribed-post` notification reason\n\n- Updated dependencies [[`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7)]:\n  - @atproto/api@0.15.20\n\n## 0.1.124\n\n### Patch Changes\n\n- Updated dependencies [[`376778a92`](https://github.com/bluesky-social/atproto/commit/376778a92f08fb6709c4cde736bfaca7393a72e1)]:\n  - @atproto/api@0.15.19\n\n## 0.1.123\n\n### Patch Changes\n\n- Updated dependencies [[`e3e31b2b9`](https://github.com/bluesky-social/atproto/commit/e3e31b2b9bf8c4de6b2d7fa992c3b3795686ea72)]:\n  - @atproto/api@0.15.18\n\n## 0.1.122\n\n### Patch Changes\n\n- [#3990](https://github.com/bluesky-social/atproto/pull/3990) [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add activity subscription lexicons\n\n- Updated dependencies [[`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b)]:\n  - @atproto/api@0.15.17\n\n## 0.1.121\n\n### Patch Changes\n\n- [#3966](https://github.com/bluesky-social/atproto/pull/3966) [`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef) Thanks [@mozzius](https://github.com/mozzius)! - Rename notification preference lexicon \"filter\" key to \"include\"\n\n- Updated dependencies [[`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef)]:\n  - @atproto/api@0.15.16\n\n## 0.1.120\n\n### Patch Changes\n\n- Updated dependencies [[`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80), [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80)]:\n  - @atproto/api@0.15.15\n\n## 0.1.119\n\n### Patch Changes\n\n- Updated dependencies [[`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c)]:\n  - @atproto/xrpc-server@0.8.0\n\n## 0.1.118\n\n### Patch Changes\n\n- [#3901](https://github.com/bluesky-social/atproto/pull/3901) [`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1) Thanks [@mozzius](https://github.com/mozzius)! - Add notification preferences V2 lexicons\n\n- Updated dependencies [[`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1)]:\n  - @atproto/api@0.15.14\n\n## 0.1.117\n\n### Patch Changes\n\n- Updated dependencies [[`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d)]:\n  - @atproto/api@0.15.13\n\n## 0.1.116\n\n### Patch Changes\n\n- Updated dependencies [[`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f)]:\n  - @atproto/xrpc-server@0.7.19\n\n## 0.1.115\n\n### Patch Changes\n\n- Updated dependencies [[`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187)]:\n  - @atproto/api@0.15.12\n\n## 0.1.114\n\n### Patch Changes\n\n- Updated dependencies [[`a978681fd`](https://github.com/bluesky-social/atproto/commit/a978681fde1c138a5298bae77e5dc36ce155f955)]:\n  - @atproto/api@0.15.11\n\n## 0.1.113\n\n### Patch Changes\n\n- Updated dependencies [[`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9), [`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9)]:\n  - @atproto/api@0.15.10\n\n## 0.1.112\n\n### Patch Changes\n\n- [#3882](https://github.com/bluesky-social/atproto/pull/3882) [`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159) Thanks [@mozzius](https://github.com/mozzius)! - add a \"via\" field to reposts and likes allowing a reference a repost, and then give a notification when a repost is liked or reposted.\n\n- Updated dependencies [[`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159)]:\n  - @atproto/api@0.15.9\n\n## 0.1.111\n\n### Patch Changes\n\n- Updated dependencies [[`80f402f36`](https://github.com/bluesky-social/atproto/commit/80f402f3663af08fd048300738d04c67aa2b9cb8)]:\n  - @atproto/api@0.15.8\n\n## 0.1.110\n\n### Patch Changes\n\n- Updated dependencies [[`86b315388`](https://github.com/bluesky-social/atproto/commit/86b3153884099ceeb0cfdb9d2bfdd447c39fb35a)]:\n  - @atproto/api@0.15.7\n\n## 0.1.109\n\n### Patch Changes\n\n- [#3802](https://github.com/bluesky-social/atproto/pull/3802) [`3301a2697`](https://github.com/bluesky-social/atproto/commit/3301a2697f2ad32d4912ba4781c515755cf1386e) Thanks [@foysalit](https://github.com/foysalit)! - Disable default jetstream service url\n\n- [#3700](https://github.com/bluesky-social/atproto/pull/3700) [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Consistenlty log errors\n\n- Updated dependencies [[`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba), [`3a65b68f7`](https://github.com/bluesky-social/atproto/commit/3a65b68f7dc63c8bfbea0ae615f8ae984272f2e4)]:\n  - @atproto/xrpc@0.7.0\n  - @atproto/lexicon@0.4.11\n  - @atproto/xrpc-server@0.7.18\n  - @atproto/api@0.15.6\n  - @atproto/common@0.4.11\n  - @atproto/identity@0.4.8\n  - @atproto/crypto@0.4.4\n\n## 0.1.108\n\n### Patch Changes\n\n- [#3765](https://github.com/bluesky-social/atproto/pull/3765) [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9) Thanks [@foysalit](https://github.com/foysalit)! - Add verification lexicons to ozone\n\n- Updated dependencies [[`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9), [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9)]:\n  - @atproto/api@0.15.5\n  - @atproto/xrpc-server@0.7.17\n\n## 0.1.107\n\n### Patch Changes\n\n- [#3768](https://github.com/bluesky-social/atproto/pull/3768) [`7af77f3ed`](https://github.com/bluesky-social/atproto/commit/7af77f3edfe52f77729f61de4188e8375f03b4ef) Thanks [@devinivy](https://github.com/devinivy)! - Support tools.ozone.hosting.getAccountHistory\n\n- Updated dependencies [[`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c), [`7af77f3ed`](https://github.com/bluesky-social/atproto/commit/7af77f3edfe52f77729f61de4188e8375f03b4ef)]:\n  - @atproto/xrpc-server@0.7.16\n  - @atproto/api@0.15.4\n\n## 0.1.106\n\n### Patch Changes\n\n- Updated dependencies [[`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9)]:\n  - @atproto/api@0.15.3\n\n## 0.1.105\n\n### Patch Changes\n\n- Updated dependencies [[`553c988f1`](https://github.com/bluesky-social/atproto/commit/553c988f1d226b3d2fbe94c117b088f5c82db794)]:\n  - @atproto/api@0.15.2\n\n## 0.1.104\n\n### Patch Changes\n\n- Updated dependencies [[`688268b6a`](https://github.com/bluesky-social/atproto/commit/688268b6a5ee30f0922ee152ffbd26583d164ae4), [`8d99915ce`](https://github.com/bluesky-social/atproto/commit/8d99915ce02c73b9b37bf121ccd2703fa14a906a)]:\n  - @atproto/api@0.15.1\n\n## 0.1.103\n\n### Patch Changes\n\n- Updated dependencies [[`23462184d`](https://github.com/bluesky-social/atproto/commit/23462184dc941ba2fc3b4d054985a53715585020)]:\n  - @atproto/api@0.15.0\n\n## 0.1.102\n\n### Patch Changes\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove reference to missing \"bin\" executable\n\n- [#3735](https://github.com/bluesky-social/atproto/pull/3735) [`0759f0fee`](https://github.com/bluesky-social/atproto/commit/0759f0feeded73cfc15d4eb4231bd74354076ea4) Thanks [@devinivy](https://github.com/devinivy)! - Added well-known metadata endpoint to ozone.\n\n## 0.1.101\n\n### Patch Changes\n\n- Updated dependencies [[`fc61662d7`](https://github.com/bluesky-social/atproto/commit/fc61662d7b88597f78383e37ee54264a8bb4b670), [`ca07871c4`](https://github.com/bluesky-social/atproto/commit/ca07871c487abc99fe7b7f8671aa8d98eb5dc4bb)]:\n  - @atproto/api@0.14.22\n\n## 0.1.100\n\n### Patch Changes\n\n- Updated dependencies [[`8b7bf7e8f`](https://github.com/bluesky-social/atproto/commit/8b7bf7e8f0e5447c68633a87a2a3cff99f9e7e1c)]:\n  - @atproto/api@0.14.21\n\n## 0.1.99\n\n### Patch Changes\n\n- Updated dependencies [[`0e681d303`](https://github.com/bluesky-social/atproto/commit/0e681d3036fd0b35c6d2198638392051b2ce4c81)]:\n  - @atproto/api@0.14.20\n\n## 0.1.98\n\n### Patch Changes\n\n- Updated dependencies [[`efb302db1`](https://github.com/bluesky-social/atproto/commit/efb302db1a615b68795c725a22489dbd0400e011), [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/api@0.14.19\n  - @atproto/common@0.4.10\n  - @atproto/identity@0.4.7\n  - @atproto/lexicon@0.4.10\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.7.15\n  - @atproto/xrpc@0.6.12\n\n## 0.1.97\n\n### Patch Changes\n\n- Updated dependencies [[`04b6230cd`](https://github.com/bluesky-social/atproto/commit/04b6230cd2fbfe4a06cb00ab8ccb8e6c87c6c546)]:\n  - @atproto/api@0.14.18\n\n## 0.1.96\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`2b7efb6cb`](https://github.com/bluesky-social/atproto/commit/2b7efb6cb1c93a108570efdafe9d9ec3f1018dfa), [`b0a0f1484`](https://github.com/bluesky-social/atproto/commit/b0a0f1484378adeb5e2aa20b9b6ff2c2eca0f740), [`0eea698be`](https://github.com/bluesky-social/atproto/commit/0eea698bef76520ae4cc0e1f2efbb588a0459556), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/api@0.14.17\n  - @atproto/common@0.4.9\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.7.14\n\n## 0.1.95\n\n### Patch Changes\n\n- [#3695](https://github.com/bluesky-social/atproto/pull/3695) [`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Fix last reaction lexicon\n\n- Updated dependencies [[`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c)]:\n  - @atproto/api@0.14.16\n\n## 0.1.94\n\n### Patch Changes\n\n- Updated dependencies [[`b4ab5011b`](https://github.com/bluesky-social/atproto/commit/b4ab5011bcc64f9f05122a8773806af8e0c13146)]:\n  - @atproto/api@0.14.15\n\n## 0.1.93\n\n### Patch Changes\n\n- [#3685](https://github.com/bluesky-social/atproto/pull/3685) [`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update chat reaction lexicon\n\n- Updated dependencies [[`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5)]:\n  - @atproto/api@0.14.14\n\n## 0.1.92\n\n### Patch Changes\n\n- [#3651](https://github.com/bluesky-social/atproto/pull/3651) [`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd) Thanks [@foysalit](https://github.com/foysalit)! - Add getSubjects endpoint to ozone for fetching detailed view of multiple subjects\n\n- Updated dependencies [[`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd)]:\n  - @atproto/api@0.14.13\n\n## 0.1.91\n\n### Patch Changes\n\n- [#3674](https://github.com/bluesky-social/atproto/pull/3674) [`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0) Thanks [@mozzius](https://github.com/mozzius)! - run codegen for changes in chat lexicon\n\n- Updated dependencies [[`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0)]:\n  - @atproto/api@0.14.12\n\n## 0.1.90\n\n### Patch Changes\n\n- Updated dependencies [[`d87ffc7bf`](https://github.com/bluesky-social/atproto/commit/d87ffc7bfe3c1e792dc84a320544eb2e053d61ce)]:\n  - @atproto/api@0.14.11\n\n## 0.1.89\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n  - @atproto/api@0.14.10\n  - @atproto/lexicon@0.4.9\n  - @atproto/xrpc@0.6.11\n  - @atproto/xrpc-server@0.7.13\n\n## 0.1.88\n\n### Patch Changes\n\n- [#3587](https://github.com/bluesky-social/atproto/pull/3587) [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952) Thanks [@foysalit](https://github.com/foysalit)! - Add searchable handle and displayName to ozone team members\n\n- Updated dependencies [[`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952)]:\n  - @atproto/api@0.14.9\n\n## 0.1.87\n\n### Patch Changes\n\n- Updated dependencies [[`38320191e`](https://github.com/bluesky-social/atproto/commit/38320191e559f8b928c6e951a9b4a6207240bfc1), [`6bcbb6d8c`](https://github.com/bluesky-social/atproto/commit/6bcbb6d8cd3696280935ff7892d8e191fd21fa49), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`dc6e4ecb0`](https://github.com/bluesky-social/atproto/commit/dc6e4ecb0e09bbf4bc7a79c6ac43fb6da4166200)]:\n  - @atproto/api@0.14.8\n  - @atproto/syntax@0.3.4\n  - @atproto/lexicon@0.4.8\n  - @atproto/xrpc@0.6.10\n  - @atproto/xrpc-server@0.7.12\n\n## 0.1.86\n\n### Patch Changes\n\n- Updated dependencies [[`99e2809ca`](https://github.com/bluesky-social/atproto/commit/99e2809ca2ebf70acaa10254f140a8dd0fad4305), [`27b0a7be1`](https://github.com/bluesky-social/atproto/commit/27b0a7be1ed1b6e098114791d84ec9dc844db552), [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492)]:\n  - @atproto/api@0.14.7\n\n## 0.1.85\n\n### Patch Changes\n\n- Updated dependencies [[`44f81f2eb`](https://github.com/bluesky-social/atproto/commit/44f81f2eb9229e21aec4472b3a05e855396dbec5)]:\n  - @atproto/api@0.14.6\n\n## 0.1.84\n\n### Patch Changes\n\n- [#3572](https://github.com/bluesky-social/atproto/pull/3572) [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a) Thanks [@foysalit](https://github.com/foysalit)! - Make comment property optional on ozone comment event\n\n- Updated dependencies [[`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f), [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a), [`6e382f67a`](https://github.com/bluesky-social/atproto/commit/6e382f67aa73532efadfea80ff96a27b526cb178)]:\n  - @atproto/api@0.14.5\n\n## 0.1.83\n\n### Patch Changes\n\n- [#3561](https://github.com/bluesky-social/atproto/pull/3561) [`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c) Thanks [@foysalit](https://github.com/foysalit)! - Add Divert event to the list of allowed ozone events\n\n- Updated dependencies [[`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c)]:\n  - @atproto/api@0.14.4\n\n## 0.1.82\n\n### Patch Changes\n\n- Updated dependencies [[`22af31a89`](https://github.com/bluesky-social/atproto/commit/22af31a898476c5e317aea263af366bddda120d6), [`01874c4be`](https://github.com/bluesky-social/atproto/commit/01874c4be73a41ffb8fe28378f674949aa2c938f)]:\n  - @atproto/api@0.14.3\n\n## 0.1.81\n\n### Patch Changes\n\n- [#3554](https://github.com/bluesky-social/atproto/pull/3554) [`7449f8607`](https://github.com/bluesky-social/atproto/commit/7449f8607c1be948a0b55611c21075757c3d7261) Thanks [@foysalit](https://github.com/foysalit)! - Allow filtering Ozone members based on role and status\n\n## 0.1.80\n\n### Patch Changes\n\n- [#3546](https://github.com/bluesky-social/atproto/pull/3546) [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9) Thanks [@foysalit](https://github.com/foysalit)! - Add reporter stats endpoint on ozone service\n\n- Updated dependencies [[`010f10c6f`](https://github.com/bluesky-social/atproto/commit/010f10c6f212f699ad42c0349a58bbcf2172e3cc), [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9)]:\n  - @atproto/api@0.14.2\n\n## 0.1.79\n\n### Patch Changes\n\n- [#3520](https://github.com/bluesky-social/atproto/pull/3520) [`20e57bacf`](https://github.com/bluesky-social/atproto/commit/20e57bacf9bb2ae8a118eadbfc291f3213b8dc2f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve indexing for \"chat.bsky.moderation.getMessageContext\"\n\n- Updated dependencies [[`ba5bb6e66`](https://github.com/bluesky-social/atproto/commit/ba5bb6e667fb58bbefd332844957de575e102ca3)]:\n  - @atproto/api@0.14.1\n\n## 0.1.78\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update Lexicon derived code to better reflect data typings. In particular, Lexicon derived interfaces will now explicitly include the `$type` property that can be present in the data.\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/api@0.14.0\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n  - @atproto/xrpc@0.6.9\n  - @atproto/xrpc-server@0.7.11\n\n## 0.1.77\n\n### Patch Changes\n\n- [#3495](https://github.com/bluesky-social/atproto/pull/3495) [`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153) Thanks [@foysalit](https://github.com/foysalit)! - Add a priority score to ozone subjects\n\n- Updated dependencies [[`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153)]:\n  - @atproto/api@0.13.35\n\n## 0.1.76\n\n### Patch Changes\n\n- Updated dependencies [[`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486), [`636951e47`](https://github.com/bluesky-social/atproto/commit/636951e4728cd52c2e5355eb93b47d7e869b67e9)]:\n  - @atproto/api@0.13.34\n\n## 0.1.75\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`87ed907a6`](https://github.com/bluesky-social/atproto/commit/87ed907a6b96b408c02c9af819cec8380a453254)]:\n  - @atproto/xrpc-server@0.7.10\n  - @atproto/identity@0.4.6\n  - @atproto/lexicon@0.4.6\n  - @atproto/common@0.4.8\n  - @atproto/crypto@0.4.4\n  - @atproto/syntax@0.3.2\n  - @atproto/xrpc@0.6.8\n  - @atproto/api@0.13.33\n\n## 0.1.74\n\n### Patch Changes\n\n- [#3437](https://github.com/bluesky-social/atproto/pull/3437) [`da7a831a7`](https://github.com/bluesky-social/atproto/commit/da7a831a7318343ba1ee98de3811ba337c043dbd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Code cleanup\n\n- [#3352](https://github.com/bluesky-social/atproto/pull/3352) [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905) Thanks [@foysalit](https://github.com/foysalit)! - Auto resolve appeals when taking down\n\n- [#3463](https://github.com/bluesky-social/atproto/pull/3463) [`8810885b8`](https://github.com/bluesky-social/atproto/commit/8810885b8e7fa0377e6c000c091eec1dd85ed261) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix typo in recordsStats's $type property\n\n- Updated dependencies [[`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9), [`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/xrpc-server@0.7.9\n  - @atproto/api@0.13.32\n  - @atproto/xrpc@0.6.7\n  - @atproto/common@0.4.7\n  - @atproto/crypto@0.4.3\n\n## 0.1.73\n\n### Patch Changes\n\n- Updated dependencies [[`8c6c7813a`](https://github.com/bluesky-social/atproto/commit/8c6c7813a9c2110c8fe21acdca8f09554a1983ce)]:\n  - @atproto/api@0.13.31\n\n## 0.1.72\n\n### Patch Changes\n\n- [#3426](https://github.com/bluesky-social/atproto/pull/3426) [`1ada2d093`](https://github.com/bluesky-social/atproto/commit/1ada2d093427e45b6d59a16cf146bf5282560c7b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix getSubjectStatuses query\n\n- Updated dependencies [[`e6e6aea38`](https://github.com/bluesky-social/atproto/commit/e6e6aea3814e3d0bb42a537f80d77947e85fa73f), [`c0a75d310`](https://github.com/bluesky-social/atproto/commit/c0a75d310aa92c067799a97d1acc5bd0543114c5)]:\n  - @atproto/api@0.13.30\n\n## 0.1.71\n\n### Patch Changes\n\n- Updated dependencies [[`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12)]:\n  - @atproto/xrpc-server@0.7.8\n\n## 0.1.70\n\n### Patch Changes\n\n- [#3416](https://github.com/bluesky-social/atproto/pull/3416) [`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add account and record level statistics when querring `tools.ozone.moderation.queryStatuses`.\n\n- Updated dependencies [[`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6)]:\n  - @atproto/api@0.13.29\n\n## 0.1.69\n\n### Patch Changes\n\n- Updated dependencies [[`cbf17066f`](https://github.com/bluesky-social/atproto/commit/cbf17066f314fbc7f2e943127ee4a9f589f8bec2)]:\n  - @atproto/api@0.13.28\n\n## 0.1.68\n\n### Patch Changes\n\n- Updated dependencies [[`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61)]:\n  - @atproto/xrpc-server@0.7.7\n\n## 0.1.67\n\n### Patch Changes\n\n- [#3344](https://github.com/bluesky-social/atproto/pull/3344) [`48a0e9d60`](https://github.com/bluesky-social/atproto/commit/48a0e9d6060c2dc93899f13f2fc7cc76c04fbcd9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly dispose of unused http responses\n\n- Updated dependencies [[`e277158f7`](https://github.com/bluesky-social/atproto/commit/e277158f70a831b04fde3ec84b3c1eaa6ce82e9d)]:\n  - @atproto/api@0.13.27\n\n## 0.1.66\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n  - @atproto/identity@0.4.5\n  - @atproto/xrpc-server@0.7.6\n\n## 0.1.65\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on Axios\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/identity@0.4.4\n  - @atproto/api@0.13.26\n  - @atproto/common@0.4.6\n  - @atproto/lexicon@0.4.5\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.5\n  - @atproto/xrpc@0.6.6\n\n## 0.1.64\n\n### Patch Changes\n\n- [#3271](https://github.com/bluesky-social/atproto/pull/3271) [`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a) Thanks [@foysalit](https://github.com/foysalit)! - Allow setting policy names with takedown actions and when querying events\n\n- Updated dependencies [[`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a)]:\n  - @atproto/api@0.13.25\n\n## 0.1.63\n\n### Patch Changes\n\n- [#3294](https://github.com/bluesky-social/atproto/pull/3294) [`d90d999de`](https://github.com/bluesky-social/atproto/commit/d90d999defda01a9b04dbce129e254990062c283) Thanks [@foysalit](https://github.com/foysalit)! - Limit tags filter to 25 max and remove 25 char limit for tag item\n\n- [#3310](https://github.com/bluesky-social/atproto/pull/3310) [`6d1ad3783`](https://github.com/bluesky-social/atproto/commit/6d1ad37836f275e03bc115e944a3195b82f3398d) Thanks [@foysalit](https://github.com/foysalit)! - Remove ASCII landing page for ozone in favor of client app\n\n- Updated dependencies [[`d90d999de`](https://github.com/bluesky-social/atproto/commit/d90d999defda01a9b04dbce129e254990062c283)]:\n  - @atproto/api@0.13.24\n\n## 0.1.62\n\n### Patch Changes\n\n- [#3280](https://github.com/bluesky-social/atproto/pull/3280) [`9ea2cce9a`](https://github.com/bluesky-social/atproto/commit/9ea2cce9a4c0a08994a8cb5abc81dc4bc2221d0c) Thanks [@foysalit](https://github.com/foysalit)! - Apply ozone queue splitting at the database query level\n\n- Updated dependencies [[`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7), [`9ea2cce9a`](https://github.com/bluesky-social/atproto/commit/9ea2cce9a4c0a08994a8cb5abc81dc4bc2221d0c)]:\n  - @atproto/api@0.13.23\n\n## 0.1.61\n\n### Patch Changes\n\n- Updated dependencies [[`f22383cee`](https://github.com/bluesky-social/atproto/commit/f22383cee8feb8b9f761c801ab6e07ad8dc019ed)]:\n  - @atproto/api@0.13.22\n\n## 0.1.60\n\n### Patch Changes\n\n- Updated dependencies [[`dced566de`](https://github.com/bluesky-social/atproto/commit/dced566de5079ef4208801db476a7e7416f5e5aa)]:\n  - @atproto/api@0.13.21\n\n## 0.1.59\n\n### Patch Changes\n\n- [#3222](https://github.com/bluesky-social/atproto/pull/3222) [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34) Thanks [@gaearon](https://github.com/gaearon)! - Add optional reasons param to listNotifications\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95), [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34)]:\n  - @atproto/common@0.4.5\n  - @atproto/lexicon@0.4.4\n  - @atproto/api@0.13.20\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.4\n  - @atproto/xrpc@0.6.5\n\n## 0.1.58\n\n### Patch Changes\n\n- [#3171](https://github.com/bluesky-social/atproto/pull/3171) [`ed2236220`](https://github.com/bluesky-social/atproto/commit/ed2236220900ab9a6132c525289cfdd959733a42) Thanks [@foysalit](https://github.com/foysalit)! - Allow moderators to optionally acknowledge all open subjects of an account when acknowledging account level reports\n\n- Updated dependencies [[`ed2236220`](https://github.com/bluesky-social/atproto/commit/ed2236220900ab9a6132c525289cfdd959733a42)]:\n  - @atproto/api@0.13.19\n\n## 0.1.57\n\n### Patch Changes\n\n- [#3082](https://github.com/bluesky-social/atproto/pull/3082) [`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc) Thanks [@gaearon](https://github.com/gaearon)! - Add hotness as a thread sorting option\n\n- Updated dependencies [[`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc)]:\n  - @atproto/api@0.13.18\n\n## 0.1.56\n\n### Patch Changes\n\n- Updated dependencies [[`a4b528e5f`](https://github.com/bluesky-social/atproto/commit/a4b528e5f51c8bfca56b293b0059b88d138ec421), [`2e7aa211d`](https://github.com/bluesky-social/atproto/commit/2e7aa211d2cbc629899c7f87f1713b13b932750b)]:\n  - @atproto/api@0.13.17\n\n## 0.1.55\n\n### Patch Changes\n\n- [#2988](https://github.com/bluesky-social/atproto/pull/2988) [`48d08a469`](https://github.com/bluesky-social/atproto/commit/48d08a469f75837e3b7e879d286d12780440b8b8) Thanks [@foysalit](https://github.com/foysalit)! - Make durationInHours optional for mute reporter event\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`48d08a469`](https://github.com/bluesky-social/atproto/commit/48d08a469f75837e3b7e879d286d12780440b8b8), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`561431fe4`](https://github.com/bluesky-social/atproto/commit/561431fe4897e81767dc768e9a31020d09bf86ff)]:\n  - @atproto/syntax@0.3.1\n  - @atproto/api@0.13.16\n  - @atproto/lexicon@0.4.3\n  - @atproto/xrpc@0.6.4\n  - @atproto/xrpc-server@0.7.3\n\n## 0.1.54\n\n### Patch Changes\n\n- [#2661](https://github.com/bluesky-social/atproto/pull/2661) [`d6f33b474`](https://github.com/bluesky-social/atproto/commit/d6f33b4742e0b94722a993efc7d18833d9416bb6) Thanks [@foysalit](https://github.com/foysalit)! - Add mod events and status filter for account and record hosting status\n\n- [#2905](https://github.com/bluesky-social/atproto/pull/2905) [`c4b5e5395`](https://github.com/bluesky-social/atproto/commit/c4b5e53957463c37dd16fdd1b897d4ab02ab8e84) Thanks [@foysalit](https://github.com/foysalit)! - Add user specific and instance-wide settings api for ozone\n\n- Updated dependencies [[`d6f33b474`](https://github.com/bluesky-social/atproto/commit/d6f33b4742e0b94722a993efc7d18833d9416bb6), [`b6eeb81c6`](https://github.com/bluesky-social/atproto/commit/b6eeb81c6d454b5ae91b05a21fc1820274c1b429), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`839202a3d`](https://github.com/bluesky-social/atproto/commit/839202a3d2b01de25de900cec7540019545798c6), [`e680d55ca`](https://github.com/bluesky-social/atproto/commit/e680d55ca2d7f6b213e2a8693eba6be39163ba41), [`c4b5e5395`](https://github.com/bluesky-social/atproto/commit/c4b5e53957463c37dd16fdd1b897d4ab02ab8e84)]:\n  - @atproto/api@0.13.15\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.2\n  - @atproto/identity@0.4.3\n\n## 0.1.53\n\n### Patch Changes\n\n- Updated dependencies [[`209238769`](https://github.com/bluesky-social/atproto/commit/209238769c0bf38bf04f7fa9621eeb176b5c0ed8), [`73f40e63a`](https://github.com/bluesky-social/atproto/commit/73f40e63abe3283efc0a27eef781c00b497caad1)]:\n  - @atproto/api@0.13.14\n\n## 0.1.52\n\n### Patch Changes\n\n- [#2914](https://github.com/bluesky-social/atproto/pull/2914) [`19e36afb2`](https://github.com/bluesky-social/atproto/commit/19e36afb2c13dbc7b1033eb3cab5e7fc6f496fdc) Thanks [@foysalit](https://github.com/foysalit)! - Add collections and subjectType filters to ozone's queryEvents and queryStatuses endpoints\n\n- Updated dependencies [[`19e36afb2`](https://github.com/bluesky-social/atproto/commit/19e36afb2c13dbc7b1033eb3cab5e7fc6f496fdc)]:\n  - @atproto/api@0.13.13\n\n## 0.1.51\n\n### Patch Changes\n\n- [#2636](https://github.com/bluesky-social/atproto/pull/2636) [`22d039a22`](https://github.com/bluesky-social/atproto/commit/22d039a229e3ef08a793e1c98b473b1b8e18ac5e) Thanks [@foysalit](https://github.com/foysalit)! - Sets api to manage lists of strings on ozone, mostly aimed for automod configuration\n\n- Updated dependencies [[`22d039a22`](https://github.com/bluesky-social/atproto/commit/22d039a229e3ef08a793e1c98b473b1b8e18ac5e)]:\n  - @atproto/api@0.13.12\n\n## 0.1.50\n\n### Patch Changes\n\n- [#2862](https://github.com/bluesky-social/atproto/pull/2862) [`08ed0a5a9`](https://github.com/bluesky-social/atproto/commit/08ed0a5a916685b2aaea783706e6d6287a2aa287) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add proper typings\n\n## 0.1.49\n\n### Patch Changes\n\n- Updated dependencies [[`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44)]:\n  - @atproto/api@0.13.11\n\n## 0.1.48\n\n### Patch Changes\n\n- [#2855](https://github.com/bluesky-social/atproto/pull/2855) [`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5) Thanks [@dholms](https://github.com/dholms)! - Add tools.ozone.signature lexicons\n\n- Updated dependencies [[`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5)]:\n  - @atproto/api@0.13.10\n\n## 0.1.47\n\n### Patch Changes\n\n- [#2836](https://github.com/bluesky-social/atproto/pull/2836) [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c) Thanks [@foysalit](https://github.com/foysalit)! - Add getRepos and getRecords endpoints for bulk fetching\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13), [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c)]:\n  - @atproto/common@0.4.4\n  - @atproto/api@0.13.9\n  - @atproto/crypto@0.4.1\n  - @atproto/xrpc-server@0.7.1\n\n## 0.1.46\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/xrpc@0.6.3\n  - @atproto/api@0.13.8\n  - @atproto/xrpc-server@0.7.0\n  - @atproto/lexicon@0.4.2\n  - @atproto/common@0.4.3\n  - @atproto/identity@0.4.2\n  - @atproto/crypto@0.4.1\n\n## 0.1.45\n\n### Patch Changes\n\n- [#2807](https://github.com/bluesky-social/atproto/pull/2807) [`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e) Thanks [@foysalit](https://github.com/foysalit)! - Introduce a acknowledgeAccountSubjects flag on takedown event to ack all subjects from the author that need review\n\n- Updated dependencies [[`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/api@0.13.7\n  - @atproto/common@0.4.2\n  - @atproto/xrpc-server@0.6.4\n  - @atproto/xrpc@0.6.2\n  - @atproto/crypto@0.4.1\n\n## 0.1.44\n\n### Patch Changes\n\n- [#2787](https://github.com/bluesky-social/atproto/pull/2787) [`642c7ae96`](https://github.com/bluesky-social/atproto/commit/642c7ae968b0dd2bfb448aa6eba0c1fd9312d909) Thanks [@foysalit](https://github.com/foysalit)! - Improve query performance on moderation_event table\n\n## 0.1.43\n\n### Patch Changes\n\n- [#2762](https://github.com/bluesky-social/atproto/pull/2762) [`325859b8b`](https://github.com/bluesky-social/atproto/commit/325859b8bff8dcfdd1eb8cabd51bffedb03aad87) Thanks [@foysalit](https://github.com/foysalit)! - Tag moderation subjects with the content type of embeds in the records\n\n- [#2780](https://github.com/bluesky-social/atproto/pull/2780) [`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d) Thanks [@foysalit](https://github.com/foysalit)! - Add language property to communication templates\n\n- Updated dependencies [[`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d)]:\n  - @atproto/api@0.13.6\n\n## 0.1.42\n\n### Patch Changes\n\n- Updated dependencies [[`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14)]:\n  - @atproto/api@0.13.5\n\n## 0.1.41\n\n### Patch Changes\n\n- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f), [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/xrpc-server@0.6.3\n  - @atproto/xrpc@0.6.1\n  - @atproto/api@0.13.4\n  - @atproto/crypto@0.4.1\n  - @atproto/identity@0.4.1\n\n## 0.1.40\n\n### Patch Changes\n\n- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]:\n  - @atproto/api@0.13.3\n\n## 0.1.39\n\n### Patch Changes\n\n- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]:\n  - @atproto/api@0.13.2\n\n## 0.1.38\n\n### Patch Changes\n\n- [#2663](https://github.com/bluesky-social/atproto/pull/2663) [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae) Thanks [@dholms](https://github.com/dholms)! - Set lxm claim on service auth calls from ozone\n\n- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]:\n  - @atproto/xrpc-server@0.6.2\n\n## 0.1.37\n\n### Patch Changes\n\n- Updated dependencies [[`22af354a5`](https://github.com/bluesky-social/atproto/commit/22af354a5db595d7cbc0e65f02601de3565337e1)]:\n  - @atproto/api@0.13.1\n\n## 0.1.36\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/lexicon@0.4.1\n  - @atproto/xrpc@0.6.0\n  - @atproto/api@0.13.0\n  - @atproto/xrpc-server@0.6.1\n\n## 0.1.35\n\n### Patch Changes\n\n- Updated dependencies [[`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c), [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c)]:\n  - @atproto/api@0.12.29\n  - @atproto/xrpc-server@0.6.0\n\n## 0.1.34\n\n### Patch Changes\n\n- [#2676](https://github.com/bluesky-social/atproto/pull/2676) [`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove `app.bsky.feed.detach` record, to be replaced by `app.bsky.feed.postgate` record in a future release.\n\n- Updated dependencies [[`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41)]:\n  - @atproto/api@0.12.28\n\n## 0.1.33\n\n### Patch Changes\n\n- [#2664](https://github.com/bluesky-social/atproto/pull/2664) [`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `app.bsky.feed.detach` record lexicons.\n\n- Updated dependencies [[`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec)]:\n  - @atproto/api@0.12.27\n\n## 0.1.32\n\n### Patch Changes\n\n- [#2276](https://github.com/bluesky-social/atproto/pull/2276) [`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Updates muted words lexicons to include new attributes `id`, `actorTarget`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words.\n\n- Updated dependencies [[`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7)]:\n  - @atproto/api@0.12.26\n\n## 0.1.31\n\n### Patch Changes\n\n- Updated dependencies [[`12dcdb668`](https://github.com/bluesky-social/atproto/commit/12dcdb668c8ec0f8a89689c326ab3e9dbc6d2f3c), [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b)]:\n  - @atproto/api@0.12.25\n\n## 0.1.30\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Obfuscate request headers in logs using utils from @atproto/common\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/common@0.4.1\n  - @atproto/crypto@0.4.0\n  - @atproto/xrpc-server@0.5.3\n\n## 0.1.29\n\n### Patch Changes\n\n- [#2635](https://github.com/bluesky-social/atproto/pull/2635) [`2f40203fb`](https://github.com/bluesky-social/atproto/commit/2f40203fb453934aaf5d353b680d89b8a1febd0f) Thanks [@dholms](https://github.com/dholms)! - Add in-memory did cachee\n\n- Updated dependencies [[`ed5810179`](https://github.com/bluesky-social/atproto/commit/ed5810179006f254f2035fe1f0e3c4798080cfe0), [`0529bec99`](https://github.com/bluesky-social/atproto/commit/0529bec99183439829a3553f45ac7203763144c3)]:\n  - @atproto/api@0.12.24\n\n## 0.1.28\n\n### Patch Changes\n\n- [#2492](https://github.com/bluesky-social/atproto/pull/2492) [`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863) Thanks [@pfrazee](https://github.com/pfrazee)! - Added bsky app state preference and improved protections against race conditions in preferences sdk\n\n- Updated dependencies [[`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863)]:\n  - @atproto/api@0.12.23\n\n## 0.1.27\n\n### Patch Changes\n\n- [#2553](https://github.com/bluesky-social/atproto/pull/2553) [`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02) Thanks [@devinivy](https://github.com/devinivy)! - Support for starter packs (app.bsky.graph.starterpack)\n\n- Updated dependencies [[`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02), [`615a96ddc`](https://github.com/bluesky-social/atproto/commit/615a96ddc2965251cfab060dfc43fc1a51ef4bff)]:\n  - @atproto/api@0.12.22\n  - @atproto/xrpc-server@0.5.2\n\n## 0.1.26\n\n### Patch Changes\n\n- [#2460](https://github.com/bluesky-social/atproto/pull/2460) [`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24) Thanks [@foysalit](https://github.com/foysalit)! - Add DB backed team member management for ozone\n\n- Updated dependencies [[`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24)]:\n  - @atproto/api@0.12.21\n\n## 0.1.25\n\n### Patch Changes\n\n- Updated dependencies [[`ea0f10b5d`](https://github.com/bluesky-social/atproto/commit/ea0f10b5d0d334eb587032c54d5ace9ea811cf26)]:\n  - @atproto/api@0.12.20\n\n## 0.1.24\n\n### Patch Changes\n\n- Updated dependencies [[`7c1973841`](https://github.com/bluesky-social/atproto/commit/7c1973841dab416ae19435d37853aeea1f579d39)]:\n  - @atproto/api@0.12.19\n\n## 0.1.23\n\n### Patch Changes\n\n- Updated dependencies [[`58abcbd8b`](https://github.com/bluesky-social/atproto/commit/58abcbd8b6e42a1f66bda6acc3ee6a2c0894e546)]:\n  - @atproto/api@0.12.18\n\n## 0.1.22\n\n### Patch Changes\n\n- [#2426](https://github.com/bluesky-social/atproto/pull/2426) [`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.admin.searchAccounts lexicon to allow searching for accounts using email address\n\n- Updated dependencies [[`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd)]:\n  - @atproto/api@0.12.17\n\n## 0.1.21\n\n### Patch Changes\n\n- Updated dependencies [[`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46)]:\n  - @atproto/api@0.12.16\n\n## 0.1.20\n\n### Patch Changes\n\n- Updated dependencies [[`255d5ea1f`](https://github.com/bluesky-social/atproto/commit/255d5ea1f06726547cdbe59c83bd18f2d4746912)]:\n  - @atproto/api@0.12.15\n\n## 0.1.19\n\n### Patch Changes\n\n- Updated dependencies [[`c4af6a409`](https://github.com/bluesky-social/atproto/commit/c4af6a409ea2171c3cf1d0e7c8ed496794a3f049)]:\n  - @atproto/api@0.12.14\n\n## 0.1.18\n\n### Patch Changes\n\n- [#2522](https://github.com/bluesky-social/atproto/pull/2522) [`53551be6c`](https://github.com/bluesky-social/atproto/commit/53551be6cf092a9b4d2e132788b94ac0d4ffcecc) Thanks [@devinivy](https://github.com/devinivy)! - Set max-age CORS header to max practical value\n\n## 0.1.17\n\n### Patch Changes\n\n- Updated dependencies [[`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9)]:\n  - @atproto/api@0.12.13\n\n## 0.1.16\n\n### Patch Changes\n\n- [#2442](https://github.com/bluesky-social/atproto/pull/2442) [`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.label.queryLabels endpoint on appview and allow viewing external labels through ozone\n\n- Updated dependencies [[`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4)]:\n  - @atproto/api@0.12.12\n\n## 0.1.15\n\n### Patch Changes\n\n- Updated dependencies [[`06d2328ee`](https://github.com/bluesky-social/atproto/commit/06d2328eeb8d706018dbdf7cc7b9862dd65b96cb)]:\n  - @atproto/api@0.12.11\n\n## 0.1.14\n\n### Patch Changes\n\n- Updated dependencies [[`d32f7215f`](https://github.com/bluesky-social/atproto/commit/d32f7215f69bc87f50890d9cfdb09840c2fbaa41)]:\n  - @atproto/api@0.12.10\n\n## 0.1.13\n\n### Patch Changes\n\n- Updated dependencies [[`f83b4c8ca`](https://github.com/bluesky-social/atproto/commit/f83b4c8cad01cebc1b67caa6c7ebe45f07b2f318)]:\n  - @atproto/api@0.12.9\n\n## 0.1.12\n\n### Patch Changes\n\n- Updated dependencies [[`58f719cc1`](https://github.com/bluesky-social/atproto/commit/58f719cc1c8d0ebd5ad7cf11221372b671cd7857)]:\n  - @atproto/api@0.12.8\n\n## 0.1.11\n\n### Patch Changes\n\n- [#2390](https://github.com/bluesky-social/atproto/pull/2390) [`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933) Thanks [@foysalit](https://github.com/foysalit)! - Allow muting reports from accounts via `#modEventMuteReporter` event\n\n- Updated dependencies [[`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933)]:\n  - @atproto/api@0.12.7\n\n## 0.1.10\n\n### Patch Changes\n\n- Updated dependencies [[`b9b7c5821`](https://github.com/bluesky-social/atproto/commit/b9b7c582199d57d2fe0af8af5c8c411ed34f5b9d)]:\n  - @atproto/api@0.12.6\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [[`3424a1770`](https://github.com/bluesky-social/atproto/commit/3424a17703891f5678ec76ef97e696afb3288b22)]:\n  - @atproto/api@0.12.5\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`93a4a4df9`](https://github.com/bluesky-social/atproto/commit/93a4a4df9ce38f89a5d05e300d247b85fb007e05)]:\n  - @atproto/api@0.12.4\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [[`0edef0ec0`](https://github.com/bluesky-social/atproto/commit/0edef0ec01403fd6097a4d2875b68313f2f1261f), [`c6d758b8b`](https://github.com/bluesky-social/atproto/commit/c6d758b8b63f4ef50b2ab9afc62164e92a53e7f0)]:\n  - @atproto/api@0.12.3\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`cd4fcc709`](https://github.com/bluesky-social/atproto/commit/cd4fcc709fe8d725a4af769ce21f53711fe5622a)]:\n  - @atproto/xrpc-server@0.5.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`abc6f82da`](https://github.com/bluesky-social/atproto/commit/abc6f82da38abef2b1bbe8d9e41a0534a5418c9e)]:\n  - @atproto/api@0.12.2\n\n## 0.1.4\n\n### Patch Changes\n\n- [#2342](https://github.com/bluesky-social/atproto/pull/2342) [`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds the `associated` property to `profile` and `profile-basic` views, bringing them in line with `profile-detailed` views.\n\n- Updated dependencies [[`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5)]:\n  - @atproto/api@0.12.1\n\n## 0.1.3\n\n### Patch Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9), [`36f2e966c`](https://github.com/bluesky-social/atproto/commit/36f2e966cba6cc90ba4320520da5c7381cfb8086)]:\n  - @atproto/xrpc-server@0.5.0\n  - @atproto/identity@0.4.0\n  - @atproto/lexicon@0.4.0\n  - @atproto/common@0.4.0\n  - @atproto/crypto@0.4.0\n  - @atproto/syntax@0.3.0\n  - @atproto/xrpc@0.5.0\n  - @atproto/api@0.12.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`7dd9941b7`](https://github.com/bluesky-social/atproto/commit/7dd9941b73dbbd82601740e021cc87d765af60ca)]:\n  - @atproto/api@0.11.2\n\n## 0.1.1\n\n### Patch Changes\n\n- [`b95c3955d`](https://github.com/bluesky-social/atproto/commit/b95c3955d0b8263a44a3d2a46b35b1831d9e504a) Thanks [@devinivy](https://github.com/devinivy)! - Second pass on no false negs in labels\n\n- Updated dependencies []:\n  - @atproto/api@0.11.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [`971d3e4c2`](https://github.com/bluesky-social/atproto/commit/971d3e4c26ecfda746e83d458391715752ea7064) Thanks [@devinivy](https://github.com/devinivy)! - Resign labels on overwrite, omit neg:false on labels\n\n### Patch Changes\n\n- Updated dependencies [[`219480764`](https://github.com/bluesky-social/atproto/commit/2194807644cbdb0021e867437693300c1b0e55f5)]:\n  - @atproto/api@0.11.1\n\n## 0.0.17\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0), [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/identity@0.3.3\n  - @atproto/api@0.11.0\n  - @atproto/common@0.3.4\n  - @atproto/lexicon@0.3.3\n  - @atproto/syntax@0.2.1\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.4\n\n## 0.0.16\n\n### Patch Changes\n\n- [#2279](https://github.com/bluesky-social/atproto/pull/2279) [`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655) Thanks [@gaearon](https://github.com/gaearon)! - Change Following feed prefs to only show replies from people you follow by default\n\n- Updated dependencies [[`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655)]:\n  - @atproto/api@0.10.5\n\n## 0.0.15\n\n### Patch Changes\n\n- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]:\n  - @atproto/api@0.10.4\n\n## 0.0.14\n\n### Patch Changes\n\n- Updated dependencies [[`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed)]:\n  - @atproto/api@0.10.3\n\n## 0.0.13\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410), [`43531905c`](https://github.com/bluesky-social/atproto/commit/43531905ce1aec6d36d9be5943782811ecca6e6d), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/api@0.10.2\n  - @atproto/lexicon@0.3.2\n  - @atproto/xrpc-server@0.4.3\n\n## 0.0.12\n\n### Patch Changes\n\n- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]:\n  - @atproto/api@0.10.1\n\n## 0.0.11\n\n### Patch Changes\n\n- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]:\n  - @atproto/api@0.10.0\n\n## 0.0.10\n\n### Patch Changes\n\n- [#2192](https://github.com/bluesky-social/atproto/pull/2192) [`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e) Thanks [@foysalit](https://github.com/foysalit)! - Tag event on moderation subjects and allow filtering events and subjects by tags\n\n- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]:\n  - @atproto/api@0.9.8\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [[`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]:\n  - @atproto/api@0.9.7\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]:\n  - @atproto/api@0.9.6\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [[`8994d363`](https://github.com/bluesky-social/atproto/commit/8994d3633adad1c02569d6d44ae896e18195e8e2)]:\n  - @atproto/api@0.9.5\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [[`4171c04a`](https://github.com/bluesky-social/atproto/commit/4171c04ad81c5734a4558bc41fa1c4f3a1aba18c)]:\n  - @atproto/api@0.9.4\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [[`5368245a`](https://github.com/bluesky-social/atproto/commit/5368245a6ef7095c86ad166fb04ff9bef27c3c3e)]:\n  - @atproto/api@0.9.3\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [[`15f38560`](https://github.com/bluesky-social/atproto/commit/15f38560b9e2dc3af8cf860826e7477234fe6a2d)]:\n  - @atproto/api@0.9.2\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`c6fc73ae`](https://github.com/bluesky-social/atproto/commit/c6fc73aee6c245d12f876abd11889b8dbd0ce2ed)]:\n  - @atproto/api@0.9.1\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [[`e43396af`](https://github.com/bluesky-social/atproto/commit/e43396af0973748dd2d034e88d35cf7ae8b4df2c), [`bf8d718c`](https://github.com/bluesky-social/atproto/commit/bf8d718cf918ac8d8a2cb1f57fde80535284642d), [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6)]:\n  - @atproto/api@0.9.0\n"
  },
  {
    "path": "packages/ozone/README.md",
    "content": "# @atproto/ozone: Bluesky Moderation Service\n\nBackend service for moderating the Bluesky network.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/ozone)](https://www.npmjs.com/package/@atproto/ozone)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/ozone/bin/migration-create.ts",
    "content": "#!/usr/bin/env ts-node\n\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\n\nexport async function main() {\n  const now = new Date()\n  const prefix = now.toISOString().replace(/[^a-z0-9]/gi, '') // Order of migrations matches alphabetical order of their names\n  const name = process.argv[2]\n  if (!name || !name.match(/^[a-z0-9-]+$/)) {\n    process.exitCode = 1\n    return console.error(\n      'Must pass a migration name consisting of lowercase digits, numbers, and dashes.',\n    )\n  }\n  const filename = `${prefix}-${name}`\n  const dir = path.join(__dirname, '..', 'src', 'db', 'migrations')\n\n  await fs.writeFile(path.join(dir, `${filename}.ts`), template, { flag: 'wx' })\n  await fs.writeFile(\n    path.join(dir, 'index.ts'),\n    `export * as _${prefix} from './${filename}'\\n`,\n    { flag: 'a' },\n  )\n}\n\nconst template = `import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Migration code\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  // Migration code\n}\n`\n\nmain()\n"
  },
  {
    "path": "packages/ozone/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Ozone',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  transformIgnorePatterns: [\n    `/node_modules/.pnpm/(?!(get-port|lande|toygrad)@)`,\n  ],\n  testTimeout: 60000,\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/ozone/package.json",
    "content": "{\n  \"name\": \"@atproto/ozone\",\n  \"version\": \"0.1.167\",\n  \"license\": \"MIT\",\n  \"description\": \"Backend service for moderating the Bluesky network.\",\n  \"keywords\": [\n    \"atproto\",\n    \"bluesky\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/ozone\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"codegen\": \"lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"start\": \"node --enable-source-maps dist/bin.js\",\n    \"test\": \"../dev-infra/with-test-redis-and-db.sh jest\",\n    \"test:log\": \"tail -50 test.log | pino-pretty\",\n    \"test:updateSnapshot\": \"jest --updateSnapshot\",\n    \"migration:create\": \"ts-node ./bin/migration-create.ts\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"dependencies\": {\n    \"@atproto/api\": \"workspace:^\",\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/identity\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/ws-client\": \"workspace:^\",\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"@atproto/xrpc-server\": \"workspace:^\",\n    \"@did-plc/lib\": \"^0.0.1\",\n    \"compression\": \"^1.7.4\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.17.2\",\n    \"http-terminator\": \"^3.2.0\",\n    \"kysely\": \"^0.22.0\",\n    \"lande\": \"^1.0.10\",\n    \"multiformats\": \"^9.9.0\",\n    \"p-queue\": \"^6.6.2\",\n    \"pg\": \"^8.10.0\",\n    \"pino-http\": \"^8.2.1\",\n    \"structured-headers\": \"^1.0.1\",\n    \"typed-emitter\": \"^2.1.0\",\n    \"uint8arrays\": \"3.0.0\",\n    \"undici\": \"^6.14.1\",\n    \"ws\": \"^8.12.0\"\n  },\n  \"devDependencies\": {\n    \"@atproto/lex-cli\": \"workspace:^\",\n    \"@atproto/pds\": \"workspace:^\",\n    \"@did-plc/server\": \"^0.0.1\",\n    \"@types/cors\": \"^2.8.12\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-serve-static-core\": \"^4.17.36\",\n    \"@types/pg\": \"^8.6.6\",\n    \"@types/qs\": \"^6.9.7\",\n    \"jest\": \"^28.1.2\",\n    \"ts-node\": \"^10.8.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/api/chat/getActorMetadata.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { ids } from '../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.chat.bsky.moderation.getActorMetadata({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      if (!ctx.chatAgent) {\n        throw new InvalidRequestError('No chat agent configured')\n      }\n      const res = await ctx.chatAgent.api.chat.bsky.moderation.getActorMetadata(\n        params,\n        await ctx.chatAuth(ids.ChatBskyModerationGetActorMetadata),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/chat/getMessageContext.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { ids } from '../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.chat.bsky.moderation.getMessageContext({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth }) => {\n      if (!ctx.chatAgent) {\n        throw new InvalidRequestError('No chat agent configured')\n      }\n      const maxWindowSize = auth.credentials.isModerator ? 5 : 0\n      const before = Math.min(maxWindowSize, params.before)\n      const after = Math.min(maxWindowSize, params.after)\n\n      // Ensure that the requested message was actually reported to prevent arbitrary lookups\n      const found = await ctx.db.db\n        .selectFrom('moderation_event')\n        .select('id')\n        .where('subjectMessageId', '=', params.messageId)\n        // uses \"moderation_event_message_id_idx\" index\n        .where('subjectMessageId', 'is not', null)\n        .where('action', '=', 'tools.ozone.moderation.defs#modEventReport')\n        .limit(1)\n        .executeTakeFirst()\n      if (!found) {\n        throw new InvalidRequestError('No report for requested message')\n      }\n\n      const res =\n        await ctx.chatAgent.api.chat.bsky.moderation.getMessageContext(\n          { ...params, before, after },\n          await ctx.chatAuth(ids.ChatBskyModerationGetMessageContext),\n        )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/chat/index.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport getActorMetadata from './getActorMetadata'\nimport getMessageContext from './getMessageContext'\n\nexport default function (server: Server, ctx: AppContext) {\n  getActorMetadata(server, ctx)\n  getMessageContext(server, ctx)\n  return server\n}\n"
  },
  {
    "path": "packages/ozone/src/api/communication/createTemplate.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { isDuplicateTemplateNameError } from '../../communication-service/util'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.communication.createTemplate({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { createdBy, lang, ...template } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to create a communication template',\n        )\n      }\n\n      // Once auth starts providing us with the caller's DID, we can get rid of this check\n      if (!createdBy) {\n        throw new InvalidRequestError('createdBy field is required')\n      }\n\n      const communicationTemplate = ctx.communicationTemplateService(db)\n\n      try {\n        const newTemplate = await communicationTemplate.create({\n          ...template,\n          // We are not using ?? here because we want to use null instead of potentially empty string\n          lang: lang || null,\n          disabled: false,\n          lastUpdatedBy: createdBy,\n        })\n\n        return {\n          encoding: 'application/json',\n          body: communicationTemplate.view(newTemplate),\n        }\n      } catch (err) {\n        if (isDuplicateTemplateNameError(err)) {\n          throw new InvalidRequestError(\n            `${template.name} already exists. Please choose a different name.`,\n            'DuplicateTemplateName',\n          )\n        }\n        throw err\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/communication/deleteTemplate.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.communication.deleteTemplate({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { id } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to delete a communication template',\n        )\n      }\n\n      const communicationTemplate = ctx.communicationTemplateService(db)\n      await communicationTemplate.delete(Number(id))\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/communication/listTemplates.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.communication.listTemplates({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a full moderator to view list of communication template',\n        )\n      }\n\n      const communicationTemplate = ctx.communicationTemplateService(db)\n      const list = await communicationTemplate.list()\n\n      return {\n        encoding: 'application/json',\n        body: {\n          communicationTemplates: list.map((item) =>\n            communicationTemplate.view(item),\n          ),\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/communication/updateTemplate.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { isDuplicateTemplateNameError } from '../../communication-service/util'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.communication.updateTemplate({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { id, updatedBy, ...template } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to update a communication template',\n        )\n      }\n\n      // Once auth starts providing us with the caller's DID, we can get rid of this check\n      if (!updatedBy) {\n        throw new InvalidRequestError('updatedBy field is required')\n      }\n\n      if (!Object.keys(template).length) {\n        throw new InvalidRequestError('Missing update data in request body')\n      }\n\n      const communicationTemplate = ctx.communicationTemplateService(db)\n      try {\n        const updatedTemplate = await communicationTemplate.update(Number(id), {\n          ...template,\n          lastUpdatedBy: updatedBy,\n        })\n\n        return {\n          encoding: 'application/json',\n          body: communicationTemplate.view(updatedTemplate),\n        }\n      } catch (err) {\n        if (isDuplicateTemplateNameError(err)) {\n          throw new InvalidRequestError(\n            `${template.name} already exists. Please choose a different name.`,\n            'DuplicateTemplateName',\n          )\n        }\n        throw err\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/health.ts",
    "content": "import { Router } from 'express'\nimport { sql } from 'kysely'\nimport { AppContext } from '../context'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  router.get('/robots.txt', function (req, res) {\n    res.type('text/plain')\n    res.send(\n      '# Hello Friends!\\n\\n# Crawling the public parts of the API is allowed. HTTP 429 (\"backoff\") status codes are used for rate-limiting. Up to a handful concurrent requests should be ok.\\nUser-agent: *\\nAllow: /',\n    )\n  })\n\n  router.get('/xrpc/_health', async function (req, res) {\n    const { version } = ctx.cfg.service\n    try {\n      await sql`select 1`.execute(ctx.db.db)\n    } catch (err) {\n      req.log.error({ err }, 'failed health check')\n      return res.status(503).send({ version, error: 'Service Unavailable' })\n    }\n    res.send({ version })\n  })\n\n  return router\n}\n"
  },
  {
    "path": "packages/ozone/src/api/index.ts",
    "content": "import { AppContext } from '../context'\nimport { Server } from '../lexicon'\nimport chat from './chat'\nimport createTemplate from './communication/createTemplate'\nimport deleteTemplate from './communication/deleteTemplate'\nimport listTemplates from './communication/listTemplates'\nimport updateTemplate from './communication/updateTemplate'\nimport fetchLabels from './label/fetchLabels'\nimport queryLabels from './label/queryLabels'\nimport subscribeLabels from './label/subscribeLabels'\nimport cancelScheduledActions from './moderation/cancelScheduledActions'\nimport emitEvent from './moderation/emitEvent'\nimport getAccountTimeline from './moderation/getAccountTimeline'\nimport getEvent from './moderation/getEvent'\nimport adminGetRecord from './moderation/getRecord'\nimport adminGetRecords from './moderation/getRecords'\nimport getRepo from './moderation/getRepo'\nimport getReporterStats from './moderation/getReporterStats'\nimport getRepos from './moderation/getRepos'\nimport getSubjects from './moderation/getSubjects'\nimport listScheduledActions from './moderation/listScheduledActions'\nimport queryEvents from './moderation/queryEvents'\nimport queryStatuses from './moderation/queryStatuses'\nimport scheduleAction from './moderation/scheduleAction'\nimport searchRepos from './moderation/searchRepos'\nimport proxied from './proxied'\nimport createReport from './report/createReport'\nimport addSafelinkRule from './safelink/addRule'\nimport querySafelinkEvents from './safelink/queryEvents'\nimport querySafelinkRules from './safelink/queryRules'\nimport removeSafelinkRule from './safelink/removeRule'\nimport updateSafelinkRule from './safelink/updateRule'\nimport getConfig from './server/getConfig'\nimport setAddValues from './set/addValues'\nimport deleteSet from './set/deleteSet'\nimport setDeleteValues from './set/deleteValues'\nimport setGetValues from './set/getValues'\nimport querySets from './set/querySets'\nimport upsertSet from './set/upsertSet'\nimport listOptions from './setting/listOptions'\nimport removeOptions from './setting/removeOptions'\nimport upsertOption from './setting/upsertOption'\nimport addMember from './team/addMember'\nimport deleteMember from './team/deleteMember'\nimport listMembers from './team/listMembers'\nimport updateMember from './team/updateMember'\nimport grantVerifications from './verification/grantVerifications'\nimport listVerifications from './verification/listVerifications'\nimport revokeVerifications from './verification/revokeVerifications'\n\nexport * as health from './health'\n\nexport * as wellKnown from './well-known'\n\nexport default function (server: Server, ctx: AppContext) {\n  createReport(server, ctx)\n  emitEvent(server, ctx)\n  searchRepos(server, ctx)\n  adminGetRecord(server, ctx)\n  adminGetRecords(server, ctx)\n  getRepo(server, ctx)\n  getRepos(server, ctx)\n  getEvent(server, ctx)\n  queryEvents(server, ctx)\n  queryStatuses(server, ctx)\n  queryLabels(server, ctx)\n  subscribeLabels(server, ctx)\n  fetchLabels(server, ctx)\n  listTemplates(server, ctx)\n  createTemplate(server, ctx)\n  updateTemplate(server, ctx)\n  deleteTemplate(server, ctx)\n  listMembers(server, ctx)\n  addMember(server, ctx)\n  updateMember(server, ctx)\n  deleteMember(server, ctx)\n  chat(server, ctx)\n  proxied(server, ctx)\n  getConfig(server, ctx)\n  setAddValues(server, ctx)\n  setGetValues(server, ctx)\n  querySets(server, ctx)\n  upsertSet(server, ctx)\n  setDeleteValues(server, ctx)\n  deleteSet(server, ctx)\n  upsertOption(server, ctx)\n  listOptions(server, ctx)\n  removeOptions(server, ctx)\n  getReporterStats(server, ctx)\n  getSubjects(server, ctx)\n  grantVerifications(server, ctx)\n  revokeVerifications(server, ctx)\n  listVerifications(server, ctx)\n  addSafelinkRule(server, ctx)\n  updateSafelinkRule(server, ctx)\n  removeSafelinkRule(server, ctx)\n  querySafelinkEvents(server, ctx)\n  querySafelinkRules(server, ctx)\n  getAccountTimeline(server, ctx)\n  scheduleAction(server, ctx)\n  listScheduledActions(server, ctx)\n  cancelScheduledActions(server, ctx)\n  return server\n}\n"
  },
  {
    "path": "packages/ozone/src/api/label/fetchLabels.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.temp.fetchLabels({\n    auth: ctx.authVerifier.standardOptionalOrAdminToken,\n    handler: async ({ params }) => {\n      const { limit } = params\n      const since =\n        params.since !== undefined ? new Date(params.since).toISOString() : ''\n      const labelRes = await ctx.db.db\n        .selectFrom('label')\n        .selectAll()\n        .orderBy('label.cts', 'asc')\n        .where('cts', '>', since)\n        .limit(limit)\n        .execute()\n\n      const modSrvc = ctx.modService(ctx.db)\n      const labels = await Promise.all(\n        labelRes.map((l) => modSrvc.views.formatLabelAndEnsureSig(l)),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          labels,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/label/queryLabels.ts",
    "content": "import { sql } from 'kysely'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.label.queryLabels(async ({ params }) => {\n    const { uriPatterns, sources, limit, cursor } = params\n    let builder = ctx.db.db.selectFrom('label').selectAll().limit(limit)\n    // if includes '*', then we don't need a where clause\n    if (!uriPatterns.includes('*')) {\n      builder = builder.where((qb) => {\n        // starter where clause that is always false so that we can chain `orWhere`s\n        qb = qb.where(sql`1 = 0`)\n        for (const pattern of uriPatterns) {\n          // if no '*', then we're looking for an exact match\n          if (!pattern.includes('*')) {\n            qb = qb.orWhere('uri', '=', pattern)\n          } else {\n            if (pattern.indexOf('*') < pattern.length - 1) {\n              throw new InvalidRequestError(`invalid pattern: ${pattern}`)\n            }\n            const searchPattern = pattern\n              .slice(0, -1)\n              .replaceAll('%', '') // sanitize search pattern\n              .replaceAll('_', '\\\\_') // escape any underscores\n            qb = qb.orWhere('uri', 'like', `${searchPattern}%`)\n          }\n        }\n        return qb\n      })\n    }\n    if (sources && sources.length > 0) {\n      builder = builder.where('src', 'in', sources)\n    }\n    if (cursor) {\n      const cursorId = parseInt(cursor, 10)\n      if (isNaN(cursorId)) {\n        throw new InvalidRequestError('invalid cursor')\n      }\n      builder = builder.where('id', '>', cursorId)\n    }\n\n    const res = await builder.execute()\n\n    const modSrvc = ctx.modService(ctx.db)\n    const labels = await Promise.all(\n      res.map((l) => modSrvc.views.formatLabelAndEnsureSig(l)),\n    )\n    const resCursor = res.at(-1)?.id.toString(10)\n\n    return {\n      encoding: 'application/json',\n      body: {\n        cursor: resCursor,\n        labels,\n      },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/label/subscribeLabels.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { Outbox } from '../../sequencer/outbox'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.label.subscribeLabels(async function* ({\n    params,\n    signal,\n  }) {\n    const { cursor } = params\n    const outbox = new Outbox(ctx.sequencer)\n\n    if (cursor !== undefined) {\n      const curr = await ctx.sequencer.curr()\n      if (cursor > (curr ?? 0)) {\n        throw new InvalidRequestError('Cursor in the future.', 'FutureCursor')\n      }\n    }\n\n    for await (const evt of outbox.events(cursor, signal)) {\n      yield { $type: '#labels', ...evt }\n    }\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/cancelScheduledActions.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { subjectFromInput } from '../../mod-service/subject'\nimport { ScheduledTakedownTag } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.cancelScheduledActions({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { subjects, comment } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to cancel scheduled actions',\n        )\n      }\n\n      const createdBy =\n        access.type === 'admin_token' ? ctx.cfg.service.did : access.iss\n      const now = new Date()\n\n      const result = await db.transaction(async (tx) => {\n        const scheduledActionService = ctx.scheduledActionService(tx)\n        const modService = ctx.modService(tx)\n\n        const cancellations =\n          await scheduledActionService.cancelScheduledActions(subjects)\n\n        for (const subject of cancellations.succeeded) {\n          await modService.logEvent({\n            event: {\n              $type: 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n              comment,\n            },\n            subject: subjectFromInput({\n              did: subject,\n              $type: 'com.atproto.admin.defs#repoRef',\n            }),\n            createdBy,\n            createdAt: now,\n          })\n          await modService.logEvent({\n            event: {\n              $type: 'tools.ozone.moderation.defs#modEventTag',\n              remove: [ScheduledTakedownTag],\n              add: [],\n            },\n            subject: subjectFromInput({\n              did: subject,\n              $type: 'com.atproto.admin.defs#repoRef',\n            }),\n            createdBy,\n            createdAt: now,\n          })\n        }\n\n        return cancellations\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          succeeded: result.succeeded,\n          failed: result.failed,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/emitEvent.ts",
    "content": "import { isModEventDivert } from '@atproto/api/dist/client/types/tools/ozone/moderation/defs'\nimport {\n  AuthRequiredError,\n  ForbiddenError,\n  InvalidRequestError,\n} from '@atproto/xrpc-server'\nimport { AdminTokenOutput, ModeratorOutput } from '../../auth-verifier'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { ids } from '../../lexicon/lexicons'\nimport {\n  ModEventTag,\n  isAgeAssuranceEvent,\n  isAgeAssuranceOverrideEvent,\n  isAgeAssurancePurgeEvent,\n  isModEventAcknowledge,\n  isModEventEmail,\n  isModEventLabel,\n  isModEventMuteReporter,\n  isModEventReport,\n  isModEventReverseTakedown,\n  isModEventTag,\n  isModEventTakedown,\n  isModEventUnmuteReporter,\n  isRevokeAccountCredentialsEvent,\n} from '../../lexicon/types/tools/ozone/moderation/defs'\nimport { HandlerInput } from '../../lexicon/types/tools/ozone/moderation/emitEvent'\nimport { httpLogger } from '../../logger'\nimport { subjectFromInput } from '../../mod-service/subject'\nimport { SettingService } from '../../setting/service'\nimport { TagService } from '../../tag-service'\nimport { getTagForReport } from '../../tag-service/util'\nimport { retryHttp } from '../../util'\nimport { getEventType } from '../util'\nimport { assertProtectedTagAction, getProtectedTags } from './util'\n\nconst handleModerationEvent = async ({\n  ctx,\n  input,\n  auth,\n}: {\n  ctx: AppContext\n  input: HandlerInput\n  auth: ModeratorOutput | AdminTokenOutput\n}) => {\n  const access = auth.credentials\n  const createdBy =\n    auth.credentials.type === 'moderator'\n      ? auth.credentials.iss\n      : input.body.createdBy\n  const db = ctx.db\n  const moderationService = ctx.modService(db)\n  const settingService = ctx.settingService(db)\n  const { event, externalId } = input.body\n  const isAcknowledgeEvent = isModEventAcknowledge(event)\n  const isTakedownEvent = isModEventTakedown(event)\n  const isReverseTakedownEvent = isModEventReverseTakedown(event)\n  const isLabelEvent = isModEventLabel(event)\n  const subject = subjectFromInput(\n    input.body.subject,\n    input.body.subjectBlobCids,\n  )\n\n  if (isAgeAssuranceEvent(event) && !subject.isRepo()) {\n    throw new InvalidRequestError('Invalid subject type')\n  }\n\n  if (isAgeAssuranceOverrideEvent(event)) {\n    if (!subject.isRepo()) {\n      throw new InvalidRequestError('Invalid subject type')\n    }\n    if (!auth.credentials.isModerator) {\n      throw new AuthRequiredError(\n        'Must be a full moderator to override age assurance',\n      )\n    }\n  }\n\n  if (isAgeAssurancePurgeEvent(event)) {\n    if (!subject.isRepo()) {\n      throw new InvalidRequestError('Invalid subject type')\n    }\n    if (!auth.credentials.isModerator) {\n      throw new ForbiddenError(\n        'Must be a moderator to purge age assurance events',\n      )\n    }\n  }\n\n  if (isRevokeAccountCredentialsEvent(event)) {\n    if (!subject.isRepo()) {\n      throw new InvalidRequestError('Invalid subject type')\n    }\n\n    if (!auth.credentials.isAdmin) {\n      throw new AuthRequiredError(\n        'Must be an admin to revoke account credentials',\n      )\n    }\n\n    if (!ctx.pdsAgent) {\n      throw new InvalidRequestError('PDS not configured')\n    }\n\n    await ctx.pdsAgent.com.atproto.temp.revokeAccountCredentials(\n      { account: subject.did },\n      await ctx.pdsAuth(ids.ComAtprotoTempRevokeAccountCredentials),\n    )\n  }\n\n  // if less than moderator access then can only take ack and escalation actions\n  if (isTakedownEvent || isReverseTakedownEvent) {\n    if (!access.isModerator) {\n      throw new AuthRequiredError(\n        'Must be a full moderator to take this type of action',\n      )\n    }\n\n    // Non admins should not be able to take down feed generators\n    if (\n      !access.isAdmin &&\n      subject.recordPath?.includes('app.bsky.feed.generator/')\n    ) {\n      throw new AuthRequiredError(\n        'Must be a full admin to take this type of action on feed generators',\n      )\n    }\n  }\n  // if less than moderator access then can not apply labels\n  if (!access.isModerator && isLabelEvent) {\n    throw new AuthRequiredError('Must be a full moderator to label content')\n  }\n\n  if (isLabelEvent) {\n    validateLabels([\n      ...(event.createLabelVals ?? []),\n      ...(event.negateLabelVals ?? []),\n    ])\n  }\n\n  const isTakedownOrReverseTakedownEvent =\n    isTakedownEvent || isReverseTakedownEvent\n  if (isTakedownOrReverseTakedownEvent || isLabelEvent) {\n    const status = await moderationService.getStatus(subject)\n\n    if (status?.takendown && isTakedownEvent) {\n      throw new InvalidRequestError(`Subject is already taken down`)\n    }\n\n    if (!status?.takendown && isReverseTakedownEvent) {\n      throw new InvalidRequestError(`Subject is not taken down`)\n    }\n\n    if (status?.tags?.length) {\n      const protectedTags = await getProtectedTags(\n        settingService,\n        ctx.cfg.service.did,\n      )\n\n      if (protectedTags) {\n        assertProtectedTagAction({\n          protectedTags,\n          subjectTags: status.tags,\n          actionAuthor: createdBy,\n          isAdmin: auth.credentials.isAdmin,\n          isModerator: auth.credentials.isModerator,\n          isTriage: auth.credentials.isTriage,\n        })\n      }\n    }\n\n    if (status?.takendown && isReverseTakedownEvent && subject.isRecord()) {\n      // due to the way blob status is modeled, we should reverse takedown on all\n      // blobs for the record being restored, which aren't taken down on another record.\n      subject.blobCids = status.blobCids ?? []\n    }\n  }\n\n  if (isModEventEmail(event) && event.content) {\n    // sending email prior to logging the event to avoid a long transaction below\n    if (!subject.isRepo()) {\n      throw new InvalidRequestError('Email can only be sent to a repo subject')\n    }\n    const { content, subjectLine } = event\n    // on error, don't fail the whole event. instead, log the event data with isDelivered false\n    try {\n      await retryHttp(() =>\n        ctx.modService(db).sendEmail({\n          subject: subjectLine,\n          content,\n          recipientDid: subject.did,\n        }),\n      )\n      event.isDelivered = true\n    } catch (err) {\n      event.isDelivered = false\n      httpLogger.error({ err, event }, 'failed to send mod event email')\n    }\n  }\n\n  if (isModEventDivert(event) && subject.isRecord()) {\n    if (!ctx.blobDiverter) {\n      throw new InvalidRequestError(\n        'BlobDiverter not configured for this service',\n      )\n    }\n    await ctx.blobDiverter.uploadBlobOnService(subject.info())\n  }\n\n  if (\n    (isModEventMuteReporter(event) || isModEventUnmuteReporter(event)) &&\n    !subject.isRepo()\n  ) {\n    throw new InvalidRequestError('Subject must be a repo when muting reporter')\n  }\n\n  if (isModEventTag(event)) {\n    await assertTagAuth(settingService, ctx.cfg.service.did, event, auth)\n  }\n\n  if (isModEventReport(event)) {\n    await ctx.moderationServiceProfile().validateReasonType(event.reportType)\n  }\n\n  const moderationEvent = await db.transaction(async (dbTxn) => {\n    const moderationTxn = ctx.modService(dbTxn)\n\n    if (externalId) {\n      const existingEvent = await moderationTxn.getEventByExternalId(\n        getEventType(event.$type),\n        externalId,\n        subject,\n      )\n\n      if (existingEvent) {\n        throw new InvalidRequestError(\n          `An event with the same external ID already exists for the subject.`,\n          'DuplicateExternalId',\n        )\n      }\n    }\n\n    const result = await moderationTxn.logEvent({\n      event,\n      subject,\n      createdBy,\n      modTool: input.body.modTool,\n      externalId,\n    })\n\n    const tagService = new TagService(\n      subject,\n      result.subjectStatus,\n      ctx.cfg.service.did,\n      moderationTxn,\n    )\n\n    const initialTags = isModEventReport(event)\n      ? [getTagForReport(event.reportType)]\n      : undefined\n    await tagService.evaluateForSubject(initialTags)\n\n    if (subject.isRepo()) {\n      if (isTakedownEvent) {\n        const isSuspend = !!result.event.durationInHours\n        await moderationTxn.takedownRepo(\n          subject,\n          result.event.id,\n          new Set(\n            result.event.meta?.targetServices\n              ? `${result.event.meta.targetServices}`.split(',')\n              : undefined,\n          ),\n          isSuspend,\n        )\n      } else if (isReverseTakedownEvent) {\n        await moderationTxn.reverseTakedownRepo(subject)\n      }\n    }\n\n    if (subject.isRecord()) {\n      if (isTakedownEvent) {\n        await moderationTxn.takedownRecord(\n          subject,\n          result.event.id,\n          new Set(\n            result.event.meta?.targetServices\n              ? `${result.event.meta.targetServices}`.split(',')\n              : undefined,\n          ),\n        )\n      } else if (isReverseTakedownEvent) {\n        await moderationTxn.reverseTakedownRecord(subject)\n      }\n    }\n\n    if (\n      (isTakedownEvent || isAcknowledgeEvent) &&\n      result.event.meta?.acknowledgeAccountSubjects\n    ) {\n      await moderationTxn.resolveSubjectsForAccount(\n        subject.did,\n        createdBy,\n        result.event,\n      )\n    }\n\n    if (isLabelEvent) {\n      await moderationTxn.formatAndCreateLabels(\n        result.event.subjectUri ?? result.event.subjectDid,\n        result.event.subjectCid,\n        {\n          create: result.event.createLabelVals?.length\n            ? result.event.createLabelVals.split(' ')\n            : undefined,\n          negate: result.event.negateLabelVals?.length\n            ? result.event.negateLabelVals.split(' ')\n            : undefined,\n        },\n        result.event.durationInHours ?? undefined,\n      )\n    }\n\n    return result.event\n  })\n\n  return moderationService.views.formatEvent(moderationEvent)\n}\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.emitEvent({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      try {\n        const moderationEvent = await handleModerationEvent({\n          input,\n          auth,\n          ctx,\n        })\n\n        // On divert events, we need to automatically take down the blobs\n        if (isModEventDivert(input.body.event)) {\n          await handleModerationEvent({\n            auth,\n            ctx,\n            input: {\n              ...input,\n              body: {\n                ...input.body,\n                event: {\n                  ...input.body.event,\n                  $type: 'tools.ozone.moderation.defs#modEventTakedown',\n                  comment:\n                    '[DIVERT_SIDE_EFFECT]: Automatically taking down after divert event',\n                },\n                modTool: input.body.modTool,\n              },\n            },\n          })\n        }\n\n        return {\n          encoding: 'application/json',\n          body: moderationEvent,\n        }\n      } catch (err) {\n        httpLogger.error(\n          { err, body: input.body },\n          'failed to emit moderation event',\n        )\n        throw err\n      }\n    },\n  })\n}\n\nconst assertTagAuth = async (\n  settingService: SettingService,\n  serviceDid: string,\n  event: ModEventTag,\n  auth: ModeratorOutput | AdminTokenOutput,\n) => {\n  // admins can add/remove any tag\n  if (auth.credentials.isAdmin) return\n\n  const protectedTags = await getProtectedTags(settingService, serviceDid)\n\n  if (!protectedTags) {\n    return\n  }\n\n  for (const tag of Object.keys(protectedTags)) {\n    if (event.add.includes(tag) || event.remove.includes(tag)) {\n      // if specific moderators are configured to manage this tag but the current user\n      // is not one of them, then throw an error\n      const configuredModerators = protectedTags[tag]?.['moderators']\n      if (\n        configuredModerators &&\n        !configuredModerators.includes(auth.credentials.iss)\n      ) {\n        throw new InvalidRequestError(`Not allowed to manage tag: ${tag}`)\n      }\n\n      const configuredRoles = protectedTags[tag]?.['roles']\n      if (configuredRoles) {\n        // admins can already do everything so we only check for moderator and triage role config\n        if (\n          auth.credentials.isModerator &&\n          !configuredRoles.includes('tools.ozone.team.defs#roleModerator')\n        ) {\n          throw new InvalidRequestError(\n            `Can not manage tag ${tag} with moderator role`,\n          )\n        } else if (\n          auth.credentials.isTriage &&\n          !configuredRoles.includes('tools.ozone.team.defs#roleTriage')\n        ) {\n          throw new InvalidRequestError(\n            `Can not manage tag ${tag} with triage role`,\n          )\n        }\n      }\n    }\n  }\n}\n\nconst validateLabels = (labels: string[]) => {\n  for (const label of labels) {\n    for (const char of badChars) {\n      if (label.includes(char)) {\n        throw new InvalidRequestError(`Invalid label: ${label}`)\n      }\n    }\n  }\n}\n\nconst badChars = [' ', ',', ';', `'`, `\"`]\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getAccountTimeline.ts",
    "content": "import { ToolsOzoneModerationGetAccountTimeline } from '@atproto/api'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { ids } from '../../lexicon/lexicons'\nimport { dateFromDatetime } from '../../mod-service/util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getAccountTimeline({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const { did } = params\n      const db = ctx.db\n      const modService = ctx.modService(db)\n      const [modEventHistory, accountHistory, plcHistory] =\n        await Promise.allSettled([\n          modService.getAccountTimeline(did),\n          getAccountHistory(ctx, did),\n          getPlcHistory(ctx, did),\n        ])\n      const timelineByDay = new Map<\n        string,\n        ToolsOzoneModerationGetAccountTimeline.TimelineItemSummary[]\n      >()\n\n      if (modEventHistory.status === 'fulfilled') {\n        for (const row of modEventHistory.value) {\n          const day = timelineByDay.get(row.day)\n          const summary = {\n            eventSubjectType: row.subjectUri ? 'record' : 'account',\n            eventType: row.action,\n            count: row.count,\n          }\n          if (day) {\n            day.push(summary)\n            timelineByDay.set(row.day, day)\n          } else {\n            timelineByDay.set(row.day, [summary])\n          }\n        }\n      } else {\n        throw modEventHistory.reason\n      }\n\n      if (accountHistory.status === 'fulfilled') {\n        for (const [rowDay, row] of Object.entries(accountHistory.value)) {\n          const day = timelineByDay.get(rowDay)\n          const summaries: ToolsOzoneModerationGetAccountTimeline.TimelineItemSummary[] =\n            []\n          for (const [eventType, count] of Object.entries(row)) {\n            summaries.push({\n              eventSubjectType: 'account',\n              eventType,\n              count,\n            })\n          }\n          if (day) {\n            day.push(...summaries)\n            timelineByDay.set(rowDay, day)\n          } else {\n            timelineByDay.set(rowDay, summaries)\n          }\n        }\n      }\n\n      if (plcHistory.status === 'fulfilled') {\n        for (const [rowDay, row] of Object.entries(plcHistory.value)) {\n          const day = timelineByDay.get(rowDay)\n          const summaries: ToolsOzoneModerationGetAccountTimeline.TimelineItemSummary[] =\n            []\n          for (const [eventType, count] of Object.entries(row)) {\n            summaries.push({\n              eventSubjectType: 'account',\n              eventType,\n              count,\n            })\n          }\n          if (day) {\n            day.push(...summaries)\n            timelineByDay.set(rowDay, day)\n          } else {\n            timelineByDay.set(rowDay, summaries)\n          }\n        }\n      }\n\n      const timeline: ToolsOzoneModerationGetAccountTimeline.TimelineItem[] = []\n\n      for (const [day, summary] of timelineByDay.entries()) {\n        timeline.push({ day, summary: summary.flat() })\n      }\n\n      return {\n        encoding: 'application/json',\n        body: { timeline },\n      }\n    },\n  })\n}\n\nconst getAccountHistory = async (ctx: AppContext, did: string) => {\n  const events: Record<string, Record<string, number>> = {}\n\n  if (!ctx.pdsAgent) {\n    return events\n  }\n\n  const auth = await ctx.pdsAuth(ids.ToolsOzoneHostingGetAccountHistory)\n  let cursor: string | undefined = undefined\n\n  do {\n    const { data } = await ctx.pdsAgent.tools.ozone.hosting.getAccountHistory(\n      { did, cursor },\n      auth,\n    )\n    cursor = data.cursor\n    for (const event of data.events) {\n      // This should never happen and the check is here only because typescript screams at us otherwise\n      if (!event.$type) {\n        continue\n      }\n\n      const day = dateFromDatetime(new Date(event.createdAt))\n      events[day] ??= {}\n      events[day][event.$type] ??= 0\n      events[day][event.$type]++\n    }\n  } while (cursor)\n\n  return events\n}\n\nconst PLC_OPERATION_MAP = {\n  create: 'tools.ozone.moderation.defs#timelineEventPlcCreate',\n  plc_operation: 'tools.ozone.moderation.defs#timelineEventPlcOperation',\n  plc_tombstone: 'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n}\n\nconst getPlcHistory = async (ctx: AppContext, did: string) => {\n  const events: Record<string, Record<string, number>> = {}\n\n  if (!ctx.plcClient) {\n    return events\n  }\n\n  const result = await ctx.plcClient.getAuditableLog(did)\n  for (const event of result) {\n    // Skip events that are not mapped, this means we will have to add correct mapping if/when new event types are introduced here\n    if (!Object.hasOwn(PLC_OPERATION_MAP, event.operation.type)) {\n      continue\n    }\n    const day = dateFromDatetime(new Date(event.createdAt))\n    events[day] ??= {}\n    const eventType =\n      PLC_OPERATION_MAP[event.operation.type] || event.operation.type\n    events[day][eventType] ??= 0\n    events[day][eventType]++\n  }\n\n  return events\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getEvent.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getEvent({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const { id } = params\n      const db = ctx.db\n      const modService = ctx.modService(db)\n      const event = await modService.getEventOrThrow(id)\n      const eventDetail = await modService.views.eventDetail(event)\n      return {\n        encoding: 'application/json',\n        body: eventDetail,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getRecord.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { addAccountInfoToRepoView, getPdsAccountInfos } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getRecord({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth, req }) => {\n      const db = ctx.db\n      const labelers = ctx.reqLabelers(req)\n\n      const [records, accountInfos] = await Promise.all([\n        ctx.modService(db).views.recordDetails([params], labelers),\n        getPdsAccountInfos(ctx, [new AtUri(params.uri).hostname]),\n      ])\n\n      const record = records.get(params.uri)\n\n      if (!record) {\n        throw new InvalidRequestError(\n          `Could not locate record: ${params.uri}`,\n          'RecordNotFound',\n        )\n      }\n\n      record.repo = addAccountInfoToRepoView(\n        record.repo,\n        accountInfos.get(record.repo.did) || null,\n        auth.credentials.isModerator,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: record,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getRecords.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { addAccountInfoToRepoView, getPdsAccountInfos } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getRecords({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth, req }) => {\n      const db = ctx.db\n      const labelers = ctx.reqLabelers(req)\n\n      const [records, accountInfos] = await Promise.all([\n        ctx.modService(db).views.recordDetails(\n          params.uris.map((uri) => ({ uri })),\n          labelers,\n        ),\n        getPdsAccountInfos(\n          ctx,\n          params.uris.map((uri) => new AtUri(uri).hostname),\n        ),\n      ])\n\n      const results = params.uris.map((uri) => {\n        const record = records.get(uri)\n        if (!record) {\n          return {\n            uri,\n            $type: 'tools.ozone.moderation.defs#recordViewNotFound',\n          }\n        }\n\n        return {\n          $type: 'tools.ozone.moderation.defs#recordViewDetail',\n          ...record,\n          repo: addAccountInfoToRepoView(\n            record.repo,\n            accountInfos.get(record.repo.did) || null,\n            auth.credentials.isModerator,\n          ),\n        }\n      })\n\n      return {\n        encoding: 'application/json',\n        body: { records: results },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getRepo.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { addAccountInfoToRepoViewDetail, getPdsAccountInfos } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getRepo({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth, req }) => {\n      const { did } = params\n      const db = ctx.db\n      const labelers = ctx.reqLabelers(req)\n      const [partialRepos, accountInfo] = await Promise.all([\n        ctx.modService(db).views.repoDetails([did], labelers),\n        getPdsAccountInfos(ctx, [did]),\n      ])\n\n      const partialRepo = partialRepos.get(did)\n      if (!partialRepo) {\n        throw new InvalidRequestError('Repo not found', 'RepoNotFound')\n      }\n\n      const repo = addAccountInfoToRepoViewDetail(\n        partialRepo,\n        accountInfo.get(did) || null,\n        auth.credentials.isModerator,\n      )\n      return {\n        encoding: 'application/json',\n        body: repo,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getReporterStats.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getReporterStats({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const db = ctx.db\n\n      const stats = await ctx.modService(db).getReporterStats(params.dids)\n\n      return {\n        encoding: 'application/json',\n        body: { stats },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getRepos.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { addAccountInfoToRepoViewDetail, getPdsAccountInfos } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getRepos({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth, req }) => {\n      const { dids } = params\n      const db = ctx.db\n      const labelers = ctx.reqLabelers(req)\n      const [partialRepos, accountInfo] = await Promise.all([\n        ctx.modService(db).views.repoDetails(dids, labelers),\n        getPdsAccountInfos(ctx, dids),\n      ])\n\n      const repos = dids.map((did) => {\n        const partialRepo = partialRepos.get(did)\n        if (!partialRepo) {\n          return {\n            did,\n            $type: 'tools.ozone.moderation.defs#repoViewNotFound',\n          }\n        }\n        return {\n          ...addAccountInfoToRepoViewDetail(\n            partialRepo,\n            accountInfo.get(did) || null,\n            auth.credentials.isModerator,\n          ),\n          $type: 'tools.ozone.moderation.defs#repoViewDetail',\n        }\n      })\n\n      return {\n        encoding: 'application/json',\n        body: { repos },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/getSubjects.ts",
    "content": "import { ToolsOzoneModerationDefs } from '@atproto/api'\nimport { AtUri } from '@atproto/syntax'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { SubjectView } from '../../lexicon/types/tools/ozone/moderation/defs'\nimport { addAccountInfoToRepoViewDetail, getPdsAccountInfos } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.getSubjects({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth, req }) => {\n      const { subjects } = params\n      const db = ctx.db\n      const labelers = ctx.reqLabelers(req)\n      const uris = new Set<string>()\n      const dids = new Set<string>()\n\n      for (const subject of subjects) {\n        if (subject.startsWith('did:')) {\n          dids.add(subject)\n        }\n        if (subject.startsWith('at://')) {\n          uris.add(subject)\n          dids.add(new AtUri(subject).host)\n        }\n      }\n\n      const didsArray = Array.from(dids)\n      const modViews = ctx.modService(db).views\n      const [partialRepos, accountInfo, recordInfo, profiles] =\n        await Promise.all([\n          modViews.repoDetails(didsArray, labelers),\n          getPdsAccountInfos(ctx, didsArray),\n          modViews.recordDetails(\n            Array.from(uris).map((uri) => ({ uri })),\n            labelers,\n          ),\n          modViews.getProfiles(didsArray),\n        ])\n\n      const missingSubjects: string[] = []\n      const subjectWithDetails = new Map<string, SubjectView>()\n\n      for (const subject of subjects) {\n        const type = subject.startsWith('did:') ? 'account' : 'record'\n        const did = type === 'account' ? subject : new AtUri(subject).host\n        const partialRepo = partialRepos.get(did)\n        const repo = partialRepo\n          ? addAccountInfoToRepoViewDetail(\n              partialRepo,\n              accountInfo.get(did) || null,\n              auth.credentials.isModerator,\n            )\n          : undefined\n        const profile = profiles.get(did)\n        const record = type === 'record' ? recordInfo.get(subject) : undefined\n        const status =\n          type === 'record'\n            ? record?.moderation.subjectStatus\n            : repo?.moderation.subjectStatus\n\n        subjectWithDetails.set(subject, {\n          type,\n          repo,\n          record,\n          profile: profile && {\n            $type: 'app.bsky.actor.defs#profileViewDetailed',\n            ...profile,\n          },\n          status,\n          subject,\n        })\n\n        if ((type === 'record' && !record) || (type === 'account' && !repo)) {\n          missingSubjects.push(subject)\n        }\n      }\n\n      // When a subject is repo or record but the repo/record was deleted, we still want to attach moderation status if any exists\n      const missingSubjectStatuses =\n        await modViews.getSubjectStatus(missingSubjects)\n\n      for (const [subject, status] of missingSubjectStatuses) {\n        const subjectView = subjectWithDetails.get(subject)\n        if (subjectView)\n          subjectView.status = modViews.formatSubjectStatus(status)\n      }\n\n      const allSubjects: ToolsOzoneModerationDefs.SubjectView[] = []\n      for (const subject of subjects) {\n        const subjectView = subjectWithDetails.get(subject)\n        if (subjectView) allSubjects.push(subjectView)\n      }\n\n      return {\n        encoding: 'application/json',\n        body: { subjects: allSubjects },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/listScheduledActions.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getScheduledActionStatus } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.listScheduledActions({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input }) => {\n      const db = ctx.db\n      const {\n        startsAfter,\n        endsBefore,\n        subjects,\n        statuses,\n        limit = 50,\n        cursor,\n      } = input.body\n\n      const scheduledActionService = ctx.scheduledActionService(db)\n\n      const parsedStatuses = statuses.map((status) =>\n        getScheduledActionStatus(status),\n      )\n\n      const result = await scheduledActionService.listScheduledActions({\n        cursor,\n        limit,\n        startTime: startsAfter ? new Date(startsAfter) : undefined,\n        endTime: endsBefore ? new Date(endsBefore) : undefined,\n        subjects,\n        statuses: parsedStatuses,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          actions: result.actions.map((action) =>\n            scheduledActionService.formatScheduledAction(action),\n          ),\n          cursor: result.cursor,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/queryEvents.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getEventType } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.queryEvents({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const {\n        subject,\n        limit = 50,\n        cursor,\n        sortDirection = 'desc',\n        types,\n        includeAllUserRecords = false,\n        hasComment,\n        comment,\n        createdBy,\n        createdAfter,\n        createdBefore,\n        addedLabels = [],\n        removedLabels = [],\n        addedTags = [],\n        removedTags = [],\n        reportTypes,\n        collections = [],\n        subjectType,\n        policies,\n        modTool,\n        ageAssuranceState,\n        batchId,\n        withStrike,\n      } = params\n      const db = ctx.db\n      const modService = ctx.modService(db)\n      const results = await modService.getEvents({\n        types: types?.length ? types.map(getEventType) : [],\n        subject,\n        createdBy,\n        limit,\n        cursor,\n        sortDirection,\n        includeAllUserRecords,\n        hasComment,\n        comment,\n        createdAfter,\n        createdBefore,\n        addedLabels,\n        addedTags,\n        removedLabels,\n        removedTags,\n        reportTypes,\n        collections,\n        subjectType,\n        policies,\n        modTool,\n        ageAssuranceState,\n        batchId,\n        withStrike,\n      })\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: results.cursor,\n          events: results.events.map((evt) =>\n            modService.views.formatEvent(evt),\n          ),\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/queryStatuses.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.queryStatuses({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const db = ctx.db\n      const modService = ctx.modService(db)\n      const results = await modService.getSubjectStatuses(params)\n      const subjectStatuses = results.statuses.map((status) =>\n        modService.views.formatSubjectStatus(status),\n      )\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: results.cursor,\n          subjectStatuses,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/scheduleAction.ts",
    "content": "import { ToolsOzoneModerationScheduleAction } from '@atproto/api'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { subjectFromInput } from '../../mod-service/subject'\nimport { ExecutionSchedule } from '../../scheduled-action/types'\nimport { getScheduledActionType } from '../util'\nimport { ScheduledTakedownTag } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.scheduleAction({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { action, subjects, createdBy, scheduling, modTool } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to schedule actions')\n      }\n\n      if (access.type === 'admin_token' && !createdBy) {\n        throw new AuthRequiredError(\n          'Must specify createdBy when using admin auth',\n        )\n      }\n\n      const actionType = getScheduledActionType(\n        action.$type?.split('#')[1] || '',\n      )\n\n      const succeeded: string[] = []\n      const failed: ToolsOzoneModerationScheduleAction.FailedScheduling[] = []\n\n      // Defining alternatively required fields is not supported by lexicons so we need to manually validate here\n      if (!scheduling.executeAt && !scheduling.executeAfter) {\n        throw new InvalidRequestError('Must specify an execution schedule')\n      }\n\n      const executionSchedule: ExecutionSchedule = scheduling.executeAt\n        ? { executeAt: new Date(scheduling.executeAt) }\n        : {\n            executeAfter: new Date(scheduling.executeAfter!),\n            executeUntil: scheduling.executeUntil\n              ? new Date(scheduling.executeUntil)\n              : undefined,\n          }\n\n      const eventData = { ...action, modTool }\n      const actualCreatedBy =\n        access.type === 'admin_token' ? createdBy : access.iss\n\n      const now = new Date()\n      for (const subject of subjects) {\n        try {\n          await db.transaction(async (tx) => {\n            const modService = ctx.modService(tx)\n            const scheduledActionService = ctx.scheduledActionService(tx)\n            // register the action in database\n            await scheduledActionService.scheduleAction({\n              action: actionType,\n              eventData,\n              did: subject,\n              createdBy: actualCreatedBy,\n              ...executionSchedule,\n            })\n            // log an event in the mod event stream\n            if (ToolsOzoneModerationScheduleAction.isTakedown(action)) {\n              await modService.logEvent({\n                event: {\n                  $type: 'tools.ozone.moderation.defs#scheduleTakedownEvent',\n                  executeAfter: scheduling.executeAfter,\n                  executeUntil: scheduling.executeUntil,\n                  executeAt: scheduling.executeAt,\n                  comment: action.comment,\n                },\n                subject: subjectFromInput({\n                  did: subject,\n                  $type: 'com.atproto.admin.defs#repoRef',\n                }),\n                createdBy: actualCreatedBy,\n                createdAt: now,\n                modTool,\n              })\n              await modService.logEvent({\n                event: {\n                  $type: 'tools.ozone.moderation.defs#modEventTag',\n                  add: [ScheduledTakedownTag],\n                  remove: [],\n                },\n                subject: subjectFromInput({\n                  did: subject,\n                  $type: 'com.atproto.admin.defs#repoRef',\n                }),\n                createdBy,\n                createdAt: now,\n              })\n            }\n            succeeded.push(subject)\n          })\n        } catch (error) {\n          let errorMessage = 'Unknown error'\n          let errorCode: string | undefined\n\n          if (error instanceof InvalidRequestError) {\n            errorMessage = error.message\n            errorCode = 'InvalidRequest'\n          } else if (error instanceof Error) {\n            errorMessage = error.message\n          }\n\n          failed.push({\n            subject,\n            error: errorMessage,\n            errorCode,\n          })\n        }\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          succeeded,\n          failed,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/searchRepos.ts",
    "content": "import { mapDefined } from '@atproto/common'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { ids } from '../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.moderation.searchRepos({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const modService = ctx.modService(ctx.db)\n\n      // prefer new 'q' query param over deprecated 'term'\n      const query = params.q ?? params.term\n\n      // special case for did searches - do exact match\n      if (query?.startsWith('did:')) {\n        const repos = await modService.views.repos([query])\n        const found = repos.get(query)\n        return {\n          encoding: 'application/json',\n          body: {\n            repos: found ? [found] : [],\n          },\n        }\n      }\n\n      const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors(\n        params,\n        await ctx.appviewAuth(ids.AppBskyActorSearchActors),\n      )\n      const repoMap = await modService.views.repos(\n        res.data.actors.map((a) => a.did),\n      )\n      const repos = mapDefined(res.data.actors, (actor) =>\n        repoMap.get(actor.did),\n      )\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: res.data.cursor,\n          repos,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/moderation/util.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ProtectedTagSettingKey } from '../../setting/constants'\nimport { SettingService } from '../../setting/service'\nimport { ProtectedTagSetting } from '../../setting/types'\n\nexport const getProtectedTags = async (\n  settingService: SettingService,\n  serviceDid: string,\n) => {\n  const protectedTagSetting = await settingService.query({\n    keys: [ProtectedTagSettingKey],\n    scope: 'instance',\n    did: serviceDid,\n    limit: 1,\n  })\n\n  // if no protected tags are configured, then no need to do further check\n  if (!protectedTagSetting.options.length) {\n    return\n  }\n\n  return protectedTagSetting.options[0].value as ProtectedTagSetting\n}\n\nexport const assertProtectedTagAction = ({\n  protectedTags,\n  subjectTags,\n  actionAuthor,\n  isModerator,\n  isAdmin,\n  isTriage,\n}: {\n  protectedTags: ProtectedTagSetting\n  subjectTags: string[]\n  actionAuthor: string\n  isModerator: boolean\n  isAdmin: boolean\n  isTriage: boolean\n}) => {\n  subjectTags.forEach((tag) => {\n    if (!Object.hasOwn(protectedTags, tag)) return\n    if (\n      protectedTags[tag]['moderators'] &&\n      !protectedTags[tag]['moderators'].includes(actionAuthor)\n    ) {\n      throw new InvalidRequestError(\n        `Not allowed to action on protected tag: ${tag}`,\n      )\n    }\n\n    if (protectedTags[tag]['roles']) {\n      if (isAdmin) {\n        if (\n          protectedTags[tag]['roles'].includes(\n            'tools.ozone.team.defs#roleAdmin',\n          )\n        ) {\n          return\n        }\n        throw new InvalidRequestError(\n          `Not allowed to action on protected tag: ${tag}`,\n        )\n      }\n\n      if (isModerator) {\n        if (\n          protectedTags[tag]['roles'].includes(\n            'tools.ozone.team.defs#roleModerator',\n          )\n        ) {\n          return\n        }\n\n        throw new InvalidRequestError(\n          `Not allowed to action on protected tag: ${tag}`,\n        )\n      }\n\n      if (isTriage) {\n        if (\n          protectedTags[tag]['roles'].includes(\n            'tools.ozone.team.defs#roleTriage',\n          )\n        ) {\n          return\n        }\n\n        throw new InvalidRequestError(\n          `Not allowed to action on protected tag: ${tag}`,\n        )\n      }\n    }\n  })\n}\n\nexport const ScheduledTakedownTag = 'scheduled-takedown'\n"
  },
  {
    "path": "packages/ozone/src/api/proxied.ts",
    "content": "import { AppContext } from '../context'\nimport { Server } from '../lexicon'\nimport { ids } from '../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.app.bsky.actor.getProfile({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.actor.getProfile(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyActorGetProfile),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.actor.getProfiles({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.actor.getProfiles(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyActorGetProfiles),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.feed.getAuthorFeed({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.feed.getAuthorFeed(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyFeedGetAuthorFeed),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.feed.searchPosts({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.feed.searchPosts(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyFeedSearchPosts),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.feed.getPostThread({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.feed.getPostThread(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyFeedGetPostThread),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.feed.getFeedGenerator({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.feed.getFeedGenerator(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyFeedGetFeedGenerator),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getFollows({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.graph.getFollows(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetFollows),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getFollowers({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.graph.getFollowers(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetFollowers),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getList({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.graph.getList(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetList),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getLists({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.graph.getLists(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetLists),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.com.atproto.admin.searchAccounts({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      if (!ctx.pdsAgent) {\n        throw new Error('PDS not configured')\n      }\n      const res = await ctx.pdsAgent.com.atproto.admin.searchAccounts(\n        request.params,\n        await ctx.pdsAuth(ids.ComAtprotoAdminSearchAccounts),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.com.atproto.temp.revokeAccountCredentials({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      if (!ctx.pdsAgent) {\n        throw new Error('PDS not configured')\n      }\n      await ctx.pdsAgent.com.atproto.temp.revokeAccountCredentials(\n        request.input.body,\n        await ctx.pdsAuth(ids.ComAtprotoTempRevokeAccountCredentials),\n      )\n    },\n  })\n\n  server.tools.ozone.hosting.getAccountHistory({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      if (!ctx.pdsAgent) {\n        throw new Error('PDS not configured')\n      }\n      const res = await ctx.pdsAgent.tools.ozone.hosting.getAccountHistory(\n        request.params,\n        await ctx.pdsAuth(ids.ToolsOzoneHostingGetAccountHistory),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.tools.ozone.signature.findRelatedAccounts({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      if (!ctx.pdsAgent) {\n        throw new Error('PDS not configured')\n      }\n      const res = await ctx.pdsAgent.tools.ozone.signature.findRelatedAccounts(\n        request.params,\n        await ctx.pdsAuth(ids.ToolsOzoneSignatureFindRelatedAccounts),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.tools.ozone.signature.searchAccounts({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      if (!ctx.pdsAgent) {\n        throw new Error('PDS not configured')\n      }\n      const res = await ctx.pdsAgent.tools.ozone.signature.searchAccounts(\n        request.params,\n        await ctx.pdsAuth(ids.ToolsOzoneSignatureSearchAccounts),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.tools.ozone.signature.findCorrelation({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      if (!ctx.pdsAgent) {\n        throw new Error('PDS not configured')\n      }\n      const res = await ctx.pdsAgent.tools.ozone.signature.findCorrelation(\n        request.params,\n        await ctx.pdsAuth(ids.ToolsOzoneSignatureFindCorrelation),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getStarterPack({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.api.app.bsky.graph.getStarterPack(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetStarterPack),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getStarterPacks({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.graph.getStarterPacks(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetStarterPacks),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.graph.getActorStarterPacks({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.graph.getActorStarterPacks(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyGraphGetActorStarterPacks),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.feed.getLikes({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.feed.getLikes(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyFeedGetLikes),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.feed.getRepostedBy({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.feed.getRepostedBy(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyFeedGetRepostedBy),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n\n  server.app.bsky.actor.searchActorsTypeahead({\n    auth: ctx.authVerifier.moderator,\n    handler: async (request) => {\n      const res = await ctx.appviewAgent.app.bsky.actor.searchActorsTypeahead(\n        request.params,\n        await ctx.appviewAuth(ids.AppBskyActorSearchActorsTypeahead),\n      )\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/report/createReport.ts",
    "content": "import { ForbiddenError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { ReasonType } from '../../lexicon/types/com/atproto/moderation/defs'\nimport { ModerationService } from '../../mod-service'\nimport { subjectFromInput } from '../../mod-service/subject'\nimport { TagService } from '../../tag-service'\nimport { getTagForReport } from '../../tag-service/util'\nimport { isAppealReport } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.moderation.createReport({\n    auth: ctx.authVerifier.standard,\n    handler: async ({ input, auth }) => {\n      const requester =\n        'iss' in auth.credentials ? auth.credentials.iss : ctx.cfg.service.did\n      const { reasonType, reason, modTool } = input.body\n      const subject = subjectFromInput(input.body.subject)\n\n      // If the report is an appeal, the requester must be the author of the subject\n      if (isAppealReport(reasonType) && requester !== subject.did) {\n        throw new ForbiddenError('You cannot appeal this report')\n      }\n\n      const db = ctx.db\n\n      await Promise.all([\n        assertValidReporter(ctx.modService(db), reasonType, requester),\n        ctx.moderationServiceProfile().validateReasonType(reasonType),\n      ])\n\n      const report = await db.transaction(async (dbTxn) => {\n        const moderationTxn = ctx.modService(dbTxn)\n        const { event: reportEvent, subjectStatus } =\n          await moderationTxn.report({\n            reason,\n            subject,\n            reasonType,\n            reportedBy: requester || ctx.cfg.service.did,\n            modTool,\n          })\n\n        const tagService = new TagService(\n          subject,\n          subjectStatus,\n          ctx.cfg.service.did,\n          moderationTxn,\n        )\n        await tagService.evaluateForSubject([getTagForReport(reasonType)])\n\n        return reportEvent\n      })\n\n      const body = ctx.modService(db).views.formatReport(report)\n      return {\n        encoding: 'application/json',\n        body,\n      }\n    },\n  })\n}\n\nconst assertValidReporter = async (\n  modService: ModerationService,\n  reasonType: ReasonType,\n  did: string,\n) => {\n  const reporterStatus = await modService.getCurrentStatus({ did })\n\n  // If we don't have a mod status for the reporter, no need to do further checks\n  if (!reporterStatus.length) {\n    return\n  }\n\n  // For appeals, we just need to make sure that the account does not have pending appeal\n  if (isAppealReport(reasonType)) {\n    if (reporterStatus[0]?.appealed) {\n      throw new ForbiddenError(\n        'Awaiting decision on previous appeal',\n        'AlreadyAppealed',\n      )\n    }\n    return\n  }\n\n  // For non appeals, we need to make sure the reporter account is not already in takendown status\n  // This is necessary because we allow takendown accounts call createReport but that's only meant for appeals\n  // and we need to make sure takendown accounts don't abuse this endpoint\n  if (reporterStatus[0]?.takendown) {\n    throw new ForbiddenError(\n      'Report not accepted from takendown account',\n      'AccountTakedown',\n    )\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/api/safelink/addRule.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport {\n  getSafelinkAction,\n  getSafelinkPattern,\n  getSafelinkReason,\n} from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.safelink.addRule({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { url, pattern, action, reason, comment, createdBy } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to add URL rules')\n      }\n\n      if (access.type === 'admin_token' && !createdBy) {\n        throw new AuthRequiredError(\n          'Must specify createdBy when using admin auth',\n        )\n      }\n\n      const safelinkRuleService = ctx.safelinkRuleService(db)\n\n      const event = await safelinkRuleService.addRule({\n        url,\n        pattern: getSafelinkPattern(pattern),\n        action: getSafelinkAction(action),\n        reason: getSafelinkReason(reason),\n        createdBy:\n          access.type === 'admin_token'\n            ? createdBy || ctx.cfg.service.did\n            : access.iss,\n        comment,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: safelinkRuleService.formatEvent(event),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/safelink/queryEvents.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getSafelinkPattern } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.safelink.queryEvents({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input }) => {\n      const db = ctx.db\n      const { cursor, limit, urls, patternType, sortDirection } = input.body\n\n      const safelinkRuleService = ctx.safelinkRuleService(db)\n      const result = await safelinkRuleService.queryEvents({\n        cursor,\n        limit,\n        urls,\n        patternType: patternType ? getSafelinkPattern(patternType) : undefined,\n        direction: sortDirection as 'asc' | 'desc' | undefined,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: result.cursor,\n          events: result.events.map((event) =>\n            safelinkRuleService.formatEvent(event),\n          ),\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/safelink/queryRules.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport {\n  getSafelinkAction,\n  getSafelinkPattern,\n  getSafelinkReason,\n} from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.safelink.queryRules({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input }) => {\n      const db = ctx.db\n      const {\n        cursor,\n        limit,\n        urls,\n        patternType,\n        actions,\n        reason,\n        createdBy,\n        sortDirection,\n      } = input.body\n\n      const safelinkRuleService = ctx.safelinkRuleService(db)\n      const result = await safelinkRuleService.getActiveRules({\n        cursor,\n        limit,\n        urls,\n        patternType: patternType ? getSafelinkPattern(patternType) : undefined,\n        actions:\n          actions && actions.length > 0\n            ? actions.map(getSafelinkAction)\n            : undefined,\n        reason: reason ? getSafelinkReason(reason) : undefined,\n        createdBy,\n        direction: sortDirection as 'asc' | 'desc' | undefined,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: result.cursor,\n          rules: result.rules.map((rule) => ({\n            url: rule.url,\n            pattern: rule.pattern,\n            action: rule.action,\n            reason: rule.reason,\n            createdBy: rule.createdBy,\n            createdAt: new Date(rule.createdAt).toISOString(),\n            updatedAt: new Date(rule.updatedAt).toISOString(),\n            comment: rule.comment || undefined,\n          })),\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/safelink/removeRule.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getSafelinkPattern } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.safelink.removeRule({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { url, pattern, comment, createdBy } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to remove URL rules')\n      }\n\n      if (access.type === 'admin_token' && !createdBy) {\n        throw new AuthRequiredError(\n          'Must specify createdBy when using admin auth',\n        )\n      }\n\n      const safelinkRuleService = ctx.safelinkRuleService(db)\n\n      const event = await safelinkRuleService.removeRule({\n        url,\n        pattern: getSafelinkPattern(pattern),\n        createdBy:\n          access.type === 'admin_token'\n            ? createdBy || ctx.cfg.service.did\n            : access.iss,\n        comment,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: safelinkRuleService.formatEvent(event),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/safelink/updateRule.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport {\n  getSafelinkAction,\n  getSafelinkPattern,\n  getSafelinkReason,\n} from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.safelink.updateRule({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { url, pattern, action, reason, comment, createdBy } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to update URL rules')\n      }\n\n      if (access.type === 'admin_token' && !createdBy) {\n        throw new AuthRequiredError(\n          'Must specify createdBy when using admin auth',\n        )\n      }\n\n      const safelinkRuleService = ctx.safelinkRuleService(db)\n\n      const event = await safelinkRuleService.updateRule({\n        url,\n        pattern: getSafelinkPattern(pattern),\n        action: getSafelinkAction(action),\n        reason: getSafelinkReason(reason),\n        createdBy:\n          access.type === 'admin_token'\n            ? createdBy || ctx.cfg.service.did\n            : access.iss,\n        comment,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: safelinkRuleService.formatEvent(event),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/server/getConfig.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server, TOOLS_OZONE_TEAM } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.server.getConfig({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ auth }) => {\n      return {\n        encoding: 'application/json',\n        body: {\n          appview: {\n            url: ctx.cfg.appview.url,\n          },\n          blobDivert: {\n            url: ctx.cfg.blobDivert?.url,\n          },\n          pds: {\n            url: ctx.cfg.pds?.url,\n          },\n          chat: {\n            url: ctx.cfg.chat?.url,\n          },\n          viewer: {\n            role: auth.credentials.isAdmin\n              ? TOOLS_OZONE_TEAM.DefsRoleAdmin\n              : auth.credentials.isModerator\n                ? TOOLS_OZONE_TEAM.DefsRoleModerator\n                : TOOLS_OZONE_TEAM.DefsRoleTriage,\n          },\n          verifierDid: ctx.cfg.verifier?.did || undefined,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/set/addValues.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.set.addValues({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { name, values } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to add values to a set',\n        )\n      }\n\n      const setService = ctx.setService(db)\n      const set = await setService.getByName(name)\n      if (!set) {\n        throw new InvalidRequestError(`Set with name \"${name}\" does not exist`)\n      }\n\n      await setService.addValues(set.id, values)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/set/deleteSet.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.set.deleteSet({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { name } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to delete a set')\n      }\n\n      const setService = ctx.setService(db)\n      const set = await setService.getByName(name)\n      if (!set) {\n        throw new InvalidRequestError(\n          `Set with name \"${name}\" does not exist`,\n          'SetNotFound',\n        )\n      }\n\n      await setService.removeSet(set.id)\n\n      return {\n        encoding: 'application/json',\n        body: {},\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/set/deleteValues.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.set.deleteValues({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { name, values } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to remove values from a set',\n        )\n      }\n\n      const setService = ctx.setService(db)\n      const set = await setService.getByName(name)\n      if (!set) {\n        throw new InvalidRequestError(\n          `Set with name \"${name}\" does not exist`,\n          'SetNotFound',\n        )\n      }\n\n      await setService.removeValues(set.id, values)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/set/getValues.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.set.getValues({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { name, limit, cursor } = params\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to get set details')\n      }\n\n      const setService = ctx.setService(db)\n\n      const result = await setService.getSetWithValues({\n        name,\n        limit,\n        cursor,\n      })\n\n      if (!result) {\n        throw new InvalidRequestError(\n          `Set with name \"${name}\" not found`,\n          'SetNotFound',\n        )\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          set: setService.view(result.set),\n          values: result.values,\n          cursor: result.cursor,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/set/querySets.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.set.querySets({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { limit, cursor, namePrefix, sortBy, sortDirection } = params\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError('Must be a moderator to query sets')\n      }\n\n      const setService = ctx.setService(db)\n\n      const queryResult = await setService.query({\n        limit,\n        cursor,\n        namePrefix,\n        sortBy,\n        sortDirection,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          sets: queryResult.sets.map((set) => setService.view(set)),\n          cursor: queryResult.cursor,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/set/upsertSet.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.set.upsertSet({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { name, description } = input.body\n\n      if (!access.isModerator) {\n        throw new AuthRequiredError(\n          'Must be a moderator to create or update a set',\n        )\n      }\n\n      const setService = ctx.setService(db)\n\n      await setService.upsert({\n        name,\n        description: description ?? null,\n      })\n      const setWithSize = await setService.getByNameWithSize(name)\n\n      // Unlikely to happen since we just upserted the set\n      if (!setWithSize) {\n        throw new InvalidRequestError(`Set not found`)\n      }\n\n      return {\n        encoding: 'application/json',\n        body: setService.view(setWithSize),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/setting/listOptions.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.setting.listOptions({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { prefix, scope, keys, limit, cursor } = params\n      let did = ctx.cfg.service.did\n\n      if (scope === 'personal') {\n        if (access.type !== 'moderator') {\n          throw new AuthRequiredError(\n            'Must use moderator auth to get personal set details',\n          )\n        }\n\n        did = access.iss\n      }\n\n      const settingService = ctx.settingService(db)\n\n      const result = await settingService.query({\n        scope: scope === 'personal' ? 'personal' : 'instance',\n        did,\n        keys,\n        prefix,\n        limit,\n        cursor,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          options: result.options.map((option) => settingService.view(option)),\n          cursor: result.cursor,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/setting/removeOptions.ts",
    "content": "import { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Member } from '../../db/schema/member'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.setting.removeOptions({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { keys, scope } = input.body\n      let did = ctx.cfg.service.did\n      let managerRole: Member['role'][] = []\n\n      if (scope === 'personal') {\n        if (access.type !== 'moderator') {\n          throw new AuthRequiredError(\n            'Must use moderator auth to delete personal setting',\n          )\n        }\n\n        did = access.iss\n      }\n\n      // When attempting to delete an instance setting using admin_token will allow removing any setting\n      // otherwise, admins can remove settings that are manageable by all roles\n      // moderators can remove settings that are manageable by moderator and triage roles\n      // triage can remove settings that are manageable by triage role\n      if (scope === 'instance') {\n        managerRole = [\n          'tools.ozone.team.defs#roleModerator',\n          'tools.ozone.team.defs#roleTriage',\n          'tools.ozone.team.defs#roleAdmin',\n          'tools.ozone.team.defs#roleVerifier',\n        ]\n\n        if (access.type !== 'admin_token' && !access.isAdmin) {\n          if (access.isModerator) {\n            managerRole = [\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n            ]\n          } else if (access.isTriage) {\n            managerRole = ['tools.ozone.team.defs#roleTriage']\n          }\n        }\n      }\n\n      const settingService = ctx.settingService(db)\n\n      await settingService.removeOptions(keys, {\n        scope: scope === 'personal' ? 'personal' : 'instance',\n        managerRole,\n        did,\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {},\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/setting/upsertOption.ts",
    "content": "import assert from 'node:assert'\nimport { ToolsOzoneTeamDefs } from '@atproto/api'\nimport { AuthRequiredError } from '@atproto/xrpc-server'\nimport { AdminTokenOutput, ModeratorOutput } from '../../auth-verifier'\nimport { AppContext } from '../../context'\nimport { Member } from '../../db/schema/member'\nimport { Server } from '../../lexicon'\nimport { SettingService } from '../../setting/service'\nimport { settingValidators } from '../../setting/validators'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.setting.upsertOption({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { key, value, description, managerRole, scope } = input.body\n      const serviceDid = ctx.cfg.service.did\n      let ownerDid = serviceDid\n\n      if (scope === 'personal' && access.type !== 'moderator') {\n        throw new AuthRequiredError(\n          'Must use moderator auth to create or update a personal setting',\n        )\n      }\n\n      // if the caller is using moderator auth and storing personal setting\n      // use the caller's DID as the owner\n      if (scope === 'personal' && access.type === 'moderator') {\n        ownerDid = access.iss\n      }\n\n      const now = new Date()\n      const baseOption = {\n        key,\n        value,\n        did: ownerDid,\n        createdBy: ownerDid,\n        lastUpdatedBy: ownerDid,\n        description: description || '',\n        createdAt: now,\n        updatedAt: now,\n      }\n\n      const settingService = ctx.settingService(db)\n      if (scope === 'personal') {\n        await settingService.upsert({\n          ...baseOption,\n          scope: 'personal',\n          managerRole: null,\n        })\n      } else {\n        const manageableRoles = getRolesForInstanceOption(access)\n        const existingSetting = await getExistingSetting(\n          settingService,\n          ownerDid,\n          key,\n          'instance',\n        )\n\n        if (\n          existingSetting?.managerRole &&\n          !manageableRoles.includes(existingSetting.managerRole)\n        ) {\n          throw new AuthRequiredError(`Not permitted to update setting ${key}`)\n        }\n        const option = {\n          ...baseOption,\n          scope: 'instance' as const,\n          managerRole: getManagerRole(managerRole),\n        }\n\n        if (settingValidators.has(key)) {\n          await settingValidators.get(key)?.(option)\n        }\n\n        await settingService.upsert(option)\n      }\n\n      const newOption = await getExistingSetting(\n        settingService,\n        ownerDid,\n        key,\n        scope,\n      )\n      assert(newOption, 'Failed to get the updated setting')\n\n      return {\n        encoding: 'application/json',\n        body: {\n          option: settingService.view(newOption),\n        },\n      }\n    },\n  })\n}\n\nconst getExistingSetting = async (\n  settingService: SettingService,\n  did: string,\n  key: string,\n  scope: string,\n) => {\n  const result = await settingService.query({\n    scope: scope === 'personal' ? 'personal' : 'instance',\n    keys: [key],\n    limit: 1,\n    did,\n  })\n\n  return result.options[0]\n}\n\nconst getRolesForInstanceOption = (\n  access: AdminTokenOutput['credentials'] | ModeratorOutput['credentials'],\n) => {\n  const fullPermission = [\n    ToolsOzoneTeamDefs.ROLEADMIN,\n    ToolsOzoneTeamDefs.ROLEMODERATOR,\n    ToolsOzoneTeamDefs.ROLETRIAGE,\n    ToolsOzoneTeamDefs.ROLEVERIFIER,\n  ]\n  if (access.type === 'admin_token') {\n    return fullPermission\n  }\n\n  if (access.isAdmin) {\n    return fullPermission\n  }\n\n  if (access.isModerator) {\n    return [ToolsOzoneTeamDefs.ROLEMODERATOR, ToolsOzoneTeamDefs.ROLETRIAGE]\n  }\n\n  if (access.isVerifier) {\n    return [ToolsOzoneTeamDefs.ROLEVERIFIER]\n  }\n\n  return [ToolsOzoneTeamDefs.ROLETRIAGE]\n}\n\nconst getManagerRole = (role?: string) => {\n  let managerRole: Member['role'] | null = null\n\n  if (role === ToolsOzoneTeamDefs.ROLEADMIN) {\n    managerRole = ToolsOzoneTeamDefs.ROLEADMIN\n  } else if (role === ToolsOzoneTeamDefs.ROLEMODERATOR) {\n    managerRole = ToolsOzoneTeamDefs.ROLEMODERATOR\n  } else if (role === ToolsOzoneTeamDefs.ROLETRIAGE) {\n    managerRole = ToolsOzoneTeamDefs.ROLETRIAGE\n  } else if (role === ToolsOzoneTeamDefs.ROLEVERIFIER) {\n    managerRole = ToolsOzoneTeamDefs.ROLEVERIFIER\n  }\n\n  return managerRole\n}\n"
  },
  {
    "path": "packages/ozone/src/api/team/addMember.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getMemberRole } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.team.addMember({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { did, role } = input.body\n\n      if (!access.isAdmin) {\n        throw new AuthRequiredError('Must be an admin to add a member')\n      }\n\n      const newMember = await db.transaction(async (dbTxn) => {\n        const teamService = ctx.teamService(dbTxn)\n        const alreadyExists = await teamService.doesMemberExist(did)\n\n        if (alreadyExists) {\n          throw new InvalidRequestError(\n            'member already exists',\n            'MemberAlreadyExists',\n          )\n        }\n\n        const profiles = await teamService.getProfiles([did])\n        const profile = profiles.get(did)\n\n        const member = await teamService.create({\n          did,\n          handle: profile?.handle || null,\n          displayName: profile?.displayName || null,\n          disabled: false,\n          role: getMemberRole(role),\n          lastUpdatedBy:\n            access.type === 'admin_token' ? 'admin_token' : access.iss,\n        })\n        const memberView = await teamService.view([member])\n        return memberView[0]\n      })\n\n      return {\n        encoding: 'application/json',\n        body: newMember,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/team/deleteMember.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.team.deleteMember({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { did } = input.body\n\n      if (!access.isAdmin) {\n        throw new AuthRequiredError('Must be an admin to delete a member')\n      }\n      if ('did' in auth.credentials && did === auth.credentials.did) {\n        throw new InvalidRequestError(\n          'You can not delete yourself from the team',\n          'CannotDeleteSelf',\n        )\n      }\n      await db.transaction(async (dbTxn) => {\n        const teamService = ctx.teamService(dbTxn)\n        await teamService.assertCanDelete(did)\n        await teamService.delete(did)\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/team/listMembers.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.team.listMembers({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ params }) => {\n      const teamService = ctx.teamService(ctx.db)\n      const { members, cursor } = await teamService.list(params)\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor,\n          members: await teamService.view(members),\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/team/updateMember.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getMemberRole } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.team.updateMember({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      const access = auth.credentials\n      const db = ctx.db\n      const { did, role, ...rest } = input.body\n\n      if (!access.isAdmin) {\n        throw new AuthRequiredError('Must be an admin to update a member')\n      }\n\n      if (did === ctx.cfg.service.did) {\n        throw new InvalidRequestError('Can not update service owner')\n      }\n\n      const updatedMember = await db.transaction(async (dbTxn) => {\n        const teamService = ctx.teamService(dbTxn)\n\n        const memberExists = await teamService.doesMemberExist(did)\n\n        if (!memberExists) {\n          throw new InvalidRequestError('member not found', 'MemberNotFound')\n        }\n\n        const updated = await teamService.update(did, {\n          ...rest,\n          ...(role ? { role: getMemberRole(role) } : {}),\n          lastUpdatedBy:\n            access.type === 'admin_token' ? 'admin_token' : access.iss,\n        })\n        const memberView = await teamService.view([updated])\n        return memberView[0]\n      })\n\n      return {\n        encoding: 'application/json',\n        body: updatedMember,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/util.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../context'\nimport { Member } from '../db/schema/member'\nimport { ModerationEvent } from '../db/schema/moderation_event'\nimport { ids } from '../lexicon/lexicons'\nimport { AccountView } from '../lexicon/types/com/atproto/admin/defs'\nimport { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'\nimport {\n  REVIEWCLOSED,\n  REVIEWESCALATED,\n  REVIEWNONE,\n  REVIEWOPEN,\n  RepoView,\n  RepoViewDetail,\n} from '../lexicon/types/tools/ozone/moderation/defs'\nimport {\n  ROLEADMIN,\n  ROLEMODERATOR,\n  ROLETRIAGE,\n  ROLEVERIFIER,\n} from '../lexicon/types/tools/ozone/team/defs'\nimport { ModerationSubjectStatusRow } from '../mod-service/types'\n\nexport const getPdsAccountInfos = async (\n  ctx: AppContext,\n  dids: string[],\n): Promise<Map<string, AccountView | null>> => {\n  const results = new Map<string, AccountView | null>()\n\n  const agent = ctx.pdsAgent\n  if (!agent || !dids.length) return results\n\n  const auth = await ctx.pdsAuth(ids.ComAtprotoAdminGetAccountInfos)\n  if (!auth) return results\n\n  try {\n    const res = await agent.com.atproto.admin.getAccountInfos({ dids }, auth)\n    res.data.infos.forEach((info) => {\n      results.set(info.did, info)\n    })\n    return results\n  } catch {\n    return results\n  }\n}\n\nfunction un$type<T extends object>(obj: T): Omit<T, '$type'> {\n  if ('$type' in obj) {\n    const { $type: _, ...rest } = obj\n    return rest\n  }\n  return obj\n}\n\nexport const addAccountInfoToRepoViewDetail = (\n  repoView: RepoView | RepoViewDetail,\n  accountInfo: AccountView | null,\n  includeEmail = false,\n): RepoViewDetail => {\n  if (!accountInfo) {\n    return un$type({\n      ...repoView,\n      moderation: un$type(repoView.moderation),\n    })\n  }\n\n  const {\n    email,\n    deactivatedAt,\n    emailConfirmedAt,\n    inviteNote,\n    invitedBy,\n    invites,\n    invitesDisabled,\n    threatSignatures,\n    // pick some duplicate/unwanted details out\n    $type: _accountType,\n    did: _did,\n    handle: _handle,\n    indexedAt: _indexedAt,\n    relatedRecords: _relatedRecords,\n    ...otherAccountInfo\n  } = accountInfo\n  return {\n    ...otherAccountInfo,\n    ...un$type(repoView),\n    moderation: un$type(repoView.moderation),\n    email: includeEmail ? email : undefined,\n    invitedBy,\n    invitesDisabled,\n    inviteNote,\n    invites,\n    emailConfirmedAt,\n    deactivatedAt,\n    threatSignatures,\n  }\n}\n\nexport const addAccountInfoToRepoView = (\n  repoView: RepoView,\n  accountInfo: AccountView | null,\n  includeEmail = false,\n): RepoView => {\n  if (!accountInfo) return repoView\n  return {\n    ...repoView,\n    email: includeEmail ? accountInfo.email : undefined,\n    invitedBy: accountInfo.invitedBy,\n    invitesDisabled: accountInfo.invitesDisabled,\n    inviteNote: accountInfo.inviteNote,\n    deactivatedAt: accountInfo.deactivatedAt,\n    threatSignatures: accountInfo.threatSignatures,\n  }\n}\n\nexport const getEventType = (type: string) => {\n  if (eventTypes.has(type)) {\n    return type as ModerationEvent['action']\n  }\n  throw new InvalidRequestError('Invalid event type')\n}\n\nexport const getReviewState = (reviewState?: string) => {\n  if (!reviewState) return undefined\n  if (reviewStates.has(reviewState)) {\n    return reviewState as ModerationSubjectStatusRow['reviewState']\n  }\n  throw new InvalidRequestError('Invalid review state')\n}\n\nconst reviewStates = new Set([\n  REVIEWCLOSED,\n  REVIEWESCALATED,\n  REVIEWOPEN,\n  REVIEWNONE,\n])\n\nconst eventTypes = new Set([\n  'tools.ozone.moderation.defs#modEventTakedown',\n  'tools.ozone.moderation.defs#modEventAcknowledge',\n  'tools.ozone.moderation.defs#modEventEscalate',\n  'tools.ozone.moderation.defs#modEventComment',\n  'tools.ozone.moderation.defs#modEventLabel',\n  'tools.ozone.moderation.defs#modEventReport',\n  'tools.ozone.moderation.defs#modEventMute',\n  'tools.ozone.moderation.defs#modEventUnmute',\n  'tools.ozone.moderation.defs#modEventMuteReporter',\n  'tools.ozone.moderation.defs#modEventUnmuteReporter',\n  'tools.ozone.moderation.defs#modEventReverseTakedown',\n  'tools.ozone.moderation.defs#modEventEmail',\n  'tools.ozone.moderation.defs#modEventResolveAppeal',\n  'tools.ozone.moderation.defs#modEventTag',\n  'tools.ozone.moderation.defs#modEventDivert',\n  'tools.ozone.moderation.defs#accountEvent',\n  'tools.ozone.moderation.defs#identityEvent',\n  'tools.ozone.moderation.defs#recordEvent',\n  'tools.ozone.moderation.defs#modEventPriorityScore',\n  'tools.ozone.moderation.defs#ageAssuranceEvent',\n  'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n  'tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n  'tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n  'tools.ozone.moderation.defs#scheduleTakedownEvent',\n  'tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n])\n\nexport const getMemberRole = (role: string) => {\n  if (memberRoles.has(role)) {\n    return role as Member['role']\n  }\n  throw new InvalidRequestError('Invalid member role')\n}\n\nconst memberRoles = new Set([\n  ROLEADMIN,\n  ROLEMODERATOR,\n  ROLETRIAGE,\n  ROLEVERIFIER,\n])\n\nexport const OZONE_APPEAL_REASON_TYPE = 'tools.ozone.report.defs#reasonAppeal'\nconst APPEAL_REASON_TYPES = [REASONAPPEAL, OZONE_APPEAL_REASON_TYPE]\nexport const isAppealReport = (reasonType?: string): boolean => {\n  return !!reasonType && APPEAL_REASON_TYPES.includes(reasonType)\n}\n\nexport const getSafelinkPattern = (pattern: string): SafelinkPatternType => {\n  if (safelinkPatterns.has(pattern)) {\n    return pattern as SafelinkPatternType\n  }\n  throw new InvalidRequestError('Invalid safelink pattern type')\n}\n\nexport const getSafelinkAction = (action: string): SafelinkActionType => {\n  if (safelinkActions.has(action)) {\n    return action as SafelinkActionType\n  }\n  throw new InvalidRequestError('Invalid safelink action type')\n}\n\nexport const getSafelinkReason = (reason: string): SafelinkReasonType => {\n  if (safelinkReasons.has(reason)) {\n    return reason as SafelinkReasonType\n  }\n  throw new InvalidRequestError('Invalid safelink reason type')\n}\n\nexport const getSafelinkEventType = (eventType: string): SafelinkEventType => {\n  if (safelinkEventTypes.has(eventType)) {\n    return eventType as SafelinkEventType\n  }\n  throw new InvalidRequestError('Invalid safelink event type')\n}\n\nexport type SafelinkEventType = 'addRule' | 'updateRule' | 'removeRule'\nexport type SafelinkPatternType = 'domain' | 'url'\nexport type SafelinkActionType = 'block' | 'warn' | 'whitelist'\nexport type SafelinkReasonType = 'csam' | 'spam' | 'phishing' | 'none'\n\nconst safelinkPatterns = new Set(['domain', 'url'])\nconst safelinkActions = new Set(['block', 'warn', 'whitelist'])\nconst safelinkReasons = new Set(['csam', 'spam', 'phishing', 'none'])\nconst safelinkEventTypes = new Set(['addRule', 'updateRule', 'removeRule'])\n\nexport const getScheduledActionType = (action: string): ScheduledActionType => {\n  if (scheduledActionTypes.has(action)) {\n    return action as ScheduledActionType\n  }\n  throw new InvalidRequestError('Invalid scheduled action type')\n}\n\nexport const getScheduledActionStatus = (\n  status: string,\n): ScheduledActionStatus => {\n  if (scheduledActionStatuses.has(status)) {\n    return status as ScheduledActionStatus\n  }\n  throw new InvalidRequestError('Invalid scheduled action status')\n}\n\nexport type ScheduledActionType = 'takedown'\nexport type ScheduledActionStatus =\n  | 'pending'\n  | 'executed'\n  | 'cancelled'\n  | 'failed'\n\nconst scheduledActionTypes = new Set(['takedown'])\nconst scheduledActionStatuses = new Set([\n  'pending',\n  'executed',\n  'cancelled',\n  'failed',\n])\n"
  },
  {
    "path": "packages/ozone/src/api/verification/grantVerifications.ts",
    "content": "import { Selectable } from 'kysely'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Verification } from '../../db/schema/verification'\nimport { Server } from '../../lexicon'\nimport { getReposForVerifications } from '../../verification/util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.verification.grantVerifications({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth, req }) => {\n      if (!ctx.cfg.verifier) {\n        throw new InvalidRequestError('Verifier not configured')\n      }\n\n      if (!auth.credentials.isVerifier) {\n        throw new AuthRequiredError(\n          'Must be an admin or verifier to grant verifications',\n        )\n      }\n\n      const modViews = ctx.modService(ctx.db).views\n      const profilesBefore = await modViews.getProfiles(\n        input.body.verifications.map((v) => v.subject),\n      )\n\n      // Filter out any subject for which, the current issuer already has a valid verification record indexed\n      const verificationsToBeGranted = input.body.verifications.filter(\n        (verificationInput) => {\n          const hasValidVerification = profilesBefore\n            .get(verificationInput.subject)\n            ?.verification?.verifications.find(\n              (v) => v.issuer === ctx.cfg.verifier?.did && v.isValid,\n            )\n          return !hasValidVerification\n        },\n      )\n\n      const verificationIssuer = ctx.verificationIssuer(ctx.cfg.verifier)\n      const verificationService = ctx.verificationService(ctx.db)\n      const { grantedVerifications, failedVerifications } =\n        await verificationIssuer.verify(verificationsToBeGranted)\n\n      if (!grantedVerifications.length) {\n        return {\n          encoding: 'application/json',\n          body: {\n            verifications: [],\n            failedVerifications,\n          },\n        }\n      }\n\n      const createdVerifications: Selectable<Verification>[] = []\n      const verificationEntries =\n        await verificationService.create(grantedVerifications)\n\n      const dids = new Set<string>([ctx.cfg.verifier.did])\n\n      for (const verification of verificationEntries) {\n        createdVerifications.push(verification)\n        dids.add(verification.subject)\n      }\n\n      const didsArr = Array.from(dids)\n      const [repos, profiles] = await Promise.all([\n        getReposForVerifications(\n          ctx,\n          ctx.reqLabelers(req),\n          ctx.modService(ctx.db),\n          didsArr,\n          auth.credentials.isModerator,\n        ),\n        modViews.getProfiles(didsArr),\n      ])\n      const verifications = verificationService.view(\n        createdVerifications,\n        repos,\n        profiles,\n      )\n      return {\n        encoding: 'application/json',\n        body: {\n          verifications,\n          failedVerifications,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/verification/listVerifications.ts",
    "content": "import { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\nimport { getReposForVerifications } from '../../verification/util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.verification.listVerifications({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ req, params, auth }) => {\n      const modViews = ctx.modService(ctx.db).views\n      const verificationService = ctx.verificationService(ctx.db)\n      const { verifications, cursor } = await verificationService.list(params)\n\n      const dids = new Set<string>()\n      for (const verification of verifications) {\n        dids.add(verification.subject)\n        dids.add(verification.issuer)\n      }\n\n      const didsArr = Array.from(dids)\n      const [repos, profiles] = await Promise.all([\n        getReposForVerifications(\n          ctx,\n          ctx.reqLabelers(req),\n          ctx.modService(ctx.db),\n          didsArr,\n          auth.credentials.isModerator,\n        ),\n        modViews.getProfiles(didsArr),\n      ])\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor,\n          verifications: verificationService.view(\n            verifications,\n            repos,\n            profiles,\n          ),\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/verification/revokeVerifications.ts",
    "content": "import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../context'\nimport { Server } from '../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.tools.ozone.verification.revokeVerifications({\n    auth: ctx.authVerifier.modOrAdminToken,\n    handler: async ({ input, auth }) => {\n      if (!ctx.cfg.verifier) {\n        throw new InvalidRequestError('Verifier not configured')\n      }\n\n      if (!auth.credentials.isVerifier) {\n        throw new AuthRequiredError(\n          'Must be an admin or verifier to revoke verifications',\n        )\n      }\n\n      const verificationIssuer = ctx.verificationIssuer(ctx.cfg.verifier)\n      const { uris, revokeReason } = input.body\n      const { revokedVerifications, failedRevocations } =\n        await verificationIssuer.revoke({ uris })\n\n      if (revokedVerifications.length) {\n        const verificationService = ctx.verificationService(ctx.db)\n        await verificationService.markRevoked({\n          uris: revokedVerifications,\n          revokeReason,\n          revokedBy:\n            'iss' in auth.credentials ? auth.credentials.iss : undefined,\n        })\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          revokedVerifications,\n          failedRevocations,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/api/well-known.ts",
    "content": "import { Router } from 'express'\nimport { AppContext } from '../context'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  router.get('/.well-known/did.json', (_req, res) => {\n    const hostname =\n      ctx.cfg.service.publicUrl && new URL(ctx.cfg.service.publicUrl).hostname\n    if (!hostname || ctx.cfg.service.did !== `did:web:${hostname}`) {\n      return res.sendStatus(404)\n    }\n    res.json({\n      '@context': [\n        'https://www.w3.org/ns/did/v1',\n        'https://w3id.org/security/multikey/v1',\n      ],\n      id: ctx.cfg.service.did,\n      verificationMethod: [\n        {\n          id: `${ctx.cfg.service.did}#atproto_label`,\n          type: 'Multikey',\n          controller: ctx.cfg.service.did,\n          publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''),\n        },\n      ],\n      service: [\n        {\n          id: '#atproto_labeler',\n          type: 'AtprotoLabeler',\n          serviceEndpoint: `https://${hostname}`,\n        },\n      ],\n    })\n  })\n\n  router.get('/.well-known/ozone-metadata.json', (_req, res) => {\n    return res.json({\n      did: ctx.cfg.service.did,\n      url: ctx.cfg.service.publicUrl,\n      publicKey: ctx.signingKey.did(),\n    })\n  })\n\n  return router\n}\n"
  },
  {
    "path": "packages/ozone/src/auth-verifier.ts",
    "content": "import express from 'express'\nimport * as ui8 from 'uint8arrays'\nimport { IdResolver } from '@atproto/identity'\nimport {\n  AuthRequiredError,\n  parseReqNsid,\n  verifyJwt,\n} from '@atproto/xrpc-server'\nimport { TeamService } from './team'\n\ntype ReqCtx = {\n  req: express.Request\n}\n\nexport type AdminTokenOutput = {\n  credentials: {\n    type: 'admin_token'\n    isAdmin: true\n    isModerator: true\n    isTriage: true\n    isVerifier: true\n  }\n}\n\nexport type ModeratorOutput = {\n  credentials: {\n    type: 'moderator'\n    aud: string\n    iss: string\n    isAdmin: boolean\n    isModerator: boolean\n    isTriage: boolean\n    isVerifier: boolean\n  }\n}\n\ntype StandardOutput = {\n  credentials: {\n    type: 'standard'\n    aud: string\n    iss: string\n    isAdmin: boolean\n    isModerator: boolean\n    isTriage: boolean\n    isVerifier: boolean\n  }\n}\n\ntype NullOutput = {\n  credentials: {\n    type: 'none'\n    iss: null\n  }\n}\n\nexport type AuthVerifierOpts = {\n  serviceDid: string\n  adminPassword: string\n  teamService: TeamService\n}\n\nexport class AuthVerifier {\n  serviceDid: string\n  teamService: TeamService\n  private adminPassword: string\n\n  constructor(\n    public idResolver: IdResolver,\n    opts: AuthVerifierOpts,\n  ) {\n    this.serviceDid = opts.serviceDid\n    this.adminPassword = opts.adminPassword\n    this.teamService = opts.teamService\n  }\n\n  modOrAdminToken = async (\n    reqCtx: ReqCtx,\n  ): Promise<ModeratorOutput | AdminTokenOutput> => {\n    if (isBasicToken(reqCtx.req)) {\n      return this.adminToken(reqCtx)\n    } else {\n      return this.moderator(reqCtx)\n    }\n  }\n\n  moderator = async (reqCtx: ReqCtx): Promise<ModeratorOutput> => {\n    const creds = await this.standard(reqCtx)\n    if (!creds.credentials.isTriage && !creds.credentials.isVerifier) {\n      throw new AuthRequiredError('not a moderator account')\n    }\n    return {\n      credentials: {\n        ...creds.credentials,\n        type: 'moderator',\n      },\n    }\n  }\n\n  standard = async (reqCtx: ReqCtx): Promise<StandardOutput> => {\n    const getSigningKey = async (\n      did: string,\n      forceRefresh: boolean,\n    ): Promise<string> => {\n      const atprotoData = await this.idResolver.did.resolveAtprotoData(\n        did,\n        forceRefresh,\n      )\n      return atprotoData.signingKey\n    }\n\n    const jwtStr = getJwtStrFromReq(reqCtx.req)\n    if (!jwtStr) {\n      throw new AuthRequiredError('missing jwt', 'MissingJwt')\n    }\n    const nsid = parseReqNsid(reqCtx.req)\n    const payload = await verifyJwt(\n      jwtStr,\n      this.serviceDid,\n      nsid,\n      getSigningKey,\n    )\n    const iss = payload.iss\n\n    const member = await this.teamService.getMember(iss)\n\n    if (member?.disabled) {\n      throw new AuthRequiredError('member is disabled', 'MemberDisabled')\n    }\n\n    const { isAdmin, isModerator, isTriage, isVerifier } =\n      this.teamService.getMemberRole(member)\n\n    return {\n      credentials: {\n        type: 'standard',\n        iss,\n        aud: payload.aud,\n        isAdmin,\n        isModerator,\n        isTriage,\n        isVerifier,\n      },\n    }\n  }\n\n  standardOptional = async (\n    reqCtx: ReqCtx,\n  ): Promise<StandardOutput | NullOutput> => {\n    if (isBearerToken(reqCtx.req)) {\n      return this.standard(reqCtx)\n    }\n    return this.nullCreds()\n  }\n\n  standardOptionalOrAdminToken = async (\n    reqCtx: ReqCtx,\n  ): Promise<StandardOutput | AdminTokenOutput | NullOutput> => {\n    if (isBearerToken(reqCtx.req)) {\n      return this.standard(reqCtx)\n    } else if (isBasicToken(reqCtx.req)) {\n      return this.adminToken(reqCtx)\n    } else {\n      return this.nullCreds()\n    }\n  }\n\n  adminToken = async (reqCtx: ReqCtx): Promise<AdminTokenOutput> => {\n    const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '')\n    const { username, password } = parsed ?? {}\n    if (username !== 'admin' || password !== this.adminPassword) {\n      throw new AuthRequiredError()\n    }\n    return {\n      credentials: {\n        type: 'admin_token',\n        isAdmin: true,\n        isModerator: true,\n        isTriage: true,\n        isVerifier: true,\n      },\n    }\n  }\n\n  nullCreds(): NullOutput {\n    return {\n      credentials: {\n        type: 'none',\n        iss: null,\n      },\n    }\n  }\n}\n\nconst BEARER = 'Bearer '\nconst BASIC = 'Basic '\n\nconst isBearerToken = (req: express.Request): boolean => {\n  return req.headers.authorization?.startsWith(BEARER) ?? false\n}\n\nconst isBasicToken = (req: express.Request): boolean => {\n  return req.headers.authorization?.startsWith(BASIC) ?? false\n}\n\nexport const getJwtStrFromReq = (req: express.Request): string | null => {\n  const { authorization } = req.headers\n  if (!authorization?.startsWith(BEARER)) {\n    return null\n  }\n  return authorization.slice(BEARER.length).trim()\n}\n\nexport const parseBasicAuth = (\n  token: string,\n): { username: string; password: string } | null => {\n  if (!token.startsWith(BASIC)) return null\n  const b64 = token.slice(BASIC.length)\n  let parsed: string[]\n  try {\n    parsed = ui8.toString(ui8.fromString(b64, 'base64pad'), 'utf8').split(':')\n  } catch (err) {\n    return null\n  }\n  const [username, password] = parsed\n  if (!username || !password) return null\n  return { username, password }\n}\n"
  },
  {
    "path": "packages/ozone/src/background.ts",
    "content": "import PQueue from 'p-queue'\nimport { Database } from './db'\nimport { dbLogger } from './logger'\nimport { boundAbortController, isCausedBySignal, startInterval } from './util'\n\ntype Task = (db: Database, signal: AbortSignal) => Promise<void>\n\n/**\n * A simple queue for in-process, out-of-band/backgrounded work\n */\nexport class BackgroundQueue {\n  private abortController = new AbortController()\n  private queue: PQueue\n\n  public get signal() {\n    return this.abortController.signal\n  }\n\n  public get destroyed() {\n    return this.signal.aborted\n  }\n\n  constructor(\n    protected db: Database,\n    queueOpts?: { concurrency?: number },\n  ) {\n    this.queue = new PQueue(queueOpts ?? { concurrency: 20 })\n  }\n\n  getStats() {\n    return {\n      runningCount: this.queue.pending,\n      waitingCount: this.queue.size,\n    }\n  }\n\n  /**\n   * Add a task that will be executed at some point in the future.\n   *\n   * The task will be executed even if the backgroundQueue is destroyed, unless\n   * the provided `signal` is aborted.\n   *\n   * The `signal` provided to the task will be aborted whenever either the\n   * backgroundQueue is destroyed or the provided `signal` is aborted.\n   */\n  async add(task: Task, signal?: AbortSignal): Promise<void> {\n    if (this.destroyed) {\n      return\n    }\n\n    const abortController = boundAbortController(this.signal, signal)\n\n    return this.queue.add<void>(async () => {\n      try {\n        // Do not run the task if the signal provided to the task has become\n        // aborted. Do not use `abortController.signal` here since we do not\n        // want to abort the task if the backgroundQueue is being destroyed.\n        if (signal?.aborted) return\n\n        // The task will receive a \"combined signal\" allowing it to abort if\n        // either the backgroundQueue is destroyed or the provided signal is\n        // aborted.\n        await task(this.db, abortController.signal)\n      } catch (err) {\n        if (!isCausedBySignal(err, abortController.signal)) {\n          dbLogger.error({ err }, 'background queue task failed')\n        }\n      } finally {\n        abortController.abort()\n      }\n    })\n  }\n\n  async processAll() {\n    await this.queue.onIdle()\n  }\n\n  /**\n   * On destroy we stop accepting new tasks, but complete all\n   * pending/in-progress tasks. Tasks can decide to abort their current\n   * operation based on the signal they received. The application calls this\n   * only once http connections have drained (tasks no longer being added).\n   */\n  async destroy() {\n    this.abortController.abort()\n    await this.queue.onIdle()\n  }\n}\n\n/**\n * A simple periodic background task runner. This class will schedule a task to\n * run through a provided {@link BackgroundQueue} at a fixed interval. The task\n * will never run more than once concurrently, and will wait at least `interval`\n * milliseconds between the end of one run and the start of the next.\n */\nexport class PeriodicBackgroundTask {\n  private abortController: AbortController\n\n  private intervalPromise?: Promise<void>\n  private runningPromise?: Promise<void>\n\n  public get signal() {\n    return this.abortController.signal\n  }\n\n  public get destroyed() {\n    return this.signal.aborted\n  }\n\n  constructor(\n    protected backgroundQueue: BackgroundQueue,\n    protected interval: number,\n    protected task: Task,\n  ) {\n    if (!Number.isFinite(interval) || interval <= 0) {\n      throw new TypeError('interval must be a positive number')\n    }\n\n    // Bind this class's signal to the backgroundQueue's signal (destroying this\n    // instance if the backgroundQueue is destroyed)\n    this.abortController = boundAbortController(backgroundQueue.signal)\n  }\n\n  public run(signal?: AbortSignal): Promise<void> {\n    // `startInterval` already ensures that only one run is in progress at a\n    // time. However, we want to be able to expose a `run()` method that can be\n    // used to force a run, which could cause concurrent executions. We prevent\n    // this using the `runningPromise` property.\n\n    if (this.runningPromise) return this.runningPromise\n\n    // Combine the `this.signal` with the provided `signal`, if any.\n    const abortController = boundAbortController(this.signal, signal)\n\n    const promise = this.backgroundQueue.add(this.task, abortController.signal)\n\n    return (this.runningPromise = promise).finally(() => {\n      if (this.runningPromise === promise) this.runningPromise = undefined\n\n      // Cleanup the listeners added by `boundAbortController`\n      abortController.abort()\n    })\n  }\n\n  public start() {\n    // Noop if already started. Throws if this.signal is aborted (instance is\n    // destroyed).\n    this.intervalPromise ||= startInterval(\n      async (signal) => this.run(signal),\n      this.interval,\n      this.signal,\n    )\n  }\n\n  public async destroy() {\n    // @NOTE This instance does not \"own\" the backgroundQueue, so we do not\n    // destroy it here.\n\n    this.abortController.abort()\n\n    await this.intervalPromise\n    this.intervalPromise = undefined\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/communication-service/template.ts",
    "content": "import { Selectable } from 'kysely'\nimport { Database } from '../db'\nimport { CommunicationTemplate } from '../db/schema/communication_template'\nimport { TemplateView } from '../lexicon/types/tools/ozone/communication/defs'\n\nexport type CommunicationTemplateServiceCreator = (\n  db: Database,\n) => CommunicationTemplateService\n\nexport class CommunicationTemplateService {\n  constructor(public db: Database) {}\n\n  static creator() {\n    return (db: Database) => new CommunicationTemplateService(db)\n  }\n\n  async list(): Promise<Selectable<CommunicationTemplate>[]> {\n    const list = await this.db.db\n      .selectFrom('communication_template')\n      .selectAll()\n      .execute()\n\n    return list\n  }\n\n  async create({\n    name,\n    contentMarkdown,\n    subject,\n    lang,\n    disabled,\n    updatedAt,\n    createdAt,\n    lastUpdatedBy,\n  }: Omit<\n    Selectable<CommunicationTemplate>,\n    'id' | 'createdAt' | 'updatedAt'\n  > & {\n    createdAt?: Date\n    updatedAt?: Date\n  }): Promise<Selectable<CommunicationTemplate>> {\n    const newTemplate = await this.db.db\n      .insertInto('communication_template')\n      .values({\n        name,\n        contentMarkdown,\n        subject,\n        lang,\n        disabled,\n        lastUpdatedBy,\n        updatedAt: updatedAt || new Date(),\n        createdAt: createdAt || new Date(),\n      })\n      .returningAll()\n      .executeTakeFirstOrThrow()\n\n    return newTemplate\n  }\n\n  async update(\n    id: number,\n    {\n      name,\n      contentMarkdown,\n      subject,\n      disabled,\n      lang,\n      updatedAt,\n      lastUpdatedBy,\n    }: Partial<Omit<Selectable<CommunicationTemplate>, 'id' | 'createdAt'>>,\n  ): Promise<Selectable<CommunicationTemplate>> {\n    const updatedTemplate = await this.db.db\n      .updateTable('communication_template')\n      .where('id', '=', id)\n      .set({\n        name,\n        contentMarkdown,\n        subject,\n        lang,\n        disabled,\n        lastUpdatedBy,\n        updatedAt: updatedAt || new Date(),\n      })\n      .returningAll()\n      .executeTakeFirstOrThrow()\n\n    return updatedTemplate\n  }\n\n  async delete(id: number): Promise<void> {\n    await this.db.db\n      .deleteFrom('communication_template')\n      .where('id', '=', id)\n      .execute()\n  }\n\n  view(template: Selectable<CommunicationTemplate>): TemplateView {\n    return {\n      id: `${template.id}`,\n      name: template.name,\n      contentMarkdown: template.contentMarkdown,\n      disabled: template.disabled,\n      lang: template.lang || undefined,\n      subject: template.subject || undefined,\n      createdAt: template.createdAt.toISOString(),\n      updatedAt: template.updatedAt.toISOString(),\n      lastUpdatedBy: template.lastUpdatedBy,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/communication-service/util.ts",
    "content": "// Postgresql will throw a specific error code with the constraint when trying to create a template with duplicate name\n// see https://www.postgresql.org/docs/current/errcodes-appendix.html\nexport const isDuplicateTemplateNameError = (err: any) => {\n  return (\n    err?.['code'] === '23505' &&\n    err?.['constraint'] === 'communication_template_unique_name'\n  )\n}\n"
  },
  {
    "path": "packages/ozone/src/config/config.ts",
    "content": "import assert from 'node:assert'\nimport { DAY, HOUR, MINUTE } from '@atproto/common'\nimport { OzoneEnvironment } from './env'\n\n// off-config but still from env:\n// logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION\n\nexport const envToCfg = (env: OzoneEnvironment): OzoneConfig => {\n  const port = env.port ?? 3000\n  assert(env.publicUrl, 'publicUrl is required')\n  assert(env.serverDid, 'serverDid is required')\n  const serviceCfg: OzoneConfig['service'] = {\n    port,\n    publicUrl: env.publicUrl,\n    did: env.serverDid,\n    version: env.version,\n    devMode: env.devMode,\n    serviceRecordCacheTTL: env.serviceRecordCacheTTL ?? 5 * MINUTE, // default 5 mins\n  }\n\n  assert(env.dbPostgresUrl, 'dbPostgresUrl is required')\n  const dbCfg: OzoneConfig['db'] = {\n    postgresUrl: env.dbPostgresUrl,\n    postgresSchema: env.dbPostgresSchema,\n    poolSize: env.dbPoolSize,\n    poolMaxUses: env.dbPoolMaxUses,\n    poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs,\n    materializedViewRefreshIntervalMs: env.dbMaterializedViewRefreshIntervalMs,\n    teamProfileRefreshIntervalMs: env.dbTeamProfileRefreshIntervalMs,\n  }\n\n  assert(env.appviewUrl, 'appviewUrl is required')\n  assert(env.appviewDid, 'appviewDid is required')\n  const appviewCfg: OzoneConfig['appview'] = {\n    url: env.appviewUrl,\n    did: env.appviewDid,\n    pushEvents: !!env.appviewPushEvents,\n  }\n\n  let pdsCfg: OzoneConfig['pds'] = null\n  if (env.pdsUrl || env.pdsDid) {\n    assert(env.pdsUrl, 'pdsUrl is required')\n    assert(env.pdsDid, 'pdsDid is required')\n    pdsCfg = {\n      url: env.pdsUrl,\n      did: env.pdsDid,\n    }\n  }\n\n  let chatCfg: OzoneConfig['chat'] = null\n  if (env.chatUrl || env.chatDid) {\n    assert(env.chatUrl, 'chatUrl is required when chatDid is provided')\n    assert(env.chatDid, 'chatDid is required when chatUrl is provided')\n    chatCfg = {\n      url: env.chatUrl,\n      did: env.chatDid,\n    }\n  }\n\n  const cdnCfg: OzoneConfig['cdn'] = {\n    paths: env.cdnPaths,\n  }\n\n  assert(env.didPlcUrl, 'didPlcUrl is required')\n  const identityCfg: OzoneConfig['identity'] = {\n    plcUrl: env.didPlcUrl,\n    cacheMaxTTL: env.didCacheMaxTTL ?? DAY,\n    cacheStaleTTL: env.didCacheStaleTTL ?? HOUR,\n  }\n\n  const blobDivertServiceCfg =\n    env.blobDivertUrl && env.blobDivertAdminPassword\n      ? {\n          url: env.blobDivertUrl,\n          adminPassword: env.blobDivertAdminPassword,\n        }\n      : null\n  const accessCfg: OzoneConfig['access'] = {\n    admins: env.adminDids,\n    moderators: env.moderatorDids,\n    triage: env.triageDids,\n  }\n  const verifierCfg: OzoneConfig['verifier'] =\n    env.verifierUrl && env.verifierDid && env.verifierPassword\n      ? {\n          url: env.verifierUrl,\n          did: env.verifierDid,\n          password: env.verifierPassword,\n          issuersToIndex: env.verifierIssuersToIndex,\n        }\n      : null\n\n  return {\n    service: serviceCfg,\n    db: dbCfg,\n    appview: appviewCfg,\n    pds: pdsCfg,\n    chat: chatCfg,\n    cdn: cdnCfg,\n    identity: identityCfg,\n    blobDivert: blobDivertServiceCfg,\n    access: accessCfg,\n    verifier: verifierCfg,\n    jetstreamUrl: env.jetstreamUrl,\n  }\n}\n\nexport type OzoneConfig = {\n  service: ServiceConfig\n  db: DatabaseConfig\n  appview: AppviewConfig\n  pds: PdsConfig | null\n  chat: ChatConfig | null\n  cdn: CdnConfig\n  identity: IdentityConfig\n  blobDivert: BlobDivertConfig | null\n  access: AccessConfig\n  jetstreamUrl?: string\n  verifier: VerifierConfig | null\n}\n\nexport type ServiceConfig = {\n  port: number\n  publicUrl: string\n  did: string\n  version?: string\n  devMode?: boolean\n  serviceRecordCacheTTL: number // in ms, default 5 mins\n}\n\nexport type BlobDivertConfig = {\n  url: string\n  adminPassword: string\n}\n\nexport type DatabaseConfig = {\n  postgresUrl: string\n  postgresSchema?: string\n  poolSize?: number\n  poolMaxUses?: number\n  poolIdleTimeoutMs?: number\n  materializedViewRefreshIntervalMs?: number\n  teamProfileRefreshIntervalMs?: number\n}\n\nexport type AppviewConfig = {\n  url: string\n  did: string\n  pushEvents: boolean\n}\n\nexport type PdsConfig = {\n  url: string\n  did: string\n}\n\nexport type ChatConfig = {\n  url: string\n  did: string\n}\n\nexport type CdnConfig = {\n  paths?: string[]\n}\n\nexport type IdentityConfig = {\n  plcUrl: string\n  cacheStaleTTL: number\n  cacheMaxTTL: number\n}\n\nexport type AccessConfig = {\n  admins: string[]\n  moderators: string[]\n  triage: string[]\n}\n\nexport type VerifierConfig = {\n  url: string\n  did: string\n  password: string\n  jetstreamUrl?: string\n  issuersToIndex?: string[]\n}\n"
  },
  {
    "path": "packages/ozone/src/config/env.ts",
    "content": "import { envBool, envInt, envList, envStr } from '@atproto/common'\n\nexport const readEnv = (): OzoneEnvironment => {\n  return {\n    nodeEnv: envStr('NODE_ENV'),\n    devMode: envBool('OZONE_DEV_MODE'),\n    version: envStr('OZONE_VERSION'),\n    port: envInt('OZONE_PORT'),\n    publicUrl: envStr('OZONE_PUBLIC_URL'),\n    serverDid: envStr('OZONE_SERVER_DID'),\n    serviceRecordCacheTTL: envInt('OZONE_SERVICE_RECORD_CACHE_TTL'),\n    appviewUrl: envStr('OZONE_APPVIEW_URL'),\n    appviewDid: envStr('OZONE_APPVIEW_DID'),\n    appviewPushEvents: envBool('OZONE_APPVIEW_PUSH_EVENTS'),\n    pdsUrl: envStr('OZONE_PDS_URL'),\n    pdsDid: envStr('OZONE_PDS_DID'),\n    chatUrl: envStr('OZONE_CHAT_URL'),\n    chatDid: envStr('OZONE_CHAT_DID'),\n    dbPostgresUrl: envStr('OZONE_DB_POSTGRES_URL'),\n    dbPostgresSchema: envStr('OZONE_DB_POSTGRES_SCHEMA'),\n    dbPoolSize: envInt('OZONE_DB_POOL_SIZE'),\n    dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'),\n    dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'),\n    dbMaterializedViewRefreshIntervalMs: envInt(\n      'OZONE_DB_MATERIALIZED_VIEW_REFRESH_INTERVAL_MS',\n    ),\n    dbTeamProfileRefreshIntervalMs: envInt(\n      'OZONE_DB_TEAM_PROFILE_REFRESH_INTERVAL_MS',\n    ),\n    didPlcUrl: envStr('OZONE_DID_PLC_URL'),\n    didCacheStaleTTL: envInt('OZONE_DID_CACHE_STALE_TTL'),\n    didCacheMaxTTL: envInt('OZONE_DID_CACHE_MAX_TTL'),\n    cdnPaths: envList('OZONE_CDN_PATHS'),\n    adminDids: envList('OZONE_ADMIN_DIDS'),\n    moderatorDids: envList('OZONE_MODERATOR_DIDS'),\n    triageDids: envList('OZONE_TRIAGE_DIDS'),\n    adminPassword: envStr('OZONE_ADMIN_PASSWORD'),\n    signingKeyHex: envStr('OZONE_SIGNING_KEY_HEX'),\n    blobDivertUrl: envStr('OZONE_BLOB_DIVERT_URL'),\n    blobDivertAdminPassword: envStr('OZONE_BLOB_DIVERT_ADMIN_PASSWORD'),\n    verifierUrl: envStr('OZONE_VERIFIER_URL'),\n    verifierDid: envStr('OZONE_VERIFIER_DID'),\n    verifierPassword: envStr('OZONE_VERIFIER_PASSWORD'),\n    verifierIssuersToIndex: envList('OZONE_VERIFIER_ISSUERS_TO_INDEX'),\n    jetstreamUrl: envStr('OZONE_JETSTREAM_URL'),\n  }\n}\n\nexport type OzoneEnvironment = {\n  nodeEnv?: string\n  devMode?: boolean\n  version?: string\n  port?: number\n  publicUrl?: string\n  serverDid?: string\n  serviceRecordCacheTTL?: number\n  appviewUrl?: string\n  appviewDid?: string\n  appviewPushEvents?: boolean\n  pdsUrl?: string\n  pdsDid?: string\n  chatUrl?: string\n  chatDid?: string\n  dbPostgresUrl?: string\n  dbPostgresSchema?: string\n  dbPoolSize?: number\n  dbPoolMaxUses?: number\n  dbPoolIdleTimeoutMs?: number\n  dbMaterializedViewRefreshIntervalMs?: number\n  dbTeamProfileRefreshIntervalMs?: number\n  didPlcUrl?: string\n  didCacheStaleTTL?: number\n  didCacheMaxTTL?: number\n  cdnPaths?: string[]\n  adminDids: string[]\n  moderatorDids: string[]\n  triageDids: string[]\n  adminPassword?: string\n  signingKeyHex?: string\n  blobDivertUrl?: string\n  blobDivertAdminPassword?: string\n  verifierUrl?: string\n  verifierDid?: string\n  verifierPassword?: string\n  verifierIssuersToIndex?: string[]\n  jetstreamUrl?: string\n}\n"
  },
  {
    "path": "packages/ozone/src/config/index.ts",
    "content": "export * from './config'\nexport * from './env'\nexport * from './secrets'\n"
  },
  {
    "path": "packages/ozone/src/config/secrets.ts",
    "content": "import assert from 'node:assert'\nimport { OzoneEnvironment } from './env'\n\nexport const envToSecrets = (env: OzoneEnvironment): OzoneSecrets => {\n  assert(env.adminPassword)\n  assert(env.signingKeyHex)\n\n  return {\n    adminPassword: env.adminPassword,\n    signingKeyHex: env.signingKeyHex,\n  }\n}\n\nexport type OzoneSecrets = {\n  adminPassword: string\n  signingKeyHex: string\n}\n"
  },
  {
    "path": "packages/ozone/src/context.ts",
    "content": "import assert from 'node:assert'\nimport * as plc from '@did-plc/lib'\nimport express from 'express'\nimport { AtpAgent } from '@atproto/api'\nimport { Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport { DidCache, IdResolver, MemoryCache } from '@atproto/identity'\nimport { createServiceAuthHeaders } from '@atproto/xrpc-server'\nimport { AuthVerifier } from './auth-verifier'\nimport { BackgroundQueue } from './background'\nimport {\n  CommunicationTemplateService,\n  CommunicationTemplateServiceCreator,\n} from './communication-service/template'\nimport { OzoneConfig, OzoneSecrets } from './config'\nimport { EventPusher } from './daemon'\nimport { BlobDiverter } from './daemon/blob-diverter'\nimport { Database } from './db'\nimport { ImageInvalidator } from './image-invalidator'\nimport { ModerationService, ModerationServiceCreator } from './mod-service'\nimport {\n  ModerationServiceProfile,\n  ModerationServiceProfileCreator,\n} from './mod-service/profile'\nimport { StrikeService, StrikeServiceCreator } from './mod-service/strike'\nimport {\n  SafelinkRuleService,\n  SafelinkRuleServiceCreator,\n} from './safelink/service'\nimport {\n  ScheduledActionService,\n  ScheduledActionServiceCreator,\n} from './scheduled-action/service'\nimport { Sequencer } from './sequencer/sequencer'\nimport { SetService, SetServiceCreator } from './set/service'\nimport { SettingService, SettingServiceCreator } from './setting/service'\nimport { TeamService, TeamServiceCreator } from './team'\nimport {\n  LABELER_HEADER_NAME,\n  ParsedLabelers,\n  defaultLabelerHeader,\n  getSigningKeyId,\n  parseLabelerHeader,\n} from './util'\nimport {\n  VerificationIssuer,\n  VerificationIssuerCreator,\n} from './verification/issuer'\nimport {\n  VerificationService,\n  VerificationServiceCreator,\n} from './verification/service'\n\nexport type AppContextOptions = {\n  db: Database\n  cfg: OzoneConfig\n  modService: ModerationServiceCreator\n  moderationServiceProfile: ModerationServiceProfileCreator\n  communicationTemplateService: CommunicationTemplateServiceCreator\n  safelinkRuleService: SafelinkRuleServiceCreator\n  scheduledActionService: ScheduledActionServiceCreator\n  setService: SetServiceCreator\n  settingService: SettingServiceCreator\n  strikeService: StrikeServiceCreator\n  teamService: TeamServiceCreator\n  appviewAgent: AtpAgent\n  pdsAgent: AtpAgent | undefined\n  chatAgent: AtpAgent | undefined\n  blobDiverter?: BlobDiverter\n  signingKey: Keypair\n  signingKeyId: number\n  didCache: DidCache\n  idResolver: IdResolver\n  imgInvalidator?: ImageInvalidator\n  backgroundQueue: BackgroundQueue\n  sequencer: Sequencer\n  authVerifier: AuthVerifier\n  verificationService: VerificationServiceCreator\n  verificationIssuer: VerificationIssuerCreator\n}\n\nexport class AppContext {\n  constructor(\n    private opts: AppContextOptions,\n    private secrets: OzoneSecrets,\n  ) {}\n\n  static async fromConfig(\n    cfg: OzoneConfig,\n    secrets: OzoneSecrets,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<AppContext> {\n    const db = new Database({\n      url: cfg.db.postgresUrl,\n      schema: cfg.db.postgresSchema,\n      poolSize: cfg.db.poolSize,\n      poolMaxUses: cfg.db.poolMaxUses,\n      poolIdleTimeoutMs: cfg.db.poolIdleTimeoutMs,\n    })\n    const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex)\n    const signingKeyId = await getSigningKeyId(db, signingKey.did())\n    const appviewAgent = new AtpAgent({ service: cfg.appview.url })\n    const pdsAgent = cfg.pds\n      ? new AtpAgent({ service: cfg.pds.url })\n      : undefined\n    const chatAgent = cfg.chat\n      ? new AtpAgent({ service: cfg.chat.url })\n      : undefined\n\n    const didCache = new MemoryCache(\n      cfg.identity.cacheStaleTTL,\n      cfg.identity.cacheMaxTTL,\n    )\n    const idResolver = new IdResolver({\n      plcUrl: cfg.identity.plcUrl,\n      didCache,\n    })\n\n    const createAuthHeaders = (aud: string, lxm: string) =>\n      createServiceAuthHeaders({\n        iss: `${cfg.service.did}#atproto_labeler`,\n        aud,\n        lxm,\n        keypair: signingKey,\n      })\n\n    const backgroundQueue = new BackgroundQueue(db)\n    const blobDiverter = cfg.blobDivert\n      ? new BlobDiverter(db, {\n          idResolver,\n          serviceConfig: cfg.blobDivert,\n        })\n      : undefined\n    const eventPusher = new EventPusher(db, createAuthHeaders, {\n      appview: cfg.appview.pushEvents ? cfg.appview : undefined,\n      pds: cfg.pds ?? undefined,\n    })\n\n    const communicationTemplateService = CommunicationTemplateService.creator()\n    const safelinkRuleService = SafelinkRuleService.creator()\n    const scheduledActionService = ScheduledActionService.creator()\n    const teamService = TeamService.creator(\n      appviewAgent,\n      cfg.appview.did,\n      createAuthHeaders,\n    )\n    const setService = SetService.creator()\n    const settingService = SettingService.creator()\n    const strikeService = StrikeService.creator()\n    const verificationService = VerificationService.creator()\n    const verificationIssuer = VerificationIssuer.creator()\n    const moderationServiceProfile = ModerationServiceProfile.creator(\n      cfg,\n      appviewAgent,\n    )\n    const modService = ModerationService.creator(\n      signingKey,\n      signingKeyId,\n      cfg,\n      backgroundQueue,\n      idResolver,\n      eventPusher,\n      appviewAgent,\n      createAuthHeaders,\n      strikeService,\n      overrides?.imgInvalidator,\n    )\n\n    const sequencer = new Sequencer(modService(db))\n\n    const authVerifier = new AuthVerifier(idResolver, {\n      serviceDid: cfg.service.did,\n      adminPassword: secrets.adminPassword,\n      teamService: teamService(db),\n    })\n\n    return new AppContext(\n      {\n        db,\n        cfg,\n        modService,\n        moderationServiceProfile,\n        communicationTemplateService,\n        safelinkRuleService,\n        scheduledActionService,\n        teamService,\n        setService,\n        settingService,\n        strikeService,\n        appviewAgent,\n        pdsAgent,\n        chatAgent,\n        signingKey,\n        signingKeyId,\n        didCache,\n        idResolver,\n        backgroundQueue,\n        sequencer,\n        authVerifier,\n        blobDiverter,\n        verificationService,\n        verificationIssuer,\n        ...(overrides ?? {}),\n      },\n      secrets,\n    )\n  }\n\n  assignPort(port: number) {\n    assert(\n      !this.cfg.service.port || this.cfg.service.port === port,\n      'Conflicting port in config',\n    )\n    this.opts.cfg.service.port = port\n  }\n\n  get db(): Database {\n    return this.opts.db\n  }\n\n  get cfg(): OzoneConfig {\n    return this.opts.cfg\n  }\n\n  get modService(): ModerationServiceCreator {\n    return this.opts.modService\n  }\n\n  get blobDiverter(): BlobDiverter | undefined {\n    return this.opts.blobDiverter\n  }\n\n  get communicationTemplateService(): CommunicationTemplateServiceCreator {\n    return this.opts.communicationTemplateService\n  }\n\n  get safelinkRuleService(): SafelinkRuleServiceCreator {\n    return this.opts.safelinkRuleService\n  }\n\n  get scheduledActionService(): ScheduledActionServiceCreator {\n    return this.opts.scheduledActionService\n  }\n\n  get teamService(): TeamServiceCreator {\n    return this.opts.teamService\n  }\n\n  get setService(): SetServiceCreator {\n    return this.opts.setService\n  }\n\n  get settingService(): SettingServiceCreator {\n    return this.opts.settingService\n  }\n\n  get strikeService(): StrikeServiceCreator {\n    return this.opts.strikeService\n  }\n\n  get verificationService(): VerificationServiceCreator {\n    return this.opts.verificationService\n  }\n\n  get verificationIssuer(): VerificationIssuerCreator {\n    return this.opts.verificationIssuer\n  }\n\n  get moderationServiceProfile(): ModerationServiceProfileCreator {\n    return this.opts.moderationServiceProfile\n  }\n\n  get appviewAgent(): AtpAgent {\n    return this.opts.appviewAgent\n  }\n\n  get pdsAgent(): AtpAgent | undefined {\n    return this.opts.pdsAgent\n  }\n\n  get chatAgent(): AtpAgent | undefined {\n    return this.opts.chatAgent\n  }\n\n  get signingKey(): Keypair {\n    return this.opts.signingKey\n  }\n\n  get signingKeyId(): number {\n    return this.opts.signingKeyId\n  }\n\n  get plcClient(): plc.Client {\n    return new plc.Client(this.cfg.identity.plcUrl)\n  }\n\n  get didCache(): DidCache {\n    return this.opts.didCache\n  }\n\n  get idResolver(): IdResolver {\n    return this.opts.idResolver\n  }\n\n  get backgroundQueue(): BackgroundQueue {\n    return this.opts.backgroundQueue\n  }\n\n  get sequencer(): Sequencer {\n    return this.opts.sequencer\n  }\n\n  get authVerifier(): AuthVerifier {\n    return this.opts.authVerifier\n  }\n\n  async serviceAuthHeaders(aud: string, lxm: string) {\n    const iss = `${this.cfg.service.did}#atproto_labeler`\n    return createServiceAuthHeaders({\n      iss,\n      aud,\n      lxm,\n      keypair: this.signingKey,\n    })\n  }\n\n  async pdsAuth(lxm: string) {\n    if (!this.cfg.pds) {\n      return undefined\n    }\n    return this.serviceAuthHeaders(this.cfg.pds.did, lxm)\n  }\n\n  async appviewAuth(lxm: string) {\n    return this.serviceAuthHeaders(this.cfg.appview.did, lxm)\n  }\n\n  async chatAuth(lxm: string) {\n    if (!this.cfg.chat) {\n      throw new Error('No chat service configured')\n    }\n    return this.serviceAuthHeaders(this.cfg.chat.did, lxm)\n  }\n\n  devOverride(overrides: Partial<AppContextOptions>) {\n    this.opts = {\n      ...this.opts,\n      ...overrides,\n    }\n  }\n\n  reqLabelers(req: express.Request): ParsedLabelers {\n    const val = req.header(LABELER_HEADER_NAME)\n    let parsed: ParsedLabelers | null\n    try {\n      parsed = parseLabelerHeader(val, this.cfg.service.did)\n    } catch (err) {\n      parsed = null\n    }\n    if (!parsed) return defaultLabelerHeader([])\n    return parsed\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/blob-diverter.ts",
    "content": "import { Readable } from 'node:stream'\nimport { finished, pipeline } from 'node:stream/promises'\nimport { CID } from 'multiformats/cid'\nimport * as undici from 'undici'\nimport {\n  VerifyCidTransform,\n  allFulfilled,\n  createDecoders,\n  getPdsEndpoint,\n} from '@atproto/common'\nimport { IdResolver } from '@atproto/identity'\nimport { ResponseType, XRPCError } from '@atproto/xrpc'\nimport { BlobDivertConfig } from '../config'\nimport { Database } from '../db'\nimport { retryHttp } from '../util'\n\nexport class BlobDiverter {\n  serviceConfig: BlobDivertConfig\n  idResolver: IdResolver\n\n  constructor(\n    public db: Database,\n    services: {\n      idResolver: IdResolver\n      serviceConfig: BlobDivertConfig\n    },\n  ) {\n    this.serviceConfig = services.serviceConfig\n    this.idResolver = services.idResolver\n  }\n\n  /**\n   * @throws {XRPCError} so that retryHttp can handle retries\n   */\n  async getBlob(options: GetBlobOptions): Promise<Blob> {\n    const blobUrl = getBlobUrl(options)\n\n    const blobResponse = await undici\n      .request(blobUrl, {\n        headersTimeout: 10e3,\n        bodyTimeout: 30e3,\n      })\n      .catch((err) => {\n        throw asXrpcClientError(err, `Error fetching blob ${options.cid}`)\n      })\n\n    if (blobResponse.statusCode !== 200) {\n      await blobResponse.body.dump()\n      throw new XRPCError(\n        blobResponse.statusCode,\n        undefined,\n        `Error downloading blob ${options.cid}`,\n      )\n    }\n\n    try {\n      const type = blobResponse.headers['content-type']\n      const encoding = blobResponse.headers['content-encoding']\n\n      const verifier = new VerifyCidTransform(CID.parse(options.cid))\n\n      void pipeline([\n        blobResponse.body,\n        ...createDecoders(encoding),\n        verifier,\n      ]).catch((_err) => {})\n\n      return {\n        type: typeof type === 'string' ? type : 'application/octet-stream',\n        stream: verifier,\n      }\n    } catch (err) {\n      // Typically un-supported content encoding\n      await blobResponse.body.dump()\n      throw err\n    }\n  }\n\n  /**\n   * @throws {XRPCError} so that retryHttp can handle retries\n   */\n  async uploadBlob(blob: Blob, report: ReportBlobOptions) {\n    const uploadUrl = reportBlobUrl(this.serviceConfig.url, report)\n\n    const result = await undici\n      .request(uploadUrl, {\n        method: 'POST',\n        body: blob.stream,\n        headersTimeout: 30e3,\n        bodyTimeout: 10e3,\n        headers: {\n          Authorization: basicAuth('admin', this.serviceConfig.adminPassword),\n          'content-type': blob.type,\n        },\n      })\n      .catch((err) => {\n        throw asXrpcClientError(err, `Error uploading blob ${report.did}`)\n      })\n\n    if (result.statusCode !== 200) {\n      await result.body.dump()\n      throw new XRPCError(\n        result.statusCode,\n        undefined,\n        `Error uploading blob ${report.did}`,\n      )\n    }\n\n    await finished(result.body.resume())\n  }\n\n  async uploadBlobOnService({\n    subjectDid: did,\n    subjectUri: uri,\n    subjectBlobCids,\n  }: {\n    subjectDid: string\n    subjectUri: string | null\n    subjectBlobCids: string[]\n  }): Promise<void> {\n    const didDoc = await this.idResolver.did.resolve(did)\n    if (!didDoc) throw new Error('Error resolving DID')\n\n    const pds = getPdsEndpoint(didDoc)\n    if (!pds) throw new Error('Error resolving PDS')\n\n    await allFulfilled(\n      subjectBlobCids.map((cid) =>\n        retryHttp(async () => {\n          // attempt to download and upload within the same retry block since\n          // the blob stream is not reusable\n          const blob = await this.getBlob({ pds, cid, did })\n          return this.uploadBlob(blob, { did, uri })\n        }),\n      ),\n    ).catch((err) => {\n      throw new XRPCError(\n        ResponseType.UpstreamFailure,\n        undefined,\n        'Failed to process blobs',\n        undefined,\n        { cause: err },\n      )\n    })\n  }\n}\n\nconst basicAuth = (username: string, password: string) => {\n  return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')\n}\n\ntype Blob = {\n  type: string\n  stream: Readable\n}\n\ntype GetBlobOptions = {\n  pds: string\n  did: string\n  cid: string\n}\n\nfunction getBlobUrl({ pds, did, cid }: GetBlobOptions): URL {\n  const url = new URL(`/xrpc/com.atproto.sync.getBlob`, pds)\n  url.searchParams.set('did', did)\n  url.searchParams.set('cid', cid)\n  return url\n}\n\ntype ReportBlobOptions = {\n  did: string\n  uri: string | null\n}\n\nfunction reportBlobUrl(service: string, { did, uri }: ReportBlobOptions): URL {\n  const url = new URL(`/xrpc/com.atproto.unspecced.reportBlob`, service)\n  url.searchParams.set('did', did)\n  if (uri != null) url.searchParams.set('uri', uri)\n  return url\n}\n\nfunction asXrpcClientError(err: unknown, message: string) {\n  return new XRPCError(ResponseType.Unknown, undefined, message, undefined, {\n    cause: err,\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/context.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { allFulfilled } from '@atproto/common'\nimport { Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { createServiceAuthHeaders } from '@atproto/xrpc-server'\nimport { BackgroundQueue } from '../background'\nimport { OzoneConfig, OzoneSecrets } from '../config'\nimport { Database } from '../db'\nimport { ModerationService } from '../mod-service'\nimport { StrikeService } from '../mod-service/strike'\nimport { ScheduledActionService } from '../scheduled-action/service'\nimport { SettingService } from '../setting/service'\nimport { TeamService } from '../team'\nimport { getSigningKeyId } from '../util'\nimport { EventPusher } from './event-pusher'\nimport { EventReverser } from './event-reverser'\nimport { MaterializedViewRefresher } from './materialized-view-refresher'\nimport { ScheduledActionProcessor } from './scheduled-action-processor'\nimport { StrikeExpiryProcessor } from './strike-expiry-processor'\nimport { TeamProfileSynchronizer } from './team-profile-synchronizer'\nimport { VerificationListener } from './verification-listener'\n\nexport type DaemonContextOptions = {\n  db: Database\n  cfg: OzoneConfig\n  backgroundQueue: BackgroundQueue\n  signingKey: Keypair\n  eventPusher: EventPusher\n  eventReverser: EventReverser\n  materializedViewRefresher: MaterializedViewRefresher\n  teamProfileSynchronizer: TeamProfileSynchronizer\n  scheduledActionProcessor: ScheduledActionProcessor\n  strikeExpiryProcessor: StrikeExpiryProcessor\n  verificationListener?: VerificationListener\n}\n\nexport class DaemonContext {\n  constructor(private opts: DaemonContextOptions) {}\n\n  static async fromConfig(\n    cfg: OzoneConfig,\n    secrets: OzoneSecrets,\n    overrides?: Partial<DaemonContextOptions>,\n  ): Promise<DaemonContext> {\n    const db = new Database({\n      url: cfg.db.postgresUrl,\n      schema: cfg.db.postgresSchema,\n    })\n    const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex)\n    const signingKeyId = await getSigningKeyId(db, signingKey.did())\n\n    const idResolver = new IdResolver({\n      plcUrl: cfg.identity.plcUrl,\n    })\n\n    const appviewAgent = new AtpAgent({ service: cfg.appview.url })\n    const createAuthHeaders = (aud: string, lxm: string) =>\n      createServiceAuthHeaders({\n        iss: `${cfg.service.did}#atproto_labeler`,\n        aud,\n        lxm,\n        keypair: signingKey,\n      })\n\n    const eventPusher = new EventPusher(db, createAuthHeaders, {\n      appview: cfg.appview.pushEvents ? cfg.appview : undefined,\n      pds: cfg.pds ?? undefined,\n    })\n\n    const backgroundQueue = new BackgroundQueue(db)\n\n    const settingService = SettingService.creator()\n    const strikeService = StrikeService.creator()\n    const modService = ModerationService.creator(\n      signingKey,\n      signingKeyId,\n      cfg,\n      backgroundQueue,\n      idResolver,\n      eventPusher,\n      appviewAgent,\n      createAuthHeaders,\n      strikeService,\n    )\n    const scheduledActionService = ScheduledActionService.creator()\n    const teamService = TeamService.creator(\n      appviewAgent,\n      cfg.appview.did,\n      createAuthHeaders,\n    )\n    const teamProfileSynchronizer = new TeamProfileSynchronizer(\n      backgroundQueue,\n      teamService(db),\n      cfg.db.teamProfileRefreshIntervalMs,\n    )\n\n    const eventReverser = new EventReverser(db, modService)\n\n    const materializedViewRefresher = new MaterializedViewRefresher(\n      backgroundQueue,\n      cfg.db.materializedViewRefreshIntervalMs,\n    )\n\n    const scheduledActionProcessor = new ScheduledActionProcessor(\n      db,\n      cfg.service.did,\n      settingService,\n      modService,\n      scheduledActionService,\n    )\n\n    const strikeExpiryProcessor = new StrikeExpiryProcessor(db, strikeService)\n\n    // Only spawn the listener if verifier config exists and a jetstream URL is provided\n    const verificationListener =\n      cfg.verifier && cfg.jetstreamUrl\n        ? new VerificationListener(\n            db,\n            cfg.jetstreamUrl,\n            cfg.verifier?.issuersToIndex,\n          )\n        : undefined\n\n    return new DaemonContext({\n      db,\n      cfg,\n      backgroundQueue,\n      signingKey,\n      eventPusher,\n      eventReverser,\n      materializedViewRefresher,\n      teamProfileSynchronizer,\n      scheduledActionProcessor,\n      strikeExpiryProcessor,\n      verificationListener,\n      ...(overrides ?? {}),\n    })\n  }\n\n  get db(): Database {\n    return this.opts.db\n  }\n\n  get cfg(): OzoneConfig {\n    return this.opts.cfg\n  }\n\n  get backgroundQueue(): BackgroundQueue {\n    return this.opts.backgroundQueue\n  }\n\n  get eventPusher(): EventPusher {\n    return this.opts.eventPusher\n  }\n\n  get eventReverser(): EventReverser {\n    return this.opts.eventReverser\n  }\n\n  get materializedViewRefresher(): MaterializedViewRefresher {\n    return this.opts.materializedViewRefresher\n  }\n\n  get teamProfileSynchronizer(): TeamProfileSynchronizer {\n    return this.opts.teamProfileSynchronizer\n  }\n\n  get scheduledActionProcessor(): ScheduledActionProcessor {\n    return this.opts.scheduledActionProcessor\n  }\n\n  get strikeExpiryProcessor(): StrikeExpiryProcessor {\n    return this.opts.strikeExpiryProcessor\n  }\n\n  get verificationListener(): VerificationListener | undefined {\n    return this.opts.verificationListener\n  }\n\n  async start() {\n    this.eventPusher.start()\n    this.eventReverser.start()\n    this.materializedViewRefresher.start()\n    this.teamProfileSynchronizer.start()\n    this.scheduledActionProcessor.start()\n    this.strikeExpiryProcessor.start()\n    this.verificationListener?.start()\n  }\n\n  async processAll() {\n    // Sequential because the materialized view values depend on the events.\n    await this.eventPusher.processAll()\n    await this.materializedViewRefresher.run()\n    await this.teamProfileSynchronizer.run()\n  }\n\n  async destroy() {\n    try {\n      await allFulfilled([\n        this.eventReverser.destroy(),\n        this.eventPusher.destroy(),\n        this.materializedViewRefresher.destroy(),\n        this.teamProfileSynchronizer.destroy(),\n        this.scheduledActionProcessor.destroy(),\n        this.strikeExpiryProcessor.destroy(),\n        this.verificationListener?.stop(),\n      ])\n    } finally {\n      await this.backgroundQueue.destroy()\n      await this.db.close()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/event-pusher.ts",
    "content": "import assert from 'node:assert'\nimport { Insertable, Selectable } from 'kysely'\nimport { AtpAgent } from '@atproto/api'\nimport { SECOND } from '@atproto/common'\nimport { Database } from '../db'\nimport { BlobPushEvent } from '../db/schema/blob_push_event'\nimport { RepoPushEventType } from '../db/schema/repo_push_event'\nimport { ids } from '../lexicon/lexicons'\nimport { InputSchema } from '../lexicon/types/com/atproto/admin/updateSubjectStatus'\nimport { dbLogger } from '../logger'\nimport { retryHttp } from '../util'\n\ntype EventSubject = InputSchema['subject']\n\ntype PollState = {\n  timer?: NodeJS.Timeout\n  promise: Promise<void>\n}\n\ntype AuthHeaders = {\n  headers: {\n    authorization: string\n  }\n}\n\ntype Service = {\n  agent: AtpAgent\n  did: string\n}\n\nexport class EventPusher {\n  destroyed = false\n\n  repoPollState: PollState = {\n    promise: Promise.resolve(),\n  }\n  recordPollState: PollState = {\n    promise: Promise.resolve(),\n  }\n  blobPollState: PollState = {\n    promise: Promise.resolve(),\n  }\n\n  appview: Service | undefined\n  pds: Service | undefined\n\n  constructor(\n    public db: Database,\n    public createAuthHeaders: (\n      aud: string,\n      method: string,\n    ) => Promise<AuthHeaders>,\n    services: {\n      appview?: {\n        url: string\n        did: string\n      }\n      pds?: {\n        url: string\n        did: string\n      }\n    },\n  ) {\n    if (services.appview) {\n      this.appview = {\n        agent: new AtpAgent({ service: services.appview.url }),\n        did: services.appview.did,\n      }\n    }\n    if (services.pds) {\n      this.pds = {\n        agent: new AtpAgent({ service: services.pds.url }),\n        did: services.pds.did,\n      }\n    }\n  }\n\n  start() {\n    this.poll(this.repoPollState, () => this.pushRepoEvents())\n    this.poll(this.recordPollState, () => this.pushRecordEvents())\n    this.poll(this.blobPollState, () => this.pushBlobEvents())\n  }\n\n  // event pusher may be configured with both appview and pds\n  // but the takedown may particularly want only one of them\n  // unless the target services are specified, we will push to all configured services\n  getTakedownServices(targetServices: Set<string>): RepoPushEventType[] {\n    let configured: RepoPushEventType[] = []\n    if (this.pds) configured.push('pds_takedown')\n    if (this.appview) configured.push('appview_takedown')\n\n    if (!targetServices.size) {\n      return configured\n    }\n\n    if (!targetServices.has('appview')) {\n      configured = configured.filter(\n        (service) => service !== 'appview_takedown',\n      )\n    }\n    if (!targetServices.has('pds')) {\n      configured = configured.filter((service) => service !== 'pds_takedown')\n    }\n\n    return configured\n  }\n\n  poll(state: PollState, fn: () => Promise<void>) {\n    if (this.destroyed) return\n    state.promise = fn()\n      .catch((err) => {\n        dbLogger.error({ err }, 'event push failed')\n      })\n      .finally(() => {\n        state.timer = setTimeout(() => this.poll(state, fn), 30 * SECOND)\n      })\n  }\n\n  async processAll() {\n    await Promise.all([\n      this.pushRepoEvents(),\n      this.pushRecordEvents(),\n      this.pushBlobEvents(),\n      this.repoPollState.promise,\n      this.recordPollState.promise,\n      this.blobPollState.promise,\n    ])\n  }\n\n  async destroy() {\n    this.destroyed = true\n    const destroyState = (state: PollState) => {\n      if (state.timer) {\n        clearTimeout(state.timer)\n      }\n      return state.promise\n    }\n    await Promise.all([\n      destroyState(this.repoPollState),\n      destroyState(this.recordPollState),\n      destroyState(this.blobPollState),\n    ])\n  }\n\n  async pushRepoEvents() {\n    const toPush = await this.db.db\n      .selectFrom('repo_push_event')\n      .select('id')\n      .forUpdate()\n      .skipLocked()\n      .where('confirmedAt', 'is', null)\n      .where('attempts', '<', 10)\n      .execute()\n    await Promise.all(toPush.map((evt) => this.attemptRepoEvent(evt.id)))\n  }\n\n  async pushRecordEvents() {\n    const toPush = await this.db.db\n      .selectFrom('record_push_event')\n      .select('id')\n      .forUpdate()\n      .skipLocked()\n      .where('confirmedAt', 'is', null)\n      .where('attempts', '<', 10)\n      .execute()\n    await Promise.all(toPush.map((evt) => this.attemptRecordEvent(evt.id)))\n  }\n\n  async pushBlobEvents() {\n    const toPush = await this.db.db\n      .selectFrom('blob_push_event')\n      .select('id')\n      .forUpdate()\n      .skipLocked()\n      .where('confirmedAt', 'is', null)\n      .where('attempts', '<', 10)\n      .execute()\n    await Promise.all(toPush.map((evt) => this.attemptBlobEvent(evt.id)))\n  }\n\n  private async updateSubjectOnService(\n    service: Service,\n    subject: EventSubject,\n    takedownRef: string | null,\n  ): Promise<boolean> {\n    const auth = await this.createAuthHeaders(\n      service.did,\n      ids.ComAtprotoAdminUpdateSubjectStatus,\n    )\n    try {\n      await retryHttp(() =>\n        service.agent.com.atproto.admin.updateSubjectStatus(\n          {\n            subject,\n            takedown: {\n              applied: !!takedownRef,\n              ref: takedownRef ?? undefined,\n            },\n          },\n          {\n            ...auth,\n            encoding: 'application/json',\n          },\n        ),\n      )\n      return true\n    } catch (err) {\n      dbLogger.error({ err, subject, takedownRef }, 'failed to push out event')\n      return false\n    }\n  }\n\n  async attemptRepoEvent(id: number) {\n    await this.db.transaction(async (dbTxn) => {\n      const evt = await dbTxn.db\n        .selectFrom('repo_push_event')\n        .selectAll()\n        .forUpdate()\n        .skipLocked()\n        .where('id', '=', id)\n        .where('confirmedAt', 'is', null)\n        .executeTakeFirst()\n      if (!evt) return\n      const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview\n      assert(service)\n      const subject = {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: evt.subjectDid,\n      }\n      const succeeded = await this.updateSubjectOnService(\n        service,\n        subject,\n        evt.takedownRef,\n      )\n      await dbTxn.db\n        .updateTable('repo_push_event')\n        .set(\n          succeeded\n            ? { confirmedAt: new Date() }\n            : {\n                lastAttempted: new Date(),\n                attempts: (evt.attempts ?? 0) + 1,\n              },\n        )\n        .where('subjectDid', '=', evt.subjectDid)\n        .where('eventType', '=', evt.eventType)\n        .execute()\n    })\n  }\n\n  async attemptRecordEvent(id: number) {\n    await this.db.transaction(async (dbTxn) => {\n      const evt = await dbTxn.db\n        .selectFrom('record_push_event')\n        .selectAll()\n        .forUpdate()\n        .skipLocked()\n        .where('id', '=', id)\n        .where('confirmedAt', 'is', null)\n        .executeTakeFirst()\n      if (!evt) return\n      const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview\n      assert(service)\n      const subject = {\n        $type: 'com.atproto.repo.strongRef',\n        uri: evt.subjectUri,\n        cid: evt.subjectCid,\n      }\n      const succeeded = await this.updateSubjectOnService(\n        service,\n        subject,\n        evt.takedownRef,\n      )\n      await dbTxn.db\n        .updateTable('record_push_event')\n        .set(\n          succeeded\n            ? { confirmedAt: new Date() }\n            : {\n                lastAttempted: new Date(),\n                attempts: (evt.attempts ?? 0) + 1,\n              },\n        )\n        .where('subjectUri', '=', evt.subjectUri)\n        .where('eventType', '=', evt.eventType)\n        .execute()\n    })\n  }\n\n  async attemptBlobEvent(id: number) {\n    await this.db.transaction(async (dbTxn) => {\n      const evt = await dbTxn.db\n        .selectFrom('blob_push_event')\n        .selectAll()\n        .forUpdate()\n        .skipLocked()\n        .where('id', '=', id)\n        .where('confirmedAt', 'is', null)\n        .executeTakeFirst()\n      if (!evt) return\n\n      const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview\n      assert(service)\n      const subject = {\n        $type: 'com.atproto.admin.defs#repoBlobRef',\n        did: evt.subjectDid,\n        cid: evt.subjectBlobCid,\n      }\n      const succeeded = await this.updateSubjectOnService(\n        service,\n        subject,\n        evt.takedownRef,\n      )\n      await this.markBlobEventAttempt(dbTxn, evt, succeeded)\n    })\n  }\n\n  async markBlobEventAttempt(\n    dbTxn: Database,\n    event: Selectable<BlobPushEvent>,\n    succeeded: boolean,\n  ) {\n    await dbTxn.db\n      .updateTable('blob_push_event')\n      .set(\n        succeeded\n          ? { confirmedAt: new Date() }\n          : {\n              lastAttempted: new Date(),\n              attempts: (event.attempts ?? 0) + 1,\n            },\n      )\n      .where('subjectDid', '=', event.subjectDid)\n      .where('subjectBlobCid', '=', event.subjectBlobCid)\n      .where('eventType', '=', event.eventType)\n      .execute()\n  }\n\n  async logBlobPushEvent(\n    blobValues: Insertable<BlobPushEvent>[],\n    takedownRef?: string | null,\n  ) {\n    return this.db.db\n      .insertInto('blob_push_event')\n      .values(blobValues)\n      .onConflict((oc) =>\n        oc.columns(['subjectDid', 'subjectBlobCid', 'eventType']).doUpdateSet({\n          takedownRef,\n          confirmedAt: null,\n          attempts: 0,\n          lastAttempted: null,\n        }),\n      )\n      .returning([\n        'id',\n        'subjectDid',\n        'subjectUri',\n        'subjectBlobCid',\n        'eventType',\n      ])\n      .execute()\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/event-reverser.ts",
    "content": "import { MINUTE } from '@atproto/common'\nimport { Database } from '../db'\nimport { dbLogger } from '../logger'\nimport { ModerationServiceCreator, ReversalSubject } from '../mod-service'\n\nexport class EventReverser {\n  destroyed = false\n  reversalPromise: Promise<void> = Promise.resolve()\n  timer?: NodeJS.Timeout\n\n  constructor(\n    private db: Database,\n    private modService: ModerationServiceCreator,\n  ) {}\n\n  start() {\n    this.poll()\n  }\n\n  poll() {\n    if (this.destroyed) return\n    this.reversalPromise = this.findAndRevertDueActions()\n      .catch((err) =>\n        dbLogger.error({ err }, 'moderation action reversal errored'),\n      )\n      .finally(() => {\n        this.timer = setTimeout(() => this.poll(), getInterval())\n      })\n  }\n\n  async destroy() {\n    this.destroyed = true\n    if (this.timer) {\n      clearTimeout(this.timer)\n      this.timer = undefined\n    }\n    await this.reversalPromise\n  }\n\n  async revertState(subject: ReversalSubject) {\n    await this.db.transaction(async (dbTxn) => {\n      const moderationTxn = this.modService(dbTxn)\n      const originalEvent =\n        await moderationTxn.getLastReversibleEventForSubject(subject)\n      if (originalEvent) {\n        await moderationTxn.revertState({\n          action: originalEvent.action,\n          createdBy: originalEvent.createdBy,\n          comment:\n            '[SCHEDULED_REVERSAL] Reverting action as originally scheduled',\n          subject: subject.subject,\n          createdAt: new Date(),\n        })\n      }\n    })\n  }\n\n  async findAndRevertDueActions() {\n    const moderationService = this.modService(this.db)\n    const subjectsDueForReversal =\n      await moderationService.getSubjectsDueForReversal()\n\n    // We shouldn't have too many actions due for reversal at any given time, so running in parallel is probably fine\n    // Internally, each reversal runs within its own transaction\n    await Promise.all(subjectsDueForReversal.map(this.revertState.bind(this)))\n  }\n}\n\nconst getInterval = (): number => {\n  // super basic synchronization by agreeing when the intervals land relative to unix timestamp\n  const now = Date.now()\n  const intervalMs = MINUTE\n  const nextIteration = Math.ceil(now / intervalMs)\n  return nextIteration * intervalMs - now\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/index.ts",
    "content": "import { OzoneConfig, OzoneSecrets } from '../config'\nimport { AppContextOptions } from '../context'\nimport { DaemonContext } from './context'\n\nexport { EventPusher } from './event-pusher'\nexport { BlobDiverter } from './blob-diverter'\nexport { EventReverser } from './event-reverser'\nexport { ScheduledActionProcessor } from './scheduled-action-processor'\nexport { StrikeExpiryProcessor } from './strike-expiry-processor'\n\nexport class OzoneDaemon {\n  constructor(public ctx: DaemonContext) {}\n  static async create(\n    cfg: OzoneConfig,\n    secrets: OzoneSecrets,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<OzoneDaemon> {\n    const ctx = await DaemonContext.fromConfig(cfg, secrets, overrides)\n    return new OzoneDaemon(ctx)\n  }\n\n  async start() {\n    await this.ctx.start()\n  }\n\n  async processAll() {\n    await this.ctx.processAll()\n  }\n\n  async destroy() {\n    await this.ctx.destroy()\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/materialized-view-refresher.ts",
    "content": "import { sql } from 'kysely'\nimport { MINUTE } from '@atproto/common'\nimport { BackgroundQueue, PeriodicBackgroundTask } from '../background'\nimport { dbLogger } from '../logger'\n\nexport class MaterializedViewRefresher extends PeriodicBackgroundTask {\n  constructor(backgroundQueue: BackgroundQueue, interval = 30 * MINUTE) {\n    super(backgroundQueue, interval, async ({ db }, signal) => {\n      for (const view of [\n        'account_events_stats',\n        'record_events_stats',\n        'account_record_events_stats',\n        'account_record_status_stats',\n      ]) {\n        if (signal.aborted) break\n\n        // Kysely does not provide a way to cancel a running query. Because of\n        // this, killing the process during a refresh will cause the process to\n        // wait for the current refresh to finish before exiting. This is not\n        // ideal, but it is the best we can do until Kysely provides a way to\n        // cancel a query.\n        try {\n          await sql`REFRESH MATERIALIZED VIEW CONCURRENTLY ${sql.id(view)}`.execute(\n            db,\n          )\n          dbLogger.info(`refreshed materialized view ${view}`)\n        } catch (err) {\n          dbLogger.error({ err, view }, 'failed to refresh materialized view')\n        }\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/scheduled-action-processor.ts",
    "content": "import { Selectable } from 'kysely'\nimport { MINUTE, SECOND } from '@atproto/common'\nimport {\n  assertProtectedTagAction,\n  getProtectedTags,\n} from '../api/moderation/util'\nimport { Database } from '../db'\nimport { ScheduledAction } from '../db/schema/scheduled-action'\nimport {\n  ModEventTakedown,\n  ModTool,\n} from '../lexicon/types/tools/ozone/moderation/defs'\nimport { dbLogger } from '../logger'\nimport { ModerationService, ModerationServiceCreator } from '../mod-service'\nimport { RepoSubject } from '../mod-service/subject'\nimport { ModEventType } from '../mod-service/types'\nimport { ScheduledActionServiceCreator } from '../scheduled-action/service'\nimport { SettingService, SettingServiceCreator } from '../setting/service'\nimport { retryHttp } from '../util'\n\nexport class ScheduledActionProcessor {\n  destroyed = false\n  processingPromise: Promise<void> = Promise.resolve()\n  timer?: NodeJS.Timeout\n\n  constructor(\n    private db: Database,\n    private serviceDid: string,\n    private settingService: SettingServiceCreator,\n    private modService: ModerationServiceCreator,\n    private scheduledActionService: ScheduledActionServiceCreator,\n  ) {}\n\n  start() {\n    this.poll()\n  }\n\n  poll() {\n    if (this.destroyed) return\n    this.processingPromise = this.findAndExecuteScheduledActions()\n      .catch((err) =>\n        dbLogger.error({ err }, 'scheduled action processing errored'),\n      )\n      .finally(() => {\n        this.timer = setTimeout(() => this.poll(), getInterval())\n      })\n  }\n\n  async destroy() {\n    this.destroyed = true\n    if (this.timer) {\n      clearTimeout(this.timer)\n      this.timer = undefined\n    }\n    await this.processingPromise\n  }\n\n  async executeScheduledAction(actionId: number) {\n    await this.db.transaction(async (dbTxn) => {\n      const settingService = this.settingService(dbTxn)\n      const moderationTxn = this.modService(dbTxn)\n      const scheduledActionTxn = this.scheduledActionService(dbTxn)\n\n      try {\n        // maybe overfetching here to get the action again within the transaction to ensure it's still pending\n        const action = await dbTxn.db\n          .selectFrom('scheduled_action')\n          .selectAll()\n          .where('id', '=', actionId)\n          .where('status', '=', 'pending')\n          .executeTakeFirst()\n\n        if (!action) {\n          // already processed or cancelled\n          return\n        }\n\n        let event: ModEventType\n        const email = {\n          subject: '',\n          content: '',\n        }\n        let modTool: ModTool | undefined\n\n        // Create the appropriate moderation action based on the scheduled action type\n        switch (action.action) {\n          case 'takedown':\n            {\n              const eventData = action.eventData as ModEventTakedown & {\n                modTool?: ModTool\n                emailSubject?: string\n                emailContent?: string\n              }\n              modTool = eventData.modTool\n              event = {\n                $type: 'tools.ozone.moderation.defs#modEventTakedown',\n                comment: `[SCHEDULED_ACTION] ${eventData.comment || 'Scheduled takedown executed'}`,\n                durationInHours: eventData.durationInHours,\n                acknowledgeAccountSubjects:\n                  eventData.acknowledgeAccountSubjects,\n                policies: eventData.policies,\n                severityLevel: eventData.severityLevel,\n                strikeCount: eventData.strikeCount,\n              }\n\n              if (eventData.emailSubject && eventData.emailContent) {\n                email.subject = eventData.emailSubject\n                email.content = eventData.emailContent\n              }\n            }\n            break\n          default:\n            throw new Error(\n              `Unsupported scheduled action type: ${action.action}`,\n            )\n        }\n\n        const moderationEvent = await this.performTakedown({\n          action,\n          event,\n          modTool,\n          moderationTxn,\n          settingService,\n          email,\n        })\n\n        // Mark the scheduled action as executed\n        await scheduledActionTxn.markActionAsExecuted(\n          actionId,\n          moderationEvent.event.id,\n        )\n\n        dbLogger.info(\n          {\n            did: action.did,\n            scheduledActionId: actionId,\n            moderationEventId: moderationEvent.event.id,\n          },\n          'executed scheduled action',\n        )\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : 'Unknown error'\n\n        // mark as failed\n        await scheduledActionTxn.markActionAsFailed(actionId, errorMessage)\n\n        dbLogger.error(\n          {\n            scheduledActionId: actionId,\n            error: errorMessage,\n          },\n          'failed to execute scheduled action',\n        )\n      }\n    })\n  }\n\n  async performTakedown({\n    email,\n    action,\n    event,\n    modTool,\n    moderationTxn,\n    settingService,\n  }: {\n    email: { subject: string; content: string }\n    action: Selectable<ScheduledAction>\n    event: ModEventType\n    modTool: ModTool | undefined\n\n    moderationTxn: ModerationService\n    settingService: SettingService\n  }) {\n    const subject = new RepoSubject(action.did)\n\n    const status = await moderationTxn.getStatus(subject)\n\n    if (status?.takendown) {\n      throw new Error(`Account is already taken down`)\n    }\n\n    if (status?.tags?.length) {\n      const protectedTags = await getProtectedTags(\n        settingService,\n        this.serviceDid,\n      )\n\n      if (protectedTags) {\n        assertProtectedTagAction({\n          protectedTags,\n          subjectTags: status.tags,\n          actionAuthor: action.createdBy,\n          isAdmin: true,\n          isModerator: false,\n          isTriage: false,\n        })\n      }\n    }\n\n    // log the event which also applies the necessary state changes to moderation subject\n    const moderationEvent = await moderationTxn.logEvent({\n      event,\n      subject,\n      modTool,\n      createdBy: action.createdBy,\n    })\n\n    // register the takedown in event pusher\n    await moderationTxn.takedownRepo(\n      subject,\n      moderationEvent.event.id,\n      new Set(\n        moderationEvent.event.meta?.targetServices\n          ? `${moderationEvent.event.meta.targetServices}`.split(',')\n          : undefined,\n      ),\n    )\n\n    if (email.content && email.subject) {\n      let isDelivered = false\n      try {\n        await retryHttp(() =>\n          moderationTxn.sendEmail({\n            ...email,\n            recipientDid: action.did,\n          }),\n        )\n        isDelivered = true\n      } catch (err) {\n        dbLogger.error(\n          { err, did: action.did },\n          'failed to send takedown email',\n        )\n      }\n      await moderationTxn.logEvent({\n        event: {\n          content: email.content,\n          subjectLine: email.subject,\n          $type: 'tools.ozone.moderation.defs#modEventEmail',\n          comment: [\n            'Communication attached to scheduled action',\n            isDelivered ? '' : 'Email delivery failed',\n          ].join('.'),\n          isDelivered,\n        },\n        subject,\n        modTool,\n        createdBy: action.createdBy,\n      })\n    }\n\n    return moderationEvent\n  }\n\n  async findAndExecuteScheduledActions() {\n    const scheduledActionService = this.scheduledActionService(this.db)\n    const now = new Date()\n\n    const actionsToExecute =\n      await scheduledActionService.getPendingActionsToExecute(now)\n\n    for (const action of actionsToExecute) {\n      // For randomized execution, check if we should execute now or wait\n      if (action.randomizeExecution && action.executeAfter) {\n        const executeAfter = new Date(action.executeAfter)\n        // Default to a 30 second window for execution\n        const executeUntil = action.executeUntil\n          ? new Date(action.executeUntil)\n          : new Date(executeAfter.getTime() + 30 * SECOND)\n\n        // Only execute if we're past the earliest time\n        if (now < executeAfter) {\n          continue\n        }\n\n        // For randomized scheduling, randomly decide whether to execute now\n        // The probability increases as we get closer to the deadline\n        const timeRange = executeUntil.getTime() - executeAfter.getTime()\n        const timeElapsed = now.getTime() - executeAfter.getTime()\n        const executeProb = Math.min(timeElapsed / timeRange, 1)\n\n        // Execute with increasing probability as we approach the deadline\n        // Always execute if we're at or past the deadline\n        if (now < executeUntil && Math.random() > executeProb * 0.1) {\n          continue\n        }\n      }\n\n      await this.executeScheduledAction(action.id)\n    }\n  }\n}\n\nconst getInterval = (): number => {\n  // Process scheduled actions every minute\n  const now = Date.now()\n  const intervalMs = MINUTE\n  const nextIteration = Math.ceil(now / intervalMs)\n  return nextIteration * intervalMs - now\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/strike-expiry-processor.ts",
    "content": "import { HOUR } from '@atproto/common'\nimport { Database } from '../db'\nimport { dbLogger } from '../logger'\nimport { StrikeServiceCreator } from '../mod-service/strike'\n\nconst JOB_NAME = 'strike_expiry'\n\nexport class StrikeExpiryProcessor {\n  destroyed = false\n  processingPromise: Promise<void> = Promise.resolve()\n  timer?: NodeJS.Timeout\n\n  constructor(\n    private db: Database,\n    private strikeServiceCreator: StrikeServiceCreator,\n  ) {}\n\n  start() {\n    this.initializeCursor().then(() => this.poll())\n  }\n\n  poll() {\n    if (this.destroyed) return\n    this.processingPromise = this.processExpiredStrikes()\n      .catch((err) =>\n        dbLogger.error({ err }, 'strike expiry processing errored'),\n      )\n      .finally(() => {\n        this.timer = setTimeout(() => this.poll(), getInterval())\n      })\n  }\n\n  async destroy() {\n    this.destroyed = true\n    if (this.timer) {\n      clearTimeout(this.timer)\n      this.timer = undefined\n    }\n    await this.processingPromise\n  }\n\n  async initializeCursor() {\n    await this.db.db\n      .insertInto('job_cursor')\n      .values({\n        job: JOB_NAME,\n        cursor: null,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  }\n\n  async getCursor(): Promise<string | null> {\n    const entry = await this.db.db\n      .selectFrom('job_cursor')\n      .select('cursor')\n      .where('job', '=', JOB_NAME)\n      .executeTakeFirst()\n\n    return entry?.cursor || null\n  }\n\n  async updateCursor(cursor: string): Promise<void> {\n    await this.db.db\n      .updateTable('job_cursor')\n      .set({ cursor })\n      .where('job', '=', JOB_NAME)\n      .execute()\n  }\n\n  async processExpiredStrikes() {\n    const now = new Date()\n    const strikeService = this.strikeServiceCreator(this.db)\n    const lastProcessedAt = await this.getCursor()\n    const affectedSubjects = await strikeService.getExpiredStrikeSubjects(\n      lastProcessedAt || undefined,\n    )\n\n    if (!affectedSubjects.length) {\n      dbLogger.info('no expired strikes to process')\n      await this.updateCursor(now.toISOString())\n      return\n    }\n\n    dbLogger.info(\n      { count: affectedSubjects.length },\n      'processing subjects with expired strikes',\n    )\n\n    await Promise.all(\n      affectedSubjects.map(({ subjectDid }) => {\n        return strikeService.updateSubjectStrikeCount(subjectDid)\n      }),\n    )\n\n    await this.updateCursor(now.toISOString())\n\n    dbLogger.info(\n      { processed: affectedSubjects.length },\n      'strike expiry processing completed',\n    )\n  }\n}\n\nconst getInterval = (): number => {\n  // Run every hour, synchronized to the hour boundary\n  const now = Date.now()\n  const intervalMs = HOUR\n  const nextIteration = Math.ceil(now / intervalMs)\n  return nextIteration * intervalMs - now\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/team-profile-synchronizer.ts",
    "content": "import { HOUR } from '@atproto/common'\nimport { BackgroundQueue, PeriodicBackgroundTask } from '../background'\nimport { TeamService } from '../team'\n\nexport class TeamProfileSynchronizer extends PeriodicBackgroundTask {\n  constructor(\n    backgroundQueue: BackgroundQueue,\n    teamService: TeamService,\n    interval = 24 * HOUR,\n  ) {\n    super(backgroundQueue, interval, async () => {\n      await teamService.syncMemberProfiles()\n    })\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/daemon/verification-listener.ts",
    "content": "import { lexicons } from '@atproto/api'\nimport { BackgroundQueue } from '../background'\nimport { Database } from '../db'\nimport { CommitCreateEvent, Jetstream } from '../jetstream/service'\nimport { verificationLogger } from '../logger'\nimport { VerificationService } from '../verification/service'\n\ntype VerificationRecord = {\n  subject: string\n  handle: string\n  displayName: string\n  createdAt: string\n}\n\nexport class VerificationListener {\n  destroyed = false\n  private cursor?: number\n  private jetstream: Jetstream | null = null\n  private collection = 'app.bsky.graph.verification'\n  public backgroundQueue = new BackgroundQueue(this.db, { concurrency: 1 })\n  private verificationService = VerificationService.creator()(this.db)\n\n  constructor(\n    private db: Database,\n    private jetstreamUrl: string,\n    private verifierIssuersToIndex?: string[],\n  ) {}\n\n  // When the queue has capacity, this method returns true which means we can continue to handle events\n  // otherwise, it will close jetstream connection and wait for all previously queued events to be processed first\n  // and then start jetstream listener again before returning false. At that point, the previous listeners should\n  // have updates the cursor in db to the last processed event and the new listener will start from that cursor\n  async ensureCoolDown() {\n    const { waitingCount, runningCount } = this.backgroundQueue.getStats()\n    if (waitingCount > 50 || runningCount > 50) {\n      verificationLogger.warn(`Background queue is full, pausing listener`)\n      this.jetstream?.close()\n      await this.backgroundQueue.processAll()\n      await this.start()\n      return false\n    }\n    return true\n  }\n\n  handleNewVerification(\n    issuer: string,\n    uri: string,\n    cid: string,\n    record: VerificationRecord,\n    cursor: number,\n  ) {\n    this.backgroundQueue.add(async () => {\n      try {\n        const { subject, handle, displayName, createdAt } = record\n        await this.verificationService.create([\n          { uri, cid, issuer, subject, handle, displayName, createdAt },\n        ])\n        await this.updateCursor(cursor)\n      } catch (err) {\n        verificationLogger.error(\n          err,\n          'Error handling verification create event',\n        )\n      }\n    })\n  }\n\n  handleDeletedVerification(uri: string, cursor: number) {\n    this.backgroundQueue.add(async () => {\n      try {\n        await this.verificationService.markRevoked({\n          uris: [uri],\n        })\n        await this.updateCursor(cursor)\n      } catch (err) {\n        verificationLogger.error(\n          err,\n          'Error handling verification delete event',\n        )\n      }\n    })\n  }\n\n  async getCursor() {\n    await this.verificationService.createFirehoseCursor()\n    const cursor = await this.verificationService.getFirehoseCursor()\n    if (cursor) {\n      this.cursor = cursor\n    }\n    return this.cursor\n  }\n\n  async updateCursor(cursor: number) {\n    // Assuming cursors are always incremental, if we have processed an event with higher value cursor, let's not update to a lower value\n    if (this.cursor && this.cursor >= cursor) {\n      return\n    }\n\n    // This will only update if the cursor is higher than the current one in db\n    const updatedCursor =\n      await this.verificationService.updateFirehoseCursor(cursor)\n\n    if (updatedCursor) {\n      this.cursor = updatedCursor\n    }\n  }\n\n  async start() {\n    await this.getCursor()\n\n    this.jetstream = new Jetstream({\n      endpoint: this.jetstreamUrl,\n      cursor: this.cursor || undefined,\n      wantedCollections: [this.collection],\n      wantedDids: this.verifierIssuersToIndex?.length\n        ? this.verifierIssuersToIndex\n        : undefined,\n    })\n\n    await this.jetstream.start({\n      onCreate: {\n        [this.collection]: async (e: CommitCreateEvent<VerificationRecord>) => {\n          const recordValidity = lexicons.validate(\n            this.collection,\n            e.commit.record,\n          )\n\n          if (!recordValidity.success) {\n            verificationLogger.error(\n              recordValidity.error,\n              'Invalid verification record in the firehose',\n            )\n            return\n          }\n\n          const hasCapacity = await this.ensureCoolDown()\n          if (hasCapacity) {\n            const issuer = e.did\n            const { record, rkey, collection, cid } = e.commit\n            const uri = `at://${issuer}/${collection}/${rkey}`\n            this.handleNewVerification(issuer, uri, cid, record, e.time_us)\n          }\n        },\n      },\n      onDelete: {\n        [this.collection]: async (e) => {\n          const hasCapacity = await this.ensureCoolDown()\n          if (hasCapacity) {\n            this.handleDeletedVerification(\n              `at://${e.did}/${e.commit.collection}/${e.commit.rkey}`,\n              e.time_us,\n            )\n          }\n        },\n      },\n    })\n  }\n\n  stop() {\n    this.jetstream?.close()\n    this.backgroundQueue.destroy()\n    this.destroyed = true\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/db/index.ts",
    "content": "import assert from 'node:assert'\nimport { EventEmitter } from 'node:stream'\nimport {\n  Kysely,\n  KyselyPlugin,\n  Migrator,\n  PluginTransformQueryArgs,\n  PluginTransformResultArgs,\n  PostgresDialect,\n  QueryResult,\n  RootOperationNode,\n  UnknownRow,\n} from 'kysely'\nimport { Pool as PgPool, types as pgTypes } from 'pg'\nimport TypedEmitter from 'typed-emitter'\nimport { dbLogger } from '../logger'\nimport * as migrations from './migrations'\nimport { CtxMigrationProvider } from './migrations/provider'\nimport { DatabaseSchema, DatabaseSchemaType } from './schema'\nimport { PgOptions } from './types'\n\nexport class Database {\n  pool: PgPool\n  db: DatabaseSchema\n  migrator: Migrator\n  txEvt = new EventEmitter() as TxnEmitter\n  destroyed = false\n  isPrimary = false\n\n  constructor(\n    public opts: PgOptions,\n    instances?: { db: DatabaseSchema; pool: PgPool },\n  ) {\n    // if instances are provided, use those\n    if (instances) {\n      this.db = instances.db\n      this.pool = instances.pool\n    } else {\n      // else create a pool & connect\n      const { schema, url } = opts\n      const pool =\n        opts.pool ??\n        new PgPool({\n          connectionString: url,\n          max: opts.poolSize,\n          maxUses: opts.poolMaxUses,\n          idleTimeoutMillis: opts.poolIdleTimeoutMs,\n        })\n\n      // Select count(*) and other pg bigints as js integer\n      pgTypes.setTypeParser(pgTypes.builtins.INT8, (n) => parseInt(n, 10))\n\n      // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema)\n      if (schema && !/^[a-z_]+$/i.test(schema)) {\n        throw new Error(\n          `Postgres schema must only contain [A-Za-z_]: ${schema}`,\n        )\n      }\n\n      pool.on('error', onPoolError)\n      pool.on('connect', (client) => {\n        client.on('error', onClientError)\n        // Used for trigram indexes, e.g. on actor search\n        client.query('SET pg_trgm.word_similarity_threshold TO .4;')\n        if (schema) {\n          // Shared objects such as extensions will go in the public schema\n          client.query(`SET search_path TO \"${schema}\",public;`)\n        }\n      })\n\n      this.pool = pool\n      this.db = new Kysely<DatabaseSchemaType>({\n        dialect: new PostgresDialect({ pool }),\n      })\n    }\n\n    this.migrator = new Migrator({\n      db: this.db,\n      migrationTableSchema: opts.schema,\n      provider: new CtxMigrationProvider(migrations, 'pg'),\n    })\n  }\n\n  get schema(): string | undefined {\n    return this.opts.schema\n  }\n\n  get isTransaction() {\n    return this.db.isTransaction\n  }\n\n  assertTransaction() {\n    assert(this.isTransaction, 'Transaction required')\n  }\n\n  assertNotTransaction() {\n    assert(!this.isTransaction, 'Cannot be in a transaction')\n  }\n\n  async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {\n    const leakyTxPlugin = new LeakyTxPlugin()\n    const { dbTxn, txRes } = await this.db\n      .withPlugin(leakyTxPlugin)\n      .transaction()\n      .execute(async (txn) => {\n        const dbTxn = new Database(this.opts, {\n          db: txn,\n          pool: this.pool,\n        })\n        const txRes = await fn(dbTxn)\n          .catch(async (err) => {\n            leakyTxPlugin.endTx()\n            // ensure that all in-flight queries are flushed & the connection is open\n            await dbTxn.db.getExecutor().provideConnection(noopAsync)\n            throw err\n          })\n          .finally(() => leakyTxPlugin.endTx())\n        return { dbTxn, txRes }\n      })\n    dbTxn?.txEvt.emit('commit')\n    return txRes\n  }\n\n  onCommit(fn: () => void) {\n    this.assertTransaction()\n    this.txEvt.once('commit', fn)\n  }\n\n  async close(): Promise<void> {\n    if (this.destroyed) return\n    await this.db.destroy()\n    this.destroyed = true\n  }\n\n  async migrateToOrThrow(migration: string) {\n    if (this.schema) {\n      await this.db.schema.createSchema(this.schema).ifNotExists().execute()\n    }\n    const { error, results } = await this.migrator.migrateTo(migration)\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n\n  async migrateToLatestOrThrow() {\n    if (this.schema) {\n      await this.db.schema.createSchema(this.schema).ifNotExists().execute()\n    }\n    const { error, results } = await this.migrator.migrateToLatest()\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n}\n\nexport default Database\n\nconst onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error')\nconst onClientError = (err: Error) => dbLogger.error({ err }, 'db client error')\n\n// utils\n// -------\n\nclass LeakyTxPlugin implements KyselyPlugin {\n  private txOver = false\n\n  endTx() {\n    this.txOver = true\n  }\n\n  transformQuery(args: PluginTransformQueryArgs): RootOperationNode {\n    if (this.txOver) {\n      throw new Error('tx already failed')\n    }\n    return args.node\n  }\n\n  async transformResult(\n    args: PluginTransformResultArgs,\n  ): Promise<QueryResult<UnknownRow>> {\n    return args.result\n  }\n}\n\ntype TxnEmitter = TypedEmitter<TxnEvents>\n\ntype TxnEvents = {\n  commit: () => void\n}\n\nconst noopAsync = async () => {}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20231219T205730722Z-init.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Moderation event\n  await db.schema\n    .createTable('moderation_event')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('action', 'varchar', (col) => col.notNull())\n    .addColumn('subjectType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar')\n    .addColumn('subjectCid', 'varchar')\n    .addColumn('comment', 'text')\n    .addColumn('meta', 'jsonb')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('reversedAt', 'varchar')\n    .addColumn('reversedBy', 'varchar')\n    .addColumn('durationInHours', 'integer')\n    .addColumn('expiresAt', 'varchar')\n    .addColumn('reversedReason', 'text')\n    .addColumn('createLabelVals', 'varchar')\n    .addColumn('negateLabelVals', 'varchar')\n    .addColumn('legacyRefId', 'integer')\n    .execute()\n\n  // Moderation subject status\n  await db.schema\n    .createTable('moderation_subject_status')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n\n    // Identifiers\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    // Default to '' so that we can apply unique constraints on did and recordPath columns\n    .addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo(''))\n    .addColumn('blobCids', 'jsonb')\n    .addColumn('recordCid', 'varchar')\n\n    // human review team state\n    .addColumn('reviewState', 'varchar', (col) => col.notNull())\n    .addColumn('comment', 'varchar')\n    .addColumn('muteUntil', 'varchar')\n    .addColumn('lastReviewedAt', 'varchar')\n    .addColumn('lastReviewedBy', 'varchar')\n\n    // report state\n    .addColumn('lastReportedAt', 'varchar')\n    .addColumn('lastAppealedAt', 'varchar')\n\n    // visibility/intervention state\n    .addColumn('takendown', 'boolean', (col) => col.defaultTo(false).notNull())\n    .addColumn('suspendUntil', 'varchar')\n    .addColumn('appealed', 'boolean')\n\n    // timestamps\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addUniqueConstraint('moderation_status_unique_idx', ['did', 'recordPath'])\n    .execute()\n\n  await db.schema\n    .createIndex('moderation_subject_status_blob_cids_idx')\n    .on('moderation_subject_status')\n    .using('gin')\n    .column('blobCids')\n    .execute()\n\n  // Label\n  await db.schema\n    .createTable('label')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('src', 'varchar', (col) => col.notNull())\n    .addColumn('uri', 'varchar', (col) => col.notNull())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('val', 'varchar', (col) => col.notNull())\n    .addColumn('neg', 'boolean', (col) => col.notNull())\n    .addColumn('cts', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .createIndex('unique_label_idx')\n    .unique()\n    .on('label')\n    .columns(['src', 'uri', 'cid', 'val'])\n    .execute()\n  await db.schema\n    .createIndex('label_uri_index')\n    .on('label')\n    .column('uri')\n    .execute()\n\n  // Push Events\n  await db.schema\n    .createTable('repo_push_event')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('eventType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('takedownRef', 'varchar')\n    .addColumn('confirmedAt', 'timestamptz')\n    .addColumn('lastAttempted', 'timestamptz')\n    .addColumn('attempts', 'integer', (col) => col.notNull().defaultTo(0))\n    .addUniqueConstraint('repo_push_event_unique_evt', [\n      'subjectDid',\n      'eventType',\n    ])\n    .execute()\n  await db.schema\n    .createIndex('repo_push_confirmation_idx')\n    .on('repo_push_event')\n    .columns(['confirmedAt', 'attempts'])\n    .execute()\n\n  await db.schema\n    .createTable('record_push_event')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('eventType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar', (col) => col.notNull())\n    .addColumn('subjectCid', 'varchar')\n    .addColumn('takedownRef', 'varchar')\n    .addColumn('confirmedAt', 'timestamptz')\n    .addColumn('lastAttempted', 'timestamptz')\n    .addColumn('attempts', 'integer', (col) => col.notNull().defaultTo(0))\n    .addUniqueConstraint('record_push_event_unique_evt', [\n      'subjectUri',\n      'eventType',\n    ])\n    .execute()\n  await db.schema\n    .createIndex('record_push_event_did_type_idx')\n    .on('record_push_event')\n    .columns(['subjectDid', 'eventType'])\n    .execute()\n  await db.schema\n    .createIndex('record_push_confirmation_idx')\n    .on('record_push_event')\n    .columns(['confirmedAt', 'attempts'])\n    .execute()\n\n  await db.schema\n    .createTable('blob_push_event')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('eventType', 'varchar', (col) => col.notNull())\n    .addColumn('subjectDid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectBlobCid', 'varchar', (col) => col.notNull())\n    .addColumn('subjectUri', 'varchar')\n    .addColumn('takedownRef', 'varchar')\n    .addColumn('confirmedAt', 'timestamptz')\n    .addColumn('lastAttempted', 'timestamptz')\n    .addColumn('attempts', 'integer', (col) => col.notNull().defaultTo(0))\n    .addUniqueConstraint('blob_push_event_unique_evt', [\n      'subjectDid',\n      'subjectBlobCid',\n      'eventType',\n    ])\n    .execute()\n  await db.schema\n    .createIndex('blob_push_confirmation_idx')\n    .on('blob_push_event')\n    .columns(['confirmedAt', 'attempts'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('moderation_event').execute()\n  await db.schema.dropTable('moderation_subject_status').execute()\n  await db.schema.dropTable('label').execute()\n  await db.schema.dropTable('repo_push_event').execute()\n  await db.schema.dropTable('record_push_event').execute()\n  await db.schema.dropTable('blob_push_event').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240116T085607200Z-communication-template.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('communication_template')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('name', 'varchar', (col) => col.notNull())\n    .addColumn('contentMarkdown', 'varchar', (col) => col.notNull())\n    .addColumn('subject', 'varchar')\n    .addColumn('disabled', 'boolean', (col) => col.defaultTo(false).notNull())\n    .addColumn('createdAt', 'timestamptz')\n    .addColumn('updatedAt', 'timestamptz')\n    .addColumn('lastUpdatedBy', 'varchar', (col) => col.notNull())\n    .addUniqueConstraint('communication_template_unique_name', [\n      'name',\n      'disabled',\n    ])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('communication_template')\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('subjectBlobCids', 'jsonb')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('subjectBlobCids')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240208T213404429Z-add-tags-column-to-moderation-subject.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('addedTags', 'jsonb')\n    .execute()\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('removedTags', 'jsonb')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('tags', 'jsonb')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('addedTags')\n    .execute()\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('removedTags')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('tags')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240228T003647759Z-add-label-sigs.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('label').addColumn('exp', 'varchar').execute()\n  await db.schema\n    .alterTable('label')\n    .addColumn('sig', sql`bytea`)\n    .execute()\n  await db.schema\n    .alterTable('label')\n    .addColumn('signingKeyId', 'integer')\n    .execute()\n  await db.schema\n    .createTable('signing_key')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('key', 'varchar', (col) => col.notNull().unique())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('signing_key')\n  await db.schema.alterTable('label').dropColumn('exp').execute()\n  await db.schema.alterTable('label').dropColumn('sig').execute()\n  await db.schema.alterTable('label').dropColumn('signingKey').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240408T192432676Z-mute-reporting.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('muteReportingUntil', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('muteReportingUntil')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240506T225055595Z-message-subject.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('subjectMessageId', 'varchar')\n    .execute()\n  // support lookup for chat.bsky.moderation.getMessageContext\n  await db.schema\n    .createIndex('moderation_event_message_id_index')\n    .on('moderation_event')\n    .column('subjectMessageId')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('subjectMessageId')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240521T211332580Z-member.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('member')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('role', 'varchar', (col) => col.notNull())\n    .addColumn('disabled', 'boolean', (col) => col.defaultTo(false).notNull())\n    .addColumn('createdAt', 'timestamptz', (col) => col.notNull())\n    .addColumn('updatedAt', 'timestamptz', (col) => col.notNull())\n    .addColumn('lastUpdatedBy', 'varchar', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('member')\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240814T003647759Z-event-created-at-index.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('moderation_event_created_at_idx')\n    .on('moderation_event')\n    .column('createdAt')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('moderation_event_created_at_idx').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240903T205730722Z-add-template-lang.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('communication_template')\n    .addColumn('lang', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('moderation_event').dropColumn('lang').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20240904T205730722Z-add-subject-did-index.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('moderation_event_subject_did_idx')\n    .on('moderation_event')\n    .column('subjectDid')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('moderation_event_subject_did_idx').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20241001T205730722Z-subject-status-review-state-index.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('moderation_subject_status_review_state_idx')\n    .on('moderation_subject_status')\n    .column('reviewState')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .dropIndex('moderation_subject_status_review_state_idx')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20241008T205730722Z-sets.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Create the sets table\n  await db.schema\n    .createTable('set_detail')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('name', 'varchar', (col) => col.notNull().unique())\n    .addColumn('description', 'varchar')\n    .addColumn('createdAt', 'timestamptz', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .addColumn('updatedAt', 'timestamptz', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .execute()\n\n  // Create the set values table\n  await db.schema\n    .createTable('set_value')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('setId', 'integer', (col) =>\n      col.notNull().references('set_detail.id'),\n    )\n    .addColumn('value', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'timestamptz', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .execute()\n\n  // Add indexes for better performance\n  await db.schema\n    .createIndex('set_detail_name_idx')\n    .on('set_detail')\n    .column('name')\n    .execute()\n\n  // Create a unique constraint on setId and value\n  await db.schema\n    .alterTable('set_value')\n    .addUniqueConstraint('set_value_setid_value_unique', ['setId', 'value'])\n    .execute()\n  await db.schema\n    .createIndex('set_value_setid_created_at_idx')\n    .on('set_value')\n    .columns(['setId', 'createdAt'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('set_value').execute()\n  await db.schema.dropTable('set_detail').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20241018T205730722Z-setting.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('setting')\n    .addColumn('id', 'serial', (col) => col.primaryKey())\n    .addColumn('key', 'text', (col) => col.notNull())\n    .addColumn('did', 'text', (col) => col.notNull())\n    .addColumn('value', 'jsonb', (col) => col.notNull())\n    .addColumn('description', 'text')\n    .addColumn('createdAt', 'timestamptz', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .addColumn('updatedAt', 'timestamptz', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .addColumn('managerRole', 'text')\n    .addColumn('scope', 'text', (col) => col.notNull())\n    .addColumn('createdBy', 'text', (col) => col.notNull())\n    .addColumn('lastUpdatedBy', 'text', (col) => col.notNull())\n    .addUniqueConstraint('setting_did_scope_key_idx', ['did', 'scope', 'key'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('setting').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('hostingStatus', 'varchar', (col) =>\n      col.notNull().defaultTo('unknown'),\n    )\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('hostingDeletedAt', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('hostingUpdatedAt', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('hostingCreatedAt', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('hostingDeactivatedAt', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('hostingReactivatedAt', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('hostingStatus')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('hostingDeletedAt')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('hostingUpdatedAt')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('hostingCreatedAt')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('hostingDeactivatedAt')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('hostingReactivatedAt')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts",
    "content": "import { Kysely, sql } from 'kysely'\nimport { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs'\nimport {\n  REVIEWESCALATED,\n  REVIEWOPEN,\n} from '../../lexicon/types/tools/ozone/moderation/defs'\nimport { DatabaseSchemaType } from '../schema'\nimport * as modEvent from '../schema/moderation_event'\nimport * as modStatus from '../schema/moderation_subject_status'\nimport * as recordEventsStats from '../schema/record_events_stats'\n\nexport async function up(db: Kysely<any>): Promise<void> {\n  // Used by \"tools.ozone.moderation.queryStatuses\". Reduces query cost by two\n  // order of magnitudes when sorting using \"reportedRecordsCount\" or\n  // \"takendownRecordsCount\" and filtering by \"reviewState\".\n  await db.schema\n    .createIndex('moderation_subject_status_did_id_review_state_idx')\n    .on('moderation_subject_status')\n    .column('did')\n    .expression(sql`\"id\" ASC NULLS FIRST`)\n    .column('reviewState')\n    .execute()\n\n  // ~6sec for 16M events\n  await db.schema\n    .createView('account_events_stats')\n    .materialized()\n    .ifNotExists()\n    .as(\n      (db as Kysely<modEvent.PartialDB>)\n        .selectFrom('moderation_event')\n        .where('subjectType', '=', 'com.atproto.admin.defs#repoRef')\n        .where('subjectUri', 'is', null)\n        .select('subjectDid')\n        .select([\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'\n              AND ${eb.ref('durationInHours')} IS NULL\n            )`.as('takedownCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'\n              AND ${eb.ref('durationInHours')} IS NOT NULL\n            )`.as('suspendCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate'\n            )`.as('escalateCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'\n              AND ${eb.ref('meta')} ->> 'reportType' != ${REASONAPPEAL}\n            )`.as('reportCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'\n              AND ${eb.ref('meta')} ->> 'reportType' = ${REASONAPPEAL}\n            )`.as('appealCount'),\n        ])\n        .groupBy('subjectDid'),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('account_events_stats_did_idx')\n    .unique()\n    .on('account_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_events_stats_suspend_count_idx')\n    .on('account_events_stats')\n    .expression(sql`\"suspendCount\" ASC NULLS FIRST`)\n    .column('subjectDid')\n    .execute()\n\n  // ~50sec for 16M events\n  await db.schema\n    .createView('record_events_stats')\n    .materialized()\n    .ifNotExists()\n    .as(\n      (db as Kysely<modEvent.PartialDB>)\n        .selectFrom('moderation_event')\n        .select([\n          'subjectDid',\n          'subjectUri',\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate')`.as(\n              'escalateCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' != 'com.atproto.moderation.defs#reasonAppeal')`.as(\n              'reportCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' = 'com.atproto.moderation.defs#reasonAppeal')`.as(\n              'appealCount',\n            ),\n        ])\n        .where('subjectType', '=', 'com.atproto.repo.strongRef')\n        .where('subjectUri', 'is not', null)\n        .groupBy(['subjectDid', 'subjectUri']),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('record_events_stats_uri_idx')\n    .unique()\n    .on('record_events_stats')\n    .column('subjectUri')\n    .execute()\n\n  await db.schema\n    .createIndex('record_events_stats_did_idx')\n    .on('record_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createView('account_record_events_stats')\n    .materialized()\n    .ifNotExists()\n    .as(\n      (db as Kysely<recordEventsStats.PartialDB>)\n        .selectFrom('record_events_stats')\n        .select([\n          'subjectDid',\n          (eb) =>\n            // Casting to \"bigint\" because \"numeric\" gets casted to a string\n            // by default by postgres-node.\n            sql<number>`SUM(${eb.ref('reportCount')})::bigint`.as(\n              'totalReports',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reportCount')} > 0)`.as(\n              'reportedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('escalateCount')} > 0)`.as(\n              'escalatedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('appealCount')} > 0)`.as(\n              'appealedCount',\n            ),\n        ])\n        .groupBy('subjectDid'),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_events_stats_did_idx')\n    .unique()\n    .on('account_record_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_events_stats_reported_count_idx')\n    .on('account_record_events_stats')\n    .expression(sql`\"reportedCount\" ASC NULLS FIRST`)\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createView('account_record_status_stats')\n    .materialized()\n    .ifNotExists()\n    .as(\n      (db as Kysely<modStatus.PartialDB>)\n        .selectFrom('moderation_subject_status')\n        .select('did')\n        .select([\n          sql<number>`COUNT(*)`.as('subjectCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reviewState')} IN (${REVIEWOPEN}, ${REVIEWESCALATED}))`.as(\n              'pendingCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reviewState')} NOT IN (${REVIEWOPEN}, ${REVIEWESCALATED}))`.as(\n              'processedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('takendown')})`.as(\n              'takendownCount',\n            ),\n        ])\n        .where('recordPath', '!=', '')\n        .groupBy('did'),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_status_stats_did_idx')\n    .unique()\n    .on('account_record_status_stats')\n    .column('did')\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_status_stats_takendown_count_idx')\n    .on('account_record_status_stats')\n    .expression(sql`\"takendownCount\" ASC NULLS FIRST`)\n    .column('did')\n    .execute()\n}\n\nexport async function down(db: Kysely<DatabaseSchemaType>): Promise<void> {\n  db.schema.dropView('account_record_status_stats').materialized().execute()\n  db.schema.dropView('account_record_events_stats').materialized().execute()\n  db.schema.dropView('record_events_stats').materialized().execute()\n  db.schema.dropView('account_events_stats').materialized().execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250204T003647759Z-add-subject-priority-score.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('priorityScore', 'integer', (col) =>\n      col.notNull().defaultTo('0'),\n    )\n    .execute()\n  await db.schema\n    .createIndex('moderation_subject_status_priority_score_index')\n    .on('moderation_subject_status')\n    .column('priorityScore')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('priorityScore')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250211T003647759Z-add-reporter-stats-index.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await sql`\n    CREATE INDEX \"moderation_event_account_reports_idx\"\n    ON moderation_event(\"createdBy\",\"subjectDid\", \"createdAt\")\n    WHERE \"subjectUri\" IS NULL\n    AND \"action\" = 'tools.ozone.moderation.defs#modEventReport'\n  `.execute(db)\n\n  await sql`\n    CREATE INDEX \"moderation_event_record_reports_idx\"\n    ON moderation_event(\"createdBy\",\"subjectDid\",\"subjectUri\", \"createdAt\")\n    WHERE \"subjectUri\" IS NOT NULL\n    AND \"action\" = 'tools.ozone.moderation.defs#modEventReport'\n  `.execute(db)\n\n  await sql`\n    CREATE INDEX \"moderation_event_account_actions_ids\"\n    ON moderation_event(\"subjectDid\",\"action\", \"createdAt\")\n    WHERE \"subjectUri\" IS NULL\n    AND \"action\" IN ( 'tools.ozone.moderation.defs#modEventTakedown', 'tools.ozone.moderation.defs#modEventLabel')\n  `.execute(db)\n\n  await sql`\n    CREATE INDEX \"moderation_event_record_actions_ids\"\n    ON moderation_event(\"subjectDid\",\"subjectUri\", \"action\", \"createdAt\")\n    WHERE \"subjectUri\" IS NOT NULL\n    AND \"action\" IN ( 'tools.ozone.moderation.defs#modEventTakedown', 'tools.ozone.moderation.defs#modEventLabel')\n  `.execute(db)\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('moderation_event_account_reports_idx').execute()\n  await db.schema.dropIndex('moderation_event_record_reports_idx').execute()\n  await db.schema.dropIndex('moderation_event_account_actions_ids').execute()\n  await db.schema.dropIndex('moderation_event_record_actions_ids').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250211T132135150Z-moderation-event-message-partial-idx.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\n// support lookup for chat.bsky.moderation.getMessageContext\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // @NOTE: These queries should be run with the \"CONCURRENTLY\" option in\n  // production to avoid locking the table. This is not supported by Kysely.\n  await db.schema.dropIndex('moderation_event_message_id_index').execute()\n  await db.schema\n    .createIndex('moderation_event_message_id_idx')\n    .on('moderation_event')\n    // https://github.com/kysely-org/kysely/issues/302\n    .expression(\n      sql`\"subjectMessageId\") WHERE (\"subjectMessageId\" IS NOT NULL AND \"action\" = 'tools.ozone.moderation.defs#modEventReport'`,\n    )\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('moderation_event_message_id_idx').execute()\n  await db.schema\n    .createIndex('moderation_event_message_id_index')\n    .on('moderation_event')\n    .column('subjectMessageId')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250221T132135150Z-member-details.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('member').addColumn('handle', 'text').execute()\n  await db.schema\n    .alterTable('member')\n    .addColumn('displayName', 'text')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('member').dropColumn('handle').execute()\n  await db.schema.alterTable('member').dropColumn('displayName').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250404T201720309Z-subject-status-sort-idxs.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  const ref = db.dynamic.ref\n  await sql`CREATE INDEX moderation_subject_status_sort_idx ON ${ref('moderation_subject_status')} (${ref('lastReportedAt')} DESC NULLS LAST, ${ref('id')} DESC NULLS LAST);`.execute(\n    db,\n  )\n  await sql`CREATE INDEX moderation_subject_status_unreviewed_sort_idx ON ${ref('moderation_subject_status')} (${ref('lastReportedAt')} DESC NULLS LAST, ${ref('id')} DESC NULLS LAST) WHERE ${ref('reviewState')} = 'tools.ozone.moderation.defs#reviewNone';`.execute(\n    db,\n  )\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('moderation_subject_status_sort_idx').execute()\n  await db.schema\n    .dropIndex('moderation_subject_status_unreviewed_sort_idx')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250415T201720309Z-verification.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('verification')\n    .addColumn('uri', 'text', (col) => col.notNull().primaryKey())\n    .addColumn('cid', 'text', (col) => col.notNull())\n    .addColumn('issuer', 'text', (col) => col.notNull())\n    .addColumn('subject', 'text', (col) => col.notNull())\n    .addColumn('handle', 'text', (col) => col.notNull())\n    .addColumn('displayName', 'text', (col) => col.notNull())\n    .addColumn('revokeReason', 'text')\n    .addColumn('revokedBy', 'text')\n    .addColumn('revokedAt', 'text')\n    .addColumn('createdAt', 'text', (col) => col.notNull())\n    .addColumn('updatedAt', 'text', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .execute()\n  await db.schema\n    .createIndex('verification_issuer_idx')\n    .on('verification')\n    .column('issuer')\n    .execute()\n  await db.schema\n    .createIndex('verification_createdat_uri_idx')\n    .on('verification')\n    .columns(['createdAt', 'uri'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('verification').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250417T201720309Z-firehose-cursor.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('firehose_cursor')\n    .addColumn('service', 'text', (col) => col.primaryKey())\n    .addColumn('cursor', 'bigint')\n    .addColumn('updatedAt', 'text', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('firehose_cursor').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250609T110704000Z-safelink.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('safelink_event')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('eventType', 'varchar', (col) => col.notNull())\n    .addColumn('url', 'varchar', (col) => col.notNull())\n    .addColumn('pattern', 'varchar', (col) => col.notNull())\n    .addColumn('action', 'varchar', (col) => col.notNull())\n    .addColumn('reason', 'varchar', (col) => col.notNull())\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('comment', 'text')\n    .execute()\n\n  await db.schema\n    .createTable('safelink_rule')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('url', 'varchar', (col) => col.notNull())\n    .addColumn('pattern', 'varchar', (col) => col.notNull())\n    .addColumn('action', 'varchar', (col) => col.notNull())\n    .addColumn('reason', 'varchar', (col) => col.notNull())\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('comment', 'text')\n    .addUniqueConstraint('safelink_rule_url_pattern_key', ['url', 'pattern'])\n    .execute()\n\n  await db.schema\n    .createIndex('safelink_event_url_pattern_idx')\n    .on('safelink_event')\n    .columns(['url', 'pattern'])\n    .execute()\n\n  await db.schema\n    .createIndex('safelink_rule_action_idx')\n    .on('safelink_rule')\n    .column('action')\n    .execute()\n\n  await db.schema\n    .createIndex('safelink_rule_reason_idx')\n    .on('safelink_rule')\n    .column('reason')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('safelink_rule').execute()\n  await db.schema.dropTable('safelink_event').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('modTool', 'jsonb')\n    .execute()\n\n  await db.schema\n    .createIndex('moderation_event_mod_tool_name_idx')\n    .on('moderation_event')\n    .expression(sql`(\"modTool\" ->> 'name')`)\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('moderation_event').dropColumn('modTool').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250701T000000000Z-add-age-assurance-state.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('ageAssuranceState', 'varchar', (col) =>\n      col.notNull().defaultTo('unknown'),\n    )\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .addColumn('ageAssuranceUpdatedBy', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('ageAssuranceState')\n    .execute()\n  await db.schema\n    .alterTable('moderation_subject_status')\n    .dropColumn('ageAssuranceUpdatedBy')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250715T000000000Z-add-mod-event-external-id.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('externalId', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('externalId')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250718T150931000Z-update-appeal-reason-stats.ts",
    "content": "import { Kysely, sql } from 'kysely'\nimport { OZONE_APPEAL_REASON_TYPE } from '../../api/util'\nimport { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs'\nimport { DatabaseSchemaType } from '../schema'\nimport * as modEvent from '../schema/moderation_event'\nimport * as recordEventsStats from '../schema/record_events_stats'\n\nexport async function up(db: Kysely<any>): Promise<void> {\n  // Drop and recreate materialized views to update appeal reason counting\n  // to include both REASONAPPEAL and OZONE_APPEAL_REASON_TYPE\n  // The primary difference between the old and new query is that we were using = and != operators\n  // to match against the meta->>'reportType' field and now we use IN and NOT IN\n\n  // Drop existing materialized views in reverse dependency order\n  await db.schema\n    .dropView('account_record_events_stats')\n    .materialized()\n    .execute()\n  await db.schema.dropView('record_events_stats').materialized().execute()\n  await db.schema.dropView('account_events_stats').materialized().execute()\n\n  // Recreate account_events_stats with updated appeal counting\n  await db.schema\n    .createView('account_events_stats')\n    .materialized()\n    .as(\n      (db as Kysely<modEvent.PartialDB>)\n        .selectFrom('moderation_event')\n        .where('subjectType', '=', 'com.atproto.admin.defs#repoRef')\n        .where('subjectUri', 'is', null)\n        .select('subjectDid')\n        .select([\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'\n              AND ${eb.ref('durationInHours')} IS NULL\n            )`.as('takedownCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'\n              AND ${eb.ref('durationInHours')} IS NOT NULL\n            )`.as('suspendCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate'\n            )`.as('escalateCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'\n              AND ${eb.ref('meta')} ->> 'reportType' NOT IN (${REASONAPPEAL}, ${OZONE_APPEAL_REASON_TYPE})\n            )`.as('reportCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'\n              AND ${eb.ref('meta')} ->> 'reportType' IN (${REASONAPPEAL}, ${OZONE_APPEAL_REASON_TYPE})\n            )`.as('appealCount'),\n        ])\n        .groupBy('subjectDid'),\n    )\n    .execute()\n\n  // Recreate record_events_stats with updated appeal counting\n  await db.schema\n    .createView('record_events_stats')\n    .materialized()\n    .as(\n      (db as Kysely<modEvent.PartialDB>)\n        .selectFrom('moderation_event')\n        .select([\n          'subjectDid',\n          'subjectUri',\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate')`.as(\n              'escalateCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' NOT IN (${REASONAPPEAL}, ${OZONE_APPEAL_REASON_TYPE}))`.as(\n              'reportCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' IN (${REASONAPPEAL}, ${OZONE_APPEAL_REASON_TYPE}))`.as(\n              'appealCount',\n            ),\n        ])\n        .where('subjectType', '=', 'com.atproto.repo.strongRef')\n        .where('subjectUri', 'is not', null)\n        .groupBy(['subjectDid', 'subjectUri']),\n    )\n    .execute()\n\n  // Recreate account_record_events_stats (unchanged logic, but depends on record_events_stats)\n  await db.schema\n    .createView('account_record_events_stats')\n    .materialized()\n    .as(\n      (db as Kysely<recordEventsStats.PartialDB>)\n        .selectFrom('record_events_stats')\n        .select([\n          'subjectDid',\n          (eb) =>\n            sql<number>`SUM(${eb.ref('reportCount')})::bigint`.as(\n              'totalReports',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reportCount')} > 0)`.as(\n              'reportedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('escalateCount')} > 0)`.as(\n              'escalatedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('appealCount')} > 0)`.as(\n              'appealedCount',\n            ),\n        ])\n        .groupBy('subjectDid'),\n    )\n    .execute()\n\n  // Recreate all indexes for the materialized views\n  await db.schema\n    .createIndex('account_events_stats_did_idx')\n    .unique()\n    .on('account_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_events_stats_suspend_count_idx')\n    .on('account_events_stats')\n    .expression(sql`\"suspendCount\" ASC NULLS FIRST`)\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('record_events_stats_uri_idx')\n    .unique()\n    .on('record_events_stats')\n    .column('subjectUri')\n    .execute()\n\n  await db.schema\n    .createIndex('record_events_stats_did_idx')\n    .on('record_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_events_stats_did_idx')\n    .unique()\n    .on('account_record_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_events_stats_reported_count_idx')\n    .on('account_record_events_stats')\n    .expression(sql`\"reportedCount\" ASC NULLS FIRST`)\n    .column('subjectDid')\n    .execute()\n}\n\nexport async function down(db: Kysely<DatabaseSchemaType>): Promise<void> {\n  // Drop the updated materialized views\n  await db.schema\n    .dropView('account_record_events_stats')\n    .materialized()\n    .execute()\n  await db.schema.dropView('record_events_stats').materialized().execute()\n  await db.schema.dropView('account_events_stats').materialized().execute()\n\n  // Recreate the original views with single appeal reason type\n  await db.schema\n    .createView('account_events_stats')\n    .materialized()\n    .as(\n      (db as Kysely<modEvent.PartialDB>)\n        .selectFrom('moderation_event')\n        .where('subjectType', '=', 'com.atproto.admin.defs#repoRef')\n        .where('subjectUri', 'is', null)\n        .select('subjectDid')\n        .select([\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'\n              AND ${eb.ref('durationInHours')} IS NULL\n            )`.as('takedownCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'\n              AND ${eb.ref('durationInHours')} IS NOT NULL\n            )`.as('suspendCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate'\n            )`.as('escalateCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'\n              AND ${eb.ref('meta')} ->> 'reportType' != ${REASONAPPEAL}\n            )`.as('reportCount'),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER(\n              WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'\n              AND ${eb.ref('meta')} ->> 'reportType' = ${REASONAPPEAL}\n            )`.as('appealCount'),\n        ])\n        .groupBy('subjectDid'),\n    )\n    .execute()\n\n  await db.schema\n    .createView('record_events_stats')\n    .materialized()\n    .as(\n      (db as Kysely<modEvent.PartialDB>)\n        .selectFrom('moderation_event')\n        .select([\n          'subjectDid',\n          'subjectUri',\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate')`.as(\n              'escalateCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' != 'com.atproto.moderation.defs#reasonAppeal')`.as(\n              'reportCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' = 'com.atproto.moderation.defs#reasonAppeal')`.as(\n              'appealCount',\n            ),\n        ])\n        .where('subjectType', '=', 'com.atproto.repo.strongRef')\n        .where('subjectUri', 'is not', null)\n        .groupBy(['subjectDid', 'subjectUri']),\n    )\n    .execute()\n\n  await db.schema\n    .createView('account_record_events_stats')\n    .materialized()\n    .as(\n      (db as Kysely<recordEventsStats.PartialDB>)\n        .selectFrom('record_events_stats')\n        .select([\n          'subjectDid',\n          (eb) =>\n            sql<number>`SUM(${eb.ref('reportCount')})::bigint`.as(\n              'totalReports',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reportCount')} > 0)`.as(\n              'reportedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('escalateCount')} > 0)`.as(\n              'escalatedCount',\n            ),\n          (eb) =>\n            sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('appealCount')} > 0)`.as(\n              'appealedCount',\n            ),\n        ])\n        .groupBy('subjectDid'),\n    )\n    .execute()\n\n  // Recreate indexes\n  await db.schema\n    .createIndex('account_events_stats_did_idx')\n    .unique()\n    .on('account_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_events_stats_suspend_count_idx')\n    .on('account_events_stats')\n    .expression(sql`\"suspendCount\" ASC NULLS FIRST`)\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('record_events_stats_uri_idx')\n    .unique()\n    .on('record_events_stats')\n    .column('subjectUri')\n    .execute()\n\n  await db.schema\n    .createIndex('record_events_stats_did_idx')\n    .on('record_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_events_stats_did_idx')\n    .unique()\n    .on('account_record_events_stats')\n    .column('subjectDid')\n    .execute()\n\n  await db.schema\n    .createIndex('account_record_events_stats_reported_count_idx')\n    .on('account_record_events_stats')\n    .expression(sql`\"reportedCount\" ASC NULLS FIRST`)\n    .column('subjectDid')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250813T000000000Z-mod-tool-batch-id-index.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // Only small percentage of moderation events have a batchId in modTool meta property so we're creating a partial index\n  await sql`\n    CREATE INDEX moderation_event_mod_tool_batch_id_idx\n    ON moderation_event ((\"modTool\" -> 'meta' ->> 'batchId'))\n    WHERE \"modTool\" #> '{meta,batchId}' IS NOT NULL\n  `.execute(db)\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('moderation_event_mod_tool_batch_id_idx').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20250923T000000000Z-scheduled-actions.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('scheduled_action')\n    .addColumn('id', 'bigserial', (col) => col.primaryKey())\n    .addColumn('action', 'varchar', (col) => col.notNull())\n    .addColumn('eventData', 'jsonb')\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('executeAt', 'varchar')\n    .addColumn('executeAfter', 'varchar')\n    .addColumn('executeUntil', 'varchar')\n    .addColumn('randomizeExecution', 'boolean', (col) =>\n      col.notNull().defaultTo(false),\n    )\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('status', 'varchar', (col) => col.notNull().defaultTo('pending'))\n    .addColumn('lastExecutedAt', 'varchar')\n    .addColumn('lastFailureReason', 'text')\n    .addColumn('executionEventId', 'bigint')\n    .execute()\n\n  // Unique constraint to prevent multiple pending actions for the same subject\n  await sql`\n    CREATE UNIQUE INDEX scheduled_action_unique_pending_subject\n    ON scheduled_action (did, action)\n    WHERE status = 'pending'\n  `.execute(db)\n\n  // for task runner to query pending actions efficiently\n  await db.schema\n    .createIndex('scheduled_action_execute_time_idx')\n    .on('scheduled_action')\n    .columns(['executeAt', 'executeAfter', 'status'])\n    .execute()\n\n  // for querying actions by subject\n  await db.schema\n    .createIndex('scheduled_action_did_idx')\n    .on('scheduled_action')\n    .column('did')\n    .execute()\n\n  // we require status to be always passed when listing scheduled actions and use createdAt for pagination\n  await db.schema\n    .createIndex('scheduled_action_status_created_at_idx')\n    .on('scheduled_action')\n    .columns(['status', 'createdAt'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('scheduled_action').execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20251008T120000000Z-add-strike-system.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('severityLevel', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('strikeCount', 'integer')\n    .execute()\n  await db.schema\n    .alterTable('moderation_event')\n    .addColumn('strikeExpiresAt', 'varchar')\n    .execute()\n\n  await db.schema\n    .createTable('account_strike')\n    .addColumn('did', 'text', (col) => col.primaryKey())\n    .addColumn('firstStrikeAt', 'varchar')\n    .addColumn('lastStrikeAt', 'varchar')\n    .addColumn('activeStrikeCount', 'integer', (col) =>\n      col.notNull().defaultTo(0),\n    )\n    .addColumn('totalStrikeCount', 'integer', (col) =>\n      col.notNull().defaultTo(0),\n    )\n    .execute()\n\n  await db.schema\n    .createTable('job_cursor')\n    .addColumn('job', 'text', (col) => col.primaryKey())\n    .addColumn('cursor', 'text')\n    .addColumn('updatedAt', 'text', (col) =>\n      col.defaultTo(sql`now()`).notNull(),\n    )\n    .execute()\n\n  // This supports fast look up for background job that aggregates strike data per subjectDid\n  await db.schema\n    .createIndex('moderation_event_subject_did_strike_count_idx')\n    .on('moderation_event')\n    .columns(['subjectDid', 'strikeCount'])\n    .execute()\n\n  // This supports fast lookup in the background job that needs to find strikes that have expired\n  await sql`\n    CREATE INDEX moderation_event_strike_expires_at_strike_count_idx\n    ON moderation_event (\"strikeExpiresAt\", \"strikeCount\")\n    WHERE \"strikeExpiresAt\" IS NOT NULL AND \"strikeCount\" IS NOT NULL\n  `.execute(db)\n\n  // for sorting and filtering by active strike count\n  await db.schema\n    .createIndex('account_strike_active_count_idx')\n    .on('account_strike')\n    .column('activeStrikeCount')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .dropIndex('moderation_event_subject_did_strike_count_idx')\n    .execute()\n  await db.schema\n    .dropIndex('moderation_event_strike_expires_at_strike_count_idx')\n    .execute()\n  await db.schema.dropIndex('account_strike_active_count_idx').execute()\n\n  await db.schema.dropTable('account_strike').execute()\n  await db.schema.dropTable('job_cursor').execute()\n\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('severityLevel')\n    .execute()\n\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('strikeCount')\n    .execute()\n\n  await db.schema\n    .alterTable('moderation_event')\n    .dropColumn('strikeExpiresAt')\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/20260210T154806448Z-mod-event-created-by-indexes.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  // @NOTE: These queries should be run with the \"CONCURRENTLY\" option in\n  // production to avoid locking the table. This is not supported by Kysely.\n  await db.schema\n    .dropIndex('moderation_event_created_by_idx')\n    .ifExists()\n    .execute()\n  await db.schema\n    .createIndex('moderation_event_created_by_idx')\n    .on('moderation_event')\n    .columns(['createdBy', 'createdAt', 'id'])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .dropIndex('moderation_event_created_by_idx')\n    .ifExists()\n    .execute()\n}\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/index.ts",
    "content": "// NOTE this file can be edited by hand, but it is also appended to by the migration:create command.\n// It's important that every migration is exported from here with the proper name. We'd simplify\n// this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process.\n\nexport * as _20231219T205730722Z from './20231219T205730722Z-init'\nexport * as _20240116T085607200Z from './20240116T085607200Z-communication-template'\nexport * as _20240201T051104136Z from './20240201T051104136Z-mod-event-blobs'\nexport * as _20240208T213404429Z from './20240208T213404429Z-add-tags-column-to-moderation-subject'\nexport * as _20240228T003647759Z from './20240228T003647759Z-add-label-sigs'\nexport * as _20240408T192432676Z from './20240408T192432676Z-mute-reporting'\nexport * as _20240506T225055595Z from './20240506T225055595Z-message-subject'\nexport * as _20240430T211332580Z from './20240521T211332580Z-member'\nexport * as _20240814T003647759Z from './20240814T003647759Z-event-created-at-index'\nexport * as _20240903T205730722Z from './20240903T205730722Z-add-template-lang'\nexport * as _20240904T205730722Z from './20240904T205730722Z-add-subject-did-index'\nexport * as _20241001T205730722Z from './20241001T205730722Z-subject-status-review-state-index'\nexport * as _20241008T205730722Z from './20241008T205730722Z-sets'\nexport * as _20241018T205730722Z from './20241018T205730722Z-setting'\nexport * as _20241026T205730722Z from './20241026T205730722Z-add-hosting-status-to-subject-status'\nexport * as _20241220T144630860Z from './20241220T144630860Z-stats-materialized-views'\nexport * as _20250204T003647759Z from './20250204T003647759Z-add-subject-priority-score'\nexport * as _20250211T003647759Z from './20250211T003647759Z-add-reporter-stats-index'\nexport * as _20250211T132135150Z from './20250211T132135150Z-moderation-event-message-partial-idx'\nexport * as _20250221T132135150Z from './20250221T132135150Z-member-details'\nexport * as _20250404T201720309Z from './20250404T201720309Z-subject-status-sort-idxs'\nexport * as _20250415T201720309Z from './20250415T201720309Z-verification'\nexport * as _20250417T201720309Z from './20250417T201720309Z-firehose-cursor'\nexport * as _20250609T110704000Z from './20250609T110704000Z-safelink'\nexport * as _20250618T180246000Z from './20250618T180246000Z-add-mod-tool-to-moderation-event'\nexport * as _20250701T000000000Z from './20250701T000000000Z-add-age-assurance-state'\nexport * as _20250715T000000000Z from './20250715T000000000Z-add-mod-event-external-id'\nexport * as _20250718T150931000Z from './20250718T150931000Z-update-appeal-reason-stats'\nexport * as _20250813T000000000Z from './20250813T000000000Z-mod-tool-batch-id-index'\nexport * as _20250923T000000000Z from './20250923T000000000Z-scheduled-actions'\nexport * as _20251008T120000000Z from './20251008T120000000Z-add-strike-system'\nexport * as _20260210T154806448Z from './20260210T154806448Z-mod-event-created-by-indexes'\n"
  },
  {
    "path": "packages/ozone/src/db/migrations/provider.ts",
    "content": "import { Kysely, Migration, MigrationProvider } from 'kysely'\n\n// Passes a context argument to migrations. We use this to thread the dialect into migrations\n\nexport class CtxMigrationProvider<T> implements MigrationProvider {\n  constructor(\n    private migrations: Record<string, CtxMigration<T>>,\n    private ctx: T,\n  ) {}\n  async getMigrations(): Promise<Record<string, Migration>> {\n    const ctxMigrations: Record<string, Migration> = {}\n    Object.entries(this.migrations).forEach(([name, migration]) => {\n      ctxMigrations[name] = {\n        up: async (db) => await migration.up(db, this.ctx),\n        down: async (db) => await migration.down?.(db, this.ctx),\n      }\n    })\n    return ctxMigrations\n  }\n}\n\nexport interface CtxMigration<T> {\n  up(db: Kysely<unknown>, ctx: T): Promise<void>\n  down?(db: Kysely<unknown>, ctx: T): Promise<void>\n}\n"
  },
  {
    "path": "packages/ozone/src/db/pagination.ts",
    "content": "import { DynamicModule, sql } from 'kysely'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AnyQb, DbRef } from './types'\n\nexport type Cursor = { primary: string; secondary: string }\nexport type LabeledResult = {\n  primary: string | number\n  secondary: string | number\n}\n\n/**\n * The GenericKeyset is an abstract class that sets-up the interface and partial implementation\n * of a keyset-paginated cursor with two parts. There are three types involved:\n *  - Result: a raw result (i.e. a row from the db) containing data that will make-up a cursor.\n *    - E.g. { createdAt: '2022-01-01T12:00:00Z', cid: 'bafyx' }\n *  - LabeledResult: a Result processed such that the \"primary\" and \"secondary\" parts of the cursor are labeled.\n *    - E.g. { primary: '2022-01-01T12:00:00Z', secondary: 'bafyx' }\n *  - Cursor: the two string parts that make-up the packed/string cursor.\n *    - E.g. packed cursor '1641038400000::bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' }\n *\n * These types relate as such. Implementers define the relations marked with a *:\n *   Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor\n *                     ↳ SQL Condition\n */\nexport abstract class GenericKeyset<R, LR extends LabeledResult> {\n  constructor(\n    public primary: DbRef,\n    public secondary: DbRef,\n  ) {}\n  abstract labelResult(result: R): LR\n  abstract labeledResultToCursor(labeled: LR): Cursor\n  abstract cursorToLabeledResult(cursor: Cursor): LR\n  packFromResult(results: R | R[]): string | undefined {\n    const result = Array.isArray(results) ? results.at(-1) : results\n    if (!result) return\n    return this.pack(this.labelResult(result))\n  }\n  pack(labeled?: LR): string | undefined {\n    if (!labeled) return\n    const cursor = this.labeledResultToCursor(labeled)\n    return this.packCursor(cursor)\n  }\n  unpack(cursorStr?: string): LR | undefined {\n    const cursor = this.unpackCursor(cursorStr)\n    if (!cursor) return\n    return this.cursorToLabeledResult(cursor)\n  }\n  packCursor(cursor?: Cursor): string | undefined {\n    if (!cursor) return\n    return `${cursor.primary}::${cursor.secondary}`\n  }\n  unpackCursor(cursorStr?: string): Cursor | undefined {\n    if (!cursorStr) return\n    const result = cursorStr.split('::')\n    const [primary, secondary, ...others] = result\n    if (!primary || !secondary || others.length > 0) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary,\n      secondary,\n    }\n  }\n  getSql(labeled?: LR, direction?: 'asc' | 'desc', tryIndex?: boolean) {\n    if (labeled === undefined) return\n    if (tryIndex) {\n      // The tryIndex param will likely disappear and become the default implementation: here for now for gradual rollout query-by-query.\n      if (direction === 'asc') {\n        return sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}))`\n      } else {\n        return sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}))`\n      }\n    } else {\n      // @NOTE this implementation can struggle to use an index on (primary, secondary) for pagination due to the \"or\" usage.\n      if (direction === 'asc') {\n        return sql`((${this.primary} > ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} > ${labeled.secondary}))`\n      } else {\n        return sql`((${this.primary} < ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} < ${labeled.secondary}))`\n      }\n    }\n  }\n}\n\ntype StatusKeysetParam = {\n  lastReviewedAt: string | null\n  lastReportedAt: string | null\n  id: number\n}\n\nexport class StatusKeyset extends GenericKeyset<StatusKeysetParam, Cursor> {\n  labelResult(result: StatusKeysetParam): Cursor\n  labelResult(result: StatusKeysetParam) {\n    const primaryField = (\n      this.primary as ReturnType<DynamicModule['ref']>\n    ).dynamicReference.includes('lastReviewedAt')\n      ? 'lastReviewedAt'\n      : 'lastReportedAt'\n\n    return {\n      primary: result[primaryField]\n        ? new Date(`${result[primaryField]}`).getTime().toString()\n        : '',\n      secondary: result.id.toString(),\n    }\n  }\n  labeledResultToCursor(labeled: Cursor) {\n    return {\n      primary: labeled.primary,\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    return {\n      primary: cursor.primary\n        ? new Date(parseInt(cursor.primary, 10)).toISOString()\n        : '',\n      secondary: cursor.secondary,\n    }\n  }\n  unpackCursor(cursorStr?: string): Cursor | undefined {\n    if (!cursorStr) return\n    const result = cursorStr.split('::')\n    const [primary, secondary, ...others] = result\n    if (!secondary || others.length > 0) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary,\n      secondary,\n    }\n  }\n  // This is specifically built to handle nullable columns as primary sorting column\n  getSql(labeled?: Cursor, direction?: 'asc' | 'desc') {\n    if (labeled === undefined) return\n    if (direction === 'asc') {\n      return !labeled.primary\n        ? sql`(${this.primary} IS NULL AND ${this.secondary} > ${labeled.secondary})`\n        : sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}) OR (${this.primary} is null))`\n    } else {\n      return !labeled.primary\n        ? sql`(${this.primary} IS NULL AND ${this.secondary} < ${labeled.secondary})`\n        : sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}) OR (${this.primary} is null))`\n    }\n  }\n}\n\ntype TimeIdKeysetParam = {\n  id: number\n  createdAt: string | Date\n}\ntype TimeIdResult = TimeIdKeysetParam\n\nexport class TimeIdKeyset extends GenericKeyset<TimeIdKeysetParam, Cursor> {\n  labelResult(result: TimeIdResult): Cursor\n  labelResult(result: TimeIdResult) {\n    return { primary: result.createdAt, secondary: result.id.toString() }\n  }\n  labeledResultToCursor(labeled: Cursor) {\n    return {\n      primary: new Date(labeled.primary).getTime().toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    const primaryDate = new Date(parseInt(cursor.primary, 10))\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n      secondary: cursor.secondary,\n    }\n  }\n}\n\ntype CreatedAtUriKeysetParam = {\n  createdAt: string\n  uri: string\n}\n\nexport class CreatedAtUriKeyset extends GenericKeyset<\n  CreatedAtUriKeysetParam,\n  Cursor\n> {\n  labelResult(result: CreatedAtUriKeysetParam): Cursor\n  labelResult(result: CreatedAtUriKeysetParam) {\n    return { primary: result.createdAt, secondary: result.uri }\n  }\n  labeledResultToCursor(labeled: Cursor) {\n    return {\n      primary: new Date(labeled.primary).getTime().toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    const primaryDate = new Date(parseInt(cursor.primary, 10))\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n      secondary: cursor.secondary,\n    }\n  }\n}\n\nexport const paginate = <\n  QB extends AnyQb,\n  K extends GenericKeyset<unknown, any>,\n>(\n  qb: QB,\n  opts: {\n    limit?: number\n    cursor?: string\n    direction?: 'asc' | 'desc'\n    keyset: K\n    tryIndex?: boolean\n    // By default, pg does nullsFirst\n    nullsLast?: boolean\n  },\n): QB => {\n  const {\n    limit,\n    cursor,\n    keyset,\n    direction = 'desc',\n    tryIndex,\n    nullsLast,\n  } = opts\n  const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex)\n  return qb\n    .if(!!limit, (q) => q.limit(limit as number))\n    .if(!nullsLast, (q) =>\n      q.orderBy(keyset.primary, direction).orderBy(keyset.secondary, direction),\n    )\n    .if(!!nullsLast, (q) =>\n      q\n        .orderBy(\n          direction === 'asc'\n            ? sql`${keyset.primary} asc nulls last`\n            : sql`${keyset.primary} desc nulls last`,\n        )\n        .orderBy(\n          direction === 'asc'\n            ? sql`${keyset.secondary} asc nulls last`\n            : sql`${keyset.secondary} desc nulls last`,\n        ),\n    )\n    .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/account_events_stats.ts",
    "content": "import { GeneratedAlways, Selectable } from 'kysely'\n\nexport const tableName = 'account_events_stats'\n\nexport type AccountEventsStats = {\n  subjectDid: GeneratedAlways<string>\n  takedownCount: GeneratedAlways<number>\n  suspendCount: GeneratedAlways<number>\n  escalateCount: GeneratedAlways<number>\n  reportCount: GeneratedAlways<number>\n  appealCount: GeneratedAlways<number>\n}\n\nexport type AccountEventsStatsRow = Selectable<AccountEventsStats>\n\nexport type PartialDB = { [tableName]: AccountEventsStats }\n"
  },
  {
    "path": "packages/ozone/src/db/schema/account_record_events_stats.ts",
    "content": "import { GeneratedAlways, Selectable } from 'kysely'\n\nexport const tableName = 'account_record_events_stats'\n\ntype AccountRecordEventsStats = {\n  subjectDid: GeneratedAlways<string>\n  totalReports: GeneratedAlways<number>\n  reportedCount: GeneratedAlways<number>\n  escalatedCount: GeneratedAlways<number>\n  appealedCount: GeneratedAlways<number>\n}\n\nexport type AccountRecordEventsStatsRow = Selectable<AccountRecordEventsStats>\n\nexport type PartialDB = { [tableName]: AccountRecordEventsStats }\n"
  },
  {
    "path": "packages/ozone/src/db/schema/account_record_status_stats.ts",
    "content": "import { GeneratedAlways, Selectable } from 'kysely'\n\nexport const tableName = 'account_record_status_stats'\n\ntype AccountRecordStatusStats = {\n  did: GeneratedAlways<string>\n  subjectCount: GeneratedAlways<number>\n  pendingCount: GeneratedAlways<number>\n  processedCount: GeneratedAlways<number>\n  takendownCount: GeneratedAlways<number>\n}\n\nexport type AccountRecordStatusStatsRow = Selectable<AccountRecordStatusStats>\n\nexport type PartialDB = { [tableName]: AccountRecordStatusStats }\n"
  },
  {
    "path": "packages/ozone/src/db/schema/account_strike.ts",
    "content": "export const accountStrikeTableName = 'account_strike'\n\nexport interface AccountStrike {\n  did: string // Primary key\n  firstStrikeAt: string | null\n  lastStrikeAt: string | null\n  activeStrikeCount: number\n  totalStrikeCount: number\n}\n\nexport type PartialDB = {\n  [accountStrikeTableName]: AccountStrike\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/blob_push_event.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const eventTableName = 'blob_push_event'\n\nexport type BlobPushEventType = 'pds_takedown' | 'appview_takedown'\n\nexport interface BlobPushEvent {\n  id: Generated<number>\n  eventType: BlobPushEventType\n  subjectDid: string\n  subjectBlobCid: string\n  subjectUri: string | null\n  takedownRef: string | null\n  confirmedAt: Date | null\n  lastAttempted: Date | null\n  attempts: Generated<number>\n}\n\nexport type PartialDB = {\n  [eventTableName]: BlobPushEvent\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/communication_template.ts",
    "content": "import { Generated, GeneratedAlways } from 'kysely'\n\nexport const communicationTemplateTableName = 'communication_template'\n\nexport interface CommunicationTemplate {\n  id: GeneratedAlways<number>\n  name: string\n  contentMarkdown: string\n  subject: string | null\n  lang: string | null\n  disabled: Generated<boolean>\n  createdAt: Date\n  updatedAt: Date\n  lastUpdatedBy: string\n}\n\nexport type PartialDB = {\n  [communicationTemplateTableName]: CommunicationTemplate\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/firehose_cursor.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const firehoseCursorTableName = 'firehose_cursor'\n\nexport interface FirehoseCursor {\n  service: string\n  cursor: number | null\n  updatedAt: Generated<string>\n}\n\nexport type PartialDB = {\n  [firehoseCursorTableName]: FirehoseCursor\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/index.ts",
    "content": "import { Kysely } from 'kysely'\nimport * as accountEventsStats from './account_events_stats'\nimport * as accountRecordEventsStats from './account_record_events_stats'\nimport * as accountRecordStatusStats from './account_record_status_stats'\nimport * as accountStrike from './account_strike'\nimport * as blobPushEvent from './blob_push_event'\nimport * as communicationTemplate from './communication_template'\nimport * as firehoseCursor from './firehose_cursor'\nimport * as jobCursor from './job_cursor'\nimport * as label from './label'\nimport * as member from './member'\nimport * as modEvent from './moderation_event'\nimport * as modSubjectStatus from './moderation_subject_status'\nimport * as set from './ozone_set'\nimport * as recordEventsStats from './record_events_stats'\nimport * as recordPushEvent from './record_push_event'\nimport * as repoPushEvent from './repo_push_event'\nimport * as safelink from './safelink'\nimport * as scheduledAction from './scheduled-action'\nimport * as setting from './setting'\nimport * as signingKey from './signing_key'\nimport * as verification from './verification'\n\nexport type DatabaseSchemaType = modEvent.PartialDB &\n  modSubjectStatus.PartialDB &\n  label.PartialDB &\n  signingKey.PartialDB &\n  repoPushEvent.PartialDB &\n  recordPushEvent.PartialDB &\n  blobPushEvent.PartialDB &\n  communicationTemplate.PartialDB &\n  set.PartialDB &\n  member.PartialDB &\n  setting.PartialDB &\n  accountEventsStats.PartialDB &\n  recordEventsStats.PartialDB &\n  accountRecordEventsStats.PartialDB &\n  accountRecordStatusStats.PartialDB &\n  accountStrike.PartialDB &\n  verification.PartialDB &\n  firehoseCursor.PartialDB &\n  jobCursor.PartialDB &\n  safelink.PartialDB &\n  scheduledAction.PartialDB\n\nexport type DatabaseSchema = Kysely<DatabaseSchemaType>\n\nexport default DatabaseSchema\n"
  },
  {
    "path": "packages/ozone/src/db/schema/job_cursor.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const jobCursorTableName = 'job_cursor'\n\nexport interface JobCursor {\n  job: string\n  cursor: string | null\n  updatedAt: Generated<string>\n}\n\nexport type PartialDB = {\n  [jobCursorTableName]: JobCursor\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/label.ts",
    "content": "import { Generated, Selectable } from 'kysely'\n\nexport const tableName = 'label'\n\nexport interface Label {\n  id: Generated<number>\n  src: string\n  uri: string\n  cid: string\n  val: string\n  neg: boolean\n  cts: string\n  exp: string | null\n  sig: Buffer | null\n  signingKeyId: number | null\n}\n\nexport type LabelRow = Selectable<Label>\n\nexport type PartialDB = { [tableName]: Label }\n\nexport const LabelChannel = 'label_channel' // used with notify/listen\n"
  },
  {
    "path": "packages/ozone/src/db/schema/member.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const memberTableName = 'member'\n\nexport interface Member {\n  did: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleModerator'\n  disabled: Generated<boolean>\n  handle: string | null\n  displayName: string | null\n  createdAt: Date\n  updatedAt: Date\n  lastUpdatedBy: string\n}\n\nexport type PartialDB = {\n  [memberTableName]: Member\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/moderation_event.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const eventTableName = 'moderation_event'\n\nexport interface ModerationEvent {\n  id: Generated<number>\n  action:\n    | 'tools.ozone.moderation.defs#modEventTakedown'\n    | 'tools.ozone.moderation.defs#modEventAcknowledge'\n    | 'tools.ozone.moderation.defs#modEventEscalate'\n    | 'tools.ozone.moderation.defs#modEventComment'\n    | 'tools.ozone.moderation.defs#modEventLabel'\n    | 'tools.ozone.moderation.defs#modEventReport'\n    | 'tools.ozone.moderation.defs#modEventMute'\n    | 'tools.ozone.moderation.defs#modEventUnmute'\n    | 'tools.ozone.moderation.defs#modEventMuteReporter'\n    | 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n    | 'tools.ozone.moderation.defs#modEventReverseTakedown'\n    | 'tools.ozone.moderation.defs#modEventEmail'\n    | 'tools.ozone.moderation.defs#modEventResolveAppeal'\n    | 'tools.ozone.moderation.defs#modEventTag'\n    | 'tools.ozone.moderation.defs#accountEvent'\n    | 'tools.ozone.moderation.defs#identityEvent'\n    | 'tools.ozone.moderation.defs#recordEvent'\n    | 'tools.ozone.moderation.defs#modEventPriorityScore'\n    | 'tools.ozone.moderation.defs#ageAssuranceEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n    | 'tools.ozone.moderation.defs#ageAssurancePurgeEvent'\n    | 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n  subjectType:\n    | 'com.atproto.admin.defs#repoRef'\n    | 'com.atproto.repo.strongRef'\n    | 'chat.bsky.convo.defs#messageRef'\n  subjectDid: string\n  subjectUri: string | null\n  subjectCid: string | null\n  subjectBlobCids: string[] | null\n  subjectMessageId: string | null\n  createLabelVals: string | null\n  negateLabelVals: string | null\n  comment: string | null\n  createdAt: string\n  createdBy: string\n  durationInHours: number | null\n  expiresAt: string | null\n  meta: Record<string, string | boolean | number> | null\n  addedTags: string[] | null\n  removedTags: string[] | null\n  legacyRefId: number | null\n  modTool: { name: string; meta?: { [_ in string]: unknown } } | null\n  externalId: string | null\n  severityLevel: string | null\n  strikeCount: number | null\n  strikeExpiresAt: string | null\n}\n\nexport type PartialDB = {\n  [eventTableName]: ModerationEvent\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/moderation_subject_status.ts",
    "content": "import { Generated } from 'kysely'\nimport {\n  REVIEWCLOSED,\n  REVIEWESCALATED,\n  REVIEWNONE,\n  REVIEWOPEN,\n} from '../../lexicon/types/tools/ozone/moderation/defs'\n\nexport const subjectStatusTableName = 'moderation_subject_status'\n\nexport interface ModerationSubjectStatus {\n  id: Generated<number>\n  did: string\n  recordPath: string\n  recordCid: string | null\n  blobCids: string[] | null\n  reviewState:\n    | typeof REVIEWCLOSED\n    | typeof REVIEWOPEN\n    | typeof REVIEWESCALATED\n    | typeof REVIEWNONE\n  createdAt: string\n  updatedAt: string\n  lastReviewedBy: string | null\n  lastReviewedAt: string | null\n  lastReportedAt: string | null\n  lastAppealedAt: string | null\n  hostingUpdatedAt: string | null\n  hostingDeletedAt: string | null\n  hostingCreatedAt: string | null\n  hostingDeactivatedAt: string | null\n  hostingReactivatedAt: string | null\n  hostingStatus: string | null\n  muteUntil: string | null\n  muteReportingUntil: string | null\n  suspendUntil: string | null\n  takendown: boolean\n  appealed: boolean | null\n  comment: string | null\n  tags: string[] | null\n  priorityScore?: number\n  ageAssuranceState: string\n  ageAssuranceUpdatedBy?: string | null\n}\n\nexport type PartialDB = {\n  [subjectStatusTableName]: ModerationSubjectStatus\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/ozone_set.ts",
    "content": "import { Generated, GeneratedAlways } from 'kysely'\n\nexport const ozoneSetTableName = 'set_detail'\nexport const ozoneSetValueTableName = 'set_value'\n\nexport interface SetDetail {\n  id: GeneratedAlways<number>\n  name: string\n  description: string | null\n  createdAt: Generated<Date>\n  updatedAt: Generated<Date>\n}\n\nexport interface SetValue {\n  id: GeneratedAlways<number>\n  setId: number\n  value: string\n  createdAt: Generated<Date>\n}\n\nexport type PartialDB = {\n  [ozoneSetTableName]: SetDetail\n  [ozoneSetValueTableName]: SetValue\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/record_events_stats.ts",
    "content": "import { GeneratedAlways, Selectable } from 'kysely'\n\nexport const tableName = 'record_events_stats'\n\nexport type RecordEventsStats = {\n  subjectDid: GeneratedAlways<string>\n  subjectUri: GeneratedAlways<string>\n  escalateCount: GeneratedAlways<number>\n  reportCount: GeneratedAlways<number>\n  appealCount: GeneratedAlways<number>\n}\n\nexport type RecordEventsStatsRow = Selectable<RecordEventsStats>\n\nexport type PartialDB = { [tableName]: RecordEventsStats }\n"
  },
  {
    "path": "packages/ozone/src/db/schema/record_push_event.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const eventTableName = 'record_push_event'\n\nexport type RecordPushEventType = 'pds_takedown' | 'appview_takedown'\n\nexport interface RecordPushEvent {\n  id: Generated<number>\n  eventType: RecordPushEventType\n  subjectDid: string\n  subjectUri: string\n  subjectCid: string\n  takedownRef: string | null\n  confirmedAt: Date | null\n  lastAttempted: Date | null\n  attempts: Generated<number>\n}\n\nexport type PartialDB = {\n  [eventTableName]: RecordPushEvent\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/repo_push_event.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const eventTableName = 'repo_push_event'\n\nexport type RepoPushEventType = 'pds_takedown' | 'appview_takedown'\n\nexport interface RepoPushEvent {\n  id: Generated<number>\n  eventType: RepoPushEventType\n  subjectDid: string\n  takedownRef: string | null\n  confirmedAt: Date | null\n  lastAttempted: Date | null\n  attempts: Generated<number>\n}\n\nexport type PartialDB = {\n  [eventTableName]: RepoPushEvent\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/safelink.ts",
    "content": "import { GeneratedAlways } from 'kysely'\nimport {\n  SafelinkActionType,\n  SafelinkEventType,\n  SafelinkPatternType,\n  SafelinkReasonType,\n} from '../../api/util'\n\nexport const safelinkRuleTableName = 'safelink_rule'\nexport const safelinkEventTableName = 'safelink_event'\n\nexport interface SafelinkRule {\n  id: GeneratedAlways<number>\n  url: string\n  pattern: SafelinkPatternType\n  action: SafelinkActionType\n  reason: SafelinkReasonType\n  createdBy: string\n  createdAt: string\n  updatedAt: string\n  comment: string | null\n}\n\nexport interface SafelinkEvent {\n  id: GeneratedAlways<number>\n  eventType: SafelinkEventType\n  url: string\n  pattern: SafelinkPatternType\n  action: SafelinkActionType\n  reason: SafelinkReasonType\n  createdBy: string\n  createdAt: string\n  comment: string | null\n}\n\nexport type PartialDB = {\n  [safelinkRuleTableName]: SafelinkRule\n  [safelinkEventTableName]: SafelinkEvent\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/scheduled-action.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport const scheduledActionTableName = 'scheduled_action'\n\nexport interface ScheduledAction {\n  id: GeneratedAlways<number>\n  action: string\n  eventData: unknown | null\n  did: string\n  executeAt: string | null\n  executeAfter: string | null\n  executeUntil: string | null\n  randomizeExecution: boolean\n  createdBy: string\n  createdAt: string\n  updatedAt: string\n  status: string\n  lastExecutedAt: string | null\n  lastFailureReason: string | null\n  executionEventId: number | null\n}\n\nexport type PartialDB = {\n  [scheduledActionTableName]: ScheduledAction\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/setting.ts",
    "content": "import { Generated, GeneratedAlways } from 'kysely'\nimport { Member } from './member'\n\nexport const settingTableName = 'setting'\n\nexport type SettingScope = 'personal' | 'instance'\n\nexport interface Setting {\n  id: GeneratedAlways<number>\n  key: string\n  value: Record<string, unknown>\n  managerRole: Member['role'] | null\n  description: string | null\n  did: string\n  scope: SettingScope\n  lastUpdatedBy: string\n  createdBy: string\n  createdAt: Generated<Date>\n  updatedAt: Generated<Date>\n}\n\nexport type PartialDB = {\n  [settingTableName]: Setting\n}\n"
  },
  {
    "path": "packages/ozone/src/db/schema/signing_key.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const tableName = 'signing_key'\n\nexport interface SigningKey {\n  id: Generated<number>\n  key: string\n}\n\nexport type PartialDB = { [tableName]: SigningKey }\n"
  },
  {
    "path": "packages/ozone/src/db/schema/verification.ts",
    "content": "import { Generated } from 'kysely'\n\nexport const verificationTableName = 'verification'\n\nexport interface Verification {\n  uri: string\n  cid: string\n  issuer: string\n  subject: string\n  handle: string\n  displayName: string\n  revokeReason: string | null\n  revokedBy: string | null\n  revokedAt: string | null\n  createdAt: string\n  updatedAt: Generated<string>\n}\n\nexport type PartialDB = {\n  [verificationTableName]: Verification\n}\n"
  },
  {
    "path": "packages/ozone/src/db/types.ts",
    "content": "import { DynamicModule, RawBuilder, SelectQueryBuilder, sql } from 'kysely'\nimport { Pool as PgPool } from 'pg'\n\nexport type DbRef = RawBuilder | ReturnType<DynamicModule['ref']>\n\nexport type AnyQb = SelectQueryBuilder<any, any, any>\n\nexport type PgOptions = {\n  url: string\n  pool?: PgPool\n  schema?: string\n  poolSize?: number\n  poolMaxUses?: number\n  poolIdleTimeoutMs?: number\n}\n\nexport const jsonb = <T>(val: T) => {\n  if (val === null) return sql<T>`null`\n  return sql<T>`${JSON.stringify(val)}::jsonb`\n}\n"
  },
  {
    "path": "packages/ozone/src/error.ts",
    "content": "import { ErrorRequestHandler } from 'express'\nimport { XRPCError } from '@atproto/xrpc-server'\nimport { httpLogger as log } from './logger'\n\nexport const handler: ErrorRequestHandler = (err, _req, res, next) => {\n  log.error({ err }, 'unexpected internal server error')\n  if (res.headersSent) {\n    return next(err)\n  }\n  const serverError = XRPCError.fromError(err)\n  res.status(serverError.type).json(serverError.payload)\n}\n"
  },
  {
    "path": "packages/ozone/src/image-invalidator.ts",
    "content": "// Invalidation is a general interface for propagating an image blob\n// takedown through any caches where a representation of it may be stored.\n// @NOTE this does not remove the blob from storage: just invalidates it from caches.\n// @NOTE keep in sync with same interface in aws/src/cloudfront.ts\nexport interface ImageInvalidator {\n  invalidate(subject: string, paths: string[]): Promise<void>\n}\n"
  },
  {
    "path": "packages/ozone/src/index.ts",
    "content": "import events from 'node:events'\nimport http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport compression from 'compression'\nimport cors from 'cors'\nimport express from 'express'\nimport { HttpTerminator, createHttpTerminator } from 'http-terminator'\nimport { DAY, SECOND } from '@atproto/common'\nimport API, { health, wellKnown } from './api'\nimport { OzoneConfig, OzoneSecrets } from './config'\nimport { AppContext, AppContextOptions } from './context'\nimport { Member } from './db/schema/member'\nimport * as error from './error'\nimport { createServer } from './lexicon'\nimport { dbLogger, loggerMiddleware } from './logger'\n\nexport * from './config'\nexport { type ImageInvalidator } from './image-invalidator'\nexport { Database } from './db'\nexport { EventPusher, EventReverser, OzoneDaemon } from './daemon'\nexport { AppContext } from './context'\nexport { httpLogger } from './logger'\n\nexport class OzoneService {\n  public ctx: AppContext\n  public app: express.Application\n  public server?: http.Server\n  private terminator?: HttpTerminator\n  private dbStatsInterval?: NodeJS.Timeout\n\n  constructor(opts: { ctx: AppContext; app: express.Application }) {\n    this.ctx = opts.ctx\n    this.app = opts.app\n  }\n\n  static async create(\n    cfg: OzoneConfig,\n    secrets: OzoneSecrets,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<OzoneService> {\n    const app = express()\n    app.set('trust proxy', true)\n    app.use(cors({ maxAge: DAY / SECOND }))\n    app.use(loggerMiddleware)\n    app.use(compression())\n\n    const ctx = await AppContext.fromConfig(cfg, secrets, overrides)\n\n    let server = createServer({\n      validateResponse: false,\n      payload: {\n        jsonLimit: 100 * 1024, // 100kb\n        textLimit: 100 * 1024, // 100kb\n        blobLimit: 5 * 1024 * 1024, // 5mb\n      },\n    })\n\n    server = API(server, ctx)\n\n    app.use(health.createRouter(ctx))\n    app.use(wellKnown.createRouter(ctx))\n    app.use(server.xrpc.router)\n    app.use(error.handler)\n\n    return new OzoneService({ ctx, app })\n  }\n\n  async seedInitialMembers() {\n    const members: Array<{ role: Member['role']; did: string }> = []\n    this.ctx.cfg.access.admins.forEach((did) =>\n      members.push({\n        role: 'tools.ozone.team.defs#roleAdmin',\n        did,\n      }),\n    )\n    this.ctx.cfg.access.triage.forEach((did) =>\n      members.push({\n        role: 'tools.ozone.team.defs#roleTriage',\n        did,\n      }),\n    )\n    this.ctx.cfg.access.moderators.forEach((did) =>\n      members.push({\n        role: 'tools.ozone.team.defs#roleModerator',\n        did,\n      }),\n    )\n\n    for (const member of members) {\n      const service = this.ctx.teamService(this.ctx.db)\n      await service.upsert({\n        ...member,\n        lastUpdatedBy: this.ctx.cfg.service.did,\n      })\n    }\n  }\n\n  async start(): Promise<http.Server> {\n    if (this.dbStatsInterval) {\n      throw new Error(`${this.constructor.name} already started`)\n    }\n\n    // Any moderator that are configured via env var may not exist in the database\n    // so we need to sync them from env var to the database\n    await this.seedInitialMembers()\n\n    const { db, backgroundQueue } = this.ctx\n    this.dbStatsInterval = setInterval(() => {\n      dbLogger.info(\n        {\n          idleCount: db.pool.idleCount,\n          totalCount: db.pool.totalCount,\n          waitingCount: db.pool.waitingCount,\n        },\n        'db pool stats',\n      )\n      dbLogger.info(backgroundQueue.getStats(), 'background queue stats')\n    }, 10000)\n    await this.ctx.sequencer.start()\n    const server = this.app.listen(this.ctx.cfg.service.port)\n    this.server = server\n    server.keepAliveTimeout = 90000\n    this.terminator = createHttpTerminator({ server })\n    await events.once(server, 'listening')\n    const { port } = server.address() as AddressInfo\n    this.ctx.assignPort(port)\n    return server\n  }\n\n  async destroy(): Promise<void> {\n    await this.terminator?.terminate()\n    await this.ctx.backgroundQueue.destroy()\n    await this.ctx.sequencer.destroy()\n    await this.ctx.db.close()\n    clearInterval(this.dbStatsInterval)\n    this.dbStatsInterval = undefined\n  }\n}\n\nexport default OzoneService\n"
  },
  {
    "path": "packages/ozone/src/jetstream/service.ts",
    "content": "import { WebSocketKeepAlive } from '@atproto/ws-client'\n\ntype JetstreamRecord = Record<string, unknown>\ntype OnCreateCallback<T extends JetstreamRecord> = (\n  e: CommitCreateEvent<T>,\n) => Promise<void>\n\nexport type JetstreamOptions = {\n  endpoint: string\n  /**\n   * The record collections that you want to receive updates for.\n   * Leave this empty to receive updates for all record collections.\n   */\n  wantedCollections?: string[]\n  /**\n   * The DIDs that you want to receive updates for.\n   * Leave this empty to receive updates for all DIDs.\n   */\n  wantedDids?: string[]\n\n  /**\n   * The Unix timestamp in microseconds that you want to receive updates from.\n   */\n  cursor?: number\n}\nexport type EventBase = {\n  did: string\n  time_us: number\n  // @TODO: Limited to just commit events for now\n  kind: 'commit'\n}\nexport type CommitBase = {\n  collection: string\n  rkey: string\n  cid: string\n}\nexport interface CommitCreateEvent<RecordType extends JetstreamRecord>\n  extends EventBase {\n  kind: 'commit'\n  commit: {\n    operation: 'create'\n    record: RecordType\n  } & CommitBase\n}\n\nexport interface CommitDeleteEvent extends EventBase {\n  kind: 'commit'\n  commit: {\n    operation: 'delete'\n  } & CommitBase\n}\n\nexport class Jetstream {\n  public ws?: WebSocketKeepAlive\n  public url: URL\n  /** The current cursor. */\n  public cursor?: number\n\n  constructor(opts: JetstreamOptions) {\n    this.url = new URL(opts.endpoint)\n    opts.wantedCollections?.forEach((collection) => {\n      this.url.searchParams.append('wantedCollections', collection)\n    })\n    opts.wantedDids?.forEach((did) => {\n      this.url.searchParams.append('wantedDids', did)\n    })\n    if (opts.cursor) this.cursor = opts.cursor\n  }\n\n  async start(options: {\n    onCreate?: Record<string, OnCreateCallback<any>>\n    onDelete?: Record<string, (e: CommitDeleteEvent) => Promise<void>>\n  }) {\n    this.ws = new WebSocketKeepAlive({\n      getUrl: async () => {\n        if (this.cursor)\n          this.url.searchParams.set('cursor', this.cursor.toString())\n        return this.url.toString()\n      },\n    })\n\n    for await (const message of this.ws) {\n      const parsedMessage = JSON.parse(message.toString())\n      if (parsedMessage.kind === 'commit') {\n        const { collection, operation, record } = parsedMessage.commit || {}\n\n        if (operation === 'create') {\n          options.onCreate?.[collection]?.(\n            parsedMessage as CommitCreateEvent<typeof record>,\n          )\n        } else if (operation === 'delete') {\n          options.onDelete?.[collection]?.(parsedMessage as CommitDeleteEvent)\n        }\n      }\n    }\n  }\n\n  /**\n   * Closes the WebSocket connection.\n   */\n  close() {\n    this.ws?.ws?.close()\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/index.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type Auth,\n  type Options as XrpcOptions,\n  Server as XrpcServer,\n  type StreamConfigOrHandler,\n  type MethodConfigOrHandler,\n  createServer as createXrpcServer,\n} from '@atproto/xrpc-server'\nimport { schemas } from './lexicons.js'\nimport * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences.js'\nimport * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile.js'\nimport * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles.js'\nimport * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions.js'\nimport * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences.js'\nimport * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors.js'\nimport * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead.js'\nimport * as AppBskyAgeassuranceBegin from './types/app/bsky/ageassurance/begin.js'\nimport * as AppBskyAgeassuranceGetConfig from './types/app/bsky/ageassurance/getConfig.js'\nimport * as AppBskyAgeassuranceGetState from './types/app/bsky/ageassurance/getState.js'\nimport * as AppBskyBookmarkCreateBookmark from './types/app/bsky/bookmark/createBookmark.js'\nimport * as AppBskyBookmarkDeleteBookmark from './types/app/bsky/bookmark/deleteBookmark.js'\nimport * as AppBskyBookmarkGetBookmarks from './types/app/bsky/bookmark/getBookmarks.js'\nimport * as AppBskyContactDismissMatch from './types/app/bsky/contact/dismissMatch.js'\nimport * as AppBskyContactGetMatches from './types/app/bsky/contact/getMatches.js'\nimport * as AppBskyContactGetSyncStatus from './types/app/bsky/contact/getSyncStatus.js'\nimport * as AppBskyContactImportContacts from './types/app/bsky/contact/importContacts.js'\nimport * as AppBskyContactRemoveData from './types/app/bsky/contact/removeData.js'\nimport * as AppBskyContactSendNotification from './types/app/bsky/contact/sendNotification.js'\nimport * as AppBskyContactStartPhoneVerification from './types/app/bsky/contact/startPhoneVerification.js'\nimport * as AppBskyContactVerifyPhone from './types/app/bsky/contact/verifyPhone.js'\nimport * as AppBskyDraftCreateDraft from './types/app/bsky/draft/createDraft.js'\nimport * as AppBskyDraftDeleteDraft from './types/app/bsky/draft/deleteDraft.js'\nimport * as AppBskyDraftGetDrafts from './types/app/bsky/draft/getDrafts.js'\nimport * as AppBskyDraftUpdateDraft from './types/app/bsky/draft/updateDraft.js'\nimport * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator.js'\nimport * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds.js'\nimport * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes.js'\nimport * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed.js'\nimport * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed.js'\nimport * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator.js'\nimport * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators.js'\nimport * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'\nimport * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'\nimport * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'\nimport * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'\nimport * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'\nimport * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'\nimport * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'\nimport * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'\nimport * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline.js'\nimport * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts.js'\nimport * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions.js'\nimport * as AppBskyGraphGetActorStarterPacks from './types/app/bsky/graph/getActorStarterPacks.js'\nimport * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks.js'\nimport * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers.js'\nimport * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows.js'\nimport * as AppBskyGraphGetKnownFollowers from './types/app/bsky/graph/getKnownFollowers.js'\nimport * as AppBskyGraphGetList from './types/app/bsky/graph/getList.js'\nimport * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks.js'\nimport * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes.js'\nimport * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists.js'\nimport * as AppBskyGraphGetListsWithMembership from './types/app/bsky/graph/getListsWithMembership.js'\nimport * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes.js'\nimport * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelationships.js'\nimport * as AppBskyGraphGetStarterPack from './types/app/bsky/graph/getStarterPack.js'\nimport * as AppBskyGraphGetStarterPacks from './types/app/bsky/graph/getStarterPacks.js'\nimport * as AppBskyGraphGetStarterPacksWithMembership from './types/app/bsky/graph/getStarterPacksWithMembership.js'\nimport * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor.js'\nimport * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor.js'\nimport * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList.js'\nimport * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread.js'\nimport * as AppBskyGraphSearchStarterPacks from './types/app/bsky/graph/searchStarterPacks.js'\nimport * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'\nimport * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'\nimport * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'\nimport * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'\nimport * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'\nimport * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'\nimport * as AppBskyNotificationListActivitySubscriptions from './types/app/bsky/notification/listActivitySubscriptions.js'\nimport * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'\nimport * as AppBskyNotificationPutActivitySubscription from './types/app/bsky/notification/putActivitySubscription.js'\nimport * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'\nimport * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'\nimport * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'\nimport * as AppBskyNotificationUnregisterPush from './types/app/bsky/notification/unregisterPush.js'\nimport * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'\nimport * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'\nimport * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacks from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'\nimport * as AppBskyUnspeccedGetPostThreadOtherV2 from './types/app/bsky/unspecced/getPostThreadOtherV2.js'\nimport * as AppBskyUnspeccedGetPostThreadV2 from './types/app/bsky/unspecced/getPostThreadV2.js'\nimport * as AppBskyUnspeccedGetSuggestedFeeds from './types/app/bsky/unspecced/getSuggestedFeeds.js'\nimport * as AppBskyUnspeccedGetSuggestedFeedsSkeleton from './types/app/bsky/unspecced/getSuggestedFeedsSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedOnboardingUsers from './types/app/bsky/unspecced/getSuggestedOnboardingUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacks from './types/app/bsky/unspecced/getSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedUsers from './types/app/bsky/unspecced/getSuggestedUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedUsersSkeleton from './types/app/bsky/unspecced/getSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspecced/getSuggestionsSkeleton.js'\nimport * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions.js'\nimport * as AppBskyUnspeccedGetTrendingTopics from './types/app/bsky/unspecced/getTrendingTopics.js'\nimport * as AppBskyUnspeccedGetTrends from './types/app/bsky/unspecced/getTrends.js'\nimport * as AppBskyUnspeccedGetTrendsSkeleton from './types/app/bsky/unspecced/getTrendsSkeleton.js'\nimport * as AppBskyUnspeccedInitAgeAssurance from './types/app/bsky/unspecced/initAgeAssurance.js'\nimport * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton.js'\nimport * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton.js'\nimport * as AppBskyUnspeccedSearchStarterPacksSkeleton from './types/app/bsky/unspecced/searchStarterPacksSkeleton.js'\nimport * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus.js'\nimport * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits.js'\nimport * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo.js'\nimport * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount.js'\nimport * as ChatBskyActorExportAccountData from './types/chat/bsky/actor/exportAccountData.js'\nimport * as ChatBskyConvoAcceptConvo from './types/chat/bsky/convo/acceptConvo.js'\nimport * as ChatBskyConvoAddReaction from './types/chat/bsky/convo/addReaction.js'\nimport * as ChatBskyConvoDeleteMessageForSelf from './types/chat/bsky/convo/deleteMessageForSelf.js'\nimport * as ChatBskyConvoGetConvo from './types/chat/bsky/convo/getConvo.js'\nimport * as ChatBskyConvoGetConvoAvailability from './types/chat/bsky/convo/getConvoAvailability.js'\nimport * as ChatBskyConvoGetConvoForMembers from './types/chat/bsky/convo/getConvoForMembers.js'\nimport * as ChatBskyConvoGetLog from './types/chat/bsky/convo/getLog.js'\nimport * as ChatBskyConvoGetMessages from './types/chat/bsky/convo/getMessages.js'\nimport * as ChatBskyConvoLeaveConvo from './types/chat/bsky/convo/leaveConvo.js'\nimport * as ChatBskyConvoListConvos from './types/chat/bsky/convo/listConvos.js'\nimport * as ChatBskyConvoMuteConvo from './types/chat/bsky/convo/muteConvo.js'\nimport * as ChatBskyConvoRemoveReaction from './types/chat/bsky/convo/removeReaction.js'\nimport * as ChatBskyConvoSendMessage from './types/chat/bsky/convo/sendMessage.js'\nimport * as ChatBskyConvoSendMessageBatch from './types/chat/bsky/convo/sendMessageBatch.js'\nimport * as ChatBskyConvoUnmuteConvo from './types/chat/bsky/convo/unmuteConvo.js'\nimport * as ChatBskyConvoUpdateAllRead from './types/chat/bsky/convo/updateAllRead.js'\nimport * as ChatBskyConvoUpdateRead from './types/chat/bsky/convo/updateRead.js'\nimport * as ChatBskyModerationGetActorMetadata from './types/chat/bsky/moderation/getActorMetadata.js'\nimport * as ChatBskyModerationGetMessageContext from './types/chat/bsky/moderation/getMessageContext.js'\nimport * as ChatBskyModerationUpdateActorAccess from './types/chat/bsky/moderation/updateActorAccess.js'\nimport * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount.js'\nimport * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites.js'\nimport * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes.js'\nimport * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites.js'\nimport * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo.js'\nimport * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos.js'\nimport * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes.js'\nimport * as ComAtprotoAdminGetSubjectStatus from './types/com/atproto/admin/getSubjectStatus.js'\nimport * as ComAtprotoAdminSearchAccounts from './types/com/atproto/admin/searchAccounts.js'\nimport * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail.js'\nimport * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail.js'\nimport * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle.js'\nimport * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword.js'\nimport * as ComAtprotoAdminUpdateAccountSigningKey from './types/com/atproto/admin/updateAccountSigningKey.js'\nimport * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus.js'\nimport * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials.js'\nimport * as ComAtprotoIdentityRefreshIdentity from './types/com/atproto/identity/refreshIdentity.js'\nimport * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature.js'\nimport * as ComAtprotoIdentityResolveDid from './types/com/atproto/identity/resolveDid.js'\nimport * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle.js'\nimport * as ComAtprotoIdentityResolveIdentity from './types/com/atproto/identity/resolveIdentity.js'\nimport * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation.js'\nimport * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation.js'\nimport * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle.js'\nimport * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels.js'\nimport * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels.js'\nimport * as ComAtprotoLexiconResolveLexicon from './types/com/atproto/lexicon/resolveLexicon.js'\nimport * as ComAtprotoModerationCreateReport from './types/com/atproto/moderation/createReport.js'\nimport * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'\nimport * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js'\nimport * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js'\nimport * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js'\nimport * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js'\nimport * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js'\nimport * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js'\nimport * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js'\nimport * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js'\nimport * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'\nimport * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount.js'\nimport * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus.js'\nimport * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail.js'\nimport * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount.js'\nimport * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword.js'\nimport * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode.js'\nimport * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes.js'\nimport * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession.js'\nimport * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount.js'\nimport * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount.js'\nimport * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession.js'\nimport * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer.js'\nimport * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes.js'\nimport * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth.js'\nimport * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession.js'\nimport * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords.js'\nimport * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession.js'\nimport * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete.js'\nimport * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation.js'\nimport * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate.js'\nimport * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset.js'\nimport * as ComAtprotoServerReserveSigningKey from './types/com/atproto/server/reserveSigningKey.js'\nimport * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword.js'\nimport * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword.js'\nimport * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail.js'\nimport * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob.js'\nimport * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks.js'\nimport * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout.js'\nimport * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead.js'\nimport * as ComAtprotoSyncGetHostStatus from './types/com/atproto/sync/getHostStatus.js'\nimport * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit.js'\nimport * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord.js'\nimport * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo.js'\nimport * as ComAtprotoSyncGetRepoStatus from './types/com/atproto/sync/getRepoStatus.js'\nimport * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs.js'\nimport * as ComAtprotoSyncListHosts from './types/com/atproto/sync/listHosts.js'\nimport * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos.js'\nimport * as ComAtprotoSyncListReposByCollection from './types/com/atproto/sync/listReposByCollection.js'\nimport * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate.js'\nimport * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl.js'\nimport * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos.js'\nimport * as ComAtprotoTempAddReservedHandle from './types/com/atproto/temp/addReservedHandle.js'\nimport * as ComAtprotoTempCheckHandleAvailability from './types/com/atproto/temp/checkHandleAvailability.js'\nimport * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue.js'\nimport * as ComAtprotoTempDereferenceScope from './types/com/atproto/temp/dereferenceScope.js'\nimport * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels.js'\nimport * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification.js'\nimport * as ComAtprotoTempRevokeAccountCredentials from './types/com/atproto/temp/revokeAccountCredentials.js'\nimport * as ToolsOzoneCommunicationCreateTemplate from './types/tools/ozone/communication/createTemplate.js'\nimport * as ToolsOzoneCommunicationDeleteTemplate from './types/tools/ozone/communication/deleteTemplate.js'\nimport * as ToolsOzoneCommunicationListTemplates from './types/tools/ozone/communication/listTemplates.js'\nimport * as ToolsOzoneCommunicationUpdateTemplate from './types/tools/ozone/communication/updateTemplate.js'\nimport * as ToolsOzoneHostingGetAccountHistory from './types/tools/ozone/hosting/getAccountHistory.js'\nimport * as ToolsOzoneModerationCancelScheduledActions from './types/tools/ozone/moderation/cancelScheduledActions.js'\nimport * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent.js'\nimport * as ToolsOzoneModerationGetAccountTimeline from './types/tools/ozone/moderation/getAccountTimeline.js'\nimport * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent.js'\nimport * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord.js'\nimport * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords.js'\nimport * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/getRepo.js'\nimport * as ToolsOzoneModerationGetReporterStats from './types/tools/ozone/moderation/getReporterStats.js'\nimport * as ToolsOzoneModerationGetRepos from './types/tools/ozone/moderation/getRepos.js'\nimport * as ToolsOzoneModerationGetSubjects from './types/tools/ozone/moderation/getSubjects.js'\nimport * as ToolsOzoneModerationListScheduledActions from './types/tools/ozone/moderation/listScheduledActions.js'\nimport * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents.js'\nimport * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses.js'\nimport * as ToolsOzoneModerationScheduleAction from './types/tools/ozone/moderation/scheduleAction.js'\nimport * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos.js'\nimport * as ToolsOzoneSafelinkAddRule from './types/tools/ozone/safelink/addRule.js'\nimport * as ToolsOzoneSafelinkQueryEvents from './types/tools/ozone/safelink/queryEvents.js'\nimport * as ToolsOzoneSafelinkQueryRules from './types/tools/ozone/safelink/queryRules.js'\nimport * as ToolsOzoneSafelinkRemoveRule from './types/tools/ozone/safelink/removeRule.js'\nimport * as ToolsOzoneSafelinkUpdateRule from './types/tools/ozone/safelink/updateRule.js'\nimport * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig.js'\nimport * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues.js'\nimport * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet.js'\nimport * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues.js'\nimport * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues.js'\nimport * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets.js'\nimport * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet.js'\nimport * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions.js'\nimport * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions.js'\nimport * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption.js'\nimport * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation.js'\nimport * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts.js'\nimport * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts.js'\nimport * as ToolsOzoneTeamAddMember from './types/tools/ozone/team/addMember.js'\nimport * as ToolsOzoneTeamDeleteMember from './types/tools/ozone/team/deleteMember.js'\nimport * as ToolsOzoneTeamListMembers from './types/tools/ozone/team/listMembers.js'\nimport * as ToolsOzoneTeamUpdateMember from './types/tools/ozone/team/updateMember.js'\nimport * as ToolsOzoneVerificationGrantVerifications from './types/tools/ozone/verification/grantVerifications.js'\nimport * as ToolsOzoneVerificationListVerifications from './types/tools/ozone/verification/listVerifications.js'\nimport * as ToolsOzoneVerificationRevokeVerifications from './types/tools/ozone/verification/revokeVerifications.js'\n\nexport const APP_BSKY_ACTOR = {\n  StatusLive: 'app.bsky.actor.status#live',\n}\nexport const APP_BSKY_FEED = {\n  DefsRequestLess: 'app.bsky.feed.defs#requestLess',\n  DefsRequestMore: 'app.bsky.feed.defs#requestMore',\n  DefsClickthroughItem: 'app.bsky.feed.defs#clickthroughItem',\n  DefsClickthroughAuthor: 'app.bsky.feed.defs#clickthroughAuthor',\n  DefsClickthroughReposter: 'app.bsky.feed.defs#clickthroughReposter',\n  DefsClickthroughEmbed: 'app.bsky.feed.defs#clickthroughEmbed',\n  DefsContentModeUnspecified: 'app.bsky.feed.defs#contentModeUnspecified',\n  DefsContentModeVideo: 'app.bsky.feed.defs#contentModeVideo',\n  DefsInteractionSeen: 'app.bsky.feed.defs#interactionSeen',\n  DefsInteractionLike: 'app.bsky.feed.defs#interactionLike',\n  DefsInteractionRepost: 'app.bsky.feed.defs#interactionRepost',\n  DefsInteractionReply: 'app.bsky.feed.defs#interactionReply',\n  DefsInteractionQuote: 'app.bsky.feed.defs#interactionQuote',\n  DefsInteractionShare: 'app.bsky.feed.defs#interactionShare',\n}\nexport const APP_BSKY_GRAPH = {\n  DefsModlist: 'app.bsky.graph.defs#modlist',\n  DefsCuratelist: 'app.bsky.graph.defs#curatelist',\n  DefsReferencelist: 'app.bsky.graph.defs#referencelist',\n}\nexport const COM_ATPROTO_MODERATION = {\n  DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam',\n  DefsReasonViolation: 'com.atproto.moderation.defs#reasonViolation',\n  DefsReasonMisleading: 'com.atproto.moderation.defs#reasonMisleading',\n  DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',\n  DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',\n  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',\n  DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',\n}\nexport const TOOLS_OZONE_MODERATION = {\n  DefsReviewOpen: 'tools.ozone.moderation.defs#reviewOpen',\n  DefsReviewEscalated: 'tools.ozone.moderation.defs#reviewEscalated',\n  DefsReviewClosed: 'tools.ozone.moderation.defs#reviewClosed',\n  DefsReviewNone: 'tools.ozone.moderation.defs#reviewNone',\n  DefsTimelineEventPlcCreate:\n    'tools.ozone.moderation.defs#timelineEventPlcCreate',\n  DefsTimelineEventPlcOperation:\n    'tools.ozone.moderation.defs#timelineEventPlcOperation',\n  DefsTimelineEventPlcTombstone:\n    'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n}\nexport const TOOLS_OZONE_REPORT = {\n  DefsReasonAppeal: 'tools.ozone.report.defs#reasonAppeal',\n  DefsReasonOther: 'tools.ozone.report.defs#reasonOther',\n  DefsReasonViolenceAnimal: 'tools.ozone.report.defs#reasonViolenceAnimal',\n  DefsReasonViolenceThreats: 'tools.ozone.report.defs#reasonViolenceThreats',\n  DefsReasonViolenceGraphicContent:\n    'tools.ozone.report.defs#reasonViolenceGraphicContent',\n  DefsReasonViolenceGlorification:\n    'tools.ozone.report.defs#reasonViolenceGlorification',\n  DefsReasonViolenceExtremistContent:\n    'tools.ozone.report.defs#reasonViolenceExtremistContent',\n  DefsReasonViolenceTrafficking:\n    'tools.ozone.report.defs#reasonViolenceTrafficking',\n  DefsReasonViolenceOther: 'tools.ozone.report.defs#reasonViolenceOther',\n  DefsReasonSexualAbuseContent:\n    'tools.ozone.report.defs#reasonSexualAbuseContent',\n  DefsReasonSexualNCII: 'tools.ozone.report.defs#reasonSexualNCII',\n  DefsReasonSexualDeepfake: 'tools.ozone.report.defs#reasonSexualDeepfake',\n  DefsReasonSexualAnimal: 'tools.ozone.report.defs#reasonSexualAnimal',\n  DefsReasonSexualUnlabeled: 'tools.ozone.report.defs#reasonSexualUnlabeled',\n  DefsReasonSexualOther: 'tools.ozone.report.defs#reasonSexualOther',\n  DefsReasonChildSafetyCSAM: 'tools.ozone.report.defs#reasonChildSafetyCSAM',\n  DefsReasonChildSafetyGroom: 'tools.ozone.report.defs#reasonChildSafetyGroom',\n  DefsReasonChildSafetyPrivacy:\n    'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n  DefsReasonChildSafetyHarassment:\n    'tools.ozone.report.defs#reasonChildSafetyHarassment',\n  DefsReasonChildSafetyOther: 'tools.ozone.report.defs#reasonChildSafetyOther',\n  DefsReasonHarassmentTroll: 'tools.ozone.report.defs#reasonHarassmentTroll',\n  DefsReasonHarassmentTargeted:\n    'tools.ozone.report.defs#reasonHarassmentTargeted',\n  DefsReasonHarassmentHateSpeech:\n    'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n  DefsReasonHarassmentDoxxing:\n    'tools.ozone.report.defs#reasonHarassmentDoxxing',\n  DefsReasonHarassmentOther: 'tools.ozone.report.defs#reasonHarassmentOther',\n  DefsReasonMisleadingBot: 'tools.ozone.report.defs#reasonMisleadingBot',\n  DefsReasonMisleadingImpersonation:\n    'tools.ozone.report.defs#reasonMisleadingImpersonation',\n  DefsReasonMisleadingSpam: 'tools.ozone.report.defs#reasonMisleadingSpam',\n  DefsReasonMisleadingScam: 'tools.ozone.report.defs#reasonMisleadingScam',\n  DefsReasonMisleadingElections:\n    'tools.ozone.report.defs#reasonMisleadingElections',\n  DefsReasonMisleadingOther: 'tools.ozone.report.defs#reasonMisleadingOther',\n  DefsReasonRuleSiteSecurity: 'tools.ozone.report.defs#reasonRuleSiteSecurity',\n  DefsReasonRuleProhibitedSales:\n    'tools.ozone.report.defs#reasonRuleProhibitedSales',\n  DefsReasonRuleBanEvasion: 'tools.ozone.report.defs#reasonRuleBanEvasion',\n  DefsReasonRuleOther: 'tools.ozone.report.defs#reasonRuleOther',\n  DefsReasonSelfHarmContent: 'tools.ozone.report.defs#reasonSelfHarmContent',\n  DefsReasonSelfHarmED: 'tools.ozone.report.defs#reasonSelfHarmED',\n  DefsReasonSelfHarmStunts: 'tools.ozone.report.defs#reasonSelfHarmStunts',\n  DefsReasonSelfHarmSubstances:\n    'tools.ozone.report.defs#reasonSelfHarmSubstances',\n  DefsReasonSelfHarmOther: 'tools.ozone.report.defs#reasonSelfHarmOther',\n}\nexport const TOOLS_OZONE_TEAM = {\n  DefsRoleAdmin: 'tools.ozone.team.defs#roleAdmin',\n  DefsRoleModerator: 'tools.ozone.team.defs#roleModerator',\n  DefsRoleTriage: 'tools.ozone.team.defs#roleTriage',\n  DefsRoleVerifier: 'tools.ozone.team.defs#roleVerifier',\n}\n\nexport function createServer(options?: XrpcOptions): Server {\n  return new Server(options)\n}\n\nexport class Server {\n  xrpc: XrpcServer\n  app: AppNS\n  chat: ChatNS\n  com: ComNS\n  tools: ToolsNS\n\n  constructor(options?: XrpcOptions) {\n    this.xrpc = createXrpcServer(schemas, options)\n    this.app = new AppNS(this)\n    this.chat = new ChatNS(this)\n    this.com = new ComNS(this)\n    this.tools = new ToolsNS(this)\n  }\n}\n\nexport class AppNS {\n  _server: Server\n  bsky: AppBskyNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.bsky = new AppBskyNS(server)\n  }\n}\n\nexport class AppBskyNS {\n  _server: Server\n  actor: AppBskyActorNS\n  ageassurance: AppBskyAgeassuranceNS\n  bookmark: AppBskyBookmarkNS\n  contact: AppBskyContactNS\n  draft: AppBskyDraftNS\n  embed: AppBskyEmbedNS\n  feed: AppBskyFeedNS\n  graph: AppBskyGraphNS\n  labeler: AppBskyLabelerNS\n  notification: AppBskyNotificationNS\n  richtext: AppBskyRichtextNS\n  unspecced: AppBskyUnspeccedNS\n  video: AppBskyVideoNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.actor = new AppBskyActorNS(server)\n    this.ageassurance = new AppBskyAgeassuranceNS(server)\n    this.bookmark = new AppBskyBookmarkNS(server)\n    this.contact = new AppBskyContactNS(server)\n    this.draft = new AppBskyDraftNS(server)\n    this.embed = new AppBskyEmbedNS(server)\n    this.feed = new AppBskyFeedNS(server)\n    this.graph = new AppBskyGraphNS(server)\n    this.labeler = new AppBskyLabelerNS(server)\n    this.notification = new AppBskyNotificationNS(server)\n    this.richtext = new AppBskyRichtextNS(server)\n    this.unspecced = new AppBskyUnspeccedNS(server)\n    this.video = new AppBskyVideoNS(server)\n  }\n}\n\nexport class AppBskyActorNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetPreferences.QueryParams,\n      AppBskyActorGetPreferences.HandlerInput,\n      AppBskyActorGetPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getProfile<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetProfile.QueryParams,\n      AppBskyActorGetProfile.HandlerInput,\n      AppBskyActorGetProfile.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getProfile' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getProfiles<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetProfiles.QueryParams,\n      AppBskyActorGetProfiles.HandlerInput,\n      AppBskyActorGetProfiles.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getProfiles' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetSuggestions.QueryParams,\n      AppBskyActorGetSuggestions.HandlerInput,\n      AppBskyActorGetSuggestions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getSuggestions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorPutPreferences.QueryParams,\n      AppBskyActorPutPreferences.HandlerInput,\n      AppBskyActorPutPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.putPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActors<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorSearchActors.QueryParams,\n      AppBskyActorSearchActors.HandlerInput,\n      AppBskyActorSearchActors.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.searchActors' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActorsTypeahead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorSearchActorsTypeahead.QueryParams,\n      AppBskyActorSearchActorsTypeahead.HandlerInput,\n      AppBskyActorSearchActorsTypeahead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.searchActorsTypeahead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyAgeassuranceNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  begin<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceBegin.QueryParams,\n      AppBskyAgeassuranceBegin.HandlerInput,\n      AppBskyAgeassuranceBegin.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.begin' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceGetConfig.QueryParams,\n      AppBskyAgeassuranceGetConfig.HandlerInput,\n      AppBskyAgeassuranceGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getState<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceGetState.QueryParams,\n      AppBskyAgeassuranceGetState.HandlerInput,\n      AppBskyAgeassuranceGetState.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.getState' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyBookmarkNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createBookmark<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkCreateBookmark.QueryParams,\n      AppBskyBookmarkCreateBookmark.HandlerInput,\n      AppBskyBookmarkCreateBookmark.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.createBookmark' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteBookmark<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkDeleteBookmark.QueryParams,\n      AppBskyBookmarkDeleteBookmark.HandlerInput,\n      AppBskyBookmarkDeleteBookmark.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.deleteBookmark' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBookmarks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkGetBookmarks.QueryParams,\n      AppBskyBookmarkGetBookmarks.HandlerInput,\n      AppBskyBookmarkGetBookmarks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.getBookmarks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyContactNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  dismissMatch<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactDismissMatch.QueryParams,\n      AppBskyContactDismissMatch.HandlerInput,\n      AppBskyContactDismissMatch.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.dismissMatch' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMatches<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactGetMatches.QueryParams,\n      AppBskyContactGetMatches.HandlerInput,\n      AppBskyContactGetMatches.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.getMatches' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSyncStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactGetSyncStatus.QueryParams,\n      AppBskyContactGetSyncStatus.HandlerInput,\n      AppBskyContactGetSyncStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.getSyncStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  importContacts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactImportContacts.QueryParams,\n      AppBskyContactImportContacts.HandlerInput,\n      AppBskyContactImportContacts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.importContacts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeData<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactRemoveData.QueryParams,\n      AppBskyContactRemoveData.HandlerInput,\n      AppBskyContactRemoveData.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.removeData' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendNotification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactSendNotification.QueryParams,\n      AppBskyContactSendNotification.HandlerInput,\n      AppBskyContactSendNotification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.sendNotification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  startPhoneVerification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactStartPhoneVerification.QueryParams,\n      AppBskyContactStartPhoneVerification.HandlerInput,\n      AppBskyContactStartPhoneVerification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.startPhoneVerification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  verifyPhone<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactVerifyPhone.QueryParams,\n      AppBskyContactVerifyPhone.HandlerInput,\n      AppBskyContactVerifyPhone.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.verifyPhone' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyDraftNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftCreateDraft.QueryParams,\n      AppBskyDraftCreateDraft.HandlerInput,\n      AppBskyDraftCreateDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.createDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftDeleteDraft.QueryParams,\n      AppBskyDraftDeleteDraft.HandlerInput,\n      AppBskyDraftDeleteDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.deleteDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getDrafts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftGetDrafts.QueryParams,\n      AppBskyDraftGetDrafts.HandlerInput,\n      AppBskyDraftGetDrafts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.getDrafts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftUpdateDraft.QueryParams,\n      AppBskyDraftUpdateDraft.HandlerInput,\n      AppBskyDraftUpdateDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.updateDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyEmbedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n\nexport class AppBskyFeedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  describeFeedGenerator<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedDescribeFeedGenerator.QueryParams,\n      AppBskyFeedDescribeFeedGenerator.HandlerInput,\n      AppBskyFeedDescribeFeedGenerator.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getActorFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetActorFeeds.QueryParams,\n      AppBskyFeedGetActorFeeds.HandlerInput,\n      AppBskyFeedGetActorFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getActorFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getActorLikes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetActorLikes.QueryParams,\n      AppBskyFeedGetActorLikes.HandlerInput,\n      AppBskyFeedGetActorLikes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAuthorFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetAuthorFeed.QueryParams,\n      AppBskyFeedGetAuthorFeed.HandlerInput,\n      AppBskyFeedGetAuthorFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeed.QueryParams,\n      AppBskyFeedGetFeed.HandlerInput,\n      AppBskyFeedGetFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedGenerator<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedGenerator.QueryParams,\n      AppBskyFeedGetFeedGenerator.HandlerInput,\n      AppBskyFeedGetFeedGenerator.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedGenerator' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedGenerators<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedGenerators.QueryParams,\n      AppBskyFeedGetFeedGenerators.HandlerInput,\n      AppBskyFeedGetFeedGenerators.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedGenerators' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedSkeleton.QueryParams,\n      AppBskyFeedGetFeedSkeleton.HandlerInput,\n      AppBskyFeedGetFeedSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLikes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetLikes.QueryParams,\n      AppBskyFeedGetLikes.HandlerInput,\n      AppBskyFeedGetLikes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getLikes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetListFeed.QueryParams,\n      AppBskyFeedGetListFeed.HandlerInput,\n      AppBskyFeedGetListFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getListFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetPostThread.QueryParams,\n      AppBskyFeedGetPostThread.HandlerInput,\n      AppBskyFeedGetPostThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetPosts.QueryParams,\n      AppBskyFeedGetPosts.HandlerInput,\n      AppBskyFeedGetPosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getPosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getQuotes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetQuotes.QueryParams,\n      AppBskyFeedGetQuotes.HandlerInput,\n      AppBskyFeedGetQuotes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getQuotes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepostedBy<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetRepostedBy.QueryParams,\n      AppBskyFeedGetRepostedBy.HandlerInput,\n      AppBskyFeedGetRepostedBy.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getRepostedBy' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetSuggestedFeeds.QueryParams,\n      AppBskyFeedGetSuggestedFeeds.HandlerInput,\n      AppBskyFeedGetSuggestedFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getSuggestedFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTimeline<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetTimeline.QueryParams,\n      AppBskyFeedGetTimeline.HandlerInput,\n      AppBskyFeedGetTimeline.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchPosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedSearchPosts.QueryParams,\n      AppBskyFeedSearchPosts.HandlerInput,\n      AppBskyFeedSearchPosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.searchPosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendInteractions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedSendInteractions.QueryParams,\n      AppBskyFeedSendInteractions.HandlerInput,\n      AppBskyFeedSendInteractions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.sendInteractions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyGraphNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getActorStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetActorStarterPacks.QueryParams,\n      AppBskyGraphGetActorStarterPacks.HandlerInput,\n      AppBskyGraphGetActorStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getActorStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetBlocks.QueryParams,\n      AppBskyGraphGetBlocks.HandlerInput,\n      AppBskyGraphGetBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFollowers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetFollowers.QueryParams,\n      AppBskyGraphGetFollowers.HandlerInput,\n      AppBskyGraphGetFollowers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getFollowers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFollows<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetFollows.QueryParams,\n      AppBskyGraphGetFollows.HandlerInput,\n      AppBskyGraphGetFollows.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getFollows' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getKnownFollowers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetKnownFollowers.QueryParams,\n      AppBskyGraphGetKnownFollowers.HandlerInput,\n      AppBskyGraphGetKnownFollowers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getKnownFollowers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetList.QueryParams,\n      AppBskyGraphGetList.HandlerInput,\n      AppBskyGraphGetList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListBlocks.QueryParams,\n      AppBskyGraphGetListBlocks.HandlerInput,\n      AppBskyGraphGetListBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListMutes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListMutes.QueryParams,\n      AppBskyGraphGetListMutes.HandlerInput,\n      AppBskyGraphGetListMutes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListMutes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLists<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetLists.QueryParams,\n      AppBskyGraphGetLists.HandlerInput,\n      AppBskyGraphGetLists.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getLists' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListsWithMembership<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListsWithMembership.QueryParams,\n      AppBskyGraphGetListsWithMembership.HandlerInput,\n      AppBskyGraphGetListsWithMembership.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListsWithMembership' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMutes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetMutes.QueryParams,\n      AppBskyGraphGetMutes.HandlerInput,\n      AppBskyGraphGetMutes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getMutes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRelationships<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetRelationships.QueryParams,\n      AppBskyGraphGetRelationships.HandlerInput,\n      AppBskyGraphGetRelationships.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getRelationships' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPack<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPack.QueryParams,\n      AppBskyGraphGetStarterPack.HandlerInput,\n      AppBskyGraphGetStarterPack.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPack' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPacks.QueryParams,\n      AppBskyGraphGetStarterPacks.HandlerInput,\n      AppBskyGraphGetStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPacksWithMembership<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPacksWithMembership.QueryParams,\n      AppBskyGraphGetStarterPacksWithMembership.HandlerInput,\n      AppBskyGraphGetStarterPacksWithMembership.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPacksWithMembership' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFollowsByActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetSuggestedFollowsByActor.QueryParams,\n      AppBskyGraphGetSuggestedFollowsByActor.HandlerInput,\n      AppBskyGraphGetSuggestedFollowsByActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getSuggestedFollowsByActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteActor.QueryParams,\n      AppBskyGraphMuteActor.HandlerInput,\n      AppBskyGraphMuteActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteActorList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteActorList.QueryParams,\n      AppBskyGraphMuteActorList.HandlerInput,\n      AppBskyGraphMuteActorList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteActorList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteThread.QueryParams,\n      AppBskyGraphMuteThread.HandlerInput,\n      AppBskyGraphMuteThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphSearchStarterPacks.QueryParams,\n      AppBskyGraphSearchStarterPacks.HandlerInput,\n      AppBskyGraphSearchStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.searchStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteActor.QueryParams,\n      AppBskyGraphUnmuteActor.HandlerInput,\n      AppBskyGraphUnmuteActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteActorList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteActorList.QueryParams,\n      AppBskyGraphUnmuteActorList.HandlerInput,\n      AppBskyGraphUnmuteActorList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteThread.QueryParams,\n      AppBskyGraphUnmuteThread.HandlerInput,\n      AppBskyGraphUnmuteThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyLabelerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getServices<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyLabelerGetServices.QueryParams,\n      AppBskyLabelerGetServices.HandlerInput,\n      AppBskyLabelerGetServices.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.labeler.getServices' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyNotificationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationGetPreferences.QueryParams,\n      AppBskyNotificationGetPreferences.HandlerInput,\n      AppBskyNotificationGetPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.getPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getUnreadCount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationGetUnreadCount.QueryParams,\n      AppBskyNotificationGetUnreadCount.HandlerInput,\n      AppBskyNotificationGetUnreadCount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.getUnreadCount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listActivitySubscriptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationListActivitySubscriptions.QueryParams,\n      AppBskyNotificationListActivitySubscriptions.HandlerInput,\n      AppBskyNotificationListActivitySubscriptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.listActivitySubscriptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listNotifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationListNotifications.QueryParams,\n      AppBskyNotificationListNotifications.HandlerInput,\n      AppBskyNotificationListNotifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.listNotifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putActivitySubscription<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutActivitySubscription.QueryParams,\n      AppBskyNotificationPutActivitySubscription.HandlerInput,\n      AppBskyNotificationPutActivitySubscription.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putActivitySubscription' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutPreferences.QueryParams,\n      AppBskyNotificationPutPreferences.HandlerInput,\n      AppBskyNotificationPutPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferencesV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutPreferencesV2.QueryParams,\n      AppBskyNotificationPutPreferencesV2.HandlerInput,\n      AppBskyNotificationPutPreferencesV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putPreferencesV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  registerPush<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationRegisterPush.QueryParams,\n      AppBskyNotificationRegisterPush.HandlerInput,\n      AppBskyNotificationRegisterPush.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.registerPush' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unregisterPush<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationUnregisterPush.QueryParams,\n      AppBskyNotificationUnregisterPush.HandlerInput,\n      AppBskyNotificationUnregisterPush.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.unregisterPush' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateSeen<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationUpdateSeen.QueryParams,\n      AppBskyNotificationUpdateSeen.HandlerInput,\n      AppBskyNotificationUpdateSeen.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.updateSeen' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyRichtextNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n\nexport class AppBskyUnspeccedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getAgeAssuranceState<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetAgeAssuranceState.QueryParams,\n      AppBskyUnspeccedGetAgeAssuranceState.HandlerInput,\n      AppBskyUnspeccedGetAgeAssuranceState.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getAgeAssuranceState' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetConfig.QueryParams,\n      AppBskyUnspeccedGetConfig.HandlerInput,\n      AppBskyUnspeccedGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedUsersSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPopularFeedGenerators<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPopularFeedGenerators.QueryParams,\n      AppBskyUnspeccedGetPopularFeedGenerators.HandlerInput,\n      AppBskyUnspeccedGetPopularFeedGenerators.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThreadOtherV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPostThreadOtherV2.QueryParams,\n      AppBskyUnspeccedGetPostThreadOtherV2.HandlerInput,\n      AppBskyUnspeccedGetPostThreadOtherV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPostThreadOtherV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThreadV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPostThreadV2.QueryParams,\n      AppBskyUnspeccedGetPostThreadV2.HandlerInput,\n      AppBskyUnspeccedGetPostThreadV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPostThreadV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedFeeds.QueryParams,\n      AppBskyUnspeccedGetSuggestedFeeds.HandlerInput,\n      AppBskyUnspeccedGetSuggestedFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeedsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedFeedsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedOnboardingUsers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.QueryParams,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.HandlerInput,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedOnboardingUsers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedStarterPacks.QueryParams,\n      AppBskyUnspeccedGetSuggestedStarterPacks.HandlerInput,\n      AppBskyUnspeccedGetSuggestedStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedUsers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedUsers.QueryParams,\n      AppBskyUnspeccedGetSuggestedUsers.HandlerInput,\n      AppBskyUnspeccedGetSuggestedUsers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedUsers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedUsersSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedUsersSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestionsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestionsSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestionsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestionsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestionsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTaggedSuggestions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTaggedSuggestions.QueryParams,\n      AppBskyUnspeccedGetTaggedSuggestions.HandlerInput,\n      AppBskyUnspeccedGetTaggedSuggestions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrendingTopics<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrendingTopics.QueryParams,\n      AppBskyUnspeccedGetTrendingTopics.HandlerInput,\n      AppBskyUnspeccedGetTrendingTopics.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrendingTopics' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrends<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrends.QueryParams,\n      AppBskyUnspeccedGetTrends.HandlerInput,\n      AppBskyUnspeccedGetTrends.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrends' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrendsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrendsSkeleton.QueryParams,\n      AppBskyUnspeccedGetTrendsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetTrendsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrendsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  initAgeAssurance<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedInitAgeAssurance.QueryParams,\n      AppBskyUnspeccedInitAgeAssurance.HandlerInput,\n      AppBskyUnspeccedInitAgeAssurance.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.initAgeAssurance' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActorsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchActorsSkeleton.QueryParams,\n      AppBskyUnspeccedSearchActorsSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchActorsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchActorsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchPostsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchPostsSkeleton.QueryParams,\n      AppBskyUnspeccedSearchPostsSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchPostsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchPostsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyVideoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getJobStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoGetJobStatus.QueryParams,\n      AppBskyVideoGetJobStatus.HandlerInput,\n      AppBskyVideoGetJobStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.getJobStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getUploadLimits<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoGetUploadLimits.QueryParams,\n      AppBskyVideoGetUploadLimits.HandlerInput,\n      AppBskyVideoGetUploadLimits.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.getUploadLimits' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  uploadVideo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoUploadVideo.QueryParams,\n      AppBskyVideoUploadVideo.HandlerInput,\n      AppBskyVideoUploadVideo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.uploadVideo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatNS {\n  _server: Server\n  bsky: ChatBskyNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.bsky = new ChatBskyNS(server)\n  }\n}\n\nexport class ChatBskyNS {\n  _server: Server\n  actor: ChatBskyActorNS\n  convo: ChatBskyConvoNS\n  moderation: ChatBskyModerationNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.actor = new ChatBskyActorNS(server)\n    this.convo = new ChatBskyConvoNS(server)\n    this.moderation = new ChatBskyModerationNS(server)\n  }\n}\n\nexport class ChatBskyActorNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyActorDeleteAccount.QueryParams,\n      ChatBskyActorDeleteAccount.HandlerInput,\n      ChatBskyActorDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.actor.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  exportAccountData<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyActorExportAccountData.QueryParams,\n      ChatBskyActorExportAccountData.HandlerInput,\n      ChatBskyActorExportAccountData.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.actor.exportAccountData' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatBskyConvoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  acceptConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoAcceptConvo.QueryParams,\n      ChatBskyConvoAcceptConvo.HandlerInput,\n      ChatBskyConvoAcceptConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.acceptConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  addReaction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoAddReaction.QueryParams,\n      ChatBskyConvoAddReaction.HandlerInput,\n      ChatBskyConvoAddReaction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.addReaction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteMessageForSelf<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoDeleteMessageForSelf.QueryParams,\n      ChatBskyConvoDeleteMessageForSelf.HandlerInput,\n      ChatBskyConvoDeleteMessageForSelf.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.deleteMessageForSelf' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvo.QueryParams,\n      ChatBskyConvoGetConvo.HandlerInput,\n      ChatBskyConvoGetConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvoAvailability<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvoAvailability.QueryParams,\n      ChatBskyConvoGetConvoAvailability.HandlerInput,\n      ChatBskyConvoGetConvoAvailability.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvoAvailability' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvoForMembers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvoForMembers.QueryParams,\n      ChatBskyConvoGetConvoForMembers.HandlerInput,\n      ChatBskyConvoGetConvoForMembers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvoForMembers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLog<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetLog.QueryParams,\n      ChatBskyConvoGetLog.HandlerInput,\n      ChatBskyConvoGetLog.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getLog' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMessages<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetMessages.QueryParams,\n      ChatBskyConvoGetMessages.HandlerInput,\n      ChatBskyConvoGetMessages.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getMessages' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  leaveConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoLeaveConvo.QueryParams,\n      ChatBskyConvoLeaveConvo.HandlerInput,\n      ChatBskyConvoLeaveConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.leaveConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listConvos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoListConvos.QueryParams,\n      ChatBskyConvoListConvos.HandlerInput,\n      ChatBskyConvoListConvos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.listConvos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoMuteConvo.QueryParams,\n      ChatBskyConvoMuteConvo.HandlerInput,\n      ChatBskyConvoMuteConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.muteConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeReaction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoRemoveReaction.QueryParams,\n      ChatBskyConvoRemoveReaction.HandlerInput,\n      ChatBskyConvoRemoveReaction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.removeReaction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendMessage<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoSendMessage.QueryParams,\n      ChatBskyConvoSendMessage.HandlerInput,\n      ChatBskyConvoSendMessage.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.sendMessage' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendMessageBatch<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoSendMessageBatch.QueryParams,\n      ChatBskyConvoSendMessageBatch.HandlerInput,\n      ChatBskyConvoSendMessageBatch.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.sendMessageBatch' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUnmuteConvo.QueryParams,\n      ChatBskyConvoUnmuteConvo.HandlerInput,\n      ChatBskyConvoUnmuteConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.unmuteConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAllRead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUpdateAllRead.QueryParams,\n      ChatBskyConvoUpdateAllRead.HandlerInput,\n      ChatBskyConvoUpdateAllRead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.updateAllRead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateRead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUpdateRead.QueryParams,\n      ChatBskyConvoUpdateRead.HandlerInput,\n      ChatBskyConvoUpdateRead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.updateRead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatBskyModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getActorMetadata<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationGetActorMetadata.QueryParams,\n      ChatBskyModerationGetActorMetadata.HandlerInput,\n      ChatBskyModerationGetActorMetadata.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.getActorMetadata' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMessageContext<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationGetMessageContext.QueryParams,\n      ChatBskyModerationGetMessageContext.HandlerInput,\n      ChatBskyModerationGetMessageContext.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.getMessageContext' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateActorAccess<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationUpdateActorAccess.QueryParams,\n      ChatBskyModerationUpdateActorAccess.HandlerInput,\n      ChatBskyModerationUpdateActorAccess.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.updateActorAccess' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComNS {\n  _server: Server\n  atproto: ComAtprotoNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.atproto = new ComAtprotoNS(server)\n  }\n}\n\nexport class ComAtprotoNS {\n  _server: Server\n  admin: ComAtprotoAdminNS\n  identity: ComAtprotoIdentityNS\n  label: ComAtprotoLabelNS\n  lexicon: ComAtprotoLexiconNS\n  moderation: ComAtprotoModerationNS\n  repo: ComAtprotoRepoNS\n  server: ComAtprotoServerNS\n  sync: ComAtprotoSyncNS\n  temp: ComAtprotoTempNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.admin = new ComAtprotoAdminNS(server)\n    this.identity = new ComAtprotoIdentityNS(server)\n    this.label = new ComAtprotoLabelNS(server)\n    this.lexicon = new ComAtprotoLexiconNS(server)\n    this.moderation = new ComAtprotoModerationNS(server)\n    this.repo = new ComAtprotoRepoNS(server)\n    this.server = new ComAtprotoServerNS(server)\n    this.sync = new ComAtprotoSyncNS(server)\n    this.temp = new ComAtprotoTempNS(server)\n  }\n}\n\nexport class ComAtprotoAdminNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDeleteAccount.QueryParams,\n      ComAtprotoAdminDeleteAccount.HandlerInput,\n      ComAtprotoAdminDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  disableAccountInvites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDisableAccountInvites.QueryParams,\n      ComAtprotoAdminDisableAccountInvites.HandlerInput,\n      ComAtprotoAdminDisableAccountInvites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  disableInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDisableInviteCodes.QueryParams,\n      ComAtprotoAdminDisableInviteCodes.HandlerInput,\n      ComAtprotoAdminDisableInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.disableInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  enableAccountInvites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminEnableAccountInvites.QueryParams,\n      ComAtprotoAdminEnableAccountInvites.HandlerInput,\n      ComAtprotoAdminEnableAccountInvites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInfo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetAccountInfo.QueryParams,\n      ComAtprotoAdminGetAccountInfo.HandlerInput,\n      ComAtprotoAdminGetAccountInfo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getAccountInfo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInfos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetAccountInfos.QueryParams,\n      ComAtprotoAdminGetAccountInfos.HandlerInput,\n      ComAtprotoAdminGetAccountInfos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getAccountInfos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetInviteCodes.QueryParams,\n      ComAtprotoAdminGetInviteCodes.HandlerInput,\n      ComAtprotoAdminGetInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSubjectStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetSubjectStatus.QueryParams,\n      ComAtprotoAdminGetSubjectStatus.HandlerInput,\n      ComAtprotoAdminGetSubjectStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getSubjectStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminSearchAccounts.QueryParams,\n      ComAtprotoAdminSearchAccounts.HandlerInput,\n      ComAtprotoAdminSearchAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.searchAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminSendEmail.QueryParams,\n      ComAtprotoAdminSendEmail.HandlerInput,\n      ComAtprotoAdminSendEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountEmail.QueryParams,\n      ComAtprotoAdminUpdateAccountEmail.HandlerInput,\n      ComAtprotoAdminUpdateAccountEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountHandle.QueryParams,\n      ComAtprotoAdminUpdateAccountHandle.HandlerInput,\n      ComAtprotoAdminUpdateAccountHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountPassword.QueryParams,\n      ComAtprotoAdminUpdateAccountPassword.HandlerInput,\n      ComAtprotoAdminUpdateAccountPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountSigningKey<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountSigningKey.QueryParams,\n      ComAtprotoAdminUpdateAccountSigningKey.HandlerInput,\n      ComAtprotoAdminUpdateAccountSigningKey.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountSigningKey' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateSubjectStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateSubjectStatus.QueryParams,\n      ComAtprotoAdminUpdateSubjectStatus.HandlerInput,\n      ComAtprotoAdminUpdateSubjectStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateSubjectStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoIdentityNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getRecommendedDidCredentials<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityGetRecommendedDidCredentials.QueryParams,\n      ComAtprotoIdentityGetRecommendedDidCredentials.HandlerInput,\n      ComAtprotoIdentityGetRecommendedDidCredentials.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.getRecommendedDidCredentials' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  refreshIdentity<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityRefreshIdentity.QueryParams,\n      ComAtprotoIdentityRefreshIdentity.HandlerInput,\n      ComAtprotoIdentityRefreshIdentity.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.refreshIdentity' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPlcOperationSignature<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityRequestPlcOperationSignature.QueryParams,\n      ComAtprotoIdentityRequestPlcOperationSignature.HandlerInput,\n      ComAtprotoIdentityRequestPlcOperationSignature.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.requestPlcOperationSignature' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveDid<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveDid.QueryParams,\n      ComAtprotoIdentityResolveDid.HandlerInput,\n      ComAtprotoIdentityResolveDid.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveDid' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveHandle.QueryParams,\n      ComAtprotoIdentityResolveHandle.HandlerInput,\n      ComAtprotoIdentityResolveHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveIdentity<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveIdentity.QueryParams,\n      ComAtprotoIdentityResolveIdentity.HandlerInput,\n      ComAtprotoIdentityResolveIdentity.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveIdentity' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  signPlcOperation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentitySignPlcOperation.QueryParams,\n      ComAtprotoIdentitySignPlcOperation.HandlerInput,\n      ComAtprotoIdentitySignPlcOperation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.signPlcOperation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  submitPlcOperation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentitySubmitPlcOperation.QueryParams,\n      ComAtprotoIdentitySubmitPlcOperation.HandlerInput,\n      ComAtprotoIdentitySubmitPlcOperation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.submitPlcOperation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityUpdateHandle.QueryParams,\n      ComAtprotoIdentityUpdateHandle.HandlerInput,\n      ComAtprotoIdentityUpdateHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.updateHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoLabelNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  queryLabels<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoLabelQueryLabels.QueryParams,\n      ComAtprotoLabelQueryLabels.HandlerInput,\n      ComAtprotoLabelQueryLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.label.queryLabels' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  subscribeLabels<A extends Auth = void>(\n    cfg: StreamConfigOrHandler<\n      A,\n      ComAtprotoLabelSubscribeLabels.QueryParams,\n      ComAtprotoLabelSubscribeLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore\n    return this._server.xrpc.streamMethod(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoLexiconNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  resolveLexicon<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoLexiconResolveLexicon.QueryParams,\n      ComAtprotoLexiconResolveLexicon.HandlerInput,\n      ComAtprotoLexiconResolveLexicon.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.lexicon.resolveLexicon' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createReport<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoModerationCreateReport.QueryParams,\n      ComAtprotoModerationCreateReport.HandlerInput,\n      ComAtprotoModerationCreateReport.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.moderation.createReport' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoRepoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  applyWrites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoApplyWrites.QueryParams,\n      ComAtprotoRepoApplyWrites.HandlerInput,\n      ComAtprotoRepoApplyWrites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.applyWrites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoCreateRecord.QueryParams,\n      ComAtprotoRepoCreateRecord.HandlerInput,\n      ComAtprotoRepoCreateRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.createRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoDeleteRecord.QueryParams,\n      ComAtprotoRepoDeleteRecord.HandlerInput,\n      ComAtprotoRepoDeleteRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.deleteRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  describeRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoDescribeRepo.QueryParams,\n      ComAtprotoRepoDescribeRepo.HandlerInput,\n      ComAtprotoRepoDescribeRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.describeRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoGetRecord.QueryParams,\n      ComAtprotoRepoGetRecord.HandlerInput,\n      ComAtprotoRepoGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  importRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoImportRepo.QueryParams,\n      ComAtprotoRepoImportRepo.HandlerInput,\n      ComAtprotoRepoImportRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.importRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listMissingBlobs<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoListMissingBlobs.QueryParams,\n      ComAtprotoRepoListMissingBlobs.HandlerInput,\n      ComAtprotoRepoListMissingBlobs.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.listMissingBlobs' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listRecords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoListRecords.QueryParams,\n      ComAtprotoRepoListRecords.HandlerInput,\n      ComAtprotoRepoListRecords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.listRecords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoPutRecord.QueryParams,\n      ComAtprotoRepoPutRecord.HandlerInput,\n      ComAtprotoRepoPutRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.putRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  uploadBlob<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoUploadBlob.QueryParams,\n      ComAtprotoRepoUploadBlob.HandlerInput,\n      ComAtprotoRepoUploadBlob.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoServerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  activateAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerActivateAccount.QueryParams,\n      ComAtprotoServerActivateAccount.HandlerInput,\n      ComAtprotoServerActivateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.activateAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkAccountStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCheckAccountStatus.QueryParams,\n      ComAtprotoServerCheckAccountStatus.HandlerInput,\n      ComAtprotoServerCheckAccountStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.checkAccountStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  confirmEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerConfirmEmail.QueryParams,\n      ComAtprotoServerConfirmEmail.HandlerInput,\n      ComAtprotoServerConfirmEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateAccount.QueryParams,\n      ComAtprotoServerCreateAccount.HandlerInput,\n      ComAtprotoServerCreateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createAppPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateAppPassword.QueryParams,\n      ComAtprotoServerCreateAppPassword.HandlerInput,\n      ComAtprotoServerCreateAppPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createAppPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createInviteCode<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateInviteCode.QueryParams,\n      ComAtprotoServerCreateInviteCode.HandlerInput,\n      ComAtprotoServerCreateInviteCode.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createInviteCode' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateInviteCodes.QueryParams,\n      ComAtprotoServerCreateInviteCodes.HandlerInput,\n      ComAtprotoServerCreateInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateSession.QueryParams,\n      ComAtprotoServerCreateSession.HandlerInput,\n      ComAtprotoServerCreateSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deactivateAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeactivateAccount.QueryParams,\n      ComAtprotoServerDeactivateAccount.HandlerInput,\n      ComAtprotoServerDeactivateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deactivateAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeleteAccount.QueryParams,\n      ComAtprotoServerDeleteAccount.HandlerInput,\n      ComAtprotoServerDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeleteSession.QueryParams,\n      ComAtprotoServerDeleteSession.HandlerInput,\n      ComAtprotoServerDeleteSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deleteSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  describeServer<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDescribeServer.QueryParams,\n      ComAtprotoServerDescribeServer.HandlerInput,\n      ComAtprotoServerDescribeServer.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.describeServer' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetAccountInviteCodes.QueryParams,\n      ComAtprotoServerGetAccountInviteCodes.HandlerInput,\n      ComAtprotoServerGetAccountInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getAccountInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getServiceAuth<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetServiceAuth.QueryParams,\n      ComAtprotoServerGetServiceAuth.HandlerInput,\n      ComAtprotoServerGetServiceAuth.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getServiceAuth' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetSession.QueryParams,\n      ComAtprotoServerGetSession.HandlerInput,\n      ComAtprotoServerGetSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listAppPasswords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerListAppPasswords.QueryParams,\n      ComAtprotoServerListAppPasswords.HandlerInput,\n      ComAtprotoServerListAppPasswords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.listAppPasswords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  refreshSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRefreshSession.QueryParams,\n      ComAtprotoServerRefreshSession.HandlerInput,\n      ComAtprotoServerRefreshSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.refreshSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestAccountDelete<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestAccountDelete.QueryParams,\n      ComAtprotoServerRequestAccountDelete.HandlerInput,\n      ComAtprotoServerRequestAccountDelete.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestAccountDelete' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestEmailConfirmation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestEmailConfirmation.QueryParams,\n      ComAtprotoServerRequestEmailConfirmation.HandlerInput,\n      ComAtprotoServerRequestEmailConfirmation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestEmailUpdate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestEmailUpdate.QueryParams,\n      ComAtprotoServerRequestEmailUpdate.HandlerInput,\n      ComAtprotoServerRequestEmailUpdate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPasswordReset<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestPasswordReset.QueryParams,\n      ComAtprotoServerRequestPasswordReset.HandlerInput,\n      ComAtprotoServerRequestPasswordReset.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestPasswordReset' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  reserveSigningKey<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerReserveSigningKey.QueryParams,\n      ComAtprotoServerReserveSigningKey.HandlerInput,\n      ComAtprotoServerReserveSigningKey.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.reserveSigningKey' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resetPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerResetPassword.QueryParams,\n      ComAtprotoServerResetPassword.HandlerInput,\n      ComAtprotoServerResetPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.resetPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeAppPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRevokeAppPassword.QueryParams,\n      ComAtprotoServerRevokeAppPassword.HandlerInput,\n      ComAtprotoServerRevokeAppPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerUpdateEmail.QueryParams,\n      ComAtprotoServerUpdateEmail.HandlerInput,\n      ComAtprotoServerUpdateEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.updateEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoSyncNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getBlob<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetBlob.QueryParams,\n      ComAtprotoSyncGetBlob.HandlerInput,\n      ComAtprotoSyncGetBlob.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getBlob' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetBlocks.QueryParams,\n      ComAtprotoSyncGetBlocks.HandlerInput,\n      ComAtprotoSyncGetBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getCheckout<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetCheckout.QueryParams,\n      ComAtprotoSyncGetCheckout.HandlerInput,\n      ComAtprotoSyncGetCheckout.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getCheckout' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getHead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetHead.QueryParams,\n      ComAtprotoSyncGetHead.HandlerInput,\n      ComAtprotoSyncGetHead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getHead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getHostStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetHostStatus.QueryParams,\n      ComAtprotoSyncGetHostStatus.HandlerInput,\n      ComAtprotoSyncGetHostStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getHostStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLatestCommit<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetLatestCommit.QueryParams,\n      ComAtprotoSyncGetLatestCommit.HandlerInput,\n      ComAtprotoSyncGetLatestCommit.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getLatestCommit' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRecord.QueryParams,\n      ComAtprotoSyncGetRecord.HandlerInput,\n      ComAtprotoSyncGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRepo.QueryParams,\n      ComAtprotoSyncGetRepo.HandlerInput,\n      ComAtprotoSyncGetRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepoStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRepoStatus.QueryParams,\n      ComAtprotoSyncGetRepoStatus.HandlerInput,\n      ComAtprotoSyncGetRepoStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRepoStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listBlobs<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListBlobs.QueryParams,\n      ComAtprotoSyncListBlobs.HandlerInput,\n      ComAtprotoSyncListBlobs.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listBlobs' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listHosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListHosts.QueryParams,\n      ComAtprotoSyncListHosts.HandlerInput,\n      ComAtprotoSyncListHosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listHosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListRepos.QueryParams,\n      ComAtprotoSyncListRepos.HandlerInput,\n      ComAtprotoSyncListRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listReposByCollection<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListReposByCollection.QueryParams,\n      ComAtprotoSyncListReposByCollection.HandlerInput,\n      ComAtprotoSyncListReposByCollection.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listReposByCollection' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  notifyOfUpdate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncNotifyOfUpdate.QueryParams,\n      ComAtprotoSyncNotifyOfUpdate.HandlerInput,\n      ComAtprotoSyncNotifyOfUpdate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.notifyOfUpdate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestCrawl<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncRequestCrawl.QueryParams,\n      ComAtprotoSyncRequestCrawl.HandlerInput,\n      ComAtprotoSyncRequestCrawl.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.requestCrawl' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  subscribeRepos<A extends Auth = void>(\n    cfg: StreamConfigOrHandler<\n      A,\n      ComAtprotoSyncSubscribeRepos.QueryParams,\n      ComAtprotoSyncSubscribeRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.subscribeRepos' // @ts-ignore\n    return this._server.xrpc.streamMethod(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoTempNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addReservedHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempAddReservedHandle.QueryParams,\n      ComAtprotoTempAddReservedHandle.HandlerInput,\n      ComAtprotoTempAddReservedHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.addReservedHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkHandleAvailability<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempCheckHandleAvailability.QueryParams,\n      ComAtprotoTempCheckHandleAvailability.HandlerInput,\n      ComAtprotoTempCheckHandleAvailability.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.checkHandleAvailability' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkSignupQueue<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempCheckSignupQueue.QueryParams,\n      ComAtprotoTempCheckSignupQueue.HandlerInput,\n      ComAtprotoTempCheckSignupQueue.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.checkSignupQueue' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  dereferenceScope<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempDereferenceScope.QueryParams,\n      ComAtprotoTempDereferenceScope.HandlerInput,\n      ComAtprotoTempDereferenceScope.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.dereferenceScope' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  fetchLabels<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempFetchLabels.QueryParams,\n      ComAtprotoTempFetchLabels.HandlerInput,\n      ComAtprotoTempFetchLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.fetchLabels' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPhoneVerification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempRequestPhoneVerification.QueryParams,\n      ComAtprotoTempRequestPhoneVerification.HandlerInput,\n      ComAtprotoTempRequestPhoneVerification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.requestPhoneVerification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeAccountCredentials<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempRevokeAccountCredentials.QueryParams,\n      ComAtprotoTempRevokeAccountCredentials.HandlerInput,\n      ComAtprotoTempRevokeAccountCredentials.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.revokeAccountCredentials' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsNS {\n  _server: Server\n  ozone: ToolsOzoneNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.ozone = new ToolsOzoneNS(server)\n  }\n}\n\nexport class ToolsOzoneNS {\n  _server: Server\n  communication: ToolsOzoneCommunicationNS\n  hosting: ToolsOzoneHostingNS\n  moderation: ToolsOzoneModerationNS\n  safelink: ToolsOzoneSafelinkNS\n  server: ToolsOzoneServerNS\n  set: ToolsOzoneSetNS\n  setting: ToolsOzoneSettingNS\n  signature: ToolsOzoneSignatureNS\n  team: ToolsOzoneTeamNS\n  verification: ToolsOzoneVerificationNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.communication = new ToolsOzoneCommunicationNS(server)\n    this.hosting = new ToolsOzoneHostingNS(server)\n    this.moderation = new ToolsOzoneModerationNS(server)\n    this.safelink = new ToolsOzoneSafelinkNS(server)\n    this.server = new ToolsOzoneServerNS(server)\n    this.set = new ToolsOzoneSetNS(server)\n    this.setting = new ToolsOzoneSettingNS(server)\n    this.signature = new ToolsOzoneSignatureNS(server)\n    this.team = new ToolsOzoneTeamNS(server)\n    this.verification = new ToolsOzoneVerificationNS(server)\n  }\n}\n\nexport class ToolsOzoneCommunicationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createTemplate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationCreateTemplate.QueryParams,\n      ToolsOzoneCommunicationCreateTemplate.HandlerInput,\n      ToolsOzoneCommunicationCreateTemplate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.createTemplate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteTemplate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationDeleteTemplate.QueryParams,\n      ToolsOzoneCommunicationDeleteTemplate.HandlerInput,\n      ToolsOzoneCommunicationDeleteTemplate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.deleteTemplate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listTemplates<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationListTemplates.QueryParams,\n      ToolsOzoneCommunicationListTemplates.HandlerInput,\n      ToolsOzoneCommunicationListTemplates.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.listTemplates' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateTemplate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationUpdateTemplate.QueryParams,\n      ToolsOzoneCommunicationUpdateTemplate.HandlerInput,\n      ToolsOzoneCommunicationUpdateTemplate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.updateTemplate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneHostingNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getAccountHistory<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneHostingGetAccountHistory.QueryParams,\n      ToolsOzoneHostingGetAccountHistory.HandlerInput,\n      ToolsOzoneHostingGetAccountHistory.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.hosting.getAccountHistory' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  cancelScheduledActions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationCancelScheduledActions.QueryParams,\n      ToolsOzoneModerationCancelScheduledActions.HandlerInput,\n      ToolsOzoneModerationCancelScheduledActions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.cancelScheduledActions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  emitEvent<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationEmitEvent.QueryParams,\n      ToolsOzoneModerationEmitEvent.HandlerInput,\n      ToolsOzoneModerationEmitEvent.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.emitEvent' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountTimeline<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetAccountTimeline.QueryParams,\n      ToolsOzoneModerationGetAccountTimeline.HandlerInput,\n      ToolsOzoneModerationGetAccountTimeline.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getAccountTimeline' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getEvent<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetEvent.QueryParams,\n      ToolsOzoneModerationGetEvent.HandlerInput,\n      ToolsOzoneModerationGetEvent.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getEvent' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRecord.QueryParams,\n      ToolsOzoneModerationGetRecord.HandlerInput,\n      ToolsOzoneModerationGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRecords.QueryParams,\n      ToolsOzoneModerationGetRecords.HandlerInput,\n      ToolsOzoneModerationGetRecords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRecords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRepo.QueryParams,\n      ToolsOzoneModerationGetRepo.HandlerInput,\n      ToolsOzoneModerationGetRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getReporterStats<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetReporterStats.QueryParams,\n      ToolsOzoneModerationGetReporterStats.HandlerInput,\n      ToolsOzoneModerationGetReporterStats.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getReporterStats' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRepos.QueryParams,\n      ToolsOzoneModerationGetRepos.HandlerInput,\n      ToolsOzoneModerationGetRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSubjects<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetSubjects.QueryParams,\n      ToolsOzoneModerationGetSubjects.HandlerInput,\n      ToolsOzoneModerationGetSubjects.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getSubjects' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listScheduledActions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationListScheduledActions.QueryParams,\n      ToolsOzoneModerationListScheduledActions.HandlerInput,\n      ToolsOzoneModerationListScheduledActions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.listScheduledActions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryEvents<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationQueryEvents.QueryParams,\n      ToolsOzoneModerationQueryEvents.HandlerInput,\n      ToolsOzoneModerationQueryEvents.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.queryEvents' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryStatuses<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationQueryStatuses.QueryParams,\n      ToolsOzoneModerationQueryStatuses.HandlerInput,\n      ToolsOzoneModerationQueryStatuses.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.queryStatuses' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  scheduleAction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationScheduleAction.QueryParams,\n      ToolsOzoneModerationScheduleAction.HandlerInput,\n      ToolsOzoneModerationScheduleAction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.scheduleAction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationSearchRepos.QueryParams,\n      ToolsOzoneModerationSearchRepos.HandlerInput,\n      ToolsOzoneModerationSearchRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.searchRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSafelinkNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addRule<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkAddRule.QueryParams,\n      ToolsOzoneSafelinkAddRule.HandlerInput,\n      ToolsOzoneSafelinkAddRule.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.addRule' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryEvents<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkQueryEvents.QueryParams,\n      ToolsOzoneSafelinkQueryEvents.HandlerInput,\n      ToolsOzoneSafelinkQueryEvents.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.queryEvents' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryRules<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkQueryRules.QueryParams,\n      ToolsOzoneSafelinkQueryRules.HandlerInput,\n      ToolsOzoneSafelinkQueryRules.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.queryRules' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeRule<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkRemoveRule.QueryParams,\n      ToolsOzoneSafelinkRemoveRule.HandlerInput,\n      ToolsOzoneSafelinkRemoveRule.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.removeRule' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateRule<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkUpdateRule.QueryParams,\n      ToolsOzoneSafelinkUpdateRule.HandlerInput,\n      ToolsOzoneSafelinkUpdateRule.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.updateRule' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneServerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneServerGetConfig.QueryParams,\n      ToolsOzoneServerGetConfig.HandlerInput,\n      ToolsOzoneServerGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.server.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSetNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addValues<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetAddValues.QueryParams,\n      ToolsOzoneSetAddValues.HandlerInput,\n      ToolsOzoneSetAddValues.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.addValues' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteSet<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetDeleteSet.QueryParams,\n      ToolsOzoneSetDeleteSet.HandlerInput,\n      ToolsOzoneSetDeleteSet.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.deleteSet' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteValues<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetDeleteValues.QueryParams,\n      ToolsOzoneSetDeleteValues.HandlerInput,\n      ToolsOzoneSetDeleteValues.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.deleteValues' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getValues<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetGetValues.QueryParams,\n      ToolsOzoneSetGetValues.HandlerInput,\n      ToolsOzoneSetGetValues.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.getValues' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  querySets<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetQuerySets.QueryParams,\n      ToolsOzoneSetQuerySets.HandlerInput,\n      ToolsOzoneSetQuerySets.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.querySets' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  upsertSet<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetUpsertSet.QueryParams,\n      ToolsOzoneSetUpsertSet.HandlerInput,\n      ToolsOzoneSetUpsertSet.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.upsertSet' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSettingNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  listOptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSettingListOptions.QueryParams,\n      ToolsOzoneSettingListOptions.HandlerInput,\n      ToolsOzoneSettingListOptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.setting.listOptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeOptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSettingRemoveOptions.QueryParams,\n      ToolsOzoneSettingRemoveOptions.HandlerInput,\n      ToolsOzoneSettingRemoveOptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.setting.removeOptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  upsertOption<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSettingUpsertOption.QueryParams,\n      ToolsOzoneSettingUpsertOption.HandlerInput,\n      ToolsOzoneSettingUpsertOption.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.setting.upsertOption' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSignatureNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  findCorrelation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSignatureFindCorrelation.QueryParams,\n      ToolsOzoneSignatureFindCorrelation.HandlerInput,\n      ToolsOzoneSignatureFindCorrelation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.signature.findCorrelation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  findRelatedAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSignatureFindRelatedAccounts.QueryParams,\n      ToolsOzoneSignatureFindRelatedAccounts.HandlerInput,\n      ToolsOzoneSignatureFindRelatedAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.signature.findRelatedAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSignatureSearchAccounts.QueryParams,\n      ToolsOzoneSignatureSearchAccounts.HandlerInput,\n      ToolsOzoneSignatureSearchAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.signature.searchAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneTeamNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addMember<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamAddMember.QueryParams,\n      ToolsOzoneTeamAddMember.HandlerInput,\n      ToolsOzoneTeamAddMember.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.addMember' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteMember<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamDeleteMember.QueryParams,\n      ToolsOzoneTeamDeleteMember.HandlerInput,\n      ToolsOzoneTeamDeleteMember.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.deleteMember' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listMembers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamListMembers.QueryParams,\n      ToolsOzoneTeamListMembers.HandlerInput,\n      ToolsOzoneTeamListMembers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.listMembers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateMember<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamUpdateMember.QueryParams,\n      ToolsOzoneTeamUpdateMember.HandlerInput,\n      ToolsOzoneTeamUpdateMember.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.updateMember' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneVerificationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  grantVerifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneVerificationGrantVerifications.QueryParams,\n      ToolsOzoneVerificationGrantVerifications.HandlerInput,\n      ToolsOzoneVerificationGrantVerifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.verification.grantVerifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listVerifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneVerificationListVerifications.QueryParams,\n      ToolsOzoneVerificationListVerifications.HandlerInput,\n      ToolsOzoneVerificationListVerifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.verification.listVerifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeVerifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneVerificationRevokeVerifications.QueryParams,\n      ToolsOzoneVerificationRevokeVerifications.HandlerInput,\n      ToolsOzoneVerificationRevokeVerifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.verification.revokeVerifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/lexicons.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type LexiconDoc,\n  Lexicons,\n  ValidationError,\n  type ValidationResult,\n} from '@atproto/lexicon'\nimport { type $Typed, is$typed, maybe$typed } from './util.js'\n\nexport const schemaDict = {\n  AppBskyActorDefs: {\n    lexicon: 1,\n    id: 'app.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileView: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileViewDetailed: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          website: {\n            type: 'string',\n            format: 'uri',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          banner: {\n            type: 'string',\n            format: 'uri',\n          },\n          followersCount: {\n            type: 'integer',\n          },\n          followsCount: {\n            type: 'integer',\n          },\n          postsCount: {\n            type: 'integer',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          joinedViaStarterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          pinnedPost: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileAssociated: {\n        type: 'object',\n        properties: {\n          lists: {\n            type: 'integer',\n          },\n          feedgens: {\n            type: 'integer',\n          },\n          starterPacks: {\n            type: 'integer',\n          },\n          labeler: {\n            type: 'boolean',\n          },\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedChat',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedActivitySubscription',\n          },\n          germ: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedGerm',\n          },\n        },\n      },\n      profileAssociatedChat: {\n        type: 'object',\n        required: ['allowIncoming'],\n        properties: {\n          allowIncoming: {\n            type: 'string',\n            knownValues: ['all', 'none', 'following'],\n          },\n        },\n      },\n      profileAssociatedGerm: {\n        type: 'object',\n        required: ['showButtonTo', 'messageMeUrl'],\n        properties: {\n          messageMeUrl: {\n            type: 'string',\n            format: 'uri',\n          },\n          showButtonTo: {\n            type: 'string',\n            knownValues: ['usersIFollow', 'everyone'],\n          },\n        },\n      },\n      profileAssociatedActivitySubscription: {\n        type: 'object',\n        required: ['allowSubscriptions'],\n        properties: {\n          allowSubscriptions: {\n            type: 'string',\n            knownValues: ['followers', 'mutuals', 'none'],\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.\",\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          mutedByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          blockedBy: {\n            type: 'boolean',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blockingByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          knownFollowers: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#knownFollowers',\n          },\n          activitySubscription: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n      knownFollowers: {\n        type: 'object',\n        description: \"The subject's followers whom you also follow\",\n        required: ['count', 'followers'],\n        properties: {\n          count: {\n            type: 'integer',\n          },\n          followers: {\n            type: 'array',\n            minLength: 0,\n            maxLength: 5,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      verificationState: {\n        type: 'object',\n        description:\n          'Represents the verification information about the user this object is attached to.',\n        required: ['verifications', 'verifiedStatus', 'trustedVerifierStatus'],\n        properties: {\n          verifications: {\n            type: 'array',\n            description:\n              'All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#verificationView',\n            },\n          },\n          verifiedStatus: {\n            type: 'string',\n            description: \"The user's status as a verified account.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n          trustedVerifierStatus: {\n            type: 'string',\n            description: \"The user's status as a trusted verifier.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n        },\n      },\n      verificationView: {\n        type: 'object',\n        description: 'An individual verification for an associated subject.',\n        required: ['issuer', 'uri', 'isValid', 'createdAt'],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          isValid: {\n            type: 'boolean',\n            description:\n              'True if the verification passes validation, otherwise false.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n        },\n      },\n      preferences: {\n        type: 'array',\n        items: {\n          type: 'union',\n          refs: [\n            'lex:app.bsky.actor.defs#adultContentPref',\n            'lex:app.bsky.actor.defs#contentLabelPref',\n            'lex:app.bsky.actor.defs#savedFeedsPref',\n            'lex:app.bsky.actor.defs#savedFeedsPrefV2',\n            'lex:app.bsky.actor.defs#personalDetailsPref',\n            'lex:app.bsky.actor.defs#declaredAgePref',\n            'lex:app.bsky.actor.defs#feedViewPref',\n            'lex:app.bsky.actor.defs#threadViewPref',\n            'lex:app.bsky.actor.defs#interestsPref',\n            'lex:app.bsky.actor.defs#mutedWordsPref',\n            'lex:app.bsky.actor.defs#hiddenPostsPref',\n            'lex:app.bsky.actor.defs#bskyAppStatePref',\n            'lex:app.bsky.actor.defs#labelersPref',\n            'lex:app.bsky.actor.defs#postInteractionSettingsPref',\n            'lex:app.bsky.actor.defs#verificationPrefs',\n            'lex:app.bsky.actor.defs#liveEventPreferences',\n          ],\n        },\n      },\n      adultContentPref: {\n        type: 'object',\n        required: ['enabled'],\n        properties: {\n          enabled: {\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      contentLabelPref: {\n        type: 'object',\n        required: ['label', 'visibility'],\n        properties: {\n          labelerDid: {\n            type: 'string',\n            description:\n              'Which labeler does this preference apply to? If undefined, applies globally.',\n            format: 'did',\n          },\n          label: {\n            type: 'string',\n          },\n          visibility: {\n            type: 'string',\n            knownValues: ['ignore', 'show', 'warn', 'hide'],\n          },\n        },\n      },\n      savedFeed: {\n        type: 'object',\n        required: ['id', 'type', 'value', 'pinned'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          type: {\n            type: 'string',\n            knownValues: ['feed', 'list', 'timeline'],\n          },\n          value: {\n            type: 'string',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      savedFeedsPrefV2: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#savedFeed',\n            },\n          },\n        },\n      },\n      savedFeedsPref: {\n        type: 'object',\n        required: ['pinned', 'saved'],\n        properties: {\n          pinned: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          saved: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          timelineIndex: {\n            type: 'integer',\n          },\n        },\n      },\n      personalDetailsPref: {\n        type: 'object',\n        properties: {\n          birthDate: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The birth date of account owner.',\n          },\n        },\n      },\n      declaredAgePref: {\n        type: 'object',\n        description:\n          \"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.\",\n        properties: {\n          isOverAge13: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 13 years of age.',\n          },\n          isOverAge16: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 16 years of age.',\n          },\n          isOverAge18: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 18 years of age.',\n          },\n        },\n      },\n      feedViewPref: {\n        type: 'object',\n        required: ['feed'],\n        properties: {\n          feed: {\n            type: 'string',\n            description:\n              'The URI of the feed, or an identifier which describes the feed.',\n          },\n          hideReplies: {\n            type: 'boolean',\n            description: 'Hide replies in the feed.',\n          },\n          hideRepliesByUnfollowed: {\n            type: 'boolean',\n            description:\n              'Hide replies in the feed if they are not by followed users.',\n            default: true,\n          },\n          hideRepliesByLikeCount: {\n            type: 'integer',\n            description:\n              'Hide replies in the feed if they do not have this number of likes.',\n          },\n          hideReposts: {\n            type: 'boolean',\n            description: 'Hide reposts in the feed.',\n          },\n          hideQuotePosts: {\n            type: 'boolean',\n            description: 'Hide quote posts in the feed.',\n          },\n        },\n      },\n      threadViewPref: {\n        type: 'object',\n        properties: {\n          sort: {\n            type: 'string',\n            description: 'Sorting mode for threads.',\n            knownValues: [\n              'oldest',\n              'newest',\n              'most-likes',\n              'random',\n              'hotness',\n            ],\n          },\n        },\n      },\n      interestsPref: {\n        type: 'object',\n        required: ['tags'],\n        properties: {\n          tags: {\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'string',\n              maxLength: 640,\n              maxGraphemes: 64,\n            },\n            description:\n              \"A list of tags which describe the account owner's interests gathered during onboarding.\",\n          },\n        },\n      },\n      mutedWordTarget: {\n        type: 'string',\n        knownValues: ['content', 'tag'],\n        maxLength: 640,\n        maxGraphemes: 64,\n      },\n      mutedWord: {\n        type: 'object',\n        description: 'A word that the account owner has muted.',\n        required: ['value', 'targets'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n            description: 'The muted word itself.',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          targets: {\n            type: 'array',\n            description: 'The intended targets of the muted word.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWordTarget',\n            },\n          },\n          actorTarget: {\n            type: 'string',\n            description:\n              'Groups of users to apply the muted word to. If undefined, applies to all users.',\n            knownValues: ['all', 'exclude-following'],\n            default: 'all',\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the muted word will expire and no longer be applied.',\n          },\n        },\n      },\n      mutedWordsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWord',\n            },\n            description: 'A list of words the account owner has muted.',\n          },\n        },\n      },\n      hiddenPostsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            description:\n              'A list of URIs of posts the account owner has hidden.',\n          },\n        },\n      },\n      labelersPref: {\n        type: 'object',\n        required: ['labelers'],\n        properties: {\n          labelers: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#labelerPrefItem',\n            },\n          },\n        },\n      },\n      labelerPrefItem: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      bskyAppStatePref: {\n        description:\n          \"A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.\",\n        type: 'object',\n        properties: {\n          activeProgressGuide: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#bskyAppProgressGuide',\n          },\n          queuedNudges: {\n            description:\n              'An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.',\n            type: 'array',\n            maxLength: 1000,\n            items: {\n              type: 'string',\n              maxLength: 100,\n            },\n          },\n          nuxs: {\n            description: 'Storage for NUXs the user has encountered.',\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#nux',\n            },\n          },\n        },\n      },\n      bskyAppProgressGuide: {\n        description:\n          'If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.',\n        type: 'object',\n        required: ['guide'],\n        properties: {\n          guide: {\n            type: 'string',\n            maxLength: 100,\n          },\n        },\n      },\n      nux: {\n        type: 'object',\n        description: 'A new user experiences (NUX) storage object',\n        required: ['id', 'completed'],\n        properties: {\n          id: {\n            type: 'string',\n            maxLength: 100,\n          },\n          completed: {\n            type: 'boolean',\n            default: false,\n          },\n          data: {\n            description:\n              'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.',\n            type: 'string',\n            maxLength: 3000,\n            maxGraphemes: 300,\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the NUX will expire and should be considered completed.',\n          },\n        },\n      },\n      verificationPrefs: {\n        type: 'object',\n        description: 'Preferences for how verified accounts appear in the app.',\n        required: [],\n        properties: {\n          hideBadges: {\n            description:\n              'Hide the blue check badges for verified accounts and trusted verifiers.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      liveEventPreferences: {\n        type: 'object',\n        description: 'Preferences for live events.',\n        properties: {\n          hiddenFeedIds: {\n            description:\n              'A list of feed IDs that the user has hidden from live events.',\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          hideAllFeeds: {\n            description: 'Whether to hide all feeds from live events.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      postInteractionSettingsPref: {\n        type: 'object',\n        description:\n          '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.',\n        required: [],\n        properties: {\n          threadgateAllowRules: {\n            description:\n              'Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n        },\n      },\n      statusView: {\n        type: 'object',\n        required: ['status', 'record'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          status: {\n            type: 'string',\n            description: 'The status for the account.',\n            knownValues: ['app.bsky.actor.status#live'],\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            description: 'An optional embed associated with the status.',\n            refs: ['lex:app.bsky.embed.external#view'],\n          },\n          expiresAt: {\n            type: 'string',\n            description:\n              'The date when this status will expire. The application might choose to no longer return the status after expiration.',\n            format: 'datetime',\n          },\n          isActive: {\n            type: 'boolean',\n            description:\n              'True if the status is not expired, false if it is expired. Only present if expiration was set.',\n          },\n          isDisabled: {\n            type: 'boolean',\n            description:\n              \"True if the user's go-live access has been disabled by a moderator, false otherwise.\",\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfile',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID of account to fetch profile of.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfiles: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfiles',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get detailed profile views of multiple actors.',\n        parameters: {\n          type: 'params',\n          required: ['actors'],\n          properties: {\n            actors: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['profiles'],\n            properties: {\n              profiles: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.profile',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account profile.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          properties: {\n            displayName: {\n              type: 'string',\n              maxGraphemes: 64,\n              maxLength: 640,\n            },\n            description: {\n              type: 'string',\n              description: 'Free-form profile description text.',\n              maxGraphemes: 256,\n              maxLength: 2560,\n            },\n            pronouns: {\n              type: 'string',\n              description: 'Free-form pronouns text.',\n              maxGraphemes: 20,\n              maxLength: 200,\n            },\n            website: {\n              type: 'string',\n              format: 'uri',\n            },\n            avatar: {\n              type: 'blob',\n              description:\n                \"Small image to be displayed next to posts from account. AKA, 'profile picture'\",\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            banner: {\n              type: 'blob',\n              description:\n                'Larger horizontal image to display behind profile view.',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values, specific to the Bluesky application, on the overall account.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            joinedViaStarterPack: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            pinnedPost: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Set the private preferences attached to the account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActors: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActors',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actors (profiles) matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActorsTypeahead: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActorsTypeahead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description: 'Search query prefix; not a full query string.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorStatus: {\n    lexicon: 1,\n    id: 'app.bsky.actor.status',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account status.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['status', 'createdAt'],\n          properties: {\n            status: {\n              type: 'string',\n              description: 'The status for the account.',\n              knownValues: ['app.bsky.actor.status#live'],\n            },\n            embed: {\n              type: 'union',\n              description: 'An optional embed associated with the status.',\n              refs: ['lex:app.bsky.embed.external'],\n            },\n            durationMinutes: {\n              type: 'integer',\n              description:\n                'The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.',\n              minimum: 1,\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      live: {\n        type: 'token',\n        description:\n          'Advertises an account as currently offering live content.',\n      },\n    },\n  },\n  AppBskyAgeassuranceBegin: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.begin',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate Age Assurance for an account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive Age Assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the Age Assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n              regionCode: {\n                type: 'string',\n                description:\n                  \"An optional ISO 3166-2 code of the user's region or state within the country.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#state',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n          {\n            name: 'RegionNotSupported',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyAgeassuranceDefs: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.defs',\n    defs: {\n      access: {\n        description:\n          \"The access level granted based on Age Assurance data we've processed.\",\n        type: 'string',\n        knownValues: ['unknown', 'none', 'safe', 'full'],\n      },\n      status: {\n        type: 'string',\n        description: 'The status of the Age Assurance process.',\n        knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n      },\n      state: {\n        type: 'object',\n        description: \"The user's computed Age Assurance state.\",\n        required: ['status', 'access'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#status',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      stateMetadata: {\n        type: 'object',\n        description:\n          'Additional metadata needed to compute Age Assurance state client-side.',\n        required: [],\n        properties: {\n          accountCreatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The account creation timestamp.',\n          },\n        },\n      },\n      config: {\n        type: 'object',\n        description: '',\n        required: ['regions'],\n        properties: {\n          regions: {\n            type: 'array',\n            description: 'The per-region Age Assurance configuration.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.ageassurance.defs#configRegion',\n            },\n          },\n        },\n      },\n      configRegion: {\n        type: 'object',\n        description: 'The Age Assurance configuration for a specific region.',\n        required: ['countryCode', 'minAccessAge', 'rules'],\n        properties: {\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code this configuration applies to.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.',\n          },\n          minAccessAge: {\n            type: 'integer',\n            description:\n              'The minimum age (as a whole integer) required to use Bluesky in this region.',\n          },\n          rules: {\n            type: 'array',\n            description:\n              '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.',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.ageassurance.defs#configRegionRuleDefault',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan',\n              ],\n            },\n          },\n        },\n      },\n      configRegionRuleDefault: {\n        type: 'object',\n        description: 'Age Assurance rule that applies by default.',\n        required: ['access'],\n        properties: {\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountNewerThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is equal-to or newer than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountOlderThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is older than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        description: 'Object used to store Age Assurance data in stash.',\n        required: ['createdAt', 'status', 'access', 'attemptId', 'countryCode'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the Age Assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n          access: {\n            description:\n              \"The access level granted based on Age Assurance data we've processed.\",\n            type: 'string',\n            knownValues: ['unknown', 'none', 'safe', 'full'],\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for Age Assurance.',\n          },\n          initIp: {\n            type: 'string',\n            description:\n              'The IP address used when initiating the Age Assurance flow.',\n          },\n          initUa: {\n            type: 'string',\n            description:\n              'The user agent used when initiating the Age Assurance flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description:\n              'The IP address used when completing the Age Assurance flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description:\n              'The user agent used when completing the Age Assurance flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns Age Assurance configuration for use on the client.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#config',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetState: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.',\n        parameters: {\n          type: 'params',\n          required: ['countryCode'],\n          properties: {\n            countryCode: {\n              type: 'string',\n            },\n            regionCode: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['state', 'metadata'],\n            properties: {\n              state: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#state',\n              },\n              metadata: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#stateMetadata',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkCreateBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.createBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkDefs: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.defs',\n    defs: {\n      bookmark: {\n        description: 'Object used to store bookmark data in stash.',\n        type: 'object',\n        required: ['subject'],\n        properties: {\n          subject: {\n            description:\n              'A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      bookmarkView: {\n        type: 'object',\n        required: ['subject', 'item'],\n        properties: {\n          subject: {\n            description: 'A strong ref to the bookmarked record.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          item: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#blockedPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#postView',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkDeleteBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.deleteBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkGetBookmarks: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.getBookmarks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Gets views of records bookmarked by the authenticated user. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['bookmarks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              bookmarks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.bookmark.defs#bookmarkView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDefs: {\n    lexicon: 1,\n    id: 'app.bsky.contact.defs',\n    defs: {\n      matchAndContactIndex: {\n        description:\n          '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.',\n        type: 'object',\n        required: ['match', 'contactIndex'],\n        properties: {\n          match: {\n            description: 'Profile of the matched user.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          contactIndex: {\n            description: 'The index of this match in the import contact input.',\n            type: 'integer',\n            minimum: 0,\n            maximum: 999,\n          },\n        },\n      },\n      syncStatus: {\n        type: 'object',\n        required: ['syncedAt', 'matchesCount'],\n        properties: {\n          syncedAt: {\n            description: 'Last date when contacts where imported.',\n            type: 'string',\n            format: 'datetime',\n          },\n          matchesCount: {\n            description:\n              '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.',\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n      notification: {\n        description:\n          'A stash object to be sent via bsync representing a notification to be created.',\n        type: 'object',\n        required: ['from', 'to'],\n        properties: {\n          from: {\n            description: 'The DID of who this notification comes from.',\n            type: 'string',\n            format: 'did',\n          },\n          to: {\n            description: 'The DID of who this notification should go to.',\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDismissMatch: {\n    lexicon: 1,\n    id: 'app.bsky.contact.dismissMatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                description: \"The subject's DID to dismiss the match with.\",\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetMatches: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getMatches',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matches'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              matches: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidLimit',\n          },\n          {\n            name: 'InvalidCursor',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetSyncStatus: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getSyncStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets the user's current contact import status. Requires authentication.\",\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              syncStatus: {\n                description:\n                  \"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.\",\n                type: 'ref',\n                ref: 'lex:app.bsky.contact.defs#syncStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactImportContacts: {\n    lexicon: 1,\n    id: 'app.bsky.contact.importContacts',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'contacts'],\n            properties: {\n              token: {\n                description:\n                  'JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.',\n                type: 'string',\n              },\n              contacts: {\n                description:\n                  \"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`.\",\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                minLength: 1,\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matchesAndContactIndexes'],\n            properties: {\n              matchesAndContactIndexes: {\n                description:\n                  'The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.',\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.contact.defs#matchAndContactIndex',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidContacts',\n          },\n          {\n            name: 'TooManyContacts',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactRemoveData: {\n    lexicon: 1,\n    id: 'app.bsky.contact.removeData',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactSendNotification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.sendNotification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'System endpoint to send notifications related to contact imports. Requires role authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['from', 'to'],\n            properties: {\n              from: {\n                description: 'The DID of who this notification comes from.',\n                type: 'string',\n                format: 'did',\n              },\n              to: {\n                description: 'The DID of who this notification should go to.',\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactStartPhoneVerification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.startPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          '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.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone'],\n            properties: {\n              phone: {\n                description: 'The phone number to receive the code via SMS.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactVerifyPhone: {\n    lexicon: 1,\n    id: 'app.bsky.contact.verifyPhone',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone', 'code'],\n            properties: {\n              phone: {\n                description:\n                  'The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n              code: {\n                description:\n                  'The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                description:\n                  'JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InvalidCode',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftCreateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.createDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draft',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'The ID of the created draft.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DraftLimitReached',\n            description:\n              'Trying to insert a new draft when the limit was already reached.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftDefs: {\n    lexicon: 1,\n    id: 'app.bsky.draft.defs',\n    defs: {\n      draftWithId: {\n        description:\n          'A draft with an identifier, used to store drafts in private storage (stash).',\n        type: 'object',\n        required: ['id', 'draft'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n        },\n      },\n      draft: {\n        description: 'A draft containing an array of draft posts.',\n        type: 'object',\n        required: ['posts'],\n        properties: {\n          deviceId: {\n            type: 'string',\n            description:\n              'UUIDv4 identifier of the device that created this draft.',\n            maxLength: 100,\n          },\n          deviceName: {\n            type: 'string',\n            description:\n              'The device and/or platform on which the draft was created.',\n            maxLength: 100,\n          },\n          posts: {\n            description: 'Array of draft posts that compose this draft.',\n            type: 'array',\n            minLength: 1,\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftPost',\n            },\n          },\n          langs: {\n            type: 'array',\n            description:\n              'Indicates human language of posts primary text content.',\n            maxLength: 3,\n            items: {\n              type: 'string',\n              format: 'language',\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Embedding rules for the postgates to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n          threadgateAllow: {\n            description:\n              'Allow-rules for the threadgate to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n        },\n      },\n      draftPost: {\n        description: 'One of the posts that compose a draft.',\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n            description:\n              '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.',\n          },\n          labels: {\n            type: 'union',\n            description:\n              'Self-label values for this post. Effectively content warnings.',\n            refs: ['lex:com.atproto.label.defs#selfLabels'],\n          },\n          embedImages: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedImage',\n            },\n            maxLength: 4,\n          },\n          embedVideos: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedVideo',\n            },\n            maxLength: 1,\n          },\n          embedExternals: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedExternal',\n            },\n            maxLength: 1,\n          },\n          embedRecords: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedRecord',\n            },\n            maxLength: 1,\n          },\n        },\n      },\n      draftView: {\n        description: 'View to present drafts data to users.',\n        type: 'object',\n        required: ['id', 'draft', 'createdAt', 'updatedAt'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n          createdAt: {\n            description: 'The time the draft was created.',\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            description: 'The time the draft was last updated.',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      draftEmbedLocalRef: {\n        type: 'object',\n        required: ['path'],\n        properties: {\n          path: {\n            type: 'string',\n            description:\n              'Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts.',\n            minLength: 1,\n            maxLength: 1024,\n          },\n        },\n      },\n      draftEmbedCaption: {\n        type: 'object',\n        required: ['lang', 'content'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          content: {\n            type: 'string',\n            maxLength: 10000,\n          },\n        },\n      },\n      draftEmbedImage: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n        },\n      },\n      draftEmbedVideo: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedCaption',\n            },\n            maxLength: 20,\n          },\n        },\n      },\n      draftEmbedExternal: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      draftEmbedRecord: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftDeleteDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.deleteDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Deletes a draft by ID. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftGetDrafts: {\n    lexicon: 1,\n    id: 'app.bsky.draft.getDrafts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets views of user drafts. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['drafts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              drafts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.draft.defs#draftView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftUpdateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.updateDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"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.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draftWithId',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.embed.defs',\n    defs: {\n      aspectRatio: {\n        type: 'object',\n        description:\n          'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n            minimum: 1,\n          },\n          height: {\n            type: 'integer',\n            minimum: 1,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedExternal: {\n    lexicon: 1,\n    id: 'app.bsky.embed.external',\n    defs: {\n      main: {\n        type: 'object',\n        description:\n          \"A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).\",\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#external',\n          },\n        },\n      },\n      external: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#viewExternal',\n          },\n        },\n      },\n      viewExternal: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedImages: {\n    lexicon: 1,\n    id: 'app.bsky.embed.images',\n    description: 'A set of images embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#image',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      image: {\n        type: 'object',\n        required: ['image', 'alt'],\n        properties: {\n          image: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#viewImage',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      viewImage: {\n        type: 'object',\n        required: ['thumb', 'fullsize', 'alt'],\n        properties: {\n          thumb: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.',\n          },\n          fullsize: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.',\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecord: {\n    lexicon: 1,\n    id: 'app.bsky.embed.record',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.record#viewRecord',\n              'lex:app.bsky.embed.record#viewNotFound',\n              'lex:app.bsky.embed.record#viewBlocked',\n              'lex:app.bsky.embed.record#viewDetached',\n              'lex:app.bsky.feed.defs#generatorView',\n              'lex:app.bsky.graph.defs#listView',\n              'lex:app.bsky.labeler.defs#labelerView',\n              'lex:app.bsky.graph.defs#starterPackViewBasic',\n            ],\n          },\n        },\n      },\n      viewRecord: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'value', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          value: {\n            type: 'unknown',\n            description: 'The record data itself.',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          embeds: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images#view',\n                'lex:app.bsky.embed.video#view',\n                'lex:app.bsky.embed.external#view',\n                'lex:app.bsky.embed.record#view',\n                'lex:app.bsky.embed.recordWithMedia#view',\n              ],\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      viewNotFound: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      viewBlocked: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      viewDetached: {\n        type: 'object',\n        required: ['uri', 'detached'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          detached: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecordWithMedia: {\n    lexicon: 1,\n    id: 'app.bsky.embed.recordWithMedia',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images',\n              'lex:app.bsky.embed.video',\n              'lex:app.bsky.embed.external',\n            ],\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record#view',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedVideo: {\n    lexicon: 1,\n    id: 'app.bsky.embed.video',\n    description: 'A video embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['video'],\n        properties: {\n          video: {\n            type: 'blob',\n            description:\n              'The mp4 video file. May be up to 100mb, formerly limited to 50mb.',\n            accept: ['video/mp4'],\n            maxSize: 100000000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.video#caption',\n            },\n            maxLength: 20,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the video, for accessibility.',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n      caption: {\n        type: 'object',\n        required: ['lang', 'file'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          file: {\n            type: 'blob',\n            accept: ['text/vtt'],\n            maxSize: 20000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['cid', 'playlist'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          playlist: {\n            type: 'string',\n            format: 'uri',\n          },\n          thumbnail: {\n            type: 'string',\n            format: 'uri',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.feed.defs',\n    defs: {\n      postView: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'record', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n              'lex:app.bsky.embed.record#view',\n              'lex:app.bsky.embed.recordWithMedia#view',\n            ],\n          },\n          bookmarkCount: {\n            type: 'integer',\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          threadgate: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadgateView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.\",\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          bookmarked: {\n            type: 'boolean',\n          },\n          threadMuted: {\n            type: 'boolean',\n          },\n          replyDisabled: {\n            type: 'boolean',\n          },\n          embeddingDisabled: {\n            type: 'boolean',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      threadContext: {\n        type: 'object',\n        description:\n          'Metadata about this post within the context of the thread it is in.',\n        properties: {\n          rootAuthorLike: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      feedViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#replyRef',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#reasonRepost',\n              'lex:app.bsky.feed.defs#reasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context provided by feed generator that may be passed back alongside interactions.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          grandparentAuthor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            description:\n              'When parent is a reply to another post, this is the author of that post.',\n          },\n        },\n      },\n      reasonRepost: {\n        type: 'object',\n        required: ['by', 'indexedAt'],\n        properties: {\n          by: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#threadViewPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          replies: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.defs#threadViewPost',\n                'lex:app.bsky.feed.defs#notFoundPost',\n                'lex:app.bsky.feed.defs#blockedPost',\n              ],\n            },\n          },\n          threadContext: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadContext',\n          },\n        },\n      },\n      notFoundPost: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      blockedPost: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      blockedAuthor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n        },\n      },\n      generatorView: {\n        type: 'object',\n        required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          acceptsInteractions: {\n            type: 'boolean',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#generatorViewerState',\n          },\n          contentMode: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#contentModeUnspecified',\n              'app.bsky.feed.defs#contentModeVideo',\n            ],\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      generatorViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonFeedPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#skeletonReasonRepost',\n              'lex:app.bsky.feed.defs#skeletonReasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context that will be passed through to client and may be passed to feed generator back alongside interactions.',\n            maxLength: 2000,\n          },\n        },\n      },\n      skeletonReasonRepost: {\n        type: 'object',\n        required: ['repost'],\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonReasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadgateView: {\n        type: 'object',\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          lists: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listViewBasic',\n            },\n          },\n        },\n      },\n      interaction: {\n        type: 'object',\n        properties: {\n          item: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          event: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#requestLess',\n              'app.bsky.feed.defs#requestMore',\n              'app.bsky.feed.defs#clickthroughItem',\n              'app.bsky.feed.defs#clickthroughAuthor',\n              'app.bsky.feed.defs#clickthroughReposter',\n              'app.bsky.feed.defs#clickthroughEmbed',\n              'app.bsky.feed.defs#interactionSeen',\n              'app.bsky.feed.defs#interactionLike',\n              'app.bsky.feed.defs#interactionRepost',\n              'app.bsky.feed.defs#interactionReply',\n              'app.bsky.feed.defs#interactionQuote',\n              'app.bsky.feed.defs#interactionShare',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      requestLess: {\n        type: 'token',\n        description:\n          'Request that less content like the given feed item be shown in the feed',\n      },\n      requestMore: {\n        type: 'token',\n        description:\n          'Request that more content like the given feed item be shown in the feed',\n      },\n      clickthroughItem: {\n        type: 'token',\n        description: 'User clicked through to the feed item',\n      },\n      clickthroughAuthor: {\n        type: 'token',\n        description: 'User clicked through to the author of the feed item',\n      },\n      clickthroughReposter: {\n        type: 'token',\n        description: 'User clicked through to the reposter of the feed item',\n      },\n      clickthroughEmbed: {\n        type: 'token',\n        description:\n          'User clicked through to the embedded content of the feed item',\n      },\n      contentModeUnspecified: {\n        type: 'token',\n        description: 'Declares the feed generator returns any types of posts.',\n      },\n      contentModeVideo: {\n        type: 'token',\n        description:\n          'Declares the feed generator returns posts containing app.bsky.embed.video embeds.',\n      },\n      interactionSeen: {\n        type: 'token',\n        description: 'Feed item was seen by user',\n      },\n      interactionLike: {\n        type: 'token',\n        description: 'User liked the feed item',\n      },\n      interactionRepost: {\n        type: 'token',\n        description: 'User reposted the feed item',\n      },\n      interactionReply: {\n        type: 'token',\n        description: 'User replied to the feed item',\n      },\n      interactionQuote: {\n        type: 'token',\n        description: 'User quoted the feed item',\n      },\n      interactionShare: {\n        type: 'token',\n        description: 'User shared the feed item',\n      },\n    },\n  },\n  AppBskyFeedDescribeFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.describeFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'feeds'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.describeFeedGenerator#feed',\n                },\n              },\n              links: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.describeFeedGenerator#links',\n              },\n            },\n          },\n        },\n      },\n      feed: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n          },\n          termsOfService: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.generator',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.',\n        key: 'any',\n        record: {\n          type: 'object',\n          required: ['did', 'displayName', 'createdAt'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            displayName: {\n              type: 'string',\n              maxGraphemes: 24,\n              maxLength: 240,\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            acceptsInteractions: {\n              type: 'boolean',\n              description:\n                'Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions',\n            },\n            labels: {\n              type: 'union',\n              description: 'Self-label values',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            contentMode: {\n              type: 'string',\n              knownValues: [\n                'app.bsky.feed.defs#contentModeUnspecified',\n                'app.bsky.feed.defs#contentModeVideo',\n              ],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a list of feeds (feed generator records) created by the actor (in the actor's repo).\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetAuthorFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getAuthorFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            filter: {\n              type: 'string',\n              description:\n                'Combinations of post/repost types to include in response.',\n              knownValues: [\n                'posts_with_replies',\n                'posts_no_replies',\n                'posts_with_media',\n                'posts_and_author_threads',\n                'posts_with_video',\n              ],\n              default: 'posts_with_replies',\n            },\n            includePins: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a hydrated feed from an actor's selected feed generator. Implemented by App View.\",\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator. Implemented by AppView.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the feed generator record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['view', 'isOnline', 'isValid'],\n            properties: {\n              view: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#generatorView',\n              },\n              isOnline: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service has been online recently, or else seems to be inactive.',\n              },\n              isValid: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service is compatible with the record declaration.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of feed generators.',\n        parameters: {\n          type: 'params',\n          required: ['feeds'],\n          properties: {\n            feeds: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference to feed generator record describing the specific feed being requested.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#skeletonFeedPost',\n                },\n              },\n              reqId: {\n                type: 'string',\n                description:\n                  'Unique identifier per request that may be passed back alongside interactions.',\n                maxLength: 100,\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get like records which reference a subject (by AT-URI and CID).',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the subject (eg, a post record).',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'CID of the subject record (aka, specific version of record), to filter likes.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'likes'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              likes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.getLikes#like',\n                },\n              },\n            },\n          },\n        },\n      },\n      like: {\n        type: 'object',\n        required: ['indexedAt', 'createdAt', 'actor'],\n        properties: {\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          actor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetListFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getListFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownList',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPostThread: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPostThread',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to post record.',\n            },\n            depth: {\n              type: 'integer',\n              description:\n                'How many levels of reply depth should be included in response.',\n              default: 6,\n              minimum: 0,\n              maximum: 1000,\n            },\n            parentHeight: {\n              type: 'integer',\n              description:\n                'How many levels of parent (and grandparent, etc) post to include.',\n              default: 80,\n              minimum: 0,\n              maximum: 1000,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.defs#threadViewPost',\n                  'lex:app.bsky.feed.defs#notFoundPost',\n                  'lex:app.bsky.feed.defs#blockedPost',\n                ],\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'NotFound',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.\",\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              description: 'List of post AT-URIs to return hydrated views for.',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetQuotes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getQuotes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of quotes for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to quotes of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'posts'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetRepostedBy: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getRepostedBy',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of reposts for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to reposts of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'repostedBy'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              repostedBy: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested feeds (feed generators) for the requesting account.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetTimeline: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.\",\n        parameters: {\n          type: 'params',\n          properties: {\n            algorithm: {\n              type: 'string',\n              description:\n                \"Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedLike: {\n    lexicon: 1,\n    id: 'app.bsky.feed.like',\n    defs: {\n      main: {\n        type: 'record',\n        description: \"Record declaring a 'like' of a piece of subject content.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.post',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'Record containing a Bluesky post.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['text', 'createdAt'],\n          properties: {\n            text: {\n              type: 'string',\n              maxLength: 3000,\n              maxGraphemes: 300,\n              description:\n                'The primary post content. May be an empty string, if there are embeds.',\n            },\n            entities: {\n              type: 'array',\n              description: 'DEPRECATED: replaced by app.bsky.richtext.facet.',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.post#entity',\n              },\n            },\n            facets: {\n              type: 'array',\n              description:\n                'Annotations of text (mentions, URLs, hashtags, etc)',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            reply: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.post#replyRef',\n            },\n            embed: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images',\n                'lex:app.bsky.embed.video',\n                'lex:app.bsky.embed.external',\n                'lex:app.bsky.embed.record',\n                'lex:app.bsky.embed.recordWithMedia',\n              ],\n            },\n            langs: {\n              type: 'array',\n              description:\n                'Indicates human language of post primary text content.',\n              maxLength: 3,\n              items: {\n                type: 'string',\n                format: 'language',\n              },\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values for this post. Effectively content warnings.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            tags: {\n              type: 'array',\n              description:\n                'Additional hashtags, in addition to any included in post text and facets.',\n              maxLength: 8,\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Client-declared timestamp when this post was originally created.',\n            },\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          parent: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      entity: {\n        type: 'object',\n        description: 'Deprecated: use facets instead.',\n        required: ['index', 'type', 'value'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.post#textSlice',\n          },\n          type: {\n            type: 'string',\n            description: \"Expected values are 'mention' and 'link'.\",\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n      textSlice: {\n        type: 'object',\n        description:\n          'Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.',\n        required: ['start', 'end'],\n        properties: {\n          start: {\n            type: 'integer',\n            minimum: 0,\n          },\n          end: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPostgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.postgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          'Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.',\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            detachedEmbeddingUris: {\n              type: 'array',\n              maxLength: 50,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description:\n                'List of AT-URIs embedding this post that the author has detached from.',\n            },\n            embeddingRules: {\n              description:\n                'List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: ['lex:app.bsky.feed.postgate#disableRule'],\n              },\n            },\n          },\n        },\n      },\n      disableRule: {\n        type: 'object',\n        description: 'Disables embedding of this post.',\n        properties: {},\n      },\n    },\n  },\n  AppBskyFeedRepost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.repost',\n    defs: {\n      main: {\n        description:\n          \"Record representing a 'repost' of an existing Bluesky post.\",\n        type: 'record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedSearchPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.searchPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedSendInteractions: {\n    lexicon: 1,\n    id: 'app.bsky.feed.sendInteractions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Send information about interactions with feed items back to the feed generator that served them.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['interactions'],\n            properties: {\n              feed: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              interactions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#interaction',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedThreadgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.threadgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          \"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.\",\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            allow: {\n              description:\n                'List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.threadgate#mentionRule',\n                  'lex:app.bsky.feed.threadgate#followerRule',\n                  'lex:app.bsky.feed.threadgate#followingRule',\n                  'lex:app.bsky.feed.threadgate#listRule',\n                ],\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            hiddenReplies: {\n              type: 'array',\n              maxLength: 300,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description: 'List of hidden reply URIs.',\n            },\n          },\n        },\n      },\n      mentionRule: {\n        type: 'object',\n        description: 'Allow replies from actors mentioned in your post.',\n        properties: {},\n      },\n      followerRule: {\n        type: 'object',\n        description: 'Allow replies from actors who follow you.',\n        properties: {},\n      },\n      followingRule: {\n        type: 'object',\n        description: 'Allow replies from actors you follow.',\n        properties: {},\n      },\n      listRule: {\n        type: 'object',\n        description: 'Allow replies from actors on a list.',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphBlock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.block',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'DID of the account to be blocked.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphDefs: {\n    lexicon: 1,\n    id: 'app.bsky.graph.defs',\n    defs: {\n      listViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'name', 'purpose'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'name', 'purpose', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listItemView: {\n        type: 'object',\n        required: ['uri', 'subject'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n      starterPackView: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          listItemsSample: {\n            type: 'array',\n            maxLength: 12,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listItemView',\n            },\n          },\n          feeds: {\n            type: 'array',\n            maxLength: 3,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.defs#generatorView',\n            },\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      starterPackViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listPurpose: {\n        type: 'string',\n        knownValues: [\n          'app.bsky.graph.defs#modlist',\n          'app.bsky.graph.defs#curatelist',\n          'app.bsky.graph.defs#referencelist',\n        ],\n      },\n      modlist: {\n        type: 'token',\n        description:\n          'A list of actors to apply an aggregate moderation action (mute/block) on.',\n      },\n      curatelist: {\n        type: 'token',\n        description:\n          'A list of actors used for curation purposes such as list feeds or interaction gating.',\n      },\n      referencelist: {\n        type: 'token',\n        description:\n          'A list of actors used for only for reference purposes such as within a starter pack.',\n      },\n      listViewerState: {\n        type: 'object',\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          blocked: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      notFoundActor: {\n        type: 'object',\n        description: 'indicates that a handle or DID could not be resolved',\n        required: ['actor', 'notFound'],\n        properties: {\n          actor: {\n            type: 'string',\n            format: 'at-identifier',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      relationship: {\n        type: 'object',\n        description:\n          'lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor follows this DID, this is the AT-URI of the follow record',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is followed by this DID, contains the AT-URI of the follow record',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID, this is the AT-URI of the block record',\n          },\n          blockedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID, contains the AT-URI of the block record',\n          },\n          blockingByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID via a block list, this is the AT-URI of the listblock record',\n          },\n          blockedByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphFollow: {\n    lexicon: 1,\n    id: 'app.bsky.graph.follow',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetActorStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getActorStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of starter packs created by the actor.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates which accounts the requesting account is currently blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blocks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blocks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollows: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollows',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which a specified account (actor) follows.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'follows'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              follows: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetKnownFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getKnownFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor) and are followed by the viewer.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getList',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets a 'view' (with additional context) of a specified list.\",\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the list record to hydrate.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list', 'items'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              list: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#listView',\n              },\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listItemView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get mod lists that the requesting account (actor) is blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetLists: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getLists',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to enumerate lists from.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListsWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListsWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['listsWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              listsWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getListsWithMembership#listWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      listWithMembership: {\n        description:\n          'A list and an optional list item indicating membership of a target user to that list.',\n        type: 'object',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['mutes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              mutes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetRelationships: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getRelationships',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates public relationships between one account, and a list of other accounts. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Primary account requesting relationships for.',\n            },\n            others: {\n              type: 'array',\n              description:\n                \"List of 'other' accounts to be related back to the primary.\",\n              maxLength: 30,\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['relationships'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              relationships: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.graph.defs#relationship',\n                    'lex:app.bsky.graph.defs#notFoundActor',\n                  ],\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ActorNotFound',\n            description:\n              'the primary actor at-identifier could not be resolved',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyGraphGetStarterPack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPack',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets a view of a starter pack.',\n        parameters: {\n          type: 'params',\n          required: ['starterPack'],\n          properties: {\n            starterPack: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the starter pack record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPack'],\n            properties: {\n              starterPack: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#starterPackView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get views for a list of starter packs.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacksWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacksWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacksWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacksWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      starterPackWithMembership: {\n        description:\n          'A starter pack and an optional list item indicating membership of a target user to that starter pack.',\n        type: 'object',\n        required: ['starterPack'],\n        properties: {\n          starterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetSuggestedFollowsByActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getSuggestedFollowsByActor',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n              isFallback: {\n                type: 'boolean',\n                description:\n                  'DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid',\n                default: false,\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.list',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'purpose', 'createdAt'],\n          properties: {\n            purpose: {\n              type: 'ref',\n              description:\n                'Defines the purpose of the list (aka, moderation-oriented or curration-oriented)',\n              ref: 'lex:app.bsky.graph.defs#listPurpose',\n            },\n            name: {\n              type: 'string',\n              maxLength: 64,\n              minLength: 1,\n              description: 'Display name for list; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListblock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listblock',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a block relationship against an entire an entire list of accounts (actors).',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the mod list record.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListitem: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listitem',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'list', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'The account which is included on the list.',\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to the list record (app.bsky.graph.list).',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphSearchStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.searchStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find starter packs matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphStarterpack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.starterpack',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record defining a starter pack of actors and feeds for new users.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'list', 'createdAt'],\n          properties: {\n            name: {\n              type: 'string',\n              maxGraphemes: 50,\n              maxLength: 500,\n              minLength: 1,\n              description: 'Display name for starter pack; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            feeds: {\n              type: 'array',\n              maxLength: 3,\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.starterpack#feedItem',\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      feedItem: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified list of accounts. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified thread. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphVerification: {\n    lexicon: 1,\n    id: 'app.bsky.graph.verification',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'handle', 'displayName', 'createdAt'],\n          properties: {\n            subject: {\n              description: 'DID of the subject the verification applies to.',\n              type: 'string',\n              format: 'did',\n            },\n            handle: {\n              description:\n                'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n              type: 'string',\n              format: 'handle',\n            },\n            displayName: {\n              description:\n                'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n              type: 'string',\n            },\n            createdAt: {\n              description: 'Date of when the verification was created.',\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerDefs: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.defs',\n    defs: {\n      labelerView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      labelerViewDetailed: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'policies', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          policies: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          reasonTypes: {\n            description:\n              \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#reasonType',\n            },\n          },\n          subjectTypes: {\n            description:\n              'The set of subject types (account, record, etc) this service accepts reports on.',\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#subjectType',\n            },\n          },\n          subjectCollections: {\n            type: 'array',\n            description:\n              'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n            items: {\n              type: 'string',\n              format: 'nsid',\n            },\n          },\n        },\n      },\n      labelerViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      labelerPolicies: {\n        type: 'object',\n        required: ['labelValues'],\n        properties: {\n          labelValues: {\n            type: 'array',\n            description:\n              'The label values which this labeler publishes. May include global or custom labels.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValue',\n            },\n          },\n          labelValueDefinitions: {\n            type: 'array',\n            description:\n              'Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinition',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerGetServices: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.getServices',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of labeler services.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            detailed: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['views'],\n            properties: {\n              views: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.labeler.defs#labelerView',\n                    'lex:app.bsky.labeler.defs#labelerViewDetailed',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerService: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.service',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of the existence of labeler service.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['policies', 'createdAt'],\n          properties: {\n            policies: {\n              type: 'ref',\n              ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            reasonTypes: {\n              description:\n                \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n            },\n            subjectTypes: {\n              description:\n                'The set of subject types (account, record, etc) this service accepts reports on.',\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#subjectType',\n              },\n            },\n            subjectCollections: {\n              type: 'array',\n              description:\n                'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDeclaration: {\n    lexicon: 1,\n    id: 'app.bsky.notification.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"A declaration of the user's choices related to notifications that can be produced by them.\",\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowSubscriptions'],\n          properties: {\n            allowSubscriptions: {\n              type: 'string',\n              description:\n                \"A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.\",\n              knownValues: ['followers', 'mutuals', 'none'],\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDefs: {\n    lexicon: 1,\n    id: 'app.bsky.notification.defs',\n    defs: {\n      recordDeleted: {\n        type: 'object',\n        properties: {},\n      },\n      chatPreference: {\n        type: 'object',\n        required: ['include', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'accepted'],\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      filterablePreference: {\n        type: 'object',\n        required: ['include', 'list', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'follows'],\n          },\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preference: {\n        type: 'object',\n        required: ['list', 'push'],\n        properties: {\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preferences: {\n        type: 'object',\n        required: [\n          'chat',\n          'follow',\n          'like',\n          'likeViaRepost',\n          'mention',\n          'quote',\n          'reply',\n          'repost',\n          'repostViaRepost',\n          'starterpackJoined',\n          'subscribedPost',\n          'unverified',\n          'verified',\n        ],\n        properties: {\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#chatPreference',\n          },\n          follow: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          like: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          likeViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          mention: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          quote: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repostViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          starterpackJoined: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          subscribedPost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          unverified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          verified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n        },\n      },\n      activitySubscription: {\n        type: 'object',\n        required: ['post', 'reply'],\n        properties: {\n          post: {\n            type: 'boolean',\n          },\n          reply: {\n            type: 'boolean',\n          },\n        },\n      },\n      subjectActivitySubscription: {\n        description:\n          'Object used to store activity subscription data in stash.',\n        type: 'object',\n        required: ['subject', 'activitySubscription'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get notification-related preferences for an account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetUnreadCount: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getUnreadCount',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Count the number of unread notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            priority: {\n              type: 'boolean',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['count'],\n            properties: {\n              count: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListActivitySubscriptions: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listActivitySubscriptions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subscriptions'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subscriptions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListNotifications: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listNotifications',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            reasons: {\n              description: 'Notification reasons to include in response.',\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'A reason that matches the reason property of #notification.',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            priority: {\n              type: 'boolean',\n            },\n            cursor: {\n              type: 'string',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['notifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              notifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.notification.listNotifications#notification',\n                },\n              },\n              priority: {\n                type: 'boolean',\n              },\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      notification: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'author',\n          'reason',\n          'record',\n          'isRead',\n          'indexedAt',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          reason: {\n            type: 'string',\n            description:\n              'The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.',\n            knownValues: [\n              'like',\n              'repost',\n              'follow',\n              'mention',\n              'reply',\n              'quote',\n              'starterpack-joined',\n              'verified',\n              'unverified',\n              'like-via-repost',\n              'repost-via-repost',\n              'subscribed-post',\n              'contact-match',\n            ],\n          },\n          reasonSubject: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          record: {\n            type: 'unknown',\n          },\n          isRead: {\n            type: 'boolean',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutActivitySubscription: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putActivitySubscription',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'activitySubscription'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['priority'],\n            properties: {\n              priority: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferencesV2: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferencesV2',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              chat: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#chatPreference',\n              },\n              follow: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              like: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              likeViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              mention: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              quote: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              reply: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repostViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              starterpackJoined: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              subscribedPost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              unverified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              verified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationRegisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.registerPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Register to receive push notifications, via a specified service, for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n              ageRestricted: {\n                type: 'boolean',\n                description: 'Set to true when the actor is age restricted',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUnregisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.unregisterPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUpdateSeen: {\n    lexicon: 1,\n    id: 'app.bsky.notification.updateSeen',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify server that the requesting account has seen notifications. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['seenAt'],\n            properties: {\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyRichtextFacet: {\n    lexicon: 1,\n    id: 'app.bsky.richtext.facet',\n    defs: {\n      main: {\n        type: 'object',\n        description: 'Annotation of a sub-string within rich text.',\n        required: ['index', 'features'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.richtext.facet#byteSlice',\n          },\n          features: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.richtext.facet#mention',\n                'lex:app.bsky.richtext.facet#link',\n                'lex:app.bsky.richtext.facet#tag',\n              ],\n            },\n          },\n        },\n      },\n      mention: {\n        type: 'object',\n        description:\n          \"Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.\",\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      link: {\n        type: 'object',\n        description:\n          'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      tag: {\n        type: 'object',\n        description:\n          \"Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').\",\n        required: ['tag'],\n        properties: {\n          tag: {\n            type: 'string',\n            maxLength: 640,\n            maxGraphemes: 64,\n          },\n        },\n      },\n      byteSlice: {\n        type: 'object',\n        description:\n          'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.',\n        required: ['byteStart', 'byteEnd'],\n        properties: {\n          byteStart: {\n            type: 'integer',\n            minimum: 0,\n          },\n          byteEnd: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.defs',\n    defs: {\n      skeletonSearchPost: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonSearchActor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      skeletonSearchStarterPack: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      trendingTopic: {\n        type: 'object',\n        required: ['topic', 'link'],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n        },\n      },\n      skeletonTrend: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'dids',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          dids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n      },\n      trendView: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'actors',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          actors: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      threadItemPost: {\n        type: 'object',\n        required: [\n          'post',\n          'moreParents',\n          'moreReplies',\n          'opThread',\n          'hiddenByThreadgate',\n          'mutedByViewer',\n        ],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          moreParents: {\n            type: 'boolean',\n            description:\n              'This post has more parents that were not present in the response. This is just a boolean, without the number of parents.',\n          },\n          moreReplies: {\n            type: 'integer',\n            description:\n              'This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate.',\n          },\n          opThread: {\n            type: 'boolean',\n            description:\n              'This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread.',\n          },\n          hiddenByThreadgate: {\n            type: 'boolean',\n            description:\n              'The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.',\n          },\n          mutedByViewer: {\n            type: 'boolean',\n            description:\n              'This is by an account muted by the viewer requesting it.',\n          },\n        },\n      },\n      threadItemNoUnauthenticated: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemNotFound: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemBlocked: {\n        type: 'object',\n        required: ['author'],\n        properties: {\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      ageAssuranceState: {\n        type: 'object',\n        description:\n          'The computed state of the age assurance process, returned to the user in question on certain authenticated requests.',\n        required: ['status'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description: 'Object used to store age assurance data in stash.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for AA.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetAgeAssuranceState: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getAgeAssuranceState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get miscellaneous runtime configuration.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              checkEmailConfirmed: {\n                type: 'boolean',\n              },\n              liveNow: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getConfig#liveNowConfig',\n                },\n              },\n            },\n          },\n        },\n      },\n      liveNowConfig: {\n        type: 'object',\n        required: ['did', 'domains'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          domains: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedOnboardingUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPopularFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPopularFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'An unspecced view of globally popular feed generators.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            query: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadOtherV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadOtherV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadOtherV2#threadItem',\n                },\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: ['lex:app.bsky.unspecced.defs#threadItemPost'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post.',\n            },\n            above: {\n              type: 'boolean',\n              description: 'Whether to include parents above the anchor.',\n              default: true,\n            },\n            below: {\n              type: 'integer',\n              description:\n                'How many levels of replies to include below the anchor.',\n              default: 6,\n              minimum: 0,\n              maximum: 20,\n            },\n            branchingFactor: {\n              type: 'integer',\n              description:\n                'Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).',\n              default: 10,\n              minimum: 0,\n              maximum: 100,\n            },\n            sort: {\n              type: 'string',\n              description: 'Sorting for the thread replies.',\n              knownValues: ['newest', 'oldest', 'top'],\n              default: 'oldest',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread', 'hasOtherReplies'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadV2#threadItem',\n                },\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n              hasOtherReplies: {\n                type: 'boolean',\n                description:\n                  'Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them.',\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.unspecced.defs#threadItemPost',\n              'lex:app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n              'lex:app.bsky.unspecced.defs#threadItemNotFound',\n              'lex:app.bsky.unspecced.defs#threadItemBlocked',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested feeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedOnboardingUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedOnboardingUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestionsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestionsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            relativeToDid: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n              relativeToDid: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.',\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTaggedSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTaggedSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggestions (feeds and users) tagged with categories',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',\n                },\n              },\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['tag', 'subjectType', 'subject'],\n        properties: {\n          tag: {\n            type: 'string',\n          },\n          subjectType: {\n            type: 'string',\n            knownValues: ['actor', 'feed'],\n          },\n          subject: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendingTopics: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendingTopics',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of trending topics',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['topics', 'suggested'],\n            properties: {\n              topics: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n              suggested: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrends: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrends',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get the current trends on the network',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonTrend',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedInitAgeAssurance: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.initAgeAssurance',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchActorsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchActorsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Actors (profile) search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            typeahead: {\n              type: 'boolean',\n              description: \"If true, acts as fast/simple 'typeahead' query.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchPostsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchPostsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Posts search, returns only skeleton',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                \"DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Starter Pack search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchStarterPack',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyVideoDefs: {\n    lexicon: 1,\n    id: 'app.bsky.video.defs',\n    defs: {\n      jobStatus: {\n        type: 'object',\n        required: ['jobId', 'did', 'state'],\n        properties: {\n          jobId: {\n            type: 'string',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          state: {\n            type: 'string',\n            description:\n              'The state of the video processing job. All values not listed as a known value indicate that the job is in process.',\n            knownValues: ['JOB_STATE_COMPLETED', 'JOB_STATE_FAILED'],\n          },\n          progress: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n            description: 'Progress within the current processing state.',\n          },\n          blob: {\n            type: 'blob',\n          },\n          error: {\n            type: 'string',\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetJobStatus: {\n    lexicon: 1,\n    id: 'app.bsky.video.getJobStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get status details for a video processing job.',\n        parameters: {\n          type: 'params',\n          required: ['jobId'],\n          properties: {\n            jobId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetUploadLimits: {\n    lexicon: 1,\n    id: 'app.bsky.video.getUploadLimits',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get video upload limits for the authenticated user.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canUpload'],\n            properties: {\n              canUpload: {\n                type: 'boolean',\n              },\n              remainingDailyVideos: {\n                type: 'integer',\n              },\n              remainingDailyBytes: {\n                type: 'integer',\n              },\n              message: {\n                type: 'string',\n              },\n              error: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoUploadVideo: {\n    lexicon: 1,\n    id: 'app.bsky.video.uploadVideo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Upload a video to be processed then stored on the PDS.',\n        input: {\n          encoding: 'video/mp4',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeclaration: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky chat account.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowIncoming'],\n          properties: {\n            allowIncoming: {\n              type: 'string',\n              knownValues: ['all', 'none', 'following'],\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          chatDisabled: {\n            type: 'boolean',\n            description:\n              'Set to true when the actor cannot actively participate in conversations',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeleteAccount: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorExportAccountData: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.exportAccountData',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/jsonl',\n        },\n      },\n    },\n  },\n  ChatBskyConvoAcceptConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.acceptConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rev: {\n                description:\n                  'Rev when the convo was accepted. If not present, the convo was already accepted.',\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoAddReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.addReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionLimitReached',\n            description:\n              \"Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent.\",\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.defs',\n    defs: {\n      messageRef: {\n        type: 'object',\n        required: ['did', 'messageId', 'convoId'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          convoId: {\n            type: 'string',\n          },\n          messageId: {\n            type: 'string',\n          },\n        },\n      },\n      messageInput: {\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record'],\n          },\n        },\n      },\n      messageView: {\n        type: 'object',\n        required: ['id', 'rev', 'text', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record#view'],\n          },\n          reactions: {\n            type: 'array',\n            description:\n              'Reactions to this message, in ascending order of creation time.',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.convo.defs#reactionView',\n            },\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      deletedMessageView: {\n        type: 'object',\n        required: ['id', 'rev', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      messageViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      reactionView: {\n        type: 'object',\n        required: ['value', 'sender', 'createdAt'],\n        properties: {\n          value: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionViewSender',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reactionViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      messageAndReactionView: {\n        type: 'object',\n        required: ['message', 'reaction'],\n        properties: {\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      convoView: {\n        type: 'object',\n        required: ['id', 'rev', 'members', 'muted', 'unreadCount'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          members: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.actor.defs#profileViewBasic',\n            },\n          },\n          lastMessage: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          lastReaction: {\n            type: 'union',\n            refs: ['lex:chat.bsky.convo.defs#messageAndReactionView'],\n          },\n          muted: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['request', 'accepted'],\n          },\n          unreadCount: {\n            type: 'integer',\n          },\n        },\n      },\n      logBeginConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logAcceptConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logLeaveConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logMuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logUnmuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logCreateMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logDeleteMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logReadMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logAddReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      logRemoveReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoDeleteMessageForSelf: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.deleteMessageForSelf',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#deletedMessageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvo',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoAvailability: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canChat'],\n            properties: {\n              canChat: {\n                type: 'boolean',\n              },\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoForMembers: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoForMembers',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetLog: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getLog',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: [],\n          properties: {\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['logs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              logs: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#logBeginConvo',\n                    'lex:chat.bsky.convo.defs#logAcceptConvo',\n                    'lex:chat.bsky.convo.defs#logLeaveConvo',\n                    'lex:chat.bsky.convo.defs#logMuteConvo',\n                    'lex:chat.bsky.convo.defs#logUnmuteConvo',\n                    'lex:chat.bsky.convo.defs#logCreateMessage',\n                    'lex:chat.bsky.convo.defs#logDeleteMessage',\n                    'lex:chat.bsky.convo.defs#logReadMessage',\n                    'lex:chat.bsky.convo.defs#logAddReaction',\n                    'lex:chat.bsky.convo.defs#logRemoveReaction',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetMessages: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getMessages',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoLeaveConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.leaveConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'rev'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              rev: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoListConvos: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.listConvos',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            readState: {\n              type: 'string',\n              knownValues: ['unread'],\n            },\n            status: {\n              type: 'string',\n              knownValues: ['request', 'accepted'],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              convos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#convoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoMuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.muteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoRemoveReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.removeReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoSendMessage: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessage',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'message'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageInput',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoSendMessageBatch: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessageBatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.sendMessageBatch#batchItem',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#messageView',\n                },\n              },\n            },\n          },\n        },\n      },\n      batchItem: {\n        type: 'object',\n        required: ['convoId', 'message'],\n        properties: {\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageInput',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUnmuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.unmuteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateAllRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateAllRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              status: {\n                type: 'string',\n                knownValues: ['request', 'accepted'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['updatedCount'],\n            properties: {\n              updatedCount: {\n                description: 'The count of updated convos.',\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetActorMetadata: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getActorMetadata',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['day', 'month', 'all'],\n            properties: {\n              day: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              month: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              all: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n            },\n          },\n        },\n      },\n      metadata: {\n        type: 'object',\n        required: [\n          'messagesSent',\n          'messagesReceived',\n          'convos',\n          'convosStarted',\n        ],\n        properties: {\n          messagesSent: {\n            type: 'integer',\n          },\n          messagesReceived: {\n            type: 'integer',\n          },\n          convos: {\n            type: 'integer',\n          },\n          convosStarted: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetMessageContext: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getMessageContext',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['messageId'],\n          properties: {\n            convoId: {\n              type: 'string',\n              description:\n                'Conversation that the message is from. NOTE: this field will eventually be required.',\n            },\n            messageId: {\n              type: 'string',\n            },\n            before: {\n              type: 'integer',\n              default: 5,\n            },\n            after: {\n              type: 'integer',\n              default: 5,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationUpdateActorAccess: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.updateActorAccess',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor', 'allowAccess'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              allowAccess: {\n                type: 'boolean',\n              },\n              ref: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDefs: {\n    lexicon: 1,\n    id: 'com.atproto.admin.defs',\n    defs: {\n      statusAttr: {\n        type: 'object',\n        required: ['applied'],\n        properties: {\n          applied: {\n            type: 'boolean',\n          },\n          ref: {\n            type: 'string',\n          },\n        },\n      },\n      accountView: {\n        type: 'object',\n        required: ['did', 'handle', 'indexedAt'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoRef: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      repoBlobRef: {\n        type: 'object',\n        required: ['did', 'cid'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      threatSignature: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.admin.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable an account from receiving new invite codes, but does not invalidate existing codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for disabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable some set of codes and/or all codes associated with a set of users.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminEnableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.enableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Re-enable an account's ability to receive invite codes.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for enabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfo: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about an account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfos: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['infos'],\n            properties: {\n              infos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get an admin view of invite codes.',\n        parameters: {\n          type: 'params',\n          properties: {\n            sort: {\n              type: 'string',\n              knownValues: ['recent', 'usage'],\n              default: 'recent',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 500,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getSubjectStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the service-specific admin status of a subject (account, record, or blob).',\n        parameters: {\n          type: 'params',\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            blob: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSearchAccounts: {\n    lexicon: 1,\n    id: 'com.atproto.admin.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of accounts that matches your search query.',\n        parameters: {\n          type: 'params',\n          properties: {\n            email: {\n              type: 'string',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSendEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.sendEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Send email to a user's account email address.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['recipientDid', 'content', 'senderDid'],\n            properties: {\n              recipientDid: {\n                type: 'string',\n                format: 'did',\n              },\n              content: {\n                type: 'string',\n              },\n              subject: {\n                type: 'string',\n              },\n              senderDid: {\n                type: 'string',\n                format: 'did',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  \"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sent'],\n            properties: {\n              sent: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account', 'email'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n                description: 'The handle or DID of the repo.',\n              },\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountHandle: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's handle.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'handle'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountPassword: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the password for a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Administrative action to update an account's signing key in their Did document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'signingKey'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              signingKey: {\n                type: 'string',\n                format: 'did',\n                description: 'Did-key formatted public key',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateSubjectStatus',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the service-specific admin status of a subject (account, record, or blob).',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityDefs: {\n    lexicon: 1,\n    id: 'com.atproto.identity.defs',\n    defs: {\n      identityInfo: {\n        type: 'object',\n        required: ['did', 'handle', 'didDoc'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.\",\n          },\n          didDoc: {\n            type: 'unknown',\n            description: 'The complete DID document for the identity.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityGetRecommendedDidCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.identity.getRecommendedDidCredentials',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Describe the credentials that should be included in the DID doc of an account that is migrating to this service.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rotationKeys: {\n                description:\n                  'Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.',\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityRefreshIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.refreshIdentity',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier'],\n            properties: {\n              identifier: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityRequestPlcOperationSignature: {\n    lexicon: 1,\n    id: 'com.atproto.identity.requestPlcOperationSignature',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to in order to request a signed PLC operation. Requires Auth.',\n      },\n    },\n  },\n  ComAtprotoIdentityResolveDid: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveDid',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves DID to DID document. Does not bi-directionally verify handle.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['didDoc'],\n            properties: {\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for the identity.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveHandle',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description: 'The handle to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveIdentity',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).',\n        parameters: {\n          type: 'params',\n          required: ['identifier'],\n          properties: {\n            identifier: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentitySignPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.signPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Signs a PLC operation to update some value(s) in the requesting DID's document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              token: {\n                description:\n                  'A token received through com.atproto.identity.requestPlcOperationSignature',\n                type: 'string',\n              },\n              rotationKeys: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n                description: 'A signed DID PLC operation.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentitySubmitPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.submitPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityUpdateHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.updateHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'The new handle.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelDefs: {\n    lexicon: 1,\n    id: 'com.atproto.label.defs',\n    defs: {\n      label: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto resource (eg, repo or record).',\n        required: ['src', 'uri', 'val', 'cts'],\n        properties: {\n          ver: {\n            type: 'integer',\n            description: 'The AT Protocol version of the label object.',\n          },\n          src: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the actor who created this label.',\n          },\n          uri: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'AT URI of the record, repository (account), or other resource that this label applies to.',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n            description:\n              \"Optionally, CID specifying the specific version of 'uri' resource this label applies to.\",\n          },\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n          neg: {\n            type: 'boolean',\n            description:\n              'If true, this is a negation label, overwriting a previous label.',\n          },\n          cts: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when this label was created.',\n          },\n          exp: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp at which this label expires (no longer applies).',\n          },\n          sig: {\n            type: 'bytes',\n            description: 'Signature of dag-cbor encoded label.',\n          },\n        },\n      },\n      selfLabels: {\n        type: 'object',\n        description:\n          'Metadata tags on an atproto record, published by the author within the record.',\n        required: ['values'],\n        properties: {\n          values: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#selfLabel',\n            },\n            maxLength: 10,\n          },\n        },\n      },\n      selfLabel: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',\n        required: ['val'],\n        properties: {\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n        },\n      },\n      labelValueDefinition: {\n        type: 'object',\n        description:\n          'Declares a label value and its expected interpretations and behaviors.',\n        required: ['identifier', 'severity', 'blurs', 'locales'],\n        properties: {\n          identifier: {\n            type: 'string',\n            description:\n              \"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).\",\n            maxLength: 100,\n            maxGraphemes: 100,\n          },\n          severity: {\n            type: 'string',\n            description:\n              \"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.\",\n            knownValues: ['inform', 'alert', 'none'],\n          },\n          blurs: {\n            type: 'string',\n            description:\n              \"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.\",\n            knownValues: ['content', 'media', 'none'],\n          },\n          defaultSetting: {\n            type: 'string',\n            description: 'The default setting for this label.',\n            knownValues: ['ignore', 'warn', 'hide'],\n            default: 'warn',\n          },\n          adultOnly: {\n            type: 'boolean',\n            description:\n              'Does the user need to have adult content enabled in order to configure this label?',\n          },\n          locales: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',\n            },\n          },\n        },\n      },\n      labelValueDefinitionStrings: {\n        type: 'object',\n        description:\n          'Strings which describe the label in the UI, localized into a specific language.',\n        required: ['lang', 'name', 'description'],\n        properties: {\n          lang: {\n            type: 'string',\n            description:\n              'The code of the language these strings are written in.',\n            format: 'language',\n          },\n          name: {\n            type: 'string',\n            description: 'A short human-readable name for the label.',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            description:\n              'A longer description of what the label means and why it might be applied.',\n            maxGraphemes: 10000,\n            maxLength: 100000,\n          },\n        },\n      },\n      labelValue: {\n        type: 'string',\n        knownValues: [\n          '!hide',\n          '!no-promote',\n          '!warn',\n          '!no-unauthenticated',\n          'dmca-violation',\n          'doxxing',\n          'porn',\n          'sexual',\n          'nudity',\n          'nsfl',\n          'gore',\n        ],\n      },\n    },\n  },\n  ComAtprotoLabelQueryLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.queryLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.',\n        parameters: {\n          type: 'params',\n          required: ['uriPatterns'],\n          properties: {\n            uriPatterns: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                \"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.\",\n            },\n            sources: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n              description:\n                'Optional list of label sources (DIDs) to filter on.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelSubscribeLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.subscribeLabels',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.label.subscribeLabels#labels',\n              'lex:com.atproto.label.subscribeLabels#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n        ],\n      },\n      labels: {\n        type: 'object',\n        required: ['seq', 'labels'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLexiconResolveLexicon: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.resolveLexicon',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Resolves an atproto lexicon (NSID) to a schema.',\n        parameters: {\n          type: 'params',\n          properties: {\n            nsid: {\n              format: 'nsid',\n              type: 'string',\n              description: 'The lexicon NSID to resolve.',\n            },\n          },\n          required: ['nsid'],\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n                description: 'The CID of the lexicon schema record.',\n              },\n              schema: {\n                type: 'ref',\n                ref: 'lex:com.atproto.lexicon.schema#main',\n                description: 'The resolved lexicon schema record.',\n              },\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n                description: 'The AT-URI of the lexicon schema record.',\n              },\n            },\n            required: ['uri', 'cid', 'schema'],\n          },\n        },\n        errors: [\n          {\n            description: 'No lexicon was resolved for the NSID.',\n            name: 'LexiconNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoLexiconSchema: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.schema',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).\",\n        key: 'nsid',\n        record: {\n          type: 'object',\n          required: ['lexicon'],\n          properties: {\n            lexicon: {\n              type: 'integer',\n              description:\n                \"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.\",\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationCreateReport: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.createReport',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['reasonType', 'subject'],\n            properties: {\n              reasonType: {\n                type: 'ref',\n                description:\n                  'Indicates the broad category of violation the report is for.',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n                description:\n                  'Additional context about the content and violation.',\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.createReport#modTool',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'id',\n              'reasonType',\n              'subject',\n              'reportedBy',\n              'createdAt',\n            ],\n            properties: {\n              id: {\n                type: 'integer',\n              },\n              reasonType: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              reportedBy: {\n                type: 'string',\n                format: 'did',\n              },\n              createdAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationDefs: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'com.atproto.moderation.defs#reasonSpam',\n          'com.atproto.moderation.defs#reasonViolation',\n          'com.atproto.moderation.defs#reasonMisleading',\n          'com.atproto.moderation.defs#reasonSexual',\n          'com.atproto.moderation.defs#reasonRude',\n          'com.atproto.moderation.defs#reasonOther',\n          'com.atproto.moderation.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonSpam: {\n        type: 'token',\n        description:\n          'Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.',\n      },\n      reasonViolation: {\n        type: 'token',\n        description:\n          'Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.',\n      },\n      reasonMisleading: {\n        type: 'token',\n        description:\n          'Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.',\n      },\n      reasonSexual: {\n        type: 'token',\n        description:\n          'Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.',\n      },\n      reasonRude: {\n        type: 'token',\n        description:\n          'Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.',\n      },\n      reasonOther: {\n        type: 'token',\n        description:\n          'Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.',\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      subjectType: {\n        type: 'string',\n        description: 'Tag describing a type of subject that might be reported.',\n        knownValues: ['account', 'record', 'chat'],\n      },\n    },\n  },\n  ComAtprotoRepoApplyWrites: {\n    lexicon: 1,\n    id: 'com.atproto.repo.applyWrites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'writes'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              writes: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#create',\n                    'lex:com.atproto.repo.applyWrites#update',\n                    'lex:com.atproto.repo.applyWrites#delete',\n                  ],\n                  closed: true,\n                },\n              },\n              swapCommit: {\n                type: 'string',\n                description:\n                  'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              results: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#createResult',\n                    'lex:com.atproto.repo.applyWrites#updateResult',\n                    'lex:com.atproto.repo.applyWrites#deleteResult',\n                  ],\n                  closed: true,\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that the 'swapCommit' parameter did not match current commit.\",\n          },\n        ],\n      },\n      create: {\n        type: 'object',\n        description: 'Operation which creates a new record.',\n        required: ['collection', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            maxLength: 512,\n            format: 'record-key',\n            description:\n              'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      update: {\n        type: 'object',\n        description: 'Operation which updates an existing record.',\n        required: ['collection', 'rkey', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      delete: {\n        type: 'object',\n        description: 'Operation which deletes an existing record.',\n        required: ['collection', 'rkey'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n        },\n      },\n      createResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      updateResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      deleteResult: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n    },\n  },\n  ComAtprotoRepoCreateRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.createRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Create a single new repository record. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'record'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record itself. Must contain a $type field.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that 'swapCommit' didn't match current repo commit.\",\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDefs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.defs',\n    defs: {\n      commitMeta: {\n        type: 'object',\n        required: ['cid', 'rev'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoDeleteRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.deleteRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDescribeRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.describeRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about an account and repository, including the list of collections. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'handle',\n              'did',\n              'didDoc',\n              'collections',\n              'handleIsCorrect',\n            ],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for this account.',\n              },\n              collections: {\n                type: 'array',\n                description:\n                  'List of all the collections (NSIDs) for which this repo contains at least one record.',\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              handleIsCorrect: {\n                type: 'boolean',\n                description:\n                  'Indicates if handle is currently valid (resolves bi-directionally)',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a single record from a repository. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection', 'rkey'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record collection.',\n            },\n            rkey: {\n              type: 'string',\n              description: 'The Record Key.',\n              format: 'record-key',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'The CID of the version of the record. If not specified, then return the most recent version.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'value'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              value: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoImportRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.importRepo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.',\n        input: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListMissingBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listMissingBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blobs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blobs: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob',\n                },\n              },\n            },\n          },\n        },\n      },\n      recordBlob: {\n        type: 'object',\n        required: ['cid', 'recordUri'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListRecords: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List a range of records in a repository, matching a specific collection. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record type.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n              description: 'The number of records to return.',\n            },\n            cursor: {\n              type: 'string',\n            },\n            reverse: {\n              type: 'boolean',\n              description: 'Flag to reverse the order of the returned records.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              records: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listRecords#record',\n                },\n              },\n            },\n          },\n        },\n      },\n      record: {\n        type: 'object',\n        required: ['uri', 'cid', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoPutRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.putRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey', 'record'],\n            nullable: ['swapRecord'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record to write.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoStrongRef: {\n    lexicon: 1,\n    id: 'com.atproto.repo.strongRef',\n    description: 'A URI with a content-hash fingerprint.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoUploadBlob: {\n    lexicon: 1,\n    id: 'com.atproto.repo.uploadBlob',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.',\n        input: {\n          encoding: '*/*',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blob'],\n            properties: {\n              blob: {\n                type: 'blob',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerActivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.activateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.\",\n      },\n    },\n  },\n  ComAtprotoServerCheckAccountStatus: {\n    lexicon: 1,\n    id: 'com.atproto.server.checkAccountStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'activated',\n              'validDid',\n              'repoCommit',\n              'repoRev',\n              'repoBlocks',\n              'indexedRecords',\n              'privateStateValues',\n              'expectedBlobs',\n              'importedBlobs',\n            ],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              validDid: {\n                type: 'boolean',\n              },\n              repoCommit: {\n                type: 'string',\n                format: 'cid',\n              },\n              repoRev: {\n                type: 'string',\n              },\n              repoBlocks: {\n                type: 'integer',\n              },\n              indexedRecords: {\n                type: 'integer',\n              },\n              privateStateValues: {\n                type: 'integer',\n              },\n              expectedBlobs: {\n                type: 'integer',\n              },\n              importedBlobs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerConfirmEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.confirmEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'token'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountNotFound',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InvalidEmail',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an account. Implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Requested handle for the account.',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Pre-existing atproto DID, being imported to a new account.',\n              },\n              inviteCode: {\n                type: 'string',\n              },\n              verificationCode: {\n                type: 'string',\n              },\n              verificationPhone: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n                description:\n                  'Initial account password. May need to meet instance-specific password strength requirements.',\n              },\n              recoveryKey: {\n                type: 'string',\n                description:\n                  'DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.',\n              },\n              plcOp: {\n                type: 'unknown',\n                description:\n                  'A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            description:\n              'Account login session returned on successful account creation.',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID of the new account.',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'Complete DID document.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidHandle',\n          },\n          {\n            name: 'InvalidPassword',\n          },\n          {\n            name: 'InvalidInviteCode',\n          },\n          {\n            name: 'HandleNotAvailable',\n          },\n          {\n            name: 'UnsupportedDomain',\n          },\n          {\n            name: 'UnresolvableDid',\n          },\n          {\n            name: 'IncompatibleDidDoc',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an App Password.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description:\n                  'A short name for the App Password, to help distinguish them.',\n              },\n              privileged: {\n                type: 'boolean',\n                description:\n                  \"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.createAppPassword#appPassword',\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'password', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          password: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCode: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCode',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an invite code.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['useCount'],\n            properties: {\n              useCount: {\n                type: 'integer',\n              },\n              forAccount: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['code'],\n            properties: {\n              code: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create invite codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codeCount', 'useCount'],\n            properties: {\n              codeCount: {\n                type: 'integer',\n                default: 1,\n              },\n              useCount: {\n                type: 'integer',\n              },\n              forAccounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.createInviteCodes#accountCodes',\n                },\n              },\n            },\n          },\n        },\n      },\n      accountCodes: {\n        type: 'object',\n        required: ['account', 'codes'],\n        properties: {\n          account: {\n            type: 'string',\n          },\n          codes: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.createSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an authentication session.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier', 'password'],\n            properties: {\n              identifier: {\n                type: 'string',\n                description:\n                  'Handle or other identifier supported by the server for the authenticating user.',\n              },\n              password: {\n                type: 'string',\n              },\n              authFactorToken: {\n                type: 'string',\n              },\n              allowTakendown: {\n                type: 'boolean',\n                description:\n                  'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'AuthFactorTokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeactivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deactivateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              deleteAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'A recommendation to server as to how long they should hold onto the deactivated account before deleting.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDefs: {\n    lexicon: 1,\n    id: 'com.atproto.server.defs',\n    defs: {\n      inviteCode: {\n        type: 'object',\n        required: [\n          'code',\n          'available',\n          'disabled',\n          'forAccount',\n          'createdBy',\n          'createdAt',\n          'uses',\n        ],\n        properties: {\n          code: {\n            type: 'string',\n          },\n          available: {\n            type: 'integer',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          forAccount: {\n            type: 'string',\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          uses: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCodeUse',\n            },\n          },\n        },\n      },\n      inviteCodeUse: {\n        type: 'object',\n        required: ['usedBy', 'usedAt'],\n        properties: {\n          usedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          usedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password', 'token'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeleteSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        errors: [\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDescribeServer: {\n    lexicon: 1,\n    id: 'com.atproto.server.describeServer',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Describes the server's account creation requirements and capabilities. Implemented by PDS.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'availableUserDomains'],\n            properties: {\n              inviteCodeRequired: {\n                type: 'boolean',\n                description:\n                  'If true, an invite code must be supplied to create an account on this instance.',\n              },\n              phoneVerificationRequired: {\n                type: 'boolean',\n                description:\n                  'If true, a phone verification token must be supplied to create an account on this instance.',\n              },\n              availableUserDomains: {\n                type: 'array',\n                description:\n                  'List of domain suffixes that can be used in account handles.',\n                items: {\n                  type: 'string',\n                },\n              },\n              links: {\n                type: 'ref',\n                description: 'URLs of service policy documents.',\n                ref: 'lex:com.atproto.server.describeServer#links',\n              },\n              contact: {\n                type: 'ref',\n                description: 'Contact information',\n                ref: 'lex:com.atproto.server.describeServer#contact',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n            format: 'uri',\n          },\n          termsOfService: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      contact: {\n        type: 'object',\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerGetAccountInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.getAccountInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get all invite codes for the current account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            includeUsed: {\n              type: 'boolean',\n              default: true,\n            },\n            createAvailable: {\n              type: 'boolean',\n              default: true,\n              description:\n                \"Controls whether any new 'earned' but not 'created' invites should be created.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateCreate',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetServiceAuth: {\n    lexicon: 1,\n    id: 'com.atproto.server.getServiceAuth',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a signed token on behalf of the requesting DID for the requested service.',\n        parameters: {\n          type: 'params',\n          required: ['aud'],\n          properties: {\n            aud: {\n              type: 'string',\n              format: 'did',\n              description:\n                'The DID of the service that the token will be used to authenticate with',\n            },\n            exp: {\n              type: 'integer',\n              description:\n                'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',\n            },\n            lxm: {\n              type: 'string',\n              format: 'nsid',\n              description:\n                'Lexicon (XRPC) method to bind the requested token to',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadExpiration',\n            description:\n              'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.getSession',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about the current auth session. Requires auth.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'did'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerListAppPasswords: {\n    lexicon: 1,\n    id: 'com.atproto.server.listAppPasswords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all App Passwords.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['passwords'],\n            properties: {\n              passwords: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.listAppPasswords#appPassword',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRefreshSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.refreshSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  \"Hosting status of the account. If not specified, then assume 'active'.\",\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRequestAccountDelete: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestAccountDelete',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account deletion via email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailConfirmation: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailConfirmation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to confirm ownership of email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Request a token in order to update email.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['tokenRequired'],\n            properties: {\n              tokenRequired: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRequestPasswordReset: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestPasswordReset',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account password reset via email.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerReserveSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.server.reserveSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID to reserve a key for.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['signingKey'],\n            properties: {\n              signingKey: {\n                type: 'string',\n                description:\n                  'The public key for the reserved signing key, in did:key serialization.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerResetPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.resetPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Reset a user account password using a token.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'password'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRevokeAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.revokeAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Revoke an App Password by name.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerUpdateEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.updateEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              token: {\n                type: 'string',\n                description:\n                  \"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.\",\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'TokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncDefs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.defs',\n    defs: {\n      hostStatus: {\n        type: 'string',\n        knownValues: ['active', 'idle', 'offline', 'throttled', 'banned'],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlob: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlob',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cid'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the account.',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description: 'The CID of the blob to fetch',\n            },\n          },\n        },\n        output: {\n          encoding: '*/*',\n        },\n        errors: [\n          {\n            name: 'BlobNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlocks: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cids'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            cids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'BlockNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetCheckout: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getCheckout',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'DEPRECATED - please use com.atproto.sync.getRepo instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoSyncGetHead: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED - please use com.atproto.sync.getLatestCommit instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HeadNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetHostStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHostStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns information about a specified upstream host, as consumed by the server. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          required: ['hostname'],\n          properties: {\n            hostname: {\n              type: 'string',\n              description:\n                'Hostname of the host (eg, PDS or relay) being queried.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n              },\n              seq: {\n                type: 'integer',\n                description:\n                  'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n              },\n              accountCount: {\n                type: 'integer',\n                description:\n                  'Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.',\n              },\n              status: {\n                type: 'ref',\n                ref: 'lex:com.atproto.sync.defs#hostStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetLatestCommit: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getLatestCommit',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the current commit CID & revision of the specified repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cid', 'rev'],\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'collection', 'rkey'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            rkey: {\n              type: 'string',\n              description: 'Record Key',\n              format: 'record-key',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepo: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.\",\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description:\n                \"The revision ('rev') of the repo to create a diff from.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepoStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepoStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'active'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: [\n                  'takendown',\n                  'suspended',\n                  'deleted',\n                  'deactivated',\n                  'desynchronized',\n                  'throttled',\n                ],\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n                description:\n                  'Optional field, the current rev of the repo, if active=true',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description: 'Optional revision of the repo to list blobs since.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cids'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              cids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListHosts: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listHosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 200,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hosts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hosts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listHosts#host',\n                },\n                description:\n                  'Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.',\n              },\n            },\n          },\n        },\n      },\n      host: {\n        type: 'object',\n        required: ['hostname'],\n        properties: {\n          hostname: {\n            type: 'string',\n            description: 'hostname of server; not a URL (no scheme)',\n          },\n          seq: {\n            type: 'integer',\n            description:\n              'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n          },\n          accountCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:com.atproto.sync.defs#hostStatus',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listRepos#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did', 'head', 'rev'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          head: {\n            type: 'string',\n            format: 'cid',\n            description: 'Current repo commit CID',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n          active: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListReposByCollection: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listReposByCollection',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DIDs which have records with the given collection NSID.',\n        parameters: {\n          type: 'params',\n          required: ['collection'],\n          properties: {\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            limit: {\n              type: 'integer',\n              description:\n                'Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.',\n              minimum: 1,\n              maximum: 2000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listReposByCollection#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncNotifyOfUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.sync.notifyOfUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (usually a PDS) that is notifying of update.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncRequestCrawl: {\n    lexicon: 1,\n    id: 'com.atproto.sync.requestCrawl',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (eg, PDS) that is requesting to be crawled.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostBanned',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncSubscribeRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.subscribeRepos',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.sync.subscribeRepos#commit',\n              'lex:com.atproto.sync.subscribeRepos#sync',\n              'lex:com.atproto.sync.subscribeRepos#identity',\n              'lex:com.atproto.sync.subscribeRepos#account',\n              'lex:com.atproto.sync.subscribeRepos#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n          {\n            name: 'ConsumerTooSlow',\n            description:\n              'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.',\n          },\n        ],\n      },\n      commit: {\n        type: 'object',\n        description:\n          'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.',\n        required: [\n          'seq',\n          'rebase',\n          'tooBig',\n          'repo',\n          'commit',\n          'rev',\n          'since',\n          'blocks',\n          'ops',\n          'blobs',\n          'time',\n        ],\n        nullable: ['since'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          rebase: {\n            type: 'boolean',\n            description: 'DEPRECATED -- unused',\n          },\n          tooBig: {\n            type: 'boolean',\n            description:\n              'DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.',\n          },\n          repo: {\n            type: 'string',\n            format: 'did',\n            description:\n              \"The repo this event comes from. Note that all other message types name this field 'did'.\",\n          },\n          commit: {\n            type: 'cid-link',\n            description: 'Repo commit object CID.',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.',\n          },\n          since: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the last emitted commit from this repo (if any).',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.\",\n            maxLength: 2000000,\n          },\n          ops: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.sync.subscribeRepos#repoOp',\n              description:\n                'List of repo mutation operations in this commit (eg, records created, updated, or deleted).',\n            },\n            maxLength: 200,\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'cid-link',\n              description:\n                'DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.',\n            },\n          },\n          prevData: {\n            type: 'cid-link',\n            description:\n              \"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.\",\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      sync: {\n        type: 'object',\n        description:\n          'Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.',\n        required: ['seq', 'did', 'blocks', 'rev', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description:\n              'The account this repo event corresponds to. Must match that in the commit object.',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.\",\n            maxLength: 10000,\n          },\n          rev: {\n            type: 'string',\n            description:\n              'The rev of the commit. This value must match that in the commit object.',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      identity: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.\",\n        required: ['seq', 'did', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.\",\n          },\n        },\n      },\n      account: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.\",\n        required: ['seq', 'did', 'time', 'active'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a reason for why the account is not active.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n      repoOp: {\n        type: 'object',\n        description: 'A repo operation, ie a mutation of a single record.',\n        required: ['action', 'path', 'cid'],\n        nullable: ['cid'],\n        properties: {\n          action: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          path: {\n            type: 'string',\n          },\n          cid: {\n            type: 'cid-link',\n            description:\n              'For creates and updates, the new record CID. For deletions, null.',\n          },\n          prev: {\n            type: 'cid-link',\n            description:\n              'For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempAddReservedHandle: {\n    lexicon: 1,\n    id: 'com.atproto.temp.addReservedHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a handle to the set of reserved handles.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckHandleAvailability: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkHandleAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description:\n                'Tentative handle. Will be checked for availability or used to build handle suggestions.',\n            },\n            email: {\n              type: 'string',\n              description:\n                'User-provided email. Might be used to build handle suggestions.',\n            },\n            birthDate: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'User-provided birth date. Might be used to build handle suggestions.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'result'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Echo of the input handle.',\n              },\n              result: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.temp.checkHandleAvailability#resultAvailable',\n                  'lex:com.atproto.temp.checkHandleAvailability#resultUnavailable',\n                ],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n            description: 'An invalid email was provided.',\n          },\n        ],\n      },\n      resultAvailable: {\n        type: 'object',\n        description: 'Indicates the provided handle is available.',\n        properties: {},\n      },\n      resultUnavailable: {\n        type: 'object',\n        description:\n          'Indicates the provided handle is unavailable and gives suggestions of available handles.',\n        required: ['suggestions'],\n        properties: {\n          suggestions: {\n            type: 'array',\n            description:\n              'List of suggested handles based on the provided inputs.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.temp.checkHandleAvailability#suggestion',\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['handle', 'method'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          method: {\n            type: 'string',\n            description:\n              'Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckSignupQueue: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkSignupQueue',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Check accounts location in signup queue.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['activated'],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              placeInQueue: {\n                type: 'integer',\n              },\n              estimatedTimeMs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempDereferenceScope: {\n    lexicon: 1,\n    id: 'com.atproto.temp.dereferenceScope',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Allows finding the oauth permission scope from a reference',\n        parameters: {\n          type: 'params',\n          required: ['scope'],\n          properties: {\n            scope: {\n              type: 'string',\n              description: \"The scope reference (starts with 'ref:')\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['scope'],\n            properties: {\n              scope: {\n                type: 'string',\n                description: 'The full oauth permission scope',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidScopeReference',\n            description: 'An invalid scope reference was provided.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoTempFetchLabels: {\n    lexicon: 1,\n    id: 'com.atproto.temp.fetchLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.',\n        parameters: {\n          type: 'params',\n          properties: {\n            since: {\n              type: 'integer',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRequestPhoneVerification: {\n    lexicon: 1,\n    id: 'com.atproto.temp.requestPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a verification code to be sent to the supplied phone number',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phoneNumber'],\n            properties: {\n              phoneNumber: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRevokeAccountCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.temp.revokeAccountCredentials',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationCreateTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.createTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Administrative action to create a new, re-usable communication (email for now) template.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'contentMarkdown', 'name'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the template.',\n              },\n              contentMarkdown: {\n                type: 'string',\n                description:\n                  'Content of the template, markdown supported, can contain variable placeholders.',\n              },\n              subject: {\n                type: 'string',\n                description: 'Subject of the message, used in emails.',\n              },\n              lang: {\n                type: 'string',\n                format: 'language',\n                description: 'Message language.',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'DID of the user who is creating the template.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.communication.defs#templateView',\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateTemplateName',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneCommunicationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.defs',\n    defs: {\n      templateView: {\n        type: 'object',\n        required: [\n          'id',\n          'name',\n          'contentMarkdown',\n          'disabled',\n          'lastUpdatedBy',\n          'createdAt',\n          'updatedAt',\n        ],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          name: {\n            type: 'string',\n            description: 'Name of the template.',\n          },\n          subject: {\n            type: 'string',\n            description:\n              'Content of the template, can contain markdown and variable placeholders.',\n          },\n          contentMarkdown: {\n            type: 'string',\n            description: 'Subject of the message, used in emails.',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          lang: {\n            type: 'string',\n            format: 'language',\n            description: 'Message language.',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who last updated the template.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationDeleteTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.deleteTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a communication template.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationListTemplates: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.listTemplates',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of all communication templates.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['communicationTemplates'],\n            properties: {\n              communicationTemplates: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.communication.defs#templateView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationUpdateTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.updateTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Administrative action to update an existing communication template. Allows passing partial fields to patch specific fields only.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'ID of the template to be updated.',\n              },\n              name: {\n                type: 'string',\n                description: 'Name of the template.',\n              },\n              lang: {\n                type: 'string',\n                format: 'language',\n                description: 'Message language.',\n              },\n              contentMarkdown: {\n                type: 'string',\n                description:\n                  'Content of the template, markdown supported, can contain variable placeholders.',\n              },\n              subject: {\n                type: 'string',\n                description: 'Subject of the message, used in emails.',\n              },\n              updatedBy: {\n                type: 'string',\n                format: 'did',\n                description: 'DID of the user who is updating the template.',\n              },\n              disabled: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.communication.defs#templateView',\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateTemplateName',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneHostingGetAccountHistory: {\n    lexicon: 1,\n    id: 'tools.ozone.hosting.getAccountHistory',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get account history, e.g. log of updated email addresses or other identity information.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            events: {\n              type: 'array',\n              items: {\n                type: 'string',\n                knownValues: [\n                  'accountCreated',\n                  'emailUpdated',\n                  'emailConfirmed',\n                  'passwordUpdated',\n                  'handleUpdated',\n                ],\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.hosting.getAccountHistory#event',\n                },\n              },\n            },\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        required: ['details', 'createdBy', 'createdAt'],\n        properties: {\n          details: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.hosting.getAccountHistory#accountCreated',\n              'lex:tools.ozone.hosting.getAccountHistory#emailUpdated',\n              'lex:tools.ozone.hosting.getAccountHistory#emailConfirmed',\n              'lex:tools.ozone.hosting.getAccountHistory#passwordUpdated',\n              'lex:tools.ozone.hosting.getAccountHistory#handleUpdated',\n            ],\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      accountCreated: {\n        type: 'object',\n        required: [],\n        properties: {\n          email: {\n            type: 'string',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n        },\n      },\n      emailUpdated: {\n        type: 'object',\n        required: ['email'],\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n      emailConfirmed: {\n        type: 'object',\n        required: ['email'],\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n      passwordUpdated: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n      handleUpdated: {\n        type: 'object',\n        required: ['handle'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationCancelScheduledActions: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.cancelScheduledActions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Cancel all pending scheduled moderation actions for specified subjects',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjects'],\n            properties: {\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description:\n                  'Array of DID subjects to cancel scheduled actions for',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  'Optional comment describing the reason for cancellation',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.cancelScheduledActions#cancellationResults',\n          },\n        },\n      },\n      cancellationResults: {\n        type: 'object',\n        required: ['succeeded', 'failed'],\n        properties: {\n          succeeded: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n            description:\n              'DIDs for which all pending scheduled actions were successfully cancelled',\n          },\n          failed: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.cancelScheduledActions#failedCancellation',\n            },\n            description:\n              'DIDs for which cancellation failed with error details',\n          },\n        },\n      },\n      failedCancellation: {\n        type: 'object',\n        required: ['did', 'error'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          error: {\n            type: 'string',\n          },\n          errorCode: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.defs',\n    defs: {\n      modEventView: {\n        type: 'object',\n        required: [\n          'id',\n          'event',\n          'subject',\n          'subjectBlobCids',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          event: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#modEventTakedown',\n              'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n              'lex:tools.ozone.moderation.defs#modEventComment',\n              'lex:tools.ozone.moderation.defs#modEventReport',\n              'lex:tools.ozone.moderation.defs#modEventLabel',\n              'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n              'lex:tools.ozone.moderation.defs#modEventEscalate',\n              'lex:tools.ozone.moderation.defs#modEventMute',\n              'lex:tools.ozone.moderation.defs#modEventUnmute',\n              'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventEmail',\n              'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n              'lex:tools.ozone.moderation.defs#modEventDivert',\n              'lex:tools.ozone.moderation.defs#modEventTag',\n              'lex:tools.ozone.moderation.defs#accountEvent',\n              'lex:tools.ozone.moderation.defs#identityEvent',\n              'lex:tools.ozone.moderation.defs#recordEvent',\n              'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n              'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n              'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n              'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.admin.defs#repoRef',\n              'lex:com.atproto.repo.strongRef',\n              'lex:chat.bsky.convo.defs#messageRef',\n            ],\n          },\n          subjectBlobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          creatorHandle: {\n            type: 'string',\n          },\n          subjectHandle: {\n            type: 'string',\n          },\n          modTool: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modTool',\n          },\n        },\n      },\n      modEventViewDetail: {\n        type: 'object',\n        required: [\n          'id',\n          'event',\n          'subject',\n          'subjectBlobs',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          event: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#modEventTakedown',\n              'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n              'lex:tools.ozone.moderation.defs#modEventComment',\n              'lex:tools.ozone.moderation.defs#modEventReport',\n              'lex:tools.ozone.moderation.defs#modEventLabel',\n              'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n              'lex:tools.ozone.moderation.defs#modEventEscalate',\n              'lex:tools.ozone.moderation.defs#modEventMute',\n              'lex:tools.ozone.moderation.defs#modEventUnmute',\n              'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventEmail',\n              'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n              'lex:tools.ozone.moderation.defs#modEventDivert',\n              'lex:tools.ozone.moderation.defs#modEventTag',\n              'lex:tools.ozone.moderation.defs#accountEvent',\n              'lex:tools.ozone.moderation.defs#identityEvent',\n              'lex:tools.ozone.moderation.defs#recordEvent',\n              'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n              'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n              'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n              'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoView',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n              'lex:tools.ozone.moderation.defs#recordView',\n              'lex:tools.ozone.moderation.defs#recordViewNotFound',\n            ],\n          },\n          subjectBlobs: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.defs#blobView',\n            },\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          modTool: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modTool',\n          },\n        },\n      },\n      subjectStatusView: {\n        type: 'object',\n        required: ['id', 'subject', 'createdAt', 'updatedAt', 'reviewState'],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.admin.defs#repoRef',\n              'lex:com.atproto.repo.strongRef',\n              'lex:chat.bsky.convo.defs#messageRef',\n            ],\n          },\n          hosting: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#accountHosting',\n              'lex:tools.ozone.moderation.defs#recordHosting',\n            ],\n          },\n          subjectBlobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n          subjectRepoHandle: {\n            type: 'string',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing when the last update was made to the moderation status of the subject',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing the first moderation status impacting event was emitted on the subject',\n          },\n          reviewState: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectReviewState',\n          },\n          comment: {\n            type: 'string',\n            description: 'Sticky comment on the subject.',\n          },\n          priorityScore: {\n            type: 'integer',\n            description:\n              'Numeric value representing the level of priority. Higher score means higher priority.',\n            minimum: 0,\n            maximum: 100,\n          },\n          muteUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          muteReportingUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastReviewedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          lastReviewedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastReportedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastAppealedAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing when the author of the subject appealed a moderation action',\n          },\n          takendown: {\n            type: 'boolean',\n          },\n          appealed: {\n            type: 'boolean',\n            description:\n              'True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.',\n          },\n          suspendUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          tags: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          accountStats: {\n            description: 'Statistics related to the account subject',\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#accountStats',\n          },\n          recordsStats: {\n            description:\n              \"Statistics related to the record subjects authored by the subject's account\",\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordsStats',\n          },\n          accountStrike: {\n            description:\n              'Strike information for the account (account-level only)',\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#accountStrike',\n          },\n          ageAssuranceState: {\n            type: 'string',\n            description: 'Current age assurance state of the subject.',\n            knownValues: ['pending', 'assured', 'unknown', 'reset', 'blocked'],\n          },\n          ageAssuranceUpdatedBy: {\n            type: 'string',\n            description:\n              'Whether or not the last successful update to age assurance was made by the user or admin.',\n            knownValues: ['admin', 'user'],\n          },\n        },\n      },\n      subjectView: {\n        description:\n          \"Detailed view of a subject. For record subjects, the author's repo and profile will be returned.\",\n        type: 'object',\n        required: ['type', 'subject'],\n        properties: {\n          type: {\n            type: 'ref',\n            ref: 'lex:com.atproto.moderation.defs#subjectType',\n          },\n          subject: {\n            type: 'string',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoViewDetail',\n          },\n          profile: {\n            type: 'union',\n            refs: [],\n          },\n          record: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordViewDetail',\n          },\n        },\n      },\n      accountStats: {\n        description: 'Statistics about a particular account subject',\n        type: 'object',\n        properties: {\n          reportCount: {\n            description: 'Total number of reports on the account',\n            type: 'integer',\n          },\n          appealCount: {\n            description:\n              'Total number of appeals against a moderation action on the account',\n            type: 'integer',\n          },\n          suspendCount: {\n            description: 'Number of times the account was suspended',\n            type: 'integer',\n          },\n          escalateCount: {\n            description: 'Number of times the account was escalated',\n            type: 'integer',\n          },\n          takedownCount: {\n            description: 'Number of times the account was taken down',\n            type: 'integer',\n          },\n        },\n      },\n      recordsStats: {\n        description: 'Statistics about a set of record subject items',\n        type: 'object',\n        properties: {\n          totalReports: {\n            description:\n              'Cumulative sum of the number of reports on the items in the set',\n            type: 'integer',\n          },\n          reportedCount: {\n            description: 'Number of items that were reported at least once',\n            type: 'integer',\n          },\n          escalatedCount: {\n            description: 'Number of items that were escalated at least once',\n            type: 'integer',\n          },\n          appealedCount: {\n            description: 'Number of items that were appealed at least once',\n            type: 'integer',\n          },\n          subjectCount: {\n            description: 'Total number of item in the set',\n            type: 'integer',\n          },\n          pendingCount: {\n            description:\n              'Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state',\n            type: 'integer',\n          },\n          processedCount: {\n            description:\n              'Number of item currently in \"reviewNone\" or \"reviewClosed\" state',\n            type: 'integer',\n          },\n          takendownCount: {\n            description: 'Number of item currently taken down',\n            type: 'integer',\n          },\n        },\n      },\n      accountStrike: {\n        description: 'Strike information for an account',\n        type: 'object',\n        properties: {\n          activeStrikeCount: {\n            description:\n              'Current number of active strikes (excluding expired strikes)',\n            type: 'integer',\n          },\n          totalStrikeCount: {\n            description:\n              'Total number of strikes ever received (including expired strikes)',\n            type: 'integer',\n          },\n          firstStrikeAt: {\n            description: 'Timestamp of the first strike received',\n            type: 'string',\n            format: 'datetime',\n          },\n          lastStrikeAt: {\n            description: 'Timestamp of the most recent strike received',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      subjectReviewState: {\n        type: 'string',\n        knownValues: [\n          'tools.ozone.moderation.defs#reviewOpen',\n          'tools.ozone.moderation.defs#reviewEscalated',\n          'tools.ozone.moderation.defs#reviewClosed',\n          'tools.ozone.moderation.defs#reviewNone',\n        ],\n      },\n      reviewOpen: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator',\n      },\n      reviewEscalated: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator',\n      },\n      reviewClosed: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator',\n      },\n      reviewNone: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it',\n      },\n      modEventTakedown: {\n        type: 'object',\n        description: 'Take down a subject permanently or temporarily',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the takedown should be in effect before automatically expiring.',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that drove the decision.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\",\n          },\n          targetServices: {\n            type: 'array',\n            items: {\n              type: 'string',\n              knownValues: ['appview', 'pds'],\n            },\n            description:\n              'List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.',\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user for this violation.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n        },\n      },\n      modEventReverseTakedown: {\n        type: 'object',\n        description: 'Revert take down action on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policy infraction for which takedown is being reversed.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation. Usually set from the last policy infraction's severity.\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              \"Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity.\",\n          },\n        },\n      },\n      modEventResolveAppeal: {\n        type: 'object',\n        description: 'Resolve appeal on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe resolution.',\n          },\n        },\n      },\n      modEventComment: {\n        type: 'object',\n        description:\n          'Add a comment to a subject. An empty comment will clear any previously set sticky comment.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          sticky: {\n            type: 'boolean',\n            description: 'Make the comment persistent on the subject',\n          },\n        },\n      },\n      modEventReport: {\n        type: 'object',\n        description: 'Report a subject',\n        required: ['reportType'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          isReporterMuted: {\n            type: 'boolean',\n            description:\n              \"Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject.\",\n          },\n          reportType: {\n            type: 'ref',\n            ref: 'lex:com.atproto.moderation.defs#reasonType',\n          },\n        },\n      },\n      modEventLabel: {\n        type: 'object',\n        description: 'Apply/Negate labels on a subject',\n        required: ['createLabelVals', 'negateLabelVals'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          createLabelVals: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          negateLabelVals: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the label will remain on the subject. Only applies on labels that are being added.',\n          },\n        },\n      },\n      modEventPriorityScore: {\n        type: 'object',\n        description:\n          'Set priority score of the subject. Higher score means higher priority.',\n        required: ['score'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          score: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description:\n          'Age assurance info coming directly from users. Only works on DID subjects.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n      ageAssuranceOverrideEvent: {\n        type: 'object',\n        description:\n          'Age assurance status override by moderators. Only works on DID subjects.',\n        required: ['comment', 'status'],\n        properties: {\n          status: {\n            type: 'string',\n            description:\n              'The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.',\n            knownValues: ['assured', 'reset', 'blocked'],\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n          comment: {\n            type: 'string',\n            minLength: 1,\n            description: 'Comment describing the reason for the override.',\n          },\n        },\n      },\n      ageAssurancePurgeEvent: {\n        type: 'object',\n        description:\n          'Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only.',\n        required: ['comment'],\n        properties: {\n          comment: {\n            type: 'string',\n            minLength: 1,\n            description: 'Comment describing the reason for the purge.',\n          },\n        },\n      },\n      revokeAccountCredentialsEvent: {\n        type: 'object',\n        description:\n          'Account credentials revocation by moderators. Only works on DID subjects.',\n        required: ['comment'],\n        properties: {\n          comment: {\n            minLength: 1,\n            type: 'string',\n            description: 'Comment describing the reason for the revocation.',\n          },\n        },\n      },\n      modEventAcknowledge: {\n        type: 'object',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n        },\n      },\n      modEventEscalate: {\n        type: 'object',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      modEventMute: {\n        type: 'object',\n        description: 'Mute incoming reports on a subject',\n        required: ['durationInHours'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description: 'Indicates how long the subject should remain muted.',\n          },\n        },\n      },\n      modEventUnmute: {\n        type: 'object',\n        description: 'Unmute action on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n        },\n      },\n      modEventMuteReporter: {\n        type: 'object',\n        description: 'Mute incoming reports from an account',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the account should remain muted. Falsy value here means a permanent mute.',\n          },\n        },\n      },\n      modEventUnmuteReporter: {\n        type: 'object',\n        description: 'Unmute incoming reports from an account',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n        },\n      },\n      modEventEmail: {\n        type: 'object',\n        description: 'Keep a log of outgoing email to a user',\n        required: ['subjectLine'],\n        properties: {\n          subjectLine: {\n            type: 'string',\n            description: 'The subject line of the email sent to the user.',\n          },\n          content: {\n            type: 'string',\n            description: 'The content of the email sent to the user.',\n          },\n          comment: {\n            type: 'string',\n            description: 'Additional comment about the outgoing comm.',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that necessitated the email.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n          isDelivered: {\n            type: 'boolean',\n            description:\n              \"Indicates whether the email was successfully delivered to the user's inbox.\",\n          },\n        },\n      },\n      modEventDivert: {\n        type: 'object',\n        description:\n          \"Divert a record's blobs to a 3rd party service for further scanning/tagging\",\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      modEventTag: {\n        type: 'object',\n        description: 'Add/Remove a tag on a subject',\n        required: ['add', 'remove'],\n        properties: {\n          add: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n            description:\n              \"Tags to be added to the subject. If already exists, won't be duplicated.\",\n          },\n          remove: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n            description:\n              \"Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated.\",\n          },\n          comment: {\n            type: 'string',\n            description: 'Additional comment about added/removed tags.',\n          },\n        },\n      },\n      accountEvent: {\n        type: 'object',\n        description:\n          'Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp', 'active'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            knownValues: [\n              'unknown',\n              'deactivated',\n              'deleted',\n              'takendown',\n              'suspended',\n              'tombstoned',\n            ],\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      identityEvent: {\n        type: 'object',\n        description:\n          'Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          pdsHost: {\n            type: 'string',\n            format: 'uri',\n          },\n          tombstone: {\n            type: 'boolean',\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      recordEvent: {\n        type: 'object',\n        description:\n          'Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp', 'op'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          op: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      scheduleTakedownEvent: {\n        type: 'object',\n        description: 'Logs a scheduled takedown action for an account.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      cancelScheduledTakedownEvent: {\n        type: 'object',\n        description:\n          'Logs cancellation of a scheduled takedown action for an account.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      repoView: {\n        type: 'object',\n        required: [\n          'did',\n          'handle',\n          'relatedRecords',\n          'indexedAt',\n          'moderation',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoViewDetail: {\n        type: 'object',\n        required: [\n          'did',\n          'handle',\n          'relatedRecords',\n          'indexedAt',\n          'moderation',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderationDetail',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoViewNotFound: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      recordView: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'value',\n          'blobCids',\n          'indexedAt',\n          'moderation',\n          'repo',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n          blobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoView',\n          },\n        },\n      },\n      recordViewDetail: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'value',\n          'blobs',\n          'indexedAt',\n          'moderation',\n          'repo',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.defs#blobView',\n            },\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderationDetail',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoView',\n          },\n        },\n      },\n      recordViewNotFound: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      moderation: {\n        type: 'object',\n        properties: {\n          subjectStatus: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n        },\n      },\n      moderationDetail: {\n        type: 'object',\n        properties: {\n          subjectStatus: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n        },\n      },\n      blobView: {\n        type: 'object',\n        required: ['cid', 'mimeType', 'size', 'createdAt'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          mimeType: {\n            type: 'string',\n          },\n          size: {\n            type: 'integer',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          details: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#imageDetails',\n              'lex:tools.ozone.moderation.defs#videoDetails',\n            ],\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n        },\n      },\n      imageDetails: {\n        type: 'object',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n          },\n          height: {\n            type: 'integer',\n          },\n        },\n      },\n      videoDetails: {\n        type: 'object',\n        required: ['width', 'height', 'length'],\n        properties: {\n          width: {\n            type: 'integer',\n          },\n          height: {\n            type: 'integer',\n          },\n          length: {\n            type: 'integer',\n          },\n        },\n      },\n      accountHosting: {\n        type: 'object',\n        required: ['status'],\n        properties: {\n          status: {\n            type: 'string',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'unknown',\n            ],\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deletedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          reactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      recordHosting: {\n        type: 'object',\n        required: ['status'],\n        properties: {\n          status: {\n            type: 'string',\n            knownValues: ['deleted', 'unknown'],\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deletedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reporterStats: {\n        type: 'object',\n        required: [\n          'did',\n          'accountReportCount',\n          'recordReportCount',\n          'reportedAccountCount',\n          'reportedRecordCount',\n          'takendownAccountCount',\n          'takendownRecordCount',\n          'labeledAccountCount',\n          'labeledRecordCount',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          accountReportCount: {\n            type: 'integer',\n            description:\n              'The total number of reports made by the user on accounts.',\n          },\n          recordReportCount: {\n            type: 'integer',\n            description:\n              'The total number of reports made by the user on records.',\n          },\n          reportedAccountCount: {\n            type: 'integer',\n            description: 'The total number of accounts reported by the user.',\n          },\n          reportedRecordCount: {\n            type: 'integer',\n            description: 'The total number of records reported by the user.',\n          },\n          takendownAccountCount: {\n            type: 'integer',\n            description:\n              \"The total number of accounts taken down as a result of the user's reports.\",\n          },\n          takendownRecordCount: {\n            type: 'integer',\n            description:\n              \"The total number of records taken down as a result of the user's reports.\",\n          },\n          labeledAccountCount: {\n            type: 'integer',\n            description:\n              \"The total number of accounts labeled as a result of the user's reports.\",\n          },\n          labeledRecordCount: {\n            type: 'integer',\n            description:\n              \"The total number of records labeled as a result of the user's reports.\",\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'automod', 'ozone/workspace')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n      timelineEventPlcCreate: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for a PLC create operation',\n      },\n      timelineEventPlcOperation: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for generic PLC operation',\n      },\n      timelineEventPlcTombstone: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for a PLC tombstone operation',\n      },\n      scheduledActionView: {\n        type: 'object',\n        description: 'View of a scheduled moderation action',\n        required: ['id', 'action', 'did', 'createdBy', 'createdAt', 'status'],\n        properties: {\n          id: {\n            type: 'integer',\n            description: 'Auto-incrementing row ID',\n          },\n          action: {\n            type: 'string',\n            knownValues: ['takedown'],\n            description: 'Type of action to be executed',\n          },\n          eventData: {\n            type: 'unknown',\n            description:\n              'Serialized event object that will be propagated to the event when performed',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description: 'Subject DID for the action',\n          },\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Exact time to execute the action',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Earliest time to execute the action (for randomized scheduling)',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Latest time to execute the action (for randomized scheduling)',\n          },\n          randomizeExecution: {\n            type: 'boolean',\n            description:\n              'Whether execution time should be randomized within the specified range',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who created this scheduled action',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the scheduled action was created',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the scheduled action was last updated',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['pending', 'executed', 'cancelled', 'failed'],\n            description: 'Current status of the scheduled action',\n          },\n          lastExecutedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the action was last attempted to be executed',\n          },\n          lastFailureReason: {\n            type: 'string',\n            description: 'Reason for the last execution failure',\n          },\n          executionEventId: {\n            type: 'integer',\n            description:\n              'ID of the moderation event created when action was successfully executed',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationEmitEvent: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.emitEvent',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Take a moderation action on an actor.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['event', 'subject', 'createdBy'],\n            properties: {\n              event: {\n                type: 'union',\n                refs: [\n                  'lex:tools.ozone.moderation.defs#modEventTakedown',\n                  'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n                  'lex:tools.ozone.moderation.defs#modEventEscalate',\n                  'lex:tools.ozone.moderation.defs#modEventComment',\n                  'lex:tools.ozone.moderation.defs#modEventLabel',\n                  'lex:tools.ozone.moderation.defs#modEventReport',\n                  'lex:tools.ozone.moderation.defs#modEventMute',\n                  'lex:tools.ozone.moderation.defs#modEventUnmute',\n                  'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n                  'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n                  'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n                  'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n                  'lex:tools.ozone.moderation.defs#modEventEmail',\n                  'lex:tools.ozone.moderation.defs#modEventDivert',\n                  'lex:tools.ozone.moderation.defs#modEventTag',\n                  'lex:tools.ozone.moderation.defs#accountEvent',\n                  'lex:tools.ozone.moderation.defs#identityEvent',\n                  'lex:tools.ozone.moderation.defs#recordEvent',\n                  'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n                  'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n                  'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n                  'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n                  'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n                  'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n                  'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n                ],\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              subjectBlobCids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.defs#modTool',\n              },\n              externalId: {\n                type: 'string',\n                description:\n                  'An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modEventView',\n          },\n        },\n        errors: [\n          {\n            name: 'SubjectHasAction',\n          },\n          {\n            name: 'DuplicateExternalId',\n            description:\n              'An event with the same external ID already exists for the subject.',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetAccountTimeline: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getAccountTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get timeline of all available events of an account. This includes moderation events, account history and did history.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['timeline'],\n            properties: {\n              timeline: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItem',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n      timelineItem: {\n        type: 'object',\n        required: ['day', 'summary'],\n        properties: {\n          day: {\n            type: 'string',\n          },\n          summary: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItemSummary',\n            },\n          },\n        },\n      },\n      timelineItemSummary: {\n        type: 'object',\n        required: ['eventSubjectType', 'eventType', 'count'],\n        properties: {\n          eventSubjectType: {\n            type: 'string',\n            knownValues: ['account', 'record', 'chat'],\n          },\n          eventType: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.moderation.defs#modEventTakedown',\n              'tools.ozone.moderation.defs#modEventReverseTakedown',\n              'tools.ozone.moderation.defs#modEventComment',\n              'tools.ozone.moderation.defs#modEventReport',\n              'tools.ozone.moderation.defs#modEventLabel',\n              'tools.ozone.moderation.defs#modEventAcknowledge',\n              'tools.ozone.moderation.defs#modEventEscalate',\n              'tools.ozone.moderation.defs#modEventMute',\n              'tools.ozone.moderation.defs#modEventUnmute',\n              'tools.ozone.moderation.defs#modEventMuteReporter',\n              'tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'tools.ozone.moderation.defs#modEventEmail',\n              'tools.ozone.moderation.defs#modEventResolveAppeal',\n              'tools.ozone.moderation.defs#modEventDivert',\n              'tools.ozone.moderation.defs#modEventTag',\n              'tools.ozone.moderation.defs#accountEvent',\n              'tools.ozone.moderation.defs#identityEvent',\n              'tools.ozone.moderation.defs#recordEvent',\n              'tools.ozone.moderation.defs#modEventPriorityScore',\n              'tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'tools.ozone.moderation.defs#ageAssuranceEvent',\n              'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'tools.ozone.moderation.defs#timelineEventPlcCreate',\n              'tools.ozone.moderation.defs#timelineEventPlcOperation',\n              'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n              'tools.ozone.hosting.getAccountHistory#accountCreated',\n              'tools.ozone.hosting.getAccountHistory#emailConfirmed',\n              'tools.ozone.hosting.getAccountHistory#passwordUpdated',\n              'tools.ozone.hosting.getAccountHistory#handleUpdated',\n              'tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          count: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetEvent: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getEvent',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a moderation event.',\n        parameters: {\n          type: 'params',\n          required: ['id'],\n          properties: {\n            id: {\n              type: 'integer',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modEventViewDetail',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRecord: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a record.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordViewDetail',\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetRecords: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some records.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              records: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:tools.ozone.moderation.defs#recordViewDetail',\n                    'lex:tools.ozone.moderation.defs#recordViewNotFound',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRepo: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a repository.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoViewDetail',\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetReporterStats: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getReporterStats',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get reporter stats for a list of users.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['stats'],\n            properties: {\n              stats: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#reporterStats',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRepos: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some repositories.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:tools.ozone.moderation.defs#repoViewDetail',\n                    'lex:tools.ozone.moderation.defs#repoViewNotFound',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetSubjects: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getSubjects',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about subjects.',\n        parameters: {\n          type: 'params',\n          required: ['subjects'],\n          properties: {\n            subjects: {\n              type: 'array',\n              maxLength: 100,\n              minLength: 1,\n              items: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjects'],\n            properties: {\n              subjects: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#subjectView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationListScheduledActions: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.listScheduledActions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'List scheduled moderation actions with optional filtering',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['statuses'],\n            properties: {\n              startsAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'Filter actions scheduled to execute after this time',\n              },\n              endsBefore: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'Filter actions scheduled to execute before this time',\n              },\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description: 'Filter actions for specific DID subjects',\n              },\n              statuses: {\n                type: 'array',\n                minLength: 1,\n                items: {\n                  type: 'string',\n                  knownValues: ['pending', 'executed', 'cancelled', 'failed'],\n                },\n                description: 'Filter actions by status',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actions'],\n            properties: {\n              actions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#scheduledActionView',\n                },\n              },\n              cursor: {\n                type: 'string',\n                description: 'Cursor for next page of results',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationQueryEvents: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.queryEvents',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List moderation events related to a subject.',\n        parameters: {\n          type: 'params',\n          properties: {\n            types: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned.',\n            },\n            createdBy: {\n              type: 'string',\n              format: 'did',\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'desc',\n              enum: ['asc', 'desc'],\n              description:\n                'Sort direction for the events. Defaults to descending order of created at timestamp.',\n            },\n            createdAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Retrieve events created after a given timestamp',\n            },\n            createdBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Retrieve events created before a given timestamp',\n            },\n            subject: {\n              type: 'string',\n              format: 'uri',\n            },\n            collections: {\n              type: 'array',\n              maxLength: 20,\n              description:\n                \"If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n            subjectType: {\n              type: 'string',\n              description:\n                \"If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n              knownValues: ['account', 'record'],\n            },\n            includeAllUserRecords: {\n              type: 'boolean',\n              default: false,\n              description:\n                \"If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            hasComment: {\n              type: 'boolean',\n              description: 'If true, only events with comments are returned',\n            },\n            comment: {\n              type: 'string',\n              description:\n                'If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition.',\n            },\n            addedLabels: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these labels were added are returned',\n            },\n            removedLabels: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these labels were removed are returned',\n            },\n            addedTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these tags were added are returned',\n            },\n            removedTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these tags were removed are returned',\n            },\n            reportTypes: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            policies: {\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'If specified, only events where the action policies match any of the given policies are returned',\n              },\n            },\n            modTool: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where the modTool name matches any of the given values are returned',\n            },\n            batchId: {\n              type: 'string',\n              description:\n                'If specified, only events where the batchId matches the given value are returned',\n            },\n            ageAssuranceState: {\n              type: 'string',\n              description:\n                'If specified, only events where the age assurance state matches the given value are returned',\n              knownValues: [\n                'pending',\n                'assured',\n                'unknown',\n                'reset',\n                'blocked',\n              ],\n            },\n            withStrike: {\n              type: 'boolean',\n              description:\n                'If specified, only events where strikeCount value is set are returned.',\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#modEventView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationQueryStatuses: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.queryStatuses',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'View moderation statuses of subjects (record or repo).',\n        parameters: {\n          type: 'params',\n          properties: {\n            queueCount: {\n              type: 'integer',\n              description:\n                'Number of queues being used by moderators. Subjects will be split among all queues.',\n            },\n            queueIndex: {\n              type: 'integer',\n              description:\n                'Index of the queue to fetch subjects from. Works only when queueCount value is specified.',\n            },\n            queueSeed: {\n              type: 'string',\n              description: 'A seeder to shuffle/balance the queue items.',\n            },\n            includeAllUserRecords: {\n              type: 'boolean',\n              description:\n                \"All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned.\",\n            },\n            subject: {\n              type: 'string',\n              format: 'uri',\n              description: 'The subject to get the status for.',\n            },\n            comment: {\n              type: 'string',\n              description: 'Search subjects by keyword from comments',\n            },\n            reportedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reported after a given timestamp',\n            },\n            reportedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reported before a given timestamp',\n            },\n            reviewedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reviewed after a given timestamp',\n            },\n            hostingDeletedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was deleted after a given timestamp',\n            },\n            hostingDeletedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was deleted before a given timestamp',\n            },\n            hostingUpdatedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was updated after a given timestamp',\n            },\n            hostingUpdatedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was updated before a given timestamp',\n            },\n            hostingStatuses: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'Search subjects by the status of the associated record/account',\n            },\n            reviewedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reviewed before a given timestamp',\n            },\n            includeMuted: {\n              type: 'boolean',\n              description:\n                \"By default, we don't include muted subjects in the results. Set this to true to include them.\",\n            },\n            onlyMuted: {\n              type: 'boolean',\n              description:\n                'When set to true, only muted subjects and reporters will be returned.',\n            },\n            reviewState: {\n              type: 'string',\n              description: 'Specify when fetching subjects in a certain state',\n              knownValues: [\n                'tools.ozone.moderation.defs#reviewOpen',\n                'tools.ozone.moderation.defs#reviewClosed',\n                'tools.ozone.moderation.defs#reviewEscalated',\n                'tools.ozone.moderation.defs#reviewNone',\n              ],\n            },\n            ignoreSubjects: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'uri',\n              },\n            },\n            lastReviewedBy: {\n              type: 'string',\n              format: 'did',\n              description:\n                'Get all subject statuses that were reviewed by a specific moderator',\n            },\n            sortField: {\n              type: 'string',\n              default: 'lastReportedAt',\n              enum: [\n                'lastReviewedAt',\n                'lastReportedAt',\n                'reportedRecordsCount',\n                'takendownRecordsCount',\n                'priorityScore',\n              ],\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'desc',\n              enum: ['asc', 'desc'],\n            },\n            takendown: {\n              type: 'boolean',\n              description: 'Get subjects that were taken down',\n            },\n            appealed: {\n              type: 'boolean',\n              description: 'Get subjects in unresolved appealed status',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            tags: {\n              type: 'array',\n              maxLength: 25,\n              items: {\n                type: 'string',\n                description:\n                  'Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters',\n              },\n            },\n            excludeTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            collections: {\n              type: 'array',\n              maxLength: 20,\n              description:\n                \"If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n            subjectType: {\n              type: 'string',\n              description:\n                \"If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n              knownValues: ['account', 'record'],\n            },\n            minAccountSuspendCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many suspensions will be returned.',\n            },\n            minReportedRecordsCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many reported records will be returned.',\n            },\n            minTakendownRecordsCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many taken down records will be returned.',\n            },\n            minPriorityScore: {\n              minimum: 0,\n              maximum: 100,\n              type: 'integer',\n              description:\n                'If specified, only subjects that have priority score value above the given value will be returned.',\n            },\n            minStrikeCount: {\n              type: 'integer',\n              minimum: 1,\n              description:\n                'If specified, only subjects that belong to an account that has at least this many active strikes will be returned.',\n            },\n            ageAssuranceState: {\n              type: 'string',\n              description:\n                'If specified, only subjects with the given age assurance state will be returned.',\n              knownValues: [\n                'pending',\n                'assured',\n                'unknown',\n                'reset',\n                'blocked',\n              ],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjectStatuses'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subjectStatuses: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationScheduleAction: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.scheduleAction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Schedule a moderation action to be executed at a future time',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['action', 'subjects', 'createdBy', 'scheduling'],\n            properties: {\n              action: {\n                type: 'union',\n                refs: ['lex:tools.ozone.moderation.scheduleAction#takedown'],\n              },\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description: 'Array of DID subjects to schedule the action for',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n              },\n              scheduling: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.scheduleAction#schedulingConfig',\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.defs#modTool',\n                description:\n                  'This will be propagated to the moderation event when it is applied',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.scheduleAction#scheduledActionResults',\n          },\n        },\n      },\n      takedown: {\n        type: 'object',\n        description: 'Schedule a takedown action',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the takedown should be in effect before automatically expiring.',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that drove the decision.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user when takedown is applied.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n          emailContent: {\n            type: 'string',\n            description: 'Email content to be sent to the user upon takedown.',\n          },\n          emailSubject: {\n            type: 'string',\n            description:\n              'Subject of the email to be sent to the user upon takedown.',\n          },\n        },\n      },\n      schedulingConfig: {\n        type: 'object',\n        description: 'Configuration for when the action should be executed',\n        properties: {\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Exact time to execute the action',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Earliest time to execute the action (for randomized scheduling)',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Latest time to execute the action (for randomized scheduling)',\n          },\n        },\n      },\n      scheduledActionResults: {\n        type: 'object',\n        required: ['succeeded', 'failed'],\n        properties: {\n          succeeded: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n          failed: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.scheduleAction#failedScheduling',\n            },\n          },\n        },\n      },\n      failedScheduling: {\n        type: 'object',\n        required: ['subject', 'error'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          error: {\n            type: 'string',\n          },\n          errorCode: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationSearchRepos: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.searchRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Find repositories based on a search term.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead\",\n            },\n            q: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#repoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneReportDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.report.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      reasonOther: {\n        type: 'token',\n        description: 'An issue not included in these options',\n      },\n      reasonViolenceAnimal: {\n        type: 'token',\n        description: 'Animal welfare violations',\n      },\n      reasonViolenceThreats: {\n        type: 'token',\n        description: 'Threats or incitement',\n      },\n      reasonViolenceGraphicContent: {\n        type: 'token',\n        description: 'Graphic violent content',\n      },\n      reasonViolenceGlorification: {\n        type: 'token',\n        description: 'Glorification of violence',\n      },\n      reasonViolenceExtremistContent: {\n        type: 'token',\n        description:\n          \"Extremist content. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonViolenceTrafficking: {\n        type: 'token',\n        description: 'Human trafficking',\n      },\n      reasonViolenceOther: {\n        type: 'token',\n        description: 'Other violent content',\n      },\n      reasonSexualAbuseContent: {\n        type: 'token',\n        description: 'Adult sexual abuse content',\n      },\n      reasonSexualNCII: {\n        type: 'token',\n        description: 'Non-consensual intimate imagery',\n      },\n      reasonSexualDeepfake: {\n        type: 'token',\n        description: 'Deepfake adult content',\n      },\n      reasonSexualAnimal: {\n        type: 'token',\n        description: 'Animal sexual abuse',\n      },\n      reasonSexualUnlabeled: {\n        type: 'token',\n        description: 'Unlabelled adult content',\n      },\n      reasonSexualOther: {\n        type: 'token',\n        description: 'Other sexual violence content',\n      },\n      reasonChildSafetyCSAM: {\n        type: 'token',\n        description:\n          \"Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonChildSafetyGroom: {\n        type: 'token',\n        description:\n          \"Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonChildSafetyPrivacy: {\n        type: 'token',\n        description: 'Privacy violation involving a minor',\n      },\n      reasonChildSafetyHarassment: {\n        type: 'token',\n        description: 'Harassment or bullying of minors',\n      },\n      reasonChildSafetyOther: {\n        type: 'token',\n        description:\n          \"Other child safety. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonHarassmentTroll: {\n        type: 'token',\n        description: 'Trolling',\n      },\n      reasonHarassmentTargeted: {\n        type: 'token',\n        description: 'Targeted harassment',\n      },\n      reasonHarassmentHateSpeech: {\n        type: 'token',\n        description: 'Hate speech',\n      },\n      reasonHarassmentDoxxing: {\n        type: 'token',\n        description: 'Doxxing',\n      },\n      reasonHarassmentOther: {\n        type: 'token',\n        description: 'Other harassing or hateful content',\n      },\n      reasonMisleadingBot: {\n        type: 'token',\n        description: 'Fake account or bot',\n      },\n      reasonMisleadingImpersonation: {\n        type: 'token',\n        description: 'Impersonation',\n      },\n      reasonMisleadingSpam: {\n        type: 'token',\n        description: 'Spam',\n      },\n      reasonMisleadingScam: {\n        type: 'token',\n        description: 'Scam',\n      },\n      reasonMisleadingElections: {\n        type: 'token',\n        description: 'False information about elections',\n      },\n      reasonMisleadingOther: {\n        type: 'token',\n        description: 'Other misleading content',\n      },\n      reasonRuleSiteSecurity: {\n        type: 'token',\n        description: 'Hacking or system attacks',\n      },\n      reasonRuleProhibitedSales: {\n        type: 'token',\n        description: 'Promoting or selling prohibited items or services',\n      },\n      reasonRuleBanEvasion: {\n        type: 'token',\n        description: 'Banned user returning',\n      },\n      reasonRuleOther: {\n        type: 'token',\n        description: 'Other',\n      },\n      reasonSelfHarmContent: {\n        type: 'token',\n        description: 'Content promoting or depicting self-harm',\n      },\n      reasonSelfHarmED: {\n        type: 'token',\n        description: 'Eating disorders',\n      },\n      reasonSelfHarmStunts: {\n        type: 'token',\n        description: 'Dangerous challenges or activities',\n      },\n      reasonSelfHarmSubstances: {\n        type: 'token',\n        description: 'Dangerous substances or drug abuse',\n      },\n      reasonSelfHarmOther: {\n        type: 'token',\n        description: 'Other dangerous content',\n      },\n    },\n  },\n  ToolsOzoneSafelinkAddRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.addRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a new URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern', 'action', 'reason'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to apply the rule to',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              action: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#actionType',\n              },\n              reason: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#reasonType',\n              },\n              comment: {\n                type: 'string',\n                description: 'Optional comment about the decision',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'Author DID. Only respected when using admin auth',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidUrl',\n            description: 'The provided URL is invalid',\n          },\n          {\n            name: 'RuleAlreadyExists',\n            description: 'A rule for this URL/domain already exists',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSafelinkDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.defs',\n    defs: {\n      event: {\n        type: 'object',\n        description: 'An event for URL safety decisions',\n        required: [\n          'id',\n          'eventType',\n          'url',\n          'pattern',\n          'action',\n          'reason',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n            description: 'Auto-incrementing row ID',\n          },\n          eventType: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#eventType',\n          },\n          url: {\n            type: 'string',\n            description: 'The URL that this rule applies to',\n          },\n          pattern: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#patternType',\n          },\n          action: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#actionType',\n          },\n          reason: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#reasonType',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who created this rule',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          comment: {\n            type: 'string',\n            description: 'Optional comment about the decision',\n          },\n        },\n      },\n      eventType: {\n        type: 'string',\n        knownValues: ['addRule', 'updateRule', 'removeRule'],\n      },\n      patternType: {\n        type: 'string',\n        knownValues: ['domain', 'url'],\n      },\n      actionType: {\n        type: 'string',\n        knownValues: ['block', 'warn', 'whitelist'],\n      },\n      reasonType: {\n        type: 'string',\n        knownValues: ['csam', 'spam', 'phishing', 'none'],\n      },\n      urlRule: {\n        type: 'object',\n        description: 'Input for creating a URL safety rule',\n        required: [\n          'url',\n          'pattern',\n          'action',\n          'reason',\n          'createdBy',\n          'createdAt',\n          'updatedAt',\n        ],\n        properties: {\n          url: {\n            type: 'string',\n            description: 'The URL or domain to apply the rule to',\n          },\n          pattern: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#patternType',\n          },\n          action: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#actionType',\n          },\n          reason: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#reasonType',\n          },\n          comment: {\n            type: 'string',\n            description: 'Optional comment about the decision',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user added the rule.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when the rule was created',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when the rule was last updated',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkQueryEvents: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.queryEvents',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Query URL safety audit events',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              urls: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by specific URLs or domains',\n              },\n              patternType: {\n                type: 'string',\n                description: 'Filter by pattern type',\n              },\n              sortDirection: {\n                type: 'string',\n                knownValues: ['asc', 'desc'],\n                default: 'desc',\n                description: 'Sort direction',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n                description:\n                  'Next cursor for pagination. Only present if there are more results.',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.safelink.defs#event',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkQueryRules: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.queryRules',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Query URL safety rules',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              urls: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by specific URLs or domains',\n              },\n              patternType: {\n                type: 'string',\n                description: 'Filter by pattern type',\n              },\n              actions: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by action types',\n              },\n              reason: {\n                type: 'string',\n                description: 'Filter by reason type',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'Filter by rule creator',\n              },\n              sortDirection: {\n                type: 'string',\n                knownValues: ['asc', 'desc'],\n                default: 'desc',\n                description: 'Sort direction',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['rules'],\n            properties: {\n              cursor: {\n                type: 'string',\n                description:\n                  'Next cursor for pagination. Only present if there are more results.',\n              },\n              rules: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.safelink.defs#urlRule',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkRemoveRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.removeRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Remove an existing URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to remove the rule for',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  'Optional comment about why the rule is being removed',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Optional DID of the user. Only respected when using admin auth.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'RuleNotFound',\n            description: 'No active rule found for this URL/domain',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSafelinkUpdateRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.updateRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Update an existing URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern', 'action', 'reason'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to update the rule for',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              action: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#actionType',\n              },\n              reason: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#reasonType',\n              },\n              comment: {\n                type: 'string',\n                description: 'Optional comment about the update',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Optional DID to credit as the creator. Only respected for admin_token authentication.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'RuleNotFound',\n            description: 'No active rule found for this URL/domain',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneServerGetConfig: {\n    lexicon: 1,\n    id: 'tools.ozone.server.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: \"Get details about ozone's server configuration.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              appview: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              pds: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              blobDivert: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              chat: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              viewer: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#viewerConfig',\n              },\n              verifierDid: {\n                type: 'string',\n                format: 'did',\n                description: 'The did of the verifier used for verification.',\n              },\n            },\n          },\n        },\n      },\n      serviceConfig: {\n        type: 'object',\n        properties: {\n          url: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      viewerConfig: {\n        type: 'object',\n        properties: {\n          role: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetAddValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.addValues',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name', 'values'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to add values to',\n              },\n              values: {\n                type: 'array',\n                minLength: 1,\n                maxLength: 1000,\n                items: {\n                  type: 'string',\n                },\n                description: 'Array of string values to add to the set',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.set.defs',\n    defs: {\n      set: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            minLength: 3,\n            maxLength: 128,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n        },\n      },\n      setView: {\n        type: 'object',\n        required: ['name', 'setSize', 'createdAt', 'updatedAt'],\n        properties: {\n          name: {\n            type: 'string',\n            minLength: 3,\n            maxLength: 128,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n          setSize: {\n            type: 'integer',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetDeleteSet: {\n    lexicon: 1,\n    id: 'tools.ozone.set.deleteSet',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Delete an entire set. Attempting to delete a set that does not exist will result in an error.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to delete',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetDeleteValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.deleteValues',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name', 'values'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to delete values from',\n              },\n              values: {\n                type: 'array',\n                minLength: 1,\n                items: {\n                  type: 'string',\n                },\n                description: 'Array of string values to delete from the set',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetGetValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.getValues',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a specific set and its values',\n        parameters: {\n          type: 'params',\n          required: ['name'],\n          properties: {\n            name: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['set', 'values'],\n            properties: {\n              set: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.set.defs#setView',\n              },\n              values: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              cursor: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetQuerySets: {\n    lexicon: 1,\n    id: 'tools.ozone.set.querySets',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Query available sets',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            namePrefix: {\n              type: 'string',\n            },\n            sortBy: {\n              type: 'string',\n              enum: ['name', 'createdAt', 'updatedAt'],\n              default: 'name',\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'asc',\n              enum: ['asc', 'desc'],\n              description: 'Defaults to ascending order of name field.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sets'],\n            properties: {\n              sets: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.set.defs#setView',\n                },\n              },\n              cursor: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetUpsertSet: {\n    lexicon: 1,\n    id: 'tools.ozone.set.upsertSet',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create or update set metadata',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.set.defs#set',\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.set.defs#setView',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.defs',\n    defs: {\n      option: {\n        type: 'object',\n        required: [\n          'key',\n          'value',\n          'did',\n          'scope',\n          'createdBy',\n          'lastUpdatedBy',\n        ],\n        properties: {\n          key: {\n            type: 'string',\n            format: 'nsid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          value: {\n            type: 'unknown',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          managerRole: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n          scope: {\n            type: 'string',\n            knownValues: ['instance', 'personal'],\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingListOptions: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.listOptions',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List settings with optional filtering',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            scope: {\n              type: 'string',\n              knownValues: ['instance', 'personal'],\n              default: 'instance',\n            },\n            prefix: {\n              type: 'string',\n              description: 'Filter keys by prefix',\n            },\n            keys: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n              description:\n                'Filter for only the specified keys. Ignored if prefix is provided',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['options'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              options: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.setting.defs#option',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingRemoveOptions: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.removeOptions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete settings by key',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['keys', 'scope'],\n            properties: {\n              keys: {\n                type: 'array',\n                minLength: 1,\n                maxLength: 200,\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              scope: {\n                type: 'string',\n                knownValues: ['instance', 'personal'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingUpsertOption: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.upsertOption',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create or update setting option',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['key', 'scope', 'value'],\n            properties: {\n              key: {\n                type: 'string',\n                format: 'nsid',\n              },\n              scope: {\n                type: 'string',\n                knownValues: ['instance', 'personal'],\n              },\n              value: {\n                type: 'unknown',\n              },\n              description: {\n                type: 'string',\n                maxLength: 2000,\n              },\n              managerRole: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleTriage',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleAdmin',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['option'],\n            properties: {\n              option: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.setting.defs#option',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.defs',\n    defs: {\n      sigDetail: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureFindCorrelation: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.findCorrelation',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find all correlated threat signatures between 2 or more accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['details'],\n            properties: {\n              details: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.signature.defs#sigDetail',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureFindRelatedAccounts: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.findRelatedAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get accounts that share some matching threat signatures with the root account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.signature.findRelatedAccounts#relatedAccount',\n                },\n              },\n            },\n          },\n        },\n      },\n      relatedAccount: {\n        type: 'object',\n        required: ['account'],\n        properties: {\n          account: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n          similarities: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.signature.defs#sigDetail',\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureSearchAccounts: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Search for accounts that match one or more threat signature values.',\n        parameters: {\n          type: 'params',\n          required: ['values'],\n          properties: {\n            values: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneTeamAddMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.addMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a member to the ozone team. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'role'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              role: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleAdmin',\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleTriage',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.team.defs#member',\n          },\n        },\n        errors: [\n          {\n            name: 'MemberAlreadyExists',\n            description: 'Member already exists in the team.',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneTeamDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.team.defs',\n    defs: {\n      member: {\n        type: 'object',\n        required: ['did', 'role'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          profile: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n          },\n          role: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n        },\n      },\n      roleAdmin: {\n        type: 'token',\n        description:\n          'Admin role. Highest level of access, can perform all actions.',\n      },\n      roleModerator: {\n        type: 'token',\n        description: 'Moderator role. Can perform most actions.',\n      },\n      roleTriage: {\n        type: 'token',\n        description:\n          'Triage role. Mostly intended for monitoring and escalating issues.',\n      },\n      roleVerifier: {\n        type: 'token',\n        description: 'Verifier role. Only allowed to issue verifications.',\n      },\n    },\n  },\n  ToolsOzoneTeamDeleteMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.deleteMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a member from ozone team. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'MemberNotFound',\n            description: 'The member being deleted does not exist',\n          },\n          {\n            name: 'CannotDeleteSelf',\n            description: 'You can not delete yourself from the team',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneTeamListMembers: {\n    lexicon: 1,\n    id: 'tools.ozone.team.listMembers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all members with access to the ozone service.',\n        parameters: {\n          type: 'params',\n          properties: {\n            q: {\n              type: 'string',\n            },\n            disabled: {\n              type: 'boolean',\n            },\n            roles: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['members'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              members: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.team.defs#member',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneTeamUpdateMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.updateMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update a member in the ozone service. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              disabled: {\n                type: 'boolean',\n              },\n              role: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleAdmin',\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleTriage',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.team.defs#member',\n          },\n        },\n        errors: [\n          {\n            name: 'MemberNotFound',\n            description: 'The member being updated does not exist in the team',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneVerificationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.defs',\n    defs: {\n      verificationView: {\n        type: 'object',\n        description: 'Verification data for the associated subject.',\n        required: [\n          'issuer',\n          'uri',\n          'subject',\n          'handle',\n          'displayName',\n          'createdAt',\n        ],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'string',\n            format: 'did',\n            description: 'The subject of the verification.',\n          },\n          handle: {\n            type: 'string',\n            description:\n              'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            description:\n              'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n          revokeReason: {\n            type: 'string',\n            description:\n              'Describes the reason for revocation, also indicating that the verification is no longer valid.',\n          },\n          revokedAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was revoked.',\n            format: 'datetime',\n          },\n          revokedBy: {\n            type: 'string',\n            description: 'The user who revoked this verification.',\n            format: 'did',\n          },\n          subjectProfile: {\n            type: 'union',\n            refs: [],\n          },\n          issuerProfile: {\n            type: 'union',\n            refs: [],\n          },\n          subjectRepo: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoViewDetail',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n            ],\n          },\n          issuerRepo: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoViewDetail',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationGrantVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.grantVerifications',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Grant verifications to multiple subjects. Allows batch processing of up to 100 verifications at once.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications'],\n            properties: {\n              verifications: {\n                type: 'array',\n                description: 'Array of verification requests to process',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.grantVerifications#verificationInput',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications', 'failedVerifications'],\n            properties: {\n              verifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.defs#verificationView',\n                },\n              },\n              failedVerifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.grantVerifications#grantError',\n                },\n              },\n            },\n          },\n        },\n      },\n      verificationInput: {\n        type: 'object',\n        required: ['subject', 'handle', 'displayName'],\n        properties: {\n          subject: {\n            type: 'string',\n            description: 'The did of the subject being verified',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            description:\n              'Handle of the subject the verification applies to at the moment of verifying.',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            description:\n              'Display name of the subject the verification applies to at the moment of verifying.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp for verification record. Defaults to current time when not specified.',\n          },\n        },\n      },\n      grantError: {\n        type: 'object',\n        description: 'Error object for failed verifications.',\n        required: ['error', 'subject'],\n        properties: {\n          error: {\n            type: 'string',\n            description: 'Error message describing the reason for failure.',\n          },\n          subject: {\n            type: 'string',\n            description: 'The did of the subject being verified',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationListVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.listVerifications',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List verifications',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'string',\n              description: 'Pagination cursor',\n            },\n            limit: {\n              type: 'integer',\n              description: 'Maximum number of results to return',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            createdAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Filter to verifications created after this timestamp',\n            },\n            createdBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Filter to verifications created before this timestamp',\n            },\n            issuers: {\n              type: 'array',\n              maxLength: 100,\n              description: 'Filter to verifications from specific issuers',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            subjects: {\n              type: 'array',\n              description: 'Filter to specific verified DIDs',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            sortDirection: {\n              type: 'string',\n              description: 'Sort direction for creation date',\n              enum: ['asc', 'desc'],\n              default: 'desc',\n            },\n            isRevoked: {\n              type: 'boolean',\n              description:\n                'Filter to verifications that are revoked or not. By default, includes both.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              verifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.defs#verificationView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationRevokeVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.revokeVerifications',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke previously granted verifications in batches of up to 100.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uris'],\n            properties: {\n              uris: {\n                type: 'array',\n                description: 'Array of verification record uris to revoke',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  description:\n                    'The AT-URI of the verification record to revoke.',\n                  format: 'at-uri',\n                },\n              },\n              revokeReason: {\n                type: 'string',\n                description:\n                  'Reason for revoking the verification. This is optional and can be omitted if not needed.',\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['revokedVerifications', 'failedRevocations'],\n            properties: {\n              revokedVerifications: {\n                type: 'array',\n                description: 'List of verification uris successfully revoked',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n              failedRevocations: {\n                type: 'array',\n                description:\n                  \"List of verification uris that couldn't be revoked, including failure reasons\",\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.revokeVerifications#revokeError',\n                },\n              },\n            },\n          },\n        },\n      },\n      revokeError: {\n        type: 'object',\n        description: 'Error object for failed revocations',\n        required: ['uri', 'error'],\n        properties: {\n          uri: {\n            type: 'string',\n            description:\n              'The AT-URI of the verification record that failed to revoke.',\n            format: 'at-uri',\n          },\n          error: {\n            type: 'string',\n            description:\n              'Description of the error that occurred during revocation.',\n          },\n        },\n      },\n    },\n  },\n} as const satisfies Record<string, LexiconDoc>\nexport const schemas = Object.values(schemaDict) satisfies LexiconDoc[]\nexport const lexicons: Lexicons = new Lexicons(schemas)\n\nexport function validate<T extends { $type: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType: true,\n): ValidationResult<T>\nexport function validate<T extends { $type?: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: false,\n): ValidationResult<T>\nexport function validate(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: boolean,\n): ValidationResult {\n  return (requiredType ? is$typed : maybe$typed)(v, id, hash)\n    ? lexicons.validate(`${id}#${hash}`, v)\n    : {\n        success: false,\n        error: new ValidationError(\n          `Must be an object with \"${hash === 'main' ? id : `${id}#${hash}`}\" $type property`,\n        ),\n      }\n}\n\nexport const ids = {\n  AppBskyActorDefs: 'app.bsky.actor.defs',\n  AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences',\n  AppBskyActorGetProfile: 'app.bsky.actor.getProfile',\n  AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles',\n  AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions',\n  AppBskyActorProfile: 'app.bsky.actor.profile',\n  AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',\n  AppBskyActorSearchActors: 'app.bsky.actor.searchActors',\n  AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',\n  AppBskyActorStatus: 'app.bsky.actor.status',\n  AppBskyAgeassuranceBegin: 'app.bsky.ageassurance.begin',\n  AppBskyAgeassuranceDefs: 'app.bsky.ageassurance.defs',\n  AppBskyAgeassuranceGetConfig: 'app.bsky.ageassurance.getConfig',\n  AppBskyAgeassuranceGetState: 'app.bsky.ageassurance.getState',\n  AppBskyBookmarkCreateBookmark: 'app.bsky.bookmark.createBookmark',\n  AppBskyBookmarkDefs: 'app.bsky.bookmark.defs',\n  AppBskyBookmarkDeleteBookmark: 'app.bsky.bookmark.deleteBookmark',\n  AppBskyBookmarkGetBookmarks: 'app.bsky.bookmark.getBookmarks',\n  AppBskyContactDefs: 'app.bsky.contact.defs',\n  AppBskyContactDismissMatch: 'app.bsky.contact.dismissMatch',\n  AppBskyContactGetMatches: 'app.bsky.contact.getMatches',\n  AppBskyContactGetSyncStatus: 'app.bsky.contact.getSyncStatus',\n  AppBskyContactImportContacts: 'app.bsky.contact.importContacts',\n  AppBskyContactRemoveData: 'app.bsky.contact.removeData',\n  AppBskyContactSendNotification: 'app.bsky.contact.sendNotification',\n  AppBskyContactStartPhoneVerification:\n    'app.bsky.contact.startPhoneVerification',\n  AppBskyContactVerifyPhone: 'app.bsky.contact.verifyPhone',\n  AppBskyDraftCreateDraft: 'app.bsky.draft.createDraft',\n  AppBskyDraftDefs: 'app.bsky.draft.defs',\n  AppBskyDraftDeleteDraft: 'app.bsky.draft.deleteDraft',\n  AppBskyDraftGetDrafts: 'app.bsky.draft.getDrafts',\n  AppBskyDraftUpdateDraft: 'app.bsky.draft.updateDraft',\n  AppBskyEmbedDefs: 'app.bsky.embed.defs',\n  AppBskyEmbedExternal: 'app.bsky.embed.external',\n  AppBskyEmbedImages: 'app.bsky.embed.images',\n  AppBskyEmbedRecord: 'app.bsky.embed.record',\n  AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',\n  AppBskyEmbedVideo: 'app.bsky.embed.video',\n  AppBskyFeedDefs: 'app.bsky.feed.defs',\n  AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator',\n  AppBskyFeedGenerator: 'app.bsky.feed.generator',\n  AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds',\n  AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes',\n  AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',\n  AppBskyFeedGetFeed: 'app.bsky.feed.getFeed',\n  AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator',\n  AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators',\n  AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton',\n  AppBskyFeedGetLikes: 'app.bsky.feed.getLikes',\n  AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed',\n  AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',\n  AppBskyFeedGetPosts: 'app.bsky.feed.getPosts',\n  AppBskyFeedGetQuotes: 'app.bsky.feed.getQuotes',\n  AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',\n  AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds',\n  AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline',\n  AppBskyFeedLike: 'app.bsky.feed.like',\n  AppBskyFeedPost: 'app.bsky.feed.post',\n  AppBskyFeedPostgate: 'app.bsky.feed.postgate',\n  AppBskyFeedRepost: 'app.bsky.feed.repost',\n  AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts',\n  AppBskyFeedSendInteractions: 'app.bsky.feed.sendInteractions',\n  AppBskyFeedThreadgate: 'app.bsky.feed.threadgate',\n  AppBskyGraphBlock: 'app.bsky.graph.block',\n  AppBskyGraphDefs: 'app.bsky.graph.defs',\n  AppBskyGraphFollow: 'app.bsky.graph.follow',\n  AppBskyGraphGetActorStarterPacks: 'app.bsky.graph.getActorStarterPacks',\n  AppBskyGraphGetBlocks: 'app.bsky.graph.getBlocks',\n  AppBskyGraphGetFollowers: 'app.bsky.graph.getFollowers',\n  AppBskyGraphGetFollows: 'app.bsky.graph.getFollows',\n  AppBskyGraphGetKnownFollowers: 'app.bsky.graph.getKnownFollowers',\n  AppBskyGraphGetList: 'app.bsky.graph.getList',\n  AppBskyGraphGetListBlocks: 'app.bsky.graph.getListBlocks',\n  AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes',\n  AppBskyGraphGetLists: 'app.bsky.graph.getLists',\n  AppBskyGraphGetListsWithMembership: 'app.bsky.graph.getListsWithMembership',\n  AppBskyGraphGetMutes: 'app.bsky.graph.getMutes',\n  AppBskyGraphGetRelationships: 'app.bsky.graph.getRelationships',\n  AppBskyGraphGetStarterPack: 'app.bsky.graph.getStarterPack',\n  AppBskyGraphGetStarterPacks: 'app.bsky.graph.getStarterPacks',\n  AppBskyGraphGetStarterPacksWithMembership:\n    'app.bsky.graph.getStarterPacksWithMembership',\n  AppBskyGraphGetSuggestedFollowsByActor:\n    'app.bsky.graph.getSuggestedFollowsByActor',\n  AppBskyGraphList: 'app.bsky.graph.list',\n  AppBskyGraphListblock: 'app.bsky.graph.listblock',\n  AppBskyGraphListitem: 'app.bsky.graph.listitem',\n  AppBskyGraphMuteActor: 'app.bsky.graph.muteActor',\n  AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList',\n  AppBskyGraphMuteThread: 'app.bsky.graph.muteThread',\n  AppBskyGraphSearchStarterPacks: 'app.bsky.graph.searchStarterPacks',\n  AppBskyGraphStarterpack: 'app.bsky.graph.starterpack',\n  AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor',\n  AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList',\n  AppBskyGraphUnmuteThread: 'app.bsky.graph.unmuteThread',\n  AppBskyGraphVerification: 'app.bsky.graph.verification',\n  AppBskyLabelerDefs: 'app.bsky.labeler.defs',\n  AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',\n  AppBskyLabelerService: 'app.bsky.labeler.service',\n  AppBskyNotificationDeclaration: 'app.bsky.notification.declaration',\n  AppBskyNotificationDefs: 'app.bsky.notification.defs',\n  AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',\n  AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',\n  AppBskyNotificationListActivitySubscriptions:\n    'app.bsky.notification.listActivitySubscriptions',\n  AppBskyNotificationListNotifications:\n    'app.bsky.notification.listNotifications',\n  AppBskyNotificationPutActivitySubscription:\n    'app.bsky.notification.putActivitySubscription',\n  AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',\n  AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',\n  AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',\n  AppBskyNotificationUnregisterPush: 'app.bsky.notification.unregisterPush',\n  AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',\n  AppBskyRichtextFacet: 'app.bsky.richtext.facet',\n  AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',\n  AppBskyUnspeccedGetAgeAssuranceState:\n    'app.bsky.unspecced.getAgeAssuranceState',\n  AppBskyUnspeccedGetConfig: 'app.bsky.unspecced.getConfig',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetPopularFeedGenerators:\n    'app.bsky.unspecced.getPopularFeedGenerators',\n  AppBskyUnspeccedGetPostThreadOtherV2:\n    'app.bsky.unspecced.getPostThreadOtherV2',\n  AppBskyUnspeccedGetPostThreadV2: 'app.bsky.unspecced.getPostThreadV2',\n  AppBskyUnspeccedGetSuggestedFeeds: 'app.bsky.unspecced.getSuggestedFeeds',\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton:\n    'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n  AppBskyUnspeccedGetSuggestedOnboardingUsers:\n    'app.bsky.unspecced.getSuggestedOnboardingUsers',\n  AppBskyUnspeccedGetSuggestedStarterPacks:\n    'app.bsky.unspecced.getSuggestedStarterPacks',\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetSuggestedUsers: 'app.bsky.unspecced.getSuggestedUsers',\n  AppBskyUnspeccedGetSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetSuggestionsSkeleton:\n    'app.bsky.unspecced.getSuggestionsSkeleton',\n  AppBskyUnspeccedGetTaggedSuggestions:\n    'app.bsky.unspecced.getTaggedSuggestions',\n  AppBskyUnspeccedGetTrendingTopics: 'app.bsky.unspecced.getTrendingTopics',\n  AppBskyUnspeccedGetTrends: 'app.bsky.unspecced.getTrends',\n  AppBskyUnspeccedGetTrendsSkeleton: 'app.bsky.unspecced.getTrendsSkeleton',\n  AppBskyUnspeccedInitAgeAssurance: 'app.bsky.unspecced.initAgeAssurance',\n  AppBskyUnspeccedSearchActorsSkeleton:\n    'app.bsky.unspecced.searchActorsSkeleton',\n  AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton',\n  AppBskyUnspeccedSearchStarterPacksSkeleton:\n    'app.bsky.unspecced.searchStarterPacksSkeleton',\n  AppBskyVideoDefs: 'app.bsky.video.defs',\n  AppBskyVideoGetJobStatus: 'app.bsky.video.getJobStatus',\n  AppBskyVideoGetUploadLimits: 'app.bsky.video.getUploadLimits',\n  AppBskyVideoUploadVideo: 'app.bsky.video.uploadVideo',\n  ChatBskyActorDeclaration: 'chat.bsky.actor.declaration',\n  ChatBskyActorDefs: 'chat.bsky.actor.defs',\n  ChatBskyActorDeleteAccount: 'chat.bsky.actor.deleteAccount',\n  ChatBskyActorExportAccountData: 'chat.bsky.actor.exportAccountData',\n  ChatBskyConvoAcceptConvo: 'chat.bsky.convo.acceptConvo',\n  ChatBskyConvoAddReaction: 'chat.bsky.convo.addReaction',\n  ChatBskyConvoDefs: 'chat.bsky.convo.defs',\n  ChatBskyConvoDeleteMessageForSelf: 'chat.bsky.convo.deleteMessageForSelf',\n  ChatBskyConvoGetConvo: 'chat.bsky.convo.getConvo',\n  ChatBskyConvoGetConvoAvailability: 'chat.bsky.convo.getConvoAvailability',\n  ChatBskyConvoGetConvoForMembers: 'chat.bsky.convo.getConvoForMembers',\n  ChatBskyConvoGetLog: 'chat.bsky.convo.getLog',\n  ChatBskyConvoGetMessages: 'chat.bsky.convo.getMessages',\n  ChatBskyConvoLeaveConvo: 'chat.bsky.convo.leaveConvo',\n  ChatBskyConvoListConvos: 'chat.bsky.convo.listConvos',\n  ChatBskyConvoMuteConvo: 'chat.bsky.convo.muteConvo',\n  ChatBskyConvoRemoveReaction: 'chat.bsky.convo.removeReaction',\n  ChatBskyConvoSendMessage: 'chat.bsky.convo.sendMessage',\n  ChatBskyConvoSendMessageBatch: 'chat.bsky.convo.sendMessageBatch',\n  ChatBskyConvoUnmuteConvo: 'chat.bsky.convo.unmuteConvo',\n  ChatBskyConvoUpdateAllRead: 'chat.bsky.convo.updateAllRead',\n  ChatBskyConvoUpdateRead: 'chat.bsky.convo.updateRead',\n  ChatBskyModerationGetActorMetadata: 'chat.bsky.moderation.getActorMetadata',\n  ChatBskyModerationGetMessageContext: 'chat.bsky.moderation.getMessageContext',\n  ChatBskyModerationUpdateActorAccess: 'chat.bsky.moderation.updateActorAccess',\n  ComAtprotoAdminDefs: 'com.atproto.admin.defs',\n  ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount',\n  ComAtprotoAdminDisableAccountInvites:\n    'com.atproto.admin.disableAccountInvites',\n  ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',\n  ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites',\n  ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo',\n  ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos',\n  ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes',\n  ComAtprotoAdminGetSubjectStatus: 'com.atproto.admin.getSubjectStatus',\n  ComAtprotoAdminSearchAccounts: 'com.atproto.admin.searchAccounts',\n  ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail',\n  ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail',\n  ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle',\n  ComAtprotoAdminUpdateAccountPassword:\n    'com.atproto.admin.updateAccountPassword',\n  ComAtprotoAdminUpdateAccountSigningKey:\n    'com.atproto.admin.updateAccountSigningKey',\n  ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus',\n  ComAtprotoIdentityDefs: 'com.atproto.identity.defs',\n  ComAtprotoIdentityGetRecommendedDidCredentials:\n    'com.atproto.identity.getRecommendedDidCredentials',\n  ComAtprotoIdentityRefreshIdentity: 'com.atproto.identity.refreshIdentity',\n  ComAtprotoIdentityRequestPlcOperationSignature:\n    'com.atproto.identity.requestPlcOperationSignature',\n  ComAtprotoIdentityResolveDid: 'com.atproto.identity.resolveDid',\n  ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle',\n  ComAtprotoIdentityResolveIdentity: 'com.atproto.identity.resolveIdentity',\n  ComAtprotoIdentitySignPlcOperation: 'com.atproto.identity.signPlcOperation',\n  ComAtprotoIdentitySubmitPlcOperation:\n    'com.atproto.identity.submitPlcOperation',\n  ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle',\n  ComAtprotoLabelDefs: 'com.atproto.label.defs',\n  ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels',\n  ComAtprotoLabelSubscribeLabels: 'com.atproto.label.subscribeLabels',\n  ComAtprotoLexiconResolveLexicon: 'com.atproto.lexicon.resolveLexicon',\n  ComAtprotoLexiconSchema: 'com.atproto.lexicon.schema',\n  ComAtprotoModerationCreateReport: 'com.atproto.moderation.createReport',\n  ComAtprotoModerationDefs: 'com.atproto.moderation.defs',\n  ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites',\n  ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord',\n  ComAtprotoRepoDefs: 'com.atproto.repo.defs',\n  ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord',\n  ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo',\n  ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',\n  ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo',\n  ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs',\n  ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',\n  ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',\n  ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',\n  ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',\n  ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount',\n  ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus',\n  ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',\n  ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',\n  ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',\n  ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',\n  ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes',\n  ComAtprotoServerCreateSession: 'com.atproto.server.createSession',\n  ComAtprotoServerDeactivateAccount: 'com.atproto.server.deactivateAccount',\n  ComAtprotoServerDefs: 'com.atproto.server.defs',\n  ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount',\n  ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession',\n  ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer',\n  ComAtprotoServerGetAccountInviteCodes:\n    'com.atproto.server.getAccountInviteCodes',\n  ComAtprotoServerGetServiceAuth: 'com.atproto.server.getServiceAuth',\n  ComAtprotoServerGetSession: 'com.atproto.server.getSession',\n  ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords',\n  ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',\n  ComAtprotoServerRequestAccountDelete:\n    'com.atproto.server.requestAccountDelete',\n  ComAtprotoServerRequestEmailConfirmation:\n    'com.atproto.server.requestEmailConfirmation',\n  ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',\n  ComAtprotoServerRequestPasswordReset:\n    'com.atproto.server.requestPasswordReset',\n  ComAtprotoServerReserveSigningKey: 'com.atproto.server.reserveSigningKey',\n  ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',\n  ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',\n  ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',\n  ComAtprotoSyncDefs: 'com.atproto.sync.defs',\n  ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',\n  ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',\n  ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',\n  ComAtprotoSyncGetHead: 'com.atproto.sync.getHead',\n  ComAtprotoSyncGetHostStatus: 'com.atproto.sync.getHostStatus',\n  ComAtprotoSyncGetLatestCommit: 'com.atproto.sync.getLatestCommit',\n  ComAtprotoSyncGetRecord: 'com.atproto.sync.getRecord',\n  ComAtprotoSyncGetRepo: 'com.atproto.sync.getRepo',\n  ComAtprotoSyncGetRepoStatus: 'com.atproto.sync.getRepoStatus',\n  ComAtprotoSyncListBlobs: 'com.atproto.sync.listBlobs',\n  ComAtprotoSyncListHosts: 'com.atproto.sync.listHosts',\n  ComAtprotoSyncListRepos: 'com.atproto.sync.listRepos',\n  ComAtprotoSyncListReposByCollection: 'com.atproto.sync.listReposByCollection',\n  ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate',\n  ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',\n  ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',\n  ComAtprotoTempAddReservedHandle: 'com.atproto.temp.addReservedHandle',\n  ComAtprotoTempCheckHandleAvailability:\n    'com.atproto.temp.checkHandleAvailability',\n  ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue',\n  ComAtprotoTempDereferenceScope: 'com.atproto.temp.dereferenceScope',\n  ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels',\n  ComAtprotoTempRequestPhoneVerification:\n    'com.atproto.temp.requestPhoneVerification',\n  ComAtprotoTempRevokeAccountCredentials:\n    'com.atproto.temp.revokeAccountCredentials',\n  ToolsOzoneCommunicationCreateTemplate:\n    'tools.ozone.communication.createTemplate',\n  ToolsOzoneCommunicationDefs: 'tools.ozone.communication.defs',\n  ToolsOzoneCommunicationDeleteTemplate:\n    'tools.ozone.communication.deleteTemplate',\n  ToolsOzoneCommunicationListTemplates:\n    'tools.ozone.communication.listTemplates',\n  ToolsOzoneCommunicationUpdateTemplate:\n    'tools.ozone.communication.updateTemplate',\n  ToolsOzoneHostingGetAccountHistory: 'tools.ozone.hosting.getAccountHistory',\n  ToolsOzoneModerationCancelScheduledActions:\n    'tools.ozone.moderation.cancelScheduledActions',\n  ToolsOzoneModerationDefs: 'tools.ozone.moderation.defs',\n  ToolsOzoneModerationEmitEvent: 'tools.ozone.moderation.emitEvent',\n  ToolsOzoneModerationGetAccountTimeline:\n    'tools.ozone.moderation.getAccountTimeline',\n  ToolsOzoneModerationGetEvent: 'tools.ozone.moderation.getEvent',\n  ToolsOzoneModerationGetRecord: 'tools.ozone.moderation.getRecord',\n  ToolsOzoneModerationGetRecords: 'tools.ozone.moderation.getRecords',\n  ToolsOzoneModerationGetRepo: 'tools.ozone.moderation.getRepo',\n  ToolsOzoneModerationGetReporterStats:\n    'tools.ozone.moderation.getReporterStats',\n  ToolsOzoneModerationGetRepos: 'tools.ozone.moderation.getRepos',\n  ToolsOzoneModerationGetSubjects: 'tools.ozone.moderation.getSubjects',\n  ToolsOzoneModerationListScheduledActions:\n    'tools.ozone.moderation.listScheduledActions',\n  ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents',\n  ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses',\n  ToolsOzoneModerationScheduleAction: 'tools.ozone.moderation.scheduleAction',\n  ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos',\n  ToolsOzoneReportDefs: 'tools.ozone.report.defs',\n  ToolsOzoneSafelinkAddRule: 'tools.ozone.safelink.addRule',\n  ToolsOzoneSafelinkDefs: 'tools.ozone.safelink.defs',\n  ToolsOzoneSafelinkQueryEvents: 'tools.ozone.safelink.queryEvents',\n  ToolsOzoneSafelinkQueryRules: 'tools.ozone.safelink.queryRules',\n  ToolsOzoneSafelinkRemoveRule: 'tools.ozone.safelink.removeRule',\n  ToolsOzoneSafelinkUpdateRule: 'tools.ozone.safelink.updateRule',\n  ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig',\n  ToolsOzoneSetAddValues: 'tools.ozone.set.addValues',\n  ToolsOzoneSetDefs: 'tools.ozone.set.defs',\n  ToolsOzoneSetDeleteSet: 'tools.ozone.set.deleteSet',\n  ToolsOzoneSetDeleteValues: 'tools.ozone.set.deleteValues',\n  ToolsOzoneSetGetValues: 'tools.ozone.set.getValues',\n  ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets',\n  ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet',\n  ToolsOzoneSettingDefs: 'tools.ozone.setting.defs',\n  ToolsOzoneSettingListOptions: 'tools.ozone.setting.listOptions',\n  ToolsOzoneSettingRemoveOptions: 'tools.ozone.setting.removeOptions',\n  ToolsOzoneSettingUpsertOption: 'tools.ozone.setting.upsertOption',\n  ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs',\n  ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation',\n  ToolsOzoneSignatureFindRelatedAccounts:\n    'tools.ozone.signature.findRelatedAccounts',\n  ToolsOzoneSignatureSearchAccounts: 'tools.ozone.signature.searchAccounts',\n  ToolsOzoneTeamAddMember: 'tools.ozone.team.addMember',\n  ToolsOzoneTeamDefs: 'tools.ozone.team.defs',\n  ToolsOzoneTeamDeleteMember: 'tools.ozone.team.deleteMember',\n  ToolsOzoneTeamListMembers: 'tools.ozone.team.listMembers',\n  ToolsOzoneTeamUpdateMember: 'tools.ozone.team.updateMember',\n  ToolsOzoneVerificationDefs: 'tools.ozone.verification.defs',\n  ToolsOzoneVerificationGrantVerifications:\n    'tools.ozone.verification.grantVerifications',\n  ToolsOzoneVerificationListVerifications:\n    'tools.ozone.verification.listVerifications',\n  ToolsOzoneVerificationRevokeVerifications:\n    'tools.ozone.verification.revokeVerifications',\n} as const\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyNotificationDefs from '../notification/defs.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'app.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  createdAt?: string\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n\nexport interface ProfileView {\n  $type?: 'app.bsky.actor.defs#profileView'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  description?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileView = 'profileView'\n\nexport function isProfileView<V>(v: V) {\n  return is$typed(v, id, hashProfileView)\n}\n\nexport function validateProfileView<V>(v: V) {\n  return validate<ProfileView & V>(v, id, hashProfileView)\n}\n\nexport interface ProfileViewDetailed {\n  $type?: 'app.bsky.actor.defs#profileViewDetailed'\n  did: string\n  handle: string\n  displayName?: string\n  description?: string\n  pronouns?: string\n  website?: string\n  avatar?: string\n  banner?: string\n  followersCount?: number\n  followsCount?: number\n  postsCount?: number\n  associated?: ProfileAssociated\n  joinedViaStarterPack?: AppBskyGraphDefs.StarterPackViewBasic\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewDetailed = 'profileViewDetailed'\n\nexport function isProfileViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashProfileViewDetailed)\n}\n\nexport function validateProfileViewDetailed<V>(v: V) {\n  return validate<ProfileViewDetailed & V>(v, id, hashProfileViewDetailed)\n}\n\nexport interface ProfileAssociated {\n  $type?: 'app.bsky.actor.defs#profileAssociated'\n  lists?: number\n  feedgens?: number\n  starterPacks?: number\n  labeler?: boolean\n  chat?: ProfileAssociatedChat\n  activitySubscription?: ProfileAssociatedActivitySubscription\n  germ?: ProfileAssociatedGerm\n}\n\nconst hashProfileAssociated = 'profileAssociated'\n\nexport function isProfileAssociated<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociated)\n}\n\nexport function validateProfileAssociated<V>(v: V) {\n  return validate<ProfileAssociated & V>(v, id, hashProfileAssociated)\n}\n\nexport interface ProfileAssociatedChat {\n  $type?: 'app.bsky.actor.defs#profileAssociatedChat'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n}\n\nconst hashProfileAssociatedChat = 'profileAssociatedChat'\n\nexport function isProfileAssociatedChat<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedChat)\n}\n\nexport function validateProfileAssociatedChat<V>(v: V) {\n  return validate<ProfileAssociatedChat & V>(v, id, hashProfileAssociatedChat)\n}\n\nexport interface ProfileAssociatedGerm {\n  $type?: 'app.bsky.actor.defs#profileAssociatedGerm'\n  messageMeUrl: string\n  showButtonTo: 'usersIFollow' | 'everyone' | (string & {})\n}\n\nconst hashProfileAssociatedGerm = 'profileAssociatedGerm'\n\nexport function isProfileAssociatedGerm<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedGerm)\n}\n\nexport function validateProfileAssociatedGerm<V>(v: V) {\n  return validate<ProfileAssociatedGerm & V>(v, id, hashProfileAssociatedGerm)\n}\n\nexport interface ProfileAssociatedActivitySubscription {\n  $type?: 'app.bsky.actor.defs#profileAssociatedActivitySubscription'\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n}\n\nconst hashProfileAssociatedActivitySubscription =\n  'profileAssociatedActivitySubscription'\n\nexport function isProfileAssociatedActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedActivitySubscription)\n}\n\nexport function validateProfileAssociatedActivitySubscription<V>(v: V) {\n  return validate<ProfileAssociatedActivitySubscription & V>(\n    v,\n    id,\n    hashProfileAssociatedActivitySubscription,\n  )\n}\n\n/** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.actor.defs#viewerState'\n  muted?: boolean\n  mutedByList?: AppBskyGraphDefs.ListViewBasic\n  blockedBy?: boolean\n  blocking?: string\n  blockingByList?: AppBskyGraphDefs.ListViewBasic\n  following?: string\n  followedBy?: string\n  knownFollowers?: KnownFollowers\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** The subject's followers whom you also follow */\nexport interface KnownFollowers {\n  $type?: 'app.bsky.actor.defs#knownFollowers'\n  count: number\n  followers: ProfileViewBasic[]\n}\n\nconst hashKnownFollowers = 'knownFollowers'\n\nexport function isKnownFollowers<V>(v: V) {\n  return is$typed(v, id, hashKnownFollowers)\n}\n\nexport function validateKnownFollowers<V>(v: V) {\n  return validate<KnownFollowers & V>(v, id, hashKnownFollowers)\n}\n\n/** Represents the verification information about the user this object is attached to. */\nexport interface VerificationState {\n  $type?: 'app.bsky.actor.defs#verificationState'\n  /** All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included. */\n  verifications: VerificationView[]\n  /** The user's status as a verified account. */\n  verifiedStatus: 'valid' | 'invalid' | 'none' | (string & {})\n  /** The user's status as a trusted verifier. */\n  trustedVerifierStatus: 'valid' | 'invalid' | 'none' | (string & {})\n}\n\nconst hashVerificationState = 'verificationState'\n\nexport function isVerificationState<V>(v: V) {\n  return is$typed(v, id, hashVerificationState)\n}\n\nexport function validateVerificationState<V>(v: V) {\n  return validate<VerificationState & V>(v, id, hashVerificationState)\n}\n\n/** An individual verification for an associated subject. */\nexport interface VerificationView {\n  $type?: 'app.bsky.actor.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** True if the verification passes validation, otherwise false. */\n  isValid: boolean\n  /** Timestamp when the verification was created. */\n  createdAt: string\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n\nexport type Preferences = (\n  | $Typed<AdultContentPref>\n  | $Typed<ContentLabelPref>\n  | $Typed<SavedFeedsPref>\n  | $Typed<SavedFeedsPrefV2>\n  | $Typed<PersonalDetailsPref>\n  | $Typed<DeclaredAgePref>\n  | $Typed<FeedViewPref>\n  | $Typed<ThreadViewPref>\n  | $Typed<InterestsPref>\n  | $Typed<MutedWordsPref>\n  | $Typed<HiddenPostsPref>\n  | $Typed<BskyAppStatePref>\n  | $Typed<LabelersPref>\n  | $Typed<PostInteractionSettingsPref>\n  | $Typed<VerificationPrefs>\n  | $Typed<LiveEventPreferences>\n  | { $type: string }\n)[]\n\nexport interface AdultContentPref {\n  $type?: 'app.bsky.actor.defs#adultContentPref'\n  enabled: boolean\n}\n\nconst hashAdultContentPref = 'adultContentPref'\n\nexport function isAdultContentPref<V>(v: V) {\n  return is$typed(v, id, hashAdultContentPref)\n}\n\nexport function validateAdultContentPref<V>(v: V) {\n  return validate<AdultContentPref & V>(v, id, hashAdultContentPref)\n}\n\nexport interface ContentLabelPref {\n  $type?: 'app.bsky.actor.defs#contentLabelPref'\n  /** Which labeler does this preference apply to? If undefined, applies globally. */\n  labelerDid?: string\n  label: string\n  visibility: 'ignore' | 'show' | 'warn' | 'hide' | (string & {})\n}\n\nconst hashContentLabelPref = 'contentLabelPref'\n\nexport function isContentLabelPref<V>(v: V) {\n  return is$typed(v, id, hashContentLabelPref)\n}\n\nexport function validateContentLabelPref<V>(v: V) {\n  return validate<ContentLabelPref & V>(v, id, hashContentLabelPref)\n}\n\nexport interface SavedFeed {\n  $type?: 'app.bsky.actor.defs#savedFeed'\n  id: string\n  type: 'feed' | 'list' | 'timeline' | (string & {})\n  value: string\n  pinned: boolean\n}\n\nconst hashSavedFeed = 'savedFeed'\n\nexport function isSavedFeed<V>(v: V) {\n  return is$typed(v, id, hashSavedFeed)\n}\n\nexport function validateSavedFeed<V>(v: V) {\n  return validate<SavedFeed & V>(v, id, hashSavedFeed)\n}\n\nexport interface SavedFeedsPrefV2 {\n  $type?: 'app.bsky.actor.defs#savedFeedsPrefV2'\n  items: SavedFeed[]\n}\n\nconst hashSavedFeedsPrefV2 = 'savedFeedsPrefV2'\n\nexport function isSavedFeedsPrefV2<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPrefV2)\n}\n\nexport function validateSavedFeedsPrefV2<V>(v: V) {\n  return validate<SavedFeedsPrefV2 & V>(v, id, hashSavedFeedsPrefV2)\n}\n\nexport interface SavedFeedsPref {\n  $type?: 'app.bsky.actor.defs#savedFeedsPref'\n  pinned: string[]\n  saved: string[]\n  timelineIndex?: number\n}\n\nconst hashSavedFeedsPref = 'savedFeedsPref'\n\nexport function isSavedFeedsPref<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPref)\n}\n\nexport function validateSavedFeedsPref<V>(v: V) {\n  return validate<SavedFeedsPref & V>(v, id, hashSavedFeedsPref)\n}\n\nexport interface PersonalDetailsPref {\n  $type?: 'app.bsky.actor.defs#personalDetailsPref'\n  /** The birth date of account owner. */\n  birthDate?: string\n}\n\nconst hashPersonalDetailsPref = 'personalDetailsPref'\n\nexport function isPersonalDetailsPref<V>(v: V) {\n  return is$typed(v, id, hashPersonalDetailsPref)\n}\n\nexport function validatePersonalDetailsPref<V>(v: V) {\n  return validate<PersonalDetailsPref & V>(v, id, hashPersonalDetailsPref)\n}\n\n/** 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. */\nexport interface DeclaredAgePref {\n  $type?: 'app.bsky.actor.defs#declaredAgePref'\n  /** Indicates if the user has declared that they are over 13 years of age. */\n  isOverAge13?: boolean\n  /** Indicates if the user has declared that they are over 16 years of age. */\n  isOverAge16?: boolean\n  /** Indicates if the user has declared that they are over 18 years of age. */\n  isOverAge18?: boolean\n}\n\nconst hashDeclaredAgePref = 'declaredAgePref'\n\nexport function isDeclaredAgePref<V>(v: V) {\n  return is$typed(v, id, hashDeclaredAgePref)\n}\n\nexport function validateDeclaredAgePref<V>(v: V) {\n  return validate<DeclaredAgePref & V>(v, id, hashDeclaredAgePref)\n}\n\nexport interface FeedViewPref {\n  $type?: 'app.bsky.actor.defs#feedViewPref'\n  /** The URI of the feed, or an identifier which describes the feed. */\n  feed: string\n  /** Hide replies in the feed. */\n  hideReplies?: boolean\n  /** Hide replies in the feed if they are not by followed users. */\n  hideRepliesByUnfollowed: boolean\n  /** Hide replies in the feed if they do not have this number of likes. */\n  hideRepliesByLikeCount?: number\n  /** Hide reposts in the feed. */\n  hideReposts?: boolean\n  /** Hide quote posts in the feed. */\n  hideQuotePosts?: boolean\n}\n\nconst hashFeedViewPref = 'feedViewPref'\n\nexport function isFeedViewPref<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPref)\n}\n\nexport function validateFeedViewPref<V>(v: V) {\n  return validate<FeedViewPref & V>(v, id, hashFeedViewPref)\n}\n\nexport interface ThreadViewPref {\n  $type?: 'app.bsky.actor.defs#threadViewPref'\n  /** Sorting mode for threads. */\n  sort?:\n    | 'oldest'\n    | 'newest'\n    | 'most-likes'\n    | 'random'\n    | 'hotness'\n    | (string & {})\n}\n\nconst hashThreadViewPref = 'threadViewPref'\n\nexport function isThreadViewPref<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPref)\n}\n\nexport function validateThreadViewPref<V>(v: V) {\n  return validate<ThreadViewPref & V>(v, id, hashThreadViewPref)\n}\n\nexport interface InterestsPref {\n  $type?: 'app.bsky.actor.defs#interestsPref'\n  /** A list of tags which describe the account owner's interests gathered during onboarding. */\n  tags: string[]\n}\n\nconst hashInterestsPref = 'interestsPref'\n\nexport function isInterestsPref<V>(v: V) {\n  return is$typed(v, id, hashInterestsPref)\n}\n\nexport function validateInterestsPref<V>(v: V) {\n  return validate<InterestsPref & V>(v, id, hashInterestsPref)\n}\n\nexport type MutedWordTarget = 'content' | 'tag' | (string & {})\n\n/** A word that the account owner has muted. */\nexport interface MutedWord {\n  $type?: 'app.bsky.actor.defs#mutedWord'\n  id?: string\n  /** The muted word itself. */\n  value: string\n  /** The intended targets of the muted word. */\n  targets: MutedWordTarget[]\n  /** Groups of users to apply the muted word to. If undefined, applies to all users. */\n  actorTarget: 'all' | 'exclude-following' | (string & {})\n  /** The date and time at which the muted word will expire and no longer be applied. */\n  expiresAt?: string\n}\n\nconst hashMutedWord = 'mutedWord'\n\nexport function isMutedWord<V>(v: V) {\n  return is$typed(v, id, hashMutedWord)\n}\n\nexport function validateMutedWord<V>(v: V) {\n  return validate<MutedWord & V>(v, id, hashMutedWord)\n}\n\nexport interface MutedWordsPref {\n  $type?: 'app.bsky.actor.defs#mutedWordsPref'\n  /** A list of words the account owner has muted. */\n  items: MutedWord[]\n}\n\nconst hashMutedWordsPref = 'mutedWordsPref'\n\nexport function isMutedWordsPref<V>(v: V) {\n  return is$typed(v, id, hashMutedWordsPref)\n}\n\nexport function validateMutedWordsPref<V>(v: V) {\n  return validate<MutedWordsPref & V>(v, id, hashMutedWordsPref)\n}\n\nexport interface HiddenPostsPref {\n  $type?: 'app.bsky.actor.defs#hiddenPostsPref'\n  /** A list of URIs of posts the account owner has hidden. */\n  items: string[]\n}\n\nconst hashHiddenPostsPref = 'hiddenPostsPref'\n\nexport function isHiddenPostsPref<V>(v: V) {\n  return is$typed(v, id, hashHiddenPostsPref)\n}\n\nexport function validateHiddenPostsPref<V>(v: V) {\n  return validate<HiddenPostsPref & V>(v, id, hashHiddenPostsPref)\n}\n\nexport interface LabelersPref {\n  $type?: 'app.bsky.actor.defs#labelersPref'\n  labelers: LabelerPrefItem[]\n}\n\nconst hashLabelersPref = 'labelersPref'\n\nexport function isLabelersPref<V>(v: V) {\n  return is$typed(v, id, hashLabelersPref)\n}\n\nexport function validateLabelersPref<V>(v: V) {\n  return validate<LabelersPref & V>(v, id, hashLabelersPref)\n}\n\nexport interface LabelerPrefItem {\n  $type?: 'app.bsky.actor.defs#labelerPrefItem'\n  did: string\n}\n\nconst hashLabelerPrefItem = 'labelerPrefItem'\n\nexport function isLabelerPrefItem<V>(v: V) {\n  return is$typed(v, id, hashLabelerPrefItem)\n}\n\nexport function validateLabelerPrefItem<V>(v: V) {\n  return validate<LabelerPrefItem & V>(v, id, hashLabelerPrefItem)\n}\n\n/** A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this. */\nexport interface BskyAppStatePref {\n  $type?: 'app.bsky.actor.defs#bskyAppStatePref'\n  activeProgressGuide?: BskyAppProgressGuide\n  /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */\n  queuedNudges?: string[]\n  /** Storage for NUXs the user has encountered. */\n  nuxs?: Nux[]\n}\n\nconst hashBskyAppStatePref = 'bskyAppStatePref'\n\nexport function isBskyAppStatePref<V>(v: V) {\n  return is$typed(v, id, hashBskyAppStatePref)\n}\n\nexport function validateBskyAppStatePref<V>(v: V) {\n  return validate<BskyAppStatePref & V>(v, id, hashBskyAppStatePref)\n}\n\n/** If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress. */\nexport interface BskyAppProgressGuide {\n  $type?: 'app.bsky.actor.defs#bskyAppProgressGuide'\n  guide: string\n}\n\nconst hashBskyAppProgressGuide = 'bskyAppProgressGuide'\n\nexport function isBskyAppProgressGuide<V>(v: V) {\n  return is$typed(v, id, hashBskyAppProgressGuide)\n}\n\nexport function validateBskyAppProgressGuide<V>(v: V) {\n  return validate<BskyAppProgressGuide & V>(v, id, hashBskyAppProgressGuide)\n}\n\n/** A new user experiences (NUX) storage object */\nexport interface Nux {\n  $type?: 'app.bsky.actor.defs#nux'\n  id: string\n  completed: boolean\n  /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */\n  data?: string\n  /** The date and time at which the NUX will expire and should be considered completed. */\n  expiresAt?: string\n}\n\nconst hashNux = 'nux'\n\nexport function isNux<V>(v: V) {\n  return is$typed(v, id, hashNux)\n}\n\nexport function validateNux<V>(v: V) {\n  return validate<Nux & V>(v, id, hashNux)\n}\n\n/** Preferences for how verified accounts appear in the app. */\nexport interface VerificationPrefs {\n  $type?: 'app.bsky.actor.defs#verificationPrefs'\n  /** Hide the blue check badges for verified accounts and trusted verifiers. */\n  hideBadges: boolean\n}\n\nconst hashVerificationPrefs = 'verificationPrefs'\n\nexport function isVerificationPrefs<V>(v: V) {\n  return is$typed(v, id, hashVerificationPrefs)\n}\n\nexport function validateVerificationPrefs<V>(v: V) {\n  return validate<VerificationPrefs & V>(v, id, hashVerificationPrefs)\n}\n\n/** Preferences for live events. */\nexport interface LiveEventPreferences {\n  $type?: 'app.bsky.actor.defs#liveEventPreferences'\n  /** A list of feed IDs that the user has hidden from live events. */\n  hiddenFeedIds?: string[]\n  /** Whether to hide all feeds from live events. */\n  hideAllFeeds: boolean\n}\n\nconst hashLiveEventPreferences = 'liveEventPreferences'\n\nexport function isLiveEventPreferences<V>(v: V) {\n  return is$typed(v, id, hashLiveEventPreferences)\n}\n\nexport function validateLiveEventPreferences<V>(v: V) {\n  return validate<LiveEventPreferences & V>(v, id, hashLiveEventPreferences)\n}\n\n/** 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. */\nexport interface PostInteractionSettingsPref {\n  $type?: 'app.bsky.actor.defs#postInteractionSettingsPref'\n  /** Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  threadgateAllowRules?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n  /** Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashPostInteractionSettingsPref = 'postInteractionSettingsPref'\n\nexport function isPostInteractionSettingsPref<V>(v: V) {\n  return is$typed(v, id, hashPostInteractionSettingsPref)\n}\n\nexport function validatePostInteractionSettingsPref<V>(v: V) {\n  return validate<PostInteractionSettingsPref & V>(\n    v,\n    id,\n    hashPostInteractionSettingsPref,\n  )\n}\n\nexport interface StatusView {\n  $type?: 'app.bsky.actor.defs#statusView'\n  uri?: string\n  cid?: string\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  record: { [_ in string]: unknown }\n  embed?: $Typed<AppBskyEmbedExternal.View> | { $type: string }\n  /** The date when this status will expire. The application might choose to no longer return the status after expiration. */\n  expiresAt?: string\n  /** True if the status is not expired, false if it is expired. Only present if expiration was set. */\n  isActive?: boolean\n  /** True if the user's go-live access has been disabled by a moderator, false otherwise. */\n  isDisabled?: boolean\n}\n\nconst hashStatusView = 'statusView'\n\nexport function isStatusView<V>(v: V) {\n  return is$typed(v, id, hashStatusView)\n}\n\nexport function validateStatusView<V>(v: V) {\n  return validate<StatusView & V>(v, id, hashStatusView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/getProfile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfile'\n\nexport type QueryParams = {\n  /** Handle or DID of account to fetch profile of. */\n  actor: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyActorDefs.ProfileViewDetailed\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/getProfiles.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfiles'\n\nexport type QueryParams = {\n  actors: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  profiles: AppBskyActorDefs.ProfileViewDetailed[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/getSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getSuggestions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/profile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.profile'\n\nexport interface Main {\n  $type: 'app.bsky.actor.profile'\n  displayName?: string\n  /** Free-form profile description text. */\n  description?: string\n  /** Free-form pronouns text. */\n  pronouns?: string\n  website?: string\n  /** Small image to be displayed next to posts from account. AKA, 'profile picture' */\n  avatar?: BlobRef\n  /** Larger horizontal image to display behind profile view. */\n  banner?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/searchActors.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActors'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActorsTypeahead'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query prefix; not a full query string. */\n  q?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/actor/status.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.status'\n\nexport interface Main {\n  $type: 'app.bsky.actor.status'\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  embed?: $Typed<AppBskyEmbedExternal.Main> | { $type: string }\n  /** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. */\n  durationMinutes?: number\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Advertises an account as currently offering live content. */\nexport const LIVE = `${id}#live`\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/ageassurance/begin.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.begin'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive Age Assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the Age Assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n  /** An optional ISO 3166-2 code of the user's region or state within the country. */\n  regionCode?: string\n}\n\nexport type OutputSchema = AppBskyAgeassuranceDefs.State\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidEmail'\n    | 'DidTooLong'\n    | 'InvalidInitiation'\n    | 'RegionNotSupported'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/ageassurance/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.defs'\n\n/** The access level granted based on Age Assurance data we've processed. */\nexport type Access = 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n/** The status of the Age Assurance process. */\nexport type Status =\n  | 'unknown'\n  | 'pending'\n  | 'assured'\n  | 'blocked'\n  | (string & {})\n\n/** The user's computed Age Assurance state. */\nexport interface State {\n  $type?: 'app.bsky.ageassurance.defs#state'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  status: Status\n  access: Access\n}\n\nconst hashState = 'state'\n\nexport function isState<V>(v: V) {\n  return is$typed(v, id, hashState)\n}\n\nexport function validateState<V>(v: V) {\n  return validate<State & V>(v, id, hashState)\n}\n\n/** Additional metadata needed to compute Age Assurance state client-side. */\nexport interface StateMetadata {\n  $type?: 'app.bsky.ageassurance.defs#stateMetadata'\n  /** The account creation timestamp. */\n  accountCreatedAt?: string\n}\n\nconst hashStateMetadata = 'stateMetadata'\n\nexport function isStateMetadata<V>(v: V) {\n  return is$typed(v, id, hashStateMetadata)\n}\n\nexport function validateStateMetadata<V>(v: V) {\n  return validate<StateMetadata & V>(v, id, hashStateMetadata)\n}\n\nexport interface Config {\n  $type?: 'app.bsky.ageassurance.defs#config'\n  /** The per-region Age Assurance configuration. */\n  regions: ConfigRegion[]\n}\n\nconst hashConfig = 'config'\n\nexport function isConfig<V>(v: V) {\n  return is$typed(v, id, hashConfig)\n}\n\nexport function validateConfig<V>(v: V) {\n  return validate<Config & V>(v, id, hashConfig)\n}\n\n/** The Age Assurance configuration for a specific region. */\nexport interface ConfigRegion {\n  $type?: 'app.bsky.ageassurance.defs#configRegion'\n  /** The ISO 3166-1 alpha-2 country code this configuration applies to. */\n  countryCode: string\n  /** The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. */\n  regionCode?: string\n  /** The minimum age (as a whole integer) required to use Bluesky in this region. */\n  minAccessAge: number\n  /** 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. */\n  rules: (\n    | $Typed<ConfigRegionRuleDefault>\n    | $Typed<ConfigRegionRuleIfDeclaredOverAge>\n    | $Typed<ConfigRegionRuleIfDeclaredUnderAge>\n    | $Typed<ConfigRegionRuleIfAssuredOverAge>\n    | $Typed<ConfigRegionRuleIfAssuredUnderAge>\n    | $Typed<ConfigRegionRuleIfAccountNewerThan>\n    | $Typed<ConfigRegionRuleIfAccountOlderThan>\n    | { $type: string }\n  )[]\n}\n\nconst hashConfigRegion = 'configRegion'\n\nexport function isConfigRegion<V>(v: V) {\n  return is$typed(v, id, hashConfigRegion)\n}\n\nexport function validateConfigRegion<V>(v: V) {\n  return validate<ConfigRegion & V>(v, id, hashConfigRegion)\n}\n\n/** Age Assurance rule that applies by default. */\nexport interface ConfigRegionRuleDefault {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleDefault'\n  access: Access\n}\n\nconst hashConfigRegionRuleDefault = 'configRegionRuleDefault'\n\nexport function isConfigRegionRuleDefault<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleDefault)\n}\n\nexport function validateConfigRegionRuleDefault<V>(v: V) {\n  return validate<ConfigRegionRuleDefault & V>(\n    v,\n    id,\n    hashConfigRegionRuleDefault,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfDeclaredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredOverAge =\n  'configRegionRuleIfDeclaredOverAge'\n\nexport function isConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredOverAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves under a certain age. */\nexport interface ConfigRegionRuleIfDeclaredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredUnderAge =\n  'configRegionRuleIfDeclaredUnderAge'\n\nexport function isConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfAssuredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredOverAge = 'configRegionRuleIfAssuredOverAge'\n\nexport function isConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredOverAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be under a certain age. */\nexport interface ConfigRegionRuleIfAssuredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredUnderAge =\n  'configRegionRuleIfAssuredUnderAge'\n\nexport function isConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the account is equal-to or newer than a certain date. */\nexport interface ConfigRegionRuleIfAccountNewerThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountNewerThan =\n  'configRegionRuleIfAccountNewerThan'\n\nexport function isConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountNewerThan)\n}\n\nexport function validateConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountNewerThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountNewerThan,\n  )\n}\n\n/** Age Assurance rule that applies if the account is older than a certain date. */\nexport interface ConfigRegionRuleIfAccountOlderThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountOlderThan =\n  'configRegionRuleIfAccountOlderThan'\n\nexport function isConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountOlderThan)\n}\n\nexport function validateConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountOlderThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountOlderThan,\n  )\n}\n\n/** Object used to store Age Assurance data in stash. */\nexport interface Event {\n  $type?: 'app.bsky.ageassurance.defs#event'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the Age Assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n  /** The access level granted based on Age Assurance data we've processed. */\n  access: 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The email used for Age Assurance. */\n  email?: string\n  /** The IP address used when initiating the Age Assurance flow. */\n  initIp?: string\n  /** The user agent used when initiating the Age Assurance flow. */\n  initUa?: string\n  /** The IP address used when completing the Age Assurance flow. */\n  completeIp?: string\n  /** The user agent used when completing the Age Assurance flow. */\n  completeUa?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/ageassurance/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyAgeassuranceDefs.Config\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/ageassurance/getState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getState'\n\nexport type QueryParams = {\n  countryCode: string\n  regionCode?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  state: AppBskyAgeassuranceDefs.State\n  metadata: AppBskyAgeassuranceDefs.StateMetadata\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/bookmark/createBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.createBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n  cid: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnsupportedCollection'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/bookmark/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.defs'\n\n/** Object used to store bookmark data in stash. */\nexport interface Bookmark {\n  $type?: 'app.bsky.bookmark.defs#bookmark'\n  subject: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashBookmark = 'bookmark'\n\nexport function isBookmark<V>(v: V) {\n  return is$typed(v, id, hashBookmark)\n}\n\nexport function validateBookmark<V>(v: V) {\n  return validate<Bookmark & V>(v, id, hashBookmark)\n}\n\nexport interface BookmarkView {\n  $type?: 'app.bsky.bookmark.defs#bookmarkView'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  item:\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.PostView>\n    | { $type: string }\n}\n\nconst hashBookmarkView = 'bookmarkView'\n\nexport function isBookmarkView<V>(v: V) {\n  return is$typed(v, id, hashBookmarkView)\n}\n\nexport function validateBookmarkView<V>(v: V) {\n  return validate<BookmarkView & V>(v, id, hashBookmarkView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/bookmark/deleteBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.deleteBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnsupportedCollection'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/bookmark/getBookmarks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyBookmarkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.getBookmarks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  bookmarks: AppBskyBookmarkDefs.BookmarkView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.defs'\n\n/** 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. */\nexport interface MatchAndContactIndex {\n  $type?: 'app.bsky.contact.defs#matchAndContactIndex'\n  match: AppBskyActorDefs.ProfileView\n  /** The index of this match in the import contact input. */\n  contactIndex: number\n}\n\nconst hashMatchAndContactIndex = 'matchAndContactIndex'\n\nexport function isMatchAndContactIndex<V>(v: V) {\n  return is$typed(v, id, hashMatchAndContactIndex)\n}\n\nexport function validateMatchAndContactIndex<V>(v: V) {\n  return validate<MatchAndContactIndex & V>(v, id, hashMatchAndContactIndex)\n}\n\nexport interface SyncStatus {\n  $type?: 'app.bsky.contact.defs#syncStatus'\n  /** Last date when contacts where imported. */\n  syncedAt: string\n  /** 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. */\n  matchesCount: number\n}\n\nconst hashSyncStatus = 'syncStatus'\n\nexport function isSyncStatus<V>(v: V) {\n  return is$typed(v, id, hashSyncStatus)\n}\n\nexport function validateSyncStatus<V>(v: V) {\n  return validate<SyncStatus & V>(v, id, hashSyncStatus)\n}\n\n/** A stash object to be sent via bsync representing a notification to be created. */\nexport interface Notification {\n  $type?: 'app.bsky.contact.defs#notification'\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/dismissMatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.dismissMatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The subject's DID to dismiss the match with. */\n  subject: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/getMatches.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getMatches'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  matches: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InvalidLimit' | 'InvalidCursor' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/getSyncStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getSyncStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  syncStatus?: AppBskyContactDefs.SyncStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/importContacts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.importContacts'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`. */\n  token: string\n  /** 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`. */\n  contacts: string[]\n}\n\nexport interface OutputSchema {\n  /** The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list. */\n  matchesAndContactIndexes: AppBskyContactDefs.MatchAndContactIndex[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidDid'\n    | 'InvalidContacts'\n    | 'TooManyContacts'\n    | 'InvalidToken'\n    | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/removeData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.removeData'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/sendNotification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.sendNotification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/startPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.startPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to receive the code via SMS. */\n  phone: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RateLimitExceeded' | 'InvalidDid' | 'InvalidPhone' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/contact/verifyPhone.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.verifyPhone'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`. */\n  phone: string\n  /** The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`. */\n  code: string\n}\n\nexport interface OutputSchema {\n  /** JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call. */\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'RateLimitExceeded'\n    | 'InvalidDid'\n    | 'InvalidPhone'\n    | 'InvalidCode'\n    | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/draft/createDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.createDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.Draft\n}\n\nexport interface OutputSchema {\n  /** The ID of the created draft. */\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DraftLimitReached'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/draft/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.defs'\n\n/** A draft with an identifier, used to store drafts in private storage (stash). */\nexport interface DraftWithId {\n  $type?: 'app.bsky.draft.defs#draftWithId'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n}\n\nconst hashDraftWithId = 'draftWithId'\n\nexport function isDraftWithId<V>(v: V) {\n  return is$typed(v, id, hashDraftWithId)\n}\n\nexport function validateDraftWithId<V>(v: V) {\n  return validate<DraftWithId & V>(v, id, hashDraftWithId)\n}\n\n/** A draft containing an array of draft posts. */\nexport interface Draft {\n  $type?: 'app.bsky.draft.defs#draft'\n  /** UUIDv4 identifier of the device that created this draft. */\n  deviceId?: string\n  /** The device and/or platform on which the draft was created. */\n  deviceName?: string\n  /** Array of draft posts that compose this draft. */\n  posts: DraftPost[]\n  /** Indicates human language of posts primary text content. */\n  langs?: string[]\n  /** Embedding rules for the postgates to be created when this draft is published. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n  /** Allow-rules for the threadgate to be created when this draft is published. */\n  threadgateAllow?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashDraft = 'draft'\n\nexport function isDraft<V>(v: V) {\n  return is$typed(v, id, hashDraft)\n}\n\nexport function validateDraft<V>(v: V) {\n  return validate<Draft & V>(v, id, hashDraft)\n}\n\n/** One of the posts that compose a draft. */\nexport interface DraftPost {\n  $type?: 'app.bsky.draft.defs#draftPost'\n  /** 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. */\n  text: string\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  embedImages?: DraftEmbedImage[]\n  embedVideos?: DraftEmbedVideo[]\n  embedExternals?: DraftEmbedExternal[]\n  embedRecords?: DraftEmbedRecord[]\n}\n\nconst hashDraftPost = 'draftPost'\n\nexport function isDraftPost<V>(v: V) {\n  return is$typed(v, id, hashDraftPost)\n}\n\nexport function validateDraftPost<V>(v: V) {\n  return validate<DraftPost & V>(v, id, hashDraftPost)\n}\n\n/** View to present drafts data to users. */\nexport interface DraftView {\n  $type?: 'app.bsky.draft.defs#draftView'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n  /** The time the draft was created. */\n  createdAt: string\n  /** The time the draft was last updated. */\n  updatedAt: string\n}\n\nconst hashDraftView = 'draftView'\n\nexport function isDraftView<V>(v: V) {\n  return is$typed(v, id, hashDraftView)\n}\n\nexport function validateDraftView<V>(v: V) {\n  return validate<DraftView & V>(v, id, hashDraftView)\n}\n\nexport interface DraftEmbedLocalRef {\n  $type?: 'app.bsky.draft.defs#draftEmbedLocalRef'\n  /** Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts. */\n  path: string\n}\n\nconst hashDraftEmbedLocalRef = 'draftEmbedLocalRef'\n\nexport function isDraftEmbedLocalRef<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedLocalRef)\n}\n\nexport function validateDraftEmbedLocalRef<V>(v: V) {\n  return validate<DraftEmbedLocalRef & V>(v, id, hashDraftEmbedLocalRef)\n}\n\nexport interface DraftEmbedCaption {\n  $type?: 'app.bsky.draft.defs#draftEmbedCaption'\n  lang: string\n  content: string\n}\n\nconst hashDraftEmbedCaption = 'draftEmbedCaption'\n\nexport function isDraftEmbedCaption<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedCaption)\n}\n\nexport function validateDraftEmbedCaption<V>(v: V) {\n  return validate<DraftEmbedCaption & V>(v, id, hashDraftEmbedCaption)\n}\n\nexport interface DraftEmbedImage {\n  $type?: 'app.bsky.draft.defs#draftEmbedImage'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n}\n\nconst hashDraftEmbedImage = 'draftEmbedImage'\n\nexport function isDraftEmbedImage<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedImage)\n}\n\nexport function validateDraftEmbedImage<V>(v: V) {\n  return validate<DraftEmbedImage & V>(v, id, hashDraftEmbedImage)\n}\n\nexport interface DraftEmbedVideo {\n  $type?: 'app.bsky.draft.defs#draftEmbedVideo'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n  captions?: DraftEmbedCaption[]\n}\n\nconst hashDraftEmbedVideo = 'draftEmbedVideo'\n\nexport function isDraftEmbedVideo<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedVideo)\n}\n\nexport function validateDraftEmbedVideo<V>(v: V) {\n  return validate<DraftEmbedVideo & V>(v, id, hashDraftEmbedVideo)\n}\n\nexport interface DraftEmbedExternal {\n  $type?: 'app.bsky.draft.defs#draftEmbedExternal'\n  uri: string\n}\n\nconst hashDraftEmbedExternal = 'draftEmbedExternal'\n\nexport function isDraftEmbedExternal<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedExternal)\n}\n\nexport function validateDraftEmbedExternal<V>(v: V) {\n  return validate<DraftEmbedExternal & V>(v, id, hashDraftEmbedExternal)\n}\n\nexport interface DraftEmbedRecord {\n  $type?: 'app.bsky.draft.defs#draftEmbedRecord'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashDraftEmbedRecord = 'draftEmbedRecord'\n\nexport function isDraftEmbedRecord<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedRecord)\n}\n\nexport function validateDraftEmbedRecord<V>(v: V) {\n  return validate<DraftEmbedRecord & V>(v, id, hashDraftEmbedRecord)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/draft/deleteDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.deleteDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/draft/getDrafts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.getDrafts'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  drafts: AppBskyDraftDefs.DraftView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/draft/updateDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.updateDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.DraftWithId\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/embed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.defs'\n\n/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */\nexport interface AspectRatio {\n  $type?: 'app.bsky.embed.defs#aspectRatio'\n  width: number\n  height: number\n}\n\nconst hashAspectRatio = 'aspectRatio'\n\nexport function isAspectRatio<V>(v: V) {\n  return is$typed(v, id, hashAspectRatio)\n}\n\nexport function validateAspectRatio<V>(v: V) {\n  return validate<AspectRatio & V>(v, id, hashAspectRatio)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/embed/external.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.external'\n\n/** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). */\nexport interface Main {\n  $type?: 'app.bsky.embed.external'\n  external: External\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface External {\n  $type?: 'app.bsky.embed.external#external'\n  uri: string\n  title: string\n  description: string\n  thumb?: BlobRef\n}\n\nconst hashExternal = 'external'\n\nexport function isExternal<V>(v: V) {\n  return is$typed(v, id, hashExternal)\n}\n\nexport function validateExternal<V>(v: V) {\n  return validate<External & V>(v, id, hashExternal)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.external#view'\n  external: ViewExternal\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewExternal {\n  $type?: 'app.bsky.embed.external#viewExternal'\n  uri: string\n  title: string\n  description: string\n  thumb?: string\n}\n\nconst hashViewExternal = 'viewExternal'\n\nexport function isViewExternal<V>(v: V) {\n  return is$typed(v, id, hashViewExternal)\n}\n\nexport function validateViewExternal<V>(v: V) {\n  return validate<ViewExternal & V>(v, id, hashViewExternal)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/embed/images.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.images'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.images'\n  images: Image[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Image {\n  $type?: 'app.bsky.embed.images#image'\n  image: BlobRef\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashImage = 'image'\n\nexport function isImage<V>(v: V) {\n  return is$typed(v, id, hashImage)\n}\n\nexport function validateImage<V>(v: V) {\n  return validate<Image & V>(v, id, hashImage)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.images#view'\n  images: ViewImage[]\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewImage {\n  $type?: 'app.bsky.embed.images#viewImage'\n  /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */\n  thumb: string\n  /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */\n  fullsize: string\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashViewImage = 'viewImage'\n\nexport function isViewImage<V>(v: V) {\n  return is$typed(v, id, hashViewImage)\n}\n\nexport function validateViewImage<V>(v: V) {\n  return validate<ViewImage & V>(v, id, hashViewImage)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/embed/record.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as AppBskyLabelerDefs from '../labeler/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\nimport type * as AppBskyEmbedRecordWithMedia from './recordWithMedia.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.record'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.record'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.record#view'\n  record:\n    | $Typed<ViewRecord>\n    | $Typed<ViewNotFound>\n    | $Typed<ViewBlocked>\n    | $Typed<ViewDetached>\n    | $Typed<AppBskyFeedDefs.GeneratorView>\n    | $Typed<AppBskyGraphDefs.ListView>\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyGraphDefs.StarterPackViewBasic>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewRecord {\n  $type?: 'app.bsky.embed.record#viewRecord'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  /** The record data itself. */\n  value: { [_ in string]: unknown }\n  labels?: ComAtprotoLabelDefs.Label[]\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  embeds?: (\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  )[]\n  indexedAt: string\n}\n\nconst hashViewRecord = 'viewRecord'\n\nexport function isViewRecord<V>(v: V) {\n  return is$typed(v, id, hashViewRecord)\n}\n\nexport function validateViewRecord<V>(v: V) {\n  return validate<ViewRecord & V>(v, id, hashViewRecord)\n}\n\nexport interface ViewNotFound {\n  $type?: 'app.bsky.embed.record#viewNotFound'\n  uri: string\n  notFound: true\n}\n\nconst hashViewNotFound = 'viewNotFound'\n\nexport function isViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashViewNotFound)\n}\n\nexport function validateViewNotFound<V>(v: V) {\n  return validate<ViewNotFound & V>(v, id, hashViewNotFound)\n}\n\nexport interface ViewBlocked {\n  $type?: 'app.bsky.embed.record#viewBlocked'\n  uri: string\n  blocked: true\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashViewBlocked = 'viewBlocked'\n\nexport function isViewBlocked<V>(v: V) {\n  return is$typed(v, id, hashViewBlocked)\n}\n\nexport function validateViewBlocked<V>(v: V) {\n  return validate<ViewBlocked & V>(v, id, hashViewBlocked)\n}\n\nexport interface ViewDetached {\n  $type?: 'app.bsky.embed.record#viewDetached'\n  uri: string\n  detached: true\n}\n\nconst hashViewDetached = 'viewDetached'\n\nexport function isViewDetached<V>(v: V) {\n  return is$typed(v, id, hashViewDetached)\n}\n\nexport function validateViewDetached<V>(v: V) {\n  return validate<ViewDetached & V>(v, id, hashViewDetached)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/embed/recordWithMedia.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedRecord from './record.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.recordWithMedia'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.recordWithMedia'\n  record: AppBskyEmbedRecord.Main\n  media:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | { $type: string }\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.recordWithMedia#view'\n  record: AppBskyEmbedRecord.View\n  media:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/embed/video.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.video'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.video'\n  /** The mp4 video file. May be up to 100mb, formerly limited to 50mb. */\n  video: BlobRef\n  captions?: Caption[]\n  /** Alt text description of the video, for accessibility. */\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Caption {\n  $type?: 'app.bsky.embed.video#caption'\n  lang: string\n  file: BlobRef\n}\n\nconst hashCaption = 'caption'\n\nexport function isCaption<V>(v: V) {\n  return is$typed(v, id, hashCaption)\n}\n\nexport function validateCaption<V>(v: V) {\n  return validate<Caption & V>(v, id, hashCaption)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.video#view'\n  cid: string\n  playlist: string\n  thumbnail?: string\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.defs'\n\nexport interface PostView {\n  $type?: 'app.bsky.feed.defs#postView'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  record: { [_ in string]: unknown }\n  embed?:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<AppBskyEmbedRecord.View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  bookmarkCount?: number\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  indexedAt: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  threadgate?: ThreadgateView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashPostView = 'postView'\n\nexport function isPostView<V>(v: V) {\n  return is$typed(v, id, hashPostView)\n}\n\nexport function validatePostView<V>(v: V) {\n  return validate<PostView & V>(v, id, hashPostView)\n}\n\n/** Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.feed.defs#viewerState'\n  repost?: string\n  like?: string\n  bookmarked?: boolean\n  threadMuted?: boolean\n  replyDisabled?: boolean\n  embeddingDisabled?: boolean\n  pinned?: boolean\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** Metadata about this post within the context of the thread it is in. */\nexport interface ThreadContext {\n  $type?: 'app.bsky.feed.defs#threadContext'\n  rootAuthorLike?: string\n}\n\nconst hashThreadContext = 'threadContext'\n\nexport function isThreadContext<V>(v: V) {\n  return is$typed(v, id, hashThreadContext)\n}\n\nexport function validateThreadContext<V>(v: V) {\n  return validate<ThreadContext & V>(v, id, hashThreadContext)\n}\n\nexport interface FeedViewPost {\n  $type?: 'app.bsky.feed.defs#feedViewPost'\n  post: PostView\n  reply?: ReplyRef\n  reason?: $Typed<ReasonRepost> | $Typed<ReasonPin> | { $type: string }\n  /** Context provided by feed generator that may be passed back alongside interactions. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashFeedViewPost = 'feedViewPost'\n\nexport function isFeedViewPost<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPost)\n}\n\nexport function validateFeedViewPost<V>(v: V) {\n  return validate<FeedViewPost & V>(v, id, hashFeedViewPost)\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.defs#replyRef'\n  root:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  parent:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  grandparentAuthor?: AppBskyActorDefs.ProfileViewBasic\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\nexport interface ReasonRepost {\n  $type?: 'app.bsky.feed.defs#reasonRepost'\n  by: AppBskyActorDefs.ProfileViewBasic\n  uri?: string\n  cid?: string\n  indexedAt: string\n}\n\nconst hashReasonRepost = 'reasonRepost'\n\nexport function isReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashReasonRepost)\n}\n\nexport function validateReasonRepost<V>(v: V) {\n  return validate<ReasonRepost & V>(v, id, hashReasonRepost)\n}\n\nexport interface ReasonPin {\n  $type?: 'app.bsky.feed.defs#reasonPin'\n}\n\nconst hashReasonPin = 'reasonPin'\n\nexport function isReasonPin<V>(v: V) {\n  return is$typed(v, id, hashReasonPin)\n}\n\nexport function validateReasonPin<V>(v: V) {\n  return validate<ReasonPin & V>(v, id, hashReasonPin)\n}\n\nexport interface ThreadViewPost {\n  $type?: 'app.bsky.feed.defs#threadViewPost'\n  post: PostView\n  parent?:\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  replies?: (\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  )[]\n  threadContext?: ThreadContext\n}\n\nconst hashThreadViewPost = 'threadViewPost'\n\nexport function isThreadViewPost<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPost)\n}\n\nexport function validateThreadViewPost<V>(v: V) {\n  return validate<ThreadViewPost & V>(v, id, hashThreadViewPost)\n}\n\nexport interface NotFoundPost {\n  $type?: 'app.bsky.feed.defs#notFoundPost'\n  uri: string\n  notFound: true\n}\n\nconst hashNotFoundPost = 'notFoundPost'\n\nexport function isNotFoundPost<V>(v: V) {\n  return is$typed(v, id, hashNotFoundPost)\n}\n\nexport function validateNotFoundPost<V>(v: V) {\n  return validate<NotFoundPost & V>(v, id, hashNotFoundPost)\n}\n\nexport interface BlockedPost {\n  $type?: 'app.bsky.feed.defs#blockedPost'\n  uri: string\n  blocked: true\n  author: BlockedAuthor\n}\n\nconst hashBlockedPost = 'blockedPost'\n\nexport function isBlockedPost<V>(v: V) {\n  return is$typed(v, id, hashBlockedPost)\n}\n\nexport function validateBlockedPost<V>(v: V) {\n  return validate<BlockedPost & V>(v, id, hashBlockedPost)\n}\n\nexport interface BlockedAuthor {\n  $type?: 'app.bsky.feed.defs#blockedAuthor'\n  did: string\n  viewer?: AppBskyActorDefs.ViewerState\n}\n\nconst hashBlockedAuthor = 'blockedAuthor'\n\nexport function isBlockedAuthor<V>(v: V) {\n  return is$typed(v, id, hashBlockedAuthor)\n}\n\nexport function validateBlockedAuthor<V>(v: V) {\n  return validate<BlockedAuthor & V>(v, id, hashBlockedAuthor)\n}\n\nexport interface GeneratorView {\n  $type?: 'app.bsky.feed.defs#generatorView'\n  uri: string\n  cid: string\n  did: string\n  creator: AppBskyActorDefs.ProfileView\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  likeCount?: number\n  acceptsInteractions?: boolean\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: GeneratorViewerState\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  indexedAt: string\n}\n\nconst hashGeneratorView = 'generatorView'\n\nexport function isGeneratorView<V>(v: V) {\n  return is$typed(v, id, hashGeneratorView)\n}\n\nexport function validateGeneratorView<V>(v: V) {\n  return validate<GeneratorView & V>(v, id, hashGeneratorView)\n}\n\nexport interface GeneratorViewerState {\n  $type?: 'app.bsky.feed.defs#generatorViewerState'\n  like?: string\n}\n\nconst hashGeneratorViewerState = 'generatorViewerState'\n\nexport function isGeneratorViewerState<V>(v: V) {\n  return is$typed(v, id, hashGeneratorViewerState)\n}\n\nexport function validateGeneratorViewerState<V>(v: V) {\n  return validate<GeneratorViewerState & V>(v, id, hashGeneratorViewerState)\n}\n\nexport interface SkeletonFeedPost {\n  $type?: 'app.bsky.feed.defs#skeletonFeedPost'\n  post: string\n  reason?:\n    | $Typed<SkeletonReasonRepost>\n    | $Typed<SkeletonReasonPin>\n    | { $type: string }\n  /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */\n  feedContext?: string\n}\n\nconst hashSkeletonFeedPost = 'skeletonFeedPost'\n\nexport function isSkeletonFeedPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonFeedPost)\n}\n\nexport function validateSkeletonFeedPost<V>(v: V) {\n  return validate<SkeletonFeedPost & V>(v, id, hashSkeletonFeedPost)\n}\n\nexport interface SkeletonReasonRepost {\n  $type?: 'app.bsky.feed.defs#skeletonReasonRepost'\n  repost: string\n}\n\nconst hashSkeletonReasonRepost = 'skeletonReasonRepost'\n\nexport function isSkeletonReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonRepost)\n}\n\nexport function validateSkeletonReasonRepost<V>(v: V) {\n  return validate<SkeletonReasonRepost & V>(v, id, hashSkeletonReasonRepost)\n}\n\nexport interface SkeletonReasonPin {\n  $type?: 'app.bsky.feed.defs#skeletonReasonPin'\n}\n\nconst hashSkeletonReasonPin = 'skeletonReasonPin'\n\nexport function isSkeletonReasonPin<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonPin)\n}\n\nexport function validateSkeletonReasonPin<V>(v: V) {\n  return validate<SkeletonReasonPin & V>(v, id, hashSkeletonReasonPin)\n}\n\nexport interface ThreadgateView {\n  $type?: 'app.bsky.feed.defs#threadgateView'\n  uri?: string\n  cid?: string\n  record?: { [_ in string]: unknown }\n  lists?: AppBskyGraphDefs.ListViewBasic[]\n}\n\nconst hashThreadgateView = 'threadgateView'\n\nexport function isThreadgateView<V>(v: V) {\n  return is$typed(v, id, hashThreadgateView)\n}\n\nexport function validateThreadgateView<V>(v: V) {\n  return validate<ThreadgateView & V>(v, id, hashThreadgateView)\n}\n\nexport interface Interaction {\n  $type?: 'app.bsky.feed.defs#interaction'\n  item?: string\n  event?:\n    | 'app.bsky.feed.defs#requestLess'\n    | 'app.bsky.feed.defs#requestMore'\n    | 'app.bsky.feed.defs#clickthroughItem'\n    | 'app.bsky.feed.defs#clickthroughAuthor'\n    | 'app.bsky.feed.defs#clickthroughReposter'\n    | 'app.bsky.feed.defs#clickthroughEmbed'\n    | 'app.bsky.feed.defs#interactionSeen'\n    | 'app.bsky.feed.defs#interactionLike'\n    | 'app.bsky.feed.defs#interactionRepost'\n    | 'app.bsky.feed.defs#interactionReply'\n    | 'app.bsky.feed.defs#interactionQuote'\n    | 'app.bsky.feed.defs#interactionShare'\n    | (string & {})\n  /** Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashInteraction = 'interaction'\n\nexport function isInteraction<V>(v: V) {\n  return is$typed(v, id, hashInteraction)\n}\n\nexport function validateInteraction<V>(v: V) {\n  return validate<Interaction & V>(v, id, hashInteraction)\n}\n\n/** Request that less content like the given feed item be shown in the feed */\nexport const REQUESTLESS = `${id}#requestLess`\n/** Request that more content like the given feed item be shown in the feed */\nexport const REQUESTMORE = `${id}#requestMore`\n/** User clicked through to the feed item */\nexport const CLICKTHROUGHITEM = `${id}#clickthroughItem`\n/** User clicked through to the author of the feed item */\nexport const CLICKTHROUGHAUTHOR = `${id}#clickthroughAuthor`\n/** User clicked through to the reposter of the feed item */\nexport const CLICKTHROUGHREPOSTER = `${id}#clickthroughReposter`\n/** User clicked through to the embedded content of the feed item */\nexport const CLICKTHROUGHEMBED = `${id}#clickthroughEmbed`\n/** Declares the feed generator returns any types of posts. */\nexport const CONTENTMODEUNSPECIFIED = `${id}#contentModeUnspecified`\n/** Declares the feed generator returns posts containing app.bsky.embed.video embeds. */\nexport const CONTENTMODEVIDEO = `${id}#contentModeVideo`\n/** Feed item was seen by user */\nexport const INTERACTIONSEEN = `${id}#interactionSeen`\n/** User liked the feed item */\nexport const INTERACTIONLIKE = `${id}#interactionLike`\n/** User reposted the feed item */\nexport const INTERACTIONREPOST = `${id}#interactionRepost`\n/** User replied to the feed item */\nexport const INTERACTIONREPLY = `${id}#interactionReply`\n/** User quoted the feed item */\nexport const INTERACTIONQUOTE = `${id}#interactionQuote`\n/** User shared the feed item */\nexport const INTERACTIONSHARE = `${id}#interactionShare`\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.describeFeedGenerator'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  feeds: Feed[]\n  links?: Links\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Feed {\n  $type?: 'app.bsky.feed.describeFeedGenerator#feed'\n  uri: string\n}\n\nconst hashFeed = 'feed'\n\nexport function isFeed<V>(v: V) {\n  return is$typed(v, id, hashFeed)\n}\n\nexport function validateFeed<V>(v: V) {\n  return validate<Feed & V>(v, id, hashFeed)\n}\n\nexport interface Links {\n  $type?: 'app.bsky.feed.describeFeedGenerator#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/generator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.generator'\n\nexport interface Main {\n  $type: 'app.bsky.feed.generator'\n  did: string\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  /** Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions */\n  acceptsInteractions?: boolean\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getActorFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorFeeds'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getActorLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorLikes'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BlockedActor' | 'BlockedByActor'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getAuthorFeed'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n  /** Combinations of post/repost types to include in response. */\n  filter:\n    | 'posts_with_replies'\n    | 'posts_no_replies'\n    | 'posts_with_media'\n    | 'posts_and_author_threads'\n    | 'posts_with_video'\n    | (string & {})\n  includePins: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BlockedActor' | 'BlockedByActor'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeed'\n\nexport type QueryParams = {\n  feed: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownFeed'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerator'\n\nexport type QueryParams = {\n  /** AT-URI of the feed generator record. */\n  feed: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  view: AppBskyFeedDefs.GeneratorView\n  /** Indicates whether the feed generator service has been online recently, or else seems to be inactive. */\n  isOnline: boolean\n  /** Indicates whether the feed generator service is compatible with the record declaration. */\n  isValid: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerators'\n\nexport type QueryParams = {\n  feeds: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedSkeleton'\n\nexport type QueryParams = {\n  /** Reference to feed generator record describing the specific feed being requested. */\n  feed: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.SkeletonFeedPost[]\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownFeed'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getLikes'\n\nexport type QueryParams = {\n  /** AT-URI of the subject (eg, a post record). */\n  uri: string\n  /** CID of the subject record (aka, specific version of record), to filter likes. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  likes: Like[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Like {\n  $type?: 'app.bsky.feed.getLikes#like'\n  indexedAt: string\n  createdAt: string\n  actor: AppBskyActorDefs.ProfileView\n}\n\nconst hashLike = 'like'\n\nexport function isLike<V>(v: V) {\n  return is$typed(v, id, hashLike)\n}\n\nexport function validateLike<V>(v: V) {\n  return validate<Like & V>(v, id, hashLike)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getListFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getListFeed'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownList'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getPostThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPostThread'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. */\n  uri: string\n  /** How many levels of reply depth should be included in response. */\n  depth: number\n  /** How many levels of parent (and grandparent, etc) post to include. */\n  parentHeight: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  thread:\n    | $Typed<AppBskyFeedDefs.ThreadViewPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | { $type: string }\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'NotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPosts'\n\nexport type QueryParams = {\n  /** List of post AT-URIs to return hydrated views for. */\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getQuotes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getQuotes'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to quotes of specific version (by CID) of the post record. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getRepostedBy.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getRepostedBy'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to reposts of specific version (by CID) of the post record. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  repostedBy: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/getTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getTimeline'\n\nexport type QueryParams = {\n  /** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. */\n  algorithm?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/like.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.like'\n\nexport interface Main {\n  $type: 'app.bsky.feed.like'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/post.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.post'\n\nexport interface Main {\n  $type: 'app.bsky.feed.post'\n  /** The primary post content. May be an empty string, if there are embeds. */\n  text: string\n  /** DEPRECATED: replaced by app.bsky.richtext.facet. */\n  entities?: Entity[]\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  reply?: ReplyRef\n  embed?:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | $Typed<AppBskyEmbedRecord.Main>\n    | $Typed<AppBskyEmbedRecordWithMedia.Main>\n    | { $type: string }\n  /** Indicates human language of post primary text content. */\n  langs?: string[]\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  /** Additional hashtags, in addition to any included in post text and facets. */\n  tags?: string[]\n  /** Client-declared timestamp when this post was originally created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.post#replyRef'\n  root: ComAtprotoRepoStrongRef.Main\n  parent: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\n/** Deprecated: use facets instead. */\nexport interface Entity {\n  $type?: 'app.bsky.feed.post#entity'\n  index: TextSlice\n  /** Expected values are 'mention' and 'link'. */\n  type: string\n  value: string\n}\n\nconst hashEntity = 'entity'\n\nexport function isEntity<V>(v: V) {\n  return is$typed(v, id, hashEntity)\n}\n\nexport function validateEntity<V>(v: V) {\n  return validate<Entity & V>(v, id, hashEntity)\n}\n\n/** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. */\nexport interface TextSlice {\n  $type?: 'app.bsky.feed.post#textSlice'\n  start: number\n  end: number\n}\n\nconst hashTextSlice = 'textSlice'\n\nexport function isTextSlice<V>(v: V) {\n  return is$typed(v, id, hashTextSlice)\n}\n\nexport function validateTextSlice<V>(v: V) {\n  return validate<TextSlice & V>(v, id, hashTextSlice)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/postgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.postgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.postgate'\n  createdAt: string\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of AT-URIs embedding this post that the author has detached from. */\n  detachedEmbeddingUris?: string[]\n  /** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  embeddingRules?: ($Typed<DisableRule> | { $type: string })[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Disables embedding of this post. */\nexport interface DisableRule {\n  $type?: 'app.bsky.feed.postgate#disableRule'\n}\n\nconst hashDisableRule = 'disableRule'\n\nexport function isDisableRule<V>(v: V) {\n  return is$typed(v, id, hashDisableRule)\n}\n\nexport function validateDisableRule<V>(v: V) {\n  return validate<DisableRule & V>(v, id, hashDisableRule)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/repost.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.repost'\n\nexport interface Main {\n  $type: 'app.bsky.feed.repost'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/searchPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.searchPosts'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/sendInteractions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.sendInteractions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  feed?: string\n  interactions: AppBskyFeedDefs.Interaction[]\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/feed/threadgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.threadgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.threadgate'\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  allow?: (\n    | $Typed<MentionRule>\n    | $Typed<FollowerRule>\n    | $Typed<FollowingRule>\n    | $Typed<ListRule>\n    | { $type: string }\n  )[]\n  createdAt: string\n  /** List of hidden reply URIs. */\n  hiddenReplies?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Allow replies from actors mentioned in your post. */\nexport interface MentionRule {\n  $type?: 'app.bsky.feed.threadgate#mentionRule'\n}\n\nconst hashMentionRule = 'mentionRule'\n\nexport function isMentionRule<V>(v: V) {\n  return is$typed(v, id, hashMentionRule)\n}\n\nexport function validateMentionRule<V>(v: V) {\n  return validate<MentionRule & V>(v, id, hashMentionRule)\n}\n\n/** Allow replies from actors who follow you. */\nexport interface FollowerRule {\n  $type?: 'app.bsky.feed.threadgate#followerRule'\n}\n\nconst hashFollowerRule = 'followerRule'\n\nexport function isFollowerRule<V>(v: V) {\n  return is$typed(v, id, hashFollowerRule)\n}\n\nexport function validateFollowerRule<V>(v: V) {\n  return validate<FollowerRule & V>(v, id, hashFollowerRule)\n}\n\n/** Allow replies from actors you follow. */\nexport interface FollowingRule {\n  $type?: 'app.bsky.feed.threadgate#followingRule'\n}\n\nconst hashFollowingRule = 'followingRule'\n\nexport function isFollowingRule<V>(v: V) {\n  return is$typed(v, id, hashFollowingRule)\n}\n\nexport function validateFollowingRule<V>(v: V) {\n  return validate<FollowingRule & V>(v, id, hashFollowingRule)\n}\n\n/** Allow replies from actors on a list. */\nexport interface ListRule {\n  $type?: 'app.bsky.feed.threadgate#listRule'\n  list: string\n}\n\nconst hashListRule = 'listRule'\n\nexport function isListRule<V>(v: V) {\n  return is$typed(v, id, hashListRule)\n}\n\nexport function validateListRule<V>(v: V) {\n  return validate<ListRule & V>(v, id, hashListRule)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/block.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.block'\n\nexport interface Main {\n  $type: 'app.bsky.graph.block'\n  /** DID of the account to be blocked. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.defs'\n\nexport interface ListViewBasic {\n  $type?: 'app.bsky.graph.defs#listViewBasic'\n  uri: string\n  cid: string\n  name: string\n  purpose: ListPurpose\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt?: string\n}\n\nconst hashListViewBasic = 'listViewBasic'\n\nexport function isListViewBasic<V>(v: V) {\n  return is$typed(v, id, hashListViewBasic)\n}\n\nexport function validateListViewBasic<V>(v: V) {\n  return validate<ListViewBasic & V>(v, id, hashListViewBasic)\n}\n\nexport interface ListView {\n  $type?: 'app.bsky.graph.defs#listView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  name: string\n  purpose: ListPurpose\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt: string\n}\n\nconst hashListView = 'listView'\n\nexport function isListView<V>(v: V) {\n  return is$typed(v, id, hashListView)\n}\n\nexport function validateListView<V>(v: V) {\n  return validate<ListView & V>(v, id, hashListView)\n}\n\nexport interface ListItemView {\n  $type?: 'app.bsky.graph.defs#listItemView'\n  uri: string\n  subject: AppBskyActorDefs.ProfileView\n}\n\nconst hashListItemView = 'listItemView'\n\nexport function isListItemView<V>(v: V) {\n  return is$typed(v, id, hashListItemView)\n}\n\nexport function validateListItemView<V>(v: V) {\n  return validate<ListItemView & V>(v, id, hashListItemView)\n}\n\nexport interface StarterPackView {\n  $type?: 'app.bsky.graph.defs#starterPackView'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  list?: ListViewBasic\n  listItemsSample?: ListItemView[]\n  feeds?: AppBskyFeedDefs.GeneratorView[]\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackView = 'starterPackView'\n\nexport function isStarterPackView<V>(v: V) {\n  return is$typed(v, id, hashStarterPackView)\n}\n\nexport function validateStarterPackView<V>(v: V) {\n  return validate<StarterPackView & V>(v, id, hashStarterPackView)\n}\n\nexport interface StarterPackViewBasic {\n  $type?: 'app.bsky.graph.defs#starterPackViewBasic'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  listItemCount?: number\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackViewBasic = 'starterPackViewBasic'\n\nexport function isStarterPackViewBasic<V>(v: V) {\n  return is$typed(v, id, hashStarterPackViewBasic)\n}\n\nexport function validateStarterPackViewBasic<V>(v: V) {\n  return validate<StarterPackViewBasic & V>(v, id, hashStarterPackViewBasic)\n}\n\nexport type ListPurpose =\n  | 'app.bsky.graph.defs#modlist'\n  | 'app.bsky.graph.defs#curatelist'\n  | 'app.bsky.graph.defs#referencelist'\n  | (string & {})\n\n/** A list of actors to apply an aggregate moderation action (mute/block) on. */\nexport const MODLIST = `${id}#modlist`\n/** A list of actors used for curation purposes such as list feeds or interaction gating. */\nexport const CURATELIST = `${id}#curatelist`\n/** A list of actors used for only for reference purposes such as within a starter pack. */\nexport const REFERENCELIST = `${id}#referencelist`\n\nexport interface ListViewerState {\n  $type?: 'app.bsky.graph.defs#listViewerState'\n  muted?: boolean\n  blocked?: string\n}\n\nconst hashListViewerState = 'listViewerState'\n\nexport function isListViewerState<V>(v: V) {\n  return is$typed(v, id, hashListViewerState)\n}\n\nexport function validateListViewerState<V>(v: V) {\n  return validate<ListViewerState & V>(v, id, hashListViewerState)\n}\n\n/** indicates that a handle or DID could not be resolved */\nexport interface NotFoundActor {\n  $type?: 'app.bsky.graph.defs#notFoundActor'\n  actor: string\n  notFound: true\n}\n\nconst hashNotFoundActor = 'notFoundActor'\n\nexport function isNotFoundActor<V>(v: V) {\n  return is$typed(v, id, hashNotFoundActor)\n}\n\nexport function validateNotFoundActor<V>(v: V) {\n  return validate<NotFoundActor & V>(v, id, hashNotFoundActor)\n}\n\n/** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) */\nexport interface Relationship {\n  $type?: 'app.bsky.graph.defs#relationship'\n  did: string\n  /** if the actor follows this DID, this is the AT-URI of the follow record */\n  following?: string\n  /** if the actor is followed by this DID, contains the AT-URI of the follow record */\n  followedBy?: string\n  /** if the actor blocks this DID, this is the AT-URI of the block record */\n  blocking?: string\n  /** if the actor is blocked by this DID, contains the AT-URI of the block record */\n  blockedBy?: string\n  /** if the actor blocks this DID via a block list, this is the AT-URI of the listblock record */\n  blockingByList?: string\n  /** if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record */\n  blockedByList?: string\n}\n\nconst hashRelationship = 'relationship'\n\nexport function isRelationship<V>(v: V) {\n  return is$typed(v, id, hashRelationship)\n}\n\nexport function validateRelationship<V>(v: V) {\n  return validate<Relationship & V>(v, id, hashRelationship)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/follow.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.follow'\n\nexport interface Main {\n  $type: 'app.bsky.graph.follow'\n  subject: string\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getActorStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getActorStarterPacks'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getBlocks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blocks: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getFollows.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollows'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  follows: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getKnownFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getKnownFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getList'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the list record to hydrate. */\n  list: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  list: AppBskyGraphDefs.ListView\n  items: AppBskyGraphDefs.ListItemView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getListBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListBlocks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getListMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListMutes'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getLists.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getLists'\n\nexport type QueryParams = {\n  /** The account (actor) to enumerate lists from. */\n  actor: string\n  limit: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getListsWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListsWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  listsWithMembership: ListWithMembership[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** A list and an optional list item indicating membership of a target user to that list. */\nexport interface ListWithMembership {\n  $type?: 'app.bsky.graph.getListsWithMembership#listWithMembership'\n  list: AppBskyGraphDefs.ListView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashListWithMembership = 'listWithMembership'\n\nexport function isListWithMembership<V>(v: V) {\n  return is$typed(v, id, hashListWithMembership)\n}\n\nexport function validateListWithMembership<V>(v: V) {\n  return validate<ListWithMembership & V>(v, id, hashListWithMembership)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getMutes'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  mutes: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getRelationships.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getRelationships'\n\nexport type QueryParams = {\n  /** Primary account requesting relationships for. */\n  actor: string\n  /** List of 'other' accounts to be related back to the primary. */\n  others?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actor?: string\n  relationships: (\n    | $Typed<AppBskyGraphDefs.Relationship>\n    | $Typed<AppBskyGraphDefs.NotFoundActor>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ActorNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getStarterPack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPack'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the starter pack record. */\n  starterPack: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPack: AppBskyGraphDefs.StarterPackView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacks'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacksWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacksWithMembership: StarterPackWithMembership[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** A starter pack and an optional list item indicating membership of a target user to that starter pack. */\nexport interface StarterPackWithMembership {\n  $type?: 'app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership'\n  starterPack: AppBskyGraphDefs.StarterPackView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashStarterPackWithMembership = 'starterPackWithMembership'\n\nexport function isStarterPackWithMembership<V>(v: V) {\n  return is$typed(v, id, hashStarterPackWithMembership)\n}\n\nexport function validateStarterPackWithMembership<V>(v: V) {\n  return validate<StarterPackWithMembership & V>(\n    v,\n    id,\n    hashStarterPackWithMembership,\n  )\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getSuggestedFollowsByActor'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: AppBskyActorDefs.ProfileView[]\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n  /** DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid */\n  isFallback?: boolean\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/list.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.list'\n\nexport interface Main {\n  $type: 'app.bsky.graph.list'\n  purpose: AppBskyGraphDefs.ListPurpose\n  /** Display name for list; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/listblock.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listblock'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listblock'\n  /** Reference (AT-URI) to the mod list record. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/listitem.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listitem'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listitem'\n  /** The account which is included on the list. */\n  subject: string\n  /** Reference (AT-URI) to the list record (app.bsky.graph.list). */\n  list: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/muteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/muteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/muteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/searchStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.searchStarterPacks'\n\nexport type QueryParams = {\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/starterpack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.starterpack'\n\nexport interface Main {\n  $type: 'app.bsky.graph.starterpack'\n  /** Display name for starter pack; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  feeds?: FeedItem[]\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface FeedItem {\n  $type?: 'app.bsky.graph.starterpack#feedItem'\n  uri: string\n}\n\nconst hashFeedItem = 'feedItem'\n\nexport function isFeedItem<V>(v: V) {\n  return is$typed(v, id, hashFeedItem)\n}\n\nexport function validateFeedItem<V>(v: V) {\n  return validate<FeedItem & V>(v, id, hashFeedItem)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/unmuteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/unmuteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/unmuteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/graph/verification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.verification'\n\nexport interface Main {\n  $type: 'app.bsky.graph.verification'\n  /** DID of the subject the verification applies to. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Date of when the verification was created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/labeler/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.defs'\n\nexport interface LabelerView {\n  $type?: 'app.bsky.labeler.defs#labelerView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabelerView = 'labelerView'\n\nexport function isLabelerView<V>(v: V) {\n  return is$typed(v, id, hashLabelerView)\n}\n\nexport function validateLabelerView<V>(v: V) {\n  return validate<LabelerView & V>(v, id, hashLabelerView)\n}\n\nexport interface LabelerViewDetailed {\n  $type?: 'app.bsky.labeler.defs#labelerViewDetailed'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  policies: LabelerPolicies\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n}\n\nconst hashLabelerViewDetailed = 'labelerViewDetailed'\n\nexport function isLabelerViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewDetailed)\n}\n\nexport function validateLabelerViewDetailed<V>(v: V) {\n  return validate<LabelerViewDetailed & V>(v, id, hashLabelerViewDetailed)\n}\n\nexport interface LabelerViewerState {\n  $type?: 'app.bsky.labeler.defs#labelerViewerState'\n  like?: string\n}\n\nconst hashLabelerViewerState = 'labelerViewerState'\n\nexport function isLabelerViewerState<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewerState)\n}\n\nexport function validateLabelerViewerState<V>(v: V) {\n  return validate<LabelerViewerState & V>(v, id, hashLabelerViewerState)\n}\n\nexport interface LabelerPolicies {\n  $type?: 'app.bsky.labeler.defs#labelerPolicies'\n  /** The label values which this labeler publishes. May include global or custom labels. */\n  labelValues: ComAtprotoLabelDefs.LabelValue[]\n  /** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */\n  labelValueDefinitions?: ComAtprotoLabelDefs.LabelValueDefinition[]\n}\n\nconst hashLabelerPolicies = 'labelerPolicies'\n\nexport function isLabelerPolicies<V>(v: V) {\n  return is$typed(v, id, hashLabelerPolicies)\n}\n\nexport function validateLabelerPolicies<V>(v: V) {\n  return validate<LabelerPolicies & V>(v, id, hashLabelerPolicies)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/labeler/getServices.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.getServices'\n\nexport type QueryParams = {\n  dids: string[]\n  detailed: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  views: (\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyLabelerDefs.LabelerViewDetailed>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/labeler/service.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.service'\n\nexport interface Main {\n  $type: 'app.bsky.labeler.service'\n  policies: AppBskyLabelerDefs.LabelerPolicies\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.declaration'\n\nexport interface Main {\n  $type: 'app.bsky.notification.declaration'\n  /** A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'. */\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.defs'\n\nexport interface RecordDeleted {\n  $type?: 'app.bsky.notification.defs#recordDeleted'\n}\n\nconst hashRecordDeleted = 'recordDeleted'\n\nexport function isRecordDeleted<V>(v: V) {\n  return is$typed(v, id, hashRecordDeleted)\n}\n\nexport function validateRecordDeleted<V>(v: V) {\n  return validate<RecordDeleted & V>(v, id, hashRecordDeleted)\n}\n\nexport interface ChatPreference {\n  $type?: 'app.bsky.notification.defs#chatPreference'\n  include: 'all' | 'accepted' | (string & {})\n  push: boolean\n}\n\nconst hashChatPreference = 'chatPreference'\n\nexport function isChatPreference<V>(v: V) {\n  return is$typed(v, id, hashChatPreference)\n}\n\nexport function validateChatPreference<V>(v: V) {\n  return validate<ChatPreference & V>(v, id, hashChatPreference)\n}\n\nexport interface FilterablePreference {\n  $type?: 'app.bsky.notification.defs#filterablePreference'\n  include: 'all' | 'follows' | (string & {})\n  list: boolean\n  push: boolean\n}\n\nconst hashFilterablePreference = 'filterablePreference'\n\nexport function isFilterablePreference<V>(v: V) {\n  return is$typed(v, id, hashFilterablePreference)\n}\n\nexport function validateFilterablePreference<V>(v: V) {\n  return validate<FilterablePreference & V>(v, id, hashFilterablePreference)\n}\n\nexport interface Preference {\n  $type?: 'app.bsky.notification.defs#preference'\n  list: boolean\n  push: boolean\n}\n\nconst hashPreference = 'preference'\n\nexport function isPreference<V>(v: V) {\n  return is$typed(v, id, hashPreference)\n}\n\nexport function validatePreference<V>(v: V) {\n  return validate<Preference & V>(v, id, hashPreference)\n}\n\nexport interface Preferences {\n  $type?: 'app.bsky.notification.defs#preferences'\n  chat: ChatPreference\n  follow: FilterablePreference\n  like: FilterablePreference\n  likeViaRepost: FilterablePreference\n  mention: FilterablePreference\n  quote: FilterablePreference\n  reply: FilterablePreference\n  repost: FilterablePreference\n  repostViaRepost: FilterablePreference\n  starterpackJoined: Preference\n  subscribedPost: Preference\n  unverified: Preference\n  verified: Preference\n}\n\nconst hashPreferences = 'preferences'\n\nexport function isPreferences<V>(v: V) {\n  return is$typed(v, id, hashPreferences)\n}\n\nexport function validatePreferences<V>(v: V) {\n  return validate<Preferences & V>(v, id, hashPreferences)\n}\n\nexport interface ActivitySubscription {\n  $type?: 'app.bsky.notification.defs#activitySubscription'\n  post: boolean\n  reply: boolean\n}\n\nconst hashActivitySubscription = 'activitySubscription'\n\nexport function isActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashActivitySubscription)\n}\n\nexport function validateActivitySubscription<V>(v: V) {\n  return validate<ActivitySubscription & V>(v, id, hashActivitySubscription)\n}\n\n/** Object used to store activity subscription data in stash. */\nexport interface SubjectActivitySubscription {\n  $type?: 'app.bsky.notification.defs#subjectActivitySubscription'\n  subject: string\n  activitySubscription: ActivitySubscription\n}\n\nconst hashSubjectActivitySubscription = 'subjectActivitySubscription'\n\nexport function isSubjectActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashSubjectActivitySubscription)\n}\n\nexport function validateSubjectActivitySubscription<V>(v: V) {\n  return validate<SubjectActivitySubscription & V>(\n    v,\n    id,\n    hashSubjectActivitySubscription,\n  )\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/getUnreadCount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getUnreadCount'\n\nexport type QueryParams = {\n  priority?: boolean\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  count: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/listActivitySubscriptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listActivitySubscriptions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subscriptions: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/listNotifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listNotifications'\n\nexport type QueryParams = {\n  /** Notification reasons to include in response. */\n  reasons?: string[]\n  limit: number\n  priority?: boolean\n  cursor?: string\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  notifications: Notification[]\n  priority?: boolean\n  seenAt?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Notification {\n  $type?: 'app.bsky.notification.listNotifications#notification'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileView\n  /** The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. */\n  reason:\n    | 'like'\n    | 'repost'\n    | 'follow'\n    | 'mention'\n    | 'reply'\n    | 'quote'\n    | 'starterpack-joined'\n    | 'verified'\n    | 'unverified'\n    | 'like-via-repost'\n    | 'repost-via-repost'\n    | 'subscribed-post'\n    | 'contact-match'\n    | (string & {})\n  reasonSubject?: string\n  record: { [_ in string]: unknown }\n  isRead: boolean\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/putActivitySubscription.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putActivitySubscription'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject: string\n  activitySubscription: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface OutputSchema {\n  subject: string\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  priority: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/putPreferencesV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferencesV2'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  chat?: AppBskyNotificationDefs.ChatPreference\n  follow?: AppBskyNotificationDefs.FilterablePreference\n  like?: AppBskyNotificationDefs.FilterablePreference\n  likeViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  mention?: AppBskyNotificationDefs.FilterablePreference\n  quote?: AppBskyNotificationDefs.FilterablePreference\n  reply?: AppBskyNotificationDefs.FilterablePreference\n  repost?: AppBskyNotificationDefs.FilterablePreference\n  repostViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  starterpackJoined?: AppBskyNotificationDefs.Preference\n  subscribedPost?: AppBskyNotificationDefs.Preference\n  unverified?: AppBskyNotificationDefs.Preference\n  verified?: AppBskyNotificationDefs.Preference\n}\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/registerPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.registerPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n  /** Set to true when the actor is age restricted */\n  ageRestricted?: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/unregisterPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.unregisterPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/notification/updateSeen.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.updateSeen'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  seenAt: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/richtext/facet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.richtext.facet'\n\n/** Annotation of a sub-string within rich text. */\nexport interface Main {\n  $type?: 'app.bsky.richtext.facet'\n  index: ByteSlice\n  features: ($Typed<Mention> | $Typed<Link> | $Typed<Tag> | { $type: string })[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\n/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */\nexport interface Mention {\n  $type?: 'app.bsky.richtext.facet#mention'\n  did: string\n}\n\nconst hashMention = 'mention'\n\nexport function isMention<V>(v: V) {\n  return is$typed(v, id, hashMention)\n}\n\nexport function validateMention<V>(v: V) {\n  return validate<Mention & V>(v, id, hashMention)\n}\n\n/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */\nexport interface Link {\n  $type?: 'app.bsky.richtext.facet#link'\n  uri: string\n}\n\nconst hashLink = 'link'\n\nexport function isLink<V>(v: V) {\n  return is$typed(v, id, hashLink)\n}\n\nexport function validateLink<V>(v: V) {\n  return validate<Link & V>(v, id, hashLink)\n}\n\n/** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */\nexport interface Tag {\n  $type?: 'app.bsky.richtext.facet#tag'\n  tag: string\n}\n\nconst hashTag = 'tag'\n\nexport function isTag<V>(v: V) {\n  return is$typed(v, id, hashTag)\n}\n\nexport function validateTag<V>(v: V) {\n  return validate<Tag & V>(v, id, hashTag)\n}\n\n/** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */\nexport interface ByteSlice {\n  $type?: 'app.bsky.richtext.facet#byteSlice'\n  byteStart: number\n  byteEnd: number\n}\n\nconst hashByteSlice = 'byteSlice'\n\nexport function isByteSlice<V>(v: V) {\n  return is$typed(v, id, hashByteSlice)\n}\n\nexport function validateByteSlice<V>(v: V) {\n  return validate<ByteSlice & V>(v, id, hashByteSlice)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.defs'\n\nexport interface SkeletonSearchPost {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchPost'\n  uri: string\n}\n\nconst hashSkeletonSearchPost = 'skeletonSearchPost'\n\nexport function isSkeletonSearchPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchPost)\n}\n\nexport function validateSkeletonSearchPost<V>(v: V) {\n  return validate<SkeletonSearchPost & V>(v, id, hashSkeletonSearchPost)\n}\n\nexport interface SkeletonSearchActor {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchActor'\n  did: string\n}\n\nconst hashSkeletonSearchActor = 'skeletonSearchActor'\n\nexport function isSkeletonSearchActor<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchActor)\n}\n\nexport function validateSkeletonSearchActor<V>(v: V) {\n  return validate<SkeletonSearchActor & V>(v, id, hashSkeletonSearchActor)\n}\n\nexport interface SkeletonSearchStarterPack {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchStarterPack'\n  uri: string\n}\n\nconst hashSkeletonSearchStarterPack = 'skeletonSearchStarterPack'\n\nexport function isSkeletonSearchStarterPack<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchStarterPack)\n}\n\nexport function validateSkeletonSearchStarterPack<V>(v: V) {\n  return validate<SkeletonSearchStarterPack & V>(\n    v,\n    id,\n    hashSkeletonSearchStarterPack,\n  )\n}\n\nexport interface TrendingTopic {\n  $type?: 'app.bsky.unspecced.defs#trendingTopic'\n  topic: string\n  displayName?: string\n  description?: string\n  link: string\n}\n\nconst hashTrendingTopic = 'trendingTopic'\n\nexport function isTrendingTopic<V>(v: V) {\n  return is$typed(v, id, hashTrendingTopic)\n}\n\nexport function validateTrendingTopic<V>(v: V) {\n  return validate<TrendingTopic & V>(v, id, hashTrendingTopic)\n}\n\nexport interface SkeletonTrend {\n  $type?: 'app.bsky.unspecced.defs#skeletonTrend'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  dids: string[]\n}\n\nconst hashSkeletonTrend = 'skeletonTrend'\n\nexport function isSkeletonTrend<V>(v: V) {\n  return is$typed(v, id, hashSkeletonTrend)\n}\n\nexport function validateSkeletonTrend<V>(v: V) {\n  return validate<SkeletonTrend & V>(v, id, hashSkeletonTrend)\n}\n\nexport interface TrendView {\n  $type?: 'app.bsky.unspecced.defs#trendView'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nconst hashTrendView = 'trendView'\n\nexport function isTrendView<V>(v: V) {\n  return is$typed(v, id, hashTrendView)\n}\n\nexport function validateTrendView<V>(v: V) {\n  return validate<TrendView & V>(v, id, hashTrendView)\n}\n\nexport interface ThreadItemPost {\n  $type?: 'app.bsky.unspecced.defs#threadItemPost'\n  post: AppBskyFeedDefs.PostView\n  /** This post has more parents that were not present in the response. This is just a boolean, without the number of parents. */\n  moreParents: boolean\n  /** This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. */\n  moreReplies: number\n  /** This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. */\n  opThread: boolean\n  /** The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. */\n  hiddenByThreadgate: boolean\n  /** This is by an account muted by the viewer requesting it. */\n  mutedByViewer: boolean\n}\n\nconst hashThreadItemPost = 'threadItemPost'\n\nexport function isThreadItemPost<V>(v: V) {\n  return is$typed(v, id, hashThreadItemPost)\n}\n\nexport function validateThreadItemPost<V>(v: V) {\n  return validate<ThreadItemPost & V>(v, id, hashThreadItemPost)\n}\n\nexport interface ThreadItemNoUnauthenticated {\n  $type?: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated'\n}\n\nconst hashThreadItemNoUnauthenticated = 'threadItemNoUnauthenticated'\n\nexport function isThreadItemNoUnauthenticated<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNoUnauthenticated)\n}\n\nexport function validateThreadItemNoUnauthenticated<V>(v: V) {\n  return validate<ThreadItemNoUnauthenticated & V>(\n    v,\n    id,\n    hashThreadItemNoUnauthenticated,\n  )\n}\n\nexport interface ThreadItemNotFound {\n  $type?: 'app.bsky.unspecced.defs#threadItemNotFound'\n}\n\nconst hashThreadItemNotFound = 'threadItemNotFound'\n\nexport function isThreadItemNotFound<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNotFound)\n}\n\nexport function validateThreadItemNotFound<V>(v: V) {\n  return validate<ThreadItemNotFound & V>(v, id, hashThreadItemNotFound)\n}\n\nexport interface ThreadItemBlocked {\n  $type?: 'app.bsky.unspecced.defs#threadItemBlocked'\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashThreadItemBlocked = 'threadItemBlocked'\n\nexport function isThreadItemBlocked<V>(v: V) {\n  return is$typed(v, id, hashThreadItemBlocked)\n}\n\nexport function validateThreadItemBlocked<V>(v: V) {\n  return validate<ThreadItemBlocked & V>(v, id, hashThreadItemBlocked)\n}\n\n/** The computed state of the age assurance process, returned to the user in question on certain authenticated requests. */\nexport interface AgeAssuranceState {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceState'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n}\n\nconst hashAgeAssuranceState = 'ageAssuranceState'\n\nexport function isAgeAssuranceState<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceState)\n}\n\nexport function validateAgeAssuranceState<V>(v: V) {\n  return validate<AgeAssuranceState & V>(v, id, hashAgeAssuranceState)\n}\n\n/** Object used to store age assurance data in stash. */\nexport interface AgeAssuranceEvent {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The email used for AA. */\n  email?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getAgeAssuranceState'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  checkEmailConfirmed?: boolean\n  liveNow?: LiveNowConfig[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface LiveNowConfig {\n  $type?: 'app.bsky.unspecced.getConfig#liveNowConfig'\n  did: string\n  domains: string[]\n}\n\nconst hashLiveNowConfig = 'liveNowConfig'\n\nexport function isLiveNowConfig<V>(v: V) {\n  return is$typed(v, id, hashLiveNowConfig)\n}\n\nexport function validateLiveNowConfig<V>(v: V) {\n  return validate<LiveNowConfig & V>(v, id, hashLiveNowConfig)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPopularFeedGenerators'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  query?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadOtherV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post. */\n  anchor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of other thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadOtherV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post. */\n  anchor: string\n  /** Whether to include parents above the anchor. */\n  above: boolean\n  /** How many levels of replies to include below the anchor. */\n  below: number\n  /** Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated). */\n  branchingFactor: number\n  /** Sorting for the thread replies. */\n  sort: 'newest' | 'oldest' | 'top' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n  /** Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them. */\n  hasOtherReplies: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value:\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemPost>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNotFound>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemBlocked>\n    | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedFeedsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeedsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedOnboardingUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedOnboardingUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestionsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit: number\n  cursor?: string\n  /** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. */\n  relativeToDid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n  /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */\n  relativeToDid?: string\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTaggedSuggestions'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: Suggestion[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Suggestion {\n  $type?: 'app.bsky.unspecced.getTaggedSuggestions#suggestion'\n  tag: string\n  subjectType: 'actor' | 'feed' | (string & {})\n  subject: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getTrendingTopics.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendingTopics'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  topics: AppBskyUnspeccedDefs.TrendingTopic[]\n  suggested: AppBskyUnspeccedDefs.TrendingTopic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getTrends.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrends'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.TrendView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.SkeletonTrend[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.initAgeAssurance'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n}\n\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidEmail' | 'DidTooLong' | 'InvalidInitiation'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchActorsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  /** If true, acts as fast/simple 'typeahead' query. */\n  typeahead?: boolean\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchPostsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries. */\n  viewer?: string\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyUnspeccedDefs.SkeletonSearchPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/unspecced/searchStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  starterPacks: AppBskyUnspeccedDefs.SkeletonSearchStarterPack[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/video/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.defs'\n\nexport interface JobStatus {\n  $type?: 'app.bsky.video.defs#jobStatus'\n  jobId: string\n  did: string\n  /** The state of the video processing job. All values not listed as a known value indicate that the job is in process. */\n  state: 'JOB_STATE_COMPLETED' | 'JOB_STATE_FAILED' | (string & {})\n  /** Progress within the current processing state. */\n  progress?: number\n  blob?: BlobRef\n  error?: string\n  message?: string\n}\n\nconst hashJobStatus = 'jobStatus'\n\nexport function isJobStatus<V>(v: V) {\n  return is$typed(v, id, hashJobStatus)\n}\n\nexport function validateJobStatus<V>(v: V) {\n  return validate<JobStatus & V>(v, id, hashJobStatus)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/video/getJobStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getJobStatus'\n\nexport type QueryParams = {\n  jobId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/video/getUploadLimits.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getUploadLimits'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canUpload: boolean\n  remainingDailyVideos?: number\n  remainingDailyBytes?: number\n  message?: string\n  error?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/app/bsky/video/uploadVideo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.uploadVideo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport interface HandlerInput {\n  encoding: 'video/mp4'\n  body: stream.Readable\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/actor/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.declaration'\n\nexport interface Main {\n  $type: 'chat.bsky.actor.declaration'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'chat.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  avatar?: string\n  associated?: AppBskyActorDefs.ProfileAssociated\n  viewer?: AppBskyActorDefs.ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** Set to true when the actor cannot actively participate in conversations */\n  chatDisabled?: boolean\n  verification?: AppBskyActorDefs.VerificationState\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/actor/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.deleteAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/actor/exportAccountData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.exportAccountData'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/jsonl'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/acceptConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.acceptConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  /** Rev when the convo was accepted. If not present, the convo was already accepted. */\n  rev?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/addReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.addReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'ReactionMessageDeleted'\n    | 'ReactionLimitReached'\n    | 'ReactionInvalidValue'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../../../app/bsky/richtext/facet.js'\nimport type * as AppBskyEmbedRecord from '../../../app/bsky/embed/record.js'\nimport type * as ChatBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.defs'\n\nexport interface MessageRef {\n  $type?: 'chat.bsky.convo.defs#messageRef'\n  did: string\n  convoId: string\n  messageId: string\n}\n\nconst hashMessageRef = 'messageRef'\n\nexport function isMessageRef<V>(v: V) {\n  return is$typed(v, id, hashMessageRef)\n}\n\nexport function validateMessageRef<V>(v: V) {\n  return validate<MessageRef & V>(v, id, hashMessageRef)\n}\n\nexport interface MessageInput {\n  $type?: 'chat.bsky.convo.defs#messageInput'\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.Main> | { $type: string }\n}\n\nconst hashMessageInput = 'messageInput'\n\nexport function isMessageInput<V>(v: V) {\n  return is$typed(v, id, hashMessageInput)\n}\n\nexport function validateMessageInput<V>(v: V) {\n  return validate<MessageInput & V>(v, id, hashMessageInput)\n}\n\nexport interface MessageView {\n  $type?: 'chat.bsky.convo.defs#messageView'\n  id: string\n  rev: string\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.View> | { $type: string }\n  /** Reactions to this message, in ascending order of creation time. */\n  reactions?: ReactionView[]\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashMessageView = 'messageView'\n\nexport function isMessageView<V>(v: V) {\n  return is$typed(v, id, hashMessageView)\n}\n\nexport function validateMessageView<V>(v: V) {\n  return validate<MessageView & V>(v, id, hashMessageView)\n}\n\nexport interface DeletedMessageView {\n  $type?: 'chat.bsky.convo.defs#deletedMessageView'\n  id: string\n  rev: string\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashDeletedMessageView = 'deletedMessageView'\n\nexport function isDeletedMessageView<V>(v: V) {\n  return is$typed(v, id, hashDeletedMessageView)\n}\n\nexport function validateDeletedMessageView<V>(v: V) {\n  return validate<DeletedMessageView & V>(v, id, hashDeletedMessageView)\n}\n\nexport interface MessageViewSender {\n  $type?: 'chat.bsky.convo.defs#messageViewSender'\n  did: string\n}\n\nconst hashMessageViewSender = 'messageViewSender'\n\nexport function isMessageViewSender<V>(v: V) {\n  return is$typed(v, id, hashMessageViewSender)\n}\n\nexport function validateMessageViewSender<V>(v: V) {\n  return validate<MessageViewSender & V>(v, id, hashMessageViewSender)\n}\n\nexport interface ReactionView {\n  $type?: 'chat.bsky.convo.defs#reactionView'\n  value: string\n  sender: ReactionViewSender\n  createdAt: string\n}\n\nconst hashReactionView = 'reactionView'\n\nexport function isReactionView<V>(v: V) {\n  return is$typed(v, id, hashReactionView)\n}\n\nexport function validateReactionView<V>(v: V) {\n  return validate<ReactionView & V>(v, id, hashReactionView)\n}\n\nexport interface ReactionViewSender {\n  $type?: 'chat.bsky.convo.defs#reactionViewSender'\n  did: string\n}\n\nconst hashReactionViewSender = 'reactionViewSender'\n\nexport function isReactionViewSender<V>(v: V) {\n  return is$typed(v, id, hashReactionViewSender)\n}\n\nexport function validateReactionViewSender<V>(v: V) {\n  return validate<ReactionViewSender & V>(v, id, hashReactionViewSender)\n}\n\nexport interface MessageAndReactionView {\n  $type?: 'chat.bsky.convo.defs#messageAndReactionView'\n  message: MessageView\n  reaction: ReactionView\n}\n\nconst hashMessageAndReactionView = 'messageAndReactionView'\n\nexport function isMessageAndReactionView<V>(v: V) {\n  return is$typed(v, id, hashMessageAndReactionView)\n}\n\nexport function validateMessageAndReactionView<V>(v: V) {\n  return validate<MessageAndReactionView & V>(v, id, hashMessageAndReactionView)\n}\n\nexport interface ConvoView {\n  $type?: 'chat.bsky.convo.defs#convoView'\n  id: string\n  rev: string\n  members: ChatBskyActorDefs.ProfileViewBasic[]\n  lastMessage?:\n    | $Typed<MessageView>\n    | $Typed<DeletedMessageView>\n    | { $type: string }\n  lastReaction?: $Typed<MessageAndReactionView> | { $type: string }\n  muted: boolean\n  status?: 'request' | 'accepted' | (string & {})\n  unreadCount: number\n}\n\nconst hashConvoView = 'convoView'\n\nexport function isConvoView<V>(v: V) {\n  return is$typed(v, id, hashConvoView)\n}\n\nexport function validateConvoView<V>(v: V) {\n  return validate<ConvoView & V>(v, id, hashConvoView)\n}\n\nexport interface LogBeginConvo {\n  $type?: 'chat.bsky.convo.defs#logBeginConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogBeginConvo = 'logBeginConvo'\n\nexport function isLogBeginConvo<V>(v: V) {\n  return is$typed(v, id, hashLogBeginConvo)\n}\n\nexport function validateLogBeginConvo<V>(v: V) {\n  return validate<LogBeginConvo & V>(v, id, hashLogBeginConvo)\n}\n\nexport interface LogAcceptConvo {\n  $type?: 'chat.bsky.convo.defs#logAcceptConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogAcceptConvo = 'logAcceptConvo'\n\nexport function isLogAcceptConvo<V>(v: V) {\n  return is$typed(v, id, hashLogAcceptConvo)\n}\n\nexport function validateLogAcceptConvo<V>(v: V) {\n  return validate<LogAcceptConvo & V>(v, id, hashLogAcceptConvo)\n}\n\nexport interface LogLeaveConvo {\n  $type?: 'chat.bsky.convo.defs#logLeaveConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogLeaveConvo = 'logLeaveConvo'\n\nexport function isLogLeaveConvo<V>(v: V) {\n  return is$typed(v, id, hashLogLeaveConvo)\n}\n\nexport function validateLogLeaveConvo<V>(v: V) {\n  return validate<LogLeaveConvo & V>(v, id, hashLogLeaveConvo)\n}\n\nexport interface LogMuteConvo {\n  $type?: 'chat.bsky.convo.defs#logMuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogMuteConvo = 'logMuteConvo'\n\nexport function isLogMuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogMuteConvo)\n}\n\nexport function validateLogMuteConvo<V>(v: V) {\n  return validate<LogMuteConvo & V>(v, id, hashLogMuteConvo)\n}\n\nexport interface LogUnmuteConvo {\n  $type?: 'chat.bsky.convo.defs#logUnmuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogUnmuteConvo = 'logUnmuteConvo'\n\nexport function isLogUnmuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogUnmuteConvo)\n}\n\nexport function validateLogUnmuteConvo<V>(v: V) {\n  return validate<LogUnmuteConvo & V>(v, id, hashLogUnmuteConvo)\n}\n\nexport interface LogCreateMessage {\n  $type?: 'chat.bsky.convo.defs#logCreateMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogCreateMessage = 'logCreateMessage'\n\nexport function isLogCreateMessage<V>(v: V) {\n  return is$typed(v, id, hashLogCreateMessage)\n}\n\nexport function validateLogCreateMessage<V>(v: V) {\n  return validate<LogCreateMessage & V>(v, id, hashLogCreateMessage)\n}\n\nexport interface LogDeleteMessage {\n  $type?: 'chat.bsky.convo.defs#logDeleteMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogDeleteMessage = 'logDeleteMessage'\n\nexport function isLogDeleteMessage<V>(v: V) {\n  return is$typed(v, id, hashLogDeleteMessage)\n}\n\nexport function validateLogDeleteMessage<V>(v: V) {\n  return validate<LogDeleteMessage & V>(v, id, hashLogDeleteMessage)\n}\n\nexport interface LogReadMessage {\n  $type?: 'chat.bsky.convo.defs#logReadMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogReadMessage = 'logReadMessage'\n\nexport function isLogReadMessage<V>(v: V) {\n  return is$typed(v, id, hashLogReadMessage)\n}\n\nexport function validateLogReadMessage<V>(v: V) {\n  return validate<LogReadMessage & V>(v, id, hashLogReadMessage)\n}\n\nexport interface LogAddReaction {\n  $type?: 'chat.bsky.convo.defs#logAddReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogAddReaction = 'logAddReaction'\n\nexport function isLogAddReaction<V>(v: V) {\n  return is$typed(v, id, hashLogAddReaction)\n}\n\nexport function validateLogAddReaction<V>(v: V) {\n  return validate<LogAddReaction & V>(v, id, hashLogAddReaction)\n}\n\nexport interface LogRemoveReaction {\n  $type?: 'chat.bsky.convo.defs#logRemoveReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogRemoveReaction = 'logRemoveReaction'\n\nexport function isLogRemoveReaction<V>(v: V) {\n  return is$typed(v, id, hashLogRemoveReaction)\n}\n\nexport function validateLogRemoveReaction<V>(v: V) {\n  return validate<LogRemoveReaction & V>(v, id, hashLogRemoveReaction)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/deleteMessageForSelf.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.deleteMessageForSelf'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.DeletedMessageView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/getConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvo'\n\nexport type QueryParams = {\n  convoId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/getConvoAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoAvailability'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canChat: boolean\n  convo?: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/getConvoForMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoForMembers'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/getLog.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getLog'\n\nexport type QueryParams = {\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  logs: (\n    | $Typed<ChatBskyConvoDefs.LogBeginConvo>\n    | $Typed<ChatBskyConvoDefs.LogAcceptConvo>\n    | $Typed<ChatBskyConvoDefs.LogLeaveConvo>\n    | $Typed<ChatBskyConvoDefs.LogMuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogUnmuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogCreateMessage>\n    | $Typed<ChatBskyConvoDefs.LogDeleteMessage>\n    | $Typed<ChatBskyConvoDefs.LogReadMessage>\n    | $Typed<ChatBskyConvoDefs.LogAddReaction>\n    | $Typed<ChatBskyConvoDefs.LogRemoveReaction>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/getMessages.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getMessages'\n\nexport type QueryParams = {\n  convoId: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/leaveConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.leaveConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convoId: string\n  rev: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/listConvos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.listConvos'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  readState?: 'unread' | (string & {})\n  status?: 'request' | 'accepted' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  convos: ChatBskyConvoDefs.ConvoView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/muteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.muteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/removeReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.removeReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ReactionMessageDeleted' | 'ReactionInvalidValue'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/sendMessage.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessage'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.MessageView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/sendMessageBatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessageBatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  items: BatchItem[]\n}\n\nexport interface OutputSchema {\n  items: ChatBskyConvoDefs.MessageView[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface BatchItem {\n  $type?: 'chat.bsky.convo.sendMessageBatch#batchItem'\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nconst hashBatchItem = 'batchItem'\n\nexport function isBatchItem<V>(v: V) {\n  return is$typed(v, id, hashBatchItem)\n}\n\nexport function validateBatchItem<V>(v: V) {\n  return validate<BatchItem & V>(v, id, hashBatchItem)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/unmuteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.unmuteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/updateAllRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateAllRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  status?: 'request' | 'accepted' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** The count of updated convos. */\n  updatedCount: number\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/convo/updateRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId?: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/moderation/getActorMetadata.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getActorMetadata'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  day: Metadata\n  month: Metadata\n  all: Metadata\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Metadata {\n  $type?: 'chat.bsky.moderation.getActorMetadata#metadata'\n  messagesSent: number\n  messagesReceived: number\n  convos: number\n  convosStarted: number\n}\n\nconst hashMetadata = 'metadata'\n\nexport function isMetadata<V>(v: V) {\n  return is$typed(v, id, hashMetadata)\n}\n\nexport function validateMetadata<V>(v: V) {\n  return validate<Metadata & V>(v, id, hashMetadata)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/moderation/getMessageContext.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from '../convo/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getMessageContext'\n\nexport type QueryParams = {\n  /** Conversation that the message is from. NOTE: this field will eventually be required. */\n  convoId?: string\n  messageId: string\n  before: number\n  after: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/chat/bsky/moderation/updateActorAccess.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.updateActorAccess'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n  allowAccess: boolean\n  ref?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.defs'\n\nexport interface StatusAttr {\n  $type?: 'com.atproto.admin.defs#statusAttr'\n  applied: boolean\n  ref?: string\n}\n\nconst hashStatusAttr = 'statusAttr'\n\nexport function isStatusAttr<V>(v: V) {\n  return is$typed(v, id, hashStatusAttr)\n}\n\nexport function validateStatusAttr<V>(v: V) {\n  return validate<StatusAttr & V>(v, id, hashStatusAttr)\n}\n\nexport interface AccountView {\n  $type?: 'com.atproto.admin.defs#accountView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords?: { [_ in string]: unknown }[]\n  indexedAt: string\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  emailConfirmedAt?: string\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ThreatSignature[]\n}\n\nconst hashAccountView = 'accountView'\n\nexport function isAccountView<V>(v: V) {\n  return is$typed(v, id, hashAccountView)\n}\n\nexport function validateAccountView<V>(v: V) {\n  return validate<AccountView & V>(v, id, hashAccountView)\n}\n\nexport interface RepoRef {\n  $type?: 'com.atproto.admin.defs#repoRef'\n  did: string\n}\n\nconst hashRepoRef = 'repoRef'\n\nexport function isRepoRef<V>(v: V) {\n  return is$typed(v, id, hashRepoRef)\n}\n\nexport function validateRepoRef<V>(v: V) {\n  return validate<RepoRef & V>(v, id, hashRepoRef)\n}\n\nexport interface RepoBlobRef {\n  $type?: 'com.atproto.admin.defs#repoBlobRef'\n  did: string\n  cid: string\n  recordUri?: string\n}\n\nconst hashRepoBlobRef = 'repoBlobRef'\n\nexport function isRepoBlobRef<V>(v: V) {\n  return is$typed(v, id, hashRepoBlobRef)\n}\n\nexport function validateRepoBlobRef<V>(v: V) {\n  return validate<RepoBlobRef & V>(v, id, hashRepoBlobRef)\n}\n\nexport interface ThreatSignature {\n  $type?: 'com.atproto.admin.defs#threatSignature'\n  property: string\n  value: string\n}\n\nconst hashThreatSignature = 'threatSignature'\n\nexport function isThreatSignature<V>(v: V) {\n  return is$typed(v, id, hashThreatSignature)\n}\n\nexport function validateThreatSignature<V>(v: V) {\n  return validate<ThreatSignature & V>(v, id, hashThreatSignature)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for disabled invites. */\n  note?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codes?: string[]\n  accounts?: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.enableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for enabled invites. */\n  note?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/getAccountInfo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoAdminDefs.AccountView\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/getAccountInfos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  infos: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/getInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getInviteCodes'\n\nexport type QueryParams = {\n  sort: 'recent' | 'usage' | (string & {})\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getSubjectStatus'\n\nexport type QueryParams = {\n  did?: string\n  uri?: string\n  blob?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.searchAccounts'\n\nexport type QueryParams = {\n  email?: string\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/sendEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.sendEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  recipientDid: string\n  content: string\n  subject?: string\n  senderDid: string\n  /** Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers */\n  comment?: string\n}\n\nexport interface OutputSchema {\n  sent: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo. */\n  account: string\n  email: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  handle: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  /** Did-key formatted public key */\n  signingKey: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateSubjectStatus'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.defs'\n\nexport interface IdentityInfo {\n  $type?: 'com.atproto.identity.defs#identityInfo'\n  did: string\n  /** The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document. */\n  handle: string\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nconst hashIdentityInfo = 'identityInfo'\n\nexport function isIdentityInfo<V>(v: V) {\n  return is$typed(v, id, hashIdentityInfo)\n}\n\nexport function validateIdentityInfo<V>(v: V) {\n  return validate<IdentityInfo & V>(v, id, hashIdentityInfo)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/getRecommendedDidCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.getRecommendedDidCredentials'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs. */\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/refreshIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.refreshIdentity'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  identifier: string\n}\n\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound' | 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/requestPlcOperationSignature.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.requestPlcOperationSignature'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/resolveDid.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveDid'\n\nexport type QueryParams = {\n  /** DID to resolve. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/resolveHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveHandle'\n\nexport type QueryParams = {\n  /** The handle to resolve. */\n  handle: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/resolveIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveIdentity'\n\nexport type QueryParams = {\n  /** Handle or DID to resolve. */\n  identifier: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound' | 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/signPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.signPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A token received through com.atproto.identity.requestPlcOperationSignature */\n  token?: string\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport interface OutputSchema {\n  /** A signed DID PLC operation. */\n  operation: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/submitPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.submitPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  operation: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/identity/updateHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.updateHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The new handle. */\n  handle: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/label/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.defs'\n\n/** Metadata tag on an atproto resource (eg, repo or record). */\nexport interface Label {\n  $type?: 'com.atproto.label.defs#label'\n  /** The AT Protocol version of the label object. */\n  ver?: number\n  /** DID of the actor who created this label. */\n  src: string\n  /** AT URI of the record, repository (account), or other resource that this label applies to. */\n  uri: string\n  /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */\n  cid?: string\n  /** The short string name of the value or type of this label. */\n  val: string\n  /** If true, this is a negation label, overwriting a previous label. */\n  neg?: boolean\n  /** Timestamp when this label was created. */\n  cts: string\n  /** Timestamp at which this label expires (no longer applies). */\n  exp?: string\n  /** Signature of dag-cbor encoded label. */\n  sig?: Uint8Array\n}\n\nconst hashLabel = 'label'\n\nexport function isLabel<V>(v: V) {\n  return is$typed(v, id, hashLabel)\n}\n\nexport function validateLabel<V>(v: V) {\n  return validate<Label & V>(v, id, hashLabel)\n}\n\n/** Metadata tags on an atproto record, published by the author within the record. */\nexport interface SelfLabels {\n  $type?: 'com.atproto.label.defs#selfLabels'\n  values: SelfLabel[]\n}\n\nconst hashSelfLabels = 'selfLabels'\n\nexport function isSelfLabels<V>(v: V) {\n  return is$typed(v, id, hashSelfLabels)\n}\n\nexport function validateSelfLabels<V>(v: V) {\n  return validate<SelfLabels & V>(v, id, hashSelfLabels)\n}\n\n/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */\nexport interface SelfLabel {\n  $type?: 'com.atproto.label.defs#selfLabel'\n  /** The short string name of the value or type of this label. */\n  val: string\n}\n\nconst hashSelfLabel = 'selfLabel'\n\nexport function isSelfLabel<V>(v: V) {\n  return is$typed(v, id, hashSelfLabel)\n}\n\nexport function validateSelfLabel<V>(v: V) {\n  return validate<SelfLabel & V>(v, id, hashSelfLabel)\n}\n\n/** Declares a label value and its expected interpretations and behaviors. */\nexport interface LabelValueDefinition {\n  $type?: 'com.atproto.label.defs#labelValueDefinition'\n  /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */\n  identifier: string\n  /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */\n  severity: 'inform' | 'alert' | 'none' | (string & {})\n  /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */\n  blurs: 'content' | 'media' | 'none' | (string & {})\n  /** The default setting for this label. */\n  defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})\n  /** Does the user need to have adult content enabled in order to configure this label? */\n  adultOnly?: boolean\n  locales: LabelValueDefinitionStrings[]\n}\n\nconst hashLabelValueDefinition = 'labelValueDefinition'\n\nexport function isLabelValueDefinition<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinition)\n}\n\nexport function validateLabelValueDefinition<V>(v: V) {\n  return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)\n}\n\n/** Strings which describe the label in the UI, localized into a specific language. */\nexport interface LabelValueDefinitionStrings {\n  $type?: 'com.atproto.label.defs#labelValueDefinitionStrings'\n  /** The code of the language these strings are written in. */\n  lang: string\n  /** A short human-readable name for the label. */\n  name: string\n  /** A longer description of what the label means and why it might be applied. */\n  description: string\n}\n\nconst hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'\n\nexport function isLabelValueDefinitionStrings<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinitionStrings)\n}\n\nexport function validateLabelValueDefinitionStrings<V>(v: V) {\n  return validate<LabelValueDefinitionStrings & V>(\n    v,\n    id,\n    hashLabelValueDefinitionStrings,\n  )\n}\n\nexport type LabelValue =\n  | '!hide'\n  | '!no-promote'\n  | '!warn'\n  | '!no-unauthenticated'\n  | 'dmca-violation'\n  | 'doxxing'\n  | 'porn'\n  | 'sexual'\n  | 'nudity'\n  | 'nsfl'\n  | 'gore'\n  | (string & {})\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/label/queryLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.queryLabels'\n\nexport type QueryParams = {\n  /** List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI. */\n  uriPatterns: string[]\n  /** Optional list of label sources (DIDs) to filter on. */\n  sources?: string[]\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/label/subscribeLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport { ErrorFrame } from '@atproto/xrpc-server'\nimport { IncomingMessage } from 'node:http'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.subscribeLabels'\n\nexport type QueryParams = {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\nexport type OutputSchema = $Typed<Labels> | $Typed<Info> | { $type: string }\nexport type HandlerError = ErrorFrame<'FutureCursor'>\nexport type HandlerOutput = HandlerError | OutputSchema\n\nexport interface Labels {\n  $type?: 'com.atproto.label.subscribeLabels#labels'\n  seq: number\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabels = 'labels'\n\nexport function isLabels<V>(v: V) {\n  return is$typed(v, id, hashLabels)\n}\n\nexport function validateLabels<V>(v: V) {\n  return validate<Labels & V>(v, id, hashLabels)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.label.subscribeLabels#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/lexicon/resolveLexicon.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLexiconSchema from './schema.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.resolveLexicon'\n\nexport type QueryParams = {\n  /** The lexicon NSID to resolve. */\n  nsid: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The CID of the lexicon schema record. */\n  cid: string\n  schema: ComAtprotoLexiconSchema.Main\n  /** The AT-URI of the lexicon schema record. */\n  uri: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'LexiconNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/lexicon/schema.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.schema'\n\nexport interface Main {\n  $type: 'com.atproto.lexicon.schema'\n  /** Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system. */\n  lexicon: number\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/moderation/createReport.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.createReport'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  /** Additional context about the content and violation. */\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  modTool?: ModTool\n}\n\nexport interface OutputSchema {\n  id: number\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  reportedBy: string\n  createdAt: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'com.atproto.moderation.createReport#modTool'\n  /** Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.defs'\n\nexport type ReasonType =\n  | 'com.atproto.moderation.defs#reasonSpam'\n  | 'com.atproto.moderation.defs#reasonViolation'\n  | 'com.atproto.moderation.defs#reasonMisleading'\n  | 'com.atproto.moderation.defs#reasonSexual'\n  | 'com.atproto.moderation.defs#reasonRude'\n  | 'com.atproto.moderation.defs#reasonOther'\n  | 'com.atproto.moderation.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`. */\nexport const REASONSPAM = `${id}#reasonSpam`\n/** Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`. */\nexport const REASONVIOLATION = `${id}#reasonViolation`\n/** Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`. */\nexport const REASONMISLEADING = `${id}#reasonMisleading`\n/** Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`. */\nexport const REASONSEXUAL = `${id}#reasonSexual`\n/** Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`. */\nexport const REASONRUDE = `${id}#reasonRude`\n/** Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`. */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n\n/** Tag describing a type of subject that might be reported. */\nexport type SubjectType = 'account' | 'record' | 'chat' | (string & {})\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/applyWrites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.applyWrites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  writes: ($Typed<Create> | $Typed<Update> | $Typed<Delete>)[]\n  /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  results?: (\n    | $Typed<CreateResult>\n    | $Typed<UpdateResult>\n    | $Typed<DeleteResult>\n  )[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Operation which creates a new record. */\nexport interface Create {\n  $type?: 'com.atproto.repo.applyWrites#create'\n  collection: string\n  /** NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. */\n  rkey?: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashCreate = 'create'\n\nexport function isCreate<V>(v: V) {\n  return is$typed(v, id, hashCreate)\n}\n\nexport function validateCreate<V>(v: V) {\n  return validate<Create & V>(v, id, hashCreate)\n}\n\n/** Operation which updates an existing record. */\nexport interface Update {\n  $type?: 'com.atproto.repo.applyWrites#update'\n  collection: string\n  rkey: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashUpdate = 'update'\n\nexport function isUpdate<V>(v: V) {\n  return is$typed(v, id, hashUpdate)\n}\n\nexport function validateUpdate<V>(v: V) {\n  return validate<Update & V>(v, id, hashUpdate)\n}\n\n/** Operation which deletes an existing record. */\nexport interface Delete {\n  $type?: 'com.atproto.repo.applyWrites#delete'\n  collection: string\n  rkey: string\n}\n\nconst hashDelete = 'delete'\n\nexport function isDelete<V>(v: V) {\n  return is$typed(v, id, hashDelete)\n}\n\nexport function validateDelete<V>(v: V) {\n  return validate<Delete & V>(v, id, hashDelete)\n}\n\nexport interface CreateResult {\n  $type?: 'com.atproto.repo.applyWrites#createResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashCreateResult = 'createResult'\n\nexport function isCreateResult<V>(v: V) {\n  return is$typed(v, id, hashCreateResult)\n}\n\nexport function validateCreateResult<V>(v: V) {\n  return validate<CreateResult & V>(v, id, hashCreateResult)\n}\n\nexport interface UpdateResult {\n  $type?: 'com.atproto.repo.applyWrites#updateResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashUpdateResult = 'updateResult'\n\nexport function isUpdateResult<V>(v: V) {\n  return is$typed(v, id, hashUpdateResult)\n}\n\nexport function validateUpdateResult<V>(v: V) {\n  return validate<UpdateResult & V>(v, id, hashUpdateResult)\n}\n\nexport interface DeleteResult {\n  $type?: 'com.atproto.repo.applyWrites#deleteResult'\n}\n\nconst hashDeleteResult = 'deleteResult'\n\nexport function isDeleteResult<V>(v: V) {\n  return is$typed(v, id, hashDeleteResult)\n}\n\nexport function validateDeleteResult<V>(v: V) {\n  return validate<DeleteResult & V>(v, id, hashDeleteResult)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/createRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.createRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey?: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record itself. Must contain a $type field. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.defs'\n\nexport interface CommitMeta {\n  $type?: 'com.atproto.repo.defs#commitMeta'\n  cid: string\n  rev: string\n}\n\nconst hashCommitMeta = 'commitMeta'\n\nexport function isCommitMeta<V>(v: V) {\n  return is$typed(v, id, hashCommitMeta)\n}\n\nexport function validateCommitMeta<V>(v: V) {\n  return validate<CommitMeta & V>(v, id, hashCommitMeta)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/deleteRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.deleteRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Compare and swap with the previous record by CID. */\n  swapRecord?: string\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/describeRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.describeRepo'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  /** The complete DID document for this account. */\n  didDoc: { [_ in string]: unknown }\n  /** List of all the collections (NSIDs) for which this repo contains at least one record. */\n  collections: string[]\n  /** Indicates if handle is currently valid (resolves bi-directionally) */\n  handleIsCorrect: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.getRecord'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** The CID of the version of the record. If not specified, then return the most recent version. */\n  cid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  value: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RecordNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/importRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.importRepo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface HandlerInput {\n  encoding: 'application/vnd.ipld.car'\n  body: stream.Readable\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/listMissingBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listMissingBlobs'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blobs: RecordBlob[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface RecordBlob {\n  $type?: 'com.atproto.repo.listMissingBlobs#recordBlob'\n  cid: string\n  recordUri: string\n}\n\nconst hashRecordBlob = 'recordBlob'\n\nexport function isRecordBlob<V>(v: V) {\n  return is$typed(v, id, hashRecordBlob)\n}\n\nexport function validateRecordBlob<V>(v: V) {\n  return validate<RecordBlob & V>(v, id, hashRecordBlob)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/listRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listRecords'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record type. */\n  collection: string\n  /** The number of records to return. */\n  limit: number\n  cursor?: string\n  /** Flag to reverse the order of the returned records. */\n  reverse?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  records: Record[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Record {\n  $type?: 'com.atproto.repo.listRecords#record'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashRecord = 'record'\n\nexport function isRecord<V>(v: V) {\n  return is$typed(v, id, hashRecord)\n}\n\nexport function validateRecord<V>(v: V) {\n  return validate<Record & V>(v, id, hashRecord)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/putRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.putRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record to write. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */\n  swapRecord?: string | null\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/strongRef.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.strongRef'\n\nexport interface Main {\n  $type?: 'com.atproto.repo.strongRef'\n  uri: string\n  cid: string\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/repo/uploadBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.uploadBlob'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  blob: BlobRef\n}\n\nexport interface HandlerInput {\n  encoding: '*/*'\n  body: stream.Readable\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/activateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.activateAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/checkAccountStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.checkAccountStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  validDid: boolean\n  repoCommit: string\n  repoRev: string\n  repoBlocks: number\n  indexedRecords: number\n  privateStateValues: number\n  expectedBlobs: number\n  importedBlobs: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/confirmEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.confirmEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/createAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email?: string\n  /** Requested handle for the account. */\n  handle: string\n  /** Pre-existing atproto DID, being imported to a new account. */\n  did?: string\n  inviteCode?: string\n  verificationCode?: string\n  verificationPhone?: string\n  /** Initial account password. May need to meet instance-specific password strength requirements. */\n  password?: string\n  /** DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. */\n  recoveryKey?: string\n  /** A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. */\n  plcOp?: { [_ in string]: unknown }\n}\n\n/** Account login session returned on successful account creation. */\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  /** The DID of the new account. */\n  did: string\n  /** Complete DID document. */\n  didDoc?: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidHandle'\n    | 'InvalidPassword'\n    | 'InvalidInviteCode'\n    | 'HandleNotAvailable'\n    | 'UnsupportedDomain'\n    | 'UnresolvableDid'\n    | 'IncompatibleDidDoc'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/createAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A short name for the App Password, to help distinguish them. */\n  name: string\n  /** If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients. */\n  privileged?: boolean\n}\n\nexport type OutputSchema = AppPassword\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.createAppPassword#appPassword'\n  name: string\n  password: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/createInviteCode.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCode'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  useCount: number\n  forAccount?: string\n}\n\nexport interface OutputSchema {\n  code: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/createInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codeCount: number\n  useCount: number\n  forAccounts?: string[]\n}\n\nexport interface OutputSchema {\n  codes: AccountCodes[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AccountCodes {\n  $type?: 'com.atproto.server.createInviteCodes#accountCodes'\n  account: string\n  codes: string[]\n}\n\nconst hashAccountCodes = 'accountCodes'\n\nexport function isAccountCodes<V>(v: V) {\n  return is$typed(v, id, hashAccountCodes)\n}\n\nexport function validateAccountCodes<V>(v: V) {\n  return validate<AccountCodes & V>(v, id, hashAccountCodes)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createSession'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Handle or other identifier supported by the server for the authenticating user. */\n  identifier: string\n  password: string\n  authFactorToken?: string\n  /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */\n  allowTakendown?: boolean\n}\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown' | 'AuthFactorTokenRequired'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/deactivateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deactivateAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A recommendation to server as to how long they should hold onto the deactivated account before deleting. */\n  deleteAfter?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.defs'\n\nexport interface InviteCode {\n  $type?: 'com.atproto.server.defs#inviteCode'\n  code: string\n  available: number\n  disabled: boolean\n  forAccount: string\n  createdBy: string\n  createdAt: string\n  uses: InviteCodeUse[]\n}\n\nconst hashInviteCode = 'inviteCode'\n\nexport function isInviteCode<V>(v: V) {\n  return is$typed(v, id, hashInviteCode)\n}\n\nexport function validateInviteCode<V>(v: V) {\n  return validate<InviteCode & V>(v, id, hashInviteCode)\n}\n\nexport interface InviteCodeUse {\n  $type?: 'com.atproto.server.defs#inviteCodeUse'\n  usedBy: string\n  usedAt: string\n}\n\nconst hashInviteCodeUse = 'inviteCodeUse'\n\nexport function isInviteCodeUse<V>(v: V) {\n  return is$typed(v, id, hashInviteCodeUse)\n}\n\nexport function validateInviteCodeUse<V>(v: V) {\n  return validate<InviteCodeUse & V>(v, id, hashInviteCodeUse)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/deleteSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidToken' | 'ExpiredToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/describeServer.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.describeServer'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** If true, an invite code must be supplied to create an account on this instance. */\n  inviteCodeRequired?: boolean\n  /** If true, a phone verification token must be supplied to create an account on this instance. */\n  phoneVerificationRequired?: boolean\n  /** List of domain suffixes that can be used in account handles. */\n  availableUserDomains: string[]\n  links?: Links\n  contact?: Contact\n  did: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Links {\n  $type?: 'com.atproto.server.describeServer#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n\nexport interface Contact {\n  $type?: 'com.atproto.server.describeServer#contact'\n  email?: string\n}\n\nconst hashContact = 'contact'\n\nexport function isContact<V>(v: V) {\n  return is$typed(v, id, hashContact)\n}\n\nexport function validateContact<V>(v: V) {\n  return validate<Contact & V>(v, id, hashContact)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getAccountInviteCodes'\n\nexport type QueryParams = {\n  includeUsed: boolean\n  /** Controls whether any new 'earned' but not 'created' invites should be created. */\n  createAvailable: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateCreate'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/getServiceAuth.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getServiceAuth'\n\nexport type QueryParams = {\n  /** The DID of the service that the token will be used to authenticate with */\n  aud: string\n  /** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */\n  exp?: number\n  /** Lexicon (XRPC) method to bind the requested token to */\n  lxm?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  token: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadExpiration'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/getSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/listAppPasswords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.listAppPasswords'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  passwords: AppPassword[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.listAppPasswords#appPassword'\n  name: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/refreshSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.refreshSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** Hosting status of the account. If not specified, then assume 'active'. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown' | 'InvalidToken' | 'ExpiredToken'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/requestAccountDelete.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestAccountDelete'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailConfirmation'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailUpdate'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  tokenRequired: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/requestPasswordReset.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestPasswordReset'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/reserveSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.reserveSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID to reserve a key for. */\n  did?: string\n}\n\nexport interface OutputSchema {\n  /** The public key for the reserved signing key, in did:key serialization. */\n  signingKey: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/resetPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.resetPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  token: string\n  password: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/revokeAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.revokeAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  name: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/server/updateEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.updateEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  emailAuthFactor?: boolean\n  /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */\n  token?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.defs'\n\nexport type HostStatus =\n  | 'active'\n  | 'idle'\n  | 'offline'\n  | 'throttled'\n  | 'banned'\n  | (string & {})\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlob'\n\nexport type QueryParams = {\n  /** The DID of the account. */\n  did: string\n  /** The CID of the blob to fetch */\n  cid: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: '*/*'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'BlobNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlocks'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  cids: string[]\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'BlockNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getCheckout.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getCheckout'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getHead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHead'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  root: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HeadNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getHostStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHostStatus'\n\nexport type QueryParams = {\n  /** Hostname of the host (eg, PDS or relay) being queried. */\n  hostname: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  /** Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts. */\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HostNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getLatestCommit.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getLatestCommit'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cid: string\n  rev: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRecord'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  collection: string\n  /** Record Key */\n  rkey: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'RecordNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepo'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** The revision ('rev') of the repo to create a diff from. */\n  since?: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/getRepoStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepoStatus'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  active: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n  /** Optional field, the current rev of the repo, if active=true */\n  rev?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/listBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listBlobs'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** Optional revision of the repo to list blobs since. */\n  since?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  cids: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/listHosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listHosts'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first. */\n  hosts: Host[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Host {\n  $type?: 'com.atproto.sync.listHosts#host'\n  /** hostname of server; not a URL (no scheme) */\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nconst hashHost = 'host'\n\nexport function isHost<V>(v: V) {\n  return is$typed(v, id, hashHost)\n}\n\nexport function validateHost<V>(v: V) {\n  return validate<Host & V>(v, id, hashHost)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/listRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listRepos'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listRepos#repo'\n  did: string\n  /** Current repo commit CID */\n  head: string\n  rev: string\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/listReposByCollection.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listReposByCollection'\n\nexport type QueryParams = {\n  collection: string\n  /** Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists. */\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listReposByCollection#repo'\n  did: string\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.notifyOfUpdate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (usually a PDS) that is notifying of update. */\n  hostname: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/requestCrawl.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.requestCrawl'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (eg, PDS) that is requesting to be crawled. */\n  hostname: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HostBanned'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/sync/subscribeRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport { ErrorFrame } from '@atproto/xrpc-server'\nimport { IncomingMessage } from 'node:http'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.subscribeRepos'\n\nexport type QueryParams = {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\nexport type OutputSchema =\n  | $Typed<Commit>\n  | $Typed<Sync>\n  | $Typed<Identity>\n  | $Typed<Account>\n  | $Typed<Info>\n  | { $type: string }\nexport type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'>\nexport type HandlerOutput = HandlerError | OutputSchema\n\n/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */\nexport interface Commit {\n  $type?: 'com.atproto.sync.subscribeRepos#commit'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** DEPRECATED -- unused */\n  rebase: boolean\n  /** DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */\n  tooBig: boolean\n  /** The repo this event comes from. Note that all other message types name this field 'did'. */\n  repo: string\n  /** Repo commit object CID. */\n  commit: CID\n  /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */\n  rev: string\n  /** The rev of the last emitted commit from this repo (if any). */\n  since: string | null\n  /** CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list. */\n  blocks: Uint8Array\n  ops: RepoOp[]\n  blobs: CID[]\n  /** The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose. */\n  prevData?: CID\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashCommit = 'commit'\n\nexport function isCommit<V>(v: V) {\n  return is$typed(v, id, hashCommit)\n}\n\nexport function validateCommit<V>(v: V) {\n  return validate<Commit & V>(v, id, hashCommit)\n}\n\n/** Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository. */\nexport interface Sync {\n  $type?: 'com.atproto.sync.subscribeRepos#sync'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** The account this repo event corresponds to. Must match that in the commit object. */\n  did: string\n  /** CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'. */\n  blocks: Uint8Array\n  /** The rev of the commit. This value must match that in the commit object. */\n  rev: string\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashSync = 'sync'\n\nexport function isSync<V>(v: V) {\n  return is$typed(v, id, hashSync)\n}\n\nexport function validateSync<V>(v: V) {\n  return validate<Sync & V>(v, id, hashSync)\n}\n\n/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */\nexport interface Identity {\n  $type?: 'com.atproto.sync.subscribeRepos#identity'\n  seq: number\n  did: string\n  time: string\n  /** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */\n  handle?: string\n}\n\nconst hashIdentity = 'identity'\n\nexport function isIdentity<V>(v: V) {\n  return is$typed(v, id, hashIdentity)\n}\n\nexport function validateIdentity<V>(v: V) {\n  return validate<Identity & V>(v, id, hashIdentity)\n}\n\n/** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */\nexport interface Account {\n  $type?: 'com.atproto.sync.subscribeRepos#account'\n  seq: number\n  did: string\n  time: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  /** If active=false, this optional field indicates a reason for why the account is not active. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashAccount = 'account'\n\nexport function isAccount<V>(v: V) {\n  return is$typed(v, id, hashAccount)\n}\n\nexport function validateAccount<V>(v: V) {\n  return validate<Account & V>(v, id, hashAccount)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.sync.subscribeRepos#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n\n/** A repo operation, ie a mutation of a single record. */\nexport interface RepoOp {\n  $type?: 'com.atproto.sync.subscribeRepos#repoOp'\n  action: 'create' | 'update' | 'delete' | (string & {})\n  path: string\n  /** For creates and updates, the new record CID. For deletions, null. */\n  cid: CID | null\n  /** For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined. */\n  prev?: CID\n}\n\nconst hashRepoOp = 'repoOp'\n\nexport function isRepoOp<V>(v: V) {\n  return is$typed(v, id, hashRepoOp)\n}\n\nexport function validateRepoOp<V>(v: V) {\n  return validate<RepoOp & V>(v, id, hashRepoOp)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/addReservedHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.addReservedHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  handle: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/checkHandleAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkHandleAvailability'\n\nexport type QueryParams = {\n  /** Tentative handle. Will be checked for availability or used to build handle suggestions. */\n  handle: string\n  /** User-provided email. Might be used to build handle suggestions. */\n  email?: string\n  /** User-provided birth date. Might be used to build handle suggestions. */\n  birthDate?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Echo of the input handle. */\n  handle: string\n  result:\n    | $Typed<ResultAvailable>\n    | $Typed<ResultUnavailable>\n    | { $type: string }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidEmail'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Indicates the provided handle is available. */\nexport interface ResultAvailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultAvailable'\n}\n\nconst hashResultAvailable = 'resultAvailable'\n\nexport function isResultAvailable<V>(v: V) {\n  return is$typed(v, id, hashResultAvailable)\n}\n\nexport function validateResultAvailable<V>(v: V) {\n  return validate<ResultAvailable & V>(v, id, hashResultAvailable)\n}\n\n/** Indicates the provided handle is unavailable and gives suggestions of available handles. */\nexport interface ResultUnavailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'\n  /** List of suggested handles based on the provided inputs. */\n  suggestions: Suggestion[]\n}\n\nconst hashResultUnavailable = 'resultUnavailable'\n\nexport function isResultUnavailable<V>(v: V) {\n  return is$typed(v, id, hashResultUnavailable)\n}\n\nexport function validateResultUnavailable<V>(v: V) {\n  return validate<ResultUnavailable & V>(v, id, hashResultUnavailable)\n}\n\nexport interface Suggestion {\n  $type?: 'com.atproto.temp.checkHandleAvailability#suggestion'\n  handle: string\n  /** Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics. */\n  method: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkSignupQueue'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  placeInQueue?: number\n  estimatedTimeMs?: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/dereferenceScope.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.dereferenceScope'\n\nexport type QueryParams = {\n  /** The scope reference (starts with 'ref:') */\n  scope: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The full oauth permission scope */\n  scope: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidScopeReference'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/fetchLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.fetchLabels'\n\nexport type QueryParams = {\n  since?: number\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.requestPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  phoneNumber: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/com/atproto/temp/revokeAccountCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.revokeAccountCredentials'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/communication/createTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.createTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the template. */\n  name: string\n  /** Content of the template, markdown supported, can contain variable placeholders. */\n  contentMarkdown: string\n  /** Subject of the message, used in emails. */\n  subject: string\n  /** Message language. */\n  lang?: string\n  /** DID of the user who is creating the template. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneCommunicationDefs.TemplateView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateTemplateName'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/communication/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.defs'\n\nexport interface TemplateView {\n  $type?: 'tools.ozone.communication.defs#templateView'\n  id: string\n  /** Name of the template. */\n  name: string\n  /** Content of the template, can contain markdown and variable placeholders. */\n  subject?: string\n  /** Subject of the message, used in emails. */\n  contentMarkdown: string\n  disabled: boolean\n  /** Message language. */\n  lang?: string\n  /** DID of the user who last updated the template. */\n  lastUpdatedBy: string\n  createdAt: string\n  updatedAt: string\n}\n\nconst hashTemplateView = 'templateView'\n\nexport function isTemplateView<V>(v: V) {\n  return is$typed(v, id, hashTemplateView)\n}\n\nexport function validateTemplateView<V>(v: V) {\n  return validate<TemplateView & V>(v, id, hashTemplateView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/communication/deleteTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.deleteTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/communication/listTemplates.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.listTemplates'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  communicationTemplates: ToolsOzoneCommunicationDefs.TemplateView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/communication/updateTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.updateTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** ID of the template to be updated. */\n  id: string\n  /** Name of the template. */\n  name?: string\n  /** Message language. */\n  lang?: string\n  /** Content of the template, markdown supported, can contain variable placeholders. */\n  contentMarkdown?: string\n  /** Subject of the message, used in emails. */\n  subject?: string\n  /** DID of the user who is updating the template. */\n  updatedBy?: string\n  disabled?: boolean\n}\n\nexport type OutputSchema = ToolsOzoneCommunicationDefs.TemplateView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateTemplateName'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/hosting/getAccountHistory.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.hosting.getAccountHistory'\n\nexport type QueryParams = {\n  did: string\n  events?:\n    | 'accountCreated'\n    | 'emailUpdated'\n    | 'emailConfirmed'\n    | 'passwordUpdated'\n    | 'handleUpdated'\n    | (string & {})[]\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  events: Event[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Event {\n  $type?: 'tools.ozone.hosting.getAccountHistory#event'\n  details:\n    | $Typed<AccountCreated>\n    | $Typed<EmailUpdated>\n    | $Typed<EmailConfirmed>\n    | $Typed<PasswordUpdated>\n    | $Typed<HandleUpdated>\n    | { $type: string }\n  createdBy: string\n  createdAt: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n\nexport interface AccountCreated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#accountCreated'\n  email?: string\n  handle?: string\n}\n\nconst hashAccountCreated = 'accountCreated'\n\nexport function isAccountCreated<V>(v: V) {\n  return is$typed(v, id, hashAccountCreated)\n}\n\nexport function validateAccountCreated<V>(v: V) {\n  return validate<AccountCreated & V>(v, id, hashAccountCreated)\n}\n\nexport interface EmailUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#emailUpdated'\n  email: string\n}\n\nconst hashEmailUpdated = 'emailUpdated'\n\nexport function isEmailUpdated<V>(v: V) {\n  return is$typed(v, id, hashEmailUpdated)\n}\n\nexport function validateEmailUpdated<V>(v: V) {\n  return validate<EmailUpdated & V>(v, id, hashEmailUpdated)\n}\n\nexport interface EmailConfirmed {\n  $type?: 'tools.ozone.hosting.getAccountHistory#emailConfirmed'\n  email: string\n}\n\nconst hashEmailConfirmed = 'emailConfirmed'\n\nexport function isEmailConfirmed<V>(v: V) {\n  return is$typed(v, id, hashEmailConfirmed)\n}\n\nexport function validateEmailConfirmed<V>(v: V) {\n  return validate<EmailConfirmed & V>(v, id, hashEmailConfirmed)\n}\n\nexport interface PasswordUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#passwordUpdated'\n}\n\nconst hashPasswordUpdated = 'passwordUpdated'\n\nexport function isPasswordUpdated<V>(v: V) {\n  return is$typed(v, id, hashPasswordUpdated)\n}\n\nexport function validatePasswordUpdated<V>(v: V) {\n  return validate<PasswordUpdated & V>(v, id, hashPasswordUpdated)\n}\n\nexport interface HandleUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#handleUpdated'\n  handle: string\n}\n\nconst hashHandleUpdated = 'handleUpdated'\n\nexport function isHandleUpdated<V>(v: V) {\n  return is$typed(v, id, hashHandleUpdated)\n}\n\nexport function validateHandleUpdated<V>(v: V) {\n  return validate<HandleUpdated & V>(v, id, hashHandleUpdated)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/cancelScheduledActions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.cancelScheduledActions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of DID subjects to cancel scheduled actions for */\n  subjects: string[]\n  /** Optional comment describing the reason for cancellation */\n  comment?: string\n}\n\nexport type OutputSchema = CancellationResults\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface CancellationResults {\n  $type?: 'tools.ozone.moderation.cancelScheduledActions#cancellationResults'\n  /** DIDs for which all pending scheduled actions were successfully cancelled */\n  succeeded: string[]\n  /** DIDs for which cancellation failed with error details */\n  failed: FailedCancellation[]\n}\n\nconst hashCancellationResults = 'cancellationResults'\n\nexport function isCancellationResults<V>(v: V) {\n  return is$typed(v, id, hashCancellationResults)\n}\n\nexport function validateCancellationResults<V>(v: V) {\n  return validate<CancellationResults & V>(v, id, hashCancellationResults)\n}\n\nexport interface FailedCancellation {\n  $type?: 'tools.ozone.moderation.cancelScheduledActions#failedCancellation'\n  did: string\n  error: string\n  errorCode?: string\n}\n\nconst hashFailedCancellation = 'failedCancellation'\n\nexport function isFailedCancellation<V>(v: V) {\n  return is$typed(v, id, hashFailedCancellation)\n}\n\nexport function validateFailedCancellation<V>(v: V) {\n  return validate<FailedCancellation & V>(v, id, hashFailedCancellation)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as ChatBskyConvoDefs from '../../../chat/bsky/convo/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\nimport type * as AppBskyAgeassuranceDefs from '../../../app/bsky/ageassurance/defs.js'\nimport type * as ComAtprotoServerDefs from '../../../com/atproto/server/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.defs'\n\nexport interface ModEventView {\n  $type?: 'tools.ozone.moderation.defs#modEventView'\n  id: number\n  event:\n    | $Typed<ModEventTakedown>\n    | $Typed<ModEventReverseTakedown>\n    | $Typed<ModEventComment>\n    | $Typed<ModEventReport>\n    | $Typed<ModEventLabel>\n    | $Typed<ModEventAcknowledge>\n    | $Typed<ModEventEscalate>\n    | $Typed<ModEventMute>\n    | $Typed<ModEventUnmute>\n    | $Typed<ModEventMuteReporter>\n    | $Typed<ModEventUnmuteReporter>\n    | $Typed<ModEventEmail>\n    | $Typed<ModEventResolveAppeal>\n    | $Typed<ModEventDivert>\n    | $Typed<ModEventTag>\n    | $Typed<AccountEvent>\n    | $Typed<IdentityEvent>\n    | $Typed<RecordEvent>\n    | $Typed<ModEventPriorityScore>\n    | $Typed<AgeAssuranceEvent>\n    | $Typed<AgeAssuranceOverrideEvent>\n    | $Typed<AgeAssurancePurgeEvent>\n    | $Typed<RevokeAccountCredentialsEvent>\n    | $Typed<ScheduleTakedownEvent>\n    | $Typed<CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ChatBskyConvoDefs.MessageRef>\n    | { $type: string }\n  subjectBlobCids: string[]\n  createdBy: string\n  createdAt: string\n  creatorHandle?: string\n  subjectHandle?: string\n  modTool?: ModTool\n}\n\nconst hashModEventView = 'modEventView'\n\nexport function isModEventView<V>(v: V) {\n  return is$typed(v, id, hashModEventView)\n}\n\nexport function validateModEventView<V>(v: V) {\n  return validate<ModEventView & V>(v, id, hashModEventView)\n}\n\nexport interface ModEventViewDetail {\n  $type?: 'tools.ozone.moderation.defs#modEventViewDetail'\n  id: number\n  event:\n    | $Typed<ModEventTakedown>\n    | $Typed<ModEventReverseTakedown>\n    | $Typed<ModEventComment>\n    | $Typed<ModEventReport>\n    | $Typed<ModEventLabel>\n    | $Typed<ModEventAcknowledge>\n    | $Typed<ModEventEscalate>\n    | $Typed<ModEventMute>\n    | $Typed<ModEventUnmute>\n    | $Typed<ModEventMuteReporter>\n    | $Typed<ModEventUnmuteReporter>\n    | $Typed<ModEventEmail>\n    | $Typed<ModEventResolveAppeal>\n    | $Typed<ModEventDivert>\n    | $Typed<ModEventTag>\n    | $Typed<AccountEvent>\n    | $Typed<IdentityEvent>\n    | $Typed<RecordEvent>\n    | $Typed<ModEventPriorityScore>\n    | $Typed<AgeAssuranceEvent>\n    | $Typed<AgeAssuranceOverrideEvent>\n    | $Typed<AgeAssurancePurgeEvent>\n    | $Typed<RevokeAccountCredentialsEvent>\n    | $Typed<ScheduleTakedownEvent>\n    | $Typed<CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<RepoView>\n    | $Typed<RepoViewNotFound>\n    | $Typed<RecordView>\n    | $Typed<RecordViewNotFound>\n    | { $type: string }\n  subjectBlobs: BlobView[]\n  createdBy: string\n  createdAt: string\n  modTool?: ModTool\n}\n\nconst hashModEventViewDetail = 'modEventViewDetail'\n\nexport function isModEventViewDetail<V>(v: V) {\n  return is$typed(v, id, hashModEventViewDetail)\n}\n\nexport function validateModEventViewDetail<V>(v: V) {\n  return validate<ModEventViewDetail & V>(v, id, hashModEventViewDetail)\n}\n\nexport interface SubjectStatusView {\n  $type?: 'tools.ozone.moderation.defs#subjectStatusView'\n  id: number\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ChatBskyConvoDefs.MessageRef>\n    | { $type: string }\n  hosting?: $Typed<AccountHosting> | $Typed<RecordHosting> | { $type: string }\n  subjectBlobCids?: string[]\n  subjectRepoHandle?: string\n  /** Timestamp referencing when the last update was made to the moderation status of the subject */\n  updatedAt: string\n  /** Timestamp referencing the first moderation status impacting event was emitted on the subject */\n  createdAt: string\n  reviewState: SubjectReviewState\n  /** Sticky comment on the subject. */\n  comment?: string\n  /** Numeric value representing the level of priority. Higher score means higher priority. */\n  priorityScore?: number\n  muteUntil?: string\n  muteReportingUntil?: string\n  lastReviewedBy?: string\n  lastReviewedAt?: string\n  lastReportedAt?: string\n  /** Timestamp referencing when the author of the subject appealed a moderation action */\n  lastAppealedAt?: string\n  takendown?: boolean\n  /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */\n  appealed?: boolean\n  suspendUntil?: string\n  tags?: string[]\n  accountStats?: AccountStats\n  recordsStats?: RecordsStats\n  accountStrike?: AccountStrike\n  /** Current age assurance state of the subject. */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n  /** Whether or not the last successful update to age assurance was made by the user or admin. */\n  ageAssuranceUpdatedBy?: 'admin' | 'user' | (string & {})\n}\n\nconst hashSubjectStatusView = 'subjectStatusView'\n\nexport function isSubjectStatusView<V>(v: V) {\n  return is$typed(v, id, hashSubjectStatusView)\n}\n\nexport function validateSubjectStatusView<V>(v: V) {\n  return validate<SubjectStatusView & V>(v, id, hashSubjectStatusView)\n}\n\n/** Detailed view of a subject. For record subjects, the author's repo and profile will be returned. */\nexport interface SubjectView {\n  $type?: 'tools.ozone.moderation.defs#subjectView'\n  type: ComAtprotoModerationDefs.SubjectType\n  subject: string\n  status?: SubjectStatusView\n  repo?: RepoViewDetail\n  profile?: { $type: string }\n  record?: RecordViewDetail\n}\n\nconst hashSubjectView = 'subjectView'\n\nexport function isSubjectView<V>(v: V) {\n  return is$typed(v, id, hashSubjectView)\n}\n\nexport function validateSubjectView<V>(v: V) {\n  return validate<SubjectView & V>(v, id, hashSubjectView)\n}\n\n/** Statistics about a particular account subject */\nexport interface AccountStats {\n  $type?: 'tools.ozone.moderation.defs#accountStats'\n  /** Total number of reports on the account */\n  reportCount?: number\n  /** Total number of appeals against a moderation action on the account */\n  appealCount?: number\n  /** Number of times the account was suspended */\n  suspendCount?: number\n  /** Number of times the account was escalated */\n  escalateCount?: number\n  /** Number of times the account was taken down */\n  takedownCount?: number\n}\n\nconst hashAccountStats = 'accountStats'\n\nexport function isAccountStats<V>(v: V) {\n  return is$typed(v, id, hashAccountStats)\n}\n\nexport function validateAccountStats<V>(v: V) {\n  return validate<AccountStats & V>(v, id, hashAccountStats)\n}\n\n/** Statistics about a set of record subject items */\nexport interface RecordsStats {\n  $type?: 'tools.ozone.moderation.defs#recordsStats'\n  /** Cumulative sum of the number of reports on the items in the set */\n  totalReports?: number\n  /** Number of items that were reported at least once */\n  reportedCount?: number\n  /** Number of items that were escalated at least once */\n  escalatedCount?: number\n  /** Number of items that were appealed at least once */\n  appealedCount?: number\n  /** Total number of item in the set */\n  subjectCount?: number\n  /** Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state */\n  pendingCount?: number\n  /** Number of item currently in \"reviewNone\" or \"reviewClosed\" state */\n  processedCount?: number\n  /** Number of item currently taken down */\n  takendownCount?: number\n}\n\nconst hashRecordsStats = 'recordsStats'\n\nexport function isRecordsStats<V>(v: V) {\n  return is$typed(v, id, hashRecordsStats)\n}\n\nexport function validateRecordsStats<V>(v: V) {\n  return validate<RecordsStats & V>(v, id, hashRecordsStats)\n}\n\n/** Strike information for an account */\nexport interface AccountStrike {\n  $type?: 'tools.ozone.moderation.defs#accountStrike'\n  /** Current number of active strikes (excluding expired strikes) */\n  activeStrikeCount?: number\n  /** Total number of strikes ever received (including expired strikes) */\n  totalStrikeCount?: number\n  /** Timestamp of the first strike received */\n  firstStrikeAt?: string\n  /** Timestamp of the most recent strike received */\n  lastStrikeAt?: string\n}\n\nconst hashAccountStrike = 'accountStrike'\n\nexport function isAccountStrike<V>(v: V) {\n  return is$typed(v, id, hashAccountStrike)\n}\n\nexport function validateAccountStrike<V>(v: V) {\n  return validate<AccountStrike & V>(v, id, hashAccountStrike)\n}\n\nexport type SubjectReviewState =\n  | 'tools.ozone.moderation.defs#reviewOpen'\n  | 'tools.ozone.moderation.defs#reviewEscalated'\n  | 'tools.ozone.moderation.defs#reviewClosed'\n  | 'tools.ozone.moderation.defs#reviewNone'\n  | (string & {})\n\n/** Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator */\nexport const REVIEWOPEN = `${id}#reviewOpen`\n/** Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator */\nexport const REVIEWESCALATED = `${id}#reviewEscalated`\n/** Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator */\nexport const REVIEWCLOSED = `${id}#reviewClosed`\n/** Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it */\nexport const REVIEWNONE = `${id}#reviewNone`\n\n/** Take down a subject permanently or temporarily */\nexport interface ModEventTakedown {\n  $type?: 'tools.ozone.moderation.defs#modEventTakedown'\n  comment?: string\n  /** Indicates how long the takedown should be in effect before automatically expiring. */\n  durationInHours?: number\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n  /** Names/Keywords of the policies that drove the decision. */\n  policies?: string[]\n  /** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */\n  severityLevel?: string\n  /** List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services. */\n  targetServices?: ('appview' | 'pds' | (string & {}))[]\n  /** Number of strikes to assign to the user for this violation. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n}\n\nconst hashModEventTakedown = 'modEventTakedown'\n\nexport function isModEventTakedown<V>(v: V) {\n  return is$typed(v, id, hashModEventTakedown)\n}\n\nexport function validateModEventTakedown<V>(v: V) {\n  return validate<ModEventTakedown & V>(v, id, hashModEventTakedown)\n}\n\n/** Revert take down action on a subject */\nexport interface ModEventReverseTakedown {\n  $type?: 'tools.ozone.moderation.defs#modEventReverseTakedown'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n  /** Names/Keywords of the policy infraction for which takedown is being reversed. */\n  policies?: string[]\n  /** Severity level of the violation. Usually set from the last policy infraction's severity. */\n  severityLevel?: string\n  /** Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity. */\n  strikeCount?: number\n}\n\nconst hashModEventReverseTakedown = 'modEventReverseTakedown'\n\nexport function isModEventReverseTakedown<V>(v: V) {\n  return is$typed(v, id, hashModEventReverseTakedown)\n}\n\nexport function validateModEventReverseTakedown<V>(v: V) {\n  return validate<ModEventReverseTakedown & V>(\n    v,\n    id,\n    hashModEventReverseTakedown,\n  )\n}\n\n/** Resolve appeal on a subject */\nexport interface ModEventResolveAppeal {\n  $type?: 'tools.ozone.moderation.defs#modEventResolveAppeal'\n  /** Describe resolution. */\n  comment?: string\n}\n\nconst hashModEventResolveAppeal = 'modEventResolveAppeal'\n\nexport function isModEventResolveAppeal<V>(v: V) {\n  return is$typed(v, id, hashModEventResolveAppeal)\n}\n\nexport function validateModEventResolveAppeal<V>(v: V) {\n  return validate<ModEventResolveAppeal & V>(v, id, hashModEventResolveAppeal)\n}\n\n/** Add a comment to a subject. An empty comment will clear any previously set sticky comment. */\nexport interface ModEventComment {\n  $type?: 'tools.ozone.moderation.defs#modEventComment'\n  comment?: string\n  /** Make the comment persistent on the subject */\n  sticky?: boolean\n}\n\nconst hashModEventComment = 'modEventComment'\n\nexport function isModEventComment<V>(v: V) {\n  return is$typed(v, id, hashModEventComment)\n}\n\nexport function validateModEventComment<V>(v: V) {\n  return validate<ModEventComment & V>(v, id, hashModEventComment)\n}\n\n/** Report a subject */\nexport interface ModEventReport {\n  $type?: 'tools.ozone.moderation.defs#modEventReport'\n  comment?: string\n  /** Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject. */\n  isReporterMuted?: boolean\n  reportType: ComAtprotoModerationDefs.ReasonType\n}\n\nconst hashModEventReport = 'modEventReport'\n\nexport function isModEventReport<V>(v: V) {\n  return is$typed(v, id, hashModEventReport)\n}\n\nexport function validateModEventReport<V>(v: V) {\n  return validate<ModEventReport & V>(v, id, hashModEventReport)\n}\n\n/** Apply/Negate labels on a subject */\nexport interface ModEventLabel {\n  $type?: 'tools.ozone.moderation.defs#modEventLabel'\n  comment?: string\n  createLabelVals: string[]\n  negateLabelVals: string[]\n  /** Indicates how long the label will remain on the subject. Only applies on labels that are being added. */\n  durationInHours?: number\n}\n\nconst hashModEventLabel = 'modEventLabel'\n\nexport function isModEventLabel<V>(v: V) {\n  return is$typed(v, id, hashModEventLabel)\n}\n\nexport function validateModEventLabel<V>(v: V) {\n  return validate<ModEventLabel & V>(v, id, hashModEventLabel)\n}\n\n/** Set priority score of the subject. Higher score means higher priority. */\nexport interface ModEventPriorityScore {\n  $type?: 'tools.ozone.moderation.defs#modEventPriorityScore'\n  comment?: string\n  score: number\n}\n\nconst hashModEventPriorityScore = 'modEventPriorityScore'\n\nexport function isModEventPriorityScore<V>(v: V) {\n  return is$typed(v, id, hashModEventPriorityScore)\n}\n\nexport function validateModEventPriorityScore<V>(v: V) {\n  return validate<ModEventPriorityScore & V>(v, id, hashModEventPriorityScore)\n}\n\n/** Age assurance info coming directly from users. Only works on DID subjects. */\nexport interface AgeAssuranceEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  access?: AppBskyAgeassuranceDefs.Access\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode?: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n\n/** Age assurance status override by moderators. Only works on DID subjects. */\nexport interface AgeAssuranceOverrideEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n  /** The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state. */\n  status: 'assured' | 'reset' | 'blocked' | (string & {})\n  access?: AppBskyAgeassuranceDefs.Access\n  /** Comment describing the reason for the override. */\n  comment: string\n}\n\nconst hashAgeAssuranceOverrideEvent = 'ageAssuranceOverrideEvent'\n\nexport function isAgeAssuranceOverrideEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceOverrideEvent)\n}\n\nexport function validateAgeAssuranceOverrideEvent<V>(v: V) {\n  return validate<AgeAssuranceOverrideEvent & V>(\n    v,\n    id,\n    hashAgeAssuranceOverrideEvent,\n  )\n}\n\n/** Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only. */\nexport interface AgeAssurancePurgeEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssurancePurgeEvent'\n  /** Comment describing the reason for the purge. */\n  comment: string\n}\n\nconst hashAgeAssurancePurgeEvent = 'ageAssurancePurgeEvent'\n\nexport function isAgeAssurancePurgeEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssurancePurgeEvent)\n}\n\nexport function validateAgeAssurancePurgeEvent<V>(v: V) {\n  return validate<AgeAssurancePurgeEvent & V>(v, id, hashAgeAssurancePurgeEvent)\n}\n\n/** Account credentials revocation by moderators. Only works on DID subjects. */\nexport interface RevokeAccountCredentialsEvent {\n  $type?: 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n  /** Comment describing the reason for the revocation. */\n  comment: string\n}\n\nconst hashRevokeAccountCredentialsEvent = 'revokeAccountCredentialsEvent'\n\nexport function isRevokeAccountCredentialsEvent<V>(v: V) {\n  return is$typed(v, id, hashRevokeAccountCredentialsEvent)\n}\n\nexport function validateRevokeAccountCredentialsEvent<V>(v: V) {\n  return validate<RevokeAccountCredentialsEvent & V>(\n    v,\n    id,\n    hashRevokeAccountCredentialsEvent,\n  )\n}\n\nexport interface ModEventAcknowledge {\n  $type?: 'tools.ozone.moderation.defs#modEventAcknowledge'\n  comment?: string\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n}\n\nconst hashModEventAcknowledge = 'modEventAcknowledge'\n\nexport function isModEventAcknowledge<V>(v: V) {\n  return is$typed(v, id, hashModEventAcknowledge)\n}\n\nexport function validateModEventAcknowledge<V>(v: V) {\n  return validate<ModEventAcknowledge & V>(v, id, hashModEventAcknowledge)\n}\n\nexport interface ModEventEscalate {\n  $type?: 'tools.ozone.moderation.defs#modEventEscalate'\n  comment?: string\n}\n\nconst hashModEventEscalate = 'modEventEscalate'\n\nexport function isModEventEscalate<V>(v: V) {\n  return is$typed(v, id, hashModEventEscalate)\n}\n\nexport function validateModEventEscalate<V>(v: V) {\n  return validate<ModEventEscalate & V>(v, id, hashModEventEscalate)\n}\n\n/** Mute incoming reports on a subject */\nexport interface ModEventMute {\n  $type?: 'tools.ozone.moderation.defs#modEventMute'\n  comment?: string\n  /** Indicates how long the subject should remain muted. */\n  durationInHours: number\n}\n\nconst hashModEventMute = 'modEventMute'\n\nexport function isModEventMute<V>(v: V) {\n  return is$typed(v, id, hashModEventMute)\n}\n\nexport function validateModEventMute<V>(v: V) {\n  return validate<ModEventMute & V>(v, id, hashModEventMute)\n}\n\n/** Unmute action on a subject */\nexport interface ModEventUnmute {\n  $type?: 'tools.ozone.moderation.defs#modEventUnmute'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n}\n\nconst hashModEventUnmute = 'modEventUnmute'\n\nexport function isModEventUnmute<V>(v: V) {\n  return is$typed(v, id, hashModEventUnmute)\n}\n\nexport function validateModEventUnmute<V>(v: V) {\n  return validate<ModEventUnmute & V>(v, id, hashModEventUnmute)\n}\n\n/** Mute incoming reports from an account */\nexport interface ModEventMuteReporter {\n  $type?: 'tools.ozone.moderation.defs#modEventMuteReporter'\n  comment?: string\n  /** Indicates how long the account should remain muted. Falsy value here means a permanent mute. */\n  durationInHours?: number\n}\n\nconst hashModEventMuteReporter = 'modEventMuteReporter'\n\nexport function isModEventMuteReporter<V>(v: V) {\n  return is$typed(v, id, hashModEventMuteReporter)\n}\n\nexport function validateModEventMuteReporter<V>(v: V) {\n  return validate<ModEventMuteReporter & V>(v, id, hashModEventMuteReporter)\n}\n\n/** Unmute incoming reports from an account */\nexport interface ModEventUnmuteReporter {\n  $type?: 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n}\n\nconst hashModEventUnmuteReporter = 'modEventUnmuteReporter'\n\nexport function isModEventUnmuteReporter<V>(v: V) {\n  return is$typed(v, id, hashModEventUnmuteReporter)\n}\n\nexport function validateModEventUnmuteReporter<V>(v: V) {\n  return validate<ModEventUnmuteReporter & V>(v, id, hashModEventUnmuteReporter)\n}\n\n/** Keep a log of outgoing email to a user */\nexport interface ModEventEmail {\n  $type?: 'tools.ozone.moderation.defs#modEventEmail'\n  /** The subject line of the email sent to the user. */\n  subjectLine: string\n  /** The content of the email sent to the user. */\n  content?: string\n  /** Additional comment about the outgoing comm. */\n  comment?: string\n  /** Names/Keywords of the policies that necessitated the email. */\n  policies?: string[]\n  /** Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense */\n  severityLevel?: string\n  /** Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n  /** Indicates whether the email was successfully delivered to the user's inbox. */\n  isDelivered?: boolean\n}\n\nconst hashModEventEmail = 'modEventEmail'\n\nexport function isModEventEmail<V>(v: V) {\n  return is$typed(v, id, hashModEventEmail)\n}\n\nexport function validateModEventEmail<V>(v: V) {\n  return validate<ModEventEmail & V>(v, id, hashModEventEmail)\n}\n\n/** Divert a record's blobs to a 3rd party service for further scanning/tagging */\nexport interface ModEventDivert {\n  $type?: 'tools.ozone.moderation.defs#modEventDivert'\n  comment?: string\n}\n\nconst hashModEventDivert = 'modEventDivert'\n\nexport function isModEventDivert<V>(v: V) {\n  return is$typed(v, id, hashModEventDivert)\n}\n\nexport function validateModEventDivert<V>(v: V) {\n  return validate<ModEventDivert & V>(v, id, hashModEventDivert)\n}\n\n/** Add/Remove a tag on a subject */\nexport interface ModEventTag {\n  $type?: 'tools.ozone.moderation.defs#modEventTag'\n  /** Tags to be added to the subject. If already exists, won't be duplicated. */\n  add: string[]\n  /** Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. */\n  remove: string[]\n  /** Additional comment about added/removed tags. */\n  comment?: string\n}\n\nconst hashModEventTag = 'modEventTag'\n\nexport function isModEventTag<V>(v: V) {\n  return is$typed(v, id, hashModEventTag)\n}\n\nexport function validateModEventTag<V>(v: V) {\n  return validate<ModEventTag & V>(v, id, hashModEventTag)\n}\n\n/** Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface AccountEvent {\n  $type?: 'tools.ozone.moderation.defs#accountEvent'\n  comment?: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  status?:\n    | 'unknown'\n    | 'deactivated'\n    | 'deleted'\n    | 'takendown'\n    | 'suspended'\n    | 'tombstoned'\n    | (string & {})\n  timestamp: string\n}\n\nconst hashAccountEvent = 'accountEvent'\n\nexport function isAccountEvent<V>(v: V) {\n  return is$typed(v, id, hashAccountEvent)\n}\n\nexport function validateAccountEvent<V>(v: V) {\n  return validate<AccountEvent & V>(v, id, hashAccountEvent)\n}\n\n/** Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface IdentityEvent {\n  $type?: 'tools.ozone.moderation.defs#identityEvent'\n  comment?: string\n  handle?: string\n  pdsHost?: string\n  tombstone?: boolean\n  timestamp: string\n}\n\nconst hashIdentityEvent = 'identityEvent'\n\nexport function isIdentityEvent<V>(v: V) {\n  return is$typed(v, id, hashIdentityEvent)\n}\n\nexport function validateIdentityEvent<V>(v: V) {\n  return validate<IdentityEvent & V>(v, id, hashIdentityEvent)\n}\n\n/** Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface RecordEvent {\n  $type?: 'tools.ozone.moderation.defs#recordEvent'\n  comment?: string\n  op: 'create' | 'update' | 'delete' | (string & {})\n  cid?: string\n  timestamp: string\n}\n\nconst hashRecordEvent = 'recordEvent'\n\nexport function isRecordEvent<V>(v: V) {\n  return is$typed(v, id, hashRecordEvent)\n}\n\nexport function validateRecordEvent<V>(v: V) {\n  return validate<RecordEvent & V>(v, id, hashRecordEvent)\n}\n\n/** Logs a scheduled takedown action for an account. */\nexport interface ScheduleTakedownEvent {\n  $type?: 'tools.ozone.moderation.defs#scheduleTakedownEvent'\n  comment?: string\n  executeAt?: string\n  executeAfter?: string\n  executeUntil?: string\n}\n\nconst hashScheduleTakedownEvent = 'scheduleTakedownEvent'\n\nexport function isScheduleTakedownEvent<V>(v: V) {\n  return is$typed(v, id, hashScheduleTakedownEvent)\n}\n\nexport function validateScheduleTakedownEvent<V>(v: V) {\n  return validate<ScheduleTakedownEvent & V>(v, id, hashScheduleTakedownEvent)\n}\n\n/** Logs cancellation of a scheduled takedown action for an account. */\nexport interface CancelScheduledTakedownEvent {\n  $type?: 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n  comment?: string\n}\n\nconst hashCancelScheduledTakedownEvent = 'cancelScheduledTakedownEvent'\n\nexport function isCancelScheduledTakedownEvent<V>(v: V) {\n  return is$typed(v, id, hashCancelScheduledTakedownEvent)\n}\n\nexport function validateCancelScheduledTakedownEvent<V>(v: V) {\n  return validate<CancelScheduledTakedownEvent & V>(\n    v,\n    id,\n    hashCancelScheduledTakedownEvent,\n  )\n}\n\nexport interface RepoView {\n  $type?: 'tools.ozone.moderation.defs#repoView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords: { [_ in string]: unknown }[]\n  indexedAt: string\n  moderation: Moderation\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invitesDisabled?: boolean\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ComAtprotoAdminDefs.ThreatSignature[]\n}\n\nconst hashRepoView = 'repoView'\n\nexport function isRepoView<V>(v: V) {\n  return is$typed(v, id, hashRepoView)\n}\n\nexport function validateRepoView<V>(v: V) {\n  return validate<RepoView & V>(v, id, hashRepoView)\n}\n\nexport interface RepoViewDetail {\n  $type?: 'tools.ozone.moderation.defs#repoViewDetail'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords: { [_ in string]: unknown }[]\n  indexedAt: string\n  moderation: ModerationDetail\n  labels?: ComAtprotoLabelDefs.Label[]\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  inviteNote?: string\n  emailConfirmedAt?: string\n  deactivatedAt?: string\n  threatSignatures?: ComAtprotoAdminDefs.ThreatSignature[]\n}\n\nconst hashRepoViewDetail = 'repoViewDetail'\n\nexport function isRepoViewDetail<V>(v: V) {\n  return is$typed(v, id, hashRepoViewDetail)\n}\n\nexport function validateRepoViewDetail<V>(v: V) {\n  return validate<RepoViewDetail & V>(v, id, hashRepoViewDetail)\n}\n\nexport interface RepoViewNotFound {\n  $type?: 'tools.ozone.moderation.defs#repoViewNotFound'\n  did: string\n}\n\nconst hashRepoViewNotFound = 'repoViewNotFound'\n\nexport function isRepoViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashRepoViewNotFound)\n}\n\nexport function validateRepoViewNotFound<V>(v: V) {\n  return validate<RepoViewNotFound & V>(v, id, hashRepoViewNotFound)\n}\n\nexport interface RecordView {\n  $type?: 'tools.ozone.moderation.defs#recordView'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n  blobCids: string[]\n  indexedAt: string\n  moderation: Moderation\n  repo: RepoView\n}\n\nconst hashRecordView = 'recordView'\n\nexport function isRecordView<V>(v: V) {\n  return is$typed(v, id, hashRecordView)\n}\n\nexport function validateRecordView<V>(v: V) {\n  return validate<RecordView & V>(v, id, hashRecordView)\n}\n\nexport interface RecordViewDetail {\n  $type?: 'tools.ozone.moderation.defs#recordViewDetail'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n  blobs: BlobView[]\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n  moderation: ModerationDetail\n  repo: RepoView\n}\n\nconst hashRecordViewDetail = 'recordViewDetail'\n\nexport function isRecordViewDetail<V>(v: V) {\n  return is$typed(v, id, hashRecordViewDetail)\n}\n\nexport function validateRecordViewDetail<V>(v: V) {\n  return validate<RecordViewDetail & V>(v, id, hashRecordViewDetail)\n}\n\nexport interface RecordViewNotFound {\n  $type?: 'tools.ozone.moderation.defs#recordViewNotFound'\n  uri: string\n}\n\nconst hashRecordViewNotFound = 'recordViewNotFound'\n\nexport function isRecordViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashRecordViewNotFound)\n}\n\nexport function validateRecordViewNotFound<V>(v: V) {\n  return validate<RecordViewNotFound & V>(v, id, hashRecordViewNotFound)\n}\n\nexport interface Moderation {\n  $type?: 'tools.ozone.moderation.defs#moderation'\n  subjectStatus?: SubjectStatusView\n}\n\nconst hashModeration = 'moderation'\n\nexport function isModeration<V>(v: V) {\n  return is$typed(v, id, hashModeration)\n}\n\nexport function validateModeration<V>(v: V) {\n  return validate<Moderation & V>(v, id, hashModeration)\n}\n\nexport interface ModerationDetail {\n  $type?: 'tools.ozone.moderation.defs#moderationDetail'\n  subjectStatus?: SubjectStatusView\n}\n\nconst hashModerationDetail = 'moderationDetail'\n\nexport function isModerationDetail<V>(v: V) {\n  return is$typed(v, id, hashModerationDetail)\n}\n\nexport function validateModerationDetail<V>(v: V) {\n  return validate<ModerationDetail & V>(v, id, hashModerationDetail)\n}\n\nexport interface BlobView {\n  $type?: 'tools.ozone.moderation.defs#blobView'\n  cid: string\n  mimeType: string\n  size: number\n  createdAt: string\n  details?: $Typed<ImageDetails> | $Typed<VideoDetails> | { $type: string }\n  moderation?: Moderation\n}\n\nconst hashBlobView = 'blobView'\n\nexport function isBlobView<V>(v: V) {\n  return is$typed(v, id, hashBlobView)\n}\n\nexport function validateBlobView<V>(v: V) {\n  return validate<BlobView & V>(v, id, hashBlobView)\n}\n\nexport interface ImageDetails {\n  $type?: 'tools.ozone.moderation.defs#imageDetails'\n  width: number\n  height: number\n}\n\nconst hashImageDetails = 'imageDetails'\n\nexport function isImageDetails<V>(v: V) {\n  return is$typed(v, id, hashImageDetails)\n}\n\nexport function validateImageDetails<V>(v: V) {\n  return validate<ImageDetails & V>(v, id, hashImageDetails)\n}\n\nexport interface VideoDetails {\n  $type?: 'tools.ozone.moderation.defs#videoDetails'\n  width: number\n  height: number\n  length: number\n}\n\nconst hashVideoDetails = 'videoDetails'\n\nexport function isVideoDetails<V>(v: V) {\n  return is$typed(v, id, hashVideoDetails)\n}\n\nexport function validateVideoDetails<V>(v: V) {\n  return validate<VideoDetails & V>(v, id, hashVideoDetails)\n}\n\nexport interface AccountHosting {\n  $type?: 'tools.ozone.moderation.defs#accountHosting'\n  status:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'unknown'\n    | (string & {})\n  updatedAt?: string\n  createdAt?: string\n  deletedAt?: string\n  deactivatedAt?: string\n  reactivatedAt?: string\n}\n\nconst hashAccountHosting = 'accountHosting'\n\nexport function isAccountHosting<V>(v: V) {\n  return is$typed(v, id, hashAccountHosting)\n}\n\nexport function validateAccountHosting<V>(v: V) {\n  return validate<AccountHosting & V>(v, id, hashAccountHosting)\n}\n\nexport interface RecordHosting {\n  $type?: 'tools.ozone.moderation.defs#recordHosting'\n  status: 'deleted' | 'unknown' | (string & {})\n  updatedAt?: string\n  createdAt?: string\n  deletedAt?: string\n}\n\nconst hashRecordHosting = 'recordHosting'\n\nexport function isRecordHosting<V>(v: V) {\n  return is$typed(v, id, hashRecordHosting)\n}\n\nexport function validateRecordHosting<V>(v: V) {\n  return validate<RecordHosting & V>(v, id, hashRecordHosting)\n}\n\nexport interface ReporterStats {\n  $type?: 'tools.ozone.moderation.defs#reporterStats'\n  did: string\n  /** The total number of reports made by the user on accounts. */\n  accountReportCount: number\n  /** The total number of reports made by the user on records. */\n  recordReportCount: number\n  /** The total number of accounts reported by the user. */\n  reportedAccountCount: number\n  /** The total number of records reported by the user. */\n  reportedRecordCount: number\n  /** The total number of accounts taken down as a result of the user's reports. */\n  takendownAccountCount: number\n  /** The total number of records taken down as a result of the user's reports. */\n  takendownRecordCount: number\n  /** The total number of accounts labeled as a result of the user's reports. */\n  labeledAccountCount: number\n  /** The total number of records labeled as a result of the user's reports. */\n  labeledRecordCount: number\n}\n\nconst hashReporterStats = 'reporterStats'\n\nexport function isReporterStats<V>(v: V) {\n  return is$typed(v, id, hashReporterStats)\n}\n\nexport function validateReporterStats<V>(v: V) {\n  return validate<ReporterStats & V>(v, id, hashReporterStats)\n}\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'tools.ozone.moderation.defs#modTool'\n  /** Name/identifier of the source (e.g., 'automod', 'ozone/workspace') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n\n/** Moderation event timeline event for a PLC create operation */\nexport const TIMELINEEVENTPLCCREATE = `${id}#timelineEventPlcCreate`\n/** Moderation event timeline event for generic PLC operation */\nexport const TIMELINEEVENTPLCOPERATION = `${id}#timelineEventPlcOperation`\n/** Moderation event timeline event for a PLC tombstone operation */\nexport const TIMELINEEVENTPLCTOMBSTONE = `${id}#timelineEventPlcTombstone`\n\n/** View of a scheduled moderation action */\nexport interface ScheduledActionView {\n  $type?: 'tools.ozone.moderation.defs#scheduledActionView'\n  /** Auto-incrementing row ID */\n  id: number\n  /** Type of action to be executed */\n  action: 'takedown' | (string & {})\n  /** Serialized event object that will be propagated to the event when performed */\n  eventData?: { [_ in string]: unknown }\n  /** Subject DID for the action */\n  did: string\n  /** Exact time to execute the action */\n  executeAt?: string\n  /** Earliest time to execute the action (for randomized scheduling) */\n  executeAfter?: string\n  /** Latest time to execute the action (for randomized scheduling) */\n  executeUntil?: string\n  /** Whether execution time should be randomized within the specified range */\n  randomizeExecution?: boolean\n  /** DID of the user who created this scheduled action */\n  createdBy: string\n  /** When the scheduled action was created */\n  createdAt: string\n  /** When the scheduled action was last updated */\n  updatedAt?: string\n  /** Current status of the scheduled action */\n  status: 'pending' | 'executed' | 'cancelled' | 'failed' | (string & {})\n  /** When the action was last attempted to be executed */\n  lastExecutedAt?: string\n  /** Reason for the last execution failure */\n  lastFailureReason?: string\n  /** ID of the moderation event created when action was successfully executed */\n  executionEventId?: number\n}\n\nconst hashScheduledActionView = 'scheduledActionView'\n\nexport function isScheduledActionView<V>(v: V) {\n  return is$typed(v, id, hashScheduledActionView)\n}\n\nexport function validateScheduledActionView<V>(v: V) {\n  return validate<ScheduledActionView & V>(v, id, hashScheduledActionView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/emitEvent.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.emitEvent'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  event:\n    | $Typed<ToolsOzoneModerationDefs.ModEventTakedown>\n    | $Typed<ToolsOzoneModerationDefs.ModEventAcknowledge>\n    | $Typed<ToolsOzoneModerationDefs.ModEventEscalate>\n    | $Typed<ToolsOzoneModerationDefs.ModEventComment>\n    | $Typed<ToolsOzoneModerationDefs.ModEventLabel>\n    | $Typed<ToolsOzoneModerationDefs.ModEventReport>\n    | $Typed<ToolsOzoneModerationDefs.ModEventMute>\n    | $Typed<ToolsOzoneModerationDefs.ModEventUnmute>\n    | $Typed<ToolsOzoneModerationDefs.ModEventMuteReporter>\n    | $Typed<ToolsOzoneModerationDefs.ModEventUnmuteReporter>\n    | $Typed<ToolsOzoneModerationDefs.ModEventReverseTakedown>\n    | $Typed<ToolsOzoneModerationDefs.ModEventResolveAppeal>\n    | $Typed<ToolsOzoneModerationDefs.ModEventEmail>\n    | $Typed<ToolsOzoneModerationDefs.ModEventDivert>\n    | $Typed<ToolsOzoneModerationDefs.ModEventTag>\n    | $Typed<ToolsOzoneModerationDefs.AccountEvent>\n    | $Typed<ToolsOzoneModerationDefs.IdentityEvent>\n    | $Typed<ToolsOzoneModerationDefs.RecordEvent>\n    | $Typed<ToolsOzoneModerationDefs.ModEventPriorityScore>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssuranceEvent>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssuranceOverrideEvent>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssurancePurgeEvent>\n    | $Typed<ToolsOzoneModerationDefs.RevokeAccountCredentialsEvent>\n    | $Typed<ToolsOzoneModerationDefs.ScheduleTakedownEvent>\n    | $Typed<ToolsOzoneModerationDefs.CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  subjectBlobCids?: string[]\n  createdBy: string\n  modTool?: ToolsOzoneModerationDefs.ModTool\n  /** An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject. */\n  externalId?: string\n}\n\nexport type OutputSchema = ToolsOzoneModerationDefs.ModEventView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SubjectHasAction' | 'DuplicateExternalId'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getAccountTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getAccountTimeline'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  timeline: TimelineItem[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface TimelineItem {\n  $type?: 'tools.ozone.moderation.getAccountTimeline#timelineItem'\n  day: string\n  summary: TimelineItemSummary[]\n}\n\nconst hashTimelineItem = 'timelineItem'\n\nexport function isTimelineItem<V>(v: V) {\n  return is$typed(v, id, hashTimelineItem)\n}\n\nexport function validateTimelineItem<V>(v: V) {\n  return validate<TimelineItem & V>(v, id, hashTimelineItem)\n}\n\nexport interface TimelineItemSummary {\n  $type?: 'tools.ozone.moderation.getAccountTimeline#timelineItemSummary'\n  eventSubjectType: 'account' | 'record' | 'chat' | (string & {})\n  eventType:\n    | 'tools.ozone.moderation.defs#modEventTakedown'\n    | 'tools.ozone.moderation.defs#modEventReverseTakedown'\n    | 'tools.ozone.moderation.defs#modEventComment'\n    | 'tools.ozone.moderation.defs#modEventReport'\n    | 'tools.ozone.moderation.defs#modEventLabel'\n    | 'tools.ozone.moderation.defs#modEventAcknowledge'\n    | 'tools.ozone.moderation.defs#modEventEscalate'\n    | 'tools.ozone.moderation.defs#modEventMute'\n    | 'tools.ozone.moderation.defs#modEventUnmute'\n    | 'tools.ozone.moderation.defs#modEventMuteReporter'\n    | 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n    | 'tools.ozone.moderation.defs#modEventEmail'\n    | 'tools.ozone.moderation.defs#modEventResolveAppeal'\n    | 'tools.ozone.moderation.defs#modEventDivert'\n    | 'tools.ozone.moderation.defs#modEventTag'\n    | 'tools.ozone.moderation.defs#accountEvent'\n    | 'tools.ozone.moderation.defs#identityEvent'\n    | 'tools.ozone.moderation.defs#recordEvent'\n    | 'tools.ozone.moderation.defs#modEventPriorityScore'\n    | 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n    | 'tools.ozone.moderation.defs#timelineEventPlcCreate'\n    | 'tools.ozone.moderation.defs#timelineEventPlcOperation'\n    | 'tools.ozone.moderation.defs#timelineEventPlcTombstone'\n    | 'tools.ozone.hosting.getAccountHistory#accountCreated'\n    | 'tools.ozone.hosting.getAccountHistory#emailConfirmed'\n    | 'tools.ozone.hosting.getAccountHistory#passwordUpdated'\n    | 'tools.ozone.hosting.getAccountHistory#handleUpdated'\n    | 'tools.ozone.moderation.defs#scheduleTakedownEvent'\n    | 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n    | (string & {})\n  count: number\n}\n\nconst hashTimelineItemSummary = 'timelineItemSummary'\n\nexport function isTimelineItemSummary<V>(v: V) {\n  return is$typed(v, id, hashTimelineItemSummary)\n}\n\nexport function validateTimelineItemSummary<V>(v: V) {\n  return validate<TimelineItemSummary & V>(v, id, hashTimelineItemSummary)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getEvent.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getEvent'\n\nexport type QueryParams = {\n  id: number\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.ModEventViewDetail\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRecord'\n\nexport type QueryParams = {\n  uri: string\n  cid?: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.RecordViewDetail\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RecordNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRecords'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  records: (\n    | $Typed<ToolsOzoneModerationDefs.RecordViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RecordViewNotFound>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRepo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.RepoViewDetail\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getReporterStats.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getReporterStats'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  stats: ToolsOzoneModerationDefs.ReporterStats[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRepos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  repos: (\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/getSubjects.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getSubjects'\n\nexport type QueryParams = {\n  subjects: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subjects: ToolsOzoneModerationDefs.SubjectView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/listScheduledActions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.listScheduledActions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Filter actions scheduled to execute after this time */\n  startsAfter?: string\n  /** Filter actions scheduled to execute before this time */\n  endsBefore?: string\n  /** Filter actions for specific DID subjects */\n  subjects?: string[]\n  /** Filter actions by status */\n  statuses: ('pending' | 'executed' | 'cancelled' | 'failed' | (string & {}))[]\n  /** Maximum number of results to return */\n  limit: number\n  /** Cursor for pagination */\n  cursor?: string\n}\n\nexport interface OutputSchema {\n  actions: ToolsOzoneModerationDefs.ScheduledActionView[]\n  /** Cursor for next page of results */\n  cursor?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/queryEvents.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.queryEvents'\n\nexport type QueryParams = {\n  /** The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned. */\n  types?: string[]\n  createdBy?: string\n  /** Sort direction for the events. Defaults to descending order of created at timestamp. */\n  sortDirection: 'asc' | 'desc'\n  /** Retrieve events created after a given timestamp */\n  createdAfter?: string\n  /** Retrieve events created before a given timestamp */\n  createdBefore?: string\n  subject?: string\n  /** If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored. */\n  collections?: string[]\n  /** If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */\n  subjectType?: 'account' | 'record' | (string & {})\n  /** If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned. */\n  includeAllUserRecords: boolean\n  limit: number\n  /** If true, only events with comments are returned */\n  hasComment?: boolean\n  /** If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition. */\n  comment?: string\n  /** If specified, only events where all of these labels were added are returned */\n  addedLabels?: string[]\n  /** If specified, only events where all of these labels were removed are returned */\n  removedLabels?: string[]\n  /** If specified, only events where all of these tags were added are returned */\n  addedTags?: string[]\n  /** If specified, only events where all of these tags were removed are returned */\n  removedTags?: string[]\n  reportTypes?: string[]\n  policies?: string[]\n  /** If specified, only events where the modTool name matches any of the given values are returned */\n  modTool?: string[]\n  /** If specified, only events where the batchId matches the given value are returned */\n  batchId?: string\n  /** If specified, only events where the age assurance state matches the given value are returned */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n  /** If specified, only events where strikeCount value is set are returned. */\n  withStrike?: boolean\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  events: ToolsOzoneModerationDefs.ModEventView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.queryStatuses'\n\nexport type QueryParams = {\n  /** Number of queues being used by moderators. Subjects will be split among all queues. */\n  queueCount?: number\n  /** Index of the queue to fetch subjects from. Works only when queueCount value is specified. */\n  queueIndex?: number\n  /** A seeder to shuffle/balance the queue items. */\n  queueSeed?: string\n  /** All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned. */\n  includeAllUserRecords?: boolean\n  /** The subject to get the status for. */\n  subject?: string\n  /** Search subjects by keyword from comments */\n  comment?: string\n  /** Search subjects reported after a given timestamp */\n  reportedAfter?: string\n  /** Search subjects reported before a given timestamp */\n  reportedBefore?: string\n  /** Search subjects reviewed after a given timestamp */\n  reviewedAfter?: string\n  /** Search subjects where the associated record/account was deleted after a given timestamp */\n  hostingDeletedAfter?: string\n  /** Search subjects where the associated record/account was deleted before a given timestamp */\n  hostingDeletedBefore?: string\n  /** Search subjects where the associated record/account was updated after a given timestamp */\n  hostingUpdatedAfter?: string\n  /** Search subjects where the associated record/account was updated before a given timestamp */\n  hostingUpdatedBefore?: string\n  /** Search subjects by the status of the associated record/account */\n  hostingStatuses?: string[]\n  /** Search subjects reviewed before a given timestamp */\n  reviewedBefore?: string\n  /** By default, we don't include muted subjects in the results. Set this to true to include them. */\n  includeMuted?: boolean\n  /** When set to true, only muted subjects and reporters will be returned. */\n  onlyMuted?: boolean\n  /** Specify when fetching subjects in a certain state */\n  reviewState?:\n    | 'tools.ozone.moderation.defs#reviewOpen'\n    | 'tools.ozone.moderation.defs#reviewClosed'\n    | 'tools.ozone.moderation.defs#reviewEscalated'\n    | 'tools.ozone.moderation.defs#reviewNone'\n    | (string & {})\n  ignoreSubjects?: string[]\n  /** Get all subject statuses that were reviewed by a specific moderator */\n  lastReviewedBy?: string\n  sortField:\n    | 'lastReviewedAt'\n    | 'lastReportedAt'\n    | 'reportedRecordsCount'\n    | 'takendownRecordsCount'\n    | 'priorityScore'\n  sortDirection: 'asc' | 'desc'\n  /** Get subjects that were taken down */\n  takendown?: boolean\n  /** Get subjects in unresolved appealed status */\n  appealed?: boolean\n  limit: number\n  tags?: string[]\n  excludeTags?: string[]\n  cursor?: string\n  /** If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored. */\n  collections?: string[]\n  /** If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */\n  subjectType?: 'account' | 'record' | (string & {})\n  /** If specified, only subjects that belong to an account that has at least this many suspensions will be returned. */\n  minAccountSuspendCount?: number\n  /** If specified, only subjects that belong to an account that has at least this many reported records will be returned. */\n  minReportedRecordsCount?: number\n  /** If specified, only subjects that belong to an account that has at least this many taken down records will be returned. */\n  minTakendownRecordsCount?: number\n  /** If specified, only subjects that have priority score value above the given value will be returned. */\n  minPriorityScore?: number\n  /** If specified, only subjects that belong to an account that has at least this many active strikes will be returned. */\n  minStrikeCount?: number\n  /** If specified, only subjects with the given age assurance state will be returned. */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subjectStatuses: ToolsOzoneModerationDefs.SubjectStatusView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/scheduleAction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.scheduleAction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  action: $Typed<Takedown> | { $type: string }\n  /** Array of DID subjects to schedule the action for */\n  subjects: string[]\n  createdBy: string\n  scheduling: SchedulingConfig\n  modTool?: ToolsOzoneModerationDefs.ModTool\n}\n\nexport type OutputSchema = ScheduledActionResults\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Schedule a takedown action */\nexport interface Takedown {\n  $type?: 'tools.ozone.moderation.scheduleAction#takedown'\n  comment?: string\n  /** Indicates how long the takedown should be in effect before automatically expiring. */\n  durationInHours?: number\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n  /** Names/Keywords of the policies that drove the decision. */\n  policies?: string[]\n  /** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */\n  severityLevel?: string\n  /** Number of strikes to assign to the user when takedown is applied. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n  /** Email content to be sent to the user upon takedown. */\n  emailContent?: string\n  /** Subject of the email to be sent to the user upon takedown. */\n  emailSubject?: string\n}\n\nconst hashTakedown = 'takedown'\n\nexport function isTakedown<V>(v: V) {\n  return is$typed(v, id, hashTakedown)\n}\n\nexport function validateTakedown<V>(v: V) {\n  return validate<Takedown & V>(v, id, hashTakedown)\n}\n\n/** Configuration for when the action should be executed */\nexport interface SchedulingConfig {\n  $type?: 'tools.ozone.moderation.scheduleAction#schedulingConfig'\n  /** Exact time to execute the action */\n  executeAt?: string\n  /** Earliest time to execute the action (for randomized scheduling) */\n  executeAfter?: string\n  /** Latest time to execute the action (for randomized scheduling) */\n  executeUntil?: string\n}\n\nconst hashSchedulingConfig = 'schedulingConfig'\n\nexport function isSchedulingConfig<V>(v: V) {\n  return is$typed(v, id, hashSchedulingConfig)\n}\n\nexport function validateSchedulingConfig<V>(v: V) {\n  return validate<SchedulingConfig & V>(v, id, hashSchedulingConfig)\n}\n\nexport interface ScheduledActionResults {\n  $type?: 'tools.ozone.moderation.scheduleAction#scheduledActionResults'\n  succeeded: string[]\n  failed: FailedScheduling[]\n}\n\nconst hashScheduledActionResults = 'scheduledActionResults'\n\nexport function isScheduledActionResults<V>(v: V) {\n  return is$typed(v, id, hashScheduledActionResults)\n}\n\nexport function validateScheduledActionResults<V>(v: V) {\n  return validate<ScheduledActionResults & V>(v, id, hashScheduledActionResults)\n}\n\nexport interface FailedScheduling {\n  $type?: 'tools.ozone.moderation.scheduleAction#failedScheduling'\n  subject: string\n  error: string\n  errorCode?: string\n}\n\nconst hashFailedScheduling = 'failedScheduling'\n\nexport function isFailedScheduling<V>(v: V) {\n  return is$typed(v, id, hashFailedScheduling)\n}\n\nexport function validateFailedScheduling<V>(v: V) {\n  return validate<FailedScheduling & V>(v, id, hashFailedScheduling)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/moderation/searchRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.searchRepos'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead */\n  term?: string\n  q?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: ToolsOzoneModerationDefs.RepoView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/report/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.report.defs'\n\nexport type ReasonType =\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n/** An issue not included in these options */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Animal welfare violations */\nexport const REASONVIOLENCEANIMAL = `${id}#reasonViolenceAnimal`\n/** Threats or incitement */\nexport const REASONVIOLENCETHREATS = `${id}#reasonViolenceThreats`\n/** Graphic violent content */\nexport const REASONVIOLENCEGRAPHICCONTENT = `${id}#reasonViolenceGraphicContent`\n/** Glorification of violence */\nexport const REASONVIOLENCEGLORIFICATION = `${id}#reasonViolenceGlorification`\n/** Extremist content. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONVIOLENCEEXTREMISTCONTENT = `${id}#reasonViolenceExtremistContent`\n/** Human trafficking */\nexport const REASONVIOLENCETRAFFICKING = `${id}#reasonViolenceTrafficking`\n/** Other violent content */\nexport const REASONVIOLENCEOTHER = `${id}#reasonViolenceOther`\n/** Adult sexual abuse content */\nexport const REASONSEXUALABUSECONTENT = `${id}#reasonSexualAbuseContent`\n/** Non-consensual intimate imagery */\nexport const REASONSEXUALNCII = `${id}#reasonSexualNCII`\n/** Deepfake adult content */\nexport const REASONSEXUALDEEPFAKE = `${id}#reasonSexualDeepfake`\n/** Animal sexual abuse */\nexport const REASONSEXUALANIMAL = `${id}#reasonSexualAnimal`\n/** Unlabelled adult content */\nexport const REASONSEXUALUNLABELED = `${id}#reasonSexualUnlabeled`\n/** Other sexual violence content */\nexport const REASONSEXUALOTHER = `${id}#reasonSexualOther`\n/** Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYCSAM = `${id}#reasonChildSafetyCSAM`\n/** Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYGROOM = `${id}#reasonChildSafetyGroom`\n/** Privacy violation involving a minor */\nexport const REASONCHILDSAFETYPRIVACY = `${id}#reasonChildSafetyPrivacy`\n/** Harassment or bullying of minors */\nexport const REASONCHILDSAFETYHARASSMENT = `${id}#reasonChildSafetyHarassment`\n/** Other child safety. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYOTHER = `${id}#reasonChildSafetyOther`\n/** Trolling */\nexport const REASONHARASSMENTTROLL = `${id}#reasonHarassmentTroll`\n/** Targeted harassment */\nexport const REASONHARASSMENTTARGETED = `${id}#reasonHarassmentTargeted`\n/** Hate speech */\nexport const REASONHARASSMENTHATESPEECH = `${id}#reasonHarassmentHateSpeech`\n/** Doxxing */\nexport const REASONHARASSMENTDOXXING = `${id}#reasonHarassmentDoxxing`\n/** Other harassing or hateful content */\nexport const REASONHARASSMENTOTHER = `${id}#reasonHarassmentOther`\n/** Fake account or bot */\nexport const REASONMISLEADINGBOT = `${id}#reasonMisleadingBot`\n/** Impersonation */\nexport const REASONMISLEADINGIMPERSONATION = `${id}#reasonMisleadingImpersonation`\n/** Spam */\nexport const REASONMISLEADINGSPAM = `${id}#reasonMisleadingSpam`\n/** Scam */\nexport const REASONMISLEADINGSCAM = `${id}#reasonMisleadingScam`\n/** False information about elections */\nexport const REASONMISLEADINGELECTIONS = `${id}#reasonMisleadingElections`\n/** Other misleading content */\nexport const REASONMISLEADINGOTHER = `${id}#reasonMisleadingOther`\n/** Hacking or system attacks */\nexport const REASONRULESITESECURITY = `${id}#reasonRuleSiteSecurity`\n/** Promoting or selling prohibited items or services */\nexport const REASONRULEPROHIBITEDSALES = `${id}#reasonRuleProhibitedSales`\n/** Banned user returning */\nexport const REASONRULEBANEVASION = `${id}#reasonRuleBanEvasion`\n/** Other */\nexport const REASONRULEOTHER = `${id}#reasonRuleOther`\n/** Content promoting or depicting self-harm */\nexport const REASONSELFHARMCONTENT = `${id}#reasonSelfHarmContent`\n/** Eating disorders */\nexport const REASONSELFHARMED = `${id}#reasonSelfHarmED`\n/** Dangerous challenges or activities */\nexport const REASONSELFHARMSTUNTS = `${id}#reasonSelfHarmStunts`\n/** Dangerous substances or drug abuse */\nexport const REASONSELFHARMSUBSTANCES = `${id}#reasonSelfHarmSubstances`\n/** Other dangerous content */\nexport const REASONSELFHARMOTHER = `${id}#reasonSelfHarmOther`\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/safelink/addRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.addRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to apply the rule to */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  action: ToolsOzoneSafelinkDefs.ActionType\n  reason: ToolsOzoneSafelinkDefs.ReasonType\n  /** Optional comment about the decision */\n  comment?: string\n  /** Author DID. Only respected when using admin auth */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidUrl' | 'RuleAlreadyExists'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/safelink/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.defs'\n\n/** An event for URL safety decisions */\nexport interface Event {\n  $type?: 'tools.ozone.safelink.defs#event'\n  /** Auto-incrementing row ID */\n  id: number\n  eventType: EventType\n  /** The URL that this rule applies to */\n  url: string\n  pattern: PatternType\n  action: ActionType\n  reason: ReasonType\n  /** DID of the user who created this rule */\n  createdBy: string\n  createdAt: string\n  /** Optional comment about the decision */\n  comment?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n\nexport type EventType = 'addRule' | 'updateRule' | 'removeRule' | (string & {})\nexport type PatternType = 'domain' | 'url' | (string & {})\nexport type ActionType = 'block' | 'warn' | 'whitelist' | (string & {})\nexport type ReasonType = 'csam' | 'spam' | 'phishing' | 'none' | (string & {})\n\n/** Input for creating a URL safety rule */\nexport interface UrlRule {\n  $type?: 'tools.ozone.safelink.defs#urlRule'\n  /** The URL or domain to apply the rule to */\n  url: string\n  pattern: PatternType\n  action: ActionType\n  reason: ReasonType\n  /** Optional comment about the decision */\n  comment?: string\n  /** DID of the user added the rule. */\n  createdBy: string\n  /** Timestamp when the rule was created */\n  createdAt: string\n  /** Timestamp when the rule was last updated */\n  updatedAt: string\n}\n\nconst hashUrlRule = 'urlRule'\n\nexport function isUrlRule<V>(v: V) {\n  return is$typed(v, id, hashUrlRule)\n}\n\nexport function validateUrlRule<V>(v: V) {\n  return validate<UrlRule & V>(v, id, hashUrlRule)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/safelink/queryEvents.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.queryEvents'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Cursor for pagination */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit: number\n  /** Filter by specific URLs or domains */\n  urls?: string[]\n  /** Filter by pattern type */\n  patternType?: string\n  /** Sort direction */\n  sortDirection: 'asc' | 'desc' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** Next cursor for pagination. Only present if there are more results. */\n  cursor?: string\n  events: ToolsOzoneSafelinkDefs.Event[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/safelink/queryRules.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.queryRules'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Cursor for pagination */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit: number\n  /** Filter by specific URLs or domains */\n  urls?: string[]\n  /** Filter by pattern type */\n  patternType?: string\n  /** Filter by action types */\n  actions?: string[]\n  /** Filter by reason type */\n  reason?: string\n  /** Filter by rule creator */\n  createdBy?: string\n  /** Sort direction */\n  sortDirection: 'asc' | 'desc' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** Next cursor for pagination. Only present if there are more results. */\n  cursor?: string\n  rules: ToolsOzoneSafelinkDefs.UrlRule[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/safelink/removeRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.removeRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to remove the rule for */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  /** Optional comment about why the rule is being removed */\n  comment?: string\n  /** Optional DID of the user. Only respected when using admin auth. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RuleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/safelink/updateRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.updateRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to update the rule for */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  action: ToolsOzoneSafelinkDefs.ActionType\n  reason: ToolsOzoneSafelinkDefs.ReasonType\n  /** Optional comment about the update */\n  comment?: string\n  /** Optional DID to credit as the creator. Only respected for admin_token authentication. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RuleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/server/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.server.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  appview?: ServiceConfig\n  pds?: ServiceConfig\n  blobDivert?: ServiceConfig\n  chat?: ServiceConfig\n  viewer?: ViewerConfig\n  /** The did of the verifier used for verification. */\n  verifierDid?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ServiceConfig {\n  $type?: 'tools.ozone.server.getConfig#serviceConfig'\n  url?: string\n}\n\nconst hashServiceConfig = 'serviceConfig'\n\nexport function isServiceConfig<V>(v: V) {\n  return is$typed(v, id, hashServiceConfig)\n}\n\nexport function validateServiceConfig<V>(v: V) {\n  return validate<ServiceConfig & V>(v, id, hashServiceConfig)\n}\n\nexport interface ViewerConfig {\n  $type?: 'tools.ozone.server.getConfig#viewerConfig'\n  role?:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n}\n\nconst hashViewerConfig = 'viewerConfig'\n\nexport function isViewerConfig<V>(v: V) {\n  return is$typed(v, id, hashViewerConfig)\n}\n\nexport function validateViewerConfig<V>(v: V) {\n  return validate<ViewerConfig & V>(v, id, hashViewerConfig)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/addValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.addValues'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to add values to */\n  name: string\n  /** Array of string values to add to the set */\n  values: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.defs'\n\nexport interface Set {\n  $type?: 'tools.ozone.set.defs#set'\n  name: string\n  description?: string\n}\n\nconst hashSet = 'set'\n\nexport function isSet<V>(v: V) {\n  return is$typed(v, id, hashSet)\n}\n\nexport function validateSet<V>(v: V) {\n  return validate<Set & V>(v, id, hashSet)\n}\n\nexport interface SetView {\n  $type?: 'tools.ozone.set.defs#setView'\n  name: string\n  description?: string\n  setSize: number\n  createdAt: string\n  updatedAt: string\n}\n\nconst hashSetView = 'setView'\n\nexport function isSetView<V>(v: V) {\n  return is$typed(v, id, hashSetView)\n}\n\nexport function validateSetView<V>(v: V) {\n  return validate<SetView & V>(v, id, hashSetView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/deleteSet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.deleteSet'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to delete */\n  name: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SetNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/deleteValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.deleteValues'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to delete values from */\n  name: string\n  /** Array of string values to delete from the set */\n  values: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SetNotFound'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/getValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.getValues'\n\nexport type QueryParams = {\n  name: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  set: ToolsOzoneSetDefs.SetView\n  values: string[]\n  cursor?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SetNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/querySets.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.querySets'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  namePrefix?: string\n  sortBy: 'name' | 'createdAt' | 'updatedAt'\n  /** Defaults to ascending order of name field. */\n  sortDirection: 'asc' | 'desc'\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  sets: ToolsOzoneSetDefs.SetView[]\n  cursor?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/set/upsertSet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.upsertSet'\n\nexport type QueryParams = {}\nexport type InputSchema = ToolsOzoneSetDefs.Set\nexport type OutputSchema = ToolsOzoneSetDefs.SetView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/setting/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.defs'\n\nexport interface Option {\n  $type?: 'tools.ozone.setting.defs#option'\n  key: string\n  did: string\n  value: { [_ in string]: unknown }\n  description?: string\n  createdAt?: string\n  updatedAt?: string\n  managerRole?:\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n  scope: 'instance' | 'personal' | (string & {})\n  createdBy: string\n  lastUpdatedBy: string\n}\n\nconst hashOption = 'option'\n\nexport function isOption<V>(v: V) {\n  return is$typed(v, id, hashOption)\n}\n\nexport function validateOption<V>(v: V) {\n  return validate<Option & V>(v, id, hashOption)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/setting/listOptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSettingDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.listOptions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  scope: 'instance' | 'personal' | (string & {})\n  /** Filter keys by prefix */\n  prefix?: string\n  /** Filter for only the specified keys. Ignored if prefix is provided */\n  keys?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  options: ToolsOzoneSettingDefs.Option[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/setting/removeOptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.removeOptions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  keys: string[]\n  scope: 'instance' | 'personal' | (string & {})\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/setting/upsertOption.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSettingDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.upsertOption'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  key: string\n  scope: 'instance' | 'personal' | (string & {})\n  value: { [_ in string]: unknown }\n  description?: string\n  managerRole?:\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleAdmin'\n    | (string & {})\n}\n\nexport interface OutputSchema {\n  option: ToolsOzoneSettingDefs.Option\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/signature/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.defs'\n\nexport interface SigDetail {\n  $type?: 'tools.ozone.signature.defs#sigDetail'\n  property: string\n  value: string\n}\n\nconst hashSigDetail = 'sigDetail'\n\nexport function isSigDetail<V>(v: V) {\n  return is$typed(v, id, hashSigDetail)\n}\n\nexport function validateSigDetail<V>(v: V) {\n  return validate<SigDetail & V>(v, id, hashSigDetail)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/signature/findCorrelation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSignatureDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.findCorrelation'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  details: ToolsOzoneSignatureDefs.SigDetail[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/signature/findRelatedAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ToolsOzoneSignatureDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.findRelatedAccounts'\n\nexport type QueryParams = {\n  did: string\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: RelatedAccount[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface RelatedAccount {\n  $type?: 'tools.ozone.signature.findRelatedAccounts#relatedAccount'\n  account: ComAtprotoAdminDefs.AccountView\n  similarities?: ToolsOzoneSignatureDefs.SigDetail[]\n}\n\nconst hashRelatedAccount = 'relatedAccount'\n\nexport function isRelatedAccount<V>(v: V) {\n  return is$typed(v, id, hashRelatedAccount)\n}\n\nexport function validateRelatedAccount<V>(v: V) {\n  return validate<RelatedAccount & V>(v, id, hashRelatedAccount)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/signature/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.searchAccounts'\n\nexport type QueryParams = {\n  values: string[]\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/team/addMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.addMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleTriage'\n    | (string & {})\n}\n\nexport type OutputSchema = ToolsOzoneTeamDefs.Member\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'MemberAlreadyExists'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/team/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.defs'\n\nexport interface Member {\n  $type?: 'tools.ozone.team.defs#member'\n  did: string\n  disabled?: boolean\n  profile?: AppBskyActorDefs.ProfileViewDetailed\n  createdAt?: string\n  updatedAt?: string\n  lastUpdatedBy?: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n}\n\nconst hashMember = 'member'\n\nexport function isMember<V>(v: V) {\n  return is$typed(v, id, hashMember)\n}\n\nexport function validateMember<V>(v: V) {\n  return validate<Member & V>(v, id, hashMember)\n}\n\n/** Admin role. Highest level of access, can perform all actions. */\nexport const ROLEADMIN = `${id}#roleAdmin`\n/** Moderator role. Can perform most actions. */\nexport const ROLEMODERATOR = `${id}#roleModerator`\n/** Triage role. Mostly intended for monitoring and escalating issues. */\nexport const ROLETRIAGE = `${id}#roleTriage`\n/** Verifier role. Only allowed to issue verifications. */\nexport const ROLEVERIFIER = `${id}#roleVerifier`\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/team/deleteMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.deleteMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'MemberNotFound' | 'CannotDeleteSelf'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/team/listMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.listMembers'\n\nexport type QueryParams = {\n  q?: string\n  disabled?: boolean\n  roles?: string[]\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  members: ToolsOzoneTeamDefs.Member[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/team/updateMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.updateMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  disabled?: boolean\n  role?:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleTriage'\n    | (string & {})\n}\n\nexport type OutputSchema = ToolsOzoneTeamDefs.Member\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'MemberNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/verification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from '../moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.defs'\n\n/** Verification data for the associated subject. */\nexport interface VerificationView {\n  $type?: 'tools.ozone.verification.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** The subject of the verification. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Timestamp when the verification was created. */\n  createdAt: string\n  /** Describes the reason for revocation, also indicating that the verification is no longer valid. */\n  revokeReason?: string\n  /** Timestamp when the verification was revoked. */\n  revokedAt?: string\n  /** The user who revoked this verification. */\n  revokedBy?: string\n  subjectProfile?: { $type: string }\n  issuerProfile?: { $type: string }\n  subjectRepo?:\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n  issuerRepo?:\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/verification/grantVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneVerificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.grantVerifications'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of verification requests to process */\n  verifications: VerificationInput[]\n}\n\nexport interface OutputSchema {\n  verifications: ToolsOzoneVerificationDefs.VerificationView[]\n  failedVerifications: GrantError[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface VerificationInput {\n  $type?: 'tools.ozone.verification.grantVerifications#verificationInput'\n  /** The did of the subject being verified */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying. */\n  displayName: string\n  /** Timestamp for verification record. Defaults to current time when not specified. */\n  createdAt?: string\n}\n\nconst hashVerificationInput = 'verificationInput'\n\nexport function isVerificationInput<V>(v: V) {\n  return is$typed(v, id, hashVerificationInput)\n}\n\nexport function validateVerificationInput<V>(v: V) {\n  return validate<VerificationInput & V>(v, id, hashVerificationInput)\n}\n\n/** Error object for failed verifications. */\nexport interface GrantError {\n  $type?: 'tools.ozone.verification.grantVerifications#grantError'\n  /** Error message describing the reason for failure. */\n  error: string\n  /** The did of the subject being verified */\n  subject: string\n}\n\nconst hashGrantError = 'grantError'\n\nexport function isGrantError<V>(v: V) {\n  return is$typed(v, id, hashGrantError)\n}\n\nexport function validateGrantError<V>(v: V) {\n  return validate<GrantError & V>(v, id, hashGrantError)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/verification/listVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneVerificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.listVerifications'\n\nexport type QueryParams = {\n  /** Pagination cursor */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit: number\n  /** Filter to verifications created after this timestamp */\n  createdAfter?: string\n  /** Filter to verifications created before this timestamp */\n  createdBefore?: string\n  /** Filter to verifications from specific issuers */\n  issuers?: string[]\n  /** Filter to specific verified DIDs */\n  subjects?: string[]\n  /** Sort direction for creation date */\n  sortDirection: 'asc' | 'desc'\n  /** Filter to verifications that are revoked or not. By default, includes both. */\n  isRevoked?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  verifications: ToolsOzoneVerificationDefs.VerificationView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/ozone/src/lexicon/types/tools/ozone/verification/revokeVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.revokeVerifications'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of verification record uris to revoke */\n  uris: string[]\n  /** Reason for revoking the verification. This is optional and can be omitted if not needed. */\n  revokeReason?: string\n}\n\nexport interface OutputSchema {\n  /** List of verification uris successfully revoked */\n  revokedVerifications: string[]\n  /** List of verification uris that couldn't be revoked, including failure reasons */\n  failedRevocations: RevokeError[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Error object for failed revocations */\nexport interface RevokeError {\n  $type?: 'tools.ozone.verification.revokeVerifications#revokeError'\n  /** The AT-URI of the verification record that failed to revoke. */\n  uri: string\n  /** Description of the error that occurred during revocation. */\n  error: string\n}\n\nconst hashRevokeError = 'revokeError'\n\nexport function isRevokeError<V>(v: V) {\n  return is$typed(v, id, hashRevokeError)\n}\n\nexport function validateRevokeError<V>(v: V) {\n  return validate<RevokeError & V>(v, id, hashRevokeError)\n}\n"
  },
  {
    "path": "packages/ozone/src/lexicon/util.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\n\nimport { type ValidationResult } from '@atproto/lexicon'\n\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n\nexport type $Typed<V, T extends string = string> = V & { $type: T }\nexport type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>\n\nexport type $Type<Id extends string, Hash extends string> = Hash extends 'main'\n  ? Id\n  : `${Id}#${Hash}`\n\nfunction isObject<V>(v: V): v is V & object {\n  return v != null && typeof v === 'object'\n}\n\nfunction is$type<Id extends string, Hash extends string>(\n  $type: unknown,\n  id: Id,\n  hash: Hash,\n): $type is $Type<Id, Hash> {\n  return hash === 'main'\n    ? $type === id\n    : // $type === `${id}#${hash}`\n      typeof $type === 'string' &&\n        $type.length === id.length + 1 + hash.length &&\n        $type.charCodeAt(id.length) === 35 /* '#' */ &&\n        $type.startsWith(id) &&\n        $type.endsWith(hash)\n}\n\nexport type $TypedObject<\n  V,\n  Id extends string,\n  Hash extends string,\n> = V extends {\n  $type: $Type<Id, Hash>\n}\n  ? V\n  : V extends { $type?: string }\n    ? V extends { $type?: infer T extends $Type<Id, Hash> }\n      ? V & { $type: T }\n      : never\n    : V & { $type: $Type<Id, Hash> }\n\nexport function is$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is $TypedObject<V, Id, Hash> {\n  return isObject(v) && '$type' in v && is$type(v.$type, id, hash)\n}\n\nexport function maybe$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is V & object & { $type?: $Type<Id, Hash> } {\n  return (\n    isObject(v) &&\n    ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)\n  )\n}\n\nexport type Validator<R = unknown> = (v: unknown) => ValidationResult<R>\nexport type ValidatorParam<V extends Validator> =\n  V extends Validator<infer R> ? R : never\n\n/**\n * Utility function that allows to convert a \"validate*\" utility function into a\n * type predicate.\n */\nexport function asPredicate<V extends Validator>(validate: V) {\n  return function <T>(v: T): v is T & ValidatorParam<V> {\n    return validate(v).success\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/logger.ts",
    "content": "import { type IncomingMessage } from 'node:http'\nimport { pinoHttp, stdSerializers } from 'pino-http'\nimport { obfuscateHeaders, subsystemLogger } from '@atproto/common'\n\nexport const dbLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('ozone:db')\nexport const seqLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('ozone:sequencer')\nexport const httpLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('ozone')\nexport const langLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('ozone:lang')\nexport const verificationLogger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('ozone:verification')\n\nexport const loggerMiddleware = pinoHttp({\n  logger: httpLogger,\n  serializers: {\n    err: (err: unknown) => ({\n      code: err?.['code'],\n      message: err?.['message'],\n    }),\n    req: (req: IncomingMessage) => {\n      const serialized = stdSerializers.req(req)\n      const headers = obfuscateHeaders(serialized.headers)\n      return { ...serialized, headers }\n    },\n  },\n})\n"
  },
  {
    "path": "packages/ozone/src/mod-service/index.ts",
    "content": "import { Insertable, RawBuilder, sql } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { AtpAgent, ToolsOzoneModerationDefs } from '@atproto/api'\nimport { addHoursToDate, chunkArray } from '@atproto/common'\nimport { Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { AtUri, INVALID_HANDLE } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { getReviewState } from '../api/util'\nimport { BackgroundQueue } from '../background'\nimport { OzoneConfig } from '../config'\nimport { EventPusher } from '../daemon'\nimport { Database } from '../db'\nimport { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination'\nimport { BlobPushEvent } from '../db/schema/blob_push_event'\nimport { LabelChannel } from '../db/schema/label'\nimport { ModerationEvent } from '../db/schema/moderation_event'\nimport { jsonb } from '../db/types'\nimport { ImageInvalidator } from '../image-invalidator'\nimport { ids } from '../lexicon/lexicons'\nimport { RepoBlobRef, RepoRef } from '../lexicon/types/com/atproto/admin/defs'\nimport { Label } from '../lexicon/types/com/atproto/label/defs'\nimport { ReasonType } from '../lexicon/types/com/atproto/moderation/defs'\nimport { Main as StrongRef } from '../lexicon/types/com/atproto/repo/strongRef'\nimport {\n  REVIEWESCALATED,\n  REVIEWOPEN,\n  isAccountEvent,\n  isAgeAssuranceEvent,\n  isAgeAssuranceOverrideEvent,\n  isAgeAssurancePurgeEvent,\n  isIdentityEvent,\n  isModEventAcknowledge,\n  isModEventComment,\n  isModEventEmail,\n  isModEventLabel,\n  isModEventMute,\n  isModEventPriorityScore,\n  isModEventReport,\n  isModEventReverseTakedown,\n  isModEventTag,\n  isModEventTakedown,\n  isRecordEvent,\n  isScheduleTakedownEvent,\n} from '../lexicon/types/tools/ozone/moderation/defs'\nimport { QueryParams as QueryStatusParams } from '../lexicon/types/tools/ozone/moderation/queryStatuses'\nimport { httpLogger as log } from '../logger'\nimport { LABELER_HEADER_NAME, ParsedLabelers } from '../util'\nimport {\n  adjustModerationSubjectStatus,\n  getStatusIdentifierFromSubject,\n  moderationSubjectStatusQueryBuilder,\n} from './status'\nimport { StrikeService, StrikeServiceCreator } from './strike'\nimport {\n  ModSubject,\n  RecordSubject,\n  RepoSubject,\n  subjectFromStatusRow,\n} from './subject'\nimport {\n  ModEventType,\n  ModerationEventRow,\n  ModerationSubjectStatusRow,\n  ModerationSubjectStatusRowWithHandle,\n  ReporterStats,\n  ReporterStatsResult,\n  ReversibleModerationEvent,\n} from './types'\nimport {\n  dateFromDbDatetime,\n  formatLabel,\n  formatLabelRow,\n  getPdsAgentForRepo,\n  signLabel,\n} from './util'\nimport { AuthHeaders, ModerationViews } from './views'\n\nexport type ModerationServiceCreator = (db: Database) => ModerationService\n\nexport class ModerationService {\n  constructor(\n    public db: Database,\n    public signingKey: Keypair,\n    public signingKeyId: number,\n    public cfg: OzoneConfig,\n    public backgroundQueue: BackgroundQueue,\n    public idResolver: IdResolver,\n    public eventPusher: EventPusher,\n    public appviewAgent: AtpAgent,\n    private createAuthHeaders: (\n      aud: string,\n      method: string,\n    ) => Promise<AuthHeaders>,\n    public strikeService: StrikeService,\n    public imgInvalidator?: ImageInvalidator,\n  ) {}\n\n  static creator(\n    signingKey: Keypair,\n    signingKeyId: number,\n    cfg: OzoneConfig,\n    backgroundQueue: BackgroundQueue,\n    idResolver: IdResolver,\n    eventPusher: EventPusher,\n    appviewAgent: AtpAgent,\n    createAuthHeaders: (aud: string, method: string) => Promise<AuthHeaders>,\n    strikeServiceCreator: StrikeServiceCreator,\n    imgInvalidator?: ImageInvalidator,\n  ) {\n    return (db: Database) => {\n      const strikeService = strikeServiceCreator(db)\n      return new ModerationService(\n        db,\n        signingKey,\n        signingKeyId,\n        cfg,\n        backgroundQueue,\n        idResolver,\n        eventPusher,\n        appviewAgent,\n        createAuthHeaders,\n        strikeService,\n        imgInvalidator,\n      )\n    }\n  }\n\n  views = new ModerationViews(\n    this.db,\n    this.signingKey,\n    this.signingKeyId,\n    this.appviewAgent,\n    async (method: string, labelers?: ParsedLabelers) => {\n      const authHeaders = await this.createAuthHeaders(\n        this.cfg.appview.did,\n        method,\n      )\n      if (labelers?.dids?.length) {\n        authHeaders.headers[LABELER_HEADER_NAME] = labelers.dids.join(', ')\n      }\n      return authHeaders\n    },\n    this.idResolver,\n    this.cfg.service.devMode,\n  )\n\n  async getEvent(id: number): Promise<ModerationEventRow | undefined> {\n    return await this.db.db\n      .selectFrom('moderation_event')\n      .selectAll()\n      .where('id', '=', id)\n      .executeTakeFirst()\n  }\n\n  async getEventOrThrow(id: number): Promise<ModerationEventRow> {\n    const event = await this.getEvent(id)\n    if (!event) throw new InvalidRequestError('Moderation event not found')\n    return event\n  }\n\n  async getEventByExternalId(\n    eventType: ModerationEvent['action'],\n    externalId: string,\n    subject: ModSubject,\n  ): Promise<boolean> {\n    const result = await this.db.db\n      .selectFrom('moderation_event')\n      .where('action', '=', eventType)\n      .where('externalId', '=', externalId)\n      .where('subjectDid', '=', subject.did)\n      .select(sql`1`.as('exists'))\n      .limit(1)\n      .executeTakeFirst()\n    return !!result\n  }\n\n  async getEvents(opts: {\n    subject?: string\n    createdBy?: string\n    limit: number\n    cursor?: string\n    includeAllUserRecords: boolean\n    types: ModerationEvent['action'][]\n    sortDirection?: 'asc' | 'desc'\n    hasComment?: boolean\n    comment?: string\n    createdAfter?: string\n    createdBefore?: string\n    addedLabels: string[]\n    removedLabels: string[]\n    addedTags: string[]\n    removedTags: string[]\n    reportTypes?: string[]\n    collections: string[]\n    subjectType?: string\n    policies?: string[]\n    modTool?: string[]\n    ageAssuranceState?: string\n    batchId?: string\n    withStrike?: boolean\n  }): Promise<{ cursor?: string; events: ModerationEventRow[] }> {\n    const {\n      subject,\n      createdBy,\n      limit,\n      cursor,\n      includeAllUserRecords,\n      sortDirection = 'desc',\n      types,\n      hasComment,\n      comment,\n      createdAfter,\n      createdBefore,\n      addedLabels,\n      removedLabels,\n      addedTags,\n      removedTags,\n      reportTypes,\n      collections,\n      subjectType,\n      policies,\n      modTool,\n      ageAssuranceState,\n      batchId,\n      withStrike,\n    } = opts\n    const { ref } = this.db.db.dynamic\n    let builder = this.db.db.selectFrom('moderation_event').selectAll()\n\n    if (subject) {\n      const isSubjectAtUri = subject.startsWith('at://')\n      const subjectDid = isSubjectAtUri ? new AtUri(subject).hostname : subject\n      const subjectUri = isSubjectAtUri ? subject : null\n      // regardless of subjectUri check, we always want to query against subjectDid column since that's indexed\n      builder = builder.where('subjectDid', '=', subjectDid)\n\n      // if requester wants to include all user records, let's ignore matching on subjectUri\n      if (!includeAllUserRecords) {\n        builder = builder\n          .if(!subjectUri, (q) => q.where('subjectUri', 'is', null))\n          .if(!!subjectUri, (q) => q.where('subjectUri', '=', subjectUri))\n      }\n    } else if (subjectType === 'account') {\n      builder = builder.where('subjectUri', 'is', null)\n    } else if (subjectType === 'record') {\n      builder = builder.where('subjectUri', 'is not', null)\n    }\n\n    // If subjectType is set to 'account' let that take priority and ignore collections filter\n    if (collections.length && subjectType !== 'account') {\n      builder = builder.where('subjectUri', 'is not', null).where((qb) => {\n        collections.forEach((collection) => {\n          qb = qb.orWhere('subjectUri', 'like', `%/${collection}/%`)\n        })\n        return qb\n      })\n    }\n\n    if (types.length) {\n      builder = builder.where((qb) => {\n        if (types.length === 1) {\n          return qb.where('action', '=', types[0])\n        }\n\n        return qb.where('action', 'in', types)\n      })\n    }\n    if (createdBy) {\n      builder = builder.where('createdBy', '=', createdBy)\n    }\n    if (createdAfter) {\n      builder = builder.where('createdAt', '>=', createdAfter)\n    }\n    if (createdBefore) {\n      builder = builder.where('createdAt', '<=', createdBefore)\n    }\n\n    if (comment) {\n      // the input may end in || in which case, there may be item in the array which is just '' and we want to ignore those\n      const keywords = comment.split('||').filter((keyword) => !!keyword.trim())\n      if (keywords.length > 1) {\n        builder = builder.where((qb) => {\n          keywords.forEach((keyword) => {\n            qb = qb.orWhere('comment', 'ilike', `%${keyword}%`)\n          })\n          return qb\n        })\n      } else if (keywords.length === 1) {\n        builder = builder.where('comment', 'ilike', `%${keywords[0]}%`)\n      }\n    }\n    if (hasComment) {\n      builder = builder.where('comment', 'is not', null)\n    }\n\n    // If multiple labels are passed, then only retrieve events where all those labels exist\n    if (addedLabels.length) {\n      addedLabels.forEach((label) => {\n        builder = builder.where('createLabelVals', 'ilike', `%${label}%`)\n      })\n    }\n    if (removedLabels.length) {\n      removedLabels.forEach((label) => {\n        builder = builder.where('negateLabelVals', 'ilike', `%${label}%`)\n      })\n    }\n    if (addedTags.length) {\n      builder = builder.where(sql`${ref('addedTags')} @> ${jsonb(addedTags)}`)\n    }\n    if (removedTags.length) {\n      builder = builder.where(\n        sql`${ref('removedTags')} @> ${jsonb(removedTags)}`,\n      )\n    }\n    if (reportTypes?.length) {\n      builder = builder.where(sql`meta->>'reportType'`, 'in', reportTypes)\n    }\n    if (policies?.length) {\n      builder = builder.where((qb) => {\n        policies.forEach((policy) => {\n          qb = qb.orWhere(sql`meta->>'policies'`, 'ilike', `%${policy}%`)\n        })\n        return qb\n      })\n    }\n    if (modTool?.length) {\n      builder = builder\n        .where('modTool', 'is not', null)\n        .where(sql`(\"modTool\" ->> 'name')`, 'in', modTool)\n    }\n    if (batchId) {\n      builder = builder\n        .where('modTool', 'is not', null)\n        .where(sql`(\"modTool\" -> 'meta' ->> 'batchId')`, '=', batchId)\n    }\n    if (ageAssuranceState) {\n      builder = builder\n        .where('action', 'in', [\n          'tools.ozone.moderation.defs#ageAssuranceEvent',\n          'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n        ])\n        .where(sql`meta->>'status'`, '=', ageAssuranceState)\n    }\n\n    if (withStrike !== undefined) {\n      builder = builder.where('strikeCount', 'is not', null)\n    }\n\n    const keyset = new TimeIdKeyset(\n      ref(`moderation_event.createdAt`),\n      ref('moderation_event.id'),\n    )\n    const paginatedBuilder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      direction: sortDirection,\n      tryIndex: true,\n    })\n\n    const result = await paginatedBuilder.execute()\n\n    const infos = await this.views.getAccoutInfosByDid([\n      ...result.map((row) => row.subjectDid),\n      ...result.map((row) => row.createdBy),\n    ])\n\n    const resultWithHandles = result.map((r) => ({\n      ...r,\n      creatorHandle: infos.get(r.createdBy)?.handle,\n      subjectHandle: infos.get(r.subjectDid)?.handle,\n    }))\n\n    return { cursor: keyset.packFromResult(result), events: resultWithHandles }\n  }\n\n  async getReport(id: number): Promise<ModerationEventRow | undefined> {\n    return await this.db.db\n      .selectFrom('moderation_event')\n      .where('action', '=', 'tools.ozone.moderation.defs#modEventReport')\n      .selectAll()\n      .where('id', '=', id)\n      .executeTakeFirst()\n  }\n\n  async getCurrentStatus(\n    subject: { did: string } | { uri: AtUri } | { cids: CID[] },\n  ) {\n    let builder = this.db.db.selectFrom('moderation_subject_status').selectAll()\n    if ('did' in subject) {\n      builder = builder.where('did', '=', subject.did)\n    } else if ('uri' in subject) {\n      builder = builder.where('recordPath', '=', subject.uri.toString())\n    }\n    // TODO: Handle the cid status\n    return await builder.execute()\n  }\n\n  async resolveSubjectsForAccount(\n    did: string,\n    createdBy: string,\n    accountEvent: ModerationEventRow,\n  ) {\n    const subjectsToBeResolved = await this.db.db\n      .selectFrom('moderation_subject_status')\n      .where('did', '=', did)\n      .where('recordPath', '!=', '')\n      .where('reviewState', 'in', [REVIEWESCALATED, REVIEWOPEN])\n      .selectAll()\n      .execute()\n\n    if (subjectsToBeResolved.length === 0) {\n      return\n    }\n\n    let accountEventInfo = `Account Event ID: ${accountEvent.id}`\n    if (accountEvent.comment) {\n      accountEventInfo += ` | Account Event Comment: ${accountEvent.comment}`\n    }\n    // Process subjects in chunks of 100 since each of these will trigger multiple db queries\n    for (const subjects of chunkArray(subjectsToBeResolved, 100)) {\n      await Promise.all(\n        subjects.map(async (subject) => {\n          const eventData = {\n            createdBy,\n            subject: subjectFromStatusRow(subject),\n          }\n\n          // For consistency's sake, when acknowledging appealed subjects, we should first resolve the appeal\n          if (subject.appealed) {\n            await this.logEvent({\n              event: {\n                $type: 'tools.ozone.moderation.defs#modEventResolveAppeal',\n                comment: `[AUTO_RESOLVE_ON_ACCOUNT_ACTION]: Automatically resolving all appealed content due to account level action | ${accountEventInfo}`,\n              },\n              ...eventData,\n            })\n          }\n\n          await this.logEvent({\n            event: {\n              $type: 'tools.ozone.moderation.defs#modEventAcknowledge',\n              comment: `[AUTO_RESOLVE_ON_ACCOUNT_ACTION]: Automatically resolving all reported content due to account level action | ${accountEventInfo}`,\n            },\n            ...eventData,\n          })\n        }),\n      )\n    }\n  }\n\n  async logEvent(info: {\n    event: ModEventType\n    subject: ModSubject\n    createdBy: string\n    createdAt?: Date\n    modTool?: ToolsOzoneModerationDefs.ModTool\n    externalId?: string\n  }): Promise<{\n    event: ModerationEventRow\n    subjectStatus: ModerationSubjectStatusRow | null\n  }> {\n    this.db.assertTransaction()\n    const {\n      event,\n      subject,\n      createdBy,\n      externalId,\n      createdAt = new Date(),\n      modTool,\n    } = info\n\n    const createLabelVals =\n      isModEventLabel(event) && event.createLabelVals.length > 0\n        ? event.createLabelVals.join(' ')\n        : undefined\n    const negateLabelVals =\n      isModEventLabel(event) && event.negateLabelVals.length > 0\n        ? event.negateLabelVals.join(' ')\n        : undefined\n\n    const meta: Record<string, string | number | boolean> = {}\n\n    const addedTags = isModEventTag(event) ? jsonb(event.add) : null\n    const removedTags = isModEventTag(event) ? jsonb(event.remove) : null\n\n    if (isModEventReport(event)) {\n      meta.reportType = event.reportType\n    }\n\n    if (isModEventComment(event) && event.sticky) {\n      meta.sticky = event.sticky\n    }\n\n    if (isModEventEmail(event)) {\n      meta.subjectLine = event.subjectLine\n      meta.isDelivered = !!event.isDelivered\n      if (event.content) {\n        meta.content = event.content\n      }\n      if (event.policies?.length) {\n        meta.policies = event.policies.join(',')\n      }\n    }\n\n    if (isAccountEvent(event)) {\n      meta.active = event.active\n      meta.timestamp = event.timestamp\n      if (event.status) meta.status = event.status\n    }\n\n    if (isModEventPriorityScore(event)) {\n      meta.priorityScore = event.score\n    }\n\n    if (isIdentityEvent(event)) {\n      meta.timestamp = event.timestamp\n      if (event.handle) meta.handle = event.handle\n      if (event.pdsHost) meta.pdsHost = event.pdsHost\n      if (event.tombstone) meta.tombstone = event.tombstone\n    }\n\n    if (isRecordEvent(event)) {\n      meta.timestamp = event.timestamp\n      meta.op = event.op\n      if (event.cid) meta.cid = event.cid\n    }\n\n    if (isAgeAssuranceEvent(event)) {\n      meta.status = event.status\n      meta.createdAt = event.createdAt\n      if (event.attemptId) {\n        meta.attemptId = event.attemptId\n      }\n      if (event.access) {\n        meta.access = event.access\n      }\n      if (event.initIp) {\n        meta.initIp = event.initIp\n      }\n      if (event.initUa) {\n        meta.initUa = event.initUa\n      }\n      if (event.completeIp) {\n        meta.completeIp = event.completeIp\n      }\n      if (event.completeUa) {\n        meta.completeUa = event.completeUa\n      }\n    }\n\n    if (isAgeAssuranceOverrideEvent(event)) {\n      meta.status = event.status\n      if (event.access) {\n        meta.access = event.access\n      }\n    }\n\n    if (isScheduleTakedownEvent(event)) {\n      if (event.executeAfter) {\n        meta.executeAfter = event.executeAfter\n      }\n      if (event.executeAt) {\n        meta.executeAt = event.executeAt\n      }\n      if (event.executeUntil) {\n        meta.executeUntil = event.executeUntil\n      }\n    }\n\n    if (\n      (isModEventTakedown(event) || isModEventAcknowledge(event)) &&\n      event.acknowledgeAccountSubjects\n    ) {\n      meta.acknowledgeAccountSubjects = true\n    }\n\n    if (isModEventTakedown(event) && event.policies?.length) {\n      meta.policies = event.policies.join(',')\n    }\n\n    if (isModEventTakedown(event) && event.targetServices?.length) {\n      meta.targetServices = event.targetServices.join(',')\n    }\n\n    // Keep trace of reports that came in while the reporter was in muted stated\n    if (isModEventReport(event)) {\n      const isReportingMuted = await this.isReportingMutedForSubject(createdBy)\n      if (isReportingMuted) {\n        meta.isReporterMuted = true\n      }\n    }\n\n    const subjectInfo = subject.info()\n\n    // Store severityLevel, strikeCount, and strikeExpiresAt if provided\n    // These values should be calculated by the client based on configuration\n    // processNewEvent will update the account_strike table with the new strike count\n    let severityLevel: string | null = null\n    let strikeCount: number | null = null\n    let strikeExpiresAt: string | null = null\n\n    if (\n      isModEventTakedown(event) ||\n      isModEventEmail(event) ||\n      isModEventReverseTakedown(event)\n    ) {\n      // Store severityLevel if provided (for display/tracking)\n      if (event.severityLevel) {\n        severityLevel = event.severityLevel\n      }\n      // Store explicit strikeCount if provided\n      if (event.strikeCount !== undefined) {\n        strikeCount = event.strikeCount\n      }\n      // Store strikeExpiresAt if provided by client\n      if ('strikeExpiresAt' in event && event.strikeExpiresAt) {\n        strikeExpiresAt = event.strikeExpiresAt\n      }\n    }\n\n    const modEvent = await this.db.db\n      .insertInto('moderation_event')\n      .values({\n        comment:\n          ('comment' in event &&\n            typeof event.comment === 'string' &&\n            event.comment) ||\n          null,\n        action: event.$type as ModerationEvent['action'],\n        createdAt: createdAt.toISOString(),\n        createdBy,\n        createLabelVals,\n        negateLabelVals,\n        addedTags,\n        removedTags,\n        durationInHours:\n          'durationInHours' in event && event.durationInHours\n            ? Number(event.durationInHours)\n            : null,\n        meta: Object.assign(meta, subjectInfo.meta),\n        expiresAt:\n          (isModEventTakedown(event) || isModEventMute(event)) &&\n          event.durationInHours\n            ? addHoursToDate(event.durationInHours, createdAt).toISOString()\n            : undefined,\n        subjectType: subjectInfo.subjectType,\n        subjectDid: subjectInfo.subjectDid,\n        subjectUri: subjectInfo.subjectUri,\n        subjectCid: subjectInfo.subjectCid,\n        subjectBlobCids: jsonb(subjectInfo.subjectBlobCids),\n        subjectMessageId: subjectInfo.subjectMessageId,\n        modTool: modTool ? jsonb(modTool) : null,\n        externalId: externalId ?? null,\n        severityLevel,\n        strikeCount,\n        strikeExpiresAt,\n      })\n      .returningAll()\n      .executeTakeFirstOrThrow()\n\n    const subjectStatus = await adjustModerationSubjectStatus(\n      this.db,\n      modEvent,\n      subject.blobCids,\n    )\n\n    if (isAgeAssurancePurgeEvent(event)) {\n      await this.purgeAgeAssuranceEvents(subjectInfo.subjectDid)\n    }\n\n    // Updates are only needed if strikeCount is numeric (in some cases even 0)\n    if (modEvent.strikeCount !== null) {\n      try {\n        await this.strikeService.updateSubjectStrikeCount(modEvent.subjectDid)\n      } catch (error) {\n        // Log error but don't fail the entire operation to ensure that events are logged even if updating strike count fails\n        log.error(\n          { err: error, modEventId: modEvent.id },\n          'Error processing strikes for moderation event',\n        )\n      }\n    }\n\n    return { event: modEvent, subjectStatus }\n  }\n\n  async purgeAgeAssuranceEvents(subjectDid: string) {\n    this.db.assertTransaction()\n    await this.db.db\n      .deleteFrom('moderation_event')\n      .where('subjectDid', '=', subjectDid)\n      .where('action', '=', 'tools.ozone.moderation.defs#ageAssuranceEvent')\n      .execute()\n  }\n\n  async getLastReversibleEventForSubject(subject: ReversalSubject) {\n    // If the subject is neither suspended nor muted don't bother finding the last reversible event\n    // Ideally, this should never happen because the caller of this method should only call this\n    // after ensuring that the suspended or muted subjects are being reversed\n    if (!subject.reverseMute && !subject.reverseSuspend) {\n      return null\n    }\n\n    let builder = this.db.db\n      .selectFrom('moderation_event')\n      .where('subjectDid', '=', subject.subject.did)\n\n    if (subject.subject.recordPath) {\n      builder = builder.where(\n        'subjectUri',\n        'like',\n        `%${subject.subject.recordPath}%`,\n      )\n    }\n\n    // Means the subject was suspended and needs to be unsuspended\n    if (subject.reverseSuspend) {\n      builder = builder\n        .where('action', '=', 'tools.ozone.moderation.defs#modEventTakedown')\n        .where('durationInHours', 'is not', null)\n    }\n    if (subject.reverseMute) {\n      builder = builder\n        .where('action', '=', 'tools.ozone.moderation.defs#modEventMute')\n        .where('durationInHours', 'is not', null)\n    }\n\n    return await builder\n      .orderBy('id', 'desc')\n      .selectAll()\n      .limit(1)\n      .executeTakeFirst()\n  }\n\n  async getSubjectsDueForReversal(): Promise<ReversalSubject[]> {\n    const now = new Date().toISOString()\n    const subjects = await this.db.db\n      .selectFrom('moderation_subject_status')\n      .where('suspendUntil', '<', now)\n      .orWhere('muteUntil', '<', now)\n      .selectAll()\n      .execute()\n\n    return subjects.map((row) => ({\n      subject: subjectFromStatusRow(row),\n      reverseSuspend: !!row.suspendUntil && row.suspendUntil < now,\n      reverseMute: !!row.muteUntil && row.muteUntil < now,\n    }))\n  }\n\n  async isSubjectSuspended(did: string): Promise<boolean> {\n    const res = await this.db.db\n      .selectFrom('moderation_subject_status')\n      .where('did', '=', did)\n      .where('recordPath', '=', '')\n      .where('suspendUntil', '>', new Date().toISOString())\n      .select('did')\n      .limit(1)\n      .executeTakeFirst()\n    return !!res\n  }\n\n  async revertState({\n    createdBy,\n    createdAt,\n    comment,\n    action,\n    subject,\n  }: ReversibleModerationEvent): Promise<ModerationEventRow> {\n    const isRevertingTakedown =\n      action === 'tools.ozone.moderation.defs#modEventTakedown'\n    this.db.assertTransaction()\n    const { event } = await this.logEvent({\n      event: {\n        $type: isRevertingTakedown\n          ? 'tools.ozone.moderation.defs#modEventReverseTakedown'\n          : 'tools.ozone.moderation.defs#modEventUnmute',\n        comment: comment ?? undefined,\n      },\n      createdAt,\n      createdBy,\n      subject,\n    })\n\n    if (isRevertingTakedown) {\n      if (subject.isRepo()) {\n        await this.reverseTakedownRepo(subject)\n      } else if (subject.isRecord()) {\n        await this.reverseTakedownRecord(subject)\n      }\n    }\n\n    return event\n  }\n\n  async takedownRepo(\n    subject: RepoSubject,\n    takedownId: number,\n    targetServices: Set<string>,\n    isSuspend = false,\n  ) {\n    const takedownRef = `BSKY-${\n      isSuspend ? 'SUSPEND' : 'TAKEDOWN'\n    }-${takedownId}`\n\n    const values = this.eventPusher\n      .getTakedownServices(targetServices)\n      .map((eventType) => ({\n        eventType,\n        subjectDid: subject.did,\n        takedownRef,\n      }))\n\n    // The label is consumed by appview if we opt for appview only takedown, this is needed\n    // if we opt for pds level takedown, adding the label doesn't hurt\n    const takedownLabel = isSuspend ? SUSPEND_LABEL : TAKEDOWN_LABEL\n    await this.formatAndCreateLabels(subject.did, null, {\n      create: [takedownLabel],\n    })\n\n    // If we dont have to push any events, return early\n    if (!values.length) {\n      return\n    }\n\n    const repoEvts = await this.db.db\n      .insertInto('repo_push_event')\n      .values(values)\n      .onConflict((oc) =>\n        oc.columns(['subjectDid', 'eventType']).doUpdateSet({\n          takedownRef,\n          confirmedAt: null,\n          attempts: 0,\n          lastAttempted: null,\n        }),\n      )\n      .returning('id')\n      .execute()\n\n    this.db.onCommit(() => {\n      this.backgroundQueue.add(async () => {\n        await Promise.all(\n          repoEvts.map((evt) => this.eventPusher.attemptRepoEvent(evt.id)),\n        )\n      })\n    })\n  }\n\n  async reverseTakedownRepo(subject: RepoSubject) {\n    const repoEvts = await this.db.db\n      .updateTable('repo_push_event')\n      .where('eventType', 'in', TAKEDOWNS)\n      .where('subjectDid', '=', subject.did)\n      .set({\n        takedownRef: null,\n        confirmedAt: null,\n        attempts: 0,\n        lastAttempted: null,\n      })\n      .returning('id')\n      .execute()\n\n    const existingTakedownLabels = await this.db.db\n      .selectFrom('label')\n      .where('label.uri', '=', subject.did)\n      .where('label.val', 'in', [TAKEDOWN_LABEL, SUSPEND_LABEL])\n      .where('neg', '=', false)\n      .selectAll()\n      .execute()\n\n    const takedownVals = existingTakedownLabels.map((row) => row.val)\n    await this.formatAndCreateLabels(subject.did, null, {\n      negate: takedownVals,\n    })\n\n    this.db.onCommit(() => {\n      this.backgroundQueue.add(async () => {\n        await Promise.all(\n          repoEvts.map((evt) => this.eventPusher.attemptRepoEvent(evt.id)),\n        )\n      })\n    })\n  }\n\n  async takedownRecord(\n    subject: RecordSubject,\n    takedownId: number,\n    targetServices: Set<string>,\n  ) {\n    this.db.assertTransaction()\n    await this.formatAndCreateLabels(subject.uri, subject.cid, {\n      create: [TAKEDOWN_LABEL],\n    })\n\n    const takedownRef = `BSKY-TAKEDOWN-${takedownId}`\n    const blobCids = subject.blobCids\n    if (blobCids && blobCids.length > 0) {\n      const blobValues: Insertable<BlobPushEvent>[] = []\n      for (const eventType of this.eventPusher.getTakedownServices(\n        targetServices,\n      )) {\n        for (const cid of blobCids) {\n          blobValues.push({\n            eventType,\n            takedownRef,\n            subjectDid: subject.did,\n            subjectUri: subject.uri || null,\n            subjectBlobCid: cid.toString(),\n          })\n        }\n      }\n      const blobEvts = await this.eventPusher.logBlobPushEvent(\n        blobValues,\n        takedownRef,\n      )\n\n      this.db.onCommit(() => {\n        this.backgroundQueue.add(async () => {\n          await Promise.allSettled(\n            blobEvts.map((evt) =>\n              this.eventPusher\n                .attemptBlobEvent(evt.id)\n                .catch((err) =>\n                  log.error({ ...evt, err }, 'failed to push blob event'),\n                ),\n            ),\n          )\n\n          if (this.imgInvalidator) {\n            await Promise.allSettled(\n              (subject.blobCids ?? []).map((cid) => {\n                const paths = (this.cfg.cdn.paths ?? []).map((path) =>\n                  path.replace('%s', subject.did).replace('%s', cid),\n                )\n                return this.imgInvalidator\n                  ?.invalidate(cid, paths)\n                  .catch((err) =>\n                    log.error(\n                      { err, paths, cid },\n                      'failed to invalidate blob on cdn',\n                    ),\n                  )\n              }),\n            )\n          }\n        })\n      })\n    }\n  }\n\n  async reverseTakedownRecord(subject: RecordSubject) {\n    this.db.assertTransaction()\n    await this.formatAndCreateLabels(subject.uri, subject.cid, {\n      negate: [TAKEDOWN_LABEL],\n    })\n\n    const blobCids = subject.blobCids\n    if (blobCids && blobCids.length > 0) {\n      const blobEvts = await this.db.db\n        .updateTable('blob_push_event')\n        .where('eventType', 'in', TAKEDOWNS)\n        .where('subjectDid', '=', subject.did)\n        .where(\n          'subjectBlobCid',\n          'in',\n          blobCids.map((c) => c.toString()),\n        )\n        .set({\n          takedownRef: null,\n          confirmedAt: null,\n          attempts: 0,\n          lastAttempted: null,\n        })\n        .returning('id')\n        .execute()\n\n      this.db.onCommit(() => {\n        this.backgroundQueue.add(async () => {\n          await Promise.all(\n            blobEvts.map((evt) => this.eventPusher.attemptBlobEvent(evt.id)),\n          )\n        })\n      })\n    }\n  }\n\n  async report(info: {\n    reasonType: ReasonType\n    reason?: string\n    subject: ModSubject\n    reportedBy: string\n    createdAt?: Date\n    modTool?: {\n      name: string\n      meta?: { [_ in string]: unknown }\n    }\n  }): Promise<{\n    event: ModerationEventRow\n    subjectStatus: ModerationSubjectStatusRow | null\n  }> {\n    const {\n      reasonType,\n      reason,\n      reportedBy,\n      createdAt = new Date(),\n      subject,\n      modTool,\n    } = info\n\n    const result = await this.logEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventReport',\n        reportType: reasonType,\n        comment: reason,\n      },\n      createdBy: reportedBy,\n      subject,\n      createdAt,\n      modTool,\n    })\n\n    return result\n  }\n\n  async getSubjectStatuses({\n    queueCount,\n    queueIndex,\n    queueSeed = '',\n    includeAllUserRecords,\n    cursor,\n    limit = 50,\n    takendown,\n    appealed,\n    reviewState,\n    reviewedAfter,\n    reviewedBefore,\n    reportedAfter,\n    reportedBefore,\n    includeMuted = false,\n    hostingDeletedBefore,\n    hostingDeletedAfter,\n    hostingUpdatedBefore,\n    hostingUpdatedAfter,\n    hostingStatuses,\n    onlyMuted = false,\n    ignoreSubjects,\n    sortDirection = 'desc',\n    lastReviewedBy,\n    sortField = 'lastReportedAt',\n    subject,\n    tags,\n    excludeTags,\n    collections,\n    subjectType,\n    minAccountSuspendCount,\n    minReportedRecordsCount,\n    minTakendownRecordsCount,\n    minPriorityScore,\n    minStrikeCount,\n    ageAssuranceState,\n  }: QueryStatusParams): Promise<{\n    statuses: ModerationSubjectStatusRowWithHandle[]\n    cursor?: string\n  }> {\n    let builder = moderationSubjectStatusQueryBuilder(this.db.db)\n\n    const { ref } = this.db.db.dynamic\n\n    if (subject) {\n      const subjectInfo = getStatusIdentifierFromSubject(subject)\n      builder = builder.where(\n        'moderation_subject_status.did',\n        '=',\n        subjectInfo.did,\n      )\n\n      if (!includeAllUserRecords) {\n        builder = builder.where((qb) =>\n          subjectInfo.recordPath\n            ? qb.where(\n                'moderation_subject_status.recordPath',\n                '=',\n                subjectInfo.recordPath,\n              )\n            : qb.where('moderation_subject_status.recordPath', '=', ''),\n        )\n      }\n    } else if (subjectType === 'account') {\n      builder = builder.where('moderation_subject_status.recordPath', '=', '')\n    } else if (subjectType === 'record') {\n      builder = builder.where('moderation_subject_status.recordPath', '!=', '')\n    }\n\n    // Only fetch items that belongs to the specified queue when specified\n    if (\n      !subject &&\n      queueCount &&\n      queueCount > 0 &&\n      queueIndex !== undefined &&\n      queueIndex >= 0 &&\n      queueIndex < queueCount\n    ) {\n      builder = builder.where(\n        queueSeed\n          ? sql`ABS(HASHTEXT(${queueSeed} || moderation_subject_status.did)) % ${queueCount}`\n          : sql`ABS(HASHTEXT(moderation_subject_status.did)) % ${queueCount}`,\n        '=',\n        queueIndex,\n      )\n    }\n\n    // If subjectType is set to 'account' let that take priority and ignore collections filter\n    if (subjectType !== 'account' && collections?.length) {\n      builder = builder\n        .where('moderation_subject_status.recordPath', '!=', '')\n        .where((qb) => {\n          for (const collection of collections) {\n            qb = qb.orWhere(\n              'moderation_subject_status.recordPath',\n              'like',\n              `${collection}/%`,\n            )\n          }\n          return qb\n        })\n    }\n\n    if (ignoreSubjects?.length) {\n      builder = builder\n        .where('moderation_subject_status.did', 'not in', ignoreSubjects)\n        .where('moderation_subject_status.recordPath', 'not in', ignoreSubjects)\n    }\n\n    const reviewStateNormalized = getReviewState(reviewState)\n    if (reviewStateNormalized) {\n      builder = builder.where(\n        'moderation_subject_status.reviewState',\n        '=',\n        reviewStateNormalized,\n      )\n    }\n\n    if (lastReviewedBy) {\n      builder = builder.where(\n        'moderation_subject_status.lastReviewedBy',\n        '=',\n        lastReviewedBy,\n      )\n    }\n\n    if (reviewedAfter) {\n      builder = builder.where(\n        'moderation_subject_status.lastReviewedAt',\n        '>',\n        reviewedAfter,\n      )\n    }\n\n    if (reviewedBefore) {\n      builder = builder.where(\n        'moderation_subject_status.lastReviewedAt',\n        '<',\n        reviewedBefore,\n      )\n    }\n\n    if (hostingUpdatedAfter) {\n      builder = builder.where(\n        'moderation_subject_status.hostingUpdatedAt',\n        '>',\n        hostingUpdatedAfter,\n      )\n    }\n\n    if (hostingUpdatedBefore) {\n      builder = builder.where(\n        'moderation_subject_status.hostingUpdatedAt',\n        '<',\n        hostingUpdatedBefore,\n      )\n    }\n\n    if (hostingDeletedAfter) {\n      builder = builder.where(\n        'moderation_subject_status.hostingDeletedAt',\n        '>',\n        hostingDeletedAfter,\n      )\n    }\n\n    if (hostingDeletedBefore) {\n      builder = builder.where(\n        'moderation_subject_status.hostingDeletedAt',\n        '<',\n        hostingDeletedBefore,\n      )\n    }\n\n    if (hostingStatuses?.length) {\n      builder = builder.where(\n        'moderation_subject_status.hostingStatus',\n        'in',\n        hostingStatuses,\n      )\n    }\n\n    if (reportedAfter) {\n      builder = builder.where(\n        'moderation_subject_status.lastReviewedAt',\n        '>',\n        reportedAfter,\n      )\n    }\n\n    if (reportedBefore) {\n      builder = builder.where(\n        'moderation_subject_status.lastReportedAt',\n        '<',\n        reportedBefore,\n      )\n    }\n\n    if (takendown) {\n      builder = builder.where('moderation_subject_status.takendown', '=', true)\n    }\n\n    if (appealed !== undefined) {\n      builder =\n        appealed === false\n          ? builder.where('moderation_subject_status.appealed', 'is', null)\n          : builder.where('moderation_subject_status.appealed', '=', appealed)\n    }\n\n    if (!includeMuted) {\n      builder = builder.where((qb) =>\n        qb\n          .where(\n            'moderation_subject_status.muteUntil',\n            '<',\n            new Date().toISOString(),\n          )\n          .orWhere('moderation_subject_status.muteUntil', 'is', null),\n      )\n    }\n\n    if (onlyMuted) {\n      builder = builder.where((qb) =>\n        qb\n          .where(\n            'moderation_subject_status.muteUntil',\n            '>',\n            new Date().toISOString(),\n          )\n          .orWhere(\n            'moderation_subject_status.muteReportingUntil',\n            '>',\n            new Date().toISOString(),\n          ),\n      )\n    }\n\n    // [\"tag1\", \"tag2 && tag3\", \"tag4\"] => [[\"tag1\"], [\"tag2\", \"tag3\"], [\"tag4\"]]\n    const conditions = parseTags(tags)\n    if (conditions?.length) {\n      // [[\"tag1\"], [\"tag2\", \"tag3\"], [\"tag4\"]] => (tags ? 'tag1') OR (tags ? 'tag2' AND tags ? 'tag3') OR (tags ? 'tag4')\n      builder = builder.where((qb) => {\n        for (const subTags of conditions) {\n          // OR between every conditions items (subTags)\n          qb = qb.orWhere((qb) => {\n            // AND between every subTags items (subTag)\n            for (const subTag of subTags) {\n              qb = qb.where(\n                sql`${ref('moderation_subject_status.tags')} ? ${subTag}`,\n              )\n            }\n            return qb\n          })\n        }\n        return qb\n      })\n    }\n\n    if (excludeTags?.length) {\n      builder = builder.where((qb) =>\n        qb\n          .where(\n            sql`NOT(${ref('moderation_subject_status.tags')} ?| array[${sql.join(excludeTags)}]::TEXT[])`,\n          )\n          .orWhere('tags', 'is', null),\n      )\n    }\n\n    if (minAccountSuspendCount != null && minAccountSuspendCount > 0) {\n      builder = builder.where(\n        'account_events_stats.suspendCount',\n        '>=',\n        minAccountSuspendCount,\n      )\n    }\n\n    if (minTakendownRecordsCount != null && minTakendownRecordsCount > 0) {\n      builder = builder.where(\n        'account_record_status_stats.takendownCount',\n        '>=',\n        minTakendownRecordsCount,\n      )\n    }\n\n    if (minReportedRecordsCount != null && minReportedRecordsCount > 0) {\n      builder = builder.where(\n        'account_record_events_stats.reportedCount',\n        '>=',\n        minReportedRecordsCount,\n      )\n    }\n\n    if (minPriorityScore != null && minPriorityScore >= 0) {\n      builder = builder.where(\n        'moderation_subject_status.priorityScore',\n        '>=',\n        minPriorityScore,\n      )\n    }\n\n    if (minStrikeCount != null && minStrikeCount >= 0) {\n      builder = builder.where(\n        'account_strike.activeStrikeCount',\n        '>=',\n        minStrikeCount,\n      )\n    }\n\n    if (ageAssuranceState) {\n      builder = builder.where(\n        'moderation_subject_status.ageAssuranceState',\n        '=',\n        ageAssuranceState,\n      )\n    }\n\n    const keyset = new StatusKeyset(\n      sortField === 'reportedRecordsCount'\n        ? ref(`account_record_events_stats.reportedCount`)\n        : sortField === 'takendownRecordsCount'\n          ? ref(`account_record_status_stats.takendownCount`)\n          : sortField === 'priorityScore'\n            ? ref(`moderation_subject_status.priorityScore`)\n            : ref(`moderation_subject_status.${sortField}`),\n      ref('moderation_subject_status.id'),\n    )\n    const paginatedBuilder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      direction: sortDirection,\n      tryIndex: true,\n      nullsLast: true,\n    })\n    const results = await paginatedBuilder.execute()\n\n    const infos = await this.views.getAccoutInfosByDid(\n      results.map((r) => r.did),\n    )\n\n    return {\n      statuses: results.map((r) => ({\n        ...r,\n        handle: infos.get(r.did)?.handle ?? INVALID_HANDLE,\n      })),\n      cursor: keyset.packFromResult(results),\n    }\n  }\n\n  async getStatus(\n    subject: ModSubject,\n  ): Promise<ModerationSubjectStatusRow | null> {\n    const result = await this.db.db\n      .selectFrom('moderation_subject_status')\n      .where('did', '=', subject.did)\n      .where('recordPath', '=', subject.recordPath ?? '')\n      .selectAll()\n      .executeTakeFirst()\n    return result ?? null\n  }\n\n  // This is used to check if the reporter of an incoming report is muted from reporting\n  // so we want to make sure this look up is as fast as possible\n  async isReportingMutedForSubject(did: string) {\n    const result = await this.db.db\n      .selectFrom('moderation_subject_status')\n      .where('did', '=', did)\n      .where('recordPath', '=', '')\n      .where('muteReportingUntil', '>', new Date().toISOString())\n      .select(sql`true`.as('status'))\n      .executeTakeFirst()\n\n    return !!result\n  }\n\n  async formatAndCreateLabels(\n    uri: string,\n    cid: string | null,\n    labels: { create?: string[]; negate?: string[] },\n    durationInHours?: number,\n  ): Promise<Label[]> {\n    const exp =\n      durationInHours !== undefined\n        ? addHoursToDate(durationInHours).toISOString()\n        : undefined\n    const { create = [], negate = [] } = labels\n    const toCreate = create.map((val) => ({\n      src: this.cfg.service.did,\n      uri,\n      cid: cid ?? undefined,\n      val,\n      exp,\n      cts: new Date().toISOString(),\n    }))\n    const toNegate = negate.map((val) => ({\n      src: this.cfg.service.did,\n      uri,\n      cid: cid ?? undefined,\n      val,\n      neg: true,\n      cts: new Date().toISOString(),\n    }))\n    const formatted = [...toCreate, ...toNegate]\n    return this.createLabels(formatted)\n  }\n\n  async createLabels(labels: Label[]): Promise<Label[]> {\n    if (labels.length < 1) return []\n    const signedLabels = await Promise.all(\n      labels.map((l) => signLabel(l, this.signingKey)),\n    )\n    const dbVals = signedLabels.map((l) => formatLabelRow(l, this.signingKeyId))\n    const { ref } = this.db.db.dynamic\n    const excluded = (col: string) => ref(`excluded.${col}`)\n    const res = await this.db.db\n      .insertInto('label')\n      .values(dbVals)\n      .onConflict((oc) =>\n        oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({\n          id: sql`${excluded('id')}`,\n          neg: sql`${excluded('neg')}`,\n          cts: sql`${excluded('cts')}`,\n          exp: sql`${excluded('exp')}`,\n          sig: sql`${excluded('sig')}`,\n          signingKeyId: sql`${excluded('signingKeyId')}`,\n        }),\n      )\n      .returningAll()\n      .execute()\n    await sql`notify ${ref(LabelChannel)}`.execute(this.db.db)\n    return res.map((row) => formatLabel(row))\n  }\n\n  async sendEmail(opts: {\n    content: string\n    recipientDid: string\n    subject: string\n  }) {\n    const { subject, content, recipientDid } = opts\n    const { agent: pdsAgent, url } = await getPdsAgentForRepo(\n      this.idResolver,\n      recipientDid,\n      this.cfg.service.devMode,\n    )\n    if (!pdsAgent) {\n      throw new InvalidRequestError('Invalid pds service in DID doc')\n    }\n    const { data: serverInfo } =\n      await pdsAgent.com.atproto.server.describeServer()\n    if (serverInfo.did !== `did:web:${url.hostname}`) {\n      // @TODO do bidirectional check once implemented. in the meantime,\n      // matching did to hostname we're talking to is pretty good.\n      throw new InvalidRequestError('Invalid pds service in DID doc')\n    }\n    const { data: delivery } = await pdsAgent.com.atproto.admin.sendEmail(\n      {\n        subject,\n        content,\n        recipientDid,\n        senderDid: this.cfg.service.did,\n      },\n      {\n        encoding: 'application/json',\n        ...(await this.createAuthHeaders(\n          serverInfo.did,\n          ids.ComAtprotoAdminSendEmail,\n        )),\n      },\n    )\n    if (!delivery.sent) {\n      throw new InvalidRequestError('Email was accepted but not sent')\n    }\n  }\n\n  async buildModerationQuery(\n    subjectType: 'account' | 'record',\n    createdByDids: string[],\n    isActionQuery: boolean,\n  ): Promise<(Partial<ReporterStatsResult> & { did: string })[]> {\n    if (!createdByDids.length) return []\n\n    const actionTypes = [\n      'tools.ozone.moderation.defs#modEventTakedown',\n      'tools.ozone.moderation.defs#modEventLabel',\n    ] as const\n\n    const countAll = () => {\n      return sql<number>`COUNT(*)`\n    }\n    const countAllDistinctBy = (ref: RawBuilder) => {\n      return sql<number>`COUNT(DISTINCT ${ref})`\n    }\n    const countTakedownsDistinctBy = (ref: RawBuilder) => {\n      return sql<number>`COUNT(DISTINCT ${ref}) FILTER (\n        WHERE actions.\"action\" = 'tools.ozone.moderation.defs#modEventTakedown'\n      )`\n    }\n    const countLabelsDistinctBy = (ref: RawBuilder) => {\n      return sql<number>`COUNT(DISTINCT ${ref}) FILTER (\n        WHERE actions.\"action\" = 'tools.ozone.moderation.defs#modEventLabel'\n      )`\n    }\n\n    const query = this.db.db\n      .selectFrom('moderation_event as reports')\n      .where(\n        'reports.action',\n        '=',\n        'tools.ozone.moderation.defs#modEventReport',\n      )\n      .where(\n        'reports.subjectUri',\n        subjectType === 'account' ? 'is' : 'is not',\n        null,\n      )\n      .where('reports.createdBy', 'in', createdByDids)\n      .select(['reports.createdBy as did'])\n\n    if (!isActionQuery) {\n      if (subjectType === 'account') {\n        return query\n          .select([\n            () => countAll().as('accountReportCount'),\n            (eb) =>\n              countAllDistinctBy(eb.ref('reports.subjectDid')).as(\n                'reportedAccountCount',\n              ),\n          ])\n          .groupBy('reports.createdBy')\n          .execute()\n      } else {\n        return query\n          .select([\n            () => countAll().as('recordReportCount'),\n            (eb) =>\n              countAllDistinctBy(eb.ref('reports.subjectUri')).as(\n                'reportedRecordCount',\n              ),\n          ])\n          .groupBy('reports.createdBy')\n          .execute()\n      }\n    }\n\n    if (subjectType === 'account') {\n      return query\n        .leftJoin('moderation_event as actions', (join) =>\n          join\n            .onRef('actions.subjectDid', '=', 'reports.subjectDid')\n            .on('actions.subjectUri', 'is', null)\n            .onRef('actions.createdAt', '>', 'reports.createdAt')\n            .on('actions.action', 'in', actionTypes),\n        )\n        .select([\n          (eb) =>\n            countTakedownsDistinctBy(eb.ref('actions.subjectDid')).as(\n              'takendownAccountCount',\n            ),\n          (eb) =>\n            countLabelsDistinctBy(eb.ref('actions.subjectDid')).as(\n              'labeledAccountCount',\n            ),\n        ])\n        .groupBy('reports.createdBy')\n        .execute()\n    } else {\n      return query\n        .leftJoin('moderation_event as actions', (join) =>\n          join\n            .onRef('actions.subjectDid', '=', 'reports.subjectDid')\n            .onRef('actions.subjectUri', '=', 'reports.subjectUri')\n            .onRef('actions.createdAt', '>', 'reports.createdAt')\n            .on('actions.action', 'in', actionTypes),\n        )\n        .select([\n          (eb) =>\n            countTakedownsDistinctBy(eb.ref('actions.subjectUri')).as(\n              'takendownRecordCount',\n            ),\n          (eb) =>\n            countLabelsDistinctBy(eb.ref('actions.subjectUri')).as(\n              'labeledRecordCount',\n            ),\n        ])\n        .groupBy('reports.createdBy')\n        .execute()\n    }\n  }\n\n  async getReporterStats(dids: string[]) {\n    const [accountReports, recordReports, accountActions, recordActions] =\n      await Promise.all([\n        this.buildModerationQuery('account', dids, false),\n        this.buildModerationQuery('record', dids, false),\n        this.buildModerationQuery('account', dids, true),\n        this.buildModerationQuery('record', dids, true),\n      ])\n\n    // Create a map to hold the aggregated stats for each `did`\n    const statsMap = new Map<string, ReporterStats>()\n\n    // Helper function to ensure a `did` entry exists in the map\n    const ensureDidEntry = (did: string) => {\n      if (!statsMap.has(did)) {\n        statsMap.set(did, {\n          did,\n          accountReportCount: 0,\n          recordReportCount: 0,\n          reportedAccountCount: 0,\n          reportedRecordCount: 0,\n          takendownAccountCount: 0,\n          takendownRecordCount: 0,\n          labeledAccountCount: 0,\n          labeledRecordCount: 0,\n        })\n      }\n      return statsMap.get(did)!\n    }\n\n    // Merge accountReports\n    for (const report of accountReports) {\n      const entry = ensureDidEntry(report.did)\n      entry.accountReportCount = report.accountReportCount ?? 0\n      entry.reportedAccountCount = report.reportedAccountCount ?? 0\n    }\n\n    // Merge recordReports\n    for (const report of recordReports) {\n      const entry = ensureDidEntry(report.did)\n      entry.recordReportCount = report.recordReportCount ?? 0\n      entry.reportedRecordCount = report.reportedRecordCount ?? 0\n    }\n\n    // Merge accountActions\n    for (const action of accountActions) {\n      const entry = ensureDidEntry(action.did)\n      entry.takendownAccountCount = action.takendownAccountCount ?? 0\n      entry.labeledAccountCount = action.labeledAccountCount ?? 0\n    }\n\n    // Merge recordActions\n    for (const action of recordActions) {\n      const entry = ensureDidEntry(action.did)\n      entry.takendownRecordCount = action.takendownRecordCount ?? 0\n      entry.labeledRecordCount = action.labeledRecordCount ?? 0\n    }\n\n    // Convert map values to an array and return\n    return Array.from(statsMap.values())\n  }\n\n  async getAccountTimeline(did: string) {\n    const { ref } = this.db.db.dynamic\n    // Without the subquery approach, pg tries to do the sort operation first which can be super expensive when a subjectDid has too many entries\n    const result = await this.db.db\n      .selectFrom(\n        this.db.db\n          .selectFrom('moderation_event')\n          .where('subjectDid', '=', did)\n          .select([\n            dateFromDbDatetime(ref('createdAt')).as('day'),\n            'subjectUri',\n            'action',\n            sql<number>`count(*)`.as('count'),\n          ])\n          .groupBy(['day', 'subjectUri', 'action'])\n          .as('results'),\n      )\n      .selectAll()\n      .orderBy('day', 'desc')\n      .execute()\n    return result\n  }\n}\n\nconst parseTags = (tags?: string[]) =>\n  tags\n    ?.map((tag) =>\n      tag\n        .split(/\\s*&&\\s*/g)\n        .map((subTag) => subTag.trim())\n        // Ignore invalid syntax (\"\", \"tag1 &&\", \"&& tag2\", \"tag1 && && tag2\", etc.)\n        .filter(Boolean),\n    )\n    // Ignore invalid items\n    .filter((subTags): subTags is [string, ...string[]] => subTags.length > 0)\n\nconst TAKEDOWNS = ['pds_takedown' as const, 'appview_takedown' as const]\n\nexport const TAKEDOWN_LABEL = '!takedown'\nexport const SUSPEND_LABEL = '!suspend'\n\nexport type TakedownSubjects = {\n  did: string\n  subjects: (RepoRef | RepoBlobRef | StrongRef)[]\n}\n\nexport type ReversalSubject = {\n  subject: ModSubject\n  reverseSuspend: boolean\n  reverseMute: boolean\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/profile.ts",
    "content": "import AtpAgent, { AppBskyLabelerDefs } from '@atproto/api'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { OzoneConfig } from '../config'\nimport {\n  REASONAPPEAL,\n  REASONMISLEADING,\n  REASONOTHER,\n  REASONRUDE,\n  REASONSEXUAL,\n  REASONSPAM,\n  REASONVIOLATION,\n} from '../lexicon/types/com/atproto/moderation/defs'\nimport { httpLogger } from '../logger'\n\n// Reverse mapping from new ozone namespaced reason types to old com.atproto namespaced reason types\nexport const NEW_TO_OLD_REASON_MAPPING: Record<string, string> = {\n  'tools.ozone.report.defs#reasonAppeal': REASONAPPEAL,\n  'tools.ozone.report.defs#reasonOther': REASONOTHER,\n\n  'tools.ozone.report.defs#reasonViolenceAnimal': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonViolenceThreats': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonViolenceGraphicContent': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonViolenceGlorification': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonViolenceExtremistContent': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonViolenceTrafficking': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonViolenceOther': REASONVIOLATION,\n\n  'tools.ozone.report.defs#reasonSexualAbuseContent': REASONSEXUAL,\n  'tools.ozone.report.defs#reasonSexualNCII': REASONSEXUAL,\n  'tools.ozone.report.defs#reasonSexualDeepfake': REASONSEXUAL,\n  'tools.ozone.report.defs#reasonSexualAnimal': REASONSEXUAL,\n  'tools.ozone.report.defs#reasonSexualUnlabeled': REASONSEXUAL,\n  'tools.ozone.report.defs#reasonSexualOther': REASONSEXUAL,\n\n  'tools.ozone.report.defs#reasonChildSafetyCSAM': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonChildSafetyGroom': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonChildSafetyPrivacy': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonChildSafetyHarassment': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonChildSafetyOther': REASONVIOLATION,\n\n  'tools.ozone.report.defs#reasonHarassmentTroll': REASONRUDE,\n  'tools.ozone.report.defs#reasonHarassmentTargeted': REASONRUDE,\n  'tools.ozone.report.defs#reasonHarassmentHateSpeech': REASONRUDE,\n  'tools.ozone.report.defs#reasonHarassmentDoxxing': REASONRUDE,\n  'tools.ozone.report.defs#reasonHarassmentOther': REASONRUDE,\n\n  'tools.ozone.report.defs#reasonMisleadingBot': REASONMISLEADING,\n  'tools.ozone.report.defs#reasonMisleadingImpersonation': REASONMISLEADING,\n  'tools.ozone.report.defs#reasonMisleadingSpam': REASONSPAM,\n  'tools.ozone.report.defs#reasonMisleadingScam': REASONMISLEADING,\n  'tools.ozone.report.defs#reasonMisleadingElections': REASONMISLEADING,\n  'tools.ozone.report.defs#reasonMisleadingOther': REASONMISLEADING,\n\n  'tools.ozone.report.defs#reasonRuleSiteSecurity': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonRuleProhibitedSales': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonRuleBanEvasion': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonRuleOther': REASONVIOLATION,\n\n  'tools.ozone.report.defs#reasonSelfHarmContent': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonSelfHarmED': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonSelfHarmStunts': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonSelfHarmSubstances': REASONVIOLATION,\n  'tools.ozone.report.defs#reasonSelfHarmOther': REASONVIOLATION,\n}\n\ninterface CacheEntry {\n  profile: AppBskyLabelerDefs.LabelerViewDetailed | null\n  timestamp: number\n}\n\nexport type ModerationServiceProfileCreator = () => ModerationServiceProfile\n\nexport class ModerationServiceProfile {\n  private cache: CacheEntry | null = null\n  private CACHE_TTL: number\n\n  constructor(\n    private cfg: OzoneConfig,\n    private appviewAgent: AtpAgent,\n    cacheTTL?: number,\n  ) {\n    this.CACHE_TTL = cacheTTL || cfg.service.serviceRecordCacheTTL\n  }\n\n  static creator(\n    cfg: OzoneConfig,\n    appviewAgent: AtpAgent,\n  ): ModerationServiceProfileCreator {\n    return () => new ModerationServiceProfile(cfg, appviewAgent)\n  }\n\n  async getProfile() {\n    const now = Date.now()\n\n    if (!this.cache || now - this.cache.timestamp > this.CACHE_TTL) {\n      try {\n        const { data } = await this.appviewAgent.app.bsky.labeler.getServices({\n          dids: [this.cfg.service.did],\n          detailed: true,\n        })\n\n        if (AppBskyLabelerDefs.isLabelerViewDetailed(data.views?.[0])) {\n          this.cache = {\n            profile: data.views[0],\n            timestamp: now,\n          }\n        }\n      } catch (e) {\n        // On error, fail open\n        httpLogger.error(`Failed to fetch labeler profile: ${e?.['message']}`)\n      }\n    }\n\n    return this.cache?.profile || null\n  }\n\n  async validateReasonType(reasonType: string): Promise<string> {\n    const profile = await this.getProfile()\n\n    if (!Array.isArray(profile?.reasonTypes)) {\n      return reasonType\n    }\n\n    const supportedReasonTypes = profile.reasonTypes\n\n    // Check if the reason type is directly supported\n    if (supportedReasonTypes.includes(reasonType)) {\n      return reasonType\n    }\n\n    // Allow new reason types only if they map to a supported old reason type\n    const mappedOldReason = NEW_TO_OLD_REASON_MAPPING[reasonType]\n    if (mappedOldReason && supportedReasonTypes.includes(mappedOldReason)) {\n      return reasonType\n    }\n\n    throw new InvalidRequestError(`Invalid reason type: ${reasonType}`)\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/status.ts",
    "content": "// This may require better organization but for now, just dumping functions here containing DB queries for moderation status\n\nimport { HOUR } from '@atproto/common'\nimport { AtUri } from '@atproto/syntax'\nimport { isAppealReport } from '../api/util'\nimport { Database } from '../db'\nimport { DatabaseSchema } from '../db/schema'\nimport { jsonb } from '../db/types'\nimport {\n  REVIEWCLOSED,\n  REVIEWESCALATED,\n  REVIEWNONE,\n  REVIEWOPEN,\n} from '../lexicon/types/tools/ozone/moderation/defs'\nimport { ModerationEventRow, ModerationSubjectStatusRow } from './types'\n\nconst getSubjectStatusForModerationEvent = ({\n  currentStatus,\n  action,\n  createdBy,\n  createdAt,\n  durationInHours,\n}: {\n  currentStatus?: ModerationSubjectStatusRow\n  action: string\n  createdBy: string\n  createdAt: string\n  durationInHours: number | null\n}): Partial<ModerationSubjectStatusRow> => {\n  const defaultReviewState = currentStatus\n    ? currentStatus.reviewState\n    : REVIEWNONE\n\n  switch (action) {\n    case 'tools.ozone.moderation.defs#modEventAcknowledge':\n      return {\n        lastReviewedBy: createdBy,\n        reviewState: REVIEWCLOSED,\n        lastReviewedAt: createdAt,\n      }\n    case 'tools.ozone.moderation.defs#modEventReport':\n      return {\n        reviewState: REVIEWOPEN,\n        lastReportedAt: createdAt,\n      }\n    case 'tools.ozone.moderation.defs#modEventEscalate':\n      return {\n        lastReviewedBy: createdBy,\n        reviewState: REVIEWESCALATED,\n        lastReviewedAt: createdAt,\n      }\n    case 'tools.ozone.moderation.defs#modEventReverseTakedown':\n      return {\n        lastReviewedBy: createdBy,\n        reviewState: REVIEWCLOSED,\n        takendown: false,\n        suspendUntil: null,\n        lastReviewedAt: createdAt,\n      }\n    case 'tools.ozone.moderation.defs#modEventUnmuteReporter':\n      return {\n        lastReviewedBy: createdBy,\n        muteReportingUntil: null,\n        // It's not likely to receive an unmute event that does not already have a status row\n        // but if it does happen, default to unnecessary\n        reviewState: defaultReviewState,\n        lastReviewedAt: createdAt,\n      }\n    case 'tools.ozone.moderation.defs#modEventUnmute':\n      return {\n        lastReviewedBy: createdBy,\n        muteUntil: null,\n        // It's not likely to receive an unmute event that does not already have a status row\n        // but if it does happen, default to unnecessary\n        reviewState: defaultReviewState,\n        lastReviewedAt: createdAt,\n      }\n    case 'tools.ozone.moderation.defs#modEventTakedown':\n      return {\n        // If we are doing a takedown, safe to move the item out of appealed state\n        ...(currentStatus?.appealed ? { appealed: false } : {}),\n        takendown: true,\n        lastReviewedBy: createdBy,\n        reviewState: REVIEWCLOSED,\n        lastReviewedAt: createdAt,\n        suspendUntil: durationInHours\n          ? new Date(Date.now() + durationInHours * HOUR).toISOString()\n          : null,\n      }\n    case 'tools.ozone.moderation.defs#modEventMuteReporter':\n      return {\n        lastReviewedBy: createdBy,\n        lastReviewedAt: createdAt,\n        // By default, mute for 24hrs\n        muteReportingUntil: new Date(\n          Date.now() + (durationInHours || 24) * HOUR,\n        ).toISOString(),\n        // It's not likely to receive a mute event on a subject that does not already have a status row\n        // but if it does happen, default to unnecessary\n        reviewState: defaultReviewState,\n      }\n    case 'tools.ozone.moderation.defs#modEventMute':\n      return {\n        lastReviewedBy: createdBy,\n        lastReviewedAt: createdAt,\n        // By default, mute for 24hrs\n        muteUntil: new Date(\n          Date.now() + (durationInHours || 24) * HOUR,\n        ).toISOString(),\n        // It's not likely to receive a mute event on a subject that does not already have a status row\n        // but if it does happen, default to unnecessary\n        reviewState: defaultReviewState,\n      }\n    case 'tools.ozone.moderation.defs#modEventComment':\n      return {\n        lastReviewedBy: createdBy,\n        lastReviewedAt: createdAt,\n        reviewState: defaultReviewState,\n      }\n    case 'tools.ozone.moderation.defs#modEventTag':\n      return { tags: [], reviewState: defaultReviewState }\n    case 'tools.ozone.moderation.defs#modEventResolveAppeal':\n      return {\n        appealed: false,\n      }\n    case 'tools.ozone.moderation.defs#ageAssuranceEvent':\n    case 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent':\n    case 'tools.ozone.moderation.defs#ageAssurancePurgeEvent':\n      return {\n        reviewState: defaultReviewState,\n      }\n    default:\n      return {}\n  }\n}\n\nconst hostingEvents = [\n  'tools.ozone.moderation.defs#accountEvent',\n  'tools.ozone.moderation.defs#identityEvent',\n  'tools.ozone.moderation.defs#recordEvent',\n]\n\nconst getSubjectStatusForRecordEvent = ({\n  event,\n  currentStatus,\n}: {\n  event: ModerationEventRow\n  currentStatus?: ModerationSubjectStatusRow\n}): Partial<ModerationSubjectStatusRow> => {\n  const timestamp =\n    typeof event.meta?.timestamp === 'string'\n      ? event.meta?.timestamp\n      : event.createdAt\n\n  if (event.action === 'tools.ozone.moderation.defs#recordEvent') {\n    if (event.meta?.op === 'delete') {\n      return {\n        hostingStatus: 'deleted',\n        hostingDeletedAt: timestamp,\n      }\n    } else if (event.meta?.op === 'update') {\n      return {\n        hostingStatus: 'active',\n        hostingUpdatedAt: timestamp,\n      }\n    }\n    return {}\n  }\n\n  if (event.action === 'tools.ozone.moderation.defs#accountEvent') {\n    const status: Partial<ModerationSubjectStatusRow> = {\n      hostingUpdatedAt: timestamp,\n    }\n\n    if (event.meta?.status) {\n      status.hostingStatus = `${event.meta?.status}`\n    }\n\n    if (event.meta?.status === 'deleted') {\n      status.hostingDeletedAt = timestamp\n    } else if (event.meta?.status === 'deactivated') {\n      status.hostingDeactivatedAt = timestamp\n    } else {\n      // When deactivated accounts are re-activated, we receive the event with just the active flag set to true\n      // so we want to make sure that the hostingStatus is not set to an outdated value\n      if (\n        currentStatus?.hostingStatus === 'deactivated' &&\n        event.meta?.active\n      ) {\n        status.hostingStatus = 'active'\n        status.hostingReactivatedAt = timestamp\n      }\n    }\n\n    return status\n  }\n\n  if (event.action === 'tools.ozone.moderation.defs#identityEvent') {\n    const status: Partial<ModerationSubjectStatusRow> = {\n      hostingUpdatedAt: timestamp,\n    }\n\n    if (event.meta?.tombstone) {\n      status.hostingStatus = 'tombstoned'\n      status.hostingDeletedAt = timestamp\n    }\n\n    return status\n  }\n\n  return {}\n}\n\nexport const moderationSubjectStatusQueryBuilder = (db: DatabaseSchema) => {\n  // @NOTE: Using select() instead of selectAll() below because the materialized\n  // views might be incomplete, and we don't want the null `did` columns to\n  // interfere with the (never null) `did` column from the\n  // `moderation_subject_status` table in the results\n  return db\n    .selectFrom('moderation_subject_status')\n    .selectAll('moderation_subject_status')\n    .leftJoin('account_events_stats', (join) =>\n      join.onRef(\n        'moderation_subject_status.did',\n        '=',\n        'account_events_stats.subjectDid',\n      ),\n    )\n    .select([\n      'account_events_stats.takedownCount',\n      'account_events_stats.suspendCount',\n      'account_events_stats.escalateCount',\n      'account_events_stats.reportCount',\n      'account_events_stats.appealCount',\n    ])\n    .leftJoin('account_record_events_stats', (join) =>\n      join.onRef(\n        'moderation_subject_status.did',\n        '=',\n        'account_record_events_stats.subjectDid',\n      ),\n    )\n    .select([\n      'account_record_events_stats.totalReports',\n      'account_record_events_stats.reportedCount',\n      'account_record_events_stats.escalatedCount',\n      'account_record_events_stats.appealedCount',\n    ])\n    .leftJoin('account_record_status_stats', (join) =>\n      join.onRef(\n        'moderation_subject_status.did',\n        '=',\n        'account_record_status_stats.did',\n      ),\n    )\n    .select([\n      'account_record_status_stats.subjectCount',\n      'account_record_status_stats.pendingCount',\n      'account_record_status_stats.processedCount',\n      'account_record_status_stats.takendownCount',\n    ])\n    .leftJoin('account_strike', (join) =>\n      join.onRef('moderation_subject_status.did', '=', 'account_strike.did'),\n    )\n    .select([\n      'account_strike.activeStrikeCount as strikeCount',\n      'account_strike.totalStrikeCount',\n      'account_strike.firstStrikeAt',\n      'account_strike.lastStrikeAt',\n    ])\n}\n\n// Based on a given moderation action event, this function will update the moderation status of the subject\n// If there's no existing status, it will create one\n// If the action event does not affect the status, it will do nothing\nexport const adjustModerationSubjectStatus = async (\n  db: Database,\n  moderationEvent: ModerationEventRow,\n  blobCids?: string[],\n): Promise<ModerationSubjectStatusRow | null> => {\n  const {\n    action,\n    subjectDid,\n    subjectUri,\n    subjectCid,\n    createdBy,\n    meta,\n    addedTags,\n    removedTags,\n    comment,\n    createdAt,\n  } = moderationEvent\n\n  // If subjectUri exists, it's not a repoRef so pass along the uri to get identifier back\n  const identifier = getStatusIdentifierFromSubject(subjectUri || subjectDid)\n\n  db.assertTransaction()\n\n  const now = new Date().toISOString()\n  const currentStatus = await db.db\n    .selectFrom('moderation_subject_status')\n    .where('did', '=', identifier.did)\n    .where('recordPath', '=', identifier.recordPath)\n    // Make sure we respect other updates that may be happening at the same time\n    .forUpdate()\n    .selectAll()\n    .executeTakeFirst()\n\n  if (hostingEvents.includes(action)) {\n    const newStatus = getSubjectStatusForRecordEvent({\n      event: moderationEvent,\n      currentStatus,\n    })\n    if (!Object.keys(newStatus).length) {\n      return currentStatus || null\n    }\n\n    const status = await db.db\n      .insertInto('moderation_subject_status')\n      .values({\n        ...identifier,\n        ...newStatus,\n        // newStatus doesn't contain a reviewState or takendown so in case this is a new entry\n        // we need to set a default values so that the insert doesn't fail\n        reviewState: currentStatus ? currentStatus.reviewState : REVIEWNONE,\n        // @TODO: should we try to update this based on status property of account event?\n        // For now we're the only one emitting takedowns so i don't think it makes too much of a difference\n        takendown: currentStatus ? currentStatus.takendown : false,\n        ageAssuranceState: currentStatus\n          ? currentStatus.ageAssuranceState\n          : 'unknown',\n        createdAt: now,\n        updatedAt: now,\n      })\n      .onConflict((oc) =>\n        oc.constraint('moderation_status_unique_idx').doUpdateSet({\n          ...newStatus,\n          updatedAt: now,\n        }),\n      )\n      .returningAll()\n      .executeTakeFirst()\n\n    return status || null\n  }\n\n  // If reporting is muted for this reporter, we don't want to update the subject status\n  if (meta?.isReporterMuted) {\n    return currentStatus || null\n  }\n\n  const isAppealEvent =\n    action === 'tools.ozone.moderation.defs#modEventReport' &&\n    meta?.reportType &&\n    isAppealReport(`${meta.reportType}`)\n\n  const subjectStatus = getSubjectStatusForModerationEvent({\n    currentStatus,\n    action,\n    createdBy,\n    createdAt,\n    durationInHours: moderationEvent.durationInHours,\n  })\n\n  if (\n    currentStatus?.reviewState === REVIEWESCALATED &&\n    subjectStatus.reviewState !== REVIEWCLOSED\n  ) {\n    // If the current status is escalated only allow incoming events to move the state to\n    // reviewClosed because escalated subjects should never move to any other state\n    subjectStatus.reviewState = REVIEWESCALATED\n  }\n\n  if (currentStatus && subjectStatus.reviewState === REVIEWNONE) {\n    // reviewNone is ONLY allowed when there is no current status\n    // If there is a current status, it should not be allowed to move back to reviewNone\n    subjectStatus.reviewState = currentStatus.reviewState\n  }\n\n  // Set these because we don't want to override them if they're already set\n  const defaultData = {\n    comment: null,\n    // Defaulting reviewState to open for any event may not be the desired behavior.\n    // For instance, if a subject never had any event and we just want to leave a comment to keep an eye on it\n    // that shouldn't mean we want to review the subject\n    reviewState: REVIEWNONE,\n    recordCid: subjectCid || null,\n    ageAssuranceState: currentStatus?.ageAssuranceState || 'unknown',\n  }\n  const newStatus = {\n    ...defaultData,\n    ...subjectStatus,\n  }\n\n  if (\n    action === 'tools.ozone.moderation.defs#modEventPriorityScore' &&\n    typeof meta?.priorityScore === 'number'\n  ) {\n    newStatus.priorityScore = meta?.priorityScore\n    subjectStatus.priorityScore = meta?.priorityScore\n  }\n\n  if (\n    action === 'tools.ozone.moderation.defs#modEventReverseTakedown' &&\n    !subjectStatus.takendown\n  ) {\n    newStatus.takendown = false\n    subjectStatus.takendown = false\n  }\n\n  if (isAppealEvent) {\n    newStatus.appealed = true\n    subjectStatus.appealed = true\n    newStatus.lastAppealedAt = createdAt\n    subjectStatus.lastAppealedAt = createdAt\n    // Set reviewState to escalated when appeal events are emitted\n    subjectStatus.reviewState = REVIEWESCALATED\n    newStatus.reviewState = REVIEWESCALATED\n  }\n\n  if (\n    action === 'tools.ozone.moderation.defs#modEventResolveAppeal' &&\n    subjectStatus.appealed\n  ) {\n    newStatus.appealed = false\n    subjectStatus.appealed = false\n  }\n\n  if (\n    action === 'tools.ozone.moderation.defs#modEventComment' &&\n    meta?.sticky\n  ) {\n    newStatus.comment = comment\n    subjectStatus.comment = comment\n  }\n\n  if (action === 'tools.ozone.moderation.defs#modEventTag') {\n    let tags = currentStatus?.tags || []\n    if (addedTags?.length) {\n      tags = tags.concat(addedTags)\n    }\n    if (removedTags?.length) {\n      tags = tags.filter((tag) => !removedTags.includes(tag))\n    }\n    newStatus.tags = jsonb([...new Set(tags)]) as unknown as string[]\n    subjectStatus.tags = newStatus.tags\n  }\n\n  if (action === 'tools.ozone.moderation.defs#ageAssuranceEvent') {\n    // Only when the last update was made by an admin AND state was set to reset user event can override final state\n    if (\n      currentStatus?.ageAssuranceUpdatedBy !== 'admin' ||\n      currentStatus?.ageAssuranceState === 'reset'\n    ) {\n      if (typeof meta?.status === 'string') {\n        newStatus.ageAssuranceState = meta.status\n        subjectStatus.ageAssuranceState = meta.status\n        newStatus.ageAssuranceUpdatedBy = 'user'\n        subjectStatus.ageAssuranceUpdatedBy = 'user'\n      }\n    }\n  }\n\n  if (action === 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent') {\n    if (typeof meta?.status === 'string') {\n      newStatus.ageAssuranceState = meta.status\n      subjectStatus.ageAssuranceState = meta.status\n      newStatus.ageAssuranceUpdatedBy = 'admin'\n      subjectStatus.ageAssuranceUpdatedBy = 'admin'\n    }\n  }\n\n  if (action === 'tools.ozone.moderation.defs#ageAssurancePurgeEvent') {\n    newStatus.ageAssuranceState = 'unknown'\n    subjectStatus.ageAssuranceState = 'unknown'\n    newStatus.ageAssuranceUpdatedBy = null\n    subjectStatus.ageAssuranceUpdatedBy = null\n  }\n\n  if (blobCids?.length) {\n    const newBlobCids = jsonb(\n      blobCids,\n    ) as unknown as ModerationSubjectStatusRow['blobCids']\n    newStatus.blobCids = newBlobCids\n    subjectStatus.blobCids = newBlobCids\n  }\n\n  const insertQuery = db.db\n    .insertInto('moderation_subject_status')\n    .values({\n      ...identifier,\n      ...newStatus,\n      createdAt: now,\n      updatedAt: now,\n    } as ModerationSubjectStatusRow)\n    .onConflict((oc) =>\n      oc.constraint('moderation_status_unique_idx').doUpdateSet({\n        ...subjectStatus,\n        updatedAt: now,\n      }),\n    )\n\n  const status = await insertQuery.returningAll().executeTakeFirst()\n  return status || null\n}\n\nexport const getStatusIdentifierFromSubject = (\n  subject: string | AtUri,\n): { did: string; recordPath: string } => {\n  const isSubjectString = typeof subject === 'string'\n  if (isSubjectString && subject.startsWith('did:')) {\n    return {\n      did: subject,\n      recordPath: '',\n    }\n  }\n\n  if (isSubjectString && !subject.startsWith('at://')) {\n    throw new Error('Subject is neither a did nor an at-uri')\n  }\n\n  const uri = isSubjectString ? new AtUri(subject) : subject\n  return {\n    did: uri.host,\n    recordPath: `${uri.collection}/${uri.rkey}`,\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/strike.ts",
    "content": "import { Database } from '../db'\n\nexport type StrikeServiceCreator = (db: Database) => StrikeService\n\nexport class StrikeService {\n  constructor(private db: Database) {}\n\n  static creator() {\n    return (db: Database) => {\n      return new StrikeService(db)\n    }\n  }\n\n  /**\n   * Update the strike count in account_strike table\n   */\n  async updateSubjectStrikeCount(subjectDid: string): Promise<void> {\n    const now = new Date().toISOString()\n\n    // This should not incur too many rows since we tend to do permanent takedown on relatively low strike count\n    // and we have a very specific index to support this query\n    const events = await this.db.db\n      .selectFrom('moderation_event')\n      .where('subjectDid', '=', subjectDid)\n      .where('strikeCount', '<>', 0)\n      .select(['strikeCount', 'strikeExpiresAt', 'createdAt'])\n      .orderBy('createdAt', 'asc')\n      .execute()\n\n    if (!events.length) {\n      return\n    }\n\n    let activeStrikeCount = 0\n    let totalStrikeCount = 0\n\n    const firstStrikeAt = events[0].createdAt\n    const lastStrikeAt = events[events.length - 1].createdAt\n\n    for (const event of events) {\n      const strikeCount = event.strikeCount || 0\n      totalStrikeCount += strikeCount\n\n      // Count as active if not expired\n      const isActive =\n        event.strikeExpiresAt === null || event.strikeExpiresAt > now\n      if (isActive) {\n        activeStrikeCount += strikeCount\n      }\n    }\n\n    await this.db.db\n      .insertInto('account_strike')\n      .values({\n        did: subjectDid,\n        activeStrikeCount,\n        totalStrikeCount,\n        firstStrikeAt,\n        lastStrikeAt,\n      })\n      .onConflict((oc) =>\n        oc.column('did').doUpdateSet({\n          activeStrikeCount,\n          totalStrikeCount,\n          firstStrikeAt,\n          lastStrikeAt,\n        }),\n      )\n      .execute()\n  }\n\n  /**\n   * Get distinct subjects with expired strikes since a given timestamp\n   * Used by the strike expiry processor to find accounts that need strike count updates\n   */\n  async getExpiredStrikeSubjects(\n    afterTimestamp?: string,\n  ): Promise<Array<{ subjectDid: string }>> {\n    const now = new Date().toISOString()\n\n    let query = this.db.db\n      .selectFrom('moderation_event')\n      .where('strikeExpiresAt', 'is not', null)\n      .where('strikeExpiresAt', '<=', now)\n      .where('strikeCount', '<>', 0)\n      .select('subjectDid')\n      .distinct()\n\n    // Only process strikes that expired since the last run\n    if (afterTimestamp) {\n      query = query.where('strikeExpiresAt', '>=', afterTimestamp)\n    }\n\n    return await query.execute()\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/subject.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport * as ChatBskyConvoDefs from '../lexicon/types/chat/bsky/convo/defs'\nimport { RepoRef, isRepoRef } from '../lexicon/types/com/atproto/admin/defs'\nimport { InputSchema as ReportInput } from '../lexicon/types/com/atproto/moderation/createReport'\nimport * as ComAtprotoRepoStrongRef from '../lexicon/types/com/atproto/repo/strongRef'\nimport { InputSchema as ActionInput } from '../lexicon/types/tools/ozone/moderation/emitEvent'\nimport { $Typed, asPredicate } from '../lexicon/util'\nimport { ModerationEventRow, ModerationSubjectStatusRow } from './types'\n\ntype SubjectInput = ReportInput['subject'] | ActionInput['subject']\n\ntype StrongRef = ComAtprotoRepoStrongRef.Main\nconst isStrongRef = asPredicate(ComAtprotoRepoStrongRef.validateMain)\n\ntype MessageRef = ChatBskyConvoDefs.MessageRef\nconst isValidMessageRef = asPredicate(ChatBskyConvoDefs.validateMessageRef)\n\nconst isMessageRefWithoutConvoId = (\n  subject: unknown,\n): subject is $Typed<Omit<MessageRef, 'convoId'> & { convoId?: string }> =>\n  subject != null &&\n  typeof subject === 'object' &&\n  isValidMessageRef({ convoId: '', ...subject })\n\nexport const subjectFromInput = (\n  subject: SubjectInput,\n  blobs?: string[],\n): ModSubject => {\n  if (isRepoRef(subject)) {\n    if (blobs && blobs.length > 0) {\n      throw new InvalidRequestError('Blobs do not apply to repo subjects')\n    }\n    return new RepoSubject(subject.did)\n  }\n  if (isStrongRef(subject)) {\n    return new RecordSubject(subject.uri, subject.cid, blobs)\n  }\n  // @NOTE #messageRef is not a report input for com.atproto.moderation.createReport.\n  // we are taking advantage of the open union in order for bsky.chat to interoperate here.\n  if (isValidMessageRef(subject)) {\n    return new MessageSubject(subject.did, subject.convoId, subject.messageId)\n  }\n  // @TODO we should start to require subject.convoId is a string in order to properly validate\n  // the #messageRef. temporarily allowing it to be optional as a stopgap for rollout.\n  // The next \"if\" can be removed once convoId is consistently provided.\n  if (isMessageRefWithoutConvoId(subject)) {\n    return new MessageSubject(\n      subject.did,\n      subject.convoId ?? '',\n      subject.messageId,\n    )\n  }\n\n  throw new InvalidRequestError('Invalid subject')\n}\n\nexport const subjectFromEventRow = (row: ModerationEventRow): ModSubject => {\n  if (\n    row.subjectType === 'com.atproto.repo.strongRef' &&\n    row.subjectUri &&\n    row.subjectCid\n  ) {\n    return new RecordSubject(\n      row.subjectUri,\n      row.subjectCid,\n      row.subjectBlobCids ?? [],\n    )\n  } else if (\n    row.subjectType === 'chat.bsky.convo.defs#messageRef' &&\n    row.subjectMessageId\n  ) {\n    const convoId =\n      typeof row.meta?.['convoId'] === 'string' ? row.meta['convoId'] : ''\n    return new MessageSubject(row.subjectDid, convoId, row.subjectMessageId)\n  } else {\n    return new RepoSubject(row.subjectDid)\n  }\n}\n\nexport const subjectFromStatusRow = (\n  row: ModerationSubjectStatusRow,\n): ModSubject => {\n  if (row.recordPath && row.recordCid) {\n    // Not too intuitive but the recordpath is basically <collection>/<rkey>\n    // which is what the last 2 params of .make() arguments are\n    const uri = AtUri.make(row.did, ...row.recordPath.split('/')).toString()\n    return new RecordSubject(uri.toString(), row.recordCid, row.blobCids ?? [])\n  } else {\n    return new RepoSubject(row.did)\n  }\n}\n\ntype SubjectInfo = {\n  subjectType:\n    | 'com.atproto.admin.defs#repoRef'\n    | 'com.atproto.repo.strongRef'\n    | 'chat.bsky.convo.defs#messageRef'\n  subjectDid: string\n  subjectUri: string | null\n  subjectCid: string | null\n  subjectBlobCids: string[] | null\n  subjectMessageId: string | null\n  meta: Record<string, string | undefined> | null\n}\n\nexport interface ModSubject {\n  did: string\n  recordPath: string | undefined\n  blobCids?: string[]\n  isRepo(): this is RepoSubject\n  isRecord(): this is RecordSubject\n  isMessage(): this is MessageSubject\n  info(): SubjectInfo\n  lex(): $Typed<RepoRef> | $Typed<StrongRef> | $Typed<MessageRef>\n}\n\nexport class RepoSubject implements ModSubject {\n  blobCids = undefined\n  recordPath = undefined\n  constructor(public did: string) {}\n  isRepo(): this is RepoSubject {\n    return true\n  }\n  isRecord(): this is RecordSubject {\n    return false\n  }\n  isMessage(): this is MessageSubject {\n    return false\n  }\n  info() {\n    return {\n      subjectType: 'com.atproto.admin.defs#repoRef' as const,\n      subjectDid: this.did,\n      subjectUri: null,\n      subjectCid: null,\n      subjectBlobCids: null,\n      subjectMessageId: null,\n      meta: null,\n    }\n  }\n  lex(): $Typed<RepoRef> {\n    return {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: this.did,\n    }\n  }\n}\n\nexport class RecordSubject implements ModSubject {\n  parsedUri: AtUri\n  did: string\n  recordPath: string\n  constructor(\n    public uri: string,\n    public cid: string,\n    public blobCids?: string[],\n  ) {\n    this.parsedUri = new AtUri(uri)\n    this.did = this.parsedUri.hostname\n    this.recordPath = `${this.parsedUri.collection}/${this.parsedUri.rkey}`\n  }\n  isRepo(): this is RepoSubject {\n    return false\n  }\n  isRecord(): this is RecordSubject {\n    return true\n  }\n  isMessage(): this is MessageSubject {\n    return false\n  }\n  info() {\n    return {\n      subjectType: 'com.atproto.repo.strongRef' as const,\n      subjectDid: this.did,\n      subjectUri: this.uri,\n      subjectCid: this.cid,\n      subjectBlobCids: this.blobCids ?? [],\n      subjectMessageId: null,\n      meta: null,\n    }\n  }\n  lex(): $Typed<StrongRef> {\n    return {\n      $type: 'com.atproto.repo.strongRef',\n      uri: this.uri,\n      cid: this.cid,\n    }\n  }\n}\n\nexport class MessageSubject implements ModSubject {\n  blobCids = undefined\n  recordPath = undefined\n  constructor(\n    public did: string,\n    public convoId: string,\n    public messageId: string,\n  ) {}\n  isRepo(): this is RepoSubject {\n    return false\n  }\n  isRecord(): this is RecordSubject {\n    return false\n  }\n  isMessage(): this is MessageSubject {\n    return true\n  }\n  info() {\n    return {\n      subjectType: 'chat.bsky.convo.defs#messageRef' as const,\n      subjectDid: this.did,\n      subjectUri: null,\n      subjectCid: null,\n      subjectBlobCids: null,\n      subjectMessageId: this.messageId,\n      meta: { convoId: this.convoId || undefined },\n    }\n  }\n  lex(): $Typed<MessageRef> {\n    return {\n      $type: 'chat.bsky.convo.defs#messageRef',\n      did: this.did,\n      convoId: this.convoId,\n      messageId: this.messageId,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/types.ts",
    "content": "import { type Selectable } from 'kysely'\nimport { ModerationEvent } from '../db/schema/moderation_event'\nimport { ModerationSubjectStatus } from '../db/schema/moderation_subject_status'\nimport { ModEventView } from '../lexicon/types/tools/ozone/moderation/defs'\nimport { ModSubject } from './subject'\n\nexport type ModerationEventRow = Selectable<ModerationEvent>\nexport type ReversibleModerationEvent = Pick<\n  ModerationEventRow,\n  'createdBy' | 'comment' | 'action'\n> & {\n  createdAt?: Date\n  subject: ModSubject\n}\n\nexport type ModerationEventRowWithHandle = ModerationEventRow & {\n  subjectHandle?: string | null\n  creatorHandle?: string | null\n}\nexport type ModerationSubjectStatusRow = Selectable<ModerationSubjectStatus>\nexport type ModerationSubjectStatusRowWithStats = ModerationSubjectStatusRow & {\n  // account_events_stats\n  takedownCount: number | null\n  suspendCount: number | null\n  escalateCount: number | null\n  reportCount: number | null\n  appealCount: number | null\n\n  // account_record_events_stats\n  totalReports: number | null\n  reportedCount: number | null\n  escalatedCount: number | null\n  appealedCount: number | null\n\n  // account_record_status_stats\n  subjectCount: number | null\n  pendingCount: number | null\n  processedCount: number | null\n  takendownCount: number | null\n\n  // account_strike\n  strikeCount: number | null\n  totalStrikeCount: number | null\n  firstStrikeAt: string | null\n  lastStrikeAt: string | null\n}\n\nexport type ModerationSubjectStatusRowWithHandle =\n  ModerationSubjectStatusRowWithStats & { handle: string | null }\n\nexport type ModEventType = ModEventView['event']\n\ntype AccountHostingView = {\n  $type: 'tools.ozone.moderation.defs#accountHosting'\n  status: 'active' | 'takendown' | 'suspended' | 'deleted' | 'deactivated'\n  createdAt?: Date\n  updatedAt?: Date\n  deletedAt?: Date\n  deactivatedAt?: Date\n  reactivatedAt?: Date\n}\n\ntype RecordHostingView = {\n  $type: 'tools.ozone.moderation.defs#recordHosting'\n  status: 'active' | 'deleted'\n  createdAt?: Date\n  updatedAt?: Date\n  deletedAt?: Date\n}\n\nexport type ModerationSubjectHostingView =\n  | AccountHostingView\n  | RecordHostingView\n\nexport type ReporterStats = {\n  did: string\n  accountReportCount: number\n  recordReportCount: number\n  reportedAccountCount: number\n  reportedRecordCount: number\n  takendownAccountCount: number\n  takendownRecordCount: number\n  labeledAccountCount: number\n  labeledRecordCount: number\n}\n\nexport type ReporterStatsResult = {\n  accountReportCount?: number\n  recordReportCount?: number\n  reportedAccountCount?: number\n  reportedRecordCount?: number\n  takendownAccountCount?: number\n  takendownRecordCount?: number\n  labeledAccountCount?: number\n  labeledRecordCount?: number\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/util.ts",
    "content": "import net from 'node:net'\nimport { sql } from 'kysely'\nimport AtpAgent from '@atproto/api'\nimport { cborEncode, noUndefinedVals } from '@atproto/common'\nimport { Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { LabelRow } from '../db/schema/label'\nimport { DbRef } from '../db/types'\nimport { Label } from '../lexicon/types/com/atproto/label/defs'\n\nexport type SignedLabel = Label & { sig: Uint8Array }\n\nexport const formatLabel = (row: LabelRow): Label => {\n  return noUndefinedVals({\n    ver: 1,\n    src: row.src,\n    uri: row.uri,\n    cid: row.cid === '' ? undefined : row.cid,\n    val: row.val,\n    neg: row.neg === true ? true : undefined,\n    cts: row.cts,\n    exp: row.exp ?? undefined,\n    sig: row.sig ? new Uint8Array(row.sig) : undefined,\n  } satisfies Label) as unknown as Label\n}\n\nexport const formatLabelRow = (\n  label: Label,\n  signingKeyId?: number,\n): Omit<LabelRow, 'id'> => {\n  return {\n    src: label.src,\n    uri: label.uri,\n    cid: label.cid ?? '',\n    val: label.val,\n    neg: !!label.neg,\n    cts: label.cts,\n    exp: label.exp ?? null,\n    sig: label.sig ? Buffer.from(label.sig) : null,\n    signingKeyId: signingKeyId ?? null,\n  }\n}\n\nexport const signLabel = async (\n  label: Label,\n  signingKey: Keypair,\n): Promise<SignedLabel> => {\n  const { ver, src, uri, cid, val, neg, cts, exp } = label\n  // @TODO cborEncode now ignores undefined properties, so we might not need to\n  // reformat the label here. We might want to consider this if we ever re-visit\n  // the logic below:\n  const reformatted = noUndefinedVals({\n    ver: ver ?? 1,\n    src,\n    uri,\n    cid,\n    val,\n    neg: neg === true ? true : undefined,\n    cts,\n    exp,\n  } satisfies Label) as unknown as Label\n\n  const bytes = cborEncode(reformatted)\n  const sig = await signingKey.sign(bytes)\n  return {\n    ...reformatted,\n    sig,\n  }\n}\n\nexport const isSafeUrl = (url: URL) => {\n  if (url.protocol !== 'https:') return false\n  if (!url.hostname || url.hostname === 'localhost') return false\n  if (net.isIP(url.hostname) !== 0) return false\n  return true\n}\n\nexport const getPdsAgentForRepo = async (\n  idResolver: IdResolver,\n  did: string,\n  devMode?: boolean,\n) => {\n  const { pds } = await idResolver.did.resolveAtprotoData(did)\n  const url = new URL(pds)\n  if (!devMode && !isSafeUrl(url)) {\n    return { url, agent: null }\n  }\n\n  return { url, agent: new AtpAgent({ service: url }) }\n}\n\nexport const dateFromDatetime = (datetime: Date) => {\n  const [date] = datetime.toISOString().split('T')\n  return date\n}\n\nexport const dateFromDbDatetime = (dateRef: DbRef) => {\n  return sql<string>`SPLIT_PART(${dateRef}, 'T', 1)`\n}\n"
  },
  {
    "path": "packages/ozone/src/mod-service/views.ts",
    "content": "import { sql } from 'kysely'\nimport {\n  AppBskyActorDefs,\n  AtpAgent,\n  ComAtprotoRepoGetRecord,\n} from '@atproto/api'\nimport { chunkArray, dedupeStrs } from '@atproto/common'\nimport { Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { BlobRef } from '@atproto/lexicon'\nimport { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'\nimport { Database } from '../db'\nimport { LabelRow } from '../db/schema/label'\nimport { ids } from '../lexicon/lexicons'\nimport { FeedViewPost } from '../lexicon/types/app/bsky/feed/defs'\nimport { AccountView } from '../lexicon/types/com/atproto/admin/defs'\nimport {\n  Label,\n  validateSelfLabels,\n} from '../lexicon/types/com/atproto/label/defs'\nimport { OutputSchema as ReportOutput } from '../lexicon/types/com/atproto/moderation/createReport'\nimport { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs'\nimport {\n  BlobView,\n  ModEventView,\n  ModEventViewDetail,\n  RecordView,\n  RecordViewDetail,\n  RepoView,\n  SubjectStatusView,\n  isAccountEvent,\n  isAgeAssuranceEvent,\n  isAgeAssuranceOverrideEvent,\n  isIdentityEvent,\n  isModEventAcknowledge,\n  isModEventComment,\n  isModEventEmail,\n  isModEventEscalate,\n  isModEventLabel,\n  isModEventMute,\n  isModEventMuteReporter,\n  isModEventPriorityScore,\n  isModEventReport,\n  isModEventReverseTakedown,\n  isModEventTag,\n  isModEventTakedown,\n  isRecordEvent,\n  isScheduleTakedownEvent,\n} from '../lexicon/types/tools/ozone/moderation/defs'\nimport { Un$Typed, asPredicate } from '../lexicon/util'\nimport { dbLogger, httpLogger } from '../logger'\nimport { ParsedLabelers } from '../util'\nimport { moderationSubjectStatusQueryBuilder } from './status'\nimport { subjectFromEventRow, subjectFromStatusRow } from './subject'\nimport {\n  ModerationEventRowWithHandle,\n  ModerationSubjectStatusRowWithHandle,\n} from './types'\nimport { formatLabel, getPdsAgentForRepo, signLabel } from './util'\n\nconst isValidSelfLabels = asPredicate(validateSelfLabels)\n\nconst ifString = (val: unknown): string | undefined =>\n  typeof val === 'string' ? val : undefined\nconst ifBoolean = (val: unknown): boolean | undefined =>\n  typeof val === 'boolean' ? val : undefined\nconst ifNumber = (val: unknown): number | undefined =>\n  typeof val === 'number' ? val : undefined\n\nexport type AuthHeaders = {\n  headers: {\n    authorization: string\n    'atproto-accept-labelers'?: string\n  }\n}\n\nexport class ModerationViews {\n  constructor(\n    private db: Database,\n    private signingKey: Keypair,\n    private signingKeyId: number,\n    private appviewAgent: AtpAgent,\n    private appviewAuth: (method: string) => Promise<AuthHeaders>,\n    public idResolver: IdResolver,\n    public devMode?: boolean,\n  ) {}\n\n  async getAccoutInfosByDid(dids: string[]): Promise<Map<string, AccountView>> {\n    if (dids.length === 0) return new Map()\n    const auth = await this.appviewAuth(ids.ComAtprotoAdminGetAccountInfos)\n    if (!auth) return new Map()\n    try {\n      const res = await this.appviewAgent.com.atproto.admin.getAccountInfos(\n        {\n          dids: dedupeStrs(dids),\n        },\n        auth,\n      )\n      return res.data.infos.reduce((acc, cur) => {\n        return acc.set(cur.did, cur)\n      }, new Map<string, AccountView>())\n    } catch (err) {\n      httpLogger.error(\n        { err, dids },\n        'failed to resolve account infos from appview',\n      )\n      return new Map()\n    }\n  }\n\n  async repos(dids: string[]): Promise<Map<string, RepoView>> {\n    if (dids.length === 0) return new Map()\n    const [infos, subjectStatuses] = await Promise.all([\n      this.getAccoutInfosByDid(dids),\n      this.getSubjectStatus(dids),\n    ])\n\n    return dids.reduce((acc, did) => {\n      const info = infos.get(did)\n      if (!info) return acc\n      const status = subjectStatuses.get(did)\n      return acc.set(did, {\n        // No email or invite info on appview\n        did,\n        handle: info.handle,\n        relatedRecords: info.relatedRecords ?? [],\n        indexedAt: info.indexedAt,\n        moderation: {\n          subjectStatus: status ? this.formatSubjectStatus(status) : undefined,\n        },\n      })\n    }, new Map<string, RepoView>())\n  }\n\n  formatEvent(row: ModerationEventRowWithHandle): Un$Typed<ModEventView> {\n    const eventView: Un$Typed<ModEventView> = {\n      id: row.id,\n      event: {\n        $type: row.action,\n        comment: row.comment ?? undefined,\n      },\n      subject: subjectFromEventRow(row).lex(),\n      subjectBlobCids: row.subjectBlobCids ?? [],\n      createdBy: row.createdBy,\n      createdAt: row.createdAt,\n      subjectHandle: row.subjectHandle ?? undefined,\n      creatorHandle: row.creatorHandle ?? undefined,\n      modTool: row.modTool\n        ? {\n            name: row.modTool.name,\n            meta: row.modTool.meta,\n          }\n        : undefined,\n    }\n\n    const { event } = eventView\n    const meta = row.meta || {}\n\n    if (\n      isModEventMuteReporter(event) ||\n      isModEventTakedown(event) ||\n      isModEventLabel(event) ||\n      isModEventMute(event)\n    ) {\n      event.durationInHours = row.durationInHours ?? undefined\n    }\n\n    if (\n      (isModEventTakedown(event) || isModEventAcknowledge(event)) &&\n      meta.acknowledgeAccountSubjects\n    ) {\n      event.acknowledgeAccountSubjects = ifBoolean(\n        meta.acknowledgeAccountSubjects,\n      )!\n    }\n\n    if (isModEventPriorityScore(event)) {\n      event.score = ifNumber(meta?.priorityScore) ?? 0\n    }\n\n    if (\n      isModEventTakedown(event) ||\n      isModEventEmail(event) ||\n      isModEventReverseTakedown(event)\n    ) {\n      if (typeof meta.policies === 'string' && meta.policies.length > 0) {\n        event.policies = meta.policies.split(',')\n      }\n\n      event.strikeCount = ifNumber(row.strikeCount)\n      event.severityLevel = ifString(row.severityLevel)\n\n      if (isModEventTakedown(event) || isModEventEmail(event)) {\n        event.strikeExpiresAt = ifString(row.strikeExpiresAt)\n      }\n    }\n\n    if (isModEventTakedown(event)) {\n      if (\n        typeof meta.targetServices === 'string' &&\n        meta.targetServices.length > 0\n      ) {\n        event.targetServices = meta.targetServices.split(',')\n      }\n    }\n\n    if (isModEventLabel(event)) {\n      event.createLabelVals = row.createLabelVals?.length\n        ? row.createLabelVals.split(' ')\n        : []\n      event.negateLabelVals = row.negateLabelVals?.length\n        ? row.negateLabelVals.split(' ')\n        : []\n    } else if (\n      isModEventAcknowledge(event) ||\n      isModEventTakedown(event) ||\n      isModEventEscalate(event)\n    ) {\n      // This is for legacy data only, for new events, these types of events\n      // won't have labels attached:\n\n      if (row.createLabelVals?.length) {\n        // @ts-expect-error legacy\n        event.createLabelVals = row.createLabelVals.split(' ')\n      }\n\n      if (row.negateLabelVals?.length) {\n        // @ts-expect-error legacy\n        event.negateLabelVals = row.negateLabelVals.split(' ')\n      }\n    }\n\n    if (isModEventReport(event)) {\n      event.isReporterMuted = !!meta.isReporterMuted\n      event.reportType = ifString(meta.reportType)!\n    }\n\n    if (isModEventEmail(event)) {\n      event.content = ifString(meta.content)!\n      event.subjectLine = ifString(meta.subjectLine)!\n      event.isDelivered = ifBoolean(meta.isDelivered)\n    }\n\n    if (isModEventComment(event) && meta.sticky) {\n      event.sticky = true\n    }\n\n    if (isModEventTag(event)) {\n      event.add = row.addedTags || []\n      event.remove = row.removedTags || []\n    }\n\n    if (isAccountEvent(event)) {\n      event.active = !!meta.active\n      event.timestamp = ifString(meta.timestamp)!\n      event.status = ifString(meta.status)!\n    }\n\n    if (isIdentityEvent(event)) {\n      event.timestamp = ifString(meta.timestamp)!\n      event.handle = ifString(meta.handle)!\n      event.pdsHost = ifString(meta.pdsHost)!\n      event.tombstone = !!meta.tombstone\n    }\n\n    if (isRecordEvent(event)) {\n      event.op = ifString(meta.op)!\n      event.cid = ifString(meta.cid)!\n      event.timestamp = ifString(meta.timestamp)!\n    }\n\n    if (isAgeAssuranceEvent(event)) {\n      event.status = ifString(meta.status)!\n      event.access = ifString(meta.access)!\n      event.createdAt = ifString(meta.createdAt)!\n      event.attemptId = ifString(meta.attemptId)!\n      event.initIp = ifString(meta.initIp)\n      event.initUa = ifString(meta.initUa)\n      event.completeIp = ifString(meta.completeIp)\n      event.completeUa = ifString(meta.completeUa)\n    }\n\n    if (isAgeAssuranceOverrideEvent(event)) {\n      event.status = ifString(meta.status)!\n      event.access = ifString(meta.access)!\n    }\n\n    if (isScheduleTakedownEvent(event)) {\n      event.executeAt = ifString(meta.executeAt)\n      event.executeAfter = ifString(meta.executeAfter)\n      event.executeUntil = ifString(meta.executeUntil)\n    }\n\n    return eventView\n  }\n\n  async eventDetail(\n    result: ModerationEventRowWithHandle,\n  ): Promise<ModEventViewDetail> {\n    const subjectId =\n      result.subjectType === 'com.atproto.admin.defs#repoRef'\n        ? result.subjectDid\n        : result.subjectUri\n    if (!subjectId) {\n      throw new Error(`Bad subject: ${result.id}`)\n    }\n    const subject = await this.subject(subjectId)\n    const eventView = this.formatEvent(result)\n    const allBlobs = 'value' in subject ? findBlobRefs(subject.value) : []\n    const subjectBlobs = await this.blob(\n      allBlobs.filter((blob) =>\n        eventView.subjectBlobCids.includes(blob.ref.toString()),\n      ),\n    )\n    return {\n      ...eventView,\n      subject,\n      subjectBlobs,\n    }\n  }\n\n  async repoDetails(\n    dids: string[],\n    labelers?: ParsedLabelers,\n  ): Promise<Map<string, RepoView>> {\n    const results = new Map<string, RepoView>()\n    if (!dids.length) {\n      return results\n    }\n\n    const [repos, localLabels, externalLabels] = await Promise.all([\n      this.repos(dids),\n      this.labels(dids),\n      this.getExternalLabels(dids, labelers),\n    ])\n\n    repos.forEach((repo, did) => {\n      const labels = [\n        ...(localLabels.get(did) || []),\n        ...(externalLabels.get(did) || []),\n      ]\n      const repoView = {\n        ...repo,\n        labels,\n        moderation: {\n          ...repo.moderation,\n        },\n      }\n      results.set(did, repoView)\n    })\n\n    return results\n  }\n\n  async fetchRecord(\n    params: ComAtprotoRepoGetRecord.QueryParams,\n    appviewAuth: AuthHeaders,\n  ) {\n    try {\n      const record = await this.appviewAgent.com.atproto.repo.getRecord(\n        params,\n        appviewAuth,\n      )\n      return record\n    } catch (err) {\n      if (err instanceof ComAtprotoRepoGetRecord.RecordNotFoundError) {\n        // If pds fetch fails, just return null regardless of the error\n        try {\n          const { agent: pdsAgent } = await getPdsAgentForRepo(\n            this.idResolver,\n            params.repo,\n            this.devMode,\n          )\n          if (!pdsAgent) {\n            return null\n          }\n\n          const record = await pdsAgent.com.atproto.repo.getRecord(params)\n          return record\n        } catch (error) {\n          return null\n        }\n      }\n\n      return null\n    }\n  }\n\n  async fetchRecords(\n    subjects: RecordSubject[],\n  ): Promise<Map<string, RecordInfo>> {\n    const appviewAuth = await this.appviewAuth(ids.ComAtprotoRepoGetRecord)\n    if (!appviewAuth) return new Map()\n\n    const fetched = await Promise.all(\n      subjects.map(async (subject) => {\n        const uri = new AtUri(subject.uri)\n        const params = {\n          repo: uri.hostname,\n          collection: uri.collection,\n          rkey: uri.rkey,\n          cid: subject.cid,\n        }\n        return this.fetchRecord(params, appviewAuth)\n      }),\n    )\n    return fetched.reduce((acc, cur) => {\n      if (!cur) return acc\n      const data = cur.data\n      const indexedAt = new Date().toISOString()\n      return acc.set(data.uri, { ...data, cid: data.cid ?? '', indexedAt })\n    }, new Map<string, RecordInfo>())\n  }\n\n  async records(subjects: RecordSubject[]) {\n    const uris = subjects.map((record) => new AtUri(record.uri))\n    const dids = uris.map((u) => u.hostname)\n\n    const [repos, subjectStatuses, records] = await Promise.all([\n      this.repos(dids),\n      this.getSubjectStatus(subjects.map((s) => s.uri)),\n      this.fetchRecords(subjects),\n    ])\n\n    const map = new Map<\n      string,\n      // Because the result of this function is used to build RecordViewDetail,\n      // we explicitly type the result without the $type field, so can be used\n      // as both RecordView and RecordViewDetail, without having to cast or\n      // override the $type field.\n      RecordView & {\n        $type?: undefined\n        moderation: { $type?: undefined; subjectStatus?: SubjectStatusView }\n      }\n    >()\n\n    for (const uri of uris) {\n      const repo = repos.get(uri.hostname)\n      if (!repo) continue\n      const record = records.get(uri.toString())\n      if (!record) continue\n      const subjectStatus = subjectStatuses.get(uri.toString())\n\n      map.set(uri.toString(), {\n        uri: uri.toString(),\n        cid: record.cid,\n        value: record.value,\n        blobCids: findBlobRefs(record.value).map((blob) => blob.ref.toString()),\n        indexedAt: record.indexedAt,\n        repo,\n        moderation: {\n          subjectStatus: subjectStatus\n            ? this.formatSubjectStatus(subjectStatus)\n            : undefined,\n        },\n      })\n    }\n\n    return map\n  }\n\n  async recordDetails(\n    subjects: RecordSubject[],\n    labelers?: ParsedLabelers,\n  ): Promise<Map<string, RecordViewDetail>> {\n    const results = new Map<string, RecordViewDetail>()\n    if (!subjects.length) {\n      return results\n    }\n\n    const subjectUris = subjects.map((s) => s.uri)\n    const [records, subjectStatusesResult, localLabels, externalLabels] =\n      await Promise.all([\n        this.records(subjects),\n        this.getSubjectStatus(subjectUris),\n        this.labels(subjectUris),\n        this.getExternalLabels(subjectUris, labelers),\n      ])\n\n    await Promise.all(\n      Array.from(records.entries()).map(async ([uri, record]) => {\n        const selfLabels = getSelfLabels({\n          uri: record.uri,\n          cid: record.cid,\n          record: record.value,\n        })\n\n        const status = subjectStatusesResult.get(uri)\n        const blobs = await this.blob(findBlobRefs(record.value))\n\n        results.set(uri, {\n          ...record,\n          blobs,\n          moderation: {\n            ...record.moderation,\n            subjectStatus: status\n              ? this.formatSubjectStatus(status)\n              : undefined,\n          },\n          labels: [\n            ...(localLabels.get(uri) || []),\n            ...selfLabels,\n            ...(externalLabels.get(uri) || []),\n          ],\n        })\n      }),\n    )\n\n    return results\n  }\n\n  async getExternalLabels(\n    subjects: string[],\n    labelers?: ParsedLabelers,\n  ): Promise<Map<string, Label[]>> {\n    const results = new Map<string, Label[]>()\n    if (!labelers?.dids.length && !labelers?.redact.size) return results\n    try {\n      const {\n        data: { labels },\n      } = await this.appviewAgent.com.atproto.label.queryLabels({\n        uriPatterns: subjects,\n        sources: labelers.dids,\n      })\n      labels.forEach((label) => {\n        if (!results.has(label.uri)) {\n          results.set(label.uri, [label])\n          return\n        }\n        results.get(label.uri)?.push(label)\n      })\n      return results\n    } catch (err) {\n      httpLogger.error(\n        { err, subjects, labelers },\n        'failed to resolve labels from appview',\n      )\n      return results\n    }\n  }\n\n  formatReport(report: ModerationEventRowWithHandle): ReportOutput {\n    return {\n      id: report.id,\n      createdAt: report.createdAt,\n      // Ideally, we would never have a report entry that does not have a reasonType but at the schema level\n      // we are not guarantying that so in whatever case, if we end up with such entries, default to 'other'\n      reasonType: report.meta?.reportType\n        ? (report.meta?.reportType as string)\n        : REASONOTHER,\n      reason: report.comment ?? undefined,\n      reportedBy: report.createdBy,\n      subject: subjectFromEventRow(report).lex() as ReportOutput['subject'],\n    }\n  }\n  // Partial view for subjects\n\n  async subject(subject: string): Promise<SubjectView> {\n    if (subject.startsWith('did:')) {\n      const repos = await this.repos([subject])\n      const repo = repos.get(subject)\n      if (repo) {\n        return {\n          ...repo,\n          $type: 'tools.ozone.moderation.defs#repoView',\n        }\n      } else {\n        return {\n          $type: 'tools.ozone.moderation.defs#repoViewNotFound',\n          did: subject,\n        }\n      }\n    } else {\n      const records = await this.records([{ uri: subject }])\n      const record = records.get(subject)\n      if (record) {\n        return {\n          ...record,\n          $type: 'tools.ozone.moderation.defs#recordView',\n        }\n      } else {\n        return {\n          $type: 'tools.ozone.moderation.defs#recordViewNotFound',\n          uri: subject,\n        }\n      }\n    }\n  }\n\n  // Partial view for blobs\n\n  async blob(blobs: BlobRef[]): Promise<BlobView[]> {\n    if (!blobs.length) return []\n    const { ref } = this.db.db.dynamic\n    const modStatusResults = await moderationSubjectStatusQueryBuilder(\n      this.db.db,\n    )\n      .where(\n        sql<string>`${ref(\n          'moderation_subject_status.blobCids',\n        )} @> ${JSON.stringify(blobs.map((blob) => blob.ref.toString()))}`,\n      )\n      .executeTakeFirst()\n\n    const statusByCid = (modStatusResults?.blobCids || [])?.reduce(\n      (acc, cur) => Object.assign(acc, { [cur]: modStatusResults }),\n      {},\n    )\n    // Intentionally missing details field, since we don't have any on appview.\n    // We also don't know when the blob was created, so we use a canned creation time.\n    const unknownTime = new Date(0).toISOString()\n    return blobs.map((blob) => {\n      const cid = blob.ref.toString()\n      const subjectStatus = statusByCid[cid]\n        ? this.formatSubjectStatus(statusByCid[cid])\n        : undefined\n\n      return {\n        cid,\n        mimeType: blob.mimeType,\n        size: blob.size,\n        createdAt: unknownTime,\n        moderation: {\n          subjectStatus,\n        },\n      }\n    })\n  }\n\n  async labels(\n    subjects: string[],\n    includeNeg?: boolean,\n  ): Promise<Map<string, Label[]>> {\n    const now = new Date().toISOString()\n    const labels = new Map<string, Label[]>()\n    const res = await this.db.db\n      .selectFrom('label')\n      .where('label.uri', 'in', subjects)\n      .where((qb) =>\n        qb.where('label.exp', 'is', null).orWhere('label.exp', '>', now),\n      )\n      .if(!includeNeg, (qb) => qb.where('neg', '=', false))\n      .selectAll()\n      .execute()\n\n    await Promise.all(\n      res.map(async (labelRow) => {\n        const signedLabel = await this.formatLabelAndEnsureSig(labelRow)\n\n        const current = labels.get(labelRow.uri)\n        if (current) current.push(signedLabel)\n        else labels.set(labelRow.uri, [signedLabel])\n      }),\n    )\n    return labels\n  }\n\n  async formatLabelAndEnsureSig(row: LabelRow) {\n    const formatted = formatLabel(row)\n    if (!!row.sig && row.signingKeyId === this.signingKeyId) {\n      return formatted\n    }\n    const signed = await signLabel(formatted, this.signingKey)\n    try {\n      await this.db.db\n        .updateTable('label')\n        .set({ sig: Buffer.from(signed.sig), signingKeyId: this.signingKeyId })\n        .where('id', '=', row.id)\n        .execute()\n    } catch (err) {\n      dbLogger.error({ err, label: row }, 'failed to update resigned label')\n    }\n    return signed\n  }\n\n  async getSubjectStatus(\n    subjects: string[],\n  ): Promise<Map<string, ModerationSubjectStatusRowWithHandle>> {\n    if (!subjects.length) return new Map()\n\n    const parsedSubjects = subjects.map(parseSubjectId)\n\n    const builder = moderationSubjectStatusQueryBuilder(this.db.db)\n      //\n      .where((qb) => {\n        for (const sub of parsedSubjects) {\n          qb = qb.orWhere((qb) =>\n            qb\n              .where('moderation_subject_status.did', '=', sub.did)\n              .where(\n                'moderation_subject_status.recordPath',\n                '=',\n                sub.recordPath ?? '',\n              ),\n          )\n        }\n        return qb\n      })\n\n    const [statusRes, accountsByDid] = await Promise.all([\n      builder.execute(),\n      this.getAccoutInfosByDid(parsedSubjects.map((s) => s.did)),\n    ])\n\n    return new Map(\n      statusRes.map((row): [string, ModerationSubjectStatusRowWithHandle] => {\n        const subjectId = formatSubjectId(row.did, row.recordPath)\n        const handle = accountsByDid.get(row.did)?.handle ?? INVALID_HANDLE\n        return [subjectId, { ...row, handle }]\n      }),\n    )\n  }\n\n  formatSubjectStatus(\n    status: ModerationSubjectStatusRowWithHandle,\n  ): SubjectStatusView {\n    const statusView: SubjectStatusView = {\n      id: status.id,\n      reviewState: status.reviewState,\n      createdAt: status.createdAt,\n      updatedAt: status.updatedAt,\n      comment: status.comment ?? undefined,\n      lastReviewedBy: status.lastReviewedBy ?? undefined,\n      lastReviewedAt: status.lastReviewedAt ?? undefined,\n      lastReportedAt: status.lastReportedAt ?? undefined,\n      lastAppealedAt: status.lastAppealedAt ?? undefined,\n      muteUntil: status.muteUntil ?? undefined,\n      muteReportingUntil: status.muteReportingUntil ?? undefined,\n      suspendUntil: status.suspendUntil ?? undefined,\n      takendown: status.takendown ?? undefined,\n      appealed: status.appealed ?? undefined,\n      subjectRepoHandle: status.handle ?? undefined,\n      subjectBlobCids: status.blobCids || [],\n      tags: status.tags || [],\n      priorityScore: status.priorityScore,\n      ageAssuranceState: status.ageAssuranceState ?? undefined,\n      ageAssuranceUpdatedBy: status.ageAssuranceUpdatedBy ?? undefined,\n      subject: subjectFromStatusRow(\n        status,\n      ).lex() as SubjectStatusView['subject'],\n\n      accountStats: {\n        // Explicitly typing to allow for easy manipulation (e.g. to strip from tests snapshots)\n        $type: 'tools.ozone.moderation.defs#accountStats',\n\n        // account_events_stats\n        reportCount: status.reportCount ?? undefined,\n        appealCount: status.appealCount ?? undefined,\n        suspendCount: status.suspendCount ?? undefined,\n        takedownCount: status.takedownCount ?? undefined,\n        escalateCount: status.escalateCount ?? undefined,\n      },\n\n      recordsStats: {\n        // Explicitly typing to allow for easy manipulation (e.g. to strip from tests snapshots)\n        $type: 'tools.ozone.moderation.defs#recordsStats',\n\n        // account_record_events_stats\n        totalReports: status.totalReports ?? undefined,\n        reportedCount: status.reportedCount ?? undefined,\n        escalatedCount: status.escalatedCount ?? undefined,\n        appealedCount: status.appealedCount ?? undefined,\n\n        // account_record_status_stats\n        subjectCount: status.subjectCount ?? undefined,\n        pendingCount: status.pendingCount ?? undefined,\n        processedCount: status.processedCount ?? undefined,\n        takendownCount: status.takendownCount ?? undefined,\n      },\n\n      accountStrike:\n        status.strikeCount !== null || status.totalStrikeCount !== null\n          ? {\n              $type: 'tools.ozone.moderation.defs#accountStrike',\n              activeStrikeCount: status.strikeCount ?? undefined,\n              totalStrikeCount: status.totalStrikeCount ?? undefined,\n              firstStrikeAt: status.firstStrikeAt ?? undefined,\n              lastStrikeAt: status.lastStrikeAt ?? undefined,\n            }\n          : undefined,\n    }\n\n    if (status.recordPath !== '') {\n      statusView.hosting = {\n        $type: 'tools.ozone.moderation.defs#recordHosting',\n        updatedAt: status.hostingUpdatedAt ?? undefined,\n        deletedAt: status.hostingDeletedAt ?? undefined,\n        status: status.hostingStatus ?? 'unknown',\n      }\n    } else {\n      statusView.hosting = {\n        $type: 'tools.ozone.moderation.defs#accountHosting',\n        updatedAt: status.hostingUpdatedAt ?? undefined,\n        deletedAt: status.hostingDeletedAt ?? undefined,\n        status: status.hostingStatus ?? 'unknown',\n        deactivatedAt: status.hostingDeactivatedAt ?? undefined,\n        reactivatedAt: status.hostingReactivatedAt ?? undefined,\n      }\n    }\n\n    return statusView\n  }\n\n  async fetchAuthorFeed(actor: string): Promise<FeedViewPost[]> {\n    const auth = await this.appviewAuth(ids.AppBskyFeedGetAuthorFeed)\n    if (!auth) return []\n    const {\n      data: { feed },\n    } = await this.appviewAgent.app.bsky.feed.getAuthorFeed({ actor }, auth)\n\n    return feed\n  }\n\n  async getProfiles(dids: string[]) {\n    const profiles = new Map<string, AppBskyActorDefs.ProfileViewDetailed>()\n\n    const auth = await this.appviewAuth(ids.AppBskyActorGetProfiles)\n    if (!auth) return profiles\n\n    for (const actors of chunkArray(dids, 25)) {\n      const { data } = await this.appviewAgent.getProfiles({ actors }, auth)\n\n      data.profiles.forEach((profile) => {\n        profiles.set(profile.did, profile)\n      })\n    }\n\n    return profiles\n  }\n}\n\ntype RecordSubject = { uri: string; cid?: string }\n\ntype SubjectView = ModEventViewDetail['subject']\n// @TODO tidy\n// type SubjectView = ModEventViewDetail['subject'] & ReportViewDetail['subject']\n\ntype RecordInfo = {\n  uri: string\n  cid: string\n  value: Record<string, unknown>\n  indexedAt: string\n}\n\nfunction parseSubjectId(subject: string): { did: string; recordPath?: string } {\n  if (subject.startsWith('did:')) {\n    return { did: subject }\n  }\n  const uri = new AtUri(subject)\n  return { did: uri.hostname, recordPath: `${uri.collection}/${uri.rkey}` }\n}\n\nfunction formatSubjectId(did: string, recordPath?: string) {\n  return recordPath ? `at://${did}/${recordPath}` : did\n}\n\nfunction findBlobRefs(value: unknown, refs: BlobRef[] = []) {\n  if (value instanceof BlobRef) {\n    refs.push(value)\n  } else if (Array.isArray(value)) {\n    value.forEach((val) => findBlobRefs(val, refs))\n  } else if (value && typeof value === 'object') {\n    Object.values(value).forEach((val) => findBlobRefs(val, refs))\n  }\n  return refs\n}\n\nexport function getSelfLabels(details: {\n  uri: string | null\n  cid: string | null\n  record: Record<string, unknown> | null\n}): Label[] {\n  const { uri, cid, record } = details\n  if (!uri || !cid || !record) return []\n  if (!isValidSelfLabels(record.labels)) return []\n  const src = new AtUri(uri).host // record creator\n  const cts =\n    typeof record.createdAt === 'string'\n      ? normalizeDatetimeAlways(record.createdAt)\n      : new Date(0).toISOString()\n  return record.labels.values.map(({ val }) => {\n    return { src, uri, cid, val, cts }\n  })\n}\n"
  },
  {
    "path": "packages/ozone/src/safelink/service.ts",
    "content": "import { Selectable } from 'kysely'\nimport { ToolsOzoneSafelinkDefs } from '@atproto/api'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport {\n  SafelinkActionType,\n  SafelinkPatternType,\n  SafelinkReasonType,\n} from '../api/util'\nimport { Database } from '../db'\nimport { SafelinkEvent, SafelinkRule } from '../db/schema/safelink'\n\nexport type SafelinkRuleServiceCreator = (db: Database) => SafelinkRuleService\n\nexport class SafelinkRuleService {\n  constructor(public db: Database) {}\n\n  static creator() {\n    return (db: Database) => new SafelinkRuleService(db)\n  }\n\n  formatEvent(event: Selectable<SafelinkEvent>): ToolsOzoneSafelinkDefs.Event {\n    return {\n      id: event.id,\n      eventType: event.eventType,\n      url: event.url,\n      pattern: event.pattern,\n      action: event.action,\n      reason: event.reason,\n      createdBy: event.createdBy,\n      createdAt: new Date(event.createdAt).toISOString(),\n      comment: event.comment || undefined,\n    }\n  }\n\n  async addRule({\n    url,\n    pattern,\n    action,\n    reason,\n    createdBy,\n    comment,\n  }: {\n    url: string\n    pattern: SafelinkPatternType\n    action: SafelinkActionType\n    reason: SafelinkReasonType\n    createdBy: string\n    comment?: string\n  }): Promise<Selectable<SafelinkEvent>> {\n    const existingRule = await this.getActiveRule(url, pattern)\n    if (existingRule) {\n      throw new InvalidRequestError(\n        'A rule for this URL/domain already exists',\n        'RuleAlreadyExists',\n      )\n    }\n\n    const now = new Date().toISOString()\n    const rule = {\n      url,\n      pattern,\n      action,\n      reason,\n      createdBy,\n      comment: comment || null,\n      createdAt: now,\n    }\n\n    return await this.db.transaction(async (txn) => {\n      const event = await txn.db\n        .insertInto('safelink_event')\n        .values({\n          eventType: 'addRule',\n          ...rule,\n        })\n        .returningAll()\n        .executeTakeFirstOrThrow()\n\n      await txn.db\n        .insertInto('safelink_rule')\n        .values({ ...rule, updatedAt: now })\n        .execute()\n\n      return event\n    })\n  }\n\n  async updateRule({\n    url,\n    pattern,\n    action,\n    reason,\n    createdBy,\n    comment,\n  }: {\n    url: string\n    pattern: SafelinkPatternType\n    action: SafelinkActionType\n    reason: SafelinkReasonType\n    createdBy: string\n    comment?: string\n  }): Promise<Selectable<SafelinkEvent>> {\n    const existingRule = await this.getActiveRule(url, pattern)\n    if (!existingRule) {\n      throw new InvalidRequestError(\n        'No active rule found for this URL/domain',\n        'RuleNotFound',\n      )\n    }\n\n    const now = new Date().toISOString()\n    const rule = {\n      action,\n      reason,\n      createdBy,\n      comment: comment || null,\n    }\n\n    return await this.db.transaction(async (txn) => {\n      const event = await txn.db\n        .insertInto('safelink_event')\n        .values({\n          createdAt: now,\n          url: existingRule.url,\n          pattern: existingRule.pattern,\n          eventType: 'updateRule',\n          ...rule,\n        })\n        .returningAll()\n        .executeTakeFirstOrThrow()\n\n      await txn.db\n        .updateTable('safelink_rule')\n        .set(rule)\n        .where('url', '=', existingRule.url)\n        .where('pattern', '=', existingRule.pattern)\n        .execute()\n\n      return event\n    })\n  }\n\n  async removeRule({\n    url,\n    pattern,\n    createdBy,\n    comment,\n  }: {\n    url: string\n    pattern: SafelinkPatternType\n    createdBy: string\n    comment?: string\n  }): Promise<Selectable<SafelinkEvent>> {\n    const existingRule = await this.getActiveRule(url, pattern)\n    if (!existingRule) {\n      throw new InvalidRequestError(\n        'No active rule found for this URL/domain',\n        'RuleNotFound',\n      )\n    }\n\n    return await this.db.transaction(async (txn) => {\n      const event = await txn.db\n        .insertInto('safelink_event')\n        .values({\n          eventType: 'removeRule',\n          url,\n          pattern,\n          action: existingRule.action,\n          reason: existingRule.reason,\n          createdBy,\n          comment: comment || null,\n          createdAt: new Date().toISOString(),\n        })\n        .returningAll()\n        .executeTakeFirstOrThrow()\n\n      await txn.db\n        .deleteFrom('safelink_rule')\n        .where('url', '=', url)\n        .where('pattern', '=', pattern)\n        .execute()\n\n      return event\n    })\n  }\n\n  async getActiveRule(url: string, pattern: SafelinkPatternType) {\n    const rule = await this.db.db\n      .selectFrom('safelink_rule')\n      .selectAll()\n      .where('url', '=', url)\n      .where('pattern', '=', pattern)\n      .executeTakeFirst()\n\n    if (!rule) {\n      return null\n    }\n\n    return rule\n  }\n\n  async getActiveRules({\n    cursor,\n    limit = 50,\n    urls,\n    patternType,\n    actions,\n    reason,\n    createdBy,\n    direction = 'desc',\n  }: {\n    cursor?: string\n    limit?: number\n    urls?: string[]\n    patternType?: SafelinkPatternType\n    actions?: SafelinkActionType[]\n    reason?: SafelinkReasonType\n    createdBy?: string\n    direction?: 'asc' | 'desc'\n  } = {}): Promise<{\n    rules: Selectable<SafelinkRule>[]\n    cursor?: string\n  }> {\n    let query = this.db.db.selectFrom('safelink_rule').selectAll()\n\n    if (urls && urls.length > 0) {\n      query = query.where('url', 'in', urls)\n    }\n\n    if (patternType) {\n      query = query.where('pattern', '=', patternType)\n    }\n\n    if (actions && actions.length > 0) {\n      query = query.where('action', 'in', actions)\n    }\n\n    if (reason) {\n      query = query.where('reason', '=', reason)\n    }\n\n    if (createdBy) {\n      query = query.where('createdBy', '=', createdBy)\n    }\n\n    if (cursor) {\n      query = query.where(\n        'id',\n        direction === 'asc' ? '>' : '<',\n        parseInt(cursor, 10),\n      )\n    }\n\n    const rules = await query.orderBy('id', direction).limit(limit).execute()\n\n    return {\n      rules,\n      cursor: rules.at(-1)?.id?.toString(),\n    }\n  }\n\n  async queryEvents({\n    cursor,\n    limit = 50,\n    urls,\n    patternType,\n    direction = 'desc',\n  }: {\n    cursor?: string\n    limit?: number\n    urls?: string[]\n    patternType?: SafelinkPatternType\n    direction?: 'asc' | 'desc'\n  } = {}): Promise<{\n    events: Selectable<SafelinkEvent>[]\n    cursor?: string\n  }> {\n    let query = this.db.db.selectFrom('safelink_event').selectAll()\n\n    if (urls && urls.length > 0) {\n      query = query.where('url', 'in', urls)\n    }\n\n    if (patternType) {\n      query = query.where('pattern', '=', patternType)\n    }\n\n    if (cursor) {\n      query = query.where(\n        'id',\n        direction === 'asc' ? '>' : '<',\n        parseInt(cursor, 10),\n      )\n    }\n\n    const events = await query.orderBy('id', direction).limit(limit).execute()\n\n    return {\n      events,\n      cursor: events.at(-1)?.id?.toString(),\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/scheduled-action/service.ts",
    "content": "import { Selectable } from 'kysely'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ScheduledActionStatus, ScheduledActionType } from '../api/util'\nimport { Database } from '../db'\nimport { ScheduledAction } from '../db/schema/scheduled-action'\nimport { ScheduledActionView } from '../lexicon/types/tools/ozone/moderation/defs'\nimport { dbLogger } from '../logger'\nimport { SchedulingParams } from './types'\n\nexport type ScheduledActionServiceCreator = (\n  db: Database,\n) => ScheduledActionService\n\nexport class ScheduledActionService {\n  constructor(public db: Database) {}\n\n  static creator() {\n    return (db: Database) => new ScheduledActionService(db)\n  }\n\n  formatScheduledAction(\n    action: Selectable<ScheduledAction>,\n  ): ScheduledActionView {\n    return {\n      id: action.id,\n      action: action.action,\n      eventData: action.eventData as { [x: string]: unknown } | undefined,\n      did: action.did,\n      executeAt: action.executeAt\n        ? new Date(action.executeAt).toISOString()\n        : undefined,\n      executeAfter: action.executeAfter\n        ? new Date(action.executeAfter).toISOString()\n        : undefined,\n      executeUntil: action.executeUntil\n        ? new Date(action.executeUntil).toISOString()\n        : undefined,\n      randomizeExecution: action.randomizeExecution,\n      createdBy: action.createdBy,\n      createdAt: new Date(action.createdAt).toISOString(),\n      updatedAt: new Date(action.updatedAt).toISOString(),\n      status: action.status,\n      lastExecutedAt: action.lastExecutedAt\n        ? new Date(action.lastExecutedAt).toISOString()\n        : undefined,\n      lastFailureReason: action.lastFailureReason || undefined,\n      executionEventId: action.executionEventId || undefined,\n    }\n  }\n\n  async scheduleAction(\n    schedulingParams: SchedulingParams,\n  ): Promise<Selectable<ScheduledAction>> {\n    const { action, eventData, did, createdBy } = schedulingParams\n\n    // Only allow one pending action at a time for a given subject and action type\n    const existingAction = await this.getPendingActionForSubject(did, action)\n    if (existingAction) {\n      throw new InvalidRequestError(\n        'A pending scheduled action already exists for this subject',\n        'ActionAlreadyExists',\n      )\n    }\n\n    // When a time-range for action is specified, ensure that the range is valid\n    if (\n      'executeAfter' in schedulingParams &&\n      schedulingParams.executeAfter &&\n      schedulingParams.executeUntil &&\n      schedulingParams.executeAfter >= schedulingParams.executeUntil\n    ) {\n      throw new InvalidRequestError(\n        'executeAfter must be before executeUntil',\n        'InvalidScheduling',\n      )\n    }\n\n    const now = new Date().toISOString()\n    const randomizeExecution =\n      !('executeAt' in schedulingParams) && 'executeAfter' in schedulingParams\n\n    const scheduledAction = await this.db.db\n      .insertInto('scheduled_action')\n      .values({\n        action,\n        eventData: JSON.stringify(eventData),\n        did,\n        executeAt: randomizeExecution\n          ? null\n          : schedulingParams.executeAt?.toISOString(),\n        executeAfter: randomizeExecution\n          ? schedulingParams.executeAfter?.toISOString()\n          : null,\n        executeUntil: randomizeExecution\n          ? schedulingParams.executeUntil?.toISOString()\n          : null,\n        randomizeExecution,\n        createdBy,\n        createdAt: now,\n        updatedAt: now,\n        status: 'pending',\n      })\n      .returningAll()\n      .executeTakeFirstOrThrow()\n\n    return scheduledAction\n  }\n\n  async getPendingActionForSubject(\n    did: string,\n    action: ScheduledActionType,\n  ): Promise<Selectable<ScheduledAction> | null> {\n    const scheduledAction = await this.db.db\n      .selectFrom('scheduled_action')\n      .selectAll()\n      .where('did', '=', did)\n      .where('action', '=', action)\n      .where('status', '=', 'pending')\n      .executeTakeFirst()\n\n    return scheduledAction || null\n  }\n\n  async listScheduledActions({\n    cursor,\n    limit = 50,\n    startTime,\n    endTime,\n    subjects,\n    statuses = [],\n    direction = 'desc',\n  }: {\n    cursor?: string\n    limit?: number\n    startTime?: Date\n    endTime?: Date\n    subjects?: string[]\n    statuses: ScheduledActionStatus[]\n    direction?: 'asc' | 'desc'\n  }): Promise<{\n    actions: Selectable<ScheduledAction>[]\n    cursor?: string\n  }> {\n    let query = this.db.db\n      .selectFrom('scheduled_action')\n      .where('status', 'in', statuses)\n      .selectAll()\n\n    if (subjects && subjects.length > 0) {\n      query = query.where('did', 'in', subjects)\n    }\n\n    if (startTime) {\n      query = query.where((qb) => {\n        return qb\n          .orWhere('executeAt', '>=', startTime.toISOString())\n          .orWhere('executeAfter', '>=', startTime.toISOString())\n      })\n    }\n\n    if (endTime) {\n      query = query.where((qb) => {\n        return qb\n          .orWhere('executeAt', '<=', endTime.toISOString())\n          .orWhere('executeUntil', '<=', endTime.toISOString())\n          .orWhere((sqb) => {\n            return sqb\n              .where('executeUntil', 'is', null)\n              .where('executeAfter', '<=', endTime.toISOString())\n          })\n      })\n    }\n\n    if (cursor) {\n      query = query.where(\n        'id',\n        direction === 'asc' ? '>' : '<',\n        parseInt(cursor, 10),\n      )\n    }\n\n    const actions = await query.orderBy('id', direction).limit(limit).execute()\n\n    return {\n      actions,\n      cursor: actions.at(-1)?.id?.toString(),\n    }\n  }\n\n  async cancelScheduledActions(subjects: string[]): Promise<{\n    succeeded: string[]\n    failed: { did: string; error: string; errorCode?: string }[]\n  }> {\n    const succeeded: string[] = []\n    const failed: { did: string; error: string; errorCode?: string }[] = []\n\n    for (const did of subjects) {\n      try {\n        const result = await this.db.db\n          .updateTable('scheduled_action')\n          .set({\n            status: 'cancelled',\n            updatedAt: new Date().toISOString(),\n          })\n          .where('did', '=', did)\n          .where('status', '=', 'pending')\n          .executeTakeFirst()\n\n        if (result.numUpdatedRows && result.numUpdatedRows > 0) {\n          succeeded.push(did)\n        } else {\n          failed.push({\n            did,\n            error: 'No pending scheduled actions found for subject',\n            errorCode: 'NoPendingActions',\n          })\n        }\n      } catch (err) {\n        dbLogger.error({ err, subjects }, 'Error cancelling scheduled action')\n        failed.push({\n          did,\n          error: 'Unknown error',\n          errorCode: 'DatabaseError',\n        })\n      }\n    }\n\n    return { succeeded, failed }\n  }\n\n  async getPendingActionsToExecute(\n    now: Date,\n  ): Promise<Selectable<ScheduledAction>[]> {\n    return await this.db.db\n      .selectFrom('scheduled_action')\n      .selectAll()\n      .where('status', '=', 'pending')\n      .where((qb) => {\n        return qb\n          .orWhere('executeAfter', '<=', now.toISOString())\n          .orWhere('executeAt', '<=', now.toISOString())\n      })\n      .execute()\n  }\n\n  async markActionAsExecuted(\n    actionId: number,\n    executionEventId: number,\n  ): Promise<void> {\n    const now = new Date().toISOString()\n    await this.db.db\n      .updateTable('scheduled_action')\n      .set({\n        status: 'executed',\n        lastExecutedAt: now,\n        executionEventId,\n        updatedAt: now,\n      })\n      .where('id', '=', actionId)\n      .execute()\n  }\n\n  async markActionAsFailed(\n    actionId: number,\n    failureReason: string,\n  ): Promise<void> {\n    const now = new Date().toISOString()\n    await this.db.db\n      .updateTable('scheduled_action')\n      .set({\n        status: 'failed',\n        lastExecutedAt: now,\n        lastFailureReason: failureReason,\n        updatedAt: now,\n      })\n      .where('id', '=', actionId)\n      .execute()\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/scheduled-action/types.ts",
    "content": "import { ScheduledActionType } from '../api/util'\n\nexport type ExecutionSchedule =\n  | {\n      executeAt: Date\n    }\n  | {\n      executeAfter: Date\n      executeUntil?: Date\n    }\n\nexport type SchedulingParams = {\n  action: ScheduledActionType\n  eventData: unknown\n  did: string\n  createdBy: string\n} & ExecutionSchedule\n"
  },
  {
    "path": "packages/ozone/src/sequencer/index.ts",
    "content": "export * from './sequencer'\nexport * from './outbox'\n"
  },
  {
    "path": "packages/ozone/src/sequencer/outbox.ts",
    "content": "import { AsyncBuffer, AsyncBufferFullError } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { LabelsEvt, Sequencer } from './sequencer'\n\nexport type OutboxOpts = {\n  maxBufferSize: number\n}\n\nexport class Outbox {\n  private caughtUp = false\n  lastSeen = -1\n\n  cutoverBuffer: LabelsEvt[]\n  outBuffer: AsyncBuffer<LabelsEvt>\n\n  constructor(\n    public sequencer: Sequencer,\n    opts: Partial<OutboxOpts> = {},\n  ) {\n    const { maxBufferSize = 500 } = opts\n    this.cutoverBuffer = []\n    this.outBuffer = new AsyncBuffer<LabelsEvt>(maxBufferSize)\n  }\n\n  // event stream occurs in 3 phases\n  // 1. backfill events: events that have been added to the DB since the last time a connection was open.\n  // The outbox is not yet listening for new events from the sequencer\n  // 2. cutover: the outbox has caught up with where the sequencer purports to be,\n  // but the sequencer might already be halfway through sending out a round of updates.\n  // Therefore, we start accepting the sequencer's events in a buffer, while making our own request to the\n  // database to ensure we're caught up. We then dedupe the query & the buffer & stream the events in order\n  // 3. streaming: we're all caught up on historic state, so the sequencer outputs events and we\n  // immediately yield them\n  async *events(\n    backfillCursor?: number,\n    signal?: AbortSignal,\n  ): AsyncGenerator<LabelsEvt> {\n    // catch up as much as we can\n    if (backfillCursor !== undefined) {\n      for await (const evt of this.getBackfill(backfillCursor)) {\n        if (signal?.aborted) return\n        this.lastSeen = evt.seq\n        yield evt\n      }\n    } else {\n      // if not backfill, we don't need to cutover, just start streaming\n      this.caughtUp = true\n    }\n\n    // streams updates from sequencer, but buffers them for cutover as it makes a last request\n\n    const addToBuffer = (evts) => {\n      if (this.caughtUp) {\n        this.outBuffer.pushMany(evts)\n      } else {\n        this.cutoverBuffer = [...this.cutoverBuffer, ...evts]\n      }\n    }\n\n    if (!signal?.aborted) {\n      this.sequencer.on('events', addToBuffer)\n    }\n    signal?.addEventListener('abort', () =>\n      this.sequencer.off('events', addToBuffer),\n    )\n\n    const cutover = async () => {\n      // only need to perform cutover if we've been backfilling\n      if (backfillCursor !== undefined) {\n        const cutoverEvts = await this.sequencer.requestLabelRange({\n          earliestId: this.lastSeen > -1 ? this.lastSeen : backfillCursor,\n        })\n        this.outBuffer.pushMany(cutoverEvts)\n        // dont worry about dupes, we ensure order on yield\n        this.outBuffer.pushMany(this.cutoverBuffer)\n        this.caughtUp = true\n        this.cutoverBuffer = []\n      } else {\n        this.caughtUp = true\n      }\n    }\n    cutover()\n\n    while (true) {\n      try {\n        for await (const evt of this.outBuffer.events()) {\n          if (signal?.aborted) return\n          if (evt.seq > this.lastSeen) {\n            this.lastSeen = evt.seq\n            yield evt\n          }\n        }\n      } catch (err) {\n        if (err instanceof AsyncBufferFullError) {\n          throw new InvalidRequestError(\n            'Stream consumer too slow',\n            'ConsumerTooSlow',\n          )\n        } else {\n          throw err\n        }\n      }\n    }\n  }\n\n  // yields only historical events\n  async *getBackfill(backfillCursor: number) {\n    const PAGE_SIZE = 500\n    while (true) {\n      const evts = await this.sequencer.requestLabelRange({\n        earliestId: this.lastSeen > -1 ? this.lastSeen : backfillCursor,\n        limit: PAGE_SIZE,\n      })\n      for (const evt of evts) {\n        yield evt\n      }\n      // if we're within half a pagesize of the sequencer, we call it good & switch to cutover\n      const seqCursor = this.sequencer.lastSeen ?? -1\n      if (seqCursor - this.lastSeen < PAGE_SIZE / 2) break\n      if (evts.length < 1) break\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/sequencer/sequencer.ts",
    "content": "import EventEmitter from 'node:events'\nimport { Selectable } from 'kysely'\nimport { PoolClient } from 'pg'\nimport TypedEmitter from 'typed-emitter'\nimport { Database } from '../db'\nimport { Label as LabelTable, LabelChannel } from '../db/schema/label'\nimport { Labels as LabelsEvt } from '../lexicon/types/com/atproto/label/subscribeLabels'\nimport { seqLogger as log } from '../logger'\nimport { ModerationService } from '../mod-service'\n\nexport type { Labels as LabelsEvt } from '../lexicon/types/com/atproto/label/subscribeLabels'\ntype LabelRow = Selectable<LabelTable>\n\nexport class Sequencer extends (EventEmitter as new () => SequencerEmitter) {\n  db: Database\n  destroyed = false\n  pollPromise: Promise<void> | undefined\n  queued = false\n  conn: PoolClient | undefined\n\n  constructor(\n    public modSrvc: ModerationService,\n    public lastSeen = 0,\n  ) {\n    super()\n    this.db = modSrvc.db\n    // note: this does not err when surpassed, just prints a warning to stderr\n    this.setMaxListeners(100)\n  }\n\n  async start() {\n    const curr = await this.curr()\n    this.lastSeen = curr ?? 0\n    this.poll()\n    this.conn = await this.db.pool.connect()\n    this.conn.query(`listen ${LabelChannel}`) // if this errors, unhandled rejection should cause process to exit\n    this.conn.on('notification', (notif) => {\n      if (notif.channel === LabelChannel) {\n        this.poll()\n      }\n    })\n  }\n\n  async destroy() {\n    if (this.destroyed) return\n    this.destroyed = true\n    if (this.conn) {\n      this.conn.release()\n      this.conn = undefined\n    }\n    if (this.pollPromise) {\n      await this.pollPromise\n    }\n    this.emit('close')\n  }\n\n  async curr(): Promise<number | null> {\n    const got = await this.db.db\n      .selectFrom('label')\n      .selectAll()\n      .orderBy('id', 'desc')\n      .limit(1)\n      .executeTakeFirst()\n    return got?.id ?? null\n  }\n\n  async next(cursor: number): Promise<LabelRow | null> {\n    const got = await this.db.db\n      .selectFrom('label')\n      .selectAll()\n      .where('id', '>', cursor)\n      .limit(1)\n      .orderBy('id', 'asc')\n      .executeTakeFirst()\n    return got || null\n  }\n\n  async requestLabelRange(opts: {\n    earliestId?: number\n    limit?: number\n  }): Promise<LabelsEvt[]> {\n    const { earliestId, limit } = opts\n\n    let seqQb = this.db.db.selectFrom('label').selectAll().orderBy('id', 'asc')\n    if (earliestId !== undefined) {\n      seqQb = seqQb.where('id', '>', earliestId)\n    }\n    if (limit !== undefined) {\n      seqQb = seqQb.limit(limit)\n    }\n\n    const rows = await seqQb.execute()\n    if (rows.length < 1) {\n      return []\n    }\n\n    const evts: LabelsEvt[] = await Promise.all(\n      rows.map(async (row) => {\n        const formatted = await this.modSrvc.views.formatLabelAndEnsureSig(row)\n        return { seq: row.id, labels: [formatted] }\n      }),\n    )\n\n    return evts\n  }\n\n  private poll() {\n    if (this.destroyed) return\n    if (this.pollPromise) {\n      this.queued = true\n      return\n    }\n    this.queued = false\n    this.pollPromise = this.requestLabelRange({\n      earliestId: this.lastSeen,\n      limit: 500,\n    })\n      .then((evts) => {\n        this.emit('events', evts)\n        this.lastSeen = evts.at(-1)?.seq ?? this.lastSeen\n        if (evts.length > 0) {\n          this.queued = true\n        }\n      })\n      .catch((err) => {\n        log.error(\n          { err, lastSeen: this.lastSeen },\n          'sequencer failed to poll db',\n        )\n      })\n      .finally(() => {\n        this.pollPromise = undefined\n        if (this.queued) {\n          this.poll()\n        }\n      })\n  }\n}\n\ntype SequencerEvents = {\n  events: (evts: LabelsEvt[]) => void\n  close: () => void\n}\n\nexport type SequencerEmitter = TypedEmitter<SequencerEvents>\n"
  },
  {
    "path": "packages/ozone/src/set/service.ts",
    "content": "import { Selectable } from 'kysely'\nimport { Database } from '../db'\nimport { TimeIdKeyset, paginate } from '../db/pagination'\nimport { SetDetail } from '../db/schema/ozone_set'\nimport { SetView } from '../lexicon/types/tools/ozone/set/defs'\n\nexport type SetServiceCreator = (db: Database) => SetService\n\nexport class SetService {\n  constructor(public db: Database) {}\n\n  static creator() {\n    return (db: Database) => new SetService(db)\n  }\n\n  buildQueryForSetWithSize() {\n    return this.db.db.selectFrom('set_detail as s').select([\n      's.id',\n      's.name',\n      's.description',\n      's.createdAt',\n      's.updatedAt',\n      (eb) =>\n        eb\n          .selectFrom('set_value')\n          .select((e) => e.fn.count<number>('setId').as('count'))\n          .whereRef('setId', '=', 's.id')\n          .as('setSize'),\n    ])\n  }\n\n  async query({\n    limit,\n    cursor,\n    namePrefix,\n    sortBy,\n    sortDirection,\n  }: {\n    limit: number\n    cursor?: string\n    namePrefix?: string\n    sortBy: 'name' | 'createdAt' | 'updatedAt'\n    sortDirection: 'asc' | 'desc'\n  }): Promise<{\n    sets: Selectable<SetDetail & { setSize: number }>[]\n    cursor?: string\n  }> {\n    let qb = this.buildQueryForSetWithSize().limit(limit)\n\n    if (namePrefix) {\n      qb = qb.where('s.name', 'like', `${namePrefix}%`)\n    }\n\n    if (cursor) {\n      if (sortBy === 'name') {\n        qb = qb.where('s.name', sortDirection === 'asc' ? '>' : '<', cursor)\n      } else {\n        qb = qb.where(\n          `s.${sortBy}`,\n          sortDirection === 'asc' ? '>' : '<',\n          new Date(cursor),\n        )\n      }\n    }\n\n    qb = qb.orderBy(`s.${sortBy}`, sortDirection)\n\n    const sets = await qb.execute()\n    const lastItem = sets.at(-1)\n\n    return {\n      sets,\n      cursor: lastItem\n        ? sortBy === 'name'\n          ? lastItem?.name\n          : lastItem?.[sortBy].toISOString()\n        : undefined,\n    }\n  }\n\n  async getByName(name: string): Promise<Selectable<SetDetail> | undefined> {\n    const query = this.db.db\n      .selectFrom('set_detail')\n      .selectAll()\n      .where('name', '=', name)\n\n    return await query.executeTakeFirst()\n  }\n\n  async getByNameWithSize(\n    name: string,\n  ): Promise<Selectable<SetDetail & { setSize: number }> | undefined> {\n    return await this.buildQueryForSetWithSize()\n      .where('s.name', '=', name)\n      .executeTakeFirst()\n  }\n\n  async getSetWithValues({\n    name,\n    limit,\n    cursor,\n  }: {\n    name: string\n    limit: number\n    cursor?: string\n  }): Promise<\n    | {\n        set: Selectable<SetDetail & { setSize: number }>\n        values: string[]\n        cursor?: string\n      }\n    | undefined\n  > {\n    const set = await this.getByNameWithSize(name)\n    if (!set) return undefined\n\n    const { ref } = this.db.db.dynamic\n    const qb = this.db.db\n      .selectFrom('set_value')\n      .selectAll()\n      .where('setId', '=', set.id)\n\n    const keyset = new TimeIdKeyset(ref(`createdAt`), ref('id'))\n    const paginatedBuilder = paginate(qb, {\n      limit,\n      cursor,\n      keyset,\n      direction: 'asc',\n    })\n\n    const result = await paginatedBuilder.execute()\n\n    return {\n      set,\n      values: result.map((v) => v.value),\n      cursor: keyset.packFromResult(result),\n    }\n  }\n  async upsert({\n    name,\n    description,\n  }: Pick<SetDetail, 'name' | 'description'>): Promise<void> {\n    await this.db.db\n      .insertInto('set_detail')\n      .values({\n        name,\n        description,\n        updatedAt: new Date(),\n      })\n      .onConflict((oc) => {\n        // if description is provided as a string, even an empty one, update it\n        // otherwise, just update the updatedAt timestamp\n        return oc.column('name').doUpdateSet(\n          typeof description === 'string'\n            ? {\n                description,\n                updatedAt: new Date(),\n              }\n            : { updatedAt: new Date() },\n        )\n      })\n      .execute()\n  }\n\n  async addValues(setId: number, values: string[]): Promise<void> {\n    await this.db.transaction(async (txn) => {\n      const now = new Date()\n      const query = txn.db\n        .insertInto('set_value')\n        .values(\n          values.map((value) => ({\n            setId,\n            value,\n            createdAt: now,\n          })),\n        )\n        .onConflict((oc) => oc.columns(['setId', 'value']).doNothing())\n\n      await query.execute()\n\n      // Update the set's updatedAt timestamp\n      await txn.db\n        .updateTable('set_detail')\n        .set({ updatedAt: now })\n        .where('id', '=', setId)\n        .execute()\n    })\n  }\n\n  async removeValues(setId: number, values: string[]): Promise<void> {\n    if (values.length < 1) {\n      return\n    }\n    await this.db.transaction(async (txn) => {\n      const query = txn.db\n        .deleteFrom('set_value')\n        .where('setId', '=', setId)\n        .where('value', 'in', values)\n\n      await query.execute()\n\n      // Update the set's updatedAt timestamp\n      await txn.db\n        .updateTable('set_detail')\n        .set({ updatedAt: new Date() })\n        .where('id', '=', setId)\n        .execute()\n    })\n  }\n\n  async removeSet(setId: number): Promise<void> {\n    await this.db.transaction(async (txn) => {\n      await txn.db.deleteFrom('set_value').where('setId', '=', setId).execute()\n      await txn.db.deleteFrom('set_detail').where('id', '=', setId).execute()\n    })\n  }\n\n  view(set: Selectable<SetDetail> & { setSize: number }): SetView {\n    return {\n      name: set.name,\n      description: set.description || undefined,\n      setSize: set.setSize,\n      createdAt: set.createdAt.toISOString(),\n      updatedAt: set.updatedAt.toISOString(),\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/setting/constants.ts",
    "content": "export const ProtectedTagSettingKey = 'tools.ozone.setting.protectedTags'\nexport const PolicyListSettingKey = 'tools.ozone.setting.policyList'\nexport const SeverityLevelSettingKey = 'tools.ozone.setting.severityLevels'\n"
  },
  {
    "path": "packages/ozone/src/setting/service.ts",
    "content": "import assert from 'node:assert'\nimport { Selectable } from 'kysely'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { Database } from '../db'\nimport { Member } from '../db/schema/member'\nimport { Setting, SettingScope } from '../db/schema/setting'\nimport { Option } from '../lexicon/types/tools/ozone/setting/defs'\n\nexport type SettingServiceCreator = (db: Database) => SettingService\n\nexport class SettingService {\n  constructor(public db: Database) {}\n\n  static creator() {\n    return (db: Database) => new SettingService(db)\n  }\n\n  async query({\n    limit = 100,\n    scope,\n    did,\n    cursor,\n    prefix,\n    keys,\n  }: {\n    limit: number\n    scope?: 'personal' | 'instance'\n    did?: string\n    cursor?: string\n    prefix?: string\n    keys?: string[]\n  }): Promise<{\n    options: Selectable<Setting>[]\n    cursor?: string\n  }> {\n    let builder = this.db.db.selectFrom('setting').selectAll()\n\n    if (prefix) {\n      builder = builder.where('key', 'like', `${prefix}%`)\n    } else if (keys?.length) {\n      builder = builder.where('key', 'in', keys)\n    }\n\n    if (scope) {\n      builder = builder.where('scope', '=', scope)\n    }\n\n    if (did) {\n      builder = builder.where('did', '=', did)\n    }\n\n    if (cursor) {\n      const cursorId = parseInt(cursor, 10)\n      if (isNaN(cursorId)) {\n        throw new InvalidRequestError('invalid cursor')\n      }\n      builder = builder.where('id', '<', cursorId)\n    }\n\n    const options = await builder.orderBy('id', 'desc').limit(limit).execute()\n\n    return {\n      options,\n      cursor: options[options.length - 1]?.id.toString(),\n    }\n  }\n\n  async upsert(\n    option: Omit<Setting, 'id' | 'createdAt' | 'updatedAt'> & {\n      createdAt: Date\n      updatedAt: Date\n    },\n  ): Promise<void> {\n    await this.db.db\n      .insertInto('setting')\n      .values(option)\n      .onConflict((oc) => {\n        return oc.columns(['key', 'scope', 'did']).doUpdateSet({\n          value: option.value,\n          updatedAt: option.updatedAt,\n          description: option.description,\n          managerRole: option.managerRole,\n          lastUpdatedBy: option.lastUpdatedBy,\n        })\n      })\n      .execute()\n  }\n\n  async removeOptions(\n    keys: string[],\n    filters: {\n      did?: string\n      scope: SettingScope\n      managerRole: Member['role'][]\n    },\n  ): Promise<void> {\n    if (!keys.length) return\n\n    if (filters.scope === 'personal') {\n      assert(filters.did, 'did is required for personal scope')\n    }\n\n    let qb = this.db.db\n      .deleteFrom('setting')\n      .where('key', 'in', keys)\n      .where('scope', '=', filters.scope)\n\n    if (filters.managerRole.length) {\n      qb = qb.where('managerRole', 'in', filters.managerRole)\n    } else {\n      qb = qb.where('managerRole', 'is', null)\n    }\n\n    if (filters.did) {\n      qb = qb.where('did', '=', filters.did)\n    }\n\n    await qb.execute()\n  }\n\n  view(setting: Selectable<Setting>): Option {\n    const {\n      key,\n      value,\n      did,\n      description,\n      createdAt,\n      createdBy,\n      updatedAt,\n      lastUpdatedBy,\n      managerRole,\n      scope,\n    } = setting\n\n    return {\n      key,\n      value,\n      did,\n      scope,\n      createdBy,\n      lastUpdatedBy,\n      managerRole: managerRole || undefined,\n      description: description || undefined,\n      createdAt: createdAt.toISOString(),\n      updatedAt: updatedAt.toISOString(),\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/setting/types.ts",
    "content": "export type ProtectedTagSetting = {\n  [key: string]: { roles?: string[]; moderators?: string[] }\n}\n"
  },
  {
    "path": "packages/ozone/src/setting/validators.ts",
    "content": "import { Selectable } from 'kysely'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { Setting } from '../db/schema/setting'\nimport {\n  PolicyListSettingKey,\n  ProtectedTagSettingKey,\n  SeverityLevelSettingKey,\n} from './constants'\n\nexport const settingValidators = new Map<\n  string,\n  (setting: Partial<Selectable<Setting>>) => Promise<void>\n>([\n  [\n    ProtectedTagSettingKey,\n    /*\n     * Example configuration:\n     * {\n     *   \"sensitive-tag\": {\n     *     \"roles\": [\"tools.ozone.team.defs#roleAdmin\", \"tools.ozone.team.defs#roleModerator\"],\n     *     \"moderators\": [\"did:plc:example1\", \"did:plc:example2\"]\n     *   },\n     *   \"high-risk-tag\": {\n     *     \"roles\": [\"tools.ozone.team.defs#roleAdmin\"]\n     *   },\n     *   \"admin-only-tag\": {\n     *     \"moderators\": [\"did:plc:admin1\"]\n     *   }\n     * }\n     */\n    async (setting: Partial<Selectable<Setting>>) => {\n      if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {\n        throw new InvalidRequestError(\n          'Only admins should be able to configure protected tags',\n        )\n      }\n\n      if (typeof setting.value !== 'object') {\n        throw new InvalidRequestError('Invalid value')\n      }\n      for (const [key, val] of Object.entries(setting.value)) {\n        if (!val || typeof val !== 'object') {\n          throw new InvalidRequestError(`Invalid configuration for tag ${key}`)\n        }\n\n        if (!val['roles'] && !val['moderators']) {\n          throw new InvalidRequestError(\n            `Must define who a list of moderators or a role who can action subjects with ${key} tag`,\n          )\n        }\n\n        if (val['roles']) {\n          if (!Array.isArray(val['roles'])) {\n            throw new InvalidRequestError(\n              `Roles must be an array of moderator roles for tag ${key}`,\n            )\n          }\n          if (!val['roles']?.length) {\n            throw new InvalidRequestError(\n              `Must define at least one role for tag ${key}`,\n            )\n          }\n        }\n\n        if (val['moderators']) {\n          if (!Array.isArray(val['moderators'])) {\n            throw new InvalidRequestError(\n              `Moderators must be an array of moderator DIDs for tag ${key}`,\n            )\n          }\n          if (!val['moderators']?.length) {\n            throw new InvalidRequestError(\n              `Must define at least one moderator DID for tag ${key}`,\n            )\n          }\n        }\n      }\n    },\n  ],\n  [\n    PolicyListSettingKey,\n    /*\n     * Example configuration:\n     * {\n     *   \"harassment\": {\n     *     \"name\": \"Anti-Harassment\",\n     *     \"description\": \"Content that harasses, intimidates, or bullies users\",\n     *     \"severityLevels\": {\n     *       \"sev-1\": {\n     *         \"description\": \"Minor harassment\",\n     *         \"isDefault\": true\n     *       },\n     *       \"sev-2\": {\n     *         \"description\": \"Moderate harassment\",\n     *         \"isDefault\": false\n     *       },\n     *       \"sev-4\": {\n     *         \"description\": \"Severe harassment\",\n     *         \"isDefault\": false\n     *       }\n     *     }\n     *   },\n     *   \"death-threats\": {\n     *     \"name\": \"Death Threats\",\n     *     \"description\": \"Threats of violence or death against individuals\",\n     *     \"severityLevels\": {\n     *       \"death-threat\": {\n     *         \"description\": \"Death threat violation\",\n     *         \"isDefault\": true\n     *       }\n     *     }\n     *   },\n     *   \"spam\": {\n     *     \"name\": \"Spam\",\n     *     \"description\": \"Unsolicited or repetitive content\",\n     *     \"severityLevels\": {\n     *       \"sev-0\": {\n     *         \"description\": \"Minor spam\",\n     *         \"isDefault\": false\n     *       },\n     *       \"sev-1\": {\n     *         \"description\": \"Moderate spam\",\n     *         \"isDefault\": true\n     *       },\n     *       \"sev-2\": {\n     *         \"description\": \"Severe spam\",\n     *         \"isDefault\": false\n     *       }\n     *     }\n     *   },\n     *   \"minimal-policy\": {\n     *     \"name\": \"Basic Policy\",\n     *     \"description\": \"Simple policy without severity levels\"\n     *   }\n     * }\n     */\n    async (setting: Partial<Selectable<Setting>>) => {\n      if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {\n        throw new InvalidRequestError(\n          'Only admins should be able to manage policy list',\n        )\n      }\n\n      if (typeof setting.value !== 'object') {\n        throw new InvalidRequestError('Invalid value')\n      }\n      for (const [key, val] of Object.entries(setting.value)) {\n        if (!val || typeof val !== 'object') {\n          throw new InvalidRequestError(\n            `Invalid configuration for policy ${key}`,\n          )\n        }\n\n        if (!val['name'] || !val['description']) {\n          throw new InvalidRequestError(\n            `Must define a name and description for policy ${key}`,\n          )\n        }\n\n        if (val['severityLevels'] !== undefined) {\n          if (typeof val['severityLevels'] !== 'object') {\n            throw new InvalidRequestError(\n              `Severity levels must be an object for policy ${key}`,\n            )\n          }\n\n          let hasDefault = false\n          for (const [severityKey, severityVal] of Object.entries(\n            val['severityLevels'],\n          )) {\n            if (!severityVal || typeof severityVal !== 'object') {\n              throw new InvalidRequestError(\n                `Invalid configuration for severity level ${severityKey} in policy ${key}`,\n              )\n            }\n\n            if (\n              severityVal['description'] !== undefined &&\n              typeof severityVal['description'] !== 'string'\n            ) {\n              throw new InvalidRequestError(\n                `Description must be a string for severity level ${severityKey} in policy ${key}`,\n              )\n            }\n\n            if (severityVal['isDefault'] !== undefined) {\n              if (typeof severityVal['isDefault'] !== 'boolean') {\n                throw new InvalidRequestError(\n                  `isDefault must be a boolean for severity level ${severityKey} in policy ${key}`,\n                )\n              }\n              if (severityVal['isDefault']) {\n                if (hasDefault) {\n                  throw new InvalidRequestError(\n                    `Only one severity level can be the default for policy ${key}`,\n                  )\n                }\n                hasDefault = true\n              }\n            }\n\n            if (severityVal['targetServices'] !== undefined) {\n              if (!Array.isArray(severityVal['targetServices'])) {\n                throw new InvalidRequestError(\n                  `targetServices must be an array for severity level ${severityKey} in policy ${key}`,\n                )\n              }\n              for (const service of severityVal['targetServices']) {\n                if (typeof service !== 'string') {\n                  throw new InvalidRequestError(\n                    `Each target service must be a string for severity level ${severityKey} in policy ${key}`,\n                  )\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n  ],\n  [\n    SeverityLevelSettingKey,\n    /*\n     * Example configuration:\n     * {\n     *   \"sev-0\": {\n     *     \"strikeCount\": 0\n     *   },\n     *   \"sev-1\": {\n     *     \"strikeCount\": 1,\n     *     \"strikeOnOccurrence\": 2\n     *   },\n     *   \"sev-2\": {\n     *     \"strikeCount\": 2\n     *   },\n     *   \"sev-4\": {\n     *     \"strikeCount\": 4,\n     *     \"expiresInDays\": 365\n     *   },\n     *   \"sev-5\": {\n     *     \"needsTakedown\": true\n     *   },\n     *   \"death-threat\": {\n     *     \"strikeCount\": 4,\n     *     \"firstOccurrenceStrikeCount\": 4,\n     *   },\n     *   \"custom-severity\": {\n     *     \"strikeCount\": 3,\n     *     \"strikeOnOccurrence\": 1,\n     *   },\n     *   \"escalating-severity\": {\n     *     \"firstOccurrenceStrikeCount\": 2,\n     *     \"repeatOccurrenceStrikeCount\": 5\n     *   }\n     * }\n     */\n    async (setting: Partial<Selectable<Setting>>) => {\n      if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {\n        throw new InvalidRequestError(\n          'Only admins should be able to manage severity levels',\n        )\n      }\n\n      if (typeof setting.value !== 'object') {\n        throw new InvalidRequestError('Invalid value')\n      }\n\n      for (const [key, val] of Object.entries(setting.value)) {\n        if (!val || typeof val !== 'object') {\n          throw new InvalidRequestError(\n            `Invalid configuration for severity level ${key}`,\n          )\n        }\n\n        if (val['strikeCount'] !== undefined) {\n          if (\n            typeof val['strikeCount'] !== 'number' ||\n            !Number.isInteger(val['strikeCount']) ||\n            val['strikeCount'] < 0\n          ) {\n            throw new InvalidRequestError(\n              `Strike count must be a non-negative integer for severity level ${key}`,\n            )\n          }\n        }\n\n        if (val['strikeOnOccurrence'] !== undefined) {\n          if (\n            typeof val['strikeOnOccurrence'] !== 'number' ||\n            !Number.isInteger(val['strikeOnOccurrence']) ||\n            val['strikeOnOccurrence'] < 1\n          ) {\n            throw new InvalidRequestError(\n              `Strike on occurrence must be a positive integer for severity level ${key}`,\n            )\n          }\n        }\n\n        if (val['needsTakedown'] !== undefined) {\n          if (typeof val['needsTakedown'] !== 'boolean') {\n            throw new InvalidRequestError(\n              `Needs takedown must be a boolean for severity level ${key}`,\n            )\n          }\n        }\n\n        if (val['expiresInDays'] !== undefined) {\n          if (\n            typeof val['expiresInDays'] !== 'number' ||\n            !Number.isInteger(val['expiresInDays']) ||\n            val['expiresInDays'] < 0\n          ) {\n            throw new InvalidRequestError(\n              `Expires in days must be a non-negative integer for severity level ${key}`,\n            )\n          }\n        }\n\n        if (val['firstOccurrenceStrikeCount'] !== undefined) {\n          if (\n            typeof val['firstOccurrenceStrikeCount'] !== 'number' ||\n            !Number.isInteger(val['firstOccurrenceStrikeCount']) ||\n            val['firstOccurrenceStrikeCount'] < 0\n          ) {\n            throw new InvalidRequestError(\n              `First occurrence strike count must be a non-negative integer for severity level ${key}`,\n            )\n          }\n        }\n      }\n    },\n  ],\n])\n"
  },
  {
    "path": "packages/ozone/src/tag-service/content-tagger.ts",
    "content": "import { ModerationService } from '../mod-service'\nimport { ModSubject } from '../mod-service/subject'\nimport { ModerationSubjectStatusRow } from '../mod-service/types'\n\nexport abstract class ContentTagger {\n  constructor(\n    protected subject: ModSubject,\n    protected subjectStatus: ModerationSubjectStatusRow | null,\n    protected moderationService: ModerationService,\n  ) {}\n\n  protected abstract tagPrefix: string\n\n  protected abstract isApplicable(): boolean\n  protected abstract buildTags(): Promise<string[]>\n\n  async getTags(): Promise<string[]> {\n    if (!this.isApplicable()) {\n      return []\n    }\n\n    return this.buildTags()\n  }\n\n  protected tagAlreadyExists(): boolean {\n    return Boolean(\n      this.subjectStatus?.tags?.some((tag) => tag.startsWith(this.tagPrefix)),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/tag-service/embed-tagger.ts",
    "content": "import {\n  AppBskyEmbedExternal,\n  AppBskyEmbedImages,\n  AppBskyEmbedRecordWithMedia,\n  AppBskyEmbedVideo,\n  AppBskyFeedPost,\n} from '@atproto/api'\nimport { ids } from '../lexicon/lexicons'\nimport { langLogger as log } from '../logger'\nimport { ContentTagger } from './content-tagger'\n\nexport class EmbedTagger extends ContentTagger {\n  tagPrefix = 'embed:'\n\n  isApplicable(): boolean {\n    return (\n      !!this.subjectStatus &&\n      !this.tagAlreadyExists() &&\n      this.subject.isRecord() &&\n      this.subject.parsedUri.collection === ids.AppBskyFeedPost\n    )\n  }\n\n  async buildTags(): Promise<string[]> {\n    try {\n      const recordValue = await this.getRecordValue()\n      if (!recordValue) {\n        return []\n      }\n      const tags: string[] = []\n      const result = AppBskyFeedPost.validateRecord(recordValue)\n\n      if (result.success) {\n        const embedContent = AppBskyEmbedRecordWithMedia.isMain(\n          result.value.embed,\n        )\n          ? result.value.embed.media\n          : result.value.embed\n\n        if (AppBskyEmbedImages.isMain(embedContent)) {\n          tags.push(`${this.tagPrefix}image`)\n        }\n\n        if (AppBskyEmbedVideo.isMain(embedContent)) {\n          tags.push(`${this.tagPrefix}video`)\n        }\n\n        if (AppBskyEmbedExternal.isMain(embedContent)) {\n          tags.push(`${this.tagPrefix}external`)\n        }\n      }\n      return tags\n    } catch (err) {\n      log.error({ subject: this.subject, err }, 'Error getting record langs')\n      return []\n    }\n  }\n\n  async getRecordValue(): Promise<Record<string, unknown> | undefined> {\n    if (!this.subject.isRecord()) {\n      return undefined\n    }\n    const recordByUri = await this.moderationService.views.fetchRecords([\n      this.subject,\n    ])\n\n    const record = recordByUri.get(this.subject.uri)\n    return record?.value\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/tag-service/index.ts",
    "content": "import { langLogger as log } from '../logger'\nimport { ModerationService } from '../mod-service'\nimport { ModSubject } from '../mod-service/subject'\nimport { ModerationSubjectStatusRow } from '../mod-service/types'\nimport { ContentTagger } from './content-tagger'\nimport { EmbedTagger } from './embed-tagger'\nimport { LanguageTagger } from './language-tagger'\n\nexport class TagService {\n  private taggers: ContentTagger[]\n\n  constructor(\n    private subject: ModSubject,\n    protected subjectStatus: ModerationSubjectStatusRow | null,\n    private taggerDid: string,\n    private moderationService: ModerationService,\n  ) {\n    this.taggers = [\n      new LanguageTagger(subject, subjectStatus, moderationService),\n      new EmbedTagger(subject, subjectStatus, moderationService),\n      // Add more taggers as needed\n    ]\n  }\n\n  // Allow the caller to seed the initial tags\n  async evaluateForSubject(initialTags?: Iterable<string>) {\n    try {\n      const tags = new Set(initialTags)\n\n      await Promise.all(\n        this.taggers.map(async (tagger) => {\n          try {\n            const newTags = await tagger.getTags()\n            for (const newTag of newTags) {\n              tags.add(newTag)\n            }\n          } catch (e) {\n            // Don't let one tagger error stop the rest from running\n            log.error(\n              { subject: this.subject, err: e },\n              'Error applying tagger',\n            )\n          }\n        }),\n      )\n\n      // Ensure that before inserting new tags, we discard any tag that may\n      // have been evaluated to be added but is already present in the subject\n      if (this.subjectStatus?.tags?.length) {\n        for (const tag of this.subjectStatus.tags) {\n          tags.delete(tag)\n        }\n      }\n\n      if (tags.size) {\n        await this.moderationService.logEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: [...tags],\n            remove: [],\n          },\n          subject: this.subject,\n          createdBy: this.taggerDid,\n        })\n      }\n    } catch (err) {\n      log.error({ subject: this.subject, err }, 'Error tagging subject')\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/tag-service/language-data.ts",
    "content": "// Also used in the client app https://github.com/bluesky-social/social-app/blob/main/src/locale/languages.ts\ninterface Language {\n  code3: string\n  code2: string\n  name: string\n}\n\nexport const LANGUAGES: Language[] = [\n  { code3: 'aar', code2: 'aa', name: 'Afar' },\n  { code3: 'abk', code2: 'ab', name: 'Abkhazian' },\n  { code3: 'ace', code2: '', name: 'Achinese' },\n  { code3: 'ach', code2: '', name: 'Acoli' },\n  { code3: 'ada', code2: '', name: 'Adangme' },\n  { code3: 'ady', code2: '', name: 'Adyghe; Adygei' },\n  { code3: 'afa', code2: '', name: 'Afro-Asiatic languages' },\n  { code3: 'afh', code2: '', name: 'Afrihili' },\n  { code3: 'afr', code2: 'af', name: 'Afrikaans' },\n  { code3: 'ain', code2: '', name: 'Ainu' },\n  { code3: 'aka', code2: 'ak', name: 'Akan' },\n  { code3: 'akk', code2: '', name: 'Akkadian' },\n  { code3: 'alb', code2: 'sq', name: 'Albanian' },\n  { code3: 'ale', code2: '', name: 'Aleut' },\n  { code3: 'alg', code2: '', name: 'Algonquian languages' },\n  { code3: 'alt', code2: '', name: 'Southern Altai' },\n  { code3: 'amh', code2: 'am', name: 'Amharic' },\n  { code3: 'ang', code2: '', name: 'English, Old (ca.450-1100)' },\n  { code3: 'anp ', code2: 'Angika', name: 'Angika' },\n  { code3: 'apa', code2: '', name: 'Apache languages' },\n  { code3: 'ara', code2: 'ar', name: 'Arabic' },\n  {\n    code3: 'arc',\n    code2: '',\n    name: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)',\n  },\n  { code3: 'arg', code2: 'an', name: 'Aragonese' },\n  { code3: 'arm', code2: 'hy', name: 'Armenian' },\n  { code3: 'arn', code2: '', name: 'Mapudungun; Mapuche' },\n  { code3: 'arp', code2: '', name: 'Arapaho' },\n  { code3: 'art', code2: '', name: 'Artificial languages' },\n  { code3: 'arw', code2: '', name: 'Arawak' },\n  { code3: 'asm', code2: 'as', name: 'Assamese' },\n  { code3: 'ast', code2: '', name: 'Asturian; Bable; Leonese; Asturleonese' },\n  { code3: 'ath', code2: '', name: 'Athapascan languages' },\n  { code3: 'aus', code2: '', name: 'Australian languages' },\n  { code3: 'ava', code2: 'av', name: 'Avaric' },\n  { code3: 'ave', code2: 'ae', name: 'Avestan' },\n  { code3: 'awa', code2: '', name: 'Awadhi' },\n  { code3: 'aym', code2: 'ay', name: 'Aymara' },\n  { code3: 'aze', code2: 'az', name: 'Azerbaijani' },\n  { code3: 'bad', code2: '', name: 'Banda languages' },\n  { code3: 'bai', code2: '', name: 'Bamileke languages' },\n  { code3: 'bak', code2: 'ba', name: 'Bashkir' },\n  { code3: 'bal', code2: '', name: 'Baluchi' },\n  { code3: 'bam', code2: 'bm', name: 'Bambara' },\n  { code3: 'ban', code2: '', name: 'Balinese' },\n  { code3: 'baq', code2: 'eu', name: 'Basque' },\n  { code3: 'bas', code2: '', name: 'Basa' },\n  { code3: 'bat', code2: '', name: 'Baltic languages' },\n  { code3: 'bej', code2: '', name: 'Beja; Bedawiyet' },\n  { code3: 'bel', code2: 'be', name: 'Belarusian' },\n  { code3: 'bem', code2: '', name: 'Bemba' },\n  { code3: 'ben', code2: 'bn', name: 'Bengali' },\n  { code3: 'ber', code2: '', name: 'Berber languages' },\n  { code3: 'bho', code2: '', name: 'Bhojpuri' },\n  { code3: 'bih', code2: 'bh', name: 'Bihari languages' },\n  { code3: 'bik', code2: '', name: 'Bikol' },\n  { code3: 'bin', code2: '', name: 'Bini; Edo' },\n  { code3: 'bis', code2: 'bi', name: 'Bislama' },\n  { code3: 'bla', code2: '', name: 'Siksika' },\n  { code3: 'bnt', code2: '', name: 'Bantu languages' },\n  { code3: 'bod', code2: 'bo', name: 'Tibetan' },\n  { code3: 'bos', code2: 'bs', name: 'Bosnian' },\n  { code3: 'bra', code2: '', name: 'Braj' },\n  { code3: 'bre', code2: 'br', name: 'Breton' },\n  { code3: 'btk', code2: '', name: 'Batak languages' },\n  { code3: 'bua', code2: '', name: 'Buriat' },\n  { code3: 'bug', code2: '', name: 'Buginese' },\n  { code3: 'bul', code2: 'bg', name: 'Bulgarian' },\n  { code3: 'bur', code2: 'my', name: 'Burmese' },\n  { code3: 'byn', code2: '', name: 'Blin; Bilin' },\n  { code3: 'cad', code2: '', name: 'Caddo' },\n  { code3: 'cai', code2: '', name: 'Central American Indian languages' },\n  { code3: 'car', code2: '', name: 'Galibi Carib' },\n  { code3: 'cat', code2: 'ca', name: 'Catalan; Valencian' },\n  { code3: 'cau', code2: '', name: 'Caucasian languages' },\n  { code3: 'ceb', code2: '', name: 'Cebuano' },\n  { code3: 'cel', code2: '', name: 'Celtic languages' },\n  { code3: 'ces', code2: 'cs', name: 'Czech' },\n  { code3: 'cha', code2: 'ch', name: 'Chamorro' },\n  { code3: 'chb', code2: '', name: 'Chibcha' },\n  { code3: 'che', code2: 'ce', name: 'Chechen' },\n  { code3: 'chg', code2: '', name: 'Chagatai' },\n  { code3: 'chi', code2: 'zh', name: 'Chinese' },\n  { code3: 'chk', code2: '', name: 'Chuukese' },\n  { code3: 'chm', code2: '', name: 'Mari' },\n  { code3: 'chn', code2: '', name: 'Chinook jargon' },\n  { code3: 'cho', code2: '', name: 'Choctaw' },\n  { code3: 'chp', code2: '', name: 'Chipewyan; Dene Suline' },\n  { code3: 'chr', code2: '', name: 'Cherokee' },\n  {\n    code3: 'chu',\n    code2: 'cu',\n    name: 'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic',\n  },\n  { code3: 'chv', code2: 'cv', name: 'Chuvash' },\n  { code3: 'chy', code2: '', name: 'Cheyenne' },\n  { code3: 'cmc', code2: '', name: 'Chamic languages' },\n  { code3: 'cnr', code2: '', name: 'Montenegrin' },\n  { code3: 'cop', code2: '', name: 'Coptic' },\n  { code3: 'cor', code2: 'kw', name: 'Cornish' },\n  { code3: 'cos', code2: 'co', name: 'Corsican' },\n  { code3: 'cpe', code2: '', name: 'Creoles and pidgins, English based' },\n  { code3: 'cpf', code2: '', name: 'Creoles and pidgins, French-based' },\n  { code3: 'cpp', code2: '', name: 'Creoles and pidgins, Portuguese-based' },\n  { code3: 'cre', code2: 'cr', name: 'Cree' },\n  { code3: 'crh', code2: '', name: 'Crimean Tatar; Crimean Turkish' },\n  { code3: 'crp', code2: '', name: 'Creoles and pidgins' },\n  { code3: 'csb', code2: '', name: 'Kashubian' },\n  { code3: 'cus', code2: '', name: 'Cushitic languages' },\n  { code3: 'cym', code2: 'cy', name: 'Welsh' },\n  { code3: 'cze', code2: 'cs', name: 'Czech' },\n  { code3: 'dak', code2: '', name: 'Dakota' },\n  { code3: 'dan', code2: 'da', name: 'Danish' },\n  { code3: 'dar', code2: '', name: 'Dargwa' },\n  { code3: 'day', code2: '', name: 'Land Dayak languages' },\n  { code3: 'del', code2: '', name: 'Delaware' },\n  { code3: 'den', code2: '', name: 'Slave (Athapascan)' },\n  { code3: 'deu', code2: 'de', name: 'German' },\n  { code3: 'dgr', code2: '', name: 'Dogrib' },\n  { code3: 'din', code2: '', name: 'Dinka' },\n  { code3: 'div', code2: 'dv', name: 'Divehi; Dhivehi; Maldivian' },\n  { code3: 'doi', code2: '', name: 'Dogri' },\n  { code3: 'dra', code2: '', name: 'Dravidian languages' },\n  { code3: 'dsb', code2: '', name: 'Lower Sorbian' },\n  { code3: 'dua', code2: '', name: 'Duala' },\n  { code3: 'dum', code2: '', name: 'Dutch, Middle (ca.1050-1350)' },\n  { code3: 'dut', code2: 'nl', name: 'Dutch; Flemish' },\n  { code3: 'dyu', code2: '', name: 'Dyula' },\n  { code3: 'dzo', code2: 'dz', name: 'Dzongkha' },\n  { code3: 'efi', code2: '', name: 'Efik' },\n  { code3: 'egy', code2: '', name: 'Egyptian (Ancient)' },\n  { code3: 'eka', code2: '', name: 'Ekajuk' },\n  { code3: 'ell', code2: 'el', name: 'Greek, Modern (1453-)' },\n  { code3: 'elx', code2: '', name: 'Elamite' },\n  { code3: 'eng', code2: 'en', name: 'English' },\n  { code3: 'enm', code2: '', name: 'English, Middle (1100-1500)' },\n  { code3: 'epo', code2: 'eo', name: 'Esperanto' },\n  { code3: 'est', code2: 'et', name: 'Estonian' },\n  { code3: 'eus', code2: 'eu', name: 'Basque' },\n  { code3: 'ewe', code2: 'ee', name: 'Ewe' },\n  { code3: 'ewo', code2: '', name: 'Ewondo' },\n  { code3: 'fan', code2: '', name: 'Fang' },\n  { code3: 'fao', code2: 'fo', name: 'Faroese' },\n  { code3: 'fas', code2: 'fa', name: 'Persian' },\n  { code3: 'fat', code2: '', name: 'Fanti' },\n  { code3: 'fij', code2: 'fj', name: 'Fijian' },\n  { code3: 'fil', code2: '', name: 'Filipino; Pilipino' },\n  { code3: 'fin', code2: 'fi', name: 'Finnish' },\n  { code3: 'fiu', code2: '', name: 'Finno-Ugrian languages' },\n  { code3: 'fon', code2: '', name: 'Fon' },\n  { code3: 'fra', code2: 'fr', name: 'French' },\n  { code3: 'fre', code2: 'fr', name: 'French' },\n  { code3: 'frm', code2: '', name: 'French, Middle (ca.1400-1600)' },\n  { code3: 'fro', code2: '', name: 'French, Old (842-ca.1400)' },\n  { code3: 'frr', code2: '', name: 'Northern Frisian' },\n  { code3: 'frs', code2: '', name: 'Eastern Frisian' },\n  { code3: 'fry', code2: 'fy', name: 'Western Frisian' },\n  { code3: 'ful', code2: 'ff', name: 'Fulah' },\n  { code3: 'fur', code2: '', name: 'Friulian' },\n  { code3: 'gaa', code2: '', name: 'Ga' },\n  { code3: 'gay', code2: '', name: 'Gayo' },\n  { code3: 'gba', code2: '', name: 'Gbaya' },\n  { code3: 'gem', code2: '', name: 'Germanic languages' },\n  { code3: 'geo', code2: 'ka', name: 'Georgian' },\n  { code3: 'ger', code2: 'de', name: 'German' },\n  { code3: 'gez', code2: '', name: 'Geez' },\n  { code3: 'gil', code2: '', name: 'Gilbertese' },\n  { code3: 'gla', code2: 'gd', name: 'Gaelic; Scottish Gaelic' },\n  { code3: 'gle', code2: 'ga', name: 'Irish' },\n  { code3: 'glg', code2: 'gl', name: 'Galician' },\n  { code3: 'glv', code2: 'gv', name: 'Manx' },\n  { code3: 'gmh', code2: '', name: 'German, Middle High (ca.1050-1500)' },\n  { code3: 'goh', code2: '', name: 'German, Old High (ca.750-1050)' },\n  { code3: 'gon', code2: '', name: 'Gondi' },\n  { code3: 'gor', code2: '', name: 'Gorontalo' },\n  { code3: 'got', code2: '', name: 'Gothic' },\n  { code3: 'grb', code2: '', name: 'Grebo' },\n  { code3: 'grc', code2: '', name: 'Greek, Ancient (to 1453)' },\n  { code3: 'gre', code2: 'el', name: 'Greek, Modern (1453-)' },\n  { code3: 'grn', code2: 'gn', name: 'Guarani' },\n  { code3: 'gsw', code2: '', name: 'Swiss German; Alemannic; Alsatian' },\n  { code3: 'gujgu', code2: 'Gujarati', name: 'goudjrati' },\n  { code3: 'gwi', code2: '', name: \"Gwich'in\" },\n  { code3: 'hai', code2: '', name: 'Haida' },\n  { code3: 'hat', code2: 'ht', name: 'Haitian; Haitian Creole' },\n  { code3: 'hau', code2: 'ha', name: 'Hausa' },\n  { code3: 'haw', code2: '', name: 'Hawaiian' },\n  { code3: 'heb', code2: 'he', name: 'Hebrew' },\n  { code3: 'her', code2: 'hz', name: 'Herero' },\n  { code3: 'hil', code2: '', name: 'Hiligaynon' },\n  {\n    code3: 'him',\n    code2: '',\n    name: 'Himachali languages; Western Pahari languages',\n  },\n  { code3: 'hin', code2: 'hi', name: 'Hindi' },\n  { code3: 'hit', code2: '', name: 'Hittite' },\n  { code3: 'hmn', code2: '', name: 'Hmong; Mong' },\n  { code3: 'hmo', code2: 'ho', name: 'Hiri Motu' },\n  { code3: 'hrv', code2: 'hr', name: 'Croatian' },\n  { code3: 'hsb', code2: '', name: 'Upper Sorbian' },\n  { code3: 'hun', code2: 'hu', name: 'Hungarian' },\n  { code3: 'hup', code2: '', name: 'Hupa' },\n  { code3: 'hye', code2: 'hy', name: 'Armenian' },\n  { code3: 'iba', code2: '', name: 'Iban' },\n  { code3: 'ibo', code2: 'ig', name: 'Igbo' },\n  { code3: 'ice', code2: 'is', name: 'Icelandic' },\n  { code3: 'ido', code2: 'io', name: 'Ido' },\n  { code3: 'iii', code2: 'ii', name: 'Sichuan Yi; Nuosu' },\n  { code3: 'ijo', code2: '', name: 'Ijo languages' },\n  { code3: 'iku', code2: 'iu', name: 'Inuktitut' },\n  { code3: 'ile', code2: 'ie', name: 'Interlingue; Occidental' },\n  { code3: 'ilo', code2: '', name: 'Iloko' },\n  {\n    code3: 'ina',\n    code2: 'ia',\n    name: 'Interlingua (International Auxiliary Language Association)',\n  },\n  { code3: 'inc', code2: '', name: 'Indic languages' },\n  { code3: 'ind', code2: 'id', name: 'Indonesian' },\n  { code3: 'ine', code2: '', name: 'Indo-European languages' },\n  { code3: 'inh', code2: '', name: 'Ingush' },\n  { code3: 'ipk', code2: 'ik', name: 'Inupiaq' },\n  { code3: 'ira', code2: '', name: 'Iranian languages' },\n  { code3: 'iro', code2: '', name: 'Iroquoian languages' },\n  { code3: 'isl', code2: 'is', name: 'Icelandic' },\n  { code3: 'ita', code2: 'it', name: 'Italian' },\n  { code3: 'jav', code2: 'jv', name: 'Javanese' },\n  { code3: 'jbo', code2: '', name: 'Lojban' },\n  { code3: 'jpn', code2: 'ja', name: 'Japanese' },\n  { code3: 'jpr', code2: '', name: 'Judeo-Persian' },\n  { code3: 'jrb', code2: '', name: 'Judeo-Arabic' },\n  { code3: 'kaa', code2: '', name: 'Kara-Kalpak' },\n  { code3: 'kab', code2: '', name: 'Kabyle' },\n  { code3: 'kac', code2: '', name: 'Kachin; Jingpho' },\n  { code3: 'kal', code2: 'kl', name: 'Kalaallisut; Greenlandic' },\n  { code3: 'kam', code2: '', name: 'Kamba' },\n  { code3: 'kan', code2: 'kn', name: 'Kannada' },\n  { code3: 'kar', code2: '', name: 'Karen languages' },\n  { code3: 'kas', code2: 'ks', name: 'Kashmiri' },\n  { code3: 'kat', code2: 'ka', name: 'Georgian' },\n  { code3: 'kau', code2: 'kr', name: 'Kanuri' },\n  { code3: 'kaw', code2: '', name: 'Kawi' },\n  { code3: 'kaz', code2: 'kk', name: 'Kazakh' },\n  { code3: 'kbd', code2: '', name: 'Kabardian' },\n  { code3: 'kha', code2: '', name: 'Khasi' },\n  { code3: 'khi', code2: '', name: 'Khoisan languages' },\n  { code3: 'khm', code2: 'km', name: 'Central Khmer' },\n  { code3: 'kho', code2: '', name: 'Khotanese; Sakan' },\n  { code3: 'kik', code2: 'ki', name: 'Kikuyu; Gikuyu' },\n  { code3: 'kin', code2: 'rw', name: 'Kinyarwanda' },\n  { code3: 'kir', code2: 'ky', name: 'Kirghiz; Kyrgyz' },\n  { code3: 'kmb', code2: '', name: 'Kimbundu' },\n  { code3: 'kok', code2: '', name: 'Konkani' },\n  { code3: 'kom', code2: 'kv', name: 'Komi' },\n  { code3: 'kon', code2: 'kg', name: 'Kongo' },\n  { code3: 'kor', code2: 'ko', name: 'Korean' },\n  { code3: 'kos', code2: '', name: 'Kosraean' },\n  { code3: 'kpe', code2: '', name: 'Kpelle' },\n  { code3: 'krc', code2: '', name: 'Karachay-Balkar' },\n  { code3: 'krl', code2: '', name: 'Karelian' },\n  { code3: 'kro', code2: '', name: 'Kru languages' },\n  { code3: 'kru', code2: '', name: 'Kurukh' },\n  { code3: 'kua', code2: 'kj', name: 'Kuanyama; Kwanyama' },\n  { code3: 'kum', code2: '', name: 'Kumyk' },\n  { code3: 'kur', code2: 'ku', name: 'Kurdish' },\n  { code3: 'kut', code2: '', name: 'Kutenai' },\n  { code3: 'lad', code2: '', name: 'Ladino' },\n  { code3: 'lah', code2: '', name: 'Lahnda' },\n  { code3: 'lam', code2: '', name: 'Lamba' },\n  { code3: 'lao', code2: 'lo', name: 'Lao' },\n  { code3: 'lat', code2: 'la', name: 'Latin' },\n  { code3: 'lav', code2: 'lv', name: 'Latvian' },\n  { code3: 'lez', code2: '', name: 'Lezghian' },\n  { code3: 'lim', code2: 'li', name: 'Limburgan; Limburger; Limburgish' },\n  { code3: 'lin', code2: 'ln', name: 'Lingala' },\n  { code3: 'lit', code2: 'lt', name: 'Lithuanian' },\n  { code3: 'lol', code2: '', name: 'Mongo' },\n  { code3: 'loz', code2: '', name: 'Lozi' },\n  { code3: 'ltz', code2: 'lb', name: 'Luxembourgish; Letzeburgesch' },\n  { code3: 'lua', code2: '', name: 'Luba-Lulua' },\n  { code3: 'lub', code2: 'lu', name: 'Luba-Katanga' },\n  { code3: 'lug', code2: 'lg', name: 'Ganda' },\n  { code3: 'lui', code2: '', name: 'Luiseno' },\n  { code3: 'lun', code2: '', name: 'Lunda' },\n  {\n    code3: 'luo',\n    code2: ' Luo (Kenya and Tanzania)',\n    name: 'luo (Kenya et Tanzanie)',\n  },\n  { code3: 'lus', code2: '', name: 'Lushai' },\n  { code3: 'mac', code2: 'mk', name: 'Macedonian' },\n  { code3: 'mad', code2: '', name: 'Madurese' },\n  { code3: 'mag', code2: '', name: 'Magahi' },\n  { code3: 'mah', code2: 'mh', name: 'Marshallese' },\n  { code3: 'mai', code2: '', name: 'Maithili' },\n  { code3: 'mak', code2: '', name: 'Makasar' },\n  { code3: 'mal', code2: 'ml', name: 'Malayalam' },\n  { code3: 'man', code2: '', name: 'Mandingo' },\n  { code3: 'mao', code2: 'mi', name: 'Maori' },\n  { code3: 'map', code2: '', name: 'Austronesian languages' },\n  { code3: 'mar', code2: 'mr', name: 'Marathi' },\n  { code3: 'mas', code2: '', name: 'Masai' },\n  { code3: 'may', code2: 'ms', name: 'Malay' },\n  { code3: 'mdf', code2: '', name: 'Moksha' },\n  { code3: 'mdr', code2: '', name: 'Mandar' },\n  { code3: 'men', code2: '', name: 'Mende' },\n  { code3: 'mga', code2: '', name: 'Irish, Middle (900-1200)' },\n  { code3: 'mic', code2: '', name: \"Mi'kmaq; Micmac\" },\n  { code3: 'min', code2: '', name: 'Minangkabau' },\n  { code3: 'mis', code2: '', name: 'Uncoded languages' },\n  { code3: 'mkd', code2: 'mk', name: 'Macedonian' },\n  { code3: 'mkh', code2: '', name: 'Mon-Khmer languages' },\n  { code3: 'mlg', code2: 'mg', name: 'Malagasy' },\n  { code3: 'mlt', code2: 'mt', name: 'Maltese' },\n  { code3: 'mnc', code2: '', name: 'Manchu' },\n  { code3: 'mni', code2: '', name: 'Manipuri' },\n  { code3: 'mno', code2: '', name: 'Manobo languages' },\n  { code3: 'moh', code2: '', name: 'Mohawk' },\n  { code3: 'mon', code2: 'mn', name: 'Mongolian' },\n  { code3: 'mos', code2: '', name: 'Mossi' },\n  { code3: 'mri', code2: 'mi', name: 'Maori' },\n  { code3: 'msa', code2: 'ms', name: 'Malay' },\n  { code3: 'mul', code2: '', name: 'Multiple languages' },\n  { code3: 'mun', code2: '', name: 'Munda languages' },\n  { code3: 'mus', code2: '', name: 'Creek' },\n  { code3: 'mwl', code2: '', name: 'Mirandese' },\n  { code3: 'mwr', code2: '', name: 'Marwari' },\n  { code3: 'mya', code2: 'my', name: 'Burmese' },\n  { code3: 'myn', code2: '', name: 'Mayan languages' },\n  { code3: 'myv', code2: '', name: 'Erzya' },\n  { code3: 'nah', code2: '', name: 'Nahuatl languages' },\n  { code3: 'nai', code2: '', name: 'North American Indian languages' },\n  { code3: 'nap', code2: '', name: 'Neapolitan' },\n  { code3: 'nau', code2: 'na', name: 'Nauru' },\n  { code3: 'nav', code2: 'nv', name: 'Navajo; Navaho' },\n  { code3: 'nbl', code2: 'nr', name: 'Ndebele, South; South Ndebele' },\n  { code3: 'nde', code2: 'nd', name: 'Ndebele, North; North Ndebele' },\n  { code3: 'ndo', code2: 'ng', name: 'Ndonga' },\n  {\n    code3: 'nds',\n    code2: '',\n    name: 'Low German; Low Saxon; German, Low; Saxon, Low',\n  },\n  { code3: 'nep', code2: 'ne', name: 'Nepali' },\n  { code3: 'new', code2: '', name: 'Nepal Bhasa; Newari' },\n  { code3: 'nia', code2: '', name: 'Nias' },\n  { code3: 'nic', code2: '', name: 'Niger-Kordofanian languages' },\n  { code3: 'niu', code2: '', name: 'Niuean' },\n  { code3: 'nld', code2: 'nl', name: 'Dutch; Flemish' },\n  { code3: 'nno', code2: 'nn', name: 'Norwegian Nynorsk; Nynorsk, Norwegian' },\n  { code3: 'nob', code2: 'nb', name: 'Bokmål, Norwegian; Norwegian Bokmål' },\n  { code3: 'nog', code2: '', name: 'Nogai' },\n  { code3: 'non', code2: '', name: 'Norse, Old' },\n  { code3: 'nor', code2: 'no', name: 'Norwegian' },\n  { code3: 'nqo', code2: '', name: \"N'Ko\" },\n  { code3: 'nso', code2: '', name: 'Pedi; Sepedi; Northern Sotho' },\n  { code3: 'nub', code2: '', name: 'Nubian languages' },\n  {\n    code3: 'nwc',\n    code2: '',\n    name: 'Classical Newari; Old Newari; Classical Nepal Bhasa',\n  },\n  { code3: 'nya', code2: 'ny', name: 'Chichewa; Chewa; Nyanja' },\n  { code3: 'nym', code2: '', name: 'Nyamwezi' },\n  { code3: 'nyn', code2: '', name: 'Nyankole' },\n  { code3: 'nyo', code2: '', name: 'Nyoro' },\n  { code3: 'nzi', code2: '', name: 'Nzima' },\n  { code3: 'oci', code2: 'oc', name: 'Occitan (post 1500)' },\n  { code3: 'oji', code2: 'oj', name: 'Ojibwa' },\n  { code3: 'ori', code2: 'or', name: 'Oriya' },\n  { code3: 'orm', code2: 'om', name: 'Oromo' },\n  { code3: 'osa', code2: '', name: 'Osage' },\n  { code3: 'oss', code2: 'os', name: 'Ossetian; Ossetic' },\n  { code3: 'ota', code2: '', name: 'Turkish, Ottoman (1500-1928)' },\n  { code3: 'oto', code2: '', name: 'Otomian languages' },\n  { code3: 'paa', code2: '', name: 'Papuan languages' },\n  { code3: 'pag', code2: '', name: 'Pangasinan' },\n  { code3: 'pal', code2: ' ', name: 'Pahlavi' },\n  { code3: 'pam', code2: ' ', name: 'Pampanga; Kapampangan' },\n  { code3: 'pan', code2: 'paPanjabi; Punjabi', name: 'pendjabi' },\n  { code3: 'pap', code2: ' ', name: 'Papiamento' },\n  { code3: 'pau', code2: ' ', name: 'Palauan' },\n  { code3: 'peo', code2: ' ', name: 'Persian, Old (ca.600-400 B.C.)' },\n  { code3: 'per', code2: 'fa', name: 'Persian' },\n  { code3: 'phi', code2: ' ', name: 'Philippine languages' },\n  { code3: 'phn', code2: ' ', name: 'Phoenician' },\n  { code3: 'pli', code2: 'pi', name: 'Pali' },\n  { code3: 'pol', code2: 'pl', name: 'Polish' },\n  { code3: 'pon', code2: ' ', name: 'Pohnpeian' },\n  { code3: 'por', code2: 'pt', name: 'Portuguese' },\n  { code3: 'pra', code2: ' ', name: 'Prakrit languages' },\n  {\n    code3: 'pro',\n    code2: ' ',\n    name: 'Provençal, Old (to 1500);Occitan, Old (to 1500)',\n  },\n  { code3: 'pus', code2: 'ps', name: 'Pushto; Pashto' },\n  { code3: 'que', code2: 'qu', name: 'Quechua' },\n  { code3: 'raj', code2: ' ', name: 'Rajasthani' },\n  { code3: 'rap', code2: ' ', name: 'Rapanui' },\n  { code3: 'rar', code2: ' ', name: 'Rarotongan; Cook Islands Maori' },\n  { code3: 'roa', code2: ' ', name: 'Romance languages' },\n  { code3: 'roh', code2: 'rm', name: 'Romansh' },\n  { code3: 'rom', code2: ' ', name: 'Romany' },\n  { code3: 'rum', code2: 'ro', name: 'Romanian; Moldavian; Moldovan' },\n  { code3: 'ron', code2: 'ro', name: 'Romanian; Moldavian; Moldovan' },\n  { code3: 'run', code2: 'rn', name: 'Rundi' },\n  { code3: 'rup', code2: ' ', name: 'Aromanian; Arumanian; Macedo-Romanian' },\n  { code3: 'rus', code2: 'ru', name: 'Russian' },\n  { code3: 'sad', code2: ' ', name: 'Sandawe' },\n  { code3: 'sag', code2: 'sg', name: 'Sango' },\n  { code3: 'sah', code2: ' ', name: 'Yakut' },\n  { code3: 'sai', code2: ' ', name: 'South American Indian languages' },\n  { code3: 'sal', code2: ' ', name: 'Salishan languages' },\n  { code3: 'sam', code2: ' ', name: 'Samaritan Aramaic' },\n  { code3: 'san', code2: 'sa', name: 'Sanskrit' },\n  { code3: 'sas', code2: ' ', name: 'Sasak' },\n  { code3: 'sat', code2: ' ', name: 'Santali' },\n  { code3: 'scn', code2: ' ', name: 'Sicilian' },\n  { code3: 'sco', code2: ' ', name: 'Scots' },\n  { code3: 'sel', code2: ' ', name: 'Selkup' },\n  { code3: 'sem', code2: ' ', name: 'Semitic languages' },\n  { code3: 'sga', code2: ' ', name: 'Irish, Old (to 900)' },\n  { code3: 'sgn', code2: ' ', name: 'Sign Languages' },\n  { code3: 'shn', code2: ' ', name: 'Shan' },\n  { code3: 'sid', code2: ' ', name: 'Sidamo' },\n  { code3: 'sin', code2: 'si', name: 'Sinhala; Sinhalese' },\n  { code3: 'sio', code2: ' ', name: 'Siouan languages' },\n  { code3: 'sit', code2: ' ', name: 'Sino-Tibetan languages' },\n  { code3: 'sla', code2: ' ', name: 'Slavic languages' },\n  { code3: 'slo', code2: 'sk', name: 'Slovak' },\n  { code3: 'slk', code2: 'sk', name: 'Slovak' },\n  { code3: 'slv', code2: 'sl', name: 'Slovenian' },\n  { code3: 'sma', code2: ' ', name: 'Southern Sami' },\n  { code3: 'sme', code2: 'se', name: 'Northern Sami' },\n  { code3: 'smi', code2: ' ', name: 'Sami languages' },\n  { code3: 'smj', code2: ' ', name: 'Lule Sami' },\n  { code3: 'smn', code2: ' ', name: 'Inari Sami' },\n  { code3: 'smo', code2: 'sm', name: 'Samoan' },\n  { code3: 'sms', code2: ' ', name: 'Skolt Sami' },\n  { code3: 'sna', code2: 'sn', name: 'Shona' },\n  { code3: 'snd', code2: 'sd', name: 'Sindhi' },\n  { code3: 'snk', code2: ' ', name: 'Soninke' },\n  { code3: 'sog', code2: ' ', name: 'Sogdian' },\n  { code3: 'som', code2: 'so', name: 'Somali' },\n  { code3: 'son', code2: ' ', name: 'Songhai languages' },\n  { code3: 'sot', code2: 'st', name: 'Sotho, Southern' },\n  { code3: 'spa', code2: 'es', name: 'Spanish' },\n  { code3: 'sqi', code2: 'sq', name: 'Albanian' },\n  { code3: 'srd', code2: 'sc', name: 'Sardinian' },\n  { code3: 'srn', code2: ' ', name: 'Sranan Tongo' },\n  { code3: 'srp', code2: 'sr', name: 'Serbian' },\n  { code3: 'srr', code2: ' ', name: 'Serer' },\n  { code3: 'ssa', code2: ' ', name: 'Nilo-Saharan languages' },\n  { code3: 'ssw', code2: 'ss', name: 'Swati' },\n  { code3: 'suk', code2: ' ', name: 'Sukuma' },\n  { code3: 'sun', code2: 'su', name: 'Sundanese' },\n  { code3: 'sus', code2: ' ', name: 'Susu' },\n  { code3: 'sux', code2: ' ', name: 'Sumerian' },\n  { code3: 'swa', code2: 'sw', name: 'Swahili' },\n  { code3: 'swe', code2: 'sv', name: 'Swedish' },\n  { code3: 'syc', code2: ' ', name: 'Classical Syriac' },\n  { code3: 'syr', code2: ' ', name: 'Syriac' },\n  { code3: 'tah', code2: 'ty', name: 'Tahitian' },\n  { code3: 'tai', code2: ' ', name: 'Tai languages' },\n  { code3: 'tam', code2: 'ta', name: 'Tamil' },\n  { code3: 'tat', code2: 'tt', name: 'Tatar' },\n  { code3: 'tel', code2: 'te', name: 'Telugu' },\n  { code3: 'tem', code2: ' ', name: 'Timne' },\n  { code3: 'ter', code2: ' ', name: 'Tereno' },\n  { code3: 'tet', code2: ' ', name: 'Tetum' },\n  { code3: 'tgk', code2: 'tg', name: 'Tajik' },\n  { code3: 'tgl', code2: 'tl', name: 'Tagalog' },\n  { code3: 'tha', code2: 'th', name: 'Thai' },\n  { code3: 'tib', code2: 'bo', name: 'Tibetan' },\n  { code3: 'tig', code2: ' ', name: 'Tigre' },\n  { code3: 'tir', code2: 'ti', name: 'Tigrinya' },\n  { code3: 'tiv', code2: ' ', name: 'Tiv' },\n  { code3: 'tkl', code2: ' ', name: 'Tokelau' },\n  { code3: 'tlh', code2: ' ', name: 'Klingon; tlhIngan-Hol' },\n  { code3: 'tli', code2: ' ', name: 'Tlingit' },\n  { code3: 'tmh', code2: ' ', name: 'Tamashek' },\n  { code3: 'tog', code2: ' ', name: 'Tonga (Nyasa)' },\n  { code3: 'ton', code2: 'to', name: 'Tonga (Tonga Islands)' },\n  { code3: 'tpi', code2: ' ', name: 'Tok Pisin' },\n  { code3: 'tsi', code2: ' ', name: 'Tsimshian' },\n  { code3: 'tsn', code2: 'tn', name: 'Tswana' },\n  { code3: 'tso', code2: 'ts', name: 'Tsonga' },\n  { code3: 'tuk', code2: 'tk', name: 'Turkmen' },\n  { code3: 'tum', code2: ' ', name: 'Tumbuka' },\n  { code3: 'tup', code2: ' ', name: 'Tupi languages' },\n  { code3: 'tur', code2: 'tr', name: 'Turkish' },\n  { code3: 'tut', code2: ' ', name: 'Altaic languages' },\n  { code3: 'tvl', code2: ' ', name: 'Tuvalu' },\n  { code3: 'twi', code2: 'tw', name: 'Twi' },\n  { code3: 'tyv', code2: ' ', name: 'Tuvinian' },\n  { code3: 'udm', code2: ' ', name: 'Udmurt' },\n  { code3: 'uga', code2: ' ', name: 'Ugaritic' },\n  { code3: 'uig', code2: 'ug', name: 'Uighur; Uyghur' },\n  { code3: 'ukr', code2: 'uk', name: 'Ukrainian' },\n  { code3: 'umb', code2: ' ', name: 'Umbundu' },\n  { code3: 'und', code2: ' ', name: 'Undetermined' },\n  { code3: 'urd', code2: 'ur', name: 'Urdu' },\n  { code3: 'uzb', code2: 'uz', name: 'Uzbek' },\n  { code3: 'vai', code2: ' ', name: 'Vai' },\n  { code3: 'ven', code2: 've', name: 'Venda' },\n  { code3: 'vie', code2: 'vi', name: 'Vietnamese' },\n  { code3: 'vol', code2: 'vo', name: 'Volapük' },\n  { code3: 'vot', code2: ' ', name: 'Votic' },\n  { code3: 'wak', code2: ' ', name: 'Wakashan languages' },\n  { code3: 'wal', code2: ' ', name: 'Wolaitta; Wolaytta' },\n  { code3: 'war', code2: ' ', name: 'Waray' },\n  { code3: 'was', code2: ' ', name: 'Washo' },\n  { code3: 'wel', code2: 'cy', name: 'Welsh' },\n  { code3: 'wen', code2: ' ', name: 'Sorbian languages' },\n  { code3: 'wln', code2: 'wa', name: 'Walloon' },\n  { code3: 'wol', code2: 'wo', name: 'Wolof' },\n  { code3: 'xal', code2: ' ', name: 'Kalmyk; Oirat' },\n  { code3: 'xho', code2: 'xh', name: 'Xhosa' },\n  { code3: 'yao', code2: ' ', name: 'Yao' },\n  { code3: 'yap', code2: ' ', name: 'Yapese' },\n  { code3: 'yid', code2: 'yi', name: 'Yiddish' },\n  { code3: 'yor', code2: 'yo', name: 'Yoruba' },\n  { code3: 'ypk', code2: ' ', name: 'Yupik languages' },\n  { code3: 'zap', code2: ' ', name: 'Zapotec' },\n  { code3: 'zbl', code2: ' ', name: 'Blissymbols; Blissymbolics; Bliss' },\n  { code3: 'zen', code2: ' ', name: 'Zenaga' },\n  { code3: 'zgh', code2: ' ', name: 'Standard Moroccan Tamazight' },\n  { code3: 'zha', code2: 'za', name: 'Zhuang; Chuang' },\n  { code3: 'zho', code2: 'zh', name: 'Chinese' },\n  { code3: 'znd', code2: ' ', name: 'Zande languages' },\n  { code3: 'zul', code2: 'zu', name: 'Zulu' },\n  { code3: 'zun', code2: ' ', name: 'Zuni' },\n  {\n    code3: 'zza',\n    code2: '',\n    name: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki',\n  },\n]\n\nexport const LANGUAGES_MAP_CODE3 = Object.fromEntries(\n  LANGUAGES.map((lang) => [lang.code3, lang]),\n)\n\nexport function code3ToCode2(lang: string): string {\n  if (lang.length === 3) {\n    return LANGUAGES_MAP_CODE3[lang]?.code2 || lang\n  }\n  return lang\n}\n"
  },
  {
    "path": "packages/ozone/src/tag-service/language-tagger.ts",
    "content": "import {\n  AppBskyActorProfile,\n  AppBskyFeedGenerator,\n  AppBskyFeedPost,\n  AppBskyGraphList,\n} from '@atproto/api'\nimport { langLogger as log } from '../logger'\nimport { ContentTagger } from './content-tagger'\nimport { code3ToCode2 } from './language-data'\n\nconst ifString = (value: unknown): string | undefined =>\n  typeof value === 'string' ? value : undefined\nconst isStringProp = (obj: object, prop: string): string | undefined =>\n  prop in obj ? ifString(obj[prop]) : undefined\n\nexport class LanguageTagger extends ContentTagger {\n  tagPrefix = 'lang:'\n\n  isApplicable(): boolean {\n    return !!this.subjectStatus && !this.tagAlreadyExists()\n  }\n\n  async buildTags(): Promise<string[]> {\n    try {\n      const recordLangs = await this.getRecordLang()\n      return recordLangs\n        ? recordLangs.map((lang) => `${this.tagPrefix}${lang}`)\n        : [`${this.tagPrefix}und`]\n    } catch (err) {\n      log.error({ subject: this.subject, err }, 'Error getting record langs')\n      return []\n    }\n  }\n\n  getTextFromRecord(recordValue: Record<string, unknown>): string | undefined {\n    let text: string | undefined\n\n    if (AppBskyGraphList.isRecord(recordValue)) {\n      text =\n        isStringProp(recordValue, 'description') ||\n        isStringProp(recordValue, 'name')\n    } else if (\n      AppBskyFeedGenerator.isRecord(recordValue) ||\n      AppBskyActorProfile.isRecord(recordValue)\n    ) {\n      text =\n        isStringProp(recordValue, 'description') ||\n        isStringProp(recordValue, 'displayName')\n    } else if (AppBskyFeedPost.isRecord(recordValue)) {\n      text = isStringProp(recordValue, 'text')\n    }\n\n    return text?.trim()\n  }\n\n  async getRecordLang(): Promise<string[] | null> {\n    const langs = new Set<string>()\n\n    if (\n      this.subject.isRepo() ||\n      (this.subject.isRecord() &&\n        this.subject.uri.endsWith('/app.bsky.actor.profile/self'))\n    ) {\n      const feed = await this.moderationService.views.fetchAuthorFeed(\n        this.subject.did,\n      )\n      feed.forEach((item) => {\n        const itemLangs = item.post.record['langs'] as string[] | null\n        if (itemLangs?.length) {\n          // Pick the first fragment of the lang code so that instead of `en-US` and `en-GB` we get `en`\n          itemLangs.forEach((lang) => langs.add(lang.split('-')[0]))\n        }\n      })\n    }\n\n    if (this.subject.isRecord()) {\n      const recordByUri = await this.moderationService.views.fetchRecords([\n        this.subject,\n      ])\n      const record = recordByUri.get(this.subject.uri)\n      const recordLang = record?.value.langs as string[] | null\n      const recordText = record\n        ? this.getTextFromRecord(record.value)\n        : undefined\n      if (recordLang?.length) {\n        recordLang\n          .map((lang) => lang.split('-')[0])\n          .forEach((lang) => langs.add(lang))\n      } else if (recordText) {\n        // 'lande' is an esm module, so we need to import it dynamically\n        const { default: lande } = await import('lande')\n        const detectedLanguages = lande(recordText)\n        if (detectedLanguages.length) {\n          const langCode = code3ToCode2(detectedLanguages[0][0])\n          if (langCode) langs.add(langCode)\n        }\n      }\n    }\n\n    return langs.size > 0 ? Array.from(langs) : null\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/tag-service/util.ts",
    "content": "import { ReasonType } from '../lexicon/types/com/atproto/moderation/defs'\n\nexport const getTagForReport = (reasonType: ReasonType) => {\n  const reasonWithoutPrefix = reasonType\n    .replace('com.atproto.moderation.defs#reason', '')\n    .replace('tools.ozone.report.defs#reason', '')\n\n  const kebabCase = reasonWithoutPrefix\n    .replace(/([a-z])([A-Z])/g, '$1-$2')\n    .toLowerCase()\n\n  return `report:${kebabCase}`\n}\n"
  },
  {
    "path": "packages/ozone/src/team/index.ts",
    "content": "import { Selectable } from 'kysely'\nimport AtpAgent from '@atproto/api'\nimport { chunkArray } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { Database } from '../db'\nimport { Member } from '../db/schema/member'\nimport { ids } from '../lexicon/lexicons'\nimport { ProfileViewDetailed } from '../lexicon/types/app/bsky/actor/defs'\nimport { Member as TeamMember } from '../lexicon/types/tools/ozone/team/defs'\nimport { httpLogger } from '../logger'\nimport { AuthHeaders } from '../mod-service/views'\n\nexport type TeamServiceCreator = (db: Database) => TeamService\n\nexport class TeamService {\n  constructor(\n    public db: Database,\n    private appviewAgent: AtpAgent,\n    private appviewDid: string,\n    private createAuthHeaders: (\n      aud: string,\n      method: string,\n    ) => Promise<AuthHeaders>,\n  ) {}\n\n  static creator(\n    appviewAgent: AtpAgent,\n    appviewDid: string,\n    createAuthHeaders: (aud: string, method: string) => Promise<AuthHeaders>,\n  ) {\n    return (db: Database) =>\n      new TeamService(db, appviewAgent, appviewDid, createAuthHeaders)\n  }\n\n  async list({\n    cursor,\n    limit = 25,\n    roles,\n    disabled,\n    q,\n  }: {\n    q?: string\n    cursor?: string\n    limit?: number\n    disabled?: boolean\n    roles?: string[]\n  }): Promise<{ members: Selectable<Member>[]; cursor?: string }> {\n    let builder = this.db.db.selectFrom('member').selectAll()\n    if (cursor) {\n      builder = builder.where('createdAt', '>', new Date(cursor))\n    }\n    if (roles !== undefined) {\n      const knownRoles = roles.filter(\n        (r) =>\n          r === 'tools.ozone.team.defs#roleAdmin' ||\n          r === 'tools.ozone.team.defs#roleModerator' ||\n          r === 'tools.ozone.team.defs#roleVerifier' ||\n          r === 'tools.ozone.team.defs#roleTriage',\n      )\n\n      // Optimization: no need to query to know that no values will be returned\n      if (!knownRoles.length) return { members: [] }\n\n      builder = builder.where('role', 'in', knownRoles)\n    }\n    if (disabled !== undefined) {\n      builder = builder.where('disabled', disabled ? 'is' : 'is not', true)\n    }\n    if (q) {\n      builder = builder.where((qb) =>\n        qb\n          .orWhere('handle', 'ilike', `%${q}%`)\n          .orWhere('displayName', 'ilike', `%${q}%`),\n      )\n    }\n\n    const members = await builder\n      .limit(limit)\n      .orderBy('createdAt', 'asc')\n      .orderBy('handle', 'asc')\n      .execute()\n\n    return { members, cursor: members.at(-1)?.createdAt.toISOString() }\n  }\n\n  async create({\n    role,\n    did,\n    disabled,\n    updatedAt,\n    createdAt,\n    lastUpdatedBy,\n  }: Omit<Selectable<Member>, 'createdAt' | 'updatedAt'> & {\n    createdAt?: Date\n    updatedAt?: Date\n  }): Promise<Selectable<Member>> {\n    const now = new Date()\n    const newMember = await this.db.db\n      .insertInto('member')\n      .values({\n        role,\n        did,\n        disabled,\n        lastUpdatedBy,\n        updatedAt: updatedAt || now,\n        createdAt: createdAt || now,\n      })\n      .returningAll()\n      .executeTakeFirstOrThrow()\n\n    return newMember\n  }\n\n  async upsert({\n    role,\n    did,\n    lastUpdatedBy,\n  }: Pick<\n    Selectable<Member>,\n    'role' | 'did' | 'lastUpdatedBy'\n  >): Promise<void> {\n    const now = new Date()\n    await this.db.db\n      .insertInto('member')\n      .values({\n        role,\n        did,\n        lastUpdatedBy,\n        disabled: false,\n        updatedAt: now,\n        createdAt: now,\n      })\n      .onConflict((oc) =>\n        oc.column('did').doUpdateSet({ role, updatedAt: now, lastUpdatedBy }),\n      )\n      .execute()\n  }\n\n  async update(\n    did: string,\n    updates: Partial<\n      Pick<\n        Selectable<Member>,\n        'role' | 'disabled' | 'lastUpdatedBy' | 'updatedAt'\n      >\n    >,\n  ): Promise<Selectable<Member>> {\n    const { role, disabled, lastUpdatedBy, updatedAt = new Date() } = updates\n    const updatedMember = await this.db.db\n      .updateTable('member')\n      .where('did', '=', did)\n      .set({\n        role,\n        disabled,\n        lastUpdatedBy,\n        updatedAt,\n      })\n      .returningAll()\n      .executeTakeFirstOrThrow()\n\n    return updatedMember\n  }\n\n  async delete(did: string): Promise<void> {\n    await this.db.db.deleteFrom('member').where('did', '=', did).execute()\n  }\n\n  async assertCanDelete(did: string): Promise<void> {\n    const memberExists = await this.doesMemberExist(did)\n\n    if (!memberExists) {\n      throw new InvalidRequestError('member not found', 'MemberNotFound')\n    }\n  }\n\n  async doesMemberExist(did: string): Promise<boolean> {\n    const member = await this.db.db\n      .selectFrom('member')\n      .select('did')\n      .where('did', '=', did)\n      .executeTakeFirst()\n\n    return !!member\n  }\n\n  async getMember(did: string): Promise<Selectable<Member> | undefined> {\n    const member = await this.db.db\n      .selectFrom('member')\n      .selectAll()\n      .where('did', '=', did)\n      .executeTakeFirst()\n\n    return member\n  }\n\n  getMemberRole(member?: Selectable<Member>) {\n    const isAdmin = member?.role === 'tools.ozone.team.defs#roleAdmin'\n    const isModerator =\n      isAdmin || member?.role === 'tools.ozone.team.defs#roleModerator'\n    const isTriage =\n      isModerator || member?.role === 'tools.ozone.team.defs#roleTriage'\n    const isVerifier =\n      isAdmin || member?.role === 'tools.ozone.team.defs#roleVerifier'\n\n    return {\n      isModerator,\n      isAdmin,\n      isTriage,\n      isVerifier,\n    }\n  }\n\n  // getProfiles() only allows 25 DIDs at a time so we need to query in chunks\n  async getProfiles(dids: string[]): Promise<Map<string, ProfileViewDetailed>> {\n    const profiles = new Map<string, ProfileViewDetailed>()\n\n    try {\n      const headers = await this.createAuthHeaders(\n        this.appviewDid,\n        ids.AppBskyActorGetProfiles,\n      )\n\n      for (const actors of chunkArray(dids, 25)) {\n        const { data } = await this.appviewAgent.getProfiles(\n          { actors },\n          headers,\n        )\n\n        data.profiles.forEach((profile) => {\n          profiles.set(profile.did, profile)\n        })\n      }\n    } catch (err) {\n      httpLogger.error({ err, dids }, 'Failed to get profiles for team members')\n    }\n\n    return profiles\n  }\n\n  async syncMemberProfiles(): Promise<void> {\n    let lastDid = ''\n    // Max 25 profiles can be fetched at a time so let's pull 25 members at a time from the db and update their profile details\n    do {\n      const members = await this.db.db\n        .selectFrom('member')\n        .select(['did'])\n        .limit(25)\n        .if(!!lastDid, (q) => q.where('did', '>', lastDid))\n        .orderBy('did', 'asc')\n        .execute()\n\n      const dids = members.map((member) => member.did)\n      const profiles = await this.getProfiles(dids)\n\n      for (const profile of profiles.values()) {\n        await this.db.db\n          .updateTable('member')\n          .where('did', '=', profile.did)\n          .set({\n            handle: profile.handle,\n            displayName: profile.displayName || null,\n          })\n          .execute()\n      }\n\n      lastDid = dids.at(-1) || ''\n    } while (lastDid)\n  }\n\n  async view(members: Selectable<Member>[]): Promise<TeamMember[]> {\n    const profiles = await this.getProfiles(members.map(({ did }) => did))\n    return members.map((member) => {\n      return {\n        did: member.did,\n        role: member.role,\n        disabled: member.disabled,\n        profile: profiles.get(member.did),\n        createdAt: member.createdAt.toISOString(),\n        updatedAt: member.updatedAt.toISOString(),\n        lastUpdatedBy: member.lastUpdatedBy,\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/util.ts",
    "content": "import assert from 'node:assert'\nimport { parseList } from 'structured-headers'\nimport { createRetryable } from '@atproto/common'\nimport { ResponseType, XRPCError } from '@atproto/xrpc'\nimport { Database } from './db'\n\nexport const getSigningKeyId = async (\n  db: Database,\n  signingKey: string,\n): Promise<number> => {\n  const selectRes = await db.db\n    .selectFrom('signing_key')\n    .selectAll()\n    .where('key', '=', signingKey)\n    .executeTakeFirst()\n  if (selectRes) {\n    return selectRes.id\n  }\n  const insertRes = await db.db\n    .insertInto('signing_key')\n    .values({ key: signingKey })\n    .returningAll()\n    .executeTakeFirstOrThrow()\n  return insertRes.id\n}\n\nexport const RETRYABLE_HTTP_STATUS_CODES = new Set([\n  408, 425, 429, 500, 502, 503, 504, 522, 524,\n])\n\nexport const retryHttp = createRetryable((err: unknown) => {\n  if (err instanceof XRPCError) {\n    if (err.status === ResponseType.Unknown) return true\n    return RETRYABLE_HTTP_STATUS_CODES.has(err.status)\n  }\n  return false\n})\n\nexport type ParsedLabelers = {\n  dids: string[]\n  redact: Set<string>\n}\n\nexport const LABELER_HEADER_NAME = 'atproto-accept-labelers'\n\nexport const parseLabelerHeader = (\n  header: string | undefined,\n  ignoreDid?: string,\n): ParsedLabelers | null => {\n  if (!header) return null\n  const labelerDids = new Set<string>()\n  const redactDids = new Set<string>()\n  const parsed = parseList(header)\n  for (const item of parsed) {\n    const did = item[0].toString()\n    if (!did) {\n      return null\n    }\n    if (did === ignoreDid) {\n      continue\n    }\n    labelerDids.add(did)\n    const redact = item[1].get('redact')?.valueOf()\n    if (redact === true) {\n      redactDids.add(did)\n    }\n  }\n  return {\n    dids: [...labelerDids],\n    redact: redactDids,\n  }\n}\n\nexport const defaultLabelerHeader = (dids: string[]): ParsedLabelers => {\n  return {\n    dids,\n    redact: new Set(dids),\n  }\n}\n\nexport const formatLabelerHeader = (parsed: ParsedLabelers): string => {\n  const parts = parsed.dids.map((did) =>\n    parsed.redact.has(did) ? `${did};redact` : did,\n  )\n  return parts.join(',')\n}\n\n/**\n * Utility function similar to `setInterval()`. The main difference is that the\n * execution is controlled through a signal and that the function will wait for\n * `interval` milliseconds *between* the end of the previous execution and the\n * start of the next one (instead of starting the execution every `interval`\n * milliseconds), ensuring that the function is not running concurrently.\n *\n * @param fn The function to execute. That function must not throw any error\n * other than {@link signal}'s {@link AbortSignal.reason} or an {@link Error}\n * that has the {@link signal}'s {@link AbortSignal.reason} as its\n * {@link Error.cause}.\n *\n * @returns A promise that resolves when the signal is aborted, and the last\n * execution is done.\n *\n * @throws {AbortSignal['reason']} if the {@link signal} is already aborted.\n * @throws {TypeError} if {@link fn} throws an unexpected error (with the\n * unexpected error as the {@link Error.cause}).\n */\nexport async function startInterval(\n  fn: (signal: AbortSignal) => void | Promise<void>,\n  interval: number,\n  signal: AbortSignal,\n  runImmediately = false,\n) {\n  signal.throwIfAborted()\n\n  // Renaming for clarity\n  const inputSignal = signal\n\n  const intervalController = new AbortController()\n  const intervalSignal = intervalController.signal\n\n  return new Promise<void>((resolve, reject) => {\n    let timer: NodeJS.Timeout | undefined\n\n    const run = async () => {\n      // Cloning the signal for this particular run to prevent memory leaks\n      const runController = boundAbortController(intervalSignal)\n      const runSignal = runController.signal\n\n      try {\n        await fn(runSignal)\n      } catch (err) {\n        if (err != null && isCausedBySignal(err, runSignal)) {\n          // Silently ignore the error if it is caused by the signal. At this\n          // point, the interval controller was aborted, which will cause the\n          // promise to resolve in the \"finally\" block bellow.\n        } else {\n          // Invalid behavior: stop the interval and reject the promise.\n          const error = new TypeError('Unexpected error', { cause: err })\n\n          // Rejecting here will make `resolve()` in the \"finally\" block to be a\n          // no-op. Rejecting before aborting the controller to ensure the\n          // promise does not get resolved by the `abort` event listeners.\n          reject(error)\n\n          // Using `error` as abort reason to avoid creating an AbortError.\n          intervalController.abort(error)\n        }\n      } finally {\n        // Cleanup the listeners added by `boundAbortController`\n        runController.abort()\n\n        if (intervalSignal.aborted) resolve()\n        else schedule()\n      }\n    }\n\n    const schedule = () => {\n      assert(timer === undefined, 'unexpected state')\n      timer = setTimeout(() => {\n        timer = undefined // \"running\" state\n        void run()\n      }, interval)\n    }\n\n    inputSignal.addEventListener(\n      'abort',\n      // This function will only be called if the `inputSignal` is aborted\n      // before the interval controller is aborted.\n      () => {\n        // Stop the interval, using the input signal's reason\n        intervalController.abort(inputSignal.reason)\n\n        if (timer === undefined) {\n          // `fn` is currently running; `run`'s finally block will resolve the\n          // promise.\n        } else {\n          // The execution was scheduled but not started yet. Clear the timer\n          // and resolve the promise.\n          clearTimeout(timer)\n          resolve()\n        }\n      },\n      // Remove the listener whenever the interval is aborted.\n      { signal: intervalSignal },\n    )\n\n    if (runImmediately) void run()\n    else schedule()\n  })\n}\n\n/**\n * Determines whether the cause of an error is a signal's reason\n */\nexport function isCausedBySignal(err: unknown, signal: AbortSignal) {\n  if (!signal.aborted) return false\n  if (signal.reason == null) return false // Ignore nullish reasons\n  return (\n    err === signal.reason ||\n    (err instanceof Error && err.cause === signal.reason)\n  )\n}\n\n/**\n * Creates an AbortController that will be aborted when any of the given signals\n * is aborted.\n *\n * @note Make sure to call `abortController.abort()` when you are done with\n * the controller to avoid memory leaks.\n *\n * @throws if any of the input signals is already aborted.\n */\nexport function boundAbortController(\n  ...signals: readonly (AbortSignal | undefined | null)[]\n): AbortController {\n  for (const signal of signals) {\n    signal?.throwIfAborted()\n  }\n\n  const abortController = new AbortController()\n  const abort = function (event: Event) {\n    abortController.abort((event.target as AbortSignal)?.reason)\n  }\n\n  for (const signal of signals) {\n    signal?.addEventListener('abort', abort, { signal: abortController.signal })\n  }\n\n  return abortController\n}\n"
  },
  {
    "path": "packages/ozone/src/verification/issuer.ts",
    "content": "import { Selectable } from 'kysely'\nimport { Agent, AtUri, CredentialSession } from '@atproto/api'\nimport { VerifierConfig } from '../config'\nimport { Verification } from '../db/schema/verification'\n\nexport type VerificationInput = {\n  displayName: string\n  handle: string\n  subject: string\n  createdAt?: string\n}\n\nexport type VerificationIssuerCreator = (\n  verifierConfig: VerifierConfig,\n) => VerificationIssuer\n\nconst HANDLE_INVALID = 'handle.invalid'\n\nexport class VerificationIssuer {\n  private session = new CredentialSession(new URL(this.verifierConfig.url))\n  private agent = new Agent(this.session)\n  constructor(private verifierConfig: VerifierConfig) {}\n\n  static creator() {\n    return (verifierConfig: VerifierConfig) =>\n      new VerificationIssuer(verifierConfig)\n  }\n\n  async getAgent() {\n    if (!this.session.hasSession) {\n      await this.session.login({\n        identifier: this.verifierConfig.did,\n        password: this.verifierConfig.password,\n      })\n    }\n\n    // Trigger a test request to check if the session is still valid, if not, we will login again\n    try {\n      await this.agent.com.atproto.server.getSession()\n    } catch (err) {\n      if ((err as any).status === 401) {\n        await this.session.login({\n          identifier: this.verifierConfig.did,\n          password: this.verifierConfig.password,\n        })\n      }\n    }\n\n    return this.agent\n  }\n\n  async verify(verifications: VerificationInput[]) {\n    const grantedVerifications: Selectable<Verification>[] = []\n    const failedVerifications: {\n      $type: 'tools.ozone.verification.grantVerifications#grantError'\n      subject: string\n      error: string\n    }[] = []\n    const now = new Date().toISOString()\n    const agent = await this.getAgent()\n    await Promise.allSettled(\n      verifications.map(async ({ displayName, handle, subject, createdAt }) => {\n        if (handle.toLowerCase() === HANDLE_INVALID) {\n          failedVerifications.push({\n            $type: 'tools.ozone.verification.grantVerifications#grantError',\n            error: 'Cannot verify with invalid handle',\n            subject,\n          })\n          return\n        }\n\n        try {\n          const verificationRecord = {\n            createdAt: createdAt || now,\n            issuer: this.verifierConfig.did,\n            displayName,\n            handle,\n            subject,\n          }\n          const {\n            data: { uri, cid },\n          } = await agent.com.atproto.repo.createRecord({\n            repo: this.verifierConfig.did,\n            record: verificationRecord,\n            collection: 'app.bsky.graph.verification',\n          })\n          grantedVerifications.push({\n            ...verificationRecord,\n            uri,\n            cid,\n            revokedAt: null,\n            updatedAt: now,\n            revokedBy: null,\n            revokeReason: null,\n          })\n        } catch (err) {\n          failedVerifications.push({\n            $type: 'tools.ozone.verification.grantVerifications#grantError',\n            error: (err as Error).message,\n            subject,\n          })\n          return\n        }\n      }),\n    )\n\n    return { grantedVerifications, failedVerifications }\n  }\n\n  async revoke({ uris }: { uris: string[] }) {\n    const revokedVerifications: string[] = []\n    const failedRevocations: Array<{ uri: string; error: string }> = []\n\n    const agent = await this.getAgent()\n\n    await Promise.allSettled(\n      uris.map(async (uri) => {\n        try {\n          const atUri = new AtUri(uri)\n\n          if (atUri.collection !== 'app.bsky.graph.verification') {\n            throw new Error(`Only verification records can be revoked`)\n          }\n\n          if (atUri.host !== this.verifierConfig.did) {\n            throw new Error(\n              `Cannot revoke verification record ${uri} not issued by ${this.verifierConfig.did}`,\n            )\n          }\n\n          await agent.com.atproto.repo.deleteRecord({\n            collection: atUri.collection,\n            repo: this.verifierConfig.did,\n            rkey: atUri.rkey,\n          })\n          revokedVerifications.push(uri)\n        } catch (err) {\n          failedRevocations.push({ uri, error: (err as Error).message })\n          return\n        }\n      }),\n    )\n\n    return { revokedVerifications, failedRevocations }\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/verification/service.ts",
    "content": "import { Selectable } from 'kysely'\nimport {\n  $Typed,\n  AppBskyActorDefs,\n  AtUri,\n  ToolsOzoneModerationDefs,\n  ToolsOzoneVerificationDefs,\n} from '@atproto/api'\nimport { Database } from '../db'\nimport { CreatedAtUriKeyset, paginate } from '../db/pagination'\nimport { Verification } from '../db/schema/verification'\n\nexport type VerificationServiceCreator = (db: Database) => VerificationService\n\nexport class VerificationService {\n  constructor(public db: Database) {}\n\n  static creator() {\n    return (db: Database) => new VerificationService(db)\n  }\n\n  async create(\n    verifications: Pick<\n      Verification,\n      | 'uri'\n      | 'issuer'\n      | 'subject'\n      | 'handle'\n      | 'displayName'\n      | 'createdAt'\n      | 'cid'\n    >[],\n  ) {\n    return this.db.transaction(async (tx) => {\n      return tx.db\n        .insertInto('verification')\n        .values(verifications)\n        .onConflict((oc) => oc.doNothing())\n        .returningAll()\n        .execute()\n    })\n  }\n\n  async markRevoked({\n    uris,\n    revokedBy,\n    revokedAt,\n    revokeReason,\n  }: {\n    uris: string[]\n    revokedBy?: string\n    revokedAt?: string\n    revokeReason?: string\n  }) {\n    const now = new Date().toISOString()\n    return this.db.transaction(async (tx) => {\n      for (const uri of uris) {\n        return tx.db\n          .updateTable('verification')\n          .set({\n            revokeReason,\n            updatedAt: now,\n            revokedAt: revokedAt || now,\n            // Allow setting revokedBy to a moderator/verifier DID and if it isn't set, default to the author of the verification record\n            revokedBy: revokedBy || new AtUri(uri).host,\n          })\n          .where('uri', '=', uri)\n          .where('revokedAt', 'is', null)\n          .execute()\n      }\n    })\n  }\n\n  async list({\n    sortDirection,\n    cursor,\n    createdAfter,\n    createdBefore,\n    issuers = [],\n    subjects = [],\n    isRevoked,\n    limit = 100,\n  }: {\n    sortDirection?: 'asc' | 'desc'\n    cursor?: string\n    createdAfter?: string\n    createdBefore?: string\n    issuers?: string[]\n    subjects?: string[]\n    isRevoked?: boolean\n    limit?: number\n  }) {\n    const { ref } = this.db.db.dynamic\n\n    let qb = this.db.db.selectFrom('verification').selectAll()\n\n    if (issuers.length) {\n      qb = qb.where('issuer', 'in', issuers)\n    }\n\n    if (isRevoked !== undefined) {\n      qb = qb.where('revokedAt', isRevoked ? 'is not' : 'is', null)\n    }\n\n    if (subjects.length) {\n      qb = qb.where('subject', 'in', subjects)\n    }\n\n    if (createdAfter) {\n      qb = qb.where('createdAt', '>=', createdAfter)\n    }\n\n    if (createdBefore) {\n      qb = qb.where('createdAt', '<=', createdBefore)\n    }\n\n    const keyset = new CreatedAtUriKeyset(ref(`createdAt`), ref('uri'))\n    const paginatedBuilder = paginate(qb, {\n      limit,\n      cursor,\n      keyset,\n      tryIndex: true,\n      direction: sortDirection === 'desc' ? 'desc' : 'asc',\n    })\n\n    const result = await paginatedBuilder.execute()\n    return { verifications: result, cursor: keyset.packFromResult(result) }\n  }\n\n  view(\n    verifications: Selectable<Verification>[],\n    repos: Map<\n      string,\n      | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n      | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    >,\n    profiles: Map<string, AppBskyActorDefs.ProfileViewDetailed>,\n  ): $Typed<ToolsOzoneVerificationDefs.VerificationView>[] {\n    return verifications.map((verification) => {\n      const issuerRepo = repos.get(verification.issuer)\n      const subjectRepo = repos.get(verification.subject)\n      const subjectProfile = profiles.get(verification.subject)\n      const issuerProfile = profiles.get(verification.issuer)\n      return {\n        $type: 'tools.ozone.verification.defs#verificationView',\n        uri: verification.uri,\n        issuer: verification.issuer,\n        subject: verification.subject,\n        createdAt: verification.createdAt,\n        displayName: verification.displayName,\n        handle: verification.handle,\n        updatedAt: verification.updatedAt || undefined,\n        revokedAt: verification.revokedAt || undefined,\n        revokedBy: verification.revokedBy || undefined,\n        revokeReason: verification.revokeReason || undefined,\n        issuerRepo,\n        subjectRepo,\n        subjectProfile: subjectProfile\n          ? {\n              $type: 'app.bsky.actor.defs#profileViewDetailed',\n              ...subjectProfile,\n            }\n          : undefined,\n        issuerProfile: issuerProfile\n          ? {\n              $type: 'app.bsky.actor.defs#profileViewDetailed',\n              ...issuerProfile,\n            }\n          : undefined,\n      }\n    })\n  }\n\n  async getFirehoseCursor() {\n    const entry = await this.db.db\n      .selectFrom('firehose_cursor')\n      .select('cursor')\n      .where('service', '=', 'verification')\n      .executeTakeFirst()\n\n    return entry?.cursor || null\n  }\n\n  createFirehoseCursor() {\n    return this.db.db\n      .insertInto('firehose_cursor')\n      .values({\n        service: 'verification',\n        cursor: null,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  }\n\n  async updateFirehoseCursor(cursor: number) {\n    const updated = await this.db.db\n      .updateTable('firehose_cursor')\n      .set({ cursor })\n      .where('service', '=', 'verification')\n      .where((qb) =>\n        qb.where('cursor', '<', cursor).orWhere('cursor', 'is', null),\n      )\n      .returningAll()\n      .executeTakeFirst()\n\n    return updated?.cursor\n  }\n}\n"
  },
  {
    "path": "packages/ozone/src/verification/util.ts",
    "content": "import { $Typed, ToolsOzoneModerationDefs } from '@atproto/api'\nimport { addAccountInfoToRepoViewDetail, getPdsAccountInfos } from '../api/util'\nimport { AppContext } from '../context'\nimport { ModerationService } from '../mod-service'\nimport { ParsedLabelers } from '../util'\n\nexport const getReposForVerifications = async (\n  ctx: AppContext,\n  labelers: ParsedLabelers,\n  modService: ModerationService,\n  dids: string[],\n  isModerator: boolean,\n) => {\n  const [partialRepos, accountInfo] = await Promise.all([\n    modService.views.repoDetails(dids, labelers),\n    getPdsAccountInfos(ctx, dids),\n  ])\n\n  const repos = new Map<\n    string,\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n  >(\n    dids.map((did) => {\n      const partialRepo = partialRepos.get(did)\n      if (!partialRepo) {\n        return [\n          did,\n          {\n            did,\n            $type: 'tools.ozone.moderation.defs#repoViewNotFound',\n          },\n        ]\n      }\n      return [\n        did,\n        {\n          ...addAccountInfoToRepoViewDetail(\n            partialRepo,\n            accountInfo.get(did) || null,\n            isModerator,\n          ),\n          $type: 'tools.ozone.moderation.defs#repoViewDetail',\n        },\n      ]\n    }),\n  )\n\n  return repos\n}\n"
  },
  {
    "path": "packages/ozone/test.env",
    "content": "LOG_ENABLED=true\nLOG_DESTINATION=test.log\n"
  },
  {
    "path": "packages/ozone/tests/3p-labeler.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n  createOzoneDid,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { LABELER_HEADER_NAME } from '../src/util'\n\ndescribe('labels from 3p labelers', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let thirdPartyLabeler: TestOzone\n  let agent: AtpAgent\n  let thirdPartyAgent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  let thirdPartyModClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_third_party_labeler_main',\n    })\n    ozone = network.ozone\n\n    const ozoneKey = await Secp256k1Keypair.create({ exportable: true })\n    const ozoneDid = await createOzoneDid(network.plc.url, ozoneKey)\n    thirdPartyLabeler = await TestOzone.create({\n      port: ozone.port + 10,\n      plcUrl: network.plc.url,\n      signingKey: ozoneKey,\n      serverDid: ozoneDid,\n      dbPostgresSchema: `ozone_admin_third_party_labeler_third_party`,\n      dbPostgresUrl: ozone.ctx.cfg.db.postgresUrl,\n      appviewUrl: network.bsky.url,\n      appviewDid: network.bsky.ctx.cfg.serverDid,\n      appviewPushEvents: true,\n      pdsUrl: network.pds.url,\n      pdsDid: network.pds.ctx.cfg.service.did,\n    })\n\n    thirdPartyAgent = thirdPartyLabeler.getClient()\n    agent = ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    thirdPartyModClient = thirdPartyLabeler.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n    await thirdPartyLabeler.close()\n  })\n\n  const getPostSubject = () => ({\n    $type: 'com.atproto.repo.strongRef',\n    uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n    cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n  })\n\n  const adjustLabels = async ({\n    uri,\n    cid,\n    src,\n    createLabelVals = [],\n    negateLabelVals = [],\n  }: {\n    uri: string\n    src: string\n    cid?: string\n    createLabelVals?: string[]\n    negateLabelVals?: string[]\n  }) => {\n    const labelEntries = createLabelVals.map((val) => ({\n      uri,\n      cid: cid || '',\n      val,\n      cts: new Date().toISOString(),\n      neg: false,\n      src,\n    }))\n\n    negateLabelVals.forEach((val) => {\n      labelEntries.push({\n        uri,\n        cid: cid || '',\n        val,\n        cts: new Date().toISOString(),\n        neg: true,\n        src,\n      })\n    })\n    await network.bsky.db.db.insertInto('label').values(labelEntries).execute()\n  }\n\n  const labelAccount = async (\n    client: ModeratorClient,\n    {\n      createLabelVals = [],\n      negateLabelVals = [],\n    }: { createLabelVals?: string[]; negateLabelVals?: string[] },\n  ) => {\n    const subject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.alice,\n    }\n    await client.emitEvent(\n      {\n        subject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventLabel',\n          createLabelVals,\n          negateLabelVals,\n        },\n        createdBy: sc.dids.carol,\n      },\n      'moderator',\n    )\n    await adjustLabels({\n      createLabelVals,\n      negateLabelVals,\n      uri: sc.dids.alice,\n      src: client.ozone.ctx.cfg.service.did,\n    })\n  }\n\n  const labelPost = async (\n    client: ModeratorClient,\n    {\n      createLabelVals = [],\n      negateLabelVals = [],\n    }: { createLabelVals?: string[]; negateLabelVals?: string[] },\n  ) => {\n    const subject = getPostSubject()\n    await client.emitEvent(\n      {\n        subject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventLabel',\n          createLabelVals,\n          negateLabelVals,\n        },\n        createdBy: sc.dids.carol,\n      },\n      'moderator',\n    )\n    await adjustLabels({\n      createLabelVals,\n      negateLabelVals,\n      uri: subject.uri,\n      cid: subject.cid,\n      src: client.ozone.ctx.cfg.service.did,\n    })\n  }\n\n  describe('record labels', () => {\n    it('includes only labels from current authority by default', async () => {\n      await Promise.all([\n        labelPost(modClient, { createLabelVals: ['spam'] }),\n        labelPost(thirdPartyModClient, { createLabelVals: ['weird'] }),\n      ])\n\n      const [\n        { data: recordFromCurrentAuthority },\n        { data: recordFromThirdParty },\n      ] = await Promise.all([\n        agent.api.tools.ozone.moderation.getRecord(\n          { uri: sc.posts[sc.dids.alice][0].ref.uriStr },\n          {\n            headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecord),\n          },\n        ),\n        thirdPartyAgent.api.tools.ozone.moderation.getRecord(\n          { uri: sc.posts[sc.dids.alice][0].ref.uriStr },\n          {\n            headers: await thirdPartyLabeler.modHeaders(\n              ids.ToolsOzoneModerationGetRecord,\n            ),\n          },\n        ),\n      ])\n\n      const currentAuthorityLabels = recordFromCurrentAuthority.labels?.map(\n        (l) => l.val,\n      )\n      const thirdPartyLabels = recordFromThirdParty.labels?.map((l) => l.val)\n      expect(currentAuthorityLabels).toContain('spam')\n      expect(currentAuthorityLabels).not.toContain('weird')\n      expect(thirdPartyLabels).toContain('weird')\n      expect(thirdPartyLabels).not.toContain('spam')\n    })\n\n    it('includes labels from all authorities requested via header', async () => {\n      const authHeaders = await ozone.modHeaders(\n        ids.ToolsOzoneModerationGetRecord,\n      )\n      const { data: recordIncludingExternalLabels } =\n        await agent.api.tools.ozone.moderation.getRecord(\n          { uri: sc.posts[sc.dids.alice][0].ref.uriStr },\n          {\n            headers: {\n              ...authHeaders,\n              [LABELER_HEADER_NAME]: [\n                thirdPartyLabeler.ctx.cfg.service.did,\n                ozone.ctx.cfg.service.did,\n              ].join(','),\n            },\n          },\n        )\n      const labelVals = recordIncludingExternalLabels.labels?.map((l) => l.val)\n      const labelSources = recordIncludingExternalLabels.labels?.map(\n        (l) => l.src,\n      )\n      expect(labelVals).toContain('weird')\n      expect(labelVals).toContain('spam')\n      expect(labelSources).toContain(thirdPartyLabeler.ctx.cfg.service.did)\n      expect(labelSources).toContain(ozone.ctx.cfg.service.did)\n    })\n  })\n\n  describe('repo labels', () => {\n    it('includes only labels from current authority by default', async () => {\n      await Promise.all([\n        labelAccount(modClient, { createLabelVals: ['spam'] }),\n        labelAccount(thirdPartyModClient, { createLabelVals: ['weird'] }),\n      ])\n\n      const [{ data: repoFromCurrentAuthority }, { data: repoFromThirdParty }] =\n        await Promise.all([\n          agent.api.tools.ozone.moderation.getRepo(\n            { did: sc.dids.alice },\n            {\n              headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo),\n            },\n          ),\n          thirdPartyAgent.api.tools.ozone.moderation.getRepo(\n            { did: sc.dids.alice },\n            {\n              headers: await thirdPartyLabeler.modHeaders(\n                ids.ToolsOzoneModerationGetRepo,\n              ),\n            },\n          ),\n        ])\n\n      const currentAuthorityLabels = repoFromCurrentAuthority.labels?.map(\n        (l) => l.val,\n      )\n      const thirdPartyLabels = repoFromThirdParty.labels?.map((l) => l.val)\n      expect(currentAuthorityLabels).toContain('spam')\n      expect(currentAuthorityLabels).not.toContain('weird')\n      expect(thirdPartyLabels).toContain('weird')\n      expect(thirdPartyLabels).not.toContain('spam')\n    })\n\n    it('includes labels from all authorities requested via header', async () => {\n      const authHeaders = await ozone.modHeaders(\n        ids.ToolsOzoneModerationGetRepo,\n      )\n      const { data: recordIncludingExternalLabels } =\n        await agent.api.tools.ozone.moderation.getRepo(\n          { did: sc.dids.alice },\n          {\n            headers: {\n              ...authHeaders,\n              [LABELER_HEADER_NAME]: [\n                thirdPartyLabeler.ctx.cfg.service.did,\n                ozone.ctx.cfg.service.did,\n              ].join(','),\n            },\n          },\n        )\n      const labelVals = recordIncludingExternalLabels.labels?.map((l) => l.val)\n      const labelSources = recordIncludingExternalLabels.labels?.map(\n        (l) => l.src,\n      )\n      expect(labelVals).toContain('weird')\n      expect(labelVals).toContain('spam')\n      expect(labelSources).toContain(thirdPartyLabeler.ctx.cfg.service.did)\n      expect(labelSources).toContain(ozone.ctx.cfg.service.did)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/account-strikes.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`account-strikes tracks strikes and exposes them through queryStatuses and queryEvents 1`] = `\nObject {\n  \"accountStats\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n  },\n  \"accountStrike\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#accountStrike\",\n    \"activeStrikeCount\": 5,\n    \"firstStrikeAt\": \"1970-01-01T00:00:00.000Z\",\n    \"lastStrikeAt\": \"1970-01-01T00:00:00.000Z\",\n    \"totalStrikeCount\": 5,\n  },\n  \"ageAssuranceState\": \"unknown\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"hosting\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n    \"status\": \"unknown\",\n  },\n  \"id\": 9,\n  \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"lastReviewedBy\": \"user(0)\",\n  \"priorityScore\": 0,\n  \"recordsStats\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n  },\n  \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(1)\",\n  },\n  \"subjectBlobCids\": Array [],\n  \"subjectRepoHandle\": \"alice.test\",\n  \"tags\": Array [\n    \"lang:und\",\n  ],\n  \"takendown\": true,\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`account-strikes tracks strikes and exposes them through queryStatuses and queryEvents 2`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReverseTakedown\",\n      \"comment\": \"Appeal granted - reversing first takedown\",\n      \"severityLevel\": \"sev-2\",\n      \"strikeCount\": -2,\n    },\n    \"id\": 11,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTakedown\",\n      \"comment\": \"Warning only\",\n      \"severityLevel\": \"sev-0\",\n      \"strikeCount\": 0,\n    },\n    \"id\": 9,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(1)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTakedown\",\n      \"comment\": \"Second sev-1 violation\",\n      \"policies\": Array [\n        \"spam-policy\",\n      ],\n      \"severityLevel\": \"sev-1\",\n      \"strikeCount\": 1,\n    },\n    \"id\": 7,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(1)\",\n      \"uri\": \"record(1)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTakedown\",\n      \"comment\": \"First sev-1 violation\",\n      \"policies\": Array [\n        \"spam-policy\",\n      ],\n      \"severityLevel\": \"sev-1\",\n      \"strikeCount\": 0,\n    },\n    \"id\": 5,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(2)\",\n      \"uri\": \"record(2)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTakedown\",\n      \"comment\": \"Second violation\",\n      \"severityLevel\": \"sev-2\",\n      \"strikeCount\": 2,\n    },\n    \"id\": 3,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(3)\",\n      \"uri\": \"record(3)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTakedown\",\n      \"comment\": \"First violation\",\n      \"severityLevel\": \"sev-2\",\n      \"strikeCount\": 2,\n    },\n    \"id\": 1,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/age-assurance.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`age assurance events handles age assurance events from user 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(1)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#ageAssuranceEvent\",\n    \"attemptId\": \"attempt-123\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"initIp\": \"123.456.789.012\",\n    \"initUa\": \"Mozilla/5.0\",\n    \"status\": \"pending\",\n  },\n  \"id\": 1,\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(0)\",\n  },\n  \"subjectBlobCids\": Array [],\n}\n`;\n\nexports[`age assurance events handles age assurance events from user 2`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(1)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#ageAssuranceEvent\",\n    \"attemptId\": \"attempt-345\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"initIp\": \"234.567.890.123\",\n    \"initUa\": \"Mozilla/5.0\",\n    \"status\": \"pending\",\n  },\n  \"id\": 3,\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(0)\",\n  },\n  \"subjectBlobCids\": Array [],\n}\n`;\n\nexports[`age assurance events handles age assurance events from user 3`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(1)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#ageAssuranceEvent\",\n    \"attemptId\": \"attempt-345\",\n    \"completeIp\": \"345.678.901.234\",\n    \"completeUa\": \"Mozilla/5.0\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"initIp\": \"234.567.890.123\",\n    \"initUa\": \"Mozilla/5.0\",\n    \"status\": \"assured\",\n  },\n  \"id\": 5,\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(0)\",\n  },\n  \"subjectBlobCids\": Array [],\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/blob-divert.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`blob divert sends blobs to configured divert service and marks divert date 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(0)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#modEventDivert\",\n    \"comment\": \"Diverting for test\",\n  },\n  \"id\": 1,\n  \"subject\": Object {\n    \"$type\": \"com.atproto.repo.strongRef\",\n    \"cid\": \"cids(0)\",\n    \"uri\": \"record(0)\",\n  },\n  \"subjectBlobCids\": Array [\n    \"cids(1)\",\n    \"cids(2)\",\n  ],\n}\n`;\n\nexports[`blob divert sends blobs to configured divert service and marks divert date 2`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(1)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#modEventDivert\",\n    \"comment\": \"Diverting for test\",\n  },\n  \"id\": 1,\n  \"subject\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#recordView\",\n    \"blobCids\": Array [\n      \"cids(1)\",\n      \"cids(2)\",\n    ],\n    \"cid\": \"cids(0)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"moderation\": Object {\n      \"subjectStatus\": Object {\n        \"accountStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n        },\n        \"ageAssuranceState\": \"unknown\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"hosting\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n          \"status\": \"unknown\",\n        },\n        \"id\": 1,\n        \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedBy\": \"user(1)\",\n        \"priorityScore\": 0,\n        \"recordsStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n        },\n        \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n        \"subject\": Object {\n          \"$type\": \"com.atproto.repo.strongRef\",\n          \"cid\": \"cids(0)\",\n          \"uri\": \"record(0)\",\n        },\n        \"subjectBlobCids\": Array [\n          \"cids(1)\",\n          \"cids(2)\",\n        ],\n        \"subjectRepoHandle\": \"carol.test\",\n        \"tags\": Array [\n          \"lang:it\",\n          \"embed:image\",\n        ],\n        \"takendown\": true,\n        \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n      },\n    },\n    \"repo\": Object {\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"moderation\": Object {},\n      \"relatedRecords\": Array [],\n    },\n    \"uri\": \"record(0)\",\n    \"value\": Object {\n      \"$type\": \"app.bsky.feed.post\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(1)\",\n                },\n                \"size\": 4114,\n              },\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"image\": Object {\n                \"$type\": \"blob\",\n                \"mimeType\": \"image/jpeg\",\n                \"ref\": Object {\n                  \"$link\": \"cids(2)\",\n                },\n                \"size\": 12736,\n              },\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"cid\": \"cids(3)\",\n            \"uri\": \"record(1)\",\n          },\n        },\n      },\n      \"text\": \"hi im carol\",\n    },\n  },\n  \"subjectBlobCids\": Array [\n    \"cids(1)\",\n    \"cids(2)\",\n  ],\n  \"subjectBlobs\": Array [\n    Object {\n      \"cid\": \"cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"mimeType\": \"image/jpeg\",\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 1,\n          \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedBy\": \"user(1)\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.repo.strongRef\",\n            \"cid\": \"cids(0)\",\n            \"uri\": \"record(0)\",\n          },\n          \"subjectBlobCids\": Array [\n            \"cids(1)\",\n            \"cids(2)\",\n          ],\n          \"tags\": Array [\n            \"lang:it\",\n            \"embed:image\",\n          ],\n          \"takendown\": true,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"size\": 4114,\n    },\n    Object {\n      \"cid\": \"cids(2)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"mimeType\": \"image/jpeg\",\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 1,\n          \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedBy\": \"user(1)\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.repo.strongRef\",\n            \"cid\": \"cids(0)\",\n            \"uri\": \"record(0)\",\n          },\n          \"subjectBlobCids\": Array [\n            \"cids(1)\",\n            \"cids(2)\",\n          ],\n          \"tags\": Array [\n            \"lang:it\",\n            \"embed:image\",\n          ],\n          \"takendown\": true,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"size\": 12736,\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-account-timeline.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`account timeline Returns entire timeline of events for a given account 1`] = `\nArray [\n  Object {\n    \"count\": 1,\n    \"eventSubjectType\": \"record\",\n    \"eventType\": \"tools.ozone.moderation.defs#modEventReport\",\n  },\n  Object {\n    \"count\": 1,\n    \"eventSubjectType\": \"record\",\n    \"eventType\": \"tools.ozone.moderation.defs#modEventTag\",\n  },\n  Object {\n    \"count\": 2,\n    \"eventSubjectType\": \"account\",\n    \"eventType\": \"tools.ozone.moderation.defs#modEventReport\",\n  },\n  Object {\n    \"count\": 1,\n    \"eventSubjectType\": \"account\",\n    \"eventType\": \"tools.ozone.moderation.defs#modEventTag\",\n  },\n  Object {\n    \"count\": 1,\n    \"eventSubjectType\": \"account\",\n    \"eventType\": \"tools.ozone.moderation.defs#modEventTakedown\",\n  },\n  Object {\n    \"count\": 1,\n    \"eventSubjectType\": \"account\",\n    \"eventType\": \"tools.ozone.moderation.defs#timelineEventPlcOperation\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-record.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`admin get record view gets a record by uri and cid. 1`] = `\nObject {\n  \"blobCids\": Array [],\n  \"blobs\": Array [],\n  \"cid\": \"cids(0)\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"sig\": Object {\n        \"$bytes\": \"sig(0)\",\n      },\n      \"src\": \"user(2)\",\n      \"uri\": \"record(0)\",\n      \"val\": \"!takedown\",\n      \"ver\": 1,\n    },\n    Object {\n      \"cid\": \"cids(0)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(0)\",\n      \"uri\": \"record(0)\",\n      \"val\": \"self-label\",\n    },\n  ],\n  \"moderation\": Object {\n    \"subjectStatus\": Object {\n      \"accountStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      },\n      \"ageAssuranceState\": \"unknown\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"hosting\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n        \"status\": \"unknown\",\n      },\n      \"id\": 1,\n      \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedBy\": \"user(1)\",\n      \"priorityScore\": 0,\n      \"recordsStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n        \"appealedCount\": 0,\n        \"escalatedCount\": 0,\n        \"pendingCount\": 0,\n        \"processedCount\": 1,\n        \"reportedCount\": 1,\n        \"subjectCount\": 1,\n        \"takendownCount\": 1,\n        \"totalReports\": 2,\n      },\n      \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n      \"subject\": Object {\n        \"$type\": \"com.atproto.repo.strongRef\",\n        \"cid\": \"cids(0)\",\n        \"uri\": \"record(0)\",\n      },\n      \"subjectBlobCids\": Array [],\n      \"subjectRepoHandle\": \"alice.test\",\n      \"tags\": Array [\n        \"report:spam\",\n        \"lang:en\",\n        \"report:other\",\n      ],\n      \"takendown\": true,\n      \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  },\n  \"repo\": Object {\n    \"did\": \"user(0)\",\n    \"email\": \"alice@test.com\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invitesDisabled\": false,\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(1)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"displayName\": \"ali\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n      },\n    ],\n  },\n  \"uri\": \"record(0)\",\n  \"value\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Object {\n      \"$type\": \"com.atproto.label.defs#selfLabels\",\n      \"values\": Array [\n        Object {\n          \"val\": \"self-label\",\n        },\n      ],\n    },\n    \"text\": \"hey there\",\n  },\n}\n`;\n\nexports[`admin get record view gets a record by uri, even when taken down. 1`] = `\nObject {\n  \"blobCids\": Array [],\n  \"blobs\": Array [],\n  \"cid\": \"cids(0)\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"sig\": Object {\n        \"$bytes\": \"sig(0)\",\n      },\n      \"src\": \"user(2)\",\n      \"uri\": \"record(0)\",\n      \"val\": \"!takedown\",\n      \"ver\": 1,\n    },\n    Object {\n      \"cid\": \"cids(0)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(0)\",\n      \"uri\": \"record(0)\",\n      \"val\": \"self-label\",\n    },\n  ],\n  \"moderation\": Object {\n    \"subjectStatus\": Object {\n      \"accountStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      },\n      \"ageAssuranceState\": \"unknown\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"hosting\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n        \"status\": \"unknown\",\n      },\n      \"id\": 1,\n      \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedBy\": \"user(1)\",\n      \"priorityScore\": 0,\n      \"recordsStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n        \"appealedCount\": 0,\n        \"escalatedCount\": 0,\n        \"pendingCount\": 0,\n        \"processedCount\": 1,\n        \"reportedCount\": 1,\n        \"subjectCount\": 1,\n        \"takendownCount\": 1,\n        \"totalReports\": 2,\n      },\n      \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n      \"subject\": Object {\n        \"$type\": \"com.atproto.repo.strongRef\",\n        \"cid\": \"cids(0)\",\n        \"uri\": \"record(0)\",\n      },\n      \"subjectBlobCids\": Array [],\n      \"subjectRepoHandle\": \"alice.test\",\n      \"tags\": Array [\n        \"report:spam\",\n        \"lang:en\",\n        \"report:other\",\n      ],\n      \"takendown\": true,\n      \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  },\n  \"repo\": Object {\n    \"did\": \"user(0)\",\n    \"email\": \"alice@test.com\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invitesDisabled\": false,\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(1)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"displayName\": \"ali\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n      },\n    ],\n  },\n  \"uri\": \"record(0)\",\n  \"value\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Object {\n      \"$type\": \"com.atproto.label.defs#selfLabels\",\n      \"values\": Array [\n        Object {\n          \"val\": \"self-label\",\n        },\n      ],\n    },\n    \"text\": \"hey there\",\n  },\n}\n`;\n\nexports[`admin get record view gets record from pds if appview does not have it. 1`] = `\nObject {\n  \"blobCids\": Array [],\n  \"blobs\": Array [],\n  \"cid\": \"cids(0)\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"moderation\": Object {},\n  \"repo\": Object {\n    \"did\": \"user(0)\",\n    \"email\": \"carol@test.com\",\n    \"handle\": \"carol.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invitesDisabled\": false,\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [],\n  },\n  \"uri\": \"record(0)\",\n  \"value\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"text\": \"this is test\",\n  },\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-records.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`admin get records view get multiple records by uris 1`] = `\nObject {\n  \"records\": Array [\n    Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordViewDetail\",\n      \"blobCids\": Array [],\n      \"blobs\": Array [],\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"sig\": Object {\n            \"$bytes\": \"sig(0)\",\n          },\n          \"src\": \"user(2)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"!takedown\",\n          \"ver\": 1,\n        },\n        Object {\n          \"cid\": \"cids(0)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 1,\n          \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedBy\": \"user(1)\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n            \"appealedCount\": 0,\n            \"escalatedCount\": 0,\n            \"pendingCount\": 0,\n            \"processedCount\": 1,\n            \"reportedCount\": 1,\n            \"subjectCount\": 1,\n            \"takendownCount\": 1,\n            \"totalReports\": 2,\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.repo.strongRef\",\n            \"cid\": \"cids(0)\",\n            \"uri\": \"record(0)\",\n          },\n          \"subjectBlobCids\": Array [],\n          \"subjectRepoHandle\": \"alice.test\",\n          \"tags\": Array [\n            \"report:spam\",\n            \"lang:en\",\n            \"report:other\",\n          ],\n          \"takendown\": true,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"repo\": Object {\n        \"did\": \"user(0)\",\n        \"email\": \"alice@test.com\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"invitesDisabled\": false,\n        \"moderation\": Object {},\n        \"relatedRecords\": Array [\n          Object {\n            \"$type\": \"app.bsky.actor.profile\",\n            \"avatar\": Object {\n              \"$type\": \"blob\",\n              \"mimeType\": \"image/jpeg\",\n              \"ref\": Object {\n                \"$link\": \"cids(1)\",\n              },\n              \"size\": 3976,\n            },\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"description\": \"its me!\",\n            \"displayName\": \"ali\",\n            \"labels\": Object {\n              \"$type\": \"com.atproto.label.defs#selfLabels\",\n              \"values\": Array [\n                Object {\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"val\": \"self-label-b\",\n                },\n              ],\n            },\n          },\n        ],\n      },\n      \"uri\": \"record(0)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n    },\n    Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordViewDetail\",\n      \"blobCids\": Array [],\n      \"blobs\": Array [],\n      \"cid\": \"cids(2)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"moderation\": Object {},\n      \"repo\": Object {\n        \"did\": \"user(3)\",\n        \"email\": \"bob@test.com\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"invitesDisabled\": false,\n        \"moderation\": Object {},\n        \"relatedRecords\": Array [\n          Object {\n            \"$type\": \"app.bsky.actor.profile\",\n            \"avatar\": Object {\n              \"$type\": \"blob\",\n              \"mimeType\": \"image/jpeg\",\n              \"ref\": Object {\n                \"$link\": \"cids(1)\",\n              },\n              \"size\": 3976,\n            },\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"description\": \"hi im bob label_me\",\n            \"displayName\": \"bobby\",\n          },\n        ],\n      },\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n    },\n    Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordViewNotFound\",\n      \"uri\": \"record(2)\",\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-repo.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`admin get repo view gets a repo by did, even when taken down. 1`] = `\nObject {\n  \"did\": \"user(0)\",\n  \"email\": \"alice@test.com\",\n  \"handle\": \"alice.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"invites\": Array [],\n  \"invitesDisabled\": false,\n  \"labels\": Array [\n    Object {\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"sig\": Object {\n        \"$bytes\": \"sig(0)\",\n      },\n      \"src\": \"user(2)\",\n      \"uri\": \"user(0)\",\n      \"val\": \"!takedown\",\n      \"ver\": 1,\n    },\n  ],\n  \"moderation\": Object {\n    \"subjectStatus\": Object {\n      \"accountStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n        \"appealCount\": 0,\n        \"escalateCount\": 0,\n        \"reportCount\": 2,\n        \"suspendCount\": 0,\n        \"takedownCount\": 1,\n      },\n      \"ageAssuranceState\": \"unknown\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"hosting\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n        \"status\": \"unknown\",\n      },\n      \"id\": 1,\n      \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedBy\": \"user(1)\",\n      \"priorityScore\": 0,\n      \"recordsStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      },\n      \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n      \"subject\": Object {\n        \"$type\": \"com.atproto.admin.defs#repoRef\",\n        \"did\": \"user(0)\",\n      },\n      \"subjectBlobCids\": Array [],\n      \"subjectRepoHandle\": \"alice.test\",\n      \"tags\": Array [\n        \"lang:und\",\n        \"report:spam\",\n        \"report:other\",\n      ],\n      \"takendown\": true,\n      \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  },\n  \"relatedRecords\": Array [\n    Object {\n      \"$type\": \"app.bsky.actor.profile\",\n      \"avatar\": Object {\n        \"$type\": \"blob\",\n        \"mimeType\": \"image/jpeg\",\n        \"ref\": Object {\n          \"$link\": \"cids(0)\",\n        },\n        \"size\": 3976,\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"displayName\": \"ali\",\n      \"labels\": Object {\n        \"$type\": \"com.atproto.label.defs#selfLabels\",\n        \"values\": Array [\n          Object {\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"val\": \"self-label-b\",\n          },\n        ],\n      },\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-repos.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`admin get multiple repos gets multiple repos by did 1`] = `\nObject {\n  \"repos\": Array [\n    Object {\n      \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n      \"did\": \"user(0)\",\n      \"email\": \"alice@test.com\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"invites\": Array [],\n      \"invitesDisabled\": false,\n      \"labels\": Array [\n        Object {\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"sig\": Object {\n            \"$bytes\": \"sig(0)\",\n          },\n          \"src\": \"user(2)\",\n          \"uri\": \"user(0)\",\n          \"val\": \"!takedown\",\n          \"ver\": 1,\n        },\n      ],\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n            \"appealCount\": 0,\n            \"escalateCount\": 0,\n            \"reportCount\": 2,\n            \"suspendCount\": 0,\n            \"takedownCount\": 1,\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 1,\n          \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedBy\": \"user(1)\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.admin.defs#repoRef\",\n            \"did\": \"user(0)\",\n          },\n          \"subjectBlobCids\": Array [],\n          \"subjectRepoHandle\": \"alice.test\",\n          \"tags\": Array [\n            \"lang:und\",\n            \"report:spam\",\n            \"report:other\",\n          ],\n          \"takendown\": true,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"relatedRecords\": Array [\n        Object {\n          \"$type\": \"app.bsky.actor.profile\",\n          \"avatar\": Object {\n            \"$type\": \"blob\",\n            \"mimeType\": \"image/jpeg\",\n            \"ref\": Object {\n              \"$link\": \"cids(0)\",\n            },\n            \"size\": 3976,\n          },\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"its me!\",\n          \"displayName\": \"ali\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"val\": \"self-label-b\",\n              },\n            ],\n          },\n        },\n      ],\n    },\n    Object {\n      \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n      \"did\": \"user(3)\",\n      \"email\": \"bob@test.com\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"invites\": Array [],\n      \"invitesDisabled\": false,\n      \"labels\": Array [],\n      \"moderation\": Object {},\n      \"relatedRecords\": Array [\n        Object {\n          \"$type\": \"app.bsky.actor.profile\",\n          \"avatar\": Object {\n            \"$type\": \"blob\",\n            \"mimeType\": \"image/jpeg\",\n            \"ref\": Object {\n              \"$link\": \"cids(0)\",\n            },\n            \"size\": 3976,\n          },\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"hi im bob label_me\",\n          \"displayName\": \"bobby\",\n        },\n      ],\n    },\n    Object {\n      \"$type\": \"tools.ozone.moderation.defs#repoViewNotFound\",\n      \"did\": \"did:web:xyz\",\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-starter-pack.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`admin get starter pack view getStarterPack() gets a starterpack by uri 1`] = `\nObject {\n  \"starterPack\": Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(3)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"feeds\": Array [\n      Object {\n        \"cid\": \"cids(3)\",\n        \"creator\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"its me!\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(3)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"did\": \"did:web:example.com\",\n        \"displayName\": \"alice's feedgen\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {},\n      },\n    ],\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"joinedAllTimeCount\": 0,\n    \"joinedWeekCount\": 0,\n    \"labels\": Array [],\n    \"list\": Object {\n      \"cid\": \"cids(4)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 3,\n      \"name\": \"n/a\",\n      \"purpose\": \"app.bsky.graph.defs#referencelist\",\n      \"uri\": \"record(1)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n    \"listItemsSample\": Array [\n      Object {\n        \"subject\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"uri\": \"record(4)\",\n      },\n      Object {\n        \"subject\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"hi im bob label_me\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"uri\": \"record(5)\",\n      },\n      Object {\n        \"subject\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n            \"chat\": Object {\n              \"allowIncoming\": \"none\",\n            },\n          },\n          \"did\": \"user(5)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"uri\": \"record(6)\",\n      },\n    ],\n    \"record\": Object {\n      \"$type\": \"app.bsky.graph.starterpack\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"feeds\": Array [\n        Object {\n          \"uri\": \"record(2)\",\n        },\n      ],\n      \"list\": \"record(1)\",\n      \"name\": \"alice's starter pack\",\n    },\n    \"uri\": \"record(0)\",\n  },\n}\n`;\n\nexports[`admin get starter pack view getStarterPack() gets a starterpack while taken down 1`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"creator\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"feeds\": Array [\n    Object {\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"did\": \"did:web:example.com\",\n      \"displayName\": \"alice's feedgen\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {},\n    },\n  ],\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"joinedAllTimeCount\": 0,\n  \"joinedWeekCount\": 0,\n  \"labels\": Array [],\n  \"list\": Object {\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"n/a\",\n    \"purpose\": \"app.bsky.graph.defs#referencelist\",\n    \"uri\": \"record(1)\",\n    \"viewer\": Object {\n      \"muted\": false,\n    },\n  },\n  \"listItemsSample\": Array [\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(6)\",\n          \"following\": \"record(5)\",\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(4)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(9)\",\n          \"following\": \"record(8)\",\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(7)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(5)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"following\": \"record(11)\",\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(10)\",\n    },\n  ],\n  \"record\": Object {\n    \"$type\": \"app.bsky.graph.starterpack\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"feeds\": Array [\n      Object {\n        \"uri\": \"record(2)\",\n      },\n    ],\n    \"list\": \"record(1)\",\n    \"name\": \"alice's starter pack\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`admin get starter pack view getStarterPack() gets a starterpack while taken down 2`] = `\nObject {\n  \"cid\": \"cids(0)\",\n  \"creator\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(2)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(3)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"feeds\": Array [\n    Object {\n      \"cid\": \"cids(3)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(3)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"did\": \"did:web:example.com\",\n      \"displayName\": \"alice's feedgen\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(2)\",\n      \"viewer\": Object {},\n    },\n  ],\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"joinedAllTimeCount\": 0,\n  \"joinedWeekCount\": 0,\n  \"labels\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"cts\": \"1970-01-01T00:00:00.000Z\",\n      \"src\": \"user(2)\",\n      \"uri\": \"record(0)\",\n      \"val\": \"!takedown\",\n    },\n  ],\n  \"list\": Object {\n    \"cid\": \"cids(4)\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 3,\n    \"name\": \"n/a\",\n    \"purpose\": \"app.bsky.graph.defs#referencelist\",\n    \"uri\": \"record(1)\",\n    \"viewer\": Object {\n      \"muted\": false,\n    },\n  },\n  \"listItemsSample\": Array [\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(3)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(4)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(4)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(5)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n          \"chat\": Object {\n            \"allowIncoming\": \"none\",\n          },\n        },\n        \"did\": \"user(6)\",\n        \"handle\": \"dan.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n  ],\n  \"record\": Object {\n    \"$type\": \"app.bsky.graph.starterpack\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"feeds\": Array [\n      Object {\n        \"uri\": \"record(2)\",\n      },\n    ],\n    \"list\": \"record(1)\",\n    \"name\": \"alice's starter pack\",\n  },\n  \"uri\": \"record(0)\",\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/get-subjects.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`admin get multiple subjects with all relevant details gets multiple subjects with records 1`] = `\nArray [\n  Object {\n    \"profile\": Object {\n      \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"followersCount\": 2,\n      \"followsCount\": 3,\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"postsCount\": 4,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"repo\": Object {\n      \"did\": \"user(0)\",\n      \"email\": \"alice@test.com\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"invites\": Array [],\n      \"invitesDisabled\": false,\n      \"labels\": Array [\n        Object {\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"sig\": Object {\n            \"$bytes\": \"sig(0)\",\n          },\n          \"src\": \"user(2)\",\n          \"uri\": \"user(0)\",\n          \"val\": \"!takedown\",\n          \"ver\": 1,\n        },\n      ],\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n            \"appealCount\": 0,\n            \"escalateCount\": 0,\n            \"reportCount\": 1,\n            \"suspendCount\": 0,\n            \"takedownCount\": 1,\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 3,\n          \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedBy\": \"user(1)\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n            \"appealedCount\": 0,\n            \"escalatedCount\": 0,\n            \"pendingCount\": 1,\n            \"processedCount\": 0,\n            \"reportedCount\": 1,\n            \"subjectCount\": 1,\n            \"takendownCount\": 0,\n            \"totalReports\": 1,\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.admin.defs#repoRef\",\n            \"did\": \"user(0)\",\n          },\n          \"subjectBlobCids\": Array [],\n          \"subjectRepoHandle\": \"alice.test\",\n          \"tags\": Array [\n            \"report:other\",\n            \"lang:und\",\n          ],\n          \"takendown\": true,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"relatedRecords\": Array [\n        Object {\n          \"$type\": \"app.bsky.actor.profile\",\n          \"avatar\": Object {\n            \"$type\": \"blob\",\n            \"mimeType\": \"image/jpeg\",\n            \"ref\": Object {\n              \"$link\": \"cids(0)\",\n            },\n            \"size\": 3976,\n          },\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"its me!\",\n          \"displayName\": \"ali\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"val\": \"self-label-b\",\n              },\n            ],\n          },\n        },\n      ],\n    },\n    \"status\": Object {\n      \"accountStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n        \"appealCount\": 0,\n        \"escalateCount\": 0,\n        \"reportCount\": 1,\n        \"suspendCount\": 0,\n        \"takedownCount\": 1,\n      },\n      \"ageAssuranceState\": \"unknown\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"hosting\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n        \"status\": \"unknown\",\n      },\n      \"id\": 3,\n      \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedBy\": \"user(1)\",\n      \"priorityScore\": 0,\n      \"recordsStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n        \"appealedCount\": 0,\n        \"escalatedCount\": 0,\n        \"pendingCount\": 1,\n        \"processedCount\": 0,\n        \"reportedCount\": 1,\n        \"subjectCount\": 1,\n        \"takendownCount\": 0,\n        \"totalReports\": 1,\n      },\n      \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n      \"subject\": Object {\n        \"$type\": \"com.atproto.admin.defs#repoRef\",\n        \"did\": \"user(0)\",\n      },\n      \"subjectBlobCids\": Array [],\n      \"subjectRepoHandle\": \"alice.test\",\n      \"tags\": Array [\n        \"report:other\",\n        \"lang:und\",\n      ],\n      \"takendown\": true,\n      \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n    \"subject\": \"user(0)\",\n    \"type\": \"account\",\n  },\n  Object {\n    \"profile\": Object {\n      \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"followersCount\": 2,\n      \"followsCount\": 3,\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"postsCount\": 4,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"record\": Object {\n      \"blobCids\": Array [],\n      \"blobs\": Array [],\n      \"cid\": \"cids(2)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label\",\n        },\n      ],\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n            \"appealCount\": 0,\n            \"escalateCount\": 0,\n            \"reportCount\": 1,\n            \"suspendCount\": 0,\n            \"takedownCount\": 1,\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 1,\n          \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n            \"appealedCount\": 0,\n            \"escalatedCount\": 0,\n            \"pendingCount\": 1,\n            \"processedCount\": 0,\n            \"reportedCount\": 1,\n            \"subjectCount\": 1,\n            \"takendownCount\": 0,\n            \"totalReports\": 1,\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.repo.strongRef\",\n            \"cid\": \"cids(2)\",\n            \"uri\": \"record(1)\",\n          },\n          \"subjectBlobCids\": Array [],\n          \"subjectRepoHandle\": \"alice.test\",\n          \"tags\": Array [\n            \"report:spam\",\n            \"lang:en\",\n          ],\n          \"takendown\": false,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"repo\": Object {\n        \"did\": \"user(0)\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"moderation\": Object {\n          \"subjectStatus\": Object {\n            \"accountStats\": Object {\n              \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n              \"appealCount\": 0,\n              \"escalateCount\": 0,\n              \"reportCount\": 1,\n              \"suspendCount\": 0,\n              \"takedownCount\": 1,\n            },\n            \"ageAssuranceState\": \"unknown\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"hosting\": Object {\n              \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n              \"status\": \"unknown\",\n            },\n            \"id\": 3,\n            \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"lastReviewedBy\": \"user(1)\",\n            \"priorityScore\": 0,\n            \"recordsStats\": Object {\n              \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n              \"appealedCount\": 0,\n              \"escalatedCount\": 0,\n              \"pendingCount\": 1,\n              \"processedCount\": 0,\n              \"reportedCount\": 1,\n              \"subjectCount\": 1,\n              \"takendownCount\": 0,\n              \"totalReports\": 1,\n            },\n            \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n            \"subject\": Object {\n              \"$type\": \"com.atproto.admin.defs#repoRef\",\n              \"did\": \"user(0)\",\n            },\n            \"subjectBlobCids\": Array [],\n            \"subjectRepoHandle\": \"alice.test\",\n            \"tags\": Array [\n              \"report:other\",\n              \"lang:und\",\n            ],\n            \"takendown\": true,\n            \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n          },\n        },\n        \"relatedRecords\": Array [\n          Object {\n            \"$type\": \"app.bsky.actor.profile\",\n            \"avatar\": Object {\n              \"$type\": \"blob\",\n              \"mimeType\": \"image/jpeg\",\n              \"ref\": Object {\n                \"$link\": \"cids(0)\",\n              },\n              \"size\": 3976,\n            },\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"description\": \"its me!\",\n            \"displayName\": \"ali\",\n            \"labels\": Object {\n              \"$type\": \"com.atproto.label.defs#selfLabels\",\n              \"values\": Array [\n                Object {\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"val\": \"self-label-b\",\n                },\n              ],\n            },\n          },\n        ],\n      },\n      \"uri\": \"record(1)\",\n      \"value\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label\",\n            },\n          ],\n        },\n        \"text\": \"hey there\",\n      },\n    },\n    \"repo\": Object {\n      \"did\": \"user(0)\",\n      \"email\": \"alice@test.com\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"invites\": Array [],\n      \"invitesDisabled\": false,\n      \"labels\": Array [\n        Object {\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"sig\": Object {\n            \"$bytes\": \"sig(0)\",\n          },\n          \"src\": \"user(2)\",\n          \"uri\": \"user(0)\",\n          \"val\": \"!takedown\",\n          \"ver\": 1,\n        },\n      ],\n      \"moderation\": Object {\n        \"subjectStatus\": Object {\n          \"accountStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n            \"appealCount\": 0,\n            \"escalateCount\": 0,\n            \"reportCount\": 1,\n            \"suspendCount\": 0,\n            \"takedownCount\": 1,\n          },\n          \"ageAssuranceState\": \"unknown\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"hosting\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n            \"status\": \"unknown\",\n          },\n          \"id\": 3,\n          \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"lastReviewedBy\": \"user(1)\",\n          \"priorityScore\": 0,\n          \"recordsStats\": Object {\n            \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n            \"appealedCount\": 0,\n            \"escalatedCount\": 0,\n            \"pendingCount\": 1,\n            \"processedCount\": 0,\n            \"reportedCount\": 1,\n            \"subjectCount\": 1,\n            \"takendownCount\": 0,\n            \"totalReports\": 1,\n          },\n          \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n          \"subject\": Object {\n            \"$type\": \"com.atproto.admin.defs#repoRef\",\n            \"did\": \"user(0)\",\n          },\n          \"subjectBlobCids\": Array [],\n          \"subjectRepoHandle\": \"alice.test\",\n          \"tags\": Array [\n            \"report:other\",\n            \"lang:und\",\n          ],\n          \"takendown\": true,\n          \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n        },\n      },\n      \"relatedRecords\": Array [\n        Object {\n          \"$type\": \"app.bsky.actor.profile\",\n          \"avatar\": Object {\n            \"$type\": \"blob\",\n            \"mimeType\": \"image/jpeg\",\n            \"ref\": Object {\n              \"$link\": \"cids(0)\",\n            },\n            \"size\": 3976,\n          },\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"its me!\",\n          \"displayName\": \"ali\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"val\": \"self-label-b\",\n              },\n            ],\n          },\n        },\n      ],\n    },\n    \"status\": Object {\n      \"accountStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n        \"appealCount\": 0,\n        \"escalateCount\": 0,\n        \"reportCount\": 1,\n        \"suspendCount\": 0,\n        \"takedownCount\": 1,\n      },\n      \"ageAssuranceState\": \"unknown\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"hosting\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n        \"status\": \"unknown\",\n      },\n      \"id\": 1,\n      \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"priorityScore\": 0,\n      \"recordsStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n        \"appealedCount\": 0,\n        \"escalatedCount\": 0,\n        \"pendingCount\": 1,\n        \"processedCount\": 0,\n        \"reportedCount\": 1,\n        \"subjectCount\": 1,\n        \"takendownCount\": 0,\n        \"totalReports\": 1,\n      },\n      \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n      \"subject\": Object {\n        \"$type\": \"com.atproto.repo.strongRef\",\n        \"cid\": \"cids(2)\",\n        \"uri\": \"record(1)\",\n      },\n      \"subjectBlobCids\": Array [],\n      \"subjectRepoHandle\": \"alice.test\",\n      \"tags\": Array [\n        \"report:spam\",\n        \"lang:en\",\n      ],\n      \"takendown\": false,\n      \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n    \"subject\": \"record(1)\",\n    \"type\": \"record\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`moderation-events get event gets an event by specific id 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(2)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n    \"comment\": \"X\",\n    \"isReporterMuted\": false,\n    \"reportType\": \"com.atproto.moderation.defs#reasonMisleading\",\n  },\n  \"id\": 1,\n  \"subject\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#repoView\",\n    \"did\": \"user(0)\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"moderation\": Object {\n      \"subjectStatus\": Object {\n        \"accountStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n          \"appealCount\": 0,\n          \"escalateCount\": 1,\n          \"reportCount\": 4,\n          \"suspendCount\": 0,\n          \"takedownCount\": 0,\n        },\n        \"ageAssuranceState\": \"unknown\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"hosting\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n          \"status\": \"unknown\",\n        },\n        \"id\": 1,\n        \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedBy\": \"user(1)\",\n        \"priorityScore\": 0,\n        \"recordsStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n          \"appealedCount\": 0,\n          \"escalatedCount\": 0,\n          \"pendingCount\": 2,\n          \"processedCount\": 0,\n          \"reportedCount\": 2,\n          \"subjectCount\": 2,\n          \"takendownCount\": 0,\n          \"totalReports\": 3,\n        },\n        \"reviewState\": \"tools.ozone.moderation.defs#reviewEscalated\",\n        \"subject\": Object {\n          \"$type\": \"com.atproto.admin.defs#repoRef\",\n          \"did\": \"user(0)\",\n        },\n        \"subjectBlobCids\": Array [],\n        \"subjectRepoHandle\": \"alice.test\",\n        \"tags\": Array [\n          \"report:misleading\",\n          \"lang:und\",\n          \"report:spam\",\n        ],\n        \"takendown\": false,\n        \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n      },\n    },\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(0)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"displayName\": \"ali\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n      },\n    ],\n  },\n  \"subjectBlobCids\": Array [],\n  \"subjectBlobs\": Array [],\n}\n`;\n\nexports[`moderation-events query events returns all events for record or repo 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"alice.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"X\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 11,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(2)\",\n    \"creatorHandle\": \"mod-authority.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTag\",\n      \"add\": Array [\n        \"report:spam\",\n        \"lang:en\",\n        \"lang:i\",\n      ],\n      \"remove\": Array [],\n    },\n    \"id\": 6,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"alice.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"X\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 5,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n]\n`;\n\nexports[`moderation-events query events returns all events for record or repo 2`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"X\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 10,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"mod-authority.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTag\",\n      \"add\": Array [\n        \"report:spam\",\n        \"lang:en\",\n      ],\n      \"remove\": Array [],\n    },\n    \"id\": 4,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"X\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 3,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n]\n`;\n\nexports[`moderation-events query events returns events matching multiple keywords in comment 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"rainy days feel lazy\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 17,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"november rain\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 15,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n]\n`;\n\nexports[`moderation-events query events returns events matching multiple keywords in comment 2`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"rainy days feel lazy\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 17,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"november rain\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 15,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n]\n`;\n\nexports[`moderation-events query events returns events matching multiple keywords in comment 3`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"rainy days feel lazy\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 17,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"bob.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"november rain\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 15,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"alice.test\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`moderation-statuses query statuses returns statuses filtered by subject language 1`] = `\nArray [\n  Object {\n    \"accountStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      \"appealCount\": 0,\n      \"escalateCount\": 0,\n      \"reportCount\": 2,\n      \"suspendCount\": 0,\n      \"takedownCount\": 0,\n    },\n    \"ageAssuranceState\": \"unknown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"hosting\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n      \"status\": \"unknown\",\n    },\n    \"id\": 7,\n    \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"priorityScore\": 0,\n    \"recordsStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      \"appealedCount\": 0,\n      \"escalatedCount\": 0,\n      \"pendingCount\": 1,\n      \"processedCount\": 0,\n      \"reportedCount\": 1,\n      \"subjectCount\": 1,\n      \"takendownCount\": 0,\n      \"totalReports\": 2,\n    },\n    \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectRepoHandle\": \"bob.test\",\n    \"tags\": Array [\n      \"report:spam\",\n      \"lang:en\",\n      \"lang:i\",\n    ],\n    \"takendown\": false,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"accountStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      \"appealCount\": 0,\n      \"escalateCount\": 0,\n      \"reportCount\": 2,\n      \"suspendCount\": 0,\n      \"takedownCount\": 0,\n    },\n    \"ageAssuranceState\": \"unknown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"hosting\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n      \"status\": \"unknown\",\n    },\n    \"id\": 5,\n    \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"priorityScore\": 0,\n    \"recordsStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      \"appealedCount\": 0,\n      \"escalatedCount\": 0,\n      \"pendingCount\": 1,\n      \"processedCount\": 0,\n      \"reportedCount\": 1,\n      \"subjectCount\": 1,\n      \"takendownCount\": 0,\n      \"totalReports\": 2,\n    },\n    \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectRepoHandle\": \"bob.test\",\n    \"tags\": Array [\n      \"report:spam\",\n      \"lang:en\",\n      \"lang:i\",\n    ],\n    \"takendown\": false,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n]\n`;\n\nexports[`moderation-statuses query statuses returns statuses for subjects that received moderation events 1`] = `\nArray [\n  Object {\n    \"accountStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      \"appealCount\": 0,\n      \"escalateCount\": 0,\n      \"reportCount\": 2,\n      \"suspendCount\": 0,\n      \"takedownCount\": 0,\n    },\n    \"ageAssuranceState\": \"unknown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"hosting\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n      \"status\": \"unknown\",\n    },\n    \"id\": 7,\n    \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"priorityScore\": 0,\n    \"recordsStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      \"appealedCount\": 0,\n      \"escalatedCount\": 0,\n      \"pendingCount\": 1,\n      \"processedCount\": 0,\n      \"reportedCount\": 1,\n      \"subjectCount\": 1,\n      \"takendownCount\": 0,\n      \"totalReports\": 2,\n    },\n    \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectRepoHandle\": \"bob.test\",\n    \"tags\": Array [\n      \"report:spam\",\n      \"lang:en\",\n      \"lang:i\",\n    ],\n    \"takendown\": false,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"accountStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      \"appealCount\": 0,\n      \"escalateCount\": 0,\n      \"reportCount\": 2,\n      \"suspendCount\": 0,\n      \"takedownCount\": 0,\n    },\n    \"ageAssuranceState\": \"unknown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"hosting\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n      \"status\": \"unknown\",\n    },\n    \"id\": 5,\n    \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"priorityScore\": 0,\n    \"recordsStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      \"appealedCount\": 0,\n      \"escalatedCount\": 0,\n      \"pendingCount\": 1,\n      \"processedCount\": 0,\n      \"reportedCount\": 1,\n      \"subjectCount\": 1,\n      \"takendownCount\": 0,\n      \"totalReports\": 2,\n    },\n    \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectRepoHandle\": \"bob.test\",\n    \"tags\": Array [\n      \"report:spam\",\n      \"lang:en\",\n      \"lang:i\",\n    ],\n    \"takendown\": false,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"accountStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      \"appealCount\": 0,\n      \"escalateCount\": 0,\n      \"reportCount\": 2,\n      \"suspendCount\": 0,\n      \"takedownCount\": 0,\n    },\n    \"ageAssuranceState\": \"unknown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"hosting\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n      \"status\": \"unknown\",\n    },\n    \"id\": 3,\n    \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"priorityScore\": 0,\n    \"recordsStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      \"appealedCount\": 0,\n      \"escalatedCount\": 0,\n      \"pendingCount\": 1,\n      \"processedCount\": 0,\n      \"reportedCount\": 1,\n      \"subjectCount\": 1,\n      \"takendownCount\": 0,\n      \"totalReports\": 2,\n    },\n    \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(1)\",\n      \"uri\": \"record(1)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectRepoHandle\": \"alice.test\",\n    \"tags\": Array [\n      \"report:spam\",\n      \"lang:ha\",\n    ],\n    \"takendown\": false,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"accountStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n      \"appealCount\": 0,\n      \"escalateCount\": 0,\n      \"reportCount\": 2,\n      \"suspendCount\": 0,\n      \"takedownCount\": 0,\n    },\n    \"ageAssuranceState\": \"unknown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"hosting\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n      \"status\": \"unknown\",\n    },\n    \"id\": 1,\n    \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"priorityScore\": 0,\n    \"recordsStats\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n      \"appealedCount\": 0,\n      \"escalatedCount\": 0,\n      \"pendingCount\": 1,\n      \"processedCount\": 0,\n      \"reportedCount\": 1,\n      \"subjectCount\": 1,\n      \"takendownCount\": 0,\n      \"totalReports\": 2,\n    },\n    \"reviewState\": \"tools.ozone.moderation.defs#reviewOpen\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(1)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectRepoHandle\": \"alice.test\",\n    \"tags\": Array [\n      \"report:misleading\",\n      \"lang:und\",\n    ],\n    \"takendown\": false,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/moderation.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`moderation reporting creates reports of a DM chat. 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 14,\n    \"reasonType\": \"com.atproto.moderation.defs#reasonSpam\",\n    \"reportedBy\": \"user(0)\",\n    \"subject\": Object {\n      \"$type\": \"chat.bsky.convo.defs#messageRef\",\n      \"convoId\": \"testconvoid1\",\n      \"did\": \"user(1)\",\n      \"messageId\": \"testmessageid1\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 16,\n    \"reason\": \"defamation\",\n    \"reasonType\": \"com.atproto.moderation.defs#reasonOther\",\n    \"reportedBy\": \"user(1)\",\n    \"subject\": Object {\n      \"$type\": \"chat.bsky.convo.defs#messageRef\",\n      \"convoId\": \"\",\n      \"did\": \"user(1)\",\n      \"messageId\": \"testmessageid2\",\n    },\n  },\n]\n`;\n\nexports[`moderation reporting creates reports of a record. 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 7,\n    \"reasonType\": \"com.atproto.moderation.defs#reasonSpam\",\n    \"reportedBy\": \"user(0)\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 9,\n    \"reason\": \"defamation\",\n    \"reasonType\": \"com.atproto.moderation.defs#reasonOther\",\n    \"reportedBy\": \"user(1)\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(1)\",\n      \"uri\": \"record(1)\",\n    },\n  },\n]\n`;\n\nexports[`moderation reporting creates reports of a repo. 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 1,\n    \"reasonType\": \"com.atproto.moderation.defs#reasonSpam\",\n    \"reportedBy\": \"user(0)\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(1)\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 3,\n    \"reason\": \"impersonation\",\n    \"reasonType\": \"com.atproto.moderation.defs#reasonOther\",\n    \"reportedBy\": \"user(2)\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(1)\",\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/report-reason.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`report reason createReport only passes for allowed reason types 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"id\": 1,\n  \"reasonType\": \"tools.ozone.report.defs#reasonHarassmentTroll\",\n  \"reportedBy\": \"user(0)\",\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(1)\",\n  },\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/safelink.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`safelink management addRule allows admins to add rules 1`] = `\nObject {\n  \"action\": \"block\",\n  \"comment\": \"Known phishing domain targeting users\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(0)\",\n  \"eventType\": \"addRule\",\n  \"id\": 1,\n  \"pattern\": \"domain\",\n  \"reason\": \"phishing\",\n  \"url\": \"https://malicious-site.com\",\n}\n`;\n\nexports[`safelink management queryEvents allows querying safelink events 1`] = `\nArray [\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Escalated to block\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"updateRule\",\n    \"id\": 10,\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"url\": \"https://events-test.com\",\n  },\n  Object {\n    \"action\": \"warn\",\n    \"comment\": \"Initial rule creation\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 9,\n    \"pattern\": \"domain\",\n    \"reason\": \"spam\",\n    \"url\": \"https://events-test.com\",\n  },\n  Object {\n    \"action\": \"warn\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 8,\n    \"pattern\": \"url\",\n    \"reason\": \"spam\",\n    \"url\": \"https://query-test2.com/specific-path\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 7,\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"url\": \"https://query-test1.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 6,\n    \"pattern\": \"domain\",\n    \"reason\": \"spam\",\n    \"url\": \"https://remove-test2.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Removing rule - false positive\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"removeRule\",\n    \"id\": 5,\n    \"pattern\": \"url\",\n    \"reason\": \"csam\",\n    \"url\": \"https://remove-test.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Rule to be removed\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 4,\n    \"pattern\": \"url\",\n    \"reason\": \"csam\",\n    \"url\": \"https://remove-test.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Updated: confirmed phishing site\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"updateRule\",\n    \"id\": 3,\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"url\": \"https://update-test.com\",\n  },\n  Object {\n    \"action\": \"warn\",\n    \"comment\": \"Initially marked as spam\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 2,\n    \"pattern\": \"domain\",\n    \"reason\": \"spam\",\n    \"url\": \"https://update-test.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Known phishing domain targeting users\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"eventType\": \"addRule\",\n    \"id\": 1,\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"url\": \"https://malicious-site.com\",\n  },\n]\n`;\n\nexports[`safelink management queryRules allows querying all active rules 1`] = `\nArray [\n  Object {\n    \"action\": \"warn\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"pattern\": \"url\",\n    \"reason\": \"spam\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"url\": \"https://query-test2.com/specific-path\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"url\": \"https://query-test1.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"pattern\": \"domain\",\n    \"reason\": \"spam\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"url\": \"https://remove-test2.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Updated: confirmed phishing site\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"url\": \"https://update-test.com\",\n  },\n  Object {\n    \"action\": \"block\",\n    \"comment\": \"Known phishing domain targeting users\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"pattern\": \"domain\",\n    \"reason\": \"phishing\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"url\": \"https://malicious-site.com\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/scheduled-action.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`scheduled action management listScheduledActions allows moderators to list all scheduled actions 1`] = `\nArray [\n  Object {\n    \"action\": \"takedown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"did\": \"user(0)\",\n    \"eventData\": Object {\n      \"$type\": \"tools.ozone.moderation.scheduleAction#takedown\",\n      \"comment\": \"test\",\n      \"policies\": Array [\n        \"spam\",\n      ],\n    },\n    \"executeAfter\": \"1970-01-01T00:00:00.000Z\",\n    \"executeUntil\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 3,\n    \"randomizeExecution\": true,\n    \"status\": \"pending\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"action\": \"takedown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"did\": \"user(2)\",\n    \"eventData\": Object {\n      \"$type\": \"tools.ozone.moderation.scheduleAction#takedown\",\n      \"comment\": \"test\",\n      \"policies\": Array [\n        \"spam\",\n      ],\n    },\n    \"executeAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 2,\n    \"randomizeExecution\": false,\n    \"status\": \"pending\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"action\": \"takedown\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"did\": \"user(3)\",\n    \"eventData\": Object {\n      \"$type\": \"tools.ozone.moderation.scheduleAction#takedown\",\n      \"comment\": \"test\",\n      \"policies\": Array [\n        \"spam\",\n      ],\n    },\n    \"executeAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 1,\n    \"randomizeExecution\": false,\n    \"status\": \"pending\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/sets.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ozone-sets querySets returns all sets when no parameters are provided 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"Another test set\",\n    \"name\": \"another-set\",\n    \"setSize\": 0,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"Test set 1\",\n    \"name\": \"test-set-1\",\n    \"setSize\": 0,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"name\": \"test-set-2\",\n    \"setSize\": 0,\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n]\n`;\n\nexports[`ozone-sets upsertSet creates a new set 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"description\": \"A new test set\",\n  \"name\": \"new-test-set\",\n  \"setSize\": 0,\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`ozone-sets upsertSet updates an existing set 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"description\": \"Updated description\",\n  \"name\": \"new-test-set\",\n  \"setSize\": 0,\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/settings.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ozone-settings listOptions returns all personal settings 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"description\": \"List of external labelers that will be plugged into the client views\",\n    \"did\": \"user(1)\",\n    \"key\": \"tools.ozone.setting.client.externalLabelers\",\n    \"lastUpdatedBy\": \"user(1)\",\n    \"managerRole\": \"tools.ozone.team.defs#roleAdmin\",\n    \"scope\": \"instance\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"value\": Object {\n      \"dids\": Array [\n        \"user(0)\",\n      ],\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"description\": \"This determines how each queue is balanced when sorted by oldest first\",\n    \"did\": \"user(1)\",\n    \"key\": \"tools.ozone.setting.client.queueHash\",\n    \"lastUpdatedBy\": \"user(1)\",\n    \"managerRole\": \"tools.ozone.team.defs#roleAdmin\",\n    \"scope\": \"instance\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"value\": Object {\n      \"val\": 10.5,\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"description\": \"This determines how many queues the client interface will show\",\n    \"did\": \"user(1)\",\n    \"key\": \"tools.ozone.setting.client.queues\",\n    \"lastUpdatedBy\": \"user(1)\",\n    \"managerRole\": \"tools.ozone.team.defs#roleAdmin\",\n    \"scope\": \"instance\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"value\": Object {\n      \"stratosphere\": Object {\n        \"name\": \"Stratosphere\",\n      },\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/team.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`team management addMember only allows admins to add member 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"did\": \"user(0)\",\n  \"disabled\": false,\n  \"lastUpdatedBy\": \"user(1)\",\n  \"role\": \"tools.ozone.team.defs#roleAdmin\",\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n\nexports[`team management listMembers allows all members to list all members 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleAdmin\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(2)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleTriage\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(3)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleModerator\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(4)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleModerator\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(1)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": true,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"description\": \"The pretend version of mod.bsky.app\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"Dev-env Moderation\",\n      \"followersCount\": 0,\n      \"followsCount\": 0,\n      \"handle\": \"mod-authority.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 0,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleAdmin\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(5)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(5)\",\n      \"displayName\": \"ali\",\n      \"followersCount\": 2,\n      \"followsCount\": 3,\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(5)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(5)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"postsCount\": 4,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleAdmin\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(7)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(8)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(7)\",\n      \"displayName\": \"bobby\",\n      \"followersCount\": 2,\n      \"followsCount\": 2,\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 3,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleModerator\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(9)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"did\": \"user(9)\",\n      \"followersCount\": 2,\n      \"followsCount\": 1,\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"postsCount\": 2,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleTriage\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n]\n`;\n\nexports[`team management listMembers allows all members to list all members 2`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleAdmin\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(2)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleTriage\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(3)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleModerator\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(4)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"role\": \"tools.ozone.team.defs#roleModerator\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(1)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": true,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"description\": \"The pretend version of mod.bsky.app\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"Dev-env Moderation\",\n      \"followersCount\": 0,\n      \"followsCount\": 0,\n      \"handle\": \"mod-authority.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 0,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleAdmin\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(5)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(6)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(5)\",\n      \"displayName\": \"ali\",\n      \"followersCount\": 2,\n      \"followsCount\": 3,\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(5)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(5)\",\n          \"uri\": \"record(0)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"postsCount\": 4,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleAdmin\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(7)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(8)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(7)\",\n      \"displayName\": \"bobby\",\n      \"followersCount\": 2,\n      \"followsCount\": 2,\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 3,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleModerator\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(9)\",\n    \"disabled\": false,\n    \"lastUpdatedBy\": \"user(1)\",\n    \"profile\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"did\": \"user(9)\",\n      \"followersCount\": 2,\n      \"followsCount\": 1,\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"postsCount\": 2,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"role\": \"tools.ozone.team.defs#roleTriage\",\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/verification-listener.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`verification-listener indexes new and revoked verifications 1`] = `\nArray [\n  Object {\n    \"$type\": \"tools.ozone.verification.defs#verificationView\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"displayName\": \"Alice\",\n    \"handle\": \"alice.test\",\n    \"issuer\": \"user(0)\",\n    \"issuerProfile\": Object {\n      \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"followersCount\": 2,\n      \"followsCount\": 2,\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 3,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"issuerRepo\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n      \"did\": \"user(0)\",\n      \"email\": \"bob@test.com\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"invites\": Array [],\n      \"invitesDisabled\": false,\n      \"labels\": Array [],\n      \"moderation\": Object {},\n      \"relatedRecords\": Array [\n        Object {\n          \"$type\": \"app.bsky.actor.profile\",\n          \"avatar\": Object {\n            \"$type\": \"blob\",\n            \"mimeType\": \"image/jpeg\",\n            \"ref\": Object {\n              \"$link\": \"cids(0)\",\n            },\n            \"size\": 3976,\n          },\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"hi im bob label_me\",\n          \"displayName\": \"bobby\",\n        },\n      ],\n    },\n    \"revokedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"revokedBy\": \"user(0)\",\n    \"subject\": \"user(1)\",\n    \"subjectProfile\": Object {\n      \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"followersCount\": 2,\n      \"followsCount\": 3,\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"postsCount\": 4,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"subjectRepo\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n      \"did\": \"user(1)\",\n      \"email\": \"alice@test.com\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"invites\": Array [],\n      \"invitesDisabled\": false,\n      \"labels\": Array [],\n      \"moderation\": Object {},\n      \"relatedRecords\": Array [\n        Object {\n          \"$type\": \"app.bsky.actor.profile\",\n          \"avatar\": Object {\n            \"$type\": \"blob\",\n            \"mimeType\": \"image/jpeg\",\n            \"ref\": Object {\n              \"$link\": \"cids(0)\",\n            },\n            \"size\": 3976,\n          },\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"description\": \"its me!\",\n          \"displayName\": \"ali\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"val\": \"self-label-b\",\n              },\n            ],\n          },\n        },\n      ],\n    },\n    \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"uri\": \"record(0)\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/ozone/tests/__snapshots__/verification.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`verification list returns paginated list of verifications 1`] = `\nObject {\n  \"$type\": \"tools.ozone.verification.defs#verificationView\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"displayName\": \"bobby\",\n  \"handle\": \"bob.test\",\n  \"issuer\": \"user(0)\",\n  \"issuerProfile\": Object {\n    \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"its me!\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"followersCount\": 2,\n    \"followsCount\": 3,\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"postsCount\": 4,\n    \"verification\": Object {\n      \"trustedVerifierStatus\": \"valid\",\n      \"verifications\": Array [],\n      \"verifiedStatus\": \"none\",\n    },\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"issuerRepo\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n    \"did\": \"user(0)\",\n    \"email\": \"alice@test.com\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invites\": Array [],\n    \"invitesDisabled\": false,\n    \"labels\": Array [],\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(0)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"displayName\": \"ali\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n      },\n    ],\n  },\n  \"subject\": \"user(1)\",\n  \"subjectProfile\": Object {\n    \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(1)\",\n    \"displayName\": \"bobby\",\n    \"followersCount\": 2,\n    \"followsCount\": 2,\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"postsCount\": 3,\n    \"verification\": Object {\n      \"trustedVerifierStatus\": \"none\",\n      \"verifications\": Array [\n        Object {\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"isValid\": true,\n          \"issuer\": \"user(0)\",\n          \"uri\": \"record(0)\",\n        },\n      ],\n      \"verifiedStatus\": \"valid\",\n    },\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"subjectRepo\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n    \"did\": \"user(1)\",\n    \"email\": \"bob@test.com\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invites\": Array [],\n    \"invitesDisabled\": false,\n    \"labels\": Array [],\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(0)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"displayName\": \"bobby\",\n      },\n    ],\n  },\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"uri\": \"record(0)\",\n}\n`;\n\nexports[`verification list returns paginated list of verifications 2`] = `\nObject {\n  \"$type\": \"tools.ozone.verification.defs#verificationView\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"displayName\": \"\",\n  \"handle\": \"carol.test\",\n  \"issuer\": \"user(0)\",\n  \"issuerProfile\": Object {\n    \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"its me!\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"followersCount\": 2,\n    \"followsCount\": 3,\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(1)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"postsCount\": 4,\n    \"verification\": Object {\n      \"trustedVerifierStatus\": \"valid\",\n      \"verifications\": Array [],\n      \"verifiedStatus\": \"none\",\n    },\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"issuerRepo\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n    \"did\": \"user(0)\",\n    \"email\": \"alice@test.com\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invites\": Array [],\n    \"invitesDisabled\": false,\n    \"labels\": Array [],\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(0)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"displayName\": \"ali\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n      },\n    ],\n  },\n  \"revokeReason\": \"Testing\",\n  \"revokedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"revokedBy\": \"user(0)\",\n  \"subject\": \"user(1)\",\n  \"subjectProfile\": Object {\n    \"$type\": \"app.bsky.actor.defs#profileViewDetailed\",\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"feedgens\": 0,\n      \"labeler\": false,\n      \"lists\": 0,\n      \"starterPacks\": 0,\n    },\n    \"did\": \"user(1)\",\n    \"followersCount\": 2,\n    \"followsCount\": 1,\n    \"handle\": \"carol.test\",\n    \"labels\": Array [],\n    \"postsCount\": 2,\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  \"subjectRepo\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#repoViewDetail\",\n    \"did\": \"user(1)\",\n    \"email\": \"carol@test.com\",\n    \"handle\": \"carol.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invites\": Array [],\n    \"invitesDisabled\": false,\n    \"labels\": Array [],\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [],\n  },\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"uri\": \"record(0)\",\n}\n`;\n"
  },
  {
    "path": "packages/ozone/tests/_util.ts",
    "content": "import { Server } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { type Express } from 'express'\nimport { CID } from 'multiformats/cid'\nimport { lexToJson } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport {\n  isView as isEmbedRecordView,\n  isViewRecord,\n} from '../src/lexicon/types/app/bsky/embed/record'\nimport { isView as isEmbedRecordWithMediaView } from '../src/lexicon/types/app/bsky/embed/recordWithMedia'\nimport {\n  FeedViewPost,\n  PostView,\n  ThreadViewPost,\n  isPostView,\n  isReasonRepost,\n  isThreadViewPost,\n} from '../src/lexicon/types/app/bsky/feed/defs'\n\nexport const identity = <T>(x: T) => x\n\n// Swap out identifiers and dates with stable\n// values for the purpose of snapshot testing\nexport const forSnapshot = (obj: unknown) => {\n  const records = { [kTake]: 'record' }\n  const collections = { [kTake]: 'collection' }\n  const users = { [kTake]: 'user' }\n  const cids = { [kTake]: 'cids' }\n  const sigs = { [kTake]: 'sig' }\n  const unknown = { [kTake]: 'unknown' }\n  const toWalk = lexToJson(obj as any) // remove any blobrefs/cids\n  return mapLeafValues(toWalk, (item) => {\n    const asCid = CID.asCID(item)\n    if (asCid !== null) {\n      return take(cids, asCid.toString())\n    }\n    if (typeof item !== 'string') {\n      return item\n    }\n    const str = item.startsWith('did:plc:') ? `at://${item}` : item\n    if (str.startsWith('at://')) {\n      const uri = new AtUri(str)\n      if (uri.rkey) {\n        return take(records, str)\n      }\n      if (uri.collection) {\n        return take(collections, str)\n      }\n      if (uri.hostname) {\n        return take(users, str)\n      }\n      return take(unknown, str)\n    }\n    if (str.match(/^\\d{4}-\\d{2}-\\d{2}T/)) {\n      if (str.match(/\\d{6}Z$/)) {\n        return constantDate.replace('Z', '000Z') // e.g. microseconds in record createdAt\n      } else if (str.endsWith('+00:00')) {\n        return constantDate.replace('Z', '+00:00') // e.g. timezone in record createdAt\n      } else {\n        return constantDate\n      }\n    }\n    if (str.match(/^\\d+::bafy/)) {\n      return constantKeysetCursor\n    }\n    if (str.match(/\\/img\\/[^/]+\\/.+\\/did:plc:[^/]+\\/[^/@]+(?:@[\\w]+)?$/)) {\n      // Match image urls, stripping optional format suffix (e.g. @webp) for stable snapshots\n      const match = str.match(\n        /\\/img\\/[^/]+\\/.+\\/(did:plc:[^/]+)\\/([^/@]+)(?:@[\\w]+)?$/,\n      )\n      if (!match) return str\n      const [, did, cid] = match\n      return str\n        .replace(did, take(users, did))\n        .replace(new RegExp(`${cid}(?:@\\\\w+)?`), take(cids, cid))\n    }\n    // decent check for 64-byte base64 encoded signatures\n    if (str.length === 86 && !str.includes(' ')) {\n      return take(sigs, str)\n    }\n    let isCid: boolean\n    try {\n      CID.parse(str)\n      isCid = true\n    } catch (_err) {\n      isCid = false\n    }\n    if (isCid) {\n      return take(cids, str)\n    }\n    return item\n  })\n}\n\n// Feed testing utils\n\nexport const getOriginator = (item: FeedViewPost) => {\n  if (isReasonRepost(item.reason)) {\n    return item.reason.by.did\n  } else {\n    return item.post.author.did\n  }\n}\n\n// Useful for remapping ids in snapshot testing, to make snapshots deterministic.\n// E.g. you may use this to map this:\n//   [{ uri: 'did://rad'}, { uri: 'did://bad' }, { uri: 'did://rad'}]\n// to this:\n//   [{ uri: '0'}, { uri: '1' }, { uri: '0'}]\nconst kTake = Symbol('take')\nexport function take(obj, value: string): string\nexport function take(obj, value: string | undefined): string | undefined\nexport function take(\n  obj: { [s: string]: number; [kTake]?: string },\n  value: string | undefined,\n): string | undefined {\n  if (value === undefined) {\n    return\n  }\n  if (!(value in obj)) {\n    obj[value] = Object.keys(obj).length\n  }\n  const kind = obj[kTake]\n  return typeof kind === 'string'\n    ? `${kind}(${obj[value]})`\n    : String(obj[value])\n}\n\nexport const constantDate = new Date(0).toISOString()\nexport const constantKeysetCursor = '0000000000000::bafycid'\n\nconst mapLeafValues = (obj: unknown, fn: (val: unknown) => unknown) => {\n  if (Array.isArray(obj)) {\n    return obj.map((item) => mapLeafValues(item, fn))\n  }\n  if (obj && typeof obj === 'object') {\n    return Object.entries(obj).reduce(\n      (collect, [name, value]) =>\n        Object.assign(collect, { [name]: mapLeafValues(value, fn) }),\n      {},\n    )\n  }\n  return fn(obj)\n}\n\nexport const paginateAll = async <T extends { cursor?: string }>(\n  fn: (cursor?: string) => Promise<T>,\n  limit = Infinity,\n): Promise<T[]> => {\n  const results: T[] = []\n  let cursor\n  do {\n    const res = await fn(cursor)\n    results.push(res)\n    cursor = res.cursor\n  } while (cursor && results.length < limit)\n  return results\n}\n\n// @NOTE mutates\nexport const stripViewer = <T extends { viewer?: unknown }>(val: T): T => {\n  delete val.viewer\n  return val\n}\n\nconst extractRecordEmbed = (embed: PostView['embed']) =>\n  isEmbedRecordView(embed)\n    ? isViewRecord(embed.record)\n      ? embed.record\n      : undefined\n    : isEmbedRecordWithMediaView(embed)\n      ? isViewRecord(embed.record.record)\n        ? embed.record.record\n        : undefined\n      : undefined\n\n// @NOTE mutates\nexport const stripViewerFromPost = (postUnknown: object): PostView => {\n  if ('$type' in postUnknown && !isPostView(postUnknown)) {\n    throw new Error('Expected post view')\n  }\n  const post = postUnknown as PostView\n  post.author = stripViewer(post.author)\n\n  const recordEmbed = extractRecordEmbed(post.embed)\n  if (recordEmbed) {\n    recordEmbed.author = stripViewer(recordEmbed.author)\n    recordEmbed.embeds?.forEach((deepEmbed) => {\n      const deepRecordEmbed = extractRecordEmbed(deepEmbed)\n      if (deepRecordEmbed) {\n        deepRecordEmbed.author = stripViewer(deepRecordEmbed.author)\n      }\n    })\n  }\n  return stripViewer(post)\n}\n\n// @NOTE mutates\nexport const stripViewerFromThread = <T extends ThreadViewPost>(\n  thread: T,\n): Omit<T, 'viewer'> => {\n  if (!isThreadViewPost(thread)) return thread\n  // @ts-expect-error\n  delete thread.viewer\n  thread.post = stripViewerFromPost(thread.post)\n  if (isThreadViewPost(thread.parent)) {\n    thread.parent = stripViewerFromThread(thread.parent)\n  }\n  if (thread.replies) {\n    thread.replies = thread.replies.map((r) =>\n      isThreadViewPost(r) ? stripViewerFromThread(r) : r,\n    )\n  }\n  return thread\n}\n\nexport async function startServer(app: Express) {\n  return new Promise<{\n    origin: string\n    server: Server\n    stop: () => Promise<void>\n  }>((resolve, reject) => {\n    const onListen = () => {\n      const port = (server.address() as AddressInfo).port\n      resolve({\n        server,\n        origin: `http://localhost:${port}`,\n        stop: () => stopServer(server),\n      })\n      cleanup()\n    }\n    const onError = (err: Error) => {\n      reject(err)\n      cleanup()\n    }\n    const cleanup = () => {\n      server.removeListener('listening', onListen)\n      server.removeListener('error', onError)\n    }\n\n    const server = app\n      .listen(0)\n      .once('listening', onListen)\n      .once('error', onError)\n  })\n}\n\nexport async function stopServer(server: Server) {\n  return new Promise<void>((resolve, reject) => {\n    server.close((err) => {\n      if (err) {\n        reject(err)\n      } else {\n        resolve()\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "packages/ozone/tests/account-strikes.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { SeverityLevelSettingKey } from '../src/setting/constants'\nimport { forSnapshot } from './_util'\n\nconst strikeConfig = {\n  'sev-0': { strikeCount: 0 },\n  'sev-1': {\n    strikeCount: 1,\n    strikeOnOccurrence: 2,\n    expiresInDays: 365,\n  },\n  'sev-2': { strikeCount: 2, expiresInDays: 365 },\n  'sev-4': { strikeCount: 4, expiresInDays: 0 },\n  'sev-5': { needsTakedown: true },\n}\n\ndescribe('account-strikes', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  const configureSeverityLevels = async () => {\n    // Configure severity level settings\n    await agent.tools.ozone.setting.upsertOption(\n      {\n        scope: 'instance',\n        key: SeverityLevelSettingKey,\n        value: strikeConfig,\n        description: 'Severity level configuration for strike system',\n        managerRole: 'tools.ozone.team.defs#roleAdmin',\n      },\n      {\n        encoding: 'application/json',\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneSettingUpsertOption,\n          'admin',\n        ),\n      },\n    )\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_account_strikes',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n    await configureSeverityLevels()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('tracks strikes and exposes them through queryStatuses and queryEvents', async () => {\n    const aliceSubject = repoSubject(sc.dids.alice)\n    const alicePost = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n    }\n\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: strikeConfig['sev-2'].strikeCount,\n        comment: 'First violation',\n      },\n      subject: alicePost,\n    })\n\n    const alicePost2 = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][1].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][1].ref.cidStr,\n    }\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: strikeConfig['sev-2'].strikeCount,\n        comment: 'Second violation',\n      },\n      subject: alicePost2,\n    })\n\n    const alicePost3 = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][2].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][2].ref.cidStr,\n    }\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-1',\n        strikeCount: 0, // First occurrence - warning\n        policies: ['spam-policy'],\n        comment: 'First sev-1 violation',\n      },\n      subject: alicePost3,\n    })\n\n    // Issue second sev-1 takedown with same policy on another post (second occurrence, should be 1 strike, total 5)\n    const aliceReply = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.replies[sc.dids.alice][0].ref.uriStr,\n      cid: sc.replies[sc.dids.alice][0].ref.cidStr,\n    }\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-1',\n        strikeCount: strikeConfig['sev-1'].strikeCount, // Second occurrence - actual strike\n        policies: ['spam-policy'],\n        comment: 'Second sev-1 violation',\n      },\n      subject: aliceReply,\n    })\n\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-0',\n        strikeCount: strikeConfig['sev-0'].strikeCount,\n        comment: 'Warning only',\n      },\n      subject: aliceSubject,\n    })\n\n    let statusResult = await modClient.queryStatuses({\n      subject: sc.dids.alice,\n    })\n\n    expect(statusResult.subjectStatuses.length).toBeGreaterThan(0)\n    expect(forSnapshot(statusResult.subjectStatuses[0])).toMatchSnapshot()\n    const strikeCountBefore =\n      statusResult.subjectStatuses[0].accountStrike?.activeStrikeCount\n\n    // Reverse one of the takedowns with negative strikeCount\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: -2, // Negative to reverse strikes\n        comment: 'Appeal granted - reversing first takedown',\n      },\n      subject: alicePost,\n    })\n\n    // Verify strikes were reduced (if strikeCount tracking is implemented)\n    statusResult = await modClient.queryStatuses({\n      subject: sc.dids.alice,\n    })\n    const strikeCountAfter =\n      statusResult.subjectStatuses[0].accountStrike?.activeStrikeCount\n    expect(strikeCountAfter).toBe(3)\n    expect(strikeCountAfter).toBeLessThan(strikeCountBefore!)\n\n    const eventsWithStrikes = await modClient.queryEvents({\n      subject: sc.dids.alice,\n      includeAllUserRecords: true,\n      withStrike: true,\n    })\n\n    expect(forSnapshot(eventsWithStrikes.events)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/ack-all-subjects-of-account.test.ts",
    "content": "import { ComAtprotoRepoStrongRef } from '@atproto/api'\nimport {\n  ModeratorClient,\n  RecordRef,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { isRepoRef } from '../src/lexicon/types/com/atproto/admin/defs'\nimport {\n  REASONAPPEAL,\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport {\n  REVIEWCLOSED,\n  REVIEWESCALATED,\n  REVIEWOPEN,\n  SubjectStatusView,\n} from '../src/lexicon/types/tools/ozone/moderation/defs'\n\ndescribe('acknowledge all subjects of account', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  const recordSubject = (ref: RecordRef) => ({\n    $type: 'com.atproto.repo.strongRef',\n    uri: ref.uriStr,\n    cid: ref.cidStr,\n  })\n\n  const getReviewStateBySubject = (subjects: SubjectStatusView[]) => {\n    const states = new Map<string, SubjectStatusView>()\n\n    subjects.forEach((item) => {\n      if (ComAtprotoRepoStrongRef.isMain(item.subject)) {\n        states.set(item.subject.uri, item)\n      } else if (isRepoRef(item.subject)) {\n        states.set(item.subject.did, item)\n      }\n    })\n\n    return states\n  }\n\n  const reportUserAndPost = async (did: string) => {\n    const postOne = sc.posts[did][0].ref\n    const postTwo = sc.posts[did][1].ref\n    await Promise.all([\n      sc.createReport({\n        reasonType: REASONSPAM,\n        subject: repoSubject(did),\n        reportedBy: sc.dids.carol,\n      }),\n      sc.createReport({\n        reasonType: REASONOTHER,\n        reason: 'defamation',\n        subject: recordSubject(postOne),\n        reportedBy: sc.dids.carol,\n      }),\n      sc.createReport({\n        reasonType: REASONOTHER,\n        reason: 'defamation',\n        subject: recordSubject(postTwo),\n        reportedBy: sc.dids.carol,\n      }),\n    ])\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventReport',\n        reportType: REASONAPPEAL,\n      },\n      subject: recordSubject(postTwo),\n    })\n\n    return { postOne, postTwo }\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_ack_all_subjects_of_account',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('acknowledges all open/escalated review subjects with takedown.', async () => {\n    const { postOne, postTwo } = await reportUserAndPost(sc.dids.bob)\n\n    const { subjectStatuses: statusesBefore } = await modClient.queryStatuses({\n      subject: sc.dids.bob,\n      includeAllUserRecords: true,\n    })\n\n    await modClient.performTakedown({\n      subject: repoSubject(sc.dids.bob),\n      acknowledgeAccountSubjects: true,\n    })\n\n    const { subjectStatuses: statusesAfter } = await modClient.queryStatuses({\n      subject: sc.dids.bob,\n      includeAllUserRecords: true,\n    })\n\n    const reviewStatesBefore = getReviewStateBySubject(statusesBefore)\n    const reviewStatesAfter = getReviewStateBySubject(statusesAfter)\n\n    // Check that review states before were different for different subjects\n    expect(reviewStatesBefore.get(postOne.uriStr)?.reviewState).toBe(REVIEWOPEN)\n    expect(reviewStatesBefore.get(postTwo.uriStr)?.reviewState).toBe(\n      REVIEWESCALATED,\n    )\n    expect(reviewStatesBefore.get(sc.dids.bob)?.reviewState).toBe(REVIEWOPEN)\n\n    // Check that review states after are all closed\n    expect(reviewStatesAfter.get(postOne.uriStr)?.reviewState).toBe(\n      REVIEWCLOSED,\n    )\n    expect(reviewStatesAfter.get(postTwo.uriStr)?.reviewState).toBe(\n      REVIEWCLOSED,\n    )\n    expect(reviewStatesAfter.get(sc.dids.bob)?.reviewState).toBe(REVIEWCLOSED)\n  })\n\n  it('acknowledges all open/escalated review subjects with acknowledge.', async () => {\n    const { postOne, postTwo } = await reportUserAndPost(sc.dids.alice)\n\n    const { subjectStatuses: statusesBefore } = await modClient.queryStatuses({\n      subject: sc.dids.alice,\n      includeAllUserRecords: true,\n    })\n\n    await modClient.emitEvent({\n      subject: repoSubject(sc.dids.alice),\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventAcknowledge',\n        acknowledgeAccountSubjects: true,\n      },\n    })\n\n    const { subjectStatuses: statusesAfter } = await modClient.queryStatuses({\n      subject: sc.dids.alice,\n      includeAllUserRecords: true,\n    })\n\n    const reviewStatesBefore = getReviewStateBySubject(statusesBefore)\n    const reviewStatesAfter = getReviewStateBySubject(statusesAfter)\n\n    // Check that review states before were different for different subjects\n    expect(reviewStatesBefore.get(postOne.uriStr)?.reviewState).toBe(REVIEWOPEN)\n    expect(reviewStatesBefore.get(postTwo.uriStr)?.reviewState).toBe(\n      REVIEWESCALATED,\n    )\n    expect(reviewStatesBefore.get(sc.dids.alice)?.reviewState).toBe(REVIEWOPEN)\n\n    // Check that review states after are all closed\n    expect(reviewStatesAfter.get(postOne.uriStr)?.reviewState).toBe(\n      REVIEWCLOSED,\n    )\n    expect(reviewStatesAfter.get(postTwo.uriStr)?.reviewState).toBe(\n      REVIEWCLOSED,\n    )\n    expect(reviewStatesAfter.get(sc.dids.alice)?.reviewState).toBe(REVIEWCLOSED)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/age-assurance.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { forSnapshot } from './_util'\n\ndescribe('age assurance events', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_age_assurance',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('handles age assurance events from user', async () => {\n    const aliceSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.alice,\n    }\n    const bobSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n\n    const alicePendingEvent = await modClient.emitEvent({\n      subject: aliceSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'pending',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-123',\n        initIp: '123.456.789.012',\n        initUa: 'Mozilla/5.0',\n      },\n    })\n\n    const bobPendingEvent = await modClient.emitEvent({\n      subject: bobSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'pending',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-345',\n        initIp: '234.567.890.123',\n        initUa: 'Mozilla/5.0',\n      },\n    })\n\n    const bobAssuredEvent = await modClient.emitEvent({\n      subject: bobSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'assured',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-345',\n        initIp: '234.567.890.123',\n        initUa: 'Mozilla/5.0',\n        completeIp: '345.678.901.234',\n        completeUa: 'Mozilla/5.0',\n      },\n    })\n\n    expect(forSnapshot(alicePendingEvent)).toMatchSnapshot()\n    expect(forSnapshot(bobPendingEvent)).toMatchSnapshot()\n    expect(forSnapshot(bobAssuredEvent)).toMatchSnapshot()\n\n    // Verify that age assurance state is correctly set for each subject\n    const [{ subjectStatuses: aliceStatus }, { subjectStatuses: bobStatus }] =\n      await Promise.all([\n        modClient.queryStatuses({\n          subject: sc.dids.alice,\n        }),\n        modClient.queryStatuses({\n          subject: sc.dids.bob,\n        }),\n      ])\n\n    expect(aliceStatus[0].ageAssuranceState).toBe('pending')\n    expect(bobStatus[0].ageAssuranceState).toBe('assured')\n\n    // Verify that queryEvents allow filtering by ageAssuranceState\n    try {\n      const [{ events: pendingEvents }, { events: unknownEvents }] =\n        await Promise.all([\n          modClient.queryEvents({\n            ageAssuranceState: 'pending',\n          }),\n          modClient.queryEvents({\n            ageAssuranceState: 'assured',\n          }),\n        ])\n      expect(pendingEvents.length).toEqual(2)\n      pendingEvents.forEach((event) => {\n        expect(event.event.$type).toBe(\n          'tools.ozone.moderation.defs#ageAssuranceEvent',\n        )\n        expect(event.event['status']).toBe('pending')\n      })\n\n      expect(unknownEvents.length).toBeGreaterThan(0)\n      unknownEvents.forEach((event) => {\n        expect(event.event.$type).toBe(\n          'tools.ozone.moderation.defs#ageAssuranceEvent',\n        )\n        expect(event.event['status']).toBe('assured')\n      })\n    } catch (error) {\n      console.error('Error querying events:', error)\n      throw error\n    }\n\n    // Verify that queryStatuses allows filtering by ageAssuranceState\n    const { subjectStatuses: pendingStatuses } = await modClient.queryStatuses({\n      ageAssuranceState: 'pending',\n    })\n    expect(pendingStatuses.length).toEqual(1)\n    pendingStatuses.forEach((status) => {\n      expect(status.ageAssuranceState).toBe('pending')\n    })\n  })\n\n  it('purge event removes ageAssuranceEvents but keeps overrides, and resets status', async () => {\n    const danSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.dan,\n    }\n\n    await modClient.emitEvent({\n      subject: danSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'pending',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-dan-1',\n      },\n    })\n    await modClient.emitEvent({\n      subject: danSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n        status: 'assured',\n        comment: 'Admin verified dan',\n      },\n    })\n\n    const { subjectStatuses: beforePurge } = await modClient.queryStatuses({\n      subject: sc.dids.dan,\n    })\n    expect(beforePurge[0].ageAssuranceState).toBe('assured')\n    expect(beforePurge[0].ageAssuranceUpdatedBy).toBe('admin')\n\n    const { events: beforePurgeEvents } = await modClient.queryEvents({\n      subject: sc.dids.dan,\n      types: [\n        'tools.ozone.moderation.defs#ageAssuranceEvent',\n        'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n      ],\n    })\n    expect(beforePurgeEvents.length).toBe(2)\n\n    // Emit the purge event\n    const purgeEvent = await modClient.emitEvent({\n      subject: danSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n        comment: 'Purging age assurance data per user request',\n      },\n    })\n    expect(purgeEvent.event.$type).toBe(\n      'tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n    )\n\n    // ageAssuranceEvent rows should be deleted\n    const { events: aaEventsAfterPurge } = await modClient.queryEvents({\n      subject: sc.dids.dan,\n      types: ['tools.ozone.moderation.defs#ageAssuranceEvent'],\n    })\n    expect(aaEventsAfterPurge.length).toBe(0)\n\n    // ageAssuranceOverrideEvent rows should be preserved\n    const { events: overrideEventsAfterPurge } = await modClient.queryEvents({\n      subject: sc.dids.dan,\n      types: ['tools.ozone.moderation.defs#ageAssuranceOverrideEvent'],\n    })\n    expect(overrideEventsAfterPurge.length).toBe(1)\n\n    // Status should be reset to unknown and updatedBy set to the purging moderator's DID\n    const { subjectStatuses: afterPurge } = await modClient.queryStatuses({\n      subject: sc.dids.dan,\n    })\n    expect(afterPurge[0].ageAssuranceState).toBe('unknown')\n    expect(afterPurge[0].ageAssuranceUpdatedBy).toBeFalsy()\n  })\n\n  it('purge event fails for record subjects', async () => {\n    const postSubject = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n    }\n\n    await expect(\n      modClient.emitEvent({\n        subject: postSubject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n          comment: 'Should fail',\n        },\n      }),\n    ).rejects.toThrow('Invalid subject type')\n  })\n\n  it('purge event only removes ageAssuranceEvents, not overrides or other events', async () => {\n    const carolSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.carol,\n    }\n\n    // Add a non-AA event that should survive the purge\n    const commentEvent = await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventComment',\n        comment: 'A non-AA comment that should survive purge',\n      },\n    })\n\n    // Add an ageAssuranceEvent that should be removed\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'pending',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-carol-purge-1',\n      },\n    })\n\n    // Add an ageAssuranceOverrideEvent that should survive the purge\n    const overrideEvent = await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n        status: 'assured',\n        comment: 'Override that should survive purge',\n      },\n    })\n\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n        comment: 'Purging carol age assurance data',\n      },\n    })\n\n    const { events: afterPurge } = await modClient.queryEvents({\n      subject: sc.dids.carol,\n    })\n\n    const aaEventsAfterPurge = afterPurge.filter(\n      (e) => e.event.$type === 'tools.ozone.moderation.defs#ageAssuranceEvent',\n    )\n    expect(aaEventsAfterPurge.length).toBe(0)\n    expect(afterPurge.some((e) => e.id === overrideEvent.id)).toBe(true)\n    expect(afterPurge.some((e) => e.id === commentEvent.id)).toBe(true)\n  })\n\n  it('admin override behavior for age assurance states', async () => {\n    const carolSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.carol,\n    }\n\n    // Verify that user emitted state is overridden by admin emitted state\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'pending',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-carol-1',\n      },\n    })\n\n    const { subjectStatuses } = await modClient.queryStatuses({\n      subject: sc.dids.carol,\n    })\n    expect(subjectStatuses[0].ageAssuranceState).toBe('pending')\n    expect(subjectStatuses[0].ageAssuranceUpdatedBy).toBe('user')\n\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n        status: 'assured',\n        comment: 'Admin verification completed',\n      },\n    })\n\n    const { subjectStatuses: afterAdminAssurance } =\n      await modClient.queryStatuses({\n        subject: sc.dids.carol,\n      })\n    expect(afterAdminAssurance[0].ageAssuranceState).toBe('assured')\n    expect(afterAdminAssurance[0].ageAssuranceUpdatedBy).toBe('admin')\n\n    // Verify that user emitted state can not override admin emitted state\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'pending',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-carol-2',\n      },\n    })\n\n    const { subjectStatuses: afterCarolsAttempt } =\n      await modClient.queryStatuses({\n        subject: sc.dids.carol,\n      })\n    expect(afterCarolsAttempt[0].ageAssuranceState).toBe('assured')\n    expect(afterCarolsAttempt[0].ageAssuranceUpdatedBy).toBe('admin')\n\n    // Verify that admin can reset state to allow the user to override\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n        status: 'reset',\n        comment: 'Reset to allow user to set state again',\n      },\n    })\n\n    const { subjectStatuses: afterReset } = await modClient.queryStatuses({\n      subject: sc.dids.carol,\n    })\n    expect(afterReset[0].ageAssuranceState).toBe('reset')\n    expect(afterReset[0].ageAssuranceUpdatedBy).toBe('admin')\n\n    await modClient.emitEvent({\n      subject: carolSubject,\n      event: {\n        $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n        status: 'assured',\n        createdAt: new Date().toISOString(),\n        attemptId: 'attempt-carol-3',\n      },\n    })\n\n    const { subjectStatuses: afterCarolAssured } =\n      await modClient.queryStatuses({\n        subject: sc.dids.carol,\n      })\n    expect(afterCarolAssured[0].ageAssuranceState).toBe('assured')\n    expect(afterCarolAssured[0].ageAssuranceUpdatedBy).toBe('user')\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/blob-divert.test.ts",
    "content": "import assert from 'node:assert'\nimport { ToolsOzoneModerationDefs } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ResponseType, XRPCError } from '@atproto/xrpc'\nimport { forSnapshot, identity } from './_util'\n\ndescribe('blob divert', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_blob_divert_test',\n      ozone: {\n        blobDivertUrl: `https://blob-report.com`,\n        blobDivertAdminPassword: 'test-auth-token',\n      },\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const mockReportServiceResponse = (succeeds: boolean) => {\n    const blobDiverter = network.ozone.ctx.blobDiverter\n    assert(blobDiverter)\n    return jest\n      .spyOn(blobDiverter, 'uploadBlob')\n      .mockImplementation(async () => {\n        if (!succeeds) {\n          // Using an XRPCError to trigger retries\n          throw new XRPCError(ResponseType.Unknown, undefined)\n        }\n      })\n  }\n\n  const getSubject = () => ({\n    $type: 'com.atproto.repo.strongRef',\n    uri: sc.posts[sc.dids.carol][0].ref.uriStr,\n    cid: sc.posts[sc.dids.carol][0].ref.cidStr,\n  })\n\n  const getImages = () => sc.posts[sc.dids.carol][0].images\n\n  const emitDivertEvent = async () =>\n    modClient.emitEvent(\n      {\n        subject: getSubject(),\n        // @ts-expect-error \"tools.ozone.moderation.defs#modEventDivert\" is not part of the event open union\n        event: identity<ToolsOzoneModerationDefs.ModEventDivert>({\n          $type: 'tools.ozone.moderation.defs#modEventDivert',\n          comment: 'Diverting for test',\n        }),\n        createdBy: sc.dids.alice,\n        subjectBlobCids: getImages().map((img) => img.image.ref.toString()),\n      },\n      'moderator',\n    )\n\n  it('fails and keeps attempt count when report service fails to accept upload.', async () => {\n    // Simulate failure to fail upload\n    const reportServiceRequest = mockReportServiceResponse(false)\n    try {\n      await expect(emitDivertEvent()).rejects.toThrow('Failed to process blobs')\n\n      // 1 initial attempt + 3 retries\n      expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length * 4)\n    } finally {\n      reportServiceRequest.mockRestore()\n    }\n  })\n\n  it('sends blobs to configured divert service and marks divert date', async () => {\n    // Simulate success to accept upload\n    const reportServiceRequest = mockReportServiceResponse(true)\n    try {\n      const divertEvent = await emitDivertEvent()\n\n      expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length)\n      expect(forSnapshot(divertEvent)).toMatchSnapshot()\n\n      const { subjectStatuses } = await modClient.queryStatuses({\n        subject: getSubject().uri,\n      })\n\n      expect(subjectStatuses[0].takendown).toBe(true)\n\n      const event = await modClient.getEvent(divertEvent.id)\n      expect(forSnapshot(event)).toMatchSnapshot()\n    } finally {\n      reportServiceRequest.mockRestore()\n    }\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/communication-templates.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\n\ndescribe('communication-templates', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_communication_templates',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const templateOne = {\n    name: 'Test name 1',\n    subject: 'Test subject 1',\n    contentMarkdown: 'Test content 1',\n  }\n\n  const listTemplates = async () => {\n    const { data } = await agent.tools.ozone.communication.listTemplates(\n      {},\n      {\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneCommunicationListTemplates,\n          'moderator',\n        ),\n      },\n    )\n    return data.communicationTemplates\n  }\n\n  describe('create templates', () => {\n    it('only allows admins to create new templates', async () => {\n      const moderatorReq = agent.tools.ozone.communication.createTemplate(\n        { ...templateOne, createdBy: sc.dids.bob },\n        {\n          encoding: 'application/json',\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneCommunicationCreateTemplate,\n            'triage',\n          ),\n        },\n      )\n      await expect(moderatorReq).rejects.toThrow(\n        'Must be a moderator to create a communication template',\n      )\n      const modReq = await agent.tools.ozone.communication.createTemplate(\n        { ...templateOne, createdBy: sc.dids.bob },\n        {\n          encoding: 'application/json',\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneCommunicationCreateTemplate,\n            'admin',\n          ),\n        },\n      )\n\n      expect(modReq.data).toMatchObject({\n        ...templateOne,\n        lastUpdatedBy: sc.dids.bob,\n      })\n    })\n  })\n  describe('list templates', () => {\n    it('returns all saved templates', async () => {\n      const listBefore = await listTemplates()\n      expect(listBefore.length).toEqual(1)\n      expect(listBefore[0]).toMatchObject(templateOne)\n\n      const templateTwo = {\n        ...templateOne,\n        name: 'Test template 2',\n      }\n      await agent.tools.ozone.communication.createTemplate(\n        { ...templateTwo, createdBy: sc.dids.bob },\n        {\n          encoding: 'application/json',\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneCommunicationCreateTemplate,\n            'admin',\n          ),\n        },\n      )\n\n      const listAfter = await listTemplates()\n      expect(listAfter.length).toEqual(2)\n      expect(listAfter[1]).toMatchObject(templateTwo)\n    })\n  })\n  describe('update template', () => {\n    it('allows moderators to update a template by id', async () => {\n      const { data } = await agent.tools.ozone.communication.updateTemplate(\n        { id: '1', updatedBy: sc.dids.bob, name: '1 Test template' },\n        {\n          encoding: 'application/json',\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneCommunicationUpdateTemplate,\n            'admin',\n          ),\n        },\n      )\n\n      expect(data.name).not.toEqual(templateOne.name)\n      expect(data.name).toEqual('1 Test template')\n    })\n  })\n  describe('delete template', () => {\n    it('allows admins to remove a template by id', async () => {\n      const modReq = agent.tools.ozone.communication.deleteTemplate(\n        { id: '1' },\n        {\n          encoding: 'application/json',\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneCommunicationDeleteTemplate,\n            'triage',\n          ),\n        },\n      )\n\n      await expect(modReq).rejects.toThrow(\n        'Must be a moderator to delete a communication template',\n      )\n\n      await agent.tools.ozone.communication.deleteTemplate(\n        { id: '1' },\n        {\n          encoding: 'application/json',\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneCommunicationDeleteTemplate,\n            'moderator',\n          ),\n        },\n      )\n      const list = await listTemplates()\n      expect(list.length).toEqual(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/content-tagger.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { REASONMISLEADING } from '../dist/lexicon/types/com/atproto/moderation/defs'\nimport { REASONSPAM } from '../src/lexicon/types/com/atproto/moderation/defs'\n\ndescribe('moderation subject content tagging', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_content_tagger_test',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getStatus = async (subject: string) => {\n    const { subjectStatuses } = await modClient.queryStatuses({\n      subject,\n    })\n\n    return subjectStatuses[0]\n  }\n\n  describe('lang tagger', () => {\n    it('Adds language tag to post from text', async () => {\n      const createPostAndReport = async (text: string) => {\n        const post = await sc.post(sc.dids.carol, text)\n        await network.processAll()\n        const report = await sc.createReport({\n          reasonType: REASONSPAM,\n          subject: {\n            $type: 'com.atproto.repo.strongRef',\n            uri: post.ref.uriStr,\n            cid: post.ref.cidStr,\n          },\n          reportedBy: sc.dids.alice,\n        })\n\n        return { post, report }\n      }\n      const [japanesePost, greekPost] = await Promise.all([\n        createPostAndReport('Xで有名な人達＋反AIや絵描きによくない'),\n        createPostAndReport(\n          'Λορεμ ιπσθμ δολορ σιτ αμετ, μει θτ vιδιτ νοστρθμ προπριαε',\n        ),\n      ])\n\n      const [japanesePostStatus, greekPostStatus] = await Promise.all([\n        getStatus(japanesePost.post.ref.uriStr),\n        getStatus(greekPost.post.ref.uriStr),\n      ])\n\n      expect(japanesePostStatus.tags).toContain('lang:ja')\n      expect(greekPostStatus.tags).toContain('lang:el')\n    })\n\n    it('Uses name/description text for language tag for list', async () => {\n      const createListAndReport = async (\n        name: string,\n        description?: string,\n      ) => {\n        const list = await sc.createList(sc.dids.carol, name, 'mod', {\n          description,\n        })\n        await network.processAll()\n        const report = await sc.createReport({\n          reasonType: REASONSPAM,\n          subject: {\n            $type: 'com.atproto.repo.strongRef',\n            uri: list.uriStr,\n            cid: list.cidStr,\n          },\n          reportedBy: sc.dids.alice,\n        })\n        return { list, report }\n      }\n\n      const [listWithDescription, listWithoutDescription] = await Promise.all([\n        createListAndReport(\n          'よくない',\n          'Xで有名な人達＋反AIや絵描きによくない感情を持つ人達＋絵描き詐称',\n        ),\n        createListAndReport('人達＋反AIや絵描きによくない感情'),\n      ])\n\n      const [japaneseListStatus, chineseListStatus] = await Promise.all([\n        getStatus(listWithDescription.list.uriStr),\n        getStatus(listWithoutDescription.list.uriStr),\n      ])\n\n      expect(japaneseListStatus.tags).toContain('lang:ja')\n      expect(chineseListStatus.tags).toContain('lang:ja')\n    })\n  })\n\n  describe('embed tagger', () => {\n    it('Adds image tag to post with image', async () => {\n      const postWithImageMediaEmbed = sc.posts[sc.dids.carol][0]\n      const postWithImageEmbed = sc.replies[sc.dids.bob][0]\n      await Promise.all([\n        sc.createReport({\n          reasonType: REASONSPAM,\n          subject: {\n            $type: 'com.atproto.repo.strongRef',\n            uri: postWithImageMediaEmbed.ref.uriStr,\n            cid: postWithImageMediaEmbed.ref.cidStr,\n          },\n          reportedBy: sc.dids.alice,\n        }),\n        sc.createReport({\n          reasonType: REASONSPAM,\n          subject: {\n            $type: 'com.atproto.repo.strongRef',\n            uri: postWithImageEmbed.ref.uriStr,\n            cid: postWithImageEmbed.ref.cidStr,\n          },\n          reportedBy: sc.dids.alice,\n        }),\n      ])\n\n      const [mediaImagePostStatus, imagePostStatus] = await Promise.all([\n        getStatus(postWithImageMediaEmbed.ref.uriStr),\n        getStatus(postWithImageEmbed.ref.uriStr),\n      ])\n      expect(mediaImagePostStatus.tags).toContain('embed:image')\n      expect(imagePostStatus.tags).toContain('embed:image')\n    })\n  })\n\n  describe('report tagger', () => {\n    it('Adds report reason tag', async () => {\n      await Promise.all([\n        sc.createReport({\n          reasonType: REASONSPAM,\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.carol,\n          },\n          reportedBy: sc.dids.alice,\n        }),\n        sc.createReport({\n          reasonType: REASONMISLEADING,\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.carol,\n          },\n          reportedBy: sc.dids.alice,\n        }),\n      ])\n\n      const accountStatus = await getStatus(sc.dids.carol)\n\n      expect(accountStatus.tags).toContain('report:spam')\n      expect(accountStatus.tags).toContain('report:misleading')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/db.test.ts",
    "content": "import { sql } from 'kysely'\nimport { wait } from '@atproto/common'\nimport { TestNetwork } from '@atproto/dev-env'\nimport { Database } from '../src'\n\ndescribe('db', () => {\n  let network: TestNetwork\n  let db: Database\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_db',\n    })\n    db = network.ozone.ctx.db\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('handles client errors without crashing.', async () => {\n    const tryKillConnection = db.transaction(async (dbTxn) => {\n      const result = await sql`select pg_backend_pid() as pid;`.execute(\n        dbTxn.db,\n      )\n      const pid = result.rows[0]?.['pid'] as number\n      await sql`select pg_terminate_backend(${pid});`.execute(db.db)\n      await sql`select 1;`.execute(dbTxn.db)\n    })\n    // This should throw, but no unhandled error\n    await expect(tryKillConnection).rejects.toThrow()\n  })\n\n  it('handles pool errors without crashing.', async () => {\n    const conn1 = await db.pool.connect()\n    const conn2 = await db.pool.connect()\n    const result = await conn1.query('select pg_backend_pid() as pid;')\n    const conn1pid: number = result.rows[0].pid\n    conn1.release()\n    await wait(100) // let release apply, conn is now idle on pool.\n    await conn2.query(`select pg_terminate_backend(${conn1pid});`)\n    conn2.release()\n  })\n\n  describe('transaction()', () => {\n    it('commits changes', async () => {\n      const result = await db.transaction(async (dbTxn) => {\n        return await dbTxn.db\n          .insertInto('repo_push_event')\n          .values({\n            eventType: 'pds_takedown',\n            subjectDid: 'x',\n          })\n          .returning('subjectDid')\n          .executeTakeFirst()\n      })\n\n      if (!result) {\n        return expect(result).toBeTruthy()\n      }\n\n      expect(result.subjectDid).toEqual('x')\n\n      const row = await db.db\n        .selectFrom('repo_push_event')\n        .selectAll()\n        .where('subjectDid', '=', 'x')\n        .executeTakeFirst()\n\n      expect(row).toMatchObject({\n        eventType: 'pds_takedown',\n        subjectDid: 'x',\n      })\n    })\n\n    it('rolls-back changes on failure', async () => {\n      const promise = db.transaction(async (dbTxn) => {\n        await dbTxn.db\n          .insertInto('repo_push_event')\n          .values({\n            eventType: 'pds_takedown',\n            subjectDid: 'y',\n          })\n          .returning('subjectDid')\n          .executeTakeFirst()\n\n        throw new Error('Oops!')\n      })\n\n      await expect(promise).rejects.toThrow('Oops!')\n\n      const row = await db.db\n        .selectFrom('repo_push_event')\n        .selectAll()\n        .where('subjectDid', '=', 'y')\n        .executeTakeFirst()\n\n      expect(row).toBeUndefined()\n    })\n\n    it('indicates isTransaction', async () => {\n      expect(db.isTransaction).toEqual(false)\n\n      await db.transaction(async (dbTxn) => {\n        expect(db.isTransaction).toEqual(false)\n        expect(dbTxn.isTransaction).toEqual(true)\n      })\n\n      expect(db.isTransaction).toEqual(false)\n    })\n\n    it('asserts transaction', async () => {\n      expect(() => db.assertTransaction()).toThrow('Transaction required')\n\n      await db.transaction(async (dbTxn) => {\n        expect(() => dbTxn.assertTransaction()).not.toThrow()\n      })\n    })\n\n    it('does not allow leaky transactions', async () => {\n      let leakedTx: Database | undefined\n\n      const tx = db.transaction(async (dbTxn) => {\n        leakedTx = dbTxn\n        await dbTxn.db\n          .insertInto('repo_push_event')\n          .values({ eventType: 'pds_takedown', subjectDid: 'a' })\n          .execute()\n        throw new Error('test tx failed')\n      })\n      await expect(tx).rejects.toThrow('test tx failed')\n\n      const attempt = leakedTx?.db\n        .insertInto('repo_push_event')\n        .values({ eventType: 'pds_takedown', subjectDid: 'b' })\n        .execute()\n      await expect(attempt).rejects.toThrow('tx already failed')\n\n      const res = await db.db\n        .selectFrom('repo_push_event')\n        .selectAll()\n        .where('subjectDid', 'in', ['a', 'b'])\n        .execute()\n\n      expect(res.length).toBe(0)\n    })\n\n    it('ensures all inflight queries are rolled back', async () => {\n      let promise: Promise<unknown> | undefined = undefined\n      const names: string[] = []\n      try {\n        await db.transaction(async (dbTxn) => {\n          const queries: Promise<unknown>[] = []\n          for (let i = 0; i < 20; i++) {\n            const name = `user${i}`\n            const query = dbTxn.db\n              .insertInto('repo_push_event')\n              .values({\n                eventType: 'pds_takedown',\n                subjectDid: name,\n              })\n              .execute()\n            names.push(name)\n            queries.push(query)\n          }\n          promise = Promise.allSettled(queries)\n          throw new Error()\n        })\n      } catch (err) {\n        expect(err).toBeDefined()\n      }\n      if (promise) {\n        await promise\n      }\n\n      const res = await db.db\n        .selectFrom('repo_push_event')\n        .selectAll()\n        .where('subjectDid', 'in', names)\n        .execute()\n      expect(res.length).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/expiring-label.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\n\ndescribe('expiring label', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  let agent: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_expiring_label_test',\n    })\n    sc = network.getSeedClient()\n    agent = network.ozone.getClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const emitExpiringLabel = async (did: string) =>\n    modClient.emitEvent(\n      {\n        subject: { $type: 'com.atproto.admin.defs#repoRef', did },\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventLabel',\n          comment: 'Testing expiring label',\n          createLabelVals: ['expiring'],\n          negateLabelVals: [],\n          durationInHours: 1,\n        },\n        createdBy: sc.dids.alice,\n      },\n      'moderator',\n    )\n\n  it('Returns expiring label only within expiration period', async () => {\n    const getRepo = async (did: string) =>\n      agent.tools.ozone.moderation.getRepo(\n        { did },\n        {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneModerationGetRepo,\n          ),\n        },\n      )\n\n    const now = new Date().toISOString()\n    await emitExpiringLabel(sc.dids.carol)\n    const { data: repoWithExpiringLabel } = await getRepo(sc.dids.carol)\n    expect(repoWithExpiringLabel.labels?.[0].val).toEqual('expiring')\n    // Manually expire the label in db\n    await network.ozone.ctx.db.db\n      .updateTable('label')\n      .set({ exp: now })\n      .where('uri', '=', sc.dids.carol)\n      .execute()\n\n    const { data: repoAfterExpiringLabel } = await getRepo(sc.dids.carol)\n    expect(repoAfterExpiringLabel.labels?.length).toEqual(0)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-account-timeline.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { REASONSPAM } from '../dist/lexicon/types/com/atproto/moderation/defs'\nimport { ids } from '../src/lexicon/lexicons'\nimport { forSnapshot } from './_util'\n\ndescribe('account timeline', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  let agent: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_account_timeline_test',\n    })\n    sc = network.getSeedClient()\n    agent = network.ozone.getClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n\n    // Trigger some moderation events\n    await Promise.all([\n      sc.createReport({\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        reasonType: REASONSPAM,\n        reportedBy: sc.dids.bob,\n      }),\n      sc.createReport({\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        reasonType: REASONSPAM,\n        reportedBy: sc.dids.carol,\n      }),\n      sc.createReport({\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n          cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n        },\n        reasonType: REASONSPAM,\n        reportedBy: sc.dids.bob,\n      }),\n    ])\n    await modClient.performTakedown({\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('Returns entire timeline of events for a given account', async () => {\n    const getAccountTimeline = async (did: string) =>\n      agent.tools.ozone.moderation.getAccountTimeline(\n        { did },\n        {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneModerationGetAccountTimeline,\n          ),\n        },\n      )\n\n    const {\n      data: { timeline },\n    } = await getAccountTimeline(sc.dids.alice)\n\n    expect(forSnapshot(timeline[0].summary)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-config.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { TOOLS_OZONE_TEAM } from '../src/lexicon'\nimport { ids } from '../src/lexicon/lexicons'\n\ndescribe('get-config', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_server_config',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getConfig = async (role: 'moderator' | 'admin' | 'triage') => {\n    const { data } = await agent.api.tools.ozone.server.getConfig(\n      {},\n      {\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneServerGetConfig,\n          role,\n        ),\n      },\n    )\n    return data\n  }\n\n  it('returns server config', async () => {\n    const moderatorConfig = await getConfig('moderator')\n    expect(moderatorConfig.appview?.url).toBe(network.ozone.ctx.cfg.appview.url)\n    expect(moderatorConfig.pds?.url).toBe(network.ozone.ctx.cfg.pds?.url)\n    expect(moderatorConfig.blobDivert?.url).toBe(\n      network.ozone.ctx.cfg.blobDivert?.url,\n    )\n    expect(moderatorConfig.chat?.url).toBe(undefined)\n    expect(moderatorConfig.viewer?.role).toEqual(\n      TOOLS_OZONE_TEAM.DefsRoleModerator,\n    )\n  })\n\n  it('returns the right role for the viewer', async () => {\n    const adminConfig = await getConfig('admin')\n    expect(adminConfig.viewer?.role).toBe(TOOLS_OZONE_TEAM.DefsRoleAdmin)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-lists.test.ts",
    "content": "import { AtpAgent, BSKY_LABELER_DID } from '@atproto/api'\nimport {\n  ModeratorClient,\n  RecordRef,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { TAKEDOWN_LABEL } from '../src/mod-service'\n\ndescribe('admin get lists', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let appviewAgent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  let alicesList: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_lists',\n    })\n    ozone = network.ozone\n    agent = ozone.getClient()\n    appviewAgent = network.bsky.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    await basicSeed(sc)\n    alicesList = await sc.createList(sc.dids.alice, \"Alice's List\", 'mod')\n    AtpAgent.configure({ appLabelers: [ozone.ctx.cfg.service.did] })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    AtpAgent.configure({ appLabelers: [BSKY_LABELER_DID] })\n    await network.close()\n  })\n\n  const getAlicesList = async () => {\n    const [{ data: fromOzone }, { data: fromAppview }] = await Promise.all([\n      agent.api.app.bsky.graph.getLists(\n        { actor: sc.dids.alice },\n        { headers: await ozone.modHeaders(ids.AppBskyGraphGetLists) },\n      ),\n      appviewAgent.api.app.bsky.graph.getLists({ actor: sc.dids.alice }),\n    ])\n\n    return { fromOzone, fromAppview }\n  }\n\n  it('returns lists from takendown account', async () => {\n    const beforeTakedown = await getAlicesList()\n    expect(beforeTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)\n    expect(beforeTakedown.fromAppview.lists[0].uri).toEqual(alicesList.uriStr)\n\n    // Takedown alice's account\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await network.processAll()\n\n    const afterTakedown = await getAlicesList()\n\n    // Verify that takendown list is shown when queried through ozone but not through appview\n    expect(afterTakedown.fromAppview.lists.length).toBe(0)\n    expect(afterTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)\n\n    // Reverse alice's account takedown\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventReverseTakedown' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await network.processAll()\n  })\n\n  it('returns takendown lists', async () => {\n    const beforeTakedown = await getAlicesList()\n    expect(beforeTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)\n    expect(beforeTakedown.fromAppview.lists[0].uri).toEqual(alicesList.uriStr)\n\n    // Takedown alice's list using a !takedown label\n    await network.bsky.db.db\n      .insertInto('label')\n      .values({\n        src: ozone.ctx.cfg.service.did,\n        uri: alicesList.uriStr,\n        cid: alicesList.cidStr,\n        val: TAKEDOWN_LABEL,\n        neg: false,\n        cts: new Date().toISOString(),\n      })\n      .execute()\n\n    const afterTakedown = await getAlicesList()\n\n    // Verify that takendown list is shown when queried through ozone but not through appview\n    expect(afterTakedown.fromAppview.lists.length).toBe(0)\n    expect(afterTakedown.fromOzone.lists[0].uri).toEqual(alicesList.uriStr)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-profiles.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\n\ndescribe('get profiles through ozone', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_get_profiles',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('allows getting profiles by dids for takendown accounts.', async () => {\n    const getProfiles = async (actors: string[]) => {\n      const { data } = await modClient.agent.app.bsky.actor.getProfiles(\n        { actors },\n        {\n          headers: await network.ozone.modHeaders(\n            'app.bsky.actor.getProfiles',\n            'admin',\n          ),\n        },\n      )\n\n      return data.profiles\n    }\n    const profilesBefore = await getProfiles([sc.dids.bob, sc.dids.carol])\n\n    await modClient.performTakedown({\n      subject: repoSubject(sc.dids.bob),\n    })\n\n    const profilesAfterFromOzone = await getProfiles([\n      sc.dids.bob,\n      sc.dids.carol,\n    ])\n\n    const appviewAgent = network.bsky.getClient()\n    const {\n      data: { profiles: profilesFromAppview },\n    } = await appviewAgent.app.bsky.actor.getProfiles({\n      actors: [sc.dids.bob, sc.dids.carol],\n    })\n\n    expect(profilesBefore.length).toEqual(profilesAfterFromOzone.length)\n    expect(\n      profilesAfterFromOzone.find((p) => p.did === sc.dids.bob),\n    ).toBeTruthy()\n    expect(profilesFromAppview.find((p) => p.did === sc.dids.bob)).toBeFalsy()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-record.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { AtUri } from '@atproto/syntax'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { forSnapshot } from './_util'\n\ndescribe('admin get record view', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_record',\n    })\n    ozone = network.ozone\n    agent = ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    await sc.createReport({\n      reportedBy: sc.dids.bob,\n      reasonType: REASONSPAM,\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: REASONOTHER,\n      reason: 'defamation',\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: sc.posts[sc.dids.alice][0].ref.uriStr,\n    })\n  })\n\n  it('gets a record by uri, even when taken down.', async () => {\n    const result = await agent.tools.ozone.moderation.getRecord(\n      { uri: sc.posts[sc.dids.alice][0].ref.uriStr },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecord) },\n    )\n    expect(forSnapshot(result.data)).toMatchSnapshot()\n  })\n\n  it('gets a record by uri and cid.', async () => {\n    const result = await agent.tools.ozone.moderation.getRecord(\n      {\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecord) },\n    )\n    expect(forSnapshot(result.data)).toMatchSnapshot()\n  })\n\n  it('fails when record does not exist.', async () => {\n    const promise = agent.tools.ozone.moderation.getRecord(\n      {\n        uri: AtUri.make(\n          sc.dids.alice,\n          'app.bsky.feed.post',\n          'badrkey',\n        ).toString(),\n      },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecord) },\n    )\n    await expect(promise).rejects.toThrow('Could not locate record')\n  })\n\n  it('fails when record cid does not exist.', async () => {\n    const promise = agent.tools.ozone.moderation.getRecord(\n      {\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][1].ref.cidStr, // Mismatching cid\n      },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecord) },\n    )\n    await expect(promise).rejects.toThrow('Could not locate record')\n  })\n\n  it('gets record from pds if appview does not have it.', async () => {\n    const post = await sc.post(sc.dids.carol, 'this is test')\n    const { data: postFromOzone } =\n      await agent.tools.ozone.moderation.getRecord(\n        { uri: post.ref.uriStr },\n        { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecord) },\n      )\n    expect(forSnapshot(postFromOzone)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-records.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { forSnapshot } from './_util'\n\ndescribe('admin get records view', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_records',\n    })\n    ozone = network.ozone\n    agent = ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    await sc.createReport({\n      reportedBy: sc.dids.bob,\n      reasonType: REASONSPAM,\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: REASONOTHER,\n      reason: 'defamation',\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await network.bsky.ctx.dataplane.takedownRecord({\n      recordUri: sc.posts[sc.dids.alice][0].ref.uriStr,\n    })\n  })\n\n  it('get multiple records by uris', async () => {\n    const { data } = await agent.tools.ozone.moderation.getRecords(\n      {\n        uris: [\n          sc.posts[sc.dids.alice][0].ref.uriStr,\n          sc.posts[sc.dids.bob][0].ref.uriStr,\n          //     create a uri for a non-existent collection\n          sc.posts[sc.dids.bob][0].ref.uriStr.replace('.post', '.test'),\n        ],\n      },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRecords) },\n    )\n\n    expect(forSnapshot(data)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-repo.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { forSnapshot } from './_util'\n\ndescribe('admin get repo view', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_repo',\n    })\n    ozone = network.ozone\n    agent = ozone.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    await basicSeed(sc)\n    await pdsAgent.com.atproto.server.deactivateAccount(\n      {},\n      { encoding: 'application/json', headers: sc.getHeaders(sc.dids.dan) },\n    )\n    await network.processAll()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventAcknowledge' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.bob,\n      reasonType: REASONSPAM,\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: REASONOTHER,\n      reason: 'defamation',\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n  })\n\n  it('gets a repo by did, even when taken down.', async () => {\n    const result = await agent.api.tools.ozone.moderation.getRepo(\n      { did: sc.dids.alice },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo) },\n    )\n    expect(forSnapshot(result.data)).toMatchSnapshot()\n  })\n\n  it('does not include account emails for triage mods.', async () => {\n    const { data: admin } = await agent.api.tools.ozone.moderation.getRepo(\n      { did: sc.dids.bob },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo) },\n    )\n    const { data: moderator } = await agent.api.tools.ozone.moderation.getRepo(\n      { did: sc.dids.bob },\n      {\n        headers: await ozone.modHeaders(\n          ids.ToolsOzoneModerationGetRepo,\n          'moderator',\n        ),\n      },\n    )\n    const { data: triage } = await agent.api.tools.ozone.moderation.getRepo(\n      { did: sc.dids.bob },\n      {\n        headers: await ozone.modHeaders(\n          ids.ToolsOzoneModerationGetRepo,\n          'triage',\n        ),\n      },\n    )\n    expect(admin.email).toEqual('bob@test.com')\n    expect(moderator.email).toEqual('bob@test.com')\n    expect(triage.email).toBeUndefined()\n    expect(triage).toEqual({ ...admin, email: undefined })\n  })\n\n  it('includes emailConfirmedAt timestamp', async () => {\n    const { data: beforeEmailVerification } =\n      await agent.api.tools.ozone.moderation.getRepo(\n        { did: sc.dids.bob },\n        { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo) },\n      )\n\n    expect(beforeEmailVerification.emailConfirmedAt).toBeUndefined()\n    const timestampBeforeVerification = Date.now()\n    const bobsAccount = sc.accounts[sc.dids.bob]\n    const verificationToken =\n      await network.pds.ctx.accountManager.createEmailToken(\n        sc.dids.bob,\n        'confirm_email',\n      )\n    await pdsAgent.api.com.atproto.server.confirmEmail(\n      { email: bobsAccount.email, token: verificationToken },\n      {\n        encoding: 'application/json',\n\n        headers: sc.getHeaders(sc.dids.bob),\n      },\n    )\n    const { data: afterEmailVerification } =\n      await agent.api.tools.ozone.moderation.getRepo(\n        { did: sc.dids.bob },\n        { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo) },\n      )\n\n    expect(afterEmailVerification.emailConfirmedAt).toBeTruthy()\n    expect(\n      new Date(afterEmailVerification.emailConfirmedAt as string).getTime(),\n    ).toBeGreaterThan(timestampBeforeVerification)\n  })\n\n  it('returns deactivation state', async () => {\n    const res = await agent.api.tools.ozone.moderation.getRepo(\n      { did: sc.dids.dan },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo) },\n    )\n\n    expect(res.data.deactivatedAt).toBeDefined()\n  })\n\n  it('fails when repo does not exist.', async () => {\n    const promise = agent.api.tools.ozone.moderation.getRepo(\n      { did: 'did:plc:doesnotexist' },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepo) },\n    )\n    await expect(promise).rejects.toThrow('Repo not found')\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-reporter-stats.test.ts",
    "content": "import {\n  ComAtprotoModerationDefs,\n  ToolsOzoneModerationDefs,\n} from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\n\ndescribe('reporter-stats', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_reporter_stats',\n      ozone: {\n        dbMaterializedViewRefreshIntervalMs: 1000,\n      },\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getReporterStats = async (\n    did: string,\n  ): Promise<ToolsOzoneModerationDefs.ReporterStats | undefined> => {\n    const { stats } = await modClient.getReporterStats([did])\n    return stats[0]\n  }\n\n  it('updates reporter stats based on actions', async () => {\n    const bobsPostSubject1 = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.bob][0].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][0].ref.cidStr,\n    }\n    const bobsPostSubject2 = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.bob][1].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][1].ref.cidStr,\n    }\n    const carolsAccountSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.carol,\n    }\n\n    await Promise.all([\n      sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: ComAtprotoModerationDefs.REASONMISLEADING,\n        reason: 'misleading',\n        subject: bobsPostSubject1,\n      }),\n      sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: ComAtprotoModerationDefs.REASONOTHER,\n        reason: 'test',\n        subject: bobsPostSubject1,\n      }),\n      sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: ComAtprotoModerationDefs.REASONOTHER,\n        reason: 'test',\n        subject: bobsPostSubject2,\n      }),\n      sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: ComAtprotoModerationDefs.REASONMISLEADING,\n        reason: 'misleading',\n        subject: carolsAccountSubject,\n      }),\n    ])\n\n    await network.processAll()\n    const statsAfterReport = await getReporterStats(sc.dids.alice)\n    expect(statsAfterReport).toMatchObject({\n      did: sc.dids.alice,\n      accountReportCount: 1,\n      recordReportCount: 3,\n      reportedAccountCount: 1,\n      reportedRecordCount: 2,\n      takendownAccountCount: 0,\n      takendownRecordCount: 0,\n      labeledAccountCount: 0,\n      labeledRecordCount: 0,\n    })\n\n    await Promise.all([\n      modClient.performTakedown({\n        subject: bobsPostSubject1,\n        policies: ['trolling'],\n      }),\n      modClient.performTakedown({\n        subject: bobsPostSubject2,\n        policies: ['trolling'],\n      }),\n      modClient.emitEvent({\n        subject: carolsAccountSubject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventLabel',\n          createLabelVals: ['spam'],\n          negateLabelVals: [],\n        },\n      }),\n    ])\n\n    await network.processAll()\n    await new Promise((resolve) => setTimeout(resolve, 1000))\n    const statsAfterAction = await getReporterStats(sc.dids.alice)\n    expect(statsAfterAction).toMatchObject({\n      did: sc.dids.alice,\n      accountReportCount: 1,\n      recordReportCount: 3,\n      reportedAccountCount: 1,\n      reportedRecordCount: 2,\n      takendownAccountCount: 0,\n      takendownRecordCount: 2,\n      labeledAccountCount: 1,\n      labeledRecordCount: 0,\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-repos.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { forSnapshot } from './_util'\n\ndescribe('admin get multiple repos', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_repos',\n    })\n    ozone = network.ozone\n    agent = ozone.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    await basicSeed(sc)\n    await pdsAgent.com.atproto.server.deactivateAccount(\n      {},\n      { encoding: 'application/json', headers: sc.getHeaders(sc.dids.dan) },\n    )\n    await network.processAll()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventAcknowledge' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.bob,\n      reasonType: REASONSPAM,\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: REASONOTHER,\n      reason: 'defamation',\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n  })\n\n  it('gets multiple repos by did', async () => {\n    const { data } = await agent.tools.ozone.moderation.getRepos(\n      { dids: [sc.dids.alice, sc.dids.bob, 'did:web:xyz'] },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetRepos) },\n    )\n\n    expect(forSnapshot(data)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-starter-pack.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  RecordRef,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { TAKEDOWN_LABEL } from '../src/mod-service'\nimport { forSnapshot } from './_util'\n\ndescribe('admin get starter pack view', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let sc: SeedClient\n  let sp1: RecordRef\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_starterpack',\n    })\n    ozone = network.ozone\n    AtpAgent.configure({ appLabelers: [ozone.ctx.cfg.service.did] })\n    agent = ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    const feedgen = await sc.createFeedGen(\n      sc.dids.alice,\n      'did:web:example.com',\n      \"alice's feedgen\",\n    )\n    sp1 = await sc.createStarterPack(\n      sc.dids.alice,\n      \"alice's starter pack\",\n      [sc.dids.bob, sc.dids.carol, sc.dids.dan],\n      [feedgen.uriStr],\n    )\n    await network.processAll()\n  })\n\n  describe('getStarterPack()', () => {\n    it('gets a starterpack by uri', async () => {\n      const result = await agent.api.app.bsky.graph.getStarterPack(\n        { starterPack: sp1.uriStr },\n        { headers: await ozone.modHeaders(ids.AppBskyGraphGetStarterPack) },\n      )\n      expect(forSnapshot(result.data)).toMatchSnapshot()\n    })\n\n    it('gets a starterpack while taken down', async () => {\n      // Validate that appview returns starterpacks before takedown\n      const appviewAgent = network.bsky.getClient()\n      const beforeTakedownFromAppview =\n        await appviewAgent.api.app.bsky.graph.getStarterPack(\n          { starterPack: sp1.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              sc.dids.alice,\n              ids.AppBskyGraphGetStarterPack,\n            ),\n          },\n        )\n\n      expect(\n        forSnapshot(beforeTakedownFromAppview.data.starterPack),\n      ).toMatchSnapshot()\n\n      await network.bsky.db.db\n        .insertInto('label')\n        .values({\n          src: ozone.ctx.cfg.service.did,\n          uri: sp1.uriStr,\n          cid: sp1.cidStr,\n          val: TAKEDOWN_LABEL,\n          neg: false,\n          cts: new Date().toISOString(),\n        })\n        .execute()\n\n      const afterTakedownFromOzone =\n        await agent.api.app.bsky.graph.getStarterPack(\n          { starterPack: sp1.uriStr },\n          { headers: await ozone.modHeaders(ids.AppBskyGraphGetStarterPack) },\n        )\n\n      // validate that ozone returns starterpacks after takedown\n      expect(\n        forSnapshot(afterTakedownFromOzone.data.starterPack),\n      ).toMatchSnapshot()\n\n      // validate that appview does not return starterpack after takedown\n      await expect(\n        appviewAgent.api.app.bsky.graph.getStarterPack(\n          { starterPack: sp1.uriStr },\n          {\n            headers: await network.serviceHeaders(\n              sc.dids.alice,\n              ids.AppBskyGraphGetStarterPack,\n            ),\n          },\n        ),\n      ).rejects.toThrow('Starter pack not found')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/get-subjects.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { forSnapshot } from './_util'\n\ndescribe('admin get multiple subjects with all relevant details', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let agent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_get_subjects',\n    })\n    ozone = network.ozone\n    agent = ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    await sc.createReport({\n      reportedBy: sc.dids.bob,\n      reasonType: REASONSPAM,\n      subject: {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      },\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: REASONOTHER,\n      reason: 'defamation',\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      },\n    })\n  })\n\n  it('gets multiple subjects with records', async () => {\n    const {\n      data: { subjects },\n    } = await agent.tools.ozone.moderation.getSubjects(\n      { subjects: [sc.dids.alice, sc.posts[sc.dids.alice][0].ref.uriStr] },\n      { headers: await ozone.modHeaders(ids.ToolsOzoneModerationGetSubjects) },\n    )\n\n    expect(forSnapshot(subjects)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/mod-tool.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\n\ndescribe('mod-tool tracking', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_mod_tool_test',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('stores and returns modTool with name and meta metadata', async () => {\n    const subject = {\n      $type: 'com.atproto.repo.strongRef' as const,\n      uri: sc.posts[sc.dids.bob][0].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][0].ref.cidStr,\n    }\n\n    const modTool = {\n      name: 'automod/1.1.3',\n      meta: {\n        confidence: 85,\n        rules: ['high_risk_country', 'spam_detection'],\n        version: '1.1.3',\n        environment: 'production',\n      },\n    }\n\n    const emittedEvent = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['spam'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool,\n    })\n\n    expect(emittedEvent.modTool).toEqual(modTool)\n    expect(emittedEvent.modTool?.name).toBe('automod/1.1.3')\n    expect(emittedEvent.modTool?.meta).toEqual({\n      confidence: 85,\n      rules: ['high_risk_country', 'spam_detection'],\n      version: '1.1.3',\n      environment: 'production',\n    })\n\n    const queryResult = await modClient.queryEvents({\n      subject: subject.uri,\n    })\n\n    const foundEvent = queryResult.events.find((e) => e.id === emittedEvent.id)\n    expect(foundEvent?.modTool).toEqual(modTool)\n  })\n\n  it('filters events by modTool name', async () => {\n    const subject = {\n      $type: 'com.atproto.repo.strongRef' as const,\n      uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n    }\n\n    const event1 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['test1'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: { name: 'automod/1.1.3' },\n    })\n\n    const event2 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['test2'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: { name: 'ozone-web/1.0.0' },\n    })\n\n    const event3 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['test3'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: { name: 'mobile-app/2.1.0' },\n    })\n\n    const automodResults = await modClient.queryEvents({\n      subject: subject.uri,\n      modTool: ['automod/1.1.3'],\n    })\n\n    expect(automodResults.events).toHaveLength(1)\n    expect(automodResults.events[0].id).toBe(event1.id)\n    expect(automodResults.events[0].modTool?.name).toBe('automod/1.1.3')\n\n    const multipleResults = await modClient.queryEvents({\n      subject: subject.uri,\n      modTool: ['automod/1.1.3', 'ozone-web/1.0.0'],\n    })\n\n    expect(multipleResults.events).toHaveLength(2)\n    const eventIds = multipleResults.events.map((e) => e.id)\n    expect(eventIds).toContain(event1.id)\n    expect(eventIds).toContain(event2.id)\n    expect(eventIds).not.toContain(event3.id)\n  })\n\n  it('filters events by batchId and supports pagination', async () => {\n    const subject = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.carol][0].ref.uriStr,\n      cid: sc.posts[sc.dids.carol][0].ref.cidStr,\n    }\n\n    const batchId1 = 'batch-123'\n    const batchId2 = 'batch-456'\n\n    // Create events with first batchId\n    const event1 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['batch1-event1'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: {\n        name: 'automod/1.1.3',\n        meta: { batchId: batchId1 },\n      },\n    })\n\n    const event2 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['batch1-event2'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: {\n        name: 'automod/1.1.3',\n        meta: { batchId: batchId1 },\n      },\n    })\n\n    const event3 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['batch1-event3'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: {\n        name: 'ozone-ui/workspace',\n        meta: { batchId: batchId1 },\n      },\n    })\n\n    // Create events with second batchId\n    const event4 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['batch2-event1'],\n        negateLabelVals: [],\n      },\n      subject,\n      modTool: {\n        name: 'ozone-ui/workspace',\n        meta: { batchId: batchId2 },\n      },\n    })\n\n    // Create event without batchId\n    const event5 = await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['no-batch'],\n        negateLabelVals: [],\n      },\n      subject,\n    })\n\n    // Test filtering by first batchId\n    const batch1Results = await modClient.queryEvents({\n      subject: subject.uri,\n      batchId: batchId1,\n    })\n\n    expect(batch1Results.events).toHaveLength(3)\n    const batch1EventIds = batch1Results.events.map((e) => e.id)\n    expect(batch1EventIds).toContain(event1.id)\n    expect(batch1EventIds).toContain(event2.id)\n    expect(batch1EventIds).toContain(event3.id)\n    expect(batch1EventIds).not.toContain(event4.id)\n    expect(batch1EventIds).not.toContain(event5.id)\n\n    // Verify all events have the correct batchId\n    batch1Results.events.forEach((event) => {\n      expect(event.modTool?.meta?.batchId).toBe(batchId1)\n    })\n\n    // Test filtering by second batchId\n    const batch2Results = await modClient.queryEvents({\n      subject: subject.uri,\n      batchId: batchId2,\n    })\n\n    expect(batch2Results.events).toHaveLength(1)\n    expect(batch2Results.events[0].id).toBe(event4.id)\n    expect(batch2Results.events[0].modTool?.meta?.batchId).toBe(batchId2)\n\n    // Test pagination with batchId filter\n    const paginatedResults = await modClient.queryEvents({\n      subject: subject.uri,\n      batchId: batchId1,\n      limit: 2,\n    })\n\n    expect(paginatedResults.events).toHaveLength(2)\n    expect(paginatedResults.cursor).toBeTruthy()\n\n    // Get next page\n    const nextPageResults = await modClient.queryEvents({\n      subject: subject.uri,\n      batchId: batchId1,\n      limit: 2,\n      cursor: paginatedResults.cursor,\n    })\n\n    expect(nextPageResults.events).toHaveLength(1)\n\n    // Verify all paginated results have correct batchId\n    const allPaginatedEvents = paginatedResults.events.concat(\n      nextPageResults.events,\n    )\n    allPaginatedEvents.forEach((event) => {\n      expect(event.modTool?.meta?.batchId).toBe(batchId1)\n    })\n\n    // Verify we got all 3 events across pages\n    const allPaginatedIds = paginatedResults.events\n      .map((e) => e.id)\n      .concat(nextPageResults.events.map((e) => e.id))\n    expect(allPaginatedIds).toContain(event1.id)\n    expect(allPaginatedIds).toContain(event2.id)\n    expect(allPaginatedIds).toContain(event3.id)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/moderation-appeals.test.ts",
    "content": "import {\n  ComAtprotoModerationDefs,\n  ToolsOzoneModerationDefs,\n} from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport {\n  REASONMISLEADING,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { REVIEWESCALATED } from '../src/lexicon/types/tools/ozone/moderation/defs'\n\ndescribe('moderation-appeals', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_moderation_appeals',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const assertSubjectStatus = async (\n    subject: string,\n    status: string,\n    appealed: boolean | undefined,\n  ): Promise<ToolsOzoneModerationDefs.SubjectStatusView | undefined> => {\n    const res = await modClient.queryStatuses({\n      subject,\n    })\n    expect(res.subjectStatuses[0]?.reviewState).toEqual(status)\n    expect(res.subjectStatuses[0]?.appealed).toEqual(appealed)\n    return res.subjectStatuses[0]\n  }\n\n  describe('appeals from users', () => {\n    const getBobsPostSubject = () => ({\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.bob][1].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][1].ref.cidStr,\n    })\n    const getCarolPostSubject = () => ({\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.carol][0].ref.uriStr,\n      cid: sc.posts[sc.dids.carol][0].ref.cidStr,\n    })\n    const assertBobsPostStatus = async (\n      status: string,\n      appealed: boolean | undefined,\n    ) => assertSubjectStatus(getBobsPostSubject().uri, status, appealed)\n\n    it('only changes subject status if original author of the content or a moderator is appealing', async () => {\n      // Create a report by alice\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: REASONMISLEADING,\n        },\n        subject: getBobsPostSubject(),\n      })\n\n      await assertBobsPostStatus(ToolsOzoneModerationDefs.REVIEWOPEN, undefined)\n\n      // Create a report as normal user with appeal type\n      expect(\n        sc.createReport({\n          reportedBy: sc.dids.carol,\n          reasonType: ComAtprotoModerationDefs.REASONAPPEAL,\n          reason: 'appealing',\n          subject: getBobsPostSubject(),\n        }),\n      ).rejects.toThrow('You cannot appeal this report')\n\n      // Verify that the appeal status did not change\n      await assertBobsPostStatus(ToolsOzoneModerationDefs.REVIEWOPEN, undefined)\n\n      // Emit report event as moderator\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: ComAtprotoModerationDefs.REASONAPPEAL,\n        },\n        subject: getBobsPostSubject(),\n      })\n\n      // Verify that appeal status changed when appeal report was emitted by moderator\n      const status = await assertBobsPostStatus(REVIEWESCALATED, true)\n      // @ts-expect-error unspecced ?\n      expect(status?.appealedAt).not.toBeNull()\n\n      // Create a report as normal user for carol's post\n      await sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: REASONMISLEADING,\n        reason: 'lies!',\n        subject: getCarolPostSubject(),\n      })\n\n      // Verify that the appeal status on carol's post is undefined\n      await assertSubjectStatus(\n        getCarolPostSubject().uri,\n        ToolsOzoneModerationDefs.REVIEWOPEN,\n        undefined,\n      )\n\n      await sc.createReport({\n        reportedBy: sc.dids.carol,\n        reasonType: ComAtprotoModerationDefs.REASONAPPEAL,\n        reason: 'appealing',\n        subject: getCarolPostSubject(),\n      })\n      // Verify that the appeal status on carol's post is true\n      await assertSubjectStatus(\n        getCarolPostSubject().uri,\n        REVIEWESCALATED,\n        true,\n      )\n    })\n    it('allows multiple appeals and updates last appealed timestamp', async () => {\n      // Resolve appeal with acknowledge\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventResolveAppeal',\n        },\n        subject: getBobsPostSubject(),\n      })\n\n      const previousStatus = await assertBobsPostStatus(REVIEWESCALATED, false)\n\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: ComAtprotoModerationDefs.REASONAPPEAL,\n        },\n        subject: getBobsPostSubject(),\n      })\n\n      // Verify that even after the appeal event by bob for his post, the appeal status is true again with new timestamp\n      const newStatus = await assertBobsPostStatus(REVIEWESCALATED, true)\n      expect(\n        new Date(`${previousStatus?.lastAppealedAt}`).getTime(),\n      ).toBeLessThan(new Date(`${newStatus?.lastAppealedAt}`).getTime())\n    })\n  })\n\n  describe('appeal resolution', () => {\n    const getAlicesPostSubject = () => ({\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][1].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][1].ref.cidStr,\n    })\n    it('appeal status is maintained while review state changes based on incoming events', async () => {\n      // Bob reports alice's post\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: REASONMISLEADING,\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      // Moderator acknowledges the report, assume a label was applied too\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventAcknowledge',\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      // Alice appeals the report\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: ComAtprotoModerationDefs.REASONAPPEAL,\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      await assertSubjectStatus(\n        getAlicesPostSubject().uri,\n        REVIEWESCALATED,\n        true,\n      )\n\n      // Bob reports it again\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: REASONSPAM,\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      // Assert that the status is still REVIEWESCALATED, as report events are meant to do\n      await assertSubjectStatus(\n        getAlicesPostSubject().uri,\n        REVIEWESCALATED,\n        true,\n      )\n\n      // Emit an escalation event\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventEscalate',\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      await assertSubjectStatus(\n        getAlicesPostSubject().uri,\n        REVIEWESCALATED,\n        true,\n      )\n\n      // Emit an acknowledge event\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventAcknowledge',\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      // Assert that status moved on to reviewClosed while appealed status is still true\n      await assertSubjectStatus(\n        getAlicesPostSubject().uri,\n        ToolsOzoneModerationDefs.REVIEWCLOSED,\n        true,\n      )\n\n      // Emit a resolveAppeal event\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventResolveAppeal',\n          comment: 'lgtm',\n        },\n        subject: getAlicesPostSubject(),\n      })\n\n      // Assert that status stayed the same while appealed status is still true\n      await assertSubjectStatus(\n        getAlicesPostSubject().uri,\n        ToolsOzoneModerationDefs.REVIEWCLOSED,\n        false,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/moderation-events.test.ts",
    "content": "import assert from 'node:assert'\nimport EventEmitter, { once } from 'node:events'\nimport { ToolsOzoneModerationDefs } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { isRepoRef } from '../src/lexicon/types/com/atproto/admin/defs'\nimport {\n  REASONAPPEAL,\n  REASONMISLEADING,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { isMain as isStrongRef } from '../src/lexicon/types/com/atproto/repo/strongRef'\nimport { forSnapshot } from './_util'\n\ndescribe('moderation-events', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const seedEvents = async () => {\n    const bobsAccount = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n    const alicesAccount = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.alice,\n    }\n    const bobsPost = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.bob][0].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][0].ref.cidStr,\n    }\n    const alicesPost = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n    }\n\n    for (let i = 0; i < 4; i++) {\n      await sc.createReport({\n        reasonType: i % 2 ? REASONSPAM : REASONMISLEADING,\n        reason: 'X',\n        //   Report bob's account by alice and vice versa\n        subject: i % 2 ? bobsAccount : alicesAccount,\n        reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob,\n      })\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'X',\n        //   Report bob's post by alice and vice versa\n        subject: i % 2 ? bobsPost : alicesPost,\n        reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob,\n      })\n    }\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_moderation_events',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n    await seedEvents()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('query events', () => {\n    it('returns all events for record or repo', async () => {\n      const [bobsEvents, alicesPostEvents] = await Promise.all([\n        modClient.queryEvents({\n          subject: sc.dids.bob,\n        }),\n        modClient.queryEvents({\n          subject: sc.posts[sc.dids.alice][0].ref.uriStr,\n        }),\n      ])\n\n      expect(forSnapshot(bobsEvents.events)).toMatchSnapshot()\n      expect(forSnapshot(alicesPostEvents.events)).toMatchSnapshot()\n    })\n\n    it('filters events by types', async () => {\n      const alicesAccount = {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.alice,\n      }\n      await Promise.all([\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventComment',\n            comment: 'X',\n          },\n          subject: alicesAccount,\n        }),\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventEscalate',\n            comment: 'X',\n          },\n          subject: alicesAccount,\n        }),\n      ])\n      const [allEvents, reportEvents] = await Promise.all([\n        modClient.queryEvents({\n          subject: sc.dids.alice,\n        }),\n        modClient.queryEvents({\n          subject: sc.dids.alice,\n          types: ['tools.ozone.moderation.defs#modEventReport'],\n        }),\n      ])\n\n      expect(allEvents.events.length).toBeGreaterThan(\n        reportEvents.events.length,\n      )\n      expect(\n        [...new Set(reportEvents.events.map((e) => e.event.$type))].length,\n      ).toEqual(1)\n\n      expect(\n        [...new Set(allEvents.events.map((e) => e.event.$type))].length,\n      ).toEqual(4)\n    })\n\n    it('returns events for all content by user', async () => {\n      const [forAccount, forPost] = await Promise.all([\n        modClient.queryEvents({\n          subject: sc.dids.bob,\n          includeAllUserRecords: true,\n        }),\n        modClient.queryEvents({\n          subject: sc.posts[sc.dids.bob][0].ref.uriStr,\n          includeAllUserRecords: true,\n        }),\n      ])\n\n      expect(forAccount.events.length).toEqual(forPost.events.length)\n      // Save events are returned from both requests\n      expect(forPost.events.map(({ id }) => id).sort()).toEqual(\n        forAccount.events.map(({ id }) => id).sort(),\n      )\n    })\n\n    it('returns paginated list of events with cursor', async () => {\n      const allEvents = await modClient.queryEvents({\n        subject: sc.dids.bob,\n        includeAllUserRecords: true,\n      })\n\n      const getPaginatedEvents = async (\n        sortDirection: 'asc' | 'desc' = 'desc',\n      ) => {\n        let defaultCursor: undefined | string = undefined\n        const events: ToolsOzoneModerationDefs.ModEventView[] = []\n        let count = 0\n        do {\n          // get 1 event at a time and check we get all events\n          const res = await modClient.queryEvents({\n            limit: 1,\n            subject: sc.dids.bob,\n            includeAllUserRecords: true,\n            cursor: defaultCursor,\n            sortDirection,\n          })\n          events.push(...res.events)\n          defaultCursor = res.cursor\n          count++\n          // The count is a circuit breaker to prevent infinite loop in case of failing test\n        } while (defaultCursor && count < 10)\n\n        return events\n      }\n\n      const defaultEvents = await getPaginatedEvents()\n      const reversedEvents = await getPaginatedEvents('asc')\n\n      expect(allEvents.events.length).toEqual(6)\n      expect(defaultEvents.length).toEqual(allEvents.events.length)\n      expect(reversedEvents.length).toEqual(allEvents.events.length)\n      // First event in the reversed list is the last item in the default list\n      expect(reversedEvents[0].id).toEqual(\n        defaultEvents[defaultEvents.length - 1].id,\n      )\n    })\n\n    it('returns report events matching reportType filters', async () => {\n      const [spamEvents, misleadingEvents] = await Promise.all([\n        modClient.queryEvents({\n          reportTypes: [REASONSPAM],\n        }),\n        modClient.queryEvents({\n          reportTypes: [REASONMISLEADING, REASONAPPEAL],\n        }),\n      ])\n\n      expect(misleadingEvents.events.length).toEqual(2)\n      expect(spamEvents.events.length).toEqual(6)\n    })\n\n    it('returns events matching keyword in comment', async () => {\n      const [eventsWithX, eventsWithTest, eventsWithComment] =\n        await Promise.all([\n          modClient.queryEvents({\n            comment: 'X',\n          }),\n          modClient.queryEvents({\n            comment: 'test',\n          }),\n          modClient.queryEvents({\n            hasComment: true,\n          }),\n        ])\n\n      expect(eventsWithX.events.length).toEqual(10)\n      expect(eventsWithTest.events.length).toEqual(0)\n      expect(eventsWithComment.events.length).toEqual(10)\n    })\n\n    it('returns events matching multiple keywords in comment', async () => {\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'november rain',\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        reportedBy: sc.dids.bob,\n      })\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'rainy days feel lazy',\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        reportedBy: sc.dids.bob,\n      })\n      const [eventsMatchingBothKeywords, unusedTrailingSeparator, extraSpaces] =\n        await Promise.all([\n          modClient.queryEvents({\n            hasComment: true,\n            comment: 'november||lazy',\n          }),\n          modClient.queryEvents({\n            hasComment: true,\n            comment: 'november||lazy||',\n          }),\n          modClient.queryEvents({\n            hasComment: true,\n            comment: '||november||lazy||  ',\n          }),\n        ])\n\n      expect(forSnapshot(eventsMatchingBothKeywords.events)).toMatchSnapshot()\n      expect(forSnapshot(unusedTrailingSeparator.events)).toMatchSnapshot()\n      expect(forSnapshot(extraSpaces.events)).toMatchSnapshot()\n    })\n\n    it('returns events matching filter params for labels', async () => {\n      const [negatedLabelEvent, createdLabelEvent] = await Promise.all([\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventLabel',\n            comment: 'X',\n            negateLabelVals: ['L1', 'L2'],\n            createLabelVals: [],\n          },\n          //   Report bob's account by alice and vice versa\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.alice,\n          },\n        }),\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventLabel',\n            comment: 'X',\n            createLabelVals: ['L1', 'L2'],\n            negateLabelVals: [],\n          },\n          //   Report bob's account by alice and vice versa\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n        }),\n      ])\n      const [withTwoLabels, withoutTwoLabels, withOneLabel, withoutOneLabel] =\n        await Promise.all([\n          modClient.queryEvents({\n            addedLabels: ['L1', 'L3'],\n          }),\n          modClient.queryEvents({\n            removedLabels: ['L1', 'L2'],\n          }),\n          modClient.queryEvents({\n            addedLabels: ['L1'],\n          }),\n          modClient.queryEvents({\n            removedLabels: ['L2'],\n          }),\n        ])\n\n      // Verify that when querying for events where 2 different labels were added\n      // events where all of the labels from the list was added are returned\n      expect(withTwoLabels.events.length).toEqual(0)\n      expect(negatedLabelEvent.id).toEqual(withoutTwoLabels.events[0].id)\n\n      expect(createdLabelEvent.id).toEqual(withOneLabel.events[0].id)\n      expect(negatedLabelEvent.id).toEqual(withoutOneLabel.events[0].id)\n    })\n    it('returns events matching filter params for tags', async () => {\n      const tagEvent = async ({\n        add,\n        remove,\n      }: {\n        add: string[]\n        remove: string[]\n      }) =>\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            comment: 'X',\n            add,\n            remove,\n          },\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.carol,\n          },\n        })\n      const addEvent = await tagEvent({ add: ['L1', 'L2'], remove: [] })\n      const addAndRemoveEvent = await tagEvent({ add: ['L3'], remove: ['L2'] })\n      const [addFinder, addAndRemoveFinder, _removeFinder] = await Promise.all([\n        modClient.queryEvents({\n          addedTags: ['L1'],\n        }),\n        modClient.queryEvents({\n          addedTags: ['L3'],\n          removedTags: ['L2'],\n        }),\n        modClient.queryEvents({\n          removedTags: ['L2'],\n        }),\n      ])\n\n      expect(addFinder.events.length).toEqual(1)\n      expect(addEvent.id).toEqual(addFinder.events[0].id)\n\n      expect(addAndRemoveEvent.id).toEqual(addAndRemoveFinder.events[0].id)\n      expect(addAndRemoveEvent.id).toEqual(addAndRemoveFinder.events[0].id)\n      assert(ToolsOzoneModerationDefs.isModEventTag(addAndRemoveEvent.event))\n      expect(addAndRemoveEvent.event.add).toEqual(['L3'])\n      expect(addAndRemoveEvent.event.remove).toEqual(['L2'])\n    })\n\n    it('returns events for specified collections', async () => {\n      const sp = await sc.createStarterPack(\n        sc.dids.alice,\n        \"alice's about to get blocked starter pack\",\n        [sc.dids.bob, sc.dids.carol],\n        [],\n      )\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'X',\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          ...sp.raw,\n        },\n        reportedBy: sc.dids.bob,\n      })\n\n      const [\n        onlyStarterPackReports,\n        onlyAlicesStarterPackReports,\n        onlyBobsStarterPackReports,\n        onlyPostReports,\n      ] = await Promise.all([\n        modClient.queryEvents({\n          types: ['tools.ozone.moderation.defs#modEventReport'],\n          collections: ['app.bsky.graph.starterpack'],\n        }),\n        modClient.queryEvents({\n          subject: sc.dids.alice,\n          includeAllUserRecords: true,\n          types: ['tools.ozone.moderation.defs#modEventReport'],\n          collections: ['app.bsky.graph.starterpack'],\n        }),\n        modClient.queryEvents({\n          subject: sc.dids.bob,\n          includeAllUserRecords: true,\n          types: ['tools.ozone.moderation.defs#modEventReport'],\n          collections: ['app.bsky.graph.starterpack'],\n        }),\n        modClient.queryEvents({\n          types: ['tools.ozone.moderation.defs#modEventReport'],\n          collections: ['app.bsky.feed.post'],\n        }),\n      ])\n\n      expect(onlyStarterPackReports.events.length).toEqual(1)\n      assert(isStrongRef(onlyStarterPackReports.events[0].subject))\n      expect(onlyStarterPackReports.events[0].subject.uri).toContain(\n        'app.bsky.graph.starterpack',\n      )\n\n      expect(onlyAlicesStarterPackReports.events.length).toEqual(1)\n      assert(isStrongRef(onlyAlicesStarterPackReports.events[0].subject))\n      expect(onlyAlicesStarterPackReports.events[0].subject.uri).toContain(\n        sp.uriStr,\n      )\n      expect(onlyBobsStarterPackReports.events.length).toEqual(0)\n      expect(onlyPostReports.events.length).toEqual(4)\n    })\n\n    it('returns events for account or records', async () => {\n      const [onlyAccountReports, onlyRecordReports, onlyReportsOnBobsAccount] =\n        await Promise.all([\n          modClient.queryEvents({\n            types: ['tools.ozone.moderation.defs#modEventReport'],\n            subjectType: 'account',\n          }),\n          modClient.queryEvents({\n            types: ['tools.ozone.moderation.defs#modEventReport'],\n            subjectType: 'record',\n          }),\n          modClient.queryEvents({\n            subject: sc.dids.bob,\n            types: ['tools.ozone.moderation.defs#modEventReport'],\n            subjectType: 'record',\n          }),\n        ])\n\n      assert(\n        onlyAccountReports.events.every((e) => !isStrongRef(e.subject)),\n        'only account reports are returned, no event has a uri',\n      )\n\n      assert(\n        onlyRecordReports.events.every(\n          (e) => isStrongRef(e.subject) && e.subject.uri,\n        ),\n        'only record reports are returned, all events have a uri',\n      )\n\n      assert(\n        onlyReportsOnBobsAccount.events.every(\n          (e) => isRepoRef(e.subject) && e.subject.did === sc.dids.bob,\n        ),\n        \"only bob's account reports are returned, no events have a URI even though the subjectType is record\",\n      )\n    })\n\n    it('queries events by creator', async () => {\n      const now = new Date()\n      const startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000) // 1 day ago\n      const endDate = new Date(now.getTime() + 24 * 60 * 60 * 1000) // 1 day from now\n\n      const events = await modClient.queryEvents({\n        createdBy: network.ozone.moderatorAccnt.did,\n        createdAfter: startDate.toISOString(),\n        createdBefore: endDate.toISOString(),\n        limit: 25,\n        sortDirection: 'desc',\n      })\n\n      expect(events.events.length).toBeGreaterThan(0)\n      expect(events.events.length).toBeLessThanOrEqual(25)\n\n      // Verify sorting\n      for (let i = 1; i < events.events.length; i++) {\n        const prev = events.events[i - 1]\n        const curr = events.events[i]\n        const prevTime = new Date(prev.createdAt).getTime()\n        const currTime = new Date(curr.createdAt).getTime()\n\n        if (prevTime === currTime) {\n          expect(prev.id).toBeGreaterThan(curr.id)\n        } else {\n          expect(prevTime).toBeGreaterThan(currTime)\n        }\n      }\n\n      // Verify createdBy is correct\n      events.events.forEach((event) => {\n        expect(event.createdBy).toEqual(network.ozone.moderatorAccnt.did)\n      })\n    })\n  })\n\n  describe('get event', () => {\n    it('gets an event by specific id', async () => {\n      const data = await modClient.getEvent(1)\n      expect(forSnapshot(data)).toMatchSnapshot()\n    })\n  })\n\n  describe('deduping by external id', () => {\n    it('fails on events with duplicate external id', async () => {\n      const externalId = 'external-id-1'\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n          status: 'pending',\n          createdAt: new Date().toISOString(),\n          attemptId: 'attempt-1',\n        },\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        externalId,\n      })\n      await expect(\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#ageAssuranceEvent',\n            status: 'pending',\n            createdAt: new Date().toISOString(),\n            attemptId: 'attempt-1',\n          },\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.alice,\n          },\n          externalId,\n        }),\n      ).rejects.toThrow(\n        'An event with the same external ID already exists for the subject.',\n      )\n    })\n  })\n\n  describe('blobs', () => {\n    it('are tracked on takedown event', async () => {\n      const post = sc.posts[sc.dids.carol][0]\n      assert(post.images.length > 1)\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        },\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n        subjectBlobCids: [post.images[0].image.ref.toString()],\n      })\n      const result = await modClient.queryEvents({\n        subject: post.ref.uriStr,\n        types: ['tools.ozone.moderation.defs#modEventTakedown'],\n      })\n      expect(result.events[0]).toMatchObject({\n        createdBy: network.ozone.moderatorAccnt.did,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        },\n        subjectBlobCids: [post.images[0].image.ref.toString()],\n      })\n    })\n\n    it(\"are tracked on reverse-takedown event even if they aren't specified\", async () => {\n      const post = sc.posts[sc.dids.carol][0]\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        },\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n      })\n      const result = await modClient.queryEvents({\n        subject: post.ref.uriStr,\n      })\n      expect(result.events[0]).toMatchObject({\n        createdBy: network.ozone.moderatorAccnt.did,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        },\n        subjectBlobCids: [post.images[0].image.ref.toString()],\n      })\n    })\n  })\n\n  describe('email event', () => {\n    let sendMailOriginal\n    const mailCatcher = new EventEmitter()\n    const getMailFrom = async (\n      promise,\n    ): Promise<{ to: string; subject: string; from: string }> => {\n      const result = await Promise.all([once(mailCatcher, 'mail'), promise])\n      return result[0][0]\n    }\n\n    beforeAll(() => {\n      const mailer = network.pds.ctx.moderationMailer\n      // Catch emails for use in tests\n      sendMailOriginal = mailer.transporter.sendMail\n      mailer.transporter.sendMail = async (opts) => {\n        const result = await sendMailOriginal.call(mailer.transporter, opts)\n        mailCatcher.emit('mail', opts)\n        return result\n      }\n    })\n\n    afterAll(() => {\n      network.pds.ctx.moderationMailer.transporter.sendMail = sendMailOriginal\n    })\n\n    it('sends email via pds.', async () => {\n      const mail = await getMailFrom(\n        modClient.emitEvent({\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventEmail',\n            comment: 'Reaching out to Alice',\n            subjectLine: 'Hello',\n            content: 'Hey Alice, how are you?',\n          },\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.alice,\n          },\n        }),\n      )\n      expect(mail).toEqual({\n        to: 'alice@test.com',\n        subject: 'Hello',\n        html: 'Hey Alice, how are you?',\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/moderation-status-tags.test.ts",
    "content": "import assert from 'node:assert'\nimport { ComAtprotoAdminDefs } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { REASONSPAM } from '../src/lexicon/types/com/atproto/moderation/defs'\n\ndescribe('moderation-status-tags', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_moderation_status_tags',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('manage tags on subject status', () => {\n    it('adds and removes tags on a subject', async () => {\n      const bobsAccount = {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.bob,\n      }\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'X',\n        subject: bobsAccount,\n        reportedBy: sc.dids.alice,\n      })\n      await modClient.emitEvent({\n        subject: bobsAccount,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTag',\n          add: ['interaction-churn'],\n          remove: [],\n        },\n      })\n      const statusAfterInteractionTag = await modClient.queryStatuses({\n        subject: bobsAccount.did,\n      })\n      expect(statusAfterInteractionTag.subjectStatuses[0].tags).toContain(\n        'interaction-churn',\n      )\n\n      await modClient.emitEvent({\n        subject: bobsAccount,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTag',\n          remove: ['interaction-churn'],\n          add: ['follow-churn'],\n        },\n      })\n      const statusAfterFollowTag = await modClient.queryStatuses({\n        subject: bobsAccount.did,\n      })\n\n      expect(statusAfterFollowTag.subjectStatuses[0].tags).not.toContain(\n        'interaction-churn',\n      )\n      expect(statusAfterFollowTag.subjectStatuses[0].tags).toContain(\n        'follow-churn',\n      )\n    })\n\n    it('allows filtering by tags', async () => {\n      await modClient.emitEvent({\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTag',\n          add: ['report:spam', 'lang:ja', 'lang:en'],\n          remove: [],\n        },\n      })\n      const [englishAndJapaneseQueue, englishOrJapaneseQueue] =\n        await Promise.all([\n          modClient.queryStatuses({\n            tags: ['lang:ja&&lang:en'],\n          }),\n          modClient.queryStatuses({\n            tags: ['report:ja', 'lang:en'],\n          }),\n        ])\n\n      // Verify that the queue only contains 1 item with both en and ja tags which is alice's account\n      expect(englishAndJapaneseQueue.subjectStatuses.length).toEqual(1)\n      const { subject } = englishAndJapaneseQueue.subjectStatuses[0]\n      assert(ComAtprotoAdminDefs.isRepoRef(subject))\n      expect(subject.did).toEqual(sc.dids.alice)\n\n      // Verify that when querying for either en or ja tags, both alice and bob are returned\n      expect(englishOrJapaneseQueue.subjectStatuses.length).toEqual(2)\n      const englishOrJapaneseDids = englishOrJapaneseQueue.subjectStatuses.map(\n        ({ subject }) => {\n          assert(ComAtprotoAdminDefs.isRepoRef(subject))\n          return subject.did\n        },\n      )\n      expect(englishOrJapaneseDids).toContain(sc.dids.alice)\n      expect(englishOrJapaneseDids).toContain(sc.dids.bob)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/moderation-statuses.test.ts",
    "content": "import assert from 'node:assert'\nimport {\n  ToolsOzoneModerationDefs,\n  ToolsOzoneModerationQueryStatuses,\n} from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { isRepoRef } from '../src/lexicon/types/com/atproto/admin/defs'\nimport {\n  REASONMISLEADING,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { isMain as isStrongRef } from '../src/lexicon/types/com/atproto/repo/strongRef'\nimport {\n  REVIEWNONE,\n  REVIEWOPEN,\n} from '../src/lexicon/types/tools/ozone/moderation/defs'\nimport { forSnapshot } from './_util'\n\ndescribe('moderation-statuses', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const seedEvents = async () => {\n    const bobsAccount = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n    const carlasAccount = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.alice,\n    }\n    const bobsPost = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.bob][0].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][0].ref.cidStr,\n    }\n    const alicesPost = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.alice][1].ref.uriStr,\n      cid: sc.posts[sc.dids.alice][1].ref.cidStr,\n    }\n\n    for (let i = 0; i < 4; i++) {\n      await sc.createReport({\n        reasonType: i % 2 ? REASONSPAM : REASONMISLEADING,\n        reason: 'X',\n        //   Report bob's account by alice and vice versa\n        subject: i % 2 ? bobsAccount : carlasAccount,\n        reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob,\n      })\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'X',\n        //   Report bob's post by alice and vice versa\n        subject: i % 2 ? bobsPost : alicesPost,\n        reportedBy: i % 2 ? sc.dids.alice : sc.dids.bob,\n      })\n    }\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_moderation_statuses',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n    await seedEvents()\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('query statuses', () => {\n    it('returns statuses for subjects that received moderation events', async () => {\n      const response = await modClient.queryStatuses({})\n\n      expect(forSnapshot(response.subjectStatuses)).toMatchSnapshot()\n    })\n\n    it('returns statuses filtered by subject language', async () => {\n      const klingonQueue = await modClient.queryStatuses({\n        tags: ['lang:i'],\n      })\n\n      expect(forSnapshot(klingonQueue.subjectStatuses)).toMatchSnapshot()\n\n      const nonKlingonQueue = await modClient.queryStatuses({\n        excludeTags: ['lang:i'],\n      })\n\n      // Verify that the klingon tagged subject is not returned when excluding klingon\n      expect(nonKlingonQueue.subjectStatuses.map((s) => s.id)).not.toContain(\n        klingonQueue.subjectStatuses[0].id,\n      )\n\n      // Verify multi lang tag exclusion\n      Promise.all(\n        nonKlingonQueue.subjectStatuses.map((s, i) => {\n          return modClient.emitEvent({\n            subject: s.subject,\n            event: {\n              $type: 'tools.ozone.moderation.defs#modEventTag',\n              add: [i % 2 ? 'lang:jp' : 'lang:it'],\n              remove: [],\n              comment: 'Adding custom lang tag',\n            },\n            createdBy: sc.dids.alice,\n          })\n        }),\n      )\n\n      const queueWithoutKlingonAndItalian = await modClient.queryStatuses({\n        excludeTags: ['lang:i', 'lang:it'],\n      })\n\n      queueWithoutKlingonAndItalian.subjectStatuses\n        .map((s) => s.tags)\n        .flat()\n        .forEach((tag) => {\n          expect(['lang:it', 'lang:i']).not.toContain(tag)\n        })\n    })\n\n    it('returns paginated statuses', async () => {\n      // We know there will be exactly 4 statuses in db\n      const getPaginatedStatuses = async (\n        params: ToolsOzoneModerationQueryStatuses.QueryParams,\n      ) => {\n        let cursor: string | undefined = ''\n        const statuses: ToolsOzoneModerationDefs.SubjectStatusView[] = []\n        let count = 0\n        do {\n          const results = await modClient.queryStatuses({\n            limit: 1,\n            cursor,\n            ...params,\n          })\n          cursor = results.cursor\n          statuses.push(...results.subjectStatuses)\n          count++\n          // The count is just a brake-check to prevent infinite loop\n        } while (cursor && count < 10)\n\n        return statuses\n      }\n\n      const list = await getPaginatedStatuses({})\n      expect(list[0].id).toEqual(7)\n      expect(list[list.length - 1].id).toEqual(1)\n\n      await modClient.emitEvent({\n        subject: list[1].subject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventAcknowledge',\n          comment: 'X',\n        },\n      })\n\n      const listReviewedFirst = await getPaginatedStatuses({\n        sortDirection: 'desc',\n        sortField: 'lastReviewedAt',\n      })\n\n      // Verify that the item that was recently reviewed comes up first when sorted descendingly\n      // while the result set always contains same number of items regardless of sorting\n      expect(listReviewedFirst[0].id).toEqual(list[1].id)\n      expect(listReviewedFirst.length).toEqual(list.length)\n    })\n\n    it('returns statuses for specified collections', async () => {\n      const sp = await sc.createStarterPack(\n        sc.dids.alice,\n        \"alice's about to get blocked starter pack\",\n        [sc.dids.bob, sc.dids.carol],\n        [],\n      )\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        reason: 'X',\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          ...sp.raw,\n        },\n        reportedBy: sc.dids.bob,\n      })\n\n      const [\n        onlyStarterPackStatuses,\n        onlyAlicesStarterPackStatuses,\n        onlyBobsStarterPackStatuses,\n        onlyPostStatuses,\n      ] = await Promise.all([\n        modClient.queryStatuses({\n          collections: ['app.bsky.graph.starterpack'],\n        }),\n        modClient.queryStatuses({\n          subject: sc.dids.alice,\n          includeAllUserRecords: true,\n          collections: ['app.bsky.graph.starterpack'],\n        }),\n        modClient.queryStatuses({\n          subject: sc.dids.bob,\n          includeAllUserRecords: true,\n          collections: ['app.bsky.graph.starterpack'],\n        }),\n        modClient.queryStatuses({\n          collections: ['app.bsky.feed.post'],\n        }),\n      ])\n\n      expect(onlyStarterPackStatuses.subjectStatuses.length).toEqual(1)\n      assert(isStrongRef(onlyStarterPackStatuses.subjectStatuses[0].subject))\n      expect(onlyStarterPackStatuses.subjectStatuses[0].subject.uri).toContain(\n        'app.bsky.graph.starterpack',\n      )\n      expect(onlyAlicesStarterPackStatuses.subjectStatuses.length).toEqual(1)\n      assert(\n        isStrongRef(onlyAlicesStarterPackStatuses.subjectStatuses[0].subject),\n      )\n      expect(\n        onlyAlicesStarterPackStatuses.subjectStatuses[0].subject.uri,\n      ).toEqual(sp.uriStr)\n      expect(onlyBobsStarterPackStatuses.subjectStatuses.length).toEqual(0)\n      expect(onlyPostStatuses.subjectStatuses.length).toEqual(2)\n    })\n\n    it('returns statuses for account or records', async () => {\n      const [\n        onlyAccountStatuses,\n        onlyRecordStatuses,\n        onlyStatusesOnBobsAccount,\n      ] = await Promise.all([\n        modClient.queryStatuses({\n          subjectType: 'account',\n        }),\n        modClient.queryStatuses({\n          subjectType: 'record',\n        }),\n        modClient.queryStatuses({\n          subject: sc.dids.bob,\n          subjectType: 'record',\n        }),\n      ])\n\n      assert(\n        onlyAccountStatuses.subjectStatuses.every(\n          (e) => !isStrongRef(e.subject),\n        ),\n        'only account statuses are returned, no event has a uri',\n      )\n\n      assert(\n        onlyRecordStatuses.subjectStatuses.every(\n          (e) => isStrongRef(e.subject) && e.subject.uri,\n        ),\n        'only record statuses are returned, all events have a uri',\n      )\n\n      assert(\n        onlyStatusesOnBobsAccount.subjectStatuses.every(\n          (e) => isRepoRef(e.subject) && e.subject.did === sc.dids.bob,\n        ),\n        \"only bob's account statuses are returned, no events have a URI even though the subjectType is record\",\n      )\n    })\n  })\n\n  describe('reviewState changes', () => {\n    it('only sets state to #reviewNone on first non-impactful event', async () => {\n      const bobsAccount = {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.bob,\n      }\n      const alicesPost = {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n        cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n      }\n      const getBobsAccountStatus = async () => {\n        const data = await modClient.queryStatuses({\n          subject: bobsAccount.did,\n        })\n\n        return data.subjectStatuses[0]\n      }\n      // Since bob's account already had a reviewState, it won't be changed by non-impactful events\n      const bobsAccountStatusBeforeTag = await getBobsAccountStatus()\n\n      await Promise.all([\n        modClient.emitEvent({\n          subject: bobsAccount,\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['newTag'],\n            remove: [],\n            comment: 'X',\n          },\n          createdBy: sc.dids.alice,\n        }),\n        modClient.emitEvent({\n          subject: bobsAccount,\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventComment',\n            comment: 'X',\n          },\n          createdBy: sc.dids.alice,\n        }),\n      ])\n      const bobsAccountStatusAfterTag = await getBobsAccountStatus()\n\n      expect(bobsAccountStatusBeforeTag.reviewState).toEqual(\n        bobsAccountStatusAfterTag.reviewState,\n      )\n\n      // Since alice's post didn't have a reviewState it is set to reviewNone on first non-impactful event\n      const getAlicesPostStatus = async () => {\n        const data = await modClient.queryStatuses({\n          subject: alicesPost.uri,\n        })\n\n        return data.subjectStatuses[0]\n      }\n\n      const alicesPostStatusBeforeTag = await getAlicesPostStatus()\n      expect(alicesPostStatusBeforeTag).toBeUndefined()\n\n      await modClient.emitEvent({\n        subject: alicesPost,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventComment',\n          comment: 'X',\n        },\n        createdBy: sc.dids.alice,\n      })\n      const alicesPostStatusAfterTag = await getAlicesPostStatus()\n      expect(alicesPostStatusAfterTag.reviewState).toEqual(REVIEWNONE)\n\n      await modClient.emitEvent({\n        subject: alicesPost,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReport',\n          reportType: REASONMISLEADING,\n          comment: 'X',\n        },\n        createdBy: sc.dids.alice,\n      })\n      const alicesPostStatusAfterReport = await getAlicesPostStatus()\n      expect(alicesPostStatusAfterReport.reviewState).toEqual(REVIEWOPEN)\n    })\n  })\n\n  describe('blobs', () => {\n    it('are tracked on takendown subject', async () => {\n      const post = sc.posts[sc.dids.carol][0]\n      assert(post.images.length > 1)\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        },\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n        subjectBlobCids: [post.images[0].image.ref.toString()],\n        createdBy: sc.dids.alice,\n      })\n      const result = await modClient.queryStatuses({\n        subject: post.ref.uriStr,\n      })\n      expect(result.subjectStatuses.length).toBe(1)\n      expect(result.subjectStatuses[0]).toMatchObject({\n        takendown: true,\n        subjectBlobCids: [post.images[0].image.ref.toString()],\n      })\n    })\n\n    it('are tracked on reverse-takendown subject based on previous status', async () => {\n      const post = sc.posts[sc.dids.carol][0]\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        },\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n      })\n      const result = await modClient.queryStatuses({\n        subject: post.ref.uriStr,\n      })\n      expect(result.subjectStatuses.length).toBe(1)\n      expect(result.subjectStatuses[0]).toMatchObject({\n        takendown: false,\n        subjectBlobCids: [post.images[0].image.ref.toString()],\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/moderation.test.ts",
    "content": "import {\n  AtpAgent,\n  ChatBskyConvoDefs,\n  ToolsOzoneModerationEmitEvent,\n} from '@atproto/api'\nimport { HOUR } from '@atproto/common'\nimport {\n  ImageRef,\n  ModeratorClient,\n  RecordRef,\n  SeedClient,\n  TestNetwork,\n  TestOzone,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { AtUri } from '@atproto/syntax'\nimport { EventReverser } from '../src'\nimport { ImageInvalidator } from '../src/image-invalidator'\nimport { ids } from '../src/lexicon/lexicons'\nimport {\n  REASONMISLEADING,\n  REASONOTHER,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport {\n  ModEventLabel,\n  REVIEWCLOSED,\n  REVIEWESCALATED,\n} from '../src/lexicon/types/tools/ozone/moderation/defs'\nimport { TAKEDOWN_LABEL } from '../src/mod-service'\nimport { forSnapshot, identity } from './_util'\n\ndescribe('moderation', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n  let mockInvalidator: MockInvalidator\n  let agent: AtpAgent\n  let bskyAgent: AtpAgent\n  let pdsAgent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  const recordSubject = (ref: RecordRef) => ({\n    $type: 'com.atproto.repo.strongRef',\n    uri: ref.uriStr,\n    cid: ref.cidStr,\n  })\n\n  const getLabel = async (uri: string, val: string, neg = false) => {\n    return ozone.ctx.db.db\n      .selectFrom('label')\n      .selectAll()\n      .where('uri', '=', uri)\n      .where('val', '=', val)\n      .where('neg', '=', neg)\n      .executeTakeFirst()\n  }\n\n  beforeAll(async () => {\n    mockInvalidator = new MockInvalidator()\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_moderation',\n      ozone: {\n        imgInvalidator: mockInvalidator,\n        cdnPaths: ['/path1/%s/%s', '/path2/%s/%s'],\n      },\n    })\n    ozone = network.ozone\n    agent = network.ozone.getClient()\n    bskyAgent = network.bsky.getClient()\n    pdsAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('reporting', () => {\n    it('creates reports of a repo.', async () => {\n      const reportA = await sc.createReport({\n        reasonType: REASONSPAM,\n        subject: repoSubject(sc.dids.bob),\n        reportedBy: sc.dids.alice,\n      })\n      const reportB = await sc.createReport({\n        reasonType: REASONOTHER,\n        reason: 'impersonation',\n        subject: repoSubject(sc.dids.bob),\n        reportedBy: sc.dids.carol,\n      })\n      expect(forSnapshot([reportA, reportB])).toMatchSnapshot()\n    })\n\n    it(\"allows reporting a repo that doesn't exist.\", async () => {\n      const promise = sc.createReport({\n        reasonType: REASONSPAM,\n        subject: repoSubject('did:plc:unknown'),\n        reportedBy: sc.dids.alice,\n      })\n      await expect(promise).resolves.toBeDefined()\n    })\n\n    it('creates reports of a record.', async () => {\n      const postA = sc.posts[sc.dids.bob][0].ref\n      const postB = sc.posts[sc.dids.bob][1].ref\n      const reportA = await sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: REASONSPAM,\n        subject: recordSubject(postA),\n      })\n      const reportB = await sc.createReport({\n        reasonType: REASONOTHER,\n        reason: 'defamation',\n        subject: recordSubject(postB),\n        reportedBy: sc.dids.carol,\n      })\n      expect(forSnapshot([reportA, reportB])).toMatchSnapshot()\n    })\n\n    it(\"allows reporting a record that doesn't exist.\", async () => {\n      const postA = sc.posts[sc.dids.bob][0].ref\n      const postB = sc.posts[sc.dids.bob][1].ref\n      const postUriBad = new AtUri(postA.uriStr)\n      postUriBad.rkey = 'badrkey'\n\n      const promiseA = sc.createReport({\n        reasonType: REASONSPAM,\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: postUriBad.toString(),\n          cid: postA.cidStr,\n        },\n        reportedBy: sc.dids.alice,\n      })\n      await expect(promiseA).resolves.toBeDefined()\n\n      const promiseB = sc.createReport({\n        reasonType: REASONOTHER,\n        reason: 'defamation',\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: postB.uri.toString(),\n          cid: postA.cidStr, // bad cid\n        },\n        reportedBy: sc.dids.carol,\n      })\n      await expect(promiseB).resolves.toBeDefined()\n    })\n\n    it('creates reports of a DM chat.', async () => {\n      const messageId1 = 'testmessageid1'\n      const messageId2 = 'testmessageid2'\n      const reportA = await sc.createReport({\n        reportedBy: sc.dids.alice,\n        reasonType: REASONSPAM,\n        // @ts-expect-error \"chat.bsky.convo.defs#messageRef\" is not spec'd as subject\n        subject: identity<ChatBskyConvoDefs.MessageRef>({\n          $type: 'chat.bsky.convo.defs#messageRef',\n          did: sc.dids.carol,\n          messageId: messageId1,\n          convoId: 'testconvoid1',\n        }),\n      })\n      const reportB = await sc.createReport({\n        reportedBy: sc.dids.carol,\n        reasonType: REASONOTHER,\n        reason: 'defamation',\n        // @ts-expect-error \"chat.bsky.convo.defs#messageRef\" is not spec'd as subject\n        subject: identity<ChatBskyConvoDefs.MessageRef>({\n          $type: 'chat.bsky.convo.defs#messageRef',\n          did: sc.dids.carol,\n          messageId: messageId2,\n          // @ts-expect-error convoId intentionally missing, restore once this behavior is deprecated\n          convoId: undefined,\n        }),\n      })\n      expect(forSnapshot([reportA, reportB])).toMatchSnapshot()\n      const events = await ozone.ctx.db.db\n        .selectFrom('moderation_event')\n        .selectAll()\n        .where('subjectMessageId', 'in', [messageId1, messageId2])\n        .where('action', '=', 'tools.ozone.moderation.defs#modEventReport')\n        .execute()\n      expect(events.length).toBe(2)\n      expect(\n        events.every(\n          (row) => row.subjectType === 'chat.bsky.convo.defs#messageRef',\n        ),\n      ).toBe(true)\n    })\n  })\n\n  describe('actioning', () => {\n    it('resolves reports on repos and records.', async () => {\n      const post = sc.posts[sc.dids.bob][1].ref\n\n      await Promise.all([\n        sc.createReport({\n          reasonType: REASONSPAM,\n          subject: repoSubject(sc.dids.bob),\n          reportedBy: sc.dids.alice,\n        }),\n        sc.createReport({\n          reasonType: REASONOTHER,\n          reason: 'defamation',\n          subject: recordSubject(post),\n          reportedBy: sc.dids.carol,\n        }),\n      ])\n\n      await modClient.performTakedown({\n        subject: repoSubject(sc.dids.bob),\n      })\n\n      const moderationStatusOnBobsAccount = await modClient.queryStatuses({\n        subject: sc.dids.bob,\n      })\n\n      // Validate that subject status is set to review closed and takendown flag is on\n      expect(moderationStatusOnBobsAccount.subjectStatuses[0]).toMatchObject({\n        reviewState: REVIEWCLOSED,\n        takendown: true,\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n      })\n\n      // Cleanup\n      await modClient.performReverseTakedown({\n        subject: repoSubject(sc.dids.bob),\n      })\n    })\n\n    it('supports escalating a subject', async () => {\n      const alicesPostRef = sc.posts[sc.dids.alice][0].ref\n      const alicesPostSubject = {\n        $type: 'com.atproto.repo.strongRef',\n        uri: alicesPostRef.uri.toString(),\n        cid: alicesPostRef.cid.toString(),\n      }\n      await modClient.emitEvent(\n        {\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventEscalate',\n            comment: 'Y',\n          },\n          subject: alicesPostSubject,\n          createdBy: 'did:example:admin',\n        },\n        'triage',\n      )\n\n      const alicesPostStatus = await modClient.queryStatuses({\n        subject: alicesPostRef.uri.toString(),\n      })\n\n      expect(alicesPostStatus.subjectStatuses[0]).toMatchObject({\n        reviewState: REVIEWESCALATED,\n        takendown: false,\n        subject: alicesPostSubject,\n      })\n    })\n\n    it('adds persistent comment on subject through comment event', async () => {\n      const alicesPostRef = sc.posts[sc.dids.alice][0].ref\n      const alicesPostSubject = {\n        $type: 'com.atproto.repo.strongRef',\n        uri: alicesPostRef.uri.toString(),\n        cid: alicesPostRef.cid.toString(),\n      }\n      await modClient.emitEvent(\n        {\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventComment',\n            sticky: true,\n            comment: 'This is a persistent note',\n          },\n          subject: alicesPostSubject,\n          createdBy: 'did:example:admin',\n        },\n        'triage',\n      )\n\n      const alicesPostStatus = await modClient.queryStatuses({\n        subject: alicesPostRef.uri.toString(),\n      })\n\n      expect(alicesPostStatus.subjectStatuses[0].comment).toEqual(\n        'This is a persistent note',\n      )\n    })\n\n    it('reverses status when revert event is triggered.', async () => {\n      const alicesPostRef = sc.posts[sc.dids.alice][0].ref\n      const emitModEvent = async (\n        event: ToolsOzoneModerationEmitEvent.InputSchema['event'],\n        overwrites: Partial<ToolsOzoneModerationEmitEvent.InputSchema> = {},\n      ) => {\n        const baseAction = {\n          subject: {\n            $type: 'com.atproto.repo.strongRef',\n            uri: alicesPostRef.uriStr,\n            cid: alicesPostRef.cidStr,\n          },\n          createdBy: 'did:example:admin',\n        }\n        return modClient.emitEvent({\n          event,\n          ...baseAction,\n          ...overwrites,\n        })\n      }\n      // Validate that subject status is marked as escalated\n      await emitModEvent({\n        $type: 'tools.ozone.moderation.defs#modEventReport',\n        reportType: REASONSPAM,\n      })\n      await emitModEvent({\n        $type: 'tools.ozone.moderation.defs#modEventReport',\n        reportType: REASONMISLEADING,\n      })\n      await emitModEvent({\n        $type: 'tools.ozone.moderation.defs#modEventEscalate',\n      })\n      const alicesPostStatusAfterEscalation = await modClient.queryStatuses({\n        subject: alicesPostRef.uriStr,\n      })\n      expect(\n        alicesPostStatusAfterEscalation.subjectStatuses[0].reviewState,\n      ).toEqual(REVIEWESCALATED)\n\n      // Validate that subject status is marked as takendown\n\n      await emitModEvent({\n        $type: 'tools.ozone.moderation.defs#modEventLabel',\n        createLabelVals: ['nsfw'],\n        negateLabelVals: [],\n      })\n      await emitModEvent({\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n      })\n\n      const alicesPostStatusAfterTakedown = await modClient.queryStatuses({\n        subject: alicesPostRef.uriStr,\n      })\n      expect(alicesPostStatusAfterTakedown.subjectStatuses[0]).toMatchObject({\n        reviewState: REVIEWCLOSED,\n        takendown: true,\n      })\n\n      await emitModEvent({\n        $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n      })\n      const alicesPostStatusAfterRevert = await modClient.queryStatuses({\n        subject: alicesPostRef.uriStr,\n      })\n      // Validate that after reverting, the status of the subject is reverted to the last status changing event\n      expect(alicesPostStatusAfterRevert.subjectStatuses[0]).toMatchObject({\n        reviewState: REVIEWCLOSED,\n        takendown: false,\n      })\n      // Validate that after reverting, the last review date of the subject\n      // DOES NOT update to the the last status changing event\n      expect(\n        new Date(\n          alicesPostStatusAfterEscalation.subjectStatuses[0]\n            .lastReviewedAt as string,\n        ) <\n          new Date(\n            alicesPostStatusAfterRevert.subjectStatuses[0]\n              .lastReviewedAt as string,\n          ),\n      ).toBeTruthy()\n    })\n\n    it('negates an existing label.', async () => {\n      const { ctx } = ozone\n      const post = sc.posts[sc.dids.bob][0].ref\n      const bobsPostSubject = {\n        $type: 'com.atproto.repo.strongRef',\n        uri: post.uriStr,\n        cid: post.cidStr,\n      }\n      const modService = ctx.modService(ctx.db)\n      await modService.formatAndCreateLabels(post.uriStr, post.cidStr, {\n        create: ['kittens'],\n      })\n      await emitLabelEvent({\n        negateLabelVals: ['kittens'],\n        createLabelVals: [],\n        subject: bobsPostSubject,\n      })\n      await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])\n\n      await emitLabelEvent({\n        createLabelVals: ['kittens'],\n        negateLabelVals: [],\n        subject: bobsPostSubject,\n      })\n      await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['kittens'])\n      // Cleanup\n      await modService.formatAndCreateLabels(post.uriStr, post.cidStr, {\n        negate: ['kittens'],\n      })\n    })\n\n    it('no-ops when negating an already-negated label and reverses.', async () => {\n      const { ctx } = ozone\n      const post = sc.posts[sc.dids.bob][0].ref\n      const modService = ctx.modService(ctx.db)\n      await emitLabelEvent({\n        negateLabelVals: ['bears'],\n        createLabelVals: [],\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.uriStr,\n          cid: post.cidStr,\n        },\n      })\n      await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])\n      await emitLabelEvent({\n        createLabelVals: ['bears'],\n        negateLabelVals: [],\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.uriStr,\n          cid: post.cidStr,\n        },\n      })\n      await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['bears'])\n      // Cleanup\n      await modService.formatAndCreateLabels(post.uriStr, post.cidStr, {\n        negate: ['bears'],\n      })\n    })\n\n    it('creates non-existing labels and reverses.', async () => {\n      const post = sc.posts[sc.dids.bob][0].ref\n      await emitLabelEvent({\n        createLabelVals: ['puppies', 'doggies'],\n        negateLabelVals: [],\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.uriStr,\n          cid: post.cidStr,\n        },\n      })\n      await expect(getRecordLabels(post.uriStr)).resolves.toEqual([\n        'puppies',\n        'doggies',\n      ])\n      await emitLabelEvent({\n        negateLabelVals: ['puppies', 'doggies'],\n        createLabelVals: [],\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.uriStr,\n          cid: post.cidStr,\n        },\n      })\n      await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])\n    })\n\n    it('creates labels on a repo and reverses.', async () => {\n      await emitLabelEvent({\n        createLabelVals: ['puppies', 'doggies'],\n        negateLabelVals: [],\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n      })\n      await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([\n        'puppies',\n        'doggies',\n      ])\n      await emitLabelEvent({\n        negateLabelVals: ['puppies', 'doggies'],\n        createLabelVals: [],\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n      })\n      await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([])\n    })\n\n    it('creates and negates labels on a repo and reverses.', async () => {\n      const { ctx } = ozone\n      const modService = ctx.modService(ctx.db)\n      await modService.formatAndCreateLabels(sc.dids.bob, null, {\n        create: ['kittens'],\n      })\n      await emitLabelEvent({\n        createLabelVals: ['puppies'],\n        negateLabelVals: ['kittens'],\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n      })\n      await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['puppies'])\n\n      await emitLabelEvent({\n        negateLabelVals: ['puppies'],\n        createLabelVals: ['kittens'],\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n      })\n      await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens'])\n    })\n\n    it('creates expiring label', async () => {\n      await emitLabelEvent({\n        createLabelVals: ['temp'],\n        negateLabelVals: [],\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n        durationInHours: 24,\n      })\n      const repo = await getRepo(sc.dids.bob)\n      // Losely check that the expiry date is set to above 23 hours from now\n      expect(\n        `${repo?.labels?.[0].exp}` >\n          new Date(Date.now() + 23 * HOUR).toISOString(),\n      ).toBeTruthy()\n    })\n\n    it('does not allow triage moderators to label.', async () => {\n      const attemptLabel = modClient.emitEvent(\n        {\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventLabel',\n            negateLabelVals: ['a'],\n            createLabelVals: ['b', 'c'],\n          },\n          createdBy: 'did:example:moderator',\n          reason: 'Y',\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n        },\n        'triage',\n      )\n      await expect(attemptLabel).rejects.toThrow(\n        'Must be a full moderator to label content',\n      )\n    })\n\n    it('does not allow take down event on takendown post or reverse takedown on available post.', async () => {\n      await modClient.performTakedown({\n        subject: repoSubject(sc.dids.bob),\n      })\n      await expect(\n        modClient.performTakedown({\n          subject: repoSubject(sc.dids.bob),\n        }),\n      ).rejects.toThrow('Subject is already taken down')\n\n      // Cleanup\n      await modClient.performReverseTakedown({\n        subject: repoSubject(sc.dids.bob),\n      })\n      await expect(\n        modClient.performReverseTakedown({\n          subject: repoSubject(sc.dids.bob),\n        }),\n      ).rejects.toThrow('Subject is not taken down')\n    })\n\n    it('fans out repo takedowns', async () => {\n      await modClient.performTakedown({\n        subject: repoSubject(sc.dids.bob),\n      })\n      await ozone.processAll()\n\n      const pdsRes1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: sc.dids.bob,\n        },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      expect(pdsRes1.data.takedown?.applied).toBe(true)\n\n      const bskyRes1 = await bskyAgent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: sc.dids.bob,\n        },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      expect(bskyRes1.data.takedown?.applied).toBe(true)\n\n      const takedownLabel1 = await getLabel(sc.dids.bob, TAKEDOWN_LABEL)\n      expect(takedownLabel1).toBeDefined()\n\n      // cleanup\n      await modClient.performReverseTakedown({\n        subject: repoSubject(sc.dids.bob),\n      })\n      await ozone.processAll()\n\n      const pdsRes2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: sc.dids.bob,\n        },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      expect(pdsRes2.data.takedown?.applied).toBe(false)\n\n      const bskyRes2 = await bskyAgent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: sc.dids.bob,\n        },\n        { headers: network.bsky.adminAuthHeaders() },\n      )\n      expect(bskyRes2.data.takedown?.applied).toBe(false)\n\n      const takedownLabel2 = await getLabel(sc.dids.bob, TAKEDOWN_LABEL)\n      expect(takedownLabel2).toBeUndefined()\n    })\n\n    it('allows full moderators to takedown.', async () => {\n      await modClient.emitEvent(\n        {\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTakedown',\n          },\n          createdBy: 'did:example:moderator',\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n        },\n        'moderator',\n      )\n      // cleanup\n      await reverse({\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n      })\n    })\n\n    it('does not allow non-full moderators to takedown.', async () => {\n      const attemptTakedownTriage = modClient.emitEvent(\n        {\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTakedown',\n          },\n          createdBy: 'did:example:moderator',\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n        },\n        'triage',\n      )\n      await expect(attemptTakedownTriage).rejects.toThrow(\n        'Must be a full moderator to take this type of action',\n      )\n    })\n\n    it('automatically reverses actions marked with duration', async () => {\n      await sc.createReport({\n        reasonType: REASONSPAM,\n        subject: repoSubject(sc.dids.bob),\n        reportedBy: sc.dids.alice,\n      })\n      const action = await modClient.performTakedown({\n        subject: repoSubject(sc.dids.bob),\n        // Use negative value to set the expiry time in the past so that the action is automatically reversed\n        // right away without having to wait n number of hours for a successful assertion\n        durationInHours: -1,\n      })\n      await ozone.processAll()\n\n      const statusesAfterTakedown = await modClient.queryStatuses(\n        { subject: sc.dids.bob },\n        'moderator',\n      )\n\n      expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({\n        takendown: true,\n      })\n\n      // In the actual app, this will be instantiated and run on server startup\n      const reverser = new EventReverser(\n        network.ozone.ctx.db,\n        // @ts-expect-error Error due to circular dependency with the dev-env package\n        network.ozone.ctx.modService,\n      )\n      await reverser.findAndRevertDueActions()\n      await ozone.processAll()\n\n      const [eventList, statuses] = await Promise.all([\n        modClient.queryEvents({ subject: sc.dids.bob }, 'moderator'),\n        modClient.queryStatuses({ subject: sc.dids.bob }, 'moderator'),\n      ])\n\n      expect(statuses.subjectStatuses[0]).toMatchObject({\n        takendown: false,\n        reviewState: REVIEWCLOSED,\n      })\n      // Verify that the automatic reversal is attributed to the original moderator of the temporary action\n      // and that the reason is set to indicate that the action was automatically reversed.\n      expect(eventList.events[0]).toMatchObject({\n        createdBy: action.createdBy,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n          comment:\n            '[SCHEDULED_REVERSAL] Reverting action as originally scheduled',\n        },\n      })\n    })\n\n    async function emitLabelEvent(\n      opts: Partial<ToolsOzoneModerationEmitEvent.InputSchema> & {\n        subject: ToolsOzoneModerationEmitEvent.InputSchema['subject']\n        createLabelVals: ModEventLabel['createLabelVals']\n        negateLabelVals: ModEventLabel['negateLabelVals']\n        durationInHours?: ModEventLabel['durationInHours']\n      },\n    ) {\n      const { createLabelVals, negateLabelVals, durationInHours } = opts\n      const event = await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventLabel',\n          createLabelVals,\n          negateLabelVals,\n          durationInHours,\n        },\n        createdBy: 'did:example:admin',\n        reason: 'Y',\n        ...opts,\n      })\n      return event\n    }\n\n    async function reverse(\n      opts: Partial<ToolsOzoneModerationEmitEvent.InputSchema> & {\n        subject: ToolsOzoneModerationEmitEvent.InputSchema['subject']\n      },\n    ) {\n      await modClient.emitEvent({\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        },\n        createdBy: 'did:example:admin',\n        reason: 'Y',\n        ...opts,\n      })\n    }\n\n    async function getRecordLabels(uri: string) {\n      const result = await agent.tools.ozone.moderation.getRecord(\n        { uri },\n        {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneModerationGetRecord,\n          ),\n        },\n      )\n      const labels = result.data.labels ?? []\n      return labels.map((l) => l.val)\n    }\n\n    async function getRepo(did: string) {\n      const result = await agent.tools.ozone.moderation.getRepo(\n        { did },\n        {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneModerationGetRepo,\n          ),\n        },\n      )\n      return result.data\n    }\n\n    async function getRepoLabels(did: string) {\n      const result = await getRepo(did)\n      const labels = result.labels ?? []\n      return labels.map((l) => l.val)\n    }\n  })\n\n  describe('blob takedown', () => {\n    let post: { ref: RecordRef; images: ImageRef[] }\n    let blob: ImageRef\n    let imageUri: string\n    beforeAll(async () => {\n      const { ctx } = network.bsky\n      post = sc.posts[sc.dids.carol][0]\n      blob = post.images[1]\n      imageUri = ctx.views.imgUriBuilder\n        .getPresetUri(\n          'feed_thumbnail',\n          sc.dids.carol,\n          blob.image.ref.toString(),\n        )\n        .replace(ctx.cfg.publicUrl || '', network.bsky.url)\n      // Warm image server cache\n      await fetch(imageUri)\n      const cached = await fetch(imageUri)\n      expect(cached.headers.get('x-cache')).toEqual('hit')\n      await modClient.performTakedown({\n        subject: recordSubject(post.ref),\n        subjectBlobCids: [blob.image.ref.toString()],\n      })\n      await ozone.processAll()\n    })\n\n    it('sets blobCids in moderation status', async () => {\n      const { subjectStatuses } = await modClient.queryStatuses({\n        subject: post.ref.uriStr,\n      })\n\n      expect(subjectStatuses[0].subjectBlobCids).toEqual([\n        blob.image.ref.toString(),\n      ])\n    })\n\n    it('prevents resolution of blob', async () => {\n      const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}`\n      const resolveBlob = await fetch(`${network.bsky.url}${blobPath}`)\n      expect(resolveBlob.status).toEqual(404)\n      expect(await resolveBlob.json()).toEqual({\n        error: 'NotFoundError',\n        message: 'Blob not found',\n      })\n    })\n\n    // @TODO add back in with image invalidation, see bluesky-social/atproto#2087\n    it.skip('prevents image blob from being served, even when cached.', async () => {\n      const fetchImage = await fetch(imageUri)\n      expect(fetchImage.status).toEqual(404)\n      expect(await fetchImage.json()).toMatchObject({\n        message: 'Blob not found',\n      })\n    })\n\n    it('invalidates the image in the cdn', async () => {\n      const blobCid = blob.image.ref.toString()\n      expect(mockInvalidator.invalidated.length).toBe(1)\n      expect(mockInvalidator.invalidated.at(0)?.subject).toBe(blobCid)\n      expect(mockInvalidator.invalidated.at(0)?.paths.at(0)).toEqual(\n        `/path1/${sc.dids.carol}/${blobCid}`,\n      )\n      expect(mockInvalidator.invalidated.at(0)?.paths.at(1)).toEqual(\n        `/path2/${sc.dids.carol}/${blobCid}`,\n      )\n    })\n\n    it('fans takedown out to pds', async () => {\n      const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: sc.dids.carol,\n          blob: blob.image.ref.toString(),\n        },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      expect(res.data.takedown?.applied).toBe(true)\n    })\n\n    it('restores blob when action is reversed.', async () => {\n      await modClient.performReverseTakedown({\n        subject: recordSubject(post.ref),\n        subjectBlobCids: [blob.image.ref.toString()],\n      })\n\n      await ozone.processAll()\n\n      // Can resolve blob\n      const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}`\n      const resolveBlob = await fetch(`${network.bsky.url}${blobPath}`)\n      expect(resolveBlob.status).toEqual(200)\n\n      // Can fetch through image server\n      const fetchImage = await fetch(imageUri)\n      expect(fetchImage.status).toEqual(200)\n      const size = Number(fetchImage.headers.get('content-length'))\n      expect(size).toBeGreaterThan(9000)\n    })\n\n    it('fans reversal out to pds', async () => {\n      const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: sc.dids.carol,\n          blob: blob.image.ref.toString(),\n        },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      expect(res.data.takedown?.applied).toBe(false)\n    })\n  })\n})\n\nclass MockInvalidator implements ImageInvalidator {\n  invalidated: { subject: string; paths: string[] }[] = []\n\n  async invalidate(subject: string, paths: string[]) {\n    this.invalidated.push({ subject, paths })\n  }\n}\n"
  },
  {
    "path": "packages/ozone/tests/protected-tags.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport {\n  ROLEADMIN,\n  ROLEMODERATOR,\n} from '../dist/lexicon/types/tools/ozone/team/defs'\nimport { ProtectedTagSettingKey } from '../src/setting/constants'\n\ndescribe('protected-tags', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  const basicSetting = {\n    key: ProtectedTagSettingKey,\n    scope: 'instance',\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_protected_tags',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('Settings management', () => {\n    it('validates settings', async () => {\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEMODERATOR,\n          value: {\n            vip: {},\n          },\n        }),\n      ).rejects.toThrow(\n        'Only admins should be able to configure protected tags',\n      )\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEADMIN,\n          // @ts-expect-error testing invalid value here\n          value: ['test'],\n        }),\n      ).rejects.toThrow('Invalid configuration')\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEADMIN,\n          value: { vip: 'test' },\n        }),\n      ).rejects.toThrow('Invalid configuration')\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEADMIN,\n          value: { vip: { weirdValue: 1 } },\n        }),\n      ).rejects.toThrow(/Must define who a list of moderators or a role/gi)\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEADMIN,\n          value: { vip: { roles: 'test' } },\n        }),\n      ).rejects.toThrow(/Roles must be an array of moderator/gi)\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEADMIN,\n          value: { vip: { roles: 'test' } },\n        }),\n      ).rejects.toThrow(/Roles must be an array of moderator/gi)\n      await expect(\n        modClient.upsertSettingOption({\n          ...basicSetting,\n          managerRole: ROLEADMIN,\n          value: { vip: { moderators: 1 } },\n        }),\n      ).rejects.toThrow(/Moderators must be an array of moderator/gi)\n    })\n  })\n  describe('Protected subject via tags', () => {\n    afterEach(async () => {\n      await modClient.removeSettingOptions({\n        keys: [ProtectedTagSettingKey],\n        scope: 'instance',\n      })\n    })\n    it('only allows configured roles to add/remove protected tags', async () => {\n      await modClient.upsertSettingOption({\n        ...basicSetting,\n        managerRole: ROLEADMIN,\n        value: { vip: { roles: ['tools.ozone.team.defs#roleAdmin'] } },\n      })\n\n      await expect(\n        modClient.emitEvent({\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['vip'],\n            remove: [],\n          },\n        }),\n      ).rejects.toThrow(/Can not manage tag vip/gi)\n\n      await modClient.emitEvent(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['vip'],\n            remove: [],\n          },\n        },\n        'admin',\n      )\n      await expect(\n        modClient.emitEvent({\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: [],\n            remove: ['vip'],\n          },\n        }),\n      ).rejects.toThrow(/Can not manage tag vip/gi)\n\n      // Verify that since admins are configured to manage this tag, admin actions go through\n      const removeTag = await modClient.emitEvent(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTakedown',\n          },\n        },\n        'admin',\n      )\n\n      expect(removeTag.id).toBeTruthy()\n    })\n    it('only allows configured moderators to add/remove protected tags', async () => {\n      await modClient.upsertSettingOption({\n        ...basicSetting,\n        managerRole: ROLEADMIN,\n        value: { vip: { moderators: [network.ozone.adminAccnt.did] } },\n      })\n\n      // By default, this query is made with moderator account's did\n      await expect(\n        modClient.emitEvent({\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['vip'],\n            remove: [],\n          },\n        }),\n      ).rejects.toThrow(/Not allowed to manage tag: vip/gi)\n\n      await modClient.emitEvent(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['vip'],\n            remove: [],\n          },\n        },\n        'admin',\n      )\n\n      await expect(\n        modClient.emitEvent({\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: [],\n            remove: ['vip'],\n          },\n        }),\n      ).rejects.toThrow(/Not allowed to manage tag: vip/gi)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/query-labels.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { cborEncode } from '@atproto/common'\nimport { Secp256k1Keypair, verifySignature } from '@atproto/crypto'\nimport { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env'\nimport { DisconnectError } from '@atproto/ws-client'\nimport { Subscription } from '@atproto/xrpc-server'\nimport { ids, lexicons } from '../src/lexicon/lexicons'\nimport { Label } from '../src/lexicon/types/com/atproto/label/defs'\nimport {\n  OutputSchema as LabelMessage,\n  isLabels,\n} from '../src/lexicon/types/com/atproto/label/subscribeLabels'\nimport { ModerationService } from '../src/mod-service'\nimport { getSigningKeyId } from '../src/util'\n\ndescribe('ozone query labels', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n\n  let labels: Label[]\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_query_labels',\n    })\n\n    agent = network.ozone.getClient()\n\n    const toCreate = [\n      {\n        src: EXAMPLE_LABELER,\n        uri: 'did:example:blah',\n        val: 'spam',\n        cts: new Date().toISOString(),\n      },\n      {\n        src: EXAMPLE_LABELER,\n        uri: 'did:example:blah',\n        val: 'impersonation',\n        cts: new Date().toISOString(),\n      },\n      {\n        src: EXAMPLE_LABELER,\n        uri: 'at://did:example:blah/app.bsky.feed.post/1234abcde',\n        val: 'spam',\n        cts: new Date().toISOString(),\n      },\n      {\n        src: EXAMPLE_LABELER,\n        uri: 'at://did:example:blah/app.bsky.feed.post/1234abcfg',\n        val: 'spam',\n        cts: new Date().toISOString(),\n      },\n      {\n        src: EXAMPLE_LABELER,\n        uri: 'at://did:example:blah/app.bsky.actor.profile/self',\n        val: 'spam',\n        cts: new Date().toISOString(),\n      },\n      {\n        src: EXAMPLE_LABELER,\n        uri: 'did:example:thing',\n        val: 'spam',\n        cts: new Date().toISOString(),\n      },\n    ]\n\n    const modService = network.ozone.ctx.modService(network.ozone.ctx.db)\n    labels = await modService.createLabels(toCreate)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('returns all labels', async () => {\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['*'],\n    })\n    expect(res.data.labels).toEqual(labels)\n  })\n\n  it('returns all labels even when an additional pattern is supplied', async () => {\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['*', 'did:example:blah'],\n    })\n    expect(res.data.labels).toEqual(labels)\n  })\n\n  it('returns all labels that match an exact uri pattern', async () => {\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['did:example:blah'],\n    })\n    expect(res.data.labels).toEqual(labels.slice(0, 2))\n  })\n\n  it('returns all labels that match one of multiple exact uris', async () => {\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: [\n        'at://did:example:blah/app.bsky.feed.post/1234abcfg',\n        'at://did:example:blah/app.bsky.actor.profile/self',\n      ],\n    })\n    expect(res.data.labels).toEqual(labels.slice(3, 5))\n  })\n\n  it('returns all labels that match one of multiple uris, exact & glob', async () => {\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],\n    })\n    expect(res.data.labels).toEqual(labels.slice(0, 5))\n  })\n\n  it('paginates', async () => {\n    const res1 = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],\n      limit: 3,\n    })\n    const res2 = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],\n      limit: 3,\n      cursor: res1.data.cursor,\n    })\n\n    expect([...res1.data.labels, ...res2.data.labels]).toEqual(\n      labels.slice(0, 5),\n    )\n  })\n\n  it('returns validly signed labels', async () => {\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['*'],\n    })\n    const signingKey = network.ozone.ctx.signingKey.did()\n    for (const label of res.data.labels) {\n      const { sig, ...rest } = label\n      if (!sig) {\n        throw new Error('Missing signature')\n      }\n      const encodedLabel = cborEncode(rest)\n      const isValid = await verifySignature(signingKey, encodedLabel, sig)\n      expect(isValid).toBe(true)\n    }\n  })\n\n  it('resigns labels if the signingKey changes', async () => {\n    // mock changing the signing key for the service\n    const ctx = network.ozone.ctx\n    const origModServiceFn = ctx.modService\n\n    const modSrvc = ctx.modService(ctx.db)\n    const newSigningKey = await Secp256k1Keypair.create()\n    const newSigningKeyId = await getSigningKeyId(ctx.db, newSigningKey.did())\n    ctx.devOverride({\n      // @ts-ignore\n      modService: ModerationService.creator(\n        newSigningKey,\n        newSigningKeyId,\n        ctx.cfg,\n        // @ts-ignore\n        modSrvc.backgroundQueue,\n        ctx.idResolver,\n        // @ts-ignore\n        modSrvc.eventPusher,\n        modSrvc.appviewAgent,\n        ctx.serviceAuthHeaders,\n        ctx.strikeService,\n      ),\n    })\n\n    const res = await agent.api.com.atproto.label.queryLabels({\n      uriPatterns: ['*'],\n    })\n    for (const label of res.data.labels) {\n      const { sig, ...rest } = label\n      if (!sig) {\n        throw new Error('Missing signature')\n      }\n      const encodedLabel = cborEncode(rest)\n      const isValid = await verifySignature(\n        newSigningKey.did(),\n        encodedLabel,\n        sig,\n      )\n      expect(isValid).toBe(true)\n    }\n\n    await network.ozone.processAll()\n\n    const fromDb = await ctx.db.db.selectFrom('label').selectAll().execute()\n    expect(fromDb.every((row) => row.signingKeyId === newSigningKeyId)).toBe(\n      true,\n    )\n\n    ctx.devOverride({\n      modService: origModServiceFn,\n    })\n  })\n\n  describe('subscribeLabels', () => {\n    it('streams all labels from initial cursor.', async () => {\n      const ac = new AbortController()\n      let doneTimer: NodeJS.Timeout\n      const resetDoneTimer = () => {\n        clearTimeout(doneTimer)\n        doneTimer = setTimeout(() => ac.abort(new DisconnectError()), 100)\n      }\n      const sub = new Subscription({\n        signal: ac.signal,\n        service: agent.service.origin.replace('http://', 'ws://'),\n        method: ids.ComAtprotoLabelSubscribeLabels,\n        getParams() {\n          return { cursor: 0 }\n        },\n        validate(obj) {\n          return lexicons.assertValidXrpcMessage<LabelMessage>(\n            ids.ComAtprotoLabelSubscribeLabels,\n            obj,\n          )\n        },\n      })\n      const streamedLabels: Label[] = []\n      for await (const message of sub) {\n        resetDoneTimer()\n        if (isLabels(message)) {\n          for (const label of message.labels) {\n            // sigs are currently parsed as a Buffer which is a Uint8Array under the hood, but fails our equality test so we cast to Uint8Array\n            streamedLabels.push({\n              ...label,\n              sig: label.sig ? new Uint8Array(label.sig) : undefined,\n            })\n          }\n        }\n      }\n      expect(streamedLabels).toEqual(labels)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/record-and-account-events.test.ts",
    "content": "import assert from 'node:assert'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { isRepoRef } from '../src/lexicon/types/com/atproto/admin/defs'\nimport { REASONMISLEADING } from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { isMain as isStrongRef } from '../src/lexicon/types/com/atproto/repo/strongRef'\nimport {\n  REVIEWOPEN,\n  SubjectStatusView,\n  isAccountHosting,\n  isRecordHosting,\n} from '../src/lexicon/types/tools/ozone/moderation/defs'\nimport { InputSchema } from '../src/lexicon/types/tools/ozone/moderation/emitEvent'\n\ndescribe('record and account events on moderation subjects', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_record_and_account_events',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getSubjectStatus = async (\n    subject: string,\n  ): Promise<SubjectStatusView | undefined> => {\n    const res = await modClient.queryStatuses({\n      subject,\n    })\n    return res.subjectStatuses[0]\n  }\n\n  describe('record events', () => {\n    const emitRecordEvent = async (\n      subject: InputSchema['subject'],\n      op: 'create' | 'update' | 'delete',\n    ) => {\n      return await modClient.emitEvent(\n        {\n          event: {\n            op,\n            timestamp: new Date().toISOString(),\n            $type: 'tools.ozone.moderation.defs#recordEvent',\n          },\n          subject,\n          createdBy: 'did:example:admin',\n        },\n        'admin',\n      )\n    }\n\n    it('saves updated and deleted timestamps and record status', async () => {\n      const bobsPostSubject = {\n        $type: 'com.atproto.repo.strongRef',\n        uri: sc.posts[sc.dids.bob][1].ref.uriStr,\n        cid: sc.posts[sc.dids.bob][1].ref.cidStr,\n      }\n\n      await sc.createReport({\n        reportedBy: sc.dids.carol,\n        reasonType: REASONMISLEADING,\n        reason: 'misleading',\n        subject: bobsPostSubject,\n      })\n\n      await emitRecordEvent(bobsPostSubject, 'update')\n      const statusAfterUpdate = await getSubjectStatus(bobsPostSubject.uri)\n      assert(isRecordHosting(statusAfterUpdate?.hosting))\n      expect(statusAfterUpdate.hosting?.updatedAt).toBeTruthy()\n\n      await emitRecordEvent(bobsPostSubject, 'delete')\n      const statusAfterDelete = await getSubjectStatus(bobsPostSubject.uri)\n      assert(isRecordHosting(statusAfterDelete?.hosting))\n      expect(statusAfterDelete.hosting?.deletedAt).toBeTruthy()\n      expect(statusAfterDelete.hosting?.status).toEqual('deleted')\n      // Ensure that due to delete or update event, review state does not change\n      expect(statusAfterDelete.reviewState).toEqual(REVIEWOPEN)\n    })\n  })\n  describe('account/identity events', () => {\n    const emitAccountEvent = async (\n      subject: InputSchema['subject'],\n      active: boolean,\n      status?: 'takendown' | 'deleted' | 'deactivated' | 'suspended',\n    ) => {\n      return await modClient.emitEvent(\n        {\n          event: {\n            status,\n            active,\n            timestamp: new Date().toISOString(),\n            $type: 'tools.ozone.moderation.defs#accountEvent',\n          },\n          subject,\n          createdBy: 'did:example:admin',\n        },\n        'admin',\n      )\n    }\n\n    it('saves updated and deleted timestamps and account status', async () => {\n      const carolsAccountSubject = {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids.carol,\n      }\n\n      await sc.createReport({\n        reportedBy: sc.dids.carol,\n        reasonType: REASONMISLEADING,\n        reason: 'misleading',\n        subject: carolsAccountSubject,\n      })\n\n      await emitAccountEvent(carolsAccountSubject, false, 'deactivated')\n      const statusAfterDeactivation = await getSubjectStatus(\n        carolsAccountSubject.did,\n      )\n      assert(isAccountHosting(statusAfterDeactivation?.hosting))\n      expect(statusAfterDeactivation.hosting.deactivatedAt).toBeTruthy()\n      expect(statusAfterDeactivation.hosting.status).toEqual('deactivated')\n      expect(statusAfterDeactivation.reviewState).toEqual(REVIEWOPEN)\n\n      await emitAccountEvent(carolsAccountSubject, true)\n      const statusAfterReactivation = await getSubjectStatus(\n        carolsAccountSubject.did,\n      )\n      assert(isAccountHosting(statusAfterReactivation?.hosting))\n      expect(statusAfterReactivation.hosting.updatedAt).toBeTruthy()\n      expect(statusAfterReactivation.hosting.status).toEqual('active')\n      expect(statusAfterReactivation.hosting.deletedAt).toBeFalsy()\n    })\n\n    it('gets statuses by hosting properties', async () => {\n      await Promise.all([\n        emitAccountEvent(\n          {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.carol,\n          },\n          false,\n          'deactivated',\n        ),\n        emitAccountEvent(\n          {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n          false,\n          'deleted',\n        ),\n      ])\n      const [\n        { subjectStatuses: deactivatedOrDeletedStatuses },\n        { subjectStatuses: deletedStatusesInPastDay },\n        { subjectStatuses: deletedStatusesBeforeYesterday },\n      ] = await Promise.all([\n        modClient.queryStatuses({\n          hostingStatuses: ['deactivated', 'deleted'],\n        }),\n        modClient.queryStatuses({\n          hostingDeletedAfter: new Date(\n            Date.now() - 1000 * 60 * 60 * 24,\n          ).toISOString(),\n        }),\n        modClient.queryStatuses({\n          hostingDeletedBefore: new Date(\n            Date.now() - 1000 * 60 * 60 * 24,\n          ).toISOString(),\n        }),\n      ])\n\n      expect(deactivatedOrDeletedStatuses.length).toEqual(3)\n      expect(deletedStatusesInPastDay.length).toEqual(2)\n      assert(isStrongRef(deletedStatusesInPastDay[0]?.subject))\n      expect(deletedStatusesInPastDay[0]?.subject.uri).toEqual(\n        sc.posts[sc.dids.bob][1].ref.uriStr,\n      )\n      assert(isRepoRef(deletedStatusesInPastDay[1]?.subject))\n      expect(deletedStatusesInPastDay[1]?.subject.did).toEqual(sc.dids.bob)\n      expect(deletedStatusesBeforeYesterday.length).toEqual(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/repo-search.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  usersBulkSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { OutputSchema as SearchReposOutputSchema } from '../src/lexicon/types/tools/ozone/moderation/searchRepos'\nimport { paginateAll } from './_util'\n\ndescribe('admin repo search view', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  let headers: { [s: string]: string }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_admin_repo_search',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await usersBulkSeed(sc)\n    headers = await network.ozone.modHeaders(\n      ids.ToolsOzoneModerationSearchRepos,\n    )\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  beforeAll(async () => {\n    await modClient.emitEvent({\n      event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: sc.dids['cara-wiegand69.test'],\n      },\n    })\n    await network.ozone.processAll()\n  })\n\n  it('gives relevant results', async () => {\n    const result = await agent.api.tools.ozone.moderation.searchRepos(\n      { term: 'car' },\n      { headers },\n    )\n\n    const handles = result.data.repos.map((u) => u.handle)\n\n    const shouldContain = [\n      'cara-wiegand69.test', // Present despite repo takedown\n      'carlos6.test',\n      'carolina-mcderm77.test',\n    ]\n\n    shouldContain.forEach((handle) => expect(handles).toContain(handle))\n\n    const shouldNotContain = [\n      'sven70.test',\n      'hilario84.test',\n      'santa-hermann78.test',\n      'dylan61.test',\n      'preston-harris.test',\n      'loyce95.test',\n      'melyna-zboncak.test',\n    ]\n\n    shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle))\n  })\n\n  it('finds repo by did', async () => {\n    const term = sc.dids['cara-wiegand69.test']\n    const res = await agent.api.tools.ozone.moderation.searchRepos(\n      { term },\n      { headers },\n    )\n\n    expect(res.data.repos.length).toEqual(1)\n    expect(res.data.repos[0].did).toEqual(term)\n  })\n\n  it('paginates with term', async () => {\n    const results = (results: SearchReposOutputSchema[]) =>\n      results.flatMap((res) => res.repos)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.tools.ozone.moderation.searchRepos(\n        { term: 'p', cursor, limit: 3 },\n        { headers },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator)\n    paginatedAll.forEach((res) =>\n      expect(res.repos.length).toBeLessThanOrEqual(3),\n    )\n\n    const full = await agent.api.tools.ozone.moderation.searchRepos(\n      { term: 'p' },\n      { headers },\n    )\n\n    expect(full.data.repos.length).toBeGreaterThan(3)\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n\n  it('paginates without term', async () => {\n    const results = (results: SearchReposOutputSchema[]) =>\n      results.flatMap((res) => res.repos)\n    const paginator = async (cursor?: string) => {\n      const res = await agent.api.tools.ozone.moderation.searchRepos(\n        { cursor, limit: 3 },\n        { headers },\n      )\n      return res.data\n    }\n\n    const paginatedAll = await paginateAll(paginator, 5)\n    paginatedAll.forEach((res) =>\n      expect(res.repos.length).toBeLessThanOrEqual(3),\n    )\n\n    const full = await agent.api.tools.ozone.moderation.searchRepos(\n      { limit: 15 },\n      { headers },\n    )\n\n    expect(results(paginatedAll)).toEqual(results([full.data]))\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/report-muting.test.ts",
    "content": "import {\n  ComAtprotoModerationDefs,\n  ToolsOzoneModerationDefs,\n} from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport {\n  REVIEWNONE,\n  REVIEWOPEN,\n} from '../src/lexicon/types/tools/ozone/moderation/defs'\n\ndescribe('report-muting', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_report_muting',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const assertSubjectStatus = async (\n    subject: string,\n    status?: string,\n  ): Promise<ToolsOzoneModerationDefs.SubjectStatusView | undefined> => {\n    const res = await modClient.queryStatuses({\n      subject,\n    })\n    expect(res.subjectStatuses[0]?.reviewState).toEqual(status)\n    return res.subjectStatuses[0]\n  }\n\n  it('does not change reviewState when muted reporter reports', async () => {\n    const bobsPostSubject = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[sc.dids.bob][1].ref.uriStr,\n      cid: sc.posts[sc.dids.bob][1].ref.cidStr,\n    }\n    const carolsAccountSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.carol,\n    }\n\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventMuteReporter',\n        durationInHours: 24,\n      },\n      subject: carolsAccountSubject,\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: ComAtprotoModerationDefs.REASONMISLEADING,\n      reason: 'misleading',\n      subject: bobsPostSubject,\n    })\n\n    // Verify that a subject status was not created for bob's post since the reporter was muted\n    await assertSubjectStatus(bobsPostSubject.uri, REVIEWNONE)\n    // Verify, however, that the event was logged\n    await modClient.queryEvents({\n      subject: bobsPostSubject.uri,\n    })\n\n    // Verify that reporting mute duration is stored for the reporter\n    const carolsStatus = await assertSubjectStatus(sc.dids.carol, REVIEWNONE)\n    expect(\n      new Date(`${carolsStatus?.muteReportingUntil}`).getTime(),\n    ).toBeGreaterThan(Date.now())\n\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventUnmuteReporter',\n      },\n      subject: carolsAccountSubject,\n    })\n    await sc.createReport({\n      reportedBy: sc.dids.carol,\n      reasonType: ComAtprotoModerationDefs.REASONMISLEADING,\n      reason: 'misleading',\n      subject: bobsPostSubject,\n    })\n\n    // Verify that a subject status was created for bob's post since the reporter was no longer muted\n    await assertSubjectStatus(bobsPostSubject.uri, REVIEWOPEN)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/report-reason.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport {\n  REASONRUDE,\n  REASONSPAM,\n} from '../src/lexicon/types/com/atproto/moderation/defs'\nimport { ModerationServiceProfile } from '../src/mod-service/profile'\nimport { forSnapshot } from './_util'\n\ndescribe('report reason', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let pdsAgent: AtpAgent\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_report',\n    })\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    // Login with ozone's service account owner and update the service profile definition\n    pdsAgent = network.pds.getClient()\n    await pdsAgent.login({\n      identifier: 'mod-authority.test',\n      password: 'hunter2',\n    })\n    await pdsAgent.com.atproto.repo.putRecord({\n      repo: network.ozone.ctx.cfg.service.did,\n      collection: 'app.bsky.labeler.service',\n      rkey: 'self',\n      record: {\n        policies: { labelValues: [] },\n        reasonTypes: ['tools.ozone.report.defs#reasonHarassmentTroll'],\n        createdAt: new Date().toISOString(),\n      },\n    })\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('createReport', () => {\n    it('only passes for allowed reason types', async () => {\n      await expect(\n        sc.createReport({\n          reasonType: 'tools.ozone.report.defs#reasonHarassmentFake',\n          subject: repoSubject(sc.dids.bob),\n          reportedBy: sc.dids.alice,\n        }),\n      ).rejects.toThrow('Invalid reason type')\n\n      const validReport = await sc.createReport({\n        reasonType: 'tools.ozone.report.defs#reasonHarassmentTroll',\n        subject: repoSubject(sc.dids.bob),\n        reportedBy: sc.dids.alice,\n      })\n\n      expect(forSnapshot(validReport)).toMatchSnapshot()\n    })\n  })\n  describe('ModerationServiceProfile', () => {\n    it('should validate against updated labeler profile when cache expires', async () => {\n      const moderationServiceProfile = new ModerationServiceProfile(\n        network.ozone.ctx.cfg,\n        network.ozone.ctx.appviewAgent,\n        500,\n      )\n\n      await expect(\n        moderationServiceProfile.validateReasonType(\n          'tools.ozone.report.defs#reasonHarassmentFake',\n        ),\n      ).rejects.toThrow('Invalid reason type')\n\n      // Update labeler profile to add the new reason type\n      await pdsAgent.com.atproto.repo.putRecord({\n        repo: network.ozone.ctx.cfg.service.did,\n        collection: 'app.bsky.labeler.service',\n        rkey: 'self',\n        record: {\n          policies: { labelValues: [] },\n          reasonTypes: ['tools.ozone.report.defs#reasonHarassmentFake'],\n          createdAt: new Date().toISOString(),\n        },\n      })\n      await network.processAll()\n\n      // immediately after the update, the reason type still fails due to cache\n      await expect(\n        moderationServiceProfile.validateReasonType(\n          'tools.ozone.report.defs#reasonHarassmentFake',\n        ),\n      ).rejects.toThrow('Invalid reason type')\n\n      // add some manual delay to ensure cache is expired and try again\n      await new Promise((resolve) => setTimeout(resolve, 500))\n      await expect(\n        moderationServiceProfile.validateReasonType(\n          'tools.ozone.report.defs#reasonHarassmentFake',\n        ),\n      ).resolves.toEqual('tools.ozone.report.defs#reasonHarassmentFake')\n    })\n\n    it('should validate mapped reason types', async () => {\n      const moderationServiceProfile = new ModerationServiceProfile(\n        network.ozone.ctx.cfg,\n        network.ozone.ctx.appviewAgent,\n        500,\n      )\n\n      // Set up labeler profile with old reason types only\n      await pdsAgent.com.atproto.repo.putRecord({\n        repo: network.ozone.ctx.cfg.service.did,\n        collection: 'app.bsky.labeler.service',\n        rkey: 'self',\n        record: {\n          policies: { labelValues: [] },\n          reasonTypes: [REASONSPAM, REASONRUDE],\n          createdAt: new Date().toISOString(),\n        },\n      })\n      await network.processAll()\n\n      await new Promise((resolve) => setTimeout(resolve, 500))\n\n      await expect(\n        moderationServiceProfile.validateReasonType(\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n        ),\n      ).resolves.toEqual('tools.ozone.report.defs#reasonMisleadingSpam')\n\n      // directly supported old reason types work\n      await expect(\n        moderationServiceProfile.validateReasonType(REASONSPAM),\n      ).resolves.toEqual(REASONSPAM)\n\n      // new reason types that don't map to supported old reason types are rejected\n      await expect(\n        moderationServiceProfile.validateReasonType(\n          'tools.ozone.report.defs#reasonViolenceThreats',\n        ),\n      ).rejects.toThrow('Invalid reason type')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/revoke-account-credentials.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\n\ndescribe('revoke account credentials event', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_revoke_account_credentials',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('fails on non account subjects and for non admins', async () => {\n    await expect(\n      modClient.emitEvent({\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: sc.posts[sc.dids.alice][0].ref.uriStr,\n          cid: sc.posts[sc.dids.alice][0].ref.cidStr,\n        },\n        event: {\n          $type: 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n          comment: 'user was hacked',\n        },\n      }),\n    ).rejects.toThrow('Invalid subject type')\n    await expect(\n      modClient.emitEvent({\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        event: {\n          $type: 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n          comment: 'user was hacked',\n        },\n      }),\n    ).rejects.toThrow('Must be an admin to revoke account credentials')\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/safelink.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { forSnapshot } from './_util'\n\ndescribe('safelink management', () => {\n  let network: TestNetwork\n  let adminAgent: AtpAgent\n  let modAgent: AtpAgent\n  let triageAgent: AtpAgent\n  let sc: SeedClient\n\n  const getAdminHeaders = async (route: string) => {\n    return {\n      headers: await network.ozone.modHeaders(route, 'admin'),\n    }\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_safelink_test',\n    })\n    adminAgent = network.ozone.getClient()\n    modAgent = network.ozone.getClient()\n    triageAgent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('addRule', () => {\n    const testRule = {\n      url: 'https://malicious-site.com',\n      pattern: 'domain',\n      action: 'block',\n      reason: 'phishing',\n      comment: 'Known phishing domain targeting users',\n    }\n\n    it('allows admins to add rules', async () => {\n      const { data: adminRule } = await adminAgent.tools.ozone.safelink.addRule(\n        testRule,\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n      expect(forSnapshot(adminRule)).toMatchSnapshot()\n    })\n\n    it('rejects triage role from adding rules', async () => {\n      await expect(\n        triageAgent.tools.ozone.safelink.addRule(testRule, {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneSafelinkAddRule,\n            'triage',\n          ),\n        }),\n      ).rejects.toThrow('Must be a moderator to add URL rules')\n    })\n\n    it('prevents duplicate rules for same URL/pattern combination', async () => {\n      await expect(\n        adminAgent.tools.ozone.safelink.addRule(\n          testRule,\n          await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n        ),\n      ).rejects.toThrow('A rule for this URL/domain already exists')\n    })\n\n    it('validates invalid pattern types', async () => {\n      await expect(\n        adminAgent.tools.ozone.safelink.addRule(\n          {\n            ...testRule,\n            url: 'https://new-site.com',\n            pattern: 'invalid-pattern',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n        ),\n      ).rejects.toThrow('Invalid safelink pattern type')\n    })\n\n    it('validates invalid action types', async () => {\n      await expect(\n        adminAgent.tools.ozone.safelink.addRule(\n          {\n            ...testRule,\n            url: 'https://new-site2.com',\n            action: 'invalid-action',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n        ),\n      ).rejects.toThrow('Invalid safelink action type')\n    })\n\n    it('validates invalid reason types', async () => {\n      await expect(\n        adminAgent.tools.ozone.safelink.addRule(\n          {\n            ...testRule,\n            url: 'https://new-site3.com',\n            reason: 'invalid-reason',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n        ),\n      ).rejects.toThrow('Invalid safelink reason type')\n    })\n  })\n\n  describe('updateRule', () => {\n    const updateTestRule = {\n      url: 'https://update-test.com',\n      pattern: 'domain',\n      action: 'warn',\n      reason: 'spam',\n      comment: 'Initially marked as spam',\n    }\n\n    beforeAll(async () => {\n      await adminAgent.tools.ozone.safelink.addRule(\n        updateTestRule,\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n    })\n\n    it('allows updating existing rules', async () => {\n      const updatedData = {\n        url: updateTestRule.url,\n        pattern: updateTestRule.pattern,\n        action: 'block',\n        reason: 'phishing',\n        comment: 'Updated: confirmed phishing site',\n      }\n\n      const { data: updated } = await modAgent.tools.ozone.safelink.updateRule(\n        updatedData,\n        await getAdminHeaders(ids.ToolsOzoneSafelinkUpdateRule),\n      )\n      const { data: queried } = await modAgent.tools.ozone.safelink.queryRules(\n        { urls: [updateTestRule.url] },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n      )\n      expect(updated).toMatchObject(updatedData)\n      expect(queried.rules[0]).toMatchObject(updatedData)\n    })\n\n    it('rejects triage role from updating rules', async () => {\n      await expect(\n        triageAgent.tools.ozone.safelink.updateRule(updateTestRule, {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneSafelinkUpdateRule,\n            'triage',\n          ),\n        }),\n      ).rejects.toThrow('Must be a moderator to update URL rules')\n    })\n\n    it('throws error when updating non-existent rule', async () => {\n      await expect(\n        adminAgent.tools.ozone.safelink.updateRule(\n          {\n            ...updateTestRule,\n            url: 'https://non-existent.com',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkUpdateRule),\n        ),\n      ).rejects.toThrow('No active rule found for this URL/domain')\n    })\n  })\n\n  describe('removeRule', () => {\n    const removeTestRule = {\n      url: 'https://remove-test.com',\n      pattern: 'url',\n      action: 'block',\n      reason: 'csam',\n      comment: 'Rule to be removed',\n    }\n\n    beforeAll(async () => {\n      await adminAgent.tools.ozone.safelink.addRule(\n        removeTestRule,\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n    })\n\n    it('allows admins and moderators to remove existing rules', async () => {\n      const { data: removed } =\n        await adminAgent.tools.ozone.safelink.removeRule(\n          {\n            url: removeTestRule.url,\n            pattern: removeTestRule.pattern,\n            comment: 'Removing rule - false positive',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkRemoveRule),\n        )\n\n      expect(removed.eventType).toEqual('removeRule')\n      expect(removed.url).toEqual(removeTestRule.url)\n      expect(removed.comment).toEqual('Removing rule - false positive')\n    })\n\n    it('rejects non-moderators from removing rules', async () => {\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: 'https://remove-test2.com',\n          pattern: 'domain',\n          action: 'block',\n          reason: 'spam',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n\n      await expect(\n        triageAgent.tools.ozone.safelink.removeRule(\n          {\n            url: 'https://remove-test2.com',\n            pattern: 'domain',\n          },\n          {\n            headers: await network.ozone.modHeaders(\n              ids.ToolsOzoneSafelinkRemoveRule,\n              'triage',\n            ),\n          },\n        ),\n      ).rejects.toThrow('Must be a moderator to remove URL rules')\n    })\n\n    it('throws error when removing non-existent rule', async () => {\n      await expect(\n        adminAgent.tools.ozone.safelink.removeRule(\n          {\n            url: 'https://never-existed.com',\n            pattern: 'domain',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkRemoveRule),\n        ),\n      ).rejects.toThrow('No active rule found for this URL/domain')\n    })\n  })\n\n  describe('queryRules', () => {\n    beforeAll(async () => {\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: 'https://query-test1.com',\n          pattern: 'domain',\n          action: 'block',\n          reason: 'phishing',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: 'https://query-test2.com/specific-path',\n          pattern: 'url',\n          action: 'warn',\n          reason: 'spam',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n    })\n\n    it('allows querying all active rules', async () => {\n      const { data: result } = await modAgent.tools.ozone.safelink.queryRules(\n        {},\n        await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n      )\n\n      expect(result.rules.length).toBeGreaterThan(0)\n      expect(forSnapshot(result.rules)).toMatchSnapshot()\n    })\n\n    it('allows filtering rules by action', async () => {\n      const { data: blocked } =\n        await adminAgent.tools.ozone.safelink.queryRules(\n          {\n            actions: ['block'],\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n        )\n\n      const { data: warned } = await adminAgent.tools.ozone.safelink.queryRules(\n        {\n          actions: ['warn'],\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n      )\n\n      expect(blocked.rules.every((rule) => rule.action === 'block')).toBe(true)\n      expect(warned.rules.every((rule) => rule.action === 'warn')).toBe(true)\n    })\n\n    it('allows filtering rules by reason', async () => {\n      const { data: phishing } =\n        await adminAgent.tools.ozone.safelink.queryRules(\n          {\n            reason: 'phishing',\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n        )\n\n      const { data: spam } = await adminAgent.tools.ozone.safelink.queryRules(\n        {\n          reason: 'spam',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n      )\n\n      expect(phishing.rules.every((rule) => rule.reason === 'phishing')).toBe(\n        true,\n      )\n      expect(spam.rules.every((rule) => rule.reason === 'spam')).toBe(true)\n    })\n\n    it('allows searching by URL', async () => {\n      const { data: result } = await adminAgent.tools.ozone.safelink.queryRules(\n        {\n          urls: ['https://query-test1.com'],\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n      )\n\n      expect(result.rules.length).toEqual(1)\n      expect(result.rules[0]?.url).toEqual('https://query-test1.com')\n    })\n\n    it('supports pagination', async () => {\n      const headers = await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules)\n      const { data: page1 } = await adminAgent.tools.ozone.safelink.queryRules(\n        { limit: 4 },\n        headers,\n      )\n\n      expect(page1.rules.length).toEqual(4)\n\n      const { data: page2 } = await adminAgent.tools.ozone.safelink.queryRules(\n        {\n          limit: 5,\n          cursor: page1.cursor,\n        },\n        headers,\n      )\n\n      expect(page2.rules.length).toEqual(1)\n    })\n  })\n\n  describe('queryEvents', () => {\n    beforeAll(async () => {\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: 'https://events-test.com',\n          pattern: 'domain',\n          action: 'warn',\n          reason: 'spam',\n          comment: 'Initial rule creation',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n      await adminAgent.tools.ozone.safelink.updateRule(\n        {\n          url: 'https://events-test.com',\n          pattern: 'domain',\n          action: 'block',\n          reason: 'phishing',\n          comment: 'Escalated to block',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkUpdateRule),\n      )\n    })\n\n    it('allows querying safelink events', async () => {\n      const { data: result } = await modAgent.tools.ozone.safelink.queryEvents(\n        {},\n        await getAdminHeaders(ids.ToolsOzoneSafelinkQueryEvents),\n      )\n\n      expect(result.events.length).toBeGreaterThan(0)\n      expect(forSnapshot(result.events)).toMatchSnapshot()\n    })\n\n    it('allows filtering events by URL', async () => {\n      const { data: result } =\n        await adminAgent.tools.ozone.safelink.queryEvents(\n          {\n            urls: ['https://events-test.com'],\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryEvents),\n        )\n\n      expect(\n        result.events.every((event) => event.url === 'https://events-test.com'),\n      ).toBe(true)\n      expect(result.events.length).toBeGreaterThanOrEqual(2)\n    })\n\n    it('supports pagination', async () => {\n      const headers = await getAdminHeaders(ids.ToolsOzoneSafelinkQueryEvents)\n      const { data: page1 } = await adminAgent.tools.ozone.safelink.queryEvents(\n        {\n          limit: 9,\n        },\n        headers,\n      )\n\n      const { data: page2 } = await adminAgent.tools.ozone.safelink.queryEvents(\n        {\n          limit: 10,\n          cursor: page1.cursor,\n        },\n        headers,\n      )\n\n      const { data: page3 } = await adminAgent.tools.ozone.safelink.queryEvents(\n        {\n          limit: 10,\n          cursor: page2.cursor,\n        },\n        headers,\n      )\n\n      expect(page1.events.length).toBeLessThanOrEqual(9)\n      expect(page2.events.length).toEqual(1)\n      expect(page3.cursor).toBeUndefined()\n    })\n  })\n\n  describe('event history over time', () => {\n    it('maintains audit trail through rule lifecycle', async () => {\n      const testUrl = 'https://lifecycle-test.com'\n      const pattern = 'domain'\n\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: testUrl,\n          pattern,\n          action: 'warn',\n          reason: 'spam',\n          comment: 'Initial warning',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule),\n      )\n\n      await modAgent.tools.ozone.safelink.updateRule(\n        {\n          url: testUrl,\n          pattern,\n          action: 'block',\n          reason: 'phishing',\n          comment: 'Escalated to block',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkUpdateRule),\n      )\n\n      await adminAgent.tools.ozone.safelink.removeRule(\n        {\n          url: testUrl,\n          pattern,\n          comment: 'False positive',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSafelinkRemoveRule),\n      )\n\n      const { data: events } =\n        await adminAgent.tools.ozone.safelink.queryEvents(\n          {\n            urls: [testUrl],\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryEvents),\n        )\n\n      expect(events.events.length).toEqual(3)\n      const eventTypes = events.events.map((e) => e.eventType).sort()\n      expect(eventTypes).toEqual(['addRule', 'updateRule', 'removeRule'].sort())\n\n      const { data: queryResult } =\n        await adminAgent.tools.ozone.safelink.queryRules(\n          {\n            urls: [testUrl],\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n        )\n      expect(queryResult.rules.length).toEqual(0)\n    })\n\n    it('handles domain vs URL pattern precedence correctly', async () => {\n      const domain = 'precedence-test.com'\n      const specificUrl = 'https://precedence-test.com/safe-page'\n      const headers = await getAdminHeaders(ids.ToolsOzoneSafelinkAddRule)\n\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: domain,\n          pattern: 'domain',\n          action: 'block',\n          reason: 'phishing',\n        },\n        headers,\n      )\n\n      await adminAgent.tools.ozone.safelink.addRule(\n        {\n          url: specificUrl,\n          pattern: 'url',\n          action: 'whitelist',\n          reason: 'none',\n        },\n        headers,\n      )\n\n      const { data: specificResult } =\n        await adminAgent.tools.ozone.safelink.queryRules(\n          {\n            urls: [specificUrl],\n          },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n        )\n      expect(specificResult.rules.length).toEqual(1)\n      expect(specificResult.rules[0]?.action).toEqual('whitelist')\n\n      const { data: domainResult } =\n        await adminAgent.tools.ozone.safelink.queryRules(\n          { urls: [domain] },\n          await getAdminHeaders(ids.ToolsOzoneSafelinkQueryRules),\n        )\n      expect(domainResult.rules.length).toEqual(1)\n      expect(domainResult.rules[0]?.action).toEqual('block')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/scheduled-action-processor.test.ts",
    "content": "import {\n  AtpAgent,\n  ToolsOzoneModerationListScheduledActions,\n} from '@atproto/api'\nimport { HOUR, MINUTE } from '@atproto/common'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ModEventTakedown } from '../dist/lexicon/types/tools/ozone/moderation/defs'\nimport { ids } from '../src/lexicon/lexicons'\nimport { ProtectedTagSettingKey } from '../src/setting/constants'\n\ndescribe('scheduled action processor', () => {\n  let network: TestNetwork\n  let adminAgent: AtpAgent\n  let sc: SeedClient\n\n  const getAdminHeaders = async (route: string) => {\n    return {\n      headers: await network.ozone.modHeaders(route, 'admin'),\n    }\n  }\n\n  const scheduleTestAction = async (\n    subject: string,\n    scheduling: any,\n    emailData?: { emailSubject?: string; emailContent?: string },\n  ) => {\n    return await adminAgent.tools.ozone.moderation.scheduleAction(\n      {\n        action: {\n          $type: 'tools.ozone.moderation.scheduleAction#takedown',\n          comment: 'Test scheduled takedown',\n          policies: ['spam'],\n          severityLevel: 'sev-1',\n          strikeCount: 1,\n          ...emailData,\n        },\n        subjects: [subject],\n        createdBy: 'did:plc:moderator',\n        scheduling,\n      },\n      await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n    )\n  }\n\n  const getScheduledActions = async (\n    statuses: ToolsOzoneModerationListScheduledActions.InputSchema['statuses'],\n    subjects?: string[],\n  ) => {\n    const { data } =\n      await adminAgent.tools.ozone.moderation.listScheduledActions(\n        { subjects, statuses },\n        await getAdminHeaders(ids.ToolsOzoneModerationListScheduledActions),\n      )\n    return data.actions\n  }\n\n  const getModerationEvents = async (subject: string, types?: string[]) => {\n    const { data } = await adminAgent.tools.ozone.moderation.queryEvents(\n      { subject, types },\n      await getAdminHeaders(ids.ToolsOzoneModerationQueryEvents),\n    )\n    return data.events\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_scheduled_action_processor_test',\n    })\n    adminAgent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('findAndExecuteScheduledActions', () => {\n    it('processes actions scheduled for immediate execution', async () => {\n      const testSubject = sc.dids.alice\n\n      const pastTime = new Date(Date.now() - 1000).toISOString()\n      await scheduleTestAction(\n        testSubject,\n        { executeAt: pastTime },\n        {\n          emailSubject: 'Test Email Subject',\n          emailContent: 'Test Email Content',\n        },\n      )\n\n      const pendingActions = await getScheduledActions(\n        ['pending'],\n        [testSubject],\n      )\n      expect(pendingActions.length).toBe(1)\n\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      const executedActions = await getScheduledActions(\n        ['executed'],\n        [testSubject],\n      )\n      expect(executedActions.length).toBe(1)\n      expect(executedActions[0].status).toBe('executed')\n      expect(executedActions[0].executionEventId).toBeDefined()\n\n      const modEvents = await getModerationEvents(testSubject, [\n        'tools.ozone.moderation.defs#modEventTakedown',\n        'tools.ozone.moderation.defs#modEventEmail',\n      ])\n      expect(modEvents.length).toBe(2)\n      const takedownEvent = modEvents.find(\n        (e) => e.event.$type === 'tools.ozone.moderation.defs#modEventTakedown',\n      )\n      const emailEvent = modEvents.find(\n        (e) => e.event.$type === 'tools.ozone.moderation.defs#modEventEmail',\n      )\n\n      expect(takedownEvent?.event['comment']).toBeDefined()\n\n      expect(emailEvent?.event['subjectLine']).toBe('Test Email Subject')\n      expect(emailEvent?.event['content']).toBe('Test Email Content')\n    })\n\n    it('skips actions scheduled for future execution', async () => {\n      const testSubject = sc.dids.bob\n\n      // Schedule an action for future execution (1 hour from now)\n      const futureTime = new Date(Date.now() + HOUR).toISOString()\n      await scheduleTestAction(testSubject, { executeAt: futureTime })\n\n      // Process scheduled actions\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      // Verify action is still pending\n      const pendingActions = await getScheduledActions(\n        ['pending'],\n        [testSubject],\n      )\n      expect(pendingActions.length).toBe(1)\n\n      const executedActions = await getScheduledActions(\n        ['executed'],\n        [testSubject],\n      )\n      expect(executedActions.length).toBe(0)\n    })\n\n    it('skips randomized actions before executeAfter time', async () => {\n      const testSubject = 'did:plc:future_randomized'\n\n      // Schedule an action with future executeAfter\n      const futureAfter = new Date(Date.now() + 30 * MINUTE).toISOString()\n      const futureUntil = new Date(Date.now() + HOUR).toISOString()\n      await scheduleTestAction(testSubject, {\n        executeAfter: futureAfter,\n        executeUntil: futureUntil,\n      })\n\n      // Process scheduled actions\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      // Verify action is still pending\n      const pendingActions = await getScheduledActions(\n        ['pending'],\n        [testSubject],\n      )\n      expect(pendingActions.length).toBe(1)\n    })\n\n    it('always executes randomized actions past executeUntil deadline', async () => {\n      const testSubject = 'did:plc:overdue_randomized'\n\n      // Schedule an action that's past its deadline\n      const pastAfter = new Date(Date.now() - HOUR).toISOString()\n      const pastUntil = new Date(Date.now() - 30 * MINUTE).toISOString()\n      await scheduleTestAction(testSubject, {\n        executeAfter: pastAfter,\n        executeUntil: pastUntil,\n      })\n\n      // Process scheduled actions\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      // Verify action is executed (should always execute past deadline)\n      const executedActions = await getScheduledActions(\n        ['executed'],\n        [testSubject],\n      )\n      expect(executedActions.length).toBe(1)\n      expect(executedActions[0].status).toBe('executed')\n    })\n  })\n\n  describe('executeScheduledAction', () => {\n    it('handles takedown actions with all properties', async () => {\n      const testSubject = 'did:plc:detailed_takedown'\n\n      // Schedule a detailed takedown action\n      await adminAgent.tools.ozone.moderation.scheduleAction(\n        {\n          action: {\n            $type: 'tools.ozone.moderation.scheduleAction#takedown',\n            comment: 'Detailed takedown test',\n            durationInHours: 24,\n            acknowledgeAccountSubjects: true,\n            policies: ['spam', 'harassment'],\n          },\n          subjects: [testSubject],\n          createdBy: 'did:plc:moderator',\n          scheduling: {\n            executeAt: new Date(Date.now() - 1000).toISOString(),\n          },\n          modTool: { name: 'test-tool' },\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n      )\n\n      // Process the action\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      // Verify the moderation event has all properties\n      const modEvents = await getModerationEvents(testSubject, [\n        'tools.ozone.moderation.defs#modEventTakedown',\n        'tools.ozone.moderation.defs#modEventEmail',\n      ])\n      // No email was sent\n      expect(modEvents.length).toBe(1)\n\n      const takedownEvent = modEvents[0].event as ModEventTakedown\n      expect(takedownEvent.comment).toContain('[SCHEDULED_ACTION]')\n      expect(takedownEvent.comment).toContain('Detailed takedown test')\n      expect(takedownEvent.durationInHours).toBe(24)\n      expect(takedownEvent.acknowledgeAccountSubjects).toBe(true)\n      expect(takedownEvent.policies).toEqual(['spam', 'harassment'])\n    })\n\n    it('marks action as failed when moderation event creation fails', async () => {\n      const testSubject = 'did:plc:invalid_subject'\n\n      await scheduleTestAction(testSubject, {\n        executeAt: new Date(Date.now() - 1000).toISOString(),\n      })\n\n      const pendingActions = await getScheduledActions(\n        ['pending'],\n        [testSubject],\n      )\n      expect(pendingActions.length).toBe(1)\n      const actionId = pendingActions[0].id\n\n      // Manually update the action type to trigger error in processing\n      await network.ozone.ctx.db.db\n        .updateTable('scheduled_action')\n        .set({ action: 'unknown' })\n        .where('id', '=', actionId)\n        .execute()\n\n      await network.ozone.daemon.ctx.scheduledActionProcessor.executeScheduledAction(\n        actionId,\n      )\n\n      const failedActions = await getScheduledActions(['failed'], [testSubject])\n      expect(failedActions.length).toBe(1)\n      expect(failedActions[0].status).toBe('failed')\n      expect(failedActions[0].lastFailureReason).toBeDefined()\n    })\n\n    it('skips actions that are no longer pending', async () => {\n      const testSubject = 'did:plc:already_processed'\n\n      // Schedule and then cancel an action\n      await scheduleTestAction(testSubject, {\n        executeAt: new Date(Date.now() - 1000).toISOString(),\n      })\n\n      await adminAgent.tools.ozone.moderation.cancelScheduledActions(\n        { subjects: [testSubject] },\n        await getAdminHeaders(ids.ToolsOzoneModerationCancelScheduledActions),\n      )\n\n      const cancelledActions = await getScheduledActions(\n        ['cancelled'],\n        [testSubject],\n      )\n      expect(cancelledActions.length).toBe(1)\n      const actionId = cancelledActions[0].id\n\n      await network.ozone.daemon.ctx.scheduledActionProcessor.executeScheduledAction(\n        actionId,\n      )\n\n      const modEvents = await getModerationEvents(testSubject)\n      const takedownEvents = modEvents.filter(\n        (e) => e.event.$type === 'tools.ozone.moderation.defs#modEventTakedown',\n      )\n      expect(takedownEvents.length).toBe(0)\n    })\n\n    it('processes multiple actions in batch', async () => {\n      const subjects = ['did:plc:batch1', 'did:plc:batch2', 'did:plc:batch3']\n      const pastTime = new Date(Date.now() - 1000).toISOString()\n\n      for (const subject of subjects) {\n        await scheduleTestAction(subject, { executeAt: pastTime })\n      }\n\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      const executedActions = await getScheduledActions(['executed'], subjects)\n      expect(executedActions.length).toBe(3)\n\n      for (const subject of subjects) {\n        const modEvents = await getModerationEvents(subject, [\n          'tools.ozone.moderation.defs#modEventTakedown',\n        ])\n        expect(modEvents.length).toBe(1)\n      }\n    })\n  })\n\n  describe('takedown validation checks', () => {\n    it('fails when trying to takedown an already taken down account', async () => {\n      const testSubject = 'did:plc:already_takendown'\n\n      // takedown the account manually\n      await adminAgent.tools.ozone.moderation.emitEvent(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: testSubject,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTakedown',\n            comment: 'Manual takedown first',\n          },\n          createdBy: adminAgent.session?.did || 'did:plc:admin',\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationEmitEvent),\n      )\n\n      // Schedule a takedown for the already taken down account\n      await scheduleTestAction(testSubject, {\n        executeAt: new Date(Date.now() - 1000).toISOString(),\n      })\n\n      // Process the scheduled action\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      // Verify the scheduled action failed\n      const failedActions = await getScheduledActions(['failed'], [testSubject])\n      expect(failedActions.length).toBe(1)\n      expect(failedActions[0].status).toBe('failed')\n      expect(failedActions[0].lastFailureReason).toContain(\n        'Account is already taken down',\n      )\n    })\n\n    it('enforces protected tag restrictions when account has protected tags', async () => {\n      const testSubject = 'did:plc:protected_tag_test'\n\n      // add the protected tag to the account\n      await adminAgent.tools.ozone.moderation.emitEvent(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: testSubject,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['vip'],\n            remove: [],\n          },\n          createdBy: adminAgent.session?.did || 'did:plc:admin',\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationEmitEvent),\n      )\n\n      // add protected tag setting for the instance and make that tag actionable by a mod only\n      await adminAgent.tools.ozone.setting.upsertOption(\n        {\n          key: ProtectedTagSettingKey,\n          scope: 'instance',\n          managerRole: 'tools.ozone.team.defs#roleAdmin',\n          value: { vip: { moderators: [sc.dids.alice] } },\n        },\n        await getAdminHeaders(ids.ToolsOzoneSettingUpsertOption),\n      )\n\n      // Schedule a takedown action created by a non-admin moderator\n      await adminAgent.tools.ozone.moderation.scheduleAction(\n        {\n          action: {\n            $type: 'tools.ozone.moderation.scheduleAction#takedown',\n            comment: 'Test protected tag enforcement',\n          },\n          subjects: [testSubject],\n          createdBy: 'did:plc:non_admin_moderator', // Non-admin creator\n          scheduling: {\n            executeAt: new Date(Date.now() - 1000).toISOString(),\n          },\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n      )\n\n      // Process the scheduled action\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      // Verify the scheduled action failed due to protected tag restrictions\n      const failedActions = await getScheduledActions(['failed'], [testSubject])\n      expect(failedActions.length).toBe(1)\n      expect(failedActions[0].status).toBe('failed')\n      expect(failedActions[0].lastFailureReason).toContain('tag')\n\n      // Clean up protected tags setting\n      await adminAgent.tools.ozone.setting.removeOptions(\n        {\n          keys: [ProtectedTagSettingKey],\n          scope: 'instance',\n        },\n        await getAdminHeaders(ids.ToolsOzoneSettingRemoveOptions),\n      )\n    })\n\n    it('allows takedown of accounts with protected tags when created by authorized user', async () => {\n      const testSubject = 'did:plc:authorized_protected_tag_test'\n\n      // Set up protected tags configuration allowing admins\n      await adminAgent.tools.ozone.setting.upsertOption(\n        {\n          key: ProtectedTagSettingKey,\n          scope: 'instance',\n          managerRole: 'tools.ozone.team.defs#roleAdmin',\n          value: { vip: { roles: ['tools.ozone.team.defs#roleAdmin'] } },\n        },\n        await getAdminHeaders(ids.ToolsOzoneSettingUpsertOption),\n      )\n\n      // Add a protected tag to the account\n      await adminAgent.tools.ozone.moderation.emitEvent(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: testSubject,\n          },\n          event: {\n            $type: 'tools.ozone.moderation.defs#modEventTag',\n            add: ['vip'],\n            remove: [],\n          },\n          createdBy: adminAgent.session?.did || 'did:plc:admin',\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationEmitEvent),\n      )\n\n      await adminAgent.tools.ozone.moderation.scheduleAction(\n        {\n          action: {\n            $type: 'tools.ozone.moderation.scheduleAction#takedown',\n            comment: 'Admin takedown of protected account',\n          },\n          subjects: [testSubject],\n          createdBy: network.ozone.ctx.cfg.service.did,\n          scheduling: {\n            executeAt: new Date(Date.now() - 1000).toISOString(),\n          },\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n      )\n\n      await network.ozone.daemon.ctx.scheduledActionProcessor.findAndExecuteScheduledActions()\n\n      const executedActions = await getScheduledActions(\n        ['executed'],\n        [testSubject],\n      )\n      expect(executedActions.length).toBe(1)\n      expect(executedActions[0].status).toBe('executed')\n\n      const modEvents = await getModerationEvents(testSubject, [\n        'tools.ozone.moderation.defs#modEventTakedown',\n      ])\n      expect(modEvents.length).toBe(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/scheduled-action.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { forSnapshot } from './_util'\n\nconst allStatuses = ['pending', 'executed', 'cancelled', 'failed']\n\ndescribe('scheduled action management', () => {\n  let network: TestNetwork\n  let adminAgent: AtpAgent\n  let modAgent: AtpAgent\n  let triageAgent: AtpAgent\n  let sc: SeedClient\n\n  const getAdminHeaders = async (route: string) => {\n    return {\n      headers: await network.ozone.modHeaders(route, 'admin'),\n    }\n  }\n\n  const getModHeaders = async (route: string) => {\n    return {\n      headers: await network.ozone.modHeaders(route, 'moderator'),\n    }\n  }\n\n  const getModEvent = async (params: {\n    subject: string\n    cancellation?: boolean\n  }) => {\n    const {\n      data: { events },\n    } = await adminAgent.tools.ozone.moderation.queryEvents(\n      {\n        subject: params.subject,\n        types: [\n          params.cancellation\n            ? 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n            : 'tools.ozone.moderation.defs#scheduleTakedownEvent',\n        ],\n      },\n      await getAdminHeaders(ids.ToolsOzoneModerationQueryEvents),\n    )\n    return events\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_scheduled_action_test',\n    })\n    adminAgent = network.ozone.getClient()\n    modAgent = network.ozone.getClient()\n    triageAgent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('scheduleAction', () => {\n    const getTestAction = () => ({\n      action: {\n        $type: 'tools.ozone.moderation.scheduleAction#takedown',\n        comment: 'test',\n        policies: ['spam'],\n      },\n      subjects: [sc.dids.carol, sc.dids.bob],\n      createdBy: 'did:plc:moderator',\n      scheduling: {\n        executeAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now\n      },\n    })\n\n    it('allows admins to schedule actions', async () => {\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.scheduleAction(\n          getTestAction(),\n          await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n        )\n      const bobsModEvents = await getModEvent({ subject: sc.dids.bob })\n\n      expect(result.succeeded.length).toBe(2)\n      expect(result.failed.length).toBe(0)\n      expect(result.succeeded).toContain(sc.dids.carol)\n      expect(result.succeeded).toContain(sc.dids.bob)\n      expect(bobsModEvents.length).toBe(1)\n    })\n\n    it('rejects triage role from scheduling actions', async () => {\n      await expect(\n        triageAgent.tools.ozone.moderation.scheduleAction(getTestAction(), {\n          headers: await network.ozone.modHeaders(\n            ids.ToolsOzoneModerationScheduleAction,\n            'triage',\n          ),\n        }),\n      ).rejects.toThrow('Must be a moderator to schedule actions')\n    })\n\n    it('supports scheduling with time range (executeAfter/executeUntil)', async () => {\n      const rangeAction = {\n        ...getTestAction(),\n        subjects: [sc.dids.alice],\n        scheduling: {\n          executeAfter: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30 min from now\n          executeUntil: new Date(Date.now() + 90 * 60 * 1000).toISOString(), // 90 min from now\n        },\n      }\n\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.scheduleAction(\n          rangeAction,\n          await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n        )\n      expect(result.succeeded.length).toBe(1)\n      expect(result.succeeded).toContain(sc.dids.alice)\n    })\n\n    it('prevents scheduling multiple actions for same subject', async () => {\n      const duplicateAction = {\n        ...getTestAction(),\n        subjects: [sc.dids.carol],\n        scheduling: {\n          executeAt: new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(),\n        },\n      }\n\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.scheduleAction(\n          duplicateAction,\n          await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n        )\n      expect(result.succeeded.length).toBe(0)\n      expect(result.failed.length).toBe(1)\n      expect(result.failed[0].subject).toBe(sc.dids.carol)\n      expect(result.failed[0].error).toContain(\n        'A pending scheduled action already exists',\n      )\n    })\n\n    it('validates scheduling parameters', async () => {\n      const invalidAction = {\n        ...getTestAction(),\n        subjects: ['did:plc:test_invalid'],\n        scheduling: {\n          executeAfter: new Date(Date.now() + 90 * 60 * 1000).toISOString(),\n          executeUntil: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // executeUntil before executeAfter\n        },\n      }\n\n      const { data } = await adminAgent.tools.ozone.moderation.scheduleAction(\n        invalidAction,\n        await getAdminHeaders(ids.ToolsOzoneModerationScheduleAction),\n      )\n\n      expect(data.failed.length).toBe(1)\n      expect(data.failed[0].subject).toBe('did:plc:test_invalid')\n      expect(data.failed[0].error).toContain(\n        'executeAfter must be before executeUntil',\n      )\n    })\n  })\n\n  describe('listScheduledActions', () => {\n    it('allows moderators to list all scheduled actions', async () => {\n      const { data: result } =\n        await modAgent.tools.ozone.moderation.listScheduledActions(\n          { statuses: allStatuses },\n          await getModHeaders(ids.ToolsOzoneModerationListScheduledActions),\n        )\n\n      expect(result.actions.length).toBeGreaterThan(0)\n      expect(forSnapshot(result.actions)).toMatchSnapshot()\n    })\n\n    it('allows filtering by subjects', async () => {\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.listScheduledActions(\n          {\n            subjects: [sc.dids.carol],\n            statuses: allStatuses,\n          },\n          await getAdminHeaders(ids.ToolsOzoneModerationListScheduledActions),\n        )\n\n      expect(result.actions.length).toBeGreaterThan(0)\n      result.actions.forEach((action) => {\n        expect(action.did).toBe(sc.dids.carol)\n      })\n    })\n\n    it('allows filtering by status', async () => {\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.listScheduledActions(\n          {\n            statuses: ['pending'],\n          },\n          await getAdminHeaders(ids.ToolsOzoneModerationListScheduledActions),\n        )\n\n      expect(result.actions.length).toBeGreaterThan(0)\n      result.actions.forEach((action) => {\n        expect(action.status).toBe('pending')\n      })\n    })\n\n    it('supports time range filtering', async () => {\n      const now = new Date()\n      const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000)\n      const twoHoursFromNow = new Date(now.getTime() + 2 * 60 * 60 * 1000)\n\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.listScheduledActions(\n          {\n            startsAfter: oneHourAgo.toISOString(),\n            endsBefore: twoHoursFromNow.toISOString(),\n            statuses: allStatuses,\n          },\n          await getAdminHeaders(ids.ToolsOzoneModerationListScheduledActions),\n        )\n\n      expect(result.actions.length).toBeGreaterThan(0)\n    })\n\n    it('supports pagination', async () => {\n      const headers = await getAdminHeaders(\n        ids.ToolsOzoneModerationListScheduledActions,\n      )\n      const { data: page1 } =\n        await adminAgent.tools.ozone.moderation.listScheduledActions(\n          { limit: 2, statuses: allStatuses },\n          headers,\n        )\n\n      expect(page1.actions.length).toBe(2)\n      expect(page1.cursor).toBeDefined()\n\n      const { data: page2 } =\n        await adminAgent.tools.ozone.moderation.listScheduledActions(\n          {\n            limit: 2,\n            statuses: allStatuses,\n            cursor: page1.cursor,\n          },\n          headers,\n        )\n\n      expect(page2.actions.length).toBeGreaterThan(0)\n      expect(page1.actions.map((a) => a.did)).not.toContain(\n        page2.actions[0].did,\n      )\n    })\n  })\n\n  describe('cancelScheduledActions', () => {\n    it('allows moderators to cancel scheduled actions', async () => {\n      const { data: result } =\n        await modAgent.tools.ozone.moderation.cancelScheduledActions(\n          {\n            subjects: [sc.dids.bob],\n          },\n          await getModHeaders(ids.ToolsOzoneModerationCancelScheduledActions),\n        )\n      const bobsModEvents = await getModEvent({\n        subject: sc.dids.bob,\n        cancellation: true,\n      })\n      expect(result.succeeded.length).toBe(1)\n      expect(result.failed.length).toBe(0)\n      expect(result.succeeded).toContain(sc.dids.bob)\n      expect(bobsModEvents.length).toBe(1)\n    })\n\n    it('allows admins to cancel scheduled actions', async () => {\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.cancelScheduledActions(\n          {\n            subjects: [sc.dids.carol],\n          },\n          await getAdminHeaders(ids.ToolsOzoneModerationCancelScheduledActions),\n        )\n\n      expect(result.succeeded.length).toBe(1)\n      expect(result.failed.length).toBe(0)\n      expect(result.succeeded).toContain(sc.dids.carol)\n\n      const {\n        data: { actions },\n      } = await adminAgent.tools.ozone.moderation.listScheduledActions(\n        {\n          statuses: allStatuses,\n          subjects: [sc.dids.carol],\n        },\n        await getAdminHeaders(ids.ToolsOzoneModerationListScheduledActions),\n      )\n\n      expect(actions[0].status).toBe('cancelled')\n    })\n\n    it('handles cancellation of non-existent actions', async () => {\n      const { data: result } =\n        await adminAgent.tools.ozone.moderation.cancelScheduledActions(\n          {\n            subjects: ['did:plc:nonexistent'],\n          },\n          await getAdminHeaders(ids.ToolsOzoneModerationCancelScheduledActions),\n        )\n\n      expect(result.succeeded.length).toBe(0)\n      expect(result.failed.length).toBe(1)\n      expect(result.failed[0].did).toBe('did:plc:nonexistent')\n      expect(result.failed[0].error).toContain(\n        'No pending scheduled actions found',\n      )\n    })\n\n    it('rejects triage moderators from cancelling actions', async () => {\n      await expect(\n        triageAgent.tools.ozone.moderation.cancelScheduledActions(\n          { subjects: [sc.dids.carol] },\n          {\n            headers: await network.ozone.modHeaders(\n              ids.ToolsOzoneModerationCancelScheduledActions,\n              'triage',\n            ),\n          },\n        ),\n      ).rejects.toThrow('Must be a moderator to cancel scheduled actions')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/sequencer.test.ts",
    "content": "import { readFromGenerator, wait } from '@atproto/common'\nimport { randomStr } from '@atproto/crypto'\nimport { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env'\nimport { Label } from '../src/lexicon/types/com/atproto/label/defs'\nimport { LabelsEvt, Sequencer } from '../src/sequencer'\nimport { Outbox } from '../src/sequencer/outbox'\n\ndescribe('sequencer', () => {\n  let network: TestNetwork\n  let sequencer: Sequencer\n\n  let totalEvts = 0\n  let lastSeen: number\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_sequencer',\n    })\n    // @ts-expect-error\n    sequencer = network.ozone.ctx.sequencer\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const loadFromDb = (lastSeen: number) => {\n    return sequencer.db.db\n      .selectFrom('label')\n      .selectAll()\n      .where('id', '>', lastSeen)\n      .orderBy('id', 'asc')\n      .execute()\n  }\n\n  const evtToDbRow = (e: LabelsEvt) => {\n    const { ver: _, ...label } = e.labels[0]\n    return {\n      id: e.seq,\n      ...label,\n      neg: !!label.neg,\n      cid: label.cid ? label.cid : '',\n      exp: null,\n      sig: label.sig ? Buffer.from(label.sig) : null,\n      signingKeyId: network.ozone.ctx.signingKeyId,\n    }\n  }\n\n  const caughtUp = (outbox: Outbox): (() => Promise<boolean>) => {\n    return async () => {\n      const lastEvt = await outbox.sequencer.curr()\n      if (lastEvt === null) return true\n      return outbox.lastSeen >= (lastEvt ?? 0)\n    }\n  }\n\n  const createLabels = async (count: number): Promise<Label[]> => {\n    const labels: Label[] = []\n    for (let i = 0; i < count; i++) {\n      const did = `did:example:${randomStr(10, 'base32')}`\n      const label = {\n        src: EXAMPLE_LABELER,\n        uri: did,\n        val: 'spam',\n        neg: false,\n        cts: new Date().toISOString(),\n      }\n      await network.ozone.ctx.db.transaction((dbTxn) =>\n        network.ozone.ctx.modService(dbTxn).createLabels([label]),\n      )\n      labels.push(label)\n    }\n    return labels\n  }\n\n  it('sends to outbox', async () => {\n    const count = 20\n    totalEvts += count\n    await createLabels(count)\n    const outbox = new Outbox(sequencer)\n    const evts = await readFromGenerator(outbox.events(-1), caughtUp(outbox))\n    expect(evts.length).toBe(totalEvts)\n\n    const fromDb = await loadFromDb(-1)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('sequences negative labels', async () => {\n    const count = 5\n    totalEvts += count\n    const created = await createLabels(count)\n    const toNegate = created\n      .slice(0, 2)\n      .map((l) => ({ ...l, neg: true, cts: new Date().toISOString() }))\n    await network.ozone.ctx\n      .modService(network.ozone.ctx.db)\n      .createLabels(toNegate)\n\n    const outbox = new Outbox(sequencer)\n    const evts = await readFromGenerator(\n      outbox.events(lastSeen),\n      caughtUp(outbox),\n    )\n    expect(evts.length).toBe(count)\n\n    const fromDb = await loadFromDb(lastSeen)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n    expect(evts[3].labels[0].uri).toEqual(toNegate[0].uri)\n    expect(evts[3].labels[0].neg).toBe(true)\n    expect(evts[4].labels[0].uri).toEqual(toNegate[1].uri)\n    expect(evts[4].labels[0].neg).toBe(true)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('handles cut over', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer)\n    const createPromise = createLabels(count)\n    const [evts] = await Promise.all([\n      readFromGenerator(outbox.events(-1), caughtUp(outbox), createPromise),\n      createPromise,\n    ])\n    expect(evts.length).toBe(totalEvts)\n\n    const fromDb = await loadFromDb(-1)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('only gets events after cursor', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer)\n    const createPromise = createLabels(count)\n    const [evts] = await Promise.all([\n      readFromGenerator(\n        outbox.events(lastSeen),\n        caughtUp(outbox),\n        createPromise,\n      ),\n      createPromise,\n    ])\n\n    // +1 because we send the lastSeen date as well\n    expect(evts.length).toBe(count)\n\n    const fromDb = await loadFromDb(lastSeen)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('buffers events that are not being read', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer)\n    const createPromise = createLabels(count)\n    const gen = outbox.events(lastSeen)\n    // read enough to start streaming then wait so that the rest go into the buffer,\n    // then stream out from buffer\n    const [firstPart] = await Promise.all([\n      readFromGenerator(gen, caughtUp(outbox), createPromise, 5),\n      createPromise,\n    ])\n    const secondPart = await readFromGenerator(\n      gen,\n      caughtUp(outbox),\n      createPromise,\n    )\n    const evts = [...firstPart, ...secondPart]\n    expect(evts.length).toBe(count)\n\n    const fromDb = await loadFromDb(lastSeen)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('errors when buffer is overloaded', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer, { maxBufferSize: 5 })\n    const gen = outbox.events(lastSeen)\n    const createPromise = createLabels(count)\n    // read enough to start streaming then wait to stream rest until buffer is overloaded\n    const overloadBuffer = async () => {\n      await Promise.all([\n        readFromGenerator(gen, caughtUp(outbox), createPromise, 5),\n        createPromise,\n      ])\n      await wait(500)\n      await readFromGenerator(gen, caughtUp(outbox), createPromise)\n    }\n    await expect(overloadBuffer).rejects.toThrow('Stream consumer too slow')\n\n    await createPromise\n\n    const fromDb = await loadFromDb(lastSeen)\n    lastSeen = fromDb.at(-1)?.id ?? lastSeen\n  })\n\n  it('handles many open connections', async () => {\n    const count = 20\n    const outboxes: Outbox[] = []\n    for (let i = 0; i < 50; i++) {\n      outboxes.push(new Outbox(sequencer))\n    }\n    const createPromise = createLabels(count)\n    const readOutboxes = Promise.all(\n      outboxes.map((o) =>\n        readFromGenerator(o.events(lastSeen), caughtUp(o), createPromise),\n      ),\n    )\n    const [results] = await Promise.all([readOutboxes, createPromise])\n    const fromDb = await loadFromDb(lastSeen)\n    for (const result of results) {\n      expect(result.length).toBe(count)\n      expect(result.map(evtToDbRow)).toEqual(fromDb)\n    }\n    lastSeen = results[0].at(-1)?.seq ?? lastSeen\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/server.test.ts",
    "content": "import express from 'express'\nimport { TestNetwork, TestOzone } from '@atproto/dev-env'\nimport { handler as errorHandler } from '../src/error'\nimport { startServer } from './_util'\n\ndescribe('server', () => {\n  let network: TestNetwork\n  let ozone: TestOzone\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_server',\n    })\n    ozone = network.ozone\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('preserves 404s.', async () => {\n    const response = await fetch(`${ozone.url}/unknown`)\n    expect(response.status).toEqual(404)\n  })\n\n  it('error handler turns unknown errors into 500s.', async () => {\n    const app = express()\n      .get('/oops', () => {\n        throw new Error('Oops!')\n      })\n      .use(errorHandler)\n\n    const { origin, stop } = await startServer(app)\n    try {\n      const response = await fetch(new URL(`/oops`, origin))\n      expect(response.status).toEqual(500)\n      await expect(response.json()).resolves.toEqual({\n        error: 'InternalServerError',\n        message: 'Internal Server Error',\n      })\n    } finally {\n      await stop()\n    }\n  })\n\n  it('healthcheck succeeds when database is available.', async () => {\n    const response = await fetch(`${network.bsky.url}/xrpc/_health`)\n    expect(response.status).toEqual(200)\n    await expect(response.json()).resolves.toEqual({ version: 'unknown' })\n  })\n\n  it('healthcheck fails when database is unavailable.', async () => {\n    // destroy sequencer to release connection that would prevent the db from closing\n    await ozone.ctx.sequencer.destroy()\n    await ozone.ctx.db.close()\n\n    const res = await fetch(`${ozone.url}/xrpc/_health`)\n\n    expect(res.status).toEqual(503)\n    await expect(res.json()).resolves.toEqual({\n      version: '0.0.0',\n      error: 'Service Unavailable',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/sets.test.ts",
    "content": "import AtpAgent, {\n  ToolsOzoneSetDefs,\n  ToolsOzoneSetQuerySets,\n} from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { forSnapshot } from './_util'\n\ndescribe('ozone-sets', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  const sampleSet1 = {\n    name: 'test-set-1',\n    description: 'Test set 1',\n  }\n\n  const sampleSet2 = {\n    name: 'test-set-2',\n  }\n\n  const sampleSet3 = {\n    name: 'another-set',\n    description: 'Another test set',\n  }\n\n  const upsertSet = async (set: ToolsOzoneSetDefs.Set) => {\n    const { data } = await agent.tools.ozone.set.upsertSet(set, {\n      encoding: 'application/json',\n      headers: await network.ozone.modHeaders(\n        ids.ToolsOzoneSetUpsertSet,\n        'admin',\n      ),\n    })\n\n    return data\n  }\n\n  const removeSet = async (name: string) => {\n    await agent.tools.ozone.set.deleteSet(\n      { name },\n      {\n        encoding: 'application/json',\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneSetDeleteSet,\n          'admin',\n        ),\n      },\n    )\n  }\n\n  const addValues = async (name: string, values: string[]) => {\n    await agent.tools.ozone.set.addValues(\n      { name, values },\n      {\n        encoding: 'application/json',\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneSetAddValues,\n          'admin',\n        ),\n      },\n    )\n  }\n\n  const getValues = async (name: string, limit?: number, cursor?: string) => {\n    const { data } = await agent.tools.ozone.set.getValues(\n      { name, limit, cursor },\n      {\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneSetGetValues,\n          'moderator',\n        ),\n      },\n    )\n    return data\n  }\n\n  const querySets = async (params: ToolsOzoneSetQuerySets.QueryParams) => {\n    const { data } = await agent.tools.ozone.set.querySets(params, {\n      headers: await network.ozone.modHeaders(\n        ids.ToolsOzoneSetQuerySets,\n        'moderator',\n      ),\n    })\n    return data\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_sets',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('querySets', () => {\n    beforeAll(async () => {\n      await Promise.all([\n        upsertSet(sampleSet1),\n        upsertSet(sampleSet2),\n        upsertSet(sampleSet3),\n      ])\n    })\n    afterAll(async () => {\n      await Promise.all([\n        removeSet(sampleSet1.name),\n        removeSet(sampleSet2.name),\n        removeSet(sampleSet3.name),\n      ])\n    })\n    it('returns all sets when no parameters are provided', async () => {\n      const result = await querySets({})\n      expect(result.sets.length).toBe(3)\n      expect(forSnapshot(result.sets)).toMatchSnapshot()\n    })\n\n    it('limits the number of returned sets', async () => {\n      const result = await querySets({ limit: 2 })\n      expect(result.sets.length).toBe(2)\n      expect(result.cursor).toBeDefined()\n    })\n\n    it('returns sets after the cursor', async () => {\n      const firstPage = await querySets({ limit: 2 })\n      const secondPage = await querySets({ cursor: firstPage.cursor })\n      expect(secondPage.sets.length).toBe(1)\n      expect(secondPage.sets[0].name).toBe('test-set-2')\n    })\n\n    it('filters sets by name prefix', async () => {\n      const result = await querySets({ namePrefix: 'test-' })\n      expect(result.sets.length).toBe(2)\n      expect(result.sets.map((s) => s.name)).toEqual([\n        'test-set-1',\n        'test-set-2',\n      ])\n    })\n\n    it('sorts sets by given column and direction', async () => {\n      const sortedByName = await querySets({ sortBy: 'name' })\n      expect(sortedByName.sets.map((s) => s.name)).toEqual([\n        'another-set',\n        'test-set-1',\n        'test-set-2',\n      ])\n      const reverseSortedByName = await querySets({\n        sortBy: 'name',\n        sortDirection: 'desc',\n      })\n      expect(reverseSortedByName.sets.map((s) => s.name)).toEqual([\n        'test-set-2',\n        'test-set-1',\n        'another-set',\n      ])\n    })\n  })\n\n  describe('upsertSet', () => {\n    afterAll(async () => {\n      await removeSet('new-test-set')\n    })\n    it('creates a new set', async () => {\n      const result = await upsertSet({\n        name: 'new-test-set',\n        description: 'A new test set',\n      })\n      expect(forSnapshot(result)).toMatchSnapshot()\n    })\n\n    it('updates an existing set', async () => {\n      const result = await upsertSet({\n        name: 'new-test-set',\n        description: 'Updated description',\n      })\n      expect(forSnapshot(result)).toMatchSnapshot()\n    })\n\n    it('allows setting empty description', async () => {\n      const result = await upsertSet({\n        name: 'new-test-set',\n        description: '',\n      })\n      expect(result.description).toBeUndefined()\n    })\n  })\n\n  describe('addValues', () => {\n    beforeAll(async () => {\n      await upsertSet(sampleSet1)\n      await upsertSet(sampleSet2)\n    })\n    afterAll(async () => {\n      await removeSet(sampleSet1.name)\n      await removeSet(sampleSet2.name)\n    })\n    it('adds new values to an existing set', async () => {\n      const newValues = ['value1', 'value2', 'value3']\n      await addValues(sampleSet1.name, newValues)\n\n      const result = await getValues(sampleSet1.name)\n      expect(result.values).toEqual(expect.arrayContaining(newValues))\n    })\n\n    it('does not duplicate existing values', async () => {\n      const initialValues = ['initial1', 'initial2']\n      await addValues(sampleSet2.name, initialValues)\n\n      const newValues = ['initial2', 'new1', 'new2']\n      await addValues(sampleSet2.name, newValues)\n\n      const result = await getValues(sampleSet2.name)\n      expect(result.values).toEqual(\n        expect.arrayContaining([...initialValues, 'new1', 'new2']),\n      )\n      expect(result.values.filter((v) => v === 'initial2').length).toBe(1)\n    })\n  })\n\n  describe('getValues', () => {\n    beforeAll(async () => {\n      await upsertSet(sampleSet1)\n    })\n    afterAll(async () => {\n      await removeSet(sampleSet1.name)\n    })\n    it('paginates values from a set', async () => {\n      const allValues = Array.from({ length: 9 }, (_, i) => `value${i}`)\n      await addValues(sampleSet1.name, allValues)\n\n      const firstPage = await getValues(sampleSet1.name, 3)\n      const secondPage = await getValues(sampleSet1.name, 3, firstPage.cursor)\n      const lastPage = await getValues(sampleSet1.name, 3, secondPage.cursor)\n\n      expect(firstPage.values).toEqual(allValues.slice(0, 3))\n      expect(secondPage.values).toEqual(allValues.slice(3, 6))\n      expect(lastPage.values).toEqual(allValues.slice(6, 9))\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/settings.test.ts",
    "content": "import AtpAgent, {\n  ToolsOzoneSettingListOptions,\n  ToolsOzoneSettingUpsertOption,\n} from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { SettingScope } from '../dist/db/schema/setting'\nimport { ids } from '../src/lexicon/lexicons'\nimport { forSnapshot } from './_util'\n\ndescribe('ozone-settings', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  const upsertOption = async (\n    setting: ToolsOzoneSettingUpsertOption.InputSchema,\n    callerRole: 'admin' | 'moderator' | 'triage' = 'admin',\n  ) => {\n    const { data } = await agent.tools.ozone.setting.upsertOption(setting, {\n      encoding: 'application/json',\n      headers: await network.ozone.modHeaders(\n        ids.ToolsOzoneSettingUpsertOption,\n        callerRole,\n      ),\n    })\n\n    return data\n  }\n\n  const removeOptions = async (\n    keys: string[],\n    scope: SettingScope,\n    callerRole: 'admin' | 'moderator' | 'triage' = 'admin',\n  ) => {\n    await agent.tools.ozone.setting.removeOptions(\n      { keys, scope },\n      {\n        encoding: 'application/json',\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneSettingRemoveOptions,\n          callerRole,\n        ),\n      },\n    )\n  }\n\n  const listOptions = async (\n    params: ToolsOzoneSettingListOptions.QueryParams,\n    callerRole: 'admin' | 'moderator' | 'triage' = 'moderator',\n  ) => {\n    const { data } = await agent.tools.ozone.setting.listOptions(params, {\n      headers: await network.ozone.modHeaders(\n        ids.ToolsOzoneSettingListOptions,\n        callerRole,\n      ),\n    })\n    return data\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_settings',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('upsertOption', () => {\n    afterAll(async () => {\n      await removeOptions(\n        ['tools.ozone.setting.upsertTest.labelers'],\n        'personal',\n      )\n    })\n    it('only allows managerRole to update instance settings', async () => {\n      await upsertOption({\n        scope: 'instance',\n        key: 'tools.ozone.setting.upsertTest.labelers',\n        value: { dids: ['did:plc:xyz'] },\n        description: 'triage users can not update this',\n        managerRole: 'tools.ozone.team.defs#roleModerator',\n      })\n\n      await expect(\n        upsertOption(\n          {\n            scope: 'instance',\n            key: 'tools.ozone.setting.upsertTest.labelers',\n            value: { noDids: 'test' },\n            description: 'triage users can not update this',\n            managerRole: 'tools.ozone.team.defs#roleModerator',\n          },\n          'triage',\n        ),\n      ).rejects.toThrow(/Not permitted/gi)\n\n      await upsertOption(\n        {\n          scope: 'instance',\n          key: 'tools.ozone.setting.upsertTest.labelers',\n          value: { noDids: 'test' },\n          description:\n            'My personal labelers that i want to use when browsing ozone',\n          managerRole: 'tools.ozone.team.defs#roleModerator',\n        },\n        'moderator',\n      )\n\n      const afterUpdatedByModerator = await listOptions(\n        {\n          scope: 'instance',\n          prefix: 'tools.ozone.setting.upsertTest.labelers',\n        },\n        'moderator',\n      )\n      expect(afterUpdatedByModerator.options[0].value?.['dids']).toBeFalsy()\n      expect(afterUpdatedByModerator.options[0].value?.['noDids']).toEqual(\n        'test',\n      )\n      await upsertOption(\n        {\n          scope: 'instance',\n          key: 'tools.ozone.setting.upsertTest.labelers',\n          value: { dids: 'test' },\n          description:\n            'My personal labelers that i want to use when browsing ozone',\n          managerRole: 'tools.ozone.team.defs#roleModerator',\n        },\n        'moderator',\n      )\n\n      const afterUpdatedByAdmin = await listOptions(\n        {\n          scope: 'instance',\n          prefix: 'tools.ozone.setting.upsertTest.labelers',\n        },\n        'admin',\n      )\n      expect(afterUpdatedByAdmin.options[0].value?.['noDids']).toBeFalsy()\n      expect(afterUpdatedByAdmin.options[0].value?.['dids']).toEqual('test')\n    })\n  })\n\n  describe('listOptions', () => {\n    beforeAll(async () => {\n      await upsertOption({\n        scope: 'instance',\n        key: 'tools.ozone.setting.client.queues',\n        value: { stratosphere: { name: 'Stratosphere' } },\n        description:\n          'This determines how many queues the client interface will show',\n        managerRole: 'tools.ozone.team.defs#roleAdmin',\n      })\n      await upsertOption({\n        scope: 'instance',\n        key: 'tools.ozone.setting.client.queueHash',\n        value: { val: 10.5 },\n        description:\n          'This determines how each queue is balanced when sorted by oldest first',\n        managerRole: 'tools.ozone.team.defs#roleAdmin',\n      })\n      await upsertOption({\n        scope: 'instance',\n        key: 'tools.ozone.setting.client.externalLabelers',\n        value: { dids: ['did:plc:xyz'] },\n        description:\n          'List of external labelers that will be plugged into the client views',\n        managerRole: 'tools.ozone.team.defs#roleAdmin',\n      })\n    })\n\n    afterAll(async () => {\n      await removeOptions(\n        [\n          'tools.ozone.setting.client.queues',\n          'tools.ozone.setting.client.queueHash',\n          'tools.ozone.setting.client.externalLabelers',\n        ],\n        'instance',\n      )\n    })\n\n    it('returns all personal settings', async () => {\n      const result = await listOptions({ prefix: 'tools.ozone.setting.client' })\n      expect(result.options.length).toBe(3)\n\n      expect(forSnapshot(result.options)).toMatchSnapshot()\n    })\n\n    it('allows paginating options', async () => {\n      const params = { prefix: 'tools.ozone.setting.client', limit: 1 }\n      const pageOne = await listOptions(params)\n      const pageTwo = await listOptions({\n        ...params,\n        cursor: pageOne.cursor,\n      })\n      const pageThree = await listOptions({\n        ...params,\n        cursor: pageTwo.cursor,\n      })\n      const pageFour = await listOptions({\n        ...params,\n        cursor: pageThree.cursor,\n      })\n\n      expect(pageFour.options.length).toBe(0)\n      expect(pageFour.cursor).toBeUndefined()\n    })\n  })\n\n  describe('removeOptions', () => {\n    afterAll(async () => {\n      await Promise.all([\n        removeOptions(['tools.ozone.setting.personal.labelers'], 'personal'),\n        removeOptions(\n          ['tools.ozone.setting.only.mod', 'tools.ozone.setting.only.admin'],\n          'instance',\n        ),\n      ])\n    })\n\n    it('only allows the owner to delete personal setting', async () => {\n      await upsertOption({\n        scope: 'personal',\n        key: 'tools.ozone.setting.personal.labelers',\n        value: { dids: ['did:plc:xyz'] },\n        description:\n          'My personal labelers that i want to use when browsing ozone',\n        managerRole: 'tools.ozone.team.defs#roleOwner',\n      })\n\n      // one user can't remove personal setting of another\n      await removeOptions(\n        ['tools.ozone.setting.personal.labelers'],\n        'personal',\n        'triage',\n      )\n      const list = await listOptions({ scope: 'personal' }, 'admin')\n      expect(list.options.length).toBe(1)\n\n      // the owner of the personal setting can remove their own setting\n      await removeOptions(['tools.ozone.setting.personal.labelers'], 'personal')\n      const listAfterRemoval = await listOptions({ scope: 'personal' }, 'admin')\n      expect(listAfterRemoval.options.length).toBe(0)\n    })\n\n    it('only allows managerRole to delete instance setting', async () => {\n      await Promise.all([\n        upsertOption({\n          scope: 'instance',\n          key: 'tools.ozone.setting.only.mod',\n          value: { dids: ['did:plc:xyz'] },\n          description: 'Triage mods can not manage these',\n          managerRole: 'tools.ozone.team.defs#roleModerator',\n        }),\n        upsertOption({\n          scope: 'instance',\n          key: 'tools.ozone.setting.only.admin',\n          value: { dids: ['did:plc:xyz'] },\n          description: 'Moderators or triage mods can not manage these',\n          managerRole: 'tools.ozone.team.defs#roleAdmin',\n        }),\n      ])\n\n      await Promise.all([\n        removeOptions(['tools.ozone.setting.only.mod'], 'instance', 'triage'),\n        removeOptions(\n          ['tools.ozone.setting.only.admin'],\n          'instance',\n          'moderator',\n        ),\n        removeOptions(['tools.ozone.setting.only.admin'], 'instance', 'triage'),\n      ])\n\n      const afterFailedAttempt = await listOptions(\n        { scope: 'instance', prefix: 'tools.ozone.setting.only' },\n        'admin',\n      )\n      const keysAfterFailedAttempt = afterFailedAttempt.options.map(\n        (o) => o.key,\n      )\n\n      const keys = [\n        'tools.ozone.setting.only.mod',\n        'tools.ozone.setting.only.admin',\n      ]\n\n      keys.forEach((key) => expect(keysAfterFailedAttempt).toContain(key))\n\n      await Promise.all([\n        removeOptions(['tools.ozone.setting.only.mod'], 'instance', 'admin'),\n        removeOptions(['tools.ozone.setting.only.admin'], 'instance', 'admin'),\n      ])\n\n      const afterRemoval = await listOptions(\n        { scope: 'instance', prefix: 'tools.ozone.setting.only' },\n        'admin',\n      )\n      expect(afterRemoval.options.length).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/strike-expiry-processor.test.ts",
    "content": "import AtpAgent from '@atproto/api'\nimport { SECOND } from '@atproto/common'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { SeverityLevelSettingKey } from '../src/setting/constants'\n\nconst strikeConfig = {\n  'sev-1': {\n    strikeCount: 1,\n    expiresInDays: 0, // Set to 0 so we can use future timestamps\n  },\n  'sev-2': {\n    strikeCount: 2,\n    expiresInDays: 0,\n  },\n}\n\ndescribe('strike expiry processor', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  const configureSeverityLevels = async () => {\n    await agent.tools.ozone.setting.upsertOption(\n      {\n        scope: 'instance',\n        key: SeverityLevelSettingKey,\n        value: strikeConfig,\n        description: 'Severity level configuration for strike system',\n        managerRole: 'tools.ozone.team.defs#roleAdmin',\n      },\n      {\n        encoding: 'application/json',\n        headers: await network.ozone.modHeaders(\n          ids.ToolsOzoneSettingUpsertOption,\n          'admin',\n        ),\n      },\n    )\n  }\n\n  const getAccountStatus = async (did: string) => {\n    const { subjectStatuses } = await modClient.queryStatuses({ subject: did })\n    return subjectStatuses[0]\n  }\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_strike_expiry_processor',\n    })\n    agent = network.ozone.getClient()\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await network.processAll()\n    await configureSeverityLevels()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('processes expired strikes and updates active strike count', async () => {\n    const bobDid = sc.dids.bob\n    const bobPost1 = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[bobDid][0].ref.uriStr,\n      cid: sc.posts[bobDid][0].ref.cidStr,\n    }\n    const bobPost2 = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: sc.posts[bobDid][1].ref.uriStr,\n      cid: sc.posts[bobDid][1].ref.cidStr,\n    }\n\n    // first strike on a post that expires in 2 seconds\n    const expiresAt1 = new Date(Date.now() + 2 * SECOND).toISOString()\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: 2,\n        strikeExpiresAt: expiresAt1,\n        comment: 'First violation - expires soon',\n      },\n      subject: bobPost1,\n    })\n\n    // second strike on another post that expires in 3 seconds\n    const expiresAt2 = new Date(Date.now() + 3 * SECOND).toISOString()\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        strikeCount: 1,\n        severityLevel: 'sev-1',\n        strikeExpiresAt: expiresAt2,\n        comment: 'Second violation - expires later',\n      },\n      subject: bobPost2,\n    })\n\n    // account-level event to ensure account status is created\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventComment',\n        comment: 'Account under review',\n      },\n      subject: repoSubject(bobDid),\n    })\n\n    // Verify initial state - both strikes are active\n    let status = await getAccountStatus(bobDid)\n    expect(status.accountStrike).toBeDefined()\n    expect(status.accountStrike!.activeStrikeCount).toBe(3) // 2 + 1\n    expect(status.accountStrike!.totalStrikeCount).toBe(3)\n\n    // Wait for first strike to expire\n    await new Promise((resolve) => setTimeout(resolve, 2.1 * SECOND))\n\n    // Run the processor\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n\n    // Verify first strike expired - only second strike remains active\n    status = await getAccountStatus(bobDid)\n    expect(status.accountStrike).toBeDefined()\n    expect(status.accountStrike!.activeStrikeCount).toBe(1) // Only second strike\n    expect(status.accountStrike!.totalStrikeCount).toBe(3) // Total unchanged\n\n    // Wait for second strike to expire\n    await new Promise((resolve) => setTimeout(resolve, 1 * SECOND))\n\n    // Run the processor again\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n\n    // Verify all strikes expired\n    status = await getAccountStatus(bobDid)\n    expect(status.accountStrike).toBeDefined()\n    expect(status.accountStrike!.activeStrikeCount).toBe(0)\n    expect(status.accountStrike!.totalStrikeCount).toBe(3) // Total unchanged\n  })\n\n  it('handles accounts with no expired strikes', async () => {\n    const aliceDid = sc.dids.alice\n\n    // strike that expires far in the future\n    const expiresAt = new Date(Date.now() + 1000 * SECOND).toISOString()\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: 2,\n        strikeExpiresAt: expiresAt,\n        comment: 'Future expiry',\n      },\n      subject: repoSubject(aliceDid),\n    })\n\n    // Get initial state\n    let status = await getAccountStatus(aliceDid)\n    expect(status.accountStrike).toBeDefined()\n    const initialActiveCount = status.accountStrike!.activeStrikeCount!\n    expect(initialActiveCount).toBe(2)\n\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n\n    // Verify nothing changed\n    status = await getAccountStatus(aliceDid)\n    expect(status.accountStrike).toBeDefined()\n    expect(status.accountStrike!.activeStrikeCount).toBe(initialActiveCount)\n  })\n\n  it('handles strikes with no expiry date (permanent strikes)', async () => {\n    const carolDid = sc.dids.carol\n\n    // permanent strike (no expiry)\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: 2,\n        comment: 'Permanent strike - no expiry',\n      },\n      subject: repoSubject(carolDid),\n    })\n\n    // Get initial state\n    let status = await getAccountStatus(carolDid)\n    expect(status.accountStrike).toBeDefined()\n    expect(status.accountStrike!.activeStrikeCount).toBe(2)\n\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n\n    // Verify permanent strikes remain active\n    status = await getAccountStatus(carolDid)\n    expect(status.accountStrike).toBeDefined()\n    expect(status.accountStrike!.activeStrikeCount).toBe(2)\n    expect(status.accountStrike!.totalStrikeCount).toBe(2)\n  })\n\n  it('processes multiple accounts with expired strikes in batch', async () => {\n    const danDid = 'did:plc:dan'\n    const eveDid = 'did:plc:eve'\n\n    const expiresAt = new Date(Date.now() + 1 * SECOND).toISOString()\n\n    // strikes to multiple accounts\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-1',\n        strikeCount: 1,\n        strikeExpiresAt: expiresAt,\n        comment: 'Dan violation',\n      },\n      subject: repoSubject(danDid),\n    })\n\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-2',\n        strikeCount: 2,\n        strikeExpiresAt: expiresAt,\n        comment: 'Eve violation',\n      },\n      subject: repoSubject(eveDid),\n    })\n\n    // Verify initial states\n    let danStatus = await getAccountStatus(danDid)\n    let eveStatus = await getAccountStatus(eveDid)\n    expect(danStatus.accountStrike?.activeStrikeCount).toBe(1)\n    expect(eveStatus.accountStrike?.activeStrikeCount).toBe(2)\n\n    // Wait for strikes to expire\n    await new Promise((resolve) => setTimeout(resolve, 1.1 * SECOND))\n\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n\n    // Verify both accounts' strikes expired\n    danStatus = await getAccountStatus(danDid)\n    eveStatus = await getAccountStatus(eveDid)\n    expect(danStatus.accountStrike?.activeStrikeCount).toBe(0)\n    expect(eveStatus.accountStrike?.activeStrikeCount).toBe(0)\n  })\n\n  it('updates cursor to track last processed timestamp', async () => {\n    const frankDid = 'did:plc:frank'\n    const expiresAt = new Date(Date.now() + 1 * SECOND).toISOString()\n\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        severityLevel: 'sev-1',\n        strikeCount: 1,\n        strikeExpiresAt: expiresAt,\n        comment: 'Frank violation',\n      },\n      subject: repoSubject(frankDid),\n    })\n\n    // Wait for strike to expire\n    await new Promise((resolve) => setTimeout(resolve, 1.1 * SECOND))\n\n    // Get cursor before processing\n    const cursorBefore =\n      await network.ozone.daemon.ctx.strikeExpiryProcessor.getCursor()\n\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n\n    // Get cursor after processing\n    const cursorAfter =\n      await network.ozone.daemon.ctx.strikeExpiryProcessor.getCursor()\n\n    expect(cursorAfter).not.toBe(cursorBefore)\n    expect(cursorAfter).toBeTruthy()\n\n    // Verify strike was processed\n    const status = await getAccountStatus(frankDid)\n    expect(status.accountStrike?.activeStrikeCount).toBe(0)\n\n    // running processor again should not reprocess the same strike\n    await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()\n    const cursorAfterSecond =\n      await network.ozone.daemon.ctx.strikeExpiryProcessor.getCursor()\n    expect(cursorAfterSecond).not.toBe(cursorAfter)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/subject-priority-score.test.ts",
    "content": "import {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\nimport { REASONSPAM } from '../dist/lexicon/types/com/atproto/moderation/defs'\n\ndescribe('moderation', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let modClient: ModeratorClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_priority_score',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    await basicSeed(sc)\n    await Promise.all([\n      sc.createReport({\n        reasonType: REASONSPAM,\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n        reportedBy: sc.dids.carol,\n      }),\n      sc.createReport({\n        reasonType: REASONSPAM,\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        reportedBy: sc.dids.carol,\n      }),\n      sc.createReport({\n        reasonType: REASONSPAM,\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: sc.posts[sc.dids.bob][0].ref.uriStr,\n          cid: sc.posts[sc.dids.bob][0].ref.cidStr,\n        },\n        reportedBy: sc.dids.carol,\n      }),\n    ])\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('allows setting a priority score.', async () => {\n    const { subjectStatuses: before } = await modClient.queryStatuses({})\n    await Promise.all([\n      modClient.emitEvent({\n        subject: before[before.length - 1].subject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventPriorityScore',\n          score: 10,\n        },\n      }),\n      modClient.emitEvent({\n        subject: before[before.length - 2].subject,\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventPriorityScore',\n          score: 5,\n        },\n      }),\n    ])\n    const { subjectStatuses: after } = await modClient.queryStatuses({\n      sortDirection: 'desc',\n      sortField: 'priorityScore',\n    })\n\n    // Verify that highest priority score item is first\n    expect(after[0].priorityScore).toBe(10)\n    expect(after[1].priorityScore).toBe(5)\n    expect(after[0].subject).toMatchObject(before[before.length - 1].subject)\n    expect(after[1].subject).toMatchObject(before[before.length - 2].subject)\n  })\n\n  it('allows setting a priority score.', async () => {\n    const { subjectStatuses } = await modClient.queryStatuses({\n      minPriorityScore: 6,\n      sortDirection: 'desc',\n      sortField: 'priorityScore',\n    })\n\n    // Verify that highest priority score item is first\n    expect(subjectStatuses[0].priorityScore).toBe(10)\n    expect(subjectStatuses.length).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/takedown.test.ts",
    "content": "import assert from 'node:assert'\nimport {\n  AtpAgent,\n  ComAtprotoAdminDefs,\n  ToolsOzoneModerationDefs,\n} from '@atproto/api'\nimport {\n  ModeratorClient,\n  SeedClient,\n  TestNetwork,\n  basicSeed,\n} from '@atproto/dev-env'\n\ndescribe('moderation', () => {\n  let network: TestNetwork\n\n  let sc: SeedClient\n  let modClient: ModeratorClient\n  let pdsAgent: AtpAgent\n  let bskyAgent: AtpAgent\n\n  const repoSubject = (did: string) => ({\n    $type: 'com.atproto.admin.defs#repoRef',\n    did,\n  })\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_takedown',\n    })\n    sc = network.getSeedClient()\n    modClient = network.ozone.getModClient()\n    pdsAgent = network.pds.getClient()\n    bskyAgent = network.bsky.getClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('allows specifying policy for takedown actions.', async () => {\n    await modClient.performTakedown({\n      subject: repoSubject(sc.dids.bob),\n      policies: ['trolling'],\n    })\n\n    // Verify that that the takedown even exposes the policy specified for it\n    const { events: eventViews } = await modClient.queryEvents({\n      subject: sc.dids.bob,\n      types: ['tools.ozone.moderation.defs#modEventTakedown'],\n    })\n\n    const { event } = eventViews[0]\n\n    assert(ToolsOzoneModerationDefs.isModEventTakedown(event))\n    expect(event.policies?.[0]).toEqual('trolling')\n\n    // Verify that event stream can be filtered by policy\n    const { events: filteredEvents } = await modClient.queryEvents({\n      subject: sc.dids.bob,\n      policies: ['trolling'],\n    })\n\n    const { subject } = filteredEvents[0]\n\n    assert(ComAtprotoAdminDefs.isRepoRef(subject))\n    expect(subject.did).toEqual(sc.dids.bob)\n  })\n\n  it('applies takedown only to specified service when targetServices is set', async () => {\n    await modClient.emitEvent({\n      event: {\n        $type: 'tools.ozone.moderation.defs#modEventTakedown',\n        targetServices: ['appview'],\n      },\n      subject: repoSubject(sc.dids.carol),\n    })\n\n    await network.processAll()\n\n    const [pdsStatus, appviewStatus, carolsEvents] = await Promise.all([\n      pdsAgent.com.atproto.admin.getSubjectStatus(\n        { did: sc.dids.carol },\n        { headers: network.pds.adminAuthHeaders() },\n      ),\n      bskyAgent.com.atproto.admin.getSubjectStatus(\n        { did: sc.dids.carol },\n        { headers: network.bsky.adminAuthHeaders() },\n      ),\n      modClient.queryEvents({\n        subject: sc.dids.carol,\n        types: ['tools.ozone.moderation.defs#modEventTakedown'],\n      }),\n    ])\n\n    expect(pdsStatus.data.takedown?.applied).toBe(false)\n    expect(appviewStatus.data.takedown?.applied).toBe(true)\n\n    const event = carolsEvents.events[0].event\n    assert(ToolsOzoneModerationDefs.isModEventTakedown(event))\n    expect(event.targetServices).toEqual(['appview'])\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/team.test.ts",
    "content": "import { AtpAgent, ToolsOzoneTeamDefs } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { forSnapshot } from './_util'\n\ndescribe('team management', () => {\n  let network: TestNetwork\n  let adminAgent: AtpAgent\n  let triageAgent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_team_test',\n      ozone: {\n        dbTeamProfileRefreshIntervalMs: 100,\n      },\n    })\n    adminAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    await network.ozone.addAdminDid(sc.dids.alice)\n    await network.ozone.addModeratorDid(sc.dids.bob)\n    await network.ozone.addTriageDid(sc.dids.carol)\n    await adminAgent.login({\n      identifier: sc.accounts[sc.dids.alice].handle,\n      password: sc.accounts[sc.dids.alice].password,\n    })\n    triageAgent = network.pds.getClient()\n    await triageAgent.login({\n      identifier: sc.accounts[sc.dids.carol].handle,\n      password: sc.accounts[sc.dids.carol].password,\n    })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('listMembers', () => {\n    it('allows all members to list all members', async () => {\n      const [{ data: forAdmin }, { data: forTriage }] = await Promise.all([\n        adminAgent.tools.ozone.team.listMembers({}),\n        triageAgent.tools.ozone.team.listMembers({}),\n      ])\n\n      expect(forSnapshot(forAdmin.members)).toMatchSnapshot()\n      expect(forSnapshot(forTriage.members)).toMatchSnapshot()\n      // Validate that the list looks the same to both admin and triage members\n\n      expect(forAdmin.members.length).toEqual(forTriage.members.length)\n    })\n    it('allows filtering members by role', async () => {\n      const [{ data: onlyAdmins }, { data: onlyTriage }] = await Promise.all([\n        adminAgent.tools.ozone.team.listMembers({\n          roles: [ToolsOzoneTeamDefs.ROLEADMIN],\n        }),\n        adminAgent.tools.ozone.team.listMembers({\n          roles: [ToolsOzoneTeamDefs.ROLETRIAGE],\n        }),\n      ])\n\n      expect(\n        onlyAdmins.members.find(\n          ({ role }) => role !== ToolsOzoneTeamDefs.ROLEADMIN,\n        ),\n      ).toBeUndefined()\n\n      expect(\n        onlyTriage.members.find(\n          ({ role }) => role !== ToolsOzoneTeamDefs.ROLETRIAGE,\n        ),\n      ).toBeUndefined()\n    })\n    it('allows filtering members by disabled status', async () => {\n      const [{ data: onlyDisabled }, { data: onlyEnabled }] = await Promise.all(\n        [\n          adminAgent.tools.ozone.team.listMembers({\n            disabled: true,\n          }),\n          adminAgent.tools.ozone.team.listMembers({\n            disabled: false,\n          }),\n        ],\n      )\n\n      expect(\n        onlyDisabled.members.find(({ disabled }) => !disabled),\n      ).toBeUndefined()\n\n      expect(\n        onlyEnabled.members.find(({ disabled }) => disabled),\n      ).toBeUndefined()\n    })\n    it('allows filtering members by handle/display name', async () => {\n      const [{ data: matchingHandle }, { data: matchingName }] =\n        await Promise.all([\n          adminAgent.tools.ozone.team.listMembers({\n            q: 'bob',\n          }),\n          adminAgent.tools.ozone.team.listMembers({\n            q: 'dev',\n          }),\n        ])\n\n      expect(matchingHandle.members.length).toEqual(1)\n      expect(matchingHandle.members[0]?.profile?.handle).toEqual('bob.test')\n      expect(matchingName.members.length).toEqual(1)\n      expect(matchingName.members[0]?.profile?.handle).toEqual(\n        'mod-authority.test',\n      )\n    })\n  })\n\n  describe('addMember', () => {\n    const newMemberData = {\n      did: 'did:plc:newMember',\n      role: 'tools.ozone.team.defs#roleAdmin',\n      disabled: false,\n    }\n    it('only allows admins to add member', async () => {\n      await expect(\n        triageAgent.tools.ozone.team.addMember(newMemberData),\n      ).rejects.toThrow('Must be an admin to add a member')\n      const { data: newMember } =\n        await adminAgent.tools.ozone.team.addMember(newMemberData)\n      expect(forSnapshot(newMember)).toMatchSnapshot()\n    })\n    it('throws error when trying to add existing member', async () => {\n      await expect(\n        adminAgent.tools.ozone.team.addMember(newMemberData),\n      ).rejects.toThrow('member already exists')\n    })\n  })\n  describe('deleteMember', () => {\n    it('only allows admins to delete members', async () => {\n      const {\n        data: { members: initialMembers },\n      } = await adminAgent.tools.ozone.team.listMembers({})\n      await expect(\n        triageAgent.tools.ozone.team.deleteMember({\n          did: sc.dids.bob,\n        }),\n      ).rejects.toThrow('Must be an admin to delete a member')\n\n      await adminAgent.tools.ozone.team.deleteMember({\n        did: sc.dids.bob,\n      })\n      const {\n        data: { members: membersAfterDelete },\n      } = await adminAgent.tools.ozone.team.listMembers({})\n\n      expect(membersAfterDelete.length).toEqual(initialMembers.length - 1)\n      expect(membersAfterDelete.map(({ did }) => did)).not.toContain(\n        sc.dids.bob,\n      )\n    })\n\n    it('throws error when trying to remove non-existent member', async () => {\n      await expect(\n        adminAgent.tools.ozone.team.deleteMember({\n          did: 'did:plc:test',\n        }),\n      ).rejects.toThrow('member not found')\n    })\n  })\n  describe('updateMember', () => {\n    it('allows admins to update member', async () => {\n      const getCarol = async () => {\n        const {\n          data: { members },\n        } = await adminAgent.tools.ozone.team.listMembers({})\n\n        return members.find(({ did }) => did === sc.dids.carol)\n      }\n      await expect(\n        triageAgent.tools.ozone.team.updateMember({\n          disabled: false,\n          did: sc.dids.carol,\n          role: 'tools.ozone.team.defs#roleAdmin',\n        }),\n      ).rejects.toThrow('Must be an admin to update a member')\n\n      await adminAgent.tools.ozone.team.updateMember({\n        did: sc.dids.carol,\n        role: 'tools.ozone.team.defs#roleAdmin',\n      })\n      const carolAfterRoleChange = await getCarol()\n      expect(carolAfterRoleChange?.role).toEqual(\n        'tools.ozone.team.defs#roleAdmin',\n      )\n      // Verify that params that we didn't send did not get updated\n      expect(carolAfterRoleChange?.disabled).toEqual(false)\n\n      await adminAgent.tools.ozone.team.updateMember({\n        did: sc.dids.carol,\n        disabled: true,\n      })\n      const carolAfterDisable = await getCarol()\n      expect(carolAfterDisable?.disabled).toEqual(true)\n      // Verify that params that we didn't send did not get updated\n      expect(carolAfterDisable?.role).toEqual('tools.ozone.team.defs#roleAdmin')\n    })\n    it('throws error when trying to update non-existent member', async () => {\n      await expect(\n        adminAgent.tools.ozone.team.updateMember({\n          disabled: false,\n          did: 'did:plc:test',\n          role: 'tools.ozone.team.defs#roleAdmin',\n        }),\n      ).rejects.toThrow('member not found')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/verification-listener.test.ts",
    "content": "import { Sender, WebSocketServer } from 'ws'\nimport { AppBskyGraphVerification, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { forSnapshot } from './_util'\n\ndescribe('verification-listener', () => {\n  let network: TestNetwork\n  let sc: SeedClient\n  let adminAgent: AtpAgent\n  let jetstream: WebSocketServer\n  let relay: Sender\n\n  beforeAll(async () => {\n    const jetstreamPort = 2511\n    jetstream = new WebSocketServer({\n      port: jetstreamPort,\n    })\n    jetstream.on('connection', (ws) => {\n      relay = ws\n    })\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_verification_listener_test',\n      ozone: {\n        verifierUrl: 'http://localhost:2583',\n        verifierDid: 'did:example:verifier',\n        verifierPassword: 'test',\n        jetstreamUrl: `ws://localhost:${jetstreamPort}`,\n      },\n    })\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    adminAgent = network.pds.getClient()\n    await adminAgent.login({\n      identifier: sc.accounts[sc.dids.alice].handle,\n      password: sc.accounts[sc.dids.alice].password,\n    })\n    await network.ozone.addAdminDid(sc.dids.alice)\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await jetstream.close()\n    await network.close()\n  })\n\n  it('indexes new and revoked verifications', async () => {\n    const { verificationListener } = network.ozone.daemon.ctx\n    const createEvent = {\n      kind: 'commit',\n      did: sc.dids.bob,\n      time_us: 123456789,\n      commit: {\n        rev: 'xyz',\n        operation: 'create',\n        collection: 'app.bsky.graph.verification',\n        rkey: 'abcdefg',\n        cid: 'xyz',\n        record: {\n          $type: 'app.bsky.graph.verification',\n          subject: sc.dids.alice,\n          handle: sc.accounts[sc.dids.alice].handle,\n          displayName: 'Alice',\n          createdAt: new Date().toISOString(),\n        } satisfies AppBskyGraphVerification.Record,\n      },\n    }\n    const deleteEvent = {\n      kind: 'commit',\n      did: sc.dids.bob,\n      time_us: 123456799,\n      commit: {\n        rev: 'yza',\n        operation: 'delete',\n        collection: 'app.bsky.graph.verification',\n        rkey: 'abcdefg',\n      },\n    }\n    relay.send(JSON.stringify(createEvent))\n    relay.send(JSON.stringify(deleteEvent))\n    const verificationService = network.ozone.ctx.verificationService(\n      network.ozone.ctx.db,\n    )\n    // Wait for the listener to process the events\n    let hasCursorUpdated = false\n    let attempt = 0\n    do {\n      const cursor = await verificationService.getFirehoseCursor()\n      hasCursorUpdated = cursor === 123456799\n      attempt++\n    } while (!hasCursorUpdated && attempt < 20)\n    // Give the processor enough time to handle the events\n    const {\n      data: { verifications },\n    } = await adminAgent.tools.ozone.verification.listVerifications({})\n    const cursor = await verificationListener?.getCursor()\n\n    expect(forSnapshot(verifications)).toMatchSnapshot()\n    expect(cursor).toEqual(123456799)\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tests/verification.test.ts",
    "content": "import { AppBskyActorDefs, AtpAgent, asPredicate } from '@atproto/api'\nimport { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'\nimport { forSnapshot } from './_util'\n\nconst isValidProfile = asPredicate(AppBskyActorDefs.validateProfileViewDetailed)\n\ndescribe('verification', () => {\n  let network: TestNetwork\n  let adminAgent: AtpAgent\n  let triageAgent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'ozone_verification_test',\n    })\n    adminAgent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n\n    await network.ozone.addAdminDid(sc.dids.alice)\n    await network.ozone.addModeratorDid(sc.dids.bob)\n    await network.ozone.addTriageDid(sc.dids.carol)\n    await adminAgent.login({\n      identifier: sc.accounts[sc.dids.alice].handle,\n      password: sc.accounts[sc.dids.alice].password,\n    })\n    triageAgent = network.pds.getClient()\n    await triageAgent.login({\n      identifier: sc.accounts[sc.dids.carol].handle,\n      password: sc.accounts[sc.dids.carol].password,\n    })\n    const {\n      data: { password },\n    } = await adminAgent.com.atproto.server.createAppPassword({\n      name: 'verifier',\n    })\n    network.ozone.ctx.cfg.verifier = {\n      url: network.pds.url,\n      did: sc.dids.alice,\n      password,\n    }\n\n    await network.processAll()\n    await network.bsky.db.db\n      .updateTable('actor')\n      .set({ trustedVerifier: true })\n      .where('did', 'in', [sc.dids.alice])\n      .execute()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  describe('list', () => {\n    // @TODO: This tests encapsulates the entire grant->revoke->list flow. we should have more detailed test for each path\n    it('returns paginated list of verifications', async () => {\n      const {\n        data: { verifications },\n      } = await adminAgent.tools.ozone.verification.grantVerifications({\n        verifications: [\n          {\n            subject: sc.dids.bob,\n            handle: sc.accounts[sc.dids.bob].handle,\n            displayName: 'bobby',\n          },\n          {\n            subject: sc.dids.carol,\n            handle: sc.accounts[sc.dids.carol].handle,\n            displayName: '',\n          },\n        ],\n      })\n\n      const grantedVerificationUri = verifications.find(\n        (v) => v.subject === sc.dids.carol,\n      )?.uri\n\n      expect(grantedVerificationUri).toBeDefined()\n\n      if (grantedVerificationUri) {\n        await adminAgent.tools.ozone.verification.revokeVerifications({\n          uris: [grantedVerificationUri],\n          revokeReason: 'Testing',\n        })\n      }\n\n      await network.processAll()\n\n      const { data } =\n        await adminAgent.tools.ozone.verification.listVerifications({})\n\n      expect(data.verifications.find((v) => v.revokedAt)?.uri).toEqual(\n        grantedVerificationUri,\n      )\n      const bob = data.verifications.find((v) => v.subject === sc.dids.bob)\n      const carol = data.verifications.find((v) => v.subject === sc.dids.carol)\n\n      if (\n        !isValidProfile(bob?.subjectProfile) ||\n        !isValidProfile(carol?.subjectProfile)\n      ) {\n        throw Error('Invalid profiles')\n      }\n\n      expect(forSnapshot(bob)).toMatchSnapshot()\n      expect(forSnapshot(carol)).toMatchSnapshot()\n\n      // Assert that profile record carries valid verification status for bob but not for carol\n      expect(carol.revokedAt).toBeDefined()\n      expect(carol.revokeReason).toEqual('Testing')\n      expect(carol.subjectProfile.verification).toBeUndefined()\n      expect(bob.subjectProfile?.verification?.verifiedStatus).toEqual('valid')\n    })\n  })\n\n  describe('grant', () => {\n    it('fails for non-admins and non-verifiers', async () => {\n      const attemptAsAdmin =\n        triageAgent.tools.ozone.verification.grantVerifications({\n          verifications: [\n            {\n              subject: sc.dids.bob,\n              handle: sc.accounts[sc.dids.bob].handle,\n              displayName: 'Bob',\n            },\n          ],\n        })\n      await expect(attemptAsAdmin).rejects.toThrow(\n        'Must be an admin or verifier to grant verifications',\n      )\n    })\n\n    it('fails if the handle is invalid', async () => {\n      const {\n        data: { verifications, failedVerifications },\n      } = await adminAgent.tools.ozone.verification.grantVerifications({\n        verifications: [\n          {\n            subject: sc.dids.dan,\n            handle: 'handle.invalid',\n            displayName: 'Bob',\n          },\n        ],\n      })\n\n      expect(verifications.length).toEqual(0)\n      expect(failedVerifications.length).toEqual(1)\n      const failed = failedVerifications.at(0)\n      expect(failed!.error).toEqual('Cannot verify with invalid handle')\n      expect(failed!.subject).toEqual(sc.dids.dan)\n    })\n  })\n\n  it('does not publish record if a valid one already exists', async () => {\n    const { data: beforePublish } =\n      await adminAgent.tools.ozone.verification.listVerifications({\n        subjects: [sc.dids.bob],\n      })\n    const {\n      data: { verifications },\n    } = await adminAgent.tools.ozone.verification.grantVerifications({\n      verifications: [\n        {\n          subject: sc.dids.bob,\n          handle: sc.accounts[sc.dids.bob].handle,\n          displayName: 'bobby',\n        },\n      ],\n    })\n\n    const { data: afterPublish } =\n      await adminAgent.tools.ozone.verification.listVerifications({\n        subjects: [sc.dids.bob],\n      })\n\n    // assert that the response does not contain any new verification\n    expect(verifications.length).toEqual(0)\n    // assert that the list of verifications in db hasn't changed\n    expect(afterPublish.verifications.length).toEqual(\n      beforePublish.verifications.length,\n    )\n  })\n})\n"
  },
  {
    "path": "packages/ozone/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/ozone/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/ozone/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/pds/CHANGELOG.md",
    "content": "# @atproto/pds\n\n## 0.4.216\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-data@0.0.14\n  - @atproto/xrpc-server@0.10.17\n  - @atproto/common@0.5.15\n  - @atproto/lex-cbor@0.0.15\n  - @atproto/oauth-provider@0.15.14\n\n## 0.4.215\n\n### Patch Changes\n\n- [#4712](https://github.com/bluesky-social/atproto/pull/4712) [`383e157`](https://github.com/bluesky-social/atproto/commit/383e157021564a6fb51baac584dd3e4f988f1d33) Thanks [@devinivy](https://github.com/devinivy)! - remove format from img urls by default\n\n- [#4709](https://github.com/bluesky-social/atproto/pull/4709) [`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa) Thanks [@foysalit](https://github.com/foysalit)! - Introduce a purge event to remove ozone's data on age assurance\n\n- Updated dependencies [[`9f9f71a`](https://github.com/bluesky-social/atproto/commit/9f9f71a6a3e58ccbd5e6d3ee079b570096cb11fa), [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f), [`192685f`](https://github.com/bluesky-social/atproto/commit/192685fca75a68c9c50a94817d3f27da7fc02f56)]:\n  - @atproto/api@0.19.4\n  - @atproto/syntax@0.5.1\n  - @atproto/repo@0.8.13\n  - @atproto/xrpc-server@0.10.16\n  - @atproto/oauth-provider@0.15.13\n\n## 0.4.214\n\n### Patch Changes\n\n- [#4683](https://github.com/bluesky-social/atproto/pull/4683) [`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Introduce recIdStr field\n\n- Updated dependencies [[`6634140`](https://github.com/bluesky-social/atproto/commit/66341400d49d1210619b000a040852d87085c32c), [`0e5df95`](https://github.com/bluesky-social/atproto/commit/0e5df95e3a8d81931524848d301cd43d1f12fb78)]:\n  - @atproto/api@0.19.2\n\n## 0.4.213\n\n### Patch Changes\n\n- [#4698](https://github.com/bluesky-social/atproto/pull/4698) [`d5f4224`](https://github.com/bluesky-social/atproto/commit/d5f4224f73894a62d23d2375950cdadce6f130f4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Updates internal pipethough error handling\n\n- [#4704](https://github.com/bluesky-social/atproto/pull/4704) [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42) Thanks [@ds-boyce](https://github.com/ds-boyce)! - Add feed to sendInteractions input\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`137065b`](https://github.com/bluesky-social/atproto/commit/137065b333b8c9b97e6b3b2ac6147c7509a1ae42), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common@0.5.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/xrpc-server@0.10.15\n  - @atproto/api@0.19.1\n  - @atproto/lexicon@0.6.2\n  - @atproto/oauth-provider@0.15.12\n  - @atproto/oauth-scopes@0.3.2\n  - @atproto/lex-cbor@0.0.14\n\n## 0.4.212\n\n### Patch Changes\n\n- Updated dependencies [[`450f085`](https://github.com/bluesky-social/atproto/commit/450f0856630fa08c20dc60fef8b5d2a07b9a2552)]:\n  - @atproto/api@0.19.0\n  - @atproto/oauth-provider@0.15.11\n\n## 0.4.211\n\n### Patch Changes\n\n- Updated dependencies [[`66b7295`](https://github.com/bluesky-social/atproto/commit/66b72950e8bcb39cac3382116bd282b3bb692f16), [`dc9644b`](https://github.com/bluesky-social/atproto/commit/dc9644bbeb1892931809568895162d823e4743d2)]:\n  - @atproto/lex-cbor@0.0.13\n  - @atproto/identity@0.4.12\n  - @atproto/common@0.5.13\n  - @atproto/xrpc-server@0.10.14\n\n## 0.4.210\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n  - @atproto/oauth-provider@0.15.10\n  - @atproto/common@0.5.12\n  - @atproto/lex-cbor@0.0.12\n  - @atproto/xrpc-server@0.10.13\n\n## 0.4.209\n\n### Patch Changes\n\n- [#4616](https://github.com/bluesky-social/atproto/pull/4616) [`8711f6e`](https://github.com/bluesky-social/atproto/commit/8711f6e1b870d27080a4cb7e56e58bf538ab5778) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Force `getBlob` to trigger a browser download\n\n- Updated dependencies [[`60f84eb`](https://github.com/bluesky-social/atproto/commit/60f84ebe47016828add07b143c403e331c58ee78), [`50dfbec`](https://github.com/bluesky-social/atproto/commit/50dfbec512682d35e8108b952e8f0533da71beef), [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317), [`f8c84eb`](https://github.com/bluesky-social/atproto/commit/f8c84ebd3db960234cbd72dae6d1ab57d9361317)]:\n  - @atproto/api@0.18.21\n\n## 0.4.208\n\n### Patch Changes\n\n- Updated dependencies [[`a2e4e95`](https://github.com/bluesky-social/atproto/commit/a2e4e9584730c1742aca7c1fcc59533a7c159740), [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b), [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b), [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b), [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`00e6dbd`](https://github.com/bluesky-social/atproto/commit/00e6dbdcea295cfa3dff7eb7517420039cc3e821), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`78fee14`](https://github.com/bluesky-social/atproto/commit/78fee144ff46ffc4585f318c72eea98e4357ba7b)]:\n  - @atproto/oauth-provider@0.15.8\n  - @atproto/lex-cbor@0.0.11\n  - @atproto/lex-data@0.0.11\n  - @atproto/identity@0.4.11\n  - @atproto/common@0.5.11\n  - @atproto/xrpc-server@0.10.12\n\n## 0.4.207\n\n### Patch Changes\n\n- Updated dependencies [[`25cea46`](https://github.com/bluesky-social/atproto/commit/25cea46aaa3d84521d1e977b67d3ac3581304ba1), [`49b3806`](https://github.com/bluesky-social/atproto/commit/49b38069ed4b5bd1ef71e967c78e5123b1c1f6f1), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/api@0.18.19\n  - @atproto/common@0.5.10\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-cbor@0.0.10\n  - @atproto/xrpc-server@0.10.11\n  - @atproto/oauth-provider@0.15.7\n\n## 0.4.206\n\n### Patch Changes\n\n- [#4581](https://github.com/bluesky-social/atproto/pull/4581) [`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7) Thanks [@mozzius](https://github.com/mozzius)! - Add `presentation` to video embed as a hint to the client about how to display the video\n\n- Updated dependencies [[`2830dae`](https://github.com/bluesky-social/atproto/commit/2830daeaa6f580fbf777a0f832d64a6579616dc7)]:\n  - @atproto/api@0.18.18\n  - @atproto/oauth-provider@0.15.6\n  - @atproto/oauth-scopes@0.3.1\n\n## 0.4.205\n\n### Patch Changes\n\n- [#4564](https://github.com/bluesky-social/atproto/pull/4564) [`3fbec80`](https://github.com/bluesky-social/atproto/commit/3fbec803ed188cef9baa998ac3e66ccb8c0f1e5c) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - Stop probing image dimensions unnecessarily (the data was never used)\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove un-necessary call to `noUndefinedVals`\n\n- Updated dependencies [[`cbd5837`](https://github.com/bluesky-social/atproto/commit/cbd5837f015e6b5e098a60098faea82e7f9419f3), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`d8e5363`](https://github.com/bluesky-social/atproto/commit/d8e53636c84da6dd3dd69e1d260f4fa617f3883c), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`9bdd358`](https://github.com/bluesky-social/atproto/commit/9bdd35881aa7efce6595ef708ba13d99c473d114), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`ecf5921`](https://github.com/bluesky-social/atproto/commit/ecf59214d59d9d2530c197c0679d26e76c6a60ef), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`fa4ef5e`](https://github.com/bluesky-social/atproto/commit/fa4ef5e8150b6ae7fabdc90b847370481e1a6b33), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/api@0.18.17\n  - @atproto/syntax@0.4.3\n  - @atproto/lex-cbor@0.0.9\n  - @atproto/lexicon@0.6.1\n  - @atproto/oauth-provider@0.15.5\n  - @atproto/lex-data@0.0.9\n  - @atproto/common@0.5.9\n  - @atproto/xrpc-server@0.10.10\n\n## 0.4.204\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`3ffebd0`](https://github.com/bluesky-social/atproto/commit/3ffebd0bf25776308e06e4b083dc2d0e156d9ac0), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/api@0.18.14\n  - @atproto/common@0.5.8\n  - @atproto/lex-cbor@0.0.8\n  - @atproto/xrpc-server@0.10.9\n  - @atproto/oauth-provider@0.15.4\n\n## 0.4.203\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-cbor@0.0.7\n  - @atproto/common@0.5.7\n  - @atproto/xrpc-server@0.10.8\n  - @atproto/oauth-provider@0.15.3\n\n## 0.4.202\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n  - @atproto/common@0.5.6\n  - @atproto/lex-cbor@0.0.6\n  - @atproto/xrpc-server@0.10.7\n  - @atproto/oauth-provider@0.15.2\n\n## 0.4.201\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n  - @atproto/common@0.5.5\n  - @atproto/lex-cbor@0.0.5\n  - @atproto/xrpc-server@0.10.6\n  - @atproto/oauth-provider@0.15.1\n\n## 0.4.200\n\n### Patch Changes\n\n- [#4470](https://github.com/bluesky-social/atproto/pull/4470) [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `refreshSession` now returns `email` and `emailConfirmed` fields (same as `createSession` and `getSession`)\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`10cf1c1`](https://github.com/bluesky-social/atproto/commit/10cf1c10188596724b0c38a5af507d95a382a164), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41), [`5d8e7a6`](https://github.com/bluesky-social/atproto/commit/5d8e7a6588fc9e57e15d83d47bb45103205e3e41)]:\n  - @atproto/lex-data@0.0.4\n  - @atproto/api@0.18.9\n  - @atproto/lex-cbor@0.0.4\n  - @atproto/oauth-provider@0.15.0\n  - @atproto/common@0.5.4\n  - @atproto/xrpc-server@0.10.5\n\n## 0.4.199\n\n### Patch Changes\n\n- [#4432](https://github.com/bluesky-social/atproto/pull/4432) [`39fa570`](https://github.com/bluesky-social/atproto/commit/39fa57080fa04aa547b093cfeaaced3e2e62fc41) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add new read-only `#declaredAgePref` with computed age flags e.g. `isOverAge18`.\n\n- Updated dependencies [[`39fa570`](https://github.com/bluesky-social/atproto/commit/39fa57080fa04aa547b093cfeaaced3e2e62fc41), [`f4cef84`](https://github.com/bluesky-social/atproto/commit/f4cef84494114ca927c66428920ca3dc24ad2b1e)]:\n  - @atproto/api@0.18.6\n\n## 0.4.198\n\n### Patch Changes\n\n- [#4423](https://github.com/bluesky-social/atproto/pull/4423) [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763) Thanks [@foysalit](https://github.com/foysalit)! - Add min length for required comment fields in ozone events\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`380aa3b`](https://github.com/bluesky-social/atproto/commit/380aa3bfe73b5c4e59961c27ae988786b69c129d), [`308f432`](https://github.com/bluesky-social/atproto/commit/308f432f7aef196b4df0a6dc7c5367ab5a8b8964), [`a6e16cd`](https://github.com/bluesky-social/atproto/commit/a6e16cd0cd3029caf63ce2312dc5207532654763), [`7e1d458`](https://github.com/bluesky-social/atproto/commit/7e1d45877bca0f615e7b1313cfcc66823b3de758)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/lexicon@0.6.0\n  - @atproto/api@0.18.5\n  - @atproto/lex-cbor@0.0.3\n  - @atproto/common@0.5.3\n  - @atproto/xrpc-server@0.10.3\n  - @atproto/repo@0.8.12\n  - @atproto/xrpc@0.7.7\n  - @atproto/oauth-provider@0.14.1\n\n## 0.4.197\n\n### Patch Changes\n\n- Updated dependencies [[`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db), [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69), [`8012627`](https://github.com/bluesky-social/atproto/commit/8012627a1226cb2f1c753385ad2497b6b43ffd2e), [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`90f1569`](https://github.com/bluesky-social/atproto/commit/90f15698ee63d9a7374f1206754eda5d530873d7), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4), [`0adc852`](https://github.com/bluesky-social/atproto/commit/0adc852c31ffa154c1b93e38182c35880ecdb4ba), [`be8e6c1`](https://github.com/bluesky-social/atproto/commit/be8e6c1f25814202b98e2616a217599a6c46e0db)]:\n  - @atproto/oauth-provider@0.14.0\n  - @atproto/oauth-scopes@0.3.0\n  - @atproto/lex-data@0.0.2\n  - @atproto/lex-cbor@0.0.2\n  - @atproto/syntax@0.4.2\n  - @atproto/crypto@0.4.5\n  - @atproto/api@0.18.4\n  - @atproto/common@0.5.2\n  - @atproto/xrpc-server@0.10.2\n\n## 0.4.196\n\n### Patch Changes\n\n- [#4347](https://github.com/bluesky-social/atproto/pull/4347) [`69f53d6`](https://github.com/bluesky-social/atproto/commit/69f53d632d84f255cafa8b10698184048a71b97b) Thanks [@bnewbold](https://github.com/bnewbold)! - lexicon updates to have fully-qualified token refs in knownValue lists\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af), [`69f53d6`](https://github.com/bluesky-social/atproto/commit/69f53d632d84f255cafa8b10698184048a71b97b)]:\n  - @atproto/lex-cbor@0.0.1\n  - @atproto/lex-data@0.0.1\n  - @atproto/api@0.18.3\n  - @atproto/common@0.5.1\n  - @atproto/xrpc-server@0.10.1\n\n## 0.4.195\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/xrpc-server@0.10.0\n  - @atproto/common@0.5.0\n  - @atproto/api@0.18.2\n  - @atproto/aws@0.2.31\n  - @atproto/identity@0.4.10\n  - @atproto/lexicon@0.5.2\n  - @atproto/repo@0.8.11\n  - @atproto-labs/xrpc-utils@0.0.24\n  - @atproto/crypto@0.4.4\n  - @atproto/lexicon-resolver@0.2.4\n  - @atproto/oauth-provider@0.13.5\n  - @atproto/oauth-scopes@0.2.2\n  - @atproto/xrpc@0.7.6\n\n## 0.4.194\n\n### Patch Changes\n\n- [#4340](https://github.com/bluesky-social/atproto/pull/4340) [`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e) Thanks [@foysalit](https://github.com/foysalit)! - Add optional email data to scheduled action api in ozone\n\n- [#4344](https://github.com/bluesky-social/atproto/pull/4344) [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc) Thanks [@foysalit](https://github.com/foysalit)! - Add targetServices param to takedown events allowing mods to specify which service to apply takedown on\n\n- Updated dependencies [[`032abf6b5`](https://github.com/bluesky-social/atproto/commit/032abf6b500fd36f3c0fc1af83bf62caae44fa6e), [`9115325c7`](https://github.com/bluesky-social/atproto/commit/9115325c7b36f0293f87f79bb8edb49f72fec2bc), [`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/api@0.18.1\n  - @atproto/xrpc-server@0.9.6\n  - @atproto-labs/xrpc-utils@0.0.23\n\n## 0.4.193\n\n### Patch Changes\n\n- [#4330](https://github.com/bluesky-social/atproto/pull/4330) [`3628cebfb`](https://github.com/bluesky-social/atproto/commit/3628cebfbb04ba49f326bbf411a2d15de2900302) Thanks [@mistydemeo](https://github.com/mistydemeo)! - adjust explicit-slurs regex\n\n## 0.4.192\n\n### Patch Changes\n\n- Updated dependencies [[`94ddc8219`](https://github.com/bluesky-social/atproto/commit/94ddc8219c144475df622137ab88895255136eda), [`39b5c08e0`](https://github.com/bluesky-social/atproto/commit/39b5c08e0799468eba0c3bf50f4f5a8104c35f34)]:\n  - @atproto/api@0.18.0\n\n## 0.4.191\n\n### Patch Changes\n\n- Updated dependencies [[`15fe80c39`](https://github.com/bluesky-social/atproto/commit/15fe80c39ff428652dfaa6b30c0bdb59a145aac6)]:\n  - @atproto/api@0.17.7\n\n## 0.4.190\n\n### Patch Changes\n\n- Updated dependencies [[`7c1429fe3`](https://github.com/bluesky-social/atproto/commit/7c1429fe36226d0d57e57c037ba4221d2fbd57ee)]:\n  - @atproto/api@0.17.6\n\n## 0.4.189\n\n### Patch Changes\n\n- [#4279](https://github.com/bluesky-social/atproto/pull/4279) [`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031) Thanks [@foysalit](https://github.com/foysalit)! - Add strike system to ozone\n\n- Updated dependencies [[`601401afc`](https://github.com/bluesky-social/atproto/commit/601401afce9f4da2e8a257f8dcca996dd64e6031)]:\n  - @atproto/api@0.17.5\n\n## 0.4.188\n\n### Patch Changes\n\n- Updated dependencies [[`f496fa2c4`](https://github.com/bluesky-social/atproto/commit/f496fa2c4d9316229523454c691c75c269aba21e)]:\n  - @atproto/oauth-provider@0.13.4\n\n## 0.4.187\n\n### Patch Changes\n\n- Updated dependencies [[`8c03d75b6`](https://github.com/bluesky-social/atproto/commit/8c03d75b6c11bed15b58bfa7ff4bf68199fc6511), [`a8e307ef4`](https://github.com/bluesky-social/atproto/commit/a8e307ef4851b164ee38bb5149343631e329f143), [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58), [`1e702ea67`](https://github.com/bluesky-social/atproto/commit/1e702ea675e3697e050be1f28e54bb1298b56436)]:\n  - @atproto/oauth-provider@0.13.3\n  - @atproto/api@0.17.4\n  - @atproto-labs/fetch-node@0.2.0\n  - @atproto/lexicon-resolver@0.2.3\n\n## 0.4.186\n\n### Patch Changes\n\n- Updated dependencies [[`386f583cf`](https://github.com/bluesky-social/atproto/commit/386f583cffa2c596a12be4e98dde498f3b8670f6)]:\n  - @atproto/api@0.17.3\n\n## 0.4.185\n\n### Patch Changes\n\n- Updated dependencies [[`1cb5b9b80`](https://github.com/bluesky-social/atproto/commit/1cb5b9b80c20a054f7fbacd89d0d440dc2241d81)]:\n  - @atproto/api@0.17.2\n\n## 0.4.184\n\n### Patch Changes\n\n- [#4241](https://github.com/bluesky-social/atproto/pull/4241) [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd) Thanks [@foysalit](https://github.com/foysalit)! - Add scheduled action api to ozone\n\n- Updated dependencies [[`e71d265dd`](https://github.com/bluesky-social/atproto/commit/e71d265dd4ef35dcd5bb7606b528f417d6af2b70), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3), [`591de1952`](https://github.com/bluesky-social/atproto/commit/591de19524639341a7dd64ee75c482c645c186fd)]:\n  - @atproto/oauth-provider@0.13.2\n  - @atproto/api@0.17.1\n  - @atproto/oauth-scopes@0.2.1\n\n## 0.4.183\n\n### Patch Changes\n\n- Updated dependencies [[`dba2d30e2`](https://github.com/bluesky-social/atproto/commit/dba2d30e2c4ce0eb624f2139b485719d14474940), [`7f38ee03c`](https://github.com/bluesky-social/atproto/commit/7f38ee03c01357686a4ce54cdf8eed4e37074a58)]:\n  - @atproto/api@0.17.0\n\n## 0.4.182\n\n### Patch Changes\n\n- Updated dependencies [[`8dd77bad2`](https://github.com/bluesky-social/atproto/commit/8dd77bad2fdee20e39d3787198d960c19d8df3d0), [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33), [`1a5d7427b`](https://github.com/bluesky-social/atproto/commit/1a5d7427bf5811a019e7b50c7c2af711b8f2dd33)]:\n  - @atproto/repo@0.8.10\n  - @atproto/api@0.16.11\n  - @atproto/aws@0.2.30\n  - @atproto/lexicon-resolver@0.2.2\n  - @atproto/oauth-provider@0.13.1\n\n## 0.4.181\n\n### Patch Changes\n\n- [#4083](https://github.com/bluesky-social/atproto/pull/4083) [`0c20539c7`](https://github.com/bluesky-social/atproto/commit/0c20539c7185f6070d4337dbda3da92c39a3434f) Thanks [@dholms](https://github.com/dholms)! - Add env for max import size on repo.importRepo\n\n- Updated dependencies [[`8dc4caf55`](https://github.com/bluesky-social/atproto/commit/8dc4caf55840578c835b4c851d4a599c15627a78)]:\n  - @atproto/api@0.16.10\n\n## 0.4.180\n\n### Patch Changes\n\n- Updated dependencies [[`7351589a3`](https://github.com/bluesky-social/atproto/commit/7351589a317ff438c6010154e642a297adb76aa8), [`7351589a3`](https://github.com/bluesky-social/atproto/commit/7351589a317ff438c6010154e642a297adb76aa8), [`7351589a3`](https://github.com/bluesky-social/atproto/commit/7351589a317ff438c6010154e642a297adb76aa8)]:\n  - @atproto/oauth-provider@0.13.0\n\n## 0.4.179\n\n### Patch Changes\n\n- [#4191](https://github.com/bluesky-social/atproto/pull/4191) [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Always log warnings when DPOP proof `htu` contains # or ?.\n\n- [#4191](https://github.com/bluesky-social/atproto/pull/4191) [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add logging around scope dereferencing operations\n\n- Updated dependencies [[`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05), [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05), [`cf4117966`](https://github.com/bluesky-social/atproto/commit/cf4117966c1b1c1786a25bb352c12ad57b617a05)]:\n  - @atproto/oauth-provider@0.12.1\n\n## 0.4.178\n\n### Patch Changes\n\n- [#4189](https://github.com/bluesky-social/atproto/pull/4189) [`ff30786af`](https://github.com/bluesky-social/atproto/commit/ff30786af6f72ad6506939bfca01a3f55a096c1c) Thanks [@foysalit](https://github.com/foysalit)! - Add revoke credentials moderation event type to lexicons\n\n- Updated dependencies [[`ff30786af`](https://github.com/bluesky-social/atproto/commit/ff30786af6f72ad6506939bfca01a3f55a096c1c)]:\n  - @atproto/api@0.16.9\n\n## 0.4.177\n\n### Patch Changes\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove un-necessary concurrency when making sqlite DB updates\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add logging on (some) blob store operation failures\n\n- [#3881](https://github.com/bluesky-social/atproto/pull/3881) [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add expanded moderation report reasons as outlined in\n  [RFC-0009](https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity)\n\n- [#4162](https://github.com/bluesky-social/atproto/pull/4162) [`6d7bf4bff`](https://github.com/bluesky-social/atproto/commit/6d7bf4bffc3fee7a1fca488e6b75699385a04f37) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove old, never resolved, lexicons from the database\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use a dedicated logger (named `pds:blob-store`) for `BlobStore` operations\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fully read CAR stream before starting transaction in `importRepo`, resulting in shorter transactions\n\n- Updated dependencies [[`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`a5b20f021`](https://github.com/bluesky-social/atproto/commit/a5b20f0218bd13e3c5d7681de2263dcc850b7523), [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`8914f9abd`](https://github.com/bluesky-social/atproto/commit/8914f9abde2059c551d7e4c8d104227986098b82), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/oauth-scopes@0.2.0\n  - @atproto/aws@0.2.29\n  - @atproto/repo@0.8.9\n  - @atproto/oauth-provider@0.12.0\n  - @atproto/api@0.16.8\n  - @atproto-labs/simple-store-redis@0.0.1\n  - @atproto/lexicon-resolver@0.2.1\n  - @atproto/common@0.4.12\n  - @atproto/identity@0.4.9\n  - @atproto/lexicon@0.5.1\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc-server@0.9.5\n  - @atproto/xrpc@0.7.5\n  - @atproto-labs/xrpc-utils@0.0.22\n\n## 0.4.176\n\n### Patch Changes\n\n- Updated dependencies [[`09717f29a`](https://github.com/bluesky-social/atproto/commit/09717f29ac7ca742c9c3310980dbe4d112b7597f)]:\n  - @atproto/api@0.16.7\n\n## 0.4.175\n\n### Patch Changes\n\n- [#4155](https://github.com/bluesky-social/atproto/pull/4155) [`d54d278ab`](https://github.com/bluesky-social/atproto/commit/d54d278abd679fbb44ff795d02b53b7caab31301) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Log `cid` and `uri` as string on successful lexicon resolution\n\n- Updated dependencies [[`d54d278ab`](https://github.com/bluesky-social/atproto/commit/d54d278abd679fbb44ff795d02b53b7caab31301)]:\n  - @atproto/oauth-provider@0.11.2\n\n## 0.4.174\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-provider@0.11.1\n\n## 0.4.173\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/oauth-scopes@0.1.0\n  - @atproto/lexicon-resolver@0.2.0\n  - @atproto/oauth-provider@0.11.0\n  - @atproto-labs/fetch-node@0.1.10\n  - @atproto/xrpc@0.7.4\n  - @atproto/api@0.16.6\n  - @atproto/repo@0.8.8\n  - @atproto/xrpc-server@0.9.4\n  - @atproto-labs/xrpc-utils@0.0.21\n  - @atproto/aws@0.2.28\n\n## 0.4.172\n\n### Patch Changes\n\n- [#4142](https://github.com/bluesky-social/atproto/pull/4142) [`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - add com.atproto.temp.revokeAccountCredentials lexicon schema\n\n- Updated dependencies [[`66dbf8db6`](https://github.com/bluesky-social/atproto/commit/66dbf8db6dd9defeee140accd2e7b25d13feb8b6)]:\n  - @atproto/api@0.16.5\n\n## 0.4.171\n\n### Patch Changes\n\n- [#4141](https://github.com/bluesky-social/atproto/pull/4141) [`e1967c1c2`](https://github.com/bluesky-social/atproto/commit/e1967c1c2abb03bb84de424d01042c40a599b5b3) Thanks [@devinivy](https://github.com/devinivy)! - Avoid extra lookup when configured with appview details\n\n- [#4133](https://github.com/bluesky-social/atproto/pull/4133) [`c0126f4a8`](https://github.com/bluesky-social/atproto/commit/c0126f4a84940826a1d7511802cdc69260ed46df) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error handling when destroying pipethrough stream\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/api@0.16.4\n  - @atproto/repo@0.8.7\n  - @atproto/xrpc@0.7.3\n  - @atproto/xrpc-server@0.9.3\n  - @atproto/aws@0.2.27\n  - @atproto-labs/xrpc-utils@0.0.20\n\n## 0.4.170\n\n### Patch Changes\n\n- [#4109](https://github.com/bluesky-social/atproto/pull/4109) [`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e) Thanks [@foysalit](https://github.com/foysalit)! - Add batchId filter to tools.ozone.moderation.queryEvents endpoint\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12), [`3156ddf61`](https://github.com/bluesky-social/atproto/commit/3156ddf61519fede9ed148478f082184a1e3242e), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/repo@0.8.6\n  - @atproto/api@0.16.3\n  - @atproto/lexicon@0.4.13\n  - @atproto/aws@0.2.26\n  - @atproto/xrpc@0.7.2\n  - @atproto/xrpc-server@0.9.2\n  - @atproto-labs/xrpc-utils@0.0.19\n\n## 0.4.169\n\n### Patch Changes\n\n- [#4107](https://github.com/bluesky-social/atproto/pull/4107) [`369a20116`](https://github.com/bluesky-social/atproto/commit/369a2011615bd98a2a45c8600be45228d857a524) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow use of handle in `applyWrites`'s `repo`\n\n- [#4107](https://github.com/bluesky-social/atproto/pull/4107) [`369a20116`](https://github.com/bluesky-social/atproto/commit/369a2011615bd98a2a45c8600be45228d857a524) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Perf: Avoid fetching account data twice in `putRecord`\n\n- [#4104](https://github.com/bluesky-social/atproto/pull/4104) [`75162ffb9`](https://github.com/bluesky-social/atproto/commit/75162ffb9e35bf56b3f3cb19a12ebd495bdc0af8) Thanks [@DavidBuchanan314](https://github.com/DavidBuchanan314)! - Fix putRecord auth check\n\n## 0.4.168\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-provider@0.10.2\n\n## 0.4.167\n\n### Patch Changes\n\n- Updated dependencies [[`c274bd1b3`](https://github.com/bluesky-social/atproto/commit/c274bd1b38813abd5b287f1c94dca1fd62854918), [`832866c33`](https://github.com/bluesky-social/atproto/commit/832866c33b442443c52bc5891840f7527d81f926)]:\n  - @atproto/oauth-scopes@0.0.2\n  - @atproto/oauth-provider@0.10.1\n\n## 0.4.166\n\n### Patch Changes\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow configuring trusted OAuth clients\n\n- [#3806](https://github.com/bluesky-social/atproto/pull/3806) [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for OAuth ATProto scopes\n\n- Updated dependencies [[`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add), [`1dbc7750d`](https://github.com/bluesky-social/atproto/commit/1dbc7750d2bede009a776a44a170a19120b52fc8), [`43fbeda63`](https://github.com/bluesky-social/atproto/commit/43fbeda63e12134e8ebac73b4c2005b0918fc888), [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add), [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add), [`1899b1fc1`](https://github.com/bluesky-social/atproto/commit/1899b1fc16bc5cd7bb930ec697898766c3a05add)]:\n  - @atproto/oauth-provider@0.10.0\n  - @atproto/oauth-scopes@0.0.1\n\n## 0.4.165\n\n### Patch Changes\n\n- Updated dependencies [[`c370d933b`](https://github.com/bluesky-social/atproto/commit/c370d933b76b4e15b83a82b40d1b6a32bd54add6)]:\n  - @atproto/api@0.16.2\n\n## 0.4.164\n\n### Patch Changes\n\n- [#3927](https://github.com/bluesky-social/atproto/pull/3927) [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef) Thanks [@foysalit](https://github.com/foysalit)! - Introduces ozone event timeline lexicons\n\n- Updated dependencies [[`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`171efadb4`](https://github.com/bluesky-social/atproto/commit/171efadb49f842aa8ff3bf9d790caa6e0e0456ef), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671)]:\n  - @atproto/xrpc-server@0.9.1\n  - @atproto/api@0.16.1\n  - @atproto-labs/xrpc-utils@0.0.18\n\n## 0.4.163\n\n### Patch Changes\n\n- Updated dependencies [[`9751eebd7`](https://github.com/bluesky-social/atproto/commit/9751eebd718066984a91046b63e410caecd64022)]:\n  - @atproto/api@0.16.0\n\n## 0.4.162\n\n### Patch Changes\n\n- Updated dependencies [[`8787fd9de`](https://github.com/bluesky-social/atproto/commit/8787fd9dea769716412c9883e355cd496664bc6e), [`dc84906c8`](https://github.com/bluesky-social/atproto/commit/dc84906c865e8a97939a909dd3f75decde538363)]:\n  - @atproto/api@0.15.27\n\n## 0.4.161\n\n### Patch Changes\n\n- [#4041](https://github.com/bluesky-social/atproto/pull/4041) [`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add `unregisterPush` API\n\n- [#4048](https://github.com/bluesky-social/atproto/pull/4048) [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e) Thanks [@foysalit](https://github.com/foysalit)! - Add externalId to ozone events for deduping events per subject and event type\n\n- Updated dependencies [[`083566ddf`](https://github.com/bluesky-social/atproto/commit/083566ddfc3c9263423ebd5e59bfdbfe7b091c82), [`3b356c509`](https://github.com/bluesky-social/atproto/commit/3b356c5096a269f1be6c4e69bdee7f5d14eb5d7e)]:\n  - @atproto/api@0.15.26\n\n## 0.4.160\n\n### Patch Changes\n\n- Updated dependencies [[`88c136427`](https://github.com/bluesky-social/atproto/commit/88c136427451a20d21812a1aa88a70cf21904138)]:\n  - @atproto/api@0.15.25\n\n## 0.4.159\n\n### Patch Changes\n\n- [#4034](https://github.com/bluesky-social/atproto/pull/4034) [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1) Thanks [@foysalit](https://github.com/foysalit)! - Add age assurance event types to ozone lexicons\n\n- [#4025](https://github.com/bluesky-social/atproto/pull/4025) [`ad18fc171`](https://github.com/bluesky-social/atproto/commit/ad18fc171e5d6acfb29694352a101f577689e0ad) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly compute token `exp` in `getServiceAuth` endpoint\n\n- Updated dependencies [[`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`34d7a0846`](https://github.com/bluesky-social/atproto/commit/34d7a0846bb14bb36a8cc2747fb7ce73005e59d1), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/xrpc-server@0.9.0\n  - @atproto/api@0.15.24\n  - @atproto/lexicon@0.4.12\n  - @atproto-labs/xrpc-utils@0.0.17\n  - @atproto/repo@0.8.5\n  - @atproto/xrpc@0.7.1\n  - @atproto/aws@0.2.25\n\n## 0.4.158\n\n### Patch Changes\n\n- [#3991](https://github.com/bluesky-social/atproto/pull/3991) [`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8) Thanks [@foysalit](https://github.com/foysalit)! - Add modTool parameter to ozone events\n\n- Updated dependencies [[`0c0381a2b`](https://github.com/bluesky-social/atproto/commit/0c0381a2bb9b9dc14ca6c1c8c4a6b966f0d516e8)]:\n  - @atproto/api@0.15.23\n\n## 0.4.157\n\n### Patch Changes\n\n- [#3945](https://github.com/bluesky-social/atproto/pull/3945) [`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b) Thanks [@foysalit](https://github.com/foysalit)! - Add safelink module in ozone\n\n- Updated dependencies [[`02c358d0c`](https://github.com/bluesky-social/atproto/commit/02c358d0ca280922c20da5be1e23b4aa9e90a30b)]:\n  - @atproto/api@0.15.22\n\n## 0.4.156\n\n### Patch Changes\n\n- Updated dependencies [[`d344723a1`](https://github.com/bluesky-social/atproto/commit/d344723a1018b2436b5453526397936bd587a2e2)]:\n  - @atproto/api@0.15.21\n\n## 0.4.155\n\n### Patch Changes\n\n- [#4005](https://github.com/bluesky-social/atproto/pull/4005) [`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7) Thanks [@mozzius](https://github.com/mozzius)! - add `subscribed-post` notification reason\n\n- Updated dependencies [[`bb65f7a6e`](https://github.com/bluesky-social/atproto/commit/bb65f7a6e22ceedb57c74a18cf0539c1dd04c0a7)]:\n  - @atproto/api@0.15.20\n\n## 0.4.154\n\n### Patch Changes\n\n- Updated dependencies [[`376778a92`](https://github.com/bluesky-social/atproto/commit/376778a92f08fb6709c4cde736bfaca7393a72e1)]:\n  - @atproto/api@0.15.19\n\n## 0.4.153\n\n### Patch Changes\n\n- Updated dependencies [[`e3e31b2b9`](https://github.com/bluesky-social/atproto/commit/e3e31b2b9bf8c4de6b2d7fa992c3b3795686ea72)]:\n  - @atproto/api@0.15.18\n\n## 0.4.152\n\n### Patch Changes\n\n- [#3953](https://github.com/bluesky-social/atproto/pull/3953) [`f792b9193`](https://github.com/bluesky-social/atproto/commit/f792b919386341d0dc4dcf873506f088af61ae16) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Disable logging of expected errors\n\n- [#3990](https://github.com/bluesky-social/atproto/pull/3990) [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add activity subscription lexicons\n\n- Updated dependencies [[`a8dee6af3`](https://github.com/bluesky-social/atproto/commit/a8dee6af33618d3072ebae7f23843242a32c926c), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`6cd120206`](https://github.com/bluesky-social/atproto/commit/6cd12020657bfb5f87e97cd16e4abb379b64f60b), [`3a1e010e1`](https://github.com/bluesky-social/atproto/commit/3a1e010e148476bfdc0028c37cafbce85a46605a), [`f792b9193`](https://github.com/bluesky-social/atproto/commit/f792b919386341d0dc4dcf873506f088af61ae16)]:\n  - @atproto/repo@0.8.4\n  - @atproto/oauth-provider@0.9.3\n  - @atproto/api@0.15.17\n  - @atproto/aws@0.2.24\n\n## 0.4.151\n\n### Patch Changes\n\n- Updated dependencies [[`68c43a94b`](https://github.com/bluesky-social/atproto/commit/68c43a94bd76dc8040cdff9406cabaf1a484d999), [`5fccbd2a1`](https://github.com/bluesky-social/atproto/commit/5fccbd2a14420e4a7c6f56ad9af4ecfe15a971e3), [`68c43a94b`](https://github.com/bluesky-social/atproto/commit/68c43a94bd76dc8040cdff9406cabaf1a484d999)]:\n  - @atproto/oauth-provider@0.9.2\n  - @atproto/repo@0.8.3\n  - @atproto/aws@0.2.23\n\n## 0.4.150\n\n### Patch Changes\n\n- [#3966](https://github.com/bluesky-social/atproto/pull/3966) [`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef) Thanks [@mozzius](https://github.com/mozzius)! - Rename notification preference lexicon \"filter\" key to \"include\"\n\n- Updated dependencies [[`97ef11657`](https://github.com/bluesky-social/atproto/commit/97ef116571909c95713017bcd7b621c8afbc90ef)]:\n  - @atproto/api@0.15.16\n\n## 0.4.149\n\n### Patch Changes\n\n- Updated dependencies [[`7d9808ca8`](https://github.com/bluesky-social/atproto/commit/7d9808ca81dc13efbb9ced56ee2edd1e03966e10), [`8bd45e2f8`](https://github.com/bluesky-social/atproto/commit/8bd45e2f898a87b3550c7f4a0c8312fad9cb4736), [`e27d90845`](https://github.com/bluesky-social/atproto/commit/e27d90845496e46e2b0e8b362d43881900d7a9e3)]:\n  - @atproto/oauth-provider@0.9.1\n  - @atproto/repo@0.8.2\n  - @atproto/aws@0.2.22\n\n## 0.4.148\n\n### Patch Changes\n\n- Updated dependencies [[`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`7f1316748`](https://github.com/bluesky-social/atproto/commit/7f1316748dedb512ae739ad51b95644baa39fe80), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6), [`349b59175`](https://github.com/bluesky-social/atproto/commit/349b59175e82ceb9500ae7c6a9a0b9b6aec9d1b6)]:\n  - @atproto/oauth-provider@0.9.0\n  - @atproto/api@0.15.15\n\n## 0.4.147\n\n### Patch Changes\n\n- Updated dependencies [[`192f3ab89`](https://github.com/bluesky-social/atproto/commit/192f3ab89c943216683541f42cc1332e9c305eee), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c)]:\n  - @atproto/oauth-provider@0.8.1\n  - @atproto/xrpc-server@0.8.0\n  - @atproto-labs/xrpc-utils@0.0.16\n\n## 0.4.146\n\n### Patch Changes\n\n- [#3901](https://github.com/bluesky-social/atproto/pull/3901) [`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1) Thanks [@mozzius](https://github.com/mozzius)! - Add notification preferences V2 lexicons\n\n- Updated dependencies [[`a48671e73`](https://github.com/bluesky-social/atproto/commit/a48671e730681f692a88053e8f137bd9e2aed5f1)]:\n  - @atproto/api@0.15.14\n\n## 0.4.145\n\n### Patch Changes\n\n- [#3929](https://github.com/bluesky-social/atproto/pull/3929) [`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Rename `getPostThreadHiddenV2` to `getPostThreadOtherV2` to better reflect the intent of the API.\n\n- [#3930](https://github.com/bluesky-social/atproto/pull/3930) [`598fcb693`](https://github.com/bluesky-social/atproto/commit/598fcb693d154fe4222f84a3ad24ed3d0b19c58d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Log invalid use of \"htu\" DPoP proof claim\n\n- Updated dependencies [[`c6eb8a12e`](https://github.com/bluesky-social/atproto/commit/c6eb8a12e291c88fea79da447f9da8608d02300d)]:\n  - @atproto/api@0.15.13\n\n## 0.4.144\n\n### Patch Changes\n\n- [#3879](https://github.com/bluesky-social/atproto/pull/3879) [`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Log clients using invalid \"htu\" claim in DPoP proof\n\n- Updated dependencies [[`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05), [`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f), [`3fa2ee3b6`](https://github.com/bluesky-social/atproto/commit/3fa2ee3b6a382709b10921da53e69a901bccbb05)]:\n  - @atproto/oauth-provider@0.8.0\n  - @atproto/xrpc-server@0.7.19\n  - @atproto-labs/xrpc-utils@0.0.15\n\n## 0.4.143\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-provider@0.7.10\n\n## 0.4.142\n\n### Patch Changes\n\n- [#3912](https://github.com/bluesky-social/atproto/pull/3912) [`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Unify `getPostThreadV2` and `getPostThreadHiddenV2` responses under `app.bsky.unspecced.defs` namespace and a single interface via `threadItemPost`.\n\n- Updated dependencies [[`a5cd018bd`](https://github.com/bluesky-social/atproto/commit/a5cd018bd5f237221902ab1b6956b46233c92187)]:\n  - @atproto/api@0.15.12\n\n## 0.4.141\n\n### Patch Changes\n\n- [#3900](https://github.com/bluesky-social/atproto/pull/3900) [`06bf684a4`](https://github.com/bluesky-social/atproto/commit/06bf684a4a3fd2b8c73d2729e4951cedca8cba5e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add max length limit to passwords\n\n- Updated dependencies [[`a978681fd`](https://github.com/bluesky-social/atproto/commit/a978681fde1c138a5298bae77e5dc36ce155f955), [`06bf684a4`](https://github.com/bluesky-social/atproto/commit/06bf684a4a3fd2b8c73d2729e4951cedca8cba5e)]:\n  - @atproto/api@0.15.11\n  - @atproto/oauth-provider@0.7.9\n\n## 0.4.140\n\n### Patch Changes\n\n- Updated dependencies [[`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9), [`1dae6c59a`](https://github.com/bluesky-social/atproto/commit/1dae6c59abe0e5aa4a7b7d0cc1dfee88f458d4b9)]:\n  - @atproto/api@0.15.10\n\n## 0.4.139\n\n### Patch Changes\n\n- [#3882](https://github.com/bluesky-social/atproto/pull/3882) [`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159) Thanks [@mozzius](https://github.com/mozzius)! - add a \"via\" field to reposts and likes allowing a reference a repost, and then give a notification when a repost is liked or reposted.\n\n- Updated dependencies [[`d1e3e68dd`](https://github.com/bluesky-social/atproto/commit/d1e3e68dd9eb7bed13d9023bc0e4ce3c448eabf5), [`79a75bb1e`](https://github.com/bluesky-social/atproto/commit/79a75bb1ed8fc14cefa246621fe1faeebf3fc159)]:\n  - @atproto/oauth-provider@0.7.8\n  - @atproto/api@0.15.9\n\n## 0.4.138\n\n### Patch Changes\n\n- [#3820](https://github.com/bluesky-social/atproto/pull/3820) [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow access to `com.atproto.server.getSession` when authenticated using oauth credentials. Email info (`email`, `emailConfirmed`) will only be exposed if the credentials were issued with the `transition:email` scope.\n\n- [#3820](https://github.com/bluesky-social/atproto/pull/3820) [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for `transition:email` oauth scope\n\n- [#3868](https://github.com/bluesky-social/atproto/pull/3868) [`eab7c9fb8`](https://github.com/bluesky-social/atproto/commit/eab7c9fb8a9fed4017455ea06666c919aea61336) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use the PDS's hostname as fallback PDS \"name\" in the auth screen\n\n- Updated dependencies [[`80f402f36`](https://github.com/bluesky-social/atproto/commit/80f402f3663af08fd048300738d04c67aa2b9cb8), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`36dbd4155`](https://github.com/bluesky-social/atproto/commit/36dbd41551f74052a3f584719a1a7edd86eca201), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4), [`8318c5718`](https://github.com/bluesky-social/atproto/commit/8318c57187a1fed443be73bfd7639f49febc7337)]:\n  - @atproto/api@0.15.8\n  - @atproto-labs/fetch-node@0.1.9\n  - @atproto/oauth-provider@0.7.7\n\n## 0.4.137\n\n### Patch Changes\n\n- [#3850](https://github.com/bluesky-social/atproto/pull/3850) [`efc64ba92`](https://github.com/bluesky-social/atproto/commit/efc64ba92511933c2100b45c0e7f4bcae9199240) Thanks [@devinivy](https://github.com/devinivy)! - calculate client ip relative to trusted ips, e.g. for rate limiting purposes\n\n- Updated dependencies [[`86b315388`](https://github.com/bluesky-social/atproto/commit/86b3153884099ceeb0cfdb9d2bfdd447c39fb35a)]:\n  - @atproto/api@0.15.7\n\n## 0.4.136\n\n### Patch Changes\n\n- [#3840](https://github.com/bluesky-social/atproto/pull/3840) [`088d06204`](https://github.com/bluesky-social/atproto/commit/088d06204f779412b94ae3363ff548a6c8d1299a) Thanks [@devinivy](https://github.com/devinivy)! - fix account management migration 005.\n\n## 0.4.135\n\n### Patch Changes\n\n- [#3816](https://github.com/bluesky-social/atproto/pull/3816) [`ab4e72084`](https://github.com/bluesky-social/atproto/commit/ab4e72084dd0ea1eb12b45cbb913595434b88675) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Improve handle resolution mechanism\n\n- [#3814](https://github.com/bluesky-social/atproto/pull/3814) [`eccbce278`](https://github.com/bluesky-social/atproto/commit/eccbce278da72c3fbbf8fbbcfcafe76ae28dcd6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Small memory leak fix.\n\n- [#3700](https://github.com/bluesky-social/atproto/pull/3700) [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Consistenlty log errors\n\n- Updated dependencies [[`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba), [`3a65b68f7`](https://github.com/bluesky-social/atproto/commit/3a65b68f7dc63c8bfbea0ae615f8ae984272f2e4)]:\n  - @atproto/xrpc@0.7.0\n  - @atproto/lexicon@0.4.11\n  - @atproto/xrpc-server@0.7.18\n  - @atproto/api@0.15.6\n  - @atproto/oauth-provider@0.7.6\n  - @atproto/common@0.4.11\n  - @atproto/identity@0.4.8\n  - @atproto/repo@0.8.1\n  - @atproto-labs/xrpc-utils@0.0.14\n  - @atproto/aws@0.2.21\n  - @atproto/crypto@0.4.4\n\n## 0.4.134\n\n### Patch Changes\n\n- [#3765](https://github.com/bluesky-social/atproto/pull/3765) [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9) Thanks [@foysalit](https://github.com/foysalit)! - Add verification lexicons to ozone\n\n- Updated dependencies [[`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9), [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9)]:\n  - @atproto/api@0.15.5\n  - @atproto/xrpc-server@0.7.17\n  - @atproto-labs/xrpc-utils@0.0.13\n\n## 0.4.133\n\n### Patch Changes\n\n- Updated dependencies [[`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c), [`7af77f3ed`](https://github.com/bluesky-social/atproto/commit/7af77f3edfe52f77729f61de4188e8375f03b4ef)]:\n  - @atproto/xrpc-server@0.7.16\n  - @atproto/api@0.15.4\n  - @atproto-labs/xrpc-utils@0.0.12\n\n## 0.4.132\n\n### Patch Changes\n\n- Updated dependencies [[`d794b0676`](https://github.com/bluesky-social/atproto/commit/d794b06763050b4b32484e90116461deae45cbe3)]:\n  - @atproto/oauth-provider@0.7.5\n\n## 0.4.131\n\n### Patch Changes\n\n- Updated dependencies [[`0087dc1c0`](https://github.com/bluesky-social/atproto/commit/0087dc1c0bafad1d0a0a1a16683d250dea031bf9)]:\n  - @atproto/api@0.15.3\n\n## 0.4.130\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/oauth-provider@0.7.4\n\n## 0.4.129\n\n### Patch Changes\n\n- [#3764](https://github.com/bluesky-social/atproto/pull/3764) [`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow customizing contrast and hue colors\n\n- [#3775](https://github.com/bluesky-social/atproto/pull/3775) [`6db9faa4a`](https://github.com/bluesky-social/atproto/commit/6db9faa4a121b573fa73e29e41dd1c183543f7f8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove unused `image-url.ts` file\n\n- Updated dependencies [[`30f9b6690`](https://github.com/bluesky-social/atproto/commit/30f9b6690e0e2c5810772e94e631322b9d89c65a)]:\n  - @atproto/oauth-provider@0.7.3\n\n## 0.4.128\n\n### Patch Changes\n\n- Updated dependencies [[`553c988f1`](https://github.com/bluesky-social/atproto/commit/553c988f1d226b3d2fbe94c117b088f5c82db794)]:\n  - @atproto/api@0.15.2\n\n## 0.4.127\n\n### Patch Changes\n\n- Updated dependencies [[`96de2acb3`](https://github.com/bluesky-social/atproto/commit/96de2acb301683effe4313cb93d7747f87a73b5e), [`688268b6a`](https://github.com/bluesky-social/atproto/commit/688268b6a5ee30f0922ee152ffbd26583d164ae4), [`8d99915ce`](https://github.com/bluesky-social/atproto/commit/8d99915ce02c73b9b37bf121ccd2703fa14a906a)]:\n  - @atproto/oauth-provider@0.7.2\n  - @atproto/api@0.15.1\n\n## 0.4.126\n\n### Patch Changes\n\n- Updated dependencies [[`23462184d`](https://github.com/bluesky-social/atproto/commit/23462184dc941ba2fc3b4d054985a53715585020)]:\n  - @atproto/api@0.15.0\n\n## 0.4.125\n\n### Patch Changes\n\n- [#3754](https://github.com/bluesky-social/atproto/pull/3754) [`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove reference to missing \"bin\" executable\n\n- Updated dependencies [[`1e461eab0`](https://github.com/bluesky-social/atproto/commit/1e461eab033f728f537db554b3072b7eda7e5e8f)]:\n  - @atproto/oauth-provider@0.7.1\n\n## 0.4.124\n\n### Patch Changes\n\n- [#3659](https://github.com/bluesky-social/atproto/pull/3659) [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add account management page for oauth sessions\n\n- Updated dependencies [[`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e), [`371e04aad`](https://github.com/bluesky-social/atproto/commit/371e04aad2a3e8ae3fe185ce15fc8eb051cab78e)]:\n  - @atproto/oauth-provider@0.7.0\n\n## 0.4.123\n\n### Patch Changes\n\n- Updated dependencies [[`fc61662d7`](https://github.com/bluesky-social/atproto/commit/fc61662d7b88597f78383e37ee54264a8bb4b670), [`ca07871c4`](https://github.com/bluesky-social/atproto/commit/ca07871c487abc99fe7b7f8671aa8d98eb5dc4bb)]:\n  - @atproto/api@0.14.22\n\n## 0.4.122\n\n### Patch Changes\n\n- Updated dependencies [[`8b7bf7e8f`](https://github.com/bluesky-social/atproto/commit/8b7bf7e8f0e5447c68633a87a2a3cff99f9e7e1c)]:\n  - @atproto/api@0.14.21\n\n## 0.4.121\n\n### Patch Changes\n\n- Updated dependencies [[`0e681d303`](https://github.com/bluesky-social/atproto/commit/0e681d3036fd0b35c6d2198638392051b2ce4c81)]:\n  - @atproto/api@0.14.20\n\n## 0.4.120\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144), [`efb302db1`](https://github.com/bluesky-social/atproto/commit/efb302db1a615b68795c725a22489dbd0400e011), [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/repo@0.8.0\n  - @atproto/api@0.14.19\n  - @atproto/common@0.4.10\n  - @atproto/identity@0.4.7\n  - @atproto/lexicon@0.4.10\n  - @atproto/aws@0.2.20\n  - @atproto/crypto@0.4.4\n  - @atproto/oauth-provider@0.6.6\n  - @atproto/xrpc-server@0.7.15\n  - @atproto/xrpc@0.6.12\n  - @atproto-labs/xrpc-utils@0.0.11\n\n## 0.4.119\n\n### Patch Changes\n\n- Updated dependencies [[`04b6230cd`](https://github.com/bluesky-social/atproto/commit/04b6230cd2fbfe4a06cb00ab8ccb8e6c87c6c546)]:\n  - @atproto/api@0.14.18\n\n## 0.4.118\n\n### Patch Changes\n\n- [#2519](https://github.com/bluesky-social/atproto/pull/2519) [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f) Thanks [@dholms](https://github.com/dholms)! - Add recovery scripts\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`2b7efb6cb`](https://github.com/bluesky-social/atproto/commit/2b7efb6cb1c93a108570efdafe9d9ec3f1018dfa), [`b0a0f1484`](https://github.com/bluesky-social/atproto/commit/b0a0f1484378adeb5e2aa20b9b6ff2c2eca0f740), [`0eea698be`](https://github.com/bluesky-social/atproto/commit/0eea698bef76520ae4cc0e1f2efbb588a0459556), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/api@0.14.17\n  - @atproto/repo@0.7.3\n  - @atproto/common@0.4.9\n  - @atproto/aws@0.2.19\n  - @atproto/crypto@0.4.4\n  - @atproto/oauth-provider@0.6.5\n  - @atproto/xrpc-server@0.7.14\n  - @atproto-labs/xrpc-utils@0.0.10\n\n## 0.4.117\n\n### Patch Changes\n\n- [#3695](https://github.com/bluesky-social/atproto/pull/3695) [`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Fix last reaction lexicon\n\n- Updated dependencies [[`652894308`](https://github.com/bluesky-social/atproto/commit/65289430806976ec13177ed9c9f0e883e8f9330c)]:\n  - @atproto/api@0.14.16\n\n## 0.4.116\n\n### Patch Changes\n\n- Updated dependencies [[`9b28184cb`](https://github.com/bluesky-social/atproto/commit/9b28184cb9c417173f46cfb5824dc197dec3e069), [`b4ab5011b`](https://github.com/bluesky-social/atproto/commit/b4ab5011bcc64f9f05122a8773806af8e0c13146), [`9b28184cb`](https://github.com/bluesky-social/atproto/commit/9b28184cb9c417173f46cfb5824dc197dec3e069)]:\n  - @atproto/oauth-provider@0.6.4\n  - @atproto/api@0.14.15\n\n## 0.4.115\n\n### Patch Changes\n\n- Updated dependencies [[`98d8a677c`](https://github.com/bluesky-social/atproto/commit/98d8a677ca4671137727d14567c8354c48c9e850), [`98d8a677c`](https://github.com/bluesky-social/atproto/commit/98d8a677ca4671137727d14567c8354c48c9e850)]:\n  - @atproto/oauth-provider@0.6.3\n\n## 0.4.114\n\n### Patch Changes\n\n- [#3685](https://github.com/bluesky-social/atproto/pull/3685) [`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Update chat reaction lexicon\n\n- Updated dependencies [[`9a05892f6`](https://github.com/bluesky-social/atproto/commit/9a05892f6fd405bf6bb96c9c8d2a9a89d5e94bc5)]:\n  - @atproto/api@0.14.14\n\n## 0.4.113\n\n### Patch Changes\n\n- Updated dependencies [[`a5a760c1f`](https://github.com/bluesky-social/atproto/commit/a5a760c1f0efd7246c9eebbc0f482d2f505de0a1)]:\n  - @atproto/oauth-provider@0.6.2\n\n## 0.4.112\n\n### Patch Changes\n\n- [#3651](https://github.com/bluesky-social/atproto/pull/3651) [`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd) Thanks [@foysalit](https://github.com/foysalit)! - Add getSubjects endpoint to ozone for fetching detailed view of multiple subjects\n\n- Updated dependencies [[`076c2f987`](https://github.com/bluesky-social/atproto/commit/076c2f9872387217806624306e3af08878d1adcd)]:\n  - @atproto/api@0.14.13\n\n## 0.4.111\n\n### Patch Changes\n\n- [#3674](https://github.com/bluesky-social/atproto/pull/3674) [`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0) Thanks [@mozzius](https://github.com/mozzius)! - run codegen for changes in chat lexicon\n\n- Updated dependencies [[`44f5c3639`](https://github.com/bluesky-social/atproto/commit/44f5c3639fcaf73865d21ec4b0c64baa641006c0)]:\n  - @atproto/api@0.14.12\n\n## 0.4.110\n\n### Patch Changes\n\n- Updated dependencies [[`d87ffc7bf`](https://github.com/bluesky-social/atproto/commit/d87ffc7bfe3c1e792dc84a320544eb2e053d61ce)]:\n  - @atproto/api@0.14.11\n\n## 0.4.109\n\n### Patch Changes\n\n- Updated dependencies [[`42807cad5`](https://github.com/bluesky-social/atproto/commit/42807cad56786e402d601ef9ed97379d5641a2c6)]:\n  - @atproto/oauth-provider@0.6.1\n\n## 0.4.108\n\n### Patch Changes\n\n- [`03fc0aa27`](https://github.com/bluesky-social/atproto/commit/03fc0aa270884523719e67bea701ef19e2dd5696) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve password reset error reporting during OAuth\n\n- Updated dependencies [[`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10), [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764), [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10), [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764), [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764), [`cc4122652`](https://github.com/bluesky-social/atproto/commit/cc4122652ed42ba55826c019d0ec57bf25df1ecd), [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764), [`9332c0f31`](https://github.com/bluesky-social/atproto/commit/9332c0f315bb7270bf346f69ecb178481ed07764), [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10), [`49528e83d`](https://github.com/bluesky-social/atproto/commit/49528e83daee8d91c1956b13cc73e9c2b79b6b10), [`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/oauth-provider@0.6.0\n  - @atproto/syntax@0.4.0\n  - @atproto/api@0.14.10\n  - @atproto/lexicon@0.4.9\n  - @atproto/repo@0.7.2\n  - @atproto/xrpc@0.6.11\n  - @atproto/xrpc-server@0.7.13\n  - @atproto/aws@0.2.18\n  - @atproto-labs/xrpc-utils@0.0.9\n\n## 0.4.107\n\n### Patch Changes\n\n- [#3624](https://github.com/bluesky-social/atproto/pull/3624) [`0ae7f416e`](https://github.com/bluesky-social/atproto/commit/0ae7f416e8055fe4d05b283449e44457161f6a93) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Emit \"sync\" event on account creation through OAuth\n\n- Updated dependencies [[`9e3eace8f`](https://github.com/bluesky-social/atproto/commit/9e3eace8f9c22141e6da80b7696cd3b3e7c38779), [`5ada66ceb`](https://github.com/bluesky-social/atproto/commit/5ada66ceb9d5b2c64f112bb62da0edc421c765bf)]:\n  - @atproto/oauth-provider@0.5.2\n  - @atproto/api@0.14.9\n\n## 0.4.106\n\n### Patch Changes\n\n- [#3587](https://github.com/bluesky-social/atproto/pull/3587) [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952) Thanks [@foysalit](https://github.com/foysalit)! - Add searchable handle and displayName to ozone team members\n\n- [#3612](https://github.com/bluesky-social/atproto/pull/3612) [`eab9c003f`](https://github.com/bluesky-social/atproto/commit/eab9c003f838d43f0135ded9d3ede3f449997597) Thanks [@devinivy](https://github.com/devinivy)! - Emit sync event on account creation\n\n- Updated dependencies [[`c01d7f5d1`](https://github.com/bluesky-social/atproto/commit/c01d7f5d155445d7741c09f91c84af64b31bdbed), [`8827ff433`](https://github.com/bluesky-social/atproto/commit/8827ff433a211d2db80840cfc4ee146a7fb44849), [`18fbfa000`](https://github.com/bluesky-social/atproto/commit/18fbfa00057dda9ef4eba77d8b4e87994893c952)]:\n  - @atproto/oauth-provider@0.5.1\n  - @atproto/api@0.14.9\n\n## 0.4.105\n\n### Patch Changes\n\n- [#3585](https://github.com/bluesky-social/atproto/pull/3585) [`38320191e`](https://github.com/bluesky-social/atproto/commit/38320191e559f8b928c6e951a9b4a6207240bfc1) Thanks [@dholms](https://github.com/dholms)! - Wrap sync v1.1 semantics. Add #sync event to subscribeRepos and deprecate #handle and #tombstone events\n\n- [#3602](https://github.com/bluesky-social/atproto/pull/3602) [`6bcbb6d8c`](https://github.com/bluesky-social/atproto/commit/6bcbb6d8cd3696280935ff7892d8e191fd21fa49) Thanks [@devinivy](https://github.com/devinivy)! - Permit 100mb video embeds.\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for account sign-ups during OAuth flows\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`38320191e`](https://github.com/bluesky-social/atproto/commit/38320191e559f8b928c6e951a9b4a6207240bfc1), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`6bcbb6d8c`](https://github.com/bluesky-social/atproto/commit/6bcbb6d8cd3696280935ff7892d8e191fd21fa49), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`dc6e4ecb0`](https://github.com/bluesky-social/atproto/commit/dc6e4ecb0e09bbf4bc7a79c6ac43fb6da4166200)]:\n  - @atproto/oauth-provider@0.5.0\n  - @atproto/api@0.14.8\n  - @atproto/syntax@0.3.4\n  - @atproto-labs/fetch-node@0.1.8\n  - @atproto/lexicon@0.4.8\n  - @atproto/repo@0.7.1\n  - @atproto/xrpc@0.6.10\n  - @atproto/xrpc-server@0.7.12\n  - @atproto/aws@0.2.17\n  - @atproto-labs/xrpc-utils@0.0.8\n\n## 0.4.104\n\n### Patch Changes\n\n- [#3580](https://github.com/bluesky-social/atproto/pull/3580) [`d4e14b7bd`](https://github.com/bluesky-social/atproto/commit/d4e14b7bdc7752476757ecfe96343d146411b784) Thanks [@dholms](https://github.com/dholms)! - Fix bug where racing writes to the same repository can get sequenced out-of-order.\n\n- [#2506](https://github.com/bluesky-social/atproto/pull/2506) [`27b0a7be1`](https://github.com/bluesky-social/atproto/commit/27b0a7be1ed1b6e098114791d84ec9dc844db552) Thanks [@bnewbold](https://github.com/bnewbold)! - remove some deprecated fields from com.atproto Lexicons\n\n- [#3553](https://github.com/bluesky-social/atproto/pull/3553) [`5cce76670`](https://github.com/bluesky-social/atproto/commit/5cce7667058981561340107e0124093203e796e3) Thanks [@devinivy](https://github.com/devinivy)! - Add x-forwarded-for to PDS-proxied requests to entryway\n\n- Updated dependencies [[`99e2809ca`](https://github.com/bluesky-social/atproto/commit/99e2809ca2ebf70acaa10254f140a8dd0fad4305), [`27b0a7be1`](https://github.com/bluesky-social/atproto/commit/27b0a7be1ed1b6e098114791d84ec9dc844db552), [`11d8d21be`](https://github.com/bluesky-social/atproto/commit/11d8d21beac4b79ac44b930197761f9d08dbb492)]:\n  - @atproto/api@0.14.7\n\n## 0.4.103\n\n### Patch Changes\n\n- [#3557](https://github.com/bluesky-social/atproto/pull/3557) [`82d5a2d36`](https://github.com/bluesky-social/atproto/commit/82d5a2d3617c40caab7a18e46c709c4b3c48e7f8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Only trust one level of proxying when computing IP during OAuthFlows\n\n- Updated dependencies [[`82d5a2d36`](https://github.com/bluesky-social/atproto/commit/82d5a2d3617c40caab7a18e46c709c4b3c48e7f8), [`82d5a2d36`](https://github.com/bluesky-social/atproto/commit/82d5a2d3617c40caab7a18e46c709c4b3c48e7f8)]:\n  - @atproto/oauth-provider@0.4.0\n\n## 0.4.102\n\n### Patch Changes\n\n- Updated dependencies [[`44f81f2eb`](https://github.com/bluesky-social/atproto/commit/44f81f2eb9229e21aec4472b3a05e855396dbec5)]:\n  - @atproto/api@0.14.6\n\n## 0.4.101\n\n### Patch Changes\n\n- [#3449](https://github.com/bluesky-social/atproto/pull/3449) [`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f) Thanks [@dholms](https://github.com/dholms)! - Updated subscribeRepo to include prev CIDs for operations and covering proofs for all ops.\n\n- [#3572](https://github.com/bluesky-social/atproto/pull/3572) [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a) Thanks [@foysalit](https://github.com/foysalit)! - Make comment property optional on ozone comment event\n\n- Updated dependencies [[`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f), [`9b643fbec`](https://github.com/bluesky-social/atproto/commit/9b643fbecac30de5cfdb80d0671bfa55e9f4512a), [`6e382f67a`](https://github.com/bluesky-social/atproto/commit/6e382f67aa73532efadfea80ff96a27b526cb178)]:\n  - @atproto/repo@0.7.0\n  - @atproto/api@0.14.5\n  - @atproto/aws@0.2.16\n\n## 0.4.100\n\n### Patch Changes\n\n- [#3561](https://github.com/bluesky-social/atproto/pull/3561) [`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c) Thanks [@foysalit](https://github.com/foysalit)! - Add Divert event to the list of allowed ozone events\n\n- Updated dependencies [[`b9cb049d9`](https://github.com/bluesky-social/atproto/commit/b9cb049d940cc706681142ef498238f74e2f539c)]:\n  - @atproto/api@0.14.4\n\n## 0.4.99\n\n### Patch Changes\n\n- Updated dependencies [[`22af31a89`](https://github.com/bluesky-social/atproto/commit/22af31a898476c5e317aea263af366bddda120d6), [`01874c4be`](https://github.com/bluesky-social/atproto/commit/01874c4be73a41ffb8fe28378f674949aa2c938f)]:\n  - @atproto/api@0.14.3\n\n## 0.4.98\n\n### Patch Changes\n\n- [#3546](https://github.com/bluesky-social/atproto/pull/3546) [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9) Thanks [@foysalit](https://github.com/foysalit)! - Add reporter stats endpoint on ozone service\n\n- Updated dependencies [[`010f10c6f`](https://github.com/bluesky-social/atproto/commit/010f10c6f212f699ad42c0349a58bbcf2172e3cc), [`a9887f687`](https://github.com/bluesky-social/atproto/commit/a9887f68778c49932d92cfea98aadcfa4d5b62e9)]:\n  - @atproto/api@0.14.2\n\n## 0.4.97\n\n### Patch Changes\n\n- Updated dependencies [[`bde6f71c4`](https://github.com/bluesky-social/atproto/commit/bde6f71c4cd33022d29da0ff23463a5838c4de24)]:\n  - @atproto/oauth-provider@0.3.1\n\n## 0.4.96\n\n### Patch Changes\n\n- [#3525](https://github.com/bluesky-social/atproto/pull/3525) [`6ea9c961a`](https://github.com/bluesky-social/atproto/commit/6ea9c961af964cd9b0d00b5073c695c5e0b3345a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Dead code cleanup.\n\n- Updated dependencies [[`6ea9c961a`](https://github.com/bluesky-social/atproto/commit/6ea9c961af964cd9b0d00b5073c695c5e0b3345a), [`6ea9c961a`](https://github.com/bluesky-social/atproto/commit/6ea9c961af964cd9b0d00b5073c695c5e0b3345a), [`ba5bb6e66`](https://github.com/bluesky-social/atproto/commit/ba5bb6e667fb58bbefd332844957de575e102ca3), [`e69e89a03`](https://github.com/bluesky-social/atproto/commit/e69e89a037829bd4f6656d6aa42b77b97b4934e5)]:\n  - @atproto/oauth-provider@0.3.0\n  - @atproto/api@0.14.1\n\n## 0.4.95\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update Lexicon derived code to better reflect data typings. In particular, Lexicon derived interfaces will now explicitly include the `$type` property that can be present in the data.\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor typing fixes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/api@0.14.0\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n  - @atproto/repo@0.6.5\n  - @atproto/xrpc@0.6.9\n  - @atproto/xrpc-server@0.7.11\n  - @atproto/aws@0.2.15\n  - @atproto-labs/xrpc-utils@0.0.7\n\n## 0.4.94\n\n### Patch Changes\n\n- [#3495](https://github.com/bluesky-social/atproto/pull/3495) [`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153) Thanks [@foysalit](https://github.com/foysalit)! - Add a priority score to ozone subjects\n\n- Updated dependencies [[`709a85b0b`](https://github.com/bluesky-social/atproto/commit/709a85b0b633b5483b7161db64b429c746239153)]:\n  - @atproto/api@0.13.35\n\n## 0.4.93\n\n### Patch Changes\n\n- Updated dependencies [[`dc8a7842e`](https://github.com/bluesky-social/atproto/commit/dc8a7842e67f5f3709e88310d2a60d384453b486), [`636951e47`](https://github.com/bluesky-social/atproto/commit/636951e4728cd52c2e5355eb93b47d7e869b67e9)]:\n  - @atproto/api@0.13.34\n\n## 0.4.92\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- [#3492](https://github.com/bluesky-social/atproto/pull/3492) [`53a577fd4`](https://github.com/bluesky-social/atproto/commit/53a577fd4bfb1c7301e14db85b42f4758b053dee) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Code refactor\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`87ed907a6`](https://github.com/bluesky-social/atproto/commit/87ed907a6b96b408c02c9af819cec8380a453254)]:\n  - @atproto/oauth-provider@0.2.17\n  - @atproto-labs/fetch-node@0.1.7\n  - @atproto/xrpc-server@0.7.10\n  - @atproto/identity@0.4.6\n  - @atproto/lexicon@0.4.6\n  - @atproto/common@0.4.8\n  - @atproto/crypto@0.4.4\n  - @atproto/syntax@0.3.2\n  - @atproto/repo@0.6.4\n  - @atproto/xrpc@0.6.8\n  - @atproto/api@0.13.33\n  - @atproto/aws@0.2.14\n  - @atproto-labs/xrpc-utils@0.0.6\n\n## 0.4.91\n\n### Patch Changes\n\n- [#3352](https://github.com/bluesky-social/atproto/pull/3352) [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905) Thanks [@foysalit](https://github.com/foysalit)! - Auto resolve appeals when taking down\n\n- [#3463](https://github.com/bluesky-social/atproto/pull/3463) [`8810885b8`](https://github.com/bluesky-social/atproto/commit/8810885b8e7fa0377e6c000c091eec1dd85ed261) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix flaky tests\n\n- [#3439](https://github.com/bluesky-social/atproto/pull/3439) [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve error reporting in case of failed PLC update operation\n\n- Updated dependencies [[`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`7f52e6735`](https://github.com/bluesky-social/atproto/commit/7f52e67354906c3bf9830d7a2924ab58d6160905), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9), [`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/xrpc-server@0.7.9\n  - @atproto/api@0.13.32\n  - @atproto/xrpc@0.6.7\n  - @atproto/common@0.4.7\n  - @atproto-labs/xrpc-utils@0.0.5\n  - @atproto/aws@0.2.13\n  - @atproto/crypto@0.4.3\n  - @atproto/oauth-provider@0.2.16\n  - @atproto/repo@0.6.3\n\n## 0.4.90\n\n### Patch Changes\n\n- Updated dependencies [[`b04943191`](https://github.com/bluesky-social/atproto/commit/b04943191b9f89a5263a77358d47d1362a6454a6), [`8c6c7813a`](https://github.com/bluesky-social/atproto/commit/8c6c7813a9c2110c8fe21acdca8f09554a1983ce)]:\n  - @atproto/oauth-provider@0.2.15\n  - @atproto/api@0.13.31\n\n## 0.4.89\n\n### Patch Changes\n\n- Updated dependencies [[`e6e6aea38`](https://github.com/bluesky-social/atproto/commit/e6e6aea3814e3d0bb42a537f80d77947e85fa73f), [`c0a75d310`](https://github.com/bluesky-social/atproto/commit/c0a75d310aa92c067799a97d1acc5bd0543114c5), [`c5a4cdb0a`](https://github.com/bluesky-social/atproto/commit/c5a4cdb0a52f4583ffe783a0b259e80263f24a8c)]:\n  - @atproto/api@0.13.30\n  - @atproto/oauth-provider@0.2.14\n\n## 0.4.88\n\n### Patch Changes\n\n- Updated dependencies [[`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12)]:\n  - @atproto/xrpc-server@0.7.8\n  - @atproto-labs/xrpc-utils@0.0.4\n\n## 0.4.87\n\n### Patch Changes\n\n- [#3416](https://github.com/bluesky-social/atproto/pull/3416) [`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `tools.ozone.moderation.queryStatuses` lexicon\n\n- Updated dependencies [[`50603b4f2`](https://github.com/bluesky-social/atproto/commit/50603b4f2ef08bd618730107ec164a57f27dcca6)]:\n  - @atproto/api@0.13.29\n\n## 0.4.86\n\n### Patch Changes\n\n- Updated dependencies [[`cbf17066f`](https://github.com/bluesky-social/atproto/commit/cbf17066f314fbc7f2e943127ee4a9f589f8bec2), [`9c0128193`](https://github.com/bluesky-social/atproto/commit/9c01281931a371304bcfa465005d7363c003bc5f)]:\n  - @atproto/api@0.13.28\n  - @atproto-labs/fetch-node@0.1.6\n  - @atproto/oauth-provider@0.2.13\n\n## 0.4.85\n\n### Patch Changes\n\n- Updated dependencies [[`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61)]:\n  - @atproto/xrpc-server@0.7.7\n  - @atproto-labs/xrpc-utils@0.0.3\n\n## 0.4.84\n\n### Patch Changes\n\n- Updated dependencies [[`e277158f7`](https://github.com/bluesky-social/atproto/commit/e277158f70a831b04fde3ec84b3c1eaa6ce82e9d)]:\n  - @atproto/api@0.13.27\n  - @atproto/oauth-provider@0.2.12\n  - @atproto-labs/fetch-node@0.1.5\n\n## 0.4.83\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n  - @atproto/aws@0.2.12\n  - @atproto/identity@0.4.5\n  - @atproto/repo@0.6.2\n  - @atproto/xrpc-server@0.7.6\n  - @atproto-labs/xrpc-utils@0.0.2\n\n## 0.4.82\n\n### Patch Changes\n\n- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on Axios\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto-labs/xrpc-utils@0.0.1\n  - @atproto/identity@0.4.4\n  - @atproto/api@0.13.26\n  - @atproto/common@0.4.6\n  - @atproto/lexicon@0.4.5\n  - @atproto/repo@0.6.1\n  - @atproto/aws@0.2.11\n  - @atproto/crypto@0.4.2\n  - @atproto/oauth-provider@0.2.11\n  - @atproto/xrpc-server@0.7.5\n  - @atproto/xrpc@0.6.6\n\n## 0.4.81\n\n### Patch Changes\n\n- Updated dependencies [[`53621f8e1`](https://github.com/bluesky-social/atproto/commit/53621f8e100a3aa3c1caff10a08d3f4ea919875a)]:\n  - @atproto/api@0.13.25\n\n## 0.4.80\n\n### Patch Changes\n\n- Updated dependencies [[`d90d999de`](https://github.com/bluesky-social/atproto/commit/d90d999defda01a9b04dbce129e254990062c283)]:\n  - @atproto/api@0.13.24\n\n## 0.4.79\n\n### Patch Changes\n\n- [#3251](https://github.com/bluesky-social/atproto/pull/3251) [`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7) Thanks [@foysalit](https://github.com/foysalit)! - Allow takendown account scope on access tokens. Allow takendown accounts to createReports at discretion of the moderation service\n\n- [#3273](https://github.com/bluesky-social/atproto/pull/3273) [`b4674a61a`](https://github.com/bluesky-social/atproto/commit/b4674a61a92ca96f89ac06e705e08c2e6af07e1b) Thanks [@dholms](https://github.com/dholms)! - Allow takendown accounts to perform account migration\n\n- Updated dependencies [[`6d308b857`](https://github.com/bluesky-social/atproto/commit/6d308b857ba2a514ee3c75ebdef7225e298ed7d7), [`9ea2cce9a`](https://github.com/bluesky-social/atproto/commit/9ea2cce9a4c0a08994a8cb5abc81dc4bc2221d0c)]:\n  - @atproto/api@0.13.23\n\n## 0.4.78\n\n### Patch Changes\n\n- Updated dependencies [[`f22383cee`](https://github.com/bluesky-social/atproto/commit/f22383cee8feb8b9f761c801ab6e07ad8dc019ed)]:\n  - @atproto/api@0.13.22\n\n## 0.4.77\n\n### Patch Changes\n\n- Updated dependencies [[`dced566de`](https://github.com/bluesky-social/atproto/commit/dced566de5079ef4208801db476a7e7416f5e5aa)]:\n  - @atproto/api@0.13.21\n\n## 0.4.76\n\n### Patch Changes\n\n- [#3033](https://github.com/bluesky-social/atproto/pull/3033) [`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a) Thanks [@dholms](https://github.com/dholms)! - Ensure we emit all relevant proof blocks for commit ops\n\n- [#3222](https://github.com/bluesky-social/atproto/pull/3222) [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34) Thanks [@gaearon](https://github.com/gaearon)! - Add optional reasons param to listNotifications\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95), [`207728d2b`](https://github.com/bluesky-social/atproto/commit/207728d2b3b819af297ecb90e6373eb7721cbe34), [`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a)]:\n  - @atproto/common@0.4.5\n  - @atproto/lexicon@0.4.4\n  - @atproto/api@0.13.20\n  - @atproto/repo@0.6.0\n  - @atproto/aws@0.2.10\n  - @atproto/crypto@0.4.2\n  - @atproto/oauth-provider@0.2.10\n  - @atproto/xrpc-server@0.7.4\n  - @atproto/xrpc@0.6.5\n\n## 0.4.75\n\n### Patch Changes\n\n- Updated dependencies [[`622654672`](https://github.com/bluesky-social/atproto/commit/6226546725d1bb0375e3c9e0d71af173e8253c4f), [`ed2236220`](https://github.com/bluesky-social/atproto/commit/ed2236220900ab9a6132c525289cfdd959733a42)]:\n  - @atproto/oauth-provider@0.2.9\n  - @atproto/api@0.13.19\n  - @atproto-labs/fetch-node@0.1.4\n\n## 0.4.74\n\n### Patch Changes\n\n- [#3092](https://github.com/bluesky-social/atproto/pull/3092) [`1e367cba2`](https://github.com/bluesky-social/atproto/commit/1e367cba2bd1ff5560c2ec5c2a5d348cd9342b65) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve email validation logic\n\n- [#3066](https://github.com/bluesky-social/atproto/pull/3066) [`5ddd51235`](https://github.com/bluesky-social/atproto/commit/5ddd51235c7e064bddcad2dd218df05d144d18d3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent use of non https: resource uri in production environments\n\n- [#3092](https://github.com/bluesky-social/atproto/pull/3092) [`1e367cba2`](https://github.com/bluesky-social/atproto/commit/1e367cba2bd1ff5560c2ec5c2a5d348cd9342b65) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update list of forbidden domain names in email addresses\n\n- Updated dependencies []:\n  - @atproto/oauth-provider@0.2.8\n\n## 0.4.73\n\n### Patch Changes\n\n- [#3082](https://github.com/bluesky-social/atproto/pull/3082) [`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc) Thanks [@gaearon](https://github.com/gaearon)! - Add hotness as a thread sorting option\n\n- Updated dependencies [[`a3ce23c4c`](https://github.com/bluesky-social/atproto/commit/a3ce23c4ccf4f40998b9d1f5731e5c905390aedc)]:\n  - @atproto/api@0.13.18\n\n## 0.4.72\n\n### Patch Changes\n\n- [#3034](https://github.com/bluesky-social/atproto/pull/3034) [`90399c859`](https://github.com/bluesky-social/atproto/commit/90399c85955301babc689c293bd3e7e1a94505a3) Thanks [@dholms](https://github.com/dholms)! - use getRecord in getFeed rather than resolving full view\n\n- Updated dependencies [[`a4b528e5f`](https://github.com/bluesky-social/atproto/commit/a4b528e5f51c8bfca56b293b0059b88d138ec421), [`2e7aa211d`](https://github.com/bluesky-social/atproto/commit/2e7aa211d2cbc629899c7f87f1713b13b932750b)]:\n  - @atproto/api@0.13.17\n\n## 0.4.71\n\n### Patch Changes\n\n- [#2990](https://github.com/bluesky-social/atproto/pull/2990) [`24423fc2d`](https://github.com/bluesky-social/atproto/commit/24423fc2dd394c99a29dbe4419b356090ef19546) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Ensure read-after-write handles `quoteCount` for `getPostThread` responses.\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`48d08a469`](https://github.com/bluesky-social/atproto/commit/48d08a469f75837e3b7e879d286d12780440b8b8), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`561431fe4`](https://github.com/bluesky-social/atproto/commit/561431fe4897e81767dc768e9a31020d09bf86ff)]:\n  - @atproto/syntax@0.3.1\n  - @atproto/api@0.13.16\n  - @atproto/lexicon@0.4.3\n  - @atproto/repo@0.5.5\n  - @atproto/xrpc@0.6.4\n  - @atproto/xrpc-server@0.7.3\n  - @atproto/aws@0.2.9\n\n## 0.4.70\n\n### Patch Changes\n\n- [#2946](https://github.com/bluesky-social/atproto/pull/2946) [`9e18ab6a3`](https://github.com/bluesky-social/atproto/commit/9e18ab6a35f47e0a9cee76221bfa0817c8a624a1) Thanks [@gaearon](https://github.com/gaearon)! - Fix getPostThread optimistic handling for URIs with handle\n\n- [#2850](https://github.com/bluesky-social/atproto/pull/2850) [`9ffeb5216`](https://github.com/bluesky-social/atproto/commit/9ffeb5216ab29919a2c1f3cc18af26c21a077d4a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use a less cryptic error message when proxying fails\n\n- [#2850](https://github.com/bluesky-social/atproto/pull/2850) [`9ffeb5216`](https://github.com/bluesky-social/atproto/commit/9ffeb5216ab29919a2c1f3cc18af26c21a077d4a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow retrying proxied requests\n\n- Updated dependencies [[`d6f33b474`](https://github.com/bluesky-social/atproto/commit/d6f33b4742e0b94722a993efc7d18833d9416bb6), [`b6eeb81c6`](https://github.com/bluesky-social/atproto/commit/b6eeb81c6d454b5ae91b05a21fc1820274c1b429), [`709ba3015`](https://github.com/bluesky-social/atproto/commit/709ba301578c1956b8eb0d89bad717615a4fd7ba), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0), [`839202a3d`](https://github.com/bluesky-social/atproto/commit/839202a3d2b01de25de900cec7540019545798c6), [`e680d55ca`](https://github.com/bluesky-social/atproto/commit/e680d55ca2d7f6b213e2a8693eba6be39163ba41), [`c4b5e5395`](https://github.com/bluesky-social/atproto/commit/c4b5e53957463c37dd16fdd1b897d4ab02ab8e84)]:\n  - @atproto/api@0.13.15\n  - @atproto/oauth-provider@0.2.7\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc-server@0.7.2\n  - @atproto/aws@0.2.8\n  - @atproto/identity@0.4.3\n  - @atproto/repo@0.5.4\n\n## 0.4.69\n\n### Patch Changes\n\n- Updated dependencies [[`209238769`](https://github.com/bluesky-social/atproto/commit/209238769c0bf38bf04f7fa9621eeb176b5c0ed8), [`73f40e63a`](https://github.com/bluesky-social/atproto/commit/73f40e63abe3283efc0a27eef781c00b497caad1)]:\n  - @atproto/api@0.13.14\n\n## 0.4.68\n\n### Patch Changes\n\n- Updated dependencies [[`19e36afb2`](https://github.com/bluesky-social/atproto/commit/19e36afb2c13dbc7b1033eb3cab5e7fc6f496fdc)]:\n  - @atproto/api@0.13.13\n\n## 0.4.67\n\n### Patch Changes\n\n- [#2924](https://github.com/bluesky-social/atproto/pull/2924) [`c1b0e176a`](https://github.com/bluesky-social/atproto/commit/c1b0e176adbc5108bff49d74fbae18de60e86732) Thanks [@dholms](https://github.com/dholms)! - Support alternate did mehtods in update handle\n\n## 0.4.66\n\n### Patch Changes\n\n- [#2871](https://github.com/bluesky-social/atproto/pull/2871) [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add Access-Control-Allow-Headers to .well-known/oauth-protected-resource response\n\n- Updated dependencies [[`8f2b80a0d`](https://github.com/bluesky-social/atproto/commit/8f2b80a0dcf118652452ea09764a947b09991e0f), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`9d40ccbb6`](https://github.com/bluesky-social/atproto/commit/9d40ccbb69103fae9aae7e3cec31e9b3116f3ba2), [`7f26b1765`](https://github.com/bluesky-social/atproto/commit/7f26b176526b9856a8f61faca6f065f0afd43abf)]:\n  - @atproto/oauth-provider@0.2.6\n\n## 0.4.65\n\n### Patch Changes\n\n- Updated dependencies [[`22d039a22`](https://github.com/bluesky-social/atproto/commit/22d039a229e3ef08a793e1c98b473b1b8e18ac5e)]:\n  - @atproto/api@0.13.12\n\n## 0.4.64\n\n### Patch Changes\n\n- Updated dependencies [[`80450cbf2`](https://github.com/bluesky-social/atproto/commit/80450cbf2ca27967ee9fe1a5f4bc590b26f1e6b2)]:\n  - @atproto-labs/fetch-node@0.1.3\n  - @atproto/oauth-provider@0.2.5\n\n## 0.4.63\n\n### Patch Changes\n\n- Updated dependencies [[`a0531ce42`](https://github.com/bluesky-social/atproto/commit/a0531ce429f5139cb0e2cc19aa9b338599947e44)]:\n  - @atproto/api@0.13.11\n\n## 0.4.62\n\n### Patch Changes\n\n- Updated dependencies [[`df14df522`](https://github.com/bluesky-social/atproto/commit/df14df522bb7986e56ee1f6a0f5d862e1ea6f4d5), [`8943c1008`](https://github.com/bluesky-social/atproto/commit/8943c10082702bbc0fc150237c6cc421251afd51)]:\n  - @atproto/api@0.13.10\n  - @atproto-labs/fetch-node@0.1.2\n  - @atproto/oauth-provider@0.2.4\n\n## 0.4.61\n\n### Patch Changes\n\n- [#2834](https://github.com/bluesky-social/atproto/pull/2834) [`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow defaulting to unencoded responses when proxying client requests that do not specify accept-encoding\n\n- [#2836](https://github.com/bluesky-social/atproto/pull/2836) [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c) Thanks [@foysalit](https://github.com/foysalit)! - Add getRepos and getRecords endpoints for bulk fetching\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13), [`a2bad977a`](https://github.com/bluesky-social/atproto/commit/a2bad977a8d941b4075ea3ffee3d6f7a0c0f467c), [`1226ed268`](https://github.com/bluesky-social/atproto/commit/1226ed2682970a58ae433b9deb11290333988ddd)]:\n  - @atproto/common@0.4.4\n  - @atproto/api@0.13.9\n  - @atproto/oauth-provider@0.2.3\n  - @atproto/aws@0.2.7\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.5.3\n  - @atproto/xrpc-server@0.7.1\n\n## 0.4.60\n\n### Patch Changes\n\n- [#2767](https://github.com/bluesky-social/atproto/pull/2767) [`922b94ce3`](https://github.com/bluesky-social/atproto/commit/922b94ce379d861faaa5cf8448b7a44f04e474d3) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Update email templates\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use a hardened, HTTP2 compatible, client to perform proxied requests\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enable keep-alive for hardened \"safe\" fetch() agent\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performances of request proxying by avoiding un-necessary content decoding & buffering.\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add SSRF protection to request proxying\n\n- [#2813](https://github.com/bluesky-social/atproto/pull/2813) [`a06634ae5`](https://github.com/bluesky-social/atproto/commit/a06634ae576217d53ef7ea7f8cbfa9faa8662634) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Update email templates with @ for handles, copy\n\n- [#2824](https://github.com/bluesky-social/atproto/pull/2824) [`b298bfd28`](https://github.com/bluesky-social/atproto/commit/b298bfd280c5de8b38b843fd852e6d2739a776d8) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent server crash when catchall proxy request are cancelled\n\n- Updated dependencies [[`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`2676206e4`](https://github.com/bluesky-social/atproto/commit/2676206e422233fefbf2d9d182e8d462f0957c93), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`ed325d863`](https://github.com/bluesky-social/atproto/commit/ed325d863ce8ea5986c5a45c3188aaa35288b7a8), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/oauth-provider@0.2.2\n  - @atproto/xrpc@0.6.3\n  - @atproto-labs/fetch-node@0.1.1\n  - @atproto/api@0.13.8\n  - @atproto/xrpc-server@0.7.0\n  - @atproto/lexicon@0.4.2\n  - @atproto/common@0.4.3\n  - @atproto/identity@0.4.2\n  - @atproto/repo@0.5.2\n  - @atproto/aws@0.2.6\n  - @atproto/crypto@0.4.1\n\n## 0.4.59\n\n### Patch Changes\n\n- [#2810](https://github.com/bluesky-social/atproto/pull/2810) [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add NUX API\n\n- Updated dependencies [[`e6bd5aecc`](https://github.com/bluesky-social/atproto/commit/e6bd5aecce7954d60e5fb263297e697ab7aab98e), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`33aa0c722`](https://github.com/bluesky-social/atproto/commit/33aa0c722226a18215af0ae1833c7c552fc7aaa7), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/api@0.13.7\n  - @atproto/common@0.4.2\n  - @atproto/xrpc-server@0.6.4\n  - @atproto/xrpc@0.6.2\n  - @atproto/aws@0.2.5\n  - @atproto/crypto@0.4.1\n  - @atproto/repo@0.5.1\n\n## 0.4.58\n\n### Patch Changes\n\n- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442)]:\n  - @atproto/repo@0.5.0\n  - @atproto/aws@0.2.4\n\n## 0.4.57\n\n### Patch Changes\n\n- Updated dependencies [[`e4d41d66f`](https://github.com/bluesky-social/atproto/commit/e4d41d66fa4757a696363f39903562458967b63d)]:\n  - @atproto/api@0.13.6\n\n## 0.4.56\n\n### Patch Changes\n\n- [#2751](https://github.com/bluesky-social/atproto/pull/2751) [`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14) Thanks [@devinivy](https://github.com/devinivy)! - Lexicons and support for video embeds within bsky posts.\n\n- Updated dependencies [[`80ada8f47`](https://github.com/bluesky-social/atproto/commit/80ada8f47628f55f3074cd16a52857e98d117e14)]:\n  - @atproto/api@0.13.5\n\n## 0.4.55\n\n### Patch Changes\n\n- Updated dependencies [[`c180cf4d8`](https://github.com/bluesky-social/atproto/commit/c180cf4d86d174c24dd640d3c95b8a461c0cd41d)]:\n  - @atproto/oauth-provider@0.2.1\n\n## 0.4.54\n\n### Patch Changes\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use locally defined authPassthru\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for \"transition:generic\" and \"transition:chat.bsky\" oauth scopes\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ignore case when checking for dpop auth scheme\n\n- [#2743](https://github.com/bluesky-social/atproto/pull/2743) [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add, and verify, a \"typ\" header to access and refresh tokens\n\n- [#2734](https://github.com/bluesky-social/atproto/pull/2734) [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow OAuthProvider to define its own CORS policies\n\n- Updated dependencies [[`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`bbca17bc5`](https://github.com/bluesky-social/atproto/commit/bbca17bc5388e0b2af26fb107347c8ab507ee42f), [`a8e1f9000`](https://github.com/bluesky-social/atproto/commit/a8e1f9000d9617c4df9d9f0e74ae0e0b73fcfd66), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`dee817b6e`](https://github.com/bluesky-social/atproto/commit/dee817b6e0fc02351d51ce310b5e65239b7c5ed7), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/oauth-provider@0.2.0\n  - @atproto/xrpc-server@0.6.3\n  - @atproto/xrpc@0.6.1\n  - @atproto/api@0.13.4\n  - @atproto/crypto@0.4.1\n  - @atproto/aws@0.2.3\n  - @atproto/identity@0.4.1\n  - @atproto/repo@0.4.3\n\n## 0.4.53\n\n### Patch Changes\n\n- Updated dependencies [[`4ab248354`](https://github.com/bluesky-social/atproto/commit/4ab2483547d5dabfba88ed4419a4f374bbd7cae7)]:\n  - @atproto/api@0.13.3\n\n## 0.4.52\n\n### Patch Changes\n\n- [#2675](https://github.com/bluesky-social/atproto/pull/2675) [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `postgate` records to power quote gating and detached quote posts, plus `hiddenReplies` to the `threadgate` record.\n\n- Updated dependencies [[`2a0c088cc`](https://github.com/bluesky-social/atproto/commit/2a0c088cc5d502ca70da9612a261186aa2f2e1fb), [`aba664fbd`](https://github.com/bluesky-social/atproto/commit/aba664fbdfbaddba321e96db2478e0bc8fc72d27)]:\n  - @atproto/api@0.13.2\n\n## 0.4.51\n\n### Patch Changes\n\n- [#2725](https://github.com/bluesky-social/atproto/pull/2725) [`f9a2f3ed1`](https://github.com/bluesky-social/atproto/commit/f9a2f3ed172ae1a8dc1cca0e893e13eac2e4955d) Thanks [@devinivy](https://github.com/devinivy)! - Fix calls from pds containing content-type but no body\n\n- Updated dependencies [[`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`35a126429`](https://github.com/bluesky-social/atproto/commit/35a1264297bc22acaa6e5ed3f4aed8c351be8bbb), [`5131b027f`](https://github.com/bluesky-social/atproto/commit/5131b027f019cf9f8ec47605648063ae1857f1e3), [`3ebcd4e61`](https://github.com/bluesky-social/atproto/commit/3ebcd4e6161291d3649d7f8a9c5ee4ac26d590a2)]:\n  - @atproto/oauth-provider@0.1.3\n\n## 0.4.50\n\n### Patch Changes\n\n- [#2711](https://github.com/bluesky-social/atproto/pull/2711) [`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly authenticate OAuth requests in catch all handler.\n\n- [#2663](https://github.com/bluesky-social/atproto/pull/2663) [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae) Thanks [@dholms](https://github.com/dholms)! - Validate lxm claims in service auth\n\n- Updated dependencies [[`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae), [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae)]:\n  - @atproto/xrpc-server@0.6.2\n\n## 0.4.49\n\n### Patch Changes\n\n- Updated dependencies [[`22af354a5`](https://github.com/bluesky-social/atproto/commit/22af354a5db595d7cbc0e65f02601de3565337e1)]:\n  - @atproto/api@0.13.1\n\n## 0.4.48\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/lexicon@0.4.1\n  - @atproto/xrpc@0.6.0\n  - @atproto/api@0.13.0\n  - @atproto/oauth-provider@0.1.2\n  - @atproto/repo@0.4.2\n  - @atproto/xrpc-server@0.6.1\n  - @atproto/aws@0.2.2\n\n## 0.4.47\n\n### Patch Changes\n\n- [#2688](https://github.com/bluesky-social/atproto/pull/2688) [`269cbc87c`](https://github.com/bluesky-social/atproto/commit/269cbc87c5ec9d65d1d479269ac5e91dffbb186c) Thanks [@dholms](https://github.com/dholms)! - Inspect bearer auth token on uploadBlob\n\n## 0.4.46\n\n### Patch Changes\n\n- [#2668](https://github.com/bluesky-social/atproto/pull/2668) [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c) Thanks [@dholms](https://github.com/dholms)! - Add lxm and nonce to signed service auth tokens.\n\n- Updated dependencies [[`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c), [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c)]:\n  - @atproto/api@0.12.29\n  - @atproto/xrpc-server@0.6.0\n\n## 0.4.45\n\n### Patch Changes\n\n- [#2676](https://github.com/bluesky-social/atproto/pull/2676) [`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Remove `app.bsky.feed.detach` record, to be replaced by `app.bsky.feed.postgate` record in a future release.\n\n- Updated dependencies [[`951a3df15`](https://github.com/bluesky-social/atproto/commit/951a3df15aa9c1f5b0a2b66cfb0e2eaf6198fe41)]:\n  - @atproto/api@0.12.28\n\n## 0.4.44\n\n### Patch Changes\n\n- [#2664](https://github.com/bluesky-social/atproto/pull/2664) [`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds `app.bsky.feed.detach` record lexicons.\n\n- Updated dependencies [[`ff803fd2b`](https://github.com/bluesky-social/atproto/commit/ff803fd2bfad92eec5f88ee9b347c174731ef4ec)]:\n  - @atproto/api@0.12.27\n\n## 0.4.43\n\n### Patch Changes\n\n- [#2276](https://github.com/bluesky-social/atproto/pull/2276) [`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Updates muted words lexicons to include new attributes `id`, `actorTarget`, and `expiresAt`. Adds and updates methods in API SDK for better management of muted words.\n\n- Updated dependencies [[`77c5306d2`](https://github.com/bluesky-social/atproto/commit/77c5306d2a40d7edd20def73163b8f93f3a30ee7)]:\n  - @atproto/api@0.12.26\n\n## 0.4.42\n\n### Patch Changes\n\n- Updated dependencies [[`12dcdb668`](https://github.com/bluesky-social/atproto/commit/12dcdb668c8ec0f8a89689c326ab3e9dbc6d2f3c), [`76c91f832`](https://github.com/bluesky-social/atproto/commit/76c91f8325363c95e25349e8e236aa2f70e63d5b)]:\n  - @atproto/api@0.12.25\n\n## 0.4.41\n\n### Patch Changes\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use new version of @atproto/oauth-provider with improved UI.\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Obfuscate request headers in logs using utils from @atproto/common\n\n- [#2633](https://github.com/bluesky-social/atproto/pull/2633) [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve parsing of Authorization header\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10), [`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/oauth-provider@0.1.1\n  - @atproto/common@0.4.1\n  - @atproto/aws@0.2.1\n  - @atproto/crypto@0.4.0\n  - @atproto/repo@0.4.1\n  - @atproto/xrpc-server@0.5.3\n\n## 0.4.40\n\n### Patch Changes\n\n- Updated dependencies [[`ed5810179`](https://github.com/bluesky-social/atproto/commit/ed5810179006f254f2035fe1f0e3c4798080cfe0), [`0529bec99`](https://github.com/bluesky-social/atproto/commit/0529bec99183439829a3553f45ac7203763144c3)]:\n  - @atproto/api@0.12.24\n\n## 0.4.39\n\n### Patch Changes\n\n- [#2492](https://github.com/bluesky-social/atproto/pull/2492) [`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863) Thanks [@pfrazee](https://github.com/pfrazee)! - Added bsky app state preference and improved protections against race conditions in preferences sdk\n\n- Updated dependencies [[`bc861a2c2`](https://github.com/bluesky-social/atproto/commit/bc861a2c25b4151fb7e070dc20d5e1e07da21863)]:\n  - @atproto/api@0.12.23\n\n## 0.4.38\n\n### Patch Changes\n\n- [#2553](https://github.com/bluesky-social/atproto/pull/2553) [`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02) Thanks [@devinivy](https://github.com/devinivy)! - Support for starter packs (app.bsky.graph.starterpack)\n\n- Updated dependencies [[`af7d3912a`](https://github.com/bluesky-social/atproto/commit/af7d3912a3b304a752ed72947eaa8cf28b35ec02), [`615a96ddc`](https://github.com/bluesky-social/atproto/commit/615a96ddc2965251cfab060dfc43fc1a51ef4bff)]:\n  - @atproto/api@0.12.22\n  - @atproto/xrpc-server@0.5.2\n\n## 0.4.37\n\n### Patch Changes\n\n- [#2460](https://github.com/bluesky-social/atproto/pull/2460) [`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24) Thanks [@foysalit](https://github.com/foysalit)! - Add DB backed team member management for ozone\n\n- Updated dependencies [[`3ad051996`](https://github.com/bluesky-social/atproto/commit/3ad0519961e2437aa4870bf1358e6c275dcdee24)]:\n  - @atproto/api@0.12.21\n\n## 0.4.36\n\n### Patch Changes\n\n- Updated dependencies [[`ea0f10b5d`](https://github.com/bluesky-social/atproto/commit/ea0f10b5d0d334eb587032c54d5ace9ea811cf26)]:\n  - @atproto/api@0.12.20\n\n## 0.4.35\n\n### Patch Changes\n\n- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens\n\n- Updated dependencies [[`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646)]:\n  - @atproto/oauth-provider@0.1.0\n  - @atproto-labs/fetch-node@0.1.0\n\n## 0.4.34\n\n### Patch Changes\n\n- Updated dependencies [[`7c1973841`](https://github.com/bluesky-social/atproto/commit/7c1973841dab416ae19435d37853aeea1f579d39)]:\n  - @atproto/api@0.12.19\n\n## 0.4.33\n\n### Patch Changes\n\n- Updated dependencies [[`58abcbd8b`](https://github.com/bluesky-social/atproto/commit/58abcbd8b6e42a1f66bda6acc3ee6a2c0894e546)]:\n  - @atproto/api@0.12.18\n\n## 0.4.32\n\n### Patch Changes\n\n- [#2426](https://github.com/bluesky-social/atproto/pull/2426) [`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd) Thanks [@foysalit](https://github.com/foysalit)! - Add com.atproto.admin.searchAccounts lexicon to allow searching for accounts using email address\n\n- [#2547](https://github.com/bluesky-social/atproto/pull/2547) [`d8e2fefa9`](https://github.com/bluesky-social/atproto/commit/d8e2fefa98581edb3837e567657aa41a1cdb21f6) Thanks [@dholms](https://github.com/dholms)! - Emit an account event on updateSubjectStatus.\n\n- Updated dependencies [[`2b21b5be2`](https://github.com/bluesky-social/atproto/commit/2b21b5be293d32c5eb5ae971c39703bc7d2224fd)]:\n  - @atproto/api@0.12.17\n\n## 0.4.31\n\n### Patch Changes\n\n- [#2539](https://github.com/bluesky-social/atproto/pull/2539) [`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46) Thanks [@dholms](https://github.com/dholms)! - Allow updating deactivation state through admin.updateSubjectStatus\n\n- Updated dependencies [[`9495af23b`](https://github.com/bluesky-social/atproto/commit/9495af23bdb328cfc71182ac80e6eb61863d7a46)]:\n  - @atproto/api@0.12.16\n\n## 0.4.30\n\n### Patch Changes\n\n- [#2531](https://github.com/bluesky-social/atproto/pull/2531) [`255d5ea1f`](https://github.com/bluesky-social/atproto/commit/255d5ea1f06726547cdbe59c83bd18f2d4746912) Thanks [@dholms](https://github.com/dholms)! - Account deactivation. Current hosting status returned on session routes.\n\n- Updated dependencies [[`255d5ea1f`](https://github.com/bluesky-social/atproto/commit/255d5ea1f06726547cdbe59c83bd18f2d4746912)]:\n  - @atproto/api@0.12.15\n\n## 0.4.29\n\n### Patch Changes\n\n- Updated dependencies [[`c4af6a409`](https://github.com/bluesky-social/atproto/commit/c4af6a409ea2171c3cf1d0e7c8ed496794a3f049)]:\n  - @atproto/api@0.12.14\n\n## 0.4.28\n\n### Patch Changes\n\n- [#2522](https://github.com/bluesky-social/atproto/pull/2522) [`53551be6c`](https://github.com/bluesky-social/atproto/commit/53551be6cf092a9b4d2e132788b94ac0d4ffcecc) Thanks [@devinivy](https://github.com/devinivy)! - Set max-age CORS header to max practical value\n\n## 0.4.27\n\n### Patch Changes\n\n- [#2517](https://github.com/bluesky-social/atproto/pull/2517) [`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9) Thanks [@dholms](https://github.com/dholms)! - Fix a bad join on privileged app passwords.\n\n- Updated dependencies [[`1d4ab5d04`](https://github.com/bluesky-social/atproto/commit/1d4ab5d046aac4539658ee6d7e61882c54d5beb9)]:\n  - @atproto/api@0.12.13\n\n## 0.4.26\n\n### Patch Changes\n\n- [#2515](https://github.com/bluesky-social/atproto/pull/2515) [`0cc5ef70f`](https://github.com/bluesky-social/atproto/commit/0cc5ef70f4e5a8e24983051d5f5ad8ee27be8684) Thanks [@dholms](https://github.com/dholms)! - Add privileged app password auth scope\n\n## 0.4.25\n\n### Patch Changes\n\n- [`0e8acb9fb`](https://github.com/bluesky-social/atproto/commit/0e8acb9fbaf3edcebd8e4f8fe4a381ede0206895) Thanks [@devinivy](https://github.com/devinivy)! - Only distribute service tokens via non-app-pass access tokens\n\n## 0.4.24\n\n### Patch Changes\n\n- [`cf25a60e2`](https://github.com/bluesky-social/atproto/commit/cf25a60e25b7531a359f0849729209a55193f7d6) Thanks [@devinivy](https://github.com/devinivy)! - Do not allow app passwords to communicate with chat service\n\n- Updated dependencies [[`1f560f021`](https://github.com/bluesky-social/atproto/commit/1f560f021c07eb9e8d76577e67fd2d7ac39cdee4)]:\n  - @atproto/api@0.12.12\n\n## 0.4.23\n\n### Patch Changes\n\n- Updated dependencies [[`06d2328ee`](https://github.com/bluesky-social/atproto/commit/06d2328eeb8d706018dbdf7cc7b9862dd65b96cb)]:\n  - @atproto/api@0.12.11\n\n## 0.4.22\n\n### Patch Changes\n\n- [#2485](https://github.com/bluesky-social/atproto/pull/2485) [`d32f7215f`](https://github.com/bluesky-social/atproto/commit/d32f7215f69bc87f50890d9cfdb09840c2fbaa41) Thanks [@devinivy](https://github.com/devinivy)! - Add lexicons for chat.bsky namespace\n\n- Updated dependencies [[`d32f7215f`](https://github.com/bluesky-social/atproto/commit/d32f7215f69bc87f50890d9cfdb09840c2fbaa41)]:\n  - @atproto/api@0.12.10\n\n## 0.4.21\n\n### Patch Changes\n\n- [`f36585013`](https://github.com/bluesky-social/atproto/commit/f365850139ffb2b5e63facfd95eedf0b87d01ee7) Thanks [@devinivy](https://github.com/devinivy)! - Support generic service proxying the PDS\n\n## 0.4.20\n\n### Patch Changes\n\n- Updated dependencies [[`f83b4c8ca`](https://github.com/bluesky-social/atproto/commit/f83b4c8cad01cebc1b67caa6c7ebe45f07b2f318)]:\n  - @atproto/api@0.12.9\n\n## 0.4.19\n\n### Patch Changes\n\n- Updated dependencies [[`58f719cc1`](https://github.com/bluesky-social/atproto/commit/58f719cc1c8d0ebd5ad7cf11221372b671cd7857)]:\n  - @atproto/api@0.12.8\n\n## 0.4.18\n\n### Patch Changes\n\n- [#2390](https://github.com/bluesky-social/atproto/pull/2390) [`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933) Thanks [@foysalit](https://github.com/foysalit)! - Allow muting reports from accounts via `#modEventMuteReporter` event\n\n- Updated dependencies [[`58551bbe0`](https://github.com/bluesky-social/atproto/commit/58551bbe0595462c44fc3b6ab5b83e520f141933)]:\n  - @atproto/api@0.12.7\n\n## 0.4.17\n\n### Patch Changes\n\n- Updated dependencies [[`b9b7c5821`](https://github.com/bluesky-social/atproto/commit/b9b7c582199d57d2fe0af8af5c8c411ed34f5b9d)]:\n  - @atproto/api@0.12.6\n\n## 0.4.16\n\n### Patch Changes\n\n- Updated dependencies [[`3424a1770`](https://github.com/bluesky-social/atproto/commit/3424a17703891f5678ec76ef97e696afb3288b22)]:\n  - @atproto/api@0.12.5\n\n## 0.4.15\n\n### Patch Changes\n\n- [#2416](https://github.com/bluesky-social/atproto/pull/2416) [`93a4a4df9`](https://github.com/bluesky-social/atproto/commit/93a4a4df9ce38f89a5d05e300d247b85fb007e05) Thanks [@devinivy](https://github.com/devinivy)! - Support for email auth factor lexicons\n\n- Updated dependencies [[`93a4a4df9`](https://github.com/bluesky-social/atproto/commit/93a4a4df9ce38f89a5d05e300d247b85fb007e05)]:\n  - @atproto/api@0.12.4\n\n## 0.4.14\n\n### Patch Changes\n\n- Updated dependencies [[`0edef0ec0`](https://github.com/bluesky-social/atproto/commit/0edef0ec01403fd6097a4d2875b68313f2f1261f), [`c6d758b8b`](https://github.com/bluesky-social/atproto/commit/c6d758b8b63f4ef50b2ab9afc62164e92a53e7f0)]:\n  - @atproto/api@0.12.3\n\n## 0.4.13\n\n### Patch Changes\n\n- Updated dependencies [[`cd4fcc709`](https://github.com/bluesky-social/atproto/commit/cd4fcc709fe8d725a4af769ce21f53711fe5622a)]:\n  - @atproto/xrpc-server@0.5.1\n\n## 0.4.12\n\n### Patch Changes\n\n- [`d77ac35d4`](https://github.com/bluesky-social/atproto/commit/d77ac35d484925d90169e6a1047cddfbe90923bc) Thanks [@devinivy](https://github.com/devinivy)! - Fix to blob deletion w/ disk store (see #2381)\n\n## 0.4.11\n\n### Patch Changes\n\n- Updated dependencies [[`abc6f82da`](https://github.com/bluesky-social/atproto/commit/abc6f82da38abef2b1bbe8d9e41a0534a5418c9e)]:\n  - @atproto/api@0.12.2\n\n## 0.4.10\n\n### Patch Changes\n\n- [#2342](https://github.com/bluesky-social/atproto/pull/2342) [`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds the `associated` property to `profile` and `profile-basic` views, bringing them in line with `profile-detailed` views.\n\n- Updated dependencies [[`eb7668c07`](https://github.com/bluesky-social/atproto/commit/eb7668c07d44f4b42ea2cc28143c64f4ba3312f5)]:\n  - @atproto/api@0.12.1\n\n## 0.4.9\n\n### Patch Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9), [`36f2e966c`](https://github.com/bluesky-social/atproto/commit/36f2e966cba6cc90ba4320520da5c7381cfb8086)]:\n  - @atproto/xrpc-server@0.5.0\n  - @atproto/identity@0.4.0\n  - @atproto/lexicon@0.4.0\n  - @atproto/common@0.4.0\n  - @atproto/crypto@0.4.0\n  - @atproto/syntax@0.3.0\n  - @atproto/repo@0.4.0\n  - @atproto/xrpc@0.5.0\n  - @atproto/api@0.12.0\n  - @atproto/aws@0.2.0\n\n## 0.4.8\n\n### Patch Changes\n\n- Updated dependencies [[`7dd9941b7`](https://github.com/bluesky-social/atproto/commit/7dd9941b73dbbd82601740e021cc87d765af60ca)]:\n  - @atproto/api@0.11.2\n\n## 0.4.7\n\n### Patch Changes\n\n- [`971d3e4c2`](https://github.com/bluesky-social/atproto/commit/971d3e4c26ecfda746e83d458391715752ea7064) Thanks [@devinivy](https://github.com/devinivy)! - PDS operators may configure a public contact email\n\n- Updated dependencies [[`219480764`](https://github.com/bluesky-social/atproto/commit/2194807644cbdb0021e867437693300c1b0e55f5)]:\n  - @atproto/api@0.11.1\n\n## 0.4.6\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0), [`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/identity@0.3.3\n  - @atproto/api@0.11.0\n  - @atproto/common@0.3.4\n  - @atproto/lexicon@0.3.3\n  - @atproto/repo@0.3.9\n  - @atproto/syntax@0.2.1\n  - @atproto/aws@0.1.9\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.4\n  - @atproto/xrpc@0.4.3\n\n## 0.4.5\n\n### Patch Changes\n\n- [#2279](https://github.com/bluesky-social/atproto/pull/2279) [`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655) Thanks [@gaearon](https://github.com/gaearon)! - Change Following feed prefs to only show replies from people you follow by default\n\n- Updated dependencies [[`192223f12`](https://github.com/bluesky-social/atproto/commit/192223f127c0b226287df1ecfcd953636db08655)]:\n  - @atproto/api@0.10.5\n\n## 0.4.4\n\n### Patch Changes\n\n- Updated dependencies [[`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f), [`6ec885992`](https://github.com/bluesky-social/atproto/commit/6ec8859929a16f9725319cc398b716acf913b01f)]:\n  - @atproto/api@0.10.4\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies [[`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed), [`2a0ceb818`](https://github.com/bluesky-social/atproto/commit/2a0ceb8180faa17de8061d4fa6c361b57a2005ed)]:\n  - @atproto/api@0.10.3\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410), [`43531905c`](https://github.com/bluesky-social/atproto/commit/43531905ce1aec6d36d9be5943782811ecca6e6d), [`61b3d2525`](https://github.com/bluesky-social/atproto/commit/61b3d25253353db2da1336004f94e7dc5adb0410)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/api@0.10.2\n  - @atproto/lexicon@0.3.2\n  - @atproto/repo@0.3.8\n  - @atproto/xrpc@0.4.2\n  - @atproto/xrpc-server@0.4.3\n  - @atproto/aws@0.1.8\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [[`514aab92d`](https://github.com/bluesky-social/atproto/commit/514aab92d26acd43859285f46318e386846522b1)]:\n  - @atproto/api@0.10.1\n\n## 0.4.0\n\n### Patch Changes\n\n- Updated dependencies [[`b60719480`](https://github.com/bluesky-social/atproto/commit/b60719480f5f00bffd074a40e8ddc03aa93d137d), [`4c511b3d9`](https://github.com/bluesky-social/atproto/commit/4c511b3d9de41ffeae3fc11db941e7df04f4468a)]:\n  - @atproto/api@0.10.0\n\n## 0.3.19\n\n### Patch Changes\n\n- Updated dependencies [[`f79cc6339`](https://github.com/bluesky-social/atproto/commit/f79cc63390ae9dbd47a4ff5d694eec25b78b788e)]:\n  - @atproto/api@0.9.8\n\n## 0.3.18\n\n### Patch Changes\n\n- Updated dependencies [[`fcf8e3faf`](https://github.com/bluesky-social/atproto/commit/fcf8e3faf311559162c3aa0d9af36f84951914bc), [`8c94979f7`](https://github.com/bluesky-social/atproto/commit/8c94979f73fc5057449e24e66ef2e09b0e17e55b)]:\n  - @atproto/repo@0.3.7\n  - @atproto/api@0.9.7\n  - @atproto/aws@0.1.7\n\n## 0.3.17\n\n### Patch Changes\n\n- Updated dependencies [[`e4ec7af03`](https://github.com/bluesky-social/atproto/commit/e4ec7af03608949fc3b00a845f547a77599b5ad0)]:\n  - @atproto/api@0.9.6\n\n## 0.3.16\n\n### Patch Changes\n\n- Updated dependencies [[`8994d363`](https://github.com/bluesky-social/atproto/commit/8994d3633adad1c02569d6d44ae896e18195e8e2)]:\n  - @atproto/api@0.9.5\n\n## 0.3.15\n\n### Patch Changes\n\n- Updated dependencies [[`4171c04a`](https://github.com/bluesky-social/atproto/commit/4171c04ad81c5734a4558bc41fa1c4f3a1aba18c)]:\n  - @atproto/api@0.9.4\n\n## 0.3.14\n\n### Patch Changes\n\n- Updated dependencies [[`5368245a`](https://github.com/bluesky-social/atproto/commit/5368245a6ef7095c86ad166fb04ff9bef27c3c3e)]:\n  - @atproto/api@0.9.3\n\n## 0.3.13\n\n### Patch Changes\n\n- Updated dependencies [[`15f38560`](https://github.com/bluesky-social/atproto/commit/15f38560b9e2dc3af8cf860826e7477234fe6a2d)]:\n  - @atproto/api@0.9.2\n\n## 0.3.12\n\n### Patch Changes\n\n- Updated dependencies [[`c6fc73ae`](https://github.com/bluesky-social/atproto/commit/c6fc73aee6c245d12f876abd11889b8dbd0ce2ed)]:\n  - @atproto/api@0.9.1\n\n## 0.3.11\n\n### Patch Changes\n\n- [#1988](https://github.com/bluesky-social/atproto/pull/1988) [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6) Thanks [@bnewbold](https://github.com/bnewbold)! - remove deprecated app.bsky.unspecced.getPopular endpoint\n\n- Updated dependencies [[`e43396af`](https://github.com/bluesky-social/atproto/commit/e43396af0973748dd2d034e88d35cf7ae8b4df2c), [`bf8d718c`](https://github.com/bluesky-social/atproto/commit/bf8d718cf918ac8d8a2cb1f57fde80535284642d), [`51fcba7a`](https://github.com/bluesky-social/atproto/commit/51fcba7a7945c604fc50e9545850a12ef0ee6da6)]:\n  - @atproto/api@0.9.0\n\n## 0.3.10\n\n### Patch Changes\n\n- Updated dependencies [[`14067733`](https://github.com/bluesky-social/atproto/commit/140677335f76b99129c1f593d9e11d64624386c6)]:\n  - @atproto/api@0.8.0\n\n## 0.3.9\n\n### Patch Changes\n\n- Updated dependencies [[`8f3f43cb`](https://github.com/bluesky-social/atproto/commit/8f3f43cb40f79ff7c52f81290daec55cfb000093)]:\n  - @atproto/api@0.7.4\n\n## 0.3.8\n\n### Patch Changes\n\n- Updated dependencies [[`7dec9df3`](https://github.com/bluesky-social/atproto/commit/7dec9df3b583ee8c06c0c6a7e32c259820dc84a5)]:\n  - @atproto/api@0.7.3\n\n## 0.3.7\n\n### Patch Changes\n\n- [#1776](https://github.com/bluesky-social/atproto/pull/1776) [`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add `posts_and_author_threads` filter to `getAuthorFeed`\n\n- Updated dependencies [[`ffe39aae`](https://github.com/bluesky-social/atproto/commit/ffe39aae8394394f73bbfaa9047a8b5818aa053a)]:\n  - @atproto/api@0.7.2\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [[`60deea17`](https://github.com/bluesky-social/atproto/commit/60deea17622f7c574c18432a55ced4e1cdc1b3a1)]:\n  - @atproto/api@0.7.1\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`45352f9b`](https://github.com/bluesky-social/atproto/commit/45352f9b6d02aa405be94e9102424d983912ca5d)]:\n  - @atproto/api@0.7.0\n\n## 0.3.4\n\n### Patch Changes\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60), [`378fc613`](https://github.com/bluesky-social/atproto/commit/378fc6132f621ca517897c9467ed5bba134b3776)]:\n  - @atproto/syntax@0.1.5\n  - @atproto/api@0.6.24\n  - @atproto/lexicon@0.3.1\n  - @atproto/repo@0.3.6\n  - @atproto/xrpc@0.4.1\n  - @atproto/xrpc-server@0.4.2\n  - @atproto/aws@0.1.6\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n  - @atproto/xrpc-server@0.4.1\n  - @atproto/aws@0.1.5\n  - @atproto/identity@0.3.2\n  - @atproto/repo@0.3.5\n  - @atproto/api@0.6.23\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`772736a0`](https://github.com/bluesky-social/atproto/commit/772736a01081f39504e1b19a1b3687783bb78f07)]:\n  - @atproto/api@0.6.23\n\n## 0.3.1\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/xrpc@0.4.0\n  - @atproto/xrpc-server@0.4.0\n  - @atproto/identity@0.3.1\n  - @atproto/common@0.3.3\n  - @atproto/crypto@0.2.3\n  - @atproto/syntax@0.1.4\n  - @atproto/repo@0.3.4\n  - @atproto/api@0.6.22\n  - @atproto/aws@0.1.4\n\n## 0.3.0\n\n### Patch Changes\n\n- Updated dependencies [[`9c98a5ba`](https://github.com/bluesky-social/atproto/commit/9c98a5baaf503b02238a6afe4f6e2b79c5181693), [`bb039d8e`](https://github.com/bluesky-social/atproto/commit/bb039d8e4ce5b7f70c4f3e86d1327e210ef24dc3), [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/api@0.6.21\n  - @atproto/identity@0.3.0\n  - @atproto/repo@0.3.3\n  - @atproto/common@0.3.2\n  - @atproto/lexicon@0.2.3\n  - @atproto/syntax@0.1.3\n  - @atproto/aws@0.1.3\n  - @atproto/xrpc-server@0.3.3\n  - @atproto/xrpc@0.3.3\n\n## 0.1.20\n\n### Patch Changes\n\n- [#1568](https://github.com/bluesky-social/atproto/pull/1568) [`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b) Thanks [@dholms](https://github.com/dholms)! - Added email verification and update flows\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/api@0.6.20\n  - @atproto/common@0.3.1\n  - @atproto/identity@0.2.1\n  - @atproto/lexicon@0.2.2\n  - @atproto/repo@0.3.2\n  - @atproto/syntax@0.1.2\n  - @atproto/xrpc-server@0.3.2\n  - @atproto/xrpc@0.3.2\n\n## 0.1.19\n\n### Patch Changes\n\n- Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]:\n  - @atproto/api@0.6.19\n\n## 0.1.18\n\n### Patch Changes\n\n- Updated dependencies [[`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf)]:\n  - @atproto/api@0.6.18\n\n## 0.1.17\n\n### Patch Changes\n\n- [#1637](https://github.com/bluesky-social/atproto/pull/1637) [`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduce general support for tags on posts\n\n- Updated dependencies [[`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd)]:\n  - @atproto/api@0.6.17\n\n## 0.1.16\n\n### Patch Changes\n\n- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]:\n  - @atproto/api@0.6.16\n\n## 0.1.15\n\n### Patch Changes\n\n- Updated dependencies [[`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f)]:\n  - @atproto/api@0.6.15\n\n## 0.1.14\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n  - @atproto/api@0.6.14\n  - @atproto/lexicon@0.2.1\n  - @atproto/repo@0.3.1\n  - @atproto/xrpc@0.3.1\n  - @atproto/xrpc-server@0.3.1\n\n## 0.1.13\n\n### Patch Changes\n\n- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]:\n  - @atproto/api@0.6.13\n"
  },
  {
    "path": "packages/pds/README.md",
    "content": "# @atproto/pds: Personal Data Server (PDS)\n\nTypeScript reference implementation of an atproto PDS.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/pds)](https://www.npmjs.com/package/@atproto/pds)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\nIf you are interested in self-hosting a PDS, you probably want this repository instead, which has a thin service wrapper, documentation, a Dockerfile, etc: https://github.com/bluesky-social/pds\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/pds/build.templates.js",
    "content": "/* eslint-env node */\n\nconst hbsPlugin = require('esbuild-plugin-handlebars')\nconst { globSync } = require('glob')\n\nrequire('esbuild').build({\n  logLevel: 'info',\n  watch: process.argv.includes('--watch'),\n  entryPoints: globSync('src/**/*.hbs'),\n  sourcemap: true,\n  outdir: 'dist/mailer/templates',\n  platform: 'node',\n  format: 'cjs',\n  plugins: [\n    hbsPlugin({\n      filter: /\\.(hbs)$/,\n      additionalHelpers: {},\n      precompileOptions: {},\n    }),\n  ],\n})\n"
  },
  {
    "path": "packages/pds/example.env",
    "content": "# See more env options in src/config/env.ts\n# Hostname - the public domain that you intend to deploy your service at\nPDS_HOSTNAME=\"example.com\"\nPDS_PORT=\"2583\"\n\n# Database config - use one or the other\nPDS_DATA_DIRECTORY=\"data\"\n\n# Blobstore - filesystem location to store uploaded blobs\nPDS_BLOBSTORE_DISK_LOCATION=\"blobs\"\n\n# Private keys - these are each expected to be 64 char hex strings (256 bit)\nPDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX=\"3ee68...\"\nPDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=\"e049f...\"\n\n# Secrets - update to secure high-entropy strings\nPDS_DPOP_SECRET=\"32-random-bytes-hex-encoded\"\nPDS_JWT_SECRET=\"jwt-secret\"\nPDS_ADMIN_PASSWORD=\"admin-pass\"\n\n# Environment - example is for live network\nPDS_DID_PLC_URL=\"https://plc.directory\"\nPDS_BSKY_APP_VIEW_URL=\"https://api.bsky.app\"\nPDS_BSKY_APP_VIEW_DID=\"did:web:api.bsky.app\"\nPDS_CRAWLERS=\"https://bsky.network\"\n\n# OAuth Provider\nPDS_OAUTH_PROVIDER_NAME=\"John's self hosted PDS\"\nPDS_OAUTH_PROVIDER_LOGO=\nPDS_OAUTH_PROVIDER_PRIMARY_COLOR=\"#7507e3\"\nPDS_OAUTH_PROVIDER_ERROR_COLOR=\nPDS_OAUTH_PROVIDER_HOME_LINK=\nPDS_OAUTH_PROVIDER_TOS_LINK=\nPDS_OAUTH_PROVIDER_POLICY_LINK=\nPDS_OAUTH_PROVIDER_SUPPORT_LINK=\n\n# Debugging\nNODE_TLS_REJECT_UNAUTHORIZED=1\nLOG_ENABLED=0\nLOG_LEVEL=info\nPDS_INVITE_REQUIRED=1\nPDS_DISABLE_SSRF_PROTECTION=0\n"
  },
  {
    "path": "packages/pds/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'PDS',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  // Jest requires all ESM dependencies to be transpiled (even if they are\n  // dynamically import()ed).\n  transformIgnorePatterns: [\n    `/node_modules/.pnpm/(?!(get-port|lande|toygrad)@)`,\n  ],\n  testTimeout: 60000,\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/pds/package.json",
    "content": "{\n  \"name\": \"@atproto/pds\",\n  \"version\": \"0.4.216\",\n  \"license\": \"MIT\",\n  \"description\": \"Reference implementation of atproto Personal Data Server (PDS)\",\n  \"keywords\": [\n    \"atproto\",\n    \"pds\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/pds\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"scripts\": {\n    \"codegen\": \"lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*\",\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"postbuild\": \"node ./build.templates.js\",\n    \"dev\": \"node ./build.templates.js --watch\",\n    \"pretest\": \"puppeteer browsers install chrome\",\n    \"test\": \"../dev-infra/with-test-redis-and-db.sh jest\",\n    \"test:sqlite\": \"jest\",\n    \"test:sqlite-only\": \"jest --testPathIgnorePatterns /tests/proxied/*\",\n    \"test:log\": \"tail -50 test.log | pino-pretty\",\n    \"test:updateSnapshot\": \"../dev-infra/with-test-redis-and-db.sh jest --updateSnapshot\",\n    \"migration:create\": \"ts-node ./bin/migration-create.ts\"\n  },\n  \"dependencies\": {\n    \"@atproto-labs/fetch-node\": \"workspace:^\",\n    \"@atproto-labs/simple-store\": \"workspace:^\",\n    \"@atproto-labs/simple-store-memory\": \"workspace:^\",\n    \"@atproto-labs/simple-store-redis\": \"workspace:^\",\n    \"@atproto-labs/xrpc-utils\": \"workspace:^\",\n    \"@atproto/api\": \"workspace:^\",\n    \"@atproto/aws\": \"workspace:^\",\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/identity\": \"workspace:^\",\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/oauth-provider\": \"workspace:^\",\n    \"@atproto/oauth-scopes\": \"workspace:^\",\n    \"@atproto/repo\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"@atproto/xrpc-server\": \"workspace:^\",\n    \"@did-plc/lib\": \"^0.0.4\",\n    \"@hapi/address\": \"^5.1.1\",\n    \"better-sqlite3\": \"^10.0.0\",\n    \"bytes\": \"^3.1.2\",\n    \"compression\": \"^1.7.4\",\n    \"cors\": \"^2.8.5\",\n    \"disposable-email-domains-js\": \"^1.5.0\",\n    \"express\": \"^4.17.2\",\n    \"express-async-errors\": \"^3.1.1\",\n    \"file-type\": \"^16.5.4\",\n    \"glob\": \"^10.3.10\",\n    \"handlebars\": \"^4.7.7\",\n    \"http-terminator\": \"^3.2.0\",\n    \"ioredis\": \"^5.3.2\",\n    \"jose\": \"^5.0.1\",\n    \"key-encoder\": \"^2.0.3\",\n    \"kysely\": \"^0.22.0\",\n    \"multiformats\": \"^9.9.0\",\n    \"nodemailer\": \"^6.8.0\",\n    \"nodemailer-html-to-text\": \"^3.2.0\",\n    \"p-queue\": \"^6.6.2\",\n    \"pino\": \"^8.21.0\",\n    \"pino-http\": \"^8.2.1\",\n    \"typed-emitter\": \"^2.1.0\",\n    \"uint8arrays\": \"3.0.0\",\n    \"undici\": \"^6.19.8\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@atproto/api\": \"workspace:^\",\n    \"@atproto/bsky\": \"workspace:^\",\n    \"@atproto/lex-cli\": \"workspace:^\",\n    \"@atproto/oauth-client-browser-example\": \"workspace:^\",\n    \"@atproto/pds-entryway\": \"npm:@atproto/pds@0.3.0-entryway.3\",\n    \"@did-plc/server\": \"^0.0.1\",\n    \"@types/cors\": \"^2.8.12\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-serve-static-core\": \"^4.17.36\",\n    \"@types/nodemailer\": \"^6.4.6\",\n    \"@types/qs\": \"^6.9.7\",\n    \"esbuild\": \"^0.14.48\",\n    \"esbuild-plugin-handlebars\": \"^1.0.3\",\n    \"get-port\": \"^6.1.2\",\n    \"jest\": \"^28.1.2\",\n    \"puppeteer\": \"^23.11.1\",\n    \"ts-node\": \"^10.8.2\",\n    \"typescript\": \"^5.6.3\",\n    \"ws\": \"^8.12.0\"\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/account-manager.ts",
    "content": "import { KeyObject } from 'node:crypto'\nimport { CID } from 'multiformats/cid'\nimport { HOUR, wait } from '@atproto/common'\nimport { IdResolver } from '@atproto/identity'\nimport { isValidTld } from '@atproto/syntax'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AuthScope } from '../auth-scope'\nimport { softDeleted } from '../db'\nimport { hasExplicitSlur } from '../handle/explicit-slurs'\nimport {\n  baseNormalizeAndValidate,\n  ensureHandleServiceConstraints,\n  isServiceDomain,\n} from '../handle/index'\nimport { StatusAttr } from '../lexicon/types/com/atproto/admin/defs'\nimport { AccountDb, EmailTokenPurpose, getDb, getMigrator } from './db'\nimport * as account from './helpers/account'\nimport { AccountStatus, ActorAccount } from './helpers/account'\nimport * as auth from './helpers/auth'\nimport * as emailToken from './helpers/email-token'\nimport * as invite from './helpers/invite'\nimport * as password from './helpers/password'\nimport * as repo from './helpers/repo'\nimport * as scrypt from './helpers/scrypt'\nimport * as token from './helpers/token'\n\nexport { AccountStatus, formatAccountStatus } from './helpers/account'\n\nexport type AccountManagerDbConfig = {\n  accountDbLoc: string\n  disableWalAutoCheckpoint: boolean\n}\n\nexport class AccountManager {\n  readonly db: AccountDb\n\n  constructor(\n    readonly idResolver: IdResolver,\n    readonly jwtKey: KeyObject,\n    readonly serviceDid: string,\n    readonly serviceHandleDomains: string[],\n    db: AccountManagerDbConfig,\n  ) {\n    this.db = getDb(db.accountDbLoc, db.disableWalAutoCheckpoint)\n  }\n\n  async migrateOrThrow() {\n    await this.db.ensureWal()\n    await getMigrator(this.db).migrateToLatestOrThrow()\n  }\n\n  close() {\n    this.db.close()\n  }\n\n  // Account\n  // ----------\n\n  async getAccount(\n    handleOrDid: string,\n    flags?: account.AvailabilityFlags,\n  ): Promise<ActorAccount | null> {\n    return account.getAccount(this.db, handleOrDid, flags)\n  }\n\n  async getAccounts(\n    dids: string[],\n    flags?: account.AvailabilityFlags,\n  ): Promise<Map<string, ActorAccount>> {\n    return account.getAccounts(this.db, dids, flags)\n  }\n\n  async getAccountByEmail(\n    email: string,\n    flags?: account.AvailabilityFlags,\n  ): Promise<ActorAccount | null> {\n    return account.getAccountByEmail(this.db, email, flags)\n  }\n\n  async isAccountActivated(did: string): Promise<boolean> {\n    const account = await this.getAccount(did, { includeDeactivated: true })\n    if (!account) return false\n    return !account.deactivatedAt\n  }\n\n  async getDidForActor(\n    handleOrDid: string,\n    flags?: account.AvailabilityFlags,\n  ): Promise<string | null> {\n    const got = await this.getAccount(handleOrDid, flags)\n    return got?.did ?? null\n  }\n\n  async getAccountStatus(handleOrDid: string): Promise<AccountStatus> {\n    const got = await this.getAccount(handleOrDid, {\n      includeDeactivated: true,\n      includeTakenDown: true,\n    })\n\n    const res = account.formatAccountStatus(got)\n    return res.active ? AccountStatus.Active : res.status\n  }\n\n  async normalizeAndValidateHandle(\n    handle: string,\n    {\n      did,\n      allowAnyValid,\n    }: {\n      did?: string\n      allowAnyValid?: boolean\n    } = {},\n  ): Promise<string> {\n    const normalized = baseNormalizeAndValidate(handle)\n\n    // tld validation\n    if (!isValidTld(normalized)) {\n      throw new InvalidRequestError(\n        'Handle TLD is invalid or disallowed',\n        'InvalidHandle',\n      )\n    }\n    // slur check\n    if (!allowAnyValid && hasExplicitSlur(normalized)) {\n      throw new InvalidRequestError(\n        'Inappropriate language in handle',\n        'InvalidHandle',\n      )\n    }\n    if (isServiceDomain(normalized, this.serviceHandleDomains)) {\n      // verify constraints on a service domain\n      ensureHandleServiceConstraints(\n        normalized,\n        this.serviceHandleDomains,\n        allowAnyValid,\n      )\n    } else {\n      if (did == null) {\n        throw new InvalidRequestError(\n          'Not a supported handle domain',\n          'UnsupportedDomain',\n        )\n      }\n      // verify resolution of a non-service domain\n      const resolvedDid = await this.idResolver.handle.resolve(normalized)\n      if (resolvedDid !== did) {\n        throw new InvalidRequestError('External handle did not resolve to DID')\n      }\n    }\n\n    return normalized\n  }\n\n  async createAccount({\n    did,\n    handle,\n    email,\n    password,\n    repoCid,\n    repoRev,\n    inviteCode,\n    deactivated,\n    refreshJwt,\n  }: {\n    did: string\n    handle: string\n    email?: string\n    password?: string\n    repoCid: CID\n    repoRev: string\n    inviteCode?: string\n    deactivated?: boolean\n    refreshJwt?: string\n  }) {\n    if (password && password.length > scrypt.NEW_PASSWORD_MAX_LENGTH) {\n      throw new InvalidRequestError('Password too long')\n    }\n\n    const passwordScrypt = password\n      ? await scrypt.genSaltAndHash(password)\n      : undefined\n\n    const now = new Date().toISOString()\n    await this.db.transaction(async (dbTxn) => {\n      if (inviteCode) {\n        await invite.ensureInviteIsAvailable(dbTxn, inviteCode)\n      }\n      await Promise.all([\n        account.registerActor(dbTxn, { did, handle, deactivated }),\n        email && passwordScrypt\n          ? account.registerAccount(dbTxn, { did, email, passwordScrypt })\n          : Promise.resolve(),\n        invite.recordInviteUse(dbTxn, {\n          did,\n          inviteCode,\n          now,\n        }),\n        refreshJwt &&\n          auth.storeRefreshToken(\n            dbTxn,\n            auth.decodeRefreshToken(refreshJwt),\n            null,\n          ),\n        repo.updateRoot(dbTxn, did, repoCid, repoRev),\n      ])\n    })\n  }\n\n  async createAccountAndSession(opts: {\n    did: string\n    handle: string\n    email?: string\n    password?: string\n    repoCid: CID\n    repoRev: string\n    inviteCode?: string\n    deactivated?: boolean\n  }) {\n    const { accessJwt, refreshJwt } = await auth.createTokens({\n      did: opts.did,\n      jwtKey: this.jwtKey,\n      serviceDid: this.serviceDid,\n      scope: AuthScope.Access,\n    })\n\n    await this.createAccount({ ...opts, refreshJwt })\n\n    return { accessJwt, refreshJwt }\n  }\n\n  // @NOTE should always be paired with a sequenceHandle().\n  // the token output from this method should be passed to sequenceHandle().\n  async updateHandle(did: string, handle: string) {\n    return account.updateHandle(this.db, did, handle)\n  }\n\n  async deleteAccount(did: string) {\n    return account.deleteAccount(this.db, did)\n  }\n\n  async takedownAccount(did: string, takedown: StatusAttr) {\n    await this.db.transaction(async (dbTxn) =>\n      Promise.all([\n        account.updateAccountTakedownStatus(dbTxn, did, takedown),\n        auth.revokeRefreshTokensByDid(dbTxn, did),\n        token.removeByDidQB(dbTxn, did).execute(),\n      ]),\n    )\n  }\n\n  async getAccountAdminStatus(did: string) {\n    return account.getAccountAdminStatus(this.db, did)\n  }\n\n  async updateRepoRoot(did: string, cid: CID, rev: string) {\n    return repo.updateRoot(this.db, did, cid, rev)\n  }\n\n  async deactivateAccount(did: string, deleteAfter: string | null) {\n    return account.deactivateAccount(this.db, did, deleteAfter)\n  }\n\n  async activateAccount(did: string) {\n    return account.activateAccount(this.db, did)\n  }\n\n  // Auth\n  // ----------\n\n  async createSession(\n    did: string,\n    appPassword: password.AppPassDescript | null,\n    isSoftDeleted = false,\n  ) {\n    const { accessJwt, refreshJwt } = await auth.createTokens({\n      did,\n      jwtKey: this.jwtKey,\n      serviceDid: this.serviceDid,\n      scope: auth.formatScope(appPassword, isSoftDeleted),\n    })\n    // For soft deleted accounts don't store refresh token so that it can't be rotated.\n    if (!isSoftDeleted) {\n      const refreshPayload = auth.decodeRefreshToken(refreshJwt)\n      await auth.storeRefreshToken(this.db, refreshPayload, appPassword)\n    }\n    return { accessJwt, refreshJwt }\n  }\n\n  async rotateRefreshToken(id: string) {\n    const token = await auth.getRefreshToken(this.db, id)\n    if (!token) return null\n\n    const now = new Date()\n\n    // take the chance to tidy all of a user's expired tokens\n    // does not need to be transactional since this is just best-effort\n    await auth.deleteExpiredRefreshTokens(this.db, token.did, now.toISOString())\n\n    // Shorten the refresh token lifespan down from its\n    // original expiration time to its revocation grace period.\n    const prevExpiresAt = new Date(token.expiresAt)\n    const REFRESH_GRACE_MS = 2 * HOUR\n    const graceExpiresAt = new Date(now.getTime() + REFRESH_GRACE_MS)\n\n    const expiresAt =\n      graceExpiresAt < prevExpiresAt ? graceExpiresAt : prevExpiresAt\n\n    if (expiresAt <= now) {\n      return null\n    }\n\n    // Determine the next refresh token id: upon refresh token\n    // reuse you always receive a refresh token with the same id.\n    const nextId = token.nextId ?? auth.getRefreshTokenId()\n\n    const { accessJwt, refreshJwt } = await auth.createTokens({\n      did: token.did,\n      jwtKey: this.jwtKey,\n      serviceDid: this.serviceDid,\n      scope: auth.formatScope(token.appPassword),\n      jti: nextId,\n    })\n\n    const refreshPayload = auth.decodeRefreshToken(refreshJwt)\n    try {\n      await this.db.transaction((dbTxn) =>\n        Promise.all([\n          auth.addRefreshGracePeriod(dbTxn, {\n            id,\n            expiresAt: expiresAt.toISOString(),\n            nextId,\n          }),\n          auth.storeRefreshToken(dbTxn, refreshPayload, token.appPassword),\n        ]),\n      )\n    } catch (err) {\n      if (err instanceof auth.ConcurrentRefreshError) {\n        return this.rotateRefreshToken(id)\n      }\n      throw err\n    }\n    return { accessJwt, refreshJwt }\n  }\n\n  async revokeRefreshToken(id: string) {\n    return auth.revokeRefreshToken(this.db, id)\n  }\n\n  // Login\n  // ----------\n\n  async login({\n    identifier,\n    password,\n  }: {\n    identifier: string\n    password: string\n  }): Promise<{\n    user: ActorAccount\n    appPassword: password.AppPassDescript | null\n    isSoftDeleted: boolean\n  }> {\n    const start = Date.now()\n    try {\n      const identifierNormalized = identifier.toLowerCase()\n\n      const user = identifierNormalized.includes('@')\n        ? await this.getAccountByEmail(identifierNormalized, {\n            includeDeactivated: true,\n            includeTakenDown: true,\n          })\n        : await this.getAccount(identifierNormalized, {\n            includeDeactivated: true,\n            includeTakenDown: true,\n          })\n\n      if (!user) {\n        throw new AuthRequiredError('Invalid identifier or password')\n      }\n      const isSoftDeleted = softDeleted(user)\n\n      let appPassword: password.AppPassDescript | null = null\n      const validAccountPass = await this.verifyAccountPassword(\n        user.did,\n        password,\n      )\n      if (!validAccountPass) {\n        // takendown/suspended accounts cannot login with app password\n        if (isSoftDeleted) {\n          throw new AuthRequiredError('Invalid identifier or password')\n        }\n        appPassword = await this.verifyAppPassword(user.did, password)\n        if (appPassword === null) {\n          throw new AuthRequiredError('Invalid identifier or password')\n        }\n      }\n\n      return { user, appPassword, isSoftDeleted }\n    } finally {\n      // Mitigate timing attacks\n      await wait(350 - (Date.now() - start))\n    }\n  }\n\n  // Passwords\n  // ----------\n\n  async createAppPassword(did: string, name: string, privileged: boolean) {\n    return password.createAppPassword(this.db, did, name, privileged)\n  }\n\n  async listAppPasswords(did: string) {\n    return password.listAppPasswords(this.db, did)\n  }\n\n  async verifyAccountPassword(\n    did: string,\n    passwordStr: string,\n  ): Promise<boolean> {\n    return password.verifyAccountPassword(this.db, did, passwordStr)\n  }\n\n  async verifyAppPassword(\n    did: string,\n    passwordStr: string,\n  ): Promise<password.AppPassDescript | null> {\n    return password.verifyAppPassword(this.db, did, passwordStr)\n  }\n\n  async revokeAppPassword(did: string, name: string) {\n    await this.db.transaction(async (dbTxn) =>\n      Promise.all([\n        password.deleteAppPassword(dbTxn, did, name),\n        auth.revokeAppPasswordRefreshToken(dbTxn, did, name),\n      ]),\n    )\n  }\n\n  // Invites\n  // ----------\n\n  async ensureInviteIsAvailable(code: string) {\n    return invite.ensureInviteIsAvailable(this.db, code)\n  }\n\n  async createInviteCodes(\n    toCreate: { account: string; codes: string[] }[],\n    useCount: number,\n  ) {\n    return invite.createInviteCodes(this.db, toCreate, useCount)\n  }\n\n  async createAccountInviteCodes(\n    forAccount: string,\n    codes: string[],\n    expectedTotal: number,\n    disabled: 0 | 1,\n  ) {\n    return invite.createAccountInviteCodes(\n      this.db,\n      forAccount,\n      codes,\n      expectedTotal,\n      disabled,\n    )\n  }\n\n  async getAccountInvitesCodes(did: string) {\n    const inviteCodes = await invite.getAccountsInviteCodes(this.db, [did])\n    return inviteCodes.get(did) ?? []\n  }\n\n  async getAccountsInvitesCodes(dids: string[]) {\n    return invite.getAccountsInviteCodes(this.db, dids)\n  }\n\n  async getInvitedByForAccounts(dids: string[]) {\n    return invite.getInvitedByForAccounts(this.db, dids)\n  }\n\n  async getInviteCodesUses(codes: string[]) {\n    return invite.getInviteCodesUses(this.db, codes)\n  }\n\n  async setAccountInvitesDisabled(did: string, disabled: boolean) {\n    return invite.setAccountInvitesDisabled(this.db, did, disabled)\n  }\n\n  async disableInviteCodes(opts: { codes: string[]; accounts: string[] }) {\n    return invite.disableInviteCodes(this.db, opts)\n  }\n\n  // Email Tokens\n  // ----------\n\n  async createEmailToken(did: string, purpose: EmailTokenPurpose) {\n    return emailToken.createEmailToken(this.db, did, purpose)\n  }\n\n  async assertValidEmailToken(\n    did: string,\n    purpose: EmailTokenPurpose,\n    token: string,\n  ) {\n    return emailToken.assertValidToken(this.db, did, purpose, token)\n  }\n\n  async assertValidEmailTokenAndCleanup(\n    did: string,\n    purpose: EmailTokenPurpose,\n    token: string,\n  ) {\n    await emailToken.assertValidToken(this.db, did, purpose, token)\n    await emailToken.deleteEmailToken(this.db, did, purpose)\n  }\n\n  async confirmEmail(opts: { did: string; token: string }) {\n    const { did, token } = opts\n    await emailToken.assertValidToken(this.db, did, 'confirm_email', token)\n    const now = new Date().toISOString()\n    await this.db.transaction((dbTxn) =>\n      Promise.all([\n        emailToken.deleteEmailToken(dbTxn, did, 'confirm_email'),\n        account.setEmailConfirmedAt(dbTxn, did, now),\n      ]),\n    )\n  }\n\n  async updateEmail(opts: { did: string; email: string }) {\n    const { did, email } = opts\n    await this.db.transaction((dbTxn) =>\n      Promise.all([\n        account.updateEmail(dbTxn, did, email),\n        emailToken.deleteAllEmailTokens(dbTxn, did),\n      ]),\n    )\n  }\n\n  async resetPassword(opts: { password: string; token: string }) {\n    const did = await emailToken.assertValidTokenAndFindDid(\n      this.db,\n      'reset_password',\n      opts.token,\n    )\n    await this.updateAccountPassword({ did, password: opts.password })\n\n    return did\n  }\n\n  async updateAccountPassword(opts: { did: string; password: string }) {\n    const { did } = opts\n    const passwordScrypt = await scrypt.genSaltAndHash(opts.password)\n    await this.db.transaction(async (dbTxn) =>\n      Promise.all([\n        password.updateUserPassword(dbTxn, { did, passwordScrypt }),\n        emailToken.deleteEmailToken(dbTxn, did, 'reset_password'),\n        auth.revokeRefreshTokensByDid(dbTxn, did),\n      ]),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/index.ts",
    "content": "import { Database, Migrator } from '../../db'\nimport migrations from './migrations'\nimport { DatabaseSchema } from './schema'\n\nexport * from './schema'\n\nexport type AccountDb = Database<DatabaseSchema>\n\nexport const getDb = (\n  location: string,\n  disableWalAutoCheckpoint = false,\n): AccountDb => {\n  const pragmas: Record<string, string> = disableWalAutoCheckpoint\n    ? { wal_autocheckpoint: '0' }\n    : {}\n  return Database.sqlite(location, { pragmas })\n}\n\nexport const getMigrator = (db: AccountDb) => {\n  return new Migrator(db.db, migrations)\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/001-init.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('app_password')\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('name', 'varchar', (col) => col.notNull())\n    .addColumn('passwordScrypt', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('app_password_pkey', ['did', 'name'])\n    .execute()\n\n  await db.schema\n    .createTable('invite_code')\n    .addColumn('code', 'varchar', (col) => col.primaryKey())\n    .addColumn('availableUses', 'integer', (col) => col.notNull())\n    .addColumn('disabled', 'int2', (col) => col.defaultTo(0))\n    .addColumn('forAccount', 'varchar', (col) => col.notNull())\n    .addColumn('createdBy', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .execute()\n  await db.schema\n    .createIndex('invite_code_for_account_idx')\n    .on('invite_code')\n    .column('forAccount')\n    .execute()\n\n  await db.schema\n    .createTable('invite_code_use')\n    .addColumn('code', 'varchar', (col) => col.notNull())\n    .addColumn('usedBy', 'varchar', (col) => col.notNull())\n    .addColumn('usedAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint(`invite_code_use_pkey`, ['code', 'usedBy'])\n    .execute()\n\n  await db.schema\n    .createTable('refresh_token')\n    .addColumn('id', 'varchar', (col) => col.primaryKey())\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('expiresAt', 'varchar', (col) => col.notNull())\n    .addColumn('nextId', 'varchar')\n    .addColumn('appPasswordName', 'varchar')\n    .execute()\n  await db.schema // Aids in refresh token cleanup\n    .createIndex('refresh_token_did_idx')\n    .on('refresh_token')\n    .column('did')\n    .execute()\n\n  await db.schema\n    .createTable('repo_root')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('rev', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .execute()\n\n  await db.schema\n    .createTable('actor')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('handle', 'varchar')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('takedownRef', 'varchar')\n    .execute()\n  await db.schema\n    .createIndex(`actor_handle_lower_idx`)\n    .unique()\n    .on('actor')\n    .expression(sql`lower(\"handle\")`)\n    .execute()\n  await db.schema\n    .createIndex('actor_cursor_idx')\n    .on('actor')\n    .columns(['createdAt', 'did'])\n    .execute()\n\n  await db.schema\n    .createTable('account')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('email', 'varchar', (col) => col.notNull())\n    .addColumn('passwordScrypt', 'varchar', (col) => col.notNull())\n    .addColumn('emailConfirmedAt', 'varchar')\n    .addColumn('invitesDisabled', 'int2', (col) => col.notNull().defaultTo(0))\n    .execute()\n  await db.schema\n    .createIndex(`account_email_lower_idx`)\n    .unique()\n    .on('account')\n    .expression(sql`lower(\"email\")`)\n    .execute()\n\n  await db.schema\n    .createTable('email_token')\n    .addColumn('purpose', 'varchar', (col) => col.notNull())\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('token', 'varchar', (col) => col.notNull())\n    .addColumn('requestedAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did'])\n    .addUniqueConstraint('email_token_purpose_token_unique', [\n      'purpose',\n      'token',\n    ])\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('email_token').execute()\n  await db.schema.dropTable('account').execute()\n  await db.schema.dropTable('actor').execute()\n  await db.schema.dropTable('repo_root').execute()\n  await db.schema.dropTable('refresh_token').execute()\n  await db.schema.dropTable('invite_code_use').execute()\n  await db.schema.dropTable('invite_code').execute()\n  await db.schema.dropTable('app_password').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/002-account-deactivation.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('actor')\n    .addColumn('deactivatedAt', 'varchar')\n    .execute()\n  await db.schema\n    .alterTable('actor')\n    .addColumn('deleteAfter', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('actor').dropColumn('deactivatedAt').execute()\n  await db.schema.alterTable('actor').dropColumn('deleteAfter').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/003-privileged-app-passwords.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .alterTable('app_password')\n    .addColumn('privileged', 'integer', (col) => col.notNull().defaultTo(0))\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('app_password').dropColumn('privileged').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/004-oauth.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('authorization_request')\n    .addColumn('id', 'varchar', (col) => col.primaryKey())\n    .addColumn('did', 'varchar')\n    .addColumn('deviceId', 'varchar')\n    .addColumn('clientId', 'varchar', (col) => col.notNull())\n    .addColumn('clientAuth', 'varchar', (col) => col.notNull())\n    .addColumn('parameters', 'varchar', (col) => col.notNull())\n    .addColumn('expiresAt', 'varchar', (col) => col.notNull())\n    .addColumn('code', 'varchar')\n    .execute()\n\n  await db.schema\n    .createIndex('authorization_request_code_idx')\n    .unique()\n    .on('authorization_request')\n    // https://github.com/kysely-org/kysely/issues/302\n    .expression(sql`code DESC) WHERE (code IS NOT NULL`)\n    .execute()\n\n  await db.schema\n    .createIndex('authorization_request_expires_at_idx')\n    .on('authorization_request')\n    .column('expiresAt')\n    .execute()\n\n  await db.schema\n    .createTable('device')\n    .addColumn('id', 'varchar', (col) => col.primaryKey())\n    .addColumn('sessionId', 'varchar', (col) => col.notNull())\n    .addColumn('userAgent', 'varchar')\n    .addColumn('ipAddress', 'varchar', (col) => col.notNull())\n    .addColumn('lastSeenAt', 'varchar', (col) => col.notNull())\n    .addUniqueConstraint('device_session_id_idx', ['sessionId'])\n    .execute()\n\n  await db.schema\n    .createTable('device_account')\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('deviceId', 'varchar', (col) => col.notNull())\n    .addColumn('authenticatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('remember', 'boolean', (col) => col.notNull())\n    .addColumn('authorizedClients', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('device_account_pk', [\n      'deviceId', // first because this table will be joined from the \"device\" table\n      'did',\n    ])\n    .addForeignKeyConstraint(\n      'device_account_device_id_fk',\n      ['deviceId'],\n      'device',\n      ['id'],\n      (qb) => qb.onDelete('cascade').onUpdate('cascade'),\n    )\n    .execute()\n\n  await db.schema\n    .createTable('token')\n    .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('tokenId', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('expiresAt', 'varchar', (col) => col.notNull())\n    .addColumn('clientId', 'varchar', (col) => col.notNull())\n    .addColumn('clientAuth', 'varchar', (col) => col.notNull())\n    .addColumn('deviceId', 'varchar')\n    .addColumn('parameters', 'varchar', (col) => col.notNull())\n    .addColumn('details', 'varchar')\n    .addColumn('code', 'varchar')\n    .addColumn('currentRefreshToken', 'varchar')\n    .addUniqueConstraint('token_current_refresh_token_unique_idx', [\n      'currentRefreshToken',\n    ])\n    .addUniqueConstraint('token_id_unique_idx', ['tokenId'])\n    .execute()\n\n  await db.schema\n    .createIndex('token_did_idx')\n    .on('token')\n    .column('did')\n    .execute()\n\n  await db.schema\n    .createIndex('token_code_idx')\n    .unique()\n    .on('token')\n    // https://github.com/kysely-org/kysely/issues/302\n    .expression(sql`code DESC) WHERE (code IS NOT NULL`)\n    .execute()\n\n  await db.schema\n    .createTable('used_refresh_token')\n    .addColumn('refreshToken', 'varchar', (col) => col.primaryKey())\n    .addColumn('tokenId', 'integer', (col) => col.notNull())\n    .addForeignKeyConstraint(\n      'used_refresh_token_fk',\n      ['tokenId'],\n      'token',\n      ['id'],\n      // uses \"used_refresh_token_id_idx\" index (when cascading)\n      (qb) => qb.onDelete('cascade').onUpdate('cascade'),\n    )\n    .execute()\n\n  await db.schema\n    .createIndex('used_refresh_token_id_idx')\n    .on('used_refresh_token')\n    .column('tokenId')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('used_refresh_token').execute()\n  await db.schema.dropTable('token').execute()\n  await db.schema.dropTable('device_account').execute()\n  await db.schema.dropTable('device').execute()\n  await db.schema.dropTable('authorization_request').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/005-oauth-account-management.ts",
    "content": "import { Kysely } from 'kysely'\nimport { HOUR } from '@atproto/common'\nimport { ClientId, DeviceId } from '@atproto/oauth-provider'\nimport { DateISO, JsonEncoded, toDateISO } from '../../../db'\n\n// @NOTE this migration has been updated to be idempotent through\n// the insertInto('account_device') step. this allows users to roll\n// forward if the migration partially succeeded on first run, failing\n// on a fk constraint during the insertInto('account_device') step.\n// this previously occurred under the following conditions:\n//  a. a user was deleted\n//  b. this user used oauth functionality with \"remember me\" selected.\n\nexport async function up(\n  db: Kysely<{\n    account: {\n      did: string\n    }\n    device_account: {\n      did: string\n      deviceId: DeviceId\n\n      remember: 0 | 1\n      authenticatedAt: string\n      authorizedClients: JsonEncoded<ClientId[]>\n    }\n    account_device: {\n      did: string\n      deviceId: DeviceId\n\n      createdAt: DateISO\n      updatedAt: DateISO\n    }\n  }>,\n): Promise<void> {\n  // Security: Delete any leftover device accounts that are not remembered\n  // @NOTE idempotent, see note at top of migration.\n  await db\n    .deleteFrom('device_account')\n    .where('remember', '=', 0)\n    .where('authenticatedAt', '<', toDateISO(new Date(Date.now() - HOUR)))\n    .execute()\n\n  // replaces \"device_account\"\n  // @NOTE idempotent from ifNotExists(), see note at top of migration.\n  await db.schema\n    .createTable('account_device')\n    .ifNotExists()\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('deviceId', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('account_device_pk', [\n      'deviceId', // first because this table will be joined from the \"device\" table\n      'did',\n    ])\n    .addForeignKeyConstraint(\n      'account_device_did_fk',\n      ['did'],\n      'account',\n      ['did'],\n      // cascade on delete, future-proofing on update (fk can't be altered)\n      (qb) => qb.onDelete('cascade').onUpdate('cascade'),\n    )\n    .addForeignKeyConstraint(\n      'account_device_device_id_fk',\n      ['deviceId'],\n      'device',\n      ['id'],\n      // cascade on delete, future-proofing on update (fk can't be altered)\n      (qb) => qb.onDelete('cascade').onUpdate('cascade'),\n    )\n    .execute()\n\n  // Migrate \"device_account\" to \"account_device\"\n  // @NOTE idempotent from onConflict(): see note at top of migration.\n  await db\n    .insertInto('account_device')\n    .columns(['did', 'deviceId', 'createdAt', 'updatedAt'])\n    .expression(\n      db\n        .selectFrom('device_account')\n        .select('did')\n        .select('deviceId')\n        .select('authenticatedAt as createdAt') // Best we can do\n        .select('authenticatedAt as updatedAt')\n        .where('remember', '=', 1)\n        .whereExists((qb) =>\n          // device_account does not have fkey on account.did,\n          // so we satisfy account_device_did_fk with this condition.\n          qb\n            .selectFrom('account')\n            .selectAll()\n            .whereRef('account.did', '=', 'device_account.did'),\n        ),\n    )\n    .onConflict((oc) => oc.doNothing())\n    .execute()\n\n  // @NOTE No need to create an index on \"deviceId\" for \"account_device\" because\n  // it is the first column in the primary key constraint\n\n  await db.schema\n    .createIndex('account_device_did_idx')\n    .on('account_device')\n    .column('did')\n    .execute()\n\n  await db.schema\n    .createTable('authorized_client')\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('clientId', 'varchar', (col) => col.notNull())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('data', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('authorized_client_pk', ['did', 'clientId'])\n    .addForeignKeyConstraint(\n      'authorized_client_did_fk',\n      ['did'],\n      'account',\n      ['did'],\n      // cascade on delete, future-proofing on update (fk can't be altered)\n      (qb) => qb.onDelete('cascade').onUpdate('cascade'),\n    )\n    .execute()\n\n  // We don't migrate the \"device_account\" authorized clients. Users will need\n  // to reauthorize the client during the next oauth flow (minor inconvenience\n  // for authenticated clients users).\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('authorized_client').execute()\n  await db.schema.dropTable('account_device').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/006-oauth-permission-sets.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema.alterTable('token').addColumn('scope', 'varchar').execute()\n\n  await db.schema\n    .createTable('lexicon')\n    .addColumn('nsid', 'varchar', (col) => col.primaryKey())\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('updatedAt', 'varchar', (col) => col.notNull())\n    .addColumn('lastSucceededAt', 'varchar')\n    .addColumn('uri', 'varchar')\n    .addColumn('lexicon', 'varchar')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('lexicon').execute()\n  await db.schema.alterTable('token').dropColumn('scope').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/007-lexicon-failures-index.ts",
    "content": "import { Kysely, sql } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createIndex('lexicon_failures_idx')\n    .on('lexicon')\n    // https://github.com/kysely-org/kysely/issues/302\n    .expression(sql`\"updatedAt\" DESC) WHERE (\"lexicon\" is NULL`)\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropIndex('lexicon_failures_idx').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/migrations/index.ts",
    "content": "import * as mig001 from './001-init'\nimport * as mig002 from './002-account-deactivation'\nimport * as mig003 from './003-privileged-app-passwords'\nimport * as mig004 from './004-oauth'\nimport * as mig005 from './005-oauth-account-management'\nimport * as mig006 from './006-oauth-permission-sets'\nimport * as mig007 from './007-lexicon-failures-index'\n\nexport default {\n  '001': mig001,\n  '002': mig002,\n  '003': mig003,\n  '004': mig004,\n  '005': mig005,\n  '006': mig006,\n  '007': mig007,\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/account-device.ts",
    "content": "import { DeviceId } from '@atproto/oauth-provider'\nimport { DateISO } from '../../../db'\n\nexport interface AccountDevice {\n  did: string\n  deviceId: DeviceId\n\n  createdAt: DateISO\n  updatedAt: DateISO\n}\n\nexport const tableName = 'account_device'\n\nexport type PartialDB = { [tableName]: AccountDevice }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/account.ts",
    "content": "import { Generated, Selectable } from 'kysely'\n\nexport interface Account {\n  did: string\n  email: string\n  passwordScrypt: string\n  emailConfirmedAt: string | null\n  invitesDisabled: Generated<0 | 1>\n}\n\nexport type AccountEntry = Selectable<Account>\n\nexport const tableName = 'account'\n\nexport type PartialDB = { [tableName]: Account }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/actor.ts",
    "content": "import { Selectable } from 'kysely'\n\nexport interface Actor {\n  did: string\n  handle: string | null\n  createdAt: string\n  takedownRef: string | null\n  deactivatedAt: string | null\n  deleteAfter: string | null\n}\n\nexport type ActorEntry = Selectable<Actor>\n\nexport const tableName = 'actor'\n\nexport type PartialDB = { [tableName]: Actor }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/app-password.ts",
    "content": "export interface AppPassword {\n  did: string\n  name: string\n  passwordScrypt: string\n  createdAt: string\n  privileged: 0 | 1\n}\n\nexport const tableName = 'app_password'\n\nexport type PartialDB = { [tableName]: AppPassword }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/authorization-request.ts",
    "content": "import { Selectable } from 'kysely'\nimport {\n  ClientAuth,\n  ClientAuthLegacy,\n  Code,\n  DeviceId,\n  OAuthAuthorizationRequestParameters,\n  OAuthClientId,\n  RequestId,\n} from '@atproto/oauth-provider'\nimport { DateISO, JsonEncoded } from '../../../db'\n\nexport interface AuthorizationRequest {\n  id: RequestId\n  did: string | null\n  deviceId: DeviceId | null\n\n  clientId: OAuthClientId\n  clientAuth: JsonEncoded<null | ClientAuth | ClientAuthLegacy>\n  parameters: JsonEncoded<OAuthAuthorizationRequestParameters>\n  expiresAt: DateISO\n  code: Code | null\n}\n\nexport type AuthorizationRequestEntry = Selectable<AuthorizationRequest>\n\nexport const tableName = 'authorization_request'\n\nexport type PartialDB = { [tableName]: AuthorizationRequest }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/authorized-client.ts",
    "content": "import { Selectable } from 'kysely'\nimport { AuthorizedClientData, OAuthClientId } from '@atproto/oauth-provider'\nimport { DateISO, JsonEncoded } from '../../../db'\n\nexport interface AuthorizedClient {\n  did: string\n  clientId: OAuthClientId\n\n  createdAt: DateISO\n  updatedAt: DateISO\n\n  data: JsonEncoded<AuthorizedClientData>\n}\n\nexport type AuthorizedClientEntry = Selectable<AuthorizedClient>\n\nexport const tableName = 'authorized_client'\n\nexport type PartialDB = { [tableName]: AuthorizedClient }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/device.ts",
    "content": "import { Selectable } from 'kysely'\nimport { DeviceId, SessionId } from '@atproto/oauth-provider'\nimport { DateISO } from '../../../db'\n\nexport interface Device {\n  id: DeviceId\n  sessionId: SessionId\n\n  userAgent: string | null\n  ipAddress: string\n  lastSeenAt: DateISO\n}\n\nexport type DeviceEntry = Selectable<Device>\n\nexport const tableName = 'device'\n\nexport type PartialDB = { [tableName]: Device }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/email-token.ts",
    "content": "export type EmailTokenPurpose =\n  | 'confirm_email'\n  | 'update_email'\n  | 'reset_password'\n  | 'delete_account'\n  | 'plc_operation'\n\nexport interface EmailToken {\n  purpose: EmailTokenPurpose\n  did: string\n  token: string\n  requestedAt: string\n}\n\nexport const tableName = 'email_token'\n\nexport type PartialDB = { [tableName]: EmailToken }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/index.ts",
    "content": "import * as account from './account'\nimport * as accountDevice from './account-device'\nimport * as actor from './actor'\nimport * as appPassword from './app-password'\nimport * as oauthRequest from './authorization-request'\nimport * as authorizedClient from './authorized-client'\nimport * as device from './device'\nimport * as emailToken from './email-token'\nimport * as inviteCode from './invite-code'\nimport * as lexicon from './lexicon'\nimport * as refreshToken from './refresh-token'\nimport * as repoRoot from './repo-root'\nimport * as token from './token'\nimport * as usedRefreshToken from './used-refresh-token'\n\nexport type DatabaseSchema = actor.PartialDB &\n  account.PartialDB &\n  accountDevice.PartialDB &\n  authorizedClient.PartialDB &\n  device.PartialDB &\n  oauthRequest.PartialDB &\n  token.PartialDB &\n  usedRefreshToken.PartialDB &\n  refreshToken.PartialDB &\n  appPassword.PartialDB &\n  repoRoot.PartialDB &\n  inviteCode.PartialDB &\n  lexicon.PartialDB &\n  emailToken.PartialDB\n\nexport type { Actor, ActorEntry } from './actor'\nexport type { Account, AccountEntry } from './account'\nexport type { AccountDevice } from './account-device'\nexport type { Device } from './device'\nexport type { AuthorizationRequest } from './authorization-request'\nexport type { Token } from './token'\nexport type { Lexicon } from './lexicon'\nexport type { UsedRefreshToken } from './used-refresh-token'\nexport type { RepoRoot } from './repo-root'\nexport type { RefreshToken } from './refresh-token'\nexport type { AppPassword } from './app-password'\nexport type { InviteCode, InviteCodeUse } from './invite-code'\nexport type { EmailToken, EmailTokenPurpose } from './email-token'\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/invite-code.ts",
    "content": "export interface InviteCode {\n  code: string\n  availableUses: number\n  disabled: 0 | 1\n  forAccount: string\n  createdBy: string\n  createdAt: string\n}\n\nexport interface InviteCodeUse {\n  code: string\n  usedBy: string\n  usedAt: string\n}\n\nexport const tableName = 'invite_code'\nexport const supportingTableName = 'invite_code_use'\n\nexport type PartialDB = {\n  [tableName]: InviteCode\n  [supportingTableName]: InviteCodeUse\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/lexicon.ts",
    "content": "import type { LexiconDocument } from '@atproto/oauth-provider'\nimport { DateISO, JsonEncoded } from '../../../db/cast'\n\nexport interface Lexicon {\n  nsid: string\n  createdAt: DateISO\n  updatedAt: DateISO\n  lastSucceededAt: null | DateISO\n  uri: null | string\n  lexicon: null | JsonEncoded<LexiconDocument>\n}\n\nexport const tableName = 'lexicon'\n\nexport type PartialDB = { [tableName]: Lexicon }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/refresh-token.ts",
    "content": "export interface RefreshToken {\n  id: string\n  did: string\n  expiresAt: string\n  appPasswordName: string | null\n  nextId: string | null\n}\n\nexport const tableName = 'refresh_token'\n\nexport type PartialDB = { [tableName]: RefreshToken }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/repo-root.ts",
    "content": "export interface RepoRoot {\n  did: string\n  cid: string\n  rev: string\n  indexedAt: string\n}\n\nexport const tableName = 'repo_root'\n\nexport type PartialDB = { [tableName]: RepoRoot }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/token.ts",
    "content": "import { Generated, Selectable } from 'kysely'\nimport {\n  ClientAuth,\n  ClientAuthLegacy,\n  Code,\n  DeviceId,\n  OAuthAuthorizationDetails,\n  OAuthAuthorizationRequestParameters,\n  OAuthClientId,\n  RefreshToken,\n  Sub,\n  TokenId,\n} from '@atproto/oauth-provider'\nimport { DateISO, JsonEncoded } from '../../../db/cast'\n\nexport interface Token {\n  id: Generated<number>\n  did: Sub\n\n  tokenId: TokenId\n  createdAt: DateISO\n  updatedAt: DateISO\n  expiresAt: DateISO\n  clientId: OAuthClientId\n  clientAuth: JsonEncoded<ClientAuth | ClientAuthLegacy>\n  deviceId: DeviceId | null\n  parameters: JsonEncoded<OAuthAuthorizationRequestParameters>\n  details: JsonEncoded<OAuthAuthorizationDetails> | null\n  code: Code | null\n  currentRefreshToken: RefreshToken | null\n  scope: string | null\n}\n\nexport type TokenEntry = Selectable<Token>\n\nexport const tableName = 'token'\n\nexport type PartialDB = { [tableName]: Token }\n"
  },
  {
    "path": "packages/pds/src/account-manager/db/schema/used-refresh-token.ts",
    "content": "import { Selectable } from 'kysely'\nimport { RefreshToken } from '@atproto/oauth-provider'\n\nexport interface UsedRefreshToken {\n  tokenId: number\n  refreshToken: RefreshToken\n}\n\nexport type UsedRefreshTokenEntry = Selectable<UsedRefreshToken>\n\nexport const tableName = 'used_refresh_token'\n\nexport type PartialDB = { [tableName]: UsedRefreshToken }\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/account-device.ts",
    "content": "import assert from 'node:assert'\nimport { DeviceId } from '@atproto/oauth-provider'\nimport { toDateISO } from '../../db'\nimport { AccountDb } from '../db'\nimport { selectAccountQB } from './account'\n\nexport function upsertQB(db: AccountDb, deviceId: DeviceId, did: string) {\n  const now = new Date()\n\n  return db.db\n    .insertInto('account_device')\n    .values({\n      did,\n      deviceId,\n      createdAt: toDateISO(now),\n      updatedAt: toDateISO(now),\n    })\n    .onConflict((oc) =>\n      // uses pk\n      oc.columns(['deviceId', 'did']).doUpdateSet({\n        updatedAt: toDateISO(now),\n      }),\n    )\n}\n\nexport function selectQB(\n  db: AccountDb,\n  filter: {\n    sub?: string\n    deviceId?: DeviceId\n  },\n) {\n  assert(\n    filter.sub != null || filter.deviceId != null,\n    'Either sub or deviceId must be provided',\n  )\n\n  return (\n    selectAccountQB(db, { includeDeactivated: true })\n      // note: query planner should use \"account_device_pk\" index\n      .innerJoin('account_device', 'account_device.did', 'actor.did')\n      .select([\n        'account_device.deviceId',\n        'account_device.createdAt as adCreatedAt',\n        'account_device.updatedAt as adUpdatedAt',\n      ])\n      .innerJoin('device', 'device.id', 'account_device.deviceId')\n      .select([\n        'device.sessionId',\n        'device.userAgent',\n        'device.ipAddress',\n        'device.lastSeenAt',\n      ])\n      .if(filter.sub != null, (qb) => qb.where('actor.did', '=', filter.sub!))\n      .if(filter.deviceId != null, (qb) =>\n        qb.where('account_device.deviceId', '=', filter.deviceId!),\n      )\n  )\n}\n\nexport function removeQB(db: AccountDb, deviceId: DeviceId, did: string) {\n  return db.db\n    .deleteFrom('account_device')\n    .where('deviceId', '=', deviceId)\n    .where('did', '=', did)\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/account.ts",
    "content": "import { DAY } from '@atproto/common'\nimport { isErrUniqueViolation, notSoftDeletedClause } from '../../db'\nimport { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'\nimport { AccountDb, ActorEntry } from '../db'\n\nexport class UserAlreadyExistsError extends Error {}\n\nexport type ActorAccount = ActorEntry & {\n  email: string | null\n  emailConfirmedAt: string | null\n  invitesDisabled: 0 | 1 | null\n}\n\nexport type AvailabilityFlags = {\n  includeTakenDown?: boolean\n  includeDeactivated?: boolean\n}\n\nexport enum AccountStatus {\n  Active = 'active',\n  Takendown = 'takendown',\n  Suspended = 'suspended',\n  Deleted = 'deleted',\n  Deactivated = 'deactivated',\n}\n\nexport const selectAccountQB = (db: AccountDb, flags?: AvailabilityFlags) => {\n  const { includeTakenDown = false, includeDeactivated = false } = flags ?? {}\n  const { ref } = db.db.dynamic\n  return db.db\n    .selectFrom('actor')\n    .leftJoin('account', 'actor.did', 'account.did')\n    .if(!includeTakenDown, (qb) => qb.where(notSoftDeletedClause(ref('actor'))))\n    .if(!includeDeactivated, (qb) =>\n      qb.where('actor.deactivatedAt', 'is', null),\n    )\n    .select([\n      'actor.did',\n      'actor.handle',\n      'actor.createdAt',\n      'actor.takedownRef',\n      'actor.deactivatedAt',\n      'actor.deleteAfter',\n      'account.email',\n      'account.emailConfirmedAt',\n      'account.invitesDisabled',\n    ])\n}\n\nexport const getAccount = async (\n  db: AccountDb,\n  handleOrDid: string,\n  flags?: AvailabilityFlags,\n): Promise<ActorAccount | null> => {\n  const found = await selectAccountQB(db, flags)\n    .where((qb) => {\n      if (handleOrDid.startsWith('did:')) {\n        return qb.where('actor.did', '=', handleOrDid)\n      } else {\n        return qb.where('actor.handle', '=', handleOrDid)\n      }\n    })\n    .executeTakeFirst()\n  return found || null\n}\n\nexport const getAccounts = async (\n  db: AccountDb,\n  dids: string[],\n  flags?: AvailabilityFlags,\n): Promise<Map<string, ActorAccount>> => {\n  const results = new Map<string, ActorAccount>()\n\n  if (!dids.length) {\n    return results\n  }\n\n  const accounts = await selectAccountQB(db, flags)\n    .where('actor.did', 'in', dids)\n    .execute()\n\n  accounts.forEach((account) => {\n    results.set(account.did, account)\n  })\n\n  return results\n}\n\nexport const getAccountByEmail = async (\n  db: AccountDb,\n  email: string,\n  flags?: AvailabilityFlags,\n): Promise<ActorAccount | null> => {\n  const found = await selectAccountQB(db, flags)\n    .where('email', '=', email.toLowerCase())\n    .executeTakeFirst()\n  return found || null\n}\n\nexport const registerActor = async (\n  db: AccountDb,\n  opts: {\n    did: string\n    handle: string\n    deactivated?: boolean\n  },\n) => {\n  const { did, handle, deactivated } = opts\n  const now = Date.now()\n  const createdAt = new Date(now).toISOString()\n  const [registered] = await db.executeWithRetry(\n    db.db\n      .insertInto('actor')\n      .values({\n        did,\n        handle,\n        createdAt,\n        deactivatedAt: deactivated ? createdAt : null,\n        deleteAfter: deactivated ? new Date(now + 3 * DAY).toISOString() : null,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .returning('did'),\n  )\n  if (!registered) {\n    throw new UserAlreadyExistsError()\n  }\n}\n\nexport const registerAccount = async (\n  db: AccountDb,\n  opts: {\n    did: string\n    email: string\n    passwordScrypt: string\n  },\n) => {\n  const { did, email, passwordScrypt } = opts\n  const [registered] = await db.executeWithRetry(\n    db.db\n      .insertInto('account')\n      .values({\n        did,\n        email: email.toLowerCase(),\n        passwordScrypt,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .returning('did'),\n  )\n  if (!registered) {\n    throw new UserAlreadyExistsError()\n  }\n}\n\nexport const deleteAccount = async (\n  db: AccountDb,\n  did: string,\n): Promise<void> => {\n  // Not done in transaction because it would be too long, prone to contention.\n  // Also, this can safely be run multiple times if it fails.\n  await db.executeWithRetry(\n    db.db.deleteFrom('repo_root').where('did', '=', did),\n  )\n  await db.executeWithRetry(\n    db.db.deleteFrom('email_token').where('did', '=', did),\n  )\n  await db.executeWithRetry(\n    db.db.deleteFrom('refresh_token').where('did', '=', did),\n  )\n  await db.executeWithRetry(\n    db.db.deleteFrom('account').where('account.did', '=', did),\n  )\n  await db.executeWithRetry(\n    db.db.deleteFrom('actor').where('actor.did', '=', did),\n  )\n}\n\nexport const updateHandle = async (\n  db: AccountDb,\n  did: string,\n  handle: string,\n) => {\n  const [res] = await db.executeWithRetry(\n    db.db\n      .updateTable('actor')\n      .set({ handle })\n      .where('did', '=', did)\n      .whereNotExists(\n        db.db.selectFrom('actor').where('handle', '=', handle).selectAll(),\n      ),\n  )\n  if (res.numUpdatedRows < 1) {\n    throw new UserAlreadyExistsError()\n  }\n}\n\nexport const updateEmail = async (\n  db: AccountDb,\n  did: string,\n  email: string,\n) => {\n  try {\n    await db.executeWithRetry(\n      db.db\n        .updateTable('account')\n        .set({\n          email: email.toLowerCase(),\n          emailConfirmedAt: null,\n        })\n        .where('did', '=', did),\n    )\n  } catch (err) {\n    if (isErrUniqueViolation(err)) {\n      throw new UserAlreadyExistsError()\n    }\n    throw err\n  }\n}\n\nexport const setEmailConfirmedAt = async (\n  db: AccountDb,\n  did: string,\n  emailConfirmedAt: string,\n) => {\n  await db.executeWithRetry(\n    db.db\n      .updateTable('account')\n      .set({ emailConfirmedAt })\n      .where('did', '=', did),\n  )\n}\n\nexport const getAccountAdminStatus = async (\n  db: AccountDb,\n  did: string,\n): Promise<{ takedown: StatusAttr; deactivated: StatusAttr } | null> => {\n  const res = await db.db\n    .selectFrom('actor')\n    .select(['takedownRef', 'deactivatedAt'])\n    .where('did', '=', did)\n    .executeTakeFirst()\n  if (!res) return null\n  const takedown = res.takedownRef\n    ? { applied: true, ref: res.takedownRef }\n    : { applied: false }\n  const deactivated = res.deactivatedAt ? { applied: true } : { applied: false }\n  return { takedown, deactivated }\n}\n\nexport const updateAccountTakedownStatus = async (\n  db: AccountDb,\n  did: string,\n  takedown: StatusAttr,\n) => {\n  const takedownRef = takedown.applied\n    ? takedown.ref ?? new Date().toISOString()\n    : null\n  await db.executeWithRetry(\n    db.db.updateTable('actor').set({ takedownRef }).where('did', '=', did),\n  )\n}\n\nexport const deactivateAccount = async (\n  db: AccountDb,\n  did: string,\n  deleteAfter: string | null,\n) => {\n  await db.executeWithRetry(\n    db.db\n      .updateTable('actor')\n      .set({\n        deactivatedAt: new Date().toISOString(),\n        deleteAfter,\n      })\n      .where('did', '=', did),\n  )\n}\n\nexport const activateAccount = async (db: AccountDb, did: string) => {\n  await db.executeWithRetry(\n    db.db\n      .updateTable('actor')\n      .set({\n        deactivatedAt: null,\n        deleteAfter: null,\n      })\n      .where('did', '=', did),\n  )\n}\n\nexport const formatAccountStatus = (\n  account: null | {\n    takedownRef: string | null\n    deactivatedAt: string | null\n  },\n) => {\n  if (!account) {\n    return { active: false, status: AccountStatus.Deleted } as const\n  } else if (account.takedownRef) {\n    return { active: false, status: AccountStatus.Takendown } as const\n  } else if (account.deactivatedAt) {\n    return { active: false, status: AccountStatus.Deactivated } as const\n  } else {\n    return { active: true, status: undefined } as const\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/auth.ts",
    "content": "import assert from 'node:assert'\nimport { KeyObject } from 'node:crypto'\nimport * as jose from 'jose'\nimport * as ui8 from 'uint8arrays'\nimport * as crypto from '@atproto/crypto'\nimport { AuthScope } from '../../auth-scope'\nimport { AccountDb } from '../db'\nimport { AppPassDescript } from './password'\n\nexport type AuthToken = {\n  scope: AuthScope\n  sub: string\n  exp: number\n}\n\nexport type RefreshToken = AuthToken & { scope: AuthScope.Refresh; jti: string }\n\nexport const createTokens = async (opts: {\n  did: string\n  jwtKey: KeyObject\n  serviceDid: string\n  scope?: AuthScope\n  jti?: string\n  expiresIn?: string | number\n}) => {\n  const { did, jwtKey, serviceDid, scope, jti, expiresIn } = opts\n  const [accessJwt, refreshJwt] = await Promise.all([\n    createAccessToken({ did, jwtKey, serviceDid, scope, expiresIn }),\n    createRefreshToken({ did, jwtKey, serviceDid, jti, expiresIn }),\n  ])\n  return { accessJwt, refreshJwt }\n}\n\nexport const createAccessToken = (opts: {\n  did: string\n  jwtKey: KeyObject\n  serviceDid: string\n  scope?: AuthScope\n  expiresIn?: string | number\n}): Promise<string> => {\n  const {\n    did,\n    jwtKey,\n    serviceDid,\n    scope = AuthScope.Access,\n    expiresIn = '120mins',\n  } = opts\n  const signer = new jose.SignJWT({ scope })\n    .setProtectedHeader({\n      typ: 'at+jwt', // https://www.rfc-editor.org/rfc/rfc9068.html\n      alg: 'HS256', // only symmetric keys supported\n    })\n    .setAudience(serviceDid)\n    .setSubject(did)\n    .setIssuedAt()\n    .setExpirationTime(expiresIn)\n  return signer.sign(jwtKey)\n}\n\nexport const createRefreshToken = (opts: {\n  did: string\n  jwtKey: KeyObject\n  serviceDid: string\n  jti?: string\n  expiresIn?: string | number\n}): Promise<string> => {\n  const {\n    did,\n    jwtKey,\n    serviceDid,\n    jti = getRefreshTokenId(),\n    expiresIn = '90days',\n  } = opts\n  const signer = new jose.SignJWT({ scope: AuthScope.Refresh })\n    .setProtectedHeader({\n      typ: 'refresh+jwt',\n      alg: 'HS256', // only symmetric keys supported\n    })\n    .setAudience(serviceDid)\n    .setSubject(did)\n    .setJti(jti)\n    .setIssuedAt()\n    .setExpirationTime(expiresIn)\n  return signer.sign(jwtKey)\n}\n\n// @NOTE unsafe for verification, should only be used w/ direct output from createRefreshToken() or createTokens()\nexport const decodeRefreshToken = (jwt: string) => {\n  const token = jose.decodeJwt(jwt)\n  assert.ok(token.scope === AuthScope.Refresh, 'not a refresh token')\n  return token as RefreshToken\n}\n\nexport const storeRefreshToken = async (\n  db: AccountDb,\n  payload: RefreshToken,\n  appPassword: AppPassDescript | null,\n) => {\n  const [result] = await db.executeWithRetry(\n    db.db\n      .insertInto('refresh_token')\n      .values({\n        id: payload.jti,\n        did: payload.sub,\n        appPasswordName: appPassword?.name,\n        expiresAt: new Date(payload.exp * 1000).toISOString(),\n      })\n      .onConflict((oc) => oc.doNothing()), // E.g. when re-granting during a refresh grace period\n  )\n  return result\n}\n\nexport const getRefreshToken = async (db: AccountDb, id: string) => {\n  const res = await db.db\n    .selectFrom('refresh_token')\n    .leftJoin('app_password', (join) =>\n      join\n        .onRef('app_password.did', '=', 'refresh_token.did')\n        .onRef('app_password.name', '=', 'refresh_token.appPasswordName'),\n    )\n    .where('id', '=', id)\n    .selectAll('refresh_token')\n    .select('app_password.privileged')\n    .executeTakeFirst()\n  if (!res) return null\n  const { did, expiresAt, appPasswordName, nextId, privileged } = res\n  return {\n    id,\n    did,\n    expiresAt,\n    nextId,\n    appPassword: appPasswordName\n      ? {\n          name: appPasswordName,\n          privileged: privileged === 1 ? true : false,\n        }\n      : null,\n  }\n}\n\nexport const deleteExpiredRefreshTokens = async (\n  db: AccountDb,\n  did: string,\n  now: string,\n) => {\n  await db.executeWithRetry(\n    db.db\n      .deleteFrom('refresh_token')\n      .where('did', '=', did)\n      .where('expiresAt', '<=', now),\n  )\n}\n\nexport const addRefreshGracePeriod = async (\n  db: AccountDb,\n  opts: {\n    id: string\n    expiresAt: string\n    nextId: string\n  },\n) => {\n  const { id, expiresAt, nextId } = opts\n  const [res] = await db.executeWithRetry(\n    db.db\n      .updateTable('refresh_token')\n      .where('id', '=', id)\n      .where((inner) =>\n        inner.where('nextId', 'is', null).orWhere('nextId', '=', nextId),\n      )\n      .set({ expiresAt, nextId })\n      .returningAll(),\n  )\n  if (!res) {\n    throw new ConcurrentRefreshError()\n  }\n}\n\nexport const revokeRefreshToken = async (db: AccountDb, id: string) => {\n  const [{ numDeletedRows }] = await db.executeWithRetry(\n    db.db.deleteFrom('refresh_token').where('id', '=', id),\n  )\n  return numDeletedRows > 0\n}\n\nexport const revokeRefreshTokensByDid = async (db: AccountDb, did: string) => {\n  const [{ numDeletedRows }] = await db.executeWithRetry(\n    db.db.deleteFrom('refresh_token').where('did', '=', did),\n  )\n  return numDeletedRows > 0\n}\n\nexport const revokeAppPasswordRefreshToken = async (\n  db: AccountDb,\n  did: string,\n  appPassName: string,\n) => {\n  const [{ numDeletedRows }] = await db.executeWithRetry(\n    db.db\n      .deleteFrom('refresh_token')\n      .where('did', '=', did)\n      .where('appPasswordName', '=', appPassName),\n  )\n\n  return numDeletedRows > 0\n}\n\nexport const getRefreshTokenId = () => {\n  return ui8.toString(crypto.randomBytes(32), 'base64')\n}\n\nexport const formatScope = (\n  appPassword: AppPassDescript | null,\n  isSoftDeleted?: boolean,\n): AuthScope => {\n  if (isSoftDeleted) return AuthScope.Takendown\n  if (!appPassword) return AuthScope.Access\n  return appPassword.privileged\n    ? AuthScope.AppPassPrivileged\n    : AuthScope.AppPass\n}\n\nexport class ConcurrentRefreshError extends Error {}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/authorization-request.ts",
    "content": "import assert from 'node:assert'\nimport { Insertable, Selectable } from 'kysely'\nimport {\n  Code,\n  FoundRequestResult,\n  RequestData,\n  RequestId,\n  UpdateRequestData,\n} from '@atproto/oauth-provider'\nimport { fromDateISO, fromJson, toDateISO, toJson } from '../../db'\nimport { AccountDb, AuthorizationRequest } from '../db'\n\nexport const rowToRequestData = (\n  row: Selectable<AuthorizationRequest>,\n): RequestData => ({\n  clientId: row.clientId,\n  clientAuth: fromJson(row.clientAuth),\n  parameters: fromJson(row.parameters),\n  expiresAt: fromDateISO(row.expiresAt),\n  deviceId: row.deviceId,\n  sub: row.did,\n  code: row.code,\n})\n\nexport const rowToFoundRequestResult = (\n  row: Selectable<AuthorizationRequest>,\n): FoundRequestResult => ({\n  requestId: row.id,\n  data: rowToRequestData(row),\n})\n\nconst requestDataToRow = (\n  id: RequestId,\n  data: RequestData,\n): Insertable<AuthorizationRequest> => ({\n  id,\n  did: data.sub,\n  deviceId: data.deviceId,\n\n  clientId: data.clientId,\n  clientAuth: toJson(data.clientAuth),\n  parameters: toJson(data.parameters),\n  expiresAt: toDateISO(data.expiresAt),\n  code: data.code,\n})\n\nexport const createQB = (db: AccountDb, id: RequestId, data: RequestData) =>\n  db.db.insertInto('authorization_request').values(requestDataToRow(id, data))\n\nexport const readQB = (db: AccountDb, id: RequestId) =>\n  db.db.selectFrom('authorization_request').where('id', '=', id).selectAll()\n\nexport const updateQB = (\n  db: AccountDb,\n  id: RequestId,\n  { code, sub, deviceId, expiresAt, parameters, ...rest }: UpdateRequestData,\n) => {\n  assert(!Object.keys(rest).length, 'Unexpected fields in UpdateRequestData')\n  return db.db\n    .updateTable('authorization_request')\n    .if(code !== undefined, (qb) => qb.set({ code }))\n    .if(sub !== undefined, (qb) => qb.set({ did: sub }))\n    .if(deviceId !== undefined, (qb) => qb.set({ deviceId }))\n    .if(expiresAt != null, (qb) => qb.set({ expiresAt: toDateISO(expiresAt!) }))\n    .if(parameters != null, (qb) => qb.set({ parameters: toJson(parameters!) }))\n    .where('id', '=', id)\n}\n\nexport const removeOldExpiredQB = (db: AccountDb, delay = 600e3) =>\n  // We allow some delay for the expiration time so that expired requests\n  // can still be returned to the OAuthProvider library for error handling.\n  db.db\n    .deleteFrom('authorization_request')\n    // uses \"authorization_request_expires_at_idx\" index\n    .where('expiresAt', '<', toDateISO(new Date(Date.now() - delay)))\n\nexport const removeByIdQB = (db: AccountDb, id: RequestId) =>\n  db.db.deleteFrom('authorization_request').where('id', '=', id)\n\nexport const consumeByCodeQB = (db: AccountDb, code: Code) =>\n  db.db\n    .deleteFrom('authorization_request')\n    // uses \"authorization_request_code_idx\" partial index (hence the null check)\n    .where('code', '=', code)\n    .where('code', 'is not', null)\n    .returningAll()\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/authorized-client.ts",
    "content": "import {\n  AuthorizedClientData,\n  AuthorizedClients,\n  ClientId,\n  Sub,\n} from '@atproto/oauth-provider'\nimport { fromJson, toDateISO, toJson } from '../../db'\nimport { AccountDb } from '../db'\n\nexport async function upsert(\n  db: AccountDb,\n  did: string,\n  clientId: ClientId,\n  data: AuthorizedClientData,\n) {\n  const now = new Date()\n\n  return db.db\n    .insertInto('authorized_client')\n    .values({\n      did,\n      clientId,\n      createdAt: toDateISO(now),\n      updatedAt: toDateISO(now),\n      data: toJson(data),\n    })\n    .onConflict((oc) =>\n      // uses \"authorized_client_pk\" idx\n      oc.columns(['did', 'clientId']).doUpdateSet({\n        updatedAt: toDateISO(now),\n        data: toJson(data),\n      }),\n    )\n    .executeTakeFirst()\n}\n\nexport async function getAuthorizedClients(\n  db: AccountDb,\n  did: string,\n): Promise<AuthorizedClients> {\n  return (await getAuthorizedClientsMulti(db, [did])).get(did)!\n}\n\nexport async function getAuthorizedClientsMulti(\n  db: AccountDb,\n  dids: Iterable<string>,\n): Promise<Map<Sub, AuthorizedClients>> {\n  // Using a Map will ensure unicity of dids (through unicity of keys)\n  const map = new Map<Sub, AuthorizedClients>(\n    Array.from(dids, (did) => [did, new Map()]),\n  )\n\n  if (map.size) {\n    const found = await db.db\n      .selectFrom('authorized_client')\n      .select('did')\n      .select('clientId')\n      .select('data')\n      // uses \"authorized_client_pk\"\n      .where('did', 'in', [...map.keys()])\n      .execute()\n\n    for (const { did, clientId, data } of found) {\n      map.get(did)!.set(clientId, fromJson(data))\n    }\n  }\n\n  return map\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/device.ts",
    "content": "import { Selectable } from 'kysely'\nimport { DeviceData, DeviceId } from '@atproto/oauth-provider'\nimport { fromDateISO, toDateISO } from '../../db'\nimport { AccountDb, Device } from '../db'\n\nexport const rowToDeviceData = (\n  row: Omit<Selectable<Device>, 'id'>,\n): DeviceData => ({\n  sessionId: row.sessionId,\n  userAgent: row.userAgent,\n  ipAddress: row.ipAddress,\n  lastSeenAt: fromDateISO(row.lastSeenAt),\n})\n\nexport const createQB = (\n  db: AccountDb,\n  deviceId: DeviceId,\n  { sessionId, userAgent, ipAddress, lastSeenAt }: DeviceData,\n) =>\n  db.db.insertInto('device').values({\n    id: deviceId,\n    sessionId,\n    userAgent,\n    ipAddress,\n    lastSeenAt: toDateISO(lastSeenAt),\n  })\n\nexport const readQB = (db: AccountDb, deviceId: DeviceId) =>\n  db.db.selectFrom('device').where('id', '=', deviceId).selectAll()\n\nexport const updateQB = (\n  db: AccountDb,\n  deviceId: DeviceId,\n  { sessionId, userAgent, ipAddress, lastSeenAt }: Partial<DeviceData>,\n) =>\n  db.db\n    .updateTable('device')\n    .if(sessionId != null, (qb) => qb.set({ sessionId }))\n    .if(userAgent != null, (qb) => qb.set({ userAgent }))\n    .if(ipAddress != null, (qb) => qb.set({ ipAddress }))\n    .if(lastSeenAt != null, (qb) =>\n      qb.set({ lastSeenAt: toDateISO(lastSeenAt!) }),\n    )\n    .where('id', '=', deviceId)\n\nexport const removeQB = (db: AccountDb, deviceId: DeviceId) =>\n  db.db.deleteFrom('device').where('id', '=', deviceId)\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/email-token.ts",
    "content": "import { MINUTE, lessThanAgoMs } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { getRandomToken } from '../../api/com/atproto/server/util'\nimport { AccountDb, EmailTokenPurpose } from '../db'\n\nexport const createEmailToken = async (\n  db: AccountDb,\n  did: string,\n  purpose: EmailTokenPurpose,\n): Promise<string> => {\n  const token = getRandomToken().toUpperCase()\n  const now = new Date().toISOString()\n  await db.executeWithRetry(\n    db.db\n      .insertInto('email_token')\n      .values({ purpose, did, token, requestedAt: now })\n      .onConflict((oc) =>\n        oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt: now }),\n      ),\n  )\n  return token\n}\n\nexport const deleteEmailToken = async (\n  db: AccountDb,\n  did: string,\n  purpose: EmailTokenPurpose,\n) => {\n  await db.executeWithRetry(\n    db.db\n      .deleteFrom('email_token')\n      .where('did', '=', did)\n      .where('purpose', '=', purpose),\n  )\n}\n\nexport const deleteAllEmailTokens = async (db: AccountDb, did: string) => {\n  await db.executeWithRetry(\n    db.db.deleteFrom('email_token').where('did', '=', did),\n  )\n}\n\nexport const assertValidToken = async (\n  db: AccountDb,\n  did: string,\n  purpose: EmailTokenPurpose,\n  token: string,\n  expirationLen = 15 * MINUTE,\n) => {\n  const res = await db.db\n    .selectFrom('email_token')\n    .selectAll()\n    .where('purpose', '=', purpose)\n    .where('did', '=', did)\n    .where('token', '=', token.toUpperCase())\n    .executeTakeFirst()\n  if (!res) {\n    throw new InvalidRequestError('Token is invalid', 'InvalidToken')\n  }\n  const expired = !lessThanAgoMs(new Date(res.requestedAt), expirationLen)\n  if (expired) {\n    throw new InvalidRequestError('Token is expired', 'ExpiredToken')\n  }\n}\n\nexport const assertValidTokenAndFindDid = async (\n  db: AccountDb,\n  purpose: EmailTokenPurpose,\n  token: string,\n  expirationLen = 15 * MINUTE,\n): Promise<string> => {\n  const res = await db.db\n    .selectFrom('email_token')\n    .select(['did', 'requestedAt'])\n    .where('purpose', '=', purpose)\n    .where('token', '=', token.toUpperCase())\n    .executeTakeFirst()\n  if (!res) {\n    throw new InvalidRequestError('Token is invalid', 'InvalidToken')\n  }\n  const expired = !lessThanAgoMs(new Date(res.requestedAt), expirationLen)\n  if (expired) {\n    throw new InvalidRequestError('Token is expired', 'ExpiredToken')\n  }\n  return res.did\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/invite.ts",
    "content": "import { chunkArray } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { countAll } from '../../db'\nimport { AccountDb, InviteCode } from '../db'\n\nexport const createInviteCodes = async (\n  db: AccountDb,\n  toCreate: { account: string; codes: string[] }[],\n  useCount: number,\n) => {\n  const now = new Date().toISOString()\n  const rows = toCreate.flatMap((account) =>\n    account.codes.map((code) => ({\n      code: code,\n      availableUses: useCount,\n      disabled: 0 as const,\n      forAccount: account.account,\n      createdBy: 'admin',\n      createdAt: now,\n    })),\n  )\n  await Promise.all(\n    chunkArray(rows, 50).map((chunk) =>\n      db.executeWithRetry(db.db.insertInto('invite_code').values(chunk)),\n    ),\n  )\n}\n\nexport const createAccountInviteCodes = async (\n  db: AccountDb,\n  forAccount: string,\n  codes: string[],\n  expectedTotal: number,\n  disabled: 0 | 1,\n): Promise<CodeDetail[]> => {\n  const now = new Date().toISOString()\n  const rows = codes.map(\n    (code) =>\n      ({\n        code,\n        availableUses: 1,\n        disabled,\n        forAccount,\n        createdBy: forAccount,\n        createdAt: now,\n      }) as InviteCode,\n  )\n  await db.executeWithRetry(db.db.insertInto('invite_code').values(rows))\n\n  const finalRoutineInviteCodes = await db.db\n    .selectFrom('invite_code')\n    .where('forAccount', '=', forAccount)\n    .where('createdBy', '!=', 'admin') // dont count admin-gifted codes aginast the user\n    .selectAll()\n    .execute()\n  if (finalRoutineInviteCodes.length > expectedTotal) {\n    throw new InvalidRequestError(\n      'attempted to create additional codes in another request',\n      'DuplicateCreate',\n    )\n  }\n\n  return rows.map((row) => ({\n    ...row,\n    available: 1,\n    disabled: row.disabled === 1,\n    uses: [],\n  }))\n}\n\nexport const recordInviteUse = async (\n  db: AccountDb,\n  opts: {\n    did: string\n    inviteCode: string | undefined\n    now: string\n  },\n) => {\n  if (!opts.inviteCode) return\n  await db.executeWithRetry(\n    db.db.insertInto('invite_code_use').values({\n      code: opts.inviteCode,\n      usedBy: opts.did,\n      usedAt: opts.now,\n    }),\n  )\n}\n\nexport const ensureInviteIsAvailable = async (\n  db: AccountDb,\n  inviteCode: string,\n): Promise<void> => {\n  const invite = await db.db\n    .selectFrom('invite_code')\n    .leftJoin('actor', 'actor.did', 'invite_code.forAccount')\n    .where('takedownRef', 'is', null)\n    .selectAll('invite_code')\n    .where('code', '=', inviteCode)\n    .executeTakeFirst()\n\n  if (!invite || invite.disabled) {\n    throw new InvalidRequestError(\n      'Provided invite code not available',\n      'InvalidInviteCode',\n    )\n  }\n\n  const uses = await db.db\n    .selectFrom('invite_code_use')\n    .select(countAll.as('count'))\n    .where('code', '=', inviteCode)\n    .executeTakeFirstOrThrow()\n\n  if (invite.availableUses <= uses.count) {\n    throw new InvalidRequestError(\n      'Provided invite code not available',\n      'InvalidInviteCode',\n    )\n  }\n}\n\nexport const selectInviteCodesQb = (db: AccountDb) => {\n  const ref = db.db.dynamic.ref\n  const builder = db.db\n    .selectFrom('invite_code')\n    .select([\n      'invite_code.code as code',\n      'invite_code.availableUses as available',\n      'invite_code.disabled as disabled',\n      'invite_code.forAccount as forAccount',\n      'invite_code.createdBy as createdBy',\n      'invite_code.createdAt as createdAt',\n      db.db\n        .selectFrom('invite_code_use')\n        .select(countAll.as('count'))\n        .whereRef('invite_code_use.code', '=', ref('invite_code.code'))\n        .as('uses'),\n    ])\n  return db.db.selectFrom(builder.as('codes')).selectAll()\n}\n\nexport const getAccountsInviteCodes = async (\n  db: AccountDb,\n  dids: string[],\n): Promise<Map<string, CodeDetail[]>> => {\n  const results = new Map<string, CodeDetail[]>()\n  // We don't want to pass an empty array to kysely and let's avoid running a query entirely if there is nothing to match for\n  if (!dids.length) return results\n  const res = await selectInviteCodesQb(db)\n    .where('forAccount', 'in', dids)\n    .execute()\n  const codes = res.map((row) => row.code)\n  const uses = await getInviteCodesUses(db, codes)\n  res.forEach((row) => {\n    const existing = results.get(row.forAccount) ?? []\n    results.set(row.forAccount, [\n      ...existing,\n      {\n        ...row,\n        uses: uses[row.code] ?? [],\n        disabled: row.disabled === 1,\n      },\n    ])\n  })\n  return results\n}\n\nexport const getInviteCodesUses = async (\n  db: AccountDb,\n  codes: string[],\n): Promise<Record<string, CodeUse[]>> => {\n  const uses: Record<string, CodeUse[]> = {}\n  if (codes.length > 0) {\n    const usesRes = await db.db\n      .selectFrom('invite_code_use')\n      .where('code', 'in', codes)\n      .orderBy('usedAt', 'desc')\n      .selectAll()\n      .execute()\n    for (const use of usesRes) {\n      const { code, usedBy, usedAt } = use\n      uses[code] ??= []\n      uses[code].push({ usedBy, usedAt })\n    }\n  }\n  return uses\n}\n\nexport const getInvitedByForAccounts = async (\n  db: AccountDb,\n  dids: string[],\n): Promise<Record<string, CodeDetail>> => {\n  if (dids.length < 1) return {}\n  const codeDetailsRes = await selectInviteCodesQb(db)\n    .where('code', 'in', (qb) =>\n      qb\n        .selectFrom('invite_code_use')\n        .where('usedBy', 'in', dids)\n        .select('code')\n        .distinct(),\n    )\n    .execute()\n  const uses = await getInviteCodesUses(\n    db,\n    codeDetailsRes.map((row) => row.code),\n  )\n  const codeDetails = codeDetailsRes.map((row) => ({\n    ...row,\n    uses: uses[row.code] ?? [],\n    disabled: row.disabled === 1,\n  }))\n  return codeDetails.reduce(\n    (acc, cur) => {\n      for (const use of cur.uses) {\n        acc[use.usedBy] = cur\n      }\n      return acc\n    },\n    {} as Record<string, CodeDetail>,\n  )\n}\n\nexport const disableInviteCodes = async (\n  db: AccountDb,\n  opts: { codes: string[]; accounts: string[] },\n) => {\n  const { codes, accounts } = opts\n  if (codes.length > 0) {\n    await db.executeWithRetry(\n      db.db\n        .updateTable('invite_code')\n        .set({ disabled: 1 })\n        .where('code', 'in', codes),\n    )\n  }\n  if (accounts.length > 0) {\n    await db.executeWithRetry(\n      db.db\n        .updateTable('invite_code')\n        .set({ disabled: 1 })\n        .where('forAccount', 'in', accounts),\n    )\n  }\n}\n\nexport const setAccountInvitesDisabled = async (\n  db: AccountDb,\n  did: string,\n  disabled: boolean,\n) => {\n  await db.executeWithRetry(\n    db.db\n      .updateTable('account')\n      .where('did', '=', did)\n      .set({ invitesDisabled: disabled ? 1 : 0 }),\n  )\n}\n\nexport type CodeDetail = {\n  code: string\n  available: number\n  disabled: boolean\n  forAccount: string\n  createdBy: string\n  createdAt: string\n  uses: CodeUse[]\n}\n\ntype CodeUse = {\n  usedBy: string\n  usedAt: string\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/lexicon.ts",
    "content": "import { Insertable } from 'kysely'\nimport { LEXICON_REFRESH_FREQUENCY, LexiconData } from '@atproto/oauth-provider'\nimport { fromDateISO, fromJson, toDateISO, toJson } from '../../db'\nimport { AccountDb, Lexicon } from '../db'\n\nexport async function upsert(db: AccountDb, nsid: string, data: LexiconData) {\n  const updates: Omit<Insertable<Lexicon>, 'nsid'> = {\n    ...data,\n    createdAt: toDateISO(data.createdAt),\n    updatedAt: toDateISO(data.updatedAt),\n    lastSucceededAt: data.lastSucceededAt\n      ? toDateISO(data.lastSucceededAt)\n      : null,\n    lexicon: data.lexicon ? toJson(data.lexicon) : null,\n  }\n\n  await db.executeWithRetry(\n    db.db\n      .insertInto('lexicon')\n      .values({ ...updates, nsid })\n      .onConflict((oc) => oc.column('nsid').doUpdateSet(updates)),\n  )\n\n  // Garbage collection: remove old, never resolved, lexicons.\n  // Uses \"lexicon_failures_idx\"\n  await db.executeWithRetry(\n    db.db\n      .deleteFrom('lexicon')\n      .where('lexicon', 'is', null)\n      .where(\n        'updatedAt',\n        '<',\n        toDateISO(new Date(Date.now() - LEXICON_REFRESH_FREQUENCY)),\n      ),\n  )\n}\n\nexport async function find(\n  db: AccountDb,\n  nsid: string,\n): Promise<LexiconData | null> {\n  const row = await db.db\n    .selectFrom('lexicon')\n    .selectAll()\n    .where('nsid', '=', nsid)\n    .executeTakeFirst()\n  if (!row) return null\n\n  return {\n    ...row,\n    createdAt: fromDateISO(row.createdAt),\n    updatedAt: fromDateISO(row.updatedAt),\n    lastSucceededAt: row.lastSucceededAt\n      ? fromDateISO(row.lastSucceededAt)\n      : null,\n    lexicon: row.lexicon ? fromJson(row.lexicon) : null,\n  }\n}\n\nexport async function remove(db: AccountDb, nsid: string) {\n  await db.db.deleteFrom('lexicon').where('nsid', '=', nsid).execute()\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/password.ts",
    "content": "import { randomStr } from '@atproto/crypto'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword'\nimport { AccountDb } from '../db'\nimport * as scrypt from './scrypt'\n\nexport type AppPassDescript = {\n  name: string\n  privileged: boolean\n}\n\nexport const verifyAccountPassword = async (\n  db: AccountDb,\n  did: string,\n  password: string,\n): Promise<boolean> => {\n  const found = await db.db\n    .selectFrom('account')\n    .selectAll()\n    .where('did', '=', did)\n    .executeTakeFirst()\n  return found ? await scrypt.verify(password, found.passwordScrypt) : false\n}\n\nexport const verifyAppPassword = async (\n  db: AccountDb,\n  did: string,\n  password: string,\n): Promise<AppPassDescript | null> => {\n  const passwordScrypt = await scrypt.hashAppPassword(did, password)\n  const found = await db.db\n    .selectFrom('app_password')\n    .selectAll()\n    .where('did', '=', did)\n    .where('passwordScrypt', '=', passwordScrypt)\n    .executeTakeFirst()\n  if (!found) return null\n  return {\n    name: found.name,\n    privileged: found.privileged === 1 ? true : false,\n  }\n}\n\nexport const updateUserPassword = async (\n  db: AccountDb,\n  opts: {\n    did: string\n    passwordScrypt: string\n  },\n) => {\n  await db.executeWithRetry(\n    db.db\n      .updateTable('account')\n      .set({ passwordScrypt: opts.passwordScrypt })\n      .where('did', '=', opts.did),\n  )\n}\n\nexport const createAppPassword = async (\n  db: AccountDb,\n  did: string,\n  name: string,\n  privileged: boolean,\n): Promise<AppPassword> => {\n  // create an app password with format:\n  // 1234-abcd-5678-efgh\n  const str = randomStr(16, 'base32').slice(0, 16)\n  const chunks = [\n    str.slice(0, 4),\n    str.slice(4, 8),\n    str.slice(8, 12),\n    str.slice(12, 16),\n  ]\n  const password = chunks.join('-')\n  const passwordScrypt = await scrypt.hashAppPassword(did, password)\n  const [got] = await db.executeWithRetry(\n    db.db\n      .insertInto('app_password')\n      .values({\n        did,\n        name,\n        passwordScrypt,\n        createdAt: new Date().toISOString(),\n        privileged: privileged ? 1 : 0,\n      })\n      .returningAll(),\n  )\n  if (!got) {\n    throw new InvalidRequestError('could not create app-specific password')\n  }\n  return {\n    name,\n    password,\n    createdAt: got.createdAt,\n    privileged,\n  }\n}\n\nexport const listAppPasswords = async (\n  db: AccountDb,\n  did: string,\n): Promise<{ name: string; createdAt: string; privileged: boolean }[]> => {\n  const res = await db.db\n    .selectFrom('app_password')\n    .select(['name', 'createdAt', 'privileged'])\n    .where('did', '=', did)\n    .orderBy('createdAt', 'desc')\n    .execute()\n  return res.map((row) => ({\n    name: row.name,\n    createdAt: row.createdAt,\n    privileged: row.privileged === 1 ? true : false,\n  }))\n}\n\nexport const deleteAppPassword = async (\n  db: AccountDb,\n  did: string,\n  name: string,\n) => {\n  await db.executeWithRetry(\n    db.db\n      .deleteFrom('app_password')\n      .where('did', '=', did)\n      .where('name', '=', name),\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/repo.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AccountDb } from '../db'\n\nexport const updateRoot = async (\n  db: AccountDb,\n  did: string,\n  cid: CID,\n  rev: string,\n) => {\n  // @TODO balance risk of a race in the case of a long retry\n  await db.executeWithRetry(\n    db.db\n      .insertInto('repo_root')\n      .values({\n        did,\n        cid: cid.toString(),\n        rev,\n        indexedAt: new Date().toISOString(),\n      })\n      .onConflict((oc) =>\n        oc.column('did').doUpdateSet({ cid: cid.toString(), rev }),\n      ),\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/scrypt.ts",
    "content": "import crypto from 'node:crypto'\nimport * as ui8 from 'uint8arrays'\nimport { sha256 } from '@atproto/crypto'\n\nexport const OLD_PASSWORD_MAX_LENGTH = 512\nexport const NEW_PASSWORD_MAX_LENGTH = 256\n\nexport const genSaltAndHash = (password: string): Promise<string> => {\n  const salt = crypto.randomBytes(16).toString('hex')\n  return hashWithSalt(password, salt)\n}\n\nexport const hashWithSalt = (\n  password: string,\n  salt: string,\n): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    crypto.scrypt(password, salt, 64, (err, hash) => {\n      if (err) return reject(err)\n      resolve(salt + ':' + hash.toString('hex'))\n    })\n  })\n}\n\nexport const verify = async (\n  password: string,\n  storedHash: string,\n): Promise<boolean> => {\n  const [salt, hash] = storedHash.split(':')\n  const derivedHash = await getDerivedHash(password, salt)\n  return hash === derivedHash\n}\n\nexport const getDerivedHash = (\n  password: string,\n  salt: string,\n): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    crypto.scrypt(password, salt, 64, (err, derivedHash) => {\n      if (err) return reject(err)\n      resolve(derivedHash.toString('hex'))\n    })\n  })\n}\n\nexport const hashAppPassword = async (\n  did: string,\n  password: string,\n): Promise<string> => {\n  const sha = await sha256(did)\n  const salt = ui8.toString(sha.slice(0, 16), 'hex')\n  return hashWithSalt(password, salt)\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/token.ts",
    "content": "import { Selectable } from 'kysely'\nimport {\n  Code,\n  NewTokenData,\n  RefreshToken,\n  TokenData,\n  TokenId,\n} from '@atproto/oauth-provider'\nimport { fromDateISO, fromJson, toDateISO, toJson } from '../../db'\nimport { AccountDb, Token } from '../db'\nimport { selectAccountQB } from './account'\n\nexport function toTokenData(row: Selectable<Token>): TokenData {\n  return {\n    createdAt: fromDateISO(row.createdAt),\n    expiresAt: fromDateISO(row.expiresAt),\n    updatedAt: fromDateISO(row.updatedAt),\n    clientId: row.clientId,\n    clientAuth: fromJson(row.clientAuth),\n    deviceId: row.deviceId,\n    sub: row.did,\n    parameters: fromJson(row.parameters),\n    code: row.code,\n    scope: row.scope,\n  }\n}\n\nconst selectTokenInfoQB = (db: AccountDb) =>\n  selectAccountQB(db, { includeDeactivated: true })\n    // uses \"token_did_idx\" index (though unlikely in practice)\n    .innerJoin('token', 'token.did', 'actor.did')\n    .select([\n      'token.id',\n      'token.tokenId',\n      'token.createdAt',\n      'token.updatedAt',\n      'token.expiresAt',\n      'token.clientId',\n      'token.clientAuth',\n      'token.deviceId',\n      'token.did',\n      'token.parameters',\n      'token.details',\n      'token.code',\n      'token.currentRefreshToken',\n      'token.scope',\n    ])\n\nexport const createQB = (\n  db: AccountDb,\n  tokenId: TokenId,\n  data: TokenData,\n  refreshToken?: RefreshToken,\n) => {\n  return db.db.insertInto('token').values({\n    tokenId,\n    createdAt: toDateISO(data.createdAt),\n    expiresAt: toDateISO(data.expiresAt),\n    updatedAt: toDateISO(data.updatedAt),\n    clientId: data.clientId,\n    clientAuth: toJson(data.clientAuth),\n    deviceId: data.deviceId,\n    did: data.sub,\n    parameters: toJson(data.parameters),\n    details: data.details ? toJson(data.details) : null,\n    code: data.code,\n    currentRefreshToken: refreshToken || null,\n    scope: data.scope,\n  })\n}\n\nexport const forRotateQB = (db: AccountDb, id: TokenId) =>\n  db.db\n    .selectFrom('token')\n    .where('tokenId', '=', id)\n    .where('currentRefreshToken', 'is not', null)\n    .select(['id', 'currentRefreshToken'])\n\nexport const findByQB = (\n  db: AccountDb,\n  search: {\n    id?: number\n    did?: string\n    code?: Code\n    tokenId?: TokenId\n    currentRefreshToken?: RefreshToken\n  },\n) => {\n  if (\n    search.id === undefined &&\n    search.did === undefined &&\n    search.code === undefined &&\n    search.tokenId === undefined &&\n    search.currentRefreshToken === undefined\n  ) {\n    // Prevent accidental scan\n    throw new TypeError('At least one search parameter is required')\n  }\n\n  return selectTokenInfoQB(db)\n    .if(search.id !== undefined, (qb) =>\n      // uses primary key index\n      qb.where('token.id', '=', search.id!),\n    )\n    .if(search.did !== undefined, (qb) =>\n      // uses \"token_did_idx\" index\n      qb.where('token.did', '=', search.did!),\n    )\n    .if(search.code !== undefined, (qb) =>\n      // uses \"token_code_idx\" partial index (hence the null check)\n      qb\n        .where('token.code', '=', search.code!)\n        .where('token.code', 'is not', null),\n    )\n    .if(search.tokenId !== undefined, (qb) =>\n      // uses \"token_token_id_idx\"\n      qb.where('token.tokenId', '=', search.tokenId!),\n    )\n    .if(search.currentRefreshToken !== undefined, (qb) =>\n      // uses \"token_refresh_token_unique_idx\"\n      qb.where('token.currentRefreshToken', '=', search.currentRefreshToken!),\n    )\n}\n\nexport const removeByDidQB = (db: AccountDb, did: string) =>\n  // uses \"token_did_idx\" index\n  db.db.deleteFrom('token').where('did', '=', did)\n\nexport const rotateQB = (\n  db: AccountDb,\n  id: number,\n  newTokenId: TokenId,\n  newRefreshToken: RefreshToken,\n  newData: NewTokenData,\n) =>\n  db.db\n    .updateTable('token')\n    .set({\n      tokenId: newTokenId,\n      currentRefreshToken: newRefreshToken,\n\n      expiresAt: toDateISO(newData.expiresAt),\n      updatedAt: toDateISO(newData.updatedAt),\n      clientAuth: toJson(newData.clientAuth),\n      scope: newData.scope,\n    })\n    // uses primary key index\n    .where('id', '=', id)\n\nexport const removeQB = (db: AccountDb, tokenId: TokenId) =>\n  // uses \"used_refresh_token_fk\" to cascade delete\n  db.db.deleteFrom('token').where('tokenId', '=', tokenId)\n"
  },
  {
    "path": "packages/pds/src/account-manager/helpers/used-refresh-token.ts",
    "content": "import { RefreshToken } from '@atproto/oauth-provider'\nimport { AccountDb } from '../db'\n\n/**\n * Note that the used refresh tokens will be removed once the token is revoked.\n * This is done through the foreign key constraint in the database.\n */\nexport const insertQB = (\n  db: AccountDb,\n  tokenId: number,\n  refreshToken: RefreshToken,\n) =>\n  db.db\n    .insertInto('used_refresh_token')\n    .values({ tokenId, refreshToken })\n    .onConflict((oc) => oc.doNothing())\n\nexport const findByTokenQB = (db: AccountDb, refreshToken: RefreshToken) =>\n  db.db\n    .selectFrom('used_refresh_token')\n    // uses primary key index\n    .where('refreshToken', '=', refreshToken)\n    .select('tokenId')\n\nexport const countQB = (db: AccountDb, refreshToken: RefreshToken) =>\n  db.db\n    .selectFrom('used_refresh_token')\n    // uses primary key index\n    .where('refreshToken', '=', refreshToken)\n    .select((qb) => qb.fn.count<number>('refreshToken').as('count'))\n"
  },
  {
    "path": "packages/pds/src/account-manager/oauth-store.ts",
    "content": "import assert from 'node:assert'\nimport { Client, createOp as createPlcOp } from '@did-plc/lib'\nimport { Selectable } from 'kysely'\nimport { Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport {\n  Account,\n  AccountStore,\n  AuthenticateAccountData,\n  AuthorizedClientData,\n  AuthorizedClients,\n  ClientId,\n  Code,\n  DeviceAccount,\n  DeviceData,\n  DeviceId,\n  DeviceStore,\n  FoundRequestResult,\n  HandleUnavailableError,\n  InvalidInviteCodeError,\n  InvalidRequestError,\n  LexiconData,\n  LexiconStore,\n  NewTokenData,\n  RefreshToken,\n  RequestData,\n  RequestId,\n  RequestStore,\n  ResetPasswordConfirmInput,\n  ResetPasswordRequestInput,\n  SignUpData,\n  Sub,\n  TokenData,\n  TokenId,\n  TokenInfo,\n  TokenStore,\n  UpdateRequestData,\n} from '@atproto/oauth-provider'\nimport {\n  AuthRequiredError as XrpcAuthRequiredError,\n  InvalidRequestError as XrpcInvalidRequestError,\n} from '@atproto/xrpc-server'\nimport { ActorStore } from '../actor-store/actor-store'\nimport { BackgroundQueue } from '../background'\nimport { fromDateISO } from '../db'\nimport { ImageUrlBuilder } from '../image/image-url-builder'\nimport { dbLogger } from '../logger'\nimport { ServerMailer } from '../mailer'\nimport { Sequencer, syncEvtDataFromCommit } from '../sequencer'\nimport { AccountManager } from './account-manager'\nimport * as schemas from './db/schema'\nimport * as accountHelper from './helpers/account'\nimport { AccountStatus } from './helpers/account'\nimport * as accountDeviceHelper from './helpers/account-device'\nimport * as authRequestHelper from './helpers/authorization-request'\nimport * as authorizedClientHelper from './helpers/authorized-client'\nimport * as deviceHelper from './helpers/device'\nimport * as lexiconHelper from './helpers/lexicon'\nimport * as tokenHelper from './helpers/token'\nimport * as usedRefreshTokenHelper from './helpers/used-refresh-token'\n\n/**\n * This class' purpose is to implement the interface needed by the OAuthProvider\n * to interact with the account database (through the {@link AccountManager}).\n *\n * @note The use of this class assumes that there is no entryway.\n */\nexport class OAuthStore\n  implements AccountStore, RequestStore, DeviceStore, LexiconStore, TokenStore\n{\n  constructor(\n    private readonly accountManager: AccountManager,\n    private readonly actorStore: ActorStore,\n    private readonly imageUrlBuilder: ImageUrlBuilder,\n    private readonly backgroundQueue: BackgroundQueue,\n    private readonly mailer: ServerMailer,\n    private readonly sequencer: Sequencer,\n    private readonly plcClient: Client,\n    private readonly plcRotationKey: Keypair,\n    private readonly publicUrl: string,\n    private readonly recoveryDidKey: string | null,\n  ) {}\n\n  private get db() {\n    const { db } = this.accountManager\n    if (db.destroyed) throw new Error('Database connection is closed')\n    return db\n  }\n\n  private get serviceDid() {\n    return this.accountManager.serviceDid\n  }\n\n  private async verifyEmailAvailability(email: string): Promise<void> {\n    // @NOTE Email validity & disposability check performed by the OAuthProvider\n\n    const account = await this.accountManager.getAccountByEmail(email, {\n      includeDeactivated: true,\n      includeTakenDown: true,\n    })\n\n    if (account) {\n      throw new InvalidRequestError(`Email already taken`)\n    }\n  }\n\n  private async verifyInviteCode(code: string) {\n    try {\n      await this.accountManager.ensureInviteIsAvailable(code)\n    } catch (err) {\n      const message =\n        err instanceof XrpcInvalidRequestError ? err.message : undefined\n      throw new InvalidInviteCodeError(message, err)\n    }\n  }\n\n  // AccountStore\n\n  async createAccount({\n    locale: _locale,\n    inviteCode,\n    handle,\n    email,\n    password,\n  }: SignUpData): Promise<Account> {\n    // @TODO Send an account creation confirmation email (+verification link) to the user (in their locale)\n    // @NOTE Password strength & length already enforced by the OAuthProvider\n\n    await Promise.all([\n      this.verifyEmailAvailability(email),\n      this.verifyHandleAvailability(handle),\n      !inviteCode || this.verifyInviteCode(inviteCode),\n    ])\n\n    // @TODO The code bellow should probably be refactored to be common with the\n    // code of the `com.atproto.server.createAccount` XRPC endpoint.\n\n    const signingKey = await Secp256k1Keypair.create({ exportable: true })\n    const signingKeyDid = signingKey.did()\n\n    const plcCreate = await createPlcOp({\n      signingKey: signingKeyDid,\n      rotationKeys: this.recoveryDidKey\n        ? [this.recoveryDidKey, this.plcRotationKey.did()]\n        : [this.plcRotationKey.did()],\n      handle,\n      pds: this.publicUrl,\n      signer: this.plcRotationKey,\n    })\n\n    const { did, op } = plcCreate\n\n    try {\n      await this.actorStore.create(did, signingKey)\n      try {\n        const commit = await this.actorStore.transact(did, (actorTxn) =>\n          actorTxn.repo.createRepo([]),\n        )\n\n        await this.plcClient.sendOperation(did, op)\n\n        await this.accountManager.createAccount({\n          did,\n          handle,\n          email,\n          password,\n          inviteCode,\n          repoCid: commit.cid,\n          repoRev: commit.rev,\n        })\n        try {\n          await this.sequencer.sequenceIdentityEvt(did, handle)\n          await this.sequencer.sequenceAccountEvt(did, AccountStatus.Active)\n          await this.sequencer.sequenceCommit(did, commit)\n          await this.sequencer.sequenceSyncEvt(\n            did,\n            syncEvtDataFromCommit(commit),\n          )\n          await this.accountManager.updateRepoRoot(did, commit.cid, commit.rev)\n          await this.actorStore.clearReservedKeypair(signingKeyDid, did)\n\n          const account = await this.accountManager.getAccount(did)\n          if (!account) throw new Error('Account not found')\n\n          return await this.buildAccount(account)\n        } catch (err) {\n          this.accountManager.deleteAccount(did)\n          throw err\n        }\n      } catch (err) {\n        await this.actorStore.destroy(did)\n        throw err\n      }\n    } catch (err) {\n      // XrpcError => OAuthError\n      if (err instanceof XrpcInvalidRequestError) {\n        throw new InvalidRequestError(err.message, err)\n      }\n      throw err\n    }\n  }\n\n  async authenticateAccount({\n    locale: _locale,\n    username: identifier,\n    password,\n    // Not supported by the PDS (yet?)\n    emailOtp = undefined,\n  }: AuthenticateAccountData): Promise<Account> {\n    // @TODO (?) Send an email to the user to notify them of the login attempt\n    try {\n      // Should never happen\n      if (emailOtp != null) {\n        throw new Error('Email OTP is not supported')\n      }\n\n      const { user, appPassword, isSoftDeleted } =\n        await this.accountManager.login({ identifier, password })\n\n      if (isSoftDeleted) {\n        throw new InvalidRequestError('Account was taken down')\n      }\n\n      if (appPassword) {\n        throw new InvalidRequestError('App passwords are not allowed')\n      }\n\n      return this.buildAccount(user)\n    } catch (err) {\n      if (err instanceof XrpcAuthRequiredError) {\n        throw new InvalidRequestError(err.message, err)\n      }\n      throw err\n    }\n  }\n\n  async setAuthorizedClient(\n    sub: Sub,\n    clientId: ClientId,\n    data: AuthorizedClientData,\n  ): Promise<void> {\n    await authorizedClientHelper.upsert(this.db, sub, clientId, data)\n  }\n\n  async getAccount(sub: Sub): Promise<{\n    account: Account\n    authorizedClients: AuthorizedClients\n  }> {\n    const accountRow = await accountHelper.getAccount(this.db, sub, {\n      includeDeactivated: true,\n    })\n\n    assert(accountRow, 'Account not found')\n\n    const account = await this.buildAccount(accountRow)\n    const authorizedClients = await authorizedClientHelper.getAuthorizedClients(\n      this.db,\n      sub,\n    )\n\n    return { account, authorizedClients }\n  }\n\n  async upsertDeviceAccount(deviceId: DeviceId, sub: string): Promise<void> {\n    await this.db.executeWithRetry(\n      accountDeviceHelper.upsertQB(this.db, deviceId, sub),\n    )\n  }\n\n  async getDeviceAccount(\n    deviceId: DeviceId,\n    sub: string,\n  ): Promise<DeviceAccount | null> {\n    const row = await accountDeviceHelper\n      .selectQB(this.db, { deviceId, sub })\n      .executeTakeFirst()\n\n    if (!row) return null\n\n    return {\n      deviceId,\n      deviceData: deviceHelper.rowToDeviceData(row),\n      account: await this.buildAccount(row),\n      authorizedClients: await authorizedClientHelper.getAuthorizedClients(\n        this.db,\n        sub,\n      ),\n      createdAt: fromDateISO(row.adCreatedAt),\n      updatedAt: fromDateISO(row.adUpdatedAt),\n    }\n  }\n\n  async removeDeviceAccount(deviceId: DeviceId, sub: Sub): Promise<void> {\n    await this.db.executeWithRetry(\n      accountDeviceHelper.removeQB(this.db, deviceId, sub),\n    )\n  }\n\n  async listDeviceAccounts(\n    filter: { sub: Sub } | { deviceId: DeviceId },\n  ): Promise<DeviceAccount[]> {\n    const rows = await accountDeviceHelper.selectQB(this.db, filter).execute()\n\n    const uniqueDids = [...new Set(rows.map((row) => row.did))]\n\n    // Enrich all distinct account with their profile data\n    const accounts = new Map(\n      await Promise.all(\n        Array.from(uniqueDids, async (did): Promise<[Sub, Account]> => {\n          const row = rows.find((r) => r.did === did)!\n          return [did, await this.buildAccount(row)]\n        }),\n      ),\n    )\n\n    const authorizedClientsMap =\n      await authorizedClientHelper.getAuthorizedClientsMulti(\n        this.db,\n        uniqueDids,\n      )\n\n    return rows.map((row) => ({\n      deviceId: row.deviceId,\n      deviceData: deviceHelper.rowToDeviceData(row),\n      account: accounts.get(row.did)!,\n      authorizedClients: authorizedClientsMap.get(row.did)!,\n      createdAt: fromDateISO(row.adCreatedAt),\n      updatedAt: fromDateISO(row.adUpdatedAt),\n    }))\n  }\n\n  async resetPasswordRequest({\n    locale: _locale,\n    email,\n  }: ResetPasswordRequestInput): Promise<Account | null> {\n    const account = await this.accountManager.getAccountByEmail(email, {\n      includeDeactivated: true,\n      includeTakenDown: true,\n    })\n\n    if (!account?.email || !account?.handle) return null\n\n    const { handle } = account\n    const token = await this.accountManager.createEmailToken(\n      account.did,\n      'reset_password',\n    )\n\n    // @TODO Use the locale to send the email in the right language\n    await this.mailer.sendResetPassword(\n      { handle, token },\n      { to: account.email },\n    )\n\n    return this.buildAccount(account)\n  }\n\n  async resetPasswordConfirm(\n    data: ResetPasswordConfirmInput,\n  ): Promise<Account | null> {\n    try {\n      const did = await this.accountManager.resetPassword(data)\n      const account = await this.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n\n      return account ? this.buildAccount(account) : null\n    } catch (err) {\n      if (err instanceof XrpcInvalidRequestError) {\n        return null\n      }\n\n      throw err\n    }\n  }\n\n  async verifyHandleAvailability(handle: string): Promise<void> {\n    // @NOTE Handle validity & normalization already enforced by the OAuthProvider\n    try {\n      const normalized =\n        await this.accountManager.normalizeAndValidateHandle(handle)\n\n      // Should never happen (OAuthProvider should have already validated the\n      // handle) This check is just a safeguard against future normalization\n      // changes.\n      if (normalized !== handle) {\n        throw new HandleUnavailableError('syntax', 'Invalid handle')\n      }\n\n      const account = await this.accountManager.getAccount(normalized, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n\n      if (account) {\n        throw new HandleUnavailableError('taken')\n      }\n    } catch (err) {\n      if (err instanceof XrpcInvalidRequestError) {\n        throw err.customErrorName === 'HandleNotAvailable'\n          ? new HandleUnavailableError('taken', err.message)\n          : new HandleUnavailableError('syntax', err.message)\n      }\n\n      throw err\n    }\n  }\n\n  // RequestStore\n\n  async createRequest(id: RequestId, data: RequestData): Promise<void> {\n    await this.db.executeWithRetry(\n      authRequestHelper.createQB(this.db, id, data),\n    )\n  }\n\n  async readRequest(id: RequestId): Promise<RequestData | null> {\n    try {\n      const row = await authRequestHelper.readQB(this.db, id).executeTakeFirst()\n      if (!row) return null\n      return authRequestHelper.rowToRequestData(row)\n    } finally {\n      // Take the opportunity to clean up expired requests. Do this after we got\n      // the current (potentially expired) request data to allow the provider to\n      // handle expired requests.\n      this.backgroundQueue.add(async () => {\n        await this.db.executeWithRetry(\n          authRequestHelper.removeOldExpiredQB(this.db),\n        )\n      })\n    }\n  }\n\n  async updateRequest(id: RequestId, data: UpdateRequestData): Promise<void> {\n    await this.db.executeWithRetry(\n      authRequestHelper.updateQB(this.db, id, data),\n    )\n  }\n\n  async deleteRequest(id: RequestId): Promise<void> {\n    await this.db.executeWithRetry(authRequestHelper.removeByIdQB(this.db, id))\n  }\n\n  async consumeRequestCode(code: Code): Promise<FoundRequestResult | null> {\n    const row = await authRequestHelper\n      .consumeByCodeQB(this.db, code)\n      .executeTakeFirst()\n    return row ? authRequestHelper.rowToFoundRequestResult(row) : null\n  }\n\n  // DeviceStore\n\n  async createDevice(deviceId: DeviceId, data: DeviceData): Promise<void> {\n    await this.db.executeWithRetry(\n      deviceHelper.createQB(this.db, deviceId, data),\n    )\n  }\n\n  async readDevice(deviceId: DeviceId): Promise<null | DeviceData> {\n    const row = await deviceHelper.readQB(this.db, deviceId).executeTakeFirst()\n    return row ? deviceHelper.rowToDeviceData(row) : null\n  }\n\n  async updateDevice(\n    deviceId: DeviceId,\n    data: Partial<DeviceData>,\n  ): Promise<void> {\n    await this.db.executeWithRetry(\n      deviceHelper.updateQB(this.db, deviceId, data),\n    )\n  }\n\n  async deleteDevice(deviceId: DeviceId): Promise<void> {\n    // Will cascade to device_account (device_account_device_id_fk)\n    await this.db.executeWithRetry(deviceHelper.removeQB(this.db, deviceId))\n  }\n\n  // LexiconStore\n\n  async findLexicon(nsid: string): Promise<LexiconData | null> {\n    return lexiconHelper.find(this.db, nsid)\n  }\n\n  async storeLexicon(nsid: string, data: LexiconData): Promise<void> {\n    return lexiconHelper.upsert(this.db, nsid, data)\n  }\n\n  async deleteLexicon(nsid: string): Promise<void> {\n    return lexiconHelper.remove(this.db, nsid)\n  }\n\n  // TokenStore\n\n  async createToken(\n    id: TokenId,\n    data: TokenData,\n    refreshToken?: RefreshToken,\n  ): Promise<void> {\n    await this.db.transaction(async (dbTxn) => {\n      if (refreshToken) {\n        const { count } = await usedRefreshTokenHelper\n          .countQB(dbTxn, refreshToken)\n          .executeTakeFirstOrThrow()\n\n        if (count > 0) {\n          throw new Error('Refresh token already in use')\n        }\n      }\n\n      return tokenHelper.createQB(dbTxn, id, data, refreshToken).execute()\n    })\n  }\n\n  async listAccountTokens(sub: Sub): Promise<TokenInfo[]> {\n    const rows = await tokenHelper.findByQB(this.db, { did: sub }).execute()\n    return Promise.all(rows.map((row) => this.toTokenInfo(row)))\n  }\n\n  async readToken(tokenId: TokenId): Promise<TokenInfo | null> {\n    const row = await tokenHelper\n      .findByQB(this.db, { tokenId })\n      .executeTakeFirst()\n    return row ? this.toTokenInfo(row) : null\n  }\n\n  async deleteToken(tokenId: TokenId): Promise<void> {\n    // Will cascade to used_refresh_token (used_refresh_token_fk)\n    await this.db.executeWithRetry(tokenHelper.removeQB(this.db, tokenId))\n  }\n\n  async rotateToken(\n    tokenId: TokenId,\n    newTokenId: TokenId,\n    newRefreshToken: RefreshToken,\n    newData: NewTokenData,\n  ): Promise<void> {\n    const err = await this.db.transaction(async (dbTxn) => {\n      const { id, currentRefreshToken } = await tokenHelper\n        .forRotateQB(dbTxn, tokenId)\n        .executeTakeFirstOrThrow()\n\n      if (currentRefreshToken) {\n        await usedRefreshTokenHelper\n          .insertQB(dbTxn, id, currentRefreshToken)\n          .execute()\n      }\n\n      const { count } = await usedRefreshTokenHelper\n        .countQB(dbTxn, newRefreshToken)\n        .executeTakeFirstOrThrow()\n\n      if (count > 0) {\n        // Do NOT throw (we don't want the transaction to be rolled back)\n        return new Error('New refresh token already in use')\n      }\n\n      await tokenHelper\n        .rotateQB(dbTxn, id, newTokenId, newRefreshToken, newData)\n        .execute()\n    })\n\n    if (err) throw err\n  }\n\n  async findTokenByRefreshToken(\n    refreshToken: RefreshToken,\n  ): Promise<TokenInfo | null> {\n    const used = await usedRefreshTokenHelper\n      .findByTokenQB(this.db, refreshToken)\n      .executeTakeFirst()\n\n    const search = used\n      ? { id: used.tokenId }\n      : { currentRefreshToken: refreshToken }\n\n    const row = await tokenHelper.findByQB(this.db, search).executeTakeFirst()\n    return row ? this.toTokenInfo(row) : null\n  }\n\n  async findTokenByCode(code: Code): Promise<TokenInfo | null> {\n    const row = await tokenHelper.findByQB(this.db, { code }).executeTakeFirst()\n    return row ? this.toTokenInfo(row) : null\n  }\n\n  private async toTokenInfo(\n    row: accountHelper.ActorAccount & Selectable<schemas.Token>,\n  ): Promise<TokenInfo> {\n    return {\n      id: row.tokenId,\n      data: tokenHelper.toTokenData(row),\n      account: await this.buildAccount(row),\n      currentRefreshToken: row.currentRefreshToken,\n    }\n  }\n\n  private async buildAccount(\n    row: accountHelper.ActorAccount,\n  ): Promise<Account> {\n    const account: Account = {\n      sub: row.did,\n      aud: this.serviceDid,\n      email: row.email || undefined,\n      email_verified: row.email ? row.emailConfirmedAt != null : undefined,\n      preferred_username: row.handle || undefined,\n    }\n\n    if (!account.name || !account.picture) {\n      const did = account.sub\n\n      const profile = await this.actorStore\n        .read(did, async (store) => {\n          return store.record.getProfileRecord()\n        })\n        .catch((err) => {\n          dbLogger.error({ err }, 'Failed to get profile record')\n          return null // No need to propagate\n        })\n\n      if (profile) {\n        const { avatar, displayName } = profile\n\n        account.name ||= displayName\n        account.picture ||= avatar\n          ? this.imageUrlBuilder.build('avatar', did, avatar.ref.toString())\n          : undefined\n      }\n    }\n\n    return account\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/account-manager/scope-reference-getter.ts",
    "content": "import Redis from 'ioredis'\nimport { Agent, ComAtprotoTempDereferenceScope } from '@atproto/api'\nimport { DAY, backoffMs, retry } from '@atproto/common'\nimport { InvalidTokenError, OAuthScope } from '@atproto/oauth-provider'\nimport { UpstreamFailureError } from '@atproto/xrpc-server'\nimport { CachedGetter, GetterOptions } from '@atproto-labs/simple-store'\nimport { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'\nimport { SimpleStoreRedis } from '@atproto-labs/simple-store-redis'\nimport { oauthLogger } from '../logger.js'\n\nconst { InvalidScopeReferenceError } = ComAtprotoTempDereferenceScope\nconst PREFIX = 'ref:'\n\ntype ScopeReference = `${typeof PREFIX}${string}`\nconst isScopeReference = (scope?: OAuthScope): scope is ScopeReference =>\n  scope != null && scope.startsWith(PREFIX) && !scope.includes(' ')\n\nconst identity = <T>(value: T): T => value\n\nexport class ScopeReferenceGetter extends CachedGetter<\n  ScopeReference,\n  OAuthScope\n> {\n  constructor(\n    protected readonly entryway: Agent,\n    redis?: Redis,\n  ) {\n    super(\n      async (ref, options) => {\n        return retry(async () => this.fetchDereferencedScope(ref, options), {\n          maxRetries: 3,\n          getWaitMs: (n) => backoffMs(n, 250, 2000),\n          retryable: (err) =>\n            !options?.signal?.aborted &&\n            !(err instanceof InvalidScopeReferenceError),\n        })\n      },\n      redis\n        ? new SimpleStoreRedis(redis, {\n            // tradeoff between wasted memory usage (by no longer used scopes)\n            // and amount of requests to entryway:\n            ttl: 1 * DAY,\n\n            keyPrefix: `auth-scope-${PREFIX}`,\n            encode: identity,\n            decode: identity,\n          })\n        : new SimpleStoreMemory({ max: 1000 }),\n    )\n  }\n\n  protected async fetchDereferencedScope(\n    ref: ScopeReference,\n    opts?: GetterOptions,\n  ): Promise<OAuthScope> {\n    oauthLogger.info({ ref }, 'Fetching scope reference')\n\n    try {\n      const response = await this.entryway.com.atproto.temp.dereferenceScope(\n        { scope: ref },\n        {\n          signal: opts?.signal,\n          headers: opts?.noCache ? { 'Cache-Control': 'no-cache' } : undefined,\n        },\n      )\n\n      const { scope } = response.data\n\n      oauthLogger.info({ ref, scope }, 'Successfully fetched scope reference')\n\n      // @NOTE the part after `PREFIX` (in the input scope) is the CID of the\n      // scope string returned by entryway. Since there is a trust\n      // relationship with the entryway, we don't need to verify or enforce\n      // that here.\n\n      return scope\n    } catch (err) {\n      oauthLogger.error({ err, ref }, 'Failed to fetch scope reference')\n\n      throw err\n    }\n  }\n\n  async dereference(scope?: OAuthScope): Promise<undefined | OAuthScope> {\n    oauthLogger.debug({ scope }, 'Dereferencing scope')\n\n    if (!isScopeReference(scope)) return scope\n    return this.get(scope).catch(handleDereferenceError)\n  }\n}\n\nfunction handleDereferenceError(cause: unknown): never {\n  if (cause instanceof InvalidScopeReferenceError) {\n    // The scope reference cannot be found on the server.\n    // Consider the session as invalid, allowing entryway to\n    // re-build the scope as the user re-authenticates. This\n    // should never happen though.\n    throw InvalidTokenError.from(cause, 'DPoP')\n  }\n\n  throw new UpstreamFailureError(\n    'Failed to fetch token permissions',\n    undefined,\n    { cause },\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/actor-store-reader.ts",
    "content": "import { Keypair } from '@atproto/crypto'\nimport { ActorStoreResources } from './actor-store-resources'\nimport { ActorStoreTransactor } from './actor-store-transactor'\nimport { ActorDb } from './db'\nimport { PreferenceReader } from './preference/reader'\nimport { RecordReader } from './record/reader'\nimport { RepoReader } from './repo/reader'\n\nexport class ActorStoreReader {\n  public readonly repo: RepoReader\n  public readonly record: RecordReader\n  public readonly pref: PreferenceReader\n\n  constructor(\n    public readonly did: string,\n    protected readonly db: ActorDb,\n    protected readonly resources: ActorStoreResources,\n    public readonly keypair: () => Promise<Keypair>,\n  ) {\n    const blobstore = resources.blobstore(did)\n\n    this.repo = new RepoReader(db, blobstore)\n    this.record = new RecordReader(db)\n    this.pref = new PreferenceReader(db)\n\n    // Invoke \"keypair\" once. Also avoids leaking \"this\" as keypair context.\n    let keypairPromise: Promise<Keypair>\n    this.keypair = () => (keypairPromise ??= Promise.resolve().then(keypair))\n  }\n\n  async transact<T>(\n    fn: (fn: ActorStoreTransactor) => T | PromiseLike<T>,\n  ): Promise<T> {\n    const keypair = await this.keypair()\n    return this.db.transaction((dbTxn) => {\n      const store = new ActorStoreTransactor(\n        this.did,\n        dbTxn,\n        keypair,\n        this.resources,\n      )\n      return fn(store)\n    })\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/actor-store-resources.ts",
    "content": "import { BlobStore } from '@atproto/repo'\nimport { BackgroundQueue } from '../background'\n\nexport type ActorStoreResources = {\n  blobstore: (did: string) => BlobStore\n  backgroundQueue: BackgroundQueue\n  reservedKeyDir?: string\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/actor-store-transactor.ts",
    "content": "import { Keypair } from '@atproto/crypto'\nimport { ActorStoreResources } from './actor-store-resources'\nimport { ActorDb } from './db'\nimport { PreferenceTransactor } from './preference/transactor'\nimport { RecordTransactor } from './record/transactor'\nimport { RepoTransactor } from './repo/transactor'\n\nexport class ActorStoreTransactor {\n  public readonly record: RecordTransactor\n  public readonly repo: RepoTransactor\n  public readonly pref: PreferenceTransactor\n\n  constructor(\n    public readonly did: string,\n    protected readonly db: ActorDb,\n    protected readonly keypair: Keypair,\n    protected readonly resources: ActorStoreResources,\n  ) {\n    const blobstore = resources.blobstore(did)\n\n    this.record = new RecordTransactor(db, blobstore)\n    this.pref = new PreferenceTransactor(db)\n    this.repo = new RepoTransactor(\n      db,\n      blobstore,\n      did,\n      keypair,\n      resources.backgroundQueue,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/actor-store-writer.ts",
    "content": "import { ActorStoreTransactor } from './actor-store-transactor'\n\nexport class ActorStoreWriter extends ActorStoreTransactor {\n  async transact<T>(\n    fn: (fn: ActorStoreTransactor) => T | PromiseLike<T>,\n  ): Promise<T> {\n    return this.db.transaction((dbTxn) => {\n      const transactor = new ActorStoreTransactor(\n        this.did,\n        dbTxn,\n        this.keypair,\n        this.resources,\n      )\n      return fn(transactor)\n    })\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/actor-store.ts",
    "content": "import assert from 'node:assert'\nimport fs, { mkdir } from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileExists, readIfExists, rmIfExists } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { ExportableKeypair, Keypair } from '@atproto/crypto'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ActorStoreConfig } from '../config'\nimport { retrySqlite } from '../db'\nimport { DiskBlobStore } from '../disk-blobstore'\nimport { blobStoreLogger } from '../logger'\nimport { ActorStoreReader } from './actor-store-reader'\nimport { ActorStoreResources } from './actor-store-resources'\nimport { ActorStoreTransactor } from './actor-store-transactor'\nimport { ActorStoreWriter } from './actor-store-writer'\nimport { ActorDb, getDb, getMigrator } from './db'\n\nexport class ActorStore {\n  reservedKeyDir: string\n\n  constructor(\n    public cfg: ActorStoreConfig,\n    public resources: ActorStoreResources,\n  ) {\n    this.reservedKeyDir = path.join(cfg.directory, 'reserved_keys')\n  }\n\n  async getLocation(did: string) {\n    const didHash = await crypto.sha256Hex(did)\n    const directory = path.join(this.cfg.directory, didHash.slice(0, 2), did)\n    const dbLocation = path.join(directory, `store.sqlite`)\n    const keyLocation = path.join(directory, `key`)\n    return { directory, dbLocation, keyLocation }\n  }\n\n  async exists(did: string): Promise<boolean> {\n    const location = await this.getLocation(did)\n    return await fileExists(location.dbLocation)\n  }\n\n  async keypair(did: string): Promise<Keypair> {\n    const { keyLocation } = await this.getLocation(did)\n    const privKey = await fs.readFile(keyLocation)\n    return crypto.Secp256k1Keypair.import(privKey)\n  }\n\n  async openDb(did: string): Promise<ActorDb> {\n    const { dbLocation } = await this.getLocation(did)\n    const exists = await fileExists(dbLocation)\n    if (!exists) {\n      throw new InvalidRequestError('Repo not found', 'NotFound')\n    }\n\n    const db = getDb(dbLocation, this.cfg.disableWalAutoCheckpoint)\n\n    // run a simple select with retry logic to ensure the db is ready (not in wal recovery mode)\n    try {\n      await retrySqlite(() =>\n        db.db.selectFrom('repo_root').selectAll().execute(),\n      )\n    } catch (err) {\n      db.close()\n      throw err\n    }\n\n    return db\n  }\n\n  async read<T>(did: string, fn: (fn: ActorStoreReader) => T | PromiseLike<T>) {\n    const db = await this.openDb(did)\n    try {\n      const getKeypair = () => this.keypair(did)\n      return await fn(new ActorStoreReader(did, db, this.resources, getKeypair))\n    } finally {\n      db.close()\n    }\n  }\n\n  async transact<T>(\n    did: string,\n    fn: (fn: ActorStoreTransactor) => T | PromiseLike<T>,\n  ) {\n    const keypair = await this.keypair(did)\n    const db = await this.openDb(did)\n    try {\n      return await db.transaction((dbTxn) => {\n        return fn(new ActorStoreTransactor(did, dbTxn, keypair, this.resources))\n      })\n    } finally {\n      db.close()\n    }\n  }\n\n  async writeNoTransaction<T>(\n    did: string,\n    fn: (fn: ActorStoreWriter) => T | PromiseLike<T>,\n  ) {\n    const keypair = await this.keypair(did)\n    const db = await this.openDb(did)\n    try {\n      return await fn(new ActorStoreWriter(did, db, keypair, this.resources))\n    } finally {\n      db.close()\n    }\n  }\n\n  async create(did: string, keypair: ExportableKeypair) {\n    const { directory, dbLocation, keyLocation } = await this.getLocation(did)\n    // ensure subdir exists\n    await mkdir(directory, { recursive: true })\n    const exists = await fileExists(dbLocation)\n    if (exists) {\n      throw new InvalidRequestError('Repo already exists', 'AlreadyExists')\n    }\n    const privKey = await keypair.export()\n    await fs.writeFile(keyLocation, privKey)\n\n    const db: ActorDb = getDb(dbLocation, this.cfg.disableWalAutoCheckpoint)\n    try {\n      await db.ensureWal()\n      const migrator = getMigrator(db)\n      await migrator.migrateToLatestOrThrow()\n    } finally {\n      db.close()\n    }\n  }\n\n  async destroy(did: string) {\n    const blobstore = this.resources.blobstore(did)\n    if (blobstore instanceof DiskBlobStore) {\n      await blobstore.deleteAll()\n    } else {\n      const cids = await this.read(did, async (store) =>\n        store.repo.blob.getBlobCids(),\n      )\n      await blobstore.deleteMany(cids).catch((err) => {\n        blobStoreLogger.error('Failed to delete blobs', { did, cids, err })\n      })\n    }\n\n    const { directory } = await this.getLocation(did)\n    await rmIfExists(directory, true)\n  }\n\n  async reserveKeypair(did?: string): Promise<string> {\n    let keyLoc: string | undefined\n    if (did) {\n      assertSafePathPart(did)\n      keyLoc = path.join(this.reservedKeyDir, did)\n      const maybeKey = await loadKey(keyLoc)\n      if (maybeKey) {\n        return maybeKey.did()\n      }\n    }\n    const keypair = await crypto.Secp256k1Keypair.create({ exportable: true })\n    const keyDid = keypair.did()\n    keyLoc = keyLoc ?? path.join(this.reservedKeyDir, keyDid)\n    await mkdir(this.reservedKeyDir, { recursive: true })\n    await fs.writeFile(keyLoc, await keypair.export())\n    return keyDid\n  }\n\n  async getReservedKeypair(\n    signingKeyOrDid: string,\n  ): Promise<ExportableKeypair | undefined> {\n    return loadKey(path.join(this.reservedKeyDir, signingKeyOrDid))\n  }\n\n  async clearReservedKeypair(keyDid: string, did?: string) {\n    await rmIfExists(path.join(this.reservedKeyDir, keyDid))\n    if (did) {\n      await rmIfExists(path.join(this.reservedKeyDir, did))\n    }\n  }\n\n  async storePlcOp(did: string, op: Uint8Array) {\n    const { directory } = await this.getLocation(did)\n    const opLoc = path.join(directory, `did-op`)\n    await fs.writeFile(opLoc, op)\n  }\n\n  async getPlcOp(did: string): Promise<Uint8Array> {\n    const { directory } = await this.getLocation(did)\n    const opLoc = path.join(directory, `did-op`)\n    return await fs.readFile(opLoc)\n  }\n\n  async clearPlcOp(did: string) {\n    const { directory } = await this.getLocation(did)\n    const opLoc = path.join(directory, `did-op`)\n    await rmIfExists(opLoc)\n  }\n}\n\nconst loadKey = async (loc: string): Promise<ExportableKeypair | undefined> => {\n  const privKey = await readIfExists(loc)\n  if (!privKey) return undefined\n  return crypto.Secp256k1Keypair.import(privKey, { exportable: true })\n}\n\nfunction assertSafePathPart(part: string) {\n  const normalized = path.normalize(part)\n  assert(\n    part === normalized &&\n      !part.startsWith('.') &&\n      !part.includes('/') &&\n      !part.includes('\\\\'),\n    `unsafe path part: ${part}`,\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/blob/reader.ts",
    "content": "import stream from 'node:stream'\nimport { CID } from 'multiformats/cid'\nimport { BlobNotFoundError, BlobStore } from '@atproto/repo'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { countAll, countDistinct, notSoftDeletedClause } from '../../db/util'\nimport { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'\nimport { ActorDb } from '../db'\n\nexport class BlobReader {\n  constructor(\n    public db: ActorDb,\n    public blobstore: BlobStore,\n  ) {}\n\n  async getBlobMetadata(\n    cid: CID,\n  ): Promise<{ size: number; mimeType?: string }> {\n    const { ref } = this.db.db.dynamic\n    const found = await this.db.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('blob.cid', '=', cid.toString())\n      .where(notSoftDeletedClause(ref('blob')))\n      .executeTakeFirst()\n    if (!found) {\n      throw new InvalidRequestError('Blob not found')\n    }\n    return {\n      size: found.size,\n      mimeType: found.mimeType,\n    }\n  }\n\n  async getBlob(\n    cid: CID,\n  ): Promise<{ size: number; mimeType?: string; stream: stream.Readable }> {\n    const metadata = await this.getBlobMetadata(cid)\n    let blobStream\n    try {\n      blobStream = await this.blobstore.getStream(cid)\n    } catch (err) {\n      if (err instanceof BlobNotFoundError) {\n        throw new InvalidRequestError('Blob not found')\n      }\n      throw err\n    }\n    return {\n      ...metadata,\n      stream: blobStream,\n    }\n  }\n\n  async listBlobs(opts: {\n    since?: string\n    cursor?: string\n    limit: number\n  }): Promise<string[]> {\n    const { since, cursor, limit } = opts\n    let builder = this.db.db\n      .selectFrom('record_blob')\n      .select('blobCid')\n      .orderBy('blobCid', 'asc')\n      .groupBy('blobCid')\n      .limit(limit)\n    if (since) {\n      builder = builder\n        .innerJoin('record', 'record.uri', 'record_blob.recordUri')\n        .where('record.repoRev', '>', since)\n    }\n    if (cursor) {\n      builder = builder.where('blobCid', '>', cursor)\n    }\n    const res = await builder.execute()\n    return res.map((row) => row.blobCid)\n  }\n\n  async getBlobTakedownStatus(cid: CID): Promise<StatusAttr | null> {\n    const res = await this.db.db\n      .selectFrom('blob')\n      .select('takedownRef')\n      .where('cid', '=', cid.toString())\n      .executeTakeFirst()\n    if (!res) return null\n    return res.takedownRef\n      ? { applied: true, ref: res.takedownRef }\n      : { applied: false }\n  }\n\n  async getRecordsForBlob(cid: CID): Promise<string[]> {\n    const res = await this.db.db\n      .selectFrom('record_blob')\n      .where('blobCid', '=', cid.toString())\n      .selectAll()\n      .execute()\n    return res.map((row) => row.recordUri)\n  }\n\n  async getBlobsForRecord(recordUri: string): Promise<string[]> {\n    const res = await this.db.db\n      .selectFrom('blob')\n      .innerJoin('record_blob', 'record_blob.blobCid', 'blob.cid')\n      .where('recordUri', '=', recordUri)\n      .select('blob.cid')\n      .execute()\n    return res.map((row) => row.cid)\n  }\n\n  async blobCount(): Promise<number> {\n    const res = await this.db.db\n      .selectFrom('blob')\n      .select(countAll.as('count'))\n      .executeTakeFirst()\n    return res?.count ?? 0\n  }\n\n  async recordBlobCount(): Promise<number> {\n    const { ref } = this.db.db.dynamic\n    const res = await this.db.db\n      .selectFrom('record_blob')\n      .select(countDistinct(ref('blobCid')).as('count'))\n      .executeTakeFirst()\n    return res?.count ?? 0\n  }\n\n  async listMissingBlobs(opts: {\n    cursor?: string\n    limit: number\n  }): Promise<{ cid: string; recordUri: string }[]> {\n    const { cursor, limit } = opts\n    let builder = this.db.db\n      .selectFrom('record_blob')\n      .whereNotExists((qb) =>\n        qb\n          .selectFrom('blob')\n          .selectAll()\n          .whereRef('blob.cid', '=', 'record_blob.blobCid'),\n      )\n      .selectAll()\n      .orderBy('blobCid', 'asc')\n      .groupBy('blobCid')\n      .limit(limit)\n    if (cursor) {\n      builder = builder.where('blobCid', '>', cursor)\n    }\n    const res = await builder.execute()\n    return res.map((row) => ({\n      cid: row.blobCid,\n      recordUri: row.recordUri,\n    }))\n  }\n\n  async getBlobCids() {\n    const blobRows = await this.db.db.selectFrom('blob').select('cid').execute()\n    return blobRows.map((row) => CID.parse(row.cid))\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/blob/transactor.ts",
    "content": "import crypto from 'node:crypto'\nimport stream from 'node:stream'\nimport bytes from 'bytes'\nimport { fromStream as fileTypeFromStream } from 'file-type'\nimport { CID } from 'multiformats/cid'\nimport PQueue from 'p-queue'\nimport {\n  SECOND,\n  cloneStream,\n  sha256RawToCid,\n  streamSize,\n} from '@atproto/common'\nimport { BlobRef } from '@atproto/lexicon'\nimport { BlobNotFoundError, BlobStore, WriteOpAction } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { BackgroundQueue } from '../../background'\nimport { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'\nimport { blobStoreLogger as log } from '../../logger'\nimport { PreparedBlobRef, PreparedWrite } from '../../repo/types'\nimport { ActorDb, Blob as BlobTable } from '../db'\nimport { BlobReader } from './reader'\n\nexport type BlobMetadata = {\n  tempKey: string\n  size: number\n  cid: CID\n  mimeType: string\n}\n\nexport class BlobTransactor extends BlobReader {\n  constructor(\n    public db: ActorDb,\n    public blobstore: BlobStore,\n    public backgroundQueue: BackgroundQueue,\n  ) {\n    super(db, blobstore)\n  }\n\n  async insertBlobs(recordUri: string, blobs: Iterable<BlobRef>) {\n    const values = Array.from(blobs, (cid) => ({\n      recordUri,\n      blobCid: cid.ref.toString(),\n    }))\n\n    if (values.length) {\n      await this.db.db\n        .insertInto('record_blob')\n        .values(values)\n        .onConflict((oc) => oc.doNothing())\n        .execute()\n    }\n  }\n\n  async uploadBlobAndGetMetadata(\n    userSuggestedMime: string,\n    blobStream: stream.Readable,\n  ): Promise<BlobMetadata> {\n    const [tempKey, size, sha256, sniffedMime] = await Promise.all([\n      this.blobstore.putTemp(cloneStream(blobStream)),\n      streamSize(cloneStream(blobStream)),\n      sha256Stream(cloneStream(blobStream)),\n      mimeTypeFromStream(cloneStream(blobStream)),\n    ])\n\n    const cid = sha256RawToCid(sha256)\n    const mimeType = sniffedMime || userSuggestedMime\n\n    return {\n      tempKey,\n      size,\n      cid,\n      mimeType,\n    }\n  }\n\n  async trackUntetheredBlob(metadata: BlobMetadata) {\n    const { tempKey, size, cid, mimeType } = metadata\n    const found = await this.db.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', cid.toString())\n      .executeTakeFirst()\n    if (found?.takedownRef) {\n      throw new InvalidRequestError('Blob has been takendown, cannot re-upload')\n    }\n\n    await this.db.db\n      .insertInto('blob')\n      .values({\n        cid: cid.toString(),\n        mimeType,\n        size,\n        tempKey,\n        createdAt: new Date().toISOString(),\n      })\n      .onConflict((oc) =>\n        oc\n          .column('cid')\n          .doUpdateSet({ tempKey })\n          .where('blob.tempKey', 'is not', null),\n      )\n      .execute()\n    return new BlobRef(cid, mimeType, size)\n  }\n\n  async processWriteBlobs(rev: string, writes: PreparedWrite[]) {\n    await this.deleteDereferencedBlobs(writes)\n\n    const ac = new AbortController()\n\n    // Limit the number of parallel requests made to the BlobStore by using a\n    // a queue with concurrency management.\n    type Task = () => Promise<void>\n    const tasks: Task[] = []\n\n    for (const write of writes) {\n      if (isCreate(write) || isUpdate(write)) {\n        for (const blob of write.blobs) {\n          tasks.push(async () => {\n            if (ac.signal.aborted) return\n            await this.associateBlob(blob, write.uri)\n            await this.verifyBlobAndMakePermanent(blob, ac.signal)\n          })\n        }\n      }\n    }\n\n    try {\n      const queue = new PQueue({\n        concurrency: 20,\n        // The blob store should already limit the time of every operation. We\n        // add a timeout here as an extra precaution.\n        timeout: 60 * SECOND,\n        throwOnTimeout: true,\n      })\n\n      // Will reject as soon as any task fails, causing the \"finally\" block\n      // below to run, aborting every other pending tasks.\n      await queue.addAll(tasks)\n    } finally {\n      ac.abort()\n    }\n  }\n\n  async updateBlobTakedownStatus(cid: CID, takedown: StatusAttr) {\n    const takedownRef = takedown.applied\n      ? takedown.ref ?? new Date().toISOString()\n      : null\n    await this.db.db\n      .updateTable('blob')\n      .set({ takedownRef })\n      .where('cid', '=', cid.toString())\n      .executeTakeFirst()\n\n    try {\n      // @NOTE find a way to not perform i/o operations during the transaction\n      // (typically by using a state in the \"blob\" table, and another process to\n      // handle the actual i/o)\n      if (takedown.applied) {\n        await this.blobstore.quarantine(cid)\n      } else {\n        await this.blobstore.unquarantine(cid)\n      }\n    } catch (err) {\n      if (!(err instanceof BlobNotFoundError)) {\n        log.error(\n          { err, cid: cid.toString() },\n          'could not update blob takedown status',\n        )\n\n        throw err\n      }\n    }\n  }\n\n  async deleteDereferencedBlobs(\n    writes: PreparedWrite[],\n    skipBlobStore?: boolean,\n  ) {\n    const deletes = writes.filter(isDelete)\n    const updates = writes.filter(isUpdate)\n    const uris = [...deletes, ...updates].map((w) => w.uri.toString())\n    if (uris.length === 0) return\n\n    const deletedRepoBlobs = await this.db.db\n      .deleteFrom('record_blob')\n      .where('recordUri', 'in', uris)\n      .returning('blobCid')\n      .execute()\n    if (deletedRepoBlobs.length === 0) return\n\n    const deletedRepoBlobCids = deletedRepoBlobs.map((row) => row.blobCid)\n    const duplicateCids = await this.db.db\n      .selectFrom('record_blob')\n      .where('blobCid', 'in', deletedRepoBlobCids)\n      .select('blobCid')\n      .execute()\n\n    const newBlobCids = writes\n      .filter((w) => isUpdate(w) || isCreate(w))\n      .flatMap((w) => w.blobs.map((b) => b.cid.toString()))\n\n    const cidsToKeep = [\n      ...newBlobCids,\n      ...duplicateCids.map((row) => row.blobCid),\n    ]\n\n    const cidsToDelete = deletedRepoBlobCids.filter(\n      (cid) => !cidsToKeep.includes(cid),\n    )\n    if (cidsToDelete.length === 0) return\n\n    await this.db.db\n      .deleteFrom('blob')\n      .where('cid', 'in', cidsToDelete)\n      .execute()\n\n    if (!skipBlobStore) {\n      this.db.onCommit(() => {\n        this.backgroundQueue.add(async () => {\n          try {\n            const cids = cidsToDelete.map((cid) => CID.parse(cid))\n            await this.blobstore.deleteMany(cids)\n          } catch (err) {\n            log.error(\n              { err, cids: cidsToDelete },\n              'could not delete blobs from blobstore',\n            )\n          }\n        })\n      })\n    }\n  }\n\n  async verifyBlobAndMakePermanent(\n    blob: PreparedBlobRef,\n    signal?: AbortSignal,\n  ): Promise<void> {\n    const found = await this.db.db\n      .selectFrom('blob')\n      .select(['tempKey', 'size', 'mimeType'])\n      .where('cid', '=', blob.cid.toString())\n      .where('takedownRef', 'is', null)\n      .executeTakeFirst()\n\n    signal?.throwIfAborted()\n\n    if (!found) {\n      throw new InvalidRequestError(\n        `Could not find blob: ${blob.cid.toString()}`,\n        'BlobNotFound',\n      )\n    }\n\n    if (found.tempKey) {\n      verifyBlob(blob, found)\n\n      // @NOTE it is less than ideal to perform async (i/o) operations during a\n      // transaction. Especially since there have been instances of the actor-db\n      // being locked, requiring to kick the processes.\n\n      // The better solution would be to update the blob state in the database\n      // (e.g. \"makeItPermanent\") and to process those updates outside of the\n      // transaction.\n\n      await this.blobstore\n        .makePermanent(found.tempKey, blob.cid)\n        .catch((err) => {\n          log.error(\n            { err, cid: blob.cid.toString() },\n            'could not make blob permanent',\n          )\n\n          throw err\n        })\n\n      signal?.throwIfAborted()\n\n      await this.db.db\n        .updateTable('blob')\n        .set({ tempKey: null })\n        .where('tempKey', '=', found.tempKey)\n        .execute()\n    }\n  }\n\n  async insertBlobMetadata(blob: PreparedBlobRef): Promise<void> {\n    await this.db.db\n      .insertInto('blob')\n      .values({\n        cid: blob.cid.toString(),\n        mimeType: blob.mimeType,\n        size: blob.size,\n        createdAt: new Date().toISOString(),\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  }\n\n  async associateBlob(blob: PreparedBlobRef, recordUri: AtUri): Promise<void> {\n    await this.db.db\n      .insertInto('record_blob')\n      .values({\n        blobCid: blob.cid.toString(),\n        recordUri: recordUri.toString(),\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  }\n}\n\nexport class CidNotFound extends Error {\n  cid: CID\n  constructor(cid: CID) {\n    super(`cid not found: ${cid.toString()}`)\n    this.cid = cid\n  }\n}\n\nasync function sha256Stream(toHash: stream.Readable): Promise<Uint8Array> {\n  const hash = crypto.createHash('sha256')\n  try {\n    for await (const chunk of toHash) {\n      hash.write(chunk)\n    }\n  } catch (err) {\n    hash.end()\n    throw err\n  }\n  hash.end()\n  return hash.read()\n}\n\nasync function mimeTypeFromStream(\n  blobStream: stream.Readable,\n): Promise<string | undefined> {\n  const fileType = await fileTypeFromStream(blobStream)\n  blobStream.destroy()\n  return fileType?.mime\n}\n\nfunction acceptedMime(mime: string, accepted: string[]): boolean {\n  if (accepted.includes('*/*')) return true\n  const globs = accepted.filter((a) => a.endsWith('/*'))\n  for (const glob of globs) {\n    const [start] = glob.split('/')\n    if (mime.startsWith(`${start}/`)) {\n      return true\n    }\n  }\n  return accepted.includes(mime)\n}\n\nfunction verifyBlob(\n  blob: PreparedBlobRef,\n  found: Pick<BlobTable, 'size' | 'mimeType'>,\n) {\n  const throwInvalid = (msg: string, errName = 'InvalidBlob') => {\n    throw new InvalidRequestError(msg, errName)\n  }\n  if (blob.constraints.maxSize && found.size > blob.constraints.maxSize) {\n    throwInvalid(\n      `This file is too large. It is ${bytes.format(\n        found.size,\n      )} but the maximum size is ${bytes.format(blob.constraints.maxSize)}.`,\n      'BlobTooLarge',\n    )\n  }\n  if (blob.mimeType !== found.mimeType) {\n    throwInvalid(\n      `Referenced Mimetype does not match stored blob. Expected: ${found.mimeType}, Got: ${blob.mimeType}`,\n      'InvalidMimeType',\n    )\n  }\n  if (\n    blob.constraints.accept &&\n    !acceptedMime(blob.mimeType, blob.constraints.accept)\n  ) {\n    throwInvalid(\n      `Wrong type of file. It is ${blob.mimeType} but it must match ${blob.constraints.accept}.`,\n      'InvalidMimeType',\n    )\n  }\n}\n\nfunction isCreate(write: PreparedWrite) {\n  return write.action === WriteOpAction.Create\n}\nfunction isUpdate(write: PreparedWrite) {\n  return write.action === WriteOpAction.Update\n}\nfunction isDelete(write: PreparedWrite) {\n  return write.action === WriteOpAction.Delete\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/index.ts",
    "content": "import { Database, Migrator } from '../../db'\nimport migrations from './migrations'\nimport { DatabaseSchema } from './schema'\nexport * from './schema'\n\nexport type ActorDb = Database<DatabaseSchema>\n\nexport const getDb = (\n  location: string,\n  disableWalAutoCheckpoint = false,\n): ActorDb => {\n  const pragmas: Record<string, string> = disableWalAutoCheckpoint\n    ? { wal_autocheckpoint: '0' }\n    : {}\n  return Database.sqlite(location, { pragmas })\n}\n\nexport const getMigrator = (db: Database<DatabaseSchema>) => {\n  return new Migrator(db.db, migrations)\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/migrations/001-init.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('repo_root')\n    .addColumn('did', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('rev', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .execute()\n\n  await db.schema\n    .createTable('repo_block')\n    .addColumn('cid', 'varchar', (col) => col.primaryKey())\n    .addColumn('repoRev', 'varchar', (col) => col.notNull())\n    .addColumn('size', 'integer', (col) => col.notNull())\n    .addColumn('content', 'blob', (col) => col.notNull())\n    .execute()\n\n  await db.schema\n    .createIndex('repo_block_repo_rev_idx')\n    .on('repo_block')\n    .columns(['repoRev', 'cid'])\n    .execute()\n\n  await db.schema\n    .createTable('record')\n    .addColumn('uri', 'varchar', (col) => col.primaryKey())\n    .addColumn('cid', 'varchar', (col) => col.notNull())\n    .addColumn('collection', 'varchar', (col) => col.notNull())\n    .addColumn('rkey', 'varchar', (col) => col.notNull())\n    .addColumn('repoRev', 'varchar', (col) => col.notNull())\n    .addColumn('indexedAt', 'varchar', (col) => col.notNull())\n    .addColumn('takedownRef', 'varchar')\n    .execute()\n  await db.schema\n    .createIndex('record_cid_idx')\n    .on('record')\n    .column('cid')\n    .execute()\n  await db.schema\n    .createIndex('record_collection_idx')\n    .on('record')\n    .column('collection')\n    .execute()\n  await db.schema\n    .createIndex('record_repo_rev_idx')\n    .on('record')\n    .column('repoRev')\n    .execute()\n\n  await db.schema\n    .createTable('blob')\n    .addColumn('cid', 'varchar', (col) => col.primaryKey())\n    .addColumn('mimeType', 'varchar', (col) => col.notNull())\n    .addColumn('size', 'integer', (col) => col.notNull())\n    .addColumn('tempKey', 'varchar')\n    .addColumn('width', 'integer')\n    .addColumn('height', 'integer')\n    .addColumn('createdAt', 'varchar', (col) => col.notNull())\n    .addColumn('takedownRef', 'varchar')\n    .execute()\n  await db.schema\n    .createIndex('blob_tempkey_idx')\n    .on('blob')\n    .column('tempKey')\n    .execute()\n\n  await db.schema\n    .createTable('record_blob')\n    .addColumn('blobCid', 'varchar', (col) => col.notNull())\n    .addColumn('recordUri', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint(`record_blob_pkey`, ['blobCid', 'recordUri'])\n    .execute()\n\n  await db.schema\n    .createTable('backlink')\n    .addColumn('uri', 'varchar', (col) => col.notNull())\n    .addColumn('path', 'varchar', (col) => col.notNull())\n    .addColumn('linkTo', 'varchar', (col) => col.notNull())\n    .addPrimaryKeyConstraint('backlinks_pkey', ['uri', 'path'])\n    .execute()\n  await db.schema\n    .createIndex('backlink_link_to_idx')\n    .on('backlink')\n    .columns(['path', 'linkTo'])\n    .execute()\n\n  await db.schema\n    .createTable('account_pref')\n    .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey())\n    .addColumn('name', 'varchar', (col) => col.notNull())\n    .addColumn('valueJson', 'text', (col) => col.notNull())\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('account_pref').execute()\n  await db.schema.dropTable('backlink').execute()\n  await db.schema.dropTable('record_blob').execute()\n  await db.schema.dropTable('blob').execute()\n  await db.schema.dropTable('record').execute()\n  await db.schema.dropTable('repo_block').execute()\n  await db.schema.dropTable('repo_root').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/migrations/index.ts",
    "content": "import * as init from './001-init'\n\nexport default {\n  '001': init,\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/account-pref.ts",
    "content": "import { GeneratedAlways } from 'kysely'\n\nexport interface AccountPref {\n  id: GeneratedAlways<number>\n  name: string\n  valueJson: string // json\n}\n\nexport const tableName = 'account_pref'\n\nexport type PartialDB = { [tableName]: AccountPref }\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/backlink.ts",
    "content": "export interface Backlink {\n  uri: string\n  path: string\n  linkTo: string\n}\n\nexport const tableName = 'backlink'\n\nexport type PartialDB = { [tableName]: Backlink }\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/blob.ts",
    "content": "export interface Blob {\n  cid: string\n  mimeType: string\n  size: number\n  tempKey: string | null\n  // width: number | null  // @TODO: actually drop these columns from the db\n  // height: number | null\n  createdAt: string\n  takedownRef: string | null\n}\n\nexport const tableName = 'blob'\n\nexport type PartialDB = { [tableName]: Blob }\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/index.ts",
    "content": "import * as accountPref from './account-pref'\nimport * as backlink from './backlink'\nimport * as blob from './blob'\nimport * as record from './record'\nimport * as recordBlob from './record-blob'\nimport * as repoBlock from './repo-block'\nimport * as repoRoot from './repo-root'\n\nexport type DatabaseSchema = accountPref.PartialDB &\n  repoRoot.PartialDB &\n  record.PartialDB &\n  backlink.PartialDB &\n  repoBlock.PartialDB &\n  blob.PartialDB &\n  recordBlob.PartialDB\n\nexport type { AccountPref } from './account-pref'\nexport type { RepoRoot } from './repo-root'\nexport type { Record } from './record'\nexport type { Backlink } from './backlink'\nexport type { RepoBlock } from './repo-block'\nexport type { Blob } from './blob'\nexport type { RecordBlob } from './record-blob'\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/record-blob.ts",
    "content": "export interface RecordBlob {\n  blobCid: string\n  recordUri: string\n}\n\nexport const tableName = 'record_blob'\n\nexport type PartialDB = { [tableName]: RecordBlob }\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/record.ts",
    "content": "// @NOTE also used by app-view (moderation)\nexport interface Record {\n  uri: string\n  cid: string\n  collection: string\n  rkey: string\n  repoRev: string\n  indexedAt: string\n  takedownRef: string | null\n}\n\nexport const tableName = 'record'\n\nexport type PartialDB = { [tableName]: Record }\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/repo-block.ts",
    "content": "export interface RepoBlock {\n  cid: string\n  repoRev: string\n  size: number\n  content: Uint8Array\n}\n\nexport const tableName = 'repo_block'\n\nexport type PartialDB = { [tableName]: RepoBlock }\n"
  },
  {
    "path": "packages/pds/src/actor-store/db/schema/repo-root.ts",
    "content": "export interface RepoRoot {\n  did: string\n  cid: string\n  rev: string\n  indexedAt: string\n}\n\nconst tableName = 'repo_root'\n\nexport type PartialDB = { [tableName]: RepoRoot }\n"
  },
  {
    "path": "packages/pds/src/actor-store/migrate.ts",
    "content": "import { sql } from 'kysely'\nimport PQueue from 'p-queue'\nimport { AppContext } from '../context'\n\nexport const forEachActorStore = async (\n  ctx: AppContext,\n  opts: { concurrency?: number },\n  fn: (ctx: AppContext, did: string) => Promise<string>,\n) => {\n  const { concurrency = 1 } = opts\n\n  const queue = new PQueue({ concurrency })\n  const actorQb = ctx.accountManager.db.db\n    .selectFrom('actor')\n    .selectAll()\n    .limit(2 * concurrency)\n  let cursor: { createdAt: string; did: string } | undefined\n  do {\n    const actors = cursor\n      ? await actorQb\n          .where(\n            sql`(\"createdAt\", \"did\")`,\n            '>',\n            sql`(${cursor.createdAt}, ${cursor.did})`,\n          )\n          .execute()\n      : await actorQb.execute()\n    queue.addAll(\n      actors.map(({ did }) => {\n        return () => fn(ctx, did)\n      }),\n    )\n    cursor = actors.at(-1)\n    await queue.onEmpty() // wait for all remaining items to be in process, then move on to next page\n  } while (cursor)\n\n  // finalize remaining work\n  await queue.onIdle()\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/preference/reader.ts",
    "content": "import { ActorDb } from '../db'\nimport {\n  DECLARED_AGE_PREF,\n  PERSONAL_DETAILS_PREF,\n  PrefAllowedOptions,\n  getAgeFromDatestring,\n  prefAllowed,\n} from './util'\n\nexport class PreferenceReader {\n  constructor(public db: ActorDb) {}\n\n  async getPreferences(\n    namespace: string,\n    opts: PrefAllowedOptions,\n  ): Promise<AccountPreference[]> {\n    const prefsRes = await this.db.db\n      .selectFrom('account_pref')\n      .orderBy('id')\n      .selectAll()\n      .execute()\n\n    const prefs = prefsRes\n      .filter((pref) => !namespace || prefMatchNamespace(namespace, pref.name))\n      .map((pref) => JSON.parse(pref.valueJson) as AccountPreference)\n    const personalDetailsPref = prefs.find(\n      (pref) => pref.$type === PERSONAL_DETAILS_PREF,\n    )\n\n    if (personalDetailsPref) {\n      if (typeof personalDetailsPref.birthDate === 'string') {\n        const age = getAgeFromDatestring(personalDetailsPref.birthDate)\n        const declaredAgePref: AccountPreference = {\n          $type: DECLARED_AGE_PREF,\n          isOverAge13: age >= 13,\n          isOverAge16: age >= 16,\n          isOverAge18: age >= 18,\n        }\n        prefs.push(declaredAgePref)\n      }\n    }\n\n    return prefs.filter((pref) => prefAllowed(pref.$type, opts))\n  }\n}\n\nexport type AccountPreference = Record<string, unknown> & { $type: string }\n\nexport const prefMatchNamespace = (namespace: string, fullname: string) => {\n  return fullname === namespace || fullname.startsWith(`${namespace}.`)\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/preference/transactor.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport {\n  AccountPreference,\n  PreferenceReader,\n  prefMatchNamespace,\n} from './reader'\nimport { PrefAllowedOptions, isReadOnlyPref, prefAllowed } from './util'\n\nexport class PreferenceTransactor extends PreferenceReader {\n  async putPreferences(\n    values: AccountPreference[],\n    namespace: string,\n    opts: PrefAllowedOptions,\n  ): Promise<void> {\n    this.db.assertTransaction()\n    if (!values.every((value) => prefMatchNamespace(namespace, value.$type))) {\n      throw new InvalidRequestError(\n        `Some preferences are not in the ${namespace} namespace`,\n      )\n    }\n    const forbiddenPrefs = values.filter((val) => !prefAllowed(val.$type, opts))\n    if (forbiddenPrefs.length > 0) {\n      throw new InvalidRequestError(\n        `Do not have authorization to set preferences: ${forbiddenPrefs.map((p) => p.$type).join(', ')}`,\n      )\n    }\n    // get all current prefs for user and prep new pref rows\n    const allPrefs = await this.db.db\n      .selectFrom('account_pref')\n      .select(['id', 'name'])\n      .execute()\n    const putPrefs = values\n      .filter((value) => !isReadOnlyPref(value.$type))\n      .map((value) => {\n        return {\n          name: value.$type,\n          valueJson: JSON.stringify(value),\n        }\n      })\n    const allPrefIdsInNamespace = allPrefs\n      .filter((pref) => prefMatchNamespace(namespace, pref.name))\n      .filter((pref) => prefAllowed(pref.name, opts))\n      .map((pref) => pref.id)\n    // replace all prefs in given namespace\n    if (allPrefIdsInNamespace.length) {\n      await this.db.db\n        .deleteFrom('account_pref')\n        .where('id', 'in', allPrefIdsInNamespace)\n        .execute()\n    }\n    if (putPrefs.length) {\n      await this.db.db.insertInto('account_pref').values(putPrefs).execute()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/preference/util.ts",
    "content": "export const PERSONAL_DETAILS_PREF = 'app.bsky.actor.defs#personalDetailsPref'\nexport const DECLARED_AGE_PREF = 'app.bsky.actor.defs#declaredAgePref'\n\nconst FULL_ACCESS_ONLY_PREFS = new Set([PERSONAL_DETAILS_PREF])\n\nexport type PrefAllowedOptions = {\n  hasAccessFull?: boolean\n}\n\nexport function prefAllowed(\n  prefType: string,\n  options?: PrefAllowedOptions,\n): boolean {\n  if (options?.hasAccessFull === true) {\n    return true\n  }\n\n  return !FULL_ACCESS_ONLY_PREFS.has(prefType)\n}\n\nconst READ_ONLY_PREFS = new Set([DECLARED_AGE_PREF])\n\nexport function isReadOnlyPref(prefType: string) {\n  return READ_ONLY_PREFS.has(prefType)\n}\n\nexport function getAgeFromDatestring(birthDate: string): number {\n  const bday = new Date(birthDate)\n  const today = new Date()\n  let age = today.getFullYear() - bday.getFullYear()\n  const m = today.getMonth() - bday.getMonth()\n  if (m < 0 || (m === 0 && today.getDate() < bday.getDate())) {\n    age--\n  }\n  return age\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/record/reader.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { CidSet, cborToLexRecord, formatDataKey } from '@atproto/repo'\nimport * as syntax from '@atproto/syntax'\nimport { AtUri, ensureValidAtUri } from '@atproto/syntax'\nimport { countAll, notSoftDeletedClause } from '../../db/util'\nimport { ids } from '../../lexicon/lexicons'\nimport { Record as ProfileRecord } from '../../lexicon/types/app/bsky/actor/profile'\nimport { Record as PostRecord } from '../../lexicon/types/app/bsky/feed/post'\nimport { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'\nimport { LocalRecords } from '../../read-after-write/types'\nimport { ActorDb, Backlink } from '../db'\n\nexport type RecordDescript = {\n  uri: string\n  path: string\n  cid: CID\n}\n\nexport class RecordReader {\n  constructor(public db: ActorDb) {}\n\n  async recordCount(): Promise<number> {\n    const res = await this.db.db\n      .selectFrom('record')\n      .select(countAll.as('count'))\n      .executeTakeFirst()\n    return res?.count ?? 0\n  }\n\n  async listAll(): Promise<RecordDescript[]> {\n    const records: RecordDescript[] = []\n    let cursor: string | undefined = ''\n    while (cursor !== undefined) {\n      const res = await this.db.db\n        .selectFrom('record')\n        .select(['uri', 'cid'])\n        .where('uri', '>', cursor)\n        .orderBy('uri', 'asc')\n        .limit(1000)\n        .execute()\n      for (const row of res) {\n        const parsed = new AtUri(row.uri)\n        records.push({\n          uri: row.uri,\n          path: formatDataKey(parsed.collection, parsed.rkey),\n          cid: CID.parse(row.cid),\n        })\n      }\n      cursor = res.at(-1)?.uri\n    }\n    return records\n  }\n\n  async listCollections(): Promise<string[]> {\n    const collections = await this.db.db\n      .selectFrom('record')\n      .select('collection')\n      .groupBy('collection')\n      .execute()\n\n    return collections.map((row) => row.collection)\n  }\n\n  async listRecordsForCollection(opts: {\n    collection: string\n    limit: number\n    reverse: boolean\n    cursor?: string\n    rkeyStart?: string\n    rkeyEnd?: string\n    includeSoftDeleted?: boolean\n  }): Promise<{ uri: string; cid: string; value: Record<string, unknown> }[]> {\n    const {\n      collection,\n      limit,\n      reverse,\n      cursor,\n      rkeyStart,\n      rkeyEnd,\n      includeSoftDeleted = false,\n    } = opts\n\n    const { ref } = this.db.db.dynamic\n    let builder = this.db.db\n      .selectFrom('record')\n      .innerJoin('repo_block', 'repo_block.cid', 'record.cid')\n      .where('record.collection', '=', collection)\n      .if(!includeSoftDeleted, (qb) =>\n        qb.where(notSoftDeletedClause(ref('record'))),\n      )\n      .orderBy('record.rkey', reverse ? 'asc' : 'desc')\n      .limit(limit)\n      .selectAll()\n\n    // prioritize cursor but fall back to soon-to-be-depcreated rkey start/end\n    if (cursor !== undefined) {\n      if (reverse) {\n        builder = builder.where('record.rkey', '>', cursor)\n      } else {\n        builder = builder.where('record.rkey', '<', cursor)\n      }\n    } else {\n      if (rkeyStart !== undefined) {\n        builder = builder.where('record.rkey', '>', rkeyStart)\n      }\n      if (rkeyEnd !== undefined) {\n        builder = builder.where('record.rkey', '<', rkeyEnd)\n      }\n    }\n    const res = await builder.execute()\n    return res.map((row) => {\n      return {\n        uri: row.uri,\n        cid: row.cid,\n        value: cborToLexRecord(row.content),\n      }\n    })\n  }\n\n  async getRecord(\n    uri: AtUri,\n    cid: string | null,\n    includeSoftDeleted = false,\n  ): Promise<{\n    uri: string\n    cid: string\n    value: Record<string, unknown>\n    indexedAt: string\n    takedownRef: string | null\n  } | null> {\n    const { ref } = this.db.db.dynamic\n    let builder = this.db.db\n      .selectFrom('record')\n      .innerJoin('repo_block', 'repo_block.cid', 'record.cid')\n      .where('record.uri', '=', uri.toString())\n      .selectAll()\n      .if(!includeSoftDeleted, (qb) =>\n        qb.where(notSoftDeletedClause(ref('record'))),\n      )\n    if (cid) {\n      builder = builder.where('record.cid', '=', cid)\n    }\n    const record = await builder.executeTakeFirst()\n    if (!record) return null\n    return {\n      uri: record.uri,\n      cid: record.cid,\n      value: cborToLexRecord(record.content),\n      indexedAt: record.indexedAt,\n      takedownRef: record.takedownRef ? record.takedownRef.toString() : null,\n    }\n  }\n\n  async hasRecord(\n    uri: AtUri,\n    cid: string | null,\n    includeSoftDeleted = false,\n  ): Promise<boolean> {\n    const { ref } = this.db.db.dynamic\n    let builder = this.db.db\n      .selectFrom('record')\n      .select('uri')\n      .where('record.uri', '=', uri.toString())\n      .if(!includeSoftDeleted, (qb) =>\n        qb.where(notSoftDeletedClause(ref('record'))),\n      )\n    if (cid) {\n      builder = builder.where('record.cid', '=', cid)\n    }\n    const record = await builder.executeTakeFirst()\n    return !!record\n  }\n\n  async getRecordTakedownStatus(uri: AtUri): Promise<StatusAttr | null> {\n    const res = await this.db.db\n      .selectFrom('record')\n      .select('takedownRef')\n      .where('uri', '=', uri.toString())\n      .executeTakeFirst()\n    if (!res) return null\n    return res.takedownRef\n      ? { applied: true, ref: res.takedownRef }\n      : { applied: false }\n  }\n\n  async getCurrentRecordCid(uri: AtUri): Promise<CID | null> {\n    const res = await this.db.db\n      .selectFrom('record')\n      .select('cid')\n      .where('uri', '=', uri.toString())\n      .executeTakeFirst()\n    return res ? CID.parse(res.cid) : null\n  }\n\n  async getRecordBacklinks(opts: {\n    collection: string\n    path: string\n    linkTo: string\n  }) {\n    const { collection, path, linkTo } = opts\n    return await this.db.db\n      .selectFrom('record')\n      .innerJoin('backlink', 'backlink.uri', 'record.uri')\n      .where('backlink.path', '=', path)\n      .where('backlink.linkTo', '=', linkTo)\n      .where('record.collection', '=', collection)\n      .selectAll('record')\n      .execute()\n  }\n\n  // @NOTE this logic is a placeholder until we allow users to specify these constraints themselves.\n  // Ensures that we don't end-up with duplicate likes, reposts, and follows from race conditions.\n\n  async getBacklinkConflicts(uri: AtUri, record: RepoRecord): Promise<AtUri[]> {\n    const conflicts: AtUri[] = []\n\n    for (const backlink of getBacklinks(uri, record)) {\n      const backlinks = await this.getRecordBacklinks({\n        collection: uri.collection,\n        path: backlink.path,\n        linkTo: backlink.linkTo,\n      })\n\n      for (const { rkey } of backlinks) {\n        conflicts.push(AtUri.make(uri.hostname, uri.collection, rkey))\n      }\n    }\n\n    return conflicts\n  }\n\n  async listExistingBlocks(): Promise<CidSet> {\n    const cids = new CidSet()\n    let cursor: string | undefined = ''\n    while (cursor !== undefined) {\n      const res = await this.db.db\n        .selectFrom('repo_block')\n        .select('cid')\n        .where('cid', '>', cursor)\n        .orderBy('cid', 'asc')\n        .limit(1000)\n        .execute()\n      for (const row of res) {\n        cids.add(CID.parse(row.cid))\n      }\n      cursor = res.at(-1)?.cid\n    }\n    return cids\n  }\n\n  async getProfileRecord() {\n    const row = await this.db.db\n      .selectFrom('record')\n      .leftJoin('repo_block', 'repo_block.cid', 'record.cid')\n      .where('record.collection', '=', ids.AppBskyActorProfile)\n      .where('record.rkey', '=', 'self')\n      .selectAll()\n      .executeTakeFirst()\n\n    if (!row?.content) return null\n\n    return cborToLexRecord(row.content) as ProfileRecord\n  }\n\n  async getRecordsSinceRev(rev: string): Promise<LocalRecords> {\n    const result: LocalRecords = { count: 0, profile: null, posts: [] }\n\n    const res = await this.db.db\n      .selectFrom('record')\n      .innerJoin('repo_block', 'repo_block.cid', 'record.cid')\n      .select([\n        'repo_block.content',\n        'uri',\n        'repo_block.cid',\n        'record.indexedAt',\n      ])\n      .where('record.repoRev', '>', rev)\n      .limit(10)\n      .orderBy('record.repoRev', 'asc')\n      .execute()\n\n    // sanity check to ensure that the clock received is not before _all_ local records (for instance in case of account migration)\n    if (res.length > 0) {\n      const sanityCheckRes = await this.db.db\n        .selectFrom('record')\n        .selectAll()\n        .where('record.repoRev', '<=', rev)\n        .limit(1)\n        .executeTakeFirst()\n\n      if (!sanityCheckRes) {\n        return result\n      }\n    }\n\n    for (const cur of res) {\n      result.count++\n\n      const uri = new AtUri(cur.uri)\n      if (uri.collection === ids.AppBskyActorProfile && uri.rkey === 'self') {\n        result.profile = {\n          uri,\n          cid: CID.parse(cur.cid),\n          indexedAt: cur.indexedAt,\n          record: cborToLexRecord(cur.content) as ProfileRecord,\n        }\n      } else if (uri.collection === ids.AppBskyFeedPost) {\n        result.posts.push({\n          uri,\n          cid: CID.parse(cur.cid),\n          indexedAt: cur.indexedAt,\n          record: cborToLexRecord(cur.content) as PostRecord,\n        })\n      }\n    }\n\n    return result\n  }\n}\n\n// @NOTE in the future this can be replaced with a more generic routine that pulls backlinks based on lex docs.\n// For now we just want to ensure we're tracking links from follows, blocks, likes, and reposts.\n\nexport const getBacklinks = (uri: AtUri, record: RepoRecord): Backlink[] => {\n  if (\n    record?.['$type'] === ids.AppBskyGraphFollow ||\n    record?.['$type'] === ids.AppBskyGraphBlock\n  ) {\n    const subject = record['subject']\n    if (typeof subject !== 'string') {\n      return []\n    }\n    try {\n      syntax.ensureValidDid(subject)\n    } catch {\n      return []\n    }\n    return [\n      {\n        uri: uri.toString(),\n        path: 'subject',\n        linkTo: subject,\n      },\n    ]\n  }\n  if (\n    record?.['$type'] === ids.AppBskyFeedLike ||\n    record?.['$type'] === ids.AppBskyFeedRepost\n  ) {\n    const subject = record['subject']\n    if (typeof subject?.['uri'] !== 'string') {\n      return []\n    }\n    try {\n      ensureValidAtUri(subject['uri'])\n    } catch {\n      return []\n    }\n    return [\n      {\n        uri: uri.toString(),\n        path: 'subject.uri',\n        linkTo: subject['uri'],\n      },\n    ]\n  }\n  return []\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/record/transactor.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { BlobStore, WriteOpAction } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs'\nimport { dbLogger as log } from '../../logger'\nimport { ActorDb, Backlink } from '../db'\nimport { RecordReader, getBacklinks } from './reader'\n\nexport class RecordTransactor extends RecordReader {\n  constructor(\n    public db: ActorDb,\n    public blobstore: BlobStore,\n  ) {\n    super(db)\n  }\n\n  async indexRecord(\n    uri: AtUri,\n    cid: CID,\n    record: RepoRecord | null,\n    action: WriteOpAction.Create | WriteOpAction.Update = WriteOpAction.Create,\n    repoRev: string,\n    timestamp?: string,\n  ) {\n    log.debug({ uri }, 'indexing record')\n    const row = {\n      uri: uri.toString(),\n      cid: cid.toString(),\n      collection: uri.collection,\n      rkey: uri.rkey,\n      repoRev: repoRev,\n      indexedAt: timestamp || new Date().toISOString(),\n    }\n    if (!uri.hostname.startsWith('did:')) {\n      throw new Error('Expected indexed URI to contain DID')\n    } else if (row.collection.length < 1) {\n      throw new Error('Expected indexed URI to contain a collection')\n    } else if (row.rkey.length < 1) {\n      throw new Error('Expected indexed URI to contain a record key')\n    }\n\n    // Track current version of record\n    await this.db.db\n      .insertInto('record')\n      .values(row)\n      .onConflict((oc) =>\n        oc.column('uri').doUpdateSet({\n          cid: row.cid,\n          repoRev: repoRev,\n          indexedAt: row.indexedAt,\n        }),\n      )\n      .execute()\n\n    if (record !== null) {\n      // Maintain backlinks\n      const backlinks = getBacklinks(uri, record)\n      if (action === WriteOpAction.Update) {\n        // On update just recreate backlinks from scratch for the record, so we can clear out\n        // the old ones. E.g. for weird cases like updating a follow to be for a different did.\n        await this.removeBacklinksByUri(uri)\n      }\n      await this.addBacklinks(backlinks)\n    }\n\n    log.info({ uri }, 'indexed record')\n  }\n\n  async deleteRecord(uri: AtUri) {\n    log.debug({ uri }, 'deleting indexed record')\n    const deleteQuery = this.db.db\n      .deleteFrom('record')\n      .where('uri', '=', uri.toString())\n    const backlinkQuery = this.db.db\n      .deleteFrom('backlink')\n      .where('uri', '=', uri.toString())\n    await Promise.all([deleteQuery.execute(), backlinkQuery.execute()])\n\n    log.info({ uri }, 'deleted indexed record')\n  }\n\n  async removeBacklinksByUri(uri: AtUri) {\n    await this.db.db\n      .deleteFrom('backlink')\n      .where('uri', '=', uri.toString())\n      .execute()\n  }\n\n  async addBacklinks(backlinks: Backlink[]) {\n    if (backlinks.length === 0) return\n    await this.db.db\n      .insertInto('backlink')\n      .values(backlinks)\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n  }\n\n  async updateRecordTakedownStatus(uri: AtUri, takedown: StatusAttr) {\n    const takedownRef = takedown.applied\n      ? takedown.ref ?? new Date().toISOString()\n      : null\n    await this.db.db\n      .updateTable('record')\n      .set({ takedownRef })\n      .where('uri', '=', uri.toString())\n      .executeTakeFirst()\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/repo/reader.ts",
    "content": "import { BlobStore } from '@atproto/repo'\nimport { SyncEvtData } from '../../repo'\nimport { BlobReader } from '../blob/reader'\nimport { ActorDb } from '../db'\nimport { RecordReader } from '../record/reader'\nimport { SqlRepoReader } from './sql-repo-reader'\n\nexport class RepoReader {\n  blob: BlobReader\n  record: RecordReader\n  storage: SqlRepoReader\n\n  constructor(\n    public db: ActorDb,\n    public blobstore: BlobStore,\n  ) {\n    this.blob = new BlobReader(db, blobstore)\n    this.record = new RecordReader(db)\n    this.storage = new SqlRepoReader(db)\n  }\n\n  async getSyncEventData(): Promise<SyncEvtData> {\n    const root = await this.storage.getRootDetailed()\n    const { blocks } = await this.storage.getBlocks([root.cid])\n    return {\n      cid: root.cid,\n      rev: root.rev,\n      blocks,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/repo/sql-repo-reader.ts",
    "content": "import { sql } from 'kysely'\nimport { CID } from 'multiformats/cid'\nimport { chunkArray } from '@atproto/common'\nimport {\n  BlockMap,\n  CarBlock,\n  CidSet,\n  ReadableBlockstore,\n  writeCarStream,\n} from '@atproto/repo'\nimport { countAll } from '../../db'\nimport { ActorDb } from '../db'\n\nexport class SqlRepoReader extends ReadableBlockstore {\n  cache: BlockMap = new BlockMap()\n\n  constructor(public db: ActorDb) {\n    super()\n  }\n\n  async getRoot(): Promise<CID> {\n    const root = await this.getRootDetailed()\n    return root.cid\n  }\n\n  async getRootDetailed(): Promise<{ cid: CID; rev: string }> {\n    const res = await this.db.db\n      .selectFrom('repo_root')\n      .select(['cid', 'rev'])\n      .limit(1)\n      .executeTakeFirstOrThrow()\n    return {\n      cid: CID.parse(res.cid),\n      rev: res.rev,\n    }\n  }\n\n  async getBytes(cid: CID): Promise<Uint8Array | null> {\n    const cached = this.cache.get(cid)\n    if (cached) return cached\n    const found = await this.db.db\n      .selectFrom('repo_block')\n      .where('repo_block.cid', '=', cid.toString())\n      .select('content')\n      .executeTakeFirst()\n    if (!found) return null\n    this.cache.set(cid, found.content)\n    return found.content\n  }\n\n  async has(cid: CID): Promise<boolean> {\n    const got = await this.getBytes(cid)\n    return !!got\n  }\n\n  async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> {\n    const cached = this.cache.getMany(cids)\n    if (cached.missing.length < 1) return cached\n    const missing = new CidSet(cached.missing)\n    const missingStr = cached.missing.map((c) => c.toString())\n    const blocks = new BlockMap()\n    for (const batch of chunkArray(missingStr, 500)) {\n      const res = await this.db.db\n        .selectFrom('repo_block')\n        .where('repo_block.cid', 'in', batch)\n        .select(['repo_block.cid as cid', 'repo_block.content as content'])\n        .execute()\n      for (const row of res) {\n        const cid = CID.parse(row.cid)\n        blocks.set(cid, row.content)\n        missing.delete(cid)\n      }\n    }\n    this.cache.addMap(blocks)\n    blocks.addMap(cached.blocks)\n    return { blocks, missing: missing.toList() }\n  }\n\n  async getCarStream(since?: string) {\n    const root = await this.getRoot()\n    if (!root) {\n      throw new RepoRootNotFoundError()\n    }\n    return writeCarStream(root, this.iterateCarBlocks(since))\n  }\n\n  async *iterateCarBlocks(since?: string): AsyncIterable<CarBlock> {\n    let cursor: RevCursor | undefined = undefined\n    // allow us to write to car while fetching the next page\n    do {\n      const res = await this.getBlockRange(since, cursor)\n      for (const row of res) {\n        yield {\n          cid: CID.parse(row.cid),\n          bytes: row.content,\n        }\n      }\n      const lastRow = res.at(-1)\n      if (lastRow && lastRow.repoRev) {\n        cursor = {\n          cid: CID.parse(lastRow.cid),\n          rev: lastRow.repoRev,\n        }\n      } else {\n        cursor = undefined\n      }\n    } while (cursor)\n  }\n\n  async getBlockRange(since?: string, cursor?: RevCursor) {\n    const { ref } = this.db.db.dynamic\n    let builder = this.db.db\n      .selectFrom('repo_block')\n      .select(['cid', 'repoRev', 'content'])\n      .orderBy('repoRev', 'desc')\n      .orderBy('cid', 'desc')\n      .limit(500)\n    if (cursor) {\n      // use this syntax to ensure we hit the index\n      builder = builder.where(\n        sql`((${ref('repoRev')}, ${ref('cid')}) < (${\n          cursor.rev\n        }, ${cursor.cid.toString()}))`,\n      )\n    }\n    if (since) {\n      builder = builder.where('repoRev', '>', since)\n    }\n    return builder.execute()\n  }\n\n  async countBlocks(): Promise<number> {\n    const res = await this.db.db\n      .selectFrom('repo_block')\n      .select(countAll.as('count'))\n      .executeTakeFirst()\n    return res?.count ?? 0\n  }\n\n  async destroy(): Promise<void> {\n    throw new Error('Destruction of SQL repo storage not allowed at runtime')\n  }\n}\n\ntype RevCursor = {\n  cid: CID\n  rev: string\n}\n\nexport class RepoRootNotFoundError extends Error {}\n"
  },
  {
    "path": "packages/pds/src/actor-store/repo/sql-repo-transactor.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { chunkArray } from '@atproto/common'\nimport { BlockMap, CommitData, RepoStorage } from '@atproto/repo'\nimport { ActorDb, RepoBlock } from '../db'\nimport { SqlRepoReader } from './sql-repo-reader'\n\nexport class SqlRepoTransactor extends SqlRepoReader implements RepoStorage {\n  cache: BlockMap = new BlockMap()\n  now: string\n\n  constructor(\n    public db: ActorDb,\n    public did: string,\n    now?: string,\n  ) {\n    super(db)\n    this.now = now ?? new Date().toISOString()\n  }\n\n  // proactively cache all blocks from a particular commit (to prevent multiple roundtrips)\n  async cacheRev(rev: string): Promise<void> {\n    const res = await this.db.db\n      .selectFrom('repo_block')\n      .where('repoRev', '=', rev)\n      .select(['repo_block.cid', 'repo_block.content'])\n      .limit(15)\n      .execute()\n    for (const row of res) {\n      this.cache.set(CID.parse(row.cid), row.content)\n    }\n  }\n\n  async putBlock(cid: CID, block: Uint8Array, rev: string): Promise<void> {\n    await this.db.db\n      .insertInto('repo_block')\n      .values({\n        cid: cid.toString(),\n        repoRev: rev,\n        size: block.length,\n        content: block,\n      })\n      .onConflict((oc) => oc.doNothing())\n      .execute()\n    this.cache.set(cid, block)\n  }\n\n  async putMany(toPut: BlockMap, rev: string): Promise<void> {\n    const blocks: RepoBlock[] = Array.from(toPut, ([cid, bytes]) => ({\n      cid: cid.toString(),\n      repoRev: rev,\n      size: bytes.length,\n      content: bytes,\n    }))\n\n    for (const batch of chunkArray(blocks, 50)) {\n      await this.db.db\n        .insertInto('repo_block')\n        .values(batch)\n        .onConflict((oc) => oc.doNothing())\n        .execute()\n    }\n  }\n\n  async deleteMany(cids: CID[]) {\n    if (cids.length < 1) return\n    const cidStrs = cids.map((c) => c.toString())\n    await this.db.db\n      .deleteFrom('repo_block')\n      .where('cid', 'in', cidStrs)\n      .execute()\n  }\n\n  async applyCommit(commit: CommitData, isCreate?: boolean) {\n    await this.updateRoot(commit.cid, commit.rev, isCreate)\n    await this.putMany(commit.newBlocks, commit.rev)\n    await this.deleteMany(commit.removedCids.toList())\n  }\n\n  async updateRoot(cid: CID, rev: string, isCreate = false): Promise<void> {\n    if (isCreate) {\n      await this.db.db\n        .insertInto('repo_root')\n        .values({\n          did: this.did,\n          cid: cid.toString(),\n          rev: rev,\n          indexedAt: this.now,\n        })\n        .execute()\n    } else {\n      await this.db.db\n        .updateTable('repo_root')\n        .set({\n          cid: cid.toString(),\n          rev: rev,\n          indexedAt: this.now,\n        })\n        .execute()\n    }\n  }\n\n  async destroy(): Promise<void> {\n    throw new Error('Destruction of SQL repo storage not allowed at runtime')\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/actor-store/repo/transactor.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport * as crypto from '@atproto/crypto'\nimport { BlobStore, Repo, WriteOpAction, formatDataKey } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { BackgroundQueue } from '../../background'\nimport { createWriteToOp, writeToOp } from '../../repo'\nimport {\n  BadCommitSwapError,\n  BadRecordSwapError,\n  CommitDataWithOps,\n  CommitOp,\n  PreparedCreate,\n  PreparedWrite,\n} from '../../repo/types'\nimport { BlobTransactor } from '../blob/transactor'\nimport { ActorDb } from '../db'\nimport { RecordTransactor } from '../record/transactor'\nimport { RepoReader } from './reader'\nimport { SqlRepoTransactor } from './sql-repo-transactor'\n\nexport class RepoTransactor extends RepoReader {\n  blob: BlobTransactor\n  record: RecordTransactor\n  storage: SqlRepoTransactor\n\n  constructor(\n    public db: ActorDb,\n    public blobstore: BlobStore,\n    public did: string,\n    public signingKey: crypto.Keypair,\n    public backgroundQueue: BackgroundQueue,\n    public now: string = new Date().toISOString(),\n  ) {\n    super(db, blobstore)\n    this.blob = new BlobTransactor(db, blobstore, backgroundQueue)\n    this.record = new RecordTransactor(db, blobstore)\n    this.storage = new SqlRepoTransactor(db, did, now)\n  }\n\n  async maybeLoadRepo(): Promise<Repo | null> {\n    const res = await this.db.db\n      .selectFrom('repo_root')\n      .select('cid')\n      .limit(1)\n      .executeTakeFirst()\n    return res ? Repo.load(this.storage, CID.parse(res.cid)) : null\n  }\n\n  async createRepo(writes: PreparedCreate[]): Promise<CommitDataWithOps> {\n    this.db.assertTransaction()\n    const commit = await Repo.formatInitCommit(\n      this.storage,\n      this.did,\n      this.signingKey,\n      writes.map(createWriteToOp),\n    )\n    await this.storage.applyCommit(commit, true)\n    await this.indexWrites(writes, commit.rev)\n    await this.blob.processWriteBlobs(commit.rev, writes)\n\n    const ops = writes.map((w) => ({\n      action: 'create' as const,\n      path: formatDataKey(w.uri.collection, w.uri.rkey),\n      cid: w.cid,\n    }))\n    return {\n      ...commit,\n      ops,\n      prevData: null,\n    }\n  }\n\n  async processWrites(\n    writes: PreparedWrite[],\n    swapCommitCid?: CID,\n  ): Promise<CommitDataWithOps> {\n    this.db.assertTransaction()\n    if (writes.length > 200) {\n      throw new InvalidRequestError('Too many writes. Max: 200')\n    }\n\n    const commit = await this.formatCommit(writes, swapCommitCid)\n    // Do not allow commits > 2MB\n    if (commit.relevantBlocks.byteSize > 2000000) {\n      throw new InvalidRequestError('Too many writes. Max event size: 2MB')\n    }\n\n    // persist the commit to repo storage\n    await this.storage.applyCommit(commit)\n    // & send to indexing\n    await this.indexWrites(writes, commit.rev)\n    // process blobs\n    await this.blob.processWriteBlobs(commit.rev, writes)\n\n    return commit\n  }\n\n  async formatCommit(\n    writes: PreparedWrite[],\n    swapCommit?: CID,\n  ): Promise<CommitDataWithOps> {\n    // this is not in a txn, so this won't actually hold the lock,\n    // we just check if it is currently held by another txn\n    const currRoot = await this.storage.getRootDetailed()\n    if (!currRoot) {\n      throw new InvalidRequestError(`No repo root found for ${this.did}`)\n    }\n    if (swapCommit && !currRoot.cid.equals(swapCommit)) {\n      throw new BadCommitSwapError(currRoot.cid)\n    }\n    // cache last commit since there's likely overlap\n    await this.storage.cacheRev(currRoot.rev)\n    const newRecordCids: CID[] = []\n    const delAndUpdateUris: AtUri[] = []\n    const commitOps: CommitOp[] = []\n    for (const write of writes) {\n      const { action, uri, swapCid } = write\n      if (action !== WriteOpAction.Delete) {\n        newRecordCids.push(write.cid)\n      }\n      if (action !== WriteOpAction.Create) {\n        delAndUpdateUris.push(uri)\n      }\n      const record = await this.record.getRecord(uri, null, true)\n      const currRecord = record ? CID.parse(record.cid) : null\n\n      const op: CommitOp = {\n        action,\n        path: formatDataKey(uri.collection, uri.rkey),\n        cid: write.action === WriteOpAction.Delete ? null : write.cid,\n      }\n      if (currRecord) {\n        op.prev = currRecord\n      }\n      commitOps.push(op)\n      if (swapCid !== undefined) {\n        if (action === WriteOpAction.Create && swapCid !== null) {\n          throw new BadRecordSwapError(currRecord) // There should be no current record for a create\n        }\n        if (action === WriteOpAction.Update && swapCid === null) {\n          throw new BadRecordSwapError(currRecord) // There should be a current record for an update\n        }\n        if (action === WriteOpAction.Delete && swapCid === null) {\n          throw new BadRecordSwapError(currRecord) // There should be a current record for a delete\n        }\n        if ((currRecord || swapCid) && !currRecord?.equals(swapCid)) {\n          throw new BadRecordSwapError(currRecord)\n        }\n      }\n    }\n\n    const repo = await Repo.load(this.storage, currRoot.cid)\n    const prevData = repo.commit.data\n    const writeOps = writes.map(writeToOp)\n    const commit = await repo.formatCommit(writeOps, this.signingKey)\n\n    // find blocks that would be deleted but are referenced by another record\n    const dupeRecordCids = await this.getDuplicateRecordCids(\n      commit.removedCids.toList(),\n      delAndUpdateUris,\n    )\n    for (const cid of dupeRecordCids) {\n      commit.removedCids.delete(cid)\n    }\n\n    // find blocks that are relevant to ops but not included in diff\n    // (for instance a record that was moved but cid stayed the same)\n    const newRecordBlocks = commit.relevantBlocks.getMany(newRecordCids)\n    if (newRecordBlocks.missing.length > 0) {\n      const missingBlocks = await this.storage.getBlocks(\n        newRecordBlocks.missing,\n      )\n      commit.relevantBlocks.addMap(missingBlocks.blocks)\n    }\n    return {\n      ...commit,\n      ops: commitOps,\n      prevData,\n    }\n  }\n\n  async indexWrites(writes: PreparedWrite[], rev: string) {\n    this.db.assertTransaction()\n\n    for (const write of writes) {\n      if (\n        write.action === WriteOpAction.Create ||\n        write.action === WriteOpAction.Update\n      ) {\n        await this.record.indexRecord(\n          write.uri,\n          write.cid,\n          write.record,\n          write.action,\n          rev,\n          this.now,\n        )\n      } else if (write.action === WriteOpAction.Delete) {\n        await this.record.deleteRecord(write.uri)\n      }\n    }\n  }\n\n  async getDuplicateRecordCids(\n    cids: CID[],\n    touchedUris: AtUri[],\n  ): Promise<CID[]> {\n    if (touchedUris.length === 0 || cids.length === 0) {\n      return []\n    }\n    const cidStrs = cids.map((c) => c.toString())\n    const uriStrs = touchedUris.map((u) => u.toString())\n    const res = await this.db.db\n      .selectFrom('record')\n      .where('cid', 'in', cidStrs)\n      .where('uri', 'not in', uriStrs)\n      .select('cid')\n      .execute()\n    return res.map((row) => CID.parse(row.cid))\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/actor/getPreferences.ts",
    "content": "import { AuthScope, isAccessFull } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { computeProxyTo, pipethrough } from '../../../../pipethrough'\n\nexport default function (server: Server, ctx: AppContext) {\n  const { bskyAppView } = ctx\n  if (!bskyAppView) return\n\n  server.app.bsky.actor.getPreferences({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.Takendown],\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyActorGetPreferences\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      const { did } = auth.credentials\n\n      // If the request has a proxy header different from the bsky app view,\n      // we need to proxy the request to the requested app view.\n      // @TODO This behavior should not be implemented as part of the XRPC framework\n      const lxm = ids.AppBskyActorGetPreferences\n      const aud = computeProxyTo(ctx, req, lxm)\n      if (aud !== `${bskyAppView.did}#bsky_appview`) {\n        return pipethrough(ctx, req, { iss: did, aud, lxm })\n      }\n\n      const hasAccessFull =\n        auth.credentials.type === 'access' &&\n        isAccessFull(auth.credentials.scope)\n\n      const preferences = await ctx.actorStore.read(did, (store) => {\n        return store.pref.getPreferences('app.bsky', {\n          hasAccessFull,\n        })\n      })\n\n      return {\n        encoding: 'application/json',\n        body: { preferences },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/actor/getProfile.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { OutputSchema } from '../../../../lexicon/types/app/bsky/actor/getProfile'\nimport { computeProxyTo } from '../../../../pipethrough'\nimport {\n  LocalRecords,\n  LocalViewer,\n  pipethroughReadAfterWrite,\n} from '../../../../read-after-write'\n\nexport default function (server: Server, ctx: AppContext) {\n  if (!ctx.bskyAppView) return\n\n  server.app.bsky.actor.getProfile({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyActorGetProfile\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async (reqCtx) => {\n      return pipethroughReadAfterWrite(ctx, reqCtx, getProfileMunge)\n    },\n  })\n}\n\nconst getProfileMunge = async (\n  localViewer: LocalViewer,\n  original: OutputSchema,\n  local: LocalRecords,\n  requester: string,\n): Promise<OutputSchema> => {\n  if (!local.profile) return original\n  if (original.did !== requester) return original\n  return localViewer.updateProfileDetailed(original, local.profile.record)\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/actor/getProfiles.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { OutputSchema } from '../../../../lexicon/types/app/bsky/actor/getProfiles'\nimport { computeProxyTo } from '../../../../pipethrough'\nimport {\n  LocalRecords,\n  LocalViewer,\n  pipethroughReadAfterWrite,\n} from '../../../../read-after-write'\n\nexport default function (server: Server, ctx: AppContext) {\n  if (!ctx.bskyAppView) return\n\n  server.app.bsky.actor.getProfiles({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyActorGetProfiles\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async (reqCtx) => {\n      return pipethroughReadAfterWrite(ctx, reqCtx, getProfilesMunge)\n    },\n  })\n}\n\nconst getProfilesMunge = async (\n  localViewer: LocalViewer,\n  original: OutputSchema,\n  local: LocalRecords,\n  requester: string,\n): Promise<OutputSchema> => {\n  const localProf = local.profile\n  if (!localProf) return original\n\n  const profiles = original.profiles.map((prof) => {\n    if (prof.did !== requester) return prof\n    return localViewer.updateProfileDetailed(prof, localProf.record)\n  })\n  return {\n    ...original,\n    profiles,\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/actor/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport getPreferences from './getPreferences'\nimport getProfile from './getProfile'\nimport getProfiles from './getProfiles'\nimport putPreferences from './putPreferences'\n\nexport default function (server: Server, ctx: AppContext) {\n  getPreferences(server, ctx)\n  getProfile(server, ctx)\n  getProfiles(server, ctx)\n  putPreferences(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/actor/putPreferences.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AccountPreference } from '../../../../actor-store/preference/reader'\nimport { isAccessFull } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { computeProxyTo, pipethrough } from '../../../../pipethrough'\n\nexport default function (server: Server, ctx: AppContext) {\n  const { bskyAppView } = ctx\n  if (!bskyAppView) return\n\n  server.app.bsky.actor.putPreferences({\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyActorPutPreferences\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async ({ req, auth, input }) => {\n      const { did } = auth.credentials\n\n      // If the request has a proxy header different from the bsky app view,\n      // we need to proxy the request to the requested app view.\n      // @TODO This behavior should not be implemented as part of the XRPC framework\n      const lxm = ids.AppBskyActorPutPreferences\n      const aud = computeProxyTo(ctx, req, lxm)\n      if (aud !== `${bskyAppView.did}#bsky_appview`) {\n        return pipethrough(ctx, req, { iss: did, aud, lxm })\n      }\n\n      const checkedPreferences: AccountPreference[] = []\n      for (const pref of input.body.preferences) {\n        if (typeof pref.$type === 'string') {\n          checkedPreferences.push(pref as AccountPreference)\n        } else {\n          throw new InvalidRequestError('Preference is missing a $type')\n        }\n      }\n\n      const hasAccessFull =\n        auth.credentials.type === 'access' &&\n        isAccessFull(auth.credentials.scope)\n\n      await ctx.actorStore.transact(did, async (actorTxn) => {\n        await actorTxn.pref.putPreferences(checkedPreferences, 'app.bsky', {\n          hasAccessFull,\n        })\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/feed/getActorLikes.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getActorLikes'\nimport { computeProxyTo } from '../../../../pipethrough'\nimport {\n  LocalRecords,\n  LocalViewer,\n  pipethroughReadAfterWrite,\n} from '../../../../read-after-write'\n\nexport default function (server: Server, ctx: AppContext) {\n  if (!ctx.bskyAppView) return\n\n  server.app.bsky.feed.getActorLikes({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyFeedGetActorLikes\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async (reqCtx) => {\n      return pipethroughReadAfterWrite(ctx, reqCtx, getAuthorMunge)\n    },\n  })\n}\n\nconst getAuthorMunge = async (\n  localViewer: LocalViewer,\n  original: OutputSchema,\n  local: LocalRecords,\n  requester: string,\n): Promise<OutputSchema> => {\n  const localProf = local.profile\n  let feed = original.feed\n  // first update any out of date profile pictures in feed\n  if (localProf) {\n    feed = feed.map((item) => {\n      if (item.post.author.did === requester) {\n        return {\n          ...item,\n          post: {\n            ...item.post,\n            author: localViewer.updateProfileViewBasic(\n              item.post.author,\n              localProf.record,\n            ),\n          },\n        }\n      } else {\n        return item\n      }\n    })\n  }\n  return {\n    ...original,\n    feed,\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { isReasonRepost } from '../../../../lexicon/types/app/bsky/feed/defs'\nimport { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed'\nimport { computeProxyTo } from '../../../../pipethrough'\nimport {\n  LocalRecords,\n  LocalViewer,\n  pipethroughReadAfterWrite,\n} from '../../../../read-after-write'\n\nexport default function (server: Server, ctx: AppContext) {\n  if (!ctx.bskyAppView) return\n\n  server.app.bsky.feed.getAuthorFeed({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyFeedGetAuthorFeed\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async (reqCtx) => {\n      return pipethroughReadAfterWrite(ctx, reqCtx, getAuthorMunge)\n    },\n  })\n}\n\nconst getAuthorMunge = async (\n  localViewer: LocalViewer,\n  original: OutputSchema,\n  local: LocalRecords,\n  requester: string,\n): Promise<OutputSchema> => {\n  const localProf = local.profile\n  // only munge on own feed\n  if (!isUsersFeed(original, requester)) {\n    return original\n  }\n  let feed = original.feed\n  // first update any out of date profile pictures in feed\n  if (localProf) {\n    feed = feed.map((item) => {\n      if (item.post.author.did === requester) {\n        return {\n          ...item,\n          post: {\n            ...item.post,\n            author: localViewer.updateProfileViewBasic(\n              item.post.author,\n              localProf.record,\n            ),\n          },\n        }\n      } else {\n        return item\n      }\n    })\n  }\n  feed = await localViewer.formatAndInsertPostsInFeed(feed, local.posts)\n  return {\n    ...original,\n    feed,\n  }\n}\n\nconst isUsersFeed = (feed: OutputSchema, requester: string) => {\n  const first = feed.feed.at(0)\n  if (!first) return false\n  if (!first.reason && first.post.author.did === requester) return true\n  if (isReasonRepost(first.reason) && first.reason.by.did === requester)\n    return true\n  return false\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/feed/getFeed.ts",
    "content": "import { InvalidRequestError } from '@atproto/oauth-provider'\nimport { AtUri } from '@atproto/syntax'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { computeProxyTo, pipethrough } from '../../../../pipethrough'\n\nexport default function (server: Server, ctx: AppContext) {\n  const { bskyAppView } = ctx\n  if (!bskyAppView) return\n\n  server.app.bsky.feed.getFeed({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyFeedGetFeed\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n        permissions.assertRpc({ aud, lxm: ids.AppBskyFeedGetFeedSkeleton })\n      },\n    }),\n    handler: async ({ params, auth, req }) => {\n      const requester = auth.credentials.did\n\n      const feedUrl = new AtUri(params.feed)\n      const { data } = await bskyAppView.agent.com.atproto.repo.getRecord({\n        repo: feedUrl.hostname,\n        collection: feedUrl.collection,\n        rkey: feedUrl.rkey,\n      })\n      const feedDid = data.value['did']\n      if (typeof feedDid !== 'string') {\n        throw new InvalidRequestError(\n          'could not resolve feed did',\n          'UnknownFeed',\n        )\n      }\n\n      return pipethrough(ctx, req, {\n        iss: requester,\n        aud: feedDid,\n        lxm: ids.AppBskyFeedGetFeedSkeleton,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/feed/getPostThread.ts",
    "content": "import assert from 'node:assert'\nimport { AtUri } from '@atproto/syntax'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport {\n  ThreadViewPost,\n  isThreadViewPost,\n} from '../../../../lexicon/types/app/bsky/feed/defs'\nimport {\n  OutputSchema,\n  QueryParams,\n} from '../../../../lexicon/types/app/bsky/feed/getPostThread'\nimport { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post'\nimport { $Typed } from '../../../../lexicon/util'\nimport {\n  PipethroughUpstreamError,\n  computeProxyTo,\n} from '../../../../pipethrough'\nimport {\n  LocalRecords,\n  LocalViewer,\n  RecordDescript,\n  formatMungedResponse,\n  getLocalLag,\n  getRepoRev,\n  pipethroughReadAfterWrite,\n} from '../../../../read-after-write'\n\nexport default function (server: Server, ctx: AppContext) {\n  if (!ctx.bskyAppView) return\n\n  server.app.bsky.feed.getPostThread({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyFeedGetPostThread\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async (reqCtx) => {\n      try {\n        return await pipethroughReadAfterWrite(ctx, reqCtx, getPostThreadMunge)\n      } catch (err) {\n        if (\n          err instanceof PipethroughUpstreamError &&\n          err.error === 'NotFound'\n        ) {\n          const { auth, params } = reqCtx\n          const requester = auth.credentials.did\n\n          const rev = getRepoRev(err.headers)\n          if (!rev) throw err\n\n          const uri = new AtUri(params.uri)\n          if (!uri.hostname.startsWith('did:')) {\n            const account = await ctx.accountManager.getAccount(uri.hostname)\n            if (account) {\n              uri.hostname = account.did\n            }\n          }\n          if (uri.hostname !== requester) throw err\n\n          const local = await ctx.actorStore.read(requester, (store) => {\n            const localViewer = ctx.localViewer(store)\n            return readAfterWriteNotFound(\n              ctx,\n              localViewer,\n              params,\n              requester,\n              rev,\n              uri,\n            )\n          })\n          if (local === null) {\n            throw err\n          } else {\n            return formatMungedResponse(local.data, local.lag)\n          }\n        } else {\n          throw err\n        }\n      }\n    },\n  })\n}\n\n// READ AFTER WRITE\n// ----------------\n\nconst getPostThreadMunge = async (\n  localViewer: LocalViewer,\n  original: OutputSchema,\n  local: LocalRecords,\n): Promise<OutputSchema> => {\n  // @TODO if is NotFoundPost, handle similarly to error\n  // @NOTE not necessary right now as we never return those for the requested uri\n  if (!isThreadViewPost(original.thread)) {\n    return original\n  }\n  const thread = await addPostsToThread(\n    localViewer,\n    original.thread,\n    local.posts,\n  )\n  return {\n    ...original,\n    thread,\n  }\n}\n\nconst addPostsToThread = async (\n  localViewer: LocalViewer,\n  original: $Typed<ThreadViewPost>,\n  posts: RecordDescript<PostRecord>[],\n) => {\n  const inThread = findPostsInThread(original, posts)\n  if (inThread.length === 0) return original\n  let thread: $Typed<ThreadViewPost> = original\n  for (const record of inThread) {\n    thread = await insertIntoThreadReplies(localViewer, thread, record)\n  }\n  return thread\n}\n\nconst findPostsInThread = (\n  thread: ThreadViewPost,\n  posts: RecordDescript<PostRecord>[],\n): RecordDescript<PostRecord>[] => {\n  return posts.filter((post) => {\n    const rootUri = post.record.reply?.root.uri\n    if (!rootUri) return false\n    if (rootUri === thread.post.uri) return true\n    return (thread.post.record as PostRecord).reply?.root.uri === rootUri\n  })\n}\n\nconst insertIntoThreadReplies = async (\n  localViewer: LocalViewer,\n  view: $Typed<ThreadViewPost>,\n  descript: RecordDescript<PostRecord>,\n): Promise<$Typed<ThreadViewPost>> => {\n  if (descript.record.reply?.parent.uri === view.post.uri) {\n    const postView = await threadPostView(localViewer, descript)\n    if (!postView) return view\n    const replies = [postView, ...(view.replies ?? [])]\n    return {\n      ...view,\n      replies,\n    }\n  }\n  if (!view.replies) return view\n  const replies = await Promise.all(\n    view.replies.map(async (reply) =>\n      isThreadViewPost(reply)\n        ? await insertIntoThreadReplies(localViewer, reply, descript)\n        : reply,\n    ),\n  )\n  return {\n    ...view,\n    replies,\n  }\n}\n\nconst threadPostView = async (\n  localViewer: LocalViewer,\n  descript: RecordDescript<PostRecord>,\n): Promise<$Typed<ThreadViewPost> | null> => {\n  const postView = await localViewer.getPost(descript)\n  if (!postView) return null\n  return {\n    $type: 'app.bsky.feed.defs#threadViewPost',\n    post: postView,\n  }\n}\n\n// Read after write on error\n// ---------------------\n\nconst readAfterWriteNotFound = async (\n  ctx: AppContext,\n  localViewer: LocalViewer,\n  params: QueryParams,\n  requester: string,\n  rev: string,\n  resolvedUri: AtUri,\n): Promise<{ data: OutputSchema; lag?: number } | null> => {\n  if (resolvedUri.hostname !== requester) {\n    return null\n  }\n  const local = await localViewer.getRecordsSinceRev(rev)\n  const found = local.posts.find(\n    (p) => p.uri.toString() === resolvedUri.toString(),\n  )\n  if (!found) return null\n  let thread = await threadPostView(localViewer, found)\n  if (!thread) return null\n  const rest = local.posts.filter(\n    (p) => p.uri.toString() !== resolvedUri.toString(),\n  )\n  thread = await addPostsToThread(localViewer, thread, rest)\n  const highestParent = getHighestParent(thread)\n  if (highestParent) {\n    try {\n      assert(ctx.bskyAppView)\n      const parentsRes =\n        await ctx.bskyAppView.agent.app.bsky.feed.getPostThread(\n          { uri: highestParent, parentHeight: params.parentHeight, depth: 0 },\n          await ctx.appviewAuthHeaders(requester, ids.AppBskyFeedGetPostThread),\n        )\n      thread.parent = parentsRes.data.thread\n    } catch (err) {\n      // do nothing\n    }\n  }\n  return {\n    data: {\n      thread,\n    },\n    lag: getLocalLag(local),\n  }\n}\n\nconst getHighestParent = (thread: ThreadViewPost): string | undefined => {\n  if (isThreadViewPost(thread.parent)) {\n    return getHighestParent(thread.parent)\n  } else {\n    return (thread.post.record as PostRecord).reply?.parent.uri\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/feed/getTimeline.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getTimeline'\nimport { computeProxyTo } from '../../../../pipethrough'\nimport {\n  LocalRecords,\n  LocalViewer,\n  pipethroughReadAfterWrite,\n} from '../../../../read-after-write'\n\nexport default function (server: Server, ctx: AppContext) {\n  if (!ctx.bskyAppView) return\n\n  server.app.bsky.feed.getTimeline({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions, { req }) => {\n        const lxm = ids.AppBskyFeedGetTimeline\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async (reqCtx) => {\n      return pipethroughReadAfterWrite(ctx, reqCtx, getTimelineMunge)\n    },\n  })\n}\n\nconst getTimelineMunge = async (\n  localViewer: LocalViewer,\n  original: OutputSchema,\n  local: LocalRecords,\n): Promise<OutputSchema> => {\n  const feed = await localViewer.formatAndInsertPostsInFeed(\n    [...original.feed],\n    local.posts,\n  )\n  return {\n    ...original,\n    feed,\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/feed/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport getActorLikes from './getActorLikes'\nimport getAuthorFeed from './getAuthorFeed'\nimport getFeed from './getFeed'\nimport getPostThread from './getPostThread'\nimport getTimeline from './getTimeline'\n\nexport default function (server: Server, ctx: AppContext) {\n  getActorLikes(server, ctx)\n  getAuthorFeed(server, ctx)\n  getFeed(server, ctx)\n  getPostThread(server, ctx)\n  getTimeline(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/index.ts",
    "content": "import { AppContext } from '../../../context'\nimport { Server } from '../../../lexicon'\nimport actor from './actor'\nimport feed from './feed'\nimport notification from './notification'\n\nexport default function (server: Server, ctx: AppContext) {\n  actor(server, ctx)\n  feed(server, ctx)\n  notification(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/notification/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport registerPush from './registerPush'\n\nexport default function (server: Server, ctx: AppContext) {\n  registerPush(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/notification/registerPush.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { getNotif } from '@atproto/identity'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { getDidDoc } from '../util/resolver'\n\nexport default function (server: Server, ctx: AppContext) {\n  const { bskyAppView } = ctx\n  if (!bskyAppView) return\n\n  server.app.bsky.notification.registerPush({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.SignupQueued],\n      authorize: () => {\n        // @NOTE this endpoint predates generic service proxying but we want to\n        // map the permission to the \"RPC\" scope for consistency. However, since\n        // the service info is only available in the request body, we can't\n        // assert permissions here.\n      },\n    }),\n    handler: async ({ auth, input }) => {\n      const { serviceDid } = input.body\n      const { did } = auth.credentials\n\n      if (auth.credentials.type === 'oauth') {\n        auth.credentials.permissions.assertRpc({\n          aud: `${serviceDid}#bsky_notif`,\n          lxm: ids.AppBskyNotificationRegisterPush,\n        })\n      }\n\n      const authHeaders = await ctx.serviceAuthHeaders(\n        did,\n        serviceDid,\n        ids.AppBskyNotificationRegisterPush,\n      )\n\n      if (bskyAppView.did === serviceDid) {\n        await bskyAppView.agent.app.bsky.notification.registerPush(input.body, {\n          ...authHeaders,\n          encoding: 'application/json',\n        })\n        return\n      }\n\n      const notifEndpoint = await getEndpoint(ctx, serviceDid)\n      const agent = new AtpAgent({ service: notifEndpoint })\n      await agent.api.app.bsky.notification.registerPush(input.body, {\n        ...authHeaders,\n        encoding: 'application/json',\n      })\n    },\n  })\n}\n\nconst getEndpoint = async (ctx: AppContext, serviceDid: string) => {\n  const doc = await getDidDoc(ctx, serviceDid)\n  const notifEndpoint = getNotif(doc)\n  if (!notifEndpoint) {\n    throw new InvalidRequestError(\n      `invalid notification service details in did document: ${serviceDid}`,\n    )\n  }\n  return notifEndpoint\n}\n"
  },
  {
    "path": "packages/pds/src/api/app/bsky/util/resolver.ts",
    "content": "import { DidDocument, PoorlyFormattedDidDocumentError } from '@atproto/identity'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\n\n// provides http-friendly errors during did resolution\nexport const getDidDoc = async (ctx: AppContext, did: string) => {\n  let resolved: DidDocument | null\n  try {\n    resolved = await ctx.idResolver.did.resolve(did)\n  } catch (err) {\n    if (err instanceof PoorlyFormattedDidDocumentError) {\n      throw new InvalidRequestError(`invalid did document: ${did}`)\n    }\n    throw err\n  }\n  if (!resolved) {\n    throw new InvalidRequestError(`could not resolve did document: ${did}`)\n  }\n  return resolved\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/deleteAccount.ts",
    "content": "import { AccountStatus } from '../../../../account-manager/account-manager'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.deleteAccount({\n    auth: ctx.authVerifier.adminToken,\n    handler: async ({ input }) => {\n      const { did } = input.body\n      await ctx.actorStore.destroy(did)\n      await ctx.accountManager.deleteAccount(did)\n      const accountSeq = await ctx.sequencer.sequenceAccountEvt(\n        did,\n        AccountStatus.Deleted,\n      )\n      await ctx.sequencer.deleteAllForUser(did, [accountSeq])\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.disableAccountInvites({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ input }) => {\n      if (ctx.cfg.entryway) {\n        throw new InvalidRequestError(\n          'Account invites are managed by the entryway service',\n        )\n      }\n      const { account } = input.body\n      await ctx.accountManager.setAccountInvitesDisabled(account, true)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.disableInviteCodes({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ input }) => {\n      if (ctx.cfg.entryway) {\n        throw new InvalidRequestError(\n          'Account invites are managed by the entryway service',\n        )\n      }\n      const { codes = [], accounts = [] } = input.body\n      if (accounts.includes('admin')) {\n        throw new InvalidRequestError('cannot disable admin invite codes')\n      }\n      await ctx.accountManager.disableInviteCodes({ codes, accounts })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.enableAccountInvites({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ input }) => {\n      if (ctx.cfg.entryway) {\n        throw new InvalidRequestError(\n          'Account invites are managed by the entryway service',\n        )\n      }\n      const { account } = input.body\n      await ctx.accountManager.setAccountInvitesDisabled(account, false)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/getAccountInfo.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { formatAccountInfo } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.getAccountInfo({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ params }) => {\n      const [account, invites, invitedBy] = await Promise.all([\n        ctx.accountManager.getAccount(params.did, {\n          includeDeactivated: true,\n          includeTakenDown: true,\n        }),\n        ctx.accountManager.getAccountInvitesCodes(params.did),\n        ctx.accountManager.getInvitedByForAccounts([params.did]),\n      ])\n      if (!account) {\n        throw new InvalidRequestError('Account not found', 'NotFound')\n      }\n      const managesOwnInvites = !ctx.cfg.entryway\n      return {\n        encoding: 'application/json',\n        body: formatAccountInfo(account, {\n          managesOwnInvites,\n          invitedBy,\n          invites,\n        }),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/getAccountInfos.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { formatAccountInfo } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.getAccountInfos({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ params }) => {\n      const [accounts, invites, invitedBy] = await Promise.all([\n        ctx.accountManager.getAccounts(params.dids, {\n          includeDeactivated: true,\n          includeTakenDown: true,\n        }),\n        ctx.accountManager.getAccountsInvitesCodes(params.dids),\n        ctx.accountManager.getInvitedByForAccounts(params.dids),\n      ])\n\n      const managesOwnInvites = !ctx.cfg.entryway\n      const infos = Array.from(accounts.values()).map((account) => {\n        return formatAccountInfo(account, {\n          managesOwnInvites,\n          invitedBy,\n          invites,\n        })\n      })\n\n      return {\n        encoding: 'application/json',\n        body: { infos },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/getInviteCodes.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { selectInviteCodesQb } from '../../../../account-manager/helpers/invite'\nimport { AppContext } from '../../../../context'\nimport {\n  Cursor,\n  GenericKeyset,\n  LabeledResult,\n  paginate,\n} from '../../../../db/pagination'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.getInviteCodes({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ params }) => {\n      if (ctx.cfg.entryway) {\n        throw new InvalidRequestError(\n          'Account invites are managed by the entryway service',\n        )\n      }\n      const { sort, limit, cursor } = params\n      const db = ctx.accountManager.db\n      const ref = db.db.dynamic.ref\n      let keyset\n      if (sort === 'recent') {\n        keyset = new TimeCodeKeyset(ref('createdAt'), ref('code'))\n      } else if (sort === 'usage') {\n        keyset = new UseCodeKeyset(ref('uses'), ref('code'))\n      } else {\n        throw new InvalidRequestError(`unknown sort method: ${sort}`)\n      }\n\n      let builder = selectInviteCodesQb(db)\n      builder = paginate(builder, {\n        limit,\n        cursor,\n        keyset,\n      })\n\n      const res = await builder.execute()\n\n      const codes = res.map((row) => row.code)\n      const uses = await ctx.accountManager.getInviteCodesUses(codes)\n\n      const resultCursor = keyset.packFromResult(res)\n      const codeDetails = res.map((row) => ({\n        ...row,\n        disabled: row.disabled === 1,\n        uses: uses[row.code] ?? [],\n      }))\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: resultCursor,\n          codes: codeDetails,\n        },\n      }\n    },\n  })\n}\n\ntype TimeCodeResult = { createdAt: string; code: string }\n\nexport class TimeCodeKeyset extends GenericKeyset<TimeCodeResult, Cursor> {\n  labelResult(result: TimeCodeResult): Cursor {\n    return { primary: result.createdAt, secondary: result.code }\n  }\n  labeledResultToCursor(labeled: Cursor) {\n    return {\n      primary: new Date(labeled.primary).getTime().toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    const primaryDate = new Date(parseInt(cursor.primary, 10))\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n      secondary: cursor.secondary,\n    }\n  }\n}\n\ntype UseCodeResult = { uses: number; code: string }\n\nexport class UseCodeKeyset extends GenericKeyset<UseCodeResult, LabeledResult> {\n  labelResult(result: UseCodeResult): LabeledResult {\n    return { primary: result.uses, secondary: result.code }\n  }\n  labeledResultToCursor(labeled: Cursor) {\n    return {\n      primary: labeled.primary.toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    const primaryCode = parseInt(cursor.primary, 10)\n    if (isNaN(primaryCode)) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryCode,\n      secondary: cursor.secondary,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSubjectStatus'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.getSubjectStatus({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ params }) => {\n      const { did, uri, blob } = params\n      let body: OutputSchema | null = null\n      if (blob) {\n        if (!did) {\n          throw new InvalidRequestError(\n            'Must provide a did to request blob state',\n          )\n        }\n        const takedown = await ctx.actorStore.read(did, (store) =>\n          store.repo.blob.getBlobTakedownStatus(CID.parse(blob)),\n        )\n        if (takedown) {\n          body = {\n            subject: {\n              $type: 'com.atproto.admin.defs#repoBlobRef',\n              did: did,\n              cid: blob,\n            },\n            takedown,\n          }\n        }\n      } else if (uri) {\n        const parsedUri = new AtUri(uri)\n        const [takedown, cid] = await ctx.actorStore.read(\n          parsedUri.hostname,\n          (store) =>\n            Promise.all([\n              store.record.getRecordTakedownStatus(parsedUri),\n              store.record.getCurrentRecordCid(parsedUri),\n            ]),\n        )\n        if (cid && takedown) {\n          body = {\n            subject: {\n              $type: 'com.atproto.repo.strongRef',\n              uri: parsedUri.toString(),\n              cid: cid.toString(),\n            },\n            takedown,\n          }\n        }\n      } else if (did) {\n        const status = await ctx.accountManager.getAccountAdminStatus(did)\n        if (status) {\n          body = {\n            subject: {\n              $type: 'com.atproto.admin.defs#repoRef',\n              did: did,\n            },\n            takedown: status.takedown,\n            deactivated: status.deactivated,\n          }\n        }\n      } else {\n        throw new InvalidRequestError('No provided subject')\n      }\n      if (body === null) {\n        throw new InvalidRequestError('Subject not found', 'NotFound')\n      }\n      return {\n        encoding: 'application/json',\n        body,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport deleteAccount from './deleteAccount'\nimport disableAccountInvites from './disableAccountInvites'\nimport disableInviteCodes from './disableInviteCodes'\nimport enableAccountInvites from './enableAccountInvites'\nimport getAccountInfo from './getAccountInfo'\nimport getAccountInfos from './getAccountInfos'\nimport getInviteCodes from './getInviteCodes'\nimport getSubjectStatus from './getSubjectStatus'\nimport sendEmail from './sendEmail'\nimport updateAccountEmail from './updateAccountEmail'\nimport updateAccountHandle from './updateAccountHandle'\nimport updateAccountPassword from './updateAccountPassword'\nimport updateSubjectStatus from './updateSubjectStatus'\n\nexport default function (server: Server, ctx: AppContext) {\n  updateSubjectStatus(server, ctx)\n  getSubjectStatus(server, ctx)\n  getAccountInfo(server, ctx)\n  getAccountInfos(server, ctx)\n  enableAccountInvites(server, ctx)\n  disableAccountInvites(server, ctx)\n  disableInviteCodes(server, ctx)\n  getInviteCodes(server, ctx)\n  updateAccountHandle(server, ctx)\n  updateAccountEmail(server, ctx)\n  updateAccountPassword(server, ctx)\n  sendEmail(server, ctx)\n  deleteAccount(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/sendEmail.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { resultPassthru } from '../../../proxy'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.sendEmail({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ input, req }) => {\n      const {\n        content,\n        recipientDid,\n        subject = 'Message via your PDS',\n      } = input.body\n\n      const account = await ctx.accountManager.getAccount(recipientDid, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('Recipient not found')\n      }\n\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.admin.sendEmail(\n            input.body,\n            await ctx.entrywayAuthHeaders(\n              req,\n              recipientDid,\n              ids.ComAtprotoAdminSendEmail,\n            ),\n          ),\n        )\n      }\n\n      if (!account.email) {\n        throw new InvalidRequestError('account does not have an email address')\n      }\n\n      await ctx.moderationMailer.send(\n        { content },\n        { subject, to: account.email },\n      )\n\n      return {\n        encoding: 'application/json',\n        body: { sent: true },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.updateAccountEmail({\n    auth: ctx.authVerifier.adminToken,\n    handler: async ({ input, req }) => {\n      const account = await ctx.accountManager.getAccount(input.body.account, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError(\n          `Account does not exist: ${input.body.account}`,\n        )\n      }\n\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.admin.updateAccountEmail(\n          input.body,\n          ctx.entrywayPassthruHeaders(req),\n        )\n        return\n      }\n\n      await ctx.accountManager.updateEmail({\n        did: account.did,\n        email: input.body.email,\n      })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { httpLogger } from '../../../../logger'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.updateAccountHandle({\n    auth: ctx.authVerifier.adminToken,\n    handler: async ({ input }) => {\n      const { did } = input.body\n      const handle = await ctx.accountManager.normalizeAndValidateHandle(\n        input.body.handle,\n        {\n          did,\n          allowAnyValid: true,\n        },\n      )\n\n      // Pessimistic check to handle spam: also enforced by updateHandle() and the db.\n      const account = await ctx.accountManager.getAccount(handle, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n\n      if (account) {\n        if (account.did !== did) {\n          throw new InvalidRequestError(`Handle already taken: ${handle}`)\n        }\n      } else {\n        if (ctx.cfg.entryway) {\n          // the pds defers to the entryway for updating the handle in the user's did doc.\n          // here was just check that the handle is already bidirectionally confirmed.\n          // @TODO if handle is taken according to this PDS, should we force-update?\n          const doc = await ctx.idResolver.did\n            .resolveAtprotoData(did, true)\n            .catch(() => undefined)\n          if (doc?.handle !== handle) {\n            throw new InvalidRequestError('Handle does not match DID doc')\n          }\n        } else {\n          await ctx.plcClient.updateHandle(did, ctx.plcRotationKey, handle)\n        }\n        await ctx.accountManager.updateHandle(did, handle)\n      }\n\n      try {\n        await ctx.sequencer.sequenceIdentityEvt(did, handle)\n      } catch (err) {\n        httpLogger.error(\n          { err, did, handle },\n          'failed to sequence handle update',\n        )\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/updateAccountPassword.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { NEW_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.updateAccountPassword({\n    auth: ctx.authVerifier.adminToken,\n    handler: async ({ input, req }) => {\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.admin.updateAccountPassword(\n          input.body,\n          ctx.entrywayPassthruHeaders(req),\n        )\n        return\n      }\n\n      const { did, password } = input.body\n\n      if (password.length > NEW_PASSWORD_MAX_LENGTH) {\n        throw new InvalidRequestError('Invalid password length.')\n      }\n\n      await ctx.accountManager.updateAccountPassword({ did, password })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport {\n  isRepoBlobRef,\n  isRepoRef,\n} from '../../../../lexicon/types/com/atproto/admin/defs'\nimport { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.admin.updateSubjectStatus({\n    auth: ctx.authVerifier.moderator,\n    handler: async ({ input }) => {\n      const { subject, takedown, deactivated } = input.body\n      if (takedown) {\n        if (isRepoRef(subject)) {\n          await ctx.accountManager.takedownAccount(subject.did, takedown)\n        } else if (isStrongRef(subject)) {\n          const uri = new AtUri(subject.uri)\n          await ctx.actorStore.transact(uri.hostname, async (store) => {\n            await store.record.updateRecordTakedownStatus(uri, takedown)\n          })\n        } else if (isRepoBlobRef(subject)) {\n          await ctx.actorStore.transact(subject.did, async (store) => {\n            await store.repo.blob.updateBlobTakedownStatus(\n              CID.parse(subject.cid),\n              takedown,\n            )\n          })\n        } else {\n          throw new InvalidRequestError('Invalid subject')\n        }\n      }\n\n      if (deactivated) {\n        if (isRepoRef(subject)) {\n          if (deactivated.applied) {\n            await ctx.accountManager.deactivateAccount(subject.did, null)\n          } else {\n            await ctx.accountManager.activateAccount(subject.did)\n          }\n        }\n      }\n\n      if (isRepoRef(subject)) {\n        const status = await ctx.accountManager.getAccountStatus(subject.did)\n        await ctx.sequencer.sequenceAccountEvt(subject.did, status)\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          subject,\n          takedown,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/admin/util.ts",
    "content": "import express from 'express'\nimport { INVALID_HANDLE } from '@atproto/syntax'\nimport { ActorAccount } from '../../../../account-manager/helpers/account'\nimport { CodeDetail } from '../../../../account-manager/helpers/invite'\n\n// Output designed to passed as second arg to AtpAgent methods.\n// The encoding field here is a quirk of the AtpAgent.\nexport function authPassthru(\n  req: express.Request,\n  withEncoding?: false,\n): { headers: { authorization: string }; encoding: undefined } | undefined\n\nexport function authPassthru(\n  req: express.Request,\n  withEncoding: true,\n):\n  | { headers: { authorization: string }; encoding: 'application/json' }\n  | undefined\n\nexport function authPassthru(req: express.Request, withEncoding?: boolean) {\n  if (req.headers.authorization) {\n    return {\n      headers: { authorization: req.headers.authorization },\n      encoding: withEncoding ? 'application/json' : undefined,\n    }\n  }\n}\n\nexport function formatAccountInfo(\n  account: ActorAccount,\n  {\n    managesOwnInvites,\n    invitedBy,\n    invites,\n  }: {\n    managesOwnInvites: boolean\n    invites: Map<string, CodeDetail[]> | CodeDetail[]\n    invitedBy: Record<string, CodeDetail>\n  },\n) {\n  let invitesResults: CodeDetail[] | undefined\n  if (managesOwnInvites) {\n    if (Array.isArray(invites)) {\n      invitesResults = invites\n    } else {\n      invitesResults = invites.get(account.did) || []\n    }\n  }\n  return {\n    did: account.did,\n    handle: account.handle ?? INVALID_HANDLE,\n    email: account.email ?? undefined,\n    indexedAt: account.createdAt,\n    emailConfirmedAt: account.emailConfirmedAt ?? undefined,\n    invitedBy: managesOwnInvites ? invitedBy[account.did] : undefined,\n    invites: invitesResults,\n    invitesDisabled: managesOwnInvites\n      ? account.invitesDisabled === 1\n      : undefined,\n    deactivatedAt: account.deactivatedAt ?? undefined,\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/getRecommendedDidCredentials.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.getRecommendedDidCredentials({\n    auth: ctx.authVerifier.authorization({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ auth }) => {\n      const requester = auth.credentials.did\n      const signingKey = await ctx.actorStore.keypair(requester)\n      const verificationMethods = {\n        atproto: signingKey.did(),\n      }\n      const account = await ctx.accountManager.getAccount(requester, {\n        includeDeactivated: true,\n      })\n      const alsoKnownAs = account?.handle\n        ? [`at://${account.handle}`]\n        : undefined\n\n      const plcRotationKey =\n        ctx.cfg.entryway?.plcRotationKey ?? ctx.plcRotationKey.did()\n      const rotationKeys = [plcRotationKey]\n      if (ctx.cfg.identity.recoveryDidKey) {\n        rotationKeys.unshift(ctx.cfg.identity.recoveryDidKey)\n      }\n\n      const services = {\n        atproto_pds: {\n          type: 'AtprotoPersonalDataServer',\n          endpoint: ctx.cfg.service.publicUrl,\n        },\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          alsoKnownAs,\n          verificationMethods,\n          rotationKeys,\n          services,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport getRecommendedDidCredentials from './getRecommendedDidCredentials'\nimport requestPlcOperationSignature from './requestPlcOperationSignature'\nimport resolveHandle from './resolveHandle'\nimport signPlcOperation from './signPlcOperation'\nimport submitPlcOperation from './submitPlcOperation'\nimport updateHandle from './updateHandle'\n\nexport default function (server: Server, ctx: AppContext) {\n  resolveHandle(server, ctx)\n  updateHandle(server, ctx)\n  getRecommendedDidCredentials(server, ctx)\n  requestPlcOperationSignature(server, ctx)\n  signPlcOperation(server, ctx)\n  submitPlcOperation(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL, AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.requestPlcOperationSignature({\n    auth: ctx.authVerifier.authorization({\n      // @NOTE Reflect any change in signPlcOperation\n      scopes: ACCESS_FULL,\n      additional: [AuthScope.Takendown],\n      authorize: (permissions) => {\n        permissions.assertIdentity({ attr: '*' })\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.identity.requestPlcOperationSignature(\n          undefined,\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoIdentityRequestPlcOperationSignature,\n          ),\n        )\n        return\n      }\n\n      const did = auth.credentials.did\n      const account = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('account not found')\n      } else if (!account.email) {\n        throw new InvalidRequestError('account does not have an email address')\n      }\n      const token = await ctx.accountManager.createEmailToken(\n        did,\n        'plc_operation',\n      )\n      await ctx.mailer.sendPlcOperation({ token }, { to: account.email })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/resolveHandle.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { baseNormalizeAndValidate } from '../../../../handle'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.resolveHandle(async ({ params }) => {\n    const handle = baseNormalizeAndValidate(params.handle)\n\n    const user = await ctx.accountManager.getAccount(handle)\n    if (user) {\n      return {\n        encoding: 'application/json',\n        body: { did: user.did },\n      }\n    }\n\n    const supportedHandle = ctx.cfg.identity.serviceHandleDomains.some(\n      (host) => handle.endsWith(host) || handle === host.slice(1),\n    )\n    // this should be in our DB & we couldn't find it, so fail\n    if (supportedHandle) {\n      throw new InvalidRequestError('Unable to resolve handle')\n    }\n\n    // This is not someone on our server, but we help with resolving anyway\n    let did: string | undefined\n\n    // Either ask appview to resolve, or perform resolution, but don't do both.\n    if (ctx.bskyAppView) {\n      try {\n        const result =\n          await ctx.bskyAppView.agent.com.atproto.identity.resolveHandle({\n            handle,\n          })\n        did = result.data.did\n      } catch {\n        // Ignore\n      }\n    } else {\n      did = await ctx.idResolver.handle.resolve(handle)\n    }\n\n    if (!did) {\n      throw new InvalidRequestError('Unable to resolve handle')\n    }\n\n    return {\n      encoding: 'application/json',\n      body: { did },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/signPlcOperation.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { check } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL, AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { resultPassthru } from '../../../proxy'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.signPlcOperation({\n    auth: ctx.authVerifier.authorization({\n      // @NOTE Should match auth rules from requestPlcOperationSignature\n      scopes: ACCESS_FULL,\n      additional: [AuthScope.Takendown],\n      authorize: (permissions) => {\n        permissions.assertIdentity({ attr: '*' })\n      },\n    }),\n    handler: async ({ auth, input, req }) => {\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.identity.signPlcOperation(\n            input.body,\n            await ctx.entrywayAuthHeaders(\n              req,\n              auth.credentials.did,\n              ids.ComAtprotoIdentitySignPlcOperation,\n            ),\n          ),\n        )\n      }\n\n      const did = auth.credentials.did\n      const { token } = input.body\n      if (!token) {\n        throw new InvalidRequestError(\n          'email confirmation token required to sign PLC operations',\n        )\n      }\n      await ctx.accountManager.assertValidEmailTokenAndCleanup(\n        did,\n        'plc_operation',\n        token,\n      )\n\n      const lastOp = await ctx.plcClient.getLastOp(did)\n      if (check.is(lastOp, plc.def.tombstone)) {\n        throw new InvalidRequestError('Did is tombstoned')\n      }\n      const operation = await plc.createUpdateOp(\n        lastOp,\n        ctx.plcRotationKey,\n        (lastOp) => ({\n          ...lastOp,\n          rotationKeys: input.body.rotationKeys ?? lastOp.rotationKeys,\n          alsoKnownAs: input.body.alsoKnownAs ?? lastOp.alsoKnownAs,\n          verificationMethods:\n            // @TODO: actually validate instead of type casting\n            (input.body.verificationMethods as\n              | undefined\n              | Record<string, string>) ?? lastOp.verificationMethods,\n          services:\n            // @TODO: actually validate instead of type casting\n            (input.body.services as\n              | undefined\n              | Record<string, { type: string; endpoint: string }>) ??\n            lastOp.services,\n        }),\n      )\n      return {\n        encoding: 'application/json',\n        body: {\n          operation,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/submitPlcOperation.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { check } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { httpLogger as log } from '../../../../logger'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.submitPlcOperation({\n    auth: ctx.authVerifier.authorization({\n      authorize: (permissions) => {\n        permissions.assertIdentity({ attr: '*' })\n      },\n    }),\n    handler: async ({ auth, input }) => {\n      const requester = auth.credentials.did\n      const op = input.body.operation\n\n      if (!check.is(op, plc.def.operation)) {\n        throw new InvalidRequestError('Invalid operation')\n      }\n\n      const rotationKey =\n        ctx.cfg.entryway?.plcRotationKey ?? ctx.plcRotationKey.did()\n      if (!op.rotationKeys.includes(rotationKey)) {\n        throw new InvalidRequestError(\n          \"Rotation keys do not include server's rotation key\",\n        )\n      }\n      if (op.services['atproto_pds']?.type !== 'AtprotoPersonalDataServer') {\n        throw new InvalidRequestError('Incorrect type on atproto_pds service')\n      }\n      if (op.services['atproto_pds']?.endpoint !== ctx.cfg.service.publicUrl) {\n        throw new InvalidRequestError(\n          'Incorrect endpoint on atproto_pds service',\n        )\n      }\n      const signingKey = await ctx.actorStore.keypair(requester)\n      if (op.verificationMethods['atproto'] !== signingKey.did()) {\n        throw new InvalidRequestError('Incorrect signing key')\n      }\n      const account = await ctx.accountManager.getAccount(requester, {\n        includeDeactivated: true,\n      })\n      if (\n        account?.handle &&\n        op.alsoKnownAs.at(0) !== `at://${account.handle}`\n      ) {\n        throw new InvalidRequestError('Incorrect handle in alsoKnownAs')\n      }\n\n      await ctx.plcClient.sendOperation(requester, op)\n      await ctx.sequencer.sequenceIdentityEvt(requester)\n\n      try {\n        await ctx.idResolver.did.resolve(requester, true)\n      } catch (err) {\n        log.error(\n          { err, did: requester },\n          'failed to refresh did after plc update',\n        )\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/identity/updateHandle.ts",
    "content": "import { DAY, MINUTE } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { httpLogger } from '../../../../logger'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.identity.updateHandle({\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      authorize: (permissions) => {\n        permissions.assertIdentity({ attr: 'handle' })\n      },\n    }),\n    rateLimit: [\n      {\n        durationMs: 5 * MINUTE,\n        points: 10,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n      {\n        durationMs: DAY,\n        points: 50,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n    ],\n    handler: async ({ auth, input, req }) => {\n      const requester = auth.credentials.did\n\n      if (ctx.entrywayAgent) {\n        // the full flow is:\n        // -> entryway(identity.updateHandle) [update handle, submit plc op]\n        // -> pds(admin.updateAccountHandle)  [track handle, sequence handle update]\n        await ctx.entrywayAgent.com.atproto.identity.updateHandle(\n          // @ts-expect-error \"did\" is not in the schema\n          { did: requester, handle: input.body.handle },\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoIdentityUpdateHandle,\n          ),\n        )\n        return\n      }\n\n      const handle = await ctx.accountManager.normalizeAndValidateHandle(\n        input.body.handle,\n        { did: requester },\n      )\n\n      // Pessimistic check to handle spam: also enforced by updateHandle() and the db.\n      const account = await ctx.accountManager.getAccount(handle, {\n        includeDeactivated: true,\n      })\n\n      if (!account) {\n        if (requester.startsWith('did:plc:')) {\n          await ctx.plcClient.updateHandle(\n            requester,\n            ctx.plcRotationKey,\n            handle,\n          )\n        } else {\n          const resolved = await ctx.idResolver.did.resolveAtprotoData(\n            requester,\n            true,\n          )\n          if (resolved.handle !== handle) {\n            throw new InvalidRequestError(\n              'DID is not properly configured for handle',\n            )\n          }\n        }\n        await ctx.accountManager.updateHandle(requester, handle)\n      } else {\n        // if we found an account with matching handle, check if it is the same as requester\n        // if so emit an identity event, otherwise error.\n        if (account.did !== requester) {\n          throw new InvalidRequestError(`Handle already taken: ${handle}`)\n        }\n      }\n\n      try {\n        await ctx.sequencer.sequenceIdentityEvt(requester, handle)\n      } catch (err) {\n        httpLogger.error(\n          { err, did: requester, handle },\n          'failed to sequence handle update',\n        )\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/index.ts",
    "content": "import { AppContext } from '../../../context'\nimport { Server } from '../../../lexicon'\nimport admin from './admin'\nimport identity from './identity'\nimport moderation from './moderation'\nimport repo from './repo'\nimport serverMethods from './server'\nimport sync from './sync'\nimport temp from './temp'\n\nexport default function (server: Server, ctx: AppContext) {\n  admin(server, ctx)\n  identity(server, ctx)\n  moderation(server, ctx)\n  repo(server, ctx)\n  serverMethods(server, ctx)\n  sync(server, ctx)\n  temp(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/moderation/createReport.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { computeProxyTo, parseProxyInfo } from '../../../../pipethrough'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.moderation.createReport({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.Takendown],\n      authorize: (permissions, { req }) => {\n        const lxm = ids.ComAtprotoModerationCreateReport\n        const aud = computeProxyTo(ctx, req, lxm)\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async ({ auth, input, req }) => {\n      const { url, did: aud } = await parseProxyInfo(\n        ctx,\n        req,\n        ids.ComAtprotoModerationCreateReport,\n      )\n      const agent = new AtpAgent({ service: url })\n      const serviceAuth = await ctx.serviceAuthHeaders(\n        auth.credentials.did,\n        aud,\n        ids.ComAtprotoModerationCreateReport,\n      )\n      const res = await agent.com.atproto.moderation.createReport(input.body, {\n        ...serviceAuth,\n        encoding: 'application/json',\n      })\n\n      return {\n        encoding: 'application/json',\n        body: res.data,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/moderation/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport createReport from './createReport'\n\nexport default function (server: Server, ctx: AppContext) {\n  createReport(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/applyWrites.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { WriteOpAction } from '@atproto/repo'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport {\n  CreateResult,\n  DeleteResult,\n  HandlerInput,\n  UpdateResult,\n  isCreate,\n  isDelete,\n  isUpdate,\n} from '../../../../lexicon/types/com/atproto/repo/applyWrites'\nimport { dbLogger } from '../../../../logger'\nimport {\n  BadCommitSwapError,\n  InvalidRecordError,\n  PreparedWrite,\n  prepareCreate,\n  prepareDelete,\n  prepareUpdate,\n} from '../../../../repo'\n\nconst ratelimitPoints = ({ input }: { input: HandlerInput }) => {\n  let points = 0\n  for (const op of input.body.writes) {\n    if (isCreate(op)) {\n      points += 3\n    } else if (isUpdate(op)) {\n      points += 2\n    } else {\n      points += 1\n    }\n  }\n  return points\n}\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.applyWrites({\n    auth: ctx.authVerifier.authorization({\n      // @NOTE the \"checkTakedown\" and \"checkDeactivated\" checks are typically\n      // performed during auth. However, since this method's \"repo\" parameter\n      // can be a handle, we will need to fetch the account again to ensure that\n      // the handle matches the DID from the request's credentials. In order to\n      // avoid fetching the account twice (during auth, and then again in the\n      // controller), the checks are disabled here:\n\n      // checkTakedown: true,\n      // checkDeactivated: true,\n      authorize: () => {\n        // Performed in the handler as it is based on the request body\n      },\n    }),\n\n    rateLimit: [\n      {\n        name: 'repo-write-hour',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: ratelimitPoints,\n      },\n      {\n        name: 'repo-write-day',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: ratelimitPoints,\n      },\n    ],\n\n    handler: async ({ input, auth }) => {\n      const { repo, validate, swapCommit, writes } = input.body\n\n      const account = await ctx.authVerifier.findAccount(repo, {\n        checkDeactivated: true,\n        checkTakedown: true,\n      })\n\n      const did = account.did\n      if (did !== auth.credentials.did) {\n        throw new AuthRequiredError()\n      }\n\n      if (writes.length > 200) {\n        throw new InvalidRequestError('Too many writes. Max: 200')\n      }\n\n      // Verify permission of every unique \"action\" / \"collection\" pair\n      if (auth.credentials.type === 'oauth') {\n        // @NOTE Unlike \"importRepo\", we do not require \"action\" = \"*\" here.\n        for (const [action, collections] of [\n          ['create', new Set(writes.filter(isCreate).map((w) => w.collection))],\n          ['update', new Set(writes.filter(isUpdate).map((w) => w.collection))],\n          ['delete', new Set(writes.filter(isDelete).map((w) => w.collection))],\n        ] as const) {\n          for (const collection of collections) {\n            auth.credentials.permissions.assertRepo({ action, collection })\n          }\n        }\n      }\n\n      // @NOTE should preserve order of ts.writes for final use in response\n      let preparedWrites: PreparedWrite[]\n      try {\n        preparedWrites = await Promise.all(\n          writes.map(async (write) => {\n            if (isCreate(write)) {\n              return prepareCreate({\n                did,\n                collection: write.collection,\n                record: write.value,\n                rkey: write.rkey,\n                validate,\n              })\n            } else if (isUpdate(write)) {\n              return prepareUpdate({\n                did,\n                collection: write.collection,\n                record: write.value,\n                rkey: write.rkey,\n                validate,\n              })\n            } else if (isDelete(write)) {\n              return prepareDelete({\n                did,\n                collection: write.collection,\n                rkey: write.rkey,\n              })\n            } else {\n              throw new InvalidRequestError(\n                `Action not supported: ${write['$type']}`,\n              )\n            }\n          }),\n        )\n      } catch (err) {\n        if (err instanceof InvalidRecordError) {\n          throw new InvalidRequestError(err.message)\n        }\n        throw err\n      }\n\n      const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined\n\n      const commit = await ctx.actorStore.transact(did, async (actorTxn) => {\n        const commit = await actorTxn.repo\n          .processWrites(preparedWrites, swapCommitCid)\n          .catch((err) => {\n            if (err instanceof BadCommitSwapError) {\n              throw new InvalidRequestError(err.message, 'InvalidSwap')\n            } else {\n              throw err\n            }\n          })\n\n        await ctx.sequencer.sequenceCommit(did, commit)\n        return commit\n      })\n\n      await ctx.accountManager\n        .updateRepoRoot(did, commit.cid, commit.rev)\n        .catch((err) => {\n          dbLogger.error(\n            { err, did, cid: commit.cid, rev: commit.rev },\n            'failed to update account root',\n          )\n        })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          commit: {\n            cid: commit.cid.toString(),\n            rev: commit.rev,\n          },\n          results: preparedWrites.map(writeToOutputResult),\n        },\n      }\n    },\n  })\n}\n\nconst writeToOutputResult = (write: PreparedWrite) => {\n  switch (write.action) {\n    case WriteOpAction.Create:\n      return {\n        $type: 'com.atproto.repo.applyWrites#createResult',\n        cid: write.cid.toString(),\n        uri: write.uri.toString(),\n        validationStatus: write.validationStatus,\n      } satisfies CreateResult\n    case WriteOpAction.Update:\n      return {\n        $type: 'com.atproto.repo.applyWrites#updateResult',\n        cid: write.cid.toString(),\n        uri: write.uri.toString(),\n        validationStatus: write.validationStatus,\n      } satisfies UpdateResult\n    case WriteOpAction.Delete:\n      return {\n        $type: 'com.atproto.repo.applyWrites#deleteResult',\n      } satisfies DeleteResult\n    default:\n      throw new Error(`Unrecognized action: ${write}`)\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/createRecord.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { InvalidRecordKeyError } from '@atproto/syntax'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { dbLogger } from '../../../../logger'\nimport {\n  BadCommitSwapError,\n  InvalidRecordError,\n  PreparedCreate,\n  prepareCreate,\n  prepareDelete,\n} from '../../../../repo'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.createRecord({\n    auth: ctx.authVerifier.authorization({\n      // @NOTE the \"checkTakedown\" and \"checkDeactivated\" checks are typically\n      // performed during auth. However, since this method's \"repo\" parameter\n      // can be a handle, we will need to fetch the account again to ensure that\n      // the handle matches the DID from the request's credentials. In order to\n      // avoid fetching the account twice (during auth, and then again in the\n      // controller), the checks are disabled here:\n\n      // checkTakedown: true,\n      // checkDeactivated: true,\n      authorize: () => {\n        // Performed in the handler as it requires the request body\n      },\n    }),\n    rateLimit: [\n      {\n        name: 'repo-write-hour',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: () => 3,\n      },\n      {\n        name: 'repo-write-day',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: () => 3,\n      },\n    ],\n    handler: async ({ input, auth }) => {\n      const { repo, collection, rkey, record, swapCommit, validate } =\n        input.body\n\n      const account = await ctx.authVerifier.findAccount(repo, {\n        checkDeactivated: true,\n        checkTakedown: true,\n      })\n\n      const did = account.did\n      if (did !== auth.credentials.did) {\n        throw new AuthRequiredError()\n      }\n\n      if (auth.credentials.type === 'oauth') {\n        auth.credentials.permissions.assertRepo({\n          action: 'create',\n          collection,\n        })\n      }\n\n      const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined\n\n      let write: PreparedCreate\n      try {\n        write = await prepareCreate({\n          did,\n          collection,\n          record,\n          rkey,\n          validate,\n        })\n      } catch (err) {\n        if (err instanceof InvalidRecordError) {\n          throw new InvalidRequestError(err.message)\n        }\n        if (err instanceof InvalidRecordKeyError) {\n          throw new InvalidRequestError(err.message)\n        }\n        throw err\n      }\n\n      const commit = await ctx.actorStore.transact(did, async (actorTxn) => {\n        const backlinkConflicts =\n          validate !== false\n            ? await actorTxn.record.getBacklinkConflicts(\n                write.uri,\n                write.record,\n              )\n            : []\n        const backlinkDeletions = backlinkConflicts.map((uri) =>\n          prepareDelete({\n            did: uri.hostname,\n            collection: uri.collection,\n            rkey: uri.rkey,\n          }),\n        )\n        const writes = [...backlinkDeletions, write]\n        const commit = await actorTxn.repo\n          .processWrites(writes, swapCommitCid)\n          .catch((err) => {\n            if (err instanceof BadCommitSwapError) {\n              throw new InvalidRequestError(err.message, 'InvalidSwap')\n            }\n            throw err\n          })\n        await ctx.sequencer.sequenceCommit(did, commit)\n        return commit\n      })\n\n      await ctx.accountManager\n        .updateRepoRoot(did, commit.cid, commit.rev)\n        .catch((err) => {\n          dbLogger.error(\n            { err, did, cid: commit.cid, rev: commit.rev },\n            'failed to update account root',\n          )\n        })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          uri: write.uri.toString(),\n          cid: write.cid.toString(),\n          commit: {\n            cid: commit.cid.toString(),\n            rev: commit.rev,\n          },\n          validationStatus: write.validationStatus,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/deleteRecord.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { dbLogger } from '../../../../logger'\nimport {\n  BadCommitSwapError,\n  BadRecordSwapError,\n  prepareDelete,\n} from '../../../../repo'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.deleteRecord({\n    auth: ctx.authVerifier.authorization({\n      // @NOTE the \"checkTakedown\" and \"checkDeactivated\" checks are typically\n      // performed during auth. However, since this method's \"repo\" parameter\n      // can be a handle, we will need to fetch the account again to ensure that\n      // the handle matches the DID from the request's credentials. In order to\n      // avoid fetching the account twice (during auth, and then again in the\n      // controller), the checks are disabled here:\n\n      // checkTakedown: true,\n      // checkDeactivated: true,\n      authorize: () => {\n        // Performed in the handler as it requires the request body\n      },\n    }),\n    rateLimit: [\n      {\n        name: 'repo-write-hour',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: () => 1,\n      },\n      {\n        name: 'repo-write-day',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: () => 1,\n      },\n    ],\n    handler: async ({ input, auth }) => {\n      const { repo, collection, rkey, swapCommit, swapRecord } = input.body\n\n      const account = await ctx.authVerifier.findAccount(repo, {\n        checkDeactivated: true,\n        checkTakedown: true,\n      })\n\n      const did = account.did\n      if (did !== auth.credentials.did) {\n        throw new AuthRequiredError()\n      }\n\n      // We can't compute permissions based on the request payload (\"input\") in\n      // the 'auth' phase, so we do it here.\n      if (auth.credentials.type === 'oauth') {\n        auth.credentials.permissions.assertRepo({\n          action: 'delete',\n          collection,\n        })\n      }\n\n      const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined\n      const swapRecordCid = swapRecord ? CID.parse(swapRecord) : undefined\n\n      const write = prepareDelete({\n        did,\n        collection,\n        rkey,\n        swapCid: swapRecordCid,\n      })\n      const commit = await ctx.actorStore.transact(did, async (actorTxn) => {\n        const record = await actorTxn.record.getRecord(write.uri, null, true)\n        if (!record) {\n          return null // No-op if record already doesn't exist\n        }\n\n        const commit = await actorTxn.repo\n          .processWrites([write], swapCommitCid)\n          .catch((err) => {\n            if (\n              err instanceof BadCommitSwapError ||\n              err instanceof BadRecordSwapError\n            ) {\n              throw new InvalidRequestError(err.message, 'InvalidSwap')\n            } else {\n              throw err\n            }\n          })\n\n        await ctx.sequencer.sequenceCommit(did, commit)\n        return commit\n      })\n\n      if (commit !== null) {\n        await ctx.accountManager\n          .updateRepoRoot(did, commit.cid, commit.rev)\n          .catch((err) => {\n            dbLogger.error(\n              { err, did, cid: commit.cid, rev: commit.rev },\n              'failed to update account root',\n            )\n          })\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          commit: commit\n            ? {\n                cid: commit.cid.toString(),\n                rev: commit.rev,\n              }\n            : undefined,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/describeRepo.ts",
    "content": "import * as id from '@atproto/identity'\nimport { INVALID_HANDLE } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from '../sync/util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.describeRepo(async ({ params }) => {\n    const { repo } = params\n\n    const account = await assertRepoAvailability(ctx, repo, false)\n\n    let didDoc\n    try {\n      didDoc = await ctx.idResolver.did.ensureResolve(account.did)\n    } catch (err) {\n      throw new InvalidRequestError(`Could not resolve DID: ${err}`)\n    }\n\n    const handle = id.getHandle(didDoc)\n    const handleIsCorrect = handle === account.handle\n\n    const collections = await ctx.actorStore.read(account.did, (store) =>\n      store.record.listCollections(),\n    )\n\n    return {\n      encoding: 'application/json',\n      body: {\n        handle: account.handle ?? INVALID_HANDLE,\n        did: account.did,\n        didDoc,\n        collections,\n        handleIsCorrect,\n      },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/getRecord.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { pipethrough } from '../../../../pipethrough'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.getRecord(async ({ req, params }) => {\n    const { repo, collection, rkey, cid } = params\n    const did = await ctx.accountManager.getDidForActor(repo)\n\n    // fetch from pds if available, if not then fetch from appview\n    if (did) {\n      const uri = AtUri.make(did, collection, rkey)\n      const record = await ctx.actorStore.read(did, (store) =>\n        store.record.getRecord(uri, cid ?? null),\n      )\n      if (!record || record.takedownRef !== null) {\n        throw new InvalidRequestError(\n          `Could not locate record: ${uri}`,\n          'RecordNotFound',\n        )\n      }\n      return {\n        encoding: 'application/json',\n        body: {\n          uri: uri.toString(),\n          cid: record.cid,\n          value: record.value,\n        },\n      }\n    }\n\n    if (!ctx.cfg.bskyAppView) {\n      throw new InvalidRequestError(`Could not locate record`)\n    }\n\n    return pipethrough(ctx, req)\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/importRepo.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { TID } from '@atproto/common'\nimport { BlobRef, LexValue, RepoRecord } from '@atproto/lexicon'\nimport {\n  BlockMap,\n  WriteOpAction,\n  getAndParseRecord,\n  readCarStream,\n  verifyDiff,\n} from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.importRepo({\n    opts: {\n      blobLimit: ctx.cfg.service.maxImportSize,\n    },\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      scopes: ACCESS_FULL,\n      authorize: (permissions) => {\n        permissions.assertAccount({ attr: 'repo', action: 'manage' })\n      },\n    }),\n    handler: async ({ input, auth }) => {\n      if (!ctx.cfg.service.acceptingImports) {\n        throw new InvalidRequestError('Service is not accepting repo imports')\n      }\n\n      const { did } = auth.credentials\n\n      // @NOTE process as much as we can before the transaction, in particular\n      // the reading of the body stream.\n      const { roots, blocks } = await readCarStream(input.body)\n      if (roots.length !== 1) {\n        await blocks.dump()\n        throw new InvalidRequestError('expected one root')\n      }\n\n      const blockMap = new BlockMap()\n      for await (const block of blocks) {\n        blockMap.set(block.cid, block.bytes)\n      }\n\n      await ctx.actorStore.transact(did, async (store) => {\n        const now = new Date().toISOString()\n        const rev = TID.nextStr()\n        const did = store.repo.did\n\n        const currRepo = await store.repo.maybeLoadRepo()\n        const diff = await verifyDiff(\n          currRepo,\n          blockMap,\n          roots[0],\n          undefined,\n          undefined,\n          { ensureLeaves: false },\n        )\n        diff.commit.rev = rev\n        await store.repo.storage.applyCommit(diff.commit, currRepo === null)\n\n        // @NOTE There is no point in performing the following concurrently\n        // since better-sqlite3 is synchronous.\n        for (const write of diff.writes) {\n          const uri = AtUri.make(did, write.collection, write.rkey)\n          if (write.action === WriteOpAction.Delete) {\n            await store.record.deleteRecord(uri)\n          } else {\n            let parsedRecord: RepoRecord\n            try {\n              // @NOTE getAndParseRecord returns a promise for historical\n              // reasons but it's internal processing is actually synchronous.\n              const parsed = await getAndParseRecord(blockMap, write.cid)\n              parsedRecord = parsed.record\n            } catch {\n              throw new InvalidRequestError(\n                `Could not parse record at '${write.collection}/${write.rkey}'`,\n              )\n            }\n\n            await store.record.indexRecord(\n              uri,\n              write.cid,\n              parsedRecord,\n              write.action,\n              rev,\n              now,\n            )\n            const recordBlobs = findBlobRefs(parsedRecord)\n            await store.repo.blob.insertBlobs(uri.toString(), recordBlobs)\n          }\n        }\n      })\n    },\n  })\n}\n\nexport const findBlobRefs = (val: LexValue, layer = 0): BlobRef[] => {\n  if (layer > 32) {\n    return []\n  }\n  // walk arrays\n  if (Array.isArray(val)) {\n    return val.flatMap((item) => findBlobRefs(item, layer + 1))\n  }\n  // objects\n  if (val && typeof val === 'object') {\n    // convert blobs, leaving the original encoding so that we don't change CIDs on re-encode\n    if (val instanceof BlobRef) {\n      return [val]\n    }\n    // retain cids & bytes\n    if (CID.asCID(val) || val instanceof Uint8Array) {\n      return []\n    }\n    return Object.values(val).flatMap((item) => findBlobRefs(item, layer + 1))\n  }\n  // pass through\n  return []\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport applyWrites from './applyWrites'\nimport createRecord from './createRecord'\nimport deleteRecord from './deleteRecord'\nimport describeRepo from './describeRepo'\nimport getRecord from './getRecord'\nimport importRepo from './importRepo'\nimport listMissingBlobs from './listMissingBlobs'\nimport listRecords from './listRecords'\nimport putRecord from './putRecord'\nimport uploadBlob from './uploadBlob'\n\nexport default function (server: Server, ctx: AppContext) {\n  applyWrites(server, ctx)\n  createRecord(server, ctx)\n  deleteRecord(server, ctx)\n  describeRepo(server, ctx)\n  getRecord(server, ctx)\n  listRecords(server, ctx)\n  putRecord(server, ctx)\n  uploadBlob(server, ctx)\n  listMissingBlobs(server, ctx)\n  importRepo(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/listMissingBlobs.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.listMissingBlobs({\n    auth: ctx.authVerifier.authorization({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ auth, params }) => {\n      const { did } = auth.credentials\n      const { limit, cursor } = params\n\n      const blobs = await ctx.actorStore.read(did, (store) =>\n        store.repo.blob.listMissingBlobs({ limit, cursor }),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          blobs,\n          cursor: blobs.at(-1)?.cid,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/listRecords.ts",
    "content": "import { AtUri } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.listRecords(async ({ params }) => {\n    const { repo, collection, limit = 50, cursor, reverse = false } = params\n\n    const did = await ctx.accountManager.getDidForActor(repo)\n    if (!did) {\n      throw new InvalidRequestError(`Could not find repo: ${repo}`)\n    }\n\n    const records = await ctx.actorStore.read(did, (store) =>\n      store.record.listRecordsForCollection({\n        collection,\n        limit,\n        reverse,\n        cursor,\n      }),\n    )\n\n    const lastRecord = records.at(-1)\n    const lastUri = lastRecord && new AtUri(lastRecord?.uri)\n\n    return {\n      encoding: 'application/json',\n      body: {\n        records,\n        // Paginate with `before` by default, paginate with `after` when using `reverse`.\n        cursor: lastUri?.rkey,\n      },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/putRecord.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { BlobRef } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { ActorStoreTransactor } from '../../../../actor-store/actor-store-transactor'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { Record as ProfileRecord } from '../../../../lexicon/types/app/bsky/actor/profile'\nimport { dbLogger } from '../../../../logger'\nimport {\n  BadCommitSwapError,\n  BadRecordSwapError,\n  InvalidRecordError,\n  PreparedCreate,\n  PreparedUpdate,\n  prepareCreate,\n  prepareUpdate,\n} from '../../../../repo'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.putRecord({\n    auth: ctx.authVerifier.authorization({\n      // @NOTE the \"checkTakedown\" and \"checkDeactivated\" checks are typically\n      // performed during auth. However, since this method's \"repo\" parameter\n      // can be a handle, we will need to fetch the account again to ensure that\n      // the handle matches the DID from the request's credentials. In order to\n      // avoid fetching the account twice (during auth, and then again in the\n      // controller), the checks are disabled here:\n\n      // checkTakedown: true,\n      // checkDeactivated: true,\n      authorize: () => {\n        // Performed in the handler as it requires the request body\n      },\n    }),\n    rateLimit: [\n      {\n        name: 'repo-write-hour',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: () => 2,\n      },\n      {\n        name: 'repo-write-day',\n        calcKey: ({ auth }) => auth.credentials.did,\n        calcPoints: () => 2,\n      },\n    ],\n    handler: async ({ auth, input }) => {\n      const {\n        repo,\n        collection,\n        rkey,\n        record,\n        validate,\n        swapCommit,\n        swapRecord,\n      } = input.body\n\n      const account = await ctx.authVerifier.findAccount(repo, {\n        checkDeactivated: true,\n        checkTakedown: true,\n      })\n\n      const did = account.did\n      if (did !== auth.credentials.did) {\n        throw new AuthRequiredError()\n      }\n\n      // We can't compute permissions based on the request payload (\"input\") in\n      // the 'auth' phase, so we do it here.\n      if (auth.credentials.type === 'oauth') {\n        auth.credentials.permissions.assertRepo({\n          action: 'create',\n          collection,\n        })\n        auth.credentials.permissions.assertRepo({\n          action: 'update',\n          collection,\n        })\n      }\n\n      const uri = AtUri.make(did, collection, rkey)\n      const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined\n      const swapRecordCid =\n        typeof swapRecord === 'string' ? CID.parse(swapRecord) : swapRecord\n\n      const { commit, write } = await ctx.actorStore.transact(\n        did,\n        async (actorTxn) => {\n          const current = await actorTxn.record.getRecord(uri, null, true)\n          const isUpdate = current !== null\n\n          // @TODO temporaray hack for legacy blob refs in profiles - remove after migrating legacy blobs\n          if (isUpdate && collection === ids.AppBskyActorProfile) {\n            await updateProfileLegacyBlobRef(actorTxn, record)\n          }\n          const writeInfo = {\n            did,\n            collection,\n            rkey,\n            record,\n            swapCid: swapRecordCid,\n            validate,\n          }\n\n          let write: PreparedCreate | PreparedUpdate\n          try {\n            write = isUpdate\n              ? await prepareUpdate(writeInfo)\n              : await prepareCreate(writeInfo)\n          } catch (err) {\n            if (err instanceof InvalidRecordError) {\n              throw new InvalidRequestError(err.message)\n            }\n            throw err\n          }\n\n          // no-op\n          if (current && current.cid === write.cid.toString()) {\n            return {\n              commit: null,\n              write,\n            }\n          }\n\n          const commit = await actorTxn.repo\n            .processWrites([write], swapCommitCid)\n            .catch((err) => {\n              if (\n                err instanceof BadCommitSwapError ||\n                err instanceof BadRecordSwapError\n              ) {\n                throw new InvalidRequestError(err.message, 'InvalidSwap')\n              } else {\n                throw err\n              }\n            })\n\n          await ctx.sequencer.sequenceCommit(did, commit)\n\n          return { commit, write }\n        },\n      )\n\n      if (commit !== null) {\n        await ctx.accountManager\n          .updateRepoRoot(did, commit.cid, commit.rev)\n          .catch((err) => {\n            dbLogger.error(\n              { err, did, cid: commit.cid, rev: commit.rev },\n              'failed to update account root',\n            )\n          })\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          uri: write.uri.toString(),\n          cid: write.cid.toString(),\n          commit: commit\n            ? {\n                cid: commit.cid.toString(),\n                rev: commit.rev,\n              }\n            : undefined,\n          validationStatus: write.validationStatus,\n        },\n      }\n    },\n  })\n}\n\n// WARNING: mutates object\nconst updateProfileLegacyBlobRef = async (\n  actorStore: ActorStoreTransactor,\n  record: Partial<ProfileRecord>,\n) => {\n  if (record.avatar && !record.avatar.original['$type']) {\n    const blob = await actorStore.repo.blob.getBlobMetadata(record.avatar.ref)\n    record.avatar = new BlobRef(\n      record.avatar.ref,\n      record.avatar.mimeType,\n      blob.size,\n    )\n  }\n  if (record.banner && !record.banner.original['$type']) {\n    const blob = await actorStore.repo.blob.getBlobMetadata(record.banner.ref)\n    record.banner = new BlobRef(\n      record.banner.ref,\n      record.banner.mimeType,\n      blob.size,\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/repo/uploadBlob.ts",
    "content": "import { DAY } from '@atproto/common'\nimport { UpstreamTimeoutError, parseReqEncoding } from '@atproto/xrpc-server'\nimport { BlobMetadata } from '../../../../actor-store/blob/transactor'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.repo.uploadBlob({\n    auth: ctx.authVerifier.authorizationOrUserServiceAuth({\n      checkTakedown: true,\n      authorize: (permissions, { req }) => {\n        const encoding = parseReqEncoding(req)\n        permissions.assertBlob({ mime: encoding })\n      },\n    }),\n    rateLimit: {\n      durationMs: DAY,\n      points: 1000,\n    },\n    handler: async ({ auth, input }) => {\n      const requester = auth.credentials.did\n\n      const blob = await ctx.actorStore.writeNoTransaction(\n        requester,\n        async (store) => {\n          let metadata: BlobMetadata\n          try {\n            metadata = await store.repo.blob.uploadBlobAndGetMetadata(\n              input.encoding,\n              input.body,\n            )\n          } catch (err) {\n            if (err?.['name'] === 'AbortError') {\n              throw new UpstreamTimeoutError(\n                'Upload timed out, please try again.',\n              )\n            }\n            throw err\n          }\n\n          return store.transact(async (actorTxn) => {\n            const blobRef =\n              await actorTxn.repo.blob.trackUntetheredBlob(metadata)\n\n            // make the blob permanent if an associated record is already indexed\n            const recordsForBlob = await actorTxn.repo.blob.getRecordsForBlob(\n              blobRef.ref,\n            )\n            if (recordsForBlob.length > 0) {\n              await actorTxn.repo.blob.verifyBlobAndMakePermanent({\n                cid: blobRef.ref,\n                mimeType: blobRef.mimeType,\n                size: blobRef.size,\n                constraints: {},\n              })\n            }\n\n            return blobRef\n          })\n        },\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          blob,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/activateAccount.ts",
    "content": "import { INVALID_HANDLE } from '@atproto/syntax'\nimport { ForbiddenError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertValidDidDocumentForService } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.activateAccount({\n    auth: ctx.authVerifier.authorization({\n      scopes: ACCESS_FULL,\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ req, auth }) => {\n      // in the case of entryway, the full flow is activateAccount (PDS) -> activateAccount (Entryway) -> updateSubjectStatus(PDS)\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.activateAccount(\n          undefined,\n          ctx.entrywayPassthruHeaders(req),\n        )\n        return\n      }\n\n      const requester = auth.credentials.did\n\n      await assertValidDidDocumentForService(ctx, requester)\n\n      const account = await ctx.accountManager.getAccount(requester, {\n        includeDeactivated: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('user not found', 'AccountNotFound')\n      }\n\n      await ctx.accountManager.activateAccount(requester)\n\n      const syncData = await ctx.actorStore.read(requester, (store) =>\n        store.repo.getSyncEventData(),\n      )\n\n      // @NOTE: we're over-emitting for now for backwards compatibility, can reduce this in the future\n      const status = await ctx.accountManager.getAccountStatus(requester)\n      await ctx.sequencer.sequenceAccountEvt(requester, status)\n      await ctx.sequencer.sequenceIdentityEvt(\n        requester,\n        account.handle ?? INVALID_HANDLE,\n      )\n      await ctx.sequencer.sequenceSyncEvt(requester, syncData)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/checkAccountStatus.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { isValidDidDocForService } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.checkAccountStatus({\n    auth: ctx.authVerifier.authorization({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ auth }) => {\n      const requester = auth.credentials.did\n      const [\n        repoRoot,\n        repoBlocks,\n        indexedRecords,\n        importedBlobs,\n        expectedBlobs,\n      ] = await ctx.actorStore.read(requester, async (store) => {\n        return await Promise.all([\n          store.repo.storage.getRootDetailed(),\n          store.repo.storage.countBlocks(),\n          store.record.recordCount(),\n          store.repo.blob.blobCount(),\n          store.repo.blob.recordBlobCount(),\n        ])\n      })\n      const [activated, validDid] = await Promise.all([\n        ctx.accountManager.isAccountActivated(requester),\n        isValidDidDocForService(ctx, requester),\n      ])\n\n      return {\n        encoding: 'application/json',\n        body: {\n          activated,\n          validDid,\n          repoCommit: repoRoot.cid.toString(),\n          repoRev: repoRoot.rev,\n          repoBlocks,\n          indexedRecords,\n          privateStateValues: 0,\n          expectedBlobs,\n          importedBlobs,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/confirmEmail.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.confirmEmail({\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      authorize: (permissions) => {\n        permissions.assertAccount({ attr: 'email', action: 'manage' })\n      },\n    }),\n    handler: async ({ auth, input, req }) => {\n      const did = auth.credentials.did\n\n      const user = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n      })\n      if (!user) {\n        throw new InvalidRequestError('user not found', 'AccountNotFound')\n      }\n\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.confirmEmail(\n          input.body,\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoServerConfirmEmail,\n          ),\n        )\n        return\n      }\n\n      const { token, email } = input.body\n\n      if (user.email !== email.toLowerCase()) {\n        throw new InvalidRequestError('invalid email', 'InvalidEmail')\n      }\n      await ctx.accountManager.confirmEmail({ did, token })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/createAccount.ts",
    "content": "import * as plc from '@did-plc/lib'\nimport { isEmailValid } from '@hapi/address'\nimport { isDisposableEmail } from 'disposable-email-domains-js'\nimport { DidDocument, MINUTE, check } from '@atproto/common'\nimport { ExportableKeypair, Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport { AtprotoData, ensureAtpDocument } from '@atproto/identity'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AccountStatus } from '../../../../account-manager/account-manager'\nimport { NEW_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt'\nimport { AppContext } from '../../../../context'\nimport { baseNormalizeAndValidate } from '../../../../handle'\nimport { Server } from '../../../../lexicon'\nimport { InputSchema as CreateAccountInput } from '../../../../lexicon/types/com/atproto/server/createAccount'\nimport { syncEvtDataFromCommit } from '../../../../sequencer'\nimport { safeResolveDidDoc } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.createAccount({\n    rateLimit: {\n      durationMs: 5 * MINUTE,\n      points: 100,\n    },\n    auth: ctx.authVerifier.userServiceAuthOptional,\n    handler: async ({ input, auth, req }) => {\n      // @NOTE Until this code and the OAuthStore's `createAccount` are\n      // refactored together, any change made here must be reflected over there.\n\n      const requester = auth.credentials?.did ?? null\n      const {\n        did,\n        handle,\n        email,\n        password,\n        inviteCode,\n        signingKey,\n        plcOp,\n        deactivated,\n      } = ctx.entrywayAgent\n        ? await validateInputsForEntrywayPds(ctx, input.body)\n        : await validateInputsForLocalPds(ctx, input.body, requester)\n\n      let didDoc: DidDocument | undefined\n      let creds: { accessJwt: string; refreshJwt: string }\n      await ctx.actorStore.create(did, signingKey)\n      try {\n        const commit = await ctx.actorStore.transact(did, (actorTxn) =>\n          actorTxn.repo.createRepo([]),\n        )\n\n        // Generate a real did with PLC\n        if (plcOp) {\n          try {\n            await ctx.plcClient.sendOperation(did, plcOp)\n          } catch (err) {\n            req.log.error(\n              { didKey: ctx.plcRotationKey.did(), handle },\n              'failed to create did:plc',\n            )\n            throw err\n          }\n        }\n\n        didDoc = await safeResolveDidDoc(ctx, did, true)\n\n        creds = await ctx.accountManager.createAccountAndSession({\n          did,\n          handle,\n          email,\n          password,\n          repoCid: commit.cid,\n          repoRev: commit.rev,\n          inviteCode,\n          deactivated,\n        })\n\n        if (!deactivated) {\n          await ctx.sequencer.sequenceIdentityEvt(did, handle)\n          await ctx.sequencer.sequenceAccountEvt(did, AccountStatus.Active)\n          await ctx.sequencer.sequenceCommit(did, commit)\n          await ctx.sequencer.sequenceSyncEvt(\n            did,\n            syncEvtDataFromCommit(commit),\n          )\n        }\n        await ctx.accountManager.updateRepoRoot(did, commit.cid, commit.rev)\n        await ctx.actorStore.clearReservedKeypair(signingKey.did(), did)\n      } catch (err) {\n        // this will only be reached if the actor store _did not_ exist before\n        await ctx.actorStore.destroy(did)\n        throw err\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          handle,\n          did: did,\n          didDoc,\n          accessJwt: creds.accessJwt,\n          refreshJwt: creds.refreshJwt,\n        },\n      }\n    },\n  })\n}\n\nconst validateInputsForEntrywayPds = async (\n  ctx: AppContext,\n  input: CreateAccountInput,\n) => {\n  const { did, plcOp } = input\n  const handle = baseNormalizeAndValidate(input.handle)\n  if (!did || !input.plcOp) {\n    throw new InvalidRequestError(\n      'non-entryway pds requires bringing a DID and plcOp',\n    )\n  }\n  if (!check.is(plcOp, plc.def.operation)) {\n    throw new InvalidRequestError('invalid plc operation', 'IncompatibleDidDoc')\n  }\n  const plcRotationKey = ctx.cfg.entryway?.plcRotationKey\n  if (!plcRotationKey || !plcOp.rotationKeys.includes(plcRotationKey)) {\n    throw new InvalidRequestError(\n      'PLC DID does not include service rotation key',\n      'IncompatibleDidDoc',\n    )\n  }\n  try {\n    await plc.assureValidOp(plcOp)\n    await plc.assureValidSig([plcRotationKey], plcOp)\n  } catch (err) {\n    throw new InvalidRequestError('invalid plc operation', 'IncompatibleDidDoc')\n  }\n  const doc = plc.formatDidDoc({ did, ...plcOp })\n  const data = ensureAtpDocument(doc)\n\n  let signingKey: ExportableKeypair | undefined\n  if (input.did) {\n    signingKey = await ctx.actorStore.getReservedKeypair(input.did)\n  }\n  if (!signingKey) {\n    signingKey = await ctx.actorStore.getReservedKeypair(data.signingKey)\n  }\n  if (!signingKey) {\n    throw new InvalidRequestError('reserved signing key does not exist')\n  }\n\n  validateAtprotoData(data, {\n    handle,\n    pds: ctx.cfg.service.publicUrl,\n    signingKey: signingKey.did(),\n  })\n\n  return {\n    did,\n    handle,\n    email: undefined,\n    password: undefined,\n    inviteCode: undefined,\n    signingKey,\n    plcOp,\n    deactivated: false,\n  }\n}\n\nconst validateInputsForLocalPds = async (\n  ctx: AppContext,\n  input: CreateAccountInput,\n  requester: string | null,\n) => {\n  const { email, password, inviteCode } = input\n  if (input.plcOp) {\n    throw new InvalidRequestError('Unsupported input: \"plcOp\"')\n  }\n\n  if (password && password.length > NEW_PASSWORD_MAX_LENGTH) {\n    throw new InvalidRequestError(\n      `Password too long. Maximum length is ${NEW_PASSWORD_MAX_LENGTH} characters.`,\n    )\n  }\n\n  if (ctx.cfg.invites.required && !inviteCode) {\n    throw new InvalidRequestError(\n      'No invite code provided',\n      'InvalidInviteCode',\n    )\n  }\n\n  if (!email) {\n    throw new InvalidRequestError('Email is required')\n  } else if (!isEmailValid(email) || isDisposableEmail(email)) {\n    throw new InvalidRequestError(\n      'This email address is not supported, please use a different email.',\n    )\n  }\n\n  // normalize & ensure valid handle\n  const handle = await ctx.accountManager.normalizeAndValidateHandle(\n    input.handle,\n    { did: input.did },\n  )\n\n  // check that the invite code still has uses\n  if (ctx.cfg.invites.required && inviteCode) {\n    await ctx.accountManager.ensureInviteIsAvailable(inviteCode)\n  }\n\n  // check that the handle and email are available\n  const [handleAccnt, emailAcct] = await Promise.all([\n    ctx.accountManager.getAccount(handle),\n    ctx.accountManager.getAccountByEmail(email),\n  ])\n  if (handleAccnt) {\n    throw new InvalidRequestError(`Handle already taken: ${handle}`)\n  } else if (emailAcct) {\n    throw new InvalidRequestError(`Email already taken: ${email}`)\n  }\n\n  // determine the did & any plc ops we need to send\n  // if the provided did document is poorly setup, we throw\n  const signingKey = await Secp256k1Keypair.create({ exportable: true })\n\n  let did: string\n  let plcOp: plc.Operation | null\n  let deactivated = false\n  if (input.did) {\n    if (input.did !== requester) {\n      throw new AuthRequiredError(\n        `Missing auth to create account with did: ${input.did}`,\n      )\n    }\n    did = input.did\n    plcOp = null\n    deactivated = true\n  } else {\n    const formatted = await formatDidAndPlcOp(ctx, handle, input, signingKey)\n    did = formatted.did\n    plcOp = formatted.plcOp\n  }\n\n  return {\n    did,\n    handle,\n    email,\n    password,\n    inviteCode,\n    signingKey,\n    plcOp,\n    deactivated,\n  }\n}\n\nconst formatDidAndPlcOp = async (\n  ctx: AppContext,\n  handle: string,\n  input: CreateAccountInput,\n  signingKey: Keypair,\n): Promise<{\n  did: string\n  plcOp: plc.Operation | null\n}> => {\n  // if the user is not bringing a DID, then we format a create op for PLC\n  const rotationKeys = [ctx.plcRotationKey.did()]\n  if (ctx.cfg.identity.recoveryDidKey) {\n    rotationKeys.unshift(ctx.cfg.identity.recoveryDidKey)\n  }\n  if (input.recoveryKey) {\n    rotationKeys.unshift(input.recoveryKey)\n  }\n  const plcCreate = await plc.createOp({\n    signingKey: signingKey.did(),\n    rotationKeys,\n    handle,\n    pds: ctx.cfg.service.publicUrl,\n    signer: ctx.plcRotationKey,\n  })\n  return {\n    did: plcCreate.did,\n    plcOp: plcCreate.op,\n  }\n}\nconst validateAtprotoData = (\n  data: AtprotoData,\n  expected: {\n    handle: string\n    pds: string\n    signingKey: string\n  },\n) => {\n  // if the user is bringing their own did:\n  // resolve the user's did doc data, including rotationKeys if did:plc\n  // determine if we have the capability to make changes to their DID\n  if (data.handle !== expected.handle) {\n    throw new InvalidRequestError(\n      'provided handle does not match DID document handle',\n      'IncompatibleDidDoc',\n    )\n  } else if (data.pds !== expected.pds) {\n    throw new InvalidRequestError(\n      'DID document pds endpoint does not match service endpoint',\n      'IncompatibleDidDoc',\n    )\n  } else if (data.signingKey !== expected.signingKey) {\n    throw new InvalidRequestError(\n      'DID document signing key does not match service signing key',\n      'IncompatibleDidDoc',\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/createAppPassword.ts",
    "content": "import { ForbiddenError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { resultPassthru } from '../../../proxy'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.createAppPassword({\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      scopes: ACCESS_FULL,\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ auth, input, req }) => {\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.server.createAppPassword(\n            input.body,\n            await ctx.entrywayAuthHeaders(\n              req,\n              auth.credentials.did,\n              ids.ComAtprotoServerCreateAppPassword,\n            ),\n          ),\n        )\n      }\n\n      const { name } = input.body\n      const appPassword = await ctx.accountManager.createAppPassword(\n        auth.credentials.did,\n        name,\n        input.body.privileged ?? false,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: appPassword,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/createInviteCode.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { genInvCode } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.createInviteCode({\n    auth: ctx.authVerifier.adminToken,\n    handler: async ({ input }) => {\n      if (ctx.cfg.entryway) {\n        throw new InvalidRequestError(\n          'Account invites are managed by the entryway service',\n        )\n      }\n      const { useCount, forAccount = 'admin' } = input.body\n\n      const code = genInvCode(ctx.cfg)\n\n      await ctx.accountManager.createInviteCodes(\n        [{ account: forAccount, codes: [code] }],\n        useCount,\n      )\n\n      return {\n        encoding: 'application/json',\n        body: { code },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/createInviteCodes.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { AccountCodes } from '../../../../lexicon/types/com/atproto/server/createInviteCodes'\nimport { genInvCodes } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.createInviteCodes({\n    auth: ctx.authVerifier.adminToken,\n    handler: async ({ input }) => {\n      if (ctx.cfg.entryway) {\n        throw new InvalidRequestError(\n          'Account invites are managed by the entryway service',\n        )\n      }\n\n      const { codeCount, useCount } = input.body\n\n      const forAccounts = input.body.forAccounts ?? ['admin']\n\n      const accountCodes: AccountCodes[] = []\n      for (const account of forAccounts) {\n        const codes = genInvCodes(ctx.cfg, codeCount)\n        accountCodes.push({ account, codes })\n      }\n      await ctx.accountManager.createInviteCodes(accountCodes, useCount)\n\n      return {\n        encoding: 'application/json',\n        body: { codes: accountCodes },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/createSession.ts",
    "content": "import { DAY, MINUTE } from '@atproto/common'\nimport { INVALID_HANDLE } from '@atproto/syntax'\nimport { AuthRequiredError } from '@atproto/xrpc-server'\nimport { formatAccountStatus } from '../../../../account-manager/account-manager'\nimport { OLD_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { resultPassthru } from '../../../proxy'\nimport { didDocForSession } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.createSession({\n    rateLimit: [\n      {\n        durationMs: DAY,\n        points: 300,\n        calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`,\n      },\n      {\n        durationMs: 5 * MINUTE,\n        points: 30,\n        calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`,\n      },\n    ],\n    handler: async ({ input, req }) => {\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.server.createSession(\n            input.body,\n            ctx.entrywayPassthruHeaders(req),\n          ),\n        )\n      }\n\n      if (input.body.password.length > OLD_PASSWORD_MAX_LENGTH) {\n        throw new AuthRequiredError(\n          'Password too long. Consider resetting your password.',\n        )\n      }\n\n      const { user, isSoftDeleted, appPassword } =\n        await ctx.accountManager.login(input.body)\n\n      if (!input.body.allowTakendown && isSoftDeleted) {\n        throw new AuthRequiredError(\n          'Account has been taken down',\n          'AccountTakedown',\n        )\n      }\n\n      const [{ accessJwt, refreshJwt }, didDoc] = await Promise.all([\n        ctx.accountManager.createSession(user.did, appPassword, isSoftDeleted),\n        didDocForSession(ctx, user.did),\n      ])\n\n      const { status, active } = formatAccountStatus(user)\n\n      return {\n        encoding: 'application/json',\n        body: {\n          accessJwt,\n          refreshJwt,\n\n          did: user.did,\n          didDoc,\n          handle: user.handle ?? INVALID_HANDLE,\n          email: user.email ?? undefined,\n          emailConfirmed: !!user.emailConfirmedAt,\n          active,\n          status,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/deactivateAccount.ts",
    "content": "import { ForbiddenError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL, AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.deactivateAccount({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.Takendown],\n      scopes: ACCESS_FULL,\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ req, auth, input }) => {\n      // in the case of entryway, the full flow is deactivateAccount (PDS) -> deactivateAccount (Entryway) -> updateSubjectStatus(PDS)\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.deactivateAccount(\n          input.body,\n          ctx.entrywayPassthruHeaders(req),\n        )\n        return\n      }\n\n      const requester = auth.credentials.did\n      await ctx.accountManager.deactivateAccount(\n        requester,\n        input.body.deleteAfter ?? null,\n      )\n      const status = await ctx.accountManager.getAccountStatus(requester)\n      await ctx.sequencer.sequenceAccountEvt(requester, status)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/deleteAccount.ts",
    "content": "import { MINUTE } from '@atproto/common'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { AccountStatus } from '../../../../account-manager/account-manager'\nimport { OLD_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.deleteAccount({\n    rateLimit: {\n      durationMs: 5 * MINUTE,\n      points: 50,\n    },\n    handler: async ({ input, req }) => {\n      const { did, password, token } = input.body\n\n      const account = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('account not found')\n      }\n\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.deleteAccount(\n          input.body,\n          ctx.entrywayPassthruHeaders(req),\n        )\n        return\n      }\n\n      if (password.length > OLD_PASSWORD_MAX_LENGTH) {\n        throw new InvalidRequestError('Invalid password length.')\n      }\n\n      const validPass = await ctx.accountManager.verifyAccountPassword(\n        did,\n        password,\n      )\n      if (!validPass) {\n        throw new AuthRequiredError('Invalid did or password')\n      }\n\n      await ctx.accountManager.assertValidEmailToken(\n        did,\n        'delete_account',\n        token,\n      )\n      await ctx.actorStore.destroy(did)\n      await ctx.accountManager.deleteAccount(did)\n      const accountSeq = await ctx.sequencer.sequenceAccountEvt(\n        did,\n        AccountStatus.Deleted,\n      )\n      await ctx.sequencer.deleteAllForUser(did, [accountSeq])\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/deleteSession.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  const { entrywayAgent } = ctx\n  if (entrywayAgent) {\n    server.com.atproto.server.deleteSession(async ({ req }) => {\n      await entrywayAgent.com.atproto.server.deleteSession(\n        undefined,\n        ctx.entrywayPassthruHeaders(req),\n      )\n    })\n  } else {\n    server.com.atproto.server.deleteSession({\n      auth: ctx.authVerifier.refresh({\n        allowExpired: true,\n      }),\n      handler: async ({ auth }) => {\n        await ctx.accountManager.revokeRefreshToken(auth.credentials.tokenId)\n      },\n    })\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/describeServer.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.describeServer(() => {\n    const availableUserDomains = ctx.cfg.identity.serviceHandleDomains\n    const inviteCodeRequired = ctx.cfg.invites.required\n    const privacyPolicy = ctx.cfg.service.privacyPolicyUrl\n    const termsOfService = ctx.cfg.service.termsOfServiceUrl\n    const contactEmailAddress = ctx.cfg.service.contactEmailAddress\n\n    return {\n      encoding: 'application/json',\n      body: {\n        did: ctx.cfg.service.did,\n        availableUserDomains,\n        inviteCodeRequired,\n        links: { privacyPolicy, termsOfService },\n        contact: {\n          email: contactEmailAddress,\n        },\n      },\n    }\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts",
    "content": "import { ForbiddenError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { CodeDetail } from '../../../../account-manager/helpers/invite'\nimport { ACCESS_FULL } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { resultPassthru } from '../../../proxy'\nimport { genInvCodes } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.getAccountInviteCodes({\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      scopes: ACCESS_FULL,\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ params, auth, req }) => {\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.server.getAccountInviteCodes(\n            params,\n            await ctx.entrywayAuthHeaders(\n              req,\n              auth.credentials.did,\n              ids.ComAtprotoServerGetAccountInviteCodes,\n            ),\n          ),\n        )\n      }\n\n      const requester = auth.credentials.did\n      const { includeUsed, createAvailable } = params\n\n      const [account, userCodes] = await Promise.all([\n        ctx.accountManager.getAccount(requester),\n        ctx.accountManager.getAccountInvitesCodes(requester),\n      ])\n      if (!account) {\n        throw new InvalidRequestError('Account not found', 'NotFound')\n      }\n\n      let created: CodeDetail[] = []\n\n      if (\n        createAvailable &&\n        ctx.cfg.invites.required &&\n        ctx.cfg.invites.interval !== null\n      ) {\n        const { toCreate, total } = calculateCodesToCreate({\n          did: requester,\n          userCreatedAt: new Date(account.createdAt).getTime(),\n          codes: userCodes,\n          epoch: ctx.cfg.invites.epoch,\n          interval: ctx.cfg.invites.interval,\n        })\n        if (toCreate > 0) {\n          const codes = genInvCodes(ctx.cfg, toCreate)\n          created = await ctx.accountManager.createAccountInviteCodes(\n            requester,\n            codes,\n            total,\n            account.invitesDisabled ?? 0,\n          )\n        }\n      }\n\n      const allCodes = [...userCodes, ...created]\n\n      const filtered = allCodes.filter((code) => {\n        if (code.disabled) return false\n        if (!includeUsed && code.uses.length >= code.available) return false\n        return true\n      })\n\n      return {\n        encoding: 'application/json',\n        body: {\n          codes: filtered,\n        },\n      }\n    },\n  })\n}\n\n/**\n * WARNING: TRICKY SUBTLE MATH - DONT MESS WITH THIS FUNCTION UNLESS YOUR'RE VERY CONFIDENT\n * if the user wishes to create available codes & the server allows that,\n * we determine the number to create by dividing their account lifetime by the interval at which they can create codes\n * if an invite epoch is provided, we only calculate available invites since that epoch\n * we allow a max of 5 open codes at a given time\n * note: even if a user is disabled from future invites, we still create the invites for bookkeeping, we just immediately disable them as well\n */\nconst calculateCodesToCreate = (opts: {\n  did: string\n  userCreatedAt: number\n  codes: CodeDetail[]\n  epoch: number\n  interval: number\n}): { toCreate: number; total: number } => {\n  // for the sake of generating routine interval codes, we do not count explicitly gifted admin codes\n  const routineCodes = opts.codes.filter((code) => code.createdBy !== 'admin')\n  const unusedRoutineCodes = routineCodes.filter(\n    (row) => !row.disabled && row.available > row.uses.length,\n  )\n\n  const userLifespan = Date.now() - opts.userCreatedAt\n\n  // how many codes a user could create within the current epoch if they have 0\n  let couldCreate: number\n\n  if (opts.userCreatedAt >= opts.epoch) {\n    // if the user was created after the epoch, then they can create a code for each interval since the epoch\n    couldCreate = Math.floor(userLifespan / opts.interval)\n  } else {\n    // if the user was created before the epoch, we:\n    // - calculate the total intervals since account creation\n    // - calculate the total intervals before the epoch\n    // - subtract the two\n    const couldCreateTotal = Math.floor(userLifespan / opts.interval)\n    const userPreEpochLifespan = opts.epoch - opts.userCreatedAt\n    const couldCreateBeforeEpoch = Math.floor(\n      userPreEpochLifespan / opts.interval,\n    )\n    couldCreate = couldCreateTotal - couldCreateBeforeEpoch\n  }\n  // we count the codes that the user has created within the current epoch\n  const epochCodes = routineCodes.filter(\n    (code) => new Date(code.createdAt).getTime() > opts.epoch,\n  )\n  // finally we the number of codes they currently have from the number that they could create, and take a max of 5\n  const toCreate = Math.min(\n    5 - unusedRoutineCodes.length,\n    couldCreate - epochCodes.length,\n  )\n  return {\n    toCreate,\n    total: routineCodes.length + toCreate,\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/getServiceAuth.ts",
    "content": "import { HOUR, MINUTE } from '@atproto/common'\nimport { InvalidRequestError, createServiceJwt } from '@atproto/xrpc-server'\nimport {\n  AuthScope,\n  isAccessPrivileged,\n  isTakendown,\n} from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { PRIVILEGED_METHODS, PROTECTED_METHODS } from '../../../../pipethrough'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.getServiceAuth({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.Takendown],\n      authorize: (permissions, ctx) => {\n        const { aud, lxm = '*' } = ctx.params\n        permissions.assertRpc({ aud, lxm })\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const did = auth.credentials.did\n\n      // @NOTE \"exp\" is expressed in seconds since epoch, not milliseconds\n      const { aud, exp, lxm = null } = params\n\n      // Takendown accounts should not be able to generate service auth tokens except for methods necessary for account migration\n      if (auth.credentials.type === 'access') {\n        // @NOTE We should probably use \"ForbiddenError\" here. Using\n        // \"InvalidRequestError\" for legacy reasons.\n        if (\n          isTakendown(auth.credentials.scope) &&\n          lxm !== ids.ComAtprotoServerCreateAccount\n        ) {\n          throw new InvalidRequestError('Bad token scope', 'InvalidToken')\n        }\n\n        // @NOTE \"oauth\" based credentials already checked through permission\n        // set in \"authorize\" method above.\n        if (\n          lxm != null &&\n          PRIVILEGED_METHODS.has(lxm) &&\n          !isAccessPrivileged(auth.credentials.scope)\n        ) {\n          throw new InvalidRequestError(\n            `insufficient access to request a service auth token for the following method: ${lxm}`,\n          )\n        }\n      }\n\n      if (exp) {\n        const diff = exp * 1000 - Date.now()\n        if (diff < 0) {\n          throw new InvalidRequestError(\n            'expiration is in past',\n            'BadExpiration',\n          )\n        } else if (diff > HOUR) {\n          throw new InvalidRequestError(\n            'cannot request a token with an expiration more than an hour in the future',\n            'BadExpiration',\n          )\n        } else if (!lxm && diff > MINUTE) {\n          throw new InvalidRequestError(\n            'cannot request a method-less token with an expiration more than a minute in the future',\n            'BadExpiration',\n          )\n        }\n      }\n\n      if (lxm && PROTECTED_METHODS.has(lxm)) {\n        throw new InvalidRequestError(\n          `cannot request a service auth token for the following protected method: ${lxm}`,\n        )\n      }\n\n      const keypair = await ctx.actorStore.keypair(did)\n\n      const token = await createServiceJwt({\n        iss: did,\n        aud,\n        exp,\n        lxm,\n        keypair,\n      })\n      return {\n        encoding: 'application/json',\n        body: {\n          token,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/getSession.ts",
    "content": "import { ComAtprotoServerGetSession } from '@atproto/api'\nimport { INVALID_HANDLE } from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { formatAccountStatus } from '../../../../account-manager/account-manager'\nimport { AccessOutput, OAuthOutput } from '../../../../auth-output'\nimport { AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { didDocForSession } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.getSession({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.SignupQueued],\n      authorize: () => {\n        // Always allowed. \"email\" access is checked in the handler.\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      if (ctx.entrywayAgent) {\n        const headers = await ctx.entrywayAuthHeaders(\n          req,\n          auth.credentials.did,\n          'com.atproto.server.getSession',\n        )\n\n        const res = await ctx.entrywayAgent.com.atproto.server.getSession(\n          undefined,\n          headers,\n        )\n\n        return {\n          encoding: 'application/json',\n          body: output(auth, res.data),\n        }\n      }\n\n      const did = auth.credentials.did\n      const [user, didDoc] = await Promise.all([\n        ctx.accountManager.getAccount(did, { includeDeactivated: true }),\n        didDocForSession(ctx, did),\n      ])\n      if (!user) {\n        throw new InvalidRequestError(\n          `Could not find user info for account: ${did}`,\n        )\n      }\n\n      const { status, active } = formatAccountStatus(user)\n\n      return {\n        encoding: 'application/json',\n        body: output(auth, {\n          did: user.did,\n          didDoc,\n          handle: user.handle ?? INVALID_HANDLE,\n          email: user.email ?? undefined,\n          emailConfirmed: !!user.emailConfirmedAt,\n          active,\n          status,\n        }),\n      }\n    },\n  })\n}\n\nfunction output(\n  { credentials }: OAuthOutput | AccessOutput,\n  data: ComAtprotoServerGetSession.OutputSchema,\n): ComAtprotoServerGetSession.OutputSchema {\n  if (\n    credentials.type === 'oauth' &&\n    !credentials.permissions.allowsAccount({ attr: 'email', action: 'read' })\n  ) {\n    const { email, emailAuthFactor, emailConfirmed, ...rest } = data\n    return rest\n  }\n\n  return data\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport activateAccount from './activateAccount'\nimport checkAccountStatus from './checkAccountStatus'\nimport confirmEmail from './confirmEmail'\nimport createAccount from './createAccount'\nimport createAppPassword from './createAppPassword'\nimport createInviteCode from './createInviteCode'\nimport createInviteCodes from './createInviteCodes'\nimport createSession from './createSession'\nimport deactivateAccount from './deactivateAccount'\nimport deleteAccount from './deleteAccount'\nimport deleteSession from './deleteSession'\nimport describeServer from './describeServer'\nimport getAccountInviteCodes from './getAccountInviteCodes'\nimport getServiceAuth from './getServiceAuth'\nimport getSession from './getSession'\nimport listAppPasswords from './listAppPasswords'\nimport refreshSession from './refreshSession'\nimport requestDelete from './requestAccountDelete'\nimport requestEmailConfirmation from './requestEmailConfirmation'\nimport requestEmailUpdate from './requestEmailUpdate'\nimport requestPasswordReset from './requestPasswordReset'\nimport reserveSigningKey from './reserveSigningKey'\nimport resetPassword from './resetPassword'\nimport revokeAppPassword from './revokeAppPassword'\nimport updateEmail from './updateEmail'\n\nexport default function (server: Server, ctx: AppContext) {\n  describeServer(server, ctx)\n  createAccount(server, ctx)\n  createInviteCode(server, ctx)\n  createInviteCodes(server, ctx)\n  getAccountInviteCodes(server, ctx)\n  reserveSigningKey(server, ctx)\n  requestDelete(server, ctx)\n  deleteAccount(server, ctx)\n  requestPasswordReset(server, ctx)\n  resetPassword(server, ctx)\n  requestEmailConfirmation(server, ctx)\n  confirmEmail(server, ctx)\n  requestEmailUpdate(server, ctx)\n  updateEmail(server, ctx)\n  createSession(server, ctx)\n  deleteSession(server, ctx)\n  getSession(server, ctx)\n  refreshSession(server, ctx)\n  createAppPassword(server, ctx)\n  listAppPasswords(server, ctx)\n  revokeAppPassword(server, ctx)\n  getServiceAuth(server, ctx)\n  checkAccountStatus(server, ctx)\n  activateAccount(server, ctx)\n  deactivateAccount(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/listAppPasswords.ts",
    "content": "import { ForbiddenError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { resultPassthru } from '../../../proxy'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.listAppPasswords({\n    auth: ctx.authVerifier.authorization({\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.server.listAppPasswords(\n            undefined,\n            await ctx.entrywayAuthHeaders(\n              req,\n              auth.credentials.did,\n              ids.ComAtprotoServerListAppPasswords,\n            ),\n          ),\n        )\n      }\n\n      const passwords = await ctx.accountManager.listAppPasswords(\n        auth.credentials.did,\n      )\n      return {\n        encoding: 'application/json',\n        body: { passwords },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/refreshSession.ts",
    "content": "import { INVALID_HANDLE } from '@atproto/syntax'\nimport { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { formatAccountStatus } from '../../../../account-manager/account-manager'\nimport { AppContext } from '../../../../context'\nimport { softDeleted } from '../../../../db/util'\nimport { Server } from '../../../../lexicon'\nimport { resultPassthru } from '../../../proxy'\nimport { didDocForSession } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.refreshSession({\n    auth: ctx.authVerifier.refresh(),\n    handler: async ({ auth, req }) => {\n      const did = auth.credentials.did\n      const user = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!user) {\n        throw new InvalidRequestError(\n          `Could not find user info for account: ${did}`,\n        )\n      }\n      if (softDeleted(user)) {\n        throw new AuthRequiredError(\n          'Account has been taken down',\n          'AccountTakedown',\n        )\n      }\n\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.server.refreshSession(\n            undefined,\n            ctx.entrywayPassthruHeaders(req),\n          ),\n        )\n      }\n\n      const [didDoc, rotated] = await Promise.all([\n        didDocForSession(ctx, user.did),\n        ctx.accountManager.rotateRefreshToken(auth.credentials.tokenId),\n      ])\n      if (rotated === null) {\n        throw new InvalidRequestError('Token has been revoked', 'ExpiredToken')\n      }\n\n      const { status, active } = formatAccountStatus(user)\n\n      return {\n        encoding: 'application/json',\n        body: {\n          accessJwt: rotated.accessJwt,\n          refreshJwt: rotated.refreshJwt,\n\n          did: user.did,\n          didDoc,\n          handle: user.handle ?? INVALID_HANDLE,\n          email: user.email ?? undefined,\n          emailConfirmed: !!user.emailConfirmedAt,\n          active,\n          status,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/requestAccountDelete.ts",
    "content": "import { DAY, HOUR } from '@atproto/common'\nimport { ForbiddenError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { ACCESS_FULL } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.requestAccountDelete({\n    rateLimit: [\n      {\n        durationMs: DAY,\n        points: 15,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n      {\n        durationMs: HOUR,\n        points: 5,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n    ],\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      scopes: ACCESS_FULL,\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      const did = auth.credentials.did\n      const account = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('account not found')\n      }\n\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.requestAccountDelete(\n          undefined,\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoServerRequestAccountDelete,\n          ),\n        )\n        return\n      }\n\n      if (!account.email) {\n        throw new InvalidRequestError('account does not have an email address')\n      }\n      const token = await ctx.accountManager.createEmailToken(\n        did,\n        'delete_account',\n      )\n      await ctx.mailer.sendAccountDelete({ token }, { to: account.email })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts",
    "content": "import { DAY, HOUR } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.requestEmailConfirmation({\n    rateLimit: [\n      {\n        durationMs: DAY,\n        points: 15,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n      {\n        durationMs: HOUR,\n        points: 5,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n    ],\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      authorize: (permissions) => {\n        permissions.assertAccount({ attr: 'email', action: 'manage' })\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      const did = auth.credentials.did\n      const account = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('account not found')\n      }\n\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.requestEmailConfirmation(\n          undefined,\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoServerRequestEmailConfirmation,\n          ),\n        )\n        return\n      }\n\n      if (!account.email) {\n        throw new InvalidRequestError('account does not have an email address')\n      }\n      const token = await ctx.accountManager.createEmailToken(\n        did,\n        'confirm_email',\n      )\n      await ctx.mailer.sendConfirmEmail({ token }, { to: account.email })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts",
    "content": "import { DAY, HOUR } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\nimport { resultPassthru } from '../../../proxy'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.requestEmailUpdate({\n    rateLimit: [\n      {\n        durationMs: DAY,\n        points: 15,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n      {\n        durationMs: HOUR,\n        points: 5,\n        calcKey: ({ auth }) => auth.credentials.did,\n      },\n    ],\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      authorize: (permissions) => {\n        permissions.assertAccount({ attr: 'email', action: 'manage' })\n      },\n    }),\n    handler: async ({ auth, req }) => {\n      const did = auth.credentials.did\n      const account = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('account not found')\n      }\n\n      if (ctx.entrywayAgent) {\n        return resultPassthru(\n          await ctx.entrywayAgent.com.atproto.server.requestEmailUpdate(\n            undefined,\n            await ctx.entrywayAuthHeaders(\n              req,\n              auth.credentials.did,\n              ids.ComAtprotoServerRequestEmailUpdate,\n            ),\n          ),\n        )\n      }\n\n      if (!account.email) {\n        throw new InvalidRequestError('account does not have an email address')\n      }\n\n      const tokenRequired = !!account.emailConfirmedAt\n      if (tokenRequired) {\n        const token = await ctx.accountManager.createEmailToken(\n          did,\n          'update_email',\n        )\n        await ctx.mailer.sendUpdateEmail({ token }, { to: account.email })\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          tokenRequired,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/requestPasswordReset.ts",
    "content": "import { DAY, HOUR } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.requestPasswordReset({\n    rateLimit: [\n      {\n        durationMs: DAY,\n        points: 50,\n      },\n      {\n        durationMs: HOUR,\n        points: 15,\n      },\n    ],\n    handler: async ({ input, req }) => {\n      const email = input.body.email.toLowerCase()\n\n      const account = await ctx.accountManager.getAccountByEmail(email, {\n        includeDeactivated: true,\n        includeTakenDown: true,\n      })\n\n      if (!account?.email) {\n        if (ctx.entrywayAgent) {\n          await ctx.entrywayAgent.com.atproto.server.requestPasswordReset(\n            input.body,\n            ctx.entrywayPassthruHeaders(req),\n          )\n          return\n        }\n        throw new InvalidRequestError('account does not have an email address')\n      }\n\n      const token = await ctx.accountManager.createEmailToken(\n        account.did,\n        'reset_password',\n      )\n      await ctx.mailer.sendResetPassword(\n        { handle: account.handle ?? account.email, token },\n        { to: account.email },\n      )\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/reserveSigningKey.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.reserveSigningKey({\n    handler: async ({ input }) => {\n      const signingKey = await ctx.actorStore.reserveKeypair(input.body.did)\n      return {\n        encoding: 'application/json',\n        body: {\n          signingKey,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/resetPassword.ts",
    "content": "import { MINUTE } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { NEW_PASSWORD_MAX_LENGTH } from '../../../../account-manager/helpers/scrypt'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.resetPassword({\n    rateLimit: [\n      {\n        durationMs: 5 * MINUTE,\n        points: 50,\n      },\n    ],\n    handler: async ({ input, req }) => {\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.resetPassword(\n          input.body,\n          ctx.entrywayPassthruHeaders(req),\n        )\n        return\n      }\n\n      const { token, password } = input.body\n\n      if (password.length > NEW_PASSWORD_MAX_LENGTH) {\n        throw new InvalidRequestError('Invalid password length.')\n      }\n\n      await ctx.accountManager.resetPassword({ token, password })\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/revokeAppPassword.ts",
    "content": "import { ForbiddenError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.revokeAppPassword({\n    auth: ctx.authVerifier.authorization({\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ auth, input, req }) => {\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.revokeAppPassword(\n          input.body,\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoServerRevokeAppPassword,\n          ),\n        )\n        return\n      }\n\n      const requester = auth.credentials.did\n      const { name } = input.body\n\n      await ctx.accountManager.revokeAppPassword(requester, name)\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/updateEmail.ts",
    "content": "import { isEmailValid } from '@hapi/address'\nimport { isDisposableEmail } from 'disposable-email-domains-js'\nimport { ForbiddenError, InvalidRequestError } from '@atproto/xrpc-server'\nimport { UserAlreadyExistsError } from '../../../../account-manager/helpers/account'\nimport { ACCESS_FULL } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { ids } from '../../../../lexicon/lexicons'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.server.updateEmail({\n    auth: ctx.authVerifier.authorization({\n      checkTakedown: true,\n      scopes: ACCESS_FULL,\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ auth, input, req }) => {\n      const did = auth.credentials.did\n      const { token, email } = input.body\n      if (!isEmailValid(email) || isDisposableEmail(email)) {\n        throw new InvalidRequestError(\n          'This email address is not supported, please use a different email.',\n        )\n      }\n      const account = await ctx.accountManager.getAccount(did, {\n        includeDeactivated: true,\n      })\n      if (!account) {\n        throw new InvalidRequestError('account not found')\n      }\n\n      if (ctx.entrywayAgent) {\n        await ctx.entrywayAgent.com.atproto.server.updateEmail(\n          input.body,\n          await ctx.entrywayAuthHeaders(\n            req,\n            auth.credentials.did,\n            ids.ComAtprotoServerUpdateEmail,\n          ),\n        )\n        return\n      }\n\n      // require valid token if account email is confirmed\n      if (account.emailConfirmedAt) {\n        if (!token) {\n          throw new InvalidRequestError(\n            'confirmation token required',\n            'TokenRequired',\n          )\n        }\n        await ctx.accountManager.assertValidEmailToken(\n          did,\n          'update_email',\n          token,\n        )\n      }\n\n      try {\n        await ctx.accountManager.updateEmail({ did, email })\n      } catch (err) {\n        if (err instanceof UserAlreadyExistsError) {\n          throw new InvalidRequestError(\n            'This email address is already in use, please use a different email.',\n          )\n        } else {\n          throw err\n        }\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/server/util.ts",
    "content": "import { getPdsEndpoint, getSigningDidKey } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { DidDocument } from '@atproto/identity'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ServerConfig } from '../../../../config'\nimport { AppContext } from '../../../../context'\nimport { httpLogger } from '../../../../logger'\n\n// generate an invite code preceded by the hostname\n// with '.'s replaced by '-'s so it is not mistakable for a link\n// ex: bsky-app-abc23-567xy\n// regex: bsky-app-[a-z2-7]{5}-[a-z2-7]{5}\nexport const genInvCode = (cfg: ServerConfig): string => {\n  return cfg.service.hostname.replaceAll('.', '-') + '-' + getRandomToken()\n}\n\nexport const genInvCodes = (cfg: ServerConfig, count: number): string[] => {\n  const codes: string[] = []\n  for (let i = 0; i < count; i++) {\n    codes.push(genInvCode(cfg))\n  }\n  return codes\n}\n\n// Formatted xxxxx-xxxxx where digits are in base32\nexport const getRandomToken = () => {\n  const token = crypto.randomStr(8, 'base32').slice(0, 10)\n  return token.slice(0, 5) + '-' + token.slice(5, 10)\n}\n\nexport const safeResolveDidDoc = async (\n  ctx: AppContext,\n  did: string,\n  forceRefresh?: boolean,\n): Promise<DidDocument | undefined> => {\n  try {\n    const didDoc = await ctx.idResolver.did.resolve(did, forceRefresh)\n    return didDoc ?? undefined\n  } catch (err) {\n    httpLogger.warn({ err, did }, 'failed to resolve did doc')\n  }\n}\n\nexport const didDocForSession = async (\n  ctx: AppContext,\n  did: string,\n  forceRefresh?: boolean,\n): Promise<DidDocument | undefined> => {\n  if (!ctx.cfg.identity.enableDidDocWithSession) return\n  return safeResolveDidDoc(ctx, did, forceRefresh)\n}\n\nexport const isValidDidDocForService = async (\n  ctx: AppContext,\n  did: string,\n): Promise<boolean> => {\n  try {\n    await assertValidDidDocumentForService(ctx, did)\n    return true\n  } catch {\n    return false\n  }\n}\n\nexport const assertValidDidDocumentForService = async (\n  ctx: AppContext,\n  did: string,\n) => {\n  if (did.startsWith('did:plc')) {\n    const resolved = await ctx.plcClient.getDocumentData(did)\n    await assertValidDocContents(ctx, did, {\n      pdsEndpoint: resolved.services['atproto_pds']?.endpoint,\n      signingKey: resolved.verificationMethods['atproto'],\n      rotationKeys: resolved.rotationKeys,\n    })\n  } else {\n    const resolved = await ctx.idResolver.did.resolve(did, true)\n    if (!resolved) {\n      throw new InvalidRequestError('Could not resolve DID')\n    }\n    await assertValidDocContents(ctx, did, {\n      pdsEndpoint: getPdsEndpoint(resolved),\n      signingKey: getSigningDidKey(resolved),\n    })\n  }\n}\n\nconst assertValidDocContents = async (\n  ctx: AppContext,\n  did: string,\n  contents: {\n    signingKey?: string\n    pdsEndpoint?: string\n    rotationKeys?: string[]\n  },\n) => {\n  const { signingKey, pdsEndpoint, rotationKeys } = contents\n\n  const plcRotationKey =\n    ctx.cfg.entryway?.plcRotationKey ?? ctx.plcRotationKey.did()\n  if (rotationKeys !== undefined && !rotationKeys.includes(plcRotationKey)) {\n    throw new InvalidRequestError(\n      'Server rotation key not included in PLC DID data',\n    )\n  }\n\n  if (!pdsEndpoint || pdsEndpoint !== ctx.cfg.service.publicUrl) {\n    throw new InvalidRequestError(\n      'DID document atproto_pds service endpoint does not match PDS public url',\n    )\n  }\n\n  const keypair = await ctx.actorStore.keypair(did)\n  if (!signingKey || signingKey !== keypair.did()) {\n    throw new InvalidRequestError(\n      'DID document verification method does not match expected signing key',\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts",
    "content": "import { isUserOrAdmin } from '../../../../../auth-verifier'\nimport { AppContext } from '../../../../../context'\nimport { Server } from '../../../../../lexicon'\nimport { getCarStream } from '../getRepo'\nimport { assertRepoAvailability } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getCheckout({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const carStream = await getCarStream(ctx, did)\n\n      return {\n        encoding: 'application/vnd.ipld.car',\n        body: carStream,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { isUserOrAdmin } from '../../../../../auth-verifier'\nimport { AppContext } from '../../../../../context'\nimport { Server } from '../../../../../lexicon'\nimport { assertRepoAvailability } from '../util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getHead({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const root = await ctx.actorStore.read(did, (store) =>\n        store.repo.storage.getRoot(),\n      )\n      if (root === null) {\n        throw new InvalidRequestError(\n          `Could not find root for DID: ${did}`,\n          'HeadNotFound',\n        )\n      }\n      return {\n        encoding: 'application/json',\n        body: { root: root.toString() },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/getBlob.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { BlobNotFoundError } from '@atproto/repo'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AuthScope } from '../../../../auth-scope'\nimport { isUserOrAdmin } from '../../../../auth-verifier'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getBlob({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      additional: [AuthScope.Takendown],\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, res, auth }) => {\n      const { did } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const cid = CID.parse(params.cid)\n      const found = await ctx.actorStore.read(params.did, async (store) => {\n        try {\n          return await store.repo.blob.getBlob(cid)\n        } catch (err) {\n          if (err instanceof BlobNotFoundError) {\n            throw new InvalidRequestError('Blob not found')\n          } else {\n            throw err\n          }\n        }\n      })\n      if (!found) {\n        throw new InvalidRequestError('Blob not found')\n      }\n      res.setHeader('content-length', found.size)\n\n      // Important Security headers\n\n      // This prevents the browser from trying to guess the content type\n      // and potentially loading the blob as executable code, or rendering it\n      // in some other unsafe way.\n      res.setHeader('x-content-type-options', 'nosniff')\n\n      // This forces the browser to download the blob instead of trying to\n      // render it when visiting the URL. This is important to prevent XSS\n      // attacks if the blob happens to be HTML. Even if JS is disabled via the\n      // CSP header below, a blob could still contain malicious HTML links.\n      res.setHeader('content-disposition', `attachment; filename=\"${cid}\"`)\n\n      // This should prevent the browser from executing the blob in any way\n      res.setHeader('content-security-policy', `default-src 'none'; sandbox`)\n\n      return {\n        // @TODO better codegen for */* mimetype\n        encoding: (found.mimeType || 'application/octet-stream') as '*/*',\n        body: found.stream,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/getBlocks.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { byteIterableToStream } from '@atproto/common'\nimport { blocksToCarStream } from '@atproto/repo'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { isUserOrAdmin } from '../../../../auth-verifier'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getBlocks({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const cids = params.cids.map((c) => CID.parse(c))\n      const got = await ctx.actorStore.read(did, (store) =>\n        store.repo.storage.getBlocks(cids),\n      )\n      if (got.missing.length > 0) {\n        const missingStr = got.missing.map((c) => c.toString())\n        throw new InvalidRequestError(`Could not find cids: ${missingStr}`)\n      }\n      const car = blocksToCarStream(null, got.blocks)\n\n      return {\n        encoding: 'application/vnd.ipld.car',\n        body: byteIterableToStream(car),\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/getLatestCommit.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { isUserOrAdmin } from '../../../../auth-verifier'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getLatestCommit({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const root = await ctx.actorStore.read(did, (store) =>\n        store.repo.storage.getRootDetailed(),\n      )\n      if (root === null) {\n        throw new InvalidRequestError(\n          `Could not find root for DID: ${did}`,\n          'RepoNotFound',\n        )\n      }\n      return {\n        encoding: 'application/json',\n        body: { cid: root.cid.toString(), rev: root.rev },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/getRecord.ts",
    "content": "import stream from 'node:stream'\nimport { byteIterableToStream } from '@atproto/common'\nimport * as repo from '@atproto/repo'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { SqlRepoReader } from '../../../../actor-store/repo/sql-repo-reader'\nimport { isUserOrAdmin } from '../../../../auth-verifier'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getRecord({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did, collection, rkey } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      // must open up the db outside of store interface so that we can close the file handle after finished streaming\n      const actorDb = await ctx.actorStore.openDb(did)\n\n      let carStream: stream.Readable\n      try {\n        const storage = new SqlRepoReader(actorDb)\n        const commit = await storage.getRoot()\n\n        if (!commit) {\n          throw new InvalidRequestError(`Could not find repo for DID: ${did}`)\n        }\n        const carIter = repo.getRecords(storage, commit, [{ collection, rkey }])\n        carStream = byteIterableToStream(carIter)\n      } catch (err) {\n        actorDb.close()\n        throw err\n      }\n      const closeDb = () => actorDb.close()\n      carStream.on('error', closeDb)\n      carStream.on('close', closeDb)\n\n      return {\n        encoding: 'application/vnd.ipld.car',\n        body: carStream,\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/getRepo.ts",
    "content": "import stream from 'node:stream'\nimport { byteIterableToStream } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport {\n  RepoRootNotFoundError,\n  SqlRepoReader,\n} from '../../../../actor-store/repo/sql-repo-reader'\nimport { AuthScope } from '../../../../auth-scope'\nimport { isUserOrAdmin } from '../../../../auth-verifier'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getRepo({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      additional: [AuthScope.Takendown],\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did, since } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const carStream = await getCarStream(ctx, did, since)\n\n      return {\n        encoding: 'application/vnd.ipld.car',\n        body: carStream,\n      }\n    },\n  })\n}\n\nexport const getCarStream = async (\n  ctx: AppContext,\n  did: string,\n  since?: string,\n): Promise<stream.Readable> => {\n  const actorDb = await ctx.actorStore.openDb(did)\n  let carStream: stream.Readable\n  try {\n    const storage = new SqlRepoReader(actorDb)\n    const carIter = await storage.getCarStream(since)\n    carStream = byteIterableToStream(carIter)\n  } catch (err) {\n    await actorDb.close()\n    if (err instanceof RepoRootNotFoundError) {\n      throw new InvalidRequestError(`Could not find repo for DID: ${did}`)\n    }\n    throw err\n  }\n  const closeDb = () => actorDb.close()\n  carStream.on('error', closeDb)\n  carStream.on('close', closeDb)\n  return carStream\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/getRepoStatus.ts",
    "content": "import { formatAccountStatus } from '../../../../account-manager/account-manager'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.getRepoStatus({\n    handler: async ({ params }) => {\n      const { did } = params\n      const account = await assertRepoAvailability(ctx, did, true)\n\n      const { active, status } = formatAccountStatus(account)\n\n      let rev: string | undefined = undefined\n      if (active) {\n        const root = await ctx.actorStore.read(did, (store) =>\n          store.repo.storage.getRootDetailed(),\n        )\n        rev = root.rev\n      }\n\n      return {\n        encoding: 'application/json',\n        body: {\n          did,\n          active,\n          status,\n          rev,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport getCheckout from './deprecated/getCheckout'\nimport getHead from './deprecated/getHead'\nimport getBlob from './getBlob'\nimport getBlocks from './getBlocks'\nimport getLatestCommit from './getLatestCommit'\nimport getRecord from './getRecord'\nimport getRepo from './getRepo'\nimport getRepoStatus from './getRepoStatus'\nimport listBlobs from './listBlobs'\nimport listRepos from './listRepos'\nimport subscribeRepos from './subscribeRepos'\n\nexport default function (server: Server, ctx: AppContext) {\n  getBlob(server, ctx)\n  getBlocks(server, ctx)\n  getLatestCommit(server, ctx)\n  getRepoStatus(server, ctx)\n  getRecord(server, ctx)\n  getRepo(server, ctx)\n  subscribeRepos(server, ctx)\n  listBlobs(server, ctx)\n  listRepos(server, ctx)\n  getCheckout(server, ctx)\n  getHead(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/listBlobs.ts",
    "content": "import { AuthScope } from '../../../../auth-scope'\nimport { isUserOrAdmin } from '../../../../auth-verifier'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { assertRepoAvailability } from './util'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.listBlobs({\n    auth: ctx.authVerifier.authorizationOrAdminTokenOptional({\n      additional: [AuthScope.Takendown],\n      authorize: () => {\n        // always allow\n      },\n    }),\n    handler: async ({ params, auth }) => {\n      const { did, since, limit, cursor } = params\n      await assertRepoAvailability(ctx, did, isUserOrAdmin(auth, did))\n\n      const blobCids = await ctx.actorStore.read(did, (store) =>\n        store.repo.blob.listBlobs({ since, limit, cursor }),\n      )\n\n      return {\n        encoding: 'application/json',\n        body: {\n          cursor: blobCids.at(-1),\n          cids: blobCids,\n        },\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/listRepos.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { formatAccountStatus } from '../../../../account-manager/account-manager'\nimport { AppContext } from '../../../../context'\nimport { Cursor, GenericKeyset, paginate } from '../../../../db/pagination'\nimport { Server } from '../../../../lexicon'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.listRepos(async ({ params }) => {\n    const { limit, cursor } = params\n    const db = ctx.accountManager.db\n    const { ref } = db.db.dynamic\n    let builder = db.db\n      .selectFrom('actor')\n      .innerJoin('repo_root', 'repo_root.did', 'actor.did')\n      .select([\n        'actor.did as did',\n        'repo_root.cid as head',\n        'repo_root.rev as rev',\n        'actor.createdAt as createdAt',\n        'actor.deactivatedAt as deactivatedAt',\n        'actor.takedownRef as takedownRef',\n      ])\n    const keyset = new TimeDidKeyset(ref('actor.createdAt'), ref('actor.did'))\n    builder = paginate(builder, {\n      limit,\n      cursor,\n      keyset,\n      direction: 'asc',\n      tryIndex: true,\n    })\n    const res = await builder.execute()\n    const repos = res.map((row) => {\n      const { active, status } = formatAccountStatus(row)\n      return {\n        did: row.did,\n        head: row.head,\n        rev: row.rev ?? '',\n        active,\n        status,\n      }\n    })\n    return {\n      encoding: 'application/json',\n      body: {\n        cursor: keyset.packFromResult(res),\n        repos,\n      },\n    }\n  })\n}\n\ntype TimeDidResult = { createdAt: string; did: string }\n\nexport class TimeDidKeyset extends GenericKeyset<TimeDidResult, Cursor> {\n  labelResult(result: TimeDidResult): Cursor {\n    return { primary: result.createdAt, secondary: result.did }\n  }\n  labeledResultToCursor(labeled: Cursor) {\n    return {\n      primary: new Date(labeled.primary).getTime().toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    const primaryDate = new Date(parseInt(cursor.primary, 10))\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n      secondary: cursor.secondary,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/subscribeRepos.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { httpLogger } from '../../../../logger'\nimport { Outbox } from '../../../../sequencer/outbox'\n\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.sync.subscribeRepos(async function* ({ params, signal }) {\n    const { cursor } = params\n    const outbox = new Outbox(ctx.sequencer, {\n      maxBufferSize: ctx.cfg.subscription.maxBuffer,\n    })\n    httpLogger.info({ cursor }, 'request to com.atproto.sync.subscribeRepos')\n\n    const backfillTime = new Date(\n      Date.now() - ctx.cfg.subscription.repoBackfillLimitMs,\n    ).toISOString()\n    let outboxCursor: number | undefined = undefined\n    if (cursor !== undefined) {\n      const [next, curr] = await Promise.all([\n        ctx.sequencer.next(cursor),\n        ctx.sequencer.curr(),\n      ])\n      if (cursor > (curr ?? 0)) {\n        throw new InvalidRequestError('Cursor in the future.', 'FutureCursor')\n      } else if (next && next.sequencedAt < backfillTime) {\n        // if cursor is before backfill time, find earliest cursor from backfill window\n        yield {\n          $type: '#info',\n          name: 'OutdatedCursor',\n          message: 'Requested cursor exceeded limit. Possibly missing events',\n        }\n        const startEvt = await ctx.sequencer.earliestAfterTime(backfillTime)\n        outboxCursor = startEvt?.seq ? startEvt.seq - 1 : undefined\n      } else {\n        outboxCursor = cursor\n      }\n    }\n\n    for await (const evt of outbox.events(outboxCursor, signal)) {\n      if (evt.type === 'commit') {\n        yield {\n          $type: '#commit',\n          seq: evt.seq,\n          time: evt.time,\n          ...evt.evt,\n        }\n      } else if (evt.type === 'sync') {\n        yield {\n          $type: '#sync',\n          seq: evt.seq,\n          time: evt.time,\n          ...evt.evt,\n        }\n      } else if (evt.type === 'identity') {\n        yield {\n          $type: '#identity',\n          seq: evt.seq,\n          time: evt.time,\n          ...evt.evt,\n        }\n      } else if (evt.type === 'account') {\n        yield {\n          $type: '#account',\n          seq: evt.seq,\n          time: evt.time,\n          ...evt.evt,\n        }\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/sync/util.ts",
    "content": "import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { ActorAccount } from '../../../../account-manager/helpers/account'\nimport { AppContext } from '../../../../context'\n\nexport const assertRepoAvailability = async (\n  ctx: AppContext,\n  did: string,\n  isAdminOrSelf: boolean,\n): Promise<ActorAccount> => {\n  const account = await ctx.accountManager.getAccount(did, {\n    includeDeactivated: true,\n    includeTakenDown: true,\n  })\n  if (!account) {\n    throw new InvalidRequestError(\n      `Could not find repo for DID: ${did}`,\n      'RepoNotFound',\n    )\n  }\n  if (isAdminOrSelf) {\n    return account\n  }\n  if (account.takedownRef) {\n    throw new InvalidRequestError(\n      `Repo has been takendown: ${did}`,\n      'RepoTakendown',\n    )\n  }\n  if (account.deactivatedAt) {\n    throw new InvalidRequestError(\n      `Repo has been deactivated: ${did}`,\n      'RepoDeactivated',\n    )\n  }\n  return account\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/temp/checkSignupQueue.ts",
    "content": "import { ForbiddenError } from '@atproto/xrpc-server'\nimport { AuthScope } from '../../../../auth-scope'\nimport { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport { resultPassthru } from '../../../proxy'\n\n// THIS IS A TEMPORARY UNSPECCED ROUTE\nexport default function (server: Server, ctx: AppContext) {\n  server.com.atproto.temp.checkSignupQueue({\n    auth: ctx.authVerifier.authorization({\n      additional: [AuthScope.SignupQueued],\n      authorize: () => {\n        throw new ForbiddenError(\n          'OAuth credentials are not supported for this endpoint',\n        )\n      },\n    }),\n    handler: async ({ req }) => {\n      if (!ctx.entrywayAgent) {\n        return {\n          encoding: 'application/json',\n          body: {\n            activated: true,\n          },\n        }\n      }\n      return resultPassthru(\n        await ctx.entrywayAgent.com.atproto.temp.checkSignupQueue(\n          undefined,\n          ctx.entrywayPassthruHeaders(req),\n        ),\n      )\n    },\n  })\n}\n"
  },
  {
    "path": "packages/pds/src/api/com/atproto/temp/index.ts",
    "content": "import { AppContext } from '../../../../context'\nimport { Server } from '../../../../lexicon'\nimport checkSignupQueue from './checkSignupQueue'\n\nexport default function (server: Server, ctx: AppContext) {\n  checkSignupQueue(server, ctx)\n}\n"
  },
  {
    "path": "packages/pds/src/api/index.ts",
    "content": "import { AppContext } from '../context'\nimport { Server } from '../lexicon'\nimport appBsky from './app/bsky'\nimport comAtproto from './com/atproto'\n\nexport default function (server: Server, ctx: AppContext) {\n  comAtproto(server, ctx)\n  appBsky(server, ctx)\n  return server\n}\n"
  },
  {
    "path": "packages/pds/src/api/proxy.ts",
    "content": "import { IncomingMessage } from 'node:http'\nimport express from 'express'\nimport { Headers } from '@atproto/xrpc'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\n\nexport const resultPassthru = <T>(result: { headers: Headers; data: T }) => {\n  // @TODO pass through any headers that we always want to forward along\n  return {\n    encoding: 'application/json' as const,\n    body: result.data,\n  }\n}\n\nexport function authPassthru(req: IncomingMessage) {\n  const { authorization } = req.headers\n\n  if (authorization) {\n    // DPoP requests are bound to the endpoint being called. Allowing them to be\n    // proxied would require that the receiving end allows DPoP proof not\n    // created for him. Since proxying is mainly there to support legacy\n    // clients, and DPoP is a new feature, we don't support DPoP requests\n    // through the proxy.\n\n    // This is fine since app views are usually called using the requester's\n    // credentials when \"auth.credentials.type === 'access'\", which is the only\n    // case were DPoP is used.\n    const [type] = authorization.split(' ', 1)\n    if (!type) {\n      throw new InvalidRequestError('Invalid authorization header')\n    }\n    if (type.toLowerCase() === 'dpop' || req.headers['dpop']) {\n      throw new InvalidRequestError('DPoP requests cannot be proxied')\n    }\n\n    return { headers: { authorization } }\n  }\n}\n\n// @NOTE this function may mutate its params input\n// future improvement here would be to forward along all untrusted ips rather than just the first (req.ip)\nexport const forwardedFor = (\n  req: express.Request,\n  params: HeadersParam | undefined,\n) => {\n  const result: HeadersParam = params ?? { headers: {} }\n  const ip = req.ip\n  if (ip) {\n    result.headers['x-forwarded-for'] = ip\n  }\n  return result\n}\n\ntype HeadersParam = { headers: Record<string, string> }\n"
  },
  {
    "path": "packages/pds/src/app-view.ts",
    "content": "import { format } from 'node:util'\nimport { AtpAgent } from '@atproto/api'\n\nexport type AppViewOptions = {\n  url: string\n  did: string\n  cdnUrlPattern?: string\n}\n\nexport class AppView {\n  public did: string\n  public agent: AtpAgent\n  private cdnUrlPattern?: string\n\n  constructor(options: AppViewOptions) {\n    this.did = options.did\n    this.agent = new AtpAgent({ service: options.url })\n    this.cdnUrlPattern = options.cdnUrlPattern\n  }\n\n  getImageUrl(pattern: string, did: string, cid: string): string | undefined {\n    if (this.cdnUrlPattern) return format(this.cdnUrlPattern, pattern, did, cid)\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/auth-output.ts",
    "content": "import { ScopePermissions } from '@atproto/oauth-scopes'\nimport { AuthScope } from './auth-scope'\n\nexport type UnauthenticatedOutput = {\n  credentials: null\n}\n\nexport type AdminTokenOutput = {\n  credentials: {\n    type: 'admin_token'\n  }\n}\n\nexport type ModServiceOutput = {\n  credentials: {\n    type: 'mod_service'\n    did: string\n  }\n}\n\nexport type AccessOutput<S extends AuthScope = AuthScope> = {\n  credentials: {\n    type: 'access'\n    did: string\n    scope: S\n  }\n}\n\nexport type OAuthOutput = {\n  credentials: {\n    type: 'oauth'\n    did: string\n    permissions: ScopePermissions\n  }\n}\n\nexport type RefreshOutput = {\n  credentials: {\n    type: 'refresh'\n    did: string\n    scope: AuthScope.Refresh\n    tokenId: string\n  }\n}\n\nexport type UserServiceAuthOutput = {\n  credentials: {\n    type: 'user_service_auth'\n    did: string\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/auth-routes.ts",
    "content": "import { Router } from 'express'\nimport {\n  HandleUnavailableError,\n  InvalidRequestError,\n  SecondAuthenticationFactorRequiredError,\n  UseDpopNonceError,\n  oauthMiddleware,\n  oauthProtectedResourceMetadataSchema,\n} from '@atproto/oauth-provider'\nimport { AppContext } from './context.js'\nimport { oauthLogger, reqSerializer } from './logger.js'\n\nexport const createRouter = ({ oauthProvider, cfg }: AppContext): Router => {\n  const router = Router()\n\n  const oauthProtectedResourceMetadata =\n    oauthProtectedResourceMetadataSchema.parse({\n      resource: cfg.service.publicUrl,\n      authorization_servers: [cfg.entryway?.url ?? cfg.service.publicUrl],\n      bearer_methods_supported: ['header'],\n      scopes_supported: [],\n      resource_documentation: 'https://atproto.com',\n    })\n\n  if (\n    !cfg.service.devMode &&\n    !oauthProtectedResourceMetadata.resource.startsWith('https://')\n  ) {\n    throw new Error('Resource URL must use the https scheme')\n  }\n\n  router.get('/.well-known/oauth-protected-resource', (req, res) => {\n    res.setHeader('Access-Control-Allow-Origin', '*')\n    res.setHeader('Access-Control-Allow-Method', '*')\n    res.setHeader('Access-Control-Allow-Headers', '*')\n    res.status(200).json(oauthProtectedResourceMetadata)\n  })\n\n  if (oauthProvider) {\n    router.use(\n      oauthMiddleware(oauthProvider, {\n        onError: (req, res, err, msg) => {\n          if (!ignoreError(err)) {\n            oauthLogger.error({ err, req: reqSerializer(req) }, msg)\n          }\n        },\n      }),\n    )\n  }\n\n  return router\n}\n\nfunction ignoreError(err: unknown): boolean {\n  if (err instanceof InvalidRequestError) {\n    return err.error_description === 'Invalid identifier or password'\n  }\n\n  return (\n    err instanceof UseDpopNonceError ||\n    err instanceof HandleUnavailableError ||\n    err instanceof SecondAuthenticationFactorRequiredError\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/auth-scope.ts",
    "content": "// @TODO sync-up with current method names, consider backwards compat.\nexport enum AuthScope {\n  Access = 'com.atproto.access',\n  Refresh = 'com.atproto.refresh',\n  AppPass = 'com.atproto.appPass',\n  AppPassPrivileged = 'com.atproto.appPassPrivileged',\n  SignupQueued = 'com.atproto.signupQueued',\n  Takendown = 'com.atproto.takendown',\n}\n\nexport const ACCESS_FULL = [AuthScope.Access] as const\nexport const ACCESS_PRIVILEGED = [\n  ...ACCESS_FULL,\n  AuthScope.AppPassPrivileged,\n] as const\nexport const ACCESS_STANDARD = [\n  ...ACCESS_PRIVILEGED,\n  AuthScope.AppPass,\n] as const\n\nconst authScopesValues = new Set(Object.values(AuthScope))\nexport function isAuthScope(val: unknown): val is AuthScope {\n  return (authScopesValues as Set<unknown>).has(val)\n}\n\nexport function isAccessFull(\n  scope: AuthScope,\n): scope is (typeof ACCESS_FULL)[number] {\n  return (ACCESS_FULL as readonly string[]).includes(scope)\n}\n\nexport function isAccessPrivileged(\n  scope: AuthScope,\n): scope is (typeof ACCESS_PRIVILEGED)[number] {\n  return (ACCESS_PRIVILEGED as readonly string[]).includes(scope)\n}\n\nexport function isTakendown(scope: unknown): scope is AuthScope.Takendown {\n  return scope === AuthScope.Takendown\n}\n"
  },
  {
    "path": "packages/pds/src/auth-verifier.ts",
    "content": "import { KeyObject, createPublicKey, createSecretKey } from 'node:crypto'\nimport { IncomingMessage, ServerResponse } from 'node:http'\nimport * as jose from 'jose'\nimport KeyEncoder from 'key-encoder'\nimport { getVerificationMaterial } from '@atproto/common'\nimport { IdResolver, getDidKeyFromMultibase } from '@atproto/identity'\nimport {\n  OAuthError,\n  OAuthVerifier,\n  VerifyTokenPayloadOptions,\n  WWWAuthenticateError,\n} from '@atproto/oauth-provider'\nimport {\n  ScopePermissions,\n  ScopePermissionsTransition,\n} from '@atproto/oauth-scopes'\nimport {\n  AuthRequiredError,\n  Awaitable,\n  ForbiddenError,\n  InvalidRequestError,\n  MethodAuthContext,\n  MethodAuthVerifier,\n  Params,\n  XRPCError,\n  parseReqNsid,\n  verifyJwt as verifyServiceJwt,\n} from '@atproto/xrpc-server'\nimport { AccountManager } from './account-manager/account-manager'\nimport { ActorAccount } from './account-manager/helpers/account'\nimport {\n  AccessOutput,\n  AdminTokenOutput,\n  ModServiceOutput,\n  OAuthOutput,\n  RefreshOutput,\n  UnauthenticatedOutput,\n  UserServiceAuthOutput,\n} from './auth-output'\nimport { ACCESS_STANDARD, AuthScope, isAuthScope } from './auth-scope'\nimport { softDeleted } from './db'\nimport { appendVary } from './util/http'\nimport { WithRequired } from './util/types'\n\nexport type VerifiedOptions = {\n  checkTakedown?: boolean\n  checkDeactivated?: boolean\n}\n\nexport type ScopedOptions<S extends AuthScope = AuthScope> = {\n  scopes?: readonly S[]\n}\n\nexport type ExtraScopedOptions<S extends AuthScope = AuthScope> = {\n  additional?: readonly S[]\n}\n\nexport type AuthorizedOptions<P extends Params = Params> = {\n  authorize: (\n    permissions: ScopePermissions,\n    ctx: MethodAuthContext<P>,\n  ) => Awaitable<void>\n}\n\nexport type AuthVerifierOpts = {\n  publicUrl: string\n  jwtKey: KeyObject\n  adminPass: string\n  dids: {\n    pds: string\n    entryway?: string\n    modService?: string\n  }\n}\n\nexport type VerifyBearerJwtOptions<S extends AuthScope = AuthScope> =\n  WithRequired<\n    Omit<jose.JWTVerifyOptions, 'scopes'> & {\n      scopes: readonly S[]\n    },\n    'audience' | 'typ'\n  >\n\nexport type VerifyBearerJwtResult<S extends AuthScope = AuthScope> = {\n  sub: string\n  aud: string\n  jti: string | undefined\n  scope: S\n}\n\nexport class AuthVerifier {\n  private _publicUrl: string\n  private _jwtKey: KeyObject\n  private _adminPass: string\n  public dids: AuthVerifierOpts['dids']\n\n  constructor(\n    public accountManager: AccountManager,\n    public idResolver: IdResolver,\n    public oauthVerifier: OAuthVerifier,\n    opts: AuthVerifierOpts,\n  ) {\n    this._publicUrl = opts.publicUrl\n    this._jwtKey = opts.jwtKey\n    this._adminPass = opts.adminPass\n    this.dids = opts.dids\n  }\n\n  // verifiers (arrow fns to preserve scope)\n\n  public unauthenticated: MethodAuthVerifier<UnauthenticatedOutput> = (ctx) => {\n    setAuthHeaders(ctx.res)\n\n    // @NOTE this auth method is typically used as fallback when no other auth\n    // method is applicable. This means that the presence of an \"authorization\"\n    // header means that that header is invalid (as it did not match any of the\n    // other auth methods).\n    if (ctx.req.headers['authorization']) {\n      throw new AuthRequiredError('Invalid authorization header')\n    }\n\n    return {\n      credentials: null,\n    }\n  }\n\n  public adminToken: MethodAuthVerifier<AdminTokenOutput> = async (ctx) => {\n    setAuthHeaders(ctx.res)\n    const parsed = parseBasicAuth(ctx.req)\n    if (!parsed) {\n      throw new AuthRequiredError()\n    }\n    const { username, password } = parsed\n    if (username !== 'admin' || password !== this._adminPass) {\n      throw new AuthRequiredError()\n    }\n\n    return { credentials: { type: 'admin_token' } }\n  }\n\n  public modService: MethodAuthVerifier<ModServiceOutput> = async (ctx) => {\n    setAuthHeaders(ctx.res)\n    if (!this.dids.modService) {\n      throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')\n    }\n    const payload = await this.verifyServiceJwt(ctx.req, {\n      iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`],\n    })\n    return {\n      credentials: {\n        type: 'mod_service',\n        did: payload.iss,\n      },\n    }\n  }\n\n  public moderator: MethodAuthVerifier<AdminTokenOutput | ModServiceOutput> =\n    async (ctx) => {\n      const type = extractAuthType(ctx.req)\n      if (type === AuthType.BEARER) {\n        return this.modService(ctx)\n      } else {\n        return this.adminToken(ctx)\n      }\n    }\n\n  protected access<S extends AuthScope>(\n    options: VerifiedOptions & Required<ScopedOptions<S>>,\n  ): MethodAuthVerifier<AccessOutput<S>> {\n    const { scopes, ...statusOptions } = options\n\n    const verifyJwtOptions: VerifyBearerJwtOptions<S> = {\n      audience: this.dids.pds,\n      typ: 'at+jwt',\n      scopes:\n        // @NOTE We can reject taken down credentials based on the scope if\n        // \"checkTakedown\" is set.\n        statusOptions.checkTakedown && scopes.includes(AuthScope.Takendown as S)\n          ? scopes.filter((s) => s !== AuthScope.Takendown)\n          : scopes,\n    }\n\n    return async (ctx) => {\n      setAuthHeaders(ctx.res)\n\n      const { sub: did, scope } = await this.verifyBearerJwt(\n        ctx.req,\n        verifyJwtOptions,\n      )\n\n      await this.verifyStatus(did, statusOptions)\n\n      return {\n        credentials: { type: 'access', did, scope },\n      }\n    }\n  }\n\n  public refresh(options?: {\n    allowExpired?: boolean\n  }): MethodAuthVerifier<RefreshOutput> {\n    const verifyOptions: VerifyBearerJwtOptions<AuthScope.Refresh> = {\n      clockTolerance: options?.allowExpired ? Infinity : undefined,\n      typ: 'refresh+jwt',\n      // when using entryway, proxying refresh credentials\n      audience: this.dids.entryway ? this.dids.entryway : this.dids.pds,\n      scopes: [AuthScope.Refresh],\n    }\n\n    return async (ctx) => {\n      setAuthHeaders(ctx.res)\n\n      const result = await this.verifyBearerJwt(ctx.req, verifyOptions)\n\n      const tokenId = result.jti\n      if (!tokenId) {\n        throw new AuthRequiredError(\n          'Unexpected missing refresh token id',\n          'MissingTokenId',\n        )\n      }\n\n      return {\n        credentials: {\n          type: 'refresh',\n          did: result.sub,\n          scope: result.scope,\n          tokenId,\n        },\n      }\n    }\n  }\n\n  public authorization<P extends Params>({\n    scopes = ACCESS_STANDARD,\n    additional = [],\n    ...options\n  }: VerifiedOptions &\n    ScopedOptions &\n    ExtraScopedOptions &\n    AuthorizedOptions<P>): MethodAuthVerifier<AccessOutput | OAuthOutput, P> {\n    const access = this.access({\n      ...options,\n      scopes: [...scopes, ...additional],\n    })\n    const oauth = this.oauth(options)\n\n    return async (ctx) => {\n      const type = extractAuthType(ctx.req)\n\n      if (type === AuthType.BEARER) {\n        return access(ctx)\n      }\n\n      if (type === AuthType.DPOP) {\n        return oauth(ctx)\n      }\n\n      // Auth headers are set through the access and oauth methods so we only\n      // need to set them here if we reach this point\n      setAuthHeaders(ctx.res)\n\n      if (type !== null) {\n        throw new InvalidRequestError(\n          'Unexpected authorization type',\n          'InvalidToken',\n        )\n      }\n\n      throw new AuthRequiredError(undefined, 'AuthMissing')\n    }\n  }\n\n  public authorizationOrAdminTokenOptional<P extends Params>(\n    opts: VerifiedOptions & ExtraScopedOptions & AuthorizedOptions<P>,\n  ): MethodAuthVerifier<\n    OAuthOutput | AccessOutput | AdminTokenOutput | UnauthenticatedOutput,\n    P\n  > {\n    const authorization = this.authorization(opts)\n    return async (ctx) => {\n      const type = extractAuthType(ctx.req)\n      if (type === AuthType.BEARER || type === AuthType.DPOP) {\n        return authorization(ctx)\n      } else if (type === AuthType.BASIC) {\n        return this.adminToken(ctx)\n      } else {\n        return this.unauthenticated(ctx)\n      }\n    }\n  }\n\n  public userServiceAuth: MethodAuthVerifier<UserServiceAuthOutput> = async (\n    ctx,\n  ) => {\n    setAuthHeaders(ctx.res)\n    const payload = await this.verifyServiceJwt(ctx.req)\n    return {\n      credentials: {\n        type: 'user_service_auth',\n        did: payload.iss,\n      },\n    }\n  }\n\n  public userServiceAuthOptional: MethodAuthVerifier<\n    UserServiceAuthOutput | UnauthenticatedOutput\n  > = async (ctx) => {\n    const type = extractAuthType(ctx.req)\n    if (type === AuthType.BEARER) {\n      return await this.userServiceAuth(ctx)\n    } else {\n      return this.unauthenticated(ctx)\n    }\n  }\n\n  public authorizationOrUserServiceAuth<P extends Params>(\n    options: VerifiedOptions &\n      ScopedOptions &\n      ExtraScopedOptions &\n      AuthorizedOptions<P>,\n  ): MethodAuthVerifier<UserServiceAuthOutput | OAuthOutput | AccessOutput, P> {\n    const authorizationVerifier = this.authorization(options)\n    return async (ctx) => {\n      if (isDefinitelyServiceAuth(ctx.req)) {\n        return this.userServiceAuth(ctx)\n      } else {\n        return authorizationVerifier(ctx)\n      }\n    }\n  }\n\n  protected oauth<P extends Params>({\n    authorize,\n    ...verifyStatusOptions\n  }: VerifiedOptions & AuthorizedOptions<P>): MethodAuthVerifier<\n    OAuthOutput,\n    P\n  > {\n    const verifyTokenOptions: VerifyTokenPayloadOptions = {\n      audience: [this.dids.pds],\n      scope: ['atproto'],\n    }\n\n    return async (ctx) => {\n      setAuthHeaders(ctx.res)\n\n      const { req, res } = ctx\n\n      // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2\n      const dpopNonce = this.oauthVerifier.nextDpopNonce()\n      if (dpopNonce) {\n        res.setHeader('DPoP-Nonce', dpopNonce)\n        res.appendHeader('Access-Control-Expose-Headers', 'DPoP-Nonce')\n      }\n\n      const originalUrl = req.originalUrl || req.url || '/'\n      const url = new URL(originalUrl, this._publicUrl)\n\n      const { scope, sub: did } = await this.oauthVerifier\n        .authenticateRequest(\n          req.method || 'GET',\n          url,\n          req.headers,\n          verifyTokenOptions,\n        )\n        .catch((err) => {\n          // Make sure to include any WWW-Authenticate header in the response\n          // (particularly useful for DPoP's \"use_dpop_nonce\" error)\n          if (err instanceof WWWAuthenticateError) {\n            res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader)\n            res.appendHeader(\n              'Access-Control-Expose-Headers',\n              'WWW-Authenticate',\n            )\n          }\n\n          if (err instanceof OAuthError) {\n            throw new XRPCError(err.status, err.error_description, err.error)\n          }\n\n          throw err\n        })\n\n      if (typeof did !== 'string' || !did.startsWith('did:')) {\n        throw new InvalidRequestError('Malformed token', 'InvalidToken')\n      }\n\n      await this.verifyStatus(did, verifyStatusOptions)\n\n      const permissions = new ScopePermissionsTransition(scope?.split(' '))\n\n      // Should never happen\n      if (!permissions.scopes.has('atproto')) {\n        throw new InvalidRequestError(\n          'OAuth token does not have \"atproto\" scope',\n          'InvalidToken',\n        )\n      }\n\n      await authorize(permissions, ctx)\n\n      return {\n        credentials: {\n          type: 'oauth',\n          did,\n          permissions,\n        },\n      }\n    }\n  }\n\n  protected async verifyStatus(\n    did: string,\n    options: VerifiedOptions,\n  ): Promise<void> {\n    if (options.checkDeactivated || options.checkTakedown) {\n      await this.findAccount(did, options)\n    }\n  }\n\n  /**\n   * Finds an account by its handle or DID, returning possibly deactivated or\n   * taken down accounts (unless `options.checkDeactivated` or\n   * `options.checkTakedown` are set to true, respectively).\n   */\n  public async findAccount(\n    handleOrDid: string,\n    options: VerifiedOptions,\n  ): Promise<ActorAccount> {\n    const account = await this.accountManager.getAccount(handleOrDid, {\n      includeDeactivated: true,\n      includeTakenDown: true,\n    })\n    if (!account) {\n      // will be turned into ExpiredToken for the client if proxied by entryway\n      throw new ForbiddenError('Account not found', 'AccountNotFound')\n    }\n    if (options.checkTakedown && softDeleted(account)) {\n      throw new AuthRequiredError(\n        'Account has been taken down',\n        'AccountTakedown',\n      )\n    }\n    if (options.checkDeactivated && account.deactivatedAt) {\n      throw new AuthRequiredError(\n        'Account is deactivated',\n        'AccountDeactivated',\n      )\n    }\n    return account\n  }\n\n  /**\n   * Wraps {@link jose.jwtVerify} into a function that also validates the token\n   * payload's type and wraps errors into {@link InvalidRequestError}.\n   */\n  protected async verifyBearerJwt<S extends AuthScope = AuthScope>(\n    req: IncomingMessage,\n    { scopes, ...options }: VerifyBearerJwtOptions<S>,\n  ): Promise<VerifyBearerJwtResult<S>> {\n    const token = bearerTokenFromReq(req)\n    if (!token) {\n      throw new AuthRequiredError(undefined, 'AuthMissing')\n    }\n\n    const { payload, protectedHeader } = await jose\n      .jwtVerify(token, this._jwtKey, { ...options, typ: undefined })\n      .catch((cause) => {\n        if (cause instanceof jose.errors.JWTExpired) {\n          throw new InvalidRequestError('Token has expired', 'ExpiredToken', {\n            cause,\n          })\n        } else {\n          throw new InvalidRequestError(\n            'Token could not be verified',\n            'InvalidToken',\n            { cause },\n          )\n        }\n      })\n\n    // @NOTE: the \"typ\" is now set in production environments, so we should be\n    // able to safely check it through jose.jwtVerify(). However, tests depend\n    // on @atproto/pds-entryway which does not set \"typ\" in the access tokens.\n    // For that reason, we still allow it to be missing.\n    if (protectedHeader.typ && options.typ !== protectedHeader.typ) {\n      throw new InvalidRequestError('Invalid token type', 'InvalidToken')\n    }\n\n    const { sub, aud, scope, lxm, cnf, jti } = payload\n\n    if (typeof lxm !== 'undefined') {\n      // Service auth tokens should never make it to here. But since service\n      // auth tokens do not have a \"typ\" header, the \"typ\" check above will not\n      // catch them. This check here is mainly to protect against the\n      // hypothetical case in which a PDS would issue service auth tokens using\n      // its private key.\n      throw new InvalidRequestError('Malformed token', 'InvalidToken')\n    }\n    if (typeof cnf !== 'undefined') {\n      // Proof-of-Possession (PoP) tokens are not allowed here\n      // https://www.rfc-editor.org/rfc/rfc7800.html\n      throw new InvalidRequestError('Malformed token', 'InvalidToken')\n    }\n    if (typeof sub !== 'string' || !sub.startsWith('did:')) {\n      throw new InvalidRequestError('Malformed token', 'InvalidToken')\n    }\n    if (typeof aud !== 'string' || !aud.startsWith('did:')) {\n      throw new InvalidRequestError('Malformed token', 'InvalidToken')\n    }\n    if (typeof jti !== 'string' && typeof jti !== 'undefined') {\n      throw new InvalidRequestError('Malformed token', 'InvalidToken')\n    }\n    if (!isAuthScope(scope) || !scopes.includes(scope as any)) {\n      throw new InvalidRequestError('Bad token scope', 'InvalidToken')\n    }\n\n    return { sub, aud, jti, scope: scope as S }\n  }\n\n  protected async verifyServiceJwt(\n    req: IncomingMessage,\n    opts?: { iss?: string[] },\n  ) {\n    const getSigningKey = async (\n      iss: string,\n      forceRefresh: boolean,\n    ): Promise<string> => {\n      if (opts?.iss && !opts.iss.includes(iss)) {\n        throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')\n      }\n      const [did, serviceId] = iss.split('#')\n      const keyId =\n        serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto'\n      const didDoc = await this.idResolver.did.resolve(did, forceRefresh)\n      if (!didDoc) {\n        throw new AuthRequiredError('could not resolve iss did')\n      }\n      const parsedKey = getVerificationMaterial(didDoc, keyId)\n      if (!parsedKey) {\n        throw new AuthRequiredError('missing or bad key in did doc')\n      }\n      const didKey = getDidKeyFromMultibase(parsedKey)\n      if (!didKey) {\n        throw new AuthRequiredError('missing or bad key in did doc')\n      }\n      return didKey\n    }\n\n    const jwtStr = bearerTokenFromReq(req)\n    if (!jwtStr) {\n      throw new AuthRequiredError('missing jwt', 'MissingJwt')\n    }\n    const nsid = parseReqNsid(req)\n    const payload = await verifyServiceJwt(jwtStr, null, nsid, getSigningKey)\n    if (\n      payload.aud !== this.dids.pds &&\n      (!this.dids.entryway || payload.aud !== this.dids.entryway)\n    ) {\n      throw new AuthRequiredError(\n        'jwt audience does not match service did',\n        'BadJwtAudience',\n      )\n    }\n    return payload\n  }\n}\n\n// HELPERS\n// ---------\n\nexport function isUserOrAdmin(\n  auth: AccessOutput | OAuthOutput | AdminTokenOutput | UnauthenticatedOutput,\n  did: string,\n): boolean {\n  if (!auth.credentials) {\n    return false\n  } else if (auth.credentials.type === 'admin_token') {\n    return true\n  } else {\n    return auth.credentials.did === did\n  }\n}\n\nenum AuthType {\n  BASIC = 'Basic',\n  BEARER = 'Bearer',\n  DPOP = 'DPoP',\n}\n\nconst parseAuthorizationHeader = (\n  req: IncomingMessage,\n): [type: null] | [type: AuthType, token: string] => {\n  const authorization = req.headers['authorization']\n  if (!authorization) return [null]\n\n  const result = authorization.split(' ')\n  if (result.length !== 2) {\n    throw new InvalidRequestError(\n      'Malformed authorization header',\n      'InvalidToken',\n    )\n  }\n\n  // authorization type is case-insensitive\n  const authType = result[0].toUpperCase()\n\n  const type = Object.hasOwn(AuthType, authType) ? AuthType[authType] : null\n  if (type) return [type, result[1]]\n\n  throw new InvalidRequestError(\n    `Unsupported authorization type: ${result[0]}`,\n    'InvalidToken',\n  )\n}\n\n/**\n * @note Not all service auth tokens are guaranteed to have \"lxm\" claim, so this\n * function should not be used to verify service auth tokens. It is only used to\n * check if a token is definitely a service auth token.\n */\nconst isDefinitelyServiceAuth = (req: IncomingMessage): boolean => {\n  const token = bearerTokenFromReq(req)\n  if (!token) return false\n  const payload = jose.decodeJwt(token)\n  return payload['lxm'] != null\n}\n\nconst extractAuthType = (req: IncomingMessage): AuthType | null => {\n  const [type] = parseAuthorizationHeader(req)\n  return type\n}\n\nconst bearerTokenFromReq = (req: IncomingMessage) => {\n  const [type, token] = parseAuthorizationHeader(req)\n  return type === AuthType.BEARER ? token : null\n}\n\nconst parseBasicAuth = (\n  req: IncomingMessage,\n): { username: string; password: string } | null => {\n  try {\n    const [type, b64] = parseAuthorizationHeader(req)\n    if (type !== AuthType.BASIC) return null\n    const decoded = Buffer.from(b64, 'base64').toString('utf8')\n    // We must not use split(':') because the password can contain colons\n    const colon = decoded.indexOf(':')\n    if (colon === -1) return null\n    const username = decoded.slice(0, colon)\n    const password = decoded.slice(colon + 1)\n    return { username, password }\n  } catch (err) {\n    return null\n  }\n}\n\nexport const createSecretKeyObject = (secret: string): KeyObject => {\n  return createSecretKey(Buffer.from(secret))\n}\n\nconst keyEncoder = new KeyEncoder('secp256k1')\nexport const createPublicKeyObject = (publicKeyHex: string): KeyObject => {\n  const key = keyEncoder.encodePublic(publicKeyHex, 'raw', 'pem')\n  return createPublicKey({ format: 'pem', key })\n}\n\nfunction setAuthHeaders(res: ServerResponse) {\n  res.setHeader('Cache-Control', 'private')\n  appendVary(res, 'Authorization')\n}\n"
  },
  {
    "path": "packages/pds/src/background.ts",
    "content": "import PQueue from 'p-queue'\nimport { dbLogger } from './logger'\n\n// A simple queue for in-process, out-of-band/backgrounded work\n\nexport class BackgroundQueue {\n  queue = new PQueue({ concurrency: 5 })\n  destroyed = false\n  constructor() {}\n\n  add(task: Task) {\n    if (this.destroyed) {\n      return\n    }\n    this.queue\n      .add(() => task())\n      .catch((err) => {\n        dbLogger.error({ err }, 'background queue task failed')\n      })\n  }\n\n  async processAll() {\n    await this.queue.onIdle()\n  }\n\n  // On destroy we stop accepting new tasks, but complete all pending/in-progress tasks.\n  // The application calls this only once http connections have drained (tasks no longer being added).\n  async destroy() {\n    this.destroyed = true\n    await this.queue.onIdle()\n  }\n}\n\ntype Task = () => Promise<void>\n"
  },
  {
    "path": "packages/pds/src/basic-routes.ts",
    "content": "import { Router } from 'express'\nimport { sql } from 'kysely'\nimport { AppContext } from './context'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  router.get('/', function (req, res) {\n    res.type('text/plain')\n    res.send(`\n         __                         __\n        /\\\\ \\\\__                     /\\\\ \\\\__\n    __  \\\\ \\\\ ,_\\\\  _____   _ __   ___\\\\ \\\\ ,_\\\\   ___\n  /'__'\\\\ \\\\ \\\\ \\\\/ /\\\\ '__'\\\\/\\\\''__\\\\/ __'\\\\ \\\\ \\\\/  / __'\\\\\n /\\\\ \\\\L\\\\.\\\\_\\\\ \\\\ \\\\_\\\\ \\\\ \\\\L\\\\ \\\\ \\\\ \\\\//\\\\ \\\\L\\\\ \\\\ \\\\ \\\\_/\\\\ \\\\L\\\\ \\\\\n \\\\ \\\\__/.\\\\_\\\\\\\\ \\\\__\\\\\\\\ \\\\ ,__/\\\\ \\\\_\\\\\\\\ \\\\____/\\\\ \\\\__\\\\ \\\\____/\n  \\\\/__/\\\\/_/ \\\\/__/ \\\\ \\\\ \\\\/  \\\\/_/ \\\\/___/  \\\\/__/\\\\/___/\n                   \\\\ \\\\_\\\\\n                    \\\\/_/\n\n\nThis is an AT Protocol Personal Data Server (aka, an atproto PDS)\n\nMost API routes are under /xrpc/\n\n      Code: https://github.com/bluesky-social/atproto\n Self-Host: https://github.com/bluesky-social/pds\n  Protocol: https://atproto.com\n`)\n  })\n\n  router.get('/robots.txt', function (req, res) {\n    res.type('text/plain')\n    res.send(\n      '# Hello!\\n\\n# Crawling the public API is allowed\\nUser-agent: *\\nAllow: /',\n    )\n  })\n\n  router.get('/xrpc/_health', async function (req, res) {\n    const { version } = ctx.cfg.service\n    try {\n      await sql`select 1`.execute(ctx.accountManager.db.db)\n    } catch (err) {\n      req.log.error({ err }, 'failed health check')\n      res.status(503).send({ version, error: 'Service Unavailable' })\n      return\n    }\n    res.send({ version })\n  })\n\n  return router\n}\n"
  },
  {
    "path": "packages/pds/src/bsky-app-view.ts",
    "content": "import { format } from 'node:util'\nimport { AtpAgent } from '@atproto/api'\n\nexport type AppViewOptions = {\n  url: string\n  did: string\n  cdnUrlPattern?: string\n}\n\nexport class BskyAppView {\n  public did: string\n  public url: string\n  public agent: AtpAgent\n  private cdnUrlPattern?: string\n\n  constructor(options: AppViewOptions) {\n    this.did = options.did\n    this.url = options.url\n    this.agent = new AtpAgent({ service: options.url })\n    this.cdnUrlPattern = options.cdnUrlPattern\n  }\n\n  getImageUrl(pattern: string, did: string, cid: string): string | undefined {\n    if (this.cdnUrlPattern) return format(this.cdnUrlPattern, pattern, did, cid)\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/config/config.ts",
    "content": "import assert from 'node:assert'\nimport path from 'node:path'\nimport { DAY, HOUR, SECOND } from '@atproto/common'\nimport { BrandingInput, HcaptchaConfig } from '@atproto/oauth-provider'\nimport { ensureValidDid } from '@atproto/syntax'\nimport { ServerEnvironment } from './env'\n\n// off-config but still from env:\n// logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION\n\nexport const envToCfg = (env: ServerEnvironment): ServerConfig => {\n  const port = env.port ?? 2583\n  const hostname = env.hostname ?? 'localhost'\n  const publicUrl =\n    hostname === 'localhost'\n      ? `http://localhost:${port}`\n      : `https://${hostname}`\n  const did = env.serviceDid ?? `did:web:${hostname}`\n  const serviceCfg: ServerConfig['service'] = {\n    port,\n    hostname,\n    publicUrl,\n    did,\n    version: env.version, // default?\n    privacyPolicyUrl: env.privacyPolicyUrl,\n    termsOfServiceUrl: env.termsOfServiceUrl,\n    contactEmailAddress: env.contactEmailAddress,\n    acceptingImports: env.acceptingImports ?? true,\n    maxImportSize: env.maxImportSize,\n    blobUploadLimit: env.blobUploadLimit ?? 5 * 1024 * 1024, // 5mb\n    devMode: env.devMode ?? false,\n  }\n\n  const dbLoc = (name: string) => {\n    return env.dataDirectory ? path.join(env.dataDirectory, name) : name\n  }\n\n  const disableWalAutoCheckpoint = env.disableWalAutoCheckpoint ?? false\n\n  const dbCfg: ServerConfig['db'] = {\n    accountDbLoc: env.accountDbLocation ?? dbLoc('account.sqlite'),\n    sequencerDbLoc: env.sequencerDbLocation ?? dbLoc('sequencer.sqlite'),\n    didCacheDbLoc: env.didCacheDbLocation ?? dbLoc('did_cache.sqlite'),\n    disableWalAutoCheckpoint,\n  }\n\n  const actorStoreCfg: ServerConfig['actorStore'] = {\n    directory: env.actorStoreDirectory ?? dbLoc('actors'),\n    cacheSize: env.actorStoreCacheSize ?? 100,\n    disableWalAutoCheckpoint,\n  }\n\n  let blobstoreCfg: ServerConfig['blobstore']\n  if (env.blobstoreS3Bucket && env.blobstoreDiskLocation) {\n    throw new Error('Cannot set both S3 and disk blobstore env vars')\n  }\n  if (env.blobstoreS3Bucket) {\n    blobstoreCfg = {\n      provider: 's3',\n      bucket: env.blobstoreS3Bucket,\n      uploadTimeoutMs: env.blobstoreS3UploadTimeoutMs || 20000,\n      region: env.blobstoreS3Region,\n      endpoint: env.blobstoreS3Endpoint,\n      forcePathStyle: env.blobstoreS3ForcePathStyle,\n    }\n    if (env.blobstoreS3AccessKeyId || env.blobstoreS3SecretAccessKey) {\n      if (!env.blobstoreS3AccessKeyId || !env.blobstoreS3SecretAccessKey) {\n        throw new Error(\n          'Must specify both S3 access key id and secret access key blobstore env vars',\n        )\n      }\n      blobstoreCfg.credentials = {\n        accessKeyId: env.blobstoreS3AccessKeyId,\n        secretAccessKey: env.blobstoreS3SecretAccessKey,\n      }\n    }\n  } else if (env.blobstoreDiskLocation) {\n    blobstoreCfg = {\n      provider: 'disk',\n      location: env.blobstoreDiskLocation,\n      tempLocation: env.blobstoreDiskTmpLocation,\n    }\n  } else {\n    throw new Error('Must configure either S3 or disk blobstore')\n  }\n\n  let serviceHandleDomains: string[]\n  if (env.serviceHandleDomains && env.serviceHandleDomains.length > 0) {\n    serviceHandleDomains = env.serviceHandleDomains\n  } else {\n    if (hostname === 'localhost') {\n      serviceHandleDomains = ['.test']\n    } else {\n      serviceHandleDomains = [`.${hostname}`]\n    }\n  }\n  const invalidDomain = serviceHandleDomains.find(\n    (domain) => domain.length < 1 || !domain.startsWith('.'),\n  )\n  if (invalidDomain) {\n    throw new Error(`Invalid handle domain: ${invalidDomain}`)\n  }\n\n  const identityCfg: ServerConfig['identity'] = {\n    plcUrl: env.didPlcUrl ?? 'https://plc.directory',\n    cacheMaxTTL: env.didCacheMaxTTL ?? DAY,\n    cacheStaleTTL: env.didCacheStaleTTL ?? HOUR,\n    resolverTimeout: env.resolverTimeout ?? 3 * SECOND,\n    recoveryDidKey: env.recoveryDidKey ?? null,\n    serviceHandleDomains,\n    handleBackupNameservers: env.handleBackupNameservers,\n    enableDidDocWithSession: !!env.enableDidDocWithSession,\n  }\n\n  let entrywayCfg: ServerConfig['entryway'] = null\n  if (env.entrywayUrl) {\n    assert(\n      env.entrywayJwtVerifyKeyK256PublicKeyHex &&\n        env.entrywayPlcRotationKey &&\n        env.entrywayDid,\n      'if entryway url is configured, must include all required entryway configuration',\n    )\n    entrywayCfg = {\n      url: env.entrywayUrl,\n      did: env.entrywayDid,\n      jwtPublicKeyHex: env.entrywayJwtVerifyKeyK256PublicKeyHex,\n      plcRotationKey: env.entrywayPlcRotationKey,\n    }\n  }\n\n  // default to being required if left undefined\n  const invitesCfg: ServerConfig['invites'] =\n    env.inviteRequired === false\n      ? {\n          required: false,\n        }\n      : {\n          required: true,\n          interval: env.inviteInterval ?? null,\n          epoch: env.inviteEpoch ?? 0,\n        }\n\n  let emailCfg: ServerConfig['email']\n  if (!env.emailFromAddress && !env.emailSmtpUrl) {\n    emailCfg = null\n  } else {\n    if (!env.emailFromAddress || !env.emailSmtpUrl) {\n      throw new Error(\n        'Partial email config, must set both emailFromAddress and emailSmtpUrl',\n      )\n    }\n    emailCfg = {\n      smtpUrl: env.emailSmtpUrl,\n      fromAddress: env.emailFromAddress,\n    }\n  }\n\n  let moderationEmailCfg: ServerConfig['moderationEmail']\n  if (!env.moderationEmailAddress && !env.moderationEmailSmtpUrl) {\n    moderationEmailCfg = null\n  } else {\n    if (!env.moderationEmailAddress || !env.moderationEmailSmtpUrl) {\n      throw new Error(\n        'Partial moderation email config, must set both emailFromAddress and emailSmtpUrl',\n      )\n    }\n    moderationEmailCfg = {\n      smtpUrl: env.moderationEmailSmtpUrl,\n      fromAddress: env.moderationEmailAddress,\n    }\n  }\n\n  const subscriptionCfg: ServerConfig['subscription'] = {\n    maxBuffer: env.maxSubscriptionBuffer ?? 500,\n    repoBackfillLimitMs: env.repoBackfillLimitMs ?? DAY,\n  }\n\n  let bskyAppViewCfg: ServerConfig['bskyAppView'] = null\n  if (env.bskyAppViewUrl) {\n    assert(\n      env.bskyAppViewDid,\n      'if bsky appview service url is configured, must configure its did as well.',\n    )\n    bskyAppViewCfg = {\n      url: env.bskyAppViewUrl,\n      did: env.bskyAppViewDid,\n      cdnUrlPattern: env.bskyAppViewCdnUrlPattern,\n    }\n  }\n\n  let modServiceCfg: ServerConfig['modService'] = null\n  if (env.modServiceUrl) {\n    assert(\n      env.modServiceDid,\n      'if mod service url is configured, must configure its did as well.',\n    )\n    modServiceCfg = {\n      url: env.modServiceUrl,\n      did: env.modServiceDid,\n    }\n  }\n\n  let reportServiceCfg: ServerConfig['reportService'] = null\n  if (env.reportServiceUrl) {\n    assert(\n      env.reportServiceDid,\n      'if report service url is configured, must configure its did as well.',\n    )\n    reportServiceCfg = {\n      url: env.reportServiceUrl,\n      did: env.reportServiceDid,\n    }\n  }\n\n  // if there's a mod service, default report service into it\n  if (modServiceCfg && !reportServiceCfg) {\n    reportServiceCfg = modServiceCfg\n  }\n\n  const redisCfg: ServerConfig['redis'] = env.redisScratchAddress\n    ? {\n        address: env.redisScratchAddress,\n        password: env.redisScratchPassword,\n      }\n    : null\n\n  const rateLimitsCfg: ServerConfig['rateLimits'] = env.rateLimitsEnabled\n    ? {\n        enabled: true,\n        bypassKey: env.rateLimitBypassKey,\n        bypassIps: env.rateLimitBypassIps?.map((ipOrCidr) =>\n          ipOrCidr.split('/')[0]?.trim(),\n        ),\n      }\n    : { enabled: false }\n\n  const crawlersCfg: ServerConfig['crawlers'] = env.crawlers ?? []\n\n  const fetchCfg: ServerConfig['fetch'] = {\n    disableSsrfProtection: env.disableSsrfProtection ?? env.devMode ?? false,\n    maxResponseSize: env.fetchMaxResponseSize ?? 512 * 1024, // 512kb\n  }\n\n  const proxyCfg: ServerConfig['proxy'] = {\n    disableSsrfProtection: env.disableSsrfProtection ?? env.devMode ?? false,\n    allowHTTP2: env.proxyAllowHTTP2 ?? false,\n    headersTimeout: env.proxyHeadersTimeout ?? 10e3,\n    bodyTimeout: env.proxyBodyTimeout ?? 30e3,\n    maxResponseSize: env.proxyMaxResponseSize ?? 10 * 1024 * 1024, // 10mb\n    maxRetries:\n      env.proxyMaxRetries != null && env.proxyMaxRetries > 0\n        ? env.proxyMaxRetries\n        : 0,\n    preferCompressed: env.proxyPreferCompressed ?? false,\n  }\n\n  const oauthCfg: ServerConfig['oauth'] = entrywayCfg\n    ? {\n        issuer: entrywayCfg.url,\n        provider: undefined,\n      }\n    : {\n        issuer: serviceCfg.publicUrl,\n        provider: {\n          hcaptcha:\n            env.hcaptchaSiteKey &&\n            env.hcaptchaSecretKey &&\n            env.hcaptchaTokenSalt\n              ? {\n                  siteKey: env.hcaptchaSiteKey,\n                  secretKey: env.hcaptchaSecretKey,\n                  tokenSalt: env.hcaptchaTokenSalt,\n                }\n              : undefined,\n          branding: {\n            name: env.serviceName ?? `${hostname} PDS`,\n            logo: env.logoUrl,\n            colors: {\n              light: env.lightColor,\n              dark: env.darkColor,\n              primary: env.primaryColor,\n              primaryContrast: env.primaryColorContrast,\n              primaryHue: env.primaryColorHue,\n              error: env.errorColor,\n              errorContrast: env.errorColorContrast,\n              errorHue: env.errorColorHue,\n              success: env.successColor,\n              successContrast: env.successColorContrast,\n              successHue: env.successColorHue,\n              warning: env.warningColor,\n              warningContrast: env.warningColorContrast,\n              warningHue: env.warningColorHue,\n            },\n            links: [\n              {\n                title: { en: 'Home', fr: 'Accueil' },\n                href: env.homeUrl,\n                rel: 'canonical' as const, // Prevents login page from being indexed\n              },\n              {\n                title: { en: 'Terms of Service' },\n                href: env.termsOfServiceUrl,\n                rel: 'terms-of-service' as const,\n              },\n              {\n                title: { en: 'Privacy Policy' },\n                href: env.privacyPolicyUrl,\n                rel: 'privacy-policy' as const,\n              },\n              {\n                title: { en: 'Support' },\n                href: env.supportUrl,\n                rel: 'help' as const,\n              },\n            ].filter(\n              <T extends { href?: string }>(f: T): f is T & { href: string } =>\n                f.href != null && f.href !== '',\n            ),\n          },\n          trustedClients: env.trustedOAuthClients,\n        },\n      }\n\n  const lexiconCfg: LexiconResolverConfig = {}\n\n  if (env.lexiconDidAuthority != null) {\n    ensureValidDid(env.lexiconDidAuthority)\n    lexiconCfg.didAuthority = env.lexiconDidAuthority\n  }\n\n  return {\n    service: serviceCfg,\n    db: dbCfg,\n    actorStore: actorStoreCfg,\n    blobstore: blobstoreCfg,\n    identity: identityCfg,\n    entryway: entrywayCfg,\n    invites: invitesCfg,\n    email: emailCfg,\n    moderationEmail: moderationEmailCfg,\n    subscription: subscriptionCfg,\n    bskyAppView: bskyAppViewCfg,\n    modService: modServiceCfg,\n    reportService: reportServiceCfg,\n    redis: redisCfg,\n    rateLimits: rateLimitsCfg,\n    crawlers: crawlersCfg,\n    fetch: fetchCfg,\n    lexicon: lexiconCfg,\n    proxy: proxyCfg,\n    oauth: oauthCfg,\n  }\n}\n\nexport type ServerConfig = {\n  service: ServiceConfig\n  db: DatabaseConfig\n  actorStore: ActorStoreConfig\n  blobstore: S3BlobstoreConfig | DiskBlobstoreConfig\n  identity: IdentityConfig\n  entryway: EntrywayConfig | null\n  invites: InvitesConfig\n  email: EmailConfig | null\n  moderationEmail: EmailConfig | null\n  subscription: SubscriptionConfig\n  bskyAppView: BksyAppViewConfig | null\n  modService: ModServiceConfig | null\n  reportService: ReportServiceConfig | null\n  redis: RedisScratchConfig | null\n  rateLimits: RateLimitsConfig\n  crawlers: string[]\n  fetch: FetchConfig\n  proxy: ProxyConfig\n  oauth: OAuthConfig\n  lexicon: LexiconResolverConfig\n}\n\nexport type ServiceConfig = {\n  port: number\n  hostname: string\n  publicUrl: string\n  did: string\n  version?: string\n  privacyPolicyUrl?: string\n  termsOfServiceUrl?: string\n  acceptingImports: boolean\n  maxImportSize?: number\n  blobUploadLimit: number\n  contactEmailAddress?: string\n  devMode: boolean\n}\n\nexport type DatabaseConfig = {\n  accountDbLoc: string\n  sequencerDbLoc: string\n  didCacheDbLoc: string\n  disableWalAutoCheckpoint: boolean\n}\n\nexport type ActorStoreConfig = {\n  directory: string\n  cacheSize: number\n  disableWalAutoCheckpoint: boolean\n}\n\nexport type S3BlobstoreConfig = {\n  provider: 's3'\n  bucket: string\n  region?: string\n  endpoint?: string\n  forcePathStyle?: boolean\n  uploadTimeoutMs?: number\n  credentials?: {\n    accessKeyId: string\n    secretAccessKey: string\n  }\n}\n\nexport type DiskBlobstoreConfig = {\n  provider: 'disk'\n  location: string\n  tempLocation?: string\n}\n\nexport type IdentityConfig = {\n  plcUrl: string\n  resolverTimeout: number\n  cacheStaleTTL: number\n  cacheMaxTTL: number\n  recoveryDidKey: string | null\n  serviceHandleDomains: string[]\n  handleBackupNameservers?: string[]\n  enableDidDocWithSession: boolean\n}\n\nexport type EntrywayConfig = {\n  url: string\n  did: string\n  jwtPublicKeyHex: string\n  plcRotationKey: string\n}\n\nexport type FetchConfig = {\n  disableSsrfProtection: boolean\n  maxResponseSize: number\n}\n\nexport type ProxyConfig = {\n  disableSsrfProtection: boolean\n  allowHTTP2: boolean\n  headersTimeout: number\n  bodyTimeout: number\n  maxResponseSize: number\n  maxRetries: number\n\n  /**\n   * When proxying requests that might get intercepted (for read-after-write) we\n   * negotiate the encoding based on the client's preferences. We will however\n   * use or own weights in order to be able to better control if the PDS will\n   * need to perform content decoding. This settings allows to prefer compressed\n   * content over uncompressed one.\n   */\n  preferCompressed: boolean\n}\n\nexport type OAuthConfig = {\n  issuer: string\n  provider?: {\n    hcaptcha?: HcaptchaConfig\n    branding: BrandingInput\n    trustedClients?: string[]\n  }\n}\n\nexport type LexiconResolverConfig = {\n  didAuthority?: `did:${string}:${string}`\n}\n\nexport type InvitesConfig =\n  | {\n      required: true\n      interval: number | null\n      epoch: number\n    }\n  | {\n      required: false\n    }\n\nexport type EmailConfig = {\n  smtpUrl: string\n  fromAddress: string\n}\n\nexport type SubscriptionConfig = {\n  maxBuffer: number\n  repoBackfillLimitMs: number\n}\n\nexport type RedisScratchConfig = {\n  address: string\n  password?: string\n}\n\nexport type RateLimitsConfig =\n  | {\n      enabled: true\n      bypassKey?: string\n      bypassIps?: string[]\n    }\n  | { enabled: false }\n\nexport type BksyAppViewConfig = {\n  url: string\n  did: string\n  cdnUrlPattern?: string\n}\n\nexport type ModServiceConfig = {\n  url: string\n  did: string\n}\n\nexport type ReportServiceConfig = {\n  url: string\n  did: string\n}\n"
  },
  {
    "path": "packages/pds/src/config/env.ts",
    "content": "import { envBool, envInt, envList, envStr } from '@atproto/common'\n\nexport function readEnv() {\n  return {\n    // service\n    port: envInt('PDS_PORT'),\n    hostname: envStr('PDS_HOSTNAME'),\n    serviceDid: envStr('PDS_SERVICE_DID'),\n    serviceName: envStr('PDS_SERVICE_NAME'),\n    version: envStr('PDS_VERSION'),\n    homeUrl: envStr('PDS_HOME_URL'),\n    logoUrl: envStr('PDS_LOGO_URL'),\n    privacyPolicyUrl: envStr('PDS_PRIVACY_POLICY_URL'),\n    supportUrl: envStr('PDS_SUPPORT_URL'),\n    termsOfServiceUrl: envStr('PDS_TERMS_OF_SERVICE_URL'),\n    contactEmailAddress: envStr('PDS_CONTACT_EMAIL_ADDRESS'),\n    acceptingImports: envBool('PDS_ACCEPTING_REPO_IMPORTS'),\n    maxImportSize: envInt('PDS_MAX_REPO_IMPORT_SIZE'),\n    blobUploadLimit: envInt('PDS_BLOB_UPLOAD_LIMIT'),\n    devMode: envBool('PDS_DEV_MODE'),\n\n    // hCaptcha\n    hcaptchaSiteKey: envStr('PDS_HCAPTCHA_SITE_KEY'),\n    hcaptchaSecretKey: envStr('PDS_HCAPTCHA_SECRET_KEY'),\n    hcaptchaTokenSalt: envStr('PDS_HCAPTCHA_TOKEN_SALT'),\n\n    // OAuth\n    trustedOAuthClients: envList('PDS_OAUTH_TRUSTED_CLIENTS'),\n\n    // branding\n    lightColor: envStr('PDS_LIGHT_COLOR'),\n    darkColor: envStr('PDS_DARK_COLOR'),\n    primaryColor: envStr('PDS_PRIMARY_COLOR'),\n    primaryColorContrast: envStr('PDS_PRIMARY_COLOR_CONTRAST'),\n    primaryColorHue: envInt('PDS_PRIMARY_COLOR_HUE'),\n    errorColor: envStr('PDS_ERROR_COLOR'),\n    errorColorContrast: envStr('PDS_ERROR_COLOR_CONTRAST'),\n    errorColorHue: envInt('PDS_ERROR_COLOR_HUE'),\n    warningColor: envStr('PDS_WARNING_COLOR'),\n    warningColorContrast: envStr('PDS_WARNING_COLOR_CONTRAST'),\n    warningColorHue: envInt('PDS_WARNING_COLOR_HUE'),\n    successColor: envStr('PDS_SUCCESS_COLOR'),\n    successColorContrast: envStr('PDS_SUCCESS_COLOR_CONTRAST'),\n    successColorHue: envInt('PDS_SUCCESS_COLOR_HUE'),\n\n    // database\n    dataDirectory: envStr('PDS_DATA_DIRECTORY'),\n    disableWalAutoCheckpoint: envBool('PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT'),\n    accountDbLocation: envStr('PDS_ACCOUNT_DB_LOCATION'),\n    sequencerDbLocation: envStr('PDS_SEQUENCER_DB_LOCATION'),\n    didCacheDbLocation: envStr('PDS_DID_CACHE_DB_LOCATION'),\n\n    // actor store\n    actorStoreDirectory: envStr('PDS_ACTOR_STORE_DIRECTORY'),\n    actorStoreCacheSize: envInt('PDS_ACTOR_STORE_CACHE_SIZE'),\n\n    // blobstore: one required\n    // s3\n    blobstoreS3Bucket: envStr('PDS_BLOBSTORE_S3_BUCKET'),\n    blobstoreS3Region: envStr('PDS_BLOBSTORE_S3_REGION'),\n    blobstoreS3Endpoint: envStr('PDS_BLOBSTORE_S3_ENDPOINT'),\n    blobstoreS3ForcePathStyle: envBool('PDS_BLOBSTORE_S3_FORCE_PATH_STYLE'),\n    blobstoreS3AccessKeyId: envStr('PDS_BLOBSTORE_S3_ACCESS_KEY_ID'),\n    blobstoreS3SecretAccessKey: envStr('PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY'),\n    blobstoreS3UploadTimeoutMs: envInt('PDS_BLOBSTORE_S3_UPLOAD_TIMEOUT_MS'),\n    // disk\n    blobstoreDiskLocation: envStr('PDS_BLOBSTORE_DISK_LOCATION'),\n    blobstoreDiskTmpLocation: envStr('PDS_BLOBSTORE_DISK_TMP_LOCATION'),\n\n    // identity\n    didPlcUrl: envStr('PDS_DID_PLC_URL'),\n    didCacheStaleTTL: envInt('PDS_DID_CACHE_STALE_TTL'),\n    didCacheMaxTTL: envInt('PDS_DID_CACHE_MAX_TTL'),\n    resolverTimeout: envInt('PDS_ID_RESOLVER_TIMEOUT'),\n    recoveryDidKey: envStr('PDS_RECOVERY_DID_KEY'),\n    serviceHandleDomains: envList('PDS_SERVICE_HANDLE_DOMAINS'), // public hostname by default\n    handleBackupNameservers: envList('PDS_HANDLE_BACKUP_NAMESERVERS'),\n    enableDidDocWithSession: envBool('PDS_ENABLE_DID_DOC_WITH_SESSION'),\n\n    // entryway\n    entrywayUrl: envStr('PDS_ENTRYWAY_URL'),\n    entrywayDid: envStr('PDS_ENTRYWAY_DID'),\n    entrywayJwtVerifyKeyK256PublicKeyHex: envStr(\n      'PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX',\n    ),\n    entrywayPlcRotationKey: envStr('PDS_ENTRYWAY_PLC_ROTATION_KEY'),\n\n    // invites\n    inviteRequired: envBool('PDS_INVITE_REQUIRED'),\n    inviteInterval: envInt('PDS_INVITE_INTERVAL'),\n    inviteEpoch: envInt('PDS_INVITE_EPOCH'),\n\n    // email\n    emailSmtpUrl: envStr('PDS_EMAIL_SMTP_URL'),\n    emailFromAddress: envStr('PDS_EMAIL_FROM_ADDRESS'),\n    moderationEmailSmtpUrl: envStr('PDS_MODERATION_EMAIL_SMTP_URL'),\n    moderationEmailAddress: envStr('PDS_MODERATION_EMAIL_ADDRESS'),\n\n    // subscription\n    maxSubscriptionBuffer: envInt('PDS_MAX_SUBSCRIPTION_BUFFER'),\n    repoBackfillLimitMs: envInt('PDS_REPO_BACKFILL_LIMIT_MS'),\n\n    // appview\n    bskyAppViewUrl: envStr('PDS_BSKY_APP_VIEW_URL'),\n    bskyAppViewDid: envStr('PDS_BSKY_APP_VIEW_DID'),\n    bskyAppViewCdnUrlPattern: envStr('PDS_BSKY_APP_VIEW_CDN_URL_PATTERN'),\n\n    // mod service\n    modServiceUrl: envStr('PDS_MOD_SERVICE_URL'),\n    modServiceDid: envStr('PDS_MOD_SERVICE_DID'),\n\n    // report service\n    reportServiceUrl: envStr('PDS_REPORT_SERVICE_URL'),\n    reportServiceDid: envStr('PDS_REPORT_SERVICE_DID'),\n\n    // rate limits\n    rateLimitsEnabled: envBool('PDS_RATE_LIMITS_ENABLED'),\n    rateLimitBypassKey: envStr('PDS_RATE_LIMIT_BYPASS_KEY'),\n    rateLimitBypassIps: envList('PDS_RATE_LIMIT_BYPASS_IPS'),\n\n    // redis\n    redisScratchAddress: envStr('PDS_REDIS_SCRATCH_ADDRESS'),\n    redisScratchPassword: envStr('PDS_REDIS_SCRATCH_PASSWORD'),\n\n    // crawlers\n    crawlers: envList('PDS_CRAWLERS'),\n\n    // secrets\n    dpopSecret: envStr('PDS_DPOP_SECRET'),\n    jwtSecret: envStr('PDS_JWT_SECRET'),\n    adminPassword: envStr('PDS_ADMIN_PASSWORD'),\n    entrywayAdminToken: envStr('PDS_ENTRYWAY_ADMIN_TOKEN'),\n\n    // kms\n    plcRotationKeyKmsKeyId: envStr('PDS_PLC_ROTATION_KEY_KMS_KEY_ID'),\n    // memory\n    plcRotationKeyK256PrivateKeyHex: envStr(\n      'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX',\n    ),\n\n    // user provided url http requests\n    disableSsrfProtection: envBool('PDS_DISABLE_SSRF_PROTECTION'),\n\n    // fetch\n    fetchMaxResponseSize: envInt('PDS_FETCH_MAX_RESPONSE_SIZE'),\n\n    // proxy\n    proxyAllowHTTP2: envBool('PDS_PROXY_ALLOW_HTTP2'),\n    proxyHeadersTimeout: envInt('PDS_PROXY_HEADERS_TIMEOUT'),\n    proxyBodyTimeout: envInt('PDS_PROXY_BODY_TIMEOUT'),\n    proxyMaxResponseSize: envInt('PDS_PROXY_MAX_RESPONSE_SIZE'),\n    proxyMaxRetries: envInt('PDS_PROXY_MAX_RETRIES'),\n    proxyPreferCompressed: envBool('PDS_PROXY_PREFER_COMPRESSED'),\n\n    // lexicon resolution\n    lexiconDidAuthority: envStr('PDS_LEXICON_AUTHORITY_DID'),\n  }\n}\n\nexport type ServerEnvironment = Partial<ReturnType<typeof readEnv>>\n"
  },
  {
    "path": "packages/pds/src/config/index.ts",
    "content": "export * from './config'\nexport * from './env'\nexport * from './secrets'\n"
  },
  {
    "path": "packages/pds/src/config/secrets.ts",
    "content": "import { ServerEnvironment } from './env'\n\nexport const envToSecrets = (env: ServerEnvironment): ServerSecrets => {\n  let plcRotationKey: ServerSecrets['plcRotationKey']\n  if (env.plcRotationKeyKmsKeyId && env.plcRotationKeyK256PrivateKeyHex) {\n    throw new Error('Cannot set both kms & memory keys for plc rotation key')\n  } else if (env.plcRotationKeyKmsKeyId) {\n    plcRotationKey = {\n      provider: 'kms',\n      keyId: env.plcRotationKeyKmsKeyId,\n    }\n  } else if (env.plcRotationKeyK256PrivateKeyHex) {\n    plcRotationKey = {\n      provider: 'memory',\n      privateKeyHex: env.plcRotationKeyK256PrivateKeyHex,\n    }\n  } else {\n    throw new Error('Must configure plc rotation key')\n  }\n\n  if (!env.jwtSecret) {\n    throw new Error('Must provide a JWT secret')\n  }\n\n  if (!env.adminPassword) {\n    throw new Error('Must provide an admin password')\n  }\n\n  return {\n    dpopSecret: env.dpopSecret,\n    jwtSecret: env.jwtSecret,\n    adminPassword: env.adminPassword,\n    plcRotationKey,\n    entrywayAdminToken: env.entrywayAdminToken ?? env.adminPassword,\n  }\n}\n\nexport type ServerSecrets = {\n  dpopSecret?: string\n  jwtSecret: string\n  adminPassword: string\n  plcRotationKey: SigningKeyKms | SigningKeyMemory\n  entrywayAdminToken?: string\n}\n\nexport type SigningKeyKms = {\n  provider: 'kms'\n  keyId: string\n}\n\nexport type SigningKeyMemory = {\n  provider: 'memory'\n  privateKeyHex: string\n}\n"
  },
  {
    "path": "packages/pds/src/context.ts",
    "content": "import assert from 'node:assert'\nimport * as plc from '@did-plc/lib'\nimport express from 'express'\nimport { Redis } from 'ioredis'\nimport * as nodemailer from 'nodemailer'\nimport * as ui8 from 'uint8arrays'\nimport * as undici from 'undici'\nimport { AtpAgent } from '@atproto/api'\nimport { KmsKeypair, S3BlobStore } from '@atproto/aws'\nimport * as crypto from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport {\n  AccessTokenMode,\n  JoseKey,\n  LexResolver,\n  OAuthProvider,\n  OAuthVerifier,\n} from '@atproto/oauth-provider'\nimport { BlobStore } from '@atproto/repo'\nimport {\n  createServiceAuthHeaders,\n  createServiceJwt,\n} from '@atproto/xrpc-server'\nimport {\n  Fetch,\n  isUnicastIp,\n  safeFetchWrap,\n  unicastLookup,\n} from '@atproto-labs/fetch-node'\nimport { AccountManager } from './account-manager/account-manager'\nimport { OAuthStore } from './account-manager/oauth-store'\nimport { ScopeReferenceGetter } from './account-manager/scope-reference-getter'\nimport { ActorStore } from './actor-store/actor-store'\nimport { authPassthru, forwardedFor } from './api/proxy'\nimport {\n  AuthVerifier,\n  createPublicKeyObject,\n  createSecretKeyObject,\n} from './auth-verifier'\nimport { BackgroundQueue } from './background'\nimport { BskyAppView } from './bsky-app-view'\nimport { ServerConfig, ServerSecrets } from './config'\nimport { Crawlers } from './crawlers'\nimport { DidSqliteCache } from './did-cache'\nimport { DiskBlobStore } from './disk-blobstore'\nimport { ImageUrlBuilder } from './image/image-url-builder'\nimport { fetchLogger, lexiconResolverLogger, oauthLogger } from './logger'\nimport { ServerMailer } from './mailer'\nimport { ModerationMailer } from './mailer/moderation'\nimport { LocalViewer, LocalViewerCreator } from './read-after-write/viewer'\nimport { getRedisClient } from './redis'\nimport { Sequencer } from './sequencer'\n\nexport type AppContextOptions = {\n  actorStore: ActorStore\n  blobstore: (did: string) => BlobStore\n  localViewer: LocalViewerCreator\n  mailer: ServerMailer\n  moderationMailer: ModerationMailer\n  didCache: DidSqliteCache\n  idResolver: IdResolver\n  plcClient: plc.Client\n  accountManager: AccountManager\n  sequencer: Sequencer\n  backgroundQueue: BackgroundQueue\n  redisScratch?: Redis\n  crawlers: Crawlers\n  bskyAppView?: BskyAppView\n  moderationAgent?: AtpAgent\n  reportingAgent?: AtpAgent\n  entrywayAgent?: AtpAgent\n  entrywayAdminAgent?: AtpAgent\n  proxyAgent: undici.Dispatcher\n  safeFetch: Fetch\n  oauthProvider?: OAuthProvider\n  authVerifier: AuthVerifier\n  plcRotationKey: crypto.Keypair\n  cfg: ServerConfig\n}\n\nexport class AppContext {\n  public actorStore: ActorStore\n  public blobstore: (did: string) => BlobStore\n  public localViewer: LocalViewerCreator\n  public mailer: ServerMailer\n  public moderationMailer: ModerationMailer\n  public didCache: DidSqliteCache\n  public idResolver: IdResolver\n  public plcClient: plc.Client\n  public accountManager: AccountManager\n  public sequencer: Sequencer\n  public backgroundQueue: BackgroundQueue\n  public redisScratch?: Redis\n  public crawlers: Crawlers\n  public bskyAppView?: BskyAppView\n  public moderationAgent: AtpAgent | undefined\n  public reportingAgent: AtpAgent | undefined\n  public entrywayAgent: AtpAgent | undefined\n  public entrywayAdminAgent: AtpAgent | undefined\n  public proxyAgent: undici.Dispatcher\n  public safeFetch: Fetch\n  public authVerifier: AuthVerifier\n  public oauthProvider?: OAuthProvider\n  public plcRotationKey: crypto.Keypair\n  public cfg: ServerConfig\n\n  constructor(opts: AppContextOptions) {\n    this.actorStore = opts.actorStore\n    this.blobstore = opts.blobstore\n    this.localViewer = opts.localViewer\n    this.mailer = opts.mailer\n    this.moderationMailer = opts.moderationMailer\n    this.didCache = opts.didCache\n    this.idResolver = opts.idResolver\n    this.plcClient = opts.plcClient\n    this.accountManager = opts.accountManager\n    this.sequencer = opts.sequencer\n    this.backgroundQueue = opts.backgroundQueue\n    this.redisScratch = opts.redisScratch\n    this.crawlers = opts.crawlers\n    this.bskyAppView = opts.bskyAppView\n    this.moderationAgent = opts.moderationAgent\n    this.reportingAgent = opts.reportingAgent\n    this.entrywayAgent = opts.entrywayAgent\n    this.entrywayAdminAgent = opts.entrywayAdminAgent\n    this.proxyAgent = opts.proxyAgent\n    this.safeFetch = opts.safeFetch\n    this.authVerifier = opts.authVerifier\n    this.oauthProvider = opts.oauthProvider\n    this.plcRotationKey = opts.plcRotationKey\n    this.cfg = opts.cfg\n  }\n\n  static async fromConfig(\n    cfg: ServerConfig,\n    secrets: ServerSecrets,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<AppContext> {\n    const blobstore =\n      cfg.blobstore.provider === 's3'\n        ? S3BlobStore.creator({\n            bucket: cfg.blobstore.bucket,\n            region: cfg.blobstore.region,\n            endpoint: cfg.blobstore.endpoint,\n            forcePathStyle: cfg.blobstore.forcePathStyle,\n            credentials: cfg.blobstore.credentials,\n            uploadTimeoutMs: cfg.blobstore.uploadTimeoutMs,\n          })\n        : DiskBlobStore.creator(\n            cfg.blobstore.location,\n            cfg.blobstore.tempLocation,\n          )\n\n    const mailTransport =\n      cfg.email !== null\n        ? nodemailer.createTransport(cfg.email.smtpUrl)\n        : nodemailer.createTransport({ jsonTransport: true })\n\n    const mailer = new ServerMailer(mailTransport, cfg)\n\n    const modMailTransport =\n      cfg.moderationEmail !== null\n        ? nodemailer.createTransport(cfg.moderationEmail.smtpUrl)\n        : nodemailer.createTransport({ jsonTransport: true })\n\n    const moderationMailer = new ModerationMailer(modMailTransport, cfg)\n\n    const didCache = new DidSqliteCache(\n      cfg.db.didCacheDbLoc,\n      cfg.identity.cacheStaleTTL,\n      cfg.identity.cacheMaxTTL,\n      cfg.db.disableWalAutoCheckpoint,\n    )\n    await didCache.migrateOrThrow()\n\n    const idResolver = new IdResolver({\n      plcUrl: cfg.identity.plcUrl,\n      didCache,\n      timeout: cfg.identity.resolverTimeout,\n      backupNameservers: cfg.identity.handleBackupNameservers,\n    })\n    const plcClient = new plc.Client(cfg.identity.plcUrl)\n\n    const backgroundQueue = new BackgroundQueue()\n    const crawlers = new Crawlers(\n      cfg.service.hostname,\n      cfg.crawlers,\n      backgroundQueue,\n    )\n    const sequencer = new Sequencer(\n      cfg.db.sequencerDbLoc,\n      crawlers,\n      undefined,\n      cfg.db.disableWalAutoCheckpoint,\n    )\n    const redisScratch = cfg.redis\n      ? getRedisClient(cfg.redis.address, cfg.redis.password)\n      : undefined\n\n    const bskyAppView = cfg.bskyAppView\n      ? new BskyAppView(cfg.bskyAppView)\n      : undefined\n\n    const moderationAgent = cfg.modService\n      ? new AtpAgent({ service: cfg.modService.url })\n      : undefined\n    const reportingAgent = cfg.reportService\n      ? new AtpAgent({ service: cfg.reportService.url })\n      : undefined\n    const entrywayAgent = cfg.entryway\n      ? new AtpAgent({ service: cfg.entryway.url })\n      : undefined\n    let entrywayAdminAgent: AtpAgent | undefined\n    if (cfg.entryway && secrets.entrywayAdminToken) {\n      entrywayAdminAgent = new AtpAgent({ service: cfg.entryway.url })\n      entrywayAdminAgent.api.setHeader(\n        'authorization',\n        basicAuthHeader('admin', secrets.entrywayAdminToken),\n      )\n    }\n\n    const jwtSecretKey = createSecretKeyObject(secrets.jwtSecret)\n    const jwtPublicKey = cfg.entryway\n      ? createPublicKeyObject(cfg.entryway.jwtPublicKeyHex)\n      : null\n\n    const imageUrlBuilder = new ImageUrlBuilder(\n      cfg.service.hostname,\n      bskyAppView,\n    )\n\n    const actorStore = new ActorStore(cfg.actorStore, {\n      blobstore,\n      backgroundQueue,\n    })\n\n    const accountManager = new AccountManager(\n      idResolver,\n      jwtSecretKey,\n      cfg.service.did,\n      cfg.identity.serviceHandleDomains,\n      cfg.db,\n    )\n    await accountManager.migrateOrThrow()\n\n    const plcRotationKey =\n      secrets.plcRotationKey.provider === 'kms'\n        ? await KmsKeypair.load({\n            keyId: secrets.plcRotationKey.keyId,\n          })\n        : await crypto.Secp256k1Keypair.import(\n            secrets.plcRotationKey.privateKeyHex,\n          )\n\n    const localViewer = LocalViewer.creator(\n      accountManager,\n      imageUrlBuilder,\n      bskyAppView,\n    )\n\n    // An agent for performing HTTP requests based on user provided URLs.\n    const proxyAgentBase = new undici.Agent({\n      allowH2: cfg.proxy.allowHTTP2, // This is experimental\n      headersTimeout: cfg.proxy.headersTimeout,\n      maxResponseSize: cfg.proxy.maxResponseSize,\n      bodyTimeout: cfg.proxy.bodyTimeout,\n      factory: cfg.proxy.disableSsrfProtection\n        ? undefined\n        : (origin, opts) => {\n            const { protocol, hostname } =\n              origin instanceof URL ? origin : new URL(origin)\n            if (protocol !== 'https:') {\n              throw new Error(`Forbidden protocol \"${protocol}\"`)\n            }\n            if (isUnicastIp(hostname) === false) {\n              throw new Error('Hostname resolved to non-unicast address')\n            }\n            return new undici.Pool(origin, opts)\n          },\n      connect: {\n        lookup: cfg.proxy.disableSsrfProtection ? undefined : unicastLookup,\n      },\n    })\n    const proxyAgent =\n      cfg.proxy.maxRetries > 0\n        ? new undici.RetryAgent(proxyAgentBase, {\n            statusCodes: [], // Only retry on socket errors\n            methods: ['GET', 'HEAD'],\n            maxRetries: cfg.proxy.maxRetries,\n          })\n        : proxyAgentBase\n\n    /**\n     * A fetch() function that protects against SSRF attacks, large responses &\n     * known bad domains. This function can safely be used to fetch user\n     * provided URLs (unless \"disableSsrfProtection\" is true, of course).\n     *\n     * @note **DO NOT** wrap `safeFetch` with any logging or other transforms as\n     * this might prevent the use of explicit `redirect: \"follow\"` init from\n     * working. See {@link safeFetchWrap}.\n     */\n    const safeFetch = safeFetchWrap({\n      allowIpHost: false,\n      allowImplicitRedirect: false,\n      responseMaxSize: cfg.fetch.maxResponseSize,\n      ssrfProtection: !cfg.fetch.disableSsrfProtection,\n\n      // @NOTE Since we are using NodeJS <= 20, unicastFetchWrap would normally\n      // *not* be using a keep-alive agent if it we are providing a fetch\n      // function that is different from `globalThis.fetch`. However, since the\n      // fetch function below is indeed calling `globalThis.fetch` without\n      // altering any argument, we can safely force the use of the keep-alive\n      // agent. This would not be the case if we used \"loggedFetch\" as that\n      // function does wrap the input & init arguments into a Request object,\n      // which, on NodeJS<=20, results in init.dispatcher *not* being used.\n      dangerouslyForceKeepAliveAgent: true,\n      fetch: function (input, init) {\n        const method =\n          init?.method ?? (input instanceof Request ? input.method : 'GET')\n        const uri = input instanceof Request ? input.url : String(input)\n\n        fetchLogger.info({ method, uri }, 'fetch')\n\n        return globalThis.fetch.call(this, input, init)\n      },\n    })\n\n    const oauthProvider = cfg.oauth.provider\n      ? new OAuthProvider({\n          issuer: cfg.oauth.issuer,\n          keyset: [await JoseKey.fromKeyLike(jwtSecretKey, undefined, 'HS256')],\n          store: new OAuthStore(\n            accountManager,\n            actorStore,\n            imageUrlBuilder,\n            backgroundQueue,\n            mailer,\n            sequencer,\n            plcClient,\n            plcRotationKey,\n            cfg.service.publicUrl,\n            cfg.identity.recoveryDidKey,\n          ),\n          redis: redisScratch,\n          dpopSecret: secrets.dpopSecret,\n          inviteCodeRequired: cfg.invites.required,\n          availableUserDomains: cfg.identity.serviceHandleDomains,\n          hcaptcha: cfg.oauth.provider.hcaptcha,\n          branding: cfg.oauth.provider.branding,\n          safeFetch,\n          lexResolver: new LexResolver({\n            fetch: safeFetch,\n            plcDirectoryUrl: cfg.identity.plcUrl,\n            hooks: {\n              onResolveAuthority: ({ nsid }) => {\n                lexiconResolverLogger.debug(\n                  { nsid: nsid.toString() },\n                  'Resolving lexicon DID authority',\n                )\n                // Override the lexicon did resolution to point to a custom PDS\n                return cfg.lexicon.didAuthority\n              },\n              onResolveAuthorityResult({ nsid, did }) {\n                lexiconResolverLogger.info(\n                  { nsid: nsid.toString(), did },\n                  'Resolved lexicon DID',\n                )\n              },\n              onResolveAuthorityError({ nsid, err }) {\n                lexiconResolverLogger.error(\n                  { nsid: nsid.toString(), err },\n                  'Lexicon DID resolution error',\n                )\n              },\n              onFetchResult({ uri, cid }) {\n                lexiconResolverLogger.info(\n                  { uri: uri.toString(), cid: cid.toString() },\n                  'Fetched lexicon',\n                )\n              },\n              onFetchError({ err, uri }) {\n                lexiconResolverLogger.error(\n                  { uri: uri.toString(), err },\n                  'Lexicon fetch error',\n                )\n              },\n            },\n          }),\n          metadata: {\n            protected_resources: [new URL(cfg.oauth.issuer).origin],\n          },\n          // If the PDS is both an authorization server & resource server (no\n          // entryway), we can afford to check the token validity on every\n          // request. This allows revoked tokens to be rejected immediately.\n          // This also allows JWT to be shorter since some claims (notably the\n          // \"scope\" claim) do not need to be included in the token.\n          accessTokenMode: AccessTokenMode.stateful,\n\n          getClientInfo(clientId) {\n            return {\n              isTrusted: cfg.oauth.provider?.trustedClients?.includes(clientId),\n            }\n          },\n        })\n      : undefined\n\n    const scopeRefGetter = entrywayAgent\n      ? new ScopeReferenceGetter(entrywayAgent, redisScratch)\n      : undefined\n\n    const oauthVerifier: OAuthVerifier =\n      oauthProvider ?? // OAuthProvider extends OAuthVerifier\n      new OAuthVerifier({\n        issuer: cfg.oauth.issuer,\n        keyset: [await JoseKey.fromKeyLike(jwtPublicKey!, undefined, 'ES256K')],\n        dpopSecret: secrets.dpopSecret,\n        redis: redisScratch,\n        onDecodeToken: async ({ payload, dpopProof }) => {\n          // @TODO drop this once oauth provider no longer accepts DPoP proof with\n          // query or fragment in \"htu\" claim.\n          if (dpopProof?.htu.match(/[?#]/)) {\n            oauthLogger.info(\n              { htu: dpopProof.htu, client_id: payload.client_id },\n              'DPoP proof \"htu\" contains query or fragment',\n            )\n          }\n\n          if (scopeRefGetter) {\n            payload.scope = await scopeRefGetter.dereference(payload.scope)\n          }\n\n          return payload\n        },\n      })\n\n    const authVerifier = new AuthVerifier(\n      accountManager,\n      idResolver,\n      oauthVerifier,\n      {\n        publicUrl: cfg.service.publicUrl,\n        jwtKey: jwtPublicKey ?? jwtSecretKey,\n        adminPass: secrets.adminPassword,\n        dids: {\n          pds: cfg.service.did,\n          entryway: cfg.entryway?.did,\n          modService: cfg.modService?.did,\n        },\n      },\n    )\n\n    return new AppContext({\n      actorStore,\n      blobstore,\n      localViewer,\n      mailer,\n      moderationMailer,\n      didCache,\n      idResolver,\n      plcClient,\n      accountManager,\n      sequencer,\n      backgroundQueue,\n      redisScratch,\n      crawlers,\n      bskyAppView,\n      moderationAgent,\n      reportingAgent,\n      entrywayAgent,\n      entrywayAdminAgent,\n      proxyAgent,\n      safeFetch,\n      authVerifier,\n      oauthProvider,\n      plcRotationKey,\n      cfg,\n      ...(overrides ?? {}),\n    })\n  }\n\n  async appviewAuthHeaders(did: string, lxm: string) {\n    assert(this.bskyAppView)\n    return this.serviceAuthHeaders(did, this.bskyAppView.did, lxm)\n  }\n\n  async entrywayAuthHeaders(req: express.Request, did: string, lxm: string) {\n    assert(this.cfg.entryway)\n    const headers = await this.serviceAuthHeaders(\n      did,\n      this.cfg.entryway.did,\n      lxm,\n    )\n    return forwardedFor(req, headers)\n  }\n\n  entrywayPassthruHeaders(req: express.Request) {\n    return forwardedFor(req, authPassthru(req))\n  }\n\n  async serviceAuthHeaders(did: string, aud: string, lxm: string) {\n    const keypair = await this.actorStore.keypair(did)\n    return createServiceAuthHeaders({\n      iss: did,\n      aud,\n      lxm,\n      keypair,\n    })\n  }\n\n  async serviceAuthJwt(did: string, aud: string, lxm: string) {\n    const keypair = await this.actorStore.keypair(did)\n    return createServiceJwt({\n      iss: did,\n      aud,\n      lxm,\n      keypair,\n    })\n  }\n}\n\nconst basicAuthHeader = (username: string, password: string) => {\n  const encoded = ui8.toString(\n    ui8.fromString(`${username}:${password}`, 'utf8'),\n    'base64pad',\n  )\n  return `Basic ${encoded}`\n}\n\nexport default AppContext\n"
  },
  {
    "path": "packages/pds/src/crawlers.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { MINUTE } from '@atproto/common'\nimport { BackgroundQueue } from './background'\nimport { crawlerLogger as log } from './logger'\n\nconst NOTIFY_THRESHOLD = 20 * MINUTE\n\nexport class Crawlers {\n  public agents: AtpAgent[]\n  public lastNotified = 0\n\n  constructor(\n    public hostname: string,\n    public crawlers: string[],\n    public backgroundQueue: BackgroundQueue,\n  ) {\n    this.agents = crawlers.map((service) => new AtpAgent({ service }))\n  }\n\n  async notifyOfUpdate() {\n    const now = Date.now()\n    if (now - this.lastNotified < NOTIFY_THRESHOLD) {\n      return\n    }\n\n    this.backgroundQueue.add(async () => {\n      await Promise.all(\n        this.agents.map(async (agent) => {\n          try {\n            await agent.api.com.atproto.sync.requestCrawl({\n              hostname: this.hostname,\n            })\n          } catch (err) {\n            log.warn(\n              { err, cralwer: agent.service.toString() },\n              'failed to request crawl',\n            )\n          }\n        }),\n      )\n      this.lastNotified = now\n    })\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/db/cast.ts",
    "content": "export type DateISO = `${string}T${string}Z`\nexport function toDateISO(date: Date) {\n  return date.toISOString() as DateISO\n}\nexport function fromDateISO(dateStr: DateISO) {\n  return new Date(dateStr)\n}\n\n/**\n * Allows to ensure that {@link JsonEncoded} is not used with non-JSON\n * serializable values (e.g. {@link Date} or {@link Function}s).\n */\nexport type Encodable =\n  | string\n  | number\n  | boolean\n  | null\n  | readonly Encodable[]\n  | { readonly [_ in string]?: Encodable }\n\nexport type JsonString<T extends Encodable> = T extends readonly unknown[]\n  ? `[${string}]`\n  : T extends object\n    ? `{${string}}`\n    : T extends string\n      ? `\"${string}\"`\n      : T extends number\n        ? `${number}`\n        : T extends boolean\n          ? `true` | `false`\n          : T extends null\n            ? `null`\n            : never\n\ndeclare const jsonEncodedType: unique symbol\nexport type JsonEncoded<T extends Encodable = Encodable> = JsonString<T> & {\n  [jsonEncodedType]: T\n}\n\nexport function toJson<T extends Encodable>(value: T): JsonEncoded<T> {\n  const json = JSON.stringify(value)\n  if (json === undefined) throw new TypeError('Input not JSONifyable')\n  return json as JsonEncoded<T>\n}\n\nexport function fromJson<T extends Encodable>(jsonStr: JsonEncoded<T>): T {\n  try {\n    return JSON.parse(jsonStr) as T\n  } catch (cause) {\n    throw new TypeError('Database contains invalid JSON', { cause })\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/db/db.ts",
    "content": "import assert from 'node:assert'\nimport SqliteDB from 'better-sqlite3'\nimport {\n  Kysely,\n  KyselyPlugin,\n  PluginTransformQueryArgs,\n  PluginTransformResultArgs,\n  QueryResult,\n  RootOperationNode,\n  SqliteDialect,\n  UnknownRow,\n  sql,\n} from 'kysely'\nimport { dbLogger } from '../logger'\nimport { retrySqlite } from './util'\n\nconst DEFAULT_PRAGMAS = {\n  // strict: 'ON', // @TODO strictness should live on table defs instead\n}\n\nexport class Database<Schema> {\n  destroyed = false\n  commitHooks: CommitHook[] = []\n\n  constructor(public db: Kysely<Schema>) {}\n\n  static sqlite<T>(\n    location: string,\n    opts?: { pragmas?: Record<string, string> },\n  ): Database<T> {\n    const sqliteDb = new SqliteDB(location, {\n      timeout: 0, // handled by application\n    })\n    const pragmas = {\n      ...DEFAULT_PRAGMAS,\n      ...(opts?.pragmas ?? {}),\n    }\n    for (const pragma of Object.keys(pragmas)) {\n      sqliteDb.pragma(`${pragma} = ${pragmas[pragma]}`)\n    }\n    const db = new Kysely<T>({\n      dialect: new SqliteDialect({\n        database: sqliteDb,\n      }),\n    })\n    return new Database(db)\n  }\n\n  async ensureWal() {\n    await sql`PRAGMA journal_mode = WAL`.execute(this.db)\n  }\n\n  async transactionNoRetry<T>(\n    fn: (db: Database<Schema>) => T | PromiseLike<T>,\n  ): Promise<T> {\n    this.assertNotTransaction()\n    const leakyTxPlugin = new LeakyTxPlugin()\n    const { hooks, txRes } = await this.db\n      .withPlugin(leakyTxPlugin)\n      .transaction()\n      .execute(async (txn) => {\n        const dbTxn = new Database(txn)\n        try {\n          const txRes = await fn(dbTxn)\n          leakyTxPlugin.endTx()\n          const hooks = dbTxn.commitHooks\n          return { hooks, txRes }\n        } catch (err) {\n          leakyTxPlugin.endTx()\n          // ensure that all in-flight queries are flushed & the connection is open\n          await txn.getExecutor().provideConnection(async () => {})\n          throw err\n        }\n      })\n    hooks.map((hook) => hook())\n    return txRes\n  }\n\n  async transaction<T>(\n    fn: (db: Database<Schema>) => T | PromiseLike<T>,\n  ): Promise<T> {\n    return retrySqlite(() => this.transactionNoRetry(fn))\n  }\n\n  async executeWithRetry<T>(query: { execute: () => Promise<T> }) {\n    if (this.isTransaction) {\n      // transaction() ensures retry on entire transaction, no need to retry individual statements.\n      return query.execute()\n    }\n    return retrySqlite(() => query.execute())\n  }\n\n  onCommit(fn: () => void) {\n    this.assertTransaction()\n    this.commitHooks.push(fn)\n  }\n\n  get isTransaction() {\n    return this.db.isTransaction\n  }\n\n  assertTransaction() {\n    assert(this.isTransaction, 'Transaction required')\n  }\n\n  assertNotTransaction() {\n    assert(!this.isTransaction, 'Cannot be in a transaction')\n  }\n\n  close(): void {\n    if (this.destroyed) return\n    this.db\n      .destroy()\n      .then(() => (this.destroyed = true))\n      .catch((err) => dbLogger.error({ err }, 'error closing db'))\n  }\n}\n\ntype CommitHook = () => void\n\nclass LeakyTxPlugin implements KyselyPlugin {\n  private txOver = false\n\n  endTx() {\n    this.txOver = true\n  }\n\n  transformQuery(args: PluginTransformQueryArgs): RootOperationNode {\n    if (this.txOver) {\n      throw new Error('tx already failed')\n    }\n    return args.node\n  }\n\n  async transformResult(\n    args: PluginTransformResultArgs,\n  ): Promise<QueryResult<UnknownRow>> {\n    return args.result\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/db/index.ts",
    "content": "export * from './db'\nexport * from './cast'\nexport * from './migrator'\nexport * from './util'\n"
  },
  {
    "path": "packages/pds/src/db/migrator.ts",
    "content": "import { Kysely, Migration, Migrator as KyselyMigrator } from 'kysely'\n\nexport class Migrator<T> extends KyselyMigrator {\n  constructor(\n    public db: Kysely<T>,\n    migrations: Record<string, Migration>,\n  ) {\n    super({\n      db,\n      provider: {\n        async getMigrations() {\n          return migrations\n        },\n      },\n    })\n  }\n\n  async migrateToOrThrow(migration: string) {\n    const { error, results } = await this.migrateTo(migration)\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n\n  async migrateToLatestOrThrow() {\n    const { error, results } = await this.migrateToLatest()\n    if (error) {\n      throw error\n    }\n    if (!results) {\n      throw new Error('An unknown failure occurred while migrating')\n    }\n    return results\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/db/pagination.ts",
    "content": "import { sql } from 'kysely'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { AnyQb, DbRef } from './util'\n\nexport type Cursor = { primary: string; secondary: string }\nexport type LabeledResult = {\n  primary: string | number\n  secondary: string | number\n}\n\n/**\n * The GenericKeyset is an abstract class that sets-up the interface and partial implementation\n * of a keyset-paginated cursor with two parts. There are three types involved:\n *  - Result: a raw result (i.e. a row from the db) containing data that will make-up a cursor.\n *    - E.g. { createdAt: '2022-01-01T12:00:00Z', cid: 'bafyx' }\n *  - LabeledResult: a Result processed such that the \"primary\" and \"secondary\" parts of the cursor are labeled.\n *    - E.g. { primary: '2022-01-01T12:00:00Z', secondary: 'bafyx' }\n *  - Cursor: the two string parts that make-up the packed/string cursor.\n *    - E.g. packed cursor '1641038400000::bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' }\n *\n * These types relate as such. Implementers define the relations marked with a *:\n *   Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor\n *                     ↳ SQL Condition\n */\nexport abstract class GenericKeyset<R, LR extends LabeledResult> {\n  constructor(\n    public primary: DbRef,\n    public secondary: DbRef,\n  ) {}\n  abstract labelResult(result: R): LR\n  abstract labeledResultToCursor(labeled: LR): Cursor\n  abstract cursorToLabeledResult(cursor: Cursor): LR\n  packFromResult(results: R | R[]): string | undefined {\n    const result = Array.isArray(results) ? results.at(-1) : results\n    if (!result) return\n    return this.pack(this.labelResult(result))\n  }\n  pack(labeled?: LR): string | undefined {\n    if (!labeled) return\n    const cursor = this.labeledResultToCursor(labeled)\n    return this.packCursor(cursor)\n  }\n  unpack(cursorStr?: string): LR | undefined {\n    const cursor = this.unpackCursor(cursorStr)\n    if (!cursor) return\n    return this.cursorToLabeledResult(cursor)\n  }\n  packCursor(cursor?: Cursor): string | undefined {\n    if (!cursor) return\n    return `${cursor.primary}::${cursor.secondary}`\n  }\n  unpackCursor(cursorStr?: string): Cursor | undefined {\n    if (!cursorStr) return\n    const result = cursorStr.split('::')\n    const [primary, secondary, ...others] = result\n    if (!primary || !secondary || others.length > 0) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary,\n      secondary,\n    }\n  }\n  getSql(labeled?: LR, direction?: 'asc' | 'desc', tryIndex?: boolean) {\n    if (labeled === undefined) return\n    if (tryIndex) {\n      // The tryIndex param will likely disappear and become the default implementation: here for now for gradual rollout query-by-query.\n      if (direction === 'asc') {\n        return sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}))`\n      } else {\n        return sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}))`\n      }\n    } else {\n      // @NOTE this implementation can struggle to use an index on (primary, secondary) for pagination due to the \"or\" usage.\n      if (direction === 'asc') {\n        return sql`((${this.primary} > ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} > ${labeled.secondary}))`\n      } else {\n        return sql`((${this.primary} < ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} < ${labeled.secondary}))`\n      }\n    }\n  }\n}\n\ntype CreatedAtCidResult = { createdAt: string; cid: string }\ntype TimeCidLabeledResult = Cursor\n\nexport class TimeCidKeyset<\n  TimeCidResult = CreatedAtCidResult,\n> extends GenericKeyset<TimeCidResult, TimeCidLabeledResult> {\n  labelResult(result: TimeCidResult): TimeCidLabeledResult\n  labelResult<TimeCidResult extends CreatedAtCidResult>(result: TimeCidResult) {\n    return { primary: result.createdAt, secondary: result.cid }\n  }\n  labeledResultToCursor(labeled: TimeCidLabeledResult) {\n    return {\n      primary: new Date(labeled.primary).getTime().toString(),\n      secondary: labeled.secondary,\n    }\n  }\n  cursorToLabeledResult(cursor: Cursor) {\n    const primaryDate = new Date(parseInt(cursor.primary, 10))\n    if (isNaN(primaryDate.getTime())) {\n      throw new InvalidRequestError('Malformed cursor')\n    }\n    return {\n      primary: primaryDate.toISOString(),\n      secondary: cursor.secondary,\n    }\n  }\n}\n\nexport const paginate = <\n  QB extends AnyQb,\n  K extends GenericKeyset<unknown, any>,\n>(\n  qb: QB,\n  opts: {\n    limit?: number\n    cursor?: string\n    direction?: 'asc' | 'desc'\n    keyset: K\n    tryIndex?: boolean\n  },\n): QB => {\n  const { limit, cursor, keyset, direction = 'desc', tryIndex } = opts\n  const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex)\n  return qb\n    .if(!!limit, (q) => q.limit(limit as number))\n    .orderBy(keyset.primary, direction)\n    .orderBy(keyset.secondary, direction)\n    .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB\n}\n"
  },
  {
    "path": "packages/pds/src/db/tables/moderation.ts",
    "content": "import { Generated } from 'kysely'\nimport {\n  REASONMISLEADING,\n  REASONOTHER,\n  REASONRUDE,\n  REASONSEXUAL,\n  REASONSPAM,\n  REASONVIOLATION,\n} from '../../lexicon/types/com/atproto/moderation/defs'\n\nexport const actionTableName = 'moderation_action'\nexport const actionSubjectBlobTableName = 'moderation_action_subject_blob'\nexport const reportTableName = 'moderation_report'\nexport const reportResolutionTableName = 'moderation_report_resolution'\n\nexport interface ModerationAction {\n  id: Generated<number>\n  action:\n    | 'tools.ozone.moderation.defs#modEventTakedown'\n    | 'tools.ozone.moderation.defs#modEventAcknowledge'\n    | 'tools.ozone.moderation.defs#modEventEscalate'\n    | 'tools.ozone.moderation.defs#modEventComment'\n    | 'tools.ozone.moderation.defs#modEventLabel'\n    | 'tools.ozone.moderation.defs#modEventReport'\n    | 'tools.ozone.moderation.defs#modEventMute'\n    | 'tools.ozone.moderation.defs#modEventUnmute'\n    | 'tools.ozone.moderation.defs#modEventMuteReporter'\n    | 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n    | 'tools.ozone.moderation.defs#modEventReverseTakedown'\n    | 'tools.ozone.moderation.defs#modEventEmail'\n    | 'tools.ozone.moderation.defs#modEventResolveAppeal'\n    | 'tools.ozone.moderation.defs#modEventDivert'\n    | 'tools.ozone.moderation.defs#accountEvent'\n    | 'tools.ozone.moderation.defs#identityEvent'\n    | 'tools.ozone.moderation.defs#recordEvent'\n    | 'tools.ozone.moderation.defs#modEventPriorityScore'\n  subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef'\n  subjectDid: string\n  subjectUri: string | null\n  subjectCid: string | null\n  createLabelVals: string | null\n  negateLabelVals: string | null\n  comment: string | null\n  createdAt: string\n  createdBy: string\n  durationInHours: number | null\n  expiresAt: string | null\n  meta: Record<string, string | boolean> | null\n}\n\nexport interface ModerationActionSubjectBlob {\n  actionId: number\n  cid: string\n  recordUri: string\n}\n\nexport interface ModerationReport {\n  id: Generated<number>\n  subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef'\n  subjectDid: string\n  subjectUri: string | null\n  subjectCid: string | null\n  reasonType:\n    | typeof REASONSPAM\n    | typeof REASONOTHER\n    | typeof REASONMISLEADING\n    | typeof REASONRUDE\n    | typeof REASONSEXUAL\n    | typeof REASONVIOLATION\n  reason: string | null\n  reportedByDid: string\n  createdAt: string\n}\n\nexport interface ModerationReportResolution {\n  reportId: number\n  actionId: number\n  createdAt: string\n  createdBy: string\n}\n\nexport type PartialDB = {\n  [actionTableName]: ModerationAction\n  [actionSubjectBlobTableName]: ModerationActionSubjectBlob\n  [reportTableName]: ModerationReport\n  [reportResolutionTableName]: ModerationReportResolution\n}\n"
  },
  {
    "path": "packages/pds/src/db/util.ts",
    "content": "import {\n  DummyDriver,\n  DynamicModule,\n  Kysely,\n  RawBuilder,\n  ReferenceExpression,\n  SelectQueryBuilder,\n  SqliteAdapter,\n  SqliteIntrospector,\n  SqliteQueryCompiler,\n  sql,\n} from 'kysely'\nimport { retry } from '@atproto/common'\n\n// Applies to repo_root or record table\nexport const notSoftDeletedClause = (alias: DbRef) => {\n  return sql`${alias}.\"takedownRef\" is null`\n}\n\nexport const softDeleted = (repoOrRecord: { takedownRef: string | null }) => {\n  return repoOrRecord.takedownRef !== null\n}\n\nexport const countAll = sql<number>`count(*)`\nexport const countDistinct = (ref: DbRef) => sql<number>`count(distinct ${ref})`\n\n// For use with doUpdateSet()\nexport const excluded = <T, S>(db: Kysely<S>, col) => {\n  return sql<T>`${db.dynamic.ref(`excluded.${col}`)}`\n}\n\n// Can be useful for large where-in clauses, to get the db to use a hash lookup on the list\nexport const valuesList = (vals: unknown[]) => {\n  return sql`(values (${sql.join(vals, sql`), (`)}))`\n}\n\nexport const dummyDialect = {\n  createAdapter() {\n    return new SqliteAdapter()\n  },\n  createDriver() {\n    return new DummyDriver()\n  },\n  createIntrospector(db) {\n    return new SqliteIntrospector(db)\n  },\n  createQueryCompiler() {\n    return new SqliteQueryCompiler()\n  },\n}\n\nexport const retrySqlite = <T>(fn: () => Promise<T>): Promise<T> => {\n  return retry(fn, {\n    retryable: retryableSqlite,\n    getWaitMs: getWaitMsSqlite,\n    maxRetries: 60, // a safety measure: getWaitMsSqlite() times out before this after 5000ms of waiting.\n  })\n}\n\nconst retryableSqlite = (err: unknown) => {\n  return typeof err?.['code'] === 'string' && RETRY_ERRORS.has(err['code'])\n}\n\n// based on sqlite's backoff strategy https://github.com/sqlite/sqlite/blob/91c8e65dd4bf17d21fbf8f7073565fe1a71c8948/src/main.c#L1704-L1713\nconst getWaitMsSqlite = (n: number, timeout = 5000) => {\n  if (n < 0) return null\n  let delay: number\n  let prior: number\n  if (n < DELAYS.length) {\n    delay = DELAYS[n]\n    prior = TOTALS[n]\n  } else {\n    delay = last(DELAYS)\n    prior = last(TOTALS) + delay * (n - (DELAYS.length - 1))\n  }\n  if (prior + delay > timeout) {\n    delay = timeout - prior\n    if (delay <= 0) return null\n  }\n  return delay\n}\n\nconst last = <T>(arr: T[]) => arr[arr.length - 1]\nconst DELAYS = [1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100]\nconst TOTALS = [0, 1, 3, 8, 18, 33, 53, 78, 103, 128, 178, 228]\nconst RETRY_ERRORS = new Set([\n  'SQLITE_BUSY',\n  'SQLITE_BUSY_SNAPSHOT',\n  'SQLITE_BUSY_RECOVERY',\n  'SQLITE_BUSY_TIMEOUT',\n])\n\nexport type Ref = ReferenceExpression<any, any>\n\nexport type DbRef = RawBuilder | ReturnType<DynamicModule['ref']>\n\nexport type AnyQb = SelectQueryBuilder<any, any, any>\n\nexport const isErrUniqueViolation = (err: unknown) => {\n  const code = err?.['code']\n  return (\n    code === '23505' || // postgres, see https://www.postgresql.org/docs/current/errcodes-appendix.html\n    code === 'SQLITE_CONSTRAINT_UNIQUE' // sqlite, see https://www.sqlite.org/rescode.html#constraint_unique\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/did-cache/db/index.ts",
    "content": "import { Database, Migrator } from '../../db'\nimport migrations from './migrations'\nimport { DidCacheSchema } from './schema'\n\nexport * from './schema'\n\nexport type DidCacheDb = Database<DidCacheSchema>\n\nexport const getDb = (\n  location: string,\n  disableWalAutoCheckpoint = false,\n): DidCacheDb => {\n  const pragmas: Record<string, string> = disableWalAutoCheckpoint\n    ? { wal_autocheckpoint: '0', synchronous: 'NORMAL' }\n    : { synchronous: 'NORMAL' }\n  return Database.sqlite(location, { pragmas })\n}\n\nexport const getMigrator = (db: DidCacheDb) => {\n  return new Migrator(db.db, migrations)\n}\n"
  },
  {
    "path": "packages/pds/src/did-cache/db/migrations.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport default {\n  '001': {\n    up: async (db: Kysely<unknown>) => {\n      await db.schema\n        .createTable('did_doc')\n        .addColumn('did', 'varchar', (col) => col.primaryKey())\n        .addColumn('doc', 'text', (col) => col.notNull())\n        .addColumn('updatedAt', 'bigint', (col) => col.notNull())\n        .execute()\n    },\n    down: async (db: Kysely<unknown>) => {\n      await db.schema.dropTable('did_doc').execute()\n    },\n  },\n}\n"
  },
  {
    "path": "packages/pds/src/did-cache/db/schema.ts",
    "content": "export interface DidDoc {\n  did: string\n  doc: string // json representation of DidDocument\n  updatedAt: number\n}\n\nexport type DidCacheSchema = {\n  did_doc: DidDoc\n}\n"
  },
  {
    "path": "packages/pds/src/did-cache/index.ts",
    "content": "import PQueue from 'p-queue'\nimport { CacheResult, DidCache, DidDocument } from '@atproto/identity'\nimport { excluded } from '../db/util'\nimport { didCacheLogger } from '../logger'\nimport { DidCacheDb, getDb, getMigrator } from './db'\n\nexport class DidSqliteCache implements DidCache {\n  db: DidCacheDb\n  public pQueue: PQueue | null //null during teardown\n\n  constructor(\n    dbLocation: string,\n    public staleTTL: number,\n    public maxTTL: number,\n    disableWalAutoCheckpoint = false,\n  ) {\n    this.db = getDb(dbLocation, disableWalAutoCheckpoint)\n    this.pQueue = new PQueue()\n  }\n\n  async cacheDid(\n    did: string,\n    doc: DidDocument,\n    prevResult?: CacheResult,\n  ): Promise<void> {\n    try {\n      if (prevResult) {\n        await this.db.executeWithRetry(\n          this.db.db\n            .updateTable('did_doc')\n            .set({ doc: JSON.stringify(doc), updatedAt: Date.now() })\n            .where('did', '=', did)\n            .where('updatedAt', '=', prevResult.updatedAt),\n        )\n      } else {\n        await this.db.executeWithRetry(\n          this.db.db\n            .insertInto('did_doc')\n            .values({ did, doc: JSON.stringify(doc), updatedAt: Date.now() })\n            .onConflict((oc) =>\n              oc.column('did').doUpdateSet({\n                doc: excluded(this.db.db, 'doc'),\n                updatedAt: excluded(this.db.db, 'updatedAt'),\n              }),\n            ),\n        )\n      }\n    } catch (err) {\n      didCacheLogger.error({ did, doc, err }, 'failed to cache did')\n    }\n  }\n\n  async refreshCache(\n    did: string,\n    getDoc: () => Promise<DidDocument | null>,\n    prevResult?: CacheResult,\n  ): Promise<void> {\n    this.pQueue?.add(async () => {\n      try {\n        const doc = await getDoc()\n        if (doc) {\n          await this.cacheDid(did, doc, prevResult)\n        } else {\n          await this.clearEntry(did)\n        }\n      } catch (err) {\n        didCacheLogger.error({ did, err }, 'refreshing did cache failed')\n      }\n    })\n  }\n\n  async checkCache(did: string): Promise<CacheResult | null> {\n    try {\n      return await this.checkCacheInternal(did)\n    } catch (err) {\n      didCacheLogger.error({ did, err }, 'failed to check did cache')\n      return null\n    }\n  }\n\n  async checkCacheInternal(did: string): Promise<CacheResult | null> {\n    const res = await this.db.db\n      .selectFrom('did_doc')\n      .where('did', '=', did)\n      .selectAll()\n      .executeTakeFirst()\n    if (!res) return null\n    const now = Date.now()\n    const updatedAt = new Date(res.updatedAt).getTime()\n    const expired = now > updatedAt + this.maxTTL\n    const stale = now > updatedAt + this.staleTTL\n    return {\n      doc: JSON.parse(res.doc) as DidDocument,\n      updatedAt,\n      did,\n      stale,\n      expired,\n    }\n  }\n\n  async clearEntry(did: string): Promise<void> {\n    try {\n      await this.db.executeWithRetry(\n        this.db.db.deleteFrom('did_doc').where('did', '=', did),\n      )\n    } catch (err) {\n      didCacheLogger.error({ did, err }, 'clearing did cache entry failed')\n    }\n  }\n\n  async clear(): Promise<void> {\n    await this.db.db.deleteFrom('did_doc').execute()\n  }\n\n  async processAll() {\n    await this.pQueue?.onIdle()\n  }\n\n  async migrateOrThrow() {\n    await this.db.ensureWal()\n    await getMigrator(this.db).migrateToLatestOrThrow()\n  }\n\n  async destroy() {\n    const pQueue = this.pQueue\n    this.pQueue = null\n    pQueue?.pause()\n    pQueue?.clear()\n    await pQueue?.onIdle()\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/disk-blobstore.ts",
    "content": "import fsSync from 'node:fs'\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport stream from 'node:stream'\nimport { CID } from 'multiformats/cid'\nimport {\n  aggregateErrors,\n  chunkArray,\n  fileExists,\n  isErrnoException,\n  rmIfExists,\n} from '@atproto/common'\nimport { randomStr } from '@atproto/crypto'\nimport { BlobNotFoundError, BlobStore } from '@atproto/repo'\nimport { blobStoreLogger as log } from './logger'\n\nexport class DiskBlobStore implements BlobStore {\n  constructor(\n    public did: string,\n    public location: string,\n    public tmpLocation: string,\n    public quarantineLocation: string,\n  ) {}\n\n  static creator(\n    location: string,\n    tmpLocation?: string,\n    quarantineLocation?: string,\n  ) {\n    return (did: string) => {\n      const tmp = tmpLocation || path.join(location, 'tempt')\n      const quarantine = quarantineLocation || path.join(location, 'quarantine')\n      return new DiskBlobStore(did, location, tmp, quarantine)\n    }\n  }\n\n  private async ensureDir() {\n    await fs.mkdir(path.join(this.location, this.did), { recursive: true })\n  }\n\n  private async ensureTemp() {\n    await fs.mkdir(path.join(this.tmpLocation, this.did), { recursive: true })\n  }\n\n  private async ensureQuarantine() {\n    await fs.mkdir(path.join(this.quarantineLocation, this.did), {\n      recursive: true,\n    })\n  }\n\n  private genKey() {\n    return randomStr(32, 'base32')\n  }\n\n  getTmpPath(key: string): string {\n    return path.join(this.tmpLocation, this.did, key)\n  }\n\n  getStoredPath(cid: CID): string {\n    return path.join(this.location, this.did, cid.toString())\n  }\n\n  getQuarantinePath(cid: CID): string {\n    return path.join(this.quarantineLocation, this.did, cid.toString())\n  }\n\n  async hasTemp(key: string): Promise<boolean> {\n    return fileExists(this.getTmpPath(key))\n  }\n\n  async hasStored(cid: CID): Promise<boolean> {\n    return fileExists(this.getStoredPath(cid))\n  }\n\n  async putTemp(bytes: Uint8Array | stream.Readable): Promise<string> {\n    await this.ensureTemp()\n    const key = this.genKey()\n    await fs.writeFile(this.getTmpPath(key), bytes)\n    return key\n  }\n\n  async makePermanent(key: string, cid: CID): Promise<void> {\n    await this.ensureDir()\n    const tmpPath = this.getTmpPath(key)\n    const storedPath = this.getStoredPath(cid)\n    const alreadyHas = await this.hasStored(cid)\n    if (!alreadyHas) {\n      const data = await fs.readFile(tmpPath)\n      await fs.writeFile(storedPath, data)\n    }\n    try {\n      await fs.rm(tmpPath)\n    } catch (err) {\n      log.error({ err, tmpPath }, 'could not delete file from temp storage')\n    }\n  }\n\n  async putPermanent(\n    cid: CID,\n    bytes: Uint8Array | stream.Readable,\n  ): Promise<void> {\n    await this.ensureDir()\n    await fs.writeFile(this.getStoredPath(cid), bytes)\n  }\n\n  async quarantine(cid: CID): Promise<void> {\n    await this.ensureQuarantine()\n    try {\n      await fs.rename(this.getStoredPath(cid), this.getQuarantinePath(cid))\n    } catch (err) {\n      throw translateErr(err)\n    }\n  }\n\n  async unquarantine(cid: CID): Promise<void> {\n    await this.ensureDir()\n    try {\n      await fs.rename(this.getQuarantinePath(cid), this.getStoredPath(cid))\n    } catch (err) {\n      throw translateErr(err)\n    }\n  }\n\n  async getBytes(cid: CID): Promise<Uint8Array> {\n    try {\n      return await fs.readFile(this.getStoredPath(cid))\n    } catch (err) {\n      throw translateErr(err)\n    }\n  }\n\n  async getStream(cid: CID): Promise<stream.Readable> {\n    const path = this.getStoredPath(cid)\n    const exists = await fileExists(path)\n    if (!exists) {\n      throw new BlobNotFoundError()\n    }\n    return fsSync.createReadStream(path)\n  }\n\n  async delete(cid: CID): Promise<void> {\n    await rmIfExists(this.getStoredPath(cid))\n  }\n\n  async deleteMany(cids: CID[]): Promise<void> {\n    const errors: unknown[] = []\n    for (const chunk of chunkArray(cids, 500)) {\n      await Promise.all(\n        chunk.map((cid) =>\n          this.delete(cid).catch((err) => {\n            log.error({ err, cid: cid.toString() }, 'error deleting blob')\n            errors.push(err)\n          }),\n        ),\n      )\n    }\n    if (errors.length) throw aggregateErrors(errors)\n  }\n\n  async deleteAll(): Promise<void> {\n    await rmIfExists(path.join(this.location, this.did), true)\n    await rmIfExists(path.join(this.tmpLocation, this.did), true)\n    await rmIfExists(path.join(this.quarantineLocation, this.did), true)\n  }\n}\n\nconst translateErr = (err: unknown): BlobNotFoundError | unknown => {\n  if (isErrnoException(err) && err.code === 'ENOENT') {\n    return new BlobNotFoundError()\n  }\n  return err\n}\n"
  },
  {
    "path": "packages/pds/src/error.ts",
    "content": "import { ErrorRequestHandler } from 'express'\nimport { OAuthError } from '@atproto/oauth-provider'\nimport { XRPCError } from '@atproto/xrpc-server'\nimport { httpLogger as log } from './logger'\n\nexport const handler: ErrorRequestHandler = (err, _req, res, next) => {\n  log.error({ err }, 'unexpected internal server error')\n  if (res.headersSent) {\n    return next(err)\n  }\n\n  if (err instanceof OAuthError) {\n    res.status(err.status).json(err.toJSON())\n    return\n  }\n\n  const serverError = XRPCError.fromError(err)\n  res.status(serverError.type).json(serverError.payload)\n}\n"
  },
  {
    "path": "packages/pds/src/handle/explicit-slurs.ts",
    "content": "// regexes were originally copied from: https://github.com/Blank-Cheque/Slurs\n// small modifications have been made\n/* eslint-disable no-misleading-character-class */\nconst explicitSlurRegexes = [\n  /\\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][hĤĥȞȟḦḧḢḣḨḩḤḥḪḫH̱ẖĦħⱧⱨꞪɦꞕΗНн][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌ][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпＮｎ][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\\b/,\n  /\\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺＯｏ0]{2}[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпＮｎ][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\\b/,\n  /\\b[fḞḟƑƒꞘꞙᵮᶂ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚＡａ@4][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶＧｇ]{1,2}([ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺＯｏ0e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅiÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌ][tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ]{1,2}([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ])?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\\b/,\n  /\\b[kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌyÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ]([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ])?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]*\\b/,\n  /\\b[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпＮｎ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌoÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺＯｏІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚＡａ][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶＧｇqꝖꝗꝘꝙɋʠ]{2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅaÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚＡａ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ]?|n[ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺＯｏ0][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶＧｇqꝖꝗꝘꝙɋʠ]|[a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚＡａ]?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\\b/,\n  /[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпＮｎ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌoÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺＯｏІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚＡａ][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶＧｇqꝖꝗꝘꝙɋʠ]{2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?/,\n  /\\b[tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚＡａ4]+[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпＮｎ]{1,2}([iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıＩｉ1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴＬｌ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ]|[yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳＥｅ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\\b/,\n]\n\nexport const hasExplicitSlur = (handle: string): boolean => {\n  return explicitSlurRegexes.some(\n    (reg) =>\n      reg.test(handle) ||\n      reg.test(\n        handle.replaceAll('.', '').replaceAll('-', '').replaceAll('_', ''),\n      ),\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/handle/index.ts",
    "content": "import {\n  InvalidHandleError,\n  normalizeAndEnsureValidHandle,\n} from '@atproto/syntax'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { reservedSubdomains } from './reserved'\n\nexport const baseNormalizeAndValidate = (handle: string) => {\n  try {\n    return normalizeAndEnsureValidHandle(handle)\n  } catch (err) {\n    if (err instanceof InvalidHandleError) {\n      throw new InvalidRequestError(err.message, 'InvalidHandle')\n    }\n    throw err\n  }\n}\n\nexport const isServiceDomain = (\n  handle: string,\n  availableUserDomains: string[],\n): boolean => {\n  return availableUserDomains.some((domain) => handle.endsWith(domain))\n}\n\nexport const ensureHandleServiceConstraints = (\n  handle: string,\n  availableUserDomains: string[],\n  allowReserved = false,\n): void => {\n  const supportedDomain =\n    availableUserDomains.find((domain) => handle.endsWith(domain)) ?? ''\n  const front = handle.slice(0, handle.length - supportedDomain.length)\n  if (front.includes('.')) {\n    throw new InvalidRequestError(\n      'Invalid characters in handle',\n      'InvalidHandle',\n    )\n  }\n  if (front.length < 3) {\n    throw new InvalidRequestError('Handle too short', 'InvalidHandle')\n  }\n  if (front.length > 18) {\n    throw new InvalidRequestError('Handle too long', 'InvalidHandle')\n  }\n  if (!allowReserved && reservedSubdomains[front]) {\n    throw new InvalidRequestError('Reserved handle', 'HandleNotAvailable')\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/handle/reserved.ts",
    "content": "const atpSpecific = [\n  'at',\n  'atp',\n  'plc',\n  'pds',\n  'did',\n  'repo',\n  'tid',\n  'nsid',\n  'xrpc',\n  'lex',\n  'lexicon',\n  'bsky',\n  'bluesky',\n  'handle',\n]\n\n// naively pulled from: https://radwebhosting.com/client_area/knowledgebase/196/List-of-Banned-Subdomain-Prefixes.html\nconst commonlyReserved = [\n  'about',\n  'abuse',\n  'access',\n  'account',\n  'accounts',\n  'acme',\n  'activate',\n  'activities',\n  'activity',\n  'ad',\n  'add',\n  'address',\n  'adm',\n  'admanager',\n  'admin',\n  'administration',\n  'administrator',\n  'administrators',\n  'admins',\n  'ads',\n  'adsense',\n  'adult',\n  'advertising',\n  'adwords',\n  'affiliate',\n  'affiliatepage',\n  'affiliates',\n  'afp',\n  'ajax',\n  'all',\n  'alpha',\n  'analysis',\n  'analytics',\n  'android',\n  'anon',\n  'anonymous',\n  'answer',\n  'answers',\n  'ap',\n  'api',\n  'apis',\n  'app',\n  'appengine',\n  'appnews',\n  'apps',\n  'archive',\n  'archives',\n  'article',\n  'asdf',\n  'asset',\n  'assets',\n  'auth',\n  'authentication',\n  'avatar',\n  'backup',\n  'bank',\n  'banner',\n  'banners',\n  'base',\n  'beginners',\n  'beta',\n  'billing',\n  'bin',\n  'binaries',\n  'binary',\n  'blackberry',\n  'blog',\n  'blogs',\n  'blogsearch',\n  'board',\n  'book',\n  'bookmark',\n  'bookmarks',\n  'books',\n  'bot',\n  'bots',\n  'bug',\n  'bugs',\n  'business',\n  'buy',\n  'buzz',\n  'cache',\n  'calendar',\n  'call',\n  'campaign',\n  'cancel',\n  'captcha',\n  'career',\n  'careers',\n  'cart',\n  'catalog',\n  'catalogs',\n  'categories',\n  'category',\n  'cdn',\n  'cgi',\n  'cgi-bin',\n  'changelog',\n  'chart',\n  'charts',\n  'chat',\n  'check',\n  'checked',\n  'checking',\n  'checkout',\n  'client',\n  'cliente',\n  'clients',\n  'clients1',\n  'cnarne',\n  'code',\n  'comercial',\n  'comment',\n  'comments',\n  'communities',\n  'community',\n  'company',\n  'compare',\n  'compras',\n  'config',\n  'configuration',\n  'confirm',\n  'confirmation',\n  'connect',\n  'contact',\n  'contacts',\n  'contactus',\n  'contact-us',\n  'contact_us',\n  'content',\n  'contest',\n  'contribute',\n  'contributor',\n  'contributors',\n  'coppa',\n  'copyright',\n  'copyrights',\n  'core',\n  'corp',\n  'countries',\n  'country',\n  'cpanel',\n  'create',\n  'css',\n  'cssproxy',\n  'customise',\n  'customize',\n  'dashboard',\n  'data',\n  'db',\n  'default',\n  'delete',\n  'demo',\n  'design',\n  'designer',\n  'desktop',\n  'destroy',\n  'dev',\n  'devel',\n  'developer',\n  'developers',\n  'devs',\n  'diagram',\n  'diary',\n  'dict',\n  'dictionary',\n  'die',\n  'dir',\n  'directory',\n  'direct_messages',\n  'direct-messages',\n  'dist',\n  'diversity',\n  'dl',\n  'dmca',\n  'doc',\n  'docs',\n  'documentation',\n  'documentations',\n  'documents',\n  'domain',\n  'domains',\n  'donate',\n  'download',\n  'downloads',\n  'e',\n  'e-mail',\n  'earth',\n  'ecommerce',\n  'edit',\n  'edits',\n  'editor',\n  'edu',\n  'education',\n  'email',\n  'embed',\n  'embedded',\n  'employment',\n  'employments',\n  'empty',\n  'enable',\n  'encrypted',\n  'end',\n  'engine',\n  'enterprise',\n  'enterprises',\n  'entries',\n  'entry',\n  'error',\n  'errorlog',\n  'errors',\n  'eval',\n  'event',\n  'example',\n  'examplecommunity',\n  'exampleopenid',\n  'examplesyn',\n  'examplesyndicated',\n  'exampleusername',\n  'exchange',\n  'exit',\n  'explore',\n  'faq',\n  'faqs',\n  'favorite',\n  'favorites',\n  'favourite',\n  'favourites',\n  'feature',\n  'features',\n  'feed',\n  'feedback',\n  'feedburner',\n  'feedproxy',\n  'feeds',\n  'file',\n  'files',\n  'finance',\n  'folder',\n  'folders',\n  'first',\n  'following',\n  'forgot',\n  'form',\n  'forms',\n  'forum',\n  'forums',\n  'founder',\n  'free',\n  'friend',\n  'friends',\n  'ftp',\n  'fuck',\n  'fun',\n  'fusion',\n  'gadget',\n  'gadgets',\n  'game',\n  'games',\n  'gears',\n  'general',\n  'geographic',\n  'get',\n  'gettingstarted',\n  'gift',\n  'gifts',\n  'gist',\n  'git',\n  'github',\n  'gmail',\n  'go',\n  'golang',\n  'goto',\n  'gov',\n  'graph',\n  'graphs',\n  'group',\n  'groups',\n  'guest',\n  'guests',\n  'guide',\n  'guides',\n  'hack',\n  'hacks',\n  'head',\n  'help',\n  'home',\n  'homepage',\n  'host',\n  'hosting',\n  'hostmaster',\n  'hostname',\n  'howto',\n  'how-to',\n  'how_to',\n  'html',\n  'htrnl',\n  'http',\n  'httpd',\n  'https',\n  'i',\n  'iamges',\n  'icon',\n  'icons',\n  'id',\n  'idea',\n  'ideas',\n  'im',\n  'image',\n  'images',\n  'img',\n  'imap',\n  'inbox',\n  'inboxes',\n  'index',\n  'indexes',\n  'info',\n  'information',\n  'inquiry',\n  'intranet',\n  'investor',\n  'investors',\n  'invitation',\n  'invitations',\n  'invite',\n  'invoice',\n  'invoices',\n  'imac',\n  'ios',\n  'ipad',\n  'iphone',\n  'irc',\n  'irnages',\n  'irng',\n  'is',\n  'issue',\n  'issues',\n  'it',\n  'item',\n  'items',\n  'java',\n  'javascript',\n  'job',\n  'jobs',\n  'join',\n  'js',\n  'json',\n  'jump',\n  'kb',\n  'knowledge-base',\n  'knowledgebase',\n  'lab',\n  'labs',\n  'language',\n  'languages',\n  'last',\n  'ldap_status',\n  'ldap-status',\n  'ldapstatus',\n  'legal',\n  'license',\n  'licenses',\n  'link',\n  'links',\n  'linux',\n  'list',\n  'lists',\n  'livejournal',\n  'lj',\n  'local',\n  'locale',\n  'location',\n  'log',\n  'log-in',\n  'log-out',\n  'login',\n  'logout',\n  'logs',\n  'log_in',\n  'log_out',\n  'm',\n  'mac',\n  'macos',\n  'macosx',\n  'mac-os',\n  'mac-os-x',\n  'mac_os_x',\n  'mail',\n  'mailer',\n  'mailing',\n  'main',\n  'maintenance',\n  'manage',\n  'manager',\n  'manual',\n  'map',\n  'maps',\n  'marketing',\n  'master',\n  'me',\n  'media',\n  'member',\n  'members',\n  'memories',\n  'memory',\n  'merchandise',\n  'message',\n  'messages',\n  'messenger',\n  'mg',\n  'microblog',\n  'microblogs',\n  'mine',\n  'mis',\n  'misc',\n  'mms',\n  'mob',\n  'mobile',\n  'model',\n  'models',\n  'money',\n  'movie',\n  'movies',\n  'mp3',\n  'mp4',\n  'msg',\n  'msn',\n  'music',\n  'mx',\n  'my',\n  'mymme',\n  'mysql',\n  'name',\n  'named',\n  'nan',\n  'navi',\n  'navigation',\n  'net',\n  'network',\n  'networks',\n  'new',\n  'news',\n  'newsletter',\n  'nick',\n  'nickname',\n  'nil',\n  'none',\n  'notes',\n  'noticias',\n  'notification',\n  'notifications',\n  'notify',\n  'ns',\n  'ns1',\n  'ns2',\n  'ns3',\n  'ns4',\n  'ns5',\n  'null',\n  'oauth',\n  'oauth-clients',\n  'oauth_clients',\n  'ocsp',\n  'offer',\n  'offers',\n  'official',\n  'old',\n  'online',\n  'openid',\n  'operator',\n  'option',\n  'options',\n  'order',\n  'orders',\n  'org',\n  'organization',\n  'organizations',\n  'other',\n  'overview',\n  'owner',\n  'owners',\n  'p0rn',\n  'pack',\n  'page',\n  'pager',\n  'pages',\n  'paid',\n  'panel',\n  'partner',\n  'partnerpage',\n  'partners',\n  'password',\n  'patch',\n  'pay',\n  'payment',\n  'people',\n  'perl',\n  'person',\n  'phone',\n  'photo',\n  'photoalbum',\n  'photos',\n  'php',\n  'phpmyadmin',\n  'phppgadmin',\n  'phpredisadmin',\n  'pic',\n  'pics',\n  'picture',\n  'pictures',\n  'ping',\n  'pixel',\n  'places',\n  'plan',\n  'plans',\n  'plugin',\n  'plugins',\n  'podcasts',\n  'policies',\n  'policy',\n  'pop',\n  'pop3',\n  'popular',\n  'porn',\n  'portal',\n  'portals',\n  'post',\n  'postfix',\n  'postmaster',\n  'posts',\n  'pr',\n  'pr0n',\n  'premium',\n  'press',\n  'price',\n  'pricing',\n  'principles',\n  'print',\n  'privacy',\n  'privacy-policy',\n  'privacypolicy',\n  'privacy_policy',\n  'private',\n  'prod',\n  'product',\n  'production',\n  'products',\n  'profile',\n  'profiles',\n  'project',\n  'projects',\n  'promo',\n  'promotions',\n  'proxies',\n  'proxy',\n  'pub',\n  'public',\n  'purchase',\n  'purpose',\n  'put',\n  'python',\n  'queries',\n  'query',\n  'radio',\n  'random',\n  'ranking',\n  'read',\n  'reader',\n  'readme',\n  'recent',\n  'recruit',\n  'recruitment',\n  'redirect',\n  'register',\n  'registration',\n  'release',\n  'remove',\n  'replies',\n  'report',\n  'reports',\n  'repositories',\n  'repository',\n  'req',\n  'request',\n  'requests',\n  'research',\n  'reset',\n  'resolve',\n  'resolver',\n  'review',\n  'rnail',\n  'rnicrosoft',\n  'roc',\n  'root',\n  'rss',\n  'ruby',\n  'rule',\n  'sag',\n  'sale',\n  'sales',\n  'sample',\n  'samples',\n  'sandbox',\n  'save',\n  'scholar',\n  'school',\n  'schools',\n  'script',\n  'scripts',\n  'search',\n  'secure',\n  'security',\n  'self',\n  'seminars',\n  'send',\n  'server',\n  'server-info',\n  'server_info',\n  'server-status',\n  'server_status',\n  'servers',\n  'service',\n  'services',\n  'session',\n  'sessions',\n  'setting',\n  'settings',\n  'setup',\n  'share',\n  'shop',\n  'shopping',\n  'shortcut',\n  'shortcuts',\n  'show',\n  'sign-in',\n  'sign-up',\n  'signin',\n  'signout',\n  'signup',\n  'sign_in',\n  'sign_up',\n  'site',\n  'sitemap',\n  'sitemaps',\n  'sitenews',\n  'sites',\n  'sketchup',\n  'sky',\n  'slash',\n  'slashinvoice',\n  'slut',\n  'smartphone',\n  'sms',\n  'smtp',\n  'soap',\n  'software',\n  'sorry',\n  'source',\n  'spec',\n  'special',\n  'spreadsheet',\n  'spreadsheets',\n  'sql',\n  'src',\n  'srntp',\n  'ssh',\n  'ssl',\n  'ssladmin',\n  'ssladministrator',\n  'sslwebmaster',\n  'ssytem',\n  'staff',\n  'stage',\n  'staging',\n  'start',\n  'stat',\n  'state',\n  'static',\n  'statistics',\n  'stats',\n  'status',\n  'store',\n  'stores',\n  'stories',\n  'style',\n  'styleguide',\n  'styles',\n  'stylesheet',\n  'stylesheets',\n  'subdomain',\n  'subscribe',\n  'subscription',\n  'subscriptions',\n  'suggest',\n  'suggestqueries',\n  'support',\n  'survey',\n  'surveys',\n  'surveytool',\n  'svn',\n  'swf',\n  'syn',\n  'sync',\n  'syndicated',\n  'sys',\n  'sysadmin',\n  'sysadministrator',\n  'sysadmins',\n  'system',\n  'tablet',\n  'tablets',\n  'tag',\n  'tags',\n  'talk',\n  'talkgadget',\n  'task',\n  'tasks',\n  'team',\n  'teams',\n  'tech',\n  'telnet',\n  'term',\n  'terms',\n  'terms-of-service',\n  'termsofservice',\n  'terms_of_service',\n  'test',\n  'testing',\n  'tests',\n  'text',\n  'theme',\n  'themes',\n  'thread',\n  'threads',\n  'ticket',\n  'tickets',\n  'tmp',\n  'todo',\n  'to-do',\n  'to_do',\n  'toml',\n  'tool',\n  'toolbar',\n  'toolbars',\n  'tools',\n  'top',\n  'topic',\n  'topics',\n  'tos',\n  'tour',\n  'trac',\n  'translate',\n  'trace',\n  'translation',\n  'translations',\n  'translator',\n  'trends',\n  'tutorial',\n  'tux',\n  'tv',\n  'twitter',\n  'txt',\n  'ul',\n  'undef',\n  'unfollow',\n  'unsubscribe',\n  'update',\n  'updates',\n  'upgrade',\n  'upgrades',\n  'upi',\n  'upload',\n  'uploads',\n  'url',\n  'usage',\n  'user',\n  'username',\n  'usernames',\n  'users',\n  'uuid',\n  'validation',\n  'validations',\n  'ver',\n  'version',\n  'video',\n  'videos',\n  'video-stats',\n  'visitor',\n  'visitors',\n  'voice',\n  'volunteer',\n  'volunteers',\n  'w',\n  'watch',\n  'wave',\n  'weather',\n  'web',\n  'webdisk',\n  'webhook',\n  'webhooks',\n  'webmail',\n  'webmaster',\n  'webmasters',\n  'webrnail',\n  'website',\n  'websites',\n  'welcome',\n  'whm',\n  'whois',\n  'widget',\n  'widgets',\n  'wifi',\n  'wiki',\n  'wikis',\n  'win',\n  'windows',\n  'word',\n  'work',\n  'works',\n  'workshop',\n  'wpad',\n  'ww',\n  'wws',\n  'www',\n  'wwws',\n  'wwww',\n  'xfn',\n  'xhtml',\n  'xhtrnl',\n  'xml',\n  'xmpp',\n  'xpg',\n  'xxx',\n  'yaml',\n  'year',\n  'yml',\n  'you',\n  'yourdomain',\n  'yourname',\n  'yoursite',\n  'yourusername',\n]\n\nconst famousAccounts = [\n  // reserving some large twitter accounts (top 100 by followers according to wikidata dump)\n  '10ronaldinho',\n  '3gerardpique',\n  'aclu',\n  'adele',\n  'akshaykumar',\n  'aliaa08',\n  'aliciakeys',\n  'amitshah',\n  'andresiniesta8',\n  'anushkasharma',\n  'arianagrande',\n  'arrahman',\n  'arvindkejriwal',\n  'avrillavigne',\n  'barackobama',\n  'bbcbreaking',\n  'bbcworld',\n  'beingsalmankhan',\n  'billgates',\n  'britneyspears',\n  'brunomars',\n  'bts_bighit',\n  'bts_twt',\n  'championsleague',\n  'chrisbrown',\n  'cnnbrk',\n  'coldplay',\n  'conanobrien',\n  'cristiano',\n  'danieltosh',\n  'davidguetta',\n  'ddlovato',\n  'deepikapadukone',\n  'drake',\n  'elisapie',\n  'ellendegeneres',\n  'elonmusk',\n  'eminem',\n  'emmawatson',\n  'fcbarcelona',\n  'foxnews',\n  'harry_styles',\n  'hillaryclinton',\n  'iamsrk',\n  'ihrithik',\n  'imvkohli',\n  'instagram',\n  'jimmyfallon',\n  'jlo',\n  'joebiden',\n  'jtimberlake',\n  'justinbieber',\n  'kaka',\n  'kanyewest',\n  'katyperry',\n  'kendalljenner',\n  'kevinhart4real',\n  'khloekardashian',\n  'kimkardashian',\n  'kingjames',\n  'kourtneykardash',\n  'kyliejenner',\n  'ladygaga',\n  'liampayne',\n  'liltunechi',\n  'manutd',\n  'mariahcarey',\n  'mileycyrus',\n  'mohamadalarefe',\n  'narendramodi',\n  'nasa',\n  'nba',\n  'neymarjr',\n  'nfl',\n  'niallofficial',\n  'nickiminaj',\n  'npr',\n  'nytimes',\n  'onedirection',\n  'oprah',\n  'pink',\n  'pitbull',\n  'playstation',\n  'pmoindia',\n  'premierleague',\n  'priyankachopra',\n  'realdonaldtrump',\n  'ricky_martin',\n  'rihanna',\n  'sachin_rt',\n  'selenagomez',\n  'shakira',\n  'shawnmendes',\n  'sportscenter',\n  'srbachchan',\n  'subhisharma100',\n  'taylorswift13',\n  'theeconomist',\n  'twitter',\n  'virendersehwag',\n  'whitehouse45',\n  'wizkhalifa',\n  'youtube',\n  'zaynmalik',\n\n  // some top instagram (https://en.wikipedia.org/wiki/List_of_most-followed_Instagram_accounts)\n  'beyonce',\n  'billieeilish',\n  'leomessi',\n  'natgeo',\n  'nike',\n  'snoopdogg',\n  'taylorswift',\n  'therock',\n\n  // ... and a couple more prominent accounts, subjectively\n  '10downingstreet',\n  'aoc',\n  'carterjwm',\n  'dril',\n  'gretathunberg',\n  'kamalaharris',\n  'kremlinrussia_e',\n  'potus',\n  'rondesantisfl',\n  'ukraine',\n  'washingtonpost',\n  'yousuck2020',\n  'zelenskyyua',\n\n  // top japan-specific accounts\n  'akiko_lawson',\n  'ariyoshihiroiki',\n  'asahi',\n  'dozle_official',\n  'famima_now',\n  'ff_xiv_jp',\n  'fujitv',\n  'gigazine',\n  'hajimesyacho',\n  'hikakin',\n  'jocx',\n  'jotx',\n  'kiyo_saiore',\n  'mainichi',\n  'matsu_bouzu',\n  'naomiosaka',\n  'nhk',\n  'nikkei',\n  'nintendo',\n  'ntv',\n  'oowareware1945',\n  'pamyurin',\n  'poke_times',\n  'rolaworld',\n  'seikintv',\n  'starbucksjapan',\n  'tbs',\n  'tbs_pr',\n  'tvasahi',\n  'tvtokyo',\n  'yokoono',\n  'yomiuri_online',\n\n  // top brazil-specific accounts\n  'brasildefato',\n  'claudialeitte',\n  'correio',\n  'em_com',\n  'estadao',\n  'folha',\n  'gazetadopovo',\n  'ivetesangalo',\n  'jairbolsonaro',\n  'jornaldobrasil',\n  'jornaloglobo',\n  'lucianohuck',\n  'lulaoficial',\n  'marcosmion',\n  'paulocoelho',\n  'portalr7',\n  'rede_globo',\n  'zerohora',\n]\n\nexport const reservedSubdomains: Record<string, boolean> = [\n  ...atpSpecific,\n  ...commonlyReserved,\n  ...famousAccounts,\n].reduce((acc, cur) => {\n  return {\n    ...acc,\n    [cur]: true,\n  }\n}, {})\n"
  },
  {
    "path": "packages/pds/src/image/image-url-builder.ts",
    "content": "import { BskyAppView } from '../bsky-app-view'\nimport { ids } from '../lexicon/lexicons'\n\nexport class ImageUrlBuilder {\n  constructor(\n    readonly pdsHostname: string,\n    readonly bskyAppView?: BskyAppView,\n  ) {}\n\n  build(pattern: string, did: string, cid: string): string {\n    return (\n      this.bskyAppView?.getImageUrl(pattern, did, cid) ??\n      `https://${this.pdsHostname}/xrpc/${ids.ComAtprotoSyncGetBlob}?did=${did}&cid=${cid}`\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/index.ts",
    "content": "// catch errors that get thrown in async route handlers\n// this is a relatively non-invasive change to express\n// they get handled in the error.handler middleware\n// leave at top of file before importing Routes\nimport 'express-async-errors'\n\nimport events from 'node:events'\nimport http from 'node:http'\nimport { PlcClientError } from '@did-plc/lib'\nimport cors from 'cors'\nimport express from 'express'\nimport { HttpTerminator, createHttpTerminator } from 'http-terminator'\nimport { DAY, HOUR, MINUTE, SECOND } from '@atproto/common'\nimport {\n  MemoryRateLimiter,\n  MethodHandler,\n  RedisRateLimiter,\n  ResponseType,\n  XRPCError,\n} from '@atproto/xrpc-server'\nimport apiRoutes from './api'\nimport * as authRoutes from './auth-routes'\nimport * as basicRoutes from './basic-routes'\nimport { ServerConfig, ServerSecrets } from './config'\nimport { AppContext, AppContextOptions } from './context'\nimport * as error from './error'\nimport { createServer } from './lexicon'\nimport * as AppBskyFeedGetFeedSkeleton from './lexicon/types/app/bsky/feed/getFeedSkeleton'\nimport { loggerMiddleware } from './logger'\nimport { proxyHandler } from './pipethrough'\nimport compression from './util/compression'\nimport * as wellKnown from './well-known'\n\nexport { createSecretKeyObject } from './auth-verifier'\nexport * from './config'\nexport { AppContext } from './context'\nexport { Database } from './db'\nexport { DiskBlobStore } from './disk-blobstore'\nexport { createServer as createLexiconServer } from './lexicon'\nexport { httpLogger } from './logger'\nexport { type CommitDataWithOps, type PreparedWrite } from './repo'\nexport * as repoPrepare from './repo/prepare'\nexport { scripts } from './scripts'\nexport * as sequencer from './sequencer'\n\n// Legacy export for backwards compatibility\nexport type SkeletonHandler = MethodHandler<\n  void,\n  AppBskyFeedGetFeedSkeleton.QueryParams,\n  AppBskyFeedGetFeedSkeleton.HandlerInput,\n  AppBskyFeedGetFeedSkeleton.HandlerOutput\n>\n\nexport class PDS {\n  public ctx: AppContext\n  public app: express.Application\n  public server?: http.Server\n  private terminator?: HttpTerminator\n  private dbStatsInterval?: NodeJS.Timeout\n  private sequencerStatsInterval?: NodeJS.Timeout\n\n  constructor(opts: { ctx: AppContext; app: express.Application }) {\n    this.ctx = opts.ctx\n    this.app = opts.app\n  }\n\n  static async create(\n    cfg: ServerConfig,\n    secrets: ServerSecrets,\n    overrides?: Partial<AppContextOptions>,\n  ): Promise<PDS> {\n    const ctx = await AppContext.fromConfig(cfg, secrets, overrides)\n\n    const { rateLimits } = ctx.cfg\n\n    const server = createServer({\n      validateResponse: false,\n      payload: {\n        jsonLimit: 150 * 1024, // 150kb\n        textLimit: 100 * 1024, // 100kb\n        blobLimit: cfg.service.blobUploadLimit,\n      },\n      catchall: proxyHandler(ctx),\n      errorParser: (err) => {\n        if (err instanceof PlcClientError) {\n          const payloadMessage =\n            typeof err.data === 'object' &&\n            err.data != null &&\n            'message' in err.data &&\n            typeof err.data.message === 'string' &&\n            err.data.message\n\n          const type =\n            err.status >= 500\n              ? ResponseType.UpstreamFailure\n              : ResponseType.InvalidRequest\n\n          return new XRPCError(\n            type,\n            payloadMessage || 'Unable to perform PLC operation',\n          )\n        }\n\n        return XRPCError.fromError(err)\n      },\n      rateLimits: rateLimits.enabled\n        ? {\n            creator: ctx.redisScratch\n              ? (opts) => new RedisRateLimiter(ctx.redisScratch, opts)\n              : (opts) => new MemoryRateLimiter(opts),\n            bypass: ({ req }) => {\n              const { bypassKey, bypassIps } = rateLimits\n              if (\n                bypassKey &&\n                bypassKey === req.headers['x-ratelimit-bypass']\n              ) {\n                return true\n              }\n              if (bypassIps && bypassIps.includes(req.ip)) {\n                return true\n              }\n              return false\n            },\n            global: [\n              {\n                name: 'global-ip',\n                durationMs: 5 * MINUTE,\n                points: 3000,\n              },\n            ],\n            shared: [\n              {\n                name: 'repo-write-hour',\n                durationMs: HOUR,\n                points: 5000, // creates=3, puts=2, deletes=1\n              },\n              {\n                name: 'repo-write-day',\n                durationMs: DAY,\n                points: 35000, // creates=3, puts=2, deletes=1\n              },\n            ],\n          }\n        : undefined,\n    })\n\n    apiRoutes(server, ctx)\n\n    const app = express()\n    app.set('trust proxy', [\n      // e.g. load balancer\n      'loopback',\n      'linklocal',\n      'uniquelocal',\n      // e.g. trust x-forwarded-for via entryway ip\n      ...getTrustedIps(cfg),\n    ])\n    app.use(loggerMiddleware)\n    app.use(compression())\n    app.use(authRoutes.createRouter(ctx)) // Before CORS\n    app.use(cors({ maxAge: DAY / SECOND }))\n    app.use(basicRoutes.createRouter(ctx))\n    app.use(wellKnown.createRouter(ctx))\n    app.use(server.xrpc.router)\n    app.use(error.handler)\n\n    return new PDS({\n      ctx,\n      app,\n    })\n  }\n\n  async start(): Promise<http.Server> {\n    await this.ctx.sequencer.start()\n    const server = this.app.listen(this.ctx.cfg.service.port)\n    this.server = server\n    this.server.keepAliveTimeout = 90000\n    this.terminator = createHttpTerminator({ server })\n    await events.once(server, 'listening')\n    return server\n  }\n\n  async destroy(): Promise<void> {\n    await this.ctx.sequencer.destroy()\n    await this.terminator?.terminate()\n    await this.ctx.backgroundQueue.destroy()\n    await this.ctx.accountManager.close()\n    await this.ctx.redisScratch?.quit()\n    await this.ctx.proxyAgent.destroy()\n    clearInterval(this.dbStatsInterval)\n    clearInterval(this.sequencerStatsInterval)\n  }\n}\n\nexport default PDS\n\nconst getTrustedIps = (cfg: ServerConfig) => {\n  if (!cfg.rateLimits.enabled) return []\n  return cfg.rateLimits.bypassIps ?? []\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/index.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type Auth,\n  type Options as XrpcOptions,\n  Server as XrpcServer,\n  type StreamConfigOrHandler,\n  type MethodConfigOrHandler,\n  createServer as createXrpcServer,\n} from '@atproto/xrpc-server'\nimport { schemas } from './lexicons.js'\nimport * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences.js'\nimport * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile.js'\nimport * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles.js'\nimport * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions.js'\nimport * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences.js'\nimport * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors.js'\nimport * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead.js'\nimport * as AppBskyAgeassuranceBegin from './types/app/bsky/ageassurance/begin.js'\nimport * as AppBskyAgeassuranceGetConfig from './types/app/bsky/ageassurance/getConfig.js'\nimport * as AppBskyAgeassuranceGetState from './types/app/bsky/ageassurance/getState.js'\nimport * as AppBskyBookmarkCreateBookmark from './types/app/bsky/bookmark/createBookmark.js'\nimport * as AppBskyBookmarkDeleteBookmark from './types/app/bsky/bookmark/deleteBookmark.js'\nimport * as AppBskyBookmarkGetBookmarks from './types/app/bsky/bookmark/getBookmarks.js'\nimport * as AppBskyContactDismissMatch from './types/app/bsky/contact/dismissMatch.js'\nimport * as AppBskyContactGetMatches from './types/app/bsky/contact/getMatches.js'\nimport * as AppBskyContactGetSyncStatus from './types/app/bsky/contact/getSyncStatus.js'\nimport * as AppBskyContactImportContacts from './types/app/bsky/contact/importContacts.js'\nimport * as AppBskyContactRemoveData from './types/app/bsky/contact/removeData.js'\nimport * as AppBskyContactSendNotification from './types/app/bsky/contact/sendNotification.js'\nimport * as AppBskyContactStartPhoneVerification from './types/app/bsky/contact/startPhoneVerification.js'\nimport * as AppBskyContactVerifyPhone from './types/app/bsky/contact/verifyPhone.js'\nimport * as AppBskyDraftCreateDraft from './types/app/bsky/draft/createDraft.js'\nimport * as AppBskyDraftDeleteDraft from './types/app/bsky/draft/deleteDraft.js'\nimport * as AppBskyDraftGetDrafts from './types/app/bsky/draft/getDrafts.js'\nimport * as AppBskyDraftUpdateDraft from './types/app/bsky/draft/updateDraft.js'\nimport * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator.js'\nimport * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds.js'\nimport * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes.js'\nimport * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed.js'\nimport * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed.js'\nimport * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator.js'\nimport * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators.js'\nimport * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'\nimport * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'\nimport * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'\nimport * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'\nimport * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'\nimport * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'\nimport * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'\nimport * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'\nimport * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline.js'\nimport * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts.js'\nimport * as AppBskyFeedSendInteractions from './types/app/bsky/feed/sendInteractions.js'\nimport * as AppBskyGraphGetActorStarterPacks from './types/app/bsky/graph/getActorStarterPacks.js'\nimport * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks.js'\nimport * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers.js'\nimport * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows.js'\nimport * as AppBskyGraphGetKnownFollowers from './types/app/bsky/graph/getKnownFollowers.js'\nimport * as AppBskyGraphGetList from './types/app/bsky/graph/getList.js'\nimport * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks.js'\nimport * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes.js'\nimport * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists.js'\nimport * as AppBskyGraphGetListsWithMembership from './types/app/bsky/graph/getListsWithMembership.js'\nimport * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes.js'\nimport * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelationships.js'\nimport * as AppBskyGraphGetStarterPack from './types/app/bsky/graph/getStarterPack.js'\nimport * as AppBskyGraphGetStarterPacks from './types/app/bsky/graph/getStarterPacks.js'\nimport * as AppBskyGraphGetStarterPacksWithMembership from './types/app/bsky/graph/getStarterPacksWithMembership.js'\nimport * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor.js'\nimport * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor.js'\nimport * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList.js'\nimport * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread.js'\nimport * as AppBskyGraphSearchStarterPacks from './types/app/bsky/graph/searchStarterPacks.js'\nimport * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'\nimport * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'\nimport * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'\nimport * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'\nimport * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'\nimport * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'\nimport * as AppBskyNotificationListActivitySubscriptions from './types/app/bsky/notification/listActivitySubscriptions.js'\nimport * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'\nimport * as AppBskyNotificationPutActivitySubscription from './types/app/bsky/notification/putActivitySubscription.js'\nimport * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'\nimport * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'\nimport * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'\nimport * as AppBskyNotificationUnregisterPush from './types/app/bsky/notification/unregisterPush.js'\nimport * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'\nimport * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'\nimport * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacks from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton from './types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'\nimport * as AppBskyUnspeccedGetPostThreadOtherV2 from './types/app/bsky/unspecced/getPostThreadOtherV2.js'\nimport * as AppBskyUnspeccedGetPostThreadV2 from './types/app/bsky/unspecced/getPostThreadV2.js'\nimport * as AppBskyUnspeccedGetSuggestedFeeds from './types/app/bsky/unspecced/getSuggestedFeeds.js'\nimport * as AppBskyUnspeccedGetSuggestedFeedsSkeleton from './types/app/bsky/unspecced/getSuggestedFeedsSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedOnboardingUsers from './types/app/bsky/unspecced/getSuggestedOnboardingUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacks from './types/app/bsky/unspecced/getSuggestedStarterPacks.js'\nimport * as AppBskyUnspeccedGetSuggestedStarterPacksSkeleton from './types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestedUsers from './types/app/bsky/unspecced/getSuggestedUsers.js'\nimport * as AppBskyUnspeccedGetSuggestedUsersSkeleton from './types/app/bsky/unspecced/getSuggestedUsersSkeleton.js'\nimport * as AppBskyUnspeccedGetSuggestionsSkeleton from './types/app/bsky/unspecced/getSuggestionsSkeleton.js'\nimport * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions.js'\nimport * as AppBskyUnspeccedGetTrendingTopics from './types/app/bsky/unspecced/getTrendingTopics.js'\nimport * as AppBskyUnspeccedGetTrends from './types/app/bsky/unspecced/getTrends.js'\nimport * as AppBskyUnspeccedGetTrendsSkeleton from './types/app/bsky/unspecced/getTrendsSkeleton.js'\nimport * as AppBskyUnspeccedInitAgeAssurance from './types/app/bsky/unspecced/initAgeAssurance.js'\nimport * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton.js'\nimport * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton.js'\nimport * as AppBskyUnspeccedSearchStarterPacksSkeleton from './types/app/bsky/unspecced/searchStarterPacksSkeleton.js'\nimport * as AppBskyVideoGetJobStatus from './types/app/bsky/video/getJobStatus.js'\nimport * as AppBskyVideoGetUploadLimits from './types/app/bsky/video/getUploadLimits.js'\nimport * as AppBskyVideoUploadVideo from './types/app/bsky/video/uploadVideo.js'\nimport * as ChatBskyActorDeleteAccount from './types/chat/bsky/actor/deleteAccount.js'\nimport * as ChatBskyActorExportAccountData from './types/chat/bsky/actor/exportAccountData.js'\nimport * as ChatBskyConvoAcceptConvo from './types/chat/bsky/convo/acceptConvo.js'\nimport * as ChatBskyConvoAddReaction from './types/chat/bsky/convo/addReaction.js'\nimport * as ChatBskyConvoDeleteMessageForSelf from './types/chat/bsky/convo/deleteMessageForSelf.js'\nimport * as ChatBskyConvoGetConvo from './types/chat/bsky/convo/getConvo.js'\nimport * as ChatBskyConvoGetConvoAvailability from './types/chat/bsky/convo/getConvoAvailability.js'\nimport * as ChatBskyConvoGetConvoForMembers from './types/chat/bsky/convo/getConvoForMembers.js'\nimport * as ChatBskyConvoGetLog from './types/chat/bsky/convo/getLog.js'\nimport * as ChatBskyConvoGetMessages from './types/chat/bsky/convo/getMessages.js'\nimport * as ChatBskyConvoLeaveConvo from './types/chat/bsky/convo/leaveConvo.js'\nimport * as ChatBskyConvoListConvos from './types/chat/bsky/convo/listConvos.js'\nimport * as ChatBskyConvoMuteConvo from './types/chat/bsky/convo/muteConvo.js'\nimport * as ChatBskyConvoRemoveReaction from './types/chat/bsky/convo/removeReaction.js'\nimport * as ChatBskyConvoSendMessage from './types/chat/bsky/convo/sendMessage.js'\nimport * as ChatBskyConvoSendMessageBatch from './types/chat/bsky/convo/sendMessageBatch.js'\nimport * as ChatBskyConvoUnmuteConvo from './types/chat/bsky/convo/unmuteConvo.js'\nimport * as ChatBskyConvoUpdateAllRead from './types/chat/bsky/convo/updateAllRead.js'\nimport * as ChatBskyConvoUpdateRead from './types/chat/bsky/convo/updateRead.js'\nimport * as ChatBskyModerationGetActorMetadata from './types/chat/bsky/moderation/getActorMetadata.js'\nimport * as ChatBskyModerationGetMessageContext from './types/chat/bsky/moderation/getMessageContext.js'\nimport * as ChatBskyModerationUpdateActorAccess from './types/chat/bsky/moderation/updateActorAccess.js'\nimport * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount.js'\nimport * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites.js'\nimport * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes.js'\nimport * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites.js'\nimport * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo.js'\nimport * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos.js'\nimport * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes.js'\nimport * as ComAtprotoAdminGetSubjectStatus from './types/com/atproto/admin/getSubjectStatus.js'\nimport * as ComAtprotoAdminSearchAccounts from './types/com/atproto/admin/searchAccounts.js'\nimport * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail.js'\nimport * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail.js'\nimport * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle.js'\nimport * as ComAtprotoAdminUpdateAccountPassword from './types/com/atproto/admin/updateAccountPassword.js'\nimport * as ComAtprotoAdminUpdateAccountSigningKey from './types/com/atproto/admin/updateAccountSigningKey.js'\nimport * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus.js'\nimport * as ComAtprotoIdentityGetRecommendedDidCredentials from './types/com/atproto/identity/getRecommendedDidCredentials.js'\nimport * as ComAtprotoIdentityRefreshIdentity from './types/com/atproto/identity/refreshIdentity.js'\nimport * as ComAtprotoIdentityRequestPlcOperationSignature from './types/com/atproto/identity/requestPlcOperationSignature.js'\nimport * as ComAtprotoIdentityResolveDid from './types/com/atproto/identity/resolveDid.js'\nimport * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle.js'\nimport * as ComAtprotoIdentityResolveIdentity from './types/com/atproto/identity/resolveIdentity.js'\nimport * as ComAtprotoIdentitySignPlcOperation from './types/com/atproto/identity/signPlcOperation.js'\nimport * as ComAtprotoIdentitySubmitPlcOperation from './types/com/atproto/identity/submitPlcOperation.js'\nimport * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle.js'\nimport * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels.js'\nimport * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels.js'\nimport * as ComAtprotoLexiconResolveLexicon from './types/com/atproto/lexicon/resolveLexicon.js'\nimport * as ComAtprotoModerationCreateReport from './types/com/atproto/moderation/createReport.js'\nimport * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites.js'\nimport * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord.js'\nimport * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord.js'\nimport * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo.js'\nimport * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord.js'\nimport * as ComAtprotoRepoImportRepo from './types/com/atproto/repo/importRepo.js'\nimport * as ComAtprotoRepoListMissingBlobs from './types/com/atproto/repo/listMissingBlobs.js'\nimport * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords.js'\nimport * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord.js'\nimport * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob.js'\nimport * as ComAtprotoServerActivateAccount from './types/com/atproto/server/activateAccount.js'\nimport * as ComAtprotoServerCheckAccountStatus from './types/com/atproto/server/checkAccountStatus.js'\nimport * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail.js'\nimport * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount.js'\nimport * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword.js'\nimport * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode.js'\nimport * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes.js'\nimport * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession.js'\nimport * as ComAtprotoServerDeactivateAccount from './types/com/atproto/server/deactivateAccount.js'\nimport * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount.js'\nimport * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession.js'\nimport * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer.js'\nimport * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes.js'\nimport * as ComAtprotoServerGetServiceAuth from './types/com/atproto/server/getServiceAuth.js'\nimport * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession.js'\nimport * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords.js'\nimport * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession.js'\nimport * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete.js'\nimport * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation.js'\nimport * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate.js'\nimport * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset.js'\nimport * as ComAtprotoServerReserveSigningKey from './types/com/atproto/server/reserveSigningKey.js'\nimport * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword.js'\nimport * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword.js'\nimport * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail.js'\nimport * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob.js'\nimport * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks.js'\nimport * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout.js'\nimport * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead.js'\nimport * as ComAtprotoSyncGetHostStatus from './types/com/atproto/sync/getHostStatus.js'\nimport * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit.js'\nimport * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord.js'\nimport * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo.js'\nimport * as ComAtprotoSyncGetRepoStatus from './types/com/atproto/sync/getRepoStatus.js'\nimport * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs.js'\nimport * as ComAtprotoSyncListHosts from './types/com/atproto/sync/listHosts.js'\nimport * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos.js'\nimport * as ComAtprotoSyncListReposByCollection from './types/com/atproto/sync/listReposByCollection.js'\nimport * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate.js'\nimport * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl.js'\nimport * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos.js'\nimport * as ComAtprotoTempAddReservedHandle from './types/com/atproto/temp/addReservedHandle.js'\nimport * as ComAtprotoTempCheckHandleAvailability from './types/com/atproto/temp/checkHandleAvailability.js'\nimport * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue.js'\nimport * as ComAtprotoTempDereferenceScope from './types/com/atproto/temp/dereferenceScope.js'\nimport * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels.js'\nimport * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification.js'\nimport * as ComAtprotoTempRevokeAccountCredentials from './types/com/atproto/temp/revokeAccountCredentials.js'\nimport * as ToolsOzoneCommunicationCreateTemplate from './types/tools/ozone/communication/createTemplate.js'\nimport * as ToolsOzoneCommunicationDeleteTemplate from './types/tools/ozone/communication/deleteTemplate.js'\nimport * as ToolsOzoneCommunicationListTemplates from './types/tools/ozone/communication/listTemplates.js'\nimport * as ToolsOzoneCommunicationUpdateTemplate from './types/tools/ozone/communication/updateTemplate.js'\nimport * as ToolsOzoneHostingGetAccountHistory from './types/tools/ozone/hosting/getAccountHistory.js'\nimport * as ToolsOzoneModerationCancelScheduledActions from './types/tools/ozone/moderation/cancelScheduledActions.js'\nimport * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent.js'\nimport * as ToolsOzoneModerationGetAccountTimeline from './types/tools/ozone/moderation/getAccountTimeline.js'\nimport * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent.js'\nimport * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord.js'\nimport * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords.js'\nimport * as ToolsOzoneModerationGetRepo from './types/tools/ozone/moderation/getRepo.js'\nimport * as ToolsOzoneModerationGetReporterStats from './types/tools/ozone/moderation/getReporterStats.js'\nimport * as ToolsOzoneModerationGetRepos from './types/tools/ozone/moderation/getRepos.js'\nimport * as ToolsOzoneModerationGetSubjects from './types/tools/ozone/moderation/getSubjects.js'\nimport * as ToolsOzoneModerationListScheduledActions from './types/tools/ozone/moderation/listScheduledActions.js'\nimport * as ToolsOzoneModerationQueryEvents from './types/tools/ozone/moderation/queryEvents.js'\nimport * as ToolsOzoneModerationQueryStatuses from './types/tools/ozone/moderation/queryStatuses.js'\nimport * as ToolsOzoneModerationScheduleAction from './types/tools/ozone/moderation/scheduleAction.js'\nimport * as ToolsOzoneModerationSearchRepos from './types/tools/ozone/moderation/searchRepos.js'\nimport * as ToolsOzoneSafelinkAddRule from './types/tools/ozone/safelink/addRule.js'\nimport * as ToolsOzoneSafelinkQueryEvents from './types/tools/ozone/safelink/queryEvents.js'\nimport * as ToolsOzoneSafelinkQueryRules from './types/tools/ozone/safelink/queryRules.js'\nimport * as ToolsOzoneSafelinkRemoveRule from './types/tools/ozone/safelink/removeRule.js'\nimport * as ToolsOzoneSafelinkUpdateRule from './types/tools/ozone/safelink/updateRule.js'\nimport * as ToolsOzoneServerGetConfig from './types/tools/ozone/server/getConfig.js'\nimport * as ToolsOzoneSetAddValues from './types/tools/ozone/set/addValues.js'\nimport * as ToolsOzoneSetDeleteSet from './types/tools/ozone/set/deleteSet.js'\nimport * as ToolsOzoneSetDeleteValues from './types/tools/ozone/set/deleteValues.js'\nimport * as ToolsOzoneSetGetValues from './types/tools/ozone/set/getValues.js'\nimport * as ToolsOzoneSetQuerySets from './types/tools/ozone/set/querySets.js'\nimport * as ToolsOzoneSetUpsertSet from './types/tools/ozone/set/upsertSet.js'\nimport * as ToolsOzoneSettingListOptions from './types/tools/ozone/setting/listOptions.js'\nimport * as ToolsOzoneSettingRemoveOptions from './types/tools/ozone/setting/removeOptions.js'\nimport * as ToolsOzoneSettingUpsertOption from './types/tools/ozone/setting/upsertOption.js'\nimport * as ToolsOzoneSignatureFindCorrelation from './types/tools/ozone/signature/findCorrelation.js'\nimport * as ToolsOzoneSignatureFindRelatedAccounts from './types/tools/ozone/signature/findRelatedAccounts.js'\nimport * as ToolsOzoneSignatureSearchAccounts from './types/tools/ozone/signature/searchAccounts.js'\nimport * as ToolsOzoneTeamAddMember from './types/tools/ozone/team/addMember.js'\nimport * as ToolsOzoneTeamDeleteMember from './types/tools/ozone/team/deleteMember.js'\nimport * as ToolsOzoneTeamListMembers from './types/tools/ozone/team/listMembers.js'\nimport * as ToolsOzoneTeamUpdateMember from './types/tools/ozone/team/updateMember.js'\nimport * as ToolsOzoneVerificationGrantVerifications from './types/tools/ozone/verification/grantVerifications.js'\nimport * as ToolsOzoneVerificationListVerifications from './types/tools/ozone/verification/listVerifications.js'\nimport * as ToolsOzoneVerificationRevokeVerifications from './types/tools/ozone/verification/revokeVerifications.js'\n\nexport const APP_BSKY_ACTOR = {\n  StatusLive: 'app.bsky.actor.status#live',\n}\nexport const APP_BSKY_FEED = {\n  DefsRequestLess: 'app.bsky.feed.defs#requestLess',\n  DefsRequestMore: 'app.bsky.feed.defs#requestMore',\n  DefsClickthroughItem: 'app.bsky.feed.defs#clickthroughItem',\n  DefsClickthroughAuthor: 'app.bsky.feed.defs#clickthroughAuthor',\n  DefsClickthroughReposter: 'app.bsky.feed.defs#clickthroughReposter',\n  DefsClickthroughEmbed: 'app.bsky.feed.defs#clickthroughEmbed',\n  DefsContentModeUnspecified: 'app.bsky.feed.defs#contentModeUnspecified',\n  DefsContentModeVideo: 'app.bsky.feed.defs#contentModeVideo',\n  DefsInteractionSeen: 'app.bsky.feed.defs#interactionSeen',\n  DefsInteractionLike: 'app.bsky.feed.defs#interactionLike',\n  DefsInteractionRepost: 'app.bsky.feed.defs#interactionRepost',\n  DefsInteractionReply: 'app.bsky.feed.defs#interactionReply',\n  DefsInteractionQuote: 'app.bsky.feed.defs#interactionQuote',\n  DefsInteractionShare: 'app.bsky.feed.defs#interactionShare',\n}\nexport const APP_BSKY_GRAPH = {\n  DefsModlist: 'app.bsky.graph.defs#modlist',\n  DefsCuratelist: 'app.bsky.graph.defs#curatelist',\n  DefsReferencelist: 'app.bsky.graph.defs#referencelist',\n}\nexport const COM_ATPROTO_MODERATION = {\n  DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam',\n  DefsReasonViolation: 'com.atproto.moderation.defs#reasonViolation',\n  DefsReasonMisleading: 'com.atproto.moderation.defs#reasonMisleading',\n  DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',\n  DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',\n  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',\n  DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',\n}\nexport const TOOLS_OZONE_MODERATION = {\n  DefsReviewOpen: 'tools.ozone.moderation.defs#reviewOpen',\n  DefsReviewEscalated: 'tools.ozone.moderation.defs#reviewEscalated',\n  DefsReviewClosed: 'tools.ozone.moderation.defs#reviewClosed',\n  DefsReviewNone: 'tools.ozone.moderation.defs#reviewNone',\n  DefsTimelineEventPlcCreate:\n    'tools.ozone.moderation.defs#timelineEventPlcCreate',\n  DefsTimelineEventPlcOperation:\n    'tools.ozone.moderation.defs#timelineEventPlcOperation',\n  DefsTimelineEventPlcTombstone:\n    'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n}\nexport const TOOLS_OZONE_REPORT = {\n  DefsReasonAppeal: 'tools.ozone.report.defs#reasonAppeal',\n  DefsReasonOther: 'tools.ozone.report.defs#reasonOther',\n  DefsReasonViolenceAnimal: 'tools.ozone.report.defs#reasonViolenceAnimal',\n  DefsReasonViolenceThreats: 'tools.ozone.report.defs#reasonViolenceThreats',\n  DefsReasonViolenceGraphicContent:\n    'tools.ozone.report.defs#reasonViolenceGraphicContent',\n  DefsReasonViolenceGlorification:\n    'tools.ozone.report.defs#reasonViolenceGlorification',\n  DefsReasonViolenceExtremistContent:\n    'tools.ozone.report.defs#reasonViolenceExtremistContent',\n  DefsReasonViolenceTrafficking:\n    'tools.ozone.report.defs#reasonViolenceTrafficking',\n  DefsReasonViolenceOther: 'tools.ozone.report.defs#reasonViolenceOther',\n  DefsReasonSexualAbuseContent:\n    'tools.ozone.report.defs#reasonSexualAbuseContent',\n  DefsReasonSexualNCII: 'tools.ozone.report.defs#reasonSexualNCII',\n  DefsReasonSexualDeepfake: 'tools.ozone.report.defs#reasonSexualDeepfake',\n  DefsReasonSexualAnimal: 'tools.ozone.report.defs#reasonSexualAnimal',\n  DefsReasonSexualUnlabeled: 'tools.ozone.report.defs#reasonSexualUnlabeled',\n  DefsReasonSexualOther: 'tools.ozone.report.defs#reasonSexualOther',\n  DefsReasonChildSafetyCSAM: 'tools.ozone.report.defs#reasonChildSafetyCSAM',\n  DefsReasonChildSafetyGroom: 'tools.ozone.report.defs#reasonChildSafetyGroom',\n  DefsReasonChildSafetyPrivacy:\n    'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n  DefsReasonChildSafetyHarassment:\n    'tools.ozone.report.defs#reasonChildSafetyHarassment',\n  DefsReasonChildSafetyOther: 'tools.ozone.report.defs#reasonChildSafetyOther',\n  DefsReasonHarassmentTroll: 'tools.ozone.report.defs#reasonHarassmentTroll',\n  DefsReasonHarassmentTargeted:\n    'tools.ozone.report.defs#reasonHarassmentTargeted',\n  DefsReasonHarassmentHateSpeech:\n    'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n  DefsReasonHarassmentDoxxing:\n    'tools.ozone.report.defs#reasonHarassmentDoxxing',\n  DefsReasonHarassmentOther: 'tools.ozone.report.defs#reasonHarassmentOther',\n  DefsReasonMisleadingBot: 'tools.ozone.report.defs#reasonMisleadingBot',\n  DefsReasonMisleadingImpersonation:\n    'tools.ozone.report.defs#reasonMisleadingImpersonation',\n  DefsReasonMisleadingSpam: 'tools.ozone.report.defs#reasonMisleadingSpam',\n  DefsReasonMisleadingScam: 'tools.ozone.report.defs#reasonMisleadingScam',\n  DefsReasonMisleadingElections:\n    'tools.ozone.report.defs#reasonMisleadingElections',\n  DefsReasonMisleadingOther: 'tools.ozone.report.defs#reasonMisleadingOther',\n  DefsReasonRuleSiteSecurity: 'tools.ozone.report.defs#reasonRuleSiteSecurity',\n  DefsReasonRuleProhibitedSales:\n    'tools.ozone.report.defs#reasonRuleProhibitedSales',\n  DefsReasonRuleBanEvasion: 'tools.ozone.report.defs#reasonRuleBanEvasion',\n  DefsReasonRuleOther: 'tools.ozone.report.defs#reasonRuleOther',\n  DefsReasonSelfHarmContent: 'tools.ozone.report.defs#reasonSelfHarmContent',\n  DefsReasonSelfHarmED: 'tools.ozone.report.defs#reasonSelfHarmED',\n  DefsReasonSelfHarmStunts: 'tools.ozone.report.defs#reasonSelfHarmStunts',\n  DefsReasonSelfHarmSubstances:\n    'tools.ozone.report.defs#reasonSelfHarmSubstances',\n  DefsReasonSelfHarmOther: 'tools.ozone.report.defs#reasonSelfHarmOther',\n}\nexport const TOOLS_OZONE_TEAM = {\n  DefsRoleAdmin: 'tools.ozone.team.defs#roleAdmin',\n  DefsRoleModerator: 'tools.ozone.team.defs#roleModerator',\n  DefsRoleTriage: 'tools.ozone.team.defs#roleTriage',\n  DefsRoleVerifier: 'tools.ozone.team.defs#roleVerifier',\n}\n\nexport function createServer(options?: XrpcOptions): Server {\n  return new Server(options)\n}\n\nexport class Server {\n  xrpc: XrpcServer\n  app: AppNS\n  chat: ChatNS\n  com: ComNS\n  tools: ToolsNS\n\n  constructor(options?: XrpcOptions) {\n    this.xrpc = createXrpcServer(schemas, options)\n    this.app = new AppNS(this)\n    this.chat = new ChatNS(this)\n    this.com = new ComNS(this)\n    this.tools = new ToolsNS(this)\n  }\n}\n\nexport class AppNS {\n  _server: Server\n  bsky: AppBskyNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.bsky = new AppBskyNS(server)\n  }\n}\n\nexport class AppBskyNS {\n  _server: Server\n  actor: AppBskyActorNS\n  ageassurance: AppBskyAgeassuranceNS\n  bookmark: AppBskyBookmarkNS\n  contact: AppBskyContactNS\n  draft: AppBskyDraftNS\n  embed: AppBskyEmbedNS\n  feed: AppBskyFeedNS\n  graph: AppBskyGraphNS\n  labeler: AppBskyLabelerNS\n  notification: AppBskyNotificationNS\n  richtext: AppBskyRichtextNS\n  unspecced: AppBskyUnspeccedNS\n  video: AppBskyVideoNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.actor = new AppBskyActorNS(server)\n    this.ageassurance = new AppBskyAgeassuranceNS(server)\n    this.bookmark = new AppBskyBookmarkNS(server)\n    this.contact = new AppBskyContactNS(server)\n    this.draft = new AppBskyDraftNS(server)\n    this.embed = new AppBskyEmbedNS(server)\n    this.feed = new AppBskyFeedNS(server)\n    this.graph = new AppBskyGraphNS(server)\n    this.labeler = new AppBskyLabelerNS(server)\n    this.notification = new AppBskyNotificationNS(server)\n    this.richtext = new AppBskyRichtextNS(server)\n    this.unspecced = new AppBskyUnspeccedNS(server)\n    this.video = new AppBskyVideoNS(server)\n  }\n}\n\nexport class AppBskyActorNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetPreferences.QueryParams,\n      AppBskyActorGetPreferences.HandlerInput,\n      AppBskyActorGetPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getProfile<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetProfile.QueryParams,\n      AppBskyActorGetProfile.HandlerInput,\n      AppBskyActorGetProfile.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getProfile' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getProfiles<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetProfiles.QueryParams,\n      AppBskyActorGetProfiles.HandlerInput,\n      AppBskyActorGetProfiles.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getProfiles' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorGetSuggestions.QueryParams,\n      AppBskyActorGetSuggestions.HandlerInput,\n      AppBskyActorGetSuggestions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.getSuggestions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorPutPreferences.QueryParams,\n      AppBskyActorPutPreferences.HandlerInput,\n      AppBskyActorPutPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.putPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActors<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorSearchActors.QueryParams,\n      AppBskyActorSearchActors.HandlerInput,\n      AppBskyActorSearchActors.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.searchActors' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActorsTypeahead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyActorSearchActorsTypeahead.QueryParams,\n      AppBskyActorSearchActorsTypeahead.HandlerInput,\n      AppBskyActorSearchActorsTypeahead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.actor.searchActorsTypeahead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyAgeassuranceNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  begin<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceBegin.QueryParams,\n      AppBskyAgeassuranceBegin.HandlerInput,\n      AppBskyAgeassuranceBegin.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.begin' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceGetConfig.QueryParams,\n      AppBskyAgeassuranceGetConfig.HandlerInput,\n      AppBskyAgeassuranceGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getState<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyAgeassuranceGetState.QueryParams,\n      AppBskyAgeassuranceGetState.HandlerInput,\n      AppBskyAgeassuranceGetState.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.ageassurance.getState' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyBookmarkNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createBookmark<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkCreateBookmark.QueryParams,\n      AppBskyBookmarkCreateBookmark.HandlerInput,\n      AppBskyBookmarkCreateBookmark.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.createBookmark' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteBookmark<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkDeleteBookmark.QueryParams,\n      AppBskyBookmarkDeleteBookmark.HandlerInput,\n      AppBskyBookmarkDeleteBookmark.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.deleteBookmark' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBookmarks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyBookmarkGetBookmarks.QueryParams,\n      AppBskyBookmarkGetBookmarks.HandlerInput,\n      AppBskyBookmarkGetBookmarks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.bookmark.getBookmarks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyContactNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  dismissMatch<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactDismissMatch.QueryParams,\n      AppBskyContactDismissMatch.HandlerInput,\n      AppBskyContactDismissMatch.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.dismissMatch' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMatches<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactGetMatches.QueryParams,\n      AppBskyContactGetMatches.HandlerInput,\n      AppBskyContactGetMatches.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.getMatches' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSyncStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactGetSyncStatus.QueryParams,\n      AppBskyContactGetSyncStatus.HandlerInput,\n      AppBskyContactGetSyncStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.getSyncStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  importContacts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactImportContacts.QueryParams,\n      AppBskyContactImportContacts.HandlerInput,\n      AppBskyContactImportContacts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.importContacts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeData<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactRemoveData.QueryParams,\n      AppBskyContactRemoveData.HandlerInput,\n      AppBskyContactRemoveData.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.removeData' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendNotification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactSendNotification.QueryParams,\n      AppBskyContactSendNotification.HandlerInput,\n      AppBskyContactSendNotification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.sendNotification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  startPhoneVerification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactStartPhoneVerification.QueryParams,\n      AppBskyContactStartPhoneVerification.HandlerInput,\n      AppBskyContactStartPhoneVerification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.startPhoneVerification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  verifyPhone<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyContactVerifyPhone.QueryParams,\n      AppBskyContactVerifyPhone.HandlerInput,\n      AppBskyContactVerifyPhone.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.contact.verifyPhone' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyDraftNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftCreateDraft.QueryParams,\n      AppBskyDraftCreateDraft.HandlerInput,\n      AppBskyDraftCreateDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.createDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftDeleteDraft.QueryParams,\n      AppBskyDraftDeleteDraft.HandlerInput,\n      AppBskyDraftDeleteDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.deleteDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getDrafts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftGetDrafts.QueryParams,\n      AppBskyDraftGetDrafts.HandlerInput,\n      AppBskyDraftGetDrafts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.getDrafts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateDraft<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyDraftUpdateDraft.QueryParams,\n      AppBskyDraftUpdateDraft.HandlerInput,\n      AppBskyDraftUpdateDraft.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.draft.updateDraft' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyEmbedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n\nexport class AppBskyFeedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  describeFeedGenerator<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedDescribeFeedGenerator.QueryParams,\n      AppBskyFeedDescribeFeedGenerator.HandlerInput,\n      AppBskyFeedDescribeFeedGenerator.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getActorFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetActorFeeds.QueryParams,\n      AppBskyFeedGetActorFeeds.HandlerInput,\n      AppBskyFeedGetActorFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getActorFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getActorLikes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetActorLikes.QueryParams,\n      AppBskyFeedGetActorLikes.HandlerInput,\n      AppBskyFeedGetActorLikes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAuthorFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetAuthorFeed.QueryParams,\n      AppBskyFeedGetAuthorFeed.HandlerInput,\n      AppBskyFeedGetAuthorFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeed.QueryParams,\n      AppBskyFeedGetFeed.HandlerInput,\n      AppBskyFeedGetFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedGenerator<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedGenerator.QueryParams,\n      AppBskyFeedGetFeedGenerator.HandlerInput,\n      AppBskyFeedGetFeedGenerator.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedGenerator' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedGenerators<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedGenerators.QueryParams,\n      AppBskyFeedGetFeedGenerators.HandlerInput,\n      AppBskyFeedGetFeedGenerators.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedGenerators' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFeedSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetFeedSkeleton.QueryParams,\n      AppBskyFeedGetFeedSkeleton.HandlerInput,\n      AppBskyFeedGetFeedSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getFeedSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLikes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetLikes.QueryParams,\n      AppBskyFeedGetLikes.HandlerInput,\n      AppBskyFeedGetLikes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getLikes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListFeed<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetListFeed.QueryParams,\n      AppBskyFeedGetListFeed.HandlerInput,\n      AppBskyFeedGetListFeed.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getListFeed' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetPostThread.QueryParams,\n      AppBskyFeedGetPostThread.HandlerInput,\n      AppBskyFeedGetPostThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetPosts.QueryParams,\n      AppBskyFeedGetPosts.HandlerInput,\n      AppBskyFeedGetPosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getPosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getQuotes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetQuotes.QueryParams,\n      AppBskyFeedGetQuotes.HandlerInput,\n      AppBskyFeedGetQuotes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getQuotes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepostedBy<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetRepostedBy.QueryParams,\n      AppBskyFeedGetRepostedBy.HandlerInput,\n      AppBskyFeedGetRepostedBy.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getRepostedBy' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetSuggestedFeeds.QueryParams,\n      AppBskyFeedGetSuggestedFeeds.HandlerInput,\n      AppBskyFeedGetSuggestedFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getSuggestedFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTimeline<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedGetTimeline.QueryParams,\n      AppBskyFeedGetTimeline.HandlerInput,\n      AppBskyFeedGetTimeline.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchPosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedSearchPosts.QueryParams,\n      AppBskyFeedSearchPosts.HandlerInput,\n      AppBskyFeedSearchPosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.searchPosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendInteractions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyFeedSendInteractions.QueryParams,\n      AppBskyFeedSendInteractions.HandlerInput,\n      AppBskyFeedSendInteractions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.feed.sendInteractions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyGraphNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getActorStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetActorStarterPacks.QueryParams,\n      AppBskyGraphGetActorStarterPacks.HandlerInput,\n      AppBskyGraphGetActorStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getActorStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetBlocks.QueryParams,\n      AppBskyGraphGetBlocks.HandlerInput,\n      AppBskyGraphGetBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFollowers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetFollowers.QueryParams,\n      AppBskyGraphGetFollowers.HandlerInput,\n      AppBskyGraphGetFollowers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getFollowers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getFollows<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetFollows.QueryParams,\n      AppBskyGraphGetFollows.HandlerInput,\n      AppBskyGraphGetFollows.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getFollows' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getKnownFollowers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetKnownFollowers.QueryParams,\n      AppBskyGraphGetKnownFollowers.HandlerInput,\n      AppBskyGraphGetKnownFollowers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getKnownFollowers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetList.QueryParams,\n      AppBskyGraphGetList.HandlerInput,\n      AppBskyGraphGetList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListBlocks.QueryParams,\n      AppBskyGraphGetListBlocks.HandlerInput,\n      AppBskyGraphGetListBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListMutes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListMutes.QueryParams,\n      AppBskyGraphGetListMutes.HandlerInput,\n      AppBskyGraphGetListMutes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListMutes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLists<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetLists.QueryParams,\n      AppBskyGraphGetLists.HandlerInput,\n      AppBskyGraphGetLists.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getLists' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getListsWithMembership<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetListsWithMembership.QueryParams,\n      AppBskyGraphGetListsWithMembership.HandlerInput,\n      AppBskyGraphGetListsWithMembership.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getListsWithMembership' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMutes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetMutes.QueryParams,\n      AppBskyGraphGetMutes.HandlerInput,\n      AppBskyGraphGetMutes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getMutes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRelationships<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetRelationships.QueryParams,\n      AppBskyGraphGetRelationships.HandlerInput,\n      AppBskyGraphGetRelationships.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getRelationships' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPack<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPack.QueryParams,\n      AppBskyGraphGetStarterPack.HandlerInput,\n      AppBskyGraphGetStarterPack.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPack' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPacks.QueryParams,\n      AppBskyGraphGetStarterPacks.HandlerInput,\n      AppBskyGraphGetStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getStarterPacksWithMembership<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetStarterPacksWithMembership.QueryParams,\n      AppBskyGraphGetStarterPacksWithMembership.HandlerInput,\n      AppBskyGraphGetStarterPacksWithMembership.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getStarterPacksWithMembership' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFollowsByActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphGetSuggestedFollowsByActor.QueryParams,\n      AppBskyGraphGetSuggestedFollowsByActor.HandlerInput,\n      AppBskyGraphGetSuggestedFollowsByActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.getSuggestedFollowsByActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteActor.QueryParams,\n      AppBskyGraphMuteActor.HandlerInput,\n      AppBskyGraphMuteActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteActorList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteActorList.QueryParams,\n      AppBskyGraphMuteActorList.HandlerInput,\n      AppBskyGraphMuteActorList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteActorList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphMuteThread.QueryParams,\n      AppBskyGraphMuteThread.HandlerInput,\n      AppBskyGraphMuteThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.muteThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphSearchStarterPacks.QueryParams,\n      AppBskyGraphSearchStarterPacks.HandlerInput,\n      AppBskyGraphSearchStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.searchStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteActor<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteActor.QueryParams,\n      AppBskyGraphUnmuteActor.HandlerInput,\n      AppBskyGraphUnmuteActor.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteActor' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteActorList<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteActorList.QueryParams,\n      AppBskyGraphUnmuteActorList.HandlerInput,\n      AppBskyGraphUnmuteActorList.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteThread<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyGraphUnmuteThread.QueryParams,\n      AppBskyGraphUnmuteThread.HandlerInput,\n      AppBskyGraphUnmuteThread.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.graph.unmuteThread' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyLabelerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getServices<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyLabelerGetServices.QueryParams,\n      AppBskyLabelerGetServices.HandlerInput,\n      AppBskyLabelerGetServices.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.labeler.getServices' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyNotificationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationGetPreferences.QueryParams,\n      AppBskyNotificationGetPreferences.HandlerInput,\n      AppBskyNotificationGetPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.getPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getUnreadCount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationGetUnreadCount.QueryParams,\n      AppBskyNotificationGetUnreadCount.HandlerInput,\n      AppBskyNotificationGetUnreadCount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.getUnreadCount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listActivitySubscriptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationListActivitySubscriptions.QueryParams,\n      AppBskyNotificationListActivitySubscriptions.HandlerInput,\n      AppBskyNotificationListActivitySubscriptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.listActivitySubscriptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listNotifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationListNotifications.QueryParams,\n      AppBskyNotificationListNotifications.HandlerInput,\n      AppBskyNotificationListNotifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.listNotifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putActivitySubscription<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutActivitySubscription.QueryParams,\n      AppBskyNotificationPutActivitySubscription.HandlerInput,\n      AppBskyNotificationPutActivitySubscription.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putActivitySubscription' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferences<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutPreferences.QueryParams,\n      AppBskyNotificationPutPreferences.HandlerInput,\n      AppBskyNotificationPutPreferences.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putPreferences' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putPreferencesV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationPutPreferencesV2.QueryParams,\n      AppBskyNotificationPutPreferencesV2.HandlerInput,\n      AppBskyNotificationPutPreferencesV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.putPreferencesV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  registerPush<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationRegisterPush.QueryParams,\n      AppBskyNotificationRegisterPush.HandlerInput,\n      AppBskyNotificationRegisterPush.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.registerPush' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unregisterPush<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationUnregisterPush.QueryParams,\n      AppBskyNotificationUnregisterPush.HandlerInput,\n      AppBskyNotificationUnregisterPush.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.unregisterPush' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateSeen<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyNotificationUpdateSeen.QueryParams,\n      AppBskyNotificationUpdateSeen.HandlerInput,\n      AppBskyNotificationUpdateSeen.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.notification.updateSeen' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyRichtextNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n}\n\nexport class AppBskyUnspeccedNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getAgeAssuranceState<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetAgeAssuranceState.QueryParams,\n      AppBskyUnspeccedGetAgeAssuranceState.HandlerInput,\n      AppBskyUnspeccedGetAgeAssuranceState.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getAgeAssuranceState' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetConfig.QueryParams,\n      AppBskyUnspeccedGetConfig.HandlerInput,\n      AppBskyUnspeccedGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getOnboardingSuggestedUsersSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.QueryParams,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.HandlerInput,\n      AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPopularFeedGenerators<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPopularFeedGenerators.QueryParams,\n      AppBskyUnspeccedGetPopularFeedGenerators.HandlerInput,\n      AppBskyUnspeccedGetPopularFeedGenerators.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThreadOtherV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPostThreadOtherV2.QueryParams,\n      AppBskyUnspeccedGetPostThreadOtherV2.HandlerInput,\n      AppBskyUnspeccedGetPostThreadOtherV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPostThreadOtherV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getPostThreadV2<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetPostThreadV2.QueryParams,\n      AppBskyUnspeccedGetPostThreadV2.HandlerInput,\n      AppBskyUnspeccedGetPostThreadV2.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getPostThreadV2' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeeds<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedFeeds.QueryParams,\n      AppBskyUnspeccedGetSuggestedFeeds.HandlerInput,\n      AppBskyUnspeccedGetSuggestedFeeds.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedFeeds' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedFeedsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedFeedsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedFeedsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedOnboardingUsers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.QueryParams,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.HandlerInput,\n      AppBskyUnspeccedGetSuggestedOnboardingUsers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedOnboardingUsers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedStarterPacks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedStarterPacks.QueryParams,\n      AppBskyUnspeccedGetSuggestedStarterPacks.HandlerInput,\n      AppBskyUnspeccedGetSuggestedStarterPacks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedStarterPacks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedUsers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedUsers.QueryParams,\n      AppBskyUnspeccedGetSuggestedUsers.HandlerInput,\n      AppBskyUnspeccedGetSuggestedUsers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedUsers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestedUsersSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestedUsersSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestedUsersSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSuggestionsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetSuggestionsSkeleton.QueryParams,\n      AppBskyUnspeccedGetSuggestionsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetSuggestionsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getSuggestionsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTaggedSuggestions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTaggedSuggestions.QueryParams,\n      AppBskyUnspeccedGetTaggedSuggestions.HandlerInput,\n      AppBskyUnspeccedGetTaggedSuggestions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrendingTopics<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrendingTopics.QueryParams,\n      AppBskyUnspeccedGetTrendingTopics.HandlerInput,\n      AppBskyUnspeccedGetTrendingTopics.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrendingTopics' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrends<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrends.QueryParams,\n      AppBskyUnspeccedGetTrends.HandlerInput,\n      AppBskyUnspeccedGetTrends.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrends' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getTrendsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedGetTrendsSkeleton.QueryParams,\n      AppBskyUnspeccedGetTrendsSkeleton.HandlerInput,\n      AppBskyUnspeccedGetTrendsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.getTrendsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  initAgeAssurance<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedInitAgeAssurance.QueryParams,\n      AppBskyUnspeccedInitAgeAssurance.HandlerInput,\n      AppBskyUnspeccedInitAgeAssurance.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.initAgeAssurance' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchActorsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchActorsSkeleton.QueryParams,\n      AppBskyUnspeccedSearchActorsSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchActorsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchActorsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchPostsSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchPostsSkeleton.QueryParams,\n      AppBskyUnspeccedSearchPostsSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchPostsSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchPostsSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchStarterPacksSkeleton<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.QueryParams,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerInput,\n      AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.unspecced.searchStarterPacksSkeleton' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class AppBskyVideoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getJobStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoGetJobStatus.QueryParams,\n      AppBskyVideoGetJobStatus.HandlerInput,\n      AppBskyVideoGetJobStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.getJobStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getUploadLimits<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoGetUploadLimits.QueryParams,\n      AppBskyVideoGetUploadLimits.HandlerInput,\n      AppBskyVideoGetUploadLimits.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.getUploadLimits' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  uploadVideo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      AppBskyVideoUploadVideo.QueryParams,\n      AppBskyVideoUploadVideo.HandlerInput,\n      AppBskyVideoUploadVideo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'app.bsky.video.uploadVideo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatNS {\n  _server: Server\n  bsky: ChatBskyNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.bsky = new ChatBskyNS(server)\n  }\n}\n\nexport class ChatBskyNS {\n  _server: Server\n  actor: ChatBskyActorNS\n  convo: ChatBskyConvoNS\n  moderation: ChatBskyModerationNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.actor = new ChatBskyActorNS(server)\n    this.convo = new ChatBskyConvoNS(server)\n    this.moderation = new ChatBskyModerationNS(server)\n  }\n}\n\nexport class ChatBskyActorNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyActorDeleteAccount.QueryParams,\n      ChatBskyActorDeleteAccount.HandlerInput,\n      ChatBskyActorDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.actor.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  exportAccountData<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyActorExportAccountData.QueryParams,\n      ChatBskyActorExportAccountData.HandlerInput,\n      ChatBskyActorExportAccountData.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.actor.exportAccountData' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatBskyConvoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  acceptConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoAcceptConvo.QueryParams,\n      ChatBskyConvoAcceptConvo.HandlerInput,\n      ChatBskyConvoAcceptConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.acceptConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  addReaction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoAddReaction.QueryParams,\n      ChatBskyConvoAddReaction.HandlerInput,\n      ChatBskyConvoAddReaction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.addReaction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteMessageForSelf<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoDeleteMessageForSelf.QueryParams,\n      ChatBskyConvoDeleteMessageForSelf.HandlerInput,\n      ChatBskyConvoDeleteMessageForSelf.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.deleteMessageForSelf' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvo.QueryParams,\n      ChatBskyConvoGetConvo.HandlerInput,\n      ChatBskyConvoGetConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvoAvailability<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvoAvailability.QueryParams,\n      ChatBskyConvoGetConvoAvailability.HandlerInput,\n      ChatBskyConvoGetConvoAvailability.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvoAvailability' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getConvoForMembers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetConvoForMembers.QueryParams,\n      ChatBskyConvoGetConvoForMembers.HandlerInput,\n      ChatBskyConvoGetConvoForMembers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getConvoForMembers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLog<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetLog.QueryParams,\n      ChatBskyConvoGetLog.HandlerInput,\n      ChatBskyConvoGetLog.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getLog' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMessages<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoGetMessages.QueryParams,\n      ChatBskyConvoGetMessages.HandlerInput,\n      ChatBskyConvoGetMessages.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.getMessages' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  leaveConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoLeaveConvo.QueryParams,\n      ChatBskyConvoLeaveConvo.HandlerInput,\n      ChatBskyConvoLeaveConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.leaveConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listConvos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoListConvos.QueryParams,\n      ChatBskyConvoListConvos.HandlerInput,\n      ChatBskyConvoListConvos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.listConvos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  muteConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoMuteConvo.QueryParams,\n      ChatBskyConvoMuteConvo.HandlerInput,\n      ChatBskyConvoMuteConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.muteConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeReaction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoRemoveReaction.QueryParams,\n      ChatBskyConvoRemoveReaction.HandlerInput,\n      ChatBskyConvoRemoveReaction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.removeReaction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendMessage<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoSendMessage.QueryParams,\n      ChatBskyConvoSendMessage.HandlerInput,\n      ChatBskyConvoSendMessage.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.sendMessage' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendMessageBatch<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoSendMessageBatch.QueryParams,\n      ChatBskyConvoSendMessageBatch.HandlerInput,\n      ChatBskyConvoSendMessageBatch.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.sendMessageBatch' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  unmuteConvo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUnmuteConvo.QueryParams,\n      ChatBskyConvoUnmuteConvo.HandlerInput,\n      ChatBskyConvoUnmuteConvo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.unmuteConvo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAllRead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUpdateAllRead.QueryParams,\n      ChatBskyConvoUpdateAllRead.HandlerInput,\n      ChatBskyConvoUpdateAllRead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.updateAllRead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateRead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyConvoUpdateRead.QueryParams,\n      ChatBskyConvoUpdateRead.HandlerInput,\n      ChatBskyConvoUpdateRead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.convo.updateRead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ChatBskyModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getActorMetadata<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationGetActorMetadata.QueryParams,\n      ChatBskyModerationGetActorMetadata.HandlerInput,\n      ChatBskyModerationGetActorMetadata.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.getActorMetadata' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getMessageContext<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationGetMessageContext.QueryParams,\n      ChatBskyModerationGetMessageContext.HandlerInput,\n      ChatBskyModerationGetMessageContext.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.getMessageContext' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateActorAccess<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ChatBskyModerationUpdateActorAccess.QueryParams,\n      ChatBskyModerationUpdateActorAccess.HandlerInput,\n      ChatBskyModerationUpdateActorAccess.HandlerOutput\n    >,\n  ) {\n    const nsid = 'chat.bsky.moderation.updateActorAccess' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComNS {\n  _server: Server\n  atproto: ComAtprotoNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.atproto = new ComAtprotoNS(server)\n  }\n}\n\nexport class ComAtprotoNS {\n  _server: Server\n  admin: ComAtprotoAdminNS\n  identity: ComAtprotoIdentityNS\n  label: ComAtprotoLabelNS\n  lexicon: ComAtprotoLexiconNS\n  moderation: ComAtprotoModerationNS\n  repo: ComAtprotoRepoNS\n  server: ComAtprotoServerNS\n  sync: ComAtprotoSyncNS\n  temp: ComAtprotoTempNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.admin = new ComAtprotoAdminNS(server)\n    this.identity = new ComAtprotoIdentityNS(server)\n    this.label = new ComAtprotoLabelNS(server)\n    this.lexicon = new ComAtprotoLexiconNS(server)\n    this.moderation = new ComAtprotoModerationNS(server)\n    this.repo = new ComAtprotoRepoNS(server)\n    this.server = new ComAtprotoServerNS(server)\n    this.sync = new ComAtprotoSyncNS(server)\n    this.temp = new ComAtprotoTempNS(server)\n  }\n}\n\nexport class ComAtprotoAdminNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDeleteAccount.QueryParams,\n      ComAtprotoAdminDeleteAccount.HandlerInput,\n      ComAtprotoAdminDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  disableAccountInvites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDisableAccountInvites.QueryParams,\n      ComAtprotoAdminDisableAccountInvites.HandlerInput,\n      ComAtprotoAdminDisableAccountInvites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  disableInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminDisableInviteCodes.QueryParams,\n      ComAtprotoAdminDisableInviteCodes.HandlerInput,\n      ComAtprotoAdminDisableInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.disableInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  enableAccountInvites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminEnableAccountInvites.QueryParams,\n      ComAtprotoAdminEnableAccountInvites.HandlerInput,\n      ComAtprotoAdminEnableAccountInvites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInfo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetAccountInfo.QueryParams,\n      ComAtprotoAdminGetAccountInfo.HandlerInput,\n      ComAtprotoAdminGetAccountInfo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getAccountInfo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInfos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetAccountInfos.QueryParams,\n      ComAtprotoAdminGetAccountInfos.HandlerInput,\n      ComAtprotoAdminGetAccountInfos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getAccountInfos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetInviteCodes.QueryParams,\n      ComAtprotoAdminGetInviteCodes.HandlerInput,\n      ComAtprotoAdminGetInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSubjectStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminGetSubjectStatus.QueryParams,\n      ComAtprotoAdminGetSubjectStatus.HandlerInput,\n      ComAtprotoAdminGetSubjectStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.getSubjectStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminSearchAccounts.QueryParams,\n      ComAtprotoAdminSearchAccounts.HandlerInput,\n      ComAtprotoAdminSearchAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.searchAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  sendEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminSendEmail.QueryParams,\n      ComAtprotoAdminSendEmail.HandlerInput,\n      ComAtprotoAdminSendEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountEmail.QueryParams,\n      ComAtprotoAdminUpdateAccountEmail.HandlerInput,\n      ComAtprotoAdminUpdateAccountEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountHandle.QueryParams,\n      ComAtprotoAdminUpdateAccountHandle.HandlerInput,\n      ComAtprotoAdminUpdateAccountHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountPassword.QueryParams,\n      ComAtprotoAdminUpdateAccountPassword.HandlerInput,\n      ComAtprotoAdminUpdateAccountPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateAccountSigningKey<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateAccountSigningKey.QueryParams,\n      ComAtprotoAdminUpdateAccountSigningKey.HandlerInput,\n      ComAtprotoAdminUpdateAccountSigningKey.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateAccountSigningKey' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateSubjectStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoAdminUpdateSubjectStatus.QueryParams,\n      ComAtprotoAdminUpdateSubjectStatus.HandlerInput,\n      ComAtprotoAdminUpdateSubjectStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.admin.updateSubjectStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoIdentityNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getRecommendedDidCredentials<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityGetRecommendedDidCredentials.QueryParams,\n      ComAtprotoIdentityGetRecommendedDidCredentials.HandlerInput,\n      ComAtprotoIdentityGetRecommendedDidCredentials.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.getRecommendedDidCredentials' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  refreshIdentity<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityRefreshIdentity.QueryParams,\n      ComAtprotoIdentityRefreshIdentity.HandlerInput,\n      ComAtprotoIdentityRefreshIdentity.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.refreshIdentity' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPlcOperationSignature<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityRequestPlcOperationSignature.QueryParams,\n      ComAtprotoIdentityRequestPlcOperationSignature.HandlerInput,\n      ComAtprotoIdentityRequestPlcOperationSignature.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.requestPlcOperationSignature' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveDid<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveDid.QueryParams,\n      ComAtprotoIdentityResolveDid.HandlerInput,\n      ComAtprotoIdentityResolveDid.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveDid' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveHandle.QueryParams,\n      ComAtprotoIdentityResolveHandle.HandlerInput,\n      ComAtprotoIdentityResolveHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resolveIdentity<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityResolveIdentity.QueryParams,\n      ComAtprotoIdentityResolveIdentity.HandlerInput,\n      ComAtprotoIdentityResolveIdentity.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.resolveIdentity' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  signPlcOperation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentitySignPlcOperation.QueryParams,\n      ComAtprotoIdentitySignPlcOperation.HandlerInput,\n      ComAtprotoIdentitySignPlcOperation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.signPlcOperation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  submitPlcOperation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentitySubmitPlcOperation.QueryParams,\n      ComAtprotoIdentitySubmitPlcOperation.HandlerInput,\n      ComAtprotoIdentitySubmitPlcOperation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.submitPlcOperation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoIdentityUpdateHandle.QueryParams,\n      ComAtprotoIdentityUpdateHandle.HandlerInput,\n      ComAtprotoIdentityUpdateHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.identity.updateHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoLabelNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  queryLabels<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoLabelQueryLabels.QueryParams,\n      ComAtprotoLabelQueryLabels.HandlerInput,\n      ComAtprotoLabelQueryLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.label.queryLabels' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  subscribeLabels<A extends Auth = void>(\n    cfg: StreamConfigOrHandler<\n      A,\n      ComAtprotoLabelSubscribeLabels.QueryParams,\n      ComAtprotoLabelSubscribeLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore\n    return this._server.xrpc.streamMethod(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoLexiconNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  resolveLexicon<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoLexiconResolveLexicon.QueryParams,\n      ComAtprotoLexiconResolveLexicon.HandlerInput,\n      ComAtprotoLexiconResolveLexicon.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.lexicon.resolveLexicon' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createReport<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoModerationCreateReport.QueryParams,\n      ComAtprotoModerationCreateReport.HandlerInput,\n      ComAtprotoModerationCreateReport.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.moderation.createReport' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoRepoNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  applyWrites<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoApplyWrites.QueryParams,\n      ComAtprotoRepoApplyWrites.HandlerInput,\n      ComAtprotoRepoApplyWrites.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.applyWrites' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoCreateRecord.QueryParams,\n      ComAtprotoRepoCreateRecord.HandlerInput,\n      ComAtprotoRepoCreateRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.createRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoDeleteRecord.QueryParams,\n      ComAtprotoRepoDeleteRecord.HandlerInput,\n      ComAtprotoRepoDeleteRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.deleteRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  describeRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoDescribeRepo.QueryParams,\n      ComAtprotoRepoDescribeRepo.HandlerInput,\n      ComAtprotoRepoDescribeRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.describeRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoGetRecord.QueryParams,\n      ComAtprotoRepoGetRecord.HandlerInput,\n      ComAtprotoRepoGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  importRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoImportRepo.QueryParams,\n      ComAtprotoRepoImportRepo.HandlerInput,\n      ComAtprotoRepoImportRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.importRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listMissingBlobs<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoListMissingBlobs.QueryParams,\n      ComAtprotoRepoListMissingBlobs.HandlerInput,\n      ComAtprotoRepoListMissingBlobs.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.listMissingBlobs' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listRecords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoListRecords.QueryParams,\n      ComAtprotoRepoListRecords.HandlerInput,\n      ComAtprotoRepoListRecords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.listRecords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  putRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoPutRecord.QueryParams,\n      ComAtprotoRepoPutRecord.HandlerInput,\n      ComAtprotoRepoPutRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.putRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  uploadBlob<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoRepoUploadBlob.QueryParams,\n      ComAtprotoRepoUploadBlob.HandlerInput,\n      ComAtprotoRepoUploadBlob.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoServerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  activateAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerActivateAccount.QueryParams,\n      ComAtprotoServerActivateAccount.HandlerInput,\n      ComAtprotoServerActivateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.activateAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkAccountStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCheckAccountStatus.QueryParams,\n      ComAtprotoServerCheckAccountStatus.HandlerInput,\n      ComAtprotoServerCheckAccountStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.checkAccountStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  confirmEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerConfirmEmail.QueryParams,\n      ComAtprotoServerConfirmEmail.HandlerInput,\n      ComAtprotoServerConfirmEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateAccount.QueryParams,\n      ComAtprotoServerCreateAccount.HandlerInput,\n      ComAtprotoServerCreateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createAppPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateAppPassword.QueryParams,\n      ComAtprotoServerCreateAppPassword.HandlerInput,\n      ComAtprotoServerCreateAppPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createAppPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createInviteCode<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateInviteCode.QueryParams,\n      ComAtprotoServerCreateInviteCode.HandlerInput,\n      ComAtprotoServerCreateInviteCode.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createInviteCode' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateInviteCodes.QueryParams,\n      ComAtprotoServerCreateInviteCodes.HandlerInput,\n      ComAtprotoServerCreateInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  createSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerCreateSession.QueryParams,\n      ComAtprotoServerCreateSession.HandlerInput,\n      ComAtprotoServerCreateSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.createSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deactivateAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeactivateAccount.QueryParams,\n      ComAtprotoServerDeactivateAccount.HandlerInput,\n      ComAtprotoServerDeactivateAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deactivateAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteAccount<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeleteAccount.QueryParams,\n      ComAtprotoServerDeleteAccount.HandlerInput,\n      ComAtprotoServerDeleteAccount.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deleteAccount' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDeleteSession.QueryParams,\n      ComAtprotoServerDeleteSession.HandlerInput,\n      ComAtprotoServerDeleteSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.deleteSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  describeServer<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerDescribeServer.QueryParams,\n      ComAtprotoServerDescribeServer.HandlerInput,\n      ComAtprotoServerDescribeServer.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.describeServer' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountInviteCodes<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetAccountInviteCodes.QueryParams,\n      ComAtprotoServerGetAccountInviteCodes.HandlerInput,\n      ComAtprotoServerGetAccountInviteCodes.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getAccountInviteCodes' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getServiceAuth<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetServiceAuth.QueryParams,\n      ComAtprotoServerGetServiceAuth.HandlerInput,\n      ComAtprotoServerGetServiceAuth.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getServiceAuth' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerGetSession.QueryParams,\n      ComAtprotoServerGetSession.HandlerInput,\n      ComAtprotoServerGetSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.getSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listAppPasswords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerListAppPasswords.QueryParams,\n      ComAtprotoServerListAppPasswords.HandlerInput,\n      ComAtprotoServerListAppPasswords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.listAppPasswords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  refreshSession<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRefreshSession.QueryParams,\n      ComAtprotoServerRefreshSession.HandlerInput,\n      ComAtprotoServerRefreshSession.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.refreshSession' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestAccountDelete<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestAccountDelete.QueryParams,\n      ComAtprotoServerRequestAccountDelete.HandlerInput,\n      ComAtprotoServerRequestAccountDelete.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestAccountDelete' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestEmailConfirmation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestEmailConfirmation.QueryParams,\n      ComAtprotoServerRequestEmailConfirmation.HandlerInput,\n      ComAtprotoServerRequestEmailConfirmation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestEmailUpdate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestEmailUpdate.QueryParams,\n      ComAtprotoServerRequestEmailUpdate.HandlerInput,\n      ComAtprotoServerRequestEmailUpdate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPasswordReset<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRequestPasswordReset.QueryParams,\n      ComAtprotoServerRequestPasswordReset.HandlerInput,\n      ComAtprotoServerRequestPasswordReset.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.requestPasswordReset' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  reserveSigningKey<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerReserveSigningKey.QueryParams,\n      ComAtprotoServerReserveSigningKey.HandlerInput,\n      ComAtprotoServerReserveSigningKey.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.reserveSigningKey' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  resetPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerResetPassword.QueryParams,\n      ComAtprotoServerResetPassword.HandlerInput,\n      ComAtprotoServerResetPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.resetPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeAppPassword<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerRevokeAppPassword.QueryParams,\n      ComAtprotoServerRevokeAppPassword.HandlerInput,\n      ComAtprotoServerRevokeAppPassword.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateEmail<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoServerUpdateEmail.QueryParams,\n      ComAtprotoServerUpdateEmail.HandlerInput,\n      ComAtprotoServerUpdateEmail.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.server.updateEmail' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoSyncNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getBlob<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetBlob.QueryParams,\n      ComAtprotoSyncGetBlob.HandlerInput,\n      ComAtprotoSyncGetBlob.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getBlob' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getBlocks<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetBlocks.QueryParams,\n      ComAtprotoSyncGetBlocks.HandlerInput,\n      ComAtprotoSyncGetBlocks.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getBlocks' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getCheckout<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetCheckout.QueryParams,\n      ComAtprotoSyncGetCheckout.HandlerInput,\n      ComAtprotoSyncGetCheckout.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getCheckout' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getHead<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetHead.QueryParams,\n      ComAtprotoSyncGetHead.HandlerInput,\n      ComAtprotoSyncGetHead.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getHead' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getHostStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetHostStatus.QueryParams,\n      ComAtprotoSyncGetHostStatus.HandlerInput,\n      ComAtprotoSyncGetHostStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getHostStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getLatestCommit<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetLatestCommit.QueryParams,\n      ComAtprotoSyncGetLatestCommit.HandlerInput,\n      ComAtprotoSyncGetLatestCommit.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getLatestCommit' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRecord.QueryParams,\n      ComAtprotoSyncGetRecord.HandlerInput,\n      ComAtprotoSyncGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRepo.QueryParams,\n      ComAtprotoSyncGetRepo.HandlerInput,\n      ComAtprotoSyncGetRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepoStatus<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncGetRepoStatus.QueryParams,\n      ComAtprotoSyncGetRepoStatus.HandlerInput,\n      ComAtprotoSyncGetRepoStatus.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.getRepoStatus' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listBlobs<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListBlobs.QueryParams,\n      ComAtprotoSyncListBlobs.HandlerInput,\n      ComAtprotoSyncListBlobs.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listBlobs' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listHosts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListHosts.QueryParams,\n      ComAtprotoSyncListHosts.HandlerInput,\n      ComAtprotoSyncListHosts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listHosts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListRepos.QueryParams,\n      ComAtprotoSyncListRepos.HandlerInput,\n      ComAtprotoSyncListRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listReposByCollection<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncListReposByCollection.QueryParams,\n      ComAtprotoSyncListReposByCollection.HandlerInput,\n      ComAtprotoSyncListReposByCollection.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.listReposByCollection' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  notifyOfUpdate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncNotifyOfUpdate.QueryParams,\n      ComAtprotoSyncNotifyOfUpdate.HandlerInput,\n      ComAtprotoSyncNotifyOfUpdate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.notifyOfUpdate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestCrawl<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoSyncRequestCrawl.QueryParams,\n      ComAtprotoSyncRequestCrawl.HandlerInput,\n      ComAtprotoSyncRequestCrawl.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.requestCrawl' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  subscribeRepos<A extends Auth = void>(\n    cfg: StreamConfigOrHandler<\n      A,\n      ComAtprotoSyncSubscribeRepos.QueryParams,\n      ComAtprotoSyncSubscribeRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.sync.subscribeRepos' // @ts-ignore\n    return this._server.xrpc.streamMethod(nsid, cfg)\n  }\n}\n\nexport class ComAtprotoTempNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addReservedHandle<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempAddReservedHandle.QueryParams,\n      ComAtprotoTempAddReservedHandle.HandlerInput,\n      ComAtprotoTempAddReservedHandle.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.addReservedHandle' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkHandleAvailability<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempCheckHandleAvailability.QueryParams,\n      ComAtprotoTempCheckHandleAvailability.HandlerInput,\n      ComAtprotoTempCheckHandleAvailability.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.checkHandleAvailability' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  checkSignupQueue<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempCheckSignupQueue.QueryParams,\n      ComAtprotoTempCheckSignupQueue.HandlerInput,\n      ComAtprotoTempCheckSignupQueue.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.checkSignupQueue' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  dereferenceScope<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempDereferenceScope.QueryParams,\n      ComAtprotoTempDereferenceScope.HandlerInput,\n      ComAtprotoTempDereferenceScope.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.dereferenceScope' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  fetchLabels<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempFetchLabels.QueryParams,\n      ComAtprotoTempFetchLabels.HandlerInput,\n      ComAtprotoTempFetchLabels.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.fetchLabels' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  requestPhoneVerification<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempRequestPhoneVerification.QueryParams,\n      ComAtprotoTempRequestPhoneVerification.HandlerInput,\n      ComAtprotoTempRequestPhoneVerification.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.requestPhoneVerification' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeAccountCredentials<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ComAtprotoTempRevokeAccountCredentials.QueryParams,\n      ComAtprotoTempRevokeAccountCredentials.HandlerInput,\n      ComAtprotoTempRevokeAccountCredentials.HandlerOutput\n    >,\n  ) {\n    const nsid = 'com.atproto.temp.revokeAccountCredentials' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsNS {\n  _server: Server\n  ozone: ToolsOzoneNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.ozone = new ToolsOzoneNS(server)\n  }\n}\n\nexport class ToolsOzoneNS {\n  _server: Server\n  communication: ToolsOzoneCommunicationNS\n  hosting: ToolsOzoneHostingNS\n  moderation: ToolsOzoneModerationNS\n  safelink: ToolsOzoneSafelinkNS\n  server: ToolsOzoneServerNS\n  set: ToolsOzoneSetNS\n  setting: ToolsOzoneSettingNS\n  signature: ToolsOzoneSignatureNS\n  team: ToolsOzoneTeamNS\n  verification: ToolsOzoneVerificationNS\n\n  constructor(server: Server) {\n    this._server = server\n    this.communication = new ToolsOzoneCommunicationNS(server)\n    this.hosting = new ToolsOzoneHostingNS(server)\n    this.moderation = new ToolsOzoneModerationNS(server)\n    this.safelink = new ToolsOzoneSafelinkNS(server)\n    this.server = new ToolsOzoneServerNS(server)\n    this.set = new ToolsOzoneSetNS(server)\n    this.setting = new ToolsOzoneSettingNS(server)\n    this.signature = new ToolsOzoneSignatureNS(server)\n    this.team = new ToolsOzoneTeamNS(server)\n    this.verification = new ToolsOzoneVerificationNS(server)\n  }\n}\n\nexport class ToolsOzoneCommunicationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  createTemplate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationCreateTemplate.QueryParams,\n      ToolsOzoneCommunicationCreateTemplate.HandlerInput,\n      ToolsOzoneCommunicationCreateTemplate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.createTemplate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteTemplate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationDeleteTemplate.QueryParams,\n      ToolsOzoneCommunicationDeleteTemplate.HandlerInput,\n      ToolsOzoneCommunicationDeleteTemplate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.deleteTemplate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listTemplates<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationListTemplates.QueryParams,\n      ToolsOzoneCommunicationListTemplates.HandlerInput,\n      ToolsOzoneCommunicationListTemplates.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.listTemplates' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateTemplate<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneCommunicationUpdateTemplate.QueryParams,\n      ToolsOzoneCommunicationUpdateTemplate.HandlerInput,\n      ToolsOzoneCommunicationUpdateTemplate.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.communication.updateTemplate' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneHostingNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getAccountHistory<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneHostingGetAccountHistory.QueryParams,\n      ToolsOzoneHostingGetAccountHistory.HandlerInput,\n      ToolsOzoneHostingGetAccountHistory.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.hosting.getAccountHistory' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneModerationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  cancelScheduledActions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationCancelScheduledActions.QueryParams,\n      ToolsOzoneModerationCancelScheduledActions.HandlerInput,\n      ToolsOzoneModerationCancelScheduledActions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.cancelScheduledActions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  emitEvent<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationEmitEvent.QueryParams,\n      ToolsOzoneModerationEmitEvent.HandlerInput,\n      ToolsOzoneModerationEmitEvent.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.emitEvent' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getAccountTimeline<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetAccountTimeline.QueryParams,\n      ToolsOzoneModerationGetAccountTimeline.HandlerInput,\n      ToolsOzoneModerationGetAccountTimeline.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getAccountTimeline' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getEvent<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetEvent.QueryParams,\n      ToolsOzoneModerationGetEvent.HandlerInput,\n      ToolsOzoneModerationGetEvent.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getEvent' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecord<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRecord.QueryParams,\n      ToolsOzoneModerationGetRecord.HandlerInput,\n      ToolsOzoneModerationGetRecord.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRecord' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRecords<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRecords.QueryParams,\n      ToolsOzoneModerationGetRecords.HandlerInput,\n      ToolsOzoneModerationGetRecords.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRecords' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepo<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRepo.QueryParams,\n      ToolsOzoneModerationGetRepo.HandlerInput,\n      ToolsOzoneModerationGetRepo.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRepo' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getReporterStats<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetReporterStats.QueryParams,\n      ToolsOzoneModerationGetReporterStats.HandlerInput,\n      ToolsOzoneModerationGetReporterStats.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getReporterStats' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetRepos.QueryParams,\n      ToolsOzoneModerationGetRepos.HandlerInput,\n      ToolsOzoneModerationGetRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getSubjects<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationGetSubjects.QueryParams,\n      ToolsOzoneModerationGetSubjects.HandlerInput,\n      ToolsOzoneModerationGetSubjects.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.getSubjects' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listScheduledActions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationListScheduledActions.QueryParams,\n      ToolsOzoneModerationListScheduledActions.HandlerInput,\n      ToolsOzoneModerationListScheduledActions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.listScheduledActions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryEvents<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationQueryEvents.QueryParams,\n      ToolsOzoneModerationQueryEvents.HandlerInput,\n      ToolsOzoneModerationQueryEvents.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.queryEvents' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryStatuses<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationQueryStatuses.QueryParams,\n      ToolsOzoneModerationQueryStatuses.HandlerInput,\n      ToolsOzoneModerationQueryStatuses.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.queryStatuses' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  scheduleAction<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationScheduleAction.QueryParams,\n      ToolsOzoneModerationScheduleAction.HandlerInput,\n      ToolsOzoneModerationScheduleAction.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.scheduleAction' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchRepos<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneModerationSearchRepos.QueryParams,\n      ToolsOzoneModerationSearchRepos.HandlerInput,\n      ToolsOzoneModerationSearchRepos.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.moderation.searchRepos' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSafelinkNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addRule<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkAddRule.QueryParams,\n      ToolsOzoneSafelinkAddRule.HandlerInput,\n      ToolsOzoneSafelinkAddRule.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.addRule' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryEvents<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkQueryEvents.QueryParams,\n      ToolsOzoneSafelinkQueryEvents.HandlerInput,\n      ToolsOzoneSafelinkQueryEvents.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.queryEvents' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  queryRules<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkQueryRules.QueryParams,\n      ToolsOzoneSafelinkQueryRules.HandlerInput,\n      ToolsOzoneSafelinkQueryRules.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.queryRules' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeRule<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkRemoveRule.QueryParams,\n      ToolsOzoneSafelinkRemoveRule.HandlerInput,\n      ToolsOzoneSafelinkRemoveRule.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.removeRule' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateRule<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSafelinkUpdateRule.QueryParams,\n      ToolsOzoneSafelinkUpdateRule.HandlerInput,\n      ToolsOzoneSafelinkUpdateRule.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.safelink.updateRule' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneServerNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  getConfig<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneServerGetConfig.QueryParams,\n      ToolsOzoneServerGetConfig.HandlerInput,\n      ToolsOzoneServerGetConfig.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.server.getConfig' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSetNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addValues<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetAddValues.QueryParams,\n      ToolsOzoneSetAddValues.HandlerInput,\n      ToolsOzoneSetAddValues.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.addValues' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteSet<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetDeleteSet.QueryParams,\n      ToolsOzoneSetDeleteSet.HandlerInput,\n      ToolsOzoneSetDeleteSet.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.deleteSet' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteValues<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetDeleteValues.QueryParams,\n      ToolsOzoneSetDeleteValues.HandlerInput,\n      ToolsOzoneSetDeleteValues.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.deleteValues' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  getValues<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetGetValues.QueryParams,\n      ToolsOzoneSetGetValues.HandlerInput,\n      ToolsOzoneSetGetValues.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.getValues' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  querySets<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetQuerySets.QueryParams,\n      ToolsOzoneSetQuerySets.HandlerInput,\n      ToolsOzoneSetQuerySets.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.querySets' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  upsertSet<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSetUpsertSet.QueryParams,\n      ToolsOzoneSetUpsertSet.HandlerInput,\n      ToolsOzoneSetUpsertSet.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.set.upsertSet' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSettingNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  listOptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSettingListOptions.QueryParams,\n      ToolsOzoneSettingListOptions.HandlerInput,\n      ToolsOzoneSettingListOptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.setting.listOptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  removeOptions<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSettingRemoveOptions.QueryParams,\n      ToolsOzoneSettingRemoveOptions.HandlerInput,\n      ToolsOzoneSettingRemoveOptions.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.setting.removeOptions' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  upsertOption<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSettingUpsertOption.QueryParams,\n      ToolsOzoneSettingUpsertOption.HandlerInput,\n      ToolsOzoneSettingUpsertOption.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.setting.upsertOption' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneSignatureNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  findCorrelation<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSignatureFindCorrelation.QueryParams,\n      ToolsOzoneSignatureFindCorrelation.HandlerInput,\n      ToolsOzoneSignatureFindCorrelation.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.signature.findCorrelation' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  findRelatedAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSignatureFindRelatedAccounts.QueryParams,\n      ToolsOzoneSignatureFindRelatedAccounts.HandlerInput,\n      ToolsOzoneSignatureFindRelatedAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.signature.findRelatedAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  searchAccounts<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneSignatureSearchAccounts.QueryParams,\n      ToolsOzoneSignatureSearchAccounts.HandlerInput,\n      ToolsOzoneSignatureSearchAccounts.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.signature.searchAccounts' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneTeamNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  addMember<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamAddMember.QueryParams,\n      ToolsOzoneTeamAddMember.HandlerInput,\n      ToolsOzoneTeamAddMember.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.addMember' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  deleteMember<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamDeleteMember.QueryParams,\n      ToolsOzoneTeamDeleteMember.HandlerInput,\n      ToolsOzoneTeamDeleteMember.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.deleteMember' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listMembers<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamListMembers.QueryParams,\n      ToolsOzoneTeamListMembers.HandlerInput,\n      ToolsOzoneTeamListMembers.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.listMembers' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  updateMember<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneTeamUpdateMember.QueryParams,\n      ToolsOzoneTeamUpdateMember.HandlerInput,\n      ToolsOzoneTeamUpdateMember.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.team.updateMember' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n\nexport class ToolsOzoneVerificationNS {\n  _server: Server\n\n  constructor(server: Server) {\n    this._server = server\n  }\n\n  grantVerifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneVerificationGrantVerifications.QueryParams,\n      ToolsOzoneVerificationGrantVerifications.HandlerInput,\n      ToolsOzoneVerificationGrantVerifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.verification.grantVerifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  listVerifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneVerificationListVerifications.QueryParams,\n      ToolsOzoneVerificationListVerifications.HandlerInput,\n      ToolsOzoneVerificationListVerifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.verification.listVerifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n\n  revokeVerifications<A extends Auth = void>(\n    cfg: MethodConfigOrHandler<\n      A,\n      ToolsOzoneVerificationRevokeVerifications.QueryParams,\n      ToolsOzoneVerificationRevokeVerifications.HandlerInput,\n      ToolsOzoneVerificationRevokeVerifications.HandlerOutput\n    >,\n  ) {\n    const nsid = 'tools.ozone.verification.revokeVerifications' // @ts-ignore\n    return this._server.xrpc.method(nsid, cfg)\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/lexicons.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport {\n  type LexiconDoc,\n  Lexicons,\n  ValidationError,\n  type ValidationResult,\n} from '@atproto/lexicon'\nimport { type $Typed, is$typed, maybe$typed } from './util.js'\n\nexport const schemaDict = {\n  AppBskyActorDefs: {\n    lexicon: 1,\n    id: 'app.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileView: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileViewDetailed: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 256,\n            maxLength: 2560,\n          },\n          pronouns: {\n            type: 'string',\n          },\n          website: {\n            type: 'string',\n            format: 'uri',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          banner: {\n            type: 'string',\n            format: 'uri',\n          },\n          followersCount: {\n            type: 'integer',\n          },\n          followsCount: {\n            type: 'integer',\n          },\n          postsCount: {\n            type: 'integer',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          joinedViaStarterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          pinnedPost: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#statusView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      profileAssociated: {\n        type: 'object',\n        properties: {\n          lists: {\n            type: 'integer',\n          },\n          feedgens: {\n            type: 'integer',\n          },\n          starterPacks: {\n            type: 'integer',\n          },\n          labeler: {\n            type: 'boolean',\n          },\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedChat',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedActivitySubscription',\n          },\n          germ: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociatedGerm',\n          },\n        },\n      },\n      profileAssociatedChat: {\n        type: 'object',\n        required: ['allowIncoming'],\n        properties: {\n          allowIncoming: {\n            type: 'string',\n            knownValues: ['all', 'none', 'following'],\n          },\n        },\n      },\n      profileAssociatedGerm: {\n        type: 'object',\n        required: ['showButtonTo', 'messageMeUrl'],\n        properties: {\n          messageMeUrl: {\n            type: 'string',\n            format: 'uri',\n          },\n          showButtonTo: {\n            type: 'string',\n            knownValues: ['usersIFollow', 'everyone'],\n          },\n        },\n      },\n      profileAssociatedActivitySubscription: {\n        type: 'object',\n        required: ['allowSubscriptions'],\n        properties: {\n          allowSubscriptions: {\n            type: 'string',\n            knownValues: ['followers', 'mutuals', 'none'],\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.\",\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          mutedByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          blockedBy: {\n            type: 'boolean',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blockingByList: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          knownFollowers: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#knownFollowers',\n          },\n          activitySubscription: {\n            description:\n              'This property is present only in selected cases, as an optimization.',\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n      knownFollowers: {\n        type: 'object',\n        description: \"The subject's followers whom you also follow\",\n        required: ['count', 'followers'],\n        properties: {\n          count: {\n            type: 'integer',\n          },\n          followers: {\n            type: 'array',\n            minLength: 0,\n            maxLength: 5,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      verificationState: {\n        type: 'object',\n        description:\n          'Represents the verification information about the user this object is attached to.',\n        required: ['verifications', 'verifiedStatus', 'trustedVerifierStatus'],\n        properties: {\n          verifications: {\n            type: 'array',\n            description:\n              'All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#verificationView',\n            },\n          },\n          verifiedStatus: {\n            type: 'string',\n            description: \"The user's status as a verified account.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n          trustedVerifierStatus: {\n            type: 'string',\n            description: \"The user's status as a trusted verifier.\",\n            knownValues: ['valid', 'invalid', 'none'],\n          },\n        },\n      },\n      verificationView: {\n        type: 'object',\n        description: 'An individual verification for an associated subject.',\n        required: ['issuer', 'uri', 'isValid', 'createdAt'],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          isValid: {\n            type: 'boolean',\n            description:\n              'True if the verification passes validation, otherwise false.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n        },\n      },\n      preferences: {\n        type: 'array',\n        items: {\n          type: 'union',\n          refs: [\n            'lex:app.bsky.actor.defs#adultContentPref',\n            'lex:app.bsky.actor.defs#contentLabelPref',\n            'lex:app.bsky.actor.defs#savedFeedsPref',\n            'lex:app.bsky.actor.defs#savedFeedsPrefV2',\n            'lex:app.bsky.actor.defs#personalDetailsPref',\n            'lex:app.bsky.actor.defs#declaredAgePref',\n            'lex:app.bsky.actor.defs#feedViewPref',\n            'lex:app.bsky.actor.defs#threadViewPref',\n            'lex:app.bsky.actor.defs#interestsPref',\n            'lex:app.bsky.actor.defs#mutedWordsPref',\n            'lex:app.bsky.actor.defs#hiddenPostsPref',\n            'lex:app.bsky.actor.defs#bskyAppStatePref',\n            'lex:app.bsky.actor.defs#labelersPref',\n            'lex:app.bsky.actor.defs#postInteractionSettingsPref',\n            'lex:app.bsky.actor.defs#verificationPrefs',\n            'lex:app.bsky.actor.defs#liveEventPreferences',\n          ],\n        },\n      },\n      adultContentPref: {\n        type: 'object',\n        required: ['enabled'],\n        properties: {\n          enabled: {\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      contentLabelPref: {\n        type: 'object',\n        required: ['label', 'visibility'],\n        properties: {\n          labelerDid: {\n            type: 'string',\n            description:\n              'Which labeler does this preference apply to? If undefined, applies globally.',\n            format: 'did',\n          },\n          label: {\n            type: 'string',\n          },\n          visibility: {\n            type: 'string',\n            knownValues: ['ignore', 'show', 'warn', 'hide'],\n          },\n        },\n      },\n      savedFeed: {\n        type: 'object',\n        required: ['id', 'type', 'value', 'pinned'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          type: {\n            type: 'string',\n            knownValues: ['feed', 'list', 'timeline'],\n          },\n          value: {\n            type: 'string',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      savedFeedsPrefV2: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#savedFeed',\n            },\n          },\n        },\n      },\n      savedFeedsPref: {\n        type: 'object',\n        required: ['pinned', 'saved'],\n        properties: {\n          pinned: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          saved: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n          },\n          timelineIndex: {\n            type: 'integer',\n          },\n        },\n      },\n      personalDetailsPref: {\n        type: 'object',\n        properties: {\n          birthDate: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The birth date of account owner.',\n          },\n        },\n      },\n      declaredAgePref: {\n        type: 'object',\n        description:\n          \"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.\",\n        properties: {\n          isOverAge13: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 13 years of age.',\n          },\n          isOverAge16: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 16 years of age.',\n          },\n          isOverAge18: {\n            type: 'boolean',\n            description:\n              'Indicates if the user has declared that they are over 18 years of age.',\n          },\n        },\n      },\n      feedViewPref: {\n        type: 'object',\n        required: ['feed'],\n        properties: {\n          feed: {\n            type: 'string',\n            description:\n              'The URI of the feed, or an identifier which describes the feed.',\n          },\n          hideReplies: {\n            type: 'boolean',\n            description: 'Hide replies in the feed.',\n          },\n          hideRepliesByUnfollowed: {\n            type: 'boolean',\n            description:\n              'Hide replies in the feed if they are not by followed users.',\n            default: true,\n          },\n          hideRepliesByLikeCount: {\n            type: 'integer',\n            description:\n              'Hide replies in the feed if they do not have this number of likes.',\n          },\n          hideReposts: {\n            type: 'boolean',\n            description: 'Hide reposts in the feed.',\n          },\n          hideQuotePosts: {\n            type: 'boolean',\n            description: 'Hide quote posts in the feed.',\n          },\n        },\n      },\n      threadViewPref: {\n        type: 'object',\n        properties: {\n          sort: {\n            type: 'string',\n            description: 'Sorting mode for threads.',\n            knownValues: [\n              'oldest',\n              'newest',\n              'most-likes',\n              'random',\n              'hotness',\n            ],\n          },\n        },\n      },\n      interestsPref: {\n        type: 'object',\n        required: ['tags'],\n        properties: {\n          tags: {\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'string',\n              maxLength: 640,\n              maxGraphemes: 64,\n            },\n            description:\n              \"A list of tags which describe the account owner's interests gathered during onboarding.\",\n          },\n        },\n      },\n      mutedWordTarget: {\n        type: 'string',\n        knownValues: ['content', 'tag'],\n        maxLength: 640,\n        maxGraphemes: 64,\n      },\n      mutedWord: {\n        type: 'object',\n        description: 'A word that the account owner has muted.',\n        required: ['value', 'targets'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n            description: 'The muted word itself.',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          targets: {\n            type: 'array',\n            description: 'The intended targets of the muted word.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWordTarget',\n            },\n          },\n          actorTarget: {\n            type: 'string',\n            description:\n              'Groups of users to apply the muted word to. If undefined, applies to all users.',\n            knownValues: ['all', 'exclude-following'],\n            default: 'all',\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the muted word will expire and no longer be applied.',\n          },\n        },\n      },\n      mutedWordsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#mutedWord',\n            },\n            description: 'A list of words the account owner has muted.',\n          },\n        },\n      },\n      hiddenPostsPref: {\n        type: 'object',\n        required: ['items'],\n        properties: {\n          items: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            description:\n              'A list of URIs of posts the account owner has hidden.',\n          },\n        },\n      },\n      labelersPref: {\n        type: 'object',\n        required: ['labelers'],\n        properties: {\n          labelers: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#labelerPrefItem',\n            },\n          },\n        },\n      },\n      labelerPrefItem: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      bskyAppStatePref: {\n        description:\n          \"A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.\",\n        type: 'object',\n        properties: {\n          activeProgressGuide: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#bskyAppProgressGuide',\n          },\n          queuedNudges: {\n            description:\n              'An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.',\n            type: 'array',\n            maxLength: 1000,\n            items: {\n              type: 'string',\n              maxLength: 100,\n            },\n          },\n          nuxs: {\n            description: 'Storage for NUXs the user has encountered.',\n            type: 'array',\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#nux',\n            },\n          },\n        },\n      },\n      bskyAppProgressGuide: {\n        description:\n          'If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.',\n        type: 'object',\n        required: ['guide'],\n        properties: {\n          guide: {\n            type: 'string',\n            maxLength: 100,\n          },\n        },\n      },\n      nux: {\n        type: 'object',\n        description: 'A new user experiences (NUX) storage object',\n        required: ['id', 'completed'],\n        properties: {\n          id: {\n            type: 'string',\n            maxLength: 100,\n          },\n          completed: {\n            type: 'boolean',\n            default: false,\n          },\n          data: {\n            description:\n              'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.',\n            type: 'string',\n            maxLength: 3000,\n            maxGraphemes: 300,\n          },\n          expiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'The date and time at which the NUX will expire and should be considered completed.',\n          },\n        },\n      },\n      verificationPrefs: {\n        type: 'object',\n        description: 'Preferences for how verified accounts appear in the app.',\n        required: [],\n        properties: {\n          hideBadges: {\n            description:\n              'Hide the blue check badges for verified accounts and trusted verifiers.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      liveEventPreferences: {\n        type: 'object',\n        description: 'Preferences for live events.',\n        properties: {\n          hiddenFeedIds: {\n            description:\n              'A list of feed IDs that the user has hidden from live events.',\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          hideAllFeeds: {\n            description: 'Whether to hide all feeds from live events.',\n            type: 'boolean',\n            default: false,\n          },\n        },\n      },\n      postInteractionSettingsPref: {\n        type: 'object',\n        description:\n          '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.',\n        required: [],\n        properties: {\n          threadgateAllowRules: {\n            description:\n              'Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n        },\n      },\n      statusView: {\n        type: 'object',\n        required: ['status', 'record'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          status: {\n            type: 'string',\n            description: 'The status for the account.',\n            knownValues: ['app.bsky.actor.status#live'],\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            description: 'An optional embed associated with the status.',\n            refs: ['lex:app.bsky.embed.external#view'],\n          },\n          expiresAt: {\n            type: 'string',\n            description:\n              'The date when this status will expire. The application might choose to no longer return the status after expiration.',\n            format: 'datetime',\n          },\n          isActive: {\n            type: 'boolean',\n            description:\n              'True if the status is not expired, false if it is expired. Only present if expiration was set.',\n          },\n          isDisabled: {\n            type: 'boolean',\n            description:\n              \"True if the user's go-live access has been disabled by a moderator, false otherwise.\",\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfile',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID of account to fetch profile of.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetProfiles: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getProfiles',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get detailed profile views of multiple actors.',\n        parameters: {\n          type: 'params',\n          required: ['actors'],\n          properties: {\n            actors: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['profiles'],\n            properties: {\n              profiles: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorGetSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.actor.getSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorProfile: {\n    lexicon: 1,\n    id: 'app.bsky.actor.profile',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account profile.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          properties: {\n            displayName: {\n              type: 'string',\n              maxGraphemes: 64,\n              maxLength: 640,\n            },\n            description: {\n              type: 'string',\n              description: 'Free-form profile description text.',\n              maxGraphemes: 256,\n              maxLength: 2560,\n            },\n            pronouns: {\n              type: 'string',\n              description: 'Free-form pronouns text.',\n              maxGraphemes: 20,\n              maxLength: 200,\n            },\n            website: {\n              type: 'string',\n              format: 'uri',\n            },\n            avatar: {\n              type: 'blob',\n              description:\n                \"Small image to be displayed next to posts from account. AKA, 'profile picture'\",\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            banner: {\n              type: 'blob',\n              description:\n                'Larger horizontal image to display behind profile view.',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values, specific to the Bluesky application, on the overall account.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            joinedViaStarterPack: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            pinnedPost: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.actor.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Set the private preferences attached to the account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActors: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActors',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actors (profiles) matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorSearchActorsTypeahead: {\n    lexicon: 1,\n    id: 'app.bsky.actor.searchActorsTypeahead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead.\",\n            },\n            q: {\n              type: 'string',\n              description: 'Search query prefix; not a full query string.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyActorStatus: {\n    lexicon: 1,\n    id: 'app.bsky.actor.status',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky account status.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['status', 'createdAt'],\n          properties: {\n            status: {\n              type: 'string',\n              description: 'The status for the account.',\n              knownValues: ['app.bsky.actor.status#live'],\n            },\n            embed: {\n              type: 'union',\n              description: 'An optional embed associated with the status.',\n              refs: ['lex:app.bsky.embed.external'],\n            },\n            durationMinutes: {\n              type: 'integer',\n              description:\n                'The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.',\n              minimum: 1,\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      live: {\n        type: 'token',\n        description:\n          'Advertises an account as currently offering live content.',\n      },\n    },\n  },\n  AppBskyAgeassuranceBegin: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.begin',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate Age Assurance for an account.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive Age Assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the Age Assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n              regionCode: {\n                type: 'string',\n                description:\n                  \"An optional ISO 3166-2 code of the user's region or state within the country.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#state',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n          {\n            name: 'RegionNotSupported',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyAgeassuranceDefs: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.defs',\n    defs: {\n      access: {\n        description:\n          \"The access level granted based on Age Assurance data we've processed.\",\n        type: 'string',\n        knownValues: ['unknown', 'none', 'safe', 'full'],\n      },\n      status: {\n        type: 'string',\n        description: 'The status of the Age Assurance process.',\n        knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n      },\n      state: {\n        type: 'object',\n        description: \"The user's computed Age Assurance state.\",\n        required: ['status', 'access'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#status',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      stateMetadata: {\n        type: 'object',\n        description:\n          'Additional metadata needed to compute Age Assurance state client-side.',\n        required: [],\n        properties: {\n          accountCreatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The account creation timestamp.',\n          },\n        },\n      },\n      config: {\n        type: 'object',\n        description: '',\n        required: ['regions'],\n        properties: {\n          regions: {\n            type: 'array',\n            description: 'The per-region Age Assurance configuration.',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.ageassurance.defs#configRegion',\n            },\n          },\n        },\n      },\n      configRegion: {\n        type: 'object',\n        description: 'The Age Assurance configuration for a specific region.',\n        required: ['countryCode', 'minAccessAge', 'rules'],\n        properties: {\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code this configuration applies to.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country.',\n          },\n          minAccessAge: {\n            type: 'integer',\n            description:\n              'The minimum age (as a whole integer) required to use Bluesky in this region.',\n          },\n          rules: {\n            type: 'array',\n            description:\n              '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.',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.ageassurance.defs#configRegionRuleDefault',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan',\n                'lex:app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan',\n              ],\n            },\n          },\n        },\n      },\n      configRegionRuleDefault: {\n        type: 'object',\n        description: 'Age Assurance rule that applies by default.',\n        required: ['access'],\n        properties: {\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfDeclaredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has declared themselves under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredOverAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAssuredUnderAge: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the user has been assured to be under a certain age.',\n        required: ['age', 'access'],\n        properties: {\n          age: {\n            type: 'integer',\n            description: 'The age threshold as a whole integer.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountNewerThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is equal-to or newer than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      configRegionRuleIfAccountOlderThan: {\n        type: 'object',\n        description:\n          'Age Assurance rule that applies if the account is older than a certain date.',\n        required: ['date', 'access'],\n        properties: {\n          date: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date threshold as a datetime string.',\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        description: 'Object used to store Age Assurance data in stash.',\n        required: ['createdAt', 'status', 'access', 'attemptId', 'countryCode'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the Age Assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n          access: {\n            description:\n              \"The access level granted based on Age Assurance data we've processed.\",\n            type: 'string',\n            knownValues: ['unknown', 'none', 'safe', 'full'],\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for Age Assurance.',\n          },\n          initIp: {\n            type: 'string',\n            description:\n              'The IP address used when initiating the Age Assurance flow.',\n          },\n          initUa: {\n            type: 'string',\n            description:\n              'The user agent used when initiating the Age Assurance flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description:\n              'The IP address used when completing the Age Assurance flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description:\n              'The user agent used when completing the Age Assurance flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns Age Assurance configuration for use on the client.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#config',\n          },\n        },\n      },\n    },\n  },\n  AppBskyAgeassuranceGetState: {\n    lexicon: 1,\n    id: 'app.bsky.ageassurance.getState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side.',\n        parameters: {\n          type: 'params',\n          required: ['countryCode'],\n          properties: {\n            countryCode: {\n              type: 'string',\n            },\n            regionCode: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['state', 'metadata'],\n            properties: {\n              state: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#state',\n              },\n              metadata: {\n                type: 'ref',\n                ref: 'lex:app.bsky.ageassurance.defs#stateMetadata',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkCreateBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.createBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkDefs: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.defs',\n    defs: {\n      bookmark: {\n        description: 'Object used to store bookmark data in stash.',\n        type: 'object',\n        required: ['subject'],\n        properties: {\n          subject: {\n            description:\n              'A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      bookmarkView: {\n        type: 'object',\n        required: ['subject', 'item'],\n        properties: {\n          subject: {\n            description: 'A strong ref to the bookmarked record.',\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          item: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#blockedPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#postView',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyBookmarkDeleteBookmark: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.deleteBookmark',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnsupportedCollection',\n            description:\n              'The URI to be bookmarked is for an unsupported collection.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyBookmarkGetBookmarks: {\n    lexicon: 1,\n    id: 'app.bsky.bookmark.getBookmarks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Gets views of records bookmarked by the authenticated user. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['bookmarks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              bookmarks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.bookmark.defs#bookmarkView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDefs: {\n    lexicon: 1,\n    id: 'app.bsky.contact.defs',\n    defs: {\n      matchAndContactIndex: {\n        description:\n          '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.',\n        type: 'object',\n        required: ['match', 'contactIndex'],\n        properties: {\n          match: {\n            description: 'Profile of the matched user.',\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          contactIndex: {\n            description: 'The index of this match in the import contact input.',\n            type: 'integer',\n            minimum: 0,\n            maximum: 999,\n          },\n        },\n      },\n      syncStatus: {\n        type: 'object',\n        required: ['syncedAt', 'matchesCount'],\n        properties: {\n          syncedAt: {\n            description: 'Last date when contacts where imported.',\n            type: 'string',\n            format: 'datetime',\n          },\n          matchesCount: {\n            description:\n              '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.',\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n      notification: {\n        description:\n          'A stash object to be sent via bsync representing a notification to be created.',\n        type: 'object',\n        required: ['from', 'to'],\n        properties: {\n          from: {\n            description: 'The DID of who this notification comes from.',\n            type: 'string',\n            format: 'did',\n          },\n          to: {\n            description: 'The DID of who this notification should go to.',\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactDismissMatch: {\n    lexicon: 1,\n    id: 'app.bsky.contact.dismissMatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                description: \"The subject's DID to dismiss the match with.\",\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetMatches: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getMatches',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matches'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              matches: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidLimit',\n          },\n          {\n            name: 'InvalidCursor',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactGetSyncStatus: {\n    lexicon: 1,\n    id: 'app.bsky.contact.getSyncStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets the user's current contact import status. Requires authentication.\",\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              syncStatus: {\n                description:\n                  \"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.\",\n                type: 'ref',\n                ref: 'lex:app.bsky.contact.defs#syncStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactImportContacts: {\n    lexicon: 1,\n    id: 'app.bsky.contact.importContacts',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'contacts'],\n            properties: {\n              token: {\n                description:\n                  'JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`.',\n                type: 'string',\n              },\n              contacts: {\n                description:\n                  \"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`.\",\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                minLength: 1,\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['matchesAndContactIndexes'],\n            properties: {\n              matchesAndContactIndexes: {\n                description:\n                  'The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list.',\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.contact.defs#matchAndContactIndex',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidContacts',\n          },\n          {\n            name: 'TooManyContacts',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactRemoveData: {\n    lexicon: 1,\n    id: 'app.bsky.contact.removeData',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactSendNotification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.sendNotification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'System endpoint to send notifications related to contact imports. Requires role authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['from', 'to'],\n            properties: {\n              from: {\n                description: 'The DID of who this notification comes from.',\n                type: 'string',\n                format: 'did',\n              },\n              to: {\n                description: 'The DID of who this notification should go to.',\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyContactStartPhoneVerification: {\n    lexicon: 1,\n    id: 'app.bsky.contact.startPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          '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.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone'],\n            properties: {\n              phone: {\n                description: 'The phone number to receive the code via SMS.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyContactVerifyPhone: {\n    lexicon: 1,\n    id: 'app.bsky.contact.verifyPhone',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phone', 'code'],\n            properties: {\n              phone: {\n                description:\n                  'The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n              code: {\n                description:\n                  'The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                description:\n                  'JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call.',\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RateLimitExceeded',\n          },\n          {\n            name: 'InvalidDid',\n          },\n          {\n            name: 'InvalidPhone',\n          },\n          {\n            name: 'InvalidCode',\n          },\n          {\n            name: 'InternalError',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftCreateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.createDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draft',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'The ID of the created draft.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DraftLimitReached',\n            description:\n              'Trying to insert a new draft when the limit was already reached.',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyDraftDefs: {\n    lexicon: 1,\n    id: 'app.bsky.draft.defs',\n    defs: {\n      draftWithId: {\n        description:\n          'A draft with an identifier, used to store drafts in private storage (stash).',\n        type: 'object',\n        required: ['id', 'draft'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n        },\n      },\n      draft: {\n        description: 'A draft containing an array of draft posts.',\n        type: 'object',\n        required: ['posts'],\n        properties: {\n          deviceId: {\n            type: 'string',\n            description:\n              'UUIDv4 identifier of the device that created this draft.',\n            maxLength: 100,\n          },\n          deviceName: {\n            type: 'string',\n            description:\n              'The device and/or platform on which the draft was created.',\n            maxLength: 100,\n          },\n          posts: {\n            description: 'Array of draft posts that compose this draft.',\n            type: 'array',\n            minLength: 1,\n            maxLength: 100,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftPost',\n            },\n          },\n          langs: {\n            type: 'array',\n            description:\n              'Indicates human language of posts primary text content.',\n            maxLength: 3,\n            items: {\n              type: 'string',\n              format: 'language',\n            },\n          },\n          postgateEmbeddingRules: {\n            description:\n              'Embedding rules for the postgates to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: ['lex:app.bsky.feed.postgate#disableRule'],\n            },\n          },\n          threadgateAllow: {\n            description:\n              'Allow-rules for the threadgate to be created when this draft is published.',\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.threadgate#mentionRule',\n                'lex:app.bsky.feed.threadgate#followerRule',\n                'lex:app.bsky.feed.threadgate#followingRule',\n                'lex:app.bsky.feed.threadgate#listRule',\n              ],\n            },\n          },\n        },\n      },\n      draftPost: {\n        description: 'One of the posts that compose a draft.',\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n            description:\n              '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.',\n          },\n          labels: {\n            type: 'union',\n            description:\n              'Self-label values for this post. Effectively content warnings.',\n            refs: ['lex:com.atproto.label.defs#selfLabels'],\n          },\n          embedImages: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedImage',\n            },\n            maxLength: 4,\n          },\n          embedVideos: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedVideo',\n            },\n            maxLength: 1,\n          },\n          embedExternals: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedExternal',\n            },\n            maxLength: 1,\n          },\n          embedRecords: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedRecord',\n            },\n            maxLength: 1,\n          },\n        },\n      },\n      draftView: {\n        description: 'View to present drafts data to users.',\n        type: 'object',\n        required: ['id', 'draft', 'createdAt', 'updatedAt'],\n        properties: {\n          id: {\n            description: 'A TID to be used as a draft identifier.',\n            type: 'string',\n            format: 'tid',\n          },\n          draft: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draft',\n          },\n          createdAt: {\n            description: 'The time the draft was created.',\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            description: 'The time the draft was last updated.',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      draftEmbedLocalRef: {\n        type: 'object',\n        required: ['path'],\n        properties: {\n          path: {\n            type: 'string',\n            description:\n              'Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts.',\n            minLength: 1,\n            maxLength: 1024,\n          },\n        },\n      },\n      draftEmbedCaption: {\n        type: 'object',\n        required: ['lang', 'content'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          content: {\n            type: 'string',\n            maxLength: 10000,\n          },\n        },\n      },\n      draftEmbedImage: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n        },\n      },\n      draftEmbedVideo: {\n        type: 'object',\n        required: ['localRef'],\n        properties: {\n          localRef: {\n            type: 'ref',\n            ref: 'lex:app.bsky.draft.defs#draftEmbedLocalRef',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 2000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.draft.defs#draftEmbedCaption',\n            },\n            maxLength: 20,\n          },\n        },\n      },\n      draftEmbedExternal: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      draftEmbedRecord: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftDeleteDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.deleteDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Deletes a draft by ID. Requires authentication.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftGetDrafts: {\n    lexicon: 1,\n    id: 'app.bsky.draft.getDrafts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets views of user drafts. Requires authentication.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['drafts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              drafts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.draft.defs#draftView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyDraftUpdateDraft: {\n    lexicon: 1,\n    id: 'app.bsky.draft.updateDraft',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"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.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['draft'],\n            properties: {\n              draft: {\n                type: 'ref',\n                ref: 'lex:app.bsky.draft.defs#draftWithId',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.embed.defs',\n    defs: {\n      aspectRatio: {\n        type: 'object',\n        description:\n          'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n            minimum: 1,\n          },\n          height: {\n            type: 'integer',\n            minimum: 1,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedExternal: {\n    lexicon: 1,\n    id: 'app.bsky.embed.external',\n    defs: {\n      main: {\n        type: 'object',\n        description:\n          \"A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).\",\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#external',\n          },\n        },\n      },\n      external: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['external'],\n        properties: {\n          external: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.external#viewExternal',\n          },\n        },\n      },\n      viewExternal: {\n        type: 'object',\n        required: ['uri', 'title', 'description'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n          title: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          thumb: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedImages: {\n    lexicon: 1,\n    id: 'app.bsky.embed.images',\n    description: 'A set of images embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#image',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      image: {\n        type: 'object',\n        required: ['image', 'alt'],\n        properties: {\n          image: {\n            type: 'blob',\n            accept: ['image/*'],\n            maxSize: 1000000,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['images'],\n        properties: {\n          images: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.images#viewImage',\n            },\n            maxLength: 4,\n          },\n        },\n      },\n      viewImage: {\n        type: 'object',\n        required: ['thumb', 'fullsize', 'alt'],\n        properties: {\n          thumb: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.',\n          },\n          fullsize: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.',\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the image, for accessibility.',\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecord: {\n    lexicon: 1,\n    id: 'app.bsky.embed.record',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record'],\n        properties: {\n          record: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.record#viewRecord',\n              'lex:app.bsky.embed.record#viewNotFound',\n              'lex:app.bsky.embed.record#viewBlocked',\n              'lex:app.bsky.embed.record#viewDetached',\n              'lex:app.bsky.feed.defs#generatorView',\n              'lex:app.bsky.graph.defs#listView',\n              'lex:app.bsky.labeler.defs#labelerView',\n              'lex:app.bsky.graph.defs#starterPackViewBasic',\n            ],\n          },\n        },\n      },\n      viewRecord: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'value', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          value: {\n            type: 'unknown',\n            description: 'The record data itself.',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          embeds: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images#view',\n                'lex:app.bsky.embed.video#view',\n                'lex:app.bsky.embed.external#view',\n                'lex:app.bsky.embed.record#view',\n                'lex:app.bsky.embed.recordWithMedia#view',\n              ],\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      viewNotFound: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      viewBlocked: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      viewDetached: {\n        type: 'object',\n        required: ['uri', 'detached'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          detached: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedRecordWithMedia: {\n    lexicon: 1,\n    id: 'app.bsky.embed.recordWithMedia',\n    description:\n      'A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images',\n              'lex:app.bsky.embed.video',\n              'lex:app.bsky.embed.external',\n            ],\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['record', 'media'],\n        properties: {\n          record: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.record#view',\n          },\n          media: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyEmbedVideo: {\n    lexicon: 1,\n    id: 'app.bsky.embed.video',\n    description: 'A video embedded in a Bluesky record (eg, a post).',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['video'],\n        properties: {\n          video: {\n            type: 'blob',\n            description:\n              'The mp4 video file. May be up to 100mb, formerly limited to 50mb.',\n            accept: ['video/mp4'],\n            maxSize: 100000000,\n          },\n          captions: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.embed.video#caption',\n            },\n            maxLength: 20,\n          },\n          alt: {\n            type: 'string',\n            description:\n              'Alt text description of the video, for accessibility.',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n      caption: {\n        type: 'object',\n        required: ['lang', 'file'],\n        properties: {\n          lang: {\n            type: 'string',\n            format: 'language',\n          },\n          file: {\n            type: 'blob',\n            accept: ['text/vtt'],\n            maxSize: 20000,\n          },\n        },\n      },\n      view: {\n        type: 'object',\n        required: ['cid', 'playlist'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          playlist: {\n            type: 'string',\n            format: 'uri',\n          },\n          thumbnail: {\n            type: 'string',\n            format: 'uri',\n          },\n          alt: {\n            type: 'string',\n            maxGraphemes: 1000,\n            maxLength: 10000,\n          },\n          aspectRatio: {\n            type: 'ref',\n            ref: 'lex:app.bsky.embed.defs#aspectRatio',\n          },\n          presentation: {\n            type: 'string',\n            description: 'A hint to the client about how to present the video.',\n            knownValues: ['default', 'gif'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.feed.defs',\n    defs: {\n      postView: {\n        type: 'object',\n        required: ['uri', 'cid', 'author', 'record', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          record: {\n            type: 'unknown',\n          },\n          embed: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.embed.images#view',\n              'lex:app.bsky.embed.video#view',\n              'lex:app.bsky.embed.external#view',\n              'lex:app.bsky.embed.record#view',\n              'lex:app.bsky.embed.recordWithMedia#view',\n            ],\n          },\n          bookmarkCount: {\n            type: 'integer',\n          },\n          replyCount: {\n            type: 'integer',\n          },\n          repostCount: {\n            type: 'integer',\n          },\n          likeCount: {\n            type: 'integer',\n          },\n          quoteCount: {\n            type: 'integer',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          threadgate: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadgateView',\n          },\n          debug: {\n            type: 'unknown',\n            description: 'Debug information for internal development',\n          },\n        },\n      },\n      viewerState: {\n        type: 'object',\n        description:\n          \"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.\",\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          bookmarked: {\n            type: 'boolean',\n          },\n          threadMuted: {\n            type: 'boolean',\n          },\n          replyDisabled: {\n            type: 'boolean',\n          },\n          embeddingDisabled: {\n            type: 'boolean',\n          },\n          pinned: {\n            type: 'boolean',\n          },\n        },\n      },\n      threadContext: {\n        type: 'object',\n        description:\n          'Metadata about this post within the context of the thread it is in.',\n        properties: {\n          rootAuthorLike: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      feedViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#replyRef',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#reasonRepost',\n              'lex:app.bsky.feed.defs#reasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context provided by feed generator that may be passed back alongside interactions.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#postView',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          grandparentAuthor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            description:\n              'When parent is a reply to another post, this is the author of that post.',\n          },\n        },\n      },\n      reasonRepost: {\n        type: 'object',\n        required: ['by', 'indexedAt'],\n        properties: {\n          by: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadViewPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          parent: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#threadViewPost',\n              'lex:app.bsky.feed.defs#notFoundPost',\n              'lex:app.bsky.feed.defs#blockedPost',\n            ],\n          },\n          replies: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.feed.defs#threadViewPost',\n                'lex:app.bsky.feed.defs#notFoundPost',\n                'lex:app.bsky.feed.defs#blockedPost',\n              ],\n            },\n          },\n          threadContext: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#threadContext',\n          },\n        },\n      },\n      notFoundPost: {\n        type: 'object',\n        required: ['uri', 'notFound'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      blockedPost: {\n        type: 'object',\n        required: ['uri', 'blocked', 'author'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          blocked: {\n            type: 'boolean',\n            const: true,\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      blockedAuthor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n        },\n      },\n      generatorView: {\n        type: 'object',\n        required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          acceptsInteractions: {\n            type: 'boolean',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#generatorViewerState',\n          },\n          contentMode: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#contentModeUnspecified',\n              'app.bsky.feed.defs#contentModeVideo',\n            ],\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      generatorViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonFeedPost: {\n        type: 'object',\n        required: ['post'],\n        properties: {\n          post: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          reason: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.feed.defs#skeletonReasonRepost',\n              'lex:app.bsky.feed.defs#skeletonReasonPin',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context that will be passed through to client and may be passed to feed generator back alongside interactions.',\n            maxLength: 2000,\n          },\n        },\n      },\n      skeletonReasonRepost: {\n        type: 'object',\n        required: ['repost'],\n        properties: {\n          repost: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonReasonPin: {\n        type: 'object',\n        properties: {},\n      },\n      threadgateView: {\n        type: 'object',\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          lists: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listViewBasic',\n            },\n          },\n        },\n      },\n      interaction: {\n        type: 'object',\n        properties: {\n          item: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          event: {\n            type: 'string',\n            knownValues: [\n              'app.bsky.feed.defs#requestLess',\n              'app.bsky.feed.defs#requestMore',\n              'app.bsky.feed.defs#clickthroughItem',\n              'app.bsky.feed.defs#clickthroughAuthor',\n              'app.bsky.feed.defs#clickthroughReposter',\n              'app.bsky.feed.defs#clickthroughEmbed',\n              'app.bsky.feed.defs#interactionSeen',\n              'app.bsky.feed.defs#interactionLike',\n              'app.bsky.feed.defs#interactionRepost',\n              'app.bsky.feed.defs#interactionReply',\n              'app.bsky.feed.defs#interactionQuote',\n              'app.bsky.feed.defs#interactionShare',\n            ],\n          },\n          feedContext: {\n            type: 'string',\n            description:\n              'Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton.',\n            maxLength: 2000,\n          },\n          reqId: {\n            type: 'string',\n            description:\n              'Unique identifier per request that may be passed back alongside interactions.',\n            maxLength: 100,\n          },\n        },\n      },\n      requestLess: {\n        type: 'token',\n        description:\n          'Request that less content like the given feed item be shown in the feed',\n      },\n      requestMore: {\n        type: 'token',\n        description:\n          'Request that more content like the given feed item be shown in the feed',\n      },\n      clickthroughItem: {\n        type: 'token',\n        description: 'User clicked through to the feed item',\n      },\n      clickthroughAuthor: {\n        type: 'token',\n        description: 'User clicked through to the author of the feed item',\n      },\n      clickthroughReposter: {\n        type: 'token',\n        description: 'User clicked through to the reposter of the feed item',\n      },\n      clickthroughEmbed: {\n        type: 'token',\n        description:\n          'User clicked through to the embedded content of the feed item',\n      },\n      contentModeUnspecified: {\n        type: 'token',\n        description: 'Declares the feed generator returns any types of posts.',\n      },\n      contentModeVideo: {\n        type: 'token',\n        description:\n          'Declares the feed generator returns posts containing app.bsky.embed.video embeds.',\n      },\n      interactionSeen: {\n        type: 'token',\n        description: 'Feed item was seen by user',\n      },\n      interactionLike: {\n        type: 'token',\n        description: 'User liked the feed item',\n      },\n      interactionRepost: {\n        type: 'token',\n        description: 'User reposted the feed item',\n      },\n      interactionReply: {\n        type: 'token',\n        description: 'User replied to the feed item',\n      },\n      interactionQuote: {\n        type: 'token',\n        description: 'User quoted the feed item',\n      },\n      interactionShare: {\n        type: 'token',\n        description: 'User shared the feed item',\n      },\n    },\n  },\n  AppBskyFeedDescribeFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.describeFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View).',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'feeds'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.describeFeedGenerator#feed',\n                },\n              },\n              links: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.describeFeedGenerator#links',\n              },\n            },\n          },\n        },\n      },\n      feed: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n          },\n          termsOfService: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.generator',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository.',\n        key: 'any',\n        record: {\n          type: 'object',\n          required: ['did', 'displayName', 'createdAt'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            displayName: {\n              type: 'string',\n              maxGraphemes: 24,\n              maxLength: 240,\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            acceptsInteractions: {\n              type: 'boolean',\n              description:\n                'Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions',\n            },\n            labels: {\n              type: 'union',\n              description: 'Self-label values',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            contentMode: {\n              type: 'string',\n              knownValues: [\n                'app.bsky.feed.defs#contentModeUnspecified',\n                'app.bsky.feed.defs#contentModeVideo',\n              ],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a list of feeds (feed generator records) created by the actor (in the actor's repo).\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetActorLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getActorLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of posts liked by an actor. Requires auth, actor must be the requesting account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetAuthorFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getAuthorFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.\",\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            filter: {\n              type: 'string',\n              description:\n                'Combinations of post/repost types to include in response.',\n              knownValues: [\n                'posts_with_replies',\n                'posts_no_replies',\n                'posts_with_media',\n                'posts_and_author_threads',\n                'posts_with_video',\n              ],\n              default: 'posts_with_replies',\n            },\n            includePins: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BlockedActor',\n          },\n          {\n            name: 'BlockedByActor',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a hydrated feed from an actor's selected feed generator. Implemented by App View.\",\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerator: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerator',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about a feed generator. Implemented by AppView.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the feed generator record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['view', 'isOnline', 'isValid'],\n            properties: {\n              view: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#generatorView',\n              },\n              isOnline: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service has been online recently, or else seems to be inactive.',\n              },\n              isValid: {\n                type: 'boolean',\n                description:\n                  'Indicates whether the feed generator service is compatible with the record declaration.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of feed generators.',\n        parameters: {\n          type: 'params',\n          required: ['feeds'],\n          properties: {\n            feeds: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetFeedSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getFeedSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service.',\n        parameters: {\n          type: 'params',\n          required: ['feed'],\n          properties: {\n            feed: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference to feed generator record describing the specific feed being requested.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#skeletonFeedPost',\n                },\n              },\n              reqId: {\n                type: 'string',\n                description:\n                  'Unique identifier per request that may be passed back alongside interactions.',\n                maxLength: 100,\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownFeed',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetLikes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getLikes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get like records which reference a subject (by AT-URI and CID).',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'AT-URI of the subject (eg, a post record).',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'CID of the subject record (aka, specific version of record), to filter likes.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'likes'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              likes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.getLikes#like',\n                },\n              },\n            },\n          },\n        },\n      },\n      like: {\n        type: 'object',\n        required: ['indexedAt', 'createdAt', 'actor'],\n        properties: {\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          actor: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetListFeed: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getListFeed',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'UnknownList',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPostThread: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPostThread',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to post record.',\n            },\n            depth: {\n              type: 'integer',\n              description:\n                'How many levels of reply depth should be included in response.',\n              default: 6,\n              minimum: 0,\n              maximum: 1000,\n            },\n            parentHeight: {\n              type: 'integer',\n              description:\n                'How many levels of parent (and grandparent, etc) post to include.',\n              default: 80,\n              minimum: 0,\n              maximum: 1000,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.defs#threadViewPost',\n                  'lex:app.bsky.feed.defs#notFoundPost',\n                  'lex:app.bsky.feed.defs#blockedPost',\n                ],\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'NotFound',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedGetPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.\",\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              description: 'List of post AT-URIs to return hydrated views for.',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetQuotes: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getQuotes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of quotes for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to quotes of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'posts'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetRepostedBy: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getRepostedBy',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of reposts for a given post.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of post record',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'If supplied, filters to reposts of specific version (by CID) of the post record.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'repostedBy'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              cursor: {\n                type: 'string',\n              },\n              repostedBy: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggested feeds (feed generators) for the requesting account.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedGetTimeline: {\n    lexicon: 1,\n    id: 'app.bsky.feed.getTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed.\",\n        parameters: {\n          type: 'params',\n          properties: {\n            algorithm: {\n              type: 'string',\n              description:\n                \"Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feed'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feed: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#feedViewPost',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedLike: {\n    lexicon: 1,\n    id: 'app.bsky.feed.like',\n    defs: {\n      main: {\n        type: 'record',\n        description: \"Record declaring a 'like' of a piece of subject content.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.post',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'Record containing a Bluesky post.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['text', 'createdAt'],\n          properties: {\n            text: {\n              type: 'string',\n              maxLength: 3000,\n              maxGraphemes: 300,\n              description:\n                'The primary post content. May be an empty string, if there are embeds.',\n            },\n            entities: {\n              type: 'array',\n              description: 'DEPRECATED: replaced by app.bsky.richtext.facet.',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.post#entity',\n              },\n            },\n            facets: {\n              type: 'array',\n              description:\n                'Annotations of text (mentions, URLs, hashtags, etc)',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            reply: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.post#replyRef',\n            },\n            embed: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.embed.images',\n                'lex:app.bsky.embed.video',\n                'lex:app.bsky.embed.external',\n                'lex:app.bsky.embed.record',\n                'lex:app.bsky.embed.recordWithMedia',\n              ],\n            },\n            langs: {\n              type: 'array',\n              description:\n                'Indicates human language of post primary text content.',\n              maxLength: 3,\n              items: {\n                type: 'string',\n                format: 'language',\n              },\n            },\n            labels: {\n              type: 'union',\n              description:\n                'Self-label values for this post. Effectively content warnings.',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            tags: {\n              type: 'array',\n              description:\n                'Additional hashtags, in addition to any included in post text and facets.',\n              maxLength: 8,\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Client-declared timestamp when this post was originally created.',\n            },\n          },\n        },\n      },\n      replyRef: {\n        type: 'object',\n        required: ['root', 'parent'],\n        properties: {\n          root: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n          parent: {\n            type: 'ref',\n            ref: 'lex:com.atproto.repo.strongRef',\n          },\n        },\n      },\n      entity: {\n        type: 'object',\n        description: 'Deprecated: use facets instead.',\n        required: ['index', 'type', 'value'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.post#textSlice',\n          },\n          type: {\n            type: 'string',\n            description: \"Expected values are 'mention' and 'link'.\",\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n      textSlice: {\n        type: 'object',\n        description:\n          'Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.',\n        required: ['start', 'end'],\n        properties: {\n          start: {\n            type: 'integer',\n            minimum: 0,\n          },\n          end: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedPostgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.postgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          'Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository.',\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            detachedEmbeddingUris: {\n              type: 'array',\n              maxLength: 50,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description:\n                'List of AT-URIs embedding this post that the author has detached from.',\n            },\n            embeddingRules: {\n              description:\n                'List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: ['lex:app.bsky.feed.postgate#disableRule'],\n              },\n            },\n          },\n        },\n      },\n      disableRule: {\n        type: 'object',\n        description: 'Disables embedding of this post.',\n        properties: {},\n      },\n    },\n  },\n  AppBskyFeedRepost: {\n    lexicon: 1,\n    id: 'app.bsky.feed.repost',\n    defs: {\n      main: {\n        description:\n          \"Record representing a 'repost' of an existing Bluesky post.\",\n        type: 'record',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedSearchPosts: {\n    lexicon: 1,\n    id: 'app.bsky.feed.searchPosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#postView',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyFeedSendInteractions: {\n    lexicon: 1,\n    id: 'app.bsky.feed.sendInteractions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Send information about interactions with feed items back to the feed generator that served them.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['interactions'],\n            properties: {\n              feed: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              interactions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#interaction',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  AppBskyFeedThreadgate: {\n    lexicon: 1,\n    id: 'app.bsky.feed.threadgate',\n    defs: {\n      main: {\n        type: 'record',\n        key: 'tid',\n        description:\n          \"Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.\",\n        record: {\n          type: 'object',\n          required: ['post', 'createdAt'],\n          properties: {\n            post: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the post record.',\n            },\n            allow: {\n              description:\n                'List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.',\n              type: 'array',\n              maxLength: 5,\n              items: {\n                type: 'union',\n                refs: [\n                  'lex:app.bsky.feed.threadgate#mentionRule',\n                  'lex:app.bsky.feed.threadgate#followerRule',\n                  'lex:app.bsky.feed.threadgate#followingRule',\n                  'lex:app.bsky.feed.threadgate#listRule',\n                ],\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            hiddenReplies: {\n              type: 'array',\n              maxLength: 300,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              description: 'List of hidden reply URIs.',\n            },\n          },\n        },\n      },\n      mentionRule: {\n        type: 'object',\n        description: 'Allow replies from actors mentioned in your post.',\n        properties: {},\n      },\n      followerRule: {\n        type: 'object',\n        description: 'Allow replies from actors who follow you.',\n        properties: {},\n      },\n      followingRule: {\n        type: 'object',\n        description: 'Allow replies from actors you follow.',\n        properties: {},\n      },\n      listRule: {\n        type: 'object',\n        description: 'Allow replies from actors on a list.',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphBlock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.block',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'DID of the account to be blocked.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphDefs: {\n    lexicon: 1,\n    id: 'app.bsky.graph.defs',\n    defs: {\n      listViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'name', 'purpose'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'name', 'purpose', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          name: {\n            type: 'string',\n            maxLength: 64,\n            minLength: 1,\n          },\n          purpose: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listPurpose',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 300,\n            maxLength: 3000,\n          },\n          descriptionFacets: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listItemView: {\n        type: 'object',\n        required: ['uri', 'subject'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n        },\n      },\n      starterPackView: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listViewBasic',\n          },\n          listItemsSample: {\n            type: 'array',\n            maxLength: 12,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.graph.defs#listItemView',\n            },\n          },\n          feeds: {\n            type: 'array',\n            maxLength: 3,\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.feed.defs#generatorView',\n            },\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      starterPackViewBasic: {\n        type: 'object',\n        required: ['uri', 'cid', 'record', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          record: {\n            type: 'unknown',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n          },\n          listItemCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedWeekCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          joinedAllTimeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      listPurpose: {\n        type: 'string',\n        knownValues: [\n          'app.bsky.graph.defs#modlist',\n          'app.bsky.graph.defs#curatelist',\n          'app.bsky.graph.defs#referencelist',\n        ],\n      },\n      modlist: {\n        type: 'token',\n        description:\n          'A list of actors to apply an aggregate moderation action (mute/block) on.',\n      },\n      curatelist: {\n        type: 'token',\n        description:\n          'A list of actors used for curation purposes such as list feeds or interaction gating.',\n      },\n      referencelist: {\n        type: 'token',\n        description:\n          'A list of actors used for only for reference purposes such as within a starter pack.',\n      },\n      listViewerState: {\n        type: 'object',\n        properties: {\n          muted: {\n            type: 'boolean',\n          },\n          blocked: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      notFoundActor: {\n        type: 'object',\n        description: 'indicates that a handle or DID could not be resolved',\n        required: ['actor', 'notFound'],\n        properties: {\n          actor: {\n            type: 'string',\n            format: 'at-identifier',\n          },\n          notFound: {\n            type: 'boolean',\n            const: true,\n          },\n        },\n      },\n      relationship: {\n        type: 'object',\n        description:\n          'lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          following: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor follows this DID, this is the AT-URI of the follow record',\n          },\n          followedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is followed by this DID, contains the AT-URI of the follow record',\n          },\n          blocking: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID, this is the AT-URI of the block record',\n          },\n          blockedBy: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID, contains the AT-URI of the block record',\n          },\n          blockingByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor blocks this DID via a block list, this is the AT-URI of the listblock record',\n          },\n          blockedByList: {\n            type: 'string',\n            format: 'at-uri',\n            description:\n              'if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphFollow: {\n    lexicon: 1,\n    id: 'app.bsky.graph.follow',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            via: {\n              type: 'ref',\n              ref: 'lex:com.atproto.repo.strongRef',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetActorStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getActorStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of starter packs created by the actor.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates which accounts the requesting account is currently blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blocks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blocks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetFollows: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getFollows',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which a specified account (actor) follows.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'follows'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              follows: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetKnownFollowers: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getKnownFollowers',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts which follow a specified account (actor) and are followed by the viewer.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'followers'],\n            properties: {\n              subject: {\n                type: 'ref',\n                ref: 'lex:app.bsky.actor.defs#profileView',\n              },\n              cursor: {\n                type: 'string',\n              },\n              followers: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getList',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Gets a 'view' (with additional context) of a specified list.\",\n        parameters: {\n          type: 'params',\n          required: ['list'],\n          properties: {\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the list record to hydrate.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list', 'items'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              list: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#listView',\n              },\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listItemView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListBlocks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get mod lists that the requesting account (actor) is blocking. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetLists: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getLists',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by a specified account (actor).',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to enumerate lists from.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['lists'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              lists: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#listView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetListsWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getListsWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            purposes: {\n              type: 'array',\n              description:\n                'Optional filter by list purpose. If not specified, all supported types are returned.',\n              items: {\n                type: 'string',\n                knownValues: ['modlist', 'curatelist'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['listsWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              listsWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getListsWithMembership#listWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      listWithMembership: {\n        description:\n          'A list and an optional list item indicating membership of a target user to that list.',\n        type: 'object',\n        required: ['list'],\n        properties: {\n          list: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetMutes: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getMutes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates accounts that the requesting account (actor) currently has muted. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['mutes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              mutes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetRelationships: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getRelationships',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates public relationships between one account, and a list of other accounts. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Primary account requesting relationships for.',\n            },\n            others: {\n              type: 'array',\n              description:\n                \"List of 'other' accounts to be related back to the primary.\",\n              maxLength: 30,\n              items: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['relationships'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              relationships: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.graph.defs#relationship',\n                    'lex:app.bsky.graph.defs#notFoundActor',\n                  ],\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ActorNotFound',\n            description:\n              'the primary actor at-identifier could not be resolved',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyGraphGetStarterPack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPack',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Gets a view of a starter pack.',\n        parameters: {\n          type: 'params',\n          required: ['starterPack'],\n          properties: {\n            starterPack: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) of the starter pack record.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPack'],\n            properties: {\n              starterPack: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.defs#starterPackView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get views for a list of starter packs.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              maxLength: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetStarterPacksWithMembership: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getStarterPacksWithMembership',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The account (actor) to check for membership.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacksWithMembership'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacksWithMembership: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership',\n                },\n              },\n            },\n          },\n        },\n      },\n      starterPackWithMembership: {\n        description:\n          'A starter pack and an optional list item indicating membership of a target user to that starter pack.',\n        type: 'object',\n        required: ['starterPack'],\n        properties: {\n          starterPack: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#starterPackView',\n          },\n          listItem: {\n            type: 'ref',\n            ref: 'lex:app.bsky.graph.defs#listItemView',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphGetSuggestedFollowsByActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.getSuggestedFollowsByActor',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account.',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'at-identifier',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n              isFallback: {\n                type: 'boolean',\n                description:\n                  'DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid',\n                default: false,\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.list',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'purpose', 'createdAt'],\n          properties: {\n            purpose: {\n              type: 'ref',\n              description:\n                'Defines the purpose of the list (aka, moderation-oriented or curration-oriented)',\n              ref: 'lex:app.bsky.graph.defs#listPurpose',\n            },\n            name: {\n              type: 'string',\n              maxLength: 64,\n              minLength: 1,\n              description: 'Display name for list; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            avatar: {\n              type: 'blob',\n              accept: ['image/png', 'image/jpeg'],\n              maxSize: 1000000,\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListblock: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listblock',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record representing a block relationship against an entire an entire list of accounts (actors).',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the mod list record.',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphListitem: {\n    lexicon: 1,\n    id: 'app.bsky.graph.listitem',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records.\",\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'list', 'createdAt'],\n          properties: {\n            subject: {\n              type: 'string',\n              format: 'did',\n              description: 'The account which is included on the list.',\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to the list record (app.bsky.graph.list).',\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified account. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphMuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.muteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphSearchStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.graph.searchStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find starter packs matching search criteria. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackViewBasic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphStarterpack: {\n    lexicon: 1,\n    id: 'app.bsky.graph.starterpack',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record defining a starter pack of actors and feeds for new users.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['name', 'list', 'createdAt'],\n          properties: {\n            name: {\n              type: 'string',\n              maxGraphemes: 50,\n              maxLength: 500,\n              minLength: 1,\n              description: 'Display name for starter pack; can not be empty.',\n            },\n            description: {\n              type: 'string',\n              maxGraphemes: 300,\n              maxLength: 3000,\n            },\n            descriptionFacets: {\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.richtext.facet',\n              },\n            },\n            list: {\n              type: 'string',\n              format: 'at-uri',\n              description: 'Reference (AT-URI) to the list record.',\n            },\n            feeds: {\n              type: 'array',\n              maxLength: 3,\n              items: {\n                type: 'ref',\n                ref: 'lex:app.bsky.graph.starterpack#feedItem',\n              },\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n      feedItem: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActor: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActor',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteActorList: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteActorList',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified list of accounts. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['list'],\n            properties: {\n              list: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphUnmuteThread: {\n    lexicon: 1,\n    id: 'app.bsky.graph.unmuteThread',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Unmutes the specified thread. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyGraphVerification: {\n    lexicon: 1,\n    id: 'app.bsky.graph.verification',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          'Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted.',\n        key: 'tid',\n        record: {\n          type: 'object',\n          required: ['subject', 'handle', 'displayName', 'createdAt'],\n          properties: {\n            subject: {\n              description: 'DID of the subject the verification applies to.',\n              type: 'string',\n              format: 'did',\n            },\n            handle: {\n              description:\n                'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n              type: 'string',\n              format: 'handle',\n            },\n            displayName: {\n              description:\n                'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n              type: 'string',\n            },\n            createdAt: {\n              description: 'Date of when the verification was created.',\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerDefs: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.defs',\n    defs: {\n      labelerView: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      labelerViewDetailed: {\n        type: 'object',\n        required: ['uri', 'cid', 'creator', 'policies', 'indexedAt'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          creator: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          policies: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n          },\n          likeCount: {\n            type: 'integer',\n            minimum: 0,\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.labeler.defs#labelerViewerState',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          reasonTypes: {\n            description:\n              \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#reasonType',\n            },\n          },\n          subjectTypes: {\n            description:\n              'The set of subject types (account, record, etc) this service accepts reports on.',\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.moderation.defs#subjectType',\n            },\n          },\n          subjectCollections: {\n            type: 'array',\n            description:\n              'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n            items: {\n              type: 'string',\n              format: 'nsid',\n            },\n          },\n        },\n      },\n      labelerViewerState: {\n        type: 'object',\n        properties: {\n          like: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      labelerPolicies: {\n        type: 'object',\n        required: ['labelValues'],\n        properties: {\n          labelValues: {\n            type: 'array',\n            description:\n              'The label values which this labeler publishes. May include global or custom labels.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValue',\n            },\n          },\n          labelValueDefinitions: {\n            type: 'array',\n            description:\n              'Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinition',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerGetServices: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.getServices',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get information about a list of labeler services.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            detailed: {\n              type: 'boolean',\n              default: false,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['views'],\n            properties: {\n              views: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:app.bsky.labeler.defs#labelerView',\n                    'lex:app.bsky.labeler.defs#labelerViewDetailed',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyLabelerService: {\n    lexicon: 1,\n    id: 'app.bsky.labeler.service',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of the existence of labeler service.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['policies', 'createdAt'],\n          properties: {\n            policies: {\n              type: 'ref',\n              ref: 'lex:app.bsky.labeler.defs#labelerPolicies',\n            },\n            labels: {\n              type: 'union',\n              refs: ['lex:com.atproto.label.defs#selfLabels'],\n            },\n            createdAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n            reasonTypes: {\n              description:\n                \"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.\",\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n            },\n            subjectTypes: {\n              description:\n                'The set of subject types (account, record, etc) this service accepts reports on.',\n              type: 'array',\n              items: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#subjectType',\n              },\n            },\n            subjectCollections: {\n              type: 'array',\n              description:\n                'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDeclaration: {\n    lexicon: 1,\n    id: 'app.bsky.notification.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"A declaration of the user's choices related to notifications that can be produced by them.\",\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowSubscriptions'],\n          properties: {\n            allowSubscriptions: {\n              type: 'string',\n              description:\n                \"A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.\",\n              knownValues: ['followers', 'mutuals', 'none'],\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationDefs: {\n    lexicon: 1,\n    id: 'app.bsky.notification.defs',\n    defs: {\n      recordDeleted: {\n        type: 'object',\n        properties: {},\n      },\n      chatPreference: {\n        type: 'object',\n        required: ['include', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'accepted'],\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      filterablePreference: {\n        type: 'object',\n        required: ['include', 'list', 'push'],\n        properties: {\n          include: {\n            type: 'string',\n            knownValues: ['all', 'follows'],\n          },\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preference: {\n        type: 'object',\n        required: ['list', 'push'],\n        properties: {\n          list: {\n            type: 'boolean',\n          },\n          push: {\n            type: 'boolean',\n          },\n        },\n      },\n      preferences: {\n        type: 'object',\n        required: [\n          'chat',\n          'follow',\n          'like',\n          'likeViaRepost',\n          'mention',\n          'quote',\n          'reply',\n          'repost',\n          'repostViaRepost',\n          'starterpackJoined',\n          'subscribedPost',\n          'unverified',\n          'verified',\n        ],\n        properties: {\n          chat: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#chatPreference',\n          },\n          follow: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          like: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          likeViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          mention: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          quote: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          reply: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          repostViaRepost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#filterablePreference',\n          },\n          starterpackJoined: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          subscribedPost: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          unverified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n          verified: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#preference',\n          },\n        },\n      },\n      activitySubscription: {\n        type: 'object',\n        required: ['post', 'reply'],\n        properties: {\n          post: {\n            type: 'boolean',\n          },\n          reply: {\n            type: 'boolean',\n          },\n        },\n      },\n      subjectActivitySubscription: {\n        description:\n          'Object used to store activity subscription data in stash.',\n        type: 'object',\n        required: ['subject', 'activitySubscription'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          activitySubscription: {\n            type: 'ref',\n            ref: 'lex:app.bsky.notification.defs#activitySubscription',\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getPreferences',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get notification-related preferences for an account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationGetUnreadCount: {\n    lexicon: 1,\n    id: 'app.bsky.notification.getUnreadCount',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Count the number of unread notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            priority: {\n              type: 'boolean',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['count'],\n            properties: {\n              count: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListActivitySubscriptions: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listActivitySubscriptions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subscriptions'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subscriptions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationListNotifications: {\n    lexicon: 1,\n    id: 'app.bsky.notification.listNotifications',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerate notifications for the requesting account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            reasons: {\n              description: 'Notification reasons to include in response.',\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'A reason that matches the reason property of #notification.',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            priority: {\n              type: 'boolean',\n            },\n            cursor: {\n              type: 'string',\n            },\n            seenAt: {\n              type: 'string',\n              format: 'datetime',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['notifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              notifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.notification.listNotifications#notification',\n                },\n              },\n              priority: {\n                type: 'boolean',\n              },\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      notification: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'author',\n          'reason',\n          'record',\n          'isRead',\n          'indexedAt',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileView',\n          },\n          reason: {\n            type: 'string',\n            description:\n              'The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.',\n            knownValues: [\n              'like',\n              'repost',\n              'follow',\n              'mention',\n              'reply',\n              'quote',\n              'starterpack-joined',\n              'verified',\n              'unverified',\n              'like-via-repost',\n              'repost-via-repost',\n              'subscribed-post',\n              'contact-match',\n            ],\n          },\n          reasonSubject: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          record: {\n            type: 'unknown',\n          },\n          isRead: {\n            type: 'boolean',\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutActivitySubscription: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putActivitySubscription',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'activitySubscription'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'string',\n                format: 'did',\n              },\n              activitySubscription: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#activitySubscription',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferences: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferences',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['priority'],\n            properties: {\n              priority: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationPutPreferencesV2: {\n    lexicon: 1,\n    id: 'app.bsky.notification.putPreferencesV2',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Set notification-related preferences for an account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              chat: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#chatPreference',\n              },\n              follow: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              like: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              likeViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              mention: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              quote: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              reply: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              repostViaRepost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#filterablePreference',\n              },\n              starterpackJoined: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              subscribedPost: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              unverified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n              verified: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preference',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['preferences'],\n            properties: {\n              preferences: {\n                type: 'ref',\n                ref: 'lex:app.bsky.notification.defs#preferences',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationRegisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.registerPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Register to receive push notifications, via a specified service, for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n              ageRestricted: {\n                type: 'boolean',\n                description: 'Set to true when the actor is age restricted',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUnregisterPush: {\n    lexicon: 1,\n    id: 'app.bsky.notification.unregisterPush',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['serviceDid', 'token', 'platform', 'appId'],\n            properties: {\n              serviceDid: {\n                type: 'string',\n                format: 'did',\n              },\n              token: {\n                type: 'string',\n              },\n              platform: {\n                type: 'string',\n                knownValues: ['ios', 'android', 'web'],\n              },\n              appId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyNotificationUpdateSeen: {\n    lexicon: 1,\n    id: 'app.bsky.notification.updateSeen',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify server that the requesting account has seen notifications. Requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['seenAt'],\n            properties: {\n              seenAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyRichtextFacet: {\n    lexicon: 1,\n    id: 'app.bsky.richtext.facet',\n    defs: {\n      main: {\n        type: 'object',\n        description: 'Annotation of a sub-string within rich text.',\n        required: ['index', 'features'],\n        properties: {\n          index: {\n            type: 'ref',\n            ref: 'lex:app.bsky.richtext.facet#byteSlice',\n          },\n          features: {\n            type: 'array',\n            items: {\n              type: 'union',\n              refs: [\n                'lex:app.bsky.richtext.facet#mention',\n                'lex:app.bsky.richtext.facet#link',\n                'lex:app.bsky.richtext.facet#tag',\n              ],\n            },\n          },\n        },\n      },\n      mention: {\n        type: 'object',\n        description:\n          \"Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.\",\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      link: {\n        type: 'object',\n        description:\n          'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      tag: {\n        type: 'object',\n        description:\n          \"Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').\",\n        required: ['tag'],\n        properties: {\n          tag: {\n            type: 'string',\n            maxLength: 640,\n            maxGraphemes: 64,\n          },\n        },\n      },\n      byteSlice: {\n        type: 'object',\n        description:\n          'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.',\n        required: ['byteStart', 'byteEnd'],\n        properties: {\n          byteStart: {\n            type: 'integer',\n            minimum: 0,\n          },\n          byteEnd: {\n            type: 'integer',\n            minimum: 0,\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedDefs: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.defs',\n    defs: {\n      skeletonSearchPost: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      skeletonSearchActor: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      skeletonSearchStarterPack: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      trendingTopic: {\n        type: 'object',\n        required: ['topic', 'link'],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          description: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n        },\n      },\n      skeletonTrend: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'dids',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          dids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n      },\n      trendView: {\n        type: 'object',\n        required: [\n          'topic',\n          'displayName',\n          'link',\n          'startedAt',\n          'postCount',\n          'actors',\n        ],\n        properties: {\n          topic: {\n            type: 'string',\n          },\n          displayName: {\n            type: 'string',\n          },\n          link: {\n            type: 'string',\n          },\n          startedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          postCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['hot'],\n          },\n          category: {\n            type: 'string',\n          },\n          actors: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.actor.defs#profileViewBasic',\n            },\n          },\n        },\n      },\n      threadItemPost: {\n        type: 'object',\n        required: [\n          'post',\n          'moreParents',\n          'moreReplies',\n          'opThread',\n          'hiddenByThreadgate',\n          'mutedByViewer',\n        ],\n        properties: {\n          post: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#postView',\n          },\n          moreParents: {\n            type: 'boolean',\n            description:\n              'This post has more parents that were not present in the response. This is just a boolean, without the number of parents.',\n          },\n          moreReplies: {\n            type: 'integer',\n            description:\n              'This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate.',\n          },\n          opThread: {\n            type: 'boolean',\n            description:\n              'This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread.',\n          },\n          hiddenByThreadgate: {\n            type: 'boolean',\n            description:\n              'The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.',\n          },\n          mutedByViewer: {\n            type: 'boolean',\n            description:\n              'This is by an account muted by the viewer requesting it.',\n          },\n        },\n      },\n      threadItemNoUnauthenticated: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemNotFound: {\n        type: 'object',\n        properties: {},\n      },\n      threadItemBlocked: {\n        type: 'object',\n        required: ['author'],\n        properties: {\n          author: {\n            type: 'ref',\n            ref: 'lex:app.bsky.feed.defs#blockedAuthor',\n          },\n        },\n      },\n      ageAssuranceState: {\n        type: 'object',\n        description:\n          'The computed state of the age assurance process, returned to the user in question on certain authenticated requests.',\n        required: ['status'],\n        properties: {\n          lastInitiatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The timestamp when this state was last updated.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured', 'blocked'],\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description: 'Object used to store age assurance data in stash.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the age assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          email: {\n            type: 'string',\n            description: 'The email used for AA.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetAgeAssuranceState: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getAgeAssuranceState',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetConfig: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get miscellaneous runtime configuration.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              checkEmailConfirmed: {\n                type: 'boolean',\n              },\n              liveNow: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getConfig#liveNowConfig',\n                },\n              },\n            },\n          },\n        },\n      },\n      liveNowConfig: {\n        type: 'object',\n        required: ['did', 'domains'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          domains: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users for onboarding. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedOnboardingUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPopularFeedGenerators: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPopularFeedGenerators',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'An unspecced view of globally popular feed generators.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            query: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadOtherV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadOtherV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadOtherV2#threadItem',\n                },\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: ['lex:app.bsky.unspecced.defs#threadItemPost'],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetPostThreadV2: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getPostThreadV2',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.\",\n        parameters: {\n          type: 'params',\n          required: ['anchor'],\n          properties: {\n            anchor: {\n              type: 'string',\n              format: 'at-uri',\n              description:\n                'Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post.',\n            },\n            above: {\n              type: 'boolean',\n              description: 'Whether to include parents above the anchor.',\n              default: true,\n            },\n            below: {\n              type: 'integer',\n              description:\n                'How many levels of replies to include below the anchor.',\n              default: 6,\n              minimum: 0,\n              maximum: 20,\n            },\n            branchingFactor: {\n              type: 'integer',\n              description:\n                'Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).',\n              default: 10,\n              minimum: 0,\n              maximum: 100,\n            },\n            sort: {\n              type: 'string',\n              description: 'Sorting for the thread replies.',\n              knownValues: ['newest', 'oldest', 'top'],\n              default: 'oldest',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['thread', 'hasOtherReplies'],\n            properties: {\n              thread: {\n                type: 'array',\n                description:\n                  'A flat list of thread items. The depth of each item is indicated by the depth property inside the item.',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getPostThreadV2#threadItem',\n                },\n              },\n              threadgate: {\n                type: 'ref',\n                ref: 'lex:app.bsky.feed.defs#threadgateView',\n              },\n              hasOtherReplies: {\n                type: 'boolean',\n                description:\n                  'Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them.',\n              },\n            },\n          },\n        },\n      },\n      threadItem: {\n        type: 'object',\n        required: ['uri', 'depth', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          depth: {\n            type: 'integer',\n            description:\n              'The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths.',\n          },\n          value: {\n            type: 'union',\n            refs: [\n              'lex:app.bsky.unspecced.defs#threadItemPost',\n              'lex:app.bsky.unspecced.defs#threadItemNoUnauthenticated',\n              'lex:app.bsky.unspecced.defs#threadItemNotFound',\n              'lex:app.bsky.unspecced.defs#threadItemBlocked',\n            ],\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeeds: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeeds',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested feeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.feed.defs#generatorView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['feeds'],\n            properties: {\n              feeds: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedOnboardingUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedOnboardingUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users for onboarding',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacks: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacks',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested starterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.graph.defs#starterPackView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsers: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of suggested users',\n        parameters: {\n          type: 'params',\n          properties: {\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.actor.defs#profileView',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestedUsersSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestedUsersSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            category: {\n              type: 'string',\n              description: 'Category of users to get suggestions for.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 50,\n              default: 25,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['dids'],\n            properties: {\n              dids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n              recId: {\n                type: 'string',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetSuggestionsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getSuggestionsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            relativeToDid: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n              relativeToDid: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer.',\n              },\n              recId: {\n                type: 'integer',\n                description: 'DEPRECATED: use recIdStr instead.',\n              },\n              recIdStr: {\n                type: 'string',\n                description:\n                  'Snowflake for this recommendation, use when submitting recommendation events.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTaggedSuggestions: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTaggedSuggestions',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a list of suggestions (feeds and users) tagged with categories',\n        parameters: {\n          type: 'params',\n          properties: {},\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['suggestions'],\n            properties: {\n              suggestions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',\n                },\n              },\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['tag', 'subjectType', 'subject'],\n        properties: {\n          tag: {\n            type: 'string',\n          },\n          subjectType: {\n            type: 'string',\n            knownValues: ['actor', 'feed'],\n          },\n          subject: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendingTopics: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendingTopics',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a list of trending topics',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['topics', 'suggested'],\n            properties: {\n              topics: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n              suggested: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendingTopic',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrends: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrends',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get the current trends on the network',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#trendView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedGetTrendsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.getTrendsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends',\n        parameters: {\n          type: 'params',\n          properties: {\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 25,\n              default: 10,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['trends'],\n            properties: {\n              trends: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonTrend',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyUnspeccedInitAgeAssurance: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.initAgeAssurance',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'language', 'countryCode'],\n            properties: {\n              email: {\n                type: 'string',\n                description:\n                  \"The user's email address to receive assurance instructions.\",\n              },\n              language: {\n                type: 'string',\n                description:\n                  \"The user's preferred language for communication during the assurance process.\",\n              },\n              countryCode: {\n                type: 'string',\n                description:\n                  \"An ISO 3166-1 alpha-2 code of the user's location.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:app.bsky.unspecced.defs#ageAssuranceState',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n          },\n          {\n            name: 'DidTooLong',\n          },\n          {\n            name: 'InvalidInitiation',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchActorsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchActorsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Actors (profile) search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking.',\n            },\n            typeahead: {\n              type: 'boolean',\n              description: \"If true, acts as fast/simple 'typeahead' query.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actors'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              actors: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchPostsSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchPostsSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Posts search, returns only skeleton',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            sort: {\n              type: 'string',\n              knownValues: ['top', 'latest'],\n              default: 'latest',\n              description: 'Specifies the ranking order of results.',\n            },\n            since: {\n              type: 'string',\n              description:\n                \"Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD).\",\n            },\n            until: {\n              type: 'string',\n              description:\n                \"Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD).\",\n            },\n            mentions: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions.',\n            },\n            author: {\n              type: 'string',\n              format: 'at-identifier',\n              description:\n                'Filter to posts by the given account. Handles are resolved to DID before query-time.',\n            },\n            lang: {\n              type: 'string',\n              format: 'language',\n              description:\n                'Filter to posts in the given language. Expected to be based on post language field, though server may override language detection.',\n            },\n            domain: {\n              type: 'string',\n              description:\n                'Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization.',\n            },\n            url: {\n              type: 'string',\n              format: 'uri',\n              description:\n                'Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching.',\n            },\n            tag: {\n              type: 'array',\n              items: {\n                type: 'string',\n                maxLength: 640,\n                maxGraphemes: 64,\n              },\n              description:\n                \"Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching.\",\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                \"DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['posts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              posts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyUnspeccedSearchStarterPacksSkeleton: {\n    lexicon: 1,\n    id: 'app.bsky.unspecced.searchStarterPacksSkeleton',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Backend Starter Pack search, returns only skeleton.',\n        parameters: {\n          type: 'params',\n          required: ['q'],\n          properties: {\n            q: {\n              type: 'string',\n              description:\n                'Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended.',\n            },\n            viewer: {\n              type: 'string',\n              format: 'did',\n              description:\n                'DID of the account making the request (not included for public/unauthenticated queries).',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 25,\n            },\n            cursor: {\n              type: 'string',\n              description:\n                'Optional pagination mechanism; may not necessarily allow scrolling through entire result set.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['starterPacks'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hitsTotal: {\n                type: 'integer',\n                description:\n                  'Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits.',\n              },\n              starterPacks: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:app.bsky.unspecced.defs#skeletonSearchStarterPack',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadQueryString',\n          },\n        ],\n      },\n    },\n  },\n  AppBskyVideoDefs: {\n    lexicon: 1,\n    id: 'app.bsky.video.defs',\n    defs: {\n      jobStatus: {\n        type: 'object',\n        required: ['jobId', 'did', 'state'],\n        properties: {\n          jobId: {\n            type: 'string',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          state: {\n            type: 'string',\n            description:\n              'The state of the video processing job. All values not listed as a known value indicate that the job is in process.',\n            knownValues: ['JOB_STATE_COMPLETED', 'JOB_STATE_FAILED'],\n          },\n          progress: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n            description: 'Progress within the current processing state.',\n          },\n          blob: {\n            type: 'blob',\n          },\n          error: {\n            type: 'string',\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetJobStatus: {\n    lexicon: 1,\n    id: 'app.bsky.video.getJobStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get status details for a video processing job.',\n        parameters: {\n          type: 'params',\n          required: ['jobId'],\n          properties: {\n            jobId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoGetUploadLimits: {\n    lexicon: 1,\n    id: 'app.bsky.video.getUploadLimits',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get video upload limits for the authenticated user.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canUpload'],\n            properties: {\n              canUpload: {\n                type: 'boolean',\n              },\n              remainingDailyVideos: {\n                type: 'integer',\n              },\n              remainingDailyBytes: {\n                type: 'integer',\n              },\n              message: {\n                type: 'string',\n              },\n              error: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  AppBskyVideoUploadVideo: {\n    lexicon: 1,\n    id: 'app.bsky.video.uploadVideo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Upload a video to be processed then stored on the PDS.',\n        input: {\n          encoding: 'video/mp4',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['jobStatus'],\n            properties: {\n              jobStatus: {\n                type: 'ref',\n                ref: 'lex:app.bsky.video.defs#jobStatus',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeclaration: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.declaration',\n    defs: {\n      main: {\n        type: 'record',\n        description: 'A declaration of a Bluesky chat account.',\n        key: 'literal:self',\n        record: {\n          type: 'object',\n          required: ['allowIncoming'],\n          properties: {\n            allowIncoming: {\n              type: 'string',\n              knownValues: ['all', 'none', 'following'],\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.defs',\n    defs: {\n      profileViewBasic: {\n        type: 'object',\n        required: ['did', 'handle'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          avatar: {\n            type: 'string',\n            format: 'uri',\n          },\n          associated: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileAssociated',\n          },\n          viewer: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#viewerState',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          chatDisabled: {\n            type: 'boolean',\n            description:\n              'Set to true when the actor cannot actively participate in conversations',\n          },\n          verification: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#verificationState',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorDeleteAccount: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ChatBskyActorExportAccountData: {\n    lexicon: 1,\n    id: 'chat.bsky.actor.exportAccountData',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/jsonl',\n        },\n      },\n    },\n  },\n  ChatBskyConvoAcceptConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.acceptConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rev: {\n                description:\n                  'Rev when the convo was accepted. If not present, the convo was already accepted.',\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoAddReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.addReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionLimitReached',\n            description:\n              \"Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent.\",\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoDefs: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.defs',\n    defs: {\n      messageRef: {\n        type: 'object',\n        required: ['did', 'messageId', 'convoId'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          convoId: {\n            type: 'string',\n          },\n          messageId: {\n            type: 'string',\n          },\n        },\n      },\n      messageInput: {\n        type: 'object',\n        required: ['text'],\n        properties: {\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record'],\n          },\n        },\n      },\n      messageView: {\n        type: 'object',\n        required: ['id', 'rev', 'text', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          text: {\n            type: 'string',\n            maxLength: 10000,\n            maxGraphemes: 1000,\n          },\n          facets: {\n            type: 'array',\n            description: 'Annotations of text (mentions, URLs, hashtags, etc)',\n            items: {\n              type: 'ref',\n              ref: 'lex:app.bsky.richtext.facet',\n            },\n          },\n          embed: {\n            type: 'union',\n            refs: ['lex:app.bsky.embed.record#view'],\n          },\n          reactions: {\n            type: 'array',\n            description:\n              'Reactions to this message, in ascending order of creation time.',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.convo.defs#reactionView',\n            },\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      deletedMessageView: {\n        type: 'object',\n        required: ['id', 'rev', 'sender', 'sentAt'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageViewSender',\n          },\n          sentAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      messageViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      reactionView: {\n        type: 'object',\n        required: ['value', 'sender', 'createdAt'],\n        properties: {\n          value: {\n            type: 'string',\n          },\n          sender: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionViewSender',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reactionViewSender: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      messageAndReactionView: {\n        type: 'object',\n        required: ['message', 'reaction'],\n        properties: {\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      convoView: {\n        type: 'object',\n        required: ['id', 'rev', 'members', 'muted', 'unreadCount'],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          rev: {\n            type: 'string',\n          },\n          members: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:chat.bsky.actor.defs#profileViewBasic',\n            },\n          },\n          lastMessage: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          lastReaction: {\n            type: 'union',\n            refs: ['lex:chat.bsky.convo.defs#messageAndReactionView'],\n          },\n          muted: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['request', 'accepted'],\n          },\n          unreadCount: {\n            type: 'integer',\n          },\n        },\n      },\n      logBeginConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logAcceptConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logLeaveConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logMuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logUnmuteConvo: {\n        type: 'object',\n        required: ['rev', 'convoId'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n        },\n      },\n      logCreateMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logDeleteMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logReadMessage: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n        },\n      },\n      logAddReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n      logRemoveReaction: {\n        type: 'object',\n        required: ['rev', 'convoId', 'message', 'reaction'],\n        properties: {\n          rev: {\n            type: 'string',\n          },\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'union',\n            refs: [\n              'lex:chat.bsky.convo.defs#messageView',\n              'lex:chat.bsky.convo.defs#deletedMessageView',\n            ],\n          },\n          reaction: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#reactionView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoDeleteMessageForSelf: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.deleteMessageForSelf',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#deletedMessageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvo',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoAvailability: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get whether the requester and the other members can chat. If an existing convo is found for these members, it is returned.',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['canChat'],\n            properties: {\n              canChat: {\n                type: 'boolean',\n              },\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetConvoForMembers: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getConvoForMembers',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['members'],\n          properties: {\n            members: {\n              type: 'array',\n              minLength: 1,\n              maxLength: 10,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetLog: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getLog',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: [],\n          properties: {\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['logs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              logs: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#logBeginConvo',\n                    'lex:chat.bsky.convo.defs#logAcceptConvo',\n                    'lex:chat.bsky.convo.defs#logLeaveConvo',\n                    'lex:chat.bsky.convo.defs#logMuteConvo',\n                    'lex:chat.bsky.convo.defs#logUnmuteConvo',\n                    'lex:chat.bsky.convo.defs#logCreateMessage',\n                    'lex:chat.bsky.convo.defs#logDeleteMessage',\n                    'lex:chat.bsky.convo.defs#logReadMessage',\n                    'lex:chat.bsky.convo.defs#logAddReaction',\n                    'lex:chat.bsky.convo.defs#logRemoveReaction',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoGetMessages: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.getMessages',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['convoId'],\n          properties: {\n            convoId: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoLeaveConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.leaveConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'rev'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              rev: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoListConvos: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.listConvos',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            readState: {\n              type: 'string',\n              knownValues: ['unread'],\n            },\n            status: {\n              type: 'string',\n              knownValues: ['request', 'accepted'],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              convos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#convoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoMuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.muteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoRemoveReaction: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.removeReaction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'messageId', 'value'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n              value: {\n                type: 'string',\n                minLength: 1,\n                maxLength: 64,\n                minGraphemes: 1,\n                maxGraphemes: 1,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: {\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageView',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ReactionMessageDeleted',\n            description:\n              'Indicates that the message has been deleted and reactions can no longer be added/removed.',\n          },\n          {\n            name: 'ReactionInvalidValue',\n            description:\n              'Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji.',\n          },\n        ],\n      },\n    },\n  },\n  ChatBskyConvoSendMessage: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessage',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId', 'message'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              message: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#messageInput',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageView',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoSendMessageBatch: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.sendMessageBatch',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.sendMessageBatch#batchItem',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['items'],\n            properties: {\n              items: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:chat.bsky.convo.defs#messageView',\n                },\n              },\n            },\n          },\n        },\n      },\n      batchItem: {\n        type: 'object',\n        required: ['convoId', 'message'],\n        properties: {\n          convoId: {\n            type: 'string',\n          },\n          message: {\n            type: 'ref',\n            ref: 'lex:chat.bsky.convo.defs#messageInput',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUnmuteConvo: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.unmuteConvo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateAllRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateAllRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              status: {\n                type: 'string',\n                knownValues: ['request', 'accepted'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['updatedCount'],\n            properties: {\n              updatedCount: {\n                description: 'The count of updated convos.',\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyConvoUpdateRead: {\n    lexicon: 1,\n    id: 'chat.bsky.convo.updateRead',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convoId'],\n            properties: {\n              convoId: {\n                type: 'string',\n              },\n              messageId: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['convo'],\n            properties: {\n              convo: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.convo.defs#convoView',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetActorMetadata: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getActorMetadata',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['actor'],\n          properties: {\n            actor: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['day', 'month', 'all'],\n            properties: {\n              day: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              month: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n              all: {\n                type: 'ref',\n                ref: 'lex:chat.bsky.moderation.getActorMetadata#metadata',\n              },\n            },\n          },\n        },\n      },\n      metadata: {\n        type: 'object',\n        required: [\n          'messagesSent',\n          'messagesReceived',\n          'convos',\n          'convosStarted',\n        ],\n        properties: {\n          messagesSent: {\n            type: 'integer',\n          },\n          messagesReceived: {\n            type: 'integer',\n          },\n          convos: {\n            type: 'integer',\n          },\n          convosStarted: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationGetMessageContext: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.getMessageContext',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['messageId'],\n          properties: {\n            convoId: {\n              type: 'string',\n              description:\n                'Conversation that the message is from. NOTE: this field will eventually be required.',\n            },\n            messageId: {\n              type: 'string',\n            },\n            before: {\n              type: 'integer',\n              default: 5,\n            },\n            after: {\n              type: 'integer',\n              default: 5,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['messages'],\n            properties: {\n              messages: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:chat.bsky.convo.defs#messageView',\n                    'lex:chat.bsky.convo.defs#deletedMessageView',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ChatBskyModerationUpdateActorAccess: {\n    lexicon: 1,\n    id: 'chat.bsky.moderation.updateActorAccess',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actor', 'allowAccess'],\n            properties: {\n              actor: {\n                type: 'string',\n                format: 'did',\n              },\n              allowAccess: {\n                type: 'boolean',\n              },\n              ref: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDefs: {\n    lexicon: 1,\n    id: 'com.atproto.admin.defs',\n    defs: {\n      statusAttr: {\n        type: 'object',\n        required: ['applied'],\n        properties: {\n          applied: {\n            type: 'boolean',\n          },\n          ref: {\n            type: 'string',\n          },\n        },\n      },\n      accountView: {\n        type: 'object',\n        required: ['did', 'handle', 'indexedAt'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoRef: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      repoBlobRef: {\n        type: 'object',\n        required: ['did', 'cid'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      threatSignature: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.admin.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable an account from receiving new invite codes, but does not invalidate existing codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for disabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminDisableInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.disableInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Disable some set of codes and/or all codes associated with a set of users.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminEnableAccountInvites: {\n    lexicon: 1,\n    id: 'com.atproto.admin.enableAccountInvites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Re-enable an account's ability to receive invite codes.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'did',\n              },\n              note: {\n                type: 'string',\n                description: 'Optional reason for enabled invites.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfo: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about an account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetAccountInfos: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getAccountInfos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['infos'],\n            properties: {\n              infos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get an admin view of invite codes.',\n        parameters: {\n          type: 'params',\n          properties: {\n            sort: {\n              type: 'string',\n              knownValues: ['recent', 'usage'],\n              default: 'recent',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 500,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminGetSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.getSubjectStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the service-specific admin status of a subject (account, record, or blob).',\n        parameters: {\n          type: 'params',\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            blob: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSearchAccounts: {\n    lexicon: 1,\n    id: 'com.atproto.admin.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of accounts that matches your search query.',\n        parameters: {\n          type: 'params',\n          properties: {\n            email: {\n              type: 'string',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminSendEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.sendEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Send email to a user's account email address.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['recipientDid', 'content', 'senderDid'],\n            properties: {\n              recipientDid: {\n                type: 'string',\n                format: 'did',\n              },\n              content: {\n                type: 'string',\n              },\n              subject: {\n                type: 'string',\n              },\n              senderDid: {\n                type: 'string',\n                format: 'did',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  \"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sent'],\n            properties: {\n              sent: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountEmail: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account', 'email'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n                description: 'The handle or DID of the repo.',\n              },\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountHandle: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Administrative action to update an account's handle.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'handle'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountPassword: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the password for a user account as an administrator.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateAccountSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateAccountSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Administrative action to update an account's signing key in their Did document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'signingKey'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              signingKey: {\n                type: 'string',\n                format: 'did',\n                description: 'Did-key formatted public key',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoAdminUpdateSubjectStatus: {\n    lexicon: 1,\n    id: 'com.atproto.admin.updateSubjectStatus',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update the service-specific admin status of a subject (account, record, or blob).',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n              deactivated: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject'],\n            properties: {\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                  'lex:com.atproto.admin.defs#repoBlobRef',\n                ],\n              },\n              takedown: {\n                type: 'ref',\n                ref: 'lex:com.atproto.admin.defs#statusAttr',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityDefs: {\n    lexicon: 1,\n    id: 'com.atproto.identity.defs',\n    defs: {\n      identityInfo: {\n        type: 'object',\n        required: ['did', 'handle', 'didDoc'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.\",\n          },\n          didDoc: {\n            type: 'unknown',\n            description: 'The complete DID document for the identity.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityGetRecommendedDidCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.identity.getRecommendedDidCredentials',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Describe the credentials that should be included in the DID doc of an account that is migrating to this service.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              rotationKeys: {\n                description:\n                  'Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.',\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityRefreshIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.refreshIdentity',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier'],\n            properties: {\n              identifier: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityRequestPlcOperationSignature: {\n    lexicon: 1,\n    id: 'com.atproto.identity.requestPlcOperationSignature',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to in order to request a signed PLC operation. Requires Auth.',\n      },\n    },\n  },\n  ComAtprotoIdentityResolveDid: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveDid',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves DID to DID document. Does not bi-directionally verify handle.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['didDoc'],\n            properties: {\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for the identity.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveHandle',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description: 'The handle to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentityResolveIdentity: {\n    lexicon: 1,\n    id: 'com.atproto.identity.resolveIdentity',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).',\n        parameters: {\n          type: 'params',\n          required: ['identifier'],\n          properties: {\n            identifier: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'Handle or DID to resolve.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.identity.defs#identityInfo',\n          },\n        },\n        errors: [\n          {\n            name: 'HandleNotFound',\n            description:\n              'The resolution process confirmed that the handle does not resolve to any DID.',\n          },\n          {\n            name: 'DidNotFound',\n            description:\n              'The DID resolution process confirmed that there is no current DID.',\n          },\n          {\n            name: 'DidDeactivated',\n            description:\n              'The DID previously existed, but has been deactivated.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoIdentitySignPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.signPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Signs a PLC operation to update some value(s) in the requesting DID's document.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              token: {\n                description:\n                  'A token received through com.atproto.identity.requestPlcOperationSignature',\n                type: 'string',\n              },\n              rotationKeys: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              alsoKnownAs: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              verificationMethods: {\n                type: 'unknown',\n              },\n              services: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n                description: 'A signed DID PLC operation.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentitySubmitPlcOperation: {\n    lexicon: 1,\n    id: 'com.atproto.identity.submitPlcOperation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['operation'],\n            properties: {\n              operation: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoIdentityUpdateHandle: {\n    lexicon: 1,\n    id: 'com.atproto.identity.updateHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'The new handle.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelDefs: {\n    lexicon: 1,\n    id: 'com.atproto.label.defs',\n    defs: {\n      label: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto resource (eg, repo or record).',\n        required: ['src', 'uri', 'val', 'cts'],\n        properties: {\n          ver: {\n            type: 'integer',\n            description: 'The AT Protocol version of the label object.',\n          },\n          src: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the actor who created this label.',\n          },\n          uri: {\n            type: 'string',\n            format: 'uri',\n            description:\n              'AT URI of the record, repository (account), or other resource that this label applies to.',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n            description:\n              \"Optionally, CID specifying the specific version of 'uri' resource this label applies to.\",\n          },\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n          neg: {\n            type: 'boolean',\n            description:\n              'If true, this is a negation label, overwriting a previous label.',\n          },\n          cts: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when this label was created.',\n          },\n          exp: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp at which this label expires (no longer applies).',\n          },\n          sig: {\n            type: 'bytes',\n            description: 'Signature of dag-cbor encoded label.',\n          },\n        },\n      },\n      selfLabels: {\n        type: 'object',\n        description:\n          'Metadata tags on an atproto record, published by the author within the record.',\n        required: ['values'],\n        properties: {\n          values: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#selfLabel',\n            },\n            maxLength: 10,\n          },\n        },\n      },\n      selfLabel: {\n        type: 'object',\n        description:\n          'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',\n        required: ['val'],\n        properties: {\n          val: {\n            type: 'string',\n            maxLength: 128,\n            description:\n              'The short string name of the value or type of this label.',\n          },\n        },\n      },\n      labelValueDefinition: {\n        type: 'object',\n        description:\n          'Declares a label value and its expected interpretations and behaviors.',\n        required: ['identifier', 'severity', 'blurs', 'locales'],\n        properties: {\n          identifier: {\n            type: 'string',\n            description:\n              \"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).\",\n            maxLength: 100,\n            maxGraphemes: 100,\n          },\n          severity: {\n            type: 'string',\n            description:\n              \"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.\",\n            knownValues: ['inform', 'alert', 'none'],\n          },\n          blurs: {\n            type: 'string',\n            description:\n              \"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.\",\n            knownValues: ['content', 'media', 'none'],\n          },\n          defaultSetting: {\n            type: 'string',\n            description: 'The default setting for this label.',\n            knownValues: ['ignore', 'warn', 'hide'],\n            default: 'warn',\n          },\n          adultOnly: {\n            type: 'boolean',\n            description:\n              'Does the user need to have adult content enabled in order to configure this label?',\n          },\n          locales: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',\n            },\n          },\n        },\n      },\n      labelValueDefinitionStrings: {\n        type: 'object',\n        description:\n          'Strings which describe the label in the UI, localized into a specific language.',\n        required: ['lang', 'name', 'description'],\n        properties: {\n          lang: {\n            type: 'string',\n            description:\n              'The code of the language these strings are written in.',\n            format: 'language',\n          },\n          name: {\n            type: 'string',\n            description: 'A short human-readable name for the label.',\n            maxGraphemes: 64,\n            maxLength: 640,\n          },\n          description: {\n            type: 'string',\n            description:\n              'A longer description of what the label means and why it might be applied.',\n            maxGraphemes: 10000,\n            maxLength: 100000,\n          },\n        },\n      },\n      labelValue: {\n        type: 'string',\n        knownValues: [\n          '!hide',\n          '!no-promote',\n          '!warn',\n          '!no-unauthenticated',\n          'dmca-violation',\n          'doxxing',\n          'porn',\n          'sexual',\n          'nudity',\n          'nsfl',\n          'gore',\n        ],\n      },\n    },\n  },\n  ComAtprotoLabelQueryLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.queryLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.',\n        parameters: {\n          type: 'params',\n          required: ['uriPatterns'],\n          properties: {\n            uriPatterns: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                \"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.\",\n            },\n            sources: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n              description:\n                'Optional list of label sources (DIDs) to filter on.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLabelSubscribeLabels: {\n    lexicon: 1,\n    id: 'com.atproto.label.subscribeLabels',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.label.subscribeLabels#labels',\n              'lex:com.atproto.label.subscribeLabels#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n        ],\n      },\n      labels: {\n        type: 'object',\n        required: ['seq', 'labels'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoLexiconResolveLexicon: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.resolveLexicon',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Resolves an atproto lexicon (NSID) to a schema.',\n        parameters: {\n          type: 'params',\n          properties: {\n            nsid: {\n              format: 'nsid',\n              type: 'string',\n              description: 'The lexicon NSID to resolve.',\n            },\n          },\n          required: ['nsid'],\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n                description: 'The CID of the lexicon schema record.',\n              },\n              schema: {\n                type: 'ref',\n                ref: 'lex:com.atproto.lexicon.schema#main',\n                description: 'The resolved lexicon schema record.',\n              },\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n                description: 'The AT-URI of the lexicon schema record.',\n              },\n            },\n            required: ['uri', 'cid', 'schema'],\n          },\n        },\n        errors: [\n          {\n            description: 'No lexicon was resolved for the NSID.',\n            name: 'LexiconNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoLexiconSchema: {\n    lexicon: 1,\n    id: 'com.atproto.lexicon.schema',\n    defs: {\n      main: {\n        type: 'record',\n        description:\n          \"Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).\",\n        key: 'nsid',\n        record: {\n          type: 'object',\n          required: ['lexicon'],\n          properties: {\n            lexicon: {\n              type: 'integer',\n              description:\n                \"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.\",\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationCreateReport: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.createReport',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['reasonType', 'subject'],\n            properties: {\n              reasonType: {\n                type: 'ref',\n                description:\n                  'Indicates the broad category of violation the report is for.',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n                description:\n                  'Additional context about the content and violation.',\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.createReport#modTool',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'id',\n              'reasonType',\n              'subject',\n              'reportedBy',\n              'createdAt',\n            ],\n            properties: {\n              id: {\n                type: 'integer',\n              },\n              reasonType: {\n                type: 'ref',\n                ref: 'lex:com.atproto.moderation.defs#reasonType',\n              },\n              reason: {\n                type: 'string',\n                maxGraphemes: 2000,\n                maxLength: 20000,\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              reportedBy: {\n                type: 'string',\n                format: 'did',\n              },\n              createdAt: {\n                type: 'string',\n                format: 'datetime',\n              },\n            },\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoModerationDefs: {\n    lexicon: 1,\n    id: 'com.atproto.moderation.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'com.atproto.moderation.defs#reasonSpam',\n          'com.atproto.moderation.defs#reasonViolation',\n          'com.atproto.moderation.defs#reasonMisleading',\n          'com.atproto.moderation.defs#reasonSexual',\n          'com.atproto.moderation.defs#reasonRude',\n          'com.atproto.moderation.defs#reasonOther',\n          'com.atproto.moderation.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonSpam: {\n        type: 'token',\n        description:\n          'Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.',\n      },\n      reasonViolation: {\n        type: 'token',\n        description:\n          'Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.',\n      },\n      reasonMisleading: {\n        type: 'token',\n        description:\n          'Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.',\n      },\n      reasonSexual: {\n        type: 'token',\n        description:\n          'Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.',\n      },\n      reasonRude: {\n        type: 'token',\n        description:\n          'Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.',\n      },\n      reasonOther: {\n        type: 'token',\n        description:\n          'Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.',\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      subjectType: {\n        type: 'string',\n        description: 'Tag describing a type of subject that might be reported.',\n        knownValues: ['account', 'record', 'chat'],\n      },\n    },\n  },\n  ComAtprotoRepoApplyWrites: {\n    lexicon: 1,\n    id: 'com.atproto.repo.applyWrites',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'writes'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              writes: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#create',\n                    'lex:com.atproto.repo.applyWrites#update',\n                    'lex:com.atproto.repo.applyWrites#delete',\n                  ],\n                  closed: true,\n                },\n              },\n              swapCommit: {\n                type: 'string',\n                description:\n                  'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [],\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              results: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:com.atproto.repo.applyWrites#createResult',\n                    'lex:com.atproto.repo.applyWrites#updateResult',\n                    'lex:com.atproto.repo.applyWrites#deleteResult',\n                  ],\n                  closed: true,\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that the 'swapCommit' parameter did not match current commit.\",\n          },\n        ],\n      },\n      create: {\n        type: 'object',\n        description: 'Operation which creates a new record.',\n        required: ['collection', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            maxLength: 512,\n            format: 'record-key',\n            description:\n              'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      update: {\n        type: 'object',\n        description: 'Operation which updates an existing record.',\n        required: ['collection', 'rkey', 'value'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n      delete: {\n        type: 'object',\n        description: 'Operation which deletes an existing record.',\n        required: ['collection', 'rkey'],\n        properties: {\n          collection: {\n            type: 'string',\n            format: 'nsid',\n          },\n          rkey: {\n            type: 'string',\n            format: 'record-key',\n          },\n        },\n      },\n      createResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      updateResult: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          validationStatus: {\n            type: 'string',\n            knownValues: ['valid', 'unknown'],\n          },\n        },\n      },\n      deleteResult: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n    },\n  },\n  ComAtprotoRepoCreateRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.createRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Create a single new repository record. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'record'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record itself. Must contain a $type field.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n            description:\n              \"Indicates that 'swapCommit' didn't match current repo commit.\",\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDefs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.defs',\n    defs: {\n      commitMeta: {\n        type: 'object',\n        required: ['cid', 'rev'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoDeleteRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.deleteRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID.',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoDescribeRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.describeRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about an account and repository, including the list of collections. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'handle',\n              'did',\n              'didDoc',\n              'collections',\n              'handleIsCorrect',\n            ],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'The complete DID document for this account.',\n              },\n              collections: {\n                type: 'array',\n                description:\n                  'List of all the collections (NSIDs) for which this repo contains at least one record.',\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              handleIsCorrect: {\n                type: 'boolean',\n                description:\n                  'Indicates if handle is currently valid (resolves bi-directionally)',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a single record from a repository. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection', 'rkey'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record collection.',\n            },\n            rkey: {\n              type: 'string',\n              description: 'The Record Key.',\n              format: 'record-key',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description:\n                'The CID of the version of the record. If not specified, then return the most recent version.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'value'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              value: {\n                type: 'unknown',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoImportRepo: {\n    lexicon: 1,\n    id: 'com.atproto.repo.importRepo',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.',\n        input: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListMissingBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listMissingBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blobs'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              blobs: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob',\n                },\n              },\n            },\n          },\n        },\n      },\n      recordBlob: {\n        type: 'object',\n        required: ['cid', 'recordUri'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          recordUri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoListRecords: {\n    lexicon: 1,\n    id: 'com.atproto.repo.listRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List a range of records in a repository, matching a specific collection. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['repo', 'collection'],\n          properties: {\n            repo: {\n              type: 'string',\n              format: 'at-identifier',\n              description: 'The handle or DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n              description: 'The NSID of the record type.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n              description: 'The number of records to return.',\n            },\n            cursor: {\n              type: 'string',\n            },\n            reverse: {\n              type: 'boolean',\n              description: 'Flag to reverse the order of the returned records.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              records: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.repo.listRecords#record',\n                },\n              },\n            },\n          },\n        },\n      },\n      record: {\n        type: 'object',\n        required: ['uri', 'cid', 'value'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoPutRecord: {\n    lexicon: 1,\n    id: 'com.atproto.repo.putRecord',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repo', 'collection', 'rkey', 'record'],\n            nullable: ['swapRecord'],\n            properties: {\n              repo: {\n                type: 'string',\n                format: 'at-identifier',\n                description:\n                  'The handle or DID of the repo (aka, current account).',\n              },\n              collection: {\n                type: 'string',\n                format: 'nsid',\n                description: 'The NSID of the record collection.',\n              },\n              rkey: {\n                type: 'string',\n                format: 'record-key',\n                description: 'The Record Key.',\n                maxLength: 512,\n              },\n              validate: {\n                type: 'boolean',\n                description:\n                  \"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.\",\n              },\n              record: {\n                type: 'unknown',\n                description: 'The record to write.',\n              },\n              swapRecord: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation',\n              },\n              swapCommit: {\n                type: 'string',\n                format: 'cid',\n                description:\n                  'Compare and swap with the previous commit by CID.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uri', 'cid'],\n            properties: {\n              uri: {\n                type: 'string',\n                format: 'at-uri',\n              },\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              commit: {\n                type: 'ref',\n                ref: 'lex:com.atproto.repo.defs#commitMeta',\n              },\n              validationStatus: {\n                type: 'string',\n                knownValues: ['valid', 'unknown'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidSwap',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoRepoStrongRef: {\n    lexicon: 1,\n    id: 'com.atproto.repo.strongRef',\n    description: 'A URI with a content-hash fingerprint.',\n    defs: {\n      main: {\n        type: 'object',\n        required: ['uri', 'cid'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoRepoUploadBlob: {\n    lexicon: 1,\n    id: 'com.atproto.repo.uploadBlob',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.',\n        input: {\n          encoding: '*/*',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['blob'],\n            properties: {\n              blob: {\n                type: 'blob',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerActivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.activateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.\",\n      },\n    },\n  },\n  ComAtprotoServerCheckAccountStatus: {\n    lexicon: 1,\n    id: 'com.atproto.server.checkAccountStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: [\n              'activated',\n              'validDid',\n              'repoCommit',\n              'repoRev',\n              'repoBlocks',\n              'indexedRecords',\n              'privateStateValues',\n              'expectedBlobs',\n              'importedBlobs',\n            ],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              validDid: {\n                type: 'boolean',\n              },\n              repoCommit: {\n                type: 'string',\n                format: 'cid',\n              },\n              repoRev: {\n                type: 'string',\n              },\n              repoBlocks: {\n                type: 'integer',\n              },\n              indexedRecords: {\n                type: 'integer',\n              },\n              privateStateValues: {\n                type: 'integer',\n              },\n              expectedBlobs: {\n                type: 'integer',\n              },\n              importedBlobs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerConfirmEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.confirmEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email', 'token'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountNotFound',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'InvalidEmail',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an account. Implemented by PDS.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Requested handle for the account.',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Pre-existing atproto DID, being imported to a new account.',\n              },\n              inviteCode: {\n                type: 'string',\n              },\n              verificationCode: {\n                type: 'string',\n              },\n              verificationPhone: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n                description:\n                  'Initial account password. May need to meet instance-specific password strength requirements.',\n              },\n              recoveryKey: {\n                type: 'string',\n                description:\n                  'DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.',\n              },\n              plcOp: {\n                type: 'unknown',\n                description:\n                  'A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            description:\n              'Account login session returned on successful account creation.',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID of the new account.',\n              },\n              didDoc: {\n                type: 'unknown',\n                description: 'Complete DID document.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidHandle',\n          },\n          {\n            name: 'InvalidPassword',\n          },\n          {\n            name: 'InvalidInviteCode',\n          },\n          {\n            name: 'HandleNotAvailable',\n          },\n          {\n            name: 'UnsupportedDomain',\n          },\n          {\n            name: 'UnresolvableDid',\n          },\n          {\n            name: 'IncompatibleDidDoc',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerCreateAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.createAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an App Password.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description:\n                  'A short name for the App Password, to help distinguish them.',\n              },\n              privileged: {\n                type: 'boolean',\n                description:\n                  \"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.\",\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.createAppPassword#appPassword',\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'password', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          password: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCode: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCode',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an invite code.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['useCount'],\n            properties: {\n              useCount: {\n                type: 'integer',\n              },\n              forAccount: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['code'],\n            properties: {\n              code: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.createInviteCodes',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create invite codes.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codeCount', 'useCount'],\n            properties: {\n              codeCount: {\n                type: 'integer',\n                default: 1,\n              },\n              useCount: {\n                type: 'integer',\n              },\n              forAccounts: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.createInviteCodes#accountCodes',\n                },\n              },\n            },\n          },\n        },\n      },\n      accountCodes: {\n        type: 'object',\n        required: ['account', 'codes'],\n        properties: {\n          account: {\n            type: 'string',\n          },\n          codes: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerCreateSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.createSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create an authentication session.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['identifier', 'password'],\n            properties: {\n              identifier: {\n                type: 'string',\n                description:\n                  'Handle or other identifier supported by the server for the authenticating user.',\n              },\n              password: {\n                type: 'string',\n              },\n              authFactorToken: {\n                type: 'string',\n              },\n              allowTakendown: {\n                type: 'boolean',\n                description:\n                  'When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'AuthFactorTokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeactivateAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deactivateAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              deleteAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'A recommendation to server as to how long they should hold onto the deactivated account before deleting.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDefs: {\n    lexicon: 1,\n    id: 'com.atproto.server.defs',\n    defs: {\n      inviteCode: {\n        type: 'object',\n        required: [\n          'code',\n          'available',\n          'disabled',\n          'forAccount',\n          'createdBy',\n          'createdAt',\n          'uses',\n        ],\n        properties: {\n          code: {\n            type: 'string',\n          },\n          available: {\n            type: 'integer',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          forAccount: {\n            type: 'string',\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          uses: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCodeUse',\n            },\n          },\n        },\n      },\n      inviteCodeUse: {\n        type: 'object',\n        required: ['usedBy', 'usedAt'],\n        properties: {\n          usedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          usedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerDeleteAccount: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteAccount',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'password', 'token'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              password: {\n                type: 'string',\n              },\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDeleteSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.deleteSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        errors: [\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerDescribeServer: {\n    lexicon: 1,\n    id: 'com.atproto.server.describeServer',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Describes the server's account creation requirements and capabilities. Implemented by PDS.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'availableUserDomains'],\n            properties: {\n              inviteCodeRequired: {\n                type: 'boolean',\n                description:\n                  'If true, an invite code must be supplied to create an account on this instance.',\n              },\n              phoneVerificationRequired: {\n                type: 'boolean',\n                description:\n                  'If true, a phone verification token must be supplied to create an account on this instance.',\n              },\n              availableUserDomains: {\n                type: 'array',\n                description:\n                  'List of domain suffixes that can be used in account handles.',\n                items: {\n                  type: 'string',\n                },\n              },\n              links: {\n                type: 'ref',\n                description: 'URLs of service policy documents.',\n                ref: 'lex:com.atproto.server.describeServer#links',\n              },\n              contact: {\n                type: 'ref',\n                description: 'Contact information',\n                ref: 'lex:com.atproto.server.describeServer#contact',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n      },\n      links: {\n        type: 'object',\n        properties: {\n          privacyPolicy: {\n            type: 'string',\n            format: 'uri',\n          },\n          termsOfService: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      contact: {\n        type: 'object',\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerGetAccountInviteCodes: {\n    lexicon: 1,\n    id: 'com.atproto.server.getAccountInviteCodes',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get all invite codes for the current account. Requires auth.',\n        parameters: {\n          type: 'params',\n          properties: {\n            includeUsed: {\n              type: 'boolean',\n              default: true,\n            },\n            createAvailable: {\n              type: 'boolean',\n              default: true,\n              description:\n                \"Controls whether any new 'earned' but not 'created' invites should be created.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['codes'],\n            properties: {\n              codes: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.defs#inviteCode',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateCreate',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetServiceAuth: {\n    lexicon: 1,\n    id: 'com.atproto.server.getServiceAuth',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a signed token on behalf of the requesting DID for the requested service.',\n        parameters: {\n          type: 'params',\n          required: ['aud'],\n          properties: {\n            aud: {\n              type: 'string',\n              format: 'did',\n              description:\n                'The DID of the service that the token will be used to authenticate with',\n            },\n            exp: {\n              type: 'integer',\n              description:\n                'The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.',\n            },\n            lxm: {\n              type: 'string',\n              format: 'nsid',\n              description:\n                'Lexicon (XRPC) method to bind the requested token to',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'BadExpiration',\n            description:\n              'Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerGetSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.getSession',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get information about the current auth session. Requires auth.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'did'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerListAppPasswords: {\n    lexicon: 1,\n    id: 'com.atproto.server.listAppPasswords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all App Passwords.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['passwords'],\n            properties: {\n              passwords: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.server.listAppPasswords#appPassword',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n        ],\n      },\n      appPassword: {\n        type: 'object',\n        required: ['name', 'createdAt'],\n        properties: {\n          name: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          privileged: {\n            type: 'boolean',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRefreshSession: {\n    lexicon: 1,\n    id: 'com.atproto.server.refreshSession',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          \"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accessJwt', 'refreshJwt', 'handle', 'did'],\n            properties: {\n              accessJwt: {\n                type: 'string',\n              },\n              refreshJwt: {\n                type: 'string',\n              },\n              handle: {\n                type: 'string',\n                format: 'handle',\n              },\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              didDoc: {\n                type: 'unknown',\n              },\n              email: {\n                type: 'string',\n              },\n              emailConfirmed: {\n                type: 'boolean',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  \"Hosting status of the account. If not specified, then assume 'active'.\",\n                knownValues: ['takendown', 'suspended', 'deactivated'],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'AccountTakedown',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'ExpiredToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRequestAccountDelete: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestAccountDelete',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account deletion via email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailConfirmation: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailConfirmation',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request an email with a code to confirm ownership of email.',\n      },\n    },\n  },\n  ComAtprotoServerRequestEmailUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestEmailUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Request a token in order to update email.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['tokenRequired'],\n            properties: {\n              tokenRequired: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerRequestPasswordReset: {\n    lexicon: 1,\n    id: 'com.atproto.server.requestPasswordReset',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Initiate a user account password reset via email.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerReserveSigningKey: {\n    lexicon: 1,\n    id: 'com.atproto.server.reserveSigningKey',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n                description: 'The DID to reserve a key for.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['signingKey'],\n            properties: {\n              signingKey: {\n                type: 'string',\n                description:\n                  'The public key for the reserved signing key, in did:key serialization.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerResetPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.resetPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Reset a user account password using a token.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['token', 'password'],\n            properties: {\n              token: {\n                type: 'string',\n              },\n              password: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoServerRevokeAppPassword: {\n    lexicon: 1,\n    id: 'com.atproto.server.revokeAppPassword',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Revoke an App Password by name.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoServerUpdateEmail: {\n    lexicon: 1,\n    id: 'com.atproto.server.updateEmail',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: \"Update an account's email.\",\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['email'],\n            properties: {\n              email: {\n                type: 'string',\n              },\n              emailAuthFactor: {\n                type: 'boolean',\n              },\n              token: {\n                type: 'string',\n                description:\n                  \"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.\",\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'ExpiredToken',\n          },\n          {\n            name: 'InvalidToken',\n          },\n          {\n            name: 'TokenRequired',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncDefs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.defs',\n    defs: {\n      hostStatus: {\n        type: 'string',\n        knownValues: ['active', 'idle', 'offline', 'throttled', 'banned'],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlob: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlob',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cid'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the account.',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n              description: 'The CID of the blob to fetch',\n            },\n          },\n        },\n        output: {\n          encoding: '*/*',\n        },\n        errors: [\n          {\n            name: 'BlobNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetBlocks: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getBlocks',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'cids'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            cids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'BlockNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetCheckout: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getCheckout',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'DEPRECATED - please use com.atproto.sync.getRepo instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n  ComAtprotoSyncGetHead: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHead',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED - please use com.atproto.sync.getLatestCommit instead',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['root'],\n            properties: {\n              root: {\n                type: 'string',\n                format: 'cid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HeadNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetHostStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getHostStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Returns information about a specified upstream host, as consumed by the server. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          required: ['hostname'],\n          properties: {\n            hostname: {\n              type: 'string',\n              description:\n                'Hostname of the host (eg, PDS or relay) being queried.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n              },\n              seq: {\n                type: 'integer',\n                description:\n                  'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n              },\n              accountCount: {\n                type: 'integer',\n                description:\n                  'Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.',\n              },\n              status: {\n                type: 'ref',\n                ref: 'lex:com.atproto.sync.defs#hostStatus',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetLatestCommit: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getLatestCommit',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the current commit CID & revision of the specified repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cid', 'rev'],\n            properties: {\n              cid: {\n                type: 'string',\n                format: 'cid',\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRecord: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.',\n        parameters: {\n          type: 'params',\n          required: ['did', 'collection', 'rkey'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            rkey: {\n              type: 'string',\n              description: 'Record Key',\n              format: 'record-key',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepo: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          \"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.\",\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description:\n                \"The revision ('rev') of the repo to create a diff from.\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncGetRepoStatus: {\n    lexicon: 1,\n    id: 'com.atproto.sync.getRepoStatus',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'active'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              active: {\n                type: 'boolean',\n              },\n              status: {\n                type: 'string',\n                description:\n                  'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n                knownValues: [\n                  'takendown',\n                  'suspended',\n                  'deleted',\n                  'deactivated',\n                  'desynchronized',\n                  'throttled',\n                ],\n              },\n              rev: {\n                type: 'string',\n                format: 'tid',\n                description:\n                  'Optional field, the current rev of the repo, if active=true',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListBlobs: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listBlobs',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n              description: 'The DID of the repo.',\n            },\n            since: {\n              type: 'string',\n              format: 'tid',\n              description: 'Optional revision of the repo to list blobs since.',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cids'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              cids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n          {\n            name: 'RepoTakendown',\n          },\n          {\n            name: 'RepoSuspended',\n          },\n          {\n            name: 'RepoDeactivated',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncListHosts: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listHosts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 200,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hosts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              hosts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listHosts#host',\n                },\n                description:\n                  'Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.',\n              },\n            },\n          },\n        },\n      },\n      host: {\n        type: 'object',\n        required: ['hostname'],\n        properties: {\n          hostname: {\n            type: 'string',\n            description: 'hostname of server; not a URL (no scheme)',\n          },\n          seq: {\n            type: 'integer',\n            description:\n              'Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).',\n          },\n          accountCount: {\n            type: 'integer',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:com.atproto.sync.defs#hostStatus',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listRepos#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did', 'head', 'rev'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          head: {\n            type: 'string',\n            format: 'cid',\n            description: 'Current repo commit CID',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n          },\n          active: {\n            type: 'boolean',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncListReposByCollection: {\n    lexicon: 1,\n    id: 'com.atproto.sync.listReposByCollection',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Enumerates all the DIDs which have records with the given collection NSID.',\n        parameters: {\n          type: 'params',\n          required: ['collection'],\n          properties: {\n            collection: {\n              type: 'string',\n              format: 'nsid',\n            },\n            limit: {\n              type: 'integer',\n              description:\n                'Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.',\n              minimum: 1,\n              maximum: 2000,\n              default: 500,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.sync.listReposByCollection#repo',\n                },\n              },\n            },\n          },\n        },\n      },\n      repo: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncNotifyOfUpdate: {\n    lexicon: 1,\n    id: 'com.atproto.sync.notifyOfUpdate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (usually a PDS) that is notifying of update.',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoSyncRequestCrawl: {\n    lexicon: 1,\n    id: 'com.atproto.sync.requestCrawl',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['hostname'],\n            properties: {\n              hostname: {\n                type: 'string',\n                description:\n                  'Hostname of the current service (eg, PDS) that is requesting to be crawled.',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'HostBanned',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoSyncSubscribeRepos: {\n    lexicon: 1,\n    id: 'com.atproto.sync.subscribeRepos',\n    defs: {\n      main: {\n        type: 'subscription',\n        description:\n          'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'integer',\n              description: 'The last known event seq number to backfill from.',\n            },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.sync.subscribeRepos#commit',\n              'lex:com.atproto.sync.subscribeRepos#sync',\n              'lex:com.atproto.sync.subscribeRepos#identity',\n              'lex:com.atproto.sync.subscribeRepos#account',\n              'lex:com.atproto.sync.subscribeRepos#info',\n            ],\n          },\n        },\n        errors: [\n          {\n            name: 'FutureCursor',\n          },\n          {\n            name: 'ConsumerTooSlow',\n            description:\n              'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.',\n          },\n        ],\n      },\n      commit: {\n        type: 'object',\n        description:\n          'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.',\n        required: [\n          'seq',\n          'rebase',\n          'tooBig',\n          'repo',\n          'commit',\n          'rev',\n          'since',\n          'blocks',\n          'ops',\n          'blobs',\n          'time',\n        ],\n        nullable: ['since'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          rebase: {\n            type: 'boolean',\n            description: 'DEPRECATED -- unused',\n          },\n          tooBig: {\n            type: 'boolean',\n            description:\n              'DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.',\n          },\n          repo: {\n            type: 'string',\n            format: 'did',\n            description:\n              \"The repo this event comes from. Note that all other message types name this field 'did'.\",\n          },\n          commit: {\n            type: 'cid-link',\n            description: 'Repo commit object CID.',\n          },\n          rev: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.',\n          },\n          since: {\n            type: 'string',\n            format: 'tid',\n            description:\n              'The rev of the last emitted commit from this repo (if any).',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.\",\n            maxLength: 2000000,\n          },\n          ops: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.sync.subscribeRepos#repoOp',\n              description:\n                'List of repo mutation operations in this commit (eg, records created, updated, or deleted).',\n            },\n            maxLength: 200,\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'cid-link',\n              description:\n                'DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.',\n            },\n          },\n          prevData: {\n            type: 'cid-link',\n            description:\n              \"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.\",\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      sync: {\n        type: 'object',\n        description:\n          'Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.',\n        required: ['seq', 'did', 'blocks', 'rev', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n            description: 'The stream sequence number of this message.',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description:\n              'The account this repo event corresponds to. Must match that in the commit object.',\n          },\n          blocks: {\n            type: 'bytes',\n            description:\n              \"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.\",\n            maxLength: 10000,\n          },\n          rev: {\n            type: 'string',\n            description:\n              'The rev of the commit. This value must match that in the commit object.',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp of when this message was originally broadcast.',\n          },\n        },\n      },\n      identity: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.\",\n        required: ['seq', 'did', 'time'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n            description:\n              \"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.\",\n          },\n        },\n      },\n      account: {\n        type: 'object',\n        description:\n          \"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.\",\n        required: ['seq', 'did', 'time', 'active'],\n        properties: {\n          seq: {\n            type: 'integer',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          time: {\n            type: 'string',\n            format: 'datetime',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            description:\n              'If active=false, this optional field indicates a reason for why the account is not active.',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'desynchronized',\n              'throttled',\n            ],\n          },\n        },\n      },\n      info: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            knownValues: ['OutdatedCursor'],\n          },\n          message: {\n            type: 'string',\n          },\n        },\n      },\n      repoOp: {\n        type: 'object',\n        description: 'A repo operation, ie a mutation of a single record.',\n        required: ['action', 'path', 'cid'],\n        nullable: ['cid'],\n        properties: {\n          action: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          path: {\n            type: 'string',\n          },\n          cid: {\n            type: 'cid-link',\n            description:\n              'For creates and updates, the new record CID. For deletions, null.',\n          },\n          prev: {\n            type: 'cid-link',\n            description:\n              'For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempAddReservedHandle: {\n    lexicon: 1,\n    id: 'com.atproto.temp.addReservedHandle',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a handle to the set of reserved handles.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle'],\n            properties: {\n              handle: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckHandleAvailability: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkHandleAvailability',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.',\n        parameters: {\n          type: 'params',\n          required: ['handle'],\n          properties: {\n            handle: {\n              type: 'string',\n              format: 'handle',\n              description:\n                'Tentative handle. Will be checked for availability or used to build handle suggestions.',\n            },\n            email: {\n              type: 'string',\n              description:\n                'User-provided email. Might be used to build handle suggestions.',\n            },\n            birthDate: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'User-provided birth date. Might be used to build handle suggestions.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['handle', 'result'],\n            properties: {\n              handle: {\n                type: 'string',\n                format: 'handle',\n                description: 'Echo of the input handle.',\n              },\n              result: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.temp.checkHandleAvailability#resultAvailable',\n                  'lex:com.atproto.temp.checkHandleAvailability#resultUnavailable',\n                ],\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidEmail',\n            description: 'An invalid email was provided.',\n          },\n        ],\n      },\n      resultAvailable: {\n        type: 'object',\n        description: 'Indicates the provided handle is available.',\n        properties: {},\n      },\n      resultUnavailable: {\n        type: 'object',\n        description:\n          'Indicates the provided handle is unavailable and gives suggestions of available handles.',\n        required: ['suggestions'],\n        properties: {\n          suggestions: {\n            type: 'array',\n            description:\n              'List of suggested handles based on the provided inputs.',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.temp.checkHandleAvailability#suggestion',\n            },\n          },\n        },\n      },\n      suggestion: {\n        type: 'object',\n        required: ['handle', 'method'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          method: {\n            type: 'string',\n            description:\n              'Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics.',\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempCheckSignupQueue: {\n    lexicon: 1,\n    id: 'com.atproto.temp.checkSignupQueue',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Check accounts location in signup queue.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['activated'],\n            properties: {\n              activated: {\n                type: 'boolean',\n              },\n              placeInQueue: {\n                type: 'integer',\n              },\n              estimatedTimeMs: {\n                type: 'integer',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempDereferenceScope: {\n    lexicon: 1,\n    id: 'com.atproto.temp.dereferenceScope',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Allows finding the oauth permission scope from a reference',\n        parameters: {\n          type: 'params',\n          required: ['scope'],\n          properties: {\n            scope: {\n              type: 'string',\n              description: \"The scope reference (starts with 'ref:')\",\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['scope'],\n            properties: {\n              scope: {\n                type: 'string',\n                description: 'The full oauth permission scope',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidScopeReference',\n            description: 'An invalid scope reference was provided.',\n          },\n        ],\n      },\n    },\n  },\n  ComAtprotoTempFetchLabels: {\n    lexicon: 1,\n    id: 'com.atproto.temp.fetchLabels',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'DEPRECATED: use queryLabels or subscribeLabels instead -- Fetch all labels from a labeler created after a certain date.',\n        parameters: {\n          type: 'params',\n          properties: {\n            since: {\n              type: 'integer',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 250,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['labels'],\n            properties: {\n              labels: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.label.defs#label',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRequestPhoneVerification: {\n    lexicon: 1,\n    id: 'com.atproto.temp.requestPhoneVerification',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Request a verification code to be sent to the supplied phone number',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['phoneNumber'],\n            properties: {\n              phoneNumber: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ComAtprotoTempRevokeAccountCredentials: {\n    lexicon: 1,\n    id: 'com.atproto.temp.revokeAccountCredentials',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke sessions, password, and app passwords associated with account. May be resolved by a password reset.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['account'],\n            properties: {\n              account: {\n                type: 'string',\n                format: 'at-identifier',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationCreateTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.createTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Administrative action to create a new, re-usable communication (email for now) template.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subject', 'contentMarkdown', 'name'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the template.',\n              },\n              contentMarkdown: {\n                type: 'string',\n                description:\n                  'Content of the template, markdown supported, can contain variable placeholders.',\n              },\n              subject: {\n                type: 'string',\n                description: 'Subject of the message, used in emails.',\n              },\n              lang: {\n                type: 'string',\n                format: 'language',\n                description: 'Message language.',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'DID of the user who is creating the template.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.communication.defs#templateView',\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateTemplateName',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneCommunicationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.defs',\n    defs: {\n      templateView: {\n        type: 'object',\n        required: [\n          'id',\n          'name',\n          'contentMarkdown',\n          'disabled',\n          'lastUpdatedBy',\n          'createdAt',\n          'updatedAt',\n        ],\n        properties: {\n          id: {\n            type: 'string',\n          },\n          name: {\n            type: 'string',\n            description: 'Name of the template.',\n          },\n          subject: {\n            type: 'string',\n            description:\n              'Content of the template, can contain markdown and variable placeholders.',\n          },\n          contentMarkdown: {\n            type: 'string',\n            description: 'Subject of the message, used in emails.',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          lang: {\n            type: 'string',\n            format: 'language',\n            description: 'Message language.',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who last updated the template.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationDeleteTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.deleteTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a communication template.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationListTemplates: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.listTemplates',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get list of all communication templates.',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['communicationTemplates'],\n            properties: {\n              communicationTemplates: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.communication.defs#templateView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneCommunicationUpdateTemplate: {\n    lexicon: 1,\n    id: 'tools.ozone.communication.updateTemplate',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Administrative action to update an existing communication template. Allows passing partial fields to patch specific fields only.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['id'],\n            properties: {\n              id: {\n                type: 'string',\n                description: 'ID of the template to be updated.',\n              },\n              name: {\n                type: 'string',\n                description: 'Name of the template.',\n              },\n              lang: {\n                type: 'string',\n                format: 'language',\n                description: 'Message language.',\n              },\n              contentMarkdown: {\n                type: 'string',\n                description:\n                  'Content of the template, markdown supported, can contain variable placeholders.',\n              },\n              subject: {\n                type: 'string',\n                description: 'Subject of the message, used in emails.',\n              },\n              updatedBy: {\n                type: 'string',\n                format: 'did',\n                description: 'DID of the user who is updating the template.',\n              },\n              disabled: {\n                type: 'boolean',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.communication.defs#templateView',\n          },\n        },\n        errors: [\n          {\n            name: 'DuplicateTemplateName',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneHostingGetAccountHistory: {\n    lexicon: 1,\n    id: 'tools.ozone.hosting.getAccountHistory',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get account history, e.g. log of updated email addresses or other identity information.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            events: {\n              type: 'array',\n              items: {\n                type: 'string',\n                knownValues: [\n                  'accountCreated',\n                  'emailUpdated',\n                  'emailConfirmed',\n                  'passwordUpdated',\n                  'handleUpdated',\n                ],\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.hosting.getAccountHistory#event',\n                },\n              },\n            },\n          },\n        },\n      },\n      event: {\n        type: 'object',\n        required: ['details', 'createdBy', 'createdAt'],\n        properties: {\n          details: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.hosting.getAccountHistory#accountCreated',\n              'lex:tools.ozone.hosting.getAccountHistory#emailUpdated',\n              'lex:tools.ozone.hosting.getAccountHistory#emailConfirmed',\n              'lex:tools.ozone.hosting.getAccountHistory#passwordUpdated',\n              'lex:tools.ozone.hosting.getAccountHistory#handleUpdated',\n            ],\n          },\n          createdBy: {\n            type: 'string',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      accountCreated: {\n        type: 'object',\n        required: [],\n        properties: {\n          email: {\n            type: 'string',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n        },\n      },\n      emailUpdated: {\n        type: 'object',\n        required: ['email'],\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n      emailConfirmed: {\n        type: 'object',\n        required: ['email'],\n        properties: {\n          email: {\n            type: 'string',\n          },\n        },\n      },\n      passwordUpdated: {\n        type: 'object',\n        required: [],\n        properties: {},\n      },\n      handleUpdated: {\n        type: 'object',\n        required: ['handle'],\n        properties: {\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationCancelScheduledActions: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.cancelScheduledActions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Cancel all pending scheduled moderation actions for specified subjects',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjects'],\n            properties: {\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description:\n                  'Array of DID subjects to cancel scheduled actions for',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  'Optional comment describing the reason for cancellation',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.cancelScheduledActions#cancellationResults',\n          },\n        },\n      },\n      cancellationResults: {\n        type: 'object',\n        required: ['succeeded', 'failed'],\n        properties: {\n          succeeded: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n            description:\n              'DIDs for which all pending scheduled actions were successfully cancelled',\n          },\n          failed: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.cancelScheduledActions#failedCancellation',\n            },\n            description:\n              'DIDs for which cancellation failed with error details',\n          },\n        },\n      },\n      failedCancellation: {\n        type: 'object',\n        required: ['did', 'error'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          error: {\n            type: 'string',\n          },\n          errorCode: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.defs',\n    defs: {\n      modEventView: {\n        type: 'object',\n        required: [\n          'id',\n          'event',\n          'subject',\n          'subjectBlobCids',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          event: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#modEventTakedown',\n              'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n              'lex:tools.ozone.moderation.defs#modEventComment',\n              'lex:tools.ozone.moderation.defs#modEventReport',\n              'lex:tools.ozone.moderation.defs#modEventLabel',\n              'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n              'lex:tools.ozone.moderation.defs#modEventEscalate',\n              'lex:tools.ozone.moderation.defs#modEventMute',\n              'lex:tools.ozone.moderation.defs#modEventUnmute',\n              'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventEmail',\n              'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n              'lex:tools.ozone.moderation.defs#modEventDivert',\n              'lex:tools.ozone.moderation.defs#modEventTag',\n              'lex:tools.ozone.moderation.defs#accountEvent',\n              'lex:tools.ozone.moderation.defs#identityEvent',\n              'lex:tools.ozone.moderation.defs#recordEvent',\n              'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n              'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n              'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n              'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.admin.defs#repoRef',\n              'lex:com.atproto.repo.strongRef',\n              'lex:chat.bsky.convo.defs#messageRef',\n            ],\n          },\n          subjectBlobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          creatorHandle: {\n            type: 'string',\n          },\n          subjectHandle: {\n            type: 'string',\n          },\n          modTool: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modTool',\n          },\n        },\n      },\n      modEventViewDetail: {\n        type: 'object',\n        required: [\n          'id',\n          'event',\n          'subject',\n          'subjectBlobs',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          event: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#modEventTakedown',\n              'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n              'lex:tools.ozone.moderation.defs#modEventComment',\n              'lex:tools.ozone.moderation.defs#modEventReport',\n              'lex:tools.ozone.moderation.defs#modEventLabel',\n              'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n              'lex:tools.ozone.moderation.defs#modEventEscalate',\n              'lex:tools.ozone.moderation.defs#modEventMute',\n              'lex:tools.ozone.moderation.defs#modEventUnmute',\n              'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'lex:tools.ozone.moderation.defs#modEventEmail',\n              'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n              'lex:tools.ozone.moderation.defs#modEventDivert',\n              'lex:tools.ozone.moderation.defs#modEventTag',\n              'lex:tools.ozone.moderation.defs#accountEvent',\n              'lex:tools.ozone.moderation.defs#identityEvent',\n              'lex:tools.ozone.moderation.defs#recordEvent',\n              'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n              'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n              'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n              'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoView',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n              'lex:tools.ozone.moderation.defs#recordView',\n              'lex:tools.ozone.moderation.defs#recordViewNotFound',\n            ],\n          },\n          subjectBlobs: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.defs#blobView',\n            },\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          modTool: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modTool',\n          },\n        },\n      },\n      subjectStatusView: {\n        type: 'object',\n        required: ['id', 'subject', 'createdAt', 'updatedAt', 'reviewState'],\n        properties: {\n          id: {\n            type: 'integer',\n          },\n          subject: {\n            type: 'union',\n            refs: [\n              'lex:com.atproto.admin.defs#repoRef',\n              'lex:com.atproto.repo.strongRef',\n              'lex:chat.bsky.convo.defs#messageRef',\n            ],\n          },\n          hosting: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#accountHosting',\n              'lex:tools.ozone.moderation.defs#recordHosting',\n            ],\n          },\n          subjectBlobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n          subjectRepoHandle: {\n            type: 'string',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing when the last update was made to the moderation status of the subject',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing the first moderation status impacting event was emitted on the subject',\n          },\n          reviewState: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectReviewState',\n          },\n          comment: {\n            type: 'string',\n            description: 'Sticky comment on the subject.',\n          },\n          priorityScore: {\n            type: 'integer',\n            description:\n              'Numeric value representing the level of priority. Higher score means higher priority.',\n            minimum: 0,\n            maximum: 100,\n          },\n          muteUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          muteReportingUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastReviewedBy: {\n            type: 'string',\n            format: 'did',\n          },\n          lastReviewedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastReportedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastAppealedAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp referencing when the author of the subject appealed a moderation action',\n          },\n          takendown: {\n            type: 'boolean',\n          },\n          appealed: {\n            type: 'boolean',\n            description:\n              'True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.',\n          },\n          suspendUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n          tags: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          accountStats: {\n            description: 'Statistics related to the account subject',\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#accountStats',\n          },\n          recordsStats: {\n            description:\n              \"Statistics related to the record subjects authored by the subject's account\",\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordsStats',\n          },\n          accountStrike: {\n            description:\n              'Strike information for the account (account-level only)',\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#accountStrike',\n          },\n          ageAssuranceState: {\n            type: 'string',\n            description: 'Current age assurance state of the subject.',\n            knownValues: ['pending', 'assured', 'unknown', 'reset', 'blocked'],\n          },\n          ageAssuranceUpdatedBy: {\n            type: 'string',\n            description:\n              'Whether or not the last successful update to age assurance was made by the user or admin.',\n            knownValues: ['admin', 'user'],\n          },\n        },\n      },\n      subjectView: {\n        description:\n          \"Detailed view of a subject. For record subjects, the author's repo and profile will be returned.\",\n        type: 'object',\n        required: ['type', 'subject'],\n        properties: {\n          type: {\n            type: 'ref',\n            ref: 'lex:com.atproto.moderation.defs#subjectType',\n          },\n          subject: {\n            type: 'string',\n          },\n          status: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoViewDetail',\n          },\n          profile: {\n            type: 'union',\n            refs: [],\n          },\n          record: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordViewDetail',\n          },\n        },\n      },\n      accountStats: {\n        description: 'Statistics about a particular account subject',\n        type: 'object',\n        properties: {\n          reportCount: {\n            description: 'Total number of reports on the account',\n            type: 'integer',\n          },\n          appealCount: {\n            description:\n              'Total number of appeals against a moderation action on the account',\n            type: 'integer',\n          },\n          suspendCount: {\n            description: 'Number of times the account was suspended',\n            type: 'integer',\n          },\n          escalateCount: {\n            description: 'Number of times the account was escalated',\n            type: 'integer',\n          },\n          takedownCount: {\n            description: 'Number of times the account was taken down',\n            type: 'integer',\n          },\n        },\n      },\n      recordsStats: {\n        description: 'Statistics about a set of record subject items',\n        type: 'object',\n        properties: {\n          totalReports: {\n            description:\n              'Cumulative sum of the number of reports on the items in the set',\n            type: 'integer',\n          },\n          reportedCount: {\n            description: 'Number of items that were reported at least once',\n            type: 'integer',\n          },\n          escalatedCount: {\n            description: 'Number of items that were escalated at least once',\n            type: 'integer',\n          },\n          appealedCount: {\n            description: 'Number of items that were appealed at least once',\n            type: 'integer',\n          },\n          subjectCount: {\n            description: 'Total number of item in the set',\n            type: 'integer',\n          },\n          pendingCount: {\n            description:\n              'Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state',\n            type: 'integer',\n          },\n          processedCount: {\n            description:\n              'Number of item currently in \"reviewNone\" or \"reviewClosed\" state',\n            type: 'integer',\n          },\n          takendownCount: {\n            description: 'Number of item currently taken down',\n            type: 'integer',\n          },\n        },\n      },\n      accountStrike: {\n        description: 'Strike information for an account',\n        type: 'object',\n        properties: {\n          activeStrikeCount: {\n            description:\n              'Current number of active strikes (excluding expired strikes)',\n            type: 'integer',\n          },\n          totalStrikeCount: {\n            description:\n              'Total number of strikes ever received (including expired strikes)',\n            type: 'integer',\n          },\n          firstStrikeAt: {\n            description: 'Timestamp of the first strike received',\n            type: 'string',\n            format: 'datetime',\n          },\n          lastStrikeAt: {\n            description: 'Timestamp of the most recent strike received',\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      subjectReviewState: {\n        type: 'string',\n        knownValues: [\n          'tools.ozone.moderation.defs#reviewOpen',\n          'tools.ozone.moderation.defs#reviewEscalated',\n          'tools.ozone.moderation.defs#reviewClosed',\n          'tools.ozone.moderation.defs#reviewNone',\n        ],\n      },\n      reviewOpen: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator',\n      },\n      reviewEscalated: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator',\n      },\n      reviewClosed: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator',\n      },\n      reviewNone: {\n        type: 'token',\n        description:\n          'Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it',\n      },\n      modEventTakedown: {\n        type: 'object',\n        description: 'Take down a subject permanently or temporarily',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the takedown should be in effect before automatically expiring.',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that drove the decision.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\",\n          },\n          targetServices: {\n            type: 'array',\n            items: {\n              type: 'string',\n              knownValues: ['appview', 'pds'],\n            },\n            description:\n              'List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.',\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user for this violation.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n        },\n      },\n      modEventReverseTakedown: {\n        type: 'object',\n        description: 'Revert take down action on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policy infraction for which takedown is being reversed.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation. Usually set from the last policy infraction's severity.\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              \"Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity.\",\n          },\n        },\n      },\n      modEventResolveAppeal: {\n        type: 'object',\n        description: 'Resolve appeal on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe resolution.',\n          },\n        },\n      },\n      modEventComment: {\n        type: 'object',\n        description:\n          'Add a comment to a subject. An empty comment will clear any previously set sticky comment.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          sticky: {\n            type: 'boolean',\n            description: 'Make the comment persistent on the subject',\n          },\n        },\n      },\n      modEventReport: {\n        type: 'object',\n        description: 'Report a subject',\n        required: ['reportType'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          isReporterMuted: {\n            type: 'boolean',\n            description:\n              \"Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject.\",\n          },\n          reportType: {\n            type: 'ref',\n            ref: 'lex:com.atproto.moderation.defs#reasonType',\n          },\n        },\n      },\n      modEventLabel: {\n        type: 'object',\n        description: 'Apply/Negate labels on a subject',\n        required: ['createLabelVals', 'negateLabelVals'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          createLabelVals: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          negateLabelVals: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the label will remain on the subject. Only applies on labels that are being added.',\n          },\n        },\n      },\n      modEventPriorityScore: {\n        type: 'object',\n        description:\n          'Set priority score of the subject. Higher score means higher priority.',\n        required: ['score'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          score: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n          },\n        },\n      },\n      ageAssuranceEvent: {\n        type: 'object',\n        description:\n          'Age assurance info coming directly from users. Only works on DID subjects.',\n        required: ['createdAt', 'status', 'attemptId'],\n        properties: {\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'The date and time of this write operation.',\n          },\n          attemptId: {\n            type: 'string',\n            description:\n              'The unique identifier for this instance of the age assurance flow, in UUID format.',\n          },\n          status: {\n            type: 'string',\n            description: 'The status of the Age Assurance process.',\n            knownValues: ['unknown', 'pending', 'assured'],\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n          countryCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow.',\n          },\n          regionCode: {\n            type: 'string',\n            description:\n              'The ISO 3166-2 region code provided when beginning the Age Assurance flow.',\n          },\n          initIp: {\n            type: 'string',\n            description: 'The IP address used when initiating the AA flow.',\n          },\n          initUa: {\n            type: 'string',\n            description: 'The user agent used when initiating the AA flow.',\n          },\n          completeIp: {\n            type: 'string',\n            description: 'The IP address used when completing the AA flow.',\n          },\n          completeUa: {\n            type: 'string',\n            description: 'The user agent used when completing the AA flow.',\n          },\n        },\n      },\n      ageAssuranceOverrideEvent: {\n        type: 'object',\n        description:\n          'Age assurance status override by moderators. Only works on DID subjects.',\n        required: ['comment', 'status'],\n        properties: {\n          status: {\n            type: 'string',\n            description:\n              'The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state.',\n            knownValues: ['assured', 'reset', 'blocked'],\n          },\n          access: {\n            type: 'ref',\n            ref: 'lex:app.bsky.ageassurance.defs#access',\n          },\n          comment: {\n            type: 'string',\n            minLength: 1,\n            description: 'Comment describing the reason for the override.',\n          },\n        },\n      },\n      ageAssurancePurgeEvent: {\n        type: 'object',\n        description:\n          'Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only.',\n        required: ['comment'],\n        properties: {\n          comment: {\n            type: 'string',\n            minLength: 1,\n            description: 'Comment describing the reason for the purge.',\n          },\n        },\n      },\n      revokeAccountCredentialsEvent: {\n        type: 'object',\n        description:\n          'Account credentials revocation by moderators. Only works on DID subjects.',\n        required: ['comment'],\n        properties: {\n          comment: {\n            minLength: 1,\n            type: 'string',\n            description: 'Comment describing the reason for the revocation.',\n          },\n        },\n      },\n      modEventAcknowledge: {\n        type: 'object',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n        },\n      },\n      modEventEscalate: {\n        type: 'object',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      modEventMute: {\n        type: 'object',\n        description: 'Mute incoming reports on a subject',\n        required: ['durationInHours'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description: 'Indicates how long the subject should remain muted.',\n          },\n        },\n      },\n      modEventUnmute: {\n        type: 'object',\n        description: 'Unmute action on a subject',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n        },\n      },\n      modEventMuteReporter: {\n        type: 'object',\n        description: 'Mute incoming reports from an account',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the account should remain muted. Falsy value here means a permanent mute.',\n          },\n        },\n      },\n      modEventUnmuteReporter: {\n        type: 'object',\n        description: 'Unmute incoming reports from an account',\n        properties: {\n          comment: {\n            type: 'string',\n            description: 'Describe reasoning behind the reversal.',\n          },\n        },\n      },\n      modEventEmail: {\n        type: 'object',\n        description: 'Keep a log of outgoing email to a user',\n        required: ['subjectLine'],\n        properties: {\n          subjectLine: {\n            type: 'string',\n            description: 'The subject line of the email sent to the user.',\n          },\n          content: {\n            type: 'string',\n            description: 'The content of the email sent to the user.',\n          },\n          comment: {\n            type: 'string',\n            description: 'Additional comment about the outgoing comm.',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that necessitated the email.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n          isDelivered: {\n            type: 'boolean',\n            description:\n              \"Indicates whether the email was successfully delivered to the user's inbox.\",\n          },\n        },\n      },\n      modEventDivert: {\n        type: 'object',\n        description:\n          \"Divert a record's blobs to a 3rd party service for further scanning/tagging\",\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      modEventTag: {\n        type: 'object',\n        description: 'Add/Remove a tag on a subject',\n        required: ['add', 'remove'],\n        properties: {\n          add: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n            description:\n              \"Tags to be added to the subject. If already exists, won't be duplicated.\",\n          },\n          remove: {\n            type: 'array',\n            items: {\n              type: 'string',\n            },\n            description:\n              \"Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated.\",\n          },\n          comment: {\n            type: 'string',\n            description: 'Additional comment about added/removed tags.',\n          },\n        },\n      },\n      accountEvent: {\n        type: 'object',\n        description:\n          'Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp', 'active'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          active: {\n            type: 'boolean',\n            description:\n              'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n          },\n          status: {\n            type: 'string',\n            knownValues: [\n              'unknown',\n              'deactivated',\n              'deleted',\n              'takendown',\n              'suspended',\n              'tombstoned',\n            ],\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      identityEvent: {\n        type: 'object',\n        description:\n          'Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          pdsHost: {\n            type: 'string',\n            format: 'uri',\n          },\n          tombstone: {\n            type: 'boolean',\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      recordEvent: {\n        type: 'object',\n        description:\n          'Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.',\n        required: ['timestamp', 'op'],\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          op: {\n            type: 'string',\n            knownValues: ['create', 'update', 'delete'],\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          timestamp: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      scheduleTakedownEvent: {\n        type: 'object',\n        description: 'Logs a scheduled takedown action for an account.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      cancelScheduledTakedownEvent: {\n        type: 'object',\n        description:\n          'Logs cancellation of a scheduled takedown action for an account.',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n        },\n      },\n      repoView: {\n        type: 'object',\n        required: [\n          'did',\n          'handle',\n          'relatedRecords',\n          'indexedAt',\n          'moderation',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoViewDetail: {\n        type: 'object',\n        required: [\n          'did',\n          'handle',\n          'relatedRecords',\n          'indexedAt',\n          'moderation',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            format: 'handle',\n          },\n          email: {\n            type: 'string',\n          },\n          relatedRecords: {\n            type: 'array',\n            items: {\n              type: 'unknown',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderationDetail',\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          invitedBy: {\n            type: 'ref',\n            ref: 'lex:com.atproto.server.defs#inviteCode',\n          },\n          invites: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.server.defs#inviteCode',\n            },\n          },\n          invitesDisabled: {\n            type: 'boolean',\n          },\n          inviteNote: {\n            type: 'string',\n          },\n          emailConfirmedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          threatSignatures: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.admin.defs#threatSignature',\n            },\n          },\n        },\n      },\n      repoViewNotFound: {\n        type: 'object',\n        required: ['did'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n      recordView: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'value',\n          'blobCids',\n          'indexedAt',\n          'moderation',\n          'repo',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n          blobCids: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoView',\n          },\n        },\n      },\n      recordViewDetail: {\n        type: 'object',\n        required: [\n          'uri',\n          'cid',\n          'value',\n          'blobs',\n          'indexedAt',\n          'moderation',\n          'repo',\n        ],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          value: {\n            type: 'unknown',\n          },\n          blobs: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.defs#blobView',\n            },\n          },\n          labels: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:com.atproto.label.defs#label',\n            },\n          },\n          indexedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderationDetail',\n          },\n          repo: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoView',\n          },\n        },\n      },\n      recordViewNotFound: {\n        type: 'object',\n        required: ['uri'],\n        properties: {\n          uri: {\n            type: 'string',\n            format: 'at-uri',\n          },\n        },\n      },\n      moderation: {\n        type: 'object',\n        properties: {\n          subjectStatus: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n        },\n      },\n      moderationDetail: {\n        type: 'object',\n        properties: {\n          subjectStatus: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n          },\n        },\n      },\n      blobView: {\n        type: 'object',\n        required: ['cid', 'mimeType', 'size', 'createdAt'],\n        properties: {\n          cid: {\n            type: 'string',\n            format: 'cid',\n          },\n          mimeType: {\n            type: 'string',\n          },\n          size: {\n            type: 'integer',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          details: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#imageDetails',\n              'lex:tools.ozone.moderation.defs#videoDetails',\n            ],\n          },\n          moderation: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#moderation',\n          },\n        },\n      },\n      imageDetails: {\n        type: 'object',\n        required: ['width', 'height'],\n        properties: {\n          width: {\n            type: 'integer',\n          },\n          height: {\n            type: 'integer',\n          },\n        },\n      },\n      videoDetails: {\n        type: 'object',\n        required: ['width', 'height', 'length'],\n        properties: {\n          width: {\n            type: 'integer',\n          },\n          height: {\n            type: 'integer',\n          },\n          length: {\n            type: 'integer',\n          },\n        },\n      },\n      accountHosting: {\n        type: 'object',\n        required: ['status'],\n        properties: {\n          status: {\n            type: 'string',\n            knownValues: [\n              'takendown',\n              'suspended',\n              'deleted',\n              'deactivated',\n              'unknown',\n            ],\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deletedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          reactivatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      recordHosting: {\n        type: 'object',\n        required: ['status'],\n        properties: {\n          status: {\n            type: 'string',\n            knownValues: ['deleted', 'unknown'],\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          deletedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n      reporterStats: {\n        type: 'object',\n        required: [\n          'did',\n          'accountReportCount',\n          'recordReportCount',\n          'reportedAccountCount',\n          'reportedRecordCount',\n          'takendownAccountCount',\n          'takendownRecordCount',\n          'labeledAccountCount',\n          'labeledRecordCount',\n        ],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          accountReportCount: {\n            type: 'integer',\n            description:\n              'The total number of reports made by the user on accounts.',\n          },\n          recordReportCount: {\n            type: 'integer',\n            description:\n              'The total number of reports made by the user on records.',\n          },\n          reportedAccountCount: {\n            type: 'integer',\n            description: 'The total number of accounts reported by the user.',\n          },\n          reportedRecordCount: {\n            type: 'integer',\n            description: 'The total number of records reported by the user.',\n          },\n          takendownAccountCount: {\n            type: 'integer',\n            description:\n              \"The total number of accounts taken down as a result of the user's reports.\",\n          },\n          takendownRecordCount: {\n            type: 'integer',\n            description:\n              \"The total number of records taken down as a result of the user's reports.\",\n          },\n          labeledAccountCount: {\n            type: 'integer',\n            description:\n              \"The total number of accounts labeled as a result of the user's reports.\",\n          },\n          labeledRecordCount: {\n            type: 'integer',\n            description:\n              \"The total number of records labeled as a result of the user's reports.\",\n          },\n        },\n      },\n      modTool: {\n        type: 'object',\n        description:\n          'Moderation tool information for tracing the source of the action',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            description:\n              \"Name/identifier of the source (e.g., 'automod', 'ozone/workspace')\",\n          },\n          meta: {\n            type: 'unknown',\n            description: 'Additional arbitrary metadata about the source',\n          },\n        },\n      },\n      timelineEventPlcCreate: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for a PLC create operation',\n      },\n      timelineEventPlcOperation: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for generic PLC operation',\n      },\n      timelineEventPlcTombstone: {\n        type: 'token',\n        description:\n          'Moderation event timeline event for a PLC tombstone operation',\n      },\n      scheduledActionView: {\n        type: 'object',\n        description: 'View of a scheduled moderation action',\n        required: ['id', 'action', 'did', 'createdBy', 'createdAt', 'status'],\n        properties: {\n          id: {\n            type: 'integer',\n            description: 'Auto-incrementing row ID',\n          },\n          action: {\n            type: 'string',\n            knownValues: ['takedown'],\n            description: 'Type of action to be executed',\n          },\n          eventData: {\n            type: 'unknown',\n            description:\n              'Serialized event object that will be propagated to the event when performed',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n            description: 'Subject DID for the action',\n          },\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Exact time to execute the action',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Earliest time to execute the action (for randomized scheduling)',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Latest time to execute the action (for randomized scheduling)',\n          },\n          randomizeExecution: {\n            type: 'boolean',\n            description:\n              'Whether execution time should be randomized within the specified range',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who created this scheduled action',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the scheduled action was created',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the scheduled action was last updated',\n          },\n          status: {\n            type: 'string',\n            knownValues: ['pending', 'executed', 'cancelled', 'failed'],\n            description: 'Current status of the scheduled action',\n          },\n          lastExecutedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'When the action was last attempted to be executed',\n          },\n          lastFailureReason: {\n            type: 'string',\n            description: 'Reason for the last execution failure',\n          },\n          executionEventId: {\n            type: 'integer',\n            description:\n              'ID of the moderation event created when action was successfully executed',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationEmitEvent: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.emitEvent',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Take a moderation action on an actor.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['event', 'subject', 'createdBy'],\n            properties: {\n              event: {\n                type: 'union',\n                refs: [\n                  'lex:tools.ozone.moderation.defs#modEventTakedown',\n                  'lex:tools.ozone.moderation.defs#modEventAcknowledge',\n                  'lex:tools.ozone.moderation.defs#modEventEscalate',\n                  'lex:tools.ozone.moderation.defs#modEventComment',\n                  'lex:tools.ozone.moderation.defs#modEventLabel',\n                  'lex:tools.ozone.moderation.defs#modEventReport',\n                  'lex:tools.ozone.moderation.defs#modEventMute',\n                  'lex:tools.ozone.moderation.defs#modEventUnmute',\n                  'lex:tools.ozone.moderation.defs#modEventMuteReporter',\n                  'lex:tools.ozone.moderation.defs#modEventUnmuteReporter',\n                  'lex:tools.ozone.moderation.defs#modEventReverseTakedown',\n                  'lex:tools.ozone.moderation.defs#modEventResolveAppeal',\n                  'lex:tools.ozone.moderation.defs#modEventEmail',\n                  'lex:tools.ozone.moderation.defs#modEventDivert',\n                  'lex:tools.ozone.moderation.defs#modEventTag',\n                  'lex:tools.ozone.moderation.defs#accountEvent',\n                  'lex:tools.ozone.moderation.defs#identityEvent',\n                  'lex:tools.ozone.moderation.defs#recordEvent',\n                  'lex:tools.ozone.moderation.defs#modEventPriorityScore',\n                  'lex:tools.ozone.moderation.defs#ageAssuranceEvent',\n                  'lex:tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n                  'lex:tools.ozone.moderation.defs#ageAssurancePurgeEvent',\n                  'lex:tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n                  'lex:tools.ozone.moderation.defs#scheduleTakedownEvent',\n                  'lex:tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n                ],\n              },\n              subject: {\n                type: 'union',\n                refs: [\n                  'lex:com.atproto.admin.defs#repoRef',\n                  'lex:com.atproto.repo.strongRef',\n                ],\n              },\n              subjectBlobCids: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                  format: 'cid',\n                },\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.defs#modTool',\n              },\n              externalId: {\n                type: 'string',\n                description:\n                  'An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modEventView',\n          },\n        },\n        errors: [\n          {\n            name: 'SubjectHasAction',\n          },\n          {\n            name: 'DuplicateExternalId',\n            description:\n              'An event with the same external ID already exists for the subject.',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetAccountTimeline: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getAccountTimeline',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get timeline of all available events of an account. This includes moderation events, account history and did history.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['timeline'],\n            properties: {\n              timeline: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItem',\n                },\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n      timelineItem: {\n        type: 'object',\n        required: ['day', 'summary'],\n        properties: {\n          day: {\n            type: 'string',\n          },\n          summary: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItemSummary',\n            },\n          },\n        },\n      },\n      timelineItemSummary: {\n        type: 'object',\n        required: ['eventSubjectType', 'eventType', 'count'],\n        properties: {\n          eventSubjectType: {\n            type: 'string',\n            knownValues: ['account', 'record', 'chat'],\n          },\n          eventType: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.moderation.defs#modEventTakedown',\n              'tools.ozone.moderation.defs#modEventReverseTakedown',\n              'tools.ozone.moderation.defs#modEventComment',\n              'tools.ozone.moderation.defs#modEventReport',\n              'tools.ozone.moderation.defs#modEventLabel',\n              'tools.ozone.moderation.defs#modEventAcknowledge',\n              'tools.ozone.moderation.defs#modEventEscalate',\n              'tools.ozone.moderation.defs#modEventMute',\n              'tools.ozone.moderation.defs#modEventUnmute',\n              'tools.ozone.moderation.defs#modEventMuteReporter',\n              'tools.ozone.moderation.defs#modEventUnmuteReporter',\n              'tools.ozone.moderation.defs#modEventEmail',\n              'tools.ozone.moderation.defs#modEventResolveAppeal',\n              'tools.ozone.moderation.defs#modEventDivert',\n              'tools.ozone.moderation.defs#modEventTag',\n              'tools.ozone.moderation.defs#accountEvent',\n              'tools.ozone.moderation.defs#identityEvent',\n              'tools.ozone.moderation.defs#recordEvent',\n              'tools.ozone.moderation.defs#modEventPriorityScore',\n              'tools.ozone.moderation.defs#revokeAccountCredentialsEvent',\n              'tools.ozone.moderation.defs#ageAssuranceEvent',\n              'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',\n              'tools.ozone.moderation.defs#timelineEventPlcCreate',\n              'tools.ozone.moderation.defs#timelineEventPlcOperation',\n              'tools.ozone.moderation.defs#timelineEventPlcTombstone',\n              'tools.ozone.hosting.getAccountHistory#accountCreated',\n              'tools.ozone.hosting.getAccountHistory#emailConfirmed',\n              'tools.ozone.hosting.getAccountHistory#passwordUpdated',\n              'tools.ozone.hosting.getAccountHistory#handleUpdated',\n              'tools.ozone.moderation.defs#scheduleTakedownEvent',\n              'tools.ozone.moderation.defs#cancelScheduledTakedownEvent',\n            ],\n          },\n          count: {\n            type: 'integer',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetEvent: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getEvent',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a moderation event.',\n        parameters: {\n          type: 'params',\n          required: ['id'],\n          properties: {\n            id: {\n              type: 'integer',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#modEventViewDetail',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRecord: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRecord',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a record.',\n        parameters: {\n          type: 'params',\n          required: ['uri'],\n          properties: {\n            uri: {\n              type: 'string',\n              format: 'at-uri',\n            },\n            cid: {\n              type: 'string',\n              format: 'cid',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#recordViewDetail',\n          },\n        },\n        errors: [\n          {\n            name: 'RecordNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetRecords: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRecords',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some records.',\n        parameters: {\n          type: 'params',\n          required: ['uris'],\n          properties: {\n            uris: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'at-uri',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['records'],\n            properties: {\n              records: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:tools.ozone.moderation.defs#recordViewDetail',\n                    'lex:tools.ozone.moderation.defs#recordViewNotFound',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRepo: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRepo',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about a repository.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.defs#repoViewDetail',\n          },\n        },\n        errors: [\n          {\n            name: 'RepoNotFound',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneModerationGetReporterStats: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getReporterStats',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get reporter stats for a list of users.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['stats'],\n            properties: {\n              stats: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#reporterStats',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetRepos: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about some repositories.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'union',\n                  refs: [\n                    'lex:tools.ozone.moderation.defs#repoViewDetail',\n                    'lex:tools.ozone.moderation.defs#repoViewNotFound',\n                  ],\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationGetSubjects: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.getSubjects',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get details about subjects.',\n        parameters: {\n          type: 'params',\n          required: ['subjects'],\n          properties: {\n            subjects: {\n              type: 'array',\n              maxLength: 100,\n              minLength: 1,\n              items: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjects'],\n            properties: {\n              subjects: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#subjectView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationListScheduledActions: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.listScheduledActions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'List scheduled moderation actions with optional filtering',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['statuses'],\n            properties: {\n              startsAfter: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'Filter actions scheduled to execute after this time',\n              },\n              endsBefore: {\n                type: 'string',\n                format: 'datetime',\n                description:\n                  'Filter actions scheduled to execute before this time',\n              },\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description: 'Filter actions for specific DID subjects',\n              },\n              statuses: {\n                type: 'array',\n                minLength: 1,\n                items: {\n                  type: 'string',\n                  knownValues: ['pending', 'executed', 'cancelled', 'failed'],\n                },\n                description: 'Filter actions by status',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['actions'],\n            properties: {\n              actions: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#scheduledActionView',\n                },\n              },\n              cursor: {\n                type: 'string',\n                description: 'Cursor for next page of results',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationQueryEvents: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.queryEvents',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List moderation events related to a subject.',\n        parameters: {\n          type: 'params',\n          properties: {\n            types: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned.',\n            },\n            createdBy: {\n              type: 'string',\n              format: 'did',\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'desc',\n              enum: ['asc', 'desc'],\n              description:\n                'Sort direction for the events. Defaults to descending order of created at timestamp.',\n            },\n            createdAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Retrieve events created after a given timestamp',\n            },\n            createdBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Retrieve events created before a given timestamp',\n            },\n            subject: {\n              type: 'string',\n              format: 'uri',\n            },\n            collections: {\n              type: 'array',\n              maxLength: 20,\n              description:\n                \"If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n            subjectType: {\n              type: 'string',\n              description:\n                \"If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n              knownValues: ['account', 'record'],\n            },\n            includeAllUserRecords: {\n              type: 'boolean',\n              default: false,\n              description:\n                \"If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned.\",\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            hasComment: {\n              type: 'boolean',\n              description: 'If true, only events with comments are returned',\n            },\n            comment: {\n              type: 'string',\n              description:\n                'If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition.',\n            },\n            addedLabels: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these labels were added are returned',\n            },\n            removedLabels: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these labels were removed are returned',\n            },\n            addedTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these tags were added are returned',\n            },\n            removedTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where all of these tags were removed are returned',\n            },\n            reportTypes: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            policies: {\n              type: 'array',\n              items: {\n                type: 'string',\n                description:\n                  'If specified, only events where the action policies match any of the given policies are returned',\n              },\n            },\n            modTool: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'If specified, only events where the modTool name matches any of the given values are returned',\n            },\n            batchId: {\n              type: 'string',\n              description:\n                'If specified, only events where the batchId matches the given value are returned',\n            },\n            ageAssuranceState: {\n              type: 'string',\n              description:\n                'If specified, only events where the age assurance state matches the given value are returned',\n              knownValues: [\n                'pending',\n                'assured',\n                'unknown',\n                'reset',\n                'blocked',\n              ],\n            },\n            withStrike: {\n              type: 'boolean',\n              description:\n                'If specified, only events where strikeCount value is set are returned.',\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#modEventView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationQueryStatuses: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.queryStatuses',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'View moderation statuses of subjects (record or repo).',\n        parameters: {\n          type: 'params',\n          properties: {\n            queueCount: {\n              type: 'integer',\n              description:\n                'Number of queues being used by moderators. Subjects will be split among all queues.',\n            },\n            queueIndex: {\n              type: 'integer',\n              description:\n                'Index of the queue to fetch subjects from. Works only when queueCount value is specified.',\n            },\n            queueSeed: {\n              type: 'string',\n              description: 'A seeder to shuffle/balance the queue items.',\n            },\n            includeAllUserRecords: {\n              type: 'boolean',\n              description:\n                \"All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned.\",\n            },\n            subject: {\n              type: 'string',\n              format: 'uri',\n              description: 'The subject to get the status for.',\n            },\n            comment: {\n              type: 'string',\n              description: 'Search subjects by keyword from comments',\n            },\n            reportedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reported after a given timestamp',\n            },\n            reportedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reported before a given timestamp',\n            },\n            reviewedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reviewed after a given timestamp',\n            },\n            hostingDeletedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was deleted after a given timestamp',\n            },\n            hostingDeletedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was deleted before a given timestamp',\n            },\n            hostingUpdatedAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was updated after a given timestamp',\n            },\n            hostingUpdatedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Search subjects where the associated record/account was updated before a given timestamp',\n            },\n            hostingStatuses: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              description:\n                'Search subjects by the status of the associated record/account',\n            },\n            reviewedBefore: {\n              type: 'string',\n              format: 'datetime',\n              description: 'Search subjects reviewed before a given timestamp',\n            },\n            includeMuted: {\n              type: 'boolean',\n              description:\n                \"By default, we don't include muted subjects in the results. Set this to true to include them.\",\n            },\n            onlyMuted: {\n              type: 'boolean',\n              description:\n                'When set to true, only muted subjects and reporters will be returned.',\n            },\n            reviewState: {\n              type: 'string',\n              description: 'Specify when fetching subjects in a certain state',\n              knownValues: [\n                'tools.ozone.moderation.defs#reviewOpen',\n                'tools.ozone.moderation.defs#reviewClosed',\n                'tools.ozone.moderation.defs#reviewEscalated',\n                'tools.ozone.moderation.defs#reviewNone',\n              ],\n            },\n            ignoreSubjects: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'uri',\n              },\n            },\n            lastReviewedBy: {\n              type: 'string',\n              format: 'did',\n              description:\n                'Get all subject statuses that were reviewed by a specific moderator',\n            },\n            sortField: {\n              type: 'string',\n              default: 'lastReportedAt',\n              enum: [\n                'lastReviewedAt',\n                'lastReportedAt',\n                'reportedRecordsCount',\n                'takendownRecordsCount',\n                'priorityScore',\n              ],\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'desc',\n              enum: ['asc', 'desc'],\n            },\n            takendown: {\n              type: 'boolean',\n              description: 'Get subjects that were taken down',\n            },\n            appealed: {\n              type: 'boolean',\n              description: 'Get subjects in unresolved appealed status',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            tags: {\n              type: 'array',\n              maxLength: 25,\n              items: {\n                type: 'string',\n                description:\n                  'Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters',\n              },\n            },\n            excludeTags: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            collections: {\n              type: 'array',\n              maxLength: 20,\n              description:\n                \"If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored.\",\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n            },\n            subjectType: {\n              type: 'string',\n              description:\n                \"If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored.\",\n              knownValues: ['account', 'record'],\n            },\n            minAccountSuspendCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many suspensions will be returned.',\n            },\n            minReportedRecordsCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many reported records will be returned.',\n            },\n            minTakendownRecordsCount: {\n              type: 'integer',\n              description:\n                'If specified, only subjects that belong to an account that has at least this many taken down records will be returned.',\n            },\n            minPriorityScore: {\n              minimum: 0,\n              maximum: 100,\n              type: 'integer',\n              description:\n                'If specified, only subjects that have priority score value above the given value will be returned.',\n            },\n            minStrikeCount: {\n              type: 'integer',\n              minimum: 1,\n              description:\n                'If specified, only subjects that belong to an account that has at least this many active strikes will be returned.',\n            },\n            ageAssuranceState: {\n              type: 'string',\n              description:\n                'If specified, only subjects with the given age assurance state will be returned.',\n              knownValues: [\n                'pending',\n                'assured',\n                'unknown',\n                'reset',\n                'blocked',\n              ],\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['subjectStatuses'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              subjectStatuses: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#subjectStatusView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationScheduleAction: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.scheduleAction',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Schedule a moderation action to be executed at a future time',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['action', 'subjects', 'createdBy', 'scheduling'],\n            properties: {\n              action: {\n                type: 'union',\n                refs: ['lex:tools.ozone.moderation.scheduleAction#takedown'],\n              },\n              subjects: {\n                type: 'array',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  format: 'did',\n                },\n                description: 'Array of DID subjects to schedule the action for',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n              },\n              scheduling: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.scheduleAction#schedulingConfig',\n              },\n              modTool: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.moderation.defs#modTool',\n                description:\n                  'This will be propagated to the moderation event when it is applied',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.moderation.scheduleAction#scheduledActionResults',\n          },\n        },\n      },\n      takedown: {\n        type: 'object',\n        description: 'Schedule a takedown action',\n        properties: {\n          comment: {\n            type: 'string',\n          },\n          durationInHours: {\n            type: 'integer',\n            description:\n              'Indicates how long the takedown should be in effect before automatically expiring.',\n          },\n          acknowledgeAccountSubjects: {\n            type: 'boolean',\n            description:\n              'If true, all other reports on content authored by this account will be resolved (acknowledged).',\n          },\n          policies: {\n            type: 'array',\n            maxLength: 5,\n            items: {\n              type: 'string',\n            },\n            description:\n              'Names/Keywords of the policies that drove the decision.',\n          },\n          severityLevel: {\n            type: 'string',\n            description:\n              \"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).\",\n          },\n          strikeCount: {\n            type: 'integer',\n            description:\n              'Number of strikes to assign to the user when takedown is applied.',\n          },\n          strikeExpiresAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'When the strike should expire. If not provided, the strike never expires.',\n          },\n          emailContent: {\n            type: 'string',\n            description: 'Email content to be sent to the user upon takedown.',\n          },\n          emailSubject: {\n            type: 'string',\n            description:\n              'Subject of the email to be sent to the user upon takedown.',\n          },\n        },\n      },\n      schedulingConfig: {\n        type: 'object',\n        description: 'Configuration for when the action should be executed',\n        properties: {\n          executeAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Exact time to execute the action',\n          },\n          executeAfter: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Earliest time to execute the action (for randomized scheduling)',\n          },\n          executeUntil: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Latest time to execute the action (for randomized scheduling)',\n          },\n        },\n      },\n      scheduledActionResults: {\n        type: 'object',\n        required: ['succeeded', 'failed'],\n        properties: {\n          succeeded: {\n            type: 'array',\n            items: {\n              type: 'string',\n              format: 'did',\n            },\n          },\n          failed: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.moderation.scheduleAction#failedScheduling',\n            },\n          },\n        },\n      },\n      failedScheduling: {\n        type: 'object',\n        required: ['subject', 'error'],\n        properties: {\n          subject: {\n            type: 'string',\n            format: 'did',\n          },\n          error: {\n            type: 'string',\n          },\n          errorCode: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneModerationSearchRepos: {\n    lexicon: 1,\n    id: 'tools.ozone.moderation.searchRepos',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Find repositories based on a search term.',\n        parameters: {\n          type: 'params',\n          properties: {\n            term: {\n              type: 'string',\n              description: \"DEPRECATED: use 'q' instead\",\n            },\n            q: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['repos'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              repos: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.moderation.defs#repoView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneReportDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.report.defs',\n    defs: {\n      reasonType: {\n        type: 'string',\n        knownValues: [\n          'tools.ozone.report.defs#reasonAppeal',\n          'tools.ozone.report.defs#reasonOther',\n          'tools.ozone.report.defs#reasonViolenceAnimal',\n          'tools.ozone.report.defs#reasonViolenceThreats',\n          'tools.ozone.report.defs#reasonViolenceGraphicContent',\n          'tools.ozone.report.defs#reasonViolenceGlorification',\n          'tools.ozone.report.defs#reasonViolenceExtremistContent',\n          'tools.ozone.report.defs#reasonViolenceTrafficking',\n          'tools.ozone.report.defs#reasonViolenceOther',\n          'tools.ozone.report.defs#reasonSexualAbuseContent',\n          'tools.ozone.report.defs#reasonSexualNCII',\n          'tools.ozone.report.defs#reasonSexualDeepfake',\n          'tools.ozone.report.defs#reasonSexualAnimal',\n          'tools.ozone.report.defs#reasonSexualUnlabeled',\n          'tools.ozone.report.defs#reasonSexualOther',\n          'tools.ozone.report.defs#reasonChildSafetyCSAM',\n          'tools.ozone.report.defs#reasonChildSafetyGroom',\n          'tools.ozone.report.defs#reasonChildSafetyPrivacy',\n          'tools.ozone.report.defs#reasonChildSafetyHarassment',\n          'tools.ozone.report.defs#reasonChildSafetyOther',\n          'tools.ozone.report.defs#reasonHarassmentTroll',\n          'tools.ozone.report.defs#reasonHarassmentTargeted',\n          'tools.ozone.report.defs#reasonHarassmentHateSpeech',\n          'tools.ozone.report.defs#reasonHarassmentDoxxing',\n          'tools.ozone.report.defs#reasonHarassmentOther',\n          'tools.ozone.report.defs#reasonMisleadingBot',\n          'tools.ozone.report.defs#reasonMisleadingImpersonation',\n          'tools.ozone.report.defs#reasonMisleadingSpam',\n          'tools.ozone.report.defs#reasonMisleadingScam',\n          'tools.ozone.report.defs#reasonMisleadingElections',\n          'tools.ozone.report.defs#reasonMisleadingOther',\n          'tools.ozone.report.defs#reasonRuleSiteSecurity',\n          'tools.ozone.report.defs#reasonRuleProhibitedSales',\n          'tools.ozone.report.defs#reasonRuleBanEvasion',\n          'tools.ozone.report.defs#reasonRuleOther',\n          'tools.ozone.report.defs#reasonSelfHarmContent',\n          'tools.ozone.report.defs#reasonSelfHarmED',\n          'tools.ozone.report.defs#reasonSelfHarmStunts',\n          'tools.ozone.report.defs#reasonSelfHarmSubstances',\n          'tools.ozone.report.defs#reasonSelfHarmOther',\n        ],\n      },\n      reasonAppeal: {\n        type: 'token',\n        description: 'Appeal a previously taken moderation action',\n      },\n      reasonOther: {\n        type: 'token',\n        description: 'An issue not included in these options',\n      },\n      reasonViolenceAnimal: {\n        type: 'token',\n        description: 'Animal welfare violations',\n      },\n      reasonViolenceThreats: {\n        type: 'token',\n        description: 'Threats or incitement',\n      },\n      reasonViolenceGraphicContent: {\n        type: 'token',\n        description: 'Graphic violent content',\n      },\n      reasonViolenceGlorification: {\n        type: 'token',\n        description: 'Glorification of violence',\n      },\n      reasonViolenceExtremistContent: {\n        type: 'token',\n        description:\n          \"Extremist content. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonViolenceTrafficking: {\n        type: 'token',\n        description: 'Human trafficking',\n      },\n      reasonViolenceOther: {\n        type: 'token',\n        description: 'Other violent content',\n      },\n      reasonSexualAbuseContent: {\n        type: 'token',\n        description: 'Adult sexual abuse content',\n      },\n      reasonSexualNCII: {\n        type: 'token',\n        description: 'Non-consensual intimate imagery',\n      },\n      reasonSexualDeepfake: {\n        type: 'token',\n        description: 'Deepfake adult content',\n      },\n      reasonSexualAnimal: {\n        type: 'token',\n        description: 'Animal sexual abuse',\n      },\n      reasonSexualUnlabeled: {\n        type: 'token',\n        description: 'Unlabelled adult content',\n      },\n      reasonSexualOther: {\n        type: 'token',\n        description: 'Other sexual violence content',\n      },\n      reasonChildSafetyCSAM: {\n        type: 'token',\n        description:\n          \"Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonChildSafetyGroom: {\n        type: 'token',\n        description:\n          \"Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonChildSafetyPrivacy: {\n        type: 'token',\n        description: 'Privacy violation involving a minor',\n      },\n      reasonChildSafetyHarassment: {\n        type: 'token',\n        description: 'Harassment or bullying of minors',\n      },\n      reasonChildSafetyOther: {\n        type: 'token',\n        description:\n          \"Other child safety. These reports will be sent only be sent to the application's Moderation Authority.\",\n      },\n      reasonHarassmentTroll: {\n        type: 'token',\n        description: 'Trolling',\n      },\n      reasonHarassmentTargeted: {\n        type: 'token',\n        description: 'Targeted harassment',\n      },\n      reasonHarassmentHateSpeech: {\n        type: 'token',\n        description: 'Hate speech',\n      },\n      reasonHarassmentDoxxing: {\n        type: 'token',\n        description: 'Doxxing',\n      },\n      reasonHarassmentOther: {\n        type: 'token',\n        description: 'Other harassing or hateful content',\n      },\n      reasonMisleadingBot: {\n        type: 'token',\n        description: 'Fake account or bot',\n      },\n      reasonMisleadingImpersonation: {\n        type: 'token',\n        description: 'Impersonation',\n      },\n      reasonMisleadingSpam: {\n        type: 'token',\n        description: 'Spam',\n      },\n      reasonMisleadingScam: {\n        type: 'token',\n        description: 'Scam',\n      },\n      reasonMisleadingElections: {\n        type: 'token',\n        description: 'False information about elections',\n      },\n      reasonMisleadingOther: {\n        type: 'token',\n        description: 'Other misleading content',\n      },\n      reasonRuleSiteSecurity: {\n        type: 'token',\n        description: 'Hacking or system attacks',\n      },\n      reasonRuleProhibitedSales: {\n        type: 'token',\n        description: 'Promoting or selling prohibited items or services',\n      },\n      reasonRuleBanEvasion: {\n        type: 'token',\n        description: 'Banned user returning',\n      },\n      reasonRuleOther: {\n        type: 'token',\n        description: 'Other',\n      },\n      reasonSelfHarmContent: {\n        type: 'token',\n        description: 'Content promoting or depicting self-harm',\n      },\n      reasonSelfHarmED: {\n        type: 'token',\n        description: 'Eating disorders',\n      },\n      reasonSelfHarmStunts: {\n        type: 'token',\n        description: 'Dangerous challenges or activities',\n      },\n      reasonSelfHarmSubstances: {\n        type: 'token',\n        description: 'Dangerous substances or drug abuse',\n      },\n      reasonSelfHarmOther: {\n        type: 'token',\n        description: 'Other dangerous content',\n      },\n    },\n  },\n  ToolsOzoneSafelinkAddRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.addRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a new URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern', 'action', 'reason'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to apply the rule to',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              action: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#actionType',\n              },\n              reason: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#reasonType',\n              },\n              comment: {\n                type: 'string',\n                description: 'Optional comment about the decision',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'Author DID. Only respected when using admin auth',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'InvalidUrl',\n            description: 'The provided URL is invalid',\n          },\n          {\n            name: 'RuleAlreadyExists',\n            description: 'A rule for this URL/domain already exists',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSafelinkDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.defs',\n    defs: {\n      event: {\n        type: 'object',\n        description: 'An event for URL safety decisions',\n        required: [\n          'id',\n          'eventType',\n          'url',\n          'pattern',\n          'action',\n          'reason',\n          'createdBy',\n          'createdAt',\n        ],\n        properties: {\n          id: {\n            type: 'integer',\n            description: 'Auto-incrementing row ID',\n          },\n          eventType: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#eventType',\n          },\n          url: {\n            type: 'string',\n            description: 'The URL that this rule applies to',\n          },\n          pattern: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#patternType',\n          },\n          action: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#actionType',\n          },\n          reason: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#reasonType',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user who created this rule',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          comment: {\n            type: 'string',\n            description: 'Optional comment about the decision',\n          },\n        },\n      },\n      eventType: {\n        type: 'string',\n        knownValues: ['addRule', 'updateRule', 'removeRule'],\n      },\n      patternType: {\n        type: 'string',\n        knownValues: ['domain', 'url'],\n      },\n      actionType: {\n        type: 'string',\n        knownValues: ['block', 'warn', 'whitelist'],\n      },\n      reasonType: {\n        type: 'string',\n        knownValues: ['csam', 'spam', 'phishing', 'none'],\n      },\n      urlRule: {\n        type: 'object',\n        description: 'Input for creating a URL safety rule',\n        required: [\n          'url',\n          'pattern',\n          'action',\n          'reason',\n          'createdBy',\n          'createdAt',\n          'updatedAt',\n        ],\n        properties: {\n          url: {\n            type: 'string',\n            description: 'The URL or domain to apply the rule to',\n          },\n          pattern: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#patternType',\n          },\n          action: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#actionType',\n          },\n          reason: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#reasonType',\n          },\n          comment: {\n            type: 'string',\n            description: 'Optional comment about the decision',\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n            description: 'DID of the user added the rule.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when the rule was created',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n            description: 'Timestamp when the rule was last updated',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkQueryEvents: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.queryEvents',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Query URL safety audit events',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              urls: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by specific URLs or domains',\n              },\n              patternType: {\n                type: 'string',\n                description: 'Filter by pattern type',\n              },\n              sortDirection: {\n                type: 'string',\n                knownValues: ['asc', 'desc'],\n                default: 'desc',\n                description: 'Sort direction',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['events'],\n            properties: {\n              cursor: {\n                type: 'string',\n                description:\n                  'Next cursor for pagination. Only present if there are more results.',\n              },\n              events: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.safelink.defs#event',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkQueryRules: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.queryRules',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Query URL safety rules',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cursor: {\n                type: 'string',\n                description: 'Cursor for pagination',\n              },\n              limit: {\n                type: 'integer',\n                minimum: 1,\n                maximum: 100,\n                default: 50,\n                description: 'Maximum number of results to return',\n              },\n              urls: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by specific URLs or domains',\n              },\n              patternType: {\n                type: 'string',\n                description: 'Filter by pattern type',\n              },\n              actions: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n                description: 'Filter by action types',\n              },\n              reason: {\n                type: 'string',\n                description: 'Filter by reason type',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description: 'Filter by rule creator',\n              },\n              sortDirection: {\n                type: 'string',\n                knownValues: ['asc', 'desc'],\n                default: 'desc',\n                description: 'Sort direction',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['rules'],\n            properties: {\n              cursor: {\n                type: 'string',\n                description:\n                  'Next cursor for pagination. Only present if there are more results.',\n              },\n              rules: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.safelink.defs#urlRule',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSafelinkRemoveRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.removeRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Remove an existing URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to remove the rule for',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              comment: {\n                type: 'string',\n                description:\n                  'Optional comment about why the rule is being removed',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Optional DID of the user. Only respected when using admin auth.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'RuleNotFound',\n            description: 'No active rule found for this URL/domain',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSafelinkUpdateRule: {\n    lexicon: 1,\n    id: 'tools.ozone.safelink.updateRule',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Update an existing URL safety rule',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['url', 'pattern', 'action', 'reason'],\n            properties: {\n              url: {\n                type: 'string',\n                description: 'The URL or domain to update the rule for',\n              },\n              pattern: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#patternType',\n              },\n              action: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#actionType',\n              },\n              reason: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.safelink.defs#reasonType',\n              },\n              comment: {\n                type: 'string',\n                description: 'Optional comment about the update',\n              },\n              createdBy: {\n                type: 'string',\n                format: 'did',\n                description:\n                  'Optional DID to credit as the creator. Only respected for admin_token authentication.',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.safelink.defs#event',\n          },\n        },\n        errors: [\n          {\n            name: 'RuleNotFound',\n            description: 'No active rule found for this URL/domain',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneServerGetConfig: {\n    lexicon: 1,\n    id: 'tools.ozone.server.getConfig',\n    defs: {\n      main: {\n        type: 'query',\n        description: \"Get details about ozone's server configuration.\",\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              appview: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              pds: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              blobDivert: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              chat: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#serviceConfig',\n              },\n              viewer: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.server.getConfig#viewerConfig',\n              },\n              verifierDid: {\n                type: 'string',\n                format: 'did',\n                description: 'The did of the verifier used for verification.',\n              },\n            },\n          },\n        },\n      },\n      serviceConfig: {\n        type: 'object',\n        properties: {\n          url: {\n            type: 'string',\n            format: 'uri',\n          },\n        },\n      },\n      viewerConfig: {\n        type: 'object',\n        properties: {\n          role: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetAddValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.addValues',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Add values to a specific set. Attempting to add values to a set that does not exist will result in an error.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name', 'values'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to add values to',\n              },\n              values: {\n                type: 'array',\n                minLength: 1,\n                maxLength: 1000,\n                items: {\n                  type: 'string',\n                },\n                description: 'Array of string values to add to the set',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.set.defs',\n    defs: {\n      set: {\n        type: 'object',\n        required: ['name'],\n        properties: {\n          name: {\n            type: 'string',\n            minLength: 3,\n            maxLength: 128,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n        },\n      },\n      setView: {\n        type: 'object',\n        required: ['name', 'setSize', 'createdAt', 'updatedAt'],\n        properties: {\n          name: {\n            type: 'string',\n            minLength: 3,\n            maxLength: 128,\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n          setSize: {\n            type: 'integer',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetDeleteSet: {\n    lexicon: 1,\n    id: 'tools.ozone.set.deleteSet',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Delete an entire set. Attempting to delete a set that does not exist will result in an error.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to delete',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetDeleteValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.deleteValues',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Delete values from a specific set. Attempting to delete values that are not in the set will not result in an error',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['name', 'values'],\n            properties: {\n              name: {\n                type: 'string',\n                description: 'Name of the set to delete values from',\n              },\n              values: {\n                type: 'array',\n                minLength: 1,\n                items: {\n                  type: 'string',\n                },\n                description: 'Array of string values to delete from the set',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetGetValues: {\n    lexicon: 1,\n    id: 'tools.ozone.set.getValues',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Get a specific set and its values',\n        parameters: {\n          type: 'params',\n          required: ['name'],\n          properties: {\n            name: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 1000,\n              default: 100,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['set', 'values'],\n            properties: {\n              set: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.set.defs#setView',\n              },\n              values: {\n                type: 'array',\n                items: {\n                  type: 'string',\n                },\n              },\n              cursor: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'SetNotFound',\n            description: 'set with the given name does not exist',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneSetQuerySets: {\n    lexicon: 1,\n    id: 'tools.ozone.set.querySets',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'Query available sets',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            namePrefix: {\n              type: 'string',\n            },\n            sortBy: {\n              type: 'string',\n              enum: ['name', 'createdAt', 'updatedAt'],\n              default: 'name',\n            },\n            sortDirection: {\n              type: 'string',\n              default: 'asc',\n              enum: ['asc', 'desc'],\n              description: 'Defaults to ascending order of name field.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['sets'],\n            properties: {\n              sets: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.set.defs#setView',\n                },\n              },\n              cursor: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSetUpsertSet: {\n    lexicon: 1,\n    id: 'tools.ozone.set.upsertSet',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create or update set metadata',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.set.defs#set',\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.set.defs#setView',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.defs',\n    defs: {\n      option: {\n        type: 'object',\n        required: [\n          'key',\n          'value',\n          'did',\n          'scope',\n          'createdBy',\n          'lastUpdatedBy',\n        ],\n        properties: {\n          key: {\n            type: 'string',\n            format: 'nsid',\n          },\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          value: {\n            type: 'unknown',\n          },\n          description: {\n            type: 'string',\n            maxGraphemes: 1024,\n            maxLength: 10240,\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          managerRole: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n          scope: {\n            type: 'string',\n            knownValues: ['instance', 'personal'],\n          },\n          createdBy: {\n            type: 'string',\n            format: 'did',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingListOptions: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.listOptions',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List settings with optional filtering',\n        parameters: {\n          type: 'params',\n          properties: {\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n            scope: {\n              type: 'string',\n              knownValues: ['instance', 'personal'],\n              default: 'instance',\n            },\n            prefix: {\n              type: 'string',\n              description: 'Filter keys by prefix',\n            },\n            keys: {\n              type: 'array',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'nsid',\n              },\n              description:\n                'Filter for only the specified keys. Ignored if prefix is provided',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['options'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              options: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.setting.defs#option',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingRemoveOptions: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.removeOptions',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete settings by key',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['keys', 'scope'],\n            properties: {\n              keys: {\n                type: 'array',\n                minLength: 1,\n                maxLength: 200,\n                items: {\n                  type: 'string',\n                  format: 'nsid',\n                },\n              },\n              scope: {\n                type: 'string',\n                knownValues: ['instance', 'personal'],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSettingUpsertOption: {\n    lexicon: 1,\n    id: 'tools.ozone.setting.upsertOption',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Create or update setting option',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['key', 'scope', 'value'],\n            properties: {\n              key: {\n                type: 'string',\n                format: 'nsid',\n              },\n              scope: {\n                type: 'string',\n                knownValues: ['instance', 'personal'],\n              },\n              value: {\n                type: 'unknown',\n              },\n              description: {\n                type: 'string',\n                maxLength: 2000,\n              },\n              managerRole: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleTriage',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleAdmin',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['option'],\n            properties: {\n              option: {\n                type: 'ref',\n                ref: 'lex:tools.ozone.setting.defs#option',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.defs',\n    defs: {\n      sigDetail: {\n        type: 'object',\n        required: ['property', 'value'],\n        properties: {\n          property: {\n            type: 'string',\n          },\n          value: {\n            type: 'string',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureFindCorrelation: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.findCorrelation',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Find all correlated threat signatures between 2 or more accounts.',\n        parameters: {\n          type: 'params',\n          required: ['dids'],\n          properties: {\n            dids: {\n              type: 'array',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['details'],\n            properties: {\n              details: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.signature.defs#sigDetail',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureFindRelatedAccounts: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.findRelatedAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Get accounts that share some matching threat signatures with the root account.',\n        parameters: {\n          type: 'params',\n          required: ['did'],\n          properties: {\n            did: {\n              type: 'string',\n              format: 'did',\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.signature.findRelatedAccounts#relatedAccount',\n                },\n              },\n            },\n          },\n        },\n      },\n      relatedAccount: {\n        type: 'object',\n        required: ['account'],\n        properties: {\n          account: {\n            type: 'ref',\n            ref: 'lex:com.atproto.admin.defs#accountView',\n          },\n          similarities: {\n            type: 'array',\n            items: {\n              type: 'ref',\n              ref: 'lex:tools.ozone.signature.defs#sigDetail',\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneSignatureSearchAccounts: {\n    lexicon: 1,\n    id: 'tools.ozone.signature.searchAccounts',\n    defs: {\n      main: {\n        type: 'query',\n        description:\n          'Search for accounts that match one or more threat signature values.',\n        parameters: {\n          type: 'params',\n          required: ['values'],\n          properties: {\n            values: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            cursor: {\n              type: 'string',\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['accounts'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              accounts: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:com.atproto.admin.defs#accountView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneTeamAddMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.addMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Add a member to the ozone team. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did', 'role'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              role: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleAdmin',\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleTriage',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.team.defs#member',\n          },\n        },\n        errors: [\n          {\n            name: 'MemberAlreadyExists',\n            description: 'Member already exists in the team.',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneTeamDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.team.defs',\n    defs: {\n      member: {\n        type: 'object',\n        required: ['did', 'role'],\n        properties: {\n          did: {\n            type: 'string',\n            format: 'did',\n          },\n          disabled: {\n            type: 'boolean',\n          },\n          profile: {\n            type: 'ref',\n            ref: 'lex:app.bsky.actor.defs#profileViewDetailed',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          updatedAt: {\n            type: 'string',\n            format: 'datetime',\n          },\n          lastUpdatedBy: {\n            type: 'string',\n          },\n          role: {\n            type: 'string',\n            knownValues: [\n              'tools.ozone.team.defs#roleAdmin',\n              'tools.ozone.team.defs#roleModerator',\n              'tools.ozone.team.defs#roleTriage',\n              'tools.ozone.team.defs#roleVerifier',\n            ],\n          },\n        },\n      },\n      roleAdmin: {\n        type: 'token',\n        description:\n          'Admin role. Highest level of access, can perform all actions.',\n      },\n      roleModerator: {\n        type: 'token',\n        description: 'Moderator role. Can perform most actions.',\n      },\n      roleTriage: {\n        type: 'token',\n        description:\n          'Triage role. Mostly intended for monitoring and escalating issues.',\n      },\n      roleVerifier: {\n        type: 'token',\n        description: 'Verifier role. Only allowed to issue verifications.',\n      },\n    },\n  },\n  ToolsOzoneTeamDeleteMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.deleteMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description: 'Delete a member from ozone team. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n          },\n        },\n        errors: [\n          {\n            name: 'MemberNotFound',\n            description: 'The member being deleted does not exist',\n          },\n          {\n            name: 'CannotDeleteSelf',\n            description: 'You can not delete yourself from the team',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneTeamListMembers: {\n    lexicon: 1,\n    id: 'tools.ozone.team.listMembers',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List all members with access to the ozone service.',\n        parameters: {\n          type: 'params',\n          properties: {\n            q: {\n              type: 'string',\n            },\n            disabled: {\n              type: 'boolean',\n            },\n            roles: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            limit: {\n              type: 'integer',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            cursor: {\n              type: 'string',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['members'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              members: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.team.defs#member',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneTeamUpdateMember: {\n    lexicon: 1,\n    id: 'tools.ozone.team.updateMember',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Update a member in the ozone service. Requires admin role.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['did'],\n            properties: {\n              did: {\n                type: 'string',\n                format: 'did',\n              },\n              disabled: {\n                type: 'boolean',\n              },\n              role: {\n                type: 'string',\n                knownValues: [\n                  'tools.ozone.team.defs#roleAdmin',\n                  'tools.ozone.team.defs#roleModerator',\n                  'tools.ozone.team.defs#roleVerifier',\n                  'tools.ozone.team.defs#roleTriage',\n                ],\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'ref',\n            ref: 'lex:tools.ozone.team.defs#member',\n          },\n        },\n        errors: [\n          {\n            name: 'MemberNotFound',\n            description: 'The member being updated does not exist in the team',\n          },\n        ],\n      },\n    },\n  },\n  ToolsOzoneVerificationDefs: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.defs',\n    defs: {\n      verificationView: {\n        type: 'object',\n        description: 'Verification data for the associated subject.',\n        required: [\n          'issuer',\n          'uri',\n          'subject',\n          'handle',\n          'displayName',\n          'createdAt',\n        ],\n        properties: {\n          issuer: {\n            type: 'string',\n            description: 'The user who issued this verification.',\n            format: 'did',\n          },\n          uri: {\n            type: 'string',\n            description: 'The AT-URI of the verification record.',\n            format: 'at-uri',\n          },\n          subject: {\n            type: 'string',\n            format: 'did',\n            description: 'The subject of the verification.',\n          },\n          handle: {\n            type: 'string',\n            description:\n              'Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying.',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            description:\n              'Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying.',\n          },\n          createdAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was created.',\n            format: 'datetime',\n          },\n          revokeReason: {\n            type: 'string',\n            description:\n              'Describes the reason for revocation, also indicating that the verification is no longer valid.',\n          },\n          revokedAt: {\n            type: 'string',\n            description: 'Timestamp when the verification was revoked.',\n            format: 'datetime',\n          },\n          revokedBy: {\n            type: 'string',\n            description: 'The user who revoked this verification.',\n            format: 'did',\n          },\n          subjectProfile: {\n            type: 'union',\n            refs: [],\n          },\n          issuerProfile: {\n            type: 'union',\n            refs: [],\n          },\n          subjectRepo: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoViewDetail',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n            ],\n          },\n          issuerRepo: {\n            type: 'union',\n            refs: [\n              'lex:tools.ozone.moderation.defs#repoViewDetail',\n              'lex:tools.ozone.moderation.defs#repoViewNotFound',\n            ],\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationGrantVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.grantVerifications',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Grant verifications to multiple subjects. Allows batch processing of up to 100 verifications at once.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications'],\n            properties: {\n              verifications: {\n                type: 'array',\n                description: 'Array of verification requests to process',\n                maxLength: 100,\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.grantVerifications#verificationInput',\n                },\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications', 'failedVerifications'],\n            properties: {\n              verifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.defs#verificationView',\n                },\n              },\n              failedVerifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.grantVerifications#grantError',\n                },\n              },\n            },\n          },\n        },\n      },\n      verificationInput: {\n        type: 'object',\n        required: ['subject', 'handle', 'displayName'],\n        properties: {\n          subject: {\n            type: 'string',\n            description: 'The did of the subject being verified',\n            format: 'did',\n          },\n          handle: {\n            type: 'string',\n            description:\n              'Handle of the subject the verification applies to at the moment of verifying.',\n            format: 'handle',\n          },\n          displayName: {\n            type: 'string',\n            description:\n              'Display name of the subject the verification applies to at the moment of verifying.',\n          },\n          createdAt: {\n            type: 'string',\n            format: 'datetime',\n            description:\n              'Timestamp for verification record. Defaults to current time when not specified.',\n          },\n        },\n      },\n      grantError: {\n        type: 'object',\n        description: 'Error object for failed verifications.',\n        required: ['error', 'subject'],\n        properties: {\n          error: {\n            type: 'string',\n            description: 'Error message describing the reason for failure.',\n          },\n          subject: {\n            type: 'string',\n            description: 'The did of the subject being verified',\n            format: 'did',\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationListVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.listVerifications',\n    defs: {\n      main: {\n        type: 'query',\n        description: 'List verifications',\n        parameters: {\n          type: 'params',\n          properties: {\n            cursor: {\n              type: 'string',\n              description: 'Pagination cursor',\n            },\n            limit: {\n              type: 'integer',\n              description: 'Maximum number of results to return',\n              minimum: 1,\n              maximum: 100,\n              default: 50,\n            },\n            createdAfter: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Filter to verifications created after this timestamp',\n            },\n            createdBefore: {\n              type: 'string',\n              format: 'datetime',\n              description:\n                'Filter to verifications created before this timestamp',\n            },\n            issuers: {\n              type: 'array',\n              maxLength: 100,\n              description: 'Filter to verifications from specific issuers',\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            subjects: {\n              type: 'array',\n              description: 'Filter to specific verified DIDs',\n              maxLength: 100,\n              items: {\n                type: 'string',\n                format: 'did',\n              },\n            },\n            sortDirection: {\n              type: 'string',\n              description: 'Sort direction for creation date',\n              enum: ['asc', 'desc'],\n              default: 'desc',\n            },\n            isRevoked: {\n              type: 'boolean',\n              description:\n                'Filter to verifications that are revoked or not. By default, includes both.',\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['verifications'],\n            properties: {\n              cursor: {\n                type: 'string',\n              },\n              verifications: {\n                type: 'array',\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.defs#verificationView',\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n  ToolsOzoneVerificationRevokeVerifications: {\n    lexicon: 1,\n    id: 'tools.ozone.verification.revokeVerifications',\n    defs: {\n      main: {\n        type: 'procedure',\n        description:\n          'Revoke previously granted verifications in batches of up to 100.',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['uris'],\n            properties: {\n              uris: {\n                type: 'array',\n                description: 'Array of verification record uris to revoke',\n                maxLength: 100,\n                items: {\n                  type: 'string',\n                  description:\n                    'The AT-URI of the verification record to revoke.',\n                  format: 'at-uri',\n                },\n              },\n              revokeReason: {\n                type: 'string',\n                description:\n                  'Reason for revoking the verification. This is optional and can be omitted if not needed.',\n                maxLength: 1000,\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['revokedVerifications', 'failedRevocations'],\n            properties: {\n              revokedVerifications: {\n                type: 'array',\n                description: 'List of verification uris successfully revoked',\n                items: {\n                  type: 'string',\n                  format: 'at-uri',\n                },\n              },\n              failedRevocations: {\n                type: 'array',\n                description:\n                  \"List of verification uris that couldn't be revoked, including failure reasons\",\n                items: {\n                  type: 'ref',\n                  ref: 'lex:tools.ozone.verification.revokeVerifications#revokeError',\n                },\n              },\n            },\n          },\n        },\n      },\n      revokeError: {\n        type: 'object',\n        description: 'Error object for failed revocations',\n        required: ['uri', 'error'],\n        properties: {\n          uri: {\n            type: 'string',\n            description:\n              'The AT-URI of the verification record that failed to revoke.',\n            format: 'at-uri',\n          },\n          error: {\n            type: 'string',\n            description:\n              'Description of the error that occurred during revocation.',\n          },\n        },\n      },\n    },\n  },\n} as const satisfies Record<string, LexiconDoc>\nexport const schemas = Object.values(schemaDict) satisfies LexiconDoc[]\nexport const lexicons: Lexicons = new Lexicons(schemas)\n\nexport function validate<T extends { $type: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType: true,\n): ValidationResult<T>\nexport function validate<T extends { $type?: string }>(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: false,\n): ValidationResult<T>\nexport function validate(\n  v: unknown,\n  id: string,\n  hash: string,\n  requiredType?: boolean,\n): ValidationResult {\n  return (requiredType ? is$typed : maybe$typed)(v, id, hash)\n    ? lexicons.validate(`${id}#${hash}`, v)\n    : {\n        success: false,\n        error: new ValidationError(\n          `Must be an object with \"${hash === 'main' ? id : `${id}#${hash}`}\" $type property`,\n        ),\n      }\n}\n\nexport const ids = {\n  AppBskyActorDefs: 'app.bsky.actor.defs',\n  AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences',\n  AppBskyActorGetProfile: 'app.bsky.actor.getProfile',\n  AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles',\n  AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions',\n  AppBskyActorProfile: 'app.bsky.actor.profile',\n  AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',\n  AppBskyActorSearchActors: 'app.bsky.actor.searchActors',\n  AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',\n  AppBskyActorStatus: 'app.bsky.actor.status',\n  AppBskyAgeassuranceBegin: 'app.bsky.ageassurance.begin',\n  AppBskyAgeassuranceDefs: 'app.bsky.ageassurance.defs',\n  AppBskyAgeassuranceGetConfig: 'app.bsky.ageassurance.getConfig',\n  AppBskyAgeassuranceGetState: 'app.bsky.ageassurance.getState',\n  AppBskyBookmarkCreateBookmark: 'app.bsky.bookmark.createBookmark',\n  AppBskyBookmarkDefs: 'app.bsky.bookmark.defs',\n  AppBskyBookmarkDeleteBookmark: 'app.bsky.bookmark.deleteBookmark',\n  AppBskyBookmarkGetBookmarks: 'app.bsky.bookmark.getBookmarks',\n  AppBskyContactDefs: 'app.bsky.contact.defs',\n  AppBskyContactDismissMatch: 'app.bsky.contact.dismissMatch',\n  AppBskyContactGetMatches: 'app.bsky.contact.getMatches',\n  AppBskyContactGetSyncStatus: 'app.bsky.contact.getSyncStatus',\n  AppBskyContactImportContacts: 'app.bsky.contact.importContacts',\n  AppBskyContactRemoveData: 'app.bsky.contact.removeData',\n  AppBskyContactSendNotification: 'app.bsky.contact.sendNotification',\n  AppBskyContactStartPhoneVerification:\n    'app.bsky.contact.startPhoneVerification',\n  AppBskyContactVerifyPhone: 'app.bsky.contact.verifyPhone',\n  AppBskyDraftCreateDraft: 'app.bsky.draft.createDraft',\n  AppBskyDraftDefs: 'app.bsky.draft.defs',\n  AppBskyDraftDeleteDraft: 'app.bsky.draft.deleteDraft',\n  AppBskyDraftGetDrafts: 'app.bsky.draft.getDrafts',\n  AppBskyDraftUpdateDraft: 'app.bsky.draft.updateDraft',\n  AppBskyEmbedDefs: 'app.bsky.embed.defs',\n  AppBskyEmbedExternal: 'app.bsky.embed.external',\n  AppBskyEmbedImages: 'app.bsky.embed.images',\n  AppBskyEmbedRecord: 'app.bsky.embed.record',\n  AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',\n  AppBskyEmbedVideo: 'app.bsky.embed.video',\n  AppBskyFeedDefs: 'app.bsky.feed.defs',\n  AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator',\n  AppBskyFeedGenerator: 'app.bsky.feed.generator',\n  AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds',\n  AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes',\n  AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',\n  AppBskyFeedGetFeed: 'app.bsky.feed.getFeed',\n  AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator',\n  AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators',\n  AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton',\n  AppBskyFeedGetLikes: 'app.bsky.feed.getLikes',\n  AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed',\n  AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',\n  AppBskyFeedGetPosts: 'app.bsky.feed.getPosts',\n  AppBskyFeedGetQuotes: 'app.bsky.feed.getQuotes',\n  AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',\n  AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds',\n  AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline',\n  AppBskyFeedLike: 'app.bsky.feed.like',\n  AppBskyFeedPost: 'app.bsky.feed.post',\n  AppBskyFeedPostgate: 'app.bsky.feed.postgate',\n  AppBskyFeedRepost: 'app.bsky.feed.repost',\n  AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts',\n  AppBskyFeedSendInteractions: 'app.bsky.feed.sendInteractions',\n  AppBskyFeedThreadgate: 'app.bsky.feed.threadgate',\n  AppBskyGraphBlock: 'app.bsky.graph.block',\n  AppBskyGraphDefs: 'app.bsky.graph.defs',\n  AppBskyGraphFollow: 'app.bsky.graph.follow',\n  AppBskyGraphGetActorStarterPacks: 'app.bsky.graph.getActorStarterPacks',\n  AppBskyGraphGetBlocks: 'app.bsky.graph.getBlocks',\n  AppBskyGraphGetFollowers: 'app.bsky.graph.getFollowers',\n  AppBskyGraphGetFollows: 'app.bsky.graph.getFollows',\n  AppBskyGraphGetKnownFollowers: 'app.bsky.graph.getKnownFollowers',\n  AppBskyGraphGetList: 'app.bsky.graph.getList',\n  AppBskyGraphGetListBlocks: 'app.bsky.graph.getListBlocks',\n  AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes',\n  AppBskyGraphGetLists: 'app.bsky.graph.getLists',\n  AppBskyGraphGetListsWithMembership: 'app.bsky.graph.getListsWithMembership',\n  AppBskyGraphGetMutes: 'app.bsky.graph.getMutes',\n  AppBskyGraphGetRelationships: 'app.bsky.graph.getRelationships',\n  AppBskyGraphGetStarterPack: 'app.bsky.graph.getStarterPack',\n  AppBskyGraphGetStarterPacks: 'app.bsky.graph.getStarterPacks',\n  AppBskyGraphGetStarterPacksWithMembership:\n    'app.bsky.graph.getStarterPacksWithMembership',\n  AppBskyGraphGetSuggestedFollowsByActor:\n    'app.bsky.graph.getSuggestedFollowsByActor',\n  AppBskyGraphList: 'app.bsky.graph.list',\n  AppBskyGraphListblock: 'app.bsky.graph.listblock',\n  AppBskyGraphListitem: 'app.bsky.graph.listitem',\n  AppBskyGraphMuteActor: 'app.bsky.graph.muteActor',\n  AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList',\n  AppBskyGraphMuteThread: 'app.bsky.graph.muteThread',\n  AppBskyGraphSearchStarterPacks: 'app.bsky.graph.searchStarterPacks',\n  AppBskyGraphStarterpack: 'app.bsky.graph.starterpack',\n  AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor',\n  AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList',\n  AppBskyGraphUnmuteThread: 'app.bsky.graph.unmuteThread',\n  AppBskyGraphVerification: 'app.bsky.graph.verification',\n  AppBskyLabelerDefs: 'app.bsky.labeler.defs',\n  AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',\n  AppBskyLabelerService: 'app.bsky.labeler.service',\n  AppBskyNotificationDeclaration: 'app.bsky.notification.declaration',\n  AppBskyNotificationDefs: 'app.bsky.notification.defs',\n  AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',\n  AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',\n  AppBskyNotificationListActivitySubscriptions:\n    'app.bsky.notification.listActivitySubscriptions',\n  AppBskyNotificationListNotifications:\n    'app.bsky.notification.listNotifications',\n  AppBskyNotificationPutActivitySubscription:\n    'app.bsky.notification.putActivitySubscription',\n  AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',\n  AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',\n  AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',\n  AppBskyNotificationUnregisterPush: 'app.bsky.notification.unregisterPush',\n  AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',\n  AppBskyRichtextFacet: 'app.bsky.richtext.facet',\n  AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',\n  AppBskyUnspeccedGetAgeAssuranceState:\n    'app.bsky.unspecced.getAgeAssuranceState',\n  AppBskyUnspeccedGetConfig: 'app.bsky.unspecced.getConfig',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacks:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacks',\n  AppBskyUnspeccedGetOnboardingSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetOnboardingSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetPopularFeedGenerators:\n    'app.bsky.unspecced.getPopularFeedGenerators',\n  AppBskyUnspeccedGetPostThreadOtherV2:\n    'app.bsky.unspecced.getPostThreadOtherV2',\n  AppBskyUnspeccedGetPostThreadV2: 'app.bsky.unspecced.getPostThreadV2',\n  AppBskyUnspeccedGetSuggestedFeeds: 'app.bsky.unspecced.getSuggestedFeeds',\n  AppBskyUnspeccedGetSuggestedFeedsSkeleton:\n    'app.bsky.unspecced.getSuggestedFeedsSkeleton',\n  AppBskyUnspeccedGetSuggestedOnboardingUsers:\n    'app.bsky.unspecced.getSuggestedOnboardingUsers',\n  AppBskyUnspeccedGetSuggestedStarterPacks:\n    'app.bsky.unspecced.getSuggestedStarterPacks',\n  AppBskyUnspeccedGetSuggestedStarterPacksSkeleton:\n    'app.bsky.unspecced.getSuggestedStarterPacksSkeleton',\n  AppBskyUnspeccedGetSuggestedUsers: 'app.bsky.unspecced.getSuggestedUsers',\n  AppBskyUnspeccedGetSuggestedUsersSkeleton:\n    'app.bsky.unspecced.getSuggestedUsersSkeleton',\n  AppBskyUnspeccedGetSuggestionsSkeleton:\n    'app.bsky.unspecced.getSuggestionsSkeleton',\n  AppBskyUnspeccedGetTaggedSuggestions:\n    'app.bsky.unspecced.getTaggedSuggestions',\n  AppBskyUnspeccedGetTrendingTopics: 'app.bsky.unspecced.getTrendingTopics',\n  AppBskyUnspeccedGetTrends: 'app.bsky.unspecced.getTrends',\n  AppBskyUnspeccedGetTrendsSkeleton: 'app.bsky.unspecced.getTrendsSkeleton',\n  AppBskyUnspeccedInitAgeAssurance: 'app.bsky.unspecced.initAgeAssurance',\n  AppBskyUnspeccedSearchActorsSkeleton:\n    'app.bsky.unspecced.searchActorsSkeleton',\n  AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton',\n  AppBskyUnspeccedSearchStarterPacksSkeleton:\n    'app.bsky.unspecced.searchStarterPacksSkeleton',\n  AppBskyVideoDefs: 'app.bsky.video.defs',\n  AppBskyVideoGetJobStatus: 'app.bsky.video.getJobStatus',\n  AppBskyVideoGetUploadLimits: 'app.bsky.video.getUploadLimits',\n  AppBskyVideoUploadVideo: 'app.bsky.video.uploadVideo',\n  ChatBskyActorDeclaration: 'chat.bsky.actor.declaration',\n  ChatBskyActorDefs: 'chat.bsky.actor.defs',\n  ChatBskyActorDeleteAccount: 'chat.bsky.actor.deleteAccount',\n  ChatBskyActorExportAccountData: 'chat.bsky.actor.exportAccountData',\n  ChatBskyConvoAcceptConvo: 'chat.bsky.convo.acceptConvo',\n  ChatBskyConvoAddReaction: 'chat.bsky.convo.addReaction',\n  ChatBskyConvoDefs: 'chat.bsky.convo.defs',\n  ChatBskyConvoDeleteMessageForSelf: 'chat.bsky.convo.deleteMessageForSelf',\n  ChatBskyConvoGetConvo: 'chat.bsky.convo.getConvo',\n  ChatBskyConvoGetConvoAvailability: 'chat.bsky.convo.getConvoAvailability',\n  ChatBskyConvoGetConvoForMembers: 'chat.bsky.convo.getConvoForMembers',\n  ChatBskyConvoGetLog: 'chat.bsky.convo.getLog',\n  ChatBskyConvoGetMessages: 'chat.bsky.convo.getMessages',\n  ChatBskyConvoLeaveConvo: 'chat.bsky.convo.leaveConvo',\n  ChatBskyConvoListConvos: 'chat.bsky.convo.listConvos',\n  ChatBskyConvoMuteConvo: 'chat.bsky.convo.muteConvo',\n  ChatBskyConvoRemoveReaction: 'chat.bsky.convo.removeReaction',\n  ChatBskyConvoSendMessage: 'chat.bsky.convo.sendMessage',\n  ChatBskyConvoSendMessageBatch: 'chat.bsky.convo.sendMessageBatch',\n  ChatBskyConvoUnmuteConvo: 'chat.bsky.convo.unmuteConvo',\n  ChatBskyConvoUpdateAllRead: 'chat.bsky.convo.updateAllRead',\n  ChatBskyConvoUpdateRead: 'chat.bsky.convo.updateRead',\n  ChatBskyModerationGetActorMetadata: 'chat.bsky.moderation.getActorMetadata',\n  ChatBskyModerationGetMessageContext: 'chat.bsky.moderation.getMessageContext',\n  ChatBskyModerationUpdateActorAccess: 'chat.bsky.moderation.updateActorAccess',\n  ComAtprotoAdminDefs: 'com.atproto.admin.defs',\n  ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount',\n  ComAtprotoAdminDisableAccountInvites:\n    'com.atproto.admin.disableAccountInvites',\n  ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',\n  ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites',\n  ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo',\n  ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos',\n  ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes',\n  ComAtprotoAdminGetSubjectStatus: 'com.atproto.admin.getSubjectStatus',\n  ComAtprotoAdminSearchAccounts: 'com.atproto.admin.searchAccounts',\n  ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail',\n  ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail',\n  ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle',\n  ComAtprotoAdminUpdateAccountPassword:\n    'com.atproto.admin.updateAccountPassword',\n  ComAtprotoAdminUpdateAccountSigningKey:\n    'com.atproto.admin.updateAccountSigningKey',\n  ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus',\n  ComAtprotoIdentityDefs: 'com.atproto.identity.defs',\n  ComAtprotoIdentityGetRecommendedDidCredentials:\n    'com.atproto.identity.getRecommendedDidCredentials',\n  ComAtprotoIdentityRefreshIdentity: 'com.atproto.identity.refreshIdentity',\n  ComAtprotoIdentityRequestPlcOperationSignature:\n    'com.atproto.identity.requestPlcOperationSignature',\n  ComAtprotoIdentityResolveDid: 'com.atproto.identity.resolveDid',\n  ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle',\n  ComAtprotoIdentityResolveIdentity: 'com.atproto.identity.resolveIdentity',\n  ComAtprotoIdentitySignPlcOperation: 'com.atproto.identity.signPlcOperation',\n  ComAtprotoIdentitySubmitPlcOperation:\n    'com.atproto.identity.submitPlcOperation',\n  ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle',\n  ComAtprotoLabelDefs: 'com.atproto.label.defs',\n  ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels',\n  ComAtprotoLabelSubscribeLabels: 'com.atproto.label.subscribeLabels',\n  ComAtprotoLexiconResolveLexicon: 'com.atproto.lexicon.resolveLexicon',\n  ComAtprotoLexiconSchema: 'com.atproto.lexicon.schema',\n  ComAtprotoModerationCreateReport: 'com.atproto.moderation.createReport',\n  ComAtprotoModerationDefs: 'com.atproto.moderation.defs',\n  ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites',\n  ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord',\n  ComAtprotoRepoDefs: 'com.atproto.repo.defs',\n  ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord',\n  ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo',\n  ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',\n  ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo',\n  ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs',\n  ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',\n  ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',\n  ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',\n  ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',\n  ComAtprotoServerActivateAccount: 'com.atproto.server.activateAccount',\n  ComAtprotoServerCheckAccountStatus: 'com.atproto.server.checkAccountStatus',\n  ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',\n  ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',\n  ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',\n  ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',\n  ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes',\n  ComAtprotoServerCreateSession: 'com.atproto.server.createSession',\n  ComAtprotoServerDeactivateAccount: 'com.atproto.server.deactivateAccount',\n  ComAtprotoServerDefs: 'com.atproto.server.defs',\n  ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount',\n  ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession',\n  ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer',\n  ComAtprotoServerGetAccountInviteCodes:\n    'com.atproto.server.getAccountInviteCodes',\n  ComAtprotoServerGetServiceAuth: 'com.atproto.server.getServiceAuth',\n  ComAtprotoServerGetSession: 'com.atproto.server.getSession',\n  ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords',\n  ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',\n  ComAtprotoServerRequestAccountDelete:\n    'com.atproto.server.requestAccountDelete',\n  ComAtprotoServerRequestEmailConfirmation:\n    'com.atproto.server.requestEmailConfirmation',\n  ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',\n  ComAtprotoServerRequestPasswordReset:\n    'com.atproto.server.requestPasswordReset',\n  ComAtprotoServerReserveSigningKey: 'com.atproto.server.reserveSigningKey',\n  ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',\n  ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',\n  ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',\n  ComAtprotoSyncDefs: 'com.atproto.sync.defs',\n  ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',\n  ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',\n  ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',\n  ComAtprotoSyncGetHead: 'com.atproto.sync.getHead',\n  ComAtprotoSyncGetHostStatus: 'com.atproto.sync.getHostStatus',\n  ComAtprotoSyncGetLatestCommit: 'com.atproto.sync.getLatestCommit',\n  ComAtprotoSyncGetRecord: 'com.atproto.sync.getRecord',\n  ComAtprotoSyncGetRepo: 'com.atproto.sync.getRepo',\n  ComAtprotoSyncGetRepoStatus: 'com.atproto.sync.getRepoStatus',\n  ComAtprotoSyncListBlobs: 'com.atproto.sync.listBlobs',\n  ComAtprotoSyncListHosts: 'com.atproto.sync.listHosts',\n  ComAtprotoSyncListRepos: 'com.atproto.sync.listRepos',\n  ComAtprotoSyncListReposByCollection: 'com.atproto.sync.listReposByCollection',\n  ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate',\n  ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',\n  ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',\n  ComAtprotoTempAddReservedHandle: 'com.atproto.temp.addReservedHandle',\n  ComAtprotoTempCheckHandleAvailability:\n    'com.atproto.temp.checkHandleAvailability',\n  ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue',\n  ComAtprotoTempDereferenceScope: 'com.atproto.temp.dereferenceScope',\n  ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels',\n  ComAtprotoTempRequestPhoneVerification:\n    'com.atproto.temp.requestPhoneVerification',\n  ComAtprotoTempRevokeAccountCredentials:\n    'com.atproto.temp.revokeAccountCredentials',\n  ToolsOzoneCommunicationCreateTemplate:\n    'tools.ozone.communication.createTemplate',\n  ToolsOzoneCommunicationDefs: 'tools.ozone.communication.defs',\n  ToolsOzoneCommunicationDeleteTemplate:\n    'tools.ozone.communication.deleteTemplate',\n  ToolsOzoneCommunicationListTemplates:\n    'tools.ozone.communication.listTemplates',\n  ToolsOzoneCommunicationUpdateTemplate:\n    'tools.ozone.communication.updateTemplate',\n  ToolsOzoneHostingGetAccountHistory: 'tools.ozone.hosting.getAccountHistory',\n  ToolsOzoneModerationCancelScheduledActions:\n    'tools.ozone.moderation.cancelScheduledActions',\n  ToolsOzoneModerationDefs: 'tools.ozone.moderation.defs',\n  ToolsOzoneModerationEmitEvent: 'tools.ozone.moderation.emitEvent',\n  ToolsOzoneModerationGetAccountTimeline:\n    'tools.ozone.moderation.getAccountTimeline',\n  ToolsOzoneModerationGetEvent: 'tools.ozone.moderation.getEvent',\n  ToolsOzoneModerationGetRecord: 'tools.ozone.moderation.getRecord',\n  ToolsOzoneModerationGetRecords: 'tools.ozone.moderation.getRecords',\n  ToolsOzoneModerationGetRepo: 'tools.ozone.moderation.getRepo',\n  ToolsOzoneModerationGetReporterStats:\n    'tools.ozone.moderation.getReporterStats',\n  ToolsOzoneModerationGetRepos: 'tools.ozone.moderation.getRepos',\n  ToolsOzoneModerationGetSubjects: 'tools.ozone.moderation.getSubjects',\n  ToolsOzoneModerationListScheduledActions:\n    'tools.ozone.moderation.listScheduledActions',\n  ToolsOzoneModerationQueryEvents: 'tools.ozone.moderation.queryEvents',\n  ToolsOzoneModerationQueryStatuses: 'tools.ozone.moderation.queryStatuses',\n  ToolsOzoneModerationScheduleAction: 'tools.ozone.moderation.scheduleAction',\n  ToolsOzoneModerationSearchRepos: 'tools.ozone.moderation.searchRepos',\n  ToolsOzoneReportDefs: 'tools.ozone.report.defs',\n  ToolsOzoneSafelinkAddRule: 'tools.ozone.safelink.addRule',\n  ToolsOzoneSafelinkDefs: 'tools.ozone.safelink.defs',\n  ToolsOzoneSafelinkQueryEvents: 'tools.ozone.safelink.queryEvents',\n  ToolsOzoneSafelinkQueryRules: 'tools.ozone.safelink.queryRules',\n  ToolsOzoneSafelinkRemoveRule: 'tools.ozone.safelink.removeRule',\n  ToolsOzoneSafelinkUpdateRule: 'tools.ozone.safelink.updateRule',\n  ToolsOzoneServerGetConfig: 'tools.ozone.server.getConfig',\n  ToolsOzoneSetAddValues: 'tools.ozone.set.addValues',\n  ToolsOzoneSetDefs: 'tools.ozone.set.defs',\n  ToolsOzoneSetDeleteSet: 'tools.ozone.set.deleteSet',\n  ToolsOzoneSetDeleteValues: 'tools.ozone.set.deleteValues',\n  ToolsOzoneSetGetValues: 'tools.ozone.set.getValues',\n  ToolsOzoneSetQuerySets: 'tools.ozone.set.querySets',\n  ToolsOzoneSetUpsertSet: 'tools.ozone.set.upsertSet',\n  ToolsOzoneSettingDefs: 'tools.ozone.setting.defs',\n  ToolsOzoneSettingListOptions: 'tools.ozone.setting.listOptions',\n  ToolsOzoneSettingRemoveOptions: 'tools.ozone.setting.removeOptions',\n  ToolsOzoneSettingUpsertOption: 'tools.ozone.setting.upsertOption',\n  ToolsOzoneSignatureDefs: 'tools.ozone.signature.defs',\n  ToolsOzoneSignatureFindCorrelation: 'tools.ozone.signature.findCorrelation',\n  ToolsOzoneSignatureFindRelatedAccounts:\n    'tools.ozone.signature.findRelatedAccounts',\n  ToolsOzoneSignatureSearchAccounts: 'tools.ozone.signature.searchAccounts',\n  ToolsOzoneTeamAddMember: 'tools.ozone.team.addMember',\n  ToolsOzoneTeamDefs: 'tools.ozone.team.defs',\n  ToolsOzoneTeamDeleteMember: 'tools.ozone.team.deleteMember',\n  ToolsOzoneTeamListMembers: 'tools.ozone.team.listMembers',\n  ToolsOzoneTeamUpdateMember: 'tools.ozone.team.updateMember',\n  ToolsOzoneVerificationDefs: 'tools.ozone.verification.defs',\n  ToolsOzoneVerificationGrantVerifications:\n    'tools.ozone.verification.grantVerifications',\n  ToolsOzoneVerificationListVerifications:\n    'tools.ozone.verification.listVerifications',\n  ToolsOzoneVerificationRevokeVerifications:\n    'tools.ozone.verification.revokeVerifications',\n} as const\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyNotificationDefs from '../notification/defs.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'app.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  createdAt?: string\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n\nexport interface ProfileView {\n  $type?: 'app.bsky.actor.defs#profileView'\n  did: string\n  handle: string\n  displayName?: string\n  pronouns?: string\n  description?: string\n  avatar?: string\n  associated?: ProfileAssociated\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileView = 'profileView'\n\nexport function isProfileView<V>(v: V) {\n  return is$typed(v, id, hashProfileView)\n}\n\nexport function validateProfileView<V>(v: V) {\n  return validate<ProfileView & V>(v, id, hashProfileView)\n}\n\nexport interface ProfileViewDetailed {\n  $type?: 'app.bsky.actor.defs#profileViewDetailed'\n  did: string\n  handle: string\n  displayName?: string\n  description?: string\n  pronouns?: string\n  website?: string\n  avatar?: string\n  banner?: string\n  followersCount?: number\n  followsCount?: number\n  postsCount?: number\n  associated?: ProfileAssociated\n  joinedViaStarterPack?: AppBskyGraphDefs.StarterPackViewBasic\n  indexedAt?: string\n  createdAt?: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  verification?: VerificationState\n  status?: StatusView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashProfileViewDetailed = 'profileViewDetailed'\n\nexport function isProfileViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashProfileViewDetailed)\n}\n\nexport function validateProfileViewDetailed<V>(v: V) {\n  return validate<ProfileViewDetailed & V>(v, id, hashProfileViewDetailed)\n}\n\nexport interface ProfileAssociated {\n  $type?: 'app.bsky.actor.defs#profileAssociated'\n  lists?: number\n  feedgens?: number\n  starterPacks?: number\n  labeler?: boolean\n  chat?: ProfileAssociatedChat\n  activitySubscription?: ProfileAssociatedActivitySubscription\n  germ?: ProfileAssociatedGerm\n}\n\nconst hashProfileAssociated = 'profileAssociated'\n\nexport function isProfileAssociated<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociated)\n}\n\nexport function validateProfileAssociated<V>(v: V) {\n  return validate<ProfileAssociated & V>(v, id, hashProfileAssociated)\n}\n\nexport interface ProfileAssociatedChat {\n  $type?: 'app.bsky.actor.defs#profileAssociatedChat'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n}\n\nconst hashProfileAssociatedChat = 'profileAssociatedChat'\n\nexport function isProfileAssociatedChat<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedChat)\n}\n\nexport function validateProfileAssociatedChat<V>(v: V) {\n  return validate<ProfileAssociatedChat & V>(v, id, hashProfileAssociatedChat)\n}\n\nexport interface ProfileAssociatedGerm {\n  $type?: 'app.bsky.actor.defs#profileAssociatedGerm'\n  messageMeUrl: string\n  showButtonTo: 'usersIFollow' | 'everyone' | (string & {})\n}\n\nconst hashProfileAssociatedGerm = 'profileAssociatedGerm'\n\nexport function isProfileAssociatedGerm<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedGerm)\n}\n\nexport function validateProfileAssociatedGerm<V>(v: V) {\n  return validate<ProfileAssociatedGerm & V>(v, id, hashProfileAssociatedGerm)\n}\n\nexport interface ProfileAssociatedActivitySubscription {\n  $type?: 'app.bsky.actor.defs#profileAssociatedActivitySubscription'\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n}\n\nconst hashProfileAssociatedActivitySubscription =\n  'profileAssociatedActivitySubscription'\n\nexport function isProfileAssociatedActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashProfileAssociatedActivitySubscription)\n}\n\nexport function validateProfileAssociatedActivitySubscription<V>(v: V) {\n  return validate<ProfileAssociatedActivitySubscription & V>(\n    v,\n    id,\n    hashProfileAssociatedActivitySubscription,\n  )\n}\n\n/** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.actor.defs#viewerState'\n  muted?: boolean\n  mutedByList?: AppBskyGraphDefs.ListViewBasic\n  blockedBy?: boolean\n  blocking?: string\n  blockingByList?: AppBskyGraphDefs.ListViewBasic\n  following?: string\n  followedBy?: string\n  knownFollowers?: KnownFollowers\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** The subject's followers whom you also follow */\nexport interface KnownFollowers {\n  $type?: 'app.bsky.actor.defs#knownFollowers'\n  count: number\n  followers: ProfileViewBasic[]\n}\n\nconst hashKnownFollowers = 'knownFollowers'\n\nexport function isKnownFollowers<V>(v: V) {\n  return is$typed(v, id, hashKnownFollowers)\n}\n\nexport function validateKnownFollowers<V>(v: V) {\n  return validate<KnownFollowers & V>(v, id, hashKnownFollowers)\n}\n\n/** Represents the verification information about the user this object is attached to. */\nexport interface VerificationState {\n  $type?: 'app.bsky.actor.defs#verificationState'\n  /** All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included. */\n  verifications: VerificationView[]\n  /** The user's status as a verified account. */\n  verifiedStatus: 'valid' | 'invalid' | 'none' | (string & {})\n  /** The user's status as a trusted verifier. */\n  trustedVerifierStatus: 'valid' | 'invalid' | 'none' | (string & {})\n}\n\nconst hashVerificationState = 'verificationState'\n\nexport function isVerificationState<V>(v: V) {\n  return is$typed(v, id, hashVerificationState)\n}\n\nexport function validateVerificationState<V>(v: V) {\n  return validate<VerificationState & V>(v, id, hashVerificationState)\n}\n\n/** An individual verification for an associated subject. */\nexport interface VerificationView {\n  $type?: 'app.bsky.actor.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** True if the verification passes validation, otherwise false. */\n  isValid: boolean\n  /** Timestamp when the verification was created. */\n  createdAt: string\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n\nexport type Preferences = (\n  | $Typed<AdultContentPref>\n  | $Typed<ContentLabelPref>\n  | $Typed<SavedFeedsPref>\n  | $Typed<SavedFeedsPrefV2>\n  | $Typed<PersonalDetailsPref>\n  | $Typed<DeclaredAgePref>\n  | $Typed<FeedViewPref>\n  | $Typed<ThreadViewPref>\n  | $Typed<InterestsPref>\n  | $Typed<MutedWordsPref>\n  | $Typed<HiddenPostsPref>\n  | $Typed<BskyAppStatePref>\n  | $Typed<LabelersPref>\n  | $Typed<PostInteractionSettingsPref>\n  | $Typed<VerificationPrefs>\n  | $Typed<LiveEventPreferences>\n  | { $type: string }\n)[]\n\nexport interface AdultContentPref {\n  $type?: 'app.bsky.actor.defs#adultContentPref'\n  enabled: boolean\n}\n\nconst hashAdultContentPref = 'adultContentPref'\n\nexport function isAdultContentPref<V>(v: V) {\n  return is$typed(v, id, hashAdultContentPref)\n}\n\nexport function validateAdultContentPref<V>(v: V) {\n  return validate<AdultContentPref & V>(v, id, hashAdultContentPref)\n}\n\nexport interface ContentLabelPref {\n  $type?: 'app.bsky.actor.defs#contentLabelPref'\n  /** Which labeler does this preference apply to? If undefined, applies globally. */\n  labelerDid?: string\n  label: string\n  visibility: 'ignore' | 'show' | 'warn' | 'hide' | (string & {})\n}\n\nconst hashContentLabelPref = 'contentLabelPref'\n\nexport function isContentLabelPref<V>(v: V) {\n  return is$typed(v, id, hashContentLabelPref)\n}\n\nexport function validateContentLabelPref<V>(v: V) {\n  return validate<ContentLabelPref & V>(v, id, hashContentLabelPref)\n}\n\nexport interface SavedFeed {\n  $type?: 'app.bsky.actor.defs#savedFeed'\n  id: string\n  type: 'feed' | 'list' | 'timeline' | (string & {})\n  value: string\n  pinned: boolean\n}\n\nconst hashSavedFeed = 'savedFeed'\n\nexport function isSavedFeed<V>(v: V) {\n  return is$typed(v, id, hashSavedFeed)\n}\n\nexport function validateSavedFeed<V>(v: V) {\n  return validate<SavedFeed & V>(v, id, hashSavedFeed)\n}\n\nexport interface SavedFeedsPrefV2 {\n  $type?: 'app.bsky.actor.defs#savedFeedsPrefV2'\n  items: SavedFeed[]\n}\n\nconst hashSavedFeedsPrefV2 = 'savedFeedsPrefV2'\n\nexport function isSavedFeedsPrefV2<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPrefV2)\n}\n\nexport function validateSavedFeedsPrefV2<V>(v: V) {\n  return validate<SavedFeedsPrefV2 & V>(v, id, hashSavedFeedsPrefV2)\n}\n\nexport interface SavedFeedsPref {\n  $type?: 'app.bsky.actor.defs#savedFeedsPref'\n  pinned: string[]\n  saved: string[]\n  timelineIndex?: number\n}\n\nconst hashSavedFeedsPref = 'savedFeedsPref'\n\nexport function isSavedFeedsPref<V>(v: V) {\n  return is$typed(v, id, hashSavedFeedsPref)\n}\n\nexport function validateSavedFeedsPref<V>(v: V) {\n  return validate<SavedFeedsPref & V>(v, id, hashSavedFeedsPref)\n}\n\nexport interface PersonalDetailsPref {\n  $type?: 'app.bsky.actor.defs#personalDetailsPref'\n  /** The birth date of account owner. */\n  birthDate?: string\n}\n\nconst hashPersonalDetailsPref = 'personalDetailsPref'\n\nexport function isPersonalDetailsPref<V>(v: V) {\n  return is$typed(v, id, hashPersonalDetailsPref)\n}\n\nexport function validatePersonalDetailsPref<V>(v: V) {\n  return validate<PersonalDetailsPref & V>(v, id, hashPersonalDetailsPref)\n}\n\n/** 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. */\nexport interface DeclaredAgePref {\n  $type?: 'app.bsky.actor.defs#declaredAgePref'\n  /** Indicates if the user has declared that they are over 13 years of age. */\n  isOverAge13?: boolean\n  /** Indicates if the user has declared that they are over 16 years of age. */\n  isOverAge16?: boolean\n  /** Indicates if the user has declared that they are over 18 years of age. */\n  isOverAge18?: boolean\n}\n\nconst hashDeclaredAgePref = 'declaredAgePref'\n\nexport function isDeclaredAgePref<V>(v: V) {\n  return is$typed(v, id, hashDeclaredAgePref)\n}\n\nexport function validateDeclaredAgePref<V>(v: V) {\n  return validate<DeclaredAgePref & V>(v, id, hashDeclaredAgePref)\n}\n\nexport interface FeedViewPref {\n  $type?: 'app.bsky.actor.defs#feedViewPref'\n  /** The URI of the feed, or an identifier which describes the feed. */\n  feed: string\n  /** Hide replies in the feed. */\n  hideReplies?: boolean\n  /** Hide replies in the feed if they are not by followed users. */\n  hideRepliesByUnfollowed: boolean\n  /** Hide replies in the feed if they do not have this number of likes. */\n  hideRepliesByLikeCount?: number\n  /** Hide reposts in the feed. */\n  hideReposts?: boolean\n  /** Hide quote posts in the feed. */\n  hideQuotePosts?: boolean\n}\n\nconst hashFeedViewPref = 'feedViewPref'\n\nexport function isFeedViewPref<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPref)\n}\n\nexport function validateFeedViewPref<V>(v: V) {\n  return validate<FeedViewPref & V>(v, id, hashFeedViewPref)\n}\n\nexport interface ThreadViewPref {\n  $type?: 'app.bsky.actor.defs#threadViewPref'\n  /** Sorting mode for threads. */\n  sort?:\n    | 'oldest'\n    | 'newest'\n    | 'most-likes'\n    | 'random'\n    | 'hotness'\n    | (string & {})\n}\n\nconst hashThreadViewPref = 'threadViewPref'\n\nexport function isThreadViewPref<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPref)\n}\n\nexport function validateThreadViewPref<V>(v: V) {\n  return validate<ThreadViewPref & V>(v, id, hashThreadViewPref)\n}\n\nexport interface InterestsPref {\n  $type?: 'app.bsky.actor.defs#interestsPref'\n  /** A list of tags which describe the account owner's interests gathered during onboarding. */\n  tags: string[]\n}\n\nconst hashInterestsPref = 'interestsPref'\n\nexport function isInterestsPref<V>(v: V) {\n  return is$typed(v, id, hashInterestsPref)\n}\n\nexport function validateInterestsPref<V>(v: V) {\n  return validate<InterestsPref & V>(v, id, hashInterestsPref)\n}\n\nexport type MutedWordTarget = 'content' | 'tag' | (string & {})\n\n/** A word that the account owner has muted. */\nexport interface MutedWord {\n  $type?: 'app.bsky.actor.defs#mutedWord'\n  id?: string\n  /** The muted word itself. */\n  value: string\n  /** The intended targets of the muted word. */\n  targets: MutedWordTarget[]\n  /** Groups of users to apply the muted word to. If undefined, applies to all users. */\n  actorTarget: 'all' | 'exclude-following' | (string & {})\n  /** The date and time at which the muted word will expire and no longer be applied. */\n  expiresAt?: string\n}\n\nconst hashMutedWord = 'mutedWord'\n\nexport function isMutedWord<V>(v: V) {\n  return is$typed(v, id, hashMutedWord)\n}\n\nexport function validateMutedWord<V>(v: V) {\n  return validate<MutedWord & V>(v, id, hashMutedWord)\n}\n\nexport interface MutedWordsPref {\n  $type?: 'app.bsky.actor.defs#mutedWordsPref'\n  /** A list of words the account owner has muted. */\n  items: MutedWord[]\n}\n\nconst hashMutedWordsPref = 'mutedWordsPref'\n\nexport function isMutedWordsPref<V>(v: V) {\n  return is$typed(v, id, hashMutedWordsPref)\n}\n\nexport function validateMutedWordsPref<V>(v: V) {\n  return validate<MutedWordsPref & V>(v, id, hashMutedWordsPref)\n}\n\nexport interface HiddenPostsPref {\n  $type?: 'app.bsky.actor.defs#hiddenPostsPref'\n  /** A list of URIs of posts the account owner has hidden. */\n  items: string[]\n}\n\nconst hashHiddenPostsPref = 'hiddenPostsPref'\n\nexport function isHiddenPostsPref<V>(v: V) {\n  return is$typed(v, id, hashHiddenPostsPref)\n}\n\nexport function validateHiddenPostsPref<V>(v: V) {\n  return validate<HiddenPostsPref & V>(v, id, hashHiddenPostsPref)\n}\n\nexport interface LabelersPref {\n  $type?: 'app.bsky.actor.defs#labelersPref'\n  labelers: LabelerPrefItem[]\n}\n\nconst hashLabelersPref = 'labelersPref'\n\nexport function isLabelersPref<V>(v: V) {\n  return is$typed(v, id, hashLabelersPref)\n}\n\nexport function validateLabelersPref<V>(v: V) {\n  return validate<LabelersPref & V>(v, id, hashLabelersPref)\n}\n\nexport interface LabelerPrefItem {\n  $type?: 'app.bsky.actor.defs#labelerPrefItem'\n  did: string\n}\n\nconst hashLabelerPrefItem = 'labelerPrefItem'\n\nexport function isLabelerPrefItem<V>(v: V) {\n  return is$typed(v, id, hashLabelerPrefItem)\n}\n\nexport function validateLabelerPrefItem<V>(v: V) {\n  return validate<LabelerPrefItem & V>(v, id, hashLabelerPrefItem)\n}\n\n/** A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this. */\nexport interface BskyAppStatePref {\n  $type?: 'app.bsky.actor.defs#bskyAppStatePref'\n  activeProgressGuide?: BskyAppProgressGuide\n  /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */\n  queuedNudges?: string[]\n  /** Storage for NUXs the user has encountered. */\n  nuxs?: Nux[]\n}\n\nconst hashBskyAppStatePref = 'bskyAppStatePref'\n\nexport function isBskyAppStatePref<V>(v: V) {\n  return is$typed(v, id, hashBskyAppStatePref)\n}\n\nexport function validateBskyAppStatePref<V>(v: V) {\n  return validate<BskyAppStatePref & V>(v, id, hashBskyAppStatePref)\n}\n\n/** If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress. */\nexport interface BskyAppProgressGuide {\n  $type?: 'app.bsky.actor.defs#bskyAppProgressGuide'\n  guide: string\n}\n\nconst hashBskyAppProgressGuide = 'bskyAppProgressGuide'\n\nexport function isBskyAppProgressGuide<V>(v: V) {\n  return is$typed(v, id, hashBskyAppProgressGuide)\n}\n\nexport function validateBskyAppProgressGuide<V>(v: V) {\n  return validate<BskyAppProgressGuide & V>(v, id, hashBskyAppProgressGuide)\n}\n\n/** A new user experiences (NUX) storage object */\nexport interface Nux {\n  $type?: 'app.bsky.actor.defs#nux'\n  id: string\n  completed: boolean\n  /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */\n  data?: string\n  /** The date and time at which the NUX will expire and should be considered completed. */\n  expiresAt?: string\n}\n\nconst hashNux = 'nux'\n\nexport function isNux<V>(v: V) {\n  return is$typed(v, id, hashNux)\n}\n\nexport function validateNux<V>(v: V) {\n  return validate<Nux & V>(v, id, hashNux)\n}\n\n/** Preferences for how verified accounts appear in the app. */\nexport interface VerificationPrefs {\n  $type?: 'app.bsky.actor.defs#verificationPrefs'\n  /** Hide the blue check badges for verified accounts and trusted verifiers. */\n  hideBadges: boolean\n}\n\nconst hashVerificationPrefs = 'verificationPrefs'\n\nexport function isVerificationPrefs<V>(v: V) {\n  return is$typed(v, id, hashVerificationPrefs)\n}\n\nexport function validateVerificationPrefs<V>(v: V) {\n  return validate<VerificationPrefs & V>(v, id, hashVerificationPrefs)\n}\n\n/** Preferences for live events. */\nexport interface LiveEventPreferences {\n  $type?: 'app.bsky.actor.defs#liveEventPreferences'\n  /** A list of feed IDs that the user has hidden from live events. */\n  hiddenFeedIds?: string[]\n  /** Whether to hide all feeds from live events. */\n  hideAllFeeds: boolean\n}\n\nconst hashLiveEventPreferences = 'liveEventPreferences'\n\nexport function isLiveEventPreferences<V>(v: V) {\n  return is$typed(v, id, hashLiveEventPreferences)\n}\n\nexport function validateLiveEventPreferences<V>(v: V) {\n  return validate<LiveEventPreferences & V>(v, id, hashLiveEventPreferences)\n}\n\n/** 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. */\nexport interface PostInteractionSettingsPref {\n  $type?: 'app.bsky.actor.defs#postInteractionSettingsPref'\n  /** Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  threadgateAllowRules?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n  /** Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashPostInteractionSettingsPref = 'postInteractionSettingsPref'\n\nexport function isPostInteractionSettingsPref<V>(v: V) {\n  return is$typed(v, id, hashPostInteractionSettingsPref)\n}\n\nexport function validatePostInteractionSettingsPref<V>(v: V) {\n  return validate<PostInteractionSettingsPref & V>(\n    v,\n    id,\n    hashPostInteractionSettingsPref,\n  )\n}\n\nexport interface StatusView {\n  $type?: 'app.bsky.actor.defs#statusView'\n  uri?: string\n  cid?: string\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  record: { [_ in string]: unknown }\n  embed?: $Typed<AppBskyEmbedExternal.View> | { $type: string }\n  /** The date when this status will expire. The application might choose to no longer return the status after expiration. */\n  expiresAt?: string\n  /** True if the status is not expired, false if it is expired. Only present if expiration was set. */\n  isActive?: boolean\n  /** True if the user's go-live access has been disabled by a moderator, false otherwise. */\n  isDisabled?: boolean\n}\n\nconst hashStatusView = 'statusView'\n\nexport function isStatusView<V>(v: V) {\n  return is$typed(v, id, hashStatusView)\n}\n\nexport function validateStatusView<V>(v: V) {\n  return validate<StatusView & V>(v, id, hashStatusView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/getProfile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfile'\n\nexport type QueryParams = {\n  /** Handle or DID of account to fetch profile of. */\n  actor: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyActorDefs.ProfileViewDetailed\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/getProfiles.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getProfiles'\n\nexport type QueryParams = {\n  actors: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  profiles: AppBskyActorDefs.ProfileViewDetailed[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/getSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.getSuggestions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/profile.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.profile'\n\nexport interface Main {\n  $type: 'app.bsky.actor.profile'\n  displayName?: string\n  /** Free-form profile description text. */\n  description?: string\n  /** Free-form pronouns text. */\n  pronouns?: string\n  website?: string\n  /** Small image to be displayed next to posts from account. AKA, 'profile picture' */\n  avatar?: BlobRef\n  /** Larger horizontal image to display behind profile view. */\n  banner?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main\n  pinnedPost?: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  preferences: AppBskyActorDefs.Preferences\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActors'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.searchActorsTypeahead'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead. */\n  term?: string\n  /** Search query prefix; not a full query string. */\n  q?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/actor/status.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.actor.status'\n\nexport interface Main {\n  $type: 'app.bsky.actor.status'\n  /** The status for the account. */\n  status: 'app.bsky.actor.status#live' | (string & {})\n  embed?: $Typed<AppBskyEmbedExternal.Main> | { $type: string }\n  /** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. */\n  durationMinutes?: number\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Advertises an account as currently offering live content. */\nexport const LIVE = `${id}#live`\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/ageassurance/begin.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.begin'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive Age Assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the Age Assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n  /** An optional ISO 3166-2 code of the user's region or state within the country. */\n  regionCode?: string\n}\n\nexport type OutputSchema = AppBskyAgeassuranceDefs.State\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidEmail'\n    | 'DidTooLong'\n    | 'InvalidInitiation'\n    | 'RegionNotSupported'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/ageassurance/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.defs'\n\n/** The access level granted based on Age Assurance data we've processed. */\nexport type Access = 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n/** The status of the Age Assurance process. */\nexport type Status =\n  | 'unknown'\n  | 'pending'\n  | 'assured'\n  | 'blocked'\n  | (string & {})\n\n/** The user's computed Age Assurance state. */\nexport interface State {\n  $type?: 'app.bsky.ageassurance.defs#state'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  status: Status\n  access: Access\n}\n\nconst hashState = 'state'\n\nexport function isState<V>(v: V) {\n  return is$typed(v, id, hashState)\n}\n\nexport function validateState<V>(v: V) {\n  return validate<State & V>(v, id, hashState)\n}\n\n/** Additional metadata needed to compute Age Assurance state client-side. */\nexport interface StateMetadata {\n  $type?: 'app.bsky.ageassurance.defs#stateMetadata'\n  /** The account creation timestamp. */\n  accountCreatedAt?: string\n}\n\nconst hashStateMetadata = 'stateMetadata'\n\nexport function isStateMetadata<V>(v: V) {\n  return is$typed(v, id, hashStateMetadata)\n}\n\nexport function validateStateMetadata<V>(v: V) {\n  return validate<StateMetadata & V>(v, id, hashStateMetadata)\n}\n\nexport interface Config {\n  $type?: 'app.bsky.ageassurance.defs#config'\n  /** The per-region Age Assurance configuration. */\n  regions: ConfigRegion[]\n}\n\nconst hashConfig = 'config'\n\nexport function isConfig<V>(v: V) {\n  return is$typed(v, id, hashConfig)\n}\n\nexport function validateConfig<V>(v: V) {\n  return validate<Config & V>(v, id, hashConfig)\n}\n\n/** The Age Assurance configuration for a specific region. */\nexport interface ConfigRegion {\n  $type?: 'app.bsky.ageassurance.defs#configRegion'\n  /** The ISO 3166-1 alpha-2 country code this configuration applies to. */\n  countryCode: string\n  /** The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country. */\n  regionCode?: string\n  /** The minimum age (as a whole integer) required to use Bluesky in this region. */\n  minAccessAge: number\n  /** 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. */\n  rules: (\n    | $Typed<ConfigRegionRuleDefault>\n    | $Typed<ConfigRegionRuleIfDeclaredOverAge>\n    | $Typed<ConfigRegionRuleIfDeclaredUnderAge>\n    | $Typed<ConfigRegionRuleIfAssuredOverAge>\n    | $Typed<ConfigRegionRuleIfAssuredUnderAge>\n    | $Typed<ConfigRegionRuleIfAccountNewerThan>\n    | $Typed<ConfigRegionRuleIfAccountOlderThan>\n    | { $type: string }\n  )[]\n}\n\nconst hashConfigRegion = 'configRegion'\n\nexport function isConfigRegion<V>(v: V) {\n  return is$typed(v, id, hashConfigRegion)\n}\n\nexport function validateConfigRegion<V>(v: V) {\n  return validate<ConfigRegion & V>(v, id, hashConfigRegion)\n}\n\n/** Age Assurance rule that applies by default. */\nexport interface ConfigRegionRuleDefault {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleDefault'\n  access: Access\n}\n\nconst hashConfigRegionRuleDefault = 'configRegionRuleDefault'\n\nexport function isConfigRegionRuleDefault<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleDefault)\n}\n\nexport function validateConfigRegionRuleDefault<V>(v: V) {\n  return validate<ConfigRegionRuleDefault & V>(\n    v,\n    id,\n    hashConfigRegionRuleDefault,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfDeclaredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredOverAge =\n  'configRegionRuleIfDeclaredOverAge'\n\nexport function isConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredOverAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has declared themselves under a certain age. */\nexport interface ConfigRegionRuleIfDeclaredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfDeclaredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfDeclaredUnderAge =\n  'configRegionRuleIfDeclaredUnderAge'\n\nexport function isConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfDeclaredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfDeclaredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfDeclaredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfDeclaredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age. */\nexport interface ConfigRegionRuleIfAssuredOverAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredOverAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredOverAge = 'configRegionRuleIfAssuredOverAge'\n\nexport function isConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredOverAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredOverAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredOverAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredOverAge,\n  )\n}\n\n/** Age Assurance rule that applies if the user has been assured to be under a certain age. */\nexport interface ConfigRegionRuleIfAssuredUnderAge {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAssuredUnderAge'\n  /** The age threshold as a whole integer. */\n  age: number\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAssuredUnderAge =\n  'configRegionRuleIfAssuredUnderAge'\n\nexport function isConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAssuredUnderAge)\n}\n\nexport function validateConfigRegionRuleIfAssuredUnderAge<V>(v: V) {\n  return validate<ConfigRegionRuleIfAssuredUnderAge & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAssuredUnderAge,\n  )\n}\n\n/** Age Assurance rule that applies if the account is equal-to or newer than a certain date. */\nexport interface ConfigRegionRuleIfAccountNewerThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountNewerThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountNewerThan =\n  'configRegionRuleIfAccountNewerThan'\n\nexport function isConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountNewerThan)\n}\n\nexport function validateConfigRegionRuleIfAccountNewerThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountNewerThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountNewerThan,\n  )\n}\n\n/** Age Assurance rule that applies if the account is older than a certain date. */\nexport interface ConfigRegionRuleIfAccountOlderThan {\n  $type?: 'app.bsky.ageassurance.defs#configRegionRuleIfAccountOlderThan'\n  /** The date threshold as a datetime string. */\n  date: string\n  access: Access\n}\n\nconst hashConfigRegionRuleIfAccountOlderThan =\n  'configRegionRuleIfAccountOlderThan'\n\nexport function isConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return is$typed(v, id, hashConfigRegionRuleIfAccountOlderThan)\n}\n\nexport function validateConfigRegionRuleIfAccountOlderThan<V>(v: V) {\n  return validate<ConfigRegionRuleIfAccountOlderThan & V>(\n    v,\n    id,\n    hashConfigRegionRuleIfAccountOlderThan,\n  )\n}\n\n/** Object used to store Age Assurance data in stash. */\nexport interface Event {\n  $type?: 'app.bsky.ageassurance.defs#event'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the Age Assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n  /** The access level granted based on Age Assurance data we've processed. */\n  access: 'unknown' | 'none' | 'safe' | 'full' | (string & {})\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The email used for Age Assurance. */\n  email?: string\n  /** The IP address used when initiating the Age Assurance flow. */\n  initIp?: string\n  /** The user agent used when initiating the Age Assurance flow. */\n  initUa?: string\n  /** The IP address used when completing the Age Assurance flow. */\n  completeIp?: string\n  /** The user agent used when completing the Age Assurance flow. */\n  completeUa?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/ageassurance/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyAgeassuranceDefs.Config\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/ageassurance/getState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyAgeassuranceDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.ageassurance.getState'\n\nexport type QueryParams = {\n  countryCode: string\n  regionCode?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  state: AppBskyAgeassuranceDefs.State\n  metadata: AppBskyAgeassuranceDefs.StateMetadata\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/bookmark/createBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.createBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n  cid: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnsupportedCollection'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/bookmark/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.defs'\n\n/** Object used to store bookmark data in stash. */\nexport interface Bookmark {\n  $type?: 'app.bsky.bookmark.defs#bookmark'\n  subject: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashBookmark = 'bookmark'\n\nexport function isBookmark<V>(v: V) {\n  return is$typed(v, id, hashBookmark)\n}\n\nexport function validateBookmark<V>(v: V) {\n  return validate<Bookmark & V>(v, id, hashBookmark)\n}\n\nexport interface BookmarkView {\n  $type?: 'app.bsky.bookmark.defs#bookmarkView'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt?: string\n  item:\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.PostView>\n    | { $type: string }\n}\n\nconst hashBookmarkView = 'bookmarkView'\n\nexport function isBookmarkView<V>(v: V) {\n  return is$typed(v, id, hashBookmarkView)\n}\n\nexport function validateBookmarkView<V>(v: V) {\n  return validate<BookmarkView & V>(v, id, hashBookmarkView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/bookmark/deleteBookmark.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.deleteBookmark'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  uri: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnsupportedCollection'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/bookmark/getBookmarks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyBookmarkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.bookmark.getBookmarks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  bookmarks: AppBskyBookmarkDefs.BookmarkView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.defs'\n\n/** 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. */\nexport interface MatchAndContactIndex {\n  $type?: 'app.bsky.contact.defs#matchAndContactIndex'\n  match: AppBskyActorDefs.ProfileView\n  /** The index of this match in the import contact input. */\n  contactIndex: number\n}\n\nconst hashMatchAndContactIndex = 'matchAndContactIndex'\n\nexport function isMatchAndContactIndex<V>(v: V) {\n  return is$typed(v, id, hashMatchAndContactIndex)\n}\n\nexport function validateMatchAndContactIndex<V>(v: V) {\n  return validate<MatchAndContactIndex & V>(v, id, hashMatchAndContactIndex)\n}\n\nexport interface SyncStatus {\n  $type?: 'app.bsky.contact.defs#syncStatus'\n  /** Last date when contacts where imported. */\n  syncedAt: string\n  /** 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. */\n  matchesCount: number\n}\n\nconst hashSyncStatus = 'syncStatus'\n\nexport function isSyncStatus<V>(v: V) {\n  return is$typed(v, id, hashSyncStatus)\n}\n\nexport function validateSyncStatus<V>(v: V) {\n  return validate<SyncStatus & V>(v, id, hashSyncStatus)\n}\n\n/** A stash object to be sent via bsync representing a notification to be created. */\nexport interface Notification {\n  $type?: 'app.bsky.contact.defs#notification'\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/dismissMatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.dismissMatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The subject's DID to dismiss the match with. */\n  subject: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/getMatches.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getMatches'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  matches: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InvalidLimit' | 'InvalidCursor' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/getSyncStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.getSyncStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  syncStatus?: AppBskyContactDefs.SyncStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/importContacts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyContactDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.importContacts'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`. */\n  token: string\n  /** 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`. */\n  contacts: string[]\n}\n\nexport interface OutputSchema {\n  /** The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list. */\n  matchesAndContactIndexes: AppBskyContactDefs.MatchAndContactIndex[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidDid'\n    | 'InvalidContacts'\n    | 'TooManyContacts'\n    | 'InvalidToken'\n    | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/removeData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.removeData'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidDid' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/sendNotification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.sendNotification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID of who this notification comes from. */\n  from: string\n  /** The DID of who this notification should go to. */\n  to: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/startPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.startPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to receive the code via SMS. */\n  phone: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RateLimitExceeded' | 'InvalidDid' | 'InvalidPhone' | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/contact/verifyPhone.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.contact.verifyPhone'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`. */\n  phone: string\n  /** The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`. */\n  code: string\n}\n\nexport interface OutputSchema {\n  /** JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call. */\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'RateLimitExceeded'\n    | 'InvalidDid'\n    | 'InvalidPhone'\n    | 'InvalidCode'\n    | 'InternalError'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/draft/createDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.createDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.Draft\n}\n\nexport interface OutputSchema {\n  /** The ID of the created draft. */\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DraftLimitReached'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/draft/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedPostgate from '../feed/postgate.js'\nimport type * as AppBskyFeedThreadgate from '../feed/threadgate.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.defs'\n\n/** A draft with an identifier, used to store drafts in private storage (stash). */\nexport interface DraftWithId {\n  $type?: 'app.bsky.draft.defs#draftWithId'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n}\n\nconst hashDraftWithId = 'draftWithId'\n\nexport function isDraftWithId<V>(v: V) {\n  return is$typed(v, id, hashDraftWithId)\n}\n\nexport function validateDraftWithId<V>(v: V) {\n  return validate<DraftWithId & V>(v, id, hashDraftWithId)\n}\n\n/** A draft containing an array of draft posts. */\nexport interface Draft {\n  $type?: 'app.bsky.draft.defs#draft'\n  /** UUIDv4 identifier of the device that created this draft. */\n  deviceId?: string\n  /** The device and/or platform on which the draft was created. */\n  deviceName?: string\n  /** Array of draft posts that compose this draft. */\n  posts: DraftPost[]\n  /** Indicates human language of posts primary text content. */\n  langs?: string[]\n  /** Embedding rules for the postgates to be created when this draft is published. */\n  postgateEmbeddingRules?: (\n    | $Typed<AppBskyFeedPostgate.DisableRule>\n    | { $type: string }\n  )[]\n  /** Allow-rules for the threadgate to be created when this draft is published. */\n  threadgateAllow?: (\n    | $Typed<AppBskyFeedThreadgate.MentionRule>\n    | $Typed<AppBskyFeedThreadgate.FollowerRule>\n    | $Typed<AppBskyFeedThreadgate.FollowingRule>\n    | $Typed<AppBskyFeedThreadgate.ListRule>\n    | { $type: string }\n  )[]\n}\n\nconst hashDraft = 'draft'\n\nexport function isDraft<V>(v: V) {\n  return is$typed(v, id, hashDraft)\n}\n\nexport function validateDraft<V>(v: V) {\n  return validate<Draft & V>(v, id, hashDraft)\n}\n\n/** One of the posts that compose a draft. */\nexport interface DraftPost {\n  $type?: 'app.bsky.draft.defs#draftPost'\n  /** 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. */\n  text: string\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  embedImages?: DraftEmbedImage[]\n  embedVideos?: DraftEmbedVideo[]\n  embedExternals?: DraftEmbedExternal[]\n  embedRecords?: DraftEmbedRecord[]\n}\n\nconst hashDraftPost = 'draftPost'\n\nexport function isDraftPost<V>(v: V) {\n  return is$typed(v, id, hashDraftPost)\n}\n\nexport function validateDraftPost<V>(v: V) {\n  return validate<DraftPost & V>(v, id, hashDraftPost)\n}\n\n/** View to present drafts data to users. */\nexport interface DraftView {\n  $type?: 'app.bsky.draft.defs#draftView'\n  /** A TID to be used as a draft identifier. */\n  id: string\n  draft: Draft\n  /** The time the draft was created. */\n  createdAt: string\n  /** The time the draft was last updated. */\n  updatedAt: string\n}\n\nconst hashDraftView = 'draftView'\n\nexport function isDraftView<V>(v: V) {\n  return is$typed(v, id, hashDraftView)\n}\n\nexport function validateDraftView<V>(v: V) {\n  return validate<DraftView & V>(v, id, hashDraftView)\n}\n\nexport interface DraftEmbedLocalRef {\n  $type?: 'app.bsky.draft.defs#draftEmbedLocalRef'\n  /** Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts. */\n  path: string\n}\n\nconst hashDraftEmbedLocalRef = 'draftEmbedLocalRef'\n\nexport function isDraftEmbedLocalRef<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedLocalRef)\n}\n\nexport function validateDraftEmbedLocalRef<V>(v: V) {\n  return validate<DraftEmbedLocalRef & V>(v, id, hashDraftEmbedLocalRef)\n}\n\nexport interface DraftEmbedCaption {\n  $type?: 'app.bsky.draft.defs#draftEmbedCaption'\n  lang: string\n  content: string\n}\n\nconst hashDraftEmbedCaption = 'draftEmbedCaption'\n\nexport function isDraftEmbedCaption<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedCaption)\n}\n\nexport function validateDraftEmbedCaption<V>(v: V) {\n  return validate<DraftEmbedCaption & V>(v, id, hashDraftEmbedCaption)\n}\n\nexport interface DraftEmbedImage {\n  $type?: 'app.bsky.draft.defs#draftEmbedImage'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n}\n\nconst hashDraftEmbedImage = 'draftEmbedImage'\n\nexport function isDraftEmbedImage<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedImage)\n}\n\nexport function validateDraftEmbedImage<V>(v: V) {\n  return validate<DraftEmbedImage & V>(v, id, hashDraftEmbedImage)\n}\n\nexport interface DraftEmbedVideo {\n  $type?: 'app.bsky.draft.defs#draftEmbedVideo'\n  localRef: DraftEmbedLocalRef\n  alt?: string\n  captions?: DraftEmbedCaption[]\n}\n\nconst hashDraftEmbedVideo = 'draftEmbedVideo'\n\nexport function isDraftEmbedVideo<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedVideo)\n}\n\nexport function validateDraftEmbedVideo<V>(v: V) {\n  return validate<DraftEmbedVideo & V>(v, id, hashDraftEmbedVideo)\n}\n\nexport interface DraftEmbedExternal {\n  $type?: 'app.bsky.draft.defs#draftEmbedExternal'\n  uri: string\n}\n\nconst hashDraftEmbedExternal = 'draftEmbedExternal'\n\nexport function isDraftEmbedExternal<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedExternal)\n}\n\nexport function validateDraftEmbedExternal<V>(v: V) {\n  return validate<DraftEmbedExternal & V>(v, id, hashDraftEmbedExternal)\n}\n\nexport interface DraftEmbedRecord {\n  $type?: 'app.bsky.draft.defs#draftEmbedRecord'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashDraftEmbedRecord = 'draftEmbedRecord'\n\nexport function isDraftEmbedRecord<V>(v: V) {\n  return is$typed(v, id, hashDraftEmbedRecord)\n}\n\nexport function validateDraftEmbedRecord<V>(v: V) {\n  return validate<DraftEmbedRecord & V>(v, id, hashDraftEmbedRecord)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/draft/deleteDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.deleteDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/draft/getDrafts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.getDrafts'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  drafts: AppBskyDraftDefs.DraftView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/draft/updateDraft.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyDraftDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.draft.updateDraft'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  draft: AppBskyDraftDefs.DraftWithId\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/embed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.defs'\n\n/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */\nexport interface AspectRatio {\n  $type?: 'app.bsky.embed.defs#aspectRatio'\n  width: number\n  height: number\n}\n\nconst hashAspectRatio = 'aspectRatio'\n\nexport function isAspectRatio<V>(v: V) {\n  return is$typed(v, id, hashAspectRatio)\n}\n\nexport function validateAspectRatio<V>(v: V) {\n  return validate<AspectRatio & V>(v, id, hashAspectRatio)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/embed/external.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.external'\n\n/** A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post). */\nexport interface Main {\n  $type?: 'app.bsky.embed.external'\n  external: External\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface External {\n  $type?: 'app.bsky.embed.external#external'\n  uri: string\n  title: string\n  description: string\n  thumb?: BlobRef\n}\n\nconst hashExternal = 'external'\n\nexport function isExternal<V>(v: V) {\n  return is$typed(v, id, hashExternal)\n}\n\nexport function validateExternal<V>(v: V) {\n  return validate<External & V>(v, id, hashExternal)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.external#view'\n  external: ViewExternal\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewExternal {\n  $type?: 'app.bsky.embed.external#viewExternal'\n  uri: string\n  title: string\n  description: string\n  thumb?: string\n}\n\nconst hashViewExternal = 'viewExternal'\n\nexport function isViewExternal<V>(v: V) {\n  return is$typed(v, id, hashViewExternal)\n}\n\nexport function validateViewExternal<V>(v: V) {\n  return validate<ViewExternal & V>(v, id, hashViewExternal)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/embed/images.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.images'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.images'\n  images: Image[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Image {\n  $type?: 'app.bsky.embed.images#image'\n  image: BlobRef\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashImage = 'image'\n\nexport function isImage<V>(v: V) {\n  return is$typed(v, id, hashImage)\n}\n\nexport function validateImage<V>(v: V) {\n  return validate<Image & V>(v, id, hashImage)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.images#view'\n  images: ViewImage[]\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewImage {\n  $type?: 'app.bsky.embed.images#viewImage'\n  /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */\n  thumb: string\n  /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */\n  fullsize: string\n  /** Alt text description of the image, for accessibility. */\n  alt: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n}\n\nconst hashViewImage = 'viewImage'\n\nexport function isViewImage<V>(v: V) {\n  return is$typed(v, id, hashViewImage)\n}\n\nexport function validateViewImage<V>(v: V) {\n  return validate<ViewImage & V>(v, id, hashViewImage)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/embed/record.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\nimport type * as AppBskyLabelerDefs from '../labeler/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\nimport type * as AppBskyEmbedRecordWithMedia from './recordWithMedia.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.record'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.record'\n  record: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.record#view'\n  record:\n    | $Typed<ViewRecord>\n    | $Typed<ViewNotFound>\n    | $Typed<ViewBlocked>\n    | $Typed<ViewDetached>\n    | $Typed<AppBskyFeedDefs.GeneratorView>\n    | $Typed<AppBskyGraphDefs.ListView>\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyGraphDefs.StarterPackViewBasic>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n\nexport interface ViewRecord {\n  $type?: 'app.bsky.embed.record#viewRecord'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  /** The record data itself. */\n  value: { [_ in string]: unknown }\n  labels?: ComAtprotoLabelDefs.Label[]\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  embeds?: (\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  )[]\n  indexedAt: string\n}\n\nconst hashViewRecord = 'viewRecord'\n\nexport function isViewRecord<V>(v: V) {\n  return is$typed(v, id, hashViewRecord)\n}\n\nexport function validateViewRecord<V>(v: V) {\n  return validate<ViewRecord & V>(v, id, hashViewRecord)\n}\n\nexport interface ViewNotFound {\n  $type?: 'app.bsky.embed.record#viewNotFound'\n  uri: string\n  notFound: true\n}\n\nconst hashViewNotFound = 'viewNotFound'\n\nexport function isViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashViewNotFound)\n}\n\nexport function validateViewNotFound<V>(v: V) {\n  return validate<ViewNotFound & V>(v, id, hashViewNotFound)\n}\n\nexport interface ViewBlocked {\n  $type?: 'app.bsky.embed.record#viewBlocked'\n  uri: string\n  blocked: true\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashViewBlocked = 'viewBlocked'\n\nexport function isViewBlocked<V>(v: V) {\n  return is$typed(v, id, hashViewBlocked)\n}\n\nexport function validateViewBlocked<V>(v: V) {\n  return validate<ViewBlocked & V>(v, id, hashViewBlocked)\n}\n\nexport interface ViewDetached {\n  $type?: 'app.bsky.embed.record#viewDetached'\n  uri: string\n  detached: true\n}\n\nconst hashViewDetached = 'viewDetached'\n\nexport function isViewDetached<V>(v: V) {\n  return is$typed(v, id, hashViewDetached)\n}\n\nexport function validateViewDetached<V>(v: V) {\n  return validate<ViewDetached & V>(v, id, hashViewDetached)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/embed/recordWithMedia.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedRecord from './record.js'\nimport type * as AppBskyEmbedImages from './images.js'\nimport type * as AppBskyEmbedVideo from './video.js'\nimport type * as AppBskyEmbedExternal from './external.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.recordWithMedia'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.recordWithMedia'\n  record: AppBskyEmbedRecord.Main\n  media:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | { $type: string }\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.recordWithMedia#view'\n  record: AppBskyEmbedRecord.View\n  media:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | { $type: string }\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/embed/video.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyEmbedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.embed.video'\n\nexport interface Main {\n  $type?: 'app.bsky.embed.video'\n  /** The mp4 video file. May be up to 100mb, formerly limited to 50mb. */\n  video: BlobRef\n  captions?: Caption[]\n  /** Alt text description of the video, for accessibility. */\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\nexport interface Caption {\n  $type?: 'app.bsky.embed.video#caption'\n  lang: string\n  file: BlobRef\n}\n\nconst hashCaption = 'caption'\n\nexport function isCaption<V>(v: V) {\n  return is$typed(v, id, hashCaption)\n}\n\nexport function validateCaption<V>(v: V) {\n  return validate<Caption & V>(v, id, hashCaption)\n}\n\nexport interface View {\n  $type?: 'app.bsky.embed.video#view'\n  cid: string\n  playlist: string\n  thumbnail?: string\n  alt?: string\n  aspectRatio?: AppBskyEmbedDefs.AspectRatio\n  /** A hint to the client about how to present the video. */\n  presentation?: 'default' | 'gif' | (string & {})\n}\n\nconst hashView = 'view'\n\nexport function isView<V>(v: V) {\n  return is$typed(v, id, hashView)\n}\n\nexport function validateView<V>(v: V) {\n  return validate<View & V>(v, id, hashView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.defs'\n\nexport interface PostView {\n  $type?: 'app.bsky.feed.defs#postView'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileViewBasic\n  record: { [_ in string]: unknown }\n  embed?:\n    | $Typed<AppBskyEmbedImages.View>\n    | $Typed<AppBskyEmbedVideo.View>\n    | $Typed<AppBskyEmbedExternal.View>\n    | $Typed<AppBskyEmbedRecord.View>\n    | $Typed<AppBskyEmbedRecordWithMedia.View>\n    | { $type: string }\n  bookmarkCount?: number\n  replyCount?: number\n  repostCount?: number\n  likeCount?: number\n  quoteCount?: number\n  indexedAt: string\n  viewer?: ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  threadgate?: ThreadgateView\n  /** Debug information for internal development */\n  debug?: { [_ in string]: unknown }\n}\n\nconst hashPostView = 'postView'\n\nexport function isPostView<V>(v: V) {\n  return is$typed(v, id, hashPostView)\n}\n\nexport function validatePostView<V>(v: V) {\n  return validate<PostView & V>(v, id, hashPostView)\n}\n\n/** Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. */\nexport interface ViewerState {\n  $type?: 'app.bsky.feed.defs#viewerState'\n  repost?: string\n  like?: string\n  bookmarked?: boolean\n  threadMuted?: boolean\n  replyDisabled?: boolean\n  embeddingDisabled?: boolean\n  pinned?: boolean\n}\n\nconst hashViewerState = 'viewerState'\n\nexport function isViewerState<V>(v: V) {\n  return is$typed(v, id, hashViewerState)\n}\n\nexport function validateViewerState<V>(v: V) {\n  return validate<ViewerState & V>(v, id, hashViewerState)\n}\n\n/** Metadata about this post within the context of the thread it is in. */\nexport interface ThreadContext {\n  $type?: 'app.bsky.feed.defs#threadContext'\n  rootAuthorLike?: string\n}\n\nconst hashThreadContext = 'threadContext'\n\nexport function isThreadContext<V>(v: V) {\n  return is$typed(v, id, hashThreadContext)\n}\n\nexport function validateThreadContext<V>(v: V) {\n  return validate<ThreadContext & V>(v, id, hashThreadContext)\n}\n\nexport interface FeedViewPost {\n  $type?: 'app.bsky.feed.defs#feedViewPost'\n  post: PostView\n  reply?: ReplyRef\n  reason?: $Typed<ReasonRepost> | $Typed<ReasonPin> | { $type: string }\n  /** Context provided by feed generator that may be passed back alongside interactions. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashFeedViewPost = 'feedViewPost'\n\nexport function isFeedViewPost<V>(v: V) {\n  return is$typed(v, id, hashFeedViewPost)\n}\n\nexport function validateFeedViewPost<V>(v: V) {\n  return validate<FeedViewPost & V>(v, id, hashFeedViewPost)\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.defs#replyRef'\n  root:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  parent:\n    | $Typed<PostView>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  grandparentAuthor?: AppBskyActorDefs.ProfileViewBasic\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\nexport interface ReasonRepost {\n  $type?: 'app.bsky.feed.defs#reasonRepost'\n  by: AppBskyActorDefs.ProfileViewBasic\n  uri?: string\n  cid?: string\n  indexedAt: string\n}\n\nconst hashReasonRepost = 'reasonRepost'\n\nexport function isReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashReasonRepost)\n}\n\nexport function validateReasonRepost<V>(v: V) {\n  return validate<ReasonRepost & V>(v, id, hashReasonRepost)\n}\n\nexport interface ReasonPin {\n  $type?: 'app.bsky.feed.defs#reasonPin'\n}\n\nconst hashReasonPin = 'reasonPin'\n\nexport function isReasonPin<V>(v: V) {\n  return is$typed(v, id, hashReasonPin)\n}\n\nexport function validateReasonPin<V>(v: V) {\n  return validate<ReasonPin & V>(v, id, hashReasonPin)\n}\n\nexport interface ThreadViewPost {\n  $type?: 'app.bsky.feed.defs#threadViewPost'\n  post: PostView\n  parent?:\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  replies?: (\n    | $Typed<ThreadViewPost>\n    | $Typed<NotFoundPost>\n    | $Typed<BlockedPost>\n    | { $type: string }\n  )[]\n  threadContext?: ThreadContext\n}\n\nconst hashThreadViewPost = 'threadViewPost'\n\nexport function isThreadViewPost<V>(v: V) {\n  return is$typed(v, id, hashThreadViewPost)\n}\n\nexport function validateThreadViewPost<V>(v: V) {\n  return validate<ThreadViewPost & V>(v, id, hashThreadViewPost)\n}\n\nexport interface NotFoundPost {\n  $type?: 'app.bsky.feed.defs#notFoundPost'\n  uri: string\n  notFound: true\n}\n\nconst hashNotFoundPost = 'notFoundPost'\n\nexport function isNotFoundPost<V>(v: V) {\n  return is$typed(v, id, hashNotFoundPost)\n}\n\nexport function validateNotFoundPost<V>(v: V) {\n  return validate<NotFoundPost & V>(v, id, hashNotFoundPost)\n}\n\nexport interface BlockedPost {\n  $type?: 'app.bsky.feed.defs#blockedPost'\n  uri: string\n  blocked: true\n  author: BlockedAuthor\n}\n\nconst hashBlockedPost = 'blockedPost'\n\nexport function isBlockedPost<V>(v: V) {\n  return is$typed(v, id, hashBlockedPost)\n}\n\nexport function validateBlockedPost<V>(v: V) {\n  return validate<BlockedPost & V>(v, id, hashBlockedPost)\n}\n\nexport interface BlockedAuthor {\n  $type?: 'app.bsky.feed.defs#blockedAuthor'\n  did: string\n  viewer?: AppBskyActorDefs.ViewerState\n}\n\nconst hashBlockedAuthor = 'blockedAuthor'\n\nexport function isBlockedAuthor<V>(v: V) {\n  return is$typed(v, id, hashBlockedAuthor)\n}\n\nexport function validateBlockedAuthor<V>(v: V) {\n  return validate<BlockedAuthor & V>(v, id, hashBlockedAuthor)\n}\n\nexport interface GeneratorView {\n  $type?: 'app.bsky.feed.defs#generatorView'\n  uri: string\n  cid: string\n  did: string\n  creator: AppBskyActorDefs.ProfileView\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  likeCount?: number\n  acceptsInteractions?: boolean\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: GeneratorViewerState\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  indexedAt: string\n}\n\nconst hashGeneratorView = 'generatorView'\n\nexport function isGeneratorView<V>(v: V) {\n  return is$typed(v, id, hashGeneratorView)\n}\n\nexport function validateGeneratorView<V>(v: V) {\n  return validate<GeneratorView & V>(v, id, hashGeneratorView)\n}\n\nexport interface GeneratorViewerState {\n  $type?: 'app.bsky.feed.defs#generatorViewerState'\n  like?: string\n}\n\nconst hashGeneratorViewerState = 'generatorViewerState'\n\nexport function isGeneratorViewerState<V>(v: V) {\n  return is$typed(v, id, hashGeneratorViewerState)\n}\n\nexport function validateGeneratorViewerState<V>(v: V) {\n  return validate<GeneratorViewerState & V>(v, id, hashGeneratorViewerState)\n}\n\nexport interface SkeletonFeedPost {\n  $type?: 'app.bsky.feed.defs#skeletonFeedPost'\n  post: string\n  reason?:\n    | $Typed<SkeletonReasonRepost>\n    | $Typed<SkeletonReasonPin>\n    | { $type: string }\n  /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */\n  feedContext?: string\n}\n\nconst hashSkeletonFeedPost = 'skeletonFeedPost'\n\nexport function isSkeletonFeedPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonFeedPost)\n}\n\nexport function validateSkeletonFeedPost<V>(v: V) {\n  return validate<SkeletonFeedPost & V>(v, id, hashSkeletonFeedPost)\n}\n\nexport interface SkeletonReasonRepost {\n  $type?: 'app.bsky.feed.defs#skeletonReasonRepost'\n  repost: string\n}\n\nconst hashSkeletonReasonRepost = 'skeletonReasonRepost'\n\nexport function isSkeletonReasonRepost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonRepost)\n}\n\nexport function validateSkeletonReasonRepost<V>(v: V) {\n  return validate<SkeletonReasonRepost & V>(v, id, hashSkeletonReasonRepost)\n}\n\nexport interface SkeletonReasonPin {\n  $type?: 'app.bsky.feed.defs#skeletonReasonPin'\n}\n\nconst hashSkeletonReasonPin = 'skeletonReasonPin'\n\nexport function isSkeletonReasonPin<V>(v: V) {\n  return is$typed(v, id, hashSkeletonReasonPin)\n}\n\nexport function validateSkeletonReasonPin<V>(v: V) {\n  return validate<SkeletonReasonPin & V>(v, id, hashSkeletonReasonPin)\n}\n\nexport interface ThreadgateView {\n  $type?: 'app.bsky.feed.defs#threadgateView'\n  uri?: string\n  cid?: string\n  record?: { [_ in string]: unknown }\n  lists?: AppBskyGraphDefs.ListViewBasic[]\n}\n\nconst hashThreadgateView = 'threadgateView'\n\nexport function isThreadgateView<V>(v: V) {\n  return is$typed(v, id, hashThreadgateView)\n}\n\nexport function validateThreadgateView<V>(v: V) {\n  return validate<ThreadgateView & V>(v, id, hashThreadgateView)\n}\n\nexport interface Interaction {\n  $type?: 'app.bsky.feed.defs#interaction'\n  item?: string\n  event?:\n    | 'app.bsky.feed.defs#requestLess'\n    | 'app.bsky.feed.defs#requestMore'\n    | 'app.bsky.feed.defs#clickthroughItem'\n    | 'app.bsky.feed.defs#clickthroughAuthor'\n    | 'app.bsky.feed.defs#clickthroughReposter'\n    | 'app.bsky.feed.defs#clickthroughEmbed'\n    | 'app.bsky.feed.defs#interactionSeen'\n    | 'app.bsky.feed.defs#interactionLike'\n    | 'app.bsky.feed.defs#interactionRepost'\n    | 'app.bsky.feed.defs#interactionReply'\n    | 'app.bsky.feed.defs#interactionQuote'\n    | 'app.bsky.feed.defs#interactionShare'\n    | (string & {})\n  /** Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton. */\n  feedContext?: string\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nconst hashInteraction = 'interaction'\n\nexport function isInteraction<V>(v: V) {\n  return is$typed(v, id, hashInteraction)\n}\n\nexport function validateInteraction<V>(v: V) {\n  return validate<Interaction & V>(v, id, hashInteraction)\n}\n\n/** Request that less content like the given feed item be shown in the feed */\nexport const REQUESTLESS = `${id}#requestLess`\n/** Request that more content like the given feed item be shown in the feed */\nexport const REQUESTMORE = `${id}#requestMore`\n/** User clicked through to the feed item */\nexport const CLICKTHROUGHITEM = `${id}#clickthroughItem`\n/** User clicked through to the author of the feed item */\nexport const CLICKTHROUGHAUTHOR = `${id}#clickthroughAuthor`\n/** User clicked through to the reposter of the feed item */\nexport const CLICKTHROUGHREPOSTER = `${id}#clickthroughReposter`\n/** User clicked through to the embedded content of the feed item */\nexport const CLICKTHROUGHEMBED = `${id}#clickthroughEmbed`\n/** Declares the feed generator returns any types of posts. */\nexport const CONTENTMODEUNSPECIFIED = `${id}#contentModeUnspecified`\n/** Declares the feed generator returns posts containing app.bsky.embed.video embeds. */\nexport const CONTENTMODEVIDEO = `${id}#contentModeVideo`\n/** Feed item was seen by user */\nexport const INTERACTIONSEEN = `${id}#interactionSeen`\n/** User liked the feed item */\nexport const INTERACTIONLIKE = `${id}#interactionLike`\n/** User reposted the feed item */\nexport const INTERACTIONREPOST = `${id}#interactionRepost`\n/** User replied to the feed item */\nexport const INTERACTIONREPLY = `${id}#interactionReply`\n/** User quoted the feed item */\nexport const INTERACTIONQUOTE = `${id}#interactionQuote`\n/** User shared the feed item */\nexport const INTERACTIONSHARE = `${id}#interactionShare`\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.describeFeedGenerator'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  feeds: Feed[]\n  links?: Links\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Feed {\n  $type?: 'app.bsky.feed.describeFeedGenerator#feed'\n  uri: string\n}\n\nconst hashFeed = 'feed'\n\nexport function isFeed<V>(v: V) {\n  return is$typed(v, id, hashFeed)\n}\n\nexport function validateFeed<V>(v: V) {\n  return validate<Feed & V>(v, id, hashFeed)\n}\n\nexport interface Links {\n  $type?: 'app.bsky.feed.describeFeedGenerator#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/generator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.generator'\n\nexport interface Main {\n  $type: 'app.bsky.feed.generator'\n  did: string\n  displayName: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  /** Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions */\n  acceptsInteractions?: boolean\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  contentMode?:\n    | 'app.bsky.feed.defs#contentModeUnspecified'\n    | 'app.bsky.feed.defs#contentModeVideo'\n    | (string & {})\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getActorFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorFeeds'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getActorLikes'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BlockedActor' | 'BlockedByActor'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getAuthorFeed'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n  /** Combinations of post/repost types to include in response. */\n  filter:\n    | 'posts_with_replies'\n    | 'posts_no_replies'\n    | 'posts_with_media'\n    | 'posts_and_author_threads'\n    | 'posts_with_video'\n    | (string & {})\n  includePins: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BlockedActor' | 'BlockedByActor'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeed'\n\nexport type QueryParams = {\n  feed: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownFeed'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerator'\n\nexport type QueryParams = {\n  /** AT-URI of the feed generator record. */\n  feed: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  view: AppBskyFeedDefs.GeneratorView\n  /** Indicates whether the feed generator service has been online recently, or else seems to be inactive. */\n  isOnline: boolean\n  /** Indicates whether the feed generator service is compatible with the record declaration. */\n  isValid: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedGenerators'\n\nexport type QueryParams = {\n  feeds: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getFeedSkeleton'\n\nexport type QueryParams = {\n  /** Reference to feed generator record describing the specific feed being requested. */\n  feed: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.SkeletonFeedPost[]\n  /** Unique identifier per request that may be passed back alongside interactions. */\n  reqId?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownFeed'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getLikes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getLikes'\n\nexport type QueryParams = {\n  /** AT-URI of the subject (eg, a post record). */\n  uri: string\n  /** CID of the subject record (aka, specific version of record), to filter likes. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  likes: Like[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Like {\n  $type?: 'app.bsky.feed.getLikes#like'\n  indexedAt: string\n  createdAt: string\n  actor: AppBskyActorDefs.ProfileView\n}\n\nconst hashLike = 'like'\n\nexport function isLike<V>(v: V) {\n  return is$typed(v, id, hashLike)\n}\n\nexport function validateLike<V>(v: V) {\n  return validate<Like & V>(v, id, hashLike)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getListFeed'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'UnknownList'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getPostThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPostThread'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. */\n  uri: string\n  /** How many levels of reply depth should be included in response. */\n  depth: number\n  /** How many levels of parent (and grandparent, etc) post to include. */\n  parentHeight: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  thread:\n    | $Typed<AppBskyFeedDefs.ThreadViewPost>\n    | $Typed<AppBskyFeedDefs.NotFoundPost>\n    | $Typed<AppBskyFeedDefs.BlockedPost>\n    | { $type: string }\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'NotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getPosts'\n\nexport type QueryParams = {\n  /** List of post AT-URIs to return hydrated views for. */\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getQuotes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getQuotes'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to quotes of specific version (by CID) of the post record. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getRepostedBy.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getRepostedBy'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of post record */\n  uri: string\n  /** If supplied, filters to reposts of specific version (by CID) of the post record. */\n  cid?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  cursor?: string\n  repostedBy: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/getTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.getTimeline'\n\nexport type QueryParams = {\n  /** Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism. */\n  algorithm?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feed: AppBskyFeedDefs.FeedViewPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/like.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.like'\n\nexport interface Main {\n  $type: 'app.bsky.feed.like'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/post.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyEmbedImages from '../embed/images.js'\nimport type * as AppBskyEmbedVideo from '../embed/video.js'\nimport type * as AppBskyEmbedExternal from '../embed/external.js'\nimport type * as AppBskyEmbedRecord from '../embed/record.js'\nimport type * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.post'\n\nexport interface Main {\n  $type: 'app.bsky.feed.post'\n  /** The primary post content. May be an empty string, if there are embeds. */\n  text: string\n  /** DEPRECATED: replaced by app.bsky.richtext.facet. */\n  entities?: Entity[]\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  reply?: ReplyRef\n  embed?:\n    | $Typed<AppBskyEmbedImages.Main>\n    | $Typed<AppBskyEmbedVideo.Main>\n    | $Typed<AppBskyEmbedExternal.Main>\n    | $Typed<AppBskyEmbedRecord.Main>\n    | $Typed<AppBskyEmbedRecordWithMedia.Main>\n    | { $type: string }\n  /** Indicates human language of post primary text content. */\n  langs?: string[]\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  /** Additional hashtags, in addition to any included in post text and facets. */\n  tags?: string[]\n  /** Client-declared timestamp when this post was originally created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface ReplyRef {\n  $type?: 'app.bsky.feed.post#replyRef'\n  root: ComAtprotoRepoStrongRef.Main\n  parent: ComAtprotoRepoStrongRef.Main\n}\n\nconst hashReplyRef = 'replyRef'\n\nexport function isReplyRef<V>(v: V) {\n  return is$typed(v, id, hashReplyRef)\n}\n\nexport function validateReplyRef<V>(v: V) {\n  return validate<ReplyRef & V>(v, id, hashReplyRef)\n}\n\n/** Deprecated: use facets instead. */\nexport interface Entity {\n  $type?: 'app.bsky.feed.post#entity'\n  index: TextSlice\n  /** Expected values are 'mention' and 'link'. */\n  type: string\n  value: string\n}\n\nconst hashEntity = 'entity'\n\nexport function isEntity<V>(v: V) {\n  return is$typed(v, id, hashEntity)\n}\n\nexport function validateEntity<V>(v: V) {\n  return validate<Entity & V>(v, id, hashEntity)\n}\n\n/** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. */\nexport interface TextSlice {\n  $type?: 'app.bsky.feed.post#textSlice'\n  start: number\n  end: number\n}\n\nconst hashTextSlice = 'textSlice'\n\nexport function isTextSlice<V>(v: V) {\n  return is$typed(v, id, hashTextSlice)\n}\n\nexport function validateTextSlice<V>(v: V) {\n  return validate<TextSlice & V>(v, id, hashTextSlice)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/postgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.postgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.postgate'\n  createdAt: string\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of AT-URIs embedding this post that the author has detached from. */\n  detachedEmbeddingUris?: string[]\n  /** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */\n  embeddingRules?: ($Typed<DisableRule> | { $type: string })[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Disables embedding of this post. */\nexport interface DisableRule {\n  $type?: 'app.bsky.feed.postgate#disableRule'\n}\n\nconst hashDisableRule = 'disableRule'\n\nexport function isDisableRule<V>(v: V) {\n  return is$typed(v, id, hashDisableRule)\n}\n\nexport function validateDisableRule<V>(v: V) {\n  return validate<DisableRule & V>(v, id, hashDisableRule)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/repost.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.repost'\n\nexport interface Main {\n  $type: 'app.bsky.feed.repost'\n  subject: ComAtprotoRepoStrongRef.Main\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/searchPosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.searchPosts'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyFeedDefs.PostView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/sendInteractions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.sendInteractions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  feed?: string\n  interactions: AppBskyFeedDefs.Interaction[]\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.feed.threadgate'\n\nexport interface Main {\n  $type: 'app.bsky.feed.threadgate'\n  /** Reference (AT-URI) to the post record. */\n  post: string\n  /** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */\n  allow?: (\n    | $Typed<MentionRule>\n    | $Typed<FollowerRule>\n    | $Typed<FollowingRule>\n    | $Typed<ListRule>\n    | { $type: string }\n  )[]\n  createdAt: string\n  /** List of hidden reply URIs. */\n  hiddenReplies?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\n/** Allow replies from actors mentioned in your post. */\nexport interface MentionRule {\n  $type?: 'app.bsky.feed.threadgate#mentionRule'\n}\n\nconst hashMentionRule = 'mentionRule'\n\nexport function isMentionRule<V>(v: V) {\n  return is$typed(v, id, hashMentionRule)\n}\n\nexport function validateMentionRule<V>(v: V) {\n  return validate<MentionRule & V>(v, id, hashMentionRule)\n}\n\n/** Allow replies from actors who follow you. */\nexport interface FollowerRule {\n  $type?: 'app.bsky.feed.threadgate#followerRule'\n}\n\nconst hashFollowerRule = 'followerRule'\n\nexport function isFollowerRule<V>(v: V) {\n  return is$typed(v, id, hashFollowerRule)\n}\n\nexport function validateFollowerRule<V>(v: V) {\n  return validate<FollowerRule & V>(v, id, hashFollowerRule)\n}\n\n/** Allow replies from actors you follow. */\nexport interface FollowingRule {\n  $type?: 'app.bsky.feed.threadgate#followingRule'\n}\n\nconst hashFollowingRule = 'followingRule'\n\nexport function isFollowingRule<V>(v: V) {\n  return is$typed(v, id, hashFollowingRule)\n}\n\nexport function validateFollowingRule<V>(v: V) {\n  return validate<FollowingRule & V>(v, id, hashFollowingRule)\n}\n\n/** Allow replies from actors on a list. */\nexport interface ListRule {\n  $type?: 'app.bsky.feed.threadgate#listRule'\n  list: string\n}\n\nconst hashListRule = 'listRule'\n\nexport function isListRule<V>(v: V) {\n  return is$typed(v, id, hashListRule)\n}\n\nexport function validateListRule<V>(v: V) {\n  return validate<ListRule & V>(v, id, hashListRule)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/block.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.block'\n\nexport interface Main {\n  $type: 'app.bsky.graph.block'\n  /** DID of the account to be blocked. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.defs'\n\nexport interface ListViewBasic {\n  $type?: 'app.bsky.graph.defs#listViewBasic'\n  uri: string\n  cid: string\n  name: string\n  purpose: ListPurpose\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt?: string\n}\n\nconst hashListViewBasic = 'listViewBasic'\n\nexport function isListViewBasic<V>(v: V) {\n  return is$typed(v, id, hashListViewBasic)\n}\n\nexport function validateListViewBasic<V>(v: V) {\n  return validate<ListViewBasic & V>(v, id, hashListViewBasic)\n}\n\nexport interface ListView {\n  $type?: 'app.bsky.graph.defs#listView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  name: string\n  purpose: ListPurpose\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: string\n  listItemCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  viewer?: ListViewerState\n  indexedAt: string\n}\n\nconst hashListView = 'listView'\n\nexport function isListView<V>(v: V) {\n  return is$typed(v, id, hashListView)\n}\n\nexport function validateListView<V>(v: V) {\n  return validate<ListView & V>(v, id, hashListView)\n}\n\nexport interface ListItemView {\n  $type?: 'app.bsky.graph.defs#listItemView'\n  uri: string\n  subject: AppBskyActorDefs.ProfileView\n}\n\nconst hashListItemView = 'listItemView'\n\nexport function isListItemView<V>(v: V) {\n  return is$typed(v, id, hashListItemView)\n}\n\nexport function validateListItemView<V>(v: V) {\n  return validate<ListItemView & V>(v, id, hashListItemView)\n}\n\nexport interface StarterPackView {\n  $type?: 'app.bsky.graph.defs#starterPackView'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  list?: ListViewBasic\n  listItemsSample?: ListItemView[]\n  feeds?: AppBskyFeedDefs.GeneratorView[]\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackView = 'starterPackView'\n\nexport function isStarterPackView<V>(v: V) {\n  return is$typed(v, id, hashStarterPackView)\n}\n\nexport function validateStarterPackView<V>(v: V) {\n  return validate<StarterPackView & V>(v, id, hashStarterPackView)\n}\n\nexport interface StarterPackViewBasic {\n  $type?: 'app.bsky.graph.defs#starterPackViewBasic'\n  uri: string\n  cid: string\n  record: { [_ in string]: unknown }\n  creator: AppBskyActorDefs.ProfileViewBasic\n  listItemCount?: number\n  joinedWeekCount?: number\n  joinedAllTimeCount?: number\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n}\n\nconst hashStarterPackViewBasic = 'starterPackViewBasic'\n\nexport function isStarterPackViewBasic<V>(v: V) {\n  return is$typed(v, id, hashStarterPackViewBasic)\n}\n\nexport function validateStarterPackViewBasic<V>(v: V) {\n  return validate<StarterPackViewBasic & V>(v, id, hashStarterPackViewBasic)\n}\n\nexport type ListPurpose =\n  | 'app.bsky.graph.defs#modlist'\n  | 'app.bsky.graph.defs#curatelist'\n  | 'app.bsky.graph.defs#referencelist'\n  | (string & {})\n\n/** A list of actors to apply an aggregate moderation action (mute/block) on. */\nexport const MODLIST = `${id}#modlist`\n/** A list of actors used for curation purposes such as list feeds or interaction gating. */\nexport const CURATELIST = `${id}#curatelist`\n/** A list of actors used for only for reference purposes such as within a starter pack. */\nexport const REFERENCELIST = `${id}#referencelist`\n\nexport interface ListViewerState {\n  $type?: 'app.bsky.graph.defs#listViewerState'\n  muted?: boolean\n  blocked?: string\n}\n\nconst hashListViewerState = 'listViewerState'\n\nexport function isListViewerState<V>(v: V) {\n  return is$typed(v, id, hashListViewerState)\n}\n\nexport function validateListViewerState<V>(v: V) {\n  return validate<ListViewerState & V>(v, id, hashListViewerState)\n}\n\n/** indicates that a handle or DID could not be resolved */\nexport interface NotFoundActor {\n  $type?: 'app.bsky.graph.defs#notFoundActor'\n  actor: string\n  notFound: true\n}\n\nconst hashNotFoundActor = 'notFoundActor'\n\nexport function isNotFoundActor<V>(v: V) {\n  return is$typed(v, id, hashNotFoundActor)\n}\n\nexport function validateNotFoundActor<V>(v: V) {\n  return validate<NotFoundActor & V>(v, id, hashNotFoundActor)\n}\n\n/** lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object) */\nexport interface Relationship {\n  $type?: 'app.bsky.graph.defs#relationship'\n  did: string\n  /** if the actor follows this DID, this is the AT-URI of the follow record */\n  following?: string\n  /** if the actor is followed by this DID, contains the AT-URI of the follow record */\n  followedBy?: string\n  /** if the actor blocks this DID, this is the AT-URI of the block record */\n  blocking?: string\n  /** if the actor is blocked by this DID, contains the AT-URI of the block record */\n  blockedBy?: string\n  /** if the actor blocks this DID via a block list, this is the AT-URI of the listblock record */\n  blockingByList?: string\n  /** if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record */\n  blockedByList?: string\n}\n\nconst hashRelationship = 'relationship'\n\nexport function isRelationship<V>(v: V) {\n  return is$typed(v, id, hashRelationship)\n}\n\nexport function validateRelationship<V>(v: V) {\n  return validate<Relationship & V>(v, id, hashRelationship)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/follow.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.follow'\n\nexport interface Main {\n  $type: 'app.bsky.graph.follow'\n  subject: string\n  createdAt: string\n  via?: ComAtprotoRepoStrongRef.Main\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getActorStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getActorStarterPacks'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getBlocks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blocks: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getFollows.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getFollows'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  follows: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getKnownFollowers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getKnownFollowers'\n\nexport type QueryParams = {\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject: AppBskyActorDefs.ProfileView\n  cursor?: string\n  followers: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getList'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the list record to hydrate. */\n  list: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  list: AppBskyGraphDefs.ListView\n  items: AppBskyGraphDefs.ListItemView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getListBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListBlocks'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getListMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListMutes'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getLists.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getLists'\n\nexport type QueryParams = {\n  /** The account (actor) to enumerate lists from. */\n  actor: string\n  limit: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  lists: AppBskyGraphDefs.ListView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getListsWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getListsWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit: number\n  cursor?: string\n  /** Optional filter by list purpose. If not specified, all supported types are returned. */\n  purposes?: 'modlist' | 'curatelist' | (string & {})[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  listsWithMembership: ListWithMembership[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** A list and an optional list item indicating membership of a target user to that list. */\nexport interface ListWithMembership {\n  $type?: 'app.bsky.graph.getListsWithMembership#listWithMembership'\n  list: AppBskyGraphDefs.ListView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashListWithMembership = 'listWithMembership'\n\nexport function isListWithMembership<V>(v: V) {\n  return is$typed(v, id, hashListWithMembership)\n}\n\nexport function validateListWithMembership<V>(v: V) {\n  return validate<ListWithMembership & V>(v, id, hashListWithMembership)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getMutes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getMutes'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  mutes: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getRelationships.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getRelationships'\n\nexport type QueryParams = {\n  /** Primary account requesting relationships for. */\n  actor: string\n  /** List of 'other' accounts to be related back to the primary. */\n  others?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actor?: string\n  relationships: (\n    | $Typed<AppBskyGraphDefs.Relationship>\n    | $Typed<AppBskyGraphDefs.NotFoundActor>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ActorNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getStarterPack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPack'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) of the starter pack record. */\n  starterPack: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPack: AppBskyGraphDefs.StarterPackView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacks'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getStarterPacksWithMembership'\n\nexport type QueryParams = {\n  /** The account (actor) to check for membership. */\n  actor: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacksWithMembership: StarterPackWithMembership[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** A starter pack and an optional list item indicating membership of a target user to that starter pack. */\nexport interface StarterPackWithMembership {\n  $type?: 'app.bsky.graph.getStarterPacksWithMembership#starterPackWithMembership'\n  starterPack: AppBskyGraphDefs.StarterPackView\n  listItem?: AppBskyGraphDefs.ListItemView\n}\n\nconst hashStarterPackWithMembership = 'starterPackWithMembership'\n\nexport function isStarterPackWithMembership<V>(v: V) {\n  return is$typed(v, id, hashStarterPackWithMembership)\n}\n\nexport function validateStarterPackWithMembership<V>(v: V) {\n  return validate<StarterPackWithMembership & V>(\n    v,\n    id,\n    hashStarterPackWithMembership,\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.getSuggestedFollowsByActor'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: AppBskyActorDefs.ProfileView[]\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n  /** DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid */\n  isFallback?: boolean\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/list.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.list'\n\nexport interface Main {\n  $type: 'app.bsky.graph.list'\n  purpose: AppBskyGraphDefs.ListPurpose\n  /** Display name for list; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  avatar?: BlobRef\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/listblock.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listblock'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listblock'\n  /** Reference (AT-URI) to the mod list record. */\n  subject: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/listitem.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.listitem'\n\nexport interface Main {\n  $type: 'app.bsky.graph.listitem'\n  /** The account which is included on the list. */\n  subject: string\n  /** Reference (AT-URI) to the list record (app.bsky.graph.list). */\n  list: string\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/muteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/muteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/muteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.muteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/searchStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.searchStarterPacks'\n\nexport type QueryParams = {\n  /** Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  starterPacks: AppBskyGraphDefs.StarterPackViewBasic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/starterpack.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../richtext/facet.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.starterpack'\n\nexport interface Main {\n  $type: 'app.bsky.graph.starterpack'\n  /** Display name for starter pack; can not be empty. */\n  name: string\n  description?: string\n  descriptionFacets?: AppBskyRichtextFacet.Main[]\n  /** Reference (AT-URI) to the list record. */\n  list: string\n  feeds?: FeedItem[]\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n\nexport interface FeedItem {\n  $type?: 'app.bsky.graph.starterpack#feedItem'\n  uri: string\n}\n\nconst hashFeedItem = 'feedItem'\n\nexport function isFeedItem<V>(v: V) {\n  return is$typed(v, id, hashFeedItem)\n}\n\nexport function validateFeedItem<V>(v: V) {\n  return validate<FeedItem & V>(v, id, hashFeedItem)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/unmuteActor.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActor'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/unmuteActorList.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteActorList'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  list: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/unmuteThread.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.unmuteThread'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  root: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/graph/verification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.graph.verification'\n\nexport interface Main {\n  $type: 'app.bsky.graph.verification'\n  /** DID of the subject the verification applies to. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Date of when the verification was created. */\n  createdAt: string\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/labeler/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.defs'\n\nexport interface LabelerView {\n  $type?: 'app.bsky.labeler.defs#labelerView'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabelerView = 'labelerView'\n\nexport function isLabelerView<V>(v: V) {\n  return is$typed(v, id, hashLabelerView)\n}\n\nexport function validateLabelerView<V>(v: V) {\n  return validate<LabelerView & V>(v, id, hashLabelerView)\n}\n\nexport interface LabelerViewDetailed {\n  $type?: 'app.bsky.labeler.defs#labelerViewDetailed'\n  uri: string\n  cid: string\n  creator: AppBskyActorDefs.ProfileView\n  policies: LabelerPolicies\n  likeCount?: number\n  viewer?: LabelerViewerState\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n}\n\nconst hashLabelerViewDetailed = 'labelerViewDetailed'\n\nexport function isLabelerViewDetailed<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewDetailed)\n}\n\nexport function validateLabelerViewDetailed<V>(v: V) {\n  return validate<LabelerViewDetailed & V>(v, id, hashLabelerViewDetailed)\n}\n\nexport interface LabelerViewerState {\n  $type?: 'app.bsky.labeler.defs#labelerViewerState'\n  like?: string\n}\n\nconst hashLabelerViewerState = 'labelerViewerState'\n\nexport function isLabelerViewerState<V>(v: V) {\n  return is$typed(v, id, hashLabelerViewerState)\n}\n\nexport function validateLabelerViewerState<V>(v: V) {\n  return validate<LabelerViewerState & V>(v, id, hashLabelerViewerState)\n}\n\nexport interface LabelerPolicies {\n  $type?: 'app.bsky.labeler.defs#labelerPolicies'\n  /** The label values which this labeler publishes. May include global or custom labels. */\n  labelValues: ComAtprotoLabelDefs.LabelValue[]\n  /** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */\n  labelValueDefinitions?: ComAtprotoLabelDefs.LabelValueDefinition[]\n}\n\nconst hashLabelerPolicies = 'labelerPolicies'\n\nexport function isLabelerPolicies<V>(v: V) {\n  return is$typed(v, id, hashLabelerPolicies)\n}\n\nexport function validateLabelerPolicies<V>(v: V) {\n  return validate<LabelerPolicies & V>(v, id, hashLabelerPolicies)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/labeler/getServices.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.getServices'\n\nexport type QueryParams = {\n  dids: string[]\n  detailed: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  views: (\n    | $Typed<AppBskyLabelerDefs.LabelerView>\n    | $Typed<AppBskyLabelerDefs.LabelerViewDetailed>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/labeler/service.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyLabelerDefs from './defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.labeler.service'\n\nexport interface Main {\n  $type: 'app.bsky.labeler.service'\n  policies: AppBskyLabelerDefs.LabelerPolicies\n  labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string }\n  createdAt: string\n  /** The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. */\n  reasonTypes?: ComAtprotoModerationDefs.ReasonType[]\n  /** The set of subject types (account, record, etc) this service accepts reports on. */\n  subjectTypes?: ComAtprotoModerationDefs.SubjectType[]\n  /** Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. */\n  subjectCollections?: string[]\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.declaration'\n\nexport interface Main {\n  $type: 'app.bsky.notification.declaration'\n  /** A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'. */\n  allowSubscriptions: 'followers' | 'mutuals' | 'none' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.defs'\n\nexport interface RecordDeleted {\n  $type?: 'app.bsky.notification.defs#recordDeleted'\n}\n\nconst hashRecordDeleted = 'recordDeleted'\n\nexport function isRecordDeleted<V>(v: V) {\n  return is$typed(v, id, hashRecordDeleted)\n}\n\nexport function validateRecordDeleted<V>(v: V) {\n  return validate<RecordDeleted & V>(v, id, hashRecordDeleted)\n}\n\nexport interface ChatPreference {\n  $type?: 'app.bsky.notification.defs#chatPreference'\n  include: 'all' | 'accepted' | (string & {})\n  push: boolean\n}\n\nconst hashChatPreference = 'chatPreference'\n\nexport function isChatPreference<V>(v: V) {\n  return is$typed(v, id, hashChatPreference)\n}\n\nexport function validateChatPreference<V>(v: V) {\n  return validate<ChatPreference & V>(v, id, hashChatPreference)\n}\n\nexport interface FilterablePreference {\n  $type?: 'app.bsky.notification.defs#filterablePreference'\n  include: 'all' | 'follows' | (string & {})\n  list: boolean\n  push: boolean\n}\n\nconst hashFilterablePreference = 'filterablePreference'\n\nexport function isFilterablePreference<V>(v: V) {\n  return is$typed(v, id, hashFilterablePreference)\n}\n\nexport function validateFilterablePreference<V>(v: V) {\n  return validate<FilterablePreference & V>(v, id, hashFilterablePreference)\n}\n\nexport interface Preference {\n  $type?: 'app.bsky.notification.defs#preference'\n  list: boolean\n  push: boolean\n}\n\nconst hashPreference = 'preference'\n\nexport function isPreference<V>(v: V) {\n  return is$typed(v, id, hashPreference)\n}\n\nexport function validatePreference<V>(v: V) {\n  return validate<Preference & V>(v, id, hashPreference)\n}\n\nexport interface Preferences {\n  $type?: 'app.bsky.notification.defs#preferences'\n  chat: ChatPreference\n  follow: FilterablePreference\n  like: FilterablePreference\n  likeViaRepost: FilterablePreference\n  mention: FilterablePreference\n  quote: FilterablePreference\n  reply: FilterablePreference\n  repost: FilterablePreference\n  repostViaRepost: FilterablePreference\n  starterpackJoined: Preference\n  subscribedPost: Preference\n  unverified: Preference\n  verified: Preference\n}\n\nconst hashPreferences = 'preferences'\n\nexport function isPreferences<V>(v: V) {\n  return is$typed(v, id, hashPreferences)\n}\n\nexport function validatePreferences<V>(v: V) {\n  return validate<Preferences & V>(v, id, hashPreferences)\n}\n\nexport interface ActivitySubscription {\n  $type?: 'app.bsky.notification.defs#activitySubscription'\n  post: boolean\n  reply: boolean\n}\n\nconst hashActivitySubscription = 'activitySubscription'\n\nexport function isActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashActivitySubscription)\n}\n\nexport function validateActivitySubscription<V>(v: V) {\n  return validate<ActivitySubscription & V>(v, id, hashActivitySubscription)\n}\n\n/** Object used to store activity subscription data in stash. */\nexport interface SubjectActivitySubscription {\n  $type?: 'app.bsky.notification.defs#subjectActivitySubscription'\n  subject: string\n  activitySubscription: ActivitySubscription\n}\n\nconst hashSubjectActivitySubscription = 'subjectActivitySubscription'\n\nexport function isSubjectActivitySubscription<V>(v: V) {\n  return is$typed(v, id, hashSubjectActivitySubscription)\n}\n\nexport function validateSubjectActivitySubscription<V>(v: V) {\n  return validate<SubjectActivitySubscription & V>(\n    v,\n    id,\n    hashSubjectActivitySubscription,\n  )\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/getPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getPreferences'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/getUnreadCount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.getUnreadCount'\n\nexport type QueryParams = {\n  priority?: boolean\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  count: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/listActivitySubscriptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listActivitySubscriptions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subscriptions: AppBskyActorDefs.ProfileView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/listNotifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.listNotifications'\n\nexport type QueryParams = {\n  /** Notification reasons to include in response. */\n  reasons?: string[]\n  limit: number\n  priority?: boolean\n  cursor?: string\n  seenAt?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  notifications: Notification[]\n  priority?: boolean\n  seenAt?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Notification {\n  $type?: 'app.bsky.notification.listNotifications#notification'\n  uri: string\n  cid: string\n  author: AppBskyActorDefs.ProfileView\n  /** The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. */\n  reason:\n    | 'like'\n    | 'repost'\n    | 'follow'\n    | 'mention'\n    | 'reply'\n    | 'quote'\n    | 'starterpack-joined'\n    | 'verified'\n    | 'unverified'\n    | 'like-via-repost'\n    | 'repost-via-repost'\n    | 'subscribed-post'\n    | 'contact-match'\n    | (string & {})\n  reasonSubject?: string\n  record: { [_ in string]: unknown }\n  isRead: boolean\n  indexedAt: string\n  labels?: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashNotification = 'notification'\n\nexport function isNotification<V>(v: V) {\n  return is$typed(v, id, hashNotification)\n}\n\nexport function validateNotification<V>(v: V) {\n  return validate<Notification & V>(v, id, hashNotification)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/putActivitySubscription.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putActivitySubscription'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject: string\n  activitySubscription: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface OutputSchema {\n  subject: string\n  activitySubscription?: AppBskyNotificationDefs.ActivitySubscription\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/putPreferences.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferences'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  priority: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/putPreferencesV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyNotificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.putPreferencesV2'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  chat?: AppBskyNotificationDefs.ChatPreference\n  follow?: AppBskyNotificationDefs.FilterablePreference\n  like?: AppBskyNotificationDefs.FilterablePreference\n  likeViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  mention?: AppBskyNotificationDefs.FilterablePreference\n  quote?: AppBskyNotificationDefs.FilterablePreference\n  reply?: AppBskyNotificationDefs.FilterablePreference\n  repost?: AppBskyNotificationDefs.FilterablePreference\n  repostViaRepost?: AppBskyNotificationDefs.FilterablePreference\n  starterpackJoined?: AppBskyNotificationDefs.Preference\n  subscribedPost?: AppBskyNotificationDefs.Preference\n  unverified?: AppBskyNotificationDefs.Preference\n  verified?: AppBskyNotificationDefs.Preference\n}\n\nexport interface OutputSchema {\n  preferences: AppBskyNotificationDefs.Preferences\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/registerPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.registerPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n  /** Set to true when the actor is age restricted */\n  ageRestricted?: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/unregisterPush.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.unregisterPush'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  serviceDid: string\n  token: string\n  platform: 'ios' | 'android' | 'web' | (string & {})\n  appId: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/notification/updateSeen.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.notification.updateSeen'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  seenAt: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/richtext/facet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.richtext.facet'\n\n/** Annotation of a sub-string within rich text. */\nexport interface Main {\n  $type?: 'app.bsky.richtext.facet'\n  index: ByteSlice\n  features: ($Typed<Mention> | $Typed<Link> | $Typed<Tag> | { $type: string })[]\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n\n/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */\nexport interface Mention {\n  $type?: 'app.bsky.richtext.facet#mention'\n  did: string\n}\n\nconst hashMention = 'mention'\n\nexport function isMention<V>(v: V) {\n  return is$typed(v, id, hashMention)\n}\n\nexport function validateMention<V>(v: V) {\n  return validate<Mention & V>(v, id, hashMention)\n}\n\n/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */\nexport interface Link {\n  $type?: 'app.bsky.richtext.facet#link'\n  uri: string\n}\n\nconst hashLink = 'link'\n\nexport function isLink<V>(v: V) {\n  return is$typed(v, id, hashLink)\n}\n\nexport function validateLink<V>(v: V) {\n  return validate<Link & V>(v, id, hashLink)\n}\n\n/** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */\nexport interface Tag {\n  $type?: 'app.bsky.richtext.facet#tag'\n  tag: string\n}\n\nconst hashTag = 'tag'\n\nexport function isTag<V>(v: V) {\n  return is$typed(v, id, hashTag)\n}\n\nexport function validateTag<V>(v: V) {\n  return validate<Tag & V>(v, id, hashTag)\n}\n\n/** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */\nexport interface ByteSlice {\n  $type?: 'app.bsky.richtext.facet#byteSlice'\n  byteStart: number\n  byteEnd: number\n}\n\nconst hashByteSlice = 'byteSlice'\n\nexport function isByteSlice<V>(v: V) {\n  return is$typed(v, id, hashByteSlice)\n}\n\nexport function validateByteSlice<V>(v: V) {\n  return validate<ByteSlice & V>(v, id, hashByteSlice)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.defs'\n\nexport interface SkeletonSearchPost {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchPost'\n  uri: string\n}\n\nconst hashSkeletonSearchPost = 'skeletonSearchPost'\n\nexport function isSkeletonSearchPost<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchPost)\n}\n\nexport function validateSkeletonSearchPost<V>(v: V) {\n  return validate<SkeletonSearchPost & V>(v, id, hashSkeletonSearchPost)\n}\n\nexport interface SkeletonSearchActor {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchActor'\n  did: string\n}\n\nconst hashSkeletonSearchActor = 'skeletonSearchActor'\n\nexport function isSkeletonSearchActor<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchActor)\n}\n\nexport function validateSkeletonSearchActor<V>(v: V) {\n  return validate<SkeletonSearchActor & V>(v, id, hashSkeletonSearchActor)\n}\n\nexport interface SkeletonSearchStarterPack {\n  $type?: 'app.bsky.unspecced.defs#skeletonSearchStarterPack'\n  uri: string\n}\n\nconst hashSkeletonSearchStarterPack = 'skeletonSearchStarterPack'\n\nexport function isSkeletonSearchStarterPack<V>(v: V) {\n  return is$typed(v, id, hashSkeletonSearchStarterPack)\n}\n\nexport function validateSkeletonSearchStarterPack<V>(v: V) {\n  return validate<SkeletonSearchStarterPack & V>(\n    v,\n    id,\n    hashSkeletonSearchStarterPack,\n  )\n}\n\nexport interface TrendingTopic {\n  $type?: 'app.bsky.unspecced.defs#trendingTopic'\n  topic: string\n  displayName?: string\n  description?: string\n  link: string\n}\n\nconst hashTrendingTopic = 'trendingTopic'\n\nexport function isTrendingTopic<V>(v: V) {\n  return is$typed(v, id, hashTrendingTopic)\n}\n\nexport function validateTrendingTopic<V>(v: V) {\n  return validate<TrendingTopic & V>(v, id, hashTrendingTopic)\n}\n\nexport interface SkeletonTrend {\n  $type?: 'app.bsky.unspecced.defs#skeletonTrend'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  dids: string[]\n}\n\nconst hashSkeletonTrend = 'skeletonTrend'\n\nexport function isSkeletonTrend<V>(v: V) {\n  return is$typed(v, id, hashSkeletonTrend)\n}\n\nexport function validateSkeletonTrend<V>(v: V) {\n  return validate<SkeletonTrend & V>(v, id, hashSkeletonTrend)\n}\n\nexport interface TrendView {\n  $type?: 'app.bsky.unspecced.defs#trendView'\n  topic: string\n  displayName: string\n  link: string\n  startedAt: string\n  postCount: number\n  status?: 'hot' | (string & {})\n  category?: string\n  actors: AppBskyActorDefs.ProfileViewBasic[]\n}\n\nconst hashTrendView = 'trendView'\n\nexport function isTrendView<V>(v: V) {\n  return is$typed(v, id, hashTrendView)\n}\n\nexport function validateTrendView<V>(v: V) {\n  return validate<TrendView & V>(v, id, hashTrendView)\n}\n\nexport interface ThreadItemPost {\n  $type?: 'app.bsky.unspecced.defs#threadItemPost'\n  post: AppBskyFeedDefs.PostView\n  /** This post has more parents that were not present in the response. This is just a boolean, without the number of parents. */\n  moreParents: boolean\n  /** This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. */\n  moreReplies: number\n  /** This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. */\n  opThread: boolean\n  /** The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. */\n  hiddenByThreadgate: boolean\n  /** This is by an account muted by the viewer requesting it. */\n  mutedByViewer: boolean\n}\n\nconst hashThreadItemPost = 'threadItemPost'\n\nexport function isThreadItemPost<V>(v: V) {\n  return is$typed(v, id, hashThreadItemPost)\n}\n\nexport function validateThreadItemPost<V>(v: V) {\n  return validate<ThreadItemPost & V>(v, id, hashThreadItemPost)\n}\n\nexport interface ThreadItemNoUnauthenticated {\n  $type?: 'app.bsky.unspecced.defs#threadItemNoUnauthenticated'\n}\n\nconst hashThreadItemNoUnauthenticated = 'threadItemNoUnauthenticated'\n\nexport function isThreadItemNoUnauthenticated<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNoUnauthenticated)\n}\n\nexport function validateThreadItemNoUnauthenticated<V>(v: V) {\n  return validate<ThreadItemNoUnauthenticated & V>(\n    v,\n    id,\n    hashThreadItemNoUnauthenticated,\n  )\n}\n\nexport interface ThreadItemNotFound {\n  $type?: 'app.bsky.unspecced.defs#threadItemNotFound'\n}\n\nconst hashThreadItemNotFound = 'threadItemNotFound'\n\nexport function isThreadItemNotFound<V>(v: V) {\n  return is$typed(v, id, hashThreadItemNotFound)\n}\n\nexport function validateThreadItemNotFound<V>(v: V) {\n  return validate<ThreadItemNotFound & V>(v, id, hashThreadItemNotFound)\n}\n\nexport interface ThreadItemBlocked {\n  $type?: 'app.bsky.unspecced.defs#threadItemBlocked'\n  author: AppBskyFeedDefs.BlockedAuthor\n}\n\nconst hashThreadItemBlocked = 'threadItemBlocked'\n\nexport function isThreadItemBlocked<V>(v: V) {\n  return is$typed(v, id, hashThreadItemBlocked)\n}\n\nexport function validateThreadItemBlocked<V>(v: V) {\n  return validate<ThreadItemBlocked & V>(v, id, hashThreadItemBlocked)\n}\n\n/** The computed state of the age assurance process, returned to the user in question on certain authenticated requests. */\nexport interface AgeAssuranceState {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceState'\n  /** The timestamp when this state was last updated. */\n  lastInitiatedAt?: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | 'blocked' | (string & {})\n}\n\nconst hashAgeAssuranceState = 'ageAssuranceState'\n\nexport function isAgeAssuranceState<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceState)\n}\n\nexport function validateAgeAssuranceState<V>(v: V) {\n  return validate<AgeAssuranceState & V>(v, id, hashAgeAssuranceState)\n}\n\n/** Object used to store age assurance data in stash. */\nexport interface AgeAssuranceEvent {\n  $type?: 'app.bsky.unspecced.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The status of the age assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The email used for AA. */\n  email?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getAgeAssuranceState'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  checkEmailConfirmed?: boolean\n  liveNow?: LiveNowConfig[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface LiveNowConfig {\n  $type?: 'app.bsky.unspecced.getConfig#liveNowConfig'\n  did: string\n  domains: string[]\n}\n\nconst hashLiveNowConfig = 'liveNowConfig'\n\nexport function isLiveNowConfig<V>(v: V) {\n  return is$typed(v, id, hashLiveNowConfig)\n}\n\nexport function validateLiveNowConfig<V>(v: V) {\n  return validate<LiveNowConfig & V>(v, id, hashLiveNowConfig)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getOnboardingSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getOnboardingSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPopularFeedGenerators'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  query?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadOtherV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post. */\n  anchor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of other thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadOtherV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost> | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getPostThreadV2'\n\nexport type QueryParams = {\n  /** Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post. */\n  anchor: string\n  /** Whether to include parents above the anchor. */\n  above: boolean\n  /** How many levels of replies to include below the anchor. */\n  below: number\n  /** Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated). */\n  branchingFactor: number\n  /** Sorting for the thread replies. */\n  sort: 'newest' | 'oldest' | 'top' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** A flat list of thread items. The depth of each item is indicated by the depth property inside the item. */\n  thread: ThreadItem[]\n  threadgate?: AppBskyFeedDefs.ThreadgateView\n  /** Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them. */\n  hasOtherReplies: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ThreadItem {\n  $type?: 'app.bsky.unspecced.getPostThreadV2#threadItem'\n  uri: string\n  /** The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths. */\n  depth: number\n  value:\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemPost>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemNotFound>\n    | $Typed<AppBskyUnspeccedDefs.ThreadItemBlocked>\n    | { $type: string }\n}\n\nconst hashThreadItem = 'threadItem'\n\nexport function isThreadItem<V>(v: V) {\n  return is$typed(v, id, hashThreadItem)\n}\n\nexport function validateThreadItem<V>(v: V) {\n  return validate<ThreadItem & V>(v, id, hashThreadItem)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedFeeds.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyFeedDefs from '../feed/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeeds'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: AppBskyFeedDefs.GeneratorView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedFeedsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedFeedsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  feeds: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedOnboardingUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedOnboardingUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyGraphDefs from '../graph/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacks'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: AppBskyGraphDefs.StarterPackView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  starterPacks: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedUsers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsers'\n\nexport type QueryParams = {\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  actors: AppBskyActorDefs.ProfileView[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestedUsersSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestedUsersSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  /** Category of users to get suggestions for. */\n  category?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  dids: string[]\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: string\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getSuggestionsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit: number\n  cursor?: string\n  /** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. */\n  relativeToDid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n  /** DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer. */\n  relativeToDid?: string\n  /** DEPRECATED: use recIdStr instead. */\n  recId?: number\n  /** Snowflake for this recommendation, use when submitting recommendation events. */\n  recIdStr?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTaggedSuggestions'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  suggestions: Suggestion[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Suggestion {\n  $type?: 'app.bsky.unspecced.getTaggedSuggestions#suggestion'\n  tag: string\n  subjectType: 'actor' | 'feed' | (string & {})\n  subject: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getTrendingTopics.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendingTopics'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  topics: AppBskyUnspeccedDefs.TrendingTopic[]\n  suggested: AppBskyUnspeccedDefs.TrendingTopic[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getTrends.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrends'\n\nexport type QueryParams = {\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.TrendView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.getTrendsSkeleton'\n\nexport type QueryParams = {\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  trends: AppBskyUnspeccedDefs.SkeletonTrend[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.initAgeAssurance'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The user's email address to receive assurance instructions. */\n  email: string\n  /** The user's preferred language for communication during the assurance process. */\n  language: string\n  /** An ISO 3166-1 alpha-2 code of the user's location. */\n  countryCode: string\n}\n\nexport type OutputSchema = AppBskyUnspeccedDefs.AgeAssuranceState\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidEmail' | 'DidTooLong' | 'InvalidInitiation'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchActorsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */\n  viewer?: string\n  /** If true, acts as fast/simple 'typeahead' query. */\n  typeahead?: boolean\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  actors: AppBskyUnspeccedDefs.SkeletonSearchActor[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchPostsSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** Specifies the ranking order of results. */\n  sort: 'top' | 'latest' | (string & {})\n  /** Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD). */\n  since?: string\n  /** Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD). */\n  until?: string\n  /** Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions. */\n  mentions?: string\n  /** Filter to posts by the given account. Handles are resolved to DID before query-time. */\n  author?: string\n  /** Filter to posts in the given language. Expected to be based on post language field, though server may override language detection. */\n  lang?: string\n  /** Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization. */\n  domain?: string\n  /** Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching. */\n  url?: string\n  /** Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching. */\n  tag?: string[]\n  /** DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries. */\n  viewer?: string\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  posts: AppBskyUnspeccedDefs.SkeletonSearchPost[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/unspecced/searchStarterPacksSkeleton.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyUnspeccedDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.unspecced.searchStarterPacksSkeleton'\n\nexport type QueryParams = {\n  /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */\n  q: string\n  /** DID of the account making the request (not included for public/unauthenticated queries). */\n  viewer?: string\n  limit: number\n  /** Optional pagination mechanism; may not necessarily allow scrolling through entire result set. */\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits. */\n  hitsTotal?: number\n  starterPacks: AppBskyUnspeccedDefs.SkeletonSearchStarterPack[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadQueryString'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/video/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.defs'\n\nexport interface JobStatus {\n  $type?: 'app.bsky.video.defs#jobStatus'\n  jobId: string\n  did: string\n  /** The state of the video processing job. All values not listed as a known value indicate that the job is in process. */\n  state: 'JOB_STATE_COMPLETED' | 'JOB_STATE_FAILED' | (string & {})\n  /** Progress within the current processing state. */\n  progress?: number\n  blob?: BlobRef\n  error?: string\n  message?: string\n}\n\nconst hashJobStatus = 'jobStatus'\n\nexport function isJobStatus<V>(v: V) {\n  return is$typed(v, id, hashJobStatus)\n}\n\nexport function validateJobStatus<V>(v: V) {\n  return validate<JobStatus & V>(v, id, hashJobStatus)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/video/getJobStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getJobStatus'\n\nexport type QueryParams = {\n  jobId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/video/getUploadLimits.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.getUploadLimits'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canUpload: boolean\n  remainingDailyVideos?: number\n  remainingDailyBytes?: number\n  message?: string\n  error?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/app/bsky/video/uploadVideo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyVideoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'app.bsky.video.uploadVideo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  jobStatus: AppBskyVideoDefs.JobStatus\n}\n\nexport interface HandlerInput {\n  encoding: 'video/mp4'\n  body: stream.Readable\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/actor/declaration.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.declaration'\n\nexport interface Main {\n  $type: 'chat.bsky.actor.declaration'\n  allowIncoming: 'all' | 'none' | 'following' | (string & {})\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/actor/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.defs'\n\nexport interface ProfileViewBasic {\n  $type?: 'chat.bsky.actor.defs#profileViewBasic'\n  did: string\n  handle: string\n  displayName?: string\n  avatar?: string\n  associated?: AppBskyActorDefs.ProfileAssociated\n  viewer?: AppBskyActorDefs.ViewerState\n  labels?: ComAtprotoLabelDefs.Label[]\n  /** Set to true when the actor cannot actively participate in conversations */\n  chatDisabled?: boolean\n  verification?: AppBskyActorDefs.VerificationState\n}\n\nconst hashProfileViewBasic = 'profileViewBasic'\n\nexport function isProfileViewBasic<V>(v: V) {\n  return is$typed(v, id, hashProfileViewBasic)\n}\n\nexport function validateProfileViewBasic<V>(v: V) {\n  return validate<ProfileViewBasic & V>(v, id, hashProfileViewBasic)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/actor/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.deleteAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/actor/exportAccountData.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.actor.exportAccountData'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/jsonl'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/acceptConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.acceptConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  /** Rev when the convo was accepted. If not present, the convo was already accepted. */\n  rev?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/addReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.addReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'ReactionMessageDeleted'\n    | 'ReactionLimitReached'\n    | 'ReactionInvalidValue'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyRichtextFacet from '../../../app/bsky/richtext/facet.js'\nimport type * as AppBskyEmbedRecord from '../../../app/bsky/embed/record.js'\nimport type * as ChatBskyActorDefs from '../actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.defs'\n\nexport interface MessageRef {\n  $type?: 'chat.bsky.convo.defs#messageRef'\n  did: string\n  convoId: string\n  messageId: string\n}\n\nconst hashMessageRef = 'messageRef'\n\nexport function isMessageRef<V>(v: V) {\n  return is$typed(v, id, hashMessageRef)\n}\n\nexport function validateMessageRef<V>(v: V) {\n  return validate<MessageRef & V>(v, id, hashMessageRef)\n}\n\nexport interface MessageInput {\n  $type?: 'chat.bsky.convo.defs#messageInput'\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.Main> | { $type: string }\n}\n\nconst hashMessageInput = 'messageInput'\n\nexport function isMessageInput<V>(v: V) {\n  return is$typed(v, id, hashMessageInput)\n}\n\nexport function validateMessageInput<V>(v: V) {\n  return validate<MessageInput & V>(v, id, hashMessageInput)\n}\n\nexport interface MessageView {\n  $type?: 'chat.bsky.convo.defs#messageView'\n  id: string\n  rev: string\n  text: string\n  /** Annotations of text (mentions, URLs, hashtags, etc) */\n  facets?: AppBskyRichtextFacet.Main[]\n  embed?: $Typed<AppBskyEmbedRecord.View> | { $type: string }\n  /** Reactions to this message, in ascending order of creation time. */\n  reactions?: ReactionView[]\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashMessageView = 'messageView'\n\nexport function isMessageView<V>(v: V) {\n  return is$typed(v, id, hashMessageView)\n}\n\nexport function validateMessageView<V>(v: V) {\n  return validate<MessageView & V>(v, id, hashMessageView)\n}\n\nexport interface DeletedMessageView {\n  $type?: 'chat.bsky.convo.defs#deletedMessageView'\n  id: string\n  rev: string\n  sender: MessageViewSender\n  sentAt: string\n}\n\nconst hashDeletedMessageView = 'deletedMessageView'\n\nexport function isDeletedMessageView<V>(v: V) {\n  return is$typed(v, id, hashDeletedMessageView)\n}\n\nexport function validateDeletedMessageView<V>(v: V) {\n  return validate<DeletedMessageView & V>(v, id, hashDeletedMessageView)\n}\n\nexport interface MessageViewSender {\n  $type?: 'chat.bsky.convo.defs#messageViewSender'\n  did: string\n}\n\nconst hashMessageViewSender = 'messageViewSender'\n\nexport function isMessageViewSender<V>(v: V) {\n  return is$typed(v, id, hashMessageViewSender)\n}\n\nexport function validateMessageViewSender<V>(v: V) {\n  return validate<MessageViewSender & V>(v, id, hashMessageViewSender)\n}\n\nexport interface ReactionView {\n  $type?: 'chat.bsky.convo.defs#reactionView'\n  value: string\n  sender: ReactionViewSender\n  createdAt: string\n}\n\nconst hashReactionView = 'reactionView'\n\nexport function isReactionView<V>(v: V) {\n  return is$typed(v, id, hashReactionView)\n}\n\nexport function validateReactionView<V>(v: V) {\n  return validate<ReactionView & V>(v, id, hashReactionView)\n}\n\nexport interface ReactionViewSender {\n  $type?: 'chat.bsky.convo.defs#reactionViewSender'\n  did: string\n}\n\nconst hashReactionViewSender = 'reactionViewSender'\n\nexport function isReactionViewSender<V>(v: V) {\n  return is$typed(v, id, hashReactionViewSender)\n}\n\nexport function validateReactionViewSender<V>(v: V) {\n  return validate<ReactionViewSender & V>(v, id, hashReactionViewSender)\n}\n\nexport interface MessageAndReactionView {\n  $type?: 'chat.bsky.convo.defs#messageAndReactionView'\n  message: MessageView\n  reaction: ReactionView\n}\n\nconst hashMessageAndReactionView = 'messageAndReactionView'\n\nexport function isMessageAndReactionView<V>(v: V) {\n  return is$typed(v, id, hashMessageAndReactionView)\n}\n\nexport function validateMessageAndReactionView<V>(v: V) {\n  return validate<MessageAndReactionView & V>(v, id, hashMessageAndReactionView)\n}\n\nexport interface ConvoView {\n  $type?: 'chat.bsky.convo.defs#convoView'\n  id: string\n  rev: string\n  members: ChatBskyActorDefs.ProfileViewBasic[]\n  lastMessage?:\n    | $Typed<MessageView>\n    | $Typed<DeletedMessageView>\n    | { $type: string }\n  lastReaction?: $Typed<MessageAndReactionView> | { $type: string }\n  muted: boolean\n  status?: 'request' | 'accepted' | (string & {})\n  unreadCount: number\n}\n\nconst hashConvoView = 'convoView'\n\nexport function isConvoView<V>(v: V) {\n  return is$typed(v, id, hashConvoView)\n}\n\nexport function validateConvoView<V>(v: V) {\n  return validate<ConvoView & V>(v, id, hashConvoView)\n}\n\nexport interface LogBeginConvo {\n  $type?: 'chat.bsky.convo.defs#logBeginConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogBeginConvo = 'logBeginConvo'\n\nexport function isLogBeginConvo<V>(v: V) {\n  return is$typed(v, id, hashLogBeginConvo)\n}\n\nexport function validateLogBeginConvo<V>(v: V) {\n  return validate<LogBeginConvo & V>(v, id, hashLogBeginConvo)\n}\n\nexport interface LogAcceptConvo {\n  $type?: 'chat.bsky.convo.defs#logAcceptConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogAcceptConvo = 'logAcceptConvo'\n\nexport function isLogAcceptConvo<V>(v: V) {\n  return is$typed(v, id, hashLogAcceptConvo)\n}\n\nexport function validateLogAcceptConvo<V>(v: V) {\n  return validate<LogAcceptConvo & V>(v, id, hashLogAcceptConvo)\n}\n\nexport interface LogLeaveConvo {\n  $type?: 'chat.bsky.convo.defs#logLeaveConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogLeaveConvo = 'logLeaveConvo'\n\nexport function isLogLeaveConvo<V>(v: V) {\n  return is$typed(v, id, hashLogLeaveConvo)\n}\n\nexport function validateLogLeaveConvo<V>(v: V) {\n  return validate<LogLeaveConvo & V>(v, id, hashLogLeaveConvo)\n}\n\nexport interface LogMuteConvo {\n  $type?: 'chat.bsky.convo.defs#logMuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogMuteConvo = 'logMuteConvo'\n\nexport function isLogMuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogMuteConvo)\n}\n\nexport function validateLogMuteConvo<V>(v: V) {\n  return validate<LogMuteConvo & V>(v, id, hashLogMuteConvo)\n}\n\nexport interface LogUnmuteConvo {\n  $type?: 'chat.bsky.convo.defs#logUnmuteConvo'\n  rev: string\n  convoId: string\n}\n\nconst hashLogUnmuteConvo = 'logUnmuteConvo'\n\nexport function isLogUnmuteConvo<V>(v: V) {\n  return is$typed(v, id, hashLogUnmuteConvo)\n}\n\nexport function validateLogUnmuteConvo<V>(v: V) {\n  return validate<LogUnmuteConvo & V>(v, id, hashLogUnmuteConvo)\n}\n\nexport interface LogCreateMessage {\n  $type?: 'chat.bsky.convo.defs#logCreateMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogCreateMessage = 'logCreateMessage'\n\nexport function isLogCreateMessage<V>(v: V) {\n  return is$typed(v, id, hashLogCreateMessage)\n}\n\nexport function validateLogCreateMessage<V>(v: V) {\n  return validate<LogCreateMessage & V>(v, id, hashLogCreateMessage)\n}\n\nexport interface LogDeleteMessage {\n  $type?: 'chat.bsky.convo.defs#logDeleteMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogDeleteMessage = 'logDeleteMessage'\n\nexport function isLogDeleteMessage<V>(v: V) {\n  return is$typed(v, id, hashLogDeleteMessage)\n}\n\nexport function validateLogDeleteMessage<V>(v: V) {\n  return validate<LogDeleteMessage & V>(v, id, hashLogDeleteMessage)\n}\n\nexport interface LogReadMessage {\n  $type?: 'chat.bsky.convo.defs#logReadMessage'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n}\n\nconst hashLogReadMessage = 'logReadMessage'\n\nexport function isLogReadMessage<V>(v: V) {\n  return is$typed(v, id, hashLogReadMessage)\n}\n\nexport function validateLogReadMessage<V>(v: V) {\n  return validate<LogReadMessage & V>(v, id, hashLogReadMessage)\n}\n\nexport interface LogAddReaction {\n  $type?: 'chat.bsky.convo.defs#logAddReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogAddReaction = 'logAddReaction'\n\nexport function isLogAddReaction<V>(v: V) {\n  return is$typed(v, id, hashLogAddReaction)\n}\n\nexport function validateLogAddReaction<V>(v: V) {\n  return validate<LogAddReaction & V>(v, id, hashLogAddReaction)\n}\n\nexport interface LogRemoveReaction {\n  $type?: 'chat.bsky.convo.defs#logRemoveReaction'\n  rev: string\n  convoId: string\n  message: $Typed<MessageView> | $Typed<DeletedMessageView> | { $type: string }\n  reaction: ReactionView\n}\n\nconst hashLogRemoveReaction = 'logRemoveReaction'\n\nexport function isLogRemoveReaction<V>(v: V) {\n  return is$typed(v, id, hashLogRemoveReaction)\n}\n\nexport function validateLogRemoveReaction<V>(v: V) {\n  return validate<LogRemoveReaction & V>(v, id, hashLogRemoveReaction)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/deleteMessageForSelf.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.deleteMessageForSelf'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.DeletedMessageView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/getConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvo'\n\nexport type QueryParams = {\n  convoId: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/getConvoAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoAvailability'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  canChat: boolean\n  convo?: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/getConvoForMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getConvoForMembers'\n\nexport type QueryParams = {\n  members: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/getLog.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getLog'\n\nexport type QueryParams = {\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  logs: (\n    | $Typed<ChatBskyConvoDefs.LogBeginConvo>\n    | $Typed<ChatBskyConvoDefs.LogAcceptConvo>\n    | $Typed<ChatBskyConvoDefs.LogLeaveConvo>\n    | $Typed<ChatBskyConvoDefs.LogMuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogUnmuteConvo>\n    | $Typed<ChatBskyConvoDefs.LogCreateMessage>\n    | $Typed<ChatBskyConvoDefs.LogDeleteMessage>\n    | $Typed<ChatBskyConvoDefs.LogReadMessage>\n    | $Typed<ChatBskyConvoDefs.LogAddReaction>\n    | $Typed<ChatBskyConvoDefs.LogRemoveReaction>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/getMessages.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.getMessages'\n\nexport type QueryParams = {\n  convoId: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/leaveConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.leaveConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convoId: string\n  rev: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/listConvos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.listConvos'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  readState?: 'unread' | (string & {})\n  status?: 'request' | 'accepted' | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  convos: ChatBskyConvoDefs.ConvoView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/muteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.muteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/removeReaction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.removeReaction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId: string\n  value: string\n}\n\nexport interface OutputSchema {\n  message: ChatBskyConvoDefs.MessageView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ReactionMessageDeleted' | 'ReactionInvalidValue'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/sendMessage.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessage'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nexport type OutputSchema = ChatBskyConvoDefs.MessageView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/sendMessageBatch.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.sendMessageBatch'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  items: BatchItem[]\n}\n\nexport interface OutputSchema {\n  items: ChatBskyConvoDefs.MessageView[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface BatchItem {\n  $type?: 'chat.bsky.convo.sendMessageBatch#batchItem'\n  convoId: string\n  message: ChatBskyConvoDefs.MessageInput\n}\n\nconst hashBatchItem = 'batchItem'\n\nexport function isBatchItem<V>(v: V) {\n  return is$typed(v, id, hashBatchItem)\n}\n\nexport function validateBatchItem<V>(v: V) {\n  return validate<BatchItem & V>(v, id, hashBatchItem)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/unmuteConvo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.unmuteConvo'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/updateAllRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateAllRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  status?: 'request' | 'accepted' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** The count of updated convos. */\n  updatedCount: number\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/convo/updateRead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.convo.updateRead'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  convoId: string\n  messageId?: string\n}\n\nexport interface OutputSchema {\n  convo: ChatBskyConvoDefs.ConvoView\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/moderation/getActorMetadata.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getActorMetadata'\n\nexport type QueryParams = {\n  actor: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  day: Metadata\n  month: Metadata\n  all: Metadata\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Metadata {\n  $type?: 'chat.bsky.moderation.getActorMetadata#metadata'\n  messagesSent: number\n  messagesReceived: number\n  convos: number\n  convosStarted: number\n}\n\nconst hashMetadata = 'metadata'\n\nexport function isMetadata<V>(v: V) {\n  return is$typed(v, id, hashMetadata)\n}\n\nexport function validateMetadata<V>(v: V) {\n  return validate<Metadata & V>(v, id, hashMetadata)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/moderation/getMessageContext.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ChatBskyConvoDefs from '../convo/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.getMessageContext'\n\nexport type QueryParams = {\n  /** Conversation that the message is from. NOTE: this field will eventually be required. */\n  convoId?: string\n  messageId: string\n  before: number\n  after: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  messages: (\n    | $Typed<ChatBskyConvoDefs.MessageView>\n    | $Typed<ChatBskyConvoDefs.DeletedMessageView>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/chat/bsky/moderation/updateActorAccess.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'chat.bsky.moderation.updateActorAccess'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  actor: string\n  allowAccess: boolean\n  ref?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.defs'\n\nexport interface StatusAttr {\n  $type?: 'com.atproto.admin.defs#statusAttr'\n  applied: boolean\n  ref?: string\n}\n\nconst hashStatusAttr = 'statusAttr'\n\nexport function isStatusAttr<V>(v: V) {\n  return is$typed(v, id, hashStatusAttr)\n}\n\nexport function validateStatusAttr<V>(v: V) {\n  return validate<StatusAttr & V>(v, id, hashStatusAttr)\n}\n\nexport interface AccountView {\n  $type?: 'com.atproto.admin.defs#accountView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords?: { [_ in string]: unknown }[]\n  indexedAt: string\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  emailConfirmedAt?: string\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ThreatSignature[]\n}\n\nconst hashAccountView = 'accountView'\n\nexport function isAccountView<V>(v: V) {\n  return is$typed(v, id, hashAccountView)\n}\n\nexport function validateAccountView<V>(v: V) {\n  return validate<AccountView & V>(v, id, hashAccountView)\n}\n\nexport interface RepoRef {\n  $type?: 'com.atproto.admin.defs#repoRef'\n  did: string\n}\n\nconst hashRepoRef = 'repoRef'\n\nexport function isRepoRef<V>(v: V) {\n  return is$typed(v, id, hashRepoRef)\n}\n\nexport function validateRepoRef<V>(v: V) {\n  return validate<RepoRef & V>(v, id, hashRepoRef)\n}\n\nexport interface RepoBlobRef {\n  $type?: 'com.atproto.admin.defs#repoBlobRef'\n  did: string\n  cid: string\n  recordUri?: string\n}\n\nconst hashRepoBlobRef = 'repoBlobRef'\n\nexport function isRepoBlobRef<V>(v: V) {\n  return is$typed(v, id, hashRepoBlobRef)\n}\n\nexport function validateRepoBlobRef<V>(v: V) {\n  return validate<RepoBlobRef & V>(v, id, hashRepoBlobRef)\n}\n\nexport interface ThreatSignature {\n  $type?: 'com.atproto.admin.defs#threatSignature'\n  property: string\n  value: string\n}\n\nconst hashThreatSignature = 'threatSignature'\n\nexport function isThreatSignature<V>(v: V) {\n  return is$typed(v, id, hashThreatSignature)\n}\n\nexport function validateThreatSignature<V>(v: V) {\n  return validate<ThreatSignature & V>(v, id, hashThreatSignature)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for disabled invites. */\n  note?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.disableInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codes?: string[]\n  accounts?: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.enableAccountInvites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n  /** Optional reason for enabled invites. */\n  note?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/getAccountInfo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoAdminDefs.AccountView\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/getAccountInfos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getAccountInfos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  infos: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/getInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from '../server/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getInviteCodes'\n\nexport type QueryParams = {\n  sort: 'recent' | 'usage' | (string & {})\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.getSubjectStatus'\n\nexport type QueryParams = {\n  did?: string\n  uri?: string\n  blob?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.searchAccounts'\n\nexport type QueryParams = {\n  email?: string\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.sendEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  recipientDid: string\n  content: string\n  subject?: string\n  senderDid: string\n  /** Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers */\n  comment?: string\n}\n\nexport interface OutputSchema {\n  sent: boolean\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo. */\n  account: string\n  email: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  handle: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/updateAccountSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateAccountSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  /** Did-key formatted public key */\n  signingKey: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from './defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.admin.updateSubjectStatus'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n  deactivated?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface OutputSchema {\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ComAtprotoAdminDefs.RepoBlobRef>\n    | { $type: string }\n  takedown?: ComAtprotoAdminDefs.StatusAttr\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.defs'\n\nexport interface IdentityInfo {\n  $type?: 'com.atproto.identity.defs#identityInfo'\n  did: string\n  /** The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document. */\n  handle: string\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nconst hashIdentityInfo = 'identityInfo'\n\nexport function isIdentityInfo<V>(v: V) {\n  return is$typed(v, id, hashIdentityInfo)\n}\n\nexport function validateIdentityInfo<V>(v: V) {\n  return validate<IdentityInfo & V>(v, id, hashIdentityInfo)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/getRecommendedDidCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.getRecommendedDidCredentials'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs. */\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/refreshIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.refreshIdentity'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  identifier: string\n}\n\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound' | 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/requestPlcOperationSignature.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.requestPlcOperationSignature'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/resolveDid.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveDid'\n\nexport type QueryParams = {\n  /** DID to resolve. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The complete DID document for the identity. */\n  didDoc: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/resolveHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveHandle'\n\nexport type QueryParams = {\n  /** The handle to resolve. */\n  handle: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/resolveIdentity.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoIdentityDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.resolveIdentity'\n\nexport type QueryParams = {\n  /** Handle or DID to resolve. */\n  identifier: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ComAtprotoIdentityDefs.IdentityInfo\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HandleNotFound' | 'DidNotFound' | 'DidDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/signPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.signPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A token received through com.atproto.identity.requestPlcOperationSignature */\n  token?: string\n  rotationKeys?: string[]\n  alsoKnownAs?: string[]\n  verificationMethods?: { [_ in string]: unknown }\n  services?: { [_ in string]: unknown }\n}\n\nexport interface OutputSchema {\n  /** A signed DID PLC operation. */\n  operation: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/submitPlcOperation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.submitPlcOperation'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  operation: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/identity/updateHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.identity.updateHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The new handle. */\n  handle: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/label/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.defs'\n\n/** Metadata tag on an atproto resource (eg, repo or record). */\nexport interface Label {\n  $type?: 'com.atproto.label.defs#label'\n  /** The AT Protocol version of the label object. */\n  ver?: number\n  /** DID of the actor who created this label. */\n  src: string\n  /** AT URI of the record, repository (account), or other resource that this label applies to. */\n  uri: string\n  /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */\n  cid?: string\n  /** The short string name of the value or type of this label. */\n  val: string\n  /** If true, this is a negation label, overwriting a previous label. */\n  neg?: boolean\n  /** Timestamp when this label was created. */\n  cts: string\n  /** Timestamp at which this label expires (no longer applies). */\n  exp?: string\n  /** Signature of dag-cbor encoded label. */\n  sig?: Uint8Array\n}\n\nconst hashLabel = 'label'\n\nexport function isLabel<V>(v: V) {\n  return is$typed(v, id, hashLabel)\n}\n\nexport function validateLabel<V>(v: V) {\n  return validate<Label & V>(v, id, hashLabel)\n}\n\n/** Metadata tags on an atproto record, published by the author within the record. */\nexport interface SelfLabels {\n  $type?: 'com.atproto.label.defs#selfLabels'\n  values: SelfLabel[]\n}\n\nconst hashSelfLabels = 'selfLabels'\n\nexport function isSelfLabels<V>(v: V) {\n  return is$typed(v, id, hashSelfLabels)\n}\n\nexport function validateSelfLabels<V>(v: V) {\n  return validate<SelfLabels & V>(v, id, hashSelfLabels)\n}\n\n/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */\nexport interface SelfLabel {\n  $type?: 'com.atproto.label.defs#selfLabel'\n  /** The short string name of the value or type of this label. */\n  val: string\n}\n\nconst hashSelfLabel = 'selfLabel'\n\nexport function isSelfLabel<V>(v: V) {\n  return is$typed(v, id, hashSelfLabel)\n}\n\nexport function validateSelfLabel<V>(v: V) {\n  return validate<SelfLabel & V>(v, id, hashSelfLabel)\n}\n\n/** Declares a label value and its expected interpretations and behaviors. */\nexport interface LabelValueDefinition {\n  $type?: 'com.atproto.label.defs#labelValueDefinition'\n  /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */\n  identifier: string\n  /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */\n  severity: 'inform' | 'alert' | 'none' | (string & {})\n  /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */\n  blurs: 'content' | 'media' | 'none' | (string & {})\n  /** The default setting for this label. */\n  defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})\n  /** Does the user need to have adult content enabled in order to configure this label? */\n  adultOnly?: boolean\n  locales: LabelValueDefinitionStrings[]\n}\n\nconst hashLabelValueDefinition = 'labelValueDefinition'\n\nexport function isLabelValueDefinition<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinition)\n}\n\nexport function validateLabelValueDefinition<V>(v: V) {\n  return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)\n}\n\n/** Strings which describe the label in the UI, localized into a specific language. */\nexport interface LabelValueDefinitionStrings {\n  $type?: 'com.atproto.label.defs#labelValueDefinitionStrings'\n  /** The code of the language these strings are written in. */\n  lang: string\n  /** A short human-readable name for the label. */\n  name: string\n  /** A longer description of what the label means and why it might be applied. */\n  description: string\n}\n\nconst hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'\n\nexport function isLabelValueDefinitionStrings<V>(v: V) {\n  return is$typed(v, id, hashLabelValueDefinitionStrings)\n}\n\nexport function validateLabelValueDefinitionStrings<V>(v: V) {\n  return validate<LabelValueDefinitionStrings & V>(\n    v,\n    id,\n    hashLabelValueDefinitionStrings,\n  )\n}\n\nexport type LabelValue =\n  | '!hide'\n  | '!no-promote'\n  | '!warn'\n  | '!no-unauthenticated'\n  | 'dmca-violation'\n  | 'doxxing'\n  | 'porn'\n  | 'sexual'\n  | 'nudity'\n  | 'nsfl'\n  | 'gore'\n  | (string & {})\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/label/queryLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.queryLabels'\n\nexport type QueryParams = {\n  /** List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI. */\n  uriPatterns: string[]\n  /** Optional list of label sources (DIDs) to filter on. */\n  sources?: string[]\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/label/subscribeLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport { ErrorFrame } from '@atproto/xrpc-server'\nimport { IncomingMessage } from 'node:http'\nimport type * as ComAtprotoLabelDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.label.subscribeLabels'\n\nexport type QueryParams = {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\nexport type OutputSchema = $Typed<Labels> | $Typed<Info> | { $type: string }\nexport type HandlerError = ErrorFrame<'FutureCursor'>\nexport type HandlerOutput = HandlerError | OutputSchema\n\nexport interface Labels {\n  $type?: 'com.atproto.label.subscribeLabels#labels'\n  seq: number\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nconst hashLabels = 'labels'\n\nexport function isLabels<V>(v: V) {\n  return is$typed(v, id, hashLabels)\n}\n\nexport function validateLabels<V>(v: V) {\n  return validate<Labels & V>(v, id, hashLabels)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.label.subscribeLabels#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/lexicon/resolveLexicon.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLexiconSchema from './schema.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.resolveLexicon'\n\nexport type QueryParams = {\n  /** The lexicon NSID to resolve. */\n  nsid: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The CID of the lexicon schema record. */\n  cid: string\n  schema: ComAtprotoLexiconSchema.Main\n  /** The AT-URI of the lexicon schema record. */\n  uri: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'LexiconNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/lexicon/schema.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.lexicon.schema'\n\nexport interface Main {\n  $type: 'com.atproto.lexicon.schema'\n  /** Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system. */\n  lexicon: number\n  [k: string]: unknown\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain, true)\n}\n\nexport {\n  type Main as Record,\n  isMain as isRecord,\n  validateMain as validateRecord,\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/moderation/createReport.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.createReport'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  /** Additional context about the content and violation. */\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  modTool?: ModTool\n}\n\nexport interface OutputSchema {\n  id: number\n  reasonType: ComAtprotoModerationDefs.ReasonType\n  reason?: string\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  reportedBy: string\n  createdAt: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'com.atproto.moderation.createReport#modTool'\n  /** Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.moderation.defs'\n\nexport type ReasonType =\n  | 'com.atproto.moderation.defs#reasonSpam'\n  | 'com.atproto.moderation.defs#reasonViolation'\n  | 'com.atproto.moderation.defs#reasonMisleading'\n  | 'com.atproto.moderation.defs#reasonSexual'\n  | 'com.atproto.moderation.defs#reasonRude'\n  | 'com.atproto.moderation.defs#reasonOther'\n  | 'com.atproto.moderation.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`. */\nexport const REASONSPAM = `${id}#reasonSpam`\n/** Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`. */\nexport const REASONVIOLATION = `${id}#reasonViolation`\n/** Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`. */\nexport const REASONMISLEADING = `${id}#reasonMisleading`\n/** Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`. */\nexport const REASONSEXUAL = `${id}#reasonSexual`\n/** Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`. */\nexport const REASONRUDE = `${id}#reasonRude`\n/** Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`. */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n\n/** Tag describing a type of subject that might be reported. */\nexport type SubjectType = 'account' | 'record' | 'chat' | (string & {})\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/applyWrites.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.applyWrites'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  writes: ($Typed<Create> | $Typed<Update> | $Typed<Delete>)[]\n  /** If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  results?: (\n    | $Typed<CreateResult>\n    | $Typed<UpdateResult>\n    | $Typed<DeleteResult>\n  )[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Operation which creates a new record. */\nexport interface Create {\n  $type?: 'com.atproto.repo.applyWrites#create'\n  collection: string\n  /** NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. */\n  rkey?: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashCreate = 'create'\n\nexport function isCreate<V>(v: V) {\n  return is$typed(v, id, hashCreate)\n}\n\nexport function validateCreate<V>(v: V) {\n  return validate<Create & V>(v, id, hashCreate)\n}\n\n/** Operation which updates an existing record. */\nexport interface Update {\n  $type?: 'com.atproto.repo.applyWrites#update'\n  collection: string\n  rkey: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashUpdate = 'update'\n\nexport function isUpdate<V>(v: V) {\n  return is$typed(v, id, hashUpdate)\n}\n\nexport function validateUpdate<V>(v: V) {\n  return validate<Update & V>(v, id, hashUpdate)\n}\n\n/** Operation which deletes an existing record. */\nexport interface Delete {\n  $type?: 'com.atproto.repo.applyWrites#delete'\n  collection: string\n  rkey: string\n}\n\nconst hashDelete = 'delete'\n\nexport function isDelete<V>(v: V) {\n  return is$typed(v, id, hashDelete)\n}\n\nexport function validateDelete<V>(v: V) {\n  return validate<Delete & V>(v, id, hashDelete)\n}\n\nexport interface CreateResult {\n  $type?: 'com.atproto.repo.applyWrites#createResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashCreateResult = 'createResult'\n\nexport function isCreateResult<V>(v: V) {\n  return is$typed(v, id, hashCreateResult)\n}\n\nexport function validateCreateResult<V>(v: V) {\n  return validate<CreateResult & V>(v, id, hashCreateResult)\n}\n\nexport interface UpdateResult {\n  $type?: 'com.atproto.repo.applyWrites#updateResult'\n  uri: string\n  cid: string\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nconst hashUpdateResult = 'updateResult'\n\nexport function isUpdateResult<V>(v: V) {\n  return is$typed(v, id, hashUpdateResult)\n}\n\nexport function validateUpdateResult<V>(v: V) {\n  return validate<UpdateResult & V>(v, id, hashUpdateResult)\n}\n\nexport interface DeleteResult {\n  $type?: 'com.atproto.repo.applyWrites#deleteResult'\n}\n\nconst hashDeleteResult = 'deleteResult'\n\nexport function isDeleteResult<V>(v: V) {\n  return is$typed(v, id, hashDeleteResult)\n}\n\nexport function validateDeleteResult<V>(v: V) {\n  return validate<DeleteResult & V>(v, id, hashDeleteResult)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/createRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.createRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey?: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record itself. Must contain a $type field. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.defs'\n\nexport interface CommitMeta {\n  $type?: 'com.atproto.repo.defs#commitMeta'\n  cid: string\n  rev: string\n}\n\nconst hashCommitMeta = 'commitMeta'\n\nexport function isCommitMeta<V>(v: V) {\n  return is$typed(v, id, hashCommitMeta)\n}\n\nexport function validateCommitMeta<V>(v: V) {\n  return validate<CommitMeta & V>(v, id, hashCommitMeta)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/deleteRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.deleteRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Compare and swap with the previous record by CID. */\n  swapRecord?: string\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  commit?: ComAtprotoRepoDefs.CommitMeta\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/describeRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.describeRepo'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  /** The complete DID document for this account. */\n  didDoc: { [_ in string]: unknown }\n  /** List of all the collections (NSIDs) for which this repo contains at least one record. */\n  collections: string[]\n  /** Indicates if handle is currently valid (resolves bi-directionally) */\n  handleIsCorrect: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.getRecord'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** The CID of the version of the record. If not specified, then return the most recent version. */\n  cid?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  uri: string\n  cid?: string\n  value: { [_ in string]: unknown }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RecordNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/importRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.importRepo'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface HandlerInput {\n  encoding: 'application/vnd.ipld.car'\n  body: stream.Readable\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/listMissingBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listMissingBlobs'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  blobs: RecordBlob[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface RecordBlob {\n  $type?: 'com.atproto.repo.listMissingBlobs#recordBlob'\n  cid: string\n  recordUri: string\n}\n\nconst hashRecordBlob = 'recordBlob'\n\nexport function isRecordBlob<V>(v: V) {\n  return is$typed(v, id, hashRecordBlob)\n}\n\nexport function validateRecordBlob<V>(v: V) {\n  return validate<RecordBlob & V>(v, id, hashRecordBlob)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/listRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.listRecords'\n\nexport type QueryParams = {\n  /** The handle or DID of the repo. */\n  repo: string\n  /** The NSID of the record type. */\n  collection: string\n  /** The number of records to return. */\n  limit: number\n  cursor?: string\n  /** Flag to reverse the order of the returned records. */\n  reverse?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  records: Record[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Record {\n  $type?: 'com.atproto.repo.listRecords#record'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n}\n\nconst hashRecord = 'record'\n\nexport function isRecord<V>(v: V) {\n  return is$typed(v, id, hashRecord)\n}\n\nexport function validateRecord<V>(v: V) {\n  return validate<Record & V>(v, id, hashRecord)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/putRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoRepoDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.putRecord'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The handle or DID of the repo (aka, current account). */\n  repo: string\n  /** The NSID of the record collection. */\n  collection: string\n  /** The Record Key. */\n  rkey: string\n  /** Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. */\n  validate?: boolean\n  /** The record to write. */\n  record: { [_ in string]: unknown }\n  /** Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation */\n  swapRecord?: string | null\n  /** Compare and swap with the previous commit by CID. */\n  swapCommit?: string\n}\n\nexport interface OutputSchema {\n  uri: string\n  cid: string\n  commit?: ComAtprotoRepoDefs.CommitMeta\n  validationStatus?: 'valid' | 'unknown' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidSwap'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/strongRef.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.strongRef'\n\nexport interface Main {\n  $type?: 'com.atproto.repo.strongRef'\n  uri: string\n  cid: string\n}\n\nconst hashMain = 'main'\n\nexport function isMain<V>(v: V) {\n  return is$typed(v, id, hashMain)\n}\n\nexport function validateMain<V>(v: V) {\n  return validate<Main & V>(v, id, hashMain)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/repo/uploadBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.repo.uploadBlob'\n\nexport type QueryParams = {}\nexport type InputSchema = string | Uint8Array | Blob\n\nexport interface OutputSchema {\n  blob: BlobRef\n}\n\nexport interface HandlerInput {\n  encoding: '*/*'\n  body: stream.Readable\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/activateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.activateAccount'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/checkAccountStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.checkAccountStatus'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  validDid: boolean\n  repoCommit: string\n  repoRev: string\n  repoBlocks: number\n  indexedRecords: number\n  privateStateValues: number\n  expectedBlobs: number\n  importedBlobs: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.confirmEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email?: string\n  /** Requested handle for the account. */\n  handle: string\n  /** Pre-existing atproto DID, being imported to a new account. */\n  did?: string\n  inviteCode?: string\n  verificationCode?: string\n  verificationPhone?: string\n  /** Initial account password. May need to meet instance-specific password strength requirements. */\n  password?: string\n  /** DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. */\n  recoveryKey?: string\n  /** A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. */\n  plcOp?: { [_ in string]: unknown }\n}\n\n/** Account login session returned on successful account creation. */\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  /** The DID of the new account. */\n  did: string\n  /** Complete DID document. */\n  didDoc?: { [_ in string]: unknown }\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'InvalidHandle'\n    | 'InvalidPassword'\n    | 'InvalidInviteCode'\n    | 'HandleNotAvailable'\n    | 'UnsupportedDomain'\n    | 'UnresolvableDid'\n    | 'IncompatibleDidDoc'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/createAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A short name for the App Password, to help distinguish them. */\n  name: string\n  /** If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients. */\n  privileged?: boolean\n}\n\nexport type OutputSchema = AppPassword\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.createAppPassword#appPassword'\n  name: string\n  password: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/createInviteCode.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCode'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  useCount: number\n  forAccount?: string\n}\n\nexport interface OutputSchema {\n  code: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/createInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createInviteCodes'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  codeCount: number\n  useCount: number\n  forAccounts?: string[]\n}\n\nexport interface OutputSchema {\n  codes: AccountCodes[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AccountCodes {\n  $type?: 'com.atproto.server.createInviteCodes#accountCodes'\n  account: string\n  codes: string[]\n}\n\nconst hashAccountCodes = 'accountCodes'\n\nexport function isAccountCodes<V>(v: V) {\n  return is$typed(v, id, hashAccountCodes)\n}\n\nexport function validateAccountCodes<V>(v: V) {\n  return validate<AccountCodes & V>(v, id, hashAccountCodes)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/createSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.createSession'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Handle or other identifier supported by the server for the authenticating user. */\n  identifier: string\n  password: string\n  authFactorToken?: string\n  /** When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned */\n  allowTakendown?: boolean\n}\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown' | 'AuthFactorTokenRequired'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/deactivateAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deactivateAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** A recommendation to server as to how long they should hold onto the deactivated account before deleting. */\n  deleteAfter?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.defs'\n\nexport interface InviteCode {\n  $type?: 'com.atproto.server.defs#inviteCode'\n  code: string\n  available: number\n  disabled: boolean\n  forAccount: string\n  createdBy: string\n  createdAt: string\n  uses: InviteCodeUse[]\n}\n\nconst hashInviteCode = 'inviteCode'\n\nexport function isInviteCode<V>(v: V) {\n  return is$typed(v, id, hashInviteCode)\n}\n\nexport function validateInviteCode<V>(v: V) {\n  return validate<InviteCode & V>(v, id, hashInviteCode)\n}\n\nexport interface InviteCodeUse {\n  $type?: 'com.atproto.server.defs#inviteCodeUse'\n  usedBy: string\n  usedAt: string\n}\n\nconst hashInviteCodeUse = 'inviteCodeUse'\n\nexport function isInviteCodeUse<V>(v: V) {\n  return is$typed(v, id, hashInviteCodeUse)\n}\n\nexport function validateInviteCodeUse<V>(v: V) {\n  return validate<InviteCodeUse & V>(v, id, hashInviteCodeUse)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/deleteAccount.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteAccount'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  password: string\n  token: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/deleteSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.deleteSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidToken' | 'ExpiredToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.describeServer'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** If true, an invite code must be supplied to create an account on this instance. */\n  inviteCodeRequired?: boolean\n  /** If true, a phone verification token must be supplied to create an account on this instance. */\n  phoneVerificationRequired?: boolean\n  /** List of domain suffixes that can be used in account handles. */\n  availableUserDomains: string[]\n  links?: Links\n  contact?: Contact\n  did: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Links {\n  $type?: 'com.atproto.server.describeServer#links'\n  privacyPolicy?: string\n  termsOfService?: string\n}\n\nconst hashLinks = 'links'\n\nexport function isLinks<V>(v: V) {\n  return is$typed(v, id, hashLinks)\n}\n\nexport function validateLinks<V>(v: V) {\n  return validate<Links & V>(v, id, hashLinks)\n}\n\nexport interface Contact {\n  $type?: 'com.atproto.server.describeServer#contact'\n  email?: string\n}\n\nconst hashContact = 'contact'\n\nexport function isContact<V>(v: V) {\n  return is$typed(v, id, hashContact)\n}\n\nexport function validateContact<V>(v: V) {\n  return validate<Contact & V>(v, id, hashContact)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoServerDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getAccountInviteCodes'\n\nexport type QueryParams = {\n  includeUsed: boolean\n  /** Controls whether any new 'earned' but not 'created' invites should be created. */\n  createAvailable: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  codes: ComAtprotoServerDefs.InviteCode[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateCreate'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/getServiceAuth.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getServiceAuth'\n\nexport type QueryParams = {\n  /** The DID of the service that the token will be used to authenticate with */\n  aud: string\n  /** The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope. */\n  exp?: number\n  /** Lexicon (XRPC) method to bind the requested token to */\n  lxm?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  token: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'BadExpiration'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/getSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.getSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/listAppPasswords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.listAppPasswords'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  passwords: AppPassword[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface AppPassword {\n  $type?: 'com.atproto.server.listAppPasswords#appPassword'\n  name: string\n  createdAt: string\n  privileged?: boolean\n}\n\nconst hashAppPassword = 'appPassword'\n\nexport function isAppPassword<V>(v: V) {\n  return is$typed(v, id, hashAppPassword)\n}\n\nexport function validateAppPassword<V>(v: V) {\n  return validate<AppPassword & V>(v, id, hashAppPassword)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/refreshSession.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.refreshSession'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  accessJwt: string\n  refreshJwt: string\n  handle: string\n  did: string\n  didDoc?: { [_ in string]: unknown }\n  email?: string\n  emailConfirmed?: boolean\n  emailAuthFactor?: boolean\n  active?: boolean\n  /** Hosting status of the account. If not specified, then assume 'active'. */\n  status?: 'takendown' | 'suspended' | 'deactivated' | (string & {})\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'AccountTakedown' | 'InvalidToken' | 'ExpiredToken'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/requestAccountDelete.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestAccountDelete'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailConfirmation'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestEmailUpdate'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  tokenRequired: boolean\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/requestPasswordReset.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.requestPasswordReset'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/reserveSigningKey.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.reserveSigningKey'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The DID to reserve a key for. */\n  did?: string\n}\n\nexport interface OutputSchema {\n  /** The public key for the reserved signing key, in did:key serialization. */\n  signingKey: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/resetPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.resetPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  token: string\n  password: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/revokeAppPassword.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.revokeAppPassword'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  name: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.server.updateEmail'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  email: string\n  emailAuthFactor?: boolean\n  /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */\n  token?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.defs'\n\nexport type HostStatus =\n  | 'active'\n  | 'idle'\n  | 'offline'\n  | 'throttled'\n  | 'banned'\n  | (string & {})\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getBlob.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlob'\n\nexport type QueryParams = {\n  /** The DID of the account. */\n  did: string\n  /** The CID of the blob to fetch */\n  cid: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: '*/*'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'BlobNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getBlocks.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getBlocks'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  cids: string[]\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'BlockNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getCheckout'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getHead.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHead'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  root: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HeadNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getHostStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getHostStatus'\n\nexport type QueryParams = {\n  /** Hostname of the host (eg, PDS or relay) being queried. */\n  hostname: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  /** Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts. */\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HostNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getLatestCommit.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getLatestCommit'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cid: string\n  rev: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRecord'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  collection: string\n  /** Record Key */\n  rkey: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?:\n    | 'RecordNotFound'\n    | 'RepoNotFound'\n    | 'RepoTakendown'\n    | 'RepoSuspended'\n    | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport stream from 'node:stream'\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepo'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** The revision ('rev') of the repo to create a diff from. */\n  since?: string\n}\nexport type InputSchema = undefined\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/vnd.ipld.car'\n  body: Uint8Array | stream.Readable\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/getRepoStatus.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.getRepoStatus'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  did: string\n  active: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n  /** Optional field, the current rev of the repo, if active=true */\n  rev?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listBlobs'\n\nexport type QueryParams = {\n  /** The DID of the repo. */\n  did: string\n  /** Optional revision of the repo to list blobs since. */\n  since?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  cids: string[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound' | 'RepoTakendown' | 'RepoSuspended' | 'RepoDeactivated'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/listHosts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoSyncDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listHosts'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  /** Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first. */\n  hosts: Host[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Host {\n  $type?: 'com.atproto.sync.listHosts#host'\n  /** hostname of server; not a URL (no scheme) */\n  hostname: string\n  /** Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor). */\n  seq?: number\n  accountCount?: number\n  status?: ComAtprotoSyncDefs.HostStatus\n}\n\nconst hashHost = 'host'\n\nexport function isHost<V>(v: V) {\n  return is$typed(v, id, hashHost)\n}\n\nexport function validateHost<V>(v: V) {\n  return validate<Host & V>(v, id, hashHost)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/listRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listRepos'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listRepos#repo'\n  did: string\n  /** Current repo commit CID */\n  head: string\n  rev: string\n  active?: boolean\n  /** If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/listReposByCollection.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.listReposByCollection'\n\nexport type QueryParams = {\n  collection: string\n  /** Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists. */\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: Repo[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Repo {\n  $type?: 'com.atproto.sync.listReposByCollection#repo'\n  did: string\n}\n\nconst hashRepo = 'repo'\n\nexport function isRepo<V>(v: V) {\n  return is$typed(v, id, hashRepo)\n}\n\nexport function validateRepo<V>(v: V) {\n  return validate<Repo & V>(v, id, hashRepo)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.notifyOfUpdate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (usually a PDS) that is notifying of update. */\n  hostname: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/requestCrawl.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.requestCrawl'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Hostname of the current service (eg, PDS) that is requesting to be crawled. */\n  hostname: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'HostBanned'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport { ErrorFrame } from '@atproto/xrpc-server'\nimport { IncomingMessage } from 'node:http'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.sync.subscribeRepos'\n\nexport type QueryParams = {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\nexport type OutputSchema =\n  | $Typed<Commit>\n  | $Typed<Sync>\n  | $Typed<Identity>\n  | $Typed<Account>\n  | $Typed<Info>\n  | { $type: string }\nexport type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'>\nexport type HandlerOutput = HandlerError | OutputSchema\n\n/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */\nexport interface Commit {\n  $type?: 'com.atproto.sync.subscribeRepos#commit'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** DEPRECATED -- unused */\n  rebase: boolean\n  /** DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */\n  tooBig: boolean\n  /** The repo this event comes from. Note that all other message types name this field 'did'. */\n  repo: string\n  /** Repo commit object CID. */\n  commit: CID\n  /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */\n  rev: string\n  /** The rev of the last emitted commit from this repo (if any). */\n  since: string | null\n  /** CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list. */\n  blocks: Uint8Array\n  ops: RepoOp[]\n  blobs: CID[]\n  /** The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose. */\n  prevData?: CID\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashCommit = 'commit'\n\nexport function isCommit<V>(v: V) {\n  return is$typed(v, id, hashCommit)\n}\n\nexport function validateCommit<V>(v: V) {\n  return validate<Commit & V>(v, id, hashCommit)\n}\n\n/** Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository. */\nexport interface Sync {\n  $type?: 'com.atproto.sync.subscribeRepos#sync'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** The account this repo event corresponds to. Must match that in the commit object. */\n  did: string\n  /** CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'. */\n  blocks: Uint8Array\n  /** The rev of the commit. This value must match that in the commit object. */\n  rev: string\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nconst hashSync = 'sync'\n\nexport function isSync<V>(v: V) {\n  return is$typed(v, id, hashSync)\n}\n\nexport function validateSync<V>(v: V) {\n  return validate<Sync & V>(v, id, hashSync)\n}\n\n/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */\nexport interface Identity {\n  $type?: 'com.atproto.sync.subscribeRepos#identity'\n  seq: number\n  did: string\n  time: string\n  /** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */\n  handle?: string\n}\n\nconst hashIdentity = 'identity'\n\nexport function isIdentity<V>(v: V) {\n  return is$typed(v, id, hashIdentity)\n}\n\nexport function validateIdentity<V>(v: V) {\n  return validate<Identity & V>(v, id, hashIdentity)\n}\n\n/** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */\nexport interface Account {\n  $type?: 'com.atproto.sync.subscribeRepos#account'\n  seq: number\n  did: string\n  time: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  /** If active=false, this optional field indicates a reason for why the account is not active. */\n  status?:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'desynchronized'\n    | 'throttled'\n    | (string & {})\n}\n\nconst hashAccount = 'account'\n\nexport function isAccount<V>(v: V) {\n  return is$typed(v, id, hashAccount)\n}\n\nexport function validateAccount<V>(v: V) {\n  return validate<Account & V>(v, id, hashAccount)\n}\n\nexport interface Info {\n  $type?: 'com.atproto.sync.subscribeRepos#info'\n  name: 'OutdatedCursor' | (string & {})\n  message?: string\n}\n\nconst hashInfo = 'info'\n\nexport function isInfo<V>(v: V) {\n  return is$typed(v, id, hashInfo)\n}\n\nexport function validateInfo<V>(v: V) {\n  return validate<Info & V>(v, id, hashInfo)\n}\n\n/** A repo operation, ie a mutation of a single record. */\nexport interface RepoOp {\n  $type?: 'com.atproto.sync.subscribeRepos#repoOp'\n  action: 'create' | 'update' | 'delete' | (string & {})\n  path: string\n  /** For creates and updates, the new record CID. For deletions, null. */\n  cid: CID | null\n  /** For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined. */\n  prev?: CID\n}\n\nconst hashRepoOp = 'repoOp'\n\nexport function isRepoOp<V>(v: V) {\n  return is$typed(v, id, hashRepoOp)\n}\n\nexport function validateRepoOp<V>(v: V) {\n  return validate<RepoOp & V>(v, id, hashRepoOp)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/addReservedHandle.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.addReservedHandle'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  handle: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/checkHandleAvailability.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkHandleAvailability'\n\nexport type QueryParams = {\n  /** Tentative handle. Will be checked for availability or used to build handle suggestions. */\n  handle: string\n  /** User-provided email. Might be used to build handle suggestions. */\n  email?: string\n  /** User-provided birth date. Might be used to build handle suggestions. */\n  birthDate?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** Echo of the input handle. */\n  handle: string\n  result:\n    | $Typed<ResultAvailable>\n    | $Typed<ResultUnavailable>\n    | { $type: string }\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidEmail'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Indicates the provided handle is available. */\nexport interface ResultAvailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultAvailable'\n}\n\nconst hashResultAvailable = 'resultAvailable'\n\nexport function isResultAvailable<V>(v: V) {\n  return is$typed(v, id, hashResultAvailable)\n}\n\nexport function validateResultAvailable<V>(v: V) {\n  return validate<ResultAvailable & V>(v, id, hashResultAvailable)\n}\n\n/** Indicates the provided handle is unavailable and gives suggestions of available handles. */\nexport interface ResultUnavailable {\n  $type?: 'com.atproto.temp.checkHandleAvailability#resultUnavailable'\n  /** List of suggested handles based on the provided inputs. */\n  suggestions: Suggestion[]\n}\n\nconst hashResultUnavailable = 'resultUnavailable'\n\nexport function isResultUnavailable<V>(v: V) {\n  return is$typed(v, id, hashResultUnavailable)\n}\n\nexport function validateResultUnavailable<V>(v: V) {\n  return validate<ResultUnavailable & V>(v, id, hashResultUnavailable)\n}\n\nexport interface Suggestion {\n  $type?: 'com.atproto.temp.checkHandleAvailability#suggestion'\n  handle: string\n  /** Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics. */\n  method: string\n}\n\nconst hashSuggestion = 'suggestion'\n\nexport function isSuggestion<V>(v: V) {\n  return is$typed(v, id, hashSuggestion)\n}\n\nexport function validateSuggestion<V>(v: V) {\n  return validate<Suggestion & V>(v, id, hashSuggestion)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/checkSignupQueue.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.checkSignupQueue'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  activated: boolean\n  placeInQueue?: number\n  estimatedTimeMs?: number\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/dereferenceScope.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.dereferenceScope'\n\nexport type QueryParams = {\n  /** The scope reference (starts with 'ref:') */\n  scope: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  /** The full oauth permission scope */\n  scope: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidScopeReference'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/fetchLabels.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoLabelDefs from '../label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.fetchLabels'\n\nexport type QueryParams = {\n  since?: number\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  labels: ComAtprotoLabelDefs.Label[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.requestPhoneVerification'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  phoneNumber: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/com/atproto/temp/revokeAccountCredentials.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'com.atproto.temp.revokeAccountCredentials'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  account: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/communication/createTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.createTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the template. */\n  name: string\n  /** Content of the template, markdown supported, can contain variable placeholders. */\n  contentMarkdown: string\n  /** Subject of the message, used in emails. */\n  subject: string\n  /** Message language. */\n  lang?: string\n  /** DID of the user who is creating the template. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneCommunicationDefs.TemplateView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateTemplateName'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/communication/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.defs'\n\nexport interface TemplateView {\n  $type?: 'tools.ozone.communication.defs#templateView'\n  id: string\n  /** Name of the template. */\n  name: string\n  /** Content of the template, can contain markdown and variable placeholders. */\n  subject?: string\n  /** Subject of the message, used in emails. */\n  contentMarkdown: string\n  disabled: boolean\n  /** Message language. */\n  lang?: string\n  /** DID of the user who last updated the template. */\n  lastUpdatedBy: string\n  createdAt: string\n  updatedAt: string\n}\n\nconst hashTemplateView = 'templateView'\n\nexport function isTemplateView<V>(v: V) {\n  return is$typed(v, id, hashTemplateView)\n}\n\nexport function validateTemplateView<V>(v: V) {\n  return validate<TemplateView & V>(v, id, hashTemplateView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/communication/deleteTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.deleteTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  id: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/communication/listTemplates.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.listTemplates'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  communicationTemplates: ToolsOzoneCommunicationDefs.TemplateView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/communication/updateTemplate.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneCommunicationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.communication.updateTemplate'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** ID of the template to be updated. */\n  id: string\n  /** Name of the template. */\n  name?: string\n  /** Message language. */\n  lang?: string\n  /** Content of the template, markdown supported, can contain variable placeholders. */\n  contentMarkdown?: string\n  /** Subject of the message, used in emails. */\n  subject?: string\n  /** DID of the user who is updating the template. */\n  updatedBy?: string\n  disabled?: boolean\n}\n\nexport type OutputSchema = ToolsOzoneCommunicationDefs.TemplateView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'DuplicateTemplateName'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/hosting/getAccountHistory.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.hosting.getAccountHistory'\n\nexport type QueryParams = {\n  did: string\n  events?:\n    | 'accountCreated'\n    | 'emailUpdated'\n    | 'emailConfirmed'\n    | 'passwordUpdated'\n    | 'handleUpdated'\n    | (string & {})[]\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  events: Event[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface Event {\n  $type?: 'tools.ozone.hosting.getAccountHistory#event'\n  details:\n    | $Typed<AccountCreated>\n    | $Typed<EmailUpdated>\n    | $Typed<EmailConfirmed>\n    | $Typed<PasswordUpdated>\n    | $Typed<HandleUpdated>\n    | { $type: string }\n  createdBy: string\n  createdAt: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n\nexport interface AccountCreated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#accountCreated'\n  email?: string\n  handle?: string\n}\n\nconst hashAccountCreated = 'accountCreated'\n\nexport function isAccountCreated<V>(v: V) {\n  return is$typed(v, id, hashAccountCreated)\n}\n\nexport function validateAccountCreated<V>(v: V) {\n  return validate<AccountCreated & V>(v, id, hashAccountCreated)\n}\n\nexport interface EmailUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#emailUpdated'\n  email: string\n}\n\nconst hashEmailUpdated = 'emailUpdated'\n\nexport function isEmailUpdated<V>(v: V) {\n  return is$typed(v, id, hashEmailUpdated)\n}\n\nexport function validateEmailUpdated<V>(v: V) {\n  return validate<EmailUpdated & V>(v, id, hashEmailUpdated)\n}\n\nexport interface EmailConfirmed {\n  $type?: 'tools.ozone.hosting.getAccountHistory#emailConfirmed'\n  email: string\n}\n\nconst hashEmailConfirmed = 'emailConfirmed'\n\nexport function isEmailConfirmed<V>(v: V) {\n  return is$typed(v, id, hashEmailConfirmed)\n}\n\nexport function validateEmailConfirmed<V>(v: V) {\n  return validate<EmailConfirmed & V>(v, id, hashEmailConfirmed)\n}\n\nexport interface PasswordUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#passwordUpdated'\n}\n\nconst hashPasswordUpdated = 'passwordUpdated'\n\nexport function isPasswordUpdated<V>(v: V) {\n  return is$typed(v, id, hashPasswordUpdated)\n}\n\nexport function validatePasswordUpdated<V>(v: V) {\n  return validate<PasswordUpdated & V>(v, id, hashPasswordUpdated)\n}\n\nexport interface HandleUpdated {\n  $type?: 'tools.ozone.hosting.getAccountHistory#handleUpdated'\n  handle: string\n}\n\nconst hashHandleUpdated = 'handleUpdated'\n\nexport function isHandleUpdated<V>(v: V) {\n  return is$typed(v, id, hashHandleUpdated)\n}\n\nexport function validateHandleUpdated<V>(v: V) {\n  return validate<HandleUpdated & V>(v, id, hashHandleUpdated)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/cancelScheduledActions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.cancelScheduledActions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of DID subjects to cancel scheduled actions for */\n  subjects: string[]\n  /** Optional comment describing the reason for cancellation */\n  comment?: string\n}\n\nexport type OutputSchema = CancellationResults\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface CancellationResults {\n  $type?: 'tools.ozone.moderation.cancelScheduledActions#cancellationResults'\n  /** DIDs for which all pending scheduled actions were successfully cancelled */\n  succeeded: string[]\n  /** DIDs for which cancellation failed with error details */\n  failed: FailedCancellation[]\n}\n\nconst hashCancellationResults = 'cancellationResults'\n\nexport function isCancellationResults<V>(v: V) {\n  return is$typed(v, id, hashCancellationResults)\n}\n\nexport function validateCancellationResults<V>(v: V) {\n  return validate<CancellationResults & V>(v, id, hashCancellationResults)\n}\n\nexport interface FailedCancellation {\n  $type?: 'tools.ozone.moderation.cancelScheduledActions#failedCancellation'\n  did: string\n  error: string\n  errorCode?: string\n}\n\nconst hashFailedCancellation = 'failedCancellation'\n\nexport function isFailedCancellation<V>(v: V) {\n  return is$typed(v, id, hashFailedCancellation)\n}\n\nexport function validateFailedCancellation<V>(v: V) {\n  return validate<FailedCancellation & V>(v, id, hashFailedCancellation)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\nimport type * as ChatBskyConvoDefs from '../../../chat/bsky/convo/defs.js'\nimport type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.js'\nimport type * as AppBskyAgeassuranceDefs from '../../../app/bsky/ageassurance/defs.js'\nimport type * as ComAtprotoServerDefs from '../../../com/atproto/server/defs.js'\nimport type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.defs'\n\nexport interface ModEventView {\n  $type?: 'tools.ozone.moderation.defs#modEventView'\n  id: number\n  event:\n    | $Typed<ModEventTakedown>\n    | $Typed<ModEventReverseTakedown>\n    | $Typed<ModEventComment>\n    | $Typed<ModEventReport>\n    | $Typed<ModEventLabel>\n    | $Typed<ModEventAcknowledge>\n    | $Typed<ModEventEscalate>\n    | $Typed<ModEventMute>\n    | $Typed<ModEventUnmute>\n    | $Typed<ModEventMuteReporter>\n    | $Typed<ModEventUnmuteReporter>\n    | $Typed<ModEventEmail>\n    | $Typed<ModEventResolveAppeal>\n    | $Typed<ModEventDivert>\n    | $Typed<ModEventTag>\n    | $Typed<AccountEvent>\n    | $Typed<IdentityEvent>\n    | $Typed<RecordEvent>\n    | $Typed<ModEventPriorityScore>\n    | $Typed<AgeAssuranceEvent>\n    | $Typed<AgeAssuranceOverrideEvent>\n    | $Typed<AgeAssurancePurgeEvent>\n    | $Typed<RevokeAccountCredentialsEvent>\n    | $Typed<ScheduleTakedownEvent>\n    | $Typed<CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ChatBskyConvoDefs.MessageRef>\n    | { $type: string }\n  subjectBlobCids: string[]\n  createdBy: string\n  createdAt: string\n  creatorHandle?: string\n  subjectHandle?: string\n  modTool?: ModTool\n}\n\nconst hashModEventView = 'modEventView'\n\nexport function isModEventView<V>(v: V) {\n  return is$typed(v, id, hashModEventView)\n}\n\nexport function validateModEventView<V>(v: V) {\n  return validate<ModEventView & V>(v, id, hashModEventView)\n}\n\nexport interface ModEventViewDetail {\n  $type?: 'tools.ozone.moderation.defs#modEventViewDetail'\n  id: number\n  event:\n    | $Typed<ModEventTakedown>\n    | $Typed<ModEventReverseTakedown>\n    | $Typed<ModEventComment>\n    | $Typed<ModEventReport>\n    | $Typed<ModEventLabel>\n    | $Typed<ModEventAcknowledge>\n    | $Typed<ModEventEscalate>\n    | $Typed<ModEventMute>\n    | $Typed<ModEventUnmute>\n    | $Typed<ModEventMuteReporter>\n    | $Typed<ModEventUnmuteReporter>\n    | $Typed<ModEventEmail>\n    | $Typed<ModEventResolveAppeal>\n    | $Typed<ModEventDivert>\n    | $Typed<ModEventTag>\n    | $Typed<AccountEvent>\n    | $Typed<IdentityEvent>\n    | $Typed<RecordEvent>\n    | $Typed<ModEventPriorityScore>\n    | $Typed<AgeAssuranceEvent>\n    | $Typed<AgeAssuranceOverrideEvent>\n    | $Typed<AgeAssurancePurgeEvent>\n    | $Typed<RevokeAccountCredentialsEvent>\n    | $Typed<ScheduleTakedownEvent>\n    | $Typed<CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<RepoView>\n    | $Typed<RepoViewNotFound>\n    | $Typed<RecordView>\n    | $Typed<RecordViewNotFound>\n    | { $type: string }\n  subjectBlobs: BlobView[]\n  createdBy: string\n  createdAt: string\n  modTool?: ModTool\n}\n\nconst hashModEventViewDetail = 'modEventViewDetail'\n\nexport function isModEventViewDetail<V>(v: V) {\n  return is$typed(v, id, hashModEventViewDetail)\n}\n\nexport function validateModEventViewDetail<V>(v: V) {\n  return validate<ModEventViewDetail & V>(v, id, hashModEventViewDetail)\n}\n\nexport interface SubjectStatusView {\n  $type?: 'tools.ozone.moderation.defs#subjectStatusView'\n  id: number\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | $Typed<ChatBskyConvoDefs.MessageRef>\n    | { $type: string }\n  hosting?: $Typed<AccountHosting> | $Typed<RecordHosting> | { $type: string }\n  subjectBlobCids?: string[]\n  subjectRepoHandle?: string\n  /** Timestamp referencing when the last update was made to the moderation status of the subject */\n  updatedAt: string\n  /** Timestamp referencing the first moderation status impacting event was emitted on the subject */\n  createdAt: string\n  reviewState: SubjectReviewState\n  /** Sticky comment on the subject. */\n  comment?: string\n  /** Numeric value representing the level of priority. Higher score means higher priority. */\n  priorityScore?: number\n  muteUntil?: string\n  muteReportingUntil?: string\n  lastReviewedBy?: string\n  lastReviewedAt?: string\n  lastReportedAt?: string\n  /** Timestamp referencing when the author of the subject appealed a moderation action */\n  lastAppealedAt?: string\n  takendown?: boolean\n  /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */\n  appealed?: boolean\n  suspendUntil?: string\n  tags?: string[]\n  accountStats?: AccountStats\n  recordsStats?: RecordsStats\n  accountStrike?: AccountStrike\n  /** Current age assurance state of the subject. */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n  /** Whether or not the last successful update to age assurance was made by the user or admin. */\n  ageAssuranceUpdatedBy?: 'admin' | 'user' | (string & {})\n}\n\nconst hashSubjectStatusView = 'subjectStatusView'\n\nexport function isSubjectStatusView<V>(v: V) {\n  return is$typed(v, id, hashSubjectStatusView)\n}\n\nexport function validateSubjectStatusView<V>(v: V) {\n  return validate<SubjectStatusView & V>(v, id, hashSubjectStatusView)\n}\n\n/** Detailed view of a subject. For record subjects, the author's repo and profile will be returned. */\nexport interface SubjectView {\n  $type?: 'tools.ozone.moderation.defs#subjectView'\n  type: ComAtprotoModerationDefs.SubjectType\n  subject: string\n  status?: SubjectStatusView\n  repo?: RepoViewDetail\n  profile?: { $type: string }\n  record?: RecordViewDetail\n}\n\nconst hashSubjectView = 'subjectView'\n\nexport function isSubjectView<V>(v: V) {\n  return is$typed(v, id, hashSubjectView)\n}\n\nexport function validateSubjectView<V>(v: V) {\n  return validate<SubjectView & V>(v, id, hashSubjectView)\n}\n\n/** Statistics about a particular account subject */\nexport interface AccountStats {\n  $type?: 'tools.ozone.moderation.defs#accountStats'\n  /** Total number of reports on the account */\n  reportCount?: number\n  /** Total number of appeals against a moderation action on the account */\n  appealCount?: number\n  /** Number of times the account was suspended */\n  suspendCount?: number\n  /** Number of times the account was escalated */\n  escalateCount?: number\n  /** Number of times the account was taken down */\n  takedownCount?: number\n}\n\nconst hashAccountStats = 'accountStats'\n\nexport function isAccountStats<V>(v: V) {\n  return is$typed(v, id, hashAccountStats)\n}\n\nexport function validateAccountStats<V>(v: V) {\n  return validate<AccountStats & V>(v, id, hashAccountStats)\n}\n\n/** Statistics about a set of record subject items */\nexport interface RecordsStats {\n  $type?: 'tools.ozone.moderation.defs#recordsStats'\n  /** Cumulative sum of the number of reports on the items in the set */\n  totalReports?: number\n  /** Number of items that were reported at least once */\n  reportedCount?: number\n  /** Number of items that were escalated at least once */\n  escalatedCount?: number\n  /** Number of items that were appealed at least once */\n  appealedCount?: number\n  /** Total number of item in the set */\n  subjectCount?: number\n  /** Number of item currently in \"reviewOpen\" or \"reviewEscalated\" state */\n  pendingCount?: number\n  /** Number of item currently in \"reviewNone\" or \"reviewClosed\" state */\n  processedCount?: number\n  /** Number of item currently taken down */\n  takendownCount?: number\n}\n\nconst hashRecordsStats = 'recordsStats'\n\nexport function isRecordsStats<V>(v: V) {\n  return is$typed(v, id, hashRecordsStats)\n}\n\nexport function validateRecordsStats<V>(v: V) {\n  return validate<RecordsStats & V>(v, id, hashRecordsStats)\n}\n\n/** Strike information for an account */\nexport interface AccountStrike {\n  $type?: 'tools.ozone.moderation.defs#accountStrike'\n  /** Current number of active strikes (excluding expired strikes) */\n  activeStrikeCount?: number\n  /** Total number of strikes ever received (including expired strikes) */\n  totalStrikeCount?: number\n  /** Timestamp of the first strike received */\n  firstStrikeAt?: string\n  /** Timestamp of the most recent strike received */\n  lastStrikeAt?: string\n}\n\nconst hashAccountStrike = 'accountStrike'\n\nexport function isAccountStrike<V>(v: V) {\n  return is$typed(v, id, hashAccountStrike)\n}\n\nexport function validateAccountStrike<V>(v: V) {\n  return validate<AccountStrike & V>(v, id, hashAccountStrike)\n}\n\nexport type SubjectReviewState =\n  | 'tools.ozone.moderation.defs#reviewOpen'\n  | 'tools.ozone.moderation.defs#reviewEscalated'\n  | 'tools.ozone.moderation.defs#reviewClosed'\n  | 'tools.ozone.moderation.defs#reviewNone'\n  | (string & {})\n\n/** Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator */\nexport const REVIEWOPEN = `${id}#reviewOpen`\n/** Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator */\nexport const REVIEWESCALATED = `${id}#reviewEscalated`\n/** Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator */\nexport const REVIEWCLOSED = `${id}#reviewClosed`\n/** Moderator review status of a subject: Unnecessary. Indicates that the subject does not need a review at the moment but there is probably some moderation related metadata available for it */\nexport const REVIEWNONE = `${id}#reviewNone`\n\n/** Take down a subject permanently or temporarily */\nexport interface ModEventTakedown {\n  $type?: 'tools.ozone.moderation.defs#modEventTakedown'\n  comment?: string\n  /** Indicates how long the takedown should be in effect before automatically expiring. */\n  durationInHours?: number\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n  /** Names/Keywords of the policies that drove the decision. */\n  policies?: string[]\n  /** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */\n  severityLevel?: string\n  /** List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services. */\n  targetServices?: ('appview' | 'pds' | (string & {}))[]\n  /** Number of strikes to assign to the user for this violation. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n}\n\nconst hashModEventTakedown = 'modEventTakedown'\n\nexport function isModEventTakedown<V>(v: V) {\n  return is$typed(v, id, hashModEventTakedown)\n}\n\nexport function validateModEventTakedown<V>(v: V) {\n  return validate<ModEventTakedown & V>(v, id, hashModEventTakedown)\n}\n\n/** Revert take down action on a subject */\nexport interface ModEventReverseTakedown {\n  $type?: 'tools.ozone.moderation.defs#modEventReverseTakedown'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n  /** Names/Keywords of the policy infraction for which takedown is being reversed. */\n  policies?: string[]\n  /** Severity level of the violation. Usually set from the last policy infraction's severity. */\n  severityLevel?: string\n  /** Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity. */\n  strikeCount?: number\n}\n\nconst hashModEventReverseTakedown = 'modEventReverseTakedown'\n\nexport function isModEventReverseTakedown<V>(v: V) {\n  return is$typed(v, id, hashModEventReverseTakedown)\n}\n\nexport function validateModEventReverseTakedown<V>(v: V) {\n  return validate<ModEventReverseTakedown & V>(\n    v,\n    id,\n    hashModEventReverseTakedown,\n  )\n}\n\n/** Resolve appeal on a subject */\nexport interface ModEventResolveAppeal {\n  $type?: 'tools.ozone.moderation.defs#modEventResolveAppeal'\n  /** Describe resolution. */\n  comment?: string\n}\n\nconst hashModEventResolveAppeal = 'modEventResolveAppeal'\n\nexport function isModEventResolveAppeal<V>(v: V) {\n  return is$typed(v, id, hashModEventResolveAppeal)\n}\n\nexport function validateModEventResolveAppeal<V>(v: V) {\n  return validate<ModEventResolveAppeal & V>(v, id, hashModEventResolveAppeal)\n}\n\n/** Add a comment to a subject. An empty comment will clear any previously set sticky comment. */\nexport interface ModEventComment {\n  $type?: 'tools.ozone.moderation.defs#modEventComment'\n  comment?: string\n  /** Make the comment persistent on the subject */\n  sticky?: boolean\n}\n\nconst hashModEventComment = 'modEventComment'\n\nexport function isModEventComment<V>(v: V) {\n  return is$typed(v, id, hashModEventComment)\n}\n\nexport function validateModEventComment<V>(v: V) {\n  return validate<ModEventComment & V>(v, id, hashModEventComment)\n}\n\n/** Report a subject */\nexport interface ModEventReport {\n  $type?: 'tools.ozone.moderation.defs#modEventReport'\n  comment?: string\n  /** Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject. */\n  isReporterMuted?: boolean\n  reportType: ComAtprotoModerationDefs.ReasonType\n}\n\nconst hashModEventReport = 'modEventReport'\n\nexport function isModEventReport<V>(v: V) {\n  return is$typed(v, id, hashModEventReport)\n}\n\nexport function validateModEventReport<V>(v: V) {\n  return validate<ModEventReport & V>(v, id, hashModEventReport)\n}\n\n/** Apply/Negate labels on a subject */\nexport interface ModEventLabel {\n  $type?: 'tools.ozone.moderation.defs#modEventLabel'\n  comment?: string\n  createLabelVals: string[]\n  negateLabelVals: string[]\n  /** Indicates how long the label will remain on the subject. Only applies on labels that are being added. */\n  durationInHours?: number\n}\n\nconst hashModEventLabel = 'modEventLabel'\n\nexport function isModEventLabel<V>(v: V) {\n  return is$typed(v, id, hashModEventLabel)\n}\n\nexport function validateModEventLabel<V>(v: V) {\n  return validate<ModEventLabel & V>(v, id, hashModEventLabel)\n}\n\n/** Set priority score of the subject. Higher score means higher priority. */\nexport interface ModEventPriorityScore {\n  $type?: 'tools.ozone.moderation.defs#modEventPriorityScore'\n  comment?: string\n  score: number\n}\n\nconst hashModEventPriorityScore = 'modEventPriorityScore'\n\nexport function isModEventPriorityScore<V>(v: V) {\n  return is$typed(v, id, hashModEventPriorityScore)\n}\n\nexport function validateModEventPriorityScore<V>(v: V) {\n  return validate<ModEventPriorityScore & V>(v, id, hashModEventPriorityScore)\n}\n\n/** Age assurance info coming directly from users. Only works on DID subjects. */\nexport interface AgeAssuranceEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssuranceEvent'\n  /** The date and time of this write operation. */\n  createdAt: string\n  /** The unique identifier for this instance of the age assurance flow, in UUID format. */\n  attemptId: string\n  /** The status of the Age Assurance process. */\n  status: 'unknown' | 'pending' | 'assured' | (string & {})\n  access?: AppBskyAgeassuranceDefs.Access\n  /** The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow. */\n  countryCode?: string\n  /** The ISO 3166-2 region code provided when beginning the Age Assurance flow. */\n  regionCode?: string\n  /** The IP address used when initiating the AA flow. */\n  initIp?: string\n  /** The user agent used when initiating the AA flow. */\n  initUa?: string\n  /** The IP address used when completing the AA flow. */\n  completeIp?: string\n  /** The user agent used when completing the AA flow. */\n  completeUa?: string\n}\n\nconst hashAgeAssuranceEvent = 'ageAssuranceEvent'\n\nexport function isAgeAssuranceEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceEvent)\n}\n\nexport function validateAgeAssuranceEvent<V>(v: V) {\n  return validate<AgeAssuranceEvent & V>(v, id, hashAgeAssuranceEvent)\n}\n\n/** Age assurance status override by moderators. Only works on DID subjects. */\nexport interface AgeAssuranceOverrideEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n  /** The status to be set for the user decided by a moderator, overriding whatever value the user had previously. Use reset to default to original state. */\n  status: 'assured' | 'reset' | 'blocked' | (string & {})\n  access?: AppBskyAgeassuranceDefs.Access\n  /** Comment describing the reason for the override. */\n  comment: string\n}\n\nconst hashAgeAssuranceOverrideEvent = 'ageAssuranceOverrideEvent'\n\nexport function isAgeAssuranceOverrideEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssuranceOverrideEvent)\n}\n\nexport function validateAgeAssuranceOverrideEvent<V>(v: V) {\n  return validate<AgeAssuranceOverrideEvent & V>(\n    v,\n    id,\n    hashAgeAssuranceOverrideEvent,\n  )\n}\n\n/** Purges all age assurance events for the subject. Only works on DID subjects. Moderator-only. */\nexport interface AgeAssurancePurgeEvent {\n  $type?: 'tools.ozone.moderation.defs#ageAssurancePurgeEvent'\n  /** Comment describing the reason for the purge. */\n  comment: string\n}\n\nconst hashAgeAssurancePurgeEvent = 'ageAssurancePurgeEvent'\n\nexport function isAgeAssurancePurgeEvent<V>(v: V) {\n  return is$typed(v, id, hashAgeAssurancePurgeEvent)\n}\n\nexport function validateAgeAssurancePurgeEvent<V>(v: V) {\n  return validate<AgeAssurancePurgeEvent & V>(v, id, hashAgeAssurancePurgeEvent)\n}\n\n/** Account credentials revocation by moderators. Only works on DID subjects. */\nexport interface RevokeAccountCredentialsEvent {\n  $type?: 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n  /** Comment describing the reason for the revocation. */\n  comment: string\n}\n\nconst hashRevokeAccountCredentialsEvent = 'revokeAccountCredentialsEvent'\n\nexport function isRevokeAccountCredentialsEvent<V>(v: V) {\n  return is$typed(v, id, hashRevokeAccountCredentialsEvent)\n}\n\nexport function validateRevokeAccountCredentialsEvent<V>(v: V) {\n  return validate<RevokeAccountCredentialsEvent & V>(\n    v,\n    id,\n    hashRevokeAccountCredentialsEvent,\n  )\n}\n\nexport interface ModEventAcknowledge {\n  $type?: 'tools.ozone.moderation.defs#modEventAcknowledge'\n  comment?: string\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n}\n\nconst hashModEventAcknowledge = 'modEventAcknowledge'\n\nexport function isModEventAcknowledge<V>(v: V) {\n  return is$typed(v, id, hashModEventAcknowledge)\n}\n\nexport function validateModEventAcknowledge<V>(v: V) {\n  return validate<ModEventAcknowledge & V>(v, id, hashModEventAcknowledge)\n}\n\nexport interface ModEventEscalate {\n  $type?: 'tools.ozone.moderation.defs#modEventEscalate'\n  comment?: string\n}\n\nconst hashModEventEscalate = 'modEventEscalate'\n\nexport function isModEventEscalate<V>(v: V) {\n  return is$typed(v, id, hashModEventEscalate)\n}\n\nexport function validateModEventEscalate<V>(v: V) {\n  return validate<ModEventEscalate & V>(v, id, hashModEventEscalate)\n}\n\n/** Mute incoming reports on a subject */\nexport interface ModEventMute {\n  $type?: 'tools.ozone.moderation.defs#modEventMute'\n  comment?: string\n  /** Indicates how long the subject should remain muted. */\n  durationInHours: number\n}\n\nconst hashModEventMute = 'modEventMute'\n\nexport function isModEventMute<V>(v: V) {\n  return is$typed(v, id, hashModEventMute)\n}\n\nexport function validateModEventMute<V>(v: V) {\n  return validate<ModEventMute & V>(v, id, hashModEventMute)\n}\n\n/** Unmute action on a subject */\nexport interface ModEventUnmute {\n  $type?: 'tools.ozone.moderation.defs#modEventUnmute'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n}\n\nconst hashModEventUnmute = 'modEventUnmute'\n\nexport function isModEventUnmute<V>(v: V) {\n  return is$typed(v, id, hashModEventUnmute)\n}\n\nexport function validateModEventUnmute<V>(v: V) {\n  return validate<ModEventUnmute & V>(v, id, hashModEventUnmute)\n}\n\n/** Mute incoming reports from an account */\nexport interface ModEventMuteReporter {\n  $type?: 'tools.ozone.moderation.defs#modEventMuteReporter'\n  comment?: string\n  /** Indicates how long the account should remain muted. Falsy value here means a permanent mute. */\n  durationInHours?: number\n}\n\nconst hashModEventMuteReporter = 'modEventMuteReporter'\n\nexport function isModEventMuteReporter<V>(v: V) {\n  return is$typed(v, id, hashModEventMuteReporter)\n}\n\nexport function validateModEventMuteReporter<V>(v: V) {\n  return validate<ModEventMuteReporter & V>(v, id, hashModEventMuteReporter)\n}\n\n/** Unmute incoming reports from an account */\nexport interface ModEventUnmuteReporter {\n  $type?: 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n  /** Describe reasoning behind the reversal. */\n  comment?: string\n}\n\nconst hashModEventUnmuteReporter = 'modEventUnmuteReporter'\n\nexport function isModEventUnmuteReporter<V>(v: V) {\n  return is$typed(v, id, hashModEventUnmuteReporter)\n}\n\nexport function validateModEventUnmuteReporter<V>(v: V) {\n  return validate<ModEventUnmuteReporter & V>(v, id, hashModEventUnmuteReporter)\n}\n\n/** Keep a log of outgoing email to a user */\nexport interface ModEventEmail {\n  $type?: 'tools.ozone.moderation.defs#modEventEmail'\n  /** The subject line of the email sent to the user. */\n  subjectLine: string\n  /** The content of the email sent to the user. */\n  content?: string\n  /** Additional comment about the outgoing comm. */\n  comment?: string\n  /** Names/Keywords of the policies that necessitated the email. */\n  policies?: string[]\n  /** Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense */\n  severityLevel?: string\n  /** Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n  /** Indicates whether the email was successfully delivered to the user's inbox. */\n  isDelivered?: boolean\n}\n\nconst hashModEventEmail = 'modEventEmail'\n\nexport function isModEventEmail<V>(v: V) {\n  return is$typed(v, id, hashModEventEmail)\n}\n\nexport function validateModEventEmail<V>(v: V) {\n  return validate<ModEventEmail & V>(v, id, hashModEventEmail)\n}\n\n/** Divert a record's blobs to a 3rd party service for further scanning/tagging */\nexport interface ModEventDivert {\n  $type?: 'tools.ozone.moderation.defs#modEventDivert'\n  comment?: string\n}\n\nconst hashModEventDivert = 'modEventDivert'\n\nexport function isModEventDivert<V>(v: V) {\n  return is$typed(v, id, hashModEventDivert)\n}\n\nexport function validateModEventDivert<V>(v: V) {\n  return validate<ModEventDivert & V>(v, id, hashModEventDivert)\n}\n\n/** Add/Remove a tag on a subject */\nexport interface ModEventTag {\n  $type?: 'tools.ozone.moderation.defs#modEventTag'\n  /** Tags to be added to the subject. If already exists, won't be duplicated. */\n  add: string[]\n  /** Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. */\n  remove: string[]\n  /** Additional comment about added/removed tags. */\n  comment?: string\n}\n\nconst hashModEventTag = 'modEventTag'\n\nexport function isModEventTag<V>(v: V) {\n  return is$typed(v, id, hashModEventTag)\n}\n\nexport function validateModEventTag<V>(v: V) {\n  return validate<ModEventTag & V>(v, id, hashModEventTag)\n}\n\n/** Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface AccountEvent {\n  $type?: 'tools.ozone.moderation.defs#accountEvent'\n  comment?: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  status?:\n    | 'unknown'\n    | 'deactivated'\n    | 'deleted'\n    | 'takendown'\n    | 'suspended'\n    | 'tombstoned'\n    | (string & {})\n  timestamp: string\n}\n\nconst hashAccountEvent = 'accountEvent'\n\nexport function isAccountEvent<V>(v: V) {\n  return is$typed(v, id, hashAccountEvent)\n}\n\nexport function validateAccountEvent<V>(v: V) {\n  return validate<AccountEvent & V>(v, id, hashAccountEvent)\n}\n\n/** Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface IdentityEvent {\n  $type?: 'tools.ozone.moderation.defs#identityEvent'\n  comment?: string\n  handle?: string\n  pdsHost?: string\n  tombstone?: boolean\n  timestamp: string\n}\n\nconst hashIdentityEvent = 'identityEvent'\n\nexport function isIdentityEvent<V>(v: V) {\n  return is$typed(v, id, hashIdentityEvent)\n}\n\nexport function validateIdentityEvent<V>(v: V) {\n  return validate<IdentityEvent & V>(v, id, hashIdentityEvent)\n}\n\n/** Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */\nexport interface RecordEvent {\n  $type?: 'tools.ozone.moderation.defs#recordEvent'\n  comment?: string\n  op: 'create' | 'update' | 'delete' | (string & {})\n  cid?: string\n  timestamp: string\n}\n\nconst hashRecordEvent = 'recordEvent'\n\nexport function isRecordEvent<V>(v: V) {\n  return is$typed(v, id, hashRecordEvent)\n}\n\nexport function validateRecordEvent<V>(v: V) {\n  return validate<RecordEvent & V>(v, id, hashRecordEvent)\n}\n\n/** Logs a scheduled takedown action for an account. */\nexport interface ScheduleTakedownEvent {\n  $type?: 'tools.ozone.moderation.defs#scheduleTakedownEvent'\n  comment?: string\n  executeAt?: string\n  executeAfter?: string\n  executeUntil?: string\n}\n\nconst hashScheduleTakedownEvent = 'scheduleTakedownEvent'\n\nexport function isScheduleTakedownEvent<V>(v: V) {\n  return is$typed(v, id, hashScheduleTakedownEvent)\n}\n\nexport function validateScheduleTakedownEvent<V>(v: V) {\n  return validate<ScheduleTakedownEvent & V>(v, id, hashScheduleTakedownEvent)\n}\n\n/** Logs cancellation of a scheduled takedown action for an account. */\nexport interface CancelScheduledTakedownEvent {\n  $type?: 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n  comment?: string\n}\n\nconst hashCancelScheduledTakedownEvent = 'cancelScheduledTakedownEvent'\n\nexport function isCancelScheduledTakedownEvent<V>(v: V) {\n  return is$typed(v, id, hashCancelScheduledTakedownEvent)\n}\n\nexport function validateCancelScheduledTakedownEvent<V>(v: V) {\n  return validate<CancelScheduledTakedownEvent & V>(\n    v,\n    id,\n    hashCancelScheduledTakedownEvent,\n  )\n}\n\nexport interface RepoView {\n  $type?: 'tools.ozone.moderation.defs#repoView'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords: { [_ in string]: unknown }[]\n  indexedAt: string\n  moderation: Moderation\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invitesDisabled?: boolean\n  inviteNote?: string\n  deactivatedAt?: string\n  threatSignatures?: ComAtprotoAdminDefs.ThreatSignature[]\n}\n\nconst hashRepoView = 'repoView'\n\nexport function isRepoView<V>(v: V) {\n  return is$typed(v, id, hashRepoView)\n}\n\nexport function validateRepoView<V>(v: V) {\n  return validate<RepoView & V>(v, id, hashRepoView)\n}\n\nexport interface RepoViewDetail {\n  $type?: 'tools.ozone.moderation.defs#repoViewDetail'\n  did: string\n  handle: string\n  email?: string\n  relatedRecords: { [_ in string]: unknown }[]\n  indexedAt: string\n  moderation: ModerationDetail\n  labels?: ComAtprotoLabelDefs.Label[]\n  invitedBy?: ComAtprotoServerDefs.InviteCode\n  invites?: ComAtprotoServerDefs.InviteCode[]\n  invitesDisabled?: boolean\n  inviteNote?: string\n  emailConfirmedAt?: string\n  deactivatedAt?: string\n  threatSignatures?: ComAtprotoAdminDefs.ThreatSignature[]\n}\n\nconst hashRepoViewDetail = 'repoViewDetail'\n\nexport function isRepoViewDetail<V>(v: V) {\n  return is$typed(v, id, hashRepoViewDetail)\n}\n\nexport function validateRepoViewDetail<V>(v: V) {\n  return validate<RepoViewDetail & V>(v, id, hashRepoViewDetail)\n}\n\nexport interface RepoViewNotFound {\n  $type?: 'tools.ozone.moderation.defs#repoViewNotFound'\n  did: string\n}\n\nconst hashRepoViewNotFound = 'repoViewNotFound'\n\nexport function isRepoViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashRepoViewNotFound)\n}\n\nexport function validateRepoViewNotFound<V>(v: V) {\n  return validate<RepoViewNotFound & V>(v, id, hashRepoViewNotFound)\n}\n\nexport interface RecordView {\n  $type?: 'tools.ozone.moderation.defs#recordView'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n  blobCids: string[]\n  indexedAt: string\n  moderation: Moderation\n  repo: RepoView\n}\n\nconst hashRecordView = 'recordView'\n\nexport function isRecordView<V>(v: V) {\n  return is$typed(v, id, hashRecordView)\n}\n\nexport function validateRecordView<V>(v: V) {\n  return validate<RecordView & V>(v, id, hashRecordView)\n}\n\nexport interface RecordViewDetail {\n  $type?: 'tools.ozone.moderation.defs#recordViewDetail'\n  uri: string\n  cid: string\n  value: { [_ in string]: unknown }\n  blobs: BlobView[]\n  labels?: ComAtprotoLabelDefs.Label[]\n  indexedAt: string\n  moderation: ModerationDetail\n  repo: RepoView\n}\n\nconst hashRecordViewDetail = 'recordViewDetail'\n\nexport function isRecordViewDetail<V>(v: V) {\n  return is$typed(v, id, hashRecordViewDetail)\n}\n\nexport function validateRecordViewDetail<V>(v: V) {\n  return validate<RecordViewDetail & V>(v, id, hashRecordViewDetail)\n}\n\nexport interface RecordViewNotFound {\n  $type?: 'tools.ozone.moderation.defs#recordViewNotFound'\n  uri: string\n}\n\nconst hashRecordViewNotFound = 'recordViewNotFound'\n\nexport function isRecordViewNotFound<V>(v: V) {\n  return is$typed(v, id, hashRecordViewNotFound)\n}\n\nexport function validateRecordViewNotFound<V>(v: V) {\n  return validate<RecordViewNotFound & V>(v, id, hashRecordViewNotFound)\n}\n\nexport interface Moderation {\n  $type?: 'tools.ozone.moderation.defs#moderation'\n  subjectStatus?: SubjectStatusView\n}\n\nconst hashModeration = 'moderation'\n\nexport function isModeration<V>(v: V) {\n  return is$typed(v, id, hashModeration)\n}\n\nexport function validateModeration<V>(v: V) {\n  return validate<Moderation & V>(v, id, hashModeration)\n}\n\nexport interface ModerationDetail {\n  $type?: 'tools.ozone.moderation.defs#moderationDetail'\n  subjectStatus?: SubjectStatusView\n}\n\nconst hashModerationDetail = 'moderationDetail'\n\nexport function isModerationDetail<V>(v: V) {\n  return is$typed(v, id, hashModerationDetail)\n}\n\nexport function validateModerationDetail<V>(v: V) {\n  return validate<ModerationDetail & V>(v, id, hashModerationDetail)\n}\n\nexport interface BlobView {\n  $type?: 'tools.ozone.moderation.defs#blobView'\n  cid: string\n  mimeType: string\n  size: number\n  createdAt: string\n  details?: $Typed<ImageDetails> | $Typed<VideoDetails> | { $type: string }\n  moderation?: Moderation\n}\n\nconst hashBlobView = 'blobView'\n\nexport function isBlobView<V>(v: V) {\n  return is$typed(v, id, hashBlobView)\n}\n\nexport function validateBlobView<V>(v: V) {\n  return validate<BlobView & V>(v, id, hashBlobView)\n}\n\nexport interface ImageDetails {\n  $type?: 'tools.ozone.moderation.defs#imageDetails'\n  width: number\n  height: number\n}\n\nconst hashImageDetails = 'imageDetails'\n\nexport function isImageDetails<V>(v: V) {\n  return is$typed(v, id, hashImageDetails)\n}\n\nexport function validateImageDetails<V>(v: V) {\n  return validate<ImageDetails & V>(v, id, hashImageDetails)\n}\n\nexport interface VideoDetails {\n  $type?: 'tools.ozone.moderation.defs#videoDetails'\n  width: number\n  height: number\n  length: number\n}\n\nconst hashVideoDetails = 'videoDetails'\n\nexport function isVideoDetails<V>(v: V) {\n  return is$typed(v, id, hashVideoDetails)\n}\n\nexport function validateVideoDetails<V>(v: V) {\n  return validate<VideoDetails & V>(v, id, hashVideoDetails)\n}\n\nexport interface AccountHosting {\n  $type?: 'tools.ozone.moderation.defs#accountHosting'\n  status:\n    | 'takendown'\n    | 'suspended'\n    | 'deleted'\n    | 'deactivated'\n    | 'unknown'\n    | (string & {})\n  updatedAt?: string\n  createdAt?: string\n  deletedAt?: string\n  deactivatedAt?: string\n  reactivatedAt?: string\n}\n\nconst hashAccountHosting = 'accountHosting'\n\nexport function isAccountHosting<V>(v: V) {\n  return is$typed(v, id, hashAccountHosting)\n}\n\nexport function validateAccountHosting<V>(v: V) {\n  return validate<AccountHosting & V>(v, id, hashAccountHosting)\n}\n\nexport interface RecordHosting {\n  $type?: 'tools.ozone.moderation.defs#recordHosting'\n  status: 'deleted' | 'unknown' | (string & {})\n  updatedAt?: string\n  createdAt?: string\n  deletedAt?: string\n}\n\nconst hashRecordHosting = 'recordHosting'\n\nexport function isRecordHosting<V>(v: V) {\n  return is$typed(v, id, hashRecordHosting)\n}\n\nexport function validateRecordHosting<V>(v: V) {\n  return validate<RecordHosting & V>(v, id, hashRecordHosting)\n}\n\nexport interface ReporterStats {\n  $type?: 'tools.ozone.moderation.defs#reporterStats'\n  did: string\n  /** The total number of reports made by the user on accounts. */\n  accountReportCount: number\n  /** The total number of reports made by the user on records. */\n  recordReportCount: number\n  /** The total number of accounts reported by the user. */\n  reportedAccountCount: number\n  /** The total number of records reported by the user. */\n  reportedRecordCount: number\n  /** The total number of accounts taken down as a result of the user's reports. */\n  takendownAccountCount: number\n  /** The total number of records taken down as a result of the user's reports. */\n  takendownRecordCount: number\n  /** The total number of accounts labeled as a result of the user's reports. */\n  labeledAccountCount: number\n  /** The total number of records labeled as a result of the user's reports. */\n  labeledRecordCount: number\n}\n\nconst hashReporterStats = 'reporterStats'\n\nexport function isReporterStats<V>(v: V) {\n  return is$typed(v, id, hashReporterStats)\n}\n\nexport function validateReporterStats<V>(v: V) {\n  return validate<ReporterStats & V>(v, id, hashReporterStats)\n}\n\n/** Moderation tool information for tracing the source of the action */\nexport interface ModTool {\n  $type?: 'tools.ozone.moderation.defs#modTool'\n  /** Name/identifier of the source (e.g., 'automod', 'ozone/workspace') */\n  name: string\n  /** Additional arbitrary metadata about the source */\n  meta?: { [_ in string]: unknown }\n}\n\nconst hashModTool = 'modTool'\n\nexport function isModTool<V>(v: V) {\n  return is$typed(v, id, hashModTool)\n}\n\nexport function validateModTool<V>(v: V) {\n  return validate<ModTool & V>(v, id, hashModTool)\n}\n\n/** Moderation event timeline event for a PLC create operation */\nexport const TIMELINEEVENTPLCCREATE = `${id}#timelineEventPlcCreate`\n/** Moderation event timeline event for generic PLC operation */\nexport const TIMELINEEVENTPLCOPERATION = `${id}#timelineEventPlcOperation`\n/** Moderation event timeline event for a PLC tombstone operation */\nexport const TIMELINEEVENTPLCTOMBSTONE = `${id}#timelineEventPlcTombstone`\n\n/** View of a scheduled moderation action */\nexport interface ScheduledActionView {\n  $type?: 'tools.ozone.moderation.defs#scheduledActionView'\n  /** Auto-incrementing row ID */\n  id: number\n  /** Type of action to be executed */\n  action: 'takedown' | (string & {})\n  /** Serialized event object that will be propagated to the event when performed */\n  eventData?: { [_ in string]: unknown }\n  /** Subject DID for the action */\n  did: string\n  /** Exact time to execute the action */\n  executeAt?: string\n  /** Earliest time to execute the action (for randomized scheduling) */\n  executeAfter?: string\n  /** Latest time to execute the action (for randomized scheduling) */\n  executeUntil?: string\n  /** Whether execution time should be randomized within the specified range */\n  randomizeExecution?: boolean\n  /** DID of the user who created this scheduled action */\n  createdBy: string\n  /** When the scheduled action was created */\n  createdAt: string\n  /** When the scheduled action was last updated */\n  updatedAt?: string\n  /** Current status of the scheduled action */\n  status: 'pending' | 'executed' | 'cancelled' | 'failed' | (string & {})\n  /** When the action was last attempted to be executed */\n  lastExecutedAt?: string\n  /** Reason for the last execution failure */\n  lastFailureReason?: string\n  /** ID of the moderation event created when action was successfully executed */\n  executionEventId?: number\n}\n\nconst hashScheduledActionView = 'scheduledActionView'\n\nexport function isScheduledActionView<V>(v: V) {\n  return is$typed(v, id, hashScheduledActionView)\n}\n\nexport function validateScheduledActionView<V>(v: V) {\n  return validate<ScheduledActionView & V>(v, id, hashScheduledActionView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/emitEvent.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.emitEvent'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  event:\n    | $Typed<ToolsOzoneModerationDefs.ModEventTakedown>\n    | $Typed<ToolsOzoneModerationDefs.ModEventAcknowledge>\n    | $Typed<ToolsOzoneModerationDefs.ModEventEscalate>\n    | $Typed<ToolsOzoneModerationDefs.ModEventComment>\n    | $Typed<ToolsOzoneModerationDefs.ModEventLabel>\n    | $Typed<ToolsOzoneModerationDefs.ModEventReport>\n    | $Typed<ToolsOzoneModerationDefs.ModEventMute>\n    | $Typed<ToolsOzoneModerationDefs.ModEventUnmute>\n    | $Typed<ToolsOzoneModerationDefs.ModEventMuteReporter>\n    | $Typed<ToolsOzoneModerationDefs.ModEventUnmuteReporter>\n    | $Typed<ToolsOzoneModerationDefs.ModEventReverseTakedown>\n    | $Typed<ToolsOzoneModerationDefs.ModEventResolveAppeal>\n    | $Typed<ToolsOzoneModerationDefs.ModEventEmail>\n    | $Typed<ToolsOzoneModerationDefs.ModEventDivert>\n    | $Typed<ToolsOzoneModerationDefs.ModEventTag>\n    | $Typed<ToolsOzoneModerationDefs.AccountEvent>\n    | $Typed<ToolsOzoneModerationDefs.IdentityEvent>\n    | $Typed<ToolsOzoneModerationDefs.RecordEvent>\n    | $Typed<ToolsOzoneModerationDefs.ModEventPriorityScore>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssuranceEvent>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssuranceOverrideEvent>\n    | $Typed<ToolsOzoneModerationDefs.AgeAssurancePurgeEvent>\n    | $Typed<ToolsOzoneModerationDefs.RevokeAccountCredentialsEvent>\n    | $Typed<ToolsOzoneModerationDefs.ScheduleTakedownEvent>\n    | $Typed<ToolsOzoneModerationDefs.CancelScheduledTakedownEvent>\n    | { $type: string }\n  subject:\n    | $Typed<ComAtprotoAdminDefs.RepoRef>\n    | $Typed<ComAtprotoRepoStrongRef.Main>\n    | { $type: string }\n  subjectBlobCids?: string[]\n  createdBy: string\n  modTool?: ToolsOzoneModerationDefs.ModTool\n  /** An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject. */\n  externalId?: string\n}\n\nexport type OutputSchema = ToolsOzoneModerationDefs.ModEventView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SubjectHasAction' | 'DuplicateExternalId'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getAccountTimeline.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getAccountTimeline'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  timeline: TimelineItem[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface TimelineItem {\n  $type?: 'tools.ozone.moderation.getAccountTimeline#timelineItem'\n  day: string\n  summary: TimelineItemSummary[]\n}\n\nconst hashTimelineItem = 'timelineItem'\n\nexport function isTimelineItem<V>(v: V) {\n  return is$typed(v, id, hashTimelineItem)\n}\n\nexport function validateTimelineItem<V>(v: V) {\n  return validate<TimelineItem & V>(v, id, hashTimelineItem)\n}\n\nexport interface TimelineItemSummary {\n  $type?: 'tools.ozone.moderation.getAccountTimeline#timelineItemSummary'\n  eventSubjectType: 'account' | 'record' | 'chat' | (string & {})\n  eventType:\n    | 'tools.ozone.moderation.defs#modEventTakedown'\n    | 'tools.ozone.moderation.defs#modEventReverseTakedown'\n    | 'tools.ozone.moderation.defs#modEventComment'\n    | 'tools.ozone.moderation.defs#modEventReport'\n    | 'tools.ozone.moderation.defs#modEventLabel'\n    | 'tools.ozone.moderation.defs#modEventAcknowledge'\n    | 'tools.ozone.moderation.defs#modEventEscalate'\n    | 'tools.ozone.moderation.defs#modEventMute'\n    | 'tools.ozone.moderation.defs#modEventUnmute'\n    | 'tools.ozone.moderation.defs#modEventMuteReporter'\n    | 'tools.ozone.moderation.defs#modEventUnmuteReporter'\n    | 'tools.ozone.moderation.defs#modEventEmail'\n    | 'tools.ozone.moderation.defs#modEventResolveAppeal'\n    | 'tools.ozone.moderation.defs#modEventDivert'\n    | 'tools.ozone.moderation.defs#modEventTag'\n    | 'tools.ozone.moderation.defs#accountEvent'\n    | 'tools.ozone.moderation.defs#identityEvent'\n    | 'tools.ozone.moderation.defs#recordEvent'\n    | 'tools.ozone.moderation.defs#modEventPriorityScore'\n    | 'tools.ozone.moderation.defs#revokeAccountCredentialsEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceEvent'\n    | 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'\n    | 'tools.ozone.moderation.defs#timelineEventPlcCreate'\n    | 'tools.ozone.moderation.defs#timelineEventPlcOperation'\n    | 'tools.ozone.moderation.defs#timelineEventPlcTombstone'\n    | 'tools.ozone.hosting.getAccountHistory#accountCreated'\n    | 'tools.ozone.hosting.getAccountHistory#emailConfirmed'\n    | 'tools.ozone.hosting.getAccountHistory#passwordUpdated'\n    | 'tools.ozone.hosting.getAccountHistory#handleUpdated'\n    | 'tools.ozone.moderation.defs#scheduleTakedownEvent'\n    | 'tools.ozone.moderation.defs#cancelScheduledTakedownEvent'\n    | (string & {})\n  count: number\n}\n\nconst hashTimelineItemSummary = 'timelineItemSummary'\n\nexport function isTimelineItemSummary<V>(v: V) {\n  return is$typed(v, id, hashTimelineItemSummary)\n}\n\nexport function validateTimelineItemSummary<V>(v: V) {\n  return validate<TimelineItemSummary & V>(v, id, hashTimelineItemSummary)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getEvent.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getEvent'\n\nexport type QueryParams = {\n  id: number\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.ModEventViewDetail\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getRecord.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRecord'\n\nexport type QueryParams = {\n  uri: string\n  cid?: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.RecordViewDetail\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RecordNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getRecords.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRecords'\n\nexport type QueryParams = {\n  uris: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  records: (\n    | $Typed<ToolsOzoneModerationDefs.RecordViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RecordViewNotFound>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getRepo.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRepo'\n\nexport type QueryParams = {\n  did: string\n}\nexport type InputSchema = undefined\nexport type OutputSchema = ToolsOzoneModerationDefs.RepoViewDetail\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RepoNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getReporterStats.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getReporterStats'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  stats: ToolsOzoneModerationDefs.ReporterStats[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getRepos'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  repos: (\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n  )[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/getSubjects.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.getSubjects'\n\nexport type QueryParams = {\n  subjects: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  subjects: ToolsOzoneModerationDefs.SubjectView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/listScheduledActions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.listScheduledActions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Filter actions scheduled to execute after this time */\n  startsAfter?: string\n  /** Filter actions scheduled to execute before this time */\n  endsBefore?: string\n  /** Filter actions for specific DID subjects */\n  subjects?: string[]\n  /** Filter actions by status */\n  statuses: ('pending' | 'executed' | 'cancelled' | 'failed' | (string & {}))[]\n  /** Maximum number of results to return */\n  limit: number\n  /** Cursor for pagination */\n  cursor?: string\n}\n\nexport interface OutputSchema {\n  actions: ToolsOzoneModerationDefs.ScheduledActionView[]\n  /** Cursor for next page of results */\n  cursor?: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/queryEvents.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.queryEvents'\n\nexport type QueryParams = {\n  /** The types of events (fully qualified string in the format of tools.ozone.moderation.defs#modEvent<name>) to filter by. If not specified, all events are returned. */\n  types?: string[]\n  createdBy?: string\n  /** Sort direction for the events. Defaults to descending order of created at timestamp. */\n  sortDirection: 'asc' | 'desc'\n  /** Retrieve events created after a given timestamp */\n  createdAfter?: string\n  /** Retrieve events created before a given timestamp */\n  createdBefore?: string\n  subject?: string\n  /** If specified, only events where the subject belongs to the given collections will be returned. When subjectType is set to 'account', this will be ignored. */\n  collections?: string[]\n  /** If specified, only events where the subject is of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */\n  subjectType?: 'account' | 'record' | (string & {})\n  /** If true, events on all record types (posts, lists, profile etc.) or records from given 'collections' param, owned by the did are returned. */\n  includeAllUserRecords: boolean\n  limit: number\n  /** If true, only events with comments are returned */\n  hasComment?: boolean\n  /** If specified, only events with comments containing the keyword are returned. Apply || separator to use multiple keywords and match using OR condition. */\n  comment?: string\n  /** If specified, only events where all of these labels were added are returned */\n  addedLabels?: string[]\n  /** If specified, only events where all of these labels were removed are returned */\n  removedLabels?: string[]\n  /** If specified, only events where all of these tags were added are returned */\n  addedTags?: string[]\n  /** If specified, only events where all of these tags were removed are returned */\n  removedTags?: string[]\n  reportTypes?: string[]\n  policies?: string[]\n  /** If specified, only events where the modTool name matches any of the given values are returned */\n  modTool?: string[]\n  /** If specified, only events where the batchId matches the given value are returned */\n  batchId?: string\n  /** If specified, only events where the age assurance state matches the given value are returned */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n  /** If specified, only events where strikeCount value is set are returned. */\n  withStrike?: boolean\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  events: ToolsOzoneModerationDefs.ModEventView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.queryStatuses'\n\nexport type QueryParams = {\n  /** Number of queues being used by moderators. Subjects will be split among all queues. */\n  queueCount?: number\n  /** Index of the queue to fetch subjects from. Works only when queueCount value is specified. */\n  queueIndex?: number\n  /** A seeder to shuffle/balance the queue items. */\n  queueSeed?: string\n  /** All subjects, or subjects from given 'collections' param, belonging to the account specified in the 'subject' param will be returned. */\n  includeAllUserRecords?: boolean\n  /** The subject to get the status for. */\n  subject?: string\n  /** Search subjects by keyword from comments */\n  comment?: string\n  /** Search subjects reported after a given timestamp */\n  reportedAfter?: string\n  /** Search subjects reported before a given timestamp */\n  reportedBefore?: string\n  /** Search subjects reviewed after a given timestamp */\n  reviewedAfter?: string\n  /** Search subjects where the associated record/account was deleted after a given timestamp */\n  hostingDeletedAfter?: string\n  /** Search subjects where the associated record/account was deleted before a given timestamp */\n  hostingDeletedBefore?: string\n  /** Search subjects where the associated record/account was updated after a given timestamp */\n  hostingUpdatedAfter?: string\n  /** Search subjects where the associated record/account was updated before a given timestamp */\n  hostingUpdatedBefore?: string\n  /** Search subjects by the status of the associated record/account */\n  hostingStatuses?: string[]\n  /** Search subjects reviewed before a given timestamp */\n  reviewedBefore?: string\n  /** By default, we don't include muted subjects in the results. Set this to true to include them. */\n  includeMuted?: boolean\n  /** When set to true, only muted subjects and reporters will be returned. */\n  onlyMuted?: boolean\n  /** Specify when fetching subjects in a certain state */\n  reviewState?:\n    | 'tools.ozone.moderation.defs#reviewOpen'\n    | 'tools.ozone.moderation.defs#reviewClosed'\n    | 'tools.ozone.moderation.defs#reviewEscalated'\n    | 'tools.ozone.moderation.defs#reviewNone'\n    | (string & {})\n  ignoreSubjects?: string[]\n  /** Get all subject statuses that were reviewed by a specific moderator */\n  lastReviewedBy?: string\n  sortField:\n    | 'lastReviewedAt'\n    | 'lastReportedAt'\n    | 'reportedRecordsCount'\n    | 'takendownRecordsCount'\n    | 'priorityScore'\n  sortDirection: 'asc' | 'desc'\n  /** Get subjects that were taken down */\n  takendown?: boolean\n  /** Get subjects in unresolved appealed status */\n  appealed?: boolean\n  limit: number\n  tags?: string[]\n  excludeTags?: string[]\n  cursor?: string\n  /** If specified, subjects belonging to the given collections will be returned. When subjectType is set to 'account', this will be ignored. */\n  collections?: string[]\n  /** If specified, subjects of the given type (account or record) will be returned. When this is set to 'account' the 'collections' parameter will be ignored. When includeAllUserRecords or subject is set, this will be ignored. */\n  subjectType?: 'account' | 'record' | (string & {})\n  /** If specified, only subjects that belong to an account that has at least this many suspensions will be returned. */\n  minAccountSuspendCount?: number\n  /** If specified, only subjects that belong to an account that has at least this many reported records will be returned. */\n  minReportedRecordsCount?: number\n  /** If specified, only subjects that belong to an account that has at least this many taken down records will be returned. */\n  minTakendownRecordsCount?: number\n  /** If specified, only subjects that have priority score value above the given value will be returned. */\n  minPriorityScore?: number\n  /** If specified, only subjects that belong to an account that has at least this many active strikes will be returned. */\n  minStrikeCount?: number\n  /** If specified, only subjects with the given age assurance state will be returned. */\n  ageAssuranceState?:\n    | 'pending'\n    | 'assured'\n    | 'unknown'\n    | 'reset'\n    | 'blocked'\n    | (string & {})\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  subjectStatuses: ToolsOzoneModerationDefs.SubjectStatusView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/scheduleAction.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.scheduleAction'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  action: $Typed<Takedown> | { $type: string }\n  /** Array of DID subjects to schedule the action for */\n  subjects: string[]\n  createdBy: string\n  scheduling: SchedulingConfig\n  modTool?: ToolsOzoneModerationDefs.ModTool\n}\n\nexport type OutputSchema = ScheduledActionResults\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Schedule a takedown action */\nexport interface Takedown {\n  $type?: 'tools.ozone.moderation.scheduleAction#takedown'\n  comment?: string\n  /** Indicates how long the takedown should be in effect before automatically expiring. */\n  durationInHours?: number\n  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */\n  acknowledgeAccountSubjects?: boolean\n  /** Names/Keywords of the policies that drove the decision. */\n  policies?: string[]\n  /** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */\n  severityLevel?: string\n  /** Number of strikes to assign to the user when takedown is applied. */\n  strikeCount?: number\n  /** When the strike should expire. If not provided, the strike never expires. */\n  strikeExpiresAt?: string\n  /** Email content to be sent to the user upon takedown. */\n  emailContent?: string\n  /** Subject of the email to be sent to the user upon takedown. */\n  emailSubject?: string\n}\n\nconst hashTakedown = 'takedown'\n\nexport function isTakedown<V>(v: V) {\n  return is$typed(v, id, hashTakedown)\n}\n\nexport function validateTakedown<V>(v: V) {\n  return validate<Takedown & V>(v, id, hashTakedown)\n}\n\n/** Configuration for when the action should be executed */\nexport interface SchedulingConfig {\n  $type?: 'tools.ozone.moderation.scheduleAction#schedulingConfig'\n  /** Exact time to execute the action */\n  executeAt?: string\n  /** Earliest time to execute the action (for randomized scheduling) */\n  executeAfter?: string\n  /** Latest time to execute the action (for randomized scheduling) */\n  executeUntil?: string\n}\n\nconst hashSchedulingConfig = 'schedulingConfig'\n\nexport function isSchedulingConfig<V>(v: V) {\n  return is$typed(v, id, hashSchedulingConfig)\n}\n\nexport function validateSchedulingConfig<V>(v: V) {\n  return validate<SchedulingConfig & V>(v, id, hashSchedulingConfig)\n}\n\nexport interface ScheduledActionResults {\n  $type?: 'tools.ozone.moderation.scheduleAction#scheduledActionResults'\n  succeeded: string[]\n  failed: FailedScheduling[]\n}\n\nconst hashScheduledActionResults = 'scheduledActionResults'\n\nexport function isScheduledActionResults<V>(v: V) {\n  return is$typed(v, id, hashScheduledActionResults)\n}\n\nexport function validateScheduledActionResults<V>(v: V) {\n  return validate<ScheduledActionResults & V>(v, id, hashScheduledActionResults)\n}\n\nexport interface FailedScheduling {\n  $type?: 'tools.ozone.moderation.scheduleAction#failedScheduling'\n  subject: string\n  error: string\n  errorCode?: string\n}\n\nconst hashFailedScheduling = 'failedScheduling'\n\nexport function isFailedScheduling<V>(v: V) {\n  return is$typed(v, id, hashFailedScheduling)\n}\n\nexport function validateFailedScheduling<V>(v: V) {\n  return validate<FailedScheduling & V>(v, id, hashFailedScheduling)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/moderation/searchRepos.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.moderation.searchRepos'\n\nexport type QueryParams = {\n  /** DEPRECATED: use 'q' instead */\n  term?: string\n  q?: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  repos: ToolsOzoneModerationDefs.RepoView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/report/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.report.defs'\n\nexport type ReasonType =\n  | 'tools.ozone.report.defs#reasonAppeal'\n  | 'tools.ozone.report.defs#reasonOther'\n  | 'tools.ozone.report.defs#reasonViolenceAnimal'\n  | 'tools.ozone.report.defs#reasonViolenceThreats'\n  | 'tools.ozone.report.defs#reasonViolenceGraphicContent'\n  | 'tools.ozone.report.defs#reasonViolenceGlorification'\n  | 'tools.ozone.report.defs#reasonViolenceExtremistContent'\n  | 'tools.ozone.report.defs#reasonViolenceTrafficking'\n  | 'tools.ozone.report.defs#reasonViolenceOther'\n  | 'tools.ozone.report.defs#reasonSexualAbuseContent'\n  | 'tools.ozone.report.defs#reasonSexualNCII'\n  | 'tools.ozone.report.defs#reasonSexualDeepfake'\n  | 'tools.ozone.report.defs#reasonSexualAnimal'\n  | 'tools.ozone.report.defs#reasonSexualUnlabeled'\n  | 'tools.ozone.report.defs#reasonSexualOther'\n  | 'tools.ozone.report.defs#reasonChildSafetyCSAM'\n  | 'tools.ozone.report.defs#reasonChildSafetyGroom'\n  | 'tools.ozone.report.defs#reasonChildSafetyPrivacy'\n  | 'tools.ozone.report.defs#reasonChildSafetyHarassment'\n  | 'tools.ozone.report.defs#reasonChildSafetyOther'\n  | 'tools.ozone.report.defs#reasonHarassmentTroll'\n  | 'tools.ozone.report.defs#reasonHarassmentTargeted'\n  | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'\n  | 'tools.ozone.report.defs#reasonHarassmentDoxxing'\n  | 'tools.ozone.report.defs#reasonHarassmentOther'\n  | 'tools.ozone.report.defs#reasonMisleadingBot'\n  | 'tools.ozone.report.defs#reasonMisleadingImpersonation'\n  | 'tools.ozone.report.defs#reasonMisleadingSpam'\n  | 'tools.ozone.report.defs#reasonMisleadingScam'\n  | 'tools.ozone.report.defs#reasonMisleadingElections'\n  | 'tools.ozone.report.defs#reasonMisleadingOther'\n  | 'tools.ozone.report.defs#reasonRuleSiteSecurity'\n  | 'tools.ozone.report.defs#reasonRuleProhibitedSales'\n  | 'tools.ozone.report.defs#reasonRuleBanEvasion'\n  | 'tools.ozone.report.defs#reasonRuleOther'\n  | 'tools.ozone.report.defs#reasonSelfHarmContent'\n  | 'tools.ozone.report.defs#reasonSelfHarmED'\n  | 'tools.ozone.report.defs#reasonSelfHarmStunts'\n  | 'tools.ozone.report.defs#reasonSelfHarmSubstances'\n  | 'tools.ozone.report.defs#reasonSelfHarmOther'\n  | (string & {})\n\n/** Appeal a previously taken moderation action */\nexport const REASONAPPEAL = `${id}#reasonAppeal`\n/** An issue not included in these options */\nexport const REASONOTHER = `${id}#reasonOther`\n/** Animal welfare violations */\nexport const REASONVIOLENCEANIMAL = `${id}#reasonViolenceAnimal`\n/** Threats or incitement */\nexport const REASONVIOLENCETHREATS = `${id}#reasonViolenceThreats`\n/** Graphic violent content */\nexport const REASONVIOLENCEGRAPHICCONTENT = `${id}#reasonViolenceGraphicContent`\n/** Glorification of violence */\nexport const REASONVIOLENCEGLORIFICATION = `${id}#reasonViolenceGlorification`\n/** Extremist content. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONVIOLENCEEXTREMISTCONTENT = `${id}#reasonViolenceExtremistContent`\n/** Human trafficking */\nexport const REASONVIOLENCETRAFFICKING = `${id}#reasonViolenceTrafficking`\n/** Other violent content */\nexport const REASONVIOLENCEOTHER = `${id}#reasonViolenceOther`\n/** Adult sexual abuse content */\nexport const REASONSEXUALABUSECONTENT = `${id}#reasonSexualAbuseContent`\n/** Non-consensual intimate imagery */\nexport const REASONSEXUALNCII = `${id}#reasonSexualNCII`\n/** Deepfake adult content */\nexport const REASONSEXUALDEEPFAKE = `${id}#reasonSexualDeepfake`\n/** Animal sexual abuse */\nexport const REASONSEXUALANIMAL = `${id}#reasonSexualAnimal`\n/** Unlabelled adult content */\nexport const REASONSEXUALUNLABELED = `${id}#reasonSexualUnlabeled`\n/** Other sexual violence content */\nexport const REASONSEXUALOTHER = `${id}#reasonSexualOther`\n/** Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYCSAM = `${id}#reasonChildSafetyCSAM`\n/** Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYGROOM = `${id}#reasonChildSafetyGroom`\n/** Privacy violation involving a minor */\nexport const REASONCHILDSAFETYPRIVACY = `${id}#reasonChildSafetyPrivacy`\n/** Harassment or bullying of minors */\nexport const REASONCHILDSAFETYHARASSMENT = `${id}#reasonChildSafetyHarassment`\n/** Other child safety. These reports will be sent only be sent to the application's Moderation Authority. */\nexport const REASONCHILDSAFETYOTHER = `${id}#reasonChildSafetyOther`\n/** Trolling */\nexport const REASONHARASSMENTTROLL = `${id}#reasonHarassmentTroll`\n/** Targeted harassment */\nexport const REASONHARASSMENTTARGETED = `${id}#reasonHarassmentTargeted`\n/** Hate speech */\nexport const REASONHARASSMENTHATESPEECH = `${id}#reasonHarassmentHateSpeech`\n/** Doxxing */\nexport const REASONHARASSMENTDOXXING = `${id}#reasonHarassmentDoxxing`\n/** Other harassing or hateful content */\nexport const REASONHARASSMENTOTHER = `${id}#reasonHarassmentOther`\n/** Fake account or bot */\nexport const REASONMISLEADINGBOT = `${id}#reasonMisleadingBot`\n/** Impersonation */\nexport const REASONMISLEADINGIMPERSONATION = `${id}#reasonMisleadingImpersonation`\n/** Spam */\nexport const REASONMISLEADINGSPAM = `${id}#reasonMisleadingSpam`\n/** Scam */\nexport const REASONMISLEADINGSCAM = `${id}#reasonMisleadingScam`\n/** False information about elections */\nexport const REASONMISLEADINGELECTIONS = `${id}#reasonMisleadingElections`\n/** Other misleading content */\nexport const REASONMISLEADINGOTHER = `${id}#reasonMisleadingOther`\n/** Hacking or system attacks */\nexport const REASONRULESITESECURITY = `${id}#reasonRuleSiteSecurity`\n/** Promoting or selling prohibited items or services */\nexport const REASONRULEPROHIBITEDSALES = `${id}#reasonRuleProhibitedSales`\n/** Banned user returning */\nexport const REASONRULEBANEVASION = `${id}#reasonRuleBanEvasion`\n/** Other */\nexport const REASONRULEOTHER = `${id}#reasonRuleOther`\n/** Content promoting or depicting self-harm */\nexport const REASONSELFHARMCONTENT = `${id}#reasonSelfHarmContent`\n/** Eating disorders */\nexport const REASONSELFHARMED = `${id}#reasonSelfHarmED`\n/** Dangerous challenges or activities */\nexport const REASONSELFHARMSTUNTS = `${id}#reasonSelfHarmStunts`\n/** Dangerous substances or drug abuse */\nexport const REASONSELFHARMSUBSTANCES = `${id}#reasonSelfHarmSubstances`\n/** Other dangerous content */\nexport const REASONSELFHARMOTHER = `${id}#reasonSelfHarmOther`\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/safelink/addRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.addRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to apply the rule to */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  action: ToolsOzoneSafelinkDefs.ActionType\n  reason: ToolsOzoneSafelinkDefs.ReasonType\n  /** Optional comment about the decision */\n  comment?: string\n  /** Author DID. Only respected when using admin auth */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'InvalidUrl' | 'RuleAlreadyExists'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/safelink/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.defs'\n\n/** An event for URL safety decisions */\nexport interface Event {\n  $type?: 'tools.ozone.safelink.defs#event'\n  /** Auto-incrementing row ID */\n  id: number\n  eventType: EventType\n  /** The URL that this rule applies to */\n  url: string\n  pattern: PatternType\n  action: ActionType\n  reason: ReasonType\n  /** DID of the user who created this rule */\n  createdBy: string\n  createdAt: string\n  /** Optional comment about the decision */\n  comment?: string\n}\n\nconst hashEvent = 'event'\n\nexport function isEvent<V>(v: V) {\n  return is$typed(v, id, hashEvent)\n}\n\nexport function validateEvent<V>(v: V) {\n  return validate<Event & V>(v, id, hashEvent)\n}\n\nexport type EventType = 'addRule' | 'updateRule' | 'removeRule' | (string & {})\nexport type PatternType = 'domain' | 'url' | (string & {})\nexport type ActionType = 'block' | 'warn' | 'whitelist' | (string & {})\nexport type ReasonType = 'csam' | 'spam' | 'phishing' | 'none' | (string & {})\n\n/** Input for creating a URL safety rule */\nexport interface UrlRule {\n  $type?: 'tools.ozone.safelink.defs#urlRule'\n  /** The URL or domain to apply the rule to */\n  url: string\n  pattern: PatternType\n  action: ActionType\n  reason: ReasonType\n  /** Optional comment about the decision */\n  comment?: string\n  /** DID of the user added the rule. */\n  createdBy: string\n  /** Timestamp when the rule was created */\n  createdAt: string\n  /** Timestamp when the rule was last updated */\n  updatedAt: string\n}\n\nconst hashUrlRule = 'urlRule'\n\nexport function isUrlRule<V>(v: V) {\n  return is$typed(v, id, hashUrlRule)\n}\n\nexport function validateUrlRule<V>(v: V) {\n  return validate<UrlRule & V>(v, id, hashUrlRule)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/safelink/queryEvents.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.queryEvents'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Cursor for pagination */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit: number\n  /** Filter by specific URLs or domains */\n  urls?: string[]\n  /** Filter by pattern type */\n  patternType?: string\n  /** Sort direction */\n  sortDirection: 'asc' | 'desc' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** Next cursor for pagination. Only present if there are more results. */\n  cursor?: string\n  events: ToolsOzoneSafelinkDefs.Event[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/safelink/queryRules.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.queryRules'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Cursor for pagination */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit: number\n  /** Filter by specific URLs or domains */\n  urls?: string[]\n  /** Filter by pattern type */\n  patternType?: string\n  /** Filter by action types */\n  actions?: string[]\n  /** Filter by reason type */\n  reason?: string\n  /** Filter by rule creator */\n  createdBy?: string\n  /** Sort direction */\n  sortDirection: 'asc' | 'desc' | (string & {})\n}\n\nexport interface OutputSchema {\n  /** Next cursor for pagination. Only present if there are more results. */\n  cursor?: string\n  rules: ToolsOzoneSafelinkDefs.UrlRule[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/safelink/removeRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.removeRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to remove the rule for */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  /** Optional comment about why the rule is being removed */\n  comment?: string\n  /** Optional DID of the user. Only respected when using admin auth. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RuleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/safelink/updateRule.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSafelinkDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.safelink.updateRule'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** The URL or domain to update the rule for */\n  url: string\n  pattern: ToolsOzoneSafelinkDefs.PatternType\n  action: ToolsOzoneSafelinkDefs.ActionType\n  reason: ToolsOzoneSafelinkDefs.ReasonType\n  /** Optional comment about the update */\n  comment?: string\n  /** Optional DID to credit as the creator. Only respected for admin_token authentication. */\n  createdBy?: string\n}\n\nexport type OutputSchema = ToolsOzoneSafelinkDefs.Event\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'RuleNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/server/getConfig.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.server.getConfig'\n\nexport type QueryParams = {}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  appview?: ServiceConfig\n  pds?: ServiceConfig\n  blobDivert?: ServiceConfig\n  chat?: ServiceConfig\n  viewer?: ViewerConfig\n  /** The did of the verifier used for verification. */\n  verifierDid?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface ServiceConfig {\n  $type?: 'tools.ozone.server.getConfig#serviceConfig'\n  url?: string\n}\n\nconst hashServiceConfig = 'serviceConfig'\n\nexport function isServiceConfig<V>(v: V) {\n  return is$typed(v, id, hashServiceConfig)\n}\n\nexport function validateServiceConfig<V>(v: V) {\n  return validate<ServiceConfig & V>(v, id, hashServiceConfig)\n}\n\nexport interface ViewerConfig {\n  $type?: 'tools.ozone.server.getConfig#viewerConfig'\n  role?:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n}\n\nconst hashViewerConfig = 'viewerConfig'\n\nexport function isViewerConfig<V>(v: V) {\n  return is$typed(v, id, hashViewerConfig)\n}\n\nexport function validateViewerConfig<V>(v: V) {\n  return validate<ViewerConfig & V>(v, id, hashViewerConfig)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/addValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.addValues'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to add values to */\n  name: string\n  /** Array of string values to add to the set */\n  values: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.defs'\n\nexport interface Set {\n  $type?: 'tools.ozone.set.defs#set'\n  name: string\n  description?: string\n}\n\nconst hashSet = 'set'\n\nexport function isSet<V>(v: V) {\n  return is$typed(v, id, hashSet)\n}\n\nexport function validateSet<V>(v: V) {\n  return validate<Set & V>(v, id, hashSet)\n}\n\nexport interface SetView {\n  $type?: 'tools.ozone.set.defs#setView'\n  name: string\n  description?: string\n  setSize: number\n  createdAt: string\n  updatedAt: string\n}\n\nconst hashSetView = 'setView'\n\nexport function isSetView<V>(v: V) {\n  return is$typed(v, id, hashSetView)\n}\n\nexport function validateSetView<V>(v: V) {\n  return validate<SetView & V>(v, id, hashSetView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/deleteSet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.deleteSet'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to delete */\n  name: string\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SetNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/deleteValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.deleteValues'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Name of the set to delete values from */\n  name: string\n  /** Array of string values to delete from the set */\n  values: string[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SetNotFound'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/getValues.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.getValues'\n\nexport type QueryParams = {\n  name: string\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  set: ToolsOzoneSetDefs.SetView\n  values: string[]\n  cursor?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'SetNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/querySets.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.querySets'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  namePrefix?: string\n  sortBy: 'name' | 'createdAt' | 'updatedAt'\n  /** Defaults to ascending order of name field. */\n  sortDirection: 'asc' | 'desc'\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  sets: ToolsOzoneSetDefs.SetView[]\n  cursor?: string\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/set/upsertSet.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSetDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.set.upsertSet'\n\nexport type QueryParams = {}\nexport type InputSchema = ToolsOzoneSetDefs.Set\nexport type OutputSchema = ToolsOzoneSetDefs.SetView\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/setting/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.defs'\n\nexport interface Option {\n  $type?: 'tools.ozone.setting.defs#option'\n  key: string\n  did: string\n  value: { [_ in string]: unknown }\n  description?: string\n  createdAt?: string\n  updatedAt?: string\n  managerRole?:\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n  scope: 'instance' | 'personal' | (string & {})\n  createdBy: string\n  lastUpdatedBy: string\n}\n\nconst hashOption = 'option'\n\nexport function isOption<V>(v: V) {\n  return is$typed(v, id, hashOption)\n}\n\nexport function validateOption<V>(v: V) {\n  return validate<Option & V>(v, id, hashOption)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/setting/listOptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSettingDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.listOptions'\n\nexport type QueryParams = {\n  limit: number\n  cursor?: string\n  scope: 'instance' | 'personal' | (string & {})\n  /** Filter keys by prefix */\n  prefix?: string\n  /** Filter for only the specified keys. Ignored if prefix is provided */\n  keys?: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  options: ToolsOzoneSettingDefs.Option[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/setting/removeOptions.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.removeOptions'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  keys: string[]\n  scope: 'instance' | 'personal' | (string & {})\n}\n\nexport interface OutputSchema {}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/setting/upsertOption.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSettingDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.setting.upsertOption'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  key: string\n  scope: 'instance' | 'personal' | (string & {})\n  value: { [_ in string]: unknown }\n  description?: string\n  managerRole?:\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleAdmin'\n    | (string & {})\n}\n\nexport interface OutputSchema {\n  option: ToolsOzoneSettingDefs.Option\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/signature/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.defs'\n\nexport interface SigDetail {\n  $type?: 'tools.ozone.signature.defs#sigDetail'\n  property: string\n  value: string\n}\n\nconst hashSigDetail = 'sigDetail'\n\nexport function isSigDetail<V>(v: V) {\n  return is$typed(v, id, hashSigDetail)\n}\n\nexport function validateSigDetail<V>(v: V) {\n  return validate<SigDetail & V>(v, id, hashSigDetail)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/signature/findCorrelation.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneSignatureDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.findCorrelation'\n\nexport type QueryParams = {\n  dids: string[]\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  details: ToolsOzoneSignatureDefs.SigDetail[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/signature/findRelatedAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\nimport type * as ToolsOzoneSignatureDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.findRelatedAccounts'\n\nexport type QueryParams = {\n  did: string\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: RelatedAccount[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface RelatedAccount {\n  $type?: 'tools.ozone.signature.findRelatedAccounts#relatedAccount'\n  account: ComAtprotoAdminDefs.AccountView\n  similarities?: ToolsOzoneSignatureDefs.SigDetail[]\n}\n\nconst hashRelatedAccount = 'relatedAccount'\n\nexport function isRelatedAccount<V>(v: V) {\n  return is$typed(v, id, hashRelatedAccount)\n}\n\nexport function validateRelatedAccount<V>(v: V) {\n  return validate<RelatedAccount & V>(v, id, hashRelatedAccount)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/signature/searchAccounts.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ComAtprotoAdminDefs from '../../../com/atproto/admin/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.signature.searchAccounts'\n\nexport type QueryParams = {\n  values: string[]\n  cursor?: string\n  limit: number\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  accounts: ComAtprotoAdminDefs.AccountView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/team/addMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.addMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleTriage'\n    | (string & {})\n}\n\nexport type OutputSchema = ToolsOzoneTeamDefs.Member\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'MemberAlreadyExists'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/team/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.defs'\n\nexport interface Member {\n  $type?: 'tools.ozone.team.defs#member'\n  did: string\n  disabled?: boolean\n  profile?: AppBskyActorDefs.ProfileViewDetailed\n  createdAt?: string\n  updatedAt?: string\n  lastUpdatedBy?: string\n  role:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleTriage'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | (string & {})\n}\n\nconst hashMember = 'member'\n\nexport function isMember<V>(v: V) {\n  return is$typed(v, id, hashMember)\n}\n\nexport function validateMember<V>(v: V) {\n  return validate<Member & V>(v, id, hashMember)\n}\n\n/** Admin role. Highest level of access, can perform all actions. */\nexport const ROLEADMIN = `${id}#roleAdmin`\n/** Moderator role. Can perform most actions. */\nexport const ROLEMODERATOR = `${id}#roleModerator`\n/** Triage role. Mostly intended for monitoring and escalating issues. */\nexport const ROLETRIAGE = `${id}#roleTriage`\n/** Verifier role. Only allowed to issue verifications. */\nexport const ROLEVERIFIER = `${id}#roleVerifier`\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/team/deleteMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.deleteMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'MemberNotFound' | 'CannotDeleteSelf'\n}\n\nexport type HandlerOutput = HandlerError | void\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/team/listMembers.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.listMembers'\n\nexport type QueryParams = {\n  q?: string\n  disabled?: boolean\n  roles?: string[]\n  limit: number\n  cursor?: string\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  members: ToolsOzoneTeamDefs.Member[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/team/updateMember.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneTeamDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.team.updateMember'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  did: string\n  disabled?: boolean\n  role?:\n    | 'tools.ozone.team.defs#roleAdmin'\n    | 'tools.ozone.team.defs#roleModerator'\n    | 'tools.ozone.team.defs#roleVerifier'\n    | 'tools.ozone.team.defs#roleTriage'\n    | (string & {})\n}\n\nexport type OutputSchema = ToolsOzoneTeamDefs.Member\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n  error?: 'MemberNotFound'\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/verification/defs.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneModerationDefs from '../moderation/defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.defs'\n\n/** Verification data for the associated subject. */\nexport interface VerificationView {\n  $type?: 'tools.ozone.verification.defs#verificationView'\n  /** The user who issued this verification. */\n  issuer: string\n  /** The AT-URI of the verification record. */\n  uri: string\n  /** The subject of the verification. */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying. */\n  displayName: string\n  /** Timestamp when the verification was created. */\n  createdAt: string\n  /** Describes the reason for revocation, also indicating that the verification is no longer valid. */\n  revokeReason?: string\n  /** Timestamp when the verification was revoked. */\n  revokedAt?: string\n  /** The user who revoked this verification. */\n  revokedBy?: string\n  subjectProfile?: { $type: string }\n  issuerProfile?: { $type: string }\n  subjectRepo?:\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n  issuerRepo?:\n    | $Typed<ToolsOzoneModerationDefs.RepoViewDetail>\n    | $Typed<ToolsOzoneModerationDefs.RepoViewNotFound>\n    | { $type: string }\n}\n\nconst hashVerificationView = 'verificationView'\n\nexport function isVerificationView<V>(v: V) {\n  return is$typed(v, id, hashVerificationView)\n}\n\nexport function validateVerificationView<V>(v: V) {\n  return validate<VerificationView & V>(v, id, hashVerificationView)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/verification/grantVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneVerificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.grantVerifications'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of verification requests to process */\n  verifications: VerificationInput[]\n}\n\nexport interface OutputSchema {\n  verifications: ToolsOzoneVerificationDefs.VerificationView[]\n  failedVerifications: GrantError[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\nexport interface VerificationInput {\n  $type?: 'tools.ozone.verification.grantVerifications#verificationInput'\n  /** The did of the subject being verified */\n  subject: string\n  /** Handle of the subject the verification applies to at the moment of verifying. */\n  handle: string\n  /** Display name of the subject the verification applies to at the moment of verifying. */\n  displayName: string\n  /** Timestamp for verification record. Defaults to current time when not specified. */\n  createdAt?: string\n}\n\nconst hashVerificationInput = 'verificationInput'\n\nexport function isVerificationInput<V>(v: V) {\n  return is$typed(v, id, hashVerificationInput)\n}\n\nexport function validateVerificationInput<V>(v: V) {\n  return validate<VerificationInput & V>(v, id, hashVerificationInput)\n}\n\n/** Error object for failed verifications. */\nexport interface GrantError {\n  $type?: 'tools.ozone.verification.grantVerifications#grantError'\n  /** Error message describing the reason for failure. */\n  error: string\n  /** The did of the subject being verified */\n  subject: string\n}\n\nconst hashGrantError = 'grantError'\n\nexport function isGrantError<V>(v: V) {\n  return is$typed(v, id, hashGrantError)\n}\n\nexport function validateGrantError<V>(v: V) {\n  return validate<GrantError & V>(v, id, hashGrantError)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/verification/listVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\nimport type * as ToolsOzoneVerificationDefs from './defs.js'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.listVerifications'\n\nexport type QueryParams = {\n  /** Pagination cursor */\n  cursor?: string\n  /** Maximum number of results to return */\n  limit: number\n  /** Filter to verifications created after this timestamp */\n  createdAfter?: string\n  /** Filter to verifications created before this timestamp */\n  createdBefore?: string\n  /** Filter to verifications from specific issuers */\n  issuers?: string[]\n  /** Filter to specific verified DIDs */\n  subjects?: string[]\n  /** Sort direction for creation date */\n  sortDirection: 'asc' | 'desc'\n  /** Filter to verifications that are revoked or not. By default, includes both. */\n  isRevoked?: boolean\n}\nexport type InputSchema = undefined\n\nexport interface OutputSchema {\n  cursor?: string\n  verifications: ToolsOzoneVerificationDefs.VerificationView[]\n}\n\nexport type HandlerInput = void\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n"
  },
  {
    "path": "packages/pds/src/lexicon/types/tools/ozone/verification/revokeVerifications.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\nimport { type ValidationResult, BlobRef } from '@atproto/lexicon'\nimport { CID } from 'multiformats/cid'\nimport { validate as _validate } from '../../../../lexicons'\nimport {\n  type $Typed,\n  is$typed as _is$typed,\n  type OmitKey,\n} from '../../../../util'\n\nconst is$typed = _is$typed,\n  validate = _validate\nconst id = 'tools.ozone.verification.revokeVerifications'\n\nexport type QueryParams = {}\n\nexport interface InputSchema {\n  /** Array of verification record uris to revoke */\n  uris: string[]\n  /** Reason for revoking the verification. This is optional and can be omitted if not needed. */\n  revokeReason?: string\n}\n\nexport interface OutputSchema {\n  /** List of verification uris successfully revoked */\n  revokedVerifications: string[]\n  /** List of verification uris that couldn't be revoked, including failure reasons */\n  failedRevocations: RevokeError[]\n}\n\nexport interface HandlerInput {\n  encoding: 'application/json'\n  body: InputSchema\n}\n\nexport interface HandlerSuccess {\n  encoding: 'application/json'\n  body: OutputSchema\n  headers?: { [key: string]: string }\n}\n\nexport interface HandlerError {\n  status: number\n  message?: string\n}\n\nexport type HandlerOutput = HandlerError | HandlerSuccess\n\n/** Error object for failed revocations */\nexport interface RevokeError {\n  $type?: 'tools.ozone.verification.revokeVerifications#revokeError'\n  /** The AT-URI of the verification record that failed to revoke. */\n  uri: string\n  /** Description of the error that occurred during revocation. */\n  error: string\n}\n\nconst hashRevokeError = 'revokeError'\n\nexport function isRevokeError<V>(v: V) {\n  return is$typed(v, id, hashRevokeError)\n}\n\nexport function validateRevokeError<V>(v: V) {\n  return validate<RevokeError & V>(v, id, hashRevokeError)\n}\n"
  },
  {
    "path": "packages/pds/src/lexicon/util.ts",
    "content": "/**\n * GENERATED CODE - DO NOT MODIFY\n */\n\nimport { type ValidationResult } from '@atproto/lexicon'\n\nexport type OmitKey<T, K extends keyof T> = {\n  [K2 in keyof T as K2 extends K ? never : K2]: T[K2]\n}\n\nexport type $Typed<V, T extends string = string> = V & { $type: T }\nexport type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>\n\nexport type $Type<Id extends string, Hash extends string> = Hash extends 'main'\n  ? Id\n  : `${Id}#${Hash}`\n\nfunction isObject<V>(v: V): v is V & object {\n  return v != null && typeof v === 'object'\n}\n\nfunction is$type<Id extends string, Hash extends string>(\n  $type: unknown,\n  id: Id,\n  hash: Hash,\n): $type is $Type<Id, Hash> {\n  return hash === 'main'\n    ? $type === id\n    : // $type === `${id}#${hash}`\n      typeof $type === 'string' &&\n        $type.length === id.length + 1 + hash.length &&\n        $type.charCodeAt(id.length) === 35 /* '#' */ &&\n        $type.startsWith(id) &&\n        $type.endsWith(hash)\n}\n\nexport type $TypedObject<\n  V,\n  Id extends string,\n  Hash extends string,\n> = V extends {\n  $type: $Type<Id, Hash>\n}\n  ? V\n  : V extends { $type?: string }\n    ? V extends { $type?: infer T extends $Type<Id, Hash> }\n      ? V & { $type: T }\n      : never\n    : V & { $type: $Type<Id, Hash> }\n\nexport function is$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is $TypedObject<V, Id, Hash> {\n  return isObject(v) && '$type' in v && is$type(v.$type, id, hash)\n}\n\nexport function maybe$typed<V, Id extends string, Hash extends string>(\n  v: V,\n  id: Id,\n  hash: Hash,\n): v is V & object & { $type?: $Type<Id, Hash> } {\n  return (\n    isObject(v) &&\n    ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)\n  )\n}\n\nexport type Validator<R = unknown> = (v: unknown) => ValidationResult<R>\nexport type ValidatorParam<V extends Validator> =\n  V extends Validator<infer R> ? R : never\n\n/**\n * Utility function that allows to convert a \"validate*\" utility function into a\n * type predicate.\n */\nexport function asPredicate<V extends Validator>(validate: V) {\n  return function <T>(v: T): v is T & ValidatorParam<V> {\n    return validate(v).success\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/logger.ts",
    "content": "import { type IncomingMessage } from 'node:http'\nimport { stdSerializers } from 'pino'\nimport { pinoHttp } from 'pino-http'\nimport { obfuscateHeaders, subsystemLogger } from '@atproto/common'\n\nexport const blobStoreLogger = subsystemLogger('pds:blob-store')\nexport const dbLogger = subsystemLogger('pds:db')\nexport const didCacheLogger = subsystemLogger('pds:did-cache')\nexport const readStickyLogger = subsystemLogger('pds:read-sticky')\nexport const redisLogger = subsystemLogger('pds:redis')\nexport const seqLogger = subsystemLogger('pds:sequencer')\nexport const mailerLogger = subsystemLogger('pds:mailer')\nexport const labelerLogger = subsystemLogger('pds:labeler')\nexport const crawlerLogger = subsystemLogger('pds:crawler')\nexport const httpLogger = subsystemLogger('pds')\nexport const fetchLogger = subsystemLogger('pds:fetch')\nexport const oauthLogger = subsystemLogger('pds:oauth')\nexport const lexiconResolverLogger = subsystemLogger('pds:lexicon-resolver')\n\nexport const loggerMiddleware = pinoHttp({\n  logger: httpLogger,\n  serializers: {\n    req: reqSerializer,\n    err: (err: unknown) => ({\n      code: err?.['code'],\n      message: err?.['message'],\n    }),\n  },\n})\n\nexport function reqSerializer(req: IncomingMessage) {\n  const serialized = stdSerializers.req(req)\n  const headers = obfuscateHeaders(serialized.headers)\n  return { ...serialized, headers }\n}\n"
  },
  {
    "path": "packages/pds/src/mailer/index.ts",
    "content": "import { Transporter } from 'nodemailer'\nimport Mail from 'nodemailer/lib/mailer'\nimport SMTPTransport from 'nodemailer/lib/smtp-transport'\nimport { htmlToText } from 'nodemailer-html-to-text'\nimport { ServerConfig } from '../config'\nimport { mailerLogger } from '../logger'\nimport * as templates from './templates'\n\n// @TODO Add support for i18n\n\nexport class ServerMailer {\n  constructor(\n    public readonly transporter: Transporter<SMTPTransport.SentMessageInfo>,\n    private readonly config: ServerConfig,\n  ) {\n    transporter.use('compile', htmlToText())\n  }\n\n  // The returned config can be used inside email templates.\n  static getEmailConfig(_config: ServerConfig) {\n    return {}\n  }\n\n  async sendResetPassword(\n    params: { handle: string; token: string },\n    mailOpts: Mail.Options,\n  ) {\n    await this.sendTemplate('resetPassword', params, {\n      subject: 'Password Reset Requested',\n      ...mailOpts,\n    })\n  }\n\n  async sendAccountDelete(params: { token: string }, mailOpts: Mail.Options) {\n    await this.sendTemplate('deleteAccount', params, {\n      subject: 'Account Deletion Requested',\n      ...mailOpts,\n    })\n  }\n\n  async sendConfirmEmail(params: { token: string }, mailOpts: Mail.Options) {\n    await this.sendTemplate('confirmEmail', params, {\n      subject: 'Email Confirmation',\n      ...mailOpts,\n    })\n  }\n\n  async sendUpdateEmail(params: { token: string }, mailOpts: Mail.Options) {\n    await this.sendTemplate('updateEmail', params, {\n      subject: 'Email Update Requested',\n      ...mailOpts,\n    })\n  }\n\n  async sendPlcOperation(params: { token: string }, mailOpts: Mail.Options) {\n    await this.sendTemplate('plcOperation', params, {\n      subject: 'PLC Update Operation Requested',\n      ...mailOpts,\n    })\n  }\n\n  private async sendTemplate<K extends keyof typeof templates>(\n    templateName: K,\n    params: Parameters<(typeof templates)[K]>[0],\n    mailOpts: Mail.Options,\n  ) {\n    const html = templates[templateName]({\n      ...params,\n      config: ServerMailer.getEmailConfig(this.config),\n    } as any)\n    const res = await this.transporter.sendMail({\n      ...mailOpts,\n      from: mailOpts.from ?? this.config.email?.fromAddress,\n      html,\n    })\n    if (!this.config.email?.smtpUrl) {\n      mailerLogger.debug(\n        'No SMTP URL has been configured. Intended to send email:\\n' +\n          JSON.stringify(res, null, 2),\n      )\n    }\n    return res\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/mailer/moderation.ts",
    "content": "import { Transporter } from 'nodemailer'\nimport Mail from 'nodemailer/lib/mailer'\nimport SMTPTransport from 'nodemailer/lib/smtp-transport'\nimport { htmlToText } from 'nodemailer-html-to-text'\nimport { ServerConfig } from '../config'\nimport { mailerLogger } from '../logger'\n\nexport class ModerationMailer {\n  private config: ServerConfig\n  transporter: Transporter<SMTPTransport.SentMessageInfo>\n\n  constructor(\n    transporter: Transporter<SMTPTransport.SentMessageInfo>,\n    config: ServerConfig,\n  ) {\n    this.config = config\n    this.transporter = transporter\n    this.transporter.use('compile', htmlToText())\n  }\n\n  async send({ content }: { content: string }, mailOpts: Mail.Options) {\n    const mail = {\n      ...mailOpts,\n      html: content,\n      from: this.config.moderationEmail?.fromAddress,\n    }\n\n    const res = await this.transporter.sendMail(mail)\n\n    if (!this.config.moderationEmail?.smtpUrl) {\n      mailerLogger.debug(\n        'Moderation email auth is not configured. Intended to send email:\\n' +\n          JSON.stringify(res, null, 2),\n      )\n    }\n    return res\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/mailer/templates/confirm-email.d.ts",
    "content": "import { TemplateDelegate } from 'handlebars'\n\ndeclare const template: TemplateDelegate<{ token: string }>\nexport default template\n"
  },
  {
    "path": "packages/pds/src/mailer/templates/confirm-email.hbs",
    "content": "<html dir=\"ltr\" lang=\"en\">\n\n  <head>\n    <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n    <meta name=\"x-apple-disable-message-reformatting\" />\n    <title>Confirm your email</title>\n    <meta\n      name=\"description\"\n      content=\"To confirm your email, enter the code provided in the app.\"\n    />\n  </head>\n  <div\n    style=\"display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0\"\n  >To confirm your email, enter the code provided in the app.<div\n    > ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿</div>\n  </div>\n\n  <body\n    style=\"padding:12px;padding-bottom:40px;background-color:hsl(211, 20%, 95.3%)\"\n  >\n    <table\n      align=\"center\"\n      width=\"100%\"\n      border=\"0\"\n      cellPadding=\"0\"\n      cellSpacing=\"0\"\n      role=\"presentation\"\n      style=\"max-width:37.5em\"\n    >\n      <tbody>\n        <tr style=\"width:100%\">\n          <td>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding-top:24px;padding-bottom:24px\"\n            >\n              <tbody>\n                <tr>\n                  <td><img\n                      alt=\"Bluesky\"\n                      src=\"https://bsky.social/about/images/email/email_logo_default.png\"\n                      style=\"display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto\"\n                    /></td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding:24px;padding-bottom:16px;border-radius:12px;background-color:#FFFFFF\"\n            >\n              <tbody>\n                <tr>\n                  <td>\n                    <h1\n                      style=\"font-size:26px;letter-spacing:0.25px;color:#000000;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0\"\n                    >Confirm your email</h1>\n                    <p\n                      style=\"font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px;padding-right:32px\"\n                    >To confirm this email for your account, please enter the\n                      code below in the app or<!-- -->\n                      <a\n                        href=\"https://bsky.app/intent/verify-email?code={{token}}\"\n                        style=\"color:#067df7;text-decoration:none;text-decoration-line:underline;font-size:16px;letter-spacing:0.25px\"\n                        target=\"_blank\"\n                      >click here.</a></p><code\n                      style=\"display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase\"\n                    >{{token}}</code>\n                    <p\n                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px\"\n                    >If you didn&#x27;t request an email confirmation, you can\n                      safely ignore this email.</p>\n                    <table\n                      align=\"center\"\n                      width=\"100%\"\n                      border=\"0\"\n                      cellPadding=\"0\"\n                      cellSpacing=\"0\"\n                      role=\"presentation\"\n                      style=\"padding-top:24px\"\n                    >\n                      <tbody>\n                        <tr>\n                          <td>\n                            <hr\n                              style=\"width:100%;border:none;border-top:1px solid #eaeaea;margin:0\"\n                            />\n                            <table\n                              align=\"center\"\n                              width=\"100%\"\n                              border=\"0\"\n                              cellPadding=\"0\"\n                              cellSpacing=\"0\"\n                              role=\"presentation\"\n                              style=\"padding-top:16px;vertical-align:middle\"\n                            >\n                              <tbody style=\"width:100%\">\n                                <tr style=\"width:100%\">\n                                  <td data-id=\"__react-email-column\">\n                                    <p\n                                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px\"\n                                    ><a\n                                        href=\"https://bsky.app\"\n                                        style=\"color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px\"\n                                        target=\"_blank\"\n                                      >Bluesky</a>, the social internet</p>\n                                  </td>\n                                  <td\n                                    data-id=\"__react-email-column\"\n                                    style=\"width:24px\"\n                                  ><img\n                                      alt=\"🦋\"\n                                      src=\"https://bsky.social/about/images/email/email_mark_dark.png\"\n                                      style=\"display:block;outline:none;border:none;text-decoration:none;width:24px\"\n                                    /></td>\n                                </tr>\n                              </tbody>\n                            </table>\n                          </td>\n                        </tr>\n                      </tbody>\n                    </table>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"height:500px\"\n            >\n              <tbody>\n                <tr>\n                  <td></td>\n                </tr>\n              </tbody>\n            </table>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </body>\n\n</html>"
  },
  {
    "path": "packages/pds/src/mailer/templates/delete-account.d.ts",
    "content": "import { TemplateDelegate } from 'handlebars'\n\ndeclare const template: TemplateDelegate<{ token: string }>\nexport default template\n"
  },
  {
    "path": "packages/pds/src/mailer/templates/delete-account.hbs",
    "content": "<html dir=\"ltr\" lang=\"en\">\n\n  <head>\n    <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n    <meta name=\"x-apple-disable-message-reformatting\" />\n    <title>Delete your account</title>\n    <meta\n      name=\"description\"\n      content=\"To permanently delete your account, please enter the code provided in the app along with your password.\"\n    />\n  </head>\n  <div\n    style=\"display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0\"\n  >To permanently delete your account, please enter the code provided in the app\n    along with your password.<div\n    > ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿</div>\n  </div>\n\n  <body\n    style=\"padding:12px;padding-bottom:40px;background-color:hsl(211, 20%, 95.3%)\"\n  >\n    <table\n      align=\"center\"\n      width=\"100%\"\n      border=\"0\"\n      cellPadding=\"0\"\n      cellSpacing=\"0\"\n      role=\"presentation\"\n      style=\"max-width:37.5em\"\n    >\n      <tbody>\n        <tr style=\"width:100%\">\n          <td>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding-top:24px;padding-bottom:24px\"\n            >\n              <tbody>\n                <tr>\n                  <td><img\n                      alt=\"Bluesky\"\n                      src=\"https://bsky.social/about/images/email/email_logo_default.png\"\n                      style=\"display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto\"\n                    /></td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding:24px;padding-bottom:16px;border-radius:12px;background-color:#FFFFFF\"\n            >\n              <tbody>\n                <tr>\n                  <td>\n                    <h1\n                      style=\"font-size:26px;letter-spacing:0.25px;color:#000000;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0\"\n                    >Delete your account</h1>\n                    <p\n                      style=\"font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px;padding-right:32px\"\n                    ><span style=\"font-weight:600\">To permanently delete your\n                        account,</span>\n                      <!-- -->please enter the code below in the app along with\n                      your password.</p><code\n                      style=\"display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase\"\n                    >{{token}}</code>\n                    <p\n                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-right:32px\"\n                    >👉 If you didn&#x27;t request an account deletion,<!-- -->\n                      <span style=\"font-weight:600\">you should update your\n                        password immediately.</span></p>\n                    <table\n                      align=\"center\"\n                      width=\"100%\"\n                      border=\"0\"\n                      cellPadding=\"0\"\n                      cellSpacing=\"0\"\n                      role=\"presentation\"\n                      style=\"padding-top:24px\"\n                    >\n                      <tbody>\n                        <tr>\n                          <td>\n                            <hr\n                              style=\"width:100%;border:none;border-top:1px solid #eaeaea;margin:0\"\n                            />\n                            <table\n                              align=\"center\"\n                              width=\"100%\"\n                              border=\"0\"\n                              cellPadding=\"0\"\n                              cellSpacing=\"0\"\n                              role=\"presentation\"\n                              style=\"padding-top:16px;vertical-align:middle\"\n                            >\n                              <tbody style=\"width:100%\">\n                                <tr style=\"width:100%\">\n                                  <td data-id=\"__react-email-column\">\n                                    <p\n                                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px\"\n                                    ><a\n                                        href=\"https://bsky.app\"\n                                        style=\"color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px\"\n                                        target=\"_blank\"\n                                      >Bluesky</a>, the social internet</p>\n                                  </td>\n                                  <td\n                                    data-id=\"__react-email-column\"\n                                    style=\"width:24px\"\n                                  ><img\n                                      alt=\"🦋\"\n                                      src=\"https://bsky.social/about/images/email/email_mark_dark.png\"\n                                      style=\"display:block;outline:none;border:none;text-decoration:none;width:24px\"\n                                    /></td>\n                                </tr>\n                              </tbody>\n                            </table>\n                          </td>\n                        </tr>\n                      </tbody>\n                    </table>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"height:500px\"\n            >\n              <tbody>\n                <tr>\n                  <td></td>\n                </tr>\n              </tbody>\n            </table>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </body>\n\n</html>"
  },
  {
    "path": "packages/pds/src/mailer/templates/plc-operation.d.ts",
    "content": "import { TemplateDelegate } from 'handlebars'\n\ndeclare const template: TemplateDelegate<{ token: string }>\nexport default template\n"
  },
  {
    "path": "packages/pds/src/mailer/templates/plc-operation.hbs",
    "content": "<html dir=\"ltr\" lang=\"en\">\n\n  <head>\n    <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n    <meta name=\"x-apple-disable-message-reformatting\" />\n    <title>PLC update requested</title>\n    <meta\n      name=\"description\"\n      content=\"We received a request to update your PLC.\"\n    />\n  </head>\n  <div\n    style=\"display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0\"\n  >We received a request to update your PLC.<div\n    > ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿</div>\n  </div>\n\n  <body\n    style=\"padding:12px;padding-bottom:40px;background-color:hsl(211, 20%, 95.3%)\"\n  >\n    <table\n      align=\"center\"\n      width=\"100%\"\n      border=\"0\"\n      cellPadding=\"0\"\n      cellSpacing=\"0\"\n      role=\"presentation\"\n      style=\"max-width:37.5em\"\n    >\n      <tbody>\n        <tr style=\"width:100%\">\n          <td>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding-top:24px;padding-bottom:24px\"\n            >\n              <tbody>\n                <tr>\n                  <td><img\n                      alt=\"Bluesky\"\n                      src=\"https://bsky.social/about/images/email/email_logo_default.png\"\n                      style=\"display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto\"\n                    /></td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding:24px;padding-bottom:16px;border-radius:12px;background-color:#FFFFFF\"\n            >\n              <tbody>\n                <tr>\n                  <td>\n                    <h1\n                      style=\"font-size:26px;letter-spacing:0.25px;color:#000000;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0\"\n                    >PLC update requested</h1>\n                    <p\n                      style=\"font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px\"\n                    >We received a request to update your PLC identity. Your\n                      confirmation code is:</p><code\n                      style=\"display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase\"\n                    >{{token}}</code>\n                    <p\n                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px\"\n                    >Updating your PLC identity is a very sensitive operation.\n                      Please only proceed if you are confident in what you are\n                      doing.</p>\n                    <table\n                      align=\"center\"\n                      width=\"100%\"\n                      border=\"0\"\n                      cellPadding=\"0\"\n                      cellSpacing=\"0\"\n                      role=\"presentation\"\n                      style=\"padding-top:24px\"\n                    >\n                      <tbody>\n                        <tr>\n                          <td>\n                            <hr\n                              style=\"width:100%;border:none;border-top:1px solid #eaeaea;margin:0\"\n                            />\n                            <table\n                              align=\"center\"\n                              width=\"100%\"\n                              border=\"0\"\n                              cellPadding=\"0\"\n                              cellSpacing=\"0\"\n                              role=\"presentation\"\n                              style=\"padding-top:16px;vertical-align:middle\"\n                            >\n                              <tbody style=\"width:100%\">\n                                <tr style=\"width:100%\">\n                                  <td data-id=\"__react-email-column\">\n                                    <p\n                                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px\"\n                                    ><a\n                                        href=\"https://bsky.app\"\n                                        style=\"color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px\"\n                                        target=\"_blank\"\n                                      >Bluesky</a>, the social internet</p>\n                                  </td>\n                                  <td\n                                    data-id=\"__react-email-column\"\n                                    style=\"width:24px\"\n                                  ><img\n                                      alt=\"🦋\"\n                                      src=\"https://bsky.social/about/images/email/email_mark_dark.png\"\n                                      style=\"display:block;outline:none;border:none;text-decoration:none;width:24px\"\n                                    /></td>\n                                </tr>\n                              </tbody>\n                            </table>\n                          </td>\n                        </tr>\n                      </tbody>\n                    </table>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"height:500px\"\n            >\n              <tbody>\n                <tr>\n                  <td></td>\n                </tr>\n              </tbody>\n            </table>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </body>\n\n</html>"
  },
  {
    "path": "packages/pds/src/mailer/templates/reset-password.d.ts",
    "content": "import { TemplateDelegate } from 'handlebars'\n\ndeclare const template: TemplateDelegate<{ token: string; handle: string }>\nexport default template\n"
  },
  {
    "path": "packages/pds/src/mailer/templates/reset-password.hbs",
    "content": "<html dir=\"ltr\" lang=\"en\">\n\n  <head>\n    <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n    <meta name=\"x-apple-disable-message-reformatting\" />\n    <title>Reset password</title>\n    <meta\n      name=\"description\"\n      content=\"We received a request to reset the password for the account @{{handle}}.\"\n    />\n  </head>\n  <div\n    style=\"display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0\"\n  >We received a request to reset the password for the account @{{handle}}.<div\n    > ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿</div>\n  </div>\n\n  <body\n    style=\"padding:12px;padding-bottom:40px;background-color:hsl(211, 20%, 95.3%)\"\n  >\n    <table\n      align=\"center\"\n      width=\"100%\"\n      border=\"0\"\n      cellPadding=\"0\"\n      cellSpacing=\"0\"\n      role=\"presentation\"\n      style=\"max-width:37.5em\"\n    >\n      <tbody>\n        <tr style=\"width:100%\">\n          <td>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding-top:24px;padding-bottom:24px\"\n            >\n              <tbody>\n                <tr>\n                  <td><img\n                      alt=\"Bluesky\"\n                      src=\"https://bsky.social/about/images/email/email_logo_default.png\"\n                      style=\"display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto\"\n                    /></td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding:24px;padding-bottom:16px;border-radius:12px;background-color:#FFFFFF\"\n            >\n              <tbody>\n                <tr>\n                  <td>\n                    <h1\n                      style=\"font-size:26px;letter-spacing:0.25px;color:#000000;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0\"\n                    >Reset password</h1>\n                    <p\n                      style=\"font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px\"\n                    >We received a request to reset the password for the account<!-- -->\n                      <span\n                        style=\"color:hsl(211, 99%, 53%)\"\n                      >@<!-- -->{{handle}}<!-- -->.</span></p><code\n                      style=\"display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase\"\n                    >{{token}}</code>\n                    <p\n                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px\"\n                    >To choose a new password, please enter the code above in\n                      the app along with your new password.</p>\n                    <table\n                      align=\"center\"\n                      width=\"100%\"\n                      border=\"0\"\n                      cellPadding=\"0\"\n                      cellSpacing=\"0\"\n                      role=\"presentation\"\n                      style=\"padding-top:24px\"\n                    >\n                      <tbody>\n                        <tr>\n                          <td>\n                            <hr\n                              style=\"width:100%;border:none;border-top:1px solid #eaeaea;margin:0\"\n                            />\n                            <table\n                              align=\"center\"\n                              width=\"100%\"\n                              border=\"0\"\n                              cellPadding=\"0\"\n                              cellSpacing=\"0\"\n                              role=\"presentation\"\n                              style=\"padding-top:16px;vertical-align:middle\"\n                            >\n                              <tbody style=\"width:100%\">\n                                <tr style=\"width:100%\">\n                                  <td data-id=\"__react-email-column\">\n                                    <p\n                                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px\"\n                                    ><a\n                                        href=\"https://bsky.app\"\n                                        style=\"color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px\"\n                                        target=\"_blank\"\n                                      >Bluesky</a>, the social internet</p>\n                                  </td>\n                                  <td\n                                    data-id=\"__react-email-column\"\n                                    style=\"width:24px\"\n                                  ><img\n                                      alt=\"🦋\"\n                                      src=\"https://bsky.social/about/images/email/email_mark_dark.png\"\n                                      style=\"display:block;outline:none;border:none;text-decoration:none;width:24px\"\n                                    /></td>\n                                </tr>\n                              </tbody>\n                            </table>\n                          </td>\n                        </tr>\n                      </tbody>\n                    </table>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"height:500px\"\n            >\n              <tbody>\n                <tr>\n                  <td></td>\n                </tr>\n              </tbody>\n            </table>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </body>\n\n</html>"
  },
  {
    "path": "packages/pds/src/mailer/templates/update-email.d.ts",
    "content": "import { TemplateDelegate } from 'handlebars'\n\ndeclare const template: TemplateDelegate<{ token: string }>\nexport default template\n"
  },
  {
    "path": "packages/pds/src/mailer/templates/update-email.hbs",
    "content": "<html dir=\"ltr\" lang=\"en\">\n\n  <head>\n    <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n    <meta name=\"x-apple-disable-message-reformatting\" />\n    <title>Update your email</title>\n    <meta\n      name=\"description\"\n      content=\"To update the email for your account, enter the code provided in the app along with your new email.\"\n    />\n  </head>\n  <div\n    style=\"display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0\"\n  >To update the email for your account, enter the code provided in the app\n    along with your new email.<div\n    > ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿ ‌​‍‎‏﻿</div>\n  </div>\n\n  <body\n    style=\"padding:12px;padding-bottom:40px;background-color:hsl(211, 20%, 95.3%)\"\n  >\n    <table\n      align=\"center\"\n      width=\"100%\"\n      border=\"0\"\n      cellPadding=\"0\"\n      cellSpacing=\"0\"\n      role=\"presentation\"\n      style=\"max-width:37.5em\"\n    >\n      <tbody>\n        <tr style=\"width:100%\">\n          <td>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding-top:24px;padding-bottom:24px\"\n            >\n              <tbody>\n                <tr>\n                  <td><img\n                      alt=\"Bluesky\"\n                      src=\"https://bsky.social/about/images/email/email_logo_default.png\"\n                      style=\"display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto\"\n                    /></td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"padding:24px;padding-bottom:16px;border-radius:12px;background-color:#FFFFFF\"\n            >\n              <tbody>\n                <tr>\n                  <td>\n                    <h1\n                      style=\"font-size:26px;letter-spacing:0.25px;color:#000000;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0\"\n                    >Update your email</h1>\n                    <p\n                      style=\"font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px;padding-right:32px\"\n                    >To update the email for your account, enter the code below\n                      in the app along with your new email.</p><code\n                      style=\"display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase\"\n                    >{{token}}</code>\n                    <p\n                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px\"\n                    >If you didn&#x27;t request an email update, you can safely\n                      ignore this email.</p>\n                    <table\n                      align=\"center\"\n                      width=\"100%\"\n                      border=\"0\"\n                      cellPadding=\"0\"\n                      cellSpacing=\"0\"\n                      role=\"presentation\"\n                      style=\"padding-top:24px\"\n                    >\n                      <tbody>\n                        <tr>\n                          <td>\n                            <hr\n                              style=\"width:100%;border:none;border-top:1px solid #eaeaea;margin:0\"\n                            />\n                            <table\n                              align=\"center\"\n                              width=\"100%\"\n                              border=\"0\"\n                              cellPadding=\"0\"\n                              cellSpacing=\"0\"\n                              role=\"presentation\"\n                              style=\"padding-top:16px;vertical-align:middle\"\n                            >\n                              <tbody style=\"width:100%\">\n                                <tr style=\"width:100%\">\n                                  <td data-id=\"__react-email-column\">\n                                    <p\n                                      style=\"font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px\"\n                                    ><a\n                                        href=\"https://bsky.app\"\n                                        style=\"color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px\"\n                                        target=\"_blank\"\n                                      >Bluesky</a>, the social internet</p>\n                                  </td>\n                                  <td\n                                    data-id=\"__react-email-column\"\n                                    style=\"width:24px\"\n                                  ><img\n                                      alt=\"🦋\"\n                                      src=\"https://bsky.social/about/images/email/email_mark_dark.png\"\n                                      style=\"display:block;outline:none;border:none;text-decoration:none;width:24px\"\n                                    /></td>\n                                </tr>\n                              </tbody>\n                            </table>\n                          </td>\n                        </tr>\n                      </tbody>\n                    </table>\n                  </td>\n                </tr>\n              </tbody>\n            </table>\n            <table\n              align=\"center\"\n              width=\"100%\"\n              border=\"0\"\n              cellPadding=\"0\"\n              cellSpacing=\"0\"\n              role=\"presentation\"\n              style=\"height:500px\"\n            >\n              <tbody>\n                <tr>\n                  <td></td>\n                </tr>\n              </tbody>\n            </table>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </body>\n\n</html>"
  },
  {
    "path": "packages/pds/src/mailer/templates.ts",
    "content": "export { default as resetPassword } from './templates/reset-password'\nexport { default as deleteAccount } from './templates/delete-account'\nexport { default as confirmEmail } from './templates/confirm-email'\nexport { default as updateEmail } from './templates/update-email'\nexport { default as plcOperation } from './templates/plc-operation'\n"
  },
  {
    "path": "packages/pds/src/pipethrough.ts",
    "content": "import { IncomingHttpHeaders, ServerResponse } from 'node:http'\nimport { PassThrough, Readable, finished } from 'node:stream'\nimport { Request } from 'express'\nimport { Dispatcher } from 'undici'\nimport {\n  decodeStream,\n  getServiceEndpoint,\n  omit,\n  streamToNodeBuffer,\n} from '@atproto/common'\nimport { RpcPermissionMatch } from '@atproto/oauth-scopes'\nimport {\n  CatchallHandler,\n  HandlerPipeThroughBuffer,\n  HandlerPipeThroughStream,\n  InternalServerError,\n  InvalidRequestError,\n  ResponseType,\n  XRPCError as XRPCServerError,\n  excludeErrorResult,\n  parseReqNsid,\n} from '@atproto/xrpc-server'\nimport { buildProxiedContentEncoding } from '@atproto-labs/xrpc-utils'\nimport { isAccessPrivileged } from './auth-scope'\nimport { AppContext } from './context'\nimport { ids } from './lexicon/lexicons'\nimport { httpLogger } from './logger'\n\nexport const proxyHandler = (ctx: AppContext): CatchallHandler => {\n  const performAuth = ctx.authVerifier.authorization<RpcPermissionMatch>({\n    authorize: (permissions, { params }) => permissions.assertRpc(params),\n  })\n\n  return async (req, res, next) => {\n    // /!\\ Hot path\n    try {\n      if (\n        req.method !== 'GET' &&\n        req.method !== 'HEAD' &&\n        req.method !== 'POST'\n      ) {\n        throw new XRPCServerError(\n          ResponseType.InvalidRequest,\n          'XRPC requests only supports GET and POST',\n        )\n      }\n\n      const body = req.method === 'POST' ? req : undefined\n      if (body != null && !body.readable) {\n        // Body was already consumed by a previous middleware\n        throw new InternalServerError('Request body is not readable')\n      }\n\n      const lxm = parseReqNsid(req)\n      if (PROTECTED_METHODS.has(lxm)) {\n        throw new InvalidRequestError('Bad token method', 'InvalidToken')\n      }\n\n      const { url: origin, did: aud } = await parseProxyInfo(ctx, req, lxm)\n\n      const authResult = await performAuth({ req, res, params: { lxm, aud } })\n\n      const { credentials } = excludeErrorResult(authResult)\n\n      if (\n        credentials.type === 'access' &&\n        !isAccessPrivileged(credentials.scope) &&\n        PRIVILEGED_METHODS.has(lxm)\n      ) {\n        throw new InvalidRequestError('Bad token method', 'InvalidToken')\n      }\n\n      const headers: IncomingHttpHeaders = {\n        'accept-encoding': req.headers['accept-encoding'] || 'identity',\n        'accept-language': req.headers['accept-language'],\n        'atproto-accept-labelers': req.headers['atproto-accept-labelers'],\n        'x-bsky-topics': req.headers['x-bsky-topics'],\n\n        'content-type': body && req.headers['content-type'],\n        'content-encoding': body && req.headers['content-encoding'],\n        'content-length': body && req.headers['content-length'],\n\n        authorization: `Bearer ${await ctx.serviceAuthJwt(credentials.did, aud, lxm)}`,\n      }\n\n      const dispatchOptions: Dispatcher.RequestOptions = {\n        origin,\n        method: req.method,\n        path: req.originalUrl,\n        body,\n        headers,\n      }\n\n      await pipethroughStream(ctx, dispatchOptions, (upstream) => {\n        res.status(upstream.statusCode)\n\n        for (const [name, val] of responseHeaders(upstream.headers)) {\n          res.setHeader(name, val)\n        }\n\n        // Note that we should not need to manually handle errors here (e.g. by\n        // destroying the response), as the http server will handle them for us.\n        res.on('error', logResponseError)\n\n        // Tell undici to write the upstream response directly to the response\n        return res\n      })\n    } catch (err) {\n      next(err)\n    }\n  }\n}\n\nexport type PipethroughOptions = {\n  /**\n   * Specify the issuer (requester) for service auth. If not provided, no\n   * authorization headers will be added to the request.\n   */\n  iss?: string\n\n  /**\n   * Override the audience for service auth. If not provided, the audience will\n   * be determined based on the proxy service.\n   */\n  aud?: string\n\n  /**\n   * Override the lexicon method for service auth. If not provided, the lexicon\n   * method will be determined based on the request path.\n   */\n  lxm?: string\n}\n\nexport async function pipethrough(\n  ctx: AppContext,\n  req: Request,\n  options?: PipethroughOptions,\n): Promise<\n  HandlerPipeThroughStream & {\n    stream: Readable\n    headers: Record<string, string>\n    encoding: string\n  }\n> {\n  if (req.method !== 'GET' && req.method !== 'HEAD') {\n    // pipethrough() is used from within xrpcServer handlers, which means that\n    // the request body either has been parsed or is a readable stream that has\n    // been piped for decoding & size limiting. Because of this, forwarding the\n    // request body requires re-encoding it. Since we currently do not use\n    // pipethrough() with procedures, proxying of request body is not\n    // implemented.\n    throw new InternalServerError(\n      `Proxying of ${req.method} requests is not supported`,\n    )\n  }\n\n  const lxm = parseReqNsid(req)\n\n  const { url: origin, did: aud } = await parseProxyInfo(ctx, req, lxm)\n\n  const dispatchOptions: Dispatcher.RequestOptions = {\n    origin,\n    method: req.method,\n    path: req.originalUrl,\n    headers: {\n      'accept-language': req.headers['accept-language'],\n      'atproto-accept-labelers': req.headers['atproto-accept-labelers'],\n      'x-bsky-topics': req.headers['x-bsky-topics'],\n\n      // Because we sometimes need to interpret the response (e.g. during\n      // read-after-write, through asPipeThroughBuffer()), we need to ask the\n      // upstream server for an encoding that both the requester and the PDS can\n      // understand. Since we might have to do the decoding ourselves, we will\n      // use our own preferences (and weight) to negotiate the encoding.\n      'accept-encoding': buildProxiedContentEncoding(\n        req.headers['accept-encoding'],\n        ctx.cfg.proxy.preferCompressed,\n      ),\n\n      authorization: options?.iss\n        ? `Bearer ${await ctx.serviceAuthJwt(options.iss, options.aud ?? aud, options.lxm ?? lxm)}`\n        : undefined,\n    },\n\n    // Use a high water mark to buffer more data while performing async\n    // operations before this stream is consumed. This is especially useful\n    // while processing read-after-write operations.\n    highWaterMark: 2 * 65536, // twice the default (64KiB)\n  }\n\n  const { headers, body } = await pipethroughRequest(ctx, dispatchOptions)\n\n  return {\n    encoding: safeString(headers['content-type']) ?? 'application/json',\n    headers: Object.fromEntries(responseHeaders(headers)),\n    stream: body,\n  }\n}\n\n// Request setup/formatting\n// -------------------\n\nexport function computeProxyTo(\n  ctx: AppContext,\n  req: Request,\n  lxm: string,\n): string {\n  const proxyToHeader = req.header('atproto-proxy')\n  if (proxyToHeader) return proxyToHeader\n\n  const service = defaultService(ctx, lxm)\n  if (service.serviceInfo) {\n    return `${service.serviceInfo.did}#${service.serviceId}`\n  }\n\n  throw new InvalidRequestError(`No service configured for ${lxm}`)\n}\n\nexport async function parseProxyInfo(\n  ctx: AppContext,\n  req: Request,\n  lxm: string,\n): Promise<{ url: string; did: string }> {\n  // /!\\ Hot path\n\n  const proxyToHeader = req.header('atproto-proxy')\n  if (proxyToHeader) return parseProxyHeader(ctx, proxyToHeader)\n\n  const { serviceInfo } = defaultService(ctx, lxm)\n  if (serviceInfo) return serviceInfo\n\n  throw new InvalidRequestError(`No service configured for ${lxm}`)\n}\n\nexport const parseProxyHeader = async (\n  // Using subset of AppContext for testing purposes\n  ctx: Pick<AppContext, 'cfg' | 'idResolver'>,\n  proxyTo: string,\n): Promise<{ did: string; url: string }> => {\n  // /!\\ Hot path\n\n  const hashIndex = proxyTo.indexOf('#')\n\n  if (hashIndex === 0) {\n    throw new InvalidRequestError('no did specified in proxy header')\n  }\n\n  if (hashIndex === -1 || hashIndex === proxyTo.length - 1) {\n    throw new InvalidRequestError('no service id specified in proxy header')\n  }\n\n  // More than one hash\n  if (proxyTo.indexOf('#', hashIndex + 1) !== -1) {\n    throw new InvalidRequestError('invalid proxy header format')\n  }\n\n  // Basic validation\n  if (proxyTo.includes(' ')) {\n    throw new InvalidRequestError('proxy header cannot contain spaces')\n  }\n\n  const did = proxyTo.slice(0, hashIndex)\n\n  // Special case a configured appview, while still proxying correctly any other appview\n  if (\n    ctx.cfg.bskyAppView &&\n    proxyTo === `${ctx.cfg.bskyAppView.did}#bsky_appview`\n  ) {\n    return { did, url: ctx.cfg.bskyAppView.url }\n  }\n\n  const didDoc = await ctx.idResolver.did.resolve(did)\n  if (!didDoc) {\n    throw new InvalidRequestError('could not resolve proxy did')\n  }\n\n  const serviceId = proxyTo.slice(hashIndex)\n  const url = getServiceEndpoint(didDoc, { id: serviceId })\n  if (!url) {\n    throw new InvalidRequestError('could not resolve proxy did service url')\n  }\n\n  return { did, url }\n}\n\n/**\n * Utility function that wraps the undici stream() function and handles request\n * and response errors by wrapping them in XRPCError instances. This function is\n * more efficient than \"pipethroughRequest\" when a writable stream to pipe the\n * upstream response to is available.\n */\nasync function pipethroughStream(\n  ctx: AppContext,\n  dispatchOptions: Dispatcher.RequestOptions,\n  successStreamFactory: Dispatcher.StreamFactory,\n): Promise<void> {\n  return new Promise<void>((resolve, reject) => {\n    void ctx.proxyAgent\n      .stream(dispatchOptions, (upstream) => {\n        if (upstream.statusCode >= 400) {\n          const passThrough = new PassThrough()\n\n          void tryParsingError(upstream.headers, passThrough).then((parsed) => {\n            const xrpcError = new PipethroughUpstreamError(upstream, parsed, {\n              cause: dispatchOptions,\n            })\n\n            reject(xrpcError)\n          }, reject)\n\n          return passThrough\n        }\n\n        const writable = successStreamFactory(upstream)\n\n        // As soon as the control was passed to the writable stream (i.e. by\n        // returning the writable hereafter), pipethroughStream() is considered\n        // to have succeeded. Any error occurring while writing upstream data to\n        // the writable stream should be handled through the stream's error\n        // state (i.e. successStreamFactory() must ensure that error events on\n        // the returned writable will be handled).\n        resolve()\n\n        return writable\n      })\n      // The following catch block will be triggered with either network errors\n      // or writable stream errors. In the latter case, the promise will already\n      // be resolved, and reject()ing it there after will have no effect. Those\n      // error would still be logged by the successStreamFactory() function.\n      .catch(handleUpstreamRequestError)\n      .catch(reject)\n  })\n}\n\n/**\n * Utility function that wraps the undici request() function and handles request\n * and response errors by wrapping them in XRPCError instances.\n */\nasync function pipethroughRequest(\n  ctx: AppContext,\n  dispatchOptions: Dispatcher.RequestOptions,\n) {\n  // HandlerPipeThroughStream requires a readable stream to be returned, so we\n  // use the (less efficient) request() function instead.\n\n  const upstream = await ctx.proxyAgent\n    .request(dispatchOptions)\n    .catch(handleUpstreamRequestError)\n\n  if (upstream.statusCode >= 400) {\n    const parsed = await tryParsingError(upstream.headers, upstream.body)\n\n    throw new PipethroughUpstreamError(upstream, parsed, {\n      cause: dispatchOptions,\n    })\n  }\n\n  return upstream\n}\n\nfunction handleUpstreamRequestError(\n  err: unknown,\n  message = 'Upstream service unreachable',\n): never {\n  httpLogger.error({ err }, message)\n  throw new XRPCServerError(ResponseType.UpstreamFailure, message, undefined, {\n    cause: err,\n  })\n}\n\n// Request parsing/forwarding\n// -------------------\n\nexport function isJsonContentType(contentType?: string): boolean | undefined {\n  if (!contentType) return undefined\n  return /application\\/(?:\\w+\\+)?json/i.test(contentType)\n}\n\nasync function tryParsingError(\n  headers: IncomingHttpHeaders,\n  readable: Readable,\n): Promise<{ error?: string; message?: string }> {\n  if (isJsonContentType(headers['content-type']) === false) {\n    // We don't known how to parse non JSON content types so we can discard the\n    // whole response.\n\n    // Since we don't care about the response, we would normally just destroy\n    // the stream. However, if the underlying HTTP connection is an HTTP/1.1\n    // connection, this also destroys the underlying (keep-alive) TCP socket. In\n    // order to avoid destroying the TCP socket, while avoiding the cost of\n    // consuming too much IO, we give it a chance to finish first.\n\n    // @NOTE we need to listen (and ignore) \"error\" events, otherwise the\n    // process could crash (since we drain the stream asynchronously here). This\n    // is performed through the \"finished\" call below.\n\n    const to = setTimeout(() => {\n      readable.destroy()\n    }, 100)\n    finished(readable, (_err) => {\n      clearTimeout(to)\n    })\n    readable.resume()\n\n    return {}\n  }\n\n  try {\n    const buffer = await bufferUpstreamResponse(\n      readable,\n      headers['content-encoding'],\n    )\n\n    const errInfo: unknown = JSON.parse(buffer.toString('utf8'))\n    return {\n      error: safeString(errInfo?.['error']),\n      message: safeString(errInfo?.['message']),\n    }\n  } catch (err) {\n    // Failed to read, decode, buffer or parse. No big deal.\n    return {}\n  }\n}\n\nasync function bufferUpstreamResponse(\n  readable: Readable,\n  contentEncoding?: string | string[],\n): Promise<Buffer> {\n  try {\n    return await streamToNodeBuffer(decodeStream(readable, contentEncoding))\n  } catch (err) {\n    if (!readable.destroyed) readable.destroy()\n\n    throw new XRPCServerError(\n      ResponseType.UpstreamFailure,\n      err instanceof TypeError ? err.message : 'unable to decode request body',\n      undefined,\n      { cause: err },\n    )\n  }\n}\n\nexport async function asPipeThroughBuffer(\n  input: HandlerPipeThroughStream,\n): Promise<HandlerPipeThroughBuffer> {\n  return {\n    buffer: await bufferUpstreamResponse(\n      input.stream,\n      input.headers?.['content-encoding'],\n    ),\n    headers: omit(input.headers, ['content-encoding', 'content-length']),\n    encoding: input.encoding,\n  }\n}\n\n// Response parsing/forwarding\n// -------------------\n\nconst RES_HEADERS_TO_FORWARD = [\n  'atproto-repo-rev',\n  'atproto-content-labelers',\n  'retry-after',\n]\n\nfunction* responseHeaders(\n  headers: IncomingHttpHeaders,\n  includeContentHeaders = true,\n): Generator<[string, string]> {\n  if (includeContentHeaders) {\n    const length = headers['content-length']\n    if (length) yield ['content-length', length]\n\n    const encoding = headers['content-encoding']\n    if (encoding) yield ['content-encoding', encoding]\n\n    const type = headers['content-type']\n    if (type) yield ['content-type', type]\n\n    const language = headers['content-language']\n    if (language) yield ['content-language', language]\n  }\n\n  for (let i = 0; i < RES_HEADERS_TO_FORWARD.length; i++) {\n    const name = RES_HEADERS_TO_FORWARD[i]\n    const val = headers[name]\n\n    if (val != null) {\n      const value: string = Array.isArray(val) ? val.join(',') : val\n      yield [name, value]\n    }\n  }\n}\n\n// Utils\n// -------------------\n\n/**\n * Performs lexicon method matching on a set of methods,\n * taking into account that they are treated case-insensitively.\n */\nexport class LxmSet {\n  private inner: Set<string>\n  private original: Iterable<string>\n  constructor(items: Iterable<string>) {\n    this.inner = new Set(Array.from(items, normalizeLxm))\n    this.original = items\n  }\n  has(lxm: string) {\n    return this.inner.has(normalizeLxm(lxm))\n  }\n  *[Symbol.iterator](): Iterator<string> {\n    yield* this.original\n  }\n}\n\nexport function normalizeLxm(lxm: string) {\n  return lxm.toLowerCase()\n}\n\nexport const CHAT_BSKY_METHODS = new LxmSet([\n  ids.ChatBskyActorDeleteAccount,\n  ids.ChatBskyActorExportAccountData,\n  ids.ChatBskyConvoDeleteMessageForSelf,\n  ids.ChatBskyConvoGetConvo,\n  ids.ChatBskyConvoGetConvoForMembers,\n  ids.ChatBskyConvoGetLog,\n  ids.ChatBskyConvoGetMessages,\n  ids.ChatBskyConvoLeaveConvo,\n  ids.ChatBskyConvoListConvos,\n  ids.ChatBskyConvoMuteConvo,\n  ids.ChatBskyConvoSendMessage,\n  ids.ChatBskyConvoSendMessageBatch,\n  ids.ChatBskyConvoUnmuteConvo,\n  ids.ChatBskyConvoUpdateRead,\n])\n\nexport const PRIVILEGED_METHODS = new LxmSet([\n  ...CHAT_BSKY_METHODS,\n  ids.ComAtprotoServerCreateAccount,\n])\n\n// These endpoints are related to account management and must be used directly,\n// not proxied or service-authed. Service auth may be utilized between PDS and\n// entryway for these methods.\nexport const PROTECTED_METHODS = new LxmSet([\n  ids.ComAtprotoAdminSendEmail,\n  ids.ComAtprotoIdentityRequestPlcOperationSignature,\n  ids.ComAtprotoIdentitySignPlcOperation,\n  ids.ComAtprotoIdentityUpdateHandle,\n  ids.ComAtprotoServerActivateAccount,\n  ids.ComAtprotoServerConfirmEmail,\n  ids.ComAtprotoServerCreateAppPassword,\n  ids.ComAtprotoServerDeactivateAccount,\n  ids.ComAtprotoServerGetAccountInviteCodes,\n  ids.ComAtprotoServerGetSession,\n  ids.ComAtprotoServerListAppPasswords,\n  ids.ComAtprotoServerRequestAccountDelete,\n  ids.ComAtprotoServerRequestEmailConfirmation,\n  ids.ComAtprotoServerRequestEmailUpdate,\n  ids.ComAtprotoServerRevokeAppPassword,\n  ids.ComAtprotoServerUpdateEmail,\n])\n\nconst defaultService = (\n  ctx: AppContext,\n  nsid: string,\n): {\n  serviceId: string\n  serviceInfo: { url: string; did: string } | null\n} => {\n  switch (nsid) {\n    case ids.ToolsOzoneTeamAddMember:\n    case ids.ToolsOzoneTeamDeleteMember:\n    case ids.ToolsOzoneTeamUpdateMember:\n    case ids.ToolsOzoneTeamListMembers:\n    case ids.ToolsOzoneCommunicationCreateTemplate:\n    case ids.ToolsOzoneCommunicationDeleteTemplate:\n    case ids.ToolsOzoneCommunicationUpdateTemplate:\n    case ids.ToolsOzoneCommunicationListTemplates:\n    case ids.ToolsOzoneModerationEmitEvent:\n    case ids.ToolsOzoneModerationGetEvent:\n    case ids.ToolsOzoneModerationGetRecord:\n    case ids.ToolsOzoneModerationGetRepo:\n    case ids.ToolsOzoneModerationQueryEvents:\n    case ids.ToolsOzoneModerationQueryStatuses:\n    case ids.ToolsOzoneModerationSearchRepos:\n    case ids.ToolsOzoneModerationGetAccountTimeline:\n    case ids.ToolsOzoneVerificationListVerifications:\n    case ids.ToolsOzoneVerificationGrantVerifications:\n    case ids.ToolsOzoneVerificationRevokeVerifications:\n    case ids.ToolsOzoneSafelinkAddRule:\n    case ids.ToolsOzoneSafelinkUpdateRule:\n    case ids.ToolsOzoneSafelinkRemoveRule:\n    case ids.ToolsOzoneSafelinkQueryEvents:\n    case ids.ToolsOzoneSafelinkQueryRules:\n    case ids.ToolsOzoneModerationListScheduledActions:\n    case ids.ToolsOzoneModerationCancelScheduledActions:\n    case ids.ToolsOzoneModerationScheduleAction:\n      return {\n        serviceId: 'atproto_labeler',\n        serviceInfo: ctx.cfg.modService,\n      }\n    case ids.ComAtprotoModerationCreateReport:\n      return {\n        serviceId: 'atproto_labeler',\n        serviceInfo: ctx.cfg.reportService,\n      }\n    default:\n      return {\n        serviceId: 'bsky_appview',\n        serviceInfo: ctx.cfg.bskyAppView,\n      }\n  }\n}\n\nconst safeString = (str: unknown): string | undefined => {\n  return typeof str === 'string' ? str : undefined\n}\n\nfunction logResponseError(this: ServerResponse, err: unknown): void {\n  httpLogger.warn({ err }, 'error forwarding upstream response')\n}\n\nexport class PipethroughUpstreamError extends XRPCServerError {\n  constructor(\n    readonly upstream: {\n      statusCode: number\n      headers: IncomingHttpHeaders\n    },\n    payload: { message?: string; error?: string },\n    options?: ErrorOptions,\n  ) {\n    const status =\n      upstream.statusCode === 500\n        ? ResponseType.UpstreamFailure\n        : upstream.statusCode\n\n    super(status, payload.message, payload.error, options)\n  }\n\n  get headers(): Record<string, string> {\n    return Object.fromEntries(responseHeaders(this.upstream.headers, false))\n  }\n\n  get error() {\n    return this.customErrorName ?? this.typeName\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/read-after-write/index.ts",
    "content": "export * from './types'\nexport * from './util'\nexport * from './viewer'\n"
  },
  {
    "path": "packages/pds/src/read-after-write/types.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/syntax'\nimport { Headers } from '@atproto/xrpc'\nimport { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'\nimport { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post'\nimport { LocalViewer } from './viewer'\n\nexport type LocalRecords = {\n  count: number\n  profile: RecordDescript<ProfileRecord> | null\n  posts: RecordDescript<PostRecord>[]\n}\n\nexport type RecordDescript<T> = {\n  uri: AtUri\n  cid: CID\n  indexedAt: string\n  record: T\n}\n\nexport type ApiRes<T> = {\n  headers: Headers\n  data: T\n}\n\nexport type MungeFn<T> = (\n  localViewer: LocalViewer,\n  original: T,\n  local: LocalRecords,\n  requester: string,\n) => Promise<T>\n\nexport type HandlerResponse<T> = {\n  encoding: 'application/json'\n  body: T\n  headers?: Record<string, string>\n}\n"
  },
  {
    "path": "packages/pds/src/read-after-write/util.ts",
    "content": "import express from 'express'\nimport { jsonToLex } from '@atproto/lexicon'\nimport { HeadersMap } from '@atproto/xrpc'\nimport {\n  HandlerPipeThrough,\n  HandlerPipeThroughBuffer,\n  parseReqNsid,\n} from '@atproto/xrpc-server'\nimport { AppContext } from '../context'\nimport { lexicons } from '../lexicon/lexicons'\nimport { readStickyLogger as log } from '../logger'\nimport {\n  asPipeThroughBuffer,\n  isJsonContentType,\n  pipethrough,\n} from '../pipethrough'\nimport { HandlerResponse, LocalRecords, MungeFn } from './types'\n\nconst REPO_REV_HEADER = 'atproto-repo-rev'\n\nexport const getRepoRev = (headers: HeadersMap): string | undefined => {\n  return headers[REPO_REV_HEADER]\n}\n\nexport const getLocalLag = (local: LocalRecords): number | undefined => {\n  let oldest: string | undefined = local.profile?.indexedAt\n  for (const post of local.posts) {\n    if (!oldest || post.indexedAt < oldest) {\n      oldest = post.indexedAt\n    }\n  }\n  if (!oldest) return undefined\n  return Date.now() - new Date(oldest).getTime()\n}\n\nexport const pipethroughReadAfterWrite = async <T>(\n  ctx: AppContext,\n  reqCtx: { req: express.Request; auth: { credentials: { did: string } } },\n  munge: MungeFn<T>,\n): Promise<HandlerResponse<T> | HandlerPipeThrough> => {\n  const { req, auth } = reqCtx\n  const requester = auth.credentials.did\n\n  const streamRes = await pipethrough(ctx, req, { iss: requester })\n\n  const rev = getRepoRev(streamRes.headers)\n  if (!rev) return streamRes\n\n  if (isJsonContentType(streamRes.headers['content-type']) === false) {\n    // content-type is present but not JSON, we can't munge this\n    return streamRes\n  }\n\n  // if the munging fails, we can't return the original response because the\n  // stream will already have been read. If we end-up buffering the response,\n  // we'll return the buffered response in case of an error.\n  let bufferRes: HandlerPipeThroughBuffer | undefined\n\n  try {\n    const lxm = parseReqNsid(req)\n\n    return await ctx.actorStore.read(requester, async (store) => {\n      const local = await store.record.getRecordsSinceRev(rev)\n      if (local.count === 0) return streamRes\n\n      const { buffer } = (bufferRes = await asPipeThroughBuffer(streamRes))\n\n      const lex = jsonToLex(JSON.parse(buffer.toString('utf8')))\n\n      const parsedRes = lexicons.assertValidXrpcOutput(lxm, lex) as T\n\n      const localViewer = ctx.localViewer(store)\n\n      const data = await munge(localViewer, parsedRes, local, requester)\n      return formatMungedResponse(data, getLocalLag(local))\n    })\n  } catch (err) {\n    // The error occurred while reading the stream, this is non-recoverable\n    if (!bufferRes && !streamRes.stream.readable) throw err\n\n    log.warn({ err, requester }, 'error in read after write munge')\n    return bufferRes ?? streamRes\n  }\n}\n\nexport const formatMungedResponse = <T>(\n  body: T,\n  lag?: number,\n): HandlerResponse<T> => ({\n  encoding: 'application/json',\n  body,\n  headers:\n    lag !== undefined\n      ? {\n          'Atproto-Upstream-Lag': lag.toString(10),\n        }\n      : undefined,\n})\n"
  },
  {
    "path": "packages/pds/src/read-after-write/viewer.ts",
    "content": "import { AtUri, INVALID_HANDLE } from '@atproto/syntax'\nimport { createServiceAuthHeaders } from '@atproto/xrpc-server'\nimport { AccountManager } from '../account-manager/account-manager'\nimport { ActorStoreReader } from '../actor-store/actor-store-reader'\nimport { BskyAppView } from '../bsky-app-view'\nimport { ImageUrlBuilder } from '../image/image-url-builder'\nimport { ids } from '../lexicon/lexicons'\nimport {\n  ProfileView,\n  ProfileViewBasic,\n  ProfileViewDetailed,\n} from '../lexicon/types/app/bsky/actor/defs'\nimport { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'\nimport {\n  Main as EmbedExternal,\n  View as EmbedExternalView,\n  isMain as isEmbedExternal,\n} from '../lexicon/types/app/bsky/embed/external'\nimport {\n  Main as EmbedImages,\n  View as EmbedImagesView,\n  isMain as isEmbedImages,\n} from '../lexicon/types/app/bsky/embed/images'\nimport {\n  Main as EmbedRecord,\n  View as EmbedRecordView,\n  ViewRecord,\n  isMain as isEmbedRecord,\n} from '../lexicon/types/app/bsky/embed/record'\nimport {\n  Main as EmbedRecordWithMedia,\n  isMain as isEmbedRecordWithMedia,\n} from '../lexicon/types/app/bsky/embed/recordWithMedia'\nimport {\n  FeedViewPost,\n  GeneratorView,\n  PostView,\n} from '../lexicon/types/app/bsky/feed/defs'\nimport { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post'\nimport { ListView } from '../lexicon/types/app/bsky/graph/defs'\nimport { $Typed } from '../lexicon/util'\nimport { LocalRecords, RecordDescript } from './types'\n\ntype CommonSignedUris = 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize'\n\nexport type LocalViewerCreator = (\n  actorStoreReader: ActorStoreReader,\n) => LocalViewer\n\nexport class LocalViewer {\n  constructor(\n    public readonly actorStoreReader: ActorStoreReader,\n    public readonly accountManager: AccountManager,\n    public readonly imageUrlBuilder: ImageUrlBuilder,\n    public readonly bskyAppView?: BskyAppView,\n  ) {}\n\n  get did() {\n    return this.actorStoreReader.did\n  }\n\n  static creator(\n    accountManager: AccountManager,\n    imageUrlBuilder: ImageUrlBuilder,\n    bskyAppView?: BskyAppView,\n  ): LocalViewerCreator {\n    return (actorStore) =>\n      new LocalViewer(actorStore, accountManager, imageUrlBuilder, bskyAppView)\n  }\n\n  getImageUrl(pattern: CommonSignedUris, cid: string) {\n    return this.imageUrlBuilder.build(pattern, this.did, cid)\n  }\n\n  async serviceAuthHeaders(did: string, lxm: string) {\n    if (!this.bskyAppView) {\n      throw new Error('Could not find bsky appview did')\n    }\n    const keypair = await this.actorStoreReader.keypair()\n\n    return createServiceAuthHeaders({\n      iss: did,\n      aud: this.bskyAppView.did,\n      lxm,\n      keypair,\n    })\n  }\n\n  async getRecordsSinceRev(rev: string): Promise<LocalRecords> {\n    return this.actorStoreReader.record.getRecordsSinceRev(rev)\n  }\n\n  async getProfileBasic(): Promise<ProfileViewBasic | null> {\n    const [profileRes, accountRes] = await Promise.all([\n      this.actorStoreReader.record.getProfileRecord(),\n      this.accountManager.getAccount(this.did),\n    ])\n\n    if (!accountRes) return null\n\n    return {\n      did: this.did,\n      handle: accountRes.handle ?? INVALID_HANDLE,\n      displayName: profileRes?.displayName,\n      avatar: profileRes?.avatar\n        ? this.getImageUrl('avatar', profileRes.avatar.ref.toString())\n        : undefined,\n    }\n  }\n\n  async formatAndInsertPostsInFeed(\n    feed: FeedViewPost[],\n    posts: RecordDescript<PostRecord>[],\n  ): Promise<FeedViewPost[]> {\n    if (posts.length === 0) {\n      return feed\n    }\n    const lastTime = feed.at(-1)?.post.indexedAt ?? new Date(0).toISOString()\n    const inFeed = posts.filter((p) => p.indexedAt > lastTime)\n    const newestToOldest = inFeed.reverse()\n    const maybeFormatted = await Promise.all(\n      newestToOldest.map((p) => this.getPost(p)),\n    )\n    const formatted = maybeFormatted.filter((p) => p !== null) as PostView[]\n    for (const post of formatted) {\n      const idx = feed.findIndex((fi) => fi.post.indexedAt < post.indexedAt)\n      if (idx >= 0) {\n        feed.splice(idx, 0, { post })\n      } else {\n        feed.push({ post })\n      }\n    }\n    return feed\n  }\n\n  async getPost(\n    descript: RecordDescript<PostRecord>,\n  ): Promise<PostView | null> {\n    const { uri, cid, indexedAt, record } = descript\n    const author = await this.getProfileBasic()\n    if (!author) return null\n    const embed = record.embed\n      ? await this.formatPostEmbed(author.did, record)\n      : undefined\n    return {\n      uri: uri.toString(),\n      cid: cid.toString(),\n      likeCount: 0, // counts presumed to be 0 directly after post creation\n      replyCount: 0,\n      repostCount: 0,\n      quoteCount: 0,\n      author,\n      record,\n      embed: embed ?? undefined,\n      indexedAt,\n    }\n  }\n\n  async formatPostEmbed(did: string, post: PostRecord) {\n    const embed = post.embed\n    if (!embed) return null\n    if (isEmbedImages(embed) || isEmbedExternal(embed)) {\n      return this.formatSimpleEmbed(embed)\n    } else if (isEmbedRecord(embed)) {\n      return this.formatRecordEmbed(embed)\n    } else if (isEmbedRecordWithMedia(embed)) {\n      return this.formatRecordWithMediaEmbed(did, embed)\n    } else {\n      return null\n    }\n  }\n\n  async formatSimpleEmbed(\n    embed: $Typed<EmbedImages> | $Typed<EmbedExternal>,\n  ): Promise<$Typed<EmbedImagesView> | $Typed<EmbedExternalView>> {\n    if (isEmbedImages(embed)) {\n      const images = embed.images.map((img) => ({\n        thumb: this.getImageUrl('feed_thumbnail', img.image.ref.toString()),\n        fullsize: this.getImageUrl('feed_fullsize', img.image.ref.toString()),\n        aspectRatio: img.aspectRatio,\n        alt: img.alt,\n      }))\n      return {\n        $type: 'app.bsky.embed.images#view',\n        images,\n      }\n    } else if (isEmbedExternal(embed)) {\n      const { uri, title, description, thumb } = embed.external\n      return {\n        $type: 'app.bsky.embed.external#view',\n        external: {\n          uri,\n          title,\n          description,\n          thumb: thumb\n            ? this.getImageUrl('feed_thumbnail', thumb.ref.toString())\n            : undefined,\n        },\n      }\n    } else {\n      // @ts-expect-error\n      throw new TypeError(`Unexpected embed type: ${embed.$type}`)\n    }\n  }\n\n  async formatRecordEmbed(\n    embed: EmbedRecord,\n  ): Promise<$Typed<EmbedRecordView>> {\n    const view = await this.formatRecordEmbedInternal(embed)\n    return {\n      $type: 'app.bsky.embed.record#view',\n      record:\n        view === null\n          ? {\n              $type: 'app.bsky.embed.record#viewNotFound',\n              uri: embed.record.uri,\n            }\n          : view,\n    }\n  }\n\n  private async formatRecordEmbedInternal(\n    embed: EmbedRecord,\n  ): Promise<\n    null | $Typed<ViewRecord> | $Typed<GeneratorView> | $Typed<ListView>\n  > {\n    if (!this.bskyAppView) {\n      return null\n    }\n    const collection = new AtUri(embed.record.uri).collection\n    if (collection === ids.AppBskyFeedPost) {\n      const res = await this.bskyAppView.agent.app.bsky.feed.getPosts(\n        { uris: [embed.record.uri] },\n        await this.serviceAuthHeaders(this.did, ids.AppBskyFeedGetPosts),\n      )\n      const post = res.data.posts[0]\n      if (!post) return null\n      return {\n        $type: 'app.bsky.embed.record#viewRecord',\n        uri: post.uri,\n        cid: post.cid,\n        author: post.author,\n        value: post.record,\n        labels: post.labels,\n        embeds: post.embed ? [post.embed] : undefined,\n        indexedAt: post.indexedAt,\n      }\n    } else if (collection === ids.AppBskyFeedGenerator) {\n      const res = await this.bskyAppView.agent.app.bsky.feed.getFeedGenerator(\n        { feed: embed.record.uri },\n        await this.serviceAuthHeaders(\n          this.did,\n          ids.AppBskyFeedGetFeedGenerator,\n        ),\n      )\n      return {\n        $type: 'app.bsky.feed.defs#generatorView',\n        ...res.data.view,\n      }\n    } else if (collection === ids.AppBskyGraphList) {\n      const res = await this.bskyAppView.agent.app.bsky.graph.getList(\n        { list: embed.record.uri },\n        await this.serviceAuthHeaders(this.did, ids.AppBskyGraphGetList),\n      )\n      return {\n        $type: 'app.bsky.graph.defs#listView',\n        ...res.data.list,\n      }\n    }\n    return null\n  }\n\n  async formatRecordWithMediaEmbed(did: string, embed: EmbedRecordWithMedia) {\n    if (!isEmbedImages(embed.media) && !isEmbedExternal(embed.media)) {\n      return null\n    }\n    const media = this.formatSimpleEmbed(embed.media)\n    const record = await this.formatRecordEmbed(embed.record)\n    return {\n      $type: 'app.bsky.embed.recordWithMedia#view',\n      record,\n      media,\n    }\n  }\n\n  updateProfileViewBasic<\n    T extends ProfileViewDetailed | ProfileViewBasic | ProfileView,\n  >(view: T, record: ProfileRecord): T {\n    return {\n      ...view,\n      displayName: record.displayName,\n      avatar: record.avatar\n        ? this.getImageUrl('avatar', record.avatar.ref.toString())\n        : undefined,\n    }\n  }\n\n  updateProfileView<\n    T extends ProfileViewDetailed | ProfileViewBasic | ProfileView,\n  >(view: T, record: ProfileRecord): T {\n    return {\n      ...this.updateProfileViewBasic(view, record),\n      description: record.description,\n    }\n  }\n\n  updateProfileDetailed<T extends ProfileViewDetailed>(\n    view: T,\n    record: ProfileRecord,\n  ): T {\n    return {\n      ...this.updateProfileView(view, record),\n      banner: record.banner\n        ? this.getImageUrl('banner', record.banner.ref.toString())\n        : undefined,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/redis.ts",
    "content": "import assert from 'node:assert'\nimport { Redis } from 'ioredis'\nimport { redisLogger } from './logger'\n\nexport const getRedisClient = (host: string, password?: string): Redis => {\n  const redisAddr = redisAddressParts(host)\n  const redis = new Redis({\n    ...redisAddr,\n    password,\n  })\n  redis.on('error', (err) => {\n    redisLogger.error({ host, err }, 'redis error')\n  })\n  return redis\n}\n\nexport const redisAddressParts = (\n  addr: string,\n  defaultPort = 6379,\n): { host: string; port: number } => {\n  const [host, portStr, ...others] = addr.split(':')\n  const port = portStr ? parseInt(portStr, 10) : defaultPort\n  assert(host && !isNaN(port) && !others.length, `invalid address: ${addr}`)\n  return { host, port }\n}\n"
  },
  {
    "path": "packages/pds/src/repo/index.ts",
    "content": "export * from './prepare'\nexport * from './types'\n"
  },
  {
    "path": "packages/pds/src/repo/prepare.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { TID, check, dataToCborBlock } from '@atproto/common'\nimport {\n  BlobRef,\n  LexValue,\n  LexiconDefNotFoundError,\n  RepoRecord,\n  ValidationError,\n  lexToIpld,\n  untypedJsonBlobRef,\n} from '@atproto/lexicon'\nimport {\n  RecordCreateOp,\n  RecordDeleteOp,\n  RecordUpdateOp,\n  RecordWriteOp,\n  WriteOpAction,\n  cborToLex,\n} from '@atproto/repo'\nimport {\n  AtUri,\n  ensureValidDatetime,\n  ensureValidRecordKey,\n} from '@atproto/syntax'\nimport { hasExplicitSlur } from '../handle/explicit-slurs'\nimport * as lex from '../lexicon/lexicons'\nimport * as AppBskyActorProfile from '../lexicon/types/app/bsky/actor/profile'\nimport * as AppBskyFeedGenerator from '../lexicon/types/app/bsky/feed/generator'\nimport * as AppBskyFeedPost from '../lexicon/types/app/bsky/feed/post'\nimport * as AppBskyGraphList from '../lexicon/types/app/bsky/graph/list'\nimport * as AppBskyGraphStarterpack from '../lexicon/types/app/bsky/graph/starterpack'\nimport { isTag } from '../lexicon/types/app/bsky/richtext/facet'\nimport { asPredicate } from '../lexicon/util'\nimport {\n  InvalidRecordError,\n  PreparedBlobRef,\n  PreparedCreate,\n  PreparedDelete,\n  PreparedUpdate,\n  PreparedWrite,\n  ValidationStatus,\n} from './types'\n\nconst isValidFeedGenerator = asPredicate(AppBskyFeedGenerator.validateRecord)\nconst isValidStarterPack = asPredicate(AppBskyGraphStarterpack.validateRecord)\nconst isValidPost = asPredicate(AppBskyFeedPost.validateRecord)\nconst isValidList = asPredicate(AppBskyGraphList.validateRecord)\nconst isValidProfile = asPredicate(AppBskyActorProfile.validateRecord)\n\nexport const assertValidRecordWithStatus = (\n  record: Record<string, unknown>,\n  opts: { requireLexicon: boolean },\n): ValidationStatus => {\n  if (typeof record.$type !== 'string') {\n    throw new InvalidRecordError('No $type provided')\n  }\n  try {\n    lex.lexicons.assertValidRecord(record.$type, record)\n    assertValidCreatedAt(record)\n  } catch (e) {\n    if (e instanceof LexiconDefNotFoundError) {\n      if (opts.requireLexicon) {\n        throw new InvalidRecordError(e.message)\n      } else {\n        return 'unknown'\n      }\n    }\n    throw new InvalidRecordError(\n      `Invalid ${record.$type} record: ${\n        e instanceof Error ? e.message : String(e)\n      }`,\n    )\n  }\n  return 'valid'\n}\n\n// additional more rigorous check on datetimes\n// this check will eventually be in the lex sdk, but this will stop the bleed until then\nexport const assertValidCreatedAt = (record: Record<string, unknown>) => {\n  const createdAt = record['createdAt']\n  if (typeof createdAt !== 'string') {\n    return\n  }\n  try {\n    ensureValidDatetime(createdAt)\n  } catch {\n    throw new ValidationError(\n      'createdAt must be an valid atproto datetime (both RFC-3339 and ISO-8601)',\n    )\n  }\n}\n\nexport const setCollectionName = (\n  collection: string,\n  record: RepoRecord,\n  validate: boolean,\n) => {\n  if (!record.$type) {\n    record.$type = collection\n  }\n  if (validate && record.$type !== collection) {\n    throw new InvalidRecordError(\n      `Invalid $type: expected ${collection}, got ${record.$type}`,\n    )\n  }\n  return record\n}\n\nexport const prepareCreate = async (opts: {\n  did: string\n  collection: string\n  rkey?: string\n  swapCid?: CID | null\n  record: RepoRecord\n  validate?: boolean\n}): Promise<PreparedCreate> => {\n  const { did, collection, swapCid, validate } = opts\n  const maybeValidate = validate !== false\n  const record = setCollectionName(collection, opts.record, maybeValidate)\n  let validationStatus: ValidationStatus\n  if (maybeValidate) {\n    validationStatus = assertValidRecordWithStatus(record, {\n      requireLexicon: validate === true,\n    })\n  }\n  const nextRkey = TID.next()\n  const rkey = opts.rkey || nextRkey.toString()\n  // @TODO: validate against Lexicon record 'key' type, not just overall recordkey syntax\n  ensureValidRecordKey(rkey)\n  assertNoExplicitSlurs(rkey, record)\n  return {\n    action: WriteOpAction.Create,\n    uri: AtUri.make(did, collection, rkey),\n    cid: await cidForSafeRecord(record),\n    swapCid,\n    record,\n    blobs: blobsForWrite(record, maybeValidate),\n    validationStatus,\n  }\n}\n\nexport const prepareUpdate = async (opts: {\n  did: string\n  collection: string\n  rkey: string\n  swapCid?: CID | null\n  record: RepoRecord\n  validate?: boolean\n}): Promise<PreparedUpdate> => {\n  const { did, collection, rkey, swapCid, validate } = opts\n  const maybeValidate = validate !== false\n  const record = setCollectionName(collection, opts.record, maybeValidate)\n  let validationStatus: ValidationStatus\n  if (maybeValidate) {\n    validationStatus = assertValidRecordWithStatus(record, {\n      requireLexicon: validate === true,\n    })\n  }\n  assertNoExplicitSlurs(rkey, record)\n  return {\n    action: WriteOpAction.Update,\n    uri: AtUri.make(did, collection, rkey),\n    cid: await cidForSafeRecord(record),\n    swapCid,\n    record,\n    blobs: blobsForWrite(record, maybeValidate),\n    validationStatus,\n  }\n}\n\nexport const prepareDelete = (opts: {\n  did: string\n  collection: string\n  rkey: string\n  swapCid?: CID | null\n}): PreparedDelete => {\n  const { did, collection, rkey, swapCid } = opts\n  return {\n    action: WriteOpAction.Delete,\n    uri: AtUri.make(did, collection, rkey),\n    swapCid,\n  }\n}\n\nexport const createWriteToOp = (write: PreparedCreate): RecordCreateOp => ({\n  action: WriteOpAction.Create,\n  collection: write.uri.collection,\n  rkey: write.uri.rkey,\n  record: write.record,\n})\n\nexport const updateWriteToOp = (write: PreparedUpdate): RecordUpdateOp => ({\n  action: WriteOpAction.Update,\n  collection: write.uri.collection,\n  rkey: write.uri.rkey,\n  record: write.record,\n})\n\nexport const deleteWriteToOp = (write: PreparedDelete): RecordDeleteOp => ({\n  action: WriteOpAction.Delete,\n  collection: write.uri.collection,\n  rkey: write.uri.rkey,\n})\n\nexport const writeToOp = (write: PreparedWrite): RecordWriteOp => {\n  switch (write.action) {\n    case WriteOpAction.Create:\n      return createWriteToOp(write)\n    case WriteOpAction.Update:\n      return updateWriteToOp(write)\n    case WriteOpAction.Delete:\n      return deleteWriteToOp(write)\n    default:\n      throw new Error(`Unrecognized action: ${write}`)\n  }\n}\n\nasync function cidForSafeRecord(record: RepoRecord) {\n  try {\n    const block = await dataToCborBlock(lexToIpld(record))\n    cborToLex(block.bytes)\n    return block.cid\n  } catch (err) {\n    // Block does not properly transform between lex and cbor\n    const badRecordErr = new InvalidRecordError('Bad record')\n    badRecordErr.cause = err\n    throw badRecordErr\n  }\n}\n\nfunction assertNoExplicitSlurs(rkey: string, record: RepoRecord) {\n  const toCheck: string[] = []\n\n  if (isValidProfile(record)) {\n    if (record.displayName) toCheck.push(record.displayName)\n  } else if (isValidList(record)) {\n    toCheck.push(record.name)\n  } else if (isValidStarterPack(record)) {\n    toCheck.push(record.name)\n  } else if (isValidFeedGenerator(record)) {\n    toCheck.push(rkey)\n    toCheck.push(record.displayName)\n  } else if (isValidPost(record)) {\n    if (record.tags) {\n      toCheck.push(...record.tags)\n    }\n\n    for (const facet of record.facets || []) {\n      for (const feat of facet.features) {\n        if (isTag(feat)) {\n          toCheck.push(feat.tag)\n        }\n      }\n    }\n  }\n  if (hasExplicitSlur(toCheck.join(' '))) {\n    throw new InvalidRecordError('Unacceptable slur in record')\n  }\n}\n\ntype FoundBlobRef = {\n  ref: BlobRef\n  path: string[]\n}\n\nexport const blobsForWrite = (\n  record: RepoRecord,\n  validate: boolean,\n): PreparedBlobRef[] => {\n  const refs = findBlobRefs(record)\n  const recordType =\n    typeof record['$type'] === 'string' ? record['$type'] : undefined\n\n  for (const ref of refs) {\n    if (check.is(ref.ref.original, untypedJsonBlobRef)) {\n      throw new InvalidRecordError(`Legacy blob ref at '${ref.path.join('/')}'`)\n    }\n  }\n\n  return refs.map(({ ref, path }) => ({\n    size: ref.size,\n    cid: ref.ref,\n    mimeType: ref.mimeType,\n    constraints:\n      validate && recordType\n        ? CONSTRAINTS[recordType]?.[path.join('/')] ?? {}\n        : {},\n  }))\n}\n\nexport const findBlobRefs = (\n  val: LexValue,\n  path: string[] = [],\n  layer = 0,\n): FoundBlobRef[] => {\n  if (layer > 32) {\n    return []\n  }\n  // walk arrays\n  if (Array.isArray(val)) {\n    return val.flatMap((item) => findBlobRefs(item, path, layer + 1))\n  }\n  // objects\n  if (val && typeof val === 'object') {\n    // convert blobs, leaving the original encoding so that we don't change CIDs on re-encode\n    if (val instanceof BlobRef) {\n      return [\n        {\n          ref: val,\n          path,\n        },\n      ]\n    }\n    // retain cids & bytes\n    if (CID.asCID(val) || val instanceof Uint8Array) {\n      return []\n    }\n    return Object.entries(val).flatMap(([key, item]) =>\n      findBlobRefs(item, [...path, key], layer + 1),\n    )\n  }\n  // pass through\n  return []\n}\n\nconst CONSTRAINTS = {\n  [lex.ids.AppBskyActorProfile]: {\n    avatar:\n      lex.schemaDict.AppBskyActorProfile.defs.main.record.properties.avatar,\n    banner:\n      lex.schemaDict.AppBskyActorProfile.defs.main.record.properties.banner,\n  },\n  [lex.ids.AppBskyFeedGenerator]: {\n    avatar:\n      lex.schemaDict.AppBskyFeedGenerator.defs.main.record.properties.avatar,\n  },\n  [lex.ids.AppBskyGraphList]: {\n    avatar: lex.schemaDict.AppBskyGraphList.defs.main.record.properties.avatar,\n  },\n  [lex.ids.AppBskyFeedPost]: {\n    'embed/images/image':\n      lex.schemaDict.AppBskyEmbedImages.defs.image.properties.image,\n    'embed/external/thumb':\n      lex.schemaDict.AppBskyEmbedExternal.defs.external.properties.thumb,\n    'embed/media/images/image':\n      lex.schemaDict.AppBskyEmbedImages.defs.image.properties.image,\n    'embed/media/external/thumb':\n      lex.schemaDict.AppBskyEmbedExternal.defs.external.properties.thumb,\n  },\n}\n"
  },
  {
    "path": "packages/pds/src/repo/types.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { BlockMap, CommitData, WriteOpAction } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\n\nexport type ValidationStatus = 'valid' | 'unknown' | undefined\n\nexport type BlobConstraint = {\n  accept?: string[]\n  maxSize?: number\n}\n\nexport type PreparedBlobRef = {\n  size: number\n  cid: CID\n  mimeType: string\n  constraints: BlobConstraint\n}\n\nexport type PreparedCreate = {\n  action: WriteOpAction.Create\n  uri: AtUri\n  cid: CID\n  swapCid?: CID | null\n  record: RepoRecord\n  blobs: PreparedBlobRef[]\n  validationStatus: ValidationStatus\n}\n\nexport type PreparedUpdate = {\n  action: WriteOpAction.Update\n  uri: AtUri\n  cid: CID\n  swapCid?: CID | null\n  record: RepoRecord\n  blobs: PreparedBlobRef[]\n  validationStatus: ValidationStatus\n}\n\nexport type PreparedDelete = {\n  action: WriteOpAction.Delete\n  uri: AtUri\n  swapCid?: CID | null\n}\n\nexport type CommitOp = {\n  action: 'create' | 'update' | 'delete'\n  path: string\n  cid: CID | null\n  prev?: CID\n}\n\nexport type CommitDataWithOps = CommitData & {\n  ops: CommitOp[]\n  prevData: CID | null\n}\n\nexport type PreparedWrite = PreparedCreate | PreparedUpdate | PreparedDelete\n\nexport type SyncEvtData = {\n  cid: CID\n  rev: string\n  blocks: BlockMap\n}\n\nexport class InvalidRecordError extends Error {}\n\nexport class BadCommitSwapError extends Error {\n  constructor(public cid: CID) {\n    super(`Commit was at ${cid.toString()}`)\n  }\n}\n\nexport class BadRecordSwapError extends Error {\n  constructor(public cid: CID | null) {\n    super(`Record was at ${cid?.toString() ?? 'null'}`)\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/README.md",
    "content": "# PDS Scripts\n\nThis directory includes some low-level administrative scripts primarily meant to help recover from situations of data loss or repository corruption.\n\nThese scripts are included in the Docker image. The recommended way to run them is to shell into the PDS container (`docker exec -it pds /bin/sh`) and run the relevant script using `node run-script.js SCRIPT_NAME`.\n\n### rebuild-repo\n\nRebuild a repo's MST and sign a new commit based on data stored in the actor's record table. Intended to be used if a repository is corrupted and there are missing MST blocks.\n\n`node run-script.js rebuild-repo DID`\n\n### publish-identity\n\nPublishes an identity event on the PDS's outgoing firehose for the relevant DID. Intended to be used if a user's identity is out of date and a refresh of their identity needs to be pushed through the system.\n\n`node run-script.js publish-identity DID`\n`node run-script.js publish-identity DID1 DID2 DID3 ...`\n`node run-script.js publish-identity-file dids.txt` (where `dids.txt` is a `\\n` delimited text file of dids)\n\n### rotate-keys\n\nEnsures that an account's signing key in their PLC DID document matches the signing key that the PDS is holidng for them locally. If not, then update their PLC document. Does not work for `did:web`s. Intended to be used in recovery situations where an accounts' signing key is lost and it needs to be re-generated. This script _does not_ regenerate the key.\n\n`node run-script.js rotate-keys DID`\n`node run-script.js rotate-keys DID1 DID2 DID3 ...`\n`node run-script.js rotate-keys-file dids.txt` (where `dids.txt` is a `\\n` delimited text file of dids)\n`node run-script.js rotate-keys-recovery` (to be used after `sequencer-recovery` script)\n\n### sequencer-recovery\n\nReplays the sequencer file on top of actor stores. Creates new actor stores & keys for actors that do not exist but are in the sequencer. Deletes actor stores & entries in the accounts DB when processing an account deletion on the stream. This script is meant to be re-runnable. Though because it processes events in parallel, it is important to be discerning about the cursor you pick up from if you stop & start the script.\n\nDoes _not_ rotate signing keys even if it generates them. Signing keys that need to be rotated are stored in a recovery DB and can be actually rotated with the `rotate-keys-recovery` script.\n\nIntended to be used for recovery from data loss.\n\n`node run-script.js sequencer-recovery START_CURSOR CONCURRENCY` (both params are optional & default to `0` and `10` respectively)\n\nFailures are also tracked in the recovery DB and can be recovered from with `node run-script.js recovery-repair-repos`. Which will rebuild repos (a la `rebuild-repo`) and then play back the events from the sequencer only pertaining to the recovered DIDs.\n"
  },
  {
    "path": "packages/pds/src/scripts/index.ts",
    "content": "import { publishIdentity, publishIdentityFromFile } from './publish-identity'\nimport { rebuildRepo } from './rebuild-repo'\nimport {\n  rotateKeys,\n  rotateKeysFromFile,\n  rotateKeysRecovery,\n} from './rotate-keys'\nimport { sequencerRecovery } from './sequencer-recovery'\nimport { repairRepos } from './sequencer-recovery/repair-repos'\n\nexport const scripts = {\n  'rebuild-repo': rebuildRepo,\n  'sequencer-recovery': sequencerRecovery,\n  'recovery-repair-repos': repairRepos,\n  'rotate-keys': rotateKeys,\n  'rotate-keys-file': rotateKeysFromFile,\n  'rotate-keys-recovery': rotateKeysRecovery,\n  'publish-identity': publishIdentity,\n  'publish-identity-file': publishIdentityFromFile,\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/publish-identity.ts",
    "content": "import fs from 'node:fs/promises'\nimport { wait } from '@atproto/common'\nimport { Sequencer } from '../sequencer'\nimport { parseIntArg } from './util'\n\nexport type PublishIdentityContext = {\n  sequencer: Sequencer\n}\n\nexport const publishIdentity = async (\n  ctx: PublishIdentityContext,\n  args: string[],\n) => {\n  const dids = args\n  await publishIdentityEvtForDids(ctx, dids)\n  console.log('DONE')\n}\n\nexport const publishIdentityFromFile = async (\n  ctx: PublishIdentityContext,\n  args: string[],\n) => {\n  const filepath = args[0]\n  if (!filepath) {\n    throw new Error('Expected filepath as argument')\n  }\n  const timeBetween = args[1] ? parseIntArg(args[1]) : 5\n  const file = await fs.readFile(filepath)\n  const dids = file\n    .toString()\n    .split('\\n')\n    .map((did) => did.trim())\n\n  await publishIdentityEvtForDids(ctx, dids, timeBetween)\n  console.log('DONE')\n}\n\nexport const publishIdentityEvtForDids = async (\n  ctx: PublishIdentityContext,\n  dids: string[],\n  timeBetween = 0,\n) => {\n  for (const did of dids) {\n    try {\n      await ctx.sequencer.sequenceIdentityEvt(did)\n      console.log(`published identity evt for ${did}`)\n    } catch (err) {\n      console.error(`failed to sequence new identity evt for ${did}: ${err}`)\n    }\n    if (timeBetween > 0) {\n      await wait(timeBetween)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/rebuild-repo.ts",
    "content": "import readline from 'node:readline/promises'\nimport { TID } from '@atproto/common'\nimport {\n  BlockMap,\n  CidSet,\n  MST,\n  MemoryBlockstore,\n  signCommit,\n} from '@atproto/repo'\nimport { AccountManager } from '../account-manager/account-manager'\nimport { ActorStore } from '../actor-store/actor-store'\nimport { Sequencer } from '../sequencer'\n\nexport interface RebuildContext {\n  sequencer: Sequencer\n  accountManager: AccountManager\n  actorStore: ActorStore\n}\n\nexport const rebuildRepoScript = async (\n  ctx: RebuildContext,\n  args: string[],\n) => {\n  const did = args[0]\n  if (!did || !did.startsWith('did:')) {\n    throw new Error('Expected DID as argument')\n  }\n  return rebuildRepo(ctx, did, true)\n}\n\nexport const rebuildRepo = async (\n  ctx: RebuildContext,\n  did: string,\n  promptUser: boolean,\n) => {\n  const memoryStore = new MemoryBlockstore()\n  const commit = await ctx.actorStore.transact(did, async (store) => {\n    const rootDetails = await store.repo.storage.getRootDetailed()\n    const records = await store.record.listAll()\n    const existingCids = await store.record.listExistingBlocks()\n\n    // increment existing rev by 1 ms\n    const revTid = TID.fromStr(rootDetails.rev)\n    const rev = TID.fromTime(\n      revTid.timestamp() + 1,\n      revTid.clockid(),\n    ).toString()\n\n    let mst = await MST.create(memoryStore)\n    for (const record of records) {\n      mst = await mst.add(record.path, record.cid)\n    }\n    const newBlocks = new BlockMap()\n    for await (const node of mst.walk()) {\n      if (node.isTree()) {\n        const pointer = await node.getPointer()\n        if (!existingCids.has(pointer)) {\n          const serialized = await node.serialize()\n          newBlocks.set(serialized.cid, serialized.bytes)\n        }\n      }\n    }\n    const mstCids = await mst.allCids()\n    const toDelete = new CidSet(existingCids.toList()).subtractSet(mstCids)\n    const newCommit = await signCommit(\n      {\n        did,\n        version: 3,\n        rev,\n        prev: null,\n        data: await mst.getPointer(),\n      },\n      store.repo.signingKey,\n    )\n    const commitCid = await newBlocks.add(newCommit)\n\n    if (promptUser) {\n      console.log('Record count: ', records.length)\n      console.log('Existing blocks: ', existingCids.toList().length)\n      console.log('Deleting blocks:', toDelete.toList().length)\n      console.log('Adding blocks: ', newBlocks.size)\n\n      const shouldContinue = await promptContinue()\n      if (!shouldContinue) {\n        throw new Error('Aborted')\n      }\n    }\n\n    await store.repo.storage.deleteMany(toDelete.toList())\n    await store.repo.storage.putMany(newBlocks, rev)\n    await store.repo.storage.updateRoot(commitCid, rev)\n    return {\n      cid: commitCid,\n      rev,\n      since: null,\n      prev: null,\n      newBlocks,\n      relevantBlocks: newBlocks,\n      removedCids: toDelete,\n      ops: [],\n      blobs: new CidSet(),\n      prevData: null,\n    }\n  })\n  await ctx.accountManager.updateRepoRoot(did, commit.cid, commit.rev)\n  const syncData = await ctx.actorStore.read(did, (store) =>\n    store.repo.getSyncEventData(),\n  )\n  await ctx.sequencer.sequenceSyncEvt(did, syncData)\n}\n\nconst promptContinue = async (): Promise<boolean> => {\n  const rl = readline.createInterface({\n    input: process.stdin,\n    output: process.stdout,\n  })\n  const answer = await rl.question('Continue? y/n ')\n  return answer === ''\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/rotate-keys.ts",
    "content": "import fs from 'node:fs/promises'\nimport * as plc from '@did-plc/lib'\nimport PQueue from 'p-queue'\nimport AtpAgent from '@atproto/api'\nimport { Keypair } from '@atproto/crypto'\nimport { IdResolver } from '@atproto/identity'\nimport { ActorStore } from '../actor-store/actor-store'\nimport { SyncEvtData } from '../repo'\nimport { Sequencer } from '../sequencer'\nimport { getRecoveryDbFromSequencerLoc } from './sequencer-recovery/recovery-db'\nimport { parseIntArg } from './util'\n\nexport type RotateKeysContext = {\n  sequencer: Sequencer\n  actorStore: ActorStore\n  idResolver: IdResolver\n  plcClient: plc.Client\n  plcRotationKey: Keypair\n  entrywayAdminAgent?: AtpAgent\n}\n\nexport const rotateKeys = async (ctx: RotateKeysContext, args: string[]) => {\n  const dids = args\n  await rotateKeysForRepos(ctx, dids, 10)\n}\n\nexport const rotateKeysFromFile = async (\n  ctx: RotateKeysContext,\n  args: string[],\n) => {\n  const filepath = args[0]\n  if (!filepath) {\n    throw new Error('Expected filepath as argument')\n  }\n  const concurrency = args[1] ? parseIntArg(args[1]) : 25\n  const file = await fs.readFile(filepath)\n  const dids = file\n    .toString()\n    .split('\\n')\n    .map((did) => did.trim())\n    .filter((did) => did.startsWith('did:plc'))\n\n  await rotateKeysForRepos(ctx, dids, concurrency)\n}\n\nexport const rotateKeysRecovery = async (\n  ctx: RotateKeysContext,\n  args: string[],\n) => {\n  const concurrency = args[1] ? parseIntArg(args[0]) : 10\n\n  const recoveryDb = await getRecoveryDbFromSequencerLoc(\n    ctx.sequencer.dbLocation,\n  )\n  const rows = await recoveryDb.db\n    .selectFrom('new_account')\n    .select('did')\n    .where('new_account.published', '=', 0)\n    .execute()\n  const dids = rows.map((r) => r.did)\n\n  await rotateKeysForRepos(ctx, dids, concurrency, async (did) => {\n    await recoveryDb.db\n      .updateTable('new_account')\n      .set({ published: 1 })\n      .where('did', '=', did)\n      .execute()\n  })\n}\n\nconst rotateKeysForRepos = async (\n  ctx: RotateKeysContext,\n  dids: string[],\n  concurrency: number,\n  onSuccess?: (did: string) => Promise<void>,\n) => {\n  const queue = new PQueue({ concurrency })\n  let completed = 0\n  for (const did of dids) {\n    queue.add(async () => {\n      try {\n        await updatePlcSigningKey(ctx, did)\n      } catch (err) {\n        console.error(`failed to update key for ${did}: ${err}`)\n        return\n      }\n      let syncData: SyncEvtData\n      try {\n        syncData = await ctx.actorStore.transact(did, async (actorTxn) => {\n          await actorTxn.repo.processWrites([])\n          return actorTxn.repo.getSyncEventData()\n        })\n      } catch (err) {\n        console.error(`failed to write new commit for ${did}: ${err}`)\n        return\n      }\n      try {\n        await ctx.sequencer.sequenceIdentityEvt(did)\n      } catch (err) {\n        console.error(`failed to sequence new identity evt for ${did}: ${err}`)\n        return\n      }\n      try {\n        await ctx.sequencer.sequenceSyncEvt(did, syncData)\n      } catch (err) {\n        console.error(`failed to sequence for ${did}: ${err}`)\n        return\n      }\n      if (onSuccess) {\n        await onSuccess(did)\n      }\n      completed++\n      if (completed % 10 === 0) {\n        console.log(`${completed}/${dids.length}`)\n      }\n    })\n  }\n  await queue.onIdle()\n  console.log('DONE')\n}\n\nconst updatePlcSigningKey = async (ctx: RotateKeysContext, did: string) => {\n  const updateTo = await ctx.actorStore.keypair(did)\n  const currSigningKey = await ctx.idResolver.did.resolveAtprotoKey(did, true)\n  if (updateTo.did() === currSigningKey) {\n    // already up to date\n    return\n  }\n  if (ctx.entrywayAdminAgent) {\n    await ctx.entrywayAdminAgent.api.com.atproto.admin.updateAccountSigningKey({\n      did,\n      signingKey: updateTo.did(),\n    })\n  } else {\n    await ctx.plcClient.updateAtprotoKey(\n      did,\n      ctx.plcRotationKey,\n      updateTo.did(),\n    )\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/sequencer-recovery/index.ts",
    "content": "import { parseIntArg } from '../util'\nimport { Recoverer, RecovererContextNoDb } from './recoverer'\nimport { getRecoveryDbFromSequencerLoc } from './recovery-db'\n\nexport const sequencerRecovery = async (\n  ctx: RecovererContextNoDb,\n  args: string[],\n) => {\n  const cursor = args[0] ? parseIntArg(args[0]) : 0\n  const concurrency = args[1] ? parseIntArg(args[1]) : 10\n\n  const recoveryDb = await getRecoveryDbFromSequencerLoc(\n    ctx.sequencer.dbLocation,\n  )\n\n  const recover = new Recoverer(\n    { ...ctx, recoveryDb },\n    {\n      concurrency,\n    },\n  )\n  await recover.run(cursor)\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/sequencer-recovery/recoverer.ts",
    "content": "import { rmIfExists } from '@atproto/common'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport {\n  BlockMap,\n  CidSet,\n  CommitData,\n  WriteOpAction,\n  cborToLexRecord,\n  parseDataKey,\n  readCar,\n} from '@atproto/repo'\nimport {\n  AccountManager,\n  AccountStatus,\n} from '../../account-manager/account-manager'\nimport { ActorStore } from '../../actor-store/actor-store'\nimport { ActorStoreTransactor } from '../../actor-store/actor-store-transactor'\nimport { countAll } from '../../db'\nimport {\n  PreparedWrite,\n  prepareCreate,\n  prepareDelete,\n  prepareUpdate,\n} from '../../repo'\nimport { AccountEvt, CommitEvt, SeqEvt, Sequencer } from '../../sequencer'\nimport { RecoveryDb } from './recovery-db'\nimport { UserQueues } from './user-queues'\n\nexport type RecovererContextNoDb = {\n  sequencer: Sequencer\n  accountManager: AccountManager\n  actorStore: ActorStore\n}\n\nexport type RecovererContext = RecovererContextNoDb & {\n  recoveryDb: RecoveryDb\n}\n\nconst PAGE_SIZE = 5000\n\nexport class Recoverer {\n  queues: UserQueues\n  failed: Set<string>\n\n  constructor(\n    public ctx: RecovererContext,\n    opts: { concurrency: number },\n  ) {\n    this.queues = new UserQueues(opts.concurrency)\n    this.failed = new Set()\n  }\n\n  async run(startCursor = 0) {\n    const failed = await this.ctx.recoveryDb.db\n      .selectFrom('failed')\n      .select('did')\n      .execute()\n    for (const row of failed) {\n      this.failed.add(row.did)\n    }\n\n    const totalRes = await this.ctx.sequencer.db.db\n      .selectFrom('repo_seq')\n      .select(countAll.as('count'))\n      .executeTakeFirstOrThrow()\n    const totalEvts = totalRes.count\n    let completed = 0\n\n    let cursor: number | undefined = startCursor\n    while (cursor !== undefined) {\n      const page = await this.ctx.sequencer.requestSeqRange({\n        earliestSeq: cursor,\n        limit: PAGE_SIZE,\n      })\n      page.forEach((evt) => this.processEvent(evt))\n      cursor = page.at(-1)?.seq\n\n      await this.queues.onEmpty()\n\n      completed += PAGE_SIZE\n      const percentComplete = (completed / totalEvts) * 100\n      console.log(`${percentComplete.toFixed(2)}% - ${cursor}`)\n    }\n\n    await this.queues.processAll()\n  }\n\n  async processAll() {\n    await this.queues.processAll()\n  }\n\n  async destroy() {\n    await this.queues.destroy()\n  }\n\n  processEvent(evt: SeqEvt) {\n    const did = didFromEvt(evt)\n    if (!did) {\n      return\n    }\n    this.queues.addToUser(did, async () => {\n      if (this.failed.has(did)) {\n        return\n      }\n      await processSeqEvt(this.ctx, evt).catch(async (err) => {\n        this.failed.add(did)\n        await trackFailure(this.ctx.recoveryDb, did, err)\n      })\n    })\n  }\n}\n\nexport const processSeqEvt = async (ctx: RecovererContext, evt: SeqEvt) => {\n  // only need to process commits & tombstones\n  if (evt.type === 'account') {\n    await processAccountEvt(ctx, evt.evt)\n  }\n  if (evt.type === 'commit') {\n    await processCommit(ctx, evt.evt).catch()\n  }\n}\n\nconst processCommit = async (ctx: RecovererContext, evt: CommitEvt) => {\n  const did = evt.repo\n  const { writes, blocks } = await parseCommitEvt(evt)\n  if (evt.since === null) {\n    const actorExists = await ctx.actorStore.exists(did)\n    if (!actorExists) {\n      await processRepoCreation(ctx, evt, writes, blocks)\n      return\n    }\n  }\n  await ctx.actorStore.transact(did, async (actorTxn) => {\n    const root = await actorTxn.repo.storage.getRootDetailed()\n    if (root.rev >= evt.rev) {\n      return\n    }\n    const commit = await actorTxn.repo.formatCommit(writes)\n    commit.newBlocks = blocks\n    commit.cid = evt.commit\n    commit.rev = evt.rev\n    await actorTxn.repo.storage.applyCommit(commit)\n    await actorTxn.repo.indexWrites(writes, commit.rev)\n    await trackBlobs(actorTxn, writes)\n  })\n}\n\nconst processRepoCreation = async (\n  ctx: RecovererContext,\n  evt: CommitEvt,\n  writes: PreparedWrite[],\n  blocks: BlockMap,\n) => {\n  const did = evt.repo\n  const keypair = await Secp256k1Keypair.create({ exportable: true })\n  await ctx.actorStore.create(did, keypair)\n  const commit: CommitData = {\n    cid: evt.commit,\n    rev: evt.rev,\n    since: evt.since,\n    prev: null,\n    newBlocks: blocks,\n    relevantBlocks: new BlockMap(),\n    removedCids: new CidSet(),\n  }\n  await ctx.actorStore.transact(did, async (actorTxn) => {\n    await actorTxn.repo.storage.applyCommit(commit, true)\n    await actorTxn.repo.indexWrites(writes, commit.rev)\n    await actorTxn.repo.blob.processWriteBlobs(commit.rev, writes)\n  })\n  await trackNewAccount(ctx.recoveryDb, did)\n}\n\nconst processAccountEvt = async (ctx: RecovererContext, evt: AccountEvt) => {\n  // do not need to process deactivation/takedowns because we backup account DB as well\n  if (evt.status !== AccountStatus.Deleted) {\n    return\n  }\n  const { directory } = await ctx.actorStore.getLocation(evt.did)\n  await rmIfExists(directory, true)\n  await ctx.accountManager.deleteAccount(evt.did)\n}\n\nconst trackBlobs = async (\n  store: ActorStoreTransactor,\n  writes: PreparedWrite[],\n) => {\n  await store.repo.blob.deleteDereferencedBlobs(writes)\n\n  for (const write of writes) {\n    if (\n      write.action === WriteOpAction.Create ||\n      write.action === WriteOpAction.Update\n    ) {\n      for (const blob of write.blobs) {\n        await store.repo.blob.insertBlobMetadata(blob)\n        await store.repo.blob.associateBlob(blob, write.uri)\n      }\n    }\n  }\n}\n\nconst trackFailure = async (\n  recoveryDb: RecoveryDb,\n  did: string,\n  err: unknown,\n) => {\n  await recoveryDb.db\n    .insertInto('failed')\n    .values({\n      did,\n      error: err?.toString(),\n      fixed: 0,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .execute()\n}\n\nconst trackNewAccount = async (recoveryDb: RecoveryDb, did: string) => {\n  await recoveryDb.db\n    .insertInto('new_account')\n    .values({\n      did,\n      published: 0,\n    })\n    .onConflict((oc) => oc.doNothing())\n    .execute()\n}\n\nconst parseCommitEvt = async (\n  evt: CommitEvt,\n): Promise<{\n  writes: PreparedWrite[]\n  blocks: BlockMap\n}> => {\n  const did = evt.repo\n  const evtCar = await readCar(evt.blocks, { skipCidVerification: true })\n  const writesUnfiltered = await Promise.all(\n    evt.ops.map(async (op) => {\n      const { collection, rkey } = parseDataKey(op.path)\n      if (op.action === 'delete') {\n        return prepareDelete({ did, collection, rkey })\n      }\n      if (!op.cid) return undefined\n      const recordBytes = evtCar.blocks.get(op.cid)\n      if (!recordBytes) return undefined\n      const record = cborToLexRecord(recordBytes)\n\n      if (op.action === 'create') {\n        return prepareCreate({\n          did,\n          collection,\n          rkey,\n          record,\n          validate: false,\n        })\n      } else {\n        return prepareUpdate({\n          did,\n          collection,\n          rkey,\n          record,\n          validate: false,\n        })\n      }\n    }),\n  )\n  const writes = writesUnfiltered.filter(\n    (w) => w !== undefined,\n  ) as PreparedWrite[]\n  return {\n    writes,\n    blocks: evtCar.blocks,\n  }\n}\n\nconst didFromEvt = (evt: SeqEvt): string | null => {\n  if (evt.type === 'account') {\n    return evt.evt.did\n  } else if (evt.type === 'commit') {\n    return evt.evt.repo\n  } else {\n    return null\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/sequencer-recovery/recovery-db.ts",
    "content": "import path from 'node:path'\nimport { Kysely } from 'kysely'\nimport { Database, Migrator } from '../../db'\n\nexport interface NewAccount {\n  did: string\n  published: 0 | 1\n}\n\nexport interface Failed {\n  did: string\n  error: string | null\n  fixed: 0 | 1\n}\n\nexport type RecoveryDbSchema = {\n  new_account: NewAccount\n  failed: Failed\n}\n\nexport type RecoveryDb = Database<RecoveryDbSchema>\n\nexport const getRecoveryDbFromSequencerLoc = (\n  sequencerLoc: string,\n): Promise<RecoveryDb> => {\n  const recoveryDbLoc = path.join(path.dirname(sequencerLoc), 'recovery.sqlite')\n  return getAndMigrateRecoveryDb(recoveryDbLoc)\n}\n\nexport const getAndMigrateRecoveryDb = async (\n  location: string,\n  disableWalAutoCheckpoint = false,\n): Promise<RecoveryDb> => {\n  const pragmas: Record<string, string> = disableWalAutoCheckpoint\n    ? { wal_autocheckpoint: '0' }\n    : {}\n  const db = Database.sqlite(location, pragmas)\n  const migrator = new Migrator(db.db, migrations)\n  await migrator.migrateToLatestOrThrow()\n  return db\n}\n\nconst migrations = {\n  '001': {\n    up: async (db: Kysely<unknown>) => {\n      await db.schema\n        .createTable('new_account')\n        .addColumn('did', 'varchar', (col) => col.primaryKey())\n        .addColumn('published', 'int2', (col) => col.notNull())\n        .execute()\n\n      await db.schema\n        .createTable('failed')\n        .addColumn('did', 'varchar', (col) => col.primaryKey())\n        .addColumn('error', 'varchar')\n        .addColumn('fixed', 'int2', (col) => col.notNull())\n        .execute()\n    },\n    down: async (db: Kysely<unknown>) => {\n      await db.schema.dropTable('new_account').execute()\n      await db.schema.dropTable('failed').execute()\n    },\n  },\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/sequencer-recovery/repair-repos.ts",
    "content": "import { parseRepoSeqRows } from '../../sequencer'\nimport { rebuildRepo } from '../rebuild-repo'\nimport {\n  RecovererContext,\n  RecovererContextNoDb,\n  processSeqEvt,\n} from './recoverer'\nimport { getRecoveryDbFromSequencerLoc } from './recovery-db'\n\nexport const repairRepos = async (ctx: RecovererContextNoDb) => {\n  const recoveryDb = await getRecoveryDbFromSequencerLoc(\n    ctx.sequencer.dbLocation,\n  )\n  const repairRes = await recoveryDb.db\n    .selectFrom('failed')\n    .select('did')\n    .where('failed.fixed', '=', 0)\n    .execute()\n  const dids = repairRes.map((row) => row.did)\n  let fixed = 0\n  for (const did of dids) {\n    await rebuildRepo(ctx, did, false)\n    await recoverFromSequencer({ ...ctx, recoveryDb }, did)\n    fixed++\n    console.log(`${fixed}/${dids.length}`)\n  }\n}\n\nconst recoverFromSequencer = async (ctx: RecovererContext, did: string) => {\n  const didEvts = await ctx.sequencer.db.db\n    .selectFrom('repo_seq')\n    .selectAll()\n    .where('did', '=', did)\n    .orderBy('seq', 'asc')\n    .execute()\n  const seqEvts = parseRepoSeqRows(didEvts)\n  for (const evt of seqEvts) {\n    await processSeqEvt(ctx, evt)\n  }\n  await ctx.recoveryDb.db\n    .updateTable('failed')\n    .set({\n      fixed: 1,\n      error: null,\n    })\n    .where('did', '=', did)\n    .execute()\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/sequencer-recovery/user-queues.ts",
    "content": "import PQueue from 'p-queue'\nexport class UserQueues {\n  main: PQueue\n  queues = new Map<string, PQueue>()\n\n  constructor(concurrency: number) {\n    this.main = new PQueue({ concurrency })\n  }\n\n  async addToUser(did: string, task: () => Promise<void>) {\n    if (this.main.isPaused) return\n    return this.main.add(() => {\n      return this.getQueue(did).add(task)\n    })\n  }\n\n  private getQueue(did: string) {\n    let queue = this.queues.get(did)\n    if (!queue) {\n      queue = new PQueue({ concurrency: 1 })\n      queue.once('idle', () => this.queues.delete(did))\n      this.queues.set(did, queue)\n    }\n    return queue\n  }\n\n  async onEmpty() {\n    await this.main.onEmpty()\n  }\n\n  async processAll() {\n    await this.main.onIdle()\n  }\n\n  async destroy() {\n    this.main.pause()\n    this.main.clear()\n    this.queues.forEach((q) => q.clear())\n    await this.processAll()\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/scripts/util.ts",
    "content": "export const parseIntArg = (arg: string): number => {\n  const parsed = parseInt(arg, 10)\n  if (isNaN(parsed)) {\n    throw new Error(`Invalid arg, expected number: ${arg}`)\n  }\n  return parsed\n}\n"
  },
  {
    "path": "packages/pds/src/sequencer/db/index.ts",
    "content": "import { Database, Migrator } from '../../db'\nimport migrations from './migrations'\nimport { SequencerDbSchema } from './schema'\n\nexport * from './schema'\n\nexport type SequencerDb = Database<SequencerDbSchema>\n\nexport const getDb = (\n  location: string,\n  disableWalAutoCheckpoint = false,\n): SequencerDb => {\n  const pragmas: Record<string, string> = disableWalAutoCheckpoint\n    ? { wal_autocheckpoint: '0' }\n    : {}\n  return Database.sqlite(location, pragmas)\n}\n\nexport const getMigrator = (db: Database<SequencerDbSchema>) => {\n  return new Migrator(db.db, migrations)\n}\n"
  },
  {
    "path": "packages/pds/src/sequencer/db/migrations/001-init.ts",
    "content": "import { Kysely } from 'kysely'\n\nexport async function up(db: Kysely<unknown>): Promise<void> {\n  await db.schema\n    .createTable('repo_seq')\n    .addColumn('seq', 'integer', (col) => col.autoIncrement().primaryKey())\n    .addColumn('did', 'varchar', (col) => col.notNull())\n    .addColumn('eventType', 'varchar', (col) => col.notNull())\n    .addColumn('event', 'blob', (col) => col.notNull())\n    .addColumn('invalidated', 'int2', (col) => col.notNull().defaultTo(0))\n    .addColumn('sequencedAt', 'varchar', (col) => col.notNull())\n    .execute()\n  // for filtering seqs based on did\n  await db.schema\n    .createIndex('repo_seq_did_idx')\n    .on('repo_seq')\n    .column('did')\n    .execute()\n  // for filtering seqs based on event type\n  await db.schema\n    .createIndex('repo_seq_event_type_idx')\n    .on('repo_seq')\n    .column('eventType')\n    .execute()\n  // for entering into the seq stream at a particular time\n  await db.schema\n    .createIndex('repo_seq_sequenced_at_index')\n    .on('repo_seq')\n    .column('sequencedAt')\n    .execute()\n}\n\nexport async function down(db: Kysely<unknown>): Promise<void> {\n  await db.schema.dropTable('repo_seq').execute()\n}\n"
  },
  {
    "path": "packages/pds/src/sequencer/db/migrations/index.ts",
    "content": "import * as init from './001-init'\n\nexport default {\n  '001': init,\n}\n"
  },
  {
    "path": "packages/pds/src/sequencer/db/schema.ts",
    "content": "import { Generated, GeneratedAlways, Insertable, Selectable } from 'kysely'\n\nexport type RepoSeqEventType = 'append' | 'sync' | 'identity' | 'account'\n\nexport interface RepoSeq {\n  seq: GeneratedAlways<number>\n  did: string\n  eventType: RepoSeqEventType\n  event: Uint8Array\n  invalidated: Generated<0 | 1>\n  sequencedAt: string\n}\n\nexport type RepoSeqInsert = Insertable<RepoSeq>\nexport type RepoSeqEntry = Selectable<RepoSeq>\n\nexport type SequencerDbSchema = {\n  repo_seq: RepoSeq\n}\n"
  },
  {
    "path": "packages/pds/src/sequencer/events.ts",
    "content": "import assert from 'node:assert'\nimport { z } from 'zod'\nimport { cborEncode, schema } from '@atproto/common'\nimport { BlockMap, blocksToCarFile } from '@atproto/repo'\nimport { AccountStatus } from '../account-manager/account-manager'\nimport { CommitDataWithOps, SyncEvtData } from '../repo'\nimport { RepoSeqInsert } from './db'\n\nexport const formatSeqCommit = async (\n  did: string,\n  commitData: CommitDataWithOps,\n): Promise<RepoSeqInsert> => {\n  const blocksToSend = new BlockMap()\n  blocksToSend.addMap(commitData.newBlocks)\n  blocksToSend.addMap(commitData.relevantBlocks)\n\n  const evt = {\n    repo: did,\n    commit: commitData.cid,\n    rev: commitData.rev,\n    since: commitData.since,\n    blocks: await blocksToCarFile(commitData.cid, blocksToSend),\n    ops: commitData.ops,\n    prevData: commitData.prevData ?? undefined,\n    // deprecated (but still required) fields\n    rebase: false,\n    tooBig: false,\n    blobs: [],\n  }\n\n  return {\n    did,\n    eventType: 'append' as const,\n    event: cborEncode(evt),\n    sequencedAt: new Date().toISOString(),\n  }\n}\n\nexport const formatSeqSyncEvt = async (\n  did: string,\n  data: SyncEvtData,\n): Promise<RepoSeqInsert> => {\n  const blocks = await blocksToCarFile(data.cid, data.blocks)\n  const evt: SyncEvt = {\n    did,\n    rev: data.rev,\n    blocks,\n  }\n  return {\n    did,\n    eventType: 'sync',\n    event: cborEncode(evt),\n    sequencedAt: new Date().toISOString(),\n  }\n}\n\nexport const syncEvtDataFromCommit = (\n  commitData: CommitDataWithOps,\n): SyncEvtData => {\n  const { blocks, missing } = commitData.relevantBlocks.getMany([\n    commitData.cid,\n  ])\n  assert(\n    !missing.length,\n    'commit block was not found, could not build sync event',\n  )\n  return {\n    rev: commitData.rev,\n    cid: commitData.cid,\n    blocks,\n  }\n}\n\nexport const formatSeqIdentityEvt = async (\n  did: string,\n  handle?: string,\n): Promise<RepoSeqInsert> => {\n  const evt: IdentityEvt = {\n    did,\n  }\n  if (handle) {\n    evt.handle = handle\n  }\n  return {\n    did,\n    eventType: 'identity',\n    event: cborEncode(evt),\n    sequencedAt: new Date().toISOString(),\n  }\n}\n\nexport const formatSeqAccountEvt = async (\n  did: string,\n  status: AccountStatus,\n): Promise<RepoSeqInsert> => {\n  const evt: AccountEvt = {\n    did,\n    active: status === 'active',\n  }\n  if (status !== AccountStatus.Active) {\n    evt.status = status\n  }\n\n  return {\n    did,\n    eventType: 'account',\n    event: cborEncode(evt),\n    sequencedAt: new Date().toISOString(),\n  }\n}\n\nexport const commitEvtOp = z.object({\n  action: z.union([\n    z.literal('create'),\n    z.literal('update'),\n    z.literal('delete'),\n  ]),\n  path: z.string(),\n  cid: schema.cid.nullable(),\n  prev: schema.cid.optional(),\n})\nexport type CommitEvtOp = z.infer<typeof commitEvtOp>\n\nexport const commitEvt = z.object({\n  rebase: z.boolean(),\n  tooBig: z.boolean(),\n  repo: z.string(),\n  commit: schema.cid,\n  rev: z.string(),\n  since: z.string().nullable(),\n  blocks: schema.bytes,\n  ops: z.array(commitEvtOp),\n  blobs: z.array(schema.cid),\n  prevData: schema.cid.optional(),\n})\nexport type CommitEvt = z.infer<typeof commitEvt>\n\nexport const syncEvt = z.object({\n  did: z.string(),\n  blocks: schema.bytes,\n  rev: z.string(),\n})\nexport type SyncEvt = z.infer<typeof syncEvt>\n\nexport const identityEvt = z.object({\n  did: z.string(),\n  handle: z.string().optional(),\n})\nexport type IdentityEvt = z.infer<typeof identityEvt>\n\nexport const accountEvt = z.object({\n  did: z.string(),\n  active: z.boolean(),\n  status: z\n    .enum([\n      AccountStatus.Takendown,\n      AccountStatus.Suspended,\n      AccountStatus.Deleted,\n      AccountStatus.Deactivated,\n    ])\n    .optional(),\n})\nexport type AccountEvt = z.infer<typeof accountEvt>\n\ntype TypedCommitEvt = {\n  type: 'commit'\n  seq: number\n  time: string\n  evt: CommitEvt\n}\ntype TypedSyncEvt = {\n  type: 'sync'\n  seq: number\n  time: string\n  evt: SyncEvt\n}\ntype TypedIdentityEvt = {\n  type: 'identity'\n  seq: number\n  time: string\n  evt: IdentityEvt\n}\ntype TypedAccountEvt = {\n  type: 'account'\n  seq: number\n  time: string\n  evt: AccountEvt\n}\nexport type SeqEvt =\n  | TypedCommitEvt\n  | TypedSyncEvt\n  | TypedIdentityEvt\n  | TypedAccountEvt\n"
  },
  {
    "path": "packages/pds/src/sequencer/index.ts",
    "content": "export * from './sequencer'\nexport * from './outbox'\nexport * from './events'\n"
  },
  {
    "path": "packages/pds/src/sequencer/outbox.ts",
    "content": "import { AsyncBuffer, AsyncBufferFullError } from '@atproto/common'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { SeqEvt, Sequencer } from '.'\n\nexport type OutboxOpts = {\n  maxBufferSize: number\n}\n\nexport class Outbox {\n  private caughtUp = false\n  lastSeen = -1\n\n  cutoverBuffer: SeqEvt[]\n  outBuffer: AsyncBuffer<SeqEvt>\n\n  constructor(\n    public sequencer: Sequencer,\n    opts: Partial<OutboxOpts> = {},\n  ) {\n    const { maxBufferSize = 500 } = opts\n    this.cutoverBuffer = []\n    this.outBuffer = new AsyncBuffer<SeqEvt>(maxBufferSize)\n  }\n\n  // event stream occurs in 3 phases\n  // 1. backfill events: events that have been added to the DB since the last time a connection was open.\n  // The outbox is not yet listening for new events from the sequencer\n  // 2. cutover: the outbox has caught up with where the sequencer purports to be,\n  // but the sequencer might already be halfway through sending out a round of updates.\n  // Therefore, we start accepting the sequencer's events in a buffer, while making our own request to the\n  // database to ensure we're caught up. We then dedupe the query & the buffer & stream the events in order\n  // 3. streaming: we're all caught up on historic state, so the sequencer outputs events and we\n  // immediately yield them\n  async *events(\n    backfillCursor?: number,\n    signal?: AbortSignal,\n  ): AsyncGenerator<SeqEvt> {\n    // catch up as much as we can\n    if (backfillCursor !== undefined) {\n      for await (const evt of this.getBackfill(backfillCursor)) {\n        if (signal?.aborted) return\n        this.lastSeen = evt.seq\n        yield evt\n      }\n    } else {\n      // if not backfill, we don't need to cutover, just start streaming\n      this.caughtUp = true\n    }\n\n    // streams updates from sequencer, but buffers them for cutover as it makes a last request\n\n    const addToBuffer = (evts) => {\n      if (this.caughtUp) {\n        this.outBuffer.pushMany(evts)\n      } else {\n        this.cutoverBuffer = [...this.cutoverBuffer, ...evts]\n      }\n    }\n\n    if (!signal?.aborted) {\n      this.sequencer.on('events', addToBuffer)\n    }\n    signal?.addEventListener('abort', () =>\n      this.sequencer.off('events', addToBuffer),\n    )\n\n    const cutover = async () => {\n      // only need to perform cutover if we've been backfilling\n      if (backfillCursor !== undefined) {\n        const cutoverEvts = await this.sequencer.requestSeqRange({\n          earliestSeq: this.lastSeen > -1 ? this.lastSeen : backfillCursor,\n        })\n        this.outBuffer.pushMany(cutoverEvts)\n        // dont worry about dupes, we ensure order on yield\n        this.outBuffer.pushMany(this.cutoverBuffer)\n        this.caughtUp = true\n        this.cutoverBuffer = []\n      } else {\n        this.caughtUp = true\n      }\n    }\n    cutover()\n\n    while (true) {\n      try {\n        for await (const evt of this.outBuffer.events()) {\n          if (signal?.aborted) return\n          if (evt.seq > this.lastSeen) {\n            this.lastSeen = evt.seq\n            yield evt\n          }\n        }\n      } catch (err) {\n        if (err instanceof AsyncBufferFullError) {\n          throw new InvalidRequestError(\n            'Stream consumer too slow',\n            'ConsumerTooSlow',\n          )\n        } else {\n          throw err\n        }\n      }\n    }\n  }\n\n  // yields only historical events\n  async *getBackfill(backfillCursor: number) {\n    const PAGE_SIZE = 500\n    while (true) {\n      const evts = await this.sequencer.requestSeqRange({\n        earliestSeq: this.lastSeen > -1 ? this.lastSeen : backfillCursor,\n        limit: PAGE_SIZE,\n      })\n      for (const evt of evts) {\n        yield evt\n      }\n      // if we're within half a pagesize of the sequencer, we call it good & switch to cutover\n      const seqCursor = this.sequencer.lastSeen ?? -1\n      if (seqCursor - this.lastSeen < PAGE_SIZE / 2) break\n      if (evts.length < 1) break\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/sequencer/sequencer.ts",
    "content": "import EventEmitter from 'node:events'\nimport TypedEmitter from 'typed-emitter'\nimport { SECOND, cborDecode, wait } from '@atproto/common'\nimport { AccountStatus } from '../account-manager/helpers/account'\nimport { Crawlers } from '../crawlers'\nimport { seqLogger as log } from '../logger'\nimport { CommitDataWithOps, SyncEvtData } from '../repo'\nimport {\n  RepoSeqEntry,\n  RepoSeqInsert,\n  SequencerDb,\n  getDb,\n  getMigrator,\n} from './db'\nimport {\n  AccountEvt,\n  CommitEvt,\n  IdentityEvt,\n  SeqEvt,\n  SyncEvt,\n  formatSeqAccountEvt,\n  formatSeqCommit,\n  formatSeqIdentityEvt,\n  formatSeqSyncEvt,\n} from './events'\n\nexport * from './events'\n\nexport class Sequencer extends (EventEmitter as new () => SequencerEmitter) {\n  db: SequencerDb\n  destroyed = false\n  pollPromise: Promise<void> | null = null\n  triesWithNoResults = 0\n\n  constructor(\n    public dbLocation: string,\n    public crawlers: Crawlers,\n    public lastSeen = 0,\n    disableWalAutoCheckpoint = false,\n  ) {\n    super()\n    // note: this does not err when surpassed, just prints a warning to stderr\n    this.setMaxListeners(100)\n    this.db = getDb(dbLocation, disableWalAutoCheckpoint)\n  }\n\n  async start() {\n    await this.db.ensureWal()\n    const migrator = getMigrator(this.db)\n    await migrator.migrateToLatestOrThrow()\n    const curr = await this.curr()\n    this.lastSeen = curr ?? 0\n    if (this.pollPromise === null) {\n      this.pollPromise = this.pollDb()\n    }\n  }\n\n  async destroy() {\n    this.destroyed = true\n    if (this.pollPromise) {\n      await this.pollPromise\n    }\n    this.emit('close')\n  }\n\n  async curr(): Promise<number | null> {\n    const got = await this.db.db\n      .selectFrom('repo_seq')\n      .selectAll()\n      .orderBy('seq', 'desc')\n      .limit(1)\n      .executeTakeFirst()\n    return got?.seq ?? null\n  }\n\n  async next(cursor: number): Promise<SeqRow | null> {\n    const got = await this.db.db\n      .selectFrom('repo_seq')\n      .selectAll()\n      .where('seq', '>', cursor)\n      .limit(1)\n      .orderBy('seq', 'asc')\n      .executeTakeFirst()\n    return got || null\n  }\n\n  async earliestAfterTime(time: string): Promise<SeqRow | null> {\n    const got = await this.db.db\n      .selectFrom('repo_seq')\n      .selectAll()\n      .where('sequencedAt', '>=', time)\n      .orderBy('sequencedAt', 'asc')\n      .limit(1)\n      .executeTakeFirst()\n    return got || null\n  }\n\n  async requestSeqRange(opts: {\n    earliestSeq?: number\n    latestSeq?: number\n    earliestTime?: string\n    limit?: number\n  }): Promise<SeqEvt[]> {\n    const { earliestSeq, latestSeq, earliestTime, limit } = opts\n\n    let seqQb = this.db.db\n      .selectFrom('repo_seq')\n      .selectAll()\n      .orderBy('seq', 'asc')\n      .where('invalidated', '=', 0)\n    if (earliestSeq !== undefined) {\n      seqQb = seqQb.where('seq', '>', earliestSeq)\n    }\n    if (latestSeq !== undefined) {\n      seqQb = seqQb.where('seq', '<=', latestSeq)\n    }\n    if (earliestTime !== undefined) {\n      seqQb = seqQb.where('sequencedAt', '>=', earliestTime)\n    }\n    if (limit !== undefined) {\n      seqQb = seqQb.limit(limit)\n    }\n\n    const rows = await seqQb.execute()\n    if (rows.length < 1) {\n      return []\n    }\n\n    return parseRepoSeqRows(rows)\n  }\n\n  private async pollDb(): Promise<void> {\n    if (this.destroyed) return\n    // if already polling, do not start another poll\n    try {\n      const evts = await this.requestSeqRange({\n        earliestSeq: this.lastSeen,\n        limit: 1000,\n      })\n      if (evts.length > 0) {\n        this.triesWithNoResults = 0\n        this.emit('events', evts)\n        this.lastSeen = evts.at(-1)?.seq ?? this.lastSeen\n      } else {\n        await this.exponentialBackoff()\n      }\n      this.pollPromise = this.pollDb()\n    } catch (err) {\n      log.error({ err, lastSeen: this.lastSeen }, 'sequencer failed to poll db')\n      await this.exponentialBackoff()\n      this.pollPromise = this.pollDb()\n    }\n  }\n\n  // when no results, exponential backoff on pulling, with a max of a second wait\n  private async exponentialBackoff(): Promise<void> {\n    this.triesWithNoResults++\n    const waitTime = Math.min(Math.pow(2, this.triesWithNoResults), SECOND)\n    await wait(waitTime)\n  }\n\n  async sequenceEvt(evt: RepoSeqInsert): Promise<number> {\n    const [{ seq }] = await this.db.executeWithRetry(\n      this.db.db.insertInto('repo_seq').values(evt).returning('seq'),\n    )\n    this.crawlers.notifyOfUpdate()\n    return seq\n  }\n\n  async sequenceCommit(\n    did: string,\n    commitData: CommitDataWithOps,\n  ): Promise<number> {\n    const evt = await formatSeqCommit(did, commitData)\n    return await this.sequenceEvt(evt)\n  }\n\n  async sequenceSyncEvt(did: string, data: SyncEvtData) {\n    const evt = await formatSeqSyncEvt(did, data)\n    return await this.sequenceEvt(evt)\n  }\n\n  async sequenceIdentityEvt(did: string, handle?: string): Promise<number> {\n    const evt = await formatSeqIdentityEvt(did, handle)\n    return await this.sequenceEvt(evt)\n  }\n\n  async sequenceAccountEvt(\n    did: string,\n    status: AccountStatus,\n  ): Promise<number> {\n    const evt = await formatSeqAccountEvt(did, status)\n    return await this.sequenceEvt(evt)\n  }\n\n  async deleteAllForUser(did: string, excludingSeqs: number[] = []) {\n    await this.db.executeWithRetry(\n      this.db.db\n        .deleteFrom('repo_seq')\n        .where('did', '=', did)\n        .if(excludingSeqs.length > 0, (qb) =>\n          qb.where('seq', 'not in', excludingSeqs),\n        ),\n    )\n  }\n}\n\nexport const parseRepoSeqRows = (rows: RepoSeqEntry[]): SeqEvt[] => {\n  const seqEvts: SeqEvt[] = []\n  for (const row of rows) {\n    // should never hit this because of WHERE clause\n    if (row.seq === null) {\n      continue\n    }\n    const evt = cborDecode(row.event)\n    if (row.eventType === 'append') {\n      seqEvts.push({\n        type: 'commit',\n        seq: row.seq,\n        time: row.sequencedAt,\n        evt: evt as CommitEvt,\n      })\n    } else if (row.eventType === 'sync') {\n      seqEvts.push({\n        type: 'sync',\n        seq: row.seq,\n        time: row.sequencedAt,\n        evt: evt as SyncEvt,\n      })\n    } else if (row.eventType === 'identity') {\n      seqEvts.push({\n        type: 'identity',\n        seq: row.seq,\n        time: row.sequencedAt,\n        evt: evt as IdentityEvt,\n      })\n    } else if (row.eventType === 'account') {\n      seqEvts.push({\n        type: 'account',\n        seq: row.seq,\n        time: row.sequencedAt,\n        evt: evt as AccountEvt,\n      })\n    }\n  }\n  return seqEvts\n}\n\ntype SeqRow = RepoSeqEntry\n\ntype SequencerEvents = {\n  events: (evts: SeqEvt[]) => void\n  close: () => void\n}\n\nexport type SequencerEmitter = TypedEmitter<SequencerEvents>\n\nexport default Sequencer\n"
  },
  {
    "path": "packages/pds/src/util/compression.ts",
    "content": "import compression from 'compression'\nimport express from 'express'\n\nexport default function () {\n  return compression({\n    filter,\n  })\n}\n\nfunction filter(_req: express.Request, res: express.Response) {\n  const contentType = res.getHeader('Content-type')\n  if (contentType === 'application/vnd.ipld.car') {\n    return true\n  }\n  return compression.filter(_req, res)\n}\n"
  },
  {
    "path": "packages/pds/src/util/debug.ts",
    "content": "export const debugCatch = <Func extends (...args: any[]) => any>(fn: Func) => {\n  return async (...args: Parameters<Func>) => {\n    try {\n      return (await fn(...args)) as Awaited<ReturnType<Func>>\n    } catch (err) {\n      console.error(err)\n      throw err\n    }\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/util/http.ts",
    "content": "import { ServerResponse } from 'node:http'\n\n/**\n * Set or appends a value to the `Vary` header in the response, only if the\n * value is not already present.\n */\nexport function appendVary(res: ServerResponse, value: string) {\n  if (!varyContains(res, value.toLowerCase())) {\n    res.appendHeader('Vary', value)\n  }\n}\n\nfunction varyContains(res: ServerResponse, searchValue: string) {\n  const headerValue = res.getHeader('Vary')\n  switch (typeof headerValue) {\n    case 'string':\n      return varyStringContains(headerValue, searchValue)\n    case 'object':\n      // headerValue is a string[] here\n      return headerValue.some((h) => varyStringContains(h, searchValue))\n    default:\n      return false\n  }\n}\n\nfunction varyStringContains(headerValue: string, searchValue: string): boolean {\n  return headerValue\n    .split(',')\n    .map((v) => v.trim().toLowerCase())\n    .some((v) => v === searchValue || v === `*`)\n}\n"
  },
  {
    "path": "packages/pds/src/util/params.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\n\nexport const parseCidParam = (cid: string): CID => {\n  try {\n    return CID.parse(cid)\n  } catch (err) {\n    if (err instanceof SyntaxError) {\n      throw new InvalidRequestError('Invalid cid')\n    }\n    throw err\n  }\n}\n"
  },
  {
    "path": "packages/pds/src/util/types.ts",
    "content": "export type Simplify<T> = {\n  [K in keyof T]: T[K]\n} & NonNullable<unknown>\n\nexport type WithRequired<T, K extends keyof T> = Simplify<\n  Omit<T, K> & Required<Pick<T, K>>\n>\n"
  },
  {
    "path": "packages/pds/src/well-known.ts",
    "content": "import { Router } from 'express'\nimport { AppContext } from './context'\n\nexport const createRouter = (ctx: AppContext): Router => {\n  const router = Router()\n\n  router.get('/.well-known/atproto-did', async function (req, res) {\n    const handle = req.hostname\n    const supportedHandle = ctx.cfg.identity.serviceHandleDomains.some(\n      (host) => handle.endsWith(host) || handle === host.slice(1),\n    )\n    if (!supportedHandle) {\n      return res.status(404).send('User not found')\n    }\n    let did: string | undefined\n    try {\n      const user = await ctx.accountManager.getAccount(handle)\n      did = user?.did\n    } catch (err) {\n      return res.status(500).send('Internal Server Error')\n    }\n    if (!did) {\n      return res.status(404).send('User not found')\n    }\n    res.type('text/plain').send(did)\n  })\n\n  return router\n}\n"
  },
  {
    "path": "packages/pds/test.env",
    "content": "LOG_ENABLED=true\nLOG_DESTINATION=test.log"
  },
  {
    "path": "packages/pds/tests/__snapshots__/takedown-appeal.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`appeal account takedown actor takedown allows appeal request. 1`] = `\nObject {\n  \"accountStats\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n    \"appealCount\": 0,\n    \"escalateCount\": 0,\n    \"reportCount\": 0,\n    \"suspendCount\": 0,\n    \"takedownCount\": 1,\n  },\n  \"ageAssuranceState\": \"unknown\",\n  \"appealed\": true,\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"hosting\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n    \"status\": \"unknown\",\n  },\n  \"id\": 1,\n  \"lastAppealedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"lastReviewedBy\": \"user(0)\",\n  \"priorityScore\": 0,\n  \"recordsStats\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n  },\n  \"reviewState\": \"tools.ozone.moderation.defs#reviewEscalated\",\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(1)\",\n  },\n  \"subjectBlobCids\": Array [],\n  \"subjectRepoHandle\": \"jeff.test\",\n  \"tags\": Array [\n    \"lang:und\",\n    \"report:appeal\",\n  ],\n  \"takendown\": true,\n  \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n}\n`;\n"
  },
  {
    "path": "packages/pds/tests/_puppeteer.ts",
    "content": "import assert from 'node:assert'\nimport { type Browser, type Page } from 'puppeteer'\n\nexport class PageHelper implements AsyncDisposable {\n  constructor(protected readonly page: Page) {}\n\n  async goto(url: string) {\n    await this.page.goto(url)\n  }\n\n  async waitForNetworkIdle() {\n    await this.page.waitForNetworkIdle()\n  }\n\n  async navigationAction(run: () => Promise<unknown>): Promise<void> {\n    const promise = this.page.waitForNavigation()\n    await run()\n    await promise\n    await this.waitForNetworkIdle()\n  }\n\n  async checkTitle(expected: string) {\n    await this.waitForNetworkIdle()\n    await expect(this.page.title()).resolves.toBe(expected)\n  }\n\n  async clickOn(selector: string) {\n    const elementHandle = await this.getVisibleElement(selector)\n    await elementHandle.click()\n    return elementHandle\n  }\n\n  async clickOnButton(text: string) {\n    return this.clickOn(`button::-p-text(${JSON.stringify(text)})`)\n  }\n\n  async typeIn(selector: string, text: string) {\n    const elementHandle = await this.getVisibleElement(selector)\n    elementHandle.focus()\n    await elementHandle.type(text)\n    return elementHandle\n  }\n\n  async typeInInput(name: string, text: string) {\n    return this.typeIn(`input[name=${JSON.stringify(name)}]`, text)\n  }\n\n  async ensureTextVisibility(text: string, tag = 'p') {\n    await this.page.waitForSelector(`${tag}::-p-text(${JSON.stringify(text)})`)\n  }\n\n  protected async getVisibleElement(selector: string) {\n    const elementHandle = await this.page.waitForSelector(selector)\n\n    expect(elementHandle).not.toBeNull()\n    assert(elementHandle)\n\n    await expect(elementHandle.isVisible()).resolves.toBe(true)\n\n    return elementHandle\n  }\n\n  async [Symbol.asyncDispose]() {\n    return this.page.close()\n  }\n\n  static async from(\n    browser: Browser,\n    options?: { languages?: readonly string[] },\n  ) {\n    const page = await browser.newPage()\n\n    if (options?.languages?.length) {\n      // Spoof navigator language settings\n      await page.evaluateOnNewDocument(`\n        Object.defineProperty(navigator, 'languages', {\n          get: () => ${JSON.stringify(options.languages)},\n        })\n        Object.defineProperty(navigator, 'language', {\n          get: () => ${JSON.stringify(options.languages[0])},\n        })\n      `)\n    }\n\n    return new PageHelper(page)\n  }\n}\n"
  },
  {
    "path": "packages/pds/tests/_util.ts",
    "content": "import { Server } from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { type Express } from 'express'\nimport { CID } from 'multiformats/cid'\nimport { ToolsOzoneModerationDefs } from '@atproto/api'\nimport { lexToJson } from '@atproto/lexicon'\nimport { AtUri } from '@atproto/syntax'\nimport {\n  FeedViewPost,\n  isReasonRepost,\n} from '../src/lexicon/types/app/bsky/feed/defs'\n\n// Swap out identifiers and dates with stable\n// values for the purpose of snapshot testing\nexport const forSnapshot = (obj: unknown) => {\n  const records = { [kTake]: 'record' }\n  const collections = { [kTake]: 'collection' }\n  const users = { [kTake]: 'user' }\n  const cids = { [kTake]: 'cids' }\n  const unknown = { [kTake]: 'unknown' }\n  const toWalk = lexToJson(obj as any) // remove any blobrefs/cids\n  return mapLeafValues(toWalk, (item) => {\n    const asCid = CID.asCID(item)\n    if (asCid !== null) {\n      return take(cids, asCid.toString())\n    }\n    if (typeof item !== 'string') {\n      return item\n    }\n    const str = item.startsWith('did:plc:') ? `at://${item}` : item\n    if (str.startsWith('at://')) {\n      const uri = new AtUri(str)\n      if (uri.rkey) {\n        return take(records, str)\n      }\n      if (uri.collection) {\n        return take(collections, str)\n      }\n      if (uri.hostname) {\n        return take(users, str)\n      }\n      return take(unknown, str)\n    }\n    if (str.match(/^\\d{4}-\\d{2}-\\d{2}T/)) {\n      if (str.match(/\\d{6}Z$/)) {\n        return constantDate.replace('Z', '000Z') // e.g. microseconds in record createdAt\n      } else if (str.endsWith('+00:00')) {\n        return constantDate.replace('Z', '+00:00') // e.g. timezone in record createdAt\n      } else {\n        return constantDate\n      }\n    }\n    // handles both pds and appview cursor separators\n    if (str.match(/^\\d+(?:__|::)bafy/)) {\n      return constantKeysetCursor\n    }\n    if (str.match(/^\\d+(?:__|::)did:plc/)) {\n      return constantDidCursor\n    }\n    if (str.match(/\\/image\\/[^/]+\\/.+\\/did:plc:[^/]+\\/[^/@]+(?:@[\\w]+)?$/)) {\n      // Match image urls (pds), stripping optional format suffix (e.g. @webp) for stable snapshots\n      const match = str.match(\n        /\\/image\\/([^/]+)\\/.+\\/(did:plc:[^/]+)\\/([^/@]+)(?:@[\\w]+)?$/,\n      )\n      if (!match) return str\n      const [, sig, did, cid] = match\n      return str\n        .replace(sig, 'sig()')\n        .replace(did, take(users, did))\n        .replace(new RegExp(`${cid}(?:@\\\\w+)?`), take(cids, cid))\n    }\n    if (str.match(/\\/img\\/[^/]+\\/.+\\/did:plc:[^/]+\\/[^/@]+(?:@[\\w]+)?$/)) {\n      // Match image urls (bsky w/ presets), stripping optional format suffix (e.g. @webp) for stable snapshots\n      const match = str.match(\n        /\\/img\\/[^/]+\\/.+\\/(did:plc:[^/]+)\\/([^/@]+)(?:@[\\w]+)?$/,\n      )\n      if (!match) return str\n      const [, did, cid] = match\n      return str\n        .replace(did, take(users, did))\n        .replace(new RegExp(`${cid}(?:@\\\\w+)?`), take(cids, cid))\n    }\n    if (str.startsWith('localhost-')) {\n      return 'invite-code'\n    }\n    if (str.match(/^\\d+::pds-public-url-/)) {\n      return '0000000000000::invite-code'\n    }\n    let isCid: boolean\n    try {\n      CID.parse(str)\n      isCid = true\n    } catch (_err) {\n      isCid = false\n    }\n    if (isCid) {\n      return take(cids, str)\n    }\n    return item\n  })\n}\n\n// Feed testing utils\n\nexport const getOriginator = (item: FeedViewPost) => {\n  if (isReasonRepost(item.reason)) {\n    return item.reason.by.did\n  } else {\n    return item.post.author.did\n  }\n}\n\n// Useful for remapping ids in snapshot testing, to make snapshots deterministic.\n// E.g. you may use this to map this:\n//   [{ uri: 'did://rad'}, { uri: 'did://bad' }, { uri: 'did://rad'}]\n// to this:\n//   [{ uri: '0'}, { uri: '1' }, { uri: '0'}]\nconst kTake = Symbol('take')\nexport function take(obj, value: string): string\nexport function take(obj, value: string | undefined): string | undefined\nexport function take(\n  obj: { [s: string]: number; [kTake]?: string },\n  value: string | undefined,\n): string | undefined {\n  if (value === undefined) {\n    return\n  }\n  if (!(value in obj)) {\n    obj[value] = Object.keys(obj).length\n  }\n  const kind = obj[kTake]\n  return typeof kind === 'string'\n    ? `${kind}(${obj[value]})`\n    : String(obj[value])\n}\n\nexport const constantDate = new Date(0).toISOString()\nexport const constantKeysetCursor = '0000000000000::bafycid'\nexport const constantDidCursor = '0000000000000::did'\n\nconst mapLeafValues = (obj: unknown, fn: (val: unknown) => unknown) => {\n  if (Array.isArray(obj)) {\n    return obj.map((item) => mapLeafValues(item, fn))\n  }\n  if (obj && typeof obj === 'object') {\n    return Object.entries(obj).reduce(\n      (collect, [name, value]) =>\n        Object.assign(collect, { [name]: mapLeafValues(value, fn) }),\n      {},\n    )\n  }\n  return fn(obj)\n}\n\nexport const paginateAll = async <T extends { cursor?: string }>(\n  fn: (cursor?: string) => Promise<T>,\n  limit = Infinity,\n): Promise<T[]> => {\n  const results: T[] = []\n  let cursor\n  do {\n    const res = await fn(cursor)\n    results.push(res)\n    cursor = res.cursor\n  } while (cursor && results.length < limit)\n  return results\n}\n\nexport async function startServer(app: Express) {\n  return new Promise<{\n    origin: string\n    server: Server\n    stop: () => Promise<void>\n  }>((resolve, reject) => {\n    const onListen = () => {\n      const port = (server.address() as AddressInfo).port\n      resolve({\n        server,\n        origin: `http://localhost:${port}`,\n        stop: () => stopServer(server),\n      })\n      cleanup()\n    }\n    const onError = (err: Error) => {\n      reject(err)\n      cleanup()\n    }\n    const cleanup = () => {\n      server.removeListener('listening', onListen)\n      server.removeListener('error', onError)\n    }\n\n    const server = app\n      .listen(0)\n      .once('listening', onListen)\n      .once('error', onError)\n  })\n}\n\nexport async function stopServer(server: Server) {\n  return new Promise<void>((resolve, reject) => {\n    server.close((err) => {\n      if (err) {\n        reject(err)\n      } else {\n        resolve()\n      }\n    })\n  })\n}\n\nconst normalizeSubjectStatus = (\n  subject: ToolsOzoneModerationDefs.SubjectStatusView,\n) => {\n  return { ...subject, tags: subject.tags?.sort() }\n}\n\nexport const forSubjectStatusSnapshot = (\n  status:\n    | ToolsOzoneModerationDefs.SubjectStatusView\n    | ToolsOzoneModerationDefs.SubjectStatusView[],\n) => {\n  if (Array.isArray(status)) {\n    return forSnapshot(status.map(normalizeSubjectStatus))\n  }\n\n  return forSnapshot(normalizeSubjectStatus(status))\n}\n"
  },
  {
    "path": "packages/pds/tests/account-deactivation.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport {\n  ImageRef,\n  SeedClient,\n  TestNetworkNoAppView,\n  basicSeed,\n} from '@atproto/dev-env'\n\ndescribe('account deactivation', () => {\n  let network: TestNetworkNoAppView\n\n  let sc: SeedClient\n  let agent: AtpAgent\n\n  let alice: string\n  let aliceAvatar: ImageRef\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'account_deactivation',\n    })\n\n    sc = network.getSeedClient()\n    agent = network.pds.getClient()\n\n    await basicSeed(sc)\n    alice = sc.dids.alice\n\n    aliceAvatar = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    await sc.updateProfile(alice, {\n      avatar: aliceAvatar.image,\n    })\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('deactivates account', async () => {\n    await agent.com.atproto.server.deactivateAccount(\n      {},\n      { encoding: 'application/json', headers: sc.getHeaders(alice) },\n    )\n  })\n\n  it('returns deactivated status', async () => {\n    const res = await agent.com.atproto.sync.getRepoStatus({ did: alice })\n    expect(res.data).toEqual({\n      did: alice,\n      active: false,\n      status: 'deactivated',\n    })\n\n    const adminRes = await agent.com.atproto.admin.getAccountInfo(\n      {\n        did: alice,\n      },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(typeof adminRes.data.deactivatedAt).toBeDefined()\n  })\n\n  it('no longer serves repo data', async () => {\n    await expect(\n      agent.com.atproto.sync.getRepo({ did: alice }),\n    ).rejects.toThrow(/Repo has been deactivated/)\n    await expect(\n      agent.com.atproto.sync.getLatestCommit({ did: alice }),\n    ).rejects.toThrow(/Repo has been deactivated/)\n    await expect(\n      agent.com.atproto.sync.listBlobs({ did: alice }),\n    ).rejects.toThrow(/Repo has been deactivated/)\n    const recordUri = sc.posts[alice][0].ref.uri\n    await expect(\n      agent.com.atproto.sync.getRecord({\n        did: alice,\n        collection: recordUri.collection,\n        rkey: recordUri.rkey,\n      }),\n    ).rejects.toThrow(/Repo has been deactivated/)\n    await expect(\n      agent.com.atproto.repo.getRecord({\n        repo: alice,\n        collection: recordUri.collection,\n        rkey: recordUri.rkey,\n      }),\n    ).rejects.toThrow()\n    await expect(\n      agent.com.atproto.repo.describeRepo({\n        repo: alice,\n      }),\n    ).rejects.toThrow(/Repo has been deactivated/)\n\n    await expect(\n      agent.com.atproto.sync.getBlob({\n        did: alice,\n        cid: aliceAvatar.image.ref.toString(),\n      }),\n    ).rejects.toThrow(/Repo has been deactivated/)\n    const listedRepos = await agent.com.atproto.sync.listRepos()\n    const listedAlice = listedRepos.data.repos.find((r) => r.did === alice)\n    expect(listedAlice?.active).toBe(false)\n    expect(listedAlice?.status).toBe('deactivated')\n  })\n\n  it('no longer resolves handle', async () => {\n    await expect(\n      agent.com.atproto.identity.resolveHandle({\n        handle: sc.accounts[alice].handle,\n      }),\n    ).rejects.toThrow()\n  })\n\n  it('still allows login and returns status', async () => {\n    const res = await agent.com.atproto.server.createSession({\n      identifier: alice,\n      password: sc.accounts[alice].password,\n    })\n    expect(res.data.status).toEqual('deactivated')\n  })\n\n  it('returns status on getSession', async () => {\n    const res = await agent.com.atproto.server.getSession(undefined, {\n      headers: sc.getHeaders(alice),\n    })\n    expect(res.data.status).toEqual('deactivated')\n  })\n\n  it('does not allow writes', async () => {\n    const createAttempt = agent.com.atproto.repo.createRecord(\n      {\n        repo: alice,\n        collection: 'app.bsky.feed.post',\n        record: {\n          text: 'blah',\n          createdAt: new Date().toISOString(),\n        },\n      },\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    const uri = sc.posts[alice][0].ref.uri\n    await expect(createAttempt).rejects.toThrow('Account is deactivated')\n\n    const putAttempt = agent.com.atproto.repo.putRecord(\n      {\n        repo: alice,\n        collection: uri.collection,\n        rkey: uri.rkey,\n        record: {\n          text: 'blah',\n          createdAt: new Date().toISOString(),\n        },\n      },\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    await expect(putAttempt).rejects.toThrow('Account is deactivated')\n\n    const deleteAttempt = agent.com.atproto.repo.deleteRecord(\n      {\n        repo: alice,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      },\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    await expect(deleteAttempt).rejects.toThrow('Account is deactivated')\n  })\n\n  it('reactivates', async () => {\n    await agent.com.atproto.server.activateAccount(undefined, {\n      headers: sc.getHeaders(alice),\n    })\n\n    await agent.com.atproto.sync.getRepo({ did: alice })\n\n    const statusRes = await agent.com.atproto.sync.getRepoStatus({ did: alice })\n    expect(statusRes.data.active).toBe(true)\n    expect(statusRes.data.status).toBeUndefined()\n\n    const adminRes = await agent.com.atproto.admin.getAccountInfo(\n      {\n        did: alice,\n      },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(adminRes.data.deactivatedAt).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/account-deletion.test.ts",
    "content": "import { EventEmitter, once } from 'node:events'\nimport { Selectable } from 'kysely'\nimport Mail from 'nodemailer/lib/mailer'\nimport { AtpAgent } from '@atproto/api'\nimport { fileExists } from '@atproto/common'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { BlobNotFoundError } from '@atproto/repo'\nimport { AppContext } from '../src'\nimport {\n  Account,\n  AppPassword,\n  EmailToken,\n  RefreshToken,\n  RepoRoot,\n} from '../src/account-manager/db'\nimport { ServerMailer } from '../src/mailer'\nimport { RepoSeq } from '../src/sequencer/db'\nimport basicSeed from './seeds/basic'\n\ndescribe('account deletion', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let ctx: AppContext\n  let mailer: ServerMailer\n  let initialDbContents: DbContents\n  let updatedDbContents: DbContents\n  const mailCatcher = new EventEmitter()\n  let _origSendMail\n\n  // chose carol because she has blobs\n  let carol\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'account_deletion',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    mailer = ctx.mailer\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    carol = sc.accounts[sc.dids.carol]\n\n    // Catch emails for use in tests\n    _origSendMail = mailer.transporter.sendMail\n    mailer.transporter.sendMail = async (opts) => {\n      const result = await _origSendMail.call(mailer.transporter, opts)\n      mailCatcher.emit('mail', opts)\n      return result\n    }\n\n    initialDbContents = await getDbContents(ctx)\n  })\n\n  afterAll(async () => {\n    mailer.transporter.sendMail = _origSendMail\n    await network.close()\n  })\n\n  const getMailFrom = async (promise): Promise<Mail.Options> => {\n    const result = await Promise.all([once(mailCatcher, 'mail'), promise])\n    return result[0][0]\n  }\n\n  const getTokenFromMail = (mail: Mail.Options) =>\n    mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5})</i)?.[1]\n\n  let token\n\n  it('requests account deletion', async () => {\n    const mail = await getMailFrom(\n      agent.api.com.atproto.server.requestAccountDelete(undefined, {\n        headers: sc.getHeaders(carol.did),\n      }),\n    )\n\n    expect(mail.to).toEqual(carol.email)\n    expect(mail.html).toContain('To permanently delete your account')\n\n    token = getTokenFromMail(mail)\n    if (!token) {\n      return expect(token).toBeDefined()\n    }\n  })\n\n  it('fails account deletion with a bad token', async () => {\n    const attempt = agent.api.com.atproto.server.deleteAccount({\n      token: '123456',\n      did: carol.did,\n      password: carol.password,\n    })\n    await expect(attempt).rejects.toThrow('Token is invalid')\n  })\n\n  it('fails account deletion with a bad password', async () => {\n    const attempt = agent.api.com.atproto.server.deleteAccount({\n      token,\n      did: carol.did,\n      password: 'bad-pass',\n    })\n    await expect(attempt).rejects.toThrow('Invalid did or password')\n  })\n\n  it('deletes account with a valid token & password', async () => {\n    // Perform account deletion, including when the account is already \"taken down\"\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: carol.did,\n        },\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    await agent.api.com.atproto.server.deleteAccount({\n      token,\n      did: carol.did,\n      password: carol.password,\n    })\n    await network.processAll() // Finish background hard-deletions\n  })\n\n  it('no longer lets the user log in', async () => {\n    const attempt = agent.api.com.atproto.server.createSession({\n      identifier: carol.handle,\n      password: carol.password,\n    })\n    await expect(attempt).rejects.toThrow('Invalid identifier or password')\n  })\n\n  it('no longer store the user account or repo', async () => {\n    updatedDbContents = await getDbContents(ctx)\n    expect(updatedDbContents.repoRoots).toEqual(\n      initialDbContents.repoRoots.filter((row) => row.did !== carol.did),\n    )\n    expect(updatedDbContents.userAccounts).toEqual(\n      initialDbContents.userAccounts.filter((row) => row.did !== carol.did),\n    )\n    // check we didn't touch other user seqs\n    expect(\n      updatedDbContents.repoSeqs.filter((row) => row.did !== carol.did),\n    ).toEqual(initialDbContents.repoSeqs.filter((row) => row.did !== carol.did))\n    // check all seqs for this did are gone, except for the tombstone & account events\n    expect(\n      updatedDbContents.repoSeqs\n        .filter((row) => row.did === carol.did)\n        .every((row) => row.eventType === 'account'),\n    ).toBe(true)\n    // check we do have a account (deletion) event for this did\n    expect(\n      updatedDbContents.repoSeqs.filter(\n        (row) => row.did === carol.did && row.eventType === 'account',\n      ).length,\n    ).toEqual(1)\n    expect(updatedDbContents.appPasswords).toEqual(\n      initialDbContents.appPasswords.filter((row) => row.did !== carol.did),\n    )\n    expect(updatedDbContents.emailTokens).toEqual(\n      initialDbContents.emailTokens.filter((row) => row.did !== carol.did),\n    )\n    expect(updatedDbContents.refreshTokens).toEqual(\n      initialDbContents.refreshTokens.filter((row) => row.did !== carol.did),\n    )\n  })\n\n  it('deletes the users actor store', async () => {\n    const carolLoc = await network.pds.ctx.actorStore.getLocation(carol.did)\n    const dbExists = await fileExists(carolLoc.dbLocation)\n    expect(dbExists).toBe(false)\n    const walExists = await fileExists(`${carolLoc.dbLocation}-wal`)\n    expect(walExists).toBe(false)\n    const shmExists = await fileExists(`${carolLoc.dbLocation}-shm`)\n    expect(shmExists).toBe(false)\n  })\n\n  it('deletes relevant blobs', async () => {\n    const imgs = sc.posts[carol.did][0].images\n    const first = imgs[0].image.ref\n    const second = imgs[1].image.ref\n    const blobstore = network.pds.ctx.blobstore(carol.did)\n    const attempt1 = blobstore.getBytes(first)\n    await expect(attempt1).rejects.toThrow(BlobNotFoundError)\n    const attempt2 = blobstore.getBytes(second)\n    await expect(attempt2).rejects.toThrow(BlobNotFoundError)\n  })\n\n  it('maintains blobs from other actors', async () => {\n    const bobBlobstore = network.pds.ctx.blobstore(sc.dids.bob)\n    const [img] = sc.replies[sc.dids.bob][0].images\n    const attempt = bobBlobstore.getBytes(img.image.ref)\n    await expect(attempt).resolves.toBeDefined()\n  })\n\n  it('can delete an empty user', async () => {\n    const eve = await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@test.com',\n      password: 'eve-test',\n    })\n\n    const mail = await getMailFrom(\n      agent.api.com.atproto.server.requestAccountDelete(undefined, {\n        headers: sc.getHeaders(eve.did),\n      }),\n    )\n\n    const token = getTokenFromMail(mail)\n    if (!token) {\n      return expect(token).toBeDefined()\n    }\n    await agent.api.com.atproto.server.deleteAccount({\n      token,\n      did: eve.did,\n      password: eve.password,\n    })\n  })\n\n  it('can be performed by an administrator.', async () => {\n    const ferris = await sc.createAccount('ferris', {\n      handle: 'ferris.test',\n      email: 'ferris@test.com',\n      password: 'ferris-test',\n    })\n\n    const tryUnauthed = agent.api.com.atproto.admin.deleteAccount({\n      did: ferris.did,\n    })\n    await expect(tryUnauthed).rejects.toThrow('Authentication Required')\n\n    const { data: acct } = await agent.api.com.atproto.admin.getAccountInfo(\n      { did: ferris.did },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(acct.did).toBe(ferris.did)\n\n    await agent.api.com.atproto.admin.deleteAccount(\n      { did: ferris.did },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n\n    const tryGetAccountInfo = agent.api.com.atproto.admin.getAccountInfo(\n      { did: ferris.did },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    await expect(tryGetAccountInfo).rejects.toThrow('Account not found')\n  })\n})\n\ntype DbContents = {\n  repoRoots: RepoRoot[]\n  userAccounts: Selectable<Account>[]\n  appPasswords: AppPassword[]\n  emailTokens: EmailToken[]\n  refreshTokens: RefreshToken[]\n  repoSeqs: Selectable<RepoSeq>[]\n}\n\nconst getDbContents = async (ctx: AppContext): Promise<DbContents> => {\n  const { sequencer, accountManager } = ctx\n  const db = accountManager.db\n  const [\n    repoRoots,\n    userAccounts,\n    appPasswords,\n    emailTokens,\n    refreshTokens,\n    repoSeqs,\n  ] = await Promise.all([\n    db.db.selectFrom('repo_root').orderBy('did').selectAll().execute(),\n    db.db.selectFrom('account').orderBy('did').selectAll().execute(),\n    db.db\n      .selectFrom('app_password')\n      .orderBy('did')\n      .orderBy('name')\n      .selectAll()\n      .execute(),\n    db.db.selectFrom('email_token').orderBy('token').selectAll().execute(),\n    db.db.selectFrom('refresh_token').orderBy('id').selectAll().execute(),\n    sequencer.db.db.selectFrom('repo_seq').orderBy('seq').selectAll().execute(),\n  ])\n\n  return {\n    repoRoots,\n    userAccounts,\n    appPasswords,\n    emailTokens,\n    refreshTokens,\n    repoSeqs,\n  }\n}\n"
  },
  {
    "path": "packages/pds/tests/account-migration.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtUri, AtpAgent } from '@atproto/api'\nimport {\n  SeedClient,\n  TestNetworkNoAppView,\n  TestPds,\n  mockNetworkUtilities,\n} from '@atproto/dev-env'\nimport { readCar } from '@atproto/repo'\nimport { ids } from '../src/lexicon/lexicons'\n\ndescribe('account migration', () => {\n  let network: TestNetworkNoAppView\n  let newPds: TestPds\n\n  let sc: SeedClient\n  let oldAgent: AtpAgent\n  let newAgent: AtpAgent\n\n  let alice: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'account_migration',\n    })\n    newPds = await TestPds.create({\n      didPlcUrl: network.plc.url,\n    })\n    mockNetworkUtilities(newPds)\n\n    sc = network.getSeedClient()\n    oldAgent = network.pds.getClient()\n    newAgent = newPds.getClient()\n\n    await sc.createAccount('alice', {\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n    alice = sc.dids.alice\n\n    for (let i = 0; i < 100; i++) {\n      await sc.post(alice, 'test post')\n    }\n    const img1 = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/at.png',\n      'image/png',\n    )\n    const img2 = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-alt.jpg',\n      'image/jpeg',\n    )\n    const img3 = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n\n    await sc.post(alice, 'test', undefined, [img1])\n    await sc.post(alice, 'test', undefined, [img1, img2])\n    await sc.post(alice, 'test', undefined, [img3])\n\n    await network.processAll()\n\n    await oldAgent.login({\n      identifier: sc.accounts[alice].handle,\n      password: sc.accounts[alice].password,\n    })\n  })\n\n  afterAll(async () => {\n    await newPds.close()\n    await network.close()\n  })\n\n  it('migrates an account', async () => {\n    const describeRes = await newAgent.api.com.atproto.server.describeServer()\n    const newServerDid = describeRes.data.did\n\n    const serviceJwtRes = await oldAgent.com.atproto.server.getServiceAuth({\n      aud: newServerDid,\n      lxm: ids.ComAtprotoServerCreateAccount,\n    })\n    const serviceJwt = serviceJwtRes.data.token\n\n    await newAgent.api.com.atproto.server.createAccount(\n      {\n        handle: 'new-alice.test',\n        email: 'alice@test.com',\n        password: 'alice-pass',\n        did: alice,\n      },\n      {\n        headers: { authorization: `Bearer ${serviceJwt}` },\n        encoding: 'application/json',\n      },\n    )\n    await newAgent.login({\n      identifier: 'new-alice.test',\n      password: 'alice-pass',\n    })\n\n    const statusRes1 = await newAgent.com.atproto.server.checkAccountStatus()\n    expect(statusRes1.data).toMatchObject({\n      activated: false,\n      validDid: false,\n      repoBlocks: 2, // commit & empty data root\n      indexedRecords: 0,\n      privateStateValues: 0,\n      expectedBlobs: 0,\n      importedBlobs: 0,\n    })\n\n    const repoRes = await oldAgent.com.atproto.sync.getRepo({ did: alice })\n    const carBlocks = await readCar(repoRes.data)\n\n    await newAgent.com.atproto.repo.importRepo(repoRes.data, {\n      encoding: 'application/vnd.ipld.car',\n    })\n\n    const statusRes2 = await newAgent.com.atproto.server.checkAccountStatus()\n    expect(statusRes2.data).toMatchObject({\n      activated: false,\n      validDid: false,\n      indexedRecords: 103,\n      privateStateValues: 0,\n      expectedBlobs: 3,\n      importedBlobs: 0,\n    })\n    expect(statusRes2.data.repoBlocks).toBe(carBlocks.blocks.size)\n\n    const missingBlobs = await newAgent.com.atproto.repo.listMissingBlobs()\n    expect(missingBlobs.data.blobs.length).toBe(3)\n\n    let blobCursor: string | undefined = undefined\n    do {\n      const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({\n        did: alice,\n        cursor: blobCursor,\n      })\n      for (const cid of listedBlobs.data.cids) {\n        const blobRes = await oldAgent.com.atproto.sync.getBlob({\n          did: alice,\n          cid,\n        })\n        await newAgent.com.atproto.repo.uploadBlob(blobRes.data, {\n          encoding: blobRes.headers['content-type'],\n        })\n      }\n      blobCursor = listedBlobs.data.cursor\n    } while (blobCursor)\n\n    const statusRes3 = await newAgent.com.atproto.server.checkAccountStatus()\n    expect(statusRes3.data.expectedBlobs).toBe(3)\n    expect(statusRes3.data.importedBlobs).toBe(3)\n\n    const prefs = await oldAgent.api.app.bsky.actor.getPreferences()\n    await newAgent.api.app.bsky.actor.putPreferences(prefs.data)\n\n    const getDidCredentials =\n      await newAgent.com.atproto.identity.getRecommendedDidCredentials()\n\n    await oldAgent.com.atproto.identity.requestPlcOperationSignature()\n    const res = await network.pds.ctx.accountManager.db.db\n      .selectFrom('email_token')\n      .selectAll()\n      .where('did', '=', alice)\n      .where('purpose', '=', 'plc_operation')\n      .executeTakeFirst()\n    const token = res?.token\n    assert(token)\n\n    const plcOp = await oldAgent.com.atproto.identity.signPlcOperation({\n      token,\n      ...getDidCredentials.data,\n    })\n\n    await newAgent.com.atproto.identity.submitPlcOperation({\n      operation: plcOp.data.operation,\n    })\n\n    await newAgent.com.atproto.server.activateAccount()\n\n    const statusRes4 = await newAgent.com.atproto.server.checkAccountStatus()\n    expect(statusRes4.data).toMatchObject({\n      activated: true,\n      validDid: true,\n      indexedRecords: 103,\n      privateStateValues: 0,\n      expectedBlobs: 3,\n      importedBlobs: 3,\n    })\n\n    await oldAgent.com.atproto.server.deactivateAccount({})\n\n    const statusResOldPds =\n      await oldAgent.com.atproto.server.checkAccountStatus()\n    expect(statusResOldPds.data).toMatchObject({\n      activated: false,\n      validDid: false,\n    })\n\n    const postRes = await newAgent.api.app.bsky.feed.post.create(\n      { repo: alice },\n      {\n        text: 'new pds!',\n        createdAt: new Date().toISOString(),\n      },\n    )\n    const postUri = new AtUri(postRes.uri)\n    const fetchedPost = await newAgent.api.app.bsky.feed.post.get({\n      repo: postUri.hostname,\n      rkey: postUri.rkey,\n    })\n    expect(fetchedPost.value.text).toEqual('new pds!')\n    const statusRes5 = await newAgent.com.atproto.server.checkAccountStatus()\n    expect(statusRes5.data.indexedRecords).toBe(104)\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/account.test.ts",
    "content": "import { EventEmitter, once } from 'node:events'\nimport Mail from 'nodemailer/lib/mailer'\nimport { AtpAgent, ComAtprotoServerResetPassword } from '@atproto/api'\nimport * as crypto from '@atproto/crypto'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { IdResolver } from '@atproto/identity'\nimport { AppContext } from '../src'\nimport { ServerMailer } from '../src/mailer'\n\nconst email = 'alice@test.com'\nconst handle = 'alice.test'\nconst password = 'test123'\nconst passwordAlt = 'test456'\nconst minsToMs = 60 * 1000\n\ndescribe('account', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let agent: AtpAgent\n  let mailer: ServerMailer\n  let idResolver: IdResolver\n  const mailCatcher = new EventEmitter()\n  let _origSendMail\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'account',\n      pds: {\n        contactEmailAddress: 'abuse@example.com',\n        termsOfServiceUrl: 'https://example.com/tos',\n        privacyPolicyUrl: 'https://example.com/privacy-policy',\n      },\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    mailer = network.pds.ctx.mailer\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    idResolver = network.pds.ctx.idResolver\n    agent = network.pds.getClient()\n\n    // Catch emails for use in tests\n    _origSendMail = mailer.transporter.sendMail\n    mailer.transporter.sendMail = async (opts) => {\n      const result = await _origSendMail.call(mailer.transporter, opts)\n      mailCatcher.emit('mail', opts)\n      return result\n    }\n  })\n\n  afterAll(async () => {\n    mailer.transporter.sendMail = _origSendMail\n    await network.close()\n  })\n\n  it('serves the accounts system config', async () => {\n    const res = await agent.api.com.atproto.server.describeServer({})\n    expect(res.data.inviteCodeRequired).toBe(false)\n    expect(res.data.availableUserDomains[0]).toBe('.test')\n    expect(typeof res.data.inviteCodeRequired).toBe('boolean')\n    expect(res.data.links?.privacyPolicy).toBe(\n      'https://example.com/privacy-policy',\n    )\n    expect(res.data.links?.termsOfService).toBe('https://example.com/tos')\n    expect(res.data.contact?.email).toBe('abuse@example.com')\n  })\n\n  it('fails on invalid handles', async () => {\n    const promise = agent.api.com.atproto.server.createAccount({\n      email: 'bad-handle@test.com',\n      handle: 'did:bad-handle.test',\n      password: 'asdf',\n    })\n    await expect(promise).rejects.toThrow('Input/handle must be a valid handle')\n  })\n\n  describe('email validation', () => {\n    it('succeeds on allowed emails', async () => {\n      const promise = agent.api.com.atproto.server.createAccount({\n        email: 'ok-email@gmail.com',\n        handle: 'ok-email.test',\n        password: 'asdf',\n      })\n      await expect(promise).resolves.toBeTruthy()\n    })\n\n    it('fails on disallowed emails', async () => {\n      const promise = agent.api.com.atproto.server.createAccount({\n        email: 'bad-email@disposeamail.com',\n        handle: 'bad-email.test',\n        password: 'asdf',\n      })\n      await expect(promise).rejects.toThrow(\n        'This email address is not supported, please use a different email.',\n      )\n    })\n  })\n\n  let did: string\n  let jwt: string\n\n  it('creates an account', async () => {\n    const res = await agent.api.com.atproto.server.createAccount({\n      email,\n      handle,\n      password,\n    })\n    did = res.data.did\n    jwt = res.data.accessJwt\n\n    expect(typeof jwt).toBe('string')\n    expect(did.startsWith('did:plc:')).toBeTruthy()\n    expect(res.data.handle).toEqual(handle)\n  })\n\n  it('generates a properly formatted PLC DID', async () => {\n    const didData = await idResolver.did.resolveAtprotoData(did)\n    const signingKey = await network.pds.ctx.actorStore.keypair(did)\n\n    expect(didData.did).toBe(did)\n    expect(didData.handle).toBe(handle)\n    expect(didData.signingKey).toBe(signingKey.did())\n    expect(didData.pds).toBe(network.pds.url)\n  })\n\n  it('allows a custom set recovery key', async () => {\n    const recoveryKey = (await crypto.P256Keypair.create()).did()\n    const res = await agent.api.com.atproto.server.createAccount({\n      email: 'custom-recovery@test.com',\n      handle: 'custom-recovery.test',\n      password: 'custom-recovery',\n      recoveryKey,\n    })\n\n    const didData = await ctx.plcClient.getDocumentData(res.data.did)\n\n    expect(didData.rotationKeys).toEqual([\n      recoveryKey,\n      ctx.cfg.identity.recoveryDidKey,\n      ctx.plcRotationKey.did(),\n    ])\n  })\n\n  // @NOTE currently disabled until we allow a user to resver a keypair before migration\n  // it('allows a user to bring their own DID', async () => {\n  //   const userKey = await crypto.Secp256k1Keypair.create()\n  //   const handle = 'byo-did.test'\n  //   const did = await ctx.plcClient.createDid({\n  //     signingKey: ctx.repoSigningKey.did(),\n  //     handle,\n  //     rotationKeys: [\n  //       userKey.did(),\n  //       ctx.cfg.identity.recoveryDidKey ?? '',\n  //       ctx.plcRotationKey.did(),\n  //     ],\n  //     pds: network.pds.url,\n  //     signer: userKey,\n  //   })\n\n  //   const res = await agent.api.com.atproto.server.createAccount({\n  //     email: 'byo-did@test.com',\n  //     handle,\n  //     did,\n  //     password: 'byo-did-pass',\n  //   })\n\n  //   expect(res.data.handle).toEqual(handle)\n  //   expect(res.data.did).toEqual(did)\n  // })\n\n  // it('requires that the did a user brought be correctly set up for the server', async () => {\n  //   const userKey = await crypto.Secp256k1Keypair.create()\n  //   const baseDidInfo = {\n  //     signingKey: ctx.repoSigningKey.did(),\n  //     handle: 'byo-did.test',\n  //     rotationKeys: [\n  //       userKey.did(),\n  //       ctx.cfg.identity.recoveryDidKey ?? '',\n  //       ctx.plcRotationKey.did(),\n  //     ],\n  //     pds: ctx.cfg.service.publicUrl,\n  //     signer: userKey,\n  //   }\n  //   const baseAccntInfo = {\n  //     email: 'byo-did@test.com',\n  //     handle: 'byo-did.test',\n  //     password: 'byo-did-pass',\n  //   }\n\n  //   const did1 = await ctx.plcClient.createDid({\n  //     ...baseDidInfo,\n  //     handle: 'different-handle.test',\n  //   })\n  //   const attempt1 = agent.api.com.atproto.server.createAccount({\n  //     ...baseAccntInfo,\n  //     did: did1,\n  //   })\n  //   await expect(attempt1).rejects.toThrow(\n  //     'provided handle does not match DID document handle',\n  //   )\n\n  //   const did2 = await ctx.plcClient.createDid({\n  //     ...baseDidInfo,\n  //     pds: 'https://other-pds.com',\n  //   })\n  //   const attempt2 = agent.api.com.atproto.server.createAccount({\n  //     ...baseAccntInfo,\n  //     did: did2,\n  //   })\n  //   await expect(attempt2).rejects.toThrow(\n  //     'DID document pds endpoint does not match service endpoint',\n  //   )\n\n  //   const did3 = await ctx.plcClient.createDid({\n  //     ...baseDidInfo,\n  //     rotationKeys: [userKey.did()],\n  //   })\n  //   const attempt3 = agent.api.com.atproto.server.createAccount({\n  //     ...baseAccntInfo,\n  //     did: did3,\n  //   })\n  //   await expect(attempt3).rejects.toThrow(\n  //     'PLC DID does not include service rotation key',\n  //   )\n\n  //   const did4 = await ctx.plcClient.createDid({\n  //     ...baseDidInfo,\n  //     signingKey: userKey.did(),\n  //   })\n  //   const attempt4 = agent.api.com.atproto.server.createAccount({\n  //     ...baseAccntInfo,\n  //     did: did4,\n  //   })\n  //   await expect(attempt4).rejects.toThrow(\n  //     'DID document signing key does not match service signing key',\n  //   )\n  // })\n\n  it('allows administrative email updates', async () => {\n    await agent.api.com.atproto.admin.updateAccountEmail(\n      {\n        account: handle,\n        email: 'alIce-NEw@teST.com',\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n\n    const accnt = await ctx.accountManager.getAccount(handle)\n    expect(accnt?.email).toBe('alice-new@test.com')\n\n    await agent.api.com.atproto.admin.updateAccountEmail(\n      {\n        account: did,\n        email,\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n\n    const accnt2 = await ctx.accountManager.getAccount(handle)\n    expect(accnt2?.email).toBe(email)\n  })\n\n  it('disallows duplicate email addresses and handles', async () => {\n    const email = 'bob@test.com'\n    const handle = 'bob.test'\n    const password = 'test123'\n    await agent.api.com.atproto.server.createAccount({\n      email,\n      handle,\n      password,\n    })\n\n    await expect(\n      agent.api.com.atproto.server.createAccount({\n        email: email.toUpperCase(),\n        handle: 'carol.test',\n        password,\n      }),\n    ).rejects.toThrow('Email already taken: BOB@TEST.COM')\n\n    await expect(\n      agent.api.com.atproto.server.createAccount({\n        email: 'carol@test.com',\n        handle: handle.toUpperCase(),\n        password,\n      }),\n    ).rejects.toThrow('Handle already taken: bob.test')\n  })\n\n  it('disallows improperly formatted handles', async () => {\n    const tryHandle = async (handle: string) => {\n      await agent.api.com.atproto.server.createAccount({\n        email: 'john@test.com',\n        handle,\n        password: 'test123',\n      })\n    }\n    await expect(tryHandle('did:john')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('john.bsky.io')).rejects.toThrow(\n      'Not a supported handle domain',\n    )\n    await expect(tryHandle('j.test')).rejects.toThrow('Handle too short')\n    await expect(tryHandle('jayromy-johnber12345678910.test')).rejects.toThrow(\n      'Handle too long',\n    )\n    await expect(tryHandle('jo_hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo!hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo%hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo&hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo*hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo|hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo:hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo/hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('about.test')).rejects.toThrow('Reserved handle')\n    await expect(tryHandle('atp.test')).rejects.toThrow('Reserved handle')\n  })\n\n  it('handles racing signups for same handle', async () => {\n    const COUNT = 10\n\n    let successes = 0\n    let failures = 0\n    const promises: Promise<unknown>[] = []\n    for (let i = 0; i < COUNT; i++) {\n      const attempt = async () => {\n        try {\n          await agent.api.com.atproto.server.createAccount({\n            email: `matching@test.com`,\n            handle: `matching.test`,\n            password: `password`,\n          })\n          successes++\n        } catch (err) {\n          failures++\n        }\n      }\n      promises.push(attempt())\n    }\n    await Promise.all(promises)\n    expect(successes).toBe(1)\n    expect(failures).toBe(9)\n  })\n\n  it('fails on unauthenticated requests', async () => {\n    await expect(agent.api.com.atproto.server.getSession({})).rejects.toThrow()\n  })\n\n  it('logs in', async () => {\n    const res = await agent.api.com.atproto.server.createSession({\n      identifier: handle,\n      password,\n    })\n    jwt = res.data.accessJwt\n    expect(typeof jwt).toBe('string')\n    expect(res.data.handle).toBe('alice.test')\n    expect(res.data.did).toBe(did)\n    expect(res.data.email).toBe(email)\n  })\n\n  it('can perform authenticated requests', async () => {\n    // @TODO each test should be able to run independently & concurrently\n    agent.api.setHeader('authorization', `Bearer ${jwt}`)\n    const res = await agent.api.com.atproto.server.getSession({})\n    expect(res.data.did).toBe(did)\n    expect(res.data.handle).toBe(handle)\n    expect(res.data.email).toBe(email)\n  })\n\n  const getMailFrom = async (promise): Promise<Mail.Options> => {\n    const result = await Promise.all([once(mailCatcher, 'mail'), promise])\n    return result[0][0]\n  }\n\n  const getTokenFromMail = (mail: Mail.Options) =>\n    mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5})</i)?.[1]\n\n  it('can reset account password', async () => {\n    const mail = await getMailFrom(\n      agent.api.com.atproto.server.requestPasswordReset({ email }),\n    )\n\n    expect(mail.to).toEqual(email)\n    expect(mail.html).toContain('Reset password')\n    expect(mail.html).toContain('alice.test')\n\n    const token = getTokenFromMail(mail)\n\n    if (token === undefined) {\n      return expect(token).toBeDefined()\n    }\n\n    await agent.api.com.atproto.server.resetPassword({\n      token,\n      password: passwordAlt,\n    })\n\n    // Logs in with new password and not previous password\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: handle,\n        password,\n      }),\n    ).rejects.toThrow('Invalid identifier or password')\n\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: handle,\n        password: passwordAlt,\n      }),\n    ).resolves.toBeDefined()\n  })\n\n  it('allows only single-use of password reset token', async () => {\n    const mail = await getMailFrom(\n      agent.api.com.atproto.server.requestPasswordReset({ email }),\n    )\n\n    const token = getTokenFromMail(mail)\n\n    if (token === undefined) {\n      return expect(token).toBeDefined()\n    }\n\n    // Reset back from passwordAlt to password\n    await agent.api.com.atproto.server.resetPassword({ token, password })\n\n    // Reuse of token fails\n    await expect(\n      agent.api.com.atproto.server.resetPassword({ token, password }),\n    ).rejects.toThrow(ComAtprotoServerResetPassword.InvalidTokenError)\n\n    // Logs in with new password and not previous password\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: handle,\n        password: passwordAlt,\n      }),\n    ).rejects.toThrow('Invalid identifier or password')\n\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: handle,\n        password,\n      }),\n    ).resolves.toBeDefined()\n  })\n\n  it('changing password invalidates past refresh tokens', async () => {\n    const mail = await getMailFrom(\n      agent.api.com.atproto.server.requestPasswordReset({ email }),\n    )\n\n    expect(mail.to).toEqual(email)\n    expect(mail.html).toContain('Reset password')\n    expect(mail.html).toContain('alice.test')\n\n    const token = getTokenFromMail(mail)\n\n    if (token === undefined) {\n      return expect(token).toBeDefined()\n    }\n\n    const session = await agent.api.com.atproto.server.createSession({\n      identifier: handle,\n      password,\n    })\n\n    await agent.api.com.atproto.server.resetPassword({\n      token: token.toLowerCase(), // Reset should work case-insensitively\n      password,\n    })\n\n    await expect(\n      agent.api.com.atproto.server.refreshSession(undefined, {\n        headers: { authorization: `Bearer ${session.data.refreshJwt}` },\n      }),\n    ).rejects.toThrow('Token has been revoked')\n  })\n\n  it('allows only unexpired password reset tokens', async () => {\n    await agent.api.com.atproto.server.requestPasswordReset({ email })\n\n    const res = await ctx.accountManager.db.db\n      .updateTable('email_token')\n      .where('purpose', '=', 'reset_password')\n      .where('did', '=', did)\n      .set({\n        requestedAt: new Date(Date.now() - 16 * minsToMs).toISOString(),\n      })\n      .returning(['token'])\n      .executeTakeFirst()\n    if (!res?.token) {\n      throw new Error('Missing reset token')\n    }\n\n    // Use of expired token fails\n    await expect(\n      agent.api.com.atproto.server.resetPassword({\n        token: res.token,\n        password: passwordAlt,\n      }),\n    ).rejects.toThrow(ComAtprotoServerResetPassword.ExpiredTokenError)\n\n    // Still logs in with previous password\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: handle,\n        password: passwordAlt,\n      }),\n    ).rejects.toThrow('Invalid identifier or password')\n\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: handle,\n        password,\n      }),\n    ).resolves.toBeDefined()\n  })\n\n  it('allows an admin to update password', async () => {\n    const tryUnauthed = agent.api.com.atproto.admin.updateAccountPassword({\n      did,\n      password: 'new-admin-pass',\n    })\n    await expect(tryUnauthed).rejects.toThrow('Authentication Required')\n\n    await agent.api.com.atproto.admin.updateAccountPassword(\n      { did, password: 'new-admin-password' },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n\n    // old password fails\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: did,\n        password,\n      }),\n    ).rejects.toThrow('Invalid identifier or password')\n\n    await expect(\n      agent.api.com.atproto.server.createSession({\n        identifier: did,\n        password: 'new-admin-password',\n      }),\n    ).resolves.toBeDefined()\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/app-passwords.test.ts",
    "content": "import * as jose from 'jose'\nimport { AtpAgent } from '@atproto/api'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\n\ndescribe('app_passwords', () => {\n  let network: TestNetworkNoAppView\n  let accntAgent: AtpAgent\n  let appAgent: AtpAgent\n  let priviAgent: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'app_passwords',\n    })\n    accntAgent = network.pds.getClient()\n    appAgent = network.pds.getClient()\n    priviAgent = network.pds.getClient()\n\n    await accntAgent.createAccount({\n      handle: 'alice.test',\n      email: 'alice@test.com',\n      password: 'alice-pass',\n    })\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  let appPass: string\n  let privilegedAppPass: string\n\n  it('creates an app-specific password', async () => {\n    const res = await accntAgent.api.com.atproto.server.createAppPassword({\n      name: 'test-pass',\n    })\n    expect(res.data.name).toBe('test-pass')\n    expect(res.data.privileged).toBe(false)\n    appPass = res.data.password\n  })\n\n  it('creates a privileged app-specific password', async () => {\n    const res = await accntAgent.api.com.atproto.server.createAppPassword({\n      name: 'privi-pass',\n      privileged: true,\n    })\n    expect(res.data.name).toBe('privi-pass')\n    expect(res.data.privileged).toBe(true)\n    privilegedAppPass = res.data.password\n  })\n\n  it('creates a session with an app-specific password', async () => {\n    const res1 = await appAgent.login({\n      identifier: 'alice.test',\n      password: appPass,\n    })\n    expect(res1.data.did).toEqual(accntAgent.session?.did)\n    const res2 = await priviAgent.login({\n      identifier: 'alice.test',\n      password: privilegedAppPass,\n    })\n    expect(res2.data.did).toEqual(accntAgent.session?.did)\n  })\n\n  it('creates an access token for an app with a restricted scope', () => {\n    const decoded = jose.decodeJwt(appAgent.session?.accessJwt ?? '')\n    expect(decoded?.scope).toEqual('com.atproto.appPass')\n\n    const decodedPrivi = jose.decodeJwt(priviAgent.session?.accessJwt ?? '')\n    expect(decodedPrivi?.scope).toEqual('com.atproto.appPassPrivileged')\n  })\n\n  it('allows actions to be performed from app', async () => {\n    await appAgent.api.app.bsky.feed.post.create(\n      {\n        repo: appAgent.assertDid,\n      },\n      {\n        text: 'Testing testing',\n        createdAt: new Date().toISOString(),\n      },\n    )\n    await priviAgent.api.app.bsky.feed.post.create(\n      {\n        repo: priviAgent.assertDid,\n      },\n      {\n        text: 'testing again',\n        createdAt: new Date().toISOString(),\n      },\n    )\n  })\n\n  it('restricts full access actions', async () => {\n    const attempt1 = appAgent.api.com.atproto.server.createAppPassword({\n      name: 'another-one',\n    })\n    await expect(attempt1).rejects.toThrow('Bad token scope')\n    const attempt2 = priviAgent.api.com.atproto.server.createAppPassword({\n      name: 'another-one',\n    })\n    await expect(attempt2).rejects.toThrow('Bad token scope')\n  })\n\n  it('restricts privileged app password actions', async () => {\n    const attempt = appAgent.api.chat.bsky.convo.listConvos({})\n    await expect(attempt).rejects.toThrow('Bad token method')\n  })\n\n  it('restricts privileged app password actions', async () => {\n    const attempt = appAgent.api.chat.bsky.convo.listConvos()\n    await expect(attempt).rejects.toThrow('Bad token method')\n  })\n\n  it('restricts service auth token methods for non-privileged access tokens', async () => {\n    const attemptCaseSensitive = appAgent.api.com.atproto.server.getServiceAuth(\n      {\n        aud: 'did:example:test',\n        lxm: 'com.atproto.server.createAccount',\n      },\n    )\n    await expect(attemptCaseSensitive).rejects.toThrow(\n      /insufficient access to request a service auth token for the following method/,\n    )\n    const attemptCaseInsensitive =\n      appAgent.api.com.atproto.server.getServiceAuth({\n        aud: 'did:example:test',\n        lxm: 'com.atproto.server.createaccount',\n      })\n    await expect(attemptCaseInsensitive).rejects.toThrow(\n      /insufficient access to request a service auth token for the following method/,\n    )\n  })\n\n  it('allows privileged service auth token scopes for privileged access tokens', async () => {\n    await priviAgent.api.com.atproto.server.getServiceAuth({\n      aud: 'did:example:test',\n      lxm: 'com.atproto.server.createAccount',\n    })\n  })\n\n  it('persists scope across refreshes', async () => {\n    const session = await appAgent.api.com.atproto.server.refreshSession(\n      undefined,\n      {\n        headers: {\n          authorization: `Bearer ${appAgent.session?.refreshJwt}`,\n        },\n      },\n    )\n\n    // allows any access auth\n    await appAgent.api.app.bsky.feed.post.create(\n      {\n        repo: appAgent.assertDid,\n      },\n      {\n        text: 'Testing testing',\n        createdAt: new Date().toISOString(),\n      },\n      {\n        authorization: `Bearer ${session.data.accessJwt}`,\n      },\n    )\n\n    // allows privileged app passwords or higher\n    const priviAttempt = appAgent.api.com.atproto.server.getServiceAuth({\n      aud: 'did:example:test',\n      lxm: 'com.atproto.server.createAccount',\n    })\n    await expect(priviAttempt).rejects.toThrow(\n      /insufficient access to request a service auth token for the following method/,\n    )\n\n    // allows only full access auth\n    const fullAttempt = appAgent.api.com.atproto.server.createAppPassword(\n      {\n        name: 'another-one',\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: `Bearer ${session.data.accessJwt}` },\n      },\n    )\n    await expect(fullAttempt).rejects.toThrow('Bad token scope')\n  })\n\n  it('persists privileged scope across refreshes', async () => {\n    const session = await priviAgent.api.com.atproto.server.refreshSession(\n      undefined,\n      {\n        headers: {\n          authorization: `Bearer ${priviAgent.session?.refreshJwt}`,\n        },\n      },\n    )\n\n    // allows any access auth\n    await priviAgent.api.app.bsky.feed.post.create(\n      {\n        repo: priviAgent.assertDid,\n      },\n      {\n        text: 'Testing testing',\n        createdAt: new Date().toISOString(),\n      },\n      {\n        authorization: `Bearer ${session.data.accessJwt}`,\n      },\n    )\n\n    // allows privileged app passwords or higher\n    await priviAgent.api.com.atproto.server.getServiceAuth({\n      aud: 'did:example:test',\n    })\n\n    // allows only full access auth\n    const attempt = priviAgent.api.com.atproto.server.createAppPassword(\n      {\n        name: 'another-one',\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: `Bearer ${session.data.accessJwt}` },\n      },\n    )\n    await expect(attempt).rejects.toThrow('Bad token scope')\n  })\n\n  it('lists available app-specific passwords', async () => {\n    const res = await appAgent.api.com.atproto.server.listAppPasswords()\n    expect(res.data.passwords.length).toBe(2)\n    expect(res.data.passwords[0].name).toEqual('privi-pass')\n    expect(res.data.passwords[0].privileged).toEqual(true)\n    expect(res.data.passwords[1].name).toEqual('test-pass')\n    expect(res.data.passwords[1].privileged).toEqual(false)\n  })\n\n  it('revokes an app-specific password', async () => {\n    await appAgent.api.com.atproto.server.revokeAppPassword({\n      name: 'test-pass',\n    })\n  })\n\n  it('no longer allows session refresh after revocation', async () => {\n    const attempt = appAgent.api.com.atproto.server.refreshSession(undefined, {\n      headers: {\n        authorization: `Bearer ${appAgent.session?.refreshJwt}`,\n      },\n    })\n    await expect(attempt).rejects.toThrow('Token has been revoked')\n  })\n\n  it('no longer allows session creation after revocation', async () => {\n    const newAgent = network.pds.getClient()\n    const attempt = newAgent.login({\n      identifier: 'alice.test',\n      password: appPass,\n    })\n    await expect(attempt).rejects.toThrow('Invalid identifier or password')\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/auth.test.ts",
    "content": "import * as jose from 'jose'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { createRefreshToken } from '../src/account-manager/helpers/auth'\n\ndescribe('auth', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'auth',\n    })\n    agent = network.pds.getClient()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const createAccount = async (info) => {\n    const { data } = await agent.com.atproto.server.createAccount(info)\n    return data\n  }\n  const getSession = async (jwt) => {\n    const { data } = await agent.com.atproto.server.getSession(\n      {},\n      {\n        headers: SeedClient.getHeaders(jwt),\n      },\n    )\n    return data\n  }\n  const createSession = async (info) => {\n    const { data } = await agent.com.atproto.server.createSession(info)\n    return data\n  }\n  const deleteSession = async (jwt) => {\n    await agent.com.atproto.server.deleteSession(undefined, {\n      headers: SeedClient.getHeaders(jwt),\n    })\n  }\n  const refreshSession = async (jwt: string) => {\n    const { data } = await agent.com.atproto.server.refreshSession(undefined, {\n      headers: SeedClient.getHeaders(jwt),\n    })\n    return data\n  }\n\n  it('provides valid access and refresh token on account creation.', async () => {\n    const email = 'alice@test.com'\n    const account = await createAccount({\n      handle: 'alice.test',\n      email,\n      password: 'password',\n    })\n    // Valid access token\n    const sessionInfo = await getSession(account.accessJwt)\n    expect(sessionInfo).toEqual({\n      did: account.did,\n      handle: account.handle,\n      email,\n      emailConfirmed: false,\n      active: true,\n    })\n    // Valid refresh token\n    const nextSession = await refreshSession(account.refreshJwt)\n    expect(nextSession).toEqual(\n      expect.objectContaining({\n        did: account.did,\n        handle: account.handle,\n      }),\n    )\n  })\n\n  it('provides valid access and refresh token on session creation.', async () => {\n    const email = 'bob@test.com'\n    await createAccount({\n      handle: 'bob.test',\n      email,\n      password: 'password',\n    })\n    const session = await createSession({\n      identifier: 'bob.test',\n      password: 'password',\n    })\n    // Valid access token\n    const sessionInfo = await getSession(session.accessJwt)\n    expect(sessionInfo).toEqual({\n      did: session.did,\n      handle: session.handle,\n      email,\n      emailConfirmed: false,\n      active: true,\n    })\n    // Valid refresh token\n    const nextSession = await refreshSession(session.refreshJwt)\n    expect(nextSession).toEqual(\n      expect.objectContaining({\n        did: session.did,\n        handle: session.handle,\n      }),\n    )\n  })\n\n  it('allows session creation using email address.', async () => {\n    const session = await createSession({\n      identifier: 'bob@TEST.com',\n      password: 'password',\n    })\n    expect(session.handle).toEqual('bob.test')\n  })\n\n  it('fails on session creation with a bad password.', async () => {\n    const sessionPromise = createSession({\n      identifier: 'bob.test',\n      password: 'wrong-pass',\n    })\n    await expect(sessionPromise).rejects.toThrow(\n      'Invalid identifier or password',\n    )\n  })\n\n  it('provides valid access and refresh token on session refresh.', async () => {\n    const email = 'carol@test.com'\n    const account = await createAccount({\n      handle: 'carol.test',\n      password: 'password',\n      email,\n    })\n    const session = await refreshSession(account.refreshJwt)\n    // Valid access token\n    const sessionInfo = await getSession(session.accessJwt)\n    expect(sessionInfo).toEqual({\n      did: session.did,\n      handle: session.handle,\n      email,\n      emailConfirmed: false,\n      active: true,\n    })\n    // Valid refresh token\n    const nextSession = await refreshSession(session.refreshJwt)\n    expect(nextSession).toEqual(\n      expect.objectContaining({\n        did: session.did,\n        handle: session.handle,\n      }),\n    )\n  })\n\n  it('handles racing refreshes', async () => {\n    const email = 'dan@test.com'\n    const account = await createAccount({\n      handle: 'dan.test',\n      password: 'password',\n      email,\n    })\n    const tokenIdPromises: Promise<string>[] = []\n    const doRefresh = async () => {\n      const res = await refreshSession(account.refreshJwt)\n      const decoded = jose.decodeJwt(res.refreshJwt)\n      if (!decoded?.jti) {\n        throw new Error('undefined jti on refresh token')\n      }\n      return decoded.jti\n    }\n    for (let i = 0; i < 10; i++) {\n      tokenIdPromises.push(doRefresh())\n    }\n    const tokenIds = await Promise.all(tokenIdPromises)\n    for (let i = 0; i < 10; i++) {\n      expect(tokenIds[i]).toEqual(tokenIds[0])\n    }\n  })\n\n  it('refresh token provides new token with same id on multiple uses during grace period.', async () => {\n    const account = await createAccount({\n      handle: 'eve.test',\n      email: 'eve@test.com',\n      password: 'password',\n    })\n    const refresh1 = await refreshSession(account.refreshJwt)\n    const refresh2 = await refreshSession(account.refreshJwt)\n\n    const token0 = jose.decodeJwt(account.refreshJwt)\n    const token1 = jose.decodeJwt(refresh1.refreshJwt)\n    const token2 = jose.decodeJwt(refresh2.refreshJwt)\n\n    expect(typeof token1?.jti).toEqual('string')\n    expect(token1?.jti).toEqual(token2?.jti)\n    expect(token1?.jti).not.toEqual(token0?.jti)\n    expect(token2?.jti).not.toEqual(token0?.jti)\n  })\n\n  it('refresh token is revoked after grace period completes.', async () => {\n    const { db } = network.pds.ctx.accountManager\n    const account = await createAccount({\n      handle: 'evan.test',\n      email: 'evan@test.com',\n      password: 'password',\n    })\n    await refreshSession(account.refreshJwt)\n    const token = jose.decodeJwt(account.refreshJwt)\n\n    // Update expiration (i.e. grace period) to end immediately\n    const refreshUpdated = await db.db\n      .updateTable('refresh_token')\n      .set({ expiresAt: new Date().toISOString() })\n      .where('id', '=', token?.jti ?? '')\n      .executeTakeFirst()\n    expect(Number(refreshUpdated.numUpdatedRows)).toEqual(1)\n\n    // Token can no longer be used\n    const refreshAgain = refreshSession(account.refreshJwt)\n    await expect(refreshAgain).rejects.toThrow('Token has been revoked')\n\n    // Ensure that token was cleaned-up\n    const refreshInfo = await db.db\n      .selectFrom('refresh_token')\n      .selectAll()\n      .where('id', '=', token?.jti ?? '')\n      .executeTakeFirst()\n    expect(refreshInfo).toBeUndefined()\n  })\n\n  it('refresh token is revoked when session is deleted.', async () => {\n    const account = await createAccount({\n      handle: 'finn.test',\n      email: 'finn@test.com',\n      password: 'password',\n    })\n    await deleteSession(account.refreshJwt)\n    const refreshDeleted = refreshSession(account.refreshJwt)\n    await expect(refreshDeleted).rejects.toThrow('Token has been revoked')\n    await deleteSession(account.refreshJwt) // No problem double-revoking a token\n  })\n\n  it('access token cannot be used to refresh a session.', async () => {\n    const account = await createAccount({\n      handle: 'gordon.test',\n      email: 'gordon@test.com',\n      password: 'password',\n    })\n    const refreshWithAccess = refreshSession(account.accessJwt)\n    await expect(refreshWithAccess).rejects.toThrow('Invalid token type')\n  })\n\n  it('expired refresh token cannot be used to refresh a session.', async () => {\n    const account = await createAccount({\n      handle: 'holga.test',\n      email: 'holga@test.com',\n      password: 'password',\n    })\n    const refreshJwt = await createRefreshToken({\n      did: account.did,\n      jwtKey: network.pds.jwtSecretKey(),\n      serviceDid: network.pds.ctx.cfg.service.did,\n      expiresIn: -1,\n    })\n    const refreshExpired = refreshSession(refreshJwt)\n    await expect(refreshExpired).rejects.toThrow('Token has expired')\n    await deleteSession(refreshJwt) // No problem revoking an expired token\n  })\n\n  it('actor takedown disallows fresh session.', async () => {\n    const account = await createAccount({\n      handle: 'iris.test',\n      email: 'iris@test.com',\n      password: 'password',\n    })\n    await agent.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: account.did,\n        },\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n    await expect(\n      createSession({ identifier: 'iris.test', password: 'password' }),\n    ).rejects.toMatchObject({\n      error: 'AccountTakedown',\n    })\n  })\n\n  it('actor takedown disallows refresh session.', async () => {\n    const account = await createAccount({\n      handle: 'jared.test',\n      email: 'jared@test.com',\n      password: 'password',\n    })\n    await agent.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: account.did,\n        },\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n    await expect(refreshSession(account.refreshJwt)).rejects.toMatchObject({\n      error: 'AccountTakedown',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/blob-deletes.test.ts",
    "content": "import { AtpAgent, BlobRef } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { AppContext } from '../src'\nimport { ids } from '../src/lexicon/lexicons'\n\ndescribe('blob deletes', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let ctx: AppContext\n\n  let alice: string\n  let bob: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'blob_deletes',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await sc.createAccount('alice', {\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n    await sc.createAccount('bob', {\n      email: 'bob@test.com',\n      handle: 'bob.test',\n      password: 'bob-pass',\n    })\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getDbBlobsForDid = (did: string) => {\n    return ctx.actorStore.read(did, (store) => store.repo.blob.getBlobCids())\n  }\n\n  it('deletes blob when record is deleted', async () => {\n    const img = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    const post = await sc.post(alice, 'test', undefined, [img])\n    await sc.deletePost(alice, post.ref.uri)\n    await network.processAll()\n\n    const dbBlobs = await getDbBlobsForDid(alice)\n    expect(dbBlobs.length).toBe(0)\n\n    const hasImg = await ctx.blobstore(alice).hasStored(img.image.ref)\n    expect(hasImg).toBeFalsy()\n  })\n\n  it('deletes blob when blob-ref in record is updated', async () => {\n    const img = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    const img2 = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n    await updateProfile(sc, alice, img.image, img.image)\n    await updateProfile(sc, alice, img2.image, img2.image)\n    await network.processAll()\n\n    const dbBlobs = await getDbBlobsForDid(alice)\n    expect(dbBlobs.length).toBe(1)\n    expect(dbBlobs[0].toString()).toEqual(img2.image.ref.toString())\n\n    const hasImg = await ctx.blobstore(alice).hasStored(img.image.ref)\n    expect(hasImg).toBeFalsy()\n\n    const hasImg2 = await ctx.blobstore(alice).hasStored(img2.image.ref)\n    expect(hasImg2).toBeTruthy()\n\n    // reset\n    await updateProfile(sc, alice)\n  })\n\n  it('does not delete blob when blob-ref in record is not updated', async () => {\n    const img = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    const img2 = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n    await updateProfile(sc, alice, img.image, img.image)\n    await updateProfile(sc, alice, img.image, img2.image)\n    await network.processAll()\n\n    const dbBlobs = await getDbBlobsForDid(alice)\n    expect(dbBlobs.length).toBe(2)\n\n    const hasImg = await ctx.blobstore(alice).hasStored(img.image.ref)\n    expect(hasImg).toBeTruthy()\n\n    const hasImg2 = await ctx.blobstore(alice).hasStored(img2.image.ref)\n    expect(hasImg2).toBeTruthy()\n    await updateProfile(sc, alice)\n  })\n\n  it('does not delete blob when blob is reused by another record in same commit', async () => {\n    const img = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-portrait-small.jpg',\n      'image/jpeg',\n    )\n    const post = await sc.post(alice, 'post', undefined, [img])\n\n    await agent.com.atproto.repo.applyWrites(\n      {\n        repo: alice,\n        writes: [\n          {\n            $type: 'com.atproto.repo.applyWrites#delete',\n            collection: 'app.bsky.feed.post',\n            rkey: post.ref.uri.rkey,\n          },\n          {\n            $type: 'com.atproto.repo.applyWrites#create',\n            collection: 'app.bsky.feed.post',\n            value: {\n              text: 'post2',\n              embed: {\n                $type: 'app.bsky.embed.images',\n                images: [\n                  {\n                    image: img.image,\n                    alt: 'alt',\n                  },\n                ],\n              },\n              createdAt: new Date().toISOString(),\n            },\n          },\n        ],\n      },\n      { encoding: 'application/json', headers: sc.getHeaders(alice) },\n    )\n    await network.processAll()\n\n    const dbBlobs = await getDbBlobsForDid(alice)\n    expect(dbBlobs.length).toBe(1)\n\n    const hasImg = await ctx.blobstore(alice).hasStored(img.image.ref)\n    expect(hasImg).toBeTruthy()\n  })\n\n  it('does delete blob from user blob store if another user is using it', async () => {\n    const imgAlice = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n    const imgBob = await sc.uploadFile(\n      bob,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n    const postAlice = await sc.post(alice, 'post', undefined, [imgAlice])\n    await sc.post(bob, 'post', undefined, [imgBob])\n    await sc.deletePost(alice, postAlice.ref.uri)\n    await network.processAll()\n\n    const hasImg = await ctx.blobstore(alice).hasStored(imgAlice.image.ref)\n    expect(hasImg).toBeFalsy()\n  })\n})\n\nasync function updateProfile(\n  sc: SeedClient,\n  did: string,\n  avatar?: BlobRef,\n  banner?: BlobRef,\n) {\n  return await sc.agent.api.com.atproto.repo.putRecord(\n    {\n      repo: did,\n      collection: ids.AppBskyActorProfile,\n      rkey: 'self',\n      record: {\n        avatar: avatar,\n        banner: banner,\n      },\n    },\n    { encoding: 'application/json', headers: sc.getHeaders(did) },\n  )\n}\n"
  },
  {
    "path": "packages/pds/tests/create-post.test.ts",
    "content": "import {\n  AppBskyFeedPost,\n  AppBskyRichtextFacet,\n  AtUri,\n  AtpAgent,\n  RichText,\n  Un$Typed,\n} from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport basicSeed from './seeds/basic'\n\ndescribe('pds posts record creation', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'views_posts',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('allows for creating posts with tags', async () => {\n    const post: Un$Typed<AppBskyFeedPost.Record> = {\n      text: 'hello world',\n      tags: ['javascript', 'hehe'],\n      createdAt: new Date().toISOString(),\n    }\n\n    const res = await agent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.alice },\n      post,\n      sc.getHeaders(sc.dids.alice),\n    )\n    const { value: record } = await agent.api.app.bsky.feed.post.get({\n      repo: sc.dids.alice,\n      rkey: new AtUri(res.uri).rkey,\n    })\n\n    expect(record).toBeTruthy()\n    expect(record.tags).toEqual(['javascript', 'hehe'])\n  })\n\n  it('handles RichText tag facets as well', async () => {\n    const rt = new RichText({ text: 'hello #world' })\n    await rt.detectFacets(agent)\n\n    const post: Un$Typed<AppBskyFeedPost.Record> = {\n      text: rt.text,\n      facets: rt.facets,\n      createdAt: new Date().toISOString(),\n    }\n\n    const res = await agent.api.app.bsky.feed.post.create(\n      { repo: sc.dids.alice },\n      post,\n      sc.getHeaders(sc.dids.alice),\n    )\n    const { value: record } = await agent.api.app.bsky.feed.post.get({\n      repo: sc.dids.alice,\n      rkey: new AtUri(res.uri).rkey,\n    })\n\n    expect(record).toBeTruthy()\n    expect(\n      record.facets?.every((f) => {\n        return AppBskyRichtextFacet.isTag(f.features[0])\n      }),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/crud.test.ts",
    "content": "import assert from 'node:assert'\nimport fs from 'node:fs/promises'\nimport { AppBskyFeedPostRecord, AtpAgent } from '@atproto/api'\nimport { TID, cidForCbor, ui8ToArrayBuffer } from '@atproto/common'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { BlobRef } from '@atproto/lexicon'\nimport { BlobNotFoundError } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { AppContext } from '../src/context'\nimport { ids, lexicons } from '../src/lexicon/lexicons'\nimport { isMain as isImagesEmbed } from '../src/lexicon/types/app/bsky/embed/images'\nimport * as Post from '../src/lexicon/types/app/bsky/feed/post'\nimport { forSnapshot, paginateAll } from './_util'\n\ndescribe('crud operations', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let agent: AtpAgent\n  let aliceAgent: AtpAgent\n  let bobAgent: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'crud',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    agent = network.pds.getClient()\n    aliceAgent = network.pds.getClient()\n    bobAgent = network.pds.getClient()\n\n    await aliceAgent.createAccount({\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n\n    await bobAgent.createAccount({\n      email: 'bob@test.com',\n      handle: 'bob.test',\n      password: 'bob-pass',\n    })\n\n    expect(bobAgent.accountDid).not.toBe(aliceAgent.accountDid)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('registers users', async () => {\n    const res = await agent.api.com.atproto.server.createAccount({\n      handle: 'user1.test',\n      email: 'user1@test.com',\n      password: 'password',\n    })\n    expect(res.data.handle).toBe('user1.test')\n    expect(res.data.accessJwt).toBeDefined()\n  })\n\n  it('describes repo', async () => {\n    const description = await agent.api.com.atproto.repo.describeRepo({\n      repo: aliceAgent.accountDid,\n    })\n    expect(description.data.handle).toBe('alice.test')\n    expect(description.data.did).toBe(aliceAgent.accountDid)\n    const description2 = await agent.api.com.atproto.repo.describeRepo({\n      repo: bobAgent.accountDid,\n    })\n    expect(description2.data.handle).toBe('bob.test')\n    expect(description2.data.did).toBe(bobAgent.accountDid)\n  })\n\n  it('creates records', async () => {\n    const res = await aliceAgent.api.com.atproto.repo.createRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.post',\n      record: {\n        $type: 'app.bsky.feed.post',\n        text: 'Hello, world!',\n        createdAt: new Date().toISOString(),\n      },\n    })\n    const uri = new AtUri(res.data.uri)\n    expect(res.data.uri).toBe(\n      `at://${aliceAgent.accountDid}/app.bsky.feed.post/${uri.rkey}`,\n    )\n\n    const res1 = await agent.api.com.atproto.repo.listRecords({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.post',\n    })\n    expect(res1.data.records.length).toBe(1)\n    expect(res1.data.records[0].uri).toBe(uri.toString())\n    expect((res1.data.records[0].value as Post.Record).text).toBe(\n      'Hello, world!',\n    )\n\n    const res2 = await agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    expect(res2.records.length).toBe(1)\n    expect(res2.records[0].uri).toBe(uri.toString())\n    expect(res2.records[0].value.text).toBe('Hello, world!')\n\n    const res3 = await agent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.post',\n      rkey: uri.rkey,\n    })\n    expect(res3.data.uri).toBe(uri.toString())\n    expect((res3.data.value as Post.Record).text).toBe('Hello, world!')\n\n    const res4 = await agent.app.bsky.feed.post.get({\n      repo: aliceAgent.accountDid,\n      rkey: uri.rkey,\n    })\n    expect(res4.uri).toBe(uri.toString())\n    expect(res4.value.text).toBe('Hello, world!')\n\n    await aliceAgent.api.com.atproto.repo.deleteRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.post',\n      rkey: uri.rkey,\n    })\n    const res5 = await agent.api.com.atproto.repo.listRecords({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.post',\n    })\n    expect(res5.data.records.length).toBe(0)\n  })\n\n  it('CRUDs records with the semantic sugars', async () => {\n    const res1 = await aliceAgent.app.bsky.feed.post.create(\n      { repo: aliceAgent.accountDid },\n      {\n        $type: 'app.bsky.feed.post',\n        text: 'Hello, world!',\n        createdAt: new Date().toISOString(),\n      },\n    )\n    const uri = new AtUri(res1.uri)\n\n    const res2 = await agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    expect(res2.records.length).toBe(1)\n\n    await aliceAgent.app.bsky.feed.post.delete({\n      repo: aliceAgent.accountDid,\n      rkey: uri.rkey,\n    })\n\n    const res3 = await agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    expect(res3.records.length).toBe(0)\n  })\n\n  it('attaches images to a post', async () => {\n    const file = await fs.readFile('../dev-env/assets/key-landscape-small.jpg')\n    const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(file, {\n      encoding: 'image/jpeg',\n    })\n    const uploaded = uploadedRes.data.blob\n    // Expect blobstore not to have image yet\n    await expect(\n      ctx.blobstore(aliceAgent.accountDid).getBytes(uploaded.ref),\n    ).rejects.toThrow(BlobNotFoundError)\n    // Associate image with post, image should be placed in blobstore\n    const res = await aliceAgent.app.bsky.feed.post.create(\n      { repo: aliceAgent.accountDid },\n      {\n        $type: 'app.bsky.feed.post',\n        text: \"Here's a key!\",\n        createdAt: new Date().toISOString(),\n        embed: {\n          $type: 'app.bsky.embed.images',\n          images: [{ image: uploaded, alt: '' }],\n        },\n      },\n    )\n    // Ensure image is on post record\n    const postUri = new AtUri(res.uri)\n    const post = await aliceAgent.app.bsky.feed.post.get({\n      rkey: postUri.rkey,\n      repo: aliceAgent.accountDid,\n    })\n    assert(isImagesEmbed(post.value.embed))\n    const images = post.value.embed.images\n    expect(images.length).toEqual(1)\n    expect(uploaded.ref.equals(images[0].image.ref)).toBeTruthy()\n    // Ensure that the uploaded image is now in the blobstore, i.e. doesn't throw BlobNotFoundError\n    await ctx.blobstore(aliceAgent.accountDid).getBytes(uploaded.ref)\n    // Cleanup\n    await aliceAgent.app.bsky.feed.post.delete({\n      rkey: postUri.rkey,\n      repo: aliceAgent.accountDid,\n    })\n  })\n\n  it('creates records with the correct key described by the schema', async () => {\n    const res1 = await aliceAgent.app.bsky.actor.profile.create(\n      { repo: aliceAgent.accountDid },\n      {\n        displayName: 'alice',\n        createdAt: new Date().toISOString(),\n      },\n    )\n    const uri = new AtUri(res1.uri)\n    expect(uri.rkey).toBe('self')\n  })\n\n  describe('paginates', () => {\n    let uri1: AtUri\n    let uri2: AtUri\n    let uri3: AtUri\n    let uri4: AtUri\n    let uri5: AtUri\n\n    beforeAll(async () => {\n      const createPost = async (text: string) => {\n        const res = await aliceAgent.app.bsky.feed.post.create(\n          { repo: aliceAgent.accountDid },\n          {\n            $type: 'app.bsky.feed.post',\n            text,\n            createdAt: new Date().toISOString(),\n          },\n        )\n        return new AtUri(res.uri)\n      }\n      uri1 = await createPost('Post 1')\n      uri2 = await createPost('Post 2')\n      uri3 = await createPost('Post 3')\n      uri4 = await createPost('Post 4')\n      uri5 = await createPost('Post 5')\n    })\n\n    afterAll(async () => {\n      for (const uri of [uri1, uri2, uri3, uri4, uri5]) {\n        await aliceAgent.app.bsky.feed.post.delete({\n          repo: aliceAgent.accountDid,\n          rkey: uri.rkey,\n        })\n      }\n    })\n\n    it('in forwards order', async () => {\n      const results = (\n        results: Awaited<ReturnType<AppBskyFeedPostRecord['list']>>[],\n      ) => results.flatMap((res) => res.records)\n      const paginator = async (cursor?: string) => {\n        const res = await agent.app.bsky.feed.post.list({\n          repo: aliceAgent.accountDid,\n          cursor,\n          limit: 2,\n        })\n        return res\n      }\n\n      const paginatedAll = await paginateAll(paginator)\n      paginatedAll.forEach((res) =>\n        expect(res.records.length).toBeLessThanOrEqual(2),\n      )\n\n      const full = await agent.app.bsky.feed.post.list({\n        repo: aliceAgent.accountDid,\n      })\n\n      expect(full.records.length).toEqual(5)\n      expect(results(paginatedAll)).toEqual(results([full]))\n    })\n\n    it('in reverse order', async () => {\n      const results = (\n        results: Awaited<ReturnType<AppBskyFeedPostRecord['list']>>[],\n      ) => results.flatMap((res) => res.records)\n      const paginator = async (cursor?: string) => {\n        const res = await agent.app.bsky.feed.post.list({\n          repo: aliceAgent.accountDid,\n          reverse: true,\n          cursor,\n          limit: 2,\n        })\n        return res\n      }\n\n      const paginatedAll = await paginateAll(paginator)\n      paginatedAll.forEach((res) =>\n        expect(res.records.length).toBeLessThanOrEqual(2),\n      )\n\n      const full = await agent.app.bsky.feed.post.list({\n        repo: aliceAgent.accountDid,\n        reverse: true,\n      })\n\n      expect(full.records.length).toEqual(5)\n      expect(results(paginatedAll)).toEqual(results([full]))\n    })\n\n    it('reverses', async () => {\n      const forwards = await agent.app.bsky.feed.post.list({\n        repo: aliceAgent.accountDid,\n      })\n      const reverse = await agent.app.bsky.feed.post.list({\n        repo: aliceAgent.accountDid,\n        reverse: true,\n      })\n      expect(forwards.cursor).toEqual(uri1.rkey)\n      expect(reverse.cursor).toEqual(uri5.rkey)\n      expect(forwards.records.length).toEqual(5)\n      expect(reverse.records.length).toEqual(5)\n      expect(forwards.records.reverse()).toEqual(reverse.records)\n    })\n  })\n\n  describe('deleteRecord', () => {\n    it('deletes a record if it exists', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: { text: 'post', createdAt: new Date().toISOString() },\n      })\n      const uri = new AtUri(post.uri)\n      await repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).rejects.toThrow('Could not locate record')\n    })\n\n    it(\"no-ops if record doesn't exist\", async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: { text: 'post', createdAt: new Date().toISOString() },\n      })\n      const uri = new AtUri(post.uri)\n      await repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).rejects.toThrow('Could not locate record')\n      const attemptDelete = repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(attemptDelete).resolves.toBeDefined()\n    })\n\n    it('does not delete the underlying block if it is referenced elsewhere', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      const record = { text: 'post', createdAt: new Date().toISOString() }\n      const { data: post1 } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record,\n      })\n      const { data: post2 } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record,\n      })\n      const uri1 = new AtUri(post1.uri)\n      await repo.deleteRecord({\n        repo: uri1.host,\n        collection: uri1.collection,\n        rkey: uri1.rkey,\n      })\n      const uri2 = new AtUri(post2.uri)\n      const checkPost2 = await repo.getRecord({\n        repo: uri2.host,\n        collection: uri2.collection,\n        rkey: uri2.rkey,\n      })\n      expect(checkPost2).toBeDefined()\n      expect(checkPost2.data.value).toMatchObject(record)\n    })\n  })\n\n  describe('putRecord', () => {\n    const profilePath = {\n      collection: ids.AppBskyActorProfile,\n      rkey: 'self',\n    }\n\n    it(\"creates a new record if it doesn't already exist\", async () => {\n      const { repo } = bobAgent.api.com.atproto\n      const exists = repo.getRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n      })\n      await expect(exists).rejects.toThrow('Could not locate record')\n\n      const { data: put } = await repo.putRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n        record: {\n          displayName: 'Robert',\n        },\n      })\n      expect(put.uri).toEqual(\n        `at://${bobAgent.accountDid}/${ids.AppBskyActorProfile}/self`,\n      )\n\n      const { data: profile } = await repo.getRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n      })\n      expect(profile.value).toEqual({\n        $type: ids.AppBskyActorProfile,\n        displayName: 'Robert',\n      })\n    })\n\n    it('updates a record if it already exists', async () => {\n      const { repo } = bobAgent.api.com.atproto\n      const { data: put } = await repo.putRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n        record: {\n          displayName: 'Robert',\n          description: 'Dog lover',\n        },\n      })\n      expect(put.uri).toEqual(\n        `at://${bobAgent.accountDid}/${ids.AppBskyActorProfile}/self`,\n      )\n\n      const { data: profile } = await repo.getRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n      })\n      expect(profile.value).toEqual({\n        $type: ids.AppBskyActorProfile,\n        displayName: 'Robert',\n        description: 'Dog lover',\n      })\n    })\n\n    it('still works if repo is specified by handle', async () => {\n      await bobAgent.api.com.atproto.repo.putRecord({\n        repo: 'bob.test',\n        collection: ids.AppBskyGraphFollow,\n        rkey: TID.nextStr(),\n        record: {\n          subject: aliceAgent.accountDid,\n          createdAt: new Date().toISOString(),\n        },\n      })\n    })\n\n    it('does not produce commit on no-op update', async () => {\n      const { repo } = bobAgent.api.com.atproto\n      const rootRes1 = await bobAgent.api.com.atproto.sync.getLatestCommit({\n        did: bobAgent.accountDid,\n      })\n      const { data: put } = await repo.putRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n        record: {\n          displayName: 'Robert',\n          description: 'Dog lover',\n        },\n      })\n      expect(put.uri).toEqual(\n        `at://${bobAgent.accountDid}/${ids.AppBskyActorProfile}/self`,\n      )\n\n      const rootRes2 = await bobAgent.api.com.atproto.sync.getLatestCommit({\n        did: bobAgent.accountDid,\n      })\n\n      expect(rootRes2.data.cid).toEqual(rootRes1.data.cid)\n      expect(rootRes2.data.rev).toEqual(rootRes1.data.rev)\n    })\n\n    it('fails on user mismatch', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      const put = repo.putRecord({\n        repo: bobAgent.accountDid,\n        collection: ids.AppBskyGraphFollow,\n        rkey: TID.nextStr(),\n        record: {\n          subject: aliceAgent.accountDid,\n          createdAt: new Date().toISOString(),\n        },\n      })\n      await expect(put).rejects.toThrow('Authentication Required')\n    })\n\n    it('fails on invalid record', async () => {\n      const { repo } = bobAgent.api.com.atproto\n      const put = repo.putRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n        record: {\n          displayName: 'Robert',\n          description: 3.141,\n        },\n      })\n      await expect(put).rejects.toThrow(\n        'Invalid app.bsky.actor.profile record: Record/description must be a string',\n      )\n      const { data: profile } = await repo.getRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n      })\n      expect(profile.value).toEqual({\n        $type: ids.AppBskyActorProfile,\n        displayName: 'Robert',\n        description: 'Dog lover',\n      })\n    })\n\n    // @TODO remove after migrating legacy blobs\n    it('updates a legacy blob ref when updating profile', async () => {\n      const { repo } = bobAgent.api.com.atproto\n      const file = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')\n      const uploadedRes = await repo.uploadBlob(file, {\n        encoding: 'image/jpeg',\n      })\n\n      await repo.putRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n        record: {\n          displayName: 'Robert',\n          avatar: BlobRef.fromJsonRef({\n            mimeType: uploadedRes.data.blob.mimeType,\n            cid: uploadedRes.data.blob.ref.toString(),\n          }),\n        },\n      })\n\n      const got = await repo.getRecord({\n        ...profilePath,\n        repo: bobAgent.accountDid,\n      })\n      const gotAvatar = got.data.value['avatar'] as BlobRef\n      expect(gotAvatar.original).toEqual(uploadedRes.data.blob.original)\n    })\n  })\n\n  // Validation\n  // --------------\n\n  it('defaults an undefined $type on records', async () => {\n    const res = await aliceAgent.api.com.atproto.repo.createRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.post',\n      record: {\n        text: 'blah',\n        createdAt: new Date().toISOString(),\n      },\n    })\n    const uri = new AtUri(res.data.uri)\n    const got = await agent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: uri.collection,\n      rkey: uri.rkey,\n    })\n    expect(got.data.value['$type']).toBe(uri.collection)\n  })\n\n  it('requires the schema to be known if explicitly validating', async () => {\n    const prom = aliceAgent.api.com.atproto.repo.createRecord({\n      validate: true,\n      repo: aliceAgent.accountDid,\n      collection: 'com.example.foobar',\n      record: { $type: 'com.example.foobar' },\n    })\n    await expect(prom).rejects.toThrow(\n      'Lexicon not found: lex:com.example.foobar',\n    )\n  })\n\n  it('does not require the schema to be known if not explicitly validating', async () => {\n    const prom = await aliceAgent.api.com.atproto.repo.createRecord({\n      // validate not set\n      repo: aliceAgent.accountDid,\n      collection: 'com.example.foobar',\n      record: { $type: 'com.example.foobar' },\n    })\n    expect(prom.data.validationStatus).toBe('unknown')\n  })\n\n  it('requires the $type to match the schema', async () => {\n    await expect(\n      aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.post',\n        record: { $type: 'app.bsky.feed.like' },\n      }),\n    ).rejects.toThrow(\n      'Invalid $type: expected app.bsky.feed.post, got app.bsky.feed.like',\n    )\n  })\n\n  it('requires valid rkey', async () => {\n    await expect(\n      aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.generator',\n        record: {\n          $type: 'app.bsky.feed.generator',\n          did: 'did:web:dummy.example.com',\n          displayName: 'dummy',\n          createdAt: new Date().toISOString(),\n        },\n        rkey: '..',\n      }),\n    ).rejects.toThrow('Input/rkey must be a valid Record Key')\n  })\n\n  it('validates the record on write', async () => {\n    await expect(\n      aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.post',\n        record: { $type: 'app.bsky.feed.post' },\n      }),\n    ).rejects.toThrow(\n      'Invalid app.bsky.feed.post record: Record must have the property \"text\"',\n    )\n  })\n\n  it('validates datetimes more rigorously than lex sdk', async () => {\n    const postRecord = {\n      $type: 'app.bsky.feed.post',\n      text: 'test',\n      createdAt: '1985-04-12T23:20:50.123',\n    }\n    lexicons.assertValidRecord('app.bsky.feed.post', postRecord)\n    await expect(\n      aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.post',\n        record: postRecord,\n      }),\n    ).rejects.toThrow(\n      'Invalid app.bsky.feed.post record: createdAt must be an valid atproto datetime (both RFC-3339 and ISO-8601)',\n    )\n  })\n\n  describe('unvalidated writes', () => {\n    it('disallows creation of unknown lexicons when validate is set to true', async () => {\n      const attempt = aliceAgent.api.com.atproto.repo.createRecord({\n        validate: true,\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing',\n        },\n      })\n      await expect(attempt).rejects.toThrow(\n        'Lexicon not found: lex:com.example.record',\n      )\n    })\n\n    it('allows creation of unknown lexicons when validate is not set to true', async () => {\n      // validate: default\n      const res1 = await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing1',\n        },\n      })\n      expect(res1.data.validationStatus).toBe('unknown')\n      const record1 = await ctx.actorStore.read(\n        aliceAgent.accountDid,\n        (store) =>\n          store.record.getRecord(new AtUri(res1.data.uri), res1.data.cid),\n      )\n      expect(record1?.value).toEqual({\n        $type: 'com.example.record',\n        blah: 'thing1',\n      })\n      // validate: false\n      const res2 = await aliceAgent.api.com.atproto.repo.createRecord({\n        validate: false,\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing2',\n        },\n      })\n      expect(res2.data.validationStatus).toBeUndefined()\n      const record2 = await ctx.actorStore.read(\n        aliceAgent.accountDid,\n        (store) =>\n          store.record.getRecord(new AtUri(res2.data.uri), res2.data.cid),\n      )\n      expect(record2?.value).toEqual({\n        $type: 'com.example.record',\n        blah: 'thing2',\n      })\n    })\n\n    it('allows update of unknown lexicons when validate is set to false', async () => {\n      const createRes = await aliceAgent.api.com.atproto.repo.createRecord({\n        validate: false,\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing',\n        },\n      })\n      const uri = new AtUri(createRes.data.uri)\n      // validate: default\n      const updateRes1 = await aliceAgent.api.com.atproto.repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        rkey: uri.rkey,\n        record: {\n          blah: 'something else',\n        },\n      })\n      const record1 = await ctx.actorStore.read(\n        aliceAgent.accountDid,\n        (store) => store.record.getRecord(uri, updateRes1.data.cid),\n      )\n      expect(record1?.value).toEqual({\n        $type: 'com.example.record',\n        blah: 'something else',\n      })\n      // validate: false\n      const updateRes2 = await aliceAgent.api.com.atproto.repo.putRecord({\n        validate: false,\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        rkey: uri.rkey,\n        record: {\n          blah: 'something else',\n        },\n      })\n      const record2 = await ctx.actorStore.read(\n        aliceAgent.accountDid,\n        (store) => store.record.getRecord(uri, updateRes2.data.cid),\n      )\n      expect(record2?.value).toEqual({\n        $type: 'com.example.record',\n        blah: 'something else',\n      })\n    })\n\n    it('applyWrites returns results with validation status', async () => {\n      const existing1 = await aliceAgent.api.com.atproto.repo.createRecord({\n        validate: false,\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing1',\n        },\n      })\n      const existing2 = await aliceAgent.api.com.atproto.repo.createRecord({\n        validate: false,\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing2',\n        },\n      })\n      const {\n        data: { results },\n      } = await aliceAgent.com.atproto.repo.applyWrites({\n        repo: aliceAgent.accountDid,\n        writes: [\n          {\n            $type: `${ids.ComAtprotoRepoApplyWrites}#create`,\n            collection: ids.AppBskyFeedPost,\n            value: {\n              $type: ids.AppBskyFeedPost,\n              text: '👋',\n              createdAt: new Date().toISOString(),\n            },\n          },\n          {\n            $type: `${ids.ComAtprotoRepoApplyWrites}#update`,\n            collection: 'com.example.record',\n            rkey: new AtUri(existing1.data.uri).rkey,\n            value: {},\n          },\n          {\n            $type: `${ids.ComAtprotoRepoApplyWrites}#delete`,\n            collection: 'com.example.record',\n            rkey: new AtUri(existing2.data.uri).rkey,\n          },\n        ],\n      })\n      expect(forSnapshot(results)).toEqual([\n        {\n          $type: `${ids.ComAtprotoRepoApplyWrites}#createResult`,\n          cid: 'cids(0)',\n          uri: 'record(0)',\n          validationStatus: 'valid',\n        },\n        {\n          $type: `${ids.ComAtprotoRepoApplyWrites}#updateResult`,\n          cid: 'cids(1)',\n          uri: 'record(1)',\n          validationStatus: 'unknown',\n        },\n        { $type: `${ids.ComAtprotoRepoApplyWrites}#deleteResult` },\n      ])\n    })\n\n    it('correctly associates images with unknown record types', async () => {\n      const file = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')\n      const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(\n        file,\n        {\n          encoding: 'image/jpeg',\n        },\n      )\n\n      const res = await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing',\n          image: uploadedRes.data.blob,\n        },\n        validate: false,\n      })\n      const record = await ctx.actorStore.read(aliceAgent.accountDid, (store) =>\n        store.record.getRecord(new AtUri(res.data.uri), res.data.cid),\n      )\n      assert(record)\n      expect(record.value).toMatchObject({\n        $type: 'com.example.record',\n        blah: 'thing',\n      })\n      const recordBlobs = await ctx.actorStore.read(\n        aliceAgent.assertDid,\n        (store) => store.repo.blob.getBlobsForRecord(record.uri),\n      )\n      expect(recordBlobs.length).toBe(1)\n      expect(recordBlobs.at(0)).toBe(uploadedRes.data.blob.ref.toString())\n    })\n\n    it('enforces record type constraint even when unvalidated', async () => {\n      const attempt = aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          $type: 'com.example.other',\n          blah: 'thing',\n        },\n      })\n      await expect(attempt).rejects.toThrow(\n        'Invalid $type: expected com.example.record, got com.example.other',\n      )\n    })\n\n    it('enforces blob ref format even when unvalidated', async () => {\n      const file = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')\n      const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(\n        file,\n        {\n          encoding: 'image/jpeg',\n        },\n      )\n\n      const attempt = aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'com.example.record',\n        record: {\n          blah: 'thing',\n          image: {\n            cid: uploadedRes.data.blob.ref.toString(),\n            mimeType: uploadedRes.data.blob.mimeType,\n          },\n        },\n        validate: false,\n      })\n      await expect(attempt).rejects.toThrow(`Legacy blob ref at 'image'`)\n    })\n  })\n\n  describe('compare-and-swap', () => {\n    let recordCount = 0 // Ensures unique cids\n    const postRecord = () => ({\n      text: `post (${++recordCount})`,\n      createdAt: new Date().toISOString(),\n    })\n    const profileRecord = () => ({\n      displayName: `ali (${++recordCount})`,\n    })\n\n    it('createRecord succeeds on proper commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: commit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        swapCommit: commit.cid,\n        record: postRecord(),\n      })\n      const uri = new AtUri(post.uri)\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).resolves.toBeDefined()\n    })\n\n    it('createRecord fails on bad commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: staleCommit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      // Update repo, change head\n      await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const attemptCreate = repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        swapCommit: staleCommit.cid,\n        record: postRecord(),\n      })\n      await expect(attemptCreate).rejects.toMatchObject({\n        error: 'InvalidSwap',\n      })\n    })\n\n    it('deleteRecord succeeds on proper commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const { data: commit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      const uri = new AtUri(post.uri)\n      await repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n        swapCommit: commit.cid,\n      })\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).rejects.toThrow('Could not locate record')\n    })\n\n    it('deleteRecord fails on bad commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: staleCommit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const uri = new AtUri(post.uri)\n      const attemptDelete = repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n        swapCommit: staleCommit.cid,\n      })\n      await expect(attemptDelete).rejects.toMatchObject({\n        error: 'InvalidSwap',\n      })\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).resolves.toBeDefined()\n    })\n\n    it('deleteRecord succeeds on proper record cas', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const uri = new AtUri(post.uri)\n      await repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n        swapRecord: post.cid,\n      })\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).rejects.toThrow('Could not locate record')\n    })\n\n    it('deleteRecord fails on bad record cas', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      const { data: post } = await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const uri = new AtUri(post.uri)\n      const attemptDelete = repo.deleteRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n        swapRecord: (await cidForCbor({})).toString(),\n      })\n      await expect(attemptDelete).rejects.toMatchObject({\n        error: 'InvalidSwap',\n      })\n      const checkPost = repo.getRecord({\n        repo: uri.host,\n        collection: uri.collection,\n        rkey: uri.rkey,\n      })\n      await expect(checkPost).resolves.toBeDefined()\n    })\n\n    it('putRecord succeeds on proper commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: commit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      const { data: profile } = await repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        swapCommit: commit.cid,\n        record: profileRecord(),\n      })\n      const { data: checkProfile } = await repo.getRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n      })\n      expect(checkProfile.cid).toEqual(profile.cid)\n    })\n\n    it('putRecord fails on bad commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: staleCommit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      // Update repo, change head\n      await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const attemptPut = repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        swapCommit: staleCommit.cid,\n        record: profileRecord(),\n      })\n      await expect(attemptPut).rejects.toMatchObject({ error: 'InvalidSwap' })\n    })\n\n    it('putRecord succeeds on proper record cas', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      // Start with missing profile record, to test swapRecord=null\n      await repo.deleteRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n      })\n      // Test swapRecord w/ null (ensures create)\n      const { data: profile1 } = await repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        swapRecord: null,\n        record: profileRecord(),\n      })\n      const { data: checkProfile1 } = await repo.getRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n      })\n      expect(checkProfile1.cid).toEqual(profile1.cid)\n      // Test swapRecord w/ cid (ensures update)\n      const { data: profile2 } = await repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        swapRecord: profile1.cid,\n        record: profileRecord(),\n      })\n      const { data: checkProfile2 } = await repo.getRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n      })\n      expect(checkProfile2.cid).toEqual(profile2.cid)\n    })\n\n    it('putRecord fails on bad record cas', async () => {\n      const { repo } = aliceAgent.api.com.atproto\n      // Test swapRecord w/ null (ensures create)\n      const attemptPut1 = repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        swapRecord: null,\n        record: profileRecord(),\n      })\n      await expect(attemptPut1).rejects.toMatchObject({ error: 'InvalidSwap' })\n      // Test swapRecord w/ cid (ensures update)\n      const attemptPut2 = repo.putRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyActorProfile,\n        rkey: 'self',\n        swapRecord: (await cidForCbor({})).toString(),\n        record: profileRecord(),\n      })\n      await expect(attemptPut2).rejects.toMatchObject({ error: 'InvalidSwap' })\n    })\n\n    it('applyWrites succeeds on proper commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: commit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      await repo.applyWrites({\n        repo: aliceAgent.accountDid,\n        swapCommit: commit.cid,\n        writes: [\n          {\n            $type: `${ids.ComAtprotoRepoApplyWrites}#create`,\n            collection: ids.AppBskyFeedPost,\n            value: { $type: ids.AppBskyFeedPost, ...postRecord() },\n          },\n        ],\n      })\n    })\n\n    it('applyWrites fails on bad commit cas', async () => {\n      const { repo, sync } = aliceAgent.api.com.atproto\n      const { data: staleCommit } = await sync.getLatestCommit({\n        did: aliceAgent.accountDid,\n      })\n      // Update repo, change head\n      await repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: ids.AppBskyFeedPost,\n        record: postRecord(),\n      })\n      const attemptApplyWrite = repo.applyWrites({\n        repo: aliceAgent.accountDid,\n        swapCommit: staleCommit.cid,\n        writes: [\n          {\n            $type: `${ids.ComAtprotoRepoApplyWrites}#create`,\n            collection: ids.AppBskyFeedPost,\n            value: { $type: ids.AppBskyFeedPost, ...postRecord() },\n          },\n        ],\n      })\n      await expect(attemptApplyWrite).rejects.toMatchObject({\n        error: 'InvalidSwap',\n      })\n    })\n\n    it(\"writes fail on values that can't reliably transform between cbor to lex\", async () => {\n      const passthroughBody = (data: unknown) =>\n        ui8ToArrayBuffer(new TextEncoder().encode(JSON.stringify(data)))\n\n      const result = aliceAgent.call(\n        'com.atproto.repo.createRecord',\n        {},\n        passthroughBody({\n          repo: aliceAgent.accountDid,\n          collection: 'app.bsky.feed.post',\n          record: {\n            text: 'x',\n            createdAt: new Date().toISOString(),\n            deepObject: createDeepObject(4000),\n          },\n        }),\n        {\n          encoding: 'application/json',\n        },\n      )\n\n      await expect(result).rejects.toMatchObject({\n        status: 400,\n        error: 'InvalidRequest',\n      })\n    })\n  })\n\n  it('prevents duplicate likes', async () => {\n    const now = new Date().toISOString()\n    const uriA = AtUri.make(\n      bobAgent.accountDid,\n      'app.bsky.feed.post',\n      TID.nextStr(),\n    )\n    const cidA = await cidForCbor({ post: 'a' })\n    const uriB = AtUri.make(\n      bobAgent.accountDid,\n      'app.bsky.feed.post',\n      TID.nextStr(),\n    )\n    const cidB = await cidForCbor({ post: 'b' })\n\n    const { data: like1 } = await aliceAgent.api.com.atproto.repo.createRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.like',\n      record: {\n        $type: 'app.bsky.feed.like',\n        subject: { uri: uriA.toString(), cid: cidA.toString() },\n        createdAt: now,\n      },\n    })\n    const { data: like2 } = await aliceAgent.api.com.atproto.repo.createRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.like',\n      record: {\n        $type: 'app.bsky.feed.like',\n        subject: { uri: uriB.toString(), cid: cidB.toString() },\n        createdAt: now,\n      },\n    })\n    const { data: like3 } = await aliceAgent.api.com.atproto.repo.createRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.like',\n      record: {\n        $type: 'app.bsky.feed.like',\n        subject: { uri: uriA.toString(), cid: cidA.toString() },\n        createdAt: now,\n      },\n    })\n\n    const getLike1 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.like',\n      rkey: new AtUri(like1.uri).rkey,\n    })\n\n    await expect(getLike1).rejects.toThrow('Could not locate record:')\n\n    const getLike2 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.like',\n      rkey: new AtUri(like2.uri).rkey,\n    })\n\n    await expect(getLike2).resolves.toBeDefined()\n\n    const getLike3 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.like',\n      rkey: new AtUri(like3.uri).rkey,\n    })\n\n    await expect(getLike3).resolves.toBeDefined()\n  })\n\n  it('prevents duplicate reposts', async () => {\n    const now = new Date().toISOString()\n    const uriA = AtUri.make(\n      bobAgent.accountDid,\n      'app.bsky.feed.post',\n      TID.nextStr(),\n    )\n    const cidA = await cidForCbor({ post: 'a' })\n    const uriB = AtUri.make(\n      bobAgent.accountDid,\n      'app.bsky.feed.post',\n      TID.nextStr(),\n    )\n    const cidB = await cidForCbor({ post: 'b' })\n\n    const { data: repost1 } =\n      await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.repost',\n        record: {\n          $type: 'app.bsky.feed.repost',\n          subject: { uri: uriA.toString(), cid: cidA.toString() },\n          createdAt: now,\n        },\n      })\n    const { data: repost2 } =\n      await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.repost',\n        record: {\n          $type: 'app.bsky.feed.repost',\n          subject: { uri: uriB.toString(), cid: cidB.toString() },\n          createdAt: now,\n        },\n      })\n    const { data: repost3 } =\n      await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.feed.repost',\n        record: {\n          $type: 'app.bsky.feed.repost',\n          subject: { uri: uriA.toString(), cid: cidA.toString() },\n          createdAt: now,\n        },\n      })\n\n    const getRepost1 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.repost',\n      rkey: new AtUri(repost1.uri).rkey,\n    })\n\n    await expect(getRepost1).rejects.toThrow('Could not locate record:')\n\n    const getRepost2 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.repost',\n      rkey: new AtUri(repost2.uri).rkey,\n    })\n\n    await expect(getRepost2).resolves.toBeDefined()\n\n    const getRepost3 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.feed.repost',\n      rkey: new AtUri(repost3.uri).rkey,\n    })\n\n    await expect(getRepost3).resolves.toBeDefined()\n  })\n\n  it('prevents duplicate blocks', async () => {\n    const now = new Date().toISOString()\n\n    const { data: block1 } = await aliceAgent.api.com.atproto.repo.createRecord(\n      {\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.graph.block',\n        record: {\n          $type: 'app.bsky.graph.block',\n          subject: bobAgent.accountDid,\n          createdAt: now,\n        },\n      },\n    )\n\n    const { data: block2 } = await bobAgent.api.com.atproto.repo.createRecord({\n      repo: bobAgent.accountDid,\n      collection: 'app.bsky.graph.block',\n      record: {\n        $type: 'app.bsky.graph.block',\n        subject: aliceAgent.accountDid,\n        createdAt: now,\n      },\n    })\n\n    const { data: block3 } = await aliceAgent.api.com.atproto.repo.createRecord(\n      {\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.graph.block',\n        record: {\n          $type: 'app.bsky.graph.block',\n          subject: bobAgent.accountDid,\n          createdAt: now,\n        },\n      },\n    )\n\n    const getBlock1 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.graph.block',\n      rkey: new AtUri(block1.uri).rkey,\n    })\n\n    await expect(getBlock1).rejects.toThrow('Could not locate record:')\n\n    const getBlock2 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: bobAgent.accountDid,\n      collection: 'app.bsky.graph.block',\n      rkey: new AtUri(block2.uri).rkey,\n    })\n\n    await expect(getBlock2).resolves.toBeDefined()\n\n    const getBlock3 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.graph.block',\n      rkey: new AtUri(block3.uri).rkey,\n    })\n\n    await expect(getBlock3).resolves.toBeDefined()\n  })\n\n  it('prevents duplicate follows', async () => {\n    const now = new Date().toISOString()\n\n    const { data: follow1 } =\n      await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.graph.follow',\n        record: {\n          $type: 'app.bsky.graph.follow',\n          subject: bobAgent.accountDid,\n          createdAt: now,\n        },\n      })\n    const { data: follow2 } = await bobAgent.api.com.atproto.repo.createRecord({\n      repo: bobAgent.accountDid,\n      collection: 'app.bsky.graph.follow',\n      record: {\n        $type: 'app.bsky.graph.follow',\n        subject: aliceAgent.accountDid,\n        createdAt: now,\n      },\n    })\n    const { data: follow3 } =\n      await aliceAgent.api.com.atproto.repo.createRecord({\n        repo: aliceAgent.accountDid,\n        collection: 'app.bsky.graph.follow',\n        record: {\n          $type: 'app.bsky.graph.follow',\n          subject: bobAgent.accountDid,\n          createdAt: now,\n        },\n      })\n\n    const getFollow1 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.graph.follow',\n      rkey: new AtUri(follow1.uri).rkey,\n    })\n\n    await expect(getFollow1).rejects.toThrow('Could not locate record:')\n\n    const getFollow2 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: bobAgent.accountDid,\n      collection: 'app.bsky.graph.follow',\n      rkey: new AtUri(follow2.uri).rkey,\n    })\n\n    await expect(getFollow2).resolves.toBeDefined()\n\n    const getFollow3 = aliceAgent.api.com.atproto.repo.getRecord({\n      repo: aliceAgent.accountDid,\n      collection: 'app.bsky.graph.follow',\n      rkey: new AtUri(follow3.uri).rkey,\n    })\n\n    await expect(getFollow3).resolves.toBeDefined()\n  })\n\n  // Moderation\n  // --------------\n\n  it(\"doesn't serve taken-down record\", async () => {\n    const created = await aliceAgent.app.bsky.feed.post.create(\n      { repo: aliceAgent.accountDid },\n      {\n        $type: 'app.bsky.feed.post',\n        text: 'Hello, world!',\n        createdAt: new Date().toISOString(),\n      },\n    )\n    const postUri = new AtUri(created.uri)\n    const post = await agent.app.bsky.feed.post.get({\n      repo: aliceAgent.accountDid,\n      rkey: postUri.rkey,\n    })\n    const posts = await agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    expect(posts.records.map((r) => r.uri)).toContain(post.uri)\n\n    const subject = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: created.uri,\n      cid: created.cid,\n    }\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject,\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n\n    const postTakedownPromise = agent.app.bsky.feed.post.get({\n      repo: aliceAgent.accountDid,\n      rkey: postUri.rkey,\n    })\n    await expect(postTakedownPromise).rejects.toThrow('Could not locate record')\n    const postsTakedown = await agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    expect(postsTakedown.records.map((r) => r.uri)).not.toContain(post.uri)\n\n    // Cleanup\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n  })\n\n  it(\"doesn't serve taken-down actor\", async () => {\n    const posts = await agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    expect(posts.records.length).toBeGreaterThan(0)\n\n    const subject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: aliceAgent.accountDid,\n    }\n\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject,\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n\n    const tryListPosts = agent.app.bsky.feed.post.list({\n      repo: aliceAgent.accountDid,\n    })\n    await expect(tryListPosts).rejects.toThrow(/Could not find repo/)\n\n    // Cleanup\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n  })\n})\n\nfunction createDeepObject(depth: number) {\n  const obj: any = {}\n  let iter = obj\n  for (let i = 0; i < depth; ++i) {\n    iter.x = {}\n    iter = iter.x\n  }\n  return obj\n}\n"
  },
  {
    "path": "packages/pds/tests/db.test.ts",
    "content": "import { TestNetworkNoAppView } from '@atproto/dev-env'\n// Importing from `dist` to circumvent circular dependency typing issues\nimport { AccountDb } from '../dist/account-manager/db'\n\ndescribe('db', () => {\n  let network: TestNetworkNoAppView\n  let db: AccountDb\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'db',\n    })\n    db = network.pds.ctx.accountManager.db\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('commits changes', async () => {\n    const result = await db.transaction(async (dbTxn) => {\n      return await dbTxn.db\n        .insertInto('repo_root')\n        .values({\n          did: 'x',\n          cid: 'x',\n          rev: 'x',\n          indexedAt: 'bad-date',\n        })\n        .returning('did')\n        .executeTakeFirst()\n    })\n\n    if (!result) {\n      return expect(result).toBeTruthy()\n    }\n\n    expect(result.did).toEqual('x')\n\n    const row = await db.db\n      .selectFrom('repo_root')\n      .selectAll()\n      .where('did', '=', 'x')\n      .executeTakeFirst()\n\n    expect(row).toEqual({\n      did: 'x',\n      cid: 'x',\n      rev: 'x',\n      indexedAt: 'bad-date',\n    })\n  })\n\n  it('rolls-back changes on failure', async () => {\n    const promise = db.transaction(async (dbTxn) => {\n      await dbTxn.db\n        .insertInto('repo_root')\n        .values({\n          did: 'y',\n          cid: 'y',\n          rev: 'y',\n          indexedAt: 'bad-date',\n        })\n        .returning('did')\n        .executeTakeFirst()\n\n      throw new Error('Oops!')\n    })\n\n    await expect(promise).rejects.toThrow('Oops!')\n\n    const row = await db.db\n      .selectFrom('repo_root')\n      .selectAll()\n      .where('did', '=', 'y')\n      .executeTakeFirst()\n\n    expect(row).toBeUndefined()\n  })\n\n  it('indicates isTransaction', async () => {\n    expect(db.isTransaction).toEqual(false)\n\n    await db.transaction(async (dbTxn) => {\n      expect(db.isTransaction).toEqual(false)\n      expect(dbTxn.isTransaction).toEqual(true)\n    })\n\n    expect(db.isTransaction).toEqual(false)\n  })\n\n  it('asserts transaction', async () => {\n    expect(() => db.assertTransaction()).toThrow('Transaction required')\n\n    await db.transaction(async (dbTxn) => {\n      expect(() => dbTxn.assertTransaction()).not.toThrow()\n    })\n  })\n\n  it('does not allow leaky transactions', async () => {\n    let leakedTx: AccountDb | undefined\n\n    const tx = db.transaction(async (dbTxn) => {\n      leakedTx = dbTxn\n      await dbTxn.db\n        .insertInto('repo_root')\n        .values({ cid: 'a', did: 'a', rev: 'a', indexedAt: 'bad-date' })\n        .execute()\n      throw new Error('test tx failed')\n    })\n    await expect(tx).rejects.toThrow('test tx failed')\n\n    const attempt = leakedTx?.db\n      .insertInto('repo_root')\n      .values({ cid: 'b', did: 'b', rev: 'b', indexedAt: 'bad-date' })\n      .execute()\n    await expect(attempt).rejects.toThrow('tx already failed')\n\n    const res = await db.db\n      .selectFrom('repo_root')\n      .selectAll()\n      .where('did', 'in', ['a', 'b'])\n      .execute()\n\n    expect(res.length).toBe(0)\n  })\n\n  it('ensures all inflight queries are rolled back', async () => {\n    let promise: Promise<unknown> | undefined = undefined\n    const names: string[] = []\n    try {\n      await db.transaction(async (dbTxn) => {\n        const queries: Promise<unknown>[] = []\n        for (let i = 0; i < 20; i++) {\n          const name = `user${i}`\n          const query = dbTxn.db\n            .insertInto('repo_root')\n            .values({\n              cid: name,\n              did: name,\n              rev: name,\n              indexedAt: 'bad-date',\n            })\n            .execute()\n          names.push(name)\n          queries.push(query)\n        }\n        promise = Promise.allSettled(queries)\n        throw new Error()\n      })\n    } catch (err) {\n      expect(err).toBeDefined()\n    }\n    if (promise) {\n      await promise\n    }\n\n    const res = await db.db\n      .selectFrom('repo_root')\n      .selectAll()\n      .where('did', 'in', names)\n      .execute()\n    expect(res.length).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/email-confirmation.test.ts",
    "content": "import { EventEmitter, once } from 'node:events'\nimport Mail from 'nodemailer/lib/mailer'\nimport {\n  AtpAgent,\n  ComAtprotoServerConfirmEmail,\n  ComAtprotoServerUpdateEmail,\n} from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { ServerMailer } from '../src/mailer'\nimport userSeed from './seeds/users'\n\ndescribe('email confirmation', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let mailer: ServerMailer\n  const mailCatcher = new EventEmitter()\n  let _origSendMail\n\n  let alice\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'email_confirmation',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    mailer = network.pds.ctx.mailer\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await userSeed(sc)\n    alice = sc.accounts[sc.dids.alice]\n\n    // Catch emails for use in tests\n    _origSendMail = mailer.transporter.sendMail\n    mailer.transporter.sendMail = async (opts) => {\n      const result = await _origSendMail.call(mailer.transporter, opts)\n      mailCatcher.emit('mail', opts)\n      return result\n    }\n  })\n\n  afterAll(async () => {\n    mailer.transporter.sendMail = _origSendMail\n    await network.close()\n  })\n\n  const getMailFrom = async (promise): Promise<Mail.Options> => {\n    const result = await Promise.all([once(mailCatcher, 'mail'), promise])\n    return result[0][0]\n  }\n\n  const getTokenFromMail = (mail: Mail.Options) =>\n    mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5})</i)?.[1]\n\n  it('starts a user out unverified', async () => {\n    const session = await agent.api.com.atproto.server.getSession(\n      {},\n      { headers: sc.getHeaders(alice.did) },\n    )\n    expect(session.data.emailConfirmed).toEqual(false)\n  })\n\n  it('allows email update without token when unverified', async () => {\n    const res = await agent.api.com.atproto.server.requestEmailUpdate(\n      undefined,\n      { headers: sc.getHeaders(alice.did) },\n    )\n    expect(res.data.tokenRequired).toBe(false)\n\n    await agent.api.com.atproto.server.updateEmail(\n      {\n        email: 'new-alice@example.com',\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    const session = await agent.api.com.atproto.server.getSession(\n      {},\n      { headers: sc.getHeaders(alice.did) },\n    )\n    expect(session.data.email).toEqual('new-alice@example.com')\n    expect(session.data.emailConfirmed).toEqual(false)\n    alice.email = session.data.email\n  })\n\n  let confirmToken\n\n  it('requests email confirmation', async () => {\n    const mail = await getMailFrom(\n      agent.api.com.atproto.server.requestEmailConfirmation(undefined, {\n        headers: sc.getHeaders(alice.did),\n      }),\n    )\n    expect(mail.to).toEqual(alice.email)\n    expect(mail.html).toContain('Confirm your email')\n    confirmToken = getTokenFromMail(mail)\n    expect(confirmToken).toBeDefined()\n  })\n\n  it('fails email confirmation with a bad token', async () => {\n    const attempt = agent.api.com.atproto.server.confirmEmail(\n      {\n        email: alice.email,\n        token: '123456',\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      ComAtprotoServerConfirmEmail.InvalidTokenError,\n    )\n  })\n\n  it('fails email confirmation with a bad token', async () => {\n    const attempt = agent.api.com.atproto.server.confirmEmail(\n      {\n        email: 'fake-alice@example.com',\n        token: confirmToken,\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      ComAtprotoServerConfirmEmail.InvalidEmailError,\n    )\n  })\n\n  it('confirms email', async () => {\n    await agent.api.com.atproto.server.confirmEmail(\n      {\n        email: alice.email,\n        token: confirmToken,\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    const session = await agent.api.com.atproto.server.getSession(\n      {},\n      { headers: sc.getHeaders(alice.did) },\n    )\n    expect(session.data.emailConfirmed).toBe(true)\n  })\n\n  it('disallows email update without token when verified', async () => {\n    const attempt = agent.api.com.atproto.server.updateEmail(\n      {\n        email: 'new-alice-2@example.com',\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      ComAtprotoServerUpdateEmail.TokenRequiredError,\n    )\n  })\n\n  let updateToken\n\n  it('requests email update', async () => {\n    const reqUpdate = async () => {\n      const res = await agent.api.com.atproto.server.requestEmailUpdate(\n        undefined,\n        {\n          headers: sc.getHeaders(alice.did),\n        },\n      )\n      expect(res.data.tokenRequired).toBe(true)\n    }\n    const mail = await getMailFrom(reqUpdate())\n    expect(mail.to).toEqual(alice.email)\n    expect(mail.html).toContain('Update your email')\n    updateToken = getTokenFromMail(mail)\n    expect(updateToken).toBeDefined()\n  })\n\n  it('fails email update with a bad token', async () => {\n    const attempt = agent.api.com.atproto.server.updateEmail(\n      {\n        email: 'new-alice-2@example.com',\n        token: '123456',\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      ComAtprotoServerUpdateEmail.InvalidTokenError,\n    )\n  })\n\n  it('fails email update with a badly formatted email', async () => {\n    const attempt = agent.api.com.atproto.server.updateEmail(\n      {\n        email: 'bad-email@disposeamail.com',\n        token: updateToken,\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      'This email address is not supported, please use a different email.',\n    )\n  })\n\n  it('fails email update with in-use email', async () => {\n    const attempt = agent.api.com.atproto.server.updateEmail(\n      {\n        email: 'bob@test.com',\n        token: updateToken,\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      'This email address is already in use, please use a different email.',\n    )\n  })\n\n  it('updates email', async () => {\n    await agent.api.com.atproto.server.updateEmail(\n      {\n        email: 'new-alice-2@example.com',\n        token: updateToken,\n      },\n      { headers: sc.getHeaders(alice.did), encoding: 'application/json' },\n    )\n\n    const session = await agent.api.com.atproto.server.getSession(\n      {},\n      { headers: sc.getHeaders(alice.did) },\n    )\n    expect(session.data.email).toBe('new-alice-2@example.com')\n    expect(session.data.emailConfirmed).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/entryway.test.ts",
    "content": "import assert from 'node:assert'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport * as plcLib from '@did-plc/lib'\nimport getPort from 'get-port'\nimport { decodeJwt } from 'jose'\nimport * as ui8 from 'uint8arrays'\nimport { AtpAgent } from '@atproto/api'\nimport { Secp256k1Keypair, randomStr } from '@atproto/crypto'\nimport { SeedClient, TestPds, TestPlc, mockResolvers } from '@atproto/dev-env'\nimport * as pdsEntryway from '@atproto/pds-entryway'\nimport { parseReqNsid } from '@atproto/xrpc-server'\n\ndescribe('entryway', () => {\n  let plc: TestPlc\n  let pds: TestPds\n  let entryway: pdsEntryway.PDS\n  let pdsAgent: AtpAgent\n  let entrywayAgent: AtpAgent\n  let alice: string\n  let accessToken: string\n\n  beforeAll(async () => {\n    const jwtSigningKey = await Secp256k1Keypair.create({ exportable: true })\n    const plcRotationKey = await Secp256k1Keypair.create({ exportable: true })\n    const entrywayPort = await getPort()\n    plc = await TestPlc.create({})\n    pds = await TestPds.create({\n      entrywayUrl: `http://localhost:${entrywayPort}`,\n      entrywayDid: 'did:example:entryway',\n      entrywayJwtVerifyKeyK256PublicKeyHex: getPublicHex(jwtSigningKey),\n      entrywayPlcRotationKey: plcRotationKey.did(),\n      adminPassword: 'admin-pass',\n      serviceHandleDomains: [],\n      didPlcUrl: plc.url,\n      serviceDid: 'did:example:pds',\n      inviteRequired: false,\n    })\n    entryway = await createEntryway({\n      dbPostgresSchema: 'entryway',\n      port: entrywayPort,\n      adminPassword: 'admin-pass',\n      jwtSigningKeyK256PrivateKeyHex: await getPrivateHex(jwtSigningKey),\n      plcRotationKeyK256PrivateKeyHex: await getPrivateHex(plcRotationKey),\n      inviteRequired: false,\n      serviceDid: 'did:example:entryway',\n      didPlcUrl: plc.url,\n    })\n    mockResolvers(pds.ctx.idResolver, pds)\n    mockResolvers(entryway.ctx.idResolver, pds)\n    await entryway.ctx.db.db\n      .insertInto('pds')\n      .values({\n        did: pds.ctx.cfg.service.did,\n        host: new URL(pds.ctx.cfg.service.publicUrl).host,\n        weight: 1,\n      })\n      .execute()\n    pdsAgent = pds.getClient()\n    entrywayAgent = new AtpAgent({\n      service: entryway.ctx.cfg.service.publicUrl,\n    })\n  })\n\n  afterAll(async () => {\n    await plc.close()\n    await entryway.destroy()\n    await pds.close()\n  })\n\n  it('creates account.', async () => {\n    const res = await entrywayAgent.api.com.atproto.server.createAccount({\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'test123',\n    })\n    alice = res.data.did\n    accessToken = res.data.accessJwt\n\n    const account = await pds.ctx.accountManager.getAccount(alice)\n    expect(account?.did).toEqual(alice)\n    expect(account?.handle).toEqual('alice.test')\n  })\n\n  it('auths with both services.', async () => {\n    const entrywaySession =\n      await entrywayAgent.api.com.atproto.server.getSession(undefined, {\n        headers: SeedClient.getHeaders(accessToken),\n      })\n    const pdsSession = await pdsAgent.api.com.atproto.server.getSession(\n      undefined,\n      { headers: SeedClient.getHeaders(accessToken) },\n    )\n    expect(entrywaySession.data).toEqual(pdsSession.data)\n  })\n\n  it('updates handle from pds.', async () => {\n    await pdsAgent.api.com.atproto.identity.updateHandle(\n      { handle: 'alice2.test' },\n      {\n        headers: SeedClient.getHeaders(accessToken),\n        encoding: 'application/json',\n      },\n    )\n    const doc = await pds.ctx.idResolver.did.resolve(alice)\n    const handleToDid = await pds.ctx.idResolver.handle.resolve('alice2.test')\n    const accountFromPds = await pds.ctx.accountManager.getAccount(alice)\n    const accountFromEntryway = await entryway.ctx.services\n      .account(entryway.ctx.db)\n      .getAccount(alice)\n    expect(doc?.alsoKnownAs).toEqual(['at://alice2.test'])\n    expect(handleToDid).toEqual(alice)\n    expect(accountFromPds?.handle).toEqual('alice2.test')\n    expect(accountFromEntryway?.handle).toEqual('alice2.test')\n  })\n\n  it('updates handle from entryway.', async () => {\n    await entrywayAgent.api.com.atproto.identity.updateHandle(\n      { handle: 'alice3.test' },\n      await pds.ctx.serviceAuthHeaders(\n        alice,\n        'did:example:entryway',\n        'com.atproto.identity.updateHandle',\n      ),\n    )\n    const doc = await entryway.ctx.idResolver.did.resolve(alice)\n    const handleToDid =\n      await entryway.ctx.idResolver.handle.resolve('alice3.test')\n    const accountFromPds = await pds.ctx.accountManager.getAccount(alice)\n    const accountFromEntryway = await entryway.ctx.services\n      .account(entryway.ctx.db)\n      .getAccount(alice)\n    expect(doc?.alsoKnownAs).toEqual(['at://alice3.test'])\n    expect(handleToDid).toEqual(alice)\n    expect(accountFromPds?.handle).toEqual('alice3.test')\n    expect(accountFromEntryway?.handle).toEqual('alice3.test')\n  })\n\n  it('does not allow bringing own op to account creation.', async () => {\n    const {\n      data: { signingKey },\n    } = await pdsAgent.api.com.atproto.server.reserveSigningKey({})\n    const rotationKey = await Secp256k1Keypair.create()\n    const plcCreate = await plcLib.createOp({\n      signingKey,\n      rotationKeys: [rotationKey.did(), entryway.ctx.plcRotationKey.did()],\n      handle: 'weirdalice.test',\n      pds: pds.ctx.cfg.service.publicUrl,\n      signer: rotationKey,\n    })\n    const tryCreateAccount = pdsAgent.api.com.atproto.server.createAccount({\n      did: plcCreate.did,\n      plcOp: plcCreate.op,\n      handle: 'weirdalice.test',\n    })\n    await expect(tryCreateAccount).rejects.toThrow('invalid plc operation')\n  })\n})\n\nconst createEntryway = async (\n  config: pdsEntryway.ServerEnvironment & {\n    adminPassword: string\n    jwtSigningKeyK256PrivateKeyHex: string\n    plcRotationKeyK256PrivateKeyHex: string\n  },\n) => {\n  const signingKey = await Secp256k1Keypair.create({ exportable: true })\n  const recoveryKey = await Secp256k1Keypair.create({ exportable: true })\n  const env: pdsEntryway.ServerEnvironment = {\n    isEntryway: true,\n    recoveryDidKey: recoveryKey.did(),\n    serviceHandleDomains: ['.test'],\n    dbPostgresUrl: process.env.DB_POSTGRES_URL,\n    blobstoreDiskLocation: path.join(os.tmpdir(), randomStr(8, 'base32')),\n    bskyAppViewUrl: 'https://appview.invalid',\n    bskyAppViewDid: 'did:example:invalid',\n    bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s',\n    jwtSecret: randomStr(8, 'base32'),\n    repoSigningKeyK256PrivateKeyHex: await getPrivateHex(signingKey),\n    modServiceUrl: 'https://mod.invalid',\n    modServiceDid: 'did:example:invalid',\n    ...config,\n  }\n  const cfg = pdsEntryway.envToCfg(env)\n  const secrets = pdsEntryway.envToSecrets(env)\n  const server = await pdsEntryway.PDS.create(cfg, secrets)\n  await server.ctx.db.migrateToLatestOrThrow()\n  await server.start()\n  // patch entryway access token verification to handle internal service auth pds -> entryway\n  const origValidateAccessToken =\n    server.ctx.authVerifier.validateAccessToken.bind(server.ctx.authVerifier)\n  server.ctx.authVerifier.validateAccessToken = async (req, scopes) => {\n    const jwt = req.headers.authorization?.replace('Bearer ', '') ?? ''\n    const claims = decodeJwt(jwt)\n    if (claims.aud === 'did:example:entryway') {\n      assert(claims.lxm === parseReqNsid(req), 'bad lxm claim in service auth')\n      assert(claims.aud, 'missing aud claim in service auth')\n      assert(claims.iss, 'missing iss claim in service auth')\n      return {\n        artifacts: jwt,\n        credentials: {\n          type: 'access',\n          scope: 'com.atproto.access' as any,\n          audience: claims.aud,\n          did: claims.iss,\n        },\n      }\n    }\n    return origValidateAccessToken(req, scopes)\n  }\n  // @TODO temp hack because entryway teardown calls signupActivator.run() by mistake\n  server.ctx.signupActivator.run = server.ctx.signupActivator.destroy\n  return server\n}\n\nconst getPublicHex = (key: Secp256k1Keypair) => {\n  return key.publicKeyStr('hex')\n}\n\nconst getPrivateHex = async (key: Secp256k1Keypair) => {\n  return ui8.toString(await key.export(), 'hex')\n}\n"
  },
  {
    "path": "packages/pds/tests/file-uploads.test.ts",
    "content": "import fs from 'node:fs/promises'\nimport { gzipSync } from 'node:zlib'\nimport * as uint8arrays from 'uint8arrays'\nimport { AtpAgent } from '@atproto/api'\nimport { randomBytes } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { BlobRef } from '@atproto/lexicon'\nimport { AppContext } from '../src'\nimport { ActorDb } from '../src/actor-store/db'\nimport { DiskBlobStore } from '../src/disk-blobstore'\nimport { users } from './seeds/users'\n\ndescribe('file uploads', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let aliceDb: ActorDb\n  let alice: string\n  let bob: string\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'file_uploads',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await sc.createAccount('alice', users.alice)\n    await sc.createAccount('bob', users.bob)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    aliceDb = await network.pds.ctx.actorStore.openDb(alice)\n  })\n\n  afterAll(async () => {\n    aliceDb.close()\n    await network.close()\n  })\n\n  let smallBlob: BlobRef\n  let smallFile: Uint8Array\n\n  it('handles client abort', async () => {\n    const abortController = new AbortController()\n    const BlobStore = ctx.blobstore('did:invalid')\n      .constructor as typeof DiskBlobStore\n    const _putTemp = BlobStore.prototype.putTemp\n    BlobStore.prototype.putTemp = function (...args) {\n      // Abort just as processing blob in packages/pds/src/services/repo/blobs.ts\n      process.nextTick(() => abortController.abort())\n      return _putTemp.call(this, ...args)\n    }\n    const response = fetch(\n      `${network.pds.url}/xrpc/com.atproto.repo.uploadBlob`,\n      {\n        method: 'post',\n        body: Buffer.alloc(5000000), // Enough bytes to get some chunking going on\n        signal: abortController.signal,\n        headers: {\n          ...sc.getHeaders(alice),\n          'content-type': 'image/jpeg',\n        },\n      },\n    )\n    await expect(response).rejects.toThrow('operation was aborted')\n    // Cleanup\n    BlobStore.prototype.putTemp = _putTemp\n    // This test would fail from an uncaught exception: this grace period gives time for that to surface\n    await new Promise((res) => setTimeout(res, 10))\n  })\n\n  it('uploads files', async () => {\n    smallFile = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')\n    const res = await agent.com.atproto.repo.uploadBlob(smallFile, {\n      headers: sc.getHeaders(alice),\n      encoding: 'image/jpeg',\n    })\n    smallBlob = res.data.blob\n\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', smallBlob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.mimeType).toBe('image/jpeg')\n    expect(found?.size).toBe(smallFile.length)\n    expect(found?.tempKey).toBeDefined()\n    const hasKey = await ctx.blobstore(alice).hasTemp(found?.tempKey as string)\n    expect(hasKey).toBeTruthy()\n  })\n\n  it('can reference the file', async () => {\n    await sc.updateProfile(alice, { displayName: 'Alice', avatar: smallBlob })\n  })\n\n  it('after being referenced, the file is moved to permanent storage', async () => {\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', smallBlob.ref.toString())\n      .executeTakeFirst()\n    expect(found?.tempKey).toBeNull()\n    const hasStored = ctx.blobstore(alice).hasStored(smallBlob.ref)\n    expect(hasStored).toBeTruthy()\n    const storedBytes = await ctx.blobstore(alice).getBytes(smallBlob.ref)\n    expect(uint8arrays.equals(smallFile, storedBytes)).toBeTruthy()\n  })\n\n  it('can fetch the file after being referenced', async () => {\n    const { headers, data } = await agent.com.atproto.sync.getBlob({\n      did: alice,\n      cid: smallBlob.ref.toString(),\n    })\n    expect(headers['content-type']).toEqual('image/jpeg')\n    expect(headers['content-security-policy']).toEqual(\n      `default-src 'none'; sandbox`,\n    )\n    expect(headers['x-content-type-options']).toEqual('nosniff')\n    expect(uint8arrays.equals(smallFile, new Uint8Array(data))).toBeTruthy()\n  })\n\n  let largeBlob: BlobRef\n  let largeFile: Uint8Array\n\n  it('does not allow referencing a file that is outside blob constraints', async () => {\n    largeFile = await fs.readFile('../dev-env/assets/hd-key.jpg')\n    const res = await agent.com.atproto.repo.uploadBlob(largeFile, {\n      headers: sc.getHeaders(alice),\n      encoding: 'image/jpeg',\n    })\n    largeBlob = res.data.blob\n\n    const profilePromise = sc.updateProfile(alice, {\n      avatar: largeBlob,\n    })\n\n    await expect(profilePromise).rejects.toThrow()\n  })\n\n  it('does not make a blob permanent if referencing failed', async () => {\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', largeBlob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.tempKey).toBeDefined()\n    const hasTemp = await ctx.blobstore(alice).hasTemp(found?.tempKey as string)\n    expect(hasTemp).toBeTruthy()\n    const hasStored = await ctx.blobstore(alice).hasStored(largeBlob.ref)\n    expect(hasStored).toBeFalsy()\n  })\n\n  it('permits duplicate uploads of the same file', async () => {\n    const file = await fs.readFile('../dev-env/assets/key-landscape-small.jpg')\n    const { data: uploadA } = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(alice),\n      encoding: 'image/jpeg',\n    } as any)\n    const { data: uploadB } = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(bob),\n      encoding: 'image/jpeg',\n    } as any)\n    expect(uploadA).toEqual(uploadB)\n\n    await sc.updateProfile(alice, {\n      displayName: 'Alice',\n      avatar: uploadA.blob,\n    })\n    const profileA = await agent.app.bsky.actor.profile.get({\n      repo: alice,\n      rkey: 'self',\n    })\n    // @ts-expect-error \"cid\" is not documented as \"com.atproto.repo.uploadBlob\" output\n    expect((profileA.value as any).avatar.cid).toEqual(uploadA.cid)\n    await sc.updateProfile(bob, {\n      displayName: 'Bob',\n      avatar: uploadB.blob,\n    })\n    const profileB = await agent.app.bsky.actor.profile.get({\n      repo: bob,\n      rkey: 'self',\n    })\n    // @ts-expect-error \"cid\" is not documented as \"com.atproto.repo.uploadBlob\" output\n    expect((profileB.value as any).avatar.cid).toEqual(uploadA.cid)\n    const { data: uploadAfterPermanent } =\n      await agent.com.atproto.repo.uploadBlob(file, {\n        headers: sc.getHeaders(alice),\n        encoding: 'image/jpeg',\n      } as any)\n    expect(uploadAfterPermanent).toEqual(uploadA)\n    const blob = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', uploadAfterPermanent.blob.ref.toString())\n      .executeTakeFirstOrThrow()\n    expect(blob.tempKey).toEqual(null)\n  })\n\n  it('supports compression during upload', async () => {\n    const { data: uploaded } = await agent.com.atproto.repo.uploadBlob(\n      gzipSync(smallFile),\n      {\n        encoding: 'image/jpeg',\n        headers: {\n          ...sc.getHeaders(alice),\n          'content-encoding': 'gzip',\n        },\n      } as any,\n    )\n    expect(uploaded.blob.ref.equals(smallBlob.ref)).toBeTruthy()\n  })\n\n  it('corrects a bad mimetype', async () => {\n    const file = await fs.readFile('../dev-env/assets/key-landscape-large.jpg')\n    const res = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(alice),\n      encoding: 'video/mp4',\n    } as any)\n\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', res.data.blob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.mimeType).toBe('image/jpeg')\n  })\n\n  it('handles pngs', async () => {\n    const file = await fs.readFile('../dev-env/assets/at.png')\n    const res = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(alice),\n      encoding: 'image/png',\n    })\n\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', res.data.blob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.mimeType).toBe('image/png')\n  })\n\n  it('handles unknown mimetypes', async () => {\n    const file = await randomBytes(20000)\n    const res = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(alice),\n      encoding: 'test/fake',\n    } as any)\n\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', res.data.blob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.mimeType).toBe('test/fake')\n  })\n\n  it('handles text', async () => {\n    const file = 'hello world!'\n    const res = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(alice),\n      encoding: 'text/plain',\n    } as any)\n\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', res.data.blob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.mimeType).toBe('text/plain')\n  })\n\n  it('handles json', async () => {\n    const file = '{\"hello\":\"world\"}'\n    const res = await agent.com.atproto.repo.uploadBlob(file, {\n      headers: sc.getHeaders(alice),\n      encoding: 'application/json',\n    } as any)\n\n    const found = await aliceDb.db\n      .selectFrom('blob')\n      .selectAll()\n      .where('cid', '=', res.data.blob.ref.toString())\n      .executeTakeFirst()\n\n    expect(found?.mimeType).toBe('application/json')\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/handle-validation.test.ts",
    "content": "import { isValidTld } from '@atproto/syntax'\nimport { ensureHandleServiceConstraints } from '../src/handle'\n\ndescribe('handle validation', () => {\n  it('validates service constraints', () => {\n    const domains = ['.bsky.app', '.test']\n    const expectThrow = (handle: string, err: string) => {\n      expect(() => ensureHandleServiceConstraints(handle, domains)).toThrow(err)\n    }\n    expectThrow('john.bsky.io', 'Invalid characters in handle')\n    expectThrow('john.com', 'Invalid characters in handle')\n    expectThrow('j.test', 'Handle too short')\n    expectThrow('uk.test', 'Handle too short')\n    expectThrow('john.test.bsky.app', 'Invalid characters in handle')\n    expectThrow('about.test', 'Reserved handle')\n    expectThrow('atp.test', 'Reserved handle')\n    expectThrow('barackobama.test', 'Reserved handle')\n  })\n\n  it('handles bad tlds', () => {\n    expect(isValidTld('atproto.local')).toBe(false)\n    expect(isValidTld('atproto.arpa')).toBe(false)\n    expect(isValidTld('atproto.invalid')).toBe(false)\n    expect(isValidTld('atproto.localhost')).toBe(false)\n    expect(isValidTld('atproto.onion')).toBe(false)\n    expect(isValidTld('atproto.internal')).toBe(false)\n  })\n\n  it('validates handle length', () => {\n    const domains = [\n      '.loooooooooooooooooong-pds-over18chars.mybsky.mydomain.com',\n      '.test',\n    ]\n    const expectThrow = (handle: string, err: string) => {\n      expect(() => ensureHandleServiceConstraints(handle, domains)).toThrow(err)\n    }\n    const expectNotThrow = (handle: string, _memo: string) => {\n      expect(() =>\n        ensureHandleServiceConstraints(handle, domains),\n      ).not.toThrow()\n    }\n    expectThrow('usernamepartover18c.test', 'Handle too long')\n    expectNotThrow(\n      'u23456789012345678.test',\n      'safe up to 18 chars in first segment of the handle',\n    )\n    expectThrow(\n      'usernamepartover18c.loooooooooooooooooong-pds-over18chars.mybsky.mydomain.com',\n      'Handle too long',\n    )\n    expectNotThrow(\n      'u23456789012345678.loooooooooooooooooong-pds-over18chars.mybsky.mydomain.com',\n      'safe long domain in the handle',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/handles.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { IdResolver } from '@atproto/identity'\nimport { AppContext } from '../src'\nimport basicSeed from './seeds/basic'\n\n// outside of suite so they can be used in mock\nlet alice: string\nlet bob: string\n\njest.mock('node:dns/promises', () => {\n  return {\n    resolveTxt: (domain: string) => {\n      if (domain === '_atproto.alice.external') {\n        return [[`did=${alice}`]]\n      }\n      if (domain === '_atproto.bob.external') {\n        return [[`did=${bob}`]]\n      }\n      return []\n    },\n  }\n})\n\ndescribe('handles', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n  let ctx: AppContext\n  let idResolver: IdResolver\n\n  const newHandle = 'alice2.test'\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'handles',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    idResolver = new IdResolver({ plcUrl: ctx.cfg.identity.plcUrl })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getHandleFromDb = async (did: string): Promise<string | undefined> => {\n    const res = await ctx.accountManager.getAccount(did)\n    return res?.handle ?? undefined\n  }\n\n  it('resolves handles', async () => {\n    const res = await agent.api.com.atproto.identity.resolveHandle({\n      handle: 'alice.test',\n    })\n    expect(res.data.did).toBe(alice)\n  })\n\n  it('resolves non-normalize handles', async () => {\n    const res = await agent.api.com.atproto.identity.resolveHandle({\n      handle: 'aLicE.tEst',\n    })\n    expect(res.data.did).toBe(alice)\n  })\n\n  it('allows a user to change their handle', async () => {\n    await agent.api.com.atproto.identity.updateHandle(\n      { handle: newHandle },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    const attemptOld = agent.api.com.atproto.identity.resolveHandle({\n      handle: 'alice.test',\n    })\n    await expect(attemptOld).rejects.toThrow('Unable to resolve handle')\n    const attemptNew = await agent.api.com.atproto.identity.resolveHandle({\n      handle: newHandle,\n    })\n    expect(attemptNew.data.did).toBe(alice)\n  })\n\n  it('updates their did document', async () => {\n    const data = await idResolver.did.resolveAtprotoData(alice)\n    expect(data.handle).toBe(newHandle)\n  })\n\n  it('allows a user to login with their new handle', async () => {\n    const res = await agent.api.com.atproto.server.createSession({\n      identifier: newHandle,\n      password: sc.accounts[alice].password,\n    })\n    sc.accounts[alice].accessJwt = res.data.accessJwt\n    sc.accounts[alice].refreshJwt = res.data.refreshJwt\n  })\n\n  it('does not allow taking a handle that already exists', async () => {\n    const attempt = agent.api.com.atproto.identity.updateHandle(\n      { handle: 'Bob.test' },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow('Handle already taken: bob.test')\n  })\n\n  it('handle updates are idempotent', async () => {\n    await agent.api.com.atproto.identity.updateHandle(\n      { handle: 'Bob.test' },\n      { headers: sc.getHeaders(bob), encoding: 'application/json' },\n    )\n  })\n\n  it('if handle update fails, it does not update their did document', async () => {\n    const data = await idResolver.did.resolveAtprotoData(alice)\n    expect(data.handle).toBe(newHandle)\n  })\n\n  it('disallows improperly formatted handles', async () => {\n    const tryHandle = async (handle: string) => {\n      await agent.api.com.atproto.identity.updateHandle(\n        { handle },\n        { headers: sc.getHeaders(alice), encoding: 'application/json' },\n      )\n    }\n    await expect(tryHandle('did:john')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('john.bsky.io')).rejects.toThrow(\n      'External handle did not resolve to DID',\n    )\n    await expect(tryHandle('j.test')).rejects.toThrow('Handle too short')\n    await expect(tryHandle('jayromy-johnber12345678910.test')).rejects.toThrow(\n      'Handle too long',\n    )\n    await expect(tryHandle('jo_hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo!hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo%hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo&hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo*hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo|hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo:hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('jo/hn.test')).rejects.toThrow(\n      'Input/handle must be a valid handle',\n    )\n    await expect(tryHandle('about.test')).rejects.toThrow('Reserved handle')\n    await expect(tryHandle('atp.test')).rejects.toThrow('Reserved handle')\n  })\n\n  it('allows updating to a dns handles', async () => {\n    await agent.api.com.atproto.identity.updateHandle(\n      {\n        handle: 'alice.external',\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    const dbHandle = await getHandleFromDb(alice)\n    expect(dbHandle).toBe('alice.external')\n\n    const data = await idResolver.did.resolveAtprotoData(alice)\n    expect(data.handle).toBe('alice.external')\n  })\n\n  it('does not allow updating to an invalid dns handle', async () => {\n    const attempt = agent.api.com.atproto.identity.updateHandle(\n      {\n        handle: 'bob.external',\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await expect(attempt).rejects.toThrow(\n      'External handle did not resolve to DID',\n    )\n\n    const attempt2 = agent.api.com.atproto.identity.updateHandle(\n      {\n        handle: 'noexist.external',\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    await expect(attempt2).rejects.toThrow(\n      'External handle did not resolve to DID',\n    )\n\n    const dbHandle = await getHandleFromDb(alice)\n    expect(dbHandle).toBe('alice.external')\n  })\n\n  it('allows admin overrules of service domains', async () => {\n    await agent.api.com.atproto.admin.updateAccountHandle(\n      {\n        did: bob,\n        handle: 'bob-alt.test',\n      },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n\n    const dbHandle = await getHandleFromDb(bob)\n    expect(dbHandle).toBe('bob-alt.test')\n  })\n\n  it('allows admin override of reserved domains', async () => {\n    await agent.api.com.atproto.admin.updateAccountHandle(\n      {\n        did: bob,\n        handle: 'dril.test',\n      },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n\n    const dbHandle = await getHandleFromDb(bob)\n    expect(dbHandle).toBe('dril.test')\n  })\n\n  it('requires admin auth', async () => {\n    const attempt = agent.api.com.atproto.admin.updateAccountHandle(\n      {\n        did: bob,\n        handle: 'bob-alt.test',\n      },\n      {\n        headers: sc.getHeaders(bob),\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow('Authentication Required')\n    const attempt2 = agent.api.com.atproto.admin.updateAccountHandle({\n      did: bob,\n      handle: 'bob-alt.test',\n    })\n    await expect(attempt2).rejects.toThrow('Authentication Required')\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/invite-codes.test.ts",
    "content": "import { AtpAgent, ComAtprotoServerCreateAccount } from '@atproto/api'\nimport { DAY } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { AppContext } from '../src'\nimport { genInvCodes } from '../src/api/com/atproto/server/util'\n\ndescribe('account', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let agent: AtpAgent\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'invite_codes',\n      pds: {\n        inviteRequired: true,\n        inviteInterval: DAY,\n        inviteEpoch: Date.now() - 3 * DAY,\n      },\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    agent = network.pds.getClient()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('describes the fact that invites are required', async () => {\n    const res = await agent.api.com.atproto.server.describeServer({})\n    expect(res.data.inviteCodeRequired).toBe(true)\n  })\n\n  it('succeeds with a valid code', async () => {\n    const code = await createInviteCode(network, agent, 1)\n    await createAccountWithInvite(agent, code)\n  })\n\n  it('fails on bad invite code', async () => {\n    const promise = createAccountWithInvite(agent, 'fake-invite')\n    await expect(promise).rejects.toThrow(\n      ComAtprotoServerCreateAccount.InvalidInviteCodeError,\n    )\n  })\n\n  it('fails on invite code from takendown account', async () => {\n    const account = await makeLoggedInAccount(network, agent)\n    // assign an invite code to the user\n    const code = await createInviteCode(network, agent, 1, account.did)\n    // takedown the user's account\n    const subject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: account.did,\n    }\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject,\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    // attempt to create account with the previously generated invite code\n    const promise = createAccountWithInvite(agent, code)\n    await expect(promise).rejects.toThrow(\n      ComAtprotoServerCreateAccount.InvalidInviteCodeError,\n    )\n\n    // double check that reversing the takedown action makes the invite code valid again\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    // attempt to create account with the previously generated invite code\n    await createAccountWithInvite(agent, code)\n  })\n\n  it('fails on used up invite code', async () => {\n    const code = await createInviteCode(network, agent, 2)\n    await createAccountsWithInvite(agent, code, 2)\n    const promise = createAccountWithInvite(agent, code)\n    await expect(promise).rejects.toThrow(\n      ComAtprotoServerCreateAccount.InvalidInviteCodeError,\n    )\n  })\n\n  it('handles racing invite code uses', async () => {\n    const inviteCode = await createInviteCode(network, agent, 1)\n    const COUNT = 10\n\n    let successes = 0\n    let failures = 0\n    const promises: Promise<unknown>[] = []\n    for (let i = 0; i < COUNT; i++) {\n      const attempt = async () => {\n        try {\n          await createAccountWithInvite(agent, inviteCode)\n          successes++\n        } catch (err) {\n          failures++\n        }\n      }\n      promises.push(attempt())\n    }\n    await Promise.all(promises)\n    expect(successes).toBe(1)\n    expect(failures).toBe(9)\n  })\n\n  it('allow users to get available user invites', async () => {\n    const account = await makeLoggedInAccount(network, agent)\n\n    // no codes available yet\n    const res1 = await account.com.atproto.server.getAccountInviteCodes()\n    expect(res1.data.codes.length).toBe(0)\n\n    // next, pretend account was made 2 days in the past\n    const twoDaysAgo = new Date(Date.now() - 2 * DAY).toISOString()\n    await ctx.accountManager.db.db\n      .updateTable('actor')\n      .set({ createdAt: twoDaysAgo })\n      .where('did', '=', account.accountDid)\n      .execute()\n    const res2 = await account.com.atproto.server.getAccountInviteCodes()\n    expect(res2.data.codes.length).toBe(2)\n\n    // use both invites and confirm we can't get any more\n    for (const code of res2.data.codes) {\n      await createAccountWithInvite(agent, code.code)\n    }\n\n    const res3 = await account.com.atproto.server.getAccountInviteCodes()\n    expect(res3.data.codes.length).toBe(2)\n  })\n\n  it('admin gifted codes to not impact a users available codes', async () => {\n    const account = await makeLoggedInAccount(network, agent)\n\n    // again, pretend account was made 2 days ago\n    const twoDaysAgo = new Date(Date.now() - 2 * DAY).toISOString()\n    await ctx.accountManager.db.db\n      .updateTable('actor')\n      .set({ createdAt: twoDaysAgo })\n      .where('did', '=', account.accountDid)\n      .execute()\n\n    await createInviteCode(network, agent, 1, account.accountDid)\n    await createInviteCode(network, agent, 1, account.accountDid)\n    await createInviteCode(network, agent, 1, account.accountDid)\n\n    const res = await account.com.atproto.server.getAccountInviteCodes()\n    expect(res.data.codes.length).toBe(5)\n\n    const fromAdmin = res.data.codes.filter(\n      (code) => code.createdBy === 'admin',\n    )\n    expect(fromAdmin.length).toBe(3)\n\n    const fromSelf = res.data.codes.filter(\n      (code) => code.createdBy === account.accountDid,\n    )\n    expect(fromSelf.length).toBe(2)\n  })\n\n  it('creates invites based on epoch', async () => {\n    const account = await makeLoggedInAccount(network, agent)\n\n    // first, pretend account was made 2 days ago & get those two codes\n    const twoDaysAgo = new Date(Date.now() - 2 * DAY).toISOString()\n    await ctx.accountManager.db.db\n      .updateTable('actor')\n      .set({ createdAt: twoDaysAgo })\n      .where('did', '=', account.accountDid)\n      .execute()\n\n    const res1 = await account.com.atproto.server.getAccountInviteCodes()\n    expect(res1.data.codes.length).toBe(2)\n\n    // then pretend account was made ever so slightly over 10 days ago\n    const tenDaysAgo = new Date(Date.now() - 10.01 * DAY).toISOString()\n    await ctx.accountManager.db.db\n      .updateTable('actor')\n      .set({ createdAt: tenDaysAgo })\n      .where('did', '=', account.accountDid)\n      .execute()\n\n    // we have a 3 day epoch so should still get 3 code\n    const res2 = await account.com.atproto.server.getAccountInviteCodes()\n    expect(res2.data.codes.length).toBe(3)\n\n    // use up these codes\n    for (const code of res2.data.codes) {\n      await createAccountWithInvite(agent, code.code)\n    }\n\n    // we pad their account with some additional unused codes from the past which should not allow them to generate anymore\n    const inviteRows = genInvCodes(ctx.cfg, 10).map((code) => ({\n      code: code,\n      availableUses: 1,\n      disabled: 0 as const,\n      forAccount: account.accountDid,\n      createdBy: account.accountDid,\n      createdAt: new Date(Date.now() - 5 * DAY).toISOString(),\n    }))\n    await ctx.accountManager.db.db\n      .insertInto('invite_code')\n      .values(inviteRows)\n      .execute()\n    const res3 = await account.com.atproto.server.getAccountInviteCodes({\n      includeUsed: false,\n    })\n    expect(res3.data.codes.length).toBe(10)\n\n    // no we use the codes which should still not allow them to generate anymore\n    await ctx.accountManager.db.db\n      .insertInto('invite_code_use')\n      .values(\n        inviteRows.map((row) => ({\n          code: row.code,\n          usedBy: 'did:example:test',\n          usedAt: new Date().toISOString(),\n        })),\n      )\n      .execute()\n\n    const res4 = await account.com.atproto.server.getAccountInviteCodes({\n      includeUsed: false,\n    })\n    expect(res4.data.codes.length).toBe(0)\n  })\n\n  it('prevents use of disabled codes', async () => {\n    const first = await createInviteCode(network, agent, 1)\n    const account = await makeLoggedInAccount(network, agent)\n    const second = await createInviteCode(network, agent, 1, account.accountDid)\n\n    // disabled first by code & second by did\n    await agent.api.com.atproto.admin.disableInviteCodes(\n      {\n        codes: [first],\n        accounts: [account.accountDid],\n      },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n\n    await expect(createAccountWithInvite(agent, first)).rejects.toThrow(\n      ComAtprotoServerCreateAccount.InvalidInviteCodeError,\n    )\n    await expect(createAccountWithInvite(agent, second)).rejects.toThrow(\n      ComAtprotoServerCreateAccount.InvalidInviteCodeError,\n    )\n  })\n\n  it('does not allow disabling all admin codes', async () => {\n    const attempt = agent.api.com.atproto.admin.disableInviteCodes(\n      {\n        accounts: ['admin'],\n      },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow('cannot disable admin invite codes')\n  })\n\n  it('creates many invite codes', async () => {\n    const accounts = ['did:example:one', 'did:example:two', 'did:example:three']\n    const res = await agent.api.com.atproto.server.createInviteCodes(\n      {\n        useCount: 2,\n        codeCount: 2,\n        forAccounts: accounts,\n      },\n      {\n        headers: network.pds.adminAuthHeaders(),\n        encoding: 'application/json',\n      },\n    )\n    expect(res.data.codes.length).toBe(3)\n    const fromDb = await ctx.accountManager.db.db\n      .selectFrom('invite_code')\n      .selectAll()\n      .where('forAccount', 'in', accounts)\n      .execute()\n    expect(fromDb.length).toBe(6)\n    const dbCodesByUser = {}\n    for (const row of fromDb) {\n      expect(row.disabled).toBe(0)\n      expect(row.availableUses).toBe(2)\n      dbCodesByUser[row.forAccount] ??= []\n      dbCodesByUser[row.forAccount].push(row.code)\n    }\n    for (const { account, codes } of res.data.codes) {\n      expect(codes.length).toBe(2)\n      expect(codes.sort()).toEqual(dbCodesByUser[account].sort())\n    }\n  })\n})\n\nconst createInviteCode = async (\n  network: TestNetworkNoAppView,\n  agent: AtpAgent,\n  uses: number,\n  forAccount?: string,\n): Promise<string> => {\n  const res = await agent.api.com.atproto.server.createInviteCode(\n    { useCount: uses, forAccount },\n    {\n      headers: network.pds.adminAuthHeaders(),\n      encoding: 'application/json',\n    },\n  )\n  return res.data.code\n}\n\nconst createAccountWithInvite = async (agent: AtpAgent, code: string) => {\n  const name = crypto.randomStr(5, 'base32')\n  const res = await agent.api.com.atproto.server.createAccount({\n    email: `${name}@test.com`,\n    handle: `${name}.test`,\n    password: name,\n    inviteCode: code,\n  })\n  return {\n    ...res.data,\n    password: name,\n  }\n}\n\nconst createAccountsWithInvite = async (\n  agent: AtpAgent,\n  code: string,\n  count = 0,\n) => {\n  for (let i = 0; i < count; i++) {\n    await createAccountWithInvite(agent, code)\n  }\n}\n\nconst makeLoggedInAccount = async (\n  network: TestNetworkNoAppView,\n  inviterAgent: AtpAgent,\n) => {\n  const code = await createInviteCode(network, inviterAgent, 1)\n  const account = await createAccountWithInvite(inviterAgent, code)\n  const agent = network.pds.getClient()\n  await agent.login({\n    identifier: account.handle,\n    password: account.password,\n  })\n  return agent\n}\n"
  },
  {
    "path": "packages/pds/tests/invites-admin.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { randomStr } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\n\ndescribe('pds admin invite views', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'views_admin_invites',\n      pds: {\n        inviteRequired: true,\n        inviteInterval: 1,\n      },\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  let alice: string\n  let bob: string\n  let carol: string\n\n  beforeAll(async () => {\n    const adminCode = await agent.api.com.atproto.server.createInviteCode(\n      { useCount: 10 },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n\n    await sc.createAccount('alice', {\n      handle: 'alice.test',\n      email: 'alice@test.com',\n      password: 'alice',\n      inviteCode: adminCode.data.code,\n    })\n    await sc.createAccount('bob', {\n      handle: 'bob.test',\n      email: 'bob@test.com',\n      password: 'bob',\n      inviteCode: adminCode.data.code,\n    })\n    await sc.createAccount('carol', {\n      handle: 'carol.test',\n      email: 'carol@test.com',\n      password: 'carol',\n      inviteCode: adminCode.data.code,\n    })\n\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n\n    const aliceCodes = await agent.api.com.atproto.server.getAccountInviteCodes(\n      {},\n      { headers: sc.getHeaders(alice) },\n    )\n    await agent.api.com.atproto.server.getAccountInviteCodes(\n      {},\n      { headers: sc.getHeaders(bob) },\n    )\n    await agent.api.com.atproto.server.createInviteCode(\n      { useCount: 5, forAccount: alice },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n    await agent.api.com.atproto.admin.disableInviteCodes(\n      { codes: [adminCode.data.code], accounts: [bob] },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n\n    const useCode = async (code: string) => {\n      const name = randomStr(8, 'base32')\n      await agent.api.com.atproto.server.createAccount({\n        handle: `${name}.test`,\n        email: `${name}@test.com`,\n        password: name,\n        inviteCode: code,\n      })\n    }\n\n    await useCode(aliceCodes.data.codes[0].code)\n    await useCode(aliceCodes.data.codes[1].code)\n  })\n\n  it('gets a list of invite codes by recency', async () => {\n    const result = await agent.api.com.atproto.admin.getInviteCodes(\n      {},\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    let lastDate = result.data.codes[0].createdAt\n    for (const code of result.data.codes) {\n      expect(code.createdAt <= lastDate).toBeTruthy()\n      lastDate = code.createdAt\n    }\n    expect(result.data.codes.length).toBe(12)\n    expect(result.data.codes[0]).toMatchObject({\n      available: 5,\n      disabled: false,\n      forAccount: alice,\n      createdBy: 'admin',\n    })\n    expect(result.data.codes[0].uses.length).toBe(0)\n    expect(result.data.codes.at(-1)).toMatchObject({\n      available: 10,\n      disabled: true,\n      forAccount: 'admin',\n      createdBy: 'admin',\n    })\n    expect(result.data.codes.at(-1)?.uses.length).toBe(3)\n  })\n\n  it('paginates by recency', async () => {\n    const full = await agent.api.com.atproto.admin.getInviteCodes(\n      {},\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    const first = await agent.api.com.atproto.admin.getInviteCodes(\n      { limit: 5 },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    const second = await agent.api.com.atproto.admin.getInviteCodes(\n      { cursor: first.data.cursor },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    const combined = [...first.data.codes, ...second.data.codes]\n    expect(combined).toEqual(full.data.codes)\n  })\n\n  it('gets a list of invite codes by usage', async () => {\n    const result = await agent.api.com.atproto.admin.getInviteCodes(\n      { sort: 'usage' },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    let lastUseCount = result.data.codes[0].uses.length\n    for (const code of result.data.codes) {\n      expect(code.uses.length).toBeLessThanOrEqual(lastUseCount)\n      lastUseCount = code.uses.length\n    }\n    expect(result.data.codes[0]).toMatchObject({\n      available: 10,\n      disabled: true,\n      forAccount: 'admin',\n      createdBy: 'admin',\n    })\n    expect(result.data.codes[0].uses.length).toBe(3)\n  })\n\n  it('paginates by usage', async () => {\n    const full = await agent.api.com.atproto.admin.getInviteCodes(\n      { sort: 'usage' },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    const first = await agent.api.com.atproto.admin.getInviteCodes(\n      { sort: 'usage', limit: 5 },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    const second = await agent.api.com.atproto.admin.getInviteCodes(\n      { sort: 'usage', cursor: first.data.cursor },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    const combined = [...first.data.codes, ...second.data.codes]\n    expect(combined).toEqual(full.data.codes)\n  })\n\n  it('hydrates invites into admin.getAccountInfo', async () => {\n    const aliceView = await agent.api.com.atproto.admin.getAccountInfo(\n      { did: alice },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(aliceView.data.invitedBy?.available).toBe(10)\n    expect(aliceView.data.invitedBy?.uses.length).toBe(3)\n    expect(aliceView.data.invites?.length).toBe(6)\n  })\n\n  it('disables an account from getting additional invite codes', async () => {\n    await agent.api.com.atproto.admin.disableAccountInvites(\n      { account: carol },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n\n    const repoRes = await agent.api.com.atproto.admin.getAccountInfo(\n      { did: carol },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(repoRes.data.invitesDisabled).toBe(true)\n\n    const invRes = await agent.api.com.atproto.server.getAccountInviteCodes(\n      {},\n      { headers: sc.getHeaders(carol) },\n    )\n    expect(invRes.data.codes.length).toBe(0)\n  })\n\n  it('allows setting reason when enabling and disabling invite codes', async () => {\n    await agent.api.com.atproto.admin.enableAccountInvites(\n      { account: carol },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n\n    const afterEnable = await agent.api.com.atproto.admin.getAccountInfo(\n      { did: carol },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(afterEnable.data.invitesDisabled).toBe(false)\n\n    await agent.api.com.atproto.admin.disableAccountInvites(\n      { account: carol },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n\n    const afterDisable = await agent.api.com.atproto.admin.getAccountInfo(\n      { did: carol },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(afterDisable.data.invitesDisabled).toBe(true)\n  })\n\n  it('creates codes in the background but disables them', async () => {\n    const res = await network.pds.ctx.accountManager.db.db\n      .selectFrom('invite_code')\n      .where('forAccount', '=', carol)\n      .selectAll()\n      .execute()\n    expect(res.length).toBe(5)\n    expect(res.every((row) => row.disabled === 1))\n  })\n\n  it('re-enables an accounts invites', async () => {\n    await agent.api.com.atproto.admin.enableAccountInvites(\n      { account: carol },\n      { encoding: 'application/json', headers: network.pds.adminAuthHeaders() },\n    )\n\n    const repoRes = await agent.api.com.atproto.admin.getAccountInfo(\n      { did: carol },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    expect(repoRes.data.invitesDisabled).toBe(false)\n\n    const invRes = await agent.api.com.atproto.server.getAccountInviteCodes(\n      {},\n      { headers: sc.getHeaders(carol) },\n    )\n    expect(invRes.data.codes.length).toBeGreaterThan(0)\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/moderation.test.ts",
    "content": "import assert from 'node:assert'\nimport { AtpAgent } from '@atproto/api'\nimport { ImageRef, SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { BlobNotFoundError } from '@atproto/repo'\nimport {\n  RepoBlobRef,\n  RepoRef,\n  isRepoBlobRef,\n  isRepoRef,\n} from '../src/lexicon/types/com/atproto/admin/defs'\nimport {\n  Main as StrongRef,\n  isMain as isStrongRef,\n} from '../src/lexicon/types/com/atproto/repo/strongRef'\nimport { $Typed } from '../src/lexicon/util'\nimport basicSeed from './seeds/basic'\n\ndescribe('moderation', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let repoSubject: $Typed<RepoRef>\n  let recordSubject: $Typed<StrongRef>\n  let blobSubject: $Typed<RepoBlobRef>\n  let blobRef: ImageRef\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'moderation',\n    })\n\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    await network.processAll()\n    repoSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n    const post = sc.posts[sc.dids.carol][0]\n    recordSubject = {\n      $type: 'com.atproto.repo.strongRef',\n      uri: post.ref.uriStr,\n      cid: post.ref.cidStr,\n    }\n    blobRef = post.images[1]\n    blobSubject = {\n      $type: 'com.atproto.admin.defs#repoBlobRef',\n      did: sc.dids.carol,\n      cid: blobRef.image.ref.toString(),\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('takes down accounts', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        did: repoSubject.did,\n      },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    assert(isRepoRef(res.data.subject))\n    expect(res.data.subject.did).toEqual(sc.dids.bob)\n    expect(res.data.takedown?.applied).toBe(true)\n    expect(res.data.takedown?.ref).toBe('test-repo')\n  })\n\n  it('restores takendown accounts', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        did: repoSubject.did,\n      },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    assert(isRepoRef(res.data.subject))\n    expect(res.data.subject.did).toEqual(sc.dids.bob)\n    expect(res.data.takedown?.applied).toBe(false)\n    expect(res.data.takedown?.ref).toBeUndefined()\n  })\n\n  it('takes down records', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: recordSubject,\n        takedown: { applied: true, ref: 'test-record' },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        uri: recordSubject.uri,\n      },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    assert(isStrongRef(res.data.subject))\n    expect(res.data.subject.uri).toEqual(recordSubject.uri)\n    expect(res.data.takedown?.applied).toBe(true)\n    expect(res.data.takedown?.ref).toBe('test-record')\n  })\n\n  it('restores takendown records', async () => {\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: recordSubject,\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        uri: recordSubject.uri,\n      },\n      { headers: network.pds.adminAuthHeaders() },\n    )\n    assert(isStrongRef(res.data.subject))\n    expect(res.data.subject.uri).toEqual(recordSubject.uri)\n    expect(res.data.takedown?.applied).toBe(false)\n    expect(res.data.takedown?.ref).toBeUndefined()\n  })\n\n  describe('blob takedown', () => {\n    it('takes down blobs', async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: blobSubject,\n          takedown: { applied: true, ref: 'test-blob' },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n      const res = await agent.api.com.atproto.admin.getSubjectStatus(\n        {\n          did: blobSubject.did,\n          blob: blobSubject.cid,\n        },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      assert(isRepoBlobRef(res.data.subject))\n      expect(res.data.subject.did).toEqual(blobSubject.did)\n      assert(isRepoBlobRef(res.data.subject))\n      expect(res.data.subject.cid).toEqual(blobSubject.cid)\n      expect(res.data.takedown?.applied).toBe(true)\n      expect(res.data.takedown?.ref).toBe('test-blob')\n    })\n\n    it('removes blob from the store', async () => {\n      const tryGetBytes = network.pds.ctx\n        .blobstore(blobSubject.did)\n        .getBytes(blobRef.image.ref)\n      await expect(tryGetBytes).rejects.toThrow(BlobNotFoundError)\n    })\n\n    it('prevents blob from being referenced again.', async () => {\n      const referenceBlob = sc.post(sc.dids.carol, 'pic', [], [blobRef])\n      await expect(referenceBlob).rejects.toThrow('Could not find blob:')\n    })\n\n    it('prevents blob from being reuploaded', async () => {\n      const attempt = sc.uploadFile(\n        sc.dids.carol,\n        '../dev-env/assets/key-alt.jpg',\n        'image/jpeg',\n      )\n      await expect(attempt).rejects.toThrow(\n        'Blob has been takendown, cannot re-upload',\n      )\n    })\n\n    it('prevents image blob from being served.', async () => {\n      const attempt = agent.api.com.atproto.sync.getBlob({\n        did: sc.dids.carol,\n        cid: blobRef.image.ref.toString(),\n      })\n      await expect(attempt).rejects.toThrow('Blob not found')\n    })\n\n    it('restores blob when takedown is removed', async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: blobSubject,\n          takedown: { applied: false },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n\n      // Can post and reference blob\n      const post = await sc.post(sc.dids.carol, 'pic', [], [blobRef])\n      expect(post.images[0].image.ref.equals(blobRef.image.ref)).toBeTruthy()\n\n      // Can fetch through image server\n      const res = await agent.api.com.atproto.sync.getBlob({\n        did: sc.dids.carol,\n        cid: blobRef.image.ref.toString(),\n      })\n\n      expect(res.data.byteLength).toBeGreaterThan(9000)\n    })\n\n    it('prevents blobs of takendown accounts from being served.', async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.carol,\n          },\n          takedown: { applied: true },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n      const blobParams = {\n        did: sc.dids.carol,\n        cid: blobRef.image.ref.toString(),\n      }\n      // public, disallow\n      const attempt1 = agent.api.com.atproto.sync.getBlob(blobParams)\n      await expect(attempt1).rejects.toThrow(/Repo has been takendown/)\n      // logged-in, disallow\n      const attempt2 = agent.api.com.atproto.sync.getBlob(blobParams, {\n        headers: sc.getHeaders(sc.dids.bob),\n      })\n      await expect(attempt2).rejects.toThrow(/Repo has been takendown/)\n      // logged-in as account, allow\n      const res1 = await agent.api.com.atproto.sync.getBlob(blobParams, {\n        headers: sc.getHeaders(sc.dids.carol),\n      })\n      expect(res1.data.byteLength).toBeGreaterThan(9000)\n      // admin role, allow\n      const res2 = await agent.api.com.atproto.sync.getBlob(blobParams, {\n        headers: network.pds.adminAuthHeaders(),\n      })\n      expect(res2.data.byteLength).toBeGreaterThan(9000)\n      // revert takedown\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.carol,\n          },\n          takedown: { applied: false },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/moderator-auth.test.ts",
    "content": "import assert from 'node:assert'\nimport * as plc from '@did-plc/lib'\nimport { AtpAgent } from '@atproto/api'\nimport { Keypair, Secp256k1Keypair } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { createServiceAuthHeaders } from '@atproto/xrpc-server'\nimport { ids } from '../src/lexicon/lexicons'\nimport { RepoRef, isRepoRef } from '../src/lexicon/types/com/atproto/admin/defs'\nimport { $Typed } from '../src/lexicon/util'\nimport usersSeed from './seeds/users'\n\ndescribe('moderator auth', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let repoSubject: $Typed<RepoRef>\n\n  let modServiceDid: string\n  let altModDid: string\n  let modServiceKey: Secp256k1Keypair\n  let pdsDid: string\n\n  const opAndDid = async (handle: string, key: Keypair) => {\n    const op = await plc.signOperation(\n      {\n        type: 'plc_operation',\n        alsoKnownAs: [handle],\n        verificationMethods: {\n          atproto: key.did(),\n        },\n        rotationKeys: [key.did()],\n        services: {},\n        prev: null,\n      },\n      key,\n    )\n    const did = await plc.didForCreateOp(op)\n    return { op, did }\n  }\n\n  beforeAll(async () => {\n    // kinda goofy but we need to know the dids before creating the testnet for the PDS's config\n    modServiceKey = await Secp256k1Keypair.create()\n    const modServiceInfo = await opAndDid('mod.test', modServiceKey)\n    const altModInfo = await opAndDid('alt-mod.test', modServiceKey)\n    modServiceDid = modServiceInfo.did\n    altModDid = altModInfo.did\n\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'pds_moderator_auth',\n      pds: {\n        modServiceDid: modServiceInfo.did,\n        modServiceUrl: 'https://mod.invalid',\n      },\n    })\n\n    pdsDid = network.pds.ctx.cfg.service.did\n\n    const plcClient = network.plc.getClient()\n    await plcClient.sendOperation(modServiceInfo.did, modServiceInfo.op)\n    await plcClient.sendOperation(altModInfo.did, altModInfo.op)\n\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    await network.processAll()\n    repoSubject = {\n      $type: 'com.atproto.admin.defs#repoRef',\n      did: sc.dids.bob,\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('allows service auth requests from the configured appview did', async () => {\n    const updateHeaders = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: pdsDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: modServiceKey,\n    })\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...updateHeaders,\n        encoding: 'application/json',\n      },\n    )\n\n    const getHeaders = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: pdsDid,\n      lxm: ids.ComAtprotoAdminGetSubjectStatus,\n      keypair: modServiceKey,\n    })\n    const res = await agent.api.com.atproto.admin.getSubjectStatus(\n      {\n        did: repoSubject.did,\n      },\n      getHeaders,\n    )\n    assert(isRepoRef(res.data.subject))\n    expect(res.data.subject.did).toBe(repoSubject.did)\n    expect(res.data.takedown?.applied).toBe(true)\n  })\n\n  it('does not allow requests from another did', async () => {\n    const headers = await createServiceAuthHeaders({\n      iss: altModDid,\n      aud: pdsDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: modServiceKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow('Untrusted issuer')\n  })\n\n  it('does not allow requests with a bad signature', async () => {\n    const badKey = await Secp256k1Keypair.create()\n    const headers = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: pdsDid,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: badKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow(\n      'jwt signature does not match jwt issuer',\n    )\n  })\n\n  it('does not allow requests with a bad aud', async () => {\n    // repo subject is bob, so we set alice as the audience\n    const headers = await createServiceAuthHeaders({\n      iss: modServiceDid,\n      aud: sc.dids.alice,\n      lxm: ids.ComAtprotoAdminUpdateSubjectStatus,\n      keypair: modServiceKey,\n    })\n    const attempt = agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: repoSubject,\n        takedown: { applied: true, ref: 'test-repo' },\n      },\n      {\n        ...headers,\n        encoding: 'application/json',\n      },\n    )\n    await expect(attempt).rejects.toThrow(\n      'jwt audience does not match service did',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/oauth.test.ts",
    "content": "import { once } from 'node:events'\nimport {\n  IncomingMessage,\n  Server,\n  ServerResponse,\n  createServer,\n} from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { type Browser, launch } from 'puppeteer'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport files from '@atproto/oauth-client-browser-example' with { type: 'json' }\nimport { PageHelper } from './_puppeteer.js'\n\ndescribe('oauth', () => {\n  let browser: Browser\n  let network: TestNetworkNoAppView\n  let server: Server\n\n  let appUrl: string\n\n  // @NOTE We are using another language than \"en\" as default language to\n  // test the language negotiation.\n  const languages = ['fr-BE', 'fr', 'en-US', 'en']\n\n  beforeAll(async () => {\n    browser = await launch({\n      browser: 'chrome', // \"firefox\"\n\n      // For debugging:\n      // headless: false,\n      // devtools: true,\n      // slowMo: 250,\n    })\n\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'oauth',\n    })\n\n    const sc = network.getSeedClient()\n\n    await sc.createAccount('alice', {\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n\n    server = createServer(clientHandler)\n    server.listen(0)\n    await once(server, 'listening')\n\n    const { port } = server.address() as AddressInfo\n\n    appUrl = `http://127.0.0.1:${port}?${new URLSearchParams({\n      plc_directory_url: network.plc.url,\n      handle_resolver: network.pds.url,\n      sign_up_url: network.pds.url,\n      env: 'test',\n      scope: `account:email identity:* repo:*`,\n    })}`\n  })\n\n  afterAll(async () => {\n    await server?.close()\n    await network?.close()\n    await browser?.close()\n  })\n\n  // This uses prompt=create under the hood:\n  it('Allows to sign-up through OAuth', async () => {\n    const page = await PageHelper.from(browser, { languages })\n\n    await page.goto(appUrl)\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.navigationAction(async () => {\n      await page.clickOnButton(`Sign up with ${new URL(network.pds.url).host}`)\n    })\n\n    await page.checkTitle('Créer un compte')\n\n    await page.typeInInput('handle', 'bob')\n\n    await page.clickOnButton('Suivant')\n\n    await page.typeInInput('email', 'bob@test.com')\n    await page.typeInInput('password', 'bob-pass')\n\n    await page.clickOnButton(\"S'inscrire\")\n\n    await page.ensureTextVisibility(\n      `L'application demande un contrôle total sur votre identité, ce qui signifie qu'elle pourrait casser de façon permanente, ou même usurper, votre compte. N'authorisez l'accès qu'aux applications auxquelles vous faites vraiment confiance.`,\n    )\n\n    // Make sure the new account is propagated to the PLC directory, allowing\n    // the client to resolve the account's did\n    await network.processAll()\n\n    await page.navigationAction(async () => {\n      await page.clickOnButton(\"Authoriser l'accès\")\n    })\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.ensureTextVisibility('Token info', 'h2')\n\n    await page.clickOn('button[aria-label=\"User menu\"]')\n\n    await page.clickOnButton('Sign out')\n\n    await page.waitForNetworkIdle()\n\n    // TODO: Find out why we can't use \"using\" here\n    await page[Symbol.asyncDispose]()\n  })\n\n  it('Allows login or signup through OAuth via a choice', async () => {\n    const page = await PageHelper.from(browser, { languages })\n\n    await page.goto(appUrl)\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.navigationAction(async () => {\n      await page.clickOnButton(`Login with ${new URL(network.pds.url).host}`)\n    })\n\n    await page.checkTitle('Authentification')\n\n    await page.ensureTextVisibility('Annuler', 'button')\n    await page.ensureTextVisibility('Se connecter', 'button')\n    await page.ensureTextVisibility('Créer un nouveau compte', 'button')\n\n    // Cancel the OAuth flow:\n    await page.navigationAction(async () => {\n      await page.clickOnButton('Annuler')\n    })\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.ensureTextVisibility('Login with the Atmosphere', 'h2')\n\n    await page.waitForNetworkIdle()\n\n    // TODO: Find out why we can't use \"using\" here\n    await page[Symbol.asyncDispose]()\n  })\n\n  it('allows resetting the password', async () => {\n    const sendTemplateMock = jest\n      .spyOn(network.pds.ctx.mailer, 'sendResetPassword')\n      .mockImplementation(async () => {\n        // noop\n      })\n\n    const page = await PageHelper.from(browser, { languages })\n\n    await page.goto(appUrl)\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.navigationAction(async () => {\n      const input = await page.typeIn('input[name=\"identifier\"]', 'alice.test')\n\n      await input.press('Enter')\n    })\n\n    await page.checkTitle('Connexion')\n\n    await page.clickOnButton('Oublié ?')\n\n    await page.checkTitle('Mot de passe oublié')\n\n    await page.typeInInput('email', 'alice@test.com')\n\n    expect(sendTemplateMock).toHaveBeenCalledTimes(0)\n\n    await page.clickOnButton('Suivant')\n\n    await page.checkTitle('Réinitialiser le mot de passe')\n\n    expect(sendTemplateMock).toHaveBeenCalledTimes(1)\n\n    const [params] = sendTemplateMock.mock.lastCall\n    expect(params).toEqual({\n      handle: 'alice.test',\n      token: expect.any(String),\n    })\n\n    await page.typeInInput('code', params.token)\n\n    await page.typeInInput('password', 'alice-new-pass')\n\n    await page.clickOnButton('Suivant')\n\n    await page.checkTitle('Mot de passe mis à jour')\n\n    await page.ensureTextVisibility('Mot de passe mis à jour !', 'h2')\n\n    // TODO: Find out why we can't use \"using\" here\n    await page[Symbol.asyncDispose]()\n\n    sendTemplateMock.mockRestore()\n  })\n\n  it('Allows to sign-in through OAuth', async () => {\n    const page = await PageHelper.from(browser, { languages })\n\n    await page.goto(appUrl)\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.navigationAction(async () => {\n      const input = await page.typeIn('input[name=\"identifier\"]', 'alice.test')\n\n      await input.press('Enter')\n    })\n\n    await page.checkTitle('Connexion')\n\n    await page.typeIn('input[type=\"password\"]', 'alice-new-pass')\n\n    // Make sure the warning is visible\n    await page.ensureTextVisibility('Avertissement', 'h3')\n\n    await page.clickOn(\n      'label::-p-text(Se souvenir de ce compte sur cet appareil)',\n    )\n\n    await page.clickOnButton('Se connecter')\n\n    await page.checkTitle(\"Authoriser l'accès\")\n\n    await page.navigationAction(async () => {\n      await page.clickOnButton(\"Authoriser l'accès\")\n    })\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.ensureTextVisibility('Token info', 'h2')\n\n    await page.clickOn('button[aria-label=\"User menu\"]')\n\n    await page.clickOnButton('Sign out')\n\n    await page.waitForNetworkIdle()\n\n    // TODO: Find out why we can't use \"using\" here\n    await page[Symbol.asyncDispose]()\n  })\n\n  it('remembers the session', async () => {\n    const page = await PageHelper.from(browser, { languages })\n\n    await page.goto(appUrl)\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.navigationAction(async () => {\n      const input = await page.typeIn('input[name=\"identifier\"]', 'alice.test')\n\n      await input.press('Enter')\n    })\n\n    await page.checkTitle(\"Authoriser l'accès\")\n\n    await page.navigationAction(async () => {\n      await page.clickOnButton(\"Authoriser l'accès\")\n    })\n\n    await page.checkTitle('OAuth Client Example')\n\n    await page.ensureTextVisibility('Token info', 'h2')\n\n    await page.clickOn('button[aria-label=\"User menu\"]')\n\n    await page.clickOnButton('Sign out')\n\n    await page.waitForNetworkIdle()\n\n    // TODO: Find out why we can't use \"using\" here\n    await page[Symbol.asyncDispose]()\n  })\n})\n\nfunction clientHandler(\n  req: IncomingMessage,\n  res: ServerResponse,\n  next?: (err?: unknown) => void,\n): void {\n  const path = req.url?.split('?')[0].slice(1) || 'index.html'\n  const file = Object.hasOwn(files, path) ? files[path] : null\n\n  if (file) {\n    res\n      .writeHead(200, 'OK', { 'content-type': file.mime })\n      .end(Buffer.from(file.data, 'base64'))\n  } else if (next) {\n    next()\n  } else {\n    res\n      .writeHead(404, 'Not Found', { 'content-type': 'text/plain' })\n      .end('Page not found')\n  }\n}\n"
  },
  {
    "path": "packages/pds/tests/plc-operations.test.ts",
    "content": "import assert from 'node:assert'\nimport { once } from 'node:events'\nimport { EventEmitter } from 'node:stream'\nimport * as plc from '@did-plc/lib'\nimport Mail from 'nodemailer/lib/mailer'\nimport { AtpAgent } from '@atproto/api'\nimport { check } from '@atproto/common'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView, basicSeed } from '@atproto/dev-env'\nimport { AppContext } from '../src'\n\ndescribe('plc operations', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  const mailCatcher = new EventEmitter()\n  let _origSendMail\n\n  let alice: string\n\n  let sampleKey: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'plc_operations',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    const mailer = ctx.mailer\n\n    sc = network.getSeedClient()\n    agent = network.pds.getClient()\n\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    await network.processAll()\n\n    sampleKey = (await Secp256k1Keypair.create()).did()\n\n    // Catch emails for use in tests\n    _origSendMail = mailer.transporter.sendMail\n    mailer.transporter.sendMail = async (opts) => {\n      const result = await _origSendMail.call(mailer.transporter, opts)\n      mailCatcher.emit('mail', opts)\n      return result\n    }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getMailFrom = async (promise): Promise<Mail.Options> => {\n    const result = await Promise.all([once(mailCatcher, 'mail'), promise])\n    return result[0][0]\n  }\n\n  const getTokenFromMail = (mail: Mail.Options) =>\n    mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5})</i)?.[1]\n\n  const signOp = async (did: string, op: Partial<plc.Operation>) => {\n    const lastOp = await ctx.plcClient.getLastOp(did)\n    if (check.is(lastOp, plc.def.tombstone)) {\n      throw new Error('Did is tombstoned')\n    }\n    return plc.createUpdateOp(lastOp, ctx.plcRotationKey, (lastOp) => ({\n      ...lastOp,\n      rotationKeys: op.rotationKeys ?? lastOp.rotationKeys,\n      alsoKnownAs: op.alsoKnownAs ?? lastOp.alsoKnownAs,\n      verificationMethods: op.verificationMethods ?? lastOp.verificationMethods,\n      services: op.services ?? lastOp.services,\n    }))\n  }\n\n  const expectFailedOp = async (\n    did: string,\n    op: Partial<plc.Operation>,\n    expectedErr?: string,\n  ) => {\n    const operation = await signOp(did, op)\n    const attempt = agent.com.atproto.identity.submitPlcOperation(\n      { operation },\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    await expect(attempt).rejects.toThrow(expectedErr)\n  }\n\n  it(\"prevents submitting an operation that removes the server's rotation key\", async () => {\n    await expectFailedOp(\n      alice,\n      { rotationKeys: [sampleKey] },\n      \"Rotation keys do not include server's rotation key\",\n    )\n  })\n\n  it('prevents submitting an operation that incorrectly sets the signing key', async () => {\n    await expectFailedOp(\n      alice,\n      {\n        verificationMethods: {\n          atproto: sampleKey,\n        },\n      },\n      'Incorrect signing key',\n    )\n  })\n\n  it('prevents submitting an operation that incorrectly sets the handle', async () => {\n    await expectFailedOp(\n      alice,\n      {\n        alsoKnownAs: ['at://new-alice.test'],\n      },\n      'Incorrect handle in alsoKnownAs',\n    )\n  })\n\n  it('prevents submitting an operation that incorrectly sets the pds endpoint', async () => {\n    await expectFailedOp(\n      alice,\n      {\n        services: {\n          atproto_pds: {\n            type: 'AtprotoPersonalDataServer',\n            endpoint: 'https://example.com',\n          },\n        },\n      },\n      'Incorrect endpoint on atproto_pds service',\n    )\n  })\n\n  it('prevents submitting an operation that incorrectly sets the pds service type', async () => {\n    await expectFailedOp(\n      alice,\n      {\n        services: {\n          atproto_pds: {\n            type: 'NotAPersonalDataServer',\n            endpoint: ctx.cfg.service.publicUrl,\n          },\n        },\n      },\n      'Incorrect type on atproto_pds service',\n    )\n  })\n\n  it('does not allow signing plc operation without a token', async () => {\n    const attempt = agent.com.atproto.identity.signPlcOperation(\n      {\n        rotationKeys: [sampleKey],\n      },\n      { encoding: 'application/json', headers: sc.getHeaders(alice) },\n    )\n    await expect(attempt).rejects.toThrow(\n      'email confirmation token required to sign PLC operations',\n    )\n  })\n\n  let token: string\n\n  it('requests a plc signature', async () => {\n    const mail = await getMailFrom(\n      agent.api.com.atproto.identity.requestPlcOperationSignature(undefined, {\n        headers: sc.getHeaders(alice),\n      }),\n    )\n\n    expect(mail.to).toEqual(sc.accounts[alice].email)\n    expect(mail.html).toContain('PLC update requested')\n\n    const gotToken = getTokenFromMail(mail)\n    assert(gotToken)\n    token = gotToken\n  })\n\n  it('does not sign a plc operation with a bad token', async () => {\n    const attempt = agent.api.com.atproto.identity.signPlcOperation(\n      {\n        token: '123456',\n        rotationKeys: [sampleKey],\n      },\n      { encoding: 'application/json', headers: sc.getHeaders(alice) },\n    )\n    await expect(attempt).rejects.toThrow('Token is invalid')\n  })\n\n  let operation: any\n\n  it('signs a plc operation with a valid token', async () => {\n    const res = await agent.api.com.atproto.identity.signPlcOperation(\n      {\n        token,\n        rotationKeys: [sampleKey, ctx.plcRotationKey.did()],\n      },\n      { encoding: 'application/json', headers: sc.getHeaders(alice) },\n    )\n    const currData = await ctx.plcClient.getDocumentData(alice)\n    expect(res.data.operation['alsoKnownAs']).toEqual(currData.alsoKnownAs)\n    expect(res.data.operation['verificationMethods']).toEqual(\n      currData.verificationMethods,\n    )\n    expect(res.data.operation['services']).toEqual(currData.services)\n    expect(res.data.operation['rotationKeys']).toEqual([\n      sampleKey,\n      ctx.plcRotationKey.did(),\n    ])\n    operation = res.data.operation\n  })\n\n  it('submits a valid operation', async () => {\n    await agent.com.atproto.identity.submitPlcOperation(\n      { operation },\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    const didData = await ctx.plcClient.getDocumentData(alice)\n    expect(didData.rotationKeys).toEqual([sampleKey, ctx.plcRotationKey.did()])\n  })\n\n  it('emits an identity event after a valid operation', async () => {\n    const lastEvt = await ctx.sequencer.db.db\n      .selectFrom('repo_seq')\n      .selectAll()\n      .orderBy('repo_seq.seq', 'desc')\n      .limit(1)\n      .executeTakeFirst()\n    assert(lastEvt)\n    expect(lastEvt.did).toBe(alice)\n    expect(lastEvt.eventType).toBe('identity')\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/preferences.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport usersSeed from './seeds/users'\n\ndescribe('user preferences', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n  let appPassHeaders: { authorization: string }\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'preferences',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    const appPass = await network.pds.ctx.accountManager.createAppPassword(\n      sc.dids.alice,\n      'test app pass',\n      false,\n    )\n    const res = await agent.com.atproto.server.createSession({\n      identifier: sc.dids.alice,\n      password: appPass.password,\n    })\n    appPassHeaders = { authorization: `Bearer ${res.data.accessJwt}` }\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('requires auth to set or put preferences.', async () => {\n    const tryPut = agent.api.app.bsky.actor.putPreferences({\n      preferences: [\n        { $type: 'app.bsky.actor.defs#adultContentPref', enabled: false },\n      ],\n    })\n    await expect(tryPut).rejects.toThrow('Authentication Required')\n    const tryGet = agent.api.app.bsky.actor.getPreferences()\n    await expect(tryGet).rejects.toThrow('Authentication Required')\n  })\n\n  it('gets preferences, before any are set.', async () => {\n    const { data } = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: sc.getHeaders(sc.dids.alice) },\n    )\n    expect(data).toEqual({\n      preferences: [],\n    })\n  })\n\n  it('only gets preferences in app.bsky namespace.', async () => {\n    await network.pds.ctx.actorStore.transact(sc.dids.alice, (store) =>\n      store.pref.putPreferences(\n        [{ $type: 'com.atproto.server.defs#unknown' }],\n        'com.atproto',\n        {\n          hasAccessFull: true,\n        },\n      ),\n    )\n    const { data } = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: sc.getHeaders(sc.dids.alice) },\n    )\n    expect(data).toEqual({ preferences: [] })\n  })\n\n  it('puts preferences, all creates.', async () => {\n    await agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [\n          { $type: 'app.bsky.actor.defs#adultContentPref', enabled: false },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'dogs',\n            visibility: 'show',\n          },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'cats',\n            visibility: 'warn',\n          },\n        ],\n      },\n      { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n    )\n    const { data } = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: sc.getHeaders(sc.dids.alice) },\n    )\n    expect(data).toEqual({\n      preferences: [\n        { $type: 'app.bsky.actor.defs#adultContentPref', enabled: false },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'dogs',\n          visibility: 'show',\n        },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'cats',\n          visibility: 'warn',\n        },\n      ],\n    })\n    // Ensure other prefs were not clobbered\n    const otherPrefs = await network.pds.ctx.actorStore.read(\n      sc.dids.alice,\n      (store) =>\n        store.pref.getPreferences('com.atproto', {\n          hasAccessFull: true,\n        }),\n    )\n    expect(otherPrefs).toEqual([{ $type: 'com.atproto.server.defs#unknown' }])\n  })\n\n  it('puts preferences, updates and removals.', async () => {\n    await agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [\n          { $type: 'app.bsky.actor.defs#adultContentPref', enabled: true },\n          {\n            $type: 'app.bsky.actor.defs#contentLabelPref',\n            label: 'dogs',\n            visibility: 'warn',\n          },\n        ],\n      },\n      { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n    )\n    const { data } = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: sc.getHeaders(sc.dids.alice) },\n    )\n    expect(data).toEqual({\n      preferences: [\n        { $type: 'app.bsky.actor.defs#adultContentPref', enabled: true },\n        {\n          $type: 'app.bsky.actor.defs#contentLabelPref',\n          label: 'dogs',\n          visibility: 'warn',\n        },\n      ],\n    })\n  })\n\n  it('puts preferences, clearing them.', async () => {\n    await agent.api.app.bsky.actor.putPreferences(\n      { preferences: [] },\n      { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n    )\n    const { data } = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: sc.getHeaders(sc.dids.alice) },\n    )\n    expect(data).toEqual({ preferences: [] })\n  })\n\n  it('fails putting preferences outside namespace.', async () => {\n    const tryPut = agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [\n          { $type: 'app.bsky.actor.defs#adultContentPref', enabled: false },\n          {\n            $type: 'com.atproto.server.defs#unknown',\n            // @ts-expect-error un-spec'ed prop\n            hello: 'world',\n          },\n        ],\n      },\n      { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n    )\n    await expect(tryPut).rejects.toThrow(\n      'Some preferences are not in the app.bsky namespace',\n    )\n  })\n\n  it('fails putting preferences without $type.', async () => {\n    const tryPut = agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [\n          { $type: 'app.bsky.actor.defs#adultContentPref', enabled: false },\n          // @ts-expect-error this is what we are testing !\n          {\n            label: 'dogs',\n            visibility: 'warn',\n          },\n        ],\n      },\n      { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n    )\n    await expect(tryPut).rejects.toThrow(\n      'Input/preferences/1 must be an object which includes the \"$type\" property',\n    )\n  })\n\n  it('does not read permissioned preferences with an app password', async () => {\n    await agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [\n          {\n            $type: 'app.bsky.actor.defs#personalDetailsPref',\n            birthDate: new Date().toISOString(),\n          },\n        ],\n      },\n      { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n    )\n    const res = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: appPassHeaders },\n    )\n    expect(res.data.preferences).toEqual([\n      {\n        $type: 'app.bsky.actor.defs#declaredAgePref',\n        isOverAge13: false,\n        isOverAge16: false,\n        isOverAge18: false,\n      },\n    ])\n  })\n\n  it('does not write permissioned preferences with an app password', async () => {\n    const tryPut = agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [\n          {\n            $type: 'app.bsky.actor.defs#personalDetailsPref',\n            birthDate: new Date().toISOString(),\n          },\n        ],\n      },\n      { headers: appPassHeaders, encoding: 'application/json' },\n    )\n    await expect(tryPut).rejects.toThrow(\n      /Do not have authorization to set preferences/,\n    )\n  })\n\n  it('does not remove permissioned preferences with an app password', async () => {\n    await agent.api.app.bsky.actor.putPreferences(\n      {\n        preferences: [],\n      },\n      { headers: appPassHeaders, encoding: 'application/json' },\n    )\n    const res = await agent.api.app.bsky.actor.getPreferences(\n      {},\n      { headers: sc.getHeaders(sc.dids.alice) },\n    )\n    const scopedPref = res.data.preferences.find(\n      (pref) => pref.$type === 'app.bsky.actor.defs#personalDetailsPref',\n    )\n    expect(scopedPref).toBeDefined()\n  })\n\n  describe('personalDetailsPref and declaredAgePref', () => {\n    const birthDate = new Date(1970, 0, 1).toISOString()\n\n    it('declaredAgePref is computed and returned for authed user', async () => {\n      await agent.api.app.bsky.actor.putPreferences(\n        {\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#personalDetailsPref',\n              birthDate,\n            },\n          ],\n        },\n        { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n      )\n      const res = await agent.api.app.bsky.actor.getPreferences(\n        {},\n        { headers: sc.getHeaders(sc.dids.alice) },\n      )\n      expect(res.data.preferences).toContainEqual({\n        $type: 'app.bsky.actor.defs#declaredAgePref',\n        isOverAge13: true,\n        isOverAge16: true,\n        isOverAge18: true,\n      })\n    })\n\n    it('declaredAgePref is computed and returned for app password', async () => {\n      await agent.api.app.bsky.actor.putPreferences(\n        {\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#personalDetailsPref',\n              birthDate,\n            },\n          ],\n        },\n        { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n      )\n      const res = await agent.api.app.bsky.actor.getPreferences(\n        {},\n        { headers: appPassHeaders },\n      )\n      expect(res.data.preferences).toContainEqual({\n        $type: 'app.bsky.actor.defs#declaredAgePref',\n        isOverAge13: true,\n        isOverAge16: true,\n        isOverAge18: true,\n      })\n    })\n\n    it('user cannot set declaredAgePref', async () => {\n      await agent.api.app.bsky.actor.putPreferences(\n        {\n          preferences: [\n            {\n              $type: 'app.bsky.actor.defs#personalDetailsPref',\n              birthDate,\n            },\n            {\n              $type: 'app.bsky.actor.defs#declaredAgePref',\n              isOverAge13: false,\n              isOverAge16: false,\n              isOverAge18: false,\n            },\n          ],\n        },\n        { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },\n      )\n      const res = await agent.api.app.bsky.actor.getPreferences(\n        {},\n        { headers: sc.getHeaders(sc.dids.alice) },\n      )\n      expect(res.data.preferences).toEqual([\n        {\n          $type: 'app.bsky.actor.defs#personalDetailsPref',\n          birthDate,\n        },\n        {\n          $type: 'app.bsky.actor.defs#declaredAgePref',\n          isOverAge13: true,\n          isOverAge16: true,\n          isOverAge18: true,\n        },\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`proxies admin requests creates reports of a repo. 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 1,\n    \"reasonType\": \"com.atproto.moderation.defs#reasonSpam\",\n    \"reportedBy\": \"user(0)\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(1)\",\n    },\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"id\": 3,\n    \"reason\": \"impersonation\",\n    \"reasonType\": \"com.atproto.moderation.defs#reasonOther\",\n    \"reportedBy\": \"user(2)\",\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(1)\",\n    },\n  },\n]\n`;\n\nexports[`proxies admin requests fetches a list of events. 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"testmod.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventAcknowledge\",\n    },\n    \"id\": 7,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(2)\",\n    \"creatorHandle\": \"mod-authority.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTag\",\n      \"add\": Array [\n        \"report:other\",\n      ],\n      \"remove\": Array [],\n    },\n    \"id\": 4,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(3)\",\n    \"creatorHandle\": \"carol.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"comment\": \"impersonation\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonOther\",\n    },\n    \"id\": 3,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(2)\",\n    \"creatorHandle\": \"mod-authority.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTag\",\n      \"add\": Array [\n        \"report:spam\",\n        \"lang:en\",\n        \"lang:i\",\n      ],\n      \"remove\": Array [],\n    },\n    \"id\": 2,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(4)\",\n    \"creatorHandle\": \"alice.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventReport\",\n      \"isReporterMuted\": false,\n      \"reportType\": \"com.atproto.moderation.defs#reasonSpam\",\n    },\n    \"id\": 1,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.admin.defs#repoRef\",\n      \"did\": \"user(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n]\n`;\n\nexports[`proxies admin requests fetches event details. 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(2)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#modEventTag\",\n    \"add\": Array [\n      \"report:spam\",\n      \"lang:en\",\n      \"lang:i\",\n    ],\n    \"remove\": Array [],\n  },\n  \"id\": 2,\n  \"subject\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#repoView\",\n    \"did\": \"user(0)\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"moderation\": Object {\n      \"subjectStatus\": Object {\n        \"accountStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n          \"appealCount\": 0,\n          \"escalateCount\": 0,\n          \"reportCount\": 2,\n          \"suspendCount\": 0,\n          \"takedownCount\": 0,\n        },\n        \"ageAssuranceState\": \"unknown\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"hosting\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n          \"status\": \"unknown\",\n        },\n        \"id\": 1,\n        \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedBy\": \"user(1)\",\n        \"priorityScore\": 0,\n        \"recordsStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n          \"appealedCount\": 0,\n          \"escalatedCount\": 0,\n          \"pendingCount\": 0,\n          \"processedCount\": 1,\n          \"reportedCount\": 0,\n          \"subjectCount\": 1,\n          \"takendownCount\": 0,\n          \"totalReports\": 0,\n        },\n        \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n        \"subject\": Object {\n          \"$type\": \"com.atproto.admin.defs#repoRef\",\n          \"did\": \"user(0)\",\n        },\n        \"subjectBlobCids\": Array [],\n        \"subjectRepoHandle\": \"bob.test\",\n        \"tags\": Array [\n          \"report:spam\",\n          \"lang:en\",\n          \"lang:i\",\n          \"report:other\",\n        ],\n        \"takendown\": false,\n        \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n      },\n    },\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(0)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"displayName\": \"bobby\",\n      },\n    ],\n  },\n  \"subjectBlobCids\": Array [],\n  \"subjectBlobs\": Array [],\n}\n`;\n\nexports[`proxies admin requests fetches moderation events. 1`] = `\nArray [\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(0)\",\n    \"creatorHandle\": \"mod-authority.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventTag\",\n      \"add\": Array [\n        \"lang:en\",\n      ],\n      \"remove\": Array [],\n    },\n    \"id\": 6,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n  Object {\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"user(1)\",\n    \"creatorHandle\": \"testmod.test\",\n    \"event\": Object {\n      \"$type\": \"tools.ozone.moderation.defs#modEventAcknowledge\",\n    },\n    \"id\": 5,\n    \"subject\": Object {\n      \"$type\": \"com.atproto.repo.strongRef\",\n      \"cid\": \"cids(0)\",\n      \"uri\": \"record(0)\",\n    },\n    \"subjectBlobCids\": Array [],\n    \"subjectHandle\": \"bob.test\",\n  },\n]\n`;\n\nexports[`proxies admin requests fetches record details. 1`] = `\nObject {\n  \"blobCids\": Array [],\n  \"blobs\": Array [],\n  \"cid\": \"cids(0)\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"moderation\": Object {\n    \"subjectStatus\": Object {\n      \"accountStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n        \"appealCount\": 0,\n        \"escalateCount\": 0,\n        \"reportCount\": 2,\n        \"suspendCount\": 0,\n        \"takedownCount\": 0,\n      },\n      \"ageAssuranceState\": \"unknown\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"hosting\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordHosting\",\n        \"status\": \"unknown\",\n      },\n      \"id\": 5,\n      \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"lastReviewedBy\": \"user(1)\",\n      \"priorityScore\": 0,\n      \"recordsStats\": Object {\n        \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n        \"appealedCount\": 0,\n        \"escalatedCount\": 0,\n        \"pendingCount\": 0,\n        \"processedCount\": 1,\n        \"reportedCount\": 0,\n        \"subjectCount\": 1,\n        \"takendownCount\": 0,\n        \"totalReports\": 0,\n      },\n      \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n      \"subject\": Object {\n        \"$type\": \"com.atproto.repo.strongRef\",\n        \"cid\": \"cids(0)\",\n        \"uri\": \"record(0)\",\n      },\n      \"subjectBlobCids\": Array [],\n      \"subjectRepoHandle\": \"bob.test\",\n      \"tags\": Array [\n        \"lang:en\",\n      ],\n      \"takendown\": false,\n      \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  },\n  \"repo\": Object {\n    \"did\": \"user(0)\",\n    \"email\": \"bob@test.com\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"invitedBy\": Object {\n      \"available\": 10,\n      \"code\": \"invite-code\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"createdBy\": \"admin\",\n      \"disabled\": false,\n      \"forAccount\": \"admin\",\n      \"uses\": Array [\n        Object {\n          \"usedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"usedBy\": \"user(1)\",\n        },\n        Object {\n          \"usedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"usedBy\": \"user(2)\",\n        },\n        Object {\n          \"usedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"usedBy\": \"user(3)\",\n        },\n        Object {\n          \"usedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"usedBy\": \"user(0)\",\n        },\n        Object {\n          \"usedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"usedBy\": \"user(4)\",\n        },\n      ],\n    },\n    \"invitesDisabled\": true,\n    \"moderation\": Object {\n      \"subjectStatus\": Object {\n        \"accountStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountStats\",\n          \"appealCount\": 0,\n          \"escalateCount\": 0,\n          \"reportCount\": 2,\n          \"suspendCount\": 0,\n          \"takedownCount\": 0,\n        },\n        \"ageAssuranceState\": \"unknown\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"hosting\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#accountHosting\",\n          \"status\": \"unknown\",\n        },\n        \"id\": 1,\n        \"lastReportedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"lastReviewedBy\": \"user(1)\",\n        \"priorityScore\": 0,\n        \"recordsStats\": Object {\n          \"$type\": \"tools.ozone.moderation.defs#recordsStats\",\n          \"appealedCount\": 0,\n          \"escalatedCount\": 0,\n          \"pendingCount\": 0,\n          \"processedCount\": 1,\n          \"reportedCount\": 0,\n          \"subjectCount\": 1,\n          \"takendownCount\": 0,\n          \"totalReports\": 0,\n        },\n        \"reviewState\": \"tools.ozone.moderation.defs#reviewClosed\",\n        \"subject\": Object {\n          \"$type\": \"com.atproto.admin.defs#repoRef\",\n          \"did\": \"user(0)\",\n        },\n        \"subjectBlobCids\": Array [],\n        \"subjectRepoHandle\": \"bob.test\",\n        \"tags\": Array [\n          \"report:spam\",\n          \"lang:en\",\n          \"lang:i\",\n          \"report:other\",\n        ],\n        \"takendown\": false,\n        \"updatedAt\": \"1970-01-01T00:00:00.000Z\",\n      },\n    },\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(1)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"displayName\": \"bobby\",\n      },\n    ],\n  },\n  \"uri\": \"record(0)\",\n  \"value\": Object {\n    \"$type\": \"app.bsky.feed.post\",\n    \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n    \"text\": \"bobby boy here\",\n  },\n}\n`;\n\nexports[`proxies admin requests fetches repo details. 1`] = `\nObject {\n  \"did\": \"user(0)\",\n  \"email\": \"eve@test.com\",\n  \"handle\": \"eve.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"invitedBy\": Object {\n    \"available\": 1,\n    \"code\": \"invite-code\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"createdBy\": \"admin\",\n    \"disabled\": false,\n    \"forAccount\": \"user(1)\",\n    \"uses\": Array [\n      Object {\n        \"usedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"usedBy\": \"user(0)\",\n      },\n    ],\n  },\n  \"invites\": Array [],\n  \"invitesDisabled\": false,\n  \"labels\": Array [],\n  \"moderation\": Object {},\n  \"relatedRecords\": Array [],\n}\n`;\n\nexports[`proxies admin requests searches repos. 1`] = `\nArray [\n  Object {\n    \"did\": \"user(0)\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"moderation\": Object {},\n    \"relatedRecords\": Array [\n      Object {\n        \"$type\": \"app.bsky.actor.profile\",\n        \"avatar\": Object {\n          \"$type\": \"blob\",\n          \"mimeType\": \"image/jpeg\",\n          \"ref\": Object {\n            \"$link\": \"cids(0)\",\n          },\n          \"size\": 3976,\n        },\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"displayName\": \"ali\",\n        \"labels\": Object {\n          \"$type\": \"com.atproto.label.defs#selfLabels\",\n          \"values\": Array [\n            Object {\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"val\": \"self-label-b\",\n            },\n          ],\n        },\n      },\n    ],\n  },\n]\n`;\n\nexports[`proxies admin requests takes actions and resolves reports 1`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(0)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#modEventAcknowledge\",\n  },\n  \"id\": 5,\n  \"subject\": Object {\n    \"$type\": \"com.atproto.repo.strongRef\",\n    \"cid\": \"cids(0)\",\n    \"uri\": \"record(0)\",\n  },\n  \"subjectBlobCids\": Array [],\n}\n`;\n\nexports[`proxies admin requests takes actions and resolves reports 2`] = `\nObject {\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"createdBy\": \"user(1)\",\n  \"event\": Object {\n    \"$type\": \"tools.ozone.moderation.defs#modEventAcknowledge\",\n  },\n  \"id\": 7,\n  \"subject\": Object {\n    \"$type\": \"com.atproto.admin.defs#repoRef\",\n    \"did\": \"user(0)\",\n  },\n  \"subjectBlobCids\": Array [],\n}\n`;\n"
  },
  {
    "path": "packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`feedgen proxy view performs basic proxy of getFeed 1`] = `\nObject {\n  \"feed\": Array [\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(0)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(0)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(0)\",\n            \"val\": \"self-label\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label\",\n              },\n            ],\n          },\n          \"text\": \"hey there\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(0)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(4)\",\n            \"following\": \"record(3)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images#view\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)\",\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"$type\": \"app.bsky.embed.record#viewRecord\",\n              \"author\": Object {\n                \"associated\": Object {\n                  \"activitySubscription\": Object {\n                    \"allowSubscriptions\": \"followers\",\n                  },\n                },\n                \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(5)/cids(1)\",\n                \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                \"did\": \"user(4)\",\n                \"displayName\": \"bobby\",\n                \"handle\": \"bob.test\",\n                \"labels\": Array [],\n                \"viewer\": Object {\n                  \"blockedBy\": false,\n                  \"followedBy\": \"record(7)\",\n                  \"following\": \"record(6)\",\n                  \"muted\": false,\n                },\n              },\n              \"cid\": \"cids(6)\",\n              \"embeds\": Array [],\n              \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n              \"labels\": Array [],\n              \"likeCount\": 0,\n              \"quoteCount\": 1,\n              \"replyCount\": 0,\n              \"repostCount\": 0,\n              \"uri\": \"record(5)\",\n              \"value\": Object {\n                \"$type\": \"app.bsky.feed.post\",\n                \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                \"langs\": Array [\n                  \"en-US\",\n                  \"i-klingon\",\n                ],\n                \"text\": \"bob back at it again!\",\n              },\n            },\n          },\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 2,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.recordWithMedia\",\n            \"media\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(4)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n                Object {\n                  \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(5)\",\n                    },\n                    \"size\": 12736,\n                  },\n                },\n              ],\n            },\n            \"record\": Object {\n              \"record\": Object {\n                \"cid\": \"cids(6)\",\n                \"uri\": \"record(5)\",\n              },\n            },\n          },\n          \"text\": \"hi im carol\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(8)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/pds/tests/proxied/__snapshots__/views.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`proxies view requests actor.getProfile 1`] = `\nObject {\n  \"associated\": Object {\n    \"activitySubscription\": Object {\n      \"allowSubscriptions\": \"followers\",\n    },\n    \"feedgens\": 0,\n    \"labeler\": false,\n    \"lists\": 0,\n    \"starterPacks\": 0,\n  },\n  \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n  \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n  \"description\": \"hi im bob label_me\",\n  \"did\": \"user(0)\",\n  \"displayName\": \"bobby\",\n  \"followersCount\": 2,\n  \"followsCount\": 2,\n  \"handle\": \"bob.test\",\n  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n  \"labels\": Array [],\n  \"postsCount\": 3,\n  \"viewer\": Object {\n    \"blockedBy\": false,\n    \"followedBy\": \"record(1)\",\n    \"following\": \"record(0)\",\n    \"knownFollowers\": Object {\n      \"count\": 1,\n      \"followers\": Array [\n        Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [\n            Object {\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"user(2)\",\n              \"val\": \"repo-action-label\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(2)\",\n            \"muted\": false,\n          },\n        },\n      ],\n    },\n    \"muted\": false,\n  },\n}\n`;\n\nexports[`proxies view requests actor.getProfiles 1`] = `\nObject {\n  \"profiles\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 1,\n        \"labeler\": false,\n        \"lists\": 1,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"ali\",\n      \"followersCount\": 2,\n      \"followsCount\": 3,\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(4)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(0)\",\n          \"uri\": \"record(4)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"postsCount\": 4,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"knownFollowers\": Object {\n          \"count\": 2,\n          \"followers\": Array [\n            Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(2)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(1)\",\n                \"following\": \"record(0)\",\n                \"muted\": false,\n              },\n            },\n            Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(4)\",\n              \"handle\": \"carol.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(3)\",\n                \"following\": \"record(2)\",\n                \"muted\": false,\n              },\n            },\n          ],\n        },\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n        \"feedgens\": 0,\n        \"labeler\": false,\n        \"lists\": 0,\n        \"starterPacks\": 0,\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(2)\",\n      \"displayName\": \"bobby\",\n      \"followersCount\": 2,\n      \"followsCount\": 2,\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"postsCount\": 3,\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"knownFollowers\": Object {\n          \"count\": 1,\n          \"followers\": Array [\n            Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(5)\",\n              \"handle\": \"dan.test\",\n              \"labels\": Array [\n                Object {\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"did:example:labeler\",\n                  \"uri\": \"user(5)\",\n                  \"val\": \"repo-action-label\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"following\": \"record(5)\",\n                \"muted\": false,\n              },\n            },\n          ],\n        },\n        \"muted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests actor.getSuggestions 1`] = `\nObject {\n  \"actors\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(0)\",\n        \"knownFollowers\": Object {\n          \"count\": 1,\n          \"followers\": Array [\n            Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(2)\",\n              \"displayName\": \"ali\",\n              \"handle\": \"alice.test\",\n              \"labels\": Array [\n                Object {\n                  \"cid\": \"cids(1)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(2)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"cid\": \"cids(1)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(2)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-b\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(2)\",\n                \"following\": \"record(1)\",\n                \"muted\": false,\n              },\n            },\n          ],\n        },\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(4)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [\n        Object {\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"user(4)\",\n          \"val\": \"repo-action-label\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"knownFollowers\": Object {\n          \"count\": 1,\n          \"followers\": Array [\n            Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(2)\",\n              \"displayName\": \"ali\",\n              \"handle\": \"alice.test\",\n              \"labels\": Array [\n                Object {\n                  \"cid\": \"cids(1)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(2)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-a\",\n                },\n                Object {\n                  \"cid\": \"cids(1)\",\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"user(2)\",\n                  \"uri\": \"record(3)\",\n                  \"val\": \"self-label-b\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(2)\",\n                \"following\": \"record(1)\",\n                \"muted\": false,\n              },\n            },\n          ],\n        },\n        \"muted\": false,\n      },\n    },\n  ],\n  \"cursor\": \"1:2:3\",\n}\n`;\n\nexports[`proxies view requests actor.searchActor 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"its me!\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(0)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(0)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(2)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(2)\",\n      \"following\": \"record(1)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(4)\",\n    \"handle\": \"carol.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(4)\",\n      \"following\": \"record(3)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(5)\",\n    \"handle\": \"dan.test\",\n    \"labels\": Array [\n      Object {\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"user(5)\",\n        \"val\": \"repo-action-label\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"following\": \"record(5)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"description\": \"the repo containing all the lexicons that can be resolved in dev\",\n    \"did\": \"user(6)\",\n    \"displayName\": \"Lexicon Authority\",\n    \"handle\": \"lex-authority.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"labeler\": true,\n    },\n    \"description\": \"The pretend version of mod.bsky.app\",\n    \"did\": \"user(7)\",\n    \"displayName\": \"Dev-env Moderation\",\n    \"handle\": \"mod-authority.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n]\n`;\n\nexports[`proxies view requests actor.searchActorTypeahead 1`] = `\nArray [\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(0)\",\n    \"displayName\": \"ali\",\n    \"handle\": \"alice.test\",\n    \"labels\": Array [\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(0)\",\n        \"val\": \"self-label-a\",\n      },\n      Object {\n        \"cid\": \"cids(1)\",\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"user(0)\",\n        \"uri\": \"record(0)\",\n        \"val\": \"self-label-b\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"did\": \"user(2)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(2)\",\n      \"following\": \"record(1)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(4)\",\n    \"handle\": \"carol.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(4)\",\n      \"following\": \"record(3)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(5)\",\n    \"handle\": \"dan.test\",\n    \"labels\": Array [\n      Object {\n        \"cts\": \"1970-01-01T00:00:00.000Z\",\n        \"src\": \"did:example:labeler\",\n        \"uri\": \"user(5)\",\n        \"val\": \"repo-action-label\",\n      },\n    ],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"following\": \"record(5)\",\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"did\": \"user(6)\",\n    \"displayName\": \"Lexicon Authority\",\n    \"handle\": \"lex-authority.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n  Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n      \"labeler\": true,\n    },\n    \"did\": \"user(7)\",\n    \"displayName\": \"Dev-env Moderation\",\n    \"handle\": \"mod-authority.test\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"muted\": false,\n    },\n  },\n]\n`;\n\nexports[`proxies view requests feed.getAuthorFeed 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"feed\": Array [\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(0)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(2)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(3)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(0)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reply\": Object {\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(2)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(2)\",\n                \"uri\": \"record(4)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(2)\",\n                \"uri\": \"record(4)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(2)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(2)\",\n                \"uri\": \"record(4)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(4)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(2)\",\n                \"uri\": \"record(4)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(5)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n          \"text\": \"bobby boy here\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(5)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(2)\",\n            \"following\": \"record(1)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(6)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"langs\": Array [\n            \"en-US\",\n            \"i-klingon\",\n          ],\n          \"text\": \"bob back at it again!\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(6)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests feed.getFeedGenerator 1`] = `\nObject {\n  \"isOnline\": true,\n  \"isValid\": true,\n  \"view\": Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(2)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n    \"did\": \"user(0)\",\n    \"displayName\": \"MyFeed\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"likeCount\": 0,\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {},\n  },\n}\n`;\n\nexports[`proxies view requests feed.getFeedGenerators 1`] = `\nObject {\n  \"feeds\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(1)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(1)\",\n            \"uri\": \"record(1)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"did\": \"user(0)\",\n      \"displayName\": \"MyFeed\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {},\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests feed.getLikes 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"likes\": Array [\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(0)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(1)\",\n          \"following\": \"record(0)\",\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n    Object {\n      \"actor\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(0)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(2)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(1)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(2)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(1)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(2)\",\n            \"uri\": \"record(2)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    },\n  ],\n  \"uri\": \"record(3)\",\n}\n`;\n\nexports[`proxies view requests feed.getListFeed 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"feed\": Array [\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(0)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(4)\",\n              \"uri\": \"record(3)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"thanks bob\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 1,\n        \"uri\": \"record(0)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reply\": Object {\n        \"grandparentAuthor\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(2)\",\n            \"displayName\": \"bobby\",\n            \"handle\": \"bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(5)\",\n              \"following\": \"record(4)\",\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(4)\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images#view\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n              },\n            ],\n          },\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(5)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n              ],\n            },\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(2)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(2)\",\n              },\n            },\n            \"text\": \"hear that label_me label_me_2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(5)\",\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(5)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reply\": Object {\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(6)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record#view\",\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(4)\",\n              \"handle\": \"dan.test\",\n              \"labels\": Array [\n                Object {\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"did:example:labeler\",\n                  \"uri\": \"user(4)\",\n                  \"val\": \"repo-action-label\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"following\": \"record(8)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(7)\",\n            \"embeds\": Array [\n              Object {\n                \"$type\": \"app.bsky.embed.record#view\",\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"did\": \"user(5)\",\n                    \"handle\": \"carol.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(11)\",\n                      \"following\": \"record(10)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(8)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [],\n                  \"likeCount\": 2,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(9)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"embed\": Object {\n                      \"$type\": \"app.bsky.embed.recordWithMedia\",\n                      \"media\": Object {\n                        \"$type\": \"app.bsky.embed.images\",\n                        \"images\": Array [\n                          Object {\n                            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                            \"image\": Object {\n                              \"$type\": \"blob\",\n                              \"mimeType\": \"image/jpeg\",\n                              \"ref\": Object {\n                                \"$link\": \"cids(5)\",\n                              },\n                              \"size\": 4114,\n                            },\n                          },\n                          Object {\n                            \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                            \"image\": Object {\n                              \"$type\": \"blob\",\n                              \"mimeType\": \"image/jpeg\",\n                              \"ref\": Object {\n                                \"$link\": \"cids(9)\",\n                              },\n                              \"size\": 12736,\n                            },\n                          },\n                        ],\n                      },\n                      \"record\": Object {\n                        \"record\": Object {\n                          \"cid\": \"cids(10)\",\n                          \"uri\": \"record(12)\",\n                        },\n                      },\n                    },\n                    \"text\": \"hi im carol\",\n                  },\n                },\n              },\n            ],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 1,\n            \"uri\": \"record(7)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"embed\": Object {\n                \"$type\": \"app.bsky.embed.record\",\n                \"record\": Object {\n                  \"cid\": \"cids(8)\",\n                  \"uri\": \"record(9)\",\n                },\n              },\n              \"facets\": Array [\n                Object {\n                  \"features\": Array [\n                    Object {\n                      \"$type\": \"app.bsky.richtext.facet#mention\",\n                      \"did\": \"user(0)\",\n                    },\n                  ],\n                  \"index\": Object {\n                    \"byteEnd\": 18,\n                    \"byteStart\": 0,\n                  },\n                },\n              ],\n              \"text\": \"@alice.bluesky.xyz is the best\",\n            },\n          },\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 2,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.record\",\n            \"record\": Object {\n              \"cid\": \"cids(7)\",\n              \"uri\": \"record(7)\",\n            },\n          },\n          \"text\": \"yoohoo label_me\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(6)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(5)\",\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(11)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n          \"text\": \"bobby boy here\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(13)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(3)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(2)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(5)\",\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(10)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"langs\": Array [\n            \"en-US\",\n            \"i-klingon\",\n          ],\n          \"text\": \"bob back at it again!\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(12)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(12)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(12)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(14)\",\n            \"val\": \"self-label\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label\",\n              },\n            ],\n          },\n          \"text\": \"hey there\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(14)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests feed.getPosts 1`] = `\nObject {\n  \"posts\": Array [\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(0)\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 0,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"langs\": Array [\n          \"en-US\",\n          \"i-klingon\",\n        ],\n        \"text\": \"bob back at it again!\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"threadMuted\": false,\n      },\n    },\n    Object {\n      \"author\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(5)\",\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"bookmarkCount\": 0,\n      \"cid\": \"cids(2)\",\n      \"embed\": Object {\n        \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n        \"media\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)\",\n            },\n            Object {\n              \"alt\": \"../dev-env/assets/key-alt.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)\",\n            },\n          ],\n        },\n        \"record\": Object {\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"did\": \"user(0)\",\n              \"displayName\": \"bobby\",\n              \"handle\": \"bob.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(2)\",\n                \"following\": \"record(1)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(0)\",\n            \"embeds\": Array [],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(0)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"langs\": Array [\n                \"en-US\",\n                \"i-klingon\",\n              ],\n              \"text\": \"bob back at it again!\",\n            },\n          },\n        },\n      },\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"likeCount\": 2,\n      \"quoteCount\": 1,\n      \"record\": Object {\n        \"$type\": \"app.bsky.feed.post\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(3)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(4)\",\n                  },\n                  \"size\": 12736,\n                },\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"cid\": \"cids(0)\",\n              \"uri\": \"record(0)\",\n            },\n          },\n        },\n        \"text\": \"hi im carol\",\n      },\n      \"replyCount\": 0,\n      \"repostCount\": 0,\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"bookmarked\": false,\n        \"embeddingDisabled\": false,\n        \"like\": \"record(6)\",\n        \"threadMuted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests feed.getRepostedBy 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"repostedBy\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n  ],\n  \"uri\": \"record(2)\",\n}\n`;\n\nexports[`proxies view requests feed.getTimeline 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"feed\": Array [\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(0)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(4)\",\n              \"uri\": \"record(3)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"thanks bob\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 1,\n        \"uri\": \"record(0)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reason\": Object {\n        \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n        \"by\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [\n            Object {\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"user(2)\",\n              \"val\": \"repo-action-label\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"cid\": \"cids(5)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"uri\": \"record(5)\",\n      },\n      \"reply\": Object {\n        \"grandparentAuthor\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(3)\",\n            \"displayName\": \"bobby\",\n            \"handle\": \"bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(7)\",\n              \"following\": \"record(6)\",\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(4)\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images#view\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n              },\n            ],\n          },\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(6)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n              ],\n            },\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(2)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(2)\",\n              },\n            },\n            \"text\": \"hear that label_me label_me_2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reason\": Object {\n        \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n        \"by\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [\n            Object {\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"user(2)\",\n              \"val\": \"repo-action-label\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"cid\": \"cids(7)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"uri\": \"record(8)\",\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [\n            Object {\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"user(2)\",\n              \"val\": \"repo-action-label\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record#view\",\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(5)\",\n              \"handle\": \"carol.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(12)\",\n                \"following\": \"record(11)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(9)\",\n            \"embeds\": Array [\n              Object {\n                \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n                \"media\": Object {\n                  \"$type\": \"app.bsky.embed.images#view\",\n                  \"images\": Array [\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                      \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                      \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n                    },\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                      \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n                      \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n                    },\n                  ],\n                },\n                \"record\": Object {\n                  \"record\": Object {\n                    \"$type\": \"app.bsky.embed.record#viewRecord\",\n                    \"author\": Object {\n                      \"associated\": Object {\n                        \"activitySubscription\": Object {\n                          \"allowSubscriptions\": \"followers\",\n                        },\n                      },\n                      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                      \"did\": \"user(3)\",\n                      \"displayName\": \"bobby\",\n                      \"handle\": \"bob.test\",\n                      \"labels\": Array [],\n                      \"viewer\": Object {\n                        \"blockedBy\": false,\n                        \"followedBy\": \"record(7)\",\n                        \"following\": \"record(6)\",\n                        \"muted\": false,\n                      },\n                    },\n                    \"cid\": \"cids(11)\",\n                    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"labels\": Array [],\n                    \"likeCount\": 0,\n                    \"quoteCount\": 1,\n                    \"replyCount\": 0,\n                    \"repostCount\": 0,\n                    \"uri\": \"record(13)\",\n                    \"value\": Object {\n                      \"$type\": \"app.bsky.feed.post\",\n                      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                      \"langs\": Array [\n                        \"en-US\",\n                        \"i-klingon\",\n                      ],\n                      \"text\": \"bob back at it again!\",\n                    },\n                  },\n                },\n              },\n            ],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 2,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(10)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"embed\": Object {\n                \"$type\": \"app.bsky.embed.recordWithMedia\",\n                \"media\": Object {\n                  \"$type\": \"app.bsky.embed.images\",\n                  \"images\": Array [\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                      \"image\": Object {\n                        \"$type\": \"blob\",\n                        \"mimeType\": \"image/jpeg\",\n                        \"ref\": Object {\n                          \"$link\": \"cids(6)\",\n                        },\n                        \"size\": 4114,\n                      },\n                    },\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                      \"image\": Object {\n                        \"$type\": \"blob\",\n                        \"mimeType\": \"image/jpeg\",\n                        \"ref\": Object {\n                          \"$link\": \"cids(10)\",\n                        },\n                        \"size\": 12736,\n                      },\n                    },\n                  ],\n                },\n                \"record\": Object {\n                  \"record\": Object {\n                    \"cid\": \"cids(11)\",\n                    \"uri\": \"record(13)\",\n                  },\n                },\n              },\n              \"text\": \"hi im carol\",\n            },\n          },\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.record\",\n            \"record\": Object {\n              \"cid\": \"cids(9)\",\n              \"uri\": \"record(10)\",\n            },\n          },\n          \"facets\": Array [\n            Object {\n              \"features\": Array [\n                Object {\n                  \"$type\": \"app.bsky.richtext.facet#mention\",\n                  \"did\": \"user(0)\",\n                },\n              ],\n              \"index\": Object {\n                \"byteEnd\": 18,\n                \"byteStart\": 0,\n              },\n            },\n          ],\n          \"text\": \"@alice.bluesky.xyz is the best\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 1,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reason\": Object {\n        \"$type\": \"app.bsky.feed.defs#reasonRepost\",\n        \"by\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(5)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(12)\",\n            \"following\": \"record(11)\",\n            \"muted\": false,\n          },\n        },\n        \"cid\": \"cids(12)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"uri\": \"record(14)\",\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(0)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(4)\",\n              \"uri\": \"record(3)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"thanks bob\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 1,\n        \"uri\": \"record(0)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reply\": Object {\n        \"grandparentAuthor\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(3)\",\n            \"displayName\": \"bobby\",\n            \"handle\": \"bob.test\",\n            \"labels\": Array [],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"followedBy\": \"record(7)\",\n              \"following\": \"record(6)\",\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(4)\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images#view\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n              },\n            ],\n          },\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 0,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"embed\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(6)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n              ],\n            },\n            \"reply\": Object {\n              \"parent\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(2)\",\n              },\n              \"root\": Object {\n                \"cid\": \"cids(3)\",\n                \"uri\": \"record(2)\",\n              },\n            },\n            \"text\": \"hear that label_me label_me_2\",\n          },\n          \"replyCount\": 1,\n          \"repostCount\": 0,\n          \"uri\": \"record(3)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(5)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(12)\",\n            \"following\": \"record(11)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(13)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"of course\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(15)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reply\": Object {\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(4)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.images#view\",\n          \"images\": Array [\n            Object {\n              \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n              \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)\",\n              \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)\",\n            },\n          ],\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.images\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"image\": Object {\n                  \"$type\": \"blob\",\n                  \"mimeType\": \"image/jpeg\",\n                  \"ref\": Object {\n                    \"$link\": \"cids(6)\",\n                  },\n                  \"size\": 4114,\n                },\n              },\n            ],\n          },\n          \"reply\": Object {\n            \"parent\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n            \"root\": Object {\n              \"cid\": \"cids(3)\",\n              \"uri\": \"record(2)\",\n            },\n          },\n          \"text\": \"hear that label_me label_me_2\",\n        },\n        \"replyCount\": 1,\n        \"repostCount\": 0,\n        \"uri\": \"record(3)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n      \"reply\": Object {\n        \"parent\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n        \"root\": Object {\n          \"$type\": \"app.bsky.feed.defs#postView\",\n          \"author\": Object {\n            \"associated\": Object {\n              \"activitySubscription\": Object {\n                \"allowSubscriptions\": \"followers\",\n              },\n            },\n            \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n            \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n            \"did\": \"user(0)\",\n            \"displayName\": \"ali\",\n            \"handle\": \"alice.test\",\n            \"labels\": Array [\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-a\",\n              },\n              Object {\n                \"cid\": \"cids(2)\",\n                \"cts\": \"1970-01-01T00:00:00.000Z\",\n                \"src\": \"user(0)\",\n                \"uri\": \"record(1)\",\n                \"val\": \"self-label-b\",\n              },\n            ],\n            \"viewer\": Object {\n              \"blockedBy\": false,\n              \"muted\": false,\n            },\n          },\n          \"bookmarkCount\": 0,\n          \"cid\": \"cids(3)\",\n          \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Array [],\n          \"likeCount\": 3,\n          \"quoteCount\": 0,\n          \"record\": Object {\n            \"$type\": \"app.bsky.feed.post\",\n            \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n            \"text\": \"again\",\n          },\n          \"replyCount\": 2,\n          \"repostCount\": 1,\n          \"uri\": \"record(2)\",\n          \"viewer\": Object {\n            \"bookmarked\": false,\n            \"embeddingDisabled\": false,\n            \"threadMuted\": false,\n          },\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(14)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record#view\",\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(2)\",\n              \"handle\": \"dan.test\",\n              \"labels\": Array [\n                Object {\n                  \"cts\": \"1970-01-01T00:00:00.000Z\",\n                  \"src\": \"did:example:labeler\",\n                  \"uri\": \"user(2)\",\n                  \"val\": \"repo-action-label\",\n                },\n              ],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"following\": \"record(4)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(8)\",\n            \"embeds\": Array [\n              Object {\n                \"$type\": \"app.bsky.embed.record#view\",\n                \"record\": Object {\n                  \"$type\": \"app.bsky.embed.record#viewRecord\",\n                  \"author\": Object {\n                    \"associated\": Object {\n                      \"activitySubscription\": Object {\n                        \"allowSubscriptions\": \"followers\",\n                      },\n                    },\n                    \"did\": \"user(5)\",\n                    \"handle\": \"carol.test\",\n                    \"labels\": Array [],\n                    \"viewer\": Object {\n                      \"blockedBy\": false,\n                      \"followedBy\": \"record(12)\",\n                      \"following\": \"record(11)\",\n                      \"muted\": false,\n                    },\n                  },\n                  \"cid\": \"cids(9)\",\n                  \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                  \"labels\": Array [],\n                  \"likeCount\": 2,\n                  \"quoteCount\": 1,\n                  \"replyCount\": 0,\n                  \"repostCount\": 0,\n                  \"uri\": \"record(10)\",\n                  \"value\": Object {\n                    \"$type\": \"app.bsky.feed.post\",\n                    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"embed\": Object {\n                      \"$type\": \"app.bsky.embed.recordWithMedia\",\n                      \"media\": Object {\n                        \"$type\": \"app.bsky.embed.images\",\n                        \"images\": Array [\n                          Object {\n                            \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                            \"image\": Object {\n                              \"$type\": \"blob\",\n                              \"mimeType\": \"image/jpeg\",\n                              \"ref\": Object {\n                                \"$link\": \"cids(6)\",\n                              },\n                              \"size\": 4114,\n                            },\n                          },\n                          Object {\n                            \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                            \"image\": Object {\n                              \"$type\": \"blob\",\n                              \"mimeType\": \"image/jpeg\",\n                              \"ref\": Object {\n                                \"$link\": \"cids(10)\",\n                              },\n                              \"size\": 12736,\n                            },\n                          },\n                        ],\n                      },\n                      \"record\": Object {\n                        \"record\": Object {\n                          \"cid\": \"cids(11)\",\n                          \"uri\": \"record(13)\",\n                        },\n                      },\n                    },\n                    \"text\": \"hi im carol\",\n                  },\n                },\n              },\n            ],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 0,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 1,\n            \"uri\": \"record(9)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"embed\": Object {\n                \"$type\": \"app.bsky.embed.record\",\n                \"record\": Object {\n                  \"cid\": \"cids(9)\",\n                  \"uri\": \"record(10)\",\n                },\n              },\n              \"facets\": Array [\n                Object {\n                  \"features\": Array [\n                    Object {\n                      \"$type\": \"app.bsky.richtext.facet#mention\",\n                      \"did\": \"user(0)\",\n                    },\n                  ],\n                  \"index\": Object {\n                    \"byteEnd\": 18,\n                    \"byteStart\": 0,\n                  },\n                },\n              ],\n              \"text\": \"@alice.bluesky.xyz is the best\",\n            },\n          },\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 2,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.record\",\n            \"record\": Object {\n              \"cid\": \"cids(8)\",\n              \"uri\": \"record(9)\",\n            },\n          },\n          \"text\": \"yoohoo label_me\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(16)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(15)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000+00:00\",\n          \"text\": \"bobby boy here\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(17)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(3)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 3,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000000Z\",\n          \"text\": \"again\",\n        },\n        \"replyCount\": 2,\n        \"repostCount\": 1,\n        \"uri\": \"record(2)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [\n            Object {\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"user(2)\",\n              \"val\": \"repo-action-label\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(8)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.record#view\",\n          \"record\": Object {\n            \"$type\": \"app.bsky.embed.record#viewRecord\",\n            \"author\": Object {\n              \"associated\": Object {\n                \"activitySubscription\": Object {\n                  \"allowSubscriptions\": \"followers\",\n                },\n              },\n              \"did\": \"user(5)\",\n              \"handle\": \"carol.test\",\n              \"labels\": Array [],\n              \"viewer\": Object {\n                \"blockedBy\": false,\n                \"followedBy\": \"record(12)\",\n                \"following\": \"record(11)\",\n                \"muted\": false,\n              },\n            },\n            \"cid\": \"cids(9)\",\n            \"embeds\": Array [\n              Object {\n                \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n                \"media\": Object {\n                  \"$type\": \"app.bsky.embed.images#view\",\n                  \"images\": Array [\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                      \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                      \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n                    },\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                      \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n                      \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n                    },\n                  ],\n                },\n                \"record\": Object {\n                  \"record\": Object {\n                    \"$type\": \"app.bsky.embed.record#viewRecord\",\n                    \"author\": Object {\n                      \"associated\": Object {\n                        \"activitySubscription\": Object {\n                          \"allowSubscriptions\": \"followers\",\n                        },\n                      },\n                      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                      \"did\": \"user(3)\",\n                      \"displayName\": \"bobby\",\n                      \"handle\": \"bob.test\",\n                      \"labels\": Array [],\n                      \"viewer\": Object {\n                        \"blockedBy\": false,\n                        \"followedBy\": \"record(7)\",\n                        \"following\": \"record(6)\",\n                        \"muted\": false,\n                      },\n                    },\n                    \"cid\": \"cids(11)\",\n                    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n                    \"labels\": Array [],\n                    \"likeCount\": 0,\n                    \"quoteCount\": 1,\n                    \"replyCount\": 0,\n                    \"repostCount\": 0,\n                    \"uri\": \"record(13)\",\n                    \"value\": Object {\n                      \"$type\": \"app.bsky.feed.post\",\n                      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                      \"langs\": Array [\n                        \"en-US\",\n                        \"i-klingon\",\n                      ],\n                      \"text\": \"bob back at it again!\",\n                    },\n                  },\n                },\n              },\n            ],\n            \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n            \"labels\": Array [],\n            \"likeCount\": 2,\n            \"quoteCount\": 1,\n            \"replyCount\": 0,\n            \"repostCount\": 0,\n            \"uri\": \"record(10)\",\n            \"value\": Object {\n              \"$type\": \"app.bsky.feed.post\",\n              \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n              \"embed\": Object {\n                \"$type\": \"app.bsky.embed.recordWithMedia\",\n                \"media\": Object {\n                  \"$type\": \"app.bsky.embed.images\",\n                  \"images\": Array [\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                      \"image\": Object {\n                        \"$type\": \"blob\",\n                        \"mimeType\": \"image/jpeg\",\n                        \"ref\": Object {\n                          \"$link\": \"cids(6)\",\n                        },\n                        \"size\": 4114,\n                      },\n                    },\n                    Object {\n                      \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                      \"image\": Object {\n                        \"$type\": \"blob\",\n                        \"mimeType\": \"image/jpeg\",\n                        \"ref\": Object {\n                          \"$link\": \"cids(10)\",\n                        },\n                        \"size\": 12736,\n                      },\n                    },\n                  ],\n                },\n                \"record\": Object {\n                  \"record\": Object {\n                    \"cid\": \"cids(11)\",\n                    \"uri\": \"record(13)\",\n                  },\n                },\n              },\n              \"text\": \"hi im carol\",\n            },\n          },\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.record\",\n            \"record\": Object {\n              \"cid\": \"cids(9)\",\n              \"uri\": \"record(10)\",\n            },\n          },\n          \"facets\": Array [\n            Object {\n              \"features\": Array [\n                Object {\n                  \"$type\": \"app.bsky.richtext.facet#mention\",\n                  \"did\": \"user(0)\",\n                },\n              ],\n              \"index\": Object {\n                \"byteEnd\": 18,\n                \"byteStart\": 0,\n              },\n            },\n          ],\n          \"text\": \"@alice.bluesky.xyz is the best\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 1,\n        \"uri\": \"record(9)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(2)\",\n          \"handle\": \"dan.test\",\n          \"labels\": Array [\n            Object {\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"did:example:labeler\",\n              \"uri\": \"user(2)\",\n              \"val\": \"repo-action-label\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"following\": \"record(4)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(16)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"text\": \"dan here!\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(18)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"did\": \"user(5)\",\n          \"handle\": \"carol.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(12)\",\n            \"following\": \"record(11)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(9)\",\n        \"embed\": Object {\n          \"$type\": \"app.bsky.embed.recordWithMedia#view\",\n          \"media\": Object {\n            \"$type\": \"app.bsky.embed.images#view\",\n            \"images\": Array [\n              Object {\n                \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)\",\n              },\n              Object {\n                \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                \"fullsize\": \"https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)\",\n                \"thumb\": \"https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)\",\n              },\n            ],\n          },\n          \"record\": Object {\n            \"record\": Object {\n              \"$type\": \"app.bsky.embed.record#viewRecord\",\n              \"author\": Object {\n                \"associated\": Object {\n                  \"activitySubscription\": Object {\n                    \"allowSubscriptions\": \"followers\",\n                  },\n                },\n                \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n                \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                \"did\": \"user(3)\",\n                \"displayName\": \"bobby\",\n                \"handle\": \"bob.test\",\n                \"labels\": Array [],\n                \"viewer\": Object {\n                  \"blockedBy\": false,\n                  \"followedBy\": \"record(7)\",\n                  \"following\": \"record(6)\",\n                  \"muted\": false,\n                },\n              },\n              \"cid\": \"cids(11)\",\n              \"embeds\": Array [],\n              \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n              \"labels\": Array [],\n              \"likeCount\": 0,\n              \"quoteCount\": 1,\n              \"replyCount\": 0,\n              \"repostCount\": 0,\n              \"uri\": \"record(13)\",\n              \"value\": Object {\n                \"$type\": \"app.bsky.feed.post\",\n                \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n                \"langs\": Array [\n                  \"en-US\",\n                  \"i-klingon\",\n                ],\n                \"text\": \"bob back at it again!\",\n              },\n            },\n          },\n        },\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 2,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"embed\": Object {\n            \"$type\": \"app.bsky.embed.recordWithMedia\",\n            \"media\": Object {\n              \"$type\": \"app.bsky.embed.images\",\n              \"images\": Array [\n                Object {\n                  \"alt\": \"../dev-env/assets/key-landscape-small.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(6)\",\n                    },\n                    \"size\": 4114,\n                  },\n                },\n                Object {\n                  \"alt\": \"../dev-env/assets/key-alt.jpg\",\n                  \"image\": Object {\n                    \"$type\": \"blob\",\n                    \"mimeType\": \"image/jpeg\",\n                    \"ref\": Object {\n                      \"$link\": \"cids(10)\",\n                    },\n                    \"size\": 12736,\n                  },\n                },\n              ],\n            },\n            \"record\": Object {\n              \"record\": Object {\n                \"cid\": \"cids(11)\",\n                \"uri\": \"record(13)\",\n              },\n            },\n          },\n          \"text\": \"hi im carol\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(10)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"like\": \"record(19)\",\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(3)\",\n          \"displayName\": \"bobby\",\n          \"handle\": \"bob.test\",\n          \"labels\": Array [],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"followedBy\": \"record(7)\",\n            \"following\": \"record(6)\",\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(11)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"likeCount\": 0,\n        \"quoteCount\": 1,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"langs\": Array [\n            \"en-US\",\n            \"i-klingon\",\n          ],\n          \"text\": \"bob back at it again!\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(13)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n    Object {\n      \"post\": Object {\n        \"author\": Object {\n          \"associated\": Object {\n            \"activitySubscription\": Object {\n              \"allowSubscriptions\": \"followers\",\n            },\n          },\n          \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"did\": \"user(0)\",\n          \"displayName\": \"ali\",\n          \"handle\": \"alice.test\",\n          \"labels\": Array [\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-a\",\n            },\n            Object {\n              \"cid\": \"cids(2)\",\n              \"cts\": \"1970-01-01T00:00:00.000Z\",\n              \"src\": \"user(0)\",\n              \"uri\": \"record(1)\",\n              \"val\": \"self-label-b\",\n            },\n          ],\n          \"viewer\": Object {\n            \"blockedBy\": false,\n            \"muted\": false,\n          },\n        },\n        \"bookmarkCount\": 0,\n        \"cid\": \"cids(17)\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(17)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(0)\",\n            \"uri\": \"record(20)\",\n            \"val\": \"self-label\",\n          },\n        ],\n        \"likeCount\": 0,\n        \"quoteCount\": 0,\n        \"record\": Object {\n          \"$type\": \"app.bsky.feed.post\",\n          \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n          \"labels\": Object {\n            \"$type\": \"com.atproto.label.defs#selfLabels\",\n            \"values\": Array [\n              Object {\n                \"val\": \"self-label\",\n              },\n            ],\n          },\n          \"text\": \"hey there\",\n        },\n        \"replyCount\": 0,\n        \"repostCount\": 0,\n        \"uri\": \"record(20)\",\n        \"viewer\": Object {\n          \"bookmarked\": false,\n          \"embeddingDisabled\": false,\n          \"threadMuted\": false,\n        },\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests graph.getBlocks 1`] = `\nObject {\n  \"blocks\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"blocking\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"blocking\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n  ],\n  \"cursor\": \"0000000000000::bafycid\",\n}\n`;\n\nexports[`proxies view requests graph.getFollowers 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"followers\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"dan.test\",\n      \"labels\": Array [\n        Object {\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"did:example:labeler\",\n          \"uri\": \"user(0)\",\n          \"val\": \"repo-action-label\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(1)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(3)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(3)\",\n      \"following\": \"record(2)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`proxies view requests graph.getFollows 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"follows\": Array [\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"did\": \"user(0)\",\n      \"handle\": \"carol.test\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(1)\",\n        \"following\": \"record(0)\",\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(2)/cids(0)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"its me!\",\n      \"did\": \"user(1)\",\n      \"displayName\": \"ali\",\n      \"handle\": \"alice.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-a\",\n        },\n        Object {\n          \"cid\": \"cids(1)\",\n          \"cts\": \"1970-01-01T00:00:00.000Z\",\n          \"src\": \"user(1)\",\n          \"uri\": \"record(2)\",\n          \"val\": \"self-label-b\",\n        },\n      ],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"muted\": false,\n      },\n    },\n  ],\n  \"subject\": Object {\n    \"associated\": Object {\n      \"activitySubscription\": Object {\n        \"allowSubscriptions\": \"followers\",\n      },\n    },\n    \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(0)\",\n    \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n    \"description\": \"hi im bob label_me\",\n    \"did\": \"user(3)\",\n    \"displayName\": \"bobby\",\n    \"handle\": \"bob.test\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"viewer\": Object {\n      \"blockedBy\": false,\n      \"followedBy\": \"record(4)\",\n      \"following\": \"record(3)\",\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`proxies view requests graph.getList 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"items\": Array [\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"did\": \"user(2)\",\n        \"handle\": \"carol.test\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(5)\",\n          \"following\": \"record(4)\",\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(3)\",\n    },\n    Object {\n      \"subject\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(4)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"its me!\",\n        \"did\": \"user(3)\",\n        \"displayName\": \"ali\",\n        \"handle\": \"alice.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(3)\",\n            \"uri\": \"record(7)\",\n            \"val\": \"self-label-a\",\n          },\n          Object {\n            \"cid\": \"cids(2)\",\n            \"cts\": \"1970-01-01T00:00:00.000Z\",\n            \"src\": \"user(3)\",\n            \"uri\": \"record(7)\",\n            \"val\": \"self-label-b\",\n          },\n        ],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"uri\": \"record(6)\",\n    },\n  ],\n  \"list\": Object {\n    \"cid\": \"cids(0)\",\n    \"creator\": Object {\n      \"associated\": Object {\n        \"activitySubscription\": Object {\n          \"allowSubscriptions\": \"followers\",\n        },\n      },\n      \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n      \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n      \"description\": \"hi im bob label_me\",\n      \"did\": \"user(0)\",\n      \"displayName\": \"bobby\",\n      \"handle\": \"bob.test\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"viewer\": Object {\n        \"blockedBy\": false,\n        \"followedBy\": \"record(2)\",\n        \"following\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n    \"description\": \"bob's list of mutes\",\n    \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n    \"labels\": Array [],\n    \"listItemCount\": 2,\n    \"name\": \"bob mutes\",\n    \"purpose\": \"app.bsky.graph.defs#modlist\",\n    \"uri\": \"record(0)\",\n    \"viewer\": Object {\n      \"muted\": false,\n    },\n  },\n}\n`;\n\nexports[`proxies view requests graph.getListBlocks 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"muted\": false,\n        },\n      },\n      \"description\": \"bob's list of mutes\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 2,\n      \"name\": \"bob mutes\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"blocked\": \"record(1)\",\n        \"muted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests graph.getLists 1`] = `\nObject {\n  \"cursor\": \"0000000000000::bafycid\",\n  \"lists\": Array [\n    Object {\n      \"cid\": \"cids(0)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"a second list\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 0,\n      \"name\": \"another list\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(0)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n    Object {\n      \"cid\": \"cids(2)\",\n      \"creator\": Object {\n        \"associated\": Object {\n          \"activitySubscription\": Object {\n            \"allowSubscriptions\": \"followers\",\n          },\n        },\n        \"avatar\": \"https://bsky.public.url/img/avatar/plain/user(1)/cids(1)\",\n        \"createdAt\": \"1970-01-01T00:00:00.000Z\",\n        \"description\": \"hi im bob label_me\",\n        \"did\": \"user(0)\",\n        \"displayName\": \"bobby\",\n        \"handle\": \"bob.test\",\n        \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n        \"labels\": Array [],\n        \"viewer\": Object {\n          \"blockedBy\": false,\n          \"followedBy\": \"record(2)\",\n          \"following\": \"record(1)\",\n          \"muted\": false,\n        },\n      },\n      \"description\": \"bob's list of mutes\",\n      \"indexedAt\": \"1970-01-01T00:00:00.000Z\",\n      \"labels\": Array [],\n      \"listItemCount\": 2,\n      \"name\": \"bob mutes\",\n      \"purpose\": \"app.bsky.graph.defs#modlist\",\n      \"uri\": \"record(3)\",\n      \"viewer\": Object {\n        \"muted\": false,\n      },\n    },\n  ],\n}\n`;\n\nexports[`proxies view requests unspecced.getPopularFeedGenerators 1`] = `\nObject {\n  \"feeds\": Array [],\n}\n`;\n"
  },
  {
    "path": "packages/pds/tests/proxied/admin.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { forSnapshot } from '../_util'\nimport basicSeed from '../seeds/basic'\n\ndescribe('proxies admin requests', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let moderator: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'proxy_admin',\n      pds: {\n        inviteRequired: true,\n      },\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    const { data: invite } =\n      await agent.api.com.atproto.server.createInviteCode(\n        { useCount: 10 },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n    await basicSeed(sc, {\n      inviteCode: invite.code,\n      addModLabels: network.bsky,\n    })\n    const modAccount = await sc.createAccount('moderator', {\n      handle: 'testmod.test',\n      email: 'testmod@test.com',\n      password: 'testmod-pass',\n      inviteCode: invite.code,\n    })\n    moderator = modAccount.did\n    await network.ozone.addModeratorDid(moderator)\n\n    await network.processAll()\n  })\n\n  beforeAll(async () => {\n    const { data: invite } =\n      await agent.api.com.atproto.server.createInviteCode(\n        { useCount: 1, forAccount: sc.dids.alice },\n        {\n          headers: network.pds.adminAuthHeaders(),\n          encoding: 'application/json',\n        },\n      )\n    await agent.api.com.atproto.admin.disableAccountInvites(\n      { account: sc.dids.bob },\n      { headers: network.pds.adminAuthHeaders(), encoding: 'application/json' },\n    )\n    await sc.createAccount('eve', {\n      handle: 'eve.test',\n      email: 'eve@test.com',\n      password: 'password',\n      inviteCode: invite.code,\n    })\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('creates reports of a repo.', async () => {\n    const { data: reportA } =\n      await agent.api.com.atproto.moderation.createReport(\n        {\n          reasonType: 'com.atproto.moderation.defs#reasonSpam',\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n        },\n        {\n          headers: sc.getHeaders(sc.dids.alice),\n          encoding: 'application/json',\n        },\n      )\n    const { data: reportB } =\n      await agent.api.com.atproto.moderation.createReport(\n        {\n          reasonType: 'com.atproto.moderation.defs#reasonOther',\n          reason: 'impersonation',\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: sc.dids.bob,\n          },\n        },\n        {\n          headers: sc.getHeaders(sc.dids.carol),\n          encoding: 'application/json',\n        },\n      )\n    expect(forSnapshot([reportA, reportB])).toMatchSnapshot()\n  })\n\n  it('takes actions and resolves reports', async () => {\n    const post = sc.posts[sc.dids.bob][1]\n    const { data: actionA } = await agent.api.tools.ozone.moderation.emitEvent(\n      {\n        event: { $type: 'tools.ozone.moderation.defs#modEventAcknowledge' },\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n        createdBy: 'did:example:admin',\n        // @ts-expect-error\n        reason: 'Y',\n      },\n      {\n        headers: sc.getHeaders(moderator),\n        encoding: 'application/json',\n      },\n    )\n    expect(forSnapshot(actionA)).toMatchSnapshot()\n    const { data: actionB } = await agent.api.tools.ozone.moderation.emitEvent(\n      {\n        event: { $type: 'tools.ozone.moderation.defs#modEventAcknowledge' },\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.bob,\n        },\n        createdBy: 'did:example:admin',\n        // @ts-expect-error\n        reason: 'Y',\n      },\n      {\n        headers: sc.getHeaders(moderator),\n        encoding: 'application/json',\n      },\n    )\n    expect(forSnapshot(actionB)).toMatchSnapshot()\n  })\n\n  it('fetches moderation events.', async () => {\n    const { data: result } = await agent.api.tools.ozone.moderation.queryEvents(\n      {\n        subject: sc.posts[sc.dids.bob][1].ref.uriStr,\n      },\n      { headers: sc.getHeaders(moderator) },\n    )\n    expect(forSnapshot(result.events)).toMatchSnapshot()\n  })\n\n  it('fetches repo details.', async () => {\n    const { data: result } = await agent.api.tools.ozone.moderation.getRepo(\n      { did: sc.dids.eve },\n      { headers: sc.getHeaders(moderator) },\n    )\n    expect(forSnapshot(result)).toMatchSnapshot()\n  })\n\n  it('fetches record details.', async () => {\n    const post = sc.posts[sc.dids.bob][1]\n    const { data: result } = await agent.api.tools.ozone.moderation.getRecord(\n      { uri: post.ref.uriStr },\n      { headers: sc.getHeaders(moderator) },\n    )\n    expect(forSnapshot(result)).toMatchSnapshot()\n  })\n\n  it('fetches event details.', async () => {\n    const { data: result } = await agent.api.tools.ozone.moderation.getEvent(\n      { id: 2 },\n      { headers: sc.getHeaders(moderator) },\n    )\n    expect(forSnapshot(result)).toMatchSnapshot()\n  })\n\n  it('fetches a list of events.', async () => {\n    const { data: result } = await agent.api.tools.ozone.moderation.queryEvents(\n      { subject: sc.dids.bob },\n      { headers: sc.getHeaders(moderator) },\n    )\n    expect(forSnapshot(result.events)).toMatchSnapshot()\n  })\n\n  it('searches repos.', async () => {\n    const { data: result } = await agent.api.tools.ozone.moderation.searchRepos(\n      { term: 'alice' },\n      { headers: sc.getHeaders(moderator) },\n    )\n    expect(forSnapshot(result.repos)).toMatchSnapshot()\n  })\n\n  it('passes through errors.', async () => {\n    const tryGetRepo = agent.api.tools.ozone.moderation.getRepo(\n      { did: 'did:does:not:exist' },\n      { headers: sc.getHeaders(moderator) },\n    )\n    await expect(tryGetRepo).rejects.toThrow('Repo not found')\n    const tryGetRecord = agent.api.tools.ozone.moderation.getRecord(\n      { uri: 'at://did:does:not:exist/bad.collection.name/badrkey' },\n      { headers: sc.getHeaders(moderator) },\n    )\n    await expect(tryGetRecord).rejects.toThrow('Could not locate record')\n  })\n\n  it('takesdown and labels repos, and reverts.', async () => {\n    // takedown repo\n    await agent.api.tools.ozone.moderation.emitEvent(\n      {\n        event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        createdBy: 'did:example:admin',\n        // @ts-expect-error\n        reason: 'Y',\n        createLabelVals: ['dogs'],\n        negateLabelVals: ['cats'],\n      },\n      {\n        headers: sc.getHeaders(moderator),\n        encoding: 'application/json',\n      },\n    )\n    await network.processAll()\n    // check profile and labels\n    const tryGetProfileAppview = agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.alice },\n      {\n        headers: { ...sc.getHeaders(sc.dids.carol) },\n      },\n    )\n    await expect(tryGetProfileAppview).rejects.toThrow(\n      'Account has been suspended',\n    )\n    // reverse action\n    await agent.api.tools.ozone.moderation.emitEvent(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: sc.dids.alice,\n        },\n        event: {\n          $type: 'tools.ozone.moderation.defs#modEventReverseTakedown',\n        },\n        createdBy: 'did:example:admin',\n        // @ts-expect-error\n        reason: 'X',\n      },\n      {\n        headers: sc.getHeaders(moderator),\n        encoding: 'application/json',\n      },\n    )\n    await network.processAll()\n    // check profile and labels\n    const { data: profileAppview } = await agent.api.app.bsky.actor.getProfile(\n      { actor: sc.dids.alice },\n      {\n        headers: { ...sc.getHeaders(sc.dids.carol) },\n      },\n    )\n    expect(profileAppview).toEqual(\n      expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }),\n    )\n  })\n\n  it('takesdown and labels records, and reverts.', async () => {\n    const post = sc.posts[sc.dids.alice][0]\n    // takedown post\n    await agent.api.tools.ozone.moderation.emitEvent(\n      {\n        event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n        createdBy: 'did:example:admin',\n        // @ts-expect-error\n        reason: 'Y',\n        createLabelVals: ['dogs'],\n        negateLabelVals: ['cats'],\n      },\n      {\n        headers: sc.getHeaders(moderator),\n        encoding: 'application/json',\n      },\n    )\n    await network.processAll()\n\n    // check takedown label has been created\n    const label = await network.ozone.ctx.db.db\n      .selectFrom('label')\n      .selectAll()\n      .where('val', '=', '!takedown')\n      .where('uri', '=', post.ref.uriStr)\n      .where('cid', '=', post.ref.cidStr)\n      .executeTakeFirst()\n    expect(label).toBeDefined()\n    expect(label?.neg).toBe(false)\n\n    // reverse action\n    await agent.api.tools.ozone.moderation.emitEvent(\n      {\n        subject: {\n          $type: 'com.atproto.repo.strongRef',\n          uri: post.ref.uriStr,\n          cid: post.ref.cidStr,\n        },\n        event: { $type: 'tools.ozone.moderation.defs#modEventReverseTakedown' },\n        createdBy: 'did:example:admin',\n        // @ts-expect-error\n        reason: 'X',\n      },\n      {\n        headers: sc.getHeaders(moderator),\n        encoding: 'application/json',\n      },\n    )\n    await network.processAll()\n\n    // check takedown label has been negated\n    const labelNeg = await network.ozone.ctx.db.db\n      .selectFrom('label')\n      .selectAll()\n      .where('val', '=', '!takedown')\n      .where('uri', '=', post.ref.uriStr)\n      .where('cid', '=', post.ref.cidStr)\n      .executeTakeFirst()\n    expect(labelNeg?.neg).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/proxied/feedgen.test.ts",
    "content": "import { AtUri, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { InvalidRequestError } from '@atproto/xrpc-server'\nimport { forSnapshot } from '../_util'\nimport basicSeed from '../seeds/basic'\n\ndescribe('feedgen proxy view', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let feedUri: AtUri\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'proxy_feedgen',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc, { addModLabels: network.bsky })\n\n    feedUri = AtUri.make(sc.dids.alice, 'app.bsky.feed.generator', 'mutuals')\n\n    const feedGen = await network.createFeedGen({\n      [feedUri.toString()]: ({ params }) => {\n        if (params.feed !== feedUri.toString()) {\n          throw new InvalidRequestError('Unknown feed')\n        }\n        return {\n          encoding: 'application/json',\n          body: {\n            feed: [\n              { post: sc.posts[sc.dids.alice][0].ref.uriStr },\n              { post: sc.posts[sc.dids.carol][0].ref.uriStr },\n            ],\n          },\n        }\n      },\n    })\n\n    // publish feed\n    await agent.api.app.bsky.feed.generator.create(\n      { repo: sc.dids.alice, rkey: feedUri.rkey },\n      {\n        did: feedGen.did,\n        displayName: 'Test feed',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('performs basic proxy of getFeed', async () => {\n    const { data: feed } = await agent.api.app.bsky.feed.getFeed(\n      { feed: feedUri.toString() },\n      {\n        headers: { ...sc.getHeaders(sc.dids.alice) },\n      },\n    )\n    expect(forSnapshot(feed)).toMatchSnapshot()\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/proxied/notif.test.ts",
    "content": "import { once } from 'node:events'\nimport http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport express from 'express'\nimport { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { verifyJwt } from '@atproto/xrpc-server'\nimport { createServer } from '../../src/lexicon'\nimport usersSeed from '../seeds/users'\n\ndescribe('notif service proxy', () => {\n  let network: TestNetworkNoAppView\n  let notifServer: http.Server\n  let notifDid: string\n  let agent: AtpAgent\n  let sc: SeedClient\n  const spy: { current: unknown } = { current: null }\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'proxy_notifs',\n    })\n    network.pds.server.app.get\n    const plc = network.plc.getClient()\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n    await network.processAll()\n    // piggybacking existing plc did, turn it into a notif service\n    notifServer = await createMockNotifService(spy)\n    notifDid = sc.dids.dan\n    await plc.updateData(notifDid, network.pds.ctx.plcRotationKey, (x) => {\n      const addr = notifServer.address() as AddressInfo\n      x.services['bsky_notif'] = {\n        type: 'BskyNotificationService',\n        endpoint: `http://localhost:${addr.port}`,\n      }\n      return x\n    })\n    await network.pds.ctx.idResolver.did.resolve(notifDid, true)\n  })\n\n  afterAll(async () => {\n    await network.close()\n    notifServer.close()\n    await once(notifServer, 'close')\n  })\n\n  it('proxies to notif service.', async () => {\n    await agent.api.app.bsky.notification.registerPush(\n      {\n        serviceDid: notifDid,\n        token: 'tok1',\n        platform: 'web',\n        appId: 'app1',\n      },\n      {\n        headers: sc.getHeaders(sc.dids.bob),\n        encoding: 'application/json',\n      },\n    )\n    expect(spy.current?.['input']).toEqual({\n      serviceDid: notifDid,\n      token: 'tok1',\n      platform: 'web',\n      appId: 'app1',\n    })\n\n    const auth = await verifyJwt(\n      spy.current?.['jwt'] as string,\n      notifDid,\n      'app.bsky.notification.registerPush',\n      async (did) => {\n        const keypair = await network.pds.ctx.actorStore.keypair(did)\n        return keypair.did()\n      },\n    )\n    expect(auth.iss).toEqual(sc.dids.bob)\n  })\n})\n\nasync function createMockNotifService(ref: { current: unknown }) {\n  const app = express()\n  const svc = createServer()\n  svc.app.bsky.notification.registerPush(({ input, req }) => {\n    ref.current = {\n      input: input.body,\n      jwt: req.headers.authorization?.replace('Bearer ', ''),\n    }\n  })\n  app.use(svc.xrpc.router)\n  const server = app.listen()\n  await once(server, 'listening')\n  return server\n}\n"
  },
  {
    "path": "packages/pds/tests/proxied/procedures.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport basicSeed from '../seeds/basic'\n\ndescribe('proxies appview procedures', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let bob: string\n  let carol: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'proxy_procedures',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc, { addModLabels: network.bsky })\n    await network.processAll()\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('maintains muted actors.', async () => {\n    // mute actors\n    await agent.api.app.bsky.graph.muteActor(\n      { actor: bob },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    await agent.api.app.bsky.graph.muteActor(\n      { actor: carol },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    // check\n    const { data: result1 } = await agent.api.app.bsky.graph.getMutes(\n      {},\n      { headers: sc.getHeaders(alice) },\n    )\n    expect(result1.mutes.map((x) => x.handle)).toEqual([\n      'carol.test',\n      'bob.test',\n    ])\n    // unmute actors\n    await agent.api.app.bsky.graph.unmuteActor(\n      { actor: bob },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    // check\n    const { data: result2 } = await agent.api.app.bsky.graph.getMutes(\n      {},\n      { headers: sc.getHeaders(alice) },\n    )\n    expect(result2.mutes.map((x) => x.handle)).toEqual(['carol.test'])\n  })\n\n  it('maintains muted actor lists.', async () => {\n    // setup lists\n    const bobList = await agent.api.app.bsky.graph.list.create(\n      { repo: bob },\n      {\n        name: 'bob mutes',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: \"bob's list of mutes\",\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n    const carolList = await agent.api.app.bsky.graph.list.create(\n      { repo: carol },\n      {\n        name: 'carol mutes',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: \"carol's list of mutes\",\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(carol),\n    )\n    await network.processAll()\n\n    // mute lists\n    await agent.api.app.bsky.graph.muteActorList(\n      { list: bobList.uri },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    await agent.api.app.bsky.graph.muteActorList(\n      { list: carolList.uri },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    await network.processAll()\n    // check\n    const { data: result1 } = await agent.api.app.bsky.graph.getListMutes(\n      {},\n      { headers: sc.getHeaders(alice) },\n    )\n    expect(result1.lists.map((x) => x.uri)).toEqual([\n      carolList.uri,\n      bobList.uri,\n    ])\n    // unmute lists\n    await agent.api.app.bsky.graph.unmuteActorList(\n      { list: bobList.uri },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    // check\n    const { data: result2 } = await agent.api.app.bsky.graph.getListMutes(\n      {},\n      { headers: sc.getHeaders(alice) },\n    )\n    expect(result2.lists.map((x) => x.uri)).toEqual([carolList.uri])\n  })\n\n  it('maintains notification last seen state.', async () => {\n    // check original notifs\n    const { data: result1 } =\n      await agent.api.app.bsky.notification.listNotifications(\n        {},\n        { headers: sc.getHeaders(alice) },\n      )\n    expect(result1.notifications.length).toBeGreaterThanOrEqual(5)\n    expect(\n      result1.notifications.every((n, i) => {\n        return (i === 0 && !n.isRead) || (i !== 0 && n.isRead)\n      }),\n    ).toBe(true)\n    // update last seen\n    const { indexedAt: lastSeenAt } = result1.notifications[2]\n    await agent.api.app.bsky.notification.updateSeen(\n      { seenAt: lastSeenAt },\n      {\n        headers: sc.getHeaders(alice),\n        encoding: 'application/json',\n      },\n    )\n    // check\n    const { data: result2 } =\n      await agent.api.app.bsky.notification.listNotifications(\n        {},\n        { headers: sc.getHeaders(alice) },\n      )\n    expect(result2.notifications.some((n) => n.isRead)).toBe(true)\n    expect(result2.notifications.some((n) => !n.isRead)).toBe(true)\n    expect(result2.notifications).toEqual(\n      result1.notifications.map((n) => ({\n        ...n,\n        isRead: n.indexedAt < lastSeenAt,\n      })),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/proxied/proxy-catchall.test.ts",
    "content": "import { once } from 'node:events'\nimport http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { setTimeout as sleep } from 'node:timers/promises'\nimport * as plc from '@did-plc/lib'\nimport express from 'express'\nimport AtpAgent from '@atproto/api'\nimport { Keypair } from '@atproto/crypto'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { LexiconDoc } from '@atproto/lexicon'\n\nconst lexicons = [\n  {\n    lexicon: 1,\n    id: 'com.example.ok',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: { type: 'object', properties: { foo: { type: 'string' } } },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.slow',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: { type: 'object', properties: { foo: { type: 'string' } } },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.abort',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: { type: 'object', properties: { foo: { type: 'string' } } },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'com.example.error',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: { type: 'object', properties: { foo: { type: 'string' } } },\n        },\n      },\n    },\n  },\n] as const satisfies LexiconDoc[]\n\ndescribe('proxy header', () => {\n  let network: TestNetworkNoAppView\n  let alice: AtpAgent\n\n  let proxyServer: ProxyServer\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'proxy_catchall',\n    })\n\n    const serviceId = 'proxy_test'\n\n    proxyServer = await ProxyServer.create(\n      network.pds.ctx.plcClient,\n      network.pds.ctx.plcRotationKey,\n      serviceId,\n    )\n\n    alice = network.pds.getClient().withProxy(serviceId, proxyServer.did)\n\n    for (const lex of lexicons) alice.lex.add(lex)\n\n    await alice.createAccount({\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await proxyServer?.close()\n    await network?.close()\n  })\n\n  it('rejects when upstream unavailable', async () => {\n    const serviceId = 'foo_bar'\n\n    const proxyServer = await ProxyServer.create(\n      network.pds.ctx.plcClient,\n      network.pds.ctx.plcRotationKey,\n      serviceId,\n    )\n\n    // Make sure the service is not available\n    await proxyServer.close()\n\n    const client = alice.withProxy(serviceId, proxyServer.did)\n    for (const lex of lexicons) client.lex.add(lex)\n\n    await expect(client.call('com.example.ok')).rejects.toThrow(\n      'Upstream service unreachable',\n    )\n  })\n\n  it('successfully proxies requests', async () => {\n    await expect(alice.call('com.example.ok')).resolves.toMatchObject({\n      data: { foo: 'ok' },\n      success: true,\n    })\n  })\n\n  it('handles cancelled upstream requests', async () => {\n    await expect(alice.call('com.example.abort')).rejects.toThrow('terminated')\n  })\n\n  it('handles failing upstream requests', async () => {\n    await expect(alice.call('com.example.error')).rejects.toThrowError(\n      expect.objectContaining({\n        status: 502,\n        error: 'FooBar',\n        message: 'My message',\n      }),\n    )\n  })\n\n  it('handles cancelled downstream requests', async () => {\n    const ac = new AbortController()\n\n    setTimeout(() => ac.abort(), 20)\n\n    await expect(\n      alice.call('com.example.slow', {}, undefined, { signal: ac.signal }),\n    ).rejects.toThrow('This operation was aborted')\n\n    await expect(alice.call('com.example.slow')).resolves.toMatchObject({\n      data: { foo: 'slow' },\n      success: true,\n    })\n  })\n})\n\nclass ProxyServer {\n  constructor(\n    private server: http.Server,\n    public did: string,\n  ) {}\n\n  static async create(\n    plcClient: plc.Client,\n    keypair: Keypair,\n    serviceId: string,\n  ): Promise<ProxyServer> {\n    const app = express()\n\n    app.get('/xrpc/com.example.ok', (req, res) => {\n      res.status(200)\n      res.setHeader('content-type', 'application/json')\n      res.send('{\"foo\":\"ok\"}')\n    })\n\n    app.get('/xrpc/com.example.slow', async (req, res) => {\n      const wait = async (ms: number) => {\n        if (res.destroyed) return\n        const ac = new AbortController()\n        const abort = () => ac.abort()\n        res.on('close', abort)\n        try {\n          await sleep(ms, undefined, { signal: ac.signal })\n        } finally {\n          res.off('close', abort)\n        }\n      }\n\n      await wait(50)\n\n      res.status(200)\n      res.setHeader('content-type', 'application/json')\n      res.flushHeaders()\n\n      await wait(50)\n\n      for (const char of '{\"foo\":\"slow\"}') {\n        res.write(char)\n        await wait(10)\n      }\n\n      res.end()\n    })\n\n    app.get('/xrpc/com.example.abort', async (req, res) => {\n      res.status(200)\n      res.setHeader('content-type', 'application/json')\n      res.write('{\"foo\"')\n      await sleep(50)\n      res.destroy(new Error('abort'))\n    })\n\n    app.get('/xrpc/com.example.error', async (req, res) => {\n      res.status(500).json({ error: 'FooBar', message: 'My message' })\n    })\n\n    const server = app.listen(0)\n    server.keepAliveTimeout = 30 * 1000\n    server.headersTimeout = 35 * 1000\n    await once(server, 'listening')\n    const { port } = server.address() as AddressInfo\n\n    const plcOp = await plc.signOperation(\n      {\n        type: 'plc_operation',\n        rotationKeys: [keypair.did()],\n        alsoKnownAs: [],\n        verificationMethods: {},\n        services: {\n          [serviceId]: {\n            type: 'TestAtprotoService',\n            endpoint: `http://localhost:${port}`,\n          },\n        },\n        prev: null,\n      },\n      keypair,\n    )\n    const did = await plc.didForCreateOp(plcOp)\n    await plcClient.sendOperation(did, plcOp)\n    return new ProxyServer(server, did)\n  }\n\n  async close() {\n    await new Promise<void>((resolve, reject) => {\n      this.server.close((err) => {\n        if (err) reject(err)\n        else resolve()\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "packages/pds/tests/proxied/proxy-header.test.ts",
    "content": "import assert from 'node:assert'\nimport { once } from 'node:events'\nimport http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport * as plc from '@did-plc/lib'\nimport express from 'express'\nimport { Keypair } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env'\nimport { verifyJwt } from '@atproto/xrpc-server'\nimport { parseProxyHeader } from '../../src/pipethrough'\n\ndescribe('proxy header', () => {\n  let network: TestNetworkNoAppView\n  let sc: SeedClient\n\n  let alice: string\n\n  let proxyServer: ProxyServer\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'proxy_header',\n    })\n    sc = network.getSeedClient()\n    await usersSeed(sc)\n\n    proxyServer = await ProxyServer.create(\n      network.pds.ctx.plcClient,\n      network.pds.ctx.plcRotationKey,\n      'atproto_test',\n    )\n\n    alice = sc.dids.alice\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await proxyServer.close()\n    await network.close()\n  })\n\n  it('parses proxy header', async () => {\n    expect(parseProxyHeader(network.pds.ctx, `#atproto_test`)).rejects.toThrow(\n      'no did specified in proxy header',\n    )\n\n    expect(\n      parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test#foo`),\n    ).rejects.toThrow('invalid proxy header format')\n\n    expect(\n      parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test `),\n    ).rejects.toThrow('proxy header cannot contain spaces')\n\n    expect(\n      parseProxyHeader(network.pds.ctx, ` ${proxyServer.did}#atproto_test`),\n    ).rejects.toThrow('proxy header cannot contain spaces')\n\n    expect(parseProxyHeader(network.pds.ctx, `did:foo#bar`)).rejects.toThrow(\n      'Poorly formatted DID: did:foo',\n    )\n\n    expect(\n      parseProxyHeader(network.pds.ctx, `did:foo:bar#baz`),\n    ).rejects.toThrow('Unsupported DID method: did:foo:bar')\n\n    expect(\n      parseProxyHeader(network.pds.ctx, `did:toString:foo#bar`),\n    ).rejects.toThrow('Unsupported DID method: did:toString:foo')\n\n    expect(parseProxyHeader(network.pds.ctx, `foo#bar`)).rejects.toThrow(\n      'Poorly formatted DID: foo',\n    )\n\n    expect(\n      parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test`),\n    ).resolves.toEqual({\n      did: proxyServer.did,\n      url: proxyServer.url,\n    })\n  })\n\n  it('proxies requests based on header', async () => {\n    const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`\n    await fetch(`${network.pds.url}${path}`, {\n      headers: {\n        ...sc.getHeaders(alice),\n        'atproto-proxy': `${proxyServer.did}#atproto_test`,\n      },\n    })\n    const req = proxyServer.requests.at(-1)\n    assert(req)\n    expect(req.url).toEqual(path)\n    assert(req.auth)\n    const verified = await verifyJwt(\n      req.auth.replace('Bearer ', ''),\n      proxyServer.did,\n      'app.bsky.actor.getProfile',\n      (iss) => network.pds.ctx.idResolver.did.resolveAtprotoKey(iss, true),\n    )\n    expect(verified.aud).toBe(proxyServer.did)\n    expect(verified.iss).toBe(alice)\n  })\n\n  it('fails on a non-existant did', async () => {\n    const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`\n    const response = await fetch(`${network.pds.url}${path}`, {\n      headers: {\n        ...sc.getHeaders(alice),\n        'atproto-proxy': `did:plc:12345678123456781234578#atproto_test`,\n      },\n    })\n\n    await expect(response.json()).resolves.toMatchObject({\n      message: 'could not resolve proxy did',\n    })\n\n    expect(proxyServer.requests.length).toBe(1)\n  })\n\n  it('fails when a service is not specified', async () => {\n    const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`\n    const response = await fetch(`${network.pds.url}${path}`, {\n      headers: {\n        ...sc.getHeaders(alice),\n        'atproto-proxy': proxyServer.did,\n      },\n    })\n\n    await expect(response.json()).resolves.toMatchObject({\n      message: 'no service id specified in proxy header',\n    })\n\n    expect(proxyServer.requests.length).toBe(1)\n  })\n\n  it('fails on a non-existant service', async () => {\n    const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`\n    const response = await fetch(`${network.pds.url}${path}`, {\n      headers: {\n        ...sc.getHeaders(alice),\n        'atproto-proxy': `${proxyServer.did}#atproto_bad`,\n      },\n    })\n\n    await expect(response.json()).resolves.toMatchObject({\n      message: 'could not resolve proxy did service url',\n    })\n\n    expect(proxyServer.requests.length).toBe(1)\n  })\n\n  it('handles failing manual pipethroughs', async () => {\n    // This is a PDS endpoint which uses a manual pipethrough() in its handler\n    const path = '/xrpc/app.bsky.actor.getPreferences'\n    const res = await fetch(`${network.pds.url}${path}`, {\n      headers: {\n        ...sc.getHeaders(alice),\n        'atproto-proxy': `${proxyServer.did}#atproto_test`,\n      },\n    })\n    await res.arrayBuffer() // drain\n    expect(res.status).toBe(501)\n  })\n})\n\ntype ProxyReq = {\n  url: string\n  auth: string | undefined\n}\n\nclass ProxyServer {\n  constructor(\n    public server: http.Server,\n    public url: string,\n    public did: string,\n    public requests: ProxyReq[],\n  ) {}\n\n  static async create(\n    plcClient: plc.Client,\n    keypair: Keypair,\n    serviceId: string,\n  ): Promise<ProxyServer> {\n    const requests: ProxyReq[] = []\n    const app = express()\n\n    // This is a PDS endpoint which uses a manual pipethrough() in its handler\n    app.get('/xrpc/app.bsky.actor.getPreferences', (req, res) => {\n      res.sendStatus(501)\n    })\n\n    app.get('*', (req, res) => {\n      requests.push({\n        url: req.url,\n        auth: req.header('authorization'),\n      })\n      res.sendStatus(200)\n    })\n\n    const server = app.listen(0)\n    await once(server, 'listening')\n\n    const { port } = server.address() as AddressInfo\n\n    const url = `http://localhost:${port}`\n    const plcOp = await plc.signOperation(\n      {\n        type: 'plc_operation',\n        rotationKeys: [keypair.did()],\n        alsoKnownAs: [],\n        verificationMethods: {},\n        services: {\n          [serviceId]: {\n            type: 'TestAtprotoService',\n            endpoint: url,\n          },\n        },\n        prev: null,\n      },\n      keypair,\n    )\n    const did = await plc.didForCreateOp(plcOp)\n    await plcClient.sendOperation(did, plcOp)\n    return new ProxyServer(server, url, did, requests)\n  }\n\n  close(): Promise<void> {\n    return new Promise<void>((resolve) => {\n      this.server.close(() => resolve())\n    })\n  }\n}\n"
  },
  {
    "path": "packages/pds/tests/proxied/read-after-write.test.ts",
    "content": "import assert from 'node:assert'\nimport util from 'node:util'\nimport { request } from 'undici'\nimport { AtpAgent } from '@atproto/api'\nimport { RecordRef, SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { isView as isExternalEmbedView } from '../../src/lexicon/types/app/bsky/embed/external'\nimport { isView as isImagesEmbedView } from '../../src/lexicon/types/app/bsky/embed/images'\nimport { isView as isRecordEmbedView } from '../../src/lexicon/types/app/bsky/embed/record'\nimport {\n  ThreadViewPost,\n  isThreadViewPost,\n} from '../../src/lexicon/types/app/bsky/feed/defs'\nimport basicSeed from '../seeds/basic'\n\ndescribe('proxy read after write', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let carol: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'proxy_read_after_write',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc, { addModLabels: network.bsky })\n    await network.processAll()\n    alice = sc.dids.alice\n    carol = sc.dids.carol\n    await network.bsky.sub.destroy()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('handles read after write on profiles', async () => {\n    await sc.updateProfile(alice, { displayName: 'blah' })\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    expect(res.data.displayName).toEqual('blah')\n    expect(res.data.description).toBeUndefined()\n  })\n\n  it('handles image formatting', async () => {\n    assert(network.pds.ctx.cfg.bskyAppView)\n    const blob = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n    await sc.updateProfile(alice, { displayName: 'blah', avatar: blob.image })\n\n    const res = await agent.api.app.bsky.actor.getProfile(\n      { actor: alice },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    expect(res.data.avatar).toEqual(\n      util.format(\n        network.pds.ctx.cfg.bskyAppView.cdnUrlPattern,\n        'avatar',\n        alice,\n        blob.image.ref.toString(),\n      ),\n    )\n  })\n\n  it('handles read after write on getAuthorFeed', async () => {\n    const res = await agent.api.app.bsky.feed.getAuthorFeed(\n      { actor: alice },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    for (const item of res.data.feed) {\n      if (item.post.author.did === alice) {\n        expect(item.post.author.displayName).toEqual('blah')\n      }\n    }\n  })\n\n  let replyRef1: RecordRef\n  let replyRef2: RecordRef\n\n  it('handles read after write on threads', async () => {\n    const reply1 = await sc.reply(\n      alice,\n      sc.posts[alice][0].ref,\n      sc.posts[alice][0].ref,\n      'another reply',\n    )\n    const reply2 = await sc.reply(\n      alice,\n      sc.posts[alice][0].ref,\n      reply1.ref,\n      'another another reply',\n    )\n    replyRef1 = reply1.ref\n    replyRef2 = reply2.ref\n    const res = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][0].ref.uriStr },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    const thread = res.data.thread as ThreadViewPost\n    const layerOne = thread.replies as ThreadViewPost[]\n    expect(layerOne.length).toBe(1)\n    expect(layerOne[0].post.uri).toEqual(reply1.ref.uriStr)\n    const layerTwo = layerOne[0].replies as ThreadViewPost[]\n    expect(layerTwo.length).toBe(1)\n    expect(layerTwo[0].post.uri).toEqual(reply2.ref.uriStr)\n\n    const aliceHandle = sc.accounts[alice].handle\n    const handleUriStr = thread.post.uri.replace(alice, aliceHandle)\n    expect(handleUriStr).not.toEqual(thread.post.uri)\n    const handleRes = await agent.api.app.bsky.feed.getPostThread(\n      { uri: handleUriStr },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    expect(handleRes.data.thread).toEqual(res.data.thread)\n  })\n\n  it('handles read after write on a thread that is not found on appview', async () => {\n    const res = await agent.api.app.bsky.feed.getPostThread(\n      { uri: replyRef1.uriStr },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    const thread = res.data.thread as ThreadViewPost\n    expect(thread.post.uri).toEqual(replyRef1.uriStr)\n    expect((thread.parent as ThreadViewPost).post.uri).toEqual(\n      sc.posts[alice][0].ref.uriStr,\n    )\n    expect(thread.replies?.length).toEqual(1)\n    expect((thread.replies?.at(0) as ThreadViewPost).post.uri).toEqual(\n      replyRef2.uriStr,\n    )\n\n    const aliceHandle = sc.accounts[alice].handle\n    const handleUriStr = thread.post.uri.replace(alice, aliceHandle)\n    expect(handleUriStr).not.toEqual(thread.post.uri)\n    const handleRes = await agent.api.app.bsky.feed.getPostThread(\n      { uri: handleUriStr },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    expect(handleRes.data.thread).toEqual(res.data.thread)\n  })\n\n  it('handles read after write on threads with record embeds', async () => {\n    assert(network.pds.ctx.cfg.bskyAppView)\n    const img = await sc.uploadFile(\n      alice,\n      '../dev-env/assets/key-landscape-small.jpg',\n      'image/jpeg',\n    )\n    const replyRes1 = await agent.api.app.bsky.feed.post.create(\n      { repo: alice },\n      {\n        text: 'images test',\n        reply: {\n          root: sc.posts[alice][2].ref.raw,\n          parent: sc.posts[alice][2].ref.raw,\n        },\n        embed: {\n          $type: 'app.bsky.embed.images',\n          images: [\n            {\n              image: img.image,\n              aspectRatio: { height: 2, width: 1 },\n              alt: 'alt text',\n            },\n          ],\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    const replyRes2 = await agent.api.app.bsky.feed.post.create(\n      { repo: alice },\n      {\n        text: 'external test',\n        reply: {\n          root: sc.posts[alice][2].ref.raw,\n          parent: {\n            uri: replyRes1.uri,\n            cid: replyRes1.cid,\n          },\n        },\n        embed: {\n          $type: 'app.bsky.embed.external',\n          external: {\n            uri: 'https://example.com',\n            title: 'TestImage',\n            description: 'testLink',\n            thumb: img.image,\n          },\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n\n    const res = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[alice][2].ref.uriStr },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    assert(isThreadViewPost(res.data.thread))\n    // @ts-ignore \"pnpm verify:types\" fails though VSCode doesn't complain\n    assert(res.data.thread.replies, 'replies is undefined')\n    // @ts-ignore \"pnpm verify:types\" fails though VSCode doesn't complain\n    const { replies } = res.data.thread\n    expect(replies.length).toBe(1)\n    assert(isThreadViewPost(replies[0]))\n    expect(replies[0].post.uri).toEqual(replyRes1.uri)\n    const { embed } = replies[0].post\n    assert(isImagesEmbedView(embed))\n    expect(embed.images[0].fullsize).toEqual(\n      util.format(\n        network.pds.ctx.cfg.bskyAppView.cdnUrlPattern,\n        'feed_fullsize',\n        alice,\n        img.image.ref.toString(),\n      ),\n    )\n    expect(embed.images[0].aspectRatio).toEqual({ height: 2, width: 1 })\n    expect(embed.images[0].alt).toBe('alt text')\n    assert(replies[0].replies, 'replies[0].replies is undefined')\n    expect(replies[0].replies.length).toBe(1)\n    assert(isThreadViewPost(replies[0].replies[0]))\n    expect(replies[0].replies[0].post.uri).toEqual(replyRes2.uri)\n    const external = replies[0].replies[0].post.embed\n    assert(isExternalEmbedView(external))\n    expect(external.external.title).toEqual('TestImage')\n    expect(external.external.thumb).toEqual(\n      util.format(\n        network.pds.ctx.cfg.bskyAppView.cdnUrlPattern,\n        'feed_thumbnail',\n        alice,\n        img.image.ref.toString(),\n      ),\n    )\n  })\n\n  it('handles read after write on threads with record embeds', async () => {\n    const replyRes = await agent.api.app.bsky.feed.post.create(\n      { repo: alice },\n      {\n        text: 'blah',\n        reply: {\n          root: sc.posts[carol][0].ref.raw,\n          parent: sc.posts[carol][0].ref.raw,\n        },\n        embed: {\n          $type: 'app.bsky.embed.record',\n          record: sc.posts[alice][0].ref.raw,\n        },\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    const res = await agent.api.app.bsky.feed.getPostThread(\n      { uri: sc.posts[carol][0].ref.uriStr },\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    assert(isThreadViewPost(res.data.thread))\n    assert(res.data.thread.replies, 'replies is undefined')\n    const { replies } = res.data.thread\n    expect(replies.length).toBe(1)\n    assert(isThreadViewPost(replies[0]))\n    expect(replies[0].post.uri).toEqual(replyRes.uri)\n    const embed = replies[0].post.embed\n    assert(isRecordEmbedView(embed))\n    assert('uri' in embed.record) // @TODO: assert based in \"$type\"\n    expect(embed.record.uri).toEqual(sc.posts[alice][0].ref.uriStr)\n  })\n\n  it('handles read after write on getTimeline', async () => {\n    const postRes = await agent.api.app.bsky.feed.post.create(\n      { repo: alice },\n      {\n        text: 'poast',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    expect(res.data.feed[0].post.uri).toEqual(postRes.uri)\n  })\n\n  it('returns lag headers', async () => {\n    const res = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      { headers: { ...sc.getHeaders(alice) } },\n    )\n    const lag = res.headers['atproto-upstream-lag']\n    assert(lag !== undefined)\n    const parsed = parseInt(lag)\n    expect(parsed > 0).toBe(true)\n  })\n\n  it('negotiates encoding', async () => {\n    const identity = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      { headers: { ...sc.getHeaders(alice), 'accept-encoding': 'identity' } },\n    )\n    expect(identity.headers['content-encoding']).toBeUndefined()\n\n    const gzip = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      {\n        headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip, *;q=0' },\n      },\n    )\n    expect(gzip.headers['content-encoding']).toBe('gzip')\n  })\n\n  it('defaults to identity encoding', async () => {\n    // Not using the \"agent\" because \"fetch()\" will add \"accept-encoding: gzip,\n    // deflate\" if not \"accept-encoding\" header is provided\n    const res = await request(\n      new URL(`/xrpc/app.bsky.feed.getTimeline`, agent.dispatchUrl),\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(res.statusCode).toBe(200)\n    expect(res.headers['content-encoding']).toBeUndefined()\n  })\n\n  it('falls back to identity encoding', async () => {\n    const invalid = await agent.api.app.bsky.feed.getTimeline(\n      {},\n      { headers: { ...sc.getHeaders(alice), 'accept-encoding': 'invalid' } },\n    )\n\n    expect(invalid.headers['content-encoding']).toBeUndefined()\n  })\n\n  it('errors when failing to negotiate encoding', async () => {\n    await expect(\n      agent.api.app.bsky.feed.getTimeline(\n        {},\n        {\n          headers: {\n            ...sc.getHeaders(alice),\n            'accept-encoding': 'invalid, *;q=0',\n          },\n        },\n      ),\n    ).rejects.toThrow(\n      expect.objectContaining({\n        status: 406,\n        message: 'this service does not support any of the requested encodings',\n      }),\n    )\n  })\n\n  it('errors on invalid content-encoding format', async () => {\n    await expect(\n      agent.api.app.bsky.feed.getTimeline(\n        {},\n        {\n          headers: {\n            ...sc.getHeaders(alice),\n            'accept-encoding': ';q=1',\n          },\n        },\n      ),\n    ).rejects.toThrow(\n      expect.objectContaining({\n        status: 400,\n        message: 'Invalid accept-encoding: \";q=1\"',\n      }),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/proxied/views.test.ts",
    "content": "import { AtUri, AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { forSnapshot } from '../_util'\nimport basicSeed from '../seeds/basic'\n\ndescribe('proxies view requests', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'proxy_views',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc, { addModLabels: network.bsky })\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n    const listRef = await sc.createList(alice, 'test list', 'curate')\n    await sc.addToList(alice, alice, listRef)\n    await sc.addToList(alice, bob, listRef)\n    await network.processAll()\n  })\n\n  beforeAll(async () => {\n    await agent.app.bsky.feed.generator.create(\n      { repo: alice, rkey: 'all' },\n      {\n        did: 'did:example:feedgen',\n        displayName: 'All',\n        description: 'Provides all feed candidates',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(alice),\n    )\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('actor.getProfile', async () => {\n    const res = await agent.app.bsky.actor.getProfile(\n      {\n        actor: bob,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('actor.getProfiles', async () => {\n    const res = await agent.app.bsky.actor.getProfiles(\n      {\n        actors: [alice, bob],\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('actor.getSuggestions', async () => {\n    // mock some suggestions\n    const suggestions = [\n      { did: sc.dids.bob, order: 1 },\n      { did: sc.dids.carol, order: 2 },\n      { did: sc.dids.dan, order: 3 },\n    ]\n    await network.bsky.db.db\n      .insertInto('suggested_follow')\n      .values(suggestions)\n      .execute()\n\n    const res = await agent.app.bsky.actor.getSuggestions(\n      {},\n      {\n        headers: { ...sc.getHeaders(carol) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.actor.getSuggestions(\n      {\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(carol) },\n      },\n    )\n    const pt2 = await agent.app.bsky.actor.getSuggestions(\n      {\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(carol) },\n      },\n    )\n    expect([...pt1.data.actors, ...pt2.data.actors]).toEqual(res.data.actors)\n  })\n\n  it('actor.searchActor', async () => {\n    const res = await agent.app.bsky.actor.searchActors(\n      {\n        term: '.test',\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    // sort because pagination is done off of did\n    const sortedFull = res.data.actors.sort((a, b) =>\n      a.handle > b.handle ? 1 : -1,\n    )\n    expect(forSnapshot(sortedFull)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.actor.searchActors(\n      {\n        term: '.test',\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.actor.searchActors(\n      {\n        term: '.test',\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const sortedPaginated = [...pt1.data.actors, ...pt2.data.actors].sort(\n      (a, b) => (a.handle > b.handle ? 1 : -1),\n    )\n    expect(sortedPaginated).toEqual(sortedFull)\n  })\n\n  it('actor.searchActorTypeahead', async () => {\n    const res = await agent.app.bsky.actor.searchActorsTypeahead(\n      {\n        term: '.test',\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const sorted = res.data.actors.sort((a, b) =>\n      a.handle > b.handle ? 1 : -1,\n    )\n    expect(forSnapshot(sorted)).toMatchSnapshot()\n  })\n\n  it('feed.getAuthorFeed', async () => {\n    const res = await agent.app.bsky.feed.getAuthorFeed(\n      {\n        actor: bob,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.feed.getAuthorFeed(\n      {\n        actor: bob,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.feed.getAuthorFeed(\n      {\n        actor: bob,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed)\n  })\n\n  it('feed.getListFeed', async () => {\n    const list = Object.values(sc.lists[alice])[0].ref.uriStr\n    const res = await agent.app.bsky.feed.getListFeed(\n      {\n        list,\n      },\n      {\n        headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.feed.getListFeed(\n      {\n        list,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' },\n      },\n    )\n    const pt2 = await agent.app.bsky.feed.getListFeed(\n      {\n        list,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' },\n      },\n    )\n    expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed)\n  })\n\n  it('feed.getLikes', async () => {\n    const postUri = sc.posts[carol][0].ref.uriStr\n    const res = await agent.app.bsky.feed.getLikes(\n      {\n        uri: postUri,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.feed.getLikes(\n      {\n        uri: postUri,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.feed.getLikes(\n      {\n        uri: postUri,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.likes, ...pt2.data.likes]).toEqual(res.data.likes)\n  })\n\n  it('feed.getRepostedBy', async () => {\n    const postUri = sc.posts[dan][1].ref.uriStr\n    const res = await agent.app.bsky.feed.getRepostedBy(\n      {\n        uri: postUri,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.feed.getRepostedBy(\n      {\n        uri: postUri,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.feed.getRepostedBy(\n      {\n        uri: postUri,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.repostedBy, ...pt2.data.repostedBy]).toEqual(\n      res.data.repostedBy,\n    )\n  })\n\n  it('feed.getPosts', async () => {\n    const uris = [sc.posts[bob][0].ref.uriStr, sc.posts[carol][0].ref.uriStr]\n    const res = await agent.app.bsky.feed.getPosts(\n      {\n        uris,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('feed.getTimeline', async () => {\n    const res = await agent.app.bsky.feed.getTimeline(\n      {},\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.feed.getTimeline(\n      {\n        limit: 2,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.feed.getTimeline(\n      {\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed)\n  })\n\n  // @TODO disabled during appview v2 buildout\n  it('unspecced.getPopularFeedGenerators', async () => {\n    const res = await agent.app.bsky.unspecced.getPopularFeedGenerators(\n      {},\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  let feedUri: string\n  it('feed.getFeedGenerator', async () => {\n    feedUri = AtUri.make(\n      sc.dids.alice,\n      'app.bsky.feed.generator',\n      'my-feed',\n    ).toString()\n    const gen = await network.createFeedGen({\n      [feedUri]: async () => {\n        return {\n          encoding: 'application/json',\n          body: { feed: [] },\n        }\n      },\n    })\n    await agent.app.bsky.feed.generator.create(\n      { repo: sc.dids.alice, rkey: 'my-feed' },\n      {\n        did: gen.did,\n        displayName: 'MyFeed',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(sc.dids.alice),\n    )\n    await network.processAll()\n    const res = await agent.app.bsky.feed.getFeedGenerator(\n      { feed: feedUri },\n      {\n        headers: { ...sc.getHeaders(sc.dids.alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('feed.getFeedGenerators', async () => {\n    const res = await agent.app.bsky.feed.getFeedGenerators(\n      { feeds: [feedUri.toString()] },\n      {\n        headers: { ...sc.getHeaders(sc.dids.alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n  })\n\n  it('graph.getBlocks', async () => {\n    await sc.block(alice, bob)\n    await sc.block(alice, carol)\n    await network.processAll()\n    const res = await agent.app.bsky.graph.getBlocks(\n      {},\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.graph.getBlocks(\n      {\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.graph.getBlocks(\n      {\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.blocks, ...pt2.data.blocks]).toEqual(res.data.blocks)\n    await sc.unblock(alice, bob)\n    await sc.unblock(alice, carol)\n    await network.processAll()\n  })\n\n  it('graph.getFollows', async () => {\n    const res = await agent.app.bsky.graph.getFollows(\n      { actor: bob },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.graph.getFollows(\n      {\n        actor: bob,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.graph.getFollows(\n      {\n        actor: bob,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.follows, ...pt2.data.follows]).toEqual(res.data.follows)\n  })\n\n  it('graph.getFollowers', async () => {\n    const res = await agent.app.bsky.graph.getFollowers(\n      { actor: bob },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.graph.getFollowers(\n      {\n        actor: bob,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.graph.getFollowers(\n      {\n        actor: bob,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.followers, ...pt2.data.followers]).toEqual(\n      res.data.followers,\n    )\n  })\n\n  let listUri: string\n\n  it('graph.getList', async () => {\n    const bobList = await agent.app.bsky.graph.list.create(\n      { repo: bob },\n      {\n        name: 'bob mutes',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: \"bob's list of mutes\",\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n    listUri = bobList.uri.toString()\n    await agent.app.bsky.graph.list.create(\n      { repo: bob },\n      {\n        name: 'another list',\n        purpose: 'app.bsky.graph.defs#modlist',\n        description: 'a second list',\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n    await agent.app.bsky.graph.listitem.create(\n      { repo: bob },\n      {\n        subject: alice,\n        list: listUri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n    await agent.app.bsky.graph.listitem.create(\n      { repo: bob },\n      {\n        subject: carol,\n        list: listUri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n    await network.processAll()\n\n    const res = await agent.app.bsky.graph.getList(\n      { list: listUri },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.graph.getList(\n      {\n        list: listUri,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.graph.getList(\n      {\n        list: listUri,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.items, ...pt2.data.items]).toEqual(res.data.items)\n  })\n\n  it('graph.getLists', async () => {\n    const res = await agent.app.bsky.graph.getLists(\n      { actor: bob },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect(forSnapshot(res.data)).toMatchSnapshot()\n    const pt1 = await agent.app.bsky.graph.getLists(\n      {\n        actor: bob,\n        limit: 1,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    const pt2 = await agent.app.bsky.graph.getLists(\n      {\n        actor: bob,\n        cursor: pt1.data.cursor,\n      },\n      {\n        headers: { ...sc.getHeaders(alice) },\n      },\n    )\n    expect([...pt1.data.lists, ...pt2.data.lists]).toEqual(res.data.lists)\n  })\n\n  it('graph.getListBlocks', async () => {\n    await agent.app.bsky.graph.listblock.create(\n      { repo: bob },\n      {\n        subject: listUri,\n        createdAt: new Date().toISOString(),\n      },\n      sc.getHeaders(bob),\n    )\n    await network.processAll()\n    const pt1 = await agent.app.bsky.graph.getListBlocks(\n      {},\n      { headers: sc.getHeaders(bob) },\n    )\n    expect(forSnapshot(pt1.data)).toMatchSnapshot()\n    const pt2 = await agent.app.bsky.graph.getListBlocks(\n      { cursor: pt1.data.cursor },\n      { headers: sc.getHeaders(bob) },\n    )\n    expect(pt2.data.lists).toEqual([])\n    expect(pt2.data.cursor).not.toBeDefined()\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/races.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { wait } from '@atproto/common'\nimport { Keypair } from '@atproto/crypto'\nimport { TestNetworkNoAppView } from '@atproto/dev-env'\nimport { readCarWithRoot, verifyRepo } from '@atproto/repo'\nimport { AppContext } from '../src/context'\nimport { PreparedCreate, prepareCreate } from '../src/repo'\n\ndescribe('races', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let agent: AtpAgent\n  let did: string\n  let signingKey: Keypair\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'races',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    agent = network.pds.getClient()\n    await agent.createAccount({\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n    did = agent.accountDid\n    signingKey = await network.pds.ctx.actorStore.keypair(did)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const processCommitWithWait = async (\n    did: string,\n    write: PreparedCreate,\n    waitMs: number,\n  ) => {\n    const now = new Date().toISOString()\n    return ctx.actorStore.transact(did, async (store) => {\n      const commitData = await store.repo.formatCommit([write])\n      await store.repo.storage.applyCommit(commitData)\n      await wait(waitMs)\n      await store.repo.indexWrites([write], now)\n      return write\n    })\n  }\n\n  it('handles races in record routes', async () => {\n    const write = await prepareCreate({\n      did,\n      collection: 'app.bsky.feed.post',\n      record: {\n        text: 'one',\n        createdAt: new Date().toISOString(),\n      },\n      validate: true,\n    })\n\n    const processPromise = processCommitWithWait(did, write, 500)\n\n    const createdPost = await agent.api.app.bsky.feed.post.create(\n      { repo: did },\n      { text: 'two', createdAt: new Date().toISOString() },\n    )\n\n    await processPromise\n\n    const listed = await agent.api.app.bsky.feed.post.list({ repo: did })\n    expect(listed.records.length).toBe(2)\n\n    const carRes = await agent.api.com.atproto.sync.getRepo({ did })\n    const car = await readCarWithRoot(carRes.data)\n    const verified = await verifyRepo(\n      car.blocks,\n      car.root,\n      did,\n      signingKey.did(),\n    )\n    expect(verified.creates.length).toBe(2)\n    expect(verified.creates[0].cid.toString()).toEqual(write.cid.toString())\n    expect(verified.creates[1].cid.toString()).toEqual(\n      createdPost.cid.toString(),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/rate-limits.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { randomStr } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport userSeed from './seeds/basic'\n\ndescribe('rate limits', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n  let alice: string\n  let bob: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'rate_limits',\n      pds: {\n        redisScratchAddress: process.env.REDIS_HOST,\n        redisScratchPassword: process.env.REDIS_PASSWORD,\n        rateLimitsEnabled: true,\n      },\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await userSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('rate limits by ip', async () => {\n    const attempt = () =>\n      agent.api.com.atproto.server.resetPassword({\n        token: randomStr(4, 'base32'),\n        password: 'asdf1234',\n      })\n    for (let i = 0; i < 50; i++) {\n      try {\n        await attempt()\n      } catch (err) {\n        // do nothing\n      }\n    }\n    await expect(attempt).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('rate limits by a custom key', async () => {\n    const attempt = () =>\n      agent.api.com.atproto.server.createSession({\n        identifier: sc.accounts[alice].handle,\n        password: 'asdf1234',\n      })\n    for (let i = 0; i < 30; i++) {\n      try {\n        await attempt()\n      } catch (err) {\n        // do nothing\n      }\n    }\n    await expect(attempt).rejects.toThrow('Rate Limit Exceeded')\n\n    // does not rate limit for another key\n    await agent.api.com.atproto.server.createSession({\n      identifier: sc.accounts[bob].handle,\n      password: sc.accounts[bob].password,\n    })\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/recovery.test.ts",
    "content": "import fs from 'node:fs/promises'\nimport * as ui8 from 'uint8arrays'\nimport AtpAgent from '@atproto/api'\nimport { renameIfExists, rmIfExists } from '@atproto/common'\nimport { SeedClient, TestNetworkNoAppView, basicSeed } from '@atproto/dev-env'\nimport { verifyRepoCar } from '@atproto/repo'\nimport { AppContext, scripts } from '../dist'\n\ndescribe('recovery', () => {\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n  let sc: SeedClient\n  let agent: AtpAgent\n  let alice: string\n  let bob: string\n  let elli: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'recovery',\n    })\n    ctx = network.pds.ctx\n    sc = network.getSeedClient()\n    agent = network.pds.getClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getStats = (did: string) => {\n    return ctx.actorStore.read(did, async (store) => {\n      const recordCount = await store.record.recordCount()\n      const root = await store.repo.storage.getRootDetailed()\n      return {\n        recordCount,\n        rev: root.rev,\n        commit: root.cid,\n      }\n    })\n  }\n\n  const getRev = (did: string) => {\n    return ctx.actorStore.read(did, async (store) => {\n      const root = await store.repo.storage.getRootDetailed()\n      return root.rev\n    })\n  }\n\n  const getCar = async (did: string, since?: string) => {\n    const res = await agent.api.com.atproto.sync.getRepo({\n      did,\n      since,\n    })\n    return res.data\n  }\n\n  const backup = async (dids: string[]) => {\n    for (const did of dids) {\n      const { dbLocation, keyLocation } = await ctx.actorStore.getLocation(did)\n      await fs.copyFile(dbLocation, `${dbLocation}-backup`)\n      await fs.copyFile(keyLocation, `${keyLocation}-backup`)\n    }\n  }\n\n  const restore = async (dids: string[]) => {\n    for (const did of dids) {\n      const { dbLocation, keyLocation } = await ctx.actorStore.getLocation(did)\n      await rmIfExists(dbLocation)\n      await rmIfExists(keyLocation)\n      await renameIfExists(`${dbLocation}-backup`, dbLocation)\n      await renameIfExists(`${keyLocation}-backup`, keyLocation)\n    }\n  }\n\n  it('recovers repos based on the sequencer ', async () => {\n    // backup alice & bob\n    await backup([alice, bob])\n\n    // grab rev times from intermediate repo states\n    // process a bunch of record creates, updates, and delets for alice\n    const startRev = await getRev(alice)\n    let middleRev = ''\n    for (let i = 0; i < 100; i++) {\n      if (i === 0) {\n        middleRev = await getRev(alice)\n      }\n      const ref = await sc.post(alice, `post-${i}`)\n      if (i % 20 === 0) {\n        await sc.updateProfile(alice, { displayName: `name-${i}` })\n      }\n      if (i % 10 === 0) {\n        await sc.deletePost(alice, ref.ref.uri)\n      } else {\n        await sc.like(alice, ref.ref)\n      }\n    }\n\n    // delete bob's account\n    const deleteToken = await ctx.accountManager.createEmailToken(\n      bob,\n      'delete_account',\n    )\n    await agent.com.atproto.server.deleteAccount({\n      token: deleteToken,\n      did: bob,\n      password: sc.accounts[bob].password,\n    })\n\n    // create a new account (elli)\n    await sc.createAccount('elli', {\n      handle: 'elli.test',\n      password: 'elli-pass',\n      email: 'elli@test.com',\n    })\n    elli = sc.dids.elli\n    for (let i = 0; i < 10; i++) {\n      await sc.post(elli, `post-${i}`)\n    }\n    // get some stats & snapshots from before we do a recovery\n    const endRev = await getRev(alice)\n    const startCarBefore = await getCar(alice, startRev)\n    const middleCarBefore = await getCar(alice, middleRev)\n    const endCarBefore = await getCar(alice, endRev)\n    const aliceStatsBefore = await getStats(alice)\n    const elliCarBefore = await getCar(elli)\n    const elliStatsBefore = await getStats(elli)\n\n    // \"restore\" all 3 accounts to their backedup state, effectively rolling back the previous mutations\n    // deleting alice's mutations, restoring bob's account, and deleting elli's account\n    await restore([alice, bob, elli])\n\n    // run recovery operation\n    await scripts['sequencer-recovery'](network.pds.ctx, ['0', '10', 'true'])\n\n    // ensure alice's CAR is exactly the same as before the loss, including intermediate states based on tracked revs\n    const startCarAfter = await getCar(alice, startRev)\n    const middleCarAfter = await getCar(alice, middleRev)\n    const endCarAfter = await getCar(alice, endRev)\n    const aliceStatsAfter = await getStats(alice)\n    expect(ui8.equals(startCarAfter, startCarBefore)).toBe(true)\n    expect(ui8.equals(middleCarAfter, middleCarBefore)).toBe(true)\n    expect(ui8.equals(endCarAfter, endCarBefore)).toBe(true)\n    expect(aliceStatsAfter).toMatchObject(aliceStatsBefore)\n\n    // ensure bob's account is re-deleted\n    const attempt = getCar(bob)\n    await expect(attempt).rejects.toThrow(/Could not find repo for DID/)\n    const bobExists = await ctx.actorStore.exists(bob)\n    expect(bobExists).toBe(false)\n\n    // ensure elli's CAR is exactly the same as before the loss\n    // this involves creating a new signing key for her and updating her DID document\n    const elliCarAfter = await getCar(elli)\n    const elliStatsAfter = await getStats(elli)\n    expect(ui8.equals(elliCarAfter, elliCarBefore)).toBe(true)\n    expect(elliStatsAfter).toMatchObject(elliStatsBefore)\n    // it creates a new keypair for elli\n    const elliKey = await ctx.actorStore.keypair(elli)\n    expect(elliKey.did()).toBeDefined()\n  })\n\n  it('rotates keys for users', async () => {\n    await scripts['rotate-keys'](network.pds.ctx, [elli])\n    const elliKey = await ctx.actorStore.keypair(elli)\n\n    const plcData = await ctx.plcClient.getDocumentData(elli)\n    expect(plcData.verificationMethods['atproto']).toEqual(elliKey.did())\n\n    // it correctly resigned elli's repo\n    const elliCar = await getCar(elli)\n    await verifyRepoCar(elliCar, elli, elliKey.did())\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/seeds/basic.ts",
    "content": "import { EXAMPLE_LABELER, SeedClient, TestBsky } from '@atproto/dev-env'\nimport { ids } from '../../src/lexicon/lexicons'\nimport usersSeed from './users'\n\nexport default async (\n  sc: SeedClient,\n  opts?: { inviteCode?: string; addModLabels?: TestBsky },\n) => {\n  await usersSeed(sc, opts)\n\n  const alice = sc.dids.alice\n  const bob = sc.dids.bob\n  const carol = sc.dids.carol\n  const dan = sc.dids.dan\n  const createdAtMicroseconds = () => ({\n    createdAt: new Date().toISOString().replace('Z', '000Z'), // microseconds\n  })\n  const createdAtTimezone = () => ({\n    createdAt: new Date().toISOString().replace('Z', '+00:00'), // iso timezone format\n  })\n\n  await sc.follow(alice, bob)\n  await sc.follow(alice, carol)\n  await sc.follow(alice, dan)\n  await sc.follow(carol, alice)\n  await sc.follow(bob, alice)\n  await sc.follow(bob, carol, createdAtMicroseconds())\n  await sc.follow(dan, bob, createdAtTimezone())\n  await sc.post(alice, posts.alice[0], undefined, undefined, undefined, {\n    labels: {\n      $type: 'com.atproto.label.defs#selfLabels',\n      values: [{ val: 'self-label' }],\n    },\n  })\n  await sc.post(bob, posts.bob[0], undefined, undefined, undefined, {\n    langs: ['en-US', 'i-klingon'],\n  })\n  const img1 = await sc.uploadFile(\n    carol,\n    '../dev-env/assets/key-landscape-small.jpg',\n    'image/jpeg',\n  )\n  const img2 = await sc.uploadFile(\n    carol,\n    '../dev-env/assets/key-alt.jpg',\n    'image/jpeg',\n  )\n  await sc.post(\n    carol,\n    posts.carol[0],\n    undefined,\n    [img1, img2], // Contains both images and a quote\n    sc.posts[bob][0].ref,\n  )\n  await sc.post(dan, posts.dan[0])\n  await sc.post(\n    dan,\n    posts.dan[1],\n    [\n      {\n        index: { byteStart: 0, byteEnd: 18 },\n        features: [\n          {\n            $type: `${ids.AppBskyRichtextFacet}#mention` as const,\n            did: alice,\n          },\n        ],\n      },\n    ],\n    undefined,\n    sc.posts[carol][0].ref, // This post contains an images embed\n  )\n  await sc.post(\n    alice,\n    posts.alice[1],\n    undefined,\n    undefined,\n    undefined,\n    createdAtMicroseconds(),\n  )\n  await sc.post(\n    bob,\n    posts.bob[1],\n    undefined,\n    undefined,\n    undefined,\n    createdAtTimezone(),\n  )\n  await sc.post(\n    alice,\n    posts.alice[2],\n    undefined,\n    undefined,\n    sc.posts[dan][1].ref, // This post contains a record embed which contains an images embed\n  )\n  await sc.like(bob, sc.posts[alice][1].ref)\n  await sc.like(bob, sc.posts[alice][2].ref)\n  await sc.like(carol, sc.posts[alice][1].ref)\n  await sc.like(carol, sc.posts[alice][2].ref)\n  await sc.like(dan, sc.posts[alice][1].ref)\n  await sc.like(alice, sc.posts[carol][0].ref, createdAtMicroseconds())\n  await sc.like(bob, sc.posts[carol][0].ref, createdAtTimezone())\n\n  const replyImg = await sc.uploadFile(\n    bob,\n    '../dev-env/assets/key-landscape-small.jpg',\n    'image/jpeg',\n  )\n  // must ensure ordering of replies in indexing\n  await sc.network.processAll()\n  await sc.reply(\n    bob,\n    sc.posts[alice][1].ref,\n    sc.posts[alice][1].ref,\n    replies.bob[0],\n    undefined,\n    [replyImg],\n  )\n  await sc.reply(\n    carol,\n    sc.posts[alice][1].ref,\n    sc.posts[alice][1].ref,\n    replies.carol[0],\n  )\n  await sc.network.processAll()\n  const alicesReplyToBob = await sc.reply(\n    alice,\n    sc.posts[alice][1].ref,\n    sc.replies[bob][0].ref,\n    replies.alice[0],\n  )\n  await sc.repost(carol, sc.posts[dan][1].ref)\n  await sc.repost(dan, sc.posts[alice][1].ref)\n  await sc.repost(dan, alicesReplyToBob.ref)\n\n  if (opts?.addModLabels) {\n    await createLabel(opts.addModLabels, { did: dan, val: 'repo-action-label' })\n  }\n\n  return sc\n}\n\nexport const posts = {\n  alice: ['hey there', 'again', 'yoohoo label_me'],\n  bob: ['bob back at it again!', 'bobby boy here', 'yoohoo'],\n  carol: ['hi im carol'],\n  dan: ['dan here!', '@alice.bluesky.xyz is the best'],\n}\n\nexport const replies = {\n  alice: ['thanks bob'],\n  bob: ['hear that label_me label_me_2'],\n  carol: ['of course'],\n}\n\nconst createLabel = async (\n  bsky: TestBsky,\n  opts: { did: string; val: string },\n) => {\n  await bsky.db.db\n    .insertInto('label')\n    .values({\n      uri: opts.did,\n      cid: '',\n      val: opts.val,\n      cts: new Date().toISOString(),\n      neg: false,\n      src: EXAMPLE_LABELER,\n    })\n    .execute()\n}\n"
  },
  {
    "path": "packages/pds/tests/seeds/follows.ts",
    "content": "import { SeedClient } from '@atproto/dev-env'\n\nexport default async (sc: SeedClient) => {\n  await sc.createAccount('alice', users.alice)\n  await sc.createAccount('bob', users.bob)\n  await sc.createAccount('carol', users.carol)\n  await sc.createAccount('dan', users.dan)\n  await sc.createAccount('eve', users.eve)\n  for (const name in sc.dids) {\n    await sc.createProfile(sc.dids[name], `display-${name}`, `descript-${name}`)\n  }\n  const alice = sc.dids.alice\n  const bob = sc.dids.bob\n  const carol = sc.dids.carol\n  const dan = sc.dids.dan\n  const eve = sc.dids.eve\n  await sc.follow(alice, bob)\n  await sc.follow(alice, carol)\n  await sc.follow(alice, dan)\n  await sc.follow(alice, eve)\n  await sc.follow(carol, alice)\n  await sc.follow(bob, alice)\n  await sc.follow(bob, carol)\n  await sc.follow(dan, alice)\n  await sc.follow(dan, bob)\n  await sc.follow(dan, eve)\n  await sc.follow(eve, alice)\n  await sc.follow(eve, carol)\n}\n\nconst users = {\n  alice: {\n    email: 'alice@test.com',\n    handle: 'alice.test',\n    password: 'alice-pass',\n  },\n  bob: {\n    email: 'bob@test.com',\n    handle: 'bob.test',\n    password: 'bob-pass',\n  },\n  carol: {\n    email: 'carol@test.com',\n    handle: 'carol.test',\n    password: 'carol-pass',\n  },\n  dan: {\n    email: 'dan@test.com',\n    handle: 'dan.test',\n    password: 'dan-pass',\n  },\n  eve: {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  },\n}\n"
  },
  {
    "path": "packages/pds/tests/seeds/likes.ts",
    "content": "import { SeedClient } from '@atproto/dev-env'\nimport basicSeed from './basic'\n\nexport default async (sc: SeedClient) => {\n  await basicSeed(sc)\n  await sc.createAccount('eve', {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  })\n  await sc.like(sc.dids.eve, sc.posts[sc.dids.alice][1].ref)\n  await sc.like(sc.dids.carol, sc.replies[sc.dids.bob][0].ref)\n  return sc\n}\n"
  },
  {
    "path": "packages/pds/tests/seeds/reposts.ts",
    "content": "import { SeedClient } from '@atproto/dev-env'\nimport basicSeed from './basic'\n\nexport default async (sc: SeedClient) => {\n  await basicSeed(sc)\n  await sc.createAccount('eve', {\n    email: 'eve@test.com',\n    handle: 'eve.test',\n    password: 'eve-pass',\n  })\n  await sc.repost(sc.dids.bob, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.carol, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.dan, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.eve, sc.posts[sc.dids.alice][2].ref)\n  await sc.repost(sc.dids.dan, sc.replies[sc.dids.bob][0].ref)\n  await sc.repost(sc.dids.eve, sc.replies[sc.dids.bob][0].ref)\n  return sc\n}\n"
  },
  {
    "path": "packages/pds/tests/seeds/thread.ts",
    "content": "import { RecordRef, SeedClient } from '@atproto/dev-env'\n\nexport default async (sc: SeedClient, did, threads: Item[]) => {\n  const refByItemId: Record<string, RecordRef> = {}\n  const rootByItemId: Record<string, RecordRef> = {}\n  await walk(threads, async (item, _depth, parent) => {\n    if (parent !== undefined) {\n      const parentRef = refByItemId[parent.id]\n      const rootRef = rootByItemId[parent.id]\n      const { ref } = await sc.reply(did, rootRef, parentRef, String(item.id))\n      refByItemId[item.id] = ref\n      rootByItemId[item.id] = rootRef\n    } else {\n      const { ref } = await sc.post(did, String(item.id))\n      refByItemId[item.id] = ref\n      rootByItemId[item.id] = ref\n    }\n  })\n}\n\nexport function item(id: number, children: Item[] = []) {\n  return { id, children }\n}\n\nexport async function walk(\n  items: Item[],\n  cb: (item: Item, depth: number, parent?: Item) => Promise<void>,\n  depth = 0,\n  parent?: Item,\n) {\n  for (const item of items) {\n    await cb(item, depth, parent)\n    await walk(item.children, cb, depth + 1, item)\n  }\n}\n\nexport interface Item {\n  id: number\n  children: Item[]\n}\n"
  },
  {
    "path": "packages/pds/tests/seeds/users-bulk.ts",
    "content": "import { chunkArray } from '@atproto/common'\nimport { SeedClient } from '@atproto/dev-env'\n\nexport default async (sc: SeedClient, max = Infinity) => {\n  // @TODO when these are run in parallel, seem to get an intermittent\n  // \"TypeError: fetch failed\" while running the tests.\n  const userSubset = users.slice(0, Math.min(max, users.length))\n  const chunks = chunkArray(userSubset, 50)\n  for (const chunk of chunks) {\n    await Promise.all(\n      chunk.map(async (user) => {\n        const { handle, displayName } = user\n        await sc.createAccount(handle, {\n          handle: handle,\n          password: 'password',\n          email: `${handle}@bsky.app`,\n        })\n        if (displayName !== null) {\n          await sc.createProfile(sc.dids[handle], displayName, '')\n        }\n      }),\n    )\n  }\n  return sc\n}\n\n// export default async (sc: SeedClient, max = Infinity) => {\n//   // @TODO when these are run in parallel, seem to get an intermittent\n//   // \"TypeError: fetch failed\" while running the tests.\n//   for (let i = 0; i < Math.min(max, users.length); ++i) {\n//     const { handle, displayName } = users[i]\n//     await sc.createAccount(handle, {\n//       handle: handle,\n//       password: 'password',\n//       email: `${handle}@bsky.app`,\n//     })\n//     if (displayName !== null) {\n//       await sc.createProfile(sc.dids[handle], displayName, '')\n//     }\n//   }\n//   return sc\n// }\n\nconst users = [\n  { handle: 'silas77.test', displayName: 'Tanya Denesik' },\n  { handle: 'nicolas-krajcik10.test', displayName: null },\n  { handle: 'lennie-strosin.test', displayName: null },\n  { handle: 'aliya-hodkiewicz.test', displayName: 'Carlton Abernathy IV' },\n  { handle: 'jeffrey-sawayn87.test', displayName: 'Patrick Sawayn' },\n  { handle: 'kaycee66.test', displayName: null },\n  { handle: 'adrienne49.test', displayName: 'Kim Streich' },\n  { handle: 'magnus53.test', displayName: 'Sally Funk' },\n  { handle: 'charles-spencer.test', displayName: null },\n  { handle: 'elta48.test', displayName: 'Dr. Lowell DuBuque' },\n  { handle: 'tressa-senger.test', displayName: null },\n  { handle: 'marietta-zboncak.test', displayName: null },\n  { handle: 'alexander-hickle.test', displayName: 'Winifred Harber' },\n  { handle: 'rodger-maggio24.test', displayName: 'Yolanda VonRueden' },\n  { handle: 'janiya48.test', displayName: 'Miss Terrell Ziemann' },\n  { handle: 'cayla-marquardt39.test', displayName: 'Rachel Kshlerin' },\n  { handle: 'jonathan-green.test', displayName: 'Erica Mertz' },\n  { handle: 'brycen-smith.test', displayName: null },\n  { handle: 'leonel-koch43.test', displayName: 'Karl Bosco IV' },\n  { handle: 'fidel-rath.test', displayName: null },\n  { handle: 'raleigh-metz.test', displayName: null },\n  { handle: 'kim41.test', displayName: null },\n  { handle: 'roderick-dibbert.test', displayName: null },\n  { handle: 'alec-bergnaum.test', displayName: 'Cody Berge' },\n  { handle: 'sven70.test', displayName: null },\n  { handle: 'ola-oconnell.test', displayName: null },\n  { handle: 'chauncey-klein.test', displayName: 'Kelvin Klein' },\n  { handle: 'ariel-krajcik.test', displayName: null },\n  { handle: 'murphy35.test', displayName: 'Mrs. Clifford Mertz' },\n  { handle: 'joshuah-parker11.test', displayName: null },\n  { handle: 'dewitt-wunsch.test', displayName: null },\n  { handle: 'kelton-nitzsche43.test', displayName: null },\n  { handle: 'dock-mann91.test', displayName: 'Miss Danielle Weber' },\n  { handle: 'herman-gleichner95.test', displayName: 'Kelli Schinner III' },\n  { handle: 'gerda-marquardt.test', displayName: 'Myron Wolf' },\n  { handle: 'jamil-batz.test', displayName: null },\n  { handle: 'hilario84.test', displayName: null },\n  { handle: 'kayli-bode.test', displayName: 'Miss Floyd McClure' },\n  { handle: 'elouise28.test', displayName: 'Alberta Fay' },\n  { handle: 'leann49.test', displayName: null },\n  { handle: 'javon24.test', displayName: null },\n  { handle: 'polly-shanahan45.test', displayName: null },\n  { handle: 'rosamond38.test', displayName: 'Karl Goyette' },\n  { handle: 'fredrick-mueller.test', displayName: null },\n  { handle: 'reina-runte33.test', displayName: 'Pablo Schmidt' },\n  { handle: 'bianka33.test', displayName: null },\n  { handle: 'carlos6.test', displayName: null },\n  { handle: 'jermain-smith.test', displayName: 'Eileen Stroman' },\n  { handle: 'gina97.test', displayName: null },\n  { handle: 'kiera97.test', displayName: null },\n  { handle: 'savannah-botsford.test', displayName: 'Darnell Kuvalis' },\n  { handle: 'lilliana-waters.test', displayName: null },\n  { handle: 'hailey-stroman.test', displayName: 'Elsa Schaden' },\n  { handle: 'dortha-terry.test', displayName: 'Nicole Bradtke' },\n  { handle: 'hank-powlowski32.test', displayName: null },\n  { handle: 'ervin-daugherty.test', displayName: null },\n  { handle: 'nannie18.test', displayName: null },\n  { handle: 'gilberto-watsica65.test', displayName: 'Ms. Ida Wilderman' },\n  { handle: 'kara-zieme58.test', displayName: 'Andres Towne' },\n  { handle: 'crystal-boyle.test', displayName: null },\n  { handle: 'tobin63.test', displayName: 'Alex Johnson' },\n  { handle: 'isai-kunze72.test', displayName: 'Marion Dickinson' },\n  { handle: 'paris-swift.test', displayName: null },\n  { handle: 'nestor90.test', displayName: 'Travis Hoppe' },\n  { handle: 'aliyah-flatley12.test', displayName: 'Loren Krajcik' },\n  { handle: 'maiya42.test', displayName: null },\n  { handle: 'dovie33.test', displayName: null },\n  { handle: 'kendra-ledner80.test', displayName: 'Sergio Hane' },\n  { handle: 'greyson-tromp3.test', displayName: null },\n  { handle: 'precious-fay.test', displayName: null },\n  { handle: 'kiana-schmitt39.test', displayName: null },\n  { handle: 'rhianna-stamm29.test', displayName: null },\n  { handle: 'tiara-mohr.test', displayName: null },\n  { handle: 'eleazar-balist70.test', displayName: 'Gordon Weissnat' },\n  { handle: 'bettie-bogisich96.test', displayName: null },\n  { handle: 'lura-jacobi55.test', displayName: null },\n  { handle: 'santa-hermann78.test', displayName: 'Melissa Johnson' },\n  { handle: 'dylan61.test', displayName: null },\n  { handle: 'ryley-kerluke.test', displayName: 'Alexander Purdy' },\n  { handle: 'moises-bins8.test', displayName: null },\n  { handle: 'angelita-schaef27.test', displayName: null },\n  { handle: 'natasha83.test', displayName: 'Dean Romaguera' },\n  { handle: 'sydni48.test', displayName: null },\n  { handle: 'darrion91.test', displayName: 'Jeanette Weimann' },\n  { handle: 'reynold-ortiz.test', displayName: null },\n  { handle: 'hassie-schuppe.test', displayName: 'Rita Zieme' },\n  { handle: 'clark-stehr8.test', displayName: 'Sammy Larkin' },\n  { handle: 'preston-harris.test', displayName: 'Ms. Bradford Thiel' },\n  { handle: 'benedict-schulist.test', displayName: 'Todd Stark' },\n  { handle: 'alden-wolff22.test', displayName: null },\n  { handle: 'joel-gulgowski.test', displayName: null },\n  { handle: 'joanie56.test', displayName: 'Ms. Darin Cole' },\n  { handle: 'israel-hermann0.test', displayName: 'Wilbur Schuster' },\n  { handle: 'tracy56.test', displayName: null },\n  { handle: 'kyle72.test', displayName: null },\n  { handle: 'gunnar-dare70.test', displayName: 'Mrs. Angelo Keeling' },\n  { handle: 'justus58.test', displayName: null },\n  { handle: 'brooke24.test', displayName: 'Clint Ward' },\n  { handle: 'angela-morissette.test', displayName: 'Jim Kertzmann' },\n  { handle: 'amy-bins.test', displayName: 'Angelina Hills' },\n  { handle: 'susanna81.test', displayName: null },\n  { handle: 'jailyn-hettinger50.test', displayName: 'Sheldon Ratke' },\n  { handle: 'wendell-hansen54.test', displayName: null },\n  { handle: 'jennyfer-spinka.test', displayName: 'Leticia Blick' },\n  { handle: 'alexandrea31.test', displayName: 'Leslie Von' },\n  { handle: 'hazle-davis.test', displayName: 'Ella Farrell' },\n  { handle: 'alta6.test', displayName: null },\n  { handle: 'sherwood4.test', displayName: 'Dr. Hattie Nienow I' },\n  { handle: 'marilie24.test', displayName: 'Gene Howell' },\n  { handle: 'jimmie-feeney82.test', displayName: null },\n  { handle: 'trisha-ohara.test', displayName: null },\n  { handle: 'jake-schuster33.test', displayName: 'Raymond Price' },\n  { handle: 'shane-torphy52.test', displayName: 'Sadie Carter' },\n  { handle: 'nakia-kuphal8.test', displayName: null },\n  { handle: 'lea-trantow.test', displayName: null },\n  { handle: 'joel62.test', displayName: 'Veronica Nitzsche' },\n  { handle: 'roosevelt33.test', displayName: 'Jay Moen' },\n  { handle: 'talon-okeefe85.test', displayName: null },\n  { handle: 'herman-dare.test', displayName: 'Eric White' },\n  { handle: 'flavio-fay.test', displayName: 'John Lindgren' },\n  { handle: 'elyse-prosacco.test', displayName: null },\n  { handle: 'jessyca-wiegand23.test', displayName: 'Debra Lockman' },\n  { handle: 'ara-spencer41.test', displayName: null },\n  { handle: 'frederic-fadel.test', displayName: null },\n  { handle: 'zora-gerlach.test', displayName: 'Noel Hansen' },\n  { handle: 'spencer4.test', displayName: 'Marjorie Gorczany' },\n  { handle: 'gage-wilkinson33.test', displayName: 'Preston Schoen V' },\n  { handle: 'kiley-runolfsson1.test', displayName: null },\n  { handle: 'ramona80.test', displayName: 'Sylvia Dietrich' },\n  { handle: 'rashad97.test', displayName: null },\n  { handle: 'kylie76.test', displayName: 'Josefina Pfeffer' },\n  { handle: 'alisha-zieme.test', displayName: null },\n  { handle: 'claud79.test', displayName: null },\n  { handle: 'jairo-kuvalis.test', displayName: 'Derrick Jacobson' },\n  { handle: 'delfina-emard.test', displayName: null },\n  { handle: 'waino-gutmann20.test', displayName: 'Wesley Kemmer' },\n  { handle: 'arvid-hermiston49.test', displayName: 'Vernon Towne PhD' },\n  { handle: 'hans79.test', displayName: 'Rex Hartmann' },\n  { handle: 'karlee-greenholt40.test', displayName: null },\n  { handle: 'nels-cummings.test', displayName: null },\n  { handle: 'andrew-maggio.test', displayName: null },\n  { handle: 'stephany75.test', displayName: null },\n  { handle: 'alba-lueilwitz.test', displayName: null },\n  { handle: 'fermin47.test', displayName: null },\n  { handle: 'milo-quitzon3.test', displayName: null },\n  { handle: 'eudora-dietrich4.test', displayName: 'Carol Littel' },\n  { handle: 'uriel-witting12.test', displayName: 'Sophia Schmidt' },\n  { handle: 'reuben-stracke48.test', displayName: 'Darrell Walker MD' },\n  { handle: 'letitia-sawayn11.test', displayName: 'Mrs. Sophie Reilly' },\n  { handle: 'macy-schaden.test', displayName: 'Lindsey Klein' },\n  { handle: 'imelda61.test', displayName: 'Shannon Beier' },\n  { handle: 'oswald-bailey.test', displayName: 'Angel Mann' },\n  { handle: 'pattie-fisher34.test', displayName: null },\n  { handle: 'loyce95.test', displayName: 'Claude Tromp' },\n  { handle: 'melyna-zboncak.test', displayName: null },\n  { handle: 'rowan-parisian.test', displayName: 'Mr. Veronica Feeney' },\n  { handle: 'lois-blanda20.test', displayName: 'Todd Rolfson' },\n  { handle: 'turner-bali76.test', displayName: null },\n  { handle: 'dee-hoppe65.test', displayName: null },\n  { handle: 'nikko-rosenbaum60.test', displayName: 'Joann Gutmann' },\n  { handle: 'cornell-rom53.test', displayName: null },\n  { handle: 'zack3.test', displayName: null },\n  { handle: 'fredrick41.test', displayName: 'Julius Kreiger' },\n  { handle: 'elwyn62.test', displayName: null },\n  { handle: 'isaias-hirthe37.test', displayName: 'Louis Cremin' },\n  { handle: 'ronaldo36.test', displayName: null },\n  { handle: 'jesse34.test', displayName: 'Bridget Schulist' },\n  { handle: 'darrel-mills17.test', displayName: null },\n  { handle: 'euna-mayert92.test', displayName: 'Grant Lang II' },\n  { handle: 'terrell92.test', displayName: null },\n  { handle: 'alyson-bogisich.test', displayName: 'Dana MacGyver' },\n  { handle: 'nicolas65.test', displayName: null },\n  { handle: 'bernita8.test', displayName: null },\n  { handle: 'gunner23.test', displayName: 'Maggie DuBuque' },\n  { handle: 'phoebe80.test', displayName: null },\n  { handle: 'cory-cruickshank.test', displayName: null },\n  { handle: 'conor-price.test', displayName: 'Ralph Daugherty III' },\n  { handle: 'rae91.test', displayName: null },\n  { handle: 'abigale-cronin.test', displayName: null },\n  { handle: 'aileen-reilly90.test', displayName: 'Charles Stanton' },\n  { handle: 'adrianna-hansen6.test', displayName: 'Elbert Langworth IV' },\n  { handle: 'pierre54.test', displayName: null },\n  { handle: 'jaida-stark62.test', displayName: 'Justin Stoltenberg MD' },\n  { handle: 'wade-witting.test', displayName: null },\n  { handle: 'yvonne-predovic5.test', displayName: 'Gregory Hamill' },\n  { handle: 'spencer-dubuque.test', displayName: null },\n  { handle: 'randi44.test', displayName: null },\n  { handle: 'maye-grimes.test', displayName: null },\n  { handle: 'margarette-effertz.test', displayName: null },\n  { handle: 'aimee98.test', displayName: null },\n  { handle: 'jaren-veum0.test', displayName: 'Dr. Omar Wolff' },\n  { handle: 'ariel-abbott54.test', displayName: 'Emanuel Powlowski' },\n  { handle: 'mercedes23.test', displayName: null },\n  { handle: 'jarrett-orn.test', displayName: null },\n  { handle: 'damion88.test', displayName: null },\n  { handle: 'nayeli-koss73.test', displayName: 'Johnny Lang' },\n  { handle: 'cara-wiegand69.test', displayName: null },\n  { handle: 'gideon-ohara51.test', displayName: null },\n  { handle: 'carolina-mcderm77.test', displayName: 'Latoya Windler' },\n  { handle: 'danyka90.test', displayName: 'Hope Kub' },\n]\n"
  },
  {
    "path": "packages/pds/tests/seeds/users.ts",
    "content": "import { SeedClient } from '@atproto/dev-env'\n\nexport default async (sc: SeedClient, opts?: { inviteCode?: string }) => {\n  await sc.createAccount('alice', {\n    ...users.alice,\n    inviteCode: opts?.inviteCode,\n  })\n  await sc.createAccount('bob', { ...users.bob, inviteCode: opts?.inviteCode })\n  await sc.createAccount('carol', {\n    ...users.carol,\n    inviteCode: opts?.inviteCode,\n  })\n  await sc.createAccount('dan', { ...users.dan, inviteCode: opts?.inviteCode })\n\n  await sc.createProfile(\n    sc.dids.alice,\n    users.alice.displayName,\n    users.alice.description,\n    users.alice.selfLabels,\n  )\n  await sc.createProfile(\n    sc.dids.bob,\n    users.bob.displayName,\n    users.bob.description,\n    users.bob.selfLabels,\n  )\n\n  return sc\n}\n\nexport const users = {\n  alice: {\n    email: 'alice@test.com',\n    handle: 'alice.test',\n    password: 'alice-pass',\n    displayName: 'ali',\n    description: 'its me!',\n    selfLabels: ['self-label-a', 'self-label-b'],\n  },\n  bob: {\n    email: 'bob@test.com',\n    handle: 'bob.test',\n    password: 'bob-pass',\n    displayName: 'bobby',\n    description: 'hi im bob label_me',\n    selfLabels: undefined,\n  },\n  carol: {\n    email: 'carol@test.com',\n    handle: 'carol.test',\n    password: 'carol-pass',\n    displayName: undefined,\n    description: undefined,\n    selfLabels: undefined,\n  },\n  dan: {\n    email: 'dan@test.com',\n    handle: 'dan.test',\n    password: 'dan-pass',\n    displayName: undefined,\n    description: undefined,\n    selfLabels: undefined,\n  },\n}\n"
  },
  {
    "path": "packages/pds/tests/sequencer.test.ts",
    "content": "import {\n  cborDecode,\n  cborEncode,\n  readFromGenerator,\n  wait,\n} from '@atproto/common'\nimport { randomStr } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { readCarWithRoot } from '@atproto/repo'\nimport { sequencer } from '../../pds'\nimport { SeqEvt, Sequencer, formatSeqSyncEvt } from '../src/sequencer'\nimport { Outbox } from '../src/sequencer/outbox'\nimport userSeed from './seeds/users'\n\ndescribe('sequencer', () => {\n  let network: TestNetworkNoAppView\n  let sequencer: Sequencer\n  let sc: SeedClient\n  let alice: string\n  let bob: string\n\n  let totalEvts\n  let lastSeen: number\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'sequencer',\n    })\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    sequencer = network.pds.ctx.sequencer\n    sc = network.getSeedClient()\n    await userSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    // 18 events in userSeed\n    totalEvts = 18\n  })\n\n  beforeEach(async () => {\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const randomPost = async (by: string) => sc.post(by, randomStr(8, 'base32'))\n  const createPosts = async (count: number): Promise<void> => {\n    const promises: Promise<unknown>[] = []\n    for (let i = 0; i < count; i++) {\n      if (i % 2 === 0) {\n        promises.push(randomPost(alice))\n      } else {\n        promises.push(randomPost(bob))\n      }\n      await Promise.all(promises)\n    }\n  }\n\n  const loadFromDb = (lastSeen: number) => {\n    return sequencer.db.db\n      .selectFrom('repo_seq')\n      .select([\n        'seq',\n        'did',\n        'eventType',\n        'event',\n        'invalidated',\n        'sequencedAt',\n      ])\n      .where('seq', '>', lastSeen)\n      .orderBy('seq', 'asc')\n      .execute()\n  }\n\n  const evtToDbRow = (e: SeqEvt) => {\n    const did = e.type === 'commit' ? e.evt.repo : e.evt.did\n    const eventType = e.type === 'commit' ? 'append' : e.type\n    return {\n      seq: e.seq,\n      did,\n      eventType,\n      event: Buffer.from(cborEncode(e.evt)),\n      invalidated: 0,\n      sequencedAt: e.time,\n    }\n  }\n\n  const caughtUp = (outbox: Outbox): (() => Promise<boolean>) => {\n    return async () => {\n      const lastEvt = await outbox.sequencer.curr()\n      if (lastEvt === null) return true\n      return outbox.lastSeen >= (lastEvt ?? 0)\n    }\n  }\n\n  it('sends to outbox', async () => {\n    const count = 20\n    totalEvts += count\n    await createPosts(count)\n    const outbox = new Outbox(sequencer)\n    const evts = await readFromGenerator(outbox.events(-1), caughtUp(outbox))\n    expect(evts.length).toBe(totalEvts)\n\n    const fromDb = await loadFromDb(-1)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('handles cut over', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer)\n    const createPromise = createPosts(count)\n    const [evts] = await Promise.all([\n      readFromGenerator(outbox.events(-1), caughtUp(outbox), createPromise),\n      createPromise,\n    ])\n    expect(evts.length).toBe(totalEvts)\n\n    const fromDb = await loadFromDb(-1)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('only gets events after cursor', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer)\n    const createPromise = createPosts(count)\n    const [evts] = await Promise.all([\n      readFromGenerator(\n        outbox.events(lastSeen),\n        caughtUp(outbox),\n        createPromise,\n      ),\n      createPromise,\n    ])\n\n    // +1 because we send the lastSeen date as well\n    expect(evts.length).toBe(count)\n\n    const fromDb = await loadFromDb(lastSeen)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('buffers events that are not being read', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer)\n    const createPromise = createPosts(count)\n    const gen = outbox.events(lastSeen)\n    // read enough to start streaming then wait so that the rest go into the buffer,\n    // then stream out from buffer\n    const [firstPart] = await Promise.all([\n      readFromGenerator(gen, caughtUp(outbox), createPromise, 5),\n      createPromise,\n    ])\n    const secondPart = await readFromGenerator(\n      gen,\n      caughtUp(outbox),\n      createPromise,\n    )\n    const evts = [...firstPart, ...secondPart]\n    expect(evts.length).toBe(count)\n\n    const fromDb = await loadFromDb(lastSeen)\n    expect(evts.map(evtToDbRow)).toEqual(fromDb)\n\n    lastSeen = evts.at(-1)?.seq ?? lastSeen\n  })\n\n  it('errors when buffer is overloaded', async () => {\n    const count = 20\n    totalEvts += count\n    const outbox = new Outbox(sequencer, { maxBufferSize: 5 })\n    const gen = outbox.events(lastSeen)\n    const createPromise = createPosts(count)\n    // read enough to start streaming then wait to stream rest until buffer is overloaded\n    const overloadBuffer = async () => {\n      await Promise.all([\n        readFromGenerator(gen, caughtUp(outbox), createPromise, 5),\n        createPromise,\n      ])\n      await wait(500)\n      await readFromGenerator(gen, caughtUp(outbox), createPromise)\n    }\n    await expect(overloadBuffer).rejects.toThrow('Stream consumer too slow')\n\n    await createPromise\n\n    const fromDb = await loadFromDb(lastSeen)\n    lastSeen = fromDb.at(-1)?.seq ?? lastSeen\n  })\n\n  it('handles many open connections', async () => {\n    const count = 20\n    const outboxes: Outbox[] = []\n    for (let i = 0; i < 50; i++) {\n      outboxes.push(new Outbox(sequencer))\n    }\n    const createPromise = createPosts(count)\n    const readOutboxes = Promise.all(\n      outboxes.map((o) =>\n        readFromGenerator(o.events(lastSeen), caughtUp(o), createPromise),\n      ),\n    )\n    const [results] = await Promise.all([readOutboxes, createPromise])\n    const fromDb = await loadFromDb(lastSeen)\n    for (let i = 0; i < 50; i++) {\n      const evts = results[i]\n      expect(evts.length).toBe(count)\n      expect(evts.map(evtToDbRow)).toEqual(fromDb)\n    }\n    lastSeen = results[0].at(-1)?.seq ?? lastSeen\n  })\n\n  it('root block must be returned in sync event', async () => {\n    const syncData = await network.pds.ctx.actorStore.read(\n      sc.dids.alice,\n      async (store) => {\n        const root = await store.repo.storage.getRootDetailed()\n        const { blocks } = await store.repo.storage.getBlocks([root.cid])\n        return {\n          cid: root.cid,\n          rev: root.rev,\n          blocks,\n        }\n      },\n    )\n\n    const dbEvt = await formatSeqSyncEvt(sc.dids.alice, syncData)\n    const evt = cborDecode<sequencer.SyncEvt>(dbEvt.event)\n    expect(evt.did).toBe(sc.dids.alice)\n    const car = await readCarWithRoot(evt.blocks)\n    expect(car.root.toString()).toBe(syncData.cid.toString())\n    // in the case of tooBig, the blocks must contain the root block only\n    expect(car.blocks.size).toBe(1)\n    expect(car.blocks.has(syncData.cid)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/server.test.ts",
    "content": "import { finished } from 'node:stream/promises'\nimport express from 'express'\nimport { request } from 'undici'\nimport { AtUri, AtpAgent } from '@atproto/api'\nimport { randomStr } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport { handler as errorHandler } from '../src/error'\nimport { startServer } from './_util'\nimport basicSeed from './seeds/basic'\n\ndescribe('server', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n  let alice: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'server',\n      pds: {\n        version: '0.0.0',\n      },\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('preserves 404s.', async () => {\n    const res = await fetch(`${network.pds.url}/unknown`)\n    expect(res.status).toEqual(404)\n  })\n\n  it('error handler turns unknown errors into 500s.', async () => {\n    const app = express()\n      .get('/oops', () => {\n        throw new Error('Oops!')\n      })\n      .use(errorHandler)\n\n    const { origin, stop } = await startServer(app)\n    try {\n      const res = await fetch(new URL(`/oops`, origin))\n      expect(res.status).toEqual(500)\n      await expect(res.json()).resolves.toEqual({\n        error: 'InternalServerError',\n        message: 'Internal Server Error',\n      })\n    } finally {\n      await stop()\n    }\n  })\n\n  it('limits size of json input.', async () => {\n    const res = await fetch(\n      `${network.pds.url}/xrpc/com.atproto.repo.createRecord`,\n      {\n        method: 'POST',\n        body: '\"' + 'x'.repeat(150 * 1024) + '\"', // ~150kb\n        headers: {\n          ...sc.getHeaders(alice),\n          'Content-Type': 'application/json',\n        },\n      },\n    )\n\n    expect(res.status).toEqual(413)\n    await expect(res.json()).resolves.toEqual({\n      error: 'PayloadTooLargeError',\n      message: 'request entity too large',\n    })\n  })\n\n  it('compresses large json responses', async () => {\n    // first create a large record\n    const record = {\n      text: 'blahblabh',\n      createdAt: new Date().toISOString(),\n    }\n    for (let i = 0; i < 100; i++) {\n      record[randomStr(8, 'base32')] = randomStr(32, 'base32')\n    }\n    const createRes = await agent.com.atproto.repo.createRecord(\n      {\n        repo: alice,\n        collection: 'app.bsky.feed.post',\n        record,\n      },\n      { headers: sc.getHeaders(alice), encoding: 'application/json' },\n    )\n    const uri = new AtUri(createRes.data.uri)\n\n    const res = await request(\n      `${network.pds.url}/xrpc/com.atproto.repo.getRecord?repo=${uri.host}&collection=${uri.collection}&rkey=${uri.rkey}`,\n      {\n        headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip' },\n      },\n    )\n\n    await finished(res.body.resume())\n\n    expect(res.headers['content-encoding']).toEqual('gzip')\n  })\n\n  it('compresses large car file responses', async () => {\n    const res = await request(\n      `${network.pds.url}/xrpc/com.atproto.sync.getRepo?did=${alice}`,\n      { headers: { 'accept-encoding': 'gzip' } },\n    )\n\n    await finished(res.body.resume())\n\n    expect(res.headers['content-encoding']).toEqual('gzip')\n  })\n\n  it('does not compress small payloads', async () => {\n    const res = await request(`${network.pds.url}/xrpc/_health`, {\n      headers: { 'accept-encoding': 'gzip' },\n    })\n\n    await finished(res.body.resume())\n\n    expect(res.headers['content-encoding']).toBeUndefined()\n  })\n\n  it('healthcheck succeeds when database is available.', async () => {\n    const res = await fetch(`${network.pds.url}/xrpc/_health`)\n    expect(res.status).toEqual(200)\n    await expect(res.json()).resolves.toEqual({ version: '0.0.0' })\n  })\n\n  // @TODO this is hanging for some unknown reason\n  it.skip('healthcheck fails when database is unavailable.', async () => {\n    await network.pds.ctx.accountManager.db.close()\n\n    const response = await fetch(`${network.pds.url}/xrpc/_health`)\n    expect(response.status).toEqual(503)\n    await expect(response.json()).resolves.toEqual({\n      version: 'unknown',\n      error: 'Service Unavailable',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/sync/invertible-ops.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtUri } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport * as repo from '@atproto/repo'\nimport { Subscription } from '@atproto/xrpc-server'\nimport {\n  OutputSchema as SubscribeReposOutput,\n  RepoOp,\n  isCommit,\n} from '../../src/lexicon/types/com/atproto/sync/subscribeRepos'\nimport basicSeed from '../seeds/basic'\n\ndescribe('invertible ops', () => {\n  let network: TestNetworkNoAppView\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'repo_invertible_ops',\n    })\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n\n    let posts: AtUri[] = []\n    for (let i = 0; i < 20; i++) {\n      const [aliceRef, bobRef, carolRef, danRef] = await Promise.all([\n        sc.post(sc.dids.alice, 'test'),\n        sc.post(sc.dids.bob, 'test'),\n        sc.post(sc.dids.carol, 'test'),\n        sc.post(sc.dids.dan, 'test'),\n      ])\n      posts = [\n        ...posts,\n        aliceRef.ref.uri,\n        bobRef.ref.uri,\n        carolRef.ref.uri,\n        danRef.ref.uri,\n      ]\n    }\n    for (const post of posts) {\n      await sc.deletePost(post.hostname, post)\n    }\n\n    await network.processAll()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('works', async () => {\n    const currSeq = (await network.pds.ctx.sequencer.curr()) ?? 0\n\n    const sub = new Subscription({\n      service: network.pds.url.replace('http://', 'ws://'),\n      method: 'com.atproto.sync.subscribeRepos',\n      validate: (value: unknown): SubscribeReposOutput => {\n        return value as any\n      },\n      getParams: () => {\n        return { cursor: 0 }\n      },\n    })\n\n    for await (const evt of sub) {\n      if (!isCommit(evt)) {\n        continue\n      }\n      const prevData = evt.prevData as CID | undefined\n      if (!prevData) {\n        continue\n      }\n\n      const { blocks, root } = await repo.readCarWithRoot(\n        evt.blocks as Uint8Array,\n      )\n      const storage = new repo.MemoryBlockstore(blocks)\n      const slice = await repo.Repo.load(storage, root)\n\n      let data = slice.data\n      const ops = evt.ops as RepoOp[]\n      for (const op of ops) {\n        if (op.action === 'create') {\n          data = await data.delete(op.path)\n        } else if (op.action === 'update') {\n          if (!op.prev) throw new Error('missing prev')\n          data = await data.update(op.path, op.prev)\n        } else if (op.action === 'delete') {\n          if (!op.prev) throw new Error('missing prev')\n          data = await data.add(op.path, op.prev)\n        } else {\n          throw new Error('unknown action')\n        }\n      }\n\n      const invertedRoot = await data.getPointer()\n      expect(invertedRoot.equals(prevData)).toBe(true)\n\n      if (evt.seq >= currSeq) {\n        break\n      }\n    }\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/sync/list.test.ts",
    "content": "import { AtpAgent } from '@atproto/api'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport basicSeed from '../seeds/basic'\n\ndescribe('sync listing', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'sync_list',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('lists hosted repos in order of creation', async () => {\n    const res = await agent.api.com.atproto.sync.listRepos()\n    const dids = res.data.repos.map((repo) => repo.did)\n    expect(dids).toEqual([\n      sc.dids.alice,\n      sc.dids.bob,\n      sc.dids.carol,\n      sc.dids.dan,\n    ])\n    expect(res.data.repos.every((r) => r.active === true)).toBe(true)\n  })\n\n  it('paginates listed hosted repos', async () => {\n    const full = await agent.api.com.atproto.sync.listRepos()\n    const pt1 = await agent.api.com.atproto.sync.listRepos({ limit: 2 })\n    const pt2 = await agent.api.com.atproto.sync.listRepos({\n      cursor: pt1.data.cursor,\n    })\n    expect([...pt1.data.repos, ...pt2.data.repos]).toEqual(full.data.repos)\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/sync/subscribe-repos.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { WebSocket } from 'ws'\nimport { AtpAgent } from '@atproto/api'\nimport {\n  HOUR,\n  MINUTE,\n  cborDecode,\n  readFromGenerator,\n  wait,\n} from '@atproto/common'\nimport { randomStr } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport * as repo from '@atproto/repo'\nimport { readCar } from '@atproto/repo'\nimport { ErrorFrame, Frame, MessageFrame, byFrame } from '@atproto/xrpc-server'\nimport { AppContext } from '../../src'\nimport { AccountStatus } from '../../src/account-manager/account-manager'\nimport {\n  Account as AccountEvt,\n  Commit as CommitEvt,\n  Identity as IdentityEvt,\n  Sync as SyncEvt,\n} from '../../src/lexicon/types/com/atproto/sync/subscribeRepos'\nimport basicSeed from '../seeds/basic'\n\ndescribe('repo subscribe repos', () => {\n  let serverHost: string\n\n  let network: TestNetworkNoAppView\n  let ctx: AppContext\n\n  let agent: AtpAgent\n  let sc: SeedClient\n  let alice: string\n  let bob: string\n  let carol: string\n  let dan: string\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'repo_subscribe_repos',\n      pds: {\n        repoBackfillLimitMs: HOUR,\n      },\n    })\n    serverHost = network.pds.url.replace('http://', '')\n    // @ts-expect-error Error due to circular dependency with the dev-env package\n    ctx = network.pds.ctx\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await basicSeed(sc)\n    alice = sc.dids.alice\n    bob = sc.dids.bob\n    carol = sc.dids.carol\n    dan = sc.dids.dan\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const getRepo = async (did: string): Promise<repo.VerifiedRepo> => {\n    const carRes = await agent.api.com.atproto.sync.getRepo({ did })\n    const car = await repo.readCarWithRoot(carRes.data)\n    const signingKey = await network.pds.ctx.actorStore.keypair(did)\n    return repo.verifyRepo(car.blocks, car.root, did, signingKey.did())\n  }\n\n  const getAllEvents = (userDid: string, frames: Frame[]) => {\n    const types: unknown[] = []\n    for (const frame of frames) {\n      if (frame instanceof MessageFrame) {\n        if (\n          (frame.header.t === '#commit' &&\n            (frame.body as CommitEvt).repo === userDid) ||\n          (frame.header.t === '#sync' &&\n            (frame.body as SyncEvt).did === userDid) ||\n          (frame.header.t === '#identity' &&\n            (frame.body as IdentityEvt).did === userDid) ||\n          (frame.header.t === '#account' &&\n            (frame.body as AccountEvt).did === userDid)\n        ) {\n          types.push(frame.body)\n        }\n      }\n    }\n    return types\n  }\n\n  const getEventType = <T>(frames: Frame[], type: string): T[] => {\n    const evts: T[] = []\n    for (const frame of frames) {\n      if (frame instanceof MessageFrame && frame.header.t === type) {\n        evts.push(frame.body)\n      }\n    }\n    return evts\n  }\n\n  const getSyncEvts = (frames: Frame[]): SyncEvt[] => {\n    return getEventType(frames, '#sync')\n  }\n\n  const getAccountEvts = (frames: Frame[]): AccountEvt[] => {\n    return getEventType(frames, '#account')\n  }\n\n  const getIdentityEvts = (frames: Frame[]): IdentityEvt[] => {\n    return getEventType(frames, '#identity')\n  }\n\n  const getCommitEvents = (frames: Frame[]): CommitEvt[] => {\n    return getEventType(frames, '#commit')\n  }\n\n  const verifyIdentityEvent = (\n    evt: IdentityEvt,\n    did: string,\n    handle?: string,\n  ) => {\n    expect(typeof evt.seq).toBe('number')\n    expect(evt.did).toBe(did)\n    expect(typeof evt.time).toBe('string')\n    expect(evt.handle).toEqual(handle)\n  }\n\n  const verifyAccountEvent = (\n    evt: AccountEvt,\n    did: string,\n    active: boolean,\n    status?: AccountStatus,\n  ) => {\n    expect(typeof evt.seq).toBe('number')\n    expect(evt.did).toBe(did)\n    expect(typeof evt.time).toBe('string')\n    expect(evt.active).toBe(active)\n    expect(evt.status).toBe(status)\n  }\n\n  const verifySyncEvent = async (\n    evt: SyncEvt,\n    did: string,\n    commit: CID,\n    rev: string,\n  ) => {\n    expect(typeof evt.seq).toBe('number')\n    expect(evt.did).toBe(did)\n    expect(typeof evt.time).toBe('string')\n    expect(evt.rev).toBe(rev)\n    const car = await repo.readCarWithRoot(evt.blocks)\n    expect(car.root.equals(commit)).toBe(true)\n    expect(car.blocks.size).toBe(1)\n    expect(car.blocks.has(car.root)).toBe(true)\n  }\n\n  const verifyCommitEvents = async (frames: Frame[]) => {\n    const forUser = (user: string) => (commit: CommitEvt) =>\n      commit.repo === user\n    const commits = getCommitEvents(frames)\n    await verifyRepo(alice, commits.filter(forUser(alice)))\n    await verifyRepo(bob, commits.filter(forUser(bob)))\n    await verifyRepo(carol, commits.filter(forUser(carol)))\n    await verifyRepo(dan, commits.filter(forUser(dan)))\n  }\n\n  const verifyRepo = async (did: string, evts: CommitEvt[]) => {\n    const fromRpc = await getRepo(did)\n    const contents = {} as Record<string, Record<string, CID>>\n    const allBlocks = new repo.BlockMap()\n    for (const evt of evts) {\n      const car = await readCar(evt.blocks)\n      allBlocks.addMap(car.blocks)\n      for (const op of evt.ops) {\n        const { collection, rkey } = repo.parseDataKey(op.path)\n        if (op.action === 'delete') {\n          delete contents[collection][rkey]\n        } else {\n          if (op.cid) {\n            contents[collection] ??= {}\n            contents[collection][rkey] ??= op.cid\n          }\n        }\n      }\n    }\n    for (const write of fromRpc.creates) {\n      expect(contents[write.collection][write.rkey].equals(write.cid)).toBe(\n        true,\n      )\n    }\n    const lastCommit = evts.at(-1)?.commit\n    if (!lastCommit) {\n      throw new Error('no last commit')\n    }\n    const signingKey = await network.pds.ctx.actorStore.keypair(did)\n    const fromStream = await repo.verifyRepo(\n      allBlocks,\n      lastCommit,\n      did,\n      signingKey.did(),\n    )\n    const fromRpcOps = fromRpc.creates\n    const fromStreamOps = fromStream.creates\n    expect(fromStreamOps.length).toEqual(fromRpcOps.length)\n    for (let i = 0; i < fromRpcOps.length; i++) {\n      expect(fromStreamOps[i].collection).toEqual(fromRpcOps[i].collection)\n      expect(fromStreamOps[i].rkey).toEqual(fromRpcOps[i].rkey)\n      expect(fromStreamOps[i].cid).toEqual(fromRpcOps[i].cid)\n    }\n  }\n\n  const randomPost = (by: string) => sc.post(by, randomStr(8, 'base32'))\n  const makePosts = async () => {\n    for (let i = 0; i < 10; i++) {\n      await Promise.all([\n        randomPost(alice),\n        randomPost(bob),\n        randomPost(carol),\n        randomPost(dan),\n      ])\n    }\n  }\n\n  const readTillCaughtUp = async <T>(\n    gen: AsyncGenerator<T>,\n    waitFor?: Promise<unknown>,\n  ) => {\n    const isDone = async (evt: any) => {\n      if (evt === undefined) return false\n      if (evt instanceof ErrorFrame) return true\n      const curr = await ctx.sequencer.curr()\n      return evt.body.seq === curr\n    }\n\n    return readFromGenerator(gen, isDone, waitFor)\n  }\n\n  it('emits sync event on account creation, matching temporary commit event.', async () => {\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    const syncEvts = getSyncEvts(evts)\n    const commitEvts = getCommitEvents(evts).slice(0, 4)\n    expect(syncEvts.length).toBe(4)\n\n    let i = 0\n    for (const did of [alice, bob, carol, dan]) {\n      const syncEvt = syncEvts[i]\n      const commitEvt = commitEvts[i]\n      await verifySyncEvent(syncEvt, did, commitEvt.commit, commitEvt.rev)\n      i++\n    }\n  })\n\n  it('sync backfilled events', async () => {\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    await verifyCommitEvents(evts)\n\n    const accountEvts = getAccountEvts(evts)\n    expect(accountEvts.length).toBe(4)\n    verifyAccountEvent(accountEvts[0], alice, true)\n    verifyAccountEvent(accountEvts[1], bob, true)\n    verifyAccountEvent(accountEvts[2], carol, true)\n    verifyAccountEvent(accountEvts[3], dan, true)\n    const identityEvts = getIdentityEvts(evts)\n    expect(identityEvts.length).toBe(4)\n    verifyIdentityEvent(identityEvts[0], alice, 'alice.test')\n    verifyIdentityEvent(identityEvts[1], bob, 'bob.test')\n    verifyIdentityEvent(identityEvts[2], carol, 'carol.test')\n    verifyIdentityEvent(identityEvts[3], dan, 'dan.test')\n  })\n\n  it('syncs new events', async () => {\n    const postPromise = makePosts()\n\n    const readAfterDelay = async () => {\n      await wait(200) // wait just a hair so that we catch it during cutover\n      const ws = new WebSocket(\n        `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n      )\n      const evts = await readTillCaughtUp(byFrame(ws), postPromise)\n      ws.terminate()\n      return evts\n    }\n\n    const [evts] = await Promise.all([readAfterDelay(), postPromise])\n\n    await verifyCommitEvents(evts)\n  })\n\n  it('handles no backfill', async () => {\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos`,\n    )\n\n    const makePostsAfterWait = async () => {\n      // give them just a second to get subscriptions set up\n      await wait(200)\n      await makePosts()\n    }\n\n    const postPromise = makePostsAfterWait()\n\n    const [evts] = await Promise.all([\n      readTillCaughtUp(byFrame(ws), postPromise),\n      postPromise,\n    ])\n\n    ws.terminate()\n\n    expect(evts.length).toBe(40)\n\n    await wait(100) // Let cleanup occur on server\n    expect(ctx.sequencer.listeners('events').length).toEqual(0)\n  })\n\n  it('backfills only from provided cursor', async () => {\n    const seqs = await ctx.sequencer.db.db\n      .selectFrom('repo_seq')\n      .selectAll()\n      .orderBy('seq', 'asc')\n      .execute()\n    const midPoint = Math.floor(seqs.length / 2)\n    const midPointSeq = seqs[midPoint].seq\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${midPointSeq}`,\n    )\n    const evts = await readTillCaughtUp(byFrame(ws))\n    ws.terminate()\n    const seqSlice = seqs.slice(midPoint + 1)\n    expect(evts.length).toBe(seqSlice.length)\n    for (let i = 0; i < evts.length; i++) {\n      const evt = evts[i].body as unknown as CommitEvt\n      const seq = seqSlice[i]\n      const seqEvt = cborDecode(seq.event) as { commit: CID }\n      expect(evt.time).toEqual(seq.sequencedAt)\n      expect(evt.commit.equals(seqEvt.commit)).toBeTruthy()\n      expect(evt.repo).toEqual(seq.did)\n    }\n  })\n\n  it('syncs handle changes (identity evts)', async () => {\n    await sc.updateHandle(alice, 'alice2.test')\n    await sc.updateHandle(bob, 'bob2.test')\n    await sc.updateHandle(bob, 'bob2.test') // idempotent update re-sends\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    await verifyCommitEvents(evts)\n\n    const identityEvts = getIdentityEvts(evts.slice(-3))\n    expect(identityEvts.length).toBe(3)\n    verifyIdentityEvent(identityEvts[0], alice, 'alice2.test')\n    verifyIdentityEvent(identityEvts[1], bob, 'bob2.test')\n    verifyIdentityEvent(identityEvts[2], bob, 'bob2.test')\n  })\n\n  it('resends identity events on idempotent updates', async () => {\n    const update = sc.updateHandle(bob, 'bob2.test')\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen, update)\n    ws.terminate()\n\n    const identityEvts = getIdentityEvts(evts.slice(-1))\n    verifyIdentityEvent(identityEvts[0], bob, 'bob2.test')\n  })\n\n  it('syncs account events', async () => {\n    // deactivate then reactivate alice\n    await agent.api.com.atproto.server.deactivateAccount(\n      {},\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    await agent.api.com.atproto.server.activateAccount(undefined, {\n      headers: sc.getHeaders(alice),\n    })\n\n    // takedown then restore bob\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: bob,\n        },\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: bob,\n        },\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    // @NOTE requires a larger slice because of over-emission on activateAccount - see note on route\n    const accountEvts = getAccountEvts(evts.slice(-6))\n    expect(accountEvts.length).toBe(4)\n    verifyAccountEvent(accountEvts[0], alice, false, AccountStatus.Deactivated)\n    verifyAccountEvent(accountEvts[1], alice, true)\n    verifyAccountEvent(accountEvts[2], bob, false, AccountStatus.Takendown)\n    verifyAccountEvent(accountEvts[3], bob, true)\n  })\n\n  it('syncs interleaved account events', async () => {\n    // deactivate -> takedown -> restore -> activate\n    // deactivate then reactivate alice\n    await agent.api.com.atproto.server.deactivateAccount(\n      {},\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: alice,\n        },\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    await agent.api.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: alice,\n        },\n        takedown: { applied: false },\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n    await agent.api.com.atproto.server.activateAccount(undefined, {\n      headers: sc.getHeaders(alice),\n    })\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    // @NOTE requires a larger slice because of over-emission on activateAccount - see note on route\n    const accountEvts = getAccountEvts(evts.slice(-6))\n    expect(accountEvts.length).toBe(4)\n    verifyAccountEvent(accountEvts[0], alice, false, AccountStatus.Deactivated)\n    verifyAccountEvent(accountEvts[1], alice, false, AccountStatus.Takendown)\n    verifyAccountEvent(accountEvts[2], alice, false, AccountStatus.Deactivated)\n    verifyAccountEvent(accountEvts[3], alice, true)\n  })\n\n  it('emits sync event on account activation', async () => {\n    await agent.api.com.atproto.server.deactivateAccount(\n      {},\n      {\n        encoding: 'application/json',\n        headers: sc.getHeaders(alice),\n      },\n    )\n    await agent.api.com.atproto.server.activateAccount(undefined, {\n      headers: sc.getHeaders(alice),\n    })\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    const syncEvts = getSyncEvts(evts.slice(-1))\n    expect(syncEvts.length).toBe(1)\n    const root = await ctx.actorStore.read(alice, (store) =>\n      store.repo.storage.getRootDetailed(),\n    )\n    await verifySyncEvent(syncEvts[0], alice, root.cid, root.rev)\n  })\n\n  it('syncs account deletions (account evt)', async () => {\n    const baddie1 = (\n      await sc.createAccount('baddie1.test', {\n        email: 'baddie1@test.com',\n        handle: 'baddie1.test',\n        password: 'baddie1-pass',\n      })\n    ).did\n    const baddie2 = (\n      await sc.createAccount('baddie2.test', {\n        email: 'baddie2@test.com',\n        handle: 'baddie2.test',\n        password: 'baddie2-pass',\n      })\n    ).did\n    const deleteToken = await ctx.accountManager.createEmailToken(\n      baddie1,\n      'delete_account',\n    )\n    await agent.api.com.atproto.server.deleteAccount({\n      did: baddie1,\n      password: 'baddie1-pass',\n      token: deleteToken,\n    })\n    await agent.api.com.atproto.admin.deleteAccount(\n      {\n        did: baddie2,\n      },\n      {\n        encoding: 'application/json',\n        headers: network.pds.adminAuthHeaders(),\n      },\n    )\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    const accountEvts = getAccountEvts(evts.slice(-2))\n    expect(accountEvts.length).toBe(2)\n    verifyAccountEvent(accountEvts[0], baddie1, false, AccountStatus.Deleted)\n    verifyAccountEvent(accountEvts[1], baddie2, false, AccountStatus.Deleted)\n  })\n\n  it('account deletions invalidate all seq ops', async () => {\n    const baddie3 = (\n      await sc.createAccount('baddie3', {\n        email: 'baddie3@test.com',\n        handle: 'baddie3.test',\n        password: 'baddie3-pass',\n      })\n    ).did\n\n    await randomPost(baddie3)\n    await sc.updateHandle(baddie3, 'baddie3-update.test')\n    const token = await network.pds.ctx.accountManager.createEmailToken(\n      baddie3,\n      'delete_account',\n    )\n    await agent.api.com.atproto.server.deleteAccount({\n      token,\n      did: baddie3,\n      password: sc.accounts[baddie3].password,\n    })\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n\n    const gen = byFrame(ws)\n    const evts = await readTillCaughtUp(gen)\n    ws.terminate()\n\n    const didEvts = getAllEvents(baddie3, evts)\n    expect(didEvts.length).toBe(1)\n    verifyAccountEvent(\n      didEvts[0] as AccountEvt,\n      baddie3,\n      false,\n      AccountStatus.Deleted,\n    )\n  })\n\n  it('sends info frame on out of date cursor', async () => {\n    // we rewrite the sequenceAt time for existing seqs to be past the backfill cutoff\n    // then we create some new posts\n    const overAnHourAgo = new Date(Date.now() - HOUR - MINUTE).toISOString()\n    await ctx.sequencer.db.db\n      .updateTable('repo_seq')\n      .set({ sequencedAt: overAnHourAgo })\n      .execute()\n\n    await makePosts()\n\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`,\n    )\n    const [info, ...evts] = await readTillCaughtUp(byFrame(ws))\n    ws.terminate()\n\n    if (!(info instanceof MessageFrame)) {\n      throw new Error('Expected first frame to be a MessageFrame')\n    }\n    expect(info.header.t).toBe('#info')\n    const body = info.body as Record<string, unknown>\n    expect(body.name).toEqual('OutdatedCursor')\n    expect(evts.length).toBe(40)\n  })\n\n  it('errors on future cursor', async () => {\n    const ws = new WebSocket(\n      `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${100000}`,\n    )\n    const frames = await readTillCaughtUp(byFrame(ws))\n    ws.terminate()\n    expect(frames.length).toBe(1)\n    if (!(frames[0] instanceof ErrorFrame)) {\n      throw new Error('Expected ErrorFrame')\n    }\n    expect(frames[0].body.error).toBe('FutureCursor')\n  })\n})\n"
  },
  {
    "path": "packages/pds/tests/sync/sync.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { AtpAgent } from '@atproto/api'\nimport { TID, cidForCbor } from '@atproto/common'\nimport { Keypair, randomStr } from '@atproto/crypto'\nimport { SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'\nimport * as repo from '@atproto/repo'\nimport { MemoryBlockstore } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\n\ndescribe('repo sync', () => {\n  let network: TestNetworkNoAppView\n  let agent: AtpAgent\n  let sc: SeedClient\n  let did: string\n  let signingKey: Keypair\n\n  const repoData: repo.RepoContents = {}\n  const uris: AtUri[] = []\n  const storage = new MemoryBlockstore()\n  let currRoot: CID | undefined\n  let currRev: string | undefined\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'repo_sync',\n    })\n    agent = network.pds.getClient()\n    sc = network.getSeedClient()\n    await sc.createAccount('alice', {\n      email: 'alice@test.com',\n      handle: 'alice.test',\n      password: 'alice-pass',\n    })\n    did = sc.dids.alice\n    signingKey = await network.pds.ctx.actorStore.keypair(did)\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('creates and syncs some records', async () => {\n    const ADD_COUNT = 10\n    for (let i = 0; i < ADD_COUNT; i++) {\n      const { obj, uri } = await makePost(sc, did)\n      if (!repoData[uri.collection]) {\n        repoData[uri.collection] = {}\n      }\n      repoData[uri.collection][uri.rkey] = obj\n      uris.push(uri)\n    }\n\n    const carRes = await agent.api.com.atproto.sync.getRepo({ did })\n    const car = await repo.readCarWithRoot(carRes.data)\n    const synced = await repo.verifyRepo(\n      car.blocks,\n      car.root,\n      did,\n      signingKey.did(),\n    )\n    await storage.applyCommit(synced.commit)\n    expect(synced.creates.length).toBe(ADD_COUNT)\n    const loaded = await repo.Repo.load(storage, car.root)\n    const contents = await loaded.getContents()\n    expect(contents).toEqual(repoData)\n\n    currRoot = car.root\n    currRev = loaded.commit.rev\n  })\n\n  it('syncs creates and deletes', async () => {\n    const ADD_COUNT = 10\n    const DEL_COUNT = 4\n    for (let i = 0; i < ADD_COUNT; i++) {\n      const { obj, uri } = await makePost(sc, did)\n      if (!repoData[uri.collection]) {\n        repoData[uri.collection] = {}\n      }\n      repoData[uri.collection][uri.rkey] = obj\n      uris.push(uri)\n    }\n    // delete two that are already sync & two that have not been\n    for (let i = 0; i < DEL_COUNT; i++) {\n      const uri = uris[i * 5]\n      await sc.deletePost(did, uri)\n      delete repoData[uri.collection][uri.rkey]\n    }\n\n    const carRes = await agent.api.com.atproto.sync.getRepo({ did })\n    const car = await repo.readCarWithRoot(carRes.data)\n    const currRepo = await repo.Repo.load(storage, currRoot)\n    const synced = await repo.verifyDiff(\n      currRepo,\n      car.blocks,\n      car.root,\n      did,\n      signingKey.did(),\n    )\n    expect(synced.writes.length).toBe(ADD_COUNT) // -2 because of dels of new records, +2 because of dels of old records\n    await storage.applyCommit(synced.commit)\n    const loaded = await repo.Repo.load(storage, car.root)\n    const contents = await loaded.getContents()\n    expect(contents).toEqual(repoData)\n\n    currRoot = car.root\n    currRev = loaded.commit.rev\n  })\n\n  it('syncs repo status', async () => {\n    const status = await agent.api.com.atproto.sync.getRepoStatus({ did })\n    expect(status.data).toEqual({\n      did,\n      active: true,\n      rev: currRev,\n    })\n  })\n\n  it('syncs latest repo commit', async () => {\n    const commit = await agent.api.com.atproto.sync.getLatestCommit({ did })\n    expect(commit.data.cid).toEqual(currRoot?.toString())\n  })\n\n  it('syncs `since` a given rev', async () => {\n    const repoBefore = await repo.Repo.load(storage, currRoot)\n\n    // add a post\n    const { obj, uri } = await makePost(sc, did)\n    if (!repoData[uri.collection]) {\n      repoData[uri.collection] = {}\n    }\n    repoData[uri.collection][uri.rkey] = obj\n    uris.push(uri)\n\n    const carRes = await agent.api.com.atproto.sync.getRepo({\n      did,\n      since: repoBefore.commit.rev,\n    })\n    const car = await repo.readCarWithRoot(carRes.data)\n    expect(car.blocks.size).toBeLessThan(10) // should only contain new blocks\n    const synced = await repo.verifyDiff(\n      repoBefore,\n      car.blocks,\n      car.root,\n      did,\n      signingKey.did(),\n    )\n    expect(synced.writes.length).toBe(1)\n    await storage.applyCommit(synced.commit)\n    const loaded = await repo.Repo.load(storage, car.root)\n    const contents = await loaded.getContents()\n    expect(contents).toEqual(repoData)\n\n    currRoot = car.root\n  })\n\n  it('sync a record proof', async () => {\n    const collection = Object.keys(repoData)[0]\n    const rkey = Object.keys(repoData[collection])[0]\n    const car = await agent.api.com.atproto.sync.getRecord({\n      did,\n      collection,\n      rkey,\n    })\n    const records = await repo.verifyRecords(\n      new Uint8Array(car.data),\n      did,\n      signingKey.did(),\n    )\n    const claim = {\n      collection,\n      rkey,\n      cid: await cidForCbor(repoData[collection][rkey]),\n    }\n    expect(records.length).toBe(1)\n    expect(await cidForCbor(records[0].record)).toEqual(claim.cid)\n    const result = await repo.verifyProofs(\n      new Uint8Array(car.data),\n      [claim],\n      did,\n      signingKey.did(),\n    )\n    expect(result.verified.length).toBe(1)\n    expect(result.unverified.length).toBe(0)\n  })\n\n  it('sync a proof of non-existence', async () => {\n    const collection = Object.keys(repoData)[0]\n    const rkey = TID.nextStr() // rkey that doesn't exist\n    const car = await agent.api.com.atproto.sync.getRecord({\n      did,\n      collection,\n      rkey,\n    })\n    const claim = {\n      collection,\n      rkey,\n      cid: null,\n    }\n    const result = await repo.verifyProofs(\n      new Uint8Array(car.data),\n      [claim],\n      did,\n      signingKey.did(),\n    )\n    expect(result.verified.length).toBe(1)\n    expect(result.unverified.length).toBe(0)\n  })\n\n  describe('repo takedown', () => {\n    beforeAll(async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did,\n          },\n          takedown: { applied: true },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n    })\n\n    afterAll(async () => {\n      await agent.api.com.atproto.admin.updateSubjectStatus(\n        {\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did,\n          },\n          takedown: { applied: false },\n        },\n        {\n          encoding: 'application/json',\n          headers: network.pds.adminAuthHeaders(),\n        },\n      )\n    })\n\n    it('returns takendown status', async () => {\n      const res = await agent.api.com.atproto.sync.getRepoStatus({ did })\n      expect(res.data).toEqual({\n        did,\n        active: false,\n        status: 'takendown',\n      })\n    })\n\n    it('lists as takendown in listRepos', async () => {\n      const res = await agent.api.com.atproto.sync.listRepos()\n      const found = res.data.repos.find((r) => r.did === did)\n      expect(found?.active).toBe(false)\n      expect(found?.status).toBe('takendown')\n    })\n\n    it('does not sync repo unauthed', async () => {\n      const tryGetRepo = agent.api.com.atproto.sync.getRepo({ did })\n      await expect(tryGetRepo).rejects.toThrow(/Repo has been takendown/)\n    })\n\n    it('syncs repo to owner or admin', async () => {\n      const tryGetRepoOwner = agent.api.com.atproto.sync.getRepo(\n        { did },\n        { headers: sc.getHeaders(did) },\n      )\n      await expect(tryGetRepoOwner).resolves.toBeDefined()\n      const tryGetRepoAdmin = agent.api.com.atproto.sync.getRepo(\n        { did },\n        { headers: network.pds.adminAuthHeaders() },\n      )\n      await expect(tryGetRepoAdmin).resolves.toBeDefined()\n    })\n\n    it('does not sync latest commit unauthed', async () => {\n      const tryGetLatest = agent.api.com.atproto.sync.getLatestCommit({ did })\n      await expect(tryGetLatest).rejects.toThrow(/Repo has been takendown/)\n    })\n\n    it('does not sync a record proof unauthed', async () => {\n      const collection = Object.keys(repoData)[0]\n      const rkey = Object.keys(repoData[collection])[0]\n      const tryGetRecord = agent.api.com.atproto.sync.getRecord({\n        did,\n        collection,\n        rkey,\n      })\n      await expect(tryGetRecord).rejects.toThrow(/Repo has been takendown/)\n    })\n  })\n})\n\nconst makePost = async (sc: SeedClient, did: string) => {\n  const res = await sc.post(did, randomStr(32, 'base32'))\n  const uri = res.ref.uri\n  const record = await sc.agent.api.com.atproto.repo.getRecord({\n    repo: did,\n    collection: uri.collection,\n    rkey: uri.rkey,\n  })\n  return {\n    uri,\n    obj: record.data.value,\n  }\n}\n"
  },
  {
    "path": "packages/pds/tests/takedown-appeal.test.ts",
    "content": "import { AtpAgent, ComAtprotoModerationDefs } from '@atproto/api'\nimport { SeedClient, TestNetwork } from '@atproto/dev-env'\nimport { ids } from '../src/lexicon/lexicons'\nimport { forSubjectStatusSnapshot } from './_util'\n\ndescribe('appeal account takedown', () => {\n  let network: TestNetwork\n  let agent: AtpAgent\n  let sc: SeedClient\n  let moderator: string\n\n  beforeAll(async () => {\n    network = await TestNetwork.create({\n      dbPostgresSchema: 'takedown_appeal',\n    })\n    sc = network.getSeedClient()\n    const modAccount = await sc.createAccount('moderator', {\n      handle: 'testmod.test',\n      email: 'testmod@test.com',\n      password: 'testmod-pass',\n    })\n    moderator = modAccount.did\n    await network.ozone.addModeratorDid(moderator)\n\n    agent = network.pds.getClient()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  it('actor takedown allows appeal request.', async () => {\n    const { data: account } = await agent.com.atproto.server.createAccount({\n      handle: 'jeff.test',\n      email: 'jeff@test.com',\n      password: 'password',\n    })\n    await network.processAll()\n\n    // Emit a takedown event\n    await network.ozone.getModClient().performTakedown({\n      subject: {\n        $type: 'com.atproto.admin.defs#repoRef',\n        did: account.did,\n      },\n    })\n\n    // Manually set the account as takendown at the PDS level\n    // since the takedown event only propagates when the daemon is running\n    await agent.com.atproto.admin.updateSubjectStatus(\n      {\n        subject: {\n          $type: 'com.atproto.admin.defs#repoRef',\n          did: account.did,\n        },\n        takedown: { applied: true },\n      },\n      {\n        encoding: 'application/json',\n        headers: { authorization: network.pds.adminAuth() },\n      },\n    )\n\n    await network.processAll()\n\n    // Verify user can not get session token without setting the optional param\n    await expect(\n      agent.com.atproto.server.createSession({\n        identifier: 'jeff.test',\n        password: 'password',\n      }),\n    ).rejects.toThrow('Account has been taken down')\n\n    const { data: auth } = await agent.com.atproto.server.createSession({\n      identifier: 'jeff.test',\n      password: 'password',\n      allowTakendown: true,\n    })\n\n    // send appeal event as the takendown account\n    await agent.com.atproto.moderation.createReport(\n      {\n        reasonType: ComAtprotoModerationDefs.REASONAPPEAL,\n        reason: 'I want my account back',\n        subject: { $type: 'com.atproto.admin.defs#repoRef', did: account.did },\n      },\n      {\n        headers: {\n          authorization: `Bearer ${auth.accessJwt}`,\n        },\n      },\n    )\n\n    // Verify that the appeal was created\n    const { data: result } = await agent.tools.ozone.moderation.queryStatuses(\n      {\n        subject: account.did,\n      },\n      { headers: sc.getHeaders(moderator) },\n    )\n\n    expect(result.subjectStatuses[0].appealed).toBe(true)\n    expect(\n      forSubjectStatusSnapshot(result.subjectStatuses[0]),\n    ).toMatchSnapshot()\n  })\n\n  it('takendown actor is not allowed to create reports.', async () => {\n    const { data: auth } = await agent.com.atproto.server.createSession({\n      identifier: 'jeff.test',\n      password: 'password',\n      allowTakendown: true,\n    })\n\n    // send appeal event as the takendown account\n    await expect(\n      agent.com.atproto.moderation.createReport(\n        {\n          reasonType: ComAtprotoModerationDefs.REASONRUDE,\n          reason: 'reporting others',\n          subject: {\n            $type: 'com.atproto.admin.defs#repoRef',\n            did: 'did:plc:test',\n          },\n        },\n        {\n          headers: {\n            authorization: `Bearer ${auth.accessJwt}`,\n          },\n        },\n      ),\n    ).rejects.toThrow('Report not accepted from takendown account')\n  })\n  it('takendown actor is not allowed to create records.', async () => {\n    const { data: auth } = await agent.com.atproto.server.createSession({\n      identifier: 'jeff.test',\n      password: 'password',\n      allowTakendown: true,\n    })\n\n    // send appeal event as the takendown account\n    await expect(\n      agent.com.atproto.repo.createRecord(\n        {\n          repo: auth.did,\n          collection: ids.AppBskyFeedPost,\n          // rkey: 'self',\n          record: {\n            text: 'test',\n            createdAt: new Date().toISOString(),\n          },\n        },\n        {\n          headers: {\n            authorization: `Bearer ${auth.accessJwt}`,\n          },\n          encoding: 'application/json',\n        },\n      ),\n    ).rejects.toThrow('Bad token scope')\n  })\n})\n"
  },
  {
    "path": "packages/pds/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/pds/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/pds/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"noUnusedLocals\": false\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/repo/CHANGELOG.md",
    "content": "# @atproto/repo\n\n## 0.8.13\n\n### Patch Changes\n\n- [#4729](https://github.com/bluesky-social/atproto/pull/4729) [`192685f`](https://github.com/bluesky-social/atproto/commit/192685fca75a68c9c50a94817d3f27da7fc02f56) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Optimize BufferedReader implementation\n\n## 0.8.12\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/common-web@0.4.7\n  - @atproto/common@0.5.3\n\n## 0.8.11\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common-web@0.4.4\n  - @atproto/common@0.5.0\n  - @atproto/lexicon@0.5.2\n  - @atproto/crypto@0.4.4\n\n## 0.8.10\n\n### Patch Changes\n\n- [#4223](https://github.com/bluesky-social/atproto/pull/4223) [`8dd77bad2`](https://github.com/bluesky-social/atproto/commit/8dd77bad2fdee20e39d3787198d960c19d8df3d0) Thanks [@dholms](https://github.com/dholms)! - Ensure that reading CAR files is actually done asynchronously\n\n## 0.8.9\n\n### Patch Changes\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add iterator support to the `BlockMap` class\n\n- [#4169](https://github.com/bluesky-social/atproto/pull/4169) [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Small performance improvement\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/common-web@0.4.3\n  - @atproto/common@0.4.12\n  - @atproto/lexicon@0.5.1\n  - @atproto/crypto@0.4.4\n\n## 0.8.8\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n\n## 0.8.7\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n\n## 0.8.6\n\n### Patch Changes\n\n- [#4069](https://github.com/bluesky-social/atproto/pull/4069) [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12) Thanks [@devinivy](https://github.com/devinivy)! - Verify CID to content mapping within CARs unless explicitly skipped.\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/lexicon@0.4.13\n\n## 0.8.5\n\n### Patch Changes\n\n- Updated dependencies [[`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/lexicon@0.4.12\n\n## 0.8.4\n\n### Patch Changes\n\n- [#3981](https://github.com/bluesky-social/atproto/pull/3981) [`a8dee6af3`](https://github.com/bluesky-social/atproto/commit/a8dee6af33618d3072ebae7f23843242a32c926c) Thanks [@bnewbold](https://github.com/bnewbold)! - allow tilde character in MST keys (as allowed by record key syntax)\n\n## 0.8.3\n\n### Patch Changes\n\n- [#3971](https://github.com/bluesky-social/atproto/pull/3971) [`5fccbd2a1`](https://github.com/bluesky-social/atproto/commit/5fccbd2a14420e4a7c6f56ad9af4ecfe15a971e3) Thanks [@pfrazee](https://github.com/pfrazee)! - improve performance of reading repos from car files\n\n## 0.8.2\n\n### Patch Changes\n\n- [#3956](https://github.com/bluesky-social/atproto/pull/3956) [`8bd45e2f8`](https://github.com/bluesky-social/atproto/commit/8bd45e2f898a87b3550c7f4a0c8312fad9cb4736) Thanks [@bnewbold](https://github.com/bnewbold)! - increase max MST key from 256 to 1024 chars\n\n## 0.8.1\n\n### Patch Changes\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/common-web@0.4.2\n  - @atproto/lexicon@0.4.11\n  - @atproto/common@0.4.11\n  - @atproto/crypto@0.4.4\n\n## 0.8.0\n\n### Minor Changes\n\n- [#3672](https://github.com/bluesky-social/atproto/pull/3672) [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144) Thanks [@dholms](https://github.com/dholms)! - Rewrite CAR implementation\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144), [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common-web@0.4.1\n  - @atproto/common@0.4.10\n  - @atproto/lexicon@0.4.10\n  - @atproto/crypto@0.4.4\n\n## 0.7.3\n\n### Patch Changes\n\n- [#2519](https://github.com/bluesky-social/atproto/pull/2519) [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f) Thanks [@dholms](https://github.com/dholms)! - Add revOverride in repo constructor\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/common@0.4.9\n  - @atproto/crypto@0.4.4\n\n## 0.7.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.9\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.8\n\n## 0.7.0\n\n### Minor Changes\n\n- [#3449](https://github.com/bluesky-social/atproto/pull/3449) [`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f) Thanks [@dholms](https://github.com/dholms)! - Updated subscribeRepo to include prev CIDs for operations and covering proofs for all ops.\n\n## 0.6.5\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/lexicon@0.4.7\n\n## 0.6.4\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`8a30e0ed9`](https://github.com/bluesky-social/atproto/commit/8a30e0ed9239cb2037d54fb98e70e8b0cfbc3e39), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/common-web@0.4.0\n  - @atproto/lexicon@0.4.6\n  - @atproto/common@0.4.8\n  - @atproto/crypto@0.4.4\n\n## 0.6.3\n\n### Patch Changes\n\n- Updated dependencies [[`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/common@0.4.7\n  - @atproto/crypto@0.4.3\n\n## 0.6.2\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99), [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/common-web@0.3.2\n  - @atproto/common@0.4.6\n  - @atproto/lexicon@0.4.5\n  - @atproto/crypto@0.4.2\n\n## 0.6.0\n\n### Minor Changes\n\n- [#3033](https://github.com/bluesky-social/atproto/pull/3033) [`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a) Thanks [@dholms](https://github.com/dholms)! - Add relevant proof blocks to commit data\n\n### Patch Changes\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95)]:\n  - @atproto/common@0.4.5\n  - @atproto/lexicon@0.4.4\n  - @atproto/crypto@0.4.2\n\n## 0.5.5\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/lexicon@0.4.3\n\n## 0.5.4\n\n### Patch Changes\n\n- Updated dependencies [[`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0)]:\n  - @atproto/crypto@0.4.2\n\n## 0.5.3\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]:\n  - @atproto/common@0.4.4\n  - @atproto/crypto@0.4.1\n\n## 0.5.2\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/common-web@0.3.1\n  - @atproto/lexicon@0.4.2\n  - @atproto/common@0.4.3\n  - @atproto/crypto@0.4.1\n\n## 0.5.1\n\n### Patch Changes\n\n- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/common@0.4.2\n  - @atproto/crypto@0.4.1\n\n## 0.5.0\n\n### Minor Changes\n\n- [#2752](https://github.com/bluesky-social/atproto/pull/2752) [`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442) Thanks [@dholms](https://github.com/dholms)! - Updated verifyProofs consumer method to verify Cid claims rather than record claims\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies [[`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31)]:\n  - @atproto/crypto@0.4.1\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e)]:\n  - @atproto/lexicon@0.4.1\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/common@0.4.1\n  - @atproto/crypto@0.4.0\n\n## 0.4.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/common-web@0.3.0\n  - @atproto/lexicon@0.4.0\n  - @atproto/common@0.4.0\n  - @atproto/crypto@0.4.0\n\n## 0.3.9\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n  - @atproto/identity@0.3.3\n  - @atproto/common@0.3.4\n  - @atproto/lexicon@0.3.3\n  - @atproto/syntax@0.2.1\n  - @atproto/crypto@0.3.0\n\n## 0.3.8\n\n### Patch Changes\n\n- Updated dependencies [[`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4)]:\n  - @atproto/syntax@0.2.0\n  - @atproto/lexicon@0.3.2\n\n## 0.3.7\n\n### Patch Changes\n\n- [#2173](https://github.com/bluesky-social/atproto/pull/2173) [`fcf8e3faf`](https://github.com/bluesky-social/atproto/commit/fcf8e3faf311559162c3aa0d9af36f84951914bc) Thanks [@bnewbold](https://github.com/bnewbold)! - repo commit object prev field is nullable, but no longer nullable\n\n## 0.3.6\n\n### Patch Changes\n\n- Updated dependencies [[`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60)]:\n  - @atproto/syntax@0.1.5\n  - @atproto/lexicon@0.3.1\n\n## 0.3.5\n\n### Patch Changes\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n  - @atproto/identity@0.3.2\n\n## 0.3.4\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/common-web@0.2.3\n  - @atproto/identity@0.3.1\n  - @atproto/common@0.3.3\n  - @atproto/crypto@0.2.3\n  - @atproto/syntax@0.1.4\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies [[`bb039d8e`](https://github.com/bluesky-social/atproto/commit/bb039d8e4ce5b7f70c4f3e86d1327e210ef24dc3), [`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/identity@0.3.0\n  - @atproto/common-web@0.2.2\n  - @atproto/common@0.3.2\n  - @atproto/lexicon@0.2.3\n  - @atproto/syntax@0.1.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/common-web@0.2.1\n  - @atproto/common@0.3.1\n  - @atproto/identity@0.2.1\n  - @atproto/lexicon@0.2.2\n  - @atproto/syntax@0.1.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]:\n  - @atproto/syntax@0.1.1\n  - @atproto/lexicon@0.2.1\n"
  },
  {
    "path": "packages/repo/README.md",
    "content": "# @atproto/repo: Repository and MST\n\nTypeScript library for atproto repositories, and in particular the Merkle Search Tree (MST) data structure.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/repo)](https://www.npmjs.com/package/@atproto/repo)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\nRepositories in atproto are signed key/value stores containing CBOR-encoded data records. The structure and implementation details are described in [the specification](https://atproto.com/specs/repository). This includes MST node format, serialization, structural constraints, and more.\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/repo/bench/mst.bench.ts",
    "content": "import fs from 'node:fs'\nimport { CID } from 'multiformats'\nimport { Fanout, MST, MemoryBlockstore, NodeEntry } from '../src'\nimport * as util from '../tests/_util'\n\ntype BenchmarkData = {\n  fanout: number\n  size: number\n  addTime: string\n  saveTime: string\n  walkTime: string\n  depth: number\n  maxWidth: number\n  blockstoreSize: number\n  largestProofSize: number\n  avgProofSize: number\n  widths: Record<number, number>\n}\n\ndescribe('MST Benchmarks', () => {\n  let mapping: Record<string, CID>\n  let shuffled: [string, CID][]\n\n  const size = 500000\n\n  beforeAll(async () => {\n    mapping = await util.generateBulkDataKeys(size)\n    shuffled = util.shuffle(Object.entries(mapping))\n  })\n\n  // const fanouts: Fanout[] = [8, 16, 32]\n  const fanouts: Fanout[] = [16, 32]\n  it('benchmarks various fanouts', async () => {\n    const benches: BenchmarkData[] = []\n    for (const fanout of fanouts) {\n      const blockstore = new MemoryBlockstore()\n      let mst = await MST.create(blockstore, [], { fanout })\n\n      const start = Date.now()\n\n      for (const entry of shuffled) {\n        mst = await mst.add(entry[0], entry[1])\n      }\n\n      const doneAdding = Date.now()\n\n      const root = await util.saveMst(blockstore, mst)\n\n      const doneSaving = Date.now()\n\n      const reloaded = await MST.load(blockstore, root, { fanout })\n      const widthTracker = new NodeWidths()\n      for await (const entry of reloaded.walk()) {\n        await widthTracker.trackEntry(entry)\n      }\n\n      const doneWalking = Date.now()\n\n      const paths = await reloaded.paths()\n      let largestProof = 0\n      let combinedProofSizes = 0\n      for (const path of paths) {\n        let proofSize = 0\n        for (const entry of path) {\n          if (entry.isTree()) {\n            const bytes = await blockstore.getBytes(entry.pointer)\n            if (!bytes) {\n              throw new Error(`Bytes not found: ${entry.pointer}`)\n            }\n            proofSize += bytes.byteLength\n          }\n        }\n        largestProof = Math.max(largestProof, proofSize)\n        combinedProofSizes += proofSize\n      }\n      const avgProofSize = Math.ceil(combinedProofSizes / paths.length)\n\n      const blockstoreSize = await blockstore.sizeInBytes()\n\n      benches.push({\n        fanout,\n        size,\n        addTime: secDiff(start, doneAdding),\n        saveTime: secDiff(doneAdding, doneSaving),\n        walkTime: secDiff(doneSaving, doneWalking),\n        depth: await mst.getLayer(),\n        blockstoreSize,\n        largestProofSize: largestProof,\n        avgProofSize: avgProofSize,\n        maxWidth: widthTracker.max,\n        widths: widthTracker.data,\n      })\n    }\n    writeBenchData(benches, 'mst-benchmarks')\n  })\n})\n\nconst secDiff = (first: number, second: number): string => {\n  return ((second - first) / 1000).toFixed(3)\n}\n\nclass NodeWidths {\n  data = {\n    0: 0,\n    16: 0,\n    32: 0,\n    48: 0,\n    64: 0,\n    96: 0,\n    128: 0,\n    160: 0,\n    192: 0,\n    224: 0,\n    256: 0,\n  }\n  max = 0\n\n  async trackEntry(entry: NodeEntry) {\n    if (!entry.isTree()) return\n    const entries = await entry.getEntries()\n    const width = entries.filter((e) => e.isLeaf()).length\n    this.max = Math.max(this.max, width)\n    if (width >= 0) this.data[0]++\n    if (width >= 16) this.data[16]++\n    if (width >= 32) this.data[32]++\n    if (width >= 48) this.data[48]++\n    if (width >= 64) this.data[64]++\n    if (width >= 96) this.data[96]++\n    if (width >= 128) this.data[128]++\n    if (width >= 160) this.data[160]++\n    if (width >= 192) this.data[192]++\n    if (width >= 224) this.data[224]++\n    if (width >= 256) this.data[256]++\n  }\n}\n\nconst writeBenchData = (benches: BenchmarkData[], fileLoc: string) => {\n  let toWrite = ''\n  for (const bench of benches) {\n    toWrite += `Fanout: ${bench.fanout}\n----------------------\nTime to add ${bench.size} leaves: ${bench.addTime}s\nTime to save tree with ${bench.size} leaves: ${bench.saveTime}s\nTime to reconstruct & walk ${bench.size} leaves: ${bench.walkTime}s\nTree depth: ${bench.depth}\nMax Node Width (only counting leaves): ${bench.maxWidth}\nThe total blockstore size is: ${bench.blockstoreSize} bytes\nLargest proof size: ${bench.largestProofSize} bytes\nAverage proof size: ${bench.avgProofSize} bytes\nNodes with >= 0 leaves: ${bench.widths[0]}\nNodes with >= 16 leaves: ${bench.widths[16]}\nNodes with >= 32 leaves: ${bench.widths[32]}\nNodes with >= 48 leaves: ${bench.widths[48]}\nNodes with >= 64 leaves: ${bench.widths[64]}\nNodes with >= 96 leaves: ${bench.widths[96]}\nNodes with >= 128 leaves: ${bench.widths[128]}\nNodes with >= 160 leaves: ${bench.widths[160]}\nNodes with >= 192 leaves: ${bench.widths[192]}\nNodes with >= 224 leaves: ${bench.widths[224]}\nNodes with >= 256 leaves: ${bench.widths[256]}\n\n`\n  }\n  fs.writeFileSync(fileLoc, toWrite)\n}\n"
  },
  {
    "path": "packages/repo/bench/repo.bench.ts",
    "content": "import { TID } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { MemoryBlockstore, Repo, WriteOpAction } from '../src'\nimport * as util from '../tests/_util'\n\ndescribe('Repo Benchmarks', () => {\n  const size = 10000\n\n  let blockstore: MemoryBlockstore\n  let keypair: crypto.Keypair\n  let repo: Repo\n\n  beforeAll(async () => {\n    blockstore = new MemoryBlockstore()\n    keypair = await Secp256k1Keypair.create()\n    repo = await Repo.create(blockstore, await keypair.did(), keypair)\n  })\n\n  it('calculates size', async () => {\n    for (let i = 0; i < size; i++) {\n      if (i % 500 === 0) {\n        console.log(i)\n      }\n\n      await repo.applyCommit(\n        {\n          action: WriteOpAction.Create,\n          collection: 'app.bsky.post',\n          rkey: TID.nextStr(),\n          value: {\n            $type: 'app.bsky.post',\n            text: util.randomStr(150),\n            reply: {\n              root: 'at://did:plc:1234abdefeoi23/app.bsky.post/12345678912345',\n              parent:\n                'at://did:plc:1234abdefeoi23/app.bsky.post/12345678912345',\n            },\n            createdAt: new Date().toISOString(),\n          },\n        },\n        keypair,\n      )\n    }\n\n    console.log('SIZE: ', await blockstore.sizeInBytes())\n  })\n})\n"
  },
  {
    "path": "packages/repo/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Repo',\n  transform: { '^.+\\\\.(t|j)s$': '@swc/jest' },\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/repo/package.json",
    "content": "{\n  \"name\": \"@atproto/repo\",\n  \"version\": \"0.8.13\",\n  \"license\": \"MIT\",\n  \"description\": \"atproto repo and MST implementation\",\n  \"keywords\": [\n    \"atproto\",\n    \"mst\"\n  ],\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/repo\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test:profile\": \"node --inspect ../../node_modules/.bin/jest\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/common-web\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@ipld/dag-cbor\": \"^7.0.0\",\n    \"multiformats\": \"^9.9.0\",\n    \"uint8arrays\": \"3.0.0\",\n    \"varint\": \"^6.0.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/block-map.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport * as uint8arrays from 'uint8arrays'\nimport { dataToCborBlock } from '@atproto/common'\nimport { LexValue, lexToIpld } from '@atproto/lexicon'\n\nexport class BlockMap implements Iterable<[cid: CID, bytes: Uint8Array]> {\n  private map: Map<string, Uint8Array> = new Map()\n\n  constructor(entries?: Iterable<readonly [cid: CID, bytes: Uint8Array]>) {\n    if (entries) {\n      for (const [cid, bytes] of entries) {\n        this.set(cid, bytes)\n      }\n    }\n  }\n\n  async add(value: LexValue): Promise<CID> {\n    const block = await dataToCborBlock(lexToIpld(value))\n    this.set(block.cid, block.bytes)\n    return block.cid\n  }\n\n  set(cid: CID, bytes: Uint8Array): BlockMap {\n    this.map.set(cid.toString(), bytes)\n    return this\n  }\n\n  get(cid: CID): Uint8Array | undefined {\n    return this.map.get(cid.toString())\n  }\n\n  delete(cid: CID): BlockMap {\n    this.map.delete(cid.toString())\n    return this\n  }\n\n  getMany(cids: CID[]): { blocks: BlockMap; missing: CID[] } {\n    const missing: CID[] = []\n    const blocks = new BlockMap()\n    for (const cid of cids) {\n      const got = this.map.get(cid.toString())\n      if (got) {\n        blocks.set(cid, got)\n      } else {\n        missing.push(cid)\n      }\n    }\n    return { blocks, missing }\n  }\n\n  has(cid: CID): boolean {\n    return this.map.has(cid.toString())\n  }\n\n  clear(): void {\n    this.map.clear()\n  }\n\n  forEach(cb: (bytes: Uint8Array, cid: CID) => void): void {\n    for (const [cid, bytes] of this) cb(bytes, cid)\n  }\n\n  entries(): Entry[] {\n    return Array.from(this, toEntry)\n  }\n\n  cids(): CID[] {\n    return Array.from(this.keys())\n  }\n\n  addMap(toAdd: BlockMap): BlockMap {\n    for (const [cid, bytes] of toAdd) this.set(cid, bytes)\n    return this\n  }\n\n  get size(): number {\n    return this.map.size\n  }\n\n  get byteSize(): number {\n    let size = 0\n    for (const bytes of this.values()) size += bytes.length\n    return size\n  }\n\n  equals(other: BlockMap): boolean {\n    if (this.size !== other.size) {\n      return false\n    }\n    for (const [cid, bytes] of this) {\n      const otherBytes = other.get(cid)\n      if (!otherBytes) return false\n      if (!uint8arrays.equals(bytes, otherBytes)) {\n        return false\n      }\n    }\n    return true\n  }\n\n  *keys(): Generator<CID, void, unknown> {\n    for (const key of this.map.keys()) {\n      yield CID.parse(key)\n    }\n  }\n\n  *values(): Generator<Uint8Array, void, unknown> {\n    yield* this.map.values()\n  }\n\n  *[Symbol.iterator](): Generator<[CID, Uint8Array], void, unknown> {\n    for (const [key, value] of this.map) {\n      yield [CID.parse(key), value]\n    }\n  }\n}\n\nfunction toEntry([cid, bytes]: readonly [CID, Uint8Array]): Entry {\n  return { cid, bytes }\n}\n\ntype Entry = {\n  cid: CID\n  bytes: Uint8Array\n}\n\nexport default BlockMap\n"
  },
  {
    "path": "packages/repo/src/car.ts",
    "content": "import { setImmediate } from 'node:timers/promises'\nimport * as cbor from '@ipld/dag-cbor'\nimport { CID } from 'multiformats/cid'\nimport * as ui8 from 'uint8arrays'\nimport * as varint from 'varint'\nimport {\n  check,\n  parseCidFromBytes,\n  schema,\n  streamToBuffer,\n  verifyCidForBytes,\n} from '@atproto/common'\nimport { BlockMap } from './block-map'\nimport { CarBlock } from './types'\n\nexport async function* writeCarStream(\n  root: CID | null,\n  blocks: AsyncIterable<CarBlock>,\n): AsyncIterable<Uint8Array> {\n  const header = new Uint8Array(\n    cbor.encode({\n      version: 1,\n      roots: root ? [root] : [],\n    }),\n  )\n  yield new Uint8Array(varint.encode(header.byteLength))\n  yield header\n  for await (const block of blocks) {\n    yield new Uint8Array(\n      varint.encode(block.cid.bytes.byteLength + block.bytes.byteLength),\n    )\n    yield block.cid.bytes\n    yield block.bytes\n  }\n}\n\nexport const blocksToCarFile = (\n  root: CID | null,\n  blocks: BlockMap,\n): Promise<Uint8Array> => {\n  const carStream = blocksToCarStream(root, blocks)\n  return streamToBuffer(carStream)\n}\n\nexport const blocksToCarStream = (\n  root: CID | null,\n  blocks: BlockMap,\n): AsyncIterable<Uint8Array> => {\n  return writeCarStream(root, iterateBlocks(blocks))\n}\n\nasync function* iterateBlocks(blocks: BlockMap) {\n  for (const entry of blocks.entries()) {\n    yield { cid: entry.cid, bytes: entry.bytes }\n  }\n}\n\nexport type ReadCarOptions = {\n  /**\n   * When true, does not verify CID-to-content mapping within CAR.\n   */\n  skipCidVerification?: boolean\n}\n\nexport const readCar = async (\n  bytes: Uint8Array,\n  opts?: ReadCarOptions,\n): Promise<{ roots: CID[]; blocks: BlockMap }> => {\n  const { roots, blocks } = await readCarReader(new Ui8Reader(bytes), opts)\n  const blockMap = new BlockMap()\n  for await (const block of blocks) {\n    blockMap.set(block.cid, block.bytes)\n  }\n  return { roots, blocks: blockMap }\n}\n\nexport const readCarWithRoot = async (\n  bytes: Uint8Array,\n  opts?: ReadCarOptions,\n): Promise<{ root: CID; blocks: BlockMap }> => {\n  const { roots, blocks } = await readCar(bytes, opts)\n  if (roots.length !== 1) {\n    throw new Error(`Expected one root, got ${roots.length}`)\n  }\n  const root = roots[0]\n  return {\n    root,\n    blocks,\n  }\n}\nexport type CarBlockIterable = AsyncGenerator<CarBlock, void, unknown> & {\n  dump: () => Promise<void>\n}\n\nexport const readCarStream = async (\n  car: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n  opts?: ReadCarOptions,\n): Promise<{\n  roots: CID[]\n  blocks: CarBlockIterable\n}> => {\n  return readCarReader(new BufferedReader(car), opts)\n}\n\nexport const readCarReader = async (\n  reader: BytesReader,\n  opts?: ReadCarOptions,\n): Promise<{\n  roots: CID[]\n  blocks: CarBlockIterable\n}> => {\n  try {\n    const headerSize = await readVarint(reader)\n    if (headerSize === null) {\n      throw new Error('Could not parse CAR header')\n    }\n    const headerBytes = await reader.read(headerSize)\n    const header = cbor.decode(headerBytes)\n    if (!check.is(header, schema.carHeader)) {\n      throw new Error('Could not parse CAR header')\n    }\n    return {\n      roots: header.roots,\n      blocks: readCarBlocksIter(reader, opts),\n    }\n  } catch (err) {\n    await reader.close()\n    throw err\n  }\n}\n\nconst readCarBlocksIter = (\n  reader: BytesReader,\n  opts?: ReadCarOptions,\n): CarBlockIterable => {\n  let generator = readCarBlocksIterGenerator(reader)\n  if (!opts?.skipCidVerification) {\n    generator = verifyIncomingCarBlocks(generator)\n  }\n  return Object.assign(generator, {\n    async dump() {\n      // try/finally to ensure that reader.close is called even if blocks.return throws.\n      try {\n        // Prevent the iterator from being started after this method is called.\n        await generator.return()\n      } finally {\n        // @NOTE the \"finally\" block of the async generator won't be called\n        // if the iteration was never started so we need to manually close here.\n        await reader.close()\n      }\n    },\n  })\n}\n\nasync function* readCarBlocksIterGenerator(\n  reader: BytesReader,\n): AsyncGenerator<CarBlock, void, unknown> {\n  let blocks = 0\n  try {\n    while (!reader.isDone) {\n      const blockSize = await readVarint(reader)\n      if (blockSize === null) {\n        break\n      }\n      const blockBytes = await reader.read(blockSize)\n      const cid = parseCidFromBytes(blockBytes.subarray(0, 36))\n      const bytes = blockBytes.subarray(36)\n      yield { cid, bytes }\n\n      // yield to the event loop every 25 blocks\n      // in the case the incoming CAR is synchronous, this can end up jamming up the thread\n      blocks++\n      if (blocks % 25 === 0) {\n        await setImmediate()\n      }\n    }\n  } finally {\n    await reader.close()\n  }\n}\n\nexport async function* verifyIncomingCarBlocks(\n  car: AsyncIterable<CarBlock>,\n): AsyncGenerator<CarBlock, void, unknown> {\n  for await (const block of car) {\n    await verifyCidForBytes(block.cid, block.bytes)\n    yield block\n  }\n}\n\nconst readVarint = async (reader: BytesReader): Promise<number | null> => {\n  let done = false\n  const bytes: Uint8Array[] = []\n  while (!done) {\n    const byte = await reader.read(1)\n    if (byte.byteLength === 0) {\n      if (bytes.length > 0) {\n        throw new Error('could not parse varint')\n      } else {\n        return null\n      }\n    }\n    bytes.push(byte)\n    if (byte[0] < 128) {\n      done = true\n    }\n  }\n  const concatted = ui8.concat(bytes)\n  return varint.decode(concatted)\n}\n\ninterface BytesReader {\n  isDone: boolean\n  read(bytesToRead: number): Promise<Uint8Array>\n  close(): Promise<void>\n}\n\nclass Ui8Reader implements BytesReader {\n  idx = 0\n  isDone = false\n\n  constructor(public bytes: Uint8Array) {}\n\n  async read(bytesToRead: number): Promise<Uint8Array> {\n    const value = this.bytes.subarray(this.idx, this.idx + bytesToRead)\n    this.idx += bytesToRead\n    if (this.idx >= this.bytes.length) {\n      this.isDone = true\n    }\n    return value\n  }\n\n  async close(): Promise<void> {}\n}\n\n/**\n * This code was optimized for performance. See\n * {@link https://github.com/bluesky-social/atproto/pull/4729 #4729} for more details\n * and benchmarks.\n */\nclass BufferedReader implements BytesReader {\n  iterator: Iterator<Uint8Array> | AsyncIterator<Uint8Array>\n  isDone = false\n\n  /** fifo list of chunks to consume */\n  private chunks: Uint8Array[] = []\n\n  constructor(stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>) {\n    this.iterator =\n      Symbol.asyncIterator in stream\n        ? stream[Symbol.asyncIterator]()\n        : stream[Symbol.iterator]()\n  }\n\n  /** Number of bytes currently buffered and available for reading */\n  get bufferedByteLength() {\n    let total = 0\n    for (let i = 0; i < this.chunks.length; i++) {\n      total += this.chunks[i].byteLength\n    }\n    return total\n  }\n\n  /**\n   * @note concurrent reads are **NOT** supported by the current implementation\n   * and would require call to readUntilBuffered to be using a fifo lock for\n   * read()s to be processed in fifo order.\n   */\n  async read(bytesToRead: number): Promise<Uint8Array> {\n    const bytesNeeded = bytesToRead - this.bufferedByteLength\n    if (bytesNeeded > 0 && !this.isDone) {\n      await this.readUntilBuffered(bytesNeeded)\n    }\n\n    const resultLength = Math.min(bytesToRead, this.bufferedByteLength)\n    if (resultLength <= 0) return new Uint8Array()\n\n    const firstChunk = this.consumeChunk(resultLength)\n    if (firstChunk.byteLength === resultLength) {\n      // If the data consumed from the first chunk contains all we need, return\n      // it as-is. This allows to avoid any copy operation.\n      return firstChunk\n    }\n\n    // The first chunk does not have all the data we need. We have to copy\n    // multiple chunks into a larger buffer\n    const result = new Uint8Array(resultLength)\n    let resultWriteIndex = 0\n\n    // Copy the first chunk into the result buffer\n    result.set(firstChunk, resultWriteIndex)\n    resultWriteIndex += firstChunk.byteLength\n\n    // Copy more chunks as needed (we use do-while because we *know* we need\n    // more than one chunk)\n    do {\n      const missingLength = resultLength - resultWriteIndex\n      const currentChunk = this.consumeChunk(missingLength)\n\n      result.set(currentChunk, resultWriteIndex)\n      resultWriteIndex += currentChunk.byteLength\n    } while (resultWriteIndex < resultLength)\n\n    return result\n  }\n\n  private async readUntilBuffered(bytesNeeded: number) {\n    let bytesRead = 0\n    while (bytesRead < bytesNeeded) {\n      const next = await this.iterator.next()\n      if (next.done) {\n        this.isDone = true\n        break\n      } else {\n        this.chunks.push(next.value)\n        bytesRead += next.value.byteLength\n      }\n    }\n    return bytesRead\n  }\n\n  private consumeChunk(bytesToConsume: number) {\n    const firstChunk = this.chunks[0]!\n    if (bytesToConsume < firstChunk.byteLength) {\n      // return a sub-view of the data being read and replace the first chunk\n      // with a sub-view that does not contain that data.\n\n      // @NOTE for some reason, subarray() revealed to be 7-8% slower in NodeJS\n      // benchmarks.\n\n      // this.chunks[0] = firstChunk.subarray(bytesToConsume)\n      // return firstChunk.subarray(0, bytesToConsume)\n\n      this.chunks[0] = new Uint8Array(\n        firstChunk.buffer,\n        firstChunk.byteOffset + bytesToConsume,\n        firstChunk.byteLength - bytesToConsume,\n      )\n      return new Uint8Array(\n        firstChunk.buffer,\n        firstChunk.byteOffset,\n        bytesToConsume,\n      )\n    } else {\n      // First chunk is being read in full, discard it\n      this.chunks.shift()\n      return firstChunk\n    }\n  }\n\n  async close(): Promise<void> {\n    try {\n      if (!this.isDone && this.iterator.return) {\n        await this.iterator.return()\n      }\n    } finally {\n      this.isDone = true\n      this.chunks.length = 0\n    }\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/cid-set.ts",
    "content": "import { CID } from 'multiformats'\n\nexport class CidSet {\n  private set: Set<string>\n\n  constructor(arr: CID[] = []) {\n    const strArr = arr.map((c) => c.toString())\n    this.set = new Set(strArr)\n  }\n\n  add(cid: CID): CidSet {\n    this.set.add(cid.toString())\n    return this\n  }\n\n  addSet(toMerge: CidSet): CidSet {\n    toMerge.toList().map((c) => this.add(c))\n    return this\n  }\n\n  subtractSet(toSubtract: CidSet): CidSet {\n    toSubtract.toList().map((c) => this.delete(c))\n    return this\n  }\n\n  delete(cid: CID) {\n    this.set.delete(cid.toString())\n    return this\n  }\n\n  has(cid: CID): boolean {\n    return this.set.has(cid.toString())\n  }\n\n  size(): number {\n    return this.set.size\n  }\n\n  clear(): CidSet {\n    this.set.clear()\n    return this\n  }\n\n  toList(): CID[] {\n    return [...this.set].map((c) => CID.parse(c))\n  }\n}\n\nexport default CidSet\n"
  },
  {
    "path": "packages/repo/src/data-diff.ts",
    "content": "import { CID } from 'multiformats'\nimport { BlockMap } from './block-map'\nimport { CidSet } from './cid-set'\nimport { MST, NodeEntry, mstDiff } from './mst'\n\nexport class DataDiff {\n  adds: Record<string, DataAdd> = {}\n  updates: Record<string, DataUpdate> = {}\n  deletes: Record<string, DataDelete> = {}\n\n  newMstBlocks: BlockMap = new BlockMap()\n  newLeafCids: CidSet = new CidSet()\n  removedCids: CidSet = new CidSet()\n\n  static async of(curr: MST, prev: MST | null): Promise<DataDiff> {\n    return mstDiff(curr, prev)\n  }\n\n  async nodeAdd(node: NodeEntry) {\n    if (node.isLeaf()) {\n      this.leafAdd(node.key, node.value)\n    } else {\n      const data = await node.serialize()\n      this.treeAdd(data.cid, data.bytes)\n    }\n  }\n\n  async nodeDelete(node: NodeEntry) {\n    if (node.isLeaf()) {\n      const key = node.key\n      const cid = node.value\n      this.deletes[key] = { key, cid }\n      this.removedCids.add(cid)\n    } else {\n      const cid = await node.getPointer()\n      this.treeDelete(cid)\n    }\n  }\n\n  leafAdd(key: string, cid: CID) {\n    this.adds[key] = { key, cid }\n    if (this.removedCids.has(cid)) {\n      this.removedCids.delete(cid)\n    } else {\n      this.newLeafCids.add(cid)\n    }\n  }\n\n  leafUpdate(key: string, prev: CID, cid: CID) {\n    if (prev.equals(cid)) return\n    this.updates[key] = { key, prev, cid }\n    this.removedCids.add(prev)\n    this.newLeafCids.add(cid)\n  }\n\n  leafDelete(key: string, cid: CID) {\n    this.deletes[key] = { key, cid }\n    if (this.newLeafCids.has(cid)) {\n      this.newLeafCids.delete(cid)\n    } else {\n      this.removedCids.add(cid)\n    }\n  }\n\n  treeAdd(cid: CID, bytes: Uint8Array) {\n    if (this.removedCids.has(cid)) {\n      this.removedCids.delete(cid)\n    } else {\n      this.newMstBlocks.set(cid, bytes)\n    }\n  }\n\n  treeDelete(cid: CID) {\n    if (this.newMstBlocks.has(cid)) {\n      this.newMstBlocks.delete(cid)\n    } else {\n      this.removedCids.add(cid)\n    }\n  }\n\n  addList(): DataAdd[] {\n    return Object.values(this.adds)\n  }\n\n  updateList(): DataUpdate[] {\n    return Object.values(this.updates)\n  }\n\n  deleteList(): DataDelete[] {\n    return Object.values(this.deletes)\n  }\n\n  updatedKeys(): string[] {\n    const keys = [\n      ...Object.keys(this.adds),\n      ...Object.keys(this.updates),\n      ...Object.keys(this.deletes),\n    ]\n    return [...new Set(keys)]\n  }\n}\n\nexport type DataAdd = {\n  key: string\n  cid: CID\n}\n\nexport type DataUpdate = {\n  key: string\n  prev: CID\n  cid: CID\n}\n\nexport type DataDelete = {\n  key: string\n  cid: CID\n}\n"
  },
  {
    "path": "packages/repo/src/error.ts",
    "content": "import { CID } from 'multiformats/cid'\n\nexport class MissingBlockError extends Error {\n  constructor(\n    public cid: CID,\n    def?: string,\n  ) {\n    let msg = `block not found: ${cid.toString()}`\n    if (def) {\n      msg += `, expected type: ${def}`\n    }\n    super(msg)\n  }\n}\n\nexport class MissingBlocksError extends Error {\n  constructor(\n    public context: string,\n    public cids: CID[],\n  ) {\n    const cidStr = cids.map((c) => c.toString())\n    super(`missing ${context} blocks: ${cidStr}`)\n  }\n}\n\nexport class MissingCommitBlocksError extends Error {\n  constructor(\n    public commit: CID,\n    public cids: CID[],\n  ) {\n    const cidStr = cids.map((c) => c.toString())\n    super(`missing blocks for commit ${commit.toString()}: ${cidStr}`)\n  }\n}\n\nexport class UnexpectedObjectError extends Error {\n  constructor(\n    public cid: CID,\n    public def: string,\n  ) {\n    super(`unexpected object at ${cid.toString()}, expected: ${def}`)\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/index.ts",
    "content": "export * from './block-map'\nexport * from './cid-set'\nexport * from './repo'\nexport * from './mst'\nexport * from './parse'\nexport * from './storage'\nexport * from './sync'\nexport * from './types'\nexport * from './data-diff'\nexport * from './car'\nexport * from './util'\n"
  },
  {
    "path": "packages/repo/src/logger.ts",
    "content": "import { subsystemLogger } from '@atproto/common'\n\nexport const logger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('repo')\n\nexport default logger\n"
  },
  {
    "path": "packages/repo/src/mst/diff.ts",
    "content": "import { DataDiff } from '../data-diff'\nimport { MST } from './mst'\nimport { MstWalker } from './walker'\n\nexport const nullDiff = async (tree: MST): Promise<DataDiff> => {\n  const diff = new DataDiff()\n  for await (const entry of tree.walk()) {\n    await diff.nodeAdd(entry)\n  }\n  return diff\n}\n\nexport const mstDiff = async (\n  curr: MST,\n  prev: MST | null,\n): Promise<DataDiff> => {\n  await curr.getPointer()\n  if (prev === null) {\n    return nullDiff(curr)\n  }\n\n  await prev.getPointer()\n  const diff = new DataDiff()\n\n  const leftWalker = new MstWalker(prev)\n  const rightWalker = new MstWalker(curr)\n  while (!leftWalker.status.done || !rightWalker.status.done) {\n    // if one walker is finished, continue walking the other & logging all nodes\n    if (leftWalker.status.done && !rightWalker.status.done) {\n      await diff.nodeAdd(rightWalker.status.curr)\n      await rightWalker.advance()\n      continue\n    } else if (!leftWalker.status.done && rightWalker.status.done) {\n      await diff.nodeDelete(leftWalker.status.curr)\n      await leftWalker.advance()\n      continue\n    }\n    if (leftWalker.status.done || rightWalker.status.done) break\n    const left = leftWalker.status.curr\n    const right = rightWalker.status.curr\n    if (left === null || right === null) break\n\n    // if both pointers are leaves, record an update & advance both or record the lowest key and advance that pointer\n    if (left.isLeaf() && right.isLeaf()) {\n      if (left.key === right.key) {\n        if (!left.value.equals(right.value)) {\n          diff.leafUpdate(left.key, left.value, right.value)\n        }\n        await leftWalker.advance()\n        await rightWalker.advance()\n      } else if (left.key < right.key) {\n        diff.leafDelete(left.key, left.value)\n        await leftWalker.advance()\n      } else {\n        diff.leafAdd(right.key, right.value)\n        await rightWalker.advance()\n      }\n      continue\n    }\n\n    // next, ensure that we're on the same layer\n    // if one walker is at a higher layer than the other, we need to do one of two things\n    // if the higher walker is pointed at a tree, step into that tree to try to catch up with the lower\n    // if the higher walker is pointed at a leaf, then advance the lower walker to try to catch up the higher\n    if (leftWalker.layer() > rightWalker.layer()) {\n      if (left.isLeaf()) {\n        await diff.nodeAdd(right)\n        await rightWalker.advance()\n      } else {\n        await diff.nodeDelete(left)\n        await leftWalker.stepInto()\n      }\n      continue\n    } else if (leftWalker.layer() < rightWalker.layer()) {\n      if (right.isLeaf()) {\n        await diff.nodeDelete(left)\n        await leftWalker.advance()\n      } else {\n        await diff.nodeAdd(right)\n        await rightWalker.stepInto()\n      }\n      continue\n    }\n\n    // if we're on the same level, and both pointers are trees, do a comparison\n    // if they're the same, step over. if they're different, step in to find the subdiff\n    if (left.isTree() && right.isTree()) {\n      if (left.pointer.equals(right.pointer)) {\n        await leftWalker.stepOver()\n        await rightWalker.stepOver()\n      } else {\n        await diff.nodeAdd(right)\n        await diff.nodeDelete(left)\n        await leftWalker.stepInto()\n        await rightWalker.stepInto()\n      }\n      continue\n    }\n\n    // finally, if one pointer is a tree and the other is a leaf, simply step into the tree\n    if (left.isLeaf() && right.isTree()) {\n      await diff.nodeAdd(right)\n      await rightWalker.stepInto()\n      continue\n    } else if (left.isTree() && right.isLeaf()) {\n      await diff.nodeDelete(left)\n      await leftWalker.stepInto()\n      continue\n    }\n\n    throw new Error('Unidentifiable case in diff walk')\n  }\n  return diff\n}\n"
  },
  {
    "path": "packages/repo/src/mst/index.ts",
    "content": "export * from './mst'\nexport * from './diff'\nexport * from './walker'\nexport * as mstUtil from './util'\n"
  },
  {
    "path": "packages/repo/src/mst/mst.ts",
    "content": "import { CID } from 'multiformats'\nimport { z } from 'zod'\nimport { cidForCbor, dataToCborBlock, schema as common } from '@atproto/common'\nimport { BlockMap } from '../block-map'\nimport { CidSet } from '../cid-set'\nimport { MissingBlockError, MissingBlocksError } from '../error'\nimport * as parse from '../parse'\nimport { ReadableBlockstore } from '../storage'\nimport { CarBlock } from '../types'\nimport * as util from './util'\n\n/**\n * This is an implementation of a Merkle Search Tree (MST)\n * The data structure is described here: https://hal.inria.fr/hal-02303490/document\n * The MST is an ordered, insert-order-independent, deterministic tree.\n * Keys are laid out in alphabetic order.\n * The key insight of an MST is that each key is hashed and starting 0s are counted\n * to determine which layer it falls on (5 zeros for ~32 fanout).\n * This is a merkle tree, so each subtree is referred to by it's hash (CID).\n * When a leaf is changed, ever tree on the path to that leaf is changed as well,\n * thereby updating the root hash.\n *\n * For atproto, we use SHA-256 as the key hashing algorithm, and ~4 fanout\n * (2-bits of zero per layer).\n */\n\n/**\n * A couple notes on CBOR encoding:\n *\n * There are never two neighboring subtrees.\n * Therefore, we can represent a node as an array of\n * leaves & pointers to their right neighbor (possibly null),\n * along with a pointer to the left-most subtree (also possibly null).\n *\n * Most keys in a subtree will have overlap.\n * We do compression on prefixes by describing keys as:\n * - the length of the prefix that it shares in common with the preceding key\n * - the rest of the string\n *\n * For example:\n * If the first leaf in a tree is `bsky/posts/abcdefg` and the second is `bsky/posts/abcdehi`\n * Then the first will be described as `prefix: 0, key: 'bsky/posts/abcdefg'`,\n * and the second will be described as `prefix: 16, key: 'hi'.`\n */\nconst subTreePointer = z.nullable(common.cid)\nconst treeEntry = z.object({\n  p: z.number(), // prefix count of ascii chars that this key shares with the prev key\n  k: common.bytes, // the rest of the key outside the shared prefix\n  v: common.cid, // value\n  t: subTreePointer, // next subtree (to the right of leaf)\n})\nconst nodeData = z.object({\n  l: subTreePointer, // left-most subtree\n  e: z.array(treeEntry), //entries\n})\nexport type NodeData = z.infer<typeof nodeData>\n\nexport const nodeDataDef = {\n  name: 'mst node',\n  schema: nodeData,\n}\n\nexport type NodeEntry = MST | Leaf\n\nexport type MstOpts = {\n  layer: number\n}\n\nexport class MST {\n  storage: ReadableBlockstore\n  entries: NodeEntry[] | null\n  layer: number | null\n  pointer: CID\n  outdatedPointer = false\n\n  constructor(\n    storage: ReadableBlockstore,\n    pointer: CID,\n    entries: NodeEntry[] | null,\n    layer: number | null,\n  ) {\n    this.storage = storage\n    this.entries = entries\n    this.layer = layer\n    this.pointer = pointer\n  }\n\n  static async create(\n    storage: ReadableBlockstore,\n    entries: NodeEntry[] = [],\n    opts?: Partial<MstOpts>,\n  ): Promise<MST> {\n    const pointer = await util.cidForEntries(entries)\n    const { layer = null } = opts || {}\n    return new MST(storage, pointer, entries, layer)\n  }\n\n  static async fromData(\n    storage: ReadableBlockstore,\n    data: NodeData,\n    opts?: Partial<MstOpts>,\n  ): Promise<MST> {\n    const { layer = null } = opts || {}\n    const entries = await util.deserializeNodeData(storage, data, opts)\n    const pointer = await cidForCbor(data)\n    return new MST(storage, pointer, entries, layer)\n  }\n\n  // this is really a *lazy* load, doesn't actually touch storage\n  static load(\n    storage: ReadableBlockstore,\n    cid: CID,\n    opts?: Partial<MstOpts>,\n  ): MST {\n    const { layer = null } = opts || {}\n    return new MST(storage, cid, null, layer)\n  }\n\n  // Immutability\n  // -------------------\n\n  // We never mutate an MST, we just return a new MST with updated values\n  async newTree(entries: NodeEntry[]): Promise<MST> {\n    const mst = new MST(this.storage, this.pointer, entries, this.layer)\n    mst.outdatedPointer = true\n    return mst\n  }\n\n  // Getters (lazy load)\n  // -------------------\n\n  // We don't want to load entries of every subtree, just the ones we need\n  async getEntries(): Promise<NodeEntry[]> {\n    if (this.entries) return [...this.entries]\n    if (this.pointer) {\n      const data = await this.storage.readObj(this.pointer, nodeDataDef)\n      const firstLeaf = data.e[0]\n      const layer =\n        firstLeaf !== undefined\n          ? await util.leadingZerosOnHash(firstLeaf.k)\n          : undefined\n      this.entries = await util.deserializeNodeData(this.storage, data, {\n        layer,\n      })\n\n      return this.entries\n    }\n    throw new Error('No entries or CID provided')\n  }\n\n  // We don't hash the node on every mutation for performance reasons\n  // Instead we keep track of whether the pointer is outdated and only (recursively) calculate when needed\n  async getPointer(): Promise<CID> {\n    if (!this.outdatedPointer) return this.pointer\n    const { cid } = await this.serialize()\n    this.pointer = cid\n    this.outdatedPointer = false\n    return this.pointer\n  }\n\n  async serialize(): Promise<{ cid: CID; bytes: Uint8Array }> {\n    let entries = await this.getEntries()\n    const outdated = entries.filter(\n      (e) => e.isTree() && e.outdatedPointer,\n    ) as MST[]\n    if (outdated.length > 0) {\n      await Promise.all(outdated.map((e) => e.getPointer()))\n      entries = await this.getEntries()\n    }\n    const data = util.serializeNodeData(entries)\n    const block = await dataToCborBlock(data)\n    return {\n      cid: block.cid,\n      bytes: block.bytes,\n    }\n  }\n\n  // In most cases, we get the layer of a node from a hint on creation\n  // In the case of the topmost node in the tree, we look for a key in the node & determine the layer\n  // In the case where we don't find one, we recurse down until we do.\n  // If we still can't find one, then we have an empty tree and the node is layer 0\n  async getLayer(): Promise<number> {\n    this.layer = await this.attemptGetLayer()\n    if (this.layer === null) this.layer = 0\n    return this.layer\n  }\n\n  async attemptGetLayer(): Promise<number | null> {\n    if (this.layer !== null) return this.layer\n    const entries = await this.getEntries()\n    let layer = await util.layerForEntries(entries)\n    if (layer === null) {\n      for (const entry of entries) {\n        if (entry.isTree()) {\n          const childLayer = await entry.attemptGetLayer()\n          if (childLayer !== null) {\n            layer = childLayer + 1\n            break\n          }\n        }\n      }\n    }\n    if (layer !== null) this.layer = layer\n    return layer\n  }\n\n  // Core functionality\n  // -------------------\n\n  // Return the necessary blocks to persist the MST to repo storage\n  async getUnstoredBlocks(): Promise<{ root: CID; blocks: BlockMap }> {\n    const blocks = new BlockMap()\n    const pointer = await this.getPointer()\n    const alreadyHas = await this.storage.has(pointer)\n    if (alreadyHas) return { root: pointer, blocks }\n    const entries = await this.getEntries()\n    const data = util.serializeNodeData(entries)\n    await blocks.add(data)\n    for (const entry of entries) {\n      if (entry.isTree()) {\n        const subtree = await entry.getUnstoredBlocks()\n        blocks.addMap(subtree.blocks)\n      }\n    }\n    return { root: pointer, blocks: blocks }\n  }\n\n  // Adds a new leaf for the given key/value pair\n  // Throws if a leaf with that key already exists\n  async add(key: string, value: CID, knownZeros?: number): Promise<MST> {\n    util.ensureValidMstKey(key)\n    const keyZeros = knownZeros ?? (await util.leadingZerosOnHash(key))\n    const layer = await this.getLayer()\n    const newLeaf = new Leaf(key, value)\n    if (keyZeros === layer) {\n      // it belongs in this layer\n      const index = await this.findGtOrEqualLeafIndex(key)\n      const found = await this.atIndex(index)\n      if (found?.isLeaf() && found.key === key) {\n        throw new Error(`There is already a value at key: ${key}`)\n      }\n      const prevNode = await this.atIndex(index - 1)\n      if (!prevNode || prevNode.isLeaf()) {\n        // if entry before is a leaf, (or we're on far left) we can just splice in\n        return this.spliceIn(newLeaf, index)\n      } else {\n        // else we try to split the subtree around the key\n        const splitSubTree = await prevNode.splitAround(key)\n        return this.replaceWithSplit(\n          index - 1,\n          splitSubTree[0],\n          newLeaf,\n          splitSubTree[1],\n        )\n      }\n    } else if (keyZeros < layer) {\n      // it belongs on a lower layer\n      const index = await this.findGtOrEqualLeafIndex(key)\n      const prevNode = await this.atIndex(index - 1)\n      if (prevNode && prevNode.isTree()) {\n        // if entry before is a tree, we add it to that tree\n        const newSubtree = await prevNode.add(key, value, keyZeros)\n        return this.updateEntry(index - 1, newSubtree)\n      } else {\n        const subTree = await this.createChild()\n        const newSubTree = await subTree.add(key, value, keyZeros)\n        return this.spliceIn(newSubTree, index)\n      }\n    } else {\n      // it belongs on a higher layer & we must push the rest of the tree down\n      const split = await this.splitAround(key)\n      // if the newly added key has >=2 more leading zeros than the current highest layer\n      // then we need to add in structural nodes in between as well\n      let left: MST | null = split[0]\n      let right: MST | null = split[1]\n      const layer = await this.getLayer()\n      const extraLayersToAdd = keyZeros - layer\n      // intentionally starting at 1, since first layer is taken care of by split\n      for (let i = 1; i < extraLayersToAdd; i++) {\n        if (left !== null) {\n          left = await left.createParent()\n        }\n        if (right !== null) {\n          right = await right.createParent()\n        }\n      }\n      const updated: NodeEntry[] = []\n      if (left) updated.push(left)\n      updated.push(new Leaf(key, value))\n      if (right) updated.push(right)\n      const newRoot = await MST.create(this.storage, updated, {\n        layer: keyZeros,\n      })\n      newRoot.outdatedPointer = true\n      return newRoot\n    }\n  }\n\n  // Gets the value at the given key\n  async get(key: string): Promise<CID | null> {\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const found = await this.atIndex(index)\n    if (found && found.isLeaf() && found.key === key) {\n      return found.value\n    }\n    const prev = await this.atIndex(index - 1)\n    if (prev && prev.isTree()) {\n      return prev.get(key)\n    }\n    return null\n  }\n\n  // Edits the value at the given key\n  // Throws if the given key does not exist\n  async update(key: string, value: CID): Promise<MST> {\n    util.ensureValidMstKey(key)\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const found = await this.atIndex(index)\n    if (found && found.isLeaf() && found.key === key) {\n      return this.updateEntry(index, new Leaf(key, value))\n    }\n    const prev = await this.atIndex(index - 1)\n    if (prev && prev.isTree()) {\n      const updatedTree = await prev.update(key, value)\n      return this.updateEntry(index - 1, updatedTree)\n    }\n    throw new Error(`Could not find a record with key: ${key}`)\n  }\n\n  // Deletes the value at the given key\n  async delete(key: string): Promise<MST> {\n    const altered = await this.deleteRecurse(key)\n    return altered.trimTop()\n  }\n\n  async deleteRecurse(key: string): Promise<MST> {\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const found = await this.atIndex(index)\n    // if found, remove it on this level\n    if (found?.isLeaf() && found.key === key) {\n      const prev = await this.atIndex(index - 1)\n      const next = await this.atIndex(index + 1)\n      if (prev?.isTree() && next?.isTree()) {\n        const merged = await prev.appendMerge(next)\n        return this.newTree([\n          ...(await this.slice(0, index - 1)),\n          merged,\n          ...(await this.slice(index + 2)),\n        ])\n      } else {\n        return this.removeEntry(index)\n      }\n    }\n    // else recurse down to find it\n    const prev = await this.atIndex(index - 1)\n    if (prev?.isTree()) {\n      const subtree = await prev.deleteRecurse(key)\n      const subTreeEntries = await subtree.getEntries()\n      if (subTreeEntries.length === 0) {\n        return this.removeEntry(index - 1)\n      } else {\n        return this.updateEntry(index - 1, subtree)\n      }\n    } else {\n      throw new Error(`Could not find a record with key: ${key}`)\n    }\n  }\n\n  // Simple Operations\n  // -------------------\n\n  // update entry in place\n  async updateEntry(index: number, entry: NodeEntry): Promise<MST> {\n    const update = [\n      ...(await this.slice(0, index)),\n      entry,\n      ...(await this.slice(index + 1)),\n    ]\n    return this.newTree(update)\n  }\n\n  // remove entry at index\n  async removeEntry(index: number): Promise<MST> {\n    const updated = [\n      ...(await this.slice(0, index)),\n      ...(await this.slice(index + 1)),\n    ]\n    return this.newTree(updated)\n  }\n\n  // append entry to end of the node\n  async append(entry: NodeEntry): Promise<MST> {\n    const entries = await this.getEntries()\n    return this.newTree([...entries, entry])\n  }\n\n  // prepend entry to start of the node\n  async prepend(entry: NodeEntry): Promise<MST> {\n    const entries = await this.getEntries()\n    return this.newTree([entry, ...entries])\n  }\n\n  // returns entry at index\n  async atIndex(index: number): Promise<NodeEntry | null> {\n    const entries = await this.getEntries()\n    return entries[index] ?? null\n  }\n\n  // returns a slice of the node (like array.slice)\n  async slice(\n    start?: number | undefined,\n    end?: number | undefined,\n  ): Promise<NodeEntry[]> {\n    const entries = await this.getEntries()\n    return entries.slice(start, end)\n  }\n\n  // inserts entry at index\n  async spliceIn(entry: NodeEntry, index: number): Promise<MST> {\n    const update = [\n      ...(await this.slice(0, index)),\n      entry,\n      ...(await this.slice(index)),\n    ]\n    return this.newTree(update)\n  }\n\n  // replaces an entry with [ Maybe(tree), Leaf, Maybe(tree) ]\n  async replaceWithSplit(\n    index: number,\n    left: MST | null,\n    leaf: Leaf,\n    right: MST | null,\n  ): Promise<MST> {\n    const update = await this.slice(0, index)\n    if (left) update.push(left)\n    update.push(leaf)\n    if (right) update.push(right)\n    update.push(...(await this.slice(index + 1)))\n    return this.newTree(update)\n  }\n\n  // if the topmost node in the tree only points to another tree, trim the top and return the subtree\n  async trimTop(): Promise<MST> {\n    let entries: NodeEntry[]\n    try {\n      entries = await this.getEntries()\n    } catch (err) {\n      if (err instanceof MissingBlockError) {\n        return this\n      } else {\n        throw err\n      }\n    }\n    if (entries.length === 1 && entries[0].isTree()) {\n      return entries[0].trimTop()\n    } else {\n      return this\n    }\n  }\n\n  // Subtree & Splits\n  // -------------------\n\n  // Recursively splits a sub tree around a given key\n  async splitAround(key: string): Promise<[MST | null, MST | null]> {\n    const index = await this.findGtOrEqualLeafIndex(key)\n    // split tree around key\n    const leftData = await this.slice(0, index)\n    const rightData = await this.slice(index)\n    let left = await this.newTree(leftData)\n    let right = await this.newTree(rightData)\n\n    // if the far right of the left side is a subtree,\n    // we need to split it on the key as well\n    const lastInLeft = leftData[leftData.length - 1]\n    if (lastInLeft?.isTree()) {\n      left = await left.removeEntry(leftData.length - 1)\n      const split = await lastInLeft.splitAround(key)\n      if (split[0]) {\n        left = await left.append(split[0])\n      }\n      if (split[1]) {\n        right = await right.prepend(split[1])\n      }\n    }\n\n    return [\n      (await left.getEntries()).length > 0 ? left : null,\n      (await right.getEntries()).length > 0 ? right : null,\n    ]\n  }\n\n  // The simple merge case where every key in the right tree is greater than every key in the left tree\n  // (used primarily for deletes)\n  async appendMerge(toMerge: MST): Promise<MST> {\n    if ((await this.getLayer()) !== (await toMerge.getLayer())) {\n      throw new Error(\n        'Trying to merge two nodes from different layers of the MST',\n      )\n    }\n    const thisEntries = await this.getEntries()\n    const toMergeEntries = await toMerge.getEntries()\n    const lastInLeft = thisEntries[thisEntries.length - 1]\n    const firstInRight = toMergeEntries[0]\n    if (lastInLeft?.isTree() && firstInRight?.isTree()) {\n      const merged = await lastInLeft.appendMerge(firstInRight)\n      return this.newTree([\n        ...thisEntries.slice(0, thisEntries.length - 1),\n        merged,\n        ...toMergeEntries.slice(1),\n      ])\n    } else {\n      return this.newTree([...thisEntries, ...toMergeEntries])\n    }\n  }\n\n  // Create relatives\n  // -------------------\n\n  async createChild(): Promise<MST> {\n    const layer = await this.getLayer()\n    return MST.create(this.storage, [], {\n      layer: layer - 1,\n    })\n  }\n\n  async createParent(): Promise<MST> {\n    const layer = await this.getLayer()\n    const parent = await MST.create(this.storage, [this], {\n      layer: layer + 1,\n    })\n    parent.outdatedPointer = true\n    return parent\n  }\n\n  // Finding insertion points\n  // -------------------\n\n  // finds index of first leaf node that is greater than or equal to the value\n  async findGtOrEqualLeafIndex(key: string): Promise<number> {\n    const entries = await this.getEntries()\n    const maybeIndex = entries.findIndex(\n      (entry) => entry.isLeaf() && entry.key >= key,\n    )\n    // if we can't find, we're on the end\n    return maybeIndex >= 0 ? maybeIndex : entries.length\n  }\n\n  // List operations (partial tree traversal)\n  // -------------------\n\n  // @TODO write tests for these\n\n  // Walk tree starting at key\n  async *walkFrom(key: string): AsyncIterable<NodeEntry> {\n    yield this\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const entries = await this.getEntries()\n    const found = entries[index]\n    if (found && found.isLeaf() && found.key === key) {\n      yield found\n    } else {\n      const prev = entries[index - 1]\n      if (prev) {\n        if (prev.isLeaf() && prev.key === key) {\n          yield prev\n        } else if (prev.isTree()) {\n          yield* prev.walkFrom(key)\n        }\n      }\n    }\n\n    for (let i = index; i < entries.length; i++) {\n      const entry = entries[i]\n      if (entry.isLeaf()) {\n        yield entry\n      } else {\n        yield* entry.walkFrom(key)\n      }\n    }\n  }\n\n  async *walkLeavesFrom(key: string): AsyncIterable<Leaf> {\n    for await (const node of this.walkFrom(key)) {\n      if (node.isLeaf()) {\n        yield node\n      }\n    }\n  }\n\n  async list(\n    count = Number.MAX_SAFE_INTEGER,\n    after?: string,\n    before?: string,\n  ): Promise<Leaf[]> {\n    const vals: Leaf[] = []\n    for await (const leaf of this.walkLeavesFrom(after || '')) {\n      if (leaf.key === after) continue\n      if (vals.length >= count) break\n      if (before && leaf.key >= before) break\n      vals.push(leaf)\n    }\n    return vals\n  }\n\n  async listWithPrefix(\n    prefix: string,\n    count = Number.MAX_SAFE_INTEGER,\n  ): Promise<Leaf[]> {\n    const vals: Leaf[] = []\n    for await (const leaf of this.walkLeavesFrom(prefix)) {\n      if (vals.length >= count || !leaf.key.startsWith(prefix)) break\n      vals.push(leaf)\n    }\n    return vals\n  }\n\n  // Full tree traversal\n  // -------------------\n\n  // Walk full tree & emit nodes, consumer can bail at any point by returning false\n  async *walk(): AsyncIterable<NodeEntry> {\n    yield this\n    const entries = await this.getEntries()\n    for (const entry of entries) {\n      if (entry.isTree()) {\n        for await (const e of entry.walk()) {\n          yield e\n        }\n      } else {\n        yield entry\n      }\n    }\n  }\n\n  // Walk full tree & emit nodes, consumer can bail at any point by returning false\n  async paths(): Promise<NodeEntry[][]> {\n    const entries = await this.getEntries()\n    let paths: NodeEntry[][] = []\n    for (const entry of entries) {\n      if (entry.isLeaf()) {\n        paths.push([entry])\n      }\n      if (entry.isTree()) {\n        const subPaths = await entry.paths()\n        paths = [...paths, ...subPaths.map((p) => [entry, ...p])]\n      }\n    }\n    return paths\n  }\n\n  // Walks tree & returns all nodes\n  async allNodes(): Promise<NodeEntry[]> {\n    const nodes: NodeEntry[] = []\n    for await (const entry of this.walk()) {\n      nodes.push(entry)\n    }\n    return nodes\n  }\n\n  // Walks tree & returns all cids\n  async allCids(): Promise<CidSet> {\n    const cids = new CidSet()\n    const entries = await this.getEntries()\n    for (const entry of entries) {\n      if (entry.isLeaf()) {\n        cids.add(entry.value)\n      } else {\n        const subtreeCids = await entry.allCids()\n        cids.addSet(subtreeCids)\n      }\n    }\n    cids.add(await this.getPointer())\n    return cids\n  }\n\n  // Walks tree & returns all leaves\n  async leaves() {\n    const leaves: Leaf[] = []\n    for await (const entry of this.walk()) {\n      if (entry.isLeaf()) leaves.push(entry)\n    }\n    return leaves\n  }\n\n  // Returns total leaf count\n  async leafCount(): Promise<number> {\n    const leaves = await this.leaves()\n    return leaves.length\n  }\n\n  // Reachable tree traversal\n  // -------------------\n\n  // Walk reachable branches of tree & emit nodes, consumer can bail at any point by returning false\n  async *walkReachable(): AsyncIterable<NodeEntry> {\n    yield this\n    const entries = await this.getEntries()\n    for (const entry of entries) {\n      if (entry.isTree()) {\n        try {\n          for await (const e of entry.walkReachable()) {\n            yield e\n          }\n        } catch (err) {\n          if (err instanceof MissingBlockError) {\n            continue\n          } else {\n            throw err\n          }\n        }\n      } else {\n        yield entry\n      }\n    }\n  }\n\n  async reachableLeaves(): Promise<Leaf[]> {\n    const leaves: Leaf[] = []\n    for await (const entry of this.walkReachable()) {\n      if (entry.isLeaf()) leaves.push(entry)\n    }\n    return leaves\n  }\n\n  // Sync Protocol\n\n  async *carBlockStream(): AsyncIterable<CarBlock> {\n    const leaves = new CidSet()\n    let toFetch = new CidSet()\n    toFetch.add(await this.getPointer())\n    while (toFetch.size() > 0) {\n      const nextLayer = new CidSet()\n      const fetched = await this.storage.getBlocks(toFetch.toList())\n      if (fetched.missing.length > 0) {\n        throw new MissingBlocksError('mst node', fetched.missing)\n      }\n      for (const cid of toFetch.toList()) {\n        const found = await parse.getAndParseByDef(\n          fetched.blocks,\n          cid,\n          nodeDataDef,\n        )\n        yield { cid, bytes: found.bytes }\n        const entries = await util.deserializeNodeData(this.storage, found.obj)\n\n        for (const entry of entries) {\n          if (entry.isLeaf()) {\n            leaves.add(entry.value)\n          } else {\n            nextLayer.add(await entry.getPointer())\n          }\n        }\n      }\n      toFetch = nextLayer\n    }\n    const leafData = await this.storage.getBlocks(leaves.toList())\n    if (leafData.missing.length > 0) {\n      throw new MissingBlocksError('mst leaf', leafData.missing)\n    }\n\n    for (const leaf of leafData.blocks.entries()) {\n      yield leaf\n    }\n  }\n\n  async cidsForPath(key: string): Promise<CID[]> {\n    const cids: CID[] = [await this.getPointer()]\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const found = await this.atIndex(index)\n    if (found && found.isLeaf() && found.key === key) {\n      return [...cids, found.value]\n    }\n    const prev = await this.atIndex(index - 1)\n    if (prev && prev.isTree()) {\n      return [...cids, ...(await prev.cidsForPath(key))]\n    }\n    return cids\n  }\n\n  // A covering proof is all MST nodes (leaves excluded) needed to prove the value of a given leaf\n  // and its siblings to its immediate right and left (if applicable)\n  // We simply find the immediately preceeding node and then walk from that node until we reach the\n  // first key that is greater than the requested key (the right sibling)\n  async getCoveringProof(key: string): Promise<BlockMap> {\n    const [self, left, right] = await Promise.all([\n      this.proofForKey(key),\n      this.proofForLeftSib(key),\n      this.proofForRightSib(key),\n    ])\n    return self.addMap(left).addMap(right)\n  }\n\n  async proofForKey(key: string): Promise<BlockMap> {\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const found = await this.atIndex(index)\n    let blocks: BlockMap\n    if (found && found.isLeaf() && found.key === key) {\n      blocks = new BlockMap()\n    } else {\n      const prev = await this.atIndex(index - 1)\n      if (!prev || prev.isLeaf()) {\n        return new BlockMap()\n      } else {\n        blocks = await prev.proofForKey(key)\n      }\n    }\n    const serialized = await this.serialize()\n    return blocks.set(serialized.cid, serialized.bytes)\n  }\n\n  async proofForLeftSib(key: string): Promise<BlockMap> {\n    const index = await this.findGtOrEqualLeafIndex(key)\n    const prev = await this.atIndex(index - 1)\n    let blocks: BlockMap\n    if (!prev || prev.isLeaf()) {\n      blocks = new BlockMap()\n    } else {\n      blocks = await prev.proofForLeftSib(key)\n    }\n    const serialized = await this.serialize()\n    return blocks.set(serialized.cid, serialized.bytes)\n  }\n\n  async proofForRightSib(key: string): Promise<BlockMap> {\n    const index = await this.findGtOrEqualLeafIndex(key)\n    let found = await this.atIndex(index)\n    if (!found) {\n      found = await this.atIndex(index - 1)\n    }\n    let blocks: BlockMap\n    if (!found) {\n      // shouldn't ever hit, null case\n      blocks = new BlockMap()\n    } else if (found.isTree()) {\n      blocks = await found.proofForRightSib(key)\n      // recurse down\n    } else {\n      const node =\n        found.key === key\n          ? await this.atIndex(index + 1)\n          : await this.atIndex(index - 1)\n      if (!node || node.isLeaf()) {\n        blocks = new BlockMap()\n      } else {\n        blocks = await node.proofForRightSib(key)\n      }\n    }\n    const serialized = await this.serialize()\n    return blocks.set(serialized.cid, serialized.bytes)\n  }\n\n  // Matching Leaf interface\n  // -------------------\n\n  isTree(): this is MST {\n    return true\n  }\n\n  isLeaf(): this is Leaf {\n    return false\n  }\n\n  async equals(other: NodeEntry): Promise<boolean> {\n    if (other.isLeaf()) return false\n    const thisPointer = await this.getPointer()\n    const otherPointer = await other.getPointer()\n    return thisPointer.equals(otherPointer)\n  }\n}\n\nexport class Leaf {\n  constructor(\n    public key: string,\n    public value: CID,\n  ) {}\n\n  isTree(): this is MST {\n    return false\n  }\n\n  isLeaf(): this is Leaf {\n    return true\n  }\n\n  equals(entry: NodeEntry): boolean {\n    if (entry.isLeaf()) {\n      return this.key === entry.key && this.value.equals(entry.value)\n    } else {\n      return false\n    }\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/mst/util.ts",
    "content": "import { CID } from 'multiformats'\nimport * as uint8arrays from 'uint8arrays'\nimport { cidForCbor } from '@atproto/common'\nimport { sha256 } from '@atproto/crypto'\nimport { ReadableBlockstore } from '../storage'\nimport { Leaf, MST, MstOpts, NodeData, NodeEntry } from './mst'\n\nexport const leadingZerosOnHash = async (key: string | Uint8Array) => {\n  const hash = await sha256(key)\n  let leadingZeros = 0\n  for (let i = 0; i < hash.length; i++) {\n    const byte = hash[i]\n    if (byte < 64) leadingZeros++\n    if (byte < 16) leadingZeros++\n    if (byte < 4) leadingZeros++\n    if (byte === 0) {\n      leadingZeros++\n    } else {\n      break\n    }\n  }\n  return leadingZeros\n}\n\nexport const layerForEntries = async (\n  entries: NodeEntry[],\n): Promise<number | null> => {\n  const firstLeaf = entries.find((entry) => entry.isLeaf())\n  if (!firstLeaf || firstLeaf.isTree()) return null\n  return await leadingZerosOnHash(firstLeaf.key)\n}\n\nexport const deserializeNodeData = async (\n  storage: ReadableBlockstore,\n  data: NodeData,\n  opts?: Partial<MstOpts>,\n): Promise<NodeEntry[]> => {\n  const { layer } = opts || {}\n  const entries: NodeEntry[] = []\n  if (data.l !== null) {\n    entries.push(\n      await MST.load(storage, data.l, {\n        layer: layer ? layer - 1 : undefined,\n      }),\n    )\n  }\n  let lastKey = ''\n  for (const entry of data.e) {\n    const keyStr = uint8arrays.toString(entry.k, 'ascii')\n    const key = lastKey.slice(0, entry.p) + keyStr\n    ensureValidMstKey(key)\n    entries.push(new Leaf(key, entry.v))\n    lastKey = key\n    if (entry.t !== null) {\n      entries.push(\n        await MST.load(storage, entry.t, {\n          layer: layer ? layer - 1 : undefined,\n        }),\n      )\n    }\n  }\n  return entries\n}\n\nexport const serializeNodeData = (entries: NodeEntry[]): NodeData => {\n  const data: NodeData = {\n    l: null,\n    e: [],\n  }\n  let i = 0\n  if (entries[0]?.isTree()) {\n    i++\n    data.l = entries[0].pointer\n  }\n  let lastKey = ''\n  while (i < entries.length) {\n    const leaf = entries[i]\n    const next = entries[i + 1]\n    if (!leaf.isLeaf()) {\n      throw new Error('Not a valid node: two subtrees next to each other')\n    }\n    i++\n    let subtree: CID | null = null\n    if (next?.isTree()) {\n      subtree = next.pointer\n      i++\n    }\n    ensureValidMstKey(leaf.key)\n    const prefixLen = countPrefixLen(lastKey, leaf.key)\n    data.e.push({\n      p: prefixLen,\n      k: uint8arrays.fromString(leaf.key.slice(prefixLen), 'ascii'),\n      v: leaf.value,\n      t: subtree,\n    })\n\n    lastKey = leaf.key\n  }\n  return data\n}\n\nexport const countPrefixLen = (a: string, b: string): number => {\n  let i\n  for (i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) {\n      break\n    }\n  }\n  return i\n}\n\nexport const cidForEntries = async (entries: NodeEntry[]): Promise<CID> => {\n  const data = serializeNodeData(entries)\n  return cidForCbor(data)\n}\n\nexport const isValidMstKey = (str: string): boolean => {\n  const split = str.split('/')\n  return (\n    str.length <= 1024 &&\n    split.length === 2 &&\n    split[0].length > 0 &&\n    split[1].length > 0 &&\n    isValidChars(split[0]) &&\n    isValidChars(split[1])\n  )\n}\n\nexport const validCharsRegex = /^[a-zA-Z0-9_~\\-:.]*$/\n\nexport const isValidChars = (str: string): boolean => {\n  return str.match(validCharsRegex) !== null\n}\n\nexport const ensureValidMstKey = (str: string) => {\n  if (!isValidMstKey(str)) {\n    throw new InvalidMstKeyError(str)\n  }\n}\n\nexport class InvalidMstKeyError extends Error {\n  constructor(public key: string) {\n    super(`Not a valid MST key: ${key}`)\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/mst/walker.ts",
    "content": "import { MST, NodeEntry } from './mst'\n\ntype WalkerStatusDone = {\n  done: true\n}\n\ntype WalkerStatusProgress = {\n  done: false\n  curr: NodeEntry\n  walking: MST | null // walking set to null if `curr` is the root of the tree\n  index: number\n}\n\ntype WalkerStatus = WalkerStatusDone | WalkerStatusProgress\n\nexport class MstWalker {\n  stack: WalkerStatus[] = []\n  status: WalkerStatus\n\n  constructor(public root: MST) {\n    this.status = {\n      done: false,\n      curr: root,\n      walking: null,\n      index: 0,\n    }\n  }\n\n  // return the current layer of the node you are walking\n  layer(): number {\n    if (this.status.done) {\n      throw new Error('Walk is done')\n    }\n    if (this.status.walking) {\n      return this.status.walking.layer ?? 0\n    }\n    // if curr is the root of the tree, add 1\n    if (this.status.curr.isTree()) {\n      return (this.status.curr.layer ?? 0) + 1\n    }\n    throw new Error('Could not identify layer of walk')\n  }\n\n  // move to the next node in the subtree, skipping over the subtree\n  async stepOver(): Promise<void> {\n    if (this.status.done) return\n    // if stepping over the root of the node, we're done\n    if (this.status.walking === null) {\n      this.status = { done: true }\n      return\n    }\n    const entries = await this.status.walking.getEntries()\n    this.status.index++\n    const next = entries[this.status.index]\n    if (!next) {\n      const popped = this.stack.pop()\n      if (!popped) {\n        this.status = { done: true }\n        return\n      } else {\n        this.status = popped\n        await this.stepOver()\n        return\n      }\n    } else {\n      this.status.curr = next\n    }\n  }\n\n  // step into a subtree, throws if currently pointed at a leaf\n  async stepInto(): Promise<void> {\n    if (this.status.done) return\n    // edge case for very start of walk\n    if (this.status.walking === null) {\n      if (!this.status.curr.isTree()) {\n        throw new Error('The root of the tree cannot be a leaf')\n      }\n      const next = await this.status.curr.atIndex(0)\n      if (!next) {\n        this.status = { done: true }\n      } else {\n        this.status = {\n          done: false,\n          walking: this.status.curr,\n          curr: next,\n          index: 0,\n        }\n      }\n      return\n    }\n    if (!this.status.curr.isTree()) {\n      throw new Error('No tree at pointer, cannot step into')\n    }\n\n    const next = await this.status.curr.atIndex(0)\n    if (!next) {\n      throw new Error(\n        'Tried to step into a node with 0 entries which is invalid',\n      )\n    }\n\n    this.stack.push({ ...this.status })\n    this.status.walking = this.status.curr\n    this.status.curr = next\n    this.status.index = 0\n  }\n\n  // advance the pointer to the next node in the tree,\n  // stepping into the current node if necessary\n  async advance(): Promise<void> {\n    if (this.status.done) return\n    if (this.status.curr.isLeaf()) {\n      await this.stepOver()\n    } else {\n      await this.stepInto()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/parse.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { cborDecode, check } from '@atproto/common'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { BlockMap } from './block-map'\nimport { MissingBlockError, UnexpectedObjectError } from './error'\nimport { cborToLexRecord } from './util'\n\nexport const getAndParseRecord = async (\n  blocks: BlockMap,\n  cid: CID,\n): Promise<{ record: RepoRecord; bytes: Uint8Array }> => {\n  const bytes = blocks.get(cid)\n  if (!bytes) {\n    throw new MissingBlockError(cid, 'record')\n  }\n  const record = cborToLexRecord(bytes)\n  return { record, bytes }\n}\n\nexport const getAndParseByDef = async <T>(\n  blocks: BlockMap,\n  cid: CID,\n  def: check.Def<T>,\n): Promise<{ obj: T; bytes: Uint8Array }> => {\n  const bytes = blocks.get(cid)\n  if (!bytes) {\n    throw new MissingBlockError(cid, def.name)\n  }\n  return parseObjByDef(bytes, cid, def)\n}\n\nexport const parseObjByDef = <T>(\n  bytes: Uint8Array,\n  cid: CID,\n  def: check.Def<T>,\n): { obj: T; bytes: Uint8Array } => {\n  const obj = cborDecode(bytes)\n  const res = def.schema.safeParse(obj)\n  if (res.success) {\n    return { obj: res.data, bytes }\n  } else {\n    throw new UnexpectedObjectError(cid, def.name)\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/readable-repo.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { MissingBlocksError } from './error'\nimport log from './logger'\nimport { MST } from './mst'\nimport * as parse from './parse'\nimport { ReadableBlockstore } from './storage'\nimport { Commit, RepoContents, def } from './types'\nimport * as util from './util'\n\ntype Params = {\n  storage: ReadableBlockstore\n  data: MST\n  commit: Commit\n  cid: CID\n}\n\nexport class ReadableRepo {\n  storage: ReadableBlockstore\n  data: MST\n  commit: Commit\n  cid: CID\n\n  constructor(params: Params) {\n    this.storage = params.storage\n    this.data = params.data\n    this.commit = params.commit\n    this.cid = params.cid\n  }\n\n  static async load(storage: ReadableBlockstore, commitCid: CID) {\n    const commit = await storage.readObj(commitCid, def.versionedCommit)\n    const data = await MST.load(storage, commit.data)\n    log.info({ did: commit.did }, 'loaded repo for')\n    return new ReadableRepo({\n      storage,\n      data,\n      commit: util.ensureV3Commit(commit),\n      cid: commitCid,\n    })\n  }\n\n  get did(): string {\n    return this.commit.did\n  }\n\n  get version(): number {\n    return this.commit.version\n  }\n\n  async *walkRecords(from?: string): AsyncIterable<{\n    collection: string\n    rkey: string\n    cid: CID\n    record: RepoRecord\n  }> {\n    for await (const leaf of this.data.walkLeavesFrom(from ?? '')) {\n      const { collection, rkey } = util.parseDataKey(leaf.key)\n      const record = await this.storage.readRecord(leaf.value)\n      yield { collection, rkey, cid: leaf.value, record }\n    }\n  }\n\n  async getRecord(collection: string, rkey: string): Promise<unknown | null> {\n    const dataKey = collection + '/' + rkey\n    const cid = await this.data.get(dataKey)\n    if (!cid) return null\n    return this.storage.readObj(cid, def.unknown)\n  }\n\n  async getContents(): Promise<RepoContents> {\n    const entries = await this.data.list()\n    const cids = entries.map((e) => e.value)\n    const { blocks, missing } = await this.storage.getBlocks(cids)\n    if (missing.length > 0) {\n      throw new MissingBlocksError('getContents record', missing)\n    }\n    const contents: RepoContents = {}\n    for (const entry of entries) {\n      const { collection, rkey } = util.parseDataKey(entry.key)\n      contents[collection] ??= {}\n      const parsed = await parse.getAndParseRecord(blocks, entry.value)\n      contents[collection][rkey] = parsed.record\n    }\n    return contents\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/repo.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { TID, dataToCborBlock } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { lexToIpld } from '@atproto/lexicon'\nimport { BlockMap } from './block-map'\nimport { CidSet } from './cid-set'\nimport { DataDiff } from './data-diff'\nimport log from './logger'\nimport { MST } from './mst'\nimport { ReadableRepo } from './readable-repo'\nimport { RepoStorage } from './storage'\nimport {\n  Commit,\n  CommitData,\n  RecordCreateOp,\n  RecordWriteOp,\n  WriteOpAction,\n  def,\n} from './types'\nimport * as util from './util'\n\ntype Params = {\n  storage: RepoStorage\n  data: MST\n  commit: Commit\n  cid: CID\n}\n\nexport class Repo extends ReadableRepo {\n  storage: RepoStorage\n\n  constructor(params: Params) {\n    super(params)\n    this.storage = params.storage\n  }\n\n  static async formatInitCommit(\n    storage: RepoStorage,\n    did: string,\n    keypair: crypto.Keypair,\n    initialWrites: RecordCreateOp[] = [],\n    revOverride?: string,\n  ): Promise<CommitData> {\n    const newBlocks = new BlockMap()\n\n    let data = await MST.create(storage)\n    for (const record of initialWrites) {\n      const cid = await newBlocks.add(record.record)\n      const dataKey = util.formatDataKey(record.collection, record.rkey)\n      data = await data.add(dataKey, cid)\n    }\n    const dataCid = await data.getPointer()\n    const diff = await DataDiff.of(data, null)\n    newBlocks.addMap(diff.newMstBlocks)\n\n    const rev = revOverride ?? TID.nextStr()\n    const commit = await util.signCommit(\n      {\n        did,\n        version: 3,\n        rev,\n        prev: null, // added for backwards compatibility with v2\n        data: dataCid,\n      },\n      keypair,\n    )\n    const commitCid = await newBlocks.add(commit)\n    return {\n      cid: commitCid,\n      rev,\n      since: null,\n      prev: null,\n      newBlocks,\n      relevantBlocks: newBlocks,\n      removedCids: diff.removedCids,\n    }\n  }\n\n  static async createFromCommit(\n    storage: RepoStorage,\n    commit: CommitData,\n  ): Promise<Repo> {\n    await storage.applyCommit(commit)\n    return Repo.load(storage, commit.cid)\n  }\n\n  static async create(\n    storage: RepoStorage,\n    did: string,\n    keypair: crypto.Keypair,\n    initialWrites: RecordCreateOp[] = [],\n  ): Promise<Repo> {\n    const commit = await Repo.formatInitCommit(\n      storage,\n      did,\n      keypair,\n      initialWrites,\n    )\n    return Repo.createFromCommit(storage, commit)\n  }\n\n  static async load(storage: RepoStorage, cid?: CID) {\n    const commitCid = cid || (await storage.getRoot())\n    if (!commitCid) {\n      throw new Error('No cid provided and none in storage')\n    }\n    const commit = await storage.readObj(commitCid, def.versionedCommit)\n    const data = await MST.load(storage, commit.data)\n    log.info({ did: commit.did }, 'loaded repo for')\n    return new Repo({\n      storage,\n      data,\n      commit: util.ensureV3Commit(commit),\n      cid: commitCid,\n    })\n  }\n\n  async formatCommit(\n    toWrite: RecordWriteOp | RecordWriteOp[],\n    keypair: crypto.Keypair,\n  ): Promise<CommitData> {\n    const writes = Array.isArray(toWrite) ? toWrite : [toWrite]\n    const leaves = new BlockMap()\n\n    let data = this.data\n    for (const write of writes) {\n      if (write.action === WriteOpAction.Create) {\n        const cid = await leaves.add(write.record)\n        const dataKey = write.collection + '/' + write.rkey\n        data = await data.add(dataKey, cid)\n      } else if (write.action === WriteOpAction.Update) {\n        const cid = await leaves.add(write.record)\n        const dataKey = write.collection + '/' + write.rkey\n        data = await data.update(dataKey, cid)\n      } else if (write.action === WriteOpAction.Delete) {\n        const dataKey = write.collection + '/' + write.rkey\n        data = await data.delete(dataKey)\n      }\n    }\n\n    const dataCid = await data.getPointer()\n    const diff = await DataDiff.of(data, this.data)\n    const newBlocks = diff.newMstBlocks\n    const removedCids = diff.removedCids\n\n    const proofs = await Promise.all(\n      writes.map((op) =>\n        data.getCoveringProof(util.formatDataKey(op.collection, op.rkey)),\n      ),\n    )\n    const relevantBlocks = new BlockMap()\n    for (const proof of proofs) relevantBlocks.addMap(proof)\n\n    const addedLeaves = leaves.getMany(diff.newLeafCids.toList())\n    if (addedLeaves.missing.length > 0) {\n      throw new Error(`Missing leaf blocks: ${addedLeaves.missing}`)\n    }\n    newBlocks.addMap(addedLeaves.blocks)\n    relevantBlocks.addMap(addedLeaves.blocks)\n\n    const rev = TID.nextStr(this.commit.rev)\n    const commit = await util.signCommit(\n      {\n        did: this.did,\n        version: 3,\n        rev,\n        prev: null, // added for backwards compatibility with v2\n        data: dataCid,\n      },\n      keypair,\n    )\n    const commitBlock = await dataToCborBlock(lexToIpld(commit))\n    if (!commitBlock.cid.equals(this.cid)) {\n      newBlocks.set(commitBlock.cid, commitBlock.bytes)\n      relevantBlocks.set(commitBlock.cid, commitBlock.bytes)\n      removedCids.add(this.cid)\n    }\n\n    return {\n      cid: commitBlock.cid,\n      rev,\n      since: this.commit.rev,\n      prev: this.cid,\n      newBlocks,\n      relevantBlocks,\n      removedCids,\n    }\n  }\n\n  async applyCommit(commitData: CommitData): Promise<Repo> {\n    await this.storage.applyCommit(commitData)\n    return Repo.load(this.storage, commitData.cid)\n  }\n\n  async applyWrites(\n    toWrite: RecordWriteOp | RecordWriteOp[],\n    keypair: crypto.Keypair,\n  ): Promise<Repo> {\n    const commit = await this.formatCommit(toWrite, keypair)\n    return this.applyCommit(commit)\n  }\n\n  async formatResignCommit(rev: string, keypair: crypto.Keypair) {\n    const commit = await util.signCommit(\n      {\n        did: this.did,\n        version: 3,\n        rev,\n        prev: null, // added for backwards compatibility with v2\n        data: this.commit.data,\n      },\n      keypair,\n    )\n    const newBlocks = new BlockMap()\n    const commitCid = await newBlocks.add(commit)\n    return {\n      cid: commitCid,\n      rev,\n      since: null,\n      prev: null,\n      newBlocks,\n      relevantBlocks: newBlocks,\n      removedCids: new CidSet([this.cid]),\n    }\n  }\n\n  async resignCommit(rev: string, keypair: crypto.Keypair) {\n    const formatted = await this.formatResignCommit(rev, keypair)\n    return this.applyCommit(formatted)\n  }\n}\n\nexport default Repo\n"
  },
  {
    "path": "packages/repo/src/storage/index.ts",
    "content": "export * from './readable-blockstore'\nexport * from './memory-blockstore'\nexport * from './sync-storage'\nexport * from './types'\n"
  },
  {
    "path": "packages/repo/src/storage/memory-blockstore.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { BlockMap } from '../block-map'\nimport { CommitData } from '../types'\nimport { ReadableBlockstore } from './readable-blockstore'\nimport { RepoStorage } from './types'\n\nexport class MemoryBlockstore\n  extends ReadableBlockstore\n  implements RepoStorage\n{\n  blocks: BlockMap\n  root: CID | null = null\n  rev: string | null = null\n\n  constructor(blocks?: BlockMap) {\n    super()\n    this.blocks = new BlockMap()\n    if (blocks) {\n      this.blocks.addMap(blocks)\n    }\n  }\n\n  async getRoot(): Promise<CID | null> {\n    return this.root\n  }\n\n  async getBytes(cid: CID): Promise<Uint8Array | null> {\n    return this.blocks.get(cid) || null\n  }\n\n  async has(cid: CID): Promise<boolean> {\n    return this.blocks.has(cid)\n  }\n\n  async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> {\n    return this.blocks.getMany(cids)\n  }\n\n  async putBlock(cid: CID, block: Uint8Array): Promise<void> {\n    this.blocks.set(cid, block)\n  }\n\n  async putMany(blocks: BlockMap): Promise<void> {\n    this.blocks.addMap(blocks)\n  }\n\n  async updateRoot(cid: CID, rev: string): Promise<void> {\n    this.root = cid\n    this.rev = rev\n  }\n\n  async applyCommit(commit: CommitData): Promise<void> {\n    this.root = commit.cid\n    const rmCids = commit.removedCids.toList()\n    for (const cid of rmCids) {\n      this.blocks.delete(cid)\n    }\n    commit.newBlocks.forEach((bytes, cid) => {\n      this.blocks.set(cid, bytes)\n    })\n  }\n\n  async sizeInBytes(): Promise<number> {\n    let total = 0\n    this.blocks.forEach((bytes) => {\n      total += bytes.byteLength\n    })\n    return total\n  }\n\n  async destroy(): Promise<void> {\n    this.blocks.clear()\n  }\n}\n\nexport default MemoryBlockstore\n"
  },
  {
    "path": "packages/repo/src/storage/readable-blockstore.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { check } from '@atproto/common'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { BlockMap } from '../block-map'\nimport { MissingBlockError } from '../error'\nimport * as parse from '../parse'\nimport { cborToLexRecord } from '../util'\n\nexport abstract class ReadableBlockstore {\n  abstract getBytes(cid: CID): Promise<Uint8Array | null>\n  abstract has(cid: CID): Promise<boolean>\n  abstract getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }>\n\n  async attemptRead<T>(\n    cid: CID,\n    def: check.Def<T>,\n  ): Promise<{ obj: T; bytes: Uint8Array } | null> {\n    const bytes = await this.getBytes(cid)\n    if (!bytes) return null\n    return parse.parseObjByDef(bytes, cid, def)\n  }\n\n  async readObjAndBytes<T>(\n    cid: CID,\n    def: check.Def<T>,\n  ): Promise<{ obj: T; bytes: Uint8Array }> {\n    const read = await this.attemptRead(cid, def)\n    if (!read) {\n      throw new MissingBlockError(cid, def.name)\n    }\n    return read\n  }\n\n  async readObj<T>(cid: CID, def: check.Def<T>): Promise<T> {\n    const obj = await this.readObjAndBytes(cid, def)\n    return obj.obj\n  }\n\n  async attemptReadRecord(cid: CID): Promise<RepoRecord | null> {\n    try {\n      return await this.readRecord(cid)\n    } catch {\n      return null\n    }\n  }\n\n  async readRecord(cid: CID): Promise<RepoRecord> {\n    const bytes = await this.getBytes(cid)\n    if (!bytes) {\n      throw new MissingBlockError(cid)\n    }\n    return cborToLexRecord(bytes)\n  }\n}\n\nexport default ReadableBlockstore\n"
  },
  {
    "path": "packages/repo/src/storage/sync-storage.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { BlockMap } from '../block-map'\nimport { ReadableBlockstore } from './readable-blockstore'\n\nexport class SyncStorage extends ReadableBlockstore {\n  constructor(\n    public staged: ReadableBlockstore,\n    public saved: ReadableBlockstore,\n  ) {\n    super()\n  }\n\n  async getBytes(cid: CID): Promise<Uint8Array | null> {\n    const got = await this.staged.getBytes(cid)\n    if (got) return got\n    return this.saved.getBytes(cid)\n  }\n\n  async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> {\n    const fromStaged = await this.staged.getBlocks(cids)\n    const fromSaved = await this.saved.getBlocks(fromStaged.missing)\n    const blocks = fromStaged.blocks\n    blocks.addMap(fromSaved.blocks)\n    return {\n      blocks,\n      missing: fromSaved.missing,\n    }\n  }\n\n  async has(cid: CID): Promise<boolean> {\n    return (await this.staged.has(cid)) || (await this.saved.has(cid))\n  }\n}\n\nexport default SyncStorage\n"
  },
  {
    "path": "packages/repo/src/storage/types.ts",
    "content": "import stream from 'node:stream'\nimport { CID } from 'multiformats/cid'\nimport { check } from '@atproto/common'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { BlockMap } from '../block-map'\nimport { CommitData } from '../types'\n\nexport interface RepoStorage {\n  // Writable\n  getRoot(): Promise<CID | null>\n  putBlock(cid: CID, block: Uint8Array, rev: string): Promise<void>\n  putMany(blocks: BlockMap, rev: string): Promise<void>\n  updateRoot(cid: CID, rev: string): Promise<void>\n  applyCommit(commit: CommitData)\n\n  // Readable\n  getBytes(cid: CID): Promise<Uint8Array | null>\n  has(cid: CID): Promise<boolean>\n  getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }>\n  attemptRead<T>(\n    cid: CID,\n    def: check.Def<T>,\n  ): Promise<{ obj: T; bytes: Uint8Array } | null>\n  readObjAndBytes<T>(\n    cid: CID,\n    def: check.Def<T>,\n  ): Promise<{ obj: T; bytes: Uint8Array }>\n  readObj<T>(cid: CID, def: check.Def<T>): Promise<T>\n  attemptReadRecord(cid: CID): Promise<RepoRecord | null>\n  readRecord(cid: CID): Promise<RepoRecord>\n}\n\nexport interface BlobStore {\n  putTemp(bytes: Uint8Array | stream.Readable): Promise<string>\n  makePermanent(key: string, cid: CID): Promise<void>\n  putPermanent(cid: CID, bytes: Uint8Array | stream.Readable): Promise<void>\n  quarantine(cid: CID): Promise<void>\n  unquarantine(cid: CID): Promise<void>\n  getBytes(cid: CID): Promise<Uint8Array>\n  getStream(cid: CID): Promise<stream.Readable>\n  hasTemp(key: string): Promise<boolean>\n  hasStored(cid: CID): Promise<boolean>\n  delete(cid: CID): Promise<void>\n  deleteMany(cid: CID[]): Promise<void>\n}\n\nexport class BlobNotFoundError extends Error {}\n"
  },
  {
    "path": "packages/repo/src/sync/consumer.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { BlockMap } from '../block-map'\nimport { readCarWithRoot } from '../car'\nimport { DataDiff } from '../data-diff'\nimport { MST } from '../mst'\nimport { ReadableRepo } from '../readable-repo'\nimport { MemoryBlockstore, ReadableBlockstore, SyncStorage } from '../storage'\nimport {\n  RecordCidClaim,\n  RecordClaim,\n  VerifiedDiff,\n  VerifiedRepo,\n  def,\n} from '../types'\nimport * as util from '../util'\n\nexport const verifyRepoCar = async (\n  carBytes: Uint8Array,\n  did?: string,\n  signingKey?: string,\n): Promise<VerifiedRepo> => {\n  const car = await readCarWithRoot(carBytes)\n  return verifyRepo(car.blocks, car.root, did, signingKey)\n}\n\nexport const verifyRepo = async (\n  blocks: BlockMap,\n  head: CID,\n  did?: string,\n  signingKey?: string,\n  opts?: { ensureLeaves?: boolean },\n): Promise<VerifiedRepo> => {\n  const diff = await verifyDiff(null, blocks, head, did, signingKey, opts)\n  const creates = util.ensureCreates(diff.writes)\n  return {\n    creates,\n    commit: diff.commit,\n  }\n}\n\nexport const verifyDiffCar = async (\n  repo: ReadableRepo | null,\n  carBytes: Uint8Array,\n  did?: string,\n  signingKey?: string,\n  opts?: { ensureLeaves?: boolean },\n): Promise<VerifiedDiff> => {\n  const car = await readCarWithRoot(carBytes)\n  return verifyDiff(repo, car.blocks, car.root, did, signingKey, opts)\n}\n\nexport const verifyDiff = async (\n  repo: ReadableRepo | null,\n  updateBlocks: BlockMap,\n  updateRoot: CID,\n  did?: string,\n  signingKey?: string,\n  opts?: { ensureLeaves?: boolean },\n): Promise<VerifiedDiff> => {\n  const { ensureLeaves = true } = opts ?? {}\n  const stagedStorage = new MemoryBlockstore(updateBlocks)\n  const updateStorage = repo\n    ? new SyncStorage(stagedStorage, repo.storage)\n    : stagedStorage\n  const updated = await verifyRepoRoot(\n    updateStorage,\n    updateRoot,\n    did,\n    signingKey,\n  )\n  const diff = await DataDiff.of(updated.data, repo?.data ?? null)\n  const writes = await util.diffToWriteDescripts(diff)\n  const newBlocks = diff.newMstBlocks\n  const leaves = updateBlocks.getMany(diff.newLeafCids.toList())\n  if (leaves.missing.length > 0 && ensureLeaves) {\n    throw new Error(`missing leaf blocks: ${leaves.missing}`)\n  }\n  newBlocks.addMap(leaves.blocks)\n  const removedCids = diff.removedCids\n  const commitCid = await newBlocks.add(updated.commit)\n  // ensure the commit cid actually changed\n  if (repo) {\n    if (commitCid.equals(repo.cid)) {\n      newBlocks.delete(commitCid)\n    } else {\n      removedCids.add(repo.cid)\n    }\n  }\n  return {\n    writes,\n    commit: {\n      cid: updated.cid,\n      rev: updated.commit.rev,\n      prev: repo?.cid ?? null,\n      since: repo?.commit.rev ?? null,\n      newBlocks,\n      relevantBlocks: newBlocks,\n      removedCids,\n    },\n  }\n}\n\n// @NOTE only verifies the root, not the repo contents\nconst verifyRepoRoot = async (\n  storage: ReadableBlockstore,\n  head: CID,\n  did?: string,\n  signingKey?: string,\n): Promise<ReadableRepo> => {\n  const repo = await ReadableRepo.load(storage, head)\n  if (did !== undefined && repo.did !== did) {\n    throw new RepoVerificationError(`Invalid repo did: ${repo.did}`)\n  }\n  if (signingKey !== undefined) {\n    const validSig = await util.verifyCommitSig(repo.commit, signingKey)\n    if (!validSig) {\n      throw new RepoVerificationError(\n        `Invalid signature on commit: ${repo.cid.toString()}`,\n      )\n    }\n  }\n  return repo\n}\n\nexport const verifyProofs = async (\n  proofs: Uint8Array,\n  claims: RecordCidClaim[],\n  did: string,\n  didKey: string,\n): Promise<{ verified: RecordCidClaim[]; unverified: RecordCidClaim[] }> => {\n  const car = await readCarWithRoot(proofs)\n  const blockstore = new MemoryBlockstore(car.blocks)\n  const commit = await blockstore.readObj(car.root, def.commit)\n  if (commit.did !== did) {\n    throw new RepoVerificationError(`Invalid repo did: ${commit.did}`)\n  }\n  const validSig = await util.verifyCommitSig(commit, didKey)\n  if (!validSig) {\n    throw new RepoVerificationError(\n      `Invalid signature on commit: ${car.root.toString()}`,\n    )\n  }\n  const mst = MST.load(blockstore, commit.data)\n  const verified: RecordCidClaim[] = []\n  const unverified: RecordCidClaim[] = []\n  for (const claim of claims) {\n    const found = await mst.get(\n      util.formatDataKey(claim.collection, claim.rkey),\n    )\n    const record = found ? await blockstore.readObj(found, def.map) : null\n    if (claim.cid === null) {\n      if (record === null) {\n        verified.push(claim)\n      } else {\n        unverified.push(claim)\n      }\n    } else {\n      if (claim.cid.equals(found)) {\n        verified.push(claim)\n      } else {\n        unverified.push(claim)\n      }\n    }\n  }\n  return { verified, unverified }\n}\n\nexport const verifyRecords = async (\n  proofs: Uint8Array,\n  did: string,\n  signingKey: string,\n): Promise<RecordClaim[]> => {\n  const car = await readCarWithRoot(proofs)\n  const blockstore = new MemoryBlockstore(car.blocks)\n  const commit = await blockstore.readObj(car.root, def.commit)\n  if (commit.did !== did) {\n    throw new RepoVerificationError(`Invalid repo did: ${commit.did}`)\n  }\n  const validSig = await util.verifyCommitSig(commit, signingKey)\n  if (!validSig) {\n    throw new RepoVerificationError(\n      `Invalid signature on commit: ${car.root.toString()}`,\n    )\n  }\n  const mst = MST.load(blockstore, commit.data)\n\n  const records: RecordClaim[] = []\n  const leaves = await mst.reachableLeaves()\n  for (const leaf of leaves) {\n    const { collection, rkey } = util.parseDataKey(leaf.key)\n    const record = await blockstore.attemptReadRecord(leaf.value)\n    if (record) {\n      records.push({\n        collection,\n        rkey,\n        record,\n      })\n    }\n  }\n  return records\n}\n\nexport class RepoVerificationError extends Error {}\n"
  },
  {
    "path": "packages/repo/src/sync/index.ts",
    "content": "export * from './consumer'\nexport * from './provider'\n"
  },
  {
    "path": "packages/repo/src/sync/provider.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport { writeCarStream } from '../car'\nimport { CidSet } from '../cid-set'\nimport { MissingBlocksError } from '../error'\nimport { MST } from '../mst'\nimport { ReadableBlockstore, RepoStorage } from '../storage'\nimport { RecordPath, def } from '../types'\nimport * as util from '../util'\n\n// Full Repo\n// -------------\n\nexport const getFullRepo = (\n  storage: RepoStorage,\n  commitCid: CID,\n): AsyncIterable<Uint8Array> => {\n  return writeCarStream(commitCid, iterateFullRepo(storage, commitCid))\n}\n\nasync function* iterateFullRepo(storage: RepoStorage, commitCid: CID) {\n  const commit = await storage.readObjAndBytes(commitCid, def.commit)\n  yield { cid: commitCid, bytes: commit.bytes }\n  const mst = MST.load(storage, commit.obj.data)\n  for await (const block of mst.carBlockStream()) {\n    yield block\n  }\n}\n\n// Narrow slices\n// -------------\n\nexport const getRecords = (\n  storage: ReadableBlockstore,\n  commitCid: CID,\n  paths: RecordPath[],\n): AsyncIterable<Uint8Array> => {\n  return writeCarStream(\n    commitCid,\n    iterateRecordBlocks(storage, commitCid, paths),\n  )\n}\n\nasync function* iterateRecordBlocks(\n  storage: ReadableBlockstore,\n  commitCid: CID,\n  paths: RecordPath[],\n) {\n  const commit = await storage.readObjAndBytes(commitCid, def.commit)\n  yield { cid: commitCid, bytes: commit.bytes }\n  const mst = MST.load(storage, commit.obj.data)\n  const cidsForPaths = await Promise.all(\n    paths.map((p) => mst.cidsForPath(util.formatDataKey(p.collection, p.rkey))),\n  )\n  const allCids = cidsForPaths.reduce((acc, cur) => {\n    return acc.addSet(new CidSet(cur))\n  }, new CidSet())\n  const found = await storage.getBlocks(allCids.toList())\n  if (found.missing.length > 0) {\n    throw new MissingBlocksError('writeRecordsToCarStream', found.missing)\n  }\n  for (const block of found.blocks.entries()) {\n    yield block\n  }\n}\n"
  },
  {
    "path": "packages/repo/src/types.ts",
    "content": "import { CID } from 'multiformats'\nimport { z } from 'zod'\nimport { schema as common } from '@atproto/common'\nimport { def as commonDef } from '@atproto/common-web'\nimport { RepoRecord } from '@atproto/lexicon'\nimport { BlockMap } from './block-map'\nimport { CidSet } from './cid-set'\n\n// Repo nodes\n// ---------------\n\nconst unsignedCommit = z.object({\n  did: z.string(),\n  version: z.literal(3),\n  data: common.cid,\n  rev: z.string(),\n  // `prev` added for backwards compatibility with v2, no requirement of keeping around history\n  prev: common.cid.nullable(),\n})\nexport type UnsignedCommit = z.infer<typeof unsignedCommit> & { sig?: never }\n\nconst commit = z.object({\n  did: z.string(),\n  version: z.literal(3),\n  data: common.cid,\n  rev: z.string(),\n  prev: common.cid.nullable(),\n  sig: common.bytes,\n})\nexport type Commit = z.infer<typeof commit>\n\nconst legacyV2Commit = z.object({\n  did: z.string(),\n  version: z.literal(2),\n  data: common.cid,\n  rev: z.string().optional(),\n  prev: common.cid.nullable(),\n  sig: common.bytes,\n})\nexport type LegacyV2Commit = z.infer<typeof legacyV2Commit>\n\nconst versionedCommit = z.discriminatedUnion('version', [\n  commit,\n  legacyV2Commit,\n])\nexport type VersionedCommit = z.infer<typeof versionedCommit>\n\nexport const schema = {\n  ...common,\n  commit,\n  legacyV2Commit,\n  versionedCommit,\n}\n\nexport const def = {\n  ...commonDef,\n  commit: {\n    name: 'commit',\n    schema: schema.commit,\n  },\n  versionedCommit: {\n    name: 'versioned_commit',\n    schema: schema.versionedCommit,\n  },\n}\n\n// Repo Operations\n// ---------------\n\nexport enum WriteOpAction {\n  Create = 'create',\n  Update = 'update',\n  Delete = 'delete',\n}\n\nexport type RecordCreateOp = {\n  action: WriteOpAction.Create\n  collection: string\n  rkey: string\n  record: RepoRecord\n}\n\nexport type RecordUpdateOp = {\n  action: WriteOpAction.Update\n  collection: string\n  rkey: string\n  record: RepoRecord\n}\n\nexport type RecordDeleteOp = {\n  action: WriteOpAction.Delete\n  collection: string\n  rkey: string\n}\n\nexport type RecordWriteOp = RecordCreateOp | RecordUpdateOp | RecordDeleteOp\n\nexport type RecordCreateDescript = {\n  action: WriteOpAction.Create\n  collection: string\n  rkey: string\n  cid: CID\n}\n\nexport type RecordUpdateDescript = {\n  action: WriteOpAction.Update\n  collection: string\n  rkey: string\n  prev: CID\n  cid: CID\n}\n\nexport type RecordDeleteDescript = {\n  action: WriteOpAction.Delete\n  collection: string\n  rkey: string\n  cid: CID\n}\n\nexport type RecordWriteDescript =\n  | RecordCreateDescript\n  | RecordUpdateDescript\n  | RecordDeleteDescript\n\nexport type WriteLog = RecordWriteDescript[][]\n\n// Updates/Commits\n// ---------------\n\nexport type CommitData = {\n  cid: CID\n  rev: string\n  since: string | null\n  prev: CID | null\n  newBlocks: BlockMap\n  relevantBlocks: BlockMap\n  removedCids: CidSet\n}\n\nexport type RepoUpdate = CommitData & {\n  ops: RecordWriteOp[]\n}\n\nexport type CollectionContents = Record<string, RepoRecord>\nexport type RepoContents = Record<string, CollectionContents>\n\nexport type RepoRecordWithCid = { cid: CID; value: RepoRecord }\nexport type CollectionContentsWithCids = Record<string, RepoRecordWithCid>\nexport type RepoContentsWithCids = Record<string, CollectionContentsWithCids>\n\nexport type DatastoreContents = Record<string, CID>\n\nexport type RecordPath = {\n  collection: string\n  rkey: string\n}\n\nexport type RecordCidClaim = {\n  collection: string\n  rkey: string\n  cid: CID | null\n}\n\nexport type RecordClaim = {\n  collection: string\n  rkey: string\n  record: RepoRecord | null\n}\n\n// Sync\n// ---------------\n\nexport type VerifiedDiff = {\n  writes: RecordWriteDescript[]\n  commit: CommitData\n}\n\nexport type VerifiedRepo = {\n  creates: RecordCreateDescript[]\n  commit: CommitData\n}\n\nexport type CarBlock = {\n  cid: CID\n  bytes: Uint8Array\n}\n"
  },
  {
    "path": "packages/repo/src/util.ts",
    "content": "import * as cbor from '@ipld/dag-cbor'\nimport { TID, cborDecode, check, cidForCbor, schema } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { Keypair } from '@atproto/crypto'\nimport { LexValue, RepoRecord, ipldToLex, lexToIpld } from '@atproto/lexicon'\nimport { DataDiff } from './data-diff'\nimport {\n  Commit,\n  LegacyV2Commit,\n  RecordCreateDescript,\n  RecordDeleteDescript,\n  RecordPath,\n  RecordUpdateDescript,\n  RecordWriteDescript,\n  UnsignedCommit,\n  WriteOpAction,\n} from './types'\n\nexport const diffToWriteDescripts = (\n  diff: DataDiff,\n): Promise<RecordWriteDescript[]> => {\n  return Promise.all([\n    ...diff.addList().map(async (add) => {\n      const { collection, rkey } = parseDataKey(add.key)\n      return {\n        action: WriteOpAction.Create,\n        collection,\n        rkey,\n        cid: add.cid,\n      } as RecordCreateDescript\n    }),\n    ...diff.updateList().map(async (upd) => {\n      const { collection, rkey } = parseDataKey(upd.key)\n      return {\n        action: WriteOpAction.Update,\n        collection,\n        rkey,\n        cid: upd.cid,\n        prev: upd.prev,\n      } as RecordUpdateDescript\n    }),\n    ...diff.deleteList().map((del) => {\n      const { collection, rkey } = parseDataKey(del.key)\n      return {\n        action: WriteOpAction.Delete,\n        collection,\n        rkey,\n        cid: del.cid,\n      } as RecordDeleteDescript\n    }),\n  ])\n}\n\nexport const ensureCreates = (\n  descripts: RecordWriteDescript[],\n): RecordCreateDescript[] => {\n  const creates: RecordCreateDescript[] = []\n  for (const descript of descripts) {\n    if (descript.action !== WriteOpAction.Create) {\n      throw new Error(`Unexpected action: ${descript.action}`)\n    } else {\n      creates.push(descript)\n    }\n  }\n  return creates\n}\n\nexport const parseDataKey = (key: string): RecordPath => {\n  const parts = key.split('/')\n  if (parts.length !== 2) throw new Error(`Invalid record key: ${key}`)\n  return { collection: parts[0], rkey: parts[1] }\n}\n\nexport const formatDataKey = (collection: string, rkey: string): string => {\n  return collection + '/' + rkey\n}\n\nexport const metaEqual = (a: Commit, b: Commit): boolean => {\n  return a.did === b.did && a.version === b.version\n}\n\nexport const signCommit = async (\n  unsigned: UnsignedCommit,\n  keypair: Keypair,\n): Promise<Commit> => {\n  const encoded = cbor.encode(unsigned)\n  const sig = await keypair.sign(encoded)\n  return {\n    ...unsigned,\n    sig,\n  }\n}\n\nexport const verifyCommitSig = async (\n  commit: Commit,\n  didKey: string,\n): Promise<boolean> => {\n  const { sig, ...rest } = commit\n  const encoded = cbor.encode(rest)\n  return crypto.verifySignature(didKey, encoded, sig)\n}\n\nexport const cborToLex = (val: Uint8Array): LexValue => {\n  return ipldToLex(cborDecode(val))\n}\n\nexport const cborToLexRecord = (val: Uint8Array): RepoRecord => {\n  const parsed = cborToLex(val)\n  if (!check.is(parsed, schema.map)) {\n    throw new Error('lexicon records be a json object')\n  }\n  return parsed\n}\n\nexport const cidForRecord = async (val: LexValue) => {\n  return cidForCbor(lexToIpld(val))\n}\n\nexport const ensureV3Commit = (commit: LegacyV2Commit | Commit): Commit => {\n  if (commit.version === 3) {\n    return commit\n  } else {\n    return {\n      ...commit,\n      version: 3,\n      rev: commit.rev ?? TID.nextStr(),\n    }\n  }\n}\n"
  },
  {
    "path": "packages/repo/tests/_keys.ts",
    "content": "export const A0 = 'A0/374913'\nexport const A1 = 'A1/076595'\nexport const A2 = 'A2/827942'\nexport const A3 = 'A3/578971'\nexport const A4 = 'A4/055903'\nexport const A5 = 'A5/518415'\nexport const B0 = 'B0/601692'\nexport const B1 = 'B1/986427'\nexport const B2 = 'B2/827649'\nexport const B3 = 'B3/095483'\nexport const B4 = 'B4/774183'\nexport const B5 = 'B5/116729'\nexport const C0 = 'C0/451630'\nexport const C1 = 'C1/438573'\nexport const C2 = 'C2/014073'\nexport const C3 = 'C3/564755'\nexport const C4 = 'C4/134079'\nexport const C5 = 'C5/141153'\nexport const D0 = 'D0/952776'\nexport const D1 = 'D1/834852'\nexport const D2 = 'D2/269196'\nexport const D3 = 'D3/038750'\nexport const D4 = 'D4/052059'\nexport const D5 = 'D5/563177'\nexport const E0 = 'E0/670489'\nexport const E1 = 'E1/091396'\nexport const E2 = 'E2/819540'\nexport const E3 = 'E3/391311'\nexport const E4 = 'E4/820614'\nexport const E5 = 'E5/512478'\nexport const F0 = 'F0/697858'\nexport const F1 = 'F1/085263'\nexport const F2 = 'F2/483591'\nexport const F3 = 'F3/409933'\nexport const F4 = 'F4/789697'\nexport const F5 = 'F5/271416'\nexport const G0 = 'G0/765327'\nexport const G1 = 'G1/209912'\nexport const G2 = 'G2/611528'\nexport const G3 = 'G3/649394'\nexport const G4 = 'G4/585887'\nexport const G5 = 'G5/298495'\nexport const H0 = 'H0/131238'\nexport const H1 = 'H1/566929'\nexport const H2 = 'H2/618272'\nexport const H3 = 'H3/500151'\nexport const H4 = 'H4/841548'\nexport const H5 = 'H5/642354'\nexport const I0 = 'I0/536928'\nexport const I1 = 'I1/525517'\nexport const I2 = 'I2/800680'\nexport const I3 = 'I3/818503'\nexport const I4 = 'I4/561177'\nexport const I5 = 'I5/010047'\nexport const J0 = 'J0/453243'\nexport const J1 = 'J1/217783'\nexport const J2 = 'J2/960389'\nexport const J3 = 'J3/501274'\nexport const J4 = 'J4/042054'\nexport const J5 = 'J5/743154'\nexport const K0 = 'K0/125271'\nexport const K1 = 'K1/317361'\nexport const K2 = 'K2/453868'\nexport const K3 = 'K3/214010'\nexport const K4 = 'K4/164720'\nexport const K5 = 'K5/177856'\nexport const L0 = 'L0/502889'\nexport const L1 = 'L1/574576'\nexport const L2 = 'L2/596333'\nexport const L3 = 'L3/683657'\nexport const L4 = 'L4/724989'\nexport const L5 = 'L5/093883'\nexport const M0 = 'M0/141744'\nexport const M1 = 'M1/643368'\nexport const M2 = 'M2/919782'\nexport const M3 = 'M3/836327'\nexport const M4 = 'M4/177463'\nexport const M5 = 'M5/563354'\nexport const N0 = 'N0/370604'\nexport const N1 = 'N1/563732'\nexport const N2 = 'N2/177587'\nexport const N3 = 'N3/678428'\nexport const N4 = 'N4/599183'\nexport const N5 = 'N5/567564'\nexport const O0 = 'O0/523870'\nexport const O1 = 'O1/052141'\nexport const O2 = 'O2/037651'\nexport const O3 = 'O3/773808'\nexport const O4 = 'O4/140952'\nexport const O5 = 'O5/318605'\nexport const P0 = 'P0/133157'\nexport const P1 = 'P1/394633'\nexport const P2 = 'P2/521462'\nexport const P3 = 'P3/493488'\nexport const P4 = 'P4/908754'\nexport const P5 = 'P5/109455'\nexport const Q0 = 'Q0/835234'\nexport const Q1 = 'Q1/131542'\nexport const Q2 = 'Q2/680035'\nexport const Q3 = 'Q3/253381'\nexport const Q4 = 'Q4/019053'\nexport const Q5 = 'Q5/658167'\nexport const R0 = 'R0/129386'\nexport const R1 = 'R1/363149'\nexport const R2 = 'R2/742766'\nexport const R3 = 'R3/039235'\nexport const R4 = 'R4/482275'\nexport const R5 = 'R5/817312'\nexport const S0 = 'S0/340283'\nexport const S1 = 'S1/561525'\nexport const S2 = 'S2/914574'\nexport const S3 = 'S3/909434'\nexport const S4 = 'S4/789708'\nexport const S5 = 'S5/803866'\nexport const T0 = 'T0/255204'\nexport const T1 = 'T1/716687'\nexport const T2 = 'T2/256231'\nexport const T3 = 'T3/054247'\nexport const T4 = 'T4/419247'\nexport const T5 = 'T5/509584'\nexport const U0 = 'U0/298296'\nexport const U1 = 'U1/851680'\nexport const U2 = 'U2/342856'\nexport const U3 = 'U3/597327'\nexport const U4 = 'U4/311686'\nexport const U5 = 'U5/030156'\nexport const V0 = 'V0/221100'\nexport const V1 = 'V1/741554'\nexport const V2 = 'V2/267990'\nexport const V3 = 'V3/674163'\nexport const V4 = 'V4/739931'\nexport const V5 = 'V5/573718'\nexport const W0 = 'W0/034202'\nexport const W1 = 'W1/697411'\nexport const W2 = 'W2/460313'\nexport const W3 = 'W3/189647'\nexport const W4 = 'W4/847299'\nexport const W5 = 'W5/648086'\nexport const X0 = 'X0/287498'\nexport const X1 = 'X1/044093'\nexport const X2 = 'X2/613770'\nexport const X3 = 'X3/577587'\nexport const X4 = 'X4/779391'\nexport const X5 = 'X5/339246'\nexport const Y0 = 'Y0/986350'\nexport const Y1 = 'Y1/044567'\nexport const Y2 = 'Y2/478044'\nexport const Y3 = 'Y3/757097'\nexport const Y4 = 'Y4/396913'\nexport const Y5 = 'Y5/802264'\nexport const Z0 = 'Z0/425878'\nexport const Z1 = 'Z1/127557'\nexport const Z2 = 'Z2/441927'\nexport const Z3 = 'Z3/064474'\nexport const Z4 = 'Z4/888344'\nexport const Z5 = 'Z5/977983'\n"
  },
  {
    "path": "packages/repo/tests/_util.ts",
    "content": "import fs from 'node:fs'\nimport { CID } from 'multiformats'\nimport { TID, dataToCborBlock } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { Keypair, randomBytes } from '@atproto/crypto'\nimport {\n  BlockMap,\n  CollectionContents,\n  Commit,\n  CommitData,\n  DataDiff,\n  RecordPath,\n  RecordWriteOp,\n  RepoContents,\n  WriteOpAction,\n} from '../src'\nimport { MST } from '../src/mst'\nimport { Repo } from '../src/repo'\nimport { RepoStorage } from '../src/storage'\n\ntype IdMapping = Record<string, CID>\n\nexport const randomCid = async (storage?: RepoStorage): Promise<CID> => {\n  const block = await dataToCborBlock({ test: randomStr(50) })\n  if (storage) {\n    // @ts-expect-error FIXME remove this comment (and fix the TS error)\n    await storage.putBlock(block.cid, block.bytes)\n  }\n  return block.cid\n}\n\nexport const generateBulkDataKeys = async (\n  count: number,\n  blockstore?: RepoStorage,\n): Promise<IdMapping> => {\n  const obj: IdMapping = {}\n  for (let i = 0; i < count; i++) {\n    const key = `com.example.record/${TID.nextStr()}`\n    obj[key] = await randomCid(blockstore)\n  }\n  return obj\n}\n\nexport const keysFromMapping = (mapping: IdMapping): TID[] => {\n  return Object.keys(mapping).map((id) => TID.fromStr(id))\n}\n\nexport const keysFromMappings = (mappings: IdMapping[]): TID[] => {\n  return mappings.map(keysFromMapping).flat()\n}\n\nexport const randomStr = (len: number): string => {\n  let result = ''\n  const CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'\n  for (let i = 0; i < len; i++) {\n    result += CHARS.charAt(Math.floor(Math.random() * CHARS.length))\n  }\n  return result\n}\n\nexport const shuffle = <T>(arr: T[]): T[] => {\n  const toShuffle = [...arr]\n  const shuffled: T[] = []\n  while (toShuffle.length > 0) {\n    const index = Math.floor(Math.random() * toShuffle.length)\n    shuffled.push(toShuffle[index])\n    toShuffle.splice(index, 1)\n  }\n  return shuffled\n}\n\nexport const generateObject = (): Record<string, string> => {\n  return {\n    name: randomStr(100),\n  }\n}\n\n// Mass repo mutations & checking\n// -------------------------------\n\nexport const testCollections = ['com.example.posts', 'com.example.likes']\n\nexport const fillRepo = async (\n  repo: Repo,\n  keypair: crypto.Keypair,\n  itemsPerCollection: number,\n): Promise<{ repo: Repo; data: RepoContents }> => {\n  const repoData: RepoContents = {}\n  const writes: RecordWriteOp[] = []\n  for (const collName of testCollections) {\n    const collData: CollectionContents = {}\n    for (let i = 0; i < itemsPerCollection; i++) {\n      const object = generateObject()\n      const rkey = TID.nextStr()\n      collData[rkey] = object\n      writes.push({\n        action: WriteOpAction.Create,\n        collection: collName,\n        rkey,\n        record: object,\n      })\n    }\n    repoData[collName] = collData\n  }\n  const updated = await repo.applyWrites(writes, keypair)\n  return {\n    repo: updated,\n    data: repoData,\n  }\n}\n\nexport const formatEdit = async (\n  repo: Repo,\n  prevData: RepoContents,\n  keypair: crypto.Keypair,\n  params: {\n    adds?: number\n    updates?: number\n    deletes?: number\n  },\n): Promise<{ commit: CommitData; data: RepoContents }> => {\n  const { adds = 0, updates = 0, deletes = 0 } = params\n  const repoData: RepoContents = {}\n  const writes: RecordWriteOp[] = []\n  for (const collName of testCollections) {\n    const collData = { ...(prevData[collName] ?? {}) }\n    const shuffled = shuffle(Object.entries(collData))\n\n    for (let i = 0; i < adds; i++) {\n      const object = generateObject()\n      const rkey = TID.nextStr()\n      collData[rkey] = object\n      writes.push({\n        action: WriteOpAction.Create,\n        collection: collName,\n        rkey,\n        record: object,\n      })\n    }\n\n    const toUpdate = shuffled.slice(0, updates)\n    for (let i = 0; i < toUpdate.length; i++) {\n      const object = generateObject()\n      const rkey = toUpdate[i][0]\n      collData[rkey] = object\n      writes.push({\n        action: WriteOpAction.Update,\n        collection: collName,\n        rkey,\n        record: object,\n      })\n    }\n\n    const toDelete = shuffled.slice(updates, deletes)\n    for (let i = 0; i < toDelete.length; i++) {\n      const rkey = toDelete[i][0]\n      delete collData[rkey]\n      writes.push({\n        action: WriteOpAction.Delete,\n        collection: collName,\n        rkey,\n      })\n    }\n    repoData[collName] = collData\n  }\n  const commit = await repo.formatCommit(writes, keypair)\n  return {\n    commit,\n    data: repoData,\n  }\n}\n\nexport const pathsForOps = (ops: RecordWriteOp[]): RecordPath[] =>\n  ops.map((op) => ({ collection: op.collection, rkey: op.rkey }))\n\nexport const saveMst = async (storage: RepoStorage, mst: MST): Promise<CID> => {\n  const diff = await mst.getUnstoredBlocks()\n  // @ts-expect-error FIXME remove this comment (and fix the TS error)\n  await storage.putMany(diff.blocks)\n  return diff.root\n}\n\n// Creating repo\n// -------------------\nexport const addBadCommit = async (\n  repo: Repo,\n  keypair: Keypair,\n): Promise<Repo> => {\n  const obj = generateObject()\n  const newBlocks = new BlockMap()\n  const cid = await newBlocks.add(obj)\n  const updatedData = await repo.data.add(`com.example.test/${TID.next()}`, cid)\n  const dataCid = await updatedData.getPointer()\n  const diff = await DataDiff.of(updatedData, repo.data)\n  newBlocks.addMap(diff.newMstBlocks)\n  // we generate a bad sig by signing some other data\n  const rev = TID.nextStr(repo.commit.rev)\n  const commit: Commit = {\n    ...repo.commit,\n    rev,\n    data: dataCid,\n    sig: await keypair.sign(randomBytes(256)),\n  }\n  const commitCid = await newBlocks.add(commit)\n\n  // @ts-expect-error FIXME remove this comment (and fix the TS error)\n  await repo.storage.applyCommit({\n    cid: commitCid,\n    rev,\n    prev: repo.cid,\n    newBlocks,\n    removedCids: diff.removedCids,\n  })\n  return await Repo.load(repo.storage, commitCid)\n}\n\n// Logging\n// ----------------\n\nexport const writeMstLog = async (filename: string, tree: MST) => {\n  let log = ''\n  for await (const entry of tree.walk()) {\n    if (entry.isLeaf()) continue\n    const layer = await entry.getLayer()\n    log += `Layer ${layer}: ${entry.pointer}\\n`\n    log += '--------------\\n'\n    const entries = await entry.getEntries()\n    for (const e of entries) {\n      if (e.isLeaf()) {\n        log += `Key: ${e.key} (${e.value})\\n`\n      } else {\n        log += `Subtree: ${e.pointer}\\n`\n      }\n    }\n    log += '\\n\\n'\n  }\n  fs.writeFileSync(filename, log)\n}\n\nexport const saveMstEntries = (filename: string, entries: [string, CID][]) => {\n  const writable = entries.map(([key, val]) => [key, val.toString()])\n  fs.writeFileSync(filename, JSON.stringify(writable))\n}\n\nexport const loadMstEntries = (filename: string): [string, CID][] => {\n  const contents = fs.readFileSync(filename)\n  const parsed = JSON.parse(contents.toString())\n  return parsed.map(([key, value]) => [key, CID.parse(value)])\n}\n"
  },
  {
    "path": "packages/repo/tests/car-file-fixtures.json",
    "content": "[\n  {\n    \"root\": \"bafyreiapldaco7m23c7qzc4w42r7kxmcswm64nkindtuh4vwztrpoe7m5m\",\n    \"blocks\": [\n      {\n        \"cid\": \"bafyreiapldaco7m23c7qzc4w42r7kxmcswm64nkindtuh4vwztrpoe7m5m\",\n        \"bytes\": \"oWR0ZXN0ZHJvb3Q\"\n      },\n      {\n        \"cid\": \"bafyreieteuyxvbvjbvuhsuo54qx6r3tnjxtp3ub6kb66rdjml3murxjcsy\",\n        \"bytes\": \"oWR0ZXN0WQEAM32TVA//xgHS9Gtukp0vw6whQ+TnlwF9czt5A7Dxz/URSbryc9Pdw7HESX+jC2oPI/6rwKbhSml2kxJo4MaUeIg/HWI9ixxALw5gIF/I0JC3ejXVAu1Pw6bI9RWa5TgIvAnSow0pJ6jbaaWHlxCpqqHCNHUYbIC14D9k+RK3yS0h2g+O+gRUETQt8t4jOKxEhD037cYEuJCD+fWzFoLkEkrPdUWeqlFQxGt6bflCYjZFgiZKFUo72afR3XM16+jOlhOl+EtuqFcijYJ6MIB53qI8P8HMC2RVH0Mv8UYWLcWatl+CNLykEjesnMar5CvZT8j4w5EyEiS09iD4r6bljg\"\n      },\n      {\n        \"cid\": \"bafyreiahvdwcywzm35id7hajal5l73p4bnoyaowgckeatd6sbxsacoo5jq\",\n        \"bytes\": \"oWR0ZXN0WID81CSnkdKi0OBexCStL5gex6Fkehtg7R9Jaww7LFuJC/6Or9Cdb58I+6T9Kjqhf1bf5t5WLbTGt8HOlf1Ysl3wqfdxdxnRZYjyng8YjEmH2Twkc//moluskzywxfOwhRXsXI7SYt36OUkS8AKDzmhTijOG2XWSa2QvU3iSSjP8zw\"\n      },\n      {\n        \"cid\": \"bafyreictawwxwjuto6csptbtktbbgwkrryqoakgyb3qovakgiqjzmvzi6a\",\n        \"bytes\": \"oWR0ZXN0RBnEJSE\"\n      },\n      {\n        \"cid\": \"bafyreierfkr6cwv2ux5hk6hp5qh5fhgzyr4yicad5m3pyu3ndaatyqucxu\",\n        \"bytes\": \"oWR0ZXN0WQH09xLXHen1UTIPEap18egWK6OiRVQ4oLBqjfBVQGdiSsmsIn8uXDM6wp63kCrYWLvXoI0dREVW6+RUjoQIbmDhiKbEUTuLbcFiJnwD5gSlkXdUEwO4UeLfvcAsDJXel5lLaOlOeRTfA3Wf+rEEpr/ccCuwOATBPZBjPrneFVQu8UdsvTu5W144tGxp/ptBsMBqM3yLYkYYHNRrjcAI7iFiszsfKzo28GyM199Jko11mcVhNQ8KHZS72jbsWW/HyfJsL1M/dn6sDCfIaro66mTLRddSQaheQL4NBW5FEgLUBO2VDuBT9fVIl2IwQcijZAOakjLkS1sY2SyUmdsicqGRalnOrVlC5iWQcwHDXLzm9GWz/vfuoF+jzWsdpo6cjT5MIN8uXpzoZRSjH1+UXFxCzFhXkDwL/xJUq8u/0OFGp0mzDc7RLO3gC7X/ENaVPtz/wQaZ3q00EeHvDuiaxHdKIesf/+IkbqS1XjKtaHemB511MFiVf7l2OcqsUU12A5VMreqWPwbAHLgFRDPWiLS0D7yEF3KJX1IupxBmidQMT+SlmCp7FZMdJeHJ3pzpbQv+EoKSXja7it2D2uYsMcTn2DGhlVYsCcQd8PVNKZmPXuC7D82N4sh8p2XVW4LbsDfNTOrgHLX7zRX31VNa47w5Uc03Wuk\"\n      }\n    ],\n    \"car\": \"OqJlcm9vdHOB2CpYJQABcRIgD1jAJ32a2L8Mi5bmo/VdgpWZ7jVIaOdD8rbM4vcT7OtndmVyc2lvbgEvAXESIA9YwCd9mti/DIuW5qP1XYKVme41SGjnQ/K2zOL3E+zroWR0ZXN0ZHJvb3StAgFxEiCTJTF6hqkNaHlR3eQv6O5tTeb90D5QfeiNLF7ZSN0ilqFkdGVzdFkBADN9k1QP/8YB0vRrbpKdL8OsIUPk55cBfXM7eQOw8c/1EUm68nPT3cOxxEl/owtqDyP+q8Cm4UppdpMSaODGlHiIPx1iPYscQC8OYCBfyNCQt3o11QLtT8OmyPUVmuU4CLwJ0qMNKSeo22mlh5cQqaqhwjR1GGyAteA/ZPkSt8ktIdoPjvoEVBE0LfLeIzisRIQ9N+3GBLiQg/n1sxaC5BJKz3VFnqpRUMRrem35QmI2RYImShVKO9mn0d1zNevozpYTpfhLbqhXIo2CejCAed6iPD/BzAtkVR9DL/FGFi3FmrZfgjS8pBI3rJzGq+Qr2U/I+MORMhIktPYg+K+m5Y6sAQFxEiAHqOwsWyzfUD+cCQL6v+38C12AOsYSiAmP0g3kATndTKFkdGVzdFiA/NQkp5HSotDgXsQkrS+YHsehZHobYO0fSWsMOyxbiQv+jq/QnW+fCPuk/So6oX9W3+beVi20xrfBzpX9WLJd8Kn3cXcZ0WWI8p4PGIxJh9k8JHP/5qJbrJM8sMXzsIUV7FyO0mLd+jlJEvACg85oU4ozhtl1kmtkL1N4kkoz/M8vAXESIFMFrXsmk3eFJ8wzVMITWVGOIOAo2A7g6oFGRBOWVyjwoWR0ZXN0RBnEJSGhBAFxEiCRKqPhWrql+nV47+wP0pzZxHmECAPrNvxTbRgBPEKCvaFkdGVzdFkB9PcS1x3p9VEyDxGqdfHoFiujokVUOKCwao3wVUBnYkrJrCJ/LlwzOsKet5Aq2Fi716CNHURFVuvkVI6ECG5g4YimxFE7i23BYiZ8A+YEpZF3VBMDuFHi373ALAyV3peZS2jpTnkU3wN1n/qxBKa/3HArsDgEwT2QYz653hVULvFHbL07uVteOLRsaf6bQbDAajN8i2JGGBzUa43ACO4hYrM7Hys6NvBsjNffSZKNdZnFYTUPCh2Uu9o27Flvx8nybC9TP3Z+rAwnyGq6Oupky0XXUkGoXkC+DQVuRRIC1ATtlQ7gU/X1SJdiMEHIo2QDmpIy5EtbGNkslJnbInKhkWpZzq1ZQuYlkHMBw1y85vRls/737qBfo81rHaaOnI0+TCDfLl6c6GUUox9flFxcQsxYV5A8C/8SVKvLv9DhRqdJsw3O0Szt4Au1/xDWlT7c/8EGmd6tNBHh7w7omsR3SiHrH//iJG6ktV4yrWh3pgeddTBYlX+5djnKrFFNdgOVTK3qlj8GwBy4BUQz1oi0tA+8hBdyiV9SLqcQZonUDE/kpZgqexWTHSXhyd6c6W0L/hKCkl42u4rdg9rmLDHE59gxoZVWLAnEHfD1TSmZj17guw/NjeLIfKdl1VuC27A3zUzq4By1+80V99VTWuO8OVHNN1rp\"\n  }\n]\n"
  },
  {
    "path": "packages/repo/tests/car.test.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport * as ui8 from 'uint8arrays'\nimport { dataToCborBlock, streamToBytes, wait } from '@atproto/common'\nimport { CarBlock, readCarStream, writeCarStream } from '../src'\nimport fixtures from './car-file-fixtures.json'\n\ndescribe('car', () => {\n  for (const fixture of fixtures) {\n    it('correctly writes car files', async () => {\n      const root = CID.parse(fixture.root)\n      async function* blockIter() {\n        for (const block of fixture.blocks) {\n          const cid = CID.parse(block.cid)\n          const bytes = ui8.fromString(block.bytes, 'base64')\n          yield { cid, bytes }\n        }\n      }\n      const carStream = writeCarStream(root, blockIter())\n      const car = await streamToBytes(carStream)\n      const carB64 = ui8.toString(car, 'base64')\n      expect(carB64).toEqual(fixture.car)\n    })\n\n    it('correctly reads carfiles', async () => {\n      const carStream = [ui8.fromString(fixture.car, 'base64')]\n      const { roots, blocks } = await readCarStream(carStream)\n      expect(roots.length).toBe(1)\n      expect(roots[0].toString()).toEqual(fixture.root)\n      const carBlocks: CarBlock[] = []\n      for await (const block of blocks) {\n        carBlocks.push(block)\n      }\n      expect(carBlocks.length).toEqual(fixture.blocks.length)\n      for (let i = 0; i < carBlocks.length; i++) {\n        expect(carBlocks[i].cid.toString()).toEqual(fixture.blocks[i].cid)\n        expect(ui8.toString(carBlocks[i].bytes, 'base64')).toEqual(\n          fixture.blocks[i].bytes,\n        )\n      }\n    })\n  }\n\n  it('writeCar propagates errors', async () => {\n    const iterate = async () => {\n      async function* blockIterator() {\n        await wait(1)\n        const block = await dataToCborBlock({ test: 1 })\n        yield block\n        throw new Error('Oops!')\n      }\n      const iter = writeCarStream(null, blockIterator())\n      for await (const _bytes of iter) {\n        // no-op\n      }\n    }\n    await expect(iterate).rejects.toThrow('Oops!')\n  })\n\n  it('verifies CIDs', async () => {\n    const block0 = await dataToCborBlock({ block: 0 })\n    const block1 = await dataToCborBlock({ block: 1 })\n    const block2 = await dataToCborBlock({ block: 2 })\n    const block3 = await dataToCborBlock({ block: 3 })\n    const badBlock = await dataToCborBlock({ block: 'bad' })\n    const blockIter = async function* () {\n      yield block0\n      yield block1\n      yield block2\n      yield { cid: block3.cid, bytes: badBlock.bytes }\n    }\n    const flush = async function (iter: AsyncIterable<unknown>) {\n      for await (const _ of iter) {\n        // no-op\n      }\n    }\n    const badCar = await readCarStream(writeCarStream(block0.cid, blockIter()))\n    await expect(flush(badCar.blocks)).rejects.toThrow(\n      'Not a valid CID for bytes',\n    )\n  })\n\n  it('skips CID verification', async () => {\n    const block0 = await dataToCborBlock({ block: 0 })\n    const block1 = await dataToCborBlock({ block: 1 })\n    const block2 = await dataToCborBlock({ block: 2 })\n    const block3 = await dataToCborBlock({ block: 3 })\n    const badBlock = await dataToCborBlock({ block: 'bad' })\n    const blockIter = async function* () {\n      yield block0\n      yield block1\n      yield block2\n      yield { cid: block3.cid, bytes: badBlock.bytes }\n    }\n    const flush = async function (iter: AsyncIterable<unknown>) {\n      for await (const _ of iter) {\n        // no-op\n      }\n    }\n    const badCar = await readCarStream(\n      writeCarStream(block0.cid, blockIter()),\n      { skipCidVerification: true },\n    )\n    await expect(flush(badCar.blocks)).resolves.toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "packages/repo/tests/commit-data.test.ts",
    "content": "import { Secp256k1Keypair } from '@atproto/crypto'\nimport { Repo, WriteOpAction, blocksToCarFile, verifyProofs } from '../src'\nimport { MemoryBlockstore } from '../src/storage'\n\ndescribe('Commit data', () => {\n  // @NOTE this test uses a fully deterministic tree structure\n  it('includes all relevant blocks for proof in commit data', async () => {\n    const did = 'did:example:alice'\n    const collection = 'com.atproto.test'\n    const record = {\n      test: 123,\n    }\n\n    const blockstore = new MemoryBlockstore()\n    const keypair = await Secp256k1Keypair.create()\n    let repo = await Repo.create(blockstore, did, keypair)\n\n    const keys: string[] = []\n    for (let i = 0; i < 50; i++) {\n      const rkey = `key-${i}`\n      keys.push(rkey)\n      repo = await repo.applyWrites(\n        [\n          {\n            action: WriteOpAction.Create,\n            collection,\n            rkey,\n            record,\n          },\n        ],\n        keypair,\n      )\n    }\n\n    // this test demonstrates the test case:\n    // specifically in the case of deleting the first key, there is a \"rearranged block\" that is necessary\n    // in the proof path but _is not_ in newBlocks (as it already existed in the repository)\n    {\n      const commit = await repo.formatCommit(\n        {\n          action: WriteOpAction.Delete,\n          collection,\n          rkey: keys[0],\n        },\n        keypair,\n      )\n      const car = await blocksToCarFile(commit.cid, commit.newBlocks)\n      const proofAttempt = verifyProofs(\n        car,\n        [\n          {\n            collection,\n            rkey: keys[0],\n            cid: null,\n          },\n        ],\n        did,\n        keypair.did(),\n      )\n      await expect(proofAttempt).rejects.toThrow(/block not found/)\n    }\n\n    for (const rkey of keys) {\n      const commit = await repo.formatCommit(\n        {\n          action: WriteOpAction.Delete,\n          collection,\n          rkey,\n        },\n        keypair,\n      )\n      const car = await blocksToCarFile(commit.cid, commit.relevantBlocks)\n      const proofRes = await verifyProofs(\n        car,\n        [\n          {\n            collection,\n            rkey: rkey,\n            cid: null,\n          },\n        ],\n        did,\n        keypair.did(),\n      )\n      expect(proofRes.unverified.length).toBe(0)\n      repo = await repo.applyCommit(commit)\n    }\n  })\n})\n"
  },
  {
    "path": "packages/repo/tests/commit-proof-fixtures.json",
    "content": "[\n  {\n    \"comment\": \"two deep split\",\n    \"leafValue\": \"bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454\",\n    \"keys\": [\n      \"A0/374913\",\n      \"B1/986427\",\n      \"C0/451630\",\n      \"E0/670489\",\n      \"F1/085263\",\n      \"G0/765327\"\n    ],\n    \"adds\": [\"D2/269196\"],\n    \"dels\": [],\n    \"rootBeforeCommit\": \"bafyreicraprx2xwnico4tuqir3ozsxpz46qkcpox3obf5bagicqwurghpy\",\n    \"rootAfterCommit\": \"bafyreihvay6pazw3dfa47u5d2tn3rd6pa57sr37bo5bqyvjuqc73ib65my\",\n    \"blocksInProof\": [\n      \"bafyreieazvzmba35p4phksumwfoklwe5o4ncmo7otud74idcyv4orrbzxi\",\n      \"bafyreie4227qpa4vbtbpnsvuhp322b776vjuhxsidi5hxp2gawumr4m3de\",\n      \"bafyreid44jgimksqqdratyste2moqu6zo4h6co2pknjppfoiplsqxtuxae\",\n      \"bafyreiaerlvitye7fjjwodkshtbqqdsmfsdjtnlz4vs6y4trnddshsmd5a\",\n      \"bafyreihvay6pazw3dfa47u5d2tn3rd6pa57sr37bo5bqyvjuqc73ib65my\"\n    ]\n  },\n  {\n    \"comment\": \"two deep leafless split\",\n    \"leafValue\": \"bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454\",\n    \"keys\": [\"A0/374913\", \"B0/601692\", \"D0/952776\", \"E0/670489\"],\n    \"adds\": [\"C2/014073\"],\n    \"dels\": [],\n    \"rootBeforeCommit\": \"bafyreialm5sgf7pijawbschsjpdevid5rss5ip3d4n4w6cc4mhu53sfl4i\",\n    \"rootAfterCommit\": \"bafyreibxh4iztp5l2yshz3ectg2qjpeyprpw2gogao3pvceowpq3k3thya\",\n    \"blocksInProof\": [\n      \"bafyreih7dxytqtcjv3cfia3fi3wxofeip62teqkpynnkxisxqwfchfb4bu\",\n      \"bafyreiaqbymlnvpklmogx75gozjl3y73gva43jbgwcrqu2pp5g5ejou5vm\",\n      \"bafyreicfh3st5ghtnoqyyvznjv4lhfnvl7qsndempx35i4tcmoxakqbgrm\",\n      \"bafyreieyjrrai6igjceyxzkajrxgxz37da2eufb33anvesb4ev6yzztauu\",\n      \"bafyreibxh4iztp5l2yshz3ectg2qjpeyprpw2gogao3pvceowpq3k3thya\"\n    ]\n  },\n  {\n    \"comment\": \"add on edge with neighbor two layers down\",\n    \"leafValue\": \"bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454\",\n    \"keys\": [\"A0/374913\", \"B2/827649\", \"C0/451630\"],\n    \"adds\": [\"D2/269196\"],\n    \"dels\": [],\n    \"rootBeforeCommit\": \"bafyreigc6ay2qwfk7kuevvrczummpd64nknfo4yxpaooknfymzyb7u3ntq\",\n    \"rootAfterCommit\": \"bafyreign6kxoll35r5f2ske6hjx7vg56aw3jn6r5hcopgrepzafpvohr2a\",\n    \"blocksInProof\": [\n      \"bafyreieazvzmba35p4phksumwfoklwe5o4ncmo7otud74idcyv4orrbzxi\",\n      \"bafyreidicvcjgrpm5bmhm3ndh2ysqfhgzk4chwn3m4kuvwkenfusspb4uy\",\n      \"bafyreign6kxoll35r5f2ske6hjx7vg56aw3jn6r5hcopgrepzafpvohr2a\"\n    ]\n  },\n  {\n    \"comment\": \"merge and split in multi-op commit\",\n    \"leafValue\": \"bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454\",\n    \"keys\": [\"A0/374913\", \"B2/827649\", \"D2/269196\", \"E0/670489\"],\n    \"adds\": [\"C2/014073\"],\n    \"dels\": [\"B2/827649\", \"D2/269196\"],\n    \"rootBeforeCommit\": \"bafyreiceld4icym4qjmdcn3dfgtxt7t66hdgyhvigessgmkvb56dx6amgi\",\n    \"rootAfterCommit\": \"bafyreigkalika3taqauapfha556lo36zzcjoiifny5xeru6yis3nxw5ruq\",\n    \"blocksInProof\": [\n      \"bafyreid44jgimksqqdratyste2moqu6zo4h6co2pknjppfoiplsqxtuxae\",\n      \"bafyreihytu6onh476trave25zuo63ziebkeong2755sc5nmf55uzdawgt4\",\n      \"bafyreigkalika3taqauapfha556lo36zzcjoiifny5xeru6yis3nxw5ruq\",\n      \"bafyreidnnkrdkcaswbflgtdsxm7nzs7p5f2rdous6wrlupzstuwqu5pfgm\",\n      \"bafyreia2kq243hqq3volwlzkbzzphoeqauk54sc5h7vgogq4ei5fjizxvy\"\n    ]\n  },\n  {\n    \"comment\": \"complex multi-op commit\",\n    \"leafValue\": \"bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454\",\n    \"keys\": [\n      \"B0/601692\",\n      \"C2/014073\",\n      \"D0/952776\",\n      \"E2/819540\",\n      \"F0/697858\",\n      \"H0/131238\"\n    ],\n    \"adds\": [\"A2/827942\", \"G2/611528\"],\n    \"dels\": [\"C2/014073\"],\n    \"rootBeforeCommit\": \"bafyreigr3plnts7dax6yokvinbhcqpyicdfgg6npvvyx6okc5jo55slfqi\",\n    \"rootAfterCommit\": \"bafyreiftrcrbhrwmi37u4egedlg56gk3jeh3tvmqvwgowoifuklfysyx54\",\n    \"blocksInProof\": [\n      \"bafyreih62n3gjbzzvlicuggpfydyzrp3ssyx7hdgtltd3sct3ribm3u73e\",\n      \"bafyreihrjhuoynjvgteuefin5vwnqmupyfzvmytdobpstqt3mbawgw5qhm\",\n      \"bafyreibevzst4gzkxo263syohlmq3lpxdvpjhlpyqx2ay3moh43lifydca\",\n      \"bafyreifsdd7dv2neal7zjhyrsvndkaocelqlpgfxwo4utoq2g77klih37e\",\n      \"bafyreid2wwyroodj2lxx2obikac74q77lsn6vqkoetlqqwwnr3criwlcvy\",\n      \"bafyreie55b224oljhykpsxdjq4ajn2ysksud7qm347s6kn2ei6a775faum\",\n      \"bafyreiftrcrbhrwmi37u4egedlg56gk3jeh3tvmqvwgowoifuklfysyx54\"\n    ]\n  },\n  {\n    \"comment\": \"split with earlier leaves on same layer\",\n    \"leafValue\": \"bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454\",\n    \"keys\": [\n      \"app.bsky.feed.post/3lo3kqqljmfe2\",\n      \"app.bsky.feed.post/3log4547dm6h2\",\n      \"app.bsky.feed.post/3log45inogon2\",\n      \"app.bsky.feed.post/3logaodrh74d2\",\n      \"app.bsky.feed.post/3logteazog2n2\",\n      \"app.bsky.feed.post/3lon5cqsbwrj2\",\n      \"app.bsky.feed.repost/3l6sjhvqonco2\"\n    ],\n    \"adds\": [\"app.bsky.feed.post/3lon5dzeaihj2\"],\n    \"dels\": [],\n    \"rootBeforeCommit\": \"bafyreigfcsro2up7qi7l3rxdpg7n6gjtteotkmgrrqztl5oy2tf4ncl4ji\",\n    \"rootAfterCommit\": \"bafyreig33hsjiplaixvmccy65n7rn3in5nsbtcittzx6k3w5wjfhk2sg3a\",\n    \"blocksInProof\": [\n      \"bafyreig33hsjiplaixvmccy65n7rn3in5nsbtcittzx6k3w5wjfhk2sg3a\",\n      \"bafyreih2rhjm3apcghihwfojv2em7noqkgt5qyjcnxux7do674m464oc3m\",\n      \"bafyreiajhswkduap4zvqvfhth3skdgckmk2eb5gow7vv3gvj45f4fqwmxm\"\n    ]\n  }\n]\n"
  },
  {
    "path": "packages/repo/tests/commit-proofs.test.ts",
    "content": "import { CID } from 'multiformats'\nimport { BlockMap } from '../src'\nimport { MST } from '../src/mst'\nimport { MemoryBlockstore } from '../src/storage'\nimport fixtures from './commit-proof-fixtures.json'\n\ndescribe('commit proofs', () => {\n  for (const fixture of fixtures) {\n    it(fixture.comment, async () => {\n      const { leafValue, keys, adds, dels } = fixture\n      const leaf = CID.parse(leafValue)\n\n      const storage = new MemoryBlockstore()\n      let mst = await MST.create(storage)\n      for (const key of keys) {\n        mst = await mst.add(key, leaf)\n      }\n\n      const rootBeforeCommit = await mst.getPointer()\n      expect(rootBeforeCommit.toString()).toEqual(fixture.rootBeforeCommit)\n\n      for (const key of adds) {\n        mst = await mst.add(key, leaf)\n      }\n      for (const key of dels) {\n        mst = await mst.delete(key)\n      }\n      const rootAfterCommit = await mst.getPointer()\n      expect(rootAfterCommit.toString()).toEqual(fixture.rootAfterCommit)\n      const proofs = await Promise.all(\n        [...adds, ...dels].map((key) => mst.getCoveringProof(key)),\n      )\n      const proof = proofs.reduce((acc, cur) => acc.addMap(cur), new BlockMap())\n      const blocksInProof = fixture.blocksInProof.map((cid) => CID.parse(cid))\n      for (const cid of blocksInProof) {\n        expect(proof.has(cid)).toBe(true)\n      }\n\n      const invertAdds = adds.map((k) => (mst: MST) => mst.delete(k))\n      const invertDels = dels.map((k) => (mst: MST) => mst.add(k, leaf))\n      const invertOrders = permutations([...invertAdds, ...invertDels])\n\n      const proofStorage = new MemoryBlockstore(proof)\n      for (const order of invertOrders) {\n        let proofMst = await MST.load(proofStorage, rootAfterCommit)\n        for (const fn of order) {\n          proofMst = await fn(proofMst)\n        }\n        const rootAfterInvert = await proofMst.getPointer()\n        expect(rootAfterInvert.toString()).toEqual(fixture.rootBeforeCommit)\n      }\n    })\n  }\n})\n\nfunction permutations<T>(arr: T[]): T[][] {\n  if (arr.length <= 1) return [arr]\n\n  return arr.reduce((perms: T[][], item: T, i: number) => {\n    const rest = [...arr.slice(0, i), ...arr.slice(i + 1)]\n    return perms.concat(permutations(rest).map((p) => [item, ...p]))\n  }, [])\n}\n"
  },
  {
    "path": "packages/repo/tests/covering-proofs.test.ts",
    "content": "import { CID } from 'multiformats'\nimport { BlockMap } from '../src'\nimport { MST } from '../src/mst'\nimport { MemoryBlockstore } from '../src/storage'\nimport * as k from './_keys'\n\n// @NOTE these tests are the exact same as the tests in commit-proof-fixtures.json but in code from\n// kept around currently because they are a bit easier to understand/work with\n// we should delete in the future once the fixtures are pulled into our test suite\n\ndescribe('covering proofs', () => {\n  /**\n   *\n   *                *                                  *\n   *       _________|________                      ____|____\n   *       |   |    |    |   |                    |    |    |\n   *       *   b  __*__  f   *       ->         __*__  d  __*__\n   *       |     |     |     |                 |  |  |   |  |  |\n   *       a     c     e     g                 *  b  *   *  f  *\n   *                                           |     |   |     |\n   *                                           a     c   e     g\n   *\n   *\n   *\n   */\n  it('two deep split ', async () => {\n    const storage = new MemoryBlockstore()\n    const cid = CID.parse(\n      'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n    )\n\n    let mst = await MST.create(storage)\n    mst = await mst.add(k.A0, cid)\n    mst = await mst.add(k.B1, cid)\n    mst = await mst.add(k.C0, cid)\n    mst = await mst.add(k.E0, cid)\n    mst = await mst.add(k.F1, cid)\n    mst = await mst.add(k.G0, cid)\n\n    const rootBeforeCommit = await mst.getPointer()\n\n    mst = await mst.add(k.D2, cid)\n    const proof = await mst.getCoveringProof(k.D2)\n\n    const proofStorage = new MemoryBlockstore(proof)\n    let proofMst = await MST.load(proofStorage, await mst.getPointer())\n    proofMst = await proofMst.delete(k.D2)\n    const rootAfterInvert = await proofMst.getPointer()\n    expect(rootAfterInvert.equals(rootBeforeCommit)).toBe(true)\n  })\n\n  /**\n   *\n   *                *                               *\n   *           _____|_____                      ____|____\n   *           |  |   |  |                      |   |   |\n   *           a  b   d  e       ->             *   c   *\n   *                                            |       |\n   *                                          __*__   __*__\n   *                                          |   |   |   |\n   *                                          a   b   d   e\n   *\n   *\n   *\n   */\n  it('two deep leafless splits ', async () => {\n    const storage = new MemoryBlockstore()\n    const cid = CID.parse(\n      'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n    )\n\n    let mst = await MST.create(storage)\n    mst = await mst.add(k.A0, cid)\n    mst = await mst.add(k.B0, cid)\n    mst = await mst.add(k.D0, cid)\n    mst = await mst.add(k.E0, cid)\n\n    const rootBeforeCommit = await mst.getPointer()\n\n    mst = await mst.add(k.C2, cid)\n    const proof = await mst.getCoveringProof(k.C2)\n\n    const proofStorage = new MemoryBlockstore(proof)\n    let proofMst = await MST.load(proofStorage, await mst.getPointer())\n    proofMst = await proofMst.delete(k.C2)\n    const rootAfterInvert = await proofMst.getPointer()\n    expect(rootAfterInvert.equals(rootBeforeCommit)).toBe(true)\n  })\n\n  /**\n   *\n   *               *                            *\n   *           ____|____                    ____|____\n   *          |    b    |                  |   |  |  |\n   *          *         *          ->      *   b  *  d\n   *          |         |                  |      |\n   *          a         c                  a      c\n   *\n   *\n   *\n   *\n   *\n   */\n  it('add on edge with neighbor two layers down', async () => {\n    const storage = new MemoryBlockstore()\n    const cid = CID.parse(\n      'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n    )\n\n    let mst = await MST.create(storage)\n    mst = await mst.add(k.A0, cid)\n    mst = await mst.add(k.B2, cid)\n    mst = await mst.add(k.C0, cid)\n\n    const rootBeforeCommit = await mst.getPointer()\n\n    mst = await mst.add(k.D2, cid)\n    const proof = await mst.getCoveringProof(k.D2)\n\n    const proofStorage = new MemoryBlockstore(proof)\n    let proofMst = await MST.load(proofStorage, await mst.getPointer())\n    proofMst = await proofMst.delete(k.D2)\n    const rootAfterInvert = await proofMst.getPointer()\n    expect(rootAfterInvert.equals(rootBeforeCommit)).toBe(true)\n  })\n\n  /**\n   *\n   *                *                           *\n   *           _____|_____                 _____|_____\n   *          |   |   |   |               |     |     |\n   *          *   b   d   *     ->        *     c     *\n   *          |           |               |           |\n   *          a           e               a           e\n   *\n   *\n   *\n   *\n   *\n   */\n  it('merge and split in multi op commit', async () => {\n    const storage = new MemoryBlockstore()\n    const cid = CID.parse(\n      'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n    )\n\n    let mst = await MST.create(storage)\n    mst = await mst.add(k.A0, cid)\n    mst = await mst.add(k.B2, cid)\n    mst = await mst.add(k.D2, cid)\n    mst = await mst.add(k.E0, cid)\n\n    const rootBeforeCommit = await mst.getPointer()\n\n    mst = await mst.delete(k.B2)\n    mst = await mst.delete(k.D2)\n    mst = await mst.add(k.C2, cid)\n\n    const proofs = await Promise.all([\n      mst.getCoveringProof(k.B2),\n      mst.getCoveringProof(k.D2),\n      mst.getCoveringProof(k.C2),\n    ])\n    const proof = proofs.reduce((acc, cur) => acc.addMap(cur), new BlockMap())\n    const proofStorage = new MemoryBlockstore(proof)\n\n    const addB = async (mst: MST) => mst.add(k.B2, cid)\n    const addD = async (mst: MST) => mst.add(k.D2, cid)\n    const delC = async (mst: MST) => mst.delete(k.C2)\n\n    const testOrder = async (fns: ((mst: MST) => Promise<MST>)[]) => {\n      let proofMst = await MST.load(proofStorage, await mst.getPointer())\n      for (const fn of fns) {\n        proofMst = await fn(proofMst)\n      }\n      const rootAfterInvert = await proofMst.getPointer()\n      expect(rootAfterInvert.equals(rootBeforeCommit)).toBe(true)\n    }\n\n    // test that the operations work in any order\n    await testOrder([addB, addD, delC])\n    await testOrder([addB, delC, addD])\n    await testOrder([addD, addB, delC])\n    await testOrder([addD, delC, addB])\n    await testOrder([delC, addB, addD])\n    await testOrder([delC, addD, addB])\n  })\n\n  // This complex multi op commit includes:\n  // - a two deep split\n  // - a two deep merge\n  // - an addition that requires knowledge of a leaf two deeper\n  /**\n   *\n   *                *                               *\n   *           _____|_____                    ______|_______\n   *          |  |  |  |  |                  |  |  |  |  |  |\n   *          *  c  *  e  *     ->           a  *  e  *  g  *\n   *          |     |    _|_                    |     |     |\n   *          *     *   |   |                  _*_    *     *\n   *          b     d   f   h                 |   |   |     |\n   *                                          b   d   f     h\n   *\n   *\n   *\n   */\n  it('complex multi-op commit', async () => {\n    const storage = new MemoryBlockstore()\n    const cid = CID.parse(\n      'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n    )\n\n    let mst = await MST.create(storage)\n    mst = await mst.add(k.B0, cid)\n    mst = await mst.add(k.C2, cid)\n    mst = await mst.add(k.D0, cid)\n    mst = await mst.add(k.E2, cid)\n    mst = await mst.add(k.F0, cid)\n    mst = await mst.add(k.H0, cid)\n\n    const rootBeforeCommit = await mst.getPointer()\n\n    mst = await mst.add(k.A2, cid)\n    mst = await mst.add(k.G2, cid)\n    mst = await mst.delete(k.C2)\n\n    const proofs = await Promise.all([\n      mst.getCoveringProof(k.A2),\n      mst.getCoveringProof(k.G2),\n      mst.getCoveringProof(k.C2),\n    ])\n    const proof = proofs.reduce((acc, cur) => acc.addMap(cur), new BlockMap())\n    const proofStorage = new MemoryBlockstore(proof)\n\n    const delA = async (mst: MST) => mst.delete(k.A2)\n    const delG = async (mst: MST) => mst.delete(k.G2)\n    const addC = async (mst: MST) => mst.add(k.C2, cid)\n\n    const testOrder = async (fns: ((mst: MST) => Promise<MST>)[]) => {\n      let proofMst = await MST.load(proofStorage, await mst.getPointer())\n      for (const fn of fns) {\n        proofMst = await fn(proofMst)\n      }\n      const rootAfterInvert = await proofMst.getPointer()\n      expect(rootAfterInvert.equals(rootBeforeCommit)).toBe(true)\n    }\n\n    // test that the operations work in any order\n    await testOrder([delA, delG, addC])\n    await testOrder([delA, addC, delG])\n    await testOrder([delG, delA, addC])\n    await testOrder([delG, addC, delA])\n    await testOrder([addC, delA, delG])\n    await testOrder([addC, delG, delA])\n  })\n})\n"
  },
  {
    "path": "packages/repo/tests/mst.test.ts",
    "content": "import { CID } from 'multiformats'\nimport { DataAdd, DataDelete, DataDiff, DataUpdate } from '../src/data-diff'\nimport { MST } from '../src/mst'\nimport { InvalidMstKeyError, countPrefixLen } from '../src/mst/util'\nimport { MemoryBlockstore } from '../src/storage'\nimport * as util from './_util'\n\ndescribe('Merkle Search Tree', () => {\n  let blockstore: MemoryBlockstore\n  let mst: MST\n  let mapping: Record<string, CID>\n  let shuffled: [string, CID][]\n\n  beforeAll(async () => {\n    blockstore = new MemoryBlockstore()\n    mst = await MST.create(blockstore)\n    mapping = await util.generateBulkDataKeys(1000, blockstore)\n    shuffled = util.shuffle(Object.entries(mapping))\n  })\n\n  it('adds records', async () => {\n    for (const entry of shuffled) {\n      mst = await mst.add(entry[0], entry[1])\n    }\n    for (const entry of shuffled) {\n      const got = await mst.get(entry[0])\n      expect(entry[1].equals(got)).toBeTruthy()\n    }\n\n    const totalSize = await mst.leafCount()\n    expect(totalSize).toBe(1000)\n  })\n\n  it('edits records', async () => {\n    let editedMst = mst\n    const toEdit = shuffled.slice(0, 100)\n\n    const edited: [string, CID][] = []\n    for (const entry of toEdit) {\n      const newCid = await util.randomCid()\n      editedMst = await editedMst.update(entry[0], newCid)\n      edited.push([entry[0], newCid])\n    }\n\n    for (const entry of edited) {\n      const got = await editedMst.get(entry[0])\n      expect(entry[1].equals(got)).toBeTruthy()\n    }\n\n    const totalSize = await editedMst.leafCount()\n    expect(totalSize).toBe(1000)\n  })\n\n  it('deletes records', async () => {\n    let deletedMst = mst\n    const toDelete = shuffled.slice(0, 100)\n    const theRest = shuffled.slice(100)\n    for (const entry of toDelete) {\n      deletedMst = await deletedMst.delete(entry[0])\n    }\n\n    const totalSize = await deletedMst.leafCount()\n    expect(totalSize).toBe(900)\n\n    for (const entry of toDelete) {\n      const got = await deletedMst.get(entry[0])\n      expect(got).toBe(null)\n    }\n    for (const entry of theRest) {\n      const got = await deletedMst.get(entry[0])\n      expect(entry[1].equals(got)).toBeTruthy()\n    }\n  })\n\n  it('is order independent', async () => {\n    const allNodes = await mst.allNodes()\n\n    let recreated = await MST.create(blockstore)\n    const reshuffled = util.shuffle(Object.entries(mapping))\n    for (const entry of reshuffled) {\n      recreated = await recreated.add(entry[0], entry[1])\n    }\n    const allReshuffled = await recreated.allNodes()\n\n    expect(allNodes.length).toBe(allReshuffled.length)\n    for (let i = 0; i < allNodes.length; i++) {\n      expect(await allNodes[i].equals(allReshuffled[i])).toBeTruthy()\n    }\n  })\n\n  it('saves and loads from blockstore', async () => {\n    const root = await util.saveMst(blockstore, mst)\n    const loaded = await MST.load(blockstore, root)\n    const origNodes = await mst.allNodes()\n    const loadedNodes = await loaded.allNodes()\n    expect(origNodes.length).toBe(loadedNodes.length)\n    for (let i = 0; i < origNodes.length; i++) {\n      expect(await origNodes[i].equals(loadedNodes[i])).toBeTruthy()\n    }\n  })\n\n  it('diffs', async () => {\n    let toDiff = mst\n\n    const toAdd = Object.entries(\n      await util.generateBulkDataKeys(100, blockstore),\n    )\n    const toEdit = shuffled.slice(500, 600)\n    const toDel = shuffled.slice(400, 500)\n\n    const expectedAdds: Record<string, DataAdd> = {}\n    const expectedUpdates: Record<string, DataUpdate> = {}\n    const expectedDels: Record<string, DataDelete> = {}\n\n    for (const entry of toAdd) {\n      toDiff = await toDiff.add(entry[0], entry[1])\n      expectedAdds[entry[0]] = { key: entry[0], cid: entry[1] }\n    }\n    for (const entry of toEdit) {\n      const updated = await util.randomCid()\n      toDiff = await toDiff.update(entry[0], updated)\n      expectedUpdates[entry[0]] = {\n        key: entry[0],\n        prev: entry[1],\n        cid: updated,\n      }\n    }\n    for (const entry of toDel) {\n      toDiff = await toDiff.delete(entry[0])\n      expectedDels[entry[0]] = { key: entry[0], cid: entry[1] }\n    }\n\n    const diff = await DataDiff.of(toDiff, mst)\n\n    expect(diff.addList().length).toBe(100)\n    expect(diff.updateList().length).toBe(100)\n    expect(diff.deleteList().length).toBe(100)\n\n    expect(diff.adds).toEqual(expectedAdds)\n    expect(diff.updates).toEqual(expectedUpdates)\n    expect(diff.deletes).toEqual(expectedDels)\n\n    // ensure we correctly report all added CIDs\n    for await (const entry of toDiff.walk()) {\n      let cid: CID\n      if (entry.isTree()) {\n        cid = await entry.getPointer()\n      } else {\n        cid = entry.value\n      }\n      const found =\n        (await blockstore.has(cid)) ||\n        diff.newMstBlocks.has(cid) ||\n        diff.newLeafCids.has(cid)\n      expect(found).toBeTruthy()\n    }\n  })\n\n  describe('utils', () => {\n    it('counts prefix length', () => {\n      expect(countPrefixLen('abc', 'abc')).toBe(3)\n      expect(countPrefixLen('', 'abc')).toBe(0)\n      expect(countPrefixLen('abc', '')).toBe(0)\n      expect(countPrefixLen('ab', 'abc')).toBe(2)\n      expect(countPrefixLen('abc', 'ab')).toBe(2)\n      expect(countPrefixLen('abcde', 'abc')).toBe(3)\n      expect(countPrefixLen('abc', 'abcde')).toBe(3)\n      expect(countPrefixLen('abcde', 'abc1')).toBe(3)\n      expect(countPrefixLen('abcde', 'abb')).toBe(2)\n      expect(countPrefixLen('abcde', 'qbb')).toBe(0)\n      expect(countPrefixLen('', 'asdf')).toBe(0)\n      expect(countPrefixLen('abc', 'abc\\x00')).toBe(3)\n      expect(countPrefixLen('abc\\x00', 'abc')).toBe(3)\n    })\n  })\n\n  describe('MST Interop Allowable Keys', () => {\n    let blockstore: MemoryBlockstore\n    let mst: MST\n    let cid1: CID\n\n    beforeAll(async () => {\n      blockstore = new MemoryBlockstore()\n      mst = await MST.create(blockstore)\n      cid1 = CID.parse(\n        'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n      )\n    })\n\n    const expectReject = async (key: string) => {\n      const promise = mst.add(key, cid1)\n      await expect(promise).rejects.toThrow(InvalidMstKeyError)\n    }\n\n    const expectAllow = async (key: string) => {\n      await mst.add(key, cid1)\n    }\n\n    it('rejects the empty key', async () => {\n      await expectReject('')\n    })\n\n    it('rejects a key with no collection', async () => {\n      await expectReject('asdf')\n    })\n\n    it('rejects a key with a nested collection', async () => {\n      await expectReject('nested/collection/asdf')\n    })\n\n    it('rejects on empty coll or rkey', async () => {\n      await expectReject('coll/')\n      await expectReject('/rkey')\n    })\n\n    it('rejects non-ascii chars', async () => {\n      await expectReject('coll/jalapeñoA')\n      await expectReject('coll/coöperative')\n      await expectReject('coll/abc💩')\n    })\n\n    it('rejects ascii that we dont support', async () => {\n      await expectReject('coll/key$')\n      await expectReject('coll/key%')\n      await expectReject('coll/key(')\n      await expectReject('coll/key)')\n      await expectReject('coll/key+')\n      await expectReject('coll/key=')\n      await expectReject('coll/@handle')\n      await expectReject('coll/any space')\n      await expectReject('coll/#extra')\n      await expectReject('coll/any+space')\n      await expectReject('coll/number[3]')\n      await expectReject('coll/number(3)')\n      await expectReject('coll/dHJ1ZQ==')\n      await expectReject('coll/\"quote\"')\n    })\n\n    it('rejects keys over 1024 chars', async () => {\n      await expectReject(\n        'coll/asdofiupoiwqeurfpaosidfuapsodirupasoirupasoeiruaspeoriuaspeoriu2p3o4iu1pqw3oiuaspdfoiuaspdfoiuasdfpoiasdufpwoieruapsdofiuaspdfoiuasdpfoiausdfpoasidfupasodifuaspdofiuasdpfoiasudfpoasidfuapsodfiuasdpfoiausdfpoasidufpasodifuapsdofiuasdpofiuasdfpoaisdufpaoasdofiupoiwqeurfpaosidfuapsodirupasoirupasoeiruaspeoriuaspeoriu2p3o4iu1pqw3oiuaspdfoiuaspdfoiuasdfpoiasdufpwoieruapsdofiuaspdfoiuasdpfoiausdfpoasidfupasodifuaspdofiuasdpfoiasudfpoasidfuapsodfiuasdpfoiausdfpoasidufpasodifuapsdofiuasdpofiuasdfpoaisdufpaoasdofiupoiwqeurfpaosidfuapsodirupasoirupasoeiruaspeoriuaspeoriu2p3o4iu1pqw3oiuaspdfoiuaspdfoiuasdfpoiasdufpwoieruapsdofiuaspdfoiuasdpfoiausdfpoasidfupasodifuaspdofiuasdpfoiasudfpoasidfuapsodfiuasdpfoiausdfpoasidufpasodifuapsdofiuasdpofiuasdfpoaisdufpaoasdofiupoiwqeurfpaosidfuapsodirupasoirupasoeiruaspeoriuaspeoriu2p3o4iu1pqw3oiuaspdfoiuaspdfoiuasdfpoiasdufpwoieruapsdofiuaspdfoiuasdpfoiausdfpoasidfupasodifuaspdofiuasdpfoiasudfpoasidfuapsodfiuasdpfoiausdfpoasidufpasodifuapsdofiuasdpofiuasdfpoaisdufpaoasdofiupoiwqeurfpaosidfuapsodirupasoirupasoeiruaspeoriuaspeoriu2p3o4iu1pqw3oiuaspdfoiuaspdfoiuasdfpoiasdufpwoieruapsdofiuaspdfoiuasdpfoiausdfpoasidfupasodifuaspdofiuasdpfoiasudfpoasidfuapsodfiuasdpfoiausdfpoasidufpasodifuapsdofiuasdpofiuasdfpoaisdufpao',\n      )\n    })\n\n    it('allows valid keys', async () => {\n      await expectAllow('coll/3jui7kd54zh2y')\n      await expectAllow('coll/self')\n      await expectAllow('coll/example.com')\n      await expectAllow('com.example/rkey')\n      await expectAllow('coll/~1.2-3_')\n      await expectAllow('coll/dHJ1ZQ')\n      await expectAllow('coll/pre:fix')\n      await expectAllow('coll/_')\n    })\n  })\n\n  describe('MST Interop Known Maps', () => {\n    let blockstore: MemoryBlockstore\n    let mst: MST\n    let cid1: CID\n\n    beforeAll(async () => {\n      blockstore = new MemoryBlockstore()\n      cid1 = CID.parse(\n        'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n      )\n    })\n\n    beforeEach(async () => {\n      mst = await MST.create(blockstore)\n    })\n\n    it('computes \"empty\" tree root CID', async () => {\n      expect(await mst.leafCount()).toBe(0)\n      expect((await mst.getPointer()).toString()).toBe(\n        'bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm',\n      )\n    })\n\n    it('computes \"trivial\" tree root CID', async () => {\n      mst = await mst.add('com.example.record/3jqfcqzm3fo2j', cid1)\n      expect(await mst.leafCount()).toBe(1)\n      expect((await mst.getPointer()).toString()).toBe(\n        'bafyreibj4lsc3aqnrvphp5xmrnfoorvru4wynt6lwidqbm2623a6tatzdu',\n      )\n    })\n\n    it('computes \"singlelayer2\" tree root CID', async () => {\n      mst = await mst.add('com.example.record/3jqfcqzm3fx2j', cid1)\n      expect(await mst.leafCount()).toBe(1)\n      expect(await mst.layer).toBe(2)\n      expect((await mst.getPointer()).toString()).toBe(\n        'bafyreih7wfei65pxzhauoibu3ls7jgmkju4bspy4t2ha2qdjnzqvoy33ai',\n      )\n    })\n\n    it('computes \"simple\" tree root CID', async () => {\n      mst = await mst.add('com.example.record/3jqfcqzm3fp2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fr2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fs2j', cid1) // level 1\n      mst = await mst.add('com.example.record/3jqfcqzm3ft2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm4fc2j', cid1) // level 0\n      expect(await mst.leafCount()).toBe(5)\n      expect((await mst.getPointer()).toString()).toBe(\n        'bafyreicmahysq4n6wfuxo522m6dpiy7z7qzym3dzs756t5n7nfdgccwq7m',\n      )\n    })\n  })\n\n  describe('MST Interop Edge Cases', () => {\n    let blockstore: MemoryBlockstore\n    let mst: MST\n    let cid1: CID\n\n    beforeAll(async () => {\n      blockstore = new MemoryBlockstore()\n      cid1 = CID.parse(\n        'bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454',\n      )\n    })\n\n    beforeEach(async () => {\n      mst = await MST.create(blockstore)\n    })\n\n    it('trims top of tree on delete', async () => {\n      const l1root =\n        'bafyreifnqrwbk6ffmyaz5qtujqrzf5qmxf7cbxvgzktl4e3gabuxbtatv4'\n      const l0root =\n        'bafyreie4kjuxbwkhzg2i5dljaswcroeih4dgiqq6pazcmunwt2byd725vi'\n\n      mst = await mst.add('com.example.record/3jqfcqzm3fn2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fo2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fp2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fs2j', cid1) // level 1\n      mst = await mst.add('com.example.record/3jqfcqzm3ft2j', cid1) // level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fu2j', cid1) // level 0\n\n      expect(await mst.leafCount()).toBe(6)\n      expect(await mst.layer).toBe(1)\n      expect((await mst.getPointer()).toString()).toBe(l1root)\n\n      mst = await mst.delete('com.example.record/3jqfcqzm3fs2j') // level 1\n      expect(await mst.leafCount()).toBe(5)\n      expect(await mst.layer).toBe(0)\n      expect((await mst.getPointer()).toString()).toBe(l0root)\n    })\n\n    /**\n     *\n     *                *                                  *\n     *       _________|________                      ____|_____\n     *       |   |    |    |   |                    |    |     |\n     *       *   d    *    i   *       ->           *    f     *\n     *     __|__    __|__    __|__                __|__      __|___\n     *    |  |  |  |  |  |  |  |  |              |  |  |    |  |   |\n     *    a  b  c  e  g  h  j  k  l              *  d  *    *  i   *\n     *                                         __|__   |   _|_   __|__\n     *                                        |  |  |  |  |   | |  |  |\n     *                                        a  b  c  e  g   h j  k  l\n     *\n     */\n    it('handles insertion that splits two layers down', async () => {\n      const l1root =\n        'bafyreiettyludka6fpgp33stwxfuwhkzlur6chs4d2v4nkmq2j3ogpdjem'\n      const l2root =\n        'bafyreid2x5eqs4w4qxvc5jiwda4cien3gw2q6cshofxwnvv7iucrmfohpm'\n\n      mst = await mst.add('com.example.record/3jqfcqzm3fo2j', cid1) // A; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fp2j', cid1) // B; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fr2j', cid1) // C; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fs2j', cid1) // D; level 1\n      mst = await mst.add('com.example.record/3jqfcqzm3ft2j', cid1) // E; level 0\n      // GAP for F\n      mst = await mst.add('com.example.record/3jqfcqzm3fz2j', cid1) // G; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm4fc2j', cid1) // H; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm4fd2j', cid1) // I; level 1\n      mst = await mst.add('com.example.record/3jqfcqzm4ff2j', cid1) // J; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm4fg2j', cid1) // K; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm4fh2j', cid1) // L; level 0\n\n      expect(await mst.leafCount()).toBe(11)\n      expect(await mst.layer).toBe(1)\n      expect((await mst.getPointer()).toString()).toBe(l1root)\n\n      // insert F, which will push E out of the node with G+H to a new node under D\n      mst = await mst.add('com.example.record/3jqfcqzm3fx2j', cid1) // F; level 2\n      expect(await mst.leafCount()).toBe(12)\n      expect(await mst.layer).toBe(2)\n      expect((await mst.getPointer()).toString()).toBe(l2root)\n\n      // remove F, which should push E back over with G+H\n      mst = await mst.delete('com.example.record/3jqfcqzm3fx2j') // F; level 2\n      expect(await mst.leafCount()).toBe(11)\n      expect(await mst.layer).toBe(1)\n      expect((await mst.getPointer()).toString()).toBe(l1root)\n    })\n\n    /**\n     *\n     *          *        ->            *\n     *        __|__                  __|__\n     *       |     |                |  |  |\n     *       a     c                *  b  *\n     *                              |     |\n     *                              *     *\n     *                              |     |\n     *                              a     c\n     *\n     */\n    it('handles new layers that are two higher than existing', async () => {\n      const l0root =\n        'bafyreidfcktqnfmykz2ps3dbul35pepleq7kvv526g47xahuz3rqtptmky'\n      const l2root =\n        'bafyreiavxaxdz7o7rbvr3zg2liox2yww46t7g6hkehx4i4h3lwudly7dhy'\n      const l2root2 =\n        'bafyreig4jv3vuajbsybhyvb7gggvpwh2zszwfyttjrj6qwvcsp24h6popu'\n\n      mst = await mst.add('com.example.record/3jqfcqzm3ft2j', cid1) // A; level 0\n      mst = await mst.add('com.example.record/3jqfcqzm3fz2j', cid1) // C; level 0\n      expect(await mst.leafCount()).toBe(2)\n      expect(await mst.layer).toBe(0)\n      expect((await mst.getPointer()).toString()).toBe(l0root)\n\n      // insert B, which is two levels above\n      mst = await mst.add('com.example.record/3jqfcqzm3fx2j', cid1) // B; level 2\n      expect(await mst.leafCount()).toBe(3)\n      expect(await mst.layer).toBe(2)\n      expect((await mst.getPointer()).toString()).toBe(l2root)\n\n      // remove B\n      mst = await mst.delete('com.example.record/3jqfcqzm3fx2j') // B; level 2\n      expect(await mst.leafCount()).toBe(2)\n      expect(await mst.layer).toBe(0)\n      expect((await mst.getPointer()).toString()).toBe(l0root)\n\n      // insert B (level=2) and D (level=1)\n      mst = await mst.add('com.example.record/3jqfcqzm3fx2j', cid1) // B; level 2\n      mst = await mst.add('com.example.record/3jqfcqzm4fd2j', cid1) // D; level 1\n      expect(await mst.leafCount()).toBe(4)\n      expect(await mst.layer).toBe(2)\n      expect((await mst.getPointer()).toString()).toBe(l2root2)\n\n      // remove D\n      mst = await mst.delete('com.example.record/3jqfcqzm4fd2j') // D; level 1\n      expect(await mst.leafCount()).toBe(3)\n      expect(await mst.layer).toBe(2)\n      expect((await mst.getPointer()).toString()).toBe(l2root)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/repo/tests/proofs.test.ts",
    "content": "import { TID, cidForCbor, streamToBuffer } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { RecordCidClaim, RecordPath, Repo, RepoContents } from '../src'\nimport { MemoryBlockstore } from '../src/storage'\nimport * as sync from '../src/sync'\nimport * as util from './_util'\n\ndescribe('Repo Proofs', () => {\n  let storage: MemoryBlockstore\n  let repo: Repo\n  let keypair: crypto.Keypair\n  let repoData: RepoContents\n\n  const repoDid = 'did:example:test'\n\n  beforeAll(async () => {\n    storage = new MemoryBlockstore()\n    keypair = await crypto.Secp256k1Keypair.create()\n    repo = await Repo.create(storage, repoDid, keypair)\n    const filled = await util.fillRepo(repo, keypair, 5)\n    repo = filled.repo\n    repoData = filled.data\n  })\n\n  const getProofs = async (claims: RecordPath[]) => {\n    return streamToBuffer(sync.getRecords(storage, repo.cid, claims))\n  }\n\n  const doVerify = (proofs: Uint8Array, claims: RecordCidClaim[]) => {\n    return sync.verifyProofs(proofs, claims, repoDid, keypair.did())\n  }\n\n  const contentsToClaims = async (\n    contents: RepoContents,\n  ): Promise<RecordCidClaim[]> => {\n    const claims: RecordCidClaim[] = []\n    for (const coll of Object.keys(contents)) {\n      for (const rkey of Object.keys(contents[coll])) {\n        claims.push({\n          collection: coll,\n          rkey: rkey,\n          cid: await cidForCbor(contents[coll][rkey]),\n        })\n      }\n    }\n    return claims\n  }\n\n  it('verifies valid records', async () => {\n    const claims = await contentsToClaims(repoData)\n    const proofs = await getProofs(claims)\n    const results = await doVerify(proofs, claims)\n    expect(results.verified.length).toBeGreaterThan(0)\n    expect(results.verified).toEqual(claims)\n    expect(results.unverified.length).toBe(0)\n  })\n\n  it('verifies record nonexistence', async () => {\n    const claims: RecordCidClaim[] = [\n      {\n        collection: util.testCollections[0],\n        rkey: TID.nextStr(), // does not exist\n        cid: null,\n      },\n    ]\n    const proofs = await getProofs(claims)\n    const results = await doVerify(proofs, claims)\n    expect(results.verified.length).toBeGreaterThan(0)\n    expect(results.verified).toEqual(claims)\n    expect(results.unverified.length).toBe(0)\n  })\n\n  it('does not verify a record that doesnt exist', async () => {\n    const realClaims = await contentsToClaims(repoData)\n    const claims: RecordCidClaim[] = [\n      {\n        ...realClaims[0],\n        rkey: TID.nextStr(),\n      },\n    ]\n    const proofs = await getProofs(claims)\n    const results = await doVerify(proofs, claims)\n    expect(results.verified.length).toBe(0)\n    expect(results.unverified.length).toBeGreaterThan(0)\n    expect(results.unverified).toEqual(claims)\n  })\n\n  it('does not verify an invalid record at a real path', async () => {\n    const realClaims = await contentsToClaims(repoData)\n    const claims: RecordCidClaim[] = [\n      {\n        ...realClaims[0],\n        cid: await util.randomCid(),\n      },\n    ]\n    const proofs = await getProofs(claims)\n    const results = await doVerify(proofs, claims)\n    expect(results.verified.length).toBe(0)\n    expect(results.unverified.length).toBeGreaterThan(0)\n    expect(results.unverified).toEqual(claims)\n  })\n\n  it('does not verify a delete where the record does exist', async () => {\n    const realClaims = await contentsToClaims(repoData)\n    const claims: RecordCidClaim[] = [\n      {\n        collection: realClaims[0].collection,\n        rkey: realClaims[0].rkey,\n        cid: null,\n      },\n    ]\n    const proofs = await getProofs(claims)\n    const results = await doVerify(proofs, claims)\n    expect(results.verified.length).toBe(0)\n    expect(results.unverified.length).toBeGreaterThan(0)\n    expect(results.unverified).toEqual(claims)\n  })\n\n  it('can determine record proofs from car file', async () => {\n    const possible = await contentsToClaims(repoData)\n    const claims = [\n      //random sampling of records\n      possible[0],\n      possible[4],\n      possible[5],\n      possible[8],\n    ]\n    const proofs = await getProofs(claims)\n    const records = await sync.verifyRecords(proofs, repoDid, keypair.did())\n    for (const record of records) {\n      const foundClaim = claims.find(\n        (claim) =>\n          claim.collection === record.collection && claim.rkey === record.rkey,\n      )\n      if (!foundClaim) {\n        throw new Error('Could not find record for claim')\n      }\n      expect(foundClaim.cid).toEqual(\n        await cidForCbor(repoData[record.collection][record.rkey]),\n      )\n    }\n  })\n\n  it('verifyProofs throws on a bad signature', async () => {\n    const badRepo = await util.addBadCommit(repo, keypair)\n    const claims = await contentsToClaims(repoData)\n    const proofs = await streamToBuffer(\n      sync.getRecords(storage, badRepo.cid, claims),\n    )\n    const fn = sync.verifyProofs(proofs, claims, repoDid, keypair.did())\n    await expect(fn).rejects.toThrow(sync.RepoVerificationError)\n  })\n})\n"
  },
  {
    "path": "packages/repo/tests/repo.test.ts",
    "content": "import { TID } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { RepoContents, WriteOpAction, verifyCommitSig } from '../src'\nimport { Repo } from '../src/repo'\nimport { MemoryBlockstore } from '../src/storage'\nimport * as util from './_util'\n\ndescribe('Repo', () => {\n  const collName = 'com.example.posts'\n\n  let storage: MemoryBlockstore\n  let keypair: crypto.Keypair\n  let repo: Repo\n  let repoData: RepoContents\n\n  it('creates repo', async () => {\n    storage = new MemoryBlockstore()\n    keypair = await Secp256k1Keypair.create()\n    repo = await Repo.create(storage, keypair.did(), keypair)\n  })\n\n  it('has proper metadata', async () => {\n    expect(repo.did).toEqual(keypair.did())\n    expect(repo.version).toBe(3)\n  })\n\n  it('does basic operations', async () => {\n    const rkey = TID.nextStr()\n    const record = util.generateObject()\n    repo = await repo.applyWrites(\n      {\n        action: WriteOpAction.Create,\n        collection: collName,\n        rkey,\n        record,\n      },\n      keypair,\n    )\n\n    let got = await repo.getRecord(collName, rkey)\n    expect(got).toEqual(record)\n\n    const updatedRecord = util.generateObject()\n    repo = await repo.applyWrites(\n      {\n        action: WriteOpAction.Update,\n        collection: collName,\n        rkey,\n        record: updatedRecord,\n      },\n      keypair,\n    )\n    got = await repo.getRecord(collName, rkey)\n    expect(got).toEqual(updatedRecord)\n\n    repo = await repo.applyWrites(\n      {\n        action: WriteOpAction.Delete,\n        collection: collName,\n        rkey: rkey,\n      },\n      keypair,\n    )\n    got = await repo.getRecord(collName, rkey)\n    expect(got).toBeNull()\n  })\n\n  it('adds content collections', async () => {\n    const filled = await util.fillRepo(repo, keypair, 100)\n    repo = filled.repo\n    repoData = filled.data\n    const contents = await repo.getContents()\n    expect(contents).toEqual(repoData)\n  })\n\n  it('edits and deletes content', async () => {\n    const edit = await util.formatEdit(repo, repoData, keypair, {\n      adds: 20,\n      updates: 20,\n      deletes: 20,\n    })\n    repo = await repo.applyCommit(edit.commit)\n    repoData = edit.data\n    const contents = await repo.getContents()\n    expect(contents).toEqual(repoData)\n  })\n\n  it('has a valid signature to commit', async () => {\n    const verified = await verifyCommitSig(repo.commit, keypair.did())\n    expect(verified).toBeTruthy()\n  })\n\n  it('sets correct DID', async () => {\n    expect(repo.did).toEqual(keypair.did())\n  })\n\n  it('loads from blockstore', async () => {\n    const reloadedRepo = await Repo.load(storage, repo.cid)\n\n    const contents = await reloadedRepo.getContents()\n    expect(contents).toEqual(repoData)\n    expect(repo.did).toEqual(keypair.did())\n    expect(repo.version).toBe(3)\n  })\n})\n"
  },
  {
    "path": "packages/repo/tests/sync.test.ts",
    "content": "import { streamToBuffer } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport {\n  CidSet,\n  Repo,\n  RepoContents,\n  RepoVerificationError,\n  getAndParseRecord,\n  readCar,\n  readCarWithRoot,\n} from '../src'\nimport { MemoryBlockstore } from '../src/storage'\nimport * as sync from '../src/sync'\nimport * as util from './_util'\n\ndescribe('Repo Sync', () => {\n  let storage: MemoryBlockstore\n  let repo: Repo\n  let keypair: crypto.Keypair\n  let repoData: RepoContents\n\n  const repoDid = 'did:example:test'\n\n  beforeAll(async () => {\n    storage = new MemoryBlockstore()\n    keypair = await crypto.Secp256k1Keypair.create()\n    repo = await Repo.create(storage, repoDid, keypair)\n    const filled = await util.fillRepo(repo, keypair, 20)\n    repo = filled.repo\n    repoData = filled.data\n  })\n\n  it('sync a full repo', async () => {\n    const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))\n    const car = await readCarWithRoot(carBytes)\n    const verified = await sync.verifyRepo(\n      car.blocks,\n      car.root,\n      repoDid,\n      keypair.did(),\n    )\n    const syncStorage = new MemoryBlockstore()\n    await syncStorage.applyCommit(verified.commit)\n    const loadedRepo = await Repo.load(syncStorage, car.root)\n    const contents = await loadedRepo.getContents()\n    expect(contents).toEqual(repoData)\n    const contentsFromOps: RepoContents = {}\n    for (const write of verified.creates) {\n      contentsFromOps[write.collection] ??= {}\n      const parsed = await getAndParseRecord(car.blocks, write.cid)\n      contentsFromOps[write.collection][write.rkey] = parsed.record\n    }\n    expect(contentsFromOps).toEqual(repoData)\n  })\n\n  it('does not sync duplicate blocks', async () => {\n    const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))\n    const car = await readCar(carBytes)\n    const cids = new CidSet()\n    car.blocks.forEach((_, cid) => {\n      if (cids.has(cid)) {\n        throw new Error(`duplicate block: :${cid.toString()}`)\n      }\n      cids.add(cid)\n    })\n  })\n\n  it('syncs a repo that is behind', async () => {\n    // add more to providers's repo & have consumer catch up\n    const edit = await util.formatEdit(repo, repoData, keypair, {\n      adds: 10,\n      updates: 10,\n      deletes: 10,\n    })\n    const verified = await sync.verifyDiff(\n      repo,\n      edit.commit.newBlocks,\n      edit.commit.cid,\n      repoDid,\n      keypair.did(),\n    )\n    await storage.applyCommit(verified.commit)\n    repo = await Repo.load(storage, verified.commit.cid)\n    const contents = await repo.getContents()\n    expect(contents).toEqual(edit.data)\n  })\n\n  it('throws on a bad signature', async () => {\n    const badRepo = await util.addBadCommit(repo, keypair)\n    const carBytes = await streamToBuffer(\n      sync.getFullRepo(storage, badRepo.cid),\n    )\n    const car = await readCarWithRoot(carBytes)\n    await expect(\n      sync.verifyRepo(car.blocks, car.root, repoDid, keypair.did()),\n    ).rejects.toThrow(RepoVerificationError)\n  })\n})\n"
  },
  {
    "path": "packages/repo/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/repo/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/repo/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/sync/CHANGELOG.md",
    "content": "# @atproto/sync\n\n## 0.1.40\n\n### Patch Changes\n\n- [#4705](https://github.com/bluesky-social/atproto/pull/4705) [`138f0a0`](https://github.com/bluesky-social/atproto/commit/138f0a0b374c0d78372d5095237061d46db75a32) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix test flakiness\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/common@0.5.14\n  - @atproto/xrpc-server@0.10.15\n  - @atproto/lexicon@0.6.2\n\n## 0.1.39\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lexicon@0.6.0\n  - @atproto/common@0.5.3\n  - @atproto/xrpc-server@0.10.3\n  - @atproto/repo@0.8.12\n\n## 0.1.38\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/xrpc-server@0.10.0\n  - @atproto/common@0.5.0\n  - @atproto/identity@0.4.10\n  - @atproto/lexicon@0.5.2\n  - @atproto/repo@0.8.11\n\n## 0.1.37\n\n### Patch Changes\n\n- Updated dependencies [[`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/xrpc-server@0.9.6\n\n## 0.1.36\n\n### Patch Changes\n\n- [#4038](https://github.com/bluesky-social/atproto/pull/4038) [`756ab5d87`](https://github.com/bluesky-social/atproto/commit/756ab5d87fea75e8648a6bdd545d8b441bfb2dd6) Thanks [@knotbin](https://github.com/knotbin)! - filtercollections wildcard endings\n\n## 0.1.35\n\n### Patch Changes\n\n- Updated dependencies [[`8dd77bad2`](https://github.com/bluesky-social/atproto/commit/8dd77bad2fdee20e39d3787198d960c19d8df3d0)]:\n  - @atproto/repo@0.8.10\n\n## 0.1.34\n\n### Patch Changes\n\n- Updated dependencies [[`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099), [`055a413fb`](https://github.com/bluesky-social/atproto/commit/055a413fba4fab510ec899377154f1204ab12099)]:\n  - @atproto/repo@0.8.9\n  - @atproto/common@0.4.12\n  - @atproto/identity@0.4.9\n  - @atproto/lexicon@0.5.1\n  - @atproto/xrpc-server@0.9.5\n\n## 0.1.33\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/syntax@0.4.1\n  - @atproto/repo@0.8.8\n  - @atproto/xrpc-server@0.9.4\n\n## 0.1.32\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/repo@0.8.7\n  - @atproto/xrpc-server@0.9.3\n\n## 0.1.31\n\n### Patch Changes\n\n- [#4069](https://github.com/bluesky-social/atproto/pull/4069) [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12) Thanks [@devinivy](https://github.com/devinivy)! - Perform CID-to-content verification in event CARs.\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12), [`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/repo@0.8.6\n  - @atproto/lexicon@0.4.13\n  - @atproto/xrpc-server@0.9.2\n\n## 0.1.30\n\n### Patch Changes\n\n- Updated dependencies [[`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671), [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671)]:\n  - @atproto/xrpc-server@0.9.1\n\n## 0.1.29\n\n### Patch Changes\n\n- Updated dependencies [[`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20), [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/xrpc-server@0.9.0\n  - @atproto/lexicon@0.4.12\n  - @atproto/repo@0.8.5\n\n## 0.1.28\n\n### Patch Changes\n\n- Updated dependencies [[`a8dee6af3`](https://github.com/bluesky-social/atproto/commit/a8dee6af33618d3072ebae7f23843242a32c926c)]:\n  - @atproto/repo@0.8.4\n\n## 0.1.27\n\n### Patch Changes\n\n- Updated dependencies [[`5fccbd2a1`](https://github.com/bluesky-social/atproto/commit/5fccbd2a14420e4a7c6f56ad9af4ecfe15a971e3)]:\n  - @atproto/repo@0.8.3\n\n## 0.1.26\n\n### Patch Changes\n\n- Updated dependencies [[`8bd45e2f8`](https://github.com/bluesky-social/atproto/commit/8bd45e2f898a87b3550c7f4a0c8312fad9cb4736)]:\n  - @atproto/repo@0.8.2\n\n## 0.1.25\n\n### Patch Changes\n\n- Updated dependencies [[`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c), [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c)]:\n  - @atproto/xrpc-server@0.8.0\n\n## 0.1.24\n\n### Patch Changes\n\n- Updated dependencies [[`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f)]:\n  - @atproto/xrpc-server@0.7.19\n\n## 0.1.23\n\n### Patch Changes\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812), [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba)]:\n  - @atproto/lexicon@0.4.11\n  - @atproto/xrpc-server@0.7.18\n  - @atproto/common@0.4.11\n  - @atproto/identity@0.4.8\n  - @atproto/repo@0.8.1\n\n## 0.1.22\n\n### Patch Changes\n\n- Updated dependencies [[`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9)]:\n  - @atproto/xrpc-server@0.7.17\n\n## 0.1.21\n\n### Patch Changes\n\n- Updated dependencies [[`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c)]:\n  - @atproto/xrpc-server@0.7.16\n\n## 0.1.20\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144), [`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/repo@0.8.0\n  - @atproto/common@0.4.10\n  - @atproto/identity@0.4.7\n  - @atproto/lexicon@0.4.10\n  - @atproto/xrpc-server@0.7.15\n\n## 0.1.19\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f), [`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/repo@0.7.3\n  - @atproto/common@0.4.9\n  - @atproto/xrpc-server@0.7.14\n\n## 0.1.18\n\n### Patch Changes\n\n- Updated dependencies [[`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee)]:\n  - @atproto/syntax@0.4.0\n  - @atproto/lexicon@0.4.9\n  - @atproto/repo@0.7.2\n  - @atproto/xrpc-server@0.7.13\n\n## 0.1.17\n\n### Patch Changes\n\n- [#3599](https://github.com/bluesky-social/atproto/pull/3599) [`b20907a70`](https://github.com/bluesky-social/atproto/commit/b20907a7056970ab627e6c661882cb16491801e2) Thanks [@mozzius](https://github.com/mozzius)! - Fix types of `FirehoseOptions`\n\n- [`d96b03956`](https://github.com/bluesky-social/atproto/commit/d96b03956d5c26c238f586c6bdf257c080f12746) Thanks [@devinivy](https://github.com/devinivy)! - Support firehose sync event\n\n## 0.1.16\n\n### Patch Changes\n\n- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:\n  - @atproto/syntax@0.3.4\n  - @atproto/lexicon@0.4.8\n  - @atproto/repo@0.7.1\n  - @atproto/xrpc-server@0.7.12\n\n## 0.1.15\n\n### Patch Changes\n\n- Updated dependencies [[`7e3678c08`](https://github.com/bluesky-social/atproto/commit/7e3678c089d2faa1a884a52a4fb80b8116c9854f)]:\n  - @atproto/repo@0.7.0\n\n## 0.1.14\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/syntax@0.3.3\n  - @atproto/lexicon@0.4.7\n  - @atproto/repo@0.6.5\n  - @atproto/xrpc-server@0.7.11\n\n## 0.1.13\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/xrpc-server@0.7.10\n  - @atproto/identity@0.4.6\n  - @atproto/lexicon@0.4.6\n  - @atproto/common@0.4.8\n  - @atproto/syntax@0.3.2\n  - @atproto/repo@0.6.4\n\n## 0.1.12\n\n### Patch Changes\n\n- Updated dependencies [[`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75), [`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/xrpc-server@0.7.9\n  - @atproto/common@0.4.7\n  - @atproto/repo@0.6.3\n\n## 0.1.11\n\n### Patch Changes\n\n- Updated dependencies [[`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12)]:\n  - @atproto/xrpc-server@0.7.8\n\n## 0.1.10\n\n### Patch Changes\n\n- Updated dependencies [[`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61)]:\n  - @atproto/xrpc-server@0.7.7\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/identity@0.4.5\n  - @atproto/repo@0.6.2\n  - @atproto/xrpc-server@0.7.6\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:\n  - @atproto/identity@0.4.4\n  - @atproto/common@0.4.6\n  - @atproto/lexicon@0.4.5\n  - @atproto/repo@0.6.1\n  - @atproto/xrpc-server@0.7.5\n\n## 0.1.7\n\n### Patch Changes\n\n- [#2992](https://github.com/bluesky-social/atproto/pull/2992) [`0bec389a1`](https://github.com/bluesky-social/atproto/commit/0bec389a1c53adbcfab7b877df9b291d44d8ea33) Thanks [@dholms](https://github.com/dholms)! - Pass through options to websocket\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95), [`c9848edaf`](https://github.com/bluesky-social/atproto/commit/c9848edaf0947727aa5a60e3c67eecda3f48d46a)]:\n  - @atproto/common@0.4.5\n  - @atproto/lexicon@0.4.4\n  - @atproto/repo@0.6.0\n  - @atproto/xrpc-server@0.7.4\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d), [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/syntax@0.3.1\n  - @atproto/lexicon@0.4.3\n  - @atproto/repo@0.5.5\n  - @atproto/xrpc-server@0.7.3\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [[`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0)]:\n  - @atproto/xrpc-server@0.7.2\n  - @atproto/identity@0.4.3\n  - @atproto/repo@0.5.4\n\n## 0.1.4\n\n### Patch Changes\n\n- [#2906](https://github.com/bluesky-social/atproto/pull/2906) [`d605577c2`](https://github.com/bluesky-social/atproto/commit/d605577c25d3e69c7cc0a1e858a4f009d1ea3096) Thanks [@dholms](https://github.com/dholms)! - avoid parsing commits with no relevant ops\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]:\n  - @atproto/common@0.4.4\n  - @atproto/repo@0.5.3\n  - @atproto/xrpc-server@0.7.1\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/xrpc-server@0.7.0\n  - @atproto/lexicon@0.4.2\n  - @atproto/common@0.4.3\n  - @atproto/identity@0.4.2\n  - @atproto/repo@0.5.2\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/common@0.4.2\n  - @atproto/xrpc-server@0.6.4\n  - @atproto/repo@0.5.1\n\n## 0.1.0\n\n### Minor Changes\n\n- [#2752](https://github.com/bluesky-social/atproto/pull/2752) [`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442) Thanks [@dholms](https://github.com/dholms)! - Introduced initial sync package for consuming firehose (com.atproto.sync.subscribeRepos)\n\n### Patch Changes\n\n- Updated dependencies [[`b15dec2f4`](https://github.com/bluesky-social/atproto/commit/b15dec2f4feb25ac91b169c83ccff1adbb5a9442)]:\n  - @atproto/repo@0.5.0\n"
  },
  {
    "path": "packages/sync/README.md",
    "content": "# @atproto/sync: atproto sync tools\n\nTypeScript library for syncing data from the [atproto](https://atproto.com) network. Currently only supports firehose (relay) subscriptions\n\n[![NPM](https://img.shields.io/npm/v/@atproto/sync)](https://www.npmjs.com/package/@atproto/sync)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Usage\n\nThe firehose class will spin up a websocket connection to `com.atproto.sync.subscribeRepos` on a given repo host (by default the Relay run by Bluesky).\n\nEach event will be parsed, authenticated, and then passed on to the supplied `handleEvt` which can handle indexing.\n\nOn `Commit` events, the firehose will verify signatures and repo proofs to ensure that the event is authentic. This can be disabled with the `unauthenticatedCommits` flag. Similarly on `Identity` events, the firehose will fetch the latest DID document for the repo and do bidirectional verification on the associated handle. This can be disabled with the `unauthenticatedHandles` flag.\n\nEvents of a certain type can be excluded using the `excludeIdentity`/`excludeAccount`/`excludeCommit` flags. And repo writes can be filtered down to specific collections using `filterCollections`. By default, all events are parsed and passed through to the handler. Note that this filtered currently happens client-side, though it is likely we will introduce server-side methods for doing so in the future.\n\nNon-fatal errors that are encountered will be passed to the required `onError` handler. In most cases, these can just be logged.\n\nWhen using the firehose class, events are processed serially. Each event must be finished being handled before the next one is parsed and authenticated.\n\n```ts\nimport { Firehose } from '@atproto/sync'\nimport { IdResolver } from '@atproto/identity'\n\nconst idResolver = new IdResolver()\nconst firehose = new Firehose({\n  idResolver,\n  service: 'wss://bsky.network',\n  handleEvt: async (evt) => {\n    if (evt.event === 'identity') {\n      // ...\n    } else if (evt.event === 'account') {\n      // ...\n    } else if (evt.event === 'create') {\n      // ...\n    } else if (evt.event === 'update') {\n      // ...\n    } else if (evt.event === 'delete') {\n      // ...\n    }\n  },\n  onError: (err) => {\n    console.error(err)\n  },\n  filterCollections: ['com.myexample.app'],\n})\nfirehose.start()\n\n// on service shutdown\nawait firehose.destroy()\n```\n\nFor more robust indexing pipelines, it's recommended to use the supplied `MemoryRunner` class. This provides an in-memory partitioned queue. As events from a given repo must be processed in order, this allows events to be processed concurrently while still processing events from any given repo serially.\n\nThe `MemoryRunner` also tracks an internal cursor based on the last finished consecutive work. This ensures that no events are dropped, although it does mean that some events may occassionally be replayed (if the websocket drops and reconnects) and therefore it's recommended that any indexing logic is idempotent. An optional `setCursor` parameter may be supplied to the `MemoryRunner` which can be used to persistently store the most recently processed cursor.\n\n```ts\nimport { Firehose, MemoryRunner } from '@atproto/sync'\nimport { IdResolver } from '@atproto/identity'\n\nconst idResolver = new IdResolver()\nconst runner = new MemoryRunner({\n  setCursor: (cursor) => {\n    // persist cursor\n  },\n})\nconst firehose = new Firehose({\n  idResolver,\n  runner,\n  service: 'wss://bsky.network',\n  handleEvt: async (evt) => {\n    // ...\n  },\n  onError: (err) => {\n    console.error(err)\n  },\n})\nfirehose.start()\n\n// on service shutdown\nawait firehose.destroy()\nawait runner.destroy()\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/sync/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'Sync',\n  transform: { '^.+\\\\.ts$': '@swc/jest' },\n  testTimeout: 60000,\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/sync/package.json",
    "content": "{\n  \"name\": \"@atproto/sync\",\n  \"version\": \"0.1.40\",\n  \"license\": \"MIT\",\n  \"description\": \"atproto sync library\",\n  \"keywords\": [\n    \"atproto\",\n    \"sync\",\n    \"firehose\",\n    \"relay\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/sync\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"../dev-infra/with-test-redis-and-db.sh jest\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/identity\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/repo\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/xrpc-server\": \"workspace:^\",\n    \"multiformats\": \"^9.9.0\",\n    \"p-queue\": \"^6.6.2\",\n    \"ws\": \"^8.12.0\"\n  },\n  \"devDependencies\": {\n    \"@types/ws\": \"^8.5.4\",\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/sync/src/events.ts",
    "content": "import type { CID } from 'multiformats/cid'\nimport { DidDocument } from '@atproto/identity'\nimport type { RepoRecord } from '@atproto/lexicon'\nimport { BlockMap } from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\n\nexport type Event = CommitEvt | SyncEvt | IdentityEvt | AccountEvt\n\nexport type CommitMeta = {\n  seq: number\n  time: string\n  commit: CID\n  blocks: BlockMap\n  rev: string\n  uri: AtUri\n  did: string\n  collection: string\n  rkey: string\n}\n\nexport type CommitEvt = Create | Update | Delete\n\nexport type Create = CommitMeta & {\n  event: 'create'\n  record: RepoRecord\n  cid: CID\n}\n\nexport type Update = CommitMeta & {\n  event: 'update'\n  record: RepoRecord\n  cid: CID\n}\n\nexport type Delete = CommitMeta & {\n  event: 'delete'\n}\n\nexport type SyncEvt = {\n  seq: number\n  time: string\n  event: 'sync'\n  did: string\n  cid: CID\n  rev: string\n  blocks: BlockMap\n}\n\nexport type IdentityEvt = {\n  seq: number\n  time: string\n  event: 'identity'\n  did: string\n  handle?: string\n  didDocument?: DidDocument\n}\n\nexport type AccountEvt = {\n  seq: number\n  time: string\n  event: 'account'\n  did: string\n  active: boolean\n  status?: AccountStatus\n}\n\nexport type AccountStatus =\n  | 'takendown'\n  | 'suspended'\n  | 'deleted'\n  | 'deactivated'\n"
  },
  {
    "path": "packages/sync/src/firehose/index.ts",
    "content": "import { CID } from 'multiformats/cid'\nimport type { ClientOptions } from 'ws'\nimport { Deferrable, createDeferrable, wait } from '@atproto/common'\nimport {\n  DidDocument,\n  IdResolver,\n  parseToAtprotoDocument,\n} from '@atproto/identity'\nimport {\n  RepoVerificationError,\n  cborToLexRecord,\n  formatDataKey,\n  parseDataKey,\n  readCar,\n  readCarWithRoot,\n  verifyProofs,\n} from '@atproto/repo'\nimport { AtUri } from '@atproto/syntax'\nimport { Subscription } from '@atproto/xrpc-server'\nimport {\n  AccountEvt,\n  AccountStatus,\n  CommitEvt,\n  CommitMeta,\n  Event,\n  IdentityEvt,\n  SyncEvt,\n} from '../events'\nimport { EventRunner } from '../runner'\nimport { didAndSeqForEvt } from '../util'\nimport {\n  type Account,\n  type Commit,\n  type Identity,\n  type RepoEvent,\n  RepoOp,\n  type Sync,\n  isAccount,\n  isCommit,\n  isIdentity,\n  isSync,\n  isValidRepoEvent,\n} from './lexicons'\n\nexport type FirehoseOptions = ClientOptions & {\n  idResolver: IdResolver\n\n  handleEvent: (evt: Event) => Awaited<void>\n  onError: (err: Error) => void\n  getCursor?: () => Awaited<number | undefined>\n\n  runner?: EventRunner // should only set getCursor *or* runner\n\n  service?: string\n  subscriptionReconnectDelay?: number\n\n  unauthenticatedCommits?: boolean\n  unauthenticatedHandles?: boolean\n\n  filterCollections?: string[]\n  excludeIdentity?: boolean\n  excludeAccount?: boolean\n  excludeCommit?: boolean\n  excludeSync?: boolean\n}\n\nexport class Firehose {\n  private sub: Subscription<RepoEvent>\n  private abortController: AbortController\n  private destoryDefer: Deferrable\n  private matchCollection: ((col: string) => boolean) | null = null\n\n  constructor(public opts: FirehoseOptions) {\n    this.destoryDefer = createDeferrable()\n    this.abortController = new AbortController()\n    if (this.opts.getCursor && this.opts.runner) {\n      throw new Error('Must set only `getCursor` or `runner`')\n    }\n    if (opts.filterCollections) {\n      const exact = new Set<string>()\n      const prefixes: string[] = []\n\n      for (const pattern of opts.filterCollections) {\n        if (pattern.endsWith('.*')) {\n          prefixes.push(pattern.slice(0, -2))\n        } else {\n          exact.add(pattern)\n        }\n      }\n      this.matchCollection = (col: string): boolean => {\n        if (exact.has(col)) return true\n        for (const prefix of prefixes) {\n          if (col.startsWith(prefix)) return true\n        }\n        return false\n      }\n    }\n    this.sub = new Subscription({\n      ...opts,\n      service: opts.service ?? 'wss://bsky.network',\n      method: 'com.atproto.sync.subscribeRepos',\n      signal: this.abortController.signal,\n      getParams: async () => {\n        const getCursorFn = () =>\n          this.opts.runner?.getCursor() ?? this.opts.getCursor\n        if (!getCursorFn) {\n          return undefined\n        }\n        const cursor = await getCursorFn()\n        return { cursor }\n      },\n      validate: (value: unknown) => {\n        try {\n          return isValidRepoEvent(value)\n        } catch (err) {\n          this.opts.onError(new FirehoseValidationError(err, value))\n        }\n      },\n    })\n  }\n\n  async start() {\n    try {\n      for await (const evt of this.sub) {\n        if (this.opts.runner) {\n          const parsed = didAndSeqForEvt(evt)\n          if (!parsed) {\n            continue\n          }\n          this.opts.runner.trackEvent(parsed.did, parsed.seq, async () => {\n            const parsed = await this.parseEvt(evt)\n            for (const write of parsed) {\n              try {\n                await this.opts.handleEvent(write)\n              } catch (err) {\n                this.opts.onError(new FirehoseHandlerError(err, write))\n              }\n            }\n          })\n        } else {\n          await this.processEvt(evt)\n        }\n      }\n    } catch (err) {\n      if (err && err['name'] === 'AbortError') {\n        this.destoryDefer.resolve()\n        return\n      }\n      this.opts.onError(new FirehoseSubscriptionError(err))\n      await wait(this.opts.subscriptionReconnectDelay ?? 3000)\n      return this.start()\n    }\n  }\n\n  private async parseEvt(evt: RepoEvent): Promise<Event[]> {\n    try {\n      if (isCommit(evt) && !this.opts.excludeCommit) {\n        return this.opts.unauthenticatedCommits\n          ? await parseCommitUnauthenticated(evt, this.matchCollection)\n          : await parseCommitAuthenticated(\n              this.opts.idResolver,\n              evt,\n              this.matchCollection,\n            )\n      } else if (isAccount(evt) && !this.opts.excludeAccount) {\n        const parsed = parseAccount(evt)\n        return parsed ? [parsed] : []\n      } else if (isIdentity(evt) && !this.opts.excludeIdentity) {\n        const parsed = await parseIdentity(\n          this.opts.idResolver,\n          evt,\n          this.opts.unauthenticatedHandles,\n        )\n        return parsed ? [parsed] : []\n      } else if (isSync(evt) && !this.opts.excludeSync) {\n        const parsed = await parseSync(evt)\n        return parsed ? [parsed] : []\n      } else {\n        return []\n      }\n    } catch (err) {\n      this.opts.onError(new FirehoseParseError(err, evt))\n      return []\n    }\n  }\n\n  private async processEvt(evt: RepoEvent) {\n    const parsed = await this.parseEvt(evt)\n    for (const write of parsed) {\n      try {\n        await this.opts.handleEvent(write)\n      } catch (err) {\n        this.opts.onError(new FirehoseHandlerError(err, write))\n      }\n    }\n  }\n\n  async destroy(): Promise<void> {\n    this.abortController.abort()\n    await this.destoryDefer.complete\n  }\n}\n\nexport const parseCommitAuthenticated = async (\n  idResolver: IdResolver,\n  evt: Commit,\n  matchCollection?: ((col: string) => boolean) | null,\n  forceKeyRefresh = false,\n): Promise<CommitEvt[]> => {\n  const did = evt.repo\n  const ops = maybeFilterOps(evt.ops, matchCollection)\n  if (ops.length === 0) {\n    return []\n  }\n  const claims = ops.map((op) => {\n    const { collection, rkey } = parseDataKey(op.path)\n    return {\n      collection,\n      rkey,\n      cid: op.action === 'delete' ? null : op.cid,\n    }\n  })\n  const key = await idResolver.did.resolveAtprotoKey(did, forceKeyRefresh)\n  const verifiedCids: Record<string, CID | null> = {}\n  try {\n    const results = await verifyProofs(evt.blocks, claims, did, key)\n    results.verified.forEach((op) => {\n      const path = formatDataKey(op.collection, op.rkey)\n      verifiedCids[path] = op.cid\n    })\n  } catch (err) {\n    if (err instanceof RepoVerificationError && !forceKeyRefresh) {\n      return parseCommitAuthenticated(idResolver, evt, matchCollection, true)\n    }\n    throw err\n  }\n  const verifiedOps: RepoOp[] = ops.filter((op) => {\n    if (op.action === 'delete') {\n      return verifiedCids[op.path] === null\n    } else {\n      return op.cid !== null && op.cid.equals(verifiedCids[op.path])\n    }\n  })\n  return formatCommitOps(evt, verifiedOps, {\n    skipCidVerification: true, // already checked via verifyProofs()\n  })\n}\n\nexport const parseCommitUnauthenticated = async (\n  evt: Commit,\n  matchCollection?: ((col: string) => boolean) | null,\n): Promise<CommitEvt[]> => {\n  const ops = maybeFilterOps(evt.ops, matchCollection)\n  return formatCommitOps(evt, ops)\n}\n\nconst maybeFilterOps = (\n  ops: RepoOp[],\n  matchCollection?: ((col: string) => boolean) | null,\n): RepoOp[] => {\n  if (!matchCollection) return ops\n  return ops.filter((op) => {\n    const { collection } = parseDataKey(op.path)\n    return matchCollection(collection)\n  })\n}\n\nconst formatCommitOps = async (\n  evt: Commit,\n  ops: RepoOp[],\n  options?: { skipCidVerification: boolean },\n) => {\n  const car = await readCar(evt.blocks, options)\n\n  const evts: CommitEvt[] = []\n\n  for (const op of ops) {\n    const uri = AtUri.make(evt.repo, op.path)\n\n    const meta: CommitMeta = {\n      seq: evt.seq,\n      time: evt.time,\n      commit: evt.commit,\n      blocks: car.blocks,\n      rev: evt.rev,\n      uri,\n      did: uri.host,\n      collection: uri.collection,\n      rkey: uri.rkey,\n    }\n\n    if (op.action === 'create' || op.action === 'update') {\n      if (!op.cid) continue\n      const recordBytes = car.blocks.get(op.cid)\n      if (!recordBytes) continue\n      const record = cborToLexRecord(recordBytes)\n      evts.push({\n        ...meta,\n        event: op.action as 'create' | 'update',\n        cid: op.cid,\n        record,\n      })\n    }\n\n    if (op.action === 'delete') {\n      evts.push({\n        ...meta,\n        event: 'delete',\n      })\n    }\n  }\n\n  return evts\n}\n\nexport const parseSync = async (evt: Sync): Promise<SyncEvt | null> => {\n  const car = await readCarWithRoot(evt.blocks)\n\n  return {\n    event: 'sync',\n    seq: evt.seq,\n    time: evt.time,\n    did: evt.did,\n    cid: car.root,\n    rev: evt.rev,\n    blocks: car.blocks,\n  }\n}\n\nexport const parseIdentity = async (\n  idResolver: IdResolver,\n  evt: Identity,\n  unauthenticated = false,\n): Promise<IdentityEvt | null> => {\n  const res = await idResolver.did.resolve(evt.did)\n  const handle =\n    res && !unauthenticated\n      ? await verifyHandle(idResolver, evt.did, res)\n      : undefined\n\n  return {\n    event: 'identity',\n    seq: evt.seq,\n    time: evt.time,\n    did: evt.did,\n    handle,\n    didDocument: res ?? undefined,\n  }\n}\n\nconst verifyHandle = async (\n  idResolver: IdResolver,\n  did: string,\n  didDoc: DidDocument,\n): Promise<string | undefined> => {\n  const { handle } = parseToAtprotoDocument(didDoc)\n  if (!handle) {\n    return undefined\n  }\n  const res = await idResolver.handle.resolve(handle)\n  return res === did ? handle : undefined\n}\n\nexport const parseAccount = (evt: Account): AccountEvt | undefined => {\n  if (evt.status && !isValidStatus(evt.status)) return\n  return {\n    event: 'account',\n    seq: evt.seq,\n    time: evt.time,\n    did: evt.did,\n    active: evt.active,\n    status: evt.status as AccountStatus | undefined,\n  }\n}\n\nconst isValidStatus = (str: string): str is AccountStatus => {\n  return ['takendown', 'suspended', 'deleted', 'deactivated'].includes(str)\n}\n\nexport class FirehoseValidationError extends Error {\n  constructor(\n    err: unknown,\n    public value: unknown,\n  ) {\n    super('error in firehose event lexicon validation', { cause: err })\n  }\n}\n\nexport class FirehoseParseError extends Error {\n  constructor(\n    err: unknown,\n    public event: RepoEvent,\n  ) {\n    super('error in parsing and authenticating firehose event', { cause: err })\n  }\n}\n\nexport class FirehoseSubscriptionError extends Error {\n  constructor(err: unknown) {\n    super('error on firehose subscription', { cause: err })\n  }\n}\n\nexport class FirehoseHandlerError extends Error {\n  constructor(\n    err: unknown,\n    public event: Event,\n  ) {\n    super('error in firehose event handler', { cause: err })\n  }\n}\n"
  },
  {
    "path": "packages/sync/src/firehose/lexicons.ts",
    "content": "import type { IncomingMessage } from 'node:http'\nimport type { CID } from 'multiformats/cid'\nimport { type LexiconDoc, Lexicons } from '@atproto/lexicon'\nimport type { Auth, ErrorFrame } from '@atproto/xrpc-server'\n\n// @NOTE: this file is an ugly copy job of codegen output. I'd like to clean this whole thing up\n\nexport function isObj(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null\n}\n\nexport function hasProp<K extends PropertyKey>(\n  data: object,\n  prop: K,\n): data is Record<K, unknown> {\n  return prop in data\n}\n\nexport interface QueryParams {\n  /** The last known event seq number to backfill from. */\n  cursor?: number\n}\n\nexport type RepoEvent =\n  | Commit\n  | Identity\n  | Account\n  | Sync\n  | Info\n  | { $type: string; [k: string]: unknown }\nexport type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'>\nexport type HandlerOutput = HandlerError | RepoEvent\nexport type HandlerReqCtx<HA extends Auth = never> = {\n  auth: HA\n  params: QueryParams\n  req: IncomingMessage\n  signal: AbortSignal\n}\nexport type Handler<HA extends Auth = never> = (\n  ctx: HandlerReqCtx<HA>,\n) => AsyncIterable<HandlerOutput>\n\n/** Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature. */\nexport interface Commit {\n  /** The stream sequence number of this message. */\n  seq: number\n  /** DEPRECATED -- unused */\n  rebase: boolean\n  /** Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data. */\n  tooBig: boolean\n  /** The repo this event comes from. */\n  repo: string\n  /** Repo commit object CID. */\n  commit: CID\n  /** DEPRECATED -- unused. WARNING -- nullable and optional; stick with optional to ensure golang interoperability. */\n  prev?: CID | null\n  /** The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event. */\n  rev: string\n  /** The rev of the last emitted commit from this repo (if any). */\n  since: string | null\n  /** CAR file containing relevant blocks, as a diff since the previous repo state. */\n  blocks: Uint8Array\n  ops: RepoOp[]\n  blobs: CID[]\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n  [k: string]: unknown\n}\n\nexport function isCommit(v: unknown): v is Commit {\n  return (\n    isObj(v) &&\n    hasProp(v, '$type') &&\n    v.$type === 'com.atproto.sync.subscribeRepos#commit'\n  )\n}\n\n/** Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository. */\nexport interface Sync {\n  $type?: 'com.atproto.sync.subscribeRepos#sync'\n  /** The stream sequence number of this message. */\n  seq: number\n  /** The account this repo event corresponds to. Must match that in the commit object. */\n  did: string\n  /** CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'. */\n  blocks: Uint8Array\n  /** The rev of the commit. This value must match that in the commit object. */\n  rev: string\n  /** Timestamp of when this message was originally broadcast. */\n  time: string\n}\n\nexport function isSync(v: unknown): v is Sync {\n  return (\n    isObj(v) &&\n    hasProp(v, '$type') &&\n    v.$type === 'com.atproto.sync.subscribeRepos#sync'\n  )\n}\n\n/** Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache. */\nexport interface Identity {\n  seq: number\n  did: string\n  time: string\n  /** The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. */\n  handle?: string\n  [k: string]: unknown\n}\n\nexport function isIdentity(v: unknown): v is Identity {\n  return (\n    isObj(v) &&\n    hasProp(v, '$type') &&\n    v.$type === 'com.atproto.sync.subscribeRepos#identity'\n  )\n}\n\n/** Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active. */\nexport interface Account {\n  seq: number\n  did: string\n  time: string\n  /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */\n  active: boolean\n  /** If active=false, this optional field indicates a reason for why the account is not active. */\n  status?: 'takendown' | 'suspended' | 'deleted' | 'deactivated' | string\n  [k: string]: unknown\n}\n\nexport function isAccount(v: unknown): v is Account {\n  return (\n    isObj(v) &&\n    hasProp(v, '$type') &&\n    v.$type === 'com.atproto.sync.subscribeRepos#account'\n  )\n}\n\nexport interface Info {\n  name: 'OutdatedCursor' | string\n  message?: string\n  [k: string]: unknown\n}\n\nexport function isInfo(v: unknown): v is Info {\n  return (\n    isObj(v) &&\n    hasProp(v, '$type') &&\n    v.$type === 'com.atproto.sync.subscribeRepos#info'\n  )\n}\n\n/** A repo operation, ie a mutation of a single record. */\nexport interface RepoOp {\n  action: 'create' | 'update' | 'delete' | string\n  path: string\n  /** For creates and updates, the new record CID. For deletions, null. */\n  cid: CID | null\n  [k: string]: unknown\n}\n\nexport function isRepoOp(v: unknown): v is RepoOp {\n  return (\n    isObj(v) &&\n    hasProp(v, '$type') &&\n    v.$type === 'com.atproto.sync.subscribeRepos#repoOp'\n  )\n}\n\nexport const ComAtprotoSyncSubscribeRepos: LexiconDoc = {\n  lexicon: 1,\n  id: 'com.atproto.sync.subscribeRepos',\n  defs: {\n    main: {\n      type: 'subscription',\n      description:\n        'Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.',\n      parameters: {\n        type: 'params',\n        properties: {\n          cursor: {\n            type: 'integer',\n            description: 'The last known event seq number to backfill from.',\n          },\n        },\n      },\n      message: {\n        schema: {\n          type: 'union',\n          refs: [\n            'lex:com.atproto.sync.subscribeRepos#commit',\n            'lex:com.atproto.sync.subscribeRepos#sync',\n            'lex:com.atproto.sync.subscribeRepos#identity',\n            'lex:com.atproto.sync.subscribeRepos#account',\n            'lex:com.atproto.sync.subscribeRepos#info',\n          ],\n        },\n      },\n      errors: [\n        {\n          name: 'FutureCursor',\n        },\n        {\n          name: 'ConsumerTooSlow',\n          description:\n            'If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.',\n        },\n      ],\n    },\n    commit: {\n      type: 'object',\n      description:\n        'Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.',\n      required: [\n        'seq',\n        'rebase',\n        'tooBig',\n        'repo',\n        'commit',\n        'rev',\n        'since',\n        'blocks',\n        'ops',\n        'blobs',\n        'time',\n      ],\n      nullable: ['since'],\n      properties: {\n        seq: {\n          type: 'integer',\n          description: 'The stream sequence number of this message.',\n        },\n        rebase: {\n          type: 'boolean',\n          description: 'DEPRECATED -- unused',\n        },\n        tooBig: {\n          type: 'boolean',\n          description:\n            'DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.',\n        },\n        repo: {\n          type: 'string',\n          format: 'did',\n          description:\n            \"The repo this event comes from. Note that all other message types name this field 'did'.\",\n        },\n        commit: {\n          type: 'cid-link',\n          description: 'Repo commit object CID.',\n        },\n        rev: {\n          type: 'string',\n          format: 'tid',\n          description:\n            'The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.',\n        },\n        since: {\n          type: 'string',\n          format: 'tid',\n          description:\n            'The rev of the last emitted commit from this repo (if any).',\n        },\n        blocks: {\n          type: 'bytes',\n          description:\n            \"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.\",\n          maxLength: 2000000,\n        },\n        ops: {\n          type: 'array',\n          items: {\n            type: 'ref',\n            ref: 'lex:com.atproto.sync.subscribeRepos#repoOp',\n            description:\n              'List of repo mutation operations in this commit (eg, records created, updated, or deleted).',\n          },\n          maxLength: 200,\n        },\n        blobs: {\n          type: 'array',\n          items: {\n            type: 'cid-link',\n            description:\n              'DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.',\n          },\n        },\n        prevData: {\n          type: 'cid-link',\n          description:\n            \"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.\",\n        },\n        time: {\n          type: 'string',\n          format: 'datetime',\n          description:\n            'Timestamp of when this message was originally broadcast.',\n        },\n      },\n    },\n    sync: {\n      type: 'object',\n      description:\n        'Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.',\n      required: ['seq', 'did', 'blocks', 'rev', 'time'],\n      properties: {\n        seq: {\n          type: 'integer',\n          description: 'The stream sequence number of this message.',\n        },\n        did: {\n          type: 'string',\n          format: 'did',\n          description:\n            'The account this repo event corresponds to. Must match that in the commit object.',\n        },\n        blocks: {\n          type: 'bytes',\n          description:\n            \"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.\",\n          maxLength: 10000,\n        },\n        rev: {\n          type: 'string',\n          description:\n            'The rev of the commit. This value must match that in the commit object.',\n        },\n        time: {\n          type: 'string',\n          format: 'datetime',\n          description:\n            'Timestamp of when this message was originally broadcast.',\n        },\n      },\n    },\n    identity: {\n      type: 'object',\n      description:\n        \"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.\",\n      required: ['seq', 'did', 'time'],\n      properties: {\n        seq: {\n          type: 'integer',\n        },\n        did: {\n          type: 'string',\n          format: 'did',\n        },\n        time: {\n          type: 'string',\n          format: 'datetime',\n        },\n        handle: {\n          type: 'string',\n          format: 'handle',\n          description:\n            \"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.\",\n        },\n      },\n    },\n    account: {\n      type: 'object',\n      description:\n        \"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.\",\n      required: ['seq', 'did', 'time', 'active'],\n      properties: {\n        seq: {\n          type: 'integer',\n        },\n        did: {\n          type: 'string',\n          format: 'did',\n        },\n        time: {\n          type: 'string',\n          format: 'datetime',\n        },\n        active: {\n          type: 'boolean',\n          description:\n            'Indicates that the account has a repository which can be fetched from the host that emitted this event.',\n        },\n        status: {\n          type: 'string',\n          description:\n            'If active=false, this optional field indicates a reason for why the account is not active.',\n          knownValues: [\n            'takendown',\n            'suspended',\n            'deleted',\n            'deactivated',\n            'desynchronized',\n            'throttled',\n          ],\n        },\n      },\n    },\n    info: {\n      type: 'object',\n      required: ['name'],\n      properties: {\n        name: {\n          type: 'string',\n          knownValues: ['OutdatedCursor'],\n        },\n        message: {\n          type: 'string',\n        },\n      },\n    },\n    repoOp: {\n      type: 'object',\n      description: 'A repo operation, ie a mutation of a single record.',\n      required: ['action', 'path', 'cid'],\n      nullable: ['cid'],\n      properties: {\n        action: {\n          type: 'string',\n          knownValues: ['create', 'update', 'delete'],\n        },\n        path: {\n          type: 'string',\n        },\n        cid: {\n          type: 'cid-link',\n          description:\n            'For creates and updates, the new record CID. For deletions, null.',\n        },\n        prev: {\n          type: 'cid-link',\n          description:\n            'For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.',\n        },\n      },\n    },\n  },\n}\n\nconst lexicons = new Lexicons([ComAtprotoSyncSubscribeRepos])\n\nexport const isValidRepoEvent = (evt: unknown) => {\n  return lexicons.assertValidXrpcMessage<RepoEvent>(\n    'com.atproto.sync.subscribeRepos',\n    evt,\n  )\n}\n"
  },
  {
    "path": "packages/sync/src/index.ts",
    "content": "export * from './runner'\nexport * from './firehose'\nexport * from './events'\n"
  },
  {
    "path": "packages/sync/src/runner/consecutive-list.ts",
    "content": "/**\n * Add items to a list, and mark those items as\n * completed. Upon item completion, get list of consecutive\n * items completed at the head of the list. Example:\n *\n * const consecutive = new ConsecutiveList<number>()\n * const item1 = consecutive.push(1)\n * const item2 = consecutive.push(2)\n * const item3 = consecutive.push(3)\n * item2.complete() // []\n * item1.complete() // [1, 2]\n * item3.complete() // [3]\n *\n */\nexport class ConsecutiveList<T> {\n  list: ConsecutiveItem<T>[] = []\n\n  push(value: T) {\n    const item = new ConsecutiveItem<T>(this, value)\n    this.list.push(item)\n    return item\n  }\n\n  complete(): T[] {\n    let i = 0\n    while (this.list[i]?.isComplete) {\n      i += 1\n    }\n    return this.list.splice(0, i).map((item) => item.value)\n  }\n}\n\nexport class ConsecutiveItem<T> {\n  isComplete = false\n  constructor(\n    private consecutive: ConsecutiveList<T>,\n    public value: T,\n  ) {}\n\n  complete() {\n    this.isComplete = true\n    return this.consecutive.complete()\n  }\n}\n"
  },
  {
    "path": "packages/sync/src/runner/index.ts",
    "content": "export * from './consecutive-list'\nexport * from './memory-runner'\nexport * from './types'\n"
  },
  {
    "path": "packages/sync/src/runner/memory-runner.ts",
    "content": "import PQueue from 'p-queue'\nimport { ConsecutiveList } from './consecutive-list'\nimport { EventRunner } from './types'\n\nexport type MemoryRunnerOptions = {\n  setCursor?: (cursor: number) => Promise<void>\n  concurrency?: number\n  startCursor?: number\n}\n\n// A queue with arbitrarily many partitions, each processing work sequentially.\n// Partitions are created lazily and taken out of memory when they go idle.\nexport class MemoryRunner implements EventRunner {\n  consecutive = new ConsecutiveList<number>()\n  mainQueue: PQueue\n  partitions = new Map<string, PQueue>()\n  cursor: number | undefined\n\n  constructor(public opts: MemoryRunnerOptions = {}) {\n    this.mainQueue = new PQueue({ concurrency: opts.concurrency ?? Infinity })\n    this.cursor = opts.startCursor\n  }\n\n  getCursor() {\n    return this.cursor\n  }\n\n  async addTask(partitionId: string, task: () => Promise<void>) {\n    if (this.mainQueue.isPaused) return\n    return this.mainQueue.add(() => {\n      return this.getPartition(partitionId).add(task)\n    })\n  }\n\n  private getPartition(partitionId: string) {\n    let partition = this.partitions.get(partitionId)\n    if (!partition) {\n      partition = new PQueue({ concurrency: 1 })\n      partition.once('idle', () => this.partitions.delete(partitionId))\n      this.partitions.set(partitionId, partition)\n    }\n    return partition\n  }\n\n  async trackEvent(did: string, seq: number, handler: () => Promise<void>) {\n    if (this.mainQueue.isPaused) return\n    const item = this.consecutive.push(seq)\n    await this.addTask(did, async () => {\n      await handler()\n      const latest = item.complete().at(-1)\n      if (latest !== undefined) {\n        this.cursor = latest\n        if (this.opts.setCursor) {\n          await this.opts.setCursor(this.cursor)\n        }\n      }\n    })\n  }\n\n  async processAll() {\n    await this.mainQueue.onIdle()\n  }\n\n  async destroy() {\n    this.mainQueue.pause()\n    this.mainQueue.clear()\n    this.partitions.forEach((p) => p.clear())\n    await this.mainQueue.onIdle()\n  }\n}\n"
  },
  {
    "path": "packages/sync/src/runner/types.ts",
    "content": "export interface EventRunner {\n  getCursor(): Awaited<number | undefined>\n  trackEvent(\n    did: string,\n    seq: number,\n    handler: () => Promise<void>,\n  ): Promise<void>\n}\n"
  },
  {
    "path": "packages/sync/src/util.ts",
    "content": "import {\n  RepoEvent,\n  isAccount,\n  isCommit,\n  isIdentity,\n  isSync,\n} from './firehose/lexicons'\n\nexport const didAndSeqForEvt = (\n  evt: RepoEvent,\n): { did: string; seq: number } | undefined => {\n  if (isCommit(evt)) return { seq: evt.seq, did: evt.repo }\n  else if (isAccount(evt) || isIdentity(evt) || isSync(evt))\n    return { seq: evt.seq, did: evt.did }\n  return undefined\n}\n"
  },
  {
    "path": "packages/sync/tests/firehose.test.ts",
    "content": "import { createDeferrable, wait } from '@atproto/common'\nimport {\n  SeedClient,\n  TestNetworkNoAppView,\n  mockResolvers,\n} from '@atproto/dev-env'\nimport { IdResolver } from '@atproto/identity'\nimport { Firehose, FirehoseOptions, MemoryRunner } from '../src'\nimport { Create, Event } from '../src/events'\n\ndescribe('firehose', () => {\n  let network: TestNetworkNoAppView\n  let sc: SeedClient\n  let idResolver: IdResolver\n\n  beforeAll(async () => {\n    network = await TestNetworkNoAppView.create({\n      dbPostgresSchema: 'sync_firehose',\n    })\n    idResolver = new IdResolver({ plcUrl: network.plc.url })\n    mockResolvers(idResolver, network.pds)\n    sc = network.getSeedClient()\n  })\n\n  afterAll(async () => {\n    await network.close()\n  })\n\n  const createAndReadFirehose = async (\n    count: number,\n    opts: Partial<FirehoseOptions> = {},\n    addRandomWait = false,\n  ): Promise<Event[]> => {\n    const defer = createDeferrable()\n    const evts: Event[] = []\n    const firehose = new Firehose({\n      idResolver,\n      service: network.pds.url.replace('http', 'ws'),\n      handleEvent: async (evt) => {\n        if (addRandomWait) {\n          const time = Math.floor(Math.random()) * 20\n          await wait(time)\n        }\n        evts.push(evt)\n        if (evts.length >= count) {\n          defer.resolve()\n        }\n      },\n      onError: (err) => {\n        throw err\n      },\n      ...opts,\n    })\n    firehose.start()\n    await defer.complete\n    await firehose.destroy()\n    return evts\n  }\n\n  let alice: string\n\n  it('reads events from firehose', async () => {\n    const evtsPromise = createAndReadFirehose(6)\n    await wait(10) // give the websocket just a second to spin up\n    const aliceRes = await sc.createAccount('alice', {\n      handle: 'alice.test',\n      email: 'alice@test.com',\n      password: 'alice-pass',\n    })\n    alice = aliceRes.did\n    await sc.post(alice, 'one')\n    await sc.post(alice, 'two')\n    await sc.post(alice, 'three')\n\n    const evts = await evtsPromise\n    expect(evts.length).toBe(6)\n    expect(evts.at(0)).toMatchObject({\n      event: 'identity',\n      did: alice,\n      handle: aliceRes.handle,\n      didDocument: {\n        id: alice,\n      },\n    })\n    expect(evts.at(1)).toMatchObject({\n      event: 'account',\n      did: alice,\n      active: true,\n      status: undefined,\n    })\n    expect(evts.at(2)).toMatchObject({\n      event: 'sync',\n      did: alice,\n    })\n    expect(evts.at(3)).toMatchObject({\n      event: 'create',\n      did: alice,\n      collection: 'app.bsky.feed.post',\n      record: {\n        text: 'one',\n      },\n    })\n    expect(evts.at(4)).toMatchObject({\n      event: 'create',\n      did: alice,\n      collection: 'app.bsky.feed.post',\n      record: {\n        text: 'two',\n      },\n    })\n    expect(evts.at(5)).toMatchObject({\n      event: 'create',\n      did: alice,\n      collection: 'app.bsky.feed.post',\n      record: {\n        text: 'three',\n      },\n    })\n  })\n\n  it('does not naively pass through invalid handle evts', async () => {\n    const evtsPromise = createAndReadFirehose(1)\n    await wait(10) // give the websocket just a second to spin up\n    await network.pds.ctx.sequencer.sequenceIdentityEvt(\n      alice,\n      'bad-handle.test',\n    )\n    const evts = await evtsPromise\n    expect(evts.at(0)).toMatchObject({ handle: 'alice.test' })\n  })\n\n  it('processes events through the sync queue', async () => {\n    const currCursor = await network.pds.ctx.sequencer.curr()\n    const runner = new MemoryRunner({\n      startCursor: currCursor ?? undefined,\n    })\n    const evtsPromise = createAndReadFirehose(24, { runner }, true)\n    const createAndPost = async (name: string) => {\n      const user = await sc.createAccount('name', {\n        handle: `${name}.test`,\n        email: `${name}@example.com`,\n        password: `${name}-pass`,\n      })\n      const did = user.did\n      const post1 = await sc.post(did, 'one')\n      const post2 = await sc.post(did, 'two')\n      const post3 = await sc.post(did, 'three')\n      return {\n        did,\n        post1: post1.ref.uriStr,\n        post2: post2.ref.uriStr,\n        post3: post3.ref.uriStr,\n      }\n    }\n    const res = await Promise.all([\n      createAndPost('user1'),\n      createAndPost('user2'),\n      createAndPost('user3'),\n      createAndPost('user4'),\n    ])\n    const evts = await evtsPromise\n    const user1Evts = evts.filter((e) => e.did === res[0].did)\n    const user2Evts = evts.filter((e) => e.did === res[1].did)\n    const user3Evts = evts.filter((e) => e.did === res[2].did)\n    const user4Evts = evts.filter((e) => e.did === res[3].did)\n    const EVT_ORDER = [\n      'identity',\n      'account',\n      'sync',\n      'create',\n      'create',\n      'create',\n    ]\n    expect(user1Evts.map((e) => e.event)).toEqual(EVT_ORDER)\n    expect(user2Evts.map((e) => e.event)).toEqual(EVT_ORDER)\n    expect(user3Evts.map((e) => e.event)).toEqual(EVT_ORDER)\n    expect(user4Evts.map((e) => e.event)).toEqual(EVT_ORDER)\n    expect(\n      user1Evts.slice(3, 6).map((e) => (e as Create).uri.toString()),\n    ).toEqual([res[0].post1, res[0].post2, res[0].post3])\n    expect(\n      user2Evts.slice(3, 6).map((e) => (e as Create).uri.toString()),\n    ).toEqual([res[1].post1, res[1].post2, res[1].post3])\n    expect(\n      user3Evts.slice(3, 6).map((e) => (e as Create).uri.toString()),\n    ).toEqual([res[2].post1, res[2].post2, res[2].post3])\n    expect(\n      user4Evts.slice(3, 6).map((e) => (e as Create).uri.toString()),\n    ).toEqual([res[3].post1, res[3].post2, res[3].post3])\n  })\n})\n"
  },
  {
    "path": "packages/sync/tests/runner.test.ts",
    "content": "import { wait } from '@atproto/common'\nimport { ConsecutiveList, MemoryRunner } from '../src/runner'\n\ndescribe('EventRunner utils', () => {\n  describe('ConsecutiveList', () => {\n    it('tracks consecutive complete items.', () => {\n      const consecutive = new ConsecutiveList<number>()\n      // add items\n      const item1 = consecutive.push(1)\n      const item2 = consecutive.push(2)\n      const item3 = consecutive.push(3)\n      expect(item1.isComplete).toEqual(false)\n      expect(item2.isComplete).toEqual(false)\n      expect(item3.isComplete).toEqual(false)\n      // complete items out of order\n      expect(consecutive.list.length).toBe(3)\n      expect(item2.complete()).toEqual([])\n      expect(item2.isComplete).toEqual(true)\n      expect(consecutive.list.length).toBe(3)\n      expect(item1.complete()).toEqual([1, 2])\n      expect(item1.isComplete).toEqual(true)\n      expect(consecutive.list.length).toBe(1)\n      expect(item3.complete()).toEqual([3])\n      expect(consecutive.list.length).toBe(0)\n      expect(item3.isComplete).toEqual(true)\n    })\n  })\n\n  describe('MemoryRunner', () => {\n    it('performs work in parallel across partitions, serial within a partition.', async () => {\n      const runner = new MemoryRunner({ concurrency: Infinity })\n      const complete: number[] = []\n      // partition 1 items start slow but get faster: slow should still complete first.\n      runner.addTask('1', async () => {\n        await wait(30)\n        complete.push(11)\n      })\n      runner.addTask('1', async () => {\n        await wait(20)\n        complete.push(12)\n      })\n      runner.addTask('1', async () => {\n        await wait(1)\n        complete.push(13)\n      })\n      expect(runner.partitions.size).toEqual(1)\n      // partition 2 items complete quickly except the last, which is slowest of all events.\n      runner.addTask('2', async () => {\n        await wait(1)\n        complete.push(21)\n      })\n      runner.addTask('2', async () => {\n        await wait(1)\n        complete.push(22)\n      })\n      runner.addTask('2', async () => {\n        await wait(1)\n        complete.push(23)\n      })\n      runner.addTask('2', async () => {\n        await wait(60)\n        complete.push(24)\n      })\n      expect(runner.partitions.size).toEqual(2)\n      await runner.mainQueue.onIdle()\n      expect(complete).toEqual([21, 22, 23, 11, 12, 13, 24])\n      expect(runner.partitions.size).toEqual(0)\n    })\n\n    it('limits overall concurrency.', async () => {\n      const runner = new MemoryRunner({ concurrency: 1 })\n      const complete: number[] = []\n      // if concurrency were not constrained, partition 1 would complete all items\n      // before any items from partition 2. since it is constrained, the work is complete in the order added.\n      runner.addTask('1', async () => {\n        await wait(1)\n        complete.push(11)\n      })\n      runner.addTask('2', async () => {\n        await wait(10)\n        complete.push(21)\n      })\n      runner.addTask('1', async () => {\n        await wait(1)\n        complete.push(12)\n      })\n      runner.addTask('2', async () => {\n        await wait(10)\n        complete.push(22)\n      })\n      // only partition 1 exists so far due to the concurrency\n      expect(runner.partitions.size).toEqual(1)\n      await runner.mainQueue.onIdle()\n      expect(complete).toEqual([11, 21, 12, 22])\n      expect(runner.partitions.size).toEqual(0)\n    })\n\n    it('settles with many items.', async () => {\n      const runner = new MemoryRunner({ concurrency: 100 })\n      const complete: { partition: string; id: number }[] = []\n      const partitions = new Set<string>()\n      for (let i = 0; i < 500; ++i) {\n        const partition = Math.floor(Math.random() * 16).toString(10)\n        partitions.add(partition)\n        runner.addTask(partition, async () => {\n          await wait((i % 2) * 2)\n          complete.push({ partition, id: i })\n        })\n      }\n      expect(runner.partitions.size).toBeLessThanOrEqual(partitions.size)\n      await runner.mainQueue.onIdle()\n      expect(complete.length).toEqual(500)\n      for (const partition of partitions) {\n        const ids = complete\n          .filter((item) => item.partition === partition)\n          .map((item) => item.id)\n        expect(ids).toEqual([...ids].sort((a, b) => a - b))\n      }\n      expect(runner.partitions.size).toEqual(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/sync/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/sync/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/syntax/CHANGELOG.md",
    "content": "# @atproto/syntax\n\n## 0.5.1\n\n### Patch Changes\n\n- [`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve efficiency of `AtUri` `did` getter and typing of `hostname` property\n\n## 0.5.0\n\n### Minor Changes\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove global `Date.toISOString()` overload and replace with more accurate, less permissive, `AtprotoDate` interface. This change prevent using any `Date` instance to generate a `DatetimeString`, since some JS dates can actually not be safely stringified to a `DatetimeString`.\n\n### Patch Changes\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add type safe accessors to read and write `AtUri`'s `did`, `collection` and `rkey` properties\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Test `ensureValidDatetime`, `isValidDatetime` and `normalizeDatetime` individually, for expected failures.\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow calling `DatetimeString` validation utilities with `unknown` values (instead of only `string`)\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Preserve input type when normalizing a valid `HandleString`\n\n- [#4688](https://github.com/bluesky-social/atproto/pull/4688) [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor optimization in regex based NSID string validation\n\n- [#4689](https://github.com/bluesky-social/atproto/pull/4689) [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve type strictness of `NSID`'s `authority` and `toString()` properties\n\n## 0.4.3\n\n### Patch Changes\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add new `isValidAtUri` utility\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `isValidUri` utility\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing of type assertion utilities\n\n- [#4571](https://github.com/bluesky-social/atproto/pull/4571) [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add BCP47 language validation and parsing utilities\n\n## 0.4.2\n\n### Patch Changes\n\n- [#4389](https://github.com/bluesky-social/atproto/pull/4389) [`bcae2b7`](https://github.com/bluesky-social/atproto/commit/bcae2b77b68da6dc2ec202651c8bf41fd5769f69) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve typing of various string formats\n\n## 0.4.1\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `NSID.from(stringifiable)` utility\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performance of NSID validation\n\n## 0.4.0\n\n### Minor Changes\n\n- [#3635](https://github.com/bluesky-social/atproto/pull/3635) [`670b6b5de`](https://github.com/bluesky-social/atproto/commit/670b6b5de2bf91e6944761c98eb1126fb6a681ee) Thanks [@bnewbold](https://github.com/bnewbold)! - update NSID syntax to allow non-leading digits\n\n## 0.3.4\n\n### Patch Changes\n\n- [#2945](https://github.com/bluesky-social/atproto/pull/2945) [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate unused classes\n\n## 0.3.3\n\n### Patch Changes\n\n- [#2999](https://github.com/bluesky-social/atproto/pull/2999) [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performance of isValidTid\n\n## 0.3.2\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n## 0.3.1\n\n### Patch Changes\n\n- [#2911](https://github.com/bluesky-social/atproto/pull/2911) [`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve performances of did validation\n\n## 0.3.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies [[`4eaadc0ac`](https://github.com/bluesky-social/atproto/commit/4eaadc0acb6b73b9745dd7a2b929d02e58083ab0)]:\n  - @atproto/common-web@0.2.4\n\n## 0.2.0\n\n### Minor Changes\n\n- [#2223](https://github.com/bluesky-social/atproto/pull/2223) [`0c815b964`](https://github.com/bluesky-social/atproto/commit/0c815b964c030aa0f277c40bf9786f130dc320f4) Thanks [@bnewbold](https://github.com/bnewbold)! - allow colon character in record-key syntax\n\n## 0.1.5\n\n### Patch Changes\n\n- [#1908](https://github.com/bluesky-social/atproto/pull/1908) [`3c0ef382`](https://github.com/bluesky-social/atproto/commit/3c0ef382c12a413cc971ae47ffb341236c545f60) Thanks [@gaearon](https://github.com/gaearon)! - prevent unnecessary throw/catch on uri syntax\n\n## 0.1.4\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/common-web@0.2.3\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [[`35d108ce`](https://github.com/bluesky-social/atproto/commit/35d108ce94866ce1b3d147cd0620a0ba1c4ebcd7)]:\n  - @atproto/common-web@0.2.2\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [[`41ee177f`](https://github.com/bluesky-social/atproto/commit/41ee177f5a440490280d17acd8a89bcddaffb23b)]:\n  - @atproto/common-web@0.2.1\n\n## 0.1.1\n\n### Patch Changes\n\n- [#1611](https://github.com/bluesky-social/atproto/pull/1611) [`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix imports in `README.md`.\n"
  },
  {
    "path": "packages/syntax/README.md",
    "content": "# @atproto/syntax: validation helpers for identifier strings\n\nValidation logic for [atproto](https://atproto.com) identifiers - DIDs, Handles, NSIDs, and AT URIs.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/crypto)](https://www.npmjs.com/package/@atproto/syntax)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Usage\n\n### Handles\n\nSyntax specification: <https://atproto.com/specs/handle>\n\n```typescript\nimport { isValidHandle, ensureValidHandle, isValidDid } from '@atproto/syntax'\n\nisValidHandle('alice.test') // returns true\nensureValidHandle('alice.test') // returns void\n\nisValidHandle('al!ce.test') // returns false\nensureValidHandle('al!ce.test') // throws\n\nensureValidDid('did:method:val') // returns void\nensureValidDid(':did:method:val') // throws\n```\n\n### NameSpaced IDs (NSID)\n\nSyntax specification: <https://atproto.com/specs/nsid>\n\n```typescript\nimport { NSID } from '@atproto/syntax'\n\nconst id1 = NSID.parse('com.example.foo')\nid1.authority // => 'example.com'\nid1.name // => 'foo'\nid1.toString() // => 'com.example.foo'\n\nconst id2 = NSID.create('example.com', 'foo')\nid2.authority // => 'example.com'\nid2.name // => 'foo'\nid2.toString() // => 'com.example.foo'\n\nconst id3 = NSID.create('example.com', 'someRecord')\nid3.authority // => 'example.com'\nid3.name // => 'someRecord'\nid3.toString() // => 'com.example.someRecord'\n\nNSID.isValid('com.example.foo') // => true\nNSID.isValid('com.example.someRecord') // => true\nNSID.isValid('example.com/foo') // => false\nNSID.isValid('foo') // => false\n```\n\n### AT URI\n\nSyntax specification: <https://atproto.com/specs/at-uri-scheme>\n\n```typescript\nimport { AtUri } from '@atproto/syntax'\n\nconst uri = new AtUri('at://bob.com/com.example.post/1234')\nuri.protocol // => 'at:'\nuri.origin // => 'at://bob.com'\nuri.hostname // => 'bob.com'\nuri.collection // => 'com.example.post'\nuri.rkey // => '1234'\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/syntax/benchmark.js",
    "content": "/* eslint-env node, commonjs */\n\nconst { validateNsid, validateNsidRegex } = require('.')\n\n// $ node benchmark.js\n// valid NSIDs {\n//   parsed: 181.56524884700775,\n//   regexp: 77.61082607507706,\n//   optimized: 60.183539509773254\n// }\n// invalid NSIDs {\n//   parsed: 128.7685609459877,\n//   regexp: 108.75775015354156,\n//   optimized: 53.196488440036774\n// }\n\nbench('valid NSIDs', true, [\n  'com.example.foo',\n  'o'.repeat(63) + '.foo.bar',\n  'com.' + 'o'.repeat(63) + '.foo',\n  'com.example.' + 'o'.repeat(63),\n  'com.' + 'middle.'.repeat(40) + 'foo',\n  'com.example.fooBar',\n  'net.users.bob.ping',\n  'a.b.c',\n  'm.xn--masekowski-d0b.pl',\n  'one.two.three',\n  'one.two.three.four-and.FiVe',\n  'one.2.three',\n  'a-0.b-1.c',\n  'a0.b1.cc',\n  'cn.8.lex.stuff',\n  'test.12345.record',\n  'a01.thing.record',\n  'a.0.c',\n  'xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two',\n  'a0.b1.c3',\n  'com.example.f00',\n  'onion.expyuzz4wqqyqhjn.spec.getThing',\n  'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',\n  'org.4chan.lex.getThing',\n  'cn.8.lex.stuff',\n  'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',\n  'a.'.repeat(158) + 'a',\n])\n\nbench('invalid NSIDs', false, [\n  'a.'.repeat(158) + '9',\n  'a.'.repeat(154) + 'a😅.9',\n  'o'.repeat(64) + '.foo.bar',\n  'com.' + 'o'.repeat(64) + '.foo',\n  'com.example.' + 'o'.repeat(64),\n  'com.' + 'middle.'.repeat(50) + 'foo',\n  'com.example.foo.*',\n  'com.example.foo.blah*',\n  'com.example.foo.*blah',\n  'com.exa💩ple.thing',\n  'a-0.b-1.c-3',\n  'a-0.b-1.c-o',\n  '1.0.0.127.record',\n  '0two.example.foo',\n  'example.com',\n  'com.example',\n  'a.',\n  '.one.two.three',\n  'one.two.three ',\n  'one.two..three',\n  'one .two.three',\n  ' one.two.three',\n  'com.atproto.feed.p@st',\n  'com.atproto.feed.p_st',\n  'com.atproto.feed.p*st',\n  'com.atproto.feed.po#t',\n  'com.atproto.feed.p!ot',\n  'com.example-.foo',\n  'com.-example.foo',\n  'com.example.0foo',\n  'com.example.f-o',\n])\n\nfunction bench(name, expectedResult, cases) {\n  const validators = {\n    parsed: (nsid) => validateNsid(nsid).success,\n    regexp: (nsid) => validateNsidRegex(nsid).success,\n    optimized: (nsid) => validateNsidOptimized(nsid).success,\n  }\n\n  const times = Object.fromEntries(Object.keys(validators).map((k) => [k, 0]))\n\n  for (let i = 0; i < 1000; i++) {\n    for (const [name, fn] of Object.entries(validators)) {\n      const start = performance.now()\n      for (let j = 0; j < 20; j++) {\n        for (const value of cases) {\n          if (fn(value) !== expectedResult) {\n            throw new Error(`Validator ${name} gave wrong result`)\n          }\n        }\n      }\n      times[name] += performance.now() - start\n    }\n  }\n\n  console.log(\n    name,\n    Object.fromEntries(\n      Object.entries(times).map(([k, v]) => [k, `${v.toFixed(2)} ms`]),\n    ),\n  )\n}\n\n/** @param value {string} */\nfunction validateNsidOptimized(value) {\n  const { length } = value\n  if (length > 253 + 1 + 63) {\n    return { success: false, message: 'NSID is too long (317 chars max)' }\n  }\n\n  let partCount = 1\n  let partStart = 0\n  let partHasLeadingDigit = false\n  let partHasHyphen = false\n\n  let charCode\n  for (let i = 0; i < length; i++) {\n    charCode = value.charCodeAt(i)\n\n    // Hot path: check frequent chars first\n    if (\n      (charCode >= 97 && charCode <= 122) /* a-z */ ||\n      (charCode >= 65 && charCode <= 90) /* A-Z */\n    ) {\n      // All good\n    } else if (charCode >= 48 && charCode <= 57 /* 0-9 */) {\n      if (i === 0) {\n        return {\n          success: false,\n          message: 'NSID first part may not start with a digit',\n        }\n      }\n\n      // All good\n\n      if (i === partStart) {\n        partHasLeadingDigit = true\n      }\n    } else if (charCode === 45 /* - */) {\n      if (i === partStart) {\n        return {\n          success: false,\n          message: 'NSID part can not start with hyphen',\n        }\n      }\n      if (i === length - 1 || value.charCodeAt(i + 1) === 46 /* . */) {\n        return { success: false, message: 'NSID part can not end with hyphen' }\n      }\n\n      // All good\n\n      partHasHyphen = true\n    } else if (charCode === 46 /* . */) {\n      // Check prev part size\n      if (i === partStart) {\n        return { success: false, message: 'NSID parts can not be empty' }\n      }\n      if (i - partStart > 63) {\n        return { success: false, message: 'NSID part too long (max 63 chars)' }\n      }\n\n      // All good\n\n      partCount++\n      partStart = i + 1\n      partHasHyphen = false\n      partHasLeadingDigit = false\n    } else {\n      return {\n        success: false,\n        message:\n          'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',\n      }\n    }\n  }\n\n  // Check last part size\n  if (length === partStart) {\n    return { success: false, message: 'NSID parts can not be empty' }\n  }\n  if (length - partStart > 63) {\n    return { success: false, message: 'NSID part too long (max 63 chars)' }\n  }\n\n  // Check last part chars\n  if (partHasHyphen || partHasLeadingDigit) {\n    return {\n      success: false,\n      message:\n        'NSID name part must be only letters and digits (and no leading digit)',\n    }\n  }\n\n  // Check part count\n  if (partCount < 3) {\n    return { success: false, message: 'NSID needs at least three parts' }\n  }\n\n  return { success: true, value }\n}\n"
  },
  {
    "path": "packages/syntax/package.json",
    "content": "{\n  \"name\": \"@atproto/syntax\",\n  \"version\": \"0.5.1\",\n  \"license\": \"MIT\",\n  \"description\": \"Validation for atproto identifiers and formats: DID, handle, NSID, AT URI, etc\",\n  \"keywords\": [\n    \"atproto\",\n    \"did\",\n    \"nsid\",\n    \"at-uri\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/syntax\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"vitest run\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"tslib\": \"^2.8.1\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\",\n    \"vitest\": \"^4.0.16\"\n  },\n  \"browser\": {\n    \"dns/promises\": false\n  }\n}\n"
  },
  {
    "path": "packages/syntax/src/at-identifier.ts",
    "content": "import { DidString, ensureValidDidRegex, isValidDid } from './did.js'\nimport {\n  HandleString,\n  InvalidHandleError,\n  ensureValidHandleRegex,\n  isValidHandle,\n} from './handle.js'\n\n/**\n * An \"at-identifier\" string - either a {@link DidString} or a {@link HandleString}\n *\n * @example `\"did:plc:1234...\"`, `\"did:web:example.com\"` or `\"alice.bsky.social\"`\n */\nexport type AtIdentifierString = DidString | HandleString\n\n/**\n * Discriminates {@link HandleString} from a valid {@link AtIdentifierString}.\n *\n * @return `true` if the identifier is a handle, `false` otherwise\n */\nexport function isHandleIdentifier(id: AtIdentifierString): id is HandleString {\n  return !isDidIdentifier(id)\n}\n\n/**\n * Discriminates {@link DidString} from a valid {@link AtIdentifierString}.\n *\n * @return `true` if the identifier is a DID, `false` otherwise\n */\nexport function isDidIdentifier(id: AtIdentifierString): id is DidString {\n  return id.startsWith('did:')\n}\n\n/**\n * Validates that a string is a valid {@link AtIdentifierString} format string,\n * throwing an error if it is not.\n *\n * @throws InvalidHandleError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link AtIdentifierString}\n */\nexport function assertAtIdentifierString<I>(\n  input: I,\n): asserts input is I & AtIdentifierString {\n  try {\n    if (!input || typeof input !== 'string') {\n      throw new TypeError('Identifier must be a non-empty string')\n    } else if (input.startsWith('did:')) {\n      ensureValidDidRegex(input)\n    } else {\n      ensureValidHandleRegex(input)\n    }\n  } catch (cause) {\n    throw new InvalidHandleError('Invalid DID or handle', { cause })\n  }\n}\n\n/**\n * Casts a string to a {@link AtIdentifierString} if it is a valid at-identifier\n * string, throwing an error if it is not.\n *\n * @throws InvalidHandleError if the input string does not meet the atproto 'at-identifier' format requirements.\n * @see {@link AtIdentifierString}\n */\nexport function asAtIdentifierString<I>(input: I): I & AtIdentifierString {\n  assertAtIdentifierString(input)\n  return input\n}\n\n/**\n * Type guard that checks if a value is a valid AT identifier (DID or handle).\n *\n * @param value - The value to check\n * @returns `true` if the value is a valid AT identifier\n * @see {@link AtIdentifierString}\n */\nexport function isAtIdentifierString<I>(\n  input: I,\n): input is I & AtIdentifierString {\n  if (!input || typeof input !== 'string') {\n    return false\n  } else if (input.startsWith('did:')) {\n    return isValidDid(input)\n  } else {\n    return isValidHandle(input)\n  }\n}\n\n/**\n * Returns the input if it is a valid {@link AtIdentifierString} format string, or\n * `undefined` if it is not.\n *\n * @see {@link AtIdentifierString}\n */\nexport function ifAtIdentifierString<I>(\n  input: I,\n): undefined | (I & AtIdentifierString) {\n  return isAtIdentifierString(input) ? input : undefined\n}\n\n// Legacy exports (should we deprecate these ?)\nexport {\n  assertAtIdentifierString as ensureValidAtIdentifier,\n  isAtIdentifierString as isValidAtIdentifier,\n}\n"
  },
  {
    "path": "packages/syntax/src/aturi.ts",
    "content": "import {\n  AtIdentifierString,\n  ensureValidAtIdentifier,\n  isDidIdentifier,\n} from './at-identifier.js'\nimport { AtUriString } from './aturi_validation.js'\nimport { DidString, InvalidDidError } from './did.js'\nimport { NsidString, ensureValidNsid } from './nsid.js'\nimport { RecordKeyString, ensureValidRecordKey } from './recordkey.js'\n\nexport * from './aturi_validation.js'\n\n// Re-export types used in public interface\nexport type {\n  AtIdentifierString,\n  AtUriString,\n  DidString,\n  NsidString,\n  RecordKeyString,\n}\n\nexport const ATP_URI_REGEX =\n  // proto-    --did--------------   --name----------------   --path----   --query--   --hash--\n  /^(at:\\/\\/)?((?:did:[a-z0-9:%-]+)|(?:[a-z0-9][a-z0-9.:-]*))(\\/[^?#\\s]*)?(\\?[^#\\s]+)?(#[^\\s]+)?$/i\n//                       --path-----   --query--  --hash--\nconst RELATIVE_REGEX = /^(\\/[^?#\\s]*)?(\\?[^#\\s]+)?(#[^\\s]+)?$/i\n\nexport class AtUri {\n  hash: string\n  host: AtIdentifierString\n  pathname: string\n  searchParams: URLSearchParams\n\n  constructor(uri: string, base?: string | AtUri) {\n    const parsed =\n      base !== undefined\n        ? typeof base === 'string'\n          ? Object.assign(parse(base), parseRelative(uri))\n          : Object.assign({ host: base.host }, parseRelative(uri))\n        : parse(uri)\n\n    ensureValidAtIdentifier(parsed.host)\n\n    this.hash = parsed.hash ?? ''\n    this.host = parsed.host\n    this.pathname = parsed.pathname ?? ''\n    this.searchParams = parsed.searchParams\n  }\n\n  static make(handleOrDid: string, collection?: string, rkey?: string) {\n    let str = handleOrDid\n    if (collection) str += '/' + collection\n    if (rkey) str += '/' + rkey\n    return new AtUri(str)\n  }\n\n  get protocol() {\n    return 'at:'\n  }\n\n  get origin() {\n    return `at://${this.host}` as const\n  }\n\n  get did(): DidString {\n    const { host } = this\n    if (isDidIdentifier(host)) return host\n    throw new InvalidDidError(`AtUri \"${this}\" does not have a DID hostname`)\n  }\n\n  get hostname(): AtIdentifierString {\n    return this.host\n  }\n\n  set hostname(v: string) {\n    ensureValidAtIdentifier(v)\n    this.host = v\n  }\n\n  get search() {\n    return this.searchParams.toString()\n  }\n\n  set search(v: string) {\n    this.searchParams = new URLSearchParams(v)\n  }\n\n  get collection() {\n    return this.pathname.split('/').filter(Boolean)[0] || ''\n  }\n\n  get collectionSafe(): NsidString {\n    const { collection } = this\n    ensureValidNsid(collection)\n    return collection\n  }\n\n  set collection(v: string) {\n    ensureValidNsid(v)\n    this.unsafelySetCollection(v)\n  }\n\n  unsafelySetCollection(v: string) {\n    const parts = this.pathname.split('/').filter(Boolean)\n    parts[0] = v\n    this.pathname = parts.join('/')\n  }\n\n  get rkey() {\n    return this.pathname.split('/').filter(Boolean)[1] || ''\n  }\n\n  get rkeySafe(): RecordKeyString {\n    const { rkey } = this\n    ensureValidRecordKey(rkey)\n    return rkey\n  }\n\n  set rkey(v: string) {\n    ensureValidRecordKey(v)\n    this.unsafelySetRkey(v)\n  }\n\n  unsafelySetRkey(v: string) {\n    const parts = this.pathname.split('/').filter(Boolean)\n    parts[0] ||= 'undefined'\n    parts[1] = v\n    this.pathname = parts.join('/')\n  }\n\n  get href() {\n    return this.toString()\n  }\n\n  toString(): AtUriString {\n    let path = this.pathname || '/'\n    if (!path.startsWith('/')) {\n      path = `/${path}`\n    }\n    let qs = ''\n    if (this.searchParams.size) {\n      qs = `?${this.searchParams.toString()}`\n    }\n    let hash = this.hash\n    if (hash && !hash.startsWith('#')) {\n      hash = `#${hash}`\n    }\n    return `at://${this.host}${path}${qs}${hash}` as AtUriString\n  }\n}\n\nfunction parse(str: string) {\n  const match = str.match(ATP_URI_REGEX) as null | {\n    0: string\n    1: string | undefined // proto\n    2: string // host\n    3: string | undefined // path\n    4: string | undefined // query\n    5: string | undefined // hash\n  }\n\n  if (!match) {\n    throw new Error(`Invalid AT uri: ${str}`)\n  }\n\n  return {\n    host: match[2],\n    hash: match[5],\n    pathname: match[3],\n    searchParams: new URLSearchParams(match[4]),\n  }\n}\n\nfunction parseRelative(str: string) {\n  const match = str.match(RELATIVE_REGEX) as null | {\n    0: string\n    1: string | undefined // path\n    2: string | undefined // query\n    3: string | undefined // hash\n  }\n\n  if (!match) {\n    throw new Error(`Invalid path: ${str}`)\n  }\n\n  return {\n    hash: match[3],\n    pathname: match[1],\n    searchParams: new URLSearchParams(match[2]),\n  }\n}\n"
  },
  {
    "path": "packages/syntax/src/aturi_validation.ts",
    "content": "import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'\nimport { ensureValidDidRegex } from './did.js'\nimport { ensureValidHandleRegex } from './handle.js'\nimport { NsidString, isValidNsid } from './nsid.js'\n\nexport type AtUriString =\n  | `at://${AtIdentifierString}`\n  | `at://${AtIdentifierString}/${NsidString}`\n  | `at://${AtIdentifierString}/${NsidString}/${string}`\n\n// Human-readable constraints on ATURI:\n//   - following regular URLs, a 8KByte hard total length limit\n//   - follows ATURI docs on website\n//      - all ASCII characters, no whitespace. non-ASCII could be URL-encoded\n//      - starts \"at://\"\n//      - \"authority\" is a valid DID or a valid handle\n//      - optionally, follow \"authority\" with \"/\" and valid NSID as start of path\n//      - optionally, if NSID given, follow that with \"/\" and rkey\n//      - rkey path component can include URL-encoded (\"percent encoded\"), or:\n//          ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\" / \":\" / \"@\" / \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n//          [a-zA-Z0-9._~:@!$&'\\(\\)*+,;=-]\n//      - rkey must have at least one char\n//      - regardless of path component, a fragment can follow  as \"#\" and then a JSON pointer (RFC-6901)\n\nexport function ensureValidAtUri<I extends string>(\n  input: I,\n): asserts input is I & AtUriString {\n  const fragmentIndex = input.indexOf('#')\n  if (fragmentIndex !== -1) {\n    if (input.charCodeAt(fragmentIndex + 1) !== 47) {\n      throw new Error('ATURI fragment must be non-empty and start with slash')\n    }\n    if (input.includes('#', fragmentIndex + 1)) {\n      throw new Error('ATURI can have at most one \"#\", separating fragment out')\n    }\n\n    // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace\n    const fragment = input.slice(fragmentIndex + 1)\n    if (!/^\\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\\]/-]*$/.test(fragment)) {\n      throw new Error('Disallowed characters in ATURI fragment (ASCII)')\n    }\n  }\n\n  const uri = fragmentIndex === -1 ? input : input.slice(0, fragmentIndex)\n\n  if (uri.length > 8 * 1024) {\n    throw new Error('ATURI is far too long')\n  }\n\n  if (!uri.startsWith('at://')) {\n    throw new Error('ATURI must start with \"at://\"')\n  }\n\n  // check that all chars are boring ASCII\n  if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {\n    throw new Error('Disallowed characters in ATURI (ASCII)')\n  }\n\n  const authorityEnd = uri.indexOf('/', 5)\n  const authority =\n    authorityEnd === -1 ? uri.slice(5) : uri.slice(5, authorityEnd)\n  try {\n    ensureValidAtIdentifier(authority)\n  } catch (cause) {\n    throw new Error('ATURI authority must be a valid handle or DID', { cause })\n  }\n\n  const collectionStart = authorityEnd === -1 ? -1 : authorityEnd + 1\n  const collectionEnd =\n    collectionStart === -1 ? -1 : uri.indexOf('/', collectionStart)\n\n  if (collectionStart !== -1) {\n    const collection =\n      collectionEnd === -1\n        ? uri.slice(collectionStart)\n        : uri.slice(collectionStart, collectionEnd)\n\n    if (collection.length === 0) {\n      throw new Error(\n        'ATURI can not have a slash after authority without a path segment',\n      )\n    }\n    if (!isValidNsid(collection)) {\n      throw new Error(\n        'ATURI requires first path segment (if supplied) to be valid NSID',\n      )\n    }\n  }\n\n  const recordKeyStart = collectionEnd === -1 ? -1 : collectionEnd + 1\n  const recordKeyEnd =\n    recordKeyStart === -1 ? -1 : uri.indexOf('/', recordKeyStart)\n\n  if (recordKeyStart !== -1) {\n    if (recordKeyStart === uri.length) {\n      throw new Error(\n        'ATURI can not have a slash after collection, unless record key is provided',\n      )\n    }\n    // would validate rkey here, but there are basically no constraints!\n  }\n\n  if (recordKeyEnd !== -1) {\n    throw new Error(\n      'ATURI path can have at most two parts, and no trailing slash',\n    )\n  }\n}\n\nexport function ensureValidAtUriRegex<I extends string>(\n  input: I,\n): asserts input is I & AtUriString {\n  // simple regex to enforce most constraints via just regex and length.\n  // hand wrote this regex based on above constraints. whew!\n  const aturiRegex =\n    /^at:\\/\\/(?<authority>[a-zA-Z0-9._:%-]+)(\\/(?<collection>[a-zA-Z0-9-.]+)(\\/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>\\/[a-zA-Z0-9._~:@!$&%')(*+,;=\\-[\\]/\\\\]*))?$/\n  const rm = input.match(aturiRegex)\n  if (!rm || !rm.groups) {\n    throw new Error(\"ATURI didn't validate via regex\")\n  }\n  const groups = rm.groups\n\n  try {\n    ensureValidHandleRegex(groups.authority)\n  } catch {\n    try {\n      ensureValidDidRegex(groups.authority)\n    } catch {\n      throw new Error('ATURI authority must be a valid handle or DID')\n    }\n  }\n\n  if (groups.collection && !isValidNsid(groups.collection)) {\n    throw new Error('ATURI collection path segment must be a valid NSID')\n  }\n\n  if (input.length > 8 * 1024) {\n    throw new Error('ATURI is far too long')\n  }\n}\n\nexport function isValidAtUri<I extends string>(\n  input: I,\n): input is I & AtUriString {\n  try {\n    ensureValidAtUriRegex(input)\n  } catch {\n    return false\n  }\n\n  return true\n}\n"
  },
  {
    "path": "packages/syntax/src/datetime.ts",
    "content": "/**\n * Indicates a date or string is not a valid representation of a datetime\n * according to the atproto\n * {@link https://atproto.com/specs/lexicon#datetime specification}.\n */\nexport class InvalidDatetimeError extends Error {}\n\n/**\n * A subset of {@link DatetimeString} that represent valid datetime strings with\n * the format: `YYYY-MM-DDTHH:mm:ss.sssZ`, as returned by `Date.toISOString()\n * for dates between the years 0000 and 9999.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString}\n */\nexport type ISODatetimeString =\n  // @TODO Switch to branded types for more accurate type safety.\n  `${string}-${string}-${string}T${string}:${string}:${string}.${string}Z`\n\n/**\n * Represents a {@link Date} that can be safely stringified into a valid atproto\n * {@link DatetimeString} using the {@link Date.toISOString toISOString()}\n * method.\n */\nexport interface AtprotoDate extends Date {\n  toISOString(): ISODatetimeString\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function assertAtprotoDate(date: Date): asserts date is AtprotoDate {\n  const res = parseDate(date)\n  if (!res.success) {\n    throw new InvalidDatetimeError(res.message)\n  }\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function asAtprotoDate(date: Date): AtprotoDate {\n  assertAtprotoDate(date)\n  return date\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function isAtprotoDate(date: Date): date is AtprotoDate {\n  return parseDate(date).success\n}\n\n/**\n * @see {@link AtprotoDate}\n */\nexport function ifAtprotoDate(date: Date): AtprotoDate | undefined {\n  return isAtprotoDate(date) ? date : undefined\n}\n\n/**\n * Datetime strings in atproto data structures and API calls should meet the\n * {@link https://ijmacd.github.io/rfc3339-iso8601/ intersecting} requirements\n * of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards.\n *\n * @note This literal template type is not accurate enough to ensure that a\n * string is a valid atproto datetime. The {@link DatetimeString} validation\n * functions ({@link assertDatetimeString}, {@link isDatetimeString}, etc)\n * should be used to validate that a string meets the atproto datetime\n * requirements, and the {@link toDatetimeString} function should be used to\n * convert a {@link Date} object into a valid {@link DatetimeString} string.\n *\n * @example \"2024-01-15T12:30:00Z\"\n * @example \"2024-01-15T12:30:00.000Z\"\n * @example \"2024-01-15T12:30:00+00:00\"\n * @example \"2024-01-15T11:30:00-01:00\"\n * @see {@link https://atproto.com/specs/lexicon#datetime atproto Lexicon datetime format}\n * @see {@link https://www.rfc-editor.org/rfc/rfc3339 RFC 3339}\n * @see {@link https://www.iso.org/iso-8601-date-and-time-format.html ISO 8601}\n */\nexport type DatetimeString =\n  // @TODO Switch to branded types for more accurate type safety?\n  | `${string}-${string}-${string}T${string}:${string}:${string}Z`\n  | `${string}-${string}-${string}T${string}:${string}:${string}${'+' | '-'}${string}:${string}`\n\n/**\n * Validates that a string is a valid {@link DatetimeString} format string,\n * throwing an error if it is not.\n *\n * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link DatetimeString}\n */\nexport function assertDatetimeString<I>(\n  input: I,\n): asserts input is I & DatetimeString {\n  const result = parseString(input)\n  if (!result.success) {\n    throw new InvalidDatetimeError(result.message)\n  }\n}\n\n/**\n * Casts a string to a {@link DatetimeString} if it is a valid datetime format\n * string, throwing an error if it is not.\n *\n * @throws InvalidDatetimeError if the input string does not meet the atproto 'datetime' format requirements.\n * @see {@link DatetimeString}\n */\nexport function asDatetimeString<I>(input: I): I & DatetimeString {\n  assertDatetimeString(input)\n  return input\n}\n\n/**\n * Checks if a string is a valid {@link DatetimeString} format string.\n *\n * @see {@link DatetimeString}\n */\nexport function isDatetimeString<I>(input: I): input is I & DatetimeString {\n  return parseString(input).success\n}\n\n/**\n * Returns the input if it is a valid {@link DatetimeString} format string, or\n * `undefined` if it is not.\n *\n * @see {@link DatetimeString}\n */\nexport function ifDatetimeString<I>(\n  input: I,\n): undefined | (I & DatetimeString) {\n  return isDatetimeString(input) ? input : undefined\n}\n\n/**\n * Returns the current date and time as a {@link DatetimeString}.\n *\n * @see {@link DatetimeString}\n */\nexport function currentDatetimeString(): DatetimeString {\n  return toDatetimeString(new Date())\n}\n\n/**\n * Converts any {@link Date} into a {@link DatetimeString} if possible, throwing\n * an error if the date is not a valid atproto datetime.\n *\n * This is short-hand for `asAtprotoDate(date).toISOString()`.\n *\n * @throws InvalidDatetimeError if the input date is not a valid atproto datetime (eg, it is too far in the future or past, or it normalizes to a negative year).\n * @see {@link DatetimeString}\n */\nexport function toDatetimeString(date: Date): DatetimeString {\n  return asAtprotoDate(date).toISOString()\n}\n\n/**\n * Takes a flexible datetime string and normalizes its representation.\n *\n * This function will work with any valid value that can be parsed as a date. It\n * *additionally* is more flexible about accepting datetimes that are missing\n * timezone information, and normalizing them to a valid atproto datetime.\n *\n * One use-case is a consistent, sortable string. Another is to work with older\n * invalid createdAt datetimes.\n *\n * @returns ISODatetimeString - a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax.\n * @throws InvalidDatetimeError - if the input string could not be parsed as a datetime, even with permissive parsing.\n */\nexport function normalizeDatetime(dtStr: string): ISODatetimeString {\n  // Parse the string as is\n  const date = new Date(dtStr)\n  if (isAtprotoDate(date)) {\n    return date.toISOString()\n  }\n\n  // if dtStr is not a valid date, try parsing again with a timezone\n  if (isNaN(date.getTime()) && !/.*(([+-]\\d\\d:?\\d\\d)|[a-zA-Z])$/.test(dtStr)) {\n    const date = new Date(`${dtStr}Z`)\n    if (isAtprotoDate(date)) {\n      return date.toISOString()\n    }\n  }\n\n  throw new InvalidDatetimeError(\n    'datetime did not parse as any timestamp format',\n  )\n}\n\n/**\n * Variant of {@link normalizeDatetime} which always returns a valid datetime\n * string.\n *\n * If a {@link InvalidDatetimeError} is encountered, returns the UNIX epoch time\n * as a UTC datetime (`1970-01-01T00:00:00.000Z`).\n *\n * @see {@link normalizeDatetime}\n */\nexport function normalizeDatetimeAlways(dtStr: string): ISODatetimeString {\n  try {\n    return normalizeDatetime(dtStr)\n  } catch (err) {\n    return '1970-01-01T00:00:00.000Z'\n  }\n}\n\n// Legacy exports (should we deprecate these ?)\nexport {\n  assertDatetimeString as ensureValidDatetime,\n  isDatetimeString as isValidDatetime,\n}\n\n// -----------------------------------------------------------------------------\n// ------------------------- Internal validation logic -------------------------\n// -----------------------------------------------------------------------------\n\n// Validation utils that allow avoiding try/catch for control flow (performance\n// optimization). Other syntax formats should also use this pattern to avoid\n// try/catch in their validation logic, at which point these utils can be moved\n// to a common internal utils.\ntype FailureResult = { success: false; message: string }\nconst failure = (m: string): FailureResult => ({ success: false, message: m })\ntype SuccessResult<V> = { success: true; value: V }\nconst success = <V>(v: V): SuccessResult<V> => ({ success: true, value: v })\ntype Result<V> = FailureResult | SuccessResult<V>\n\n/**\n * @see {@link https://www.rfc-editor.org/rfc/rfc3339#section-5.6 Internet Date/Time Format}\n *\n * @example\n * ```abnf\n * date-fullyear   = 4DIGIT\n * date-month      = 2DIGIT  ; 01-12\n * date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on\n *                           ; month/year\n * time-hour       = 2DIGIT  ; 00-23\n * time-minute     = 2DIGIT  ; 00-59\n * time-second     = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second\n *                           ; rules\n * time-secfrac    = \".\" 1*DIGIT\n * time-numoffset  = (\"+\" / \"-\") time-hour \":\" time-minute\n * time-offset     = \"Z\" / time-numoffset\n * partial-time    = time-hour \":\" time-minute \":\" time-second\n *                   [time-secfrac]\n * full-date       = date-fullyear \"-\" date-month \"-\" date-mday\n * full-time       = partial-time time-offset\n * date-time       = full-date \"T\" full-time\n * ```\n */\nconst DATETIME_REGEX =\n  /^(?<full_year>[0-9]{4})-(?<date_month>0[1-9]|1[012])-(?<date_mday>[0-2][0-9]|3[01])T(?<time_hour>[0-1][0-9]|2[0-3]):(?<time_minute>[0-5][0-9]):(?<time_second>[0-5][0-9]|60)(?<time_secfrac>\\.[0-9]+)?(?<time_offset>Z|(?<time_numoffset>[+-](?:[0-1][0-9]|2[0-3]):[0-5][0-9]))$/\n\n/**\n * Validates that the input is a datetime string according to atproto Lexicon\n * rules, and parses it into a Date object.\n */\nfunction parseString(input: unknown): Result<AtprotoDate> {\n  // @NOTE Performing cheap tests first\n  if (typeof input !== 'string') {\n    return failure('datetime must be a string')\n  }\n  if (input.length > 64) {\n    return failure('datetime is too long (64 chars max)')\n  }\n  if (input.endsWith('-00:00')) {\n    return failure('datetime can not use \"-00:00\" for UTC timezone')\n  }\n  if (!DATETIME_REGEX.test(input)) {\n    return failure(\n      \"datetime is not in a valid format (must match RFC 3339 & ISO 8601 with 'Z' or ±hh:mm timezone)\",\n    )\n  }\n\n  // must parse as ISO 8601; this also verifies semantics like leap seconds and\n  // correct number of days in month, which the regex does not check for\n  const date = new Date(input)\n\n  return parseDate(date)\n}\n\n/**\n * Ensures that a Date object represents a valid datetime according to atproto\n * Lexicon rules. This ensures that `date.toISOString()` will produce a valid\n * datetime string that can be used where {@link DatetimeString} is expected.\n */\nfunction parseDate(date: Date): Result<AtprotoDate> {\n  const fullYear = date.getUTCFullYear()\n  // Ensures that the date is valid. We could check isNaN(date.getTime()) here\n  // but since we'll check the year anyway, we just use that for the validity\n  // check since an invalid date will have NaN year.\n  if (Number.isNaN(fullYear)) {\n    return failure('datetime did not parse as ISO 8601')\n  }\n  // Ensure that the ISO string representation does not start with ±YYYYYY\n  if (fullYear < 0) {\n    return failure('datetime normalized to a negative time')\n  }\n  if (fullYear > 9999) {\n    return failure('datetime year is too far in the future')\n  }\n  if (fullYear < 10) {\n    return failure('datetime so close to year zero not allowed')\n  }\n  return success(date as AtprotoDate)\n}\n"
  },
  {
    "path": "packages/syntax/src/did.ts",
    "content": "// Human-readable constraints:\n//   - valid W3C DID (https://www.w3.org/TR/did-core/#did-syntax)\n//      - entire URI is ASCII: [a-zA-Z0-9._:%-]\n//      - always starts \"did:\" (lower-case)\n//      - method name is one or more lower-case letters, followed by \":\"\n//      - remaining identifier can have any of the above chars, but can not end in \":\"\n//      - it seems that a bunch of \":\" can be included, and don't need spaces between\n//      - \"%\" is used only for \"percent encoding\" and must be followed by two hex characters (and thus can't end in \"%\")\n//      - query (\"?\") and fragment (\"#\") stuff is defined for \"DID URIs\", but not as part of identifier itself\n//      - \"The current specification does not take a position on the maximum length of a DID\"\n//   - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer\n//   - hard length limit of 8KBytes\n//   - not going to validate \"percent encoding\" here\n\nexport type DidString<M extends string = string> = `did:${M}:${string}`\n\nexport function ensureValidDid<I extends string>(\n  input: I,\n): asserts input is I & DidString {\n  if (!input.startsWith('did:')) {\n    throw new InvalidDidError('DID requires \"did:\" prefix')\n  }\n\n  if (input.length > 2048) {\n    throw new InvalidDidError('DID is too long (2048 chars max)')\n  }\n\n  if (input.endsWith(':') || input.endsWith('%')) {\n    throw new InvalidDidError('DID can not end with \":\" or \"%\"')\n  }\n\n  // check that all chars are boring ASCII\n  if (!/^[a-zA-Z0-9._:%-]*$/.test(input)) {\n    throw new InvalidDidError(\n      'Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)',\n    )\n  }\n\n  const { length, 1: method } = input.split(':')\n  if (length < 3) {\n    throw new InvalidDidError(\n      'DID requires prefix, method, and method-specific content',\n    )\n  }\n\n  if (!/^[a-z]+$/.test(method)) {\n    throw new InvalidDidError('DID method must be lower-case letters')\n  }\n}\n\nconst DID_REGEX = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/\n\nexport function ensureValidDidRegex<I extends string>(\n  input: I,\n): asserts input is I & DidString {\n  // simple regex to enforce most constraints via just regex and length.\n  // hand wrote this regex based on above constraints\n  if (!DID_REGEX.test(input)) {\n    throw new InvalidDidError(\"DID didn't validate via regex\")\n  }\n\n  if (input.length > 2048) {\n    throw new InvalidDidError('DID is too long (2048 chars max)')\n  }\n}\n\nexport function isValidDid<I extends string>(input: I): input is I & DidString {\n  return input.length <= 2048 && DID_REGEX.test(input)\n}\n\nexport class InvalidDidError extends Error {}\n"
  },
  {
    "path": "packages/syntax/src/handle.ts",
    "content": "export const INVALID_HANDLE = 'handle.invalid'\n\nexport type HandleString = `${string}.${string}`\n\n// Currently these are registration-time restrictions, not protocol-level\n// restrictions. We have a couple accounts in the wild that we need to clean up\n// before hard-disallow.\n// See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains\nexport const DISALLOWED_TLDS = [\n  '.local',\n  '.arpa',\n  '.invalid',\n  '.localhost',\n  '.internal',\n  '.example',\n  '.alt',\n  // policy could concievably change on \".onion\" some day\n  '.onion',\n  // NOTE: .test is allowed in testing and devopment. In practical terms\n  // \"should\" \"never\" actually resolve and get registered in production\n]\n\n// Handle constraints, in English:\n//  - must be a possible domain name\n//    - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696,\n//      section 2. and RFC-3986, section 3. can now have leading numbers (eg,\n//      4chan.org)\n//    - \"labels\" (sub-names) are made of ASCII letters, digits, hyphens\n//    - can not start or end with a hyphen\n//    - TLD (last component) should not start with a digit\n//    - can't end with a hyphen (can end with digit)\n//    - each segment must be between 1 and 63 characters (not including any periods)\n//    - overall length can't be more than 253 characters\n//    - separated by (ASCII) periods; does not start or end with period\n//    - case insensitive\n//    - domains (handles) are equal if they are the same lower-case\n//    - punycode allowed for internationalization\n//  - no whitespace, null bytes, joining chars, etc\n//  - does not validate whether domain or TLD exists, or is a reserved or\n//    special TLD (eg, .onion or .local)\n//  - does not validate punycode\nexport function ensureValidHandle<I extends string>(\n  input: I,\n): asserts input is I & HandleString {\n  // check that all chars are boring ASCII\n  if (!/^[a-zA-Z0-9.-]*$/.test(input)) {\n    throw new InvalidHandleError(\n      'Disallowed characters in handle (ASCII letters, digits, dashes, periods only)',\n    )\n  }\n\n  if (input.length > 253) {\n    throw new InvalidHandleError('Handle is too long (253 chars max)')\n  }\n  const labels = input.split('.')\n  if (labels.length < 2) {\n    throw new InvalidHandleError('Handle domain needs at least two parts')\n  }\n  for (let i = 0; i < labels.length; i++) {\n    const l = labels[i]\n    if (l.length < 1) {\n      throw new InvalidHandleError('Handle parts can not be empty')\n    }\n    if (l.length > 63) {\n      throw new InvalidHandleError('Handle part too long (max 63 chars)')\n    }\n    if (l.endsWith('-') || l.startsWith('-')) {\n      throw new InvalidHandleError(\n        'Handle parts can not start or end with hyphens',\n      )\n    }\n    if (i + 1 === labels.length && !/^[a-zA-Z]/.test(l)) {\n      throw new InvalidHandleError(\n        'Handle final component (TLD) must start with ASCII letter',\n      )\n    }\n  }\n}\n\n// simple regex translation of above constraints\nconst HANDLE_REGEX =\n  /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/\n\nexport function ensureValidHandleRegex<I extends string>(\n  input: I,\n): asserts input is I & HandleString {\n  if (input.length > 253) {\n    throw new InvalidHandleError('Handle is too long (253 chars max)')\n  }\n  if (!HANDLE_REGEX.test(input)) {\n    throw new InvalidHandleError(\"Handle didn't validate via regex\")\n  }\n}\n\nexport function normalizeHandle(handle: HandleString): HandleString\nexport function normalizeHandle(handle: string): string\nexport function normalizeHandle(handle: string): string {\n  return handle.toLowerCase()\n}\n\nexport function normalizeAndEnsureValidHandle(handle: string): HandleString {\n  const normalized = normalizeHandle(handle)\n  ensureValidHandle(normalized)\n  return normalized\n}\n\nexport function isValidHandle<I extends string>(\n  input: I,\n): input is I & HandleString {\n  return input.length <= 253 && HANDLE_REGEX.test(input)\n}\n\nexport function isValidTld(handle: string): boolean {\n  for (const tld of DISALLOWED_TLDS) {\n    if (handle.endsWith(tld)) {\n      return false\n    }\n  }\n  return true\n}\n\nexport class InvalidHandleError extends Error {}\n/** @deprecated Never used */\nexport class ReservedHandleError extends Error {}\n/** @deprecated Never used */\nexport class UnsupportedDomainError extends Error {}\n/** @deprecated Never used */\nexport class DisallowedDomainError extends Error {}\n"
  },
  {
    "path": "packages/syntax/src/index.ts",
    "content": "export * from './at-identifier.js'\nexport * from './aturi.js'\nexport * from './datetime.js'\nexport * from './did.js'\nexport * from './handle.js'\nexport * from './nsid.js'\nexport * from './language.js'\nexport * from './recordkey.js'\nexport * from './tid.js'\nexport * from './uri.js'\n"
  },
  {
    "path": "packages/syntax/src/language.ts",
    "content": "const BCP47_REGEXP =\n  /^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/\n\nexport type LanguageTag = {\n  grandfathered?: string\n  language?: string\n  extlang?: string\n  script?: string\n  region?: string\n  variant?: string\n  extension?: string\n  privateUse?: string\n}\n\nexport function parseLanguageString(input: string): LanguageTag | null {\n  const parsed = input.match(BCP47_REGEXP)\n  if (!parsed?.groups) return null\n\n  const { groups } = parsed\n  return {\n    grandfathered: groups.grandfathered,\n    language: groups.language,\n    extlang: groups.extlang,\n    script: groups.script,\n    region: groups.region,\n    variant: groups.variant,\n    extension: groups.extension,\n    privateUse: groups.privateUseA || groups.privateUseB,\n  }\n}\n\n/**\n * Validates well-formed BCP 47 syntax\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1}\n */\nexport function isValidLanguage(input: string): boolean {\n  return BCP47_REGEXP.test(input)\n}\n"
  },
  {
    "path": "packages/syntax/src/nsid.ts",
    "content": "/*\nGrammar:\n\nalpha     = \"a\" / \"b\" / \"c\" / \"d\" / \"e\" / \"f\" / \"g\" / \"h\" / \"i\" / \"j\" / \"k\" / \"l\" / \"m\" / \"n\" / \"o\" / \"p\" / \"q\" / \"r\" / \"s\" / \"t\" / \"u\" / \"v\" / \"w\" / \"x\" / \"y\" / \"z\" / \"A\" / \"B\" / \"C\" / \"D\" / \"E\" / \"F\" / \"G\" / \"H\" / \"I\" / \"J\" / \"K\" / \"L\" / \"M\" / \"N\" / \"O\" / \"P\" / \"Q\" / \"R\" / \"S\" / \"T\" / \"U\" / \"V\" / \"W\" / \"X\" / \"Y\" / \"Z\"\nnumber    = \"1\" / \"2\" / \"3\" / \"4\" / \"5\" / \"6\" / \"7\" / \"8\" / \"9\" / \"0\"\ndelim     = \".\"\nsegment   = alpha *( alpha / number / \"-\" )\nauthority = segment *( delim segment )\nname      = alpha *( alpha / number )\nnsid      = authority delim name\n\n*/\n\nexport type NsidString = `${string}.${string}.${string}`\n\nexport class NSID {\n  readonly segments: readonly string[]\n\n  static parse(input: string): NSID {\n    return new NSID(input)\n  }\n\n  static create(authority: string, name: string): NSID {\n    const input = [...authority.split('.').reverse(), name].join('.')\n    return new NSID(input)\n  }\n\n  static isValid(nsid: string) {\n    return isValidNsid(nsid)\n  }\n\n  static from(input: { toString: () => string }): NSID {\n    if (input instanceof NSID) {\n      // No need to clone, NSID is immutable\n      return input\n    }\n    if (Array.isArray(input)) {\n      return new NSID((input as string[]).join('.'))\n    }\n    return new NSID(String(input))\n  }\n\n  constructor(nsid: string) {\n    this.segments = parseNsid(nsid)\n  }\n\n  get authority() {\n    return this.segments\n      .slice(0, this.segments.length - 1)\n      .reverse()\n      .join('.') as `${string}.${string}`\n  }\n\n  get name() {\n    return this.segments.at(this.segments.length - 1)\n  }\n\n  toString(): NsidString {\n    return this.segments.join('.') as NsidString\n  }\n}\n\nexport function ensureValidNsid<I extends string>(\n  input: I,\n): asserts input is I & NsidString {\n  const result = validateNsid(input)\n  if (!result.success) throw new InvalidNsidError(result.message)\n}\n\nexport function parseNsid(nsid: string): string[] {\n  const result = validateNsid(nsid)\n  if (!result.success) throw new InvalidNsidError(result.message)\n  return result.value\n}\n\nexport function isValidNsid<I extends string>(\n  input: I,\n): input is I & NsidString {\n  // Since the regex version is more performant for valid NSIDs, we use it when\n  // we don't care about error details.\n  return validateNsidRegex(input).success\n}\n\ntype ValidateResult<T> =\n  | { success: true; value: T }\n  | { success: false; message: string }\n\n// Human readable constraints on NSID:\n// - a valid domain in reversed notation\n// - followed by an additional period-separated name, which is camel-case letters\nexport function validateNsid(input: string): ValidateResult<string[]> {\n  if (input.length > 253 + 1 + 63) {\n    return {\n      success: false,\n      message: 'NSID is too long (317 chars max)',\n    }\n  }\n  if (hasDisallowedCharacters(input)) {\n    return {\n      success: false,\n      message:\n        'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',\n    }\n  }\n  const segments = input.split('.')\n  if (segments.length < 3) {\n    return {\n      success: false,\n      message: 'NSID needs at least three parts',\n    }\n  }\n  for (const l of segments) {\n    if (l.length < 1) {\n      return {\n        success: false,\n        message: 'NSID parts can not be empty',\n      }\n    }\n    if (l.length > 63) {\n      return {\n        success: false,\n        message: 'NSID part too long (max 63 chars)',\n      }\n    }\n    if (startsWithHyphen(l) || endsWithHyphen(l)) {\n      return {\n        success: false,\n        message: 'NSID parts can not start or end with hyphen',\n      }\n    }\n  }\n  if (startsWithNumber(segments[0])) {\n    return {\n      success: false,\n      message: 'NSID first part may not start with a digit',\n    }\n  }\n  if (!isValidIdentifier(segments[segments.length - 1])) {\n    return {\n      success: false,\n      message:\n        'NSID name part must be only letters and digits (and no leading digit)',\n    }\n  }\n  return {\n    success: true,\n    value: segments,\n  }\n}\n\nfunction hasDisallowedCharacters(v: string) {\n  return !/^[a-zA-Z0-9.-]*$/.test(v)\n}\n\nfunction startsWithNumber(v: string) {\n  const charCode = v.charCodeAt(0)\n  return charCode >= 48 && charCode <= 57\n}\n\nfunction startsWithHyphen(v: string) {\n  return v.charCodeAt(0) === 45 /* - */\n}\n\nfunction endsWithHyphen(v: string) {\n  return v.charCodeAt(v.length - 1) === 45 /* - */\n}\n\nfunction isValidIdentifier(v: string) {\n  // Note, since we already know that \"v\" only contains [a-zA-Z0-9-], we can\n  // simplify the following regex by checking only the first char and presence\n  // of \"-\".\n\n  // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v)\n  return !startsWithNumber(v) && !v.includes('-')\n}\n\n/**\n * @deprecated Use {@link ensureValidNsid} if you care about error details,\n * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or\n * {@link isValidNsid} if you just want a boolean.\n */\nexport function ensureValidNsidRegex(nsid: string): asserts nsid is NsidString {\n  const result = validateNsidRegex(nsid)\n  if (!result.success) throw new InvalidNsidError(result.message)\n}\n\n/**\n * Regexp based validation that behaves identically to the previous code but\n * provides less detailed error messages (while being 20% to 50% faster).\n */\nexport function validateNsidRegex(value: string): ValidateResult<NsidString> {\n  if (value.length > 253 + 1 + 63) {\n    return {\n      success: false,\n      message: 'NSID is too long (317 chars max)',\n    }\n  }\n\n  if (\n    // Fast check for small values\n    value.length < 5 ||\n    !/^[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:\\.[a-zA-Z](?:[a-zA-Z0-9]{0,62})?)$/.test(\n      value,\n    )\n  ) {\n    return {\n      success: false,\n      message: \"NSID didn't validate via regex\",\n    }\n  }\n\n  return {\n    success: true,\n    value: value as NsidString,\n  }\n}\n\nexport class InvalidNsidError extends Error {}\n"
  },
  {
    "path": "packages/syntax/src/recordkey.ts",
    "content": "export type RecordKeyString = string\n\nconst RECORD_KEY_MAX_LENGTH = 512\nconst RECORD_KEY_MIN_LENGTH = 1\nconst RECORD_KEY_INVALID_VALUES = new Set(['.', '..'])\nconst RECORD_KEY_REGEX = /^[a-zA-Z0-9_~.:-]{1,512}$/\n\n// https://atproto.com/specs/record-key#record-key-syntax\n// Regardless of the type, Record Keys must fulfill some baseline syntax constraints:\n// - restricted to a subset of ASCII characters -- the allowed characters are\n//   alphanumeric (A-Za-z0-9), period, dash, underscore, colon, or tilde (.-_:~)\n// - must have at least 1 and at most 512 characters\n// - the specific record key values . and .. are not allowed\n// - must be a permissible part of repository MST path string (the above\n//   constraints satisfy this condition)\n// - must be permissible to include in a path component of a URI (following\n//   RFC-3986, section 3.3). The above constraints satisfy this condition, by\n//   matching the \"unreserved\" characters allowed in generic URI paths.\n\nexport function ensureValidRecordKey<I extends string>(\n  input: I,\n): asserts input is I & RecordKeyString {\n  if (\n    input.length > RECORD_KEY_MAX_LENGTH ||\n    input.length < RECORD_KEY_MIN_LENGTH\n  ) {\n    throw new InvalidRecordKeyError(\n      `record key must be ${RECORD_KEY_MIN_LENGTH} to ${RECORD_KEY_MAX_LENGTH} characters`,\n    )\n  }\n  if (RECORD_KEY_INVALID_VALUES.has(input)) {\n    throw new InvalidRecordKeyError('record key can not be \".\" or \"..\"')\n  }\n  // simple regex to enforce most constraints via just regex and length.\n  if (!RECORD_KEY_REGEX.test(input)) {\n    throw new InvalidRecordKeyError('record key syntax not valid (regex)')\n  }\n}\n\nexport function isValidRecordKey<I extends string>(\n  input: I,\n): input is I & RecordKeyString {\n  return (\n    input.length >= RECORD_KEY_MIN_LENGTH &&\n    input.length <= RECORD_KEY_MAX_LENGTH &&\n    RECORD_KEY_REGEX.test(input) &&\n    !RECORD_KEY_INVALID_VALUES.has(input)\n  )\n}\n\nexport class InvalidRecordKeyError extends Error {}\n"
  },
  {
    "path": "packages/syntax/src/tid.ts",
    "content": "export type TidString = string\n\nconst TID_LENGTH = 13\nconst TID_REGEX = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/\n\nexport function ensureValidTid<I extends string>(\n  input: I,\n): asserts input is I & TidString {\n  if (input.length !== TID_LENGTH) {\n    throw new InvalidTidError(`TID must be ${TID_LENGTH} characters`)\n  }\n  // simple regex to enforce most constraints via just regex and length.\n  if (!TID_REGEX.test(input)) {\n    throw new InvalidTidError('TID syntax not valid (regex)')\n  }\n}\n\nexport function isValidTid<I extends string>(input: I): input is I & TidString {\n  return input.length === TID_LENGTH && TID_REGEX.test(input)\n}\n\nexport class InvalidTidError extends Error {}\n"
  },
  {
    "path": "packages/syntax/src/uri.ts",
    "content": "export type UriString = `${string}:${string}`\n\nexport function isValidUri<I extends string>(input: I): input is I & UriString {\n  return /^\\w+:(?:\\/\\/)?[^\\s/][^\\s]*$/.test(input)\n}\n"
  },
  {
    "path": "packages/syntax/tests/aturi.test.ts",
    "content": "import * as fs from 'node:fs'\nimport * as readline from 'node:readline'\nimport { describe, expect, it } from 'vitest'\nimport { AtUri, ensureValidAtUri, ensureValidAtUriRegex } from '../src'\n\ndescribe(AtUri, () => {\n  it('parses valid at uris', () => {\n    //                 input   host    path    query   hash\n    type AtUriTest = [string, string, string, string, string]\n    const TESTS: AtUriTest[] = [\n      ['foo.com', 'foo.com', '', '', ''],\n      ['at://foo.com', 'foo.com', '', '', ''],\n      ['at://foo.com/', 'foo.com', '/', '', ''],\n      ['at://foo.com/foo', 'foo.com', '/foo', '', ''],\n      ['at://foo.com/foo/', 'foo.com', '/foo/', '', ''],\n      ['at://foo.com/foo/bar', 'foo.com', '/foo/bar', '', ''],\n      ['at://foo.com?foo=bar', 'foo.com', '', 'foo=bar', ''],\n      ['at://foo.com?foo=bar&baz=buux', 'foo.com', '', 'foo=bar&baz=buux', ''],\n      ['at://foo.com/?foo=bar', 'foo.com', '/', 'foo=bar', ''],\n      ['at://foo.com/foo?foo=bar', 'foo.com', '/foo', 'foo=bar', ''],\n      ['at://foo.com/foo/?foo=bar', 'foo.com', '/foo/', 'foo=bar', ''],\n      ['at://foo.com#hash', 'foo.com', '', '', '#hash'],\n      ['at://foo.com/#hash', 'foo.com', '/', '', '#hash'],\n      ['at://foo.com/foo#hash', 'foo.com', '/foo', '', '#hash'],\n      ['at://foo.com/foo/#hash', 'foo.com', '/foo/', '', '#hash'],\n      ['at://foo.com?foo=bar#hash', 'foo.com', '', 'foo=bar', '#hash'],\n\n      [\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '',\n        '',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '',\n        '',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/',\n        '',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo',\n        '',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo/',\n        '',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/bar',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo/bar',\n        '',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar&baz=buux',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '',\n        'foo=bar&baz=buux',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/?foo=bar',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo?foo=bar',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/?foo=bar',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo/',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw#hash',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/#hash',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo#hash',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/#hash',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '/foo/',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar#hash',\n        'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',\n        '',\n        'foo=bar',\n        '#hash',\n      ],\n\n      ['did:web:localhost%3A1234', 'did:web:localhost%3A1234', '', '', ''],\n      ['at://did:web:localhost%3A1234', 'did:web:localhost%3A1234', '', '', ''],\n      [\n        'at://did:web:localhost%3A1234/',\n        'did:web:localhost%3A1234',\n        '/',\n        '',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo',\n        'did:web:localhost%3A1234',\n        '/foo',\n        '',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo/',\n        'did:web:localhost%3A1234',\n        '/foo/',\n        '',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo/bar',\n        'did:web:localhost%3A1234',\n        '/foo/bar',\n        '',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234?foo=bar',\n        'did:web:localhost%3A1234',\n        '',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234?foo=bar&baz=buux',\n        'did:web:localhost%3A1234',\n        '',\n        'foo=bar&baz=buux',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234/?foo=bar',\n        'did:web:localhost%3A1234',\n        '/',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo?foo=bar',\n        'did:web:localhost%3A1234',\n        '/foo',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo/?foo=bar',\n        'did:web:localhost%3A1234',\n        '/foo/',\n        'foo=bar',\n        '',\n      ],\n      [\n        'at://did:web:localhost%3A1234#hash',\n        'did:web:localhost%3A1234',\n        '',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:web:localhost%3A1234/#hash',\n        'did:web:localhost%3A1234',\n        '/',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo#hash',\n        'did:web:localhost%3A1234',\n        '/foo',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:web:localhost%3A1234/foo/#hash',\n        'did:web:localhost%3A1234',\n        '/foo/',\n        '',\n        '#hash',\n      ],\n      [\n        'at://did:web:localhost%3A1234?foo=bar#hash',\n        'did:web:localhost%3A1234',\n        '',\n        'foo=bar',\n        '#hash',\n      ],\n      [\n        'at://4513echo.bsky.social/app.bsky.feed.post/3jsrpdyf6ss23',\n        '4513echo.bsky.social',\n        '/app.bsky.feed.post/3jsrpdyf6ss23',\n        '',\n        '',\n      ],\n    ]\n    for (const [uri, hostname, pathname, search, hash] of TESTS) {\n      const urip = new AtUri(uri)\n      expect(urip.protocol).toBe('at:')\n      expect(urip.host).toBe(hostname)\n      expect(urip.hostname).toBe(hostname)\n      expect(urip.origin).toBe(`at://${hostname}`)\n      expect(urip.pathname).toBe(pathname)\n      expect(urip.search).toBe(search)\n      expect(urip.hash).toBe(hash)\n    }\n  })\n\n  it('handles ATP-specific parsing', () => {\n    {\n      const urip = new AtUri('at://foo.com')\n      expect(urip.collection).toBe('')\n      expect(urip.rkey).toBe('')\n    }\n    {\n      const urip = new AtUri('at://foo.com/com.example.foo')\n      expect(urip.collection).toBe('com.example.foo')\n      expect(urip.rkey).toBe('')\n    }\n    {\n      const urip = new AtUri('at://foo.com/com.example.foo/123')\n      expect(urip.collection).toBe('com.example.foo')\n      expect(urip.rkey).toBe('123')\n    }\n  })\n\n  it('supports modifications', () => {\n    const urip = new AtUri('at://foo.com')\n    expect(urip.toString()).toBe('at://foo.com/')\n\n    urip.host = 'bar.com'\n    expect(urip.toString()).toBe('at://bar.com/')\n    urip.host = 'did:web:localhost%3A1234'\n    expect(urip.toString()).toBe('at://did:web:localhost%3A1234/')\n    urip.host = 'foo.com'\n\n    urip.pathname = '/'\n    expect(urip.toString()).toBe('at://foo.com/')\n    urip.pathname = '/foo'\n    expect(urip.toString()).toBe('at://foo.com/foo')\n    urip.pathname = 'foo'\n    expect(urip.toString()).toBe('at://foo.com/foo')\n\n    urip.collection = 'com.example.foo'\n    urip.rkey = '123'\n    expect(urip.toString()).toBe('at://foo.com/com.example.foo/123')\n    urip.rkey = '124'\n    expect(urip.toString()).toBe('at://foo.com/com.example.foo/124')\n    urip.collection = 'com.other.foo'\n    expect(urip.toString()).toBe('at://foo.com/com.other.foo/124')\n    urip.pathname = ''\n    urip.rkey = '123'\n    expect(urip.toString()).toBe('at://foo.com/undefined/123')\n    urip.pathname = 'foo'\n\n    urip.search = '?foo=bar'\n    expect(urip.toString()).toBe('at://foo.com/foo?foo=bar')\n    urip.searchParams.set('baz', 'buux')\n    expect(urip.toString()).toBe('at://foo.com/foo?foo=bar&baz=buux')\n\n    urip.hash = '#hash'\n    expect(urip.toString()).toBe('at://foo.com/foo?foo=bar&baz=buux#hash')\n    urip.hash = 'hash'\n    expect(urip.toString()).toBe('at://foo.com/foo?foo=bar&baz=buux#hash')\n  })\n\n  it('supports relative URIs', () => {\n    //                 input   path    query   hash\n    type AtUriTest = [string, string, string, string]\n    const TESTS: AtUriTest[] = [\n      // input hostname pathname query hash\n      ['', '', '', ''],\n      ['/', '/', '', ''],\n      ['/foo', '/foo', '', ''],\n      ['/foo/', '/foo/', '', ''],\n      ['/foo/bar', '/foo/bar', '', ''],\n      ['?foo=bar', '', 'foo=bar', ''],\n      ['?foo=bar&baz=buux', '', 'foo=bar&baz=buux', ''],\n      ['/?foo=bar', '/', 'foo=bar', ''],\n      ['/foo?foo=bar', '/foo', 'foo=bar', ''],\n      ['/foo/?foo=bar', '/foo/', 'foo=bar', ''],\n      ['#hash', '', '', '#hash'],\n      ['/#hash', '/', '', '#hash'],\n      ['/foo#hash', '/foo', '', '#hash'],\n      ['/foo/#hash', '/foo/', '', '#hash'],\n      ['?foo=bar#hash', '', 'foo=bar', '#hash'],\n    ]\n    const BASES: string[] = [\n      'did:web:localhost%3A1234',\n      'at://did:web:localhost%3A1234',\n      'at://did:web:localhost%3A1234/foo/bar?foo=bar&baz=buux#hash',\n      'did:web:localhost%3A1234',\n      'at://did:web:localhost%3A1234',\n      'at://did:web:localhost%3A1234/foo/bar?foo=bar&baz=buux#hash',\n    ]\n\n    for (const base of BASES) {\n      const basep = new AtUri(base)\n      for (const [relative, pathname, search, hash] of TESTS) {\n        const urip = new AtUri(relative, base)\n        expect(urip.protocol).toBe('at:')\n        expect(urip.host).toBe(basep.host)\n        expect(urip.hostname).toBe(basep.hostname)\n        expect(urip.origin).toBe(basep.origin)\n        expect(urip.pathname).toBe(pathname)\n        expect(urip.search).toBe(search)\n        expect(urip.hash).toBe(hash)\n      }\n    }\n  })\n})\n\ndescribe('AtUri validation', () => {\n  const expectValid = (h: string) => {\n    ensureValidAtUri(h)\n    ensureValidAtUriRegex(h)\n  }\n  const expectInvalid = (h: string) => {\n    expect(() => ensureValidAtUri(h)).toThrow()\n    expect(() => ensureValidAtUriRegex(h)).toThrow()\n  }\n\n  it('enforces spec basics', () => {\n    expectValid('at://did:plc:asdf123')\n    expectValid('at://user.bsky.social')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/record')\n\n    expectValid('at://did:plc:asdf123#/frag')\n    expectValid('at://user.bsky.social#/frag')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post#/frag')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/record#/frag')\n\n    expectInvalid('a://did:plc:asdf123')\n    expectInvalid('at//did:plc:asdf123')\n    expectInvalid('at:/a/did:plc:asdf123')\n    expectInvalid('at:/did:plc:asdf123')\n    expectInvalid('AT://did:plc:asdf123')\n    expectInvalid('http://did:plc:asdf123')\n    expectInvalid('://did:plc:asdf123')\n    expectInvalid('at:did:plc:asdf123')\n    expectInvalid('at:/did:plc:asdf123')\n    expectInvalid('at:///did:plc:asdf123')\n    expectInvalid('at://:/did:plc:asdf123')\n    expectInvalid('at:/ /did:plc:asdf123')\n    expectInvalid('at://did:plc:asdf123 ')\n    expectInvalid('at://did:plc:asdf123/ ')\n    expectInvalid(' at://did:plc:asdf123')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post ')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post# ')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#/ ')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#/frag ')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#fr ag')\n    expectInvalid('//did:plc:asdf123')\n    expectInvalid('at://name')\n    expectInvalid('at://name.0')\n    expectInvalid('at://diD:plc:asdf123')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.p@st')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.p$st')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.p%st')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.p&st')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.p()t')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed_post')\n    expectInvalid('at://did:plc:asdf123/-com.atproto.feed.post')\n    expectInvalid('at://did:plc:asdf@123/com.atproto.feed.post')\n\n    expectInvalid('at://DID:plc:asdf123')\n    expectInvalid('at://user.bsky.123')\n    expectInvalid('at://bsky')\n    expectInvalid('at://did:plc:')\n    expectInvalid('at://did:plc:')\n    expectInvalid('at://frag')\n\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(800))\n    expectInvalid(\n      'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200),\n    )\n  })\n\n  it('has specified behavior on edge cases', () => {\n    expectInvalid('at://user.bsky.social//')\n    expectInvalid('at://user.bsky.social//com.atproto.feed.post')\n    expectInvalid('at://user.bsky.social/com.atproto.feed.post//')\n    expectInvalid(\n      'at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more',\n    )\n    expectInvalid('at://did:plc:asdf123/short/stuff')\n    expectInvalid('at://did:plc:asdf123/12345')\n  })\n\n  it('enforces no trailing slashes', () => {\n    expectValid('at://did:plc:asdf123')\n    expectInvalid('at://did:plc:asdf123/')\n\n    expectValid('at://user.bsky.social')\n    expectInvalid('at://user.bsky.social/')\n\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/')\n\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/record')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/#/frag')\n  })\n\n  it('enforces strict paths', () => {\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123')\n    expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf')\n  })\n\n  it('is very permissive about record keys', () => {\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/a')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/%23')\n\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123')\n    expectValid(\"at://did:plc:asdf123/com.atproto.feed.post/~'sdf123\")\n\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/$')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/@')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/!')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/*')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/(')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/,')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/;')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/abc%30123')\n  })\n\n  it('is probably too permissive about URL encoding', () => {\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/%30')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/%3')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/%')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/%zz')\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post/%%%')\n  })\n\n  it('is very permissive about fragments', () => {\n    expectValid('at://did:plc:asdf123#/frac')\n\n    expectInvalid('at://did:plc:asdf123#')\n    expectInvalid('at://did:plc:asdf123##')\n    expectInvalid('#at://did:plc:asdf123')\n    expectInvalid('at://did:plc:asdf123#/asdf#/asdf')\n\n    expectValid('at://did:plc:asdf123#/com.atproto.feed.post')\n    expectValid('at://did:plc:asdf123#/com.atproto.feed.post/')\n    expectValid('at://did:plc:asdf123#/asdf/')\n\n    expectValid('at://did:plc:asdf123/com.atproto.feed.post#/$@!*():,;~.sdf123')\n    expectValid('at://did:plc:asdf123#/[asfd]')\n\n    expectValid('at://did:plc:asdf123#/$')\n    expectValid('at://did:plc:asdf123#/*')\n    expectValid('at://did:plc:asdf123#/;')\n    expectValid('at://did:plc:asdf123#/,')\n  })\n\n  it('conforms to interop valid ATURIs', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/aturi_syntax_valid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectValid(line)\n    })\n  })\n\n  it('properly checks that the did property is a valid did', () => {\n    const urip = new AtUri('at://did:example:123')\n    expect(urip.did).toBe('did:example:123')\n    urip.host = 'did:example:456'\n    expect(urip.did).toBe('did:example:456')\n    urip.host = 'foo.com'\n    expect(() => urip.did).toThrow()\n  })\n\n  it('properly checks that the collection is a valid nsid', () => {\n    const urip = new AtUri('at://foo.com')\n    expect(urip.collection).toBe('')\n    expect(() => urip.collectionSafe).toThrow()\n\n    urip.collection = 'com.example.foo'\n    expect(urip.collection).toBe('com.example.foo')\n    expect(urip.collectionSafe).toBe('com.example.foo')\n\n    urip.collection = 'com.other.foo'\n    expect(urip.collection).toBe('com.other.foo')\n    expect(urip.collectionSafe).toBe('com.other.foo')\n\n    expect(() => (urip.collection = 'not a valid nsid')).toThrow()\n    expect(urip.collection).toBe('com.other.foo') // unchanged after failed set\n\n    urip.unsafelySetCollection('not-a-valid-nsid')\n    expect(urip.collection).toBe('not-a-valid-nsid')\n    expect(() => urip.collectionSafe).toThrow()\n  })\n\n  it('properly checks that the rkey is a valid record key', () => {\n    const urip = new AtUri('at://foo.com')\n    expect(urip.rkey).toBe('')\n    expect(() => urip.rkeySafe).toThrow()\n\n    urip.rkey = 'valid_rkey-123'\n    expect(urip.rkey).toBe('valid_rkey-123')\n    expect(urip.rkeySafe).toBe('valid_rkey-123')\n\n    expect(() => (urip.rkey = 'not a valid rkey')).toThrow()\n    expect(urip.rkey).toBe('valid_rkey-123') // unchanged after failed set\n\n    urip.unsafelySetRkey('not a valid rkey')\n    expect(urip.rkey).toBe('not a valid rkey')\n    expect(() => urip.rkeySafe).toThrow()\n  })\n\n  // NOTE: this package is currently more permissive than spec about AT URIs, so invalid cases are not errors\n})\n"
  },
  {
    "path": "packages/syntax/tests/datetime.test.ts",
    "content": "import * as fs from 'node:fs'\nimport { describe, expect, it, test } from 'vitest'\nimport {\n  InvalidDatetimeError,\n  ensureValidDatetime,\n  isValidDatetime,\n  normalizeDatetime,\n  normalizeDatetimeAlways,\n} from '../src'\n\nconst interopValid = readLines(\n  `${__dirname}/interop-files/datetime_syntax_valid.txt`,\n)\nconst interopInvalidSyntax = readLines(\n  `${__dirname}/interop-files/datetime_syntax_invalid.txt`,\n)\nconst interopInvalidParse = readLines(\n  `${__dirname}/interop-files/datetime_parse_invalid.txt`,\n)\n\ndescribe(ensureValidDatetime, () => {\n  describe('valid interop', () => {\n    for (const dt of interopValid) {\n      test(dt, () => {\n        expect(() => ensureValidDatetime(dt)).not.toThrow()\n      })\n    }\n  })\n\n  describe('fails on interop (invalid syntax)', () => {\n    for (const dt of interopInvalidSyntax) {\n      test(dt, () => {\n        expect(() => ensureValidDatetime(dt)).toThrow(InvalidDatetimeError)\n      })\n    }\n  })\n\n  describe('fails on interop (invalid parse)', () => {\n    for (const dt of interopInvalidParse) {\n      test(dt, () => {\n        expect(() => ensureValidDatetime(dt)).toThrow(InvalidDatetimeError)\n      })\n    }\n  })\n\n  it('rejects datetime that normalizes past year 9999 due to negative offset', () => {\n    // 9999-12-31T23:59:00-00:01 is syntactically valid, but normalizing to\n    // UTC advances it to 10000-01-01T00:00:00Z, which is out of range\n    expect(() => ensureValidDatetime('9999-12-31T23:59:00-00:01')).toThrow(\n      InvalidDatetimeError,\n    )\n  })\n})\n\ndescribe(isValidDatetime, () => {\n  describe('valid interop', () => {\n    for (const dt of interopValid) {\n      test(dt, () => {\n        expect(isValidDatetime(dt)).toBe(true)\n      })\n    }\n  })\n\n  describe('fails on interop (invalid syntax)', () => {\n    for (const dt of interopInvalidSyntax) {\n      test(dt, () => {\n        expect(isValidDatetime(dt)).toBe(false)\n      })\n    }\n  })\n\n  describe('fails on interop (invalid parse)', () => {\n    for (const dt of interopInvalidParse) {\n      test(dt, () => {\n        expect(isValidDatetime(dt)).toBe(false)\n      })\n    }\n  })\n\n  it('rejects datetime that normalizes past year 9999 due to negative offset', () => {\n    // 9999-12-31T23:59:00-00:01 is syntactically valid, but normalizing to\n    // UTC advances it to 10000-01-01T00:00:00Z, which is out of range\n    expect(isValidDatetime('9999-12-31T23:59:00-00:01')).toBe(false)\n  })\n})\n\ndescribe(normalizeDatetime, () => {\n  describe('valid interop', () => {\n    for (const dt of interopValid) {\n      test(dt, () => {\n        expect(() => normalizeDatetime(dt)).not.toThrow()\n      })\n    }\n  })\n\n  // @NOTE Normalize will actually succeed on some of the invalid syntax cases,\n  // because it is more lenient than the regex validation.\n\n  // describe('fails on interop (invalid syntax)', () => {\n  //   for (const dt of interopInvalidSyntax) {\n  //     test(dt, () => {\n  //       expect(() => normalizeDatetime(dt)).toThrow(InvalidDatetimeError)\n  //     })\n  //   }\n  // })\n\n  describe('fails on interop (invalid parse)', () => {\n    for (const dt of interopInvalidParse) {\n      test(dt, () => {\n        expect(() => normalizeDatetime(dt)).toThrow(InvalidDatetimeError)\n      })\n    }\n  })\n\n  it('normalizes valid input', () => {\n    expect(normalizeDatetime('1234-04-12T23:20:50Z')).toEqual(\n      '1234-04-12T23:20:50.000Z',\n    )\n    expect(normalizeDatetime('1985-04-12T23:20:50Z')).toEqual(\n      '1985-04-12T23:20:50.000Z',\n    )\n    expect(normalizeDatetime('1985-04-12T23:20:50.123')).toEqual(\n      '1985-04-12T23:20:50.123Z',\n    )\n    expect(normalizeDatetime('1985-04-12 23:20:50.123')).toEqual(\n      '1985-04-12T23:20:50.123Z',\n    )\n    expect(normalizeDatetime('1985-04-12T10:20:50.1+01:00')).toEqual(\n      '1985-04-12T09:20:50.100Z',\n    )\n    expect(normalizeDatetime('Fri, 02 Jan 1999 12:34:56 GMT')).toEqual(\n      '1999-01-02T12:34:56.000Z',\n    )\n  })\n\n  it('throws on invalid input', () => {\n    expect(() => normalizeDatetime('')).toThrow(InvalidDatetimeError)\n    expect(() => normalizeDatetime('blah')).toThrow(InvalidDatetimeError)\n    expect(() => normalizeDatetime('1999-19-39T23:20:50.123Z')).toThrow(\n      InvalidDatetimeError,\n    )\n    expect(() => normalizeDatetime('-000001-12-31T23:00:00.000Z')).toThrow(\n      InvalidDatetimeError,\n    )\n    expect(() => normalizeDatetime('0000-01-01T00:00:00+01:00')).toThrow(\n      InvalidDatetimeError,\n    )\n    expect(() => normalizeDatetime('0001-01-01T00:00:00+01:00')).toThrow(\n      InvalidDatetimeError,\n    )\n    // 9999-12-31T23:59:00-00:01 is syntactically valid, but normalizing to\n    // UTC advances it to 10000-01-01T00:00:00Z, which is out of range\n    expect(() => normalizeDatetime('9999-12-31T23:59:00-00:01')).toThrow(\n      InvalidDatetimeError,\n    )\n  })\n})\n\ndescribe(normalizeDatetimeAlways, () => {\n  it('normalizes valid input', () => {\n    expect(normalizeDatetimeAlways('1985-04-12T23:20:50Z')).toEqual(\n      '1985-04-12T23:20:50.000Z',\n    )\n  })\n\n  it('normalizes invalid input', () => {\n    expect(normalizeDatetimeAlways('blah')).toEqual('1970-01-01T00:00:00.000Z')\n    expect(normalizeDatetimeAlways('0000-01-01T00:00:00+01:00')).toEqual(\n      '1970-01-01T00:00:00.000Z',\n    )\n  })\n\n  describe('valid interop', () => {\n    for (const dt of interopValid) {\n      test(dt, () => {\n        // @NOTE we can't test the returned value as some will normalize while others won't.\n        expect(() => normalizeDatetimeAlways(dt)).not.toThrow()\n      })\n    }\n  })\n\n  describe('succeeds on interop (invalid syntax)', () => {\n    for (const dt of interopInvalidSyntax) {\n      test(dt, () => {\n        // @NOTE we can't test the returned value as some will normalize while others won't.\n        expect(() => normalizeDatetimeAlways(dt)).not.toThrow()\n      })\n    }\n  })\n\n  describe('succeeds on interop invalid parse', () => {\n    for (const dt of interopInvalidParse) {\n      test(dt, () => {\n        expect(normalizeDatetimeAlways(dt)).toEqual('1970-01-01T00:00:00.000Z')\n      })\n    }\n  })\n})\n\nfunction readLines(filePath: string): string[] {\n  return fs\n    .readFileSync(filePath, 'utf-8')\n    .split(/\\r?\\n/)\n    .filter((line) => !line.startsWith('#') && line.length > 0)\n}\n"
  },
  {
    "path": "packages/syntax/tests/did.test.ts",
    "content": "import * as fs from 'node:fs'\nimport * as readline from 'node:readline'\nimport { describe, expect, it } from 'vitest'\nimport { InvalidDidError, ensureValidDid, ensureValidDidRegex } from '../src'\n\ndescribe('DID permissive validation', () => {\n  const expectValid = (h: string) => {\n    ensureValidDid(h)\n    ensureValidDidRegex(h)\n  }\n  const expectInvalid = (h: string) => {\n    expect(() => ensureValidDid(h)).toThrow(InvalidDidError)\n    expect(() => ensureValidDidRegex(h)).toThrow(InvalidDidError)\n  }\n\n  it('enforces spec details', () => {\n    expectValid('did:method:val')\n    expectValid('did:method:VAL')\n    expectValid('did:method:val123')\n    expectValid('did:method:123')\n    expectValid('did:method:val-two')\n    expectValid('did:method:val_two')\n    expectValid('did:method:val.two')\n    expectValid('did:method:val:two')\n    expectValid('did:method:val%BB')\n\n    expectInvalid('did')\n    expectInvalid('didmethodval')\n    expectInvalid('method:did:val')\n    expectInvalid('did:method:')\n    expectInvalid('didmethod:val')\n    expectInvalid('did:methodval')\n    expectInvalid(':did:method:val')\n    expectInvalid('did.method.val')\n    expectInvalid('did:method:val:')\n    expectInvalid('did:method:val%')\n    expectInvalid('DID:method:val')\n    expectInvalid('did:METHOD:val')\n    expectInvalid('did:m123:val')\n\n    expectValid('did:method:' + 'v'.repeat(240))\n    expectInvalid('did:method:' + 'v'.repeat(8500))\n\n    expectValid('did:m:v')\n    expectValid('did:method::::val')\n    expectValid('did:method:-')\n    expectValid('did:method:-:_:.:%ab')\n    expectValid('did:method:.')\n    expectValid('did:method:_')\n    expectValid('did:method::.')\n\n    expectInvalid('did:method:val/two')\n    expectInvalid('did:method:val?two')\n    expectInvalid('did:method:val#two')\n    expectInvalid('did:method:val%')\n\n    expectValid(\n      'did:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid',\n    )\n  })\n\n  it('allows some real DID values', () => {\n    expectValid('did:example:123456789abcdefghi')\n    expectValid('did:plc:7iza6de2dwap2sbkpav7c6c6')\n    expectValid('did:web:example.com')\n    expectValid('did:web:localhost%3A1234')\n    expectValid('did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N')\n    expectValid('did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a')\n  })\n\n  it('conforms to interop valid DIDs', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/did_syntax_valid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectValid(line)\n    })\n  })\n\n  it('conforms to interop invalid DIDs', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/did_syntax_invalid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectInvalid(line)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/syntax/tests/handle.test.ts",
    "content": "import * as fs from 'node:fs'\nimport * as readline from 'node:readline'\nimport { describe, expect, it } from 'vitest'\nimport {\n  InvalidHandleError,\n  ensureValidHandle,\n  ensureValidHandleRegex,\n  normalizeAndEnsureValidHandle,\n} from '../src'\n\ndescribe('handle validation', () => {\n  const expectValid = (h: string) => {\n    ensureValidHandle(h)\n    ensureValidHandleRegex(h)\n  }\n  const expectInvalid = (h: string) => {\n    expect(() => ensureValidHandle(h)).toThrow(InvalidHandleError)\n    expect(() => ensureValidHandleRegex(h)).toThrow(InvalidHandleError)\n  }\n\n  it('allows valid handles', () => {\n    expectValid('A.ISI.EDU')\n    expectValid('XX.LCS.MIT.EDU')\n    expectValid('SRI-NIC.ARPA')\n    expectValid('john.test')\n    expectValid('jan.test')\n    expectValid('a234567890123456789.test')\n    expectValid('john2.test')\n    expectValid('john-john.test')\n    expectValid('john.bsky.app')\n    expectValid('jo.hn')\n    expectValid('a.co')\n    expectValid('a.org')\n    expectValid('joh.n')\n    expectValid('j0.h0')\n    const longHandle =\n      'shoooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test'\n    expect(longHandle.length).toEqual(253)\n    expectValid(longHandle)\n    expectValid('short.' + 'o'.repeat(63) + '.test')\n    expectValid('jaymome-johnber123456.test')\n    expectValid('jay.mome-johnber123456.test')\n    expectValid('john.test.bsky.app')\n\n    // NOTE: this probably isn't ever going to be a real domain, but my read of\n    // the RFC is that it would be possible\n    expectValid('john.t')\n  })\n\n  // NOTE: we may change this at the proto level; currently only disallowed at\n  // the registration level\n  it('allows .local and .arpa handles (proto-level)', () => {\n    expectValid('laptop.local')\n    expectValid('laptop.arpa')\n  })\n\n  it('allows punycode handles', () => {\n    expectValid('xn--ls8h.test') // 💩.test\n    expectValid('xn--bcher-kva.tld') // bücher.tld\n    expectValid('xn--3jk.com')\n    expectValid('xn--w3d.com')\n    expectValid('xn--vqb.com')\n    expectValid('xn--ppd.com')\n    expectValid('xn--cs9a.com')\n    expectValid('xn--8r9a.com')\n    expectValid('xn--cfd.com')\n    expectValid('xn--5jk.com')\n    expectValid('xn--2lb.com')\n  })\n\n  it('allows onion (Tor) handles', () => {\n    expectValid('expyuzz4wqqyqhjn.onion')\n    expectValid('friend.expyuzz4wqqyqhjn.onion')\n    expectValid(\n      'g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',\n    )\n    expectValid(\n      'friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',\n    )\n    expectValid(\n      'friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',\n    )\n    expectValid(\n      '2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',\n    )\n    expectValid(\n      'friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion',\n    )\n  })\n\n  it('throws on invalid handles', () => {\n    expectInvalid('did:thing.test')\n    expectInvalid('did:thing')\n    expectInvalid('john-.test')\n    expectInvalid('john.0')\n    expectInvalid('john.-')\n    expectInvalid('short.' + 'o'.repeat(64) + '.test')\n    expectInvalid('short' + '.loooooooooooooooooooooooong'.repeat(10) + '.test')\n    const longHandle =\n      'shooooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test'\n    expect(longHandle.length).toEqual(254)\n    expectInvalid(longHandle)\n    expectInvalid('xn--bcher-.tld')\n    expectInvalid('john..test')\n    expectInvalid('jo_hn.test')\n    expectInvalid('-john.test')\n    expectInvalid('.john.test')\n    expectInvalid('jo!hn.test')\n    expectInvalid('jo%hn.test')\n    expectInvalid('jo&hn.test')\n    expectInvalid('jo@hn.test')\n    expectInvalid('jo*hn.test')\n    expectInvalid('jo|hn.test')\n    expectInvalid('jo:hn.test')\n    expectInvalid('jo/hn.test')\n    expectInvalid('john💩.test')\n    expectInvalid('bücher.test')\n    expectInvalid('john .test')\n    expectInvalid('john.test.')\n    expectInvalid('john')\n    expectInvalid('john.')\n    expectInvalid('.john')\n    expectInvalid('john.test.')\n    expectInvalid('.john.test')\n    expectInvalid(' john.test')\n    expectInvalid('john.test ')\n    expectInvalid('joh-.test')\n    expectInvalid('john.-est')\n    expectInvalid('john.tes-')\n  })\n\n  it('throws on \"dotless\" TLD handles', () => {\n    expectInvalid('org')\n    expectInvalid('ai')\n    expectInvalid('gg')\n    expectInvalid('io')\n  })\n\n  it('correctly validates corner cases (modern vs. old RFCs)', () => {\n    expectValid('12345.test')\n    expectValid('8.cn')\n    expectValid('4chan.org')\n    expectValid('4chan.o-g')\n    expectValid('blah.4chan.org')\n    expectValid('thing.a01')\n    expectValid('120.0.0.1.com')\n    expectValid('0john.test')\n    expectValid('9sta--ck.com')\n    expectValid('99stack.com')\n    expectValid('0ohn.test')\n    expectValid('john.t--t')\n    expectValid('thing.0aa.thing')\n\n    expectInvalid('cn.8')\n    expectInvalid('thing.0aa')\n    expectInvalid('thing.0aa')\n  })\n\n  it('does not allow IP addresses as handles', () => {\n    expectInvalid('127.0.0.1')\n    expectInvalid('192.168.0.142')\n    expectInvalid('fe80::7325:8a97:c100:94b')\n    expectInvalid('2600:3c03::f03c:9100:feb0:af1f')\n  })\n\n  it('is consistent with examples from stackoverflow', () => {\n    const okStackoverflow = [\n      'stack.com',\n      'sta-ck.com',\n      'sta---ck.com',\n      'sta--ck9.com',\n      'stack99.com',\n      'sta99ck.com',\n      'google.com.uk',\n      'google.co.in',\n      'google.com',\n      'maselkowski.pl',\n      'm.maselkowski.pl',\n      'xn--masekowski-d0b.pl',\n      'xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s',\n      'xn--stackoverflow.com',\n      'stackoverflow.xn--com',\n      'stackoverflow.co.uk',\n      'xn--masekowski-d0b.pl',\n      'xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s',\n    ]\n    okStackoverflow.forEach(expectValid)\n\n    const badStackoverflow = [\n      '-notvalid.at-all',\n      '-thing.com',\n      'www.masełkowski.pl.com',\n    ]\n    badStackoverflow.forEach(expectInvalid)\n  })\n\n  it('conforms to interop valid handles', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/handle_syntax_valid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectValid(line)\n    })\n  })\n\n  it('conforms to interop invalid handles', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/handle_syntax_invalid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectInvalid(line)\n    })\n  })\n})\n\ndescribe('normalization', () => {\n  it('normalizes handles', () => {\n    const normalized = normalizeAndEnsureValidHandle('JoHn.TeST')\n    expect(normalized).toBe('john.test')\n  })\n\n  it('throws on invalid normalized handles', () => {\n    expect(() => normalizeAndEnsureValidHandle('JoH!n.TeST')).toThrow(\n      InvalidHandleError,\n    )\n  })\n})\n"
  },
  {
    "path": "packages/syntax/tests/language.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { isValidLanguage, parseLanguageString } from '../src'\n\ndescribe(isValidLanguage, () => {\n  it('validates BCP 47', () => {\n    // valid\n    expect(isValidLanguage('de')).toEqual(true)\n    expect(isValidLanguage('de-CH')).toEqual(true)\n    expect(isValidLanguage('de-DE-1901')).toEqual(true)\n    expect(isValidLanguage('es-419')).toEqual(true)\n    expect(isValidLanguage('sl-IT-nedis')).toEqual(true)\n    expect(isValidLanguage('mn-Cyrl-MN')).toEqual(true)\n    expect(isValidLanguage('x-fr-CH')).toEqual(true)\n    expect(\n      isValidLanguage('en-GB-boont-r-extended-sequence-x-private'),\n    ).toEqual(true)\n    expect(isValidLanguage('sr-Cyrl')).toEqual(true)\n    expect(isValidLanguage('hy-Latn-IT-arevela')).toEqual(true)\n    expect(isValidLanguage('i-klingon')).toEqual(true)\n    // invalid\n    expect(isValidLanguage('')).toEqual(false)\n    expect(isValidLanguage('x')).toEqual(false)\n    expect(isValidLanguage('de-CH-')).toEqual(false)\n    expect(isValidLanguage('i-bad-grandfathered')).toEqual(false)\n  })\n})\n\ndescribe(parseLanguageString, () => {\n  it('parses BCP 47', () => {\n    // valid\n    expect(parseLanguageString('de')).toEqual({\n      language: 'de',\n    })\n    expect(parseLanguageString('de-CH')).toEqual({\n      language: 'de',\n      region: 'CH',\n    })\n    expect(parseLanguageString('de-DE-1901')).toEqual({\n      language: 'de',\n      region: 'DE',\n      variant: '1901',\n    })\n    expect(parseLanguageString('es-419')).toEqual({\n      language: 'es',\n      region: '419',\n    })\n    expect(parseLanguageString('sl-IT-nedis')).toEqual({\n      language: 'sl',\n      region: 'IT',\n      variant: 'nedis',\n    })\n    expect(parseLanguageString('mn-Cyrl-MN')).toEqual({\n      language: 'mn',\n      script: 'Cyrl',\n      region: 'MN',\n    })\n    expect(parseLanguageString('x-fr-CH')).toEqual({\n      privateUse: 'x-fr-CH',\n    })\n    expect(\n      parseLanguageString('en-GB-boont-r-extended-sequence-x-private'),\n    ).toEqual({\n      language: 'en',\n      region: 'GB',\n      variant: 'boont',\n      extension: 'r-extended-sequence',\n      privateUse: 'x-private',\n    })\n    expect(parseLanguageString('sr-Cyrl')).toEqual({\n      language: 'sr',\n      script: 'Cyrl',\n    })\n    expect(parseLanguageString('hy-Latn-IT-arevela')).toEqual({\n      language: 'hy',\n      script: 'Latn',\n      region: 'IT',\n      variant: 'arevela',\n    })\n    expect(parseLanguageString('i-klingon')).toEqual({\n      grandfathered: 'i-klingon',\n    })\n    // invalid\n    expect(parseLanguageString('')).toEqual(null)\n    expect(parseLanguageString('x')).toEqual(null)\n    expect(parseLanguageString('de-CH-')).toEqual(null)\n    expect(parseLanguageString('i-bad-grandfathered')).toEqual(null)\n  })\n})\n"
  },
  {
    "path": "packages/syntax/tests/nsid.test.ts",
    "content": "import * as fs from 'node:fs'\nimport { describe, expect, it } from 'vitest'\nimport {\n  InvalidNsidError,\n  NSID,\n  ensureValidNsid,\n  isValidNsid,\n  parseNsid,\n  validateNsid,\n  validateNsidRegex,\n} from '../src'\n\ndescribe('NSID parsing & creation', () => {\n  it('parses valid NSIDs', () => {\n    expect(NSID.parse('com.example.foo').authority).toBe('example.com')\n    expect(NSID.parse('com.example.foo').name).toBe('foo')\n    expect(NSID.parse('com.example.foo').toString()).toBe('com.example.foo')\n    expect(NSID.parse('com.long-thing1.cool.fooBarBaz').authority).toBe(\n      'cool.long-thing1.com',\n    )\n    expect(NSID.parse('com.long-thing1.cool.fooBarBaz').name).toBe('fooBarBaz')\n    expect(NSID.parse('com.long-thing1.cool.fooBarBaz').toString()).toBe(\n      'com.long-thing1.cool.fooBarBaz',\n    )\n  })\n\n  it('creates valid NSIDs', () => {\n    expect(NSID.create('example.com', 'foo').authority).toBe('example.com')\n    expect(NSID.create('example.com', 'foo').name).toBe('foo')\n    expect(NSID.create('example.com', 'foo').toString()).toBe('com.example.foo')\n    expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').authority).toBe(\n      'cool.long-thing1.com',\n    )\n    expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').name).toBe(\n      'fooBarBaz',\n    )\n    expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').toString()).toBe(\n      'com.long-thing1.cool.fooBarBaz',\n    )\n  })\n})\n\ndescribe('NSID validation', () => {\n  const expectValid = (h: string) => {\n    expect(isValidNsid(h)).toBe(true)\n    ensureValidNsid(h)\n    expect(parseNsid(h)).toEqual(h.split('.'))\n    expect(validateNsidRegex(h)).toMatchObject({\n      success: true,\n      value: expect.any(String),\n    })\n    expect(validateNsid(h)).toMatchObject({\n      success: true,\n      value: expect.any(Array),\n    })\n  }\n  const expectInvalid = (h: string) => {\n    expect(isValidNsid(h)).toBe(false)\n    expect(() => parseNsid(h)).toThrow(InvalidNsidError)\n    expect(() => ensureValidNsid(h)).toThrow(InvalidNsidError)\n    expect(validateNsidRegex(h)).toMatchObject({\n      success: false,\n      message: expect.any(String),\n    })\n    expect(validateNsid(h)).toMatchObject({\n      success: false,\n      message: expect.any(String),\n    })\n  }\n\n  it('enforces spec details', () => {\n    expectValid('com.example.foo')\n    const longNsid = 'com.' + 'o'.repeat(63) + '.foo'\n    expectValid(longNsid)\n\n    const tooLongNsid = 'com.' + 'o'.repeat(64) + '.foo'\n    expectInvalid(tooLongNsid)\n\n    const longEnd = 'com.example.' + 'o'.repeat(63)\n    expectValid(longEnd)\n\n    const tooLongEnd = 'com.example.' + 'o'.repeat(64)\n    expectInvalid(tooLongEnd)\n\n    const longOverall = 'com.' + 'middle.'.repeat(40) + 'foo'\n    expect(longOverall.length).toBe(287)\n    expectValid(longOverall)\n\n    const tooLongOverall = 'com.' + 'middle.'.repeat(50) + 'foo'\n    expect(tooLongOverall.length).toBe(357)\n    expectInvalid(tooLongOverall)\n  })\n\n  describe('valid NSIDs', () => {\n    for (const validNsid of [\n      'com.example.foo',\n      'o'.repeat(63) + '.foo.bar',\n      'com.' + 'o'.repeat(63) + '.foo',\n      'com.example.' + 'o'.repeat(63),\n      'com.' + 'middle.'.repeat(40) + 'foo',\n\n      'a-0.b-1.c',\n      'a.0.c',\n      'a.b.c',\n      'a0.b1.c3',\n      'a0.b1.cc',\n      'a01.thing.record',\n      'cn.8.lex.stuff',\n      'com.example.f00',\n      'com.example.fooBar',\n      'm.xn--masekowski-d0b.pl',\n      'net.users.bob.ping',\n      'one.2.three',\n      'one.two.three',\n      'one.two.three.four-and.FiVe',\n      'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',\n      'onion.expyuzz4wqqyqhjn.spec.getThing',\n      'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',\n      'org.4chan.lex.getThing',\n      'test.12345.record',\n      'xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two',\n    ]) {\n      it(validNsid, () => {\n        expectValid(validNsid)\n      })\n    }\n  })\n\n  describe('invalid NSIDs', () => {\n    for (const invalidNsid of [\n      'o'.repeat(64) + '.foo.bar',\n      'com.' + 'o'.repeat(64) + '.foo',\n      'com.example.' + 'o'.repeat(64),\n      'com.' + 'middle.'.repeat(50) + 'foo',\n      'com.example.foo.*',\n      'com.example.foo.blah*',\n      'com.example.foo.*blah',\n      'com.exa💩ple.thing',\n      'a-0.b-1.c-3',\n      'a-0.b-1.c-o',\n      '1.0.0.127.record',\n      '0two.example.foo',\n      'example.com',\n      'com.example',\n      'a.',\n      '.one.two.three',\n      'one.two.three ',\n      'one.two..three',\n      'one .two.three',\n      ' one.two.three',\n      'com.atproto.feed.p@st',\n      'com.atproto.feed.p_st',\n      'com.atproto.feed.p*st',\n      'com.atproto.feed.po#t',\n      'com.atproto.feed.p!ot',\n      'com.example-.foo',\n      'com.-example.foo',\n      'com.example.0foo',\n      'com.example.f-o',\n    ]) {\n      it(invalidNsid, () => {\n        expect(validateNsid(invalidNsid)).toMatchObject({\n          success: false,\n          message: expect.any(String),\n        })\n      })\n    }\n  })\n\n  describe('conforms to interop valid NSIDs', () => {\n    for (const line of fs\n      .readFileSync(`${__dirname}/interop-files/nsid_syntax_valid.txt`)\n      .toString()\n      .split('\\n')) {\n      if (line.startsWith('#') || line.length === 0) {\n        continue\n      }\n\n      it(line, () => {\n        expectValid(line)\n      })\n    }\n  })\n\n  describe('conforms to interop invalid NSIDs', () => {\n    for (const line of fs\n      .readFileSync(`${__dirname}/interop-files/nsid_syntax_invalid.txt`)\n      .toString()\n      .split('\\n')) {\n      if (line.startsWith('#') || line.length === 0) {\n        continue\n      }\n\n      it(line, () => {\n        expectInvalid(line)\n      })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/syntax/tests/recordkey.test.ts",
    "content": "import * as fs from 'node:fs'\nimport * as readline from 'node:readline'\nimport { describe, expect, it } from 'vitest'\nimport { InvalidRecordKeyError, ensureValidRecordKey } from '../src'\n\ndescribe('recordkey validation', () => {\n  const expectValid = (r: string) => {\n    ensureValidRecordKey(r)\n  }\n  const expectInvalid = (r: string) => {\n    expect(() => ensureValidRecordKey(r)).toThrow(InvalidRecordKeyError)\n  }\n\n  it('conforms to interop valid recordkey', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/recordkey_syntax_valid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectValid(line)\n    })\n  })\n\n  it('conforms to interop invalid recordkeys', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/recordkey_syntax_invalid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectInvalid(line)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/syntax/tests/tid.test.ts",
    "content": "import * as fs from 'node:fs'\nimport * as readline from 'node:readline'\nimport { describe, expect, it } from 'vitest'\nimport { InvalidTidError, ensureValidTid } from '../src'\n\ndescribe('tid validation', () => {\n  const expectValid = (t: string) => {\n    ensureValidTid(t)\n  }\n  const expectInvalid = (t: string) => {\n    expect(() => ensureValidTid(t)).toThrow(InvalidTidError)\n  }\n\n  it('conforms to interop valid tid', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/tid_syntax_valid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectValid(line)\n    })\n  })\n\n  it('conforms to interop invalid tids', () => {\n    const lineReader = readline.createInterface({\n      input: fs.createReadStream(\n        `${__dirname}/interop-files/tid_syntax_invalid.txt`,\n      ),\n      terminal: false,\n    })\n    lineReader.on('line', (line) => {\n      if (line.startsWith('#') || line.length === 0) {\n        return\n      }\n      expectInvalid(line)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/syntax/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"importHelpers\": true,\n    \"target\": \"ES2023\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/syntax/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/syntax/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/syntax/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/tap/CHANGELOG.md",
    "content": "# @atproto/tap\n\n## 0.2.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex@0.0.22\n  - @atproto/common@0.5.15\n\n## 0.2.9\n\n### Patch Changes\n\n- Updated dependencies [[`67eb0c1`](https://github.com/bluesky-social/atproto/commit/67eb0c19ac415e762e221b2ccda9f0bcf7b3dd6f)]:\n  - @atproto/syntax@0.5.1\n  - @atproto/lex@0.0.21\n\n## 0.2.8\n\n### Patch Changes\n\n- Updated dependencies [[`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd)]:\n  - @atproto/syntax@0.5.0\n  - @atproto/lex@0.0.20\n  - @atproto/common@0.5.14\n\n## 0.2.7\n\n### Patch Changes\n\n- Updated dependencies [[`38852f0`](https://github.com/bluesky-social/atproto/commit/38852f0ddfa9fbce8036233dc6af87614e9ae4b2)]:\n  - @atproto/lex@0.0.19\n\n## 0.2.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.5.13\n  - @atproto/lex@0.0.18\n\n## 0.2.5\n\n### Patch Changes\n\n- Updated dependencies [[`009c4af`](https://github.com/bluesky-social/atproto/commit/009c4afd3643d4edf4bd05065ec93cab74610bfe)]:\n  - @atproto/lex@0.0.17\n  - @atproto/common@0.5.12\n\n## 0.2.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex@0.0.16\n\n## 0.2.3\n\n### Patch Changes\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex@0.0.15\n  - @atproto/common@0.5.11\n\n## 0.2.2\n\n### Patch Changes\n\n- Updated dependencies [[`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a), [`49b3806`](https://github.com/bluesky-social/atproto/commit/49b38069ed4b5bd1ef71e967c78e5123b1c1f6f1)]:\n  - @atproto/lex@0.0.14\n  - @atproto/common@0.5.10\n\n## 0.2.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lex@0.0.13\n\n## 0.2.0\n\n### Minor Changes\n\n- [#4532](https://github.com/bluesky-social/atproto/pull/4532) [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `record` data as parsed atproto data (including CIDs and Uint8Arrays)\n\n### Patch Changes\n\n- [#4532](https://github.com/bluesky-social/atproto/pull/4532) [`aaedafc`](https://github.com/bluesky-social/atproto/commit/aaedafc6baef106b85e0954d8474cec21c00c1c2) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace event validation from \"zod\" to \"@atproto/lex\"\n\n- [#4562](https://github.com/bluesky-social/atproto/pull/4562) [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add validation error as cause when handling invalid records\n\n- Updated dependencies [[`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b)]:\n  - @atproto/syntax@0.4.3\n  - @atproto/lex@0.0.12\n  - @atproto/common@0.5.9\n\n## 0.1.3\n\n### Patch Changes\n\n- [#4534](https://github.com/bluesky-social/atproto/pull/4534) [`0193467`](https://github.com/bluesky-social/atproto/commit/0193467463a1a49c0c7f17d129c07b687a1f5057) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make sure the `destroy()` promise resolves, even in case of error\n\n- [#4534](https://github.com/bluesky-social/atproto/pull/4534) [`0193467`](https://github.com/bluesky-social/atproto/commit/0193467463a1a49c0c7f17d129c07b687a1f5057) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `TapChannel` implement `AsyncDisposable`, allowing use with `using` keyword\n\n- Updated dependencies []:\n  - @atproto/common@0.5.8\n  - @atproto/lex@0.0.11\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.5.7\n  - @atproto/lex@0.0.10\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex@0.0.9\n  - @atproto/common@0.5.6\n\n## 0.1.0\n\n### Minor Changes\n\n- [#4483](https://github.com/bluesky-social/atproto/pull/4483) [`e3357e9`](https://github.com/bluesky-social/atproto/commit/e3357e9c781ff84430556f4c891639acfcef3486) Thanks [@dholms](https://github.com/dholms)! - Add LexIndexer\n\n## 0.0.3\n\n### Patch Changes\n\n- [#4481](https://github.com/bluesky-social/atproto/pull/4481) [`e15e7cf`](https://github.com/bluesky-social/atproto/commit/e15e7cf1a07d6a4bdd7a9a3c591690613f5414b5) Thanks [@dholms](https://github.com/dholms)! - Fix URL formatting with trailing slash\n\n- Updated dependencies []:\n  - @atproto/common@0.5.4\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4290](https://github.com/bluesky-social/atproto/pull/4290) [`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957) Thanks [@dholms](https://github.com/dholms)! - Initial version of package\n\n- Updated dependencies [[`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957)]:\n  - @atproto/ws-client@0.0.4\n"
  },
  {
    "path": "packages/tap/README.md",
    "content": "# @atproto/tap\n\nTypeScript client library for [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap/README.md), a sync utility for the AT Protocol network.\n\nTap handles firehose connections, cryptographic verification, backfill, and filtering. This client library lets you connect to a Tap instance and receive simple JSON events for the repos you care about.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/tap)](https://www.npmjs.com/package/@atproto/tap)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Quick Start\n\n```bash\nnpm install @atproto/tap\n```\n\n```ts\nimport { Tap, SimpleIndexer } from '@atproto/tap'\n\nconst tap = new Tap('http://localhost:2480', { adminPassword: 'secret' })\n\nconst indexer = new SimpleIndexer()\n\nindexer.identity(async (evt) => {\n  console.log(`${evt.did} updated identity: ${evt.handle} (${evt.status})`)\n})\n\nindexer.record(async (evt) => {\n  const uri = `at://${evt.did}/${evt.collection}/${evt.rkey}`\n  if (evt.action === 'create' || evt.action === 'update') {\n    console.log(`${evt.action}: ${uri}`)\n  } else {\n    console.log(`deleted: ${uri}`)\n  }\n})\n\nindexer.error((err) => console.error(err))\n\nconst channel = tap.channel(indexer)\nchannel.start()\n\nawait tap.addRepos(['did:plc:ewvi7nxzyoun6zhxrhs64oiz'])\n\n// On shutdown\nawait channel.destroy()\n```\n\n## Running Tap\n\nSee the [Tap README](https://github.com/bluesky-social/indigo/tree/main/cmd/tap/README.md) for details getting Tap up and running. Your app can communicate with it either locally or over the internet.\n\nThis library is intended to be used with Tap running in the default mode of \"WebScoket with acks\". In this mode, Tap provides:\n\n- **At-least-once delivery**: Events may be redelivered if the connection drops before an ack is received\n- **Per-repo ordering**: Events for the same repo are delivered in order\n- **Backfill**: When you add a repo, historical events are delivered before live events\n\n## API\n\n### `Tap`\n\nThe main client for interacting with a Tap server.\n\n```ts\nconst tap = new Tap(url: string, config?: TapConfig)\n```\n\n**Config options:**\n\n- `adminPassword?: string` - Password for Basic auth (required if Tap server has auth enabled)\n\n**Methods:**\n\n- `channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel` - Create a WebSocket channel to receive events\n- `addRepos(dids: string[]): Promise<void>` - Add repos to track (triggers backfill)\n- `removeRepos(dids: string[]): Promise<void>` - Stop tracking repos\n- `resolveDid(did: string): Promise<DidDocument | null>` - Resolve a DID to its DID document\n- `getRepoInfo(did: string): Promise<RepoInfo>` - Get info about a tracked repo\n\n### `TapChannel`\n\nWebSocket connection for receiving events. Created via `tap.channel()`.\n\n```ts\nconst channel = tap.channel(handler, opts?)\n```\n\n**Methods:**\n\n- `start(): Promise<void>` - Start receiving events. Returns a promise that resolves when the connection is destroyed or errors.\n- `destroy(): Promise<void>` - Close the connection\n\nThe channel automatically handles reconnection and keepalive. Events are automatically acknowledged after your handler completes successfully.\n\n### `SimpleIndexer`\n\nA convenience class for handling events by type. Passed into `tap.channel()` when opening a channel with Tap.\n\n```ts\nconst indexer = new SimpleIndexer()\n\nindexer.identity(async (evt: IdentityEvent) => { ... })\nindexer.record(async (evt: RecordEvent) => { ... })\nindexer.error((err: Error) => { ... })\n```\n\nIf no error handler is registered, errors will throw as unhandled exceptions.\n\n### `LexIndexer`\n\nA typed indexer that uses `@atproto/lex` schemas for type-safe event handling. Register handlers for specific record types and actions:\n\n```ts\nimport { LexIndexer } from '@atproto/tap'\nimport * as com from './lexicons/com'\n\nconst indexer = new LexIndexer()\n\n// Handle creates for a specific record type\nindexer.create(com.example.post, async (evt) => {\n  // evt.record is fully typed as com.example.post.Main\n  console.log(`New post: ${evt.record.text}`)\n})\n\n// Handle updates\nindexer.update(com.example.post, async (evt) => {\n  console.log(`Updated post: ${evt.record.text}`)\n})\n\n// Handle deletes (no record on delete events)\nindexer.delete(com.example.post, async (evt) => {\n  console.log(`Deleted: at://${evt.did}/${evt.collection}/${evt.rkey}`)\n})\n\n// Handle both creates and updates with put()\nindexer.put(com.example.like, async (evt) => {\n  console.log(`Like ${evt.action}: ${evt.record.subject.uri}`)\n})\n\n// Fallback for unhandled record types/actions\nindexer.other(async (evt) => {\n  console.log(`Unhandled: ${evt.action}, ${evt.collection}`)\n})\n\n// Identity and error handlers\nindexer.identity(async (evt) => { ... })\nindexer.error((err) => { ... })\n\nconst channel = tap.channel(indexer)\n```\n\nRecords are validated against their schemas before handlers are called. If validation fails, an error is thrown which will be picked up through the `error` handler..\n\nDuplicate handler registration throws an error, including conflicts between `put()` and `create()`/`update()` for the same schema.\n\n### `TapHandler`\n\nYou can create your own custom handler by creating a class that implements the `TapHandler` interface:\n\n```ts\ninterface TapHandler {\n  onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n  onError: (err: Error) => void\n}\n\ninterface HandlerOpts {\n  signal: AbortSignal\n  ack: () => Promise<void>\n}\n```\n\nWhen implementing a custom handler, be sure to call `ack()` when you're done processing the event.\n\n## Event Types\n\n### `RecordEvent`\n\n```ts\ntype RecordEvent = {\n  id: number\n  type: 'record'\n  action: 'create' | 'update' | 'delete'\n  did: string\n  rev: string\n  collection: string\n  rkey: string\n  record?: Record<string, unknown> // present for create/update\n  cid?: string // present for create/update\n  live: boolean // true if from firehose, false if from backfill\n}\n```\n\n### `IdentityEvent`\n\n```ts\ntype IdentityEvent = {\n  id: number\n  type: 'identity'\n  did: string\n  handle: string\n  isActive: boolean\n  status: 'active' | 'takendown' | 'suspended' | 'deactivated' | 'deleted'\n}\n```\n\n## Webhook Mode\n\nIf your Tap server is configured for webhook delivery, you can use `parseTapEvent` to validate incoming webhook payloads:\n\n```ts\nimport express from 'express'\nimport { parseTapEvent, assureAdminAuth } from '@atproto/tap'\n\nconst ADMIN_PASSWORD = process.env.ADMIN_PASSWORD\n\nconst app = express()\napp.use(express.json())\n\napp.post('/webhook', async (req, res) => {\n  try {\n    assureAdminAuth(ADMIN_PASSWORD, req.headers.authorization)\n  } catch {\n    return res.status(401).json({ error: 'Unauthorized' })\n  }\n\n  try {\n    const evt = parseTapEvent(req.body)\n    // handle event...\n    res.sendStatus(200)\n  } catch (err) {\n    console.error('Failed to process event:', err)\n    res.status(500).json({ error: 'Failed to process event' })\n  }\n})\n```\n\n## Utilities\n\n### Auth helpers\n\n```ts\nimport {\n  formatAdminAuthHeader,\n  parseAdminAuthHeader,\n  assureAdminAuth,\n} from '@atproto/tap'\n\n// Format a password into a Basic auth header value\nconst header = formatAdminAuthHeader('secret')\n// => 'Basic YWRtaW46c2VjcmV0'\n\n// Parse an auth header to extract the password (throws if invalid)\nconst password = parseAdminAuthHeader(header)\n\n// Verify auth header matches expected password (timing-safe, throws if invalid)\nassureAdminAuth('secret', req.headers.authorization)\n```\n\n### Event parsing\n\n```ts\nimport { parseTapEvent } from '@atproto/tap'\n\nconst evt = parseTapEvent(jsonData) // validates and returns typed TapEvent\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/tap/package.json",
    "content": "{\n  \"name\": \"@atproto/tap\",\n  \"version\": \"0.2.10\",\n  \"license\": \"MIT\",\n  \"description\": \"atproto tap client\",\n  \"keywords\": [\n    \"atproto\",\n    \"tap\",\n    \"sync\",\n    \"firehose\",\n    \"relay\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/tap\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/lex\": \"workspace:^\",\n    \"@atproto/syntax\": \"workspace:^\",\n    \"@atproto/ws-client\": \"workspace:^\",\n    \"ws\": \"^8.12.0\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.17\",\n    \"@types/ws\": \"^8.5.4\",\n    \"express\": \"^4.18.2\",\n    \"typescript\": \"^5.6.3\",\n    \"@vitest/coverage-v8\": \"4.0.16\",\n    \"vitest\": \"^4.0.16\"\n  }\n}\n"
  },
  {
    "path": "packages/tap/src/channel.ts",
    "content": "import { ClientOptions } from 'ws'\nimport { Deferrable, createDeferrable } from '@atproto/common'\nimport { lexParse } from '@atproto/lex'\nimport { WebSocketKeepAlive } from '@atproto/ws-client'\nimport { TapEvent, parseTapEvent } from './types'\nimport { formatAdminAuthHeader, isCausedBySignal } from './util'\n\nexport interface HandlerOpts {\n  signal: AbortSignal\n  ack: () => Promise<void>\n}\n\nexport interface TapHandler {\n  onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n  onError: (err: Error) => void\n}\n\nexport type TapWebsocketOptions = ClientOptions & {\n  adminPassword?: string\n  maxReconnectSeconds?: number\n  heartbeatIntervalMs?: number\n  onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void\n}\n\ntype BufferedAck = {\n  id: number\n  defer: Deferrable\n}\n\nexport class TapChannel implements AsyncDisposable {\n  private ws: WebSocketKeepAlive\n  private handler: TapHandler\n\n  private readonly abortController: AbortController = new AbortController()\n  private readonly destroyDefer: Deferrable = createDeferrable()\n\n  private bufferedAcks: BufferedAck[] = []\n\n  constructor(\n    url: string,\n    handler: TapHandler,\n    wsOpts: TapWebsocketOptions = {},\n  ) {\n    this.handler = handler\n    const { adminPassword, ...rest } = wsOpts\n    let headers = rest.headers\n    if (adminPassword) {\n      headers ??= {}\n      headers['Authorization'] = formatAdminAuthHeader(adminPassword)\n    }\n    this.ws = new WebSocketKeepAlive({\n      getUrl: async () => url,\n      onReconnect: () => {\n        this.flushBufferedAcks()\n      },\n      signal: this.abortController.signal,\n      ...rest,\n      headers,\n    })\n  }\n\n  async ackEvent(id: number): Promise<void> {\n    if (this.ws.isConnected()) {\n      try {\n        await this.sendAck(id)\n      } catch {\n        await this.bufferAndSendAck(id)\n      }\n    } else {\n      await this.bufferAndSendAck(id)\n    }\n  }\n\n  private async sendAck(id: number): Promise<void> {\n    await this.ws.send(JSON.stringify({ type: 'ack', id }))\n  }\n\n  // resolves after the ack has been actually sent\n  private async bufferAndSendAck(id: number): Promise<void> {\n    const defer = createDeferrable()\n    this.bufferedAcks.push({\n      id,\n      defer,\n    })\n    await defer.complete\n  }\n\n  private async flushBufferedAcks(): Promise<void> {\n    while (this.bufferedAcks.length > 0) {\n      try {\n        const ack = this.bufferedAcks.at(0)\n        if (!ack) {\n          return\n        }\n        await this.sendAck(ack.id)\n        ack.defer.resolve()\n        this.bufferedAcks = this.bufferedAcks.slice(1)\n      } catch (cause) {\n        const error = new Error(\n          `failed to send ack for event ${this.bufferedAcks[0]}`,\n          { cause },\n        )\n        this.handler.onError(error)\n        return\n      }\n    }\n  }\n\n  async start() {\n    this.abortController.signal.throwIfAborted()\n    try {\n      for await (const chunk of this.ws) {\n        await this.processWsEvent(chunk)\n      }\n    } catch (err) {\n      if (!isCausedBySignal(err, this.abortController.signal)) {\n        throw err\n      }\n    } finally {\n      this.destroyDefer.resolve()\n    }\n  }\n\n  private async processWsEvent(chunk: Uint8Array) {\n    let evt: TapEvent\n    try {\n      const data = lexParse(chunk.toString(), {\n        // Reject invalid CIDs and blobs\n        strict: true,\n      })\n      evt = parseTapEvent(data)\n    } catch (cause) {\n      const error = new Error(`Failed to parse message`, { cause })\n      this.handler.onError(error)\n      return\n    }\n\n    try {\n      await this.handler.onEvent(evt, {\n        signal: this.abortController.signal,\n        ack: async () => {\n          await this.ackEvent(evt.id)\n        },\n      })\n    } catch (cause) {\n      // Don't ack on error - let Tap retry\n      const error = new Error(`Failed to process event ${evt.id}`, { cause })\n      this.handler.onError(error)\n      return\n    }\n  }\n\n  async destroy(): Promise<void> {\n    this.abortController.abort()\n    await this.destroyDefer.complete\n  }\n\n  async [Symbol.asyncDispose](): Promise<void> {\n    await this.destroy()\n  }\n}\n"
  },
  {
    "path": "packages/tap/src/client.ts",
    "content": "import { DidDocument, didDocument } from '@atproto/common'\nimport { TapChannel, TapHandler, TapWebsocketOptions } from './channel'\nimport { RepoInfo, repoInfoSchema } from './types'\nimport { formatAdminAuthHeader } from './util'\n\nexport interface TapConfig {\n  adminPassword?: string\n}\n\nexport class Tap {\n  url: string\n  private adminPassword?: string\n  private authHeader?: string\n\n  private addReposUrl: URL\n  private removeReposUrl: URL\n\n  constructor(url: string, config: TapConfig = {}) {\n    if (!url.startsWith('http://') && !url.startsWith('https://')) {\n      throw new Error('Invalid URL, expected http:// or https://')\n    }\n    this.url = url\n    this.adminPassword = config.adminPassword\n    if (this.adminPassword) {\n      this.authHeader = formatAdminAuthHeader(this.adminPassword)\n    }\n\n    this.addReposUrl = new URL('/repos/add', this.url)\n    this.removeReposUrl = new URL('/repos/remove', this.url)\n  }\n\n  private getHeaders(): Record<string, string> {\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/json',\n    }\n    if (this.authHeader) {\n      headers['Authorization'] = this.authHeader\n    }\n    return headers\n  }\n\n  channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel {\n    const url = new URL(this.url)\n    url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'\n    url.pathname = '/channel'\n    return new TapChannel(url.toString(), handler, {\n      adminPassword: this.adminPassword,\n      ...opts,\n    })\n  }\n\n  async addRepos(dids: string[]): Promise<void> {\n    const response = await fetch(this.addReposUrl, {\n      method: 'POST',\n      headers: this.getHeaders(),\n      body: JSON.stringify({ dids }),\n    })\n    await response.body?.cancel() // expect empty body\n\n    if (!response.ok) {\n      throw new Error(`Failed to add repos: ${response.statusText}`)\n    }\n  }\n\n  async removeRepos(dids: string[]): Promise<void> {\n    const response = await fetch(this.removeReposUrl, {\n      method: 'POST',\n      headers: this.getHeaders(),\n      body: JSON.stringify({ dids }),\n    })\n    await response.body?.cancel() // expect empty body\n\n    if (!response.ok) {\n      throw new Error(`Failed to remove repos: ${response.statusText}`)\n    }\n  }\n\n  async resolveDid(did: string): Promise<DidDocument | null> {\n    const response = await fetch(new URL(`/resolve/${did}`, this.url), {\n      method: 'GET',\n      headers: this.getHeaders(),\n    })\n\n    if (response.status === 404) {\n      return null\n    } else if (!response.ok) {\n      await response.body?.cancel()\n      throw new Error(`Failed to resolve DID: ${response.statusText}`)\n    }\n    return didDocument.parse(await response.json())\n  }\n\n  async getRepoInfo(did: string): Promise<RepoInfo> {\n    const response = await fetch(new URL(`/info/${did}`, this.url), {\n      method: 'GET',\n      headers: this.getHeaders(),\n    })\n\n    if (!response.ok) {\n      await response.body?.cancel()\n      throw new Error(`Failed to get repo info: ${response.statusText}`)\n    }\n\n    return repoInfoSchema.parse(await response.json())\n  }\n}\n"
  },
  {
    "path": "packages/tap/src/index.ts",
    "content": "export * from './types'\nexport * from './client'\nexport * from './channel'\nexport * from './simple-indexer'\nexport * from './lex-indexer'\nexport * from './util'\n"
  },
  {
    "path": "packages/tap/src/lex-indexer.ts",
    "content": "import { Infer, Main, RecordSchema, getMain } from '@atproto/lex'\nimport { AtUriString, NsidString } from '@atproto/syntax'\nimport { HandlerOpts, TapHandler } from './channel'\nimport { IdentityEvent, RecordEvent, TapEvent } from './types'\n\ntype BaseRecordEvent = Omit<RecordEvent, 'record' | 'action' | 'cid'>\n\nexport type CreateEvent<R> = BaseRecordEvent & {\n  action: 'create'\n  record: R\n  cid: string\n}\n\nexport type UpdateEvent<R> = BaseRecordEvent & {\n  action: 'update'\n  record: R\n  cid: string\n}\n\nexport type PutEvent<R> = CreateEvent<R> | UpdateEvent<R>\n\nexport type DeleteEvent = BaseRecordEvent & {\n  action: 'delete'\n}\n\nexport type CreateHandler<R> = (\n  evt: CreateEvent<R>,\n  opts: HandlerOpts,\n) => Promise<void>\n\nexport type UpdateHandler<R> = (\n  evt: UpdateEvent<R>,\n  opts: HandlerOpts,\n) => Promise<void>\n\nexport type PutHandler<R> = (\n  evt: PutEvent<R>,\n  opts: HandlerOpts,\n) => Promise<void>\n\nexport type DeleteHandler = (\n  evt: DeleteEvent,\n  opts: HandlerOpts,\n) => Promise<void>\n\nexport type UntypedHandler = (\n  evt: RecordEvent,\n  opts: HandlerOpts,\n) => Promise<void>\n\nexport type IdentityHandler = (\n  evt: IdentityEvent,\n  opts: HandlerOpts,\n) => Promise<void>\n\nexport type ErrorHandler = (err: Error) => void\n\nexport type RecordHandler<R> =\n  | CreateHandler<R>\n  | UpdateHandler<R>\n  | PutHandler<R>\n  | DeleteHandler\n\ninterface RegisteredHandler {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  handler: RecordHandler<any>\n  schema: RecordSchema\n}\n\nexport class LexIndexer implements TapHandler {\n  private handlers = new Map<string, RegisteredHandler>()\n  private otherHandler: UntypedHandler | undefined\n  private identityHandler: IdentityHandler | undefined\n  private errorHandler: ErrorHandler | undefined\n\n  private handlerKey(\n    collection: NsidString,\n    action: RecordEvent['action'],\n  ): string {\n    return `${collection}:${action}`\n  }\n\n  private register<const T extends RecordSchema>(\n    action: RecordEvent['action'],\n    ns: Main<T>,\n    handler: RecordHandler<Infer<T>>,\n  ): this {\n    const schema = getMain(ns)\n    const key = this.handlerKey(schema.$type, action)\n    if (this.handlers.has(key)) {\n      throw new Error(`Handler already registered for ${key}`)\n    }\n    this.handlers.set(key, { schema, handler })\n    return this\n  }\n\n  create<const T extends RecordSchema>(\n    ns: Main<T>,\n    handler: CreateHandler<Infer<T>>,\n  ): this {\n    return this.register('create', ns, handler)\n  }\n\n  update<const T extends RecordSchema>(\n    ns: Main<T>,\n    handler: UpdateHandler<Infer<T>>,\n  ): this {\n    return this.register('update', ns, handler)\n  }\n\n  delete<const T extends RecordSchema>(\n    ns: Main<T>,\n    handler: DeleteHandler,\n  ): this {\n    return this.register('delete', ns, handler)\n  }\n\n  put<const T extends RecordSchema>(\n    ns: Main<T>,\n    handler: PutHandler<Infer<T>>,\n  ): this {\n    this.register('create', ns, handler)\n    this.register('update', ns, handler)\n    return this\n  }\n\n  other(fn: UntypedHandler): this {\n    if (this.otherHandler) {\n      throw new Error(`Handler already registered for \"other\"`)\n    }\n    this.otherHandler = fn\n    return this\n  }\n\n  identity(fn: IdentityHandler): this {\n    if (this.identityHandler) {\n      throw new Error(`Handler already registered for \"identity\"`)\n    }\n    this.identityHandler = fn\n    return this\n  }\n\n  error(fn: ErrorHandler): this {\n    if (this.errorHandler) {\n      throw new Error(`Handler already registered for \"error\"`)\n    }\n    this.errorHandler = fn\n    return this\n  }\n\n  async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {\n    if (evt.type === 'identity') {\n      await this.identityHandler?.(evt, opts)\n    } else {\n      await this.handleRecordEvent(evt, opts)\n    }\n    await opts.ack()\n  }\n\n  private async handleRecordEvent(\n    evt: RecordEvent,\n    opts: HandlerOpts,\n  ): Promise<void> {\n    const { collection, action } = evt\n    const key = this.handlerKey(collection, action)\n    const registered = this.handlers.get(key)\n\n    if (!registered) {\n      await this.otherHandler?.(evt, opts)\n      return\n    }\n\n    if (action === 'create' || action === 'update') {\n      const match = registered.schema.safeValidate(evt.record)\n      if (!match.success) {\n        const uriStr: AtUriString = `at://${evt.did}/${evt.collection}/${evt.rkey}`\n        throw new Error(`Record validation failed for ${uriStr}`, {\n          cause: match.reason,\n        })\n      }\n    }\n\n    await (registered.handler as UntypedHandler)(evt, opts)\n  }\n\n  onError(err: Error): void {\n    if (this.errorHandler) {\n      this.errorHandler(err)\n    } else {\n      throw err\n    }\n  }\n}\n"
  },
  {
    "path": "packages/tap/src/simple-indexer.ts",
    "content": "import { HandlerOpts, TapHandler } from './channel'\nimport { IdentityEvent, RecordEvent, TapEvent } from './types'\n\ntype IdentityEventHandler = (\n  evt: IdentityEvent,\n  opts?: HandlerOpts,\n) => Promise<void>\n\ntype RecordEventHandler = (\n  evt: RecordEvent,\n  opts?: HandlerOpts,\n) => Promise<void>\n\ntype ErrorHandler = (err: Error) => void\n\nexport class SimpleIndexer implements TapHandler {\n  private identityHandler: IdentityEventHandler | undefined\n  private recordHandler: RecordEventHandler | undefined\n  private errorHandler: ErrorHandler | undefined\n\n  identity(fn: IdentityEventHandler): this {\n    this.identityHandler = fn\n    return this\n  }\n\n  record(fn: RecordEventHandler): this {\n    this.recordHandler = fn\n    return this\n  }\n\n  error(fn: ErrorHandler): this {\n    this.errorHandler = fn\n    return this\n  }\n\n  async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {\n    if (evt.type === 'record') {\n      await this.recordHandler?.(evt, opts)\n    } else {\n      await this.identityHandler?.(evt, opts)\n    }\n    await opts.ack()\n  }\n\n  onError(err: Error) {\n    if (this.errorHandler) {\n      this.errorHandler(err)\n    } else {\n      throw err\n    }\n  }\n}\n"
  },
  {
    "path": "packages/tap/src/types.ts",
    "content": "import { LexMap, LexValue, l } from '@atproto/lex'\nimport { DidString, HandleString, NsidString } from '@atproto/syntax'\n\nexport const recordEventDataSchema = l.object({\n  did: l.string({ format: 'did' }),\n  rev: l.string(),\n  collection: l.string({ format: 'nsid' }),\n  rkey: l.string({ format: 'record-key' }),\n  action: l.enum(['create', 'update', 'delete']),\n  record: l.optional(l.lexMap()),\n  cid: l.optional(l.string({ format: 'cid' })),\n  live: l.boolean(),\n})\n\nexport const identityEventDataSchema = l.object({\n  did: l.string({ format: 'did' }),\n  handle: l.string({ format: 'handle' }),\n  is_active: l.boolean(),\n  status: l.enum([\n    'active',\n    'takendown',\n    'suspended',\n    'deactivated',\n    'deleted',\n  ]),\n})\n\nexport const recordEventSchema = l.object({\n  id: l.integer(),\n  type: l.literal('record'),\n  record: recordEventDataSchema,\n})\n\nexport const identityEventSchema = l.object({\n  id: l.integer(),\n  type: l.literal('identity'),\n  identity: identityEventDataSchema,\n})\n\nexport const tapEventSchema = l.discriminatedUnion('type', [\n  recordEventSchema,\n  identityEventSchema,\n])\n\nexport type RecordEvent = {\n  id: number\n  type: 'record'\n  action: 'create' | 'update' | 'delete'\n  did: DidString\n  rev: string\n  collection: NsidString\n  rkey: string\n  record?: LexMap\n  cid?: string\n  live: boolean\n}\n\nexport type IdentityEvent = {\n  id: number\n  type: 'identity'\n  did: DidString\n  handle: HandleString\n  isActive: boolean\n  status: RepoStatus\n}\n\nexport type RepoStatus =\n  | 'active'\n  | 'takendown'\n  | 'suspended'\n  | 'deactivated'\n  | 'deleted'\n\nexport type TapEvent = IdentityEvent | RecordEvent\n\nexport const parseTapEvent = (data: LexValue): TapEvent => {\n  const parsed = tapEventSchema.parse(data)\n  if (parsed.type === 'identity') {\n    return {\n      id: parsed.id,\n      type: parsed.type,\n      did: parsed.identity.did,\n      handle: parsed.identity.handle,\n      isActive: parsed.identity.is_active,\n      status: parsed.identity.status,\n    }\n  } else {\n    return {\n      id: parsed.id,\n      type: parsed.type,\n      action: parsed.record.action,\n      did: parsed.record.did,\n      rev: parsed.record.rev,\n      collection: parsed.record.collection,\n      rkey: parsed.record.rkey,\n      record: parsed.record.record,\n      cid: parsed.record.cid,\n      live: parsed.record.live,\n    }\n  }\n}\n\nexport const repoInfoSchema = l.object({\n  did: l.string(),\n  handle: l.string(),\n  state: l.string(),\n  rev: l.string(),\n  records: l.integer(),\n  error: l.optional(l.string()),\n  retries: l.optional(l.integer()),\n})\n\nexport type RepoInfo = l.Infer<typeof repoInfoSchema>\n"
  },
  {
    "path": "packages/tap/src/util.ts",
    "content": "export const formatAdminAuthHeader = (password: string) => {\n  return 'Basic ' + Buffer.from(`admin:${password}`).toString('base64')\n}\n\nexport const parseAdminAuthHeader = (header: string) => {\n  const noPrefix = header.startsWith('Basic ') ? header.slice(6) : header\n  const [username, password] = Buffer.from(noPrefix, 'base64')\n    .toString()\n    .split(':')\n  if (username !== 'admin') {\n    throw new Error(\"Unexpected username in admin headers. Expected 'admin'\")\n  }\n  return password\n}\n\nexport const assureAdminAuth = (expectedPassword: string, header: string) => {\n  const headerPassword = parseAdminAuthHeader(header)\n  const passEqual = timingSafeEqual(headerPassword, expectedPassword)\n  if (!passEqual) {\n    throw new Error('Invalid admin password')\n  }\n}\n\nconst timingSafeEqual = (a: string, b: string): boolean => {\n  const bufA = Buffer.from(a)\n  const bufB = Buffer.from(b)\n  if (bufA.length !== bufB.length) {\n    // Compare against self to maintain constant time even with length mismatch\n    Buffer.from(a).compare(Buffer.from(a))\n    return false\n  }\n  return bufA.compare(bufB) === 0\n}\n\nexport function isCausedBySignal(err: unknown, signal: AbortSignal) {\n  if (!signal.aborted) return false\n  if (signal.reason == null) return false // Ignore nullish reasons\n  return (\n    err === signal.reason ||\n    (err instanceof Error && err.cause === signal.reason)\n  )\n}\n"
  },
  {
    "path": "packages/tap/tests/_util.ts",
    "content": "import { WebSocketServer } from 'ws'\nimport { HandlerOpts } from '../src/channel'\nimport { IdentityEvent, RecordEvent } from '../src/types'\n\nexport type MockOpts = HandlerOpts & { acked: boolean }\n\nexport const createMockOpts = (): MockOpts => {\n  const opts = {\n    signal: new AbortController().signal,\n    acked: false,\n    ack: async () => {\n      opts.acked = true\n    },\n  }\n  return opts\n}\n\nexport const createRecordEvent = (\n  overrides: Partial<RecordEvent> = {},\n): RecordEvent => ({\n  id: 1,\n  type: 'record',\n  did: 'did:example:alice',\n  rev: 'abc123',\n  collection: 'com.example.post',\n  rkey: 'abc123',\n  action: 'create',\n  record: { text: 'hello' },\n  cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',\n  live: true,\n  ...overrides,\n})\n\nexport const createIdentityEvent = (): IdentityEvent => ({\n  id: 2,\n  type: 'identity',\n  did: 'did:example:alice',\n  handle: 'alice.test',\n  isActive: true,\n  status: 'active',\n})\n\nexport async function createWebSocketServer() {\n  return new Promise<WebSocketServer & AsyncDisposable>((resolve, reject) => {\n    const server = new WebSocketServer({ port: 0 }, () => {\n      server.off('error', reject)\n      resolve(\n        Object.defineProperty(server, Symbol.asyncDispose, {\n          value: disposeWebSocketServer,\n        }) as WebSocketServer & AsyncDisposable,\n      )\n    }).once('error', reject)\n  })\n}\n\nasync function disposeWebSocketServer(this: WebSocketServer) {\n  return new Promise<void>((resolve, reject) => {\n    this.close((err) => {\n      if (err) reject(err)\n      else resolve()\n    })\n  })\n}\n"
  },
  {
    "path": "packages/tap/tests/channel.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { AddressInfo } from 'ws'\nimport { TapChannel, TapHandler } from '../src/channel'\nimport { TapEvent } from '../src/types'\nimport { createWebSocketServer } from './_util'\n\nconst createRecordEvent = (id: number) => ({\n  id,\n  type: 'record' as const,\n  record: {\n    did: 'did:example:alice',\n    rev: '3abc123',\n    collection: 'com.example.post',\n    rkey: 'abc123',\n    action: 'create' as const,\n    record: { text: 'hello' },\n    cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',\n    live: true,\n  },\n})\n\nconst createIdentityEvent = (id: number) => ({\n  id,\n  type: 'identity' as const,\n  identity: {\n    did: 'did:example:alice',\n    handle: 'alice.test',\n    is_active: true,\n    status: 'active' as const,\n  },\n})\n\ndescribe('TapChannel', () => {\n  describe('receiving events', () => {\n    it('receives and parses record events', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedEvents: TapEvent[] = []\n\n      server.on('connection', (socket) => {\n        socket.send(JSON.stringify(createRecordEvent(1)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            socket.close()\n          }\n        })\n      })\n\n      const handler: TapHandler = {\n        onEvent: async (evt, opts) => {\n          receivedEvents.push(evt)\n          await opts.ack()\n        },\n        onError: (err) => {\n          throw err\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(receivedEvents).toHaveLength(1)\n      expect(receivedEvents[0].type).toBe('record')\n      expect(receivedEvents[0].did).toBe('did:example:alice')\n      if (receivedEvents[0].type === 'record') {\n        expect(receivedEvents[0].collection).toBe('com.example.post')\n        expect(receivedEvents[0].action).toBe('create')\n      }\n    })\n\n    it('receives and parses identity events', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedEvents: TapEvent[] = []\n\n      server.on('connection', (socket) => {\n        socket.send(JSON.stringify(createIdentityEvent(1)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            socket.close()\n          }\n        })\n      })\n\n      const handler: TapHandler = {\n        onEvent: async (evt, opts) => {\n          receivedEvents.push(evt)\n          await opts.ack()\n        },\n        onError: (err) => {\n          throw err\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(receivedEvents).toHaveLength(1)\n      expect(receivedEvents[0].type).toBe('identity')\n      expect(receivedEvents[0].did).toBe('did:example:alice')\n      if (receivedEvents[0].type === 'identity') {\n        expect(receivedEvents[0].handle).toBe('alice.test')\n        expect(receivedEvents[0].status).toBe('active')\n      }\n    })\n  })\n\n  describe('ack behavior', () => {\n    it('sends ack when handler calls ack()', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedAcks: number[] = []\n\n      server.on('connection', (socket) => {\n        socket.send(JSON.stringify(createRecordEvent(42)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            receivedAcks.push(msg.id)\n            socket.close()\n          }\n        })\n      })\n\n      const handler: TapHandler = {\n        onEvent: async (_evt, opts) => {\n          await opts.ack()\n        },\n        onError: (err) => {\n          throw err\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(receivedAcks).toEqual([42])\n    })\n\n    it('does not send ack if handler throws', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedAcks: number[] = []\n      const errors: Error[] = []\n\n      server.on('connection', (socket) => {\n        socket.send(JSON.stringify(createRecordEvent(1)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            receivedAcks.push(msg.id)\n          }\n        })\n        // Close after a short delay to let error propagate\n        setTimeout(() => socket.close(), 100)\n      })\n\n      const handler: TapHandler = {\n        onEvent: async () => {\n          throw new Error('Handler failed')\n        },\n        onError: (err) => {\n          errors.push(err)\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(receivedAcks).toHaveLength(0)\n      expect(errors).toHaveLength(1)\n      expect(errors[0].message).toContain('Failed to process event')\n    })\n\n    it('does not send ack if handler does not call ack()', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedAcks: number[] = []\n\n      server.on('connection', (socket) => {\n        socket.send(JSON.stringify(createRecordEvent(1)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            receivedAcks.push(msg.id)\n          }\n        })\n        // Close after a short delay\n        setTimeout(() => socket.close(), 100)\n      })\n\n      const handler: TapHandler = {\n        onEvent: async () => {\n          // Don't call ack\n        },\n        onError: (err) => {\n          throw err\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(receivedAcks).toHaveLength(0)\n    })\n\n    it('handles reconnection and receives events from new connection', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedEvents: TapEvent[] = []\n      const receivedAcks: number[] = []\n      let connectionCount = 0\n\n      server.on('connection', (socket) => {\n        connectionCount++\n        // Send a different event each connection\n        const eventId = connectionCount\n        socket.send(JSON.stringify(createRecordEvent(eventId)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            receivedAcks.push(msg.id)\n            if (connectionCount === 1) {\n              // After first ack, terminate to trigger reconnect\n              socket.terminate()\n            } else {\n              // After second ack, close cleanly\n              socket.close()\n            }\n          }\n        })\n      })\n\n      const handler: TapHandler = {\n        onEvent: async (evt, opts) => {\n          receivedEvents.push(evt)\n          await opts.ack()\n        },\n        onError: () => {},\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler, {\n        maxReconnectSeconds: 1,\n      })\n\n      await channel.start()\n\n      // Should have connected twice and received two events\n      expect(connectionCount).toBe(2)\n      expect(receivedEvents).toHaveLength(2)\n      expect(receivedEvents[0].id).toBe(1)\n      expect(receivedEvents[1].id).toBe(2)\n      expect(receivedAcks).toContain(1)\n      expect(receivedAcks).toContain(2)\n    })\n  })\n\n  describe('multiple events', () => {\n    it('processes multiple events in sequence', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const receivedEvents: TapEvent[] = []\n      const receivedAcks: number[] = []\n\n      server.on('connection', (socket) => {\n        socket.send(JSON.stringify(createRecordEvent(1)))\n        socket.send(JSON.stringify(createRecordEvent(2)))\n        socket.send(JSON.stringify(createIdentityEvent(3)))\n        socket.on('message', (data) => {\n          const msg = JSON.parse(data.toString())\n          if (msg.type === 'ack') {\n            receivedAcks.push(msg.id)\n            if (receivedAcks.length === 3) {\n              socket.close()\n            }\n          }\n        })\n      })\n\n      const handler: TapHandler = {\n        onEvent: async (evt, opts) => {\n          receivedEvents.push(evt)\n          await opts.ack()\n        },\n        onError: (err) => {\n          throw err\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(receivedEvents).toHaveLength(3)\n      expect(receivedEvents[0].id).toBe(1)\n      expect(receivedEvents[1].id).toBe(2)\n      expect(receivedEvents[2].id).toBe(3)\n      expect(receivedAcks).toEqual([1, 2, 3])\n    })\n  })\n\n  describe('auth', () => {\n    it('includes auth header when adminPassword is provided', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      let receivedAuthHeader: string | undefined\n\n      server.on('connection', (socket, request) => {\n        receivedAuthHeader = request.headers.authorization\n        socket.close()\n      })\n\n      const handler: TapHandler = {\n        onEvent: async () => {},\n        onError: () => {},\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler, {\n        adminPassword: 'secret',\n      })\n      await channel.start()\n\n      expect(receivedAuthHeader).toBe('Basic YWRtaW46c2VjcmV0')\n    })\n  })\n\n  describe('error handling', () => {\n    it('calls onError for malformed messages', async () => {\n      await using server = await createWebSocketServer()\n\n      const { port } = server.address() as AddressInfo\n\n      const errors: Error[] = []\n\n      server.on('connection', (socket) => {\n        socket.send('not valid json')\n        setTimeout(() => socket.close(), 100)\n      })\n\n      const handler: TapHandler = {\n        onEvent: async () => {},\n        onError: (err) => {\n          errors.push(err)\n        },\n      }\n\n      await using channel = new TapChannel(`ws://localhost:${port}`, handler)\n      await channel.start()\n\n      expect(errors).toHaveLength(1)\n      expect(errors[0].message).toBe('Failed to parse message')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/tap/tests/client.test.ts",
    "content": "import { once } from 'node:events'\nimport * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { default as express } from 'express'\nimport { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'\nimport { Tap } from '../src/client'\n\ndescribe('Tap client', () => {\n  describe('constructor', () => {\n    it('accepts http URL', () => {\n      const tap = new Tap('http://localhost:8080')\n      expect(tap.url).toBe('http://localhost:8080')\n    })\n\n    it('accepts https URL', () => {\n      const tap = new Tap('https://example.com')\n      expect(tap.url).toBe('https://example.com')\n    })\n\n    it('throws on invalid URL', () => {\n      expect(() => new Tap('ws://localhost:8080')).toThrow(\n        'Invalid URL, expected http:// or https://',\n      )\n      expect(() => new Tap('localhost:8080')).toThrow(\n        'Invalid URL, expected http:// or https://',\n      )\n    })\n  })\n\n  describe('HTTP methods', () => {\n    let server: http.Server<\n      typeof http.IncomingMessage,\n      typeof http.ServerResponse\n    >\n    let tap: Tap\n    let requests: {\n      path: string\n      method: string\n      body?: unknown\n      headers: http.IncomingHttpHeaders\n    }[]\n\n    beforeAll(async () => {\n      const app = express()\n      app.use(express.json())\n\n      requests = []\n\n      app.post('/repos/add', (req, res) => {\n        requests.push({\n          path: req.path,\n          method: req.method,\n          body: req.body,\n          headers: req.headers,\n        })\n        res.sendStatus(200)\n      })\n\n      app.post('/repos/remove', (req, res) => {\n        requests.push({\n          path: req.path,\n          method: req.method,\n          body: req.body,\n          headers: req.headers,\n        })\n        res.sendStatus(200)\n      })\n\n      app.get('/resolve/:did', (req, res) => {\n        requests.push({\n          path: req.path,\n          method: req.method,\n          headers: req.headers,\n        })\n        if (req.params.did === 'did:example:notfound') {\n          res.sendStatus(404)\n          return\n        }\n        res.json({\n          id: req.params.did,\n          alsoKnownAs: ['at://alice.test'],\n          verificationMethod: [],\n          service: [],\n        })\n      })\n\n      app.get('/info/:did', (req, res) => {\n        requests.push({\n          path: req.path,\n          method: req.method,\n          headers: req.headers,\n        })\n        res.json({\n          did: req.params.did,\n          handle: 'alice.test',\n          state: 'active',\n          rev: '3abc123',\n          records: 42,\n        })\n      })\n\n      server = app.listen()\n      await once(server, 'listening')\n      const { port } = server.address() as AddressInfo\n      tap = new Tap(`http://localhost:${port}`, { adminPassword: 'secret' })\n    })\n\n    afterAll(async () => {\n      await new Promise((resolve) => server.close(resolve))\n    })\n\n    beforeEach(() => {\n      requests = []\n    })\n\n    describe('addRepos', () => {\n      it('sends POST to /repos/add with dids', async () => {\n        await tap.addRepos(['did:example:alice', 'did:example:bob'])\n        expect(requests).toHaveLength(1)\n        expect(requests[0].path).toBe('/repos/add')\n        expect(requests[0].method).toBe('POST')\n        expect(requests[0].body).toEqual({\n          dids: ['did:example:alice', 'did:example:bob'],\n        })\n      })\n\n      it('includes auth header', async () => {\n        await tap.addRepos(['did:example:alice'])\n        expect(requests[0].headers.authorization).toBe('Basic YWRtaW46c2VjcmV0')\n      })\n    })\n\n    describe('removeRepos', () => {\n      it('sends POST to /repos/remove with dids', async () => {\n        await tap.removeRepos(['did:example:alice'])\n        expect(requests).toHaveLength(1)\n        expect(requests[0].path).toBe('/repos/remove')\n        expect(requests[0].method).toBe('POST')\n        expect(requests[0].body).toEqual({ dids: ['did:example:alice'] })\n      })\n    })\n\n    describe('resolveDid', () => {\n      it('fetches and parses DID document', async () => {\n        const doc = await tap.resolveDid('did:example:alice')\n        expect(doc).not.toBeNull()\n        expect(doc?.id).toBe('did:example:alice')\n        expect(doc?.alsoKnownAs).toEqual(['at://alice.test'])\n      })\n\n      it('returns null for 404', async () => {\n        const doc = await tap.resolveDid('did:example:notfound')\n        expect(doc).toBeNull()\n      })\n    })\n\n    describe('getRepoInfo', () => {\n      it('fetches and parses repo info', async () => {\n        const info = await tap.getRepoInfo('did:example:alice')\n        expect(info.did).toBe('did:example:alice')\n        expect(info.handle).toBe('alice.test')\n        expect(info.state).toBe('active')\n        expect(info.records).toBe(42)\n      })\n    })\n  })\n\n  describe('HTTP error handling', () => {\n    let server: http.Server<\n      typeof http.IncomingMessage,\n      typeof http.ServerResponse\n    >\n    let tap: Tap\n\n    beforeAll(async () => {\n      const app = express()\n      app.use(express.json())\n\n      app.post('/repos/add', (_req, res) => {\n        res.status(500).send('Internal Server Error')\n      })\n\n      app.get('/info/:did', (_req, res) => {\n        res.status(500).send('Internal Server Error')\n      })\n\n      server = app.listen()\n      await once(server, 'listening')\n      const { port } = server.address() as AddressInfo\n      tap = new Tap(`http://localhost:${port}`)\n    })\n\n    afterAll(async () => {\n      await new Promise((resolve) => server.close(resolve))\n    })\n\n    it('throws on addRepos failure', async () => {\n      await expect(tap.addRepos(['did:example:alice'])).rejects.toThrow(\n        'Failed to add repos',\n      )\n    })\n\n    it('throws on getRepoInfo failure', async () => {\n      await expect(tap.getRepoInfo('did:example:alice')).rejects.toThrow(\n        'Failed to get repo info',\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/tap/tests/lex-indexer.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { l } from '@atproto/lex'\nimport {\n  CreateEvent,\n  DeleteEvent,\n  LexIndexer,\n  UpdateEvent,\n} from '../src/lex-indexer'\nimport { IdentityEvent, RecordEvent } from '../src/types'\nimport {\n  createIdentityEvent,\n  createMockOpts,\n  createRecordEvent as baseCreateRecordEvent,\n} from './_util'\n\n// Test lexicon definitions\nconst postNsid = 'com.example.post'\ntype Post = {\n  $type: 'com.example.post'\n  text: string\n}\nconst post = {\n  main: l.record<'tid', Post>('tid', postNsid, l.object({ text: l.string() })),\n}\n\nconst likeNsid = 'com.example.like'\ntype Like = {\n  $type: 'com.example.like'\n  subject: string\n}\nconst like = {\n  main: l.record<'tid', Like>(\n    'tid',\n    likeNsid,\n    l.object({ subject: l.string() }),\n  ),\n}\n\nconst createRecordEvent = (overrides: Partial<RecordEvent> = {}): RecordEvent =>\n  baseCreateRecordEvent({\n    collection: postNsid,\n    record: { $type: postNsid, text: 'hello' },\n    ...overrides,\n  })\n\ndescribe('LexIndexer', () => {\n  describe('handler registration', () => {\n    it('registers create handler', async () => {\n      const indexer = new LexIndexer()\n      const received: CreateEvent<Post>[] = []\n\n      indexer.create(post, async (evt) => {\n        received.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(received).toHaveLength(1)\n      expect(received[0].action).toBe('create')\n      expect(received[0].record.text).toBe('hello')\n      expect(received[0].cid).toBe(\n        'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',\n      )\n    })\n\n    it('registers update handler', async () => {\n      const indexer = new LexIndexer()\n      const received: UpdateEvent<Post>[] = []\n\n      indexer.update(post, async (evt) => {\n        received.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(\n        createRecordEvent({\n          action: 'update',\n          record: { $type: postNsid, text: 'updated' },\n        }),\n        opts,\n      )\n\n      expect(received).toHaveLength(1)\n      expect(received[0].action).toBe('update')\n      expect(received[0].record.text).toBe('updated')\n    })\n\n    it('registers delete handler', async () => {\n      const indexer = new LexIndexer()\n      const received: DeleteEvent[] = []\n\n      indexer.delete(post, async (evt) => {\n        received.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(\n        createRecordEvent({\n          action: 'delete',\n          record: undefined,\n          cid: undefined,\n        }),\n        opts,\n      )\n\n      expect(received).toHaveLength(1)\n      expect(received[0].action).toBe('delete')\n    })\n\n    it('registers put handler for both create and update', async () => {\n      const indexer = new LexIndexer()\n      const received: Array<{ action: string; text: string }> = []\n\n      indexer.put(post, async (evt) => {\n        received.push({ action: evt.action, text: evt.record.text })\n      })\n\n      const opts1 = createMockOpts()\n      await indexer.onEvent(createRecordEvent({ action: 'create' }), opts1)\n\n      const opts2 = createMockOpts()\n      await indexer.onEvent(\n        createRecordEvent({\n          action: 'update',\n          record: { $type: postNsid, text: 'updated' },\n        }),\n        opts2,\n      )\n\n      expect(received).toHaveLength(2)\n      expect(received[0].action).toBe('create')\n      expect(received[1].action).toBe('update')\n    })\n  })\n\n  describe('handler routing', () => {\n    it('routes to correct handler by collection', async () => {\n      const indexer = new LexIndexer()\n      const postEvents: CreateEvent<Post>[] = []\n      const likeEvents: CreateEvent<Like>[] = []\n\n      indexer.create(post, async (evt) => {\n        postEvents.push(evt)\n      })\n      indexer.create(like, async (evt) => {\n        likeEvents.push(evt)\n      })\n\n      const opts1 = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts1)\n\n      const opts2 = createMockOpts()\n      await indexer.onEvent(\n        createRecordEvent({\n          collection: likeNsid,\n          record: { $type: likeNsid, subject: 'at://did:example:bob/post/123' },\n        }),\n        opts2,\n      )\n\n      expect(postEvents).toHaveLength(1)\n      expect(likeEvents).toHaveLength(1)\n    })\n\n    it('routes to other handler for unregistered collections', async () => {\n      const indexer = new LexIndexer()\n      const otherEvents: RecordEvent[] = []\n\n      indexer.create(post, async () => {})\n      indexer.other(async (evt) => {\n        otherEvents.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(\n        createRecordEvent({ collection: 'com.example.unknown' }),\n        opts,\n      )\n\n      expect(otherEvents).toHaveLength(1)\n      expect(otherEvents[0].collection).toBe('com.example.unknown')\n    })\n\n    it('routes to other handler for unregistered actions', async () => {\n      const indexer = new LexIndexer()\n      const otherEvents: RecordEvent[] = []\n\n      indexer.create(post, async () => {})\n      indexer.other(async (evt) => {\n        otherEvents.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent({ action: 'delete' }), opts)\n\n      expect(otherEvents).toHaveLength(1)\n      expect(otherEvents[0].action).toBe('delete')\n    })\n\n    it('routes identity events to identity handler', async () => {\n      const indexer = new LexIndexer()\n      const received: IdentityEvent[] = []\n\n      indexer.identity(async (evt) => {\n        received.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createIdentityEvent(), opts)\n\n      expect(received).toHaveLength(1)\n      expect(received[0].handle).toBe('alice.test')\n    })\n  })\n\n  describe('duplicate registration', () => {\n    it('throws on duplicate create handler', () => {\n      const indexer = new LexIndexer()\n      indexer.create(post, async () => {})\n\n      expect(() => indexer.create(post, async () => {})).toThrow(\n        'Handler already registered',\n      )\n    })\n\n    it('throws on duplicate update handler', () => {\n      const indexer = new LexIndexer()\n      indexer.update(post, async () => {})\n\n      expect(() => indexer.update(post, async () => {})).toThrow(\n        'Handler already registered',\n      )\n    })\n\n    it('throws on duplicate delete handler', () => {\n      const indexer = new LexIndexer()\n      indexer.delete(post, async () => {})\n\n      expect(() => indexer.delete(post, async () => {})).toThrow(\n        'Handler already registered',\n      )\n    })\n\n    it('throws when put conflicts with create', () => {\n      const indexer = new LexIndexer()\n      indexer.create(post, async () => {})\n\n      expect(() => indexer.put(post, async () => {})).toThrow(\n        'Handler already registered',\n      )\n    })\n\n    it('throws when create conflicts with put', () => {\n      const indexer = new LexIndexer()\n      indexer.put(post, async () => {})\n\n      expect(() => indexer.create(post, async () => {})).toThrow(\n        'Handler already registered',\n      )\n    })\n  })\n\n  describe('schema validation', () => {\n    it('validates record on create', async () => {\n      const indexer = new LexIndexer()\n      indexer.create(post, async () => {})\n\n      const opts = createMockOpts()\n      await expect(\n        indexer.onEvent(createRecordEvent({ record: { text: 123 } }), opts),\n      ).rejects.toThrow('Record validation failed')\n    })\n\n    it('validates record on update', async () => {\n      const indexer = new LexIndexer()\n      indexer.update(post, async () => {})\n\n      const opts = createMockOpts()\n      await expect(\n        indexer.onEvent(\n          createRecordEvent({ action: 'update', record: { invalid: true } }),\n          opts,\n        ),\n      ).rejects.toThrow('Record validation failed')\n    })\n  })\n\n  describe('ack behavior', () => {\n    it('calls ack after handler completes', async () => {\n      const indexer = new LexIndexer()\n      indexer.create(post, async () => {})\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(opts.acked).toBe(true)\n    })\n\n    it('calls ack when routed to other handler', async () => {\n      const indexer = new LexIndexer()\n      indexer.other(async () => {})\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(opts.acked).toBe(true)\n    })\n\n    it('calls ack even when no handler matches', async () => {\n      const indexer = new LexIndexer()\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(opts.acked).toBe(true)\n    })\n  })\n\n  describe('error handling', () => {\n    it('calls error handler when provided', () => {\n      const indexer = new LexIndexer()\n      const errors: Error[] = []\n\n      indexer.error((err) => {\n        errors.push(err)\n      })\n\n      const testError = new Error('test error')\n      indexer.onError(testError)\n\n      expect(errors).toHaveLength(1)\n      expect(errors[0]).toBe(testError)\n    })\n\n    it('throws when no error handler is registered', () => {\n      const indexer = new LexIndexer()\n      const testError = new Error('test error')\n\n      expect(() => indexer.onError(testError)).toThrow('test error')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/tap/tests/simple-indexer.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport { HandlerOpts } from '../src/channel'\nimport { SimpleIndexer } from '../src/simple-indexer'\nimport { IdentityEvent, RecordEvent } from '../src/types'\nimport { createIdentityEvent, createMockOpts, createRecordEvent } from './_util'\n\ndescribe('SimpleIndexer', () => {\n  describe('event routing', () => {\n    it('routes record events to record handler', async () => {\n      const indexer = new SimpleIndexer()\n      const receivedEvents: RecordEvent[] = []\n\n      indexer.record(async (evt) => {\n        receivedEvents.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(receivedEvents).toHaveLength(1)\n      expect(receivedEvents[0].type).toBe('record')\n      expect(receivedEvents[0].collection).toBe('com.example.post')\n    })\n\n    it('routes identity events to identity handler', async () => {\n      const indexer = new SimpleIndexer()\n      const receivedEvents: IdentityEvent[] = []\n\n      indexer.identity(async (evt) => {\n        receivedEvents.push(evt)\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createIdentityEvent(), opts)\n\n      expect(receivedEvents).toHaveLength(1)\n      expect(receivedEvents[0].type).toBe('identity')\n      expect(receivedEvents[0].handle).toBe('alice.test')\n    })\n\n    it('does not call identity handler for record events', async () => {\n      const indexer = new SimpleIndexer()\n      let identityCalled = false\n      let recordCalled = false\n\n      indexer.identity(async () => {\n        identityCalled = true\n      })\n      indexer.record(async () => {\n        recordCalled = true\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(recordCalled).toBe(true)\n      expect(identityCalled).toBe(false)\n    })\n\n    it('does not call record handler for identity events', async () => {\n      const indexer = new SimpleIndexer()\n      let identityCalled = false\n      let recordCalled = false\n\n      indexer.identity(async () => {\n        identityCalled = true\n      })\n      indexer.record(async () => {\n        recordCalled = true\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createIdentityEvent(), opts)\n\n      expect(identityCalled).toBe(true)\n      expect(recordCalled).toBe(false)\n    })\n  })\n\n  describe('ack behavior', () => {\n    it('calls ack after handler completes', async () => {\n      const indexer = new SimpleIndexer()\n      indexer.record(async () => {})\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(opts.acked).toBe(true)\n    })\n\n    it('calls ack even when no handler is registered', async () => {\n      const indexer = new SimpleIndexer()\n      // No handlers registered\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(opts.acked).toBe(true)\n    })\n  })\n\n  describe('error handling', () => {\n    it('calls error handler when provided', () => {\n      const indexer = new SimpleIndexer()\n      const errors: Error[] = []\n\n      indexer.error((err) => {\n        errors.push(err)\n      })\n\n      const testError = new Error('test error')\n      indexer.onError(testError)\n\n      expect(errors).toHaveLength(1)\n      expect(errors[0]).toBe(testError)\n    })\n\n    it('throws when no error handler is registered', () => {\n      const indexer = new SimpleIndexer()\n      const testError = new Error('test error')\n\n      expect(() => indexer.onError(testError)).toThrow('test error')\n    })\n  })\n\n  describe('handler opts passthrough', () => {\n    it('passes opts to record handler', async () => {\n      const indexer = new SimpleIndexer()\n      let receivedOpts: HandlerOpts | undefined\n\n      indexer.record(async (_evt, opts) => {\n        receivedOpts = opts\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createRecordEvent(), opts)\n\n      expect(receivedOpts).toBeDefined()\n      expect(receivedOpts?.signal).toBe(opts.signal)\n    })\n\n    it('passes opts to identity handler', async () => {\n      const indexer = new SimpleIndexer()\n      let receivedOpts: HandlerOpts | undefined\n\n      indexer.identity(async (_evt, opts) => {\n        receivedOpts = opts\n      })\n\n      const opts = createMockOpts()\n      await indexer.onEvent(createIdentityEvent(), opts)\n\n      expect(receivedOpts).toBeDefined()\n      expect(receivedOpts?.signal).toBe(opts.signal)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/tap/tests/util.test.ts",
    "content": "import { describe, expect, it } from 'vitest'\nimport {\n  assureAdminAuth,\n  formatAdminAuthHeader,\n  parseAdminAuthHeader,\n} from '../src'\n\ndescribe('util', () => {\n  describe('formatAdminAuthHeader', () => {\n    it('formats password as Basic auth header', () => {\n      const header = formatAdminAuthHeader('secret')\n      expect(header).toBe('Basic YWRtaW46c2VjcmV0')\n    })\n\n    it('uses admin as username', () => {\n      const header = formatAdminAuthHeader('secret')\n      const decoded = Buffer.from(\n        header.replace('Basic ', ''),\n        'base64',\n      ).toString()\n      expect(decoded).toBe('admin:secret')\n    })\n  })\n\n  describe('parseAdminAuthHeader', () => {\n    it('parses Basic auth header and returns password', () => {\n      const header = 'Basic YWRtaW46c2VjcmV0' // admin:secret\n      const password = parseAdminAuthHeader(header)\n      expect(password).toBe('secret')\n    })\n\n    it('handles header without Basic prefix', () => {\n      const header = 'YWRtaW46c2VjcmV0' // admin:secret (no prefix)\n      const password = parseAdminAuthHeader(header)\n      expect(password).toBe('secret')\n    })\n\n    it('throws if username is not admin', () => {\n      const header = 'Basic ' + Buffer.from('user:secret').toString('base64')\n      expect(() => parseAdminAuthHeader(header)).toThrow(\n        \"Unexpected username in admin headers. Expected 'admin'\",\n      )\n    })\n  })\n\n  describe('assureAdminAuth', () => {\n    it('does not throw when password matches', () => {\n      const header = formatAdminAuthHeader('secret')\n      expect(() => assureAdminAuth('secret', header)).not.toThrow()\n    })\n\n    it('throws when password does not match', () => {\n      const header = formatAdminAuthHeader('wrong')\n      expect(() => assureAdminAuth('secret', header)).toThrow(\n        'Invalid admin password',\n      )\n    })\n\n    it('throws when header has invalid username', () => {\n      const header =\n        'Basic ' + Buffer.from('notadmin:secret').toString('base64')\n      expect(() => assureAdminAuth('secret', header)).toThrow()\n    })\n\n    it('is timing-safe (does not leak password length)', () => {\n      // This is a basic sanity check - true timing attack tests require statistical analysis\n      const header = formatAdminAuthHeader('a')\n      const start1 = performance.now()\n      try {\n        assureAdminAuth('b', header)\n      } catch {\n        // do nothing\n      }\n      const time1 = performance.now() - start1\n\n      const longHeader = formatAdminAuthHeader('a'.repeat(1000))\n      const start2 = performance.now()\n      try {\n        assureAdminAuth('b'.repeat(1000), longHeader)\n      } catch {\n        // do nothing\n      }\n      const time2 = performance.now() - start2\n\n      // Times should be in the same order of magnitude (not a rigorous test)\n      expect(Math.abs(time1 - time2)).toBeLessThan(50)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/tap/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/tap/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/tap/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/vitest.json\",\n  \"include\": [\"./tests\", \"./src/**/*.test.ts\"],\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\"\n  }\n}\n"
  },
  {
    "path": "packages/tap/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config'\n\nexport default defineProject({\n  test: {},\n})\n"
  },
  {
    "path": "packages/ws-client/CHANGELOG.md",
    "content": "# @atproto/ws-client\n\n## 0.0.4\n\n### Patch Changes\n\n- [#4290](https://github.com/bluesky-social/atproto/pull/4290) [`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957) Thanks [@dholms](https://github.com/dholms)! - Support sending data on websocket as well as an onReconnect callback\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common@0.5.0\n\n## 0.0.2\n\n### Patch Changes\n\n- [#4348](https://github.com/bluesky-social/atproto/pull/4348) [`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8) Thanks [@dholms](https://github.com/dholms)! - Move WebSocketKeepAlive into its own package\n"
  },
  {
    "path": "packages/ws-client/README.md",
    "content": "# @atproto/ws-client: WebSocket Client Library\n\nShared Typescript library for managing a long-lived WebSocket client connection, including a heartbeat mechanism to ensure the connection remains active.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/ws-client)](https://www.npmjs.com/package/@atproto/ws-client)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/ws-client/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'WebSocket Client',\n  transform: { '^.+\\\\.(j|t)s$': '@swc/jest' },\n  transformIgnorePatterns: ['/node_modules/.pnpm/(?!(get-port)@)'],\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/ws-client/package.json",
    "content": "{\n  \"name\": \"@atproto/ws-client\",\n  \"version\": \"0.0.4\",\n  \"license\": \"MIT\",\n  \"description\": \"Websocket client library\",\n  \"keywords\": [\n    \"atproto\",\n    \"xrpc\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/ws-client\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"ws\": \"^8.12.0\"\n  },\n  \"devDependencies\": {\n    \"@types/ws\": \"^8.5.4\",\n    \"get-port\": \"^6.1.2\",\n    \"jest\": \"^28.1.2\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/ws-client/src/index.ts",
    "content": "import { ClientOptions, WebSocket, createWebSocketStream } from 'ws'\nimport { SECOND, isErrnoException, wait } from '@atproto/common'\n\nexport class WebSocketKeepAlive {\n  public ws: WebSocket | null = null\n  public initialSetup = true\n  public reconnects: number | null = null\n\n  constructor(\n    public opts: ClientOptions & {\n      getUrl: () => Promise<string>\n      maxReconnectSeconds?: number\n      signal?: AbortSignal\n      heartbeatIntervalMs?: number\n      onReconnect?: () => void\n      onReconnectError?: (\n        error: unknown,\n        n: number,\n        initialSetup: boolean,\n      ) => void\n    },\n  ) {}\n\n  async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n    const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64)\n    while (true) {\n      if (this.reconnects !== null) {\n        const duration = this.initialSetup\n          ? Math.min(1000, maxReconnectMs)\n          : backoffMs(this.reconnects++, maxReconnectMs)\n        await wait(duration)\n      }\n      const url = await this.opts.getUrl()\n      this.ws = new WebSocket(url, this.opts)\n      const ac = new AbortController()\n      if (this.opts.signal) {\n        forwardSignal(this.opts.signal, ac)\n      }\n      this.ws.once('open', () => {\n        if (!this.initialSetup && this.opts.onReconnect) {\n          this.opts.onReconnect()\n        }\n        this.initialSetup = false\n        this.reconnects = 0\n        if (this.ws) {\n          this.startHeartbeat(this.ws)\n        }\n      })\n      this.ws.once('close', (code, reason) => {\n        if (code === CloseCode.Abnormal) {\n          // Forward into an error to distinguish from a clean close\n          ac.abort(\n            new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`),\n          )\n        }\n      })\n\n      try {\n        const wsStream = createWebSocketStream(this.ws, {\n          signal: ac.signal,\n          readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together\n        })\n        for await (const chunk of wsStream) {\n          yield chunk\n        }\n      } catch (_err) {\n        const err =\n          isErrnoException(_err) && _err.code === 'ABORT_ERR'\n            ? _err.cause\n            : _err\n        if (err instanceof DisconnectError) {\n          // We cleanly end the connection\n          this.ws?.close(err.wsCode)\n          break\n        }\n        this.ws?.close() // No-ops if already closed or closing\n        if (isReconnectable(err)) {\n          this.reconnects ??= 0 // Never reconnect with a null\n          this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup)\n          continue\n        } else {\n          throw err\n        }\n      }\n      break // Other side cleanly ended stream and disconnected\n    }\n  }\n\n  send(data: string | Buffer): Promise<void> {\n    return new Promise((resolve, reject) => {\n      if (!this.ws || this.ws.readyState !== 1 /* OPEN */) {\n        reject(new Error('WebSocket is not connected'))\n        return\n      }\n      this.ws.send(data, (err) => {\n        if (err) {\n          reject(err)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  isConnected(): boolean {\n    return this.ws !== null && this.ws.readyState === 1\n  }\n\n  startHeartbeat(ws: WebSocket) {\n    let isAlive = true\n    let heartbeatInterval: NodeJS.Timeout | null = null\n\n    const checkAlive = () => {\n      if (!isAlive) {\n        return ws.terminate()\n      }\n      isAlive = false // expect websocket to no longer be alive unless we receive a \"pong\" within the interval\n      ws.ping()\n    }\n\n    checkAlive()\n    heartbeatInterval = setInterval(\n      checkAlive,\n      this.opts.heartbeatIntervalMs ?? 10 * SECOND,\n    )\n\n    ws.on('pong', () => {\n      isAlive = true\n    })\n    ws.once('close', () => {\n      if (heartbeatInterval) {\n        clearInterval(heartbeatInterval)\n        heartbeatInterval = null\n      }\n    })\n  }\n}\n\nexport default WebSocketKeepAlive\n\nclass AbnormalCloseError extends Error {\n  code = 'EWSABNORMALCLOSE'\n}\n\nexport class DisconnectError extends Error {\n  constructor(\n    public wsCode: CloseCode = CloseCode.Policy,\n    public xrpcCode?: string,\n  ) {\n    super()\n  }\n}\n\n// https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1\nexport enum CloseCode {\n  Normal = 1000,\n  Abnormal = 1006,\n  Policy = 1008,\n}\n\nfunction isReconnectable(err: unknown): boolean {\n  // Network errors are reconnectable.\n  // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.\n  // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving\n  // an invalid message is not current reconnectable, but the user can decide to skip them.\n  if (isErrnoException(err) && typeof err.code === 'string') {\n    return networkErrorCodes.includes(err.code)\n  }\n  return false\n}\n\nconst networkErrorCodes = [\n  'EWSABNORMALCLOSE',\n  'ECONNRESET',\n  'ECONNREFUSED',\n  'ECONNABORTED',\n  'EPIPE',\n  'ETIMEDOUT',\n  'ECANCELED',\n]\n\nfunction backoffMs(n: number, maxMs: number) {\n  const baseSec = Math.pow(2, n) // 1, 2, 4, ...\n  const randSec = Math.random() - 0.5 // Random jitter between -.5 and .5 seconds\n  const ms = 1000 * (baseSec + randSec)\n  return Math.min(ms, maxMs)\n}\n\nfunction forwardSignal(signal: AbortSignal, ac: AbortController) {\n  if (signal.aborted) {\n    return ac.abort(signal.reason)\n  } else {\n    signal.addEventListener('abort', () => ac.abort(signal.reason), {\n      // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625\n      signal: ac.signal,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/ws-client/tests/keepalive.test.ts",
    "content": "import getPort from 'get-port'\nimport { WebSocketServer } from 'ws'\nimport { wait } from '@atproto/common'\nimport { CloseCode, WebSocketKeepAlive } from '../src'\n\ndescribe('WebSocketKeepAlive', () => {\n  it('uses a heartbeat to reconnect if a connection is dropped', async () => {\n    // we run a server that, on first connection, pauses for longer than the heartbeat interval (doesn't return \"pong\"s)\n    // on second connection, it sends a message and then closes\n    const port = await getPort()\n    const server = new WebSocketServer({ port })\n    let firstConnection = true\n    let firstWasClosed = false\n    server.on('connection', async (socket) => {\n      if (firstConnection === true) {\n        firstConnection = false\n        socket.pause()\n        await wait(600)\n        // shouldn't send this message because the socket would be closed\n        socket.send(Buffer.from('error message'), (err) => {\n          if (err) throw err\n          socket.close(CloseCode.Normal)\n        })\n        socket.on('close', () => {\n          firstWasClosed = true\n        })\n      } else {\n        socket.send(Buffer.from('test message'), (err) => {\n          if (err) throw err\n          socket.close(CloseCode.Normal)\n        })\n      }\n    })\n\n    const wsKeepAlive = new WebSocketKeepAlive({\n      getUrl: async () => `ws://localhost:${port}`,\n      heartbeatIntervalMs: 500,\n    })\n\n    const messages: Uint8Array[] = []\n    for await (const msg of wsKeepAlive) {\n      messages.push(msg)\n    }\n\n    expect(messages).toHaveLength(1)\n    expect(Buffer.from(messages[0]).toString()).toBe('test message')\n    expect(firstWasClosed).toBe(true)\n    server.close()\n  })\n})\n"
  },
  {
    "path": "packages/ws-client/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/ws-client/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/ws-client/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "packages/xrpc/CHANGELOG.md",
    "content": "# @atproto/xrpc\n\n## 0.7.7\n\n### Patch Changes\n\n- Updated dependencies [[`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab)]:\n  - @atproto/lexicon@0.6.0\n\n## 0.7.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.5.2\n\n## 0.7.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.5.1\n\n## 0.7.4\n\n### Patch Changes\n\n- [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Explicitly set the `redirect: \"follow\"` in `fetch()` calls\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n\n## 0.7.3\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n\n## 0.7.2\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/lexicon@0.4.13\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies [[`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/lexicon@0.4.12\n\n## 0.7.0\n\n### Minor Changes\n\n- [#3792](https://github.com/bluesky-social/atproto/pull/3792) [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `ResponseType.AuthRequired` into `ResponseType.AuthenticationRequired` to match actual error name.\n\n- [#3792](https://github.com/bluesky-social/atproto/pull/3792) [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove un-necessary `ResponseTypeNames` in favor of `ResponseType`.\n\n### Patch Changes\n\n- [#3792](https://github.com/bluesky-social/atproto/pull/3792) [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add missing `NotAcceptable` key in `ResponseTypeStrings`\n\n- Updated dependencies [[`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/lexicon@0.4.11\n\n## 0.6.12\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.10\n\n## 0.6.11\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.9\n\n## 0.6.10\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.8\n\n## 0.6.9\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/lexicon@0.4.7\n\n## 0.6.8\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/lexicon@0.4.6\n\n## 0.6.7\n\n### Patch Changes\n\n- [#3456](https://github.com/bluesky-social/atproto/pull/3456) [`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Explicitly allow \"undefined\" values in `headers`\n\n## 0.6.6\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.5\n\n## 0.6.5\n\n### Patch Changes\n\n- Updated dependencies [[`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95)]:\n  - @atproto/lexicon@0.4.4\n\n## 0.6.4\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/lexicon@0.4.3\n\n## 0.6.3\n\n### Patch Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add NotAcceptable response type\n\n- Updated dependencies [[`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94)]:\n  - @atproto/lexicon@0.4.2\n\n## 0.6.2\n\n### Patch Changes\n\n- [#2464](https://github.com/bluesky-social/atproto/pull/2464) [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add UnsupportedMediaType response type\n\n## 0.6.1\n\n### Patch Changes\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve handling of fetchHandler errors when turning them into `XrpcError`.\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ability to instantiate XrpcClient from FetchHandlerObject type\n\n- [#2714](https://github.com/bluesky-social/atproto/pull/2714) [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add global headers to `XrpcClient` instances\n\n## 0.6.0\n\n### Minor Changes\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)!\n\n  #### Motivation\n\n  The motivation for these changes is the need to make the `@atproto/api` package\n  compatible with OAuth session management. We don't have OAuth client support\n  \"launched\" and documented quite yet, so you can keep using the current app\n  password authentication system. When we do \"launch\" OAuth support and begin\n  encouraging its usage in the near future (see the [OAuth\n  Roadmap](https://github.com/bluesky-social/atproto/discussions/2656)), these\n  changes will make it easier to migrate.\n\n  In addition, the redesigned session management system fixes a bug that could\n  cause the session data to become invalid when Agent clones are created (e.g.\n  using `agent.withProxy()`).\n\n  #### New Features\n\n  We've restructured the `XrpcClient` HTTP fetch handler to be specified during\n  the instantiation of the XRPC client, through the constructor, instead of using\n  a default implementation (which was statically defined).\n\n  With this refactor, the XRPC client is now more modular and reusable. Session\n  management, retries, cryptographic signing, and other request-specific logic can\n  be implemented in the fetch handler itself rather than by the calling code.\n\n  A new abstract class named `Agent`, has been added to `@atproto/api`. This class\n  will be the base class for all Bluesky agents classes in the `@atproto`\n  ecosystem. It is meant to be extended by implementations that provide session\n  management and fetch handling.\n\n  As you adapt your code to these changes, make sure to use the `Agent` type\n  wherever you expect to receive an agent, and use the `AtpAgent` type (class)\n  only to instantiate your client. The reason for this is to be forward compatible\n  with the OAuth agent implementation that will also extend `Agent`, and not\n  `AtpAgent`.\n\n  ```ts\n  import { Agent, AtpAgent } from '@atproto/api'\n\n  async function setupAgent(\n    service: string,\n    username: string,\n    password: string,\n  ): Promise<Agent> {\n    const agent = new AtpAgent({\n      service,\n      persistSession: (evt, session) => {\n        // handle session update\n      },\n    })\n\n    await agent.login(username, password)\n\n    return agent\n  }\n  ```\n\n  ```ts\n  import { Agent } from '@atproto/api'\n\n  async function doStuffWithAgent(agent: Agent, arg: string) {\n    return agent.resolveHandle(arg)\n  }\n  ```\n\n  ```ts\n  import { Agent, AtpAgent } from '@atproto/api'\n\n  class MyClass {\n    agent: Agent\n\n    constructor() {\n      this.agent = new AtpAgent()\n    }\n  }\n  ```\n\n  #### Breaking changes\n\n  Most of the changes introduced in this version are backward-compatible. However,\n  there are a couple of breaking changes you should be aware of:\n\n  - Customizing `fetch`: The ability to customize the `fetch: FetchHandler`\n    property of `@atproto/xrpc`'s `Client` and `@atproto/api`'s `AtpAgent` classes\n    has been removed. Previously, the `fetch` property could be set to a function\n    that would be used as the fetch handler for that instance, and was initialized\n    to a default fetch handler. That property is still accessible in a read-only\n    fashion through the `fetchHandler` property and can only be set during the\n    instance creation. Attempting to set/get the `fetch` property will now result\n    in an error.\n  - The `fetch()` method, as well as WhatWG compliant `Request` and `Headers`\n    constructors, must be globally available in your environment. Use a polyfill\n    if necessary.\n  - The `AtpBaseClient` has been removed. The `AtpServiceClient` has been renamed\n    `AtpBaseClient`. Any code using either of these classes will need to be\n    updated.\n  - Instead of _wrapping_ an `XrpcClient` in its `xrpc` property, the\n    `AtpBaseClient` (formerly `AtpServiceClient`) class - created through\n    `lex-cli` - now _extends_ the `XrpcClient` class. This means that a client\n    instance now passes the `instanceof XrpcClient` check. The `xrpc` property now\n    returns the instance itself and has been deprecated.\n  - `setSessionPersistHandler` is no longer available on the `AtpAgent` or\n    `BskyAgent` classes. The session handler can only be set though the\n    `persistSession` options of the `AtpAgent` constructor.\n  - The new class hierarchy is as follows:\n    - `BskyAgent` extends `AtpAgent`: but add no functionality (hence its\n      deprecation).\n    - `AtpAgent` extends `Agent`: adds password based session management.\n    - `Agent` extends `AtpBaseClient`: this abstract class that adds syntactic sugar\n      methods `app.bsky` lexicons. It also adds abstract session management\n      methods and adds atproto specific utilities\n      (`labelers` & `proxy` headers, cloning capability)\n    - `AtpBaseClient` extends `XrpcClient`: automatically code that adds fully\n      typed lexicon defined namespaces (`instance.app.bsky.feed.getPosts()`) to\n      the `XrpcClient`.\n    - `XrpcClient` is the base class.\n\n  #### Non-breaking changes\n\n  - The `com.*` and `app.*` namespaces have been made directly available to every\n    `Agent` instances.\n\n  #### Deprecations\n\n  - The default export of the `@atproto/xrpc` package has been deprecated. Use\n    named exports instead.\n  - The `Client` and `ServiceClient` classes are now deprecated. They are replaced by a single `XrpcClient` class.\n  - The default export of the `@atproto/api` package has been deprecated. Use\n    named exports instead.\n  - The `BskyAgent` has been deprecated. Use the `AtpAgent` class instead.\n  - The `xrpc` property of the `AtpClient` instances has been deprecated. The\n    instance itself should be used as the XRPC client.\n  - The `api` property of the `AtpAgent` and `BskyAgent` instances has been\n    deprecated. Use the instance itself instead.\n\n  #### Migration\n\n  ##### The `@atproto/api` package\n\n  If you were relying on the `AtpBaseClient` solely to perform validation, use\n  this:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { AtpBaseClient, ComAtprotoSyncSubscribeRepos } from '@atproto/api'\n\n  const baseClient = new AtpBaseClient()\n\n  baseClient.xrpc.lex.assertValidXrpcMessage('io.example.doStuff', {\n    // ...\n  })\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { lexicons } from '@atproto/api'\n\n  lexicons.assertValidXrpcMessage('io.example.doStuff', {\n    // ...\n  })\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If you are extending the `BskyAgent` to perform custom `session` manipulation, define your own `Agent` subclass instead:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { BskyAgent } from '@atproto/api'\n\n  class MyAgent extends BskyAgent {\n    private accessToken?: string\n\n    async createOrRefreshSession(identifier: string, password: string) {\n      // custom logic here\n\n      this.accessToken = 'my-access-jwt'\n    }\n\n    async doStuff() {\n      return this.call('io.example.doStuff', {\n        headers: {\n          Authorization: this.accessToken && `Bearer ${this.accessToken}`,\n        },\n      })\n    }\n  }\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { Agent } from '@atproto/api'\n\n  class MyAgent extends Agent {\n    private accessToken?: string\n    public did?: string\n\n    constructor(private readonly service: string | URL) {\n      super({\n        service,\n        headers: {\n          Authorization: () =>\n            this.accessToken ? `Bearer ${this.accessToken}` : null,\n        },\n      })\n    }\n\n    clone(): MyAgent {\n      const agent = new MyAgent(this.service)\n      agent.accessToken = this.accessToken\n      agent.did = this.did\n      return this.copyInto(agent)\n    }\n\n    async createOrRefreshSession(identifier: string, password: string) {\n      // custom logic here\n\n      this.did = 'did:example:123'\n      this.accessToken = 'my-access-jwt'\n    }\n  }\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If you are monkey patching the `xrpc` service client to perform client-side rate limiting, you can now do this in the `FetchHandler` function:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { BskyAgent } from '@atproto/api'\n  import { RateLimitThreshold } from 'rate-limit-threshold'\n\n  const agent = new BskyAgent()\n  const limiter = new RateLimitThreshold(3000, 300_000)\n\n  const origCall = agent.api.xrpc.call\n  agent.api.xrpc.call = async function (...args) {\n    await limiter.wait()\n    return origCall.call(this, ...args)\n  }\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { AtpAgent } from '@atproto/api'\n  import { RateLimitThreshold } from 'rate-limit-threshold'\n\n  class LimitedAtpAgent extends AtpAgent {\n    constructor(options: AtpAgentOptions) {\n      const fetch: typeof globalThis.fetch = options.fetch ?? globalThis.fetch\n      const limiter = new RateLimitThreshold(3000, 300_000)\n\n      super({\n        ...options,\n        fetch: async (...args) => {\n          await limiter.wait()\n          return fetch(...args)\n        },\n      })\n    }\n  }\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If you configure a static `fetch` handler on the `BskyAgent` class - for example\n  to modify the headers of every request - you can now do this by providing your\n  own `fetch` function:\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import { BskyAgent, defaultFetchHandler } from '@atproto/api'\n\n  BskyAgent.configure({\n    fetch: async (httpUri, httpMethod, httpHeaders, httpReqBody) => {\n      const ua = httpHeaders['User-Agent']\n\n      httpHeaders['User-Agent'] = ua ? `${ua} ${userAgent}` : userAgent\n\n      return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody)\n    },\n  })\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { AtpAgent } from '@atproto/api'\n\n  class MyAtpAgent extends AtpAgent {\n    constructor(options: AtpAgentOptions) {\n      const fetch = options.fetch ?? globalThis.fetch\n\n      super({\n        ...options,\n        fetch: async (url, init) => {\n          const headers = new Headers(init.headers)\n\n          const ua = headersList.get('User-Agent')\n          headersList.set('User-Agent', ua ? `${ua} ${userAgent}` : userAgent)\n\n          return fetch(url, { ...init, headers })\n        },\n      })\n    }\n  }\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  <!-- <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  // before\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  // after\n  ```\n\n  </td>\n  </tr>\n  </table> -->\n\n  ##### The `@atproto/xrpc` package\n\n  The `Client` and `ServiceClient` classes are now **deprecated**. If you need a\n  lexicon based client, you should update the code to use the `XrpcClient` class\n  instead.\n\n  The deprecated `ServiceClient` class now extends the new `XrpcClient` class.\n  Because of this, the `fetch` `FetchHandler` can no longer be configured on the\n  `Client` instances (including the default export of the package). If you are not\n  relying on the `fetch` `FetchHandler`, the new changes should have no impact on\n  your code. Beware that the deprecated classes will eventually be removed in a\n  future version.\n\n  Since its use has completely changed, the `FetchHandler` type has also\n  completely changed. The new `FetchHandler` type is now a function that receives\n  a `url` pathname and a `RequestInit` object and returns a `Promise<Response>`.\n  This function is responsible for making the actual request to the server.\n\n  ```ts\n  export type FetchHandler = (\n    this: void,\n    /**\n     * The URL (pathname + query parameters) to make the request to, without the\n     * origin. The origin (protocol, hostname, and port) must be added by this\n     * {@link FetchHandler}, typically based on authentication or other factors.\n     */\n    url: string,\n    init: RequestInit,\n  ) => Promise<Response>\n  ```\n\n  A noticeable change that has been introduced is that the `uri` field of the\n  `ServiceClient` class has _not_ been ported to the new `XrpcClient` class. It is\n  now the responsibility of the `FetchHandler` to determine the full URL to make\n  the request to. The same goes for the `headers`, which should now be set through\n  the `FetchHandler` function.\n\n  If you _do_ rely on the legacy `Client.fetch` property to perform custom logic\n  upon request, you will need to migrate your code to use the new `XrpcClient`\n  class. The `XrpcClient` class has a similar API to the old `ServiceClient`\n  class, but with a few differences:\n\n  - The `Client` + `ServiceClient` duality was removed in favor of a single\n    `XrpcClient` class. This means that:\n\n    - There no longer exists a centralized lexicon registry. If you need a global\n      lexicon registry, you can maintain one yourself using a `new Lexicons` (from\n      `@atproto/lexicon`).\n    - The `FetchHandler` is no longer a statically defined property of the\n      `Client` class. Instead, it is passed as an argument to the `XrpcClient`\n      constructor.\n\n  - The `XrpcClient` constructor now requires a `FetchHandler` function as the\n    first argument, and an optional `Lexicon` instance as the second argument.\n  - The `setHeader` and `unsetHeader` methods were not ported to the new\n    `XrpcClient` class. If you need to set or unset headers, you should do so in\n    the `FetchHandler` function provided in the constructor arg.\n\n  <table>\n  <tr>\n  <td><center>Before</center></td> <td><center>After</center></td>\n  </tr>\n  <tr>\n  <td>\n\n  ```ts\n  import client, { defaultFetchHandler } from '@atproto/xrpc'\n\n  client.fetch = function (\n    httpUri: string,\n    httpMethod: string,\n    httpHeaders: Headers,\n    httpReqBody: unknown,\n  ) {\n    // Custom logic here\n    return defaultFetchHandler(httpUri, httpMethod, httpHeaders, httpReqBody)\n  }\n\n  client.addLexicon({\n    lexicon: 1,\n    id: 'io.example.doStuff',\n    defs: {},\n  })\n\n  const instance = client.service('http://my-service.com')\n\n  instance.setHeader('my-header', 'my-value')\n\n  await instance.call('io.example.doStuff')\n  ```\n\n  </td>\n  <td>\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient(\n    async (url, init) => {\n      const headers = new Headers(init.headers)\n\n      headers.set('my-header', 'my-value')\n\n      // Custom logic here\n\n      const fullUrl = new URL(url, 'http://my-service.com')\n\n      return fetch(fullUrl, { ...init, headers })\n    },\n    [\n      {\n        lexicon: 1,\n        id: 'io.example.doStuff',\n        defs: {},\n      },\n    ],\n  )\n\n  await instance.call('io.example.doStuff')\n  ```\n\n  </td>\n  </tr>\n  </table>\n\n  If your fetch handler does not require any \"custom logic\", and all you need is\n  an `XrpcClient` that makes its HTTP requests towards a static service URL, the\n  previous example can be simplified to:\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient('http://my-service.com', [\n    {\n      lexicon: 1,\n      id: 'io.example.doStuff',\n      defs: {},\n    },\n  ])\n  ```\n\n  If you need to add static headers to all requests, you can instead instantiate\n  the `XrpcClient` as follows:\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient(\n    {\n      service: 'http://my-service.com',\n      headers: {\n        'my-header': 'my-value',\n      },\n    },\n    [\n      {\n        lexicon: 1,\n        id: 'io.example.doStuff',\n        defs: {},\n      },\n    ],\n  )\n  ```\n\n  If you need the headers or service url to be dynamic, you can define them using\n  functions:\n\n  ```ts\n  import { XrpcClient } from '@atproto/xrpc'\n\n  const instance = new XrpcClient(\n    {\n      service: () => 'http://my-service.com',\n      headers: {\n        'my-header': () => 'my-value',\n        'my-ignored-header': () => null, // ignored\n      },\n    },\n    [\n      {\n        lexicon: 1,\n        id: 'io.example.doStuff',\n        defs: {},\n      },\n    ],\n  )\n  ```\n\n- [#2483](https://github.com/bluesky-social/atproto/pull/2483) [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add the ability to use `fetch()` compatible `BodyInit` body when making XRPC calls.\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e)]:\n  - @atproto/lexicon@0.4.1\n\n## 0.5.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/lexicon@0.4.0\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.3.3\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.3.2\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.3.1\n\n## 0.4.0\n\n### Minor Changes\n\n- [#1801](https://github.com/bluesky-social/atproto/pull/1801) [`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89) Thanks [@gaearon](https://github.com/gaearon)! - Methods that accepts lexicons now take `LexiconDoc` type instead of `unknown`\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.2.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.2.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.2.1\n"
  },
  {
    "path": "packages/xrpc/README.md",
    "content": "# @atproto/xrpc: atproto HTTP API Client\n\nTypeScript client library for talking to [atproto](https://atproto.com) services, with Lexicon schema validation.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/xrpc)](https://www.npmjs.com/package/@atproto/xrpc)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Usage\n\n```typescript\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\n\nconst pingLexicon = {\n  lexicon: 1,\n  id: 'io.example.ping',\n  defs: {\n    main: {\n      type: 'query',\n      description: 'Ping the server',\n      parameters: {\n        type: 'params',\n        properties: { message: { type: 'string' } },\n      },\n      output: {\n        encoding: 'application/json',\n        schema: {\n          type: 'object',\n          required: ['message'],\n          properties: { message: { type: 'string' } },\n        },\n      },\n    },\n  },\n} satisfies LexiconDoc\n\nconst xrpc = new XrpcClient('https://ping.example.com', [\n  // Any number of lexicon here\n  pingLexicon,\n])\n\nconst res1 = await xrpc.call('io.example.ping', {\n  message: 'hello world',\n})\nres1.encoding // => 'application/json'\nres1.body // => {message: 'hello world'}\n```\n\n### With a custom fetch handler\n\n```typescript\nimport { XrpcClient } from '@atproto/xrpc'\n\nconst session = {\n  serviceUrl: 'https://ping.example.com',\n  token: '<my-token>',\n  async refreshToken() {\n    const { token } = await fetch('https://auth.example.com/refresh', {\n      method: 'POST',\n      headers: { Authorization: `Bearer ${this.token}` },\n    }).then((res) => res.json())\n\n    this.token = token\n\n    return token\n  },\n}\n\nconst sessionBasedFetch: FetchHandler = async (\n  url: string,\n  init: RequestInit,\n) => {\n  const headers = new Headers(init.headers)\n\n  headers.set('Authorization', `Bearer ${session.token}`)\n\n  const response = await fetch(new URL(url, session.serviceUrl), {\n    ...init,\n    headers,\n  })\n\n  if (response.status === 401) {\n    // Refresh token, then try again.\n    const newToken = await session.refreshToken()\n    headers.set('Authorization', `Bearer ${newToken}`)\n    return fetch(new URL(url, session.serviceUrl), { ...init, headers })\n  }\n\n  return response\n}\n\nconst xrpc = new XrpcClient(sessionBasedFetch, [\n  // Any number of lexicon here\n  pingLexicon,\n])\n\n//\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/xrpc/package.json",
    "content": "{\n  \"name\": \"@atproto/xrpc\",\n  \"version\": \"0.7.7\",\n  \"license\": \"MIT\",\n  \"description\": \"atproto HTTP API (XRPC) client library\",\n  \"keywords\": [\n    \"atproto\",\n    \"xrpc\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/xrpc\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/xrpc/src/client.ts",
    "content": "import { LexiconDoc, Lexicons } from '@atproto/lexicon'\nimport { CallOptions, QueryParams } from './types'\nimport { combineHeaders } from './util'\nimport { XrpcClient } from './xrpc-client'\n\n/** @deprecated Use {@link XrpcClient} instead */\nexport class Client {\n  /** @deprecated */\n  get fetch(): never {\n    throw new Error(\n      'Client.fetch is no longer supported. Use an XrpcClient instead.',\n    )\n  }\n\n  /** @deprecated */\n  set fetch(_: never) {\n    throw new Error(\n      'Client.fetch is no longer supported. Use an XrpcClient instead.',\n    )\n  }\n\n  lex = new Lexicons()\n\n  // method calls\n  //\n\n  async call(\n    serviceUri: string | URL,\n    methodNsid: string,\n    params?: QueryParams,\n    data?: BodyInit | null,\n    opts?: CallOptions,\n  ) {\n    return this.service(serviceUri).call(methodNsid, params, data, opts)\n  }\n\n  service(serviceUri: string | URL) {\n    return new ServiceClient(this, serviceUri)\n  }\n\n  // schemas\n  // =\n\n  addLexicon(doc: LexiconDoc) {\n    this.lex.add(doc)\n  }\n\n  addLexicons(docs: LexiconDoc[]) {\n    for (const doc of docs) {\n      this.addLexicon(doc)\n    }\n  }\n\n  removeLexicon(uri: string) {\n    this.lex.remove(uri)\n  }\n}\n\n/** @deprecated Use {@link XrpcClient} instead */\nexport class ServiceClient extends XrpcClient {\n  uri: URL\n\n  constructor(\n    public baseClient: Client,\n    serviceUri: string | URL,\n  ) {\n    super(async (input, init) => {\n      const headers = combineHeaders(init.headers, Object.entries(this.headers))\n      return fetch(new URL(input, this.uri), { ...init, headers })\n    }, baseClient.lex)\n    this.uri = typeof serviceUri === 'string' ? new URL(serviceUri) : serviceUri\n  }\n}\n"
  },
  {
    "path": "packages/xrpc/src/fetch-handler.ts",
    "content": "import { Gettable } from './types'\nimport { combineHeaders } from './util'\n\nexport type FetchHandler = (\n  this: void,\n  /**\n   * The URL (pathname + query parameters) to make the request to, without the\n   * origin. The origin (protocol, hostname, and port) must be added by this\n   * {@link FetchHandler}, typically based on authentication or other factors.\n   */\n  url: string,\n  init: RequestInit,\n) => Promise<Response>\n\nexport type FetchHandlerOptions = BuildFetchHandlerOptions | string | URL\n\nexport type BuildFetchHandlerOptions = {\n  /**\n   * The service URL to make requests to. This can be a string, URL, or a\n   * function that returns a string or URL. This is useful for dynamic URLs,\n   * such as a service URL that changes based on authentication.\n   */\n  service: Gettable<string | URL>\n\n  /**\n   * Headers to be added to every request. If a function is provided, it will be\n   * called on each request to get the headers. This is useful for dynamic\n   * headers, such as authentication tokens that may expire.\n   */\n  headers?: {\n    [_ in string]?: Gettable<null | string>\n  }\n\n  /**\n   * Bring your own fetch implementation. Typically useful for testing, logging,\n   * mocking, or adding retries, session management, signatures, proof of\n   * possession (DPoP), SSRF protection, etc. Defaults to the global `fetch`\n   * function.\n   */\n  fetch?: typeof globalThis.fetch\n}\n\nexport interface FetchHandlerObject {\n  fetchHandler: (\n    this: FetchHandlerObject,\n    /**\n     * The URL (pathname + query parameters) to make the request to, without the\n     * origin. The origin (protocol, hostname, and port) must be added by this\n     * {@link FetchHandler}, typically based on authentication or other factors.\n     */\n    url: string,\n    init: RequestInit,\n  ) => Promise<Response>\n}\n\nexport function buildFetchHandler(\n  options: FetchHandler | FetchHandlerObject | FetchHandlerOptions,\n): FetchHandler {\n  // Already a fetch handler (allowed for convenience)\n  if (typeof options === 'function') return options\n  if (typeof options === 'object' && 'fetchHandler' in options) {\n    return options.fetchHandler.bind(options)\n  }\n\n  const {\n    service,\n    headers: defaultHeaders = undefined,\n    fetch = globalThis.fetch,\n  } = typeof options === 'string' || options instanceof URL\n    ? { service: options }\n    : options\n\n  if (typeof fetch !== 'function') {\n    throw new TypeError(\n      'XrpcDispatcher requires fetch() to be available in your environment.',\n    )\n  }\n\n  const defaultHeadersEntries =\n    defaultHeaders != null ? Object.entries(defaultHeaders) : undefined\n\n  return async function (url, init) {\n    const base = typeof service === 'function' ? service() : service\n    const fullUrl = new URL(url, base)\n\n    const headers = combineHeaders(init.headers, defaultHeadersEntries)\n\n    return fetch(fullUrl, { ...init, headers })\n  }\n}\n"
  },
  {
    "path": "packages/xrpc/src/index.ts",
    "content": "export * from './client'\nexport * from './fetch-handler'\nexport * from './types'\nexport * from './util'\nexport * from './xrpc-client'\n\nimport { Client } from './client'\n/** @deprecated create a local {@link XrpcClient} instance instead */\nconst defaultInst = new Client()\nexport default defaultInst\n"
  },
  {
    "path": "packages/xrpc/src/types.ts",
    "content": "import { z } from 'zod'\nimport { ValidationError } from '@atproto/lexicon'\n\nexport type QueryParams = Record<string, any>\nexport type HeadersMap = Record<string, string | undefined>\n\nexport type {\n  /** @deprecated not to be confused with the WHATWG Headers constructor */\n  HeadersMap as Headers,\n}\n\nexport type Gettable<T> = T | (() => T)\n\nexport interface CallOptions {\n  encoding?: string\n  signal?: AbortSignal\n  headers?: HeadersMap\n}\n\nexport const errorResponseBody = z.object({\n  error: z.string().optional(),\n  message: z.string().optional(),\n})\nexport type ErrorResponseBody = z.infer<typeof errorResponseBody>\n\nexport enum ResponseType {\n  /**\n   * Network issue, unable to get response from the server.\n   */\n  Unknown = 1,\n  /**\n   * Response failed lexicon validation.\n   */\n  InvalidResponse = 2,\n  Success = 200,\n  InvalidRequest = 400,\n  AuthenticationRequired = 401,\n  Forbidden = 403,\n  XRPCNotSupported = 404,\n  NotAcceptable = 406,\n  PayloadTooLarge = 413,\n  UnsupportedMediaType = 415,\n  RateLimitExceeded = 429,\n  InternalServerError = 500,\n  MethodNotImplemented = 501,\n  UpstreamFailure = 502,\n  NotEnoughResources = 503,\n  UpstreamTimeout = 504,\n}\n\nexport function httpResponseCodeToEnum(status: number): ResponseType {\n  if (status in ResponseType) {\n    return status\n  } else if (status >= 100 && status < 200) {\n    return ResponseType.XRPCNotSupported\n  } else if (status >= 200 && status < 300) {\n    return ResponseType.Success\n  } else if (status >= 300 && status < 400) {\n    return ResponseType.XRPCNotSupported\n  } else if (status >= 400 && status < 500) {\n    return ResponseType.InvalidRequest\n  } else {\n    return ResponseType.InternalServerError\n  }\n}\n\nexport function httpResponseCodeToName(status: number): string {\n  return ResponseType[httpResponseCodeToEnum(status)]\n}\n\nexport const ResponseTypeStrings = {\n  [ResponseType.Unknown]: 'Unknown',\n  [ResponseType.InvalidResponse]: 'Invalid Response',\n  [ResponseType.Success]: 'Success',\n  [ResponseType.InvalidRequest]: 'Invalid Request',\n  [ResponseType.AuthenticationRequired]: 'Authentication Required',\n  [ResponseType.Forbidden]: 'Forbidden',\n  [ResponseType.XRPCNotSupported]: 'XRPC Not Supported',\n  [ResponseType.NotAcceptable]: 'Not Acceptable',\n  [ResponseType.PayloadTooLarge]: 'Payload Too Large',\n  [ResponseType.UnsupportedMediaType]: 'Unsupported Media Type',\n  [ResponseType.RateLimitExceeded]: 'Rate Limit Exceeded',\n  [ResponseType.InternalServerError]: 'Internal Server Error',\n  [ResponseType.MethodNotImplemented]: 'Method Not Implemented',\n  [ResponseType.UpstreamFailure]: 'Upstream Failure',\n  [ResponseType.NotEnoughResources]: 'Not Enough Resources',\n  [ResponseType.UpstreamTimeout]: 'Upstream Timeout',\n} as const satisfies Record<ResponseType, string>\n\nexport function httpResponseCodeToString(status: number): string {\n  return ResponseTypeStrings[httpResponseCodeToEnum(status)]\n}\n\nexport class XRPCResponse {\n  success = true\n\n  constructor(\n    public data: any,\n    public headers: HeadersMap,\n  ) {}\n}\n\nexport class XRPCError extends Error {\n  success = false\n\n  public status: ResponseType\n\n  constructor(\n    statusCode: number,\n    public error: string = httpResponseCodeToName(statusCode),\n    message?: string,\n    public headers?: HeadersMap,\n    options?: ErrorOptions,\n  ) {\n    super(message || error || httpResponseCodeToString(statusCode), options)\n\n    this.status = httpResponseCodeToEnum(statusCode)\n\n    // Pre 2022 runtimes won't handle the \"options\" constructor argument\n    const cause = options?.cause\n    if (this.cause === undefined && cause !== undefined) {\n      this.cause = cause\n    }\n  }\n\n  static from(cause: unknown, fallbackStatus?: ResponseType): XRPCError {\n    if (cause instanceof XRPCError) {\n      return cause\n    }\n\n    // Type cast the cause to an Error if it is one\n    const causeErr = cause instanceof Error ? cause : undefined\n\n    // Try and find a Response object in the cause\n    const causeResponse: Response | undefined =\n      cause instanceof Response\n        ? cause\n        : cause?.['response'] instanceof Response\n          ? cause['response']\n          : undefined\n\n    const statusCode: unknown =\n      // Extract status code from \"http-errors\" like errors\n      causeErr?.['statusCode'] ??\n      causeErr?.['status'] ??\n      // Use the status code from the response object as fallback\n      causeResponse?.status\n\n    // Convert the status code to a ResponseType\n    const status: ResponseType =\n      typeof statusCode === 'number'\n        ? httpResponseCodeToEnum(statusCode)\n        : fallbackStatus ?? ResponseType.Unknown\n\n    const message = causeErr?.message ?? String(cause)\n\n    const headers = causeResponse\n      ? Object.fromEntries(causeResponse.headers.entries())\n      : undefined\n\n    return new XRPCError(status, undefined, message, headers, { cause })\n  }\n}\n\nexport class XRPCInvalidResponseError extends XRPCError {\n  constructor(\n    public lexiconNsid: string,\n    public validationError: ValidationError,\n    public responseBody: unknown,\n  ) {\n    super(\n      ResponseType.InvalidResponse,\n      // @NOTE: This is probably wrong and should use ResponseTypeNames instead.\n      // But it would mean a breaking change.\n      ResponseTypeStrings[ResponseType.InvalidResponse],\n      `The server gave an invalid response and may be out of date.`,\n      undefined,\n      { cause: validationError },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/xrpc/src/util.ts",
    "content": "import {\n  LexXrpcProcedure,\n  LexXrpcQuery,\n  jsonStringToLex,\n  stringifyLex,\n} from '@atproto/lexicon'\nimport {\n  CallOptions,\n  ErrorResponseBody,\n  Gettable,\n  QueryParams,\n  ResponseType,\n  XRPCError,\n  errorResponseBody,\n} from './types'\n\nconst ReadableStream =\n  globalThis.ReadableStream ||\n  (class {\n    constructor() {\n      // This anonymous class will never pass any \"instanceof\" check and cannot\n      // be instantiated.\n      throw new Error('ReadableStream is not supported in this environment')\n    }\n  } as typeof globalThis.ReadableStream)\n\nexport function isErrorResponseBody(v: unknown): v is ErrorResponseBody {\n  return errorResponseBody.safeParse(v).success\n}\n\nexport function getMethodSchemaHTTPMethod(\n  schema: LexXrpcProcedure | LexXrpcQuery,\n) {\n  if (schema.type === 'procedure') {\n    return 'post'\n  }\n  return 'get'\n}\n\nexport function constructMethodCallUri(\n  nsid: string,\n  schema: LexXrpcProcedure | LexXrpcQuery,\n  serviceUri: URL,\n  params?: QueryParams,\n): string {\n  const uri = new URL(constructMethodCallUrl(nsid, schema, params), serviceUri)\n  return uri.toString()\n}\n\nexport function constructMethodCallUrl(\n  nsid: string,\n  schema: LexXrpcProcedure | LexXrpcQuery,\n  params?: QueryParams,\n): string {\n  const pathname = `/xrpc/${encodeURIComponent(nsid)}`\n  if (!params) return pathname\n\n  const searchParams: [string, string][] = []\n\n  for (const [key, value] of Object.entries(params)) {\n    const paramSchema = schema.parameters?.properties?.[key]\n    if (!paramSchema) {\n      throw new Error(`Invalid query parameter: ${key}`)\n    }\n    if (value !== undefined) {\n      if (paramSchema.type === 'array') {\n        const values = Array.isArray(value) ? value : [value]\n        for (const val of values) {\n          searchParams.push([\n            key,\n            encodeQueryParam(paramSchema.items.type, val),\n          ])\n        }\n      } else {\n        searchParams.push([key, encodeQueryParam(paramSchema.type, value)])\n      }\n    }\n  }\n\n  if (!searchParams.length) return pathname\n\n  return `${pathname}?${new URLSearchParams(searchParams).toString()}`\n}\n\nexport function encodeQueryParam(\n  type:\n    | 'string'\n    | 'float'\n    | 'integer'\n    | 'boolean'\n    | 'datetime'\n    | 'array'\n    | 'unknown',\n  value: any,\n): string {\n  if (type === 'string' || type === 'unknown') {\n    return String(value)\n  }\n  if (type === 'float') {\n    return String(Number(value))\n  } else if (type === 'integer') {\n    return String(Number(value) | 0)\n  } else if (type === 'boolean') {\n    return value ? 'true' : 'false'\n  } else if (type === 'datetime') {\n    if (value instanceof Date) {\n      return value.toISOString()\n    }\n    return String(value)\n  }\n  throw new Error(`Unsupported query param type: ${type}`)\n}\n\nexport function constructMethodCallHeaders(\n  schema: LexXrpcProcedure | LexXrpcQuery,\n  data?: unknown,\n  opts?: CallOptions,\n): Headers {\n  // Not using `new Headers(opts?.headers)` to avoid duplicating headers values\n  // due to inconsistent casing in headers name. In case of multiple headers\n  // with the same name (but using a different case), the last one will be used.\n\n  // new Headers({ 'content-type': 'foo', 'Content-Type': 'bar' }).get('content-type')\n  // => 'foo, bar'\n  const headers = new Headers()\n\n  if (opts?.headers) {\n    for (const name in opts.headers) {\n      if (headers.has(name)) {\n        throw new TypeError(`Duplicate header: ${name}`)\n      }\n\n      const value = opts.headers[name]\n      if (value != null) {\n        headers.set(name, value)\n      }\n    }\n  }\n\n  if (schema.type === 'procedure') {\n    if (opts?.encoding) {\n      headers.set('content-type', opts.encoding)\n    } else if (!headers.has('content-type') && typeof data !== 'undefined') {\n      // Special handling of BodyInit types before falling back to JSON encoding\n      if (\n        data instanceof ArrayBuffer ||\n        data instanceof ReadableStream ||\n        ArrayBuffer.isView(data)\n      ) {\n        headers.set('content-type', 'application/octet-stream')\n      } else if (data instanceof FormData) {\n        // Note: The multipart form data boundary is missing from the header\n        // we set here, making that header invalid. This special case will be\n        // handled in encodeMethodCallBody()\n        headers.set('content-type', 'multipart/form-data')\n      } else if (data instanceof URLSearchParams) {\n        headers.set(\n          'content-type',\n          'application/x-www-form-urlencoded;charset=UTF-8',\n        )\n      } else if (isBlobLike(data)) {\n        headers.set('content-type', data.type || 'application/octet-stream')\n      } else if (typeof data === 'string') {\n        headers.set('content-type', 'text/plain;charset=UTF-8')\n      }\n      // At this point, data is not a valid BodyInit type.\n      else if (isIterable(data)) {\n        headers.set('content-type', 'application/octet-stream')\n      } else if (\n        typeof data === 'boolean' ||\n        typeof data === 'number' ||\n        typeof data === 'string' ||\n        typeof data === 'object' // covers \"null\"\n      ) {\n        headers.set('content-type', 'application/json')\n      } else {\n        // symbol, function, bigint\n        throw new XRPCError(\n          ResponseType.InvalidRequest,\n          `Unsupported data type: ${typeof data}`,\n        )\n      }\n    }\n  }\n  return headers\n}\n\nexport function combineHeaders(\n  headersInit: undefined | HeadersInit,\n  defaultHeaders?: Iterable<[string, undefined | Gettable<null | string>]>,\n): undefined | HeadersInit {\n  if (!defaultHeaders) return headersInit\n\n  let headers: Headers | undefined = undefined\n\n  for (const [name, definition] of defaultHeaders) {\n    // Ignore undefined values (allowed for convenience when using\n    // Object.entries).\n    if (definition === undefined) continue\n\n    // Lazy initialization of the headers object\n    headers ??= new Headers(headersInit)\n\n    if (headers.has(name)) continue\n\n    const value = typeof definition === 'function' ? definition() : definition\n\n    if (typeof value === 'string') headers.set(name, value)\n    else if (value === null) headers.delete(name)\n    else throw new TypeError(`Invalid \"${name}\" header value: ${typeof value}`)\n  }\n\n  return headers ?? headersInit\n}\n\nfunction isBlobLike(value: unknown): value is Blob {\n  if (value == null) return false\n  if (typeof value !== 'object') return false\n  if (typeof Blob === 'function' && value instanceof Blob) return true\n\n  // Support for Blobs provided by libraries that don't use the native Blob\n  // (e.g. fetch-blob from node-fetch).\n  // https://github.com/node-fetch/fetch-blob/blob/a1a182e5978811407bef4ea1632b517567dda01f/index.js#L233-L244\n\n  const tag = value[Symbol.toStringTag]\n  if (tag === 'Blob' || tag === 'File') {\n    return 'stream' in value && typeof value.stream === 'function'\n  }\n\n  return false\n}\n\nexport function isBodyInit(value: unknown): value is BodyInit {\n  switch (typeof value) {\n    case 'string':\n      return true\n    case 'object':\n      return (\n        value instanceof ArrayBuffer ||\n        value instanceof FormData ||\n        value instanceof URLSearchParams ||\n        value instanceof ReadableStream ||\n        ArrayBuffer.isView(value) ||\n        isBlobLike(value)\n      )\n    default:\n      return false\n  }\n}\n\nexport function isIterable(\n  value: unknown,\n): value is Iterable<unknown> | AsyncIterable<unknown> {\n  return (\n    value != null &&\n    typeof value === 'object' &&\n    (Symbol.iterator in value || Symbol.asyncIterator in value)\n  )\n}\n\nexport function encodeMethodCallBody(\n  headers: Headers,\n  data?: unknown,\n): BodyInit | undefined {\n  // Silently ignore the body if there is no content-type header.\n  const contentType = headers.get('content-type')\n  if (!contentType) {\n    return undefined\n  }\n\n  if (typeof data === 'undefined') {\n    // This error would be returned by the server, but we can catch it earlier\n    // to avoid un-necessary requests. Note that a content-length of 0 does not\n    // necessary mean that the body is \"empty\" (e.g. an empty txt file).\n    throw new XRPCError(\n      ResponseType.InvalidRequest,\n      `A request body is expected but none was provided`,\n    )\n  }\n\n  if (isBodyInit(data)) {\n    if (data instanceof FormData && contentType === 'multipart/form-data') {\n      // fetch() will encode FormData payload itself, but it won't override the\n      // content-type header if already present. This would cause the boundary\n      // to be missing from the content-type header, resulting in a 400 error.\n      // Deleting the content-type header here to let fetch() re-create it.\n      headers.delete('content-type')\n    }\n\n    // Will be encoded by the fetch API.\n    return data\n  }\n\n  if (isIterable(data)) {\n    // Note that some environments support using Iterable & AsyncIterable as the\n    // body (e.g. Node's fetch), but not all of them do (browsers).\n    return iterableToReadableStream(data)\n  }\n\n  if (contentType.startsWith('text/')) {\n    return new TextEncoder().encode(String(data))\n  }\n  if (contentType.startsWith('application/json')) {\n    const json = stringifyLex(data)\n    // Server would return a 400 error if the JSON is invalid (e.g. trying to\n    // JSONify a function, or an object that implements toJSON() poorly).\n    if (json === undefined) {\n      throw new XRPCError(\n        ResponseType.InvalidRequest,\n        `Failed to encode request body as JSON`,\n      )\n    }\n    return new TextEncoder().encode(json)\n  }\n\n  // At this point, \"data\" is not a valid BodyInit value, and we don't know how\n  // to encode it into one. Passing it to fetch would result in an error. Let's\n  // throw our own error instead.\n\n  const type =\n    !data || typeof data !== 'object'\n      ? typeof data\n      : data.constructor !== Object &&\n          typeof data.constructor === 'function' &&\n          typeof data.constructor?.name === 'string'\n        ? data.constructor.name\n        : 'object'\n\n  throw new XRPCError(\n    ResponseType.InvalidRequest,\n    `Unable to encode ${type} as ${contentType} data`,\n  )\n}\n\n/**\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static}\n */\nfunction iterableToReadableStream(\n  iterable: Iterable<unknown> | AsyncIterable<unknown>,\n): ReadableStream<Uint8Array> {\n  // Use the native ReadableStream.from() if available.\n  if ('from' in ReadableStream && typeof ReadableStream.from === 'function') {\n    return ReadableStream.from(iterable)\n  }\n\n  // If you see this error, consider using a polyfill for ReadableStream. For\n  // example, the \"web-streams-polyfill\" package:\n  // https://github.com/MattiasBuelens/web-streams-polyfill\n\n  throw new TypeError(\n    'ReadableStream.from() is not supported in this environment. ' +\n      'It is required to support using iterables as the request body. ' +\n      'Consider using a polyfill or re-write your code to use a different body type.',\n  )\n}\n\nexport function httpResponseBodyParse(\n  mimeType: string | null,\n  data: ArrayBuffer | undefined,\n): any {\n  try {\n    if (mimeType) {\n      if (mimeType.includes('application/json')) {\n        const str = new TextDecoder().decode(data)\n        return jsonStringToLex(str)\n      }\n      if (mimeType.startsWith('text/')) {\n        return new TextDecoder().decode(data)\n      }\n    }\n    if (data instanceof ArrayBuffer) {\n      return new Uint8Array(data)\n    }\n    return data\n  } catch (cause) {\n    throw new XRPCError(\n      ResponseType.InvalidResponse,\n      undefined,\n      `Failed to parse response body: ${String(cause)}`,\n      undefined,\n      { cause },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/xrpc/src/xrpc-client.ts",
    "content": "import { LexiconDoc, Lexicons, ValidationError } from '@atproto/lexicon'\nimport {\n  FetchHandler,\n  FetchHandlerObject,\n  FetchHandlerOptions,\n  buildFetchHandler,\n} from './fetch-handler'\nimport {\n  CallOptions,\n  Gettable,\n  QueryParams,\n  ResponseType,\n  XRPCError,\n  XRPCInvalidResponseError,\n  XRPCResponse,\n  httpResponseCodeToEnum,\n} from './types'\nimport {\n  combineHeaders,\n  constructMethodCallHeaders,\n  constructMethodCallUrl,\n  encodeMethodCallBody,\n  getMethodSchemaHTTPMethod,\n  httpResponseBodyParse,\n  isErrorResponseBody,\n} from './util'\n\nexport class XrpcClient {\n  readonly fetchHandler: FetchHandler\n  readonly headers = new Map<string, Gettable<null | string>>()\n  readonly lex: Lexicons\n\n  constructor(\n    fetchHandlerOpts: FetchHandler | FetchHandlerObject | FetchHandlerOptions,\n    // \"Lexicons\" is redundant here (because that class implements\n    // \"Iterable<LexiconDoc>\") but we keep it for explicitness:\n    lex: Lexicons | Iterable<LexiconDoc>,\n  ) {\n    this.fetchHandler = buildFetchHandler(fetchHandlerOpts)\n\n    this.lex = lex instanceof Lexicons ? lex : new Lexicons(lex)\n  }\n\n  setHeader(key: string, value: Gettable<null | string>): void {\n    this.headers.set(key.toLowerCase(), value)\n  }\n\n  unsetHeader(key: string): void {\n    this.headers.delete(key.toLowerCase())\n  }\n\n  clearHeaders(): void {\n    this.headers.clear()\n  }\n\n  async call(\n    methodNsid: string,\n    params?: QueryParams,\n    data?: unknown,\n    opts?: CallOptions,\n  ): Promise<XRPCResponse> {\n    const def = this.lex.getDefOrThrow(methodNsid)\n    if (!def || (def.type !== 'query' && def.type !== 'procedure')) {\n      throw new TypeError(\n        `Invalid lexicon: ${methodNsid}. Must be a query or procedure.`,\n      )\n    }\n\n    // @TODO: should we validate the params and data here?\n    // this.lex.assertValidXrpcParams(methodNsid, params)\n    // if (data !== undefined) {\n    //   this.lex.assertValidXrpcInput(methodNsid, data)\n    // }\n\n    const reqUrl = constructMethodCallUrl(methodNsid, def, params)\n    const reqMethod = getMethodSchemaHTTPMethod(def)\n    const reqHeaders = constructMethodCallHeaders(def, data, opts)\n    const reqBody = encodeMethodCallBody(reqHeaders, data)\n\n    // The duplex field is required for streaming bodies, but not yet reflected\n    // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.\n    const init: RequestInit & { duplex: 'half' } = {\n      method: reqMethod,\n      headers: combineHeaders(reqHeaders, this.headers),\n      body: reqBody,\n      duplex: 'half',\n      redirect: 'follow',\n      signal: opts?.signal,\n    }\n\n    try {\n      const response = await this.fetchHandler.call(undefined, reqUrl, init)\n\n      const resStatus = response.status\n      const resHeaders = Object.fromEntries(response.headers.entries())\n      const resBodyBytes = await response.arrayBuffer()\n      const resBody = httpResponseBodyParse(\n        response.headers.get('content-type'),\n        resBodyBytes,\n      )\n\n      const resCode = httpResponseCodeToEnum(resStatus)\n      if (resCode !== ResponseType.Success) {\n        const { error = undefined, message = undefined } =\n          resBody && isErrorResponseBody(resBody) ? resBody : {}\n        throw new XRPCError(resCode, error, message, resHeaders)\n      }\n\n      try {\n        this.lex.assertValidXrpcOutput(methodNsid, resBody)\n      } catch (e: unknown) {\n        if (e instanceof ValidationError) {\n          throw new XRPCInvalidResponseError(methodNsid, e, resBody)\n        }\n\n        throw e\n      }\n\n      return new XRPCResponse(resBody, resHeaders)\n    } catch (err) {\n      throw XRPCError.from(err)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/xrpc/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/isomorphic.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/xrpc/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [{ \"path\": \"./tsconfig.build.json\" }]\n}\n"
  },
  {
    "path": "packages/xrpc-server/CHANGELOG.md",
    "content": "# @atproto/xrpc-server\n\n## 0.10.17\n\n### Patch Changes\n\n- Updated dependencies [[`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`df8328c`](https://github.com/bluesky-social/atproto/commit/df8328c3c2f211fe16ccf58fa9f3968465cbf2b0), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786), [`6a88461`](https://github.com/bluesky-social/atproto/commit/6a88461c5aa9486269f0769b7a3d52f384581786)]:\n  - @atproto/lex-client@0.0.17\n  - @atproto/lex-schema@0.0.16\n  - @atproto/lex-data@0.0.14\n  - @atproto/common@0.5.15\n  - @atproto/lex-cbor@0.0.15\n  - @atproto/lex-json@0.0.14\n\n## 0.10.16\n\n### Patch Changes\n\n- Updated dependencies [[`5a2f884`](https://github.com/bluesky-social/atproto/commit/5a2f8847efd91252971fa243d21bd52ada7aa8f4), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`112b159`](https://github.com/bluesky-social/atproto/commit/112b159ec293a5c3fff41237474a3788fc47f9ca), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe), [`3dc3791`](https://github.com/bluesky-social/atproto/commit/3dc37915436dec7e18c7dc9dcf01b72cad53fdbe)]:\n  - @atproto/lex-client@0.0.16\n  - @atproto/lex-schema@0.0.15\n\n## 0.10.15\n\n### Patch Changes\n\n- [#4691](https://github.com/bluesky-social/atproto/pull/4691) [`e8969b6`](https://github.com/bluesky-social/atproto/commit/e8969b6b3d3fed8912be53fd72b4d5288ca34766) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add support for method defined through `@atproto/lex` in addition to `@atproto/lexicon` \"codegen\"\n\n- Updated dependencies [[`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`f7c2610`](https://github.com/bluesky-social/atproto/commit/f7c26103a6d4e24e5bedbb6fd908be140420e0dd), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7), [`52834ab`](https://github.com/bluesky-social/atproto/commit/52834aba182da8df3611fd9dff924e6c6a3973a7)]:\n  - @atproto/lex-schema@0.0.14\n  - @atproto/common@0.5.14\n  - @atproto/lex-data@0.0.13\n  - @atproto/lex-client@0.0.15\n  - @atproto/lexicon@0.6.2\n  - @atproto/lex-cbor@0.0.14\n  - @atproto/lex-json@0.0.13\n\n## 0.10.14\n\n### Patch Changes\n\n- Updated dependencies [[`66b7295`](https://github.com/bluesky-social/atproto/commit/66b72950e8bcb39cac3382116bd282b3bb692f16)]:\n  - @atproto/lex-cbor@0.0.13\n  - @atproto/common@0.5.13\n\n## 0.10.13\n\n### Patch Changes\n\n- Updated dependencies [[`ea5df64`](https://github.com/bluesky-social/atproto/commit/ea5df64db9e408d2b320f5f75eb2878aef6bc6df)]:\n  - @atproto/lex-data@0.0.12\n  - @atproto/common@0.5.12\n  - @atproto/lex-cbor@0.0.12\n\n## 0.10.12\n\n### Patch Changes\n\n- Updated dependencies [[`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015), [`ed61c62`](https://github.com/bluesky-social/atproto/commit/ed61c62f3161fcde85ee9a93f8ed339c7e06c015)]:\n  - @atproto/lex-cbor@0.0.11\n  - @atproto/lex-data@0.0.11\n  - @atproto/common@0.5.11\n\n## 0.10.11\n\n### Patch Changes\n\n- Updated dependencies [[`49b3806`](https://github.com/bluesky-social/atproto/commit/49b38069ed4b5bd1ef71e967c78e5123b1c1f6f1), [`369bb02`](https://github.com/bluesky-social/atproto/commit/369bb02b9f80f0e15e5242e54f09bd4e01117f3a)]:\n  - @atproto/common@0.5.10\n  - @atproto/lex-data@0.0.10\n  - @atproto/lex-cbor@0.0.10\n\n## 0.10.10\n\n### Patch Changes\n\n- Updated dependencies [[`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`ecf5921`](https://github.com/bluesky-social/atproto/commit/ecf59214d59d9d2530c197c0679d26e76c6a60ef), [`7310b97`](https://github.com/bluesky-social/atproto/commit/7310b9704de678a3b389a741784d58bb7f79b10b), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101), [`99963d0`](https://github.com/bluesky-social/atproto/commit/99963d002a9e030e79aed5fba700e0a68f31e101)]:\n  - @atproto/lex-cbor@0.0.9\n  - @atproto/lexicon@0.6.1\n  - @atproto/lex-data@0.0.9\n  - @atproto/common@0.5.9\n\n## 0.10.9\n\n### Patch Changes\n\n- Updated dependencies [[`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e), [`dfd4bee`](https://github.com/bluesky-social/atproto/commit/dfd4bee4abbc1a3e53531bb499a6f3169c13ed9e)]:\n  - @atproto/lex-data@0.0.8\n  - @atproto/common@0.5.8\n  - @atproto/lex-cbor@0.0.8\n\n## 0.10.8\n\n### Patch Changes\n\n- Updated dependencies [[`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe), [`d78484f`](https://github.com/bluesky-social/atproto/commit/d78484f94d8ba1352ec66030115000d515c9dafe)]:\n  - @atproto/lex-data@0.0.7\n  - @atproto/lex-cbor@0.0.7\n  - @atproto/common@0.5.7\n\n## 0.10.7\n\n### Patch Changes\n\n- Updated dependencies [[`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98), [`2f78893`](https://github.com/bluesky-social/atproto/commit/2f78893ace3bbf14d4bac36837820ddb46658c98)]:\n  - @atproto/lex-data@0.0.6\n  - @atproto/common@0.5.6\n  - @atproto/lex-cbor@0.0.6\n\n## 0.10.6\n\n### Patch Changes\n\n- Updated dependencies [[`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c), [`9af7a2d`](https://github.com/bluesky-social/atproto/commit/9af7a2d12240e91248610ce4fe7d93387733c59c)]:\n  - @atproto/lex-data@0.0.5\n  - @atproto/common@0.5.5\n  - @atproto/lex-cbor@0.0.5\n\n## 0.10.5\n\n### Patch Changes\n\n- Updated dependencies [[`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece), [`e6b6107`](https://github.com/bluesky-social/atproto/commit/e6b6107e028fee964972274b71f5da1329a7bece)]:\n  - @atproto/lex-data@0.0.4\n  - @atproto/lex-cbor@0.0.4\n  - @atproto/common@0.5.4\n\n## 0.10.4\n\n### Patch Changes\n\n- Updated dependencies [[`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957)]:\n  - @atproto/ws-client@0.0.4\n\n## 0.10.3\n\n### Patch Changes\n\n- Updated dependencies [[`693784c`](https://github.com/bluesky-social/atproto/commit/693784c3a0dee4b6a29aa1e018fce682dcae148f), [`d551b0e`](https://github.com/bluesky-social/atproto/commit/d551b0e3527714c111c3ec6e4c90ad7f46369fab), [`7e1d458`](https://github.com/bluesky-social/atproto/commit/7e1d45877bca0f615e7b1313cfcc66823b3de758)]:\n  - @atproto/lex-data@0.0.3\n  - @atproto/lexicon@0.6.0\n  - @atproto/lex-cbor@0.0.3\n  - @atproto/common@0.5.3\n  - @atproto/xrpc@0.7.7\n\n## 0.10.2\n\n### Patch Changes\n\n- Updated dependencies [[`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`1d445af`](https://github.com/bluesky-social/atproto/commit/1d445af2a7fc27eca5a45869b29266e6a2a7f3ba), [`d396de0`](https://github.com/bluesky-social/atproto/commit/d396de016d1d55d08cfad1dabd3ffd9eaeea76ea), [`688f9d6`](https://github.com/bluesky-social/atproto/commit/688f9d67597ba96d6e9c4a4aec4d394d42f4cbf4)]:\n  - @atproto/lex-data@0.0.2\n  - @atproto/lex-cbor@0.0.2\n  - @atproto/crypto@0.4.5\n  - @atproto/common@0.5.2\n\n## 0.10.1\n\n### Patch Changes\n\n- Updated dependencies [[`46550d6`](https://github.com/bluesky-social/atproto/commit/46550d6c1ffb298f57d54eb1904067b2df5a40af)]:\n  - @atproto/lex-cbor@0.0.1\n  - @atproto/lex-data@0.0.1\n  - @atproto/common@0.5.1\n\n## 0.10.0\n\n### Minor Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Enforce valid DAG-CBOR encoding of message frames\n\n### Patch Changes\n\n- [#4366](https://github.com/bluesky-social/atproto/pull/4366) [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Replace `uint8arrays` with native NodeJS `Buffer`\n\n- Updated dependencies [[`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7), [`261968fd6`](https://github.com/bluesky-social/atproto/commit/261968fd65014ded613e2bf085d61a7864b8fba7)]:\n  - @atproto/common@0.5.0\n  - @atproto/lexicon@0.5.2\n  - @atproto/crypto@0.4.4\n  - @atproto/ws-client@0.0.3\n  - @atproto/xrpc@0.7.6\n\n## 0.9.6\n\n### Patch Changes\n\n- [#4348](https://github.com/bluesky-social/atproto/pull/4348) [`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8) Thanks [@dholms](https://github.com/dholms)! - Move WebSocketKeepAlive into its own package\n\n- Updated dependencies [[`1dd20d3a8`](https://github.com/bluesky-social/atproto/commit/1dd20d3a81cda29392d8d63d13082254ec5f68a8)]:\n  - @atproto/ws-client@0.0.2\n\n## 0.9.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.12\n  - @atproto/lexicon@0.5.1\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc@0.7.5\n\n## 0.9.4\n\n### Patch Changes\n\n- Updated dependencies [[`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d), [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d)]:\n  - @atproto/lexicon@0.5.0\n  - @atproto/xrpc@0.7.4\n\n## 0.9.3\n\n### Patch Changes\n\n- Updated dependencies [[`2104d9033`](https://github.com/bluesky-social/atproto/commit/2104d9033e2e1a3a7b821c1f0c5c8ffac5832d59)]:\n  - @atproto/lexicon@0.4.14\n  - @atproto/xrpc@0.7.3\n\n## 0.9.2\n\n### Patch Changes\n\n- Updated dependencies [[`331a356ce`](https://github.com/bluesky-social/atproto/commit/331a356ce27ff1d0b24747b0c16f3b54b07a0a12)]:\n  - @atproto/lexicon@0.4.13\n  - @atproto/xrpc@0.7.2\n\n## 0.9.1\n\n### Patch Changes\n\n- [#4073](https://github.com/bluesky-social/atproto/pull/4073) [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Throw `InternalServerError` instead of a `zod` validation error when handler output is not valid\n\n- [#4073](https://github.com/bluesky-social/atproto/pull/4073) [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor performance optimizations\n\n- [#4073](https://github.com/bluesky-social/atproto/pull/4073) [`82e3e7bf6`](https://github.com/bluesky-social/atproto/commit/82e3e7bf6bd16ddee6bf684ad10160593b1b0671) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose `parseReqEncoding` utility\n\n## 0.9.0\n\n### Minor Changes\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Reorganize naming of exported types\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow stronger typing of `auth` result in method and stream handlers\n\n### Patch Changes\n\n- [#4027](https://github.com/bluesky-social/atproto/pull/4027) [`5ed4a8859`](https://github.com/bluesky-social/atproto/commit/5ed4a885963f082a642e2cfb2fcc824e708fff90) Thanks [@devinivy](https://github.com/devinivy)! - Fix json and text uploads: don't parse bodies with input encoding of _/_.\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use a generic to type RateLimiter's context\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ignore `__proto__` in query string params.\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `catchall` handler will not be triggered if the XRPC method is not a valid NSID\n\n- [#3999](https://github.com/bluesky-social/atproto/pull/3999) [`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make (typed) `params` available during auth\n\n- Updated dependencies [[`8ef976d38`](https://github.com/bluesky-social/atproto/commit/8ef976d3852df4bfa376e515e131cc0810a42f20)]:\n  - @atproto/lexicon@0.4.12\n  - @atproto/xrpc@0.7.1\n\n## 0.8.0\n\n### Minor Changes\n\n- [#3886](https://github.com/bluesky-social/atproto/pull/3886) [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `bypassSecret` and `bypassIps` from rate limiter options.\n\n- [#3886](https://github.com/bluesky-social/atproto/pull/3886) [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Now applies global rate limiter to every single route. Previously, the global rate limiter was not applied if a route defined a local rate limit option.\n\n### Patch Changes\n\n- [#3884](https://github.com/bluesky-social/atproto/pull/3884) [`b675fbbf1`](https://github.com/bluesky-social/atproto/commit/b675fbbf17e000fad2b38a52db550702830a807d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Return an error if the wrong HTTP verb is used for a known XRPC method, even when a `catchall` is provided.\n\n- [#3886](https://github.com/bluesky-social/atproto/pull/3886) [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add optional `bypass` callback to global rate limits options\n\n- [#3886](https://github.com/bluesky-social/atproto/pull/3886) [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Refactor route rate limiter builder\n\n- [#3886](https://github.com/bluesky-social/atproto/pull/3886) [`0286f7ee3`](https://github.com/bluesky-social/atproto/commit/0286f7ee3d56ae50cfe0b70add60cf4785587b3c) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Performance improvement: avoid computing rate limit bypass multiple times per request\n\n## 0.7.19\n\n### Patch Changes\n\n- [#3699](https://github.com/bluesky-social/atproto/pull/3699) [`9214bd017`](https://github.com/bluesky-social/atproto/commit/9214bd01705381aed6b5bde2900d6dc5486b6e9f) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve logging of XRPC errors\n\n## 0.7.18\n\n### Patch Changes\n\n- [#3700](https://github.com/bluesky-social/atproto/pull/3700) [`b5afb723b`](https://github.com/bluesky-social/atproto/commit/b5afb723be392d236799bbcb6a55956bd12316ba) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Consistenlty log errors\n\n- Updated dependencies [[`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`f36ab48d9`](https://github.com/bluesky-social/atproto/commit/f36ab48d910fc4a3afcd22138ba014c814beb93b), [`cc485d296`](https://github.com/bluesky-social/atproto/commit/cc485d29638488928b5efec3d4b0627040589812)]:\n  - @atproto/xrpc@0.7.0\n  - @atproto/lexicon@0.4.11\n  - @atproto/common@0.4.11\n  - @atproto/crypto@0.4.4\n\n## 0.7.17\n\n### Patch Changes\n\n- [#3765](https://github.com/bluesky-social/atproto/pull/3765) [`45354c84f`](https://github.com/bluesky-social/atproto/commit/45354c84f898d79f58c14b5c0da3661beb7353f9) Thanks [@foysalit](https://github.com/foysalit)! - Expose WebSocketKeepAlive from xrpc-server package\n\n## 0.7.16\n\n### Patch Changes\n\n- [#3789](https://github.com/bluesky-social/atproto/pull/3789) [`da168588d`](https://github.com/bluesky-social/atproto/commit/da168588de59e5048d255866205bd16c5ab5f95c) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Ensure safe HTTP code in error middleware\n\n## 0.7.15\n\n### Patch Changes\n\n- Updated dependencies [[`4db923ca1`](https://github.com/bluesky-social/atproto/commit/4db923ca1c4fadd31d41c851933659e5186ee144)]:\n  - @atproto/common@0.4.10\n  - @atproto/lexicon@0.4.10\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc@0.6.12\n\n## 0.7.14\n\n### Patch Changes\n\n- Updated dependencies [[`bdbd3c3e3`](https://github.com/bluesky-social/atproto/commit/bdbd3c3e3f8fe8476a3fecac73810554846c938f)]:\n  - @atproto/common@0.4.9\n  - @atproto/crypto@0.4.4\n\n## 0.7.13\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.9\n  - @atproto/xrpc@0.6.11\n\n## 0.7.12\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.4.8\n  - @atproto/xrpc@0.6.10\n\n## 0.7.11\n\n### Patch Changes\n\n- Updated dependencies [[`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c), [`c53d943c8`](https://github.com/bluesky-social/atproto/commit/c53d943c8be5b8886254e020970a68c0f745b14c)]:\n  - @atproto/lexicon@0.4.7\n  - @atproto/xrpc@0.6.9\n\n## 0.7.10\n\n### Patch Changes\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Apply new linting rules regarding import order\n\n- [#3220](https://github.com/bluesky-social/atproto/pull/3220) [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update NodeJS engine requirement to >=18.7.0\n\n- Updated dependencies [[`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd), [`61dc0d60e`](https://github.com/bluesky-social/atproto/commit/61dc0d60e19b88c6427a54c6d95a391b5f4da7bd)]:\n  - @atproto/lexicon@0.4.6\n  - @atproto/common@0.4.8\n  - @atproto/crypto@0.4.4\n  - @atproto/xrpc@0.6.8\n\n## 0.7.9\n\n### Patch Changes\n\n- [#3439](https://github.com/bluesky-social/atproto/pull/3439) [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Export the `ResponseType` type used in the `XRPCError` constructor\n\n- [#3439](https://github.com/bluesky-social/atproto/pull/3439) [`4f2841efe`](https://github.com/bluesky-social/atproto/commit/4f2841efeb410e710e0c8da7c9204468f6256a75) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow providing a custom `errorParser` option to XRPCServer\n\n- Updated dependencies [[`fb64d50ee`](https://github.com/bluesky-social/atproto/commit/fb64d50ee220316b9f1183e5c3259629489734c9), [`52c687a05`](https://github.com/bluesky-social/atproto/commit/52c687a05c70d5660fae1de9e1bbc6297f37f1f4)]:\n  - @atproto/xrpc@0.6.7\n  - @atproto/common@0.4.7\n  - @atproto/crypto@0.4.3\n\n## 0.7.8\n\n### Patch Changes\n\n- [#3422](https://github.com/bluesky-social/atproto/pull/3422) [`1015d9692`](https://github.com/bluesky-social/atproto/commit/1015d96925898149cc60b434561e19730a1bea12) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Fix rate limit reset binding\n\n## 0.7.7\n\n### Patch Changes\n\n- [#3348](https://github.com/bluesky-social/atproto/pull/3348) [`0832a377d`](https://github.com/bluesky-social/atproto/commit/0832a377d269584a906d5062ebb5e2e6307f9c61) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Add resetRouteRateLimits to req context\n\n## 0.7.6\n\n### Patch Changes\n\n- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:\n  - @atproto/crypto@0.4.3\n\n## 0.7.5\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.4.6\n  - @atproto/lexicon@0.4.5\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc@0.6.6\n\n## 0.7.4\n\n### Patch Changes\n\n- Updated dependencies [[`588baae12`](https://github.com/bluesky-social/atproto/commit/588baae1212a3cba3bf0d95d2f268e80513fd9c4), [`9fd65ba0f`](https://github.com/bluesky-social/atproto/commit/9fd65ba0fa4caca59fd0e6156145e4c2618e3a95)]:\n  - @atproto/common@0.4.5\n  - @atproto/lexicon@0.4.4\n  - @atproto/crypto@0.4.2\n  - @atproto/xrpc@0.6.5\n\n## 0.7.3\n\n### Patch Changes\n\n- Updated dependencies [[`bac9be2d3`](https://github.com/bluesky-social/atproto/commit/bac9be2d3ec904d1f984a871f43cf89aca17289d)]:\n  - @atproto/lexicon@0.4.3\n  - @atproto/xrpc@0.6.4\n\n## 0.7.2\n\n### Patch Changes\n\n- [#2936](https://github.com/bluesky-social/atproto/pull/2936) [`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0) Thanks [@rafaelbsky](https://github.com/rafaelbsky)! - Accept a custom verifySignatureWithKey in verifyJwt\n\n- Updated dependencies [[`1982693e3`](https://github.com/bluesky-social/atproto/commit/1982693e3ea1fef4db76ac9aca3db8dc5ebf3fe0)]:\n  - @atproto/crypto@0.4.2\n\n## 0.7.1\n\n### Patch Changes\n\n- Updated dependencies [[`4098d9890`](https://github.com/bluesky-social/atproto/commit/4098d9890173f4d6c6512f2d8994eebbf12b5e13)]:\n  - @atproto/common@0.4.4\n  - @atproto/crypto@0.4.1\n\n## 0.7.0\n\n### Minor Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use Buffer instead of ArrayBuffer in pipe through handler result\n\n### Patch Changes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow HandlerPipeThrough to be used with streams\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add ErrorOptions support to XRPCError and sub-classes\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose parseContentEncoding() util\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly decompress requests with more than one content encoding\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Improve detection, and logging, of 500 server errors.\n\n- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Optimize extraction of NSID from url\n\n- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`87a1f2426`](https://github.com/bluesky-social/atproto/commit/87a1f24262e0e644b6cf31cc7a0446d9127ffa94), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:\n  - @atproto/xrpc@0.6.3\n  - @atproto/lexicon@0.4.2\n  - @atproto/common@0.4.3\n  - @atproto/crypto@0.4.1\n\n## 0.6.4\n\n### Patch Changes\n\n- [#2464](https://github.com/bluesky-social/atproto/pull/2464) [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Properly decode request body encoding\n\n- Updated dependencies [[`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93), [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93)]:\n  - @atproto/common@0.4.2\n  - @atproto/xrpc@0.6.2\n  - @atproto/crypto@0.4.1\n\n## 0.6.3\n\n### Patch Changes\n\n- [#2743](https://github.com/bluesky-social/atproto/pull/2743) [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `iat` claim to service JWTs\n\n- [#2743](https://github.com/bluesky-social/atproto/pull/2743) [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Ensure that service auth JWT headers contain an `alg` claim, and ensure that `typ`, if present, is not an unexpected type (e.g. not an access or DPoP token).\n\n- Updated dependencies [[`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`ebb318325`](https://github.com/bluesky-social/atproto/commit/ebb318325b6e80c4ea1a93a617569da2698afe31), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c), [`d9ffa3c46`](https://github.com/bluesky-social/atproto/commit/d9ffa3c460924010d7002b616cb7a0c66111cc6c)]:\n  - @atproto/xrpc@0.6.1\n  - @atproto/crypto@0.4.1\n\n## 0.6.2\n\n### Patch Changes\n\n- [#2711](https://github.com/bluesky-social/atproto/pull/2711) [`acbacbbd5`](https://github.com/bluesky-social/atproto/commit/acbacbbd5621473b14ee7a6a3132f675806d23b1) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose the request context for AuthVerifier and StreamAuthVerifier as distinct types\n\n- [#2663](https://github.com/bluesky-social/atproto/pull/2663) [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae) Thanks [@dholms](https://github.com/dholms)! - Update lxm check error name to BadJwtLexiconMethod\n\n- [#2663](https://github.com/bluesky-social/atproto/pull/2663) [`50c0ec176`](https://github.com/bluesky-social/atproto/commit/50c0ec176c223c90e7c86e1e0c059455fecfa9ae) Thanks [@dholms](https://github.com/dholms)! - Support http.IncomingMessage input to parseReqNsid()\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:\n  - @atproto/lexicon@0.4.1\n  - @atproto/xrpc@0.6.0\n\n## 0.6.0\n\n### Minor Changes\n\n- [#2668](https://github.com/bluesky-social/atproto/pull/2668) [`dc471da26`](https://github.com/bluesky-social/atproto/commit/dc471da267955d0962a8affaf983df60d962d97c) Thanks [@dholms](https://github.com/dholms)! - Add lxm and nonce to signed service auth tokens.\n\n## 0.5.3\n\n### Patch Changes\n\n- Updated dependencies [[`acc9093d2`](https://github.com/bluesky-social/atproto/commit/acc9093d2845eba02b68fb2f9db33e4f1b59bb10)]:\n  - @atproto/common@0.4.1\n  - @atproto/crypto@0.4.0\n\n## 0.5.2\n\n### Patch Changes\n\n- [`615a96ddc`](https://github.com/bluesky-social/atproto/commit/615a96ddc2965251cfab060dfc43fc1a51ef4bff) Thanks [@devinivy](https://github.com/devinivy)! - Adjust detection of lexrpc body presence, support 0-byte bodies.\n\n## 0.5.1\n\n### Patch Changes\n\n- [#2384](https://github.com/bluesky-social/atproto/pull/2384) [`cd4fcc709`](https://github.com/bluesky-social/atproto/commit/cd4fcc709fe8d725a4af769ce21f53711fe5622a) Thanks [@dholms](https://github.com/dholms)! - Add configurable catchall\n\n## 0.5.0\n\n### Minor Changes\n\n- [#2169](https://github.com/bluesky-social/atproto/pull/2169) [`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build system rework, stop bundling dependencies.\n\n### Patch Changes\n\n- Updated dependencies [[`f689bd51a`](https://github.com/bluesky-social/atproto/commit/f689bd51a2f4e02d4eca40eb2568a1fcb95494e9)]:\n  - @atproto/lexicon@0.4.0\n  - @atproto/common@0.4.0\n  - @atproto/crypto@0.4.0\n\n## 0.4.4\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.3.4\n  - @atproto/lexicon@0.3.3\n  - @atproto/crypto@0.3.0\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.3.2\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.3.1\n\n## 0.4.1\n\n### Patch Changes\n\n- [#1839](https://github.com/bluesky-social/atproto/pull/1839) [`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5) Thanks [@dholms](https://github.com/dholms)! - Prevent signature malleability through DER-encoded signatures\n\n- Updated dependencies [[`e1b5f253`](https://github.com/bluesky-social/atproto/commit/e1b5f2537a5ba4d8b951a741269b604856028ae5)]:\n  - @atproto/crypto@0.3.0\n\n## 0.4.0\n\n### Minor Changes\n\n- [#1801](https://github.com/bluesky-social/atproto/pull/1801) [`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89) Thanks [@gaearon](https://github.com/gaearon)! - Methods that accepts lexicons now take `LexiconDoc` type instead of `unknown`\n\n### Patch Changes\n\n- [#1788](https://github.com/bluesky-social/atproto/pull/1788) [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423) Thanks [@bnewbold](https://github.com/bnewbold)! - update license to \"MIT or Apache2\"\n\n- Updated dependencies [[`ce49743d`](https://github.com/bluesky-social/atproto/commit/ce49743d7f8800d33116b88001d7b512553c2c89), [`84e2d4d2`](https://github.com/bluesky-social/atproto/commit/84e2d4d2b6694f344d80c18672c78b650189d423)]:\n  - @atproto/lexicon@0.3.0\n  - @atproto/common@0.3.3\n  - @atproto/crypto@0.2.3\n\n## 0.3.3\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.3.2\n  - @atproto/lexicon@0.2.3\n\n## 0.3.2\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/common@0.3.1\n  - @atproto/lexicon@0.2.2\n\n## 0.3.1\n\n### Patch Changes\n\n- Updated dependencies []:\n  - @atproto/lexicon@0.2.1\n"
  },
  {
    "path": "packages/xrpc-server/README.md",
    "content": "# @atproto/xrpc-server: atproto HTTP API server library\n\nTypeScript library for implementing [atproto](https://atproto.com) HTTP API services, with Lexicon schema validation.\n\n[![NPM](https://img.shields.io/npm/v/@atproto/xrpc-server)](https://www.npmjs.com/package/@atproto/xrpc-server)\n[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)\n\n## Usage\n\n```typescript\nimport { LexiconDoc } from '@atproto/lexicon'\nimport * as xrpc from '@atproto/xrpc-server'\nimport express from 'express'\n\nconst lexicons: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.ping',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: { message: { type: 'string' } },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n]\n\n// create xrpc server\nconst server = xrpc.createServer(lexicons)\n\nfunction ping(ctx: {\n  auth: xrpc.HandlerAuth | undefined\n  params: xrpc.Params\n  input: xrpc.HandlerInput | undefined\n  req: express.Request\n  res: express.Response\n}) {\n  return { encoding: 'application/json', body: { message: ctx.params.message } }\n}\n\nserver.method('io.example.ping', ping)\n\n// mount in express\nconst app = express()\napp.use(server.router)\napp.listen(8080)\n```\n\n## License\n\nThis project is dual-licensed under MIT and Apache 2.0 terms:\n\n- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT)\n- 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)\n\nDownstream 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.\n"
  },
  {
    "path": "packages/xrpc-server/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  displayName: 'XRPC Server',\n  transform: { '^.+\\\\.(j|t)s$': '@swc/jest' },\n  transformIgnorePatterns: ['/node_modules/.pnpm/(?!(get-port)@)'],\n  setupFiles: ['<rootDir>/../../jest.setup.ts'],\n  moduleNameMapper: { '^(\\\\.\\\\.?\\\\/.+)\\\\.js$': ['$1.ts', '$1.js'] },\n}\n"
  },
  {
    "path": "packages/xrpc-server/package.json",
    "content": "{\n  \"name\": \"@atproto/xrpc-server\",\n  \"version\": \"0.10.17\",\n  \"license\": \"MIT\",\n  \"description\": \"atproto HTTP API (XRPC) server library\",\n  \"keywords\": [\n    \"atproto\",\n    \"xrpc\"\n  ],\n  \"homepage\": \"https://atproto.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bluesky-social/atproto\",\n    \"directory\": \"packages/xrpc-server\"\n  },\n  \"engines\": {\n    \"node\": \">=18.7.0\"\n  },\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"tsc --build tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@atproto/common\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/lex-data\": \"workspace:^\",\n    \"@atproto/lex-cbor\": \"workspace:^\",\n    \"@atproto/lex-client\": \"workspace:^\",\n    \"@atproto/lex-json\": \"workspace:^\",\n    \"@atproto/lex-schema\": \"workspace:^\",\n    \"@atproto/lexicon\": \"workspace:^\",\n    \"@atproto/ws-client\": \"workspace:^\",\n    \"@atproto/xrpc\": \"workspace:^\",\n    \"express\": \"^4.17.2\",\n    \"http-errors\": \"^2.0.0\",\n    \"mime-types\": \"^2.1.35\",\n    \"rate-limiter-flexible\": \"^2.4.1\",\n    \"ws\": \"^8.12.0\"\n  },\n  \"devDependencies\": {\n    \"@atproto/crypto\": \"workspace:^\",\n    \"@atproto/lex\": \"workspace:^\",\n    \"@atproto/lex-document\": \"workspace:^\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-serve-static-core\": \"^4.17.36\",\n    \"@types/http-errors\": \"^2.0.4\",\n    \"@types/mime-types\": \"^2.1.4\",\n    \"@types/ws\": \"^8.5.4\",\n    \"get-port\": \"^6.1.2\",\n    \"jest\": \"^28.1.2\",\n    \"jose\": \"^4.15.4\",\n    \"key-encoder\": \"^2.0.3\",\n    \"multiformats\": \"^9.9.0\",\n    \"typescript\": \"^5.6.3\"\n  }\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/auth.ts",
    "content": "import * as common from '@atproto/common'\nimport { MINUTE } from '@atproto/common'\nimport * as crypto from '@atproto/crypto'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport { AuthRequiredError } from './errors'\n\ntype ServiceJwtParams = {\n  iss: string\n  aud: string\n  iat?: number\n  exp?: number\n  lxm: string | null\n  keypair: crypto.Keypair\n}\n\ntype ServiceJwtHeaders = {\n  alg: string\n} & Record<string, unknown>\n\ntype ServiceJwtPayload = {\n  iss: DidString | `${DidString}#${string}`\n  aud: string\n  exp: number\n  lxm?: string\n  jti?: string\n}\n\nexport const createServiceJwt = async (\n  params: ServiceJwtParams,\n): Promise<string> => {\n  const { iss, aud, keypair } = params\n  const iat = params.iat ?? Math.floor(Date.now() / 1e3)\n  const exp = params.exp ?? iat + MINUTE / 1e3\n  const lxm = params.lxm ?? undefined\n  const jti = await crypto.randomStr(16, 'hex')\n  const header = {\n    typ: 'JWT',\n    alg: keypair.jwtAlg,\n  }\n  const payload = common.noUndefinedVals({\n    iat,\n    iss,\n    aud,\n    exp,\n    lxm,\n    jti,\n  })\n  const toSignStr = `${jsonToB64Url(header)}.${jsonToB64Url(payload)}`\n  const toSign = Buffer.from(toSignStr, 'utf8')\n  const sig = Buffer.from(await keypair.sign(toSign))\n  return `${toSignStr}.${sig.toString('base64url')}`\n}\n\nexport const createServiceAuthHeaders = async (params: ServiceJwtParams) => {\n  const jwt = await createServiceJwt(params)\n  return {\n    headers: { authorization: `Bearer ${jwt}` },\n  }\n}\n\nconst jsonToB64Url = (json: Record<string, unknown>): string => {\n  return Buffer.from(JSON.stringify(json)).toString('base64url')\n}\n\nexport type VerifySignatureWithKeyFn = (\n  key: string,\n  msgBytes: Uint8Array,\n  sigBytes: Uint8Array,\n  alg: string,\n) => Promise<boolean>\n\nexport const verifyJwt = async (\n  jwtStr: string,\n  ownDid: string | null, // null indicates to skip the audience check\n  lxm: string | null, // null indicates to skip the lxm check\n  getSigningKey: (\n    iss: DidString | `${DidString}#${string}`,\n    forceRefresh: boolean,\n  ) => Promise<string>,\n  verifySignatureWithKey: VerifySignatureWithKeyFn = cryptoVerifySignatureWithKey,\n): Promise<ServiceJwtPayload> => {\n  const parts = jwtStr.split('.')\n  if (parts.length !== 3) {\n    throw new AuthRequiredError('poorly formatted jwt', 'BadJwt')\n  }\n\n  const header = parseHeader(parts[0])\n\n  // The spec does not describe what to do with the \"typ\" claim. We can,\n  // however, forbid some values that are not compatible with our use case.\n  if (\n    // service tokens are not OAuth 2.0 access tokens\n    // https://datatracker.ietf.org/doc/html/rfc9068\n    header['typ'] === 'at+jwt' ||\n    // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n    header['typ'] === 'refresh+jwt' ||\n    // \"DPoP\" proofs are not meant to be used as service tokens\n    // https://datatracker.ietf.org/doc/html/rfc9449\n    header['typ'] === 'dpop+jwt'\n  ) {\n    throw new AuthRequiredError(\n      `Invalid jwt type \"${header['typ']}\"`,\n      'BadJwtType',\n    )\n  }\n\n  const payload = parsePayload(parts[1])\n  const sig = parts[2]\n\n  if (Date.now() / 1000 > payload.exp) {\n    throw new AuthRequiredError('jwt expired', 'JwtExpired')\n  }\n  if (ownDid !== null && payload.aud !== ownDid) {\n    throw new AuthRequiredError(\n      'jwt audience does not match service did',\n      'BadJwtAudience',\n    )\n  }\n  if (lxm !== null && payload.lxm !== lxm) {\n    throw new AuthRequiredError(\n      payload.lxm !== undefined\n        ? `bad jwt lexicon method (\"lxm\"). must match: ${lxm}`\n        : `missing jwt lexicon method (\"lxm\"). must match: ${lxm}`,\n      'BadJwtLexiconMethod',\n    )\n  }\n  if (!payload.iss || !isDidStringOrService(payload.iss)) {\n    throw new AuthRequiredError('jwt iss is not a valid did', 'BadJwtIss')\n  }\n\n  const msgBytes = Buffer.from(parts.slice(0, 2).join('.'), 'utf8')\n  const sigBytes = Buffer.from(sig, 'base64url')\n\n  const signingKey = await getSigningKey(payload.iss, false)\n  const { alg } = header\n\n  let validSig: boolean\n  try {\n    validSig = await verifySignatureWithKey(signingKey, msgBytes, sigBytes, alg)\n  } catch (err) {\n    throw new AuthRequiredError(\n      'could not verify jwt signature',\n      'BadJwtSignature',\n    )\n  }\n\n  if (!validSig) {\n    // get fresh signing key in case it failed due to a recent rotation\n    const freshSigningKey = await getSigningKey(payload.iss, true)\n    try {\n      validSig =\n        freshSigningKey !== signingKey\n          ? await verifySignatureWithKey(\n              freshSigningKey,\n              msgBytes,\n              sigBytes,\n              alg,\n            )\n          : false\n    } catch (err) {\n      throw new AuthRequiredError(\n        'could not verify jwt signature',\n        'BadJwtSignature',\n      )\n    }\n  }\n\n  if (!validSig) {\n    throw new AuthRequiredError(\n      'jwt signature does not match jwt issuer',\n      'BadJwtSignature',\n    )\n  }\n\n  return payload\n}\n\nexport const cryptoVerifySignatureWithKey: VerifySignatureWithKeyFn = async (\n  key: string,\n  msgBytes: Uint8Array,\n  sigBytes: Uint8Array,\n  alg: string,\n) => {\n  return crypto.verifySignature(key, msgBytes, sigBytes, {\n    jwtAlg: alg,\n    allowMalleableSig: true,\n  })\n}\n\nconst parseB64UrlToJson = (b64: string) => {\n  return JSON.parse(Buffer.from(b64, 'base64url').toString('utf8'))\n}\n\nconst parseHeader = (b64: string): ServiceJwtHeaders => {\n  const header = parseB64UrlToJson(b64)\n  if (!header || typeof header !== 'object' || typeof header.alg !== 'string') {\n    throw new AuthRequiredError('poorly formatted jwt', 'BadJwt')\n  }\n  return header\n}\n\nconst parsePayload = (b64: string): ServiceJwtPayload => {\n  const payload = parseB64UrlToJson(b64)\n  if (\n    !payload ||\n    typeof payload !== 'object' ||\n    typeof payload.iss !== 'string' ||\n    typeof payload.aud !== 'string' ||\n    typeof payload.exp !== 'number' ||\n    (payload.lxm && typeof payload.lxm !== 'string') ||\n    (payload.nonce && typeof payload.nonce !== 'string')\n  ) {\n    throw new AuthRequiredError('poorly formatted jwt', 'BadJwt')\n  }\n  return payload\n}\n\nfunction isDidStringOrService(\n  value: string,\n): value is DidString | `${DidString}#${string}` {\n  const hashIdx = value.indexOf('#')\n  if (hashIdx === -1) {\n    return isDidString(value)\n  }\n\n  // basic validation of the fragment part\n  const fragmentLen = value.length - hashIdx - 1\n  if (fragmentLen < 1 || value.includes('#', hashIdx + 1)) {\n    return false\n  }\n\n  // Validate the did part\n  const didPart = value.slice(0, hashIdx)\n  return isDidString(didPart)\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/errors.ts",
    "content": "import { isHttpError } from 'http-errors'\nimport { LexError, XrpcError } from '@atproto/lex-client'\nimport { l } from '@atproto/lex-schema'\nimport {\n  ResponseType,\n  ResponseTypeStrings,\n  XRPCError as XRPCClientError,\n  httpResponseCodeToName,\n  httpResponseCodeToString,\n} from '@atproto/xrpc'\n\n// @NOTE Do not depend (directly or indirectly) on \"./types\" here, as it would\n// create a circular dependency.\n\nexport const errorResult = l.object({\n  status: l.integer({ minimum: 400 }),\n  error: l.optional(l.string()),\n  message: l.optional(l.string()),\n})\nexport type ErrorResult = l.Infer<typeof errorResult>\n\nexport function isErrorResult(v: unknown): v is ErrorResult {\n  return errorResult.safeParse(v).success\n}\n\nexport function excludeErrorResult<V>(v: V) {\n  if (isErrorResult(v)) throw XRPCError.fromErrorResult(v)\n  return v as Exclude<V, ErrorResult>\n}\n\nexport { ResponseType }\n\nexport class XRPCError extends Error {\n  constructor(\n    public type: ResponseType,\n    public errorMessage?: string,\n    public customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(errorMessage, options)\n  }\n\n  get statusCode(): number {\n    const { type } = this\n\n    // Fool-proofing. `new XRPCError(123.5 as number, '')` does not generate a TypeScript error.\n    // Because of this, we can end-up with any numeric value instead of an actual `ResponseType`.\n    // For legacy reasons, the `type` argument is not checked in the constructor, so we check it here.\n    if (type < 400 || type >= 600 || !Number.isFinite(type)) {\n      return 500\n    }\n\n    return type\n  }\n\n  get error(): string | undefined {\n    return this.customErrorName ?? this.typeName\n  }\n\n  get payload() {\n    return {\n      error: this.error,\n      message:\n        this.type === ResponseType.InternalServerError\n          ? this.typeStr // Do not respond with error details for 500s\n          : this.errorMessage || this.typeStr,\n    }\n  }\n\n  get typeName(): string | undefined {\n    return ResponseType[this.type]\n  }\n\n  get typeStr(): string | undefined {\n    return ResponseTypeStrings[this.type]\n  }\n\n  static fromError(cause: unknown): XRPCError {\n    if (cause instanceof XRPCError) {\n      return cause\n    }\n\n    if (cause instanceof XRPCClientError) {\n      const { error, message, type } = mapFromClientError(cause)\n      return new XRPCError(type, message, error, { cause })\n    }\n\n    if (cause instanceof XrpcError) {\n      const { status, body } = cause.toDownstreamError()\n      return new XRPCError(status, body.message, body.error, { cause })\n    }\n\n    if (cause instanceof LexError) {\n      const data = cause.toJSON()\n      const type = ResponseType.InternalServerError\n      return new XRPCError(type, data.message, data.error, { cause })\n    }\n\n    if (isHttpError(cause)) {\n      return new XRPCError(cause.status, cause.message, cause.name, { cause })\n    }\n\n    if (isErrorResult(cause)) {\n      return this.fromErrorResult(cause)\n    }\n\n    if (cause instanceof Error) {\n      return new InternalServerError(cause.message, undefined, { cause })\n    }\n\n    return new InternalServerError(\n      'Unexpected internal server error',\n      undefined,\n      { cause },\n    )\n  }\n\n  static fromErrorResult(err: ErrorResult): XRPCError {\n    return new XRPCError(err.status, err.message, err.error, { cause: err })\n  }\n}\n\nexport class InvalidRequestError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(ResponseType.InvalidRequest, errorMessage, customErrorName, options)\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.InvalidRequest\n    )\n  }\n}\n\nexport class AuthRequiredError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(\n      ResponseType.AuthenticationRequired,\n      errorMessage,\n      customErrorName,\n      options,\n    )\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.AuthenticationRequired\n    )\n  }\n}\n\nexport class ForbiddenError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(ResponseType.Forbidden, errorMessage, customErrorName, options)\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError && instance.type === ResponseType.Forbidden\n    )\n  }\n}\n\nexport class InternalServerError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(\n      ResponseType.InternalServerError,\n      errorMessage,\n      customErrorName,\n      options,\n    )\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.InternalServerError\n    )\n  }\n}\n\nexport class UpstreamFailureError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(ResponseType.UpstreamFailure, errorMessage, customErrorName, options)\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.UpstreamFailure\n    )\n  }\n}\n\nexport class NotEnoughResourcesError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(\n      ResponseType.NotEnoughResources,\n      errorMessage,\n      customErrorName,\n      options,\n    )\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.NotEnoughResources\n    )\n  }\n}\n\nexport class UpstreamTimeoutError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(ResponseType.UpstreamTimeout, errorMessage, customErrorName, options)\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.UpstreamTimeout\n    )\n  }\n}\n\nexport class MethodNotImplementedError extends XRPCError {\n  constructor(\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(\n      ResponseType.MethodNotImplemented,\n      errorMessage,\n      customErrorName,\n      options,\n    )\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.MethodNotImplemented\n    )\n  }\n}\n\n/**\n * Converts an upstream XRPC {@link ResponseType} into a downstream {@link ResponseType}.\n */\nfunction mapFromClientError(error: XRPCClientError): {\n  error: string\n  message: string\n  type: ResponseType\n} {\n  switch (error.status) {\n    case ResponseType.InvalidResponse:\n      // Upstream server returned an XRPC response that is not compatible with our internal lexicon definitions for that XRPC method.\n      // @NOTE This could be reflected as both a 500 (\"we\" are at fault) and 502 (\"they\" are at fault). Let's be gents about it.\n      return {\n        error: httpResponseCodeToName(ResponseType.InternalServerError),\n        message: httpResponseCodeToString(ResponseType.InternalServerError),\n        type: ResponseType.InternalServerError,\n      }\n    case ResponseType.Unknown:\n      // Typically a network error / unknown host\n      return {\n        error: httpResponseCodeToName(ResponseType.InternalServerError),\n        message: httpResponseCodeToString(ResponseType.InternalServerError),\n        type: ResponseType.InternalServerError,\n      }\n    default:\n      return {\n        error: error.error,\n        message: error.message,\n        type: error.status,\n      }\n  }\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/index.ts",
    "content": "export * from './auth'\nexport * from './errors'\nexport * from './rate-limiter'\nexport * from './server'\nexport * from './stream'\nexport * from './types'\n\nexport {\n  ServerTimer,\n  parseReqEncoding,\n  parseReqNsid,\n  serverTimingHeader,\n} from './util'\nexport type { ServerTiming } from './util'\n"
  },
  {
    "path": "packages/xrpc-server/src/logger.ts",
    "content": "import { subsystemLogger } from '@atproto/common'\n\nexport const LOGGER_NAME = 'xrpc-server'\n\nexport const logger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger(LOGGER_NAME)\n\nexport default logger\n"
  },
  {
    "path": "packages/xrpc-server/src/rate-limiter.ts",
    "content": "import { IncomingMessage, ServerResponse } from 'node:http'\nimport {\n  RateLimiterAbstract,\n  RateLimiterMemory,\n  RateLimiterRedis,\n  RateLimiterRes,\n} from 'rate-limiter-flexible'\nimport { ResponseType, XRPCError } from './errors'\nimport { logger } from './logger'\n\n// @NOTE Do not depend (directly or indirectly) on \"./types\" here, as it would\n// create a circular dependency.\n\nexport interface RateLimiterContext {\n  req: IncomingMessage\n  res?: ServerResponse\n}\n\nexport type CalcKeyFn<C extends RateLimiterContext = RateLimiterContext> = (\n  ctx: C,\n) => string | null\nexport type CalcPointsFn<C extends RateLimiterContext = RateLimiterContext> = (\n  ctx: C,\n) => number\n\nexport interface RateLimiterI<\n  C extends RateLimiterContext = RateLimiterContext,\n> {\n  consume: RateLimiterConsume<C>\n  reset: RateLimiterReset<C>\n}\n\nexport type RateLimiterConsumeOptions<\n  C extends RateLimiterContext = RateLimiterContext,\n> = {\n  calcKey?: CalcKeyFn<C>\n  calcPoints?: CalcPointsFn<C>\n}\n\nexport type RateLimiterConsume<\n  C extends RateLimiterContext = RateLimiterContext,\n> = (\n  ctx: C,\n  opts?: RateLimiterConsumeOptions<C>,\n) => Promise<RateLimiterStatus | RateLimitExceededError | null>\n\nexport type RateLimiterStatus = {\n  limit: number\n  duration: number\n  remainingPoints: number\n  msBeforeNext: number\n  consumedPoints: number\n  isFirstInDuration: boolean\n}\n\nexport type RateLimiterResetOptions<\n  C extends RateLimiterContext = RateLimiterContext,\n> = {\n  calcKey?: CalcKeyFn<C>\n}\n\nexport type RateLimiterReset<\n  C extends RateLimiterContext = RateLimiterContext,\n> = (ctx: C, opts?: RateLimiterResetOptions<C>) => Promise<void>\n\nexport type RateLimiterOptions<\n  C extends RateLimiterContext = RateLimiterContext,\n> = {\n  keyPrefix: string\n  durationMs: number\n  points: number\n  calcKey: CalcKeyFn<C>\n  calcPoints: CalcPointsFn<C>\n  failClosed?: boolean\n}\n\nexport class RateLimiter<C extends RateLimiterContext = RateLimiterContext>\n  implements RateLimiterI<C>\n{\n  private readonly failClosed?: boolean\n  private readonly calcKey: CalcKeyFn<C>\n  private readonly calcPoints: CalcPointsFn<C>\n\n  constructor(\n    public limiter: RateLimiterAbstract,\n    options: RateLimiterOptions<C>,\n  ) {\n    this.limiter = limiter\n    this.failClosed = options.failClosed ?? false\n    this.calcKey = options.calcKey\n    this.calcPoints = options.calcPoints\n  }\n\n  async consume(\n    ctx: C,\n    opts?: RateLimiterConsumeOptions<C>,\n  ): Promise<RateLimiterStatus | RateLimitExceededError | null> {\n    const calcKey = opts?.calcKey ?? this.calcKey\n    const key = calcKey(ctx)\n    if (key === null) {\n      return null\n    }\n    const calcPoints = opts?.calcPoints ?? this.calcPoints\n    const points = calcPoints(ctx)\n    if (points < 1) {\n      return null\n    }\n    try {\n      const res = await this.limiter.consume(key, points)\n      return formatLimiterStatus(this.limiter, res)\n    } catch (err) {\n      // yes this library rejects with a res not an error\n      if (err instanceof RateLimiterRes) {\n        const status = formatLimiterStatus(this.limiter, err)\n        return new RateLimitExceededError(status)\n      } else {\n        if (this.failClosed) {\n          throw err\n        }\n        logger.error(\n          {\n            err,\n            keyPrefix: this.limiter.keyPrefix,\n            points: this.limiter.points,\n            duration: this.limiter.duration,\n          },\n          'rate limiter failed to consume points',\n        )\n        return null\n      }\n    }\n  }\n\n  async reset(ctx: C, opts?: RateLimiterResetOptions<C>): Promise<void> {\n    const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx)\n    if (key === null) {\n      return\n    }\n\n    try {\n      await this.limiter.delete(key)\n    } catch (cause) {\n      throw new Error(`rate limiter failed to reset key: ${key}`, { cause })\n    }\n  }\n}\n\nexport class MemoryRateLimiter<\n  C extends RateLimiterContext = RateLimiterContext,\n> extends RateLimiter<C> {\n  constructor(options: RateLimiterOptions<C>) {\n    const limiter = new RateLimiterMemory({\n      keyPrefix: options.keyPrefix,\n      duration: Math.floor(options.durationMs / 1000),\n      points: options.points,\n    })\n    super(limiter, options)\n  }\n}\n\nexport class RedisRateLimiter<\n  C extends RateLimiterContext = RateLimiterContext,\n> extends RateLimiter<C> {\n  constructor(storeClient: unknown, options: RateLimiterOptions<C>) {\n    const limiter = new RateLimiterRedis({\n      storeClient,\n      keyPrefix: options.keyPrefix,\n      duration: Math.floor(options.durationMs / 1000),\n      points: options.points,\n    })\n    super(limiter, options)\n  }\n}\n\nexport const formatLimiterStatus = (\n  limiter: RateLimiterAbstract,\n  res: RateLimiterRes,\n): RateLimiterStatus => {\n  return {\n    limit: limiter.points,\n    duration: limiter.duration,\n    remainingPoints: res.remainingPoints,\n    msBeforeNext: res.msBeforeNext,\n    consumedPoints: res.consumedPoints,\n    isFirstInDuration: res.isFirstInDuration,\n  }\n}\n\nexport type WrappedRateLimiterOptions<\n  C extends RateLimiterContext = RateLimiterContext,\n> = {\n  calcKey?: CalcKeyFn<C>\n  calcPoints?: CalcPointsFn<C>\n}\n\n/**\n * Wraps a {@link RateLimiterI} instance with custom key and points calculation\n * functions.\n */\nexport class WrappedRateLimiter<\n  C extends RateLimiterContext = RateLimiterContext,\n> implements RateLimiterI<C>\n{\n  private constructor(\n    private readonly rateLimiter: RateLimiterI<C>,\n    private readonly options: Readonly<WrappedRateLimiterOptions<C>>,\n  ) {}\n\n  async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {\n    return this.rateLimiter.consume(ctx, {\n      calcKey: opts?.calcKey ?? this.options.calcKey,\n      calcPoints: opts?.calcPoints ?? this.options.calcPoints,\n    })\n  }\n\n  async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {\n    return this.rateLimiter.reset(ctx, {\n      calcKey: opts?.calcKey ?? this.options.calcKey,\n    })\n  }\n\n  static from<C extends RateLimiterContext = RateLimiterContext>(\n    rateLimiter: RateLimiterI<C>,\n    { calcKey, calcPoints }: WrappedRateLimiterOptions<C> = {},\n  ): RateLimiterI<C> {\n    if (!calcKey && !calcPoints) return rateLimiter\n    return new WrappedRateLimiter<C>(rateLimiter, { calcKey, calcPoints })\n  }\n}\n\n/**\n * Combines multiple rate limiters into one.\n *\n * The combined rate limiter will return the tightest (most restrictive) of all\n * the provided rate limiters.\n */\nexport class CombinedRateLimiter<\n  C extends RateLimiterContext = RateLimiterContext,\n> implements RateLimiterI<C>\n{\n  private constructor(\n    private readonly rateLimiters: readonly RateLimiterI<C>[],\n  ) {}\n\n  async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {\n    const promises: ReturnType<RateLimiterConsume>[] = []\n    for (const rl of this.rateLimiters) promises.push(rl.consume(ctx, opts))\n    return Promise.all(promises).then(getTightestLimit)\n  }\n\n  async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {\n    const promises: ReturnType<RateLimiterReset>[] = []\n    for (const rl of this.rateLimiters) promises.push(rl.reset(ctx, opts))\n    await Promise.all(promises)\n  }\n\n  static from<C extends RateLimiterContext = RateLimiterContext>(\n    rateLimiters: readonly RateLimiterI<C>[],\n  ): RateLimiterI<C> | undefined {\n    if (rateLimiters.length === 0) return undefined\n    if (rateLimiters.length === 1) return rateLimiters[0]\n    return new CombinedRateLimiter(rateLimiters)\n  }\n}\n\nconst getTightestLimit = (\n  resps: (RateLimiterStatus | RateLimitExceededError | null)[],\n): RateLimiterStatus | RateLimitExceededError | null => {\n  let lowest: RateLimiterStatus | null = null\n  for (const resp of resps) {\n    if (resp === null) continue\n    if (resp instanceof RateLimitExceededError) return resp\n    if (lowest === null || resp.remainingPoints < lowest.remainingPoints) {\n      lowest = resp\n    }\n  }\n  return lowest\n}\n\nexport type RouteRateLimiterOptions<\n  C extends RateLimiterContext = RateLimiterContext,\n> = {\n  bypass?: (ctx: C) => boolean\n}\n\n/**\n * Wraps a {@link RateLimiterI} interface into a class that will apply the\n * appropriate headers to the response if a limit is exceeded.\n */\nexport class RouteRateLimiter<C extends RateLimiterContext = RateLimiterContext>\n  implements RateLimiterI<C>\n{\n  constructor(\n    private readonly rateLimiter: RateLimiterI<C>,\n    private readonly options: Readonly<RouteRateLimiterOptions<C>> = {},\n  ) {}\n\n  async handle(ctx: C): Promise<RateLimiterStatus | null> {\n    const { bypass } = this.options\n    if (bypass && bypass(ctx)) {\n      return null\n    }\n\n    const result = await this.consume(ctx)\n    if (result instanceof RateLimitExceededError) {\n      setStatusHeaders(ctx, result.status)\n      throw result\n    } else if (result != null) {\n      setStatusHeaders(ctx, result)\n    }\n\n    return result\n  }\n\n  async consume(...args: Parameters<RateLimiterConsume<C>>) {\n    return this.rateLimiter.consume(...args)\n  }\n\n  async reset(...args: Parameters<RateLimiterReset<C>>) {\n    return this.rateLimiter.reset(...args)\n  }\n\n  static from<C extends RateLimiterContext = RateLimiterContext>(\n    rateLimiters: readonly RateLimiterI<C>[],\n    { bypass }: RouteRateLimiterOptions<C> = {},\n  ): RouteRateLimiter<C> | undefined {\n    const rateLimiter = CombinedRateLimiter.from(rateLimiters)\n    if (!rateLimiter) return undefined\n\n    return new RouteRateLimiter(rateLimiter, { bypass })\n  }\n}\n\nfunction setStatusHeaders<C extends RateLimiterContext = RateLimiterContext>(\n  ctx: C,\n  status: RateLimiterStatus,\n) {\n  const resetAt = Math.floor((Date.now() + status.msBeforeNext) / 1e3)\n\n  ctx.res?.setHeader('RateLimit-Limit', status.limit)\n  ctx.res?.setHeader('RateLimit-Reset', resetAt)\n  ctx.res?.setHeader('RateLimit-Remaining', status.remainingPoints)\n  ctx.res?.setHeader('RateLimit-Policy', `${status.limit};w=${status.duration}`)\n}\n\nexport class RateLimitExceededError extends XRPCError {\n  constructor(\n    public status: RateLimiterStatus,\n    errorMessage?: string,\n    customErrorName?: string,\n    options?: ErrorOptions,\n  ) {\n    super(\n      ResponseType.RateLimitExceeded,\n      errorMessage,\n      customErrorName,\n      options,\n    )\n  }\n\n  [Symbol.hasInstance](instance: unknown): boolean {\n    return (\n      instance instanceof XRPCError &&\n      instance.type === ResponseType.RateLimitExceeded\n    )\n  }\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/server.ts",
    "content": "import assert from 'node:assert'\nimport { IncomingMessage } from 'node:http'\nimport { Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport express, {\n  Application,\n  ErrorRequestHandler,\n  Express,\n  RequestHandler,\n  Router,\n} from 'express'\nimport { LexValue } from '@atproto/lex-data'\nimport { l } from '@atproto/lex-schema'\nimport {\n  LexXrpcProcedure,\n  LexXrpcQuery,\n  LexXrpcSubscription,\n  LexiconDoc,\n  Lexicons,\n  lexToJson,\n} from '@atproto/lexicon'\nimport {\n  InternalServerError,\n  InvalidRequestError,\n  MethodNotImplementedError,\n  XRPCError,\n  excludeErrorResult,\n} from './errors'\nimport log, { LOGGER_NAME } from './logger'\nimport {\n  CalcKeyFn,\n  CalcPointsFn,\n  RateLimiterI,\n  RateLimiterOptions,\n  RouteRateLimiter,\n  WrappedRateLimiter,\n} from './rate-limiter'\nimport { ErrorFrame, Frame, MessageFrame, XrpcStreamServer } from './stream'\nimport {\n  Auth,\n  AuthResult,\n  AuthVerifier,\n  CatchallHandler,\n  HandlerContext,\n  Input,\n  LexMethodConfig,\n  LexMethodHandler,\n  LexMethodInput,\n  LexMethodOutput,\n  LexMethodParams,\n  LexSubscriptionConfig,\n  LexSubscriptionHandler,\n  MethodAuthContext,\n  MethodConfig,\n  MethodConfigOrHandler,\n  MethodHandler,\n  Options,\n  Output,\n  Params,\n  RouteOptions,\n  ServerRateLimitDescription,\n  StreamAuthContext,\n  StreamConfig,\n  StreamConfigOrHandler,\n  StreamContext,\n  isHandlerPipeThroughBuffer,\n  isHandlerPipeThroughStream,\n  isHandlerSuccess,\n  isSharedRateLimitOpts,\n} from './types'\nimport {\n  AuthVerifierInternal,\n  InputVerifierInternal,\n  OutputVerifierInternal,\n  ParamsVerifierInternal,\n  asArray,\n  createLexiconInputVerifier,\n  createLexiconOutputVerifier,\n  createLexiconParamsVerifier,\n  createSchemaInputVerifier,\n  createSchemaOutputVerifier,\n  createSchemaParamsVerifier,\n  extractUrlNsid,\n  setHeaders,\n} from './util'\n\nexport function createServer(lexicons?: LexiconDoc[], options?: Options) {\n  return new Server(lexicons, options)\n}\n\nexport class Server {\n  router: Express = express()\n  routes: Router = Router()\n  subscriptions = new Map<string, XrpcStreamServer>()\n  lex = new Lexicons()\n  options: Options\n  globalRateLimiter?: RouteRateLimiter<HandlerContext>\n  sharedRateLimiters?: Map<string, RateLimiterI<HandlerContext>>\n\n  constructor(lexicons?: LexiconDoc[], opts: Options = {}) {\n    if (lexicons) {\n      this.addLexicons(lexicons)\n    }\n    this.router.use(this.routes)\n    this.router.use(this.catchall)\n    this.router.use(createErrorMiddleware(opts))\n    this.router.once('mount', (app: Application) => {\n      this.enableStreamingOnListen(app)\n    })\n    this.options = opts\n\n    if (opts.rateLimits) {\n      const { global, shared, creator, bypass } = opts.rateLimits\n\n      if (global) {\n        this.globalRateLimiter = RouteRateLimiter.from(\n          global.map((options) => creator(buildRateLimiterOptions(options))),\n          { bypass },\n        )\n      }\n\n      if (shared) {\n        this.sharedRateLimiters = new Map(\n          shared.map((options) => [\n            options.name,\n            creator(buildRateLimiterOptions(options)),\n          ]),\n        )\n      }\n    }\n  }\n\n  listen(port: number, callback?: () => void) {\n    return this.router.listen(port, callback)\n  }\n\n  // handlers\n  // =\n\n  // Routes with auth\n  add<M extends l.Procedure | l.Query | l.Subscription, A extends AuthResult>(\n    ns: l.Main<M>,\n    config: M extends l.Procedure | l.Query\n      ? LexMethodConfig<M, A> & { auth: Exclude<unknown, void> }\n      : M extends l.Subscription\n        ? LexSubscriptionConfig<M, A> & { auth: Exclude<unknown, void> }\n        : never,\n  ): void\n  // Routes without auth\n  add<M extends l.Procedure | l.Query | l.Subscription>(\n    ns: l.Main<M>,\n    config: M extends l.Procedure | l.Query\n      ? LexMethodConfig<M, void> | LexMethodHandler<M, void>\n      : M extends l.Subscription\n        ? LexSubscriptionConfig<M, void> | LexSubscriptionHandler<M, void>\n        : never,\n  ): void\n  add<M extends l.Procedure | l.Query | l.Subscription, A extends Auth>(\n    ns: l.Main<M>,\n    configOfHandler: M extends l.Procedure | l.Query\n      ? LexMethodConfig<M, A> | LexMethodHandler<M, A>\n      : M extends l.Subscription\n        ? LexSubscriptionConfig<M, A> | LexSubscriptionHandler<M, A>\n        : never,\n  ): void {\n    const schema = l.getMain(ns)\n    const config =\n      typeof configOfHandler === 'function'\n        ? { handler: configOfHandler }\n        : configOfHandler\n    switch (schema.type) {\n      case 'procedure':\n        return this.addProcedureSchema(\n          schema,\n          config as LexMethodConfig<l.Procedure, A>,\n        )\n      case 'query':\n        return this.addQuerySchema(\n          schema,\n          config as LexMethodConfig<l.Query, A>,\n        )\n      case 'subscription':\n        return this.addSubscriptionSchema(\n          schema,\n          config as LexSubscriptionConfig<l.Subscription, A>,\n        )\n      default:\n        throw new TypeError(\n          // @ts-expect-error should never happen\n          `Unsupported schema ${schema.nsid} of type ${schema.type}`,\n        )\n    }\n  }\n\n  protected addProcedureSchema<M extends l.Procedure, A extends Auth>(\n    schema: M,\n    config: LexMethodConfig<M, A>,\n  ): void {\n    this.routes.post(\n      `/xrpc/${schema.nsid}`,\n      this.createHandlerInternal<\n        A,\n        LexMethodParams<M>,\n        LexMethodInput<M>,\n        LexMethodOutput<M>\n      >(\n        this.createAuthVerifier(config),\n        this.createSchemaParamsVerifier(schema),\n        this.createSchemaInputVerifier(schema, config.opts),\n        this.createRouteRateLimiter(schema.nsid, config),\n        config.handler,\n        this.createSchemaOutputVerifier(schema),\n      ),\n    )\n  }\n\n  protected addQuerySchema<M extends l.Query, A extends Auth>(\n    schema: M,\n    config: LexMethodConfig<M, A>,\n  ): void {\n    this.routes.get(\n      `/xrpc/${schema.nsid}`,\n      this.createHandlerInternal<\n        A,\n        LexMethodParams<M>,\n        LexMethodInput<M>,\n        LexMethodOutput<M>\n      >(\n        this.createAuthVerifier(config),\n        this.createSchemaParamsVerifier(schema),\n        this.createSchemaInputVerifier(schema, config.opts),\n        this.createRouteRateLimiter(schema.nsid, config),\n        config.handler,\n        this.createSchemaOutputVerifier(schema),\n      ),\n    )\n  }\n\n  protected addSubscriptionSchema<\n    M extends l.Subscription,\n    A extends Auth = void,\n  >(schema: M, config: LexSubscriptionConfig<M, A>): void {\n    const { handler } = config\n    const messageSchema =\n      this.options.validateResponse === false ? undefined : schema.message\n\n    return this.addSubscriptionInternal(\n      schema.nsid,\n      this.createSchemaParamsVerifier(schema),\n      this.createAuthVerifier(config),\n      // Wrap the handler to validate outgoing messages if a message schema\n      // is available\n      messageSchema\n        ? async function* (ctx) {\n            for await (const item of handler(ctx)) {\n              if (item instanceof Frame) {\n                messageSchema.validate(item.body)\n                yield item\n              } else {\n                yield messageSchema.validate(item)\n              }\n            }\n          }\n        : handler,\n    )\n  }\n\n  method<A extends Auth = Auth>(\n    nsid: string,\n    configOrFn: MethodConfigOrHandler<A>,\n  ): void {\n    this.addMethod(nsid, configOrFn)\n  }\n\n  addMethod<A extends Auth = Auth>(\n    nsid: string,\n    configOrFn: MethodConfigOrHandler<A>,\n  ) {\n    const config =\n      typeof configOrFn === 'function' ? { handler: configOrFn } : configOrFn\n    const def = this.lex.getDef(nsid)\n    if (def?.type === 'query' || def?.type === 'procedure') {\n      this.addRoute(nsid, def, config)\n    } else {\n      throw new Error(`Lex def for ${nsid} is not a query or a procedure`)\n    }\n  }\n\n  streamMethod<A extends Auth = Auth>(\n    nsid: string,\n    configOrFn: StreamConfigOrHandler<A, Params>,\n  ) {\n    this.addStreamMethod(nsid, configOrFn)\n  }\n\n  addStreamMethod<A extends Auth = Auth>(\n    nsid: string,\n    configOrFn: StreamConfigOrHandler<A, Params>,\n  ) {\n    const config =\n      typeof configOrFn === 'function' ? { handler: configOrFn } : configOrFn\n    const def = this.lex.getDef(nsid)\n    if (def?.type === 'subscription') {\n      this.addSubscription(nsid, def, config)\n    } else {\n      throw new Error(`Lex def for ${nsid} is not a subscription`)\n    }\n  }\n\n  // schemas\n  // =\n\n  addLexicon(doc: LexiconDoc) {\n    this.lex.add(doc)\n  }\n\n  addLexicons(docs: LexiconDoc[]) {\n    for (const doc of docs) {\n      this.addLexicon(doc)\n    }\n  }\n\n  // http\n  // =\n\n  protected async addRoute<A extends Auth = Auth>(\n    nsid: string,\n    def: LexXrpcQuery | LexXrpcProcedure,\n    config: MethodConfig<A>,\n  ) {\n    const path = `/xrpc/${nsid}`\n    const handler = this.createHandler(nsid, def, config)\n\n    if (def.type === 'procedure') {\n      this.routes.post(path, handler)\n    } else {\n      this.routes.get(path, handler)\n    }\n  }\n\n  catchall: CatchallHandler = async (req, res, next) => {\n    // catchall handler only applies to XRPC routes\n    if (!req.url.startsWith('/xrpc/')) return next()\n\n    // Validate the NSID\n    const nsid = extractUrlNsid(req.url)\n    if (!nsid) {\n      return next(new InvalidRequestError('invalid xrpc path'))\n    }\n\n    if (this.globalRateLimiter) {\n      try {\n        await this.globalRateLimiter.handle({\n          req,\n          res,\n          auth: undefined,\n          params: {},\n          input: undefined,\n          async resetRouteRateLimits() {},\n        })\n      } catch (err) {\n        return next(err)\n      }\n    }\n\n    // Ensure that known XRPC methods are only called with the correct HTTP\n    // method.\n    const def = this.lex.getDef(nsid)\n    if (def) {\n      const expectedMethod =\n        def.type === 'procedure' ? 'POST' : def.type === 'query' ? 'GET' : null\n      if (expectedMethod != null && expectedMethod !== req.method) {\n        return next(\n          new InvalidRequestError(\n            `Incorrect HTTP method (${req.method}) expected ${expectedMethod}`,\n          ),\n        )\n      }\n    }\n\n    if (this.options.catchall) {\n      this.options.catchall.call(null, req, res, next)\n    } else if (!def) {\n      next(new MethodNotImplementedError())\n    } else {\n      next()\n    }\n  }\n\n  createHandler<\n    A extends Auth = Auth,\n    P extends Params = Params,\n    I extends Input = Input,\n    O extends Output = Output,\n  >(\n    nsid: string,\n    def: LexXrpcQuery | LexXrpcProcedure,\n    cfg: MethodConfig<A, P, I, O>,\n  ): RequestHandler {\n    return this.createHandlerInternal<A, P, I, O>(\n      this.createAuthVerifier(cfg),\n      this.createLexiconParamsVerifier<P>(nsid, def),\n      this.createLexiconInputVerifier<I>(nsid, def, cfg.opts),\n      this.createRouteRateLimiter(nsid, cfg),\n      cfg.handler,\n      this.createLexiconOutputVerifier<O>(nsid, def),\n    )\n  }\n\n  protected createHandlerInternal<\n    A extends Auth,\n    P extends Params,\n    I extends Input,\n    O extends Output,\n  >(\n    authVerifier: AuthVerifierInternal<MethodAuthContext<P>, A> | null,\n    paramsVerifier: ParamsVerifierInternal<P>,\n    inputVerifier: InputVerifierInternal<I>,\n    routeLimiter: RouteRateLimiter<HandlerContext<A, P, I>> | undefined,\n    handler: MethodHandler<A, P, I, O>,\n    validateResOutput: null | OutputVerifierInternal<O>,\n  ): RequestHandler {\n    return async function (req, res, next) {\n      try {\n        // parse & validate params\n        const params = paramsVerifier(req)\n\n        // authenticate request\n        const auth: A = authVerifier\n          ? await authVerifier({ req, res, params })\n          : (undefined as A)\n\n        // parse & validate input\n        const input: I = await inputVerifier(req, res)\n\n        const ctx: HandlerContext<A, P, I> = {\n          params,\n          input,\n          auth,\n          req,\n          res,\n          resetRouteRateLimits: async () => routeLimiter?.reset(ctx),\n        }\n\n        // handle rate limits\n        if (routeLimiter) await routeLimiter.handle(ctx)\n\n        // run the handler\n        const output = (await handler(ctx)) as O\n\n        if (!output) {\n          validateResOutput?.(output)\n          res.status(200)\n          res.end()\n        } else if (isHandlerPipeThroughStream(output)) {\n          setHeaders(res, output.headers)\n          res.status(200)\n          res.header('Content-Type', output.encoding)\n          await pipeline(output.stream, res)\n        } else if (isHandlerPipeThroughBuffer(output)) {\n          setHeaders(res, output.headers)\n          res.status(200)\n          res.header('Content-Type', output.encoding)\n          res.end(output.buffer)\n        } else if (isHandlerSuccess(output)) {\n          validateResOutput?.(output)\n\n          res.status(200)\n          setHeaders(res, output.headers)\n\n          const encoding =\n            output.encoding === 'json' ? 'application/json' : output.encoding\n\n          res.header('Content-Type', encoding)\n\n          if (output.body instanceof Readable) {\n            // The \"Readable\" check comes first to avoid calling \"lexToJson\" on\n            // a stream, which would be a bug.\n            await pipeline(output.body, res)\n          } else if (encoding === 'application/json') {\n            const json = lexToJson(output.body)\n            res.json(json)\n          } else {\n            res.send(\n              Buffer.isBuffer(output.body)\n                ? output.body\n                : output.body instanceof Uint8Array\n                  ? Buffer.from(output.body)\n                  : output.body,\n            )\n          }\n        } else {\n          next(XRPCError.fromError(output))\n        }\n      } catch (err: unknown) {\n        // Express will not call the next middleware (errorMiddleware in this case)\n        // if the value passed to next is false-y (e.g. null, undefined, 0).\n        // Hence we replace it with an InternalServerError.\n        if (!err) {\n          next(new InternalServerError())\n        } else {\n          next(err)\n        }\n      }\n    }\n  }\n\n  protected async addSubscription<A extends Auth = Auth>(\n    nsid: string,\n    def: LexXrpcSubscription,\n    cfg: StreamConfig<A, Params>,\n  ) {\n    this.addSubscriptionInternal(\n      nsid,\n      this.createLexiconParamsVerifier(nsid, def),\n      this.createAuthVerifier(cfg),\n      // @NOTE outgoing messages are not validated against the lexicon schema\n      // (unlike the handlers for @atproto/lex based subscriptions)\n      cfg.handler,\n    )\n  }\n\n  protected addSubscriptionInternal<A extends Auth, P extends Params>(\n    nsid: string,\n    paramsVerifier: ParamsVerifierInternal<P>,\n    authVerifier: AuthVerifierInternal<StreamAuthContext<P>, A> | null,\n    handler: (ctx: StreamContext<A, P>) => AsyncIterable<unknown>,\n  ) {\n    this.subscriptions.set(\n      nsid,\n      new XrpcStreamServer({\n        noServer: true,\n        handler: async function* (req, signal) {\n          try {\n            // validate request\n            const params = paramsVerifier(req)\n\n            // authenticate request\n            const auth = authVerifier\n              ? await authVerifier({ req, params })\n              : (undefined as A)\n\n            // stream\n            for await (const item of handler({ req, params, auth, signal })) {\n              yield item instanceof Frame\n                ? item\n                : MessageFrame.fromLexValue(item as LexValue, nsid)\n            }\n          } catch (err) {\n            yield ErrorFrame.fromError(err)\n          }\n        },\n      }),\n    )\n  }\n\n  private createAuthVerifier<C, A extends AuthResult>(cfg: {\n    auth?: AuthVerifier<C, A>\n  }): null | AuthVerifierInternal<C, A> {\n    const { auth } = cfg\n    if (!auth) return null\n\n    return async (ctx) => {\n      const result = await auth(ctx)\n      return excludeErrorResult(result)\n    }\n  }\n\n  private createLexiconParamsVerifier<P extends Params = Params>(\n    nsid: string,\n    def: LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription,\n  ) {\n    return createLexiconParamsVerifier<P>(nsid, def, this.lex)\n  }\n\n  private createLexiconInputVerifier<I extends Input = Input>(\n    nsid: string,\n    def: LexXrpcQuery | LexXrpcProcedure,\n    opts?: RouteOptions,\n  ): InputVerifierInternal<I> {\n    return createLexiconInputVerifier(\n      nsid,\n      def,\n      {\n        blobLimit: opts?.blobLimit ?? this.options.payload?.blobLimit,\n        jsonLimit: opts?.jsonLimit ?? this.options.payload?.jsonLimit,\n        textLimit: opts?.textLimit ?? this.options.payload?.textLimit,\n      },\n      this.lex,\n    )\n  }\n\n  private createLexiconOutputVerifier<O extends Output = Output>(\n    nsid: string,\n    def: LexXrpcQuery | LexXrpcProcedure,\n  ): null | OutputVerifierInternal<O> {\n    if (this.options.validateResponse === false) {\n      return null\n    }\n    return createLexiconOutputVerifier(nsid, def, this.lex)\n  }\n\n  private createSchemaParamsVerifier<\n    M extends l.Procedure | l.Query | l.Subscription,\n  >(ns: l.Main<M>): ParamsVerifierInternal<LexMethodParams<M>> {\n    return createSchemaParamsVerifier<M>(ns)\n  }\n\n  private createSchemaInputVerifier<M extends l.Procedure | l.Query>(\n    ns: l.Main<M>,\n    opts?: RouteOptions,\n  ): InputVerifierInternal<LexMethodInput<M>> {\n    return createSchemaInputVerifier<M>(ns, {\n      blobLimit: opts?.blobLimit ?? this.options.payload?.blobLimit,\n      jsonLimit: opts?.jsonLimit ?? this.options.payload?.jsonLimit,\n      textLimit: opts?.textLimit ?? this.options.payload?.textLimit,\n    })\n  }\n\n  private createSchemaOutputVerifier<M extends l.Procedure | l.Query>(\n    ns: l.Main<M>,\n  ): null | OutputVerifierInternal<LexMethodOutput<M>> {\n    if (this.options.validateResponse === false) {\n      return null\n    }\n    return createSchemaOutputVerifier<M>(ns)\n  }\n\n  private enableStreamingOnListen(app: Application) {\n    const _listen = app.listen\n    app.listen = (...args) => {\n      // @ts-ignore the args spread\n      const httpServer = _listen.call(app, ...args)\n      httpServer.on('upgrade', (req, socket, head) => {\n        const nsid = req.url ? extractUrlNsid(req.url) : undefined\n        const sub = nsid ? this.subscriptions.get(nsid) : undefined\n        if (!sub) return socket.destroy()\n        sub.wss.handleUpgrade(req, socket, head, (ws) =>\n          sub.wss.emit('connection', ws, req),\n        )\n      })\n      return httpServer\n    }\n  }\n\n  private createRouteRateLimiter<\n    A extends Auth,\n    P extends Params,\n    I extends Input,\n    O extends Output,\n  >(\n    nsid: string,\n    config: MethodConfig<A, P, I, O>,\n  ): RouteRateLimiter<HandlerContext<A, P, I>> | undefined {\n    // @NOTE global & shared rate limiters are instantiated with a context of\n    // HandlerContext which is compatible (more generic) with the context of\n    // this route specific rate limiters (C). For this reason, it's safe to\n    // cast these with an `any` context\n\n    const globalRateLimiter = this.globalRateLimiter as\n      | RouteRateLimiter<any>\n      | undefined\n\n    // No route specific rate limiting configured, use the global rate limiter.\n    if (!config.rateLimit) return globalRateLimiter\n\n    const { rateLimits } = this.options\n\n    // @NOTE Silently ignore creation of route specific rate limiter if the\n    // `rateLimits` options was not provided to the constructor.\n    if (!rateLimits) return globalRateLimiter\n\n    const { creator, bypass } = rateLimits\n\n    const rateLimiters = asArray(config.rateLimit).map((options, i) => {\n      if (isSharedRateLimitOpts(options)) {\n        const rateLimiter = this.sharedRateLimiters?.get(options.name)\n\n        // The route config references a shared rate limiter that does not\n        // exist. This is a configuration error.\n        assert(rateLimiter, `Shared rate limiter \"${options.name}\" not defined`)\n\n        return WrappedRateLimiter.from<any>(rateLimiter, options)\n      } else {\n        return creator({\n          ...options,\n          calcKey: options.calcKey ?? defaultKey,\n          calcPoints: options.calcPoints ?? defaultPoints,\n          keyPrefix: `${nsid}-${i}`,\n        })\n      }\n    })\n\n    // If the route config contains an empty array, use global rate limiter.\n    if (!rateLimiters.length) return globalRateLimiter\n\n    // The global rate limiter (if present) should be applied in addition to\n    // the route specific rate limiters.\n    if (globalRateLimiter) rateLimiters.push(globalRateLimiter)\n\n    return RouteRateLimiter.from<any>(rateLimiters, { bypass })\n  }\n}\n\nfunction createErrorMiddleware({\n  errorParser = (err) => XRPCError.fromError(err),\n}: Options): ErrorRequestHandler {\n  return (err, req, res, next) => {\n    const nsid = extractUrlNsid(req.originalUrl)\n    const xrpcError = errorParser(err)\n\n    // Use the request's logger (if available) to benefit from request context\n    // (id, timing) and logging configuration (serialization, etc.).\n    const logger = isPinoHttpRequest(req) ? req.log : log\n\n    const isInternalError = xrpcError instanceof InternalServerError\n\n    const msgPrefix = isInternalError ? 'unhandled exception' : 'error'\n    const msgSuffix = nsid ? `xrpc method ${nsid}` : `${req.method} ${req.url}`\n    const msg = `${msgPrefix} in ${msgSuffix}`\n\n    logger.error(\n      {\n        // @NOTE Computation of error stack is an expensive operation, so\n        // we strip it for expected errors.\n        err:\n          isInternalError || process.env.NODE_ENV === 'development'\n            ? err\n            : toSimplifiedErrorLike(err),\n\n        // XRPC specific properties, for easier browsing of logs\n        nsid,\n        type: xrpcError.type,\n        status: xrpcError.statusCode,\n        payload: xrpcError.payload,\n\n        // Ensure that the logged item's name is set to LOGGER_NAME, instead of\n        // the name of the pino-http logger, to ensure consistency across logs.\n        name: LOGGER_NAME,\n      },\n      msg,\n    )\n\n    if (res.headersSent) {\n      return next(err)\n    }\n\n    return res.status(xrpcError.statusCode).json(xrpcError.payload)\n  }\n}\n\nfunction isPinoHttpRequest(req: IncomingMessage): req is IncomingMessage & {\n  log: { error: (obj: unknown, msg: string) => void }\n} {\n  return typeof (req as { log?: any }).log?.error === 'function'\n}\n\nfunction toSimplifiedErrorLike(err: unknown): unknown {\n  if (err instanceof Error) {\n    // Transform into an \"ErrorLike\" for pino's std \"err\" serializer\n    return {\n      ...err,\n      // Carry over non-enumerable properties\n      message: err.message,\n      name:\n        !Object.hasOwn(err, 'name') &&\n        Object.prototype.toString.call(err.constructor) === '[object Function]'\n          ? err.constructor.name // extract the class name for sub-classes of Error\n          : err.name,\n      // @NOTE Error.stack, Error.cause and AggregateError.error are non\n      // enumerable properties so they won't be spread to the ErrorLike\n    }\n  }\n\n  return err\n}\n\nfunction buildRateLimiterOptions<C extends HandlerContext = HandlerContext>({\n  name,\n  calcKey = defaultKey,\n  calcPoints = defaultPoints,\n  ...desc\n}: ServerRateLimitDescription<C>): RateLimiterOptions<C> {\n  return { ...desc, calcKey, calcPoints, keyPrefix: `rl-${name}` }\n}\n\nconst defaultPoints: CalcPointsFn = () => 1\n\n/**\n * @note when using a proxy, ensure headers are getting forwarded correctly:\n * `app.set('trust proxy', true)`\n *\n * @see {@link https://expressjs.com/en/guide/behind-proxies.html}\n */\nconst defaultKey: CalcKeyFn<HandlerContext> = ({ req }) => req.ip\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/frames.ts",
    "content": "import { decodeAll, encode } from '@atproto/lex-cbor'\nimport { LexValue, isPlainObject } from '@atproto/lex-data'\nimport { XRPCError } from '../errors'\nimport {\n  ErrorFrameBody,\n  ErrorFrameHeader,\n  FrameHeader,\n  FrameType,\n  MessageFrameHeader,\n  errorFrameBody,\n  frameHeader,\n} from './types'\n\nexport abstract class Frame<T extends LexValue = LexValue> {\n  abstract header: FrameHeader\n  abstract body: T\n\n  get op(): FrameType {\n    return this.header.op\n  }\n  toBytes(): Uint8Array {\n    return Buffer.concat([encode(this.header), encode(this.body)])\n  }\n  isMessage(): this is MessageFrame {\n    return this.op === FrameType.Message\n  }\n  isError(): this is ErrorFrame {\n    return this.op === FrameType.Error\n  }\n  static fromBytes(bytes: Uint8Array) {\n    const [header, body, ...rest] = decodeAll(bytes)\n    if (rest.length) {\n      throw new Error('Too many CBOR data items in frame')\n    } else if (body === undefined) {\n      throw new Error('Missing frame body')\n    }\n\n    const parsedHeader = frameHeader.safeParse(header)\n    if (!parsedHeader.success) {\n      throw new Error(`Invalid frame header: ${parsedHeader.reason.message}`)\n    }\n    const frameOp = parsedHeader.value.op\n    if (frameOp === FrameType.Message) {\n      return new MessageFrame(body, {\n        type: parsedHeader.value.t,\n      })\n    } else if (frameOp === FrameType.Error) {\n      const parsedBody = errorFrameBody.safeParse(body)\n      if (!parsedBody.success) {\n        throw new Error(\n          `Invalid error frame body: ${parsedBody.reason.message}`,\n        )\n      }\n      return new ErrorFrame(parsedBody.value)\n    } else {\n      const exhaustiveCheck: never = frameOp\n      throw new Error(`Unknown frame op: ${exhaustiveCheck}`)\n    }\n  }\n}\n\nexport class MessageFrame<T extends LexValue = LexValue> extends Frame<T> {\n  header: MessageFrameHeader\n  body: T\n\n  constructor(body: T, opts?: { type?: string }) {\n    super()\n    this.header =\n      opts?.type !== undefined\n        ? { op: FrameType.Message, t: opts?.type }\n        : { op: FrameType.Message }\n    this.body = body\n  }\n  get type() {\n    return this.header.t\n  }\n\n  static fromLexValue(data: LexValue, nsid: string) {\n    if (!isPlainObject(data)) {\n      return new MessageFrame(data)\n    }\n\n    const $type = data?.['$type']\n    if (typeof $type !== 'string') {\n      return new MessageFrame(data)\n    }\n\n    let type: string\n\n    const split = $type.split('#')\n    if (split.length === 2 && (split[0] === '' || split[0] === nsid)) {\n      type = `#${split[1]}`\n    } else {\n      type = $type\n    }\n\n    const { $type: _, ...clone } = data\n    return new MessageFrame(clone, { type })\n  }\n}\n\nexport class ErrorFrame<T extends string = string> extends Frame<\n  ErrorFrameBody<T>\n> {\n  header: ErrorFrameHeader\n  body: ErrorFrameBody<T>\n\n  constructor(body: ErrorFrameBody<T>) {\n    super()\n    this.header = { op: FrameType.Error }\n    this.body = body\n  }\n  get code() {\n    return this.body.error\n  }\n  get message() {\n    return this.body.message\n  }\n\n  static fromError(err: unknown): ErrorFrame {\n    if (err instanceof ErrorFrame) return err\n    const { error = 'Unknown', message } = XRPCError.fromError(err).payload\n    return new ErrorFrame({ error, message })\n  }\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/index.ts",
    "content": "export * from './types'\nexport * from './frames'\nexport * from './stream'\nexport * from './subscription'\nexport * from './server'\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/logger.ts",
    "content": "import { subsystemLogger } from '@atproto/common'\n\nexport const logger: ReturnType<typeof subsystemLogger> =\n  subsystemLogger('xrpc-stream')\n\nexport default logger\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/server.ts",
    "content": "import { IncomingMessage } from 'node:http'\nimport { ServerOptions, WebSocket, WebSocketServer } from 'ws'\nimport { CloseCode, DisconnectError } from '@atproto/ws-client'\nimport { ErrorFrame, Frame } from './frames'\nimport { logger } from './logger'\n\nexport class XrpcStreamServer {\n  wss: WebSocketServer\n  constructor(opts: ServerOptions & { handler: Handler }) {\n    const { handler, ...serverOpts } = opts\n    this.wss = new WebSocketServer(serverOpts)\n    this.wss.on('connection', async (socket, req) => {\n      socket.on('error', (err) => logger.error(err, 'websocket error'))\n      try {\n        const ac = new AbortController()\n        const iterator = unwrapIterator(handler(req, ac.signal, socket, this))\n        socket.once('close', () => {\n          iterator.return?.()\n          ac.abort()\n        })\n        const safeFrames = wrapIterator(iterator)\n        for await (const frame of safeFrames) {\n          await new Promise((res, rej) => {\n            socket.send(frame.toBytes(), { binary: true }, (err) => {\n              // @TODO this callback may give more aggressive on backpressure than\n              // we ultimately want, but trying it out for the time being.\n              if (err) return rej(err)\n              res(undefined)\n            })\n          })\n          if (frame instanceof ErrorFrame) {\n            throw new DisconnectError(CloseCode.Policy, frame.body.error)\n          }\n        }\n      } catch (err) {\n        if (err instanceof DisconnectError) {\n          return socket.close(err.wsCode, err.xrpcCode)\n        } else {\n          logger.error({ err }, 'websocket server error')\n          return socket.terminate()\n        }\n      }\n      socket.close(CloseCode.Normal)\n    })\n  }\n}\n\nexport type Handler = (\n  req: IncomingMessage,\n  signal: AbortSignal,\n  socket: WebSocket,\n  server: XrpcStreamServer,\n) => AsyncIterable<Frame>\n\nfunction unwrapIterator<T>(iterable: AsyncIterable<T>): AsyncIterator<T> {\n  return iterable[Symbol.asyncIterator]()\n}\n\nfunction wrapIterator<T>(iterator: AsyncIterator<T>): AsyncIterable<T> {\n  return {\n    [Symbol.asyncIterator]() {\n      return iterator\n    },\n  }\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/stream.ts",
    "content": "import { DuplexOptions } from 'node:stream'\nimport { WebSocket, createWebSocketStream } from 'ws'\nimport { ResponseType, XRPCError } from '@atproto/xrpc'\nimport { Frame, MessageFrame } from './frames'\n\nexport function streamByteChunks(ws: WebSocket, options?: DuplexOptions) {\n  return createWebSocketStream(ws, {\n    ...options,\n    readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together\n  })\n}\n\nexport async function* byFrame(ws: WebSocket, options?: DuplexOptions) {\n  const wsStream = streamByteChunks(ws, options)\n  for await (const chunk of wsStream) {\n    yield Frame.fromBytes(chunk)\n  }\n}\n\nexport async function* byMessage(ws: WebSocket, options?: DuplexOptions) {\n  const wsStream = streamByteChunks(ws, options)\n  for await (const chunk of wsStream) {\n    const msg = ensureChunkIsMessage(chunk)\n    yield msg\n  }\n}\n\nexport function ensureChunkIsMessage(chunk: Uint8Array): MessageFrame {\n  const frame = Frame.fromBytes(chunk)\n  if (frame.isMessage()) {\n    return frame\n  } else if (frame.isError()) {\n    // @TODO work -1 error code into XRPCError\n    // @ts-ignore\n    throw new XRPCError(-1, frame.code, frame.message)\n  } else {\n    throw new XRPCError(ResponseType.Unknown, undefined, 'Unknown frame type')\n  }\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/subscription.ts",
    "content": "import { ClientOptions } from 'ws'\nimport { isPlainObject } from '@atproto/lex-data'\nimport { WebSocketKeepAlive } from '@atproto/ws-client'\nimport { ensureChunkIsMessage } from './stream'\n\nexport class Subscription<T = unknown> {\n  constructor(\n    public opts: ClientOptions & {\n      service: string\n      method: string\n      maxReconnectSeconds?: number\n      heartbeatIntervalMs?: number\n      signal?: AbortSignal\n      validate: (obj: unknown) => T | undefined\n      onReconnectError?: (\n        error: unknown,\n        n: number,\n        initialSetup: boolean,\n      ) => void\n      getParams?: () =>\n        | Record<string, unknown>\n        | Promise<Record<string, unknown> | undefined>\n        | undefined\n    },\n  ) {}\n\n  async *[Symbol.asyncIterator](): AsyncGenerator<T> {\n    const ws = new WebSocketKeepAlive({\n      ...this.opts,\n      getUrl: async () => {\n        const params = (await this.opts.getParams?.()) ?? {}\n        const query = encodeQueryParams(params)\n        return `${this.opts.service}/xrpc/${this.opts.method}?${query}`\n      },\n    })\n    for await (const chunk of ws) {\n      const message = ensureChunkIsMessage(chunk)\n      const t = message.header.t\n\n      const typedBody = isPlainObject(message.body)\n        ? t !== undefined\n          ? {\n              ...message.body,\n              $type: t.startsWith('#') ? this.opts.method + t : t,\n            }\n          : message.body\n        : undefined\n\n      const result = this.opts.validate(typedBody)\n      if (result !== undefined) {\n        yield result\n      }\n    }\n  }\n}\n\nexport default Subscription\n\nfunction encodeQueryParams(obj: Record<string, unknown>): string {\n  const params = new URLSearchParams()\n  Object.entries(obj).forEach(([key, value]) => {\n    const encoded = encodeQueryParam(value)\n    if (Array.isArray(encoded)) {\n      encoded.forEach((enc) => params.append(key, enc))\n    } else {\n      params.set(key, encoded)\n    }\n  })\n  return params.toString()\n}\n\n// Adapted from xrpc, but without any lex-specific knowledge\nfunction encodeQueryParam(value: unknown): string | string[] {\n  if (typeof value === 'string') {\n    return value\n  }\n  if (typeof value === 'number') {\n    return value.toString()\n  }\n  if (typeof value === 'boolean') {\n    return value ? 'true' : 'false'\n  }\n  if (typeof value === 'undefined') {\n    return ''\n  }\n  if (typeof value === 'object') {\n    if (value instanceof Date) {\n      return value.toISOString()\n    } else if (Array.isArray(value)) {\n      return value.flatMap(encodeQueryParam)\n    } else if (!value) {\n      return ''\n    }\n  }\n  throw new Error(`Cannot encode ${typeof value}s into query params`)\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/stream/types.ts",
    "content": "import { l } from '@atproto/lex-schema'\n\nexport enum FrameType {\n  Message = 1,\n  Error = -1,\n}\n\nexport const messageFrameHeader = l.object({\n  op: l.literal(FrameType.Message), // Frame op\n  t: l.optional(l.string()), // Message body type discriminator\n})\nexport type MessageFrameHeader = l.Infer<typeof messageFrameHeader>\n\nexport const errorFrameHeader = l.object({\n  op: l.literal(FrameType.Error),\n})\nexport const errorFrameBody = l.object({\n  error: l.string(), // Error code\n  message: l.optional(l.string()), // Error message\n})\nexport type ErrorFrameHeader = l.Infer<typeof errorFrameHeader>\nexport type ErrorFrameBody<T extends string = string> = { error: T } & l.Infer<\n  typeof errorFrameBody\n>\n\nexport const frameHeader = l.union([messageFrameHeader, errorFrameHeader])\nexport type FrameHeader = l.Infer<typeof frameHeader>\n"
  },
  {
    "path": "packages/xrpc-server/src/types.ts",
    "content": "import { IncomingMessage } from 'node:http'\nimport { Readable } from 'node:stream'\nimport { NextFunction, Request, Response } from 'express'\nimport { l } from '@atproto/lex-schema'\nimport { ErrorResult, XRPCError } from './errors'\nimport { CalcKeyFn, CalcPointsFn, RateLimiterI } from './rate-limiter'\n\nexport type Awaitable<T> = T | Promise<T>\n\nexport type CatchallHandler = (\n  req: Request,\n  res: Response,\n  next: NextFunction,\n) => unknown\n\nexport type Options = {\n  validateResponse?: boolean\n  catchall?: CatchallHandler\n  payload?: RouteOptions\n  rateLimits?: {\n    creator: RateLimiterCreator<HandlerContext>\n    global?: ServerRateLimitDescription<HandlerContext>[]\n    shared?: ServerRateLimitDescription<HandlerContext>[]\n    bypass?: (ctx: HandlerContext) => boolean\n  }\n  /**\n   * By default, errors are converted to {@link XRPCError} using\n   * {@link XRPCError.fromError} before being rendered. If method handlers throw\n   * error objects that are not properly rendered in the HTTP response, this\n   * function can be used to properly convert them to {@link XRPCError}. The\n   * provided function will typically fallback to the default error conversion\n   * (`return XRPCError.fromError(err)`) if the error is not recognized.\n   *\n   * @note This function should not throw errors.\n   */\n  errorParser?: (err: unknown) => XRPCError\n}\n\nexport type UndecodedParams = Request['query']\n\nexport type Primitive = string | number | boolean\nexport type Params = { [P in string]?: undefined | Primitive | Primitive[] }\n\nexport type HandlerInput = {\n  encoding: string\n  body: unknown\n}\n\nexport type AuthResult = {\n  credentials: unknown\n  artifacts?: unknown\n}\n\nexport const headersSchema = l.dict(l.string(), l.string())\n\nexport type Headers = l.Infer<typeof headersSchema>\n\nexport const handlerSuccess = l.object({\n  encoding: l.string(),\n  body: l.unknown(),\n  headers: l.optional(headersSchema),\n})\n\nexport type HandlerSuccess = l.Infer<typeof handlerSuccess>\n\nexport const handlerPipeThroughBuffer = l.object({\n  encoding: l.string(),\n  buffer: l.custom(\n    (v): v is Buffer => v instanceof Buffer,\n    'Expected a Buffer',\n  ),\n  headers: l.optional(headersSchema),\n})\n\nexport type HandlerPipeThroughBuffer = l.Infer<typeof handlerPipeThroughBuffer>\n\nexport const handlerPipeThroughStream = l.object({\n  encoding: l.string(),\n  stream: l.custom(\n    (v): v is Readable => v instanceof Readable,\n    'Expected a Readable stream',\n  ),\n  headers: l.optional(headersSchema),\n})\n\nexport type HandlerPipeThroughStream = l.Infer<typeof handlerPipeThroughStream>\n\nexport const handlerPipeThrough = l.union([\n  handlerPipeThroughBuffer,\n  handlerPipeThroughStream,\n])\n\nexport type HandlerPipeThrough = l.Infer<typeof handlerPipeThrough>\n\nexport type Auth = void | AuthResult\nexport type Input = void | HandlerInput\nexport type Output = void | HandlerSuccess | HandlerPipeThrough | ErrorResult\n\nexport type AuthVerifier<C, A extends AuthResult = AuthResult> =\n  | ((ctx: C) => Awaitable<A | ErrorResult>)\n  | ((ctx: C) => Awaitable<A>)\n\nexport type MethodAuthContext<P extends Params = Params> = {\n  params: P\n  req: Request\n  res: Response\n}\n\nexport type MethodAuthVerifier<\n  A extends AuthResult = AuthResult,\n  P extends Params = Params,\n> = AuthVerifier<MethodAuthContext<P>, A>\n\nexport type HandlerContext<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  I extends Input = Input,\n> = MethodAuthContext<P> & {\n  auth: A\n  input: I\n  resetRouteRateLimits: () => Promise<void>\n}\n\nexport type MethodHandler<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  I extends Input = Input,\n  O extends Output = Output,\n> = (ctx: HandlerContext<A, P, I>) => Awaitable<O | HandlerPipeThrough>\n\nexport type RateLimiterCreator<T extends HandlerContext = HandlerContext> = <\n  C extends T = T,\n>(opts: {\n  keyPrefix: string\n  durationMs: number\n  points: number\n  calcKey: CalcKeyFn<C>\n  calcPoints: CalcPointsFn<C>\n  failClosed?: boolean\n}) => RateLimiterI<C>\n\nexport type ServerRateLimitDescription<\n  C extends HandlerContext = HandlerContext,\n> = {\n  name: string\n  durationMs: number\n  points: number\n  calcKey?: CalcKeyFn<C>\n  calcPoints?: CalcPointsFn<C>\n  failClosed?: boolean\n}\n\nexport type SharedRateLimitOpts<C extends HandlerContext = HandlerContext> = {\n  name: string\n  calcKey?: CalcKeyFn<C>\n  calcPoints?: CalcPointsFn<C>\n}\n\nexport type RouteRateLimitOpts<C extends HandlerContext = HandlerContext> = {\n  durationMs: number\n  points: number\n  calcKey?: CalcKeyFn<C>\n  calcPoints?: CalcPointsFn<C>\n}\n\nexport type RateLimitOpts<C extends HandlerContext = HandlerContext> =\n  | SharedRateLimitOpts<C>\n  | RouteRateLimitOpts<C>\n\nexport function isSharedRateLimitOpts<\n  C extends HandlerContext = HandlerContext,\n>(opts: RateLimitOpts<C>): opts is SharedRateLimitOpts<C> {\n  return typeof opts['name'] === 'string'\n}\n\nexport type RouteOptions = {\n  blobLimit?: number\n  jsonLimit?: number\n  textLimit?: number\n}\n\nexport type MethodAuth<\n  A extends Auth = Auth,\n  P extends Params = Params,\n> = MethodAuthVerifier<Extract<A, AuthResult>, P>\n\nexport type MethodRateLimit<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  I extends Input = Input,\n> =\n  | RateLimitOpts<HandlerContext<A, P, I>>\n  | RateLimitOpts<HandlerContext<A, P, I>>[]\n\nexport type MethodConfig<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  I extends Input = Input,\n  O extends Output = Output,\n> = {\n  handler: MethodHandler<A, P, I, O>\n  auth?: MethodAuth<A, P>\n  opts?: RouteOptions\n  rateLimit?: MethodRateLimit<A, P, I>\n}\n\nexport type MethodConfigWithAuth<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  I extends Input = Input,\n  O extends Output = Output,\n> = {\n  handler: MethodHandler<A, P, I, O>\n  auth: MethodAuth<A, P>\n  opts?: RouteOptions\n  rateLimit?: MethodRateLimit<A, P, I>\n}\n\nexport type MethodConfigOrHandler<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  I extends Input = Input,\n  O extends Output = Output,\n> = MethodHandler<A, P, I, O> | MethodConfig<A, P, I, O>\n\nexport type StreamAuthContext<P extends Params = Params> = {\n  params: P\n  req: IncomingMessage\n}\n\nexport type LexMethodParams<M extends l.Procedure | l.Query | l.Subscription> =\n  l.InferMethodParams<M>\n\nexport type LexMethodInput<M extends l.Procedure | l.Query> =\n  l.InferMethodInput<M, Readable>\n\nexport type LexMethodOutput<M extends l.Procedure | l.Query> =\n  l.InferMethodOutput<M, Readable> extends undefined\n    ? l.InferMethodOutput<M, Uint8Array | Readable> | void\n    : l.InferMethodOutput<M, Uint8Array | Readable>\n\nexport type LexMethodMessage<M extends l.Subscription> = l.InferMethodMessage<M>\n\nexport type LexMethodHandler<\n  M extends l.Procedure | l.Query,\n  A extends Auth = Auth,\n> = MethodHandler<A, LexMethodParams<M>, LexMethodInput<M>, LexMethodOutput<M>>\n\nexport type LexMethodConfig<\n  M extends l.Procedure | l.Query,\n  A extends Auth = Auth,\n> = MethodConfig<A, LexMethodParams<M>, LexMethodInput<M>, LexMethodOutput<M>>\n\nexport type LexSubscriptionHandler<\n  M extends l.Subscription,\n  A extends Auth = Auth,\n> = StreamHandler<\n  Extract<A, AuthResult>,\n  LexMethodParams<M>,\n  LexMethodMessage<M>\n>\n\nexport type LexSubscriptionConfig<\n  M extends l.Subscription,\n  A extends Auth = Auth,\n> = StreamConfig<A, LexMethodParams<M>, LexMethodMessage<M>>\n\nexport type StreamAuthVerifier<\n  A extends AuthResult = AuthResult,\n  P extends Params = Params,\n> = AuthVerifier<StreamAuthContext<P>, A>\n\nexport type StreamContext<\n  A extends Auth = Auth,\n  P extends Params = Params,\n> = StreamAuthContext<P> & {\n  auth: A\n  signal: AbortSignal\n}\n\nexport type StreamHandler<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  O = unknown,\n> = (ctx: StreamContext<A, P>) => AsyncIterable<O>\n\nexport type StreamConfig<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  O = unknown,\n> = {\n  auth?: StreamAuthVerifier<Extract<A, AuthResult>, P>\n  handler: StreamHandler<A, P, O>\n}\n\nexport type StreamConfigOrHandler<\n  A extends Auth = Auth,\n  P extends Params = Params,\n  O = unknown,\n> = StreamHandler<A, P, O> | StreamConfig<A, P, O>\n\nexport function isHandlerSuccess(output: Output): output is HandlerSuccess {\n  // We only need to discriminate between possible Output values\n  return (\n    output != null &&\n    'body' in output && // body is non optional (contrary to what type inference may suggest)\n    'encoding' in output &&\n    // Allows using objects that extends HandlerSuccess with a \"status\" field as\n    // output, as long as the status is < 400, in order to avoid being confused\n    // with ErrorResult objects.\n    (!('status' in output) ||\n      output.status == null ||\n      Number(output.status) < 400)\n  )\n}\n\nexport function isHandlerPipeThroughBuffer(\n  output: Output,\n): output is HandlerPipeThroughBuffer {\n  // We only need to discriminate between possible Output values\n  return output != null && 'buffer' in output && output['buffer'] !== undefined\n}\n\nexport function isHandlerPipeThroughStream(\n  output: Output,\n): output is HandlerPipeThroughStream {\n  // We only need to discriminate between possible Output values\n  return output != null && 'stream' in output && output['stream'] !== undefined\n}\n"
  },
  {
    "path": "packages/xrpc-server/src/util.ts",
    "content": "import assert from 'node:assert'\nimport { IncomingMessage, OutgoingMessage } from 'node:http'\nimport { Duplex, Readable, pipeline } from 'node:stream'\nimport {\n  Request as ExpressRequest,\n  Response as ExpressResponse,\n  json,\n  text,\n} from 'express'\nimport { contentType } from 'mime-types'\nimport { MaxSizeChecker, createDecoders } from '@atproto/common'\nimport { jsonToLex } from '@atproto/lex-json'\nimport { l } from '@atproto/lex-schema'\nimport {\n  type LexXrpcBody,\n  type LexXrpcProcedure,\n  type LexXrpcQuery,\n  type LexXrpcSubscription,\n  Lexicons,\n  jsonToLex as jsonToLexWithBlobRef,\n} from '@atproto/lexicon'\nimport { ResponseType } from '@atproto/xrpc'\nimport {\n  ErrorResult,\n  InternalServerError,\n  InvalidRequestError,\n  XRPCError,\n} from './errors'\nimport {\n  Auth,\n  Input,\n  LexMethodInput,\n  LexMethodOutput,\n  LexMethodParams,\n  Output,\n  Params,\n  RouteOptions,\n  UndecodedParams,\n  handlerSuccess,\n} from './types'\n\nexport type ParamsVerifierInternal<P extends Params = Params> = (\n  req: IncomingMessage | ExpressRequest,\n) => P\n\nexport type AuthVerifierInternal<C, A extends Auth = Auth> = (\n  ctx: C,\n) => Promise<Exclude<A, ErrorResult>>\n\nexport type InputVerifierInternal<I extends Input = Input> = (\n  req: ExpressRequest,\n  res: ExpressResponse,\n) => Promise<I>\n\nexport type OutputVerifierInternal<O extends Output = Output> = (\n  handleOutput: O,\n) => void\n\nexport const asArray = <T>(arr: T | T[]): T[] =>\n  Array.isArray(arr) ? arr : [arr]\n\nexport function setHeaders(\n  res: OutgoingMessage,\n  headers?: Record<string, string | number>,\n) {\n  if (headers) {\n    for (const [name, val] of Object.entries(headers)) {\n      if (val != null) res.setHeader(name, val)\n    }\n  }\n}\n\nfunction decodeQueryParams(\n  def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription,\n  params: UndecodedParams,\n): Params {\n  const decoded: Params = {}\n  for (const k in params) {\n    const val = params[k]\n    const property = def.parameters?.properties?.[k]\n    if (property) {\n      if (property.type === 'array') {\n        const vals: (typeof val)[] = []\n        decoded[k] = val\n          ? vals\n              .concat(val) // Cast to array\n              .flatMap((v) => decodeQueryParam(property.items.type, v) ?? [])\n          : undefined\n      } else {\n        decoded[k] = decodeQueryParam(property.type, val)\n      }\n    }\n  }\n  return decoded\n}\n\nexport function decodeQueryParam(\n  type: string,\n  value: unknown,\n): string | number | boolean | undefined {\n  if (!value) {\n    return undefined\n  }\n  if (type === 'string' || type === 'datetime') {\n    return String(value)\n  }\n  if (type === 'float') {\n    return Number(String(value))\n  } else if (type === 'integer') {\n    return parseInt(String(value), 10) || 0\n  } else if (type === 'boolean') {\n    return value === 'true'\n  }\n}\n\nexport function getSearchParams(url?: string): URLSearchParams | undefined {\n  if (!url) return undefined\n\n  const queryStringIdx = url.indexOf('?')\n  if (queryStringIdx === -1) return undefined\n\n  const queryString = url.slice(queryStringIdx + 1)\n  if (queryString.length === 0) return undefined\n\n  return new URLSearchParams(queryString)\n}\n\nexport function getQueryParams(\n  req: IncomingMessage | ExpressRequest,\n): UndecodedParams {\n  if ('query' in req) return req.query\n\n  const result: UndecodedParams = Object.create(null)\n\n  const searchParams = getSearchParams(req.url)\n  if (!searchParams) return result\n\n  if (searchParams.has('__proto__')) {\n    // Prevent prototype pollution\n    throw new InvalidRequestError(\n      `Invalid query parameter: __proto__`,\n      'InvalidQueryParameter',\n    )\n  }\n\n  for (const key of searchParams.keys()) {\n    const values = searchParams.getAll(key)\n    result[key] = values.length === 1 ? values[0] : values\n  }\n\n  return result\n}\n\nexport function createLexiconParamsVerifier<P extends Params = Params>(\n  nsid: string,\n  def: LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription,\n  lexicons: Lexicons,\n): ParamsVerifierInternal<P> {\n  return (req) => {\n    const queryParams = getQueryParams(req)\n    const params = decodeQueryParams(def, queryParams)\n    try {\n      return lexicons.assertValidXrpcParams(nsid, params) as P\n    } catch (e) {\n      // @NOTE WE historically did not check for specific error types here,\n      throw new InvalidRequestError(String(e))\n    }\n  }\n}\n\nexport function createSchemaParamsVerifier<\n  M extends l.Procedure | l.Query | l.Subscription,\n>(ns: l.Main<M>): ParamsVerifierInternal<LexMethodParams<M>> {\n  const schema = l.getMain(ns)\n  return (req) => {\n    const urlSearchParams = getSearchParams(req.url) ?? new URLSearchParams()\n    try {\n      const params = schema.parameters.fromURLSearchParams(urlSearchParams)\n      return params as LexMethodParams<M>\n    } catch (err) {\n      if (err instanceof l.LexValidationError) {\n        throw new InvalidRequestError(err.message)\n      }\n      throw err\n    }\n  }\n}\n\nexport function createLexiconInputVerifier<I extends Input = Input>(\n  nsid: string,\n  def: LexXrpcProcedure | LexXrpcQuery,\n  options: RouteOptions,\n  lexicons: Lexicons,\n): InputVerifierInternal<I> {\n  if (def.type === 'query' || !def.input) {\n    return async (req) => {\n      // @NOTE We allow (and ignore) \"empty\" bodies\n      if (getBodyPresence(req) === 'present') {\n        throw new InvalidRequestError(\n          `A request body was provided when none was expected`,\n        )\n      }\n\n      return undefined as I\n    }\n  }\n\n  // Lexicon definition expects a request body\n\n  const { input } = def\n  const { blobLimit } = options\n\n  const allowedEncodings = parseDefEncoding(input)\n  const checkEncoding = allowedEncodings.includes(ENCODING_ANY)\n    ? undefined // No need to check\n    : (encoding: string) => allowedEncodings.includes(encoding)\n\n  const bodyParser = createBodyParser(input.encoding, options)\n\n  return async (req, res) => {\n    if (getBodyPresence(req) === 'missing') {\n      throw new InvalidRequestError(\n        `A request body is expected but none was provided`,\n      )\n    }\n\n    const reqEncoding = parseReqEncoding(req)\n    if (checkEncoding && !checkEncoding(reqEncoding)) {\n      throw new InvalidRequestError(\n        `Wrong request encoding (Content-Type): ${reqEncoding}`,\n      )\n    }\n\n    if (bodyParser) {\n      await bodyParser(req, res)\n    }\n\n    if (input.schema) {\n      try {\n        const lexBody = req.body ? jsonToLexWithBlobRef(req.body) : req.body\n        req.body = lexicons.assertValidXrpcInput(nsid, lexBody)\n      } catch (cause) {\n        throw new InvalidRequestError(\n          cause instanceof Error ? cause.message : String(cause),\n          undefined,\n          { cause },\n        )\n      }\n    }\n\n    // if middleware already got the body, we pass that along as input\n    // otherwise, we pass along a decoded readable stream\n    const body = req.readableEnded ? req.body : decodeBodyStream(req, blobLimit)\n\n    return { encoding: reqEncoding, body } as I\n  }\n}\n\nexport function createSchemaInputVerifier<M extends l.Procedure | l.Query>(\n  ns: l.Main<M>,\n  options: RouteOptions,\n): InputVerifierInternal<LexMethodInput<M>> {\n  const schema = l.getMain(ns)\n  const { blobLimit } = options\n\n  const input: l.Payload | undefined =\n    'input' in schema ? schema.input : undefined\n\n  if (!input?.encoding) {\n    //\n    return async (req) => {\n      if (getBodyPresence(req) === 'present') {\n        throw new InvalidRequestError(\n          `A request body was provided when none was expected`,\n        )\n      }\n\n      return undefined as LexMethodInput<M>\n    }\n  }\n\n  const bodyParser = createBodyParser(input.encoding, options)\n\n  return async (req, res) => {\n    if (getBodyPresence(req) === 'missing') {\n      throw new InvalidRequestError(\n        `A request body is expected but none was provided`,\n      )\n    }\n\n    const reqEncoding = parseReqEncoding(req)\n    if (!input.matchesEncoding(reqEncoding)) {\n      throw new InvalidRequestError(\n        `Wrong request encoding (Content-Type): ${reqEncoding}`,\n      )\n    }\n\n    if (bodyParser) {\n      await bodyParser(req, res)\n    }\n\n    if (input.schema) {\n      try {\n        const lexBody = req.body ? jsonToLex(req.body) : req.body\n        req.body = input.schema.parse(lexBody)\n      } catch (cause) {\n        throw new InvalidRequestError(\n          cause instanceof Error ? cause.message : String(cause),\n          undefined,\n          { cause },\n        )\n      }\n    }\n\n    // if middleware already got the body, we pass that along as input\n    // otherwise, we pass along a decoded readable stream\n    const body = req.readableEnded ? req.body : decodeBodyStream(req, blobLimit)\n\n    return { encoding: reqEncoding, body } as LexMethodInput<M>\n  }\n}\n\nexport function createLexiconOutputVerifier<O extends Output = Output>(\n  nsid: string,\n  def: LexXrpcProcedure | LexXrpcQuery,\n  lexicons: Lexicons,\n): OutputVerifierInternal<O> {\n  const outputDef = def.output\n\n  // Expects no output\n  if (!outputDef) {\n    return (handlerOutput) => {\n      if (handlerOutput !== undefined) {\n        throw new InternalServerError(\n          `A response body was provided when none was expected`,\n        )\n      }\n    }\n  }\n\n  // An output is expected\n  return (handlerOutput) => {\n    if (handlerOutput === undefined) {\n      throw new InternalServerError(\n        `A response body is expected but none was provided`,\n      )\n    }\n\n    if (!('encoding' in handlerOutput)) {\n      // Ensure handlerOutput is valid ErrorResult\n      if ('status' in handlerOutput && handlerOutput.status >= 400) {\n        return\n      }\n\n      throw new InternalServerError(`Invalid handler output: missing encoding`)\n    }\n\n    if (!('body' in handlerOutput)) {\n      // Ensure handlerOutput is valid HandlerPipeThrough\n      if ('stream' in handlerOutput || 'buffer' in handlerOutput) {\n        return // Validation is ignored for pipe-through outputs\n      }\n\n      throw new InternalServerError(`Invalid handler output: missing body`)\n    }\n\n    // Fool-proofing (should not be necessary due to type system)\n    const result = handlerSuccess.safeParse(handlerOutput)\n    if (!result.success) {\n      throw new InternalServerError(`Invalid handler output`, undefined, {\n        cause: result.reason,\n      })\n    }\n\n    // output mime\n    const { encoding } = handlerOutput\n    if (!isValidEncoding(outputDef, encoding)) {\n      throw new InternalServerError(`Invalid response encoding: ${encoding}`)\n    }\n\n    // output schema\n    try {\n      lexicons.assertValidXrpcOutput(nsid, handlerOutput.body)\n      // @TODO Since the output verifier is typically enabled in dev/tests and\n      // disabled in production, we don't want to assign the (altered) output\n      // back to the handlerOutput object, as this would cause different\n      // behaviors between environments. Instead, we should compare the value\n      // returned by assertValidXrpcOutput with the original output and throw if\n      // they differ (indicating that the output was mutated during validation,\n      // e.g. due to default values being applied).\n    } catch (cause) {\n      const message =\n        cause instanceof Error ? cause.message : 'Output body validation failed'\n      throw new InternalServerError(message, undefined, { cause })\n    }\n  }\n}\n\nexport function createSchemaOutputVerifier<M extends l.Procedure | l.Query>(\n  ns: l.Main<M>,\n): OutputVerifierInternal<LexMethodOutput<M>> {\n  const outputSchema = l.getMain(ns).output\n  return (handlerOutput) => {\n    // @NOTE If the user of the lib wants to return an output that doesn't\n    // conform to the schema, they can use HandlerPipeThrough return types\n    if (!outputSchema.matchesEncoding(handlerOutput?.encoding)) {\n      throw new InternalServerError('Output encoding mismatch')\n    }\n    if (outputSchema.schema) {\n      const result = outputSchema.schema.safeValidate(handlerOutput?.body)\n      if (!result.success) {\n        throw new InternalServerError(result.reason.message, undefined, {\n          cause: result.reason,\n        })\n      }\n    } else if (!outputSchema.encoding && handlerOutput?.body !== undefined) {\n      throw new InternalServerError('Output body not expected')\n    }\n  }\n}\n\nexport function parseReqEncoding(req: IncomingMessage): string {\n  const encoding = normalizeMime(req.headers['content-type'])\n  if (encoding) return encoding\n  throw new InvalidRequestError(\n    `Request encoding (Content-Type) required but not provided`,\n  )\n}\n\nfunction normalizeMime(v?: string): string | null {\n  if (!v) return null\n  const fullType = contentType(v)\n  if (!fullType) return null\n  const shortType = fullType.split(';')[0]\n  if (!shortType) return null\n  return shortType\n}\n\nconst ENCODING_ANY = '*/*'\n\nfunction parseDefEncoding({ encoding }: LexXrpcBody) {\n  return encoding.split(',').map(trimString)\n}\n\nfunction trimString(str: string): string {\n  return str.trim()\n}\n\nfunction isValidEncoding(output: LexXrpcBody, encoding?: string) {\n  if (!encoding) return false\n\n  const normalized = normalizeMime(encoding)\n  if (!normalized) return false\n\n  const allowed = parseDefEncoding(output)\n  if (!allowed.length) return false\n\n  if (allowed.includes(ENCODING_ANY)) return true\n  if (allowed.includes(normalized)) return true\n\n  // Check for wildcard matches (e.g. normalized=application/json, allowed=application/*)\n  for (const allowedEnc of allowed) {\n    if (\n      allowedEnc.endsWith('/*') &&\n      normalized.startsWith(allowedEnc.slice(0, -1))\n    ) {\n      return true\n    }\n  }\n\n  return false\n}\n\ntype BodyPresence = 'missing' | 'empty' | 'present'\n\nfunction getBodyPresence(req: IncomingMessage): BodyPresence {\n  if (req.headers['transfer-encoding'] != null) return 'present'\n  if (req.headers['content-length'] === '0') return 'empty'\n  if (req.headers['content-length'] != null) return 'present'\n  return 'missing'\n}\n\nfunction createBodyParser(inputEncoding: string, options: RouteOptions) {\n  if (inputEncoding === ENCODING_ANY) {\n    // When the lexicon's input encoding is */*, the handler will determine how to process it\n    return\n  }\n  const { jsonLimit, textLimit } = options\n  const jsonParser = json({ limit: jsonLimit })\n  const textParser = text({ limit: textLimit })\n  // Transform json and text parser middlewares into a single function\n  return (req: ExpressRequest, res: ExpressResponse) => {\n    return new Promise<void>((resolve, reject) => {\n      jsonParser(req, res, (err) => {\n        if (err) return reject(XRPCError.fromError(err))\n        textParser(req, res, (err) => {\n          if (err) return reject(XRPCError.fromError(err))\n          resolve()\n        })\n      })\n    })\n  }\n}\n\nfunction decodeBodyStream(\n  req: IncomingMessage,\n  maxSize: number | undefined,\n): Readable {\n  const contentEncoding = req.headers['content-encoding']\n  const contentLength = req.headers['content-length']\n\n  const contentLengthParsed = contentLength\n    ? parseInt(contentLength, 10)\n    : undefined\n\n  if (Number.isNaN(contentLengthParsed)) {\n    throw new XRPCError(ResponseType.InvalidRequest, 'invalid content-length')\n  }\n\n  if (\n    maxSize !== undefined &&\n    contentLengthParsed !== undefined &&\n    contentLengthParsed > maxSize\n  ) {\n    throw new XRPCError(\n      ResponseType.PayloadTooLarge,\n      'request entity too large',\n    )\n  }\n\n  let transforms: Duplex[]\n  try {\n    transforms = createDecoders(contentEncoding)\n  } catch (cause) {\n    throw new XRPCError(\n      ResponseType.UnsupportedMediaType,\n      'unsupported content-encoding',\n      undefined,\n      { cause },\n    )\n  }\n\n  if (maxSize !== undefined) {\n    const maxSizeChecker = new MaxSizeChecker(\n      maxSize,\n      () =>\n        new XRPCError(ResponseType.PayloadTooLarge, 'request entity too large'),\n    )\n    transforms.push(maxSizeChecker)\n  }\n\n  return transforms.length > 0\n    ? (pipeline([req, ...transforms], () => {}) as Duplex)\n    : req\n}\n\nexport function serverTimingHeader(timings: ServerTiming[]) {\n  return timings\n    .map((timing) => {\n      let header = timing.name\n      if (timing.duration) header += `;dur=${timing.duration}`\n      if (timing.description) header += `;desc=\"${timing.description}\"`\n      return header\n    })\n    .join(', ')\n}\n\nexport class ServerTimer implements ServerTiming {\n  public duration?: number\n  private startMs?: number\n  constructor(\n    public name: string,\n    public description?: string,\n  ) {}\n  start() {\n    this.startMs = Date.now()\n    return this\n  }\n  stop() {\n    assert(this.startMs, \"timer hasn't been started\")\n    this.duration = Date.now() - this.startMs\n    return this\n  }\n}\n\nexport interface ServerTiming {\n  name: string\n  duration?: number\n  description?: string\n}\n\nexport const parseReqNsid = (req: ExpressRequest | IncomingMessage) =>\n  parseUrlNsid('originalUrl' in req ? req.originalUrl : req.url || '/')\n\n/**\n * Validates and extracts the nsid from an xrpc path\n */\nexport const parseUrlNsid = (url: string): string => {\n  const nsid = extractUrlNsid(url)\n  if (nsid) return nsid\n  throw new InvalidRequestError('invalid xrpc path')\n}\n\nexport const extractUrlNsid = (url: string): string | undefined => {\n  // /!\\ Hot path\n\n  if (\n    // Ordered by likelihood of failure\n    url.length <= 6 ||\n    url[5] !== '/' ||\n    url[4] !== 'c' ||\n    url[3] !== 'p' ||\n    url[2] !== 'r' ||\n    url[1] !== 'x' ||\n    url[0] !== '/'\n  ) {\n    return undefined\n  }\n\n  const startOfNsid = 6\n\n  let curr = startOfNsid\n  let char: number\n  let alphaNumRequired = true\n  for (; curr < url.length; curr++) {\n    char = url.charCodeAt(curr)\n    if (\n      (char >= 48 && char <= 57) || // 0-9\n      (char >= 65 && char <= 90) || // A-Z\n      (char >= 97 && char <= 122) // a-z\n    ) {\n      alphaNumRequired = false\n    } else if (char === 45 /* \"-\" */ || char === 46 /* \".\" */) {\n      if (alphaNumRequired) {\n        return undefined\n      }\n      alphaNumRequired = true\n    } else if (char === 47 /* \"/\" */) {\n      // Allow trailing slash (next char is either EOS or \"?\")\n      if (curr === url.length - 1 || url.charCodeAt(curr + 1) === 63) {\n        break\n      }\n      return undefined\n    } else if (char === 63 /* \"?\"\" */) {\n      break\n    } else {\n      return undefined\n    }\n  }\n\n  // last char was one of: '-', '.', '/'\n  if (alphaNumRequired) {\n    return undefined\n  }\n\n  // A domain name consists of minimum two characters\n  if (curr - startOfNsid < 2) {\n    return undefined\n  }\n\n  // @TODO check max length of nsid\n\n  return url.slice(startOfNsid, curr)\n}\n"
  },
  {
    "path": "packages/xrpc-server/tests/_util.ts",
    "content": "import { once } from 'node:events'\nimport * as http from 'node:http'\nimport express from 'express'\nimport {\n  LexiconDocument,\n  LexiconIterableIndexer,\n  LexiconSchemaBuilder,\n} from '@atproto/lex-document'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport {\n  AuthRequiredError,\n  MethodConfigOrHandler,\n  Options,\n  Server,\n  StreamConfigOrHandler,\n} from '../src'\n\nexport async function createServer({ router }: Server): Promise<http.Server> {\n  const app = express()\n  app.use(router)\n  const httpServer = app.listen(0)\n  await once(httpServer, 'listening')\n  return httpServer\n}\n\nexport async function closeServer(httpServer: http.Server) {\n  await new Promise((r) => {\n    httpServer.close(() => r(undefined))\n  })\n}\n\nexport function createBasicAuth(allowed: {\n  username: string\n  password: string\n}) {\n  return function (ctx: { req: http.IncomingMessage }) {\n    const header = ctx.req.headers.authorization ?? ''\n    if (!header.startsWith('Basic ')) {\n      throw new AuthRequiredError()\n    }\n    const original = header.replace('Basic ', '')\n    const [username, password] = Buffer.from(original, 'base64')\n      .toString()\n      .split(':')\n    if (username !== allowed.username || password !== allowed.password) {\n      throw new AuthRequiredError()\n    }\n    return {\n      credentials: { username },\n      artifacts: { original },\n    }\n  }\n}\n\nexport function basicAuthHeaders(creds: {\n  username: string\n  password: string\n}) {\n  return {\n    authorization:\n      'Basic ' +\n      Buffer.from(`${creds.username}:${creds.password}`).toString('base64'),\n  }\n}\n\n/**\n * Builds a lexicon server based on an `@atproto/lexicon`\n * {@link import('@atproto/lexicon').Lexicons} instance. Validation will be\n * performed by {@link import('@atproto/lexicon').Lexicons}'s various assertion\n * methods. This allows for testing the server's integration with\n * `@atproto/lexicon`.\n */\nexport async function buildMethodLexicons(\n  lexicons: LexiconDoc[],\n  handlers: Record<string, MethodConfigOrHandler | StreamConfigOrHandler>,\n  options?: Options,\n) {\n  const server = new Server(structuredClone(lexicons), options)\n  for (const [id, handler] of Object.entries(handlers)) {\n    const def = server.lex.getDef(id)\n    if (def?.type === 'subscription') {\n      server.addStreamMethod(id, handler as StreamConfigOrHandler)\n    } else {\n      server.method(id, handler as MethodConfigOrHandler)\n    }\n  }\n  return server\n}\n\n/**\n * Builds a lexicon server based on `@atproto/lex`'s\n * {@link import('@atproto/lex').Query},\n * {@link import('@atproto/lex').Procedure}, and\n * {@link import('@atproto/lex').Subscription} method definitions. Validation\n * will be performed through built schema verifiers created by\n * {@link LexiconSchemaBuilder}. This helper allows for testing the server's\n * integration with `@atproto/lex`.\n */\nexport async function buildAddLexicons(\n  lexicons: LexiconDocument[],\n  handlers: Record<string, MethodConfigOrHandler | StreamConfigOrHandler>,\n  options?: Options,\n) {\n  const server = new Server(undefined, options)\n  await using indexer = new LexiconIterableIndexer(structuredClone(lexicons))\n  await using builder = new LexiconSchemaBuilder(indexer)\n  for (const [id, handler] of Object.entries(handlers)) {\n    const schema = await builder.buildFullRef(`${id}#main`)\n    server.add(schema as any, handler as any)\n  }\n  return server\n}\n"
  },
  {
    "path": "packages/xrpc-server/tests/auth.test.ts",
    "content": "import { KeyObject, createPrivateKey } from 'node:crypto'\nimport * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport * as jose from 'jose'\nimport KeyEncoder from 'key-encoder'\nimport { MINUTE } from '@atproto/common'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XRPCError, XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport {\n  basicAuthHeaders,\n  closeServer,\n  createBasicAuth,\n  createServer,\n} from './_util'\n\nconst LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.authTest',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              present: { type: 'boolean', const: true },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              username: { type: 'string' },\n              original: { type: 'string' },\n            },\n          },\n        },\n      },\n    },\n  },\n]\n\ndescribe('Auth', () => {\n  let s: http.Server\n  const server = xrpcServer.createServer(LEXICONS)\n  server.method('io.example.authTest', {\n    auth: createBasicAuth({ username: 'admin', password: 'password' }),\n    handler: ({ auth }) => {\n      return {\n        encoding: 'application/json',\n        body: {\n          username: auth.credentials.username,\n          original: auth.artifacts.original,\n        },\n      }\n    },\n  })\n\n  let client: XrpcClient\n  beforeAll(async () => {\n    s = await createServer(server)\n    const { port } = s.address() as AddressInfo\n    client = new XrpcClient(`http://localhost:${port}`, LEXICONS)\n  })\n\n  afterAll(async () => {\n    await closeServer(s)\n  })\n\n  it('creates and validates service auth headers', async () => {\n    const keypair = await Secp256k1Keypair.create()\n    const iss = 'did:example:alice'\n    const aud = 'did:example:bob'\n    const token = await xrpcServer.createServiceJwt({\n      iss,\n      aud,\n      keypair,\n      lxm: null,\n    })\n    const validated = await xrpcServer.verifyJwt(token, null, null, async () =>\n      keypair.did(),\n    )\n    expect(validated.iss).toEqual(iss)\n    expect(validated.aud).toEqual(aud)\n    // should expire within the minute when no exp is provided\n    expect(validated.exp).toBeGreaterThan(Date.now() / 1000)\n    expect(validated.exp).toBeLessThan(Date.now() / 1000 + 60)\n    expect(typeof validated.jti).toBe('string')\n    expect(validated.lxm).toBeUndefined()\n  })\n\n  it('creates and validates service auth headers bound to a particular method', async () => {\n    const keypair = await Secp256k1Keypair.create()\n    const iss = 'did:example:alice'\n    const aud = 'did:example:bob'\n    const lxm = 'com.atproto.repo.createRecord'\n    const token = await xrpcServer.createServiceJwt({\n      iss,\n      aud,\n      keypair,\n      lxm,\n    })\n    const validated = await xrpcServer.verifyJwt(token, null, lxm, async () =>\n      keypair.did(),\n    )\n    expect(validated.iss).toEqual(iss)\n    expect(validated.aud).toEqual(aud)\n    expect(validated.lxm).toEqual(lxm)\n  })\n\n  it('fails on bad auth before invalid request payload.', async () => {\n    try {\n      await client.call(\n        'io.example.authTest',\n        {},\n        { present: false },\n        {\n          headers: basicAuthHeaders({\n            username: 'admin',\n            password: 'wrong',\n          }),\n        },\n      )\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect(e.success).toBeFalsy()\n      expect(e.error).toBe('AuthenticationRequired')\n      expect(e.message).toBe('Authentication Required')\n      expect(e.status).toBe(401)\n    }\n  })\n\n  it('fails on invalid request payload after good auth.', async () => {\n    try {\n      await client.call(\n        'io.example.authTest',\n        {},\n        { present: false },\n        {\n          headers: basicAuthHeaders({\n            username: 'admin',\n            password: 'password',\n          }),\n        },\n      )\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect(e.success).toBeFalsy()\n      expect(e.error).toBe('InvalidRequest')\n      expect(e.message).toBe('Input/present must be true')\n      expect(e.status).toBe(400)\n    }\n  })\n\n  it('succeeds on good auth and payload.', async () => {\n    const res = await client.call(\n      'io.example.authTest',\n      {},\n      { present: true },\n      {\n        headers: basicAuthHeaders({\n          username: 'admin',\n          password: 'password',\n        }),\n      },\n    )\n    expect(res.success).toBe(true)\n    expect(res.data).toEqual({\n      username: 'admin',\n      original: 'YWRtaW46cGFzc3dvcmQ=',\n    })\n  })\n\n  describe('verifyJwt()', () => {\n    it('fails on expired jwt.', async () => {\n      const keypair = await Secp256k1Keypair.create()\n      const jwt = await xrpcServer.createServiceJwt({\n        aud: 'did:example:aud',\n        iss: 'did:example:iss',\n        keypair,\n        exp: Math.floor((Date.now() - MINUTE) / 1000),\n        lxm: null,\n      })\n      const tryVerify = xrpcServer.verifyJwt(\n        jwt,\n        'did:example:aud',\n        null,\n        async () => {\n          return keypair.did()\n        },\n      )\n      await expect(tryVerify).rejects.toThrow('jwt expired')\n    })\n\n    it('fails on bad audience.', async () => {\n      const keypair = await Secp256k1Keypair.create()\n      const jwt = await xrpcServer.createServiceJwt({\n        aud: 'did:example:aud1',\n        iss: 'did:example:iss',\n        keypair,\n        lxm: null,\n      })\n      const tryVerify = xrpcServer.verifyJwt(\n        jwt,\n        'did:example:aud2',\n        null,\n        async () => {\n          return keypair.did()\n        },\n      )\n      await expect(tryVerify).rejects.toThrow(\n        'jwt audience does not match service did',\n      )\n    })\n\n    it('fails on bad lxm', async () => {\n      const keypair = await Secp256k1Keypair.create()\n      const jwt = await xrpcServer.createServiceJwt({\n        aud: 'did:example:aud1',\n        iss: 'did:example:iss',\n        keypair,\n        lxm: 'com.atproto.repo.createRecord',\n      })\n      const tryVerify = xrpcServer.verifyJwt(\n        jwt,\n        'did:example:aud1',\n        'com.atproto.repo.putRecord',\n        async () => {\n          return keypair.did()\n        },\n      )\n      await expect(tryVerify).rejects.toThrow(/bad jwt lexicon method/)\n    })\n\n    it('fails on null lxm when lxm is required', async () => {\n      const keypair = await Secp256k1Keypair.create()\n      const jwt = await xrpcServer.createServiceJwt({\n        aud: 'did:example:aud1',\n        iss: 'did:example:iss',\n        keypair,\n        lxm: null,\n      })\n      const tryVerify = xrpcServer.verifyJwt(\n        jwt,\n        'did:example:aud1',\n        'com.atproto.repo.putRecord',\n        async () => {\n          return keypair.did()\n        },\n      )\n      await expect(tryVerify).rejects.toThrow(/missing jwt lexicon method/)\n    })\n\n    it('refreshes key on verification failure.', async () => {\n      const keypair1 = await Secp256k1Keypair.create()\n      const keypair2 = await Secp256k1Keypair.create()\n      const jwt = await xrpcServer.createServiceJwt({\n        aud: 'did:example:aud',\n        iss: 'did:example:iss',\n        keypair: keypair2,\n        lxm: null,\n      })\n      let usedKeypair1 = false\n      let usedKeypair2 = false\n      const tryVerify = xrpcServer.verifyJwt(\n        jwt,\n        'did:example:aud',\n        null,\n        async (_did, forceRefresh) => {\n          if (forceRefresh) {\n            usedKeypair2 = true\n            return keypair2.did()\n          } else {\n            usedKeypair1 = true\n            return keypair1.did()\n          }\n        },\n      )\n      await expect(tryVerify).resolves.toMatchObject({\n        aud: 'did:example:aud',\n        iss: 'did:example:iss',\n      })\n      expect(usedKeypair1).toBe(true)\n      expect(usedKeypair2).toBe(true)\n    })\n\n    it('interoperates with jwts signed by other libraries.', async () => {\n      const keypair = await Secp256k1Keypair.create({ exportable: true })\n      const signingKey = await createPrivateKeyObject(keypair)\n      const payload = {\n        aud: 'did:example:aud',\n        iss: 'did:example:iss',\n        exp: Math.floor((Date.now() + MINUTE) / 1000),\n      }\n      const jwt = await new jose.SignJWT(payload)\n        .setProtectedHeader({ typ: 'JWT', alg: keypair.jwtAlg })\n        .sign(signingKey)\n      const tryVerify = xrpcServer.verifyJwt(\n        jwt,\n        'did:example:aud',\n        null,\n        async () => {\n          return keypair.did()\n        },\n      )\n      await expect(tryVerify).resolves.toEqual(payload)\n    })\n  })\n})\n\nconst createPrivateKeyObject = async (\n  privateKey: Secp256k1Keypair,\n): Promise<KeyObject> => {\n  const raw = await privateKey.export()\n  const encoder = new KeyEncoder('secp256k1')\n  const key = encoder.encodePrivate(\n    Buffer.from(raw).toString('hex'),\n    'raw',\n    'pem',\n  )\n  return createPrivateKey({ format: 'pem', key })\n}\n"
  },
  {
    "path": "packages/xrpc-server/tests/bodies.test.ts",
    "content": "import assert from 'node:assert'\nimport * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { Readable } from 'node:stream'\nimport { brotliCompressSync, deflateSync, gzipSync } from 'node:zlib'\nimport { cidForCbor } from '@atproto/common'\nimport { randomBytes } from '@atproto/crypto'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { ResponseType, XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport { logger } from '../src/logger'\nimport {\n  buildAddLexicons,\n  buildMethodLexicons,\n  closeServer,\n  createServer,\n} from './_util'\n\nconst BLOB_LIMIT = 5000\n\nconst LEXICONS = [\n  {\n    lexicon: 1,\n    id: 'io.example.validationTest',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['foo'],\n            properties: {\n              foo: { type: 'string' },\n              bar: { type: 'integer' },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['foo'],\n            properties: {\n              foo: { type: 'string' },\n              bar: { type: 'integer' },\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.validationTestTwo',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['foo'],\n            properties: {\n              foo: { type: 'string' },\n              bar: { type: 'integer' },\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.blobTest',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: '*/*',\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['cid'],\n            properties: {\n              cid: { type: 'string' },\n            },\n          },\n        },\n      },\n    },\n  },\n] as const satisfies LexiconDoc[]\n\nconst handlers = {\n  'io.example.validationTest': (ctx: xrpcServer.HandlerContext) => {\n    assert(!(ctx.input?.body instanceof Readable), 'Input is readable')\n\n    return {\n      encoding: 'application/json',\n      body: ctx.input?.body ?? null,\n    }\n  },\n  'io.example.validationTestTwo': () => {\n    return {\n      encoding: 'application/json',\n      body: { wrong: 'data' },\n    }\n  },\n  'io.example.blobTest': async (ctx: xrpcServer.HandlerContext) => {\n    assert(ctx.input?.body != null, 'Input body is required')\n    const buffer = await consumeInput(ctx.input.body)\n    const cid = await cidForCbor(buffer)\n    return {\n      encoding: 'application/json',\n      body: { cid: cid.toString() },\n    }\n  },\n}\n\nfor (const buildServer of [buildMethodLexicons, buildAddLexicons]) {\n  describe(buildServer, () => {\n    let s: http.Server\n    let client: XrpcClient\n    let url: string\n    beforeAll(async () => {\n      const server = await buildServer(LEXICONS, handlers, {\n        payload: {\n          blobLimit: BLOB_LIMIT,\n        },\n      })\n      s = await createServer(server)\n      const { port } = s.address() as AddressInfo\n      url = `http://localhost:${port}`\n      client = new XrpcClient(url, LEXICONS)\n    })\n    afterAll(async () => {\n      if (s) await closeServer(s)\n    })\n\n    test('io.example.validationTest', async () => {\n      const res = await client.call(\n        'io.example.validationTest',\n        {},\n        { foo: 'hello', bar: 123 },\n      )\n      expect(res.success).toBe(true)\n      expect(res.data.foo).toBe('hello')\n      expect(res.data.bar).toBe(123)\n    })\n\n    test('requires content-type when body is expected', async () => {\n      await expect(\n        client.call('io.example.validationTest', {}),\n      ).rejects.toMatchObject({\n        message: 'Request encoding (Content-Type) required but not provided',\n      })\n    })\n    test('validates required input properties', async () => {\n      await expect(\n        client.call('io.example.validationTest', {}, {}),\n      ).rejects.toMatchObject({\n        error: 'InvalidRequest',\n        message: expect.stringContaining('foo'),\n      })\n    })\n    test('validates input property types', async () => {\n      await expect(\n        client.call('io.example.validationTest', {}, { foo: 123 }),\n      ).rejects.toMatchObject({\n        error: 'InvalidRequest',\n        message: expect.stringContaining('foo'),\n      })\n    })\n    test('rejects invalid encoding for object data', async () => {\n      await expect(\n        client.call(\n          'io.example.validationTest',\n          {},\n          { foo: 'hello', bar: 123 },\n          { encoding: 'image/jpeg' },\n        ),\n      ).rejects.toMatchObject({\n        message: `Unable to encode object as image/jpeg data`,\n      })\n    })\n    test('rejects image/jpeg content-type for json schema', async () => {\n      await expect(\n        client.call(\n          'io.example.validationTest',\n          {},\n          // Does not need to be a valid jpeg\n          new Blob([randomBytes(123)], { type: 'image/jpeg' }),\n        ),\n      ).rejects.toMatchObject({\n        message: `Wrong request encoding (Content-Type): image/jpeg`,\n      })\n    })\n    test('rejects multipart/form-data content-type for json schema', async () => {\n      await expect(\n        client.call(\n          'io.example.validationTest',\n          {},\n          (() => {\n            const formData = new FormData()\n            formData.append('foo', 'bar')\n            return formData\n          })(),\n        ),\n      ).rejects.toMatchObject({\n        message: `Wrong request encoding (Content-Type): multipart/form-data`,\n      })\n    })\n    test('rejects application/x-www-form-urlencoded content-type for json schema', async () => {\n      await expect(\n        client.call(\n          'io.example.validationTest',\n          {},\n          new URLSearchParams([['foo', 'bar']]),\n        ),\n      ).rejects.toMatchObject({\n        message: `Wrong request encoding (Content-Type): application/x-www-form-urlencoded`,\n      })\n    })\n    test('rejects application/octet-stream blob for json schema', async () => {\n      await expect(\n        client.call(\n          'io.example.validationTest',\n          {},\n          new Blob([new Uint8Array([1])]),\n        ),\n      ).rejects.toMatchObject({\n        message: `Wrong request encoding (Content-Type): application/octet-stream`,\n      })\n    })\n    test('rejects application/octet-stream readable stream for json schema', async () => {\n      await expect(\n        client.call(\n          'io.example.validationTest',\n          {},\n          new ReadableStream({\n            pull(ctrl) {\n              ctrl.enqueue(new Uint8Array([1]))\n              ctrl.close()\n            },\n          }),\n        ),\n      ).rejects.toMatchObject({\n        message: `Wrong request encoding (Content-Type): application/octet-stream`,\n      })\n    })\n    test('rejects application/octet-stream uint8array for json schema', async () => {\n      await expect(\n        client.call('io.example.validationTest', {}, new Uint8Array([1])),\n      ).rejects.toMatchObject({\n        message: `Wrong request encoding (Content-Type): application/octet-stream`,\n      })\n    })\n\n    test('validation errors on procedures include details in logs', async () => {\n      // 500 responses don't include details, so we nab details from the logger.\n      const spy = jest.spyOn(logger, 'error')\n      try {\n        await expect(\n          client.call('io.example.validationTestTwo'),\n        ).rejects.toThrow('Internal Server Error')\n\n        expect(spy).toHaveBeenCalledWith(\n          expect.objectContaining({\n            err: expect.objectContaining({\n              message: expect.stringContaining('foo'),\n            }),\n          }),\n          'unhandled exception in xrpc method io.example.validationTestTwo',\n        )\n      } finally {\n        spy.mockRestore()\n      }\n    })\n\n    it('supports ArrayBuffers', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const bytesResponse = await client.call(\n        'io.example.blobTest',\n        {},\n        bytes,\n        {\n          encoding: 'application/octet-stream',\n        },\n      )\n      expect(bytesResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports empty payload on procedues with encoding', async () => {\n      const bytes = new Uint8Array(0)\n      const expectedCid = await cidForCbor(bytes)\n      const bytesResponse = await client.call('io.example.blobTest', {}, bytes)\n      expect(bytesResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports upload of empty txt file', async () => {\n      const txtFile = new Blob([], { type: 'text/plain' })\n      const expectedCid = await cidForCbor(await txtFile.arrayBuffer())\n      const fileResponse = await client.call('io.example.blobTest', {}, txtFile)\n      expect(fileResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports upload of json data', async () => {\n      const jsonFile = new Blob(\n        [Buffer.from(`{\"foo\":\"bar\",\"baz\":[3, null]}`)],\n        {\n          type: 'application/json',\n        },\n      )\n      const expectedCid = await cidForCbor(await jsonFile.arrayBuffer())\n      const fileResponse = await client.call(\n        'io.example.blobTest',\n        {},\n        jsonFile,\n      )\n      expect(fileResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports ArrayBufferView', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const bufferResponse = await client.call(\n        'io.example.blobTest',\n        {},\n        Buffer.from(bytes),\n      )\n      expect(bufferResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports Blob', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const blobResponse = await client.call(\n        'io.example.blobTest',\n        {},\n        new Blob([bytes], { type: 'application/octet-stream' }),\n      )\n      expect(blobResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports Blob without explicit type', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const blobResponse = await client.call(\n        'io.example.blobTest',\n        {},\n        new Blob([bytes]),\n      )\n      expect(blobResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports ReadableStream', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const streamResponse = await client.call(\n        'io.example.blobTest',\n        {},\n        // ReadableStream.from not available in node < 20\n        new ReadableStream({\n          pull(ctrl) {\n            ctrl.enqueue(bytes)\n            ctrl.close()\n          },\n        }),\n      )\n      expect(streamResponse.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports blob uploads', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const { data } = await client.call('io.example.blobTest', {}, bytes, {\n        encoding: 'application/octet-stream',\n      })\n      expect(data.cid).toEqual(expectedCid.toString())\n    })\n\n    it(`supports identity encoding`, async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const { data } = await client.call('io.example.blobTest', {}, bytes, {\n        encoding: 'application/octet-stream',\n        headers: { 'content-encoding': 'identity' },\n      })\n      expect(data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports gzip encoding', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const { data } = await client.call(\n        'io.example.blobTest',\n        {},\n        gzipSync(bytes),\n        {\n          encoding: 'application/octet-stream',\n          headers: {\n            'content-encoding': 'gzip',\n          },\n        },\n      )\n      expect(data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports deflate encoding', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const { data } = await client.call(\n        'io.example.blobTest',\n        {},\n        deflateSync(bytes),\n        {\n          encoding: 'application/octet-stream',\n          headers: {\n            'content-encoding': 'deflate',\n          },\n        },\n      )\n      expect(data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports br encoding', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const { data } = await client.call(\n        'io.example.blobTest',\n        {},\n        brotliCompressSync(bytes),\n        {\n          encoding: 'application/octet-stream',\n          headers: {\n            'content-encoding': 'br',\n          },\n        },\n      )\n      expect(data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports multiple encodings', async () => {\n      const bytes = randomBytes(1024)\n      const expectedCid = await cidForCbor(bytes)\n\n      const { data } = await client.call(\n        'io.example.blobTest',\n        {},\n        brotliCompressSync(deflateSync(gzipSync(bytes))),\n        {\n          encoding: 'application/octet-stream',\n          headers: {\n            'content-encoding':\n              'gzip, identity, deflate, identity, br, identity',\n          },\n        },\n      )\n      expect(data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('fails gracefully on invalid encodings', async () => {\n      const bytes = randomBytes(1024)\n\n      const promise = client.call(\n        'io.example.blobTest',\n        {},\n        brotliCompressSync(bytes),\n        {\n          encoding: 'application/octet-stream',\n          headers: {\n            'content-encoding': 'gzip',\n          },\n        },\n      )\n\n      await expect(promise).rejects.toThrow('unable to read input')\n    })\n\n    it('supports empty payload', async () => {\n      const bytes = new Uint8Array(0)\n      const expectedCid = await cidForCbor(bytes)\n\n      // Using \"undefined\" as body to avoid encoding as lexicon { $bytes: \"<base64>\" }\n      const result = await client.call('io.example.blobTest', {}, bytes, {\n        encoding: 'text/plain',\n      })\n\n      expect(result.data.cid).toEqual(expectedCid.toString())\n    })\n\n    it('supports max blob size (based on content-length)', async () => {\n      const bytes = randomBytes(BLOB_LIMIT + 1)\n\n      // Exactly the number of allowed bytes\n      await client.call('io.example.blobTest', {}, bytes.slice(0, BLOB_LIMIT), {\n        encoding: 'application/octet-stream',\n      })\n\n      // Over the number of allowed bytes\n      const promise = client.call('io.example.blobTest', {}, bytes, {\n        encoding: 'application/octet-stream',\n      })\n\n      await expect(promise).rejects.toThrow('request entity too large')\n    })\n\n    it('supports max blob size (missing content-length)', async () => {\n      // We stream bytes in these tests so that content-length isn't included.\n      const bytes = randomBytes(BLOB_LIMIT + 1)\n\n      // Exactly the number of allowed bytes\n      await client.call(\n        'io.example.blobTest',\n        {},\n        bytesToReadableStream(bytes.slice(0, BLOB_LIMIT)),\n        {\n          encoding: 'application/octet-stream',\n        },\n      )\n\n      // Over the number of allowed bytes.\n      const promise = client.call(\n        'io.example.blobTest',\n        {},\n        bytesToReadableStream(bytes),\n        {\n          encoding: 'application/octet-stream',\n        },\n      )\n\n      await expect(promise).rejects.toThrow('request entity too large')\n    })\n\n    it('requires any parsable Content-Type for blob uploads', async () => {\n      // not a real mimetype, but correct syntax\n      await client.call('io.example.blobTest', {}, randomBytes(BLOB_LIMIT), {\n        encoding: 'some/thing',\n      })\n    })\n\n    it('errors on an empty Content-type on blob upload', async () => {\n      // empty mimetype, but correct syntax\n      const res = await fetch(`${url}/xrpc/io.example.blobTest`, {\n        method: 'post',\n        headers: { 'Content-Type': '' },\n        body: randomBytes(BLOB_LIMIT),\n        // @ts-ignore see note in @atproto/xrpc/client.ts\n        duplex: 'half',\n      })\n      const resBody = await res.json()\n      const status = res.status\n      expect(status).toBe(400)\n      expect(resBody).toMatchObject({\n        error: 'InvalidRequest',\n        message: 'Request encoding (Content-Type) required but not provided',\n      })\n    })\n  })\n}\n\nconst bytesToReadableStream = (bytes: Uint8Array): ReadableStream => {\n  // not using ReadableStream.from(), which lacks support in some contexts including nodejs v18.\n  return new ReadableStream({\n    pull(ctrl) {\n      ctrl.enqueue(bytes)\n      ctrl.close()\n    },\n  })\n}\n\nasync function consumeInput(\n  input: Readable | string | object,\n): Promise<Buffer> {\n  if (Buffer.isBuffer(input)) {\n    return input\n  }\n  if (typeof input === 'string') {\n    return Buffer.from(input)\n  }\n  if (input instanceof Readable) {\n    try {\n      return Buffer.concat(await input.toArray())\n    } catch (err) {\n      if (err instanceof xrpcServer.XRPCError) {\n        throw err\n      } else {\n        throw new xrpcServer.XRPCError(\n          ResponseType.InvalidRequest,\n          'unable to read input',\n        )\n      }\n    }\n  }\n  throw new Error('Invalid input')\n}\n"
  },
  {
    "path": "packages/xrpc-server/tests/errors.test.ts",
    "content": "import * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XRPCError, XRPCInvalidResponseError, XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport { closeServer, createServer } from './_util'\n\nconst UPSTREAM_LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.upstreamInvalidResponse',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['expectedValue'],\n            properties: {\n              expectedValue: { type: 'string' },\n            },\n          },\n        },\n      },\n    },\n  },\n]\n\nconst LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.error',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            which: { type: 'string', default: 'foo' },\n          },\n        },\n        errors: [{ name: 'Foo' }, { name: 'Bar' }],\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.throwFalsyValue',\n    defs: {\n      main: {\n        type: 'query',\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.query',\n    defs: {\n      main: {\n        type: 'query',\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.procedure',\n    defs: {\n      main: {\n        type: 'procedure',\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.invalidResponse',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['expectedValue'],\n            properties: {\n              expectedValue: { type: 'string' },\n            },\n          },\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.invalidUpstreamResponse',\n    defs: {\n      main: {\n        type: 'query',\n      },\n    },\n  },\n]\n\nconst MISMATCHED_LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.query',\n    defs: {\n      main: {\n        type: 'procedure',\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.procedure',\n    defs: {\n      main: {\n        type: 'query',\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.doesNotExist',\n    defs: {\n      main: {\n        type: 'query',\n      },\n    },\n  },\n]\n\ndescribe('Errors', () => {\n  let upstreamS: http.Server\n  const upstreamServer = xrpcServer.createServer(UPSTREAM_LEXICONS, {\n    validateResponse: false,\n  }) // disable validateResponse to test client validation\n  upstreamServer.method('io.example.upstreamInvalidResponse', () => {\n    return { encoding: 'json', body: { something: 'else' } }\n  })\n\n  let upstreamClient: XrpcClient\n\n  let s: http.Server\n  const server = xrpcServer.createServer(LEXICONS, { validateResponse: false }) // disable validateResponse to test client validation\n  server.method('io.example.error', (ctx) => {\n    if (ctx.params.which === 'foo') {\n      throw new xrpcServer.InvalidRequestError('It was this one!', 'Foo')\n    } else if (ctx.params.which === 'bar') {\n      return { status: 400, error: 'Bar', message: 'It was that one!' }\n    } else {\n      return { status: 400 }\n    }\n  })\n  server.method('io.example.throwFalsyValue', () => {\n    throw ''\n  })\n  server.method('io.example.query', () => {\n    return undefined\n  })\n  // @ts-ignore We're intentionally giving the wrong response! -prf\n  server.method('io.example.invalidResponse', () => {\n    return { encoding: 'json', body: { something: 'else' } }\n  })\n  server.method('io.example.invalidUpstreamResponse', async () => {\n    await upstreamClient.call('io.example.upstreamInvalidResponse')\n    return {\n      encoding: 'json',\n    }\n  })\n  server.method('io.example.procedure', () => {\n    return undefined\n  })\n\n  let client: XrpcClient\n  let badClient: XrpcClient\n  beforeAll(async () => {\n    upstreamS = await createServer(upstreamServer)\n    const { port: upstreamPort } = upstreamS.address() as AddressInfo\n    upstreamClient = new XrpcClient(\n      `http://localhost:${upstreamPort}`,\n      UPSTREAM_LEXICONS,\n    )\n\n    s = await createServer(server)\n    const { port } = s.address() as AddressInfo\n    client = new XrpcClient(`http://localhost:${port}`, LEXICONS)\n    badClient = new XrpcClient(`http://localhost:${port}`, MISMATCHED_LEXICONS)\n  })\n  afterAll(async () => {\n    await closeServer(s)\n    await closeServer(upstreamS)\n  })\n\n  it('serves requests', async () => {\n    try {\n      await client.call('io.example.error', {\n        which: 'foo',\n      })\n      throw new Error('Didnt throw')\n    } catch (e) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect((e as XRPCError).success).toBeFalsy()\n      expect((e as XRPCError).error).toBe('Foo')\n      expect((e as XRPCError).message).toBe('It was this one!')\n    }\n    try {\n      await client.call('io.example.error', {\n        which: 'bar',\n      })\n      throw new Error('Didnt throw')\n    } catch (e) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect((e as XRPCError).success).toBeFalsy()\n      expect((e as XRPCError).error).toBe('Bar')\n      expect((e as XRPCError).message).toBe('It was that one!')\n    }\n    try {\n      await client.call('io.example.throwFalsyValue')\n      throw new Error('Didnt throw')\n    } catch (e) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect((e as XRPCError).success).toBeFalsy()\n      expect((e as XRPCError).error).toBe('InternalServerError')\n      expect((e as XRPCError).message).toBe('Internal Server Error')\n    }\n    try {\n      await client.call('io.example.error', {\n        which: 'other',\n      })\n      throw new Error('Didnt throw')\n    } catch (e) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect((e as XRPCError).success).toBeFalsy()\n      expect((e as XRPCError).error).toBe('InvalidRequest')\n      expect((e as XRPCError).message).toBe('Invalid Request')\n    }\n    try {\n      await client.call('io.example.invalidResponse')\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect(e).toBeInstanceOf(XRPCInvalidResponseError)\n      expect(e.success).toBeFalsy()\n      expect(e.error).toBe('Invalid Response')\n      expect(e.message).toBe(\n        'The server gave an invalid response and may be out of date.',\n      )\n      const err = e as XRPCInvalidResponseError\n      expect(err.validationError.message).toBe(\n        'Output must have the property \"expectedValue\"',\n      )\n      expect(err.responseBody).toStrictEqual({ something: 'else' })\n    }\n    try {\n      await client.call('io.example.invalidUpstreamResponse')\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect((e as XRPCError).status).toBe(500)\n      expect((e as XRPCError).success).toBeFalsy()\n      expect((e as XRPCError).error).toBe('InternalServerError')\n      expect((e as XRPCError).message).toBe('Internal Server Error')\n    }\n  })\n\n  it('serves error for missing/mismatch schemas', async () => {\n    await client.call('io.example.query') // No error\n    await client.call('io.example.procedure') // No error\n    try {\n      await badClient.call('io.example.query')\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect(e.success).toBeFalsy()\n      expect(e.error).toBe('InvalidRequest')\n      expect(e.message).toBe('Incorrect HTTP method (POST) expected GET')\n    }\n    try {\n      await badClient.call('io.example.procedure')\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect(e.success).toBeFalsy()\n      expect(e.error).toBe('InvalidRequest')\n      expect(e.message).toBe('Incorrect HTTP method (GET) expected POST')\n    }\n    try {\n      await badClient.call('io.example.doesNotExist')\n      throw new Error('Didnt throw')\n    } catch (e: any) {\n      expect(e).toBeInstanceOf(XRPCError)\n      expect(e.success).toBeFalsy()\n      expect(e.error).toBe('MethodNotImplemented')\n      expect(e.message).toBe('Method Not Implemented')\n    }\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/frames.test.ts",
    "content": "import { encode } from '@atproto/lex-cbor'\nimport { ui8Equals } from '@atproto/lex-data'\nimport { ErrorFrame, Frame, FrameType, MessageFrame } from '../src'\n\ndescribe('Frames', () => {\n  it('creates and parses message frame.', async () => {\n    const messageFrame = new MessageFrame(\n      { a: 'b', c: [1, 2, 3] },\n      { type: '#d' },\n    )\n\n    expect(messageFrame.header).toEqual({\n      op: FrameType.Message,\n      t: '#d',\n    })\n    expect(messageFrame.op).toEqual(FrameType.Message)\n    expect(messageFrame.type).toEqual('#d')\n    expect(messageFrame.body).toEqual({ a: 'b', c: [1, 2, 3] })\n\n    const bytes = messageFrame.toBytes()\n    expect(\n      ui8Equals(\n        bytes,\n        new Uint8Array([\n          /*header*/ 162, 97, 116, 98, 35, 100, 98, 111, 112, 1, /*body*/ 162,\n          97, 97, 97, 98, 97, 99, 131, 1, 2, 3,\n        ]),\n      ),\n    ).toEqual(true)\n\n    const parsedFrame = Frame.fromBytes(bytes)\n    if (!(parsedFrame instanceof MessageFrame)) {\n      throw new Error('Did not parse as message frame')\n    }\n\n    expect(parsedFrame.header).toEqual(messageFrame.header)\n    expect(parsedFrame.op).toEqual(messageFrame.op)\n    expect(parsedFrame.type).toEqual(messageFrame.type)\n    expect(parsedFrame.body).toEqual(messageFrame.body)\n  })\n\n  it('creates and parses error frame.', async () => {\n    const errorFrame = new ErrorFrame({\n      error: 'BigOops',\n      message: 'Something went awry',\n    })\n\n    expect(errorFrame.header).toEqual({ op: FrameType.Error })\n    expect(errorFrame.op).toEqual(FrameType.Error)\n    expect(errorFrame.code).toEqual('BigOops')\n    expect(errorFrame.message).toEqual('Something went awry')\n    expect(errorFrame.body).toEqual({\n      error: 'BigOops',\n      message: 'Something went awry',\n    })\n\n    const bytes = errorFrame.toBytes()\n    expect(\n      ui8Equals(\n        bytes,\n        new Uint8Array([\n          /*header*/ 161, 98, 111, 112, 32, /*body*/ 162, 101, 101, 114, 114,\n          111, 114, 103, 66, 105, 103, 79, 111, 112, 115, 103, 109, 101, 115,\n          115, 97, 103, 101, 115, 83, 111, 109, 101, 116, 104, 105, 110, 103,\n          32, 119, 101, 110, 116, 32, 97, 119, 114, 121,\n        ]),\n      ),\n    ).toEqual(true)\n\n    const parsedFrame = Frame.fromBytes(bytes)\n    if (!(parsedFrame instanceof ErrorFrame)) {\n      throw new Error('Did not parse as error frame')\n    }\n\n    expect(parsedFrame.header).toEqual(errorFrame.header)\n    expect(parsedFrame.op).toEqual(errorFrame.op)\n    expect(parsedFrame.code).toEqual(errorFrame.code)\n    expect(parsedFrame.message).toEqual(errorFrame.message)\n    expect(parsedFrame.body).toEqual(errorFrame.body)\n  })\n\n  it('parsing fails when frame is not CBOR.', async () => {\n    const bytes = Buffer.from('some utf8 bytes')\n    const emptyBytes = Buffer.alloc(0)\n    expect(() => Frame.fromBytes(bytes)).toThrow()\n    expect(() => Frame.fromBytes(emptyBytes)).toThrow()\n  })\n\n  it('parsing fails when frame header is malformed.', async () => {\n    const bytes = Buffer.concat([\n      encode({ op: -2 }), // Unknown op\n      encode({ a: 'b', c: [1, 2, 3] }),\n    ])\n\n    expect(() => Frame.fromBytes(bytes)).toThrow('Invalid frame header:')\n  })\n\n  it('parsing fails when frame is missing body.', async () => {\n    const messageFrame = new MessageFrame(\n      { a: 'b', c: [1, 2, 3] },\n      { type: '#d' },\n    )\n\n    const headerBytes = encode(messageFrame.header)\n\n    expect(() => Frame.fromBytes(headerBytes)).toThrow('Missing frame body')\n  })\n\n  it('parsing fails when frame has too many data items.', async () => {\n    const messageFrame = new MessageFrame(\n      { a: 'b', c: [1, 2, 3] },\n      { type: '#d' },\n    )\n\n    const bytes = Buffer.concat([\n      messageFrame.toBytes(),\n      encode({ d: 'e', f: [4, 5, 6] }),\n    ])\n\n    expect(() => Frame.fromBytes(bytes)).toThrow(\n      'Too many CBOR data items in frame',\n    )\n  })\n\n  it('parsing fails when error frame has invalid body.', async () => {\n    const errorFrame = new ErrorFrame({ error: 'BadOops' })\n\n    const bytes = Buffer.concat([\n      encode(errorFrame.header),\n      encode({ blah: 1 }),\n    ])\n\n    expect(() => Frame.fromBytes(bytes)).toThrow('Invalid error frame body:')\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/ipld.test.ts",
    "content": "import assert from 'node:assert'\nimport * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { CID } from 'multiformats/cid'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport { closeServer, createServer } from './_util'\n\nconst LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.ipld',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cid: {\n                type: 'cid-link',\n              },\n              bytes: {\n                type: 'bytes',\n              },\n            },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            properties: {\n              cid: {\n                type: 'cid-link',\n              },\n              bytes: {\n                type: 'bytes',\n              },\n            },\n          },\n        },\n      },\n    },\n  },\n]\n\ndescribe('Ipld vals', () => {\n  let s: http.Server\n  const server = xrpcServer.createServer(LEXICONS)\n  server.method('io.example.ipld', (ctx) => {\n    assert(ctx.input?.body, 'expected input body')\n    assert(typeof ctx.input.body === 'object', 'expected input body')\n\n    const asCid = CID.asCID(ctx.input.body['cid'])\n    if (!(asCid instanceof CID)) {\n      throw new Error('expected cid')\n    }\n    const bytes = ctx.input.body['bytes']\n    if (!(bytes instanceof Uint8Array)) {\n      throw new Error('expected bytes')\n    }\n    return { encoding: 'application/json', body: ctx.input.body }\n  })\n\n  let client: XrpcClient\n  beforeAll(async () => {\n    s = await createServer(server)\n    const { port } = s.address() as AddressInfo\n    client = new XrpcClient(`http://localhost:${port}`, LEXICONS)\n  })\n  afterAll(async () => {\n    await closeServer(s)\n  })\n\n  it('can send and receive ipld vals', async () => {\n    const cid = CID.parse(\n      'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',\n    )\n    const bytes = new Uint8Array([0, 1, 2, 3])\n    const res = await client.call(\n      'io.example.ipld',\n      {},\n      {\n        cid,\n        bytes,\n      },\n      { encoding: 'application/json' },\n    )\n    expect(res.success).toBeTruthy()\n    expect(res.headers['content-type']).toBe('application/json; charset=utf-8')\n    expect(cid.equals(res.data.cid)).toBeTruthy()\n    expect(bytes).toEqual(res.data.bytes)\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/parameters.test.ts",
    "content": "import * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport { closeServer, createServer } from './_util'\n\nconst LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.paramTest',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['str', 'int', 'bool', 'arr'],\n          properties: {\n            str: { type: 'string', minLength: 2, maxLength: 10 },\n            int: { type: 'integer', minimum: 2, maximum: 10 },\n            bool: { type: 'boolean' },\n            arr: { type: 'array', items: { type: 'integer' }, maxLength: 2 },\n            def: { type: 'integer', default: 0 },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n]\n\ndescribe('Parameters', () => {\n  let s: http.Server\n  const server = xrpcServer.createServer(LEXICONS)\n  server.method('io.example.paramTest', (ctx) => ({\n    encoding: 'json',\n    body: ctx.params,\n  }))\n\n  let client: XrpcClient\n  beforeAll(async () => {\n    s = await createServer(server)\n    const { port } = s.address() as AddressInfo\n    client = new XrpcClient(`http://localhost:${port}`, LEXICONS)\n  })\n  afterAll(async () => {\n    await closeServer(s)\n  })\n\n  it('validates query params', async () => {\n    const res1 = await client.call('io.example.paramTest', {\n      str: 'valid',\n      int: 5,\n      bool: true,\n      arr: [1, 2],\n      def: 5,\n    })\n    expect(res1.success).toBeTruthy()\n    expect(res1.data.str).toBe('valid')\n    expect(res1.data.int).toBe(5)\n    expect(res1.data.bool).toBe(true)\n    expect(res1.data.arr).toEqual([1, 2])\n    expect(res1.data.def).toEqual(5)\n\n    const res2 = await client.call('io.example.paramTest', {\n      str: 10,\n      int: '5',\n      bool: 'foo',\n      arr: '3',\n    })\n    expect(res2.success).toBeTruthy()\n    expect(res2.data.str).toBe('10')\n    expect(res2.data.int).toBe(5)\n    expect(res2.data.bool).toBe(true)\n    expect(res2.data.arr).toEqual([3])\n    expect(res2.data.def).toEqual(0)\n\n    // @TODO test sending blatantly bad types\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'n',\n        int: 5,\n        bool: true,\n        arr: [1],\n      }),\n    ).rejects.toThrow('str must not be shorter than 2 characters')\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'loooooooooooooong',\n        int: 5,\n        bool: true,\n        arr: [1],\n      }),\n    ).rejects.toThrow('str must not be longer than 10 characters')\n    await expect(\n      client.call('io.example.paramTest', {\n        int: 5,\n        bool: true,\n        arr: [1],\n      }),\n    ).rejects.toThrow(`Params must have the property \"str\"`)\n\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'valid',\n        int: -1,\n        bool: true,\n        arr: [1],\n      }),\n    ).rejects.toThrow('int can not be less than 2')\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'valid',\n        int: 11,\n        bool: true,\n        arr: [1],\n      }),\n    ).rejects.toThrow('int can not be greater than 10')\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'valid',\n        bool: true,\n        arr: [1],\n      }),\n    ).rejects.toThrow(`Params must have the property \"int\"`)\n\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'valid',\n        int: 5,\n        arr: [1],\n      }),\n    ).rejects.toThrow(`Params must have the property \"bool\"`)\n\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'valid',\n        int: 5,\n        bool: true,\n        arr: [],\n      }),\n    ).rejects.toThrow('Error: Params must have the property \"arr\"')\n    await expect(\n      client.call('io.example.paramTest', {\n        str: 'valid',\n        int: 5,\n        bool: true,\n        arr: [1, 2, 3],\n      }),\n    ).rejects.toThrow('Error: arr must not have more than 2 elements')\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/parsing.test.ts",
    "content": "import { parseUrlNsid } from '../src/util'\n\nconst testValid = (url: string, expected: string) => {\n  expect(parseUrlNsid(url)).toBe(expected)\n}\n\nconst testInvalid = (url: string, errorMessage = 'invalid xrpc path') => {\n  expect(() => parseUrlNsid(url)).toThrow(errorMessage)\n}\n\ndescribe('parseUrlNsid', () => {\n  it('should extract the NSID from the URL', () => {\n    testValid('/xrpc/blee.blah.bloo', 'blee.blah.bloo')\n    testValid('/xrpc/blee.blah.bloo?foo[]', 'blee.blah.bloo')\n    testValid('/xrpc/blee.blah.bloo?foo=bar', 'blee.blah.bloo')\n    testValid('/xrpc/com.example.nsid', 'com.example.nsid')\n    testValid('/xrpc/com.example.nsid?foo=bar', 'com.example.nsid')\n    testValid('/xrpc/com.example-domain.nsid', 'com.example-domain.nsid')\n  })\n\n  it('should allow a trailing slash', () => {\n    testValid('/xrpc/blee.blah.bloo/?', 'blee.blah.bloo')\n    testValid('/xrpc/blee.blah.bloo/?foo=', 'blee.blah.bloo')\n    testValid('/xrpc/blee.blah.bloo/?bool', 'blee.blah.bloo')\n    testValid('/xrpc/com.example.nsid/', 'com.example.nsid')\n  })\n\n  it('should throw an error if the URL is too short', () => {\n    testInvalid('/xrpc/a')\n  })\n\n  it('should throw an error if the URL is empty', () => {\n    testInvalid('')\n  })\n\n  it('should throw an error if the URL is missing the NSID', () => {\n    testInvalid('/xrpc/')\n    testInvalid('/xrpc/?')\n    testInvalid('/xrpc/?foo=bar')\n  })\n\n  it('should throw an error if the URL contains extra path segments', () => {\n    testInvalid('/xrpc/123/extra')\n    testInvalid('/xrpc/123/extra?foo=bar')\n  })\n\n  it('should throw an error if the URL is missing the XRPC path prefix', () => {\n    testInvalid('/foo/123')\n    testInvalid('/foo/com.example.nsid')\n  })\n\n  it('should throw an error if the NSID starts with a dot', () => {\n    testInvalid('/xrpc/.')\n    testInvalid('/xrpc/..')\n    testInvalid('/xrpc/....')\n    testInvalid('/xrpc/.com.example.nsid')\n    testInvalid('/xrpc/com..example.nsid')\n    testInvalid('/xrpc/com.example..nsid')\n    testInvalid('/xrpc/com.example.nsid.')\n    testInvalid('/xrpc/com.example.nsid./')\n    testInvalid('/xrpc/com.example.nsid.?foo=bar')\n    testInvalid('/xrpc/com.example.nsid./?foo=bar')\n  })\n\n  it('should throw an error if the NSID contains a misplaced dash', () => {\n    testInvalid('/xrpc/-')\n    testInvalid('/xrpc/com.example.-nsid')\n    testInvalid('/xrpc/com.example-.nsid')\n    testInvalid('/xrpc/com.-example.nsid')\n    testInvalid('/xrpc/com.-example-.nsid')\n    testInvalid('/xrpc/com.example.nsid-')\n    testInvalid('/xrpc/-com.example.nsid')\n    testInvalid('/xrpc/com.example--domain.nsid')\n  })\n\n  it('should throw an error if the URL starts with a space', () => {\n    testInvalid(' /xrpc/com.example.nsid')\n  })\n\n  it('should throw an error if the NSID contains invalid characters', () => {\n    testInvalid('/xrpc/com.example.nsid#')\n    testInvalid('/xrpc/com.example.nsid!')\n    testInvalid('/xrpc/com.example#?nsid')\n    testInvalid('/xrpc/!com.example.nsid')\n    testInvalid('/xrpc/com.example.nsid ')\n    testInvalid('/xrpc/ com.example.nsid')\n    testInvalid('/xrpc/com. example.nsid')\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/procedures.test.ts",
    "content": "import assert from 'node:assert'\nimport * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { Readable } from 'node:stream'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport {\n  buildAddLexicons,\n  buildMethodLexicons,\n  closeServer,\n  createServer,\n} from './_util'\n\nconst LEXICONS = [\n  {\n    lexicon: 1,\n    id: 'io.example.pingOne',\n    defs: {\n      main: {\n        type: 'procedure',\n        parameters: {\n          type: 'params',\n          properties: {\n            message: { type: 'string' },\n          },\n        },\n        output: {\n          encoding: 'text/plain',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.pingTwo',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'text/plain',\n        },\n        output: {\n          encoding: 'text/plain',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.pingThree',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/octet-stream',\n        },\n        output: {\n          encoding: 'application/octet-stream',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.pingFour',\n    defs: {\n      main: {\n        type: 'procedure',\n        input: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: { message: { type: 'string' } },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: { message: { type: 'string' } },\n          },\n        },\n      },\n    },\n  },\n] as const satisfies LexiconDoc[]\n\nconst handlers = {\n  'io.example.pingOne': (ctx: xrpcServer.HandlerContext) => {\n    return { encoding: 'text/plain', body: ctx.params.message }\n  },\n  'io.example.pingTwo': (ctx: xrpcServer.HandlerContext) => {\n    return { encoding: 'text/plain', body: ctx.input?.body }\n  },\n  'io.example.pingThree': async (ctx: xrpcServer.HandlerContext) => {\n    assert(ctx.input?.body instanceof Readable, 'Input not readable')\n    const buffers: Buffer[] = []\n    for await (const data of ctx.input.body) {\n      buffers.push(data)\n    }\n    return {\n      encoding: 'application/octet-stream',\n      body: Buffer.concat(buffers),\n    }\n  },\n  'io.example.pingFour': (ctx: xrpcServer.HandlerContext) => {\n    return {\n      encoding: 'application/json',\n      body: { message: ctx.input?.body?.['message'] },\n    }\n  },\n}\n\nfor (const buildServer of [buildMethodLexicons, buildAddLexicons]) {\n  describe(buildServer, () => {\n    let s: http.Server\n    let client: XrpcClient\n    let url: string\n    beforeAll(async () => {\n      const server = await buildServer(LEXICONS, handlers)\n      s = await createServer(server)\n      const { port } = s.address() as AddressInfo\n      url = `http://localhost:${port}`\n      client = new XrpcClient(url, LEXICONS)\n    })\n    afterAll(async () => {\n      if (s) await closeServer(s)\n    })\n\n    test('io.example.pingOne', async () => {\n      const res = await client.call('io.example.pingOne', {\n        message: 'hello world',\n      })\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')\n      expect(res.data).toBe('hello world')\n    })\n\n    test('io.example.pingTwo', async () => {\n      const res = await client.call('io.example.pingTwo', {}, 'hello world', {\n        encoding: 'text/plain',\n      })\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')\n      expect(res.data).toBe('hello world')\n    })\n\n    test('io.example.pingThree', async () => {\n      const res = await client.call(\n        'io.example.pingThree',\n        {},\n        new TextEncoder().encode('hello world'),\n        { encoding: 'application/octet-stream' },\n      )\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe('application/octet-stream')\n      expect(new TextDecoder().decode(res.data)).toBe('hello world')\n    })\n\n    test('io.example.pingFour', async () => {\n      const res = await client.call(\n        'io.example.pingFour',\n        {},\n        { message: 'hello world' },\n      )\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe(\n        'application/json; charset=utf-8',\n      )\n      expect(res.data?.message).toBe('hello world')\n    })\n  })\n}\n"
  },
  {
    "path": "packages/xrpc-server/tests/queries.test.ts",
    "content": "import * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport {\n  buildAddLexicons,\n  buildMethodLexicons,\n  closeServer,\n  createServer,\n} from './_util'\n\nconst LEXICONS = [\n  {\n    lexicon: 1,\n    id: 'io.example.pingOne',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            message: { type: 'string' },\n          },\n        },\n        output: {\n          encoding: 'text/plain',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.pingTwo',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            message: { type: 'string' },\n          },\n        },\n        output: {\n          encoding: 'application/octet-stream',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.pingThree',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            message: { type: 'string' },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n          schema: {\n            type: 'object',\n            required: ['message'],\n            properties: { message: { type: 'string' } },\n          },\n        },\n      },\n    },\n  },\n] as const satisfies LexiconDoc[]\n\nconst handlers = {\n  'io.example.pingOne': (ctx: xrpcServer.HandlerContext) => {\n    return { encoding: 'text/plain', body: ctx.params.message }\n  },\n  'io.example.pingTwo': (ctx: xrpcServer.HandlerContext) => {\n    return {\n      encoding: 'application/octet-stream',\n      body: new TextEncoder().encode(String(ctx.params.message)),\n    }\n  },\n  'io.example.pingThree': (ctx: xrpcServer.HandlerContext) => {\n    return {\n      encoding: 'application/json',\n      body: { message: ctx.params.message },\n      headers: { 'x-test-header-name': 'test-value' },\n    }\n  },\n}\n\nfor (const buildServer of [buildMethodLexicons, buildAddLexicons]) {\n  describe(buildServer, () => {\n    let s: http.Server\n    let client: XrpcClient\n    let url: string\n    beforeAll(async () => {\n      const server = await buildServer(LEXICONS, handlers)\n      s = await createServer(server)\n      const { port } = s.address() as AddressInfo\n      url = `http://localhost:${port}`\n      client = new XrpcClient(url, LEXICONS)\n    })\n    afterAll(async () => {\n      if (s) await closeServer(s)\n    })\n\n    test('io.example.pingOne', async () => {\n      const res = await client.call('io.example.pingOne', {\n        message: 'hello world',\n      })\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')\n      expect(res.data).toBe('hello world')\n    })\n\n    test('io.example.pingTwo', async () => {\n      const res = await client.call('io.example.pingTwo', {\n        message: 'hello world',\n      })\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe('application/octet-stream')\n      expect(new TextDecoder().decode(res.data)).toBe('hello world')\n    })\n\n    test('io.example.pingThree', async () => {\n      const res = await client.call('io.example.pingThree', {\n        message: 'hello world',\n      })\n      expect(res.success).toBeTruthy()\n      expect(res.headers['content-type']).toBe(\n        'application/json; charset=utf-8',\n      )\n      expect(res.data?.message).toBe('hello world')\n      expect(res.headers['x-test-header-name']).toEqual('test-value')\n    })\n  })\n}\n"
  },
  {
    "path": "packages/xrpc-server/tests/rate-limiter.test.ts",
    "content": "import * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { MINUTE } from '@atproto/common'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport { MemoryRateLimiter } from '../src'\nimport { closeServer, createServer } from './_util'\n\nconst LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.routeLimit',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['str'],\n          properties: {\n            str: { type: 'string' },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.routeLimitReset',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['count'],\n          properties: {\n            count: { type: 'integer' },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.sharedLimitOne',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['points'],\n          properties: {\n            points: { type: 'integer' },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.sharedLimitTwo',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          required: ['points'],\n          properties: {\n            points: { type: 'integer' },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.toggleLimit',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            shouldCount: { type: 'boolean' },\n          },\n        },\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.noLimit',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.nonExistent',\n    defs: {\n      main: {\n        type: 'query',\n        output: {\n          encoding: 'application/json',\n        },\n      },\n    },\n  },\n]\n\ndescribe('Parameters', () => {\n  let s: http.Server\n  const server = xrpcServer.createServer(LEXICONS, {\n    rateLimits: {\n      creator: (opts) => new MemoryRateLimiter(opts),\n      bypass: ({ req }) => req.headers['x-ratelimit-bypass'] === 'bypass',\n      shared: [\n        {\n          name: 'shared-limit',\n          durationMs: 5 * MINUTE,\n          points: 6,\n        },\n      ],\n      global: [\n        {\n          name: 'global-ip',\n          durationMs: 5 * MINUTE,\n          points: 100,\n        },\n      ],\n    },\n  })\n  server.method('io.example.routeLimit', {\n    rateLimit: {\n      durationMs: 5 * MINUTE,\n      points: 5,\n      calcKey: ({ params }) => params.str as string,\n    },\n    handler: (ctx) => ({\n      encoding: 'json',\n      body: ctx.params,\n    }),\n  })\n  server.method('io.example.routeLimitReset', {\n    rateLimit: {\n      durationMs: 5 * MINUTE,\n      points: 2,\n    },\n    handler: (ctx) => {\n      if (ctx.params.count === 1) {\n        ctx.resetRouteRateLimits()\n      }\n\n      return {\n        encoding: 'json',\n        body: {},\n      }\n    },\n  })\n  server.method('io.example.sharedLimitOne', {\n    rateLimit: {\n      name: 'shared-limit',\n      calcPoints: ({ params }) => params.points as number,\n    },\n    handler: (ctx) => ({\n      encoding: 'json',\n      body: ctx.params,\n    }),\n  })\n  server.method('io.example.sharedLimitTwo', {\n    rateLimit: {\n      name: 'shared-limit',\n      calcPoints: ({ params }) => params.points as number,\n    },\n    handler: (ctx) => ({\n      encoding: 'json',\n      body: ctx.params,\n    }),\n  })\n  server.method('io.example.toggleLimit', {\n    rateLimit: [\n      {\n        durationMs: 5 * MINUTE,\n        points: 5,\n        calcPoints: ({ params }) => (params.shouldCount ? 1 : 0),\n      },\n      {\n        durationMs: 5 * MINUTE,\n        points: 10,\n      },\n    ],\n    handler: (ctx) => ({\n      encoding: 'json',\n      body: ctx.params,\n    }),\n  })\n  server.method('io.example.noLimit', {\n    handler: () => ({\n      encoding: 'json',\n      body: {},\n    }),\n  })\n\n  let client: XrpcClient\n  beforeAll(async () => {\n    s = await createServer(server)\n    const { port } = s.address() as AddressInfo\n    client = new XrpcClient(`http://localhost:${port}`, LEXICONS)\n  })\n  afterAll(async () => {\n    await closeServer(s)\n  })\n\n  it('rate limits a given route', async () => {\n    const makeCall = () => client.call('io.example.routeLimit', { str: 'test' })\n    for (let i = 0; i < 5; i++) {\n      await makeCall()\n    }\n    await expect(makeCall).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('can reset route rate limits', async () => {\n    // Limit is 2.\n    // Call 0 is OK (1/2).\n    // Call 1 is OK (2/2), and resets the limit.\n    // Call 2 is OK (1/2).\n    // Call 3 is OK (2/2).\n    for (let i = 0; i < 4; i++) {\n      await client.call('io.example.routeLimitReset', { count: i })\n    }\n\n    // Call 4 exceeds the limit (3/2).\n    await expect(\n      client.call('io.example.routeLimitReset', { count: 4 }),\n    ).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('rate limits on a shared route', async () => {\n    await client.call('io.example.sharedLimitOne', { points: 1 })\n    await client.call('io.example.sharedLimitTwo', { points: 1 })\n    await client.call('io.example.sharedLimitOne', { points: 2 })\n    await client.call('io.example.sharedLimitTwo', { points: 2 })\n    await expect(\n      client.call('io.example.sharedLimitOne', { points: 1 }),\n    ).rejects.toThrow('Rate Limit Exceeded')\n    await expect(\n      client.call('io.example.sharedLimitTwo', { points: 1 }),\n    ).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('applies multiple rate-limits', async () => {\n    const makeCall = (shouldCount: boolean) =>\n      client.call('io.example.toggleLimit', { shouldCount })\n    for (let i = 0; i < 5; i++) {\n      await makeCall(true)\n    }\n    await expect(() => makeCall(true)).rejects.toThrow('Rate Limit Exceeded')\n    for (let i = 0; i < 4; i++) {\n      await makeCall(false)\n    }\n    await expect(() => makeCall(false)).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('applies global limits', async () => {\n    const makeCall = () => client.call('io.example.noLimit')\n    const calls: Promise<unknown>[] = []\n    for (let i = 0; i < 110; i++) {\n      calls.push(makeCall())\n    }\n    await expect(Promise.all(calls)).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('applies global limits to xrpc catchall', async () => {\n    const makeCall = () => client.call('io.example.nonExistent')\n    await expect(makeCall()).rejects.toThrow('Rate Limit Exceeded')\n  })\n\n  it('can bypass rate limits', async () => {\n    const makeCall = () =>\n      client.call(\n        'io.example.noLimit',\n        {},\n        {},\n        { headers: { 'X-RateLimit-Bypass': 'bypass' } },\n      )\n    const calls: Promise<unknown>[] = []\n    for (let i = 0; i < 110; i++) {\n      calls.push(makeCall())\n    }\n    await Promise.all(calls)\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/responses.test.ts",
    "content": "import * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { byteIterableToStream } from '@atproto/common'\nimport { LexiconDoc } from '@atproto/lexicon'\nimport { XrpcClient } from '@atproto/xrpc'\nimport * as xrpcServer from '../src'\nimport { closeServer, createServer } from './_util'\n\nconst LEXICONS: LexiconDoc[] = [\n  {\n    lexicon: 1,\n    id: 'io.example.readableStream',\n    defs: {\n      main: {\n        type: 'query',\n        parameters: {\n          type: 'params',\n          properties: {\n            shouldErr: { type: 'boolean' },\n          },\n        },\n        output: {\n          encoding: 'application/vnd.ipld.car',\n        },\n      },\n    },\n  },\n]\n\ndescribe('Responses', () => {\n  let s: http.Server\n  const server = xrpcServer.createServer(LEXICONS)\n  server.method('io.example.readableStream', async (ctx) => {\n    async function* iter(): AsyncIterable<Uint8Array> {\n      for (let i = 0; i < 5; i++) {\n        yield new Uint8Array([i])\n      }\n      if (ctx.params.shouldErr) {\n        throw new Error('error')\n      }\n    }\n    return {\n      encoding: 'application/vnd.ipld.car',\n      body: byteIterableToStream(iter()),\n    }\n  })\n\n  let client: XrpcClient\n  beforeAll(async () => {\n    s = await createServer(server)\n    const { port } = s.address() as AddressInfo\n    client = new XrpcClient(`http://localhost:${port}`, LEXICONS)\n  })\n  afterAll(async () => {\n    await closeServer(s)\n  })\n\n  it('returns readable streams of bytes', async () => {\n    const res = await client.call('io.example.readableStream', {\n      shouldErr: false,\n    })\n    const expected = new Uint8Array([0, 1, 2, 3, 4])\n    expect(res.data).toEqual(expected)\n  })\n\n  it('handles errs on readable streams of bytes', async () => {\n    const attempt = client.call('io.example.readableStream', {\n      shouldErr: true,\n    })\n    await expect(attempt).rejects.toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/stream.test.ts",
    "content": "import { once } from 'node:events'\nimport * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { WebSocket } from 'ws'\nimport { XRPCError } from '@atproto/xrpc'\nimport {\n  ErrorFrame,\n  Frame,\n  MessageFrame,\n  XrpcStreamServer,\n  byFrame,\n  byMessage,\n} from '../src'\n\ndescribe('Stream', () => {\n  const wait = (ms) => new Promise((res) => setTimeout(res, ms))\n  it('streams message and info frames.', async () => {\n    const httpServer = http.createServer()\n    const server = new XrpcStreamServer({\n      server: httpServer,\n      handler: async function* () {\n        await wait(1)\n        yield new MessageFrame(1)\n        await wait(1)\n        yield new MessageFrame(2)\n        await wait(1)\n        yield new MessageFrame(3)\n        return\n      },\n    })\n\n    await once(httpServer.listen(), 'listening')\n    const { port } = server.wss.address() as AddressInfo\n\n    const ws = new WebSocket(`ws://localhost:${port}`)\n    const frames: Frame[] = []\n    for await (const frame of byFrame(ws)) {\n      frames.push(frame)\n    }\n\n    expect(frames).toEqual([\n      new MessageFrame(1),\n      new MessageFrame(2),\n      new MessageFrame(3),\n    ])\n\n    httpServer.close()\n  })\n\n  it('kills handler and closes on error frame.', async () => {\n    let proceededAfterError = false\n    const httpServer = http.createServer()\n    const server = new XrpcStreamServer({\n      server: httpServer,\n      handler: async function* () {\n        await wait(1)\n        yield new MessageFrame(1)\n        await wait(1)\n        yield new MessageFrame(2)\n        await wait(1)\n        yield new ErrorFrame({ error: 'BadOops' })\n        proceededAfterError = true\n        await wait(1)\n        yield new MessageFrame(3)\n        return\n      },\n    })\n\n    await once(httpServer.listen(), 'listening')\n    const { port } = server.wss.address() as AddressInfo\n\n    const ws = new WebSocket(`ws://localhost:${port}`)\n    const frames: Frame[] = []\n    for await (const frame of byFrame(ws)) {\n      frames.push(frame)\n    }\n\n    await wait(5) // Ensure handler hasn't kept running\n    expect(proceededAfterError).toEqual(false)\n\n    expect(frames).toEqual([\n      new MessageFrame(1),\n      new MessageFrame(2),\n      new ErrorFrame({ error: 'BadOops' }),\n    ])\n\n    httpServer.close()\n  })\n\n  it('kills handler and closes client disconnect.', async () => {\n    const httpServer = http.createServer()\n    let i = 1\n    const server = new XrpcStreamServer({\n      server: httpServer,\n      handler: async function* () {\n        while (true) {\n          await wait(0)\n          yield new MessageFrame(i++)\n        }\n      },\n    })\n\n    await once(httpServer.listen(), 'listening')\n    const { port } = server.wss.address() as AddressInfo\n\n    const ws = new WebSocket(`ws://localhost:${port}`)\n    const frames: Frame[] = []\n    for await (const frame of byFrame(ws)) {\n      frames.push(frame)\n      if (frame.body === 3) ws.terminate()\n    }\n\n    // Grace period to let close take place on the server\n    await wait(5)\n    // Ensure handler hasn't kept running\n    const currentCount = i\n    await wait(5)\n    expect(i).toBe(currentCount)\n\n    httpServer.close()\n  })\n\n  describe('byMessage()', () => {\n    it('kills handler and closes client disconnect on error frame.', async () => {\n      const httpServer = http.createServer()\n      const server = new XrpcStreamServer({\n        server: httpServer,\n        handler: async function* () {\n          await wait(1)\n          yield new MessageFrame(1)\n          await wait(1)\n          yield new MessageFrame(2)\n          await wait(1)\n          yield new ErrorFrame({\n            error: 'BadOops',\n            message: 'That was a bad one',\n          })\n          await wait(1)\n          yield new MessageFrame(3)\n          return\n        },\n      })\n      await once(httpServer.listen(), 'listening')\n      const { port } = server.wss.address() as AddressInfo\n\n      const ws = new WebSocket(`ws://localhost:${port}`)\n      const frames: Frame[] = []\n\n      let error\n      try {\n        for await (const frame of byMessage(ws)) {\n          frames.push(frame)\n        }\n      } catch (err) {\n        error = err\n      }\n\n      expect(ws.readyState).toEqual(ws.CLOSING)\n      expect(frames).toEqual([new MessageFrame(1), new MessageFrame(2)])\n      expect(error).toBeInstanceOf(XRPCError)\n      if (error instanceof XRPCError) {\n        expect(error.error).toEqual('BadOops')\n        expect(error.message).toEqual('That was a bad one')\n      }\n\n      httpServer.close()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/xrpc-server/tests/subscriptions.test.ts",
    "content": "import * as http from 'node:http'\nimport { AddressInfo } from 'node:net'\nimport { WebSocket, createWebSocketStream } from 'ws'\nimport { wait } from '@atproto/common'\nimport { LexiconDoc, Lexicons } from '@atproto/lexicon'\nimport { ErrorFrame, Frame, MessageFrame, Subscription, byFrame } from '../src'\nimport * as xrpcServer from '../src'\nimport {\n  basicAuthHeaders,\n  buildAddLexicons,\n  buildMethodLexicons,\n  closeServer,\n  createBasicAuth,\n  createServer,\n} from './_util'\n\nconst LEXICONS = [\n  {\n    lexicon: 1,\n    id: 'io.example.streamOne',\n    defs: {\n      main: {\n        type: 'subscription',\n        parameters: {\n          type: 'params',\n          required: ['countdown'],\n          properties: {\n            countdown: { type: 'integer' },\n          },\n        },\n        message: {\n          schema: { type: 'union', refs: ['#countdownStatus'] },\n        },\n      },\n      countdownStatus: {\n        type: 'object',\n        required: ['count'],\n        properties: { count: { type: 'integer' } },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.streamTwo',\n    defs: {\n      main: {\n        type: 'subscription',\n        parameters: {\n          type: 'params',\n          required: ['countdown'],\n          properties: {\n            countdown: { type: 'integer' },\n          },\n        },\n        message: {\n          schema: {\n            type: 'union',\n            refs: ['#even', '#odd'],\n          },\n        },\n      },\n      even: {\n        type: 'object',\n        required: ['count'],\n        properties: { count: { type: 'integer' } },\n      },\n      odd: {\n        type: 'object',\n        required: ['count'],\n        properties: { count: { type: 'integer' } },\n      },\n    },\n  },\n  {\n    lexicon: 1,\n    id: 'io.example.streamAuth',\n    defs: {\n      main: {\n        type: 'subscription',\n        message: {\n          schema: { type: 'union', refs: ['#auth'] },\n        },\n      },\n      auth: {\n        type: 'object',\n        properties: {\n          credentials: { type: 'ref', ref: '#credentials' },\n          artifacts: { type: 'ref', ref: '#artifacts' },\n        },\n      },\n      credentials: {\n        type: 'object',\n        required: ['username'],\n        properties: {\n          username: { type: 'string' },\n        },\n      },\n      artifacts: {\n        type: 'object',\n        required: ['original'],\n        properties: {\n          original: { type: 'string' },\n        },\n      },\n    },\n  },\n] as const satisfies LexiconDoc[]\n\nconst handlers = {\n  'io.example.streamOne': async function* ({\n    params,\n  }: xrpcServer.StreamContext) {\n    const countdown = Number(params.countdown ?? 0)\n    for (let i = countdown; i >= 0; i--) {\n      await wait(0)\n      yield { $type: 'io.example.streamOne#countdownStatus', count: i }\n    }\n  },\n  'io.example.streamTwo': async function* ({\n    params,\n  }: xrpcServer.StreamContext) {\n    const countdown = Number(params.countdown ?? 0)\n    for (let i = countdown; i >= 0; i--) {\n      await wait(200)\n      yield {\n        $type: `io.example.streamTwo${i % 2 === 0 ? '#even' : '#odd'}`,\n        count: i,\n      }\n    }\n    yield {\n      $type: 'io.example.otherNsid#done',\n    }\n  },\n  'io.example.streamAuth': {\n    auth: createBasicAuth({ username: 'admin', password: 'password' }),\n    handler: async function* ({ auth }) {\n      yield { ...auth, $type: 'io.example.streamAuth#auth' }\n    },\n  },\n}\n\nfor (const buildServer of [buildMethodLexicons, buildAddLexicons]) {\n  describe(buildServer, () => {\n    // @NOTE we need to clone because \"new Lexicons\" will mutate the lexicon\n    // definitions\n    const lex = new Lexicons(structuredClone(LEXICONS))\n\n    let server: xrpcServer.Server\n    let s: http.Server\n    let port: number\n    beforeAll(async () => {\n      server = await buildServer(LEXICONS, handlers)\n      s = await createServer(server)\n      port = (s.address() as AddressInfo).port\n    })\n    afterAll(async () => {\n      if (s) await closeServer(s)\n    })\n\n    it('streams messages', async () => {\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.streamOne?countdown=5`,\n      )\n\n      const frames: Frame[] = []\n      for await (const frame of byFrame(ws)) {\n        frames.push(frame)\n      }\n\n      expect(frames).toEqual([\n        new MessageFrame({ count: 5 }, { type: '#countdownStatus' }),\n        new MessageFrame({ count: 4 }, { type: '#countdownStatus' }),\n        new MessageFrame({ count: 3 }, { type: '#countdownStatus' }),\n        new MessageFrame({ count: 2 }, { type: '#countdownStatus' }),\n        new MessageFrame({ count: 1 }, { type: '#countdownStatus' }),\n        new MessageFrame({ count: 0 }, { type: '#countdownStatus' }),\n      ])\n    })\n\n    it('streams messages in a union', async () => {\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.streamTwo?countdown=5`,\n      )\n\n      const frames: Frame[] = []\n      for await (const frame of byFrame(ws)) {\n        frames.push(frame)\n      }\n\n      expect(frames).toEqual([\n        new MessageFrame({ count: 5 }, { type: '#odd' }),\n        new MessageFrame({ count: 4 }, { type: '#even' }),\n        new MessageFrame({ count: 3 }, { type: '#odd' }),\n        new MessageFrame({ count: 2 }, { type: '#even' }),\n        new MessageFrame({ count: 1 }, { type: '#odd' }),\n        new MessageFrame({ count: 0 }, { type: '#even' }),\n        new MessageFrame({}, { type: 'io.example.otherNsid#done' }),\n      ])\n    })\n\n    it('resolves auth into handler', async () => {\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.streamAuth`,\n        {\n          headers: basicAuthHeaders({\n            username: 'admin',\n            password: 'password',\n          }),\n        },\n      )\n\n      const frames: Frame[] = []\n      for await (const frame of byFrame(ws)) {\n        frames.push(frame)\n      }\n\n      expect(frames).toEqual([\n        new MessageFrame(\n          {\n            credentials: {\n              username: 'admin',\n            },\n            artifacts: {\n              original: 'YWRtaW46cGFzc3dvcmQ=',\n            },\n          },\n          {\n            type: '#auth',\n          },\n        ),\n      ])\n    })\n\n    it('errors immediately on bad parameter', async () => {\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.streamOne`,\n      )\n\n      const frames: Frame[] = []\n      for await (const frame of byFrame(ws)) {\n        frames.push(frame)\n      }\n\n      expect(frames).toEqual([\n        expect.objectContaining({\n          body: expect.objectContaining({\n            error: 'InvalidRequest',\n            message: expect.stringContaining('countdown'),\n          }),\n        }),\n      ])\n    })\n\n    it('errors immediately on bad auth', async () => {\n      const ws = new WebSocket(\n        `ws://localhost:${port}/xrpc/io.example.streamAuth`,\n        {\n          headers: basicAuthHeaders({\n            username: 'bad',\n            password: 'wrong',\n          }),\n        },\n      )\n\n      const frames: Frame[] = []\n      for await (const frame of byFrame(ws)) {\n        frames.push(frame)\n      }\n\n      expect(frames).toEqual([\n        new ErrorFrame({\n          error: 'AuthenticationRequired',\n          message: 'Authentication Required',\n        }),\n      ])\n    })\n\n    it('does not websocket upgrade at bad endpoint', async () => {\n      const ws = new WebSocket(`ws://localhost:${port}/xrpc/does.not.exist`)\n      const drainStream = async () => {\n        for await (const bytes of createWebSocketStream(ws)) {\n          bytes // drain\n        }\n      }\n      await expect(drainStream).rejects.toHaveProperty('code', 'ECONNRESET')\n    })\n\n    describe('Subscription consumer', () => {\n      it('receives messages w/ skips', async () => {\n        const sub = new Subscription({\n          service: `ws://localhost:${port}`,\n          method: 'io.example.streamOne',\n          getParams: () => ({ countdown: 5 }),\n          validate: (obj) => {\n            const result = lex.assertValidXrpcMessage<{ count: number }>(\n              'io.example.streamOne',\n              obj,\n            )\n            if (!result.count || result.count % 2) {\n              return result\n            }\n          },\n        })\n\n        const messages: { count: number }[] = []\n        for await (const msg of sub) {\n          messages.push(msg)\n        }\n\n        expect(messages).toEqual([\n          { $type: 'io.example.streamOne#countdownStatus', count: 5 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 3 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 1 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 0 },\n        ])\n      })\n\n      it('reconnects w/ param update', async () => {\n        let countdown = 10\n        let reconnects = 0\n        const sub = new Subscription({\n          service: `ws://localhost:${port}`,\n          method: 'io.example.streamOne',\n          onReconnectError: () => reconnects++,\n          getParams: () => ({ countdown }),\n          validate: (obj) => {\n            return lex.assertValidXrpcMessage<{ count: number }>(\n              'io.example.streamOne',\n              obj,\n            )\n          },\n        })\n\n        let disconnected = false\n        for await (const msg of sub) {\n          expect(msg.count).toBeGreaterThanOrEqual(countdown - 1) // No skips\n          countdown = Math.min(countdown, msg.count) // Only allow forward movement\n          if (msg.count <= 6 && !disconnected) {\n            disconnected = true\n            server.subscriptions.forEach(({ wss }) => {\n              wss.clients.forEach((c) => c.terminate())\n            })\n          }\n        }\n\n        expect(countdown).toEqual(0)\n        expect(reconnects).toBeGreaterThan(0)\n      })\n\n      it('aborts with signal', async () => {\n        const abortController = new AbortController()\n        const sub = new Subscription({\n          service: `ws://localhost:${port}`,\n          method: 'io.example.streamOne',\n          signal: abortController.signal,\n          getParams: () => ({ countdown: 10 }),\n          validate: (obj) => {\n            const result = lex.assertValidXrpcMessage<{ count: number }>(\n              'io.example.streamOne',\n              obj,\n            )\n            return result\n          },\n        })\n\n        let error\n        let disconnected = false\n        const messages: { count: number }[] = []\n        try {\n          for await (const msg of sub) {\n            messages.push(msg)\n            if (msg.count <= 6 && !disconnected) {\n              disconnected = true\n              abortController.abort(new Error('Oops!'))\n            }\n          }\n        } catch (err) {\n          error = err\n        }\n\n        expect(error).toEqual(new Error('Oops!'))\n        expect(messages).toEqual([\n          { $type: 'io.example.streamOne#countdownStatus', count: 10 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 9 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 8 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 7 },\n          { $type: 'io.example.streamOne#countdownStatus', count: 6 },\n        ])\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "packages/xrpc-server/tsconfig.build.json",
    "content": "{\n  \"extends\": \"../../tsconfig/node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "packages/xrpc-server/tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.build.json\" },\n    { \"path\": \"./tsconfig.tests.json\" }\n  ]\n}\n"
  },
  {
    "path": "packages/xrpc-server/tsconfig.tests.json",
    "content": "{\n  \"extends\": \"../../tsconfig/tests.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  },\n  \"include\": [\"./tests\"]\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'services/*'\n  - 'packages/*'\n  - 'packages/lex/*'\n  - 'packages/oauth/*'\n  - 'packages/internal/*'\n"
  },
  {
    "path": "services/bsky/Dockerfile",
    "content": "FROM node:20.20-alpine AS build\n\nRUN corepack enable\n\nWORKDIR /app\n\nCOPY ./package.json ./\nRUN corepack prepare --activate\n\n# Move files into the image and install\nCOPY ./*.* ./\nCOPY ./tsconfig ./tsconfig\nCOPY ./lexicons ./lexicons\nCOPY ./packages/internal ./packages/internal\nCOPY ./packages/lex ./packages/lex\n\n# NOTE bsky's transitive dependencies go here: if that changes, this needs to be updated.\n# pnpm list --filter '@atproto/bsky...' --depth 0 --json | jq '.[].path' | grep -v \"/lex/\" | grep -v \"/internal/\" | sort -u\nCOPY ./packages/api ./packages/api\nCOPY ./packages/aws ./packages/aws\nCOPY ./packages/bsky ./packages/bsky\nCOPY ./packages/common-web ./packages/common-web\nCOPY ./packages/common ./packages/common\nCOPY ./packages/crypto ./packages/crypto\nCOPY ./packages/did ./packages/did\nCOPY ./packages/identity ./packages/identity\nCOPY ./packages/lex-cli ./packages/lex-cli\nCOPY ./packages/lexicon ./packages/lexicon\nCOPY ./packages/oauth/jwk-jose ./packages/oauth/jwk-jose\nCOPY ./packages/oauth/jwk-webcrypto ./packages/oauth/jwk-webcrypto\nCOPY ./packages/oauth/jwk ./packages/oauth/jwk\nCOPY ./packages/oauth/oauth-client-browser-example ./packages/oauth/oauth-client-browser-example\nCOPY ./packages/oauth/oauth-client-browser ./packages/oauth/oauth-client-browser\nCOPY ./packages/oauth/oauth-client ./packages/oauth/oauth-client\nCOPY ./packages/oauth/oauth-provider-api ./packages/oauth/oauth-provider-api\nCOPY ./packages/oauth/oauth-provider-frontend ./packages/oauth/oauth-provider-frontend\nCOPY ./packages/oauth/oauth-provider-ui ./packages/oauth/oauth-provider-ui\nCOPY ./packages/oauth/oauth-provider ./packages/oauth/oauth-provider\nCOPY ./packages/oauth/oauth-scopes ./packages/oauth/oauth-scopes\nCOPY ./packages/oauth/oauth-types ./packages/oauth/oauth-types\nCOPY ./packages/pds ./packages/pds\nCOPY ./packages/repo ./packages/repo\nCOPY ./packages/sync ./packages/sync\nCOPY ./packages/syntax ./packages/syntax\nCOPY ./packages/ws-client ./packages/ws-client\nCOPY ./packages/xrpc-server ./packages/xrpc-server\nCOPY ./packages/xrpc ./packages/xrpc\n\nCOPY ./services/bsky ./services/bsky\n\n# install all deps\nRUN PUPPETEER_SKIP_DOWNLOAD=true pnpm install --frozen-lockfile\n# build all packages with external node_modules\nRUN pnpm run --recursive --stream --filter '@atproto/crypto...' build\nRUN pnpm run --recursive --stream --filter '@atproto/bsky...' build\n# install only prod deps, hoisted to root node_modules dir\nRUN pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false\n\nWORKDIR services/bsky\n\n# Uses assets from build stage to reduce build size\nFROM node:20.20-alpine\n\n# dumb-init is used to handle signals properly.\n# runit is installed so it can be (optionally) used for logging via svlogd.\nRUN apk add --update dumb-init runit\n\n\n# Avoid zombie processes, handle signal forwarding\nENTRYPOINT [\"dumb-init\", \"--\"]\n\nWORKDIR /app/services/bsky\nCOPY --from=build /app /app\n\nEXPOSE 3000\nENV PORT=3000\nENV NODE_ENV=production\n\n# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user\nUSER node\nCMD [\"node\", \"--heapsnapshot-signal=SIGUSR2\", \"--enable-source-maps\", \"api.js\"]\n\nLABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto\nLABEL org.opencontainers.image.description=\"Bsky App View\"\nLABEL org.opencontainers.image.licenses=MIT\n"
  },
  {
    "path": "services/bsky/README.md",
    "content": "# bsky appview service\n\nThis is the service entrypoint for the bsky appview. The entrypoint command should run `api.js` with node, e.g. `node api.js`. The following env vars are supported:\n\n- `BSKY_PUBLIC_URL` - (required) the public url of the appview, e.g. `https://api.bsky.app`.\n- `BSKY_DID_PLC_URL` - (required) the url of the PLC service used for looking up did documents, e.g. `https://plc.directory`.\n- `BSKY_DATAPLANE_URL` - (required) the url where the backing dataplane service lives.\n- `BSKY_SERVICE_SIGNING_KEY` - (required) the public signing key in the form of a `did:key`, used for service-to-service auth. Advertised in the appview's `did:web` document.\n- `BSKY_ADMIN_PASSWORDS` - (alt. `BSKY_ADMIN_PASSWORD`) (required) comma-separated list of admin passwords used for role-based auth.\n- `NODE_ENV` - (recommended) for production usage, should be set to `production`. Otherwise all responses are validated on their way out. There may be other effects of not setting this to `production`, as dependencies may also implement debug modes based on its value.\n- `BSKY_VERSION` - (recommended) version of the bsky service. This is advertised by the health endpoint.\n- `BSKY_PORT` - (recommended) the port that the service will run on.\n- `BSKY_IMG_URI_ENDPOINT` - (recommended) the base url for resized images, e.g. `https://cdn.bsky.app/img`. When not set, sets-up an image resizing service directly on the appview.\n- `BSKY_SERVER_DID` - (recommended) the did of the appview service. When this is a `did:web` that matches the appview's public url, a `did:web` document is served.\n- `BSKY_HANDLE_RESOLVE_NAMESERVERS` - alternative domain name servers used for handle resolution, comma-separated.\n- `BSKY_BLOB_CACHE_LOC` - when `BSKY_IMG_URI_ENDPOINT` is not set, this determines where resized blobs are cached by the image resizing service.\n- `BSKY_COURIER_URL` - URL of courier service.\n- `BSKY_COURIER_API_KEY` - API key for courier service.\n- `BSKY_BSYNC_URL` - URL of bsync service.\n- `BSKY_BSYNC_API_KEY` - API key for bsync service.\n- `BSKY_SEARCH_URL` - (alt. `BSKY_SEARCH_ENDPOINT`) -\n- `BSKY_LABELS_FROM_ISSUER_DIDS` - comma-separated list of labelers to always use for record labels.\n- `MOD_SERVICE_DID` - the DID of the mod service, used to receive service authed requests.\n"
  },
  {
    "path": "services/bsky/api.js",
    "content": "/* eslint-env node */\n/* eslint-disable import/order */\n\n'use strict'\n\nconst dd = require('dd-trace')\n\ndd.tracer\n  .init()\n  .use('http2', {\n    client: true, // calls into dataplane\n    server: false,\n  })\n  .use('express', {\n    hooks: {\n      request: (span, req) => {\n        maintainXrpcResource(span, req)\n      },\n    },\n  })\n\n// modify tracer in order to track calls to dataplane as a service with proper resource names\nconst DATAPLANE_PREFIX = '/bsky.Service/'\nconst origStartSpan = dd.tracer._tracer.startSpan\ndd.tracer._tracer.startSpan = function (name, options) {\n  if (\n    name !== 'http.request' ||\n    options?.tags?.component !== 'http2' ||\n    !options?.tags?.['http.url']\n  ) {\n    return origStartSpan.call(this, name, options)\n  }\n  const uri = new URL(options.tags['http.url'])\n  if (!uri.pathname.startsWith(DATAPLANE_PREFIX)) {\n    return origStartSpan.call(this, name, options)\n  }\n  options.tags['service.name'] = 'dataplane-bsky'\n  options.tags['resource.name'] = uri.pathname.slice(DATAPLANE_PREFIX.length)\n  return origStartSpan.call(this, name, options)\n}\n\n// Tracer code above must come before anything else\nconst assert = require('node:assert')\nconst cluster = require('node:cluster')\nconst path = require('node:path')\n\nconst { BskyAppView, ServerConfig } = require('@atproto/bsky')\nconst { Secp256k1Keypair } = require('@atproto/crypto')\n\nconst main = async () => {\n  const env = getEnv()\n  const config = ServerConfig.readEnv()\n  assert(env.serviceSigningKey, 'must set BSKY_SERVICE_SIGNING_KEY')\n  const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey)\n  const bsky = BskyAppView.create({ config, signingKey })\n  await bsky.start()\n  // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)\n  const shutdown = async () => {\n    await bsky.destroy()\n  }\n  process.on('SIGTERM', shutdown)\n  process.on('disconnect', shutdown) // when clustering\n}\n\nconst getEnv = () => ({\n  serviceSigningKey: process.env.BSKY_SERVICE_SIGNING_KEY || undefined,\n})\n\nconst maybeParseInt = (str) => {\n  if (!str) return\n  const int = parseInt(str, 10)\n  if (isNaN(int)) return\n  return int\n}\n\nconst maintainXrpcResource = (span, req) => {\n  // Show actual xrpc method as resource rather than the route pattern\n  if (span && req.originalUrl?.startsWith('/xrpc/')) {\n    span.setTag(\n      'resource.name',\n      [\n        req.method,\n        path.posix.join(req.baseUrl || '', req.path || '', '/').slice(0, -1), // Ensures no trailing slash\n      ]\n        .filter(Boolean)\n        .join(' '),\n    )\n  }\n}\n\nconst workerCount = maybeParseInt(process.env.CLUSTER_WORKER_COUNT)\n\nif (workerCount) {\n  if (cluster.isPrimary) {\n    console.log(`primary ${process.pid} is running`)\n    const workers = new Set()\n    for (let i = 0; i < workerCount; ++i) {\n      workers.add(cluster.fork())\n    }\n    let teardown = false\n    cluster.on('exit', (worker) => {\n      workers.delete(worker)\n      if (!teardown) {\n        workers.add(cluster.fork()) // restart on crash\n      }\n    })\n    process.on('SIGTERM', () => {\n      teardown = true\n      console.log('disconnecting workers')\n      workers.forEach((w) => w.disconnect())\n    })\n  } else {\n    console.log(`worker ${process.pid} is running`)\n    main()\n  }\n} else {\n  main() // non-clustering\n}\n"
  },
  {
    "path": "services/bsky/package.json",
    "content": "{\n  \"name\": \"bsky-app-view-service\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@8.15.9\",\n  \"dependencies\": {\n    \"@atproto/bsky\": \"workspace:^\",\n    \"@atproto/crypto\": \"workspace:^\",\n    \"dd-trace\": \"3.13.2\"\n  }\n}\n"
  },
  {
    "path": "services/bsync/Dockerfile",
    "content": "FROM node:18-alpine AS build\n\nRUN corepack enable\n\nWORKDIR /app\n\nCOPY ./package.json ./\nRUN corepack prepare --activate\n\n# Move files into the image and install\nCOPY ./*.* ./\nCOPY ./tsconfig ./tsconfig\n\n# NOTE bsync's transitive dependencies go here: if that changes, this needs to be updated.\n# pnpm list --filter '@atproto/bsync...' --depth 0 --json | jq '.[].path' | sort -u\nCOPY ./packages/bsync ./packages/bsync\nCOPY ./packages/common-web ./packages/common-web\nCOPY ./packages/common ./packages/common\nCOPY ./packages/lex/lex-cbor ./packages/lex/lex-cbor\nCOPY ./packages/lex/lex-data ./packages/lex/lex-data\nCOPY ./packages/lex/lex-json ./packages/lex/lex-json\nCOPY ./packages/syntax ./packages/syntax\n\nCOPY ./services/bsync ./services/bsync\n\n# install all deps\nRUN PUPPETEER_SKIP_DOWNLOAD=true pnpm install --frozen-lockfile\n# build all packages with external node_modules\nRUN pnpm run --recursive --stream --filter '@atproto/bsync...' build\n# install only prod deps, hoisted to root node_modules dir\nRUN pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false\n\nWORKDIR services/bsync\n\n# Uses assets from build stage to reduce build size\nFROM node:18-alpine\n\nRUN apk add --update dumb-init\n\n# Avoid zombie processes, handle signal forwarding\nENTRYPOINT [\"dumb-init\", \"--\"]\n\nWORKDIR /app/services/bsync\nCOPY --from=build /app /app\n\nEXPOSE 3000\nENV BSYNC_PORT=3000\nENV NODE_ENV=production\n\n# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user\nUSER node\nCMD [\"node\", \"--enable-source-maps\", \"index.js\"]\n\nLABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto\nLABEL org.opencontainers.image.description=\"Bsync\"\nLABEL org.opencontainers.image.licenses=MIT\n"
  },
  {
    "path": "services/bsync/index.js",
    "content": "/* eslint-env node */\n\n'use strict'\n\nrequire('dd-trace') // Only works with commonjs\n  .init({ logInjection: true })\n\n// Tracer code above must come before anything else\nconst {\n  default: BsyncService,\n  envToCfg,\n  httpLogger,\n  readEnv,\n} = require('@atproto/bsync')\n\nconst main = async () => {\n  const env = readEnv()\n  const cfg = envToCfg(env)\n  const bsync = await BsyncService.create(cfg)\n  if (bsync.ctx.cfg.db.migrate) {\n    httpLogger.info('bsync db is migrating')\n    await bsync.ctx.db.migrateToLatestOrThrow()\n    httpLogger.info('bsync db migration complete')\n  }\n  await bsync.start()\n  httpLogger.info('bsync is running')\n  process.on('SIGTERM', async () => {\n    httpLogger.info('bsync is stopping')\n    await bsync.destroy()\n    httpLogger.info('bsync is stopped')\n  })\n}\n\nmain()\n"
  },
  {
    "path": "services/bsync/package.json",
    "content": "{\n  \"name\": \"bsync-service\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@8.15.9\",\n  \"dependencies\": {\n    \"@atproto/bsync\": \"workspace:^\",\n    \"dd-trace\": \"3.13.2\"\n  }\n}\n"
  },
  {
    "path": "services/ozone/Dockerfile",
    "content": "FROM node:18-alpine AS build\n\nRUN corepack enable\n\nWORKDIR /app\n\nCOPY ./package.json ./\nRUN corepack prepare --activate\n\n# Move files into the image and install\nCOPY ./*.* ./\nCOPY ./tsconfig ./tsconfig\nCOPY ./lexicons ./lexicons\nCOPY ./packages/internal ./packages/internal\nCOPY ./packages/lex ./packages/lex\n\n# NOTE ozones's transitive dependencies go here: if that changes, this needs to be updated.\n# pnpm list --filter '@atproto/aws...' --filter '@atproto/ozone...' --depth 0 --json | jq '.[].path' | grep -v \"/lex/\" | grep -v \"/internal/\" | sort -u\nCOPY ./packages/api ./packages/api\nCOPY ./packages/aws ./packages/aws\nCOPY ./packages/bsky ./packages/bsky\nCOPY ./packages/common ./packages/common\nCOPY ./packages/common-web ./packages/common-web\nCOPY ./packages/crypto ./packages/crypto\nCOPY ./packages/did ./packages/did\nCOPY ./packages/identity ./packages/identity\nCOPY ./packages/lex-cli ./packages/lex-cli\nCOPY ./packages/lexicon ./packages/lexicon\nCOPY ./packages/oauth/jwk ./packages/oauth/jwk\nCOPY ./packages/oauth/jwk-jose ./packages/oauth/jwk-jose\nCOPY ./packages/oauth/jwk-webcrypto ./packages/oauth/jwk-webcrypto\nCOPY ./packages/oauth/oauth-client ./packages/oauth/oauth-client\nCOPY ./packages/oauth/oauth-client-browser ./packages/oauth/oauth-client-browser\nCOPY ./packages/oauth/oauth-client-browser-example ./packages/oauth/oauth-client-browser-example\nCOPY ./packages/oauth/oauth-provider ./packages/oauth/oauth-provider\nCOPY ./packages/oauth/oauth-provider-api ./packages/oauth/oauth-provider-api\nCOPY ./packages/oauth/oauth-provider-frontend ./packages/oauth/oauth-provider-frontend\nCOPY ./packages/oauth/oauth-provider-ui ./packages/oauth/oauth-provider-ui\nCOPY ./packages/oauth/oauth-scopes ./packages/oauth/oauth-scopes\nCOPY ./packages/oauth/oauth-types ./packages/oauth/oauth-types\nCOPY ./packages/ozone ./packages/ozone\nCOPY ./packages/pds ./packages/pds\nCOPY ./packages/repo ./packages/repo\nCOPY ./packages/sync ./packages/sync\nCOPY ./packages/syntax ./packages/syntax\nCOPY ./packages/ws-client ./packages/ws-client\nCOPY ./packages/xrpc ./packages/xrpc\nCOPY ./packages/xrpc-server ./packages/xrpc-server\n\nCOPY ./services/ozone ./services/ozone\n\n# install all deps\nRUN PUPPETEER_SKIP_DOWNLOAD=true pnpm install --frozen-lockfile\n# build all packages with external node_modules\nRUN pnpm run --recursive --stream --filter '@atproto/aws...' --filter '@atproto/ozone...' build\n# install only prod deps, hoisted to root node_modules dir\nRUN pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false\n\nWORKDIR services/ozone\n\n# Uses assets from build stage to reduce build size\nFROM node:18-alpine\n\nRUN apk add --update dumb-init\n\n# Avoid zombie processes, handle signal forwarding\nENTRYPOINT [\"dumb-init\", \"--\"]\n\nWORKDIR /app/services/ozone\nCOPY --from=build /app /app\n\nEXPOSE 3000\nENV PORT=3000\nENV NODE_ENV=production\n\n# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user\nUSER node\nCMD [\"node\", \"--enable-source-maps\", \"api.js\"]\n\nLABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto\nLABEL org.opencontainers.image.description=\"Ozone\"\nLABEL org.opencontainers.image.licenses=MIT\n"
  },
  {
    "path": "services/ozone/api.js",
    "content": "/* eslint-env node */\n\n'use strict'\n\nrequire('dd-trace') // Only works with commonjs\n  .init({ logInjection: true })\n  .tracer.use('express', {\n    hooks: {\n      request: (span, req) => {\n        maintainXrpcResource(span, req)\n      },\n    },\n  })\n\n// Tracer code above must come before anything else\nconst path = require('node:path')\nconst {\n  BunnyInvalidator,\n  CloudfrontInvalidator,\n  MultiImageInvalidator,\n} = require('@atproto/aws')\nconst {\n  Database,\n  OzoneService,\n  envToCfg,\n  envToSecrets,\n  httpLogger,\n  readEnv,\n} = require('@atproto/ozone')\n\nconst main = async () => {\n  const env = readEnv()\n  const cfg = envToCfg(env)\n  const secrets = envToSecrets(env)\n\n  // configure zero, one, or more image invalidators\n  const imgUriEndpoint = process.env.OZONE_IMG_URI_ENDPOINT\n  const bunnyAccessKey = process.env.OZONE_BUNNY_ACCESS_KEY\n  const cfDistributionId = process.env.OZONE_CF_DISTRIBUTION_ID\n\n  const imgInvalidators = []\n\n  if (bunnyAccessKey) {\n    imgInvalidators.push(\n      new BunnyInvalidator({\n        accessKey: bunnyAccessKey,\n        urlPrefix: imgUriEndpoint,\n      }),\n    )\n  }\n\n  if (cfDistributionId) {\n    imgInvalidators.push(\n      new CloudfrontInvalidator({\n        distributionId: cfDistributionId,\n        pathPrefix: imgUriEndpoint && new URL(imgUriEndpoint).pathname,\n      }),\n    )\n  }\n\n  const imgInvalidator =\n    imgInvalidators.length > 1\n      ? new MultiImageInvalidator(imgInvalidators)\n      : imgInvalidators[0]\n\n  const migrate = process.env.OZONE_DB_MIGRATE === '1'\n  if (migrate) {\n    const db = new Database({\n      url: cfg.db.postgresUrl,\n      schema: cfg.db.postgresSchema,\n    })\n    await db.migrateToLatestOrThrow()\n    await db.close()\n  }\n\n  const ozone = await OzoneService.create(cfg, secrets, { imgInvalidator })\n\n  await ozone.start()\n\n  httpLogger.info('ozone is running')\n\n  // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)\n  process.on('SIGTERM', async () => {\n    httpLogger.info('ozone is stopping')\n\n    await ozone.destroy()\n\n    httpLogger.info('ozone is stopped')\n  })\n}\n\nconst maintainXrpcResource = (span, req) => {\n  // Show actual xrpc method as resource rather than the route pattern\n  if (span && req.originalUrl?.startsWith('/xrpc/')) {\n    span.setTag(\n      'resource.name',\n      [\n        req.method,\n        path.posix.join(req.baseUrl || '', req.path || '', '/').slice(0, -1), // Ensures no trailing slash\n      ]\n        .filter(Boolean)\n        .join(' '),\n    )\n  }\n}\n\nmain()\n"
  },
  {
    "path": "services/ozone/daemon.js",
    "content": "/* eslint-env node */\n\n'use strict'\n\nrequire('dd-trace/init') // Only works with commonjs\n\n// Tracer code above must come before anything else\nconst {\n  OzoneDaemon,\n  envToCfg,\n  envToSecrets,\n  readEnv,\n} = require('@atproto/ozone')\n\nconst main = async () => {\n  const env = readEnv()\n  const cfg = envToCfg(env)\n  const secrets = envToSecrets(env)\n  const daemon = await OzoneDaemon.create(cfg, secrets)\n\n  await daemon.start()\n  process.on('SIGTERM', async () => {\n    await daemon.destroy()\n  })\n}\n\nmain()\n"
  },
  {
    "path": "services/ozone/package.json",
    "content": "{\n  \"name\": \"ozone-service\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@8.15.9\",\n  \"dependencies\": {\n    \"@atproto/aws\": \"workspace:^\",\n    \"@atproto/ozone\": \"workspace:^\",\n    \"dd-trace\": \"3.13.2\"\n  }\n}\n"
  },
  {
    "path": "services/pds/.gitignore",
    "content": "data/*\nblobs/*\n"
  },
  {
    "path": "services/pds/Dockerfile",
    "content": "# NOTE there is an additional build stage below that should match\nFROM node:20.20-alpine3.23 AS build\n\nRUN corepack enable\n\nWORKDIR /app\n\nCOPY ./package.json ./\nRUN corepack prepare --activate\n\n# Move files into the image and install\nCOPY ./*.* ./\nCOPY ./tsconfig ./tsconfig\nCOPY ./lexicons ./lexicons\nCOPY ./packages/internal ./packages/internal\nCOPY ./packages/lex ./packages/lex\n\n# NOTE pds's transitive dependencies go here: if that changes, this needs to be updated.\n# pnpm list --filter '@atproto/pds...' --depth 0 --json | jq '.[].path' | grep -v \"/lex/\" | grep -v \"/internal/\" | sort -u\nCOPY ./packages/api ./packages/api\nCOPY ./packages/aws ./packages/aws\nCOPY ./packages/bsky ./packages/bsky\nCOPY ./packages/common-web ./packages/common-web\nCOPY ./packages/common ./packages/common\nCOPY ./packages/crypto ./packages/crypto\nCOPY ./packages/did ./packages/did\nCOPY ./packages/identity ./packages/identity\nCOPY ./packages/lex-cli ./packages/lex-cli\nCOPY ./packages/lexicon ./packages/lexicon\nCOPY ./packages/oauth/jwk-jose ./packages/oauth/jwk-jose\nCOPY ./packages/oauth/jwk-webcrypto ./packages/oauth/jwk-webcrypto\nCOPY ./packages/oauth/jwk ./packages/oauth/jwk\nCOPY ./packages/oauth/oauth-client-browser-example ./packages/oauth/oauth-client-browser-example\nCOPY ./packages/oauth/oauth-client-browser ./packages/oauth/oauth-client-browser\nCOPY ./packages/oauth/oauth-client ./packages/oauth/oauth-client\nCOPY ./packages/oauth/oauth-provider-api ./packages/oauth/oauth-provider-api\nCOPY ./packages/oauth/oauth-provider-frontend ./packages/oauth/oauth-provider-frontend\nCOPY ./packages/oauth/oauth-provider-ui ./packages/oauth/oauth-provider-ui\nCOPY ./packages/oauth/oauth-provider ./packages/oauth/oauth-provider\nCOPY ./packages/oauth/oauth-scopes ./packages/oauth/oauth-scopes\nCOPY ./packages/oauth/oauth-types ./packages/oauth/oauth-types\nCOPY ./packages/pds ./packages/pds\nCOPY ./packages/repo ./packages/repo\nCOPY ./packages/sync ./packages/sync\nCOPY ./packages/syntax ./packages/syntax\nCOPY ./packages/ws-client ./packages/ws-client\nCOPY ./packages/xrpc-server ./packages/xrpc-server\nCOPY ./packages/xrpc ./packages/xrpc\n\nCOPY ./services/pds ./services/pds\n\n# install all deps\nRUN PUPPETEER_SKIP_DOWNLOAD=true pnpm install --frozen-lockfile\n# build all packages with external node_modules\nRUN pnpm run --recursive --stream --filter '@atproto/pds...' build\n# install only prod deps, hoisted to root node_modules dir\nRUN pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false\n\nWORKDIR services/pds\n\n# Uses assets from build stage to reduce build size\nFROM node:20.20-alpine3.23\n\nRUN apk add --update dumb-init\n\n# Avoid zombie processes, handle signal forwarding\nENTRYPOINT [\"dumb-init\", \"--\"]\n\nWORKDIR /app/services/pds\nCOPY --from=build /app /app\nRUN mkdir /app/data && chown node /app/data\n\nVOLUME /app/data\nEXPOSE 3000\nENV PDS_PORT=3000\nENV NODE_ENV=production\n# potential perf issues w/ io_uring on this version of node\nENV UV_USE_IO_URING=0\n\n# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user\nUSER node\nCMD [\"node\", \"--heapsnapshot-signal=SIGUSR2\", \"--enable-source-maps\", \"--require=./tracer.js\", \"index.js\"]\n\nLABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto\nLABEL org.opencontainers.image.description=\"ATP Personal Data Server (PDS)\"\nLABEL org.opencontainers.image.licenses=MIT\n"
  },
  {
    "path": "services/pds/index.js",
    "content": "/* eslint-env node */\n\n'use strict'\n\nconst {\n  PDS,\n  envToCfg,\n  envToSecrets,\n  httpLogger,\n  readEnv,\n} = require('@atproto/pds')\nconst pkg = require('@atproto/pds/package.json')\n\nconst main = async () => {\n  const env = readEnv()\n  env.version ??= pkg.version\n  const cfg = envToCfg(env)\n  const secrets = envToSecrets(env)\n  const pds = await PDS.create(cfg, secrets)\n\n  await pds.start()\n\n  httpLogger.info('pds is running')\n  // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)\n  process.on('SIGTERM', async () => {\n    httpLogger.info('pds is stopping')\n    await pds.destroy()\n    httpLogger.info('pds is stopped')\n  })\n}\n\nmain()\n"
  },
  {
    "path": "services/pds/package.json",
    "content": "{\n  \"name\": \"plc-service\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@8.15.9\",\n  \"dependencies\": {\n    \"@atproto/pds\": \"workspace:^\",\n    \"@opentelemetry/instrumentation\": \"^0.45.0\",\n    \"dd-trace\": \"^4.18.0\",\n    \"opentelemetry-plugin-better-sqlite3\": \"^1.1.0\"\n  }\n}\n"
  },
  {
    "path": "services/pds/run-script.js",
    "content": "/* eslint-env node */\n\n'use strict'\nconst {\n  AppContext,\n  envToCfg,\n  envToSecrets,\n  readEnv,\n  scripts,\n} = require('@atproto/pds')\n\nconst main = async () => {\n  const env = readEnv()\n  const cfg = envToCfg(env)\n  const secrets = envToSecrets(env)\n  const ctx = await AppContext.fromConfig(cfg, secrets)\n  const scriptName = process.argv[2]\n  const script = scripts[scriptName ?? '']\n  if (!script) {\n    throw new Error(`could not find script: ${scriptName}`)\n  }\n  await script(ctx, process.argv.slice(3))\n  console.log('DONE')\n}\n\nmain()\n"
  },
  {
    "path": "services/pds/tracer.js",
    "content": "/* eslint-env node */\n/* eslint-disable import/order */\n\n'use strict'\n\nconst { registerInstrumentations } = require('@opentelemetry/instrumentation')\nconst {\n  BetterSqlite3Instrumentation,\n} = require('opentelemetry-plugin-better-sqlite3')\nconst { TracerProvider } = require('dd-trace') // Only works with commonjs\n  .init({ logInjection: true })\n  .use('express', {\n    hooks: { request: maintainXrpcResource },\n  })\n\nconst tracer = new TracerProvider()\ntracer.register()\n\nregisterInstrumentations({\n  tracerProvider: tracer,\n  instrumentations: [new BetterSqlite3Instrumentation()],\n})\n\nconst path = require('node:path')\n\nfunction maintainXrpcResource(span, req) {\n  // Show actual xrpc method as resource rather than the route pattern\n  if (span && req.originalUrl?.startsWith('/xrpc/')) {\n    span.setTag(\n      'resource.name',\n      [\n        req.method,\n        path.posix.join(req.baseUrl || '', req.path || '', '/').slice(0, -1), // Ensures no trailing slash\n      ]\n        .filter(Boolean)\n        .join(' '),\n    )\n  }\n}\n"
  },
  {
    "path": "tsconfig/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"checkJs\": true,\n\n    \"strict\": true,\n    \"allowUnusedLabels\": false,\n    \"allowUnreachableCode\": false,\n    \"exactOptionalPropertyTypes\": false,\n    \"noFallthroughCasesInSwitch\": false,\n    \"noImplicitAny\": false,\n    \"noImplicitReturns\": false,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": false,\n\n    \"skipLibCheck\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"preserveSymlinks\": false,\n    \"useDefineForClassFields\": true,\n\n    \"lib\": [],\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"types\": [],\n\n    \"noErrorTruncation\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"inlineSources\": true,\n    \"sourceMap\": true,\n    \"jsx\": \"preserve\",\n    \"module\": \"CommonJS\",\n    \"target\": \"ES2020\"\n  },\n  \"typeAcquisition\": {\n    \"enable\": false\n  }\n}\n"
  },
  {
    "path": "tsconfig/browser.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\", \"ESNext.Disposable\"],\n    \"jsx\": \"react-jsx\"\n  }\n}\n"
  },
  {
    "path": "tsconfig/bundler.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "tsconfig/expo.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": [\"./browser.json\"],\n  \"compilerOptions\": {\n    \"jsx\": \"react-native\",\n    \"target\": \"ES2023\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"moduleDetection\": \"force\",\n    \"esModuleInterop\": true,\n\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tsconfig/isomorphic.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    // Currently, there is not ideal way of developing a lib that is compatible\n    // with both node and the browser.\n    //\n    // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1685\n    // https://github.com/microsoft/TypeScript/issues/31535\n    // https://github.com/microsoft/TypeScript/issues/41727\n    \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "tsconfig/node.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2023\", \"ScriptHost\"],\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "tsconfig/nodenext.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2023\", \"ScriptHost\"],\n    \"types\": [\"node\"],\n    \"module\": \"Node16\",\n    \"target\": \"ES2023\",\n    \"moduleResolution\": \"Node16\"\n  }\n}\n"
  },
  {
    "path": "tsconfig/tests.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./node.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"jest\"],\n    \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"esnext\",\n    \"skipLibCheck\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "tsconfig/vitest.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./node.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"esnext\",\n    \"skipLibCheck\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [],\n  \"references\": [\n    { \"path\": \"./packages/api\" },\n    { \"path\": \"./packages/aws\" },\n    { \"path\": \"./packages/bsky\" },\n    { \"path\": \"./packages/bsync\" },\n    { \"path\": \"./packages/common\" },\n    { \"path\": \"./packages/common-web\" },\n    { \"path\": \"./packages/crypto\" },\n    { \"path\": \"./packages/dev-env\" },\n    { \"path\": \"./packages/did\" },\n    { \"path\": \"./packages/identity\" },\n    { \"path\": \"./packages/internal/did-resolver\" },\n    { \"path\": \"./packages/internal/fetch\" },\n    { \"path\": \"./packages/internal/fetch-node\" },\n    { \"path\": \"./packages/internal/handle-resolver\" },\n    { \"path\": \"./packages/internal/handle-resolver-node\" },\n    { \"path\": \"./packages/internal/pipe\" },\n    { \"path\": \"./packages/internal/rollup-plugin-bundle-manifest\" },\n    { \"path\": \"./packages/internal/simple-store\" },\n    { \"path\": \"./packages/internal/simple-store-memory\" },\n    { \"path\": \"./packages/internal/xrpc-utils\" },\n    { \"path\": \"./packages/lex/lex\" },\n    { \"path\": \"./packages/lex/lex-builder\" },\n    { \"path\": \"./packages/lex/lex-cbor\" },\n    { \"path\": \"./packages/lex/lex-client\" },\n    { \"path\": \"./packages/lex/lex-password-session\" },\n    { \"path\": \"./packages/lex/lex-data\" },\n    { \"path\": \"./packages/lex/lex-document\" },\n    { \"path\": \"./packages/lex/lex-installer\" },\n    { \"path\": \"./packages/lex/lex-json\" },\n    { \"path\": \"./packages/lex/lex-resolver\" },\n    { \"path\": \"./packages/lex/lex-server\" },\n    { \"path\": \"./packages/lex/lex-schema\" },\n    { \"path\": \"./packages/lex-cli\" },\n    { \"path\": \"./packages/lexicon\" },\n    { \"path\": \"./packages/lexicon-resolver\" },\n    { \"path\": \"./packages/oauth/jwk\" },\n    { \"path\": \"./packages/oauth/jwk-jose\" },\n    { \"path\": \"./packages/oauth/jwk-webcrypto\" },\n    { \"path\": \"./packages/oauth/oauth-client\" },\n    { \"path\": \"./packages/oauth/oauth-client-browser\" },\n    { \"path\": \"./packages/oauth/oauth-client-browser-example\" },\n    { \"path\": \"./packages/oauth/oauth-client-expo\" },\n    { \"path\": \"./packages/oauth/oauth-client-node\" },\n    { \"path\": \"./packages/oauth/oauth-provider\" },\n    { \"path\": \"./packages/oauth/oauth-provider-api\" },\n    { \"path\": \"./packages/oauth/oauth-provider-frontend\" },\n    { \"path\": \"./packages/oauth/oauth-provider-ui\" },\n    { \"path\": \"./packages/oauth/oauth-scopes\" },\n    { \"path\": \"./packages/oauth/oauth-types\" },\n    { \"path\": \"./packages/ozone\" },\n    { \"path\": \"./packages/pds\" },\n    { \"path\": \"./packages/repo\" },\n    { \"path\": \"./packages/sync\" },\n    { \"path\": \"./packages/syntax\" },\n    { \"path\": \"./packages/tap\" },\n    { \"path\": \"./packages/ws-client\" },\n    { \"path\": \"./packages/xrpc\" },\n    { \"path\": \"./packages/xrpc-server\" }\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      provider: 'v8', // or 'istanbul'\n      exclude: [\n        '**/dist/**',\n        '**/node_modules/**',\n        '**/src/lexicons/**',\n        '**/tests/**',\n      ],\n    },\n    projects: ['packages/lex/*', 'packages/syntax', 'packages/tap'],\n  },\n})\n"
  }
]